Kimi-K3 on 16× H200 141GB: Deployment Report

#136
by local0state - opened

Successfully deployed moonshotai/Kimi-K3 (2.8T MoE, MXFP4) on 2× nodes (8× NVIDIA H200 141GB each) using SGLang.
This report documents what worked, what didn't, and measured performance — hopefully
it saves others some time, since most official recipes assume Blackwell (B300/GB300)
and the H200 path is less traveled.

TL;DR

Hardware 2 nodes × 8× H200 141GB
Engine that worked SGLang (lmsysorg/sglang:kimi-k3 image), TP16 + EP16, marlin MoE backend
Engine that didn't vLLM kimi-k3 branch (vllm/vllm-openai:kimi-k3) — weight-loading memory bug on H200, details below
Weights per GPU 102.75 GB (SGLang, close to the 1561GB/16 ≈ 98GB theoretical)
KV cache per GPU 7.95 GB pool, bf16, 308,608 tokens
Server startup (cold) 13 min 02 s end-to-end (11.2 min weight load + 39 s CUDA graphs)
TTFT (short prompt) 0.37–0.54 s
Single-request decode 16.8 tok/s
Aggregate throughput ~147 tok/s at 54 concurrent requests (scaling not yet saturated)

Setup

  • Model: moonshotai/Kimi-K3 (~1.56 TB, 96 safetensors shards)
  • Launch config: the official SGLang cookbook h200 / unified / balanced profile:
NCCL_MNNVL_ENABLE=1 NCCL_CUMEM_ENABLE=1 SGLANG_ENABLE_TP_MEMORY_INBALANCE_CHECK=0 \
sglang serve \
  --trust-remote-code \
  --model-path /path/to/Kimi-K3 \
  --tp-size 16 --nnodes 2 --node-rank <0|1> --dist-init-addr <head-ip>:20000 \
  --ep-size 16 \
  --moe-runner-backend marlin \
  --decode-attention-backend flashmla \
  --enable-symm-mem \
  --mem-fraction-static 0.85 \
  --reasoning-parser kimi_k3 --tool-call-parser kimi_k3 \
  --host 0.0.0.0 --port 30000

Two environment fixes were needed on top of the official recipe:

  1. Pin the cross-node NIC on every rank (NCCL_SOCKET_IFNAME / GLOO_SOCKET_IFNAME).
    Our nodes have different interface names, so I detect it at runtime from
    /sys/class/net instead of hardcoding. Without this, Gloo bootstrap fails.
  2. Move the Triton JIT cache off the shared filesystem
    (TRITON_CACHE_DIR=/tmp/triton-cache-rank$RANK). With $HOME on shared fs, 16 ranks
    JIT-compiling the same kernel race on the cache's tmp-file + rename pattern and
    crash with FileNotFoundError during CUDA graph capture.

Measured performance (SGLang, TP16+EP16, marlin, bf16 KV)

Generation benchmark, 512-token completions, reasoning_effort=low:

Concurrency Aggregate tok/s Per-request latency (p50 / max)
1 16.8 28.3 s
4 30.0 ~28 s
8 43.9 ~31 s
16 76.9 ~43 s
32 84.4 57 s / 89 s
54 146.9 60 s / 102 s
  • TTFT: 0.37–0.54 s (short prompts, warm server)
  • Throughput was still scaling at 54 concurrent (the highest CUDA-graph batch size
    captured in this config) — with a larger --cuda-graph-max-bs or the
    high-throughput profile, expect more.
  • Note: Kimi-K3 is always-thinking; per-request latency includes reasoning tokens.
    16.8 tok/s single-stream is the marlin dequant cost on Hopper — Blackwell with
    native MXFP4 paths will be substantially faster.

What didn't work: vLLM kimi-k3 branch on H200

I tried the vLLM route first (vllm/vllm-openai:kimi-k3, since K3 support has not
landed in main/nightly yet). On H200 it currently has a weight-loading memory bug
that makes 141GB GPUs infeasible regardless of parallelism:

Parallelism Expected weights/GPU Measured (Model loading took)
TP16 (2 nodes) ~98 GB 130.2 GB
TP32 (4 nodes) ~49 GB 127.3 GB
TP16 + EP16 ~98 GB 130.2 GB
TP8 + PP4 ~49 GB 47 GB (but PP crashes, see below)

