Skip to content

Commit 2b8cc80

Browse files
authored
Arm backend: Dequantize quantized grid sample coordinates (#20962)
Arm backend: Dequantize quantized grid sample coordinates Functional progress for quantized grid sample to enable the runtime codegen to handle de-q into samplers in shaders. Signed-off-by: Rob Elliott <Robert.Elliott@arm.com> Change-Id: Ibe61128651763aa2b83d9bf187b656fe11487aed cc @digantdesai @freddan80 @per @zingo @oscarandersson8218 @mansnils @Sebastian-Larsson @rascani --------- Signed-off-by: Rob Elliott <Robert.Elliott@arm.com>
1 parent 39f4355 commit 2b8cc80

11 files changed

Lines changed: 370 additions & 61 deletions

backends/arm/TARGETS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ runtime.python_library(
8888
srcs = [
8989
"vgf/__init__.py",
9090
"vgf/_passes/__init__.py",
91+
"vgf/_passes/insert_grid_sampler_grid_dequant_pass.py",
9192
"vgf/_passes/rewrite_grid_sampler_to_tosa_custom.py",
9293
"vgf/backend.py",
9394
"vgf/check_env.py",

backends/arm/quantizer/arm_quantizer.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from executorch.backends.arm.quantizer.quantization_config import (
2424
QuantizationConfig,
2525
TOSAQuantizationConfig,
26+
VGFQuantizationConfig,
2627
)
2728
from executorch.backends.arm.quantizer.quantizer_support import (
2829
TOSA_QUANTIZER_SUPPORT_DICT,
@@ -107,6 +108,24 @@
107108
logger = logging.getLogger(__name__)
108109

109110

111+
def _wrap_vgf_quantization_config(
112+
quantization_config: Optional[QuantizationConfig],
113+
) -> Optional[QuantizationConfig]:
114+
if (
115+
quantization_config is None
116+
or isinstance(quantization_config, VGFQuantizationConfig)
117+
or not isinstance(quantization_config, TOSAQuantizationConfig)
118+
):
119+
return quantization_config
120+
return VGFQuantizationConfig(
121+
quantization_config.input_activation,
122+
quantization_config.output_activation,
123+
quantization_config.weight,
124+
quantization_config.bias,
125+
quantization_config.label,
126+
)
127+
128+
110129
def get_cond_while_submodules_ao(
111130
graph_module: GraphModule,
112131
apply_quantization: bool = False,
@@ -1404,3 +1423,31 @@ def __init__(
14041423
use_composable_quantizer: bool = True,
14051424
) -> None:
14061425
super().__init__(compile_spec, use_composable_quantizer)
1426+
1427+
def set_global(
1428+
self, quantization_config: Optional[QuantizationConfig]
1429+
) -> TOSAQuantizer:
1430+
"""Set the global quantization config for VGF lowering."""
1431+
return super().set_global(_wrap_vgf_quantization_config(quantization_config))
1432+
1433+
def set_module_type(
1434+
self, module_type: Callable, quantization_config: Optional[QuantizationConfig]
1435+
) -> TOSAQuantizer:
1436+
"""Set the quantization config for a specific module type."""
1437+
return super().set_module_type(
1438+
module_type, _wrap_vgf_quantization_config(quantization_config)
1439+
)
1440+
1441+
def set_module_name(
1442+
self, module_name: str, quantization_config: Optional[QuantizationConfig]
1443+
) -> TOSAQuantizer:
1444+
"""Set the quantization config for a specific module name."""
1445+
return super().set_module_name(
1446+
module_name, _wrap_vgf_quantization_config(quantization_config)
1447+
)
1448+
1449+
def set_io(
1450+
self, quantization_config: Optional[QuantizationConfig]
1451+
) -> TOSAQuantizer:
1452+
"""Set the quantization config used for model inputs and outputs."""
1453+
return super().set_io(_wrap_vgf_quantization_config(quantization_config))

backends/arm/quantizer/quantization_annotator.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -474,8 +474,11 @@ def _match_pattern(
474474
8: _QParams((0.999 - (-0.999)) / (1 << 8), 0),
475475
16: _QParams((0.99999 - (-0.99999)) / (1 << 16), 0),
476476
},
477-
# grid_sampler image input/output use SNORM-compatible qparams. The grid
478-
# coordinate tensor is intentionally left unquantized.
477+
# grid_sampler image input/output use SNORM-compatible qparams. The broader
478+
# quantized graph currently quantizes the grid-producing path as well, so
479+
# input 1 follows the standard activation qspec and lowering materializes a
480+
# dequant boundary before the shader. This is a functional stopgap; we may
481+
# want to preserve float grid coordinates or use a higher-precision path.
479482
torch.ops.aten.grid_sampler.default: {
480483
8: _QParams(1.0 / 127.0, 0, -127, 127),
481484
},
@@ -824,13 +827,21 @@ def any_or_hardtanh_min_zero(n: Node):
824827
quant_properties.quant_output = _QuantProperty(0, output_act_qspec)
825828
elif node.target == torch.ops.aten.grid_sampler.default:
826829
image_node = ensure_type(Node, node.args[0])
830+
grid_node = ensure_type(Node, node.args[1])
827831
grid_sampler_image_qspec = quantization_config.get_input_act_qspec(
828832
node, image_node
829833
)
834+
grid_sampler_grid_qspec = quantization_config.get_input_act_qspec(
835+
node, grid_node
836+
)
830837
grid_sampler_output_qspec = quantization_config.get_output_act_qspec(node)
831838
if grid_sampler_image_qspec is None or grid_sampler_output_qspec is None:
832839
return None
833840
quant_properties.quant_inputs = [_QuantProperty(0, grid_sampler_image_qspec)]
841+
if grid_sampler_grid_qspec is not None:
842+
quant_properties.quant_inputs.append(
843+
_QuantProperty(1, grid_sampler_grid_qspec)
844+
)
834845
quant_properties.quant_output = _QuantProperty(0, grid_sampler_output_qspec)
835846
elif node.target in (torch.ops.aten.where.self,):
836847
true_node = ensure_type(Node, node.args[1])

backends/arm/quantizer/quantization_config.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,8 @@ def _derive_qparams_fn(
259259
class TOSAQuantizationConfig(QuantizationConfig):
260260
"""Configures quantization, while enforcing TOSA specific constraints."""
261261

262+
quantize_grid_sampler_grid = False
263+
262264
SHARED_OUTPUT_ACT_QSPEC_PATTERNS = {
263265
torch.ops.aten.adaptive_avg_pool2d.default,
264266
torch.ops.aten.upsample_bilinear2d.vec,
@@ -307,12 +309,14 @@ def get_input_act_qspec(self, node=None, input_node=None):
307309
else:
308310
return SharedQuantizationSpec((node.args[0], node))
309311
elif node.target == torch.ops.aten.grid_sampler.default:
310-
if input_node != node.args[0]:
312+
if input_node == node.args[0]:
313+
input_act_qspec = super().get_input_act_qspec(node, input_node)
314+
return _get_fixed_qparams_qspec(
315+
node.target, _fixed_input_qspec_ops, input_act_qspec
316+
)
317+
if not self.quantize_grid_sampler_grid:
311318
return None
312-
input_act_qspec = super().get_input_act_qspec(node, input_node)
313-
return _get_fixed_qparams_qspec(
314-
node.target, _fixed_input_qspec_ops, input_act_qspec
315-
)
319+
return super().get_input_act_qspec(node, input_node)
316320
elif node.target in _fixed_input_qspec_ops:
317321
input_act_qspec = super().get_input_act_qspec(node, input_node)
318322
return _get_fixed_qparams_qspec(
@@ -414,3 +418,7 @@ def get_output_act_qspec(
414418
if len(node.args) == 0:
415419
return super().get_output_act_qspec()
416420
return SharedQuantizationSpec((cast(Node, node.args[0]), node))
421+
422+
423+
class VGFQuantizationConfig(TOSAQuantizationConfig):
424+
quantize_grid_sampler_grid = True

backends/arm/test/passes/test_rewrite_grid_sampler_to_tosa_custom_pass.py

Lines changed: 62 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,18 @@
88
import torch
99
import torch.nn.functional as F
1010
from executorch.backends.arm._passes import FoldAndAnnotateQParamsPass
11+
from executorch.backends.arm._passes.quant_args import QuantArgs
1112
from executorch.backends.arm.quantizer.arm_quantizer import (
1213
get_symmetric_quantization_config,
13-
TOSAQuantizer,
14+
VgfQuantizer,
1415
)
1516
from executorch.backends.arm.tosa.specification import (
1617
TosaLoweringContext,
1718
TosaSpecification,
1819
)
19-
from executorch.backends.arm.vgf._passes.rewrite_grid_sampler_to_tosa_custom import (
20+
from executorch.backends.arm.vgf import VgfCompileSpec
21+
from executorch.backends.arm.vgf._passes import (
22+
InsertGridSamplerGridDequantPass,
2023
RewriteGridSamplerToTosaCustomPass,
2124
)
2225
from executorch.backends.arm.vgf.shaders.grid_sampler import (
@@ -171,8 +174,8 @@ def test_quantized_grid_sampler_uses_int8_sampler_payload(
171174
torch.randn(1, channels, 8, 8),
172175
torch.rand(1, 4, 4, 2),
173176
)
174-
quantizer = TOSAQuantizer(
175-
TosaSpecification.create_from_string("TOSA-1.0+INT"),
177+
quantizer = VgfQuantizer(
178+
VgfCompileSpec("TOSA-1.0+INT"),
176179
use_composable_quantizer=use_composable_quantizer,
177180
)
178181
quantizer.set_global(get_symmetric_quantization_config(is_per_channel=False))
@@ -185,27 +188,81 @@ def test_quantized_grid_sampler_uses_int8_sampler_payload(
185188
edge_model = to_edge(export(converted, example_inputs, strict=True))
186189
with TosaLoweringContext(TosaSpecification.create_from_string("TOSA-1.0+FP+INT")):
187190
edge_model = edge_model.transform(
188-
[FoldAndAnnotateQParamsPass(), RewriteGridSamplerToTosaCustomPass()]
191+
[
192+
FoldAndAnnotateQParamsPass(),
193+
InsertGridSamplerGridDequantPass(),
194+
RewriteGridSamplerToTosaCustomPass(),
195+
]
189196
)
190197
nodes = list(edge_model.exported_program().graph.nodes)
191198

192199
custom_node = next(
193200
node for node in nodes if node.target == exir_ops.backend.tosa.CUSTOM.default
194201
)
195202
payload = decode_payload(custom_node.kwargs["implementation_attrs"])
203+
grid_input = custom_node.args[0][1]
196204

197205
assert payload["input_0_type"] == "Image"
198206
assert payload["input_0_vkformat"] == GRID_SAMPLER_2D_SAMPLER_INT8_VK_FORMAT
199207
assert payload["input_1_type"] == "Tensor"
200208
assert payload["input_1_vkformat"] == GRID_SAMPLER_2D_VK_FORMAT
201209
assert payload["output_0_type"] == "Image"
202210
assert payload["output_0_vkformat"] == GRID_SAMPLER_2D_SAMPLER_INT8_VK_FORMAT
211+
assert grid_input.meta["val"].dtype == torch.float32
212+
assert grid_input.target in (
213+
exir_ops.edge.quantized_decomposed.dequantize_per_tensor.default,
214+
exir_ops.edge.quantized_decomposed.dequantize_per_channel.default,
215+
)
203216
assert custom_node.meta["input_qparams"][0].qmin == -127
204217
assert custom_node.meta["input_qparams"][0].qmax == 127
218+
assert 1 not in custom_node.meta["input_qparams"]
205219
assert next(iter(custom_node.meta["output_qparams"].values())).qmin == -127
206220
assert next(iter(custom_node.meta["output_qparams"].values())).qmax == 127
207221

208222

223+
def test_quantized_grid_sampler_rejects_dequantized_grid_with_int8_image_payload():
224+
model = GridSampler2d().eval()
225+
example_inputs = (
226+
torch.randn(1, 4, 8, 8),
227+
torch.rand(1, 4, 4, 2),
228+
)
229+
quantizer = VgfQuantizer(VgfCompileSpec("TOSA-1.0+INT"))
230+
quantizer.set_global(get_symmetric_quantization_config(is_per_channel=False))
231+
232+
exported = export(model, example_inputs, strict=True)
233+
prepared = prepare_pt2e(exported.module(), quantizer)
234+
prepared(*example_inputs)
235+
converted = convert_pt2e(prepared)
236+
237+
edge_model = to_edge(export(converted, example_inputs, strict=True))
238+
with TosaLoweringContext(TosaSpecification.create_from_string("TOSA-1.0+FP+INT")):
239+
edge_model = edge_model.transform([FoldAndAnnotateQParamsPass()])
240+
241+
exported_program = edge_model.exported_program()
242+
grid_sampler_node = next(
243+
node
244+
for node in exported_program.graph.nodes
245+
if node.target == exir_ops.edge.aten.grid_sampler_2d.default
246+
)
247+
grid_node = grid_sampler_node.args[1]
248+
grid_node.meta["val"] = torch.zeros_like(grid_node.meta["val"], dtype=torch.int8)
249+
grid_sampler_node.meta["input_qparams"][1] = QuantArgs(
250+
scale=[0.01, 0.01],
251+
zp=[0, 0],
252+
qmin=-128,
253+
qmax=127,
254+
dtype=torch.int8,
255+
axis=3,
256+
per_channel=True,
257+
)
258+
259+
with pytest.raises(
260+
RuntimeError,
261+
match="grid_sampler grid dequant only supports per-tensor qparams",
262+
):
263+
InsertGridSamplerGridDequantPass()(exported_program.graph_module)
264+
265+
209266
def test_rewrite_grid_sampler_to_tosa_custom_c3_pad_for_align_corners():
210267
model = GridSampler2d()
211268
model.align_corners_ = True

backends/arm/test/quantizer/test_generic_annotater.py

Lines changed: 108 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright 2024-2025 Arm Limited and/or its affiliates.
1+
# Copyright 2024-2026 Arm Limited and/or its affiliates.
22
#
33
# This source code is licensed under the BSD-style license found in the
44
# LICENSE file in the root directory of this source tree.
@@ -7,11 +7,20 @@
77
from typing import Any, Callable, Tuple
88

99
import torch
10+
import torch.nn.functional as F
1011
from executorch.backends.arm.quantizer import is_annotated
12+
from executorch.backends.arm.quantizer.arm_quantizer import (
13+
get_symmetric_quantization_config,
14+
VgfQuantizer,
15+
)
16+
from executorch.backends.arm.quantizer.quantization_annotator import annotate_graph
1117
from executorch.backends.arm.test.tester.test_pipeline import TosaPipelineINT
18+
from executorch.backends.arm.vgf import VgfCompileSpec
1219
from executorch.backends.test.harness.stages import StageType
1320

21+
from torch.export import export
1422
from torch.fx.passes.utils.source_matcher_utils import get_source_partitions
23+
from torchao.quantization.pt2e.quantizer.quantizer import Q_ANNOTATION_KEY
1524

1625

1726
input_t1 = Tuple[torch.Tensor] # Input x
@@ -142,3 +151,101 @@ def test_concat_tosa_INT():
142151
torch.concatenate, ((torch.randn(2, 3), torch.randn(2, 3)),), dim=0
143152
),
144153
)
154+
155+
156+
class GridSampleModule(torch.nn.Module):
157+
def forward(self, x: torch.Tensor, grid: torch.Tensor) -> torch.Tensor:
158+
return F.grid_sample(
159+
x,
160+
grid,
161+
mode="bilinear",
162+
padding_mode="zeros",
163+
align_corners=False,
164+
)
165+
166+
167+
class GridFloatQuantizationConfig:
168+
def __init__(self) -> None:
169+
self.base = get_symmetric_quantization_config()
170+
171+
def get_input_act_qspec(self, node=None, input_node=None):
172+
if (
173+
node is not None
174+
and input_node is not None
175+
and node.target == torch.ops.aten.grid_sampler.default
176+
and input_node == node.args[1]
177+
):
178+
return None
179+
return self.base.get_input_act_qspec(node, input_node)
180+
181+
def get_output_act_qspec(self, node=None):
182+
return self.base.get_output_act_qspec(node)
183+
184+
def get_weight_qspec(self, node=None):
185+
return self.base.get_weight_qspec(node)
186+
187+
def get_bias_qspec(self, node=None):
188+
return self.base.get_bias_qspec(node)
189+
190+
191+
def test_grid_sampler_annotation_keeps_float_grid_when_grid_qspec_is_none():
192+
module = GridSampleModule().eval()
193+
example_inputs = (torch.randn(1, 4, 8, 8), torch.randn(1, 4, 4, 2))
194+
gm = export(module, example_inputs).graph_module
195+
196+
annotate_graph(gm, GridFloatQuantizationConfig())
197+
198+
grid_sampler_node = next(
199+
node
200+
for node in gm.graph.nodes
201+
if node.op == "call_function"
202+
and node.target == torch.ops.aten.grid_sampler.default
203+
)
204+
image_node = grid_sampler_node.args[0]
205+
grid_node = grid_sampler_node.args[1]
206+
annotation = grid_sampler_node.meta[Q_ANNOTATION_KEY]
207+
208+
assert is_annotated(grid_sampler_node)
209+
assert image_node in annotation.input_qspec_map
210+
assert grid_node not in annotation.input_qspec_map
211+
assert annotation.output_qspec is not None
212+
213+
214+
def test_grid_sampler_annotation_keeps_default_tosa_grid_float():
215+
module = GridSampleModule().eval()
216+
example_inputs = (torch.randn(1, 4, 8, 8), torch.randn(1, 4, 4, 2))
217+
gm = export(module, example_inputs).graph_module
218+
219+
annotate_graph(gm, get_symmetric_quantization_config())
220+
221+
grid_sampler_node = next(
222+
node
223+
for node in gm.graph.nodes
224+
if node.op == "call_function"
225+
and node.target == torch.ops.aten.grid_sampler.default
226+
)
227+
grid_node = grid_sampler_node.args[1]
228+
annotation = grid_sampler_node.meta[Q_ANNOTATION_KEY]
229+
230+
assert grid_node not in annotation.input_qspec_map
231+
232+
233+
def test_vgf_quantizer_quantizes_grid_sampler_grid_coords():
234+
module = GridSampleModule().eval()
235+
example_inputs = (torch.randn(1, 4, 8, 8), torch.randn(1, 4, 4, 2))
236+
gm = export(module, example_inputs).graph_module
237+
238+
quantizer = VgfQuantizer(VgfCompileSpec("TOSA-1.0+INT"))
239+
quantizer.set_global(get_symmetric_quantization_config())
240+
quantizer.annotate(gm)
241+
242+
grid_sampler_node = next(
243+
node
244+
for node in gm.graph.nodes
245+
if node.op == "call_function"
246+
and node.target == torch.ops.aten.grid_sampler.default
247+
)
248+
grid_node = grid_sampler_node.args[1]
249+
annotation = grid_sampler_node.meta[Q_ANNOTATION_KEY]
250+
251+
assert grid_node in annotation.input_qspec_map

backends/arm/vgf/_passes/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
# This source code is licensed under the BSD-style license found in the
44
# LICENSE file in the root directory of this source tree.
55

6+
from .insert_grid_sampler_grid_dequant_pass import ( # noqa
7+
InsertGridSamplerGridDequantPass,
8+
)
69
from .rewrite_grid_sampler_to_tosa_custom import ( # noqa
710
RewriteGridSamplerToTosaCustomPass,
811
)

0 commit comments

Comments
 (0)