MiMo-V2.6-Flash-RL¶
Xiaomi's 309B mixture-of-experts model (15B active) at 512K tokens per request on one 8-card Gaudi 2 node. 39 of its 48 layers attend to a 128-token window with a learned sink, which vllm-gaudi v0.26.0 cannot serve on its own, so this recipe runs in the MiMo-V2.6-Flash port image: exact sliding-window attention with sinks, window-sized KV for the sliding layers, a tiled global-attention prefill and the routed experts re-encoded to FP8 at load.
| Model | XiaomiMiMo/MiMo-V2.6-Flash-RL (MIT), revision 5711b26 |
| Hardware | 8x Gaudi 2, one node (TP 8, experts split over the 8 ranks) |
| Image | the port image, built from vllm-gaudi v0.26.0 (b059f9c), vLLM 568afb3a, Gaudi SW 1.24.1 |
| Precision | bf16 attention (FP8 block weights dequantised at load), FP8 e4m3 routed experts (exact re-encode of the MXFP4 checkpoint), bf16 KV |
| Context | 524,288 per request (the checkpoint allows 1,048,576) |
| Concurrency | --max-num-seqs 16 |
| Parsers | tools mimo, reasoning mimo |
| Recipe | recipes/XiaomiMiMo/MiMo-V2.6-Flash-RL/recipe.yaml |
Build the image¶
The port installs into a stock vllm-gaudi v0.26.0 image (Getting started). From a
checkout of this repository:
docker build -f ports/mimo-v2-6-flash/Dockerfile --build-arg BASE=<your-vllm-gaudi-v0.26.0-image> \
-t <your-mimo-v2-6-flash-image> .
install.sh checks the release commits, applies the v0.26.0 fix profile and vllm-long-prefill-share, then
the port's runner patches, and installs the plugin. It stops at the first step that does not apply. See
ports/mimo-v2-6-flash/README.md.
Launch¶
Inside the port image, where $COOKBOOK is /opt/gaudi-cookbook, with the Hugging Face cache mounted. The
first line stages the checkpoint (symlinks only; downloads it first if it is not in the cache).
export CHECKPOINT_DIR=/data/mimo-v2.6-flash-rl-hpu
export PT_HPU_LAZY_MODE=0
export VLLM_T_COMPILE_DYNAMIC_SHAPES=false
export PT_HPU_ENABLE_LAZY_COLLECTIVES=true
export VLLM_SKIP_WARMUP=false
export MIMO_SWA_KV_GROUPS=1
export MIMO_GA_TILE=1
export VLLM_LONG_PREFILL_SHARE=0.5
export VLLM_LONG_PREFILL_SHARE_ASYNC=1
export VLLM_LONG_PREFILL_MIN_TOKENS=65536
export VLLM_MOE_CHUNK=256
export VLLM_MOE_TOKEN_BOUNDARY=2048
export VLLM_ENGINE_ITERATION_TIMEOUT_S=3600
export VLLM_RPC_TIMEOUT=3600000
export VLLM_EXECUTE_MODEL_TIMEOUT_SECONDS=3600
python3 "$COOKBOOK/ports/mimo-v2-6-flash/convert/stage_checkpoint.py" XiaomiMiMo/MiMo-V2.6-Flash-RL "$CHECKPOINT_DIR" --revision 5711b268169967567844e1e560e8a3966da959b1 || exit 1
# Fixes: the v0.26.0 profile plus this recipe's opt-in fixes, in the pack's order.
# Stops with a non-zero exit code if the image does not match.
python3 "$COOKBOOK/fixes/apply_all.py" --profile v0.26.0 vllm-long-prefill-share || exit 1
vllm serve $CHECKPOINT_DIR \
--served-model-name XiaomiMiMo/MiMo-V2.6-Flash-RL \
--tensor-parallel-size 8 \
--max-model-len 524288 \
--max-num-seqs 16 \
--long-prefill-token-threshold 2048 \
--max-num-batched-tokens 8192 \
--block-size 128 \
--gpu-memory-utilization 0.9 \
--enable-prompt-tokens-details \
--trust-remote-code \
--generation-config vllm \
--override-generation-config '{"temperature": 1.0, "top_p": 0.95}' \
--chat-template "$COOKBOOK/ports/mimo-v2-6-flash/serving/chat_template.jinja" \
--reasoning-parser mimo \
--enable-auto-tool-choice \
--tool-call-parser mimo
stage_checkpoint.pywrites$CHECKPOINT_DIRwith symlinks to the checkpoint and aconfig.jsonwith two edits:quantization_configis removed (the port dequantises the FP8 block weights and re-encodes the experts itself) andlayer_typesis derived fromhybrid_layer_pattern(without it vLLM gives every layer the 128-token window). The model is served under its public name.MIMO_SWA_KV_GROUPS=1gives the 39 sliding layers window-sized KV through vLLM's hybrid KV cache manager: 5.33x the KV blocks.MIMO_GA_TILE=1runs the global layers' prefill in query tiles so a 512K prompt does not need a multi-gigabyte attention bias. Both are what makes four 512K requests fit.PT_HPU_LAZY_MODE=0with static shapes: torch.compile mode. Eager decode runs at about 5 tok/s, and the dynamic-shape compile path fails.VLLM_SKIP_WARMUP=false: the start warms up every shape for 40 to 50 minutes after a load of a few minutes. Without it each new shape compiles on first use and stalls requests for 10 to 30 s. PointPT_HPU_RECIPE_CACHE_CONFIG=<dir>,False,65536at persistent storage to reuse compiled graphs across restarts.--generation-config vllmwith the override keeps the model's sampling defaults (temperature 1.0, top_p 0.95) without the checkpoint's 2,048-tokenmax_new_tokenscap.- The chat template is the checkpoint's, with
<think>opened in the generation prompt. Without it the reasoning parser sees the previous turn's</think>as the last marker and streams the new reasoning as content.
Buckets¶
No bucket file. Bucketing uses the image defaults; with --block-size 128 and --max-num-batched-tokens 8192
warmup covers prompt chunks up to 8,192 tokens against contexts up to the full window.
Required fixes¶
All from Fixes. install.sh applies them when the image is built; the launch command runs
the same line again and only confirms them. The opt-in one is named on that line; the rest come from
--profile v0.26.0, which also applies the release's other generic fixes.
| Fix | Why this model needs it |
|---|---|
prompt-order |
prefix caching can read another request's KV in the same step |
sampling-rows-refresh |
stale device sampling rows after a batch reorder |
structured-output-rows |
structured-output steps sample each row with another request's parameters |
bad-words-index |
bad_words raises IndexError in the engine core |
logitsprocs-rows |
logit_bias, min_p and min_tokens are silently ignored |
async-inputs |
a decode the scheduler leaves out of a step (behind long-prompt chunks) reads a stale token and repeats it: 42 repeated-token signatures in 190 probes without it, 0 with it |
async-discard-counter |
AttributeError on the first preemption under async scheduling |
resumed-prefill-logits |
a request resumed after preemption repeats its earlier output |
gather-logprobs-int32 |
top_logprobs ids come back corrupted |
logprobs-decode-guard |
a bad logprob id is replaced instead of stopping the API server |
swa-decode-window |
the exact decode window (one extra block, the oldest block head-masked); the port's runner patch turns it on for mimo_v2 |
rank-exception-fatal |
an exception on a non-zero TP rank stops the engine instead of leaving ranks out of step |
vllm-long-prefill-share (opt-in) |
one 500K prompt mid-prefill held every other stream at about 0.4 tok/s; with share 0.5 they keep 14 to 18 tok/s |
rope-cpu-cache is in the profile; the port also rebuilds MiMo's RoPE tables on the CPU in float64 itself,
because the fastest rotary frequency's angle is the position and Gaudi fp32 cos and sin are wrong past about
1.5e5 radians.
Verified¶
Run on one 8-card Gaudi 2 node from the stock vllm-gaudi v0.26.0 image (b059f9c, vLLM 568afb3a), with
install.sh run as the Dockerfile runs it and this page's Launch block as written. The device gates are
ports/mimo-v2-6-flash/tests/hpu/run_gates.sh.
install.sh: commit check, the v0.26.0 profile plusvllm-long-prefill-share, both runner patches and the plugin install all applied; the launch's fix step and both patches'--checkthen report "applied"; vLLM resolvesMiMoV2ForCausalLM,MiMoV2FlashForCausalLMandMiMoV2OmniForCausalLMto the port's model.tests/hpu/device_ops.pyon one card: the sliding-window attention with sinks matches the fp32 reference in bf16 (worst relative error 0.0036), and 12 of 12 sampled expert tensors of the checkpoint re-encode to FP8 exactly and byte-identically to the CPU encode. The device's fp32cos(200000)read 0.0000 against an exact 0.9974, the reason the RoPE tables are built on the CPU.stage_checkpoint.pyon the public checkpoint (revision5711b26) produces the sameconfig.jsonas the configuration that serves production.
| Gate | Result |
|---|---|
| Decode, 256 greedy tokens (floor 20 tok/s per stream) | 33.8 / 33.2 / 25.6 tok/s per stream at 1 / 4 / 16 streams |
Tool calls: one call with arguments, choosing between tools, no call, using a result, tool_choice: "required", a call after a 32.9K-token document |
6/6 |
| Needles at 128K / 256K / 500K tokens x depths 0.1 / 0.5 / 0.9 (greedy) | 3/3 at each length. At 500K and depth 0.5 the model called the line inserted text and declined to repeat it, quoting the line with the code in the same answer |
| Four 500K-token prompts at once | 4/4, 932 s |
Window-sized KV against the stock layout, greedy (a second server with MIMO_SWA_KV_GROUPS=0) |
token-identical on two short chats, 12K and 30K prompts chunked across 8,192-token steps, a 12K prefix hit, and 2 of the 4 rows of one batched request. The other 2 batch rows (first difference at tokens 5 to 33) also differ between two runs of the window-sized layout, so they do not separate the layouts |
On the production deployment of the same code and settings: prompt NLL on a 1,535-token document 0.7986 against a streaming fp32 CPU oracle at 0.7943; GSM8K 5-shot 0.910 (200 samples); tool calls after a 138K document.
Performance¶
From the decode gate above (one run, warm server, 256 greedy tokens):
| Streams | Tokens/s per stream | Aggregate |
|---|---|---|
| 1 | 33.8 | 34 |
| 4 | 33.2 | 133 |
| 16 | 25.6 | 410 |
The production deployment measured 32.5 tok/s per stream at 16 (a different run, without user traffic). Decode
speed does not fall with context: 39 of the 48 layers read a 128-token window. The KV pool holds 56,681 blocks
(7.26M tokens of global context, 11.9x concurrency at 524,288). With VLLM_LONG_PREFILL_SHARE=0.5, other streams
decode at 14 to 18 tok/s while one 500K prompt prefills (about 0.4 tok/s without the share; separate server
starts).
Known issues¶
- At 500K tokens and depth 0.9 some seeded needles miss on both Gaudi and an H100 reference running upstream vLLM (3 of 4 there): the model's own boundary, not the port. Every needle at depths 0.1 and 0.5 hit up to 500K.
- The model reads imperative inserted text ("IMPORTANT NOTE: ...") as prompt injection and may decline to repeat it. Phrase retrieval tests as plain facts.
- Compile mode computes a new batch shape slightly differently the first time it runs. Compare outputs only on warm runs. Greedy rows of a batched request can still differ between two server starts of the same configuration; single requests repeat exactly.
prompt_logprobsover a prompt of a few hundred thousand tokens needs a multi-gigabyte logits workspace and can run the engine out of memory at--gpu-memory-utilization 0.9.- Text only. The checkpoint's vision tower, audio encoder and MTP layers are not loaded.
What not to do¶
- Do not run window-sized KV at TP4: the global and sliding layers then hold different KV heads per rank, and the port refuses to start.
- Do not turn prefix caching off without
VLLM_CONTIGUOUS_PA=false: the contiguous KV layout bypasses the per-group block tables, and the port refuses to start. - Do not add speculative decoding, or switch to lazy mode with HPU graphs (
PT_HPU_LAZY_MODE=1without--enforce-eager), with window-sized KV on: the per-layer metadata swap needs regionally compiled layers, and the port refuses both. - Do not drop
MIMO_GA_TILE=1at 512K: the first long prefill with context then builds an 8.7 GB attention bias that warmup never profiled.