Skip to content

WebGPU: Add HardSwish support - #29828

Merged
hariharans29 merged 2 commits into
microsoft:mainfrom
huningxin:copilot/add-hardswish-kernel-webgpu-ep-upstream-main
Jul 22, 2026
Merged

WebGPU: Add HardSwish support#29828
hariharans29 merged 2 commits into
microsoft:mainfrom
huningxin:copilot/add-hardswish-kernel-webgpu-ep-upstream-main

Conversation

@huningxin

Copy link
Copy Markdown
Contributor

Description

HardSwish currently falls off the WebGPU partition because the EP has no native kernel and no decomposition pass rewrites it into WebGPU-supported ops. This change adds direct WebGPU support so HardSwish stays on device for f32/f16 models.

Motivation and Context

  • HardSwish is common in mobile and vision models, such as MobileNet V3 and MediaPipe's Selfie Segmenter; without native WebGPU support it falls back to CPU, introducing avoidable GPU↔CPU transfers.
  • This change closes that gap by keeping HardSwish execution on WebGPU with the same fixed semantics as the ONNX operator.

Fix #29756

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
There may be pipelines that require an authorized user to comment /azp run to run.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR adds native HardSwish operator support to the WebGPU execution path so models using HardSwish can remain fully on the WebGPU EP (for f32/f16), avoiding CPU fallback and GPU↔CPU transfers.

Changes:

  • Add a WebGPU WGSL implementation and kernel registration for HardSwish (opset 14+).
  • Add corresponding ORT Web (JS/WebGPU) operator wiring for HardSwish and update the supported-operators doc.
  • Add a WebGPU provider unit test covering HardSwish for float32 and float16.
Show a summary per file
File Description
onnxruntime/test/providers/webgpu/hardswish_test.cc Adds WebGPU EP unit tests for HardSwish (f32/f16).
onnxruntime/core/providers/webgpu/webgpu_execution_provider.cc Registers HardSwish kernel in the WebGPU EP kernel table.
onnxruntime/core/providers/webgpu/math/unary_elementwise_ops.h Adds WGSL helper implementation string for HardSwish.
onnxruntime/core/providers/webgpu/math/unary_elementwise_ops.cc Implements and registers the HardSwish unary elementwise kernel.
onnxruntime/core/providers/js/operators/unary.cc Registers HardSwish for the JS EP unary operator set.
onnxruntime/core/providers/js/js_execution_provider.cc Adds HardSwish kernel to the JS EP kernel registry table.
js/web/lib/wasm/jsep/webgpu/ops/unary-op.ts Adds the WebGPU shader expression implementation for HardSwish.
js/web/lib/wasm/jsep/webgpu/op-resolve-rules.ts Routes ONNX HardSwish to the new WebGPU unary implementation.
js/web/docs/webgpu-operators.md Documents HardSwish as supported by WebGPU EP in ORT Web.

Review details

  • Files reviewed: 9/9 changed files
  • Comments generated: 1
  • Review effort level: Low

Comment thread onnxruntime/test/providers/webgpu/hardswish_test.cc Outdated
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
@huningxin

Copy link
Copy Markdown
Contributor Author

@adrastogi, I've fixed the copilot review comments. Could you please help trigger the build/test pipelines?

@huningxin huningxin changed the title Add HardSwish support to WebGPU WebGPU: Add HardSwish support Jul 22, 2026
@huningxin

Copy link
Copy Markdown
Contributor Author

@hariharans29, PTAL, thanks!

@hariharans29

Copy link
Copy Markdown
Member

Review: PR #29828 — WebGPU: Add HardSwish support (head e40a746)

Author: @huningxin. 2 commits (initial Copilot-SWE-agent commit 3cd37f1 + author fixup e40a746 for the Copilot review nit). CI: 84 / 86 checks OK. Fixes #29756. Awaiting human approval (@hariharans29 pinged).

Verdict: LGTM — approve. Small, well-scoped op addition. Both native WebGPU EP and JSEP paths wired symmetrically. Test covers the interesting boundary points. Copilot review already resolved. No blockers.


What it does

Adds a first-class HardSwish (opset 14+) kernel to WebGPU so it stays on device instead of falling back to CPU on models like MobileNetV3 and MediaPipe SelfieSegmenter. Nothing in the PR description or in the diff suggests an existing decomposition or MHA-style fallback pass — the operator was simply unassigned.

9 files touched:


Correctness — WGSL math

ONNX 14 HardSwish: y = x * max(0, min(1, α·x + β)) with fixed α = 1/6, β = 0.5 (no attributes).

Native WebGPU EP helper in unary_elementwise_ops.h:

fn hard_swish_v(v: vec4<x_element_t>) -> vec4<x_element_t> {
  let alpha  = x_element_t(1.0 / 6.0);
  let beta_v = vec4<x_element_t>(x_element_t(0.5));
  return v * max(vec4<x_element_t>(0.0),
                 min(vec4<x_element_t>(1.0), alpha * v + beta_v));
}
  • x_element_t is aliased via ShaderUsage::UseElementTypeAliasf32 or f16 depending on the input tensor.
  • The scalar alpha broadcasts over the vec4 in alpha * v; WGSL semantics match the equivalent all-vec4 form.
  • The 1.0/6.0 is computed at shader-compile time in the WGSL type system: for f16, f16(1.0/6.0) ≈ 0.16669921875 (nearest representable). That's an intended fp16 precision loss the ONNX spec accepts.

