Skip to content

qwen36: the chain's resident experts on its own stream (COLI_CUDA_CHAIN_EXPERTS) - #2147

Closed
kreuzzelg wants to merge 2 commits into
JustVugg:devfrom
kreuzzelg:qwen36-tier-group
Closed

kreuzzelg wants to merge 2 commits into
JustVugg:devfrom
kreuzzelg:qwen36-tier-group

Conversation

@kreuzzelg

Copy link
Copy Markdown
Contributor

The follow-up #1992 named as its first lever: the routed experts were 9.9 of the chain's 18.7 ms a token, and @m4t5k4 saw 9.3 ms of them with every expert resident. The tier's host path (qt_issue / qt_take) was built for an engine whose residual lives on the host: the row into pinned memory, one 256-thread block per output row per expert, the result back, a wait -- every layer. With the chain the row is already on the device.

What it is

The route. The chain asks the tier for the residents' tensors (qt_resident_open: the hits and their heat counted as before, the group held open against eviction and in-place swaps until the frame they ran in has been waited for; qt_resident_close) and runs them itself through a new op of the chain's table, experts: two launches per row on the chain's stream, right behind the shared expert -- a warp per output row, 16 bytes a lane, silu(gate x) * up x for every resident into a scratch, then every output row summed over the residents with the router's weights, the way gemv_i8_rows reads the trunk. int8, int4 per row and int4 grouped (gs a multiple of 32, so a 16-byte load never straddles a group) are taken, gate/up and down may differ (--down-bits). The CPU's share of the row is computed meanwhile as before and comes up through the pinned buffer; the combine adds the device's sum and the host's first. Nothing crosses the bus for the residents and the layer has no wait of its own: the next layer's A1 wait covers it.

Where it stops. One device only -- with the tier on two cards the residents of the other card would have to cross, so the host path stays (qt_resident_capable). A format the kernels decline launches nothing, does not lose the device, and turns the route off from that layer on (the message names the layer). COLI_CUDA_CHAIN_EXPERTS=0 keeps the host path for an A/B. The descriptors travel as a by-value kernel parameter (ptxas: 0 bytes stack, 2.9 KB cmem): no staging copy that could outlive its host buffer, no stream sync for a pageable source. No new export: the table grew, the symbol did not; an older DLL's shorter table is refused by the size check as before.

Measured (RTX 3070 alone, Qwen3.6-35B-A3B int4, COLI_PLACE=auto, the tier at 84 % hits, the usual 49-token prompt, 200 new tokens, two runs each)

TTFT tok/s, the run ms per decode token decode tok/s device wait / token host's experts / token (issue / CPU misses / take)
COLI_CUDA_CHAIN=1, residents through the host path 1.05 s 41.6 18.9 53 6.1 ms 9.8 (0.8 / 4.6 / 4.1)
COLI_CUDA_CHAIN=1, residents on the chain's stream 0.81-0.82 s 48.6-48.9 16.4-16.6 61 7.8 ms 5.5 (0.4 / 4.8 / 0)
Ollama 0.40.3 on the same card 26.0 38.4

-13 % a token and a quarter off the first token (the prompt's rows take the route too). What is left of the host's part is the CPU's misses (16 % of the experts, 4.8 ms, overlapping the device's frame); the device's wait grew by the residents' kernels (435 MB of expert rows a token in about 1.7 ms). The text opens as the host path's and diverges after about 150 characters, as two orders of the K summands do.

Verified

Thanks to Claude Code for the help along the way.

kreuzzelg and others added 2 commits October 17, 2026 17:28
…IN_EXPERTS)

