diff --git a/.gitignore b/.gitignore index e2d872c7..baa7882b 100644 --- a/.gitignore +++ b/.gitignore @@ -76,3 +76,6 @@ venv.bak/ *.log *.out *.html + +# Cursor workspace shadow artifact (path-bug); never commit +scripts/vllm_dissag/home/ diff --git a/benchmark/kimi_k3/README.md b/benchmark/kimi_k3/README.md index f68f1c76..1d8256d3 100644 --- a/benchmark/kimi_k3/README.md +++ b/benchmark/kimi_k3/README.md @@ -14,7 +14,8 @@ MAD supports Kimi-K3 day-0 inference across **three** serving frameworks on AMD ## Hardware requirements -- **8x MI350X or MI355X** (TP8) +- **8x MI350X or MI355X** (TP8) for single-node recipes below +- **MI300X (gfx942):** multi-node vLLM recipes live at [`scripts/vllm/kimik3_mi300x/`](../../scripts/vllm/kimik3_mi300x/) (PP2×TP8 colocated + 2P/2D disagg) - Checkpoint is ~1.56 TB — ensure the model cache volume has enough space ## Quick start (MAD-integrated) diff --git a/scripts/vllm/kimik3_mi300x/README.md b/scripts/vllm/kimik3_mi300x/README.md new file mode 100644 index 00000000..5cf2b146 --- /dev/null +++ b/scripts/vllm/kimik3_mi300x/README.md @@ -0,0 +1,68 @@ +# Kimi-K3 (MXFP4) serving on AMD Instinct MI300X (gfx942) + +[Kimi-K3](https://huggingface.co/moonshotai/Kimi-K3) is Moonshot AI's ~2.8T-parameter +Mixture-of-Experts model (natively MXFP4/QAT, hybrid MLA + Kimi-Delta-Attention, 896 +experts). These are **colocated** (single-instance) vLLM serving recipes for **MI300X / +gfx942**, complementing MAD's existing single-node gfx950/MI355X K3 recipe. + +Why MI300X needs its own recipe: MI300X has 192 GB/GPU, so the ~1.5 TB checkpoint does +**not** fit a single 8-GPU node under TP8. All recipes here shard across **2 nodes (16× +MI300X)** using PP2×TP8 (~102 GB/GPU per node). gfx942 also requires +`VLLM_ROCM_USE_AITER_MLA=0` (the AITER MLA kernel is gfx950-only). + +## Image & model + +- **Image:** `amdsiloai/vllm:kimi-k3-mi325x-release-v2` — a gfx942 K3 vLLM build. Public + and anonymously pullable (`docker pull amdsiloai/vllm:kimi-k3-mi325x-release-v2`); built + for MI325X, also runs MI300X. Override with `-e IMAGE=` if you have a newer tag. +- **Model:** `moonshotai/Kimi-K3` (MXFP4). Place on local NVMe for fast load; pass via `MODEL_DIR`. + +## The recipes + +| Folder | Parallelism | Expert all2all | MoE path | Use when | +|--------|-------------|----------------|----------|----------| +| [`pp2xtp8/`](pp2xtp8/) | PP2×TP8, no EP | — | a16w4 | Simplest baseline; lowest single-user latency. | +| [`wideep_int4_allgather/`](wideep_int4_allgather/) | PP2×TP8, EP8/node | `allgather_reducescatter` (generic) | a8w4 (`AITER_SITUV2_A8W4=1`) | Expert-parallel without MoRI kernels. | +| [`wideep_int4_moriep/`](wideep_int4_moriep/) | PP2×TP8, EP8/node | `mori_low_latency` (**MoRI-EP**) | a8w4 (`AITER_SITUV2_A8W4=1`) | MoRI-EP all2all expert dispatch (intra-node EP group). | +| [`wideep_disagg_2p2d/`](wideep_disagg_2p2d/) | 2P/2D disagg, TP2×DP8 per pool → EP16, no PP | `mori_low_latency` (**MoRI-EP**) + **MoRIIO** KV/state transfer | MXFP4 | Prefill/decode disaggregation across 4 nodes. Highest concurrent throughput; see [`RESULTS.md`](wideep_disagg_2p2d/RESULTS.md). | + +The first three are colocated (single-instance, no prefill/decode split); the +fourth splits prefill and decode across two pools. + +**`wideep_disagg_2p2d/`** — prefill/decode disaggregated serve; connector fixes are baked +into the disagg Docker image (`VLLM_REF=kimi-k3-wideep-disagg-fullsource-v3`). See +[`RESULTS.md`](wideep_disagg_2p2d/RESULTS.md) for validated NIAH and throughput numbers. +**Pick by workload:** colocated recipes give lowest single-request latency; disagg gives +**5.7× throughput at concurrency 8** (7.3× at 16) at ~4× higher single-stream latency. + +> **EP scope:** the 896 experts split **8-way across each node's 8 GPUs** (112 experts/GPU → `[EP Rank x/8]`), and that EP8 group is replicated on each of the 2 pipeline stages. The expert all2all (incl. MoRI-EP) therefore runs **intra-node**; the only cross-node traffic is the PP activation hand-off, over NCCL. ("16" is the GPU count, not the EP width.) + +## Quick start + +Each recipe is self-contained (`run.sh` + `README.md` + `niah_probe.py`). Launch the +**worker (rank 1) first, then the head (rank 0)**: + +```bash +cd / +MODEL_DIR=/path/to/Kimi-K3-MXFP4 ROLE=worker MASTER= bash run.sh # node 1 +MODEL_DIR=/path/to/Kimi-K3-MXFP4 ROLE=head MASTER= bash run.sh # node 0 (API :8000) + +# verify +curl http://:8000/v1/models +python3 niah_probe.py --url http://:8000 --model kimi-k3 --ctx 8500 --depths 0.1,0.5,0.9 +``` + +See each recipe's `README.md` for its specific flags and env. + +## Common env (set in every run.sh) + +- `VLLM_ROCM_USE_AITER_MLA=0` — **required** on gfx942. +- `--trust-remote-code --reasoning-parser kimi_k3 --mm-encoder-tp-mode data + --safetensors-load-strategy prefetch`. +- ROCm 7.2.x: `HSA_ENABLE_IPC_MODE_LEGACY=0`, `PYTORCH_ALLOC_CONF` / + `PYTORCH_HIP_ALLOC_CONF=expandable_segments:False`. +- **RDMA fabric env is cluster-specific** (`NCCL_IB_HCA`, `NCCL_IB_GID_INDEX`, and the + `MORI_*` knobs in the moriep recipe) — adjust for your cluster's NICs/GIDs. + +Overridable per recipe: `IMAGE`, `MODEL_DIR`, `MASTER`, `PORT` (8000), `MAX_MODEL_LEN` +(10240), `MAX_NUM_SEQS` (8), `GPU_UTIL` (0.90). diff --git a/scripts/vllm/kimik3_mi300x/pp2xtp8/README.md b/scripts/vllm/kimik3_mi300x/pp2xtp8/README.md new file mode 100644 index 00000000..9ba570dd --- /dev/null +++ b/scripts/vllm/kimik3_mi300x/pp2xtp8/README.md @@ -0,0 +1,45 @@ +# Kimi-K3 (MXFP4) on MI300X / gfx942 — PP2×TP8 baseline + +Serves Kimi-K3 (MXFP4) across **2 nodes** (16× MI300X): tensor-parallel 8 within each +node, pipeline-parallel 2 across nodes. Each node holds half the layers (~102 GB/GPU); +a single 8-GPU node cannot fit the model + KV. This is the simplest, lowest-latency K3 +serve on MI300X, with no expert parallelism. See `../wideep_int4_moriep` for wide-EP. + +- Image: `amdsiloai/vllm:kimi-k3-mi325x-release-v2` (gfx942 K3 vLLM build; public, anonymous pull) +- Model: `moonshotai/Kimi-K3` (MXFP4), on local NVMe or NFS +- Colocated (single instance; no prefill/decode disaggregation) + +## Run (worker FIRST, then head) + +```bash +# on the worker node (rank 1): +MODEL_DIR=/path/to/Kimi-K3-MXFP4 ROLE=worker MASTER= bash run.sh + +# on the head node (rank 0, serves the API on :8000): +MODEL_DIR=/path/to/Kimi-K3-MXFP4 ROLE=head MASTER= bash run.sh +``` + +- `MASTER` = the head node's **eth0** IP (NCCL/PP bootstrap). +- `MODEL_DIR` = path to the Kimi-K3-MXFP4 weights. Prefer **local NVMe** over NFS (much faster load). +- First start recompiles gfx942 AITER kernels (a few minutes), then cached. + +## Verify + +```bash +curl http://:8000/v1/models +python3 niah_probe.py --url http://:8000 --model kimi-k3 --ctx 8500 --depths 0.1,0.5,0.9 +``` + +## Key env (set in run.sh) + +| Var | Value | Why | +|-----|-------|-----| +| `VLLM_ROCM_USE_AITER_MLA` | `0` | Required on gfx942 — the AITER MLA kernel is gfx950-only and asserts at TP8. | +| `NCCL_SOCKET_IFNAME` / `GLOO_SOCKET_IFNAME` | `eth0` | Control-plane NIC. | +| `NCCL_IB_HCA` | `mlx5_0,2,3,4,5,7,8,9` | 8× RDMA NICs (**cluster-specific — override for your fabric**). | +| `NCCL_IB_GID_INDEX` | `3` | RoCE GID (**cluster-specific**). | +| `HSA_ENABLE_IPC_MODE_LEGACY` | `0` | ROCm 7.2.x IPC. | +| `PYTORCH_(HIP_)ALLOC_CONF` | `expandable_segments:False` | Required on ROCm 7.2.x. | + +Overridable env: `IMAGE`, `MODEL_DIR`, `MASTER`, `PORT` (8000), `MAX_MODEL_LEN` (10240), +`MAX_NUM_SEQS` (8), `GPU_UTIL` (0.90). diff --git a/scripts/vllm/kimik3_mi300x/pp2xtp8/niah_probe.py b/scripts/vllm/kimik3_mi300x/pp2xtp8/niah_probe.py new file mode 100644 index 00000000..0fed8766 --- /dev/null +++ b/scripts/vllm/kimik3_mi300x/pp2xtp8/niah_probe.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 +"""Lightweight NIAH (needle-in-a-haystack) + basic accuracy probe for a served +Kimi-K3 endpoint. No deps beyond stdlib (urllib). Run from any node that can +reach the head IP:port. + +Usage: + python3 niah_probe.py --url http://:8000 --model kimi-k3 \ + --ctx 8000 --depths 0.1,0.5,0.9 +""" +import argparse, json, time, urllib.request, urllib.error + +FILLER = ("The quick brown fox jumps over the lazy dog. " + "Grand vistas of rolling hills stretched to the horizon. ") + +def chat(url, model, prompt, max_tokens=256, timeout=180): + body = json.dumps({ + "model": model, + "messages": [{"role": "user", "content": prompt}], + "max_tokens": max_tokens, "temperature": 0, + }).encode() + req = urllib.request.Request(url.rstrip("/") + "/v1/chat/completions", + data=body, headers={"Content-Type": "application/json"}) + t0 = time.time() + with urllib.request.urlopen(req, timeout=timeout) as r: + d = json.load(r) + dt = time.time() - t0 + ch = d["choices"][0]["message"] + txt = (ch.get("content") or "") + " " + (ch.get("reasoning") or "") + return txt.strip(), d.get("usage", {}), dt + +def make_haystack(approx_words, needle, depth): + # Build filler to approx word count; insert needle at fractional depth. + words = [] + while len(words) < approx_words: + words.extend(FILLER.split()) + n = len(words) + pos = int(n * depth) + needle_words = needle.split() + words[pos:pos] = needle_words + return " ".join(words) + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--url", required=True) + ap.add_argument("--model", default="kimi-k3") + ap.add_argument("--ctx", type=int, default=8000, help="approx haystack words") + ap.add_argument("--depths", default="0.1,0.5,0.9") + args = ap.parse_args() + + print(f"== sanity ==") + txt, usage, dt = chat(args.url, args.model, "Reply with only: OK", max_tokens=8) + print(f" resp={txt!r} usage={usage} {dt:.1f}s") + + secret = "The secret passcode is HELIOTROPE-7492." + question = ("\n\nQuestion: What is the secret passcode? " + "Answer with only the passcode.") + passed = 0; total = 0 + for depth in [float(x) for x in args.depths.split(",")]: + hay = make_haystack(args.ctx, secret, depth) + prompt = hay + question + try: + txt, usage, dt = chat(args.url, args.model, prompt, max_tokens=256) + except Exception as e: + print(f" depth={depth}: ERROR {e}") + total += 1; continue + ok = "HELIOTROPE-7492" in txt.upper() + passed += ok; total += 1 + print(f" depth={depth:>4}: {'PASS' if ok else 'FAIL'} " + f"(prompt_tok={usage.get('prompt_tokens')}, {dt:.1f}s) resp={txt[:60]!r}") + print(f"== NIAH {passed}/{total} passed ==") + +if __name__ == "__main__": + main() diff --git a/scripts/vllm/kimik3_mi300x/pp2xtp8/run.sh b/scripts/vllm/kimik3_mi300x/pp2xtp8/run.sh new file mode 100755 index 00000000..aa32dcfe --- /dev/null +++ b/scripts/vllm/kimik3_mi300x/pp2xtp8/run.sh @@ -0,0 +1,63 @@ +#!/bin/bash +# Kimi-K3 (MXFP4) on MI300X / gfx942 -- PP2xTP8 baseline (no expert parallelism). +# TP8 within each node, PP2 across 2 nodes -> each node holds half the layers +# (~102 GB/GPU); a single 8-GPU node cannot fit the model + KV. Simplest, lowest- +# latency K3 serve on MI300X. Colocated (single instance; no P/D disaggregation). +# +# Usage (worker FIRST, then head): +# ROLE=worker MASTER= bash run.sh # on node1 (rank1) +# ROLE=head MASTER= bash run.sh # on node0 (rank0, serves API) +set -euo pipefail + +IMAGE="${IMAGE:-amdsiloai/vllm:kimi-k3-mi325x-release-v2}" +MODEL_DIR="${MODEL_DIR:?set MODEL_DIR=}" +ROLE="${ROLE:?set ROLE=head|worker}" +MASTER="${MASTER:?set MASTER=}" +PORT="${PORT:-8000}" +MAX_MODEL_LEN="${MAX_MODEL_LEN:-10240}" +MAX_NUM_SEQS="${MAX_NUM_SEQS:-8}" +GPU_UTIL="${GPU_UTIL:-0.90}" +CONTAINER="k3_pp2tp8_${ROLE}" +LOGHOST="${LOGHOST:-$HOME/k3run/logs}"; mkdir -p "$LOGHOST" + +# Proven fabric env (from cluster_rdma_env_recommender.py) +BOOT_NIC="eth0" +IB_HCA="mlx5_0,mlx5_2,mlx5_3,mlx5_4,mlx5_5,mlx5_7,mlx5_8,mlx5_9" +GID_INDEX=3 + +if [ "$ROLE" = "head" ]; then + NODE_RANK=0; SERVE_EXTRA="--port ${PORT}"; HEADLESS="" +else + NODE_RANK=1; SERVE_EXTRA=""; HEADLESS="--headless" +fi + +echo "[pp2tp8] node=$(hostname -s) role=$ROLE rank=$NODE_RANK master=$MASTER" +[ -f "$MODEL_DIR/model.safetensors.index.json" ] || { echo "ERROR: model missing at $MODEL_DIR"; exit 1; } +docker rm -f "$CONTAINER" 2>/dev/null || true + +docker run -d --name "$CONTAINER" \ + --network host --ipc host \ + --device /dev/kfd --device /dev/dri --device /dev/infiniband --group-add video \ + --cap-add SYS_PTRACE --security-opt seccomp=unconfined --shm-size 128g \ + --ulimit memlock=-1 --ulimit stack=67108864 \ + -e VLLM_ROCM_USE_AITER_MLA=0 \ + -e NCCL_SOCKET_IFNAME=$BOOT_NIC -e GLOO_SOCKET_IFNAME=$BOOT_NIC \ + -e NCCL_IB_DISABLE=0 -e NCCL_IB_HCA=$IB_HCA -e NCCL_IB_GID_INDEX=$GID_INDEX \ + -e NCCL_IGNORE_CPU_AFFINITY=1 -e NCCL_DEBUG=WARN \ + -e HSA_ENABLE_IPC_MODE_LEGACY=0 -e HSA_NO_SCRATCH_RECLAIM=1 \ + -e PYTORCH_ALLOC_CONF=expandable_segments:False \ + -e PYTORCH_HIP_ALLOC_CONF=expandable_segments:False \ + -v "$MODEL_DIR":/model:ro -v "$LOGHOST":/logs \ + --entrypoint bash \ + "$IMAGE" -c " + vllm serve /model --served-model-name kimi-k3 \ + --tensor-parallel-size 8 --pipeline-parallel-size 2 \ + --distributed-executor-backend mp \ + --nnodes 2 --node-rank ${NODE_RANK} --master-addr ${MASTER} --master-port 29500 ${HEADLESS} \ + --trust-remote-code --reasoning-parser kimi_k3 --mm-encoder-tp-mode data \ + --safetensors-load-strategy prefetch \ + --max-model-len ${MAX_MODEL_LEN} --max-num-seqs ${MAX_NUM_SEQS} \ + --gpu-memory-utilization ${GPU_UTIL} ${SERVE_EXTRA} 2>&1 | tee /logs/vllm_pp2tp8_${ROLE}.log + " +echo "[pp2tp8] $ROLE started. log: $LOGHOST/vllm_pp2tp8_${ROLE}.log" +[ "$ROLE" = head ] && echo "[pp2tp8] health: curl http://${MASTER}:${PORT}/v1/models" diff --git a/scripts/vllm/kimik3_mi300x/wideep_disagg_2p2d/Dockerfile.kimik3_disagg b/scripts/vllm/kimik3_mi300x/wideep_disagg_2p2d/Dockerfile.kimik3_disagg new file mode 100644 index 00000000..75fa3036 --- /dev/null +++ b/scripts/vllm/kimik3_mi300x/wideep_disagg_2p2d/Dockerfile.kimik3_disagg @@ -0,0 +1,379 @@ +# CONTEXT {'gpu_vendor': 'AMD', 'guest_os': 'UBUNTU'} +############################################################################### +# +# MIT License +# +# Copyright (c) 2025 Advanced Micro Devices, Inc. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# +################################################################################# +# ============================================================================= +# vllm_disagg_inference.ubuntu.amd.Dockerfile +# ALL connectors in one image: moriio (TP + MoRI-EP wideEP) + rixl (NIXL TP + +# DeepEP wideEP). = the fullsource MoRI stack, plus a UCX/RIXL/rocSHMEM/DeepEP +# transport layer gated by --build-arg WITH_NIXL (default 1 = everything). +# +# docker build -f docker/vllm_disagg_inference.ubuntu.amd.Dockerfile \ +# -t /vllm-disagg:local . +# export DOCKER_IMAGE_NAME=/vllm-disagg:local +# +# WITH_NIXL=1 (default) => builds UCX + RIXL(+nixlbench) + rocSHMEM + DeepEP from +# source, so all four connector combos (moriio TP/wideEP, rixl NIXL TP, DeepEP +# wideEP) are present (~+30-45 min build vs WITH_NIXL=0). +# WITH_NIXL=0 => MoRI-EP only (moriio TP/wideEP + deepep-from-base); lean, faster. +# +# STATUS: built + validated on ci_base (WITH_NIXL=1). The moriio paths (TP + MoRI-EP +# wideEP, incl. full DeepSeek-V3) are live-proven. rixl+TP builds and NIXL initializes, +# but has an image-level PyNCCL all-reduce issue on this stack (tracked separately). +# (BASE_IMAGE is a gated nightly; override --build-arg BASE_IMAGE=... as needed.) +# ============================================================================= +# Reconstructs the validated v1.2.1 (mori121) runtime stack by applying the recipe's +# component pins ON TOP of the open ROCm vLLM ci_base, cloning each source from +# public Git (no local build-contexts). Mirrors dist-inf-cookbook +# Dockerfile.vllm.mori121_shareable: +# +# - BASE: rocm/vllm-dev:ci_base-0fcd9b99... (open ROCm 7.2 / cp312 CI base). +# - MoRI -> built from ROCm/MoRI @ v1.2.1 (BUILD_UMBP=OFF). +# - AITER -> 0.1.16.post3 prebuilt rocm7.2 wheel + flydsl 0.2.2; stale JIT wiped. +# - vLLM -> COMPILED from the Wide-EP multi-pod PD reference branch (the +# connector/router reference for the 2P2D DP=EP=16 topology). Full compile: it is +# a different commit than the base's, so a .py-only overlay would be ABI-mismatched. +# - RDMA fix (expandable_segments:False x2 + HSA_ENABLE_IPC_MODE_LEGACY=0) is NOT baked +# here — it lives in scripts/vllm_dissag/connectors/.env and the launcher +# forwards it via docker -e. ROCm 7.2.3 cannot dmabuf-export VMM memory, else MoRI +# RegisterRdmaMemoryRegion EFAULTs (errno 14) on the first disagg WRITE. +# - vllm-router (vllm-project/router PR#181 = DP-rank round-robin + 2P2D KV-notify +# dpfix) built in -> no external router binary needed. +# - validated recipe knobs baked as ENV. The MoRIIO disagg fixes (#39276 notify, +# #41751 LL split, DP-rank hash-failsafe) are native in this vLLM (no runtime patcher). +# +# Build context = repo root: +# docker build -f docker/vllm_disagg_inference.ubuntu.amd.Dockerfile -t / . +# +# BASE_IMAGE is the OPEN ROCm 7.2 / cp312 vLLM CI base. Everything Wide-EP (MoRI, +# rocSHMEM, DeepEP, UCX/RIXL) is built FROM SOURCE in the steps below, so this base +# only needs to supply ROCm 7.2.x + torch 2.11 + PyTorch. It is publicly pullable +# (`docker pull rocm/vllm-dev:ci_base-...`) so this recipe builds end-to-end with no +# gated images. It must share the ROCm/torch ABI of PROVEN_K3_IMAGE so the grafted +# AITER trees stay compatible. vLLM compile is long (~30-60 min). +# ============================================================================= + +# Image ARGs used by FROM must be declared BEFORE the first FROM (buildkit global +# scope). Declaring BASE_IMAGE after the first FROM scopes it to that stage only, +# so the second FROM resolves it blank -> "base name should not be blank". +ARG PROVEN_K3_IMAGE=amdsiloai/vllm:kimi-k3-mi325x-release-v2 +# Public open base — builds the full Wide-EP stack from source (steps 1-4b below). +ARG BASE_IMAGE=rocm/vllm-dev:ci_base-0fcd9b99cc9d63202da4c858d8ebc6582c9e2491 +# (internal-only alt base that pre-bakes MoRI 1.2.x, if you have registry access: +# rocmshared/pytorch-private:vllm-rocm_07_22_2026_shikpate_mori1.2.3 — then steps 1-4b +# are redundant/overridable. The public ci_base above is the supported default.) + +# K3-aware AITER donor stage (see step 5b): ships kimik3 tuned MXFP4 MoE configs, +# working MXFP4 codegen, flydsl 0.2.4 and aiter/ops/triton/conv. Same ROCm 7.2.3 + +# torch 2.11 base as our build base, so the grafted trees are ABI-compatible. +FROM ${PROVEN_K3_IMAGE} AS proven_k3_aiter + +FROM ${BASE_IMAGE} + +ENTRYPOINT [] +WORKDIR /app + +ARG GFX_COMPILATION_ARCH="gfx942" +ARG PYTORCH_ROCM_ARCH="gfx942" +ARG MAX_JOBS=32 +# NIXL/RIXL transport for the rixl connector. Default 1 => all connectors built +# (UCX/RIXL/rocSHMEM/DeepEP). Set --build-arg WITH_NIXL=0 for a lean MoRI-EP-only image. +ARG WITH_NIXL=1 +ARG NIC_COMPILATION_ARCH="cx7" + +# ----------------------------------------------------------------------------- +# 1. MoRI: replace the base's bundled MoRI with the validated ROCm/MoRI @ v1.2.1 +# (the version for the 06_29 mori121 image, dist-inf-cookbook +# Dockerfile.vllm.mori121_shareable). v1.2.1 carries the EP/RDMA correctness fixes +# plus the ROCm-7.2.3 dmabuf registration path used by the connector .env +# (expandable_segments:False). MoRI is JIT-built, so this swaps the JIT sources the +# kernels compile from at runtime. +# BUILD CONFIG: match the cookbook build — MORI_GPU_ARCHS=gfx942, BUILD_UMBP=OFF, +# DEFAULT NIC backends. Do NOT pass USE_IONIC=OFF / USE_BNXT=OFF: disabling NIC +# backends produced a MoRI that deadlocked at the cross-node EP all-to-all init. +# ----------------------------------------------------------------------------- +ARG MORI_REPO=https://github.com/ROCm/mori.git +ARG MORI_REF=v1.2.2 +ENV MORI_GPU_ARCHS=gfx942 +# Newer MoRI added the UMBP subsystem which requires gRPC (grpcpp/grpcpp.h) not +# present in this base; UMBP is unrelated to the EP dispatch/combine kernels, so +# disable it to avoid pulling in a gRPC build dependency. +ENV BUILD_UMBP=OFF BUILD_UMBP_SPDK=OFF +# Build/install matches dist-inf-cookbook Dockerfile.vllm.mori121_shareable for v1.2.1: +# `BUILD_UMBP=OFF pip install .` (default build isolation). apt/pip build tooling kept +# for bases that lack it; harmless where already present. +RUN sed -i 's|http://|https://|g' /etc/apt/sources.list 2>/dev/null || true && \ + sed -i 's|http://|https://|g' /etc/apt/sources.list.d/*.list 2>/dev/null || true && \ + apt-get update && apt-get install -y --no-install-recommends \ + git build-essential cmake ninja-build ccache libssl-dev pkg-config curl ca-certificates && \ + pip install meson==0.64.0 "pybind11[global]" tqdm prettytable && \ + pip uninstall -y amd_mori amd-mori amd-mori-nightly mori 2>/dev/null || true && \ + rm -rf /tmp/mori-src && \ + git clone --recursive "${MORI_REPO}" /tmp/mori-src && \ + cd /tmp/mori-src && git checkout "${MORI_REF}" && git submodule update --init --recursive && \ + BUILD_UMBP=OFF pip install --no-build-isolation . && \ + python3 -c "import mori, mori.io, mori.ops; print('MoRI OK at', mori.__path__[0])" && \ + mkdir -p /app && echo "MORI_REF=${MORI_REF}@$(git -C /tmp/mori-src rev-parse HEAD)" >> /app/versions.txt && \ + rm -rf /tmp/mori-src + +# ----------------------------------------------------------------------------- +# 2. AITER: install 0.1.16.post3 (prebuilt rocm7.2 wheel + flydsl 0.2.2), then +# invalidate the stale prewarmed AITER JIT cache compiled against the old .so. +# ----------------------------------------------------------------------------- +# K3 NOTE: use AITER 0.1.19 (NOT 0.1.16.post3). K3's vision tower imports +# aiter.ops.triton.conv.conv2d which first appears in 0.1.19; 0.1.16.post3 lacks +# it -> "No module named 'aiter.ops.triton.conv'". 0.1.19 (2026-07-27) also +# includes the #3658 top_k_top_p HSA-fault fix (merged 2026-06-16) needed for +# DP-EP disagg. The base image already ships 0.1.19.dev24 with conv+#3658, so +# this is effectively a no-op/keep — but pin the release wheel for reproducibility. +ARG AITER_VERSION=0.1.19 +ARG AITER_WHEEL_URL="https://github.com/ROCm/aiter/releases/download/v0.1.19/amd_aiter-0.1.19%2Brocm7.2.manylinux.2.28-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl" +RUN echo "Bumping AITER to ${AITER_VERSION} from ${AITER_WHEEL_URL}" && \ + _W="/tmp/$(basename "${AITER_WHEEL_URL}" | sed 's/%2B/+/g')" && \ + curl -fL --retry 3 --retry-delay 2 -o "${_W}" "${AITER_WHEEL_URL}" && \ + (pip uninstall -y amd_aiter amd-aiter aiter 2>/dev/null || true) && \ + pip install --no-deps "${_W}" && \ + pip install "flydsl==0.2.4" && \ + rm -f "${_W}" && \ + python3 - <<'PYEOF' +from importlib.metadata import version as v, PackageNotFoundError +vm = None +for n in ("amd-aiter", "amd_aiter", "aiter"): + try: vm = v(n); break + except PackageNotFoundError: pass +assert vm and vm.split("+", 1)[0].startswith("0.1.19"), f"AITER not 0.1.19: {vm!r}" +print("AITER OK:", vm) +PYEOF +RUN rm -rf /opt/vllm_cache/aiter_jit /root/.aiter && echo "cleared stale AITER JIT cache" && \ + echo "AITER_VERSION=${AITER_VERSION}" >> /app/versions.txt + +# ----------------------------------------------------------------------------- +# 3. vLLM: compile from source at the 06_29 validated Wide-EP WRITE-mode branch +# (matches the published dist-inf-cookbook mori121 image). Full source compile +# (the base ships a different commit). The MoRIIO disagg fixes (#39276 notify, +# #41751 LL split, DP-rank hash-failsafe) are native in this branch, so no runtime +# patcher is needed. Override VLLM_REF to rebuild a different commit; build only +# committed commits (no working-tree edits). +# ----------------------------------------------------------------------------- +# VLLM_REPO/REF are a PUBLIC GitHub repo + branch (the Wide-EP WRITE-mode vLLM the +# disagg image builds from): the K3 + MoRIIO connector branch with the folded +# connector fixes (mamba block-ids, remote_tp normalize, mamba N-1). Override to your +# own fork/branch. +ARG GH_TOKEN="" +ARG VLLM_REPO=https://github.com/raviguptaamd/vllm.git +# v3 branch = v2 (4-KV-group routing + multi-chunk compute-progress gate) PLUS +# the KDA gather sync-free fix that unblocks long context (>500K): removes a +# per-KDA-layer device->CPU sync that hung 750K/900K prefills. With this branch +# the image has ALL fixes BAKED IN; the runtime patchers in the recipe's +# patchers/ dir then detect "already applied" and no-op (idempotent). +ARG VLLM_REF=kimi-k3-wideep-disagg-fullsource-v3 +ENV VLLM_TARGET_DEVICE=rocm \ + PYTORCH_ROCM_ARCH=${PYTORCH_ROCM_ARCH} \ + MAX_JOBS=${MAX_JOBS} +RUN rm -rf /tmp/vllm-src && \ + _VURL="${VLLM_REPO}"; [ -n "${GH_TOKEN}" ] && _VURL=$(echo "${VLLM_REPO}" | sed "s|https://|https://x-access-token:${GH_TOKEN}@|"); \ + git clone "${_VURL}" /tmp/vllm-src && \ + cd /tmp/vllm-src && git checkout "${VLLM_REF}" && \ + echo "VLLM_REF=${VLLM_REF}@$(git rev-parse HEAD)" >> /app/versions.txt && \ + pip uninstall -y vllm 2>/dev/null || true && \ + MAX_JOBS="${MAX_JOBS:-32}" NVCC_THREADS="${NVCC_THREADS:-8}" \ + pip install --no-deps --no-build-isolation -v . && \ + python3 -c "import vllm; print('vLLM', vllm.__version__, 'from', vllm.__file__)" && \ + rm -rf /tmp/vllm-src + +# Cross-check MoRI + AITER survived the vLLM install (no silent downgrade). +RUN python3 - <<'PYEOF' +from importlib.metadata import version as v, PackageNotFoundError +def get(names): + for n in names: + try: return v(n) + except PackageNotFoundError: pass + return None +av = get(("amd-aiter", "amd_aiter", "aiter")) +assert av and av.split("+", 1)[0].startswith("0.1.19"), f"AITER not 0.1.19: {av!r}" +import mori, mori.io, mori.ops +print("Post-vLLM check OK: AITER", av, "+ MoRI importable") +PYEOF + +# ----------------------------------------------------------------------------- +# 4. vllm-router (DP-rank round-robin + MoRIIO connector) — built in, so NO +# external vllm-router binary is needed (leave ROUTER_BINARY unset). +# Source = vllm-project/router PR #181 branch, which now carries BOTH the +# round-robin DP-rank fix (11841c0d) AND the 2P2D KV-notify fix (6409ac1: +# remote_dp_rank_override + remote_dp_size). The KV-notify fix is REQUIRED: +# without it the 2P2D EP=16 run reproducibly wedges with "remote blocks never +# arrived" deferred-write expiries (decode notify targets the wrong DP rank). +# This is the exact source of the validated vllm-router-2p2d-dpfix binary. +# Pinned Rust toolchain (>=1.88: router deps time/home require rustc 1.88). +# ----------------------------------------------------------------------------- +ARG ROUTER_REPO=https://github.com/raviguptaamd/router.git +ARG ROUTER_REF=ravgupta/discovery-dp-rank-roundrobin +ARG RUST_TOOLCHAIN=1.88.0 +RUN if ! command -v cargo >/dev/null 2>&1; then \ + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain "${RUST_TOOLCHAIN}"; \ + fi && \ + export PATH="/root/.cargo/bin:${PATH}" && \ + rm -rf /tmp/vllm-router-src && \ + _RURL="${ROUTER_REPO}"; [ -n "${GH_TOKEN}" ] && _RURL=$(echo "${ROUTER_REPO}" | sed "s|https://|https://x-access-token:${GH_TOKEN}@|"); \ + git clone --filter=blob:none "${_RURL}" /tmp/vllm-router-src && \ + cd /tmp/vllm-router-src && git checkout "${ROUTER_REF}" && \ + cargo build --release && \ + install -m 755 target/release/vllm-router /usr/local/bin/vllm-router && \ + vllm-router --help 2>&1 | grep -q moriio && \ + echo "VLLM_ROUTER_REF=${ROUTER_REPO}@${ROUTER_REF}@$(git -C /tmp/vllm-router-src rev-parse HEAD)" >> /app/versions.txt && \ + rm -rf /tmp/vllm-router-src + +# ----------------------------------------------------------------------------- +# 4b. WITH_NIXL=1 (default): UCX + RIXL(+nixlbench) + rocSHMEM + DeepEP from source, +# so the rixl connector (NIXL TP + DeepEP wideEP) is present. Single guarded RUN so +# WITH_NIXL=0 skips it entirely (no layers, no cost). Build-verified on ci_base. +# ----------------------------------------------------------------------------- +ENV _ROCM_DIR=/opt/rocm \ + _UCX_SOURCE=https://github.com/ROCm/ucx.git \ + _UCX_BRANCH=da3fac2a \ + _UCX_INSTALL_DIR=/usr/local/ucx/ \ + _RIXL_SOURCE=https://github.com/ROCm/RIXL.git \ + _RIXL_BRANCH=f33a5599 \ + _RIXL_INSTALL_DIR=/usr/local/RIXL/install \ + _NIXLBENCH_INSTALL_DIR=/usr/local/RIXL +RUN if [ "${WITH_NIXL}" != "1" ]; then \ + echo "WITH_NIXL=${WITH_NIXL}: skipping UCX/RIXL/rocSHMEM/DeepEP (MoRI-EP + base DeepEP only)"; \ + else set -e && \ + echo "WITH_NIXL=1: building UCX + RIXL + rocSHMEM + DeepEP" && \ + apt-get update && apt-get install -y \ + autoconf automake libtool autogen pkg-config m4 gcc make \ + librdmacm-dev rdmacm-utils infiniband-diags ibverbs-utils perftest ethtool \ + libibverbs-dev rdma-core strace libgflags-dev \ + libaio-dev liburing-dev libcpprest-dev libgrpc-dev libgrpc++-dev \ + libprotobuf-dev protobuf-compiler-grpc wget && \ + pip install meson==0.64.0 "pybind11[global]" pyyaml && \ + # UCX + cd /tmp && git clone "${_UCX_SOURCE}" && cd ucx && git checkout "${_UCX_BRANCH}" && \ + ./autogen.sh && mkdir -p build && cd build && \ + ../configure --prefix="${_UCX_INSTALL_DIR}" --with-rocm="${_ROCM_DIR}" \ + --disable-go --disable-java --disable-assertions --enable-mt && \ + make -j && make install && \ + # googletest (RIXL dep) + cd /tmp && wget -q https://github.com/google/googletest/archive/refs/tags/v1.14.0.tar.gz && \ + tar -xzf v1.14.0.tar.gz && cd googletest-1.14.0 && mkdir -p build && cd build && \ + cmake -DBUILD_SHARED_LIBS=on .. && make -j && make install && \ + # RIXL + python bindings + cd /tmp && git clone "${_RIXL_SOURCE}" && cd RIXL && git checkout "${_RIXL_BRANCH}" && \ + meson setup build/ --prefix="${_RIXL_INSTALL_DIR}" -Ducx_path="${_UCX_INSTALL_DIR}" \ + -Ddisable_gds_backend=true -Dcudapath_inc="${_ROCM_DIR}/include" -Dcudapath_lib="${_ROCM_DIR}/lib" && \ + cd build && ninja && ninja install && cd /tmp/RIXL && \ + pip install --config-settings=setup-args="-Dcudapath_inc=${_ROCM_DIR}/include" \ + --config-settings=setup-args="-Dcudapath_lib=${_ROCM_DIR}/lib" \ + --config-settings=setup-args="-Ducx_path=${_UCX_INSTALL_DIR}" \ + --config-settings=setup-args="-Ddisable_gds_backend=true" . && \ + # rocSHMEM (DeepEP dep) + cd /tmp && git clone --no-checkout --filter=blob:none https://github.com/ROCm/rocm-systems.git && \ + cd rocm-systems && git sparse-checkout set --cone projects/rocshmem && git checkout develop && \ + mkdir -p /tmp/rocshmem-build && cd /tmp/rocshmem-build && \ + /tmp/rocm-systems/projects/rocshmem/scripts/build_configs/all_backends \ + -DUSE_EXTERNAL_MPI=OFF -DGPU_TARGETS="${GFX_COMPILATION_ARCH}" && \ + # DeepEP (build develop against the installed vLLM/torch) + cd /tmp && git clone https://github.com/ROCm/DeepEP.git && cd DeepEP && \ + PYTORCH_ROCM_ARCH="${GFX_COMPILATION_ARCH}" CFLAGS="-O3 -fPIC" \ + CXXFLAGS="-O3 -fPIC --offload-arch=${GFX_COMPILATION_ARCH}" HIP_CXX_FLAGS="-O3 -fPIC" \ + python3 setup.py --variant rocm --nic "${NIC_COMPILATION_ARCH}" build develop && \ + echo "WITH_NIXL build complete" >> /app/versions.txt && \ + rm -rf /tmp/ucx /tmp/googletest-1.14.0 /tmp/v1.14.0.tar.gz /tmp/rocm-systems /tmp/rocshmem-build; \ + fi +ENV LD_LIBRARY_PATH="/usr/local/ucx/lib:/usr/local/lib:/usr/local/RIXL/install/lib:${LD_LIBRARY_PATH}" \ + PATH="/usr/local/ucx/bin:${PATH}" + +# ----------------------------------------------------------------------------- +# 5. Cache locations (structural: WHERE the JIT/compile caches live in the image). +# These are the mount target for the launcher's persistent host JIT cache. +# ----------------------------------------------------------------------------- +# The image ships NO runtime recipe / tuning / platform ENV. By design, everything +# run-tunable is applied at launch, so this image stays a clean binary/library artifact +# and the same image serves any model/cluster without a rebuild: +# - model-serving recipe (KV_BLOCK_SIZE, KV_CACHE_DTYPE, *_CUDAGRAPH_MODE, *_MORI_BACKEND, +# GPU_MEMORY_UTILIZATION, KV_CACHE_MEMORY_BYTES, VLLM_ROCM_USE_AITER_MLA, ...) +# -> scripts/vllm_dissag/models.yaml (per-model env:, so dense vs MoE differ) +# - ROCm-7.2.3 GPU-RDMA platform env (expandable_segments:False x2, MORI_GPU_ARCHS, +# HSA_ENABLE_IPC_MODE_LEGACY=0, HSA_NO_SCRATCH_RECLAIM) and the MoRI/RDMA fabric +# tuning (MORI_RDMA_TC/SL, MORI_IB_GID_INDEX, MORI_NUM_QP_PER_PE, VLLM_MORIIO_*, ...) +# -> scripts/vllm_dissag/connectors/.env (cluster-editable, no rebuild) +# The slurm launcher forwards both via `docker -e` (platform env must reach PID 1 - +# PyTorch reads alloc-conf at import). Running this image WITHOUT the launcher: set the +# vars you need yourself (see connectors/moriio.env + models.yaml for the values). +ENV AITER_JIT_DIR=/opt/vllm_cache/aiter_jit \ + VLLM_CACHE_ROOT=/opt/vllm_cache/vllm \ + TRITON_CACHE_DIR=/opt/vllm_cache/triton \ + COMGR_CACHE_DIR=/opt/vllm_cache/comgr + +# ----------------------------------------------------------------------------- +# 5b. K3-AWARE AITER GRAFT (the crux for K3 MXFP4 MoE on gfx942). +# The 0.1.19 release wheel installed in step 2 has NO Kimi-K3 MoE tuning. At the +# K3 MoE profiling shape (gfx942, M=EPxmax_tokens=131072, N=3584, K=3072, Situv2, +# mxfp4) it finds no tuned FlyDSL config and falls back to a heuristic kernel +# (flydsl_moe1_abf16_wfp4_bf16_t64x128x256_w4_bnt0) whose buffer.load.lds intrinsic +# aborts LLVM: "Do not know how to expand this operator's operand!" -> the worker +# dies natively in determine_available_memory (no Python traceback), engine init fails. +# The proven colocated image (PROVEN_K3_IMAGE, same ROCm 7.2.3 + torch 2.11 base) +# ships a K3-AWARE AITER: configs/model_configs/kimik3_{a8w4,fp4}_tuned_fmoe.csv + a +# working MXFP4->CK/int4 routing + flydsl 0.2.4 + aiter/ops/triton/conv (the K3 +# vision-tower dep, our original reason for 0.1.19). Grafting its aiter + aiter_meta +# (prebuilt hsaco) + flydsl trees over the 0.1.19 install makes K3 MXFP4 MoE compile. +# The `proven_k3_aiter` stage is declared at the top of the file (next to the main +# FROM); this COPY stays in the main stage. Placed AFTER the vLLM/router/NIXL layers +# so a later edit here does not invalidate the ~40-min vLLM compile cache. +# ----------------------------------------------------------------------------- +RUN rm -rf /usr/local/lib/python3.12/dist-packages/aiter \ + /usr/local/lib/python3.12/dist-packages/aiter_meta \ + /usr/local/lib/python3.12/dist-packages/flydsl \ + /usr/local/lib/python3.12/dist-packages/aiter*.dist-info \ + /usr/local/lib/python3.12/dist-packages/amd_aiter*.dist-info 2>/dev/null || true +COPY --from=proven_k3_aiter /usr/local/lib/python3.12/dist-packages/aiter /usr/local/lib/python3.12/dist-packages/aiter +COPY --from=proven_k3_aiter /usr/local/lib/python3.12/dist-packages/aiter_meta /usr/local/lib/python3.12/dist-packages/aiter_meta +COPY --from=proven_k3_aiter /usr/local/lib/python3.12/dist-packages/flydsl /usr/local/lib/python3.12/dist-packages/flydsl +# The proven_k3 donor's flydsl tree is 0.2.2, which the COPY above drops OVER the +# pip flydsl==0.2.4 from step 2. But K3's int4 SiTUv2 MoE path +# (_setup_kernel_k3_situ_gfx942 -> compile_moe_gemm1) hard-requires flydsl>=0.2.4 +# (ImportError otherwise -> WorkerProc init fails -> pool never starts). Re-pin 0.2.4 +# AFTER the graft so it wins, and verify. The K3-tuned MoE configs live in aiter/aiter_meta +# (still grafted); flydsl is the codegen frontend and 0.2.4 is ABI-compatible with them. +RUN pip install --no-cache-dir --force-reinstall "flydsl==0.2.4" && \ + python3 -c "import importlib.metadata as m; v=m.version('flydsl'); assert v=='0.2.4', f'flydsl {v}!=0.2.4'; print('flydsl OK', v)" && \ + echo "FLYDSL_REPIN=0.2.4 (after proven_k3 graft)" >> /app/versions.txt +RUN rm -rf /opt/vllm_cache/aiter_jit /root/.aiter && \ + echo "AITER_GRAFT=proven_k3 (kimik3 tuned fmoe configs)" >> /app/versions.txt + +# ----------------------------------------------------------------------------- +# 6. CRITICAL: scrub build-time MoRI JIT state. The `import mori` verification +# steps above compile/lock MoRI EP kernels under /root/.mori/jit on THIS build +# host, leaving stale .hsaco.lock files (ep_internode_v1, ep_internode_v1ll, ...). +# At runtime on the cluster, MoriAll2AllManager finds those locks, waits on a +# build-in-progress whose owner PID is long gone, and DEADLOCKS at ep:0 init. +# A clean image ships /root/.mori empty -> runtime compiles fresh. +# Clearing these makes the from-source image boot clean on 2P2D/4P4D. +# ----------------------------------------------------------------------------- +RUN rm -rf /root/.mori /tmp/mori_jit_* && mkdir -p /root/.mori && \ + echo "JIT_SCRUBBED: /root/.mori + /tmp/mori_jit_* cleared at build end" >> /app/versions.txt + +RUN cat /app/versions.txt 2>/dev/null | tail -20 || true diff --git a/scripts/vllm/kimik3_mi300x/wideep_disagg_2p2d/README.md b/scripts/vllm/kimik3_mi300x/wideep_disagg_2p2d/README.md new file mode 100644 index 00000000..b4d84268 --- /dev/null +++ b/scripts/vllm/kimik3_mi300x/wideep_disagg_2p2d/README.md @@ -0,0 +1,220 @@ +# wideep_disagg_2p2d — Kimi-K3 MI300X 2P/2D EP16 MoRIIO disagg + +> **✅ VALIDATED.** Single-needle NIAH passes **deterministically through 300K +> tokens** (all depths) on this 2-prefill + 2-decode EP16 disagg serve. The +> previously-open decode-recall bug is fixed (two root causes: 4-KV-group block +> routing + multi-chunk prefill transfer). See [`RESULTS.md`](RESULTS.md) for the full NIAH + +> latency/throughput tables. +> +> **When to use this vs. colocated:** disagg (TP2×DP8) is for **concurrent +> throughput** and decode-latency isolation — 5.7× throughput at concurrency 8, +> 7.3× at 16. A *single* request runs on one DP replica (2 GPUs), so single-stream +> latency is ~4× a colocated PP2×TP8 serve (which spreads one request across all +> 16 GPUs). For low-latency interactive / low-QPS, use the colocated recipes +> ([`../pp2xtp8`](../pp2xtp8), [`../wideep_int4_moriep`](../wideep_int4_moriep)); +> for high-QPS / batch, use this. + +## What this is + +Prefill/decode **disaggregated** Kimi-K3 across **4 MI300X nodes**: + +``` + Prefill pool Decode pool + ┌───────────────┐ MoRIIO (RDMA) ┌───────────────┐ + │ PM + PW │ KV + KDA state │ DM + DW │ + │ 2 nodes = 16 │ ────────────────► │ 2 nodes = 16 │ + │ GPU, EP16 │ write + notify │ GPU, EP16 │ + └───────────────┘ └───────────────┘ + ▲ router (:30000) on PM fans requests P → D +``` + +- **Per pool**: TP2 × DP8 → **EP16** expert-parallel via **MoRI-EP** all2all. +- **No pipeline parallelism.** Disaggregation is the only cross-stage split; + everything else is DP/EP. +- **Connector**: MoRIIO in **WRITE** mode — prefill RDMA-writes both the MLA + attention KV *and* the Kimi-Delta-Attention (KDA/mamba) recurrent+conv state + into the decode engine's blocks, then notifies over TCP/ZMQ. + +K3 is hybrid: 24 MLA full-attention layers (paged fp8 KV) + 69 KDA layers +(recurrent + conv state). The two live in **separate** vLLM KV-cache groups, and +the connector transfers both — routing the KDA state by the *mamba* group's block +ids (see fix 1 below). + +## Quick start + +Runs from a control host that can `ssh` to all four nodes. Edit the node +IPs/hostnames (or export `PM_NODE/PM_IP/…`) and point `MODEL_DIR` at your +Kimi-K3-MXFP4 weights (local NVMe on every node recommended). + +```bash +cd wideep_disagg_2p2d/ + +# 1. build or obtain the disagg image (see "Image" below), tag it kimik3-wideep-disagg:latest +# 2. edit the 4 node IPs at the top of run_2p2d_launch.sh (or export PM_NODE=… etc.) + +MODEL_DIR=/path/to/Kimi-K3-MXFP4 \ +PM_NODE=… PM_IP=… PW_NODE=… PW_IP=… DM_NODE=… DM_IP=… DW_NODE=… DW_IP=… \ +AUTO_ROUTER=1 \ +K3_GROUP_ROUTING=1 K3_EXTRA_FIXES=1 LOAD_STRATEGY=lazy \ +MAX_MODEL_LEN=320000 MAX_NUM_BATCHED_TOKENS=2048 GPU_UTIL=0.85 \ +bash run_2p2d_launch.sh + +# watch both masters for "Application startup complete", then (AUTO_ROUTER does this +# for you) a single vllm-router comes up on PM:30000. Verify + probe: +curl http://:30000/v1/models +python3 niah_probe.py --url http://:30000 --model kimi-k3 \ + --ctx 50000 --depths 0.1,0.5,0.9 # PASS (deterministic to 300K) +``` + +### The winning config (flags that matter) + +| Flag | Value | Why | +|------|-------|-----| +| `K3_GROUP_ROUTING` | `1` | **Fix #1** — 4-KV-group block routing (always on). | +| `K3_EXTRA_FIXES` | `1` | **Fix #2** — multi-chunk compute-progress gate + all-group accumulation. Required for recall past `max_num_batched_tokens`. | +| `LOAD_STRATEGY` | `lazy` | `prefetch` double-loads RAM when the model is on tmpfs → decode OOM. | +| `MAX_NUM_BATCHED_TOKENS` | `2048` | Best measured throughput; raising to 8192 did **not** cut latency and hurt throughput (compute-bound prefill). | +| `MAX_MODEL_LEN` | `320000` | Needed for > 131K-token NIAH (default 131072 caps ~120K). Raise to `1000000` for the full native ctx. | +| `KV_CACHE_MEMORY_BYTES` | `8e9` | KV cache budget (pinned to skip a profile_run hang, NOT a mem limit). `8e9` = 542K tokens. **Raise to `40e9` (→ 2.84M tokens, ~72 GB/GPU free) for high throughput or single requests > ~600K.** See README KV cache notes below. | +| `GPU_UTIL` | `0.85` | 0.88 razor-misses KV headroom on some nodes. | +| `KV_CACHE_DTYPE` | `fp8` (default) | Transfer geometry assumes 1-byte elements; bf16 corrupts. | +| `PREFILL_BACKEND` | `mori_low_latency` | V1 high_throughput dispatch warmup crashes on this stack. | + +**Model on tmpfs (recommended):** loading the 1.5 TB checkpoint from a tmpfs RAM +cache (`/mnt/rammodel/Kimi-K3-MXFP4`) with `LOAD_STRATEGY=lazy` is ~2 min vs +~20 min from NFS (whose page cache gets evicted between runs). + +`run_2p2d_launch.sh` deploys the scripts + image to all four nodes, +starts **workers first, then masters**, then (with `AUTO_ROUTER=1`) waits for both +masters' `/v1/models` before launching exactly one router. `run_2p2d.sh` is the +per-node entrypoint (dispatches on `ROLE=prefill_master|prefill_worker| +decode_master|decode_worker`). + +## Load-bearing env (set by the launcher) + +| Var | Default | Meaning | +|-----|---------|---------| +| `TP_SIZE` / `DP_SIZE` / `DP_LOCAL` | 2 / 8 / 4 | TP2×DP8 → EP16 per pool; 4 DP ranks per node. | +| `PREFILL_BACKEND` | `mori_low_latency` | MoRI-EP all2all backend. | +| `KV_CACHE_MEMORY_BYTES` | 8e9 | Per-engine KV cache budget. | +| `PMASTER`/`DMASTER`/`PROXY_IP` | node IPs | Pool masters + router/proxy host. | +| `PREFILL_POD_HOSTS`/`DECODE_POD_HOSTS` | node IP lists | Pool membership. | +| `MODEL_DIR` | *(required)* | Kimi-K3-MXFP4 weights path (must exist on every node). | + +### RDMA fabric (overridable; validated defaults ON) + +`run_2p2d.sh` reads all fabric from env, so **nothing is hardcoded**, but the +defaults are the validated Broadcom **Thor2 (bnxt RoCE)** values so it works +out-of-the-box on the reference cluster: + +| Var | Default (Thor2) | Override for e.g. Mellanox | +|-----|-----------------|---------------------------| +| `SOCKET_IFNAME` | `eno0` | `eth0` | +| `NCCL_IB_HCA` / `RDMA_DEVICES` | `rdma0..rdma7` | `mlx5_0,mlx5_2,…,mlx5_9` | +| `IB_GID_INDEX` | `3` | your GID | +| `THOR2_BNXT_FIX` | `1` | `0` (non-Thor2) | + +`THOR2_BNXT_FIX=1` mounts the host **v34** `libibverbs`/`libbnxt_re` onto the +image's resolved soname (the image ships v59, but the Thor2 `bnxt_re` kernel +driver only accepts v34 → otherwise 0 RDMA devices). On non-bnxt fabric set +`THOR2_BNXT_FIX=0`. Example override: +```bash +SOCKET_IFNAME=eth0 NCCL_IB_HCA=mlx5_0,mlx5_2,mlx5_3,mlx5_4,mlx5_5,mlx5_7,mlx5_8,mlx5_9 \ +RDMA_DEVICES=mlx5_0,mlx5_2,mlx5_3,mlx5_4,mlx5_5,mlx5_7,mlx5_8,mlx5_9 \ +IB_GID_INDEX=3 THOR2_BNXT_FIX=0 bash run_2p2d_launch.sh + +## The connector fixes (folded into the vLLM source branch) + +The two root-cause fixes are **folded into vLLM source** on branch +`kimi-k3-wideep-disagg-fullsource-v2` of `raviguptaamd/vllm` (the `-v2` = the +base `fullsource` branch + these fixes baked in). The Dockerfile builds that +branch (`VLLM_REF=kimi-k3-wideep-disagg-fullsource-v2`), so the **image has the +fixes baked in**. The same fixes are baked into the vLLM source branch built by the Dockerfile. **The two root-cause fixes that make NIAH pass**: + +1. **4-KV-group block routing** (`apply_kimik3_moriio_group_routing.py`, always + on, `K3_GROUP_ROUTING=1`) — K3 has **4** KV-cache groups (3 KDA/mamba + 1 MLA); + the shipped connector hardcoded 2-group indices and sent MLA KV to mamba + block-ids. Fix carries all groups' block-ids end-to-end and routes each layer + by its own group. Fixes short (≤ 1 block) recall. +2. **Multi-chunk prefill transfer** (`apply_kimik3_chunk_gate_fix.py` + + `apply_kimik3_chunked_allgrp.py`, `K3_EXTRA_FIXES=1`) — the connector detected + the final prefill chunk by *block count*, which fires after chunk 1 when a + prompt fits in ≤ 1 padded block → only `max_num_batched_tokens` of KV crossed. + Fix gates on **compute progress** from `scheduler_output` (build map → entry + defer → accumulation final-detect → post-loop sweep). Removes the razor cliff + at `max_num_batched_tokens`; recall now scales to 300K. + +Pre-existing connector fixes (still required, always on): + +3. **mamba block-id routing** — transfer KDA/mamba state by the *mamba* KV-cache + group's block ids (superseded in the general case by fix 1; kept as fallback). +4. **remote_tp_size normalize** — degenerate `remote_tp_size ≤ 1` → `world_size`, + so KV fans out to **all** decode TP ranks (not just rank 0). +5. **mamba N−1 boundary** — producer computes through token N−1, decoder recomputes + token N (matches vLLM's nixl/mooncake hybrid-PD handling). + +## Image + +[`Dockerfile.kimik3_disagg`](Dockerfile.kimik3_disagg) is **fully self-contained and +builds from a public base** — no gated images required: + +- **Base** (`BASE_IMAGE`): `rocm/vllm-dev:ci_base-0fcd9b99...` — the open ROCm 7.2 / + cp312 vLLM CI base (publicly pullable). It only supplies ROCm + torch; the entire + Wide-EP stack is built **from source** on top: + - **MoRI** `ROCm/mori @ v1.2.2` (gfx942, `BUILD_UMBP=OFF`) — the EP all2all kernels; + - **AITER** `0.1.19` wheel + `flydsl 0.2.4`; + - **vLLM** compiled from `VLLM_REPO`/`VLLM_REF` = `raviguptaamd/vllm` branch + `kimi-k3-wideep-disagg-fullsource-v2` (the folded connector fixes above); + - **vllm-router** (DP-rank round-robin + MoRIIO KV-notify) built in; + - with `WITH_NIXL=1` (default): UCX + RIXL + rocSHMEM + DeepEP from source too. +- **K3 AITER graft** (`PROVEN_K3_IMAGE`): `amdsiloai/vllm:kimi-k3-mi325x-release-v2` + (public) — only the Kimi-K3 tuned MXFP4 MoE configs / conv kernels are copied from it. + +```bash +docker build -f Dockerfile.kimik3_disagg \ + --build-arg GH_TOKEN=$(gh auth token) \ + -t kimik3-wideep-disagg:latest . +``` + +Build args (`BASE_IMAGE`, `PROVEN_K3_IMAGE`, `MORI_REF`, `VLLM_REPO`, `VLLM_REF`, +`WITH_NIXL`, `GH_TOKEN`) let you override any component. Push the result to a registry +you control and set `HUB_IMAGE` for [`load_image.sh`](load_image.sh) to pull it onto +each node. **Note:** the image builds `VLLM_REF` from GitHub, so push the vLLM fork +branch before building for a reproducible image. + +## Tests + +| Script | What | +|--------|------| +| [`niah_probe.py`](niah_probe.py) | Single-needle NIAH via the router (`--ctx --depths`). The deliverable metric — PASS to 300K. | +| [`benchmark_niah.py`](benchmark_niah.py) | Stricter 10-needle multi-needle stress (`NIAH_WORDS=…`). | +| [`concurrency_bench.py`](concurrency_bench.py) | Concurrent throughput (req/s, tok/s, latency percentiles). | + +See [`RESULTS.md`](RESULTS.md) for the full NIAH sweep + latency/throughput tables. + +## Debugging (opt-in, all default OFF) + +Turn diagnostics up/down via env flags; none change default behavior: + +| Flag | Effect | +|------|--------| +| `K3_CHUNK_GATE_DEBUG=1` | Log the chunk-gate decision per request (`entry`/`accum`/`sweep`, computed/scheduled/npt). | +| `K3_XFER_PROBE=1` | Producer offsets + src checksum, decode read-block + dst checksum. | +| `K3_DECODE_RECV_PROBE=1` | Decode reads its own KV slot norm on write-completion (~0 = bytes didn't land). | +| `K3_KDA_STATE_PROBE=1` | KDA recurrent/conv state norm at the decode read slot. | +| `K3_WRITE_BC=1` / `K3_HS_BC=1` / `K3_INPUTS_PROBE=1` | Write-delivery / handshake-dial / decode-inputs breadcrumbs. | +| `K3_WRITE_FENCE=delay K3_WRITE_FENCE_MS=…` | Sender write→notify delay (investigative; did not fix the residual race). | + +**Verify a patch landed in-container** (`/patchers` is bind-mounted; a stale file +silently re-applies old behavior): +```bash +docker exec k3disagg_prefill_master bash -lc \ + 'B=/usr/local/lib/python3.12/dist-packages/vllm/distributed/kv_transfer/kv_connector/v1/moriio; \ + grep -c k3-group-routing $B/moriio_connector.py; grep -c _k3_prog $B/moriio_connector.py' +``` + +## Status + +**VALIDATED** — single-needle NIAH passes deterministically to 300K. See +[`STATUS.md`](STATUS.md) for root cause, the fix, and the known residual +(multi-needle write race at ≥ 20K, single-needle unaffected). diff --git a/scripts/vllm/kimik3_mi300x/wideep_disagg_2p2d/RESULTS.md b/scripts/vllm/kimik3_mi300x/wideep_disagg_2p2d/RESULTS.md new file mode 100644 index 00000000..4be89153 --- /dev/null +++ b/scripts/vllm/kimik3_mi300x/wideep_disagg_2p2d/RESULTS.md @@ -0,0 +1,99 @@ +# Results — Kimi-K3 MI300X 2P/2D EP16 MoRIIO disagg + +Serve: 2 prefill + 2 decode nodes; per-pool TP2×DP8 → EP16; MoRIIO WRITE; +router on the prefill master :30000. Config: `K3_GROUP_ROUTING=1 +K3_EXTRA_FIXES=1 LOAD_STRATEGY=lazy KV_CACHE_DTYPE=fp8 +MAX_NUM_BATCHED_TOKENS=2048 GPU_UTIL=0.85 PREFILL_BACKEND=mori_low_latency`, +model in tmpfs. `MAX_MODEL_LEN=320000` for the ≥ 150K rows (131072 otherwise). + +## Single-needle NIAH — the deliverable metric +`niah_probe.py`, needle = `HELIOTROPE-7492`, greedy (temp=0), depths +0.1 / 0.5 / 0.9. **All PASS, deterministic.** + +| context (tokens) | result | eval time / request | +|------------------|------------|---------------------| +| 10K | 3/3 PASS | 5.3s | +| 50K | 3/3 PASS | 19.5s | +| 100K | 3/3 PASS | ~47s | +| 120K | 3/3 PASS | ~54s | +| 150K | 3/3 PASS | ~84s | +| 200K | 3/3 PASS | ~88s | +| **300K** | **3/3 PASS** | **~150s** | +| 500K | 3/3 PASS | ~301s | +| 750K | PASS | ~542s | +| 900K | **PASS** | **~717s** | + +**Full native-context range (10K–900K) passes** with the KDA gather sync-free fix +(vLLM branch `-v3`; see below). Eval time is sub-quadratic (500K 301s → 750K 542s +→ 900K 717s). For ctx > ~120K raise `MAX_MODEL_LEN` (default 131072 caps ~120K, +use `1000000` for full native); for a single request > ~600K raise the KV cache +`KV_CACHE_MEMORY_BYTES=40e9` (2.84M-token cache; default `8e9` = 542K tokens is +too small for one 600K+ request). See [OPTIMIZATION.md](OPTIMIZATION.md). + +**The >500K unblock (KDA gather sync-free).** Contexts above ~500K previously +hung: `gather_initial_states` ran a diagnostic `bool((indices>=n).any())` per KDA +layer per prefill chunk, each forcing a device→CPU sync (full stream drain) — +~25k drains at 750K, stalling so hard it looked like a deadlock (py-spy: the DP +rank with real work stuck in that sync while the other DP ranks waited at the +batch-coordination all_reduce). The fix gates that diagnostic behind +`K3_KDA_GATHER_LOG=1` (default OFF); the index clamp still applies, so correctness +is unchanged. Folded into vLLM branch `kimi-k3-wideep-disagg-fullsource-v3` (the +Dockerfile builds it) and shipped as `patchers/apply_kimik3_kda_gather_nosync.py`. + +**One-time warmup:** a *fresh* serve pays a single aiter MLA-kernel JIT compile +(`fmha_fwd_hd192x128`, ~15 min) on the first ≥ ~200K-token request; cached +thereafter, so the per-request times above are the warm times. + +Note: `MAX_NUM_BATCHED_TOKENS=8192` was retested and still **corrupts generation** +on this stack (garbage output) — keep the default `2048`. + +Reproduce: +```bash +python3 niah_probe.py --url http://:30000 --model kimi-k3 \ + --ctx-list 10000,50000,100000,150000,200000,300000 --depths 0.1,0.5,0.9 \ + --timeout 400 +``` + +## Multi-needle stress — 10-animal (stricter) +`benchmark_niah.py`, 10 animals hidden across a word haystack, scored found/10. + +| context (words ≈ 1.3× tokens) | found/10 | +|-------------------------------|----------| +| 2000–5000 | 10/10 | +| 10000 | 10/10 | +| 15000 | 10/10 | +| 20000 | 9/10 (typical; 10/10 seen) | +| 50000 | 6–10/10 (nondeterministic) | +| 80000 | 9/10 | + +The occasional ≤ 1-needle miss at ≥ 20K is the residual RDMA write race +(see `STATUS.md` § Known residual). Single-needle retrieval is unaffected. + +## Latency vs. throughput +`concurrency_bench.py`, 20K-token prompts, 64 output tokens, batched=2048. + +| concurrency | throughput (req/s) | vs single-stream | p50 lat | p99 lat | +|-------------|--------------------|------------------|---------|---------| +| 1 | 0.062 | — | 16.2s | 16.2s | +| 8 | **0.353** | **5.7×** | 22.7s | 22.7s | +| 16 | **0.455** | **7.3×** | 34.5s | 35.1s | + +8 requests finish in 1.4× the wall time of 1 → 5.7× throughput. That is the DP8 +payoff; scaling flattens past 8 (= 8 replicas). **Single-stream latency is ~4× +a colocated PP2×TP8 EP8 serve** (1 request on 2 GPUs vs all 16) — architectural, +not tunable. + +### `max_num_batched_tokens` — a dead end (kept at 2048) +| metric | batched=2048 | batched=8192 | +|--------|--------------|--------------| +| 200K single-stream | ~88s | 112.8s (worse) | +| 20K conc=8 throughput | 0.353 req/s | 0.245 req/s (worse) | + +Raising it did not cut latency and hurt throughput (compute-bound prefill; larger +batches contend more across 8 replicas). **Keep `MAX_NUM_BATCHED_TOKENS=2048`.** + +## Use which serve +- **Colocated PP2×TP8 EP8** (`../wideep_int4_moriep`) — lowest single-request + latency; interactive / low-QPS. +- **Disagg TP2×DP8 EP16** (this) — highest concurrent throughput + decode-latency + isolation; batch / high-QPS. NIAH to 300K. diff --git a/scripts/vllm/kimik3_mi300x/wideep_disagg_2p2d/SESSION3_LEAN.md b/scripts/vllm/kimik3_mi300x/wideep_disagg_2p2d/SESSION3_LEAN.md new file mode 100644 index 00000000..824d7201 --- /dev/null +++ b/scripts/vllm/kimik3_mi300x/wideep_disagg_2p2d/SESSION3_LEAN.md @@ -0,0 +1,62 @@ +# Session 3 — Lean production image & upstream integration + +Goal: ship a **production** Kimi-K3 MI300X 2P/2D disagg image and MAD launcher path **without** +PR #193's 33 runtime patchers. Recipe = config only (`models.yaml` + `run_2p2d*.sh` / `vllm_dissag`). + +## Build (lean) + +```bash +cd scripts/vllm/kimik3_mi300x/wideep_disagg_2p2d/ +GH_TOKEN=$(gh auth token) ./build_lean.sh +# -> kimik3-wideep-disagg-lean:latest (WITH_NIXL=0, VLLM_REF=v3) +``` + +Same Dockerfile as investigation (`Dockerfile.kimik3_disagg`); lean = `WITH_NIXL=0` + pinned `VLLM_REF`. + +| Build | `WITH_NIXL` | Connectors | Use | +|-------|-------------|------------|-----| +| Investigation (default) | 1 | moriio + rixl + DeepEP | full MAD matrix | +| **Session 3 lean** | **0** | **moriio / MoRI-EP only** | K3 2P/2D production | + +## What's baked in (no runtime patchers) + +`VLLM_REF=kimi-k3-wideep-disagg-fullsource-v3` (`raviguptaamd/vllm`) includes: + +| Fix | Was patcher | Status in v3 image | +|-----|-------------|-------------------| +| 4-KV-group block routing | `apply_kimik3_moriio_group_routing.py` | baked | +| Multi-chunk prefill transfer | `apply_kimik3_chunk_gate_fix.py` + chunked allgrp | baked | +| KDA gather sync-free (>500K) | `apply_kimik3_kda_gather_nosync.py` | baked (v3 vs v2) | +| mamba block-id / remote_tp / N−1 | various | baked in connector branch | + +Runtime patchers in PR #193 should **no-op** against v3 (idempotent detect). Do not bind-mount `/patchers` for production. + +## MAD launcher alignment (done / todo) + +| Item | Status | Notes | +|------|--------|-------| +| `vllm_dissag` W1–W5 taxonomy + Slurm guards | **done** | job 223124 F17/F26 | +| `--quantization-config` JSON tokenization | **done** | F24 `_model_config_to_array` | +| JIT cache prefill/decode split (`run_interactive.sh`) | **done** | F25; matches slurm | +| Kimi decode `DECODE_CUDAGRAPH_MODE=NONE` | **done** | matches validated standalone launch | +| PIECEWISE decode cudagraph | **open** | hangs at capture 5/9 — fix in vLLM/image (F25) | +| Lean image default in docs/CI | **this file** | `build_lean.sh` | + +## Upstream PR checklist (MAD) + +1. **Recipes** — `scripts/vllm/kimik3_mi300x/` (config + README + RESULTS); no patchers in tree. +2. **Launcher** — `scripts/vllm_dissag/` Kimi entry in `models.yaml`, moriio K3 topology, tests. +3. **Docker** — document `build_lean.sh`; do **not** duplicate Dockerfile layers in-repo. +4. **vLLM sidecar PR** — `raviguptaamd/vllm` branch `kimi-k3-wideep-disagg-fullsource-v3` (or upstream cherry-picks). +5. **Validation** — 2P/2D short NIAH 9/9 + MAD live smoke (F18/F26); extended ctx deferred (TODO-P3). + +## Known open items (not Session 3 blockers) + +- **PIECEWISE decode cudagraph** — ITL win; blocked on capture hang (F25). Standalone launch defaults `DECODE_CG=NONE`. +- **Extended NIAH 500K–900K** — KV headroom @ TP2×DP8 (F23); TODO-P3. +- **Throughput vs PR targets** — F21; tuning separate from image lean. + +## Diff target + +Session 3 MAD PR should add **launcher + yaml + docs** (<2K lines). Image build stays in recipe folder; +binary produced out-of-band via `build_lean.sh` and pushed to operator registry. diff --git a/scripts/vllm/kimik3_mi300x/wideep_disagg_2p2d/benchmark_niah.py b/scripts/vllm/kimik3_mi300x/wideep_disagg_2p2d/benchmark_niah.py new file mode 100644 index 00000000..0cdd027e --- /dev/null +++ b/scripts/vllm/kimik3_mi300x/wideep_disagg_2p2d/benchmark_niah.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python3 +# Needle-in-a-haystack long-context retrieval test. +# Adapted from vllm-project/vllm issue #47042 (GLM-5.2 sparse-MLA decode collapse), +# generalized to run against any OpenAI-compatible endpoint / model. +# +# Env: +# NIAH_URL endpoint (default http://127.0.0.1:30000/v1/chat/completions) +# NIAH_MODEL model name/tag the server serves (required — the served path) +# NIAH_WORDS comma list of context sizes in words (default 2000,8000,20000,35000) +# NIAH_MAXTOK max_tokens for the answer (default 2048) +# NIAH_TIMEOUT per-request timeout seconds (default 1800) +import os, sys, json, random, urllib.request + +URL = os.environ.get("NIAH_URL", "http://127.0.0.1:30000/v1/chat/completions") +MODEL = os.environ.get("NIAH_MODEL", "") +WORDS = [int(x) for x in os.environ.get("NIAH_WORDS", "2000,8000,20000,35000").split(",") if x.strip()] +MAXTOK = int(os.environ.get("NIAH_MAXTOK", "2048")) +TIMEOUT = float(os.environ.get("NIAH_TIMEOUT", "1800")) + +FILLER = ( + "table chair window bottle pencil garden river mountain coffee planet " + "engine guitar pillow ticket basket candle market silver button orange " + "rocket napkin ladder pepper carpet helmet jacket mirror anchor pocket " + "branch copper saddle tunnel violin wallet zipper meadow cactus pebble" +).split() +ANIMALS = ["elephant", "giraffe", "kangaroo", "penguin", "dolphin", + "tiger", "rhinoceros", "octopus", "crocodile", "panda"] + +SYSTEM = ( + "You read a word list and pick out the animals. Reply with a single " + "comma-separated list of lowercase animal names. Output nothing else." +) + + +def make_haystack(n_words, seed=0): + rng = random.Random(seed) + words = [rng.choice(FILLER) for _ in range(n_words)] + step = max(n_words // (len(ANIMALS) + 1), 1) + for i, animal in enumerate(ANIMALS): + words[min((i + 1) * step, len(words) - 1)] = animal + return " ".join(words) + + +def run(n_words): + body = { + "model": MODEL, + "messages": [ + {"role": "system", "content": SYSTEM}, + {"role": "user", "content": "Find the animals in this list:\n\n" + make_haystack(n_words)}, + ], + "temperature": 0.0, + "max_tokens": MAXTOK, + } + data = json.dumps(body).encode() + req = urllib.request.Request(URL, data=data, headers={"Content-Type": "application/json"}) + try: + with urllib.request.urlopen(req, timeout=TIMEOUT) as r: + msg = json.loads(r.read())["choices"][0]["message"] + except Exception as e: + print("words=%6d ERROR %s" % (n_words, e), flush=True) + return None + text = ((msg.get("content") or "") + " " + (msg.get("reasoning_content") or "")).lower() + found = sorted(a for a in ANIMALS if a in text) + print("words=%6d found=%2d/10 %s" % (n_words, len(found), found), flush=True) + return len(found) + + +def main(): + if not MODEL: + print("NIAH_MODEL must be set (the served model path/name)", file=sys.stderr) + sys.exit(2) + print("=== NIAH retrieval test ===", flush=True) + print("url=%s model=%s sizes=%s" % (URL, MODEL, WORDS), flush=True) + results = {} + for n in WORDS: + results[n] = run(n) + print("=== NIAH summary ===", flush=True) + for n in WORDS: + v = results[n] + print(" words=%6d found=%s/10" % (n, "ERR" if v is None else v), flush=True) + + +if __name__ == "__main__": + main() diff --git a/scripts/vllm/kimik3_mi300x/wideep_disagg_2p2d/build_lean.sh b/scripts/vllm/kimik3_mi300x/wideep_disagg_2p2d/build_lean.sh new file mode 100755 index 00000000..00a81eae --- /dev/null +++ b/scripts/vllm/kimik3_mi300x/wideep_disagg_2p2d/build_lean.sh @@ -0,0 +1,39 @@ +#!/bin/bash +# Session 3 lean production image for Kimi-K3 MI300X 2P/2D MoRIIO disagg. +# MoRI-EP only (WITH_NIXL=0) — no UCX/RIXL/rocSHMEM/DeepEP build (~30–45 min saved). +# vLLM connector fixes are baked into VLLM_REF=v3; runtime patchers not required. +# +# Usage (from this directory): +# GH_TOKEN=$(gh auth token) ./build_lean.sh +# TAG=myregistry/kimik3-disagg-lean:v1 ./build_lean.sh +# +# See SESSION3_LEAN.md for upstream PR checklist and patcher migration. +set -euo pipefail + +DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +TAG="${TAG:-kimik3-wideep-disagg-lean:latest}" +VLLM_REF="${VLLM_REF:-kimi-k3-wideep-disagg-fullsource-v3}" +WITH_NIXL="${WITH_NIXL:-0}" +GH_TOKEN="${GH_TOKEN:-}" + +BUILD_ARGS=( + --build-arg "WITH_NIXL=${WITH_NIXL}" + --build-arg "VLLM_REF=${VLLM_REF}" +) +[[ -n "$GH_TOKEN" ]] && BUILD_ARGS+=(--build-arg "GH_TOKEN=${GH_TOKEN}") + +echo "=== Session 3 lean build ===" +echo " Dockerfile: Dockerfile.kimik3_disagg" +echo " TAG: ${TAG}" +echo " WITH_NIXL: ${WITH_NIXL}" +echo " VLLM_REF: ${VLLM_REF}" +echo "" + +docker build -f "${DIR}/Dockerfile.kimik3_disagg" \ + "${BUILD_ARGS[@]}" \ + -t "${TAG}" \ + "${DIR}" + +echo "" +echo "=== done: ${TAG} ===" +echo "Push and set DOCKER_IMAGE_NAME=${TAG} for vllm_dissag / run_2p2d." diff --git a/scripts/vllm/kimik3_mi300x/wideep_disagg_2p2d/concurrency_bench.py b/scripts/vllm/kimik3_mi300x/wideep_disagg_2p2d/concurrency_bench.py new file mode 100644 index 00000000..7494f123 --- /dev/null +++ b/scripts/vllm/kimik3_mi300x/wideep_disagg_2p2d/concurrency_bench.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +"""Concurrency / throughput bench for the EP16 2P/2D disagg serve. + +Fires N identical requests concurrently at the router and reports wall time, +throughput (req/s and output tok/s), and per-request latency percentiles. +This is the metric disagg is FOR (DP8 = 8 concurrent replicas), unlike +single-stream NIAH. + +Usage: concurrency_bench.py [max_out] + e.g. concurrency_bench.py 20000 8 64 +""" +import os, sys, json, time, urllib.request, concurrent.futures as cf + +# Router endpoint. Override with ROUTER_URL, e.g. +# ROUTER_URL=http://:30000/v1/completions +ROUTER = os.environ.get("ROUTER_URL", "http://127.0.0.1:30000/v1/completions") +FILLER = ("The quick brown fox jumps over the lazy dog near the riverbank while " + "the morning sun rises over the distant mountains and birds sing. ") + +def make_prompt(ctx_tokens): + body = (FILLER * (ctx_tokens * 4 // len(FILLER) + 1))[:ctx_tokens * 4] + return body + "\n\nSummarize the above in one word:" + +def one(prompt, max_out): + data = json.dumps({"model": "kimi-k3", "prompt": prompt, + "max_tokens": max_out, "temperature": 0}).encode() + req = urllib.request.Request(ROUTER, data=data, + headers={"Content-Type": "application/json"}) + t0 = time.time() + try: + with urllib.request.urlopen(req, timeout=1800) as r: + out = json.load(r) + dt = time.time() - t0 + n = out.get("usage", {}).get("completion_tokens", max_out) + return dt, n, True + except Exception as e: + return time.time() - t0, 0, False + +def pct(xs, p): + if not xs: return 0.0 + xs = sorted(xs); i = min(len(xs) - 1, int(p / 100 * len(xs))) + return xs[i] + +def main(): + ctx = int(sys.argv[1]) if len(sys.argv) > 1 else 20000 + conc = int(sys.argv[2]) if len(sys.argv) > 2 else 8 + max_out = int(sys.argv[3]) if len(sys.argv) > 3 else 64 + prompt = make_prompt(ctx) + print(f"ctx={ctx}tok concurrency={conc} max_out={max_out}", flush=True) + t0 = time.time() + with cf.ThreadPoolExecutor(max_workers=conc) as ex: + res = list(ex.map(lambda _: one(prompt, max_out), range(conc))) + wall = time.time() - t0 + lats = [d for d, n, ok in res if ok] + okn = sum(1 for _, _, ok in res if ok) + outtok = sum(n for _, n, ok in res if ok) + print(f" ok={okn}/{conc} wall={wall:.1f}s", flush=True) + print(f" throughput: {okn/wall:.3f} req/s | {outtok/wall:.1f} out-tok/s", flush=True) + print(f" latency: mean={sum(lats)/len(lats):.1f}s p50={pct(lats,50):.1f}s " + f"p99={pct(lats,99):.1f}s min={min(lats):.1f}s max={max(lats):.1f}s", flush=True) + +if __name__ == "__main__": + main() diff --git a/scripts/vllm/kimik3_mi300x/wideep_disagg_2p2d/load_image.sh b/scripts/vllm/kimik3_mi300x/wideep_disagg_2p2d/load_image.sh new file mode 100755 index 00000000..f1bfa7e9 --- /dev/null +++ b/scripts/vllm/kimik3_mi300x/wideep_disagg_2p2d/load_image.sh @@ -0,0 +1,55 @@ +#!/bin/bash +# Self-restoring loader for the K3 disagg image. Ensures TAG is present on this node, +# trying sources in order: +# 1. already present locally -> done +# 2. docker pull from a registry you control (set HUB_IMAGE + DOCKER_USER/DOCKER_PAT) +# 3. docker load from a tar (offline fallback; set TAR) +# 4. build from Dockerfile.kimik3_disagg (last resort; needs base image + gh token) +# Usage: bash load_image.sh (env: TAG, HUB_IMAGE, DOCKER_USER, DOCKER_PAT, TAR) +# +# The image is built from Dockerfile.kimik3_disagg (see README). Push it to your own +# registry and point HUB_IMAGE at it, or rely on the local build in step 4. +set -euo pipefail + +TAG="${TAG:-kimik3-wideep-disagg:latest}" +HUB_IMAGE="${HUB_IMAGE:-}" # e.g. /kimik3-wideep-disagg:latest +DOCKER_USER="${DOCKER_USER:-}" +DOCKER_PAT="${DOCKER_PAT:-}" # set to enable authed pull +TAR="${TAR:-}" # optional: path to a saved image tar + +_have() { docker image inspect "$1" >/dev/null 2>&1; } + +# 1. already present (either the local tag or the hub tag) +if _have "$TAG"; then echo "[load] $(hostname -s): $TAG already present"; exit 0; fi +if [ -n "$HUB_IMAGE" ] && _have "$HUB_IMAGE"; then + docker tag "$HUB_IMAGE" "$TAG" 2>/dev/null || true + echo "[load] $(hostname -s): $HUB_IMAGE present -> tagged $TAG"; exit 0 +fi + +# 2. pull from a registry you control (only if HUB_IMAGE is set) +if [ -n "$HUB_IMAGE" ]; then + echo "[load] $(hostname -s): pulling $HUB_IMAGE ..." + if [ -n "$DOCKER_PAT" ]; then + echo "$DOCKER_PAT" | docker login -u "$DOCKER_USER" --password-stdin >/dev/null 2>&1 || true + fi + if docker pull "$HUB_IMAGE" 2>&1 | tail -1; then + if _have "$HUB_IMAGE"; then + docker tag "$HUB_IMAGE" "$TAG" 2>/dev/null || true + echo "[load] $(hostname -s): pulled + tagged $TAG"; exit 0 + fi + fi +fi + +# 3. offline tar fallback +if [ -f "$TAR" ]; then + echo "[load] $(hostname -s): pull failed; loading $TAR ..." + docker load -i "$TAR" 2>&1 | tail -1 + _have "$TAG" && { echo "[load] $(hostname -s): loaded $TAG from tar"; exit 0; } +fi + +# 4. build from source (last resort) +echo "[load] $(hostname -s): ERROR: could not obtain $TAG from hub or tar." +echo " Rebuild: docker build -f Dockerfile.kimik3_disagg \\" +echo " --build-arg MORI_REF=v1.2.2 --build-arg WITH_NIXL=0 \\" +echo " --build-arg GH_TOKEN=\$(gh auth token) -t $TAG ." +exit 1 diff --git a/scripts/vllm/kimik3_mi300x/wideep_disagg_2p2d/niah_probe.py b/scripts/vllm/kimik3_mi300x/wideep_disagg_2p2d/niah_probe.py new file mode 100644 index 00000000..8179e151 --- /dev/null +++ b/scripts/vllm/kimik3_mi300x/wideep_disagg_2p2d/niah_probe.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 +"""Lightweight NIAH (needle-in-a-haystack) + basic accuracy probe for a served +Kimi-K3 endpoint. No deps beyond stdlib (urllib). Run from any node that can +reach the head IP:port. + +Usage: + python3 niah_probe.py --url http://:8000 --model kimi-k3 \ + --ctx 8000 --depths 0.1,0.5,0.9 +""" +import argparse, json, time, urllib.request, urllib.error + +FILLER = ("The quick brown fox jumps over the lazy dog. " + "Grand vistas of rolling hills stretched to the horizon. ") + +def chat(url, model, prompt, max_tokens=256, timeout=180): + body = json.dumps({ + "model": model, + "messages": [{"role": "user", "content": prompt}], + "max_tokens": max_tokens, "temperature": 0, + }).encode() + req = urllib.request.Request(url.rstrip("/") + "/v1/chat/completions", + data=body, headers={"Content-Type": "application/json"}) + t0 = time.time() + with urllib.request.urlopen(req, timeout=timeout) as r: + d = json.load(r) + dt = time.time() - t0 + ch = d["choices"][0]["message"] + txt = (ch.get("content") or "") + " " + (ch.get("reasoning") or "") + return txt.strip(), d.get("usage", {}), dt + +def make_haystack(approx_words, needle, depth): + # Build filler to approx word count; insert needle at fractional depth. + words = [] + while len(words) < approx_words: + words.extend(FILLER.split()) + n = len(words) + pos = int(n * depth) + needle_words = needle.split() + words[pos:pos] = needle_words + return " ".join(words) + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--url", required=True) + ap.add_argument("--model", default="kimi-k3") + ap.add_argument("--ctx", type=int, default=8000, help="approx haystack words") + ap.add_argument("--ctx-list", default=None, + help="comma list of sizes to sweep (overrides --ctx), e.g. " + "10000,50000,100000,200000,300000") + ap.add_argument("--depths", default="0.1,0.5,0.9") + ap.add_argument("--timeout", type=float, default=600, help="per-request seconds") + args = ap.parse_args() + + print(f"== sanity ==") + txt, usage, dt = chat(args.url, args.model, "Reply with only: OK", max_tokens=8) + print(f" resp={txt!r} usage={usage} {dt:.1f}s") + + secret = "The secret passcode is HELIOTROPE-7492." + question = ("\n\nQuestion: What is the secret passcode? " + "Answer with only the passcode.") + sizes = [int(x) for x in args.ctx_list.split(",")] if args.ctx_list else [args.ctx] + depths = [float(x) for x in args.depths.split(",")] + passed = 0; total = 0 + for ctx in sizes: + for depth in depths: + hay = make_haystack(ctx, secret, depth) + prompt = hay + question + try: + txt, usage, dt = chat(args.url, args.model, prompt, + max_tokens=256, timeout=args.timeout) + except Exception as e: + print(f" ctx={ctx:>7} depth={depth}: ERROR {e}", flush=True) + total += 1; continue + ok = "HELIOTROPE-7492" in txt.upper() + passed += ok; total += 1 + print(f" ctx={ctx:>7} depth={depth:>4}: {'PASS' if ok else 'FAIL'} " + f"(prompt_tok={usage.get('prompt_tokens')}, {dt:.1f}s) " + f"resp={txt[:50]!r}", flush=True) + print(f"== NIAH {passed}/{total} passed ==") + +if __name__ == "__main__": + main() diff --git a/scripts/vllm/kimik3_mi300x/wideep_disagg_2p2d/run_2p2d.sh b/scripts/vllm/kimik3_mi300x/wideep_disagg_2p2d/run_2p2d.sh new file mode 100755 index 00000000..25091c22 --- /dev/null +++ b/scripts/vllm/kimik3_mi300x/wideep_disagg_2p2d/run_2p2d.sh @@ -0,0 +1,221 @@ +#!/bin/bash +# Kimi-K3 MXFP4 2P/2D wide-EP DISAGGREGATED serve: DP/EP16 per role, MoRI-EP +# (mori all2all) + MoRIIO connector (prefill->decode KV + KDA state transfer). +# +# Topology (4 nodes, 8 GPU each = EP16 per pool): +# Prefill pool: P-master (rank0, proxy+kv_producer) + P-worker (rank8, headless) +# Decode pool: D-master (kv_consumer) + D-worker (headless) +# Run this per node with ROLE + the shared *_ADDR env set (see run_2p2d_launch.sh). +# +# Applies connector fixes baked into the disagg image (VLLM_REF=v3). +set -euo pipefail + +IMAGE="${IMAGE:-kimik3-wideep-disagg:latest}" +MODEL_DIR="${MODEL_DIR:?set MODEL_DIR to your Kimi-K3-MXFP4 weights path}" +ROLE="${ROLE:?ROLE=prefill_master|prefill_worker|decode_master|decode_worker}" +PMASTER="${PMASTER:?prefill master eth0 IP}" +DMASTER="${DMASTER:?decode master eth0 IP}" +PROXY_IP="${PROXY_IP:-$PMASTER}" +LOGHOST="${LOGHOST:-$HOME/k3disagg/logs}"; mkdir -p "$LOGHOST" +MAX_MODEL_LEN="${MAX_MODEL_LEN:-10240}" +# Bound the K3 MoE profiling M. On gfx942 the tuned FlyDSL a8w4 configs are +# gfx950-only + unsharded (896 experts), so our EP16 (56-expert) shard ALWAYS +# falls to the heuristic FlyDSL kernel. That heuristic crashes LLVM codegen +# ("Do not know how to expand this operator's operand") at the giant profiling +# shape sorted-M=131072 (= max_num_batched_tokens 16384 x topk 8). Shrinking +# max-num-batched-tokens shrinks the profiling M so the heuristic kernel compiles. +MAX_NUM_BATCHED_TOKENS="${MAX_NUM_BATCHED_TOKENS:-2048}" +# K3 MoE requant path. gfx942 has NO scaled-MXFP4 MFMA and the a16w4 SiTUv2 +# heuristic FlyDSL kernel CANNOT codegen on gfx942 (LLVM ExpandIntegerOperand on +# a 128-bit buffer->LDS async load; all tuned a8w4 configs are gfx950-only). The +# PROVEN-coherent colocated path (logbook: NIAH 3/3 @9600) requants MoE to +# packed-int4 and runs it through Situv2 (dtype torch.int4, per_1x32) with +# AITER_SITUV2_A8W4=1 + AITER PR#4471 (SiTUv2 in the int4 stage1 epilogue). +QUANT_CONFIG="${QUANT_CONFIG:-{\"moe\":{\"weight\":\"int4_per_group_32\"}}}" +GPU_UTIL="${GPU_UTIL:-0.88}" +# --- RDMA fabric (OVERRIDABLE) ----------------------------------------------- +# Defaults are validated Broadcom Thor2 (bnxt RoCE) values: +# ibv device names rdma0..rdma7, host NIC eno0, GID index 3. On a DIFFERENT +# fabric (e.g. Mellanox mlx5) override these, e.g.: +# NCCL_IB_HCA=mlx5_0,mlx5_2,mlx5_3,mlx5_4,mlx5_5,mlx5_7,mlx5_8,mlx5_9 \ +# RDMA_DEVICES=mlx5_0,mlx5_2,... SOCKET_IFNAME=eth0 IB_GID_INDEX=3 THOR2_BNXT_FIX=0 +SOCKET_IFNAME="${SOCKET_IFNAME:-eth0}" +NCCL_IB_HCA_VAL="${NCCL_IB_HCA:-mlx5_0,mlx5_2,mlx5_3,mlx5_4,mlx5_5,mlx5_7,mlx5_8,mlx5_9}" +RDMA_DEVICES="${RDMA_DEVICES:-mlx5_0,mlx5_2,mlx5_3,mlx5_4,mlx5_5,mlx5_7,mlx5_8,mlx5_9}" +IB_GID_INDEX="${IB_GID_INDEX:-3}" +# Thor2 bnxt libibverbs ABI fix (host v34 driver vs image v59). Set THOR2_BNXT_FIX=1 +# on Broadcom Thor2; default OFF for Mellanox mlx5 (OCI). +THOR2_BNXT_FIX="${THOR2_BNXT_FIX:-0}" +THOR2_LIBIBVERBS_HOST="${THOR2_LIBIBVERBS_HOST:-/usr/lib/x86_64-linux-gnu/libibverbs.so.1.14.39.0}" +THOR2_LIBIBVERBS_IMG="${THOR2_LIBIBVERBS_IMG:-/usr/lib/x86_64-linux-gnu/libibverbs.so.1.16.62.0}" +THOR2_BNXT_HOST="${THOR2_BNXT_HOST:-/usr/local/lib/libbnxt_re-rdmav34.so}" +THOR2_BNXT_IMG="${THOR2_BNXT_IMG:-/usr/lib/x86_64-linux-gnu/libibverbs/libbnxt_re-rdmav34.so}" +if [ "${THOR2_BNXT_FIX}" = "1" ]; then + BNXT_MOUNTS="-v ${THOR2_LIBIBVERBS_HOST}:${THOR2_LIBIBVERBS_IMG}:ro -v ${THOR2_BNXT_HOST}:${THOR2_BNXT_IMG}:ro" +else + BNXT_MOUNTS="" +fi +# ----------------------------------------------------------------------------- +# Skip the boot memory-profiling forward (profile_run) entirely by pinning KV +# cache size — mirrors DeepSeek #181 (which used it for an AITER fp8 profiling +# bug). profile_run's dummy forward hangs under tp8xDP2 + mori all2all (sampler +# gather / _sync_device all2all deadlocks); pinning KV bytes bypasses all of it. +# At TP2/DP8 weights are ~137.5 GiB/GPU + 16 GiB MoRI heap, so KV room is tighter +# (~10 GiB) than at TP8 (was 14e9). Pin 8 GiB — ample for K3's tiny MLA KV +# (kv_lora_rank=512, only 24/93 full-attn layers; ~13.5 KiB/tok fp8 => ~600k tok). +KV_CACHE_MEMORY_BYTES="${KV_CACHE_MEMORY_BYTES:-8000000000}" +# Wide-EP shape per pool: TP2 x DP8 -> EP16, no PP (per user: "No PP — disagg via +# MoRIIO, DP/EP via MoRI-EP"). 16 GPUs/pool (2 nodes x 8). world=16 => EP16 (896/16 +# =56 experts/GPU via MoRI-EP all2all). DP8 = 8 independent attention streams (the +# 69/93 KDA recurrent layers stay DP-local => best decode throughput); TP2 shards +# the 106.5 GiB replicated attn+shared-expert weight to 53.3 GiB/GPU so it FITS +# (pure TP1/DP16 = 190.7 GiB weights > 192 GiB HBM; see Confluence 1830010189). +# Per-GPU: experts 84.2 + repl 53.3 = 137.5 GiB weights + 16 GiB MoRI heap. DP_SIZE +# is the GLOBAL dp count (=world/TP=16/2=8); DP_LOCAL=4 dp ranks/node (8 GPUs/2). +TP_SIZE="${TP_SIZE:-2}" +DP_SIZE="${DP_SIZE:-8}" +DP_LOCAL="${DP_LOCAL:-4}" +SERVE_PORT=20005; RPC_PORT=13345 +# JIT cache persistence (best-practice): host dir keyed per image AND per role +# CLASS. Prefill (mori_high_throughput + cudagraph NONE) and decode +# (mori_low_latency + PIECEWISE) compile DIFFERENT kernel variants under the SAME +# aiter .so filenames (module_moe_asm/moe_sorting_opus/...). Node-local nvme keeps +# prefill (025/043) and decode (047/048) caches physically separate; the per-CLASS +# subdir also prevents collision if a node ever hosts both roles. Do NOT put this +# on a SHARED FS (would let prefill/decode clobber each other's same-named .so). +ROLE_CLASS="${ROLE%%_*}" # prefill | decode +JIT_HOST="${JIT_HOST:-/tmp/$USER/vllm_jit_cache/k3disagg_${ROLE_CLASS}}"; mkdir -p "$JIT_HOST" + +VLLM_SP=/usr/local/lib/python3.12/dist-packages/vllm + +# Per-pool topology: TP2 x DP8 -> EP16 (see the TP_SIZE/DP_SIZE block above). +# Master node hosts DP ranks 0..DP_LOCAL-1; worker node hosts DP_LOCAL..2*DP_LOCAL-1 +# (=> --data-parallel-start-rank ${DP_LOCAL}). gpu-util 0.88 for the 16 GiB MoRI +# shmem heap reserved before the vLLM snapshot. +case "$ROLE" in + prefill_master) DP_ADDR=$PMASTER; KV_ROLE=kv_producer; BACKEND=mori_high_throughput; CG=NONE; HEADLESS=""; START="" ;; + prefill_worker) DP_ADDR=$PMASTER; KV_ROLE=kv_producer; BACKEND=mori_high_throughput; CG=NONE; HEADLESS="--headless"; START="--data-parallel-start-rank ${DP_LOCAL}" ;; + decode_master) DP_ADDR=$DMASTER; KV_ROLE=kv_consumer; BACKEND=mori_low_latency; CG=PIECEWISE; HEADLESS=""; START="" ;; + decode_worker) DP_ADDR=$DMASTER; KV_ROLE=kv_consumer; BACKEND=mori_low_latency; CG=PIECEWISE; HEADLESS="--headless"; START="--data-parallel-start-rank ${DP_LOCAL}" ;; + *) echo "bad ROLE=$ROLE"; exit 1 ;; +esac +# Optional cudagraph override (DECODE_CG=NONE bypasses PIECEWISE capture to isolate +# a capture-time GPU fault; PREFILL_CG likewise). Decode capture of the KDA conv +# path can fault on some builds; NONE trades decode-graph perf for stability. +case "$ROLE" in + prefill_*) [ -n "${PREFILL_CG:-}" ] && CG="$PREFILL_CG" ;; + decode_*) [ -n "${DECODE_CG:-}" ] && CG="$DECODE_CG" ;; +esac +# Optional all2all-backend override (test: mori_low_latency on both pools to rule +# out mori_high_throughput/InterNodeV1 as the profile-forward all2all deadlock). +if [[ "$ROLE" == prefill_* && -n "${PREFILL_BACKEND:-}" ]]; then BACKEND="$PREFILL_BACKEND"; fi +if [[ "$ROLE" == decode_* && -n "${DECODE_BACKEND:-}" ]]; then BACKEND="$DECODE_BACKEND"; fi + +IS_MASTER=0; [[ "$ROLE" == *_master ]] && IS_MASTER=1 +CONTAINER="k3disagg_${ROLE}" +docker rm -f "$CONTAINER" 2>/dev/null || true + +# kv-transfer-config on ALL ranks (masters AND headless workers). A headless +# worker hosts real DP ranks (e.g. decode rank1 on the worker node); WITHOUT +# --kv-transfer-config its engine never instantiates the MoRIIO connector, so it +# binds NO handshake listener -> prefill can't transfer KV to those ranks -> +# "Timed out waiting for write_ready_flags" -> EngineDead. (Was gated on +# IS_MASTER, which silently made every worker-node DP rank a transfer black hole.) +# To survive host-bash -> ssh -> docker -c quoting, pass JSON as base64 via env. +KVCFG_B64="" +if true; then + # Peer pool's per-DP-pod node IPs (ordered by pod index = global_dp_rank//dp_local). + # A prefill (kv_producer) handshakes the DECODE pool -> needs decode hosts; a decode + # (kv_consumer) notifies the PREFILL pool -> needs prefill hosts. Without this the + # connector falls back to a single peer host (the master), so KV writes/notifies to + # ranks on the peer's WORKER node silently miss -> that node's decode ranks generate + # context-free (the 50%/DP2, ~88%/DP8 wrong-answer alternation). Consumed by the + # apply_kimik3_moriio_pod_hosts patcher as multi_pod_hosts in the handshake. + if [[ "$ROLE" == prefill_* ]]; then PEER_POD_HOSTS="${DECODE_POD_HOSTS:-}"; else PEER_POD_HOSTS="${PREFILL_POD_HOSTS:-}"; fi + # MORIIO_READ_MODE=1 selects the connector's READ path (decode pulls KV, sync, + # returns N-1) instead of WRITE (prefill pushes, async, returns N). READ is the + # more-tested vLLM disagg path; toggled for A/B against the WRITE decode-consume bug. + READ_MODE_JSON=""; if [ "${MORIIO_READ_MODE:-0}" = "1" ]; then READ_MODE_JSON=",\"read_mode\":\"true\""; fi + KVCFG_JSON="{\"kv_connector\":\"MoRIIOConnector\",\"kv_role\":\"${KV_ROLE}\",\"kv_port\":\"9711\",\"kv_connector_extra_config\":{\"proxy_ip\":\"${PROXY_IP}\",\"proxy_port\":\"30000\",\"proxy_ping_port\":\"36367\",\"http_port\":\"${SERVE_PORT}\",\"local_ping_port\":\"61555\",\"handshake_port\":\"8405\",\"notify_port\":\"61005\",\"moriio_pod_hosts\":\"${PEER_POD_HOSTS}\",\"post_batch_size\":${MORIIO_POST_BATCH_SIZE:--1},\"qp_per_transfer\":${MORIIO_QP_PER_TRANSFER:-1},\"num_workers\":${MORIIO_NUM_WORKERS:-1}${READ_MODE_JSON}}}" + KVCFG_B64=$(printf '%s' "$KVCFG_JSON" | base64 -w0) +fi +# api-server ONLY on masters (headless workers must NOT bind an api-server). +# api-server-count MUST be <= data-parallel-size: the frontend DP load-balancer +# round-robins requests across data_parallel_rank [0, api_server_count). Match it +# to DP_SIZE so every request lands on a live DP rank. +if [[ $IS_MASTER -eq 1 ]]; then + APISERVERS="--api-server-count ${DP_SIZE} --port ${SERVE_PORT}" +else + APISERVERS="" +fi + +echo "[disagg] node=$(hostname -s) role=$ROLE dp_addr=$DP_ADDR kv_role=${KV_ROLE:-none} backend=$BACKEND" + +docker run -d --name "$CONTAINER" \ + --network host --ipc host \ + --device /dev/kfd --device /dev/dri --device /dev/infiniband --group-add video \ + --cap-add SYS_PTRACE --cap-add IPC_LOCK --security-opt seccomp=unconfined \ + --shm-size 128g --ulimit memlock=-1 --ulimit stack=67108864 --ulimit nofile=524288:524288 \ + -e VLLM_ROCM_USE_AITER_MLA=0 \ + -e AITER_SITUV2_A8W4=1 \ + -e VLLM_ROCM_USE_AITER=1 -e VLLM_ROCM_USE_AITER_MOE=1 \ + -e VLLM_ROCM_USE_AITER_FUSION_SHARED_EXPERTS=0 \ + -e VLLM_USE_AITER_TRITON_SILU_MUL=0 -e VLLM_ROCM_USE_AITER_RMSNORM=1 \ + -e VLLM_ENGINE_READY_TIMEOUT_S=3600 \ + -e VLLM_SSM_CONV_STATE_LAYOUT=DS \ + -e NCCL_SOCKET_IFNAME=${SOCKET_IFNAME} -e GLOO_SOCKET_IFNAME=${SOCKET_IFNAME} \ + -e NCCL_IB_DISABLE=0 -e NCCL_IB_HCA=${NCCL_IB_HCA_VAL} -e MORI_RDMA_DEVICES=${RDMA_DEVICES} -e MORI_SOCKET_IFNAME=${SOCKET_IFNAME} \ + -e NCCL_IB_GID_INDEX=${IB_GID_INDEX} -e NCCL_IGNORE_CPU_AFFINITY=1 \ + -e HSA_ENABLE_IPC_MODE_LEGACY=0 -e HSA_NO_SCRATCH_RECLAIM=1 \ + -e PYTORCH_ALLOC_CONF=expandable_segments:False -e PYTORCH_HIP_ALLOC_CONF=expandable_segments:False \ + -e MORIIO_SKIP_MAMBA="${MORIIO_SKIP_MAMBA:-0}" \ + -e VLLM_BATCH_INVARIANT="${VLLM_BATCH_INVARIANT:-0}" \ + -e AMD_SERIALIZE_KERNEL="${AMD_SERIALIZE_KERNEL:-0}" -e AMD_LOG_LEVEL="${AMD_LOG_LEVEL:-0}" \ + -e MORI_GPU_ARCHS=gfx942 -e MORI_IB_GID_INDEX=${IB_GID_INDEX} -e MORI_IB_ENABLE_RELAXED_ORDERING=1 \ + -e MORI_NUM_QP_PER_PE=8 -e MORI_SHMEM_HEAP_SIZE=17179869184 \ + -e MORI_RDMA_TC=41 -e MORI_RDMA_SL=0 -e MORI_IO_SL=1 \ + -e VLLM_MORIIO_QP_PER_TRANSFER="${VLLM_MORIIO_QP_PER_TRANSFER:-2}" -e VLLM_MORIIO_NUM_WORKERS="${VLLM_MORIIO_NUM_WORKERS:-4}" \ + -e AITER_JIT_DIR=/opt/vllm_cache/aiter -e TRITON_CACHE_DIR=/opt/vllm_cache/triton \ + -e VLLM_CACHE_ROOT=/opt/vllm_cache/vllm \ + -e KVCFG_B64="$KVCFG_B64" \ + -e QUANT_CONFIG="$QUANT_CONFIG" \ + -v "$MODEL_DIR":/model:ro -v "$LOGHOST":/logs \ + -v "$JIT_HOST":/opt/vllm_cache \ + ${BNXT_MOUNTS} \ + --entrypoint bash \ + "$IMAGE" -c " + set -e + mkdir -p /opt/vllm_cache/aiter /opt/vllm_cache/triton /opt/vllm_cache/vllm + # The NFS/DockerHub image tar is a STALE build shipping flydsl 0.2.2, but the + # K3 int4 SiTUv2 MoE path (_setup_kernel_k3_situ_gfx942 -> compile_moe_gemm1) + # hard-requires flydsl>=0.2.4 (ImportError otherwise -> WorkerProc init fails -> + # pool never starts). Dockerfile.kimik3_disagg installs 0.2.4 (line 148) but this + # tar predates that. Bump at container start (once per node, pure-python, fast). + FLYDSL_VER=\$(python3 -c 'import flydsl,importlib.metadata as m; print(m.version(\"flydsl\"))' 2>/dev/null || echo 0) + if [ \"\$FLYDSL_VER\" != \"0.2.4\" ]; then + echo \"[disagg] flydsl \$FLYDSL_VER -> upgrading to 0.2.4 (K3 int4 SiTUv2 requires >=0.2.4)\" + pip install --no-cache-dir 'flydsl==0.2.4' 2>&1 | tail -1 + fi + QUANTARG=() + if [ -n \"\$QUANT_CONFIG\" ]; then + QUANTARG=(--quantization-config \"\$QUANT_CONFIG\") + fi + KVARG=() + if [ -n \"\$KVCFG_B64\" ]; then + KVJSON=\$(printf '%s' \"\$KVCFG_B64\" | base64 -d) + KVARG=(--kv-transfer-config \"\$KVJSON\") + fi + echo '[disagg] launching vllm serve...' + vllm serve /model --served-model-name kimi-k3 --tensor-parallel-size ${TP_SIZE} \ + --data-parallel-size ${DP_SIZE} --data-parallel-size-local ${DP_LOCAL} \ + --data-parallel-address ${DP_ADDR} --data-parallel-rpc-port ${RPC_PORT} ${START} ${HEADLESS} \ + --enable-expert-parallel --all2all-backend ${BACKEND} \ + --trust-remote-code --reasoning-parser kimi_k3 --mm-encoder-tp-mode data --safetensors-load-strategy ${LOAD_STRATEGY:-prefetch} \ + --no-enable-prefix-caching --kv-cache-dtype ${KV_CACHE_DTYPE:-fp8} --block-size ${BLOCK_SIZE:-16} \ + --kv-cache-memory-bytes ${KV_CACHE_MEMORY_BYTES} \ + --max-model-len ${MAX_MODEL_LEN} --max-num-seqs 8 --max-num-batched-tokens ${MAX_NUM_BATCHED_TOKENS} --gpu-memory-utilization ${GPU_UTIL} \ + --distributed-timeout-seconds 7200 \ + --compilation-config '{\"cudagraph_mode\":\"${CG}\",\"custom_ops\":[\"+quant_fp8\"]}' \ + ${APISERVERS} \"\${QUANTARG[@]}\" \"\${KVARG[@]}\" 2>&1 | tee /logs/vllm_${ROLE}.log + " +echo "[disagg] $ROLE started -> $LOGHOST/vllm_${ROLE}.log" diff --git a/scripts/vllm/kimik3_mi300x/wideep_disagg_2p2d/run_2p2d_launch.sh b/scripts/vllm/kimik3_mi300x/wideep_disagg_2p2d/run_2p2d_launch.sh new file mode 100755 index 00000000..8e4406d8 --- /dev/null +++ b/scripts/vllm/kimik3_mi300x/wideep_disagg_2p2d/run_2p2d_launch.sh @@ -0,0 +1,84 @@ +#!/bin/bash +# Orchestrate the K3 2P/2D disagg bring-up from the head/control host. +# Prefill pool: PM (master) + PW (worker). Decode pool: DM (master) + DW (worker). +# Router runs on PM (proxy_ip). Launch order: workers first, then masters, then router. +set -euo pipefail + +# --- node eth0 IPs (EDIT for your 4-node allocation) --- +# Prefill pool = 2 nodes (master+worker); decode pool = 2 nodes (master+worker). +# PM_NODE/DM_NODE are the ssh hostnames; *_IP are the eth0 IPs the peers dial. +PM_NODE=${PM_NODE:-}; PM_IP=${PM_IP:-} # prefill master + proxy/router +PW_NODE=${PW_NODE:-}; PW_IP=${PW_IP:-} # prefill worker +DM_NODE=${DM_NODE:-}; DM_IP=${DM_IP:-} # decode master +DW_NODE=${DW_NODE:-}; DW_IP=${DW_IP:-} # decode worker + +# Path to THIS recipe folder on the control host (scp'd to each node). Override REPO=. +REPO=${REPO:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)} +# Kimi-K3 MXFP4 weights path (must exist on every node; local nvme recommended). +MODEL_DIR=${MODEL_DIR:?set MODEL_DIR to your Kimi-K3-MXFP4 path} +# Optional per-node override: MODEL_DIR_144=/path/on/144 (suffix = last hostname segment). +model_dir_for() { + local host="$1" + local suf="${host##*-}" + local v="MODEL_DIR_${suf}" + if [[ -n "${!v+x}" ]]; then echo "${!v}"; else echo "$MODEL_DIR"; fi +} +SSH="ssh -o StrictHostKeyChecking=no" +# Load-bearing env only. TP2xDP8 -> EP16 per pool (no PP). See README for knobs. +COMMON="IMAGE=${IMAGE:-kimik3-wideep-disagg:latest} TP_SIZE=${TP_SIZE:-2} DP_SIZE=${DP_SIZE:-8} DP_LOCAL=${DP_LOCAL:-4} KV_CACHE_MEMORY_BYTES=${KV_CACHE_MEMORY_BYTES:-8000000000} PREFILL_BACKEND=mori_low_latency DECODE_CG=${DECODE_CG:-NONE} MODEL_DIR=${MODEL_DIR} PMASTER=$PM_IP DMASTER=$DM_IP PROXY_IP=$PM_IP DECODE_POD_HOSTS=$DM_IP,$DW_IP PREFILL_POD_HOSTS=$PM_IP,$PW_IP THOR2_BNXT_FIX=${THOR2_BNXT_FIX:-0} SOCKET_IFNAME=${SOCKET_IFNAME:-eth0} NCCL_IB_HCA=${NCCL_IB_HCA:-mlx5_0,mlx5_2,mlx5_3,mlx5_4,mlx5_5,mlx5_7,mlx5_8,mlx5_9} RDMA_DEVICES=${RDMA_DEVICES:-mlx5_0,mlx5_2,mlx5_3,mlx5_4,mlx5_5,mlx5_7,mlx5_8,mlx5_9} IB_GID_INDEX=${IB_GID_INDEX:-3} MAX_MODEL_LEN=${MAX_MODEL_LEN:-10240} MAX_NUM_BATCHED_TOKENS=${MAX_NUM_BATCHED_TOKENS:-2048} GPU_UTIL=${GPU_UTIL:-0.88} LOAD_STRATEGY=${LOAD_STRATEGY:-lazy} AUTO_ROUTER=${AUTO_ROUTER:-0}" + +deploy() { # $1=node + $SSH "$1" 'mkdir -p ~/k3disagg/logs' 2>/dev/null + scp -o StrictHostKeyChecking=no "$REPO/run_2p2d.sh" "$REPO/load_image.sh" "$1:~/k3disagg/" >/dev/null + # Self-restoring: ensure the disagg image is present (pull from DockerHub if not). + $SSH "$1" "cd ~/k3disagg && TAG='${IMAGE:-kimik3-wideep-disagg:latest}' HUB_IMAGE='${HUB_IMAGE:-}' DOCKER_USER='${DOCKER_USER:-}' DOCKER_PAT='${DOCKER_PAT:-}' bash load_image.sh" 2>&1 | tail -1 +} + +echo "=== deploy scripts+image to 4 nodes ===" +for n in $PM_NODE $PW_NODE $DM_NODE $DW_NODE; do deploy "$n"; echo " $n ok"; done + +run_role() { # $1=node $2=role + local md; md=$(model_dir_for "$1") + $SSH "$1" "cd ~/k3disagg && $COMMON MODEL_DIR=$md ROLE=$2 bash run_2p2d.sh" 2>&1 | tail -1 +} + +echo "=== start WORKERS first ===" +run_role "$PW_NODE" prefill_worker +run_role "$DW_NODE" decode_worker +sleep 5 +echo "=== start MASTERS ===" +run_role "$PM_NODE" prefill_master +run_role "$DM_NODE" decode_master + +# Auto-start the router in the discovery window so the engines' _ping threads +# connect before exhausting MAX_PING_RETRIES (a late router misses discovery -> +# "0 prefill 0 decode"). AUTO_ROUTER=1 waits for both masters' /v1/models, then +# launches the router on PM. +if [ "${AUTO_ROUTER:-0}" = "1" ]; then + echo "=== AUTO_ROUTER: waiting for both masters, then starting router ===" + ( for t in $(seq 1 180); do + pm=$($SSH $PM_NODE "curl -s -m 5 http://$PM_IP:20005/v1/models 2>/dev/null | grep -c kimi-k3" 2>/dev/null) + dm=$($SSH $DM_NODE "curl -s -m 5 http://$DM_IP:20005/v1/models 2>/dev/null | grep -c kimi-k3" 2>/dev/null) + if [ "${pm:-0}" -ge 1 ] && [ "${dm:-0}" -ge 1 ]; then + echo "[auto-router] both masters ready; starting router" + $SSH $PM_NODE "docker exec k3disagg_prefill_master bash -c 'setsid nohup vllm-router --host 0.0.0.0 --port 30000 --vllm-pd-disaggregation --kv-connector moriio --prefill http://$PM_IP:20005 --decode http://$DM_IP:20005 --vllm-discovery-address 0.0.0.0:36367 --intra-node-data-parallel-size ${DP_LOCAL:-4} --moriio-dp-size ${DP_SIZE:-8} --policy round_robin --prefill-policy round_robin --decode-policy round_robin --log-level info > /logs/router.log 2>&1 < /dev/null &'" + break + fi + sleep 20 + done ) & + echo "[auto-router] watcher started (pid $!)" +fi + +echo "" +echo "=== bring-up started. Watch for 'Application startup complete' in: ===" +echo " PM: $SSH $PM_NODE 'docker logs -f k3disagg_prefill_master'" +echo " DM: $SSH $DM_NODE 'docker logs -f k3disagg_decode_master'" +echo "" +echo "=== once BOTH masters are up, start the router on $PM_NODE: ===" +cat < /logs/router.log 2>&1"' +EOF +echo "" +echo "=== test (after router shows 'Add Prefill'+'Add Decode'): ===" +echo " curl http://$PM_IP:30000/v1/models" +echo " python3 niah_probe.py --url http://$PM_IP:30000 --model kimi-k3 --ctx 6000 --depths 0.1,0.5,0.9" diff --git a/scripts/vllm/kimik3_mi300x/wideep_int4_allgather/README.md b/scripts/vllm/kimik3_mi300x/wideep_int4_allgather/README.md new file mode 100644 index 00000000..75f69421 --- /dev/null +++ b/scripts/vllm/kimik3_mi300x/wideep_int4_allgather/README.md @@ -0,0 +1,48 @@ +# Kimi-K3 (MXFP4) on MI300X / gfx942 — Wide-EP (generic all2all) + a8w4 + +Serves Kimi-K3 (MXFP4) across **2 nodes** (16× MI300X) with **expert parallelism**: +PP2×TP8 for weight fit (~102 GB/GPU) plus `--enable-expert-parallel`, so the 896 experts are split 8-way across each node's 8 GPUs (112/GPU), replicated per PP stage. Expert all-to-all uses the generic +`--all2all-backend allgather_reducescatter` (see `../wideep_int4_moriep` for the true +MoRI-EP kernels). `AITER_SITUV2_A8W4=1` selects the a8w4 (fp8-activation × int4-weight) +SiTU MoE path. Colocated (single instance; no prefill/decode disaggregation). + +- Image: `amdsiloai/vllm:kimi-k3-mi325x-release-v2` (gfx942 K3 vLLM build; public, anonymous pull) +- Model: `moonshotai/Kimi-K3` (MXFP4), on local NVMe or NFS + +## Run (worker FIRST, then head) + +```bash +# on the worker node (rank 1): +MODEL_DIR=/path/to/Kimi-K3-MXFP4 ROLE=worker MASTER= bash run.sh + +# on the head node (rank 0, serves the API on :8000): +MODEL_DIR=/path/to/Kimi-K3-MXFP4 ROLE=head MASTER= bash run.sh +``` + +- `MASTER` = the head node's **eth0** IP. `MODEL_DIR` = Kimi-K3-MXFP4 weights (prefer local NVMe). +- First start compiles the AITER EP MoE asm + a8w4 kernels (a few minutes), then cached. + +## Verify + +```bash +curl http://:8000/v1/models +python3 niah_probe.py --url http://:8000 --model kimi-k3 --ctx 8500 --depths 0.1,0.5,0.9 +``` +The response `system_fingerprint` contains `-ep-` when expert parallelism is active. + +## Toggles + +- Drop a8w4: remove `-e AITER_SITUV2_A8W4=1` from `run.sh` → default a16w4 MoE path. +- MoE dispatch: add `-e VLLM_ROCM_AITER_MOE_DISPATCH_POLICY=2` (multi-pass; may help MoE-heavy at higher concurrency). + +## Key env (set in run.sh) + +| Var | Value | Why | +|-----|-------|-----| +| `VLLM_ROCM_USE_AITER_MLA` | `0` | Required on gfx942 (AITER MLA is gfx950-only). | +| `AITER_SITUV2_A8W4` | `1` | Route K3 SiTU MXFP4 MoE through the a8w4 interleaved flydsl kernels. | +| `NCCL_IB_HCA` / `NCCL_IB_GID_INDEX` | 8× mlx5 / `3` | RDMA fabric (**cluster-specific — override for yours**). | +| `HSA_ENABLE_IPC_MODE_LEGACY` / `PYTORCH_(HIP_)ALLOC_CONF` | `0` / `expandable_segments:False` | ROCm 7.2.x requirements. | + +Overridable env: `IMAGE`, `MODEL_DIR`, `MASTER`, `PORT` (8000), `MAX_MODEL_LEN` (10240), +`MAX_NUM_SEQS` (8), `GPU_UTIL` (0.90). diff --git a/scripts/vllm/kimik3_mi300x/wideep_int4_allgather/niah_probe.py b/scripts/vllm/kimik3_mi300x/wideep_int4_allgather/niah_probe.py new file mode 100644 index 00000000..0fed8766 --- /dev/null +++ b/scripts/vllm/kimik3_mi300x/wideep_int4_allgather/niah_probe.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 +"""Lightweight NIAH (needle-in-a-haystack) + basic accuracy probe for a served +Kimi-K3 endpoint. No deps beyond stdlib (urllib). Run from any node that can +reach the head IP:port. + +Usage: + python3 niah_probe.py --url http://:8000 --model kimi-k3 \ + --ctx 8000 --depths 0.1,0.5,0.9 +""" +import argparse, json, time, urllib.request, urllib.error + +FILLER = ("The quick brown fox jumps over the lazy dog. " + "Grand vistas of rolling hills stretched to the horizon. ") + +def chat(url, model, prompt, max_tokens=256, timeout=180): + body = json.dumps({ + "model": model, + "messages": [{"role": "user", "content": prompt}], + "max_tokens": max_tokens, "temperature": 0, + }).encode() + req = urllib.request.Request(url.rstrip("/") + "/v1/chat/completions", + data=body, headers={"Content-Type": "application/json"}) + t0 = time.time() + with urllib.request.urlopen(req, timeout=timeout) as r: + d = json.load(r) + dt = time.time() - t0 + ch = d["choices"][0]["message"] + txt = (ch.get("content") or "") + " " + (ch.get("reasoning") or "") + return txt.strip(), d.get("usage", {}), dt + +def make_haystack(approx_words, needle, depth): + # Build filler to approx word count; insert needle at fractional depth. + words = [] + while len(words) < approx_words: + words.extend(FILLER.split()) + n = len(words) + pos = int(n * depth) + needle_words = needle.split() + words[pos:pos] = needle_words + return " ".join(words) + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--url", required=True) + ap.add_argument("--model", default="kimi-k3") + ap.add_argument("--ctx", type=int, default=8000, help="approx haystack words") + ap.add_argument("--depths", default="0.1,0.5,0.9") + args = ap.parse_args() + + print(f"== sanity ==") + txt, usage, dt = chat(args.url, args.model, "Reply with only: OK", max_tokens=8) + print(f" resp={txt!r} usage={usage} {dt:.1f}s") + + secret = "The secret passcode is HELIOTROPE-7492." + question = ("\n\nQuestion: What is the secret passcode? " + "Answer with only the passcode.") + passed = 0; total = 0 + for depth in [float(x) for x in args.depths.split(",")]: + hay = make_haystack(args.ctx, secret, depth) + prompt = hay + question + try: + txt, usage, dt = chat(args.url, args.model, prompt, max_tokens=256) + except Exception as e: + print(f" depth={depth}: ERROR {e}") + total += 1; continue + ok = "HELIOTROPE-7492" in txt.upper() + passed += ok; total += 1 + print(f" depth={depth:>4}: {'PASS' if ok else 'FAIL'} " + f"(prompt_tok={usage.get('prompt_tokens')}, {dt:.1f}s) resp={txt[:60]!r}") + print(f"== NIAH {passed}/{total} passed ==") + +if __name__ == "__main__": + main() diff --git a/scripts/vllm/kimik3_mi300x/wideep_int4_allgather/run.sh b/scripts/vllm/kimik3_mi300x/wideep_int4_allgather/run.sh new file mode 100755 index 00000000..fe52d399 --- /dev/null +++ b/scripts/vllm/kimik3_mi300x/wideep_int4_allgather/run.sh @@ -0,0 +1,67 @@ +#!/bin/bash +# Kimi-K3 (MXFP4) on MI300X / gfx942 -- Wide expert-parallel, generic all2all. +# PP2xTP8 across 2 nodes for weight fit (~102 GB/GPU) PLUS --enable-expert-parallel +# so the 896 experts split 8-way across each node's 8 GPUs (112/GPU), replicated per PP +# stage. Uses the generic --all2all-backend allgather_reducescatter (see ../wideep_int4_moriep +# MoRI-EP kernels). AITER_SITUV2_A8W4=1 selects the a8w4 (fp8-act x int4-wt) SiTU +# MoE path. Colocated (single instance; no prefill/decode disaggregation). +# +# Usage (worker FIRST, then head): +# ROLE=worker MASTER= bash run.sh # on node1 (rank1) +# ROLE=head MASTER= bash run.sh # on node0 (rank0, serves API) +set -euo pipefail + +IMAGE="${IMAGE:-amdsiloai/vllm:kimi-k3-mi325x-release-v2}" +MODEL_DIR="${MODEL_DIR:?set MODEL_DIR=}" +ROLE="${ROLE:?set ROLE=head|worker}" +MASTER="${MASTER:?set MASTER=}" +PORT="${PORT:-8000}" +MAX_MODEL_LEN="${MAX_MODEL_LEN:-10240}" +MAX_NUM_SEQS="${MAX_NUM_SEQS:-8}" +GPU_UTIL="${GPU_UTIL:-0.90}" +CONTAINER="k3_wideepint4_${ROLE}" +LOGHOST="${LOGHOST:-$HOME/k3run/logs}"; mkdir -p "$LOGHOST" + +# Proven fabric env (from cluster_rdma_env_recommender.py) +BOOT_NIC="eth0" +IB_HCA="mlx5_0,mlx5_2,mlx5_3,mlx5_4,mlx5_5,mlx5_7,mlx5_8,mlx5_9" +GID_INDEX=3 + +if [ "$ROLE" = "head" ]; then + NODE_RANK=0; SERVE_EXTRA="--port ${PORT}"; HEADLESS="" +else + NODE_RANK=1; SERVE_EXTRA=""; HEADLESS="--headless" +fi + +echo "[pp2tp8] node=$(hostname -s) role=$ROLE rank=$NODE_RANK master=$MASTER" +[ -f "$MODEL_DIR/model.safetensors.index.json" ] || { echo "ERROR: model missing at $MODEL_DIR"; exit 1; } +docker rm -f "$CONTAINER" 2>/dev/null || true + +docker run -d --name "$CONTAINER" \ + --network host --ipc host \ + --device /dev/kfd --device /dev/dri --device /dev/infiniband --group-add video \ + --cap-add SYS_PTRACE --security-opt seccomp=unconfined --shm-size 128g \ + --ulimit memlock=-1 --ulimit stack=67108864 \ + -e VLLM_ROCM_USE_AITER_MLA=0 \ + -e AITER_SITUV2_A8W4=1 \ + -e NCCL_SOCKET_IFNAME=$BOOT_NIC -e GLOO_SOCKET_IFNAME=$BOOT_NIC \ + -e NCCL_IB_DISABLE=0 -e NCCL_IB_HCA=$IB_HCA -e NCCL_IB_GID_INDEX=$GID_INDEX \ + -e NCCL_IGNORE_CPU_AFFINITY=1 -e NCCL_DEBUG=WARN \ + -e HSA_ENABLE_IPC_MODE_LEGACY=0 -e HSA_NO_SCRATCH_RECLAIM=1 \ + -e PYTORCH_ALLOC_CONF=expandable_segments:False \ + -e PYTORCH_HIP_ALLOC_CONF=expandable_segments:False \ + -v "$MODEL_DIR":/model:ro -v "$LOGHOST":/logs \ + --entrypoint bash \ + "$IMAGE" -c " + vllm serve /model --served-model-name kimi-k3 \ + --tensor-parallel-size 8 --pipeline-parallel-size 2 \ + --distributed-executor-backend mp \ + --enable-expert-parallel --all2all-backend allgather_reducescatter \ + --nnodes 2 --node-rank ${NODE_RANK} --master-addr ${MASTER} --master-port 29500 ${HEADLESS} \ + --trust-remote-code --reasoning-parser kimi_k3 --mm-encoder-tp-mode data \ + --safetensors-load-strategy prefetch \ + --max-model-len ${MAX_MODEL_LEN} --max-num-seqs ${MAX_NUM_SEQS} \ + --gpu-memory-utilization ${GPU_UTIL} ${SERVE_EXTRA} 2>&1 | tee /logs/vllm_wideepint4_${ROLE}.log + " +echo "[pp2tp8] $ROLE started. log: $LOGHOST/vllm_wideepint4_${ROLE}.log" +[ "$ROLE" = head ] && echo "[pp2tp8] health: curl http://${MASTER}:${PORT}/v1/models" diff --git a/scripts/vllm/kimik3_mi300x/wideep_int4_moriep/README.md b/scripts/vllm/kimik3_mi300x/wideep_int4_moriep/README.md new file mode 100644 index 00000000..0f5cd8fd --- /dev/null +++ b/scripts/vllm/kimik3_mi300x/wideep_int4_moriep/README.md @@ -0,0 +1,51 @@ +# Kimi-K3 (MXFP4) on MI300X / gfx942 — Wide-EP via MoRI-EP + a8w4 + +Serves Kimi-K3 (MXFP4) across **2 nodes** (16× MI300X) with **expert parallelism** over +true **MoRI-EP** all-to-all kernels: PP2×TP8 for weight fit (~102 GB/GPU) plus +`--enable-expert-parallel --all2all-backend mori_low_latency`, so the 896 experts are +split **8-way across each node's 8 GPUs** (112 experts/GPU, `[EP Rank x/8]`), replicated on +each of the 2 pipeline stages, and dispatched via MoRI all2all. The EP group is intra-node, +so MoRI-EP dispatch/combine runs across a node's 8 GPUs; the only cross-node traffic is the +PP activation hand-off (NCCL). +`AITER_SITUV2_A8W4=1` selects the a8w4 (fp8-activation × int4-weight) SiTU MoE path. +This is the "MoRI-EP + a8w4" path (vs the generic all2all in `../wideep_int4_allgather`). +Colocated (single instance; no prefill/decode disaggregation). + +- Image: `amdsiloai/vllm:kimi-k3-mi325x-release-v2` (gfx942 K3 vLLM build; public, anonymous pull) +- Model: `moonshotai/Kimi-K3` (MXFP4), on local NVMe or NFS + +## Run (worker FIRST, then head) + +```bash +# on the worker node (rank 1): +MODEL_DIR=/path/to/Kimi-K3-MXFP4 ROLE=worker MASTER= bash run.sh + +# on the head node (rank 0, serves the API on :8000): +MODEL_DIR=/path/to/Kimi-K3-MXFP4 ROLE=head MASTER= bash run.sh +``` + +- `MASTER` = the head node's **eth0** IP. `MODEL_DIR` = Kimi-K3-MXFP4 weights (prefer local NVMe). +- First start compiles the MoRI-EP dispatch kernels + a8w4 MoE (a few minutes), then cached. + +## Verify + +```bash +curl http://:8000/v1/models +python3 niah_probe.py --url http://:8000 --model kimi-k3 --ctx 8500 --depths 0.1,0.5,0.9 +``` +The response `system_fingerprint` contains `-ep-` when expert parallelism is active. + +## Key env (set in run.sh) + +| Var | Value | Why | +|-----|-------|-----| +| `VLLM_ROCM_USE_AITER_MLA` | `0` | Required on gfx942 (AITER MLA is gfx950-only). | +| `AITER_SITUV2_A8W4` | `1` | Route K3 SiTU MXFP4 MoE through the a8w4 interleaved flydsl kernels. | +| `MORI_GPU_ARCHS` | `gfx942` | MoRI-EP target arch. | +| `MORI_IB_GID_INDEX` / `MORI_IB_ENABLE_RELAXED_ORDERING` / `MORI_NUM_QP_PER_PE` | `3` / `1` / `8` | MoRI RDMA fabric tuning (**cluster-specific**). | +| `NCCL_IB_HCA` / `NCCL_IB_GID_INDEX` | 8× mlx5 / `3` | NCCL RDMA fabric (**cluster-specific — override for yours**). | +| `HSA_ENABLE_IPC_MODE_LEGACY` / `PYTORCH_(HIP_)ALLOC_CONF` | `0` / `expandable_segments:False` | ROCm 7.2.x requirements. | + +`--all2all-backend mori_low_latency` is used for both roles (low-latency MoRI-EP dispatch). +Overridable env: `IMAGE`, `MODEL_DIR`, `MASTER`, `PORT` (8000), `MAX_MODEL_LEN` (10240), +`MAX_NUM_SEQS` (8), `GPU_UTIL` (0.90). diff --git a/scripts/vllm/kimik3_mi300x/wideep_int4_moriep/niah_probe.py b/scripts/vllm/kimik3_mi300x/wideep_int4_moriep/niah_probe.py new file mode 100644 index 00000000..0fed8766 --- /dev/null +++ b/scripts/vllm/kimik3_mi300x/wideep_int4_moriep/niah_probe.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 +"""Lightweight NIAH (needle-in-a-haystack) + basic accuracy probe for a served +Kimi-K3 endpoint. No deps beyond stdlib (urllib). Run from any node that can +reach the head IP:port. + +Usage: + python3 niah_probe.py --url http://:8000 --model kimi-k3 \ + --ctx 8000 --depths 0.1,0.5,0.9 +""" +import argparse, json, time, urllib.request, urllib.error + +FILLER = ("The quick brown fox jumps over the lazy dog. " + "Grand vistas of rolling hills stretched to the horizon. ") + +def chat(url, model, prompt, max_tokens=256, timeout=180): + body = json.dumps({ + "model": model, + "messages": [{"role": "user", "content": prompt}], + "max_tokens": max_tokens, "temperature": 0, + }).encode() + req = urllib.request.Request(url.rstrip("/") + "/v1/chat/completions", + data=body, headers={"Content-Type": "application/json"}) + t0 = time.time() + with urllib.request.urlopen(req, timeout=timeout) as r: + d = json.load(r) + dt = time.time() - t0 + ch = d["choices"][0]["message"] + txt = (ch.get("content") or "") + " " + (ch.get("reasoning") or "") + return txt.strip(), d.get("usage", {}), dt + +def make_haystack(approx_words, needle, depth): + # Build filler to approx word count; insert needle at fractional depth. + words = [] + while len(words) < approx_words: + words.extend(FILLER.split()) + n = len(words) + pos = int(n * depth) + needle_words = needle.split() + words[pos:pos] = needle_words + return " ".join(words) + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--url", required=True) + ap.add_argument("--model", default="kimi-k3") + ap.add_argument("--ctx", type=int, default=8000, help="approx haystack words") + ap.add_argument("--depths", default="0.1,0.5,0.9") + args = ap.parse_args() + + print(f"== sanity ==") + txt, usage, dt = chat(args.url, args.model, "Reply with only: OK", max_tokens=8) + print(f" resp={txt!r} usage={usage} {dt:.1f}s") + + secret = "The secret passcode is HELIOTROPE-7492." + question = ("\n\nQuestion: What is the secret passcode? " + "Answer with only the passcode.") + passed = 0; total = 0 + for depth in [float(x) for x in args.depths.split(",")]: + hay = make_haystack(args.ctx, secret, depth) + prompt = hay + question + try: + txt, usage, dt = chat(args.url, args.model, prompt, max_tokens=256) + except Exception as e: + print(f" depth={depth}: ERROR {e}") + total += 1; continue + ok = "HELIOTROPE-7492" in txt.upper() + passed += ok; total += 1 + print(f" depth={depth:>4}: {'PASS' if ok else 'FAIL'} " + f"(prompt_tok={usage.get('prompt_tokens')}, {dt:.1f}s) resp={txt[:60]!r}") + print(f"== NIAH {passed}/{total} passed ==") + +if __name__ == "__main__": + main() diff --git a/scripts/vllm/kimik3_mi300x/wideep_int4_moriep/run.sh b/scripts/vllm/kimik3_mi300x/wideep_int4_moriep/run.sh new file mode 100755 index 00000000..05a35aed --- /dev/null +++ b/scripts/vllm/kimik3_mi300x/wideep_int4_moriep/run.sh @@ -0,0 +1,67 @@ +#!/bin/bash +# Kimi-K3 (MXFP4) on MI300X / gfx942 -- Wide expert-parallel via true MoRI-EP. +# PP2xTP8 across 2 nodes for weight fit (~102 GB/GPU) PLUS --enable-expert-parallel +# with --all2all-backend mori_low_latency (MoRI-EP all2all kernels) + MoRI RDMA env. +# AITER_SITUV2_A8W4=1 selects the a8w4 (fp8-act x int4-wt) SiTU MoE path. Colocated +# (single instance; no prefill/decode disaggregation). +# +# Usage (worker FIRST, then head): +# ROLE=worker MASTER= bash run.sh # on node1 (rank1) +# ROLE=head MASTER= bash run.sh # on node0 (rank0, serves API) +set -euo pipefail + +IMAGE="${IMAGE:-amdsiloai/vllm:kimi-k3-mi325x-release-v2}" +MODEL_DIR="${MODEL_DIR:?set MODEL_DIR=}" +ROLE="${ROLE:?set ROLE=head|worker}" +MASTER="${MASTER:?set MASTER=}" +PORT="${PORT:-8000}" +MAX_MODEL_LEN="${MAX_MODEL_LEN:-10240}" +MAX_NUM_SEQS="${MAX_NUM_SEQS:-8}" +GPU_UTIL="${GPU_UTIL:-0.90}" +CONTAINER="k3_moriep_${ROLE}" +LOGHOST="${LOGHOST:-$HOME/k3run/logs}"; mkdir -p "$LOGHOST" + +# Proven fabric env (from cluster_rdma_env_recommender.py) +BOOT_NIC="eth0" +IB_HCA="mlx5_0,mlx5_2,mlx5_3,mlx5_4,mlx5_5,mlx5_7,mlx5_8,mlx5_9" +GID_INDEX=3 + +if [ "$ROLE" = "head" ]; then + NODE_RANK=0; SERVE_EXTRA="--port ${PORT}"; HEADLESS="" +else + NODE_RANK=1; SERVE_EXTRA=""; HEADLESS="--headless" +fi + +echo "[pp2tp8] node=$(hostname -s) role=$ROLE rank=$NODE_RANK master=$MASTER" +[ -f "$MODEL_DIR/model.safetensors.index.json" ] || { echo "ERROR: model missing at $MODEL_DIR"; exit 1; } +docker rm -f "$CONTAINER" 2>/dev/null || true + +docker run -d --name "$CONTAINER" \ + --network host --ipc host \ + --device /dev/kfd --device /dev/dri --device /dev/infiniband --group-add video \ + --cap-add SYS_PTRACE --security-opt seccomp=unconfined --shm-size 128g \ + --ulimit memlock=-1 --ulimit stack=67108864 \ + -e VLLM_ROCM_USE_AITER_MLA=0 \ + -e AITER_SITUV2_A8W4=1 \ + -e MORI_GPU_ARCHS=gfx942 -e MORI_IB_GID_INDEX=3 -e MORI_IB_ENABLE_RELAXED_ORDERING=1 -e MORI_NUM_QP_PER_PE=8 \ + -e NCCL_SOCKET_IFNAME=$BOOT_NIC -e GLOO_SOCKET_IFNAME=$BOOT_NIC \ + -e NCCL_IB_DISABLE=0 -e NCCL_IB_HCA=$IB_HCA -e NCCL_IB_GID_INDEX=$GID_INDEX \ + -e NCCL_IGNORE_CPU_AFFINITY=1 -e NCCL_DEBUG=WARN \ + -e HSA_ENABLE_IPC_MODE_LEGACY=0 -e HSA_NO_SCRATCH_RECLAIM=1 \ + -e PYTORCH_ALLOC_CONF=expandable_segments:False \ + -e PYTORCH_HIP_ALLOC_CONF=expandable_segments:False \ + -v "$MODEL_DIR":/model:ro -v "$LOGHOST":/logs \ + --entrypoint bash \ + "$IMAGE" -c " + vllm serve /model --served-model-name kimi-k3 \ + --tensor-parallel-size 8 --pipeline-parallel-size 2 \ + --distributed-executor-backend mp \ + --enable-expert-parallel --all2all-backend mori_low_latency \ + --nnodes 2 --node-rank ${NODE_RANK} --master-addr ${MASTER} --master-port 29500 ${HEADLESS} \ + --trust-remote-code --reasoning-parser kimi_k3 --mm-encoder-tp-mode data \ + --safetensors-load-strategy prefetch \ + --max-model-len ${MAX_MODEL_LEN} --max-num-seqs ${MAX_NUM_SEQS} \ + --gpu-memory-utilization ${GPU_UTIL} ${SERVE_EXTRA} 2>&1 | tee /logs/vllm_moriep_${ROLE}.log + " +echo "[pp2tp8] $ROLE started. log: $LOGHOST/vllm_moriep_${ROLE}.log" +[ "$ROLE" = head ] && echo "[pp2tp8] health: curl http://${MASTER}:${PORT}/v1/models" diff --git a/scripts/vllm_dissag/ARCHITECTURE.md b/scripts/vllm_dissag/ARCHITECTURE.md index 5e64b83d..1a689c97 100644 --- a/scripts/vllm_dissag/ARCHITECTURE.md +++ b/scripts/vllm_dissag/ARCHITECTURE.md @@ -241,3 +241,54 @@ flowchart TD | `tests/TEST_PLAN.md` | before/after verification plan | | `benchmark_xPyD.sh`, `benchmark_long_context.sh`, `benchmark_niah.{sh,py}`, `benchmark_parser.py`, `parse_to_csv.py` | benchmark + parsing (NIAH = long-context retrieval, vllm#47042) | | `socket_barrier.py`, `socket_wait.py`, `salloc_launch.sh` | node coordination + salloc helper | + +--- + +## 5. Kimi-K3-MXFP4 worker taxonomy (W1–W5) + +K3 disagg uses **TP2×DP8 → EP16 per pool** (not DeepSeek's TP1×DP16). Five logical workers map onto +four Slurm tasks plus a co-located router on rank 0. + +```mermaid +flowchart TB + subgraph prefillPool [PrefillPool xP=2] + W1[W1 prefill_master] + W2[W2 prefill_worker headless] + end + subgraph decodePool [DecodePool yD=2] + W3[W3 decode_master] + W4[W4 decode_worker headless] + end + W5[W5 vllm-router on rank0] + W5 --> W1 + W5 --> W3 + W1 -->|MoRIIO WRITE| W3 + W2 -->|kv_producer ranks 4-7| W4 +``` + +| Worker | Recipe `ROLE=` | `NODE_RANK` | Headless | KV role | K3-specific | +|--------|----------------|-------------|----------|---------|-------------| +| **W1** | `prefill_master` | 0 (+ **W5** router) | no | `kv_producer` | `--tensor-parallel-size 2`, `--api-server-count 8` | +| **W2** | `prefill_worker` | 1 | yes, start-rank 4 | `kv_producer` | **must** carry `--kv-transfer-config` | +| **W3** | `decode_master` | `xP` (2) | no | `kv_consumer` | same TP2 + pod hosts | +| **W4** | `decode_worker` | `xP+1` (3) | yes, start-rank 4 | `kv_consumer` | **must** carry `--kv-transfer-config` | +| **W5** | (router) | 0 only | — | — | `--moriio-dp-size 8`, `--intra-node-data-parallel-size 4` | + +**Required topology:** `xP=2`, `yD=2`, **4 nodes** — enforced in `run_xPyD_models.slurm`. + +**Pod hosts:** `PREFILL_POD_HOSTS` / `DECODE_POD_HOSTS` from `IPADDRS` (first `xP` / next `yD` IPs) go +into each rank's kv JSON as `moriio_pod_hosts`. + +**JIT cache:** K3 prefill (mori HT/LL + cudagraph NONE) and decode (LL + PIECEWISE) compile different +kernel variants; Slurm mounts separate `.../prefill` vs `.../decode` cache dirs under the image key +when `MODEL_NAME=Kimi-K3-MXFP4`. + +Reference standalone launcher: [`../vllm/kimik3_mi300x/wideep_disagg_2p2d/`](../vllm/kimik3_mi300x/wideep_disagg_2p2d/). + +### Docker image (out of scope for MAD merge) + +Kimi-K3-MXFP4 **does not** use `docker/vllm_disagg_inference.ubuntu.amd.Dockerfile`. It has its own +[`Dockerfile.kimik3_disagg`](../vllm/kimik3_mi300x/wideep_disagg_2p2d/Dockerfile.kimik3_disagg) +(vLLM branch `kimi-k3-wideep-disagg-fullsource-v3`, MoRI `--no-build-isolation`, vllm-router). +Upstream integration adds **launcher + yaml + docs** only; operators build/tag +`kimik3-wideep-disagg:latest` separately and pass `DOCKER_IMAGE_NAME` to slurm. diff --git a/scripts/vllm_dissag/README.MD b/scripts/vllm_dissag/README.MD index 17660abe..c5fe6af8 100644 --- a/scripts/vllm_dissag/README.MD +++ b/scripts/vllm_dissag/README.MD @@ -53,6 +53,32 @@ slurm gate rejects any other pairing. Dense models are TP-only; the DeepSeek fam | `DeepSeek-V3` | MoE | 3, 4 (wideEP only) | validated serving (moriio+MoRI-EP) | | `DeepSeek-V3-5layer` | MoE | 3, 4 (wideEP only) | smoke/dev variant | | `DeepSeek-R1` | MoE | 3, 4 (wideEP only) | needs a co-versioned AITER image (see Caveats) | +| `Kimi-K3-MXFP4` | MoE | 3 (moriio wideEP only) | MI300X gfx942 2P/2D; **TP2×DP8**; needs `kimik3-wideep-disagg` image (see [`wideep_disagg_2p2d`](../vllm/kimik3_mi300x/wideep_disagg_2p2d/)) | + +Standalone reference launcher (debug / OCI): [`scripts/vllm/kimik3_mi300x/wideep_disagg_2p2d/`](../vllm/kimik3_mi300x/wideep_disagg_2p2d/). + +See [ARCHITECTURE.md — Kimi-K3 worker taxonomy](ARCHITECTURE.md#5-kimi-k3-mxfp4-worker-taxonomy-w1w5) for W1–W5 ↔ `NODE_RANK` mapping. + +**Separate Docker image (not merged into MAD `docker/`):** build only from +[`Dockerfile.kimik3_disagg`](../vllm/kimik3_mi300x/wideep_disagg_2p2d/Dockerfile.kimik3_disagg). +MAD integration is launcher + yaml; vLLM fork and connector fixes stay in that recipe Dockerfile. + +```bash +# Build (once per cluster / after vLLM pin change): +cd scripts/vllm/kimik3_mi300x/wideep_disagg_2p2d +docker build -f Dockerfile.kimik3_disagg \ + --build-arg VLLM_REF=kimi-k3-wideep-disagg-fullsource-v3 \ + -t kimik3-wideep-disagg:latest . + +# Run via unified launcher: +sbatch -N 4 run_xPyD_models.slurm \ + --export=ALL,MODEL_NAME=Kimi-K3-MXFP4,CONNECTOR=moriio,WIDE_EP=1,xP=2,yD=2,\ +MODEL_PATH=/path/to/Kimi-K3-MXFP4,DOCKER_IMAGE_NAME=kimik3-wideep-disagg:latest +``` + +On-disk weights may be `Kimi-K3` or `Kimi-K3-MXFP4` under `models_blog/`; set `MODEL_PATH` to the +discovered index directory on each node (launcher validates a common path across nodes, or use +the standalone `run_2p2d_launch.sh` per-node `MODEL_DIR_*` overrides for OCI). ## Prerequisites diff --git a/scripts/vllm_dissag/connectors/moriio.sh b/scripts/vllm_dissag/connectors/moriio.sh index 44c64d68..a30b5245 100644 --- a/scripts/vllm_dissag/connectors/moriio.sh +++ b/scripts/vllm_dissag/connectors/moriio.sh @@ -86,10 +86,10 @@ connector_setup_env() { export VLLM_MORIIO_DEFERRED_TIMEOUT_S="${VLLM_MORIIO_DEFERRED_TIMEOUT_S:-1800}" export VLLM_HANDSHAKE_TIMEOUT_MINS="${VLLM_HANDSHAKE_TIMEOUT_MINS:-30}" - export TRITON_CACHE_DIR="${TRITON_CACHE_DIR:-/tmp/vllm_cache/triton}" - export VLLM_CACHE_ROOT="${VLLM_CACHE_ROOT:-/tmp/vllm_cache/vllm}" - export COMGR_CACHE_DIR="${COMGR_CACHE_DIR:-/tmp/vllm_cache/comgr}" - export AITER_JIT_DIR="${AITER_JIT_DIR:-/tmp/vllm_cache/aiter_jit}" + export TRITON_CACHE_DIR="${TRITON_CACHE_DIR:-/opt/vllm_cache/triton}" + export VLLM_CACHE_ROOT="${VLLM_CACHE_ROOT:-/opt/vllm_cache/vllm}" + export COMGR_CACHE_DIR="${COMGR_CACHE_DIR:-/opt/vllm_cache/comgr}" + export AITER_JIT_DIR="${AITER_JIT_DIR:-/opt/vllm_cache/aiter_jit}" mkdir -p "${TRITON_CACHE_DIR}" "${VLLM_CACHE_ROOT}" "${COMGR_CACHE_DIR}" "${AITER_JIT_DIR}" 2>/dev/null || true if [[ "${VLLM_ROCM_USE_AITER:-1}" == "1" ]]; then @@ -118,9 +118,23 @@ connector_setup_env() { export MORI_SHMEM_HEAP_SIZE="${MORI_SHMEM_HEAP_SIZE:-17179869184}" } +_moriio_is_kimik3() { [[ "${MODEL_NAME:-}" == "Kimi-K3-MXFP4" ]]; } + _moriio_build_kv_transfer_config() { local kv_role="$1" - echo '{"kv_connector":"MoRIIOConnector","kv_role":"'"${kv_role}"'","kv_port":"'"${KV_PORT}"'","kv_connector_extra_config":{"proxy_ip":"'"${MASTER_ADDR}"'","proxy_port":"'"${PROXY_PORT}"'","proxy_ping_port":"'"${PROXY_PING_PORT}"'","http_port":"'"${SERVE_PORT}"'","local_ping_port":"'"${LOCAL_PING_PORT}"'","handshake_port":"'"${HANDSHAKE_PORT}"'","notify_port":"'"${NOTIFY_PORT}"'"}}' + local _proxy_port="${PROXY_PORT}" + local _pod_hosts="" + if _moriio_is_kimik3; then + _proxy_port="${ROUTER_PORT:-30000}" + if [[ "$kv_role" == "kv_producer" ]]; then + _pod_hosts="${DECODE_POD_HOSTS:-}" + else + _pod_hosts="${PREFILL_POD_HOSTS:-}" + fi + fi + local _pod_json="" + [[ -n "$_pod_hosts" ]] && _pod_json=',"moriio_pod_hosts":"'"${_pod_hosts}"'"' + echo '{"kv_connector":"MoRIIOConnector","kv_role":"'"${kv_role}"'","kv_port":"'"${KV_PORT}"'","kv_connector_extra_config":{"proxy_ip":"'"${MASTER_ADDR}"'","proxy_port":"'"${_proxy_port}"'","proxy_ping_port":"'"${PROXY_PING_PORT}"'","http_port":"'"${SERVE_PORT}"'","local_ping_port":"'"${LOCAL_PING_PORT}"'","handshake_port":"'"${HANDSHAKE_PORT}"'","notify_port":"'"${NOTIFY_PORT}"'"'"${_pod_json}"'}}' } connector_runtime_patch() { @@ -173,22 +187,39 @@ connector_launch_worker() { # Per-model flags from models.yaml (driver-exported; empty if none). local model_args=() local _mc; if [[ "$log_prefix" == "prefill" ]]; then _mc="${MODEL_CONFIG_PREFILL:-}"; else _mc="${MODEL_CONFIG_DECODE:-}"; fi - [[ -n "$_mc" ]] && eval "model_args=(${_mc})" + _model_config_to_array "$_mc" model_args if parallelism_is_wide_ep; then # ---- WIDE_EP=1 (MoriEP) ---- # Per-role all2all: prefill=high_throughput, decode=low_latency. The # v1.2.0 image rejects the bare "mori" alias; these names are required. + # Kimi-K3-MXFP4 uses TP2×DP8 per pool (not -tp 1); see wideep_disagg_2p2d. local _all2all="${PREFILL_MORI_BACKEND}" [[ "$log_prefix" == "decode" ]] && _all2all="${DECODE_MORI_BACKEND}" + local _tp_flag=(-tp 1) + local _dp_local="${DP_PARALLEL_SIZE_LOCAL}" + local _effective_dp="${dp_size}" + if _moriio_is_kimik3; then + local _k3_tp="${KIMIK3_TP_SIZE:-2}" + _tp_flag=(--tensor-parallel-size "${_k3_tp}") + _dp_local=$(( _GPUS_PER_NODE / _k3_tp )) + _effective_dp=$(( dp_size / _k3_tp )) + fi + local extra_args=() kv_args=() + local kv_config; kv_config=$(_moriio_build_kv_transfer_config "${kv_role}") if [[ "$role" == "master" ]]; then - extra_args+=(--api-server-count=${_GPUS_PER_NODE}) - local kv_config; kv_config=$(_moriio_build_kv_transfer_config "${kv_role}") + if _moriio_is_kimik3; then + extra_args+=(--api-server-count="${_effective_dp}") + else + extra_args+=(--api-server-count=${_GPUS_PER_NODE}) + fi kv_args+=(--kv-transfer-config "${kv_config}") else extra_args+=(--data-parallel-start-rank "${dp_start_rank}" --headless) + # K3 headless workers host real DP ranks; they MUST carry kv-transfer-config. + _moriio_is_kimik3 && kv_args+=(--kv-transfer-config "${kv_config}") fi # Recipe knobs (overridable via env / models.yaml). DeepSeek-V3 on AITER @@ -200,13 +231,15 @@ connector_launch_worker() { local _kvdtype="${KV_CACHE_DTYPE:-fp8}" local mem_args=() [[ -n "${KV_CACHE_MEMORY_BYTES:-}" ]] && mem_args+=(--kv-cache-memory-bytes "${KV_CACHE_MEMORY_BYTES}") + local _max_batched=() + [[ -n "${MAX_NUM_BATCHED_TOKENS:-}" ]] && _max_batched=(--max-num-batched-tokens "${MAX_NUM_BATCHED_TOKENS}") if [[ "${DRY_RUN:-0}" == "1" ]]; then _dryrun_emit "moriio" "${log_prefix}" "${role}" \ vllm serve "${MODEL_PATH}" \ - -tp 1 \ - --data-parallel-size "${dp_size}" \ - --data-parallel-size-local "${DP_PARALLEL_SIZE_LOCAL}" \ + "${_tp_flag[@]}" \ + --data-parallel-size "${_effective_dp}" \ + --data-parallel-size-local "${_dp_local}" \ --data-parallel-address "${dp_addr}" \ --data-parallel-rpc-port "${RPC_PORT}" \ --enable-expert-parallel \ @@ -219,14 +252,15 @@ connector_launch_worker() { --all2all-backend "${_all2all}" \ --trust-remote-code \ --distributed-timeout-seconds "${DISTRIBUTED_TIMEOUT_SECONDS:-7200}" \ - "${exec_args[@]}" "${extra_args[@]}" "${kv_args[@]}" + "${_max_batched[@]}" \ + "${exec_args[@]}" "${model_args[@]}" "${extra_args[@]}" "${kv_args[@]}" WORKER_PID=0; return 0 fi vllm serve ${MODEL_PATH} \ - -tp 1 \ - --data-parallel-size "${dp_size}" \ - --data-parallel-size-local ${DP_PARALLEL_SIZE_LOCAL} \ + "${_tp_flag[@]}" \ + --data-parallel-size "${_effective_dp}" \ + --data-parallel-size-local ${_dp_local} \ --data-parallel-address "${dp_addr}" \ --data-parallel-rpc-port ${RPC_PORT} \ --enable-expert-parallel \ @@ -239,7 +273,9 @@ connector_launch_worker() { --all2all-backend "${_all2all}" \ --trust-remote-code \ --distributed-timeout-seconds ${DISTRIBUTED_TIMEOUT_SECONDS:-7200} \ + "${_max_batched[@]}" \ "${exec_args[@]}" \ + "${model_args[@]}" \ "${extra_args[@]}" \ "${kv_args[@]}" \ 2>&1 | tee /run_logs/${SLURM_JOB_ID}/${log_prefix}_NODE${NODE_RANK}.log >/dev/null & @@ -323,7 +359,13 @@ connector_start_proxy() { # to DP ranks 0..7 while the TP server only has rank 0 -> every non-rank-0 # request fails "data_parallel_rank N out of range [0,1)" (7/8 -> 500). local _router_dp_local="${DP_PARALLEL_SIZE_LOCAL}" + local _router_moriio_dp="" parallelism_is_wide_ep || _router_dp_local=1 + if _moriio_is_kimik3; then + local _k3_tp="${KIMIK3_TP_SIZE:-2}" + _router_dp_local=$(( _GPUS_PER_NODE / _k3_tp )) + _router_moriio_dp=$(( (xP * _GPUS_PER_NODE) / _k3_tp )) + fi echo "Starting vllm-router (MoRIIO): HTTP ${ROUTER_PORT}" echo " prefill=${PREFILL_URL} decode=${DECODE_URL} dp_local=${_router_dp_local}" [ -f /root/.cargo/env ] && source /root/.cargo/env @@ -336,6 +378,8 @@ connector_start_proxy() { fi echo "Using vllm-router binary: ${ROUTER_BIN}" local _PROMETHEUS_PORT="${VLLM_ROUTER_PROMETHEUS_PORT:-29000}" + local _router_extra=() + [[ -n "$_router_moriio_dp" ]] && _router_extra+=(--moriio-dp-size "${_router_moriio_dp}") "${ROUTER_BIN}" \ --host 0.0.0.0 \ --port "${ROUTER_PORT}" \ @@ -345,6 +389,7 @@ connector_start_proxy() { --decode "${DECODE_URL}" \ --vllm-discovery-address "0.0.0.0:${PROXY_PING_PORT}" \ --intra-node-data-parallel-size "${_router_dp_local}" \ + "${_router_extra[@]}" \ --policy round_robin \ --prefill-policy round_robin \ --decode-policy round_robin \ diff --git a/scripts/vllm_dissag/connectors/rixl.sh b/scripts/vllm_dissag/connectors/rixl.sh index e6dda9b9..dcd94b83 100644 --- a/scripts/vllm_dissag/connectors/rixl.sh +++ b/scripts/vllm_dissag/connectors/rixl.sh @@ -239,7 +239,7 @@ _rixl_launch_tp() { # Per-model config string (from models.yaml; tokenized as the legacy eval did) local cfg_args=() local _mc; if [[ "$log_prefix" == "prefill" ]]; then _mc="${MODEL_CONFIG_PREFILL:-}"; else _mc="${MODEL_CONFIG_DECODE:-}"; fi - [[ -n "$_mc" ]] && eval "cfg_args=(${_mc})" + _model_config_to_array "$_mc" cfg_args if [[ "${DRY_RUN:-0}" == "1" ]]; then _dryrun_emit "rixl" "${log_prefix}" "${role}" \ @@ -295,7 +295,7 @@ _rixl_launch_deepep() { # dp: tuning is actually honored on the deepep path. local model_args=() local _mc; if [[ "$log_prefix" == "prefill" ]]; then _mc="${MODEL_CONFIG_PREFILL:-}"; else _mc="${MODEL_CONFIG_DECODE:-}"; fi - [[ -n "$_mc" ]] && eval "model_args=(${_mc})" + _model_config_to_array "$_mc" model_args if [[ "${DRY_RUN:-0}" == "1" ]]; then _dryrun_emit "deepep" "${log_prefix}" "${role}" \ diff --git a/scripts/vllm_dissag/models.yaml b/scripts/vllm_dissag/models.yaml index 23d66059..3dec6e8b 100644 --- a/scripts/vllm_dissag/models.yaml +++ b/scripts/vllm_dissag/models.yaml @@ -178,3 +178,38 @@ DeepSeek-R1: dp: "" decode: dp: "" + +# Kimi-K3-MXFP4 (MI300X gfx942 wideEP disagg). Recipe knobs from +# scripts/vllm/kimik3_mi300x/wideep_disagg_2p2d/; vLLM connector fixes live in the +# disagg image (raviguptaamd/vllm @ kimi-k3-wideep-disagg-fullsource-v3). +_kimik3_recipe_env: &kimik3_recipe_env + VLLM_USE_V1: "1" + VLLM_ROCM_USE_AITER: "1" + VLLM_ROCM_USE_AITER_MLA: "0" + VLLM_ROCM_USE_AITER_MOE: "1" + VLLM_ROCM_USE_AITER_FUSION_SHARED_EXPERTS: "0" + VLLM_USE_AITER_TRITON_SILU_MUL: "0" + VLLM_ROCM_USE_AITER_RMSNORM: "1" + AITER_SITUV2_A8W4: "1" + VLLM_SSM_CONV_STATE_LAYOUT: "DS" + KV_BLOCK_SIZE: "16" + KV_CACHE_DTYPE: "fp8" + KV_CACHE_MEMORY_BYTES: "8000000000" + GPU_MEMORY_UTILIZATION: "0.85" + MAX_NUM_BATCHED_TOKENS: "2048" + MAX_MODEL_LEN: "320000" + PREFILL_CUDAGRAPH_MODE: "NONE" + # Validated standalone launch uses DECODE_CG=NONE (run_2p2d_launch.sh default). + # PIECEWISE hangs at cudagraph capture 5/9 on K3+MoRI LL — F25; fix in Session 3 image. + DECODE_CUDAGRAPH_MODE: "NONE" + VLLM_ALL2ALL_BACKEND: "mori_low_latency" + PREFILL_MORI_BACKEND: "mori_low_latency" + DECODE_MORI_BACKEND: "mori_low_latency" + MORI_SHMEM_HEAP_SIZE: "17179869184" + +Kimi-K3-MXFP4: + env: *kimik3_recipe_env + prefill: + dp: '--served-model-name kimi-k3 --reasoning-parser kimi_k3 --mm-encoder-tp-mode data --safetensors-load-strategy lazy --max-model-len 320000 --max-num-seqs 8 --quantization-config ''{"moe":{"weight":"int4_per_group_32"}}''' + decode: + dp: '--served-model-name kimi-k3 --reasoning-parser kimi_k3 --mm-encoder-tp-mode data --safetensors-load-strategy lazy --max-model-len 320000 --max-num-seqs 8 --quantization-config ''{"moe":{"weight":"int4_per_group_32"}}''' diff --git a/scripts/vllm_dissag/run_xPyD_models.slurm b/scripts/vllm_dissag/run_xPyD_models.slurm index c71fc7e8..95a22681 100755 --- a/scripts/vllm_dissag/run_xPyD_models.slurm +++ b/scripts/vllm_dissag/run_xPyD_models.slurm @@ -87,6 +87,7 @@ VALID_MODELS=( \ "DeepSeek-R1" \ "Qwen3-32B" \ "Qwen3-30B-A3B" \ + "Kimi-K3-MXFP4" \ ) # Models allowed for CONNECTOR=moriio WIDE_EP=1 (MoRI-EP; legacy RUN_MORI=1) @@ -94,6 +95,7 @@ MORI_EP_VALID_MODELS=( \ "DeepSeek-V3" \ "DeepSeek-V3-5layer" \ "DeepSeek-R1" \ + "Kimi-K3-MXFP4" \ ) # Models allowed for CONNECTOR=rixl WIDE_EP=1 EP_BACKEND=deepep (legacy RUN_DEEPEP=1) @@ -179,7 +181,7 @@ WIDE_EP="${WIDE_EP:-0}" # the MoRI-EP / DeepEP recipe (block=16, MLA off, per-role cudagraph). Running them # in TP mode is unsupported — the TP argv would double the model's own # --compilation-config and drop the mandatory +quant_fp8 op. Reject early. -WIDE_EP_ONLY_MODELS=( "DeepSeek-V3" "DeepSeek-V3-5layer" "DeepSeek-R1" ) +WIDE_EP_ONLY_MODELS=( "DeepSeek-V3" "DeepSeek-V3-5layer" "DeepSeek-R1" "Kimi-K3-MXFP4" ) model_is_wide_ep_only() { local m="$1" for x in "${WIDE_EP_ONLY_MODELS[@]}"; do [[ "$m" == "$x" ]] && return 0; done @@ -250,6 +252,18 @@ LOG_PATH="${LOG_PATH:-/shared_inference/${USER}/model_blog_logs}" xP="${xP:-1}" #-> Number of Prefill Servers yD="${yD:-1}" #-> Number of Decode Servers +# Kimi-K3-MXFP4 disagg requires validated 2P/2D topology (TP2×DP8 per pool). +if [[ "$MODEL_NAME" == "Kimi-K3-MXFP4" ]]; then + if [[ "$WIDE_EP" != "1" || "$CONNECTOR" != "moriio" ]]; then + echo "Error: Kimi-K3-MXFP4 requires CONNECTOR=moriio WIDE_EP=1 (combo 3)." >&2 + exit 1 + fi + if [[ "$xP" -ne 2 || "$yD" -ne 2 ]]; then + echo "Error: Kimi-K3-MXFP4 requires xP=2 yD=2 (2P/2D disagg); got xP=$xP yD=$yD." >&2 + exit 1 + fi +fi + MODEL_DIR="${MODEL_DIR:-"/shared_inference/models_blog/"}" @@ -328,6 +342,10 @@ echo "" # Calculate NUM_NODES based on xP and yD NUM_NODES=$((xP + yD)) echo "Calculated NUM_NODES: $NUM_NODES (xP=$xP + yD=$yD, proxy co-located on prefill master)" +if [[ "$MODEL_NAME" == "Kimi-K3-MXFP4" && "$NUM_NODES" -ne 4 ]]; then + echo "Error: Kimi-K3-MXFP4 requires 4 nodes (xP=2 + yD=2); NUM_NODES=$NUM_NODES." >&2 + exit 1 +fi # DeepEP configuration (only exported when RUN_DEEPEP=1) if [[ "$_run_deepep" == "1" ]]; then @@ -504,7 +522,18 @@ if [ "${JIT_CACHE_PERSIST:-1}" = "1" ]; then _IMG_RAW=$(docker image inspect --format "{{.Id}}" "$DOCKER_IMAGE_NAME" 2>/dev/null); _IMG_KEY=$(printf "%s" "$_IMG_RAW" | tr -cd "a-f0-9" | cut -c1-12); _IMG_KEY="${_IMG_KEY:-noimg}"; - _JIT_CACHE_HOST="${JIT_CACHE_HOST:-/mnt/m2m_nobackup/${USER}/vllm_jit_cache/${_IMG_KEY}}"; + _JIT_BASE="${JIT_CACHE_HOST:-/mnt/m2m_nobackup/${USER}/vllm_jit_cache/${_IMG_KEY}}"; + # Kimi-K3: prefill and decode compile different AITER kernel variants — separate caches. + if [ "${MODEL_NAME}" = "Kimi-K3-MXFP4" ] && [ "${JIT_CACHE_SPLIT_K3:-1}" = "1" ]; then + if [ "${SLURM_PROCID}" -lt "${xP}" ]; then + _JIT_ROLE="prefill"; + else + _JIT_ROLE="decode"; + fi + _JIT_CACHE_HOST="${_JIT_BASE}/${_JIT_ROLE}"; + else + _JIT_CACHE_HOST="${_JIT_BASE}"; + fi mkdir -p "$_JIT_CACHE_HOST"/{aiter_jit,triton,vllm,comgr} 2>/dev/null || true; _JIT_CACHE_MOUNT="-v ${_JIT_CACHE_HOST}:/opt/vllm_cache"; echo "[jit-cache] persistent image ${_IMG_KEY}: ${_JIT_CACHE_HOST} to /opt/vllm_cache"; diff --git a/scripts/vllm_dissag/tests/argv_assert.sh b/scripts/vllm_dissag/tests/argv_assert.sh index 9c85da89..9e7b2821 100755 --- a/scripts/vllm_dissag/tests/argv_assert.sh +++ b/scripts/vllm_dissag/tests/argv_assert.sh @@ -24,6 +24,7 @@ _argv() { # connector wide_ep ep_backend model model_path } _has() { grep -qF -- "$2" <<<"$1" && { printf " PASS %s\n" "$3"; pass=$((pass+1)); } || { printf " FAIL %s (missing: %s)\n" "$3" "$2"; fail=$((fail+1)); }; } +_hasre() { grep -qE -- "$2" <<<"$1" && { printf " PASS %s\n" "$3"; pass=$((pass+1)); } || { printf " FAIL %s (missing: %s)\n" "$3" "$2"; fail=$((fail+1)); }; } _hasnot(){ grep -qF -- "$2" <<<"$1" && { printf " FAIL %s (unexpected: %s)\n" "$3" "$2"; fail=$((fail+1)); } || { printf " PASS %s\n" "$3"; pass=$((pass+1)); }; } _count() { local n; n="$(grep -cF -- "$2" <<<"$1")"; [[ "$n" == "$3" ]] && { printf " PASS %s (=%s)\n" "$4" "$n"; pass=$((pass+1)); } || { printf " FAIL %s (got %s want %s)\n" "$4" "$n" "$3"; fail=$((fail+1)); }; } @@ -47,6 +48,30 @@ _has "$B" "16" "block-size value 16 present" _count "$B" "--compilation-config" 1 "exactly one --compilation-config" _hasnot "$B" "--tensor-parallel-size" "no --tensor-parallel-size (uses -tp 1)" +echo "" +echo "=== moriio + wideEP (Kimi-K3-MXFP4, 2P/2D TP2×DP8) ===" +_argv_k3() { + env -i PATH="$PATH" HOME="$HOME" NIXL_COOKBOOK_PATH="$DIR" \ + DRY_RUN=1 NODE_RANK=0 xP=2 yD=2 CONNECTOR=moriio WIDE_EP=1 EP_BACKEND=mori \ + MODEL_NAME=Kimi-K3-MXFP4 MODEL_PATH=/m/K3 \ + MASTER_ADDR=10.0.0.1 IPADDRS=10.0.0.1,10.0.0.2,10.0.0.3,10.0.0.4 \ + GPUS_PER_NODE=8 SLURM_JOB_ID=ASSERT PROXY_TYPE=vllm_router ROUTER_PORT=30000 \ + bash "$DIR/vllm_disagg.sh" 2>/dev/null | awk '/^===DRYRUN/{f=1;next} /^===END===/{f=0} f' +} +C="$(_argv_k3)" +_has "$C" "--tensor-parallel-size" "K3 has --tensor-parallel-size" +_hasre "$C" "^2$" "K3 TP=2" +_has "$C" "--data-parallel-size" "K3 has --data-parallel-size" +_hasre "$C" "^8$" "K3 dp_size=8 (line)" +_has "$C" "--data-parallel-size-local" "K3 has dp_local flag" +_hasre "$C" "^4$" "K3 dp_local=4 (line)" +_has "$C" "--enable-expert-parallel" "K3 has EP" +_has "$C" "moriio_pod_hosts" "K3 kv config has pod hosts" +_has "$C" "--api-server-count=8" "K3 api-server-count=dp_size" +_has "$C" "--reasoning-parser" "K3 reasoning parser flag" +_has "$C" "kimi_k3" "K3 reasoning parser value" +_hasnot "$C" "-tp 1" "K3 not -tp 1" + echo "" echo "=== connector platform env files carry the RDMA-fix env ===" # The ROCm-7.2.3 GPU-RDMA env now lives in per-connector .env files; the slurm diff --git a/scripts/vllm_dissag/tests/drive_cell.sh b/scripts/vllm_dissag/tests/drive_cell.sh index 13a286ba..eba7bc44 100755 --- a/scripts/vllm_dissag/tests/drive_cell.sh +++ b/scripts/vllm_dissag/tests/drive_cell.sh @@ -39,7 +39,7 @@ RUN_TAG="int${JOBID}_${TAG}" HOSTRUN="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/run_interactive.sh" # env forwarded into each srun step (SLURM_JOB_ID set to RUN_TAG only INSIDE bash -c). -FWD="DOCKER_IMAGE_NAME MODEL_NAME MODEL_PATH CONNECTOR WIDE_EP EP_BACKEND xP yD NNODES IPADDRS MASTER_ADDR MASTER_PORT BENCHMARK_CON BENCHMARK_COMBINATIONS BENCHMARK_ITR PROXY_TYPE ROUTER_PORT ROUTER_BINARY RUN_MORI RUN_DEEPEP" +FWD="DOCKER_IMAGE_NAME MODEL_NAME MODEL_PATH CONNECTOR WIDE_EP EP_BACKEND xP yD NNODES IPADDRS MASTER_ADDR MASTER_PORT BENCHMARK_CON BENCHMARK_COMBINATIONS BENCHMARK_ITR PROXY_TYPE ROUTER_PORT ROUTER_BINARY RUN_MORI RUN_DEEPEP DECODE_CUDAGRAPH_MODE LOG_PATH" EXPORTS="" for v in $FWD; do EXPORTS="$EXPORTS $v=\"${!v:-}\""; done EXPORTS="$EXPORTS SLURM_JOB_ID=\"$RUN_TAG\"" diff --git a/scripts/vllm_dissag/tests/run_interactive.sh b/scripts/vllm_dissag/tests/run_interactive.sh index d6e1e67a..8a2ed330 100755 --- a/scripts/vllm_dissag/tests/run_interactive.sh +++ b/scripts/vllm_dissag/tests/run_interactive.sh @@ -37,10 +37,22 @@ mkdir -p /tmp/vllm_cache/{aiter_jit,triton,vllm,comgr} 2>/dev/null || true if [[ "${JIT_CACHE_PERSIST:-1}" == "1" ]]; then _IMG_KEY="$(docker image inspect --format '{{.Id}}' "$DOCKER_IMAGE_NAME" 2>/dev/null | sed 's/^sha256://; s/[^a-f0-9]//g' | cut -c1-12)" _IMG_KEY="${_IMG_KEY:-noimg}" - _JIT_CACHE_HOST="${JIT_CACHE_HOST:-/mnt/m2m_nobackup/${USER}/vllm_jit_cache/${_IMG_KEY}}" + _JIT_BASE="${JIT_CACHE_HOST:-/mnt/m2m_nobackup/${USER}/vllm_jit_cache/${_IMG_KEY}}" + # Kimi-K3: prefill and decode compile different AITER kernel variants — separate caches + # (same logic as run_xPyD_models.slurm; missing this caused PIECEWISE decode hang — F25). + if [[ "${MODEL_NAME}" == "Kimi-K3-MXFP4" && "${JIT_CACHE_SPLIT_K3:-1}" == "1" ]]; then + if [[ "${NODE_RANK:-0}" -lt "${xP:-1}" ]]; then + _JIT_ROLE="prefill" + else + _JIT_ROLE="decode" + fi + _JIT_CACHE_HOST="${_JIT_BASE}/${_JIT_ROLE}" + else + _JIT_CACHE_HOST="${_JIT_BASE}" + fi mkdir -p "$_JIT_CACHE_HOST"/{aiter_jit,triton,vllm,comgr} 2>/dev/null || true _JIT_CACHE_MOUNT="-v ${_JIT_CACHE_HOST}:/opt/vllm_cache" - echo "JIT cache (persistent, image ${_IMG_KEY}): ${_JIT_CACHE_HOST} -> /opt/vllm_cache" + echo "JIT cache (persistent, image ${_IMG_KEY}${_JIT_ROLE:+/${_JIT_ROLE}}): ${_JIT_CACHE_HOST} -> /opt/vllm_cache" else _JIT_CACHE_MOUNT="" fi @@ -49,14 +61,14 @@ fi _RDMA_MOUNTS="" _LIBDIR=/usr/lib/x86_64-linux-gnu for _lib in libibverbs.so libibverbs.so.1 librdmacm.so librdmacm.so.1; do - [ -e "$_LIBDIR/$_lib" ] && _RDMA_MOUNTS="$_RDMA_MOUNTS -v $_LIBDIR/$_lib:$_LIBDIR/$_lib:ro" + [ -f "$_LIBDIR/$_lib" ] && _RDMA_MOUNTS="$_RDMA_MOUNTS -v $_LIBDIR/$_lib:$_LIBDIR/$_lib:ro" done for _vlib in $_LIBDIR/libibverbs.so.1.* $_LIBDIR/librdmacm.so.1.*; do - [ -e "$_vlib" ] && _RDMA_MOUNTS="$_RDMA_MOUNTS -v $_vlib:$_vlib:ro" + [ -f "$_vlib" ] && _RDMA_MOUNTS="$_RDMA_MOUNTS -v $_vlib:$_vlib:ro" done for _pattern in libmlx5.so* libionic*.so* libbnxt_re*.so* libefa.so* libhns.so*; do for _vlib in $_LIBDIR/${_pattern}; do - [ -e "$_vlib" ] && _RDMA_MOUNTS="$_RDMA_MOUNTS -v $_vlib:$_vlib:ro" + [ -f "$_vlib" ] && _RDMA_MOUNTS="$_RDMA_MOUNTS -v $_vlib:$_vlib:ro" done done [ -d "$_LIBDIR/libibverbs" ] && _RDMA_MOUNTS="$_RDMA_MOUNTS -v $_LIBDIR/libibverbs:$_LIBDIR/libibverbs:ro" @@ -108,6 +120,7 @@ docker run --rm \ -e HSA_ENABLE_IPC_MODE_LEGACY=${HSA_ENABLE_IPC_MODE_LEGACY:-0} \ -e MORI_GPU_ARCHS=${MORI_GPU_ARCHS:-gfx942} \ -e HSA_NO_SCRATCH_RECLAIM=${HSA_NO_SCRATCH_RECLAIM:-1} \ + ${DECODE_CUDAGRAPH_MODE:+-e DECODE_CUDAGRAPH_MODE=$DECODE_CUDAGRAPH_MODE} \ --name $DOCKER_CONT_NAME \ $DOCKER_IMAGE_NAME -c " mkdir -p /run_logs/${SLURM_JOB_ID} diff --git a/scripts/vllm_dissag/vllm_disagg.sh b/scripts/vllm_dissag/vllm_disagg.sh index 06fbf84f..7e6dbabf 100755 --- a/scripts/vllm_dissag/vllm_disagg.sh +++ b/scripts/vllm_dissag/vllm_disagg.sh @@ -82,6 +82,9 @@ yD="${yD:-1}" echo "[vllm_disagg] topology: xP=${xP} yD=${yD} (total nodes=$((xP + yD)))" IPADDRS="${IPADDRS:-localhost}" IFS=',' read -ra IP_ARRAY <<< "${IPADDRS}" +PREFILL_POD_HOSTS="$(IFS=,; echo "${IP_ARRAY[*]:0:xP}")" +DECODE_POD_HOSTS="$(IFS=,; echo "${IP_ARRAY[*]:xP:yD}")" +export PREFILL_POD_HOSTS DECODE_POD_HOSTS echo "Listing NIXL_COOKBOOK_PATH: ${NIXL_COOKBOOK_PATH:-}" [[ -n "${NIXL_COOKBOOK_PATH:-}" ]] && ls "${NIXL_COOKBOOK_PATH}" @@ -100,6 +103,13 @@ PREFILL_DP_START_RANK=$(( NODE_RANK * _GPUS_PER_NODE )) PREFILL_MASTER_ADDR=$(echo "$IPADDRS" | awk -F',' '{print $1}') DECODE_DP_START_RANK=$(( (NODE_RANK - xP) * _GPUS_PER_NODE )) DECODE_MASTER_ADDR=$(echo "$IPADDRS" | awk -F',' -v pos="$xP" '{print $(pos+1)}') +# Kimi-K3 disagg: TP2×DP8 per pool -> 4 DP ranks/node (not 8). +if [[ "${MODEL_NAME:-}" == "Kimi-K3-MXFP4" && "${WIDE_EP:-0}" == "1" ]]; then + _k3_tp="${KIMIK3_TP_SIZE:-2}" + DP_PARALLEL_SIZE_LOCAL=$(( _GPUS_PER_NODE / _k3_tp )) + PREFILL_DP_START_RANK=$(( NODE_RANK * DP_PARALLEL_SIZE_LOCAL )) + DECODE_DP_START_RANK=$(( (NODE_RANK - xP) * DP_PARALLEL_SIZE_LOCAL )) +fi # ============================================================================= # Driver helper functions (shared by all connectors) @@ -201,6 +211,15 @@ PY fi export MODEL_CONFIG_PREFILL MODEL_CONFIG_DECODE +# Tokenize models.yaml flag strings without bash eval (JSON in --quantization-config breaks eval). +_model_config_to_array() { + local _mc="$1" + local -n _out="$2" + _out=() + [[ -z "$_mc" ]] && return 0 + mapfile -t _out < <(python3 -c 'import shlex,sys; print("\n".join(shlex.split(sys.argv[1])))' "$_mc") +} + # ============================================================================= # Load parallelism + connector, then initialize # =============================================================================