Skip to content

Commit 5ba0b0b

Browse files
authored
kimik3-fp4-b300-vllm-agentic-dspark: Kimi-K3 B300 DSpark agentic recipe (#2380)
1 parent 17ad637 commit 5ba0b0b

3 files changed

Lines changed: 323 additions & 0 deletions

File tree

Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
1+
#!/usr/bin/env bash
2+
set -eo pipefail
3+
set -x
4+
5+
# Agentic trace replay benchmark for Kimi-K3 (MXFP4) on B300 using vLLM with
6+
# DSpark speculative decoding at level 2, probabilistic drafting, synthetic
7+
# acceptance pinned to the committed golden AL (real verification for evals).
8+
#
9+
# MEASURED, for anyone reading the throughput numbers: this draft head's REAL
10+
# acceptance collapses on the agentic corpus. Its native window is 32k
11+
# (rope_parameters yarn, factor 32, original_max_position_embeddings 32768,
12+
# stretched to the declared 1M), and at the corpus's 100k-330k ISL the measured
13+
# acceptance length is 1.16-2.01 with 13-41% position-1 acceptance -- versus 4.18
14+
# AL and 0.826 position-1 on short-context work under the identical image, draft
15+
# and spec config. Synthetic acceptance is the AgentX-prescribed way to measure
16+
# system performance at a fixed acceptance target (see the SPEC_CONFIG block), so
17+
# these throughput numbers are NOT evidence that the draft predicts well at
18+
# agentic context lengths. It does not.
19+
#
20+
# MTP sibling of kimik3_fp4_b300_vllm.sh. Everything about the target server is
21+
# identical; the deltas are:
22+
# - --speculative-config method "dspark", draft Inferact/Kimi-K3-DSpark,
23+
# attention_backend FLASHINFER_MLA,
24+
# draft_sample_method probabilistic,
25+
# rejection_sample_method block
26+
# - cudagraph capture sizes are expressed in TOKENS, not sequences
27+
#
28+
# Required env vars:
29+
# MODEL, TP, CONC, KV_OFFLOADING, TOTAL_CPU_DRAM_GB, RESULT_DIR, DURATION
30+
#
31+
# TP8 is the only single-node layout. The MXFP4 checkpoint is ~1.5 TB on disk;
32+
# at TP4 that is ~375 GB of weights per GPU against B300's 288 GB of HBM, so
33+
# only the full 8-GPU shard (~188 GB/GPU) fits. Do not add TP4/TP2 arms.
34+
#
35+
# Chat formatting: AGENTS.md requires MTP scripts to pass --use-chat-template to
36+
# run_benchmark_serving. Agentic recipes never call it -- the replay drives
37+
# AIPerf against /v1/chat/completions, so prompts are already chat-formatted and
38+
# spec-decode acceptance is measured on chat-shaped inputs. Nothing to add here.
39+
40+
source "$(dirname "$0")/../../benchmark_lib.sh"
41+
42+
check_env_vars MODEL TP CONC KV_OFFLOADING TOTAL_CPU_DRAM_GB RESULT_DIR DURATION
43+
44+
# The 2.8T MXFP4 checkpoint only fits across all 8 B300s (see header).
45+
if [ "$TP" -ne 8 ]; then
46+
echo "Error: Kimi-K3 on B300 requires TP=8 (a ~1.5 TB MXFP4 checkpoint does not fit at TP<8), got TP='$TP'" >&2
47+
exit 1
48+
fi
49+
50+
if [[ -n "${EP_SIZE:-}" && "${EP_SIZE}" -gt 1 ]]; then
51+
echo "Error: this recipe ships the pure-TP8 profile; EP_SIZE='$EP_SIZE' is not wired yet" >&2
52+
exit 1
53+
fi
54+
55+
if [[ -n "${SLURM_JOB_ID:-}" ]]; then
56+
echo "JOB $SLURM_JOB_ID running on ${SLURMD_NODENAME:-unknown}"
57+
fi
58+
59+
DRAFT_MODEL="${DRAFT_MODEL:-Inferact/Kimi-K3-DSpark}"
60+
61+
# `hf download` creates the target dir if missing and is itself idempotent.
62+
# When MODEL_PATH is unset (stand-alone runs), fall back to the HF_HUB_CACHE.
63+
# Either way, MODEL_PATH is what the server is launched with.
64+
#
65+
# The draft must NOT land next to a pre-staged target: Kimi-K3 is in the
66+
# b300-nv launcher's STAGED_MODELS, so dirname(MODEL_PATH) is the read-only
67+
# /scratch/models mount and writing there fails with PermissionError. Use the
68+
# launcher's writable models dir instead, which also caches the draft across
69+
# the jobs in a sweep (same placement as DeepSeek-V4-Pro-DSpark).
70+
if [[ -n "${MODEL_PATH:-}" ]]; then
71+
if [[ ! -d "$MODEL_PATH" || -z "$(ls -A "$MODEL_PATH" 2>/dev/null)" ]]; then
72+
hf download "$MODEL" --local-dir "$MODEL_PATH"
73+
fi
74+
DRAFT_MODEL_PATH="${WRITABLE_MODELS_DIR:-/data/models}/${DRAFT_MODEL##*/}"
75+
if [[ ! -d "$DRAFT_MODEL_PATH" || -z "$(ls -A "$DRAFT_MODEL_PATH" 2>/dev/null)" ]]; then
76+
hf download "$DRAFT_MODEL" --local-dir "$DRAFT_MODEL_PATH"
77+
fi
78+
else
79+
hf download "$MODEL"
80+
export MODEL_PATH="$MODEL"
81+
hf download "$DRAFT_MODEL"
82+
DRAFT_MODEL_PATH="$DRAFT_MODEL"
83+
fi
84+
nvidia-smi
85+
86+
# ---- Resolve traces and install deps ----------------------------------------
87+
resolve_trace_source
88+
install_agentic_deps
89+
90+
# ---- Kimi-K3 production serving environment ---------------------------------
91+
export NCCL_DMABUF_ENABLE=0
92+
export VLLM_ALLREDUCE_USE_FLASHINFER=1
93+
# Required by the upstream recipe on BOTH its blackwell and nvidia paths
94+
# (recipes.vllm.ai/moonshotai/Kimi-K3), and never set here before. It defaults to
95+
# 0, and it is threaded into LatentMoERunner as
96+
# runner_args={"enable_k3_latent_moe_tail_fusion": ...} at
97+
# vllm/models/kimi_k3/nvidia/model.py:549 -- the same runner whose shared-experts
98+
# output buffer asserted (fused_moe/runner/shared_experts.py:165, all 8 TP ranks)
99+
# when the wider flag alignment was tried. With this unset we have been running a
100+
# MoE tail path upstream never exercises, so enable it on its own before
101+
# re-attempting any of the other upstream flags.
102+
export VLLM_ENABLE_K3_LATENT_MOE_TAIL_FUSION=1
103+
export VLLM_USE_RUST_FRONTEND=1
104+
# Loading ~1.5 TB of MXFP4 shards off the staged mount takes well past the
105+
# default readiness window even with fastsafetensors.
106+
export VLLM_ENGINE_READY_TIMEOUT_S=3600
107+
export PYTHONNOUSERSITE=1
108+
# AIPerf pins one pooled keep-alive connection per agentic session and reuses it
109+
# across turns, while the Rust frontend's default VLLM_HTTP_TIMEOUT_KEEP_ALIVE is
110+
# 5s. An inter-turn idle gap longer than that lets the client reuse a socket at
111+
# the moment the server closes it -> aiohttp ServerDisconnectedError -> AIPerf
112+
# treats it as a terminal warmup failure and aborts the whole job. Outlast the
113+
# client pool so the race cannot occur.
114+
export VLLM_HTTP_TIMEOUT_KEEP_ALIVE=900
115+
# Agentic warmup dispatches large prompts at once; allow up to 15 minutes of TCP
116+
# progress before AIPerf declares a connection dead.
117+
export AIPERF_HTTP_TCP_USER_TIMEOUT=900000
118+
119+
# ---- Server config ----------------------------------------------------------
120+
SERVER_LOG="$RESULT_DIR/server.log"
121+
mkdir -p "$RESULT_DIR"
122+
123+
# ---- KV offloading ----------------------------------------------------------
124+
# The generated TOTAL_CPU_DRAM_GB budget is the aggregate host-DRAM pool for the
125+
# node; SimpleCPUOffloadConnector is sized per rank. At dram-utilization 0.63 on
126+
# cluster:b300-nv this resolves to ~220 GiB per rank across the 8 TP ranks.
127+
OFFLOAD_ARGS=()
128+
case "${KV_OFFLOAD_BACKEND:-}" in
129+
"")
130+
require_agentic_kv_offload_none
131+
;;
132+
vllm-simple)
133+
require_agentic_kv_offload_backend vllm-simple
134+
CPU_BYTES_PER_RANK=$(( TOTAL_CPU_DRAM_GB * 1000 * 1000 * 1000 / TP ))
135+
# Identical prefixes must hash to identical block keys run-to-run.
136+
export PYTHONHASHSEED=42
137+
# lazy_offload must be a JSON boolean, not a quoted string: the
138+
# connector does bool(extra_config.get("lazy_offload", False)), and
139+
# bool("false") is True in Python.
140+
OFFLOAD_ARGS=(
141+
--kv-transfer-config
142+
"{\"kv_connector\":\"SimpleCPUOffloadConnector\",\"kv_role\":\"kv_both\",\"kv_connector_extra_config\":{\"cpu_bytes_to_use_per_rank\":${CPU_BYTES_PER_RANK},\"lazy_offload\":false}}"
143+
)
144+
;;
145+
*)
146+
echo "Error: unsupported KV_OFFLOAD_BACKEND='$KV_OFFLOAD_BACKEND' (expected empty or vllm-simple)" >&2
147+
exit 1
148+
;;
149+
esac
150+
151+
# ---- DSpark speculative decoding -------------------------------------------
152+
# Probabilistic drafting at DSpark level 2, with synthetic acceptance pinned to
153+
# the committed golden AL, per the AgentX policy in
154+
# golden_al_distribution/README.md: "a submission may choose any supported draft
155+
# length, but it may not substitute a different acceptance target", because
156+
# "InferenceX is evaluating inference-system performance, not the ability to
157+
# fine-tune a benchmark-specific speculative head". AL is workload-dependent by
158+
# the README's own admission, so the prescribed acceptance -- not the acceptance
159+
# this corpus happens to produce -- is what makes submissions comparable.
160+
#
161+
# Level 2 rather than 7. Draft length IS ours to choose, and the verify width is
162+
# a real system cost that synthetic acceptance does not remove: at K=7 the target
163+
# verifies 8 positions per step, which loses to no-speculation once the GPU
164+
# saturates even at the forced golden AL of 3.84. Using cost ~ 1 + m(K-1) + f,
165+
# K=2 clears break-even at both ends of the m range while K=3 is a wash at
166+
# saturation and K=7 is negative:
167+
# K=2 golden AL 2.51 -> ~1.7x low conc, ~1.2x saturated
168+
# K=3 golden AL 3.00 -> ~1.7x low conc, ~0.95x saturated
169+
# K=7 golden AL 3.84 -> ~1.3x low conc, ~0.54x saturated
170+
# The drafter's KV footprint (~25% of GPU KV) is unaffected by K and remains a
171+
# genuine cost of running spec decoding at all on 100k+ prompts.
172+
NUM_SPEC_TOKENS=2
173+
TOKENS_PER_SEQ=$((1 + NUM_SPEC_TOKENS))
174+
# Committed golden AL at K=2 on the probabilistic curve we run
175+
# (golden_al_distribution/kimik3_dspark_probabilistic_sample_method_block_rejection_sample_method.yaml:
176+
# thinking_on 2 -> 2.51). The greedy/standard curve's K=2 value is 2.45 -- do not
177+
# mix curves.
178+
SYNTHETIC_ACCEPT_LEN=2.51
179+
180+
# Throughput runs pin synthetic acceptance; the EVAL_ONLY accuracy run must use
181+
# real target verification instead. Synthetic acceptance commits drafted tokens
182+
# regardless of the target's logits, so the generated text is wrong and the
183+
# SWE-bench eval scores 0.0000 -- the same split dsv4_fp4_b300_vllm_mtp.sh makes
184+
# (and which kimik2.5_fp4_b300_mtp.sh omits; follow dsv4, not kimik2.5).
185+
# rejection_sample_method=block does real verification, so it is what EVAL_ONLY
186+
# uses. vLLM rejects synthetic_acceptance_length unless the method is 'synthetic'.
187+
if [ "${EVAL_ONLY:-false}" = "true" ]; then
188+
SPEC_CONFIG="{\"method\": \"dspark\", \"model\": \"$DRAFT_MODEL_PATH\", \"num_speculative_tokens\": $NUM_SPEC_TOKENS, \"attention_backend\": \"FLASHINFER_MLA\", \"draft_sample_method\": \"probabilistic\", \"rejection_sample_method\": \"block\"}"
189+
else
190+
SPEC_CONFIG="{\"method\": \"dspark\", \"model\": \"$DRAFT_MODEL_PATH\", \"num_speculative_tokens\": $NUM_SPEC_TOKENS, \"attention_backend\": \"FLASHINFER_MLA\", \"draft_sample_method\": \"probabilistic\", \"rejection_sample_method\": \"synthetic\", \"synthetic_acceptance_length\": $SYNTHETIC_ACCEPT_LEN}"
191+
fi
192+
193+
# Agentic fan-out: the scheduler headroom convention shared by the other agentic
194+
# recipes. Upstream pins 32 instead, but the aligned flag set crashed the engine
195+
# core on every concurrency (see the serve-flag note below), so this stays on the
196+
# value that ran green. Capture decode graphs only to this bound — a 93-layer
197+
# 2.8T model makes capturing vLLM's full 2048-wide ladder prohibitively slow.
198+
MAX_NUM_SEQS=$((2 * CONC))
199+
200+
# With spec decoding, cudagraph capture sizes are in TOKENS. vLLM rounds
201+
# configured sizes up to multiples of (1 + num_speculative_tokens) and dedups
202+
# them (adjust_cudagraph_sizes_for_spec_decode), so a plain 1..MAX_NUM_SEQS list
203+
# would collapse to covering only MAX_NUM_SEQS/(1+N) sequences and drop the
204+
# largest decode batches to eager. Enumerate the multiples explicitly so there
205+
# is one graph per decode batch of 1..MAX_NUM_SEQS sequences. The graph mode is
206+
# left at the recipe default that the non-MTP K3 arm already validated.
207+
CUDA_GRAPH_CAPTURE_SIZES=""
208+
for ((num_seqs = 1; num_seqs <= MAX_NUM_SEQS; num_seqs++)); do
209+
if [ -n "$CUDA_GRAPH_CAPTURE_SIZES" ]; then
210+
CUDA_GRAPH_CAPTURE_SIZES+=","
211+
fi
212+
CUDA_GRAPH_CAPTURE_SIZES+="$((num_seqs * TOKENS_PER_SEQ))"
213+
done
214+
COMPILATION_CONFIG="{\"cudagraph_capture_sizes\":[${CUDA_GRAPH_CAPTURE_SIZES}]}"
215+
216+
echo "Starting vllm server..."
217+
218+
{ set +x; } 2>/dev/null
219+
VLLM_CMD=(
220+
vllm serve "$MODEL_PATH" --served-model-name "$MODEL"
221+
--host 0.0.0.0
222+
--port "$PORT"
223+
--tensor-parallel-size "$TP"
224+
# Adopting the full upstream flag set (gpu-memory-utilization 0.95,
225+
# max-num-seqs 32, max-model-len 1000000, max-num-batched-tokens 32768,
226+
# --no-enable-flashinfer-autotune, VLLM_USE_V2_MODEL_RUNNER=1) killed the
227+
# engine core on every concurrency with an assertion in the FlashInfer MoE
228+
# runner's shared-experts output buffer
229+
# (fused_moe/runner/shared_experts.py:165, all 8 TP ranks at once). Only
230+
# mla_prefill_backend=TRTLLM_RAGGED is retained from that set; the rest stay
231+
# on the values that ran 12/12 green in run 30326393603.
232+
--gpu-memory-utilization 0.90
233+
--max-num-seqs "$MAX_NUM_SEQS"
234+
--max-model-len 1048576
235+
--trust-remote-code
236+
--load-format fastsafetensors
237+
--moe-backend auto
238+
--enable-prefix-caching
239+
--kv-cache-dtype fp8
240+
--reasoning-parser kimi_k3
241+
--tool-call-parser kimi_k3
242+
--enable-auto-tool-choice
243+
# FP8 KV cache requires the prefill query quantization flag. MLA prefill
244+
# runs on TRTLLM_RAGGED per the upstream Blackwell override.
245+
--attention-config '{"mla_prefill_backend":"TRTLLM_RAGGED","use_prefill_query_quantization":true}'
246+
--speculative-config "$SPEC_CONFIG"
247+
--compilation-config "$COMPILATION_CONFIG"
248+
--disable-uvicorn-access-log
249+
"${OFFLOAD_ARGS[@]}"
250+
)
251+
printf '%q ' "${VLLM_CMD[@]}" | tee "$RESULT_DIR/vllm_command.txt"
252+
printf '\n' | tee -a "$RESULT_DIR/vllm_command.txt"
253+
"${VLLM_CMD[@]}" > "$SERVER_LOG" 2>&1 &
254+
SERVER_PID=$!
255+
echo "Server PID: $SERVER_PID"
256+
257+
wait_for_server_ready --port "$PORT" --server-log "$SERVER_LOG" --server-pid "$SERVER_PID"
258+
259+
if [ "${EVAL_ONLY}" = "true" ]; then
260+
run_eval --port "$PORT"
261+
else
262+
build_replay_cmd "$RESULT_DIR"
263+
run_agentic_replay_and_write_outputs "$RESULT_DIR"
264+
fi

