fix(cuda): dispatch grouped int4 (fmt=4) in the async expert-group path - #762
Conversation
|
Sharing the measurements behind this fix, so the numbers are in one place. All on the same box (Threadripper PRO 3945WX, 12C, 94 GB RAM, RTX 3070 8 GB + Quadro RTX 4000 8 GB), same prompt, 200-token decode. colibri = qwen36 engine + CUDA VRAM expert tier (#713 / fork branch
Takeaways:
Thanks on Claude Fable 5 :-) |
|
Rebase request β and this is the one I most want landed of anything currently open.
That hits today, on the container the README actively recommends. GLM-5.2 gs64 through the CUDA async expert-group path produces wrong numbers with nothing to indicate it. The sync path has been correct since #334, so the failure is invisible to anyone comparing against a sync run β and it is exactly the shape we spent this week finding elsewhere: a fallback that silently does the wrong arithmetic instead of refusing.
If the conflict turns out to be in the CUDA backend rather than mechanical, say so and I will look at it with you β for a silent-wrong-output bug I would rather do the work than let it queue. One question while you are in there, and it may already be answered by your patch: does the same gap exist for fmt=5 (int3-g64) and fmt=8 (fp8-e4m3, block scales) on that async path? Both are group- or block-scaled and would be wrong in the same way under a Separately: #712 is still waiting on the front-end wiring β |
coli_cuda_expert_group_issue fell back to per-expert quant_matmul with gs=0, ng=1 for fmt=4 members, silently applying a single per-row scale to a grouped container -> wrong output on the async decode path that MoE decode (and VRAM expert tiers) ride. Route all-q4 groups through the JustVugg#334 kernels exactly like the sync path does; silu is fused in the dual kernel's epilogue, so no separate silu_mul launch. Also repair the grouped-g4 oracle, stale since a03c79e fused silu into the dual hidden kernels (gate[] now holds silu(g)*u and up[] is never written), and extend it with a public-API phase: upload_g + sync coli_cuda_expert_group vs async issue/take, which must match bit for bit. The API phase fails without this fix (776 mismatches) and passes with it.
The sync generic branch refused fmt=4 by name; the async fallback did not refuse anything and ran quant_matmul with gs=0,ng=1 -- per-row scale semantics -- for whatever arrived. Today fmt=5 (int3-g64) and fmt=8 (fp8-e4m3) cannot reach either path because qt_cuda_upload refuses them at the gate (and row_bytes()==0 fails the upload for unknown formats), but the dispatch shape was a whitelist: the next group- or block-scaled format to gain CUDA tensors would inherit the silent mis-scale instead of a refusal. Refuse fmt>3 in both fallbacks. fmt=6 cannot reach them (any_e8 gates above); refusing it here too is harmless defense in depth. A refusal returns 0, so the caller falls back to the CPU path: slower, never wrong.
17bf8b7 to
d9ef4ca
Compare
|
Rebased onto current On fmt=5 / fmt=8: today neither can reach the async path (or the sync one), because they never become CUDA tensors at all β But you're right about the shape. The async fallback refused nothing: anything that did arrive would run |
β¦s via the shared CUDA backend
Promote hot experts into DEVICE_LOCAL VRAM (one home device per expert,
eid % n_gpus) and compute them through the existing backend_cuda.cu
expert-group API; no new backend. Routing heat + tier.h LFRU semantics decide
placement, a parallel warmstart fills the budget before the first token
(persisted across runs via HEAT_FILE), uploads run on a background thread,
and VRAM misses fall back to the overlapped CPU int8 path β placement never
changes routing or precision.
- c/qwen36_tier.{c,h}: tier implementation; inline stubs keep the default
build CPU-only, CUDA=1 compiles the tier against the shared backend.
- c/qwen36.c: moe() integration (issue -> CPU misses + shared expert
overlapped -> take), warmstart that loads all experts to RAM and frees the
int8 copies of VRAM residents (rematerialized from packed int4 on
eviction), tier telemetry.
- docs/qwen36-cuda-tier.md: design + measured results.
Measured (12-core Zen2, RTX 3070 + Quadro RTX 4000, 35B int4, 200 tokens):
11.3 tok/s decode with two 8 GB GPUs (100% VRAM hit warm, 29 GB RSS),
Rebased onto dev with JustVugg#762 merged (the async grouped-int4 dispatch this
tier rides on) and extended for gs64 containers: qt_init takes expert_gs,
experts upload as fmt=4 with grouped scales via coli_cuda_tensor_upload_g
when expert_gs>0, CPU-miss fallbacks dispatch through matmul_qe.
Summary
coli_cuda_expert_group_issueβ the async decode path that MoE decode rides (colibri.cissues it per device each layer) β never dispatches grouped int4. Groups containing an fmt=4 member fall through to the per-expertquant_matmul(..., gs=0, ng=1)fallback, which applies a single per-row scale to a per-group-scaled container: silently wrong output, no error. The sync path (coli_cuda_expert_group) has handled this correctly since #334; the gap is only in the async split. This is the CUDA counterpart of the fmt=4 decode work tracked for Metal in #585/#587.Affected today: any grouped-int4 (gs) container decoding through the CUDA backend β e.g. GLM gs64 β whenever the async expert-group path is taken.
The fix
Route all-int4 groups with a grouped member through the existing #334 kernels, exactly like the sync path does:
grouped_hidden_g4_dual+grouped_down_g4, one launch pair per group;silu_mullaunch βgate[]already holdssilu(g)*uandup[]is never written;ng=1special case, per-row-only groups keep their existingw4branch, odd-gs groups keep falling through (and the generic path still refuses to decode fmt=4 as int2, fmt=4 CUDA attention crash β FIXED (#298); benchmark tracker + follow-ups (PROFILE accounting, cache warmth)Β #334).Net change in
backend_cuda.cu: one dispatch branch in the async function, mirroring the sync path.Test: repaired oracle + new API phase
tests/test_grouped_g4_cuda.cuhad gone stale: it was written against the pre-fusion kernels and still checkedgate[]against the raw matmul and readup[]. On currentdevit fails with 14,400 mismatches β it seems no CI runsit (needs a GPU). This PR:
silu(g)*u,up[]unchecked), andcoli_cuda_tensor_upload_g+ synccoli_cuda_expert_groupvs asyncissue/takeon mixed fmt=4/fmt=2 groups with mixed row counts. The async result must match the sync result bit for bit, and both must match the f64 CPU oracle.The API phase fails on
devwithout the fix (776 mismatches) and passes with it β it guards exactly this regression.Validation
devHardware: RTX 3070 (sm_86) + Quadro RTX 4000 (sm_75), CUDA 12.x, Linux.
Notes
coli_cuda_expert_group_resident_issueis untouched: it cleanly rejectsfmt=4 (
if(!all_s4) return 0) and callers fall back, so there is no wrongoutput there β extending it to the g4 kernels is a natural follow-up if
wanted (same pattern, silu already fused).
expert tier proposed in feat(qwen36): CUDA VRAM expert tier β heat-based placement across GPUs via the shared CUDA backendΒ #713 (the tier only uploads per-row today, precisely
because of this gap).
Thanks on Claude Fable 5 :-)