Skip to content

Commit cf59749

Browse files
authored
Arm backend: Support sampler-backed VGF grid_sample for RIFE (pytorch#20329)
Add VGF `grid_sample` custom-shader improvements needed by the RIFE optical-flow workload. This stack enables sampler-backed `grid_sample` payloads where supported, adds support for quantised int8/SNORM image payloads, handles `align_corners=True`, and updates VGF test infrastructure so multi-delegate exports can be validated. Changes - Add sampler/image payload selection for VGF `grid_sample`. - Keep unsupported cases on the existing storage-buffer fallback path. - Allow safe C3-to-C4 padding so RIFE-style 3-channel inputs can use sampler-compatible image payloads. - Add int8/SNORM image payload support for quantised `grid_sample`. - Keep grid coordinates in float for range and precision. - Add `align_corners=True` sampler shader variants. - Allow VGF tests to check an expected delegate count instead of assuming one delegate. - Add focused tests for sampler, int8/SNORM, fallback, and aligned shader selection. cc @digantdesai @freddan80 @per @zingo @oscarandersson8218 @mansnils @Sebastian-Larsson @robell @rascani --------- Signed-off-by: Baris Demir <baris.demir@arm.com>
1 parent 21a0191 commit cf59749

20 files changed

Lines changed: 893 additions & 62 deletions

backends/arm/TARGETS

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,14 @@ runtime.python_library(
100100
resources = [
101101
"vgf/shaders/grid_sampler.glsl",
102102
"vgf/shaders/grid_sampler.spirv.b64",
103+
"vgf/shaders/grid_sampler_sampler.glsl",
104+
"vgf/shaders/grid_sampler_sampler.spirv.b64",
105+
"vgf/shaders/grid_sampler_sampler_align_corners.glsl",
106+
"vgf/shaders/grid_sampler_sampler_align_corners.spirv.b64",
107+
"vgf/shaders/grid_sampler_sampler_int8.glsl",
108+
"vgf/shaders/grid_sampler_sampler_int8.spirv.b64",
109+
"vgf/shaders/grid_sampler_sampler_int8_align_corners.glsl",
110+
"vgf/shaders/grid_sampler_sampler_int8_align_corners.spirv.b64",
103111
],
104112
deps = [
105113
":arm_compile_spec",

backends/arm/operators/op_tosa_custom.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ def _vk_format_component_count(vk_format: str) -> int:
4040
"VK_FORMAT_R32G32_SFLOAT": 2,
4141
"VK_FORMAT_R8G8B8A8_UINT": 4,
4242
"VK_FORMAT_R8G8B8A8_SINT": 4,
43+
"VK_FORMAT_R8G8B8A8_SNORM": 4,
4344
"VK_FORMAT_R16G16B16A16_UINT": 4,
4445
"VK_FORMAT_R16G16B16A16_SINT": 4,
4546
"VK_FORMAT_R16G16B16A16_SFLOAT": 4,

backends/arm/quantizer/quantization_annotator.py

Lines changed: 63 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,8 @@ def __init__(self):
8484
class _QParams(NamedTuple):
8585
scale: float
8686
zero_point: int
87+
quant_min: int | None = None
88+
quant_max: int | None = None
8789

8890

8991
def _as_list(x):
@@ -472,8 +474,53 @@ def _match_pattern(
472474
8: _QParams((0.999 - (-0.999)) / (1 << 8), 0),
473475
16: _QParams((0.99999 - (-0.99999)) / (1 << 16), 0),
474476
},
477+
# grid_sampler image input/output use SNORM-compatible qparams. The grid
478+
# coordinate tensor is intentionally left unquantized.
479+
torch.ops.aten.grid_sampler.default: {
480+
8: _QParams(1.0 / 127.0, 0, -127, 127),
481+
},
482+
}
483+
484+
485+
_fixed_output_qspec_ops: dict[Any, dict[int, _QParams]] = {
486+
torch.ops.aten.grid_sampler.default: {
487+
8: _QParams(1.0 / 127.0, 0, -127, 127),
488+
},
475489
}
476490

491+
492+
def _get_fixed_qparams_qspec(
493+
node_target: Any,
494+
qparams_table: dict[Any, dict[int, _QParams]],
495+
input_act_qspec: QuantizationSpecBase,
496+
) -> FixedQParamsQuantizationSpec | None:
497+
if not isinstance(input_act_qspec, QuantizationSpec):
498+
raise ValueError("Fixed qparams require a QuantizationSpec input.")
499+
500+
num_bits = torch.iinfo(input_act_qspec.dtype).bits
501+
qparams = qparams_table[node_target].get(num_bits)
502+
if qparams is None:
503+
return None
504+
505+
return FixedQParamsQuantizationSpec(
506+
dtype=input_act_qspec.dtype,
507+
scale=qparams.scale,
508+
zero_point=qparams.zero_point,
509+
quant_min=(
510+
input_act_qspec.quant_min
511+
if qparams.quant_min is None
512+
else qparams.quant_min
513+
),
514+
quant_max=(
515+
input_act_qspec.quant_max
516+
if qparams.quant_max is None
517+
else qparams.quant_max
518+
),
519+
qscheme=input_act_qspec.qscheme,
520+
is_dynamic=input_act_qspec.is_dynamic,
521+
)
522+
523+
477524
_one_to_one: set[OpOverload] = {
478525
torch.ops.aten.abs.default,
479526
torch.ops.aten.ceil.default,
@@ -762,6 +809,16 @@ def any_or_hardtanh_min_zero(n: Node):
762809
_QuantProperty(1, input_act_qspec),
763810
]
764811
quant_properties.quant_output = _QuantProperty(0, output_act_qspec)
812+
elif node.target == torch.ops.aten.grid_sampler.default:
813+
image_node = ensure_type(Node, node.args[0])
814+
grid_sampler_image_qspec = quantization_config.get_input_act_qspec(
815+
node, image_node
816+
)
817+
grid_sampler_output_qspec = quantization_config.get_output_act_qspec(node)
818+
if grid_sampler_image_qspec is None or grid_sampler_output_qspec is None:
819+
return None
820+
quant_properties.quant_inputs = [_QuantProperty(0, grid_sampler_image_qspec)]
821+
quant_properties.quant_output = _QuantProperty(0, grid_sampler_output_qspec)
765822
elif node.target in (torch.ops.aten.where.self,):
766823
true_node = ensure_type(Node, node.args[1])
767824
input_qspec = (
@@ -825,21 +882,15 @@ def any_or_hardtanh_min_zero(n: Node):
825882
quant_properties.quant_inputs = [_QuantProperty(0, input_act_qspec)]
826883
quant_properties.quant_output = _QuantProperty(0, output_act_qspec)
827884
elif node.target in _fixed_input_qspec_ops:
828-
num_bits = torch.iinfo(input_act_qspec.dtype).bits
829-
qparams = _fixed_input_qspec_ops[node.target][num_bits]
830-
885+
fixed_input_qspec = _get_fixed_qparams_qspec(
886+
node.target, _fixed_input_qspec_ops, input_act_qspec
887+
)
888+
if fixed_input_qspec is None:
889+
return None
831890
quant_properties.quant_inputs = [
832891
_QuantProperty(
833892
0,
834-
FixedQParamsQuantizationSpec(
835-
dtype=input_act_qspec.dtype,
836-
scale=qparams.scale,
837-
zero_point=qparams.zero_point,
838-
quant_min=input_act_qspec.quant_min,
839-
quant_max=input_act_qspec.quant_max,
840-
qscheme=input_act_qspec.qscheme,
841-
is_dynamic=input_act_qspec.is_dynamic,
842-
),
893+
fixed_input_qspec,
843894
)
844895
]
845896
quant_properties.quant_output = _QuantProperty(0, output_act_qspec)

backends/arm/quantizer/quantization_config.py

Lines changed: 23 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121

2222
from torchao.quantization.pt2e.quantizer import (
2323
DerivedQuantizationSpec,
24-
FixedQParamsQuantizationSpec,
2524
QuantizationSpec,
2625
QuantizationSpecBase,
2726
SharedQuantizationSpec,
@@ -295,6 +294,7 @@ def get_input_act_qspec(self, node=None, input_node=None):
295294
# MLETORCH-1853: Fix lazy import when moving files around
296295
from executorch.backends.arm.quantizer.quantization_annotator import (
297296
_fixed_input_qspec_ops,
297+
_get_fixed_qparams_qspec,
298298
)
299299

300300
if node is None or input_node is None:
@@ -305,28 +305,17 @@ def get_input_act_qspec(self, node=None, input_node=None):
305305
return super().get_input_act_qspec(node, input_node)
306306
else:
307307
return SharedQuantizationSpec((node.args[0], node))
308+
elif node.target == torch.ops.aten.grid_sampler.default:
309+
if input_node != node.args[0]:
310+
return None
311+
input_act_qspec = super().get_input_act_qspec(node, input_node)
312+
return _get_fixed_qparams_qspec(
313+
node.target, _fixed_input_qspec_ops, input_act_qspec
314+
)
308315
elif node.target in _fixed_input_qspec_ops:
309-
310316
input_act_qspec = super().get_input_act_qspec(node, input_node)
311-
if not hasattr(input_act_qspec, "dtype") or not isinstance(
312-
input_act_qspec.dtype, torch.dtype
313-
):
314-
raise ValueError(
315-
f"{node.target} requires an input activation quantization "
316-
"spec to use fixed input qparams."
317-
)
318-
dtype = getattr(input_act_qspec, "dtype", None)
319-
num_bits = torch.iinfo(dtype).bits
320-
321-
qparams = _fixed_input_qspec_ops[node.target][num_bits]
322-
return FixedQParamsQuantizationSpec(
323-
dtype=dtype,
324-
scale=qparams.scale,
325-
zero_point=qparams.zero_point,
326-
quant_min=input_act_qspec.quant_min,
327-
quant_max=input_act_qspec.quant_max,
328-
qscheme=input_act_qspec.qscheme,
329-
is_dynamic=input_act_qspec.is_dynamic,
317+
return _get_fixed_qparams_qspec(
318+
node.target, _fixed_input_qspec_ops, input_act_qspec
330319
)
331320

332321
return super().get_input_act_qspec(node, input_node)
@@ -371,6 +360,19 @@ def get_output_act_qspec(
371360

372361
if node is None:
373362
return super().get_output_act_qspec()
363+
# MLETORCH-1853: Fix lazy import when moving files around
364+
from executorch.backends.arm.quantizer.quantization_annotator import (
365+
_fixed_output_qspec_ops,
366+
_get_fixed_qparams_qspec,
367+
)
368+
369+
if node.target in _fixed_output_qspec_ops:
370+
output_act_qspec = super().get_output_act_qspec(node)
371+
if output_act_qspec is None:
372+
return None
373+
return _get_fixed_qparams_qspec(
374+
node.target, _fixed_output_qspec_ops, output_act_qspec
375+
)
374376
if node.target not in self.SHARED_OUTPUT_ACT_QSPEC_PATTERNS:
375377
return super().get_output_act_qspec()
376378
if len(node.args) == 0:

backends/arm/quantizer/quantizer_support.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,7 @@ def check_pattern(cls, pattern):
174174
(torch.ops.aten.acos.default,),
175175
(torch.ops.aten.atanh.default,),
176176
(torch.ops.aten.einsum.default,),
177+
(torch.ops.aten.grid_sampler.default,),
177178
]
178179
)
179180
TOSA_QUANTIZER_SUPPORT_DICT: dict[tuple[OpOverload, ...], type[PatternCheck] | None] = {

backends/arm/scripts/generate_grid_sampler_spirv.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,13 @@ def main() -> None:
6565
with tempfile.TemporaryDirectory() as tmpdir:
6666
spirv_path = Path(tmpdir) / "grid_sampler.spirv"
6767
subprocess.run( # nosec B603 - glslc path is resolved explicitly.
68-
[glslc, str(args.source), "-o", str(spirv_path)],
68+
[
69+
glslc,
70+
"-fshader-stage=compute",
71+
str(args.source),
72+
"-o",
73+
str(spirv_path),
74+
],
6975
check=True,
7076
)
7177
_write_base64_spirv(spirv_path, args.output)

0 commit comments

Comments
 (0)