configs/nvidia-master.yaml

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1609,6 +1609,50 @@ kimik3-fp4-b300-vllm-agentic:
16091609
# TP8 SimpleCPUOffload (host DRAM)
16101610
- { tp: 8, ep: 1, kv-offloading: dram, kv-offload-backend: { name: vllm-simple }, conc-list: [1, 2, 4, 8, 16, 24] }
16111611

1612+
kimik3-fp4-b300-vllm-agentic-dspark:
1613+
# DSpark speculative decoding on the Inferact/Kimi-K3-DSpark draft head, at
1614+
# level 2 with probabilistic drafting and synthetic acceptance pinned to the
1615+
# committed golden AL 2.51 (golden_al_distribution/
1616+
# kimik3_dspark_probabilistic_sample_method_block_rejection_sample_method.yaml,
1617+
# thinking_on 2 -> 2.51), per the AgentX policy in that directory's README:
1618+
# draft length is the submission's choice, the acceptance target is not.
1619+
# EVAL_ONLY runs switch to real block verification, since synthetic acceptance
1620+
# commits drafts regardless of target logits and would zero the SWE-bench score.
1621+
#
1622+
# Level 2 not 7: verify width is a real system cost synthetic acceptance does
1623+
# not remove, and at K=7 the 8-position verify loses to no-speculation once the
1624+
# GPU saturates even at the forced golden AL of 3.84.
1625+
#
1626+
# The draft's REAL acceptance on this corpus is 1.16-2.01 (13-41% position-1) --
1627+
# its native window is 32k YaRN-stretched to 1M, and the same image/draft/config
1628+
# reaches AL 4.18 on short-context work. These throughput numbers measure the
1629+
# system at a prescribed acceptance, not the draft's fitness at 100k+ context.
1630+
image: vllm/vllm-openai:kimi-k3
1631+
model: moonshotai/Kimi-K3
1632+
model-prefix: kimik3
1633+
runner: cluster:b300-nv
1634+
precision: fp4
1635+
framework: vllm
1636+
multinode: false
1637+
scenarios:
1638+
# Agentic-coding only: no fixed-seq-len (1k1k / 8k1k) arms for this recipe.
1639+
agentic-coding:
1640+
- dram-utilization: 0.63
1641+
search-space:
1642+
# Mirrors the non-MTP entry's KV arms so the spec-decoding delta is
1643+
# readable at equal concurrency, but stops at conc 16 rather than 24: the
1644+
# non-MTP bring-up sweep (run 30326393603) showed the GPU-resident arm
1645+
# already thrashing at conc >= 16 (prefix cache hit 2.7%, TTFT p50 86-191s)
1646+
# because GPU KV holds only ~3.1 max-length requests, so conc 24 costs a
1647+
# full job per arm to re-measure that regime. conc 16 still exercises the
1648+
# DRAM tier meaningfully (62% external prefix cache hit rate there).
1649+
# TP8-only for the same memory reason: a ~1.5 TB MXFP4 checkpoint does not
1650+
# fit below 8 GPUs.
1651+
# TP8 GPU-resident
1652+
- { tp: 8, ep: 1, spec-decoding: mtp, kv-offloading: none, conc-list: [1, 2, 4, 8, 16] }
1653+
# TP8 SimpleCPUOffload (host DRAM)
1654+
- { tp: 8, ep: 1, spec-decoding: mtp, kv-offloading: dram, kv-offload-backend: { name: vllm-simple }, conc-list: [1, 2, 4, 8, 16] }
1655+
16121656
dsr1-fp8-b200-trt:
16131657
image: nvcr.io#nvidia/tensorrt-llm/release:1.3.0rc14
16141658
model: deepseek-ai/DeepSeek-R1-0528

0 commit comments

Comments
 (0)