KIRIGAMI

Zero-Shot Experts
How we carved a 35B Mixture-of-Experts model to fit a single 24 GB GPU — no training, no calibration, one CPU pass.
01 · The problem

A 35B MoE that won't fit

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 configAvailable 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.

02 · Why carve

The expert pool is the redundant part

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.

Kirigami (切り紙) — the Japanese art of cutting paper to create structure. Here, cutting experts to fit the card.
03 · The method

Importance-ranked expert keep — zero training

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.

Keep & renumber

Take the top-K experts, renumber them contiguously, and slice the router (mlp.gate) in the same order — routing semantics preserved exactly.

Slice the draft head

The MTP speculative-decoding head has its own experts and router — sliced to the same K so speculative decoding keeps working on the carve.

Untouched

Attention, embeddings, the shared expert and all norms are copied verbatim. Only routed experts are cut.

No training

No fine-tuning, no calibration forward passes, no distillation. ~40 minutes of CPU, then it serves.

04 · The results

Three rungs that fit — and fly

28B
192 / 256 experts
8K context
24B
160 / 256 experts
16K context
20B
128 / 256 experts
32K-class context

Footprint and headroom — measured on a single RTX 5090 laptop (24 GB), vLLM compiled:

RungExpertsWeights (VRAM)Max ctx
28B192 / 25619.8 GiB8K
24B160 / 25617.3 GiB16K
20B128 / 25614.9 GiB32K

Benchmarks — every workload we measured

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.

1 · Concurrency ladder (short prompt / short gen — aggregate tok/s)
RungSingle4-way8-way16-way
28B169271370473
24B164267369470
20B169272383489

Short 256-token generations undersell aggregate throughput ~40% vs long gen — prefill isn't amortized. See profile 2 for the 1024-token picture.

2 · Standard RAG (~4.4K-token prompt, 1024-token generation)
RungSingle decode (TTFT)8-way16-way
28B168 t/s (288 ms)574805
24B163 t/s (289 ms)558783
20B170 t/s (282 ms)568796
3 · Near-max-context (long prompt near the rung's ctx limit, 2048-token gen)
RungPrompt @ ctxSingle decode (TTFT)4-way
28B~5.5K @ 8K167 t/s (345 ms)391
24B~13K @ 16K158 t/s (965 ms)328
20B~29K @ 32K158 t/s (2.6 s)252
4 · Long-decode / thinking-mode (short ~300-token prompt, very long generation)
Rung 4096-tok decode — single (TTFT)4-way Near-ctx-limit decode — single (TTFT)2-way
28B169 t/s (70 ms)431168 t/s (84 ms) @7.5K260
24B165 t/s (70 ms)423162 t/s (76 ms) @15.5K252
20B169 t/s (74 ms)438162 t/s (72 ms) @31.5K251
5 · Sustained load (~12-way concurrent long prompts — server-side sustained rates)
RungPrefillSustained genPeak KV
28B3,319537 t/s~31%
24B4,764545 t/s
20B6,451584 t/s
What this shows (measured, throughput-only)
  • Single-stream decode holds ~160–170 tok/s across every scenario and context depth — the hybrid linear attention (GatedDeltaNet state) keeps decode flat as context grows.
  • TTFT scales with prompt length only: ~70 ms @300 tok → ~285 ms @4.4K → 345 ms / 965 ms / 2.6 s at near-max ctx (prefill ≈ 11K tok/s).
  • Aggregate throughput scales with generation length: the short-gen ladder tops ~470–490 @16-way, but 1024-token generations reach ~800 @16-way (prefill amortization; 256-token benches undersell ~40%).
  • Carving deeper (28B → 20B) buys context + concurrency headroom, not single-stream speed — active params (top-8) are constant across rungs.
  • MTP speculative decoding adds +41–54% decode on top (99.5% draft acceptance).
05 · Run it locally

Quick local inference

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.

06 · Serve it yourself

Run a rung on your own GPU

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.

RungModel repo--served-model-name--max-model-len--max-num-seqs
28Bkshitijthakkar/Kirigami-Qwen3.6-28B-A3B-NVFP4kirigami-28b819216
24Bkshitijthakkar/Kirigami-Qwen3.6-24B-A3B-NVFP4kirigami-24b1638416
20Bkshitijthakkar/Kirigami-Qwen3.6-20B-A3B-NVFP4kirigami-20b3276816

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