Skip to content

Commit 4650887

Browse files
authored
[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.
1 parent a9ce41e commit 4650887

8 files changed

Lines changed: 58 additions & 31 deletions

File tree

include/tvm/relax/attrs/image.h

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,19 @@ struct GridSampleAttrs : public AttrsNode {
149149
TVM_FFI_DECLARE_OBJECT_INFO_FINAL("relax.attrs.GridSampleAttrs", GridSampleAttrs, AttrsNode);
150150
}; // struct GridSampleAttrs
151151

152+
/*! \brief Attributes used in image affine_grid operator */
153+
struct AffineGridAttrs : public AttrsNode {
154+
bool align_corners;
155+
156+
static void RegisterReflection() {
157+
namespace refl = tvm::ffi::reflection;
158+
refl::ObjectDef<AffineGridAttrs>().def_ro(
159+
"align_corners", &AffineGridAttrs::align_corners,
160+
"If True, normalized grid coordinates map to corner pixels; otherwise to pixel centers.");
161+
}
162+
TVM_FFI_DECLARE_OBJECT_INFO_FINAL("relax.attrs.AffineGridAttrs", AffineGridAttrs, AttrsNode);
163+
}; // struct AffineGridAttrs
164+
152165
} // namespace relax
153166
} // namespace tvm
154167

python/tvm/relax/frontend/onnx/onnx_frontend.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3309,10 +3309,7 @@ class AffineGrid(OnnxOpConverter):
33093309
def _impl_v20(cls, bb, inputs, attr, params):
33103310
theta = inputs[0] # [N, 2, 3] for 2D
33113311
size = get_constant(inputs[1], params) # [N, C, H, W] for 2D
3312-
align_corners = attr.get("align_corners", 0)
3313-
3314-
if align_corners != 1:
3315-
raise NotImplementedError("AffineGrid with align_corners=0 is not yet supported in TVM")
3312+
align_corners = bool(attr.get("align_corners", 0))
33163313

33173314
# Extract size values
33183315
if isinstance(size, relax.Constant):
@@ -3328,7 +3325,7 @@ def _impl_v20(cls, bb, inputs, attr, params):
33283325
target_h, target_w = size_vals[2], size_vals[3]
33293326

33303327
# Relax affine_grid outputs [N, 2, H, W]
3331-
grid = bb.emit(relax.op.image.affine_grid(theta, (target_h, target_w)))
3328+
grid = bb.emit(relax.op.image.affine_grid(theta, (target_h, target_w), align_corners))
33323329
# Permute to ONNX convention [N, H, W, 2]
33333330
return bb.emit(relax.op.permute_dims(grid, axes=[0, 2, 3, 1]))
33343331

python/tvm/relax/op/image/image.py

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,7 @@ def grid_sample(
237237
def affine_grid(
238238
data: Expr,
239239
size: Expr | SizeLike,
240+
align_corners: bool = True,
240241
) -> Expr:
241242
"""Generate a 2D sampling grid using an affine transformation matrix.
242243
@@ -253,20 +254,18 @@ def affine_grid(
253254
The target output spatial shape (H, W). If a single integer or PrimExpr
254255
is provided, it is interpreted as a square output shape (size, size).
255256
257+
align_corners : bool
258+
If True, normalized grid coordinates map to corner pixels; if False, to
259+
pixel centers (the PyTorch / ONNX default).
260+
256261
Returns
257262
-------
258263
result : relax.Expr
259264
The output grid tensor with shape [batch, 2, H, W].
260-
261-
Note
262-
----
263-
Only `align_corners=True` is supported by this operator, matching the
264-
behavior of the underlying TOPI implementation. When using this operator
265-
via PyTorch or ONNX frontends, `align_corners=False` will be rejected.
266265
"""
267266
if isinstance(size, int | PrimExpr):
268267
size = (size, size)
269268
if isinstance(size, tuple | list):
270269
size = ShapeExpr(size)
271270

272-
return cast(Expr, _ffi_api.affine_grid(data, size))
271+
return cast(Expr, _ffi_api.affine_grid(data, size, align_corners))

python/tvm/relax/transform/legalize_ops/image.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ def _image_affine_grid(bb: BlockBuilder, call: Call) -> Expr:
6666
topi.image.affine_grid,
6767
call.args[0],
6868
target_shape=target_shape,
69+
align_corners=call.attrs.align_corners,
6970
)
7071

7172

python/tvm/topi/image/grid_sample.py

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
from tvm import te, tirx
2121

2222

23-
def affine_grid(data, target_shape):
23+
def affine_grid(data, target_shape, align_corners=True):
2424
"""affine_grid operator that generates 2D sampling grid.
2525
2626
This operation is described in https://arxiv.org/pdf/1506.02025.pdf. It generates a uniform
@@ -35,25 +35,39 @@ def affine_grid(data, target_shape):
3535
target_shape: list/tuple of two int
3636
Specifies the output shape (H, W).
3737
38+
align_corners : bool
39+
If True, normalized coordinates map to corner pixels; if False, to pixel centers
40+
(the PyTorch / ONNX default).
41+
3842
Returns
3943
-------
4044
Output : tvm.Tensor
4145
4-D with shape [batch, 2, target_height, target_width]
4246
"""
4347
assert target_shape is not None
4448
assert len(target_shape) == 2
45-
assert target_shape[0] > 1 and target_shape[1] > 1, (
46-
"target height/width should be greater than 1"
47-
)
49+
if align_corners:
50+
assert target_shape[0] > 1 and target_shape[1] > 1, (
51+
"target height/width should be greater than 1 when align_corners is True"
52+
)
4853

