This is an automated email from the ASF dual-hosted git repository.

tlopex pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tvm.git


The following commit(s) were added to refs/heads/main by this push:
     new 3aa0eb16f7 [Fix][Relax] Normalize negative indices in 
Gather/Scatter/OneHot Ops (#20219)
3aa0eb16f7 is described below

commit 3aa0eb16f70e42a7ed716b2e5a0642eda342b3fe
Author: Kryptonite <[email protected]>
AuthorDate: Sat Aug 29 08:02:47 2026 +0300

    [Fix][Relax] Normalize negative indices in Gather/Scatter/OneHot Ops 
(#20219)
    
    GatherElements, GatherND, ScatterND, and OneHot in the ONNX frontend
    forwarded negative indices as-is instead of normalizing them (idx +
    axis_length).
    
    Fixes #20217.
---
 python/tvm/relax/frontend/onnx/onnx_frontend.py | 100 +++++++++++++++-----
 tests/python/relax/test_frontend_onnx.py        | 118 ++++++++++++++++++++++++
 2 files changed, 196 insertions(+), 22 deletions(-)

diff --git a/python/tvm/relax/frontend/onnx/onnx_frontend.py 
b/python/tvm/relax/frontend/onnx/onnx_frontend.py
index 0b2e5063c8..8a40e4a612 100644
--- a/python/tvm/relax/frontend/onnx/onnx_frontend.py
+++ b/python/tvm/relax/frontend/onnx/onnx_frontend.py
@@ -1329,6 +1329,48 @@ class Cast(OnnxOpConverter):
         return relax.op.astype(inputs[0], to_type)
 
 
+def _normalize_negative_indices(bb, indices, axis_extent):
+    """Map negative indices to idx + axis_extent; skip unsigned indices. 
axis_extent
+    must be broadcastable against indices and share its dtype.
+    """
+    indices_dtype = indices.ty.dtype.dtype
+    if indices_dtype.startswith("uint"):
+        return indices
+    return bb.normalize(
+        relax.op.where(
+            relax.op.less(indices, relax.const(0, indices_dtype)),
+            relax.op.add(indices, axis_extent),
+            indices,
+        )
+    )
+
+
+def _axis_extent(bb, data, axis, indices_dtype):
+    """Compute data.shape[axis] at runtime, cast to indices_dtype."""
+    data_shape_tensor = 
bb.normalize(relax.op.shape_to_tensor(relax.op.shape_of(data)))
+    axis_extent = bb.normalize(
+        relax.op.take(data_shape_tensor, relax.const(axis, "int64"), axis=0, 
mode="wrap")
+    )
+    if indices_dtype != "int64":
+        axis_extent = bb.normalize(relax.op.astype(axis_extent, indices_dtype))
+    return axis_extent
+
+
+def _coord_axis_extents(bb, data, indices, start_axis):
+    """Compute data.shape[start_axis + j] for each coordinate slot j, cast to 
indices_dtype."""
+    indices_dtype = indices.ty.dtype.dtype
+    coord_len = indices.ty.shape[-1]
+    data_shape_tensor = 
bb.normalize(relax.op.shape_to_tensor(relax.op.shape_of(data)))
+    axis_extent = bb.normalize(
+        relax.op.strided_slice(
+            data_shape_tensor, axes=[0], begin=[start_axis], end=[start_axis + 
coord_len]
+        )
+    )
+    if indices_dtype != "int64":
+        axis_extent = bb.normalize(relax.op.astype(axis_extent, indices_dtype))
+    return axis_extent
+
+
 class Gather(OnnxOpConverter):
     """Convert an onnx Gather node into an equivalent Relax expression."""
 
@@ -1365,23 +1407,8 @@ class Gather(OnnxOpConverter):
             indices = bb.normalize(relax.op.shape_to_tensor(indices))
 
         indices_dtype = indices.ty.dtype.dtype
-        if not indices_dtype.startswith("uint"):
-            data_shape = bb.normalize(relax.op.shape_of(data))
-            data_shape_tensor = 
bb.normalize(relax.op.shape_to_tensor(data_shape))
-            axis_extent = bb.normalize(
-                relax.op.take(data_shape_tensor, relax.const(axis, "int64"), 
axis=0, mode="wrap")
-            )
-
-            if indices_dtype != "int64":
-                axis_extent = bb.normalize(relax.op.astype(axis_extent, 
indices_dtype))
-
-            indices = bb.normalize(
-                relax.op.where(
-                    relax.op.less(indices, relax.const(0, indices_dtype)),
-                    relax.op.add(indices, axis_extent),
-                    indices,
-                )
-            )
+        axis_extent = _axis_extent(bb, data, axis, indices_dtype)
+        indices = _normalize_negative_indices(bb, indices, axis_extent)
 
         return relax.op.take(data, indices, axis)
 
@@ -1391,8 +1418,15 @@ class GatherElements(OnnxOpConverter):
 
     @classmethod
     def _impl_v13(cls, bb, inputs, attr, params):
+        data = inputs[0]
+        indices = inputs[1]
         axis = attr.get("axis", 0)
-        return relax.op.gather_elements(inputs[0], inputs[1], axis)
+
+        indices_dtype = indices.ty.dtype.dtype
+        axis_extent = _axis_extent(bb, data, axis, indices_dtype)
+        indices = _normalize_negative_indices(bb, indices, axis_extent)
+
+        return relax.op.gather_elements(data, indices, axis)
 
 
 class GatherND(OnnxOpConverter):
@@ -1400,8 +1434,13 @@ class GatherND(OnnxOpConverter):
 
     @classmethod
     def _impl_v13(cls, bb, inputs, attr, params):
+        data = inputs[0]
+        indices = inputs[1]
         batch_dims = attr.get("batch_dims", 0)
-        return relax.op.gather_nd(inputs[0], inputs[1], batch_dims)
+
+        axis_extent = _coord_axis_extents(bb, data, indices, batch_dims)
+        indices = _normalize_negative_indices(bb, indices, axis_extent)
+        return relax.op.gather_nd(data, indices, batch_dims)
 
 
 def _shapes_equal(a: list[tirx.Expr] | None, b: list[tirx.Expr] | None) -> 
bool:
@@ -1531,19 +1570,31 @@ class ScatterND(OnnxOpConverter):
     def _reduction_check(attr, valid_reductions: list[str]):
         return _get_onnx_reduction(attr, valid_reductions)
 
+    @staticmethod
+    def _normalize_indices(bb, data, indices):
+        # ScatterND has no batch_dims: coordinate slot j always indexes data 
axis j.
+        axis_extent = _coord_axis_extents(bb, data, indices, start_axis=0)
+        return _normalize_negative_indices(bb, indices, axis_extent)
+
     @classmethod
     def _impl_v11(cls, bb, inputs, attr, params):
-        return relax.op.scatter_nd(inputs[0], inputs[1], inputs[2])
+        data, indices, updates = inputs
+        indices = cls._normalize_indices(bb, data, indices)
+        return relax.op.scatter_nd(data, indices, updates)
 
     @classmethod
     def _impl_v16(cls, bb, inputs, attr, params):
+        data, indices, updates = inputs
         reduction = cls._reduction_check(attr, ["update", "add", "mul"])
-        return relax.op.scatter_nd(inputs[0], inputs[1], inputs[2], reduction)
+        indices = cls._normalize_indices(bb, data, indices)
+        return relax.op.scatter_nd(data, indices, updates, reduction)
 
     @classmethod
     def _impl_v18(cls, bb, inputs, attr, params):
+        data, indices, updates = inputs
         reduction = cls._reduction_check(attr, ["update", "add", "mul", "min", 
"max"])
-        return relax.op.scatter_nd(inputs[0], inputs[1], inputs[2], reduction)
+        indices = cls._normalize_indices(bb, data, indices)
+        return relax.op.scatter_nd(data, indices, updates, reduction)
 
 
 class Compress(OnnxOpConverter):
@@ -5077,6 +5128,11 @@ class OneHot(OnnxOpConverter):
             relax.prim_value(off_value),
             relax.prim_value(on_value),
         )
+
+        indices_dtype = indices.ty.dtype.dtype
+        axis_extent = relax.const(depth, indices_dtype)
+        indices = _normalize_negative_indices(bb, indices, axis_extent)
+
         return relax.op.one_hot(indices, on_value, off_value, depth, axis)
 
 
diff --git a/tests/python/relax/test_frontend_onnx.py 
b/tests/python/relax/test_frontend_onnx.py
index 00c34b775b..a2081adab9 100644
--- a/tests/python/relax/test_frontend_onnx.py
+++ b/tests/python/relax/test_frontend_onnx.py
@@ -1816,6 +1816,37 @@ def test_gather_elements(data_shape, indices_shape, 
axis):
     check_correctness(model, inputs=input_values)
 
 
[email protected](
+    "data_shape, indices_shape, axis",
+    [
+        ([3, 4, 5], [1, 4, 5], 0),
+        ([3, 4, 5], [3, 2, 5], 1),
+        ([3, 4, 5], [3, 4, 2], 2),
+    ],
+)
+def test_gather_elements_negative_indices(data_shape, indices_shape, axis):
+    gather_elements_node = helper.make_node("GatherElements", ["data", 
"indices"], ["y"], axis=axis)
+
+    graph = helper.make_graph(
+        [gather_elements_node],
+        "gather_elements_negative_indices_test",
+        inputs=[
+            helper.make_tensor_value_info("data", TensorProto.FLOAT, 
data_shape),
+            helper.make_tensor_value_info("indices", TensorProto.INT64, 
indices_shape),
+        ],
+        outputs=[helper.make_tensor_value_info("y", TensorProto.FLOAT, 
indices_shape)],
+    )
+
+    model = helper.make_model(graph, 
producer_name="gather_elements_negative_indices_test")
+    input_values = {
+        "data": np.random.randn(*data_shape).astype("float32"),
+        "indices": np.random.randint(-data_shape[axis], data_shape[axis], 
indices_shape).astype(
+            "int64"
+        ),
+    }
+    check_correctness(model, inputs=input_values)
+
+
 @pytest.mark.parametrize(
     "data_shape, indices_shape, batch_dims",
     [
@@ -1849,6 +1880,39 @@ def test_gather_nd(data_shape, indices_shape, 
batch_dims):
     check_correctness(model, inputs=input_values)
 
 
[email protected](
+    "data_shape, indices_shape, batch_dims",
+    [
+        ([2, 2], [2, 2], 0),
+        ([2, 2], [2, 1], 0),
+        ([2, 2, 2], [1], 0),
+        ([2, 2, 2], [2, 2], 0),
+        ([2, 2, 2], [2, 1, 2], 0),
+        ([2, 2, 2], [2, 2], 1),
+        ([2, 2, 2], [2, 1], 1),
+    ],
+)
+def test_gather_nd_negative_indices(data_shape, indices_shape, batch_dims):
+    gather_nd_node = helper.make_node("GatherND", ["data", "indices"], ["y"], 
batch_dims=batch_dims)
+
+    graph = helper.make_graph(
+        [gather_nd_node],
+        "gather_nd_negative_indices_test",
+        inputs=[
+            helper.make_tensor_value_info("data", TensorProto.FLOAT, 
data_shape),
+            helper.make_tensor_value_info("indices", TensorProto.INT64, 
indices_shape),
+        ],
+        outputs=[helper.make_tensor_value_info("y", TensorProto.FLOAT, None)],
+    )
+
+    model = helper.make_model(graph, 
producer_name="gather_nd_negative_indices_test")
+    input_values = {
+        "data": np.random.randn(*data_shape).astype("float32"),
+        "indices": np.random.randint(-2, 2, indices_shape).astype("int64"),
+    }
+    check_correctness(model, inputs=input_values)
+
+
 @pytest.mark.parametrize("axis", [0, 1, 2])
 @pytest.mark.parametrize(("name", "opset"), [("Scatter", 10), 
("ScatterElements", 11)])
 def test_scatter(axis: int, name: str, opset: int):
@@ -2119,6 +2183,38 @@ def test_scatter_nd(reduction):
     verify_scatter_nd([10], [5, 1], [5])
 
 
[email protected]("reduction", ["none", "add", "mul"])
+def test_scatter_nd_negative_indices(reduction):
+    def verify_scatter_nd_negative_indices(data_shape, indices_shape, 
updates_shape):
+        scatter_nd_node = helper.make_node(
+            "ScatterND",
+            ["data", "indices", "updates"],
+            ["output"],
+            reduction=reduction,
+        )
+
+        graph = helper.make_graph(
+            [scatter_nd_node],
+            "scatter_nd_negative_indices_test",
+            inputs=[
+                helper.make_tensor_value_info("data", TensorProto.FLOAT, 
data_shape),
+                helper.make_tensor_value_info("indices", TensorProto.INT64, 
indices_shape),
+                helper.make_tensor_value_info("updates", TensorProto.FLOAT, 
updates_shape),
+            ],
+            outputs=[helper.make_tensor_value_info("output", 
TensorProto.FLOAT, data_shape)],
+        )
+
+        model = helper.make_model(graph, 
producer_name="scatter_nd_negative_indices_test")
+
+        indices = np.random.randint(-data_shape[0], data_shape[0], 
indices_shape)
+        check_correctness(model, inputs={"indices": indices}, opset=16)
+
+    verify_scatter_nd_negative_indices([8], [4, 1], [4])
+    verify_scatter_nd_negative_indices([4, 4, 4], [2, 1], [2, 4, 4])
+    verify_scatter_nd_negative_indices([4, 5, 6], [2, 3, 2], [2, 3, 6])
+    verify_scatter_nd_negative_indices([10], [5, 1], [5])
+
+
 def test_compress():
     def verify_compress(
         tensor_shape: list[int],
@@ -9859,6 +9955,28 @@ def test_onehot():
     check_correctness(model, inputs=values)
 
 
+def test_onehot_negative_indices():
+    one_hot_node = helper.make_node("OneHot", ["indices", "depth", "values"], 
["y"], axis=1)
+    graph = helper.make_graph(
+        [one_hot_node],
+        "one_hot_negative_indices_test",
+        inputs=[
+            helper.make_tensor_value_info("indices", TensorProto.INT64, [2, 
2]),
+        ],
+        initializer=[
+            helper.make_tensor("depth", TensorProto.INT64, [], [10]),
+            helper.make_tensor("values", TensorProto.FLOAT, [2], [3, 1]),
+        ],
+        outputs=[helper.make_tensor_value_info("y", TensorProto.FLOAT, [2, 10, 
2])],
+    )
+
+    model = helper.make_model(graph, 
producer_name="one_hot_negative_indices_test")
+    values = {
+        "indices": np.array([[-1, -10], [-2, 4]], dtype="int64"),
+    }
+    check_correctness(model, inputs=values)
+
+
 @pytest.mark.parametrize("axis", [None, 0, 1, -1])
 @pytest.mark.parametrize("sorted", [0, 1])
 @pytest.mark.parametrize("num_outputs", [1, 2, 3, 4])

Reply via email to