Skip to content

webgpu: the kernels take the canonical ABI, so marshal_ is gone - #5

Closed
yhirose wants to merge 1 commit into
backend-layer-3from
backend-layer-4
Closed

yhirose wants to merge 1 commit into
backend-layer-3from
backend-layer-4

Conversation

@yhirose

@yhirose yhirose commented Sep 23, 2026

Copy link
Copy Markdown
Owner

Stacked on #4 (base backend-layer-3); this PR's diff is the one commit on top. Once #4 lands, retarget this to master.

Summary

The last item on the backend-layer list. WebGPU's WGSL kernels predated the shared kernel ABI: every entry point read one 96-byte Params uniform, a family picked its operation by a number in it, and the bind group was fixed (A and B read, C written, D and E read). webgpu.h carried marshal_ to translate each kernel id's canonical params into that layout.

Now WebGPU realizes a launch the way Metal and CUDA do:

  • View i is bound whole at binding i, in the order gpu_ops.h lists the kernel's views. The binding after them is a uniform holding the views' element offsets, then the params, field for field as in gpu_abi.h. The offsets travel in the uniform because a binding offset must be 256-byte aligned, which a view's offset is not.
  • A family's operation (add, sub, ...) is the pipeline-overridable constant OP, set per kernel id from a kernel table (kernel_, the counterpart of Metal's and CUDA's kernel_name_), not a params field.
  • Each pipeline takes an auto layout from what its entry point declares, so the WGSL declarations are the kernel's side of the ABI.
  • One launch_ serves both the shared ops (through dispatch) and the backend's own ops. The N-D ops' shape metadata is an ordinary input view into the meta ring.

Removed: Params, kernel_op_, marshal_, encode_, operands_, and the hand-built layouts. webgpu.h is about 290 lines shorter. docs/backends.md no longer lists anything as not shared.

Also fixes a latent hazard. launch_ could flush between reserving a meta slot and the launch that reads it. After the flush, a later call in the new batch could be handed the same slot and overwrite it before the launch read it. Reserving a slot now flushes first when the uniform ring is also full.

Test plan

  • WebGPU under Deno, --gpu and --auto: 117/117 cases, 8789 assertions.

  • Per-family dispatch counts are identical to the previous build in both modes, compared against a build of backend-layer-3. No op started falling back to the CPU.

  • Mutations, each caught by the suite:

    • a wrong OP in the kernel table
    • a kernel reading one view at another view's offset (caught by the non-zero-offset conformance test)
    • the meta view losing its offset
    • two meta runs swapped
  • Native Metal: 117/117 cases, 11640 assertions with the device; check_qwen greedy tokens match. gpu_host reference build: 117/117. CUDA host-side trace: identical. None of these compile webgpu.h.

  • Not yet run in a browser. Everything here ran on Deno (wgpu/naga), and so does CI. Two constructs have not been exercised on Chrome's Dawn/Tint:

    • the same binding number declared with different access modes in different entry points
    • override constants

    WGSL allows both, but a run in Chrome is still worth doing.

The WGSL kernels predated the shared kernel ABI: every entry point read one
96-byte Params uniform, a family picked its operation by a number in it, and
the bind group was fixed (A and B read, C written, D and E read). webgpu.h
carried marshal_ to translate each kernel id's canonical params into that
layout, and each own op filled the same struct by hand.

Now a kernel binds view i, whole, at binding i, in the order gpu_ops.h lists
its views, and the binding after them is a uniform: the views' element
offsets, then the params, field for field as gpu_abi.h has them. The offsets
travel in the uniform because a binding offset must be 256-byte aligned,
which a view's is not. A family's operation is the pipeline-overridable
constant OP, set per kernel id from a kernel table (kernel_, the counterpart
of Metal's and CUDA's kernel_name_), and each pipeline takes its layout from
what its entry point declares. One launch_ serves the shared ops through
dispatch and the backend's own ops alike; the N-D ops' shape metadata is an
ordinary input view into the meta ring. Params, kernel_op_, marshal_,
encode_, operands_ and the hand-built layouts are gone, and
docs/backends.md no longer has anything listed as not shared.

A meta slot and its launch now always share a batch: reserving a slot
flushes when the uniform ring is full too, where before launch_ could flush
between them and let a later call rewrite the slot before the launch read it.

Verified under WebGPU via Deno: 117 cases in gpu and auto mode, with
per-family dispatch counts identical to the previous build in both modes, so
no op started declining. Four mutations were each caught (a wrong OP in the
kernel table, a kernel reading a view at another view's offset, the meta
view losing its offset, two meta runs swapped). Native Metal (117 cases,
11640 assertions with the device, check_qwen's greedy tokens), gpu_host's
reference build and the CUDA host-side trace, none of which compile this
backend, are unchanged.
@yhirose
yhirose deleted the branch backend-layer-3 September 23, 2026 02:01
@yhirose yhirose closed this Sep 23, 2026
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.

1 participant