|
9 | 9 | import torch.nn.functional as F |
10 | 10 | from executorch.backends.arm._passes import FoldAndAnnotateQParamsPass |
11 | 11 | from executorch.backends.arm._passes.quant_args import QuantArgs |
| 12 | +from executorch.backends.arm.constants import NHWC_ORDER |
12 | 13 | from executorch.backends.arm.quantizer.arm_quantizer import ( |
13 | 14 | get_symmetric_quantization_config, |
14 | 15 | VgfQuantizer, |
@@ -58,6 +59,25 @@ def forward(self, x, grid): |
58 | 59 | ) |
59 | 60 |
|
60 | 61 |
|
| 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 | + |
61 | 81 | class _RewriteGridSamplerToTosaCustomExportPass(ExportedProgramPassBase): |
62 | 82 | # The quantized grid-sampler rewrite materializes exported constant |
63 | 83 | # 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(): |
453 | 473 | assert payload["output_0_type"] == "Tensor" |
454 | 474 | assert payload["input_1_vkdescriptortype"] == "VK_DESCRIPTOR_TYPE_TENSOR_ARM" |
455 | 475 | 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 | + ) |
0 commit comments