Skip to content

Commit 3efa072

Browse files
committed
Update
[ghstack-poisoned]
1 parent 282dab8 commit 3efa072

17 files changed

Lines changed: 1246 additions & 64 deletions

backends/vulkan/_passes/fuse_patterns.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import torch
1212

1313
from executorch.exir import ExportedProgram
14+
from executorch.exir.dialects._ops import ops as exir_ops
1415
from executorch.exir.pass_base import ExportPass, PassResult
1516

1617

@@ -27,6 +28,20 @@ def call(self, graph_module: torch.fx.GraphModule):
2728
)
2829

2930
if total_replaced > 0:
31+
for node in list(graph_module.graph.nodes):
32+
if node.target != exir_ops.edge.et_vk.select_as_symint.default:
33+
continue
34+
value_range = node.meta.get("et_vk_value_range")
35+
if value_range is None:
36+
continue
37+
lower_bound, upper_bound = value_range
38+
with graph_module.graph.inserting_after(node):
39+
graph_module.graph.create_node(
40+
"call_function",
41+
exir_ops.edge.aten.sym_constrain_range.default,
42+
args=(node,),
43+
kwargs={"min": lower_bound, "max": upper_bound},
44+
)
3045
graph_module.recompile()
3146
# Re-trace the graph
3247
graph_module = super().call(graph_module).graph_module

backends/vulkan/_passes/remove_asserts.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ class RemoveAssertsTransform(ExportPass):
2828

2929
assert_ops: Set[OpType] = {
3030
torch.ops.aten._assert_scalar.default,
31+
torch.ops.aten.sym_constrain_range.default,
3132
torch.ops.aten.sym_constrain_range_for_size.default,
3233
}
3334

backends/vulkan/_passes/squeeze_unsqueeze_inputs.py

Lines changed: 45 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@
1616
from torch._ops import OpOverload
1717

1818
from torch.fx.node import Argument
19+
from torch.fx.experimental.symbolic_shapes import (
20+
statically_known_false,
21+
statically_known_true,
22+
sym_and,
23+
)
1924

2025
OpType = Union[str, OpOverload, EdgeOpOverload]
2126

@@ -26,21 +31,47 @@ class SqueezeUnsqueezeInputs(ExportPass):
2631
exir_ops.edge.aten.gelu.default,
2732
}
2833

34+
@staticmethod
35+
def _first_static_one(shape: List[int]) -> Union[int, None]: # pyre-ignore
36+
for index, dim in enumerate(shape):
37+
if statically_known_true(dim == 1):
38+
return index
39+
return None
40+
41+
def _squeezed_shape(self, shape: List[int]) -> List[int]: # pyre-ignore
42+
squeezed_shape = list(shape)
43+
while len(squeezed_shape) > 2:
44+
index = self._first_static_one(squeezed_shape)
45+
if index is None:
46+
break
47+
squeezed_shape.pop(index)
48+
return squeezed_shape
49+
2950
def should_squeeze(self, op, shape: List[int]) -> bool: # pyre-ignore
3051
if len(shape) == 3:
31-
return shape[1] == 1 and shape[0] > 1
52+
return statically_known_true(sym_and(shape[1] == 1, shape[0] > 1))
3253
if len(shape) == 4:
33-
# No need to squeeze if all dims are 1 except the width dim
34-
if shape[0] == shape[1] == shape[2] == 1:
35-
return False
36-
# No need to squeeze if batch and channel dims are 1 and height and width are > 1
37-
if shape[0] == shape[1] == 1 and shape[2] > 1 and shape[3] > 1:
38-
return False
39-
# No need to squeeze if batch dim is 1 and channel, height and width are > 1
40-
if shape[0] == 1 and shape[1] > 1 and shape[2] > 1 and shape[3] > 1:
54+
excluded_shapes = (
55+
sym_and(shape[0] == 1, shape[1] == 1, shape[2] == 1),
56+
sym_and(
57+
shape[0] == 1,
58+
shape[1] == 1,
59+
shape[2] > 1,
60+
shape[3] > 1,
61+
),
62+
sym_and(
63+
shape[0] == 1,
64+
shape[1] > 1,
65+
shape[2] > 1,
66+
shape[3] > 1,
67+
),
68+
)
69+
if any(
70+
not statically_known_false(excluded_shape)
71+
for excluded_shape in excluded_shapes
72+
):
4173
return False
42-
# Otherwise, check for squeezable dim
43-
return 1 in shape[:-1]
74+
return self._first_static_one(shape[:-1]) is not None
4475

