Repository navigation
candle-transformers: add quantized GGUF support for Qwen3.5 hybrid SSM/attention architecture - #3461
latentcollapse wants to merge 1 commit into
Conversation
|
PyTorch comparison — RTX 5060 (8151 MiB), CUDA 12.8, bf16
Worth noting: PyTorch logs If someone has |
|
If it helps, I made a working flash-linear-attention implementation for GatedDeltaNet layers which is considered as working and heavily tested on CUDA and CPU backends here https://github.com/ericcurtin/inferrs/blob/main/inferrs-models/src/models/qwen3_5_linear_attn_scan.rs using candle only |
|
Awesome work! Does your Flash-linear-attention impl handle the full hybrid architecture (both SSM layers and the attention layers) or just the SSM path? Edit: I folded it into my Candle PR because it's clean and we just ran the smoke test. Worked great. Really nice job on that, btw |
|
I've also implemented Multi-Token Prediction speculative decoding llmmanorg/llmman#198 llmmanorg/llmman#202 if it also helps |
|
When will this merge? |
Changes can be cleanly merged, but I think it's on the candle engineers to merge it when they're satisfied. Unless I haven't hit some part of my obligation yet |
|
Hi, first off, thanks a lot for this. Really appreciate the work. I wanted to try GPU-only inference on my machine and hit two things on current HEAD. Sharing a minimal repro + fixes in case they're useful. Environment: RTX 4090 (Ada, sm_89), CUDA 13.0, cudarc 0.19.8, Windows + MSVC. Model: 1. Build blocker, two calls to methods that don't exist
2. GPU dtype mismatch at runtimeOnce it built, GPU inference failed on the very first layer, while The per-layer input upcast is gated on let x = if self.device.is_cpu() && x.dtype() != DType::F32 {
x.to_dtype(DType::F32)?
} else { x };This guard has been there since the initial commit and was harmless while the SSM ran in the model dtype. Since Applying the upcast on all devices fixes GPU and leaves CPU behavior unchanged (verified on both, correct output, ~54 tok/s decode on the 4090): let x = if x.dtype() != DType::F32 {
x.to_dtype(DType::F32)?
} else { x };GPU throughput after the fix (RTX 4090)All three run correctly on GPU with the one-line change above (
(Sidenote: the Unsloth |
|
davidlandais thank you so much. I'm gonna do a review and fix those issues. I appreciate the feedback. It's definitely not perfect yet, but it's a mostly completed base so far for anyone wanting to run Qwen3.5 |
|
Tested this branch on Metal (Apple Silicon, M4 Pro) since the thread so far only shows CUDA/CPU results — hit two backend gaps, both fixed, neither in the DeltaNet scan itself. 1. RoPE inv_freq built as f64, cast to F16 — no such Metal kernel
let inv_freq: Vec<f32> = (0..dim)
.step_by(2)
.map(|i| (1f64 / rope_theta.powf(i as f64 / dim as f64)) as f32)
.collect();2. RMSNorm's pre-norm F32 cast was CPU-only In the decoder layer's let x = if x.dtype() != DType::F32 {
x.to_dtype(DType::F32)?
} else {
xResult after both fixes: Found these while testing with Claude Code — happy to share the full diff against this branch if useful, or just leave this here as a pointer. Really nice work on the chunked scan, by the way — matching it against the HF reference layer-by-layer must have been a slog. |
|
Thanks for the Qwen3.5 work. PR #3396 already introduces the public dense and quantized Qwen3.5 implementation under qwen3_5 / quantized_qwen3_5, so we should avoid maintaining a second quantized_qwen35 API and example. |
|
Sorry for the delayed response. I missed this notification while working
through another project. Thanks for the review and for pointing me at
#3396. I’ll rebase onto it, narrow the PR to the incremental parallel-scan,
testing, and selective dispatch changes, then rerun the CPU/CUDA checks and
update the branch.
…On Fri, Jul 31, 2026 at 9:47 AM Sébastien ASTORI ***@***.***> wrote:
*astorise* left a comment (huggingface/candle#3461)
<#3461 (comment)>
Thanks for the Qwen3.5 improvements. PR #3396
<#3396> already introduces
dense and quantized Qwen3.5 support, including quantized_qwen3_5.
Could this PR be rebased on top of #3396
<#3396> and narrowed to its
incremental improvements—such as the parallel scan, tests, and selective
CPU/GPU loading—instead of introducing a second quantized_qwen35
implementation and example? This would avoid duplicate public APIs and two
divergent implementations to maintain.
I applied this cleanup in my fork while preserving the implementation from
#3396 <#3396>: ***@***.***
<astorise@0b7adb29>
—
Reply to this email directly, view it on GitHub
<#3461?email_source=notifications&email_token=B3NUFUMRX3WMFA6FUJXR64T5HSPO3A5CNFSNUABFM5UWIORPF5TWS5BNNB2WEL2JONZXKZKDN5WW2ZLOOQXTKMJUGM2TMNZUG422M4TFMFZW63VGMF2XI2DPOKSWK5TFNZ2KYZTPN52GK4S7MNWGSY3L#issuecomment-5143567475>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/B3NUFUIAYUM5SEECK4ZNHU35HSPO3AVCNFSNUABFKJSXA33TNF2G64TZHM3DKNJXHE3TQNBYHNEXG43VMU5TIMRUGU4DQOJQGI4KC5QC>
.
Triage notifications, keep track of coding agent tasks and review pull
requests on the go with GitHub Mobile for iOS
<https://github.com/notifications/mobile/ios/B3NUFUMTKR5VFTR2P4HJVJD5HSPO3A5CNFSNUABFM5UWIORPF5TWS5BNNB2WEL2JONZXKZKDN5WW2ZLOOQXTKMJUGM2TMNZUG422M4TFMFZW63VGMF2XI2DPOKSWK5TFNZ2KUZTPN52GK4S7NFXXG>
and Android
<https://github.com/notifications/mobile/android/B3NUFUPFJZ5QWLUDYYIZJWD5HSPO3A5CNFSNUABFM5UWIORPF5TWS5BNNB2WEL2JONZXKZKDN5WW2ZLOOQXTKMJUGM2TMNZUG422M4TFMFZW63VGMF2XI2DPOKSWK5TFNZ2K4ZTPN52GK4S7MFXGI4TPNFSA>.
Download it today!
You are receiving this because you authored the thread.Message ID:
***@***.***>
|
…ention prefill Rebases PR huggingface#3461 on top of upstream main and narrows diff to the chunked GatedDeltaNet parallel scan algorithm, test fixtures (CUDA/CPU), and selective device dispatch.
1765abe to
cb2697c
Compare
|
Qwen3.5 (including vision part), Qwen3.6, Qwen3.8 all supported in: https://github.com/lucasjinreal/Crane/ , also with GGUF support. |
this library looks awesome. and it uses candle. and wow its really that much faster. gotta try qwen 3.8 on my MacBook. thanks. |
|
@AlpineVibrations we also supported many onnx model by extending onnx op support range, which is currently candle lack of. And we also provides many kernels with extremely crafted speed. What's more, |
|
cool. I just opened an issue for responses api support. been trying out the chat completions. pretty cool so far. |
|
also cant find the right quantized qwen 3.8 to run... |
|
candle-transformers: add chunked GatedDeltaNet scan for Qwen3.5 prefill SummaryAdds a pure-Candle chunked GatedDeltaNet scan for Qwen3.5-style linear-attention prefill. This PR was originally a larger Qwen3.5 GGUF implementation. It has since been rebased on current
What this adds
The implementation uses a chunk size of 64 and is adapted from the Qwen3.5 ValidationThe module includes regression and parity tests covering:
The numerical comparisons use tight tolerances and explicitly check for non-finite CUDA outputs in the overflow-sensitive paths. ScopeThis PR intentionally no longer contains the earlier full GGUF Qwen3.5 model implementation, CLI example, or unrelated backend/model-loading changes. Those were removed during the rebase/scope reduction so this PR can be reviewed as a focused linear-attention primitive. The branch is rebased on current upstream |
…ention prefill Rebases PR huggingface#3461 on top of upstream main and narrows diff to the chunked GatedDeltaNet parallel scan algorithm, test fixtures (CUDA/CPU), and selective device dispatch.
dd73a46 to
eb47da1
Compare
Summary
Adds
quantized_qwen35.rs— a GGUF-quantized inference implementation for Qwen3.5, the first working Gated DeltaNet + partial RoPE model in Candle.Qwen3.5 is architecturally distinct from Qwen3: it interleaves full-attention transformer layers with linear-attention SSM layers (Gated DeltaNet), uses grouped-query attention with partial RoPE (
rope.dimension_count=64), and has paired per-head RMSNorm on K/V. Getting coherent output required finding and fixing seven independent bugs. Each is documented below.Update (Apr 14): Added chunked parallel GatedDeltaNet scan for prefill, contributed by @co-mapis. Replaces the O(T) sequential recurrence with an O(T/64) chunked WY/Woodbury implementation. See new section below.
Chunked GatedDeltaNet Scan (prefill)
candle-transformers/src/models/qwen3_5_linear_attn_scan.rs— contributed by @co-mapis, adapted from the HuggingFacetorch_chunk_gated_delta_rulereference.What it does:
gated_delta_rule_chunked— O(T/64) chunked parallel scan for prefill (seq_len > 1). Processes tokens in chunks of 64 using the WY/Woodbury representation with forward substitution to solve(I + A)per chunk. State is propagated sequentially between chunks (causality requires this), but intra-chunk work is parallelised via batched GEMMs.sequential_step— single-step decode (seq_len = 1), extracted from the original loop.SSMWeights::forward: prefill → chunked scan, decode → sequential step.Float32 overflow fix (baked in):
For long sequences with Qwen3.5's decay values (~−6.72 per step),
exp(g_cumsum[i] − g_cumsum[j])in the upper triangle reachesexp(~275)→+inffor t ≥ 15 tokens.inf * 0 = NaN. Fix: zero the upper triangle in log-space before exp, then re-zero after — safe because lower-triangular entries are always ≤ 0 soexp ≤ 1.Validation:
log_g ≈ −6.72(real Qwen3.5-0.8B values)Bug elimination log
All bugs were diagnosed by comparing against
modeling_qwen3_5.pyfrom Hugging Face as the ground-truth oracle. The canonical probe throughout was: "What is the capital of France?" — expected: Paris.Bug 1 — SSM decay sign double-negation
Raw GGUF
ssm_avalues are negative. The original path negated them again, turning exponential decay into exponential growth. Fixed: loadssm_aas-is.Bug 2 — Depthwise causal conv applied backwards
apply_conv1d()kernel orientation mismatched PyTorchConv1dleft-padding convention. Fixed: reverse the kernel before application.Bug 3 — Full-attention gate dropped
attn_q.weightin GGUF encodes both query and gate concatenated. The original path used the full tensor as query, discarding the gate. Fixed: splitq_projinto(query, gate)and applysigmoid(gate)beforeo_proj.Bug 4 — Partial RoPE not honored
rope.dimension_count=64from GGUF metadata was read but not used — RoPE was applied to the full head dimension. Fixed: apply RoPE only to the first 64 dims per head.Bug 5 —
<think>preamble masking failureAn empty
<think>block in the prompt triggered chain-of-thought analysis mode. Removed from the generation path.Bug 6 — GQA paired RMSNorm hardcoded 8×8 instead of 2×8
head_count_kv=2was not threaded through norm construction; norm was applied to the wrong grouping. This caused copy-attention dominance and consistent France→United States substitution. Fixed: readhead_count_kvfrom GGUF metadata and thread through norm construction.Bug 7 — SSM norm epsilon hardcoded 1e-5 instead of 1e-6
qwen35.attention.layer_norm_rms_epsilon=1e-6from GGUF metadata was ignored; 1e-5 was used globally. This distorted 18/24 SSM layers and was the fix that restored Paris on raw factual probe. Also fixed here: KV-head count was being inferred fromblk.0(an SSM layer) instead of fromqwen35.attention.head_count_kvmetadata directly.Key lesson: No hardcoded dimensions, grouping factors, or norm conventions. Pull everything from GGUF metadata/config, or treat the path as suspect.
Validation
Tested on RTX 5060 (8 GB VRAM), all Unsloth quantizations,
temperature=0.0:9B was validated with CPU/GPU split loading (32 GPU layers, embeddings on CPU, ~6.5 GB VRAM). That split-loading API is a separate follow-up PR.
Solo dev, limited hardware — open to corrections on larger variants or alternative quantization families.
Files changed
candle-transformers/src/models/quantized_qwen35.rs— new file, GGUF-quantized hybrid SSM/attention modelcandle-transformers/src/models/qwen3_5_linear_attn_scan.rs— new file, chunked GatedDeltaNet scan (contributed by @co-mapis)candle-transformers/src/models/mod.rs— register both new modulescandle-examples/examples/quantized-qwen35/main.rs— runnable example (local GGUF + tokenizer paths)