The tier's host path (qt_issue / qt_take) was built for an engine whose
residual lives on the host: the row into pinned memory, one 256-thread
block per output row per expert, the result back, a wait -- every layer,
9.9 ms of a chain token's 18.7. With the chain the row is already on the
device, so the chain asks the tier for the residents' tensors
(qt_resident_open: hits and heat counted, the group held open against
eviction and in-place swaps until the frame they ran in has been waited
for; qt_resident_close) and runs them itself through a new op of the
chain's table, `experts`: two launches per row on the chain's stream,
behind the shared expert -- a warp per output row, 16 bytes a lane,
silu(gate x) * up x for every resident into a scratch, then every output
row summed over the residents with the router's weights, the way
gemv_i8_rows reads the trunk. int8, int4 per row and int4 grouped with gs
a multiple of 32 are taken, gate/up and down may differ (--down-bits); a
refusal launches nothing and does not lose the device, the route turns
itself off and the layer goes the host path. The CPU's share of the row
is computed meanwhile as before and comes up through the pinned buffer;
the combine adds the device's sum and the host's first. One device only:
with the tier on two cards the host path stays.

The descriptors travel as a by-value kernel parameter (ptxas: 0 bytes
stack, 2.9 KB cmem), so no staging copy can outlive its host buffer and
no pageable source forces a stream sync.

tests/test_qwen36_cuda_chain.c runs the route through the fake's
`experts` (the kernels' arithmetic on the fake's int8 copies; the slots
offered to the tier, QT_UPLOAD_SYNC=1) against the CPU path, and
COLI_CUDA_CHAIN_EXPERTS=0. tests/test_cuda_chain.cu checks the op on a
card: per-row int4, grouped gate/up over an int8 down, one expert alone,
a gs-16 refusal that leaves the device to the chain, the counter.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…y nibbles

The experts case of tests/test_cuda_chain.cu packed two's-complement
nibbles; the host's int4 is offset-binary (value + 8) and the upload
turns it signed (offset_to_signed_s4), so the device read every weight
shifted. With the container's packing the three cases match the
reference to 1e-4 on the 3070. qwen36-cuda-tier.md gets the measured
rows: 18.9 -> 16.4 ms a decode token, TTFT 1.05 -> 0.81 s.

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

Copy link
Copy Markdown
Owner

Thanks for this. It no longer merges into dev: the conflicts are in c/backend_cuda.cu and c/cuda_chain.cu (dev changed the CUDA chain since, with the speculative verify rows in #2251 and the GGUF blocks in #2252). Could you rebase it on the current dev? Once it merges I will review it and put it in the next batch.

@kreuzzelg

kreuzzelg commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor Author

@JustVugg Thanks for the look. Before I rebase: dev has had COLI_CUDA_CHAIN_MOE since 7d1e3cc, which does what this PR does and more (the routing in the frame too, the host's share signalled and awaited, a prompt chunk sorted by expert on the tensor cores). Two paths for the residents would be one too many, so I measured dev as it is on the RTX 3070 against this branch, same container (Qwen3.6-35B-A3B int4), same 49-token prompt, 200 new tokens, COLI_PLACE=auto COLI_TRUNK_PROBE=0 CUDA_EXPERT_GB=auto (the tier at 84 % hits), two runs each:

TTFT ms per decode token decode tok/s
this branch (COLI_CUDA_CHAIN_EXPERTS) 0.81 s 16.4 61
dev f76edb2, COLI_CUDA_CHAIN_MOE=0 0.35-0.37 s 16.0-17.2 58-63
dev f76edb2, the default 0.35 s hangs at token ~145 (see below) --
dev f76edb2, the default, with the fix in #2296 0.35-0.36 s 14.5-14.7 68-69

dev's path is faster here (and its first token 0.35 s against 0.82), so I am closing this one. One thing came out of the measurement: with a layer's experts partly on the host, the default hung at the second growth of the decode attention's scratch (cc_attn synchronizes the stream inside the frame while the layer before's cc_wait_host still waits for the host) -- the fix is in #2296, with the numbers. What this branch had beyond dev and may be worth a small PR on its own: gate/up and down in different formats (--down-bits containers, #1559) take the frame; dev's qt_chain_format sends such a tier back to the host's step. The CUDA graphs for the decode frames (16.4 -> 16.0 ms here, the token device-bound) will come as a PR on dev's frame structure.

Thanks to Claude Code for the help along the way.

@kreuzzelg kreuzzelg closed this Oct 22, 2026
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