Skip to content

[Bug][Relax] strided_slice with symbolic shape: out-of-bound begin/end are clipped for the loop extent but not for the index expression, producing out-of-bound reads and silent wrong values #20262

Description

@lrcyyds1

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.

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