4954
dtype = data.dtype
50-
y_step = tirx.const((2.0 - 1e-7) / (target_shape[0] - 1), dtype=dtype)
51-
x_step = tirx.const((2.0 - 1e-7) / (target_shape[1] - 1), dtype=dtype)
52-
start = tirx.const(-1.0, dtype=dtype)
55+
height, width = target_shape[0], target_shape[1]
56+
if align_corners:
57+
y_step = tirx.const((2.0 - 1e-7) / (height - 1), dtype=dtype)
58+
x_step = tirx.const((2.0 - 1e-7) / (width - 1), dtype=dtype)
59+
y_start = tirx.const(-1.0, dtype=dtype)
60+
x_start = tirx.const(-1.0, dtype=dtype)
61+
else:
62+
# Pixel centers: coordinate i maps to (2 * i + 1) / size - 1.
63+
y_step = tirx.const(2.0 / height, dtype=dtype)
64+
x_step = tirx.const(2.0 / width, dtype=dtype)
65+
y_start = tirx.const(-1.0 + 1.0 / height, dtype=dtype)
66+
x_start = tirx.const(-1.0 + 1.0 / width, dtype=dtype)
5367

5468
def _compute(n, dim, i, j):
55-
y = start + i * y_step
56-
x = start + j * x_step
69+
y = y_start + i * y_step
70+
x = x_start + j * x_step
5771
return data[n, dim, 0] * x + data[n, dim, 1] * y + data[n, dim, 2]
5872

5973
oshape = (data.shape[0], len(target_shape), *target_shape)

src/relax/op/image/resize.cc

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -354,11 +354,15 @@ TVM_REGISTER_OP("relax.image.grid_sample")
354354

355355
/* relax.image.affine_grid */
356356

357-
Expr affine_grid(Expr data, Expr size) {
357+
Expr affine_grid(Expr data, Expr size, bool align_corners) {
358+
ffi::ObjectPtr<AffineGridAttrs> attrs = ffi::make_object<AffineGridAttrs>();
359+
attrs->align_corners = align_corners;
358360
static const Op& op = Op::Get("relax.image.affine_grid");
359-
return Call(op, {std::move(data), std::move(size)}, Attrs(), {});
361+
return Call(op, {std::move(data), std::move(size)}, Attrs(attrs), {});
360362
}
361363

364+
TVM_FFI_STATIC_INIT_BLOCK() { AffineGridAttrs::RegisterReflection(); }
365+
362366
TVM_FFI_STATIC_INIT_BLOCK() {
363367
namespace refl = tvm::ffi::reflection;
364368
refl::GlobalDef().def("relax.op.image.affine_grid", affine_grid);
@@ -438,6 +442,7 @@ TVM_REGISTER_OP("relax.image.affine_grid")
438442
.set_num_inputs(2)
439443
.add_argument("data", "Tensor", "The input affine matrix tensor.")
440444
.add_argument("size", "Shape", "The target output shape (H, W).")
445+
.set_attrs_type<AffineGridAttrs>()
441446
.set_attr<FInferType>("FInferType", InferTypeAffineGrid)
442447
.set_attr<TMixedPrecisionPolicy>("TMixedPrecisionPolicy", MixedPrecisionPolicyKind::kFollow)
443448
.set_attr<bool>("FPurity", true);

src/relax/op/image/resize.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ Expr grid_sample(Expr data, Expr grid, ffi::String method, ffi::String layout,
4949
ffi::String padding_mode, bool align_corners);
5050

5151
/*! \brief Image affine_grid operator. */
52-
Expr affine_grid(Expr data, Expr size);
52+
Expr affine_grid(Expr data, Expr size, bool align_corners);
5353

5454
} // namespace relax
5555
} // namespace tvm

tests/python/relax/test_frontend_onnx.py

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5555,13 +5555,11 @@ def test_nms_score_threshold():
55555555
)
55565556

55575557

5558-
def test_affine_grid():
5559-
affine_grid_node = helper.make_node(
5560-
"AffineGrid",
5561-
["theta", "size"],
5562-
["grid"],
5563-
align_corners=1,
5564-
)
5558+
# align_corners=None omits the attribute, exercising the ONNX default of 0.
5559+
@pytest.mark.parametrize("align_corners", [None, 0, 1])
5560+
def test_affine_grid(align_corners):
5561+
attrs = {} if align_corners is None else {"align_corners": align_corners}
5562+
affine_grid_node = helper.make_node("AffineGrid", ["theta", "size"], ["grid"], **attrs)
55655563

55665564
graph = helper.make_graph(
55675565
[affine_grid_node],

0 commit comments

Comments
 (0)