Skip to content

Commit e0e90d0

Browse files
authored
Arm backend: Normalize grid-sampler custom inputs to NHWC (pytorch#20965)
Arm backend: Normalize grid-sampler custom inputs to NHWC This fixes a bug on input tensor order vs shader expectations. Ensure VGF grid_sample custom inputs are in the layout expected by the shader interface. The rewrite now permutes the sampled tensor from NCHW to NHWC beforectosa.CUSTOM, preserves the grid input as-is, and permutes the custom result back to NCHW afterward. This functions well with the newer tensor ordering rules in the Arm backend. Change-Id: Iccd829964e1142803ddb771ad5c3f405136d0d55 Signed-off-by: Rob Elliott <Robert.Elliott@arm.com> cc @digantdesai @freddan80 @per @zingo @oscarandersson8218 @mansnils @Sebastian-Larsson @rascani Signed-off-by: Rob Elliott <Robert.Elliott@arm.com>
1 parent fa4508e commit e0e90d0

2 files changed

Lines changed: 221 additions & 12 deletions

File tree

backends/arm/test/passes/test_rewrite_grid_sampler_to_tosa_custom_pass.py

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import torch.nn.functional as F
1010
from executorch.backends.arm._passes import FoldAndAnnotateQParamsPass
1111
from executorch.backends.arm._passes.quant_args import QuantArgs
12+
from executorch.backends.arm.constants import NHWC_ORDER
1213
from executorch.backends.arm.quantizer.arm_quantizer import (
1314
get_symmetric_quantization_config,
1415
VgfQuantizer,
@@ -58,6 +59,25 @@ def forward(self, x, grid):
5859
)
5960

6061

62+
class GridSampler2dWithNchwGrid(torch.nn.Module):
63+
def __init__(self):
64+
super().__init__()
65+
self.interpolation_mode_ = 0
66+
self.padding_mode_ = 0
67+
self.align_corners_ = False
68+
69+
def forward(self, x, grid_nchw):
70+
mode = ("bilinear", "nearest", "bicubic")[self.interpolation_mode_]
71+
grid = torch.permute(grid_nchw, NHWC_ORDER)
72+
return F.grid_sample(
73+
x,
74+
grid,
75+
mode=mode,
76+
padding_mode="zeros" if self.padding_mode_ == 0 else "border",
77+
align_corners=self.align_corners_,
78+
)
79+
80+
6181
class _RewriteGridSamplerToTosaCustomExportPass(ExportedProgramPassBase):
6282
# The quantized grid-sampler rewrite materializes exported constant
6383
# placeholders for grid scale/zero-point, so it needs ExportedProgram
@@ -453,3 +473,182 @@ def test_rewrite_grid_sampler_to_tosa_custom_buffer_dispatch_rounds_up_output():
453473
assert payload["output_0_type"] == "Tensor"
454474
assert payload["input_1_vkdescriptortype"] == "VK_DESCRIPTOR_TYPE_TENSOR_ARM"
455475
assert payload["workgroup_sizes"] == [2, 3, 1]
476+
477+
478+
def _call_function_nodes(graph_module: torch.fx.GraphModule) -> list[torch.fx.Node]:
479+
return [node for node in graph_module.graph.nodes if node.op == "call_function"]
480+
481+
482+
def _count_call_function_target(
483+
graph_module: torch.fx.GraphModule, target: object
484+
) -> int:
485+
return sum(
486+
1 for node in _call_function_nodes(graph_module) if node.target == target
487+
)
488+
489+
490+
def _custom_node(graph_module: torch.fx.GraphModule) -> torch.fx.Node:
491+
return next(
492+
node
493+
for node in graph_module.graph.nodes
494+
if node.target == exir_ops.backend.tosa.CUSTOM.default
495+
)
496+
497+
498+
def test_rewrite_grid_sampler_to_tosa_custom_introduces_one_input_permute_for_standard_grid():
499+
model = GridSampler2d()
500+
example_inputs = (
501+
torch.randn(1, 4, 8, 8),
502+
torch.randn(1, 4, 4, 2),
503+
)
504+
505+
edge_model = to_edge(export(model, example_inputs))
506+
original_graph_module = edge_model.exported_program().graph_module
507+
assert (
508+
_count_call_function_target(
509+
original_graph_module, exir_ops.edge.aten.permute_copy.default
510+
)
511+
== 0
512+
)
513+
514+
with TosaLoweringContext(TosaSpecification.create_from_string("TOSA-1.0+FP")):
515+
edge_model = edge_model.transform([RewriteGridSamplerToTosaCustomPass()])
516+
graph_module = edge_model.exported_program().graph_module
517+
custom_node = _custom_node(graph_module)
518+
custom_input, custom_grid = custom_node.args[0][:2]
519+
520+
assert tuple(custom_input.meta["val"].shape) == (1, 8, 8, 4)
521+
assert custom_input.target == exir_ops.edge.aten.permute_copy.default
522+
assert custom_input.args[1] == list(NHWC_ORDER)
523+
assert custom_grid.op == "placeholder"
524+
assert tuple(custom_grid.meta["val"].shape) == (1, 4, 4, 2)
525+
assert custom_grid.target == "grid"
526+
assert (
527+
_count_call_function_target(
528+
graph_module, exir_ops.edge.aten.permute_copy.default
529+
)
530+
== 2
531+
)
532+
533+
534+
def test_rewrite_grid_sampler_to_tosa_custom_preserves_existing_grid_permute():
535+
model = GridSampler2dWithNchwGrid()
536+
example_inputs = (
537+
torch.randn(1, 4, 8, 8),
538+
torch.randn(1, 2, 4, 4),
539+
)
540+
541+
edge_model = to_edge(export(model, example_inputs))
542+
original_graph_module = edge_model.exported_program().graph_module
543+
grid_sampler_node = next(
544+
node
545+
for node in original_graph_module.graph.nodes
546+
if node.target == exir_ops.edge.aten.grid_sampler_2d.default
547+
)
548+
original_grid = grid_sampler_node.args[1]
549+
550+
assert isinstance(original_grid, torch.fx.Node)
551+
assert original_grid.target == exir_ops.edge.aten.permute_copy.default
552+
assert original_grid.args[1] == list(NHWC_ORDER)
553+
assert (
554+
_count_call_function_target(
555+
original_graph_module, exir_ops.edge.aten.permute_copy.default
556+
)
557+
== 1
558+
)
559+
560+
with TosaLoweringContext(TosaSpecification.create_from_string("TOSA-1.0+FP")):
561+
edge_model = edge_model.transform([RewriteGridSamplerToTosaCustomPass()])
562+
graph_module = edge_model.exported_program().graph_module
563+
custom_node = _custom_node(graph_module)
564+
custom_input, custom_grid = custom_node.args[0][:2]
565+
566+
assert tuple(custom_input.meta["val"].shape) == (1, 8, 8, 4)
567+
assert custom_input.target == exir_ops.edge.aten.permute_copy.default
568+
assert custom_input.args[1] == list(NHWC_ORDER)
569+
assert custom_grid.target == exir_ops.edge.aten.permute_copy.default
570+
assert custom_grid.args[1] == list(NHWC_ORDER)
571+
assert tuple(custom_grid.meta["val"].shape) == (1, 4, 4, 2)
572+
assert (
573+
_count_call_function_target(
574+
graph_module, exir_ops.edge.aten.permute_copy.default
575+
)
576+
== 3
577+
)
578+
579+
580+
def test_rewrite_grid_sampler_to_tosa_custom_keeps_clean_graph_for_c3_sampler_path():
581+
model = GridSampler2d()
582+
example_inputs = (
583+
torch.randn(1, 3, 8, 8),
584+
torch.randn(1, 4, 4, 2),
585+
)
586+
587+
edge_model = to_edge(export(model, example_inputs))
588+
with TosaLoweringContext(TosaSpecification.create_from_string("TOSA-1.0+FP")):
589+
edge_model = edge_model.transform([RewriteGridSamplerToTosaCustomPass()])
590+
graph_module = edge_model.exported_program().graph_module
591+
custom_node = _custom_node(graph_module)
592+
custom_input, custom_grid = custom_node.args[0][:2]
593+
594+
assert tuple(custom_input.meta["val"].shape) == (1, 8, 8, 4)
595+
assert custom_input.target == exir_ops.edge.aten.permute_copy.default
596+
assert custom_grid.op == "placeholder"
597+
assert custom_grid.target == "grid"
598+
assert tuple(custom_grid.meta["val"].shape) == (1, 4, 4, 2)
599+
assert (
600+
_count_call_function_target(
601+
graph_module, exir_ops.edge.aten.permute_copy.default
602+
)
603+
== 2
604+
)
605+
assert (
606+
_count_call_function_target(graph_module, exir_ops.edge.aten.cat.default) == 1
607+
)
608+
assert (
609+
_count_call_function_target(graph_module, exir_ops.edge.aten.slice_copy.Tensor)
610+
>= 2
611+
)
612+
613+
614+
def test_rewrite_grid_sampler_to_tosa_custom_keeps_clean_graph_for_quantized_sampler_path():
615+
model = GridSampler2d().eval()
616+
example_inputs = (
617+
torch.randn(1, 4, 8, 8),
618+
torch.rand(1, 4, 4, 2),
619+
)
620+
quantizer = VgfQuantizer(VgfCompileSpec("TOSA-1.0+INT"))
621+
quantizer.set_global(get_symmetric_quantization_config(is_per_channel=False))
622+
623+
exported = export(model, example_inputs, strict=True)
624+
prepared = prepare_pt2e(exported.module(), quantizer)
625+
prepared(*example_inputs)
626+
converted = convert_pt2e(prepared)
627+
628+
edge_model = to_edge(export(converted, example_inputs, strict=True))
629+
with TosaLoweringContext(TosaSpecification.create_from_string("TOSA-1.0+FP+INT")):
630+
edge_model = edge_model.transform(
631+
[
632+
FoldAndAnnotateQParamsPass(),
633+
InsertGridSamplerGridDequantPass(),
634+
_RewriteGridSamplerToTosaCustomExportPass(),
635+
]
636+
)
637+
graph_module = edge_model.exported_program().graph_module
638+
custom_node = _custom_node(graph_module)
639+
custom_input, custom_grid = custom_node.args[0][:2]
640+
641+
assert tuple(custom_input.meta["val"].shape) == (1, 8, 8, 4)
642+
assert custom_input.target == exir_ops.edge.aten.permute_copy.default
643+
assert custom_grid.meta["val"].dtype == torch.int8
644+
assert custom_grid.target not in (
645+
exir_ops.edge.aten.permute_copy.default,
646+
exir_ops.edge.quantized_decomposed.dequantize_per_tensor.default,
647+
exir_ops.edge.quantized_decomposed.dequantize_per_channel.default,
648+
)
649+
assert (
650+
_count_call_function_target(
651+
graph_module, exir_ops.edge.aten.permute_copy.default
652+
)
653+
== 2
654+
)

backends/arm/vgf/_passes/rewrite_grid_sampler_to_tosa_custom.py

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,24 @@ def _supports_quantized_grid_custom(qparams: QuantArgs) -> bool:
153153
return not qparams.per_channel and qparams.dtype == torch.int8
154154

155155

156+
def _permute_to_nhwc(
157+
graph: torch.fx.Graph,
158+
tensor: torch.fx.Node,
159+
from_node: torch.fx.Node,
160+
) -> torch.fx.Node:
161+
nhwc_tensor = create_node(
162+
graph,
163+
op_target=exir_ops.edge.aten.permute_copy.default,
164+
args=(tensor, list(NHWC_ORDER)),
165+
from_node=from_node,
166+
)
167+
_set_fake_tensor_meta(
168+
nhwc_tensor,
169+
exir_ops.edge.aten.permute_copy.default(tensor.meta["val"], list(NHWC_ORDER)),
170+
)
171+
return nhwc_tensor
172+
173+
156174
class RewriteGridSamplerToTosaCustomPass(ArmPass):
157175
"""Rewrite ``aten.grid_sampler_2d`` nodes to ``tosa.CUSTOM``."""
158176

@@ -374,20 +392,12 @@ def call(self, graph_module):
374392
else None
375393
),
376394
)
377-
nhwc_input = create_node(
395+
nhwc_input = _permute_to_nhwc(
378396
graph_module.graph,
379-
op_target=exir_ops.edge.aten.permute_copy.default,
380-
args=(custom_input, list(NHWC_ORDER)),
381-
from_node=custom_input,
382-
)
383-
_set_fake_tensor_meta(
384-
nhwc_input,
385-
exir_ops.edge.aten.permute_copy.default(
386-
custom_input.meta["val"], list(NHWC_ORDER)
387-
),
397+
custom_input,
398+
custom_input,
388399
)
389-
custom_grid = grid
390-
custom_inputs = [nhwc_input, custom_grid]
400+
custom_inputs = [nhwc_input, grid]
391401
if grid_qparam_constants is not None:
392402
custom_inputs.extend(grid_qparam_constants)
393403
custom_node = create_node(

0 commit comments

Comments
 (0)