apache / apache/tvm

[Bug][Relax] FuseTIR crashes when FuseOps creates a private function named fused for nested tuple getitem[Bug]

Open
#20,194 0 comments 0 reactions 0 assignees View on GitHub
needs-triage type: bug
Dominant language
Python
Stars
13.7k
Forks
4k
Avg merge
2d 1h
Merged PRs (30d)
112

Description

### Expected behavior

The CPU target-specific Relax pipeline should compile a well-formed program that selects a tensor through a nested tuple boundary.

If FuseOps creates a private Relax function for tuple/getitem selection, FuseTIR should either lower it correctly or skip it if it is not a valid TIR fusion candidate. It should not abort because the
generated private function name is exactly fused.

### Actual behavior

The input module is well-formed. The generic default Relax pipeline builds and runs correctly.

However, the official LLVM target-default pipeline:

relax.get_default_pipeline(tvm.target.Target("llvm"))

fails in FuseTIR with:

Check failed: (func_info_.global_name != "fused") is false:

A pass-by-pass trace shows that FuseOps succeeds and the IR is still well-formed, but it creates a private Relax function named exactly fused:

@R.function(private=True)
def fused(...):
R.func_attr({"Primitive": True})
...

The next pass, FuseTIR, aborts when processing this generated function.

### Environment

OS: Linux x86_64
Python: 3.10.12
TVM version: 0.26.dev1
TVM commit: 5a8dae4d95c55c8fec9246a607a28c3ff54ffe05
Target: llvm
Relax VM exec_mode: bytecode

### Steps to reproduce

import numpy as np
import tvm
from tvm import relax

def build_module(tuple_depth):
shape = (2, 3)
dtype = "float32"

bb = relax.BlockBuilder()
x = relax.Var("x", relax.TensorType(shape, dtype))

with bb.function("main", params=[x]):
with bb.dataflow():
zero_scalar = bb.emit(relax.op.zeros((1, 1), dtype))
zero = bb.emit(relax.op.broadcast_to(zero_scalar, shape))
one_scalar = bb.emit(relax.op.ones((1, 1), dtype))
one = bb.emit(relax.op.broadcast_to(one_scalar, shape))

add_zero = bb.emit(relax.op.add(x, zero))
producer = bb.emit(relax.op.multiply(add_zero, one))
dead = bb.emit(zero)

if tuple_depth == 1:
tup = bb.emit(relax.Tuple([producer, dead]))
selected = bb.emit(relax.TupleGetItem(tup, 0))
else:
inner = bb.emit(relax.Tuple([producer, dead]))
outer = bb.emit(relax.Tuple([inner, dead]))
inner_selected = bb.emit(relax.TupleGetItem(outer, 0))
selected = bb.emit(relax.TupleGetItem(inner_selected, 0))

out = bb.emit(selected)
gv = bb.emit_output(out)

bb.emit_func_output(gv)

return bb.get()

def make_input():
data = np.arange(6, dtype="float32").reshape(2, 3)
return (np.sin(data * 0.11) + 0.25).astype("float32")

def run_build(mod, pipeline):
target = tvm.target.Target("llvm")
exe = relax.build(
mod,
target=target,
relax_pipeline=pipeline,
exec_mode="bytecode",
)
vm = relax.VirtualMachine(exe, tvm.cpu())

x_np = make_input()
actual = vm["main"](tvm.runtime.tensor(x_np, tvm.cpu())).numpy()

print("match:", np.allclose(actual, x_np, rtol=1e-5, atol=1e-5))

target = tvm.target.Target("llvm")

depth1 = build_module(tuple_depth=1)
depth2 = build_module(tuple_depth=2)

print("depth1 generic/default")
run_build(depth1, "default")

print("depth1 llvm target-default")
run_build(depth1, relax.get_default_pipeline(target))

print("depth2 generic/default")
run_build(depth2, "default")

print("depth2 llvm target-default")
# This fails locally in FuseTIR:
# Check failed: (func_info_.global_name != "fused") is false:
run_build(depth2, relax.get_default_pipeline(target))

Observed result:

depth1 generic/default
match: True

depth1 llvm target-default
match: True

depth2 generic/default
match: True

depth2 llvm target-default
tvm.error.InternalError:
Check failed: (func_info_.global_name != "fused") is false:

The depth-1 case is a control and passes. The depth-2 nested tuple/getitem case passes under generic/default but fails under the target-default pipeline.

This suggests a naming/protocol mismatch between FuseOps and FuseTIR: FuseOps may generate a private function whose global name is exactly fused, while FuseTIR rejects that name internally.

### Triage

- needs-triage
- type: bug
- relax

Contributor guide

No contributing guide indexed for this repository

Research direction

Run the supplied reproducer with the LLVM target-default pipeline and compare the depth-1 and depth-2 cases. Start by tracing the Relax FuseOps and FuseTIR passes around the generated private function named fused. Done means the nested tuple/getitem case no longer aborts and produces the expected output, while the depth-1 control case continues to pass.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
compilers
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
64/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.