Skip to content

Commit a284fc5

Browse files
SS-JIAssjia
andauthored
[ET-VK][test-utils] Implement submodule extraction utilities (pytorch#16267)
Stack from [ghstack](https://github.com/ezyang/ghstack) (oldest at bottom): * __->__ pytorch#16267 * pytorch#16266 ## Context When debugging correctness issues in ET-VK, it can be helpful to extract a subgraph of the model and test on the subgraph. ## Changes This diff/PR introduces some test utilites that can be used to extract all nodes tagged with a specified field in the `node.meta["custom"]` map into a separate `ExportedProgram`. Differential Revision: [D89216531](https://our.internmc.facebook.com/intern/diff/D89216531/) --------- Co-authored-by: ssjia <ssjia@devvm1479.ncg0.facebook.com>
1 parent 69974f3 commit a284fc5

1 file changed

Lines changed: 289 additions & 18 deletions

File tree

backends/vulkan/test/utils.py

Lines changed: 289 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,165 @@
2727
serialize_from_bundled_program_to_flatbuffer,
2828
)
2929
from executorch.exir import ExecutorchProgramManager, to_edge_transform_and_lower
30+
31+
from executorch.exir.backend.backend_api import _get_node_list_with_same_tag
32+
33+
from executorch.exir.backend.partitioner import (
34+
DelegationSpec,
35+
Partitioner,
36+
PartitionResult,
37+
)
38+
39+
from executorch.exir.backend.utils import tag_constant_data, tag_mutated_buffer
40+
41+
from executorch.exir.lowered_backend_module import (
42+
create_exported_program_from_submodule,
43+
create_submodule_from_nodes,
44+
)
3045
from executorch.extension.pybindings.portable_lib import ( # @manual
3146
_load_for_executorch_from_buffer,
3247
)
3348
from executorch.extension.pytree import tree_flatten
3449
from torch.export import export
50+
51+
from torch.export.exported_program import ExportedProgram
52+
from torch.export.graph_signature import InputKind
53+
from torch.fx.passes.infra.partitioner import CapabilityBasedPartitioner
54+
from torch.fx.passes.operator_support import OperatorSupportBase
3555
from torchao.quantization.pt2e.quantize_pt2e import convert_pt2e, prepare_pt2e
3656

3757

58+
class NodeFlagIsSetChecker(OperatorSupportBase):
59+
"""
60+
Check if a node is marked with a given field in node.meta["custom"]
61+
"""
62+
63+
def __init__(self, field: str) -> None:
64+
super().__init__()
65+
self.field = field
66+
67+
def check_field(self, node: torch.fx.Node) -> bool:
68+
if "custom" not in node.meta:
69+
return False
70+
71+
custom_map = node.meta["custom"]
72+
if self.field not in custom_map:
73+
return False
74+
75+
return custom_map[self.field]
76+
77+
def is_node_supported(self, submodules, node: torch.fx.Node) -> bool:
78+
if node.op == "placeholder" or node.op == "output":
79+
return False
80+
81+
# Check if the node itself is tagged
82+
if self.check_field(node):
83+
return True
84+
85+
# Check if any direct user of this node is tagged
86+
for user in node.users:
87+
if self.check_field(user):
88+
return True
89+
90+
return False
91+
92+
93+
class FlagBasedPartitioner(Partitioner):
94+
"""
95+
Partitioner that partitions based on whether node.meta["custom"][field] is set to
96+
True.
97+
"""
98+
99+
def __init__(self, field: str) -> None:
100+
super().__init__()
101+
self.field = field
102+
self.delegation_spec = DelegationSpec("custom_partition", [])
103+
104+
def partition(self, exported_program: ExportedProgram) -> PartitionResult:
105+
capability_partitioner = CapabilityBasedPartitioner(
106+
exported_program.graph_module,
107+
NodeFlagIsSetChecker(self.field),
108+
allows_single_node_partition=True,
109+
)
110+
partition_list = capability_partitioner.propose_partitions()
111+
112+
partition_tags = {}
113+
for partition in partition_list:
114+
for node in partition.nodes:
115+
tag = f"tag{partition.id}"
116+
node.meta["delegation_tag"] = tag
117+
partition_tags[tag] = self.delegation_spec
118+
119+
tag_constant_data(exported_program)
120+
tag_mutated_buffer(exported_program)
121+
122+
return PartitionResult(
123+
tagged_exported_program=exported_program, partition_tags=partition_tags
124+
)
125+
126+
127+
def mark_node_range(
128+
graph_module: torch.fx.GraphModule,
129+
end_idx: int = (2**31 - 1),
130+
start_idx: int = 0,
131+
field: str = "_in_target_subgraph",
132+
):
133+
call_fn_count = 0
134+
for node in graph_module.graph.nodes:
135+
if "custom" not in node.meta:
136+
node.meta["custom"] = {}
137+
138+
node.meta["custom"][field] = False
139+
140+
if node.op != "call_function":
141+
continue
142+
143+
call_fn_count += 1
144+
if call_fn_count >= start_idx and call_fn_count < end_idx:
145+
node.meta["custom"][field] = True
146+
147+
148+
def extract_submodule_program(
149+
tagged_graph_module: torch.fx.GraphModule,
150+
owning_program: ExportedProgram,
151+
field: str = "_in_target_subgraph",
152+
) -> ExportedProgram:
153+
tagged_graph_module_output_node = tagged_graph_module.graph.output_node()
154+
155+
partitioner = FlagBasedPartitioner(field)
156+
partition_result = partitioner.partition(owning_program)
157+
158+
tag, delegation_spec = next(iter(partition_result.partition_tags.items()))
159+
node_list = _get_node_list_with_same_tag(tagged_graph_module, tag, owning_program)
160+
161+
replace_ctx = tagged_graph_module._set_replace_hook(
162+
owning_program.graph_signature.get_replace_hook()
163+
)
164+
with replace_ctx:
165+
submodule, call_module_node = create_submodule_from_nodes(
166+
tagged_graph_module, node_list, tag
167+
)
168+
169+
submodule_output_node = submodule.graph.output_node()
170+
# Copy the output node meta from the original output node, because
171+
# create_submodule_from_nodes doesn't cover the meta field
172+
submodule_output_node.meta = tagged_graph_module_output_node.meta
173+
174+
(
175+
submodule_program,
176+
_,
177+
_,
178+
) = create_exported_program_from_submodule(
179+
submodule,
180+
owning_program,
181+
tag,
182+
call_module_node,
183+
False,
184+
)
185+
186+
return submodule_program
187+
188+
38189
class QuantizationMode(Enum):
39190
"""Enum to describe how a model should be quantized."""
40191

@@ -76,9 +227,97 @@ def random_uniform_tensor(shape, low=0.0, high=1.0, device=None, dtype=None):
76227
if dtype is None:
77228
dtype = torch.float32
78229

230+
# Handle integer types using randint
231+
if dtype in (
232+
torch.int,
233+
torch.int8,
234+
torch.int16,
235+
torch.int32,
236+
torch.int64,
237+
torch.long,
238+
torch.short,
239+
):
240+
low_int = int(low)
241+
high_int = int(high)
242+
# randint requires high > low, so ensure at least a range of 1
243+
if high_int <= low_int:
244+
high_int = low_int + 1
245+
return torch.randint(low_int, high_int, shape, device=device, dtype=dtype)
246+
247+
# Handle unsigned integer types
248+
if dtype in (torch.uint8,):
249+
low_int = max(0, int(low))
250+
high_int = int(high)
251+
if high_int <= low_int:
252+
high_int = low_int + 1
253+
return torch.randint(low_int, high_int, shape, device=device, dtype=dtype)
254+
255+
# Handle boolean type
256+
if dtype == torch.bool:
257+
return torch.randint(0, 2, shape, device=device, dtype=torch.int8).bool()
258+
259+
# Handle floating-point types (float16, float32, float64, bfloat16)
79260
return torch.empty(shape, device=device, dtype=dtype).uniform_(low, high)
80261

81262

263+
def generate_sample_inputs(
264+
exported_program: ExportedProgram,
265+
low: float = -1.0,
266+
high: float = 1.0,
267+
) -> Tuple[torch.Tensor, ...]:
268+
"""
269+
Analyze the exported program graph to determine input shapes and dtypes,
270+
then generate random sample inputs.
271+
272+
Uses the graph signature to identify only user inputs (excluding parameters,
273+
buffers, and other non-input placeholders).
274+
275+
Args:
276+
exported_program: The exported program to analyze
277+
low: Lower bound for random uniform values (default: -1.0)
278+
high: Upper bound for random uniform values (default: 1.0)
279+
280+
Returns:
281+
Tuple of randomly generated tensors matching the input specs
282+
"""
283+
sample_inputs = []
284+
285+
# Get the set of user input names by filtering input_specs for USER_INPUT kind
286+
user_input_names = set()
287+
for spec in exported_program.graph_signature.input_specs:
288+
if spec.kind == InputKind.USER_INPUT:
289+
if hasattr(spec.arg, "name"):
290+
user_input_names.add(spec.arg.name)
291+
292+
for node in exported_program.graph.nodes:
293+
if node.op != "placeholder":
294+
continue
295+
296+
# Only process nodes that are user inputs (not parameters, buffers, etc.)
297+
if node.name not in user_input_names:
298+
continue
299+
300+
if "val" in node.meta:
301+
val = node.meta["val"]
302+
shape = None
303+
dtype = None
304+
305+
if isinstance(val, torch.Tensor):
306+
shape = tuple(val.shape)
307+
dtype = val.dtype
308+
elif hasattr(val, "shape") and hasattr(val, "dtype"):
309+
# Handle FakeTensor or similar
310+
shape = tuple(val.shape)
311+
dtype = val.dtype
312+
313+
if shape is not None and dtype is not None:
314+
tensor = random_uniform_tensor(shape, low=low, high=high, dtype=dtype)
315+
sample_inputs.append(tensor)
316+
317+
inputs_flattened, _ = tree_flatten(sample_inputs)
318+
return inputs_flattened
319+
320+
82321
def export_model_to_vulkan(
83322
model,
84323
sample_inputs,
@@ -432,6 +671,49 @@ def lower_module_and_test_output(
432671
return True
433672

434673

674+
def create_bundled_program(
675+
executorch_program: ExecutorchProgramManager,
676+
sample_inputs: Tuple[torch.Tensor, ...],
677+
expected_outputs: List[Any],
678+
method_name: str = "forward",
679+
) -> bytes:
680+
"""
681+
Create a bundled program containing the model and test cases for correctness testing.
682+
683+
Args:
684+
executorch_program: The ExecutorchProgramManager to bundle
685+
sample_inputs: Sample inputs for the model
686+
expected_outputs: Expected outputs from running the model with sample_inputs
687+
method_name: Name of the method to test (default: "forward")
688+
689+
Returns:
690+
Serialized bundled program as bytes
691+
"""
692+
# Flatten sample inputs to match expected format
693+
inputs_flattened, _ = tree_flatten(sample_inputs)
694+
695+
# Create test suite with the sample inputs and expected outputs
696+
test_suites = [
697+
MethodTestSuite(
698+
method_name=method_name,
699+
test_cases=[
700+
MethodTestCase(
701+
inputs=inputs_flattened,
702+
expected_outputs=expected_outputs,
703+
)
704+
],
705+
)
706+
]
707+
708+
# Create bundled program
709+
bundled_program = BundledProgram(executorch_program, test_suites)
710+
711+
# Serialize to flatbuffer
712+
bundled_buffer = serialize_from_bundled_program_to_flatbuffer(bundled_program)
713+
714+
return bundled_buffer
715+
716+
435717
def save_bundled_program(
436718
model: torch.nn.Module,
437719
sample_inputs: Tuple[torch.Tensor],
@@ -470,27 +752,16 @@ def save_bundled_program(
470752
# Generate expected outputs by running the model
471753
expected_outputs = [getattr(model, method_name)(*sample_inputs, **sample_kwargs)]
472754

473-
# Flatten sample inputs to match expected format
755+
# Flatten sample inputs with kwargs to match expected format
474756
inputs_flattened, _ = tree_flatten((sample_inputs, sample_kwargs))
475757

476-
# Create test suite with the sample inputs and expected outputs
477-
test_suites = [
478-
MethodTestSuite(
479-
method_name=method_name,
480-
test_cases=[
481-
MethodTestCase(
482-
inputs=inputs_flattened,
483-
expected_outputs=expected_outputs,
484-
)
485-
],
486-
)
487-
]
488-
489758
# Create bundled program
490-
bp = BundledProgram(et_program, test_suites)
491-
492-
# Serialize to flatbuffer
493-
bp_buffer = serialize_from_bundled_program_to_flatbuffer(bp)
759+
bp_buffer = create_bundled_program(
760+
et_program,
761+
tuple(inputs_flattened),
762+
expected_outputs,
763+
method_name,
764+
)
494765

495766
# Ensure output path has correct extension
496767
if not output_path.endswith(".bpte"):

0 commit comments

Comments
 (0)