Skip to content

[js/web] Support float16 in Conv3DNaive WebGPU shader - #32357

Merged
Jiajia Qin (qjia7) merged 1 commit into
microsoft:mainfrom
Novestars:fix/webgpu-conv3d-f16
Sep 8, 2026
Merged

Jiajia Qin (qjia7) merged 1 commit into
microsoft:mainfrom
Novestars:fix/webgpu-conv3d-f16

Conversation

@Novestars

Copy link
Copy Markdown
Contributor

Description

Adds float16 support to the Conv3DNaive WebGPU shader in the JSEP WebGPU EP.

conv3d_naive_webgpu.ts already resolves the WGSL element type at line 324:

const t = tensorTypeToWsglStorageType(inputs[0].dataType);

but the generated shader hardcodes f32 in 12 places while the storage buffers are declared with inputs[0].dataType. For an fp16 model this produces a type mismatch between the accessor functions and the buffers.

This PR replaces those 12 hardcoded f32 with ${t}. No new imports, no changes outside this file, no C++ changes.

Motivation and Context

Fixes #22974.

Conv3D in float16 has never worked on the WebGPU EP. Yulong Wang (@fs-eire) noted in #22974 ("Need to implement f16 support for Conv3D") in Dec 2024.

The failure mode has since changed and is now worse. On older Chromium it raised a WGSL validation error:

error: return statement type must match its function return type, returned 'f16', expected 'f32'

On Chromium 148 it silently returns all zeros — no error, no warning, and roughly 9× faster than the fp32 path, so it looks like a successful, fast run.

Minimal repro (single Conv node, input [1,1,8,32,32] f16, weight [8,1,3,3,3], strides 1, pads 1):

model EP result
float16 webgpu (before) mean 0.00000, every sampled value 0
float16 webgpu (after) mean −0.00039, |max| 0.4080
float16 wasm mean −0.00039, |max| 0.4080
float32 webgpu mean −0.00039, |max| 0.4079

Verification (locally built js/web, npm run pull:wasm + npm run build)

Single Conv3D, input [1,1,48,168,168] f16, weight [8,1,3,3,3]:

time (median of 15) vs fp32
webgpu fp16 (patched) 27.88 ms maxAbs 7.31e-4, meanAbs 1.02e-4, rel 0.069%
webgpu fp32 41.67 ms
wasm fp16 66.39 ms agrees with patched webgpu fp16 to rel 0.062%

fp16 is 1.49× faster than fp32 and numerically correct.

A full 3D U-Net (48×168×168, 32 base channels, ~30 conv layers):

time (median of 12) vs fp32
webgpu fp16 (patched) 96.43 ms maxAbs 3.52e-1, meanAbs 2.56e-2, rel 0.872%
webgpu fp32 146.34 ms

1.52× faster. The 0.872% end-to-end relative difference is the accumulated effect of the f16 accumulator across ~30 layers — see the open question below.

A 2D model was run as a regression check and is unaffected (fp16 6.49 ms, fp32 13.5 ms, agreeing as before).

Not size-dependent — it fails identically at [1,1,48,168,168], so this is distinct from the f16 index-arithmetic issue in #28976.

Tested on Chromium 148 / Apple M5 Pro (metal-3), against onnxruntime-web 1.29.0 and 1.30.0-dev.20260826-b1f76d586a.

Tests

Added js/web/test/data/ops/conv3dncdhw_f16.jsonc (registered in the webgpu section of suite-test-list.jsonc, next to conv3dncdhw.jsonc), following the existing pad_f16.jsonc convention of keeping float16 coverage in its own file.

Six cases, with channel counts chosen to exercise every branch of the shader's inner loop:

case covers
C=1 scalar remainder
C=4 vec4
C=6 vec4 + vec2 remainder
C=7 vec4 + vec3 remainder
C=3, 3×3×3, SAME_UPPER padding
C=4, 3×3×3, SAME_UPPER, bias bias path

All input and weight values are multiples of 0.5, so every partial sum is exactly representable in float16 and the expected outputs do not depend on accumulation order. Verified against the locally built runtime: 12/12 pass on both webgpu and wasm with max diff exactly 0.0.