Per-GPU weight memory does not decrease when doubling GPUs (130 → 127 GB from
TP16 to TP32), which points to a large component being replicated per-rank instead
of sharded in the K3 branch's loader. SGLang loading the same checkpoint with the
same marlin backend lands at 102.75 GB/GPU, confirming the checkpoint itself is fine.
With 130GB weights + runtime overhead, KV cache comes up 1–2 GB short even at
gpu-memory-utilization 0.97 and max-model-len 65536 (vLLM estimated max context
22016 — unusable).

Other H200-specific landmines hit on the vLLM branch, for the record:

  • VLLM_ENABLE_K3_LATENT_MOE_TAIL_FUSION=1 (recommended in the recipe's Hopper
    profile) raises ValueError: K3 latent-MoE tail fusion requires SM100. — it's
    Blackwell-only; don't set it on H200.
  • Pipeline parallel (TP8+PP4) loads fine but crashes in CUDA graph capture with
    RuntimeError: cp_world_size must be positive from the MLA attention path —
    PP appears unsupported on the K3 branch (the recipe also lists no PP strategy).
  • --load-format fastsafetensors stages one full 16GB shard per rank with
    double-buffering (
    33 GB transient per GPU) — combined with the inflated weights
    it OOMs during loading. With the EP loader path it was worse (127 GB at 91%
    loading progress on a config whose steady state should be ~49 GB).

Notes for anyone reproducing

  • 8× B300/GB300 remains the intended single-node target for K3. On Hopper you pay
    the marlin W4A16 dequant tax and need 2× 8-GPU nodes minimum.
  • The SGLang H200 profiles are marked verified: false / in-progress in the
    cookbook as of 2026-07-28, but the balanced profile worked as-is.
  • Multi-turn users: remember K3 requires passing back the full assistant message
    including reasoning_content and tool_calls, not just content.
  • Startup timeline (cold start, weights on shared fs with warm FS cache):
    process launch → distributed init + model build 25 s → weight load 669 s
    (
    1.56 TB, load_format=auto) → decode CUDA graph capture 39 s (11 batch
    sizes up to bs=54) → warmup → ready in ~13 min total.

Following are the 2 scripts I use (generated):

run_node_sglang.sh

#!/bin/bash
# 在容器内由 srun 每节点执行一次。依赖外部传入:
#   NODE0_IP, MODEL_PATH, HOST_PORT, TP_SIZE, EP_SIZE, NNODES, MEM_FRACTION
set -euo pipefail

NODE_RANK=$SLURM_PROCID

