|
9 | 9 | import logging |
10 | 10 | import operator |
11 | 11 | from types import NoneType |
12 | | -from typing import cast, Dict, List, Optional, Union |
| 12 | +from typing import cast, Dict, List, Optional, Tuple, Union |
13 | 13 |
|
14 | 14 | import executorch.backends.vulkan.serialization.vulkan_graph_schema as vk_graph_schema |
15 | 15 | import torch |
|
32 | 32 | from executorch.exir.tensor import TensorSpec |
33 | 33 | from torch._export.utils import get_buffer, get_param, is_buffer, is_param |
34 | 34 | from torch.export import ExportedProgram |
| 35 | +from torch.export.graph_signature import OutputKind |
35 | 36 | from torch.fx import Node |
36 | 37 |
|
37 | 38 | _ScalarType = Union[bool, int, float] |
@@ -490,19 +491,80 @@ def process_call_function_node(self, node) -> None: |
490 | 491 | def process_getattr_node(self, node: Node) -> None: |
491 | 492 | self.create_node_value(node) |
492 | 493 |
|
| 494 | + def _output_nodes_in_order(self, node: Node) -> List[Tuple[Optional[int], Node]]: |
| 495 | + """ |
| 496 | + The graph outputs in order, duplicates included, each paired with its |
| 497 | + position in the output signature. |
| 498 | +
|
| 499 | + `node.all_input_nodes` de-duplicates, so a graph that returns the same |
| 500 | + value twice (`return y, y`, or `return y, y.view(...)` once a redundant |
| 501 | + view is removed) would serialize one output id for two outputs and fail |
| 502 | + the runtime's argument count check. |
| 503 | +
|
| 504 | + The position is what distinguishes those repeats from a buffer mutation |
| 505 | + that happens to alias a user output: both reference the same node, so |
| 506 | + only the signature can say which slot is which. It is None when the |
| 507 | + signature does not line up with the output args, in which case the |
| 508 | + caller falls back to matching by name. |
| 509 | + """ |
| 510 | + args = node.args[0] if len(node.args) == 1 else node.args |
| 511 | + if isinstance(args, Node): |
| 512 | + args = (args,) |
| 513 | + output_specs = getattr(self.program.graph_signature, "output_specs", None) |
| 514 | + aligned = output_specs is not None and len(output_specs) == len(args) |
| 515 | + out_nodes: List[Tuple[Optional[int], Node]] = [] |
| 516 | + for idx, arg in enumerate(args): |
| 517 | + if isinstance(arg, Node): |
| 518 | + out_nodes.append((idx if aligned else None, arg)) |
| 519 | + return out_nodes |
| 520 | + |
| 521 | + def _is_user_output_slot(self, idx: Optional[int]) -> Optional[bool]: |
| 522 | + """ |
| 523 | + Whether the output slot at `idx` is a user output, or None if unknown. |
| 524 | + """ |
| 525 | + if idx is None: |
| 526 | + return None |
| 527 | + output_specs = getattr(self.program.graph_signature, "output_specs", None) |
| 528 | + if output_specs is None: |
| 529 | + return None |
| 530 | + return output_specs[idx].kind == OutputKind.USER_OUTPUT |
| 531 | + |
493 | 532 | def process_output_node(self, node: Node) -> None: |
494 | | - for out_node in node.all_input_nodes: |
| 533 | + # Names already emitted for a buffer mutation that aliases a user |
| 534 | + # output; see the fallback branch below. |
| 535 | + emitted_mutation_aliases: set[str] = set() |
| 536 | + for idx, out_node in self._output_nodes_in_order(node): |
495 | 537 | if out_node not in self.node_to_value_ids: |
496 | 538 | raise AssertionError( |
497 | 539 | "Cannot find input to output node in node_to_value_ids. This means " |
498 | 540 | "the output node is being serialized before its corresponding " |
499 | 541 | "internal node which is not allowed." |
500 | 542 | ) |
501 | | - # Mutable buffers outputs are not included as an output to the |
502 | | - # delegate call. Skip marking them as an output. |
| 543 | + # A mutation whose buffer is aliased to its input is applied in |
| 544 | + # place, so its slot is not an output of the delegate call. Decide |
| 545 | + # per slot rather than per node: the mutation and a user output can |
| 546 | + # be the same node, and matching by name alone would either keep |
| 547 | + # both or drop both. |
| 548 | + # |
| 549 | + # Only the aliased case is filtered here. Without aliasing the |
| 550 | + # mutation still needs a slot to carry the new buffer value back, |
| 551 | + # so the old rule stands: dropping the slot would leave the graph |
| 552 | + # accepted but never updating the buffer, which is worse than the |
| 553 | + # argument-count rejection it would otherwise get. |
503 | 554 | if out_node.name in self.buffer_mutation_inputs: |
504 | | - if out_node.name not in self.buffer_mutation_user_outputs: |
505 | | - continue |
| 555 | + is_user_output = self._is_user_output_slot(idx) |
| 556 | + if is_user_output is not None: |
| 557 | + if not is_user_output: |
| 558 | + continue |
| 559 | + else: |
| 560 | + if out_node.name not in self.buffer_mutation_user_outputs: |
| 561 | + continue |
| 562 | + # Without the signature to separate the slots, the mutation |
| 563 | + # and the user output are indistinguishable occurrences of |
| 564 | + # one node, and together they get a single slot. |
| 565 | + if out_node.name in emitted_mutation_aliases: |
| 566 | + continue |
| 567 | + emitted_mutation_aliases.add(out_node.name) |
506 | 568 | elif is_mutable_buffer_node(out_node, self.program): |
507 | 569 | continue |
508 | 570 |
|
|
0 commit comments