This is an automated email from the ASF dual-hosted git repository.
cbalint13 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 4650887f6a [Relax][ONNX] Support align_corners in AffineGrid op
(#19864)
4650887f6a is described below
commit 4650887f6ab6db561246615fc6b17b5abfa81ed1
Author: Guan-Ming (Wesley) Chiu <[email protected]>
AuthorDate: Mon Jun 22 17:46:36 2026 +0800
[Relax][ONNX] Support align_corners in AffineGrid op (#19864)
## Related Issue
closes #19690
## Why
ONNX AffineGrid carries an align_corners attribute, but the Relax op
ignored it and always produced the align_corners=1 grid.
## How
- Add align_corners field to AffineGridAttrs (mirrors GridSampleAttrs).
- Thread the flag through the op, legalize pass, and TOPI compute.
- Pass the ONNX attribute through in the frontend instead of dropping
it.
---
include/tvm/relax/attrs/image.h | 13 ++++++++++
python/tvm/relax/frontend/onnx/onnx_frontend.py | 7 ++----
python/tvm/relax/op/image/image.py | 13 +++++-----
python/tvm/relax/transform/legalize_ops/image.py | 1 +
python/tvm/topi/image/grid_sample.py | 32 +++++++++++++++++-------
src/relax/op/image/resize.cc | 9 +++++--
src/relax/op/image/resize.h | 2 +-
tests/python/relax/test_frontend_onnx.py | 12 ++++-----
8 files changed, 58 insertions(+), 31 deletions(-)
diff --git a/include/tvm/relax/attrs/image.h b/include/tvm/relax/attrs/image.h
index eacbea7180..c9a7203740 100644
--- a/include/tvm/relax/attrs/image.h
+++ b/include/tvm/relax/attrs/image.h
@@ -149,6 +149,19 @@ struct GridSampleAttrs : public AttrsNode {
TVM_FFI_DECLARE_OBJECT_INFO_FINAL("relax.attrs.GridSampleAttrs",
GridSampleAttrs, AttrsNode);
}; // struct GridSampleAttrs
+/*! \brief Attributes used in image affine_grid operator */
+struct AffineGridAttrs : public AttrsNode {
+ bool align_corners;
+
+ static void RegisterReflection() {
+ namespace refl = tvm::ffi::reflection;
+ refl::ObjectDef<AffineGridAttrs>().def_ro(
+ "align_corners", &AffineGridAttrs::align_corners,
+ "If True, normalized grid coordinates map to corner pixels; otherwise
to pixel centers.");
+ }
+ TVM_FFI_DECLARE_OBJECT_INFO_FINAL("relax.attrs.AffineGridAttrs",
AffineGridAttrs, AttrsNode);
+}; // struct AffineGridAttrs
+
} // namespace relax
} // namespace tvm
diff --git a/python/tvm/relax/frontend/onnx/onnx_frontend.py
b/python/tvm/relax/frontend/onnx/onnx_frontend.py
index d550d3bc00..61f95e7130 100644
--- a/python/tvm/relax/frontend/onnx/onnx_frontend.py
+++ b/python/tvm/relax/frontend/onnx/onnx_frontend.py
@@ -3309,10 +3309,7 @@ class AffineGrid(OnnxOpConverter):
def _impl_v20(cls, bb, inputs, attr, params):
theta = inputs[0] # [N, 2, 3] for 2D
size = get_constant(inputs[1], params) # [N, C, H, W] for 2D
- align_corners = attr.get("align_corners", 0)
-
- if align_corners != 1:
- raise NotImplementedError("AffineGrid with align_corners=0 is not
yet supported in TVM")
+ align_corners = bool(attr.get("align_corners", 0))
# Extract size values
if isinstance(size, relax.Constant):
@@ -3328,7 +3325,7 @@ class AffineGrid(OnnxOpConverter):
target_h, target_w = size_vals[2], size_vals[3]
# Relax affine_grid outputs [N, 2, H, W]
- grid = bb.emit(relax.op.image.affine_grid(theta, (target_h, target_w)))
+ grid = bb.emit(relax.op.image.affine_grid(theta, (target_h, target_w),
align_corners))
# Permute to ONNX convention [N, H, W, 2]
return bb.emit(relax.op.permute_dims(grid, axes=[0, 2, 3, 1]))
diff --git a/python/tvm/relax/op/image/image.py
b/python/tvm/relax/op/image/image.py
index 323bfa74b5..29aa8457d2 100644
--- a/python/tvm/relax/op/image/image.py
+++ b/python/tvm/relax/op/image/image.py
@@ -237,6 +237,7 @@ def grid_sample(
def affine_grid(
data: Expr,
size: Expr | SizeLike,
+ align_corners: bool = True,
) -> Expr:
"""Generate a 2D sampling grid using an affine transformation matrix.
@@ -253,20 +254,18 @@ def affine_grid(
The target output spatial shape (H, W). If a single integer or PrimExpr
is provided, it is interpreted as a square output shape (size, size).
+ align_corners : bool
+ If True, normalized grid coordinates map to corner pixels; if False, to
+ pixel centers (the PyTorch / ONNX default).
+
Returns
-------
result : relax.Expr
The output grid tensor with shape [batch, 2, H, W].
-
- Note
- ----
- Only `align_corners=True` is supported by this operator, matching the
- behavior of the underlying TOPI implementation. When using this operator
- via PyTorch or ONNX frontends, `align_corners=False` will be rejected.
"""
if isinstance(size, int | PrimExpr):
size = (size, size)
if isinstance(size, tuple | list):
size = ShapeExpr(size)
- return cast(Expr, _ffi_api.affine_grid(data, size))
+ return cast(Expr, _ffi_api.affine_grid(data, size, align_corners))
diff --git a/python/tvm/relax/transform/legalize_ops/image.py
b/python/tvm/relax/transform/legalize_ops/image.py
index 42cb72f7c9..687e898a21 100644
--- a/python/tvm/relax/transform/legalize_ops/image.py
+++ b/python/tvm/relax/transform/legalize_ops/image.py
@@ -66,6 +66,7 @@ def _image_affine_grid(bb: BlockBuilder, call: Call) -> Expr:
topi.image.affine_grid,
call.args[0],
target_shape=target_shape,
+ align_corners=call.attrs.align_corners,
)
diff --git a/python/tvm/topi/image/grid_sample.py
b/python/tvm/topi/image/grid_sample.py
index 79032f41b3..cdfb7f4362 100644
--- a/python/tvm/topi/image/grid_sample.py
+++ b/python/tvm/topi/image/grid_sample.py
@@ -20,7 +20,7 @@
from tvm import te, tirx
-def affine_grid(data, target_shape):
+def affine_grid(data, target_shape, align_corners=True):
"""affine_grid operator that generates 2D sampling grid.
This operation is described in https://arxiv.org/pdf/1506.02025.pdf. It
generates a uniform
@@ -35,6 +35,10 @@ def affine_grid(data, target_shape):
target_shape: list/tuple of two int
Specifies the output shape (H, W).
+ align_corners : bool
+ If True, normalized coordinates map to corner pixels; if False, to
pixel centers
+ (the PyTorch / ONNX default).
+
Returns
-------
Output : tvm.Tensor
@@ -42,18 +46,28 @@ def affine_grid(data, target_shape):
"""
assert target_shape is not None
assert len(target_shape) == 2
- assert target_shape[0] > 1 and target_shape[1] > 1, (
- "target height/width should be greater than 1"
- )
+ if align_corners:
+ assert target_shape[0] > 1 and target_shape[1] > 1, (
+ "target height/width should be greater than 1 when align_corners
is True"
+ )
dtype = data.dtype
- y_step = tirx.const((2.0 - 1e-7) / (target_shape[0] - 1), dtype=dtype)
- x_step = tirx.const((2.0 - 1e-7) / (target_shape[1] - 1), dtype=dtype)
- start = tirx.const(-1.0, dtype=dtype)
+ height, width = target_shape[0], target_shape[1]
+ if align_corners:
+ y_step = tirx.const((2.0 - 1e-7) / (height - 1), dtype=dtype)
+ x_step = tirx.const((2.0 - 1e-7) / (width - 1), dtype=dtype)
+ y_start = tirx.const(-1.0, dtype=dtype)
+ x_start = tirx.const(-1.0, dtype=dtype)
+ else:
+ # Pixel centers: coordinate i maps to (2 * i + 1) / size - 1.
+ y_step = tirx.const(2.0 / height, dtype=dtype)
+ x_step = tirx.const(2.0 / width, dtype=dtype)
+ y_start = tirx.const(-1.0 + 1.0 / height, dtype=dtype)
+ x_start = tirx.const(-1.0 + 1.0 / width, dtype=dtype)
def _compute(n, dim, i, j):
- y = start + i * y_step
- x = start + j * x_step
+ y = y_start + i * y_step
+ x = x_start + j * x_step
return data[n, dim, 0] * x + data[n, dim, 1] * y + data[n, dim, 2]
oshape = (data.shape[0], len(target_shape), *target_shape)
diff --git a/src/relax/op/image/resize.cc b/src/relax/op/image/resize.cc
index beb89af087..b92167e031 100644
--- a/src/relax/op/image/resize.cc
+++ b/src/relax/op/image/resize.cc
@@ -354,11 +354,15 @@ TVM_REGISTER_OP("relax.image.grid_sample")
/* relax.image.affine_grid */
-Expr affine_grid(Expr data, Expr size) {
+Expr affine_grid(Expr data, Expr size, bool align_corners) {
+ ffi::ObjectPtr<AffineGridAttrs> attrs = ffi::make_object<AffineGridAttrs>();
+ attrs->align_corners = align_corners;
static const Op& op = Op::Get("relax.image.affine_grid");
- return Call(op, {std::move(data), std::move(size)}, Attrs(), {});
+ return Call(op, {std::move(data), std::move(size)}, Attrs(attrs), {});
}
+TVM_FFI_STATIC_INIT_BLOCK() { AffineGridAttrs::RegisterReflection(); }
+
TVM_FFI_STATIC_INIT_BLOCK() {
namespace refl = tvm::ffi::reflection;
refl::GlobalDef().def("relax.op.image.affine_grid", affine_grid);
@@ -438,6 +442,7 @@ TVM_REGISTER_OP("relax.image.affine_grid")
.set_num_inputs(2)
.add_argument("data", "Tensor", "The input affine matrix tensor.")
.add_argument("size", "Shape", "The target output shape (H, W).")
+ .set_attrs_type<AffineGridAttrs>()
.set_attr<FInferType>("FInferType", InferTypeAffineGrid)
.set_attr<TMixedPrecisionPolicy>("TMixedPrecisionPolicy",
MixedPrecisionPolicyKind::kFollow)
.set_attr<bool>("FPurity", true);
diff --git a/src/relax/op/image/resize.h b/src/relax/op/image/resize.h
index 06a927d3a7..382a3a162b 100644
--- a/src/relax/op/image/resize.h
+++ b/src/relax/op/image/resize.h
@@ -49,7 +49,7 @@ Expr grid_sample(Expr data, Expr grid, ffi::String method,
ffi::String layout,
ffi::String padding_mode, bool align_corners);
/*! \brief Image affine_grid operator. */
-Expr affine_grid(Expr data, Expr size);
+Expr affine_grid(Expr data, Expr size, bool align_corners);
} // namespace relax
} // namespace tvm
diff --git a/tests/python/relax/test_frontend_onnx.py
b/tests/python/relax/test_frontend_onnx.py
index d3036a2547..82e3f997d2 100644
--- a/tests/python/relax/test_frontend_onnx.py
+++ b/tests/python/relax/test_frontend_onnx.py
@@ -5555,13 +5555,11 @@ def test_nms_score_threshold():
)
-def test_affine_grid():
- affine_grid_node = helper.make_node(
- "AffineGrid",
- ["theta", "size"],
- ["grid"],
- align_corners=1,
- )
+# align_corners=None omits the attribute, exercising the ONNX default of 0.
[email protected]("align_corners", [None, 0, 1])
+def test_affine_grid(align_corners):
+ attrs = {} if align_corners is None else {"align_corners": align_corners}
+ affine_grid_node = helper.make_node("AffineGrid", ["theta", "size"],
["grid"], **attrs)
graph = helper.make_graph(
[affine_grid_node],