From 815640fd0c203d1f9fbaecca56e0dd6f0d452b8d Mon Sep 17 00:00:00 2001 From: Rob Elliott Date: Tue, 7 Jul 2026 13:38:34 +0100 Subject: [PATCH] Arm backend: Handle grid_sample qparams in shader Signed-off-by: Rob Elliott Change-Id: I53c143e323827ec4f280006bf34fcec3093af62d Signed-off-by: Rob Elliott --- backends/arm/_passes/arm_pass_manager.py | 5 +- .../arm/quantizer/quantization_annotator.py | 11 +- .../test/misc/test_custom_shader_payload.py | 56 +++++++- ...ewrite_grid_sampler_to_tosa_custom_pass.py | 128 ++++++++++++++++-- .../insert_grid_sampler_grid_dequant_pass.py | 64 ++++++++- .../rewrite_grid_sampler_to_tosa_custom.py | 125 ++++++++++++++++- backends/arm/vgf/shaders/grid_sampler.py | 43 +++++- .../shaders/grid_sampler_sampler_int8.glsl | 17 ++- .../grid_sampler_sampler_int8.spirv.b64 | 2 +- ...id_sampler_sampler_int8_align_corners.glsl | 17 ++- ...mpler_sampler_int8_align_corners.spirv.b64 | 2 +- 11 files changed, 430 insertions(+), 40 deletions(-) diff --git a/backends/arm/_passes/arm_pass_manager.py b/backends/arm/_passes/arm_pass_manager.py index 0066d78d313..32743c6f15c 100644 --- a/backends/arm/_passes/arm_pass_manager.py +++ b/backends/arm/_passes/arm_pass_manager.py @@ -217,8 +217,9 @@ def __init__(self, graph_pass: Callable[[GraphModule], PassResult | None]) -> No def call(self, exported_program: ExportedProgram) -> ExportedProgramPassResult: graph_pass = cast(Any, self.graph_pass) + has_exported_program_attr = hasattr(graph_pass, "exported_program") pass_exported_program = getattr(graph_pass, "exported_program", None) - if pass_exported_program is not None: + if has_exported_program_attr: # ExportedProgramPassManager works on a shallow copy; Arm graph # passes that store an ExportedProgram must update that copy. graph_pass.exported_program = exported_program @@ -226,7 +227,7 @@ def call(self, exported_program: ExportedProgram) -> ExportedProgramPassResult: try: result = self.graph_pass(exported_program.graph_module) finally: - if pass_exported_program is not None: + if has_exported_program_attr: graph_pass.exported_program = pass_exported_program if result is None: diff --git a/backends/arm/quantizer/quantization_annotator.py b/backends/arm/quantizer/quantization_annotator.py index 14d13236ee7..4a699e9f3eb 100644 --- a/backends/arm/quantizer/quantization_annotator.py +++ b/backends/arm/quantizer/quantization_annotator.py @@ -474,11 +474,12 @@ def _match_pattern( 8: _QParams((0.999 - (-0.999)) / (1 << 8), 0), 16: _QParams((0.99999 - (-0.99999)) / (1 << 16), 0), }, - # grid_sampler image input/output use SNORM-compatible qparams. The broader - # quantized graph currently quantizes the grid-producing path as well, so - # input 1 follows the standard activation qspec and lowering materializes a - # dequant boundary before the shader. This is a functional stopgap; we may - # want to preserve float grid coordinates or use a higher-precision path. + # grid_sampler image input/output use SNORM-compatible qparams. Input 1 + # follows the standard activation qspec, but the supported VGF lowering + # modes are still only: + # - float image / float grid / float output + # - int8 image / int8 grid / int8 output + # Mixed int8-image / float-grid shader lowering is not supported. torch.ops.aten.grid_sampler.default: { 8: _QParams(1.0 / 127.0, 0, -127, 127), }, diff --git a/backends/arm/test/misc/test_custom_shader_payload.py b/backends/arm/test/misc/test_custom_shader_payload.py index 2d1c3685bfd..fb66b8e0342 100644 --- a/backends/arm/test/misc/test_custom_shader_payload.py +++ b/backends/arm/test/misc/test_custom_shader_payload.py @@ -12,6 +12,7 @@ build_grid_sampler_2d_payload, decode_payload, encode_payload, + GRID_SAMPLER_2D_QUANTIZED_GRID_VK_FORMAT, GRID_SAMPLER_2D_SAMPLER_ALIGN_CORNERS_SHADER_BINARY, GRID_SAMPLER_2D_SAMPLER_ALIGN_CORNERS_SHADER_SOURCE, GRID_SAMPLER_2D_SAMPLER_INT8_ALIGN_CORNERS_SHADER_BINARY, @@ -109,6 +110,8 @@ def test_grid_sampler_2d_custom_shader_payload_no_target_uses_int8_sampler_for_c output_shape=(1, 4, 4, 4), input_dtype=torch.int8, output_dtype=torch.int8, + grid_dtype=torch.int8, + extra_tensor_input_vkformats=["VK_FORMAT_R32_SFLOAT", "VK_FORMAT_R32_SINT"], ) assert payload["shader_language"] == GRID_SAMPLER_2D_SHADER_LANGUAGE @@ -121,13 +124,46 @@ def test_grid_sampler_2d_custom_shader_payload_no_target_uses_int8_sampler_for_c == "VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER" ) assert payload["input_1_type"] == "Tensor" - assert payload["input_1_vkformat"] == GRID_SAMPLER_2D_VK_FORMAT + assert payload["input_1_vkformat"] == GRID_SAMPLER_2D_QUANTIZED_GRID_VK_FORMAT assert payload["input_1_vkdescriptortype"] == "VK_DESCRIPTOR_TYPE_TENSOR_ARM" + assert payload["input_2_type"] == "Tensor" + assert payload["input_2_vkformat"] == "VK_FORMAT_R32_SFLOAT" + assert payload["input_2_binding"] == 3 + assert payload["input_3_type"] == "Tensor" + assert payload["input_3_vkformat"] == "VK_FORMAT_R32_SINT" + assert payload["input_3_binding"] == 4 assert payload["output_0_type"] == "Image" assert payload["output_0_vkformat"] == GRID_SAMPLER_2D_SAMPLER_INT8_VK_FORMAT assert payload["output_0_vkdescriptortype"] == "VK_DESCRIPTOR_TYPE_STORAGE_IMAGE" +def test_grid_sampler_2d_custom_shader_payload_uses_quantized_grid_for_int8_sampler(): + payload = build_grid_sampler_2d_payload( + interpolation_mode=0, + padding_mode=0, + align_corners=False, + input_shape=(1, 4, 8, 8), + output_shape=(1, 4, 4, 4), + input_dtype=torch.int8, + output_dtype=torch.int8, + grid_dtype=torch.int8, + extra_tensor_input_vkformats=["VK_FORMAT_R32_SFLOAT", "VK_FORMAT_R32_SINT"], + ) + + assert payload["input_0_type"] == "Image" + assert payload["input_0_vkformat"] == GRID_SAMPLER_2D_SAMPLER_INT8_VK_FORMAT + assert payload["input_1_type"] == "Tensor" + assert payload["input_1_vkformat"] == GRID_SAMPLER_2D_QUANTIZED_GRID_VK_FORMAT + assert payload["input_1_vkdescriptortype"] == "VK_DESCRIPTOR_TYPE_TENSOR_ARM" + assert payload["input_2_type"] == "Tensor" + assert payload["input_2_vkformat"] == "VK_FORMAT_R32_SFLOAT" + assert payload["input_3_type"] == "Tensor" + assert payload["input_3_vkformat"] == "VK_FORMAT_R32_SINT" + assert payload["output_0_type"] == "Image" + assert payload["output_0_vkformat"] == GRID_SAMPLER_2D_SAMPLER_INT8_VK_FORMAT + assert payload["output_0_binding"] == 2 + + def test_grid_sampler_2d_custom_shader_payload_no_target_keeps_c3_on_buffer(): payload = build_grid_sampler_2d_payload( interpolation_mode=0, @@ -212,6 +248,8 @@ def test_grid_sampler_2d_custom_shader_payload_no_target_int8_align_corners_samp output_shape=(1, 4, 8, 8), input_dtype=torch.int8, output_dtype=torch.int8, + grid_dtype=torch.int8, + extra_tensor_input_vkformats=["VK_FORMAT_R32_SFLOAT", "VK_FORMAT_R32_SINT"], ) assert payload["input_0_type"] == "Image" @@ -228,6 +266,22 @@ def test_grid_sampler_2d_custom_shader_payload_no_target_int8_align_corners_samp ) +def test_grid_sampler_2d_custom_shader_payload_rejects_float_grid_for_int8_sampler(): + with pytest.raises( + ValueError, + match="Int8 sampler grid-sample payload requires an int8 grid", + ): + build_grid_sampler_2d_payload( + interpolation_mode=0, + padding_mode=0, + align_corners=False, + input_shape=(1, 4, 8, 8), + output_shape=(1, 4, 4, 4), + input_dtype=torch.int8, + output_dtype=torch.int8, + ) + + def test_grid_sampler_2d_custom_shader_payload_no_target_bicubic_buffer(): payload = build_grid_sampler_2d_payload( interpolation_mode=2, diff --git a/backends/arm/test/passes/test_rewrite_grid_sampler_to_tosa_custom_pass.py b/backends/arm/test/passes/test_rewrite_grid_sampler_to_tosa_custom_pass.py index 578f4427752..5f2b3813c89 100644 --- a/backends/arm/test/passes/test_rewrite_grid_sampler_to_tosa_custom_pass.py +++ b/backends/arm/test/passes/test_rewrite_grid_sampler_to_tosa_custom_pass.py @@ -26,6 +26,7 @@ CUSTOM_SHADER_DOMAIN_NAME, decode_payload, grid_sampler_2d_operator_name, + GRID_SAMPLER_2D_QUANTIZED_GRID_VK_FORMAT, GRID_SAMPLER_2D_SAMPLER_INT8_VK_FORMAT, GRID_SAMPLER_2D_SAMPLER_VK_FORMAT, GRID_SAMPLER_2D_SHADER_ENTRY_POINT, @@ -34,6 +35,7 @@ ) from executorch.exir import to_edge from executorch.exir.dialects._ops import ops as exir_ops +from executorch.exir.pass_base import ExportedProgramPassBase, ExportedProgramPassResult from torch.export import export from torchao.quantization.pt2e.quantize_pt2e import convert_pt2e, prepare_pt2e @@ -56,6 +58,43 @@ def forward(self, x, grid): ) +class _RewriteGridSamplerToTosaCustomExportPass(ExportedProgramPassBase): + # The quantized grid-sampler rewrite materializes exported constant + # placeholders for grid scale/zero-point, so it needs ExportedProgram + # context. Production VGF lowering injects that context via the Arm pass + # manager adapter, but this unit test drives the pass through the generic + # EXIR transform path instead. Wrap the graph pass so the test exercises + # the same rewrite logic without depending on the Arm-specific adapter. + def call(self, exported_program): + rewrite_pass = RewriteGridSamplerToTosaCustomPass(exported_program) + result = rewrite_pass(exported_program.graph_module) + exported_program._graph_module = result.graph_module + return ExportedProgramPassResult(exported_program, result.modified) + + +def test_get_first_user_input_placeholder_accepts_renamed_placeholder_node(): + model = GridSampler2d() + example_inputs = ( + torch.randn(1, 3, 8, 8), + torch.randn(1, 4, 4, 2), + ) + + exported_program = to_edge(export(model, example_inputs)).exported_program() + first_placeholder = next( + node for node in exported_program.graph.nodes if node.op == "placeholder" + ) + original_target = first_placeholder.target + first_placeholder.name = f"{original_target}_renamed" + + rewrite_pass = RewriteGridSamplerToTosaCustomPass(exported_program) + + assert ( + rewrite_pass._get_first_user_input_placeholder(exported_program.graph) + is first_placeholder + ) + assert first_placeholder.target == original_target + + def test_rewrite_grid_sampler_to_tosa_custom_vgf_no_target(): model = GridSampler2d() example_inputs = ( @@ -123,7 +162,8 @@ def test_rewrite_grid_sampler_to_tosa_custom_sampler_dispatch_rounds_up_output() edge_model = to_edge(export(model, example_inputs)) with TosaLoweringContext(TosaSpecification.create_from_string("TOSA-1.0+FP")): edge_model = edge_model.transform([RewriteGridSamplerToTosaCustomPass()]) - nodes = list(edge_model.exported_program().graph.nodes) + exported_program = edge_model.exported_program() + nodes = list(exported_program.graph.nodes) custom_node = next( node for node in nodes if node.target == exir_ops.backend.tosa.CUSTOM.default @@ -145,7 +185,8 @@ def test_rewrite_grid_sampler_to_tosa_custom_no_target_uses_sampler_for_c4(): edge_model = to_edge(export(model, example_inputs)) with TosaLoweringContext(TosaSpecification.create_from_string("TOSA-1.0+FP")): edge_model = edge_model.transform([RewriteGridSamplerToTosaCustomPass()]) - nodes = list(edge_model.exported_program().graph.nodes) + exported_program = edge_model.exported_program() + nodes = list(exported_program.graph.nodes) custom_node = next( node for node in nodes if node.target == exir_ops.backend.tosa.CUSTOM.default @@ -187,29 +228,62 @@ def test_quantized_grid_sampler_uses_int8_sampler_payload( edge_model = to_edge(export(converted, example_inputs, strict=True)) with TosaLoweringContext(TosaSpecification.create_from_string("TOSA-1.0+FP+INT")): + edge_model = edge_model.transform([FoldAndAnnotateQParamsPass()]) + grid_sampler_node = next( + node + for node in edge_model.exported_program().graph.nodes + if node.target == exir_ops.edge.aten.grid_sampler_2d.default + ) + expected_grid_qparams = grid_sampler_node.meta["input_qparams"][1] + expected_grid_scale = torch.tensor( + [expected_grid_qparams.get_scale_per_tensor()], dtype=torch.float32 + ) + expected_grid_zero_point = torch.tensor( + [expected_grid_qparams.get_zp_per_tensor()], dtype=torch.int32 + ) edge_model = edge_model.transform( [ - FoldAndAnnotateQParamsPass(), InsertGridSamplerGridDequantPass(), - RewriteGridSamplerToTosaCustomPass(), + _RewriteGridSamplerToTosaCustomExportPass(), ] ) - nodes = list(edge_model.exported_program().graph.nodes) + exported_program = edge_model.exported_program() + nodes = list(exported_program.graph.nodes) custom_node = next( node for node in nodes if node.target == exir_ops.backend.tosa.CUSTOM.default ) payload = decode_payload(custom_node.kwargs["implementation_attrs"]) grid_input = custom_node.args[0][1] - + grid_scale_input = custom_node.args[0][2] + grid_zero_point_input = custom_node.args[0][3] assert payload["input_0_type"] == "Image" assert payload["input_0_vkformat"] == GRID_SAMPLER_2D_SAMPLER_INT8_VK_FORMAT assert payload["input_1_type"] == "Tensor" - assert payload["input_1_vkformat"] == GRID_SAMPLER_2D_VK_FORMAT + assert payload["input_1_vkformat"] == GRID_SAMPLER_2D_QUANTIZED_GRID_VK_FORMAT + assert payload["input_2_type"] == "Tensor" + assert payload["input_2_vkformat"] == "VK_FORMAT_R32_SFLOAT" + assert payload["input_2_binding"] == 3 + assert payload["input_3_type"] == "Tensor" + assert payload["input_3_vkformat"] == "VK_FORMAT_R32_SINT" + assert payload["input_3_binding"] == 4 assert payload["output_0_type"] == "Image" assert payload["output_0_vkformat"] == GRID_SAMPLER_2D_SAMPLER_INT8_VK_FORMAT - assert grid_input.meta["val"].dtype == torch.float32 - assert grid_input.target in ( + assert payload["output_0_binding"] == 2 + assert grid_input.meta["val"].dtype == torch.int8 + assert grid_scale_input.op == "placeholder" + assert grid_scale_input.meta["val"].dtype == torch.float32 + assert grid_scale_input.meta["val"].shape == expected_grid_scale.shape + assert torch.equal( + exported_program.constants[grid_scale_input.name], expected_grid_scale + ) + assert grid_zero_point_input.op == "placeholder" + assert grid_zero_point_input.meta["val"].dtype == torch.int32 + assert grid_zero_point_input.meta["val"].shape == expected_grid_zero_point.shape + assert torch.equal( + exported_program.constants[grid_zero_point_input.name], expected_grid_zero_point + ) + assert grid_input.target not in ( exir_ops.edge.quantized_decomposed.dequantize_per_tensor.default, exir_ops.edge.quantized_decomposed.dequantize_per_channel.default, ) @@ -220,6 +294,42 @@ def test_quantized_grid_sampler_uses_int8_sampler_payload( assert next(iter(custom_node.meta["output_qparams"].values())).qmax == 127 +def test_quantized_grid_sampler_dequantizes_grid_for_non_sampler_path(): + model = GridSampler2d().eval() + model.interpolation_mode_ = 2 + example_inputs = ( + torch.randn(1, 4, 8, 8), + torch.rand(1, 4, 4, 2), + ) + quantizer = VgfQuantizer(VgfCompileSpec("TOSA-1.0+INT")) + quantizer.set_global(get_symmetric_quantization_config(is_per_channel=False)) + + exported = export(model, example_inputs, strict=True) + prepared = prepare_pt2e(exported.module(), quantizer) + prepared(*example_inputs) + converted = convert_pt2e(prepared) + + edge_model = to_edge(export(converted, example_inputs, strict=True)) + with TosaLoweringContext(TosaSpecification.create_from_string("TOSA-1.0+FP+INT")): + edge_model = edge_model.transform( + [FoldAndAnnotateQParamsPass(), InsertGridSamplerGridDequantPass()] + ) + + grid_sampler_node = next( + node + for node in edge_model.exported_program().graph.nodes + if node.target == exir_ops.edge.aten.grid_sampler_2d.default + ) + grid_input = grid_sampler_node.args[1] + + assert ( + grid_input.target + == exir_ops.edge.quantized_decomposed.dequantize_per_tensor.default + ) + assert grid_input.meta["val"].dtype == torch.float32 + assert 1 not in grid_sampler_node.meta["input_qparams"] + + def test_quantized_grid_sampler_rejects_dequantized_grid_with_int8_image_payload(): model = GridSampler2d().eval() example_inputs = ( diff --git a/backends/arm/vgf/_passes/insert_grid_sampler_grid_dequant_pass.py b/backends/arm/vgf/_passes/insert_grid_sampler_grid_dequant_pass.py index 708f03a735f..60f6765afde 100644 --- a/backends/arm/vgf/_passes/insert_grid_sampler_grid_dequant_pass.py +++ b/backends/arm/vgf/_passes/insert_grid_sampler_grid_dequant_pass.py @@ -3,11 +3,14 @@ # This source code is licensed under the BSD-style license found in the # LICENSE file in the root directory of this source tree. +import math + import torch from executorch.backends.arm._passes import ArmPass from executorch.backends.arm._passes.arm_pass_utils import create_node, set_node_arg from executorch.backends.arm._passes.fold_qdq_with_annotated_qparams_pass import ( get_input_qparams, + get_output_qparams, ) from executorch.backends.arm._passes.quant_args import QuantArgs from executorch.exir.dialects._ops import ops as exir_ops @@ -31,14 +34,63 @@ def _get_per_tensor_dequant_args(grid: torch.fx.Node, grid_qparams: QuantArgs) - ) +def _can_keep_quantized_grid(grid_qparams: QuantArgs) -> bool: + return not grid_qparams.per_channel and grid_qparams.dtype == torch.int8 + + +def _uses_grid_sampler_int8_snorm_qparams(qparams: QuantArgs) -> bool: + return ( + not qparams.per_channel + and math.isclose( + qparams.get_scale_per_tensor(), 1.0 / 127.0, rel_tol=1e-6, abs_tol=1e-9 + ) + and qparams.get_zp_per_tensor() == 0 + and qparams.qmin == -127 + and qparams.qmax == 127 + and qparams.dtype == torch.int8 + ) + + +def _supports_quantized_grid_sampler_path(node: torch.fx.Node) -> bool: + try: + input_qparams = get_input_qparams(node) + output_qparams = get_output_qparams(node) + except ValueError: + return False + + image_qparams = input_qparams.get(0) + if image_qparams is None or not output_qparams: + return False + if not _uses_grid_sampler_int8_snorm_qparams(image_qparams): + return False + if not _uses_grid_sampler_int8_snorm_qparams(next(iter(output_qparams.values()))): + return False + + input_tensor = node.args[0] + interpolation_mode = node.args[2] + if not isinstance(input_tensor, torch.fx.Node): + return False + if not isinstance(interpolation_mode, int): + return False + input_val = input_tensor.meta.get("val") + if not isinstance(input_val, torch.Tensor) or len(input_val.shape) != 4: + return False + if int(input_val.shape[0]) != 1 or interpolation_mode not in (0, 1): + return False + return int(input_val.shape[1]) in (3, 4) + + class InsertGridSamplerGridDequantPass(ArmPass): """Insert an explicit float boundary for quantized grid_sample grid inputs. This runs before quant-node decomposition so the standard Arm quant passes - can legalize the inserted dequant op, and later VGF custom-op rewriting sees - the expected float grid contract. Per-channel grid qparams are rejected - because the follow-up decompose pass only legalizes per-tensor - quantized_decomposed ops. + can legalize the inserted dequant op. For supported per-tensor int8 grids we + preserve the quantized grid through to the VGF custom-op rewrite so the + shader can dequantize coordinates internally. Unsupported per-tensor qparams + are still dequantized to float here, but that does not create a mixed + int8-image / float-grid shader mode; the supported shader modes remain + float/float and int8/int8. Per-channel grid qparams are rejected because the + follow-up decompose pass only legalizes per-tensor quantized_decomposed ops. """ @@ -64,6 +116,10 @@ def call(self, graph_module: torch.fx.GraphModule): raise RuntimeError( "Quantized grid_sampler grid input is missing input qparams" ) + if _can_keep_quantized_grid( + grid_qparams + ) and _supports_quantized_grid_sampler_path(node): + continue if grid_qparams.per_channel: raise RuntimeError( "grid_sampler grid dequant only supports per-tensor qparams" diff --git a/backends/arm/vgf/_passes/rewrite_grid_sampler_to_tosa_custom.py b/backends/arm/vgf/_passes/rewrite_grid_sampler_to_tosa_custom.py index 8d05ea3cafb..9627c954a4a 100644 --- a/backends/arm/vgf/_passes/rewrite_grid_sampler_to_tosa_custom.py +++ b/backends/arm/vgf/_passes/rewrite_grid_sampler_to_tosa_custom.py @@ -23,8 +23,11 @@ encode_payload, grid_sampler_2d_operator_name, ) +from executorch.backends.transforms.utils import create_constant_placeholder +from executorch.exir import ExportedProgram from executorch.exir.dialects._ops import ops as exir_ops from executorch.exir.pass_base import ExportPass, PassResult +from torch.export.graph_signature import InputKind from torch.fx.passes.shape_prop import _extract_tensor_metadata @@ -32,7 +35,7 @@ def _grid_sampler_2d_custom_fake_impl( inputs, operator_name, domain_name, implementation_attrs ) -> list[torch.Tensor]: _ = (operator_name, domain_name, implementation_attrs) - input_tensor, grid = inputs + input_tensor, grid, *_ = inputs return [ torch.empty( ( @@ -146,12 +149,25 @@ def _uses_grid_sampler_int8_snorm_metadata(node: torch.fx.Node) -> bool: ) and _uses_grid_sampler_int8_snorm_qparams(next(iter(output_qparams.values()))) +def _supports_quantized_grid_custom(qparams: QuantArgs) -> bool: + return not qparams.per_channel and qparams.dtype == torch.int8 + + class RewriteGridSamplerToTosaCustomPass(ArmPass): """Rewrite ``aten.grid_sampler_2d`` nodes to ``tosa.CUSTOM``.""" targeted_ops = (exir_ops.edge.aten.grid_sampler_2d.default,) _passes_required_after: Set[Type[ExportPass]] = set() + def __init__( + self, + exported_program: ExportedProgram | None = None, + *args, + **kwargs, + ) -> None: + super().__init__(*args, **kwargs) + self.exported_program = exported_program + @staticmethod def _encode_payload( interpolation_mode: int, @@ -160,6 +176,8 @@ def _encode_payload( input_tensor: torch.fx.Node, output_tensor: torch.fx.Node, output_dtype: torch.dtype | None = None, + grid_dtype: torch.dtype | None = None, + extra_tensor_input_vkformats: list[str] | None = None, ) -> list[int]: input_val = input_tensor.meta.get("val") if input_val is None: @@ -175,9 +193,69 @@ def _encode_payload( output_shape=tuple(output_val.shape), input_dtype=input_val.dtype, output_dtype=output_dtype, + grid_dtype=grid_dtype, + extra_tensor_input_vkformats=extra_tensor_input_vkformats, ) return encode_payload(payload) + def _get_first_user_input_placeholder(self, graph: torch.fx.Graph) -> torch.fx.Node: + if self.exported_program is None: + raise RuntimeError( + "RewriteGridSamplerToTosaCustomPass requires ExportedProgram context " + "to create constant placeholders" + ) + user_input_names = { + spec.arg.name + for spec in self.exported_program.graph_signature.input_specs + if spec.kind == InputKind.USER_INPUT + } + for graph_node in graph.nodes: + if graph_node.op != "placeholder": + continue + if ( + graph_node.name in user_input_names + or graph_node.target in user_input_names + ): + return graph_node + raise RuntimeError("Failed to find a user input placeholder in the graph") + + def _create_grid_qparam_placeholders( + self, + graph: torch.fx.Graph, + node: torch.fx.Node, + grid_qparams: QuantArgs, + ) -> tuple[torch.fx.Node, torch.fx.Node]: + if self.exported_program is None: + raise RuntimeError( + "RewriteGridSamplerToTosaCustomPass requires ExportedProgram context " + "to create qparam placeholders" + ) + + first_user_input = self._get_first_user_input_placeholder(graph) + base_name = node.name.replace(".", "_") + scale_name = f"{base_name}_grid_scale" + zp_name = f"{base_name}_grid_zero_point" + + with graph.inserting_before(first_user_input): + scale_node = create_constant_placeholder( + self.exported_program, + graph, + scale_name, + InputKind.CONSTANT_TENSOR, + torch.tensor( + [grid_qparams.get_scale_per_tensor()], dtype=torch.float32 + ), + ) + zp_node = create_constant_placeholder( + self.exported_program, + graph, + zp_name, + InputKind.CONSTANT_TENSOR, + torch.tensor([grid_qparams.get_zp_per_tensor()], dtype=torch.int32), + ) + + return scale_node, zp_node + @staticmethod def _pad_c3_input_to_c4( graph_module: torch.fx.GraphModule, @@ -240,15 +318,32 @@ def call(self, graph_module): ) use_quantized_image_payload = _uses_grid_sampler_int8_snorm_metadata(node) output_dtype = torch.int8 if use_quantized_image_payload else None + grid_qparams = None + grid_qparam_constants: tuple[torch.fx.Node, torch.fx.Node] | None = None + quantized_grid = not grid.meta["val"].dtype.is_floating_point pad_c3_for_sampler = _can_pad_c3_for_sampler( input_tensor, interpolation_mode, align_corners, ) - if not grid.meta["val"].dtype.is_floating_point: + if quantized_grid: + grid_qparams = get_input_qparams(node).get(1) + if grid_qparams is None: + raise RuntimeError( + "Quantized grid_sampler rewrite is missing grid input qparams" + ) + if not _supports_quantized_grid_custom(grid_qparams): + raise RuntimeError( + "grid_sampler rewrite only supports per-tensor int8 grids; " + "unsupported qparams should have been dequantized earlier" + ) + grid_qparam_constants = self._create_grid_qparam_placeholders( + graph_module.graph, node, grid_qparams + ) + if use_quantized_image_payload and grid_qparam_constants is None: raise RuntimeError( - "grid_sampler rewrite expected float grid input; " - "InsertGridSamplerGridDequantPass should run before this pass" + "grid_sampler int8 sampler rewrite requires a quantized int8 " + "grid with explicit scale/zero-point shader inputs" ) operator_name = grid_sampler_2d_operator_name( @@ -270,6 +365,14 @@ def call(self, graph_module): input_tensor=custom_input, output_tensor=node, output_dtype=output_dtype, + grid_dtype=( + grid.meta["val"].dtype if grid_qparams is not None else None + ), + extra_tensor_input_vkformats=( + ["VK_FORMAT_R32_SFLOAT", "VK_FORMAT_R32_SINT"] + if grid_qparam_constants is not None + else None + ), ) nhwc_input = create_node( graph_module.graph, @@ -284,11 +387,13 @@ def call(self, graph_module): ), ) custom_grid = grid - + custom_inputs = [nhwc_input, custom_grid] + if grid_qparam_constants is not None: + custom_inputs.extend(grid_qparam_constants) custom_node = create_node( graph_module.graph, op_target=exir_ops.backend.tosa.CUSTOM.default, - args=([nhwc_input, custom_grid],), + args=(custom_inputs,), kwargs={ "operator_name": operator_name, "domain_name": CUSTOM_SHADER_DOMAIN_NAME, @@ -297,6 +402,12 @@ def call(self, graph_module): from_node=node, inherit_qparams=True, ) + if grid_qparams is not None and "input_qparams" in custom_node.meta: + custom_node.meta["input_qparams"] = { + idx: qargs + for idx, qargs in custom_node.meta["input_qparams"].items() + if idx != 1 + } with graph_module.graph.inserting_after(custom_node): getitem_node = graph_module.graph.create_node( "call_function", @@ -305,7 +416,7 @@ def call(self, graph_module): kwargs={}, ) custom_output = _grid_sampler_2d_custom_fake_impl( - [nhwc_input.meta["val"], custom_grid.meta["val"]], + [input_node.meta["val"] for input_node in custom_inputs], operator_name, CUSTOM_SHADER_DOMAIN_NAME, implementation_attrs, diff --git a/backends/arm/vgf/shaders/grid_sampler.py b/backends/arm/vgf/shaders/grid_sampler.py index abfb6190019..30efed7a01b 100644 --- a/backends/arm/vgf/shaders/grid_sampler.py +++ b/backends/arm/vgf/shaders/grid_sampler.py @@ -5,7 +5,7 @@ import json from importlib.resources import files -from typing import Any +from typing import Any, Sequence CUSTOM_SHADER_DOMAIN_NAME = "com.arm.VulkanCustomShader" GRID_SAMPLER_2D_OPERATOR_NAME = "torch.nn.functional.grid_sample" @@ -33,6 +33,7 @@ ) GRID_SAMPLER_2D_SAMPLER_VK_FORMAT = "VK_FORMAT_R32G32B32A32_SFLOAT" GRID_SAMPLER_2D_SAMPLER_INT8_VK_FORMAT = "VK_FORMAT_R8G8B8A8_SNORM" +GRID_SAMPLER_2D_QUANTIZED_GRID_VK_FORMAT = "VK_FORMAT_R8_SINT" _INTERPOLATION_MODE_NAMES = { 0: "bilinear", @@ -100,6 +101,8 @@ def build_grid_sampler_2d_payload( output_shape: tuple[int, ...] | None = None, input_dtype: Any | None = None, output_dtype: Any | None = None, + grid_dtype: Any | None = None, + extra_tensor_input_vkformats: Sequence[str] | None = None, ) -> dict[str, Any]: """Build Vulkan custom shader metadata for a 2D grid sampler variant. @@ -115,6 +118,10 @@ def build_grid_sampler_2d_payload( Vulkan formats when supported. output_dtype (Any | None): Output tensor dtype. Defaults to input_dtype when omitted. + grid_dtype (Any | None): Grid tensor dtype, used to select the + quantized-grid sampler path when supported. + extra_tensor_input_vkformats (Sequence[str] | None): Vulkan formats + for any additional tensor inputs appended after the grid input. Returns: dict[str, Any]: Custom shader metadata payload. @@ -144,6 +151,22 @@ def build_grid_sampler_2d_payload( and sampler_vk_format is not None and int(interpolation_mode) in (0, 1) ) + use_quantized_grid = str(grid_dtype) == "torch.int8" + if use_quantized_grid and not ( + use_sampler + and str(input_dtype) == "torch.int8" + and str(output_dtype) == "torch.int8" + ): + raise ValueError( + "Quantized grid-sample payload is only supported for the int8 sampler path" + ) + if sampler_vk_format == GRID_SAMPLER_2D_SAMPLER_INT8_VK_FORMAT and ( + not use_quantized_grid or len(extra_tensor_input_vkformats or ()) != 2 + ): + raise ValueError( + "Int8 sampler grid-sample payload requires an int8 grid and " + "explicit scale/zero-point tensor inputs" + ) shader_file = ( _sampler_shader_file(sampler_vk_format, align_corners=align_corners) if use_sampler @@ -164,7 +187,11 @@ def build_grid_sampler_2d_payload( "input_0_binding": 0, "input_0_descriptorset": 0, "input_1_type": "Tensor", - "input_1_vkformat": GRID_SAMPLER_2D_VK_FORMAT, + "input_1_vkformat": ( + GRID_SAMPLER_2D_QUANTIZED_GRID_VK_FORMAT + if use_quantized_grid + else GRID_SAMPLER_2D_VK_FORMAT + ), "input_1_binding": 1, "input_1_descriptorset": 0, "output_0_binding": 2, @@ -200,6 +227,18 @@ def build_grid_sampler_2d_payload( "output_0_vkdescriptortype": "VK_DESCRIPTOR_TYPE_STORAGE_BUFFER", } ) + extra_tensor_input_vkformats = extra_tensor_input_vkformats or () + for extra_idx, vk_format in enumerate(extra_tensor_input_vkformats): + input_idx = 2 + extra_idx + payload.update( + { + f"input_{input_idx}_type": "Tensor", + f"input_{input_idx}_vkformat": vk_format, + f"input_{input_idx}_binding": 3 + extra_idx, + f"input_{input_idx}_descriptorset": 0, + f"input_{input_idx}_vkdescriptortype": "VK_DESCRIPTOR_TYPE_STORAGE_BUFFER", + } + ) return payload diff --git a/backends/arm/vgf/shaders/grid_sampler_sampler_int8.glsl b/backends/arm/vgf/shaders/grid_sampler_sampler_int8.glsl index 132049210db..4cb1b2f55a8 100644 --- a/backends/arm/vgf/shaders/grid_sampler_sampler_int8.glsl +++ b/backends/arm/vgf/shaders/grid_sampler_sampler_int8.glsl @@ -5,21 +5,30 @@ #version 450 #extension GL_ARM_tensors : require +#extension GL_EXT_shader_explicit_arithmetic_types_int8 : require layout(set = 0, binding = 0) uniform sampler2D inputImage; -layout(set = 0, binding = 1) uniform tensorARM grid; +layout(set = 0, binding = 1) uniform tensorARM grid; layout(set = 0, binding = 2, rgba8_snorm) uniform writeonly image2D outImage; +layout(set = 0, binding = 3) readonly buffer GridScaleBuffer { + float gridScale[]; +}; +layout(set = 0, binding = 4) readonly buffer GridZeroPointBuffer { + int gridZeroPoint[]; +}; layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in; vec2 readGridXY(ivec2 p) { uint xCoords[4] = uint[](0u, uint(p.y), uint(p.x), 0u); uint yCoords[4] = uint[](0u, uint(p.y), uint(p.x), 1u); - float xVal[1]; - float yVal[1]; + int8_t xVal[1]; + int8_t yVal[1]; tensorReadARM(grid, xCoords, xVal); tensorReadARM(grid, yCoords, yVal); - return vec2(xVal[0], yVal[0]); + return vec2( + (float(xVal[0]) - float(gridZeroPoint[0])) * gridScale[0], + (float(yVal[0]) - float(gridZeroPoint[0])) * gridScale[0]); } void main() { diff --git a/backends/arm/vgf/shaders/grid_sampler_sampler_int8.spirv.b64 b/backends/arm/vgf/shaders/grid_sampler_sampler_int8.spirv.b64 index 07f4440d3b4..90a67be948f 100644 --- a/backends/arm/vgf/shaders/grid_sampler_sampler_int8.spirv.b64 +++ b/backends/arm/vgf/shaders/grid_sampler_sampler_int8.spirv.b64 @@ -1 +1 @@ -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 \ No newline at end of file diff --git a/backends/arm/vgf/shaders/grid_sampler_sampler_int8_align_corners.glsl b/backends/arm/vgf/shaders/grid_sampler_sampler_int8_align_corners.glsl index b0aa9d303fc..68da7fc7874 100644 --- a/backends/arm/vgf/shaders/grid_sampler_sampler_int8_align_corners.glsl +++ b/backends/arm/vgf/shaders/grid_sampler_sampler_int8_align_corners.glsl @@ -5,21 +5,30 @@ #version 450 #extension GL_ARM_tensors : require +#extension GL_EXT_shader_explicit_arithmetic_types_int8 : require layout(set = 0, binding = 0) uniform sampler2D inputImage; -layout(set = 0, binding = 1) uniform tensorARM grid; +layout(set = 0, binding = 1) uniform tensorARM grid; layout(set = 0, binding = 2, rgba8_snorm) uniform writeonly image2D outImage; +layout(set = 0, binding = 3) readonly buffer GridScaleBuffer { + float gridScale[]; +}; +layout(set = 0, binding = 4) readonly buffer GridZeroPointBuffer { + int gridZeroPoint[]; +}; layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in; vec2 readGridXY(ivec2 p) { uint xCoords[4] = uint[](0u, uint(p.y), uint(p.x), 0u); uint yCoords[4] = uint[](0u, uint(p.y), uint(p.x), 1u); - float xVal[1]; - float yVal[1]; + int8_t xVal[1]; + int8_t yVal[1]; tensorReadARM(grid, xCoords, xVal); tensorReadARM(grid, yCoords, yVal); - return vec2(xVal[0], yVal[0]); + return vec2( + (float(xVal[0]) - float(gridZeroPoint[0])) * gridScale[0], + (float(yVal[0]) - float(gridZeroPoint[0])) * gridScale[0]); } vec2 alignCornersUv(vec2 gridXY) { diff --git a/backends/arm/vgf/shaders/grid_sampler_sampler_int8_align_corners.spirv.b64 b/backends/arm/vgf/shaders/grid_sampler_sampler_int8_align_corners.spirv.b64 index cd15b7a8264..d92f7c1be84 100644 --- a/backends/arm/vgf/shaders/grid_sampler_sampler_int8_align_corners.spirv.b64 +++ b/backends/arm/vgf/shaders/grid_sampler_sampler_int8_align_corners.spirv.b64 @@ -1 +1 @@ -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 \ No newline at end of file