Repository navigation
Conversation
|
Azure Pipelines: There may be pipelines that require an authorized user to comment /azp run to run. |
Review: PR #31957 β Hardswish Fusion implement for Mobilenetv3 models (head
|
| x | Gate = clamp(x/6 + 0.5, 0, 1) | y = xΒ·Gate | Expected |
|---|---|---|---|
| β3 | clamp(0, 0, 1) = 0 | 0 | 0 β |
| 0 | 0.5 | 0 | 0 β |
| 3 | clamp(1, 0, 1) = 1 | 3 | 3 β |
| 6 | clamp(1.5, 0, 1) = 1 | 6 | 6 β |
Scalar fallback correctly uses the same math with std::min/std::max. On SSE2 the scalar path routes through the SIMD kernel via _mm_set_ss/_mm_cvtss_f32, which is what HardSigmoid does β style-consistent. β
2. fused_activation.cc
Adds one else-if to the GetFusedActivationAttr stringβenum lookup, placed in the parameter-less branch (correct β HardSwish has no runtime params, unlike HardSigmoid). One-liner. β
3. nchwc_transformer.cc: the pattern matcher
TryFuseNchwcHardSwish is called from the top of TransformActivation when the activation node is HardSigmoid consuming an NCHWc conv output. The pattern to match:
conv (NCHWc) β x
ββ HardSigmoid(x, Ξ±=1/6, Ξ²=1/2) β gate
ββ Mul(x, gate) β output
Guards (all correct, each necessary):
hardsigmoid.OpType() == "HardSigmoid"and exactly one output edge.|Ξ± β 1/6| β€ 1e-6and|Ξ² β 0.5| β€ 1e-6(with defaultsΞ± = 0.2,Ξ² = 0.5β so a default HardSigmoid never matches, which is the right disambiguation from ONNX default).- Producing conv is
com.microsoft.nchwc.Conv. nchwc_input.starting_original_uses_ == 2β exactly two consumers of the conv output (the HardSigmoid + Mul, nothing else). β This is the crucial guard against fusing away a conv output that has a third consumer we'd break.- Conv doesn't already have an
activationattribute (no double-fusion). - The sibling Mul has exactly two inputs, is in the ONNX domain, and consumes both
conv_output_argand the HardSigmoid output. Handles ONNX's commutative Mul operand order (checks either position). - Neither HardSigmoid nor Mul produces a graph output.
Rewrite:
nchwc_node.AddAttribute("activation", std::string("HardSwish"));
nchwc_input.remaining_original_uses_--; // HardSigmoid use
nchwc_input.remaining_original_uses_--; // Mul use
graph_utils::RemoveNodeOutputEdges(graph_, hardsigmoid);
removed_nodes_.push_front(hardsigmoid.Index());
FuseNchwcArgument(*mul_node, nchwc_input);
removed_nodes_.push_front(mul_node->Index());Decrement-by-2 correctly matches the double-consumption. FuseNchwcArgument(mul_node, ...) routes the Mul's output NodeArg onto the NCHWc arg β same pattern the transformer already uses for Conv-Add-Activation fusion. β
Fallthrough is preserved and tested
If TryFuseNchwcHardSwish returns false, TransformActivation continues to the standard activation-fusion path β so a standalone HardSigmoid still becomes an NCHWc-Conv activation="HardSigmoid". This is verified by the second new test ConvNchwcHardSigmoidNotHardSwish. β Excellent test-of-negative.
Correctness spot-checks
Consumer walking
const auto consumers = graph_.GetConsumerNodes(conv_output_arg->Name());
for (const Node* consumer : consumers) {
if (consumer == &hardsigmoid) continue;
Node* candidate = graph_.GetNode(consumer->Index());
// ... check candidate is a matching Mul ...
mul_node = candidate;
}Since starting_original_uses_ == 2 was already asserted, there are exactly two consumers, one is the HardSigmoid, so this loop iterates exactly once for the candidate. If the candidate isn't a matching Mul, the function returns false. If it is, mul_node is set. β
Note: if a pathological input somehow had Mul(x, x) instead of Mul(x, HardSigmoid(x)), then consumes_hs would be false and the check returns false. β Also caught: the starting_original_uses_ != 2 guard would trip first because Mul(x, x) counts as two uses on x from the same Mul, plus one from HardSigmoid = 3.
orig_input_arg capture ordering
if (nchwc_input != nullptr) {
NodeArg* orig_input_arg = input_defs[0]; // β captured BEFORE rewrite
if (TryFuseNchwcHardSwish(node, orig_input_arg, *nchwc_input)) return;
input_defs[0] = nchwc_input->nchwc_arg_; // β rewrite happens after
...
}TryFuseNchwcHardSwish needs to consult graph_.GetConsumerNodes(conv_output_arg->Name()), which must key off the pre-rewrite name. Capturing before the rewrite is correct. β (A follow-on commit 7381125 β "scope orig_input_arg to nchwc guard; clarify fallthrough" β explicitly scoped this to reinforce the ordering.)
Default HardSigmoid safety
ONNX HardSigmoid defaults are Ξ± = 0.2, Ξ² = 0.5. The code correctly reads alpha = 0.2f when no attribute is present. Then |0.2 - 1/6| = 0.033... > 1e-6f β bail. So a plain HardSigmoid without an explicit Ξ±=1/6 is never misidentified. β This is precisely what ConvNchwcHardSigmoidNotHardSwish verifies.
Epsilon slack
1e-6f tolerance on |Ξ± β 1/6| and |Ξ² β 0.5| accepts float-exporter roundoff without being lax. 1.0f/6.0f in single precision is 0x3E2AAAAB = 0.16666667β¦, so exporters that emit exactly 0.16666666 (bit-diff 1 ulp) still match. Reasonable.
Test coverage
ConvNchwcHardSwishFusion: builds the diamond with the correct HardSwish params, checksHardSigmoid == 0,Mul == 0, andnchwc.Conv == 2. βConvNchwcHardSigmoidNotHardSwish: default-param HardSigmoid still fuses as HardSigmoid activation, verifiedHardSigmoid == 0(fused-away, not misidentified). β
Both tests are strong. A small strengthening (non-blocking, see nits) would be to look up the resulting NCHWc Conv and assert activation="HardSwish" explicitly.
Nits (non-blocking)
- CLA. Bot has prompted
@swetha097. Must be signed before merge β hard gate. - Full CI. 0/1 β external contributor,
@microsoft/onnxruntimemaintainer needs to/azp runto unlock Azure Pipelines. - Native
HardSwishopset-14 op is not covered. This PR handles the decomposedMul(x, HardSigmoid(x))pattern (which is what current MobileNetV3 exporters emit β reasonable scope). A future follow-up could extendTransformActivation's recognized-op set to include standaloneHardSwishas an opType, so post-opset-14 exporters that don't decompose also benefit. Note in follow-up. - Test strengthening.
ConvNchwcHardSwishFusioncould add:to positively assert the resulting attribute (rather than just op count).for (const auto& node : session.GetGraph().Nodes()) { if (node.OpType() == "Conv" && node.Domain() == kMSNchwcDomain) { const auto* activation_attr = graph_utils::GetNodeAttribute(node, "activation"); ASSERT_NE(activation_attr, nullptr); EXPECT_EQ(activation_attr->s(), "HardSwish"); } }
- 9 commits, mostly polish (style alignment, member declaration order, comment clarifications, blank-line spacing). Will squash cleanly β no history concern.
- No reviewers assigned. Needs MLAS ownership (
@yufenglee,@chenfucn) + graph-optimizer eyes (@edgchen1,@hariharans29). - Perf attribution. PR description shows a screenshot with numbers for MobileNetV3-L/S on Strix Point. Actual delta values aren't in the fetch output, but the PR text asserts "Top-1 accuracy unchanged after fusion" β good qualitative claim. Perf table review is left to the maintainer.
Merge state
- CI: 0 / 1 β needs
/azp runfrom a maintainer. - CLA: pending. Author must reply to the bot with
@microsoft-github-policy-service agree(with company="AMD" if applicable, given the "AMD Ryzen AI" perf attribution). - Approvals: none. Reviewers unassigned.
- Labels: none.
MLAS,perf, or similar would help retention.
Bottom line
Well-scoped, correctly-bounded diamond-pattern fusion with matching SIMD kernel. Every guard in TryFuseNchwcHardSwish earns its keep, the ONNX HardSwish math is exact, and the fallthrough behavior for non-HardSwish HardSigmoid is directly tested. External-contributor plumbing (CLA, /azp run) is the only real gate on merge β code is ready.
Action items:
- Author: reply to CLA bot.
- Maintainer:
/azp runto unlock full CI, then assign yourself + one MLAS + one optimizer reviewer. - Post-merge follow-up: extend
TransformActivationto also accept standaloneHardSwishopType (opset 14+ exporters), so this PR's benefit extends beyond decomposed patterns. Track as an issue.
Ready to merge once CLA, /azp run, and one MSFT-side approval are in.
There was a problem hiding this comment.
Pull request overview
Adds NCHWc ConvβHardSwish fusion for FP32 MobileNetV3 inference.
Changes:
- Adds MLAS HardSwish activation support.
- Detects and fuses decomposed HardSwish diamonds.
- Adds optimizer fusion tests.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
onnxruntime/core/mlas/inc/mlas.h |
Adds the HardSwish activation kind. |
onnxruntime/core/mlas/lib/activate.cpp |
Implements and dispatches HardSwish. |
onnxruntime/contrib_ops/cpu/fused_activation.cc |
Maps fused HardSwish attributes to MLAS. |
onnxruntime/core/optimizer/nchwc_transformer.cc |
Adds NCHWc HardSwish pattern fusion. |
onnxruntime/test/optimizer/nchwc_optimizer_test.cc |
Adds positive and fallback optimizer tests. |
π‘ Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
|
Verified effective GFLOPS increase 477->583 (mv3_large) and 257->347 (mv3_small) while the output is bitwise identical. |
@microsoft-github-policy-service agree company="Multicoreware" |
Detect the decomposed HardSwish diamond Mul(x, HardSigmoid(x)) after an NCHWc Conv (HardSigmoid gate alpha=1/6, beta=1/2), set the Conv activation attribute to HardSwish, and remove the HardSigmoid and Mul. Adds the MlasHardSwishActivation kernel and the "HardSwish" activation string mapping.
Covers the Mul(x, HardSigmoid(x)) -> Conv[activation=HardSwish] fusion and a negative case (plain HardSigmoid fuses as HardSigmoid, not misidentified as HardSwish). All 37 NchwcOptimizerTests pass.
One member declaration per line; add blank lines between member variable block, constructor, and method definitions to match the existing MlasHardSigmoidActivation formatting.
β¦s in HardSwish Add static constexpr float members (Alpha/Beta/Minimum/Maximum) for the fixed HardSwish constants. Use them in the scalar Activate(float) path instead of lane-extracting from SIMD vectors, eliminating the extraction overhead on non-SSE2 paths and making the constants self-documenting.
β¦sform* block Placing it between TransformActivation and TransformBatchNormalization broke the contiguous block of void Transform*() declarations. Move it after TrackTransposeFromNhwc, separated by a blank line with its comment.
β¦puts hardsigmoid_out does not need to be a mutable NodeArg*; use OutputDefs() and const NodeArg*. Similarly mul_inputs is read-only; use InputDefs() and const auto&.
β¦hrough Move orig_input_arg declaration inside the nchwc_input != nullptr block where it is used. Add a comment clarifying that TryFuseNchwcHardSwish returning false is intentional: execution falls through to the standard activation transform path (rewiring the input to the NCHWc arg), not a dead end.
β¦methods Align MLAS_ACTIVATION_FUNCTION<MlasHardSwishActivation>::Activate() with the spacing convention in MlasHardSigmoidActivation: blank line before the return statement in both the SIMD and scalar overloads.
β¦ough Expand the comment before the TryFuseNchwcHardSwish call to make explicit what happens when the function returns false: execution falls through to the standard activation-fusion path, where HardSigmoid can still be fused as a standalone activation into the preceding Conv.
Fix five issues flagged in the PR review: 1. test_activation.cpp: TestData was [20][7] but MlasActivationKindCount is now 8 (MlasHardSwishActivation added). The loop indexed TestData[i][kind] up to kind=7, which was out of bounds. Extend to [20][8] with correct HardSwish expected values: HardSwish(x) = x * clamp(x/6 + 0.5, 0, 1). 2. nchwc_transformer.cc (TryFuseNchwcHardSwish): NaN alpha/beta passed the std::abs tolerance checks silently (NaN comparisons always return false), so a HardSigmoid with NaN attributes would be fused as HardSwish and corrupt model results. Guard with std::isfinite before the tolerance test. 3. nchwc_transformer.cc (TryFuseNchwcHardSwish): The sibling Mul candidate was accepted without verifying it belongs to the same execution provider as the HardSigmoid. A Mul partitioned to a different EP would be silently removed from that EP subgraph. Add GetExecutionProviderType() check. 4. nchwc_optimizer_test.cc (ConvNchwcHardSigmoidNotHardSwish): The test only covered the no-Mul-diamond rejection path. Add a sub-case with a Mul(x, HardSigmoid(x)) diamond using mismatched alpha (0.2 vs 1/6) to exercise the alpha/beta guard from fix 2. 5. activate_fp16.cpp: GetFusedActivationAttr accepts activation=HardSwish (MlasHardSwishActivation), which FusedConvFp16 passes to MLAS_HALF_GEMM_ACTIVATION_PROCESSOR::Process. That switch had no case for MlasHardSwishActivation and would throw at runtime. Add the template specialization MLAS_HALF_ACTIVATION_FUNCTION<MlasHardSwishActivation> and the corresponding switch case, mirroring the FP32 activate.cpp.
fff0da2 to
939232a
Compare
[MLAS] NCHWc HardSwish fusion for MobileNetV3 FP32 CPU inference
Implementation of the hardswish fusion kernel for mobilenetv3 models.
Implementation changes done:
two standalone nodes per occurrence.
(Ξ±=1/6, Ξ²=1/2) and the Mul consumes the same conv output.
Performance:
Performance numbers on AMD Ryzen AI 9 365 (Strix Point)
NOTE: Performance taken on 31st July
STRIX 365 configration:
AMD Ryzen AI 9 365 (Strix Point) w/ Radeon 880M
Target Model: MobilenetV3-Large and MobilenetV3-Small (FP32, CPU)
Unit Tests
test_nchwc_hardswish.cpp