Skip to content

qwen3_5 cuda: dense EXL3 checkpoints (Qwen3.8-27B), with DFlash2 drafts - #1465

Merged
ashhart merged 2 commits into
ashhart:mainfrom
BHCC2025:pr-qwen-dense-exl3
Oct 29, 2026
Merged

ashhart merged 2 commits into
ashhart:mainfrom
BHCC2025:pr-qwen-dense-exl3

Conversation

@BHCC2025

Copy link
Copy Markdown
Contributor

What this changes

The CUDA EXL3 reader (#1150) admits Qwen3.5-MoE checkpoints only, and refuses a drafter on any EXL3 checkpoint. This lets it read dense ones too: turboderp's Qwen3.8-27B-exl3 packs, with z-lab/Qwen3.8-27B-DFlash2 drafts. It is the dense EXL3 path that #603 asked for and that was left with #832.

  • cuda_geometry.zig: an EXL3 config is admitted dense or MoE (learned neurons are still refused).
  • cuda_exl3_weights.zig: a dense layer's MLP loads as three Dense projections (gate_proj, up_proj, down_proj), beside the attention, delta and head projections the reader already loads. The grouped experts' scratch is sized for MoE checkpoints only.
  • cuda_native.zig: a dense EXL3 checkpoint opens DFlash2 or MTP as an MLX one does; MoE EXL3 checkpoints still refuse a drafter. The DFlash2 head already projects through Dense for EXL3 (cuda_draft_frame.zig).
  • docs/recipes/qwen3.8-27b.md: the native CUDA format list. It said the engine reads no NVFP4 or EXL3 checkpoint, which was out of date for NVFP4 as well.

No kernel, launch or rounding changes. Every projection runs on the cuda.exl3.Dense path that the MoE checkpoints already use.

Receipt

  • Environment. main at b30d5b9d plus this commit. One DGX Spark (GB10, sm_121, 128 GB unified memory), Ubuntu 24.04.5, driver 580.178.04, nvcc 13.0.88, Zig 0.17.0, zig build install native -Dsm=121.

    • Checkpoint: turboderp/Qwen3.8-27B-exl3@113cf7ab (the 4.00bpw branch: mul1 codebook, 6-bit head; both shards' sha256 match the revision's LFS ids).
    • Drafter: z-lab/Qwen3.8-27B-DFlash2@50307d4c.
    • --context 131072; greedy unless stated.
  • Before. main refuses the checkpoint at load: the native CUDA engine cannot load ... with its own glue kernels (UnsupportedQwenQuantization).

  • Exactness.

    • tf-qwen35-cuda <model> step|window|tree over 48 ids: window logits equal step logits byte for byte, and so do tree logits.
    • bench_concurrent.py --alone --serial --levels 1,4 --temperatures 0,1.0 (drafts on): alone 20/20 equal, serial 20/20 equal, 0 failed.
    • Drafted equals "draft": false: an 8,020-token chat prompt, 256 reply tokens, all three runs give the same reply hash (fresh, then the cached repeat, then "draft": false).
    • Resumed equals fresh: a two-turn 38.7k-token chat. Turn 2 resumed at 36,864 cached tokens, and both answers are correct. The 8k chat repeat above resumed at 12,288 and equalled the fresh reply.
    • bench_cache.py --require-reuse --prompt-file does not qualify here, because the prompt cache keeps no state for raw token-id completions (kept 0, the same policy for every checkpoint). Its 6 rows still give equal tokens.
    • Cancellation: two streamed 32k-token completions closed mid-prefill. The next chat reply equals the one before them (same sha256), and /health stays ok.
  • Decode (bench_openai.py --tokens 256 --reps 3 --temperatures 0).

    • Drafts on: fibonacci-raw 71.9 tok/s, gpu-chat-no-think 45.8.
    • Without a drafter: 15.5 and 15.3.
    • bench_concurrent.py aggregate at 4 streams: code 137.4 tok/s, chat 101.2 (greedy); 140.9 and 101.4 sampled.
  • Prompt speed. bench_cache.py --require-cold --prompt-file, --prompt-cache-gib 0, a fresh server per length. The prompts are exact token arrays of this repository's zig/src/**/*.zig (sorted by path, read in order) under the checkpoint's own tokenizer. All four qualified cold:

    prompt ttft_s tok/s
    2,048 5.71 359
    8,192 23.07 355
    32,768 94.11 348
    65,536 193.98 338

    That is below the MLX 4-bit checkpoint's prompt path, because EXL3 prompt rows go through Dense in 128-row blocks. Faster Dense prompt kernels are a separate change, and we have one to propose next.

  • Tests run.

    • zig build test: 4,585 passed, 25 skipped, 0 failed (Linux x86_64).
    • zig build test-cuda-host: 3,434 passed, 9 skipped, 0 failed.
    • zig fmt --check is clean on the changed files.
    • python3 tools/zig/lean_check.py: 428 problems on this branch and 428 on main, the identical list (inherited, for example tools/zig/unicode_tables.py:1). Zero new.
    • The new and changed tests fail on main and pass here. On main they fail as format coexistence: EXL3 admits MoE and dense ... (expected the dense config admitted) and a dense EXL3 config (Qwen3.8-27B's layout) is admitted, with no experts. With this change, 0 fail.
  • Not run, and why.

Checklist

  • Every token still goes through the lane rounds. No serial path, and nothing that needs drafts off.
  • Drafted output equals "draft": false, a resumed prompt equals a fresh one, and concurrent equals solo.
  • No precision traded for speed. The bits do not change: the projections run on the existing Dense path.
  • No new or changed GPU kernel.
  • Prompt processing is no slower than the last release: n/a, because the last release refuses this checkpoint. Above, it is slower than the MLX checkpoint.
  • The description says which platforms I ran (CUDA GB10 only) and which I could not.
  • New tests fail before the change, pass after it, and need no device.
  • Comments and docstrings are one line. No measurements or history in the source.
  • No personal data, machine names, internal hosts or local paths. No AI attribution lines.

Thanks to @briepala for the #603 proposal and to @alexderz and @CanadaApollo6 for the RTX 3090 work there. If either of you can run this branch on sm_86, it would close the platform gap above.

@alexderz

Copy link
Copy Markdown

RTX 3090 (sm_86) receipt for this branch, commit c127b64.

Setup. RTX 3090 24 GB, Linux. Image built from this commit with zig build native -Dsm=86 (Zig 0.17.0, CUDA 13.0.2). Checkpoint turboderp/Qwen3.8-27B-exl3@113cf7ab (4.00bpw) and drafter z-lab/Qwen3.8-27B-DFlash2@50307d4c. --context 16384. The 131072 window in the GB10 receipt does not fit this card, and I did not try above 16384. Greedy, thinking off.

Load. The server log says CUDA sm_86 device 0 (NVIDIA GeForce RTX 3090), model 17.08 GiB with the drafter and 16.07 GiB without it, own glue kernels, context 16384. First load 96 s. The drafts-off restart, weights already cached, loaded in 9.9 s. nvidia-smi during serve was about 21.6 GiB.

Exactness. One greedy chat: 33 prompt tokens, 76 completion tokens. Drafts on and --no-drafts produced the same reply (sha256 587b2b7fd854608c).

Decode, same request, wall time, so the short prefill is included:

wall tok/s
drafts on 0.36 s 209
drafts off 2.50 s 30

A repeat of that short prompt reported cached_tokens 0, so this run has no resume number.

Not run. No context above 16384, no long-prompt prefill, no teacher-forced compare against ExLlamaV3.

@ashhart

ashhart commented Oct 27, 2026

Copy link
Copy Markdown
Owner

Thanks, this is the split we hoped for. It only admits a dense EXL3 checkpoint that main refuses and runs it on the existing Dense path, so the MLX and MoE EXL3 paths are unchanged by construction. Host checks pass here on current main.

I'll repeat your exactness receipts on one of our GB10s next: the dense pack with DFlash2 for drafted == plain, concurrent == solo at levels 1 and 4 with temperatures 0 and 1.0, and resumed == fresh by token hash with prompt tails 16 and 17 rows past a kept state; plus a MoE EXL3 checkpoint and the MLX 27B against main.

Two small questions. The resumed == fresh receipt for the 38.7k two-turn chat says both answers are correct, not that they are equal. And "the 8k chat repeat resumed at 12,288" can't be right for an 8,020-token prompt. Could you check those? Not blocking, but worth a follow-up: the licences of the turboderp pack and the z-lab drafter in the recipe, and the recipe's EXL3 section, which still describes the Python engine's span-sliced head.

diwakergupta added a commit to diwakergupta/TensorFold that referenced this pull request Oct 28, 2026
…t plans as the whole head does

The 384-column tail planned eight K splits where the full head plans one,
so its logits and some proposals changed. The view now starts at column
240,128 (exl3.wide_blocks blocks), keeping ashhart#1465's bits. A host test checks
that both span views plan as the whole head does.
BHCC2025 and others added 2 commits October 29, 2026 23:46
The EXL3 reader admitted Qwen3.5-MoE checkpoints only. A dense checkpoint's MLP now loads as three Dense
projections (gate, up, down) beside the attention, delta and head projections the reader already loads, and the
grouped experts' scratch is sized only for MoE. A dense EXL3 checkpoint drafts as an MLX one does: the DFlash2 head
already projects through Dense. MoE EXL3 checkpoints still refuse a drafter. No kernel or arithmetic changes.
@ashhart
ashhart force-pushed the pr-qwen-dense-exl3 branch from c127b64 to b78c7af Compare October 29, 2026 23:46
@ashhart
ashhart merged commit c0de6f6 into ashhart:main Oct 29, 2026
9 checks passed
ashhart pushed a commit to diwakergupta/TensorFold that referenced this pull request Oct 30, 2026
…o vocabulary spans

On ashhart#1465 the drafter projects all 248,320 columns of an EXL3 target's head.
It now projects only the spans it proposes from, as column views of the
stored matrix: exl3.Weight.columns keeps the parent's strides and moves to
whole 128-column blocks, with a host test. The x3 arm shares the affine
head's packing path. MoE EXL3 targets still refuse a drafter.

The family's formats list exl3, and exl3.md and cuda.md no longer say the
native engine serves no EXL3 pack.
ashhart pushed a commit to diwakergupta/TensorFold that referenced this pull request Oct 30, 2026
…t plans as the whole head does

The 384-column tail planned eight K splits where the full head plans one,
so its logits and some proposals changed. The view now starts at column
240,128 (exl3.wide_blocks blocks), keeping ashhart#1465's bits. A host test checks
that both span views plan as the whole head does.
ashhart pushed a commit that referenced this pull request Oct 30, 2026
…o vocabulary spans

On #1465 the drafter projects all 248,320 columns of an EXL3 target's head.
It now projects only the spans it proposes from, as column views of the
stored matrix: exl3.Weight.columns keeps the parent's strides and moves to
whole 128-column blocks, with a host test. The x3 arm shares the affine
head's packing path. MoE EXL3 targets still refuse a drafter.

The family's formats list exl3, and exl3.md and cuda.md no longer say the
native engine serves no EXL3 pack.
ashhart pushed a commit that referenced this pull request Oct 30, 2026
…t plans as the whole head does

The 384-column tail planned eight K splits where the full head plans one,
so its logits and some proposals changed. The view now starts at column
240,128 (exl3.wide_blocks blocks), keeping #1465's bits. A host test checks
that both span views plan as the whole head does.
@ashhart

ashhart commented Oct 31, 2026

Copy link
Copy Markdown
Owner

Thank you @BHCC2025. This is in TensorFold 1.2.0, released today: https://github.com/ashhart/TensorFold/releases/tag/v1.2.0

It landed with your commits under your own name, so you're listed as a TensorFold contributor, and it's credited to you in the release notes and the changelog.

@BHCC2025

Copy link
Copy Markdown
Contributor Author

Thank you, and thanks for the credit. Following up on your two questions and the follow-ups:

  • The 12,288 was mislabeled. It came from a separate 13,130-token chat, repeated: the repeat reused 12,288 cached tokens and gave the same reply hash. It was not the 8,020-token prompt. The 8,020-token check is the drafted == plain one: fresh, the cached repeat and "draft": false gave one reply hash.
  • The 38.7K two-turn chat compared answers, not tokens, so it does not show resumed == fresh. Your token-hash receipt with prompt tails 16 and 17 rows past a kept state is the one that counts; I don't have a token-equal 38.7K run to add.
  • Licences: turboderp/Qwen3.8-27B-exl3, z-lab/Qwen3.8-27B-DFlash2 and the base model are all Apache-2.0 on their model cards. The recipe's EXL3 section still describes the Python engine (the head sliced by its 128-column strips, and that engine's measurements). If it helps, I can send a small docs PR that adds the licences and describes the native path.

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.

3 participants