JSEP variant in unary-op.ts:

`${a} * max(vec4<${dataType}>(0.0),
             min(vec4<${dataType}>(1.0),
                 vec4<${dataType}>(${dataType}(1.0 / 6.0)) * ${a}
                 + vec4<${dataType}>(0.5)))`

Same math, written inline with vec4 alpha broadcast (rather than scalar-broadcast). Numerically equivalent to the native path modulo compiler reordering — WGSL doesn't reassociate +/* across min/max, so both paths are bit-identical for the same input tensor type. Good.

Spot-check on the operator's interesting points (all satisfied by the test's chosen input set):

x x/6 + 0.5 clamp(0,1) y = x·clamp
-8 -0.833… 0 0 (saturated below)
-6 -0.5 0 0
-3 0 0 0 (left saturation edge)
-1 0.333… 0.333… -0.333…
0 0.5 0.5 0
0.5 0.583… 0.583… 0.292…
1 0.667… 0.667… 0.667…
3 1.0 1.0 3 (right saturation edge)
6 1.5 1 6
8 1.833… 1 8

Both saturation edges (x = ±3) and both far-saturation regions (x = -8 and x = 8) are exercised, and 0.5 sits inside the strictly-linear region as a spot check. Good boundary set.


Kernel registration completeness

  • Native WebGPU EP: WEBGPU_ELEMENTWISE_KERNEL(HardSwish, 14, WebGpuSupportedFloatTypes()) + KERNEL_CREATE_INFO(14, HardSwish) in the provider's build_kernel_create_info_function_table. Standard pattern, mirrors HardSigmoid one line above.
  • JS EP: matching JSEP_ELEMENTWISE_KERNEL(HardSwish, 14, HardSwish) + create-info entry in the JS provider's table.
  • Opset: HardSwish was introduced at opset 14, so no _VERSIONED_ lower-bound kernel is needed. Correct.

Test

hardswish_test.cc:

  • Skips via GTEST_SKIP if DefaultWebGpuExecutionProvider() returns null — standard hygiene.
  • Fixed input {-6, -3, -1, 0, 1, 3, 6, 8, -8, 0.5} with kDims{2, 5} (as std::vector<int64_t> after the Copilot fixup — better than raw C array, easier to pass to AddInput/AddOutput).
  • Expected values computed via std::transform with the exact ONNX formula (x * std::max(0.0f, std::min(1.0f, x / 6.0f + 0.5f))). reserve()'d before the transform. Clean.
  • Float32 branch: exact comparison.
  • Float16 branch: SetOutputAbsErr(0.01) + SetOutputRelErr(0.01). Sensible for fp16 given output range up to 8 (fp16 ULP near 8 is ~0.008 — the tolerance is one-ULP-ish, generous enough for the shader-side f16(1/6) rounding plus the min/max boundary noise).
  • Uses ConfigEp(std::move(webgpu_ep)).RunWithConfig() which is the current ORT test-utility convention.

Two tests (Float32, Float16) via RunHardSwishTest<T>(). Enough to cover both dtype specializations.


Copilot review disposition

Copilot AI reviewed 9/9 files, 1 comment on hardswish_test.cc, resolved by e40a746 — the fixup changed the dims declaration to std::vector<int64_t> (matches the test-file idiom elsewhere in the tree). Small nit, addressed correctly.


CI

84 / 86 checks OK. Two unaccounted for — could be pending Azure Pipelines legs that require /azp run or workflows that no code in this PR would break. Worth eyeballing but nothing in the change is a plausible cause of a real regression on the missing legs.


Minor observations (not blockers)

  1. The JSEP path writes vec4<${dataType}>(${dataType}(1.0 / 6.0)) * ${a} — first f16(1.0/6.0) scalar cast (compile-time), then vec4 broadcast, then vec4 multiply. Slightly more verbose than the native EP's scalar alpha * v but semantically identical in WGSL. Both compile down to the same broadcast-multiply. Fine as-is; if you want maximum symmetry with the native helper, factor out the scalar and rely on WGSL broadcast, but not worth churning for.
  2. webgpu-operators.md correctly shows opset 14+ (matches the schema range). Good.
  3. No com.ms.internal.nhwc variant — correct, HardSwish is a per-element unary and layout-agnostic, so the NHWC transformer doesn't need a separate kernel entry.
  4. The test skips gracefully when the WebGPU EP isn't in the build — standard pattern, but worth noting that in a WebGPU-off build the tests always skip, so the coverage guard depends on someone actually running with --use_webgpu.

Bottom line

Approve. The math is exactly the ONNX HardSwish spec, both the native WebGPU EP and JSEP paths implement it consistently, kernel registration is complete for opset 14+, and the test exercises both saturation edges plus the linear region for f32 and f16. The one Copilot nit was addressed. Merge when a maintainer signs off and the two missing CI checks come back green.

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.

Microsoft WebNN demo applications cannot successfully run in ORT-WebGPU backend with CPU fallback disabled

4 participants