# 跨节点 NIC 必须在所有 rank 上显式指定 (SGLang H200 multiNodeHints 要求),
# 各节点网卡名不同, 运行时探测: 取默认路由所在接口。容器镜像可能无 ip 命令,
# 故从 /sys 读取而非依赖 iproute2。
detect_iface() {
    local best="" max=0 sz
    for d in /sys/class/net/*; do
        n=$(basename "$d")
        case "$n" in lo|docker*|veth*|br-*|virbr*) continue ;; esac
        [ -e "$d/device" ] || continue
        read -r sz < "$d/speed" 2>/dev/null || sz=0
        [ "$sz" -gt "$max" ] 2>/dev/null && { max=$sz; best=$n; }
        [ -z "$best" ] && best=$n
    done
    echo "$best"
}
IFACE=$(detect_iface)
LOCAL_IP=$(cat /sys/class/net/"$IFACE"/address >/dev/null 2>&1 && hostname -I | awk '{print $1}')

export NCCL_SOCKET_IFNAME="$IFACE"
export GLOO_SOCKET_IFNAME="$IFACE"
export SGLANG_HOST_IP="$LOCAL_IP"

# EFA: 用宿主机挂进来的 libfabric + efa
# export LD_LIBRARY_PATH to include efa related libs if needed
export FI_PROVIDER=efa
export FI_EFA_USE_DEVICE_RDMA=1

# Triton JIT 缓存放节点本地盘: 默认落到 $HOME 共享盘时, 多 rank 并发
# JIT 同一 kernel 的 tmp+rename 互踩, FileNotFoundError (job 36)
export TRITON_CACHE_DIR="/tmp/triton-cache-rank${NODE_RANK}"

# SGLang H200 unified/balanced 档指定的 env
export NCCL_MNNVL_ENABLE=1
export NCCL_CUMEM_ENABLE=1
export SGLANG_ENABLE_TP_MEMORY_INBALANCE_CHECK=0

echo "[rank ${NODE_RANK}] host=$(hostname) iface=${IFACE} ip=${LOCAL_IP}"

exec sglang serve \
    --trust-remote-code \
    --model-path "$MODEL_PATH" \
    --tp-size "$TP_SIZE" \
    --nnodes "$NNODES" \
    --node-rank "$NODE_RANK" \
    --dist-init-addr "${NODE0_IP}:20000" \
    --ep-size "$EP_SIZE" \
    --moe-runner-backend marlin \
    --decode-attention-backend flashmla \
    --enable-symm-mem \
    --mem-fraction-static "$MEM_FRACTION" \
    --reasoning-parser kimi_k3 \
    --tool-call-parser kimi_k3 \
    --host 0.0.0.0 \
    --port "$HOST_PORT"

serve_2node_sglang.slurm

#!/bin/bash
#SBATCH --job-name=k3-sgl
#SBATCH --partition=p5e
#SBATCH --nodes=2
#SBATCH --ntasks-per-node=1
#SBATCH --gpus-per-node=8
#SBATCH --cpus-per-task=96
#SBATCH --mem=0
#SBATCH --exclusive
#SBATCH --time=7-00:00:00
#SBATCH --output=/home/ubuntu/workspace/kimi-k3-deploy/logs/sgl_%j.out
#SBATCH --error=/home/ubuntu/workspace/kimi-k3-deploy/logs/sgl_%j.err

###############################################################################
# Kimi-K3 —— SGLang 引擎
#
# 参数全部取自 SGLang 官方 cookbook 的 h200 / unified / balanced 档
# (docs.sglang.io/cookbook/autoregressive/Moonshotai/Kimi-K3.md):
#   TP16 + EP16, moe-runner-backend marlin, decode-attention-backend flashmla,
#   --enable-symm-mem, --mem-fraction-static 0.85
# H200 官方节点数就是 2×8, 与 vLLM 侧 job 31 同规模, 便于对比。
#
# 注意 (相对 vLLM 侧的已知情况):
#   - SGLang H200 档同样用 marlin, 所以每卡权重大概率仍是 ~130GB
#     (H200 无原生 MXFP4 硬件, 必须重打包成 W4A16, 体积涨 ~33%)
#   - 官方 mem-fraction-static 只有 0.85, 比我vLLM 侧的 0.97 保守很多;
#     若 KV cache 不够, 先往上调这个值, 再考虑降 context
#   - 三档 H200 配置在官方文档里均为 verified:false / in-progress,
#     成熟度与 vLLM recipe 的 H200 相当, 不是"更稳"的选择
#
###############################################################################

set -euo pipefail

export MODEL_PATH="/home/ubuntu/models/Kimi-K3"
export HOST_PORT=30000
export TP_SIZE=16
export EP_SIZE=16
export NNODES=2
export MEM_FRACTION=0.85

IMAGE="docker://lmsysorg/sglang:kimi-k3"
RUN_SCRIPT=/home/ubuntu/workspace/kimi-k3-deploy/sglang/run_node_sglang.sh

HEAD_NODE=$(scontrol show hostnames "$SLURM_JOB_NODELIST" | head -n1)
export NODE0_IP=$(srun --nodes=1 --ntasks=1 -w "$HEAD_NODE" hostname -i | awk '{print $1}')

echo "============================================"
echo "Kimi-K3 SGLang 2-node TP16+EP16 (p5e)"
echo "Nodes: $SLURM_JOB_NODELIST"
echo "Head:  $HEAD_NODE ($NODE0_IP)"
echo "API:   http://${NODE0_IP}:${HOST_PORT}/v1"
echo "============================================"

srun --container-image="$IMAGE" \
     --container-mounts=/home:/home \
     --export=ALL \
     bash "$RUN_SCRIPT" \
     2>&1 | tee /home/ubuntu/workspace/kimi-k3-deploy/logs/sgl_server_${SLURM_JOB_ID}.log
local0state changed discussion title from Kimi-K3 on AWS EC2 P5e (16× H200 141GB): Deployment Report to Kimi-K3 on 16× H200 141GB: Deployment Report
local0state changed discussion status to closed

Hi @local0state , I see that you closed the discussion. Have you found a solution to get Kimi K3 working with vLLM with 2x8 H200 ?

Sign up or log in to comment