Skip to content

[Bug][Relax] CPU default pipeline: fused PrimFunc keeps symbolic height/width unbound (MakePackedAPI ICHECK) #20238

Description

@lrcyyds1

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:

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    needs-triagePRs or issues that need to be investigated by maintainers to find the right assignees to address ittype: bug

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions