Qwen3.6-35B-A3B is a fine-grained Mixture-of-Experts model: 40 layers, 256 routed experts per layer, top-8 routing. Quantized to NVFP4 it is ~21 GB of weights — and on a 24 GB card it fails to allocate a usable KV cache by a small, fixed margin:
| Serve config | Available KV |
|---|---|
| 32K ctx, util 0.92 | −1.7 GiB |
| 16K ctx, util 0.94, vision off | −0.09 GiB |
| 8K ctx, util 0.94 | −0.23 GiB |
The deficit is context-insensitive — it's a fixed weight overflow, not a KV problem. The model is simply ~1.5 GB too big.
In a fine-grained MoE, only top-k experts fire per token — the routed-expert pool is the most redundant parameter class in the model. So instead of quantizing harder or offloading, we remove whole experts: keep the most important ones, drop the rest.
One shard-streamed CPU pass over the checkpoint. Per MoE layer, score every expert with a calibration-free importance:
Ie = ‖router_rowe‖ × mean(weight_scalee)
— router propensity × quantized-weight energy.Take the top-K experts, renumber them contiguously,
and slice the router (mlp.gate) in the same order — routing
semantics preserved exactly.
The MTP speculative-decoding head has its own experts and router — sliced to the same K so speculative decoding keeps working on the carve.
Attention, embeddings, the shared expert and all norms are copied verbatim. Only routed experts are cut.
No fine-tuning, no calibration forward passes, no distillation. ~40 minutes of CPU, then it serves.
Footprint and headroom — measured on a single RTX 5090 laptop (24 GB), vLLM compiled:
| Rung | Experts | Weights (VRAM) | Max ctx |
|---|---|---|---|
| 28B | 192 / 256 | 19.8 GiB | 8K |
| 24B | 160 / 256 | 17.3 GiB | 16K |
| 20B | 128 / 256 | 14.9 GiB | 32K |
All figures measured 2026-07-15 on the v2 MTP-aware carves, single RTX 5090 (24 GB), Blackwell sm120, vLLM compiled. Throughput in tok/s; TTFT = time to first token.
| Rung | Single | 4-way | 8-way | 16-way |
|---|---|---|---|---|
| 28B | 169 | 271 | 370 | 473 |
| 24B | 164 | 267 | 369 | 470 |
| 20B | 169 | 272 | 383 | 489 |
Short 256-token generations undersell aggregate throughput ~40% vs long gen — prefill isn't amortized. See profile 2 for the 1024-token picture.
| Rung | Single decode (TTFT) | 8-way | 16-way |
|---|---|---|---|
| 28B | 168 t/s (288 ms) | 574 | 805 |
| 24B | 163 t/s (289 ms) | 558 | 783 |
| 20B | 170 t/s (282 ms) | 568 | 796 |
| Rung | Prompt @ ctx | Single decode (TTFT) | 4-way |
|---|---|---|---|
| 28B | ~5.5K @ 8K | 167 t/s (345 ms) | 391 |
| 24B | ~13K @ 16K | 158 t/s (965 ms) | 328 |
| 20B | ~29K @ 32K | 158 t/s (2.6 s) | 252 |
| Rung | 4096-tok decode — single (TTFT) | 4-way | Near-ctx-limit decode — single (TTFT) | 2-way |
|---|---|---|---|---|
| 28B | 169 t/s (70 ms) | 431 | 168 t/s (84 ms) @7.5K | 260 |
| 24B | 165 t/s (70 ms) | 423 | 162 t/s (76 ms) @15.5K | 252 |
| 20B | 169 t/s (74 ms) | 438 | 162 t/s (72 ms) @31.5K | 251 |
| Rung | Prefill | Sustained gen | Peak KV |
|---|---|---|---|
| 28B | 3,319 | 537 t/s | ~31% |
| 24B | 4,764 | 545 t/s | — |
| 20B | 6,451 | 584 t/s | — |
The rungs are NVFP4 checkpoints — load one straight from the Hub with
transformers and generate:
from transformers import AutoModelForCausalLM, AutoTokenizer
model_id = "kshitijthakkar/Kirigami-Qwen3.6-24B-A3B-NVFP4" # or 28B / 20B
tok = AutoTokenizer.from_pretrained(model_id)
model = AutoModelForCausalLM.from_pretrained(model_id, device_map="cuda", dtype="auto")
msgs = [{"role": "user", "content": "Summarize this incident trace."}]
ids = tok.apply_chat_template(msgs, add_generation_prompt=True, return_tensors="pt").to(model.device)
out = model.generate(ids, max_new_tokens=256)
print(tok.decode(out[0][ids.shape[1]:], skip_special_tokens=True))
NVFP4 needs a Blackwell GPU (RTX 5090 / RTX Pro 6000 / B200) for native FP4 — the ~15–20 GB of weights fit a single 24 GB card. For production serving, use the vLLM recipe below.
Or, once a rung is up under vLLM (see 06), talk to it over the OpenAI-compatible API —
same client you already use for GPT/Claude, just repoint base_url:
from openai import OpenAI
client = OpenAI(base_url="http://localhost:8000/v1", api_key="EMPTY")
r = client.chat.completions.create(
model="kirigami-28b", # = --served-model-name
messages=[{"role": "user", "content": "Summarize: API p99 jumped 40x after the 14:02 deploy."}],
max_tokens=512, temperature=0.3,
)
print(r.choices[0].message.content)
Or straight curl — chat, plus streaming with one extra field:
curl http://localhost:8000/v1/chat/completions -H "Content-Type: application/json" -d '{
"model": "kirigami-28b",
"messages": [{"role":"user","content":"Explain this incident timeline."}],
"max_tokens": 512, "temperature": 0.3
}'
# streaming: add "stream": true
Tip: for direct answers (skip the thinking preamble) pass
"chat_template_kwargs": {"enable_thinking": false} in the request body.
Each rung fits a single 24 GB card. Pick a rung, then serve it under vLLM — the recommended per-rung config, then the full copy-paste command.
| Rung | Model repo | --served-model-name | --max-model-len | --max-num-seqs |
|---|---|---|---|---|
| 28B | kshitijthakkar/Kirigami-Qwen3.6-28B-A3B-NVFP4 | kirigami-28b | 8192 | 16 |
| 24B | kshitijthakkar/Kirigami-Qwen3.6-24B-A3B-NVFP4 | kirigami-24b | 16384 | 16 |
| 20B | kshitijthakkar/Kirigami-Qwen3.6-20B-A3B-NVFP4 | kirigami-20b | 32768 | 16 |
Full serve command (28B shown — swap repo / name / ctx per the table above):
vllm serve kshitijthakkar/Kirigami-Qwen3.6-28B-A3B-NVFP4 \
--served-model-name kirigami-28b \
--kv-cache-dtype fp8 \
--max-model-len 8192 \
--gpu-memory-utilization 0.94 \
--max-num-seqs 16 \
--limit-mm-per-prompt '{"image":0,"video":0}' \
--speculative-config '{"method":"qwen3_5_mtp","num_speculative_tokens":4}' \
--port 8000
moe_backend=triton — NVFP4 only accepts cutlass / flashinfer; let auto pick.--gpu-memory-utilization 0.94 is the safe ceiling (0.95 fails startup). --limit-mm-per-prompt '{"image":0,"video":0}' disables the vision tower and reclaims ~1.6 GB.torch.compile (~25 min); a persistent compile cache makes repeat boots ~4 min. Allow ~3–4 min warmup before sending requests.--speculative-config) is optional and adds +41–54% decode via speculative decoding (99.5% draft acceptance).--max-num-seqs to 8 or 4.