Expected behavior
Per the tvm.relax.op.strided_slice contract, assume_inbound defaults to false, in which case "out of bound indices will be clipped to the bound". So a slice with an out-of-bound begin/end on a
symbolic dim should behave exactly like the same slice on a static dim: clip the indices, then slice.
Actual behavior
On a static dim, out-of-bound begin/end are clipped correctly. On a symbolic dim, the clip is applied to the loop extent only — the index expression keeps the unclipped begin — so the first
steps of the slice read out of bounds, and the result silently contains garbage:
default (compiled): reads uninitialized memory (e.g. 1.88e+09 for float32 inputs in [-1, 1])
cpu_generic (fused): different garbage (2.22e+09) — the two official pipelines disagree
bytecode VM: zeros
All three execution backends violate the documented clipping contract; what you get depends on memory content (with a larger out-of-bound offset, e.g. begin = m + 50, this is a potential segfault, not just
wrong values).
Environment
OS: Linux x86_64
Target: llvm
TVM commit: 2a2b293c02269f4d9f3526c5b03a7548578e78e8 (current main)
Steps to reproduce
import numpy as np
import tvm
from tvm import relax
from tvm import tirx as tir
def build(begin_fn, end_fn, stride, symbolic):
dim = tir.Var("m", "int64") if symbolic else 8
bb = relax.BlockBuilder()
x = relax.Var("x", relax.TensorType([dim], "float32"))
with bb.function("main", params=[x]):
with bb.dataflow():
y = bb.emit(relax.op.strided_slice(
x, [0], [begin_fn(dim)], [end_fn(dim)], [stride]), "y")
gv = bb.emit_output(y)
bb.emit_func_output(gv)
return bb.get()
def run(mod, X):
exe = tvm.relax.build(mod, target=tvm.target.Target("llvm"), exec_mode="compiled")
return relax.VirtualMachine(exe, tvm.cpu())["main"](
tvm.runtime.tensor(X, tvm.cpu())).numpy()
X = np.clip(np.random.RandomState(7).randn(8), -1, 1).astype("float32")
# Symbolic dim, out-of-bound begin/end — should clip to a full reversal
mod = build(lambda d: d + 1, lambda d: -d - 1, -1, symbolic=True)
print(run(mod, X)) # [garbage, garbage, x[7], x[6], ...] — OOB reads
# Static dim, equally out-of-bound — clips correctly
mod_static = build(lambda d: 100, lambda d: -100, -1, symbolic=False)
print(run(mod_static, X)) # == X[::-1], correct
Root cause
After LegalizeOps, the generated TIR for the symbolic case is:
out[v_ax0] = x[m + 1 - v_ax0]
The extent of v_ax0 uses the min/max clip formulas (so the output shape is correct), but the index expression uses the raw, unclipped begin (m + 1). The first iterations therefore read x[m+1],
x[m], ... before landing in bounds. The static path folds the clip at compile time, which is why only the symbolic path is broken.
Fix direction: clamp the runtime begin (and end) into [0, dim-1] / [-1, dim-1] before generating the index expression, the same way the extent formulas already do.
Impact
strided_slice with symbolic dims and out-of-bound endpoints is reachable from real frontends (ONNX Slice with dynamic starts/ends on a dynamic batch/sequence axis). The failure is silent wrong-code
plus out-of-bounds reads on all three execution backends.
Expected behavior
Per the
tvm.relax.op.strided_slicecontract,assume_inbounddefaults tofalse, in which case "out of bound indices will be clipped to the bound". So a slice with an out-of-boundbegin/endon asymbolic dim should behave exactly like the same slice on a static dim: clip the indices, then slice.
Actual behavior
On a static dim, out-of-bound
begin/endare clipped correctly. On a symbolic dim, the clip is applied to the loop extent only — the index expression keeps the unclipped begin — so the firststeps of the slice read out of bounds, and the result silently contains garbage:
default(compiled): reads uninitialized memory (e.g.1.88e+09for float32 inputs in[-1, 1])cpu_generic(fused): different garbage (2.22e+09) — the two official pipelines disagreebytecodeVM: zerosAll three execution backends violate the documented clipping contract; what you get depends on memory content (with a larger out-of-bound offset, e.g.
begin = m + 50, this is a potential segfault, not justwrong values).
Environment
Steps to reproduce
Root cause
After
LegalizeOps, the generated TIR for the symbolic case is:The extent of
v_ax0uses the min/max clip formulas (so the output shape is correct), but the index expression uses the raw, unclippedbegin(m + 1). The first iterations therefore readx[m+1],x[m], ... before landing in bounds. The static path folds the clip at compile time, which is why only the symbolic path is broken.Fix direction: clamp the runtime
begin(andend) into[0, dim-1]/[-1, dim-1]before generating the index expression, the same way the extent formulas already do.Impact
strided_slicewith symbolic dims and out-of-bound endpoints is reachable from real frontends (ONNXSlicewith dynamicstarts/endson a dynamic batch/sequence axis). The failure is silent wrong-codeplus out-of-bounds reads on all three execution backends.