Skip to content

qwen38: every layer as one chain on the CUDA device (COLI_CUDA_CHAIN=1), on the qwen36 chain's table - #2089

Merged
JustVugg merged 3 commits into
JustVugg:devfrom
kreuzzelg:qwen38-cuda-chain
Oct 16, 2026
Merged

JustVugg merged 3 commits into
JustVugg:devfrom
kreuzzelg:qwen38-cuda-chain

Conversation

@kreuzzelg

Copy link
Copy Markdown
Contributor

The second chain, as agreed on #1992: Qwen3.8 Flash Next on the table #1992 brought (cuda_chain.h), qwen38_chain.h's frames and contract.

What it is

c/qwen38_cuda_chain.h. Per layer, one frame on the card: the previous block's MoE output joining the four hyper-connection streams (routed + sigmoid(gate) x shared, then hyper += inject x block), the gated residual's read (per-stream norm, the low-rank down/up pair, the stream mix, the inject weights: CC_EW_HC_*), the gated attention with QSA (the index keys into the device cache, each completed block's pooled key computed once, the indexer's top-k selection per query row, the attention over the listed positions with the output gate) or the Gated DeltaNet (the convolution with its ring, the recurrence with its state, the sigmoid-gated norm), the write-back, the second gated residual, the router logits; waited for. The host routes and runs the routed experts through the fp8 tier (q38_moe_ex routed only) and hands the sum up; the shared expert runs on the card meanwhile (frame A2). At the PLE layer the n-gram rows come from the host (disk reads, the history replayed as q38_ple does) and the key and value projections, the gate and the dilated convolution with its ring run on the device. The last frame runs the final mixer and lm_head; the streams come back when the MTP head needs them.

The matrices. The tier's int8 copies where the placer put them (Q38_TRUNK_GPU, COLI_PLACE); every other matrix of a layer, the final mixer and lm_head the chain uploads itself (Q38Weight.cc): the trunk's int8 rows when the CPU holds them (the same bits), else the BF16/F32 rows as f32. So the chain runs whatever the placement did; what it adds in VRAM are the matrices Q38_TRUNK_MIN_KB never offers (the gated residuals' low-rank pairs, the DeltaNet b/a rows, the PLE projections: 174 MiB on the 35B-class trunk) and the state (381 MiB: the K/V, index-key and pooled-key mirrors, the DeltaNet state and conv rings, the PLE ring).

State as qwen36's chain: the host's K/V and index caches canonical behind a watermark, the pooled block keys behind one of their own; the DeltaNet state, conv rings and PLE ring on the device while the chain runs, pulled before the host reads them (a pinned snapshot, the prefix cache) and pushed after it writes them (a reset as zeros, a restored snapshot as an upload). A verify runs row-wise (the warp GEMV computes each row on its own: a decode step's bits) and copies the DeltaNet state, the conv rings and the PLE ring after each of its rows into a device slot; a rejected draft swaps slot r in (q38cc_rollback) as the CPU swaps its own. A lost device zeroes the state, rebuilds it on the CPU from the prefix record (the PLE history replayed) and runs there from then on. Q38_DN_GPU's per-layer step is not set up beside the chain; KV_SLOTS > 1 keeps the chain off.

The table gains cc_qsa and cc_ple (chain_qsa.comp and chain_ple.comp ported line for line, appended to the table: an older DLL's shorter table is refused by the size check) and the recurrence at KD 4 (the tiny fixture's); the fake carries both. One fix in q38_forward: its final lm_head was bound to chain_logit under COLI_VULKAN only, so a second chain backend recomputed the logits from the CPU's mixed; it is bound for either now.

Measured (RTX 3070 alone, the FP8 checkpoint, the trunk's 553 matrices as int8 in VRAM, the tier at 16 % hits behind it, the usual 49-token prompt, 100 new tokens, two runs each)

TTFT tok/s, the run ms per token resident-mm / forward DeltaNet QSA PLE lm_head shared
Q38_DN_GPU=1 (#1870, the per-layer step) 19.0-19.1 s 1.22 / 1.26 612-632 172 ms 161 23 12 26 23
COLI_CUDA_CHAIN=1 17.7-17.9 s 1.41 / 1.43 526-535 74 ms (the chain waits 68) in the chain in the chain in the chain 10 0.4

The chain takes the whole dense part at once: -15 % per token and 1.2 s off the first token. The token stays the experts': about 1 s of expert reads per forward (24,576 FP8 experts behind a 4 GiB trunk on 8 GB) and 290 ms of routed experts, which no chain touches -- the trunk's weight is what you said on #1992 it would be. On a card with room for the experts the dense share would be the visible one. The chain is deterministic run to run; its text opens as the per-layer path's and diverges after about ninety characters (rounding order: float trees in the norms, the online softmax).

Verified

Thanks to Claude Code for the help along the way.

kreuzzelg and others added 3 commits October 14, 2026 21:01
…1), on the qwen36 chain's table

qwen38_chain.h's frames and contract on cuda_chain.h: the four hyper-connection
streams on the device (the gated residuals' read and write-back as HC ops), the
PLE layer (the n-gram rows from the host, the projections, gate and dilated
convolution with its ring on the device), QSA (the index keys into the device
cache, each completed block's pooled key once, the indexer's top-k per row,
the attention over the listed positions), the DeltaNet with the sigmoid gate,
the final mixer and lm_head; a verify's per-row copies of the DeltaNet state,
the conv rings and the PLE ring, the rollback, a lost device rebuilt from the
prefix record. The matrices: the tier's int8 copies where the placer put them,
the chain's own uploads (int8 rows when the CPU holds them, else BF16/F32 as
f32) for the rest. The table gains cc_qsa and cc_ple (chain_qsa.comp and
chain_ple.comp ported line for line) and the recurrence at KD 4; the fake
carries both. q38_forward's final lm_head is bound to the chain's logits for
either backend (it was Vulkan's guard alone).

tests/test_qwen38_cuda_chain.c: the tiny FP8 fixture's oracle with the chain
on (8/8 tokens, cosine 0.99998), a frame failing mid-run (rebuilt on the CPU,
still within the limits), and the per-matrix path; in CI beside
qwen38-dn-gpu-check. The op test covers the QSA block keys and selections
(ties as the CPU sorts, the causal case) and the PLE gate and convolution.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ify as a decode step

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
)

The fourth case of test_qwen38_cuda_chain: the tiny FP8 fixture with its
MTP head (qwen38-tiny-fp8-mtp-generate), Q38_MTP_FORCE=reject, the chain
on: every draft rejected, every verify rolling the device's copies back
(q38cc_rollback), the run within the oracle's limits (8/8 tokens, cosine
0.99998), and the fake counting the DeltaNet snapshot writes. Dropping the
rollback's buffer swap fails it (6/8 tokens): the case guards the
contract the first three did not reach. Offered on JustVugg#1992 against this
branch's 37fe058; applied as is.

Co-authored-by: mfethe1 <mfethe1@users.noreply.github.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@JustVugg
JustVugg merged commit ca78f3f into JustVugg:dev Oct 16, 2026
5 of 114 checks passed
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