Skip to content

Commit 49c6072

Browse files
authored
make device support config method-based (#19970)
Differential Revision: D101243687 Pull Request resolved: #19970
1 parent 9ccc4e7 commit 49c6072

11 files changed

Lines changed: 150 additions & 37 deletions

File tree

backends/cuda/tests/test_cuda_export.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -385,11 +385,13 @@ def forward(self, x: torch.Tensor, y: torch.Tensor) -> torch.Tensor:
385385
# Both input and output tensors should be on CUDA device for now.
386386
self.assertEqual(
387387
len(cpu_tensors),
388-
3,
389-
f"Expecteed three CPU tensors for method inputs and outputs, but found {len(cpu_tensors)}",
388+
0,
389+
f"Expected no CPU tensors: method inputs/outputs should be tagged "
390+
f"CUDA, but found {len(cpu_tensors)}",
390391
)
391392
self.assertEqual(
392393
len(cuda_tensors),
393394
3,
394-
"Expected CUDA tensors for delegate outputs",
395+
f"Expected 3 CUDA tensors (2 method inputs + 1 method output), "
396+
f"but found {len(cuda_tensors)}",
395397
)

exir/capture/BUCK

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ fbcode_target(_kind = runtime.python_library,
4747
"//executorch/exir:pass_manager",
4848
"//executorch/exir:tracer",
4949
"//executorch/exir/passes:lib",
50+
"//executorch/exir/passes:propagate_device_config",
5051
"//executorch/exir/passes:sym_shape_eval_pass",
5152
],
5253
)

exir/capture/_config.py

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from executorch.exir.dynamic_shape import DynamicMemoryPlanningMode
1414
from executorch.exir.pass_manager import PassType
1515
from executorch.exir.passes import MemoryPlanningPass, ToOutVarPass
16+
from executorch.exir.passes.propagate_device_config import PropagateDeviceConfig
1617
from executorch.exir.passes.sym_shape_eval_pass import ConstraintBasedSymShapeEvalPass
1718
from executorch.exir.tracer import ExirDynamoConfig
1819
from torch.fx._compatibility import compatibility
@@ -60,6 +61,13 @@ class ExecutorchBackendConfig:
6061
# A single memory planning pass can be defined for all the programs in the
6162
# EdgeProgramManager or can be defined per program.
6263
memory_planning_pass: Union[PassType, Dict[str, PassType]] = MemoryPlanningPass()
64+
65+
# A single propagate device config can be defined for all the programs in the
66+
# EdgeProgramManager or can be defined per program.
67+
propagate_device_config: Union[
68+
PropagateDeviceConfig, Dict[str, PropagateDeviceConfig]
69+
] = field(default_factory=PropagateDeviceConfig)
70+
6371
to_out_var_pass: PassType = ToOutVarPass(ignore_to_out_var_failure=False)
6472
dynamic_memory_planning_mode: DynamicMemoryPlanningMode = (
6573
DynamicMemoryPlanningMode.UPPER_BOUND
@@ -124,18 +132,6 @@ class ExecutorchBackendConfig:
124132
# where all tensors are planned into CPU memory regardless of device.
125133
enable_non_cpu_memory_planning: bool = False
126134

127-
# When True, method-level input tensors that feed directly into a device
128-
# delegate are NOT wrapped with _h2d_copy. The user must provide tensors
129-
# already on the target device. Useful for pipelines where inputs are
130-
# pre-staged on GPU.
131-
skip_h2d_for_method_inputs: bool = False
132-
133-
# When True, device delegate outputs that are directly method outputs
134-
# are NOT wrapped with _d2h_copy. The method outputs stay on device.
135-
# Useful for cross-method GPU pipelines where the next method consumes
136-
# GPU tensors directly.
137-
skip_d2h_for_method_outputs: bool = False
138-
139135
# Add ops to the set of re-inplace ops to be used by the reinplace pass.
140136
# Re-inplace pass checks the eligibility of an op to be re-inplaced and
141137
# memory planning pass allcoates the output buffer of the op to be the same

exir/passes/BUCK

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -460,13 +460,24 @@ fbcode_target(_kind = runtime.python_library,
460460
],
461461
)
462462

