77# pyre-strict
88
99import logging
10+ import operator
1011from typing import Any , Callable , Dict , final , List , Mapping , Optional , Set , Tuple
1112
1213import executorch .backends .vulkan .patterns as vk_patterns
5455logger : logging .Logger = logging .getLogger ("" )
5556logger .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
58111class 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