Skip to content

Commit fb3fff7

Browse files
authored
Qualcomm: skip higher-order ops in LiftConstantScalarOperands (#22252)
The pass inspects node.target._schema to lift scalar operands, but higher-order ops (e.g. wrap_with_set_grad_enabled from a torch.no_grad block) have no OpOverload schema, so the pass crashed with AttributeError on a valid exported program (surfaced exporting Mamba2 / Mixtral MoE via the QNN backend). Skip non-OpOverload targets. cc @cbilgin
1 parent 273cb33 commit fb3fff7

3 files changed

Lines changed: 42 additions & 0 deletions

File tree

‎backends/qualcomm/_passes/lift_constant_scalar_operands.py‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,11 @@ def _lift(self, gm: torch.fx.GraphModule) -> None:
184184
if (
185185
n.op != "call_function"
186186
or isinstance(n.target, (BuiltinMethodType, BuiltinFunctionType))
187+
# Higher-order ops (e.g. wrap_with_set_grad_enabled, emitted for a
188+
# torch.no_grad block) carry no schema, and lifting reads
189+
# node.target._schema. Test for the schema rather than the
190+
# OpOverload type so EdgeOpOverload targets keep lifting.
191+
or not hasattr(n.target, "_schema")
187192
or n.target in SKIP_LIFT_OPS
188193
):
189194
continue

‎backends/qualcomm/tests/rework/src/pattern.py‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4457,6 +4457,12 @@ class _Where(torch.nn.Module):
44574457
def forward(self, x):
44584458
return torch.where(x > 0, 1.0, 0.0)
44594459

4460+
class _HigherOrderOps(torch.nn.Module):
4461+
def forward(self, x):
4462+
with torch.no_grad():
4463+
y = x + 1
4464+
return y * 2
4465+
44604466
@staticmethod
44614467
@unpack_pass_fixtures
44624468
def test(
@@ -4526,6 +4532,14 @@ def lower(module):
45264532
f"got {n.meta['val'].dtype} (use_self_dtype broken?)"
45274533
)
45284534

4535+
with subtests.test(msg="skip_higher_order_ops"):
4536+
# The pass reads node.target._schema; a higher-order op has none, so it
4537+
# must be skipped rather than crash the annotation pipeline.
4538+
gm = lower(LiftConstantScalarOperands._HigherOrderOps())
4539+
assertions.assert_target_count(
4540+
gm, torch.ops.higher_order.wrap_with_set_grad_enabled, 1
4541+
)
4542+
45294543

45304544
class LpaiPartitionFallbackSupport:
45314545
_SKIP_NODE_ID_SET = {"aten_add_tensor", "aten_mean_dim"}

‎backends/qualcomm/tests/test_passes.py‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
FoldQDQ,
1212
InsertIOQDQ,
1313
InsertReshapeForReduceOps,
14+
LiftConstantScalarOperands,
1415
RemoveRedundancy,
1516
)
1617
from executorch.backends.qualcomm._passes.qnn_pass_manager import (
@@ -117,6 +118,28 @@ def test_context_loader_edge_op_is_delegated(self):
117118
QnnOperatorSupport.is_node_supported(support, None, context_loader_nodes[0])
118119
)
119120

121+
def test_lift_constant_scalar_operands_skips_higher_order_ops(self):
122+
# A `with torch.no_grad()` block is captured as a higher-order op
123+
# (wrap_with_set_grad_enabled) whose target carries no schema.
124+
# LiftConstantScalarOperands reads node.target._schema, so it must skip
125+
# such nodes instead of crashing with AttributeError.
126+
class Model(torch.nn.Module):
127+
def forward(self, x):
128+
with torch.no_grad():
129+
y = x + 1
130+
return y * 2
131+
132+
gm = torch.export.export(Model(), (torch.randn(3),), strict=True).graph_module
133+
schemaless = [
134+
node
135+
for node in gm.graph.nodes
136+
if node.op == "call_function" and not hasattr(node.target, "_schema")
137+
]
138+
self.assertTrue(schemaless, "expected a higher-order op in the graph")
139+
140+
# Must not raise.
141+
LiftConstantScalarOperands().call(gm)
142+
120143
def test_build_op_wrappers_returns_context_binary(self):
121144
op_name = "ctx_loader_build"
122145
ctx_bin = b"qnn_context_binary"

0 commit comments

Comments
 (0)