463+
fbcode_target(_kind = runtime.python_library,
464+
name = "propagate_device_config",
465+
srcs = [
466+
"propagate_device_config.py",
467+
],
468+
deps = [
469+
"//caffe2:torch",
470+
],
471+
)
472+
463473
fbcode_target(_kind = runtime.python_library,
464474
name = "propagate_device_pass",
465475
srcs = [
466476
"propagate_device_pass.py",
467477
],
468478
deps = [
469479
":device_copy_ops_registry",
480+
":propagate_device_config",
470481
"//caffe2:torch",
471482
"//executorch/exir:delegate",
472483
"//executorch/exir:lowered_backend_module",

exir/passes/memory_planning_pass.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,6 @@ def __init__(
153153
alloc_mutable_buffers: bool = True,
154154
share_mutable_buffers: bool = False,
155155
alignment: int = ALIGNMENT,
156-
enable_non_cpu_memory_planning: bool = False,
157156
) -> None:
158157
r"""
159158
alloc_graph_input/alloc_graph_output will have 4 different combinations
@@ -174,8 +173,11 @@ def __init__(
174173
self.alloc_mutable_buffers = alloc_mutable_buffers
175174
self.share_mutable_buffers = share_mutable_buffers
176175
self.alignment = alignment
177-
self.enable_non_cpu_memory_planning = enable_non_cpu_memory_planning
178176
self.state = _MemoryPlanningState()
177+
# Set by EdgeProgramManager.to_executorch() from the top-level
178+
# ExecutorchBackendConfig. When True, apply_algo partitions specs by
179+
# device so non-CPU buffers get their own memory arenas.
180+
self.enable_non_cpu_memory_planning: bool = False
179181

180182
def _set_alloc_node_spec(self, graph_module: torch.fx.GraphModule) -> None:
181183
"""
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# Copyright (c) Meta Platforms, Inc. and affiliates.
2+
# All rights reserved.
3+
#
4+
# This source code is licensed under the BSD-style license found in the
5+
# LICENSE file in the root directory of this source tree.
6+
7+
# pyre-strict
8+
9+
"""
10+
Configuration for PropagateDevicePass.
11+
12+
This is intentionally kept in a lightweight module (no heavy imports such as
13+
the et_copy op registry) so that ``ExecutorchBackendConfig`` -- which is
14+
imported throughout the codebase -- can reference ``PropagateDeviceConfig``
15+
without pulling in the device-copy op registration as an import-time side
16+
effect.
17+
"""
18+
19+
from dataclasses import dataclass
20+
from typing import Dict, Union
21+
22+
from torch.fx._compatibility import compatibility
23+
24+
25+
@compatibility(is_backward_compatible=False)
26+
@dataclass
27+
class PropagateDeviceConfig:
28+
# When True, method-level input tensors that feed directly into a device
29+
# delegate are NOT wrapped with _h2d_copy. The user must provide tensors
30+
# already on the target device. Useful for pipelines where inputs are
31+
# pre-staged on GPU.
32+
# A dict can be used to set per-method values, keyed by method name.
33+
skip_h2d_for_method_inputs: Union[bool, Dict[str, bool]] = False
34+
35+
# When True, device delegate outputs that are directly method outputs
36+
# are NOT wrapped with _d2h_copy. The method outputs stay on device.
37+
# Useful for cross-method GPU pipelines where the next method consumes
38+
# GPU tensors directly.
39+
# A dict can be used to set per-method values, keyed by method name.
40+
skip_d2h_for_method_outputs: Union[bool, Dict[str, bool]] = False
41+
42+
def __hash__(self) -> int:
43+
return hash(
44+
(
45+
str(self.skip_h2d_for_method_inputs),
46+
str(self.skip_d2h_for_method_outputs),
47+
)
48+
)
49+
50+
def __eq__(self, other: object) -> bool:
51+
if not isinstance(other, PropagateDeviceConfig):
52+
return False
53+
return (
54+
self.skip_h2d_for_method_inputs == other.skip_h2d_for_method_inputs
55+
and self.skip_d2h_for_method_outputs == other.skip_d2h_for_method_outputs
56+
)

exir/passes/propagate_device_pass.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,13 @@
1919
import torch
2020
from executorch.exir.delegate import executorch_call_delegate
2121
from executorch.exir.lowered_backend_module import LoweredBackendModule
22+
23+
# Re-exported for backward compatibility; the dataclass lives in a lightweight
24+
# module so that ExecutorchBackendConfig can reference it without importing the
25+
# et_copy op registry above.
26+
from executorch.exir.passes.propagate_device_config import ( # noqa: F401
27+
PropagateDeviceConfig,
28+
)
2229
from executorch.exir.tensor import TensorSpec
2330
from torch.fx.passes.infra.pass_base import PassBase, PassResult
2431

@@ -172,6 +179,17 @@ def __init__(
172179
self.skip_d2h_for_method_outputs = skip_d2h_for_method_outputs
173180
self.enable_non_cpu_memory_planning = enable_non_cpu_memory_planning
174181

182+
if (
183+
skip_h2d_for_method_inputs or skip_d2h_for_method_outputs
184+
) and not enable_non_cpu_memory_planning:
185+
raise ValueError(
186+
"skip_h2d_for_method_inputs and skip_d2h_for_method_outputs are "
187+
"only meaningful when enable_non_cpu_memory_planning=True, since "
188+
"they control host/device copy insertion which only happens during "
189+
"device-aware memory planning. Set enable_non_cpu_memory_planning="
190+
"True, or leave the skip options disabled."
191+
)
192+
175193
def _is_placeholder(self, node: torch.fx.Node) -> bool:
176194
"""Check if a node is a graph-level input (placeholder)."""
177195
return node.op == "placeholder"

exir/program/BUCK

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ fbcode_target(_kind = runtime.python_library,
4141
"//executorch/exir/passes:insert_write_back_for_buffers_pass",
4242
"//executorch/exir/passes:lib",
4343
"//executorch/exir/passes:normalize_view_copy_base_pass",
44+
"//executorch/exir/passes:propagate_device_config",
4445
"//executorch/exir/passes:propagate_device_pass",
4546
"//executorch/exir/passes:remove_graph_asserts_pass",
4647
"//executorch/exir/passes:remove_mixed_type_operators",

exir/program/_program.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
from executorch.exir.passes.normalize_view_copy_base_pass import (
6060
NormalizeViewCopyBasePass,
6161
)
62+
from executorch.exir.passes.propagate_device_config import PropagateDeviceConfig
6263
from executorch.exir.passes.propagate_device_pass import PropagateDevicePass
6364
from executorch.exir.passes.quant_fusion_pass import quant_fusion_and_const_prop_pass
6465
from executorch.exir.passes.reinplace import DEFAULT_INPLACEABLE_OPS, reinplace_pass
@@ -758,15 +759,22 @@ def edge_to_executorch_passes(
758759
Returns a list of passes to lower from edge to executorch.
759760
Get the pre memory planning passes based on the method name, if the pass is not in the dict, use the default pass.
760761
"""
762+
# Handle propagate device config
763+
propagate_device_config = config.propagate_device_config
764+
if isinstance(propagate_device_config, dict):
765+
device_cfg = propagate_device_config.get(name, PropagateDeviceConfig())
766+
else:
767+
device_cfg = propagate_device_config
768+
761769
passes: List[PassType] = [
762770
# ExecuTorch backend ops are unable to handle unbacked symints. So after
763771
# this pass, passes cannot be Interpreter-based, because it will fail if
764772
# there exists an unbacked symint operation.
765773
*config.passes,
766774
SpecPropPass(),
767775
PropagateDevicePass(
768-
skip_h2d_for_method_inputs=config.skip_h2d_for_method_inputs,
769-
skip_d2h_for_method_outputs=config.skip_d2h_for_method_outputs,
776+
skip_h2d_for_method_inputs=device_cfg.skip_h2d_for_method_inputs,
777+
skip_d2h_for_method_outputs=device_cfg.skip_d2h_for_method_outputs,
770778
enable_non_cpu_memory_planning=config.enable_non_cpu_memory_planning,
771779
),
772780
EdgeToBackendOpsPass(),

exir/tests/test_propagate_device_pass.py

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
from executorch.exir.passes.propagate_device_pass import (
3333
_get_target_device_from_compile_specs,
3434
_parse_device_spec_value,
35+
PropagateDeviceConfig,
3536
TARGET_DEVICE_COMPILE_SPEC_KEY,
3637
)
3738
from executorch.exir.schema import DeviceType
@@ -766,7 +767,9 @@ def forward(self, a, b):
766767
inputs = (torch.randn(2, 2), torch.randn(2, 2))
767768
et_config = ExecutorchBackendConfig(
768769
emit_stacktrace=False,
769-
skip_h2d_for_method_inputs=True,
770+
propagate_device_config=PropagateDeviceConfig(
771+
skip_h2d_for_method_inputs=True
772+
),
770773
enable_non_cpu_memory_planning=True,
771774
)
772775

@@ -822,7 +825,9 @@ def forward(self, a, b):
822825
inputs = (torch.randn(2, 2), torch.randn(2, 2))
823826
et_config = ExecutorchBackendConfig(
824827
emit_stacktrace=False,
825-
skip_d2h_for_method_outputs=True,
828+
propagate_device_config=PropagateDeviceConfig(
829+
skip_d2h_for_method_outputs=True
830+
),
826831
enable_non_cpu_memory_planning=True,
827832
)
828833

@@ -876,8 +881,10 @@ def forward(self, a, b):
876881
inputs = (torch.randn(2, 2), torch.randn(2, 2))
877882
et_config = ExecutorchBackendConfig(
878883
emit_stacktrace=False,
879-
skip_h2d_for_method_inputs=True,
880-
skip_d2h_for_method_outputs=True,
884+
propagate_device_config=PropagateDeviceConfig(
885+
skip_h2d_for_method_inputs=True,
886+
skip_d2h_for_method_outputs=True,
887+
),
881888
enable_non_cpu_memory_planning=True,
882889
)
883890

@@ -952,7 +959,9 @@ def forward(self, a, b):
952959
inputs = (torch.randn(2, 2), torch.randn(2, 2))
953960
et_config = ExecutorchBackendConfig(
954961
emit_stacktrace=False,
955-
skip_h2d_for_method_inputs=True,
962+
propagate_device_config=PropagateDeviceConfig(
963+
skip_h2d_for_method_inputs=True
964+
),
956965
enable_non_cpu_memory_planning=True,
957966
)
958967

0 commit comments

Comments
 (0)