Skip to content

[QNN EP] Fix HardSigmoidMul fusion missing reversed Mul input ordering - #535

Open
qti-chuteng wants to merge 4 commits into
mainfrom
dev/chuteng/fix-hardsigmoid-mul-fusion
Open

qti-chuteng wants to merge 4 commits into
mainfrom
dev/chuteng/fix-hardsigmoid-mul-fusion

Conversation

@qti-chuteng

Copy link
Copy Markdown
Collaborator

Summary

The HardSigmoidMulFusion pattern matcher recognizes HardSwish(x) = x * HardSigmoid(x) and fuses the HardSigmoid + Mul pair into a single QNN ElementWiseNeuron (HardSwish) op. To accept the pair, it verifies that the HardSigmoid input is also one of the two Mul inputs. That check had a copy-paste bug — both sides of the || compared Mul.Inputs()[0]:

const bool same_root_input = mul->Inputs()[0].name == hs_input ||
                             mul->Inputs()[0].name == hs_input;  // [0] on both sides

So only the first Mul input was ever inspected. When a graph uses Mul(hardsigmoid_output, root_input) — HardSigmoid output as Inputs()[0], the root as Inputs()[1] — the matcher fails to recognize the root, and the pattern is silently left unfused, degrading to separate HardSigmoid + Mul ops on the backend.

Fix

Change the second comparison to Mul.Inputs()[1] so both input orderings — Mul(root, hsig_out) and Mul(hsig_out, root) — fuse correctly.

Test

Adds qnn_node_group/hardsigmoid_mul_fusion_test.cc with two cases (normal and reversed Mul input ordering). Each dumps the QNN JSON graph and asserts the fusion actually happened: the standalone ElementWiseMultiply (Mul) is gone (count = 0) and a single ElementWiseNeuron (HardSwish) remains (count = 1).

Why this wasn't already caught

Existing accuracy tests (HardSigmoidFusedIntoHardSwish_FP32_as_FP16, HardSigmoidFusedIntoHardSwish_FP16) already use the reversed ordering Mul(hsig_out, input), but they only assert ExpectedEPNodeAssignment::All. That passes whether or not the fusion happens — both ops are still assigned to the QNN EP either way — so the missed fusion went undetected. The new test asserts the graph topology, not just EP assignment.

Scope / risk

  • One-character logic fix ([0] → [1]) plus a new test file.
  • The fix only adds matches that were previously (incorrectly) rejected; it cannot cause a non-HardSwish pattern to fuse, because the rest of the matcher (op types, single child, alpha/beta values, no graph-output) is unchanged.

Testing

  • lintrunner — clean.
  • The new tests are HTP-backend fusion tests (guarded to arm64 / linux, skipped on HTP arch <= V68). Recommend running them on an HTP-capable environment to confirm both orderings fuse; under TDD the reversed-ordering case fails before this fix and passes after.

@CLAassistant

CLAassistant commented Jun 16, 2026 •

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@qti-chuteng
qti-chuteng force-pushed the dev/chuteng/fix-hardsigmoid-mul-fusion branch from 6c67578 to 9ca710c Compare June 16, 2026 07:48
qti-chuteng and others added 2 commits June 18, 2026 13:34
The HardSigmoidMulFusion pattern matcher checks that the HardSigmoid input is
also the other input to the Mul (so the pair forms HardSwish(x) = x * HardSigmoid(x)).
The check had a copy-paste bug: both sides of the || compared Mul.Inputs()[0],
so it only ever inspected the first Mul input:

  const bool same_root_input = mul->Inputs()[0].name == hs_input ||
                               mul->Inputs()[0].name == hs_input;  // [0] twice

As a result, when the graph used Mul(hardsigmoid_output, root_input) -- i.e. the
HardSigmoid output as Inputs()[0] and the root as Inputs()[1] -- the matcher
failed and the pattern was silently left unfused, falling back to separate ops.

Fix the second comparison to Inputs()[1] so both input orderings fuse.

Add hardsigmoid_mul_fusion_test.cc covering both orderings and asserting, via the
dumped QNN JSON graph, that the standalone Mul (ElementWiseMultiply) is gone and a
single ElementWiseNeuron (HardSwish) remains. Existing accuracy tests already use
the reversed ordering but only assert EP assignment -- which passes whether or not
fusion happens -- so the missed fusion was not caught.
- Remove the outer #if defined(_WIN32) around SKIP_HTP_TEST_ON_ARCH_...:
  the macro already guards platforms internally, and the extra guard
  prevented Linux aarch64 devices (arch <= V68) from skipping correctly.
  Now consistent with all sibling node_group fusion tests.
- Switch the new test file to the Qualcomm copyright header per repo
  convention for newly added files.
@qti-yuduo
qti-yuduo force-pushed the dev/chuteng/fix-hardsigmoid-mul-fusion branch from 9ca710c to 673ef20 Compare June 18, 2026 20:34
@@ -0,0 +1,110 @@
// Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should existing testcases moved inside?


// HardSigmoid -> Mul(input, hsig_out): HardSigmoid output is the SECOND Mul input.
// This is the ordering the original same_root_input check already handled.
TEST_F(QnnHTPBackendTests, HardSigmoidMulFusion_NormalOrder_Fuses) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CI failed for all HardSigmoidMulFusion testcases in accuracy.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants