|
| 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 |
0 commit comments