Every case fails on webgpu before the patch (all-zero output) and passes after.

Open question

This substitution also makes the accumulator f16 (var value = ${t}(0)), so a 3×3×3×C_in dot product now accumulates in half precision. Keeping the accumulator in f32 and casting only at the store would be numerically safer for large channel counts, but requires extra casts around getBiasByOutputCoords and the activation snippet. I kept this PR to the minimal type fix — happy to follow up with f32 accumulation, or to fold it in here if preferred.

The measured 1.5× speedup is consistent with this shader being load-bound: the vec4<f16> loads halve the load traffic relative to vec4<f32>.

The Conv3DNaive shader resolves the WGSL element type into `t` via
tensorTypeToWsglStorageType() but never uses it: the generated WGSL
hardcodes f32 in 12 places (accessor return types, vec2/3/4 literals,
the accumulator initialiser and the final store) while the storage
buffers are declared with the tensor's actual dtype. For an fp16 model
this mismatch produces all-zero output on current Chromium (older
versions raised a WGSL validation error instead).

Replace the hardcoded f32 with the already-computed `t`. No new imports,
no change to the fp32 path.

Adds conv3dncdhw_f16.jsonc with six cases covering the scalar, vec2,
vec3 and vec4 remainder branches plus padding and bias. All values are
multiples of 0.5 so partial sums are exact in float16 and results are
independent of accumulation order; 12/12 pass on webgpu and wasm with
zero difference.

Measured on Chromium 148 / Apple M5 Pro: single Conv3D fp16 27.9 ms vs
fp32 41.7 ms (1.49x); a 30-layer 3D U-Net 96.4 ms vs 146.3 ms (1.52x),
rel. error vs fp32 0.069% / 0.87%.

Fixes microsoft#22974
Copilot AI balanced review requested due to automatic review settings September 1, 2026 18:37
@azure-pipelines

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

@Novestars

Copy link
Copy Markdown
Contributor Author

@microsoft-github-policy-service agree

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

Adds float16 support to the JSEP WebGPU Conv3D shader.

Changes:

  • Uses storage element types in generated WGSL.
  • Adds six float16 Conv3D cases.
  • Registers the new WebGPU test suite.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

File Description
conv3d_naive_webgpu.ts Updates WGSL types for float16.
conv3dncdhw_f16.jsonc Adds float16 Conv3D coverage.
suite-test-list.jsonc Registers the tests.

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread js/web/lib/wasm/jsep/webgpu/ops/3rd-party/conv3d_naive_webgpu.ts
@Novestars

Copy link
Copy Markdown
Contributor Author

@microsoft-github-policy-service agree

@qjia7

Copy link
Copy Markdown
Contributor

Xinzi He (@Novestars) Thanks for the contribution. Since the jsep will be deprecated soon and replaced by wasm + native webgpu, could you check whether the issue still exists in wasm+webgpu instead of wasm + jsep? If yes, the fixing in there will be appreciated. Thanks.

@Novestars

Copy link
Copy Markdown
Contributor Author

Thanks Jiajia Qin (@qjia7). Checked on 1.30.0-dev.20260904 (Chrome 152, Apple M5 Pro): the native WebGPU EP (onnxruntime-web/webgpu import) is already correct for fp16 Conv3D — the same models give max diff 7.3e-4 vs the wasm fp16 reference, and fp16 is 1.55× faster than fp32 (11.9 vs 18.5 ms at [1,1,48,168,168]). conv3d_naive.cc uses the x_value_t/x_element_t aliases throughout, which is exactly what this PR does for JSEP. So the bug is JSEP-only; the default onnxruntime-web / /all imports still return all zeros for fp16 Conv3D. Per docs/JSEP_Deprecation.md correctness fixes in existing JSEP kernels are still accepted, so I'm happy to keep this as a minimal JSEP-only fix for users still on the default import, or close it if you'd rather steer people to onnxruntime-web/webgpu — your call.

@qjia7
Jiajia Qin (qjia7) merged commit 6690cf0 into microsoft:main Sep 8, 2026
88 of 90 checks passed
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.

[Web] Can't use Conv3DNaive on webgpu

3 participants