Repository navigation
qwen3_5 cuda: map an EXL3 embedding from host memory only when the card has no room - #1678
Conversation
|
Thanks @diwakergupta, this is the shape we asked for. Two things before it lands:
|
82d8a50 to
4169f09
Compare
|
Thanks @ashhart. Both done in
Setting it too low just refuses in a narrow band above |
|
Thanks @diwakergupta, the bisect is exactly what we needed. With the lower margin, the branch keeps the rows on the card wherever main does, up to main's 92,672-token limit, and maps them only where main would refuse to start. GB10s are unified memory, so they never map. This lands now; it's gated and queued, and we've written the changelog line. |
|
@diwakergupta, one mechanical thing before this lands. Rebased onto current main, |
|
Thanks @ashhart. Done in The hookup moved into This also closes a small leak: if the second measure pass failed after the rows' host memory was mapped, that memory wasn't freed. I checked |
4169f09 to
bc018e7
Compare
…ly when the card has no room An EXL3 checkpoint's bf16 embedding rows (2.37 GiB on the 27B) load into the arena as before, unless a discrete card's room could not hold the weights, one stream at the asked window, the DFlash2 drafter beside them and 96 MiB for the load's other bytes (about 112 MiB on a 3090 Ti). Then the rows go to mapped pinned host memory, which the embedding gather reads in place. Unified memory never maps them. qwen3_5's Options take the server's room probe and the drafter's directory; Engine.init takes the fit they make (null: never map). Draft.streamBytesFor sizes a drafter's stream before it loads.
bc018e7 to
0740df1
Compare
|
Thank you @diwakergupta. This is merged into main with your commits under your own name, so you're listed as a TensorFold contributor. It ships in the next release, credited to you in the changelog and the release notes. |
|
Landed as e854c28. Thanks @diwakergupta! |
What this changes
A dense EXL3 checkpoint's bf16 embedding rows (2.37 GiB on the 27B) now load into mapped host memory, but only when the card has no room for them. In every configuration that fits today, the load and the default path are
main's.This is the host-mapped embedding from #1476, made automatic as @ashhart suggested when parking #1476. Unconditional mapping slowed decode on the GB10, so it's now a fallback rather than the default. Its value is on a 24 GB card: there it lets the 27B with DFlash2 serve a 98k or 128k-token window that
mainrefuses to start.cuda_host_embed.zig): the rows go to host memory when a discrete card's room is less than the weights and scratch, one stream at the asked window, the DFlash2 drafter's weights and one stream of its ring, taps and frame scratch, plus 96 MiB for the load's other bytes (kernel modules, the EXL3 kit, the server's start-up costs). Those come to about 112 MiB on the 3090 Ti, so the allowance sits below them. Too high would map rows that fit and slow decode. Too low only refuses in a narrow band abovemain's limit, wheremainrefuses too. Unified memory (the GB10) never maps them: its host memory is the same memory.qwen3_5'sOptionsgainroom(the server's room probe, whichgemma4already takes) anddrafter.openpasses them toEngine.init, which decides between the loader's measure pass and the arena allocation. Its other callers (the learner CLI and the CUDA tests) passnulland never map.Draft.streamBytesFor(config, hidden)uses the drafter's ownlayout()for one frame, plus its ring and the target's taps. Without it, the rule missed the 98k case by the drafter's 0.35 GiB.cuda_exl3_weightsskips the rows in the arena whenEngine.host_embedis set, and copies them into that pinned, device-mapped buffer. The engine frees it indeinitand on a failed load.qwen3_5: embedding rows read from host memory: no room beside a 98304-token stream.Receipt
Environment. RTX 3090 Ti (24 GB,
sm_86).turboderp/Qwen3.8-27B-exl3branch3.50bpw(rev8351c54e),z-lab/Qwen3.8-27B-DFlash2(rev50307d4c) at 4 bits, bf16 KV cache. Both binaries came fromzig build native -Dsm=86, one frommainand one from this branch (the table below:a0d24896; the limit rows:9e8c368b).What starts, by
--context. Each run was a fresh server, with the start-up line's numbers:--contextmainmain's largest)Exactness with the rows mapped (
--context 98304).tools/bench_concurrent.py --levels 1 --tokens 384 --reps 3 --alone --serial, the default code and chat cells (greedy and sampled), and--promptswith two long prompts (5,080 and 2,278 tokens):"draft": falseand every repeat equal: 0 unequal, 0 failed.token_sha256equalsmain's (at--context 32768) in 72/72 replies.Decode speed with the rows mapped, against
mainon the card, per-stream tok/s (a median of 3). Here it costs 0–1.2%;maincan't run at this window at all:main, 32k, rows on the cardDefault path. At 32k, 64k and every window up to
main's largest (92,672 here) the rows stay on the card and the same model bytes load as onmain. The code that runs per token ismain's.Tests. The full checklist suite (
zig build test test-kernel-contract test-golden test-server-security test-pull-security fuzz-cortheon test-cuda-host) plustest-qwen35-formats-host: 481/481 steps succeeded.lean_new.pyagainstmain: 0 new problems. New host test:the rows stay on the card unless a discrete card's room is short of the device bytes and one stream(cuda_host_embed.zig, run bytest-qwen35-formats-host).Not run. GB10: unified memory never maps, so its path is
main's. Metal: not affected. MLX and MoE EXL3 checkpoints: the rule is EXL3-only and the MLX loader doesn't read it; a MoE EXL3 checkpoint takes the same rule but wasn't served. Other drafters: the rule sizes only a DFlash2 drafter and counts nothing for any other.Checklist
"draft": false, and the mapped rows givemain's tokens.sm_86only.