Repository navigation
Conversation
One object per DeltaNet layer holds the short-conv ring, the recurrent state [vh x kdim x vdim], the conv and norm weights and one token's scratch in VRAM. coli_cuda_dn_step runs a decode token end to end on the card: x (host) -> in_proj GEMV -> causal conv + SiLU -> per value head the L2 norms (double, eps inside the sqrt), the decay, delta against the decayed state, the write-back, out = q^T S over the updated state, the gated RMSNorm with silu(z) -> out_proj GEMV -> out (host). Gates arrive by value. Only x and out cross the bus; the state never does. Same arithmetic as qwen36.c's deltanet(), so the CPU path stays the reference. Optional in the DLL loader (five RESOLVE_OPT); the fake backend carries a host-side reference of the same steps so an engine test runs without a card, and can be told to fail. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…s on the card Where the in_proj (qt_dnproj_init) and the out_proj (a qt_dense handle) of a DeltaNet layer sit on the same device, qt_dn_gpu_init puts the conv ring, the recurrent state and the norm/conv weights there too, and qt_dn_gpu_step runs a decode token through the backend chain. The host copy of the state stays the engine's, moved with qt_dn_gpu_set/get_state when the CPU path needs it; get_state works after a failed step as well, so the engine can continue from what the card holds. A failing step turns the layer off like every other GPU path here. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… end to end With the trunk in VRAM the CPU still spent ~8 of 39 ms/token on the convolution, the L2 norms, the recurrence and the gated norm of the thirty DeltaNet layers -- microseconds of arithmetic, paid as host round trips: the in_proj result came down, the recurrence ran on the CPU, its output went back up for the out_proj. Now a decode token computes the two gates on the CPU (two tiny matmuls) and hands x to qt_dn_gpu_step, which returns the layer's output; one upload, one download per layer. The host arrays stay canonical. Per layer, dn_dev_fresh says the card holds the host's state (nothing to upload before a GPU step) and dn_host_stale says the card advanced past the host copy. A CPU step (a prefill, S > 1) pulls first and marks the device copy old; a snapshot pulls all layers; reset_recurrent and a pin restore invalidate the device copy. A failing GPU step pulls what the card holds and continues on the CPU. Opt-in for now: Q36_DN_GPU=1, and only layers whose in_proj and out_proj the placer put on one card. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…inst the CPU path One in-memory DeltaNet layer, no container, driven through deltanet() on the fake backend: decode tokens through the GPU path give the CPU outputs; the host copy is left alone while the card advances and reads back the CPU path's state; a two-row CPU prefill after GPU tokens pulls first and continues, the GPU token after it pushes first; a failing step turns the layer off and the CPU continues from the card's state. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…experts=all Two placement details the GPU DeltaNet layer needs. The automatic placer chose the device for each offer by room alone, so a layer's in_proj and out_proj could land on different cards (28 of 30 layers qualified on two cards): now dnout goes to the device its dnproj went to when that device has the room. And a hand-written list that reserves a card for the trunk hands that card's experts to the others (JustVugg#1361), which rules out "trunk on the fast card, experts on both"; experts=all keeps every COLI_GPUS card for the experts, reserved or not. Both in test_qwen36_tier_autoplace. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…counts model layers Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Contributor
Author
|
Withdrawing this for now: the two-card numbers above showed that the layer on the card only pays off fully once the experts are homed per layer range instead of per expert index (no per-layer join on the slower card). I will bring both back together in one PR once that is measured; nothing here is abandoned. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
With the trunk in VRAM (#1657) a decoded token on the 35B still spent about 8 of 39 ms in the thirty DeltaNet layers' convolution, L2 norms, recurrence and gated norm:
dn-sub: proj 7.9 | conv 3.0 | l2n+rec 2.4 | norm+out 2.8. None of that is arithmetic -- the convolution is 33k multiply-adds per token, the recurrence 32 outer products of 128 x 128 -- it is the round trip. The in_proj's result came down to the host, the recurrence ran there, its output went back up for the out_proj: four copies and their synchronisations per layer, thirty layers per token. (Found while reading why Ollama 0.34.4 decodes this model at 36 tok/s against our 25: llama.cpp keeps the whole gated-delta layer on the card.)What the PR does
Backend (
coli_cuda_dn_*, five entry points, optional in the DLL loader): one object per DeltaNet layer holds the short-conv ring, the recurrent state [vh x kdim x vdim], the conv and norm weights and one token's scratch in VRAM.coli_cuda_dn_stepruns a decode token end to end: x up, in_proj GEMV, causal conv + SiLU with the ring advancing, per value head the L2 norms (double, eps inside the sqrt as on the CPU), the decay, the delta against the decayed state, the write-back,out = q^T Sover the updated state, the gated RMSNorm withsilu(z), out_proj GEMV, out down. The gates (a/b, 32 outputs) stay on the CPU and travel as kernel parameters. One upload, one download of 8 KB per layer; the state never crosses the bus. Same arithmetic asdeltanet(), so the CPU path stays the reference; the fake backend carries a host-side copy of the steps and can be told to fail.Tier (
qt_dn_gpu_*): where the placer put a layer's in_proj (qt_dnproj_init) and out_proj (qt_densehandle) on one device, the layer joins them there. The host copy of the state stays the engine's, moved withset/get_state;get_stateworks after a failed step, so the engine continues from what the card holds.Engine (
Q36_DN_GPU=1, opt-in): a decode token computes the two gates and hands x toqt_dn_gpu_step. The host arrays remain canonical, with two flags per layer: the device copy is fresh (nothing to upload before a GPU step) or ahead (download before any CPU use). A prompt (S > 1keeps the batched CPU path) pulls first, a--pinsnapshot pulls all layers,reset_recurrentand a pin restore invalidate the device copy, a failing GPU step pulls what the card holds and continues on the CPU. Prefix reuse and the pin machinery are untouched by construction: they only ever see host arrays that are current.Placer, two small things the measurement needed:
autoputs a layer'sdnouton the device itsdnprojwent to when that device has the room (without it 28 of 30 layers qualified on two cards), andCOLI_PLACEacceptsexperts=all-- the routed experts stay on everyCOLI_GPUScard even when a hand-written list reserves some for the trunk (the form for "trunk on the fast card, experts on both", which the reservation rule of #1361 could not express).Measured (RTX 3070, per-row int4 35B, 49-token prompt, 200 decoded tokens, trunk in VRAM,
COLI_TIMERS=1, output byte-identical between two GPU runs)Q36_DN_GPU=0Q36_DN_GPU=1The 10.8 ms that remain are the two GEMVs (16 and 8 MB per layer) and the two copies. With only
lmhead,dnproj,dnoutplaced the gain is the same (41.5 -> 35.3 ms). On two cards the layer gains the same 6-7 ms (44.3 -> 37.5 with the trunk split, 40.1 -> 32.9 with the trunk on the 3070 andexperts=all), but this box's second card is a Quadro RTX 4000 that runs a dense GEMV about 65 % slower (lm_head 7.3 against 4.4 ms), and at 99-100 % residency the MoE phase only drops from 14.0 to 11.2-13.6 ms (take5.2 ms/token waiting on the slower card's groups), so two cards end level with the 3070 alone. That is a property of this pair of cards, not of the layer.Not byte-identical to the CPU path (the GEMVs sum in a different order, as for every placed component); two GPU runs are identical to each other. Against Ollama 0.34.4 on the same box (36.4 tok/s decode, Q4_K_M, both cards) the gap goes from 43 % to 20 %.
Verified
tests/test_qwen36_dn_gpu.c(fake backend, inmake check): one in-memory DeltaNet layer driven throughdeltanet(): GPU outputs equal the CPU outputs; the host copy is left alone while the card advances and reads back the CPU path's state; a two-row CPU prefill after GPU tokens pulls first and continues, the GPU token after it pushes first; a failing step turns the layer off and the CPU continues from the card's state.tests/test_qwen36_tier_autoplace.cgains the two placer cases (dnout follows dnproj;experts=allkeeps both cards).colibri(the loader host) builds; the loader ABI tripwire counts the five optional symbols.--pinsnapshots and prefix reuse underQ36_DN_GPU=1(the code paths only read host arrays, pulled first, and the fake test covers the pull/push order; a serve smoke on the container is what I would run next if you want it before merging).Where this goes next
c/delta_attention.hsays it: the same gated delta step runs the linear-attention layers of Kimi K3 (KDA), GLM-5.3-Flash and Qwen3.8 as well; only the dimensions and the gate differ, and the gate belongs to the engine. The backend object here takes those dimensions as parameters (vh <= 64, kdim/vdim <= 256), so the other engines can hand their layers to the same five calls. I kept this PR to qwen36 so the numbers are the engine's own; happy to follow with qwen38 (48 value heads) if you want the backend step to be the shared one, or to move it behinddelta_attention.h's naming if you would rather it mirrorcoli_kda_stepexactly.Thanks to Claude Code for the help along the way.