Expected behavior
relax.get_default_pipeline(Target("llvm")) should compile a valid Relax function whose input has symbolic height/width, or reject it with a clean error. In particular, this minimal graph — conv (stride 2) →
conv → bias add — is well-typed and builds fine with the plain default build.
Actual behavior
The CPU pipeline (the one returned by relax.get_default_pipeline, i.e. cpu_generic: LegalizeOps → AnnotateTIROpPattern → FoldConstant → FuseOps → FuseTIR → …) aborts at compile time with an internal
check while wrapping the fused function:
InternalError: Check failed: undefined.size() == 0 (2 vs. 0) :
In PrimFunc fused_conv2d1_add variables (width, height) are used,
but are not passed in as API arguments
Classification: this is a compile-time abort of the compiler itself (an internal invariant violation), not a runtime crash of generated code and not a silent miscompilation — no executable is produced. The
input is a valid Relax module that another official build path accepts (see controls), so the failure is not a legitimate rejection of invalid input.
Environment
OS: Linux x86_64
Target: llvm
TVM commit: 5a8dae4d95c55c8fec9246a607a28c3ff54ffe05 (0.26.dev1)
Steps to reproduce
import tvm
from tvm import relax
from tvm.script import ir as I, relax as R
@I.ir_module
class M:
@R.function
def main(
x: R.Tensor(("batch", 3, "height", "width"), "float32"),
w1: R.Tensor((16, 3, 3, 3), "float32"),
w2: R.Tensor((16, 16, 1, 1), "float32"),
b2: R.Tensor((16,), "float32"),
):
with R.dataflow():
c1 = R.nn.conv2d(x, w1, strides=[2, 2], padding=[1, 1, 1, 1])
c2 = R.nn.conv2d(c1, w2)
bias = R.reshape(b2, R.shape([1, 16, 1, 1]))
out = R.add(c2, bias)
R.output(out)
return out
llvm = tvm.target.Target("llvm")
exe = tvm.relax.build(M, target=llvm, exec_mode="compiled",
relax_pipeline=relax.get_default_pipeline(llvm))
Controls (each removes exactly one trigger condition)
- Trigger: symbolic H/W + conv stride 2 + fused conv+add — ICHECK abort
- First conv stride 1 (intermediate shapes stay raw
height/width) — OK
- No bias add (conv→conv is not fused into one group) — OK
- Fully static shapes — OK
- Trigger under the plain default build (
relax.build without relax_pipeline) — OK
Diagnosis
After the stride-2 conv, intermediate shapes become expressions over the input SizeVars ((height - 1) // 2 + 1). When FuseOps groups the second conv with the elementwise add, the fused PrimFunc's buffers
carry those expression extents, but height/width themselves never enter the fused function's signature (not parameters, and not recoverable via T.match_buffer of the group input, whose dims are the
expressions — not the raw vars). MakePackedAPI (src/tirx/transform/make_packed_api.cc:278) then correctly rejects the function for free variables.
For contrast, when the first conv has stride 1, the fused function's input buffer has raw height/width dims, match_buffer binds them, and the build succeeds.
Real-world impact
ONNX exports with dynamic H/W inputs (e.g. Hugging Face-hosted ResNet50 / ConvNeXt-Tiny ONNX, input (batch_size, num_channels, height, width)) fail out of the box with this pipeline:
Expected behavior
relax.get_default_pipeline(Target("llvm"))should compile a valid Relax function whose input has symbolic height/width, or reject it with a clean error. In particular, this minimal graph — conv (stride 2) →conv → bias add — is well-typed and builds fine with the plain default build.
Actual behavior
The CPU pipeline (the one returned by
relax.get_default_pipeline, i.e.cpu_generic:LegalizeOps → AnnotateTIROpPattern → FoldConstant → FuseOps → FuseTIR → …) aborts at compile time with an internalcheck while wrapping the fused function:
Classification: this is a compile-time abort of the compiler itself (an internal invariant violation), not a runtime crash of generated code and not a silent miscompilation — no executable is produced. The
input is a valid Relax module that another official build path accepts (see controls), so the failure is not a legitimate rejection of invalid input.
Environment
Steps to reproduce
Controls (each removes exactly one trigger condition)
height/width) — OKrelax.buildwithoutrelax_pipeline) — OKDiagnosis
After the stride-2 conv, intermediate shapes become expressions over the input SizeVars (
(height - 1) // 2 + 1). When FuseOps groups the second conv with the elementwise add, the fused PrimFunc's bufferscarry those expression extents, but
height/widththemselves never enter the fused function's signature (not parameters, and not recoverable viaT.match_bufferof the group input, whose dims are theexpressions — not the raw vars).
MakePackedAPI(src/tirx/transform/make_packed_api.cc:278) then correctly rejects the function for free variables.For contrast, when the first conv has stride 1, the fused function's input buffer has raw
height/widthdims,match_bufferbinds them, and the build succeeds.Real-world impact
ONNX exports with dynamic H/W inputs (e.g. Hugging Face-hosted ResNet50 / ConvNeXt-Tiny ONNX, input
(batch_size, num_channels, height, width)) fail out of the box with this pipeline: