howtospark
Recipes
Moonshot logo

1 × Spark96K ctx29.4 tok/s@ 2KvLLMthinking offvanilla

Moonshot's Kimi Linear 48B-A3B (49B MoE, ~2.7B active) in BF16 on one DGX Spark, at a 98,304-token context — 29.4 tok/s single-stream decode, with the KV cache costing only 8.2 KB per token.

spark .2 · single nodepeer
15 GiB free
Expert planes87.7* GiBBF16
114 usable
99* / 114 GiB
87% of usable
Expert planesDense weightsKV cacheActivations + graphsOS reserve

* segment sizes marked with an asterisk are estimates pending a measured run

Eval scores

(compare all)

Bench card

No measured runs yet — be the first: install the spark-benchmark skill.

Overview

Kimi Linear is the rare model where the usual Spark tradeoff runs backwards. Normally the weights fit easily and you fight for KV cache; here the weights are 91.53 GiB of a 114 GiB usable pool and the KV cache is almost free. That inversion comes from the architecture: 27 layers, of which 20 are Kimi Delta Attention (a gated linear attention whose state is fixed-size per sequence) and 7 are MLA with a 512-wide compressed latent. A token of context costs 8,064 bytes across the whole model, so a 5 GiB pool holds 656,759 tokens — measured, not modeled. There is no fight for context to be had, which is why this recipe pins the KV pool small and hands the rest back rather than tuning it. What you get for the 92 GB download is a 49B-parameter model that decodes like a 3B one. Only ~2.7B parameters are active per token (top-8 of 256 routed experts plus a shared expert), so the per-token read is about 5.4 GB, which puts a hard bandwidth ceiling near 51 tok/s on GB10's ~273 GB/s. Measured single-stream decode is 29.4 tok/s at a 2,048-token prompt, or 58% of that ceiling, and it barely fades as the prompt grows: 29.5 / 29.4 / 29.2 tok/s at 512 / 2,048 / 8,192 tokens, then 28.5 at 32,768 and 26.3 at 98,304. (The first three come from a serve at --max-model-len 524288 with a 5 GiB KV pin; the last two from the config this page ships. Both are on the same node, same engine, same day.) An 11% decode decay across a 192x increase in prompt length is the linear-attention promise showing up in a measurement. What does grow is TTFT, and steeply: 0.7 s at 2,048 tokens, 8.9 s at 32,768, 37.8 s at 98,304. On this model long context costs you the wait before the first token, not the speed after it. The honest limitation is the one this page is careful about. The vendor advertises a 1,048,576-token window, and the memory for it genuinely exists — at 8,064 bytes/token even a full million tokens needs under 8 GiB of KV — but the prefill path does not survive prompts that long. We bracketed it rather than guessing: with no chunk bound, a 131,072-token prompt killed the engine about 45,000 tokens in, with the KV pool only 6.8% full. Adding --max-num-batched-tokens 4096 moved the ceiling a long way up — a 98,304-token prompt then completed normally — but a 130,000-token prompt still killed it. So 98,304 is what this recipe claims, because 98,304 is what we sent and got an answer to. vLLM 0.26.0 is worse: it will not start this model at all, dying without a traceback immediately after torch.compile whether or not the KV pool is pinned. If you need the full million-token window on a Spark today, this is not yet the configuration that delivers it. One thing this model does not offer is a speculative decoder. config.json sets num_nextn_predict_layers: 0, there is no MTP head in the checkpoint and no published EAGLE/DFlash draft for it, so the single biggest decode lever on the Spark is unavailable — 29.4 tok/s is what this model does. Note also that spec decode on a KDA linear-attention model is not merely absent but architecturally awkward: the recurrent state has to be partitioned into speculative and non-speculative slots, which is engine work rather than a flag.

  • 1 x DGX Spark (GB10, sm_121) — no tensor parallelism, no second node
  • Weights are a measured 91.53 GiB of the 114 GiB usable pool — the tightest single-Spark fit we serve
  • KV costs 8,064 bytes/token across the whole model: 20 of 27 layers are linear-attention and hold no per-token cache
  • Measured 131,352 KV tokens per GiB — a 5 GiB pin held a 656,759-token pool
  • Measured 29.4 tok/s single-stream decode at a 2,048-token prompt
  • Decode barely fades with context: 29.5 / 29.4 / 29.2 / 28.5 / 26.3 tok/s at 512 / 2,048 / 8,192 / 32,768 / 98,304-token prompts
  • ~2.7B active parameters of 49B — a ~5.4 GB per-token read, i.e. a ~51 tok/s bandwidth ceiling on GB10
  • TTFT is the real long-context cost: 0.7 s at 2,048 tokens, 8.9 s at 32,768, 37.8 s at 98,304
  • No speculative decode is possible: num_nextn_predict_layers is 0 and no draft model exists
  • The advertised 1,048,576-token window does NOT serve — a 130,000-token prompt crashes the engine mid-prefill
  • --max-num-batched-tokens 4096 is load-bearing: it is what got a 98,304-token prompt through at all
  • vLLM 0.26.0 cannot start this model — it dies without a traceback right after torch.compile
  • Verified 2026-07-27 on vLLM 0.24.0

Software requirements

  • 1 x DGX Spark (GB10, sm_121), NVIDIA driver >= 580 (CUDA-13 capable)
  • vLLM 0.24.0 with torch 2.11.0+cu130. Do NOT use 0.26.0 — see troubleshooting
  • The `hf` CLI to fetch the checkpoint (~92 GB, 20 shards)
  • No patches: KimiLinearForCausalLM is natively registered in vLLM, so --trust-remote-code is only needed for the tokenizer and config classes
  • Last verified 2026-07-27 on vLLM 0.24.0: weights 91.53 GiB, 2 GiB KV pin -> 253,166-token pool (1.93x at 131,072), decode 29.5 / 29.4 / 29.2 tok/s at 512 / 2,048 / 8,192-token prompts, and a 98,304-token prompt served end to end at 26.3 tok/s

Quick start

  1. 1

    Free the node first

    The weights alone are 91.53 GiB of a 121 GiB pool, so there is no room to share this node with anything. Wait for the process list to actually go empty — vLLM's startup check compares free memory against your utilization target, and a teardown still in flight will fail the new boot.

    pgrep -af 'vllm serve|ray::' || echo 'node is clear'
    ray stop --force 2>/dev/null || true
    sleep 20
    free -gbash

    Relaunching 3 s after a pkill fails with 'Free memory on device cuda:0 (95.11/121.69 GiB) on startup is less than desired GPU memory utilization'. That is a timing artifact, not a reason to lower the flag.

  2. 2

    Download the checkpoint

    20 safetensors shards, ~92 GB. Only this node needs it — there is no second rank to mirror to.

    hf download moonshotai/Kimi-Linear-48B-A3B-Instruct \
      --local-dir ~/models/hf/Kimi-Linear-48B-A3B-Instructbash

    Verify 20 shards and zero *.incomplete files before serving. A partial tree fails ten minutes into the load.

  3. 3

    Work out the KV budget before you pick a context

    This model's KV cost is unusually low and worth computing yourself, because it is what decides the flags. Only the layers listed in linear_attn_config.full_attn_layers hold a per-token cache, and they are MLA, so the per-token cost is (kv_lora_rank + qk_rope_head_dim) x 2 bytes x that layer count.

    python3 - <<'PY'
    import json
    c = json.load(open('/home/joemuller/models/hf/Kimi-Linear-48B-A3B-Instruct/config.json'))
    full = c['linear_attn_config']['full_attn_layers']
    kda  = c['linear_attn_config']['kda_layers']
    per_tok = (c['kv_lora_rank'] + c['qk_rope_head_dim']) * 2 * len(full)
    print(f"{len(kda)} KDA layers, {len(full)} full-attention (MLA) layers")
    print(f"KV per token: {per_tok} bytes")
    for gib in (2, 5):
        print(f"  {gib} GiB pool -> ~{gib * 2**30 // per_tok:,} tokens")
    PYbash

    Prints 8,064 bytes/token. Measured pools came in at 8,178 bytes/token — the ~1.4% gap is vLLM's page rounding.

  4. 4

    Serve it

    The KV pin is small on purpose: 2 GiB already buys 2x concurrency at the full 131,072-token context, and nothing else on this node can use the memory better. Pinning the pool explicitly also skips vLLM's memory profiling, which is the safer path on unified memory.

    PATH="$HOME/venvs/vllm/bin:$PATH" VLLM_USE_DEEP_GEMM=0 \
    TORCHINDUCTOR_COMPILE_THREADS=2 MAX_JOBS=4 \
    vllm serve ~/models/hf/Kimi-Linear-48B-A3B-Instruct \
      --served-model-name Kimi-Linear-48B-A3B-Instruct \
      --max-model-len 131072 \
      --max-num-batched-tokens 4096 \
      --kv-cache-memory-bytes 2147483648 \
      --gpu-memory-utilization 0.85 \
      --max-num-seqs 4 \
      --trust-remote-code \
      --host 0.0.0.0 --port 8000bash

    Budget ~11 minutes: weight load alone is ~630 s. Run it under setsid/nohup if you are driving the node over SSH, or the serve dies with your session.

  5. 5

    Confirm what it built

    Three lines in the log tell you the config is right. The block size is the surprising one — a hybrid model rounds the attention page up to match the mamba/KDA recurrent state, so it is 1,888 rather than the usual 16.

    grep -E "Model loading took|attention block size|GPU KV cache size|Maximum concurrency" ~/kimi-serve.logbash

    Expect 91.53 GiB, block size 1888, and a 262,708-token pool at 2.0x concurrency. All four confirmed 2026-07-27.

  6. 6

    Warm it up before you trust any number

    Four requests is enough. The first request also pays a flashinfer autotune pass that does not repeat.

    for i in 1 2 3 4; do
      curl -s http://127.0.0.1:8000/v1/chat/completions \
        -H 'Content-Type: application/json' \
        -d '{"model":"Kimi-Linear-48B-A3B-Instruct",
             "messages":[{"role":"user","content":"Write an LRU cache in Python."}],
             "max_tokens":256,"temperature":0}' > /dev/null
    donebash

