Repository navigation
Qualcomm AI Engine Direct - Keep node names unique across the QNN and Samsung passes #24316
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -188,6 +188,81 @@ def forward(self, x): | |
| ) | ||
| torch.testing.assert_close(gm(x), model(x)) | ||
|
|
||
| def test_export_pipeline_keeps_node_names_unique(self): | ||
| # torch.export leaves the input name t_1 unrecorded, and renames | ||
| # input * 2 to mul_1 to make room for the input mul without recording | ||
| # that either. The decompositions would otherwise reuse those names. | ||
| class Autocast(torch.nn.Module): | ||
| def __init__(self): | ||
| super().__init__() | ||
| self.w = torch.nn.Parameter(torch.randn(8, 8)) | ||
|
|
||
| def forward(self, x, t_1): | ||
| with torch.autocast("cpu", dtype=torch.bfloat16): | ||
| # The second transpose is named t_1 inside the region, and | ||
| # the decomposition copies it next to the input t_1. | ||
| y = x @ self.w.t() @ self.w.t() | ||
| return y.float() + t_1 | ||
|
|
||
| class Attention(torch.nn.Module): | ||
| # input is a Python builtin name. It has to survive the reservation | ||
| # as it is, because the graph signature refers to inputs by name. | ||
| def forward(self, input, k, v, mul): | ||
| return ( | ||
| torch.nn.functional.scaled_dot_product_attention(input * 2, k, v) | ||
| + mul | ||
| ) | ||
|
|
||
| cases = ( | ||
| ( | ||
| Autocast(), | ||
| (torch.randn(4, 8), torch.randn(4, 8)), | ||
| torch._higher_order_ops.wrap.wrap_with_autocast, | ||
| ), | ||
| ( | ||
| Attention(), | ||
| tuple(torch.randn(1, 2, 4, 8) for _ in range(4)), | ||
| torch.ops.aten.scaled_dot_product_attention.default, | ||
| ), | ||
| ) | ||
| no_collision = [] | ||
| for model, inputs, decomposed_op in cases: | ||
| with self.subTest(model=type(model).__name__): | ||
| ep = torch.export.export(model.eval(), inputs) | ||
| self.assertIn(decomposed_op, [n.target for n in ep.graph.nodes]) | ||
| ep = get_qnn_pass_manager_cls()().transform_for_export_pipeline(ep) | ||
| self.assertNotIn(decomposed_op, [n.target for n in ep.graph.nodes]) | ||
| # ep.module() copies the graph and renames duplicates, so lint | ||
| # the graph the passes produced. | ||
| ep.graph.lint() | ||
| self.assertEqual( | ||
| [n.name for n in ep.graph.nodes if n.op == "placeholder"], | ||
| [spec.arg.name for spec in ep.graph_signature.input_specs], | ||
| ) | ||
| torch.testing.assert_close(ep.module()(*inputs), model(*inputs)) | ||
| ep = torch.export.export(model.eval(), inputs) | ||
| with patch( | ||
| "executorch.backends.qualcomm._passes.qnn_pass_manager." | ||
| "reserve_node_names" | ||
| ) as reserve: | ||
| try: | ||
| get_qnn_pass_manager_cls()().transform_for_export_pipeline( | ||
| ep | ||
| ).graph.lint() | ||
| except Exception as e: | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If a pass fails for a different reason, this shows as an |
||
| # The pass manager wraps the lint error in its own. | ||
| self.assertIn("Node redefined name", str(e.__cause__ or e)) | ||
| else: | ||
| no_collision.append(type(model).__name__) | ||
| reserve.assert_called_once() | ||
| # A skip, not a failure: a torch update that removes the collision is fine. | ||
| if no_collision: | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If only one case stops colliding, the whole test is skipped, but the other case still tests the helper. We can skip only when |
||
| self.skipTest( | ||
| f"{no_collision} no longer collide without reserve_node_names, so " | ||
| "those cases no longer test it; see " | ||
| "https://github.com/pytorch/pytorch/issues/202927" | ||
| ) | ||
|
|
||
| def test_build_op_wrappers_returns_context_binary(self): | ||
| op_name = "ctx_loader_build" | ||
| ctx_bin = b"qnn_context_binary" | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -59,6 +59,19 @@ def _get_fake_tensor_mode(graph: torch.fx.Graph, data: torch.Tensor) -> torch.Te | |
| return FakeTensorConverter().from_real_tensor(example_fake_tensor.fake_mode, t=data) | ||
|
|
||
|
|
||
| def reserve_node_names(graph_module: torch.fx.GraphModule) -> None: | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we add a torch-only unit test in |
||
| """ | ||
| Records every node name of the top-level graph in its namespace, so that | ||
| nodes a pass adds there later get names that are not already taken. | ||
| """ | ||
| # TODO: Remove once torch.export records the names it gives inputs and the | ||
| # ops it renames for them (https://github.com/pytorch/pytorch/issues/202927). | ||
| # torch.fx has no public way to reserve a name. | ||
| namespace = graph_module.graph._graph_namespace | ||
| for node in graph_module.graph.nodes: | ||
| namespace._used_names.add(node.name) | ||
|
|
||
|
|
||
| def is_get_attr_node(node: torch.fx.Node) -> bool: | ||
| """ | ||
| Returns true if the given node is a get attr node for a tensor of the model | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
transform_for_to_edge_pipeline doesn't call this, so the name reservation doesn't cover it. Should we call the same reservation there as well ? (in a follow up PR)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for checking. I looked at this, and I think that pipeline is already safe. It always runs on an edge program, and to_edge rebuilds the graph with its own passes, so every node name is recorded again. I tested it with a model whose input is named "mul". Right after torch.export, the model's own mul node gets the name mul_1, and that name is not recorded. After to_edge, all names are recorded, and a new node named "mul" correctly becomes "mul_1". So I left it out, to keep the reservation only where it is needed.