Skip to content

Add experimental wgpu backend for Candle - #3379

Open
KimHenrikOtte wants to merge 222 commits into
huggingface:mainfrom
KimHenrikOtte:wgpu_cleanup
Open

KimHenrikOtte wants to merge 222 commits into
huggingface:mainfrom
KimHenrikOtte:wgpu_cleanup

Conversation

@KimHenrikOtte

@KimHenrikOtte KimHenrikOtte commented Feb 19, 2026 •

Copy link
Copy Markdown

Summary

This PR adds an experimental wgpu backend for Candle, enabling portable GPU compute across native and WASM targets and fixes (#344).

Advantages of wgpu compared to CUDA:

  • Easier to install — no extra SDK/toolkit required
  • Works on older GPUs that don’t meet modern CUDA capability requirements

Performance Metrics

Model / Task wgpu CPU
llama2c - 15m 378.28 token/s 327.90 token/s
llama2c - 42m 251.88 token/s 68.86 token/s
t5 - 42m 217.23 token/s 98.34 token/s
clip 1.316 s 1.0502352 s
metavoice 36.36 s too long to measure
stable-diffusion 26.39 s Error: cannot seed the CPU rng with set_seed

Included Changes (High Level)

  • Added wgpu backend implementation
  • Added wgpu-compute-layer (device setup, shader loading, buffer management, lightweight caches)
  • Added candle-wgpu-kernels (source for all Candle wgpu kernels)
  • Updated candle-wasm-test (build.rs copies tests and converts sync → async for WASM backend validation)
  • Updated candle-wasm-examples (allows selecting wgpu backend)
  • Added candle-wasm-examples/xtask (helper to simplify running WASM examples)

Notes

If preferred, I can split parts of this into separate PRs (e.g. backend core, kernels, WASM examples, wasm tests).

Kim Henrik and others added 30 commits May 6, 2024 12:56
Tanh and Matmul are supported.
To copy data from GpuStorage to Cpu a new function to_device_async was added (async is needed in wasm)

- There is no support for:
Storage Layout, Conv2d, Compare, Other Types than F32.
- added a few basic operations(relu, sin, cos, add, sub, etc.)
-Added Debug measurement Infos
- wgpu is now an optional feature
- Improved Shader Loading (only the file needed fill be loadad ,
- Performance Improvement due to chaining multiple commands in a single computePass
-erf and gelu_erf,
-fixed index_select implementation error
-Fix Build Script Error
- Fix Device MinBuffer Size
- changed function to async for llama2-wasm example
added wasm debug messages
-marked function, that may be called by an async version as deprecated
-fixed a conv2d problem
-added conc1d
- changed examples to work with wgpu(not tested so far)
changed conv2d kernel size,
added matmul1_end
- Simplified quantized shader loading
- Fixed `MutexGuard` across async methods; on WASM, the locks will be dropped before the await point
- Added multi-thread test
# Conflicts:
#	candle-core/Cargo.toml
- It previously appeared to enable the `wgpu` feature across the entire workspace.
- format document
@vandana-rajan

Copy link
Copy Markdown

Thank you @KimHenrikOtte for this. I have tried your wgpu backend and it works amazing.
One issue I found was that for Q80 models, it throws error saying

Error while parsing WGSL: :132:31 error: mixing '&' and '+' requires parenthesis v_dest[(out_base + 2) & 31u + block_index * 32] = f32(x3_unsigned)*d; ^^^^^^^ :133:31 error: mixing '&' and '+' requires parenthesis v_dest[(out_base + 3) & 31u + block_index * 32] = f32(x4_unsigned)*d;

You can reproduce this for phi example with phi_1_5_Q80 and puffin Q80 models.

Fix was a one line change I have put here - vandana-rajan@caf8bba

I can push this as a PR once your branch gets merged.

@KimHenrikOtte

Copy link
Copy Markdown
Author

Thank you @KimHenrikOtte for this. I have tried your wgpu backend and it works amazing. One issue I found was that for Q80 models, it throws error saying

Error while parsing WGSL: :132:31 error: mixing '&' and '+' requires parenthesis v_dest[(out_base + 2) & 31u + block_index * 32] = f32(x3_unsigned)*d; ^^^^^^^ :133:31 error: mixing '&' and '+' requires parenthesis v_dest[(out_base + 3) & 31u + block_index * 32] = f32(x4_unsigned)*d;

You can reproduce this for phi example with phi_1_5_Q80 and puffin Q80 models.

Fix was a one line change I have put here - vandana-rajan@caf8bba

I can push this as a PR once your branch gets merged.

I tried to reproduce the error using:

cargo run --example phi --features="wgpu" --release -- --model 1.5 --quantized --prompt "A skier slides down a frictionless slope of height 40m and length 80m. What's the skier speed at the bottom?"

This seems to work on my side. I assume you used the candle-wasm-examples/phi example?

In general, the implementation in the browser may be stricter than the native drivers used by wgpu.

Interestingly, I am currently having some issues running:

wasm-pack test --chrome --features="wgpu" --test all

or

cargo build --target wasm32-unknown-unknown --release --features="wgpu"

because candle-core now depends on tokenizers (added in #3245), which appears to pull in onig_sys. That crate requires clang to be installed.

@KimHenrikOtte

Copy link
Copy Markdown
Author

Thank you @vandana-rajan for pointing out the error. Generally, Chrome’s implementation is stricter about the format of the WASM file. I tested this branch in candle-wasm-tests using:

wasm-pack test --chrome --features="wgpu" --test all

In this branch, the build.rs of the project automatically copies the tests from candle-core, candle-nn, and candle-transformers. Unfortunately, I opted out of the quantized_tests in build.rs, so I never tested the quantized tests in the browser.

I fixed the error you mentioned and also found two similar locations where a parenthesis was missing.

@vandana-rajan

Copy link
Copy Markdown

Thats great. Thank you @KimHenrikOtte for the awesome work and prompt response.

Remove cpu_flash_test and slow tests for WASM.
@vandana-rajan vandana-rajan mentioned this pull request Apr 17, 2026
nightness added a commit to Brainwires/candle that referenced this pull request May 5, 2026
PR huggingface#3379 was based on 0.9.2 but the brainwires-framework already
declares candle ^0.10 (matching v0.10-wgpu). Bump every workspace
member's version + every internal version pin to 0.10.2 so the
framework consumer resolves cleanly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
nightness added a commit to Brainwires/candle that referenced this pull request May 6, 2026
…aming

PR huggingface#3379's regular `alloc_from_bytes` requires the entire tensor in
linear memory before upload, which is fine for native but on wasm32
caps tensor size at the linear-memory limit (~4 GB total budget).
The chat-pwa loader needed the old v0.10-wgpu pattern — pre-allocate,
chunk-write, wrap — to stream a 805 MB embedding tensor in 64 MB
slices with a tiny bounded peak.

Add two paired public methods on `wgpu_compute_layer::WgpuDevice`
(plus matching wrappers on `candle_core::WgpuDevice`):

- `alloc_uninit_storage_eager(dtype, byte_size)` — allocates a
  `BufferReferenceId` AND eagerly forces `cache.search_buffer` to
  materialize the underlying `wgpu::Buffer` now (regular
  `alloc_uninit_size` defers buffer creation to first dispatch,
  which doesn't work for streaming uploads).
- `write_to_storage_at(storage, byte_offset, data)` — calls
  `queue.write_buffer` against the eager buffer. `byte_offset` and
  `data.len()` must be 4-byte aligned per wgpu spec.

Internal plumbing in `cache::ModelCache`:
`alloc_eager_storage` mirrors `create_buffer_reference_init` minus
the data-upload step; `write_to_eager_storage` looks up the cached
buffer through the existing `BufferReferenceId → CachedBufferId →
wgpu::Buffer` resolution chain. `max_memory_size` accessor is
re-exposed so the device-level allocation guard can validate.

The chat-pwa `load_tensor_to_gpu` will rewire to this API in a
follow-up commit, restoring 64 MiB peak from the current
~805 MB peak.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
nightness added a commit to Brainwires/candle that referenced this pull request May 6, 2026
…part 4)

Adds `quantized_gemma4::ModelWeights` — the QMatMul/GGUF counterpart
to `gemma4::text::TextModel`. Mirrors `quantized_gemma3.rs`
structurally and reads weights directly from
`gguf_file::Content::tensor` so the entire matmul path uses the
quantized kernels (PR huggingface#3379's `q4_k.pwgsl`/`q5_k.pwgsl`/etc on WGPU,
candle's CPU dequant-on-fly elsewhere).

What's ported in this initial slice:
- RmsNorm input + post-attention + pre-feedforward + post-feedforward
- GQA self-attention with q_norm / k_norm / v_norm
- p-RoPE (partial 25%) on full layers, standard RoPE on sliding
- SwiGLU MLP (gate / up / down)
- KvCache (Normal for full layers, Rotating for sliding)
- KvCache reset across the whole stack via `clear_kv_cache`

What's gated off (the file-doc lists each + corresponding
`Gemma4TextConfig.disable_*` flag):
- Per-Layer Embeddings (PLE)
- AltUp (Alternating Updates)
- LAuReL (Learned Augmented Residual Layer)
- KV-share donor / receiver attention
- layer_scalar
- activation sparsity (Gaussian-topk)

Output won't match Gemma 4 reference exactly with the auxiliary
towers off — this scaffold is the keystone for Phase 5's perf gain
to actually reach inference. Adding the auxiliary towers is
straightforward Linear → QMatMul translation; the holdout is
verifying each tower against a reference output, which needs an
Ollama-published gemma4:e2b GGUF + matching reference run.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
nightness added a commit to Brainwires/candle that referenced this pull request May 6, 2026
The branch has been at v0.11-wgpu for a while (PR huggingface#3379 WebGPU backend,
flash-attn decode design doc, full QStorage::Wgpu, q4_k/q5_k/q6_k/q8_k
WGSL kernels) but the package version was still claiming 0.10.2.
Bumping the workspace + the six per-crate Cargo.tomls that don't
inherit `version.workspace = true` to advertise the actual version
consumers see (downstream pins by git rev anyway, but the version
number should reflect reality).

Per-crate cascade through workspace deps (candle-core / candle-nn /
candle-transformers / candle-datasets / candle-kernels /
candle-metal-kernels / candle-wgpu-kernels / wgpu-compute-layer /
candle-onnx / candle-ug / candle-flash-attn / candle-flash-attn-v3):
all references to `version = "0.10.2"` updated.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
KimHenrikOtte and others added 10 commits July 30, 2026 11:49
 implement load_safetensor function for handling BF16 tensors in WGPU
fixed a native crash in wgpu tests caused by wasm-only logging being used on non-wasm targets
made test logging target-aware so wasm uses browser console and native uses normal stdout
fixed wgpu test device setup so wasm uses async device creation while native uses the safe native path
improved generated wasm test async handling so await inside macro arguments is detected correctly
fixed flaky unary_grad expectations by applying lower precision only for wgpu, while keeping strict checks for other backends

This branch has not been deployed

No deployments
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.

2 participants