Key vLLM parameters

ParameterValuePurpose

API usage

Chat completion

curl -s http://127.0.0.1:8000/v1/chat/completions \
  -H 'Content-Type: application/json' \
  -d '{"model":"Kimi-Linear-48B-A3B-Instruct",
       "messages":[{"role":"user","content":"Explain linear attention in two sentences."}],
       "max_tokens":256,"temperature":0}'bash

Measure decode speed correctly

Take tok/s from the usage block's completion_tokens divided by elapsed time, not by counting SSE frames. Warm the server first — a cold run reads low.

curl -s http://127.0.0.1:8000/v1/chat/completions \
  -H 'Content-Type: application/json' \
  -d '{"model":"Kimi-Linear-48B-A3B-Instruct",
       "messages":[{"role":"user","content":"Write a red-black tree in Python."}],
       "max_tokens":256,"temperature":0,"stream":true,
       "stream_options":{"include_usage":true}}' \
  | grep -o '"completion_tokens":[0-9]*' | tail -1bash

Troubleshooting

Can it really serve the advertised 1,048,576-token context?
vLLM 0.26.0 dies right after 'torch.compile took NN s' with no error
'Free memory on device cuda:0 (95.11/121.69 GiB) on startup is less than desired GPU memory utilization (0.9, 109.52 GiB)'
pkill killed my SSH session instead of the server
Why is decode only 29 tok/s for a model with 2.7B active parameters?
The log says 'Setting attention block size to 1888 tokens'. Is that wrong?
Can I speculative-decode this model?

Revision history

What has changed on this page since it was published, and what it measured. Newest first.

  1. re-measuredaction needed

    Promoted from a benchmark-only stub to a full recipe: real serve command, memory budget, flags and troubleshooting, measured on vLLM 0.24.0.

    Context served goes from the stub's throwaway 10,240 to a verified 131,072. Decode at a 2,048-token prompt measures 29.40 tok/s, reproducing the 2026-07-09 rows (29.75) on the same node, so no drift. Marked breaking because the old rows were taken at --gpu-memory-utilization 0.85 with --max-num-seqs 8 and a 10,240 context; the flags on this page differ.

  2. compatibility

    Pinned the engine to vLLM 0.24.0 — 0.26.0 cannot start this model.

    No throughput change; 0.26.0 dies without a traceback immediately after torch.compile, both with and without --kv-cache-memory-bytes, so there is no measurement to compare against.

  3. correctness

    Established the real context ceiling by bracketing it with real requests, and added --max-num-batched-tokens 4096 as the mitigation.

    contextTokens is 98,304 (verified end to end at 26.3 tok/s decode, 37.8 s TTFT) rather than the model card's 1,048,576. Without the chunk bound a 131,072-token prompt died ~45,000 tokens in; with it, 98,304 succeeded and 130,000 still died.

Memory budget

This is the tightest single-Spark config on the site, and the interesting part is which half is tight. Weights are a measured 91.53 GiB — the engine reports it directly as 'Model loading took 91.53 GiB memory' — which is 80% of the 114 GiB usable pool before a single token of KV exists. What makes the config work anyway is the attention design: only 7 of 27 layers are full attention, and those 7 are MLA with kv_lora_rank 512 and qk_rope_head_dim 64, so a token costs (512 + 64) x 2 bytes x 7 layers = 8,064 bytes of KV. The other 20 layers are Kimi Delta Attention, whose recurrent state is a fixed size per sequence rather than per token, so they contribute nothing that grows with context. Measured: a 5 GiB KV pin yielded a 656,759-token pool, i.e. 131,352 tokens/GiB or 8,178 bytes/token once vLLM's page padding is counted — within 1.4% of the arithmetic. For comparison, that is roughly 20x more context per GiB than a conventional GQA model of this size. So KV is nearly free here and the whole budget is spent on weights. The 2 GiB pin this recipe ships measured a 253,166-token pool = 1.93x concurrency at a 131,072-token max-model-len, which is deliberate headroom rather than waste: it costs 3 GiB out of a pool that has nothing else to spend it on. Steady-state total was 106 GiB of 121 by free -g while serving, against a ~4 GiB idle baseline, so the serving working set is ~102 GiB and the overhead segment below is that residual (102 - 91.53 - 5) from the 5 GiB run.