feat(qwen36): CUDA VRAM expert tier — heat-based placement across GPUs via the shared CUDA backend - #713
Conversation
4e5ed05 to
1c53042
Compare
|
Rebased onto dev with #762 merged — the async grouped-int4 dispatch this tier rides on — and onto the current #712 branch (serve + gs64), extended for gs64 containers: Verified on a single RTX 3070: output bit-identical to the CPU run on the gs64 container (greedy, 200 tokens), 11.94 tok/s with 3731/10240 experts resident (6.15 GB VRAM), and the serve path rides the tier unchanged (tier init runs before Staying draft until #712 lands; after that this is one commit on top of it. |
|
Same situation as #712 — see my note there. #165 has landed on Your CI had never run either — it was stuck in |
4f276c8 to
6848687
Compare
6848687 to
509d185
Compare
- make qwen36 CUDA=1 no longer compiles -DCOLI_CUDA and links -lcudart for a file that contains no CUDA. NOCUDA_CFLAGS, same shape as olmoe; verified the binary no longer links cudart in either build mode. (JustVugg#713 adds real CUDA sources and takes the flags back.) - serve_one now resets first_step, seen and momentum_logits. They are per-REQUEST state that was only ever initialised per process, so COLIBRI_RESIDENT=1 collected on the server's first request and never again, and the router EMA carried one conversation's history into the next. - --ram is documented as unsupported rather than silently ignored: this engine requires full RAM residency for the expert set, so there is no budget to size. A flag that looks like it works and does not is worse than one that says so. No regression: fixture still 16/16, context gate green.
Review list C: g4/u4/d4 were malloc'd and filled on every int4 expert load and read nowhere in this PR -- +50% expert-cache RSS on the recommended gs64 container, for nothing. They are the upload source for the CUDA expert tier and come back with it in JustVugg#713, which is where qt_note() and stage() actually read them. I had reported this as done in the summary table before doing it. It was not; the buffers were still allocated. Corrected here and in a follow-up comment. Verified: the real gs64 container still loads and unpacks int4 to int8 for the CPU path, the fixture still matches its oracle 16/16 at cap 1 and 8, context gate green.
- docs/qwen36-cuda-tier.md is JustVugg#713's file and does not exist in this PR; the link pointed at nothing (review list D). Now points at the PR and says so. - the engine is no longer selected by 'model_type contains qwen'. Since fe3879c it is an exact match against the family registry (qwen3_5_moe / qwen3_5_moe_text), which is what stops this engine claiming qwen2_moe, qwen3_moe and every Qwen finetune.
…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.
509d185 to
34deee3
Compare
|
Rebased onto the #712 merge and out of draft — head Thanks for merging #712. Three things followed from that merge, plus one I did not expect. 1. The Makefile rule — neither side was right on its own#712 moved So the flags now follow the same switch the sources already follow: ifeq ($(CUDA),1)
QWEN36_TIER_SRC = qwen36_tier.c
QWEN36_CFLAGS = $(CFLAGS)
QWEN36_LDFLAGS = $(LDFLAGS)
else
QWEN36_TIER_SRC =
QWEN36_CFLAGS = $(NOCUDA_CFLAGS)
QWEN36_LDFLAGS = $(NOCUDA_LDFLAGS)
endif
2.
|
make qwen36 |
159,032 B, no CUDA symbols |
make qwen36 CUDA=1 |
1,044,120 B, qt_init + cudaMalloc present |
What is not verified here
I did not run the tier against the real 35B container in this round — the machine's baseline service is back up and the two must not run concurrently. The end-to-end numbers stand from the earlier runs (bit-identical to CPU at 11.94 tok/s on the 3070, comment 5171528999), and @sehHeiden independently built and ran this branch in #1040. What is new and verified today is that it rebases clean, builds both ways, and passes the full matrix including the ARM job your #1083 added.
Container for reproducing: qwen3.6-colibri int4-gs64, same source as linked in #712.
|
Follow-up to my previous comment: the machine freed up, so I ran the two things I had listed as not verified. Both are now measured, on head 1. Tier vs CPU parity on the full model
Output is bit-identical ( 2. The
|
| CPU-only run, identical prompt | Peak RSS |
|---|---|
| without the gate (old behaviour) | 26.19 GB |
| with the gate | 18.86 GB |
7.33 GB, a 39 % surcharge for buffers a CPU-only build never reads. Output identical, speed identical (0.32 vs 0.33 tok/s, noise). I verified the two binaries actually differ before trusting the result — with the gate folding to a constant under CUDA=0 it was worth checking that the compiler had not made the experiment vacuous.
Also green locally
tests/test_qwen36_ctx (14 checks), tests/test_makefile_cuda_scope.py (5), tests/test_family_registry.py (12) — alongside the 22/22 in CI.
Still not claimed
Long-context behaviour of the tier is unchanged from the earlier measurements and is not re-measured here; the second GPU's advantage shrinks with context depth (1.20× at 4k → 1.03× at 128k in my fork experiments), because the CPU-side KV scan dominates. That is a property of the engine, not of this PR, and I am not proposing anything about it here.
|
Running the two unverified items after merge, unprompted, with a |
Depends on: #712 (qwen36 engine) — this branch includes those commits; only the tier commit is new here.
Summary
Applies colibri's placement concept — route → place → overlap → learn — to
Qwen3.6 one level up from the GLM disk tier: RAM → VRAM. All experts stay in RAM; routing heat promotes the hot ones into DEVICE_LOCAL VRAM across one or more GPUs, computed through the existing
backend_cuda.cuexpert-group API (no new backend, unlike the Vulkan approach in #602).
CUDA=1compiles the tierTry it
Pre-converted self-contained int4 container (all numbers above were measured
with it): https://huggingface.co/Kreuzzelg/qwen36-35b-a3b-colibri-i4
Limitations
Thanks on Claude Code