4576
# Prefer not to introduce additional orchestration ops by default
4677
return False
@@ -61,18 +92,13 @@ def call_operator(
6192
if not self.should_squeeze(op, input_shape):
6293
return super().call_operator(op, args, kwargs, meta)
6394

64-
def _squeezable(shape: List[int]) -> bool:
65-
return len(shape) > 2 and 1 in shape
66-
6795
# squeeze input tensor
68-
squeeze_shape = list(input_shape)
69-
while _squeezable(squeeze_shape):
70-
squeeze_shape.remove(1)
96+
squeeze_shape = self._squeezed_shape(input_shape)
7197

7298
squeeze_out = super().call_operator(
7399
exir_ops.edge.aten.view_copy.default,
74100
(args[0], squeeze_shape),
75-
kwargs,
101+
{},
76102
meta,
77103
)
78104
# call linear on squeezed output
@@ -88,6 +114,6 @@ def _squeezable(shape: List[int]) -> bool:
88114
return super().call_operator(
89115
exir_ops.edge.aten.view_copy.default,
90116
(linear_out, unsqueeze_shape),
91-
kwargs,
117+
{},
92118
meta,
93119
)

backends/vulkan/custom_ops_lib.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -882,11 +882,22 @@ def apply_rotary_emb_hf_impl(
882882
return pattern.forward(xq, xk, freqs_cos, freqs_sin)
883883

884884

885+
def apply_rotary_emb_hf_meta(
886+
xq: torch.Tensor,
887+
xk: torch.Tensor,
888+
freqs_cos: torch.Tensor,
889+
freqs_sin: torch.Tensor,
890+
start_pos: int,
891+
):
892+
return torch.empty_like(xq), torch.empty_like(xk)
893+
894+
885895
name = "apply_rotary_emb_hf"
886896
lib.define(
887897
f"{name}(Tensor xq, Tensor xk, Tensor freqs_cos, Tensor freqs_sin, SymInt start_pos) -> (Tensor, Tensor)"
888898
)
889899
lib.impl(name, apply_rotary_emb_hf_impl, "CompositeExplicitAutograd")
900+
lib.impl(name, apply_rotary_emb_hf_meta, "Meta")
890901
apply_rotary_emb_hf_op = getattr(getattr(torch.ops, namespace), name)
891902

892903
##################################
@@ -1074,7 +1085,7 @@ def embedding_q4gsw_impl(
10741085
scales = (
10751086
weight_scales.unsqueeze(-1)
10761087
if weight_scales.dim() > 1
1077-
else weight_scales.reshape(1, 1, 1)
1088+
else weight_scales.reshape(weight.shape[0], 1, 1)
10781089
)
10791090
dequantized = unpacked_groups.float() * scales.float()
10801091
dequantized = dequantized.reshape(weight.shape[0], -1)
@@ -1098,8 +1109,15 @@ def select_as_symint_impl(x: torch.Tensor, dim: int, index: int):
10981109
return x.fake_mode.shape_env.create_unbacked_symint()
10991110

11001111

1112+
def select_as_symint_eager_impl(x: torch.Tensor, dim: int, index: int):
1113+
if x.dtype not in {torch.int32, torch.int64}:
1114+
raise ValueError("select_as_symint requires an integral input")
1115+
return x.select(dim, index).item()
1116+
1117+
11011118
name = "select_as_symint"
11021119
lib.define(f"{name}(Tensor x, int dim, int index) -> SymInt")
1120+
lib.impl(name, select_as_symint_eager_impl, "CompositeExplicitAutograd")
11031121
lib.impl(name, select_as_symint_impl, "Meta")
11041122
select_as_symint_op = getattr(getattr(torch.ops, namespace), name)
11051123

backends/vulkan/op_registry.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,15 @@ def update_features_impl(op: OpKey):
152152
# =============================================================================
153153

154154

155+
@update_features(exir_ops.edge.et_vk.select_as_symint.default)
156+
def register_select_as_symint_op():
157+
return OpFeatures(
158+
inputs_dtypes=utils.INT_T,
159+
inputs_storage=utils.ANY_STORAGE,
160+
supports_resize=True,
161+
)
162+
163+
155164
@update_features(
156165
[
157166
operator.getitem,
@@ -161,6 +170,7 @@ def update_features_impl(op: OpKey):
161170
operator.sub,
162171
operator.floordiv,
163172
operator.mul,
173+
operator.and_,
164174
operator.lt,
165175
operator.gt,
166176
operator.ge,

backends/vulkan/partitioner/vulkan_partitioner.py

Lines changed: 89 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
# pyre-strict
88

99
import logging
10+
import operator
1011
from typing import Any, Callable, Dict, final, List, Mapping, Optional, Set, Tuple
1112

1213
import executorch.backends.vulkan.patterns as vk_patterns
@@ -54,6 +55,58 @@
5455
logger: logging.Logger = logging.getLogger("")
5556
logger.setLevel(logging.INFO)
5657

58+
FP_T = utils.FP_T
59+
INT_T = utils.INT_T
60+
NONE_T = utils.NONE_T
61+
CONTIGUOUS_BUFFER = utils.CONTIGUOUS_BUFFER
62+
NO_STORAGE = utils.NO_STORAGE
63+
64+
65+
_GUARD_ONLY_SYMBOLIC_OPS: Set[Any] = {torch.sym_min, torch.sym_max}
66+
_GUARD_ONLY_EXPRESSION_OPS: Set[Any] = _GUARD_ONLY_SYMBOLIC_OPS | {
67+
operator.add,
68+
operator.sub,
69+
operator.floordiv,
70+
operator.mul,
71+
operator.and_,
72+
operator.lt,
73+
operator.gt,
74+
operator.ge,
75+
operator.le,
76+
operator.eq,
77+
}
78+
_GUARD_ONLY_SINK_OPS: Set[Any] = {
79+
torch.ops.aten._assert_scalar.default,
80+
torch.ops.aten.sym_constrain_range_for_size.default,
81+
}
82+
83+
84+
def _is_guard_only_symbolic_node(node: torch.fx.Node) -> bool:
85+
if node.target not in _GUARD_ONLY_SYMBOLIC_OPS or not node.users:
86+
return False
87+
88+
pending = list(node.users)
89+
visited: Set[torch.fx.Node] = set()
90+
reached_sink = False
91+
while pending:
92+
user = pending.pop()
93+
if user in visited:
94+
continue
95+
visited.add(user)
96+
97+
if user.op != "call_function" or utils.is_tensor_node(user):
98+
return False
99+
if user.target in _GUARD_ONLY_SINK_OPS:
100+
if user.users:
101+
return False
102+
reached_sink = True
103+
continue
104+
if user.target not in _GUARD_ONLY_EXPRESSION_OPS or not user.users:
105+
return False
106+
pending.extend(user.users)
107+
108+
return reached_sink
109+
57110

58111
class VulkanSupportedOperators(OperatorSupportBase):
59112
def __init__(
@@ -67,6 +120,7 @@ def __init__(
67120
fusable_subgraphs: Optional[List[PatternMatch]] = None,
68121
nn_module_blocklist: Optional[Set[str]] = None,
69122
nn_module_allowlist: Optional[Set[str]] = None,
123+
extra_op_features: Optional[Mapping[OpKey, OpFeatures]] = None,
70124
) -> None:
71125
super().__init__()
72126
self.texture_limits: utils.ImageExtents = texture_limits
@@ -87,6 +141,7 @@ def __init__(
87141

88142
self.nn_module_blocklist = nn_module_blocklist
89143
self.nn_module_allowlist = nn_module_allowlist
144+
self.extra_op_features: Dict[OpKey, OpFeatures] = dict(extra_op_features or {})
90145

91146
def op_node_is_compatible( # noqa: C901: Function is too complex
92147
self, node: torch.fx.Node, features: Optional[OpFeatures] = None
@@ -147,11 +202,23 @@ def op_node_is_compatible( # noqa: C901: Function is too complex
147202
def node_is_compatible(
148203
self, node: torch.fx.Node, features: Optional[OpFeatures] = None
149204
) -> Tuple[bool, str]:
205+
# Guard-only symbolic nodes (sym_min/sym_max feeding only asserts or
206+
# range constraints) are non-tensor, so this must run before the
207+
# is_tensor_node dispatch below or it is unreachable.
208+
if getattr(node, "target", None) in _GUARD_ONLY_SYMBOLIC_OPS:
209+
if _is_guard_only_symbolic_node(node):
210+
return True, "guard-only symbolic node"
211+
self.log_skip(node, "symbolic result has a live non-guard user")
212+
return False, "symbolic result has a live non-guard user"
213+
150214
if utils.is_tensor_node(node):
151215
return self.op_node_is_compatible(node, features=features)
152216
# For non-tensor nodes, just check if the op is registered
153217
elif hasattr(node, "target"):
154-
return node.target in vulkan_supported_ops, "Op is compatible"
218+
return (
219+
features is not None or node.target in vulkan_supported_ops,
220+
"Op is compatible",
221+
)
155222

156223
return False, f"Unsupported node type: {node.format_node()}"
157224

@@ -244,17 +311,22 @@ def _is_node_supported(self, node: torch.fx.Node) -> bool: # noqa: C901
244311
self.log_skip(node, "permute node of non compatible linear node")
245312
return False
246313

247-
features = None
248-
if target not in vulkan_supported_ops:
314+
features: Optional[OpFeatures] = None
315+
if target in vulkan_supported_ops:
316+
features = vulkan_supported_ops[target]
317+
elif target in self.extra_op_features:
318+
features = self.extra_op_features[target]
319+
else:
249320
# For some ops, i.e. custom ops the name is registered instead of the
250321
# OpOverload object.
251-
if hasattr(target, "name") and target.name() in vulkan_supported_ops:
252-
features = vulkan_supported_ops[target.name()]
322+
target_name = target.name() if hasattr(target, "name") else None
323+
if target_name in vulkan_supported_ops:
324+
features = vulkan_supported_ops[target_name]
325+
elif target_name in self.extra_op_features:
326+
features = self.extra_op_features[target_name]
253327
else:
254328
self.log_skip(node, "no operator implementation")
255329
return False
256-
else:
257-
features = vulkan_supported_ops[target]
258330

259331
assert features is not None
260332

@@ -341,6 +413,7 @@ def __init__(
341413
operator_allowlist: Optional[List[OpKey]] = None,
342414
nn_module_blocklist: Optional[List[str]] = None,
343415
nn_module_allowlist: Optional[List[str]] = None,
416+
extra_op_features: Optional[Mapping[OpKey, OpFeatures]] = None,
344417
) -> None:
345418
self.options: Dict[str, Any] = {}
346419
if compile_options is not None:
@@ -349,6 +422,14 @@ def __init__(
349422
compile_spec = parse_compile_options(self.options)
350423
self.delegation_spec = DelegationSpec(VulkanBackend.__name__, compile_spec)
351424

425+
self.extra_op_features: Dict[OpKey, OpFeatures] = dict(extra_op_features or {})
426+
overlapping_ops = self.extra_op_features.keys() & vulkan_supported_ops.keys()
427+
if overlapping_ops:
428+
raise ValueError(
429+
"extra_op_features contains operators already registered globally: "
430+
f"{sorted(str(op) for op in overlapping_ops)}"
431+
)
432+
352433
self.operator_blocklist: Set[OpKey] = set()
353434
if operator_blocklist is not None:
354435
for entry in operator_blocklist or []:
@@ -415,6 +496,7 @@ def partition(self, exported_program: ExportedProgram) -> PartitionResult:
415496
fusable_subgraphs=fusable_subgraphs,
416497
nn_module_blocklist=self.nn_module_blocklist,
417498
nn_module_allowlist=self.nn_module_allowlist,
499+
extra_op_features=self.extra_op_features,
418500
),
419501
allows_single_node_partition=True,
420502
)

0 commit comments

Comments
 (0)