pinpinpoo opened a new issue, #20421:
URL: https://github.com/apache/tvm/issues/20421
### Expected behavior
For `x: float16[4,4]` and `ids: int32[1]`, the fused graph should compute:
```python
y = (x + np.float16(1))[ids, :]
```
Enabling fusion should preserve the selected row.
### Actual behavior
The unfused path matches an independent NumPy reference. The fused path
produces incorrect output for nonzero indices.
With an input whose rows contain float16-rounded values `0`, `1/3`, `2/3`,
and `1`, respectively, I observed:
| ids | Expected / OFF | ON |
|---|---|---|
| [0] | [1, 1, 1, 1] | [1, 1, 1, 1] |
| [2] | [1.666015625, 1.666015625, 1.666015625, 1.666015625] | [1, 1, 1, 1] |
| [3] | [2, 2, 2, 2] | [1, 1, 1, 1] |
The 4x4 case reproduced in three independent processes. Each process tested
four inputs (one row-encoded input and three random inputs), indices 0/2/3, and
five repeated executions. OFF passed all 60 checks; ON failed all 40 checks
using nonzero indices. A 1x4 control passed on both paths.
This case was reduced from an extracted official FuseTIR test graph with a
4096x4096 input. The reduction changes only shape extents and preserves
explicit `T.int64` expressions, read/write regions, the `Primitive` function
attribute, and dataflow structure.
The saved IR points to default scheduling. Immediately after `FuseTIR`, the
index is still used:
```python
T_take_intermediate[v_ax0, v_ax1] = Out_intermediate[input_ids[v_ax0], v_ax1]
```
After `ApplyDefaultSchedule`, the body no longer reads `input_ids`. The
schedule trace records `Fallback` accepting this function. The generated CUDA
kernel is:
```cpp
extern "C" __global__ void __launch_bounds__(1024) fused_func_kernel(
half* __restrict__ T_take_intermediate_ptr,
half* __restrict__ input_embeds_ptr) {
if (((int)threadIdx.x) < 16) {
T_take_intermediate_ptr[(((int)threadIdx.x) & 3)] =
input_embeds_ptr[((int)threadIdx.x)] + __float2half_rn(1.0f);
}
}
```
The kernel has no index argument and lets four different input rows write to
each output address. The output above is the observed result; the conflicting
writes mean that the selected row is not reliable.
### Environment
- TVM: `0.26.dev0+source.8f328e8`
- Commit: `8f328e802cfe5e41fcc8f5c17e7582b1c28bfce4`
- GPU: NVIDIA RTX A6000 (`sm_86`)
- NVIDIA driver: `550.120`
- CUDA compiler: `12.4.131`
- LLVM: 15, host target `{"kind": "llvm", "mcpu": "generic"}`
- Linux x86_64, kernel `6.8.0-52-generic`, glibc 2.35
- Python: 3.11.16; NumPy: 2.4.6
### Steps to reproduce
Save the following as `repro_add_take_4x4.py` and run in the TVM CUDA
environment:
```bash
for trial in 0 1 2; do
python repro_add_take_4x4.py "$trial"
done
```
OFF skips `FuseOps` and `FuseTIR`; ON runs the CUDA pipeline with both
passes enabled. The script uses the same input arrays for both paths and saves
the IR after fusion and scheduling, plus every output comparison. The explicit
integer widths and primitive-function context are retained from the failing
graph.
```python
from pathlib import Path
import json
import sys
import numpy as np
import tvm
from tvm import relax, s_tir
from tvm.relax.backend.cuda import pipeline
from tvm.script import ir as I, relax as R, tirx as T
SOURCE = r'''
@I.ir_module
class Module:
@T.prim_func(private=True, s_tir=True)
def add(A: T.Buffer((T.int64(4), T.int64(4)), 'float16'), Out:
T.Buffer((T.int64(4), T.int64(4)), 'float16')):
for i, j in T.grid(T.int64(4), T.int64(4)):
with T.sblock('add'):
vi, vj = T.axis.remap('SS', [i, j])
T.reads(A[vi, vj])
T.writes(Out[vi, vj])
Out[vi, vj] = A[vi, vj] + T.float16(1)
@T.prim_func(private=True, s_tir=True)
def take(A: T.Buffer((T.int64(4), T.int64(4)), 'float16'), B:
T.Buffer((T.int64(1),), 'int32'), T_take: T.Buffer((T.int64(1), T.int64(4)),
'float16')):
for ax0, ax1 in T.grid(T.int64(1), T.int64(4)):
with T.sblock('T_take'):
v_ax0, v_ax1 = T.axis.remap('SS', [ax0, ax1])
T.reads(A[B[v_ax0], v_ax1], B[v_ax0])
T.writes(T_take[v_ax0, v_ax1])
T_take[v_ax0, v_ax1] = A[B[v_ax0], v_ax1]
@R.function(private=True)
def fused_func(input_ids: R.Tensor((1,), dtype='int32'), input_embeds:
R.Tensor((4, 4), dtype='float16')) -> R.Tensor((1, 4), dtype='float16'):
R.func_attr({'Primitive': 1})
cls = Module
with R.dataflow():
lv = R.call_tir(cls.add, (input_embeds,), out_ty=R.Tensor((4,
4), dtype='float16'))
gv = R.call_tir(cls.take, (lv, input_ids), out_ty=R.Tensor((1,
4), dtype='float16'))
R.output(gv)
return gv
@R.function
def main(input_ids: R.Tensor((1,), dtype='int32'), input_embeds:
R.Tensor((4, 4), dtype='float16')) -> R.Tensor((1, 4), dtype='float16'):
cls = Module
with R.dataflow():
gv: R.Tensor((1, 4), dtype='float16') =
cls.fused_func(input_ids, input_embeds)
R.output(gv)
return gv
'''
def build(source, enabled, target, directory):
directory.mkdir(parents=True, exist_ok=True)
mod = tvm.script.from_source(source, extra_vars=dict(I=I, R=R, T=T),
s_tir=True)
mod = relax.transform.ConvertToDataflow()(mod)
with target, tvm.transform.PassContext(opt_level=3):
steps = pipeline.library_dispatch_passes(target) +
pipeline.legalize_passes(target)
for i, transform in enumerate(steps):
name = str(transform.info.name).split(".")[-1]
if not enabled and name in ("FuseOps", "FuseTIR"):
continue
mod = transform(mod)
if name in ("FuseTIR", "ApplyDefaultSchedule"):
(directory / f"{i:02d}_{name}.py").write_text(mod.script())
for transform in pipeline.dataflow_lower_passes(target) +
pipeline.finalize_passes(target):
mod = transform(mod)
return relax.build(mod, target=target, relax_pipeline=None,
tir_pipeline="default")
def main():
trial = int(sys.argv[1]) if len(sys.argv) > 1 else 0
out = Path(f"add_take_repro_{trial}")
device = tvm.cuda(0)
assert device.exist
target = tvm.target.Target(
{"kind": "cuda", "arch": "sm_" +
str(device.compute_version).replace(".", "")},
host={"kind": "llvm", "mcpu": "generic"},
)
vms = {name: relax.VirtualMachine(build(SOURCE, name == "on", target,
out/name), device)
for name in ("off", "on")}
encoded = np.broadcast_to((np.arange(4, dtype=np.float32)/3)[:, None],
(4, 4)).astype("float16").copy()
datasets = [("row_encoded", encoded)] + [
(f"random_{seed}", np.random.default_rng(seed).uniform(.1, .5, (4,
4)).astype("float16"))
for seed in range(3)
]
observations = []
failures = 0
for label, x in datasets:
dx = tvm.runtime.tensor(x, device=device)
for index in (0, 2, 3):
ids = np.array([index], dtype="int32")
di = tvm.runtime.tensor(ids, device=device)
# Preserve float16 Add rounding before indexing; independent of
TVM.
expected = (x.astype("float32") +
np.float32(1)).astype("float16")[ids, :]
for repeat in range(5):
values = {}
for name in (("off", "on") if repeat % 2 == 0 else ("on",
"off")):
vm = vms[name]
vm.set_input("main", di, dx)
vm.invoke_stateful("main")
device.sync()
got = vm.get_outputs("main").numpy().copy()
assert got.shape == expected.shape and got.dtype ==
expected.dtype
values[name] = got
np.testing.assert_array_equal(values["off"], expected)
passed = bool(np.isclose(values["on"], expected, rtol=.02,
atol=.002).all())
failures += not passed
observations.append(dict(dataset=label, index=index,
repeat=repeat,
input=x.tolist(), expected=expected.tolist(),
off=values["off"].tolist(),
on=values["on"].tolist(), on_passed=passed))
if label == "row_encoded" and repeat == 0:
print("index:", index, "expected:", expected, "OFF:",
values["off"], "ON:", values["on"])
(out/"observations.json").write_text(json.dumps(observations, indent=2))
print(f"OFF: all {len(observations)} checks passed; ON:
{failures}/{len(observations)} failed")
if __name__ == "__main__":
main()
```
### Triage
- needs-triage
- backend:cuda
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]