Repository navigation
cuda qwen3_5: --learn-conversations keeps the 27B's and 35B's states on disk (#840) - #1358
Conversation
6ae01c2 to
2148602
Compare
|
Follow-up: two changes on this PR:
The Windows CI fix for cuda_disk_test.zig (platform.c.getpid, shell edits skipped on Windows) is included. The new tests use no shell and no pid formatting. Gates (35B-A3B + MTP, bf16, contiguous and TF_QWEN_KV_PAGES=256): states written, server restarted, read back. Resumed == fresh on tokens and the last row's logits hash (turn 2: 2/2; others: 4/4; concurrent resumes: 10/10). Reads at 8192 and 4096, 0 checks failed; drafted == plain 10/10; concurrent == solo. zig build test passes on this branch and on the next, and lean shows 0 new problems. |
2148602 to
8ebdedb
Compare
|
Decoupled: this PR now sits directly on main (e9b69ed), with #1357 out of its history, and it builds and tests alone. The two pieces it borrowed from #1357:
#1411 (wojo, GLM-5.3-Flash on CUDA). That PR carries our grid-starts commit (a63afd4 here as 7aab9c2) with the same patch and message, so it drops out of whichever PR lands second. Its
Each resolves by keeping both ( Otherwise the commits are unchanged: grid starts (byte-identical to the original), the rows transition test, the disk image, the Windows test fix and the probe's single cleanup path. Gates (35B-A3B + MTP, bf16, fresh servers; states written, the server restarted, then read back from disk):
|
|
Thanks for decoupling this from #1357; it now builds and tests on main alone. The grid-starts fix and its transition One small change: rebased on current main, After that we'll run the write, restart and read cell on the 27B (MLX 4-bit and NVFP4), bf16 and fp8, including a |
8ebdedb to
402e64f
Compare
|
Thanks. Rebased on main
Checks: lean 0 new (428 on base and head); |
|
Thanks: every earlier ask is met. The one-line import keeps Next is our GB10 run of the disk-restore cells on the current head: a written, restarted and read conversation equal to a fresh one at grid and off-grid positions, on main and on this branch. |
|
Main moved on under this: spill writes are now staged and written by an I/O thread (afa4fa3, cbd5305, d22d704), and Could you rebase onto main the same way, or say if you'd rather do it differently? The GB10 run of the disk-restore cells (written, restarted and read back against fresh, with a 16,384-token and a 14,000-token conversation) is queued on that merge. |
A planned family with a chunk grid (CUDA qwen3_5's 4096-row passes) keeps its RAM states on the grid when a request names no chunk starts, but the disk tier took the empty list literally: every state was named as one chunk from row 0, two states never shared a block, and `matches` refused every read because the request's starts lacked the state's position. `Options.grid` gives the disk tier the store's grid. A request that names no starts is written and matched with the grid's starts below the state, as the RAM store's `startAt` already places them. Families without a grid, and requests that name their starts, are unchanged.
…arts and requests that name none The server sends message starts once ashhart#1335 lands, so one conversation's turns can name chunk starts while another's (or an older server's) name none. Each case writes a state, restarts and reads it back from disk with the other kind of request: a state written with starts on the grid serves a request that names none; one at 1000, or at 1024 with its chunks below cut at 300, serves none; a grid state serves named starts that hold the grid's below it and its own position, and no others. Every resume is a fresh pass's bytes.
…on disk The CUDA Qwen hybrids refused --learn-conversations (cuda_refusal.zig). Their kept states are already host copies (cuda_reuse.zig): the recurrences and convolution tails, then each attention layer's key and value rows by head, in bf16 or fp8 with its scales. cuda_disk.zig lays that copy out as the rows store's image: 16 bytes of words (position, rotary shift, cache format), the recurrent state as the state's own part, and every head's rows as row parts, so 256-token blocks are shared between states and conversations as on Nemotron. A copy read back is a new host copy, restored as any other; one whose words name another position or format is refused. native/cuda_disk.zig's identity is split from Nemotron's probe, and a family that declares `kept_disk` (image, probe, facts) attaches the same store: the checkpoint, kernels, device, driver, the cache format and drafter taps, and a probe state's hash at the first chunk cut. Nemotron's identity bytes are unchanged; identityOf is public, as ashhart#1411 (GLM-5.3-Flash) factors it, and a module that opens the store itself (several ranks' parts) declares attach and is called in place of the probe. The payoff rule, the caps and the write-behind are the core's. A state whose prompt holds images stays in RAM (its image identities are not on disk). The qwen35 module imports the engine_api module the server links (build/cuda.zig), as the disk image names the rows store's types. Co-authored-by: gilby <880013+gilby@users.noreply.github.com>
… skips its shell edits on Windows
… prompt pass The qwen3_5 probe (and Nemotron's, which it copied) released the probe stream by hand after the prompt pass and on its error, but returned without a release when the wait after the pass failed, and dropped no state the pass had kept. The pass and its wait now run in one block whose defer releases the stream, and a kept state is dropped by a defer of its own. A host test drives the qwen3_5 probe with a target whose pass fails, a stream wait that fails and a pass that keeps no state: one release each time.
… in the disk facts, a shell-free disk test zig/build/cuda.zig takes the engine_api import exactly as ashhart#1357 writes it, one line with its comment, so the file stays at 600 lines and the two no longer conflict in family(). The disk identity's facts name whether int8 prompt projections are on. The restart test runs in std.testing.tmpDir and cuts a block or flips a state's byte through the file API: no shell, no fixed /tmp path, no Windows skip.
402e64f to
f593a52
Compare
|
Rebased onto main One test needed the I/O thread too. Checks: lean 0 new (428 on base and head); |
|
Merged, with your six commits as they were, and the changelog entry follows in 8c2037c. Thank you @jschmied, and @gilby for your part. It ships in 1.1.8. Our device run on a GB10 with Qwen3.8-27B: for a 16,384-token and a 14,000-token conversation, a server with |
|
This is in TensorFold 1.1.8, released today. Thank you, @jschmied and @gilby! |
Part of #840, the "Spill of evicted states" row (#68, @gilby; claimed in #840, comment 6399090685). It sits on main (
e9b69ed70), no longer stacked on #1357.On CUDA,
--learn-conversationswrote kept states to disk for Nemotron only, and the qwen3_5 family (Qwen3.8-27B, Qwen3.6-35B-A3B) refused the flag (cuda_refusal.zig). This PR gives the family the same disk tier Nemotron has: 256-token blocks shared between states, the core's payoff rule,--learn-gib,--learn-daily-gib, the free-disk floor, and the SIGTERM flush.Commits
qwen3_5 is a planned family with a 4096-row grid. A request that names no chunk starts keeps its RAM states on the grid (
Store.startAt). The disk tier read the empty list literally, so every state was named as one chunk from row 0 and no two states shared a block.matchesalso refused every read, because the request's starts did not contain the state's position.Options.gridnow gives the disk tier the grid, so such a request is written and matched with the grid's starts below the state. Families without a grid, and requests that name their starts, behave as before. New test:rows_test.zig"a grid family's requests name no chunk starts ...".The family's kept states are already host copies (
cuda_reuse.zig): the recurrences and conv tails, then the key and value rows of each attention layer by head, in bf16 or in fp8 with its scales.families/qwen3_5/cuda_disk.ziglays the copy out as the rows store's image. It has 16 bytes of words (position, rotary shift, cache format), the recurrent state as the state's own part, and each head's rows as row parts.native/cuda_disk.zigsplits its identity hash from Nemotron's probe. A family that declareskept_diskattaches the same store. Its identity covers the checkpoint, the kernels, the device, the driver, the cache format, whether a drafter set the target's taps, and the hash of a probe state at the first chunk cut. Nemotron's identity bytes do not change.TF_QWEN_KV_SHAREkeeps prompts as pool pages without the host cache, so--learn-conversationswith it is refused at startup.What a kept state holds is unchanged from the RAM cache's copy: the KV prefix (contiguous or paged, gathered page by page) plus all recurrent state (GDN state and conv window) at the mark. The DFlash2/MTP drafter context is not part of the CUDA kept state, in RAM or on disk. After any resume, the drafter's context starts at the mark (
cuda_draft.zig"a gap"). That changes acceptance only. Tokens and logits are the target's, and the drafted == plain gate below covers it.Gates (GB10, Qwen3.8-27B MLX 4-bit + DFlash2, Qwen3.6-35B-A3B MLX 4-bit + MTP)
Each config runs these arms:
--no-drafts.--learn-dir. It runs turn 2 of both conversations, read back from disk, then four more turn-2 variants solo and at c=1/2/4.Each request records its text and a hash of the last prompt row's logits. The qwen CUDA backend has no logprobs route (
first_row), so the hash comes from a gate-only log line infirstMaskedFn, which is not part of this PR.TF_IMPRINT_VERIFY=1reads back every written state and compares it byte for byte.Every row below matched in full (tokens and the logits hash), except where the row says otherwise.
TF_QWEN_KV_PAGES=256,_ALL=1)--no-draftsTF_IMPRINT_VERIFYread-back of each written stateTurn 2 of the 9.6k-token conversation, read from disk vs fresh: 27B bf16 1.69 s vs 7.73 s (658 MiB read in 0.40 s); 35B bf16 0.49 s vs 1.82 s (221 MiB in 0.13 s).
27B only (the core paths are the same for the 35B, so its runs were limited to the exactness gates):
.blktruncated and one.stbyte flipped. Each read fails its check (RowsRead / RowsHash) and its states are forgotten. The turn is recomputed and equals fresh. Nothing crashes.--learn-gib 0.8: the disk stays at 845 MB, then 422 MB, under the cap. The state that did not fit is recomputed and equals fresh.--learn-daily-gib 0.5: the second state "stays in RAM only (DailyBudgetSpent)", and its turn recomputes and equals fresh.Turn-2 TTFT of a 29,455-token two-turn chat after a restart (
ttft.py, 27B + DFlash2, 2 disk + 2 cold, alternating): 1.88 s and 1.97 s read back from disk (28,672 tokens, 1,938 MiB in 1.0 to 1.1 s), against 25.4 s and 25.5 s cold. All four runs gave the same text and the same last-row logits hash. The 35B TTFT run was skipped under the GPU-time rules, because the family's code is the same.Not shown on the device:
rows_select.zig) and is covered byrows_test.zig"a read that costs more than prefilling ...".TF_QWEN_KV_PAGES_ALLprobes another identity (paged arithmetic differs from contiguous in the last bits). States written without paging are therefore not read by a paged server: they are recomputed, which is the correct behavior (row "cross" above).Full
zig build test -Dnvcc=/usr/local/cuda/bin/nvccexits 0. lean: 0 new.The qwen3_5 commit carries
Co-authored-by: gilby <880013+gilby@users.noreply.github.com>; the grid fix is new.Overlap: @gilby's #1335 adds a line next to ours in
qwen3_5/cuda_native.zigandcuda_native_moe.zig(a textual conflict only). With #1335 the server names message starts, so its turns already carry starts; the grid fix covers requests that name none.