[WebGPU EP] Support Cast to and from uint8 - #29839
Conversation
|
Azure Pipelines: There may be pipelines that require an authorized user to comment /azp run to run. |
|
@adrastogi Please help trigger the copilot review and testing workflows, thanks! |
There was a problem hiding this comment.
Pull request overview
This PR extends the WebGPU Execution Provider’s Cast kernel to support uint8 as both an input (T1) and output (T2) type, eliminating unnecessary CPU EP fallback for uint8 cast nodes (notably in WebNN-exported graphs). It also implements the correct packed Uint8x4 load/store behavior in shader variable helpers and adds targeted WebGPU EP tests.
Changes:
- Add
uint8to WebGPUCastkernel type constraints for both inputs and outputs. - Implement
Uint8x4unpacking (GetByOffset) and packing (SetByOffset) paths souint8tensors use the intended packed storage layout. - Add WebGPU EP unit tests covering
bool -> uint8anduint8 -> {int32,float,bool}casts, including non-multiple-of-4 sizes.
Show a summary per file
| File | Description |
|---|---|
| onnxruntime/test/providers/cpu/tensor/cast_op_test.cc | Adds explicit WebGPU EP tests validating uint8 cast behavior and packed tail handling. |
| onnxruntime/core/providers/webgpu/tensor/cast.cc | Enables uint8 in Cast type constraints and ensures uint8 participates in unsigned-to-int64 zero-extension logic. |
| onnxruntime/core/providers/webgpu/shader_variable.cc | Adds Uint8x4 unpack/pack implementations matching the packed byte layout used by the WebGPU EP. |
Review details
- Files reviewed: 3/3 changed files
- Comments generated: 0
- Review effort level: Low
|
Thanks for adding uint8 Cast support to the WebGPU EP! This is also important for WebNN ORT implementation when using WebGPU EP as its backend. The WebNN logical operators use |
Review: PR #29839 — [WebGPU EP] Support Cast to and from uint8 (head
|
| Test | Direction | Notes |
|---|---|---|
BoolToUint8_WebGpu |
bool → uint8 | Exercises the Uint8x4 packing path from a bool source. |
Uint8ToInt32_WebGpu |
uint8 → int32 | Full byte range {0, 1, 128, 255, 42, 7}. Exercises Uint8x4 unpacking + widening. |
Uint8ToFloat_WebGpu |
uint8 → float | Same input values, exercises the float-conversion arm with unpacked uint8. |
Uint8ToBool_WebGpu |
uint8 → bool | Inverse of test 1. Exercises Uint8x4 unpack and Boolx4 output packing, plus the "any nonzero → true" cast-to-bool semantic. |
All shapes are {2, 3} (6 elements), deliberately not a multiple of 4 — the comment on each test documents that this exercises the final partial packed word. This is the crucial edge case for a pack-4-into-1 storage layout.
Not covered (minor gaps, not blocking):
uint8 → int64— the code path is enabled by theis_from_unsignedaddition, but no test pins the zero-extension semantic. If a regression flipped it to sign-extension, existing uint32→int64 coverage would catch the general pattern but not the specific uint8 slice.uint8 → uint8— unusual (identity cast) but legal.- Multiple-of-4 shape — the current 6-element shape does one full vec4 + a 2-element tail, so the pure-vectorized loop is exercised transitively, but a dedicated 4- or 8-element case would be tighter.
Coverage-as-is is adequate for the load-bearing behaviors (pack, unpack, tail).
Copilot review disposition
0 comments generated. Nothing to reconcile.
CI status
77 / 86 checks OK on the single commit. The 9 not-OK are likely still pending; the code doesn't look like a plausible cause of new-regression failures at first pass. Worth confirming they land green before merge.
Minor observations (none blocking)
- The pack's
& 0xFFumask is a per-lane bitwise-AND on a vec4 before the dot product — one extra SIMD op per Uint8x4 write. Trivial cost, well worth the safety. - The comment "lane 0 → low byte" is called out on both the pack and unpack sites, which makes the byte-endianness of the storage explicit and testable. Good discipline.
- The test comments deliberately explain why the 6-element shape was chosen (partial packed word) — this makes future maintainers less likely to "simplify" the tests to convenient 4- or 8-element shapes and accidentally erase the tail-coverage.
- Fifth-order nit: the
GetByOffsetImplfor Boolx4 uses inconsistent-width masks (0xFFu,0xFF00u,0xFF0000u,0xFF000000u— implicit u32) whereas the new Uint8x4 uses>> 8u & 0xFFuetc. Different styles for adjacent cases in the same switch, but each is internally consistent and the Uint8x4 form is genuinely different math. Not worth reconciling.
Bottom line
Clean, small, well-motivated PR that closes a real fallback path with concrete downstream impact (Chromium's WebNN ORT backend, confirmed by @huningxin). Pack/unpack correctness is verifiable by direct comparison to the Boolx4 baseline plus the necessary byte-mask on the pack. Test coverage exercises the load-bearing byte-packing edge cases including the partial-word tail. Approve after CI settles.
Add uint8 support to the WebGPU Cast in both directions, so uint8 Cast nodes stop falling back to the CPU EP (which aborts session init under the compile-only flow). uint8 was in neither the input (T1) nor output (T2) type constraint, so both casts *to* uint8 (e.g. a terminal bool->uint8 at a graph output) and casts *from* uint8 (back to int32/float/bool) fell back. WebGPU stores uint8 packed as Uint8x4 (4 per u32, lane 0 -> low byte), the same layout already used for bool. * to uint8 (cast.cc, shader_variable.cc): add uint8 to the output (T2) type constraint, a to-uint8 shader expression, and a Uint8x4 packing path in SetByOffset. * from uint8 (cast.cc, shader_variable.cc): add uint8 to the input (T1) type constraint, a Uint8x4 unpacking path in GetByOffset (reads the 4 bytes into a vec4<u32>, inverse of the packing), and include uint8 in is_from_unsigned so uint8->int64 zero-extends. Tests (explicit WebGPU EP), cast_op_test.cc: BoolToUint8_WebGpu (to uint8; non-multiple-of-4 shape for the partial packed word); Uint8ToInt32_WebGpu (from uint8; values 0/1/128/255 spanning the full byte range to prove each byte unpacks); Uint8ToFloat_WebGpu (from uint8, float path); Uint8ToBool_WebGpu (from uint8; the distinct Boolx4 output-packing path).
b6024f6 to
ee81e5c
Compare
|
@hariharans29 |
Re-review: PR #29839 — [WebGPU EP] Support Cast to and from uint8 (head
|
Gather on uint8 fell back to the CPU EP because uint8 was not in the T type constraint. The WebNN EP converts ONNX bool to WebNN uint8, so with WebGPU as the WebNN backend, Gather on a bool tensor becomes Gather on uint8 - which models such as detr-resnet-50 need end to end. uint8 is stored packed as Uint8x4 (four elements per u32, lane 0 -> low byte), the same layout bool already uses, so the kernel handles four output elements per thread. Each element's index math runs in its own guarded block, since the last word of a non-multiple-of-4 output is only partially populated. The accumulator holds one element per lane and is passed to SetByOffset unpacked: since microsoft#29839 SetByOffset has a Uint8x4 case that performs the byte packing itself. Pre-packing into a u32 and passing that scalar is wrong - SetByOffset would splat it across all four lanes, mask each to its low byte and re-pack, so every output word would come back as element 0's byte repeated four times: expected: [20,10,30,20 | 50,40,60,50 | 80,70,90,80] got: [20,20,20,20 | 50,50,50,50 | 80,80,80,80] uint8 needs an explicit vec4<u32> accumulator because output_value_t for Uint8x4 is the packed u32 storage type, unlike Boolx4's vec4<bool>. Note the kernel is therefore only correct in a tree containing microsoft#29839, so the branch must be built merged with main rather than standalone. Tests: Gather_axis1_indices2d_uint8 exercises the CPU EP path and, in a WebGPU build, the WebGPU EP as well. Gather_axis1_indices2d_uint8_webgpu pins the packed-byte shader explicitly, and Gather_axis0_uint8_non_multiple_of_4_webgpu covers an output size that is not a multiple of 4, where the final word holds one valid byte. Verified with main merged in on Intel Arc A770 and NVIDIA RTX A2000 over D3D12 and on Vulkan: all three tests fail before this change and pass after it on every configuration, and the full GatherOpTest suite (22 tests, covering the bool, float, int8, string and scalar-indices paths) passes.
Add uint8 support to the WebGPU Cast in both directions, so uint8 Cast nodes stop falling back to the CPU EP. uint8 was in neither the input (T1) nor output (T2) type constraint, so both casts to uint8 (e.g. a terminal bool->uint8 at a graph output) and casts from uint8 (back to int32/float/bool) fell back.
WebGPU stores uint8 packed as Uint8x4 (4 per u32, lane 0 -> low byte), the same layout already used for bool.
to uint8 (cast.cc, shader_variable.cc): add uint8 to the output (T2) type constraint, a to-uint8 shader expression, and a Uint8x4 packing path in SetByOffset.
from uint8 (cast.cc, shader_variable.cc): add uint8 to the input (T1) type constraint, a Uint8x4 unpacking path in GetByOffset (reads the 4 bytes into a vec4, inverse of the packing), and include uint8 in is_from_unsigned so uint8->int64 zero-extends.
This is a split CL from #29682 to fix part of #29756