diff --git a/.agents/skills/model-microbenchmark/SKILL.md b/.agents/skills/model-microbenchmark/SKILL.md index 6da792aba4de..b1bfabeb578f 100644 --- a/.agents/skills/model-microbenchmark/SKILL.md +++ b/.agents/skills/model-microbenchmark/SKILL.md @@ -45,7 +45,8 @@ Key flags (plus all `EngineArgs`): - `--kv-len-jitter 0.5` (default): prefixes drawn within ±50%, same on every DP rank, capped at `max_model_len`. Use `0` for exact lengths. -- `--routing auto|real|balanced`: `auto` = balanced with dummy weights. +- `--routing auto|real|balanced|zipf`: `auto` = Zipf with dummy weights; + `zipf` uses fixed alpha=1.25 skew (target only, no PCP or grouped top-k). - `--iters 20 --samples 5`, `--output-json out.json` (per-rank latency stats, `samples_us`, graph-vs-eager check). - Defaults: `--load-format dummy`, prefix caching off, no torch.compile. @@ -95,7 +96,7 @@ that the JSON summary excludes. ## Caveats - **Dummy weights lie for data-dependent kernels.** Dummy gates route every - token to the same experts (hence balanced routing). Cross-check suspicious + token to the same experts (hence synthetic routing). Cross-check suspicious kernels with real weights. - **torch.compile models (e.g. Qwen3/3.5) look slow**: no compile here. Use them for correctness only; trust perf for models under `vllm/models/`. diff --git a/.agents/skills/model-recipes/SKILL.md b/.agents/skills/model-recipes/SKILL.md index f6ee22b1946f..5100b3938a00 100644 --- a/.agents/skills/model-recipes/SKILL.md +++ b/.agents/skills/model-recipes/SKILL.md @@ -7,9 +7,9 @@ description: Find vLLM deployment recipes and configs for LLMs and Hugging Face Model serving recipes are maintained in this repository's [recipes/models/ directory](../../../recipes/models). Omni (image, video, -audio) recipes stay in vllm-project/recipes for now (moving to vllm-omni), -and Ascend NPU content is inline in these recipes (moving to vllm-ascend -overlays). The published catalog, rendered by +audio) recipes live in vllm-project/vllm-omni, and Ascend NPU content lives in +vllm-project/vllm-ascend as overlays merged into these recipes. The published +catalog, rendered by [vllm-project/recipes](https://github.com/vllm-project/recipes), is at [recipes.vllm.ai](https://recipes.vllm.ai). diff --git a/.buildkite/amd-disagg/cluster.sh b/.buildkite/amd-disagg/cluster.sh index a8906e6e578b..d71c9eb410df 100644 --- a/.buildkite/amd-disagg/cluster.sh +++ b/.buildkite/amd-disagg/cluster.sh @@ -156,6 +156,21 @@ export MORI_GPU_ARCHS="gfx950" # path fails to come up. export MORI_ENABLE_HOST_PROXY="${MORI_ENABLE_HOST_PROXY:-1}" +# NIXL UCX defaults: RC on ionic_0, GPU-direct RDMA. +if [[ "${KV_CONNECTOR:-}" == Nixl* ]]; then + export UCX_TLS="${UCX_TLS:-rc_verbs,ud_verbs,tcp,self,rocm}" + export UCX_NET_DEVICES="${UCX_NET_DEVICES:-ionic_0:1,lo}" + export UCX_IB_GID_INDEX="${UCX_IB_GID_INDEX:-${_IB_GID_INDEX}}" + export UCX_UD_VERBS_ETH_DGID_CHECK="${UCX_UD_VERBS_ETH_DGID_CHECK:-n}" + export UCX_IB_GPU_DIRECT_RDMA="${UCX_IB_GPU_DIRECT_RDMA:-yes}" + export UCX_RMA_PPLN_ENABLE="${UCX_RMA_PPLN_ENABLE:-y}" + export UCX_PROTO_EMULATION_ENABLE="${UCX_PROTO_EMULATION_ENABLE:-n}" + export UCX_KEEPALIVE_INTERVAL="${UCX_KEEPALIVE_INTERVAL:-inf}" + export UCX_UD_VERBS_RX_QUEUE_LEN="${UCX_UD_VERBS_RX_QUEUE_LEN:-1024}" + export UCX_RCACHE_MAX_UNRELEASED="${UCX_RCACHE_MAX_UNRELEASED:-1024}" + export ACCURACY_NUM_CONCURRENT="${ACCURACY_NUM_CONCURRENT:-1}" +fi + # ----------------------------------------------------------------- benchmark export BENCHMARK_COMBINATIONS="${BENCHMARK_COMBINATIONS:-1024/128 2048/128}" export BENCHMARK_CON="${BENCHMARK_CON:-32 64}" diff --git a/.buildkite/amd-disagg/pipeline-disagg.yaml b/.buildkite/amd-disagg/pipeline-disagg.yaml index 03e6d0eeccc4..71c7b8387d86 100644 --- a/.buildkite/amd-disagg/pipeline-disagg.yaml +++ b/.buildkite/amd-disagg/pipeline-disagg.yaml @@ -414,3 +414,58 @@ steps: commands: - MODEL_NAME=GLM-5.2-FP8 xP=2 yD=2 NODES=4 GPUS_PER_NODE=8 WIDE_EP_MODE=1 MORIIO_READ_MODE=0 RUN_AFTER_HEALTH=accuracy ROUTER_TYPE=vllm-router WAIT=1 bash .buildkite/amd-disagg/run-slurm-disagg-test.sh +#-------------------------------------- mi350 · disagg NIXL (UCX RDMA) · SLURM --------------------------------------# + +- label: "DeepSeek-V3-PD-1P1D-TP8-NIXL-vllm-router" + agents: + queue: amd_mi350_ainic + timeout_in_minutes: 270 + concurrency: 2 + concurrency_group: "amd-disagg/mi350/2node" + commands: + - MODEL_NAME=DeepSeek-V3 NODES=2 GPUS_PER_NODE=8 WIDE_EP_MODE=0 KV_CONNECTOR=NixlConnector RUN_AFTER_HEALTH=accuracy ROUTER_TYPE=vllm-router WAIT=1 bash .buildkite/amd-disagg/run-slurm-disagg-test.sh + +- label: "DeepSeek-V3-PD-2P2D-EP16/DP16-WideEP-NIXL-vllm-router" + agents: + queue: amd_mi350_ainic + timeout_in_minutes: 270 + concurrency: 1 + concurrency_group: "amd-disagg/mi350/4node" + commands: + - MODEL_NAME=DeepSeek-V3 xP=2 yD=2 NODES=4 GPUS_PER_NODE=8 WIDE_EP_MODE=1 KV_CONNECTOR=NixlConnector RUN_AFTER_HEALTH=accuracy ROUTER_TYPE=vllm-router WAIT=1 bash .buildkite/amd-disagg/run-slurm-disagg-test.sh + +- label: "DeepSeek-V3-PD-1P1D-EP8/DP8-WideEP-NIXL-vllm-router" + agents: + queue: amd_mi350_ainic + timeout_in_minutes: 270 + concurrency: 2 + concurrency_group: "amd-disagg/mi350/2node" + commands: + - MODEL_NAME=DeepSeek-V3 NODES=2 GPUS_PER_NODE=8 WIDE_EP_MODE=1 KV_CONNECTOR=NixlConnector RUN_AFTER_HEALTH=accuracy ROUTER_TYPE=vllm-router WAIT=1 bash .buildkite/amd-disagg/run-slurm-disagg-test.sh + +- label: "DeepSeek-V3-PD-1P1D-TP8-NIXL-PUSH-vllm-router" + agents: + queue: amd_mi350_ainic + timeout_in_minutes: 270 + concurrency: 2 + concurrency_group: "amd-disagg/mi350/2node" + commands: + - MODEL_NAME=DeepSeek-V3 NODES=2 GPUS_PER_NODE=8 WIDE_EP_MODE=0 KV_CONNECTOR=NixlPushConnector RUN_AFTER_HEALTH=accuracy ROUTER_TYPE=vllm-router WAIT=1 bash .buildkite/amd-disagg/run-slurm-disagg-test.sh + +- label: "DeepSeek-V3-PD-1P1D-EP8/DP8-WideEP-NIXL-PUSH-vllm-router" + agents: + queue: amd_mi350_ainic + timeout_in_minutes: 270 + concurrency: 2 + concurrency_group: "amd-disagg/mi350/2node" + commands: + - MODEL_NAME=DeepSeek-V3 NODES=2 GPUS_PER_NODE=8 WIDE_EP_MODE=1 KV_CONNECTOR=NixlPushConnector RUN_AFTER_HEALTH=accuracy ROUTER_TYPE=vllm-router WAIT=1 bash .buildkite/amd-disagg/run-slurm-disagg-test.sh + +- label: "DeepSeek-V3-PD-2P2D-EP16/DP16-WideEP-NIXL-PUSH-vllm-router" + agents: + queue: amd_mi350_ainic + timeout_in_minutes: 270 + concurrency: 1 + concurrency_group: "amd-disagg/mi350/4node" + commands: + - MODEL_NAME=DeepSeek-V3 xP=2 yD=2 NODES=4 GPUS_PER_NODE=8 WIDE_EP_MODE=1 KV_CONNECTOR=NixlPushConnector RUN_AFTER_HEALTH=accuracy ROUTER_TYPE=vllm-router WAIT=1 bash .buildkite/amd-disagg/run-slurm-disagg-test.sh diff --git a/.buildkite/amd-disagg/run-slurm-disagg-test.sh b/.buildkite/amd-disagg/run-slurm-disagg-test.sh index 641133b9ff45..720b11573159 100644 --- a/.buildkite/amd-disagg/run-slurm-disagg-test.sh +++ b/.buildkite/amd-disagg/run-slurm-disagg-test.sh @@ -47,6 +47,7 @@ SHARED_MOUNT="${SHARED_MOUNT:-/data}" LOG_ROOT="${LOG_ROOT:-${SHARED_MOUNT}/${USER:-$(whoami)}/disagg_logs}" DRY_RUN="${DRY_RUN:-0}" MORIIO_READ_MODE="${MORIIO_READ_MODE:-0}" +KV_CONNECTOR="${KV_CONNECTOR:-MoRIIOConnector}" WAIT="${WAIT:-0}" @@ -69,7 +70,7 @@ export DISAGG_SCRIPTS_DIR="${STAGED_DIR}" echo "[slurm-submit] staged scripts for compute nodes: ${DISAGG_SCRIPTS_DIR}" >&2 export IMAGE MODEL_NAME WIDE_EP_MODE xP yD GPUS_PER_NODE RUN_AFTER_HEALTH HEALTH_TIMEOUT_S -export SHARED_MOUNT LOG_ROOT DRY_RUN MORIIO_READ_MODE +export SHARED_MOUNT LOG_ROOT DRY_RUN MORIIO_READ_MODE KV_CONNECTOR export ROUTER_TYPE ROUTER_PORT VLLM_ROUTER_IMAGE # Leave the image's own ionic userspace in place instead of mounting the host's. export SKIP_IONIC_MOUNTS diff --git a/.buildkite/amd-disagg/run_xPyD_disagg.slurm b/.buildkite/amd-disagg/run_xPyD_disagg.slurm index e9bf8014784f..19fbb54856b0 100644 --- a/.buildkite/amd-disagg/run_xPyD_disagg.slurm +++ b/.buildkite/amd-disagg/run_xPyD_disagg.slurm @@ -23,6 +23,7 @@ LOG_ROOT="${LOG_ROOT:-/data/${USER:-$(id -un)}/disagg_logs}" RUN_AFTER_HEALTH="${RUN_AFTER_HEALTH:-accuracy}" # bench | accuracy DRY_RUN="${DRY_RUN:-0}" MORIIO_READ_MODE="${MORIIO_READ_MODE:-0}" # 0 -> prefill writes (default); 1 -> decode reads KV (read_mode) +KV_CONNECTOR="${KV_CONNECTOR:-MoRIIOConnector}" # MoRIIOConnector | NixlConnector | NixlPushConnector # ---- router / gateway (host-side; also forwarded into the container) -------- # vllm-router -> external `vllm/vllm-router` container started on the rank-0 node (default) @@ -232,7 +233,7 @@ echo "[disagg-pd] LOG_PATH=$LOG_PATH" # rank xP .. end -> decode export IPADDRS -export WIDE_EP_MODE xP yD GPUS_PER_NODE RUN_AFTER_HEALTH HEALTH_TIMEOUT_S DRY_RUN MORIIO_READ_MODE +export WIDE_EP_MODE xP yD GPUS_PER_NODE RUN_AFTER_HEALTH HEALTH_TIMEOUT_S DRY_RUN MORIIO_READ_MODE KV_CONNECTOR export IMAGE SHARED_MOUNT CONTAINER_SCRIPTS DISAGG_SCRIPTS_DIR export CLUSTER_ENV="${CLUSTER_ENV_IN}" export DOCKER_CONT_NAME="vllm_disagg_${MODEL_NAME:-model}_${SLURM_JOB_ID}" @@ -247,6 +248,21 @@ export PREFILL_PORT DECODE_PORT PROXY_PORT ROUTER_METRICS_PORT export SERVE_PORT RPC_PORT KV_PORT LOCAL_PING_PORT export PREFLIGHT PREFLIGHT_MAX_VRAM_PCT PREFLIGHT_GPU_SETTLE_S +# NIXL has no discovery service, so pass the worker URLs. +ROUTER_KV_ARGS="--kv-connector moriio --vllm-discovery-address 0.0.0.0:${PROXY_PING_PORT}" +if [[ "${KV_CONNECTOR}" == Nixl* ]]; then + nixl_startup_timeout=1800 + [[ "${WIDE_EP_MODE}" == "1" && "${xP}" -gt 1 ]] && nixl_startup_timeout=7200 + ROUTER_KV_ARGS="--kv-connector nixl --worker-startup-timeout-secs ${nixl_startup_timeout}" + if [[ "${WIDE_EP_MODE}" == "1" ]]; then + ROUTER_KV_ARGS+=" --prefill http://${IPS[0]}:${SERVE_PORT} --decode http://${IPS[$xP]}:${SERVE_PORT}" + else + for ((i = 0; i < xP; i++)); do ROUTER_KV_ARGS+=" --prefill http://${IPS[$i]}:${PREFILL_PORT}"; done + for ((i = xP; i < xP + yD; i++)); do ROUTER_KV_ARGS+=" --decode http://${IPS[$i]}:${DECODE_PORT}"; done + fi +fi +export ROUTER_KV_ARGS + echo "[disagg-pd] IPADDRS=${IPADDRS}" echo "[disagg-pd] launching ${NUM_NODES} containers (1/node) -> vllm_disagg.sh node" @@ -479,7 +495,7 @@ run_node_container() { -e USER="$(id -un)" \ -e LOGNAME="$(id -un)" \ "$VLLM_ROUTER_IMAGE" \ - bash -lc "exec vllm-router --vllm-pd-disaggregation --kv-connector moriio --vllm-discovery-address 0.0.0.0:$PROXY_PING_PORT --port $ROUTER_PORT --host 0.0.0.0 --intra-node-data-parallel-size $ROUTER_DP_LOCAL --policy $ROUTER_POLICY --prefill-policy $ROUTER_POLICY --decode-policy $ROUTER_POLICY --log-level info >> $LOG_PATH/vllm_router.log 2>&1" \ + bash -lc "exec vllm-router --vllm-pd-disaggregation $ROUTER_KV_ARGS --port $ROUTER_PORT --host 0.0.0.0 --intra-node-data-parallel-size $ROUTER_DP_LOCAL --policy $ROUTER_POLICY --prefill-policy $ROUTER_POLICY --decode-policy $ROUTER_POLICY --log-level info >> $LOG_PATH/vllm_router.log 2>&1" \ && echo "vllm-router container started ok" \ || echo "WARN: failed to start vllm-router container on $(hostname)" } 2>&1 | tee -a "$LOG_PATH/vllm_router_start.log" @@ -528,6 +544,7 @@ run_node_container() { -e HEALTH_TIMEOUT_S="$HEALTH_TIMEOUT_S" \ -e DRY_RUN="$DRY_RUN" \ -e MORIIO_READ_MODE="$MORIIO_READ_MODE" \ + -e KV_CONNECTOR="$KV_CONNECTOR" \ -e ROUTER_TYPE="$ROUTER_TYPE" \ -e ROUTER_PORT="$ROUTER_PORT" \ -e ROUTER_POLICY="$ROUTER_POLICY" \ diff --git a/.buildkite/amd-disagg/vllm_disagg.sh b/.buildkite/amd-disagg/vllm_disagg.sh index 23320aaf6b1b..37f8915c421e 100644 --- a/.buildkite/amd-disagg/vllm_disagg.sh +++ b/.buildkite/amd-disagg/vllm_disagg.sh @@ -102,6 +102,18 @@ moriio_read_mode_kv() { esac } +# NIXL KV config. UCX backend. +nixl_kv_cfg() { + cat <- - # bash .buildkite/scripts/hardware_ci/run-intel-test.sh - # 'bash .buildkite/scripts/hardware_ci/run-intel-ci-test.sh example' + - label: ":intel: (BMG) Example Test" + key: xpu-example-test + depends_on: + - image-build-xpu + timeout_in_minutes: 50 + optional: true + device: intel_gpu + agent_tags: + label: production + gpu: 2+ + mem: 24+ + no_plugin: true + env: + REGISTRY: "public.ecr.aws/q9t5s3a7" + REPO: "vllm-ci-test-repo" + source_file_dependencies: + - .buildkite/hardware_tests/intel_xpu_ci/test-intel.yaml + - .buildkite/scripts/hardware_ci/run-intel-ci-test.sh + commands: + - >- + bash .buildkite/scripts/hardware_ci/run-intel-test.sh + 'bash .buildkite/scripts/hardware_ci/run-intel-ci-test.sh example' - label: ":intel: (BMG) V1 test" key: xpu-v1-test depends_on: @@ -84,29 +83,28 @@ steps: - >- bash .buildkite/scripts/hardware_ci/run-intel-test.sh 'bash .buildkite/scripts/hardware_ci/run-intel-ci-test.sh w8a8-fp8-linear' - # Disabled until the av installation is locked. - # - label: ":intel: (BMG) Server test" - # key: xpu-server-test - # depends_on: - # - image-build-xpu - # timeout_in_minutes: 45 - # optional: true - # device: intel_gpu - # agent_tags: - # label: production - # gpu: 1+ - # mem: 16+ - # no_plugin: true - # env: - # REGISTRY: "public.ecr.aws/q9t5s3a7" - # REPO: "vllm-ci-test-repo" - # source_file_dependencies: - # - .buildkite/hardware_tests/intel_xpu_ci/test-intel.yaml - # - .buildkite/scripts/hardware_ci/run-intel-ci-test.sh - # commands: - # - >- - # bash .buildkite/scripts/hardware_ci/run-intel-test.sh - # 'bash .buildkite/scripts/hardware_ci/run-intel-ci-test.sh server' + - label: ":intel: (BMG) Server test" + key: xpu-server-test + depends_on: + - image-build-xpu + timeout_in_minutes: 45 + optional: true + device: intel_gpu + agent_tags: + label: production + gpu: 1+ + mem: 16+ + no_plugin: true + env: + REGISTRY: "public.ecr.aws/q9t5s3a7" + REPO: "vllm-ci-test-repo" + source_file_dependencies: + - .buildkite/hardware_tests/intel_xpu_ci/test-intel.yaml + - .buildkite/scripts/hardware_ci/run-intel-ci-test.sh + commands: + - >- + bash .buildkite/scripts/hardware_ci/run-intel-test.sh + 'bash .buildkite/scripts/hardware_ci/run-intel-ci-test.sh server' - label: ":intel: (BMG) Quantization test" key: xpu-quantization-test depends_on: diff --git a/.buildkite/image_build/image_build.sh b/.buildkite/image_build/image_build.sh index 6d22acd5b16c..53cb3bc044b1 100755 --- a/.buildkite/image_build/image_build.sh +++ b/.buildkite/image_build/image_build.sh @@ -203,6 +203,35 @@ resolve_parent_commit() { fi } +resolve_merge_base_cache_commit() { + # PR builds: the commit the PR branched from, whose build pushed + # vllm-ci-test-cache:. A cache source only (see ci.hcl), unlike + # the VLLM_MERGE_BASE_COMMIT build arg, which would change the cache key. + if [[ "${BUILDKITE_PULL_REQUEST:-false}" == "false" ]]; then + return 0 + fi + if [[ -n "${VLLM_CACHE_MERGE_BASE_COMMIT:-}" ]]; then + echo "Using provided VLLM_CACHE_MERGE_BASE_COMMIT: ${VLLM_CACHE_MERGE_BASE_COMMIT}" + return 0 + fi + local base_branch="${BUILDKITE_PULL_REQUEST_BASE_BRANCH:-main}" + local base_ref="refs/remotes/vllm-cache-base/${base_branch}" + git fetch --quiet --no-tags --depth=200 origin \ + "+refs/heads/${base_branch}:${base_ref}" 2>/dev/null || true + VLLM_CACHE_MERGE_BASE_COMMIT=$(git merge-base HEAD "${base_ref}" 2>/dev/null || echo "") + if [[ -z "${VLLM_CACHE_MERGE_BASE_COMMIT}" ]]; then + git fetch --quiet --no-tags --deepen=1000 origin \ + "+refs/heads/${base_branch}:${base_ref}" 2>/dev/null || true + VLLM_CACHE_MERGE_BASE_COMMIT=$(git merge-base HEAD "${base_ref}" 2>/dev/null || echo "") + fi + if [[ -n "${VLLM_CACHE_MERGE_BASE_COMMIT}" ]]; then + echo "Computed merge base with ${base_branch} for cache fallback: ${VLLM_CACHE_MERGE_BASE_COMMIT}" + export VLLM_CACHE_MERGE_BASE_COMMIT + else + echo "Could not determine merge base with ${base_branch}; skipping that cache source" + fi +} + print_bake_config() { echo "--- :page_facing_up: Resolved bake configuration" # Write to a temp directory to avoid polluting the repo root (which is the @@ -353,6 +382,7 @@ setup_buildx_builder resolve_parent_commit export PARENT_COMMIT +resolve_merge_base_cache_commit print_bake_config diff --git a/.buildkite/intel_jobs/basic_correctness_intel.yaml b/.buildkite/intel_jobs/basic_correctness_intel.yaml index 69059228179b..da6d2ec0f41b 100644 --- a/.buildkite/intel_jobs/basic_correctness_intel.yaml +++ b/.buildkite/intel_jobs/basic_correctness_intel.yaml @@ -20,6 +20,8 @@ steps: - vllm/ - tests/basic_correctness/memory/ - tests/basic_correctness/cpu_offload/ + # MRV2 model state remains in the weights pool on XPU (vllm-project/vllm#63548). + # Re-enable Mamba deep sleep once XPU nested memory pools are supported. commands: - >- bash .buildkite/scripts/hardware_ci/run-intel-test.sh @@ -28,4 +30,5 @@ steps: pytest -v -s basic_correctness/cpu_offload && pytest -v -s basic_correctness/memory - --deselect="tests/basic_correctness/memory/cumem/test_cumem.py::test_basic_cumem"' + --deselect="tests/basic_correctness/memory/cumem/test_cumem.py::test_basic_cumem" + --deselect="tests/basic_correctness/memory/sleep_mode/test_sleep_mode.py::test_deep_sleep[yujiepan/mamba2-codestral-v0.1-tiny-random]"' diff --git a/.buildkite/intel_jobs/engine_intel.yaml b/.buildkite/intel_jobs/engine_intel.yaml index fce83b7dec82..7c7c0f02c7e6 100644 --- a/.buildkite/intel_jobs/engine_intel.yaml +++ b/.buildkite/intel_jobs/engine_intel.yaml @@ -96,12 +96,15 @@ steps: - vllm/model_executor/models/qwen3_next - vllm/config/cache.py - vllm/platforms/xpu.py + - vllm/v1/engine/ - tests/v1/e2e/general/test_mamba_prefix_cache.py + - tests/v1/e2e/general/test_streaming_input.py commands: - >- bash .buildkite/scripts/hardware_ci/run-intel-test.sh 'cd tests && - pytest -v -s v1/e2e/general/test_mamba_prefix_cache.py' + pytest -v -s v1/e2e/general/test_mamba_prefix_cache.py && + pytest -v -s v1/e2e/general/test_streaming_input.py' - label: ":intel: (BMG) V1 e2e (2 GPUs)" key: v1-e2e-2-gpus diff --git a/.buildkite/intel_jobs/kernels_intel.yaml b/.buildkite/intel_jobs/kernels_intel.yaml index f3a9d58f4357..fdb087971991 100644 --- a/.buildkite/intel_jobs/kernels_intel.yaml +++ b/.buildkite/intel_jobs/kernels_intel.yaml @@ -48,3 +48,29 @@ steps: bash .buildkite/scripts/hardware_ci/run-intel-test.sh 'cd tests && pytest -v -s kernels/core/test_uva.py' + +- label: ":intel: (BMG) XPU Sparse MLA Tests" + key: xpu-sparse-mla-tests + timeout_in_minutes: 20 + device: intel_gpu + agent_tags: + label: production + gpu: 1+ + mem: 16+ + no_plugin: true + working_dir: "." + env: + REGISTRY: "public.ecr.aws/q9t5s3a7" + REPO: "vllm-ci-test-repo" + VLLM_TEST_DEVICE: "xpu" + source_file_dependencies: + - vllm/v1/attention/backends/mla/xpu_mla_sparse.py + - tests/kernels/attention/test_xpu_mla_sparse.py + - tests/v1/attention/test_xpu_mla_sparse_backend.py + commands: + - >- + bash .buildkite/scripts/hardware_ci/run-intel-test.sh + 'cd tests && + pytest -v -s + kernels/attention/test_xpu_mla_sparse.py + v1/attention/test_xpu_mla_sparse_backend.py' diff --git a/.buildkite/intel_jobs/lora_intel.yaml b/.buildkite/intel_jobs/lora_intel.yaml index 366aaf7109aa..3ff650c99752 100644 --- a/.buildkite/intel_jobs/lora_intel.yaml +++ b/.buildkite/intel_jobs/lora_intel.yaml @@ -57,7 +57,7 @@ steps: bash .buildkite/scripts/hardware_ci/run-intel-test.sh 'cd tests && export VLLM_WORKER_MULTIPROC_METHOD=spawn && - pytest -v -s lora/test_fused_moe_lora_kernel.py && + pytest -v -s lora/test_fused_moe_lora_kernel.py --deselect="lora/test_fused_moe_lora_kernel.py::test_fused_moe_lora_kernel_large_rank_fallback[xpu:0-dtype0-1-True-256]" && pytest -v -s lora/test_moe_lora_align_sum.py --deselect="tests/lora/test_moe_lora_align_sum.py::test_moe_lora_align_block_size_mixed_base_and_lora[1]"' diff --git a/.buildkite/lm-eval-harness/test_lm_eval_correctness.py b/.buildkite/lm-eval-harness/test_lm_eval_correctness.py index 6b2e293ba981..a3c8b648d777 100644 --- a/.buildkite/lm-eval-harness/test_lm_eval_correctness.py +++ b/.buildkite/lm-eval-harness/test_lm_eval_correctness.py @@ -21,6 +21,7 @@ from lm_eval.utils import simple_parse_args_string from vllm import LLM +from vllm.config import CacheConfig from vllm.distributed import cleanup_dist_env_and_memory from vllm.logger import init_logger from vllm.model_executor.model_loader.weight_utils import drop_checkpoint_cache @@ -89,6 +90,7 @@ def _shutdown_lm(lm) -> None: return gpu_memory_utilization = ( llm.llm_engine.vllm_config.cache_config.gpu_memory_utilization + or CacheConfig.DEFAULT_GPU_MEMORY_UTILIZATION ) try: llm.llm_engine.engine_core.shutdown(timeout=ROCM_ENGINE_SHUTDOWN_TIMEOUT_S) diff --git a/.buildkite/release-pipeline.yaml b/.buildkite/release-pipeline.yaml index 2de3a4bfa747..d966f3ed65d8 100644 --- a/.buildkite/release-pipeline.yaml +++ b/.buildkite/release-pipeline.yaml @@ -272,10 +272,6 @@ steps: queue: arm64_cpu_queue_release commands: - "aws ecr-public get-login-password --region us-east-1 | docker login --username AWS --password-stdin public.ecr.aws/q9t5s3a7" - # TODO(arpera): - # Remove BUILD_NIXL=true once NIXL has a wheel built against torch 2.15 - # For details: https://github.com/vllm-project/vllm/pull/60090 - # Tracking issue report: https://github.com/ai-dynamo/nixl/issues/2297 - | DOCKER_BUILDKIT=1 docker buildx build --load \ $(bash .buildkite/scripts/docker-build-metadata-args.sh cu134) \ @@ -287,7 +283,6 @@ steps: --build-arg torch_cuda_arch_list="${CUDA_ARCH_AARCH64}" \ --build-arg INSTALL_KV_CONNECTORS=true \ --build-arg INSTALL_RUBIN_PRERELEASE=true \ - --build-arg BUILD_NIXL=true \ --build-arg TRITON_INSTALL_FROM_SOURCE_REPO=https://github.com/triton-lang/triton.git \ --build-arg TRITON_INSTALL_FROM_SOURCE_REVISION=3f6e41132b5edf639bfb872ad73d4688765e08b8 \ --build-arg RUN_WHEEL_CHECK=false \ @@ -597,252 +592,96 @@ steps: # ============================================================================= # ROCm Release Pipelines (x86_64 only) # ============================================================================= - # - # One group per ROCm stack, identified by its base Dockerfile (ROCM_BASE_DOCKERFILE). - # The scripts derive the ROCm version, wheel variant, cache keys and image tags from - # it (.buildkite/scripts/rocm/stack.sh), so both groups share one set of step - # definitions, anchored on the ROCm 10.0 group. - # - # - ROCm 10.0 (TheRock, docker/Dockerfile.rocm{,_base}) is the default: it owns the - # :nightly / :latest images and the root wheel index. - # - ROCm 7.2 (docker/Dockerfile.rocm_72{,_base}) is kept for a transition period as - # rocm72 images and wheels. - # - # vLLM version is determined by the Buildkite checkout (like the CUDA pipeline). + # One group per ROCm stack, on this pipeline's own agents. A serialized step builds the stack and + # runtime from source into ECR (once per content key, with only its own ECR build cache; it never + # reads rocm/vllm-ci); the build step compiles csrc for all release arches and pushes + # :-{,-wheels,-base} (.buildkite/scripts/rocm/build.sh release); the steps + # after it only copy those (.buildkite/scripts/rocm/publish.sh). + # - ROCm 10.0 (TheRock, stack `rocm`) is the default: it owns the :nightly / :latest images and + # the root wheel index. # ============================================================================= - - group: "Build ROCm 10.0 Wheel / Image" - key: "build-rocm100" + - group: ":rocm: ROCm 10.0" + key: build-rocm100 depends_on: ~ steps: - - label: ":rocm: Build ROCm 10.0 Base Image & Wheels" - id: build-rocm100-base + - label: ":rocm: Ensure ROCm 10.0 release stack + runtime" + key: build-rocm100-release-base depends_on: ~ - agents: &rocm_build_agents - queue: cpu_queue_release - timeout_in_minutes: 360 - env: &rocm100_env - ROCM_BASE_DOCKERFILE: "docker/Dockerfile.rocm_base" - ROCM_DEFAULT_ARG: "--default" - DOCKER_BUILDKIT: "1" - S3_BUCKET: "vllm-wheels" - DOCKERHUB_USERNAME: "vllmbot" - commands: &rocm_base_commands - - | - set -euo pipefail - source .buildkite/scripts/rocm/stack.sh "$${ROCM_BASE_DOCKERFILE}" - SCCACHE_ARGS=(--build-arg USE_SCCACHE=1 --build-arg SCCACHE_BUCKET_NAME=vllm-build-sccache - --build-arg SCCACHE_REGION_NAME=us-west-2 --build-arg SCCACHE_S3_NO_CREDENTIALS=0) - aws ecr-public get-login-password --region us-east-1 | \ - docker login --username AWS --password-stdin public.ecr.aws/q9t5s3a7 - - # Base image, cached in ECR by the base Dockerfile's hash - if docker manifest inspect "$${ROCM_STACK_ECR_BASE}" > /dev/null 2>&1; then - echo "ECR image cache HIT: $${ROCM_STACK_ECR_BASE}" - else - docker buildx build --file "$${ROCM_BASE_DOCKERFILE}" --tag "$${ROCM_STACK_ECR_BASE}" \ - "$${SCCACHE_ARGS[@]}" --push . - fi - - # Base wheels, cached in S3 (see cache-rocm-base-wheels.sh) - CACHE=.buildkite/scripts/cache-rocm-base-wheels.sh - if [ "$$($${CACHE} "$${ROCM_BASE_DOCKERFILE}" check)" = "hit" ]; then - $${CACHE} "$${ROCM_BASE_DOCKERFILE}" download - else - DEBS="rocm-base-debs:$${BUILDKITE_BUILD_NUMBER}-$${ROCM_STACK_VARIANT}" - docker buildx build --file "$${ROCM_BASE_DOCKERFILE}" --tag "$${DEBS}" \ - --target debs_wheel_release "$${SCCACHE_ARGS[@]}" --load . - mkdir -p "$${ROCM_STACK_BASE_WHEELS_DIR}" - cid=$$(docker create "$${DEBS}") - docker cp "$${cid}":/app/debs/. "$${ROCM_STACK_BASE_WHEELS_DIR}/" - docker rm "$${cid}" - $${CACHE} "$${ROCM_BASE_DOCKERFILE}" upload - fi - buildkite-agent artifact upload "$${ROCM_STACK_BASE_WHEELS_DIR}/*.whl" - - - label: ":python: Build vLLM ROCm 10.0 Wheel - x86_64" - id: build-rocm100-vllm-wheel - depends_on: build-rocm100-base - agents: *rocm_build_agents - timeout_in_minutes: 180 - env: *rocm100_env - commands: &rocm_vllm_wheel_commands - - | - set -euo pipefail - source .buildkite/scripts/rocm/stack.sh "$${ROCM_BASE_DOCKERFILE}" - # Buildkite's default fetch doesn't update moved tags; version detection needs them - git fetch --tags --force origin - buildkite-agent artifact download "$${ROCM_STACK_BASE_WHEELS_DIR}/*.whl" . - aws ecr-public get-login-password --region us-east-1 | \ - docker login --username AWS --password-stdin public.ecr.aws/q9t5s3a7 - docker pull "$${ROCM_STACK_ECR_BASE}" - mkdir -p docker/context/base-wheels - touch docker/context/base-wheels/.keep - cp "$${ROCM_STACK_BASE_WHEELS_DIR}"/*.whl docker/context/base-wheels/ - docker build --file "$${ROCM_STACK_DOCKERFILE}" --target export_vllm_wheel_release \ - --output type=local,dest=rocm-dist \ - --build-arg BASE_IMAGE="$${ROCM_STACK_ECR_BASE}" --build-arg REMOTE_VLLM=0 \ - --build-arg GIT_REPO_CHECK=1 --build-arg USE_SCCACHE=1 \ - --build-arg SCCACHE_BUCKET_NAME=vllm-build-sccache \ - --build-arg SCCACHE_REGION_NAME=us-west-2 --build-arg SCCACHE_S3_NO_CREDENTIALS=0 . - mkdir -p "$${ROCM_STACK_VLLM_WHEEL_DIR}" - cp rocm-dist/*.whl "$${ROCM_STACK_VLLM_WHEEL_DIR}/" - buildkite-agent artifact upload "$${ROCM_STACK_VLLM_WHEEL_DIR}/*.whl" - - - label: ":s3: Upload ROCm 10.0 Wheels to S3" - id: upload-rocm100-wheels - depends_on: build-rocm100-vllm-wheel - agents: *rocm_build_agents - timeout_in_minutes: 60 - env: *rocm100_env - commands: &rocm_upload_commands - - | - set -euo pipefail - source .buildkite/scripts/rocm/stack.sh "$${ROCM_BASE_DOCKERFILE}" - buildkite-agent artifact download "$${ROCM_STACK_BASE_WHEELS_DIR}/*.whl" . - buildkite-agent artifact download "$${ROCM_STACK_VLLM_WHEEL_DIR}/*.whl" . - bash .buildkite/scripts/upload-rocm-wheels.sh "$${ROCM_BASE_DOCKERFILE}" - - - label: ":memo: Annotate ROCm 10.0 wheel release" - id: annotate-rocm100-release - depends_on: upload-rocm100-wheels - agents: *rocm_build_agents - env: *rocm100_env - commands: &rocm_annotate_commands - - bash .buildkite/scripts/annotate-rocm-release.sh "$${ROCM_BASE_DOCKERFILE}" $${ROCM_DEFAULT_ARG} - - # Generate https://wheels.vllm.ai/rocm/ (the default stack's variant), so users can - # `uv pip install vllm --extra-index-url https://wheels.vllm.ai/rocm/` (release only) - - block: "Generate Root Index for ROCm Wheels for Release" - key: block-generate-root-index-rocm-wheels - depends_on: - - upload-rocm100-wheels - - upload-rocm72-wheels - - - label: ":package: Generate Root Index for ROCm Wheels for Release" - depends_on: block-generate-root-index-rocm-wheels - id: generate-root-index-rocm-wheels - agents: *rocm_build_agents - env: *rocm100_env + agents: {queue: cpu_queue_release} + timeout_in_minutes: 720 # a new stack key (release never uses the CI cache) takes hours + concurrency_group: vllm/rocm/release/stack-rocm # one run builds a new key; the others then reuse it + concurrency: 1 + env: &rocm_release_env {ROCM_CI_REPO: &rocm_ecr public.ecr.aws/q9t5s3a7/vllm-release-repo, SCCACHE_BUCKET_NAME: vllm-build-sccache} commands: - - "bash tools/vllm-rocm/generate-rocm-wheels-root-index.sh" - - - label: ":docker: Build release image - x86_64 - ROCm 10.0" - id: build-rocm100-release-image - depends_on: - - step: block-build-release-images - allow_failure: true - - step: build-rocm100-base - allow_failure: false - agents: *rocm_build_agents - timeout_in_minutes: 180 - env: *rocm100_env - commands: &rocm_release_image_commands - - | - set -euo pipefail - source .buildkite/scripts/rocm/stack.sh "$${ROCM_BASE_DOCKERFILE}" - aws ecr-public get-login-password --region us-east-1 | \ - docker login --username AWS --password-stdin public.ecr.aws/q9t5s3a7 - docker pull "$${ROCM_STACK_ECR_BASE}" - docker build --file "$${ROCM_STACK_DOCKERFILE}" --target vllm-openai \ - --tag "$${ROCM_STACK_ECR_IMAGE}" --progress plain \ - --build-arg max_jobs=16 --build-arg BASE_IMAGE="$${ROCM_STACK_ECR_BASE}" \ - --build-arg INSTALL_LMCACHE=true --build-arg USE_SCCACHE=1 \ - --build-arg SCCACHE_BUCKET_NAME=vllm-build-sccache \ - --build-arg SCCACHE_REGION_NAME=us-west-2 --build-arg SCCACHE_S3_NO_CREDENTIALS=0 . - docker push "$${ROCM_STACK_ECR_IMAGE}" - - - label: "Publish nightly ROCm 10.0 image to DockerHub" - id: publish-rocm100-nightly - depends_on: build-rocm100-release-image - if: build.env("NIGHTLY") == "1" - agents: &rocm_publish_agents - queue: small_cpu_queue_release - env: *rocm100_env - commands: &rocm_publish_nightly_commands - - bash .buildkite/scripts/push-nightly-builds-rocm.sh "$${ROCM_BASE_DOCKERFILE}" $${ROCM_DEFAULT_ARG} - plugins: &rocm_dockerhub_login + - aws ecr-public get-login-password --region us-east-1 | docker login -u AWS --password-stdin $${ROCM_CI_REPO%/*} + - bash .buildkite/scripts/rocm/build.sh rocm release-base + retry: &rocm_release_retry {automatic: [{exit_status: 75, limit: 1}]} # registry error (build.sh) + plugins: &rocm_hub_login # authenticated Docker Hub pulls and pushes - docker-login#v3.0.0: username: vllmbot password-env: DOCKERHUB_TOKEN - - group: "Build ROCm 7.2 Wheel / Image" - key: "build-rocm72" - depends_on: ~ - steps: - - label: ":rocm: Build ROCm 7.2 Base Image & Wheels" - id: build-rocm72-base - depends_on: ~ - agents: *rocm_build_agents - timeout_in_minutes: 360 - env: &rocm72_env - ROCM_BASE_DOCKERFILE: "docker/Dockerfile.rocm_72_base" - ROCM_DEFAULT_ARG: "" - DOCKER_BUILDKIT: "1" - S3_BUCKET: "vllm-wheels" - DOCKERHUB_USERNAME: "vllmbot" - commands: *rocm_base_commands - - - label: ":python: Build vLLM ROCm 7.2 Wheel - x86_64" - id: build-rocm72-vllm-wheel - depends_on: build-rocm72-base - agents: *rocm_build_agents - timeout_in_minutes: 180 - env: *rocm72_env - commands: *rocm_vllm_wheel_commands - - - label: ":s3: Upload ROCm 7.2 Wheels to S3" - id: upload-rocm72-wheels - depends_on: build-rocm72-vllm-wheel - agents: *rocm_build_agents + - label: ":rocm: Build ROCm 10.0 release image + wheels" + key: build-rocm100-release + depends_on: build-rocm100-release-base + agents: {queue: cpu_queue_release} + timeout_in_minutes: 720 # an all-arch csrc compile without sccache hits takes ~3h + env: *rocm_release_env + commands: + - aws ecr-public get-login-password --region us-east-1 | docker login -u AWS --password-stdin $${ROCM_CI_REPO%/*} + - bash .buildkite/scripts/rocm/build.sh rocm release + retry: *rocm_release_retry + plugins: *rocm_hub_login + + - label: ":s3: Upload ROCm 10.0 wheels" + key: upload-rocm100-wheels + depends_on: build-rocm100-release + agents: {queue: cpu_queue_release} timeout_in_minutes: 60 - env: *rocm72_env - commands: *rocm_upload_commands - - - label: ":memo: Annotate ROCm 7.2 wheel release" - id: annotate-rocm72-release - depends_on: upload-rocm72-wheels - agents: *rocm_build_agents - env: *rocm72_env - commands: *rocm_annotate_commands - - - label: ":docker: Build release image - x86_64 - ROCm 7.2" - id: build-rocm72-release-image + env: {S3_BUCKET: vllm-wheels, ECR: *rocm_ecr} + command: bash .buildkite/scripts/rocm/publish.sh rocm wheels + plugins: *rocm_hub_login + + - label: ":docker: ROCm 10.0 image: ECR tags (+ nightly Docker Hub)" + key: build-rocm100-release-image depends_on: - step: block-build-release-images allow_failure: true - - step: build-rocm72-base - allow_failure: false - agents: *rocm_build_agents - timeout_in_minutes: 180 - env: *rocm72_env - commands: *rocm_release_image_commands - - - label: "Publish nightly ROCm 7.2 image to DockerHub" - id: publish-rocm72-nightly - depends_on: build-rocm72-release-image - if: build.env("NIGHTLY") == "1" - agents: *rocm_publish_agents - env: *rocm72_env - commands: *rocm_publish_nightly_commands - plugins: *rocm_dockerhub_login + - build-rocm100-release + agents: {queue: small_cpu_queue_release} + env: {ECR: *rocm_ecr} + command: bash .buildkite/scripts/rocm/publish.sh rocm images + plugins: *rocm_hub_login - - label: "Publish nightly XPU image to DockerHub" - depends_on: - - create-manifest-xpu - if: build.env("NIGHTLY") == "1" - agents: - queue: small_cpu_queue_release - commands: - - "bash .buildkite/scripts/xpu/push-nightly-builds-xpu.sh" - - "bash .buildkite/scripts/cleanup-nightly-builds.sh nightly- vllm/vllm-openai-xpu" - plugins: - - docker-login#v3.0.0: - username: vllmbot - password-env: DOCKERHUB_TOKEN - env: - DOCKER_BUILDKIT: "1" - DOCKERHUB_USERNAME: "vllmbot" + # Generate https://wheels.vllm.ai/rocm/ (the default stack's variant). + - block: "Generate Root Index for ROCm Wheels for Release" + key: block-generate-root-index-rocm-wheels + depends_on: upload-rocm100-wheels + + - label: ":package: Generate Root Index for ROCm Wheels for Release" + key: generate-root-index-rocm-wheels + depends_on: block-generate-root-index-rocm-wheels + agents: {queue: cpu_queue_release} + env: {S3_BUCKET: vllm-wheels} + command: VARIANT=$$(buildkite-agent meta-data get rocm-rocm-variant) bash tools/vllm-rocm/generate-rocm-wheels-root-index.sh + + - label: "Publish nightly XPU image to DockerHub" + depends_on: + - create-manifest-xpu + if: build.env("NIGHTLY") == "1" + agents: + queue: small_cpu_queue_release + commands: + - "bash .buildkite/scripts/xpu/push-nightly-builds-xpu.sh" + - "bash .buildkite/scripts/cleanup-nightly-builds.sh nightly- vllm/vllm-openai-xpu" + plugins: + - docker-login#v3.0.0: + username: vllmbot + password-env: DOCKERHUB_TOKEN + env: + DOCKER_BUILDKIT: "1" + DOCKERHUB_USERNAME: "vllmbot" # ============================================================================= # Publish to DockerHub and PyPI (at the end so all builds complete first) @@ -854,7 +693,6 @@ steps: - create-multi-arch-manifest - create-manifest-xpu - build-rocm100-release-image - - build-rocm72-release-image - input-release-version # Wait for CPU builds if their block steps were unblocked, so publish # doesn't race the in-progress CPU build. allow_failure lets publish @@ -882,7 +720,6 @@ steps: # AWS EFA variant; skipped if no EFA image was built, fails on a partial build. - "cuda-13-0-efa" - "rocm" - - "rocm72" - "xpu" - "cpu" plugins: diff --git a/.buildkite/scripts/annotate-rocm-release.sh b/.buildkite/scripts/annotate-rocm-release.sh deleted file mode 100755 index 1889db43b5ad..000000000000 --- a/.buildkite/scripts/annotate-rocm-release.sh +++ /dev/null @@ -1,87 +0,0 @@ -#!/bin/bash -# SPDX-License-Identifier: Apache-2.0 -# SPDX-FileCopyrightText: Copyright contributors to the vLLM project -# -# Generate Buildkite annotation for a ROCm stack's wheel release. -# -# Usage: annotate-rocm-release.sh [--default] -# base-dockerfile e.g. docker/Dockerfile.rocm_base or docker/Dockerfile.rocm_72_base -# --default the stack also owns the plain :latest / :v Docker Hub tags -set -ex - -# shellcheck source=.buildkite/scripts/rocm/stack.sh -source "$(dirname "${BASH_SOURCE[0]}")/rocm/stack.sh" "${1:?Usage: $0 [--default]}" -TAG_VARIANTS=("-${ROCM_STACK_VARIANT}") -if [[ "${2:-}" == "--default" ]]; then - TAG_VARIANTS=("" "-${ROCM_STACK_VARIANT}") -fi -ROCM_VERSION="$ROCM_STACK_VERSION" -VARIANT="$ROCM_STACK_VARIANT" -PYTHON_VERSION=$(sed -nE 's/^ARG PYTHON_VERSION="?([^" ]+)"?.*/\1/p' "$ROCM_STACK_BASE_DOCKERFILE") -PYTORCH_ROCM_ARCH=$(sed -nE 's/^ARG PYTORCH_ROCM_ARCH="?([^"]+)"?.*/\1/p' "$ROCM_STACK_BASE_DOCKERFILE") - -# Get release version, default to 1.0.0.dev for nightly/per-commit builds -RELEASE_VERSION=$(buildkite-agent meta-data get release-version 2>/dev/null || echo "") -if [ -z "${RELEASE_VERSION}" ]; then - RELEASE_VERSION="1.0.0.dev" -fi - -# S3 URLs -S3_BUCKET="${S3_BUCKET:-vllm-wheels}" -S3_REGION="${AWS_DEFAULT_REGION:-us-west-2}" -S3_HOST="${S3_BUCKET}.s3-website-${S3_REGION}.amazonaws.com" -S3_URL="http://${S3_HOST}" -WHEEL_PATH="rocm/${BUILDKITE_COMMIT}/${VARIANT}-wheels/" - -DOCKER_CMDS="docker pull ${ROCM_STACK_ECR_BASE} -docker pull ${ROCM_STACK_ECR_IMAGE}" -for suffix in "${TAG_VARIANTS[@]}"; do - for tag in "latest${suffix}" "v${RELEASE_VERSION}${suffix}"; do - DOCKER_CMDS+=" -docker tag ${ROCM_STACK_ECR_BASE} vllm/vllm-openai-rocm:${tag}-base -docker tag ${ROCM_STACK_ECR_IMAGE} vllm/vllm-openai-rocm:${tag} -docker push vllm/vllm-openai-rocm:${tag}-base -docker push vllm/vllm-openai-rocm:${tag}" - done -done - -buildkite-agent annotate --style 'success' --context "rocm-release-workflow-${VARIANT}" << EOF -## ROCm ${ROCM_VERSION} Wheel and Docker Image Releases -### Build Configuration -| Setting | Value | -|---------|-------| -| **ROCm Version** | ${ROCM_VERSION} | -| **Index Variant** | ${VARIANT} | -| **Python Version** | ${PYTHON_VERSION} | -| **GPU Architectures** | ${PYTORCH_ROCM_ARCH} | -| **Branch** | \`${BUILDKITE_BRANCH}\` | -| **Commit** | \`${BUILDKITE_COMMIT}\` | - -### :package: Installation - -\`\`\`bash -# This build (by commit) -pip install vllm --extra-index-url ${S3_URL}/rocm/${BUILDKITE_COMMIT}/${VARIANT}/ --trusted-host ${S3_HOST} -# Nightly (if published) -pip install vllm --extra-index-url ${S3_URL}/rocm/nightly/${VARIANT}/ --trusted-host ${S3_HOST} -\`\`\` - -### :floppy_disk: Download Wheels Directly - -\`\`\`bash -aws s3 ls s3://${S3_BUCKET}/${WHEEL_PATH} -aws s3 cp --recursive --exclude "*" --include "*.whl" s3://${S3_BUCKET}/${WHEEL_PATH} . -\`\`\` - -### :warning: Notes -- These wheels are built for **ROCm ${ROCM_VERSION}** and will NOT work with CUDA GPUs -- Supported GPU architectures: ${PYTORCH_ROCM_ARCH} -- Platform: Linux x86_64 only - -### :package: Docker Image Release - -\`\`\` -${DOCKER_CMDS} -\`\`\` - -EOF diff --git a/.buildkite/scripts/cache-rocm-base-wheels.sh b/.buildkite/scripts/cache-rocm-base-wheels.sh deleted file mode 100755 index 02fbbf52cf54..000000000000 --- a/.buildkite/scripts/cache-rocm-base-wheels.sh +++ /dev/null @@ -1,125 +0,0 @@ -#!/usr/bin/env bash -# SPDX-License-Identifier: Apache-2.0 -# SPDX-FileCopyrightText: Copyright contributors to the vLLM project -# -# Cache helper for ROCm base wheels -# -# This script manages caching of pre-built ROCm base wheels (torch, triton, etc.) -# to avoid rebuilding them when the stack's base Dockerfile hasn't changed. -# -# Usage: cache-rocm-base-wheels.sh -# base-dockerfile e.g. docker/Dockerfile.rocm_base or docker/Dockerfile.rocm_72_base -# Commands: -# check - Check if cache exists, outputs "hit" or "miss" -# upload - Upload wheels to cache -# download - Download wheels from cache -# key - Output the cache key -# path - Output the full S3 cache path -# -# Environment variables: -# S3_BUCKET - S3 bucket name (default: vllm-wheels) -# -# The cache key hashes the base Dockerfile and the repo files it COPYs in, which -# capture the ROCm version and every component pin (see rocm/stack.sh). - -set -euo pipefail - -BUCKET="${S3_BUCKET:-vllm-wheels}" -CACHE_PREFIX="rocm/cache" - -# shellcheck source=.buildkite/scripts/rocm/stack.sh -source "$(dirname "${BASH_SOURCE[0]}")/rocm/stack.sh" "${1:?Usage: $0 }" -shift -CACHE_KEY="$ROCM_STACK_WHEEL_KEY" -WHEELS_DIR="$ROCM_STACK_BASE_WHEELS_DIR" -CACHE_PATH="s3://${BUCKET}/${CACHE_PREFIX}/${CACHE_KEY}/" - -case "${1:-}" in - check) - echo "Checking cache for key: ${CACHE_KEY}" >&2 - echo "Cache path: ${CACHE_PATH}" >&2 - - # Check if cache exists by listing objects - # We look for at least one .whl file - echo "Running: aws s3 ls ${CACHE_PATH}" >&2 - S3_OUTPUT=$(aws s3 ls "${CACHE_PATH}" 2>&1) || true - echo "S3 ls output:" >&2 - echo "$S3_OUTPUT" | head -5 >&2 - - if echo "$S3_OUTPUT" | grep -q "\.whl"; then - echo "hit" - else - echo "miss" - fi - ;; - - upload) - echo "========================================" - echo "Uploading wheels to cache" - echo "========================================" - echo "Cache key: ${CACHE_KEY}" - echo "Cache path: ${CACHE_PATH}" - echo "" - - if [[ ! -d "${WHEELS_DIR}" ]]; then - echo "ERROR: ${WHEELS_DIR} directory not found" >&2 - exit 1 - fi - - WHEEL_COUNT=$(find "${WHEELS_DIR}" -maxdepth 1 -name '*.whl' 2>/dev/null | wc -l) - if [[ "$WHEEL_COUNT" -eq 0 ]]; then - echo "ERROR: No wheels found in ${WHEELS_DIR}/" >&2 - exit 1 - fi - - echo "Uploading $WHEEL_COUNT wheels..." - aws s3 cp --recursive "${WHEELS_DIR}/" "${CACHE_PATH}" - - echo "" - echo "Cache upload complete!" - echo "========================================" - ;; - - download) - echo "========================================" - echo "Downloading wheels from cache" - echo "========================================" - echo "Cache key: ${CACHE_KEY}" - echo "Cache path: ${CACHE_PATH}" - echo "" - mkdir -p "${WHEELS_DIR}" - - # Use sync with include/exclude to only download .whl files - aws s3 sync "${CACHE_PATH}" "${WHEELS_DIR}/" \ - --exclude "*" \ - --include "*.whl" - - echo "" - echo "Downloaded wheels:" - find "${WHEELS_DIR}" -maxdepth 1 -name '*.whl' -exec ls -lh {} \; - WHEEL_COUNT=$(find "${WHEELS_DIR}" -maxdepth 1 -name '*.whl' 2>/dev/null | wc -l) - echo "" - echo "Total: $WHEEL_COUNT wheels" - echo "========================================" - ;; - - key) - echo "${CACHE_KEY}" - ;; - - path) - echo "${CACHE_PATH}" - ;; - - *) - echo "Usage: $0 {check|upload|download|key|path}" >&2 - echo "" >&2 - echo "Commands:" >&2 - echo " check - Check if cache exists, outputs 'hit' or 'miss'" >&2 - echo " upload - Upload wheels from ${WHEELS_DIR}/ to cache" >&2 - echo " download - Download wheels from cache to ${WHEELS_DIR}/" >&2 - echo " key - Output the cache key" >&2 - echo " path - Output the full S3 cache path" >&2 - exit 1 - ;; -esac diff --git a/.buildkite/scripts/check-ray-compatibility.sh b/.buildkite/scripts/check-ray-compatibility.sh index ec42834802d6..8e1eef6948fe 100644 --- a/.buildkite/scripts/check-ray-compatibility.sh +++ b/.buildkite/scripts/check-ray-compatibility.sh @@ -27,6 +27,8 @@ if python3 -c "import torch; assert torch.version.hip" 2>/dev/null; then echo ">>> WARNING: ROCm ${ROCM_VER} wheel index not found at ${CANDIDATE_URL}" echo ">>> Falling back to default PyPI (resolution may be incomplete)" TORCH_INDEX_URL="" + # The image's own stack builds (TheRock torch, triton, amdsmi) are on no public index. + LOCAL_BUILDS=1 fi else if [ "${TORCH_NIGHTLY:-0}" = "1" ]; then @@ -110,6 +112,14 @@ with open(out_path, "w") as f: print(f"Wrote {len(lines)} constraints to {out_path}") PYEOF +if [[ -n "${LOCAL_BUILDS:-}" ]]; then + # Compare them at their public release. Dropped: amdsmi has none (TheRock ships it as + # sources) and triton is the stack's own, which its torch is repacked to require. + echo ">>> Pinning local builds to their public release:" + grep -E '==[^ ;]*\+|\[device-' "${WORK_DIR}/vllm-constraints.txt" || true + sed -i -E -e '/^(amdsmi|triton)([=<>!~ ;[]|$)|\[device-/d' -e 's/(==[^ ;+]+)\+[^ ;]*/\1/' "${WORK_DIR}/vllm-constraints.txt" +fi + echo ">>> Installed vllm deps (first 20 lines):" head -20 "${WORK_DIR}/vllm-constraints.txt" diff --git a/.buildkite/scripts/ci-bake-rocm.sh b/.buildkite/scripts/ci-bake-rocm.sh deleted file mode 100644 index db3d0a4acfe4..000000000000 --- a/.buildkite/scripts/ci-bake-rocm.sh +++ /dev/null @@ -1,2803 +0,0 @@ -#!/bin/bash -# ci-bake-rocm.sh - Docker buildx bake wrapper for ROCm CI builds. -# -# The wrapper keeps three build concerns separate: -# * ci_base builds are content-addressed by vllm.ci_base.content_hash. -# * test image contents record the commit while runtime tags stay build-scoped. -# * ROCm install artifacts are uploaded only for targets that export wheels. -# -# Usage: -# ci-bake-rocm.sh [TARGET] -# -# Set BAKE_PRINT_ONLY=1 to stop after docker buildx bake --print. - -set -euo pipefail - -DEFAULT_REPO_SLUG="vllm-project/vllm" -DEFAULT_CI_HCL_SOURCE="docker/ci-rocm.hcl" -DEFAULT_CI_BASE_CONTENT_FILES=".dockerignore requirements/common.txt requirements/rocm.txt requirements/test/rocm.txt tools/install_torchcodec_rocm.sh rust-toolchain.toml tests/vllm_test_utils" -DEFAULT_CI_BASE_DOCKERFILE="docker/Dockerfile.rocm" -DEFAULT_CI_BASE_DOCKERFILE_STAGES="base rust_toolchain_input_0 rust-toolchain-input rust-toolchain build_nixl lmcache_source build_lmcache build_deepep mori_base ci_base" -DEFAULT_CI_BASE_METADATA_VERSION="3" -# ROCm CI forces REMOTE_VLLM=0, so content identity covers only the selected -# local-source stages rather than unreachable remote-fetch alternatives. -DEFAULT_ROCM_CSRC_CONTENT_FILES=".dockerignore requirements/common.txt requirements/rocm.txt pyproject.toml setup.py CMakeLists.txt cmake csrc vllm/envs.py vllm/__init__.py tools/build_rust.py" -DEFAULT_ROCM_CSRC_DOCKERFILE_STAGES="base fetch_vllm_0 fetch_vllm build_vllm_dependencies rocm-triton-kernels csrc-build" -DEFAULT_ROCM_RUST_CONTENT_FILES=".dockerignore .git_archival.txt pyproject.toml requirements/build/rust.txt rust/Cargo.lock rust/Cargo.toml rust/proto rust/src rust-toolchain.toml tools/build_rust.py tools/build_rust.sh" -DEFAULT_ROCM_RUST_DOCKERFILE_STAGES="base fetch_vllm_0 fetch_vllm vllm-version rust_toolchain_input_0 rust-toolchain-input rust_input_0 rust-input rust-toolchain rust-build" -# Docker's 128-character tag limit minus the longest cache prefix -# ("csrc-rocm-branch-" and "rust-rocm-branch-", both 17 characters). -ROCM_CACHE_BRANCH_TAG_MAX_LEN=111 -CI_BASE_WRITE_SCOPE="" - -TARGET="" -CI_HCL_SOURCE="${CI_HCL_SOURCE:-}" -CI_HCL_PATH="" -CI_BASE_LABEL_OVERRIDE_PATH="" -CSRC_CACHE_OVERRIDE_PATH="" -ROCM_ARG_OVERRIDE_PATH="" -BUILD_CONTEXT_OVERRIDE_PATH="" -SCRIPT_TMP_DIR="" -BAKE_CONFIG_FILE="" -ROCM_BUILD_CONTEXT_ROOT="" -ROCM_BUILD_CONTEXT_INDEX="" -ROCM_BUILD_CONTEXT_COMMIT="" -BAKE_FILES=() -BAKE_ALLOW_ARGS=() -BAKE_TARGETS=() -DEPENDENCY_CACHE_TARGETS=() - -cleanup() { - if [[ -n "${SCRIPT_TMP_DIR}" && -d "${SCRIPT_TMP_DIR}" ]]; then - rm -rf "${SCRIPT_TMP_DIR}" - fi -} -trap cleanup EXIT - -clean_docker_tag() { - local input="$1" - echo "${input}" | sed 's/[^a-zA-Z0-9._-]/_/g' | cut -c1-128 -} - -is_url_like() { - local value="${1:-}" - [[ "${value}" =~ ^[a-zA-Z][a-zA-Z0-9+.-]*:// || "${value}" == git@*:* ]] -} - -is_full_git_sha() { - local value="${1:-}" - [[ "${value}" =~ ^[0-9a-fA-F]{40}$ ]] -} - -select_cache_branch_name() { - local candidate="" - local var="" - - for var in \ - ROCM_CACHE_BRANCH_NAME \ - BUILDKITE_PULL_REQUEST_HEAD_BRANCH \ - BUILDKITE_HEAD_BRANCH \ - BUILDKITE_BRANCH \ - VLLM_BRANCH; do - candidate="${!var:-}" - [[ -n "${candidate}" ]] || continue - is_url_like "${candidate}" && continue - is_full_git_sha "${candidate}" && continue - printf '%s\n' "${candidate}" - return 0 - done -} - -cache_scope_suffix() { - local arch_hash="" - arch_hash=$(printf '%s' "${PYTORCH_ROCM_ARCH:-default}" | sha256sum | cut -c1-12) - printf 'arch-%s\n' "${arch_hash}" -} - -compose_cache_branch_tag() { - local repo_slug="$1" - local branch="$2" - local suffix="" - local prefix="" - local max_prefix_len=0 - local max_tag_len="${ROCM_CACHE_BRANCH_TAG_MAX_LEN}" - - suffix="$(cache_scope_suffix)" - prefix="$(clean_docker_tag "${repo_slug}")-$(clean_docker_tag "${branch}")" - max_prefix_len=$((max_tag_len - ${#suffix} - 1)) - if (( max_prefix_len < 1 )); then - max_prefix_len=1 - fi - printf '%s-%s\n' "${prefix:0:${max_prefix_len}}" "${suffix}" -} - -parse_repo_slug() { - local repo_url="${1:-}" - local repo_slug="" - - if [[ -z "${repo_url}" ]]; then - printf '%s\n' "${DEFAULT_REPO_SLUG}" - return 0 - fi - - repo_slug=$(echo "${repo_url}" | sed -E 's#(git@|https?://)([^/:]+)[:/]([^/]+/[^/.]+)(\.git)?$#\3#') - if [[ "${repo_slug}" != */* ]]; then - repo_slug="${DEFAULT_REPO_SLUG}" - fi - printf '%s\n' "${repo_slug}" -} - -normalize_repo_slug() { - local repo_slug="${1:-}" - - repo_slug="${repo_slug%/}" - repo_slug="${repo_slug%.git}" - repo_slug="${repo_slug#https://github.com/}" - repo_slug="${repo_slug#http://github.com/}" - repo_slug="${repo_slug#ssh://git@github.com/}" - repo_slug="${repo_slug#git@github.com:}" - repo_slug="${repo_slug#github.com/}" - printf '%s\n' "${repo_slug}" -} - -is_trusted_ci_cache_writer() { - local actual_repo="" - local trusted_repo="" - - [[ "${BUILDKITE:-false}" == "true" ]] || return 1 - [[ "${BUILDKITE_PULL_REQUEST:-false}" == "false" ]] || return 1 - [[ "${BUILDKITE_BRANCH:-}" == "${CI_BASE_STABLE_BRANCH:-main}" ]] || return 1 - actual_repo=$(normalize_repo_slug "${BUILDKITE_REPO:-}") - trusted_repo=$(normalize_repo_slug \ - "${CI_BASE_STABLE_REPO_SLUG:-${DEFAULT_REPO_SLUG}}") - [[ -n "${actual_repo}" && "${actual_repo}" == "${trusted_repo}" ]] -} - -ci_base_write_scope() { - local identity="" - local source_repo="${BUILDKITE_PULL_REQUEST_REPO:-${BUILDKITE_REPO:-local}}" - - if is_trusted_ci_cache_writer; then - return 0 - fi - identity=$(printf '%s\n' "${source_repo}" | sha256sum | cut -c1-12) - printf 'preview-%s\n' "${identity}" -} - -configure_ci_base_write_scope() { - local scope="" - - scope=$(ci_base_write_scope) - if [[ -n "${scope}" ]]; then - CI_BASE_WRITE_SCOPE=$(clean_docker_tag "${scope}") - echo "Non-canonical cache writes use source scope: ${CI_BASE_WRITE_SCOPE}" - else - CI_BASE_WRITE_SCOPE="" - echo "Trusted main build: publishing canonical ci_base refs" - fi - export CI_BASE_WRITE_SCOPE -} - -get_buildkite_repo_slug() { - parse_repo_slug "${BUILDKITE_PULL_REQUEST_REPO:-${BUILDKITE_REPO:-}}" -} - -get_buildkite_target_repo_slug() { - parse_repo_slug "${BUILDKITE_REPO:-}" -} - -get_buildkite_target_repo_url() { - local repo_url="${BUILDKITE_REPO:-}" - - if [[ -n "${repo_url}" ]] && is_url_like "${repo_url}"; then - printf '%s\n' "${repo_url}" - return 0 - fi - - printf 'https://github.com/%s.git\n' "${DEFAULT_REPO_SLUG}" -} - -git_fetch_with_timeout() { - local timeout_secs="${ROCM_CACHE_GIT_FETCH_TIMEOUT:-60}" - local -a fetch_command=(git fetch --no-auto-maintenance) - - # Detached maintenance can race a later shallow fetch on .git/shallow. - if command -v timeout >/dev/null 2>&1; then - timeout "${timeout_secs}s" "${fetch_command[@]}" "$@" - else - "${fetch_command[@]}" "$@" - fi -} - -git_fetch_for_cache() { - git_fetch_with_timeout "$@" 2>/dev/null -} - -hash_string_short() { - printf '%s' "$1" | sha256sum | cut -c1-16 -} - -list_content_files() { - # Hash the checkout inputs Docker can intentionally consume, not ignored - # compiler/test debris left behind on a reused worker. - if [[ -n "${ROCM_BUILD_CONTEXT_ROOT:-}" ]]; then - GIT_INDEX_FILE="${ROCM_BUILD_CONTEXT_INDEX}" \ - git ls-files -z --cached -- "$1" | LC_ALL=C sort -z - else - git ls-files -z --cached --others --exclude-standard -- "$1" \ - | LC_ALL=C sort -z - fi -} - -content_regular_file() { - local file="$1" - local physical_file="" - - physical_file="${file}" - if [[ -n "${ROCM_BUILD_CONTEXT_ROOT:-}" && "${file}" != /* ]]; then - physical_file="${ROCM_BUILD_CONTEXT_ROOT}/${file}" - fi - [[ -f "${physical_file}" ]] -} - -hash_content_file() { - local file="$1" - local physical_file="" - local checksum="" - local file_mode="" - - physical_file="${file}" - if [[ -n "${ROCM_BUILD_CONTEXT_ROOT:-}" && "${file}" != /* ]]; then - physical_file="${ROCM_BUILD_CONTEXT_ROOT}/${file}" - fi - if [[ -L "${physical_file}" ]]; then - printf 'symlink:%s\ntarget:' "${file}" - readlink -n -- "${physical_file}" || return $? - printf '\n' - return - fi - if [[ ! -f "${physical_file}" ]]; then - printf 'missing:%s\n' "${file}" - return - fi - file_mode=$(stat -c '%a' "${physical_file}") || return $? - printf 'file:%s\nmode:%s\n' "${file}" "${file_mode}" - checksum=$(sha256sum < "${physical_file}") || return $? - checksum="${checksum%% *}" - printf '%s %s\n' "${checksum}" "${file}" -} - -hash_content_directory() { - if ! list_content_files "$1" | while IFS= read -r -d '' file; do - hash_content_file "${file}" || exit $? - done; then - echo "Failed to hash content under $1" >&2 - return 1 - fi -} - -compute_content_hash() { - local path="" - local physical_path="" - - for path in "$@"; do - physical_path="${path}" - if [[ -n "${ROCM_BUILD_CONTEXT_ROOT:-}" && "${path}" != /* ]]; then - physical_path="${ROCM_BUILD_CONTEXT_ROOT}/${path}" - fi - if [[ -L "${physical_path}" || -f "${physical_path}" ]]; then - hash_content_file "${path}" || return $? - elif [[ -d "${physical_path}" ]]; then - hash_content_directory "${path}" || return $? - else - printf 'missing:%s\n' "${path}" - fi - done | sha256sum | cut -d' ' -f1 -} - -validate_ci_build_context_source() { - local source_root="$1" - local context_commit="" - local source_head="" - local entry="" - local metadata="" - local mode="" - local stage="" - local path="" - local worktree_diff_status=0 - local staged_diff_status=0 - local index_modes_file="${SCRIPT_TMP_DIR}/git-index-modes" - local info_attributes="" - - if ! context_commit=$(git -C "${source_root}" rev-parse \ - --verify "${BUILDKITE_COMMIT:-HEAD}^{commit}"); then - echo "Failed to resolve the CI Docker context revision" >&2 - return 1 - fi - if ! source_head=$(git -C "${source_root}" rev-parse --verify HEAD); then - echo "Failed to resolve the checked-out CI revision" >&2 - return 1 - fi - if [[ "${context_commit}" != "${source_head}" ]]; then - echo "BUILDKITE_COMMIT does not match the checked-out CI revision" >&2 - return 1 - fi - ROCM_BUILD_CONTEXT_COMMIT="${context_commit}" - # Shared AMD workspaces may present tracked files with inflated executable - # bits. Context modes come from the pinned Git tree, so ignore only that - # filesystem drift while continuing to reject content changes. - git -C "${source_root}" -c core.fileMode=false diff \ - --quiet --no-ext-diff --ignore-submodules=none -- \ - || worktree_diff_status=$? - if (( worktree_diff_status != 0 )); then - if (( worktree_diff_status > 1 )); then - printf 'Failed to inspect tracked CI worktree changes (git diff exited %s)\n' \ - "${worktree_diff_status}" >&2 - return 1 - fi - echo "Tracked worktree changes cannot be omitted from the CI Docker context" \ - >&2 - echo "Tracked worktree diff (first 50 entries):" >&2 - git -C "${source_root}" -c core.fileMode=false diff \ - --no-ext-diff --ignore-submodules=none --name-status -- \ - | sed -n '1,50p' >&2 || true - git -C "${source_root}" -c core.fileMode=false diff \ - --no-ext-diff --ignore-submodules=none --summary -- \ - | sed -n '1,50p' >&2 || true - return 1 - fi - git -C "${source_root}" diff --cached \ - --quiet --no-ext-diff --ignore-submodules=none HEAD -- \ - || staged_diff_status=$? - if (( staged_diff_status != 0 )); then - if (( staged_diff_status > 1 )); then - printf 'Failed to inspect staged CI changes (git diff exited %s)\n' \ - "${staged_diff_status}" >&2 - return 1 - fi - echo "Staged changes cannot be omitted from the CI Docker context" >&2 - echo "Staged diff (first 50 entries):" >&2 - git -C "${source_root}" diff --cached \ - --no-ext-diff --ignore-submodules=none --name-status HEAD -- \ - | sed -n '1,50p' >&2 || true - git -C "${source_root}" diff --cached \ - --no-ext-diff --ignore-submodules=none --summary HEAD -- \ - | sed -n '1,50p' >&2 || true - return 1 - fi - # Untracked and ignored worker outputs are intentionally absent: the - # pinned Git tree, rather than mutable checkout contents, is the contract. - if ! info_attributes=$(git -C "${source_root}" \ - rev-parse --path-format=absolute --git-path info/attributes); then - echo "Failed to locate repository-local Git attributes" >&2 - return 1 - fi - if [[ -s "${info_attributes}" ]]; then - echo "Repository-local Git attributes cannot define the CI Docker context" \ - >&2 - return 1 - fi - - ROCM_BUILD_CONTEXT_INDEX="${SCRIPT_TMP_DIR}/docker-context.index" - if ! GIT_INDEX_FILE="${ROCM_BUILD_CONTEXT_INDEX}" \ - git -C "${source_root}" -c core.splitIndex=false \ - read-tree "${context_commit}^{tree}"; then - echo "Failed to create the CI Docker context index" >&2 - return 1 - fi - if ! GIT_INDEX_FILE="${ROCM_BUILD_CONTEXT_INDEX}" \ - git -C "${source_root}" ls-files --stage -z \ - > "${index_modes_file}"; then - echo "Failed to read the Git index for the CI Docker context" >&2 - return 1 - fi - while IFS= read -r -d '' entry; do - metadata="${entry%%$'\t'*}" - mode="${metadata%% *}" - stage="${metadata##* }" - path="${entry#*$'\t'}" - if [[ "${stage}" != "0" ]]; then - echo "Unmerged Git entry cannot be used as Docker context: ${path}" >&2 - return 1 - fi - case "${mode}" in - 100644|100755|120000) - ;; - 160000) - echo "Git submodule cannot be materialized in the CI Docker context: ${path}" \ - >&2 - return 1 - ;; - *) - echo "Unsupported Git mode ${mode} for ${path}" >&2 - return 1 - ;; - esac - done < "${index_modes_file}" -} - -describe_ci_revision() { - git -C "$1" describe --tags --long --abbrev=10 \ - --match 'v[0-9]*' "$2" 2>/dev/null -} - -write_ci_git_archival_metadata() { - local source_root="$1" - local context_root="$2" - local commit="${ROCM_BUILD_CONTEXT_COMMIT}" - local commit_date="" - local describe="" - local is_shallow="false" - - commit_date=$(git -C "${source_root}" show -s --format=%cI "${commit}") \ - || return $? - is_shallow=$(git -C "${source_root}" rev-parse --is-shallow-repository) \ - || return $? - if git -C "${source_root}" remote get-url origin >/dev/null 2>&1; then - # AMD agents use shallow, no-tag clones. Reach a release tag so one - # commit cannot acquire different versions from different depths. - # Versioning only needs tags and commits, not historical source trees. - echo "Synchronizing version tags for the canonical CI Docker context" - (cd "${source_root}" && git_fetch_with_timeout --quiet \ - --filter=tree:0 --prune origin '+refs/tags/*:refs/tags/*') \ - || return $? - if [[ "${is_shallow}" == "true" ]] \ - && ! describe_ci_revision "${source_root}" "${commit}" >/dev/null; then - echo "Deepening history to reach a version tag" - (cd "${source_root}" && git_fetch_with_timeout --quiet \ - --filter=tree:0 --no-tags --deepen=1000 origin "${commit}") \ - || return $? - is_shallow=$(git -C "${source_root}" \ - rev-parse --is-shallow-repository) || return $? - if [[ "${is_shallow}" == "true" ]] \ - && ! describe_ci_revision "${source_root}" "${commit}" >/dev/null; then - echo "No version tag within 1,000 commits; fetching full history" - (cd "${source_root}" && git_fetch_with_timeout --quiet \ - --filter=tree:0 --no-tags --unshallow origin "${commit}") \ - || return $? - fi - fi - fi - if ! describe=$(describe_ci_revision "${source_root}" "${commit}"); then - echo "No numeric version tag is reachable from the CI revision" >&2 - return 1 - fi - if [[ -e "${context_root}/.git_archival.txt" \ - || -L "${context_root}/.git_archival.txt" ]]; then - echo "The source tree already contains .git_archival.txt" >&2 - return 1 - fi - printf 'node: %s\nnode-date: %s\ndescribe-name: %s\n' \ - "${commit}" "${commit_date}" "${describe}" \ - > "${context_root}/.git_archival.txt" \ - && chmod 0644 -- "${context_root}/.git_archival.txt" -} - -write_build_context_override() { - local escaped_context="" - - [[ -n "${ROCM_BUILD_CONTEXT_ROOT:-}" ]] || return 0 - escaped_context=$(hcl_escape_string "${ROCM_BUILD_CONTEXT_ROOT}") || return $? - if ! { - printf 'target "_common-rocm" {\n' - printf ' context = "%s"\n' "${escaped_context}" - printf '}\n' - } > "${BUILD_CONTEXT_OVERRIDE_PATH}"; then - echo "Failed to write the CI Docker context override" >&2 - return 1 - fi - BAKE_FILES+=(-f "${BUILD_CONTEXT_OVERRIDE_PATH}") -} - -prepare_ci_build_context() { - local source_root="" - local context_root="" - - [[ "${BUILDKITE:-false}" == "true" ]] || return 0 - [[ "${REMOTE_VLLM:-0}" == "0" ]] || return 0 - - # BuildKit includes file modes in cache keys. Export the pinned revision to - # an owned context instead of changing modes in the shared checkout. - if ! source_root=$(git rev-parse --show-toplevel); then - echo "Failed to locate the CI source checkout" >&2 - return 1 - fi - validate_ci_build_context_source "${source_root}" || return $? - context_root="${SCRIPT_TMP_DIR}/docker-context" - if ! mkdir -m 0700 -- "${context_root}"; then - echo "Failed to create the owned CI Docker context" >&2 - return 1 - fi - if ! ( - umask 0022 - unset GIT_LFS_SKIP_SMUDGE - export GIT_ATTR_NOSYSTEM=1 - export GIT_INDEX_FILE="${ROCM_BUILD_CONTEXT_INDEX}" - git -C "${source_root}" \ - -c core.attributesFile=/dev/null \ - -c core.autocrlf=false \ - -c core.eol=lf \ - -c core.symlinks=true \ - --work-tree="${context_root}" \ - checkout-index --all - ); then - echo "Failed to materialize the CI Docker context from the Git index" >&2 - return 1 - fi - - # setuptools-scm understands Git's stable archive format, so wheels and - # Rust artifacts retain their exact version without copying Git history. - if ! is_ci_base_target; then - write_ci_git_archival_metadata "${source_root}" "${context_root}" \ - || return $? - fi - if [[ -e "${context_root}/.git" ]]; then - echo "Canonical CI Docker context unexpectedly contains .git" >&2 - return 1 - fi - ROCM_BUILD_CONTEXT_ROOT="${context_root}" - BAKE_ALLOW_ARGS+=(--allow "fs.read=${ROCM_BUILD_CONTEXT_ROOT}") - echo "Using canonical CI Docker context: ${ROCM_BUILD_CONTEXT_ROOT}" -} - -configure_custom_rocm_stages() { - using_custom_rocm_dockerfiles || return 0 - local context_root="${ROCM_BUILD_CONTEXT_ROOT:-.}" - local dockerfile="${context_root}/${CI_BASE_DOCKERFILE}" - local stages="" - - (cd "${context_root}" && \ - validate_rocm_dockerfile "${CI_BASE_DOCKERFILE}" \ - ci_base test export_vllm export_test_smoke csrc-build rust-build) \ - || return $? - # Hash all stages for custom layouts, including additional helper stages. - stages=$(rocm_dockerfile_stages "${dockerfile}" | paste -sd ' ') - CI_BASE_DOCKERFILE_STAGES="${CI_BASE_DOCKERFILE_STAGES:-${stages}}" - ROCM_CSRC_DOCKERFILE_STAGES="${ROCM_CSRC_DOCKERFILE_STAGES:-${stages}}" - ROCM_RUST_DOCKERFILE_STAGES="${ROCM_RUST_DOCKERFILE_STAGES:-${stages}}" -} - -compose_dependency_cache_key() { - local prefix="$1" - local material="$2" - local cleaned_prefix="" - - cleaned_prefix=$(clean_docker_tag "${prefix}" | cut -c1-96) - printf '%s-%s\n' "${cleaned_prefix}" "$(hash_string_short "${material}")" -} - -hash_dockerfile_stages() { - local dockerfile="$1" - local stages="$2" - local physical_dockerfile="" - - physical_dockerfile="${dockerfile}" - if [[ -n "${ROCM_BUILD_CONTEXT_ROOT:-}" && "${dockerfile}" != /* ]]; then - physical_dockerfile="${ROCM_BUILD_CONTEXT_ROOT}/${dockerfile}" - fi - - awk -v wanted_stages="${stages}" ' - BEGIN { - split(wanted_stages, stage_list, /[[:space:]]+/) - for (idx in stage_list) { - if (stage_list[idx] != "") { - wanted[stage_list[idx]] = 1 - } - } - emit = 0 - } - toupper($1) == "FROM" { - stage = "" - for (idx = 1; idx <= NF; idx++) { - if (tolower($idx) == "as" && idx < NF) { - stage = tolower($(idx + 1)) - } - } - emit = (stage in wanted) - } - emit { - print - } - ' "${physical_dockerfile}" -} - -discover_dockerfile_stage_args() { - local dockerfile="$1" - local stages="$2" - local physical_dockerfile="" - - physical_dockerfile="${dockerfile}" - if [[ -n "${ROCM_BUILD_CONTEXT_ROOT:-}" && "${dockerfile}" != /* ]]; then - physical_dockerfile="${ROCM_BUILD_CONTEXT_ROOT}/${dockerfile}" - fi - [[ -f "${physical_dockerfile}" ]] || return 0 - - awk -v wanted_stages="${stages}" ' - function add_arg(name) { - if (name != "" && !(name in seen)) { - seen[name] = 1 - args[++arg_count] = name - } - } - BEGIN { - split(wanted_stages, stage_list, /[[:space:]]+/) - for (idx in stage_list) { - if (stage_list[idx] != "") { - wanted[stage_list[idx]] = 1 - } - } - emit = 1 - } - { - line = $0 - if (toupper($1) == "FROM") { - stage = "" - for (idx = 1; idx <= NF; idx++) { - if (tolower($idx) == "as" && idx < NF) { - stage = tolower($(idx + 1)) - } - } - emit = (stage in wanted) - } - if (emit) { - lines[++line_count] = line - } - } - END { - for (idx = 1; idx <= line_count; idx++) { - line = lines[idx] - arg_name = line - sub(/^[[:space:]]*[Aa][Rr][Gg][[:space:]]+/, "", arg_name) - if (arg_name != line) { - sub(/[=[:space:]].*/, "", arg_name) - if (arg_name ~ /^[A-Za-z_][A-Za-z0-9_]*$/) { - add_arg(arg_name) - } - } - } - - for (idx = 1; idx <= line_count; idx++) { - line = lines[idx] - for (arg_idx = 1; arg_idx <= arg_count; arg_idx++) { - name = args[arg_idx] - if (line ~ "\\$\\{" name "([}:][^}]*)?\\}" \ - || line ~ "\\$" name "([^A-Za-z0-9_]|$)") { - used[name] = 1 - } - } - } - - for (arg_idx = 1; arg_idx <= arg_count; arg_idx++) { - name = args[arg_idx] - if (used[name]) { - print name - } - } - } - ' "${physical_dockerfile}" -} - -get_content_arg_names() { - local dockerfile="$1" - local stages="$2" - local explicit_args="${3:-}" - - if [[ -n "${explicit_args}" ]]; then - tr ' ' '\n' <<< "${explicit_args}" - else - discover_dockerfile_stage_args "${dockerfile}" "${stages}" - fi | awk 'NF && !seen[$0]++' -} - -compute_ci_base_content_hash() { - local -a content_paths=() - local -a content_args=() - local content_files_hash="" - local dockerfile="${CI_BASE_DOCKERFILE:-}" - local stages="${CI_BASE_DOCKERFILE_STAGES:-}" - - read -r -a content_paths <<< "${CI_BASE_CONTENT_FILES}" - mapfile -t content_args < <( - get_content_arg_names "${dockerfile}" "${stages}" "${CI_BASE_CONTENT_ARGS:-}" - ) - if ! content_files_hash=$(compute_content_hash "${content_paths[@]}"); then - echo "Failed to hash ci_base content files" >&2 - return 1 - fi - - { - printf 'content-files-hash:%s\n' "${content_files_hash}" - if [[ -n "${dockerfile}" ]]; then - printf 'dockerfile:%s\n' "${dockerfile}" - printf 'resolved-build-args:\n' - hash_dockerfile_arg_values "${dockerfile}" "${content_args[@]}" \ - || return 1 - if [[ -n "${stages}" ]]; then - printf 'dockerfile-stages:%s\n' "${stages}" - if content_regular_file "${dockerfile}"; then - hash_dockerfile_stages "${dockerfile}" "${stages}" - else - printf 'missing:%s\n' "${dockerfile}" - fi - fi - fi - } | sha256sum | cut -d' ' -f1 -} - -extract_dockerfile_arg_default() { - local dockerfile="$1" - local arg_name="$2" - local physical_dockerfile="" - - physical_dockerfile="${dockerfile}" - if [[ -n "${ROCM_BUILD_CONTEXT_ROOT:-}" && "${dockerfile}" != /* ]]; then - physical_dockerfile="${ROCM_BUILD_CONTEXT_ROOT}/${dockerfile}" - fi - sed -n -E "s/^[[:space:]]*[Aa][Rr][Gg][[:space:]]+${arg_name}=\"?([^\"[:space:]]+)\"?.*/\\1/p" \ - "${physical_dockerfile}" | head -1 -} - -resolve_image_digest() { - local image_ref="$1" - local attempts="${ROCM_IMAGE_DIGEST_ATTEMPTS:-4}" - local delay_secs="${ROCM_IMAGE_DIGEST_RETRY_DELAY:-2}" - local attempt=0 - local digest="" - local output="" - local status=0 - - if [[ "${image_ref}" =~ @(sha256:[0-9a-f]{64})$ ]]; then - printf '%s\n' "${BASH_REMATCH[1]}" - return - fi - if [[ ! "${attempts}" =~ ^[1-9][0-9]*$ \ - || ! "${delay_secs}" =~ ^[0-9]+$ ]]; then - echo "Invalid image digest retry configuration" >&2 - return 1 - fi - - for ((attempt = 1; attempt <= attempts; attempt++)); do - status=0 - output=$(docker buildx imagetools inspect "${image_ref}" 2>&1) || status=$? - digest=$(awk '$1 == "Digest:" { print $2; exit }' <<< "${output}") - if ((status == 0)) && [[ "${digest}" =~ ^sha256:[0-9a-f]{64}$ ]]; then - printf '%s\n' "${digest}" - return - fi - if ((attempt < attempts)); then - printf \ - 'Image digest lookup failed for %s (%d/%d, status %d); retrying\n' \ - "${image_ref}" "${attempt}" "${attempts}" "${status}" >&2 - sleep "${delay_secs}" - fi - done - - printf 'Failed to resolve digest for %s (status %d)\n%s\n' \ - "${image_ref}" "${status}" "${output:-}" >&2 - return 1 -} - -resolve_dockerfile_arg_value() { - local dockerfile="$1" - local arg_name="$2" - local env_name="${arg_name}" - local value="" - - case "${arg_name}" in - ARG_PYTORCH_ROCM_ARCH) - env_name="PYTORCH_ROCM_ARCH" - ;; - max_jobs) - env_name="CI_MAX_JOBS" - ;; - esac - - value="${!env_name:-}" - if [[ -z "${value}" && "${env_name}" != "${arg_name}" ]]; then - value="${!arg_name:-}" - fi - if [[ -z "${value}" ]] && content_regular_file "${dockerfile}"; then - value=$(extract_dockerfile_arg_default "${dockerfile}" "${arg_name}") - fi - - printf '%s\n' "${value}" -} - -hash_dockerfile_arg_values() { - local dockerfile="$1" - local arg_name="" - local arg_value="" - local digest="" - shift || true - - for arg_name in "$@"; do - [[ -n "${arg_name}" ]] || continue - arg_value=$(resolve_dockerfile_arg_value "${dockerfile}" "${arg_name}") - if [[ "${arg_name}" == "BASE_IMAGE" && -n "${arg_value}" ]]; then - if ! digest=$(resolve_image_digest "${arg_value}"); then - echo "Failed to resolve digest for BASE_IMAGE=${arg_value}" >&2 - return 1 - fi - printf 'arg:%s.digest=%s\n' "${arg_name}" "${digest}" - else - printf 'arg:%s=%s\n' "${arg_name}" "${arg_value:-}" - fi - done -} - -pin_base_image() { - local dockerfile="${CI_BASE_DOCKERFILE:-${DEFAULT_CI_BASE_DOCKERFILE}}" - local base_image="" - local digest="" - - base_image=$(resolve_dockerfile_arg_value "${dockerfile}" "BASE_IMAGE") - [[ -n "${base_image}" ]] || return 0 - if ! digest=$(resolve_image_digest "${base_image}"); then - echo "Error: could not resolve base image digest for ${base_image}" >&2 - echo "Refusing to compute content-addressed cache keys from a mutable tag." >&2 - return 1 - fi - BASE_IMAGE="${base_image%@*}@${digest}" - export BASE_IMAGE - echo "Pinned base image for this build: ${BASE_IMAGE}" -} - -is_ci_base_target() { - [[ "${TARGET}" == *"ci-base-rocm"* ]] -} - -is_commit_image_target() { - [[ -n "${IMAGE_TAG:-}" && -n "${BUILDKITE_COMMIT:-}" ]] || return 1 - is_ci_base_target && return 1 - return 0 -} - -should_upload_wheel_artifacts() { - [[ "${UPLOAD_ROCM_WHEEL_ARTIFACTS:-0}" == "1" ]] && return 0 - [[ "${TARGET}" == *"with-wheel"* \ - || "${TARGET}" == *"export-wheel"* \ - || "${TARGET}" == *"artifact"* ]] -} - -should_export_rocm_smoke() { - [[ "${TARGET}" == "test-rocm-ci-with-wheel" \ - || "${TARGET}" == "smoke-test-rocm-ci" ]] -} - -should_export_kernel_symbol_map() { - [[ "${VLLM_KERNEL_SYMBOL_MAP:-0}" == "1" ]] || return 1 - case "${TARGET}" in - test-rocm-ci|test-rocm-ci-with-wheel|test-rocm-ci-with-artifacts|\ - export-wheel-rocm|csrc-rocm-ci|smoke-test-rocm-ci|kernel-symbol-map-rocm) - return 0 ;; - *) return 1 ;; - esac -} - -export_kernel_symbol_map() { - should_export_kernel_symbol_map || return 0 - echo "--- :world_map: Exporting ROCm kernel symbol map" - rm -rf ./kernel-symbol-map-rocm || return 1 - docker buildx bake \ - "${BAKE_ALLOW_ARGS[@]}" "${BAKE_FILES[@]}" \ - --progress "${BUILDKIT_PROGRESS:-plain}" \ - kernel-symbol-map-rocm || return 1 - upload_kernel_symbol_map_if_present -} - -export_kernel_symbol_map_from_cache() { - should_export_kernel_symbol_map || return 0 - # A separate shell keeps setup failures fatal inside the optional export. - FORCE_BUILD=1 VLLM_KERNEL_SYMBOL_MAP=1 UPLOAD_ROCM_WHEEL_ARTIFACTS=0 \ - CI_HCL_SOURCE="${CI_HCL_PATH}" VLLM_BAKE_FILE="${VLLM_BAKE_FILE}" \ - BUILDER_NAME="${BUILDER_NAME}" \ - bash "${BASH_SOURCE[0]}" kernel-symbol-map-rocm \ - || echo "ROCm kernel symbol map export from cache failed; continuing without it" >&2 -} - -upload_kernel_symbol_map_if_present() { - should_export_kernel_symbol_map || return 0 - local map_dir="./kernel-symbol-map-rocm" - local map_name="kernel_symbol_map.rocm.json.gz" - if [[ ! -s "${map_dir}/${map_name}" ]]; then - echo "ROCm kernel symbol map export is missing: ${map_dir}/${map_name}" >&2 - return 1 - fi - if [[ "${BUILDKITE:-false}" == "true" ]]; then - (cd "${map_dir}" && buildkite-agent artifact upload "${map_name}") - fi -} - -verify_rocm_smoke_export() { - local marker="./build/rocm-smoke-export/vllm-smoke-ok" - local expected_smoke_id="${BUILDKITE_BUILD_ID:-local}" - local actual_smoke_id="" - - should_export_rocm_smoke || return 0 - if [[ ! -f "${marker}" ]]; then - echo "ROCm BuildKit smoke marker is missing: ${marker}" >&2 - return 1 - fi - actual_smoke_id="$(< "${marker}")" - if [[ "${actual_smoke_id}" != "${expected_smoke_id}" ]]; then - echo "ROCm BuildKit smoke marker belongs to ${actual_smoke_id}, not ${expected_smoke_id}" \ - >&2 - return 1 - fi -} - -get_remote_image_label() { - local image_ref="$1" - local label_key="$2" - local format="{{ index .Image.Config.Labels \"${label_key}\" }}" - - docker buildx imagetools inspect "${image_ref}" \ - --format "${format}" 2>/dev/null | awk 'NF { print; exit }' || true -} - -remote_ci_base_identity_is_current_with_retry() { - local image_ref="$1" - local attempts="${CI_BASE_LABEL_ATTEMPTS:-4}" - local delay_secs="${CI_BASE_LABEL_RETRY_DELAY:-2}" - local identity="" - local remote_hash="" - local remote_version="" - local content_files_hash="" - local base_digest="" - local attempt=0 - local expected_version="${CI_BASE_METADATA_VERSION:-${DEFAULT_CI_BASE_METADATA_VERSION}}" - local format='{{ index .Image.Config.Labels "vllm.ci_base.content_hash" }}|{{ index .Image.Config.Labels "vllm.ci_base.metadata_version" }}|{{ index .Image.Config.Labels "vllm.ci_base.content_files_hash" }}|{{ index .Image.Config.Labels "vllm.rocm.base_image_digest" }}' - - if [[ ! "${attempts}" =~ ^[1-9][0-9]*$ \ - || ! "${delay_secs}" =~ ^[0-9]+$ ]]; then - echo "Invalid ci_base label retry configuration" >&2 - return 1 - fi - - for ((attempt = 1; attempt <= attempts; attempt++)); do - identity=$(docker buildx imagetools inspect "${image_ref}" \ - --format "${format}" 2>/dev/null || true) - IFS='|' read -r \ - remote_hash remote_version content_files_hash base_digest <<< "${identity}" - if [[ "${remote_hash}" == "${CI_BASE_CONTENT_HASH:-}" \ - && "${remote_version}" == "${expected_version}" \ - && "${content_files_hash}" =~ ^[0-9a-f]{64}$ \ - && "${base_digest}" =~ ^sha256:[0-9a-f]{64}$ ]]; then - return 0 - fi - ((attempt == attempts)) || sleep "${delay_secs}" - done - - echo "ci_base identity did not match after ${attempts} attempts: ${image_ref}" >&2 - echo " expected hash/version: ${CI_BASE_CONTENT_HASH:-}/${expected_version}" >&2 - echo " observed hash/version: ${remote_hash:-}/${remote_version:-}" >&2 - return 1 -} - -registry_ref_exists_with_retry() { - local inspect_kind="$1" - local image_ref="$2" - local attempts="${ROCM_REGISTRY_PROBE_ATTEMPTS:-2}" - local delay_secs="${ROCM_REGISTRY_PROBE_RETRY_DELAY:-1}" - local output="" - local status=0 - local attempt=0 - - if [[ ! "${attempts}" =~ ^[1-9][0-9]*$ \ - || ! "${delay_secs}" =~ ^[0-9]+$ ]]; then - echo "Invalid registry probe retry configuration" >&2 - return 1 - fi - - for ((attempt = 1; attempt <= attempts; attempt++)); do - status=0 - if [[ "${inspect_kind}" == "manifest" ]]; then - output=$(docker manifest inspect "${image_ref}" 2>&1) || status=$? - else - output=$(docker buildx imagetools inspect "${image_ref}" 2>&1) \ - || status=$? - fi - if ((status == 0)); then - return 0 - fi - # A definitive registry miss is normal for a new content key. Retry - # transport/rate-limit failures, but do not add latency to a real 404. - if grep -Eiq \ - 'manifest unknown|no such manifest|name unknown|not found|does not exist' \ - <<< "${output}"; then - return 1 - fi - if ((attempt < attempts)); then - echo "Registry probe failed for ${image_ref} (${attempt}/${attempts}); retrying" >&2 - sleep "${delay_secs}" - fi - done - - echo "Registry probe failed after ${attempts} attempts: ${image_ref}" >&2 - return 1 -} - -remote_image_exists() { - registry_ref_exists_with_retry manifest "$1" -} - -use_existing_builder() { - echo "Using existing builder: ${BUILDER_NAME}" - docker buildx use "${BUILDER_NAME}" - docker buildx inspect --bootstrap -} - -buildx_driver() { - local builder="${1:-}" - - if [[ -n "${builder}" ]]; then - docker buildx inspect "${builder}" 2>/dev/null - else - docker buildx inspect 2>/dev/null - fi | awk -F': *' '$1 == "Driver" { print $2; exit }' -} - -builder_supports_registry_cache() { - local driver="$1" - - [[ -n "${driver}" && "${driver}" != "docker" ]] -} - -create_and_bootstrap_builder() { - local driver="$1" - local endpoint="${2:-}" - - echo "Creating builder '${BUILDER_NAME}' with ${driver} driver" - if [[ -n "${endpoint}" ]]; then - docker buildx create \ - --name "${BUILDER_NAME}" \ - --driver "${driver}" \ - --use \ - "${endpoint}" - else - docker buildx create --name "${BUILDER_NAME}" --driver "${driver}" --use - fi - docker buildx inspect --bootstrap -} - -init_config() { - # shellcheck source=.buildkite/scripts/rocm/build-config.sh - source "$(dirname "${BASH_SOURCE[0]}")/rocm/build-config.sh" - configure_rocm_build - - TARGET="${1:-test-ci}" - if using_custom_rocm_dockerfiles; then - if [[ -z "${BASE_IMAGE:-}" ]]; then - echo "Custom ROCm builds require BASE_IMAGE from the selected base handoff" >&2 - return 1 - fi - if ! is_ci_base_target && [[ -z "${CI_BASE_IMAGE:-}" ]]; then - echo "Custom ROCm builds require CI_BASE_IMAGE from the selected ci_base handoff" >&2 - return 1 - fi - fi - BAKE_TARGETS=("${TARGET}") - DEPENDENCY_CACHE_TARGETS=() - CI_HCL_SOURCE="${CI_HCL_SOURCE:-${CI_HCL_FILE:-${DEFAULT_CI_HCL_SOURCE}}}" - VLLM_BAKE_FILE="${VLLM_BAKE_FILE:-docker/docker-bake-rocm.hcl}" - BUILDER_NAME="${BUILDER_NAME:-vllm-builder}" - BUILDKIT_SOCKET="${BUILDKIT_SOCKET:-/run/buildkit/buildkitd.sock}" - PYTORCH_ROCM_ARCH="${PYTORCH_ROCM_ARCH:-gfx90a;gfx942;gfx950}" - CI_BASE_CONTENT_FILES="${CI_BASE_CONTENT_FILES:-${DEFAULT_CI_BASE_CONTENT_FILES}}" - CI_BASE_DOCKERFILE="${CI_BASE_DOCKERFILE:-${DEFAULT_CI_BASE_DOCKERFILE}}" - if ! using_custom_rocm_dockerfiles; then - CI_BASE_DOCKERFILE_STAGES="${CI_BASE_DOCKERFILE_STAGES:-${DEFAULT_CI_BASE_DOCKERFILE_STAGES}}" - fi - CI_BASE_METADATA_VERSION="${CI_BASE_METADATA_VERSION:-${DEFAULT_CI_BASE_METADATA_VERSION}}" - CI_BASE_IMAGE_TAG="${CI_BASE_IMAGE_TAG:-rocm/vllm-dev:ci_base}" - export PYTORCH_ROCM_ARCH CI_BASE_DOCKERFILE - - SCRIPT_TMP_DIR=$(mktemp -d -t ci-bake-rocm.XXXXXX) - CI_HCL_PATH="${SCRIPT_TMP_DIR}/ci.hcl" - CI_BASE_LABEL_OVERRIDE_PATH="${SCRIPT_TMP_DIR}/ci-base-label-override.hcl" - CSRC_CACHE_OVERRIDE_PATH="${SCRIPT_TMP_DIR}/rocm-csrc-cache-override.hcl" - ROCM_ARG_OVERRIDE_PATH="${SCRIPT_TMP_DIR}/rocm-arg-override.hcl" - BUILD_CONTEXT_OVERRIDE_PATH="${SCRIPT_TMP_DIR}/build-context-override.hcl" - BAKE_CONFIG_FILE="bake-config-build-${BUILDKITE_BUILD_NUMBER:-local}.json" -} - -print_header() { - echo "--- :docker: Setting up Docker buildx bake" - echo "Target: ${TARGET}" - echo "CI HCL source: ${CI_HCL_SOURCE}" - echo "vLLM bake file: ${VLLM_BAKE_FILE}" - if is_ci_base_target; then - echo "Build mode: ci_base" - elif is_commit_image_target; then - echo "Build mode: build-scoped commit image" - else - echo "Build mode: generic" - fi - if [[ "${USE_SCCACHE:-0}" == "1" ]]; then - echo "Compiler cache: sccache enabled" - fi -} - -validate_inputs() { - if using_custom_rocm_dockerfiles; then - validate_rocm_dockerfile "${ROCM_BASE_DOCKERFILE}" || return $? - validate_rocm_dockerfile "${CI_BASE_DOCKERFILE}" \ - ci_base test export_vllm export_test_smoke csrc-build rust-build || return $? - fi - if [[ ! -f "${VLLM_BAKE_FILE}" ]]; then - echo "Error: vLLM bake file not found at ${VLLM_BAKE_FILE}" - echo "Make sure you're running from the vLLM repository root" - exit 1 - fi - - if [[ -n "${CI_HCL_SOURCE:-}" ]] && is_url_like "${CI_HCL_SOURCE}"; then - echo "Error: remote CI HCL sources are not supported: ${CI_HCL_SOURCE}" - echo "Use the vLLM-owned docker/ci-rocm.hcl or set CI_HCL_SOURCE to a local file." - exit 1 - fi - - if [[ -n "${CI_HCL_SOURCE:-}" && ! -f "${CI_HCL_SOURCE}" ]]; then - echo "Error: CI HCL file not found at ${CI_HCL_SOURCE}" - echo "Set CI_HCL_SOURCE to a local file if you need an override." - exit 1 - fi -} - -load_ci_hcl() { - echo "--- :page_facing_up: Loading ci.hcl" - cp "${CI_HCL_SOURCE}" "${CI_HCL_PATH}" - echo "Copied ${CI_HCL_SOURCE} to ${CI_HCL_PATH}" -} - -init_bake_files() { - BAKE_FILES=(-f "${VLLM_BAKE_FILE}" -f "${CI_HCL_PATH}") - if using_custom_rocm_dockerfiles; then - # Custom stacks reuse local cache without replacing the - # standard ROCm registry caches for the same commit or branch. - BAKE_FILES+=(--set '*.cache-to=') - fi -} - -compute_ci_base_hash_if_needed() { - if [[ -z "${CI_BASE_CONTENT_FILES:-}" ]]; then - return 0 - fi - pin_base_image - if ! is_ci_base_target; then - return 0 - fi - if [[ "${REMOTE_VLLM:-0}" != "0" ]]; then - echo "Error: content-addressed ci_base builds require REMOTE_VLLM=0" >&2 - return 1 - fi - - CI_BASE_CONTENT_HASH=$(compute_ci_base_content_hash) - export CI_BASE_CONTENT_HASH - echo "ci_base content hash: ${CI_BASE_CONTENT_HASH:0:16}..." -} - -wants_stable_ci_base_tag() { - if [[ "${BUILDKITE_PULL_REQUEST:-false}" != "false" ]]; then - return 1 - fi - - if [[ "${CI_BASE_PUSH_STABLE_TAG:-}" == "1" ]]; then - return 0 - fi - if [[ "${CI_BASE_PUSH_STABLE_TAG:-}" == "0" ]]; then - return 1 - fi - - [[ "${NIGHTLY:-0}" == "1" && "${BUILDKITE_BRANCH:-}" == "${CI_BASE_STABLE_BRANCH:-main}" ]] -} - -trusted_ci_base_tip_matches_build() { - local branch="${CI_BASE_STABLE_BRANCH:-main}" - local build_commit="${BUILDKITE_COMMIT:-}" - local remote_tip="" - - is_trusted_ci_cache_writer || return 1 - if [[ ! "${build_commit}" =~ ^[0-9a-fA-F]{40}$ ]]; then - echo "Skipping ci_base stable tag: Buildkite commit is missing or invalid" >&2 - return 1 - fi - remote_tip=$(git ls-remote --exit-code "${BUILDKITE_REPO}" \ - "refs/heads/${branch}" 2>/dev/null | awk 'NR == 1 { print $1 }') - if [[ ! "${remote_tip}" =~ ^[0-9a-fA-F]{40}$ ]]; then - echo "Skipping ci_base stable tag: could not resolve remote ${branch} tip" >&2 - return 1 - fi - if [[ "${remote_tip,,}" != "${build_commit,,}" ]]; then - echo "Skipping ci_base stable tag: ${branch} advanced from ${build_commit} to ${remote_tip}" >&2 - return 1 - fi -} - -should_push_stable_ci_base_tag() { - wants_stable_ci_base_tag \ - && is_trusted_ci_cache_writer \ - && trusted_ci_base_tip_matches_build -} - -ci_base_tag_with_suffix() { - local base_tag="$1" - local suffix="$2" - - printf '%s-%s\n' "${base_tag}" "$(clean_docker_tag "${suffix}")" -} - -configure_ci_base_image_refs() { - local stable_tag="${CI_BASE_IMAGE_TAG:-rocm/vllm-dev:ci_base}" - local metadata_version="${CI_BASE_METADATA_VERSION:-${DEFAULT_CI_BASE_METADATA_VERSION}}" - local scope="${CI_BASE_WRITE_SCOPE:-}" - local trusted_content_tag="" - local content_tag="" - local build_tag="" - local primary_tag="" - - if [[ ! "${metadata_version}" =~ ^[A-Za-z0-9][A-Za-z0-9_.-]{0,15}$ ]]; then - echo "Invalid ci_base metadata version: ${metadata_version}" >&2 - return 1 - fi - - if [[ "${BUILDKITE:-false}" == "true" ]] \ - && ! is_full_git_sha "${BUILDKITE_COMMIT:-}"; then - echo "Invalid Buildkite commit for ci_base handoff: ${BUILDKITE_COMMIT:-}" >&2 - return 1 - fi - if [[ "${BUILDKITE:-false}" == "true" \ - && -z "${BUILDKITE_BUILD_ID:-}" ]]; then - echo "Buildkite build ID is required for the ci_base runtime handoff" >&2 - return 1 - fi - if [[ -n "${BUILDKITE_BUILD_ID:-}" ]]; then - build_tag=$(ci_base_tag_with_suffix \ - "${stable_tag}" "build-${BUILDKITE_BUILD_ID}") - fi - CI_BASE_IMAGE_TAG_BUILD_REF="${build_tag}" - export CI_BASE_IMAGE_TAG_BUILD_REF - - if [[ -z "${CI_BASE_CONTENT_HASH:-}" ]]; then - if is_ci_base_target; then - echo "Error: ci_base builds require a content hash" >&2 - return 1 - fi - CI_BASE_IMAGE="${CI_BASE_IMAGE:-${stable_tag}}" - export CI_BASE_IMAGE - return 0 - fi - - trusted_content_tag=$(ci_base_tag_with_suffix \ - "${stable_tag}" "v${metadata_version}-${CI_BASE_CONTENT_HASH}") - if [[ -n "${scope}" ]]; then - content_tag=$(ci_base_tag_with_suffix \ - "${stable_tag}" "v${metadata_version}-${scope}-${CI_BASE_CONTENT_HASH}") - else - content_tag="${trusted_content_tag}" - fi - CI_BASE_IMAGE_TAG_CONTENT_REF="${content_tag}" - CI_BASE_TRUSTED_CONTENT_REF="${trusted_content_tag}" - primary_tag="${build_tag:-${content_tag}}" - - # Main writes canonical content refs. Other builds import those refs - # read-only and publish into a source-scoped preview namespace. - # A fresh CI build first publishes its unique runtime tag. Content and - # stable aliases are created only after that image passes identity checks. - CI_BASE_STABLE_PROMOTION_REF="${stable_tag}" - CI_BASE_IMAGE_TAG="${primary_tag}" - export CI_BASE_IMAGE_TAG - export CI_BASE_IMAGE_TAG_CONTENT_REF - export CI_BASE_TRUSTED_CONTENT_REF - export CI_BASE_STABLE_PROMOTION_REF - - if is_ci_base_target; then - IMAGE_TAG="${primary_tag}" - CI_BASE_IMAGE="${primary_tag}" - export CI_BASE_IMAGE - export IMAGE_TAG - - echo "ci_base primary image tag: ${CI_BASE_IMAGE_TAG}" - if [[ -n "${build_tag}" ]]; then - echo "ci_base build image tag: ${build_tag}" - fi - echo "ci_base content image tag: ${content_tag}" - if wants_stable_ci_base_tag && is_trusted_ci_cache_writer; then - echo "ci_base stable alias is eligible for post-build promotion: ${stable_tag}" - else - echo "ci_base stable alias will not be pushed for this build" - echo "Set NIGHTLY=1 on ${CI_BASE_STABLE_BRANCH:-main} to refresh ${stable_tag}" - fi - return 0 - fi - - if [[ -z "${CI_BASE_IMAGE:-}" || "${CI_BASE_IMAGE}" == "${stable_tag}" ]]; then - CI_BASE_IMAGE="${content_tag}" - export CI_BASE_IMAGE - echo "Using ci_base image: ${CI_BASE_IMAGE}" - else - echo "Using provided CI_BASE_IMAGE override: ${CI_BASE_IMAGE}" - fi -} - -publish_ci_base_handoff_ref() { - local source_ref="${1:-${CI_BASE_IMAGE_TAG_BUILD_REF:-${CI_BASE_IMAGE_TAG_CONTENT_REF:-}}}" - local content_ref="${CI_BASE_IMAGE_TAG_CONTENT_REF:-}" - local digest="" - local handoff_ref="" - - is_ci_base_target || return 0 - if [[ -z "${source_ref}" || -z "${content_ref}" ]]; then - echo "Cannot publish ci_base handoff without source and content refs" >&2 - return 1 - fi - if ! digest=$(resolve_image_digest "${source_ref}"); then - echo "Could not resolve immutable ci_base handoff: ${source_ref}" >&2 - return 1 - fi - - handoff_ref="${content_ref%@*}@${digest}" - if ! confirm_remote_image_push "${handoff_ref}"; then - echo "Could not validate immutable ci_base handoff: ${handoff_ref}" >&2 - return 1 - fi - if command -v buildkite-agent >/dev/null 2>&1; then - if ! buildkite-agent meta-data set "rocm-ci-base-image" "${handoff_ref}"; then - echo "Could not publish required ci_base handoff metadata" >&2 - return 1 - fi - elif [[ "${BUILDKITE:-false}" == "true" ]]; then - echo "buildkite-agent not found; cannot publish ci_base handoff" >&2 - return 1 - fi - echo "Published immutable ci_base handoff: ${handoff_ref}" -} - -ci_base_output_refs() { - printf '%s\n' \ - "${CI_BASE_IMAGE_TAG_CONTENT_REF:-}" \ - "${CI_BASE_IMAGE_TAG_BUILD_REF:-}" \ - | awk 'NF && !seen[$0]++' -} - -ci_base_candidate_refs() { - printf '%s\n' \ - "${CI_BASE_TRUSTED_CONTENT_REF:-}" \ - "${CI_BASE_IMAGE_TAG_CONTENT_REF:-}" \ - "${CI_BASE_STABLE_PROMOTION_REF:-}" \ - | awk 'NF && !seen[$0]++' -} - -find_matching_ci_base_ref() { - local candidate="" - local candidate_digest="" - local immutable_candidate="" - - while IFS= read -r candidate; do - [[ -n "${candidate}" ]] || continue - # Missing content refs are normal after an input change. Avoid paying - # the full digest retry budget for each definitely absent candidate. - remote_image_exists "${candidate}" || continue - if ! candidate_digest=$(resolve_image_digest "${candidate}"); then - echo "Could not pin ci_base candidate: ${candidate}" >&2 - continue - fi - immutable_candidate="${candidate%@*}@${candidate_digest}" - if remote_ci_base_identity_is_current_with_retry "${immutable_candidate}"; then - printf '%s\n' "${immutable_candidate}" - return 0 - fi - done < <(ci_base_candidate_refs) - - return 1 -} - -refresh_ci_base_tags_from_ref() { - local source_ref="$1" - local source_digest="" - local immutable_source="" - local tag="" - local tag_digest="" - - if ! source_digest=$(resolve_image_digest "${source_ref}"); then - echo "Could not resolve selected ci_base image: ${source_ref}" >&2 - return 1 - fi - immutable_source="${source_ref%@*}@${source_digest}" - - while IFS= read -r tag; do - [[ -n "${tag}" ]] || continue - [[ "${tag}" != "${source_ref}" ]] || continue - tag_digest="" - if remote_image_exists "${tag}"; then - tag_digest=$(resolve_image_digest "${tag}" || true) - fi - if [[ "${tag_digest}" == "${source_digest}" ]]; then - echo "ci_base tag is already current: ${tag}" - continue - fi - echo "Updating ci_base tag ${tag} -> ${immutable_source}" - if ! docker buildx imagetools create --prefer-index=false \ - -t "${tag}" "${immutable_source}"; then - echo "Failed to update ci_base tag ${tag} from ${immutable_source}" >&2 - return 1 - fi - if ! tag_digest=$(resolve_image_digest "${tag}") \ - || [[ "${tag_digest}" != "${source_digest}" ]]; then - echo "Updated ci_base tag does not resolve to the selected digest: ${tag}" >&2 - return 1 - fi - done < <(ci_base_output_refs) -} - -promote_stable_ci_base_tag() { - local source_ref="${1:-${CI_BASE_IMAGE_TAG_BUILD_REF:-${CI_BASE_IMAGE_TAG_CONTENT_REF:-}}}" - local stable_ref="${CI_BASE_STABLE_PROMOTION_REF:-}" - local digest="" - local immutable_source="" - - is_ci_base_target || return 0 - wants_stable_ci_base_tag || return 0 - if ! should_push_stable_ci_base_tag; then - echo "Skipping ci_base stable promotion: build is not the trusted current main tip" - return 0 - fi - if [[ -z "${source_ref}" || -z "${stable_ref}" ]]; then - echo "Cannot promote ci_base stable tag without source and destination refs" >&2 - return 1 - fi - if ! digest=$(resolve_image_digest "${source_ref}"); then - echo "Could not pin ci_base source before stable promotion: ${source_ref}" >&2 - return 1 - fi - immutable_source="${source_ref%@*}@${digest}" - echo "Promoting ci_base stable tag from ${immutable_source}" - if ! docker buildx imagetools create --prefer-index=false \ - -t "${stable_ref}" "${immutable_source}"; then - echo "Failed to promote ci_base stable tag" >&2 - return 1 - fi - if ! confirm_remote_image_push "${stable_ref}"; then - echo "Promoted ci_base stable tag did not become visible: ${stable_ref}" >&2 - return 1 - fi -} - -maybe_reuse_matching_ci_base_ref() { - local matching_ref="" - - matching_ref=$(find_matching_ci_base_ref) || return 1 - - echo "Found existing ci_base image with matching content hash: ${matching_ref}" - if ! refresh_ci_base_tags_from_ref "${matching_ref}"; then - echo "ci_base tag refresh failed after finding an exact image; aborting" >&2 - return 2 - fi - if ! promote_stable_ci_base_tag "${matching_ref}"; then - echo "ci_base stable promotion failed after finding an exact image; aborting" >&2 - return 2 - fi - if ! publish_ci_base_handoff_ref "${matching_ref}"; then - echo "ci_base handoff failed after finding an exact image; aborting" >&2 - return 2 - fi - echo "Content hashes match -- ci_base is current" - return 0 -} - -maybe_skip_existing_image() { - local remote_revision="" - local reuse_status=0 - - if [[ -z "${IMAGE_TAG:-}" ]]; then - return 0 - fi - - if [[ "${FORCE_BUILD:-0}" == "1" ]]; then - echo "FORCE_BUILD=1 set; skipping existing-image check" - return 0 - fi - if ! is_ci_base_target \ - && { should_upload_wheel_artifacts || should_export_rocm_smoke \ - || [[ "${TARGET}" == "kernel-symbol-map-rocm" ]]; }; then - echo "Local-output targets always run for the current build" - return 0 - fi - - echo "--- :mag: Checking image tag" - echo "Image tag: ${IMAGE_TAG}" - - if is_ci_base_target && [[ -n "${CI_BASE_CONTENT_HASH:-}" ]]; then - maybe_reuse_matching_ci_base_ref || reuse_status=$? - case "${reuse_status}" in - 0) - echo "Skipping build" - exit 0 - ;; - 1) - echo "No current ci_base image matched the expected content hash" - echo "Proceeding with build" - return 0 - ;; - *) - return "${reuse_status}" - ;; - esac - fi - - if ! remote_image_exists "${IMAGE_TAG}"; then - echo "Image not found, proceeding with build" - return 0 - fi - - if is_ci_base_target; then - echo "ci_base image already exists and no content hash was configured" - echo "Skipping build" - exit 0 - fi - - if is_commit_image_target; then - remote_revision=$(get_remote_image_label "${IMAGE_TAG}" "org.opencontainers.image.revision") - if [[ -n "${remote_revision}" && "${remote_revision}" != "${BUILDKITE_COMMIT}" ]]; then - echo "Existing image revision does not match ${BUILDKITE_COMMIT}" - echo " found revision: ${remote_revision}" - echo "Rebuilding image" - return 0 - fi - - echo "Commit image already exists: ${IMAGE_TAG}" - echo "Skipping build" - export_kernel_symbol_map_from_cache - exit 0 - fi - - echo "Image already exists: ${IMAGE_TAG}" - echo "Skipping build" - export_kernel_symbol_map_from_cache - exit 0 -} - -setup_builder() { - echo "--- :buildkite: Setting up buildx builder" - - local setup_mode="${ROCM_SETUP_BUILDX_BUILDER:-auto}" - local current_driver="" - local named_driver="" - - if [[ "${setup_mode}" == "0" || "${setup_mode}" == "false" ]]; then - echo "Using current Docker buildx builder" - echo "ROCM_SETUP_BUILDX_BUILDER=${setup_mode}; cache exporters may fail if the driver is docker" - docker buildx inspect --bootstrap - echo "Active builder:" - docker buildx ls | grep -E '^\*|^NAME' || docker buildx ls - return 0 - fi - - current_driver=$(buildx_driver || true) - if [[ "${setup_mode}" != "1" ]] && builder_supports_registry_cache "${current_driver}"; then - echo "Using current Docker buildx builder with ${current_driver} driver" - docker buildx inspect --bootstrap - echo "Active builder:" - docker buildx ls | grep -E '^\*|^NAME' || docker buildx ls - return 0 - fi - - if [[ "${setup_mode}" != "1" ]]; then - echo "Current buildx driver '${current_driver:-unknown}' cannot export registry caches" - echo "Creating or using a cache-capable builder: ${BUILDER_NAME}" - fi - - if docker buildx inspect "${BUILDER_NAME}" >/dev/null 2>&1; then - named_driver=$(buildx_driver "${BUILDER_NAME}" || true) - if ! builder_supports_registry_cache "${named_driver}"; then - echo "Builder '${BUILDER_NAME}' uses ${named_driver:-unknown} driver; using ${BUILDER_NAME}-cache instead" - BUILDER_NAME="${BUILDER_NAME}-cache" - fi - fi - - if [[ -S "${BUILDKIT_SOCKET}" ]]; then - echo "Found local buildkitd socket at ${BUILDKIT_SOCKET}" - echo "Using remote driver to connect to buildkitd" - - if docker buildx inspect "${BUILDER_NAME}" >/dev/null 2>&1; then - use_existing_builder - else - create_and_bootstrap_builder remote "unix://${BUILDKIT_SOCKET}" - fi - elif docker buildx inspect "${BUILDER_NAME}" >/dev/null 2>&1; then - use_existing_builder - else - echo "No local buildkitd found, using docker-container driver" - create_and_bootstrap_builder docker-container - fi - - echo "Active builder:" - docker buildx ls | grep -E '^\*|^NAME' || docker buildx ls -} - -validate_cache_branch_tag() { - local name="$1" - local value="$2" - - if [[ -n "${value}" \ - && (! "${value}" =~ ^[A-Za-z0-9_][A-Za-z0-9_.-]*$ \ - || ${#value} -gt ${ROCM_CACHE_BRANCH_TAG_MAX_LEN}) ]]; then - echo "Invalid ${name}; expected a Docker tag component of at most ${ROCM_CACHE_BRANCH_TAG_MAX_LEN} characters" >&2 - return 1 - fi -} - -prepare_git_cache_metadata() { - local cache_branch_name="" - local cache_base_branch="${BUILDKITE_PULL_REQUEST_BASE_BRANCH:-main}" - local target_repo_slug="" - local target_repo_url="" - local merge_base_ref="" - - if is_ci_base_target; then - echo "Skipping commit-cache ancestry lookup for content-addressed ci_base" - return 0 - fi - - if [[ -z "${PARENT_COMMIT:-}" || -z "${VLLM_MERGE_BASE_COMMIT:-}" ]] \ - && git rev-parse --is-shallow-repository 2>/dev/null | grep -q "true"; then - echo "Shallow clone detected - deepening for cache key computation" - git_fetch_for_cache --filter=tree:0 --no-tags --deepen=1 \ - origin "$(git rev-parse HEAD)" || true - fi - - if [[ -z "${PARENT_COMMIT:-}" ]]; then - PARENT_COMMIT=$(git rev-parse HEAD~1 2>/dev/null || echo "") - if [[ -n "${PARENT_COMMIT}" ]]; then - export PARENT_COMMIT - echo "Computed parent commit for cache fallback: ${PARENT_COMMIT}" - else - echo "Could not determine parent commit" - fi - else - echo "Using provided PARENT_COMMIT: ${PARENT_COMMIT}" - fi - - if [[ -z "${ROCM_CACHE_BRANCH_TAG:-}" ]]; then - cache_branch_name=$(select_cache_branch_name) - if [[ -z "${cache_branch_name}" && "${BUILDKITE_PULL_REQUEST:-false}" != "false" ]]; then - cache_branch_name="pr-${BUILDKITE_PULL_REQUEST}" - echo "Using pull request number for ROCm branch cache tag: ${cache_branch_name}" - fi - fi - - if [[ -z "${ROCM_CACHE_BRANCH_TAG:-}" && -n "${cache_branch_name}" ]]; then - ROCM_CACHE_BRANCH_TAG=$( - compose_cache_branch_tag "$(get_buildkite_repo_slug)" "${cache_branch_name}" - ) - export ROCM_CACHE_BRANCH_TAG - echo "Computed ROCm branch cache tag: ${ROCM_CACHE_BRANCH_TAG} (from ${cache_branch_name})" - elif [[ -n "${ROCM_CACHE_BRANCH_TAG:-}" ]]; then - echo "Using provided ROCM_CACHE_BRANCH_TAG: ${ROCM_CACHE_BRANCH_TAG}" - elif [[ -n "${BUILDKITE_BRANCH:-}" ]]; then - echo "Skipping ROCm branch cache tag: no usable branch name found" - echo " BUILDKITE_BRANCH=${BUILDKITE_BRANCH}" - fi - - if [[ -z "${ROCM_CACHE_UPSTREAM_BRANCH_TAG:-}" \ - && -n "${BUILDKITE_PULL_REQUEST_BASE_BRANCH:-}" \ - && "${BUILDKITE_PULL_REQUEST:-false}" != "false" ]]; then - target_repo_slug=$(get_buildkite_target_repo_slug) - ROCM_CACHE_UPSTREAM_BRANCH_TAG=$( - compose_cache_branch_tag "${target_repo_slug}" "${BUILDKITE_PULL_REQUEST_BASE_BRANCH}" - ) - export ROCM_CACHE_UPSTREAM_BRANCH_TAG - echo "Computed ROCm upstream branch cache tag: ${ROCM_CACHE_UPSTREAM_BRANCH_TAG}" - elif [[ -n "${ROCM_CACHE_UPSTREAM_BRANCH_TAG:-}" ]]; then - echo "Using provided ROCM_CACHE_UPSTREAM_BRANCH_TAG: ${ROCM_CACHE_UPSTREAM_BRANCH_TAG}" - fi - - validate_cache_branch_tag \ - ROCM_CACHE_BRANCH_TAG "${ROCM_CACHE_BRANCH_TAG:-}" - validate_cache_branch_tag \ - ROCM_CACHE_UPSTREAM_BRANCH_TAG "${ROCM_CACHE_UPSTREAM_BRANCH_TAG:-}" - - if [[ -z "${VLLM_MERGE_BASE_COMMIT:-}" ]]; then - target_repo_url=$(get_buildkite_target_repo_url) - merge_base_ref="refs/remotes/vllm-cache-upstream/${cache_base_branch}" - git_fetch_for_cache --no-tags --depth=200 "${target_repo_url}" \ - "+refs/heads/${cache_base_branch}:${merge_base_ref}" 2>/dev/null || true - VLLM_MERGE_BASE_COMMIT=$(git merge-base HEAD "${merge_base_ref}" 2>/dev/null || echo "") - if [[ -z "${VLLM_MERGE_BASE_COMMIT}" ]]; then - git_fetch_for_cache --no-tags --deepen=1000 "${target_repo_url}" \ - "+refs/heads/${cache_base_branch}:${merge_base_ref}" 2>/dev/null || true - VLLM_MERGE_BASE_COMMIT=$(git merge-base HEAD "${merge_base_ref}" 2>/dev/null || echo "") - fi - if [[ -n "${VLLM_MERGE_BASE_COMMIT}" ]]; then - export VLLM_MERGE_BASE_COMMIT - echo "Computed merge base commit for cache fallback: ${VLLM_MERGE_BASE_COMMIT}" - else - echo "Could not determine merge base with ${cache_base_branch}" - fi - else - echo "Using provided VLLM_MERGE_BASE_COMMIT: ${VLLM_MERGE_BASE_COMMIT}" - fi -} - -ci_base_metadata_pairs() { - local dockerfile="${CI_BASE_DOCKERFILE:-${DEFAULT_CI_BASE_DOCKERFILE}}" - local stages="${CI_BASE_DOCKERFILE_STAGES:-${DEFAULT_CI_BASE_DOCKERFILE_STAGES}}" - local content_files="${CI_BASE_CONTENT_FILES:-${DEFAULT_CI_BASE_CONTENT_FILES}}" - local content_files_hash="" - local base_image="" - local base_image_digest="" - local -a content_paths=() - local -a content_args=() - - read -r -a content_paths <<< "${content_files}" - if [[ ${#content_paths[@]} -gt 0 ]]; then - if ! content_files_hash=$(compute_content_hash "${content_paths[@]}"); then - echo "Failed to hash ci_base metadata content files" >&2 - return 1 - fi - fi - mapfile -t content_args < <( - get_content_arg_names "${dockerfile}" "${stages}" "${CI_BASE_CONTENT_ARGS:-}" - ) - - base_image=$(resolve_dockerfile_arg_value "${dockerfile}" "BASE_IMAGE") - if [[ -n "${base_image}" ]]; then - if ! base_image_digest=$(resolve_image_digest "${base_image}"); then - echo "Failed to resolve ci_base metadata digest for ${base_image}" >&2 - return 1 - fi - fi - - metadata_pair "vllm.ci_base.metadata_version" "${CI_BASE_METADATA_VERSION:-${DEFAULT_CI_BASE_METADATA_VERSION}}" - metadata_pair "vllm.ci_base.content_hash" "${CI_BASE_CONTENT_HASH:-}" - metadata_pair "vllm.ci_base.content_files_hash" "${content_files_hash}" - metadata_pair "vllm.ci_base.content_files" "${content_files}" - metadata_pair "vllm.ci_base.content_args" "$(join_words "${content_args[@]}")" - metadata_pair "vllm.ci_base.dockerfile" "${dockerfile}" - metadata_pair "vllm.ci_base.dockerfile_stages" "${stages}" - - # The parent identity is digest-only: mutable aliases that resolve to the - # same image must produce byte-identical canonical metadata. - metadata_pair "vllm.rocm.base_image" "${base_image_digest}" - metadata_pair "vllm.rocm.base_image_digest" "${base_image_digest}" - metadata_pair "vllm.rocm.pytorch_rocm_arch" "${PYTORCH_ROCM_ARCH:-}" - metadata_pair "vllm.rocm.nic_backend" "$(resolve_dockerfile_arg_value "${dockerfile}" "NIC_BACKEND")" - metadata_pair "vllm.rocm.ainic_version" "$(resolve_dockerfile_arg_value "${dockerfile}" "AINIC_VERSION")" - metadata_pair "vllm.rocm.ubuntu_codename" "$(resolve_dockerfile_arg_value "${dockerfile}" "UBUNTU_CODENAME")" - metadata_pair "vllm.rocm.max_jobs" "$(resolve_dockerfile_arg_value "${dockerfile}" "max_jobs")" - metadata_pair "vllm.rocm.nixl_repo" "$(resolve_dockerfile_arg_value "${dockerfile}" "NIXL_REPO")" - metadata_pair "vllm.rocm.nixl_commit" "${NIXL_BRANCH:-$(resolve_dockerfile_arg_value "${dockerfile}" "NIXL_BRANCH")}" - metadata_pair "vllm.rocm.ucx_repo" "$(resolve_dockerfile_arg_value "${dockerfile}" "UCX_REPO")" - metadata_pair "vllm.rocm.ucx_commit" "${UCX_BRANCH:-$(resolve_dockerfile_arg_value "${dockerfile}" "UCX_BRANCH")}" - metadata_pair "vllm.rocm.deepep_repo" "$(resolve_dockerfile_arg_value "${dockerfile}" "DEEPEP_REPO")" - metadata_pair "vllm.rocm.deepep_commit" "${DEEPEP_BRANCH:-$(resolve_dockerfile_arg_value "${dockerfile}" "DEEPEP_BRANCH")}" - metadata_pair "vllm.rocm.deepep_nic" "$(resolve_dockerfile_arg_value "${dockerfile}" "DEEPEP_NIC")" - metadata_pair "vllm.rocm.deepep_rocm_arch" "$(resolve_dockerfile_arg_value "${dockerfile}" "DEEPEP_ROCM_ARCH")" - metadata_pair "vllm.rocm.nixl_cache_key" "${NIXL_CACHE_KEY:-}" - metadata_pair "vllm.rocm.deepep_cache_key" "${DEEPEP_CACHE_KEY:-}" -} - -write_ci_base_metadata_annotations() { - local metadata="$1" - local key="" - local value="" - - [[ -n "${metadata}" ]] || return 0 - while IFS=$'\t' read -r key value; do - [[ -n "${key}" && -n "${value}" ]] || continue - printf ' "%s",\n' \ - "$(hcl_escape_string "manifest:${key}=${value}")" - done <<< "${metadata}" -} - -write_ci_base_metadata_labels() { - local metadata="$1" - local key="" - local value="" - - [[ -n "${metadata}" ]] || return 0 - while IFS=$'\t' read -r key value; do - [[ -n "${key}" && -n "${value}" ]] || continue - printf ' "%s" = "%s"\n' \ - "$(hcl_escape_string "${key}")" \ - "$(hcl_escape_string "${value}")" - done <<< "${metadata}" -} - -write_ci_base_label_override() { - local target_name="" - local metadata="" - local -a ci_base_targets=() - - if [[ -z "${CI_BASE_CONTENT_HASH:-}" ]]; then - return 0 - fi - - mapfile -t ci_base_targets < <( - { - printf '%s\n' "ci-base-rocm" - sed -n -E 's/^target "(ci-base-rocm[^"]+)".*/\1/p' "${CI_HCL_PATH}" 2>/dev/null || true - } | awk '!seen[$0]++' - ) - - if [[ ${#ci_base_targets[@]} -eq 0 ]]; then - return 0 - fi - - metadata=$(ci_base_metadata_pairs) - - : > "${CI_BASE_LABEL_OVERRIDE_PATH}" - for target_name in "${ci_base_targets[@]}"; do - cat >> "${CI_BASE_LABEL_OVERRIDE_PATH}" <> "${CI_BASE_LABEL_OVERRIDE_PATH}" - cat >> "${CI_BASE_LABEL_OVERRIDE_PATH}" <> "${CI_BASE_LABEL_OVERRIDE_PATH}" - cat >> "${CI_BASE_LABEL_OVERRIDE_PATH}" < "${ROCM_ARG_OVERRIDE_PATH}" - - BAKE_FILES+=(-f "${ROCM_ARG_OVERRIDE_PATH}") - echo "Appended resolved ROCm Docker ARG override" -} - -write_hcl_string_list_attr() { - local indent="$1" - local attr="$2" - shift 2 - - printf '%s%s = [\n' "${indent}" "${attr}" - write_hcl_string_list_entries "${indent} " "$@" - printf '%s]\n' "${indent}" -} - -validate_cache_export_mode() { - local mode="$1" - local env_name="$2" - - case "${mode}" in - min|max) - ;; - *) - echo "Error: ${env_name} must be one of: min, max" - exit 1 - ;; - esac -} - -validate_content_cache_export_mode() { - local mode="$1" - local env_name="$2" - - case "${mode}" in - missing|always|never) - ;; - *) - echo "Error: ${env_name} must be one of: missing, always, never" - exit 1 - ;; - esac -} - -should_export_content_cache_ref() { - local cache_ref="$1" - local cache_name="$2" - local trusted_ref="${3:-${cache_ref}}" - local mode="${ROCM_CONTENT_CACHE_EXPORT_MODE:-missing}" - - case "${mode}" in - always) - echo "${cache_name} content cache export mode is always" - return 0 - ;; - never) - echo "${cache_name} content cache export mode is never" - return 1 - ;; - missing|"") - if registry_ref_exists_with_retry imagetools "${trusted_ref}"; then - echo "${cache_name} trusted content cache is visible: ${trusted_ref}" - return 1 - fi - if [[ "${cache_ref}" != "${trusted_ref}" ]] \ - && registry_ref_exists_with_retry imagetools "${cache_ref}"; then - echo "${cache_name} scoped content cache is visible: ${cache_ref}" - return 1 - fi - echo "${cache_name} content cache is missing: ${cache_ref}" - return 0 - ;; - *) - echo "Error: ROCM_CONTENT_CACHE_EXPORT_MODE must be one of: missing, always, never" - exit 1 - ;; - esac -} - -write_rocm_cache_override() { - local cache_repo="${DOCKERHUB_CACHE_REPO:-rocm/vllm-ci-cache}" - local content_cache_export_mode="${ROCM_CONTENT_CACHE_EXPORT_MODE:-missing}" - local csrc_cache_to_mode="${ROCM_CSRC_CACHE_TO_MODE:-max}" - local rust_cache_to_mode="${ROCM_RUST_CACHE_TO_MODE:-max}" - local rocm_cache_to_mode="${ROCM_FINAL_CACHE_TO_MODE:-min}" - local -a csrc_content_cache_from=() - local -a rust_content_cache_from=() - local -a combined_content_cache_from=() - local -a csrc_cache_to=() - local -a rust_cache_to=() - local -a rocm_cache_to=() - local -a export_wheel_cache_to=() - local export_csrc_cache=1 - local export_rust_cache=1 - - if ! uses_rocm_csrc_cache && ! uses_rocm_rust_cache; then - return 0 - fi - - validate_content_cache_export_mode \ - "${content_cache_export_mode}" \ - "ROCM_CONTENT_CACHE_EXPORT_MODE" - validate_cache_export_mode "${csrc_cache_to_mode}" "ROCM_CSRC_CACHE_TO_MODE" - validate_cache_export_mode "${rust_cache_to_mode}" "ROCM_RUST_CACHE_TO_MODE" - validate_cache_export_mode "${rocm_cache_to_mode}" "ROCM_FINAL_CACHE_TO_MODE" - echo "ROCm content cache export mode: ${content_cache_export_mode}" - echo "ROCm csrc cache export mode: ${csrc_cache_to_mode}" - echo "ROCm Rust fallback cache export mode: ${rust_cache_to_mode}" - echo "ROCm final image cache export mode: ${rocm_cache_to_mode}" - - if [[ -n "${ROCM_CSRC_CONTENT_CACHE_REF:-}" ]]; then - csrc_content_cache_from+=( - "type=registry,ref=${ROCM_CSRC_TRUSTED_CONTENT_CACHE_REF}" - ) - if [[ "${ROCM_CSRC_CONTENT_CACHE_REF}" != \ - "${ROCM_CSRC_TRUSTED_CONTENT_CACHE_REF}" ]]; then - csrc_content_cache_from+=( - "type=registry,ref=${ROCM_CSRC_CONTENT_CACHE_REF}" - ) - fi - if should_export_content_cache_ref \ - "${ROCM_CSRC_CONTENT_CACHE_REF}" "ROCm csrc" \ - "${ROCM_CSRC_TRUSTED_CONTENT_CACHE_REF}"; then - csrc_cache_to+=( - "type=registry,ref=${ROCM_CSRC_CONTENT_CACHE_REF},mode=${csrc_cache_to_mode},ignore-error=true" - ) - else - export_csrc_cache=0 - fi - fi - - if [[ -n "${ROCM_RUST_CONTENT_CACHE_REF:-}" ]]; then - rust_content_cache_from+=( - "type=registry,ref=${ROCM_RUST_TRUSTED_CONTENT_CACHE_REF}" - ) - if [[ "${ROCM_RUST_CONTENT_CACHE_REF}" != \ - "${ROCM_RUST_TRUSTED_CONTENT_CACHE_REF}" ]]; then - rust_content_cache_from+=( - "type=registry,ref=${ROCM_RUST_CONTENT_CACHE_REF}" - ) - fi - # Legacy commit/branch exports are only needed while the exact-input - # content ref is absent. The exact ref itself is refreshed below. - if ! should_export_content_cache_ref \ - "${ROCM_RUST_CONTENT_CACHE_REF}" "ROCm Rust" \ - "${ROCM_RUST_TRUSTED_CONTENT_CACHE_REF}"; then - export_rust_cache=0 - fi - if [[ "${content_cache_export_mode}" != "never" ]]; then - # Refresh the exact-input ref in the original solve regardless of - # which local or remote cache supplied the Rust result. - rust_cache_to+=( - "type=registry,ref=${ROCM_RUST_CONTENT_CACHE_REF},mode=min,ignore-error=true" - ) - echo "ROCm Rust exact-input cache will be refreshed (mode=min): ${ROCM_RUST_CONTENT_CACHE_REF}" - fi - fi - - combined_content_cache_from=("${csrc_content_cache_from[@]}" "${rust_content_cache_from[@]}") - - # Docker Hub cache exports are best-effort. A cache-only target failure can - # otherwise cancel the sibling image target before its manifest is pushed. - if [[ -n "${BUILDKITE_COMMIT:-}" ]]; then - if [[ ${export_csrc_cache} -eq 1 ]]; then - csrc_cache_to+=( - "type=registry,ref=${cache_repo}:csrc-rocm-${BUILDKITE_COMMIT},mode=${csrc_cache_to_mode},ignore-error=true" - ) - fi - if [[ ${export_rust_cache} -eq 1 ]]; then - rust_cache_to+=( - "type=registry,ref=${cache_repo}:rust-rocm-${BUILDKITE_COMMIT},mode=${rust_cache_to_mode},ignore-error=true" - ) - fi - rocm_cache_to+=( - "type=registry,ref=${cache_repo}:rocm-${BUILDKITE_COMMIT},mode=${rocm_cache_to_mode},ignore-error=true" - ) - fi - - if [[ -n "${ROCM_CACHE_BRANCH_TAG:-}" ]]; then - if [[ ${export_csrc_cache} -eq 1 ]]; then - csrc_cache_to+=( - "type=registry,ref=${cache_repo}:csrc-rocm-branch-${ROCM_CACHE_BRANCH_TAG},mode=${csrc_cache_to_mode},ignore-error=true" - ) - fi - if [[ ${export_rust_cache} -eq 1 ]]; then - rust_cache_to+=( - "type=registry,ref=${cache_repo}:rust-rocm-branch-${ROCM_CACHE_BRANCH_TAG},mode=${rust_cache_to_mode},ignore-error=true" - ) - fi - rocm_cache_to+=( - "type=registry,ref=${cache_repo}:rocm-branch-${ROCM_CACHE_BRANCH_TAG},mode=${rocm_cache_to_mode},ignore-error=true" - ) - fi - - # Standalone image/wheel targets reach rust-build but not the cache-only - # exporter unless it is requested explicitly. - if ((${#rust_cache_to[@]} > 0)); then - case "${TARGET}" in - test-rocm-ci|export-wheel-rocm|smoke-test-rocm-ci) - BAKE_TARGETS=("rust-rocm-ci" "${BAKE_TARGETS[@]}") - ;; - esac - fi - - if [[ "${TARGET}" == "test-rocm-ci-with-wheel" ]]; then - export_wheel_cache_to=() - else - export_wheel_cache_to=("${rocm_cache_to[@]}") - fi - - { - cat < "${CSRC_CACHE_OVERRIDE_PATH}" - - BAKE_FILES+=(-f "${CSRC_CACHE_OVERRIDE_PATH}") - echo "Appended ROCm cache override with non-fatal registry exports" -} - -extract_dependency_pins() { - local bake_dir="" - local dockerfile_rocm="" - local physical_dockerfile="" - local var="" - local val="" - - bake_dir=$(dirname "${VLLM_BAKE_FILE}") - dockerfile_rocm="${CI_BASE_DOCKERFILE:-${bake_dir}/Dockerfile.rocm}" - physical_dockerfile="${dockerfile_rocm}" - if [[ -n "${ROCM_BUILD_CONTEXT_ROOT:-}" && "${dockerfile_rocm}" != /* ]]; then - physical_dockerfile="${ROCM_BUILD_CONTEXT_ROOT}/${dockerfile_rocm}" - fi - if [[ ! -f "${physical_dockerfile}" ]]; then - return 0 - fi - - for var in NIXL_BRANCH UCX_BRANCH DEEPEP_BRANCH; do - if [[ -n "${!var:-}" ]]; then - echo "Using provided ${var}: ${!var}" - continue - fi - - val=$( - sed -n -E "s/^[[:space:]]*[Aa][Rr][Gg][[:space:]]+${var}=\"?([^\"[:space:]]+)\"?.*/\\1/p" \ - "${physical_dockerfile}" | head -1 - ) - if [[ -n "${val}" ]]; then - export "${var}=${val}" - echo "Extracted ${var}=${val} from ${dockerfile_rocm}" - fi - done -} - -compute_dependency_cache_keys() { - local bake_dir="" - local dockerfile_rocm="" - local nixl_branch="" - local ucx_branch="" - local deepep_branch="" - local nixl_material="" - local deepep_material="" - - bake_dir=$(dirname "${VLLM_BAKE_FILE}") - dockerfile_rocm="${CI_BASE_DOCKERFILE:-${bake_dir}/Dockerfile.rocm}" - nixl_branch=$(resolve_dockerfile_arg_value "${dockerfile_rocm}" "NIXL_BRANCH") - ucx_branch=$(resolve_dockerfile_arg_value "${dockerfile_rocm}" "UCX_BRANCH") - deepep_branch=$(resolve_dockerfile_arg_value "${dockerfile_rocm}" "DEEPEP_BRANCH") - - if [[ -n "${nixl_branch}" && -n "${ucx_branch}" ]]; then - nixl_material=$(compose_stage_cache_material "${dockerfile_rocm}" "base build_nixl") - NIXL_CACHE_KEY=$( - compose_dependency_cache_key \ - "${nixl_branch}-ucx-${ucx_branch}" \ - "${nixl_material}" - ) - export NIXL_CACHE_KEY - echo "NIXL dependency cache key: ${NIXL_CACHE_KEY}" - fi - - if [[ -n "${deepep_branch}" ]]; then - deepep_material=$(compose_stage_cache_material "${dockerfile_rocm}" "base build_deepep") - DEEPEP_CACHE_KEY=$( - compose_dependency_cache_key \ - "${deepep_branch}" \ - "${deepep_material}" - ) - export DEEPEP_CACHE_KEY - echo "DeepEP dependency cache key: ${DEEPEP_CACHE_KEY}" - fi -} - -compose_stage_cache_material() { - local dockerfile="$1" - local stages="$2" - local -a content_args=() - - mapfile -t content_args < <(get_content_arg_names "${dockerfile}" "${stages}" "") - { - printf 'dockerfile:%s\n' "${dockerfile}" - printf 'dockerfile-stages:%s\n' "${stages}" - hash_dockerfile_stages "${dockerfile}" "${stages}" - printf 'resolved-build-args:\n' - hash_dockerfile_arg_values "${dockerfile}" "${content_args[@]}" - } -} - -dependency_cache_ref_exists() { - local cache_ref="$1" - registry_ref_exists_with_retry imagetools "${cache_ref}" -} - -dependency_cache_ref_for_target() { - local target="$1" - local cache_repo="${DOCKERHUB_CACHE_REPO:-rocm/vllm-ci-cache}" - - case "${target}" in - nixl-rocm-ci) - if [[ -n "${NIXL_CACHE_KEY:-}" ]]; then - printf '%s\n' "${cache_repo}:nixl-rocm-${NIXL_CACHE_KEY}" - elif [[ -n "${NIXL_BRANCH:-}" ]]; then - printf '%s\n' "${cache_repo}:nixl-rocm-${NIXL_BRANCH}-ucx-${UCX_BRANCH:-}" - fi - ;; - deepep-rocm-ci) - if [[ -n "${DEEPEP_CACHE_KEY:-}" ]]; then - printf '%s\n' "${cache_repo}:deepep-rocm-${DEEPEP_CACHE_KEY}" - elif [[ -n "${DEEPEP_BRANCH:-}" ]]; then - printf '%s\n' "${cache_repo}:deepep-rocm-${DEEPEP_BRANCH}" - fi - ;; - esac -} - -add_dependency_cache_target() { - local target="$1" - - if printf '%s\n' "${DEPENDENCY_CACHE_TARGETS[@]}" | grep -qx "${target}"; then - return 0 - fi - DEPENDENCY_CACHE_TARGETS+=("${target}") -} - -resolve_ci_base_dependency_targets() { - local mode="${ROCM_DEP_CACHE_EXPORT_MODE:-missing}" - local nixl_ref="" - local deepep_ref="" - - [[ "${TARGET}" == "ci-base-rocm-ci-with-deps" ]] || return 0 - - case "${mode}" in - always) - echo "ROCM_DEP_CACHE_EXPORT_MODE=always; exporting all dependency caches serially" - for target in nixl-rocm-ci deepep-rocm-ci; do - if [[ -n "$(dependency_cache_ref_for_target "${target}")" ]]; then - add_dependency_cache_target "${target}" - fi - done - ;; - never) - BAKE_TARGETS=("ci-base-rocm-ci") - DEPENDENCY_CACHE_TARGETS=() - echo "ROCM_DEP_CACHE_EXPORT_MODE=never; building ci_base without dependency cache exports" - return 0 - ;; - missing|"") - ;; - *) - echo "Error: ROCM_DEP_CACHE_EXPORT_MODE must be one of: missing, always, never" - exit 1 - ;; - esac - - if [[ "${mode}" != "always" && -n "${NIXL_CACHE_KEY:-}" ]]; then - nixl_ref=$(dependency_cache_ref_for_target "nixl-rocm-ci") - if dependency_cache_ref_exists "${nixl_ref}"; then - echo "NIXL dependency cache exists: ${nixl_ref}" - else - echo "NIXL dependency cache missing; will seed: ${nixl_ref}" - add_dependency_cache_target "nixl-rocm-ci" - fi - fi - - if [[ "${mode}" != "always" && -n "${DEEPEP_CACHE_KEY:-}" ]]; then - deepep_ref=$(dependency_cache_ref_for_target "deepep-rocm-ci") - if dependency_cache_ref_exists "${deepep_ref}"; then - echo "DeepEP dependency cache exists: ${deepep_ref}" - else - echo "DeepEP dependency cache missing; will seed: ${deepep_ref}" - add_dependency_cache_target "deepep-rocm-ci" - fi - fi - - BAKE_TARGETS=("ci-base-rocm-ci") - if [[ ${#DEPENDENCY_CACHE_TARGETS[@]} -eq 0 ]]; then - echo "All dependency caches exist; building ci_base without dependency cache exports" - else - echo "Resolved dependency cache seed targets: ${DEPENDENCY_CACHE_TARGETS[*]}" - echo "Resolved ci_base bake targets: ${BAKE_TARGETS[*]}" - fi -} - -bake_config_targets() { - printf '%s\n' "${DEPENDENCY_CACHE_TARGETS[@]}" "${BAKE_TARGETS[@]}" \ - | awk 'NF && !seen[$0]++' -} - -print_bake_config() { - local -a print_targets=() - - echo "--- :page_facing_up: Resolved bake configuration" - mapfile -t print_targets < <(bake_config_targets) - docker buildx bake "${BAKE_ALLOW_ARGS[@]}" \ - "${BAKE_FILES[@]}" --print "${print_targets[@]}" | tee "${BAKE_CONFIG_FILE}" - - if command -v buildkite-agent >/dev/null 2>&1 && [[ -n "${BUILDKITE_BUILD_NUMBER:-}" ]]; then - buildkite-agent artifact upload "${BAKE_CONFIG_FILE}" || true - echo "Uploaded ${BAKE_CONFIG_FILE} as Buildkite artifact" - else - echo "Saved bake config to ${BAKE_CONFIG_FILE} (not in Buildkite, skipping upload)" - fi -} - -confirm_remote_image_push() { - local image_ref="$1" - - if [[ -z "${CI_BASE_CONTENT_HASH:-}" ]]; then - remote_image_exists "${image_ref}" - return - fi - - if remote_ci_base_identity_is_current_with_retry "${image_ref}"; then - return 0 - fi - - echo "Remote image does not have the expected complete ci_base identity." - return 1 -} - -verify_dependency_cache_ref() { - local cache_ref="$1" - local attempts="${ROCM_DEP_CACHE_VERIFY_ATTEMPTS:-6}" - local delay_secs="${ROCM_DEP_CACHE_VERIFY_DELAY:-5}" - local attempt - - for ((attempt = 1; attempt <= attempts; attempt++)); do - if dependency_cache_ref_exists "${cache_ref}"; then - echo "Dependency cache confirmed: ${cache_ref}" - return 0 - fi - if [[ ${attempt} -lt ${attempts} ]]; then - echo "Dependency cache not visible yet (${attempt}/${attempts}): ${cache_ref}" - sleep "${delay_secs}" - fi - done - - echo "ERROR: dependency cache was not confirmed after upload: ${cache_ref}" - return 1 -} - -seed_dependency_caches_if_needed() { - local target="" - local cache_ref="" - - if [[ "${TARGET}" != "ci-base-rocm-ci-with-deps" ]]; then - return 0 - fi - if [[ ${#DEPENDENCY_CACHE_TARGETS[@]} -eq 0 ]]; then - return 0 - fi - - echo "--- :docker: Seeding ROCm dependency caches" - echo "Dependency cache uploads are required for this build." - echo "Seeding serially to avoid concurrent Docker Hub cache exporters." - - for target in "${DEPENDENCY_CACHE_TARGETS[@]}"; do - cache_ref=$(dependency_cache_ref_for_target "${target}") - if [[ -z "${cache_ref}" ]]; then - echo "ERROR: could not resolve dependency cache ref for ${target}" - return 1 - fi - - echo "--- :docker: Seeding ${target}" - echo "Expected cache ref: ${cache_ref}" - docker buildx bake \ - "${BAKE_ALLOW_ARGS[@]}" \ - "${BAKE_FILES[@]}" \ - --progress "${BUILDKIT_PROGRESS:-plain}" \ - "${target}" - verify_dependency_cache_ref "${cache_ref}" - done -} - -run_bake() { - local confirmation_ref="${IMAGE_TAG:-}" - - if is_ci_base_target && [[ -n "${CI_BASE_IMAGE_TAG_BUILD_REF:-}" ]]; then - confirmation_ref="${CI_BASE_IMAGE_TAG_BUILD_REF}" - fi - - echo "--- :docker: Building ${TARGET}" - docker buildx bake \ - "${BAKE_ALLOW_ARGS[@]}" \ - "${BAKE_FILES[@]}" \ - --progress "${BUILDKIT_PROGRESS:-plain}" \ - "${BAKE_TARGETS[@]}" - - if is_ci_base_target; then - if ! confirm_remote_image_push "${confirmation_ref}"; then - echo "Fresh ci_base image failed identity validation: ${confirmation_ref}" >&2 - return 1 - fi - refresh_ci_base_tags_from_ref "${confirmation_ref}" - fi - echo "--- :white_check_mark: Build complete" -} - -upload_wheel_artifacts_if_present() { - local wheel_dir="./wheel-export" - local artifact_dir="artifacts/vllm-rocm-install" - local archive_name="vllm-rocm-install.tar.gz" - local metadata_dir="${wheel_dir}/.vllm-ci-artifact" - local build_base_digest="" - local expected_native_base_image="" - local native_base_image="" - local native_base_digest="" - local whl="" - local whl_name="" - local -a wheels=() - - if ! should_upload_wheel_artifacts; then - return 0 - fi - - if [[ -d "${wheel_dir}" ]]; then - mapfile -t wheels < <(find "${wheel_dir}" -maxdepth 1 -type f -name '*.whl' -print) - fi - if [[ ${#wheels[@]} -ne 1 ]]; then - echo "Expected exactly one ROCm wheel in ${wheel_dir}; found ${#wheels[@]}" >&2 - return 1 - fi - whl="${wheels[0]}" - whl_name=$(basename "${whl}") - native_base_image="${CI_BASE_IMAGE_TAG_BUILD_REF:-${CI_BASE_IMAGE:-}}" - if [[ -z "${native_base_image}" ]]; then - echo "Native ROCm artifact requires a ci_base image reference" >&2 - return 1 - fi - if [[ "${BUILDKITE:-false}" == "true" ]]; then - expected_native_base_image=$(ci_base_tag_with_suffix \ - "${CI_BASE_IMAGE_TAG:-rocm/vllm-dev:ci_base}" \ - "build-${BUILDKITE_BUILD_ID:-}") - if [[ "${native_base_image}" != "${expected_native_base_image}" ]]; then - echo "Native ROCm artifact requires the exact ci_base build handoff: ${native_base_image}" >&2 - return 1 - fi - if [[ ! "${CI_BASE_IMAGE:-}" =~ @sha256:[0-9a-f]{64}$ ]]; then - echo "ROCm artifact build base must be digest-pinned: ${CI_BASE_IMAGE:-}" >&2 - return 1 - fi - build_base_digest="${CI_BASE_IMAGE##*@}" - if ! native_base_digest=$(resolve_image_digest "${native_base_image}"); then - echo "Could not resolve native ci_base handoff: ${native_base_image}" >&2 - return 1 - fi - if [[ "${native_base_digest}" != "${build_base_digest}" ]]; then - echo "Native ci_base handoff does not match the artifact build base" >&2 - echo " native: ${native_base_image}@${native_base_digest}" >&2 - echo " build: ${CI_BASE_IMAGE}" >&2 - return 1 - fi - fi - - echo "--- :package: Uploading ROCm vLLM install artifact" - rm -rf "${artifact_dir}" "${metadata_dir}" - mkdir -p "${artifact_dir}" "${metadata_dir}" - - printf '%s\n' "${BUILDKITE_COMMIT:-local}" > "${metadata_dir}/commit.txt" - printf '%s\n' "${native_base_image}" > "${metadata_dir}/native-base-image.txt" - printf '%s\n' "${CI_BASE_IMAGE:-}" > "${metadata_dir}/ci-base-image.txt" - printf '%s\n' "${IMAGE_TAG:-}" > "${metadata_dir}/fallback-image.txt" - printf '%s\n' "${whl_name}" > "${metadata_dir}/wheel-filename.txt" - - tar -C "${wheel_dir}" -czf "${artifact_dir}/${archive_name}" . - ( - cd "${artifact_dir}" - sha256sum "${archive_name}" > "${archive_name}.sha256" - ) - echo "Created ${archive_name}: $(du -sh "${artifact_dir}/${archive_name}" | cut -f1)" - cp "${metadata_dir}"/*.txt "${artifact_dir}/" - cp "${whl}" "${artifact_dir}/${whl_name}" - echo "Copied ${whl_name}: $(du -sh "${artifact_dir}/${whl_name}" | cut -f1)" - - if command -v buildkite-agent >/dev/null 2>&1; then - buildkite-agent artifact upload "${artifact_dir}/*" || return 1 - echo "ROCm vLLM install artifacts uploaded to ${artifact_dir}/" - elif [[ "${BUILDKITE:-false}" == "true" ]]; then - echo "buildkite-agent not found; cannot upload required ROCm artifacts" >&2 - return 1 - else - echo "Not in Buildkite, skipping artifact upload" - fi - - rm -rf "${wheel_dir}" -} - -main() { - init_config "$@" - configure_ci_base_write_scope - print_header - validate_inputs - load_ci_hcl - init_bake_files - if is_ci_base_target; then - prepare_ci_build_context - configure_custom_rocm_stages - fi - compute_ci_base_hash_if_needed - configure_ci_base_image_refs - maybe_skip_existing_image - setup_builder - prepare_git_cache_metadata - # Non-ci_base builds may deepen a shallow checkout above. Derive archival - # version metadata only after that lookup sees the available tag history. - if ! is_ci_base_target; then - prepare_ci_build_context - configure_custom_rocm_stages - fi - extract_dependency_pins - write_rocm_build_arg_override - compute_dependency_cache_keys - write_ci_base_label_override - compute_rocm_csrc_content_hash_if_needed - compute_rocm_rust_content_hash_if_needed - write_rocm_cache_override - # Keep the context override last so every bake target uses the owned tree. - write_build_context_override - resolve_ci_base_dependency_targets - print_bake_config - if [[ "${BAKE_PRINT_ONLY:-0}" == "1" ]]; then - echo "BAKE_PRINT_ONLY=1 set; skipping build" - return 0 - fi - if should_upload_wheel_artifacts; then - # wheel-export is an output directory, not a BuildKit cache. Starting - # clean prevents a failed/retried export from packaging a stale wheel. - rm -rf ./wheel-export - fi - if should_export_rocm_smoke; then - # The marker is a build output, not a cache. Never accept stale output - # from an earlier build or retry. - rm -rf ./build/rocm-smoke-export - fi - if [[ "${TARGET}" == "kernel-symbol-map-rocm" ]]; then - rm -rf ./kernel-symbol-map-rocm - fi - seed_dependency_caches_if_needed - run_bake - verify_rocm_smoke_export - promote_stable_ci_base_tag - publish_ci_base_handoff_ref - upload_wheel_artifacts_if_present - if [[ "${TARGET}" == "kernel-symbol-map-rocm" ]]; then - upload_kernel_symbol_map_if_present - else - export_kernel_symbol_map \ - || echo "ROCm kernel symbol map export or upload failed; continuing without it" >&2 - fi -} - -if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then - main "$@" -fi diff --git a/.buildkite/scripts/hardware_ci/amd_ci_fast_exit.py b/.buildkite/scripts/hardware_ci/amd_ci_fast_exit.py new file mode 100644 index 000000000000..c77a19fbc966 --- /dev/null +++ b/.buildkite/scripts/hardware_ci/amd_ci_fast_exit.py @@ -0,0 +1,66 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +"""pytest plugin for AMD CI jobs: leave without native teardown once pytest is done. + +A pytest process can crash in native library destructors after its summary: +vllm/ci #96786 printed "236 passed" and then died with SIGSEGV (exit 139) during +interpreter shutdown, past the point where faulthandler reports. Once the outermost +session has finished, this runs the Python exit handlers (engine shutdown, Ray, +multiprocessing cleanup), flushes, and exits with pytest's own status, skipping +module teardown and native destructors. pytest's result is never changed, and a +command chain still stops on a real failure. + +Off when a kernel recorder is loaded (it writes its trace during native teardown) +or with VLLM_CI_FAST_EXIT=0. +""" + +import atexit +import os +import sys + +import pytest + +_outer = None +_session = None + + +def _recorder_loaded() -> bool: + return bool( + os.environ.get("ROCP_TOOL_LIBRARIES") + or os.environ.get("CUDA_INJECTION64_PATH") + or "rocprofiler" in os.environ.get("LD_PRELOAD", "") + ) + + +def pytest_configure(config): + global _outer + if _outer is None: # pytester runs nested sessions in-process; act on the outer one + _outer = config + + +def pytest_sessionfinish(session, exitstatus): + global _session + if session.config is _outer: + _session = session + + +@pytest.hookimpl(trylast=True) +def pytest_unconfigure(config): + if config is not _outer or _session is None: + return + if os.environ.get("VLLM_CI_FAST_EXIT") == "0" or _recorder_loaded(): + return + status = int(_session.exitstatus) + sys.stdout.flush() + sys.stderr.flush() + try: + atexit._run_exitfuncs() + except BaseException as exc: # an exit handler failing must not mask the result + print(f"[amd-ci] exit handler failed: {exc!r}", file=sys.stderr) + print( + f"[amd-ci] pytest finished with exit {status}; leaving without native teardown", + file=sys.stderr, + flush=True, + ) + sys.stdout.flush() + os._exit(status) diff --git a/.buildkite/scripts/hardware_ci/run-amd-test.sh b/.buildkite/scripts/hardware_ci/run-amd-test.sh index 59240b3d0a36..b649cfd7c847 100755 --- a/.buildkite/scripts/hardware_ci/run-amd-test.sh +++ b/.buildkite/scripts/hardware_ci/run-amd-test.sh @@ -1,1813 +1,212 @@ #!/bin/bash - -# shellcheck disable=SC2329 # Every function in this file is only ever reached -# transitively through handle_amd_runner_exit, the EXIT trap handler - shellcheck's -# unused-function check doesn't do full reachability analysis from an indirectly -# invoked entry point, so it flags the whole diagnostic-collection call chain as -# dead code even though it runs live on every nonzero exit. - -# This script runs ROCm tests either directly in a native CI pod or inside the -# corresponding Docker container. Multi-node tests continue to use Docker. -# -# Multi-node detection: Instead of matching on fragile group names, we detect -# multi-node jobs structurally by looking for the bracket command syntax -# "[node0_cmds] && [node1_cmds]" or via the NUM_NODES environment variable. -# -############################################################################### -# QUOTING / COMMAND PASSING -# -# Passing commands as positional arguments ($*) is fragile when the command -# string itself contains double quotes, e.g.: -# -# bash run-amd-test.sh "export FLAGS="value" && pytest -m "not slow"" -# -# The outer shell resolves the nested quotes *before* this script runs, so -# the script receives mangled input it cannot fully recover. -# -# Preferred: pass commands via the VLLM_TEST_COMMANDS environment variable: +# Run one AMD CI job in its Kubernetes pod. # -# export VLLM_TEST_COMMANDS='export FLAGS="value" && pytest -m "not slow"' -# bash run-amd-test.sh -# -# Single-quoted assignment preserves all inner double quotes verbatim. -# The $* path is kept for backward compatibility but callers should migrate. -############################################################################### +# The container image ($ROCM_CI_REPO:build-) already contains vLLM, +# all test dependencies, and /vllm-workspace. This script: +# 1. Sets up per-job local caches and validates the HF mount. +# 2. Fetches this build's wheel artifact for python_only_compile.sh. +# 3. Acquires the DPX GPU partition lock when running on a DPX agent. +# 4. Runs a preflight check (imports + GPU count). +# 5. Executes $VLLM_TEST_COMMANDS under a wall-clock timeout. +# 6. Writes a diagnostics bundle on failure. set -o pipefail -# shellcheck source=.buildkite/scripts/rocm/build-config.sh -source "$(dirname "${BASH_SOURCE[0]}")/../rocm/build-config.sh" || exit $? - -: "${BUILDKIT_PROGRESS:=plain}" -: "${TERM:=xterm-256color}" -: "${FORCE_COLOR:=1}" -: "${CLICOLOR_FORCE:=1}" -: "${PY_COLORS:=1}" -: "${ROCM_DOCKER_TTY:=1}" -: "${PYTHONFAULTHANDLER:=1}" -: "${PYTEST_TIMEOUT:=2400}" - -# Preserve runner-only values before clear_ci_orchestration_env removes them -# from the environment inherited by the test process. -amd_diagnostics_expected_gpu_count="${VLLM_CI_EXPECTED_GPU_COUNT:-1}" -amd_diagnostics_dir="${VLLM_CI_DIAGNOSTICS_DIR:-artifacts/amd-gpu-diagnostics}" -amd_diagnostics_checkout_root="${BUILDKITE_BUILD_CHECKOUT_PATH:-$(pwd -P)}" -amd_diagnostics_execution_mode="${VLLM_CI_EXECUTION_MODE:-single-node}" -amd_diagnostics_test_group="${VLLM_TEST_GROUP_NAME:-${BUILDKITE_STEP_KEY:-unknown}}" -amd_diagnostics_workspace="${VLLM_CI_WORKSPACE:-/vllm-workspace}" -amd_diagnostics_pod_name="${VLLM_CI_K8S_POD_NAME:-${POD_NAME:-${HOSTNAME:-}}}" -amd_diagnostics_k8s_namespace="${VLLM_CI_K8S_NAMESPACE:-${POD_NAMESPACE:-unknown}}" -amd_diagnostics_k8s_node_name="${VLLM_CI_K8S_NODE_NAME:-${NODE_NAME:-}}" -amd_diagnostics_collected=0 -amd_diagnostics_memory_events_path="" -amd_diagnostics_probe_budget_seconds=25 -amd_diagnostics_command_timeout_seconds=5 -amd_diagnostics_upload_timeout_seconds=20 -if [[ " ${PYTEST_ADDOPTS:-} " != *" --color"* ]]; then - PYTEST_ADDOPTS="${PYTEST_ADDOPTS:+${PYTEST_ADDOPTS} }--color=yes" -fi -if [[ " ${PYTEST_ADDOPTS:-} " != *" --durations="* ]]; then - PYTEST_ADDOPTS="${PYTEST_ADDOPTS:+${PYTEST_ADDOPTS} }--durations=25" -fi -if [[ " ${PYTEST_ADDOPTS:-} " != *" --durations-min="* ]]; then - PYTEST_ADDOPTS="${PYTEST_ADDOPTS:+${PYTEST_ADDOPTS} }--durations-min=1.0" -fi -# Dump stacks after 25 minutes, then stop an individual test after 40 minutes. -if [[ " ${PYTEST_ADDOPTS:-} " != *" faulthandler_timeout="* ]]; then - PYTEST_ADDOPTS="${PYTEST_ADDOPTS:+${PYTEST_ADDOPTS} }-o faulthandler_timeout=1500" -fi -if [[ " ${PYTEST_ADDOPTS:-} " != *" --timeout-method="* && - " ${PYTEST_ADDOPTS:-} " != *" --timeout-method "* ]]; then - PYTEST_ADDOPTS="${PYTEST_ADDOPTS:+${PYTEST_ADDOPTS} }--timeout-method=thread" -fi -export BUILDKIT_PROGRESS TERM FORCE_COLOR CLICOLOR_FORCE PY_COLORS PYTEST_ADDOPTS PYTEST_TIMEOUT ROCM_DOCKER_TTY -export PYTHONFAULTHANDLER - -# Export Python path for commands that run directly on the host. Containerized -# tests set this to /vllm-workspace below so spawned Python processes do not -# depend on their current working directory. -export PYTHONPATH="${PYTHONPATH:-..}" - -ci_started_at=$SECONDS - -############################################################################### -# Helper Functions -############################################################################### - -report_docker_usage() { - echo "--- Docker usage" - docker system df || true -} - -clear_ci_orchestration_env() { - unset -v \ - VLLM_TEST_GROUP_NAME \ - VLLM_CI_REQUIRE_PERSISTENT_HF_CACHE \ - VLLM_CI_ARTIFACT_STEP \ - VLLM_TEST_CACHE \ - VLLM_CI_EXECUTION_MODE \ - VLLM_CI_WORKSPACE \ - VLLM_CI_REQUIRE_WORKSPACE_MOUNT \ - VLLM_TEST_COMMANDS \ - VLLM_CI_BRANCH \ - CI_ROCM_DOCKERFILE_BASE \ - CI_ROCM_DOCKERFILE \ - ROCM_BASE_DOCKERFILE \ - CI_BASE_DOCKERFILE \ - VLLM_CI_BASE_IMAGE \ - VLLM_CI_FALLBACK_IMAGE \ - VLLM_CI_DOCKER_DISABLED \ - VLLM_CI_DIAGNOSTICS_DIR \ - VLLM_CI_ARTIFACT_GLOB \ - VLLM_CI_ARTIFACT_CHECKSUM_GLOB \ - VLLM_CI_EXPECTED_GPU_COUNT \ - VLLM_CI_K8S_POD_NAME \ - VLLM_CI_K8S_NAMESPACE \ - VLLM_CI_K8S_NODE_NAME \ - VLLM_CI_USE_ARTIFACTS \ - VLLM_CI_RESULTS_ROOT \ - VLLM_ALLOW_DEPRECATED_BEAM_SEARCH -} - -amd_ci_teardown_log() { - local event=$1 - shift - - printf '[amd-ci-teardown] event=%s' "${event}" >&2 - if (($#)); then - printf ' %s' "$@" >&2 - fi - printf '\n' >&2 -} - -run_docker_with_ci_timeout() { - local raw_timeout=${CONTAINER_TIMEOUT_S:-0} - local configured_timeout=0 - local elapsed=0 - local remaining_timeout=0 - local status=0 - - if [[ ! "${raw_timeout}" =~ ^(0|[1-9][0-9]{0,5})$ ]]; then - amd_ci_teardown_log configuration_error \ - "reason=invalid_timeout" "expected=integer_0_to_604800" - return 2 - fi - configured_timeout=$((10#${raw_timeout})) - if ((configured_timeout > 604800)); then - amd_ci_teardown_log configuration_error \ - "reason=invalid_timeout" "expected=integer_0_to_604800" - return 2 - fi - - remaining_timeout=${configured_timeout} - if ((configured_timeout > 0)); then - if ! command -v timeout >/dev/null 2>&1; then - amd_ci_teardown_log configuration_error \ - "reason=timeout_command_not_found" - return 127 - fi - elapsed=$((SECONDS - ci_started_at)) - if ((elapsed >= configured_timeout)); then - amd_ci_teardown_log deadline_exhausted \ - "mode=docker" "configured_s=${configured_timeout}" "elapsed_s=${elapsed}" - return 124 - fi - remaining_timeout=$((configured_timeout - elapsed)) - fi - - amd_ci_teardown_log workload_started \ - "mode=docker" "timeout_s=${remaining_timeout}" - if ((remaining_timeout == 0)); then - "$@" || status=$? - else - # Docker runs interactively, so keep it in the foreground process group. - # --verbose records both TERM and any KILL escalation caused by timeout. - timeout --verbose --foreground --signal=TERM --kill-after=10s \ - "${remaining_timeout}s" "$@" || status=$? - fi - amd_ci_teardown_log workload_finished "mode=docker" "status=${status}" - return "${status}" -} - -prepare_artifact_image() { - if [[ "${VLLM_CI_USE_ARTIFACTS:-0}" != "1" ]]; then - return 1 - fi - if ! command -v buildkite-agent >/dev/null 2>&1; then - echo "buildkite-agent not found; cannot download ROCm wheel artifact" - return 1 - fi - - local artifact_glob="${VLLM_CI_ARTIFACT_GLOB:-artifacts/vllm-rocm-install/vllm-rocm-install.tar.gz}" - local archive="" - local metadata_file="" - local base_image="${VLLM_CI_BASE_IMAGE:-rocm/vllm-dev:ci_base}" - local artifact_image="" - local artifact_key="" - local base_digest="" - local wheel_dir="" - local context_dir="" - local workspace_dir="" - - artifact_work_dir=$(mktemp -d -t vllm-rocm-artifact.XXXXXX) - wheel_dir="${artifact_work_dir}/wheels" - context_dir="${artifact_work_dir}/context" - workspace_dir="${context_dir}/workspace" - mkdir -p "${wheel_dir}" "${context_dir}/wheels" "${workspace_dir}" - - echo "--- Downloading ROCm wheel artifact" - if ! buildkite-agent artifact download "${artifact_glob}" "${artifact_work_dir}"; then - echo "Failed to download ${artifact_glob}" - return 1 - fi - buildkite-agent artifact download \ - "artifacts/vllm-rocm-install/ci-base-image.txt" \ - "${artifact_work_dir}" >/dev/null 2>&1 || true - - archive=$(find "${artifact_work_dir}" -name "vllm-rocm-install.tar.gz" -type f | head -1) - if [[ -z "${archive}" || ! -f "${archive}" ]]; then - echo "ROCm wheel artifact archive was not found" - return 1 - fi - - metadata_file=$(find "${artifact_work_dir}" -name "ci-base-image.txt" -type f | head -1) - if [[ -n "${metadata_file}" && -s "${metadata_file}" ]]; then - base_image=$(tr -d '[:space:]' < "${metadata_file}") - elif using_custom_rocm_dockerfiles; then - echo "Custom ROCm ci_base metadata is missing; using the full CI image" - return 1 - fi - - echo "--- Preparing local ROCm test image" - echo "Base image: ${base_image}" - docker pull "${base_image}" || return 1 - base_digest=$( - docker image inspect \ - --format='{{if .RepoDigests}}{{index .RepoDigests 0}}{{else}}{{.Id}}{{end}}' \ - "${base_image}" 2>/dev/null || printf '%s' "${base_image}" - ) - - artifact_key=$( - { - printf 'base-image:%s\n' "${base_digest}" - sha256sum "${archive}" - } | sha256sum | cut -c1-24 - ) - artifact_image="rocm/vllm-ci-artifact:${artifact_key}" - - if docker image inspect "${artifact_image}" >/dev/null 2>&1; then - echo "Using existing local ROCm artifact image: ${artifact_image}" - image_name="${artifact_image}" - return 0 - fi - - tar -xzf "${archive}" -C "${wheel_dir}" || return 1 - if ! ls "${wheel_dir}"/*.whl >/dev/null 2>&1; then - echo "ROCm wheel artifact did not contain a wheel" - return 1 - fi - if [[ ! -d "${wheel_dir}/tests" ]]; then - echo "ROCm wheel artifact did not contain the test workspace" - return 1 - fi - - cp "${wheel_dir}"/*.whl "${context_dir}/wheels/" || return 1 - tar -C "${wheel_dir}" \ - --exclude='*.whl' \ - --exclude='.vllm-ci-artifact' \ - --exclude='./.vllm-ci-artifact' \ - -cf - . \ - | tar -C "${workspace_dir}" -xf - || return 1 - cat > "${context_dir}/Dockerfile" <<'EOF' -ARG BASE_IMAGE -FROM ${BASE_IMAGE} -COPY wheels/ /tmp/vllm-wheels/ -COPY workspace/ /vllm-workspace/ -RUN python3 -m pip install --no-deps --force-reinstall /tmp/vllm-wheels/*.whl \ - && rm -rf /tmp/vllm-wheels -WORKDIR /vllm-workspace -EOF - - echo "--- Building local ROCm test image" - docker build \ - --pull=false \ - --progress "${BUILDKIT_PROGRESS}" \ - --build-arg "BASE_IMAGE=${base_image}" \ - -t "${artifact_image}" \ - "${context_dir}" || return 1 - image_name="${artifact_image}" - return 0 -} - -is_native_runtime() { - [[ "${AMD_CI_RUNTIME:-}" == "native" || "${NATIVE_CI:-}" == "true" ]] -} - -validate_native_workspace() { - local workspace_dir="${VLLM_CI_WORKSPACE:-/vllm-workspace}" - local workspace_real="" - local checkout_real="" - local workspace_mount="" - - mkdir -p "${workspace_dir}" || return 1 - workspace_real=$(readlink -m "${workspace_dir}") || return 1 - if [[ -n "${BUILDKITE_BUILD_CHECKOUT_PATH:-}" ]]; then - checkout_real=$(readlink -m "${BUILDKITE_BUILD_CHECKOUT_PATH}") || return 1 - if [[ "${checkout_real}" == "${workspace_real}" \ - || "${checkout_real}" == "${workspace_real}/"* \ - || "${workspace_real}" == "${checkout_real}/"* ]]; then - echo "Refusing to replace ${workspace_real}; it overlaps the Buildkite checkout ${checkout_real}" >&2 - return 1 - fi - fi - if [[ "${VLLM_CI_REQUIRE_WORKSPACE_MOUNT:-1}" == "1" ]]; then - if ! command -v findmnt >/dev/null 2>&1; then - echo "findmnt is required to verify the native workspace mount" >&2 - return 1 - fi - workspace_mount=$(findmnt -n -T "${workspace_real}" -o TARGET 2>/dev/null || true) - if [[ "$(readlink -m "${workspace_mount:-/}")" != "${workspace_real}" ]]; then - echo "Native CI requires a dedicated volume mounted at ${workspace_real}" >&2 - return 1 - fi - fi -} - -prepare_native_workspace() { - local test_commands="${1:-}" - - if [[ "${VLLM_CI_USE_ARTIFACTS:-0}" != "1" ]]; then - echo "Native CI requires VLLM_CI_USE_ARTIFACTS=1" - return 1 - fi - if ! command -v buildkite-agent >/dev/null 2>&1; then - echo "buildkite-agent not found; cannot download ROCm wheel artifact" - return 1 - fi - validate_native_workspace || return 1 - - local artifact_glob="${VLLM_CI_ARTIFACT_GLOB:-artifacts/vllm-rocm-install/vllm-rocm-install.tar.gz}" - local artifact_checksum_glob="${VLLM_CI_ARTIFACT_CHECKSUM_GLOB:-${artifact_glob}.sha256}" - local artifact_step="${VLLM_CI_ARTIFACT_STEP:-image-build-amd}" - local archive="" - local checksum="" - local download_dir="" - local metadata_dir="" - local recorded_base="" - local recorded_commit="" - local recorded_wheel="" - local checkout="" - local checkout_commit="" - local workspace_dir="${VLLM_CI_WORKSPACE:-/vllm-workspace}" - local wheel_dir="" - local attempt=0 - local attempt_dir="" - local -a archives=() - local -a checksums=() - local -a wheels=() - - artifact_work_dir=$(mktemp -d -t vllm-rocm-artifact.XXXXXX) || return 1 - wheel_dir="${artifact_work_dir}/wheels" - mkdir -p "${wheel_dir}" || return 1 - - echo "--- Downloading ROCm wheel artifact from ${artifact_step} (native in-pod)" - for attempt in 1 2 3; do - attempt_dir="${artifact_work_dir}/download-${attempt}" - rm -rf "${attempt_dir}" || return 1 - mkdir -p "${attempt_dir}" || return 1 - if buildkite-agent artifact download \ - "${artifact_glob}" "${attempt_dir}" --step "${artifact_step}" \ - && buildkite-agent artifact download \ - "${artifact_checksum_glob}" "${attempt_dir}" --step "${artifact_step}"; then - download_dir="${attempt_dir}" - break - fi - echo "Artifact download attempt ${attempt}/3 failed" - if [[ "${attempt}" -lt 3 ]]; then - sleep $((attempt * 2)) - fi - done - if [[ -z "${download_dir}" ]]; then - echo "Failed to download ${artifact_glob} and ${artifact_checksum_glob} from ${artifact_step}" - return 1 - fi - - mapfile -t archives < <( - find "${download_dir}" -name "vllm-rocm-install.tar.gz" -type f -print - ) - mapfile -t checksums < <( - find "${download_dir}" -name "vllm-rocm-install.tar.gz.sha256" -type f -print - ) - if [[ ${#archives[@]} -ne 1 || ${#checksums[@]} -ne 1 ]]; then - echo "Expected exactly one ROCm archive and checksum; found ${#archives[@]} archive(s) and ${#checksums[@]} checksum(s)" >&2 - return 1 - fi - archive="${archives[0]}" - checksum="${checksums[0]}" - if [[ "$(dirname "${archive}")" != "$(dirname "${checksum}")" ]]; then - echo "ROCm archive and checksum were downloaded to different directories" >&2 - return 1 - fi - ( - cd "$(dirname "${archive}")" - sha256sum -c "$(basename "${checksum}")" - ) || return 1 - - tar --no-same-owner -xzf "${archive}" -C "${wheel_dir}" || return 1 - mapfile -t wheels < <( - find "${wheel_dir}" -maxdepth 1 -type f -name '*.whl' -print - ) - if [[ ${#wheels[@]} -ne 1 ]]; then - echo "ROCm artifact must contain exactly one top-level wheel; found ${#wheels[@]}" >&2 - return 1 - fi - metadata_dir="${wheel_dir}/.vllm-ci-artifact" - for metadata_file in commit.txt native-base-image.txt wheel-filename.txt; do - if [[ ! -s "${metadata_dir}/${metadata_file}" ]]; then - echo "ROCm artifact metadata is missing ${metadata_file}" >&2 - return 1 - fi - done - for metadata_file in ci-base-image.txt fallback-image.txt; do - if [[ ! -f "${metadata_dir}/${metadata_file}" ]]; then - echo "ROCm artifact metadata is missing ${metadata_file}" >&2 - return 1 - fi - done - - recorded_commit=$(tr -d '\r\n' < "${metadata_dir}/commit.txt") - recorded_base=$(tr -d '\r\n' < "${metadata_dir}/native-base-image.txt") - recorded_wheel=$(tr -d '\r\n' < "${metadata_dir}/wheel-filename.txt") - if [[ -z "${BUILDKITE_COMMIT:-}" || "${recorded_commit}" != "${BUILDKITE_COMMIT}" ]]; then - echo "ROCm artifact commit ${recorded_commit} does not match ${BUILDKITE_COMMIT:-unset}" >&2 - return 1 - fi - if [[ -z "${BUILDKITE_BUILD_ID:-}" \ - || "${recorded_base}" != *":ci_base-build-${BUILDKITE_BUILD_ID}" ]]; then - echo "ROCm artifact base is not scoped to Buildkite build ${BUILDKITE_BUILD_ID:-unset}: ${recorded_base}" >&2 - return 1 - fi - if [[ -z "${VLLM_CI_BASE_IMAGE:-}" || "${recorded_base}" != "${VLLM_CI_BASE_IMAGE}" ]]; then - echo "ROCm artifact base ${recorded_base} does not match ${VLLM_CI_BASE_IMAGE:-unset}" >&2 - return 1 - fi - if [[ "${recorded_wheel}" != "$(basename "${wheels[0]}")" ]]; then - echo "ROCm artifact wheel manifest ${recorded_wheel} does not match $(basename "${wheels[0]}")" >&2 - return 1 - fi - for required_dir in tests .buildkite requirements; do - if [[ ! -d "${wheel_dir}/${required_dir}" ]]; then - echo "ROCm wheel artifact did not contain ${required_dir}/" >&2 - return 1 - fi +# --------------------------------------------------------------------------- +# Inputs — set by the pipeline / ci-infra template +# --------------------------------------------------------------------------- +commands=${VLLM_TEST_COMMANDS:?VLLM_TEST_COMMANDS must be set by the pipeline} +gpus=${VLLM_CI_EXPECTED_GPU_COUNT:-1} +[[ $gpus =~ ^(0|[1-9][0-9]*)$ ]] || { echo "Invalid VLLM_CI_EXPECTED_GPU_COUNT=$gpus" >&2; exit 1; } +job=${BUILDKITE_JOB_ID:-local} +job=${job//[^A-Za-z0-9_.-]/_} +# Short: Ray puts AF_UNIX sockets under $TMPDIR, whose paths may not exceed 107 bytes. +root=/tmp/vllm-${job##*-} +root=${root:0:22} + +# --------------------------------------------------------------------------- +# Cache directories — all under $root so they are job-local and isolated. +# HF hub is the one exception: it lives on the node's persistent NFS mount. +# --------------------------------------------------------------------------- +export HF_HOME=${HF_HOME:-/home/buildkite-agent/huggingface} +export TIKTOKEN_RS_CACHE_DIR=$HF_HOME/tiktoken-rs-cache +export PYTHONPATH=/vllm-workspace +export PYTORCH_ROCM_ARCH="" # let vLLM detect arches at runtime +export TMPDIR=$root/tmp +export VLLM_RPC_BASE_PATH=/tmp +export TORCHINDUCTOR_CACHE_DIR=$root/inductor +export TRITON_CACHE_DIR=$root/triton +export VLLM_CACHE_ROOT=$root/vllm +export XDG_CACHE_HOME=$root/xdg +export HF_DATASETS_CACHE=$root/datasets +export HF_HUB_DOWNLOAD_TIMEOUT=${HF_HUB_DOWNLOAD_TIMEOUT:-300} +export HF_HUB_ETAG_TIMEOUT=${HF_HUB_ETAG_TIMEOUT:-60} + +mkdir -p \ + "$TMPDIR" "$TORCHINDUCTOR_CACHE_DIR" "$TRITON_CACHE_DIR" \ + "$VLLM_CACHE_ROOT" "$XDG_CACHE_HOME" "$HF_DATASETS_CACHE" \ + "$TIKTOKEN_RS_CACHE_DIR" || exit 1 + +# HF_HOME must be a persistent mount, not rootfs (catches misconfigured nodes). +[[ $(findmnt -n -T "$HF_HOME" -o TARGET) != / ]] \ + || { echo "HF_HOME is on rootfs — it must be a persistent node mount" >&2; exit 1; } + +# hf-xet state corrupts on NFS; redirect it to a local tmpdir when needed. +if [[ $(findmnt -n -T "$HF_HOME" -o FSTYPE) == nfs* ]]; then + export HF_XET_CACHE=$root/hf-xet + export HF_XET_HIGH_PERFORMANCE=0 + export HF_XET_RECONSTRUCTION_USE_VECTORED_WRITE=0 +fi + +# --------------------------------------------------------------------------- +# Terminal and pytest settings +# --------------------------------------------------------------------------- +# Keep CI log output colorized. +: "${TERM:=xterm-256color}" "${FORCE_COLOR:=1}" "${CLICOLOR_FORCE:=1}" "${PY_COLORS:=1}" +export TERM FORCE_COLOR CLICOLOR_FORCE PY_COLORS + +# Fault-handler dumps stacks after 25 min; pytest-timeout kills a single test +# after 40 min (PYTEST_TIMEOUT=2400 s). Both are overridable by the pipeline. +export PYTHONFAULTHANDLER=1 +export PYTEST_TIMEOUT=${PYTEST_TIMEOUT:-2400} +export PYTEST_ADDOPTS="${PYTEST_ADDOPTS:-} \ + --color=yes \ + --durations=25 \ + --durations-min=1.0 \ + -o faulthandler_timeout=1500 \ + --timeout-method=thread" + +# After its summary, a pytest process leaves without native library teardown, where it +# has crashed with SIGSEGV after every test passed (amd_ci_fast_exit.py). +# Installed into site-packages, not put on PYTHONPATH: commands that set PYTHONPATH themselves +# still inherit PYTEST_ADDOPTS and must find the plugin. +ci_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +cp "$ci_dir/amd_ci_fast_exit.py" \ + "$(python3 -P -c 'import sysconfig; print(sysconfig.get_paths()["purelib"])')/" || exit 1 +export PYTEST_ADDOPTS="$PYTEST_ADDOPTS -p amd_ci_fast_exit" + +[[ $gpus == 0 ]] && export VLLM_TARGET_DEVICE=cpu + +# --------------------------------------------------------------------------- +# Wheel reinstall — python_only_compile.sh path +# --------------------------------------------------------------------------- +# python_only_compile.sh reinstalls vLLM around this build's wheel, which build.sh uploads as an +# artifact of the image build step (kept out of the image: only this test needs it). +if [[ $commands == *python_only_compile.sh* ]]; then + wheel_dir=$(mktemp -d) + for try in 1 2 3; do + buildkite-agent artifact download ".rocm-wheel/vllm-*.whl" "$wheel_dir" --step image-build-amd \ + && break + ((try == 3)) && exit 1 + sleep $((try * 5)) done - - echo "--- Installing ROCm wheel into pod environment" - python3 -m pip install --no-deps --force-reinstall "${wheels[0]}" || return 1 - - echo "--- Preparing ${workspace_dir} from artifact" - find "${workspace_dir}" -mindepth 1 -maxdepth 1 -exec rm -rf -- {} + || return 1 - tar -C "${wheel_dir}" \ - --exclude='*.whl' \ - --exclude='.vllm-ci-artifact' \ - --exclude='./.vllm-ci-artifact' \ - -cf - . | tar --no-same-owner -C "${workspace_dir}" -xf - || return 1 - if [[ ! -d "${workspace_dir}/tests" ]]; then - echo "Failed to stage the native test workspace" >&2 - return 1 - fi - - # The ROCm artifact intentionally contains only the installed wheel and the - # test workspace. The Python-only compilation job also needs setup.py and the - # vllm source tree, so overlay the matching Buildkite checkout for that job. - if [[ "${test_commands}" == *python_only_compile.sh* ]]; then - checkout="${BUILDKITE_BUILD_CHECKOUT_PATH:-}" - if [[ -z "${checkout}" || ! -d "${checkout}" ]]; then - echo "Python-only native CI requires BUILDKITE_BUILD_CHECKOUT_PATH" >&2 - return 1 - fi - if ! git -c "safe.directory=${checkout}" -C "${checkout}" \ - rev-parse --is-inside-work-tree >/dev/null 2>&1; then - echo "Buildkite checkout is not a Git worktree: ${checkout}" >&2 - return 1 - fi - checkout_commit=$( - git -c "safe.directory=${checkout}" -C "${checkout}" rev-parse HEAD - ) || return 1 - if [[ "${checkout_commit}" != "${recorded_commit}" ]]; then - echo "Buildkite checkout ${checkout_commit} does not match ROCm artifact ${recorded_commit}" >&2 - return 1 - fi - - # setup.py normally derives this from .git via setuptools-scm. The native - # source overlay deliberately excludes Git metadata, so preserve the exact - # version from the already installed, artifact-matched wheel. - VLLM_VERSION_OVERRIDE=$( - python3 -c 'import importlib.metadata as m; print(m.version("vllm"))' - ) || return 1 - export VLLM_VERSION_OVERRIDE - VLLM_PRECOMPILED_WHEEL_LOCATION="${wheels[0]}" - export VLLM_PRECOMPILED_WHEEL_LOCATION - echo "INFO: native Python-only wheel=${VLLM_PRECOMPILED_WHEEL_LOCATION}" - - echo "--- Overlaying full source checkout for Python-only compilation" - # Archive the verified commit instead of copying the worktree so dirty or - # untracked agent files cannot contaminate the artifact-matched workspace. - git -c "safe.directory=${checkout}" -C "${checkout}" \ - archive --format=tar "${recorded_commit}" \ - | tar --no-same-owner -C "${workspace_dir}" -xf - || return 1 - for required_source in setup.py pyproject.toml vllm; do - if [[ ! -e "${workspace_dir}/${required_source}" ]]; then - echo "Full source checkout is missing ${required_source}" >&2 - return 1 - fi - done - fi - - return 0 -} - -initialize_native_environment() { - local job_id="${BUILDKITE_JOB_ID:-${BUILDKITE_PARALLEL_JOB:-local}}" - local job_id_suffix="" - local native_root="" - local hf_fstype="" - local hf_mount="" - - if [[ "$(id -u)" -ne 0 ]]; then - echo "Native ROCm CI currently requires the ci_base container to run as root" >&2 - return 1 - fi - - job_id="${job_id//[^A-Za-z0-9_.-]/_}" - job_id_suffix="${job_id##*-}" - job_id_suffix="${job_id_suffix:0:12}" - native_root="/tmp/vllm-native-${job_id}" - TMPDIR="/tmp/vllm-${job_id_suffix}/tmp" - VLLM_RPC_BASE_PATH="/tmp" - TORCHINDUCTOR_CACHE_DIR="${native_root}/cache/torchinductor" - TRITON_CACHE_DIR="${native_root}/cache/triton" - VLLM_CACHE_ROOT="${native_root}/cache/vllm" - XDG_CACHE_HOME="${native_root}/cache/xdg" - : "${HF_HOME:=/home/buildkite-agent/huggingface}" - # datasets uses POSIX locks that are unsupported by the shared HF NFS cache. - # Keep processed datasets job-local while retaining the persistent Hub cache. - HF_DATASETS_CACHE="${native_root}/cache/huggingface/datasets" - # openai-harmony downloads its tiktoken vocab on first use and caches it under - # $TMPDIR by default, which is job-local; keep it with the persistent Hub cache. - TIKTOKEN_RS_CACHE_DIR="${HF_HOME}/tiktoken-rs-cache" - : "${HF_HUB_DOWNLOAD_TIMEOUT:=300}" - : "${HF_HUB_ETAG_TIMEOUT:=60}" - if [[ "${VLLM_CI_EXPECTED_GPU_COUNT:-1}" == "0" ]]; then - # CPU-only native jobs intentionally reuse the ROCm wheel. Make that target - # explicit so platform selection does not depend on wheel metadata. - VLLM_TARGET_DEVICE=cpu - export VLLM_TARGET_DEVICE - fi - export TMPDIR VLLM_RPC_BASE_PATH - export TORCHINDUCTOR_CACHE_DIR TRITON_CACHE_DIR VLLM_CACHE_ROOT XDG_CACHE_HOME - export HF_HOME HF_DATASETS_CACHE HF_HUB_DOWNLOAD_TIMEOUT HF_HUB_ETAG_TIMEOUT - export TIKTOKEN_RS_CACHE_DIR - export PYTORCH_ROCM_ARCH="" - - mkdir -p "${TMPDIR}" \ - "${TORCHINDUCTOR_CACHE_DIR}" \ - "${TRITON_CACHE_DIR}" \ - "${VLLM_CACHE_ROOT}" \ - "${XDG_CACHE_HOME}" \ - "${HF_HOME}" \ - "${TIKTOKEN_RS_CACHE_DIR}" \ - "${HF_DATASETS_CACHE}" || return 1 - - echo "Native compile caches: VLLM_CACHE_ROOT=${VLLM_CACHE_ROOT} TORCHINDUCTOR_CACHE_DIR=${TORCHINDUCTOR_CACHE_DIR}" - - if [[ "${VLLM_CI_REQUIRE_PERSISTENT_HF_CACHE:-0}" == "1" ]]; then - if ! command -v findmnt >/dev/null 2>&1; then - echo "findmnt is required to verify the native Hugging Face cache mount" >&2 - return 1 - fi - hf_mount=$(findmnt -n -T "${HF_HOME}" -o TARGET 2>/dev/null || true) - if [[ -z "${hf_mount}" || "${hf_mount}" == "/" ]]; then - echo "Native CI requires a persistent volume mounted at or above ${HF_HOME}" >&2 - return 1 - fi - fi - - if command -v findmnt >/dev/null 2>&1; then - hf_fstype=$(findmnt -n -T "${HF_HOME}" -o FSTYPE 2>/dev/null || true) - fi - if [[ "${hf_fstype}" == nfs || "${hf_fstype}" == nfs4 ]]; then - # Keep hf-xet state local and avoid vectored writes on shared NFS. - export HF_XET_CACHE="${native_root}/cache/hf-xet" - export HF_XET_HIGH_PERFORMANCE=0 - export HF_XET_RECONSTRUCTION_USE_VECTORED_WRITE=0 - mkdir -p "${HF_XET_CACHE}" || return 1 - echo "Configured hf-xet for shared ${hf_fstype} cache at ${HF_HOME}" - fi -} - -check_dpx_gpu_exclusivity() { - local devices=(/dev/dri/renderD*) - local device_id lock_status - - if [[ "${#devices[@]}" -ne 1 || ! -c "${devices[0]}" ]]; then - echo "DPX guard requires exactly one render device; refusing to start tests." >&2 - return 1 - fi - # This queue mounts the same host-local HF cache into every pod on the node. - if ! mountpoint -q "${HF_HOME}"; then - echo "DPX guard requires the shared HF cache mount; refusing to start tests." >&2 - return 1 - fi - device_id=$(stat -Lc '%t-%T' "${devices[0]}") || return 1 - mkdir -p "${HF_HOME}/.vllm-dpx-locks" || return 1 - # Hold the descriptor through workload and teardown; never delete the file. - exec {dpx_gpu_lock_fd}>>"${HF_HOME}/.vllm-dpx-locks/${device_id}.lock" || return 1 - flock -n -E 75 "${dpx_gpu_lock_fd}" && return 0 - lock_status=$? - if [[ "${lock_status}" -eq 75 ]]; then - echo "DPX GPU collision: ${devices[0]} (${device_id}) is already locked by another CI job; refusing to start tests." >&2 - else - echo "DPX GPU lock failed (status ${lock_status}); refusing to start tests." >&2 - fi - return 1 + wheels=("$wheel_dir"/.rocm-wheel/vllm-*.whl) + [[ ${#wheels[@]} == 1 && -f ${wheels[0]} ]] \ + || { echo "Expected exactly one wheel artifact, found: ${wheels[*]}" >&2; exit 1; } + VLLM_PRECOMPILED_WHEEL_LOCATION=${wheels[0]} + VLLM_VERSION_OVERRIDE=$(python3 -P -c \ + 'import importlib.metadata as m; print(m.version("vllm"))') || exit 1 + export VLLM_TARGET_DEVICE=rocm VLLM_VERSION_OVERRIDE VLLM_PRECOMPILED_WHEEL_LOCATION +fi + +# --------------------------------------------------------------------------- +# DPX GPU partition lock +# --------------------------------------------------------------------------- +# DPX nodes expose exactly one GPU partition per pod. A host-wide lock file +# prevents a second job from landing on the same device simultaneously. +_acquire_dpx_lock() { + local dev=(/dev/dri/renderD*) + [[ ${#dev[@]} == 1 ]] \ + || { echo "DPX: expected exactly one render device, found ${#dev[@]}" >&2; return 1; } + mkdir -p "$HF_HOME/.vllm-dpx-locks" + exec 9>>"$HF_HOME/.vllm-dpx-locks/$(stat -Lc %t-%T "${dev[0]}").lock" + flock -n 9 || { echo "DPX GPU collision on ${dev[0]}" >&2; return 1; } +} + +if [[ ${BUILDKITE_AGENT_META_DATA_QUEUE:-} == *dpx* && $gpus != 0 ]]; then + _acquire_dpx_lock || exit 1 +fi + +# --------------------------------------------------------------------------- +# Lifecycle logging +# --------------------------------------------------------------------------- +# Structured events for infra dashboards keyed on [amd-ci-teardown]. +# Usage: _teardown_log [key=val ...] +_teardown_log() { + local event=$1; shift + printf '[amd-ci-teardown] event=%s' "$event" >&2 + (($#)) && printf ' %s' "$@" >&2 + printf '\n' >&2 } -run_native_preflight() { - local expected_gpus="${VLLM_CI_EXPECTED_GPU_COUNT:-1}" - - if [[ ! "${expected_gpus}" =~ ^[0-9]+$ ]]; then - echo "Invalid VLLM_CI_EXPECTED_GPU_COUNT=${expected_gpus}" >&2 - return 1 - fi - - python3 -c "import encodings, importlib.metadata as im, importlib.util as iu; [im.version(d) for d in ('transformers', 'torch', 'ray', 'sympy', 'markupsafe', 'vllm')]; missing=[m for m in ('torch.utils.model_zoo', 'transformers.models.nomic_bert', 'ray.dag', 'sympy.physics', 'markupsafe._speedups') if iu.find_spec(m) is None]; assert not missing, missing" || return 1 - - if [[ "${expected_gpus}" == "0" ]]; then - echo "Native CPU-only AMD job: skipping ROCm device validation" - return 0 - fi - +# --------------------------------------------------------------------------- +# Diagnostics bundle (always collected on failure) +# --------------------------------------------------------------------------- +# Writes to artifacts/amd-gpu-diagnostics//diagnostics.log, which the +# pipeline's artifact_paths uploads even when the step fails. +diagnostics() { + local d=artifacts/amd-gpu-diagnostics/$job + mkdir -p "$d" + { + date + echo "node=${VLLM_CI_K8S_NODE_NAME:-?} pod=${VLLM_CI_K8S_POD_NAME:-?}" + echo "--- amd-smi ---" + timeout 10 amd-smi list + echo "--- rocm-smi pids ---" + timeout 10 rocm-smi --showpids + echo "--- top processes by RSS ---" + ps -eo pid,etime,rss,args --sort=-rss | head -30 + echo "--- disk / memory ---" + df -h /tmp /dev/shm "$HF_HOME" + free -g + echo "--- devices ---" + ls -l /dev/kfd /dev/dri + } >"$d/diagnostics.log" 2>&1 +} + +# --------------------------------------------------------------------------- +# Preflight: verify imports and GPU visibility +# --------------------------------------------------------------------------- +# -P: the checkout (cwd) has a vllm/ source tree that would shadow the installed package. +python3 -P -c "import vllm, torch, transformers, ray" || exit 1 + +if [[ $gpus != 0 ]]; then echo "--- ROCm info" - rocminfo || return 1 - VLLM_CI_EXPECTED_GPU_COUNT="${expected_gpus}" python3 - <<'PY' -import os - + rocminfo || { diagnostics; exit 1; } + python3 - <> "${log_file}" - printf '%s\n' "${title}" >> "${log_file}" - printf '===============================================================================\n' \ - >> "${log_file}" -} - -append_failure_diagnostic_note() { - local log_file=$1 - shift - printf '%s\n' "$@" >> "${log_file}" -} - -run_failure_diagnostic() { - local log_file=$1 - local probe_deadline=$2 - shift 2 - local command_status=0 - local command_timeout_seconds=${amd_diagnostics_command_timeout_seconds} - local remaining_seconds=$((probe_deadline - SECONDS)) - - { - printf '\n$' - printf ' %q' "$@" - printf '\n' - } >> "${log_file}" - - if ! command -v timeout >/dev/null 2>&1; then - echo "[timeout unavailable; bounded diagnostic command skipped]" \ - >> "${log_file}" - return 0 - fi - if [[ "${remaining_seconds}" -le 0 ]]; then - echo "[diagnostic probe budget exhausted; command skipped]" \ - >> "${log_file}" - return 0 - fi - if [[ "${remaining_seconds}" -lt "${command_timeout_seconds}" ]]; then - command_timeout_seconds=${remaining_seconds} - fi - - timeout --kill-after=1s "${command_timeout_seconds}s" "$@" \ - >> "${log_file}" 2>&1 - command_status=$? - if [[ "${command_status}" -ne 0 ]]; then - echo "[diagnostic command exited with status ${command_status}]" \ - >> "${log_file}" - fi - return 0 -} - -upload_failure_diagnostics_artifact() { - local upload_root=$1 upload_path=$2 agent_bin="" - local agent_upload_help="" - local -a upload_options=() - - if [[ -n "${BUILDKITE_BIN_PATH:-}" \ - && -x "${BUILDKITE_BIN_PATH}/buildkite-agent" ]]; then - agent_bin="${BUILDKITE_BIN_PATH}/buildkite-agent" - else - agent_bin=$(command -v buildkite-agent 2>/dev/null || true) - fi - if [[ -z "${agent_bin}" && -x /workspace/buildkite-agent ]]; then - agent_bin=/workspace/buildkite-agent - fi - [[ -n "${agent_bin}" && -n "${BUILDKITE_JOB_ID:-}" ]] || return 1 - command -v timeout >/dev/null 2>&1 || return 1 - - # Older AMD runners predate the literal upload options. A restricted path - # has no glob or delimiter characters, so it is safe with either agent. - if [[ ! "${upload_path}" =~ ^[A-Za-z0-9_][A-Za-z0-9_.-]*(/[A-Za-z0-9_][A-Za-z0-9_.-]*)*$ ]]; then - agent_upload_help=$("${agent_bin}" artifact upload --help 2>&1 || true) - if [[ "${agent_upload_help}" != *"--literal"* \ - || "${agent_upload_help}" != *"--delimiter"* ]]; then - return 1 - fi - upload_options=(--literal --delimiter "") - fi - - ( - cd "${upload_root}" || exit 1 - BUILDKITE_AGENT_DEBUG=false \ - BUILDKITE_AGENT_DEBUG_HTTP=false \ - BUILDKITE_AGENT_TRACE_HTTP=false \ - BUILDKITE_AGENT_LOG_LEVEL=error \ - timeout --kill-after=2s "${amd_diagnostics_upload_timeout_seconds}s" \ - "${agent_bin}" artifact upload \ - "${upload_options[@]}" "./${upload_path}" - ) >/dev/null 2>&1 -} - -append_failure_diagnostic_file() { - local log_file=$1 - local label=$2 - local path=$3 - local command_status=0 - - printf '\n%s:\n' "${label}" >> "${log_file}" - printf '$ cat %q\n' "${path}" >> "${log_file}" - if [[ ! -r "${path}" ]]; then - echo "[file unavailable]" >> "${log_file}" - return 0 - fi - - cat "${path}" >> "${log_file}" 2>&1 - command_status=$? - if [[ "${command_status}" -ne 0 ]]; then - echo "[diagnostic file read exited with status ${command_status}]" \ - >> "${log_file}" - fi - return 0 -} - -decode_mountinfo_path() { - local value=$1 - - value=${value//\\040/ } - value=${value//\\011/$'\t'} - value=${value//\\012/$'\n'} - value=${value//\\134/\\} - printf '%s\n' "${value}" -} - -resolve_current_cgroup_v2_dir() { - local cgroup_path="" - local mount_root="" - local mount_point="" - local relative_path="" - local candidate="" - - cgroup_path=$(awk -F: '$2 == "" { print $3; exit }' \ - /proc/self/cgroup 2>/dev/null) - [[ -n "${cgroup_path}" ]] || return 1 - - while IFS=$'\t' read -r mount_root mount_point; do - mount_root=$(decode_mountinfo_path "${mount_root}") - mount_point=$(decode_mountinfo_path "${mount_point}") - if [[ "${cgroup_path}" == "/" ]]; then - relative_path="" - elif [[ "${mount_root}" == "/" ]]; then - relative_path="${cgroup_path}" - elif [[ "${cgroup_path}" == "${mount_root}" ]]; then - relative_path="" - elif [[ "${cgroup_path}" == "${mount_root}/"* ]]; then - relative_path="/${cgroup_path#"${mount_root}/"}" - else - continue - fi - - candidate="${mount_point%/}${relative_path}" - if [[ -d "${candidate}" ]]; then - printf '%s\n' "${candidate}" - return 0 - fi - done < <( - awk ' - { - for (i = 6; i <= NF; i++) { - if ($i == "-" && $(i + 1) == "cgroup2") { - print $4 "\t" $5 - break - } - } - } - ' /proc/self/mountinfo 2>/dev/null - ) - return 1 -} - -collect_cgroup_files() { - local log_file=$1 - local title=$2 - local cgroup_dir=$3 - shift 3 - local cgroup_file="" - local files_collected=0 - - append_failure_diagnostic_section "${log_file}" "${title}" - if [[ -z "${cgroup_dir}" || ! -d "${cgroup_dir}" ]]; then - append_failure_diagnostic_note "${log_file}" \ - "resolved_cgroup_path=unavailable" - return 0 - fi - - append_failure_diagnostic_note "${log_file}" \ - "resolved_cgroup_path=${cgroup_dir}" - for cgroup_file in "$@"; do - if [[ -r "${cgroup_dir}/${cgroup_file}" ]]; then - append_failure_diagnostic_file "${log_file}" "${cgroup_file}" \ - "${cgroup_dir}/${cgroup_file}" - files_collected=$((files_collected + 1)) - fi - done - if [[ "${files_collected}" -eq 0 ]]; then - append_failure_diagnostic_note "${log_file}" \ - "[no requested cgroup files were readable]" - fi -} - -collect_cgroup_diagnostics() { - local log_file=$1 - local cgroup_dir="" - - append_failure_diagnostic_section "${log_file}" "Cgroup membership" - append_failure_diagnostic_file "${log_file}" "current_process" \ - /proc/self/cgroup - append_failure_diagnostic_file "${log_file}" "pid_namespace_init_process" \ - /proc/1/cgroup - - cgroup_dir=$(resolve_current_cgroup_v2_dir 2>/dev/null || true) - collect_cgroup_files "${log_file}" \ - "Cgroup v2 current-process resource state" "${cgroup_dir}" \ - cgroup.type cgroup.events \ - memory.current memory.peak memory.max memory.high memory.low memory.min \ - memory.oom.group memory.swap.current memory.swap.max \ - memory.events memory.events.local memory.swap.events memory.stat \ - memory.pressure \ - cpu.max cpu.max.burst cpu.weight cpu.stat cpu.pressure \ - cpuset.cpus cpuset.cpus.effective cpuset.mems cpuset.mems.effective \ - pids.current pids.max pids.events pids.events.local \ - io.stat io.max io.weight io.pressure - if [[ -n "${cgroup_dir}" && -r "${cgroup_dir}/memory.events" ]]; then - amd_diagnostics_memory_events_path="${cgroup_dir}/memory.events" - fi -} - -collect_process_diagnostics() { - local log_file=$1 - local probe_deadline=$2 - local shell_proc_dir="/proc/$$" - - append_failure_diagnostic_section "${log_file}" \ - "Runner process constraints and PID-namespace-visible processes" - append_failure_diagnostic_file "${log_file}" "diagnostic_shell_limits" \ - "${shell_proc_dir}/limits" - if command -v awk >/dev/null 2>&1; then - # shellcheck disable=SC2016 # The expression is evaluated by awk. - run_failure_diagnostic "${log_file}" "${probe_deadline}" awk ' - $1 ~ /^(Pid:|PPid:|Threads:|VmPeak:|VmSize:|VmHWM:|VmRSS:|RssAnon:|RssFile:|RssShmem:|Cpus_allowed_list:|Mems_allowed_list:|voluntary_ctxt_switches:|nonvoluntary_ctxt_switches:)$/ { - print - } - ' "${shell_proc_dir}/status" - fi - if command -v nproc >/dev/null 2>&1; then - run_failure_diagnostic "${log_file}" "${probe_deadline}" nproc - fi - run_failure_diagnostic "${log_file}" "${probe_deadline}" \ - /bin/bash -c 'ulimit -a' - if command -v ps >/dev/null 2>&1 && command -v head >/dev/null 2>&1; then - append_failure_diagnostic_note "${log_file}" \ - "process_snapshot=top_100_by_rss" - run_failure_diagnostic "${log_file}" "${probe_deadline}" \ - /bin/bash -c \ - 'ps -eo pid,ppid,stat,etimes,nlwp,pcpu,rss,vsz,comm --sort=-rss | head -n 101' - fi - return 0 -} - -collect_mount_diagnostic() { - local log_file=$1 - local probe_deadline=$2 - local label=$3 - local path=$4 - - printf '\n%s (%s):\n' "${label}" "${path}" >> "${log_file}" - if [[ ! -e "${path}" ]]; then - echo "[path unavailable]" >> "${log_file}" - return 0 - fi - if command -v findmnt >/dev/null 2>&1; then - run_failure_diagnostic "${log_file}" "${probe_deadline}" findmnt \ - -n -T "${path}" -o TARGET,FSTYPE,VFS-OPTIONS - fi - if command -v df >/dev/null 2>&1; then - run_failure_diagnostic "${log_file}" "${probe_deadline}" df -h \ - --output=fstype,size,used,avail,pcent,itotal,iused,iavail,ipcent,target \ - -- "${path}" - fi - return 0 -} - -collect_execution_resource_diagnostics() { - local log_file=$1 - local probe_deadline=$2 - - # free and /proc/meminfo generally describe the node from a pod. Cgroups are - # the live source for this container's resource limits, usage, and events. - collect_cgroup_diagnostics "${log_file}" - collect_process_diagnostics "${log_file}" "${probe_deadline}" - - append_failure_diagnostic_section "${log_file}" \ - "Mount-visible filesystem capacity" - append_failure_diagnostic_note "${log_file}" \ - "These values are filesystem views, not Kubernetes ephemeral-storage quotas." - collect_mount_diagnostic "${log_file}" "${probe_deadline}" \ - "checkout" "${amd_diagnostics_checkout_root}" - if is_native_runtime; then - collect_mount_diagnostic "${log_file}" "${probe_deadline}" \ - "native workspace emptyDir" "${amd_diagnostics_workspace}" - else - collect_mount_diagnostic "${log_file}" "${probe_deadline}" \ - "test workspace (if mounted)" "${amd_diagnostics_workspace}" - fi - collect_mount_diagnostic "${log_file}" "${probe_deadline}" \ - "temporary directory" "${TMPDIR:-/tmp}" - if [[ "${TMPDIR:-/tmp}" != "/tmp" ]]; then - collect_mount_diagnostic "${log_file}" "${probe_deadline}" \ - "system temporary directory" /tmp - fi - if is_native_runtime; then - collect_mount_diagnostic "${log_file}" "${probe_deadline}" \ - "shared-memory emptyDir" /dev/shm - else - collect_mount_diagnostic "${log_file}" "${probe_deadline}" \ - "shared-memory mount" /dev/shm - fi - if [[ -n "${HF_HOME:-}" ]]; then - collect_mount_diagnostic "${log_file}" "${probe_deadline}" \ - "Hugging Face cache" "${HF_HOME}" - fi - return 0 -} - -collect_amd_device_nodes() { - local log_file=$1 - local probe_deadline=$2 - local device="" - local device_count=0 - - for device in /dev/kfd /dev/dri/renderD*; do - if [[ ! -e "${device}" ]]; then - continue - fi - device_count=$((device_count + 1)) - if command -v stat >/dev/null 2>&1; then - run_failure_diagnostic "${log_file}" "${probe_deadline}" stat -Lc \ - '%n type=%F mode=%a owner=%u:%g device=%t:%T' -- "${device}" - else - append_failure_diagnostic_note "${log_file}" "device=${device}" - fi - done - if [[ "${device_count}" -eq 0 ]]; then - append_failure_diagnostic_note "${log_file}" \ - "No /dev/kfd or /dev/dri/renderD* device nodes are visible." - fi - return 0 -} - -collect_rocm_failure_diagnostics() { - local exit_code=$1 - local job_id="${BUILDKITE_JOB_ID:-local}" - local retry_count="${BUILDKITE_RETRY_COUNT:-0}" - local parallel_job="${BUILDKITE_PARALLEL_JOB:-0}" - local diagnostics_relative_path="" - local diagnostics_path="" - local diagnostics_parent="" - local diagnostics_fallback_root="" - local diagnostics_storage="checkout" - local diagnostics_artifact_summary="" - local checkout_real="" - local diagnostics_parent_real="" - local exit_signal="" - local probe_deadline=0 - local runtime="single-node-docker" - local diagnostics_scope="outer-runner-after-test-container-exit" - local identity_label="runner" - local identity_value="${HOSTNAME:-unknown}" - local k8s_pod="unknown" - local k8s_namespace="${amd_diagnostics_k8s_namespace}" - local oom_kill_count="" - local summary_identity_label="Runner" - local -a summary_rows=() - - if [[ "${amd_diagnostics_collected}" == "1" ]]; then - return 0 - fi - amd_diagnostics_collected=1 - - if is_native_runtime; then - runtime="native-kubernetes" - diagnostics_scope="current-container-cgroup-and-namespaces" - if [[ -n "${amd_diagnostics_pod_name}" ]]; then - identity_label="pod" - identity_value="${amd_diagnostics_pod_name}" - k8s_pod="${amd_diagnostics_pod_name}" - else - identity_label="container_hostname" - fi - elif [[ "${amd_diagnostics_execution_mode}" == "multi-node" ]]; then - runtime="multi-node-docker" - fi - if [[ "${k8s_namespace}" == "unknown" \ - && -r /var/run/secrets/kubernetes.io/serviceaccount/namespace ]]; then - k8s_namespace=$( - tr -d '\r\n' \ - < /var/run/secrets/kubernetes.io/serviceaccount/namespace 2>/dev/null - ) - fi - - job_id="${job_id//[^A-Za-z0-9_.-]/_}" - retry_count="${retry_count//[^A-Za-z0-9_.-]/_}" - parallel_job="${parallel_job//[^A-Za-z0-9_.-]/_}" - - if [[ "${exit_code}" -gt 128 && "${exit_code}" -le 192 ]]; then - exit_signal=$(kill -l "$((exit_code - 128))" 2>/dev/null || true) - fi - - # Buildkite keeps the supplied artifact path. Restrict the configurable - # directory to a checkout-relative path so the UI shows a clean artifact key. - if [[ -z "${amd_diagnostics_dir}" \ - || "${amd_diagnostics_dir}" == /* \ - || "${amd_diagnostics_dir}" == *".."* ]]; then - echo "WARNING: ignoring unsafe VLLM_CI_DIAGNOSTICS_DIR" - amd_diagnostics_dir="artifacts/amd-gpu-diagnostics" - fi - diagnostics_relative_path="${amd_diagnostics_dir}/${job_id}/diagnostics.log" - diagnostics_path="${amd_diagnostics_checkout_root}/${diagnostics_relative_path}" - diagnostics_parent=$(dirname "${diagnostics_path}") - checkout_real=$(readlink -m "${amd_diagnostics_checkout_root}" 2>/dev/null || true) - diagnostics_parent_real=$(readlink -m "${diagnostics_parent}" 2>/dev/null || true) - if [[ -z "${checkout_real}" || -z "${diagnostics_parent_real}" \ - || ("${diagnostics_parent_real}" != "${checkout_real}" \ - && "${diagnostics_parent_real}" != "${checkout_real}/"*) ]] \ - || ! mkdir -p "${diagnostics_parent}" \ - || [[ -L "${diagnostics_path}" ]] \ - || ! (set -o noclobber; : > "${diagnostics_path}") 2>/dev/null; then - echo "WARNING: unable to create AMD CI diagnostics in the checkout; using temporary storage." - diagnostics_path="" - if diagnostics_fallback_root=$(mktemp -d -t \ - vllm-amd-diagnostics.XXXXXX 2>/dev/null); then - diagnostics_path="${diagnostics_fallback_root}/${diagnostics_relative_path}" - diagnostics_parent=$(dirname "${diagnostics_path}") - if (umask 077; mkdir -p "${diagnostics_parent}" \ - && (set -o noclobber; : > "${diagnostics_path}")) 2>/dev/null; then - diagnostics_storage="temporary-fallback" - else - rm -rf -- "${diagnostics_fallback_root}" || true - diagnostics_fallback_root="" - diagnostics_path="" - fi - fi - fi - if [[ -z "${diagnostics_path}" ]]; then - echo "WARNING: unable to create AMD CI diagnostics at ${diagnostics_relative_path}." - printf '\nAMD CI failure summary\n' - printf '%-22s | %s\n' \ - "Field" "Value" \ - "----------------------" "-----" \ - "Exit code" "${exit_code}" \ - "Signal" "${exit_signal:-none}" \ - "Runtime" "${runtime}" \ - "Pod/container" "${identity_value}" \ - "Diagnostics artifact" "unavailable: ${diagnostics_relative_path}" - return 0 - fi - - { - echo "AMD CI failure diagnostics" - echo "timestamp_utc=$(date -u +%Y-%m-%dT%H:%M:%SZ)" - echo "exit_code=${exit_code}" - echo "exit_signal=${exit_signal:-none}" - echo "build_url=${BUILDKITE_BUILD_URL:-unknown}" - echo "job_id=${BUILDKITE_JOB_ID:-unknown}" - echo "step_key=${BUILDKITE_STEP_KEY:-unknown}" - echo "step_label=${BUILDKITE_LABEL:-unknown}" - echo "test_group=${amd_diagnostics_test_group}" - echo "retry_count=${retry_count}" - echo "parallel_job=${parallel_job}" - echo "execution_mode=${amd_diagnostics_execution_mode}" - echo "runtime=${runtime}" - echo "diagnostics_scope=${diagnostics_scope}" - echo "expected_gpu_count=${amd_diagnostics_expected_gpu_count}" - echo "probe_budget_seconds=${amd_diagnostics_probe_budget_seconds}" - echo "command_timeout_seconds=${amd_diagnostics_command_timeout_seconds}" - echo "diagnostics_storage=${diagnostics_storage}" - echo "agent_name=${BUILDKITE_AGENT_NAME:-unknown}" - echo "container_hostname=${HOSTNAME:-unknown}" - echo "k8s_pod=${k8s_pod}" - echo "k8s_namespace=${k8s_namespace:-unknown}" - echo "k8s_node=${amd_diagnostics_k8s_node_name:-unknown}" - echo "kernel=$(uname -srmo 2>/dev/null || echo unknown)" - } > "${diagnostics_path}" - - probe_deadline=$((SECONDS + amd_diagnostics_probe_budget_seconds)) - - # Cgroup, process, and mount state is fast and most useful for explaining OOM, - # throttling, PID exhaustion, and emptyDir pressure. Capture it before external - # probes can consume the shared deadline. - collect_execution_resource_diagnostics \ - "${diagnostics_path}" "${probe_deadline}" - - append_failure_diagnostic_section "${diagnostics_path}" \ - "AMD GPU diagnostics (tool-visible devices)" - if [[ "${amd_diagnostics_expected_gpu_count}" == "0" ]]; then - append_failure_diagnostic_note "${diagnostics_path}" \ - "CPU-only job; AMD GPU probes skipped." - else - if [[ "${runtime}" == "native-kubernetes" ]]; then - append_failure_diagnostic_note "${diagnostics_path}" \ - "SMI visibility may be broader than the Kubernetes device allocation." - else - append_failure_diagnostic_note "${diagnostics_path}" \ - "Outer-runner SMI visibility may be broader than the test container." - fi - collect_amd_device_nodes "${diagnostics_path}" "${probe_deadline}" - fi - if [[ "${amd_diagnostics_expected_gpu_count}" != "0" ]] \ - && command -v amd-smi >/dev/null 2>&1; then - run_failure_diagnostic "${diagnostics_path}" "${probe_deadline}" \ - amd-smi version - # Bus data identifies a card within the public node without publishing its - # persistent UUID, serial number, or process list. - run_failure_diagnostic "${diagnostics_path}" "${probe_deadline}" \ - amd-smi static --bus --gpu all - run_failure_diagnostic "${diagnostics_path}" "${probe_deadline}" \ - amd-smi metric --ecc --ecc-blocks --pcie --gpu all - run_failure_diagnostic "${diagnostics_path}" "${probe_deadline}" \ - amd-smi static --ras --gpu all - run_failure_diagnostic "${diagnostics_path}" "${probe_deadline}" \ - amd-smi metric --power --temperature --usage --mem-usage --gpu all - run_failure_diagnostic "${diagnostics_path}" "${probe_deadline}" \ - amd-smi xgmi --link-status --gpu all - elif [[ "${amd_diagnostics_expected_gpu_count}" != "0" ]] \ - && command -v rocm-smi >/dev/null 2>&1; then - run_failure_diagnostic "${diagnostics_path}" "${probe_deadline}" rocm-smi \ - --showbus --showreplaycount --showrasinfo --showpagesinfo - elif [[ "${amd_diagnostics_expected_gpu_count}" != "0" ]]; then - append_failure_diagnostic_note "${diagnostics_path}" \ - "Neither amd-smi nor rocm-smi is available." - fi - - if [[ "${runtime}" == "native-kubernetes" ]]; then - append_failure_diagnostic_section "${diagnostics_path}" \ - "Unauthenticated pod network reachability" - else - append_failure_diagnostic_section "${diagnostics_path}" \ - "Unauthenticated outer-runner network reachability" - fi - if command -v curl >/dev/null 2>&1; then - run_failure_diagnostic "${diagnostics_path}" "${probe_deadline}" curl -q \ - --silent --location --proto '=https' --proto-redir '=https' \ - --max-redirs 3 --output /dev/null --connect-timeout 2 --max-time 4 \ - --write-out 'target=huggingface http_code=%{http_code} dns_done_s=%{time_namelookup} connect_done_s=%{time_connect} tls_done_s=%{time_appconnect} first_byte_s=%{time_starttransfer} total_s=%{time_total}\n' \ - https://huggingface.co/api/models/gpt2 - run_failure_diagnostic "${diagnostics_path}" "${probe_deadline}" curl -q \ - --silent --location --proto '=https' --proto-redir '=https' \ - --max-redirs 3 --output /dev/null --connect-timeout 2 --max-time 4 \ - --write-out 'target=github_git_smart_http http_code=%{http_code} dns_done_s=%{time_namelookup} connect_done_s=%{time_connect} tls_done_s=%{time_appconnect} first_byte_s=%{time_starttransfer} total_s=%{time_total}\n' \ - 'https://github.com/vllm-project/ci-infra.git/info/refs?service=git-upload-pack' - else - append_failure_diagnostic_note "${diagnostics_path}" \ - "curl unavailable; reachability probes skipped." - fi - - if [[ "${runtime}" == "native-kubernetes" \ - && -n "${amd_diagnostics_memory_events_path}" \ - && -r "${amd_diagnostics_memory_events_path}" ]]; then - oom_kill_count=$( - awk '$1 == "oom_kill" { print $2; exit }' \ - "${amd_diagnostics_memory_events_path}" 2>/dev/null - ) - if [[ ! "${oom_kill_count}" =~ ^[0-9]+$ ]]; then - oom_kill_count="" - fi - fi - - case "${identity_label}" in - pod) - summary_identity_label="Kubernetes pod" - ;; - container_hostname) - summary_identity_label="Container hostname" - ;; - esac - - summary_rows=( - "Field" "Value" - "----------------------" "-----" - "Exit code" "${exit_code}" - "Signal" "${exit_signal:-none}" - "Runtime" "${runtime}" - "${summary_identity_label}" "${identity_value}" - ) - if [[ -n "${amd_diagnostics_k8s_node_name}" ]]; then - summary_rows+=("Kubernetes node" "${amd_diagnostics_k8s_node_name}") - fi - if [[ -n "${oom_kill_count}" ]]; then - summary_rows+=("Cgroup OOM kills" "${oom_kill_count}") - fi - diagnostics_artifact_summary="${diagnostics_relative_path}" - if [[ -n "${diagnostics_fallback_root}" ]]; then - if upload_failure_diagnostics_artifact \ - "${diagnostics_fallback_root}" "${diagnostics_relative_path}"; then - echo "Uploaded AMD CI diagnostics artifact from temporary storage: ${diagnostics_relative_path}" - diagnostics_artifact_summary="${diagnostics_relative_path} (direct upload)" - else - echo "WARNING: failed to upload temporary AMD CI diagnostics artifact: ${diagnostics_relative_path}" - echo "--- AMD CI diagnostics (artifact upload failed)" - cat "${diagnostics_path}" || true - diagnostics_artifact_summary="job log only: direct upload failed" - fi - rm -rf -- "${diagnostics_fallback_root}" || \ - echo "WARNING: unable to remove temporary AMD CI diagnostics storage." - fi - summary_rows+=("Diagnostics artifact" "${diagnostics_artifact_summary}") - - printf '\nAMD CI failure summary\n' - printf '%-22s | %s\n' "${summary_rows[@]}" - - return 0 -} - -handle_pytest_exit() { - local exit_code=$1 - if [ "$exit_code" -eq 5 ]; then - echo "Pytest exit code 5 (no tests collected) - treating as success." - exit 0 - fi - exit "$exit_code" -} - -############################################################################### -# Pytest marker/keyword re-quoting -# -# When commands are passed through Buildkite -> shell -> $* -> bash -c, -# quotes around multi-word pytest -m/-k expressions get stripped: -# pytest -v -s -m 'not cpu_test' v1/core -# becomes: -# pytest -v -s -m not cpu_test v1/core -# -# pytest then interprets "cpu_test" as a file path, not part of the marker. -# -# This function detects unquoted expressions after -m/-k and re-quotes them -# by collecting tokens until a recognizable boundary is reached: -# - test path (contains '/') -# - test file (ends with '.py') -# - another pytest flag (--xxx or -x single-char flags) -# - command separator (&& || ; |) -# - environment variable assignment (FOO=bar) -# -# Single-word markers (e.g. -m cpu_test, -m hybrid_model) pass through -# unquoted since they have no spaces and work fine. -# -# Already-quoted expressions (containing literal single quotes) are passed -# through untouched to avoid double-quoting well-formed shell fragments. -# -# NOTE: This ONLY fixes -m/-k flags. It cannot recover arbitrary inner -# double-quotes stripped by the calling shell (see header comment). -# Use VLLM_TEST_COMMANDS to avoid the problem entirely. -############################################################################### -re_quote_pytest_markers() { - local input="$1" - local output="" - local collecting=false - local marker_buf="" - - # Strip backslash-newline continuations, then flatten remaining newlines - local flat="${input//$'\\\n'/ }" - flat="${flat//$'\n'/ }" - - # Disable globbing to prevent *.py etc. from expanding during read -ra - local restore_glob - restore_glob="$(shopt -p -o noglob 2>/dev/null || true)" - set -o noglob - local -a words - read -ra words <<< "$flat" - eval "$restore_glob" - - for word in "${words[@]}"; do - if $collecting; then - # If the token we're about to collect already contains a literal - # single quote, the expression was already quoted upstream. - # Flush and stop collecting. - if [[ "$word" == *"'"* ]]; then - if [[ -n "$marker_buf" ]]; then - # Should not normally happen (partial buf + quote), flush raw - output+="${marker_buf} " - marker_buf="" - fi - output+="${word} " - collecting=false - continue - fi - - local is_boundary=false - case "$word" in - # Line-continuation artifact - "\\") - is_boundary=true ;; - # Command separators - "&&"|"||"|";"|"|") - is_boundary=true ;; - # Long flags (--ignore, --shard-id, etc.) - --*) - is_boundary=true ;; - # Short flags (-v, -s, -x, etc.) but NOT negative marker tokens - # like "not" which don't start with "-". Also skip -k/-m which - # would start a new marker (handled below). - -[a-zA-Z]) - is_boundary=true ;; - # Test path (contains /) - */*) - is_boundary=true ;; - # Test file (ends with .py, possibly with ::method) - *.py|*.py::*) - is_boundary=true ;; - # Environment variable assignment preceding a command (FOO=bar) - *=*) - # Only treat as boundary if it looks like VAR=value, not - # pytest filter expressions like num_gpus=2 inside markers - if [[ "$word" =~ ^[A-Z_][A-Z0-9_]*= ]]; then - is_boundary=true - fi - ;; - esac - - if $is_boundary; then - # Strip surrounding double quotes if present (from upstream - # single-to-double conversion); without this, wrapping below - # would produce '"expr"' with literal double-quote characters. - if [[ "$marker_buf" == '"'*'"' ]]; then - marker_buf="${marker_buf#\"}" - marker_buf="${marker_buf%\"}" - fi - # Flush the collected marker expression - if [[ "$marker_buf" == *" "* || "$marker_buf" == *"("* ]]; then - output+="'${marker_buf}' " - else - output+="${marker_buf} " - fi - collecting=false - marker_buf="" - # Check if this boundary word itself starts a new -m/-k - if [[ "$word" == "-m" || "$word" == "-k" ]]; then - output+="${word} " - collecting=true - # Drop stray backslash tokens silently - elif [[ "$word" == "\\" ]]; then - : - else - output+="${word} " - fi - else - # Accumulate into marker buffer - if [[ -n "$marker_buf" ]]; then - marker_buf+=" ${word}" - else - marker_buf="${word}" - fi - fi - elif [[ "$word" == "-m" || "$word" == "-k" ]]; then - output+="${word} " - collecting=true - marker_buf="" - else - output+="${word} " - fi - done - - # Flush any trailing marker expression (marker at end of command) - if $collecting && [[ -n "$marker_buf" ]]; then - # Strip surrounding double quotes (see mid-stream flush comment) - if [[ "$marker_buf" == '"'*'"' ]]; then - marker_buf="${marker_buf#\"}" - marker_buf="${marker_buf%\"}" - fi - if [[ "$marker_buf" == *" "* || "$marker_buf" == *"("* ]]; then - output+="'${marker_buf}'" - else - output+="${marker_buf}" - fi - fi - - echo "${output% }" -} - -# shellcheck disable=SC2317 # Called indirectly by the EXIT trap. -handle_amd_runner_exit() { - local exit_code=${1:-$?} - if [[ "${exit_code}" -ne 0 ]]; then - collect_rocm_failure_diagnostics "${exit_code}" - fi - return 0 -} - -# Catch both test failures and wrapper/setup failures. Runtime-specific cleanup -# traps below replace this trap and call the same handler after cleanup. -trap handle_amd_runner_exit EXIT - -############################################################################### -# Main -############################################################################### - -if is_native_runtime; then - echo "--- Native in-pod ROCm CI (AMD_CI_RUNTIME=${AMD_CI_RUNTIME:-unset}, NATIVE_CI=${NATIVE_CI:-unset})" - artifact_work_dir="" - - # shellcheck disable=SC2317 # Called indirectly by the EXIT trap. - cleanup_native_workspace() { - local exit_code=$? - if [[ -n "${artifact_work_dir}" ]]; then - rm -rf "${artifact_work_dir}" - fi - handle_amd_runner_exit "${exit_code}" - } - trap cleanup_native_workspace EXIT - - if [[ -n "${VLLM_TEST_COMMANDS:-}" ]]; then - commands="${VLLM_TEST_COMMANDS}" - commands_source="env" - else - commands="$*" - commands_source="argv" - if [[ -z "$commands" ]]; then - echo "Error: No test commands provided for native CI." >&2 - exit 1 - fi - fi - - if [[ "$commands_source" == "argv" ]]; then - commands=$(re_quote_pytest_markers "$commands") - fi - - if is_multi_node "$commands"; then - echo "Native CI does not support multi-node jobs yet." - exit 1 - fi - - if ! initialize_native_environment; then - echo "Failed to initialize the native test environment" - exit 1 - fi - if [[ "${BUILDKITE_AGENT_META_DATA_QUEUE:-}" == *dpx* \ - && "${VLLM_CI_EXPECTED_GPU_COUNT:-1}" != "0" ]]; then - check_dpx_gpu_exclusivity || exit 1 - fi - if [[ "${commands}" == *python_only_compile.sh* ]]; then - # This no-GPU job validates the ROCm precompiled/editable install path, - # rather than CPU runtime platform selection. - VLLM_TARGET_DEVICE=rocm - export VLLM_TARGET_DEVICE - fi - if ! prepare_native_workspace "${commands}"; then - echo "Failed to prepare native test workspace" - exit 1 - fi - - export PYTHONPATH="${VLLM_CI_WORKSPACE:-/vllm-workspace}" - - echo "Native test commands: $commands" - run_native_preflight || exit 1 - echo "--- Test log" - # Keep AMD CI orchestration variables out of vLLM's runtime environment. - clear_ci_orchestration_env - /bin/bash -o pipefail -c "${commands}" - handle_pytest_exit "$?" -fi - -# --- GPU initialization for legacy Docker execution --- -echo "--- ROCm info" -rocminfo - -# --- Docker status --- -report_docker_usage - -# --- Pull test image --- -echo "--- Pulling container" -image_name="${VLLM_CI_FALLBACK_IMAGE:-rocm/vllm-ci:${BUILDKITE_COMMIT:-local}}" -artifact_work_dir="" -container_name="rocm_${BUILDKITE_COMMIT}_$(tr -dc A-Za-z0-9 < /dev/urandom | head -c 10; echo)" - -# shellcheck disable=SC2317 # Called indirectly by the EXIT trap. -remove_docker_container() { - local exit_code=$? - if docker container inspect "${container_name}" >/dev/null 2>&1; then - docker rm -f "${container_name}" || true - fi - if [[ "${VLLM_CI_REMOVE_TEST_IMAGE:-0}" == "1" ]]; then - docker image rm -f "${image_name}" || true - else - # Keep images by default so later jobs on the same AMD node can reuse layers. - echo "Keeping ROCm test image locally: ${image_name}" - fi - if [[ -n "${artifact_work_dir}" ]]; then - rm -rf "${artifact_work_dir}" - fi - handle_amd_runner_exit "${exit_code}" -} -trap remove_docker_container EXIT - -# python_only_compile.sh runs `python setup.py develop` and needs the full repo tree -# under /vllm-workspace (Dockerfile.rocm test stage: mkdir src && mv vllm). -# The ROCm wheel artifact tarball only ships a thin tree (tests, etc.), so -# artifact images cannot satisfy that test — use the full rocm/vllm-ci image. -_cmd_probe="${VLLM_TEST_COMMANDS:-}" -if [[ -z "${_cmd_probe}" ]]; then - _cmd_probe="$*" -fi -if [[ "${VLLM_CI_USE_ARTIFACTS:-0}" == "1" && "${_cmd_probe}" == *python_only_compile.sh* ]]; then - echo "INFO: disabling VLLM_CI_USE_ARTIFACTS for python_only_compile (requires full /vllm-workspace tree)" - export VLLM_CI_USE_ARTIFACTS=0 -fi -unset -v _cmd_probe - -if ! prepare_artifact_image; then - echo "Using full ROCm CI image: ${image_name}" - docker pull "${image_name}" || exit 1 -fi - -# --- Prepare commands --- -echo "--- Running container" - -HF_CACHE="$(realpath ~)/huggingface" -mkdir -p "${HF_CACHE}" -HF_MOUNT="/root/.cache/huggingface" - -# Hugging Face Hub defaults to 10s request/download timeouts, while the ROCm -# CI image currently raises downloads to 60s. AMD model-test jobs routinely -# start from a cold or partially-populated shared cache, and the 60s read cap -# has still timed out before pytest reached the vLLM behavior under test. -# Keep the CI default explicit and overridable from the Buildkite environment. -: "${HF_HUB_DOWNLOAD_TIMEOUT:=300}" -: "${HF_HUB_ETAG_TIMEOUT:=60}" - -# ---- Command source selection ---- -# Prefer VLLM_TEST_COMMANDS (preserves all inner quoting intact). -# Fall back to $* for backward compatibility, but warn that inner -# double-quotes will have been stripped by the calling shell. -if [[ -n "${VLLM_TEST_COMMANDS:-}" ]]; then - commands="${VLLM_TEST_COMMANDS}" - commands_source="env" - echo "Commands sourced from VLLM_TEST_COMMANDS (quoting preserved)" -else - commands="$*" - commands_source="argv" - if [[ -z "$commands" ]]; then - echo "Error: No test commands provided." >&2 - echo "Usage:" >&2 - echo " Preferred: VLLM_TEST_COMMANDS='...' bash $0" >&2 - echo " Legacy: bash $0 \"commands here\"" >&2 - exit 1 - fi - echo "Commands sourced from positional args (legacy mode)" - echo "WARNING: Inner double-quotes in the command string may have been" - echo " stripped by the calling shell. If you see syntax errors, switch to:" - echo " export VLLM_TEST_COMMANDS='your commands here'" - echo " bash $0" -fi - -echo "Raw commands: $commands" - -# Only try to repair stripped pytest -m/-k quoting in legacy argv mode. -# VLLM_TEST_COMMANDS preserves inner quoting already, and re-quoting that path -# can corrupt embedded echo strings or otherwise well-formed shell fragments. -if [[ "$commands_source" == "argv" ]]; then - commands=$(re_quote_pytest_markers "$commands") - echo "After re-quoting: $commands" -else - echo "Skipping re-quoting for VLLM_TEST_COMMANDS input" -fi - -echo "Final commands: $commands" - -# Match native CPU jobs even when the container can see AMD devices. -cpu_platform_env=() -if [[ "${VLLM_CI_EXPECTED_GPU_COUNT:-1}" == "0" \ - && "$commands" != *python_only_compile.sh* ]]; then - cpu_platform_env=(-e "VLLM_TARGET_DEVICE=cpu") -fi - -standalone_merge_base_env=() -if [[ "$commands" == *python_only_compile.sh* ]]; then - # The ROCm test image often ships /vllm-workspace without .git. Resolve the - # wheels.vllm.ai commit from the agent checkout for this test only. - vllm_standalone_merge_base="" - checkout="${BUILDKITE_BUILD_CHECKOUT_PATH:-}" - if [[ -z "${checkout}" || ! -d "${checkout}" ]]; then - checkout="." - fi - # Pass safe.directory per-command because Buildkite uses mixed user IDs. - if git -c "safe.directory=${checkout}" -C "${checkout}" rev-parse --is-inside-work-tree >/dev/null 2>&1; then - vllm_standalone_merge_base="$( - git -c "safe.directory=${checkout}" -C "${checkout}" merge-base HEAD origin/main 2>/dev/null || true - )" - fi - if [[ -z "${vllm_standalone_merge_base}" ]]; then - vllm_standalone_merge_base="${BUILDKITE_COMMIT:-}" - fi - echo "INFO: passing CI_STANDALONE_MERGE_BASE into container: ${vllm_standalone_merge_base}" - standalone_merge_base_env=(-e "CI_STANDALONE_MERGE_BASE=${vllm_standalone_merge_base}") -fi - -MYPYTHONPATH="/vllm-workspace" - -container_job_id="${BUILDKITE_JOB_ID:-${BUILDKITE_PARALLEL_JOB:-0}}" -container_job_id="${container_job_id//[^A-Za-z0-9_.-]/_}" -container_job_id_short="${container_job_id:0:8}" -CONTAINER_TMPDIR="/tmp/vllm-${container_job_id_short}" -CONTAINER_CACHE_ROOT="/tmp/vllm-buildkite-${container_job_id}/cache" -CONTAINER_PREFLIGHT="mkdir -p \"\$TMPDIR\" \"\$TIKTOKEN_RS_CACHE_DIR\" \"\$TORCHINDUCTOR_CACHE_DIR\" \"\$TRITON_CACHE_DIR\" \"\$VLLM_CACHE_ROOT\" \"\$XDG_CACHE_HOME\" && python -c \"import encodings, importlib.metadata as im, importlib.util as iu; [im.version(d) for d in ('transformers', 'torch', 'ray', 'sympy', 'markupsafe', 'vllm')]; missing=[m for m in ('torch.utils.model_zoo', 'transformers.models.nomic_bert', 'ray.dag', 'sympy.physics', 'markupsafe._speedups') if iu.find_spec(m) is None]; assert not missing, missing\"" - -# Verify GPU access -render_gid=$(getent group render | cut -d: -f3) -if [[ -z "$render_gid" ]]; then - echo "Error: 'render' group not found. This is required for GPU access." >&2 - exit 1 +n = torch.cuda.device_count() +assert torch.version.hip and torch.cuda.is_available() and n == $gpus, \ + f"preflight: expected $gpus ROCm GPU(s), found {n}" +EOF fi -# --- RDMA device passthrough (conditional) --- -# If the host has RDMA devices, pass them through so tests like -# test_moriio_connector can access ibverbs. On hosts without RDMA -# hardware the tests will gracefully skip via _rdma_available(). -RDMA_FLAGS="" -if [ -d /dev/infiniband ]; then - echo "RDMA devices detected on host, enabling passthrough" - RDMA_FLAGS="--device /dev/infiniband --cap-add=IPC_LOCK" +# --------------------------------------------------------------------------- +# Run +# --------------------------------------------------------------------------- +# Scrub orchestration vars before handing control to test commands. +# (VLLM_CI_* that are test knobs are intentionally left — see tests/ci_envs.py) +unset VLLM_TEST_COMMANDS VLLM_CI_EXPECTED_GPU_COUNT VLLM_CI_DIAGNOSTICS_DIR + +echo "Test commands: $commands" +echo "--- Test log" +_teardown_log workload_started "timeout_s=${CONTAINER_TIMEOUT_S:-0}" + +rc=0 +if [[ ${CONTAINER_TIMEOUT_S:-0} -gt 0 ]]; then + # timeout fires TERM, then KILL after 10 s — mirrors the old docker run path. + # Not --foreground: the whole process group (pytest, engine workers) must stop, not just bash. + # That group is in the background of the job's PTY, where reading the terminal stops it + # (SIGTTIN: apt, transformers' trust_remote_code prompt), so stdin is /dev/null. + timeout --verbose --signal=TERM --kill-after=10s \ + "${CONTAINER_TIMEOUT_S}s" bash -o pipefail -c "$commands" tags. +# Copied from ECR (publish.sh). The default stack (rocm) also owns the plain :latest / :v tags. + +# pin_rocm — @, so what was checked is what is published +pin_rocm() { + local d + d=$(docker buildx imagetools inspect "$1" | awk '$1 == "Digest:" { print $2; exit }') + [[ $d =~ ^sha256:[0-9a-f]{64}$ ]] || { echo "cannot resolve the digest of $1" >&2; return 1; } + echo "$1@$d" +} publish_rocm_stack() { - local base_dockerfile="$1" default="$2" suffix - ( - # shellcheck source=.buildkite/scripts/rocm/stack.sh - source .buildkite/scripts/rocm/stack.sh "$base_dockerfile" - local suffixes=("-${ROCM_STACK_VARIANT}") - [[ "$default" == "1" ]] && suffixes=("" "-${ROCM_STACK_VARIANT}") - docker pull "$ROCM_STACK_ECR_IMAGE" - docker pull "$ROCM_STACK_ECR_BASE" - for suffix in "${suffixes[@]}"; do - for tag in "latest${suffix}" "v${RELEASE_VERSION}${suffix}"; do - docker tag "$ROCM_STACK_ECR_IMAGE" "vllm/vllm-openai-rocm:${tag}" - docker tag "$ROCM_STACK_ECR_BASE" "vllm/vllm-openai-rocm:${tag}-base" - docker push "vllm/vllm-openai-rocm:${tag}" - docker push "vllm/vllm-openai-rocm:${tag}-base" - done - done - ) + local v s sfx img base ver ecr=public.ecr.aws/q9t5s3a7/vllm-release-repo:${COMMIT} hub=vllm/vllm-openai-rocm + v=$(buildkite-agent meta-data get "rocm-$1-variant") + img=$(pin_rocm "$ecr-$v") + base=$(pin_rocm "$ecr-$v-base") + # Another build of this commit (e.g. a branch push) may have retagged it with its own version. + ver=$(docker buildx imagetools inspect "$img" \ + --format '{{index .Image.Config.Labels "org.opencontainers.image.version"}}') + if [[ ${ver%%+*} != "$RELEASE_VERSION" ]]; then + echo "ERROR: $img is version '$ver', not $RELEASE_VERSION" >&2 + exit 1 + fi + sfx=("-$v"); if [[ $1 == rocm ]]; then sfx+=(""); fi + for s in "${sfx[@]}"; do + docker buildx imagetools create -t "$hub:latest$s" -t "$hub:v${RELEASE_VERSION}$s" "$img" + docker buildx imagetools create -t "$hub:latest$s-base" -t "$hub:v${RELEASE_VERSION}$s-base" "$base" + done } if target_enabled rocm; then - publish_rocm_stack docker/Dockerfile.rocm_base 1 -fi - -if target_enabled rocm72; then - publish_rocm_stack docker/Dockerfile.rocm_72_base 0 + publish_rocm_stack rocm fi # ---- XPU ---- diff --git a/.buildkite/scripts/push-nightly-builds-rocm.sh b/.buildkite/scripts/push-nightly-builds-rocm.sh deleted file mode 100644 index 8b120883eff4..000000000000 --- a/.buildkite/scripts/push-nightly-builds-rocm.sh +++ /dev/null @@ -1,86 +0,0 @@ -#!/bin/bash -# SPDX-License-Identifier: Apache-2.0 -# SPDX-FileCopyrightText: Copyright contributors to the vLLM project -# -# Push a ROCm stack's nightly base image and nightly image from ECR to Docker Hub -# under vllm/vllm-openai-rocm, as base-nightly-, nightly- and their -# - forms, then prune old nightly tags. With --default, also push the plain -# base-nightly and nightly tags for the same images. -# Run when NIGHTLY=1 after the stack's release image has been pushed to ECR. -# -# Usage: push-nightly-builds-rocm.sh [--default] -# e.g. "docker/Dockerfile.rocm_base --default" (ROCm 10.0, also :nightly) -# and "docker/Dockerfile.rocm_72_base" (ROCm 7.2, :nightly-rocm72). -# -# Local testing (no push to Docker Hub): -# BUILDKITE_COMMIT= DRY_RUN=1 \ -# bash .buildkite/scripts/push-nightly-builds-rocm.sh docker/Dockerfile.rocm_base --default -# Requires: AWS CLI configured (for ECR public login), Docker. For full run: Docker Hub login. - -set -euxo pipefail - -usage() { - echo "Usage: $0 [--default]" >&2 -} - -PUSH_DEFAULT=0 -case "$#" in - 1) ;; - 2) - if [[ "$2" != "--default" ]]; then - usage - exit 2 - fi - PUSH_DEFAULT=1 - ;; - *) - usage - exit 2 - ;; -esac - -# Use BUILDKITE_COMMIT from env (required; set to a commit that has the image in ECR for local test) -BUILDKITE_COMMIT="${BUILDKITE_COMMIT:?Set BUILDKITE_COMMIT to the commit SHA that has the image in ECR (e.g. from a previous release pipeline run)}" -DRY_RUN="${DRY_RUN:-0}" - -# shellcheck source=.buildkite/scripts/rocm/stack.sh -source "$(dirname "${BASH_SOURCE[0]}")/rocm/stack.sh" "$1" -DOCKERHUB_REPO="vllm/vllm-openai-rocm" -VARIANT="$ROCM_STACK_VARIANT" - -BASE_TAGS=("base-nightly-${VARIANT}" "base-nightly-${VARIANT}-${BUILDKITE_COMMIT}") -TAGS=("nightly-${VARIANT}" "nightly-${VARIANT}-${BUILDKITE_COMMIT}") -if [[ "$PUSH_DEFAULT" == "1" ]]; then - BASE_TAGS+=("base-nightly" "base-nightly-${BUILDKITE_COMMIT}") - TAGS+=("nightly" "nightly-${BUILDKITE_COMMIT}") -fi - -echo "Pushing base image $ROCM_STACK_ECR_BASE as: ${BASE_TAGS[*]}" -echo "Pushing release image $ROCM_STACK_ECR_IMAGE as: ${TAGS[*]}" - -# Login to ECR and pull the stack's images -aws ecr-public get-login-password --region us-east-1 | docker login --username AWS --password-stdin public.ecr.aws/q9t5s3a7 -docker pull "$ROCM_STACK_ECR_BASE" -docker pull "$ROCM_STACK_ECR_IMAGE" - -for tag in "${BASE_TAGS[@]}"; do docker tag "$ROCM_STACK_ECR_BASE" "$DOCKERHUB_REPO:$tag"; done -for tag in "${TAGS[@]}"; do docker tag "$ROCM_STACK_ECR_IMAGE" "$DOCKERHUB_REPO:$tag"; done - -if [[ "$DRY_RUN" == "1" ]]; then - echo "[DRY_RUN] Local tags created. Exiting without push." - exit 0 -fi - -# Push to Docker Hub (docker-login plugin runs before this step in CI) -for tag in "${BASE_TAGS[@]}" "${TAGS[@]}"; do docker push "$DOCKERHUB_REPO:$tag"; done -echo "Pushed $DOCKERHUB_REPO: ${BASE_TAGS[*]} ${TAGS[*]}" - -# Keep only the last 14 builds of each tag family. The plain nightly- families -# exclude the variant ones sharing their prefix; commit SHAs never start with "rocm". -CLEANUP=.buildkite/scripts/cleanup-nightly-builds.sh -bash "$CLEANUP" "nightly-${VARIANT}-" "$DOCKERHUB_REPO" -bash "$CLEANUP" "base-nightly-${VARIANT}-" "$DOCKERHUB_REPO" -if [[ "$PUSH_DEFAULT" == "1" ]]; then - bash "$CLEANUP" "nightly-" "$DOCKERHUB_REPO" "nightly-rocm" - bash "$CLEANUP" "base-nightly-" "$DOCKERHUB_REPO" "base-nightly-rocm" -fi diff --git a/.buildkite/scripts/rocm/aiter-nightly-overlay.sh b/.buildkite/scripts/rocm/aiter-nightly-overlay.sh deleted file mode 100755 index bed4a3e09aec..000000000000 --- a/.buildkite/scripts/rocm/aiter-nightly-overlay.sh +++ /dev/null @@ -1,116 +0,0 @@ -#!/bin/bash -# Install the AITER nightly wheel over this build's ci_base. -# -# Runs in CI when AITER_NIGHTLY=1, once the stock ci_base it builds on is -# published. In order: -# 1. Select the newest nightly wheel that fits the stock ci_base -# (select_aiter_nightly_wheel.py, beside this script, run inside that image) -# 2. Build a new image: pip install that wheel over the stock ci_base -# 3. Push the new image as this build's ci_base-build tag, replacing the stock -# one, so later steps and native jobs use it; record its digest -# -# Buildkite meta-data, a key-value store shared by every job in this build: -# reads rocm-ci-base-image the stock ci_base -# r/w aiter-nightly-wheel-url the wheel step 1 chose; a retry reads it -# back and installs that same wheel -# writes aiter-nightly-ci-base the new image as tag@digest; the vLLM -# image build uses it as its ci_base -# -# Exit status: -# 10 no wheel fits this ci_base -# 11 the wheel does not install or its modules do not load -# Anything else is unexpected and passed through as is. -# Buildkite retries this step once on exit status 1 or 2, which it treats as -# transient Docker/registry failures (amd_infra_retry in the template). -# 10 and 11 sit outside that range: they are real AITER failures, and a retry -# would only fail the same way. - -set -euo pipefail - -SELECT_WHEEL="$(dirname "${BASH_SOURCE[0]}")/select_aiter_nightly_wheel.py" -BUILD_TAG="rocm/vllm-dev:ci_base-build-${BUILDKITE_BUILD_ID:?BUILDKITE_BUILD_ID is required}" - -# Scratch space for the build context and captured errors. The trap deletes it -# however the script exits: success, fail, or set -e. -scratch="$(mktemp -d)" -trap 'rm -rf "${scratch}"' EXIT -errors="${scratch}/errors" - -fail() { - local status="$1" title="$2" detail="$3" - echo "+++ :x: AITER nightly: ${title}" - echo "${detail}" >&2 - # shellcheck disable=SC2016 # the backticks are a literal markdown fence - printf ':x: **AITER nightly: %s**\n\n```\n%s\n```\n' "${title}" "${detail}" \ - | buildkite-agent annotate --style error --context aiter-nightly - exit "${status}" -} - -# Set by ensure-ci-base-amd; this step's depends_on guarantees it is there. -ci_base="$(buildkite-agent meta-data get rocm-ci-base-image)" -# Set below on the first attempt. Meta-data is per build, so a retry reuses -# that wheel, not a newer nightly. -pinned="$(buildkite-agent meta-data get aiter-nightly-wheel-url --default "")" - -echo "--- :docker: Pulling ${ci_base}" -docker pull "${ci_base}" - -echo "--- :mag: Selecting the AITER nightly for ${ci_base}" -set +e -out="$(docker run --rm -i --entrypoint python3 "${ci_base}" \ - -P - ${pinned:+--wheel-url "${pinned}"} \ - < "${SELECT_WHEEL}" 2> "${errors}")" -status=$? -set -e -cat "${errors}" >&2 -case "${status}" in - 0) ;; - 10) fail 10 "no wheel for this image" "${out}" ;; - *) fail "${status}" "could not select a wheel" "$(tail -n 20 "${errors}")" ;; -esac -# The first line is the wheel's URL; the rest, the annotation naming it. -wheel_url="$(head -n 1 <<< "${out}")" -note="$(tail -n +2 <<< "${out}")" -buildkite-agent meta-data set aiter-nightly-wheel-url "${wheel_url}" - -cat > "${scratch}/Dockerfile" <<'EOF' -ARG CI_BASE_IMAGE -FROM ${CI_BASE_IMAGE} -ARG AITER_WHEEL_URL -RUN python3 -m pip install --no-cache-dir "$AITER_WHEEL_URL" -# A wheel built against a newer glibc or libstdc++ than ci_base installs fine -# but fails every test that loads it; fail here instead. -RUN aiter="$(python3 -c 'import importlib.util as u; print(u.find_spec("aiter").origin)')" \ - && ! find "${aiter%/*}" -name '*.so' -exec ldd {} + 2>&1 | grep "version .* not found" -EOF - -echo "--- :docker: Installing ${wheel_url##*/}" -if ! docker buildx build \ - --builder default \ - --provenance=false \ - --progress plain \ - --build-arg "CI_BASE_IMAGE=${ci_base}" \ - --build-arg "AITER_WHEEL_URL=${wheel_url}" \ - -t "${BUILD_TAG}" \ - "${scratch}"; then - fail 11 "the wheel does not install" \ - "Installing or loading ${wheel_url##*/} over ${ci_base} failed; see this step's log." -fi - -docker push "${BUILD_TAG}" -# Handed off as tag@digest: build-test-image.sh rejects a ci_base that is not -# digest-pinned. -digest="$(docker buildx imagetools inspect "${BUILD_TAG}" | awk '$1 == "Digest:" { print $2; exit }')" -if [[ ! "${digest}" =~ ^sha256:[0-9a-f]{64}$ ]]; then - echo "Could not resolve the pushed digest of ${BUILD_TAG}" >&2 - exit 1 -fi -# The vLLM image build uses this as its ci_base. -buildkite-agent meta-data set aiter-nightly-ci-base "${BUILD_TAG}@${digest}" -# The annotation starts with :warning: when the wheel is stale. -style=info -if [[ "${note}" == :warning:* ]]; then - style=warning -fi -echo "${note}" | buildkite-agent annotate --style "${style}" --context aiter-nightly -echo "--- :white_check_mark: ${BUILD_TAG}@${digest}" diff --git a/.buildkite/scripts/rocm/build-ci-base.sh b/.buildkite/scripts/rocm/build-ci-base.sh deleted file mode 100755 index 7d4eb381bc27..000000000000 --- a/.buildkite/scripts/rocm/build-ci-base.sh +++ /dev/null @@ -1,48 +0,0 @@ -#!/usr/bin/env bash -# Build the ROCm ci_base image, optionally from a freshly rebuilt ROCm base. - -set -euo pipefail - -metadata_get() { - local key="$1" - if command -v buildkite-agent >/dev/null 2>&1; then - buildkite-agent meta-data get "${key}" 2>/dev/null || true - fi -} - -main() { - local base_refreshed="" - local selected_base="" - - # The content identity below describes this checkout. Do not let legacy - # external pipeline settings switch the build to a remote source tree. - export REMOTE_VLLM=0 - unset VLLM_BRANCH - - base_refreshed="$(metadata_get rocm-base-refresh)" - selected_base="$(metadata_get rocm-base-image)" - if [[ -n "${selected_base}" ]]; then - if [[ ! "${selected_base}" =~ @sha256:[0-9a-f]{64}$ ]]; then - echo "Selected ROCm base handoff is not digest-pinned: ${selected_base}" >&2 - return 1 - fi - export BASE_IMAGE="${selected_base}" - echo "Using selected ROCm base image for ci_base: ${BASE_IMAGE}" - elif [[ "${BUILDKITE:-false}" == "true" ]]; then - echo "Required ROCm base handoff metadata is missing" >&2 - return 1 - fi - - if [[ "${base_refreshed}" == "1" ]]; then - export CI_BASE_PUSH_STABLE_TAG - - CI_BASE_PUSH_STABLE_TAG="$(metadata_get rocm-base-push-stable-tag)" - CI_BASE_PUSH_STABLE_TAG="${CI_BASE_PUSH_STABLE_TAG:-0}" - - echo "Push stable ci_base tag: ${CI_BASE_PUSH_STABLE_TAG}" - fi - - bash .buildkite/scripts/ci-bake-rocm.sh ci-base-rocm-ci-with-deps -} - -main "$@" diff --git a/.buildkite/scripts/rocm/build-config.sh b/.buildkite/scripts/rocm/build-config.sh deleted file mode 100644 index f6c6ff56ae38..000000000000 --- a/.buildkite/scripts/rocm/build-config.sh +++ /dev/null @@ -1,85 +0,0 @@ -#!/usr/bin/env bash -# Shared Dockerfile selection for the normal AMD CI build chain. -# CI_ROCM_DOCKERFILE_BASE and CI_ROCM_DOCKERFILE accept repository-relative paths. -# For the legacy ROCm 7.2 stack, set CI_ROCM_DOCKERFILE_BASE=docker/Dockerfile.rocm_72_base -# and CI_ROCM_DOCKERFILE=docker/Dockerfile.rocm_72. -# Custom recipes must preserve the CI targets and artifact layout. Additional -# COPY inputs also need the corresponding *_CONTENT_FILES overrides. - -rocm_base_dockerfile() { - local default="docker/Dockerfile.rocm_base" - printf '%s\n' "${CI_ROCM_DOCKERFILE_BASE:-${ROCM_BASE_DOCKERFILE:-${default}}}" -} - -rocm_ci_dockerfile() { - local default="docker/Dockerfile.rocm" - printf '%s\n' "${CI_ROCM_DOCKERFILE:-${CI_BASE_DOCKERFILE:-${default}}}" -} - -using_custom_rocm_dockerfiles() { - [[ "$(rocm_base_dockerfile)" != "docker/Dockerfile.rocm_base" \ - || "$(rocm_ci_dockerfile)" != "docker/Dockerfile.rocm" ]] -} - -rocm_dockerfile_stages() { - awk ' - toupper($1) == "FROM" { - for (idx = 2; idx < NF; idx++) { - if (tolower($idx) == "as") print tolower($(idx + 1)) - } - } - ' "$1" -} - -validate_rocm_dockerfile() { - local dockerfile="$1" - local stage="" - local stages="" - shift - - case "${dockerfile}" in - /*|../*|*/../*|*/..|*[[:space:]]*) - echo "ROCm Dockerfile must be a repository-relative path without '..' or whitespace: ${dockerfile}" >&2 - return 2 - ;; - esac - if [[ ! -f "${dockerfile}" ]]; then - echo "ROCm Dockerfile not found: ${dockerfile}" >&2 - return 2 - fi - if (($#)) && ! awk ' - toupper($1) == "FROM" { - named = 0 - for (idx = 2; idx < NF; idx++) { - if (tolower($idx) == "as") named = 1 - } - if (!named) exit 1 - } - ' "${dockerfile}"; then - echo "ROCm Dockerfile must name every build stage: ${dockerfile}" >&2 - return 2 - fi - stages=$(rocm_dockerfile_stages "${dockerfile}") - for stage in "$@"; do - if ! grep -Fxq -- "${stage}" <<< "${stages}"; then - echo "ROCm Dockerfile ${dockerfile} is missing required stage: ${stage}" >&2 - return 2 - fi - done -} - -configure_rocm_build() { - ROCM_BASE_DOCKERFILE=$(rocm_base_dockerfile) - CI_BASE_DOCKERFILE=$(rocm_ci_dockerfile) - export ROCM_BASE_DOCKERFILE CI_BASE_DOCKERFILE - using_custom_rocm_dockerfiles || return 0 - - # Custom stacks may supply dependencies without separate build stages. - export ROCM_DEP_CACHE_EXPORT_MODE=never - - # Preserve the standard runtime tags, which are unique to each build, - # while preventing experimental builds from promoting stable images. - export ROCM_BASE_PUSH_STABLE_TAG=0 - export CI_BASE_PUSH_STABLE_TAG=0 - echo "Custom ROCm Dockerfiles: ${ROCM_BASE_DOCKERFILE} and ${CI_BASE_DOCKERFILE}" -} diff --git a/.buildkite/scripts/rocm/build-test-image.sh b/.buildkite/scripts/rocm/build-test-image.sh deleted file mode 100755 index 69c4f77d20ca..000000000000 --- a/.buildkite/scripts/rocm/build-test-image.sh +++ /dev/null @@ -1,77 +0,0 @@ -#!/usr/bin/env bash -# Build the ROCm CI test image or wheel artifact. -# -# When base selection changes, build the full image so downstream ROCm tests -# validate the selected base -> ci_base -> CI image chain. - -set -euo pipefail - -metadata_get() { - local key="$1" - if command -v buildkite-agent >/dev/null 2>&1; then - buildkite-agent meta-data get "${key}" 2>/dev/null || true - fi -} - -load_digest_handoff() { - local metadata_key="$1" - local env_name="$2" - local description="$3" - local image_ref="" - - image_ref="$(metadata_get "${metadata_key}")" - if [[ -z "${image_ref}" ]]; then - return 1 - fi - if [[ ! "${image_ref}" =~ @sha256:[0-9a-f]{64}$ ]]; then - echo "${description} is not digest-pinned: ${image_ref}" >&2 - return 1 - fi - - printf -v "${env_name}" '%s' "${image_ref}" - export "${env_name?}" - echo "Using ${description}: ${image_ref}" -} - -main() { - local base_refreshed=0 - - # This job always builds the checked-out commit. Some externally generated - # pipeline templates still inject remote-fetch settings; do not let those - # settings make the source identity commit-specific or bypass local edits. - export REMOTE_VLLM=0 - unset VLLM_BRANCH - - if ! load_digest_handoff \ - rocm-ci-base-image CI_BASE_IMAGE "ROCm ci_base handoff"; then - if [[ "${BUILDKITE:-false}" == "true" ]]; then - echo "Required ROCm ci_base handoff metadata is missing or invalid" >&2 - return 1 - fi - echo "No ROCm ci_base handoff metadata found; using the local default" - fi - - if ! load_digest_handoff \ - rocm-base-image BASE_IMAGE "ROCm base handoff"; then - if [[ "${BUILDKITE:-false}" == "true" ]]; then - echo "Required ROCm base handoff metadata is missing or invalid" >&2 - return 1 - fi - echo "No ROCm base handoff metadata found; using the local default" - fi - - if [[ "$(metadata_get rocm-base-refresh)" == "1" ]]; then - echo "The selected ROCm base differs from the current stable base" - base_refreshed=1 - fi - - if [[ "${ROCM_CI_ARTIFACT_ONLY:-0}" == "1" && "${base_refreshed}" != "1" ]]; then - echo "ROCM_CI_ARTIFACT_ONLY=1; building ROCm wheel artifact only" - IMAGE_TAG="" bash .buildkite/scripts/ci-bake-rocm.sh test-rocm-ci-with-artifacts - return - fi - - bash .buildkite/scripts/ci-bake-rocm.sh test-rocm-ci-with-wheel -} - -main "$@" diff --git a/.buildkite/scripts/rocm/build.sh b/.buildkite/scripts/rocm/build.sh new file mode 100755 index 000000000000..b61dcb913f87 --- /dev/null +++ b/.buildkite/scripts/rocm/build.sh @@ -0,0 +1,330 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +# +# Build and promote ROCm images via docker/docker-bake-rocm.hcl. +# Pushes only to $ROCM_CI_REPO and the BuildKit cache that goes with it. +# +# Usage: build.sh +# +# Modes: +# keys Print current Ks/Kr keys and whether their images exist (on-call debug). +# base Ensure stack, runtime and test-deps images exist; write their refs to Buildkite metadata. +# ci Build and push this build's per-commit test image. +# release Release image + wheels from source into the release repo (release pipeline, AWS agents). +# release-base Only the release stack and runtime (a serialized step, so one run builds a new key). +# +# Stack: rocm (TheRock ROCm 10). Force a stack rebuild: bump ARG REFRESH +# in docker/Dockerfile.rocm_base +# +# Trust: ROCM_CI_REPO is set by ci-infra (CI) or release-pipeline.yaml (release). +# Trusted main builds: rocm/vllm-ci +# PR / fork builds: rocm/vllm-dev (pr- tags and caches; trusted builds never read it) +# Other untrusted: rocm/vllm-dev (br--- tags and caches: nightlies, branch builds) +# Release pipeline: its ECR repo (never reads rocm/vllm-ci or its cache: it builds the +# stack and runtime from source, with only its own ECR cache; +# csrc objects come from the shared S3 sccache, as in CUDA) +# ROCM_CI_TRUSTED_REPO / ROCM_CI_CACHE_REPO only point a local run at a sandbox registry. +set -euo pipefail + +# --------------------------------------------------------------------------- +# Arguments and trust +# --------------------------------------------------------------------------- +STACK=$1 +MODE=$2 +REPO=${ROCM_CI_REPO:?ROCM_CI_REPO must be set (by ci-infra or release-pipeline.yaml)} +TRUSTED=${ROCM_CI_TRUSTED_REPO:-rocm/vllm-ci} + +export STACK PUSH=true +export CACHE=${ROCM_CI_CACHE_REPO:-rocm/vllm-ci-cache} # every CI build reads the trusted cache +export CACHE_TO=$CACHE: # ref prefix trusted builds write (HCL appends -) +export BUILDKIT_PROGRESS=${BUILDKIT_PROGRESS:-plain} # TTY progress redraws bloat CI logs + +# PR builds write to a per-PR cache and prefix their image tags. The release pipeline's agents +# cannot write rocm/vllm-ci or its cache: release writes its images and its own cache to $REPO, +# so the next release reuses this one's all-arch compile. +PFX="" +if [[ $MODE == release* ]]; then + CACHE="" CACHE_TO=$REPO:cache- # and reads only that cache: CI agents also run PR code +elif [[ $REPO != "$TRUSTED" ]]; then + if [[ ${BUILDKITE_PULL_REQUEST:-false} != false ]]; then + PFX="pr$BUILDKITE_PULL_REQUEST-" + else # not a PR (AITER nightly, branch or manual builds): one namespace per branch + br=${BUILDKITE_BRANCH:-local} # the hash keeps e.g. feat/x and feat-x apart + PFX="br-$(printf %s "$br" | tr -c 'A-Za-z0-9.-' - | cut -c1-40)-$(printf %s "$br" | sha256sum | cut -c1-8)-" + fi + CACHE_TO=$REPO:cache-$PFX + # Only the compile stages' cache: everything else is cheap or in the trusted cache. + export CACHE_CI=false +fi + +# Stack Dockerfile — literal path used by ci-infra ci_selector (images.py). +case $STACK in + rocm) SDF=docker/Dockerfile.rocm_base ;; + *) echo "Unknown stack: $STACK" >&2; exit 2 ;; +esac + +# --------------------------------------------------------------------------- +# BuildKit builder +# --------------------------------------------------------------------------- +# Prefer the host buildkitd (it has its own GC policy and cache). Fall back +# to a docker-container builder if no socket is present. Release RUN steps must reach EC2 IMDS +# for sccache's S3 credentials: their fallback builder uses the host network, under its own name. +B=vllm-rocm net=() +[[ $MODE == release* ]] && B=vllm-rocm-release net=(--driver-opt network=host) +if ! docker buildx inspect "$B" >/dev/null 2>&1; then + if [[ -S /run/buildkit/buildkitd.sock ]]; then + drv=(--driver remote unix:///run/buildkit/buildkitd.sock) + else + drv=(--driver docker-container "${net[@]}" \ + ${BUILDKITD_CONFIG:+--buildkitd-config "$BUILDKITD_CONFIG"}) + fi + # || inspect: tolerate a race where another job created it between the check and create. + docker buildx create --name "$B" "${drv[@]}" >/dev/null \ + || docker buildx inspect "$B" >/dev/null +fi +export BUILDX_BUILDER=$B +docker buildx version + +# --------------------------------------------------------------------------- +# Build context — an export of HEAD +# --------------------------------------------------------------------------- +# Bake builds .rocm-src, not the checkout: the content keys below hash HEAD, and BuildKit keys +# COPY on file modes, which drift on shared agents. umask 022 gives every agent git's modes. +[[ $(git rev-parse HEAD) == $(git rev-parse "${BUILDKITE_COMMIT:-HEAD}^{commit}") ]] \ + || { echo "HEAD is not BUILDKITE_COMMIT ($BUILDKITE_COMMIT)" >&2; exit 1; } +umask 022 +rm -rf .rocm-src && mkdir .rocm-src +git archive HEAD | tar -x --no-same-permissions -C .rocm-src -f - +export CONTEXT=.rocm-src + +# --------------------------------------------------------------------------- +# Git tags — required by the version stage +# --------------------------------------------------------------------------- +# setuptools_scm needs a reachable v* tag. Agents clone without tags, partially +# and with their objects in a host mirror (--reference) that the build context +# cannot see; --force updates tags that moved (e.g. a re-tag after a release +# candidate). The context has no .git: the version stage reads .git_archival.txt. +git fetch -q --force --tags --filter=tree:0 origin +git describe --tags --match 'v[0-9]*' >/dev/null 2>&1 \ + || git fetch -q --unshallow --filter=tree:0 origin +printf 'node: %s\nnode-date: %s\ndescribe-name: %s\n' "$(git rev-parse HEAD)" \ + "$(git log -1 --format=%cI)" "$(git describe --tags --long --abbrev=40 --match 'v[0-9]*')" \ + > .rocm-src/.git_archival.txt + +# --------------------------------------------------------------------------- +# Helper functions +# --------------------------------------------------------------------------- + +# digest +# Print the sha256 digest of . Returns 1 if the tag does not exist. +# The "Digest:" line of the plain output is parsed and validated (as the old CI +# scripts did): --format output and stray stderr are not safe to splice into a ref. +# Any other registry error, after retries, exits 75 (the steps retry it); in a $() +# subshell that becomes the substitution's status, which ensure() and set -e pass on. +digest() { + local out rc d try + for try in 1 2 3; do + rc=0 + out=$(docker buildx imagetools inspect "$1" 2>&1) || rc=$? + d=$(awk '$1 == "Digest:" { print $2; exit }' <<<"$out") + if ((rc == 0)) && [[ $d =~ ^sha256:[0-9a-f]{64}$ ]]; then echo "$d"; return 0; fi + if ((rc != 0)) && grep -qiE 'manifest unknown|no such manifest|name unknown|not found|does not exist' <<<"$out"; then + return 1 + fi + ((try == 3)) || sleep 5 + done + echo "registry error for $1 (exit $rc): $out" >&2 + exit 75 +} + +# label — read one image label +label() { docker buildx imagetools inspect "$1" \ + --format "{{index .Image.Config.Labels \"$2\"}}"; } + +# bake — invoke docker buildx bake with the project HCL +bake() { docker buildx bake -f docker/docker-bake-rocm.hcl "$@"; } + +# meta [value] — Buildkite build metadata +meta() { buildkite-agent meta-data "$@"; } + +# pushed — the digest that bake pushed for +pushed() { + local d + d=$(awk -F'"' -v t="$2" '/^ "/ { k = $2 } k == t && $2 == "containerimage.digest" { print $4 }' "$1") + [[ $d =~ ^sha256:[0-9a-f]{64}$ ]] && echo "$d" +} + +# --------------------------------------------------------------------------- +# Content keys +# --------------------------------------------------------------------------- +# Ks (stack key): hash of the stack Dockerfile + therock_wheels.py. +# A Ks cache miss triggers a full stack rebuild (hours). +# Kr (runtime key): Ks + the portion of Dockerfile.rocm above the per-commit +# marker + all pip requirement files. +# A Kr cache miss triggers a runtime rebuild (minutes). + +_trw=$( [[ $STACK == rocm ]] && echo tools/vllm-rocm/therock_wheels.py || true ) +# The layer format (compression, and mtimes as built: see the bake file) is part of every image's +# identity: an image left in an older format would hand later builds a second copy of the same layers. +Ks=$( { echo "layers: zstd-3, mtimes kept"; git ls-tree HEAD -- "$SDF" ${_trw:+"$_trw"}; } | sha256sum | cut -c1-16) +Kr=$( + { + echo "$Ks" + sed '/^# ==== per-commit stages/q' .rocm-src/docker/Dockerfile.rocm + git ls-tree HEAD -- \ + requirements/common.txt \ + requirements/rocm.txt \ + requirements/test/rocm.txt \ + rust-toolchain.toml \ + tests/vllm_test_utils \ + tools/install_torchcodec_rocm.sh + } | sha256sum | cut -c1-16 +) +echo "build.sh $STACK $MODE: repo=$REPO trusted=$TRUSTED Ks=$Ks Kr=$Kr" + +# --------------------------------------------------------------------------- +# ensure [] +# --------------------------------------------------------------------------- +# Resolve a content-keyed image without building if it already exists. +# Resolution order: +# 1. Trusted registry (rocm/vllm-ci:) — safe for any CI build to reuse. Not for release: +# CI agents also run PR code, so release never ships an image it did not build itself. +# 2. This repo's own tag ($REPO:$PFX) — PR builds and the release pipeline write here. +# 3. Neither found: build via bake and push. +# A means the image only counts as present when the sibling +# tag also exists (used by stack: must have both the image and -wheels); then +# both pinned refs are printed, space-separated. +ensure() { + local target=$1 tag=$2 sibling=${3:-} ref d ds refs rc mf + refs=("$REPO:$PFX$tag") + [[ $MODE == release* || $REPO == "$TRUSTED" ]] || refs=("$TRUSTED:$tag" "${refs[@]}") + + for ref in "${refs[@]}"; do + if d=$(digest "$ref") && { [[ -z $sibling ]] || ds=$(digest "$ref$sibling"); }; then + echo "Reusing $ref@$d" >&2 + echo "$ref@$d${sibling:+ $ref$sibling@$ds}" + return 0 + else + rc=$?; ((rc == 1)) || return $rc # a registry error is not a miss + fi + done + + # Neither copy found — build and push. + ref=$REPO:$PFX$tag + echo "--- :docker: Building $target as $ref" >&2 + # Explicit checks: $(ensure ...) runs in a subshell, where set -e does not apply. + mf=$(mktemp) + TAG=$ref bake --metadata-file "$mf" "$target" >&2 || { rm -f "$mf"; echo "building $ref failed" >&2; return 1; } + # The digests this bake pushed (base: stack-, stack-wheels), not the tags', which by now + # may hold a concurrent build's push of the same key. + if ! d=$(pushed "$mf" "${target/#base/stack-$STACK}") \ + || { [[ -n $sibling ]] && ! ds=$(pushed "$mf" stack-wheels); }; then + rm -f "$mf"; echo "bake recorded no digest for $ref" >&2; return 1 + fi + rm -f "$mf" + echo "$ref@$d${sibling:+ $ref$sibling@$ds}" +} + +# load — read stack/runtime refs from Buildkite metadata (written by base mode). +load() { + STACK_IMAGE=$(meta get "rocm-$STACK-stack") + STACK_WHEELS=$(meta get "rocm-$STACK-stack-wheels") + RUNTIME=$(meta get "rocm-$STACK-runtime") + TEST_DEPS=$(meta get "rocm-$STACK-test-deps") + export STACK_IMAGE STACK_WHEELS RUNTIME TEST_DEPS + for ref in "$STACK_IMAGE" "$STACK_WHEELS" "$RUNTIME" "$TEST_DEPS"; do + [[ $ref =~ @sha256:[0-9a-f]{64}$ ]] || { echo "not a pinned image ref in the metadata: '$ref'" >&2; exit 1; } + done +} + +# --------------------------------------------------------------------------- +# Mode dispatch +# --------------------------------------------------------------------------- +case $MODE in + + keys) + # Print current content keys and registry status (useful for on-call triage). + for t in "stack-$STACK-$Ks" "runtime-$STACK-$Kr" "test-deps-$STACK-$Kr"; do + d=$(digest "$TRUSTED:$t") || { (($? == 1)) || exit 75; d=MISSING; } + echo "$t $d" + done + ;; + + base) + # Ensure the stack and runtime images exist; record their digests in metadata + # so the ci mode can pull them without re-resolving. + # The "base" bake group builds the stack and its -wheels sibling together. + STACK_IMAGE=$(ensure base "stack-$STACK-$Ks" -wheels) + STACK_WHEELS=${STACK_IMAGE#* } STACK_IMAGE=${STACK_IMAGE%% *} + export STACK_IMAGE + RUNTIME=$(ensure runtime "runtime-$STACK-$Kr") + export RUNTIME + TEST_DEPS=$(ensure test-deps "test-deps-$STACK-$Kr") + + meta set "rocm-$STACK-stack" "$STACK_IMAGE" + meta set "rocm-$STACK-runtime" "$RUNTIME" + meta set "rocm-$STACK-test-deps" "$TEST_DEPS" + meta set "rocm-$STACK-stack-wheels" "$STACK_WHEELS" + echo "stack=$STACK_IMAGE runtime=$RUNTIME test-deps=$TEST_DEPS stack-wheels=$STACK_WHEELS" + ;; + + ci) + # Build and push the per-commit test image for GPU test steps. + load + export TAG=$REPO:build-$BUILDKITE_BUILD_ID + # AITER nightly (AITER_NIGHTLY=1 from the schedule): the test stage installs the newest + # AITER nightly that fits the image; an AITER_SPEC in the environment pins one instead. + rm -rf .rocm-wheel .rocm-kernel-map + bake ci + # For the python-only install test (run-amd-test.sh downloads it); not in the image. + buildkite-agent artifact upload ".rocm-wheel/vllm-*.whl" + # CI test selection (VLLM_KERNEL_SYMBOL_MAP=1, set by ci-infra): the kernel symbol map of this + # build's csrc, from the build cache. Optional: a missing map never fails the image build. + if [[ ${VLLM_KERNEL_SYMBOL_MAP:-0} == 1 ]]; then + if bake kernel-symbol-map && [[ -s .rocm-kernel-map/kernel_symbol_map.rocm.json.gz ]] \ + && (cd .rocm-kernel-map && buildkite-agent artifact upload kernel_symbol_map.rocm.json.gz); then + : + else + echo "ROCm kernel symbol map export or upload failed; continuing without it" >&2 + fi + fi + ;; + + release|release-base) + # Build and push the release image + wheels into $REPO. The stack and runtime come from this + # repo only (built here once per key, from the same Dockerfiles and keys CI tests), never from + # rocm/vllm-ci or its cache. + STACK_IMAGE=$(ensure base "stack-$STACK-$Ks" -wheels) + STACK_WHEELS=${STACK_IMAGE#* } STACK_IMAGE=${STACK_IMAGE%% *} + export STACK_IMAGE # before ensure runtime: a runtime built here must use this stack + RUNTIME=$(ensure runtime "runtime-$STACK-$Kr") + [[ $MODE == release ]] || exit 0 + export STACK_WHEELS RUNTIME + export TAG=$REPO:$STACK-$BUILDKITE_COMMIT + + # Resolve the vLLM version from the commit (setuptools_scm). + PUSH=false bake version + VLLM_VERSION=$(cat .rocm-version/vllm-version.txt) + export VLLM_VERSION + + # Skip if this exact version was already released (handles rc → final re-tags). + if digest "$TAG" >/dev/null \ + && [[ $(label "$TAG" org.opencontainers.image.version) == "$VLLM_VERSION" ]]; then + echo "Release image $TAG already exists with version $VLLM_VERSION — skipping." >&2 + exit 0 + fi + + # Wheels first: their all-arch csrc compile is the long part, and vllm-openai reuses it. + bake wheels openai-smoke + # Alias the runtime as the -base tag (the base-nightly consumer reference). + docker buildx imagetools create -t "$TAG-base" "$RUNTIME" + # Push the vllm-openai tag last — its presence marks the set as complete. + bake vllm-openai + ;; + + *) + echo "Unknown mode: $MODE" >&2 + exit 2 + ;; +esac diff --git a/.buildkite/scripts/rocm/publish.sh b/.buildkite/scripts/rocm/publish.sh new file mode 100755 index 000000000000..3593d00ada39 --- /dev/null +++ b/.buildkite/scripts/rocm/publish.sh @@ -0,0 +1,208 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +# +# Promote ROCm release artifacts without rebuilding anything. +# The release pipeline's build step (build.sh release) pushed the source images to +# ECR; this script only copies digests and uploads wheels. +# +# Usage: publish.sh +# +# Modes: +# wheels Extract the vLLM wheel from the -wheels image and upload to S3. +# images Tag the release image and its runtime base for their variant in ECR. +# With NIGHTLY=1, also push the nightly tags to Docker Hub. +# +# Required environment variables: +# BUILDKITE_COMMIT — the commit being released +# ECR — ECR repository prefix (release-pipeline.yaml's ROCM_CI_REPO) +# +# Optional: +# HUB — Docker Hub repo for nightly images (default: vllm/vllm-openai-rocm) +# NIGHTLY — set to 1 to push nightly Hub tags (default: 0) +# DOCKERHUB_USERNAME — Hub username for cleanup script (default: vllmbot) +set -euo pipefail + +# --------------------------------------------------------------------------- +# Arguments +# --------------------------------------------------------------------------- +STACK=$1 +MODE=$2 +COMMIT=${BUILDKITE_COMMIT:?BUILDKITE_COMMIT must be set} + +ECR=${ECR:?ECR must be set (by release-pipeline.yaml)} +SRC=$ECR:${STACK}-${COMMIT} # build.sh release, in the same build +HUB=${HUB:-vllm/vllm-openai-rocm} + +# --------------------------------------------------------------------------- +# Helper functions +# --------------------------------------------------------------------------- + +# retry — registry lookups: these steps have no retry rules +retry() { "$@" || { sleep 5; "$@"; } || { sleep 5; "$@"; }; } + +# pin — resolve to a pinned ref@digest (resolves once, stable for the rest of the script). +# Parses the validated "Digest:" line, as build.sh does. +pin() { + local d + d=$(retry docker buildx imagetools inspect "$1" | awk '$1 == "Digest:" { print $2; exit }') + [[ $d =~ ^sha256:[0-9a-f]{64}$ ]] || { echo "cannot resolve the digest of $1" >&2; return 1; } + echo "$1@$d" +} + +# variant — read the ai.vllm.rocm.variant image label +variant() { + retry docker buildx imagetools inspect "$1" \ + --format '{{index .Image.Config.Labels "ai.vllm.rocm.variant"}}' +} + +# --------------------------------------------------------------------------- +# Resolve source image refs (done once; images step uses them too) +# --------------------------------------------------------------------------- +# Logged in, the ECR lookups below are not subject to the anonymous rate limits. +aws ecr-public get-login-password --region us-east-1 \ + | docker login -u AWS --password-stdin "${ECR%%/*}" +IMG=$(pin "$SRC") +V=$(variant "$IMG") # e.g. rocm100 — used in S3 paths, ECR tags and Hub tags +[[ $V =~ ^rocm[0-9]+$ ]] || { echo "$IMG has no valid ai.vllm.rocm.variant label: '$V'" >&2; exit 1; } +# For the steps after either mode: root-index generation and publish-release-images.sh. +buildkite-agent meta-data set "rocm-$STACK-variant" "$V" + +# --------------------------------------------------------------------------- +# Mode dispatch +# --------------------------------------------------------------------------- +case $MODE in + + # ------------------------------------------------------------------------- + wheels) + # ------------------------------------------------------------------------- + # Extract the vLLM wheel from this commit's -wheels image and upload it + # to S3 so users can pip-install it directly. + # ------------------------------------------------------------------------- + + WHEELS_IMG=$(pin "$SRC-wheels") + + # Export wheel files out of the scratch image into ./dist/ + rm -rf dist + echo "FROM $WHEELS_IMG" | docker buildx build -q -o dist - + + # Sanity: the wheel's variant suffix must match the stack label. + # (setup.py's rocm_version_to_variant produces e.g. rocm100 for 10.0.x) + actual_variant=$(basename dist/vllm-*.whl | grep -oE 'rocm[0-9]+' | tail -1) + [[ $actual_variant == "$V" ]] \ + || { echo "Wheel variant $actual_variant != image variant $V" >&2; exit 1; } + + bash .buildkite/scripts/upload-rocm-wheels.sh "$STACK" dist + + # Buildkite annotation: pip install + direct S3 download commands. + WHL_URL="https://wheels.vllm.ai/rocm/$COMMIT/$V" + buildkite-agent annotate --style success --context "rocm-$STACK-wheels" << EOF +## :package: ROCm \`$V\` wheels published + +| | | +|---|---| +| **Variant** | \`$V\` | +| **Commit** | \`$COMMIT\` | +| **Branch** | \`${BUILDKITE_BRANCH:-?}\` | + +### Install (this commit) +\`\`\`bash +pip install vllm --extra-index-url $WHL_URL +\`\`\` + +### Install (latest nightly) +\`\`\`bash +pip install vllm --extra-index-url https://wheels.vllm.ai/rocm/nightly/$V +\`\`\` + +### Download wheels directly +\`\`\`bash +aws s3 ls s3://${S3_BUCKET:-vllm-wheels}/rocm/$COMMIT/$V-wheels/ +aws s3 cp --recursive --exclude "*" --include "*.whl" s3://${S3_BUCKET:-vllm-wheels}/rocm/$COMMIT/$V-wheels/ . +\`\`\` +EOF + ;; + + # ------------------------------------------------------------------------- + images) + # ------------------------------------------------------------------------- + # Tag the release image and its runtime base for their variant in ECR. + # With NIGHTLY=1, also tag them on Docker Hub and prune old nightly tags. + # ------------------------------------------------------------------------- + + RT=$(pin "$SRC-base") # runtime base alias (published as base-nightly*) + + # --- ECR ----------------------------------------------------------------- + docker buildx imagetools create -t "$ECR:$COMMIT-$V" "$IMG" + docker buildx imagetools create -t "$ECR:$COMMIT-$V-base" "$RT" + + ECR_IMG="$ECR:$COMMIT-$V" + ECR_BASE="$ECR:$COMMIT-$V-base" + + # --- Non-nightly annotation ---------------------------------------------- + if [[ ${NIGHTLY:-0} != 1 ]]; then + buildkite-agent annotate --style success --context "rocm-$STACK-images" << EOF +## :docker: ROCm \`$V\` images published to ECR + +| | | +|---|---| +| **Variant** | \`$V\` | +| **Commit** | \`$COMMIT\` | +| **ECR image** | \`$ECR_IMG\` | +| **ECR base** | \`$ECR_BASE\` | + +\`\`\`bash +docker pull $ECR_IMG +\`\`\` +EOF + exit 0 + fi + + # --- Nightly: Docker Hub ------------------------------------------------- + export DOCKERHUB_USERNAME=${DOCKERHUB_USERNAME:-vllmbot} + + # The default stack (rocm) owns the unqualified :nightly / :base-nightly tags. + # All stacks own the variant-qualified :nightly- tags. + suffixes=("-$V") + [[ $STACK == rocm ]] && suffixes+=("") + + hub_tags="" + for sfx in "${suffixes[@]}"; do + docker buildx imagetools create \ + -t "$HUB:nightly$sfx" -t "$HUB:nightly$sfx-$COMMIT" "$IMG" + docker buildx imagetools create \ + -t "$HUB:base-nightly$sfx" -t "$HUB:base-nightly$sfx-$COMMIT" "$RT" + hub_tags="$hub_tags \`$HUB:nightly$sfx\` \`$HUB:nightly$sfx-$COMMIT\`" + done + + # Prune old nightly tags (keep last 14 builds per tag family). + clean=.buildkite/scripts/cleanup-nightly-builds.sh + bash "$clean" "nightly-$V-" "$HUB" + bash "$clean" "base-nightly-$V-" "$HUB" + if [[ $STACK == rocm ]]; then + bash "$clean" "nightly-" "$HUB" "nightly-rocm" + bash "$clean" "base-nightly-" "$HUB" "base-nightly-rocm" + fi + + buildkite-agent annotate --style success --context "rocm-$STACK-images" << EOF +## :docker: ROCm \`$V\` nightly images published + +| | | +|---|---| +| **Variant** | \`$V\` | +| **Commit** | \`$COMMIT\` | +| **ECR image** | \`$ECR_IMG\` | +| **ECR base** | \`$ECR_BASE\` | +| **Docker Hub** |$hub_tags | + +\`\`\`bash +docker pull $HUB:nightly-$V +\`\`\` +EOF + ;; + + *) + echo "Unknown mode: $MODE" >&2 + exit 2 + ;; +esac diff --git a/.buildkite/scripts/rocm/refresh-base-image.sh b/.buildkite/scripts/rocm/refresh-base-image.sh deleted file mode 100755 index 10e594577a87..000000000000 --- a/.buildkite/scripts/rocm/refresh-base-image.sh +++ /dev/null @@ -1,613 +0,0 @@ -#!/usr/bin/env bash -# Select the content-addressed ROCm base for this checkout, building it only -# when the exact content ref is absent. - -set -euo pipefail - -# shellcheck source=.buildkite/scripts/rocm/build-config.sh -source "$(dirname "${BASH_SOURCE[0]}")/build-config.sh" -configure_rocm_build - -DOCKERFILE="${ROCM_BASE_DOCKERFILE:-docker/Dockerfile.rocm_base}" -BASE_REPO="${ROCM_BASE_IMAGE_REPO:-rocm/vllm-dev}" -CACHE_REPO="${ROCM_BASE_CACHE_REPO:-${DOCKERHUB_CACHE_REPO:-rocm/vllm-ci-cache}}" -BUILDER_NAME="${ROCM_BASE_BUILDER_NAME:-vllm-rocm-base-builder}" -DEFAULT_ROCM_BASE_METADATA_VERSION="2" -DEFAULT_ROCM_BASE_CONTENT_FILES="${DOCKERFILE}" - -ROCM_BASE_LAYER_CACHE_REF="" -BASE_CACHE_PREFIX="rocm-base" -if using_custom_rocm_dockerfiles; then - BASE_CACHE_PREFIX="rocm-base-$(printf '%s\n%s\n' \ - "${ROCM_BASE_DOCKERFILE}" "${CI_BASE_DOCKERFILE}" | sha256sum | cut -c1-12)" -fi -ROCM_BASE_TRUSTED_LAYER_CACHE_REF="${CACHE_REPO}:${BASE_CACHE_PREFIX}-main" -ROCM_BASE_STABLE_TAG_UPDATED=0 -declare -a ROCM_BASE_CACHE_ARGS=() - -metadata_set() { - local key="$1" - local value="$2" - - [[ -n "${value}" ]] || return 0 - if command -v buildkite-agent >/dev/null 2>&1; then - buildkite-agent meta-data set "${key}" "${value}" - elif [[ "${BUILDKITE:-false}" == "true" ]]; then - echo "buildkite-agent is required to publish ROCm base metadata" >&2 - return 1 - fi -} - -compute_content_hash() { - local path="" - local file="" - - for path in "$@"; do - if [[ -d "${path}" ]]; then - while IFS= read -r -d '' file; do - printf 'file:%s\n' "${file}" - sha256sum "${file}" - done < <(find "${path}" -type f -print0 | sort -z) - elif [[ -f "${path}" ]]; then - printf 'file:%s\n' "${path}" - sha256sum "${path}" - else - printf 'missing:%s\n' "${path}" - fi - done | sha256sum | cut -d' ' -f1 -} - -clean_docker_tag() { - local input="$1" - echo "${input}" | sed 's/[^a-zA-Z0-9._-]/_/g' | cut -c1-128 -} - -tag_component() { - local input="$1" - local max_chars="${2:-24}" - - clean_docker_tag "${input:-unknown}" | cut -c1-"${max_chars}" -} - -normalize_repo_slug() { - local repo_slug="${1:-}" - - repo_slug="${repo_slug%/}" - repo_slug="${repo_slug%.git}" - repo_slug="${repo_slug#https://github.com/}" - repo_slug="${repo_slug#http://github.com/}" - repo_slug="${repo_slug#ssh://git@github.com/}" - repo_slug="${repo_slug#git@github.com:}" - repo_slug="${repo_slug#github.com/}" - printf '%s\n' "${repo_slug}" -} - -is_trusted_main_build() { - local actual_repo="" - local trusted_repo="" - - [[ "${BUILDKITE:-false}" == "true" ]] || return 1 - [[ "${BUILDKITE_PULL_REQUEST:-false}" == "false" ]] || return 1 - [[ "${BUILDKITE_BRANCH:-}" == "${ROCM_BASE_STABLE_BRANCH:-main}" ]] || return 1 - actual_repo=$(normalize_repo_slug "${BUILDKITE_REPO:-}") - trusted_repo=$(normalize_repo_slug \ - "${ROCM_BASE_STABLE_REPO_SLUG:-vllm-project/vllm}") - [[ -n "${actual_repo}" && "${actual_repo}" == "${trusted_repo}" ]] -} - -rocm_base_layer_cache_scope() { - local pull_request="${BUILDKITE_PULL_REQUEST:-false}" - local branch="${BUILDKITE_PULL_REQUEST_HEAD_BRANCH:-${BUILDKITE_BRANCH:-local}}" - local identity="" - local repo_slug="" - - if is_trusted_main_build; then - printf 'main\n' - return 0 - fi - if [[ "${pull_request}" != "false" && -n "${pull_request}" ]]; then - repo_slug=$(normalize_repo_slug "${BUILDKITE_REPO:-local}") - identity=$(printf '%s\n' "${repo_slug:-local}" | sha256sum | cut -c1-12) - printf 'pr-%s-%s\n' \ - "$(tag_component "${pull_request}" 32)" "${identity}" - return 0 - fi - - identity=$(printf '%s\n%s\n' "${BUILDKITE_REPO:-local}" "${branch}" \ - | sha256sum | cut -c1-12) - printf 'preview-%s-%s\n' "$(tag_component "${branch}" 24)" "${identity}" -} - -configure_rocm_base_layer_cache() { - local scope="" - - ROCM_BASE_CACHE_ARGS=() - if [[ "${ROCM_BASE_NO_CACHE:-0}" == "1" ]]; then - ROCM_BASE_CACHE_ARGS+=(--no-cache) - ROCM_BASE_LAYER_CACHE_REF="disabled" - return 0 - fi - - scope=$(rocm_base_layer_cache_scope) - ROCM_BASE_LAYER_CACHE_REF="${CACHE_REPO}:${BASE_CACHE_PREFIX}-${scope}" - ROCM_BASE_CACHE_ARGS+=( - --cache-from "type=registry,ref=${ROCM_BASE_LAYER_CACHE_REF}" - ) - if [[ "${ROCM_BASE_LAYER_CACHE_REF}" != \ - "${ROCM_BASE_TRUSTED_LAYER_CACHE_REF}" ]]; then - ROCM_BASE_CACHE_ARGS+=( - --cache-from "type=registry,ref=${ROCM_BASE_TRUSTED_LAYER_CACHE_REF}" - ) - fi - ROCM_BASE_CACHE_ARGS+=( - --cache-to \ - "type=registry,ref=${ROCM_BASE_LAYER_CACHE_REF},mode=max,ignore-error=true" - ) -} - -extract_arg_default() { - local arg_name="$1" - - sed -n -E "s/^[[:space:]]*[Aa][Rr][Gg][[:space:]]+${arg_name}=\"?([^\"[:space:]]+)\"?.*/\\1/p" \ - "${DOCKERFILE}" | head -1 -} - -resolve_image_digest() { - local image_ref="$1" - local attempts="${ROCM_IMAGE_DIGEST_ATTEMPTS:-4}" - local delay_secs="${ROCM_IMAGE_DIGEST_RETRY_DELAY:-2}" - local attempt=0 - local output="" - local digest="" - local status=0 - - if [[ "${image_ref}" =~ @(sha256:[0-9a-f]{64})$ ]]; then - printf '%s\n' "${BASH_REMATCH[1]}" - return 0 - fi - if [[ ! "${attempts}" =~ ^[1-9][0-9]*$ \ - || ! "${delay_secs}" =~ ^[0-9]+$ ]]; then - echo "Invalid image digest retry configuration" >&2 - return 1 - fi - - for ((attempt = 1; attempt <= attempts; attempt++)); do - status=0 - output=$(docker buildx imagetools inspect "${image_ref}" 2>&1) || status=$? - digest=$(awk '$1 == "Digest:" { print $2; exit }' <<< "${output}") - if ((status == 0)) && [[ "${digest}" =~ ^sha256:[0-9a-f]{64}$ ]]; then - printf '%s\n' "${digest}" - return 0 - fi - if ((attempt < attempts)); then - printf \ - 'Image digest lookup failed for %s (%d/%d, status %d); retrying\n' \ - "${image_ref}" "${attempt}" "${attempts}" "${status}" >&2 - sleep "${delay_secs}" - fi - done - - printf 'Failed to resolve digest for %s (status %d)\n%s\n' \ - "${image_ref}" "${status}" "${output:-}" >&2 - return 1 -} - -trusted_base_content_ref() { - local base_hash="$1" - local metadata_version="$2" - - printf '%s:base-v%s-%s\n' "${BASE_REPO}" "${metadata_version}" "${base_hash}" -} - -scoped_base_content_ref() { - local base_hash="$1" - local metadata_version="$2" - - printf '%s:base-v%s-preview-%s\n' \ - "${BASE_REPO}" "${metadata_version}" "${base_hash}" -} - -find_matching_base_content_ref() { - local expected_hash="$1" - local expected_version="$2" - local image_ref="" - local digest="" - local immutable_ref="" - local label_values="" - local remote_hash="" - local remote_version="" - local attempts="${ROCM_IMAGE_DIGEST_ATTEMPTS:-4}" - local delay_secs="${ROCM_IMAGE_DIGEST_RETRY_DELAY:-2}" - local attempt=0 - local inspect_status=0 - shift 2 - - if [[ ! "${attempts}" =~ ^[1-9][0-9]*$ \ - || ! "${delay_secs}" =~ ^[0-9]+$ ]]; then - echo "Invalid image identity retry configuration" >&2 - return 1 - fi - - for image_ref in "$@"; do - if ! digest=$(resolve_image_digest "${image_ref}"); then - continue - fi - immutable_ref="${image_ref}@${digest}" - label_values="" - for ((attempt = 1; attempt <= attempts; attempt++)); do - inspect_status=0 - label_values=$(docker buildx imagetools inspect "${immutable_ref}" \ - --format '{{ index .Image.Config.Labels "vllm.rocm_base.content_hash" }} {{ index .Image.Config.Labels "vllm.rocm_base.metadata_version" }}' \ - 2>/dev/null) || inspect_status=$? - remote_hash="" - remote_version="" - if ((inspect_status == 0)) && [[ -n "${label_values}" ]]; then - read -r remote_hash remote_version _ <<< "${label_values}" - if [[ "${remote_hash}" == "${expected_hash}" \ - && "${remote_version}" == "${expected_version}" ]]; then - printf '%s\n' "${immutable_ref}" - return 0 - fi - fi - if ((attempt < attempts)); then - printf \ - 'ROCm base identity lookup incomplete or mismatched for %s (%d/%d); retrying\n' \ - "${immutable_ref}" "${attempt}" "${attempts}" >&2 - sleep "${delay_secs}" - fi - done - if ((inspect_status == 0)) \ - && [[ -n "${remote_hash}" && -n "${remote_version}" ]]; then - echo "ROCm base content ref has unexpected identity: ${immutable_ref}" >&2 - echo " expected hash/version: ${expected_hash} / ${expected_version}" >&2 - echo " found hash/version: ${remote_hash} / ${remote_version}" >&2 - # A content-addressed ref with the wrong identity is corrupt. The - # stable alias is allowed to describe older content and is only a - # migration/recovery candidate. - if [[ "${image_ref}" != "${BASE_REPO}:base" ]]; then - return 2 - fi - fi - echo "Failed to read complete ROCm base identity labels: ${immutable_ref}" >&2 - done - return 1 -} - -rocm_version_from_base_image() { - local base_image="$1" - local version="" - - version="$(sed -n -E 's/.*:([0-9]+\.[0-9]+(\.[0-9]+)?)-.*/\1/p' <<<"${base_image}")" - tag_component "${version:-${base_image}}" 16 -} - -should_push_stable_tag() { - if [[ "${BUILDKITE_PULL_REQUEST:-false}" != "false" ]]; then - return 1 - fi - - if [[ "${ROCM_BASE_PUSH_STABLE_TAG:-}" == "0" ]]; then - return 1 - fi - - is_trusted_main_build -} - -trusted_main_tip_matches_build() { - local branch="${ROCM_BASE_STABLE_BRANCH:-main}" - local build_commit="${BUILDKITE_COMMIT:-}" - local remote_tip="" - - is_trusted_main_build || return 1 - if [[ ! "${build_commit}" =~ ^[0-9a-fA-F]{40}$ ]]; then - echo "Skipping ROCm stable tag: Buildkite commit is missing or invalid" >&2 - return 1 - fi - remote_tip=$(git ls-remote --exit-code "${BUILDKITE_REPO}" \ - "refs/heads/${branch}" 2>/dev/null | awk 'NR == 1 { print $1 }') - if [[ ! "${remote_tip}" =~ ^[0-9a-fA-F]{40}$ ]]; then - echo "Skipping ROCm stable tag: could not resolve remote ${branch} tip" >&2 - return 1 - fi - if [[ "${remote_tip,,}" != "${build_commit,,}" ]]; then - echo "Skipping ROCm stable tag: ${branch} changed after ${build_commit} (${remote_tip})" >&2 - return 1 - fi - return 0 -} - -promote_stable_base_tag() { - local source_ref="$1" - local stable_tag="$2" - - ROCM_BASE_STABLE_TAG_UPDATED=0 - should_push_stable_tag && trusted_main_tip_matches_build || return 0 - - docker buildx imagetools create --prefer-index=false \ - -t "${stable_tag}" "${source_ref}" - ROCM_BASE_STABLE_TAG_UPDATED=1 -} - -setup_builder() { - echo "--- :buildkite: Setting up buildx builder for ROCm base" - if docker buildx inspect "${BUILDER_NAME}" >/dev/null 2>&1; then - docker buildx use "${BUILDER_NAME}" - else - docker buildx create --name "${BUILDER_NAME}" --driver docker-container --use - fi - docker buildx inspect --bootstrap -} - -compute_base_content_hash() { - local use_sccache="$1" - local base_image_digest="$2" - local content_files="${ROCM_BASE_CONTENT_FILES:-${DEFAULT_ROCM_BASE_CONTENT_FILES}}" - local sccache_arg="" - local sccache_value="" - local -a content_paths=() - - read -r -a content_paths <<< "${content_files}" - - { - printf 'content-files-hash:%s\n' "$(compute_content_hash "${content_paths[@]}")" - printf 'dockerfile:%s\n' "${DOCKERFILE}" - printf 'base-image-digest:%s\n' "${base_image_digest}" - printf 'use-sccache:%s\n' "${use_sccache}" - if [[ "${use_sccache}" == "1" ]]; then - # These values can change the installed binary or final image - # configuration. SCCACHE_ENDPOINT is transport-only and therefore - # deliberately excluded from image identity. - for sccache_arg in \ - SCCACHE_DOWNLOAD_URL \ - SCCACHE_BUCKET_NAME \ - SCCACHE_REGION_NAME \ - SCCACHE_S3_NO_CREDENTIALS; do - sccache_value="${!sccache_arg:-}" - if [[ -z "${sccache_value}" ]]; then - sccache_value=$(extract_arg_default "${sccache_arg}") - fi - printf 'arg:%s=%s\n' \ - "${sccache_arg}" "${sccache_value:-}" - done - fi - } | sha256sum | cut -d' ' -f1 -} - -build_base_image() { - local use_sccache="${ROCM_BASE_USE_SCCACHE:-${USE_SCCACHE:-0}}" - local base_hash="" - local base_image_arg="" - local base_image_digest="" - local pinned_base_image="" - local rocm_version="" - local triton_arg="" - local pytorch_arg="" - local pytorch_vision_arg="" - local pytorch_audio_arg="" - local fa_arg="" - local aiter_arg="" - local mori_arg="" - local python_version_arg="" - local pytorch_rocm_arch_arg="" - local dependency_summary="" - local stable_tag="${BASE_REPO}:base" - local trusted_content_tag="" - local scoped_content_tag="" - local preview_content_tag="" - local writable_content_tag="" - local build_ref="" - local immutable_ref="" - local selected_digest="" - local stable_digest="" - local reuse_status=0 - local content_files="${ROCM_BASE_CONTENT_FILES:-${DEFAULT_ROCM_BASE_CONTENT_FILES}}" - local content_files_hash="" - local metadata_version="${ROCM_BASE_METADATA_VERSION:-${DEFAULT_ROCM_BASE_METADATA_VERSION}}" - local -a sccache_args=() - local -a content_paths=() - - if [[ ! -f "${DOCKERFILE}" ]]; then - echo "Error: ROCm base Dockerfile not found: ${DOCKERFILE}" >&2 - exit 1 - fi - if [[ ! "${metadata_version}" =~ ^[A-Za-z0-9][A-Za-z0-9_.-]{0,15}$ ]]; then - echo "Invalid ROCm base metadata version: ${metadata_version}" >&2 - return 1 - fi - if [[ "${use_sccache}" != "0" && "${use_sccache}" != "1" ]]; then - echo "ROCm base USE_SCCACHE must be 0 or 1: ${use_sccache}" >&2 - return 1 - fi - - base_image_arg="$(extract_arg_default BASE_IMAGE)" - if ! base_image_digest=$(resolve_image_digest "${base_image_arg}"); then - echo "Failed to resolve ROCm base input image: ${base_image_arg}" >&2 - return 1 - fi - pinned_base_image="${base_image_arg%@*}@${base_image_digest}" - read -r -a content_paths <<< "${content_files}" - content_files_hash="$(compute_content_hash "${content_paths[@]}")" - base_hash=$(compute_base_content_hash "${use_sccache}" "${base_image_digest}") - rocm_version="$(extract_arg_default ROCM_SDK_VERSION)" - rocm_version="${rocm_version:-$(rocm_version_from_base_image "${base_image_arg}")}" - triton_arg="$(extract_arg_default TRITON_BRANCH)" - pytorch_arg="$(extract_arg_default PYTORCH_BRANCH)" - pytorch_vision_arg="$(extract_arg_default PYTORCH_VISION_BRANCH)" - pytorch_audio_arg="$(extract_arg_default PYTORCH_AUDIO_BRANCH)" - triton_arg="${triton_arg:-$(extract_arg_default TRITON_VERSION)}" - pytorch_arg="${pytorch_arg:-$(extract_arg_default TORCH_VERSION)}" - pytorch_vision_arg="${pytorch_vision_arg:-$(extract_arg_default TORCHVISION_VERSION)}" - pytorch_audio_arg="${pytorch_audio_arg:-$(extract_arg_default TORCHAUDIO_VERSION)}" - fa_arg="$(extract_arg_default FA_BRANCH)" - aiter_arg="$(extract_arg_default AITER_BRANCH)" - mori_arg="$(extract_arg_default MORI_BRANCH)" - python_version_arg="$(extract_arg_default PYTHON_VERSION)" - pytorch_rocm_arch_arg="$(extract_arg_default PYTORCH_ROCM_ARCH)" - dependency_summary="base=${base_image_arg},rocm=${rocm_version},python=${python_version_arg},pytorch=${pytorch_arg},torchvision=${pytorch_vision_arg},torchaudio=${pytorch_audio_arg},triton=${triton_arg},flash-attn=${fa_arg},aiter=${aiter_arg},mori=${mori_arg},pytorch-rocm-arch=${pytorch_rocm_arch_arg}" - trusted_content_tag=$(trusted_base_content_ref "${base_hash}" "${metadata_version}") - scoped_content_tag=$(scoped_base_content_ref "${base_hash}" "${metadata_version}") - preview_content_tag="${scoped_content_tag}" - # Preview writes share an exact-content namespace so stacked PRs can reuse - # identical images. They may import the trusted ref, but never overwrite it. - if is_trusted_main_build; then - scoped_content_tag="${trusted_content_tag}" - writable_content_tag="${trusted_content_tag}" - else - writable_content_tag="${scoped_content_tag}" - fi - - configure_rocm_base_layer_cache - - if [[ "${use_sccache}" == "1" ]]; then - for env_name in \ - SCCACHE_DOWNLOAD_URL \ - SCCACHE_ENDPOINT \ - SCCACHE_BUCKET_NAME \ - SCCACHE_REGION_NAME \ - SCCACHE_S3_NO_CREDENTIALS; do - if [[ -n "${!env_name:-}" ]]; then - sccache_args+=(--build-arg "${env_name}=${!env_name}") - fi - done - fi - - echo "--- :docker: Preparing ROCm base image" - echo "Dockerfile: ${DOCKERFILE}" - echo "Trusted content tag: ${trusted_content_tag}" - echo "Writable content tag: ${writable_content_tag}" - echo "Stable tag: ${stable_tag} ($(should_push_stable_tag && echo enabled || echo disabled))" - echo "Content hash: ${base_hash}" - echo "Dependency summary: ${dependency_summary}" - echo "USE_SCCACHE: ${use_sccache}" - echo "BuildKit layer cache: ${ROCM_BASE_LAYER_CACHE_REF}" - - if [[ "${ROCM_BASE_REFRESH_FORCE:-0}" != "1" \ - && "${ROCM_BASE_NO_CACHE:-0}" != "1" ]]; then - reuse_status=0 - if [[ "${trusted_content_tag}" == "${scoped_content_tag}" ]]; then - immutable_ref=$(find_matching_base_content_ref \ - "${base_hash}" "${metadata_version}" \ - "${trusted_content_tag}" "${preview_content_tag}" \ - "${stable_tag}") || reuse_status=$? - else - immutable_ref=$(find_matching_base_content_ref \ - "${base_hash}" "${metadata_version}" \ - "${trusted_content_tag}" "${scoped_content_tag}" \ - "${stable_tag}") \ - || reuse_status=$? - fi - else - reuse_status=1 - echo "Forced/no-cache ROCm base build; bypassing content image reuse" - fi - - if ((reuse_status > 1)); then - return "${reuse_status}" - fi - - if [[ ${reuse_status} -eq 0 && -n "${immutable_ref}" ]]; then - echo "Reusing ROCm base image with matching content: ${immutable_ref}" - if [[ "${immutable_ref%@*}" != "${writable_content_tag}" \ - && ( "${immutable_ref%@*}" == "${stable_tag}" \ - || "${immutable_ref%@*}" == "${preview_content_tag}" ) ]]; then - echo "Backfilling ROCm base content tag: ${writable_content_tag}" - docker buildx imagetools create --prefer-index=false \ - -t "${writable_content_tag}" "${immutable_ref}" - if ! immutable_ref=$(find_matching_base_content_ref \ - "${base_hash}" "${metadata_version}" \ - "${writable_content_tag}"); then - echo "Backfilled ROCm base content tag failed identity validation" >&2 - return 1 - fi - fi - else - build_ref="${writable_content_tag}" - echo "No reusable ROCm base content image found; building ${build_ref}" - setup_builder - docker buildx build \ - "${ROCM_BASE_CACHE_ARGS[@]}" \ - --pull \ - --provenance=false \ - --progress "${BUILDKIT_PROGRESS:-plain}" \ - --file "${DOCKERFILE}" \ - --build-arg "BASE_IMAGE=${pinned_base_image}" \ - --build-arg "USE_SCCACHE=${use_sccache}" \ - "${sccache_args[@]}" \ - --label "org.opencontainers.image.source=https://github.com/vllm-project/vllm" \ - --label "org.opencontainers.image.vendor=vLLM" \ - --label "org.opencontainers.image.title=vLLM ROCm base" \ - --label "vllm.rocm_base.metadata_version=${metadata_version}" \ - --label "vllm.rocm_base.content_hash=${base_hash}" \ - --label "vllm.rocm_base.content_files_hash=${content_files_hash}" \ - --label "vllm.rocm_base.dockerfile=${DOCKERFILE}" \ - --label "vllm.rocm_base.dependency_summary=${dependency_summary}" \ - --label "vllm.rocm_base.base_image=${pinned_base_image}" \ - --label "vllm.rocm_base.base_image_digest=${base_image_digest}" \ - --label "vllm.rocm_base.dependency.rocm=${rocm_version}" \ - --label "vllm.rocm_base.dependency.python=${python_version_arg}" \ - --label "vllm.rocm_base.dependency.pytorch=${pytorch_arg}" \ - --label "vllm.rocm_base.dependency.torchvision=${pytorch_vision_arg}" \ - --label "vllm.rocm_base.dependency.torchaudio=${pytorch_audio_arg}" \ - --label "vllm.rocm_base.dependency.triton=${triton_arg}" \ - --label "vllm.rocm_base.dependency.flash_attention=${fa_arg}" \ - --label "vllm.rocm_base.dependency.aiter=${aiter_arg}" \ - --label "vllm.rocm_base.dependency.mori=${mori_arg}" \ - --label "vllm.rocm_base.pytorch_rocm_arch=${pytorch_rocm_arch_arg}" \ - -t "${build_ref}" \ - --push \ - . - if ! immutable_ref=$(find_matching_base_content_ref \ - "${base_hash}" "${metadata_version}" "${build_ref}"); then - echo "Published ROCm base image failed identity validation: ${build_ref}" >&2 - return 1 - fi - fi - - selected_digest="${immutable_ref##*@}" - metadata_set "rocm-base-image" "${immutable_ref}" - metadata_set "rocm-base-push-stable-tag" "0" - stable_digest=$(resolve_image_digest "${stable_tag}" 2>/dev/null || true) - if [[ -n "${stable_digest}" && "${stable_digest}" == "${selected_digest}" ]]; then - echo "--- :white_check_mark: Existing ROCm base is current" - echo "Stable ${stable_tag} already resolves to ${selected_digest}" - return 0 - fi - - promote_stable_base_tag "${immutable_ref}" "${stable_tag}" - - metadata_set "rocm-base-push-stable-tag" "${ROCM_BASE_STABLE_TAG_UPDATED}" - metadata_set "rocm-base-refresh" "1" - - echo "--- :white_check_mark: ROCm base image published" - echo "Use BASE_IMAGE=${immutable_ref} for downstream ROCm CI builds" -} - -main() { - if using_custom_rocm_dockerfiles; then - if [[ "${ROCM_BASE_REFRESH_SKIP:-0}" == "1" ]]; then - echo "Custom ROCm Dockerfiles require ROCM_BASE_REFRESH_SKIP=0" >&2 - return 2 - fi - validate_rocm_dockerfile "${DOCKERFILE}" || return $? - fi - metadata_set "rocm-base-refresh" "0" - - if [[ "${ROCM_BASE_REFRESH_SKIP:-0}" == "1" ]]; then - local stable_ref="${BASE_REPO}:base" - local stable_digest="" - - if ! stable_digest=$(resolve_image_digest "${stable_ref}"); then - echo "Could not pin explicitly skipped ROCm base: ${stable_ref}" >&2 - return 1 - fi - metadata_set "rocm-base-image" "${stable_ref}@${stable_digest}" - metadata_set "rocm-base-push-stable-tag" "0" - echo "ROCM_BASE_REFRESH_SKIP=1; pinned current stable base at ${stable_digest}" - return 0 - fi - build_base_image -} - -if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then - main "$@" -fi diff --git a/.buildkite/scripts/rocm/select_aiter_nightly_wheel.py b/.buildkite/scripts/rocm/select_aiter_nightly_wheel.py index af3de54f6020..3559afe404d4 100755 --- a/.buildkite/scripts/rocm/select_aiter_nightly_wheel.py +++ b/.buildkite/scripts/rocm/select_aiter_nightly_wheel.py @@ -3,9 +3,9 @@ # SPDX-FileCopyrightText: Copyright contributors to the vLLM project """Select the AITER nightly wheel for a vLLM ROCm image. -aiter-nightly-overlay.sh runs this inside the stock ci_base, so it reads that -image's ROCm, torch and Python. It prints the wheel's URL, then the Buildkite -annotation naming it. +docker/Dockerfile.rocm runs this in the test image's AITER nightly step, so it +reads that image's ROCm, torch and Python. It prints the wheel's URL, then the +Buildkite annotation naming it. """ import argparse @@ -21,17 +21,21 @@ import regex as re -INDEX_URL = "https://rocm.frameworks-nightlies.amd.com/whl-multi-arch/amd-aiter/" +INDEX_URL = ( + "https://rocm.frameworks-nightlies.amd.com/whl-multi-arch-staging/amd-aiter/" +) AITER_COMMIT_URL = "https://github.com/ROCm/aiter/commit/" STALE_AFTER = datetime.timedelta(days=1) # Example: -# amd_aiter-0.1.25+rocm7.2.3.torch2.12.ae984f5.d20261008-cp312-cp312-linux_x86_64.whl -# is AITER main at commit ae984f5, built 2026-10-08, for ROCm 7.2.3, torch 2.12 -# and Python 3.12. 0.1.25 is a guess at the next release, not a real one. +# amd_aiter-0.1.28+rocm10.0.0.torch2.13.f684e21.d20261029 +# -cp312-cp312-manylinux_2_28_x86_64.whl +# is AITER main at commit f684e21, built 2026-10-29, for ROCm 10.0.0, torch 2.13 +# and Python 3.12, and runs on glibc 2.28 or newer. 0.1.28 is a guess at the +# next release, not a real one. WHEEL_NAME = re.compile( r"^amd_aiter-(?P[^+-]+)\+(?P[^-]+)" - r"-(?Pcp\d+)-(?P=python)-linux_x86_64\.whl$" + r"-(?Pcp\d+)-(?P=python)-(?:manylinux_\d+_\d+|linux)_x86_64\.whl$" ) ROCM_FIELD = re.compile(r"(?:^|\.)rocm(\d+(?:\.\d+)*)(?:a\d+)?(?=\.|$)") TORCH_FIELD = re.compile(r"(?:^|\.)torch(\d+\.\d+)(?=\.|$)") @@ -235,7 +239,9 @@ def _today() -> datetime.date: return datetime.datetime.now(datetime.UTC).date() -# Not 1 or 2, which Buildkite retries; see aiter-nightly-overlay.sh. +# Its own status for "no wheel for this image". Dockerfile.rocm fails the image +# build on any failure here; bake reports that as exit 1, which the image-build +# step retries once. EXIT_NO_WHEEL = 10 diff --git a/.buildkite/scripts/rocm/smoke-test-image.sh b/.buildkite/scripts/rocm/smoke-test-image.sh deleted file mode 100755 index b155a0897d32..000000000000 --- a/.buildkite/scripts/rocm/smoke-test-image.sh +++ /dev/null @@ -1,84 +0,0 @@ -#!/usr/bin/env bash -# Fast structural smoke test for the full ROCm CI image. - -set -euo pipefail - -run_smoke_checks() { - local required_dir="" - - for required_dir in \ - /vllm-workspace \ - /vllm-workspace/tests \ - /vllm-workspace/src/vllm; do - if [[ ! -d "${required_dir}" ]]; then - echo "Missing directory: ${required_dir}" >&2 - return 1 - fi - done - if [[ ! -x /vllm-workspace/src/vllm/vllm-rs ]]; then - echo "Missing executable: /vllm-workspace/src/vllm/vllm-rs" >&2 - return 1 - fi - - command -v python3 - command -v uv - command -v pytest - - if ! command -v amd-smi >/dev/null 2>&1 \ - && ! command -v rocminfo >/dev/null 2>&1; then - echo "No ROCm CLI found in image" >&2 - return 1 - fi - - PYTHONDONTWRITEBYTECODE=1 python3 - <<'PY' -import torch -import vllm - -print(torch.__version__) -print(vllm.__version__) -PY - - echo "AMD image smoke OK" -} - -if [[ "${1:-}" == "--inside" ]]; then - run_smoke_checks - exit -fi -if (($#)); then - echo "Usage: $0 [--inside]" >&2 - exit 2 -fi - -if [[ "${ROCM_CI_ARTIFACT_ONLY:-0}" == "1" ]]; then - base_refreshed="" - if command -v buildkite-agent >/dev/null 2>&1; then - base_refreshed="$(buildkite-agent meta-data get rocm-base-refresh 2>/dev/null || true)" - fi - if [[ "${base_refreshed}" != "1" ]]; then - echo "ROCM_CI_ARTIFACT_ONLY=1; no full image was built, skipping smoke test" - exit 0 - fi -fi - -smoke_marker="./build/rocm-smoke-export/vllm-smoke-ok" -expected_smoke_id="${BUILDKITE_BUILD_ID:-local}" -if [[ -f "${smoke_marker}" \ - && ( -z "${VLLM_CI_SMOKE_IMAGE:-}" \ - || "${VLLM_CI_SMOKE_IMAGE}" == "${IMAGE_TAG:-}" ) ]]; then - actual_smoke_id="$(< "${smoke_marker}")" - if [[ "${actual_smoke_id}" != "${expected_smoke_id}" ]]; then - echo "ROCm smoke marker belongs to ${actual_smoke_id}, not ${expected_smoke_id}" \ - >&2 - exit 1 - fi - echo "AMD image smoke OK (verified inside BuildKit)" - rm -f -- "${smoke_marker}" - rmdir -- "$(dirname "${smoke_marker}")" 2>/dev/null || true - exit -fi - -image_ref="${VLLM_CI_SMOKE_IMAGE:-${IMAGE_TAG:-rocm/vllm-ci:${BUILDKITE_COMMIT:?BUILDKITE_COMMIT is required}}}" - -docker run --rm -i --network=none --entrypoint /bin/bash "${image_ref}" \ - -s -- --inside < "${BASH_SOURCE[0]}" diff --git a/.buildkite/scripts/rocm/stack.sh b/.buildkite/scripts/rocm/stack.sh deleted file mode 100644 index 14bfeb48a017..000000000000 --- a/.buildkite/scripts/rocm/stack.sh +++ /dev/null @@ -1,78 +0,0 @@ -#!/usr/bin/env bash -# SPDX-License-Identifier: Apache-2.0 -# SPDX-FileCopyrightText: Copyright contributors to the vLLM project -# -# Derive everything the release pipeline needs about a ROCm stack from its base -# Dockerfile, so the stack's image is the single input to every ROCm script. -# -# Usage: source .buildkite/scripts/rocm/stack.sh -# e.g. docker/Dockerfile.rocm_base (ROCm 10.0) or docker/Dockerfile.rocm_72_base -# -# Exports: -# ROCM_STACK_BASE_DOCKERFILE the base Dockerfile -# ROCM_STACK_DOCKERFILE the matching vLLM Dockerfile (base name minus _base) -# ROCM_STACK_THEROCK 1 if the ROCm SDK comes from TheRock wheels -# ROCM_STACK_VERSION ROCm version, e.g. 10.0.0 or 7.2.3 -# ROCM_STACK_VARIANT wheel variant and Docker Hub tag flavor from major.minor, -# same rule as setup.py (rocm100, rocm72) -# ROCM_STACK_IMAGE_KEY hash of the base Dockerfile (base image cache key) -# ROCM_STACK_WHEEL_KEY hash of the base Dockerfile plus files it COPYs from -# the repo (base wheel cache key) -# ROCM_STACK_ECR_BASE ECR tag of the cached base image -# ROCM_STACK_ECR_IMAGE ECR tag of this commit's release image -# ROCM_STACK_BASE_WHEELS_DIR local dir for base wheels (also the artifact path) -# ROCM_STACK_VLLM_WHEEL_DIR local dir for the vLLM wheel (also the artifact path) - -rocm_stack_init() { - local base="$1" - if [[ ! -f "$base" ]]; then - echo "ROCm base Dockerfile not found: $base" >&2 - return 2 - fi - ROCM_STACK_BASE_DOCKERFILE="$base" - ROCM_STACK_DOCKERFILE="${base%_base}" - - local sdk - sdk=$(sed -nE 's/^ARG ROCM_SDK_VERSION=([0-9.]+).*/\1/p' "$base" | head -1) - if [[ -n "$sdk" ]]; then - ROCM_STACK_THEROCK=1 - ROCM_STACK_VERSION="$sdk" - else - ROCM_STACK_THEROCK=0 - # BASE_IMAGE format: rocm/dev-ubuntu-22.04:7.2.3-complete -> 7.2.3 - ROCM_STACK_VERSION=$(sed -nE 's/^ARG BASE_IMAGE=.*:([0-9]+(\.[0-9]+)+).*/\1/p' "$base" | head -1) - fi - if [[ -z "$ROCM_STACK_VERSION" ]]; then - echo "Could not determine the ROCm version from $base" >&2 - return 2 - fi - - local major minor - IFS=. read -r major minor _ <<< "$ROCM_STACK_VERSION" - ROCM_STACK_VARIANT="rocm${major}${minor}" - - ROCM_STACK_IMAGE_KEY=$(sha256sum "$base" | cut -c1-16) - local inputs - inputs=$(grep -E '^COPY ' "$base" | grep -v -- '--from' | awk '{print $2}' || true) - if [[ -z "$inputs" ]]; then - ROCM_STACK_WHEEL_KEY="$ROCM_STACK_IMAGE_KEY" - else - # shellcheck disable=SC2086 - ROCM_STACK_WHEEL_KEY=$(sha256sum "$base" $inputs | sha256sum | cut -c1-16) - fi - - local ecr="public.ecr.aws/q9t5s3a7/vllm-release-repo" - ROCM_STACK_ECR_BASE="${ecr}:${ROCM_STACK_IMAGE_KEY}-${ROCM_STACK_VARIANT}-base" - ROCM_STACK_ECR_IMAGE="${ecr}:${BUILDKITE_COMMIT:-unknown}-${ROCM_STACK_VARIANT}" - ROCM_STACK_BASE_WHEELS_DIR="artifacts/${ROCM_STACK_VARIANT}-base-wheels" - ROCM_STACK_VLLM_WHEEL_DIR="artifacts/${ROCM_STACK_VARIANT}-vllm-wheel" - - export ROCM_STACK_BASE_DOCKERFILE ROCM_STACK_DOCKERFILE ROCM_STACK_THEROCK \ - ROCM_STACK_VERSION ROCM_STACK_VARIANT ROCM_STACK_IMAGE_KEY \ - ROCM_STACK_WHEEL_KEY ROCM_STACK_ECR_BASE ROCM_STACK_ECR_IMAGE \ - ROCM_STACK_BASE_WHEELS_DIR ROCM_STACK_VLLM_WHEEL_DIR -} - -if [[ $# -gt 0 ]]; then - rocm_stack_init "$1" -fi diff --git a/.buildkite/scripts/upload-rocm-wheels.sh b/.buildkite/scripts/upload-rocm-wheels.sh index 5cf32bcfb358..d303f16eae0e 100755 --- a/.buildkite/scripts/upload-rocm-wheels.sh +++ b/.buildkite/scripts/upload-rocm-wheels.sh @@ -4,8 +4,8 @@ # # Upload ROCm wheels to S3 with proper index generation # -# Usage: upload-rocm-wheels.sh -# base-dockerfile the ROCm stack, e.g. docker/Dockerfile.rocm_base (see rocm/stack.sh) +# Usage: upload-rocm-wheels.sh +# stack rocm (TheRock); dir the stack's wheels plus the vLLM wheel # # Required environment variables: # AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY (or IAM role) @@ -23,11 +23,11 @@ set -ex # ======== Configuration ======== BUCKET="${S3_BUCKET:-vllm-wheels}" -# shellcheck source=.buildkite/scripts/rocm/stack.sh -source "$(dirname "${BASH_SOURCE[0]}")/rocm/stack.sh" "${1:?Usage: $0 }" +STACK="${1:?Usage: $0 }" +WHEEL_DIR="${2:?Usage: $0 }" +VARIANT=$(basename "$WHEEL_DIR"/vllm-*.whl | grep -oE 'rocm[0-9]+' | tail -1) ROCM_SUBPATH="rocm/${BUILDKITE_COMMIT}" -WHEEL_DIRS="$ROCM_STACK_BASE_WHEELS_DIR $ROCM_STACK_VLLM_WHEEL_DIR" -WHEEL_SUBPATH="$ROCM_SUBPATH/${ROCM_STACK_VARIANT}-wheels" +WHEEL_SUBPATH="$ROCM_SUBPATH/${VARIANT:?no vLLM wheel in $WHEEL_DIR}-wheels" S3_WHEEL_PREFIX="s3://$BUCKET/$WHEEL_SUBPATH/" INDICES_OUTPUT_DIR="rocm-indices" @@ -61,9 +61,7 @@ source .buildkite/scripts/lib/manylinux.sh # Collect all wheels rm -rf all-rocm-wheels "$INDICES_OUTPUT_DIR" mkdir -p all-rocm-wheels -for dir in $WHEEL_DIRS; do - cp "$dir"/*.whl all-rocm-wheels/ 2>/dev/null || true -done +cp "$WHEEL_DIR"/*.whl all-rocm-wheels/ 2>/dev/null || true WHEEL_COUNT=$(find all-rocm-wheels -maxdepth 1 -name '*.whl' 2>/dev/null | wc -l) echo "Total wheels to upload: $WHEEL_COUNT" @@ -119,10 +117,10 @@ sed -i 's/import pybase64 as base64/import base64/g' tools/vllm-rocm/therock_whe INDEX_ARGS=(--version "$ROCM_SUBPATH" --wheel-dir "${WHEEL_SUBPATH#rocm/}") # TheRock's SDK, device kernels and torchvision/torchaudio are linked, not hosted -if [[ "$ROCM_STACK_THEROCK" == "1" ]]; then +if [[ "$STACK" == "rocm" ]]; then $PYTHON tools/vllm-rocm/therock_wheels.py external-links \ - --dockerfile "$ROCM_STACK_BASE_DOCKERFILE" \ - --torch-wheel "$(ls "$ROCM_STACK_BASE_WHEELS_DIR"/torch-*.whl)" > therock-external-links.txt + --dockerfile docker/Dockerfile.rocm_base \ + --torch-wheel "$(ls "$WHEEL_DIR"/torch-*.whl)" > therock-external-links.txt cat therock-external-links.txt INDEX_ARGS+=(--external-links therock-external-links.txt) fi @@ -191,11 +189,11 @@ echo "Wheels available at:" echo " s3://$BUCKET/$WHEEL_SUBPATH/" echo "" echo "Install command (by commit):" -echo " pip install vllm --extra-index-url https://${BUCKET}.s3.amazonaws.com/$ROCM_SUBPATH/$ROCM_STACK_VARIANT/" +echo " pip install vllm --extra-index-url https://${BUCKET}.s3.amazonaws.com/$ROCM_SUBPATH/$VARIANT/" echo "" if [[ "${NIGHTLY:-0}" == "1" ]]; then echo "Install command (nightly):" - echo " pip install vllm --extra-index-url https://${BUCKET}.s3.amazonaws.com/rocm/nightly/$ROCM_STACK_VARIANT/" + echo " pip install vllm --extra-index-url https://${BUCKET}.s3.amazonaws.com/rocm/nightly/$VARIANT/" fi echo "" echo "Wheel count: $WHEEL_COUNT" diff --git a/.buildkite/test-amd.yaml b/.buildkite/test-amd.yaml index cfa5a1575888..173d8643eedd 100644 --- a/.buildkite/test-amd.yaml +++ b/.buildkite/test-amd.yaml @@ -14,12 +14,9 @@ # command(str): the single command to run for tests. incompatible with commands. # commands(list): the list of commands to run for the test. incompatible with command. # mirror_hardwares(list): selector tags for AMD pipelines that should include the test. -# dind(bool): when false, run the job directly in the AMD Kubernetes pod. -# When true or omitted, use the legacy Docker-in-Docker path. +# dind(bool): must be false: the job runs directly in the AMD Kubernetes pod (no Docker-in-Docker). # num_gpus(int): override the number of GPUs for the test. defaults to 1 GPU. currently supports 2,4. -# num_nodes(int): whether to simulate multi-node setup by launching multiple containers on one host, -# in this case, commands must be specified. the first command runs on the first host, the second -# command runs on the second host. +# num_nodes(int): not supported on AMD (num_nodes >= 2 fails). # timeout_in_minutes(int): sets a timeout for the step in minutes. if not specified, uses the default timeout. # parallelism(int): number of parallel jobs to run for this step. enables test sharding using $$BUILDKITE_PARALLEL_JOB # and $$BUILDKITE_PARALLEL_JOB_COUNT environment variables. @@ -121,18 +118,26 @@ steps: working_dir: "/vllm-workspace/tests" source_file_dependencies: - .buildkite/scripts/docker-build-metadata-args.sh - - .buildkite/scripts/ci-bake-rocm.sh - .buildkite/release-pipeline.yaml - docker/Dockerfile - docker/Dockerfile.cpu - docker/Dockerfile.rocm - docker/Dockerfile.rocm_base - - docker/ci-rocm.hcl - docker/docker-bake.hcl - docker/docker-bake-rocm.hcl + - .buildkite/scripts/rocm/build.sh + - .buildkite/scripts/rocm/publish.sh + - tools/vllm-rocm/therock_wheels.py - tests/tools/test_docker_build_metadata_args.py + - tests/tools/test_rocm_build.py + - tests/tools/test_amd_ci_fast_exit.py + - .buildkite/scripts/hardware_ci/amd_ci_fast_exit.py + - tests/tools/test_kernel_symbol_map.py + - tools/ci/kernel_symbol_map.py + - tools/ci/rocm_kernel_symbol_map.py commands: - - pytest -v -s tools/test_docker_build_metadata_args.py + - pytest -v -s tools/test_docker_build_metadata_args.py tools/test_rocm_build.py tools/test_amd_ci_fast_exit.py + - pytest -v -s tools/test_kernel_symbol_map.py # Disabled until torch-abi-audit transitive dependencies are locked. # - label: ":amd: (MI300) Torch Stable ABI Audit" # TBD @@ -274,29 +279,27 @@ steps: #------------------------------------------------------------ mi250 · v1 -------------------------------------------------------------# -# Disabled until the pytest plugins installation is locked. -# - label: ":amd: (MI250) Batch Invariance" # TBD -# timeout_in_minutes: 60 -# mirror_hardwares: [amdexperimental, amdproduction, amdgfx90anightly, amdmi250] -# dind: false -# agent_pool: mi250_1 -# optional: true -# working_dir: "/vllm-workspace/tests" -# source_file_dependencies: -# - vllm/v1/attention -# - vllm/model_executor/layers -# - vllm/model_executor/kernels/linear -# - vllm/model_executor/determinism -# - tests/v1/determinism/ -# - vllm/_aiter_ops.py -# - vllm/platforms/rocm.py -# commands: -# - export VLLM_GPU_MEMORY_UTILIZATION=0.8 -# - export VLLM_TEST_CLEAN_GPU_MEMORY=1 -# - export VLLM_WORKER_MULTIPROC_METHOD=spawn -# - pip install pytest-timeout pytest-forked -# - pytest -v -s v1/determinism/test_batch_invariance.py -# - VLLM_TEST_MODEL=deepseek-ai/DeepSeek-V2-Lite-Chat pytest -v -s v1/determinism/test_batch_invariance.py::test_v1_generation_is_deterministic_across_batch_sizes_with_needle -k TRITON_MLA +- label: ":amd: (MI250) Batch Invariance" # TBD + timeout_in_minutes: 60 + mirror_hardwares: [amdexperimental, amdproduction, amdgfx90anightly, amdmi250] + dind: false + agent_pool: mi250_1 + optional: true + working_dir: "/vllm-workspace/tests" + source_file_dependencies: + - vllm/v1/attention + - vllm/model_executor/layers + - vllm/model_executor/kernels/linear + - vllm/model_executor/determinism + - tests/v1/determinism/ + - vllm/_aiter_ops.py + - vllm/platforms/rocm.py + commands: + - export VLLM_GPU_MEMORY_UTILIZATION=0.8 + - export VLLM_TEST_CLEAN_GPU_MEMORY=1 + - export VLLM_WORKER_MULTIPROC_METHOD=spawn + - pytest -v -s v1/determinism/test_batch_invariance.py + - VLLM_TEST_MODEL=deepseek-ai/DeepSeek-V2-Lite-Chat pytest -v -s v1/determinism/test_batch_invariance.py::test_v1_generation_is_deterministic_across_batch_sizes_with_needle -k TRITON_MLA - label: ":amd: (MI250) CUDAGraph" # TBD timeout_in_minutes: 55 @@ -473,7 +476,9 @@ steps: 1) pytest models/transformers/test_backend.py -v -s -m 'distributed(num_gpus=2)' && pytest models/language -v -s -m 'distributed(num_gpus=2)' && - VLLM_ROCM_USE_AITER=1 pytest -v -s models/test_hyv4_rocm.py -m 'distributed(num_gpus=2)' ;; + VLLM_ROCM_USE_AITER=1 pytest -v -s models/test_hyv4_rocm.py -m 'distributed(num_gpus=2)' && + pytest -v -s models/test_mimo_v2_attention_backend.py && + pytest -v -s models/multimodal/test_mimo_v2_omni.py ;; 2) pytest models/multimodal -v -s -m 'distributed(num_gpus=2)' --ignore models/multimodal/generation/test_whisper.py && @@ -887,25 +892,23 @@ steps: - pytest -v -s -x lora/test_mixtral.py - pytest -v -s models/test_vision.py -m 'distributed(num_gpus=4)' -# Disabled until the KV connectors installation is locked. -# - label: ":amd: (MI300) Elastic EP Scaling" # TBD -# timeout_in_minutes: 55 -# mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300] -# dind: false -# agent_pool: mi300_4 -# num_gpus: 4 -# optional: true -# working_dir: "/vllm-workspace/tests" -# source_file_dependencies: -# - vllm/distributed/ -# - vllm/engine/ -# - vllm/v1/executor/ -# - vllm/compilation/ -# - tests/distributed/ -# - vllm/platforms/rocm.py -# commands: -# - uv pip install --system -r /vllm-workspace/requirements/kv_connectors_rocm.txt -# - pytest -v -s distributed/test_elastic_ep.py +- label: ":amd: (MI300) Elastic EP Scaling" # TBD + timeout_in_minutes: 55 + mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300] + dind: false + agent_pool: mi300_4 + num_gpus: 4 + optional: true + working_dir: "/vllm-workspace/tests" + source_file_dependencies: + - vllm/distributed/ + - vllm/engine/ + - vllm/v1/executor/ + - vllm/compilation/ + - tests/distributed/ + - vllm/platforms/rocm.py + commands: + - pytest -v -s distributed/test_elastic_ep.py - label: ":amd: (MI300) RayExecutorV2" # TBD timeout_in_minutes: 75 @@ -1568,8 +1571,6 @@ steps: - vllm/ - tests/models/language/generation commands: - - MAMBA_FORCE_BUILD=TRUE uv pip install --system --no-build-isolation 'git+https://github.com/AndreasKaratzas/mamba@fix-rocm-7.0-warp-size-constexpr' - - CAUSAL_CONV1D_FORCE_BUILD=TRUE uv pip install --system --no-build-isolation 'git+https://github.com/Dao-AILab/causal-conv1d@v1.6.0' - pytest -v -s models/language/generation -m '(not core_model) and (not hybrid_model)' #---------------------------------------------------- mi300 · models / multimodal ----------------------------------------------------# @@ -2020,33 +2021,31 @@ steps: #------------------------------------------------------------ mi300 · v1 -------------------------------------------------------------# -# Disabled until the pytest plugins installation is locked. -# - label: ":amd: (MI300) Batch Invariance" # TBD -# timeout_in_minutes: 60 -# mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300] -# dind: false -# agent_pool: mi300_1 -# optional: true -# working_dir: "/vllm-workspace/tests" -# source_file_dependencies: -# - vllm/v1/attention -# - vllm/model_executor/layers -# - vllm/lora/ops/triton_ops -# - vllm/model_executor/kernels/linear -# - vllm/model_executor/determinism -# - tests/v1/determinism/ -# - vllm/_aiter_ops.py -# - vllm/platforms/rocm.py -# commands: -# - export VLLM_WORKER_MULTIPROC_METHOD=spawn -# - pip install pytest-timeout pytest-forked -# - pytest -v -s v1/determinism/test_batch_invariance.py -# - pytest -v -s v1/determinism/test_rms_norm_batch_invariant.py -# - pytest -v -s v1/determinism/test_lora_shrink_batch_invariant.py -# - pytest -v -s v1/determinism/test_cutlass_batch_invariance.py -# - pytest -v -s v1/determinism/test_matmul_batch_invariant.py -# - VLLM_TEST_MODEL=deepseek-ai/DeepSeek-V2-Lite-Chat pytest -v -s v1/determinism/test_batch_invariance.py::test_v1_generation_is_deterministic_across_batch_sizes_with_needle -k TRITON_MLA -# - VLLM_TEST_MODEL=Qwen/Qwen3-30B-A3B-Thinking-2507-FP8 pytest -v -s v1/determinism/test_batch_invariance.py::test_v1_generation_is_deterministic_across_batch_sizes_with_needle -k TRITON_ATTN +- label: ":amd: (MI300) Batch Invariance" # TBD + timeout_in_minutes: 60 + mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300] + dind: false + agent_pool: mi300_1 + optional: true + working_dir: "/vllm-workspace/tests" + source_file_dependencies: + - vllm/v1/attention + - vllm/model_executor/layers + - vllm/lora/ops/triton_ops + - vllm/model_executor/kernels/linear + - vllm/model_executor/determinism + - tests/v1/determinism/ + - vllm/_aiter_ops.py + - vllm/platforms/rocm.py + commands: + - export VLLM_WORKER_MULTIPROC_METHOD=spawn + - pytest -v -s v1/determinism/test_batch_invariance.py + - pytest -v -s v1/determinism/test_rms_norm_batch_invariant.py + - pytest -v -s v1/determinism/test_lora_shrink_batch_invariant.py + - pytest -v -s v1/determinism/test_cutlass_batch_invariance.py + - pytest -v -s v1/determinism/test_matmul_batch_invariant.py + - VLLM_TEST_MODEL=deepseek-ai/DeepSeek-V2-Lite-Chat pytest -v -s v1/determinism/test_batch_invariance.py::test_v1_generation_is_deterministic_across_batch_sizes_with_needle -k TRITON_MLA + - VLLM_TEST_MODEL=Qwen/Qwen3-30B-A3B-Thinking-2507-FP8 pytest -v -s v1/determinism/test_batch_invariance.py::test_v1_generation_is_deterministic_across_batch_sizes_with_needle -k TRITON_ATTN - label: ":amd: (MI300) Batch Invariance (Distributed)" # TBD timeout_in_minutes: 45 @@ -2070,6 +2069,7 @@ steps: - VLLM_TEST_TP_SIZE=4 VLLM_TEST_DISABLE_CUSTOM_AR=1 pytest -v -s v1/determinism/test_batch_invariance.py -k bs1_vs_bsN - VLLM_TP_SIZE=4 VLLM_NEEDLE_TRIALS=2 pytest -v -s v1/determinism/test_batch_invariance.py::test_v1_generation_is_deterministic_across_batch_sizes_with_needle -k TRITON_ATTN - VLLM_TEST_MODEL=allenai/OLMoE-1B-7B-0924 VLLM_TEST_SERVER_ARGS="--data-parallel-size 4 --enable-expert-parallel --max-model-len 4096" pytest -v -s v1/determinism/test_online_batch_invariance.py + - VLLM_TEST_MODEL=ibm-granite/granite-3.0-1b-a400m-base VLLM_TEST_SERVER_ARGS="--data-parallel-size 4 --enable-expert-parallel --max-model-len 4096" pytest -v -s v1/determinism/test_online_batch_invariance.py - label: ":amd: (MI300) V1 Spec Decode Shard %N" # TBD timeout_in_minutes: 50 @@ -2159,7 +2159,9 @@ steps: - vllm/model_executor/model_loader/ - vllm/model_executor/models/extract_hidden_states.py - vllm/model_executor/models/llama.py + - vllm/models/qwen3_5/ - vllm/model_executor/models/qwen3_5.py + - vllm/model_executor/models/qwen3_5_mtp.py - vllm/model_executor/models/qwen3_next.py - vllm/model_executor/models/registry.py - vllm/transformers_utils/configs/extract_hidden_states.py @@ -2204,39 +2206,33 @@ steps: - VLLM_ROCM_USE_AITER=1 pytest -v -s v1/attention/test_sparse_mla_backends.py --shard-id=$$BUILDKITE_PARALLEL_JOB --num-shards=$$BUILDKITE_PARALLEL_JOB_COUNT -# Disabled until the tracing packages installation is locked. -# - label: ":amd: (MI300) Metrics, Tracing" # TBD -# timeout_in_minutes: 30 -# mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300] -# dind: false -# agent_pool: mi300_2 -# optional: true -# num_gpus: 2 -# working_dir: "/vllm-workspace/tests" -# source_file_dependencies: -# - vllm/ -# - tests/v1/tracing -# - tests/tracing/ -# - vllm/config/ -# - vllm/distributed/ -# - vllm/engine/ -# - vllm/inputs/ -# - vllm/model_executor/ -# - vllm/multimodal/ -# - vllm/platforms/ -# - vllm/sampling_params.py -# - vllm/tracing/ -# - vllm/transformers_utils/ -# - vllm/utils/ -# - vllm/v1/ -# commands: -# - "pip install \ -# 'opentelemetry-sdk>=1.26.0' \ -# 'opentelemetry-api>=1.26.0' \ -# 'opentelemetry-exporter-otlp>=1.26.0' \ -# 'opentelemetry-semantic-conventions-ai>=0.4.1'" -# - pytest -v -s v1/tracing -# - pytest -v -s tracing +- label: ":amd: (MI300) Metrics, Tracing" # TBD + timeout_in_minutes: 30 + mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300] + dind: false + agent_pool: mi300_2 + optional: true + num_gpus: 2 + working_dir: "/vllm-workspace/tests" + source_file_dependencies: + - vllm/ + - tests/v1/tracing + - tests/tracing/ + - vllm/config/ + - vllm/distributed/ + - vllm/engine/ + - vllm/inputs/ + - vllm/model_executor/ + - vllm/multimodal/ + - vllm/platforms/ + - vllm/sampling_params.py + - vllm/tracing/ + - vllm/transformers_utils/ + - vllm/utils/ + - vllm/v1/ + commands: + - pytest -v -s v1/tracing + - pytest -v -s tracing - label: ":amd: (MI300) V1 E2E" # TBD timeout_in_minutes: 30 @@ -2325,40 +2321,36 @@ steps: - export BASELINE_FILE='/tmp/mooncake-ec-e2e/baseline.json' - bash tests/v1/ec_connector/integration/run_epd_mooncake_ec_full_pipeline.sh -# Disabled until the KV connectors installation is locked. -# - label: ":amd: (MI300) NixlConnector PD edge case" # TBD -# timeout_in_minutes: 65 -# mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300] -# dind: false -# agent_pool: mi300_2 -# num_gpus: 2 -# optional: true -# working_dir: "/vllm-workspace/tests" -# source_file_dependencies: -# - vllm/distributed/kv_transfer/kv_connector/v1/nixl/ -# - vllm/v1/core/sched/ -# - tests/v1/kv_connector/nixl_integration/ -# - vllm/platforms/rocm.py -# commands: -# - uv pip install --system -r /vllm-workspace/requirements/kv_connectors_rocm.txt -# - PREFILL_GPU_ID=0 DECODE_GPU_ID=1 ATTENTION_BACKEND=TRITON_ATTN bash v1/kv_connector/nixl_integration/run_edge_case_test.sh +- label: ":amd: (MI300) NixlConnector PD edge case" # TBD + timeout_in_minutes: 65 + mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300] + dind: false + agent_pool: mi300_2 + num_gpus: 2 + optional: true + working_dir: "/vllm-workspace/tests" + source_file_dependencies: + - vllm/distributed/kv_transfer/kv_connector/v1/nixl/ + - vllm/v1/core/sched/ + - tests/v1/kv_connector/nixl_integration/ + - vllm/platforms/rocm.py + commands: + - PREFILL_GPU_ID=0 DECODE_GPU_ID=1 ATTENTION_BACKEND=TRITON_ATTN bash v1/kv_connector/nixl_integration/run_edge_case_test.sh -# Disabled until the KV connectors installation is locked. -# - label: ":amd: (MI300) CrossLayer KV layout Distributed NixlConnector PD accuracy" # TBD -# timeout_in_minutes: 60 -# mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300] -# dind: false -# agent_pool: mi300_4 -# num_gpus: 4 -# optional: true -# working_dir: "/vllm-workspace/tests" -# source_file_dependencies: -# - vllm/distributed/kv_transfer/kv_connector/v1/nixl/ -# - tests/v1/kv_connector/nixl_integration/ -# - vllm/platforms/rocm.py -# commands: -# - uv pip install --system -r /vllm-workspace/requirements/kv_connectors_rocm.txt -# - CROSS_LAYERS_BLOCKS=True ATTENTION_BACKEND=TRITON_ATTN bash v1/kv_connector/nixl_integration/config_sweep_accuracy_test.sh +- label: ":amd: (MI300) CrossLayer KV layout Distributed NixlConnector PD accuracy" # TBD + timeout_in_minutes: 60 + mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300] + dind: false + agent_pool: mi300_4 + num_gpus: 4 + optional: true + working_dir: "/vllm-workspace/tests" + source_file_dependencies: + - vllm/distributed/kv_transfer/kv_connector/v1/nixl/ + - tests/v1/kv_connector/nixl_integration/ + - vllm/platforms/rocm.py + commands: + - CROSS_LAYERS_BLOCKS=True ATTENTION_BACKEND=TRITON_ATTN bash v1/kv_connector/nixl_integration/config_sweep_accuracy_test.sh - label: ":amd: (MI300) Distributed DP Extended" # TBD timeout_in_minutes: 75 @@ -2384,133 +2376,119 @@ steps: - pytest -v -s v1/engine/test_engine_core_client.py::test_kv_cache_events_dp - pytest -v -s distributed/test_utils.py -# Disabled until the KV connectors installation is locked. -# - label: ":amd: (MI300) Distributed NixlConnector PD accuracy" # TBD -# timeout_in_minutes: 45 -# mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300] -# dind: false -# agent_pool: mi300_4 -# num_gpus: 4 -# optional: true -# working_dir: "/vllm-workspace/tests" -# source_file_dependencies: -# - vllm/distributed/kv_transfer/kv_connector/v1/nixl/ -# - tests/v1/kv_connector/nixl_integration/ -# - vllm/platforms/rocm.py -# commands: -# - uv pip install --system -r /vllm-workspace/requirements/kv_connectors_rocm.txt -# - ATTENTION_BACKEND=TRITON_ATTN bash v1/kv_connector/nixl_integration/config_sweep_accuracy_test.sh +- label: ":amd: (MI300) Distributed NixlConnector PD accuracy" # TBD + timeout_in_minutes: 45 + mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300] + dind: false + agent_pool: mi300_4 + num_gpus: 4 + optional: true + working_dir: "/vllm-workspace/tests" + source_file_dependencies: + - vllm/distributed/kv_transfer/kv_connector/v1/nixl/ + - tests/v1/kv_connector/nixl_integration/ + - vllm/platforms/rocm.py + commands: + - ATTENTION_BACKEND=TRITON_ATTN bash v1/kv_connector/nixl_integration/config_sweep_accuracy_test.sh -# Disabled until the KV connectors installation is locked. -# - label: ":amd: (MI300) Push NixlConnector PP prefill PD accuracy" # TBD -# timeout_in_minutes: 55 -# mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300] -# dind: false -# agent_pool: mi300_4 -# num_gpus: 4 -# optional: true -# working_dir: "/vllm-workspace/tests" -# source_file_dependencies: -# - vllm/distributed/kv_transfer/kv_connector/v1/nixl/ -# - tests/v1/kv_connector/nixl_integration/ -# - tests/v1/kv_connector/nixl_push_integration/ -# - vllm/platforms/rocm.py -# commands: -# - uv pip install --system -r /vllm-workspace/requirements/kv_connectors_rocm.txt -# - ATTENTION_BACKEND=TRITON_ATTN bash v1/kv_connector/nixl_push_integration/config_sweep_accuracy_test.sh +- label: ":amd: (MI300) Push NixlConnector PP prefill PD accuracy" # TBD + timeout_in_minutes: 55 + mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300] + dind: false + agent_pool: mi300_4 + num_gpus: 4 + optional: true + working_dir: "/vllm-workspace/tests" + source_file_dependencies: + - vllm/distributed/kv_transfer/kv_connector/v1/nixl/ + - tests/v1/kv_connector/nixl_integration/ + - tests/v1/kv_connector/nixl_push_integration/ + - vllm/platforms/rocm.py + commands: + - ATTENTION_BACKEND=TRITON_ATTN bash v1/kv_connector/nixl_push_integration/config_sweep_accuracy_test.sh -# Disabled until the KV connectors installation is locked. -# - label: ":amd: (MI300) DP EP Distributed NixlConnector PD accuracy" # TBD -# timeout_in_minutes: 30 -# mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300] -# dind: false -# agent_pool: mi300_4 -# num_gpus: 4 -# optional: true -# working_dir: "/vllm-workspace/tests" -# source_file_dependencies: -# - vllm/distributed/kv_transfer/kv_connector/v1/nixl/ -# - tests/v1/kv_connector/nixl_integration/ -# - vllm/platforms/rocm.py -# commands: -# - uv pip install --system -r /vllm-workspace/requirements/kv_connectors_rocm.txt -# - DP_EP=1 ATTENTION_BACKEND=TRITON_ATTN bash v1/kv_connector/nixl_integration/config_sweep_accuracy_test.sh +- label: ":amd: (MI300) DP EP Distributed NixlConnector PD accuracy" # TBD + timeout_in_minutes: 30 + mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300] + dind: false + agent_pool: mi300_4 + num_gpus: 4 + optional: true + working_dir: "/vllm-workspace/tests" + source_file_dependencies: + - vllm/distributed/kv_transfer/kv_connector/v1/nixl/ + - tests/v1/kv_connector/nixl_integration/ + - vllm/platforms/rocm.py + commands: + - DP_EP=1 ATTENTION_BACKEND=TRITON_ATTN bash v1/kv_connector/nixl_integration/config_sweep_accuracy_test.sh -# Disabled until the KV connectors installation is locked. -# - label: ":amd: (MI300) Hybrid SSM NixlConnector PD accuracy" # TBD -# timeout_in_minutes: 80 -# mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300] -# dind: false -# agent_pool: mi300_4 -# num_gpus: 4 -# optional: true -# working_dir: "/vllm-workspace/tests" -# source_file_dependencies: -# - vllm/distributed/kv_transfer/kv_connector/v1/nixl/ -# - tests/v1/kv_connector/nixl_integration/ -# - vllm/platforms/rocm.py -# commands: -# - uv pip install --system -r /vllm-workspace/requirements/kv_connectors_rocm.txt -# - HYBRID_SSM=1 ATTENTION_BACKEND=TRITON_ATTN bash v1/kv_connector/nixl_integration/config_sweep_accuracy_test.sh +- label: ":amd: (MI300) Hybrid SSM NixlConnector PD accuracy" # TBD + timeout_in_minutes: 80 + mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300] + dind: false + agent_pool: mi300_4 + num_gpus: 4 + optional: true + working_dir: "/vllm-workspace/tests" + source_file_dependencies: + - vllm/distributed/kv_transfer/kv_connector/v1/nixl/ + - tests/v1/kv_connector/nixl_integration/ + - vllm/platforms/rocm.py + commands: + - HYBRID_SSM=1 ATTENTION_BACKEND=TRITON_ATTN bash v1/kv_connector/nixl_integration/config_sweep_accuracy_test.sh -# Disabled until the KV connectors installation is locked. -# - label: ":amd: (MI300) Hybrid SSM NixlConnector PD prefix cache" # TBD -# timeout_in_minutes: 25 -# mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300] -# dind: false -# agent_pool: mi300_2 -# num_gpus: 2 -# optional: true -# working_dir: "/vllm-workspace/tests" -# source_file_dependencies: -# - vllm/distributed/kv_transfer/kv_connector/v1/nixl/ -# - vllm/v1/core/sched/ -# - vllm/v1/core/kv_cache_coordinator.py -# - tests/v1/kv_connector/nixl_integration/ -# - vllm/platforms/rocm.py -# commands: -# - uv pip install --system -r /vllm-workspace/requirements/kv_connectors_rocm.txt -# - ATTENTION_BACKEND=TRITON_ATTN bash v1/kv_connector/nixl_integration/run_mamba_prefix_cache_test.sh +- label: ":amd: (MI300) Hybrid SSM NixlConnector PD prefix cache" # TBD + timeout_in_minutes: 25 + mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300] + dind: false + agent_pool: mi300_2 + num_gpus: 2 + optional: true + working_dir: "/vllm-workspace/tests" + source_file_dependencies: + - vllm/distributed/kv_transfer/kv_connector/v1/nixl/ + - vllm/v1/core/sched/ + - vllm/v1/core/kv_cache_coordinator.py + - tests/v1/kv_connector/nixl_integration/ + - vllm/platforms/rocm.py + commands: + - ATTENTION_BACKEND=TRITON_ATTN bash v1/kv_connector/nixl_integration/run_mamba_prefix_cache_test.sh -# Disabled until the KV connectors installation is locked. -# - label: ":amd: (MI300) MultiConnector (Nixl+Offloading) PD accuracy" # TBD -# timeout_in_minutes: 30 -# mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300] -# dind: false -# agent_pool: mi300_2 -# num_gpus: 2 -# optional: true -# working_dir: "/vllm-workspace/tests" -# source_file_dependencies: -# - vllm/distributed/kv_transfer/kv_connector/v1/nixl/ -# - vllm/distributed/kv_transfer/kv_connector/v1/multi_connector.py -# - vllm/distributed/kv_transfer/kv_connector/v1/offloading_connector.py -# - vllm/distributed/kv_transfer/kv_connector/v1/offloading/ -# - tests/v1/kv_connector/nixl_integration/ -# - vllm/platforms/rocm.py -# commands: -# - uv pip install --system -r /vllm-workspace/requirements/kv_connectors_rocm.txt -# - ATTENTION_BACKEND=TRITON_ATTN bash v1/kv_connector/nixl_integration/run_multi_connector_accuracy_test.sh +- label: ":amd: (MI300) MultiConnector (Nixl+Offloading) PD accuracy" # TBD + timeout_in_minutes: 30 + mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300] + dind: false + agent_pool: mi300_2 + num_gpus: 2 + optional: true + working_dir: "/vllm-workspace/tests" + source_file_dependencies: + - vllm/distributed/kv_transfer/kv_connector/v1/nixl/ + - vllm/distributed/kv_transfer/kv_connector/v1/multi_connector.py + - vllm/distributed/kv_transfer/kv_connector/v1/offloading_connector.py + - vllm/distributed/kv_transfer/kv_connector/v1/offloading/ + - tests/v1/kv_connector/nixl_integration/ + - vllm/platforms/rocm.py + commands: + - ATTENTION_BACKEND=TRITON_ATTN bash v1/kv_connector/nixl_integration/run_multi_connector_accuracy_test.sh -# Disabled until the KV connectors installation is locked. -# - label: ":amd: (MI300) MultiConnector (Nixl+Offloading) PD edge cases" # TBD -# timeout_in_minutes: 25 -# mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300] -# dind: false -# agent_pool: mi300_2 -# num_gpus: 2 -# optional: true -# working_dir: "/vllm-workspace/tests" -# source_file_dependencies: -# - vllm/distributed/kv_transfer/kv_connector/v1/nixl/ -# - vllm/distributed/kv_transfer/kv_connector/v1/multi_connector.py -# - vllm/distributed/kv_transfer/kv_connector/v1/offloading_connector.py -# - vllm/distributed/kv_transfer/kv_connector/v1/offloading/ -# - tests/v1/kv_connector/nixl_integration/ -# - vllm/platforms/rocm.py -# commands: -# - uv pip install --system -r /vllm-workspace/requirements/kv_connectors_rocm.txt -# - ATTENTION_BACKEND=TRITON_ATTN bash v1/kv_connector/nixl_integration/run_multi_connector_edge_case_test.sh +- label: ":amd: (MI300) MultiConnector (Nixl+Offloading) PD edge cases" # TBD + timeout_in_minutes: 25 + mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300] + dind: false + agent_pool: mi300_2 + num_gpus: 2 + optional: true + working_dir: "/vllm-workspace/tests" + source_file_dependencies: + - vllm/distributed/kv_transfer/kv_connector/v1/nixl/ + - vllm/distributed/kv_transfer/kv_connector/v1/multi_connector.py + - vllm/distributed/kv_transfer/kv_connector/v1/offloading_connector.py + - vllm/distributed/kv_transfer/kv_connector/v1/offloading/ + - tests/v1/kv_connector/nixl_integration/ + - vllm/platforms/rocm.py + commands: + - ATTENTION_BACKEND=TRITON_ATTN bash v1/kv_connector/nixl_integration/run_multi_connector_edge_case_test.sh ######################################################################################################################################### # # @@ -2928,132 +2906,117 @@ steps: - VLLM_WORKER_MULTIPROC_METHOD=spawn pytest -v -s -rs kernels/moe/test_moe_layer.py::test_moe_layer_mori_graph - VLLM_TEST_ENABLE_MORI_MOE_LAYER=1 VLLM_WORKER_MULTIPROC_METHOD=spawn pytest -v -s -rs 'kernels/moe/test_moe_layer.py::test_moe_layer[False-mori_high_throughput-2-1-True]' --subtests='[32-512-512-8-2-bfloat16-None-False-False-False-False-mori_high_throughput-2-2-1]' -# Disabled until the KV connectors installation is locked. -# - label: ":amd: (MI355) NixlConnector PD + Spec Decode acceptance" # TBD -# timeout_in_minutes: 45 -# mirror_hardwares: [amdexperimental, amdproduction, amdgfx950nightly, amdmi355] -# dind: false -# agent_pool: mi355_2 -# num_gpus: 2 -# optional: true -# working_dir: "/vllm-workspace/tests" -# source_file_dependencies: -# - vllm/distributed/kv_transfer/kv_connector/v1/nixl/ -# - vllm/v1/worker/kv_connector_model_runner_mixin.py -# - tests/v1/kv_connector/nixl_integration/ -# - vllm/platforms/rocm.py -# commands: -# - uv pip install --system -r /vllm-workspace/requirements/kv_connectors_rocm.txt -# - KV_CACHE_MEMORY_BYTES=8G ATTENTION_BACKEND=TRITON_ATTN bash v1/kv_connector/nixl_integration/config_sweep_spec_decode_test.sh +- label: ":amd: (MI355) NixlConnector PD + Spec Decode acceptance" # TBD + timeout_in_minutes: 45 + mirror_hardwares: [amdexperimental, amdproduction, amdgfx950nightly, amdmi355] + dind: false + agent_pool: mi355_2 + num_gpus: 2 + optional: true + working_dir: "/vllm-workspace/tests" + source_file_dependencies: + - vllm/distributed/kv_transfer/kv_connector/v1/nixl/ + - vllm/v1/worker/kv_connector_model_runner_mixin.py + - tests/v1/kv_connector/nixl_integration/ + - vllm/platforms/rocm.py + commands: + - KV_CACHE_MEMORY_BYTES=8G ATTENTION_BACKEND=TRITON_ATTN bash v1/kv_connector/nixl_integration/config_sweep_spec_decode_test.sh -# Disabled until the KV connectors installation is locked. -# - label: ":amd: (MI355) Distributed NixlConnector PD accuracy" # TBD -# timeout_in_minutes: 60 -# mirror_hardwares: [amdexperimental, amdproduction, amdgfx950nightly, amdmi355] -# dind: false -# agent_pool: mi355_4 -# num_gpus: 4 -# optional: true -# working_dir: "/vllm-workspace/tests" -# source_file_dependencies: -# - vllm/distributed/kv_transfer/kv_connector/v1/nixl/ -# - tests/v1/kv_connector/nixl_integration/ -# - vllm/platforms/rocm.py -# commands: -# - uv pip install --system -r /vllm-workspace/requirements/kv_connectors_rocm.txt -# - ATTENTION_BACKEND=TRITON_ATTN bash v1/kv_connector/nixl_integration/config_sweep_accuracy_test.sh +- label: ":amd: (MI355) Distributed NixlConnector PD accuracy" # TBD + timeout_in_minutes: 60 + mirror_hardwares: [amdexperimental, amdproduction, amdgfx950nightly, amdmi355] + dind: false + agent_pool: mi355_4 + num_gpus: 4 + optional: true + working_dir: "/vllm-workspace/tests" + source_file_dependencies: + - vllm/distributed/kv_transfer/kv_connector/v1/nixl/ + - tests/v1/kv_connector/nixl_integration/ + - vllm/platforms/rocm.py + commands: + - ATTENTION_BACKEND=TRITON_ATTN bash v1/kv_connector/nixl_integration/config_sweep_accuracy_test.sh -# Disabled until the KV connectors installation is locked. -# - label: ":amd: (MI355) Distributed AITER NixlConnector PD accuracy" -# timeout_in_minutes: 60 -# mirror_hardwares: [amdexperimental, amdproduction, amdgfx950nightly, amdmi355] -# dind: false -# agent_pool: mi355_4 -# num_gpus: 4 -# optional: true -# working_dir: "/vllm-workspace/tests" -# source_file_dependencies: -# - vllm/distributed/kv_transfer/kv_connector/v1/nixl/ -# - tests/v1/kv_connector/nixl_integration/ -# - tests/v1/kv_connector/rocm_pd_accuracy_utils.py -# - tests/utils.py -# - vllm/v1/attention/backends/rocm_aiter_unified_attn.py -# - vllm/platforms/rocm.py -# - requirements/common.txt -# - requirements/kv_connectors_rocm.txt -# commands: -# - uv pip install --system -r /vllm-workspace/requirements/kv_connectors_rocm.txt -# - pytest -v -s v1/kv_connector/nixl_integration/test_accuracy_rocm.py +- label: ":amd: (MI355) Distributed AITER NixlConnector PD accuracy" + timeout_in_minutes: 60 + mirror_hardwares: [amdexperimental, amdproduction, amdgfx950nightly, amdmi355] + dind: false + agent_pool: mi355_4 + num_gpus: 4 + optional: true + working_dir: "/vllm-workspace/tests" + source_file_dependencies: + - vllm/distributed/kv_transfer/kv_connector/v1/nixl/ + - tests/v1/kv_connector/nixl_integration/ + - tests/v1/kv_connector/rocm_pd_accuracy_utils.py + - tests/utils.py + - vllm/v1/attention/backends/rocm_aiter_unified_attn.py + - vllm/platforms/rocm.py + - requirements/common.txt + commands: + - pytest -v -s v1/kv_connector/nixl_integration/test_accuracy_rocm.py -# Disabled until the KV connectors installation is locked. -# - label: ":amd: (MI355) Distributed MooncakeConnector HIP PD accuracy" -# timeout_in_minutes: 30 -# mirror_hardwares: [amdexperimental, amdproduction, amdgfx950nightly, amdmi355] -# dind: false -# agent_pool: mi355_4 -# num_gpus: 4 -# optional: true -# working_dir: "/vllm-workspace/tests" -# source_file_dependencies: -# - vllm/distributed/kv_transfer/kv_connector/v1/mooncake/ -# - tests/v1/kv_connector/mooncake_integration/ -# - tests/v1/kv_connector/rocm_pd_accuracy_utils.py -# - tests/utils.py -# - examples/disaggregated/mooncake_connector/ -# - vllm/v1/attention/backends/rocm_aiter_unified_attn.py -# - vllm/platforms/rocm.py -# - requirements/common.txt -# - requirements/kv_connectors_rocm.txt -# commands: -# - uv pip install --system -r /vllm-workspace/requirements/kv_connectors_rocm.txt -# - pytest -v -s v1/kv_connector/mooncake_integration/test_accuracy_rocm.py +- label: ":amd: (MI355) Distributed MooncakeConnector HIP PD accuracy" + timeout_in_minutes: 30 + mirror_hardwares: [amdexperimental, amdproduction, amdgfx950nightly, amdmi355] + dind: false + agent_pool: mi355_4 + num_gpus: 4 + optional: true + working_dir: "/vllm-workspace/tests" + source_file_dependencies: + - vllm/distributed/kv_transfer/kv_connector/v1/mooncake/ + - tests/v1/kv_connector/mooncake_integration/ + - tests/v1/kv_connector/rocm_pd_accuracy_utils.py + - tests/utils.py + - examples/disaggregated/mooncake_connector/ + - vllm/v1/attention/backends/rocm_aiter_unified_attn.py + - vllm/platforms/rocm.py + - requirements/common.txt + commands: + - pytest -v -s v1/kv_connector/mooncake_integration/test_accuracy_rocm.py -# Disabled until the KV connectors installation is locked. -# - label: ":amd: (MI355) DP EP Distributed NixlConnector PD accuracy" # TBD -# timeout_in_minutes: 40 -# mirror_hardwares: [amdexperimental, amdproduction, amdgfx950nightly, amdmi355] -# dind: false -# agent_pool: mi355_4 -# num_gpus: 4 -# optional: true -# working_dir: "/vllm-workspace/tests" -# source_file_dependencies: -# - vllm/distributed/kv_transfer/kv_connector/v1/nixl/ -# - tests/v1/kv_connector/nixl_integration/ -# - vllm/platforms/rocm.py -# commands: -# - uv pip install --system -r /vllm-workspace/requirements/kv_connectors_rocm.txt -# - DP_EP=1 ATTENTION_BACKEND=TRITON_ATTN bash v1/kv_connector/nixl_integration/config_sweep_accuracy_test.sh +- label: ":amd: (MI355) DP EP Distributed NixlConnector PD accuracy" # TBD + timeout_in_minutes: 40 + mirror_hardwares: [amdexperimental, amdproduction, amdgfx950nightly, amdmi355] + dind: false + agent_pool: mi355_4 + num_gpus: 4 + optional: true + working_dir: "/vllm-workspace/tests" + source_file_dependencies: + - vllm/distributed/kv_transfer/kv_connector/v1/nixl/ + - tests/v1/kv_connector/nixl_integration/ + - vllm/platforms/rocm.py + commands: + - DP_EP=1 ATTENTION_BACKEND=TRITON_ATTN bash v1/kv_connector/nixl_integration/config_sweep_accuracy_test.sh -# Disabled until the KV connectors installation is locked. -# - label: ":amd: (MI355) Kimi-Linear-48B-A3B Disaggregated DP EP" -# timeout_in_minutes: 105 -# mirror_hardwares: [amdexperimental, amdproduction, amdgfx950nightly, amdmi355] -# dind: false -# agent_pool: mi355_4 -# num_gpus: 4 -# optional: true -# working_dir: "/vllm-workspace/tests" -# source_file_dependencies: -# - vllm/platforms/rocm.py -# - vllm/models/kimi_k3/ -# - vllm/distributed/kv_transfer/kv_connector/v1/nixl/ -# - tests/v1/kv_connector/nixl_integration/ -# - requirements/kv_connectors_rocm.txt -# commands: -# - export VLLM_WORKER_MULTIPROC_METHOD=spawn -# - export ENABLE_HMA_FLAG='1' -# - export DP_EP='1' -# - export GPU_MEMORY_UTILIZATION='0.9' -# - export VLLM_SSM_CONV_STATE_LAYOUT='DS' -# - export PREFILLER_TP_SIZE='2' -# - export DECODER_TP_SIZE='2' -# - export MODEL_NAMES='moonshotai/Kimi-Linear-48B-A3B-Instruct' -# - export PREFILL_BLOCK_SIZE='2048' -# - export DECODE_BLOCK_SIZE='2048' -# - export VLLM_SERVE_EXTRA_ARGS='--trust-remote-code,--max-model-len 32768' -# - uv pip install --system -r /vllm-workspace/requirements/kv_connectors_rocm.txt -# - bash v1/kv_connector/nixl_integration/run_accuracy_test.sh +- label: ":amd: (MI355) Kimi-Linear-48B-A3B Disaggregated DP EP" + timeout_in_minutes: 105 + mirror_hardwares: [amdexperimental, amdproduction, amdgfx950nightly, amdmi355] + dind: false + agent_pool: mi355_4 + num_gpus: 4 + optional: true + working_dir: "/vllm-workspace/tests" + source_file_dependencies: + - vllm/platforms/rocm.py + - vllm/models/kimi_k3/ + - vllm/distributed/kv_transfer/kv_connector/v1/nixl/ + - tests/v1/kv_connector/nixl_integration/ + commands: + - export VLLM_WORKER_MULTIPROC_METHOD=spawn + - export ENABLE_HMA_FLAG='1' + - export DP_EP='1' + - export GPU_MEMORY_UTILIZATION='0.9' + - export VLLM_SSM_CONV_STATE_LAYOUT='DS' + - export PREFILLER_TP_SIZE='2' + - export DECODER_TP_SIZE='2' + - export MODEL_NAMES='moonshotai/Kimi-Linear-48B-A3B-Instruct' + - export PREFILL_BLOCK_SIZE='2048' + - export DECODE_BLOCK_SIZE='2048' + - export VLLM_SERVE_EXTRA_ARGS='--trust-remote-code,--max-model-len 32768' + - bash v1/kv_connector/nixl_integration/run_accuracy_test.sh - label: ":amd: (MI355) Distributed Features" # TBD timeout_in_minutes: 90 @@ -3167,45 +3130,42 @@ steps: not +quant_fp8 and not +rms_norm and DeepSeek-Coder and ROCM_AITER_MLA' -# Disabled until the KV connectors installation is locked. -# - label: ":amd: (MI355) DSv4-Flash Disaggregated DP EP" # TBD -# timeout_in_minutes: 85 -# mirror_hardwares: [amdexperimental, amdproduction, amdgfx950nightly, amdmi355] -# dind: false -# agent_pool: mi355_8 -# num_gpus: 8 -# optional: true -# working_dir: "/vllm-workspace/tests" -# source_file_dependencies: -# - requirements/kv_connectors_rocm.txt -# - vllm/distributed/kv_transfer/kv_connector/v1/nixl/ -# - vllm/model_executor/layers/fused_moe/ -# - vllm/model_executor/layers/quantization/ -# - vllm/models/deepseek_v4/ -# - vllm/v1/attention/ -# - tests/v1/kv_connector/nixl_integration/ -# - vllm/_aiter_ops.py -# - vllm/platforms/rocm.py -# commands: -# - uv pip install --system -r /vllm-workspace/requirements/kv_connectors_rocm.txt -# # A mirror does not inherit the parent env, so it is repeated here in -# # the parent's order. The layouts must stay empty: unset, the script -# # defaults to HND, which the V4 indexer rejects. -# - >- -# VLLM_ROCM_USE_AITER=1 -# ENABLE_HMA_FLAG=1 -# DP_EP=1 -# GPU_MEMORY_UTILIZATION=0.85 -# PREFILLER_TP_SIZE=4 -# DECODER_TP_SIZE=4 -# PREFILL_BLOCK_SIZE=256 -# DECODE_BLOCK_SIZE=256 -# MODEL_NAMES=deepseek-ai/DeepSeek-V4-Flash -# VLLM_ENGINE_READY_TIMEOUT_S=1800 -# VLLM_SERVE_EXTRA_ARGS=--trust-remote-code,--kv-cache-dtype,fp8 -# PREFILLER_KV_LAYOUT= -# DECODER_KV_LAYOUT= -# bash v1/kv_connector/nixl_integration/run_accuracy_test.sh +- label: ":amd: (MI355) DSv4-Flash Disaggregated DP EP" # TBD + timeout_in_minutes: 85 + mirror_hardwares: [amdexperimental, amdproduction, amdgfx950nightly, amdmi355] + dind: false + agent_pool: mi355_8 + num_gpus: 8 + optional: true + working_dir: "/vllm-workspace/tests" + source_file_dependencies: + - vllm/distributed/kv_transfer/kv_connector/v1/nixl/ + - vllm/model_executor/layers/fused_moe/ + - vllm/model_executor/layers/quantization/ + - vllm/models/deepseek_v4/ + - vllm/v1/attention/ + - tests/v1/kv_connector/nixl_integration/ + - vllm/_aiter_ops.py + - vllm/platforms/rocm.py + commands: + # A mirror does not inherit the parent env, so it is repeated here in + # the parent's order. The layouts must stay empty: unset, the script + # defaults to HND, which the V4 indexer rejects. + - >- + VLLM_ROCM_USE_AITER=1 + ENABLE_HMA_FLAG=1 + DP_EP=1 + GPU_MEMORY_UTILIZATION=0.85 + PREFILLER_TP_SIZE=4 + DECODER_TP_SIZE=4 + PREFILL_BLOCK_SIZE=256 + DECODE_BLOCK_SIZE=256 + MODEL_NAMES=deepseek-ai/DeepSeek-V4-Flash + VLLM_ENGINE_READY_TIMEOUT_S=1800 + VLLM_SERVE_EXTRA_ARGS=--trust-remote-code,--kv-cache-dtype,fp8 + PREFILLER_KV_LAYOUT= + DECODER_KV_LAYOUT= + bash v1/kv_connector/nixl_integration/run_accuracy_test.sh - label: ":amd: (MI355) Distributed Torchrun + Examples" # TBD timeout_in_minutes: 55 @@ -3621,6 +3581,7 @@ steps: optional: true working_dir: "/vllm-workspace/tests" source_file_dependencies: + - vllm/models/qwen3_5/ - vllm/model_executor/models/qwen3_5.py - vllm/model_executor/models/qwen3_5_mtp.py - vllm/model_executor/models/qwen2.py @@ -4198,6 +4159,8 @@ steps: - vllm/v1/kv_offload/ - vllm/v1/simple_kv_offload/ - tests/evals/gsm8k/test_gsm8k_offloading.py + - vllm/distributed/kv_transfer/kv_connector/v1/umbp/ + - tests/v1/kv_connector/umbp_test_utils.py - vllm/platforms/rocm.py commands: - pytest -s -v evals/gsm8k/test_gsm8k_offloading.py -k "nemotron-h-8b or gemma-4-e4b-it" @@ -4410,6 +4373,7 @@ steps: - vllm/platforms/rocm.py commands: - pytest -v -s + kernels/attention/test_minimax_m3_nvfp4_index.py kernels/test_apply_repetition_penalties.py kernels/test_awq_int4_to_int8.py kernels/test_bf16_skinny_gemm.py @@ -4597,11 +4561,11 @@ steps: --ignore=kernels/attention/test_rocm_aiter_mla_op_registration.py --shard-id=$$BUILDKITE_PARALLEL_JOB --num-shards=$$BUILDKITE_PARALLEL_JOB_COUNT -- label: ":amd: (MI355 DPX) MoE Kernels Shard %N" # TBD +- label: ":amd: (MI355) MoE Kernels Shard %N" # TBD timeout_in_minutes: 55 mirror_hardwares: [amdexperimental, amdproduction, amdgfx950nightly, amdmi355] dind: false - agent_pool: mi355_dpx + agent_pool: mi355_1 parallelism: 5 working_dir: "/vllm-workspace/tests" source_file_dependencies: @@ -4876,7 +4840,7 @@ steps: - tests/models/test_hyv4_rocm.py - tests/models/test_vision.py - tests/models/test_deepseek_v4_vl_rocm.py - - tests/models/test_deepseek_v4_vl_stream_weights.py + - tests/models/test_deepseek_v4_vl.py commands: - >- pytest -v -s @@ -4884,7 +4848,7 @@ steps: models/transformers/test_backend.py models/test_registry.py models/test_deepseek_v4_vl_rocm.py - models/test_deepseek_v4_vl_stream_weights.py + models/test_deepseek_v4_vl.py - VLLM_ROCM_USE_AITER=1 pytest -v -s models/test_hyv4_rocm.py -m 'not distributed' - pytest -v -s models/test_vision.py::test_simple_mrope_vision_model_spatial_merge @@ -4908,6 +4872,34 @@ steps: # PLE and QSA import conflicting HC registrations in a shared process. - pytest -v -s models/qwen4_exp/test_ple.py +- label: ":amd: (MI355 DPX) Step5" + timeout_in_minutes: 30 + mirror_hardwares: [amdexperimental, amdproduction, amdgfx950nightly, amdmi355] + dind: false + agent_pool: mi355_dpx + num_gpus: 1 + optional: true + working_dir: "/vllm-workspace/tests" + source_file_dependencies: + - vllm/models/step5/ + - tests/models/step5/ + - vllm/model_executor/models/step3p7.py + - vllm/model_executor/models/step3_vl.py + - vllm/model_executor/models/step_vl.py + - vllm/config/compilation.py + - vllm/model_executor/layers/indexer_topk.py + - vllm/transformers_utils/configs/step5.py + - vllm/model_executor/warmup/step5_dsa_warmup.py + - vllm/v1/attention/backends/rocm_aiter_unified_attn.py + - vllm/triton_utils/ + - vllm/platforms/rocm.py + - tests/v1/attention/utils.py + - tests/models/test_initialization.py + - tests/models/registry.py + commands: + - pytest -v -s models/step5 + - pytest -v -s models/test_initialization.py -k Step5ForConditionalGeneration + - label: ":amd: (MI355 DPX) GLM5Next" timeout_in_minutes: 35 mirror_hardwares: [amdexperimental, amdproduction, amdgfx950nightly, amdmi355] @@ -5008,11 +5000,6 @@ steps: - vllm/ - tests/models/language/generation commands: - - export GIT_CONFIG_COUNT=1 - - export GIT_CONFIG_KEY_0=http.version - - export GIT_CONFIG_VALUE_0=HTTP/1.1 - - MAMBA_FORCE_BUILD=TRUE uv pip install --system --no-build-isolation 'git+https://github.com/AndreasKaratzas/mamba@fix-rocm-7.0-warp-size-constexpr' - - CAUSAL_CONV1D_FORCE_BUILD=TRUE uv pip install --system --no-build-isolation 'git+https://github.com/Dao-AILab/causal-conv1d@v1.6.0' - pytest -v -s models/language/generation -m hybrid_model -k 'not granite-4.0-tiny-preview' --num-shards=$$BUILDKITE_PARALLEL_JOB_COUNT --shard-id=$$BUILDKITE_PARALLEL_JOB - label: ":amd: (MI355 DPX) Granite Language Model Compatibility" @@ -5028,11 +5015,6 @@ steps: - "!vllm/distributed/kv_transfer/" - tests/models/language/generation commands: - - export GIT_CONFIG_COUNT=1 - - export GIT_CONFIG_KEY_0=http.version - - export GIT_CONFIG_VALUE_0=HTTP/1.1 - - MAMBA_FORCE_BUILD=TRUE uv pip install --system --no-build-isolation 'git+https://github.com/AndreasKaratzas/mamba@fix-rocm-7.0-warp-size-constexpr' - - CAUSAL_CONV1D_FORCE_BUILD=TRUE uv pip install --system --no-build-isolation 'git+https://github.com/Dao-AILab/causal-conv1d@v1.6.0' - pytest -v -s models/language/generation -m hybrid_model -k 'granite-4.0-tiny-preview' - label: ":amd: (MI355 DPX) Language Models (Extended Generation)" # TBD @@ -5046,8 +5028,6 @@ steps: - tests/models/language/generation optional: true commands: - - MAMBA_FORCE_BUILD=TRUE uv pip install --system --no-build-isolation 'git+https://github.com/AndreasKaratzas/mamba@rocm-7.0-v2.3.0' - - CAUSAL_CONV1D_FORCE_BUILD=TRUE uv pip install --system --no-build-isolation 'git+https://github.com/Dao-AILab/causal-conv1d@v1.6.0' - pytest -v -s models/language/generation -m '(not core_model) and (not hybrid_model)' - label: ":amd: (MI355 DPX) Language Models (Extended Pooling) Shard %N" @@ -5072,6 +5052,7 @@ steps: optional: true working_dir: "/vllm-workspace/tests" source_file_dependencies: + - vllm/models/qwen3_5/ - vllm/model_executor/models/qwen3_5.py - vllm/model_executor/models/qwen3_5_mtp.py - vllm/model_executor/models/qwen2.py @@ -5276,37 +5257,35 @@ steps: #------------------------------------------------------------ mi355 · v1 -------------------------------------------------------------# -# Disabled until the pytest plugins installation is locked. -# - label: ":amd: (MI355) Batch Invariance" # TBD -# timeout_in_minutes: 60 -# mirror_hardwares: [amdexperimental, amdproduction, amdgfx950nightly, amdmi355] -# dind: false -# agent_pool: mi355_1 -# optional: true -# working_dir: "/vllm-workspace/tests" -# source_file_dependencies: -# - vllm/v1/attention -# - vllm/model_executor/layers -# - vllm/lora/ops/triton_ops -# - vllm/model_executor/kernels/linear -# - vllm/model_executor/determinism -# - tests/v1/determinism/ -# - vllm/_aiter_ops.py -# - vllm/platforms/rocm.py -# commands: -# - export VLLM_WORKER_MULTIPROC_METHOD=spawn -# - pip install pytest-timeout pytest-forked -# - pytest -v -s v1/determinism/test_batch_invariance.py -# - pytest -v -s v1/determinism/test_rms_norm_batch_invariant.py -# - pytest -v -s v1/determinism/test_lora_shrink_batch_invariant.py -# - pytest -v -s v1/determinism/test_batch_invariance_vlm.py -# - VLLM_TEST_MODEL=deepseek-ai/DeepSeek-V2-Lite-Chat pytest -v -s v1/determinism/test_batch_invariance.py::test_v1_generation_is_deterministic_across_batch_sizes_with_needle -k TRITON_MLA -# - VLLM_TEST_MODEL=Qwen/Qwen3-30B-A3B-Thinking-2507-FP8 pytest -v -s v1/determinism/test_batch_invariance.py::test_v1_generation_is_deterministic_across_batch_sizes_with_needle -k TRITON_ATTN -# - pytest -v -s v1/determinism/test_nvfp4_batch_invariant.py -# - pytest -v -s v1/determinism/test_matmul_batch_invariant.py -# - pytest -v -s v1/determinism/test_cutlass_batch_invariance.py -# - pytest -v -s v1/determinism/test_online_batch_invariance.py -# - pytest -v -s v1/determinism/test_mx_linear_batch_invariant.py +- label: ":amd: (MI355) Batch Invariance" # TBD + timeout_in_minutes: 60 + mirror_hardwares: [amdexperimental, amdproduction, amdgfx950nightly, amdmi355] + dind: false + agent_pool: mi355_1 + optional: true + working_dir: "/vllm-workspace/tests" + source_file_dependencies: + - vllm/v1/attention + - vllm/model_executor/layers + - vllm/lora/ops/triton_ops + - vllm/model_executor/kernels/linear + - vllm/model_executor/determinism + - tests/v1/determinism/ + - vllm/_aiter_ops.py + - vllm/platforms/rocm.py + commands: + - export VLLM_WORKER_MULTIPROC_METHOD=spawn + - pytest -v -s v1/determinism/test_batch_invariance.py + - pytest -v -s v1/determinism/test_rms_norm_batch_invariant.py + - pytest -v -s v1/determinism/test_lora_shrink_batch_invariant.py + - pytest -v -s v1/determinism/test_batch_invariance_vlm.py + - VLLM_TEST_MODEL=deepseek-ai/DeepSeek-V2-Lite-Chat pytest -v -s v1/determinism/test_batch_invariance.py::test_v1_generation_is_deterministic_across_batch_sizes_with_needle -k TRITON_MLA + - VLLM_TEST_MODEL=Qwen/Qwen3-30B-A3B-Thinking-2507-FP8 pytest -v -s v1/determinism/test_batch_invariance.py::test_v1_generation_is_deterministic_across_batch_sizes_with_needle -k TRITON_ATTN + - pytest -v -s v1/determinism/test_nvfp4_batch_invariant.py + - pytest -v -s v1/determinism/test_matmul_batch_invariant.py + - pytest -v -s v1/determinism/test_cutlass_batch_invariance.py + - pytest -v -s v1/determinism/test_online_batch_invariance.py + - pytest -v -s v1/determinism/test_mx_linear_batch_invariant.py - label: ":amd: (MI355) Spec Decode AL DFlash2 Nightly" timeout_in_minutes: 60 @@ -5328,7 +5307,9 @@ steps: - vllm/v1/attention/ - vllm/model_executor/models/qwen3_dflash.py - vllm/model_executor/models/qwen3_dflash2.py + - vllm/models/qwen3_5/ - vllm/model_executor/models/qwen3_5.py + - vllm/model_executor/models/qwen3_5_mtp.py - vllm/model_executor/model_loader/ - vllm/model_executor/layers/quantization/ - vllm/model_executor/kernels/linear/ @@ -5637,6 +5618,9 @@ steps: - vllm/v1/kv_cache_interface.py - tests/v1/kv_connector/unit/test_umbp_ - tests/v1/kv_connector/umbp_test_utils.py + - tests/v1/kv_connector/unit/utils.py + - tests/v1/kv_connector/conftest.py + - docker/Dockerfile.rocm_base commands: - export VLLM_WORKER_MULTIPROC_METHOD=spawn # Fail loudly instead of skipping every test if the image lacks mori. @@ -5837,6 +5821,26 @@ steps: - export VLLM_WORKER_MULTIPROC_METHOD=spawn - pytest -v -s -m 'not distributed' tests/v1/kv_connector/extract_hidden_states_integration +- label: ":amd: (MI355 DPX) UMBP Connector Integration" + timeout_in_minutes: 30 + mirror_hardwares: [amdexperimental, amdproduction, amdgfx950nightly, amdmi355] + dind: false + agent_pool: mi355_dpx + num_gpus: 1 + optional: true + working_dir: "/vllm-workspace/tests" + source_file_dependencies: + - vllm/distributed/kv_transfer/kv_connector/v1/umbp/ + - vllm/distributed/kv_transfer/kv_connector/v1/base.py + - tests/v1/kv_connector/umbp_integration/ + - tests/v1/kv_connector/umbp_test_utils.py + - tests/v1/kv_connector/conftest.py + - tests/v1/kv_connector/rocm_pd_accuracy_utils.py + - docker/Dockerfile.rocm_base + commands: + - export VLLM_WORKER_MULTIPROC_METHOD=spawn + - pytest -v -s -rs v1/kv_connector/umbp_integration + - label: ":amd: (MI355 DPX) V1 Attention Shard %N" # TBD timeout_in_minutes: 125 mirror_hardwares: [amdexperimental, amdproduction, amdgfx950nightly, amdmi355] @@ -6217,3 +6221,4 @@ steps: - vllm/envs.py commands: - pytest -v -s tests/jit_monitor/test_no_runtime_jit_rocm.py --timeout=300 + - pytest -v -s tests/jit_monitor/test_flydsl_contract.py diff --git a/.buildkite/test_areas/attention.yaml b/.buildkite/test_areas/attention.yaml index e5cbb528df01..41866227f342 100644 --- a/.buildkite/test_areas/attention.yaml +++ b/.buildkite/test_areas/attention.yaml @@ -82,6 +82,7 @@ steps: - label: ":nvidia: (DGX Spark) [Nightly] FlashInfer NVFP4 XQA" key: flashinfer-nvfp4-xqa-dgx-spark + automatic_shard: true timeout_in_minutes: 30 device: dgx-spark optional: true diff --git a/.buildkite/test_areas/basic_correctness.yaml b/.buildkite/test_areas/basic_correctness.yaml index f294301a7b1f..7cf48f6467fa 100644 --- a/.buildkite/test_areas/basic_correctness.yaml +++ b/.buildkite/test_areas/basic_correctness.yaml @@ -26,6 +26,7 @@ steps: - label: ":nvidia: (H200 MIG 18GB) Basic Correctness CuMem" key: basic-correctness-cumem + automatic_shard: true timeout_in_minutes: 68 device: h200_18gb source_file_dependencies: *basic-correctness-deps @@ -34,6 +35,7 @@ steps: - pytest -v -s basic_correctness/memory/cumem mirror: amd: + automatic_shard: true label: ":amd: (MI355 DPX) Basic Correctness CuMem" dind: false device: mi355_dpx @@ -43,6 +45,7 @@ steps: - label: ":nvidia: (H200 MIG 18GB) Basic Correctness Sleep Mode" key: basic-correctness-sleep-mode + automatic_shard: true timeout_in_minutes: 68 device: h200_18gb source_file_dependencies: *basic-correctness-deps @@ -51,6 +54,7 @@ steps: - pytest -v -s basic_correctness/memory/sleep_mode mirror: amd: + automatic_shard: true label: ":amd: (MI300) Basic Correctness Sleep Mode" dind: false device: mi300_1 @@ -60,6 +64,7 @@ steps: - label: ":nvidia: (H200 MIG 18GB) Basic Correctness CPU Offload" key: basic-correctness-cpu-offload + automatic_shard: true timeout_in_minutes: 68 device: h200_18gb source_file_dependencies: *basic-correctness-deps @@ -68,6 +73,7 @@ steps: - pytest -v -s basic_correctness/cpu_offload mirror: amd: + automatic_shard: true label: ":amd: (MI355 DPX) Basic Correctness CPU Offload" dind: false device: mi355_dpx @@ -77,6 +83,7 @@ steps: - label: ":nvidia: (H200 MIG 18GB) Basic Correctness Prefetch Offload" key: basic-correctness-prefetch-offload + automatic_shard: true timeout_in_minutes: 68 device: h200_18gb source_file_dependencies: *basic-correctness-deps @@ -85,6 +92,7 @@ steps: - pytest -v -s basic_correctness/prefetch_offload mirror: amd: + automatic_shard: true label: ":amd: (MI355 DPX) Basic Correctness Prefetch Offload" dind: false device: mi355_dpx diff --git a/.buildkite/test_areas/benchmarks.yaml b/.buildkite/test_areas/benchmarks.yaml index bd381080094f..1eca056cbb9c 100644 --- a/.buildkite/test_areas/benchmarks.yaml +++ b/.buildkite/test_areas/benchmarks.yaml @@ -16,6 +16,7 @@ steps: - pytest -v -s benchmarks/ mirror: amd: + automatic_shard: true label: ":amd: (MI355 DPX) Benchmarks CLI" dind: false device: mi355_dpx diff --git a/.buildkite/test_areas/compile.yaml b/.buildkite/test_areas/compile.yaml index 163904e6aac2..46d980df398b 100644 --- a/.buildkite/test_areas/compile.yaml +++ b/.buildkite/test_areas/compile.yaml @@ -4,6 +4,7 @@ depends_on: steps: - label: ":nvidia: (H100) Distributed Compile" key: distributed-compile-unit-tests-2xh100 + automatic_shard: true timeout_in_minutes: 45 working_dir: "/vllm-workspace/" device: h100 @@ -17,6 +18,7 @@ steps: - pytest -s -v tests/compile/passes/distributed mirror: amd: + automatic_shard: true label: ':amd: (MI300) Distributed Compile' dind: false device: mi300_2 @@ -36,6 +38,7 @@ steps: - label: ":nvidia: (B200) Fusion and Compile" key: fusion-and-compile-unit-tests-2xb200 + automatic_shard: true timeout_in_minutes: 30 working_dir: "/vllm-workspace/" device: b200-k8s @@ -65,6 +68,7 @@ steps: - pytest -v -s tests/compile/passes/distributed/test_fusion_all_reduce.py mirror: amd: + automatic_shard: true label: ":amd: (MI355) Fusion and Compile FP8" dind: false device: mi355_2 @@ -95,6 +99,7 @@ steps: - label: ":nvidia: (H100) Fusion E2E Quick" key: fusion-e2e-quick-h100 + automatic_shard: true timeout_in_minutes: 25 working_dir: "/vllm-workspace/" device: h100 @@ -113,6 +118,7 @@ steps: - pytest -v -s tests/compile/fusions_e2e/test_tp1_quant.py -k "inductor_partition and not +rms_norm and +quant_fp8 and (qwen3 or deepseek)" mirror: amd: + automatic_shard: true label: ":amd: (MI355 DPX) Fusion E2E Quick" dind: false device: mi355_dpx @@ -135,6 +141,7 @@ steps: - label: ":nvidia: (H100) Fusion E2E Config Sweep" key: fusion-e2e-config-sweep-h100 + automatic_shard: true timeout_in_minutes: 25 working_dir: "/vllm-workspace/" device: h100 @@ -154,6 +161,7 @@ steps: - pytest -v -s tests/compile/fusions_e2e/test_tp1_quant.py -k "llama-3" mirror: amd: + automatic_shard: true label: ":amd: (MI355 DPX) Fusion E2E Config Sweep" dind: false device: mi355_dpx @@ -212,6 +220,7 @@ steps: - label: ":nvidia: (H100) Fusion E2E TP2 Quick" key: fusion-e2e-tp2-quick-h100 + automatic_shard: true timeout_in_minutes: 35 working_dir: "/vllm-workspace/" device: h100 @@ -228,6 +237,7 @@ steps: - pytest -v -s tests/compile/fusions_e2e/test_tp2_ar_rms.py -k "inductor_partition and not +rms_norm and (not +quant_fp8 or +quant_fp8 and (qwen3 or deepseek))" mirror: amd: + automatic_shard: true label: ":amd: (MI355) Fusion E2E TP2 Quick" dind: false device: mi355_2 @@ -270,6 +280,7 @@ steps: - label: ":nvidia: (H100) Fusion E2E TP2 AR-RMS Config Sweep" key: fusion-e2e-tp2-ar-rms-config-sweep-h100 + automatic_shard: true timeout_in_minutes: 30 working_dir: "/vllm-workspace/" device: h100 @@ -289,6 +300,7 @@ steps: - pytest -v -s tests/compile/fusions_e2e/test_tp2_ar_rms.py -k "llama-3" mirror: amd: + automatic_shard: true label: ":amd: (MI355) Fusion E2E TP2 AR-RMS FP8 Config Sweep" dind: false device: mi355_2 @@ -315,6 +327,7 @@ steps: - label: ":nvidia: (B200) Fusion E2E TP2" key: fusion-e2e-tp2-b200 + automatic_shard: true timeout_in_minutes: 45 working_dir: "/vllm-workspace/" device: b200-k8s @@ -333,6 +346,7 @@ steps: - pytest -v -s tests/compile/fusions_e2e/test_tp2_ar_rms.py -k "(FLASHINFER and inductor_partition and not +rms_norm and (not +quant_fp8 or +quant_fp8 and (qwen3 or deepseek))) or Llama-3.1-8B-Instruct-FP4" mirror: amd: + automatic_shard: true label: ":amd: (MI355) Fusion E2E TP2 FP8 BF16 rewrites" dind: false device: mi355_2 @@ -360,6 +374,7 @@ steps: - label: ':amd: (MI355) Fusion E2E TP2 AR-RMS (Inductor Partition)' key: fusion-e2e-tp2-ar-rms-inductor-partition-amd + automatic_shard: true dind: false device: mi355_2 num_devices: 2 @@ -381,6 +396,7 @@ steps: - label: ':amd: (MI355) Fusion E2E TP2 AR-RMS (Dynamo Partition)' key: fusion-e2e-tp2-ar-rms-dynamo-partition-amd + automatic_shard: true dind: false device: mi355_2 num_devices: 2 diff --git a/.buildkite/test_areas/cuda.yaml b/.buildkite/test_areas/cuda.yaml index b3bfa9f09708..bc4afc0d9d36 100644 --- a/.buildkite/test_areas/cuda.yaml +++ b/.buildkite/test_areas/cuda.yaml @@ -4,6 +4,7 @@ depends_on: steps: - label: ":nvidia: (H200 MIG 18GB) CUDA Platform" key: platform-tests + automatic_shard: true timeout_in_minutes: 20 device: h200_18gb source_file_dependencies: @@ -19,6 +20,7 @@ steps: - pytest -v -s cuda/test_cuda_compatibility_path.py mirror: amd: + automatic_shard: true label: ":amd: (MI355 DPX) CUDA Platform" dind: false device: mi355_dpx @@ -54,6 +56,7 @@ steps: - pytest -v -s v1/cudagraph/test_encoder_cudagraph.py mirror: amd: + automatic_shard: true label: ":amd: (MI250) CUDAGraph" dind: false device: mi250_1 diff --git a/.buildkite/test_areas/distributed.yaml b/.buildkite/test_areas/distributed.yaml index 883aa03aba05..ef3d5e339f09 100644 --- a/.buildkite/test_areas/distributed.yaml +++ b/.buildkite/test_areas/distributed.yaml @@ -4,6 +4,7 @@ depends_on: steps: - label: ":nvidia: (L4) Distributed Comm Ops" key: distributed-comm-ops + automatic_shard: true timeout_in_minutes: 25 working_dir: "/vllm-workspace/tests" device: l4 @@ -21,6 +22,7 @@ steps: - pytest -v -s distributed/test_shm_storage.py mirror: amd: + automatic_shard: true label: ':amd: (MI300) Distributed Comm Ops' dind: false device: mi300_2 @@ -40,6 +42,7 @@ steps: - label: ":nvidia: (L4) Sharded RDT Weight Transfer" key: sharded-rdt-weight-transfer + automatic_shard: true device: l4 timeout_in_minutes: 20 working_dir: "/vllm-workspace/tests" @@ -55,6 +58,7 @@ steps: - pytest -v -s distributed/test_sharded_rdt_trainer.py mirror: amd: + automatic_shard: true label: ":amd: (MI355 DPX) Sharded RDT Weight Transfer" dind: false device: mi355_dpx @@ -130,6 +134,7 @@ steps: - label: ":nvidia: (L4) Distributed Compile + RPC" key: distributed-compile-rpc-tests-2-gpus + automatic_shard: true timeout_in_minutes: 65 working_dir: "/vllm-workspace/tests" device: l4 @@ -144,14 +149,17 @@ steps: - vllm/v1/engine/ - vllm/v1/worker/ - tests/compile/test_wrapper.py + - tests/compile/test_aot_compile.py - tests/entrypoints/llm/test_collective_rpc.py commands: # https://github.com/NVIDIA/nccl/issues/1838 - export NCCL_CUMEM_HOST_ENABLE=0 - pytest -v -s entrypoints/llm/test_collective_rpc.py - pytest -v -s ./compile/test_wrapper.py + - pytest -v -s ./compile/test_aot_compile.py::test_load_on_another_device mirror: amd: + automatic_shard: true label: ":amd: (MI355) Distributed Compile + RPC" dind: false device: mi355_2 @@ -176,6 +184,7 @@ steps: - label: ":nvidia: (L4) Distributed Torchrun + Shutdown" key: distributed-torchrun-shutdown-tests-2-gpus + automatic_shard: true timeout_in_minutes: 30 working_dir: "/vllm-workspace/tests" device: l4 @@ -200,6 +209,7 @@ steps: - pytest -v -s v1/worker/test_worker_memory_snapshot.py mirror: amd: + automatic_shard: true label: ':amd: (MI300) Distributed Torchrun + Shutdown' dind: false device: mi300_2 @@ -369,6 +379,7 @@ steps: - label: ":nvidia: (L4) [Nightly] Distributed Compile + Comm" key: distributed-compile-comm-4-gpus + automatic_shard: true # Nightly only: 4-GPU L4 pods wait behind smaller jobs on per-PR runs. optional: true timeout_in_minutes: 70 @@ -392,6 +403,7 @@ steps: - pytest -v -s distributed/test_multiproc_executor.py::test_multiproc_executor_multi_node mirror: amd: + automatic_shard: true # Run on PRs even when the L4 step is optional (nightly only). optional: false label: ':amd: (MI355) Distributed Compile + Comm' @@ -416,6 +428,7 @@ steps: - label: ":nvidia: (H100) [Nightly] Distributed DP + EP" key: distributed-tests-8xh100 + automatic_shard: true timeout_in_minutes: 20 optional: true device: h100 @@ -430,14 +443,18 @@ steps: - vllm/v1/executor/uniproc_executor.py - vllm/v1/worker/gpu_worker.py - tests/distributed/test_mnnvl_alltoall.py + - tests/distributed/test_eplb_execute.py commands: # https://github.com/NVIDIA/nccl/issues/1838 - export NCCL_CUMEM_HOST_ENABLE=0 # test with torchrun tp=2 and dp=4 with ep - torchrun --nproc-per-node=8 ../examples/features/torchrun/torchrun_dp_example_offline.py --tp-size=2 --pp-size=1 --dp-size=4 --enable-ep + # The L4 EPLB Execution job has no NVLink, so it skips symm_mem. + - pytest -v -s distributed/test_eplb_execute.py -k symm_mem mirror: amd: + automatic_shard: true label: ":amd: (MI355) Distributed DP + EP" dind: false device: mi355_8 @@ -462,43 +479,34 @@ steps: - export TORCH_CPP_LOG_LEVEL=INFO - torchrun --nproc-per-node=8 ../examples/features/torchrun/torchrun_dp_example_offline.py --tp-size=2 --pp-size=1 --dp-size=4 --enable-ep -- label: ":nvidia: (A100) [Nightly] Distributed" - key: distributed-tests-4xa100 - device: a100 +# Key kept from the former A100 step's mirror for job history and retries. +- label: ':amd: (MI355) Distributed' + key: amd-distributed-tests-4xa100 + dind: false + device: mi355_4 optional: true num_devices: 4 + timeout_in_minutes: 40 + working_dir: /vllm-workspace/tests + depends_on: + - image-build-amd source_file_dependencies: - vllm/ - "!vllm/distributed/kv_transfer/" + - tests/models/test_vision.py + - vllm/platforms/rocm.py + - tests/distributed/ + - tests/basic_correctness/ + - tests/lora/test_mixtral.py commands: # NOTE: don't test llama model here, it seems hf implementation is buggy # see https://github.com/vllm-project/vllm/pull/5689 for details - pytest -v -s distributed/test_custom_all_reduce.py - torchrun --nproc_per_node=2 distributed/test_ca_buffer_sharing.py - - TARGET_TEST_SUITE=A100 pytest basic_correctness/ -v -s -m 'distributed(num_gpus=2)' + - VLLM_ALLOW_INSECURE_SERIALIZATION=1 pytest -v -s distributed/test_weight_transfer.py -k multiple_sources + - TARGET_TEST_SUITE=MI355 pytest basic_correctness/ -v -s -m 'distributed(num_gpus=2)' - pytest -v -s -x lora/test_mixtral.py - mirror: - amd: - label: ':amd: (MI355) Distributed' - dind: false - device: mi355_4 - timeout_in_minutes: 40 - working_dir: /vllm-workspace/tests - depends_on: - - image-build-amd - source_file_dependencies: - - vllm/ - - tests/models/test_vision.py - - vllm/platforms/rocm.py - - tests/distributed/ - - tests/basic_correctness/ - - tests/lora/test_mixtral.py - commands: - - pytest -v -s distributed/test_custom_all_reduce.py - - torchrun --nproc_per_node=2 distributed/test_ca_buffer_sharing.py - - TARGET_TEST_SUITE=MI355 pytest basic_correctness/ -v -s -m 'distributed(num_gpus=2)' - - pytest -v -s -x lora/test_mixtral.py - - pytest -v -s models/test_vision.py -m 'distributed(num_gpus=4)' + - pytest -v -s models/test_vision.py -m 'distributed(num_gpus=4)' - label: ":nvidia: (H100) [Nightly] Distributed Features" key: distributed-tests-2xh100-2xmi300 @@ -572,6 +580,7 @@ steps: - label: ":nvidia: (B200) [Nightly] Distributed" key: distributed-tests-2xb200 + automatic_shard: true device: b200-k8s optional: true working_dir: "/vllm-workspace/" @@ -584,6 +593,7 @@ steps: - pytest -v -s tests/distributed/test_mnnvl_alltoall.py mirror: amd: + automatic_shard: true label: ":amd: (MI355) Distributed AgRs All2All" dind: false device: mi355_2 @@ -606,6 +616,7 @@ steps: - label: ":nvidia: (B200) GEMM-RS/AR" key: gemm-rs-ar-2xb200 + automatic_shard: true timeout_in_minutes: 20 device: b200-k8s working_dir: "/vllm-workspace/" @@ -618,6 +629,7 @@ steps: - pytest -v -s tests/kernels/test_gemm_rs_ar.py mirror: amd: + automatic_shard: true label: ":amd: (MI355) GEMM-RS-AR" dind: false device: mi355_4 @@ -774,11 +786,13 @@ steps: - label: ":nvidia: (L4) Distributed Preload" key: preload-2-gpus + automatic_shard: true timeout_in_minutes: 15 working_dir: "/vllm-workspace/tests" device: l4 num_devices: 2 source_file_dependencies: + - vllm/entrypoints/cli/preload.py - vllm/model_executor/model_loader/weight_cache/ - vllm/model_executor/layers/fused_moe/expert_map_manager.py - tests/model_executor/model_loader/test_weight_cache.py @@ -786,6 +800,7 @@ steps: - pytest -v -s model_executor/model_loader/test_weight_cache.py -m 'distributed(num_gpus=2)' mirror: amd: + automatic_shard: true label: ":amd: (MI355) Distributed Preload" dind: false device: mi355_2 @@ -794,6 +809,7 @@ steps: depends_on: - image-build-amd source_file_dependencies: + - vllm/entrypoints/cli/preload.py - vllm/model_executor/model_loader/weight_cache/ - vllm/model_executor/layers/fused_moe/expert_map_manager.py - tests/model_executor/model_loader/test_weight_cache.py diff --git a/.buildkite/test_areas/docker.yaml b/.buildkite/test_areas/docker.yaml index 632c3aa1322d..8e08f158c64b 100644 --- a/.buildkite/test_areas/docker.yaml +++ b/.buildkite/test_areas/docker.yaml @@ -9,16 +9,24 @@ steps: source_file_dependencies: - .buildkite/release-pipeline.yaml - .buildkite/scripts/docker-build-metadata-args.sh - - .buildkite/scripts/rocm/build-config.sh + - .buildkite/scripts/rocm/build.sh + - .buildkite/scripts/rocm/publish.sh + - tools/vllm-rocm/therock_wheels.py - docker/Dockerfile - docker/Dockerfile.cpu + - docker/Dockerfile.rocm + - docker/Dockerfile.rocm_base - docker/docker-bake.hcl + - docker/docker-bake-rocm.hcl - tests/tools/test_docker_build_metadata_args.py + - tests/tools/test_rocm_build.py + - tests/tools/test_amd_ci_fast_exit.py + - .buildkite/scripts/hardware_ci/amd_ci_fast_exit.py - tests/tools/test_kernel_symbol_map.py - tools/ci/kernel_symbol_map.py - tools/ci/rocm_kernel_symbol_map.py commands: - - pytest -v -s tools/test_docker_build_metadata_args.py + - pytest -v -s tools/test_docker_build_metadata_args.py tools/test_rocm_build.py tools/test_amd_ci_fast_exit.py - pytest -v -s tools/test_kernel_symbol_map.py mirror: amd: @@ -31,12 +39,16 @@ steps: - image-build-amd source_file_dependencies: - .buildkite/scripts/docker-build-metadata-args.sh - - .buildkite/scripts/ci-bake-rocm.sh - docker/Dockerfile.rocm - docker/Dockerfile.rocm_base - - docker/ci-rocm.hcl - docker/docker-bake-rocm.hcl + - .buildkite/scripts/rocm/build.sh + - .buildkite/scripts/rocm/publish.sh + - tools/vllm-rocm/therock_wheels.py - tests/tools/test_docker_build_metadata_args.py + - tests/tools/test_rocm_build.py + - tests/tools/test_amd_ci_fast_exit.py + - .buildkite/scripts/hardware_ci/amd_ci_fast_exit.py - tests/tools/test_kernel_symbol_map.py - tools/ci/kernel_symbol_map.py - tools/ci/rocm_kernel_symbol_map.py diff --git a/.buildkite/test_areas/engine.yaml b/.buildkite/test_areas/engine.yaml index f6f91e26f86d..f2dc9e6041fc 100644 --- a/.buildkite/test_areas/engine.yaml +++ b/.buildkite/test_areas/engine.yaml @@ -4,6 +4,7 @@ depends_on: steps: - label: ":nvidia: (H200 MIG 18GB) Engine Core" key: engine + automatic_shard: true timeout_in_minutes: 30 device: h200_18gb source_file_dependencies: @@ -39,6 +40,7 @@ steps: amd: label: ":amd: (MI355 DPX) Engine Core" dind: false + automatic_shard: true device: mi355_dpx timeout_in_minutes: 40 depends_on: @@ -63,6 +65,7 @@ steps: - pytest -v -s v1/test_tensor_ipc_queue.py mirror: amd: + automatic_shard: true label: ":amd: (MI250) V1 Engine" dind: false device: mi250_1 @@ -142,6 +145,7 @@ steps: -k "not test_mamba_prefix_cache_mrv2" mirror: amd: + automatic_shard: true label: ":amd: (MI250) E2E Core" dind: false commands: @@ -165,6 +169,7 @@ steps: - label: ":nvidia: (H200 MIG 35GB) E2E Core Large Memory" device: h200_35gb key: e2e-core-large-memory + automatic_shard: true timeout_in_minutes: 50 source_file_dependencies: - vllm/v1/ @@ -178,6 +183,7 @@ steps: v1/e2e/general/test_mamba_prefix_cache.py::test_mamba_prefix_cache_mrv2_async_flashinfer mirror: amd: + automatic_shard: true label: ":amd: (MI355 DPX) E2E Core Large Memory" dind: false device: mi355_dpx @@ -220,6 +226,7 @@ steps: - pytest -v -s v1/e2e/test_replayssm_decode.py mirror: amd: + automatic_shard: true label: ":amd: (MI355) ReplaySSM E2E" dind: false device: mi355_2 @@ -232,6 +239,7 @@ steps: - label: ":nvidia: (L4) [Nightly] V1 E2E" key: v1-e2e-2-gpus + automatic_shard: true timeout_in_minutes: 25 # TODO: Fix timeout after we have more confidence in the test stability optional: true device: l4 @@ -262,8 +270,10 @@ steps: pytest -v -s v1/e2e/spec_decode/draft_model/test_draft_model.py::test_draft_model_tensor_parallelism v1/e2e/spec_decode/draft_model/test_draft_model.py::test_draft_model_engine_args_tensor_parallelism + v1/e2e/spec_decode/draft_model/test_structured_output_pp.py::test_specdec_pp_structured_output mirror: amd: + automatic_shard: true label: ":amd: (MI300) V1 E2E" dind: false device: mi300_2 @@ -273,6 +283,7 @@ steps: - label: ":nvidia: (B200) [Nightly] V1 E2E Hybrid Chunked Prefill" key: v1-e2e-hybrid-chunked-prefill-b200 + automatic_shard: true timeout_in_minutes: 35 device: b200-k8s num_devices: 1 @@ -286,6 +297,7 @@ steps: - pytest -v -s v1/e2e/test_hybrid_chunked_prefill.py mirror: amd: + automatic_shard: true label: ":amd: (MI355 DPX) V1 E2E Hybrid Chunked Prefill" dind: false device: mi355_dpx diff --git a/.buildkite/test_areas/entrypoints.yaml b/.buildkite/test_areas/entrypoints.yaml index 8a568046eebc..72b993425e12 100644 --- a/.buildkite/test_areas/entrypoints.yaml +++ b/.buildkite/test_areas/entrypoints.yaml @@ -156,6 +156,7 @@ steps: - pytest -v -s entrypoints/openai/chat_completion mirror: amd: + automatic_shard: true label: ":amd: (MI355 DPX) Entrypoints Integration (OpenAI API chat_completion)" dind: false device: mi355_dpx @@ -185,6 +186,7 @@ steps: - pytest -v -s entrypoints/cohere mirror: amd: + automatic_shard: true label: ":amd: (MI355 DPX) Entrypoints Integration (API Server Generate)" dind: false device: mi355_dpx @@ -254,6 +256,7 @@ steps: amd: label: ":amd: (MI355 DPX) Entrypoints Integration (Multimodal)" dind: false + automatic_shard: true device: mi355_dpx timeout_in_minutes: 55 depends_on: @@ -285,6 +288,7 @@ steps: - label: ":nvidia: (H200 MIG 18GB) OpenAI API Correctness" key: openai-api-correctness + automatic_shard: true timeout_in_minutes: 20 device: h200_18gb source_file_dependencies: @@ -294,6 +298,7 @@ steps: - pytest -s entrypoints/openai/correctness/ mirror: amd: + automatic_shard: true label: ":amd: (MI355 DPX) OpenAI API Correctness" dind: false device: mi355_dpx diff --git a/.buildkite/test_areas/expert_parallelism.yaml b/.buildkite/test_areas/expert_parallelism.yaml index 1ca3eacd48cb..e9c13dad6348 100644 --- a/.buildkite/test_areas/expert_parallelism.yaml +++ b/.buildkite/test_areas/expert_parallelism.yaml @@ -4,6 +4,7 @@ depends_on: steps: - label: ":nvidia: (H200 MIG 18GB) EPLB Algorithm" key: eplb-algorithm + automatic_shard: true timeout_in_minutes: 20 device: h200_18gb working_dir: "/vllm-workspace/tests" @@ -18,6 +19,7 @@ steps: - pytest -v -s distributed/test_eplb_utils.py mirror: amd: + automatic_shard: true label: ":amd: (MI355 DPX) EPLB Algorithm" dind: false device: mi355_dpx @@ -32,6 +34,7 @@ steps: - label: ":nvidia: (L4) [Nightly] EPLB Execution" key: eplb-execution + automatic_shard: true # Nightly only: 4-GPU L4 pods wait behind smaller jobs on per-PR runs. optional: true timeout_in_minutes: 25 @@ -46,6 +49,7 @@ steps: - pytest -v -s distributed/test_eplb_spec_decode.py mirror: amd: + automatic_shard: true # Run on PRs even when the L4 step is optional (nightly only). optional: false label: ":amd: (MI355) EPLB Execution" diff --git a/.buildkite/test_areas/fault_tolerance.yaml b/.buildkite/test_areas/fault_tolerance.yaml index f9439880d454..972b405822cd 100644 --- a/.buildkite/test_areas/fault_tolerance.yaml +++ b/.buildkite/test_areas/fault_tolerance.yaml @@ -4,10 +4,10 @@ depends_on: steps: # Disabled until the KV connectors installation is locked. # - label: ":nvidia: (H100) Fault Tolerance E2E" -# key: fault-tolerance-e2e-2xh100 +# key: fault-tolerance-e2e-4xh100 # timeout_in_minutes: 35 # device: h100 -# num_devices: 2 +# num_devices: 4 # working_dir: "/vllm-workspace/tests" # source_file_dependencies: # - vllm/v1/fault_tolerance/ @@ -62,6 +62,7 @@ steps: # Keep the AMD job: its commands do not run this installation. - label: ':amd: (MI355) MoRI EP Healthy Baseline' key: amd-fault-tolerance-e2e-2xh100 + automatic_shard: true timeout_in_minutes: 10 device: mi355_2 num_devices: 2 @@ -93,6 +94,7 @@ steps: - tests/v1/fault_tolerance/ - tests/v1/distributed/test_external_lb_dp.py commands: + - pytest -v -s v1/fault_tolerance/test_expert_reload.py - VLLM_WORKER_MULTIPROC_METHOD=spawn pytest -v -s -rs kernels/moe/test_moe_layer.py::test_moe_layer_mori_graph - VLLM_TEST_ENABLE_MORI_MOE_LAYER=1 VLLM_WORKER_MULTIPROC_METHOD=spawn pytest -v -s -rs 'kernels/moe/test_moe_layer.py::test_moe_layer[False-mori_high_throughput-2-1-True]' --subtests='[32-512-512-8-2-bfloat16-None-False-False-False-False-mori_high_throughput-2-2-1]' dind: false diff --git a/.buildkite/test_areas/jit_monitor.yaml b/.buildkite/test_areas/jit_monitor.yaml index 9e40c9d1affc..2e606a988ca0 100644 --- a/.buildkite/test_areas/jit_monitor.yaml +++ b/.buildkite/test_areas/jit_monitor.yaml @@ -4,6 +4,7 @@ depends_on: steps: - label: ":nvidia: (H200 MIG 35GB) No Runtime JITs E2E" key: jit-monitor-no-runtime-jit + automatic_shard: true device: h200_35gb timeout_in_minutes: 45 source_file_dependencies: @@ -22,6 +23,7 @@ steps: - pytest -v -s jit_monitor/test_no_runtime_jit.py --timeout=900 --timeout-method=thread mirror: amd: + automatic_shard: true # Focused MRV2 regression; the broader model/backend matrix is pending. label: ':amd: (MI355 DPX) MRV2 Sampler JIT Warmup' dind: false @@ -41,3 +43,4 @@ steps: - vllm/envs.py commands: - pytest -v -s tests/jit_monitor/test_no_runtime_jit_rocm.py --timeout=300 + - pytest -v -s tests/jit_monitor/test_flydsl_contract.py diff --git a/.buildkite/test_areas/kernels.yaml b/.buildkite/test_areas/kernels.yaml index 6784ee1b28bd..9ca0ba999e2e 100644 --- a/.buildkite/test_areas/kernels.yaml +++ b/.buildkite/test_areas/kernels.yaml @@ -4,6 +4,7 @@ depends_on: steps: - label: ":nvidia: (H200 MIG 18GB) vLLM IR" key: vllm-ir-tests + automatic_shard: true timeout_in_minutes: 35 device: h200_18gb working_dir: "/vllm-workspace/" @@ -15,6 +16,7 @@ steps: - pytest -v -s tests/kernels/ir mirror: amd: + automatic_shard: true label: ":amd: (MI355 DPX) vLLM IR" dind: false device: mi355_dpx @@ -38,8 +40,10 @@ steps: source_file_dependencies: - csrc/ - tests/kernels/core + - vllm/kernels/triton/ - tests/kernels/test_concat_mla_q.py - tests/kernels/test_fused_qk_norm_rope_gate.py + - vllm/model_executor/layers/fused_qk_norm_rope.py commands: - >- pytest -v -s @@ -60,6 +64,7 @@ steps: source_file_dependencies: - csrc/ - tests/kernels/core + - vllm/kernels/triton/ - tests/kernels/test_top_k_per_row.py - tests/kernels/test_concat_mla_q.py - tests/kernels/test_rocm_fp32_router_gemm.py @@ -69,6 +74,7 @@ steps: - vllm/_aiter_ops.py - vllm/platforms/rocm.py - tests/kernels/test_fused_qk_norm_rope_gate.py + - vllm/model_executor/layers/fused_qk_norm_rope.py commands: - >- pytest -v -s @@ -81,6 +87,7 @@ steps: - label: ":nvidia: (H100) MiniMax Reduce RMS Kernels" key: kernels-minimax-reduce-rms-test-2-gpus + automatic_shard: true timeout_in_minutes: 20 num_devices: 2 device: h100 @@ -96,6 +103,7 @@ steps: - pytest -v -s kernels/core/test_minimax_reduce_rms.py mirror: amd: + automatic_shard: true label: ":amd: (MI355) MiniMax Reduce RMS Kernels" dind: false device: mi355_2 @@ -114,6 +122,7 @@ steps: - label: ":nvidia: (H100) DeepSeek V4 Kernels" key: deepseek-v4-kernel-test-h100 + automatic_shard: true timeout_in_minutes: 30 device: h100 source_file_dependencies: @@ -122,12 +131,13 @@ steps: - tests/kernels/test_fused_deepseek_v4_qnorm_rope_kv_insert.py - tests/kernels/test_top_k_per_row.py # it runs on Blackwell too - some kernels have arch-specific optimizations commands: - - pytest -v -s kernels/test_fused_deepseek_v4_*.py + - pytest -v -s kernels/test_fused_deepseek_v4_qnorm_rope_kv_insert.py - pytest -v -s kernels/test_top_k_per_row.py mirror: amd: label: ":amd: (MI355 DPX) DeepSeek V4 Fused Kernels" dind: false + automatic_shard: true device: mi355_dpx num_devices: 1 timeout_in_minutes: 60 @@ -144,6 +154,7 @@ steps: - label: ":nvidia: (H100) BF16x3 Router GEMM Kernels" key: kernels-bf16x3-router-gemm-h100 + automatic_shard: true timeout_in_minutes: 20 device: h100 source_file_dependencies: @@ -155,6 +166,7 @@ steps: - label: ":nvidia: (B200) DeepSeek V4 Kernels" key: deepseek-v4-kernel-test-b200 + automatic_shard: true timeout_in_minutes: 20 device: b200-k8s source_file_dependencies: @@ -171,13 +183,14 @@ steps: - vllm/model_executor/layers/sparse_mqa_indexer.py - tests/model_executor/layers/test_mla_short_prefill_indexer.py commands: - - pytest -v -s kernels/test_fused_deepseek_v4_*.py + - pytest -v -s kernels/test_fused_deepseek_v4_qnorm_rope_kv_insert.py - pytest -v -s kernels/moe/test_deep_gemm_mega_moe.py - pytest -v -s models/test_deepseek_v4_mega_moe.py # DeepSeek V4.1 sparse MQA logits (MXFP4 and FP8 indexer cache); SM100 only. - pytest -v -s model_executor/layers/test_mla_short_prefill_indexer.py -k sparse_mqa_logits mirror: amd: + automatic_shard: true label: ":amd: (MI355) DeepSeek V4 MoE Integration" dind: false device: mi355_1 @@ -210,6 +223,7 @@ steps: # ptxas override. - label: ":nvidia: (B200) mHC Kernels" key: kernels-mhc-test-b200 + automatic_shard: true timeout_in_minutes: 25 device: b200-k8s source_file_dependencies: @@ -221,6 +235,7 @@ steps: - pytest -v -s kernels/mhc mirror: amd: + automatic_shard: true label: ":amd: (MI355) mHC Kernels" dind: false device: mi355_1 @@ -236,6 +251,7 @@ steps: - label: ":nvidia: (B200) FLA Ops Kernels" key: kernels-fla-ops-test-b200 + automatic_shard: true timeout_in_minutes: 25 device: b200-k8s source_file_dependencies: @@ -247,6 +263,7 @@ steps: - pytest -v -s kernels/fla mirror: amd: + automatic_shard: true label: ":amd: (MI355) FLA Ops Kernels" dind: false device: mi355_1 @@ -260,6 +277,32 @@ steps: - tests/kernels/ - vllm/platforms/rocm.py +- label: ":nvidia: (B200) MiniMax M3 MegaMoE" + key: minimax-m3-mega-moe-test-b200 + automatic_shard: true + timeout_in_minutes: 20 + device: b200-k8s + source_file_dependencies: + - cmake/external_projects/deepgemm.cmake + - vllm/models/minimax_m3/nvidia/ + - vllm/utils/deep_gemm.py + - tests/models/test_minimax_m3_mega_moe.py + commands: + - pytest -v -s models/test_minimax_m3_mega_moe.py + +- label: ":nvidia: (B200) MiniMax-M3 Model Tests" + key: minimax-m3-model-tests-b200 + automatic_shard: true + timeout_in_minutes: 20 + device: b200-k8s + source_file_dependencies: + - vllm/models/minimax_m3/ + - vllm/cute_utils/ + - vllm/utils/flashinfer.py + - tests/models/minimax_m3/ + commands: + - pytest -v -s models/minimax_m3 --ignore=models/minimax_m3/amd_specialized_decode + # Catch-all for test files at the tests/kernels root. This job collects # the whole root so new files are wired by default. # Files with dedicated jobs elsewhere in this file are excluded via --ignore @@ -345,8 +388,9 @@ steps: --deselect=tests/kernels/test_bf16_skinny_gemm.py::test_every_dsv3_routed_shape_is_instantiated - pytest -v -s kernels/test_fused_muse_spark_qknorm_rope_kv_insert.py -k rocm -- label: ":nvidia: (L4) Attention Kernels Shard %N" +- label: ":nvidia: (L4) Attention Kernels" key: kernels-attention-test + automatic_shard: true device: l4 num_devices: 1 timeout_in_minutes: 30 @@ -359,15 +403,14 @@ steps: - vllm/utils/flashinfer.py - tests/kernels/attention commands: - - pytest -v -s kernels/attention --shard-id=$$BUILDKITE_PARALLEL_JOB --num-shards=$$BUILDKITE_PARALLEL_JOB_COUNT - parallelism: 7 + - pytest -v -s kernels/attention mirror: amd: - label: ":amd: (MI300) Attention Kernels Shard %N" + label: ":amd: (MI300) Attention Kernels" + automatic_shard: true dind: false device: mi300_1 timeout_in_minutes: 90 - parallelism: 6 depends_on: - image-build-amd source_file_dependencies: @@ -380,18 +423,11 @@ steps: - vllm/envs.py - vllm/platforms/rocm.py commands: - - >- - pytest -v -s - kernels/attention - --ignore=kernels/attention/test_triton_unified_attention_diffkv.py - --ignore=kernels/attention/test_rocm_aiter_mla_decode.py - --ignore=kernels/attention/test_rocm_aiter_mla_decode_metadata.py - --ignore=kernels/attention/test_rocm_aiter_mla_fp8_support.py - --ignore=kernels/attention/test_rocm_aiter_mla_op_registration.py - --shard-id=$$BUILDKITE_PARALLEL_JOB --num-shards=$$BUILDKITE_PARALLEL_JOB_COUNT + - pytest -v -s kernels/attention --ignore=kernels/attention/test_triton_unified_attention_diffkv.py --ignore=kernels/attention/test_rocm_aiter_mla_decode.py --ignore=kernels/attention/test_rocm_aiter_mla_decode_metadata.py --ignore=kernels/attention/test_rocm_aiter_mla_fp8_support.py --ignore=kernels/attention/test_rocm_aiter_mla_op_registration.py - label: ":nvidia: (H100) Attention DiffKV Kernels" key: kernels-attention-diffkv-test-h100 + automatic_shard: true timeout_in_minutes: 20 device: h100 num_devices: 1 @@ -404,6 +440,7 @@ steps: - pytest -v -s kernels/attention/test_triton_unified_attention_diffkv.py mirror: amd: + automatic_shard: true label: ":amd: (MI355 DPX) Attention DiffKV Kernels" dind: false device: mi355_dpx @@ -423,6 +460,7 @@ steps: - label: ":nvidia: (H100) MLA Kernel Test" key: kernels-flashmla-test-h100 + automatic_shard: true timeout_in_minutes: 25 device: h100 num_devices: 1 @@ -444,6 +482,7 @@ steps: - pytest -v -s kernels/attention/test_mla_cross_layer_kernel_equivalence.py mirror: amd: + automatic_shard: true label: ":amd: (MI355 DPX) MLA Kernels" dind: false device: mi355_dpx @@ -512,8 +551,9 @@ steps: commands: - pytest -v -s kernels/quantization -k "not test_nvfp4_dense_emulation" --shard-id=$$BUILDKITE_PARALLEL_JOB --num-shards=$$BUILDKITE_PARALLEL_JOB_COUNT -- label: ":nvidia: (L4) MoE Kernels Shard %N" +- label: ":nvidia: (L4) MoE Kernels" key: kernels-moe-test + automatic_shard: true device: l4 num_devices: 1 timeout_in_minutes: 50 @@ -522,20 +562,18 @@ steps: - csrc/moe/ - tests/kernels/moe - vllm/model_executor/layers/fused_moe/ + - vllm/models/muse_spark/common/ + - vllm/models/muse_spark/nvidia/ - vllm/distributed/device_communicators/ - vllm/envs.py - vllm/config commands: - - >- - pytest -v -s - kernels/moe - --ignore=kernels/moe/test_modular_oai_triton_moe.py - --shard-id=$$BUILDKITE_PARALLEL_JOB --num-shards=$$BUILDKITE_PARALLEL_JOB_COUNT - - pytest -v -s kernels/moe/test_modular_oai_triton_moe.py --shard-id=$$BUILDKITE_PARALLEL_JOB --num-shards=$$BUILDKITE_PARALLEL_JOB_COUNT - parallelism: 5 + - pytest -v -s kernels/moe --ignore=kernels/moe/test_modular_oai_triton_moe.py + - pytest -v -s kernels/moe/test_modular_oai_triton_moe.py mirror: amd: - label: ":amd: (MI300) MoE Kernels Shard %N" + label: ":amd: (MI300) MoE Kernels" + automatic_shard: true dind: false device: mi300_1 timeout_in_minutes: 55 @@ -544,6 +582,8 @@ steps: - csrc/moe/ - tests/kernels/moe - vllm/model_executor/layers/fused_moe/ + - vllm/models/muse_spark/common/ + - vllm/models/muse_spark/nvidia/ - vllm/distributed/device_communicators/ - vllm/envs.py - vllm/config @@ -566,6 +606,7 @@ steps: - tests/kernels/mamba - vllm/model_executor/layers/mamba/checkpoint.py - vllm/model_executor/layers/mamba/gdn + - vllm/models/qwen3_5/ - vllm/model_executor/layers/mamba/ops - vllm/v1/attention/backends/gdn_attn.py commands: @@ -585,6 +626,7 @@ steps: - label: ":nvidia: (H100) DeepGEMM Kernels" key: kernels-deepgemm-test-h100 + automatic_shard: true timeout_in_minutes: 35 device: h100 num_devices: 1 @@ -611,6 +653,7 @@ steps: - label: ":nvidia: (B200) MiniMax-M3 Attention Kernels" key: kernels-minimax-m3-attention-b200 + automatic_shard: true timeout_in_minutes: 30 working_dir: "/vllm-workspace/" device: b200-k8s @@ -625,6 +668,7 @@ steps: - pytest -v -s tests/kernels/attention/test_minimax_m3.py mirror: amd: + automatic_shard: true label: ":amd: (MI355 DPX) MiniMax-M3 Attention Kernels" dind: false device: mi355_dpx @@ -635,9 +679,14 @@ steps: source_file_dependencies: - vllm/_aiter_ops.py - vllm/platforms/rocm.py + - tests/kernels/attention/test_minimax_m3_nvfp4_index.py + commands: + - pytest -v -s tests/kernels/attention/test_minimax_m3.py + tests/kernels/attention/test_minimax_m3_nvfp4_index.py - label: ":nvidia: (H100) MiniMax-M3 Sparse Prefill Kernels" key: kernels-minimax-m3-sparse-prefill-h100 + automatic_shard: true timeout_in_minutes: 15 working_dir: "/vllm-workspace/" device: h100 @@ -699,6 +748,8 @@ steps: - tests/kernels/mamba/test_gdn_prefill_cutedsl.py - tests/kernels/test_bf16x3_router_gemm_cutedsl.py - tests/kernels/test_top_k_per_row.py + - csrc/fused_mqa_topk/ + - tests/kernels/attention/test_fused_mqa_topk.py - tests/kernels/quantization/test_silu_mul_nvfp4_quant.py - tests/kernels/quantization/test_block_fp8.py - tests/models/quantization/test_nvfp4.py @@ -710,6 +761,7 @@ steps: - pytest -v -s tests/kernels/attention/test_flashinfer_trtllm_attention.py - pytest -v -s tests/kernels/attention/test_flashinfer_mla_decode.py - pytest -v -s tests/kernels/test_top_k_per_row.py + - pytest -v -s tests/kernels/attention/test_fused_mqa_topk.py - pytest -v -s tests/kernels/quantization/test_silu_mul_nvfp4_quant.py - pytest -v -s tests/kernels/quantization/test_flashinfer_scaled_mm.py - pytest -v -s tests/kernels/moe/test_nvfp4_moe.py @@ -735,6 +787,7 @@ steps: - pytest -v -s tests/kernels/mamba/test_gdn_prefill_cutedsl.py mirror: amd: + automatic_shard: true label: ":amd: (MI355) NVFP4 Dense Emulation" dind: false device: mi355_1 @@ -762,6 +815,7 @@ steps: - label: ":nvidia: (B200) Gate Linear CuTeDSL Kernels" key: kernels-gate-linear-cutedsl-b200 + automatic_shard: true timeout_in_minutes: 15 working_dir: "/vllm-workspace/" device: b200-k8s @@ -783,6 +837,7 @@ steps: - pytest -v -s tests/kernels/test_ll_fp32w_gemm.py mirror: amd: + automatic_shard: true label: ":amd: (MI355) Gate Linear Router Kernels" dind: false device: mi355_1 @@ -873,6 +928,7 @@ steps: - label: ":nvidia: (H100) [Nightly] FP8 MoE Kernels" key: kernels-fp8-moe-test-1xh100 + automatic_shard: true timeout_in_minutes: 40 device: h100 num_devices: 1 @@ -890,6 +946,7 @@ steps: - label: ":nvidia: (H100) [Nightly] DeepEP FP8 MoE Kernels" key: kernels-fp8-moe-test-2xh100 + automatic_shard: true timeout_in_minutes: 45 device: h100 num_devices: 2 @@ -900,6 +957,7 @@ steps: mirror: amd: label: ":amd: (MI355) DeepEP FP8 MoE Kernels" + automatic_shard: true dind: false device: mi355_2 timeout_in_minutes: 55 @@ -919,6 +977,7 @@ steps: - label: ":nvidia: (B200) [Nightly] FP4 MoE Kernels" key: kernels-fp4-moe-test-b200 + automatic_shard: true timeout_in_minutes: 25 device: b200-k8s num_devices: 1 @@ -930,6 +989,7 @@ steps: - pytest -v -s kernels/moe/test_ocp_mx_moe.py mirror: amd: + automatic_shard: true label: ":amd: (MI355 DPX) FP4 and INT4 MoE Kernels" dind: false device: mi355_dpx @@ -961,6 +1021,7 @@ steps: - label: ":nvidia: (H100) FusedMoE Layer Kernels" key: kernels-fusedmoe-layer-test-2-h100s + automatic_shard: true timeout_in_minutes: 30 device: h100 num_devices: 2 @@ -978,6 +1039,7 @@ steps: amd: label: ":amd: (MI355) FusedMoE Layer Kernels" dind: false + automatic_shard: true device: mi355_2 timeout_in_minutes: 35 working_dir: "/vllm-workspace/tests" @@ -1004,6 +1066,7 @@ steps: - label: ":nvidia: (B200) FusedMoE Layer Kernels" key: kernels-fusedmoe-layer-test-2-b200s + automatic_shard: true timeout_in_minutes: 45 device: b200-k8s num_devices: 2 @@ -1020,6 +1083,7 @@ steps: - pytest -v -s kernels/moe/test_deepep_v2_moe.py mirror: amd: + automatic_shard: true label: ":amd: (MI355) MoRI EP Numerics" dind: false device: mi355_2 @@ -1053,6 +1117,7 @@ steps: - label: ":amd: (MI355 DPX) Native Quantization Kernels" key: amd-native-quantization-kernels-mi355 + automatic_shard: true dind: false device: mi355_dpx num_devices: 1 @@ -1081,6 +1146,7 @@ steps: - label: ":amd: (MI355 DPX) FP8 MoE Kernels" key: amd-fp8-moe-kernels-mi355 + automatic_shard: true dind: false device: mi355_dpx num_devices: 1 @@ -1111,6 +1177,7 @@ steps: - label: ":amd: (MI355 DPX) Kernels" key: amd-kernels-mi355 + automatic_shard: true dind: false device: mi355_dpx num_devices: 1 diff --git a/.buildkite/test_areas/lm_eval.yaml b/.buildkite/test_areas/lm_eval.yaml index 527203d0a818..6ecfd895cc4a 100644 --- a/.buildkite/test_areas/lm_eval.yaml +++ b/.buildkite/test_areas/lm_eval.yaml @@ -15,6 +15,7 @@ steps: - pytest -s -v evals/gsm8k/test_gsm8k_correctness.py --config-list-file=configs/models-small.yaml mirror: amd: + automatic_shard: true label: ":amd: (MI300) LM Eval Small Models" dind: false device: mi300_1 @@ -33,6 +34,7 @@ steps: - label: ":amd: (MI300) LM Eval Small Models Harness" key: amd-lm-eval-small-models-harness + automatic_shard: true dind: false device: mi300_1 timeout_in_minutes: 35 @@ -55,6 +57,7 @@ steps: - label: ":nvidia: (H200 MIG 35GB) LM Eval Watermarking" device: h200_35gb key: lm-eval-watermarking + automatic_shard: true # Recorded runs took 80–140 seconds timeout_in_minutes: 10 source_file_dependencies: @@ -69,6 +72,7 @@ steps: - pytest -s -v evals/gsm8k/test_gsm8k_correctness.py --config-list-file=configs/models-watermark.txt mirror: amd: + automatic_shard: true label: ":amd: (MI355 DPX) LM Eval Watermarking" dind: false device: mi355_dpx @@ -90,19 +94,6 @@ steps: commands: - pytest -s -v evals/gsm8k/test_gsm8k_correctness.py --config-list-file=configs/models-watermark.txt -# - label: LM Eval Large Models (4xA100) -# key: lm-eval-large-models-4xa100 -# device: a100 -# optional: true -# num_devices: 4 -# working_dir: "/vllm-workspace/.buildkite/lm-eval-harness" -# source_file_dependencies: -# - csrc/ -# - vllm/model_executor/layers/quantization -# commands: -# - export VLLM_WORKER_MULTIPROC_METHOD=spawn -# - pytest -s -v test_lm_eval_correctness.py --config-list-file=configs/models-large.txt --tp-size=4 - - label: ":nvidia: (H100) [Nightly] LM Eval Large Models" key: lm-eval-large-models-4xh100 automatic_shard: true @@ -178,6 +169,7 @@ steps: - label: ":nvidia: (B200) [Nightly] LM Eval Small Models Distributed" key: lm-eval-small-models-distributed-2xb200 + automatic_shard: true timeout_in_minutes: 120 device: b200-k8s num_devices: 2 @@ -191,6 +183,7 @@ steps: mirror: amd: label: ":amd: (MI355) LM Eval Small Models Distributed" + automatic_shard: true dind: false device: mi355_2 timeout_in_minutes: 145 @@ -265,6 +258,7 @@ steps: - label: ":nvidia: (H100) LM Eval Watermark Feature Combination" key: lm-eval-dspark-watermark-2xh100 + automatic_shard: true device: h100 num_devices: 2 # Local TP2 runs took 84–379 seconds, including startup. @@ -294,6 +288,7 @@ steps: watermarking/test_parallel_compatibility.py mirror: amd: + automatic_shard: true label: ":amd: (MI355) LM Eval Watermark Feature Combination" dind: false device: mi355_2 @@ -454,6 +449,7 @@ steps: optional: true num_devices: 2 source_file_dependencies: + - vllm/models/qwen3_5/ - vllm/model_executor/models/qwen3_5.py - vllm/model_executor/models/qwen3_5_mtp.py - vllm/model_executor/models/qwen3_next.py @@ -471,6 +467,7 @@ steps: depends_on: - image-build-amd source_file_dependencies: + - vllm/models/qwen3_5/ - vllm/model_executor/models/qwen3_5.py - vllm/model_executor/models/qwen3_5_mtp.py - vllm/model_executor/models/qwen2.py @@ -674,6 +671,7 @@ steps: - label: ":nvidia: (H200) [Nightly] LM Eval Nemotron 3 Super" key: lm-eval-nemotron-3-super-8xh200 + automatic_shard: true timeout_in_minutes: 35 device: h200 optional: true @@ -682,6 +680,7 @@ steps: - pytest -s -v evals/gsm8k/test_gsm8k_correctness.py --config-list-file=configs/models-h200-nemotron-3-super.txt mirror: amd: + automatic_shard: true label: ":amd: (MI355) LM Eval Nemotron 3 Super" dind: false device: mi355_4 @@ -718,6 +717,7 @@ steps: - label: ":amd: (MI355) [Nightly] LM Eval Large Models" key: amd-lm-eval-large-models + automatic_shard: true dind: false device: mi355_8 optional: true @@ -741,6 +741,7 @@ steps: - pytest -s -v evals/gsm8k/test_gsm8k_correctness.py --config-list-file=evals/gsm8k/configs/moe-refactor/config-h100.txt mirror: amd: + automatic_shard: true label: ":amd: (MI355) MoE Refactor Integration" dind: false device: mi355_2 @@ -770,6 +771,7 @@ steps: - pytest -s -v evals/gsm8k/test_gsm8k_correctness.py --config-list-file=evals/gsm8k/configs/moe-refactor/config-b200.txt mirror: amd: + automatic_shard: true label: ":amd: (MI355) MoE Refactor BF16 Integration" dind: false device: mi355_2 @@ -804,6 +806,7 @@ steps: - pytest -s -v evals/gsm8k/test_gsm8k_correctness.py --config-list-file=evals/gsm8k/configs/moe-refactor-dp-ep/config-b200.txt mirror: amd: + automatic_shard: true label: ":amd: (MI355) MoE Refactor BF16 DP Integration" dind: false device: mi355_2 @@ -831,6 +834,7 @@ steps: - label: ":nvidia: (L4) [Nightly] LM Eval Humming TEMPORARY" key: lm-eval-humming-l4 + automatic_shard: true optional: true timeout_in_minutes: 90 device: l4 @@ -848,45 +852,13 @@ steps: commands: - pytest -s -v evals/gsm8k/test_gsm8k_correctness.py --config-list-file=evals/gsm8k/configs/humming/config-l4.txt -- label: ":nvidia: (A100) [Nightly] LM Eval Humming Default TEMPORARY Shard %N" - key: lm-eval-humming-a100-default - optional: true - timeout_in_minutes: 75 - device: a100 - num_devices: 1 - parallelism: 3 - source_file_dependencies: - - vllm/model_executor/layers/quantization/humming.py - - vllm/model_executor/layers/quantization/utils/humming/ - - vllm/model_executor/layers/fused_moe/experts/fused_humming_moe.py - - vllm/model_executor/layers/fused_moe/oracle/ - - vllm/model_executor/kernels/linear/ - commands: - - pytest -s -v evals/gsm8k/test_gsm8k_correctness.py --config-list-file=evals/gsm8k/configs/humming/config-a100-shard-$$BUILDKITE_PARALLEL_JOB.txt - -- label: ":nvidia: (A100) [Nightly] LM Eval Humming Activation INT8 TEMPORARY Shard %N" - key: lm-eval-humming-a100-act - optional: true - timeout_in_minutes: 45 - device: a100 - num_devices: 1 - parallelism: 2 - source_file_dependencies: - - vllm/model_executor/layers/quantization/humming.py - - vllm/model_executor/layers/quantization/utils/humming/ - - vllm/model_executor/layers/fused_moe/experts/fused_humming_moe.py - - vllm/model_executor/layers/fused_moe/oracle/ - - vllm/model_executor/kernels/linear/ - commands: - - pytest -s -v evals/gsm8k/test_gsm8k_correctness.py --config-list-file=evals/gsm8k/configs/humming/config-a100-act-int8-shard-$$BUILDKITE_PARALLEL_JOB.txt - -- label: ":nvidia: (H100) [Nightly] LM Eval Humming Default TEMPORARY Shard %N" +- label: ":nvidia: (H100) [Nightly] LM Eval Humming Default TEMPORARY" key: lm-eval-humming-h100-default + automatic_shard: true optional: true timeout_in_minutes: 70 device: h100 num_devices: 1 - parallelism: 3 source_file_dependencies: - vllm/model_executor/layers/quantization/humming.py - vllm/model_executor/layers/quantization/utils/humming/ @@ -894,10 +866,11 @@ steps: - vllm/model_executor/layers/fused_moe/oracle/ - vllm/model_executor/kernels/linear/ commands: - - pytest -s -v evals/gsm8k/test_gsm8k_correctness.py --config-list-file=evals/gsm8k/configs/humming/config-h100-shard-$$BUILDKITE_PARALLEL_JOB.txt + - pytest -s -v evals/gsm8k/test_gsm8k_correctness.py --config-list-file=evals/gsm8k/configs/humming/config-h100.txt - label: ":nvidia: (H100) [Nightly] LM Eval Humming Activation FP8/INT8 TEMPORARY" key: lm-eval-humming-h100-act + automatic_shard: true optional: true timeout_in_minutes: 90 device: h100 @@ -914,6 +887,7 @@ steps: - label: ":nvidia: (H100) [Nightly] LM Eval Humming DeepEP TEMPORARY" key: lm-eval-humming-h100-deepep + automatic_shard: true optional: true timeout_in_minutes: 60 device: h100 @@ -934,6 +908,7 @@ steps: - label: ":nvidia: (B200) [Nightly] LM Eval Humming Default TEMPORARY" key: lm-eval-humming-b200-default + automatic_shard: true optional: true timeout_in_minutes: 50 device: b200-k8s @@ -965,6 +940,7 @@ steps: - label: ":nvidia: (H200 MIG 18GB) LM Eval TurboQuant k8v4" key: lm-eval-turboquant-k8v4 + automatic_shard: true timeout_in_minutes: 30 device: h200_18gb source_file_dependencies: @@ -976,6 +952,7 @@ steps: - pytest -s -v evals/gsm8k/test_gsm8k_correctness.py --config-list-file=evals/gsm8k/configs/models-turboquant-k8v4.txt mirror: amd: + automatic_shard: true label: ":amd: (MI355 DPX) LM Eval TurboQuant k8v4" dind: false device: mi355_dpx @@ -1005,6 +982,7 @@ steps: - label: ":nvidia: (H200 MIG 18GB) LM Eval TurboQuant t4nc" key: lm-eval-turboquant-t4nc + automatic_shard: true timeout_in_minutes: 30 device: h200_18gb source_file_dependencies: @@ -1018,6 +996,7 @@ steps: amd: label: ":amd: (MI355 DPX) LM Eval TurboQuant t4nc" dind: false + automatic_shard: true device: mi355_dpx num_devices: 1 timeout_in_minutes: 30 @@ -1045,6 +1024,7 @@ steps: - label: ":nvidia: (H200 MIG 18GB) LM Eval TurboQuant k3v4nc" key: lm-eval-turboquant-k3v4nc + automatic_shard: true timeout_in_minutes: 30 device: h200_18gb source_file_dependencies: @@ -1058,6 +1038,7 @@ steps: amd: label: ":amd: (MI355 DPX) LM Eval TurboQuant k3v4nc" dind: false + automatic_shard: true device: mi355_dpx num_devices: 1 timeout_in_minutes: 30 @@ -1085,6 +1066,7 @@ steps: - label: ":nvidia: (H200 MIG 18GB) LM Eval TurboQuant t3nc" key: lm-eval-turboquant-t3nc + automatic_shard: true timeout_in_minutes: 30 device: h200_18gb source_file_dependencies: @@ -1098,6 +1080,7 @@ steps: amd: label: ":amd: (MI355 DPX) LM Eval TurboQuant t3nc" dind: false + automatic_shard: true device: mi355_dpx num_devices: 1 timeout_in_minutes: 30 @@ -1126,6 +1109,7 @@ steps: # Octave kernels are built only for gfx942 and gfx1100, so this runs on MI300. - label: ":amd: (MI300) LM Eval Octave KV Cache" key: amd-lm-eval-octave-mi300 + automatic_shard: true dind: false device: mi300_1 num_devices: 1 @@ -1182,6 +1166,7 @@ steps: - label: ":nvidia: (B200) [Nightly] GPQA Eval (GPT-OSS)" key: gpqa-eval-gpt-oss-2xb200 + automatic_shard: true timeout_in_minutes: 30 device: b200-k8s optional: true @@ -1194,6 +1179,7 @@ steps: - pytest -s -v evals/gpt_oss/test_gpqa_correctness.py --config-list-file=configs/models-b200.txt mirror: amd: + automatic_shard: true label: ":amd: (MI355) GPQA Eval (GPT-OSS)" dind: false device: mi355_2 @@ -1272,6 +1258,7 @@ steps: - label: ":nvidia: (DGX Spark) [Nightly] GPQA Eval (GPT-OSS)" key: gpqa-eval-gpt-oss-spark + automatic_shard: true timeout_in_minutes: 35 device: dgx-spark optional: true @@ -1286,6 +1273,7 @@ steps: - pytest -s -v evals/gpt_oss/test_gpqa_correctness.py --config-list-file=configs/models-spark.txt mirror: amd: + automatic_shard: true label: ":amd: (MI355 DPX) GPQA Eval (GPT-OSS TP1)" # The Spark config is a backend-neutral GPT-OSS TP1 baseline. dind: false @@ -1309,6 +1297,7 @@ steps: - label: ":nvidia: (H200 MIG 35GB) LM Eval KV-Offload" key: kv-offload-small + automatic_shard: true timeout_in_minutes: 30 device: h200_35gb source_file_dependencies: @@ -1321,6 +1310,7 @@ steps: - pytest -s -v evals/gsm8k/test_gsm8k_offloading.py -k "nemotron-h-8b or gemma-4-e4b-it" mirror: amd: + automatic_shard: true label: ":amd: (MI355 DPX) LM Eval KV-Offload" dind: false device: mi355_dpx @@ -1334,10 +1324,13 @@ steps: - vllm/v1/kv_offload/ - vllm/v1/simple_kv_offload/ - tests/evals/gsm8k/test_gsm8k_offloading.py + - vllm/distributed/kv_transfer/kv_connector/v1/umbp/ + - tests/v1/kv_connector/umbp_test_utils.py - vllm/platforms/rocm.py - label: ":nvidia: (H100) LM Eval KV-Offload Medium" key: kv-offload-medium + automatic_shard: true timeout_in_minutes: 45 device: h100 num_devices: 2 @@ -1351,6 +1344,7 @@ steps: - pytest -s -v evals/gsm8k/test_gsm8k_offloading.py -k "qwen3.5-35b or deepseek-v2-lite" mirror: amd: + automatic_shard: true label: ":amd: (MI355) LM Eval KV-Offload Medium" dind: false device: mi355_2 @@ -1368,6 +1362,7 @@ steps: - label: ":nvidia: (H100) [Nightly] LM Eval KV-Offload Large" key: kv-offload-large + automatic_shard: true # Nightly only: 4-GPU H100 pods wait behind smaller jobs on per-PR runs. optional: true timeout_in_minutes: 40 @@ -1383,6 +1378,7 @@ steps: - pytest -s -v evals/gsm8k/test_gsm8k_offloading.py -k "deepseek-v4-flash" mirror: amd: + automatic_shard: true label: ":amd: (MI355) LM Eval KV-Offload Large" dind: false device: mi355_4 @@ -1402,6 +1398,7 @@ steps: - label: ":nvidia: (H200 MIG 35GB) MRCR Eval Small Models" key: mrcr-eval-small-models + automatic_shard: true device: h200_35gb timeout_in_minutes: 25 source_file_dependencies: @@ -1410,6 +1407,7 @@ steps: - pytest -s -v evals/mrcr/test_mrcr_correctness.py --config-list-file=evals/mrcr/configs/models-small.txt mirror: amd: + automatic_shard: true label: ":amd: (MI355 DPX) MRCR Eval Small Models" dind: false device: mi355_dpx @@ -1427,6 +1425,7 @@ steps: - label: ":amd: (MI355) [Nightly] LM Eval Large Models Harness" key: amd-lm-eval-large-models-harness + automatic_shard: true dind: false device: mi355_8 optional: true @@ -1452,6 +1451,7 @@ steps: - label: ":amd: (MI355) [Nightly] LM Eval Large Models ROCm Harness" key: amd-lm-eval-large-models-rocm-harness + automatic_shard: true dind: false device: mi355_8 optional: true diff --git a/.buildkite/test_areas/lora.yaml b/.buildkite/test_areas/lora.yaml index afd7d07220e5..74770b4a4d89 100644 --- a/.buildkite/test_areas/lora.yaml +++ b/.buildkite/test_areas/lora.yaml @@ -52,6 +52,7 @@ steps: source_file_dependencies: - vllm/lora - vllm/model_executor/layers/fused_moe/ + - vllm/model_executor/models/transformers/moe.py - tests/lora commands: # FIXIT: find out which code initialize cuda before running the test @@ -84,6 +85,7 @@ steps: source_file_dependencies: - vllm/lora - vllm/model_executor/layers/fused_moe/ + - vllm/model_executor/models/transformers/moe.py - vllm/platforms/rocm.py - tests/lora commands: diff --git a/.buildkite/test_areas/misc.yaml b/.buildkite/test_areas/misc.yaml index 0cbfe1d0a883..3f162810a3d4 100644 --- a/.buildkite/test_areas/misc.yaml +++ b/.buildkite/test_areas/misc.yaml @@ -40,6 +40,7 @@ steps: - label: ":nvidia: (H200 MIG 18GB) V1 Logits + Oracle" key: v1-logits-oracle + automatic_shard: true timeout_in_minutes: 45 device: h200_18gb source_file_dependencies: *v1-sample-logits-deps @@ -55,6 +56,7 @@ steps: amd: label: ":amd: (MI355 DPX) V1 Logits + Oracle" dind: false + automatic_shard: true device: mi355_dpx timeout_in_minutes: 70 depends_on: @@ -63,6 +65,7 @@ steps: - label: ":nvidia: (H200 MIG 35GB) V1 Core" device: h200_35gb key: v1-core + automatic_shard: true timeout_in_minutes: 45 source_file_dependencies: &v1-core-kv-metrics-deps - vllm/config/ @@ -98,6 +101,7 @@ steps: - pytest -v -s -m 'not cpu_test' v1/core mirror: amd: + automatic_shard: true label: ":amd: (MI355 DPX) V1 Core" dind: false device: mi355_dpx @@ -121,6 +125,7 @@ steps: amd: label: ":amd: (MI355 DPX) V1 Executor + Worker" dind: false + automatic_shard: true device: mi355_dpx timeout_in_minutes: 60 depends_on: @@ -197,6 +202,7 @@ steps: # the connector or its tests change. - label: ":amd: (MI355 DPX) UMBP Connector Unit Tests" key: amd-umbp-connector-unit-mi355 + automatic_shard: true dind: false device: mi355_dpx num_devices: 1 @@ -210,9 +216,33 @@ steps: - vllm/v1/kv_cache_interface.py - tests/v1/kv_connector/unit/test_umbp_ - tests/v1/kv_connector/umbp_test_utils.py + - tests/v1/kv_connector/unit/utils.py + - tests/v1/kv_connector/conftest.py + - docker/Dockerfile.rocm_base commands: - export VLLM_WORKER_MULTIPROC_METHOD=spawn - - pytest -v -s -rs v1/kv_connector/unit/test_umbp_*.py + - pytest -v -s -rs v1/kv_connector/unit/test_umbp_async_lookup.py v1/kv_connector/unit/test_umbp_core.py v1/kv_connector/unit/test_umbp_embedded.py v1/kv_connector/unit/test_umbp_events.py v1/kv_connector/unit/test_umbp_hybrid.py + +- label: ":amd: (MI355 DPX) UMBP Connector Integration" + key: amd-umbp-connector-integration-mi355 + dind: false + device: mi355_dpx + num_devices: 1 + timeout_in_minutes: 30 + working_dir: /vllm-workspace/tests + depends_on: + - image-build-amd + source_file_dependencies: + - vllm/distributed/kv_transfer/kv_connector/v1/umbp/ + - vllm/distributed/kv_transfer/kv_connector/v1/base.py + - tests/v1/kv_connector/umbp_integration/ + - tests/v1/kv_connector/umbp_test_utils.py + - tests/v1/kv_connector/conftest.py + - tests/v1/kv_connector/rocm_pd_accuracy_utils.py + - docker/Dockerfile.rocm_base + commands: + - export VLLM_WORKER_MULTIPROC_METHOD=spawn + - pytest -v -s -rs v1/kv_connector/umbp_integration # Single-node MoRIIO P/D on dummy weights, WRITE and READ mode: prefill and # decode on separate GPUs over xGMI, compared exactly against a direct serve. @@ -271,6 +301,7 @@ steps: - label: ":nvidia: (H200 MIG 35GB) V1 Metrics + LM Eval" device: h200_35gb key: v1-metrics-lmeval + automatic_shard: true timeout_in_minutes: 45 env: # GitHub intermittently rejects HTTP/2 upload-pack requests from the H200 @@ -288,6 +319,7 @@ steps: - pytest -v -s entrypoints/openai/correctness/test_lmeval.py::test_lm_eval_accuracy_v1_engine mirror: amd: + automatic_shard: true label: ":amd: (MI355 DPX) V1 Metrics + LM Eval" dind: false device: mi355_dpx @@ -319,13 +351,13 @@ steps: - tests/v1 - tests/watermarking - examples/disaggregated/mooncake_connector/mooncake_connector_proxy.py + - rust/src/engine-core-client/src/tests/prompt_block_hash_vectors.json device: cpu-small commands: # split the test to avoid interference - pytest -v -s -m 'cpu_test' v1/core - pytest -v -s v1/structured_output - pytest -v -s v1/test_serial_utils.py - - pytest -v -s v1/worker/test_gpu_worker.py::test_cpu_worker_without_triton - pytest -v -s v1/worker/test_cpu_worker_kv_budget.py - pytest -v -s v1/test_kv_cache_spec_registry.py - pytest -v -s v1/cudagraph/test_cudagraph_manager.py @@ -336,6 +368,7 @@ steps: - label: ":nvidia: (H200 MIG 18GB) Extract Hidden States Integration" key: extract-hidden-states-integration + automatic_shard: true timeout_in_minutes: 20 device: h200_18gb working_dir: "/vllm-workspace" @@ -350,6 +383,7 @@ steps: - pytest -v -s tests/v1/kv_connector/extract_hidden_states_integration mirror: amd: + automatic_shard: true label: ":amd: (MI355 DPX) Extract Hidden States Integration Single GPU" dind: false device: mi355_dpx @@ -372,6 +406,7 @@ steps: - label: ":nvidia: (L4) Extract Hidden States Integration" key: extract-hidden-states-integration-2-gpus + automatic_shard: true timeout_in_minutes: 20 device: l4 num_devices: 2 @@ -387,6 +422,7 @@ steps: - pytest -v -s -m 'distributed' tests/v1/kv_connector/extract_hidden_states_integration mirror: amd: + automatic_shard: true label: ":amd: (MI355) Extract Hidden States Integration" dind: false device: mi355_2 @@ -401,7 +437,9 @@ steps: - vllm/model_executor/model_loader/ - vllm/model_executor/models/extract_hidden_states.py - vllm/model_executor/models/llama.py + - vllm/models/qwen3_5/ - vllm/model_executor/models/qwen3_5.py + - vllm/model_executor/models/qwen3_5_mtp.py - vllm/model_executor/models/qwen3_next.py - vllm/model_executor/models/registry.py - vllm/transformers_utils/configs/extract_hidden_states.py @@ -493,44 +531,40 @@ steps: depends_on: - image-build-amd -# Disabled until the tracing packages installation is locked. -# - label: ":nvidia: (L4) Metrics, Tracing" -# key: metrics-tracing-2-gpus -# timeout_in_minutes: 25 -# device: l4 -# num_devices: 2 -# source_file_dependencies: -# - vllm/config/ -# - vllm/distributed/ -# - "!vllm/distributed/kv_transfer/" -# - vllm/engine/ -# - vllm/inputs/ -# - vllm/model_executor/ -# - vllm/multimodal/ -# - vllm/platforms/ -# - vllm/sampling_params.py -# - vllm/tracing/ -# - vllm/transformers_utils/ -# - vllm/utils/ -# - vllm/v1/ -# - tests/v1/tracing -# - tests/tracing/ -# commands: -# - "pip install \ -# 'opentelemetry-sdk>=1.26.0' \ -# 'opentelemetry-api>=1.26.0' \ -# 'opentelemetry-exporter-otlp>=1.26.0' \ -# 'opentelemetry-semantic-conventions-ai>=0.4.1'" -# - pytest -v -s v1/tracing -# - pytest -v -s tracing -# mirror: -# amd: -# label: ":amd: (MI300) Metrics, Tracing" -# dind: false -# device: mi300_2 -# timeout_in_minutes: 30 -# depends_on: -# - image-build-amd +- label: ":nvidia: (L4) Metrics, Tracing" + key: metrics-tracing-2-gpus + automatic_shard: true + timeout_in_minutes: 25 + device: l4 + num_devices: 2 + source_file_dependencies: + - vllm/config/ + - vllm/distributed/ + - "!vllm/distributed/kv_transfer/" + - vllm/engine/ + - vllm/inputs/ + - vllm/model_executor/ + - vllm/multimodal/ + - vllm/platforms/ + - vllm/sampling_params.py + - vllm/tracing/ + - vllm/transformers_utils/ + - vllm/utils/ + - vllm/v1/ + - tests/v1/tracing + - tests/tracing/ + commands: + - pytest -v -s v1/tracing + - pytest -v -s tracing + mirror: + amd: + automatic_shard: true + label: ":amd: (MI300) Metrics, Tracing" + dind: false + device: mi300_2 + timeout_in_minutes: 30 + depends_on: + - image-build-amd - label: ":nvidia: (H200 MIG 18GB) [Nightly] Python-only Installation" key: python-only-installation @@ -570,6 +604,7 @@ steps: - label: ":nvidia: (H200 MIG 35GB) Async Engine, Inputs, Utils, Worker" device: h200_35gb key: async-engine-inputs-utils-worker + automatic_shard: true timeout_in_minutes: 25 source_file_dependencies: - vllm/assets/ @@ -597,6 +632,7 @@ steps: amd: label: ":amd: (MI355 DPX) Async Engine, Inputs, Utils, Worker" dind: false + automatic_shard: true device: mi355_dpx timeout_in_minutes: 50 depends_on: @@ -845,149 +881,133 @@ steps: commands: - pytest -v -s tool_parsers -# Disabled until the pytest plugins installation is locked. -# - label: ":nvidia: (A100) Batch Invariance" -# key: batch-invariance-a100 -# timeout_in_minutes: 60 -# device: a100 -# source_file_dependencies: -# - vllm/v1/attention -# - vllm/model_executor/layers -# - vllm/model_executor/kernels/linear -# - vllm/model_executor/determinism -# - tests/v1/determinism/ -# commands: -# - export VLLM_WORKER_MULTIPROC_METHOD=spawn -# - pip install pytest-timeout pytest-forked -# - pytest -v -s v1/determinism/test_batch_invariance.py -# - VLLM_TEST_MODEL=deepseek-ai/DeepSeek-V2-Lite-Chat pytest -v -s v1/determinism/test_batch_invariance.py::test_v1_generation_is_deterministic_across_batch_sizes_with_needle -k TRITON_MLA -# mirror: -# amd: -# label: ":amd: (MI250) Batch Invariance" -# dind: false -# device: mi250_1 -# timeout_in_minutes: 60 -# env: -# VLLM_GPU_MEMORY_UTILIZATION: "0.8" -# VLLM_TEST_CLEAN_GPU_MEMORY: "1" -# depends_on: -# - image-build-amd -# source_file_dependencies: -# - vllm/v1/attention -# - vllm/model_executor/layers -# - vllm/model_executor/kernels/linear -# - vllm/model_executor/determinism -# - tests/v1/determinism/ -# - vllm/_aiter_ops.py -# - vllm/platforms/rocm.py +# Key kept from the former A100 step's mirror for job history and retries. +- label: ":amd: (MI250) Batch Invariance" + key: amd-batch-invariance-a100 + dind: false + device: mi250_1 + timeout_in_minutes: 60 + env: + VLLM_GPU_MEMORY_UTILIZATION: "0.8" + VLLM_TEST_CLEAN_GPU_MEMORY: "1" + depends_on: + - image-build-amd + source_file_dependencies: + - vllm/v1/attention + - vllm/model_executor/layers + - vllm/model_executor/kernels/linear + - vllm/model_executor/determinism + - tests/v1/determinism/ + - vllm/_aiter_ops.py + - vllm/platforms/rocm.py + - docker/Dockerfile.rocm_base + commands: + - export VLLM_WORKER_MULTIPROC_METHOD=spawn + - pytest -v -s v1/determinism/test_batch_invariance.py + - VLLM_TEST_MODEL=deepseek-ai/DeepSeek-V2-Lite-Chat pytest -v -s v1/determinism/test_batch_invariance.py::test_v1_generation_is_deterministic_across_batch_sizes_with_needle -k TRITON_MLA -# Disabled until the pytest plugins installation is locked. -# - label: ":nvidia: (H100) Batch Invariance" -# key: batch-invariance-h100 -# automatic_shard: true -# timeout_in_minutes: 60 -# device: h100 -# source_file_dependencies: -# - vllm/v1/attention -# - vllm/model_executor/layers -# - vllm/lora/ops/triton_ops -# - vllm/model_executor/kernels/linear -# - vllm/model_executor/determinism -# - tests/v1/determinism/ -# commands: -# - export VLLM_WORKER_MULTIPROC_METHOD=spawn -# - pip install pytest-timeout pytest-forked -# - pytest -v -s v1/determinism/test_batch_invariance.py -# - pytest -v -s v1/determinism/test_rms_norm_batch_invariant.py -# - pytest -v -s v1/determinism/test_lora_shrink_batch_invariant.py -# - VLLM_TEST_MODEL=deepseek-ai/DeepSeek-V2-Lite-Chat pytest -v -s v1/determinism/test_batch_invariance.py::test_v1_generation_is_deterministic_across_batch_sizes_with_needle -k TRITON_MLA -# - VLLM_TEST_MODEL=Qwen/Qwen3-30B-A3B-Thinking-2507-FP8 pytest -v -s v1/determinism/test_batch_invariance.py::test_v1_generation_is_deterministic_across_batch_sizes_with_needle -k FLASH_ATTN -# mirror: -# amd: -# label: ":amd: (MI300) Batch Invariance" -# dind: false -# automatic_shard: true -# device: mi300_1 -# timeout_in_minutes: 60 -# depends_on: -# - image-build-amd -# source_file_dependencies: -# - vllm/v1/attention -# - vllm/model_executor/layers -# - vllm/lora/ops/triton_ops -# - vllm/model_executor/kernels/linear -# - vllm/model_executor/determinism -# - tests/v1/determinism/ -# - vllm/_aiter_ops.py -# - vllm/platforms/rocm.py -# commands: -# - export VLLM_WORKER_MULTIPROC_METHOD=spawn -# - pip install pytest-timeout pytest-forked -# - pytest -v -s v1/determinism/test_batch_invariance.py -# - pytest -v -s v1/determinism/test_rms_norm_batch_invariant.py -# - pytest -v -s v1/determinism/test_lora_shrink_batch_invariant.py -# - pytest -v -s v1/determinism/test_cutlass_batch_invariance.py -# - pytest -v -s v1/determinism/test_matmul_batch_invariant.py -# - VLLM_TEST_MODEL=deepseek-ai/DeepSeek-V2-Lite-Chat pytest -v -s v1/determinism/test_batch_invariance.py::test_v1_generation_is_deterministic_across_batch_sizes_with_needle -k TRITON_MLA -# - VLLM_TEST_MODEL=Qwen/Qwen3-30B-A3B-Thinking-2507-FP8 pytest -v -s v1/determinism/test_batch_invariance.py::test_v1_generation_is_deterministic_across_batch_sizes_with_needle -k TRITON_ATTN +- label: ":nvidia: (H100) Batch Invariance" + key: batch-invariance-h100 + automatic_shard: true + timeout_in_minutes: 60 + device: h100 + source_file_dependencies: + - vllm/v1/attention + - vllm/model_executor/layers + - vllm/lora/ops/triton_ops + - vllm/model_executor/kernels/linear + - vllm/model_executor/determinism + - tests/v1/determinism/ + commands: + - export VLLM_WORKER_MULTIPROC_METHOD=spawn + - pytest -v -s v1/determinism/test_batch_invariance.py + - pytest -v -s v1/determinism/test_rms_norm_batch_invariant.py + - pytest -v -s v1/determinism/test_lora_shrink_batch_invariant.py + - VLLM_TEST_MODEL=deepseek-ai/DeepSeek-V2-Lite-Chat pytest -v -s v1/determinism/test_batch_invariance.py::test_v1_generation_is_deterministic_across_batch_sizes_with_needle -k TRITON_MLA + - VLLM_TEST_MODEL=Qwen/Qwen3-30B-A3B-Thinking-2507-FP8 pytest -v -s v1/determinism/test_batch_invariance.py::test_v1_generation_is_deterministic_across_batch_sizes_with_needle -k FLASH_ATTN + mirror: + amd: + label: ":amd: (MI300) Batch Invariance" + dind: false + automatic_shard: true + device: mi300_1 + timeout_in_minutes: 60 + depends_on: + - image-build-amd + source_file_dependencies: + - vllm/v1/attention + - vllm/model_executor/layers + - vllm/lora/ops/triton_ops + - vllm/model_executor/kernels/linear + - vllm/model_executor/determinism + - tests/v1/determinism/ + - vllm/_aiter_ops.py + - vllm/platforms/rocm.py + commands: + - export VLLM_WORKER_MULTIPROC_METHOD=spawn + - pytest -v -s v1/determinism/test_batch_invariance.py + - pytest -v -s v1/determinism/test_rms_norm_batch_invariant.py + - pytest -v -s v1/determinism/test_lora_shrink_batch_invariant.py + - pytest -v -s v1/determinism/test_cutlass_batch_invariance.py + - pytest -v -s v1/determinism/test_matmul_batch_invariant.py + - VLLM_TEST_MODEL=deepseek-ai/DeepSeek-V2-Lite-Chat pytest -v -s v1/determinism/test_batch_invariance.py::test_v1_generation_is_deterministic_across_batch_sizes_with_needle -k TRITON_MLA + - VLLM_TEST_MODEL=Qwen/Qwen3-30B-A3B-Thinking-2507-FP8 pytest -v -s v1/determinism/test_batch_invariance.py::test_v1_generation_is_deterministic_across_batch_sizes_with_needle -k TRITON_ATTN -# Disabled until the pytest plugins installation is locked. -# - label: ":nvidia: (B200) Batch Invariance" -# key: batch-invariance-b200 -# automatic_shard: true -# timeout_in_minutes: 45 -# device: b200-k8s -# source_file_dependencies: -# - vllm/v1/attention -# - vllm/model_executor/layers -# - vllm/lora/ops/triton_ops -# - vllm/model_executor/kernels/linear -# - vllm/model_executor/determinism -# - tests/v1/determinism/ -# commands: -# - export VLLM_WORKER_MULTIPROC_METHOD=spawn -# - pip install pytest-timeout pytest-forked -# - pytest -v -s v1/determinism/test_batch_invariance.py -# - pytest -v -s v1/determinism/test_rms_norm_batch_invariant.py -# - pytest -v -s v1/determinism/test_lora_shrink_batch_invariant.py -# - pytest -v -s v1/determinism/test_batch_invariance_vlm.py -# - VLLM_TEST_MODEL=deepseek-ai/DeepSeek-V2-Lite-Chat pytest -v -s v1/determinism/test_batch_invariance.py::test_v1_generation_is_deterministic_across_batch_sizes_with_needle -k TRITON_MLA -# - VLLM_TEST_MODEL=Qwen/Qwen3-30B-A3B-Thinking-2507-FP8 pytest -v -s v1/determinism/test_batch_invariance.py::test_v1_generation_is_deterministic_across_batch_sizes_with_needle -k FLASH_ATTN -# - pytest -v -s v1/determinism/test_nvfp4_batch_invariant.py -# - pytest -v -s v1/determinism/test_matmul_batch_invariant.py -# - pytest -v -s v1/determinism/test_cutlass_batch_invariance.py -# - pytest -v -s v1/determinism/test_online_batch_invariance.py -# mirror: -# amd: -# label: ":amd: (MI355) Batch Invariance" -# dind: false -# device: mi355_1 -# timeout_in_minutes: 60 -# depends_on: -# - image-build-amd -# source_file_dependencies: -# - vllm/v1/attention -# - vllm/model_executor/layers -# - vllm/lora/ops/triton_ops -# - vllm/model_executor/kernels/linear -# - vllm/model_executor/determinism -# - tests/v1/determinism/ -# - vllm/_aiter_ops.py -# - vllm/platforms/rocm.py -# commands: -# - export VLLM_WORKER_MULTIPROC_METHOD=spawn -# - pip install pytest-timeout pytest-forked -# - pytest -v -s v1/determinism/test_batch_invariance.py -# - pytest -v -s v1/determinism/test_rms_norm_batch_invariant.py -# - pytest -v -s v1/determinism/test_lora_shrink_batch_invariant.py -# - pytest -v -s v1/determinism/test_batch_invariance_vlm.py -# - VLLM_TEST_MODEL=deepseek-ai/DeepSeek-V2-Lite-Chat pytest -v -s v1/determinism/test_batch_invariance.py::test_v1_generation_is_deterministic_across_batch_sizes_with_needle -k TRITON_MLA -# - VLLM_TEST_MODEL=Qwen/Qwen3-30B-A3B-Thinking-2507-FP8 pytest -v -s v1/determinism/test_batch_invariance.py::test_v1_generation_is_deterministic_across_batch_sizes_with_needle -k TRITON_ATTN -# - pytest -v -s v1/determinism/test_nvfp4_batch_invariant.py -# - pytest -v -s v1/determinism/test_matmul_batch_invariant.py -# - pytest -v -s v1/determinism/test_cutlass_batch_invariance.py -# - pytest -v -s v1/determinism/test_online_batch_invariance.py -# - pytest -v -s v1/determinism/test_mx_linear_batch_invariant.py +- label: ":nvidia: (B200) Batch Invariance" + key: batch-invariance-b200 + automatic_shard: true + timeout_in_minutes: 45 + device: b200-k8s + source_file_dependencies: + - vllm/v1/attention + - vllm/model_executor/layers + - vllm/lora/ops/triton_ops + - vllm/model_executor/kernels/linear + - vllm/model_executor/determinism + - tests/v1/determinism/ + commands: + - export VLLM_WORKER_MULTIPROC_METHOD=spawn + - pytest -v -s v1/determinism/test_batch_invariance.py + - pytest -v -s v1/determinism/test_rms_norm_batch_invariant.py + - pytest -v -s v1/determinism/test_lora_shrink_batch_invariant.py + - pytest -v -s v1/determinism/test_batch_invariance_vlm.py + - VLLM_TEST_MODEL=deepseek-ai/DeepSeek-V2-Lite-Chat pytest -v -s v1/determinism/test_batch_invariance.py::test_v1_generation_is_deterministic_across_batch_sizes_with_needle -k TRITON_MLA + - VLLM_TEST_MODEL=Qwen/Qwen3-30B-A3B-Thinking-2507-FP8 pytest -v -s v1/determinism/test_batch_invariance.py::test_v1_generation_is_deterministic_across_batch_sizes_with_needle -k FLASH_ATTN + - pytest -v -s v1/determinism/test_nvfp4_batch_invariant.py + - pytest -v -s v1/determinism/test_matmul_batch_invariant.py + - pytest -v -s v1/determinism/test_cutlass_batch_invariance.py + - pytest -v -s v1/determinism/test_online_batch_invariance.py + mirror: + amd: + label: ":amd: (MI355) Batch Invariance" + dind: false + automatic_shard: true + device: mi355_1 + timeout_in_minutes: 60 + depends_on: + - image-build-amd + source_file_dependencies: + - vllm/v1/attention + - vllm/model_executor/layers + - vllm/lora/ops/triton_ops + - vllm/model_executor/kernels/linear + - vllm/model_executor/determinism + - tests/v1/determinism/ + - vllm/_aiter_ops.py + - vllm/platforms/rocm.py + commands: + - export VLLM_WORKER_MULTIPROC_METHOD=spawn + - pytest -v -s v1/determinism/test_batch_invariance.py + - pytest -v -s v1/determinism/test_rms_norm_batch_invariant.py + - pytest -v -s v1/determinism/test_lora_shrink_batch_invariant.py + - pytest -v -s v1/determinism/test_batch_invariance_vlm.py + - VLLM_TEST_MODEL=deepseek-ai/DeepSeek-V2-Lite-Chat pytest -v -s v1/determinism/test_batch_invariance.py::test_v1_generation_is_deterministic_across_batch_sizes_with_needle -k TRITON_MLA + - VLLM_TEST_MODEL=Qwen/Qwen3-30B-A3B-Thinking-2507-FP8 pytest -v -s v1/determinism/test_batch_invariance.py::test_v1_generation_is_deterministic_across_batch_sizes_with_needle -k TRITON_ATTN + - pytest -v -s v1/determinism/test_nvfp4_batch_invariant.py + - pytest -v -s v1/determinism/test_matmul_batch_invariant.py + - pytest -v -s v1/determinism/test_cutlass_batch_invariance.py + - pytest -v -s v1/determinism/test_online_batch_invariance.py + - pytest -v -s v1/determinism/test_mx_linear_batch_invariant.py - label: ":nvidia: (H100) [Nightly] Batch Invariance (Distributed)" key: batch-invariance-distributed-4xh100 @@ -1009,6 +1029,7 @@ steps: - VLLM_TEST_TP_SIZE=4 VLLM_TEST_DISABLE_CUSTOM_AR=1 pytest -v -s v1/determinism/test_batch_invariance.py -k bs1_vs_bsN - VLLM_TP_SIZE=4 VLLM_NEEDLE_TRIALS=2 pytest -v -s v1/determinism/test_batch_invariance.py::test_v1_generation_is_deterministic_across_batch_sizes_with_needle -k FLASH_ATTN - VLLM_TEST_MODEL=allenai/OLMoE-1B-7B-0924 VLLM_TEST_SERVER_ARGS="--data-parallel-size 4 --enable-expert-parallel --max-model-len 4096" pytest -v -s v1/determinism/test_online_batch_invariance.py + - VLLM_TEST_MODEL=ibm-granite/granite-3.0-1b-a400m-base VLLM_TEST_SERVER_ARGS="--data-parallel-size 4 --enable-expert-parallel --max-model-len 4096" pytest -v -s v1/determinism/test_online_batch_invariance.py mirror: amd: label: ":amd: (MI300) Batch Invariance (Distributed)" @@ -1031,10 +1052,12 @@ steps: - VLLM_TEST_TP_SIZE=4 VLLM_TEST_DISABLE_CUSTOM_AR=1 pytest -v -s v1/determinism/test_batch_invariance.py -k bs1_vs_bsN - VLLM_TP_SIZE=4 VLLM_NEEDLE_TRIALS=2 pytest -v -s v1/determinism/test_batch_invariance.py::test_v1_generation_is_deterministic_across_batch_sizes_with_needle -k TRITON_ATTN - VLLM_TEST_MODEL=allenai/OLMoE-1B-7B-0924 VLLM_TEST_SERVER_ARGS="--data-parallel-size 4 --enable-expert-parallel --max-model-len 4096" pytest -v -s v1/determinism/test_online_batch_invariance.py + - VLLM_TEST_MODEL=ibm-granite/granite-3.0-1b-a400m-base VLLM_TEST_SERVER_ARGS="--data-parallel-size 4 --enable-expert-parallel --max-model-len 4096" pytest -v -s v1/determinism/test_online_batch_invariance.py - label: ":nvidia: (H200 MIG 35GB) [Nightly] Acceptance Length (Large Models)" device: h200_35gb key: acceptance-length-test-large-models + automatic_shard: true timeout_in_minutes: 20 gpu: h100 optional: true @@ -1048,6 +1071,7 @@ steps: - pytest -v -s v1/spec_decode/test_acceptance_length.py -m slow_test mirror: amd: + automatic_shard: true label: ":amd: (MI355 DPX) Acceptance Length (Large Models)" dind: false device: mi355_dpx diff --git a/.buildkite/test_areas/models_basic.yaml b/.buildkite/test_areas/models_basic.yaml index b5c64ec7b3f0..03fbd875f782 100644 --- a/.buildkite/test_areas/models_basic.yaml +++ b/.buildkite/test_areas/models_basic.yaml @@ -4,6 +4,7 @@ depends_on: steps: - label: ":nvidia: (H200 MIG 18GB) Basic Models (Initialization)" key: basic-models-tests-initialization + automatic_shard: true timeout_in_minutes: 25 device: h200_18gb source_file_dependencies: @@ -19,6 +20,7 @@ steps: - pytest -v -s models/test_beam_construction.py mirror: amd: + automatic_shard: true label: ":amd: (MI355 DPX) Basic Models (Initialization)" dind: false device: mi355_dpx @@ -70,16 +72,17 @@ steps: - tests/models/transformers/test_backend.py - tests/models/test_registry.py - tests/models/test_deepseek_v4_vl_rocm.py + - tests/models/test_deepseek_v4_vl.py - tests/models/test_deepseek_v41_decoder_replay_layers.py - tests/models/test_deepseek_v41_replay_batch.py - tests/models/test_deepseek_v41_pcp.py - tests/models/test_deepseek_v41_pipeline.py - tests/models/transformers/test_layer_registry_e2e.py - tests/models/test_deepseek_v41_replay_start.py - - tests/models/test_deepseek_v4_vl_stream_weights.py - tests/models/test_muse_spark_rocm_loading.py - tests/models/test_muse_spark_logits_local.py - tests/models/test_muse_spark_rocm_shared_rope.py + - tests/models/test_muse_spark_rocm_mono.py commands: - >- pytest -v -s @@ -87,16 +90,17 @@ steps: models/transformers/test_backend.py models/test_registry.py models/test_deepseek_v4_vl_rocm.py + models/test_deepseek_v4_vl.py models/transformers/test_layer_registry_e2e.py models/test_deepseek_v41_replay_start.py models/test_deepseek_v41_decoder_replay_layers.py models/test_deepseek_v41_replay_batch.py models/test_deepseek_v41_pcp.py models/test_deepseek_v41_pipeline.py - models/test_deepseek_v4_vl_stream_weights.py models/test_muse_spark_rocm_loading.py models/test_muse_spark_logits_local.py models/test_muse_spark_rocm_shared_rope.py + models/test_muse_spark_rocm_mono.py mirror: amd: label: ":amd: (MI355) Basic Models (Other)" @@ -110,6 +114,7 @@ steps: - vllm/models/deepseek_v4/ - vllm/models/deepseek_v41/ - tests/models/test_deepseek_v4_vl_rocm.py + - tests/models/test_deepseek_v4_vl.py - tests/models/test_hyv4_rocm.py - tests/models/transformers/test_layer_registry_e2e.py - tests/models/test_deepseek_v41_replay_start.py @@ -122,11 +127,11 @@ steps: - tests/models/test_dsv41_mono_decode.py - tests/models/test_muse_spark_rocm_loading.py - tests/models/test_dsv4_mono_decode.py - - tests/models/test_deepseek_v4_vl_stream_weights.py - tests/models/hy_v4/test_triton_ihc.py - vllm/models/muse_spark/ - tests/models/test_muse_spark_logits_local.py - tests/models/test_muse_spark_rocm_shared_rope.py + - tests/models/test_muse_spark_rocm_mono.py commands: - >- pytest -v -s @@ -134,6 +139,7 @@ steps: models/transformers/test_backend.py models/test_registry.py models/test_deepseek_v4_vl_rocm.py + models/test_deepseek_v4_vl.py models/transformers/test_layer_registry_e2e.py models/test_deepseek_v41_replay_start.py models/test_deepseek_v41_decoder_replay_layers.py @@ -144,15 +150,16 @@ steps: models/test_deepseek_v4_rocm_compressor_gemm_fusion.py models/test_dsv41_mono_decode.py models/test_muse_spark_rocm_loading.py - models/test_deepseek_v4_vl_stream_weights.py models/test_muse_spark_logits_local.py models/test_muse_spark_rocm_shared_rope.py + models/test_muse_spark_rocm_mono.py - VLLM_ROCM_USE_AITER=1 pytest -v -s models/test_hyv4_rocm.py -m 'not distributed' - pytest -v -s models/hy_v4/test_triton_ihc.py - pytest -v -s models/test_dsv4_mono_decode.py - label: ":amd: (MI355) MiniMax M3 MonoKernel" key: minimax-m3-monokernel-mi355 + automatic_shard: true dind: false timeout_in_minutes: 45 device: mi355_4 @@ -171,6 +178,7 @@ steps: - label: ":nvidia: (B200) Inkling" key: inkling-unit-tests-b200 + automatic_shard: true timeout_in_minutes: 40 device: b200-k8s source_file_dependencies: @@ -183,6 +191,7 @@ steps: - pytest -v -s models/inkling mirror: amd: + automatic_shard: true label: ":amd: (MI355) Inkling" dind: false device: mi355_1 @@ -213,6 +222,7 @@ steps: - label: ":nvidia: (B200) Kimi K3" key: kimi-k3-unit-tests-b200 + automatic_shard: true timeout_in_minutes: 40 device: b200-k8s source_file_dependencies: @@ -229,6 +239,7 @@ steps: kernels/attention/test_kimi_k3_mla_fused_epilogue.py mirror: amd: + automatic_shard: true label: ":amd: (MI355) Kimi K3" dind: false device: mi355_1 @@ -287,16 +298,47 @@ steps: - label: ":nvidia: (H200) Step5" key: step5-unit-tests + automatic_shard: true timeout_in_minutes: 20 device: h200_35gb source_file_dependencies: - vllm/models/step5/ - tests/models/step5/ + - vllm/model_executor/warmup/step5_dsa_warmup.py commands: - pytest -v -s models/step5 + mirror: + amd: + label: ":amd: (MI355 DPX) Step5" + dind: false + device: mi355_dpx + timeout_in_minutes: 30 + working_dir: "/vllm-workspace/tests" + depends_on: + - image-build-amd + source_file_dependencies: + - vllm/models/step5/ + - tests/models/step5/ + - vllm/model_executor/models/step3p7.py + - vllm/model_executor/models/step3_vl.py + - vllm/model_executor/models/step_vl.py + - vllm/config/compilation.py + - vllm/model_executor/layers/indexer_topk.py + - vllm/transformers_utils/configs/step5.py + - vllm/model_executor/warmup/step5_dsa_warmup.py + - vllm/v1/attention/backends/rocm_aiter_unified_attn.py + - vllm/triton_utils/ + - vllm/platforms/rocm.py + - tests/v1/attention/utils.py + - tests/models/test_initialization.py + - tests/models/registry.py + commands: + - pytest -v -s models/step5 + - pytest -v -s models/test_initialization.py -k Step5ForConditionalGeneration - label: ":nvidia: (H200) GLM5Next" key: glm5next-unit-tests + automatic_shard: true timeout_in_minutes: 20 device: h200_35gb source_file_dependencies: @@ -307,6 +349,7 @@ steps: - pytest -v -s models/glm5next mirror: amd: + automatic_shard: true label: ":amd: (MI355 DPX) GLM5Next" dind: false device: mi355_dpx @@ -319,6 +362,7 @@ steps: - label: ":nvidia: (H200) Qwen4 Exp" key: qwen4-exp-unit-tests + automatic_shard: true timeout_in_minutes: 40 device: h200_35gb source_file_dependencies: @@ -336,6 +380,7 @@ steps: - pytest -v -s models/qwen4_exp/test_ple.py mirror: amd: + automatic_shard: true label: ":amd: (MI355 DPX) Qwen4 Exp" dind: false device: mi355_dpx @@ -349,6 +394,26 @@ steps: # PLE and QSA import conflicting HC registrations in a shared process. - pytest -v -s models/qwen4_exp/test_ple.py +- label: ":amd: (MI300) Qwen4 Exp" + key: qwen4-exp-unit-tests-mi300 + automatic_shard: true + timeout_in_minutes: 40 + dind: false + device: mi300_1 + working_dir: "/vllm-workspace/tests" + depends_on: + - image-build-amd + source_file_dependencies: + - vllm/models/qwen4_exp/ + - vllm/config/compilation.py + - vllm/v1/attention/backends/short_conv_attn.py + - vllm/model_executor/layers/mamba/checkpoint.py + - tests/models/qwen4_exp/ + commands: + - pytest -v -s models/qwen4_exp/test_qsa_amd.py models/qwen4_exp/test_qsa_prepare_amd.py + # PLE and QSA import conflicting HC registrations in a shared process. + - pytest -v -s models/qwen4_exp/test_ple.py + - label: ":computer: (CPU) Qwen4 Exp" key: qwen4-exp-unit-tests-cpu depends_on: @@ -366,6 +431,20 @@ steps: models/qwen4_exp/test_config.py models/qwen4_exp/test_ple.py +- label: ":nvidia: (B200) Qwen3.5" + key: qwen3-5-unit-tests-b200 + timeout_in_minutes: 20 + device: b200-k8s + source_file_dependencies: + - vllm/model_executor/layers/linear.py + - vllm/model_executor/layers/mamba/gdn + - vllm/model_executor/layers/utils.py + - vllm/models/qwen3_5/ + - vllm/model_executor/models/qwen3_next.py + - tests/models/qwen3_5/ + commands: + - pytest -v -s models/qwen3_5/ + - label: ":computer: (CPU) Basic Models Other" key: basic-models-test-other-cpu depends_on: @@ -378,6 +457,7 @@ steps: - tests/models/test_vision.py - tests/models/test_adapters.py - tests/models/test_qwen3_5_mtp_config.py + - tests/models/test_qwen3_5.py - tests/models/test_dory.py - tests/models/test_alice_ai.py - tests/models/test_alice_ai_config.py @@ -388,6 +468,7 @@ steps: - tests/model_executor/layers/test_activation.py - tests/models/test_language_model_cache_is_weak.py - tests/models/deepseek_v32/test_sequence_parallel.py + - tests/models/deepseek_v32/test_pp_topk.py - tests/models/test_deepseek_v4_fi_moe_ep.py - tests/models/test_dspark_mla.py - tests/models/transformers/test_layer_registry.py @@ -400,11 +481,13 @@ steps: models/test_vision.py models/test_adapters.py models/test_qwen3_5_mtp_config.py + models/test_qwen3_5.py models/transformers/fusers/ models/transformers/test_dynamic_rope.py model_executor/layers/test_activation.py models/test_language_model_cache_is_weak.py models/deepseek_v32/test_sequence_parallel.py + models/deepseek_v32/test_pp_topk.py models/test_deepseek_v4_fi_moe_ep.py models/test_dspark_mla.py models/transformers/test_layer_registry.py @@ -417,6 +500,7 @@ steps: - label: ':amd: (MI355) DeepSeek-V4.1 Mono Decode Numerics' key: dsv41-mono-decode-numerics-amd + automatic_shard: true dind: false device: mi355_4 num_devices: 4 @@ -434,8 +518,33 @@ steps: - pytest -v -s tests/models/test_dsv41_mono_numerics.py - pytest -v -s tests/models/test_dsv41_mono_layers.py +- label: ':amd: (MI355) DeepSeek-V4.1 Mono Decode Numerics TP8' + key: dsv41-mono-decode-numerics-tp8-amd + automatic_shard: true + dind: false + device: mi355_8 + num_devices: 8 + timeout_in_minutes: 60 + working_dir: /vllm-workspace/ + depends_on: + - image-build-amd + source_file_dependencies: + - docker/Dockerfile.rocm_base + - vllm/models/deepseek_v41/amd/ + - vllm/models/deepseek_v41/nvidia/model_state.py + - tests/models/test_dsv41_mono_numerics.py + - tests/models/test_dsv41_mono_layers.py + commands: + # only the TP8 cases; the 4-GPU step above runs TP2/TP4 (TP8 skips there) + - pytest -v -s "tests/models/test_dsv41_mono_numerics.py::test_mono_decode_layer_matches_vllm[8]" + - >- + pytest -v -s + "tests/models/test_dsv41_mono_layers.py::test_mono_decode_matches_the_model[8-8]" + "tests/models/test_dsv41_mono_layers.py::test_mono_decode_matches_the_model[8-28]" + - label: ':amd: (MI355) DeepSeek-V4-Pro Mono Decode Numerics' key: dsv4-mono-decode-numerics-amd + automatic_shard: true dind: false device: mi355_8 num_devices: 8 @@ -454,6 +563,7 @@ steps: - label: ':amd: (MI300) DeepSeek-V4.1 gfx942 Mono Decode Layers' key: dsv41-mono-decode-layers-gfx942-amd + automatic_shard: true dind: false device: mi300_4 num_devices: 4 @@ -498,6 +608,7 @@ steps: - label: ':amd: (MI355) Kimi-K3 Specialized Decode' key: kimi-k3-specialized-decode-amd + automatic_shard: true dind: false device: mi355_8 num_devices: 8 diff --git a/.buildkite/test_areas/models_distributed.yaml b/.buildkite/test_areas/models_distributed.yaml index bdaef9ec5a1b..22e38ab1aa24 100644 --- a/.buildkite/test_areas/models_distributed.yaml +++ b/.buildkite/test_areas/models_distributed.yaml @@ -4,6 +4,7 @@ depends_on: steps: - label: ":nvidia: (L4) [Nightly] Qwen4 Exp SP" key: qwen4-exp-sp-tests-4-gpus + automatic_shard: true # Nightly only: 4-GPU L4 pods wait behind smaller jobs on per-PR runs. optional: true timeout_in_minutes: 30 @@ -22,6 +23,7 @@ steps: - pytest -v -s models/qwen4_exp/test_tpsp.py mirror: amd: + automatic_shard: true # Run on PRs even when the L4 step is optional (nightly only). optional: false label: ":amd: (MI355) Qwen4 Exp SP" diff --git a/.buildkite/test_areas/models_language.yaml b/.buildkite/test_areas/models_language.yaml index 909bb30e5e0f..a157d0ae76f8 100644 --- a/.buildkite/test_areas/models_language.yaml +++ b/.buildkite/test_areas/models_language.yaml @@ -4,6 +4,7 @@ depends_on: steps: - label: ":nvidia: (H200 MIG 18GB) Language Models (Standard)" key: language-models-tests-standard + automatic_shard: true timeout_in_minutes: 30 device: h200_18gb source_file_dependencies: @@ -11,12 +12,14 @@ steps: - "!vllm/distributed/kv_transfer/" - tests/models/language commands: - # Test standard language models, excluding a subset of slow tests + # Test standard language models, excluding a subset of slow tests. + # The exclusion sits in -k so the shard planner reads -m as picking tests. - pip freeze | grep -E 'torch' - - pytest -v -s models/language -m 'core_model and (not slow_test)' + - pytest -v -s models/language -m core_model -k 'not slow_test' mirror: amd: label: ":amd: (MI355 DPX) Language Models (Standard)" + automatic_shard: true dind: false device: mi355_dpx timeout_in_minutes: 60 @@ -90,8 +93,6 @@ steps: depends_on: - image-build-amd commands: - - MAMBA_FORCE_BUILD=TRUE uv pip install --system --no-build-isolation 'git+https://github.com/AndreasKaratzas/mamba@fix-rocm-7.0-warp-size-constexpr' - - CAUSAL_CONV1D_FORCE_BUILD=TRUE uv pip install --system --no-build-isolation 'git+https://github.com/Dao-AILab/causal-conv1d@v1.6.0' - pytest -v -s models/language/generation -m hybrid_model -k 'not granite-4.0-tiny-preview' --num-shards=$$BUILDKITE_PARALLEL_JOB_COUNT --shard-id=$$BUILDKITE_PARALLEL_JOB # Granite 4 hybrid generation is sensitive to hardware-specific Triton SSD @@ -127,11 +128,6 @@ steps: - "!vllm/distributed/kv_transfer/" - tests/models/language/generation commands: - - export GIT_CONFIG_COUNT=1 - - export GIT_CONFIG_KEY_0=http.version - - export GIT_CONFIG_VALUE_0=HTTP/1.1 - - MAMBA_FORCE_BUILD=TRUE uv pip install --system --no-build-isolation 'git+https://github.com/AndreasKaratzas/mamba@fix-rocm-7.0-warp-size-constexpr' - - CAUSAL_CONV1D_FORCE_BUILD=TRUE uv pip install --system --no-build-isolation 'git+https://github.com/Dao-AILab/causal-conv1d@v1.6.0' - pytest -v -s models/language/generation -m hybrid_model -k 'granite-4.0-tiny-preview' - label: ":nvidia: (H200 MIG 35GB) [Nightly] Language Models (Extended Generation)" @@ -159,12 +155,11 @@ steps: depends_on: - image-build-amd commands: - - MAMBA_FORCE_BUILD=TRUE uv pip install --system --no-build-isolation 'git+https://github.com/AndreasKaratzas/mamba@fix-rocm-7.0-warp-size-constexpr' - - CAUSAL_CONV1D_FORCE_BUILD=TRUE uv pip install --system --no-build-isolation 'git+https://github.com/Dao-AILab/causal-conv1d@v1.6.0' - pytest -v -s models/language/generation -m '(not core_model) and (not hybrid_model)' - label: ":nvidia: (H200 MIG 18GB) [Nightly] Language Models (PPL)" key: language-models-test-ppl + automatic_shard: true timeout_in_minutes: 30 device: h200_18gb optional: true @@ -178,11 +173,13 @@ steps: amd: label: ":amd: (MI250) Language Models (PPL)" dind: false + automatic_shard: true device: mi250_1 timeout_in_minutes: 65 depends_on: - image-build-amd source_file_dependencies: + - vllm/models/qwen3_5/ - vllm/model_executor/models/qwen3_5.py - vllm/model_executor/models/qwen3_5_mtp.py - vllm/model_executor/models/qwen2.py @@ -217,10 +214,10 @@ steps: - pytest -v -s models/language/pooling -m 'not core_model' --timeout=900 --timeout-method=thread mirror: amd: - parallelism: 4 + automatic_shard: true commands: - - "pytest -v -s models/language/pooling -m 'not core_model' --num-shards=$$BUILDKITE_PARALLEL_JOB_COUNT --shard-id=$$BUILDKITE_PARALLEL_JOB" - label: ":amd: (MI355 DPX) Language Models (Extended Pooling) Shard %N" + - "pytest -v -s models/language/pooling -m 'not core_model'" + label: ":amd: (MI355 DPX) Language Models (Extended Pooling)" dind: false device: mi355_dpx timeout_in_minutes: 95 diff --git a/.buildkite/test_areas/models_multimodal.yaml b/.buildkite/test_areas/models_multimodal.yaml index 51cc3ef602ed..c47651641055 100644 --- a/.buildkite/test_areas/models_multimodal.yaml +++ b/.buildkite/test_areas/models_multimodal.yaml @@ -4,6 +4,7 @@ depends_on: steps: - label: ":nvidia: (H200 MIG 18GB) Multimodal Models (Standard) 1: qwen2" key: multi-modal-models-standard-1-qwen2 + automatic_shard: true timeout_in_minutes: 68 device: h200_18gb source_file_dependencies: @@ -17,6 +18,7 @@ steps: amd: label: ":amd: (MI355 DPX) Multimodal Models (Standard) 1: qwen2" dind: false + automatic_shard: true device: mi355_dpx timeout_in_minutes: 65 depends_on: @@ -38,6 +40,7 @@ steps: mirror: amd: label: ":amd: (MI355 DPX) Multimodal Models (Standard) 2: qwen3 + gemma" + automatic_shard: true dind: false device: mi355_dpx timeout_in_minutes: 55 @@ -47,6 +50,7 @@ steps: - label: ":nvidia: (H200 MIG 35GB) Multimodal Models (Standard) 3: llava + qwen2_vl" device: h200_35gb key: multi-modal-models-standard-3-llava-qwen2-vl + automatic_shard: true timeout_in_minutes: 40 source_file_dependencies: - vllm/ @@ -57,6 +61,7 @@ steps: - pytest -v -s models/multimodal/generation/test_qwen2_vl.py -m core_model mirror: amd: + automatic_shard: true label: ":amd: (MI250) Multimodal Models (Standard) 3: llava + qwen2_vl" dind: false device: mi250_1 @@ -144,6 +149,7 @@ steps: - label: ":nvidia: (H200 MIG 35GB) Multimodal Accuracy Eval (Small Models)" device: h200_35gb key: multi-modal-accuracy-eval-small-models + automatic_shard: true timeout_in_minutes: 30 working_dir: "/vllm-workspace/.buildkite/lm-eval-harness" source_file_dependencies: @@ -154,6 +160,7 @@ steps: - pytest -s -v test_lm_eval_correctness.py --config-list-file=configs/models-mm-small.txt --tp-size=1 mirror: amd: + automatic_shard: true label: ":amd: (MI355 DPX) Multimodal Accuracy Eval (Small Models)" dind: false device: mi355_dpx @@ -247,6 +254,7 @@ steps: amd: label: ":amd: (MI355 DPX) Multimodal Models (Extended Generation 3)" dind: false + automatic_shard: true device: mi355_dpx timeout_in_minutes: 90 depends_on: diff --git a/.buildkite/test_areas/pytorch.yaml b/.buildkite/test_areas/pytorch.yaml index 2d16e162e457..33638dd04f27 100644 --- a/.buildkite/test_areas/pytorch.yaml +++ b/.buildkite/test_areas/pytorch.yaml @@ -57,12 +57,14 @@ steps: - label: ":nvidia: (H200 MIG 35GB) PyTorch Compilation / Dynamic Shapes" device: h200_35gb key: pytorch-compilation-dynamic-shapes + automatic_shard: true timeout_in_minutes: 30 source_file_dependencies: *pytorch-compilation-deps commands: - pytest -s -v compile/dynamic_shapes/ mirror: amd: + automatic_shard: true label: ":amd: (MI300) PyTorch Compilation / Dynamic Shapes" dind: false device: mi300_1 @@ -75,6 +77,7 @@ steps: - label: ":nvidia: (H200 MIG 18GB) PyTorch Compilation H100 Cases" key: pytorch-compilation-unit-tests-h100 + automatic_shard: true timeout_in_minutes: 30 device: h200_18gb source_file_dependencies: @@ -108,6 +111,7 @@ steps: - "find compile/h100/ -name 'test_*.py' -print0 | xargs -0 -n1 -I{} pytest -s -v '{}'" mirror: amd: + automatic_shard: true label: ":amd: (MI355 DPX) PyTorch Compilation H100 Cases" dind: false device: mi355_dpx @@ -182,6 +186,7 @@ steps: - label: ":nvidia: (H200 MIG 35GB) PyTorch Fullgraph" device: h200_35gb key: pytorch-fullgraph-test + automatic_shard: true timeout_in_minutes: 90 source_file_dependencies: - vllm/__init__.py @@ -212,13 +217,11 @@ steps: - tests/compile - csrc/ commands: - # Run smoke tests under fullgraph directory, except test_full_cudagraph.py - # as it is a heavy test that is covered in other steps. - # Use `find` to launch multiple instances of pytest so that - # they do not suffer from https://github.com/vllm-project/vllm/issues/28965 - - "find compile/fullgraph/ -name 'test_*.py' -not -name 'test_full_cudagraph.py' -print0 | xargs -0 -n1 -I{} pytest -s -v '{}'" + # The heavy cudagraph test is covered in other steps. + - pytest -v -s compile/fullgraph --ignore=compile/fullgraph/test_full_cudagraph.py mirror: amd: + automatic_shard: true label: ":amd: (MI355 DPX) PyTorch Fullgraph" dind: false device: mi355_dpx @@ -235,6 +238,7 @@ steps: # those SM90 cases skip while the architecture-compatible cases still run. - label: ":nvidia: (L4) PyTorch Fullgraph CUDAGraph Compatibility" key: pytorch-fullgraph-cudagraph-l4-compatibility + automatic_shard: true device: l4 num_devices: 1 timeout_in_minutes: 60 @@ -269,6 +273,7 @@ steps: - pytest -s -v compile/fullgraph/test_full_cudagraph.py mirror: amd: + automatic_shard: true label: ':amd: (MI250) PyTorch Fullgraph CUDAGraph Compatibility' dind: false device: mi250_1 @@ -289,6 +294,7 @@ steps: - label: ":nvidia: (H200 MIG 18GB) PyTorch Nightly Dependency Override Check" key: pytorch-nightly-dependency-override-check + automatic_shard: true # if this test fails, it means the nightly torch version is not compatible with some # of the dependencies. Please check the error message and add the package to whitelist # in /vllm/tools/pre_commit/generate_nightly_torch_test.py @@ -300,6 +306,7 @@ steps: - bash standalone_tests/pytorch_nightly_dependency.sh mirror: amd: + automatic_shard: true label: ":amd: (MI300) PyTorch Nightly Dependency Override Check" dind: false device: mi300_1 diff --git a/.buildkite/test_areas/quantization.yaml b/.buildkite/test_areas/quantization.yaml index 1d066c21f0a2..9349c767be9f 100644 --- a/.buildkite/test_areas/quantization.yaml +++ b/.buildkite/test_areas/quantization.yaml @@ -56,6 +56,7 @@ steps: - label: ":nvidia: (H200 MIG 35GB) Quantized Fusions" device: h200_35gb key: quantized-fusions + automatic_shard: true timeout_in_minutes: 20 source_file_dependencies: - tests/fusion @@ -67,6 +68,7 @@ steps: - pytest -v -s fusion/ mirror: amd: + automatic_shard: true label: ":amd: (MI300) Quantized Fusions" dind: false device: mi300_1 @@ -90,6 +92,9 @@ steps: device: b200-k8s source_file_dependencies: - tests/quantization/test_blackwell_moe.py + - tests/benchmarks/test_forward_cli.py + - vllm/benchmarks/forward/ + - vllm/models/muse_spark/ - vllm/model_executor/models/deepseek_v2.py - vllm/model_executor/models/gpt_oss.py - vllm/model_executor/models/llama4.py @@ -99,9 +104,13 @@ steps: - vllm/model_executor/layers/quantization/mxfp4.py - vllm/v1/attention/backends/flashinfer.py commands: - - pytest -s -v tests/quantization/test_blackwell_moe.py + - >- + pytest -v -s + tests/quantization/test_blackwell_moe.py + tests/benchmarks/test_forward_cli.py::test_bench_forward_muse_balanced_routing mirror: amd: + automatic_shard: true label: ":amd: (MI355) Quantized MoE" dind: false device: mi355_1 diff --git a/.buildkite/test_areas/rust_frontend.yaml b/.buildkite/test_areas/rust_frontend.yaml index 4e978d63d0b1..043429033932 100644 --- a/.buildkite/test_areas/rust_frontend.yaml +++ b/.buildkite/test_areas/rust_frontend.yaml @@ -156,6 +156,7 @@ steps: - label: ":nvidia: (H200 MIG 18GB) Rust Frontend Core Correctness" key: rust-frontend-core-correctness + automatic_shard: true timeout_in_minutes: 20 device: h200_18gb working_dir: "/vllm-workspace/tests" @@ -170,6 +171,7 @@ steps: - pytest -s entrypoints/openai/correctness/test_lmeval.py::test_lm_eval_accuracy_v1_engine mirror: amd: + automatic_shard: true label: ":amd: (MI355 DPX) Rust Frontend Core Correctness" dind: false device: mi355_dpx @@ -185,6 +187,7 @@ steps: - label: ":nvidia: (H200 MIG 35GB) Rust Frontend Tool Use" key: rust-frontend-tool-use + automatic_shard: true device: h200_35gb timeout_in_minutes: 35 working_dir: "/vllm-workspace/tests" @@ -213,6 +216,7 @@ steps: - pytest -v -s entrypoints/tool_parsers/test_hermes_tool_parser.py -k granite4 mirror: amd: + automatic_shard: true label: ":amd: (MI355 DPX) Rust Frontend Tool Use" dind: false device: mi355_dpx diff --git a/.buildkite/test_areas/samplers.yaml b/.buildkite/test_areas/samplers.yaml index 9d66e785ea8a..68055d6a0fd7 100644 --- a/.buildkite/test_areas/samplers.yaml +++ b/.buildkite/test_areas/samplers.yaml @@ -5,6 +5,7 @@ steps: - label: ":nvidia: (H200 MIG 18GB) Samplers" device: h200_18gb key: samplers-test + automatic_shard: true timeout_in_minutes: 60 source_file_dependencies: - vllm/model_executor/layers @@ -19,6 +20,7 @@ steps: - VLLM_USE_FLASHINFER_SAMPLER=1 pytest -v -s samplers -k "not test_beam_search_passes_multimodal_data" mirror: amd: + automatic_shard: true label: ":amd: (MI250) Samplers" dind: false device: mi250_1 @@ -41,6 +43,7 @@ steps: - label: ":nvidia: (H200 MIG 35GB) Samplers Multimodal Beam Search" device: h200_35gb key: samplers-multimodal-beam-search + automatic_shard: true timeout_in_minutes: 30 source_file_dependencies: - vllm/model_executor/layers @@ -52,6 +55,7 @@ steps: - VLLM_USE_FLASHINFER_SAMPLER=1 pytest -v -s samplers/test_beam_search.py::test_beam_search_passes_multimodal_data mirror: amd: + automatic_shard: true label: ":amd: (MI355 DPX) Samplers Multimodal Beam Search" dind: false device: mi355_dpx diff --git a/.buildkite/test_areas/spec_decode.yaml b/.buildkite/test_areas/spec_decode.yaml index 61b6677ae586..32d377ca5efa 100644 --- a/.buildkite/test_areas/spec_decode.yaml +++ b/.buildkite/test_areas/spec_decode.yaml @@ -36,6 +36,7 @@ steps: - label: ":nvidia: (H200 MIG 35GB) Spec Decode Eagle 1: DeepSeek + Qwen" key: spec-decode-eagle-1-deepseek-qwen + automatic_shard: true timeout_in_minutes: 25 device: h200_35gb source_file_dependencies: @@ -46,6 +47,7 @@ steps: - pytest -v -s v1/e2e/spec_decode/eagle/ -k "deepseek_eagle or qwen3_eagle3" mirror: amd: + automatic_shard: true label: ":amd: (MI355 DPX) Spec Decode Eagle 1: DeepSeek + Qwen" dind: false device: mi355_dpx @@ -63,6 +65,7 @@ steps: - label: ":nvidia: (H200 MIG 35GB) Spec Decode Eagle 2: Llama 3 + Qwen VL + Other" key: spec-decode-eagle-2-llama3-qwen-vl-other + automatic_shard: true timeout_in_minutes: 25 device: h200_35gb source_file_dependencies: @@ -73,6 +76,7 @@ steps: - pytest -v -s v1/e2e/spec_decode/eagle/ -k "not deepseek_eagle and not qwen3_eagle3" mirror: amd: + automatic_shard: true label: ":amd: (MI355 DPX) Spec Decode Eagle 2: Llama 3 + Qwen VL + Other" dind: false device: mi355_dpx @@ -127,6 +131,7 @@ steps: # each job below runs whole directories. - label: ":nvidia: (H200 MIG 35GB) Spec Decode Speculators" key: spec-decode-speculators + automatic_shard: true timeout_in_minutes: 30 device: h200_35gb source_file_dependencies: @@ -139,6 +144,7 @@ steps: - pytest -v -s v1/e2e/spec_decode/speculators/ mirror: amd: + automatic_shard: true label: ":amd: (MI355 DPX) Spec Decode Speculators" dind: false device: mi355_dpx @@ -157,6 +163,7 @@ steps: - label: ":nvidia: (H200 MIG 35GB) Spec Decode MTP Gemma4" key: spec-decode-mtp-gemma4 + automatic_shard: true timeout_in_minutes: 30 device: h200_35gb source_file_dependencies: @@ -169,6 +176,7 @@ steps: - pytest -v -s v1/e2e/spec_decode/mtp/gemma4/ mirror: amd: + automatic_shard: true label: ":amd: (MI355 DPX) Spec Decode MTP Gemma4" dind: false device: mi355_dpx @@ -187,6 +195,7 @@ steps: - label: ":nvidia: (H200 MIG 35GB) Spec Decode MTP DeepSeek + MiMo" key: spec-decode-mtp-deepseek-mimo + automatic_shard: true timeout_in_minutes: 30 device: h200_35gb source_file_dependencies: @@ -200,6 +209,7 @@ steps: - pytest -v -s v1/e2e/spec_decode/mtp/mimo/ mirror: amd: + automatic_shard: true label: ":amd: (MI355 DPX) Spec Decode MTP DeepSeek + MiMo" dind: false device: mi355_dpx @@ -218,6 +228,7 @@ steps: - label: ":nvidia: (H200 MIG 35GB) Spec Decode MTP Qwen3.5" key: spec-decode-mtp-qwen3-5 + automatic_shard: true timeout_in_minutes: 30 device: h200_35gb source_file_dependencies: @@ -225,11 +236,16 @@ steps: - vllm/v1/worker/gpu/spec_decode/ - vllm/v1/attention/backends/ - vllm/transformers_utils/configs/speculators/ + - vllm/model_executor/models/qwen3_5_mtp.py + - vllm/models/qwen3_5/nvidia/mtp.py + - vllm/model_executor/models/qwen3_next_mtp.py + - vllm/config/speculative.py - tests/v1/e2e/spec_decode/ commands: - pytest -v -s v1/e2e/spec_decode/mtp/qwen3_5/ mirror: amd: + automatic_shard: true label: ":amd: (MI355 DPX) Spec Decode MTP Qwen3.5" dind: false device: mi355_dpx @@ -243,6 +259,9 @@ steps: - vllm/v1/sample/ - vllm/model_executor/layers/ - vllm/transformers_utils/configs/speculators/ + - vllm/model_executor/models/qwen3_5_mtp.py + - vllm/model_executor/models/qwen3_next_mtp.py + - vllm/config/speculative.py - tests/v1/e2e/spec_decode/ - vllm/platforms/rocm.py @@ -286,6 +305,7 @@ steps: - label: ":nvidia: (H200 MIG 35GB) Spec Decode N-Gram + Suffix" key: spec-decode-ngram-suffix + automatic_shard: true timeout_in_minutes: 20 device: h200_35gb source_file_dependencies: @@ -298,6 +318,7 @@ steps: - python3 spec_decode/test_custom_proposer.py mirror: amd: + automatic_shard: true label: ":amd: (MI355 DPX) Spec Decode N-Gram + Suffix" dind: false device: mi355_dpx @@ -352,8 +373,11 @@ steps: - vllm/v1/spec_decode/ - vllm/v1/worker/gpu/spec_decode/ - tests/v1/e2e/spec_decode/ + - vllm/model_executor/models/qwen3_dflash.py + - tests/models/test_qwen3_dflash_mxfp8_context_kv.py commands: - pytest -v -s v1/e2e/spec_decode/draft_model/ + - pytest -v -s models/test_qwen3_dflash_mxfp8_context_kv.py mirror: amd: label: ":amd: (MI355) Spec Decode Draft Model Nightly" @@ -378,6 +402,7 @@ steps: - label: ":nvidia: (H100) [Nightly] Speculators Correctness" key: speculators-correctness + automatic_shard: true timeout_in_minutes: 30 device: h100 optional: true @@ -392,6 +417,7 @@ steps: - pytest -v -s v1/spec_decode/test_speculators_correctness.py -m slow_test mirror: amd: + automatic_shard: true label: ":amd: (MI355 DPX) Speculators Correctness Nightly" dind: false device: mi355_dpx @@ -404,6 +430,7 @@ steps: - label: ":nvidia: (B200) [Nightly] Spec Decode DeepSeek MTP Parallel Load" key: spec-decode-deepseek-mtp-parallel-load-2xb200-2xmi300 + automatic_shard: true timeout_in_minutes: 30 device: b200-k8s optional: true @@ -419,6 +446,7 @@ steps: - pytest -v -s v1/e2e/spec_decode/test_mtp_parallel_load.py mirror: amd: + automatic_shard: true label: ":amd: (MI355) Spec Decode DeepSeek MTP Parallel Load" dind: false device: mi355_2 @@ -450,12 +478,15 @@ steps: - vllm/v1/worker/gpu/spec_decode/ - vllm/model_executor/models/qwen3_dflash.py - vllm/model_executor/models/qwen3_dflash2.py + - vllm/models/qwen3_5/ - vllm/model_executor/models/qwen3_5.py + - vllm/model_executor/models/qwen3_5_mtp.py - vllm/model_executor/models/laguna_dflash.py - tests/v1/e2e/spec_decode/ commands: # DFlash2 NVFP4 (27B) does not fit a 35GB MIG slice; runs on B200 below. - pytest -v -s v1/e2e/spec_decode/acceptance_rates/dflash/ -k "not dflash2" + - pytest -v -s v1/e2e/spec_decode/test_distribution_shift.py mirror: amd: label: ":amd: (MI355 DPX) Spec Decode AL DFlash Nightly" @@ -465,6 +496,7 @@ steps: timeout_in_minutes: 120 commands: - pytest -v -s v1/e2e/spec_decode/acceptance_rates/dflash/ -k "not dflash2" + - pytest -v -s v1/e2e/spec_decode/test_distribution_shift.py depends_on: - image-build-amd source_file_dependencies: @@ -484,6 +516,7 @@ steps: - label: ":nvidia: (B200) [Nightly] Spec Decode AL DFlash2" key: spec-decode-dflash2-nightly-b200 + automatic_shard: true timeout_in_minutes: 60 device: b200-k8s optional: true @@ -492,12 +525,15 @@ steps: - vllm/v1/worker/gpu/spec_decode/ - vllm/model_executor/models/qwen3_dflash.py - vllm/model_executor/models/qwen3_dflash2.py + - vllm/models/qwen3_5/ - vllm/model_executor/models/qwen3_5.py + - vllm/model_executor/models/qwen3_5_mtp.py - tests/v1/e2e/spec_decode/ commands: - pytest -v -s v1/e2e/spec_decode/acceptance_rates/dflash/ -k "dflash2" mirror: amd: + automatic_shard: true label: ":amd: (MI355) Spec Decode AL DFlash2 Nightly" dind: false device: mi355_1 @@ -517,7 +553,9 @@ steps: - vllm/v1/attention/ - vllm/model_executor/models/qwen3_dflash.py - vllm/model_executor/models/qwen3_dflash2.py + - vllm/models/qwen3_5/ - vllm/model_executor/models/qwen3_5.py + - vllm/model_executor/models/qwen3_5_mtp.py - vllm/model_executor/model_loader/ - vllm/model_executor/layers/quantization/ - vllm/model_executor/kernels/linear/ @@ -525,6 +563,7 @@ steps: - label: ":nvidia: (H100) [Nightly] Spec Decode AL XPress" key: spec-decode-xpress-nightly + automatic_shard: true timeout_in_minutes: 30 device: h100 optional: true diff --git a/.buildkite/test_areas/torch_abi.yaml b/.buildkite/test_areas/torch_abi.yaml index fb2ff09cdbf4..4c38ba0ff2e3 100644 --- a/.buildkite/test_areas/torch_abi.yaml +++ b/.buildkite/test_areas/torch_abi.yaml @@ -4,6 +4,7 @@ depends_on: steps: - label: ":nvidia: (L4) Torch Stable ABI Audit" key: torch-stable-abi-audit + automatic_shard: true timeout_in_minutes: 10 source_file_dependencies: - .buildkite/check-torch-abi.py diff --git a/.dockerignore b/.dockerignore index e0284129eefa..7ed1895898df 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,8 +1,9 @@ /.venv /build -/artifacts/vllm-rocm-install/ -/bake-config-build-*.json -/wheel-export/ +/.rocm-version/ +/.rocm-wheel/ +/.rocm-kernel-map/ +/.rocm-src/ dist vllm/*.so vllm/vllm-rs diff --git a/.github/mergify.yml b/.github/mergify.yml index 0f545a4a0784..7ece42cdecfe 100644 --- a/.github/mergify.yml +++ b/.github/mergify.yml @@ -752,6 +752,28 @@ pull_request_rules: - "stmatengss" - "LCAIZJ" +- name: label-nixl + description: Automatically apply nixl label + conditions: + - label != stale + - or: + # Any NIXL-named file: KV connector and shared utils, NIXL EP + # dispatch, EC-transfer NIXL data, the offload p2p data layer, + # tests, docs, examples. + - files~=(?i)nixl + # KV offloading paths (multi-tiered KVCR tiering and simple), plus + # their tests and docs. + - files~=kv_offload + - title~=(?i)nixl + actions: + label: + add: + - nixl + assign: + users: + - "ovidiusm" + - "iyastreb" + - name: label-scheduler description: Automatically apply scheduler label conditions: diff --git a/.github/workflows/buf.yml b/.github/workflows/buf.yml index dd695d28d529..15c7ce0e1179 100644 --- a/.github/workflows/buf.yml +++ b/.github/workflows/buf.yml @@ -17,8 +17,8 @@ jobs: proto: runs-on: ubuntu-latest steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - - uses: bufbuild/buf-action@8c6a16e16f12ba20b6470afa9c2ba9b5ba8c97c3 # v1.5.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: bufbuild/buf-action@85aebf73123b5c15fd5528aaecbf9129cddf7fa7 # v1.6.0 with: version: 1.72.0 input: rust/proto diff --git a/.github/workflows/issue_autolabel.yml b/.github/workflows/issue_autolabel.yml index 43a458df7678..03eeb71a5bda 100644 --- a/.github/workflows/issue_autolabel.yml +++ b/.github/workflows/issue_autolabel.yml @@ -15,7 +15,7 @@ jobs: steps: - name: Label issues based on keywords id: label-step - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | // Configuration: Add new labels and keywords here @@ -633,7 +633,7 @@ jobs: - name: CC users for labeled issues if: steps.label-step.outputs.labels_added != '[]' - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | // Configuration: Map labels to GitHub users to CC @@ -714,7 +714,7 @@ jobs: - name: Request missing ROCm info from issue author if: contains(steps.label-step.outputs.labels_added, 'rocm') && contains(toJSON(github.event.issue.labels.*.name), 'bug') - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | const body = (context.payload.issue.body || '').toLowerCase(); diff --git a/.github/workflows/macos-smoke-test.yml b/.github/workflows/macos-smoke-test.yml index 2e63c5676dd8..8de27d848e8b 100644 --- a/.github/workflows/macos-smoke-test.yml +++ b/.github/workflows/macos-smoke-test.yml @@ -28,9 +28,9 @@ jobs: timeout-minutes: 30 steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0 + - uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3 with: enable-cache: true cache-dependency-glob: | diff --git a/.github/workflows/notify-ci-authorized.yml b/.github/workflows/notify-ci-authorized.yml index c260cc67dba0..1396032d51cb 100644 --- a/.github/workflows/notify-ci-authorized.yml +++ b/.github/workflows/notify-ci-authorized.yml @@ -34,11 +34,11 @@ jobs: github.event.workflow_run.conclusion == 'success')) runs-on: [self-hosted, linux, x64, vllm-runners] steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: ref: ${{ github.event.repository.default_branch }} persist-credentials: false - - uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0 + - uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3 with: python-version: "3.12" - name: Notify PR author that CI is available diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index 861d4a5e498b..1410a8944828 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -49,14 +49,18 @@ jobs: if: github.repository_owner == 'vllm-project' && always() && (needs.pre-run-check.result == 'success' || needs.pre-run-check.result == 'skipped') runs-on: [self-hosted, linux, x64, vllm-runners] steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - - uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3 with: python-version: "3.12" - run: echo "::add-matcher::.github/workflows/matchers/actionlint.json" - run: echo "::add-matcher::.github/workflows/matchers/markdownlint.json" - run: echo "::add-matcher::.github/workflows/matchers/mypy.json" - run: echo "::add-matcher::.github/workflows/matchers/shellcheck.json" - - uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd # v3.0.1 + # Replaces pre-commit/action, whose latest release (v3.0.1) uses the node20 actions/cache@v4 + - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: - extra_args: --all-files --hook-stage manual + path: ~/.cache/pre-commit + key: pre-commit-uv|${{ runner.os }}|${{ env.UV_PYTHON }}|${{ hashFiles('.pre-commit-config.yaml') }} + - run: >- + uvx pre-commit run --show-diff-on-failure --color=always --all-files --hook-stage manual diff --git a/.github/workflows/proto-crate.yml b/.github/workflows/proto-crate.yml index 43be5c8c9214..ad20db39fa66 100644 --- a/.github/workflows/proto-crate.yml +++ b/.github/workflows/proto-crate.yml @@ -24,8 +24,8 @@ jobs: semver: runs-on: ubuntu-latest steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - - uses: obi1kenobi/cargo-semver-checks-action@6b69fcf40e9b5fb17adeb57e4b6ecd020649a239 # v2 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: obi1kenobi/cargo-semver-checks-action@6b69fcf40e9b5fb17adeb57e4b6ecd020649a239 # v2.9 with: manifest-path: rust/proto/Cargo.toml package: vllm-proto @@ -33,7 +33,7 @@ jobs: check: runs-on: ubuntu-latest steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - run: rustup show active-toolchain - name: Check release version if: github.event_name == 'push' @@ -56,13 +56,13 @@ jobs: contents: read id-token: write steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: ref: ${{ github.sha }} - run: rustup show active-toolchain - name: Verify package before authentication run: cargo publish --manifest-path rust/proto/Cargo.toml --locked --dry-run - - uses: rust-lang/crates-io-auth-action@c6f97d42243bad5fab37ca0427f495c86d5b1a18 # v1.0.5 + - uses: rust-lang/crates-io-auth-action@3268eb413133e2960fa4ae0e963ca76380c54cd9 # v1.0.6 id: auth - name: Publish env: diff --git a/.github/workflows/recipes-preview.yml b/.github/workflows/recipes-preview.yml index 42127e99b79e..efaca4681a67 100644 --- a/.github/workflows/recipes-preview.yml +++ b/.github/workflows/recipes-preview.yml @@ -46,7 +46,7 @@ jobs: steps: - name: Create a token for vllm-project/recipes id: token - uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 + uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0 with: app-id: ${{ vars.RECIPES_APP_ID }} private-key: ${{ secrets.RECIPES_APP_PRIVATE_KEY }} diff --git a/.github/workflows/run-ci-command.yml b/.github/workflows/run-ci-command.yml index d3336772717f..877c2d91b94b 100644 --- a/.github/workflows/run-ci-command.yml +++ b/.github/workflows/run-ci-command.yml @@ -22,11 +22,11 @@ jobs: startsWith(github.event.comment.body, '/amd-ci')) runs-on: [self-hosted, linux, x64, vllm-runners] steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: ref: ${{ github.event.repository.default_branch }} persist-credentials: false - - uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0 + - uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3 with: python-version: "3.12" - name: Authorize and run CI command diff --git a/.github/workflows/scripts/run_ci_command.py b/.github/workflows/scripts/run_ci_command.py index 64ec8cb7623b..f249c08ce5c2 100644 --- a/.github/workflows/scripts/run_ci_command.py +++ b/.github/workflows/scripts/run_ci_command.py @@ -76,11 +76,9 @@ } RETRY_STATES = "failed,timed_out,expired" CANCELABLE_BUILD_STATES = ("scheduled", "running", "failing") -SETUP_STEP_KEYS = { - "ensure-ci-base-amd", - "pre-commit", - "refresh-rocm-base-amd", -} +# ensure-ci-base-amd: the old ROCm build chain, which ci-infra still renders +# for branches that predate .buildkite/scripts/rocm/build.sh. +SETUP_STEP_KEYS = {"ensure-ci-base-amd", "refresh-rocm-base-amd", "pre-commit"} class ApiError(RuntimeError): diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index ba807fab7c3a..d2c5533d31f6 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -15,7 +15,7 @@ jobs: actions: write runs-on: ubuntu-latest steps: - - uses: actions/stale@eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899 # v10.3.0 + - uses: actions/stale@4391f3da665fdf50b6810c1a66712fb9ba21aa93 # v11.0.0 with: # Increasing this value ensures that changes to this workflow # propagate to all issues and PRs in days rather than months diff --git a/.gitignore b/.gitignore index 57d0d54fc87b..533f9cfa4921 100644 --- a/.gitignore +++ b/.gitignore @@ -251,3 +251,9 @@ vllm/grpc/vllm_engine_pb2.pyi # Ignore generated cpu headers csrc/cpu/cpu_attn_dispatch_generated.h + +# Written by .buildkite/scripts/rocm/build.sh +/.rocm-src/ +/.rocm-version/ +/.rocm-wheel/ +/.rocm-kernel-map/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c940db4ae844..b42d38d93a31 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,42 +5,43 @@ default_stages: - pre-commit # Run locally - manual # Run in CI exclude: 'vllm/third_party/.*|vllm/models/kimi_k3/nvidia/ops/third_party/.*|vllm/models/kimi_k3/amd/ops/third_party/.*' +# Revs are frozen to commit SHAs, bump them with `pre-commit autoupdate --freeze [--repo ]` repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.14.0 + rev: f9351c924055bf6c7b4a4670237d3ce141e9f57c # frozen: v0.14.0 hooks: - id: ruff-check args: [--output-format, github, --fix] - id: ruff-format - repo: https://github.com/crate-ci/typos - rev: v1.43.5 + rev: 9023c1f779400877cafdd98052113fc777e2cfdc # frozen: v1.51.1 hooks: - id: typos args: [--force-exclude] - repo: https://github.com/pre-commit/mirrors-clang-format - rev: v21.1.2 + rev: 98ad4ddb9c057de2f5b9e93e44e6db959807d68c # frozen: v23.1.3 hooks: - id: clang-format exclude: 'csrc/moe/topk_softmax_kernels.cu|vllm/third_party/.*' types_or: [c++, cuda] args: [--style=file, --verbose] - repo: https://github.com/DavidAnson/markdownlint-cli2 - rev: v0.21.0 + rev: 341e2a331ba578029e66c2e01183e7a6b5ca9ab4 # frozen: v0.24.0 hooks: - id: markdownlint-cli2 - language_version: lts + language_version: 24.22.1 # Node 26 (current lts) needs libatomic, which the CI runners lack args: [--config, tools/pre_commit/.markdownlint.yaml, --fix] - repo: https://github.com/rhysd/actionlint - rev: v1.7.7 + rev: 914e7df21a07ef503a81201c76d2b11c789d3fca # frozen: v1.7.12 hooks: - id: actionlint - repo: https://github.com/shellcheck-py/shellcheck-py - rev: 745eface02aef23e168a8afb6b5737818efbea95 # v0.11.0.1 + rev: a6564f5d57444e123433a5c9af733e3975c77229 # frozen: v0.11.0.1-1 hooks: - id: shellcheck args: ["-s", "bash", "--format=gcc", "--rcfile=tools/pre_commit/.shellcheckrc"] - repo: https://github.com/astral-sh/uv-pre-commit - rev: 0.11.1 + rev: 46787482b9fb2003c56e0f7592e312ef381829d9 # frozen: 0.13.6 hooks: - id: pip-compile args: [ @@ -74,6 +75,7 @@ repos: --no-emit-package, cupy-cuda12x, # nvidia packages (unsuffixed / unified naming) --no-emit-package, nvidia-cublas, + --no-emit-package, nvidia-cuda-cccl, --no-emit-package, nvidia-cuda-cupti, --no-emit-package, nvidia-cuda-nvrtc, --no-emit-package, nvidia-cuda-runtime, @@ -158,7 +160,7 @@ repos: ] files: ^requirements/docs\.(in|txt)$ - repo: https://github.com/pre-commit/pre-commit-hooks - rev: 3e8a8703264a2f4a69428a0aa4dcb512790b2c8c + rev: 3e8a8703264a2f4a69428a0aa4dcb512790b2c8c # frozen: v6.0.0 hooks: - id: check-json files: ^\.buildkite/.*\.json$ diff --git a/CMakeLists.txt b/CMakeLists.txt index 7694e6f8bc46..32f95b697069 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1232,6 +1232,25 @@ if(VLLM_GPU_LANG STREQUAL "CUDA" OR VLLM_GPU_LANG STREQUAL "HIP") message(STATUS "Building hadacore") endif() + # Fused MQA logits + top-k prefill kernel (SM100 family). It compiles against + # DeepGEMM's headers, so DeepGEMM is fetched here. + include(cmake/external_projects/deepgemm.cmake) + if(${CMAKE_CUDA_COMPILER_VERSION} VERSION_GREATER_EQUAL 12.9) + cuda_archs_loose_intersection(FUSED_MQA_TOPK_ARCHS "10.0f" "${CUDA_ARCHS}") + endif() + if(FUSED_MQA_TOPK_ARCHS) + set(FUSED_MQA_TOPK_SRC "csrc/fused_mqa_topk/fused_mqa_topk.cu") + set_gencode_flags_for_srcs( + SRCS "${FUSED_MQA_TOPK_SRC}" + CUDA_ARCHS "${FUSED_MQA_TOPK_ARCHS}") + set_property(SOURCE ${FUSED_MQA_TOPK_SRC} APPEND PROPERTY INCLUDE_DIRECTORIES + "${deepgemm_SOURCE_DIR}/deep_gemm/include" + "${deepgemm_SOURCE_DIR}/third-party/cutlass/include") + list(APPEND VLLM_STABLE_EXT_SRC "${FUSED_MQA_TOPK_SRC}") + message(STATUS + "Building fused MQA top-k for archs: ${FUSED_MQA_TOPK_ARCHS}") + endif() + # if CUDA endif endif() @@ -1296,6 +1315,10 @@ if(VLLM_GPU_LANG STREQUAL "CUDA" OR VLLM_GPU_LANG STREQUAL "HIP") target_compile_definitions(_C_stable_libtorch PRIVATE VLLM_ENABLE_FUSED_KDA_DECODE=1) endif() + if(FUSED_MQA_TOPK_ARCHS) + target_compile_definitions(_C_stable_libtorch PRIVATE + VLLM_ENABLE_FUSED_MQA_TOPK=1) + endif() if(FUSED_GDN_DECODE_ARCHS) target_compile_definitions(_C_stable_libtorch PRIVATE VLLM_ENABLE_FUSED_GDN_DECODE=1) @@ -1372,6 +1395,19 @@ set_gencode_flags_for_srcs( SRCS "${VLLM_MOE_EXT_SRC}" CUDA_ARCHS "${CUDA_ARCHS}") +if(VLLM_GPU_LANG STREQUAL "CUDA") + set(MUSE_SPARK_ROUTING_ARCHS "${CUDA_ARCHS}") + if(CMAKE_CUDA_COMPILER_VERSION VERSION_GREATER_EQUAL 13.0) + # Native FP32 warp-max reduction requires the Blackwell family target. + list(TRANSFORM MUSE_SPARK_ROUTING_ARCHS + REPLACE "^10[.]0([+]PTX)?$" "10.0f") + endif() + set_gencode_flags_for_srcs( + SRCS "csrc/moe/muse_spark_routing.cu" + CUDA_ARCHS "${MUSE_SPARK_ROUTING_ARCHS}") + list(APPEND VLLM_MOE_EXT_SRC "csrc/moe/muse_spark_routing.cu") +endif() + if(VLLM_GPU_LANG STREQUAL "CUDA") set(VLLM_MOE_WNA16_SRC "csrc/moe/moe_wna16.cu") @@ -1550,61 +1586,101 @@ if(VLLM_GPU_LANG STREQUAL "HIP") # # _rocm_C extension # - # The paged attention *instantiations* are split over one translation unit - # per (dtype, kv-cache dtype, head size) combination: compiling them all in a - # single file serializes ~10 min of device code generation onto one core. - # attention_dispatch.cu only holds the runtime dispatch, attention_kernels.cuh - # the kernels and launchers. A file listed here is dead weight unless - # attention_dispatch.cu also dispatches its entry point (guarded below). + # The paged attention instantiations are split over one generated translation + # unit per (dtype, kv-cache dtype, head size, block size); compiling them in a + # single file serialized ~10 min of device code generation onto one core. The + # same script emits the entry point prototypes and paged_attention()'s dispatch + # arms, so a unit cannot be compiled without being dispatched. + # attention_dispatch.cu only holds argument validation and is_navi detection, + # attention_kernels.cuh the kernels and launchers. + # + set(ROCM_ATTN_GEN_SCRIPT + ${CMAKE_CURRENT_SOURCE_DIR}/csrc/rocm/generate_paged_attn.py) + # Editing the script has to re-run configure, otherwise the units on disk stay + # stale until something else triggers a reconfigure. + set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS + ${ROCM_ATTN_GEN_SCRIPT}) + file(MD5 ${ROCM_ATTN_GEN_SCRIPT} ROCM_ATTN_GEN_SCRIPT_HASH) + # The script records this key plus the exact list of files it wrote, in a stamp + # next to the outputs; the list is what CMake uses as the source list, so a + # stray `attention_paged_attn_*.cu` or a partially deleted set cannot slip in. + # The stamp lives in the source tree (not in a per-build-directory cache) + # because the outputs do. Whether the Navi kernel set is instantiated is a + # compile definition, not part of the generated text, so the units are identical + # for every build directory and arch list. + set(ROCM_ATTN_GEN_STAMP + ${CMAKE_CURRENT_SOURCE_DIR}/csrc/rocm/.paged_attn_generated) + + # Sets OUT_SRCS to the .cu files the stamp lists, or to nothing when the stamp + # is missing, written by another version of the script, or lists a missing file + # (all of which mean "regenerate"). + function (vllm_rocm_attn_generated_srcs out_srcs) + set(${out_srcs} "" PARENT_SCOPE) + if (NOT EXISTS ${ROCM_ATTN_GEN_STAMP}) + return() + endif() + file(STRINGS ${ROCM_ATTN_GEN_STAMP} _lines) + if (NOT _lines) + return() + endif() + list(GET _lines 0 _key) + list(REMOVE_AT _lines 0) + if (NOT _key STREQUAL "${ROCM_ATTN_GEN_SCRIPT_HASH}") + return() + endif() + set(_srcs) + foreach (_f ${_lines}) + if (NOT EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/csrc/rocm/${_f}) + return() + endif() + if (_f MATCHES "\\.cu$") + list(APPEND _srcs csrc/rocm/${_f}) + endif() + endforeach() + set(${out_srcs} ${_srcs} PARENT_SCOPE) + endfunction() + + vllm_rocm_attn_generated_srcs(VLLM_ATTENTION_PAGED_ATTN_SRC) + if (NOT VLLM_ATTENTION_PAGED_ATTN_SRC) + execute_process( + COMMAND ${Python_EXECUTABLE} ${ROCM_ATTN_GEN_SCRIPT} + ${ROCM_ATTN_GEN_SCRIPT_HASH} + RESULT_VARIABLE rocm_attn_generation_result + OUTPUT_FILE ${CMAKE_CURRENT_BINARY_DIR}/rocm_attn_generation.log + ERROR_FILE ${CMAKE_CURRENT_BINARY_DIR}/rocm_attn_generation.log + ) + + if (NOT rocm_attn_generation_result EQUAL 0) + message(FATAL_ERROR "Paged attention generation failed." + " Result: \"${rocm_attn_generation_result}\"" + "\nCheck the log for details: " + "${CMAKE_CURRENT_BINARY_DIR}/rocm_attn_generation.log") + endif() + vllm_rocm_attn_generated_srcs(VLLM_ATTENTION_PAGED_ATTN_SRC) + if (NOT VLLM_ATTENTION_PAGED_ATTN_SRC) + message(FATAL_ERROR "Paged attention generation produced no sources; see " + "${CMAKE_CURRENT_BINARY_DIR}/rocm_attn_generation.log") + endif() + message(STATUS "Paged attention generation completed successfully.") + else() + message(STATUS "Paged attention generation is up to date.") + endif() + + # Configure deps too, so a plain `ninja` on an out-of-tree build dir regenerates + # a missing unit (e.g. after `git clean -X`) instead of failing on a missing + # source; see docs/contributing/incremental_build.md. pip install reruns anyway. + list(TRANSFORM VLLM_ATTENTION_PAGED_ATTN_SRC + PREPEND ${CMAKE_CURRENT_SOURCE_DIR}/ OUTPUT_VARIABLE _attn_generated_abs) + set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS + ${ROCM_ATTN_GEN_STAMP} ${_attn_generated_abs}) + set(VLLM_ROCM_EXT_SRC "csrc/rocm/torch_bindings.cpp" "csrc/rocm/skinny_gemms.cu" "csrc/rocm/skinny_gemms_int4.cu" "csrc/rocm/attention_dispatch.cu" - "csrc/rocm/attention_paged_attn_h64_f16_auto.cu" - "csrc/rocm/attention_paged_attn_h128_f16_auto.cu" - "csrc/rocm/attention_paged_attn_h64_bf16_auto.cu" - "csrc/rocm/attention_paged_attn_h128_bf16_auto.cu" - "csrc/rocm/attention_paged_attn_h64_f16_fp8_f16.cu" - "csrc/rocm/attention_paged_attn_h128_f16_fp8_f16.cu" - "csrc/rocm/attention_paged_attn_h64_f16_fp8_fp8.cu" - "csrc/rocm/attention_paged_attn_h128_f16_fp8_fp8.cu" - "csrc/rocm/attention_paged_attn_h64_bf16_fp8_f16.cu" - "csrc/rocm/attention_paged_attn_h128_bf16_fp8_f16.cu" - "csrc/rocm/attention_paged_attn_h64_bf16_fp8_fp8.cu" - "csrc/rocm/attention_paged_attn_h128_bf16_fp8_fp8.cu") - - # Each attention_paged_attn_*.cu file below instantiates one kernel set and - # exposes it as one entry point. That entry point is only reachable if - # paged_attention() in csrc/rocm/attention_dispatch.cu names it in one of its - # ATTN_HEAD_SWITCH() calls (the dispatch arms); otherwise the file is compiled - # and linked unnecessarily. Fail at configure time instead. - file(READ "${CMAKE_SOURCE_DIR}/csrc/rocm/attention_dispatch.cu" _attn_dispatch) - string(FIND "${_attn_dispatch}" "#define ATTN_HEAD_SWITCH" _attn_arms_pos) - if (_attn_arms_pos EQUAL -1) - message(FATAL_ERROR - "csrc/rocm/attention_dispatch.cu no longer defines ATTN_HEAD_SWITCH; " - "update the instantiation guard in CMakeLists.txt accordingly.") - endif() - string(SUBSTRING "${_attn_dispatch}" ${_attn_arms_pos} -1 _attn_arms) - foreach (_attn_src ${VLLM_ROCM_EXT_SRC}) - if (_attn_src MATCHES "^csrc/rocm/attention_paged_attn_.*\\.cu$") - file(READ "${CMAKE_SOURCE_DIR}/${_attn_src}" _attn_tu) - string(REGEX MATCHALL "ATTN_ENTRY\\([A-Za-z0-9_]+\\)" _attn_entries - "${_attn_tu}") - foreach (_attn_entry ${_attn_entries}) - string(REGEX REPLACE "ATTN_ENTRY\\((.*)\\)" "\\1" _attn_name - "${_attn_entry}") - if (NOT _attn_arms MATCHES "${_attn_name}[,)]") - message(FATAL_ERROR - "${_attn_src} instantiates ${_attn_name}, but " - "csrc/rocm/attention_dispatch.cu never dispatches it: the file " - "would be compiled and linked for nothing. Add it to one of the " - "ATTN_HEAD_SWITCH() calls in paged_attention().") - endif() - endforeach() - endif() - endforeach() + ${VLLM_ATTENTION_PAGED_ATTN_SRC}) + # Compress the offloaded device binaries. The 9-arch release build packs # ~2 GiB of .hip_fatbin into _rocm_C.abi3.so, which pushes the module past # the 2 GiB reach of the 32-bit PC-relative relocations in crtbegin's .text @@ -1671,6 +1747,21 @@ if(VLLM_GPU_LANG STREQUAL "HIP") target_compile_definitions(_rocm_C PRIVATE VLLM_SKIP_SKINNY_GEMMS) endif() + # The paged attention units are identical whatever the arch list is; whether the + # Navi (gfx11/gfx12) kernel set is needed is decided here, as a compile + # definition, so no generated file has to be rewritten for a different build + # directory. Keep this predicate in sync with is_navi_gpu() in + # csrc/rocm/attention_dispatch.cu. + set(_ROCM_NAVI_ARCHS ${VLLM_GPU_ARCHES}) + list(FILTER _ROCM_NAVI_ARCHS INCLUDE REGEX "gfx1[12]") + set(_ROCM_NO_NAVI_ARCHS ${VLLM_GPU_ARCHES}) + list(FILTER _ROCM_NO_NAVI_ARCHS EXCLUDE REGEX "gfx1[12]") + if(_ROCM_NO_NAVI_ARCHS AND NOT _ROCM_NAVI_ARCHS) + message(STATUS "paged attention: CDNA only (${VLLM_GPU_ARCHES}), " + "Navi kernels not instantiated") + target_compile_definitions(_rocm_C PRIVATE VLLM_ROCM_ATTN_NO_NAVI) + endif() + if(VLLM_ROCM_HAS_GFX1100) target_compile_definitions(_rocm_C PRIVATE VLLM_ROCM_GFX1100) endif() @@ -1701,7 +1792,6 @@ endif() # For CUDA we also build and ship some external projects. if (VLLM_GPU_LANG STREQUAL "CUDA") - include(cmake/external_projects/deepgemm.cmake) include(cmake/external_projects/deepselect.cmake) include(cmake/external_projects/fmha_sm100.cmake) include(cmake/external_projects/flashmla.cmake) diff --git a/benchmarks/kernels/benchmark_dsv41_mega_attn.py b/benchmarks/kernels/benchmark_dsv41_mega_attn.py index 1b3b5d9c3848..ede9021e0a36 100644 --- a/benchmarks/kernels/benchmark_dsv41_mega_attn.py +++ b/benchmarks/kernels/benchmark_dsv41_mega_attn.py @@ -118,7 +118,6 @@ def bench_decode(s_q, device, topk_extra, extra_bytes, local_heads): n_wv_group = padded_heads // 8 cos_sin = make_cos_sin_cache(65536, device) positions = torch.randint(0, 65536, (s_q,), device=device, dtype=torch.int64) - pos32 = positions.to(torch.int32) # Per-step inputs: the wq_b output (local heads) and this step's KV row. q_local = torch.randn( @@ -177,7 +176,7 @@ def mega(): ex_len, False, 0.0, - pos32, + positions, False, 64, cos_sin, diff --git a/benchmarks/kernels/benchmark_gemm_rs_ar.py b/benchmarks/kernels/benchmark_gemm_rs_ar.py index 3e47f61c74f0..9c06e3ba21f3 100644 --- a/benchmarks/kernels/benchmark_gemm_rs_ar.py +++ b/benchmarks/kernels/benchmark_gemm_rs_ar.py @@ -18,6 +18,11 @@ torchrun --nproc-per-node=4 -- benchmarks/kernels/benchmark_gemm_rs_ar.py \ --model deepseek_v41 --backend flashinfer_cutedsl + +or the MiniMax-M3 MXFP8 attention O-proj and dense down-proj at TP4 with: + + torchrun --nproc-per-node=4 -- benchmarks/kernels/benchmark_gemm_rs_ar.py \ + --model minimax_m3 --backend flashinfer_cutedsl """ import argparse @@ -45,10 +50,12 @@ # Per model: (hidden size N, global input widths K). Kimi-K3 lists the # shared-expert down-proj and attention O-proj; DeepSeek-V4.1 lists the -# ``wo_b`` output projection (o_groups * o_lora_rank). +# ``wo_b`` output projection (o_groups * o_lora_rank). MiniMax-M3 lists the +# attention O-proj and the dense-layer down-proj. _MODEL_PROJECTIONS = { "kimi_k3": (7168, (6144, 12288)), "deepseek_v41": (5120, (8192,)), + "minimax_m3": (6144, (8192, 12288)), } diff --git a/benchmarks/kernels/benchmark_mxfp8_tp4.py b/benchmarks/kernels/benchmark_mxfp8_tp4.py new file mode 100644 index 000000000000..e9ee03c8a431 --- /dev/null +++ b/benchmarks/kernels/benchmark_mxfp8_tp4.py @@ -0,0 +1,208 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +"""Validate MXFP8 TP4 linears and time HIP graph replay over rotating weights. + +Example (record the baseline and candidate SHAs): + baseline=$(git rev-parse origin/main) + git rev-parse "$baseline" HEAD + git show "$baseline":vllm/model_executor/kernels/linear/mxfp8/rocm_native.py \ + > /tmp/baseline_rocm_native.py + .venv/bin/python benchmarks/kernels/benchmark_mxfp8_tp4.py \ + --baseline /tmp/baseline_rocm_native.py + +Reports cold (>400 MiB rotating weights) and warm timings separately. Each timing +is the median of 30 device-event samples; Python wall time is never ranked. +""" + +import argparse +import importlib.util +import json +import math +import statistics +from functools import partial +from pathlib import Path + +import torch + +from vllm.model_executor.kernels.linear.mxfp8 import rocm_native +from vllm.model_executor.layers.quantization.utils.mxfp8_utils import ( + dequant_mxfp8_to_bf16, + mxfp8_e4m3_quantize, +) +from vllm.triton_utils import triton + +SHAPES = [ + (m, n, k) + for n, k in [(2560, 6144), (6144, 2048), (1536, 6144), (6144, 768)] + for m in [1, 8, 16] +] + + +def time_graph(calls): + for call in calls: + call() + torch.accelerator.synchronize() + graph = torch.cuda.CUDAGraph() + with torch.cuda.graph(graph): + for _ in range(2): + for call in calls: + call() + for _ in range(5): + graph.replay() + events = [ + (torch.cuda.Event(enable_timing=True), torch.cuda.Event(enable_timing=True)) + for _ in range(30) + ] + for start, end in events: + start.record() + for _ in range(5): + graph.replay() + end.record() + # Bound outstanding HIP graph nodes on short-K cases with many slots. + end.synchronize() + torch.accelerator.synchronize() + return statistics.median( + start.elapsed_time(end) / (10 * len(calls)) for start, end in events + ) + + +def launch(module, xq, xs, w, ws, out, config): + bm, bn, bk, nw, ns = config + m, k = xq.shape + n = w.shape[0] + return module._mxfp8_linear_kernel[(triton.cdiv(m, bm), triton.cdiv(n, bn))]( + xq, + xs, + w, + ws, + out, + m, + n, + k, + *xq.stride(), + *xs.stride(), + *w.stride(), + *ws.stride(), + *out.stride(), + BLOCK_M=bm, + BLOCK_N=bn, + BLOCK_K=bk, + num_warps=nw, + num_stages=ns, + ) + + +def emit(**data): + print(json.dumps(data), flush=True) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--baseline", type=Path) + parser.add_argument("--device", type=int, default=0) + parser.add_argument("--shape", type=int, choices=range(len(SHAPES))) + parser.add_argument("--tune", action="store_true") + parser.add_argument("--profile", action="store_true") + parser.add_argument("--asm-dir", type=Path) + args = parser.parse_args() + torch.accelerator.set_device_index(args.device) + torch.manual_seed(42) + variants = {"candidate": rocm_native} + if args.baseline: + spec = importlib.util.spec_from_file_location( + "vllm.model_executor.kernels.linear.mxfp8.baseline_rocm_native", + args.baseline, + ) + baseline = importlib.util.module_from_spec(spec) + spec.loader.exec_module(baseline) + variants = {"baseline": baseline, **variants} + emit( + torch=torch.__version__, + triton=triton.__version__, + device=str(torch.cuda.get_device_properties(args.device)), + ) + for index, (m, n, k) in enumerate(SHAPES): + if args.shape is not None and index != args.shape: + continue + x = torch.randn(m, k, device="cuda", dtype=torch.bfloat16) * 0.5 + w, ws = mxfp8_e4m3_quantize( + torch.randn(n, k, device="cuda", dtype=torch.bfloat16) * 0.05 + ) + xq, xs = mxfp8_e4m3_quantize(x) + reference = ( + dequant_mxfp8_to_bf16(xq, xs).float() + @ dequant_mxfp8_to_bf16(w, ws).float().T + ) + out = torch.empty(m, n, device="cuda", dtype=x.dtype) + slots = math.ceil(400 * 1024**2 / (n * k)) + weights = [(w.clone(), ws.clone()) for _ in range(slots)] + calls = {} + for name, module in variants.items(): + configs = [module._select_cfg(m, n, k)] + if args.tune: + configs += [ + (16, 16, 1024, 2, 2), + (16, 16, 1024, 2, 3), + (16, 16, 1024, 2, 4), + (16, 16, 512, 2, 2), + (16, 32, 256, 4, 3), + (16, 16, 256, 2, 3), + (128, 128, 128, 8, 2), + ] + for config in dict.fromkeys(configs): + if k % config[2]: + continue + fn = partial(launch, module, xq, xs, config=config, out=out) + obj = fn(w=w, ws=ws) + rel = ((out.float() - reference).norm() / reference.norm()).item() + snr = -20 * math.log10(rel) if rel else float("inf") + assert rel < 0.005 and snr >= 30, (name, config, rel, snr) + emit( + shape=[m, n, k], + variant=name, + config=config, + rel=rel, + snr_db=snr, + registers=obj.n_regs, + spills=obj.n_spills, + lds=obj.metadata.shared, + ) + if args.asm_dir: + args.asm_dir.mkdir(parents=True, exist_ok=True) + (args.asm_dir / f"{name}-{m}-{n}-{k}-{config}.amdgcn").write_text( + obj.asm["amdgcn"] + ) + if args.profile: + for _ in range(10): + fn(w=w, ws=ws) + torch.accelerator.synchronize() + continue + calls[f"{name}-kernel-{config}"] = [ + partial(fn, w=wi, ws=si) for wi, si in weights + ] + if not args.profile and not args.tune: + wrapper = partial(module._mxfp8_dot_scaled_linear, x) + actual = wrapper(w, ws) + assert ((actual.float() - reference).norm() / reference.norm()) < 0.005 + calls[f"{name}-wrapper"] = [ + partial(wrapper, wi, si) for wi, si in weights + ] + for repeat in range(2): + order = list(calls.items()) + if repeat: + order.reverse() + for name, functions in order: + cold = time_graph(functions) + warm = time_graph([functions[0]] * 32) + emit( + shape=[m, n, k], + variant=name, + repeat=repeat, + cold_ms=cold, + warm_ms=warm, + slots=slots, + ) + + +if __name__ == "__main__": + main() diff --git a/benchmarks/kernels/benchmark_reshape_and_cache_flash.py b/benchmarks/kernels/benchmark_reshape_and_cache_flash.py index 84d9cc707c40..229f5385a4c2 100644 --- a/benchmarks/kernels/benchmark_reshape_and_cache_flash.py +++ b/benchmarks/kernels/benchmark_reshape_and_cache_flash.py @@ -22,10 +22,45 @@ @torch.inference_mode() +def _make_packed_kv_caches( + num_blocks: int, + block_size: int, + num_heads: int, + head_size: int, + v_head_size: int, + kv_cache_dtype: str, + dtype: torch.dtype, + kv_cache_layout: str, + device: str, +): + """K and V as views of one [.., head_size + v_head_size] buffer. + + The asymmetric kernel needs both sides to share a head stride, which is what + a packed K|V buffer gives (and what backends such as ROCm AITER unified + attention produce). + """ + if kv_cache_dtype not in ("auto", "bfloat16", "float16", "fp8"): + raise ValueError(f"asymmetric benchmark does not support {kv_cache_dtype}.") + content = head_size + v_head_size + if kv_cache_layout == "NHD": + physical = (num_blocks, block_size, num_heads, content) + permute = None + else: + physical = (num_blocks, num_heads, block_size, content) + permute = (0, 2, 1, 3) + + cache_dtype = torch.uint8 if kv_cache_dtype == "fp8" else dtype + packed = torch.zeros(physical, dtype=cache_dtype, device=device) + view = packed if permute is None else packed.permute(permute) + key_cache, value_cache = view.split([head_size, v_head_size], dim=-1) + return key_cache, value_cache + + def run_benchmark( num_tokens: int, num_heads: int, head_size: int, + v_head_size: int | None, block_size: int, num_blocks: int, dtype: torch.dtype, @@ -37,8 +72,14 @@ def run_benchmark( device: str = "cuda", ) -> float: """Return latency (seconds) for given num_tokens.""" - if kv_cache_dtype == "fp8" and head_size % 16: - raise ValueError("fp8 kv-cache requires head_size to be a multiple of 16.") + if v_head_size is None: + v_head_size = head_size + asymmetric = v_head_size != head_size + + if kv_cache_dtype == "fp8" and (head_size % 16 or v_head_size % 16): + raise ValueError( + "fp8 kv-cache requires head_size and v_head_size to be multiples of 16." + ) if implementation not in ("cuda", "triton"): raise ValueError( @@ -47,13 +88,23 @@ def run_benchmark( ) if implementation == "triton" and kv_cache_layout == "HND": return float("nan") # Triton does not support HND layout yet. + if asymmetric and implementation != "cuda": + raise ValueError( + "asymmetric head sizes (v_head_size != head_size) are only supported " + "by the 'cuda' implementation." + ) set_random_seed(42) torch.set_default_device(device) # create random key / value tensors [T, H, D]. key = torch.randn(num_tokens, num_heads, head_size, dtype=dtype, device=device) - value = torch.randn_like(key) + if asymmetric: + value = torch.randn( + num_tokens, num_heads, v_head_size, dtype=dtype, device=device + ) + else: + value = torch.randn_like(key) # prepare the slot mapping. # each token is assigned a unique slot in the KV-cache. @@ -63,20 +114,36 @@ def run_benchmark( slot_mapping_lst = random.sample(range(num_slots), num_tokens) slot_mapping = torch.tensor(slot_mapping_lst, dtype=torch.long, device=device) - key_caches, value_caches = create_kv_caches_with_random_flash( - num_blocks, - block_size, - 1, # num_layers - num_heads, - head_size, - kv_cache_dtype, - dtype, - device=device, - cache_layout=kv_cache_layout, - ) - key_cache, value_cache = key_caches[0], value_caches[0] - # to free unused memory - del key_caches, value_caches + if asymmetric: + # K and V are views of one buffer, so both sides share the head stride; + # that is the only layout the asymmetric path accepts (see + # reshape_and_cache_flash). + key_cache, value_cache = _make_packed_kv_caches( + num_blocks, + block_size, + num_heads, + head_size, + v_head_size, + kv_cache_dtype, + dtype, + kv_cache_layout, + device, + ) + else: + key_caches, value_caches = create_kv_caches_with_random_flash( + num_blocks, + block_size, + 1, # num_layers + num_heads, + head_size, + kv_cache_dtype, + dtype, + device=device, + cache_layout=kv_cache_layout, + ) + key_cache, value_cache = key_caches[0], value_caches[0] + # to free unused memory + del key_caches, value_caches # compute per-kernel scaling factors for fp8 conversion (if used). k_scale = (key.amax() / 64.0).to(torch.float32) @@ -142,6 +209,7 @@ def main(args): num_tokens=n_tok, num_heads=args.num_heads, head_size=args.head_size, + v_head_size=args.v_head_size, block_size=args.block_size, num_blocks=args.num_blocks, dtype=STR_DTYPE_TO_TORCH_DTYPE[args.dtype], @@ -154,9 +222,14 @@ def main(args): ) rows.append([n_tok, layout, f"{lat * 1e6:.3f}"]) + v_note = ( + "" + if args.v_head_size is None + else f", head_size={args.head_size} v_head_size={args.v_head_size}" + ) print( f"Benchmark results for implementation {args.implementation}" - f" (measuring with {args.mode}):" + f" (measuring with {args.mode}{v_note}):" ) print(tabulate(rows, headers=["num_tokens", "layout", "latency (µs)"])) @@ -171,6 +244,13 @@ def main(args): choices=[64, 80, 96, 112, 120, 128, 192, 256], default=128, ) + parser.add_argument( + "--v-head-size", + type=int, + default=None, + help="V head dim; defaults to --head-size. Setting it differently " + "benchmarks the asymmetric K/V path (cuda implementation only).", + ) parser.add_argument("--block-size", type=int, choices=[16, 32], default=16) parser.add_argument("--num-blocks", type=int, default=128 * 512) diff --git a/cmake/cpu_extension.cmake b/cmake/cpu_extension.cmake index d92ad58a9cf3..39b327cb76b7 100644 --- a/cmake/cpu_extension.cmake +++ b/cmake/cpu_extension.cmake @@ -492,6 +492,8 @@ set(VLLM_EXT_SRC "csrc/cpu/activation.cpp" "csrc/cpu/sgl-kernels/fla.cpp" "csrc/cpu/utils.cpp" + "csrc/cpu/block_table.cpp" + "csrc/cpu/input_batch.cpp" "csrc/cpu/spec_decode_utils.cpp" "csrc/cpu/layernorm.cpp" "csrc/cpu/mla_decode.cpp" @@ -693,6 +695,7 @@ else() USE_SABI 3 WITH_SOABI ) + target_compile_definitions(_C PRIVATE VLLM_STANDALONE_CPU_MRV2) if (VLLM_OPENBLAS_LIB) target_compile_definitions(_C PRIVATE VLLM_HAS_OPENBLAS) endif() diff --git a/cmake/external_projects/deepgemm.cmake b/cmake/external_projects/deepgemm.cmake index 2fbfa157de88..e9d0bbbd2803 100644 --- a/cmake/external_projects/deepgemm.cmake +++ b/cmake/external_projects/deepgemm.cmake @@ -37,7 +37,7 @@ else() # (vllm-project/DeepGEMM#39), and the MX MegaMoE alignment fix # (vllm-project/DeepGEMM#43). # Uses the PyTorch 2.11 stable ABI and CPython 3.10 limited API. - set(_DEEPGEMM_UPSTREAM_TAG "0820030756945b2daec208e8e8255268360f337d") + set(_DEEPGEMM_UPSTREAM_TAG "5cdb145704ec4672ecf4b61441599521ce635df6") set(_deepgemm_fc_root "${FETCHCONTENT_BASE_DIR}") if(NOT _deepgemm_fc_root) diff --git a/cmake/external_projects/flashkda.cmake b/cmake/external_projects/flashkda.cmake index fadee58efbbd..f34efd577cca 100644 --- a/cmake/external_projects/flashkda.cmake +++ b/cmake/external_projects/flashkda.cmake @@ -13,7 +13,7 @@ else() FetchContent_Declare( flashkda GIT_REPOSITORY https://github.com/vllm-project/FlashKDA.git - GIT_TAG 17a037d98da546deb4591e967cf961a43c034d8b + GIT_TAG 9d9e928edc42475a67a523dd37814540d105f51c GIT_PROGRESS TRUE GIT_SUBMODULES cutlass ) diff --git a/cmake/external_projects/flashmla.cmake b/cmake/external_projects/flashmla.cmake index d60fd84e914b..6286e8e16303 100644 --- a/cmake/external_projects/flashmla.cmake +++ b/cmake/external_projects/flashmla.cmake @@ -19,7 +19,7 @@ else() FetchContent_Declare( flashmla GIT_REPOSITORY https://github.com/vllm-project/FlashMLA - GIT_TAG e2afa5fd8c220fbd05fe03fc24c9d5e64c24641b + GIT_TAG 847ef308bb7a6828c8117301f5a5a84a095a90e8 GIT_PROGRESS TRUE CONFIGURE_COMMAND "" BUILD_COMMAND "" diff --git a/cmake/external_projects/fmha_sm100.cmake b/cmake/external_projects/fmha_sm100.cmake index 6b84b35c9049..2336d66d23dc 100644 --- a/cmake/external_projects/fmha_sm100.cmake +++ b/cmake/external_projects/fmha_sm100.cmake @@ -17,7 +17,7 @@ else() FetchContent_Declare( fmha_sm100 GIT_REPOSITORY https://github.com/vllm-project/MSA.git - GIT_TAG 968061ab16cb14991a5c7d4c0e8eabf531be0dcf + GIT_TAG 25c144700f727b638067e98066a60507081b63ad GIT_PROGRESS TRUE CONFIGURE_COMMAND "" BUILD_COMMAND "" diff --git a/csrc/cache_kernels.cu b/csrc/cache_kernels.cu index 9567d87f443a..bd4a080c25ff 100644 --- a/csrc/cache_kernels.cu +++ b/csrc/cache_kernels.cu @@ -420,6 +420,73 @@ __global__ void reshape_and_cache_flash_kernel( } } +// Asymmetric K/V head widths. K and V are views of one packed buffer, so they +// share a head stride and the head-contiguous path below cannot apply; this +// kernel therefore walks heads explicitly and takes V's width as its one extra +// argument. It is kept separate so the symmetric kernel's codegen is unchanged. +template +__global__ void reshape_and_cache_flash_kernel_asym( + const scalar_t* __restrict__ key, // [num_tokens, num_heads, head_size] + const scalar_t* __restrict__ value, // [num_tokens, num_heads, v_head_size] + // NHD or HND: [num_blocks, block_size, num_heads, head_size], addressed + // through the strides passed by the launcher + cache_t* __restrict__ key_cache, + cache_t* __restrict__ value_cache, // same, with v_head_size + const int64_t* __restrict__ slot_mapping, // [num_tokens] + const int64_t block_stride, const int64_t page_stride, + const int64_t head_stride, const int64_t key_stride, + const int64_t value_stride, const int num_heads, const int head_size, + const int v_head_size, const int block_size, const float* k_scale, + const float* v_scale, const int kv_scale_stride) { + const int64_t token_idx = blockIdx.x; + const int64_t slot_idx = slot_mapping[token_idx]; + // NOTE: slot_idx can be -1 if the token is padded + if (slot_idx < 0) { + return; + } + const int64_t block_idx = slot_idx / block_size; + const int64_t block_offset = slot_idx % block_size; + + const scalar_t* __restrict__ key_src = key + token_idx * key_stride; + const scalar_t* __restrict__ value_src = value + token_idx * value_stride; + + cache_t* __restrict__ key_dst = + key_cache + block_idx * block_stride + block_offset * page_stride; + cache_t* __restrict__ value_dst = + value_cache + block_idx * block_stride + block_offset * page_stride; + + constexpr int VEC_SIZE = (sizeof(scalar_t) == 2) ? 8 : 4; + + const int lane = threadIdx.x & 31; // 0..31 within warp + const int warp_id = threadIdx.x >> 5; // warp index within block + const int warps_per_block = blockDim.x >> 5; + + for (int head = warp_id; head < num_heads; head += warps_per_block) { + const scalar_t* __restrict__ k_src_h = key_src + head * head_size; + const scalar_t* __restrict__ v_src_h = value_src + head * v_head_size; + + cache_t* __restrict__ k_dst_h = + key_dst + static_cast(head) * head_stride; + cache_t* __restrict__ v_dst_h = + value_dst + static_cast(head) * head_stride; + + float k_scale_val = (kv_dt == Fp8KVCacheDataType::kAuto) + ? 0.f + : k_scale[head * kv_scale_stride]; + float v_scale_val = (kv_dt == Fp8KVCacheDataType::kAuto) + ? 0.f + : v_scale[head * kv_scale_stride]; + + CopyWithScaleOp k_op{k_scale_val}; + CopyWithScaleOp v_op{v_scale_val}; + + vectorize_with_alignment(k_src_h, k_dst_h, head_size, lane, 32, + k_op); + vectorize_with_alignment(v_src_h, v_dst_h, v_head_size, lane, 32, + v_op); + } +} + template __global__ void concat_and_cache_mla_kernel( @@ -540,6 +607,7 @@ __global__ void concat_and_cache_ds_mla_kernel( const int kv_lora_rank, // const int pe_dim, // const int block_size, // + const bool has_rope_tail, // const float* scale // ) { const int64_t token_idx = blockIdx.x; @@ -556,15 +624,15 @@ __global__ void concat_and_cache_ds_mla_kernel( // For the NoPE part, each tile of 128 elements is handled by half of one warp // (16 threads). There are 4 total tiles, so 2 warps (64 threads). // Lanes 0 and 16 of each warp write the scale values for that warp's tiles. - // The RoPE part (last 64 elements) is handled by another 1 warp (32 threads). + // The optional RoPE tail is handled by another 1 warp (32 threads). // So in total, we use 3 warps (96 threads) per block. - // Cast kv_cache to 16_bit for RoPE values - scalar_t* kv_cache_16bit = - reinterpret_cast(&kv_cache[dst_idx_start]); - - // Zero the reserved RoPE tail for NoPE rows. if (threadIdx.x >= 64) { + if (!has_rope_tail) { + return; + } + scalar_t* kv_cache_16bit = + reinterpret_cast(&kv_cache[dst_idx_start]); // Each thread handles two elements of RoPE const int8_t pe_idx_start = (threadIdx.x - 64) * 2; // RoPE values start after the packed 8-bit NoPE values and the @@ -826,6 +894,20 @@ void reshape_and_cache( // KV_T is the data type of key and value tensors. // CACHE_T is the stored data type of kv-cache. // KV_DTYPE is the real data type of kv-cache. +#define CALL_RESHAPE_AND_CACHE_FLASH_ASYM(KV_T, CACHE_T, KV_DTYPE) \ + vllm::reshape_and_cache_flash_kernel_asym \ + <<>>( \ + reinterpret_cast(key.data_ptr()), \ + reinterpret_cast(value.data_ptr()), \ + reinterpret_cast(key_cache.data_ptr()), \ + reinterpret_cast(value_cache.data_ptr()), \ + slot_mapping.const_data_ptr(), block_stride, page_stride, \ + head_stride, key_stride, value_stride, num_heads, head_size, \ + v_head_size, block_size, \ + reinterpret_cast(k_scale.data_ptr()), \ + reinterpret_cast(v_scale.data_ptr()), \ + kv_scale_stride); + #define CALL_RESHAPE_AND_CACHE_FLASH(KV_T, CACHE_T, KV_DTYPE) \ vllm::reshape_and_cache_flash_kernel \ <<>>( \ @@ -863,6 +945,7 @@ void reshape_and_cache_flash( int num_tokens = slot_mapping.size(0); int num_heads = key.size(1); int head_size = key.size(2); + int v_head_size = value.size(2); const torch::stable::accelerator::DeviceGuard device_guard( key.get_device_index()); @@ -896,8 +979,8 @@ void reshape_and_cache_flash( int64_t block_stride = key_cache.stride(0); int64_t page_stride = key_cache.stride(1); int64_t head_stride = key_cache.stride(2); - STD_TORCH_CHECK(key_cache.stride(0) == value_cache.stride(0)); + STD_TORCH_CHECK(key_cache.stride(0) == value_cache.stride(0)); STD_TORCH_CHECK(k_scale.sizes().equals(v_scale.sizes()), "k_scale and v_scale must have the same shape"); STD_TORCH_CHECK(k_scale.numel() == 1 || k_scale.numel() == num_heads, @@ -905,10 +988,31 @@ void reshape_and_cache_flash( int kv_scale_stride = (k_scale.numel() > 1) ? 1 : 0; dim3 grid(num_tokens); - dim3 block(std::min(num_heads * head_size, 512)); - - DISPATCH_BY_KV_CACHE_DTYPE(key.scalar_type(), kv_cache_dtype, - CALL_RESHAPE_AND_CACHE_FLASH); + // Only models with v_head_size != head_size take the kernel below; everything + // else keeps using the kernel above, so its codegen is untouched. + if (v_head_size == head_size) { + dim3 block(std::min(num_heads * head_size, 512)); + DISPATCH_BY_KV_CACHE_DTYPE(key.scalar_type(), kv_cache_dtype, + CALL_RESHAPE_AND_CACHE_FLASH); + } else { + // V is a view of K's buffer, so the two must agree on geometry; these + // checks stay out of the symmetric path so its host-side work is unchanged. + STD_TORCH_CHECK(value_cache.stride(1) == page_stride, + "key_cache and value_cache must share the page stride"); + STD_TORCH_CHECK(key_cache.size(0) == value_cache.size(0) && + key_cache.size(1) == value_cache.size(1), + "key_cache and value_cache must share block geometry"); + STD_TORCH_CHECK(key.size(1) == value.size(1), + "key and value must have the same number of heads"); + STD_TORCH_CHECK(value_cache.stride(2) == head_stride, + "key_cache and value_cache must share a head stride"); + // Only the strided walk applies here, and V's width is the single extra + // argument. One warp per head, and at least one warp so the head loop + // always advances. + dim3 block(32 * std::min(num_heads, 16)); + DISPATCH_BY_KV_CACHE_DTYPE(key.scalar_type(), kv_cache_dtype, + CALL_RESHAPE_AND_CACHE_FLASH_ASYM); + } } // KV_T is the data type of key and value tensors. @@ -941,7 +1045,7 @@ void reshape_and_cache_flash( reinterpret_cast(kv_cache.data_ptr()), \ slot_mapping.const_data_ptr(), block_stride, entry_stride, \ kv_c_stride, k_pe_stride, kv_lora_rank, pe_dim, block_size, \ - reinterpret_cast(scale.data_ptr())); + has_rope_tail, reinterpret_cast(scale.data_ptr())); void concat_and_cache_mla( torch::stable::Tensor& kv_c, // [num_tokens, kv_lora_rank] @@ -965,6 +1069,8 @@ void concat_and_cache_mla( int kv_lora_rank = kv_c.size(1); int pe_dim = k_pe.size(1); int block_size = kv_cache.size(1); + const int64_t row_bytes = kv_cache.size(2) * kv_cache.element_size(); + const bool has_rope_tail = row_bytes == 656; const bool is_nvfp4_ds_mla = kv_cache_dtype == "nvfp4_ds_mla"; if (kv_cache_dtype == "fp8_ds_mla") { @@ -972,8 +1078,9 @@ void concat_and_cache_mla( "kv_lora_rank must be 512 for fp8_ds_mla"); STD_TORCH_CHECK(pe_dim == 64 || pe_dim == 0, "pe_dim must be 64 or 0 for fp8_ds_mla"); - STD_TORCH_CHECK(kv_cache.size(2) == 656 / kv_cache.element_size(), - "kv_cache.size(2) must be 656 bytes for fp8_ds_mla"); + STD_TORCH_CHECK(has_rope_tail || (pe_dim == 0 && row_bytes == 528), + "fp8_ds_mla requires 656-byte rows, or 528-byte rows with " + "pe_dim == 0"); STD_TORCH_CHECK(kv_c.element_size() == 2, "kv_c.element_size() must be 2 for fp8_ds_mla"); STD_TORCH_CHECK(k_pe.element_size() == 2, @@ -1011,8 +1118,8 @@ void concat_and_cache_mla( // For the NoPE part, each tile of 128 elements is handled by half of one // warp (16 threads). There are 4 total tiles, so 2 warps (64 threads). // Lanes 0 and 16 of each warp write the scale values for that warp's tiles. - // The RoPE part (last 64 elements) is handled by another 1 warp (32 - // threads). So in total, we use 3 warps (96 threads) per block. + // The optional RoPE tail is handled by another 1 warp (32 threads). + // So in total, we use 3 warps (96 threads) per block. dim3 block(96); DISPATCH_BY_KV_CACHE_DTYPE(kv_c.scalar_type(), kv_cache_dtype, CALL_CONCAT_AND_CACHE_DS_MLA); @@ -1837,6 +1944,8 @@ void cp_gather_and_upconvert_fp8_kv_cache( STD_TORCH_CHECK(dst.scalar_type() == torch::headeronly::ScalarType::BFloat16, "dst must be bfloat16"); STD_TORCH_CHECK(head_dim == 576, "head_dim must be 576 for MLA"); + STD_TORCH_CHECK(src_cache.size(2) * src_cache.element_size() >= 656, + "src_cache rows must contain at least 656 bytes for MLA"); int64_t block_table_stride = block_table.stride(0); int64_t cache_block_stride = src_cache.stride(0); diff --git a/csrc/cpu/block_table.cpp b/csrc/cpu/block_table.cpp new file mode 100644 index 000000000000..ac8f21d44d6e --- /dev/null +++ b/csrc/cpu/block_table.cpp @@ -0,0 +1,73 @@ +#include "cpu_types.hpp" + +#include +#include + +namespace cpu_utils { + +void gather_block_table_cpu(const torch::Tensor& block_table, + torch::Tensor& output, + const torch::Tensor& idx_mapping, + const torch::Tensor& num_blocks, + const int64_t num_reqs_padded) { + const int32_t* block_table_ptr = block_table.data_ptr(); + int32_t* output_ptr = output.data_ptr(); + const int64_t* idx_mapping_ptr = idx_mapping.data_ptr(); + const int32_t* num_blocks_ptr = num_blocks.data_ptr(); + + const int64_t num_reqs = idx_mapping.size(0); + const int64_t block_table_stride = block_table.stride(0); + const int64_t output_stride = output.stride(0); + for (int64_t batch_idx = 0; batch_idx < num_reqs; ++batch_idx) { + const int64_t req_idx = idx_mapping_ptr[batch_idx]; + const int64_t request_num_blocks = num_blocks_ptr[req_idx]; + std::memcpy(output_ptr + batch_idx * output_stride, + block_table_ptr + req_idx * block_table_stride, + request_num_blocks * sizeof(int32_t)); + } + + for (int64_t batch_idx = num_reqs; batch_idx < num_reqs_padded; ++batch_idx) { + std::memset(output_ptr + batch_idx * output_stride, 0, + output.size(1) * sizeof(int32_t)); + } +} + +void compute_slot_mapping_cpu( + const torch::Tensor& block_table, torch::Tensor& slot_mapping, + const torch::Tensor& idx_mapping, const torch::Tensor& query_start_loc, + const torch::Tensor& positions, const int64_t block_size, + const bool mapping_enabled, const int64_t pad_slot_id) { + const int32_t* block_table_ptr = block_table.data_ptr(); + int64_t* slot_mapping_ptr = slot_mapping.data_ptr(); + const int64_t* idx_mapping_ptr = idx_mapping.data_ptr(); + const int32_t* query_start_loc_ptr = query_start_loc.data_ptr(); + const int64_t* positions_ptr = positions.data_ptr(); + + const int64_t num_reqs = idx_mapping.size(0); + const int64_t num_tokens = query_start_loc_ptr[num_reqs]; + const int64_t block_table_stride = block_table.stride(0); + std::fill(slot_mapping_ptr + num_tokens, + slot_mapping_ptr + slot_mapping.numel(), pad_slot_id); + + for (int64_t batch_idx = 0; batch_idx < num_reqs; ++batch_idx) { + const int64_t req_state_idx = idx_mapping_ptr[batch_idx]; + const int64_t start = query_start_loc_ptr[batch_idx]; + const int64_t end = query_start_loc_ptr[batch_idx + 1]; + if (!mapping_enabled || req_state_idx < 0) { + std::fill(slot_mapping_ptr + start, slot_mapping_ptr + end, pad_slot_id); + continue; + } + + for (int64_t token_idx = start; token_idx < end; ++token_idx) { + const int64_t position = positions_ptr[token_idx]; + const int64_t block_idx = position / block_size; + const int64_t block_offset = position % block_size; + const int32_t block_number = + block_table_ptr[req_state_idx * block_table_stride + block_idx]; + slot_mapping_ptr[token_idx] = + static_cast(block_number) * block_size + block_offset; + } + } +} + +} // namespace cpu_utils diff --git a/csrc/cpu/cpu_types_scalar.hpp b/csrc/cpu/cpu_types_scalar.hpp index d94cdbc076e8..76b3aec5d45e 100644 --- a/csrc/cpu/cpu_types_scalar.hpp +++ b/csrc/cpu/cpu_types_scalar.hpp @@ -502,7 +502,7 @@ inline FP16Vec16::FP16Vec16(const FP32Vec16& v) { [&v, this](int i) { reg.val[i] = float_to_fp16(v.reg.val[i]); }); } -inline FP16Vec8 ::FP16Vec8(const FP32Vec8& v) { +inline FP16Vec8::FP16Vec8(const FP32Vec8& v) { unroll_loop( [&v, this](int i) { reg.val[i] = float_to_fp16(v.reg.val[i]); }); } diff --git a/csrc/cpu/input_batch.cpp b/csrc/cpu/input_batch.cpp new file mode 100644 index 000000000000..8f04e05b4d2b --- /dev/null +++ b/csrc/cpu/input_batch.cpp @@ -0,0 +1,295 @@ +#include "cpu_types.hpp" + +#include +#include +#include + +namespace cpu_utils { + +void prepare_prefill_inputs_cpu(torch::Tensor& input_ids, + torch::Tensor& next_prefill_tokens, + const torch::Tensor& idx_mapping, + const torch::Tensor& query_start_loc, + const torch::Tensor& all_token_ids, + const torch::Tensor& prefill_len, + const torch::Tensor& num_computed_tokens) { + int32_t* input_ids_ptr = input_ids.data_ptr(); + int32_t* next_prefill_tokens_ptr = next_prefill_tokens.data_ptr(); + const int64_t* idx_mapping_ptr = idx_mapping.data_ptr(); + const int32_t* query_start_loc_ptr = query_start_loc.data_ptr(); + const int32_t* all_token_ids_ptr = all_token_ids.data_ptr(); + const int32_t* prefill_len_ptr = prefill_len.data_ptr(); + const int32_t* num_computed_tokens_ptr = + num_computed_tokens.data_ptr(); + + const int64_t num_reqs = idx_mapping.size(0); + const int64_t num_lookahead = next_prefill_tokens.size(0); + const int64_t next_prefill_tokens_stride = next_prefill_tokens.stride(0); + const int64_t all_token_ids_stride = all_token_ids.stride(0); + + for (int64_t batch_idx = 0; batch_idx < num_reqs; ++batch_idx) { + const int64_t req_state_idx = idx_mapping_ptr[batch_idx]; + const int32_t request_prefill_len = prefill_len_ptr[req_state_idx]; + const int32_t num_computed = num_computed_tokens_ptr[req_state_idx]; + if (num_computed >= request_prefill_len) { + continue; + } + + const int32_t query_start = query_start_loc_ptr[batch_idx]; + const int32_t query_end = query_start_loc_ptr[batch_idx + 1]; + const int32_t query_len = query_end - query_start; + const int32_t* request_tokens = + all_token_ids_ptr + req_state_idx * all_token_ids_stride; + std::memcpy(input_ids_ptr + query_start, request_tokens + num_computed, + query_len * sizeof(int32_t)); + + const int64_t lookahead_start = + static_cast(num_computed) + query_len; + for (int64_t lookahead = 0; lookahead < num_lookahead; ++lookahead) { + const int64_t pos = lookahead_start + lookahead; + next_prefill_tokens_ptr[lookahead * next_prefill_tokens_stride + + req_state_idx] = + pos < request_prefill_len ? request_tokens[pos] : 0; + } + } +} + +torch::Tensor get_prompt_logprobs_token_ids_cpu( + const int64_t num_tokens, const torch::Tensor& query_start_loc, + const torch::Tensor& idx_mapping, const torch::Tensor& num_computed_tokens, + const torch::Tensor& all_token_ids) { + torch::Tensor token_ids = + torch::empty({num_tokens}, idx_mapping.options().dtype(torch::kInt64)); + + int64_t* token_ids_ptr = token_ids.data_ptr(); + const int32_t* query_start_loc_ptr = query_start_loc.data_ptr(); + const int64_t* idx_mapping_ptr = idx_mapping.data_ptr(); + const int32_t* num_computed_tokens_ptr = + num_computed_tokens.data_ptr(); + const int32_t* all_token_ids_ptr = all_token_ids.data_ptr(); + const int64_t all_token_ids_stride = all_token_ids.stride(0); + + for (int64_t batch_idx = 0; batch_idx < idx_mapping.size(0); ++batch_idx) { + const int64_t req_state_idx = idx_mapping_ptr[batch_idx]; + const int32_t query_start = query_start_loc_ptr[batch_idx]; + const int32_t query_end = query_start_loc_ptr[batch_idx + 1]; + const int32_t target_start = num_computed_tokens_ptr[req_state_idx] + 1; + const int32_t* request_tokens = + all_token_ids_ptr + req_state_idx * all_token_ids_stride; + + std::transform(request_tokens + target_start, + request_tokens + target_start + query_end - query_start, + token_ids_ptr + query_start, [](int32_t token_id) { + return static_cast(token_id); + }); + } + return token_ids; +} + +void prepare_pos_seq_lens_cpu(const torch::Tensor& idx_mapping, + const torch::Tensor& query_start_loc, + const torch::Tensor& num_computed_tokens, + torch::Tensor& pos, torch::Tensor& seq_lens) { + const int64_t* idx_mapping_ptr = idx_mapping.data_ptr(); + const int32_t* query_start_loc_ptr = query_start_loc.data_ptr(); + const int32_t* num_computed_tokens_ptr = + num_computed_tokens.data_ptr(); + int64_t* pos_ptr = pos.data_ptr(); + int32_t* seq_lens_ptr = seq_lens.data_ptr(); + + const int64_t num_reqs = idx_mapping.size(0); + std::fill(seq_lens_ptr + num_reqs, seq_lens_ptr + seq_lens.numel(), 0); + + for (int64_t req_idx = 0; req_idx < num_reqs; ++req_idx) { + const int64_t req_state_idx = idx_mapping_ptr[req_idx]; + const int32_t num_computed = num_computed_tokens_ptr[req_state_idx]; + const int64_t start = query_start_loc_ptr[req_idx]; + const int64_t end = query_start_loc_ptr[req_idx + 1]; + + seq_lens_ptr[req_idx] = num_computed + static_cast(end - start); + std::iota(pos_ptr + start, pos_ptr + end, + static_cast(num_computed)); + } +} + +void prepare_mrope_positions_cpu(torch::Tensor& positions, + const torch::Tensor& prefill_positions, + const torch::Tensor& prefill_delta, + const torch::Tensor& idx_mapping, + const torch::Tensor& query_start_loc, + const torch::Tensor& prefill_lens, + const torch::Tensor& num_computed_tokens) { + int64_t* positions_ptr = positions.data_ptr(); + const int32_t* prefill_positions_ptr = prefill_positions.data_ptr(); + const int32_t* prefill_delta_ptr = prefill_delta.data_ptr(); + const int64_t* idx_mapping_ptr = idx_mapping.data_ptr(); + const int32_t* query_start_loc_ptr = query_start_loc.data_ptr(); + const int32_t* prefill_lens_ptr = prefill_lens.data_ptr(); + const int32_t* num_computed_tokens_ptr = + num_computed_tokens.data_ptr(); + + const int64_t num_reqs = idx_mapping.size(0); + const int64_t num_dims = positions.size(0); + const int64_t positions_stride0 = positions.stride(0); + const int64_t positions_stride1 = positions.stride(1); + const int64_t prefill_positions_stride0 = prefill_positions.stride(0); + const int64_t prefill_positions_stride1 = prefill_positions.stride(1); + + for (int64_t batch_idx = 0; batch_idx < num_reqs; ++batch_idx) { + const int64_t req_state_idx = idx_mapping_ptr[batch_idx]; + const int32_t query_start = query_start_loc_ptr[batch_idx]; + const int32_t query_end = query_start_loc_ptr[batch_idx + 1]; + const int32_t query_len = query_end - query_start; + const int32_t num_computed = num_computed_tokens_ptr[req_state_idx]; + const int32_t prompt_len = std::min( + query_len, std::max(prefill_lens_ptr[req_state_idx] - num_computed, 0)); + const int32_t delta = prefill_delta_ptr[req_state_idx]; + + for (int64_t dim = 0; dim < num_dims; ++dim) { + int64_t* output = positions_ptr + dim * positions_stride0 + + query_start * positions_stride1; + const int32_t* prompt = + prefill_positions_ptr + + (req_state_idx * num_dims + dim) * prefill_positions_stride0; + + for (int32_t token_idx = 0; token_idx < prompt_len; ++token_idx) { + output[token_idx * positions_stride1] = + prompt[(num_computed + token_idx) * prefill_positions_stride1]; + } + for (int32_t token_idx = prompt_len; token_idx < query_len; ++token_idx) { + output[token_idx * positions_stride1] = + num_computed + token_idx + delta; + } + } + } +} + +torch::Tensor combine_sampled_and_draft_tokens_cpu( + torch::Tensor& input_ids, const torch::Tensor& idx_mapping, + const torch::Tensor& last_sampled_tokens, + const torch::Tensor& query_start_loc, const torch::Tensor& seq_lens, + const torch::Tensor& prefill_len, const torch::Tensor& draft_tokens, + const torch::Tensor& cu_num_logits, const int64_t num_logits, + const int64_t num_new_sampled_tokens) { + const int64_t* idx_mapping_ptr = idx_mapping.data_ptr(); + const int64_t* last_sampled_tokens_ptr = + last_sampled_tokens.data_ptr(); + const int32_t* query_start_loc_ptr = query_start_loc.data_ptr(); + const int32_t* seq_lens_ptr = seq_lens.data_ptr(); + const int32_t* prefill_len_ptr = prefill_len.data_ptr(); + const int64_t* draft_tokens_ptr = draft_tokens.data_ptr(); + const int32_t* cu_num_logits_ptr = cu_num_logits.data_ptr(); + int32_t* input_ids_ptr = input_ids.data_ptr(); + + torch::Tensor logits_indices = + torch::empty({num_logits}, input_ids.options().dtype(torch::kInt64)); + int64_t* logits_indices_ptr = logits_indices.data_ptr(); + + const int64_t num_reqs = idx_mapping.size(0); + const int64_t draft_tokens_stride = draft_tokens.stride(0); + for (int64_t req_idx = 0; req_idx < num_reqs; ++req_idx) { + const int64_t req_state_idx = idx_mapping_ptr[req_idx]; + const int64_t logits_offset = cu_num_logits_ptr[req_idx]; + const int64_t logits_offset_end = cu_num_logits_ptr[req_idx + 1]; + const int64_t request_num_logits = logits_offset_end - logits_offset; + const int64_t num_draft_tokens = + request_num_logits - num_new_sampled_tokens; + const int64_t query_end = query_start_loc_ptr[req_idx + 1]; + const int64_t logits_start = query_end - request_num_logits; + + std::iota(logits_indices_ptr + logits_offset, + logits_indices_ptr + logits_offset_end, logits_start); + + const int32_t seq_len = seq_lens_ptr[req_idx]; + const int32_t request_prefill_len = prefill_len_ptr[req_state_idx]; + if (seq_len <= request_prefill_len) { + continue; + } + + const int64_t first_logit_seq_pos = seq_len - request_num_logits; + if (num_new_sampled_tokens > 0 && + first_logit_seq_pos >= request_prefill_len) { + input_ids_ptr[logits_start] = + static_cast(last_sampled_tokens_ptr[req_state_idx]); + } + + const int64_t draft_start = query_end - num_draft_tokens; + for (int64_t i = 0; i < num_draft_tokens; ++i) { + input_ids_ptr[draft_start + i] = static_cast( + draft_tokens_ptr[req_state_idx * draft_tokens_stride + i]); + } + } + + return logits_indices; +} + +void post_update_cpu(const torch::Tensor& idx_mapping, + torch::Tensor& num_computed_tokens, + torch::Tensor& last_sampled_tokens, + const c10::optional& output_bin_counts, + const torch::Tensor& sampled_tokens, + const torch::Tensor& num_sampled, + const torch::Tensor& num_rejected, + const c10::optional& query_start_loc, + torch::Tensor& all_token_ids, torch::Tensor& total_len) { + const int64_t* idx_mapping_ptr = idx_mapping.data_ptr(); + int32_t* num_computed_tokens_ptr = num_computed_tokens.data_ptr(); + int64_t* last_sampled_tokens_ptr = last_sampled_tokens.data_ptr(); + int32_t* output_bin_counts_ptr = + output_bin_counts ? output_bin_counts->data_ptr() : nullptr; + const int64_t* sampled_tokens_ptr = sampled_tokens.data_ptr(); + const int32_t* num_sampled_ptr = num_sampled.data_ptr(); + const int32_t* num_rejected_ptr = num_rejected.data_ptr(); + const int32_t* query_start_loc_ptr = + query_start_loc ? query_start_loc->data_ptr() : nullptr; + int32_t* all_token_ids_ptr = all_token_ids.data_ptr(); + int32_t* total_len_ptr = total_len.data_ptr(); + + const int64_t num_reqs = idx_mapping.size(0); + const int64_t last_sampled_tokens_stride = last_sampled_tokens.stride(0); + const int64_t output_bin_counts_stride = + output_bin_counts ? output_bin_counts->stride(0) : 0; + const int64_t sampled_tokens_stride = sampled_tokens.stride(0); + const int64_t all_token_ids_stride = all_token_ids.stride(0); + + for (int64_t req_idx = 0; req_idx < num_reqs; ++req_idx) { + const int64_t req_state_idx = idx_mapping_ptr[req_idx]; + if (req_state_idx < 0) { + continue; + } + + const int32_t request_total_len = total_len_ptr[req_state_idx]; + const int32_t request_num_sampled = num_sampled_ptr[req_idx]; + const int64_t* request_sampled_tokens = + sampled_tokens_ptr + req_idx * sampled_tokens_stride; + int32_t* request_all_token_ids = + all_token_ids_ptr + req_state_idx * all_token_ids_stride; + + if (request_num_sampled > 0) { + last_sampled_tokens_ptr[req_state_idx * last_sampled_tokens_stride] = + request_sampled_tokens[request_num_sampled - 1]; + total_len_ptr[req_state_idx] = request_total_len + request_num_sampled; + } + + for (int32_t i = 0; i < request_num_sampled; ++i) { + const int64_t token_id = request_sampled_tokens[i]; + request_all_token_ids[request_total_len + i] = + static_cast(token_id); + if (output_bin_counts_ptr != nullptr) { + ++output_bin_counts_ptr[req_state_idx * output_bin_counts_stride + + token_id]; + } + } + + const int32_t query_len = + query_start_loc_ptr == nullptr + ? 0 + : query_start_loc_ptr[req_idx + 1] - query_start_loc_ptr[req_idx]; + const int32_t computed_delta = query_len - num_rejected_ptr[req_idx]; + if (computed_delta != 0) { + num_computed_tokens_ptr[req_state_idx] += computed_delta; + } + } +} + +} // namespace cpu_utils diff --git a/csrc/cpu/sampling/common.h b/csrc/cpu/sampling/common.h new file mode 100644 index 000000000000..d18da0079c1e --- /dev/null +++ b/csrc/cpu/sampling/common.h @@ -0,0 +1,279 @@ +#pragma once + +#include "../utils.hpp" + +#include +#include +#include +#include +#include +#include + +namespace cpu_sampling { + +// Draft proposals and target verification must draw from disjoint noise +// streams even when they use the same request seed and token position. +inline constexpr int64_t DRAFT_NOISE_SALT = 1 << 30; + +inline uint32_t murmur3_rotl32(uint32_t value, int shift) { + return (value << shift) | (value >> (32 - shift)); +} + +inline uint32_t murmur3_mix(uint32_t hash, uint32_t key) { + key *= 0xcc9e2d51; + key = murmur3_rotl32(key, 15); + key *= 0x1b873593; + hash ^= key; + hash = murmur3_rotl32(hash, 13); + return hash * 5 + 0xe6546b64; +} + +inline uint32_t murmur3_fmix32(uint32_t hash) { + hash ^= hash >> 16; + hash *= 0x85ebca6b; + hash ^= hash >> 13; + hash *= 0xc2b2ae35; + return hash ^ (hash >> 16); +} + +inline uint32_t murmur3_hash32_prefix(int64_t seed, int64_t position, + uint32_t domain) { + // Seed and position are shared by every vocabulary entry in a row. Mix this + // prefix once so the inner loop only needs to mix the token ID and finalize. + const uint64_t seed_bits = static_cast(seed); + uint32_t hash = domain; + hash = murmur3_mix(hash, static_cast(seed_bits)); + hash = murmur3_mix(hash, static_cast(seed_bits >> 32)); + hash = murmur3_mix(hash, static_cast(position)); + return hash; +} + +inline uint32_t murmur3_hash32(int64_t seed, int64_t position, + uint32_t token_id, uint32_t domain = 0) { + uint32_t hash = murmur3_hash32_prefix(seed, position, domain); + hash = murmur3_mix(hash, token_id); + return murmur3_fmix32(hash ^ 16); +} + +inline float murmur3_uniform32(int64_t seed, int64_t position, + uint32_t token_id) { + const uint32_t random = murmur3_hash32(seed, position, token_id); + // Splitting the hash makes both integer-to-float conversions exact: + // u = round_fp32((random + 0.5) / 2^32). + // Converting random directly to FP32 could discard up to eight low bits. + const int32_t high = static_cast(random >> 16); + const int32_t low = static_cast(random & 0xffff); + return static_cast(high) * 1.52587890625e-05f + + (static_cast(low) + 0.5f) * 2.3283064365386963e-10f; +} + +inline double murmur3_uniform64(int64_t seed, int64_t position, + uint32_t token_id) { + const uint64_t low = murmur3_hash32(seed, position, token_id); + const uint64_t high = murmur3_hash32(seed, position, token_id, 0x9e3779b9); + // Combine two hashes and retain the 53 bits representable by an FP64 + // significand: u = (random53 + 0.5) / 2^53. + const uint64_t random53 = ((high << 32) | low) >> 11; + double uniform = (static_cast(random53) + 0.5) * 0x1p-53; + if (uniform == 1.0) uniform -= 0x1p-53; + return uniform; +} + +inline float log1p_neg_stable(float value) { + // This is inherited from the triton impl: + // For small x, evaluate the degree-8 expansion + // log(1-x) = -x * (1 + x/2 + x^2/3 + ... + x^7/8) + // instead of forming 1-x, which loses the low bits of x. + float polynomial = 1.0f / 8.0f; + polynomial = 1.0f / 7.0f + value * polynomial; + polynomial = 1.0f / 6.0f + value * polynomial; + polynomial = 1.0f / 5.0f + value * polynomial; + polynomial = 1.0f / 4.0f + value * polynomial; + polynomial = 1.0f / 3.0f + value * polynomial; + polynomial = 1.0f / 2.0f + value * polynomial; + polynomial = 1.0f + value * polynomial; + const float series = -value * polynomial; + const float direct = std::log(std::max(1.0f - value, 5.960464477539063e-08f)); + return value < 0.25f ? series : direct; +} + +inline float gumbel_noise32(int64_t seed, int64_t position, uint32_t token_id) { + const float uniform = murmur3_uniform32(seed, position, token_id); + return -std::log(-log1p_neg_stable(uniform)); +} + +inline double gumbel_noise64(int64_t seed, int64_t position, + uint32_t token_id) { + const double uniform = murmur3_uniform64(seed, position, token_id); + return -std::log(-std::log(uniform)); +} + +template +void scalar_greedy_argmax(const scalar_t* logits, int64_t token_begin, + int64_t token_end, float& best_score, + int64_t& best_idx) { + best_score = -std::numeric_limits::infinity(); + best_idx = token_begin; + for (int64_t token_id = token_begin; token_id < token_end; ++token_id) { + const float score = static_cast(logits[token_id]); + if (score > best_score) { + best_score = score; + best_idx = token_id; + } + } +} + +template +void scalar_gumbel_argmax(const scalar_t* logits, int64_t token_begin, + int64_t token_end, int64_t seed, int64_t position, + float temperature, score_t& best_score, + int64_t& best_idx) { + best_score = -std::numeric_limits::infinity(); + best_idx = token_begin; + + for (int64_t token_id = token_begin; token_id < token_end; ++token_id) { + float logit = static_cast(logits[token_id]); + if constexpr (APPLY_TEMPERATURE) logit /= temperature; + + score_t noise; + if constexpr (std::is_same_v) { + noise = gumbel_noise64(seed, position, token_id); + } else { + noise = gumbel_noise32(seed, position, token_id); + } + const score_t score = static_cast(logit) + noise; + if (score > best_score) { + best_score = score; + best_idx = token_id; + } + } +} + +template +class SamplingImpl; + +template +class SamplingMainLoop { + public: + using scalar_t = typename sampling_impl_t::scalar_type; + + void operator()(int64_t* output, const scalar_t* logits, + int64_t logits_stride, const int64_t* expanded_idx_mapping, + const float* temperature, const int64_t* seeds, + const int64_t* positions, int64_t num_tokens, + int64_t vocab_size, bool apply_temperature, bool is_drafting, + bool use_fp64) const { + if (num_tokens == 0) return; + + // Resolve runtime options before entering the vocabulary loop so its + // temperature and score-type branches are compile-time specializations. + if (apply_temperature) { + if (use_fp64) { + execute(output, logits, logits_stride, + expanded_idx_mapping, temperature, seeds, + positions, num_tokens, vocab_size, is_drafting); + } else { + execute(output, logits, logits_stride, + expanded_idx_mapping, temperature, seeds, + positions, num_tokens, vocab_size, is_drafting); + } + } else if (use_fp64) { + execute(output, logits, logits_stride, + expanded_idx_mapping, temperature, seeds, + positions, num_tokens, vocab_size, is_drafting); + } else { + execute(output, logits, logits_stride, expanded_idx_mapping, + temperature, seeds, positions, num_tokens, + vocab_size, is_drafting); + } + } + + private: + template + void execute(int64_t* output, const scalar_t* logits, int64_t logits_stride, + const int64_t* expanded_idx_mapping, const float* temperature, + const int64_t* seeds, const int64_t* positions, + int64_t num_tokens, int64_t vocab_size, bool is_drafting) const { + // Parallelize across logit rows first; num_tokens is not necessarily the + // request count. When there are fewer rows than workers, split each row to + // expose roughly one task per worker. Keep ranges large enough to amortize + // queue overhead and align them to an ISA-chosen block multiple (32 on + // NEON) so range boundaries do not create extra scalar tails. + constexpr int64_t MIN_RANGE_SIZE = 2048; + constexpr int64_t RANGE_ALIGNMENT = sampling_impl_t::RangeAlignment; + static_assert(RANGE_ALIGNMENT > 0); + + const int max_workers = cpu_utils::get_max_threads(); + const int64_t target_ranges_per_row = + std::max(1, (max_workers + num_tokens - 1) / num_tokens); + const int64_t unaligned_range_size = std::max( + MIN_RANGE_SIZE, + (vocab_size + target_ranges_per_row - 1) / target_ranges_per_row); + const int64_t range_size = (unaligned_range_size + RANGE_ALIGNMENT - 1) / + RANGE_ALIGNMENT * RANGE_ALIGNMENT; + const int64_t num_ranges = (vocab_size + range_size - 1) / range_size; + const int64_t num_tasks = num_tokens * num_ranges; + + std::vector range_scores(num_tasks); + std::vector range_indices(num_tasks); + alignas(64) cpu_utils::Counter task_counter; + const sampling_impl_t sampling_impl; + +#pragma omp parallel + { + // Phase 1: find one winner per vocabulary range of each row. + for (;;) { + const int64_t task_id = task_counter.acquire_counter(); + if (task_id >= num_tasks) break; + + const int64_t token_idx = task_id / num_ranges; + const int64_t range_idx = task_id % num_ranges; + const int64_t token_begin = range_idx * range_size; + const int64_t token_end = + std::min(token_begin + range_size, vocab_size); + const int64_t req_state_idx = expanded_idx_mapping[token_idx]; + const bool valid_request = req_state_idx >= 0; + const float temp = valid_request ? temperature[req_state_idx] : 0.0f; + const scalar_t* row = logits + token_idx * logits_stride; + + if (temp == 0.0f) { + float best_score; + sampling_impl.greedy_argmax(row, token_begin, token_end, best_score, + range_indices[task_id]); + range_scores[task_id] = best_score; + } else { + int64_t position = positions[token_idx]; + if (is_drafting) position += DRAFT_NOISE_SALT; + sampling_impl.template gumbel_argmax( + row, token_begin, token_end, seeds[req_state_idx], position, temp, + range_scores[task_id], range_indices[task_id]); + } + } + + // Every range winner must be visible before its row is reduced. +#pragma omp barrier + // Phase 1 found the best token in each vocabulary range. Phase 2 assigns + // each row to one worker, which compares its range winners to find the + // best token for the complete vocabulary. Ranges are visited in order, + // and the strict comparison retains the first token when scores tie. +#pragma omp for schedule(static) + for (int64_t token_idx = 0; token_idx < num_tokens; ++token_idx) { + score_t best_score = -std::numeric_limits::infinity(); + int64_t best_idx = 0; + const int64_t first_range = token_idx * num_ranges; + // Vectorizing this isn't worthwhile because each row has few ranges. + for (int64_t range_idx = 0; range_idx < num_ranges; ++range_idx) { + const int64_t task_id = first_range + range_idx; + const score_t score = range_scores[task_id]; + if (score > best_score) { + best_score = score; + best_idx = range_indices[task_id]; + } + } + output[token_idx] = best_idx; + } + } + } +}; + +} // namespace cpu_sampling diff --git a/csrc/cpu/sampling/neon.h b/csrc/cpu/sampling/neon.h new file mode 100644 index 000000000000..b4808d5c3c09 --- /dev/null +++ b/csrc/cpu/sampling/neon.h @@ -0,0 +1,395 @@ +#pragma once + +#include "common.h" + +#include +#include + +#include +#include +#include + +namespace cpu_sampling::neon { + +FORCE_INLINE uint32x4_t murmur3_hash32x4(uint32x4_t hash, + uint32x4_t token_ids) { + // `hash` contains the seed/position prefix. Mix four token IDs and finalize + // four independent hashes, lane-for-lane matching murmur3_hash32(). + token_ids = vmulq_n_u32(token_ids, 0xcc9e2d51); + token_ids = vorrq_u32(vshlq_n_u32(token_ids, 15), vshrq_n_u32(token_ids, 17)); + token_ids = vmulq_n_u32(token_ids, 0x1b873593); + hash = veorq_u32(hash, token_ids); + hash = vorrq_u32(vshlq_n_u32(hash, 13), vshrq_n_u32(hash, 19)); + hash = vaddq_u32(vmulq_n_u32(hash, 5), vdupq_n_u32(0xe6546b64)); + hash = veorq_u32(hash, vdupq_n_u32(16)); + hash = veorq_u32(hash, vshrq_n_u32(hash, 16)); + hash = vmulq_n_u32(hash, 0x85ebca6b); + hash = veorq_u32(hash, vshrq_n_u32(hash, 13)); + hash = vmulq_n_u32(hash, 0xc2b2ae35); + return veorq_u32(hash, vshrq_n_u32(hash, 16)); +} + +FORCE_INLINE float32x4_t murmur3_uniform32x4(uint32x4_t hash, + uint32x4_t token_ids) { + const uint32x4_t random = murmur3_hash32x4(hash, token_ids); + // Evaluate u = round_fp32((random + 0.5) / 2^32) without first rounding the + // entire 32-bit integer to FP32. + const uint32x4_t high = vshrq_n_u32(random, 16); + const uint32x4_t low = vandq_u32(random, vdupq_n_u32(0xffff)); + return vmlaq_n_f32(vmulq_n_f32(vcvtq_f32_u32(high), 1.52587890625e-05f), + vaddq_f32(vcvtq_f32_u32(low), vdupq_n_f32(0.5f)), + 2.3283064365386963e-10f); +} + +// Adapted from data and inline_logf in Arm Optimized Routines: +// math/aarch64/advsimd/logf.c at commit +// 59afb6fed4275e2fc249df49fce43ee5e75af178. +// https://github.com/ARM-software/optimized-routines +struct PositiveNormalLogData { + float32x4_t c2; + float32x4_t c4; + float32x4_t c6; + float32x4_t ln2; + uint32x4_t offset; + uint32x4_t mantissa_mask; + float32x4_t odd_coefficients; +}; + +constexpr PositiveNormalLogData kPositiveNormalLogData = { + .c2 = {-0x1.4f9934p-3f, -0x1.4f9934p-3f, -0x1.4f9934p-3f, -0x1.4f9934p-3f}, + .c4 = {-0x1.00187cp-2f, -0x1.00187cp-2f, -0x1.00187cp-2f, -0x1.00187cp-2f}, + .c6 = {-0x1.ffffc8p-2f, -0x1.ffffc8p-2f, -0x1.ffffc8p-2f, -0x1.ffffc8p-2f}, + .ln2 = {0x1.62e43p-1f, 0x1.62e43p-1f, 0x1.62e43p-1f, 0x1.62e43p-1f}, + .offset = {0x3f2aaaab, 0x3f2aaaab, 0x3f2aaaab, 0x3f2aaaab}, + .mantissa_mask = {0x007fffff, 0x007fffff, 0x007fffff, 0x007fffff}, + .odd_coefficients = {0x1.5a9aa2p-3f, 0x1.961348p-3f, 0x1.555d7cp-2f, + -0x1.3e737cp-3f}, +}; + +// The midpoint conversion produces u = round_fp32((hash + 0.5) / 2^32), so +// 2^-33 <= u <= 1. The direct inner call is clamped to [2^-24, 1], and the +// value passed to the outer call is either the positive series result or +// -log(max(1-u, 2^-24)), approximately in [2^-33, 24*ln(2)]. Both inputs are +// therefore finite positive normal FP32 values (the smallest normal is +// 2^-126), so no special-value path is needed. +FORCE_INLINE float32x4_t positive_normal_log(float32x4_t value) { + const PositiveNormalLogData& data = kPositiveNormalLogData; + // Reduce x to x = 2^n * (1+r), evaluate the log1p polynomial in r, then + // reconstruct log(x) = n*log(2) + log1p(r). + const uint32x4_t offset_value = + vsubq_u32(vreinterpretq_u32_f32(value), data.offset); + const float32x4_t exponent = + vcvtq_f32_s32(vshrq_n_s32(vreinterpretq_s32_u32(offset_value), 23)); + const uint32x4_t mantissa = + vaddq_u32(vandq_u32(offset_value, data.mantissa_mask), data.offset); + const float32x4_t reduced = + vsubq_f32(vreinterpretq_f32_u32(mantissa), vdupq_n_f32(1.0f)); + const float32x4_t reduced_squared = vmulq_f32(reduced, reduced); + + float32x4_t first = + vfmaq_laneq_f32(data.c2, reduced, data.odd_coefficients, 0); + float32x4_t second = + vfmaq_laneq_f32(data.c4, reduced, data.odd_coefficients, 1); + float32x4_t third = + vfmaq_laneq_f32(data.c6, reduced, data.odd_coefficients, 2); + first = vfmaq_laneq_f32(first, reduced_squared, data.odd_coefficients, 3); + second = vfmaq_f32(second, first, reduced_squared); + third = vfmaq_f32(third, second, reduced_squared); + const float32x4_t leading = vfmaq_f32(reduced, data.ln2, exponent); + return vfmaq_f32(leading, third, reduced_squared); +} + +FORCE_INLINE float32x4_t log1p_neg_stable(float32x4_t value) { + // Each lane evaluates log(1-x). Use the degree-8 series near zero and the + // direct logarithm elsewhere; vbsl selects per lane without scalar branches. + float32x4_t polynomial = vdupq_n_f32(1.0f / 8.0f); + polynomial = vfmaq_f32(vdupq_n_f32(1.0f / 7.0f), value, polynomial); + polynomial = vfmaq_f32(vdupq_n_f32(1.0f / 6.0f), value, polynomial); + polynomial = vfmaq_f32(vdupq_n_f32(1.0f / 5.0f), value, polynomial); + polynomial = vfmaq_f32(vdupq_n_f32(1.0f / 4.0f), value, polynomial); + polynomial = vfmaq_f32(vdupq_n_f32(1.0f / 3.0f), value, polynomial); + polynomial = vfmaq_f32(vdupq_n_f32(1.0f / 2.0f), value, polynomial); + polynomial = vfmaq_f32(vdupq_n_f32(1.0f), value, polynomial); + const float32x4_t series = vnegq_f32(vmulq_f32(value, polynomial)); + const float32x4_t direct = + positive_normal_log(vmaxq_f32(vsubq_f32(vdupq_n_f32(1.0f), value), + vdupq_n_f32(5.960464477539063e-08f))); + return vbslq_f32(vcltq_f32(value, vdupq_n_f32(0.25f)), series, direct); +} + +FORCE_INLINE float32x4x2_t load_logits(const float* logits) { + return {vld1q_f32(logits), vld1q_f32(logits + 4)}; +} + +FORCE_INLINE float32x4x2_t load_logits(const c10::Half* logits) { + const auto values = vld1q_f16(reinterpret_cast(logits)); + return {vcvt_f32_f16(vget_low_f16(values)), + vcvt_f32_f16(vget_high_f16(values))}; +} + +FORCE_INLINE float32x4x2_t load_logits(const c10::BFloat16* logits) { +#ifdef ARM_BF16_SUPPORT + const auto values = vld1q_bf16(reinterpret_cast(logits)); + return {vcvt_f32_bf16(vget_low_bf16(values)), + vcvt_f32_bf16(vget_high_bf16(values))}; +#else + const auto values = vld1q_u16(reinterpret_cast(logits)); + return { + vreinterpretq_f32_u32(vshlq_n_u32(vmovl_u16(vget_low_u16(values)), 16)), + vreinterpretq_f32_u32(vshlq_n_u32(vmovl_u16(vget_high_u16(values)), 16)), + }; +#endif +} + +template +FORCE_INLINE void greedy_argmax(const scalar_t* logits, int64_t token_begin, + int64_t token_end, float& best_score, + int64_t& best_idx) { + constexpr int64_t VEC_SIZE = 4; + constexpr int64_t UNROLLED_SIZE = VEC_SIZE * 4; + const uint32x4_t first_offsets = {0, 1, 2, 3}; + const uint32x4_t second_offsets = + vaddq_u32(first_offsets, vdupq_n_u32(VEC_SIZE)); + const uint32x4_t third_offsets = + vaddq_u32(second_offsets, vdupq_n_u32(VEC_SIZE)); + const uint32x4_t fourth_offsets = + vaddq_u32(third_offsets, vdupq_n_u32(VEC_SIZE)); + + float32x4_t maximum = vdupq_n_f32(-std::numeric_limits::infinity()); + uint32x4_t maximum_indices = vdupq_n_u32(static_cast(token_begin)); + + int64_t token_id = token_begin; + const int64_t vector_end = + token_end - (token_end - token_begin) % UNROLLED_SIZE; + for (; token_id < vector_end; token_id += UNROLLED_SIZE) { + // Reduce 16 logits into four lanes. Lane j carries the winner among token + // offsets j, j+4, j+8, and j+12, together with its absolute token ID. + const float32x4x2_t first = load_logits(logits + token_id); + const float32x4x2_t second = load_logits(logits + token_id + 2 * VEC_SIZE); + + const float32x4_t first_half = vmaxnmq_f32(first.val[0], first.val[1]); + const uint32x4_t first_half_offsets = vbslq_u32( + vceqq_f32(first.val[0], first_half), first_offsets, second_offsets); + const float32x4_t second_half = vmaxnmq_f32(second.val[0], second.val[1]); + const uint32x4_t second_half_offsets = vbslq_u32( + vceqq_f32(second.val[0], second_half), third_offsets, fourth_offsets); + const float32x4_t block_maximum = vmaxnmq_f32(first_half, second_half); + const uint32x4_t block_offsets = + vbslq_u32(vceqq_f32(first_half, block_maximum), first_half_offsets, + second_half_offsets); + const uint32x4_t block_indices = + vaddq_u32(vdupq_n_u32(static_cast(token_id)), block_offsets); + const uint32x4_t update = vcgtq_f32(block_maximum, maximum); + maximum = vbslq_f32(update, block_maximum, maximum); + maximum_indices = vbslq_u32(update, block_indices, maximum_indices); + } + + if (token_end - token_id >= 2 * VEC_SIZE) { + const float32x4x2_t values = load_logits(logits + token_id); + const float32x4_t block_maximum = vmaxnmq_f32(values.val[0], values.val[1]); + const uint32x4_t block_offsets = vbslq_u32( + vceqq_f32(values.val[0], block_maximum), first_offsets, second_offsets); + const uint32x4_t block_indices = + vaddq_u32(vdupq_n_u32(static_cast(token_id)), block_offsets); + const uint32x4_t update = vcgtq_f32(block_maximum, maximum); + maximum = vbslq_f32(update, block_maximum, maximum); + maximum_indices = vbslq_u32(update, block_indices, maximum_indices); + token_id += 2 * VEC_SIZE; + } + + best_score = vmaxvq_f32(maximum); + // More than one lane can contain the global maximum. Select the smallest + // associated token ID to preserve first-occurrence argmax semantics. + const uint32x4_t matching_indices = + vbslq_u32(vceqq_f32(maximum, vdupq_n_f32(best_score)), maximum_indices, + vdupq_n_u32(std::numeric_limits::max())); + best_idx = vminvq_u32(matching_indices); + + for (; token_id < token_end; ++token_id) { + const float score = static_cast(logits[token_id]); + if (score > best_score) { + best_score = score; + best_idx = token_id; + } + } +} + +template +FORCE_INLINE void score_gumbel_block(const scalar_t* logits, int64_t token_id, + uint32_t token_offset, uint32x4_t hash, + uint32x4_t offsets, float temperature, + float32x4_t& winning_scores, + uint32x4_t& winning_token_offsets) { + constexpr int64_t VEC_SIZE = 8; + // For each of eight tokens, compute + // + // u = (Murmur3(seed, position, token) + 0.5) / 2^32 + // g = -log(-log(1-u)) + // scaled_logit = APPLY_TEMPERATURE ? logit / temperature : logit + // score = scaled_logit + g. + // + // The two four-lane score vectors are folded lane-wise. Output lane j is the + // winner between token offsets j and j+4; token_offset makes that ID relative + // to the caller's outer block (0, 8, 16, or 24 in the 32-token loop). + const uint32x4_t first_tokens = + vaddq_u32(vdupq_n_u32(static_cast(token_id)), offsets); + const uint32x4_t second_tokens = + vaddq_u32(first_tokens, vdupq_n_u32(VEC_SIZE / 2)); + const float32x4x2_t uniforms = {murmur3_uniform32x4(hash, first_tokens), + murmur3_uniform32x4(hash, second_tokens)}; + const float32x4x2_t inner_log = { + vnegq_f32(log1p_neg_stable(uniforms.val[0])), + vnegq_f32(log1p_neg_stable(uniforms.val[1]))}; + const float32x4x2_t noise = { + vnegq_f32(positive_normal_log(inner_log.val[0])), + vnegq_f32(positive_normal_log(inner_log.val[1]))}; + + float32x4x2_t logit = load_logits(logits + token_id); + if constexpr (APPLY_TEMPERATURE) { + // Match Triton's temperature division. Its cached-logits path divides by + // the same temperature on reload and relies on reproducing this value + // bitwise. + const float32x4_t divisor = vdupq_n_f32(temperature); + logit.val[0] = vdivq_f32(logit.val[0], divisor); + logit.val[1] = vdivq_f32(logit.val[1], divisor); + } + const float32x4_t first_scores = vaddq_f32(logit.val[0], noise.val[0]); + const float32x4_t second_scores = vaddq_f32(logit.val[1], noise.val[1]); + winning_scores = vmaxnmq_f32(first_scores, second_scores); + const uint32x4_t first_token_offsets = + vaddq_u32(offsets, vdupq_n_u32(token_offset)); + const uint32x4_t second_token_offsets = + vaddq_u32(first_token_offsets, vdupq_n_u32(VEC_SIZE / 2)); + winning_token_offsets = vbslq_u32(vceqq_f32(first_scores, winning_scores), + first_token_offsets, second_token_offsets); +} + +template +void gumbel_argmax(const scalar_t* logits, int64_t token_begin, + int64_t token_end, int64_t seed, int64_t position, + float temperature, float& best_score, int64_t& best_idx) { + constexpr int64_t VEC_SIZE = 8; + constexpr int64_t UNROLLED_SIZE = VEC_SIZE * 4; + const int64_t vector_end = + token_end - (token_end - token_begin) % UNROLLED_SIZE; + const uint32x4_t hash = vdupq_n_u32(murmur3_hash32_prefix(seed, position, 0)); + const uint32x4_t offsets = {0, 1, 2, 3}; + + best_score = -std::numeric_limits::infinity(); + best_idx = token_begin; + + int64_t token_id = token_begin; + for (; token_id < vector_end; token_id += UNROLLED_SIZE) { + // Keep four independent eight-token pipelines in flight to hide the log + // latency. + float32x4_t first_maximum; + uint32x4_t first_token_offsets; + score_gumbel_block( + logits, token_id, 0, hash, offsets, temperature, first_maximum, + first_token_offsets); + float32x4_t second_maximum; + uint32x4_t second_token_offsets; + score_gumbel_block( + logits, token_id + VEC_SIZE, VEC_SIZE, hash, offsets, temperature, + second_maximum, second_token_offsets); + float32x4_t third_maximum; + uint32x4_t third_token_offsets; + score_gumbel_block( + logits, token_id + 2 * VEC_SIZE, 2 * VEC_SIZE, hash, offsets, + temperature, third_maximum, third_token_offsets); + float32x4_t fourth_maximum; + uint32x4_t fourth_token_offsets; + score_gumbel_block( + logits, token_id + 3 * VEC_SIZE, 3 * VEC_SIZE, hash, offsets, + temperature, fourth_maximum, fourth_token_offsets); + + // reduce the four 8 token pipelines to find the block's argmax + const float32x4_t first_half_maximum = + vmaxnmq_f32(first_maximum, second_maximum); + const uint32x4_t first_half_token_offsets = + vbslq_u32(vceqq_f32(first_maximum, first_half_maximum), + first_token_offsets, second_token_offsets); + const float32x4_t second_half_maximum = + vmaxnmq_f32(third_maximum, fourth_maximum); + const uint32x4_t second_half_token_offsets = + vbslq_u32(vceqq_f32(third_maximum, second_half_maximum), + third_token_offsets, fourth_token_offsets); + const float32x4_t block_maximum = + vmaxnmq_f32(first_half_maximum, second_half_maximum); + const uint32x4_t block_token_offsets = + vbslq_u32(vceqq_f32(first_half_maximum, block_maximum), + first_half_token_offsets, second_half_token_offsets); + + const float score = vmaxnmvq_f32(block_maximum); + if (score > best_score) { + const uint32x4_t matching_offsets = vbslq_u32( + vceqq_f32(block_maximum, vdupq_n_f32(score)), block_token_offsets, + vdupq_n_u32(std::numeric_limits::max())); + best_score = score; + best_idx = token_id + vminvq_u32(matching_offsets); + } + } + + const int64_t vector_tail_end = token_end - (token_end - token_id) % VEC_SIZE; + for (; token_id < vector_tail_end; token_id += VEC_SIZE) { + float32x4_t maximum; + uint32x4_t token_offsets; + score_gumbel_block(logits, token_id, 0, hash, + offsets, temperature, + maximum, token_offsets); + const float score = vmaxnmvq_f32(maximum); + if (score > best_score) { + const uint32x4_t matching_offsets = + vbslq_u32(vceqq_f32(maximum, vdupq_n_f32(score)), token_offsets, + vdupq_n_u32(std::numeric_limits::max())); + best_score = score; + best_idx = token_id + vminvq_u32(matching_offsets); + } + } + + for (; token_id < token_end; ++token_id) { + float logit = static_cast(logits[token_id]); + if constexpr (APPLY_TEMPERATURE) logit /= temperature; + const float score = logit + gumbel_noise32(seed, position, token_id); + if (score > best_score) { + best_score = score; + best_idx = token_id; + } + } +} + +} // namespace cpu_sampling::neon + +namespace cpu_sampling { + +template +class SamplingImpl { + public: + using scalar_type = scalar_t; + static constexpr int64_t RangeAlignment = 32; + + void greedy_argmax(const scalar_t* logits, int64_t token_begin, + int64_t token_end, float& best_score, + int64_t& best_idx) const { + neon::greedy_argmax(logits, token_begin, token_end, best_score, best_idx); + } + + template + void gumbel_argmax(const scalar_t* logits, int64_t token_begin, + int64_t token_end, int64_t seed, int64_t position, + float temperature, score_t& best_score, + int64_t& best_idx) const { + if constexpr (std::is_same_v) { + // TODO: do we need to vectorize this? + scalar_gumbel_argmax( + logits, token_begin, token_end, seed, position, temperature, + best_score, best_idx); + } else { + neon::gumbel_argmax( + logits, token_begin, token_end, seed, position, temperature, + best_score, best_idx); + } + } +}; + +} // namespace cpu_sampling diff --git a/csrc/cpu/sampling/vec.h b/csrc/cpu/sampling/vec.h new file mode 100644 index 000000000000..bb9f28355d78 --- /dev/null +++ b/csrc/cpu/sampling/vec.h @@ -0,0 +1,30 @@ +#pragma once + +#include "common.h" + +namespace cpu_sampling { + +template +class SamplingImpl { + public: + using scalar_type = scalar_t; + static constexpr int64_t RangeAlignment = 1; + + void greedy_argmax(const scalar_t* logits, int64_t token_begin, + int64_t token_end, float& best_score, + int64_t& best_idx) const { + scalar_greedy_argmax(logits, token_begin, token_end, best_score, best_idx); + } + + template + void gumbel_argmax(const scalar_t* logits, int64_t token_begin, + int64_t token_end, int64_t seed, int64_t position, + float temperature, score_t& best_score, + int64_t& best_idx) const { + scalar_gumbel_argmax( + logits, token_begin, token_end, seed, position, temperature, best_score, + best_idx); + } +}; + +} // namespace cpu_sampling diff --git a/csrc/cpu/sampling_kernels.cpp b/csrc/cpu/sampling_kernels.cpp index 1d30b92a8d9a..ccca5b12ce9a 100644 --- a/csrc/cpu/sampling_kernels.cpp +++ b/csrc/cpu/sampling_kernels.cpp @@ -1,7 +1,23 @@ #include "cpu_types.hpp" +#if defined(__aarch64__) + #include "sampling/neon.h" + +template +using SelectedSamplingImpl = + cpu_sampling::SamplingImpl; +#else + #include "sampling/vec.h" + +template +using SelectedSamplingImpl = + cpu_sampling::SamplingImpl; +#endif + #include +#include +#include #include #include #include @@ -166,3 +182,133 @@ torch::Tensor greedy_argmax(const torch::Tensor& logits) { logits_contig.size(1)); return output; } + +torch::Tensor gumbel_sample_cpu(const torch::Tensor& logits, + const torch::Tensor& expanded_idx_mapping, + const torch::Tensor& temperature, + const torch::Tensor& seeds, + const torch::Tensor& positions, + bool apply_temperature, bool is_drafting, + bool use_fp64) { + TORCH_CHECK(logits.dim() == 2, "logits must be 2-D [num_tokens, vocab]"); + TORCH_CHECK(logits.stride(1) == 1, + "the logits vocabulary dimension must be contiguous"); + + auto output = + torch::empty({logits.size(0)}, logits.options().dtype(torch::kInt64)); + VLLM_DISPATCH_FLOATING_TYPES(logits.scalar_type(), "gumbel_sample_cpu", [&] { + using sampling_impl_t = SelectedSamplingImpl; + cpu_sampling::SamplingMainLoop mainloop; + mainloop(output.data_ptr(), logits.data_ptr(), + logits.stride(0), expanded_idx_mapping.data_ptr(), + temperature.data_ptr(), seeds.data_ptr(), + positions.data_ptr(), logits.size(0), logits.size(1), + apply_temperature, is_drafting, use_fp64); + }); + return output; +} + +FORCE_INLINE void fill_logits(float* logits, int64_t size, float value) { + constexpr int64_t VEC_SIZE = vec_op::FP32Vec16::VEC_ELEM_NUM; + const vec_op::FP32Vec16 values(value); + const int64_t vectorized_size = size - size % VEC_SIZE; + int64_t offset = 0; + for (; offset < vectorized_size; offset += VEC_SIZE) { + values.save(logits + offset); + } + std::fill(logits + offset, logits + size, value); +} + +void apply_logit_bias_cpu( + torch::Tensor& logits, const torch::Tensor& expanded_idx_mapping, + const torch::Tensor& positions, const torch::Tensor& num_allowed_token_ids, + const torch::Tensor& allowed_token_ids, const torch::Tensor& num_logit_bias, + const torch::Tensor& logit_bias_token_ids, const torch::Tensor& logit_bias, + const torch::Tensor& min_lens, const torch::Tensor& num_stop_token_ids, + const torch::Tensor& restore_when_all_masked, + const torch::Tensor& stop_token_ids, bool check_all_masked_rows) { + constexpr int64_t MAX_ALLOWED_TOKEN_IDS = 1024; + constexpr int64_t MAX_STOP_TOKEN_IDS = 128; + const float negative_infinity = -std::numeric_limits::infinity(); + const int64_t num_tokens = logits.size(0); + const int64_t vocab_size = logits.size(1); + + float* logits_ptr = logits.data_ptr(); + const int64_t* idx_mapping_ptr = expanded_idx_mapping.data_ptr(); + const int64_t* positions_ptr = positions.data_ptr(); + const int32_t* num_allowed_ptr = num_allowed_token_ids.data_ptr(); + const int32_t* allowed_ptr = allowed_token_ids.data_ptr(); + const int32_t* num_bias_ptr = num_logit_bias.data_ptr(); + const int32_t* bias_token_ids_ptr = logit_bias_token_ids.data_ptr(); + const float* bias_ptr = logit_bias.data_ptr(); + const int32_t* min_lens_ptr = min_lens.data_ptr(); + const int32_t* num_stop_ptr = num_stop_token_ids.data_ptr(); + const int32_t* restore_ptr = restore_when_all_masked.data_ptr(); + const int32_t* stop_token_ids_ptr = stop_token_ids.data_ptr(); + + const int64_t logits_stride = logits.stride(0); + const int64_t allowed_stride = allowed_token_ids.stride(0); + const int64_t bias_ids_stride = logit_bias_token_ids.stride(0); + const int64_t bias_stride = logit_bias.stride(0); + const int64_t stop_stride = stop_token_ids.stride(0); + +#pragma omp parallel for schedule(static) if (num_tokens > 1) + for (int64_t token_idx = 0; token_idx < num_tokens; ++token_idx) { + const int64_t req_idx = idx_mapping_ptr[token_idx]; + float* row = logits_ptr + token_idx * logits_stride; + + const int32_t num_allowed = num_allowed_ptr[req_idx]; + if (num_allowed > 0) { + std::array saved_logits; + const int32_t* row_allowed = allowed_ptr + req_idx * allowed_stride; + for (int32_t i = 0; i < num_allowed; ++i) { + saved_logits[i] = row[row_allowed[i]]; + } + fill_logits(row, vocab_size, negative_infinity); + for (int32_t i = 0; i < num_allowed; ++i) { + row[row_allowed[i]] = saved_logits[i]; + } + } + + const int32_t num_bias = num_bias_ptr[req_idx]; + const int32_t* row_bias_ids = + bias_token_ids_ptr + req_idx * bias_ids_stride; + const float* row_bias = bias_ptr + req_idx * bias_stride; + for (int32_t i = 0; i < num_bias; ++i) { + row[row_bias_ids[i]] += row_bias[i]; + } + + const int32_t num_stop = num_stop_ptr[req_idx]; + if (num_stop == 0 || + positions_ptr[token_idx] + 1 >= min_lens_ptr[req_idx]) { + continue; + } + + const int32_t* row_stop_ids = stop_token_ids_ptr + req_idx * stop_stride; + if (!check_all_masked_rows || restore_ptr[req_idx] == 0) { + for (int32_t i = 0; i < num_stop; ++i) { + row[row_stop_ids[i]] = negative_infinity; + } + continue; + } + + std::array saved_stop_logits; + for (int32_t i = 0; i < num_stop; ++i) { + saved_stop_logits[i] = row[row_stop_ids[i]]; + row[row_stop_ids[i]] = negative_infinity; + } + + const bool all_masked = + std::all_of(row, row + vocab_size, [negative_infinity](float value) { + return value == negative_infinity; + }); + if (all_masked) { + for (int32_t i = 0; i < num_stop; ++i) { + const float value = saved_stop_logits[i]; + if (std::isfinite(value)) { + row[row_stop_ids[i]] = value; + } + } + } + } +} diff --git a/csrc/cpu/torch_bindings.cpp b/csrc/cpu/torch_bindings.cpp index c68d5c6317df..cffa908841e3 100644 --- a/csrc/cpu/torch_bindings.cpp +++ b/csrc/cpu/torch_bindings.cpp @@ -406,9 +406,77 @@ void dynamic_per_token_scaled_fp8_quant( torch::Tensor& out, const torch::Tensor& input, torch::Tensor& scale, const std::optional& scale_ub); +torch::Tensor gumbel_sample_cpu(const torch::Tensor& logits, + const torch::Tensor& expanded_idx_mapping, + const torch::Tensor& temperature, + const torch::Tensor& seeds, + const torch::Tensor& positions, + bool apply_temperature, bool is_drafting, + bool use_fp64); + +void apply_logit_bias_cpu( + torch::Tensor& logits, const torch::Tensor& expanded_idx_mapping, + const torch::Tensor& positions, const torch::Tensor& num_allowed_token_ids, + const torch::Tensor& allowed_token_ids, const torch::Tensor& num_logit_bias, + const torch::Tensor& logit_bias_token_ids, const torch::Tensor& logit_bias, + const torch::Tensor& min_lens, const torch::Tensor& num_stop_token_ids, + const torch::Tensor& restore_when_all_masked, + const torch::Tensor& stop_token_ids, bool check_all_masked_rows); + void init_cpu_memory_env(std::vector node_ids); namespace cpu_utils { +// Kernels used only by standalone CPU MRV2. +#ifdef VLLM_STANDALONE_CPU_MRV2 +void gather_block_table_cpu(const torch::Tensor& block_table, + torch::Tensor& output, + const torch::Tensor& idx_mapping, + const torch::Tensor& num_blocks, + const int64_t num_reqs_padded); +void compute_slot_mapping_cpu( + const torch::Tensor& block_table, torch::Tensor& slot_mapping, + const torch::Tensor& idx_mapping, const torch::Tensor& query_start_loc, + const torch::Tensor& positions, const int64_t block_size, + const bool mapping_enabled, const int64_t pad_slot_id); +void prepare_prefill_inputs_cpu(torch::Tensor& input_ids, + torch::Tensor& next_prefill_tokens, + const torch::Tensor& idx_mapping, + const torch::Tensor& query_start_loc, + const torch::Tensor& all_token_ids, + const torch::Tensor& prefill_len, + const torch::Tensor& num_computed_tokens); +torch::Tensor get_prompt_logprobs_token_ids_cpu( + int64_t num_tokens, const torch::Tensor& query_start_loc, + const torch::Tensor& idx_mapping, const torch::Tensor& num_computed_tokens, + const torch::Tensor& all_token_ids); +void prepare_pos_seq_lens_cpu(const torch::Tensor& idx_mapping, + const torch::Tensor& query_start_loc, + const torch::Tensor& num_computed_tokens, + torch::Tensor& pos, torch::Tensor& seq_lens); +void prepare_mrope_positions_cpu(torch::Tensor& positions, + const torch::Tensor& prefill_positions, + const torch::Tensor& prefill_delta, + const torch::Tensor& idx_mapping, + const torch::Tensor& query_start_loc, + const torch::Tensor& prefill_lens, + const torch::Tensor& num_computed_tokens); +torch::Tensor combine_sampled_and_draft_tokens_cpu( + torch::Tensor& input_ids, const torch::Tensor& idx_mapping, + const torch::Tensor& last_sampled_tokens, + const torch::Tensor& query_start_loc, const torch::Tensor& seq_lens, + const torch::Tensor& prefill_len, const torch::Tensor& draft_tokens, + const torch::Tensor& cu_num_logits, const int64_t num_logits, + const int64_t num_new_sampled_tokens); +void post_update_cpu(const torch::Tensor& idx_mapping, + torch::Tensor& num_computed_tokens, + torch::Tensor& last_sampled_tokens, + const c10::optional& output_bin_counts, + const torch::Tensor& sampled_tokens, + const torch::Tensor& num_sampled, + const torch::Tensor& num_rejected, + const c10::optional& query_start_loc, + torch::Tensor& all_token_ids, torch::Tensor& total_len); +#endif void eagle_prepare_inputs_padded_kernel_impl( const torch::Tensor& cu_num_draft_tokens, const torch::Tensor& valid_sampled_tokens_count, @@ -999,6 +1067,62 @@ TORCH_LIBRARY_EXPAND(TORCH_EXTENSION_NAME, ops) { ops.def("init_cpu_memory_env(SymInt[] node_ids) -> ()", &init_cpu_memory_env); +#ifdef VLLM_STANDALONE_CPU_MRV2 + ops.def( + "gather_block_table_cpu(Tensor block_table, Tensor(a1!) output, " + "Tensor idx_mapping, Tensor num_blocks, SymInt num_reqs_padded) -> ()", + &cpu_utils::gather_block_table_cpu); + + ops.def( + "compute_slot_mapping_cpu(" + "Tensor block_table, Tensor(a1!) slot_mapping, Tensor idx_mapping, " + "Tensor query_start_loc, Tensor positions, SymInt block_size, " + "bool mapping_enabled, SymInt pad_slot_id) -> ()", + &cpu_utils::compute_slot_mapping_cpu); + + ops.def( + "prepare_prefill_inputs_cpu(Tensor(a0!) input_ids, " + "Tensor(a1!) next_prefill_tokens, Tensor idx_mapping, " + "Tensor query_start_loc, Tensor all_token_ids, Tensor prefill_len, " + "Tensor num_computed_tokens) -> ()", + &cpu_utils::prepare_prefill_inputs_cpu); + + ops.def( + "get_prompt_logprobs_token_ids_cpu(" + "SymInt num_tokens, Tensor query_start_loc, Tensor idx_mapping, " + "Tensor num_computed_tokens, Tensor all_token_ids) -> Tensor", + &cpu_utils::get_prompt_logprobs_token_ids_cpu); + + ops.def( + "prepare_pos_seq_lens_cpu(Tensor idx_mapping, Tensor query_start_loc, " + "Tensor num_computed_tokens, Tensor(a3!) pos, " + "Tensor(a4!) seq_lens) -> ()", + &cpu_utils::prepare_pos_seq_lens_cpu); + + ops.def( + "prepare_mrope_positions_cpu(" + "Tensor(a0!) positions, Tensor prefill_positions, Tensor prefill_delta, " + "Tensor idx_mapping, Tensor query_start_loc, Tensor prefill_lens, " + "Tensor num_computed_tokens) -> ()", + &cpu_utils::prepare_mrope_positions_cpu); + + ops.def( + "combine_sampled_and_draft_tokens_cpu(" + "Tensor(a0!) input_ids, Tensor idx_mapping, Tensor last_sampled_tokens, " + "Tensor query_start_loc, Tensor seq_lens, Tensor prefill_len, " + "Tensor draft_tokens, Tensor cu_num_logits, SymInt num_logits, " + "SymInt num_new_sampled_tokens) -> Tensor", + &cpu_utils::combine_sampled_and_draft_tokens_cpu); + + ops.def( + "post_update_cpu(Tensor idx_mapping, " + "Tensor(a1!) num_computed_tokens, Tensor(a2!) last_sampled_tokens, " + "Tensor(a3!)? output_bin_counts, Tensor sampled_tokens, " + "Tensor num_sampled, Tensor num_rejected, Tensor? query_start_loc, " + "Tensor(a8!) all_token_ids, Tensor(a9!) total_len) -> ()", + &cpu_utils::post_update_cpu); +#endif + // Fused sampling kernels ops.def("fused_gumbel_argmax(Tensor logits, Tensor seeds) -> Tensor"); ops.impl("fused_gumbel_argmax", torch::kCPU, &fused_gumbel_argmax); @@ -1006,6 +1130,22 @@ TORCH_LIBRARY_EXPAND(TORCH_EXTENSION_NAME, ops) { ops.def("greedy_argmax(Tensor logits) -> Tensor"); ops.impl("greedy_argmax", torch::kCPU, &greedy_argmax); + ops.def( + "gumbel_sample_cpu(Tensor logits, Tensor expanded_idx_mapping, " + "Tensor temperature, Tensor seeds, Tensor positions, " + "bool apply_temperature, bool is_drafting, bool use_fp64) -> Tensor"); + ops.impl("gumbel_sample_cpu", torch::kCPU, &gumbel_sample_cpu); + + ops.def( + "apply_logit_bias_cpu(Tensor(a0!) logits, " + "Tensor expanded_idx_mapping, Tensor positions, " + "Tensor num_allowed_token_ids, Tensor allowed_token_ids, " + "Tensor num_logit_bias, Tensor logit_bias_token_ids, " + "Tensor logit_bias, Tensor min_lens, Tensor num_stop_token_ids, " + "Tensor restore_when_all_masked, Tensor stop_token_ids, " + "bool check_all_masked_rows) -> ()", + &apply_logit_bias_cpu); + // Speculative decoding kernels ops.def( "eagle_prepare_inputs_padded_kernel_impl(Tensor cu_num_draft_tokens, " diff --git a/csrc/fused_minimax_m3_qknorm_rope_kv_insert_kernel.cu b/csrc/fused_minimax_m3_qknorm_rope_kv_insert_kernel.cu index 09484daf3315..11e76e6d9873 100644 --- a/csrc/fused_minimax_m3_qknorm_rope_kv_insert_kernel.cu +++ b/csrc/fused_minimax_m3_qknorm_rope_kv_insert_kernel.cu @@ -48,9 +48,11 @@ * ``qkv`` tensor. Caches (bf16) are scatter-written by slot. */ +#include #include #include #include +#include #include "torch_utils.h" @@ -271,11 +273,43 @@ __device__ __forceinline__ void storeElemsFp8( // Match scaled_fp8_quant(q_out): materialize q in scalar_t before applying the // inverse dequantization scale and converting it to E4M3. +#ifndef USE_ROCM +// storeScaledQElemsFp8 with inv_scale = 1, from the packed model-dtype values: +// E4M3 of a scalar_t equals E4M3 of its float, the max takes NaN and values +// below -448 to -448 as storeElemsFp8's fmaxf does, and the cvt saturates the +// top at 448. +template +__device__ __forceinline__ void storeUnitScaleQElemsFp8( + uint8_t* __restrict__ dst, float const (&elems)[kElemsPerLane]) { + using Converter = vllm::_typeConvert; + __nv_fp8x2_storage_t out2[kElemsPerLane / 2]; + #pragma unroll + for (int i = 0; i < kElemsPerLane / 2; i++) { + auto const v = + Converter::convert(make_float2(elems[2 * i], elems[2 * i + 1])); + if constexpr (std::is_same_v) { + out2[i] = __nv_cvt_bfloat16raw2_to_fp8x2( + __hmax2(v, __float2bfloat162_rn(-448.0f)), __NV_SATFINITE, __NV_E4M3); + } else { + out2[i] = __nv_cvt_halfraw2_to_fp8x2( + __hmax2(v, __float2half2_rn(-448.0f)), __NV_SATFINITE, __NV_E4M3); + } + } + *reinterpret_cast(dst) = *reinterpret_cast(out2); +} +#endif + template __device__ __forceinline__ void storeScaledQElemsFp8( uint8_t* __restrict__ dst, float const (&elems)[kElemsPerLane], float const inv_scale) { using Converter = vllm::_typeConvert; +#ifndef USE_ROCM + if (inv_scale == 1.0f) { + storeUnitScaleQElemsFp8(dst, elems); + return; + } +#endif float scaled[kElemsPerLane]; #pragma unroll for (int i = 0; i < kElemsPerLane; i++) { @@ -424,27 +458,14 @@ __device__ __forceinline__ uint16_t clearNegZeroNibbles(uint16_t packed) { return packed & (((packed & 0x7777u) + 0x7777u) | 0x7777u); } -// Quantize one K or V head row of one token into its NVFP4 page, where -// ``scale`` is the dequantization (global) scale. Every lane of the warp must -// call this (the scale-group reductions shuffle). +// The E2M1 codes (low nibble first) of four values x, already rounded to the +// model dtype, of a 16-value group with absolute maximum group_max, and the +// group's E4M3 scale in ``sf_byte``, where ``scale`` is the dequantization +// (global) scale. template -__device__ __forceinline__ void storeNvfp4CacheElems( - uint8_t* __restrict__ page, int const kv, int const head, int const token, - int const block_size, int const laneId, float const (&elems)[kElemsPerLane], - float const scale) { - // Quantize the model-dtype value that the unfused path reads back from qkv. - using Converter = vllm::_typeConvert; - float x[kElemsPerLane]; - float group_max = 0.0f; - #pragma unroll - for (int i = 0; i < kElemsPerLane; i++) { - x[i] = static_cast(Converter::convert(elems[i])); - group_max = fmaxf(group_max, fabsf(x[i])); - } - group_max = fmaxf(group_max, __shfl_xor_sync(FINAL_MASK, group_max, 1, 32)); - group_max = fmaxf(group_max, __shfl_xor_sync(FINAL_MASK, group_max, 2, 32)); - - uint8_t sf_byte; +__device__ __forceinline__ uint16_t +nvfp4EncodeElems(float const (&x)[kElemsPerLane], float const group_max, + float const scale, uint8_t& sf_byte) { float sf_rcp; float q[kElemsPerLane]; // (x / scale) / sf_value, E2M1-encoded below if (scale == 1.0f) { @@ -516,8 +537,49 @@ __device__ __forceinline__ void storeNvfp4CacheElems( for (int i = 0; i < kElemsPerLane; i++) codes |= e2m1Code(q[i]) << (4 * i); packed = static_cast(codes); #endif - packed = clearNegZeroNibbles(packed); + return clearNegZeroNibbles(packed); +} + +// Offset, within a K (kv 0) or V (kv 1) head's scale block, of the E4M3 scale +// of a token's 16-value group: linear for K, the SM100 trtllm-gen 4x4 +// token-quad swizzle for V. +__device__ __forceinline__ int nvfp4ScaleOffset(int const kv, int const token, + int const group) { + if (kv == 0) return token * kNvfp4ScaleBytes + group; + constexpr int kGroupsPerQuad = kNvfp4ScaleBytes / 4; + int const swizzled_t = (token / 4) * 4 + group / kGroupsPerQuad; + int const swizzled_s = (group % kGroupsPerQuad) * 4 + token % 4; + return swizzled_t * kNvfp4ScaleBytes + swizzled_s; +} + +// The E2M1 codes of this lane's four values of a K or V head row, rounded to +// the model dtype, and in ``sf_byte`` its 16-value group's E4M3 scale, where +// ``scale`` is the dequantization (global) scale. Every lane of the warp must +// call this (the scale-group reductions shuffle). +template +__device__ __forceinline__ uint16_t nvfp4QuantizeElems( + float const (&elems)[kElemsPerLane], float const scale, uint8_t& sf_byte) { + // Quantize the model-dtype value that the unfused path reads back from qkv. + using Converter = vllm::_typeConvert; + float x[kElemsPerLane]; + float group_max = 0.0f; + #pragma unroll + for (int i = 0; i < kElemsPerLane; i++) { + x[i] = static_cast(Converter::convert(elems[i])); + group_max = fmaxf(group_max, fabsf(x[i])); + } + group_max = fmaxf(group_max, __shfl_xor_sync(FINAL_MASK, group_max, 1, 32)); + group_max = fmaxf(group_max, __shfl_xor_sync(FINAL_MASK, group_max, 2, 32)); + + return nvfp4EncodeElems(x, group_max, scale, sf_byte); +} +// Store the codes and scale from nvfp4QuantizeElems of lane laneId's four +// values of a token's K (kv 0) or V (kv 1) head row. +__device__ __forceinline__ void nvfp4StoreElems( + uint8_t* __restrict__ page, int const kv, int const head, int const token, + int const block_size, int const laneId, uint16_t const packed, + uint8_t const sf_byte) { uint8_t* const slot = page + static_cast(2 * head + kv) * block_size * (kNvfp4DataBytes + kNvfp4ScaleBytes); @@ -526,19 +588,57 @@ __device__ __forceinline__ void storeNvfp4CacheElems( *reinterpret_cast(data) = packed; if (laneId % kNvfp4LanesPerGroup == 0) { - int const group = laneId / kNvfp4LanesPerGroup; - int offset; - if (kv == 0) { - offset = token * kNvfp4ScaleBytes + group; - } else { - constexpr int kGroupsPerQuad = kNvfp4ScaleBytes / 4; - int const swizzled_t = (token / 4) * 4 + group / kGroupsPerQuad; - int const swizzled_s = (group % kGroupsPerQuad) * 4 + token % 4; - offset = swizzled_t * kNvfp4ScaleBytes + swizzled_s; - } - slot[block_size * kNvfp4DataBytes + offset] = sf_byte; + slot[block_size * kNvfp4DataBytes + + nvfp4ScaleOffset(kv, token, laneId / kNvfp4LanesPerGroup)] = sf_byte; } } + +// nvfp4QuantizeElems for one lane holding a whole 16-value group x (already +// rounded to the model dtype): the group max is order-independent, so this is +// the same codes and scale without the shuffles. Low nibble first. +template +__device__ __forceinline__ uint2 nvfp4QuantizeGroup(float const (&x)[16], + float const scale, + uint8_t& sf_byte) { + float group_max = 0.0f; + #pragma unroll + for (int i = 0; i < 16; i++) group_max = fmaxf(group_max, fabsf(x[i])); + uint32_t codes[4]; + #pragma unroll + for (int k = 0; k < 4; k++) { + float const xk[kElemsPerLane] = {x[4 * k], x[4 * k + 1], x[4 * k + 2], + x[4 * k + 3]}; + codes[k] = nvfp4EncodeElems(xk, group_max, scale, sf_byte); + } + return make_uint2(codes[0] | codes[1] << 16, codes[2] | codes[3] << 16); +} + +// Store group ``group`` (its 8 code bytes and its scale) of a token's K +// (kv 0) or V (kv 1) head row into its NVFP4 page. +__device__ __forceinline__ void nvfp4StoreGroup( + uint8_t* __restrict__ page, int const kv, int const head, int const token, + int const block_size, int const group, uint2 const codes, + uint8_t const sf_byte) { + uint8_t* const slot = page + static_cast(2 * head + kv) * + block_size * + (kNvfp4DataBytes + kNvfp4ScaleBytes); + *reinterpret_cast(slot + token * kNvfp4DataBytes + 8 * group) = codes; + slot[block_size * kNvfp4DataBytes + nvfp4ScaleOffset(kv, token, group)] = + sf_byte; +} + +// Quantize one K or V head row of one token into its NVFP4 page, where +// ``scale`` is the dequantization (global) scale. Every lane of the warp must +// call this (the scale-group reductions shuffle). +template +__device__ __forceinline__ void storeNvfp4CacheElems( + uint8_t* __restrict__ page, int const kv, int const head, int const token, + int const block_size, int const laneId, float const (&elems)[kElemsPerLane], + float const scale) { + uint8_t sf_byte; + uint16_t const packed = nvfp4QuantizeElems(elems, scale, sf_byte); + nvfp4StoreElems(page, kv, head, token, block_size, laneId, packed, sf_byte); +} #endif // USE_ROCM // ──────────────────────────────────────────────────────────────────────────── @@ -553,18 +653,21 @@ __device__ __forceinline__ void storeNvfp4CacheElems( // Slots per token: // Q : nq (always — norm+RoPE) // K : nkv (always — norm+RoPE; +K-cache insert) -// V : nkv only if kInsertKV (V-cache insert; no warps in dense) +// V : nkv only if kInsertKV (V-cache insert) // IQ: niq only if kProcessIndex (norm+RoPE) // IK: 1 only if kProcessIndex (norm+RoPE; +index-cache insert) // cache_t/kv_dt: main attention KV-cache dtype (auto/fp8). out_idx_t/kFp8Idx: // indexer index-K cache + index-Q output dtype (scalar_t or e4m3 byte). +// kNvfp4Idx (implies kFp8Idx): the index-K cache is instead packed NVFP4 pages +// [nb, bs, 72] (bs*64 B E2M1, then bs*8 B linear E4M3 scales, no global scale), +// the layout of the MSA Q8KV4 indexer; index-Q stays e4m3. // kHasIndex means the qkv row is laid out as sparse [q|k|v|index_q|index_k]. // kProcessIndex controls whether this launch actually norms/ropes the index // branch and writes index_q/index_k outputs. Skip-index-topk reuse layers keep // kHasIndex=true but set kProcessIndex=false. template + bool kProcessIndex, bool kFp8Idx, bool kNvfp4Idx> __global__ void fusedMiniMaxM3QNormRopeKVInsertKernel( scalar_t* __restrict__ qkv, // [N, qkv_row] in/out (packs index if sparse) scalar_t* __restrict__ q_out, // [N, nq*128] contiguous, or nullptr @@ -588,6 +691,8 @@ __global__ void fusedMiniMaxM3QNormRopeKVInsertKernel( // NHD/HND layout choice is captured by the head/token strides. int64_t const kv_s_block, int64_t const kv_s_head, int64_t const kv_s_token, int64_t const kv_s_dim, + // NVFP4 index cache only: page stride in bytes. + int64_t const idx_s_block, // NVFP4 main cache only: per-layer K/V dequantization (global) scales. float const* __restrict__ kv_k_scale, float const* __restrict__ kv_v_scale) { @@ -604,8 +709,11 @@ __global__ void fusedMiniMaxM3QNormRopeKVInsertKernel( static_assert(!kProcessIndex || kHasIndex, "index processing requires sparse row layout"); + static_assert(!kNvfp4Idx || (kFp8Idx && kInsertKV), + "NVFP4 index cache requires fp8 index-Q and cache insert"); - // Slot layout (compile-time gated: dense has neither V nor index slots). + // Slot layout (compile-time gated: V slots only when inserting, index + // slots only on the sparse layer). int const v_slots = kInsertKV ? nkv : 0; int const idx_slots = kProcessIndex ? niq + 1 : 0; int const slots_per_token = nq + nkv + v_slots + idx_slots; @@ -743,7 +851,7 @@ __global__ void fusedMiniMaxM3QNormRopeKVInsertKernel( } } - // ── Cache inserts (sparse serving only). ─────────────────────────────── + // ── Cache inserts. ───────────────────────────────────────────────────── if constexpr (kInsertKV) { // Guard (not early-return) so every thread reaches the PDL trigger below. int64_t sm = -1; @@ -754,7 +862,16 @@ __global__ void fusedMiniMaxM3QNormRopeKVInsertKernel( } if (sm >= 0) { // skip padded / unscheduled tokens if (isIK) { - if constexpr (kFp8Idx) { + if constexpr (kNvfp4Idx) { +#ifndef USE_ROCM + // A single-head K side: data [bs, 64] then linear scales [bs, 8]. + storeNvfp4CacheElems( + reinterpret_cast(index_cache) + + (sm / block_size) * idx_s_block, + /*kv=*/0, /*head=*/0, static_cast(sm % block_size), + block_size, laneId, elems, /*scale=*/1.0f); +#endif + } else if constexpr (kFp8Idx) { storeElemsFp8(index_cache + sm * kHeadDim + dim_base, elems); } else { storeElems(index_cache + sm * kHeadDim + dim_base, elems); @@ -779,6 +896,15 @@ __global__ void fusedMiniMaxM3QNormRopeKVInsertKernel( int64_t const off = b * kv_s_block + head * kv_s_head + t * kv_s_token + (kv * kHeadDim + dim_base) * kv_s_dim; + if constexpr (!kHasIndex) { + // Dense layers stand in for reshape_and_cache_flash, which + // quantizes the k already materialized in the model dtype. + using Converter = vllm::_typeConvert; +#pragma unroll + for (int i = 0; i < kElemsPerLane; i++) { + elems[i] = Converter::convert(Converter::convert(elems[i])); + } + } storeCacheElems(kv_cache + off, elems); } } @@ -794,6 +920,754 @@ __global__ void fusedMiniMaxM3QNormRopeKVInsertKernel( #endif } +#ifndef USE_ROCM +// ──────────────────────────────────────────────────────────────────────────── +// Persistent row-group kernel +// ──────────────────────────────────────────────────────────────────────────── +// Writes the same bytes as the kernel above, for every instantiation of it, +// when rotary_dim == 64. A token's work is a group of warps with fixed roles: +// the q heads kQHeads (4 or 8) per warp, then K, V and index_q four heads per +// warp, then index_k. A branch of fewer than four heads, and index_k, takes +// one warp per head in the layout of the kernel above (lane l holds dims +// 4l .. 4l + 3); the others run four-row steps, where lane (g, v) holds dims +// 32c + 4v .. 32c + 4v + 3 (c = 0..3) of row g, the dims lane 8c + v holds +// above, so the stores call the same helpers with that lane id. A CTA runs +// some or all warps of the warp groups of tokens x, x + gridDim.x, ..., and +// each warp copies the qkv rows and cos/sin row of its next tokens into a +// kStages-deep shared-memory ring with cp.async. +namespace rg { + +constexpr int kRows = 4; +constexpr int kChunks = 4; +constexpr int kRotaryDim = 64; +constexpr int kStages = 3; // ring depth: two tokens in flight per warp +// A ring row: 256 B padded to 320 B, so that rows 1 apart start 16 banks apart +// and a 4-row step's 8-byte loads are conflict-free. +constexpr int kRowPitch = kHeadDim * 2 + 64; +constexpr int kCosSinBytes = kRotaryDim * 2; +enum Role : int { kQ = 0, kK, kV, kIQ, kIK }; + +// SM100's packed FP32 instructions where they exist; the same RN results. +__device__ __forceinline__ float2 mul2(float2 const a, float2 const b) { + #if defined(__CUDA_ARCH__) && __CUDA_ARCH__ >= 1000 + return __fmul2_rn(a, b); + #else + return make_float2(__fmul_rn(a.x, b.x), __fmul_rn(a.y, b.y)); + #endif +} + +__device__ __forceinline__ float2 fma2(float2 const a, float2 const b, + float2 const c) { + #if defined(__CUDA_ARCH__) && __CUDA_ARCH__ >= 1000 + return __ffma2_rn(a, b, c); + #else + return make_float2(__fmaf_rn(a.x, b.x, c.x), __fmaf_rn(a.y, b.y, c.y)); + #endif +} + +__device__ __forceinline__ float2 add2(float2 const a, float2 const b) { + #if defined(__CUDA_ARCH__) && __CUDA_ARCH__ >= 1000 + return __fadd2_rn(a, b); + #else + return make_float2(__fadd_rn(a.x, b.x), __fadd_rn(a.y, b.y)); + #endif +} + +// A head row as one lane holds it, paired for packed FP32 math: +// x[k][i] = element i of chunks 2k and 2k + 1. +using Row = float2[2][4]; + +template +__device__ __forceinline__ void loadRow(uint8_t const* row, int const v, + Row& x) { + float e[kChunks][kElemsPerLane]; + #pragma unroll + for (int c = 0; c < kChunks; c++) { + loadElems(reinterpret_cast(row + 64 * c + 8 * v), + e[c]); + } + #pragma unroll + for (int k = 0; k < 2; k++) { + #pragma unroll + for (int i = 0; i < kElemsPerLane; i++) { + x[k][i] = make_float2(e[2 * k][i], e[2 * k + 1][i]); + } + } +} + +__device__ __forceinline__ void chunkElems(Row const& x, int const c, + float (&e)[kElemsPerLane]) { + #pragma unroll + for (int i = 0; i < kElemsPerLane; i++) { + e[i] = c % 2 ? x[c / 2][i].y : x[c / 2][i].x; + } +} + +// normAndRope (Gemma RMSNorm, then NeoX RoPE on dims [0, 64)) with its +// rounding as compiled: an FFMA sum of squares per lane of the one-warp-per-row +// layout (one chunk here), the xor-16/8/4/2/1 tree (16 pairs chunks c and +// c ^ 2, 8 pairs c and c ^ 1), FFMA(sum, 1/128, eps), rsqrtf, x * rms * w, and +// x * cos -/+ RN(partner * sin) as one FFMA. +__device__ __forceinline__ void normRope(Row& x, Row const& w1, float const eps, + float const (&cos4)[4], + float const (&sin4)[4]) { + float2 sq[2]; + #pragma unroll + for (int k = 0; k < 2; k++) { + sq[k] = mul2(x[k][0], x[k][0]); // = FFMA(x, x, 0) + #pragma unroll + for (int i = 1; i < 4; i++) sq[k] = fma2(x[k][i], x[k][i], sq[k]); + } + float2 const xor16 = add2(sq[0], sq[1]); + float sum = __fadd_rn(xor16.x, xor16.y); + sum = __fadd_rn(sum, __shfl_xor_sync(FINAL_MASK, sum, 4, 32)); + sum = __fadd_rn(sum, __shfl_xor_sync(FINAL_MASK, sum, 2, 32)); + sum = __fadd_rn(sum, __shfl_xor_sync(FINAL_MASK, sum, 1, 32)); + float const rms_rcp = + rsqrtf(__fmaf_rn(sum, 1.0f / static_cast(kHeadDim), eps)); + float2 const r2 = make_float2(rms_rcp, rms_rcp); + #pragma unroll + for (int k = 0; k < 2; k++) { + #pragma unroll + for (int i = 0; i < 4; i++) x[k][i] = mul2(mul2(x[k][i], r2), w1[k][i]); + } + // x[0][i] = (x1, x2): dims 4v + i and 32 + 4v + i. + #pragma unroll + for (int i = 0; i < 4; i++) { + float2 const x_sin = mul2(x[0][i], make_float2(sin4[i], sin4[i])); + x[0][i] = make_float2(__fmaf_rn(x[0][i].x, cos4[i], -x_sin.y), + __fmaf_rn(x[0][i].y, cos4[i], x_sin.x)); + } +} + +__device__ __forceinline__ void cpAsync16(uint32_t const dst, void const* src) { + asm volatile("cp.async.cg.shared.global [%0], [%1], 16;\n" ::"r"(dst), + "l"(src) + : "memory"); +} + +__device__ __forceinline__ void cpAsyncCommit() { + asm volatile("cp.async.commit_group;\n" ::: "memory"); +} + +template +__device__ __forceinline__ void cpAsyncWait() { + asm volatile("cp.async.wait_group %0;\n" ::"n"(kPending) : "memory"); +} + +// Warp roles of a token's warp group: the q heads kQHeads at a time, then K, +// V and index_q four heads at a time, then index_k. +// A branch of fewer than kRows heads (and index_k) takes one warp per head in +// the layout of the kernel above instead of a part-empty four-row step. +struct Roles { + int nq, nkv, niq; + bool kv_one, iq_one; // one warp per head + int q_warps, kv_warps, v_warps, iq_warps, ik_warps; + int warps; // one token's warp group + + __host__ __device__ Roles(int const q_heads, int const nq_, int const nkv_, + int const niq_, bool const insert_kv, + bool const process_index) + : nq(nq_), nkv(nkv_), niq(niq_) { + kv_one = nkv < kRows; + iq_one = niq < kRows; + q_warps = (nq + q_heads - 1) / q_heads; + kv_warps = kv_one ? nkv : (nkv + kRows - 1) / kRows; + v_warps = insert_kv ? kv_warps : 0; + iq_warps = process_index ? (iq_one ? niq : (niq + kRows - 1) / kRows) : 0; + ik_warps = process_index ? 1 : 0; + warps = q_warps + kv_warps + v_warps + iq_warps + ik_warps; + } + + // Shared-memory offset of warp w's ring: the q warps' rings, then the + // others'. + __host__ __device__ int ringOffset(int const w, int const q_ring, + int const side_ring) const { + return min(w, q_warps) * q_ring + max(w - q_warps, 0) * side_ring; + } +}; + +// CTA size limit, for 80 registers per thread. +constexpr int kMaxWarps = 24; + +template +constexpr int stageBytes() { + return kQHeads * kRowPitch + kCosSinBytes; +} + +// What a warp writes besides the caches, fixed per launch so that the stores +// of the other modes are not compiled into the loop: q (kQFp8Unit: only +// q_fp8_out, unit scale; kQFp8: only q_fp8_out; kQAny: decided per store), +// index_q (kIQOut: index_q_out; kIQInPlace: back into qkv; kNone), and the +// in-place k / index_k rewrite (kInPlace or kNone). +enum Out : int { + kNone = 0, + kInPlace, + kQFp8Unit, + kQFp8, + kQAny, + kIQOut, + kIQInPlace +}; + +// One warp's loop over its CTA's tokens: rows [head0, head0 + nrows) of one +// branch, kSteps four-row steps per token. +template +__device__ __forceinline__ void runWarp( + int const head0, int const nrows, int const qkv_head0, uint8_t* const ring, + int const stage_bytes, scalar_t* __restrict__ qkv, + scalar_t* __restrict__ q_out, uint8_t* __restrict__ q_fp8_out, + out_idx_t* __restrict__ index_q_out, scalar_t const* __restrict__ norm_w, + scalar_t const* __restrict__ cos_sin_cache, + int64_t const* __restrict__ positions, + int64_t const* __restrict__ slot_mapping, cache_t* __restrict__ kv_cache, + out_idx_t* __restrict__ index_cache, float const eps, + float const q_fp8_inv_scale, int const num_tokens, int const nq, + int const niq, int const qkv_row, int const block_size, + int64_t const kv_s_block, int64_t const kv_s_head, int64_t const kv_s_token, + int64_t const kv_s_dim, int64_t const idx_s_block, float const kv_scale, + bool const write_qkv) { + constexpr bool kNorm = kRole != kV; + constexpr bool kSlots = + kInsertKV && (kRole == kK || kRole == kV || kRole == kIK); + // kOneRow: a single row in the layout of the kernel above (lane l holds + // dims 4l .. 4l + 3), for branches of one head. + constexpr int kStepRows = kOneRow ? 1 : kRows; + constexpr int kLaneChunks = kOneRow ? 1 : kChunks; + int const lane = threadIdx.x % 32; + int const g = kOneRow ? 0 : lane / 8; + int const v = kOneRow ? lane : lane % 8; + constexpr int kCopies = (kSteps * kStepRows * 16 + 31) / 32; + uint32_t const ring_smem = + static_cast(__cvta_generic_to_shared(ring)); + + // Not produced by the predecessor: loaded before the PDL wait, used after. + uint2 w_raw[kChunks]; + if constexpr (kNorm && !kOneRow) { + #pragma unroll + for (int c = 0; c < kChunks; c++) { + w_raw[c] = __ldg(reinterpret_cast(norm_w + 32 * c + 4 * v)); + } + } + + // PDL: qkv comes from the predecessor (the qkv GEMM). + #if defined(__CUDA_ARCH__) && (__CUDA_ARCH__ >= 900) + cudaGridDependencySynchronize(); + #endif + + Row w1; + if constexpr (kNorm && !kOneRow) { + float e[kChunks][kElemsPerLane]; + #pragma unroll + for (int c = 0; c < kChunks; c++) { + loadElems(reinterpret_cast(&w_raw[c]), e[c]); + } + #pragma unroll + for (int k = 0; k < 2; k++) { + #pragma unroll + for (int i = 0; i < kElemsPerLane; i++) { + w1[k][i] = make_float2(1.0f + e[2 * k][i], 1.0f + e[2 * k + 1][i]); + } + } + } + + int const stride = gridDim.x; + int const t0 = blockIdx.x; + int const n = (num_tokens - t0 + stride - 1) / stride; + // Lane j holds the position (slot) of the token of iteration base + j. + auto fetch = [&](int64_t const* src, int const base, int64_t const fill) { + int64_t const t = t0 + static_cast(base + lane) * stride; + return t < num_tokens ? static_cast(__ldg( + reinterpret_cast(src) + t)) + : fill; + }; + int pos_base = 0, slot_base = 0; + int64_t pos_l = 0, slot_l = -1; + if constexpr (kNorm) pos_l = fetch(positions, 0, 0); + if constexpr (kSlots) slot_l = fetch(slot_mapping, 0, -1); + + scalar_t const* const src0 = + qkv + static_cast(t0) * qkv_row + qkv_head0 * kHeadDim; + int64_t const src_step = static_cast(stride) * qkv_row; + auto issue_rows = [&](int const it) { + if (it >= n) return; + scalar_t const* const src = src0 + it * src_step; + uint32_t const stage = ring_smem + (it % kStages) * stage_bytes; + #pragma unroll + for (int k = 0; k < kCopies; k++) { + int const r = lane / 16 + 2 * k; + if (r < nrows && (!kOneRow || lane < 16)) { + cpAsync16(stage + r * kRowPitch + lane % 16 * 16, + src + r * kHeadDim + lane % 16 * 8); + } + } + }; + // The cos/sin row of iteration it, then the commit that closes its group. + auto issue_cos_sin = [&](int const it) { + if constexpr (kNorm) { + if (it < n) { + if (it - pos_base == 32) { + pos_base = it; + pos_l = fetch(positions, pos_base, 0); + } + int64_t const pos = __shfl_sync( + FINAL_MASK, static_cast(pos_l), it - pos_base, 32); + if (lane < kCosSinBytes / 16) { + cpAsync16(ring_smem + (it % kStages) * stage_bytes + + kSteps * kStepRows * kRowPitch + lane * 16, + cos_sin_cache + pos * kRotaryDim + lane * 8); + } + } + } + cpAsyncCommit(); + }; + + // The prologue's rows do not wait for the positions. + #pragma unroll + for (int it = 0; it < kStages - 1; it++) issue_rows(it); + #pragma unroll + for (int it = 0; it < kStages - 1; it++) issue_cos_sin(it); + + // This lane's dims 4v .. 4v + 3 (+ 32c) of row g of the first step, at the + // first token; each iteration advances them by stride tokens. Dims + // 32c + 4v of a row are lane 8c + v's dims in the kernel above. + scalar_t* in_place = qkv + static_cast(t0) * qkv_row + + (qkv_head0 + g) * kHeadDim + 4 * v; + int const head_rows = kRole == kQ ? nq : kRole == kIQ ? niq : 0; + int64_t const out_lane = + (static_cast(t0) * head_rows + head0 + g) * kHeadDim + 4 * v; + int64_t const out_step = static_cast(stride) * head_rows * kHeadDim; + int64_t out = out_lane; + + for (int it = 0; it < n; it++) { + issue_rows(it + kStages - 1); + issue_cos_sin(it + kStages - 1); + cpAsyncWait(); + __syncwarp(); + uint8_t const* const stage = ring + (it % kStages) * stage_bytes; + float cos4[4], sin4[4]; + if constexpr (kNorm) { + scalar_t const* const cs = reinterpret_cast( + stage + kSteps * kStepRows * kRowPitch); + if constexpr (!kOneRow) { + loadElems(cs + 4 * v, cos4); + loadElems(cs + kRotaryDim / 2 + 4 * v, sin4); + } + } + // Slot of the token: block and offset with one 32-bit division (the host + // checks that every slot fits). + int sm = -1; + uint32_t blk = 0, tok = 0; + if constexpr (kSlots) { + if (it - slot_base == 32) { + slot_base = it; + slot_l = fetch(slot_mapping, slot_base, -1); + } + sm = static_cast(__shfl_sync( + FINAL_MASK, static_cast(slot_l), it - slot_base, 32)); + blk = static_cast(sm) / static_cast(block_size); + tok = static_cast(sm) - blk * block_size; + } + + #pragma unroll + for (int step = 0; step < kSteps; step++) { + int const r = step * kStepRows + g; // row of the warp + int const head = head0 + r; + bool const row_ok = r < nrows; + int const so = step * kStepRows * kHeadDim; // step offset, elements + float e[kChunks][kElemsPerLane]; + if constexpr (kOneRow) { + loadElems( + reinterpret_cast(stage + r * kRowPitch) + 4 * v, + e[0]); + if constexpr (kNorm) { + normAndRope( + e[0], lane, eps, norm_w, /*do_rope=*/true, kRotaryDim, + reinterpret_cast(stage + kSteps * kRowPitch), + /*apply_norm=*/true); + } + } else { + Row x; + loadRow(stage + r * kRowPitch, v, x); + if constexpr (kNorm) normRope(x, w1, eps, cos4, sin4); + #pragma unroll + for (int c = 0; c < kChunks; c++) chunkElems(x, c, e[c]); + } + if constexpr (kRole == kQ) { + if (row_ok) { + #pragma unroll + for (int c = 0; c < kLaneChunks; c++) { + if constexpr (kOut == kQFp8Unit) { + storeUnitScaleQElemsFp8(q_fp8_out + out + so + 32 * c, + e[c]); + } else if constexpr (kOut == kQFp8) { + storeScaledQElemsFp8(q_fp8_out + out + so + 32 * c, + e[c], q_fp8_inv_scale); + } else { + if (q_out != nullptr) { + storeElems(q_out + out + so + 32 * c, e[c]); + } else if (q_fp8_out == nullptr && write_qkv) { + storeElems(in_place + so + 32 * c, e[c]); + } + if (q_fp8_out != nullptr) { + storeScaledQElemsFp8(q_fp8_out + out + so + 32 * c, + e[c], q_fp8_inv_scale); + } + } + } + } + } else if constexpr (kRole == kIQ) { + if (row_ok) { + #pragma unroll + for (int c = 0; c < kLaneChunks; c++) { + if constexpr (kOut == kIQOut) { + if constexpr (kFp8Idx) { + storeIndexQElemsFp8( + reinterpret_cast(index_q_out + out + so + 32 * c), + e[c]); + } else { + storeElems(index_q_out + out + so + 32 * c, e[c]); + } + } else if constexpr (kOut == kIQInPlace) { + storeElems(in_place + so + 32 * c, e[c]); + } + } + } + } else if constexpr (kRole == kK || kRole == kIK) { + if constexpr (kOut == kInPlace) { + if (row_ok) { + #pragma unroll + for (int c = 0; c < kLaneChunks; c++) { + storeElems(in_place + so + 32 * c, e[c]); + } + } + } + } + + if constexpr (kInsertKV && kRole == kIK) { + if constexpr (kNvfp4Idx) { + uint8_t* const page = + reinterpret_cast(index_cache) + blk * idx_s_block; + #pragma unroll + for (int c = 0; c < kLaneChunks; c++) { + uint8_t sf; + uint16_t const packed = + nvfp4QuantizeElems(e[c], 1.0f, sf); + if (sm >= 0 && row_ok) { + nvfp4StoreElems(page, /*kv=*/0, /*head=*/0, tok, block_size, + kOneRow ? lane : 8 * c + v, packed, sf); + } + } + } else if (sm >= 0 && row_ok) { + int64_t const off = static_cast(sm) * kHeadDim + 4 * v; + #pragma unroll + for (int c = 0; c < kLaneChunks; c++) { + if constexpr (kFp8Idx) { + storeElemsFp8( + reinterpret_cast(index_cache) + off + 32 * c, e[c]); + } else { + storeElems(index_cache + off + 32 * c, e[c]); + } + } + } + } else if constexpr (kInsertKV && (kRole == kK || kRole == kV)) { + constexpr int kv = kRole == kK ? 0 : 1; + if constexpr (kNvfp4 && !kOneRow) { + // One 16-value scale group per lane, regrouped through the ring + // row: lane (g, v) takes dims 16v .. 16v + 15 of row g. + uint8_t* const row = + ring + (it % kStages) * stage_bytes + r * kRowPitch; + if constexpr (kRole == kK) { + // The k in the model dtype, as the helper would quantize it (V + // rows already hold their model-dtype values). + #pragma unroll + for (int c = 0; c < kChunks; c++) { + storeElems( + reinterpret_cast(row + 64 * c + 8 * v), e[c]); + } + } + __syncwarp(); + float x[16]; + #pragma unroll + for (int k = 0; k < 4; k++) { + float q[kElemsPerLane]; + loadElems( + reinterpret_cast(row + 32 * v + 8 * k), q); + #pragma unroll + for (int i = 0; i < kElemsPerLane; i++) x[4 * k + i] = q[i]; + } + uint8_t sf; + uint2 const codes = nvfp4QuantizeGroup(x, kv_scale, sf); + if (sm >= 0 && row_ok) { + nvfp4StoreGroup( + reinterpret_cast(kv_cache) + blk * kv_s_block, kv, + head, tok, block_size, v, codes, sf); + } + } else if constexpr (kNvfp4) { + uint8_t* const page = + reinterpret_cast(kv_cache) + blk * kv_s_block; + #pragma unroll + for (int c = 0; c < kLaneChunks; c++) { + uint8_t sf; + uint16_t const packed = + nvfp4QuantizeElems(e[c], kv_scale, sf); + if (sm >= 0 && row_ok) { + nvfp4StoreElems(page, kv, head, tok, block_size, + kOneRow ? lane : 8 * c + v, packed, sf); + } + } + } else if (sm >= 0 && row_ok) { + cache_t* const dst = kv_cache + blk * kv_s_block + head * kv_s_head + + tok * kv_s_token + + (kv * kHeadDim + 4 * v) * kv_s_dim; + #pragma unroll + for (int c = 0; c < kLaneChunks; c++) { + if constexpr (!kHasIndex) { + using Converter = vllm::_typeConvert; + #pragma unroll + for (int i = 0; i < kElemsPerLane; i++) { + e[c][i] = Converter::convert(Converter::convert(e[c][i])); + } + } + storeCacheElems(dst + 32 * c * kv_s_dim, + e[c]); + } + } + } + } + in_place += src_step; + out += out_step; + __syncwarp(); + } +} + +template +__global__ void __launch_bounds__(kMaxWarps * 32) fusedMiniMaxM3RowGroupKernel( + scalar_t* __restrict__ qkv, scalar_t* __restrict__ q_out, + uint8_t* __restrict__ q_fp8_out, out_idx_t* __restrict__ index_q_out, + scalar_t const* __restrict__ q_norm_w, + scalar_t const* __restrict__ k_norm_w, + scalar_t const* __restrict__ iq_norm_w, + scalar_t const* __restrict__ ik_norm_w, + scalar_t const* __restrict__ cos_sin_cache, + int64_t const* __restrict__ positions, + int64_t const* __restrict__ slot_mapping, + int64_t const* __restrict__ index_slot_mapping, + cache_t* __restrict__ kv_cache, out_idx_t* __restrict__ index_cache, + float const eps, float const q_fp8_inv_scale, int const rotary_dim, + int const num_tokens, int const nq, int const nkv, int const niq, + int const block_size, int64_t const kv_s_block, int64_t const kv_s_head, + int64_t const kv_s_token, int64_t const kv_s_dim, int64_t const idx_s_block, + float const* __restrict__ kv_k_scale, float const* __restrict__ kv_v_scale, + bool const write_qkv) { + #if defined(__CUDA_ARCH__) && (__CUDA_ARCH__ >= 800) + extern __shared__ __align__(16) uint8_t smem[]; + Roles const roles(kQHeads, nq, nkv, niq, kInsertKV, kProcessIndex); + int const qkv_row = (nq + 2 * nkv + (kHasIndex ? niq + 1 : 0)) * kHeadDim; + // CTA (x, y) runs warps [y * cta_warps, (y + 1) * cta_warps) of the warp + // groups of tokens x, x + gridDim.x, ... + int const cta_warps = blockDim.x / 32; + int w = blockIdx.y * cta_warps + threadIdx.x / 32; + if (w >= roles.warps) return; + constexpr int kQStage = stageBytes(); + constexpr int kSideStage = stageBytes(); + int const q_ring = kStages * kQStage; + int const side_ring = kStages * kSideStage; + uint8_t* const ring_w = + smem + roles.ringOffset(w, q_ring, side_ring) - + roles.ringOffset(blockIdx.y * cta_warps, q_ring, side_ring); + #define RG_ARGS(NORM_W, SLOTS, CACHE_SCALE) \ + qkv, q_out, q_fp8_out, index_q_out, NORM_W, cos_sin_cache, positions, \ + SLOTS, kv_cache, index_cache, eps, q_fp8_inv_scale, num_tokens, nq, \ + niq, qkv_row, block_size, kv_s_block, kv_s_head, kv_s_token, \ + kv_s_dim, idx_s_block, CACHE_SCALE, write_qkv + #define RG_RUN(ROLE, STEPS, OUT) \ + runWarp + #define RG_RUN1(ROLE, OUT) \ + runWarp + if (w < roles.q_warps) { + int const h0 = w * kQHeads; + constexpr int kSteps = kQHeads / kRows; + uint8_t* const ring = ring_w; + int const rows = min(kQHeads, nq - h0); + if (q_out == nullptr && q_fp8_out != nullptr) { + if (q_fp8_inv_scale == 1.0f) { + RG_RUN(kQ, kSteps, kQFp8Unit) + (h0, rows, h0, ring, kQStage, RG_ARGS(q_norm_w, nullptr, 1.0f)); + } else { + RG_RUN(kQ, kSteps, kQFp8) + (h0, rows, h0, ring, kQStage, RG_ARGS(q_norm_w, nullptr, 1.0f)); + } + } else { + RG_RUN(kQ, kSteps, kQAny) + (h0, rows, h0, ring, kQStage, RG_ARGS(q_norm_w, nullptr, 1.0f)); + } + } else { + uint8_t* const ring = ring_w; + w -= roles.q_warps; + if (w < roles.kv_warps) { + int const h0 = roles.kv_one ? w : w * kRows; + float const scale = kNvfp4 ? *kv_k_scale : 1.0f; + int const rows = roles.kv_one ? 1 : min(kRows, nkv - h0); + if (rows == 1) { + if (write_qkv) { + RG_RUN1(kK, kInPlace) + (h0, 1, nq + h0, ring, kSideStage, + RG_ARGS(k_norm_w, slot_mapping, scale)); + } else { + RG_RUN1(kK, kNone) + (h0, 1, nq + h0, ring, kSideStage, + RG_ARGS(k_norm_w, slot_mapping, scale)); + } + } else if (write_qkv) { + RG_RUN(kK, 1, kInPlace) + (h0, rows, nq + h0, ring, kSideStage, + RG_ARGS(k_norm_w, slot_mapping, scale)); + } else { + RG_RUN(kK, 1, kNone) + (h0, rows, nq + h0, ring, kSideStage, + RG_ARGS(k_norm_w, slot_mapping, scale)); + } + } else if ((w -= roles.kv_warps) < roles.v_warps) { + if constexpr (kInsertKV) { + int const h0 = roles.kv_one ? w : w * kRows; + int const rows = roles.kv_one ? 1 : min(kRows, nkv - h0); + float const scale = kNvfp4 ? *kv_v_scale : 1.0f; + if (rows == 1) { + RG_RUN1(kV, kNone) + (h0, 1, nq + nkv + h0, ring, kSideStage, + RG_ARGS(nullptr, slot_mapping, scale)); + } else { + RG_RUN(kV, 1, kNone) + (h0, rows, nq + nkv + h0, ring, kSideStage, + RG_ARGS(nullptr, slot_mapping, scale)); + } + } + } else if ((w -= roles.v_warps) < roles.iq_warps) { + if constexpr (kProcessIndex) { + int const h0 = roles.iq_one ? w : w * kRows; + int const rows = roles.iq_one ? 1 : min(kRows, niq - h0); + int const qh0 = nq + 2 * nkv + h0; + if (rows == 1) { + if (index_q_out != nullptr) { + RG_RUN1(kIQ, kIQOut) + (h0, 1, qh0, ring, kSideStage, RG_ARGS(iq_norm_w, nullptr, 1.0f)); + } else if (write_qkv) { + RG_RUN1(kIQ, kIQInPlace) + (h0, 1, qh0, ring, kSideStage, RG_ARGS(iq_norm_w, nullptr, 1.0f)); + } else { + RG_RUN1(kIQ, kNone) + (h0, 1, qh0, ring, kSideStage, RG_ARGS(iq_norm_w, nullptr, 1.0f)); + } + } else if (index_q_out != nullptr) { + RG_RUN(kIQ, 1, kIQOut) + (h0, rows, qh0, ring, kSideStage, RG_ARGS(iq_norm_w, nullptr, 1.0f)); + } else if (write_qkv) { + RG_RUN(kIQ, 1, kIQInPlace) + (h0, rows, qh0, ring, kSideStage, RG_ARGS(iq_norm_w, nullptr, 1.0f)); + } else { + RG_RUN(kIQ, 1, kNone) + (h0, rows, qh0, ring, kSideStage, RG_ARGS(iq_norm_w, nullptr, 1.0f)); + } + } + } else if constexpr (kProcessIndex) { + if (write_qkv) { + RG_RUN1(kIK, kInPlace) + (0, 1, nq + 2 * nkv + niq, ring, kSideStage, + RG_ARGS(ik_norm_w, index_slot_mapping, 1.0f)); + } else { + RG_RUN1(kIK, kNone) + (0, 1, nq + 2 * nkv + niq, ring, kSideStage, + RG_ARGS(ik_norm_w, index_slot_mapping, 1.0f)); + } + } + } + #undef RG_RUN + #undef RG_RUN1 + #undef RG_ARGS + #if __CUDA_ARCH__ >= 900 + cudaTriggerProgrammaticLaunchCompletion(); + #endif + #endif +} + +// Shared memory of the largest CTA of cta_warps warps. +template +__host__ inline int smemBytes(Roles const& r, int const cta_warps) { + int const q_ring = kStages * stageBytes(); + int const side_ring = kStages * stageBytes(); + int bytes = 0; + for (int w0 = 0; w0 < r.warps; w0 += cta_warps) { + int const w1 = std::min(w0 + cta_warps, r.warps); + bytes = std::max(bytes, r.ringOffset(w1, q_ring, side_ring) - + r.ringOffset(w0, q_ring, side_ring)); + } + return bytes; +} + +// Launch with the grid this instantiation's occupancy allows: the CTAs walk +// the tokens with stride gridDim.x. +template +void launch(cudaLaunchConfig_t config, int const num_tokens, Args... args) { + auto* const kernel = + fusedMiniMaxM3RowGroupKernel; + // Per CTA shape (warps, shared memory), computed once: the resident CTAs + // per SM. + static bool const opted_in = + cudaFuncSetAttribute(kernel, cudaFuncAttributeMaxDynamicSharedMemorySize, + get_device_prop()->sharedMemPerBlockOptin) == + cudaSuccess; + static int smem_of[kMaxWarps + 1] = {}; + static int resident_of[kMaxWarps + 1] = {}; + int const warps = config.blockDim.x / 32; + int const smem = static_cast(config.dynamicSmemBytes); + if (smem_of[warps] != smem) { + (void)opted_in; + int n = 0; + cudaOccupancyMaxActiveBlocksPerMultiprocessor(&n, kernel, warps * 32, smem); + resident_of[warps] = n > 0 ? n : 1; + smem_of[warps] = smem; + } + int const ctas_per_token = config.gridDim.y; + config.gridDim.x = std::max( + 1, std::min(num_tokens, get_device_prop()->multiProcessorCount * + resident_of[warps] / ctas_per_token)); + cudaLaunchKernelEx(&config, kernel, args...); +} + +} // namespace rg +#endif // USE_ROCM + +#ifndef USE_ROCM +// q heads per q warp: four (one step, shorter warps) for a launch of at most +// 128 q rows, or while a token has at most 32 q heads and there are few q rows +// per SM; eight otherwise (fewer, longer warps keep more CTAs resident). +inline int rgQHeads(int const num_tokens, int const nq) { + static int const num_sms = get_device_prop()->multiProcessorCount; + int64_t const q_rows = static_cast(num_tokens) * nq; + return q_rows <= 128 || (nq <= 32 && q_rows < 64 * num_sms) ? 4 : 8; +} +// Warps per CTA: a token's whole warp group once there are enough tokens to +// give every SM CTAs; before that, eight, which spreads a token over SMs. +inline int rgCtaWarps(int const num_tokens) { + static int const num_sms = get_device_prop()->multiProcessorCount; + return 2 * num_tokens >= 3 * num_sms ? rg::kMaxWarps : 8; +} +#endif + // ──────────────────────────────────────────────────────────────────────────── // Launch wrapper // ──────────────────────────────────────────────────────────────────────────── @@ -809,9 +1683,10 @@ void launchFusedMiniMaxM3( float const q_fp8_inv_scale, int const rotary_dim, int const num_tokens, int const nq, int const nkv, int const niq, int const block_size, int64_t const kv_s_block, int64_t const kv_s_head, int64_t const kv_s_token, - int64_t const kv_s_dim, bool const has_index, bool const insert_kv, - bool const process_index, bool const fp8_idx, float const* kv_k_scale, - float const* kv_v_scale, cudaStream_t stream) { + int64_t const kv_s_dim, int64_t const idx_s_block, bool const has_index, + bool const insert_kv, bool const process_index, bool const fp8_idx, + bool const nvfp4_idx, float const* kv_k_scale, float const* kv_v_scale, + bool const write_qkv, bool const slots_fit_int32, cudaStream_t stream) { // Index outputs are scalar_t (bf16) or e4m3 bytes (uint8_t); reinterpret the // void* pointers per instantiation in the LAUNCH macro. // Slot count must match the kernel's compile-time gating. @@ -843,73 +1718,437 @@ void launchFusedMiniMaxM3( config.attrs = attrs; config.numAttrs = (sm_version >= 90) ? 1 : 0; - #define LAUNCH(HAS_INDEX, INSERT, PROCESS_INDEX, FP8, OUT_T) \ - cudaLaunchKernelEx( \ - &config, \ - fusedMiniMaxM3QNormRopeKVInsertKernel, \ - qkv, q_out, q_fp8_out, reinterpret_cast(index_q_out), \ - q_norm_w, k_norm_w, iq_norm_w, ik_norm_w, cos_sin_cache, positions, \ + // The row-group kernel: same bytes, for rotary_dim == 64 on SM80+ (cp.async). + int const q_heads = rgQHeads(num_tokens, nq); + rg::Roles const roles(q_heads, nq, nkv, niq, insert_kv, process_index); + auto const rg_smem = [&](int const warps) { + return q_heads == 8 ? rg::smemBytes<8>(roles, warps) + : rg::smemBytes<4>(roles, warps); + }; + int cta_warps = + std::min({roles.warps, rgCtaWarps(num_tokens), rg::kMaxWarps}); + // Eight warps take at most 63 KB; a whole group can exceed the 99 KB per + // block of SM 8.6 / 8.9 / 12.x. + static int const max_block_smem = get_device_prop()->sharedMemPerBlockOptin; + if (rg_smem(cta_warps) > max_block_smem) cta_warps = std::min(roles.warps, 8); + auto const aligned = [](void const* p, uintptr_t const n) { + return reinterpret_cast(p) % n == 0; + }; + bool const row_groups = + sm_version >= 80 && rotary_dim == rg::kRotaryDim && slots_fit_int32 && + aligned(qkv, 16) && aligned(cos_sin_cache, 16) && aligned(q_norm_w, 8) && + aligned(k_norm_w, 8) && aligned(iq_norm_w, 8) && aligned(ik_norm_w, 8); + cudaLaunchConfig_t rg_config = config; + rg_config.blockDim = dim3(cta_warps * 32); + rg_config.gridDim = dim3(1, (roles.warps + cta_warps - 1) / cta_warps); + rg_config.dynamicSmemBytes = rg_smem(cta_warps); + + #define LAUNCH_ARGS(OUT_T) \ + qkv, q_out, q_fp8_out, reinterpret_cast(index_q_out), q_norm_w, \ + k_norm_w, iq_norm_w, ik_norm_w, cos_sin_cache, positions, \ slot_mapping, index_slot_mapping, kv_cache, \ reinterpret_cast(index_cache), eps, q_fp8_inv_scale, \ rotary_dim, num_tokens, nq, nkv, niq, block_size, kv_s_block, \ - kv_s_head, kv_s_token, kv_s_dim, kv_k_scale, kv_v_scale) + kv_s_head, kv_s_token, kv_s_dim, idx_s_block, kv_k_scale, kv_v_scale + #define LAUNCH_RG(QH, HAS_INDEX, INSERT, PROCESS_INDEX, FP8, NVFP4_IDX, \ + OUT_T) \ + rg::launch(rg_config, num_tokens, \ + LAUNCH_ARGS(OUT_T), write_qkv) + #define LAUNCH(HAS_INDEX, INSERT, PROCESS_INDEX, FP8, NVFP4_IDX, OUT_T) \ + do { \ + if (!row_groups) { \ + cudaLaunchKernelEx( \ + &config, \ + fusedMiniMaxM3QNormRopeKVInsertKernel< \ + scalar_t, cache_t, kv_dt, kNvfp4, OUT_T, HAS_INDEX, INSERT, \ + PROCESS_INDEX, FP8, NVFP4_IDX>, \ + LAUNCH_ARGS(OUT_T)); \ + } else if (q_heads == 8) { \ + LAUNCH_RG(8, HAS_INDEX, INSERT, PROCESS_INDEX, FP8, NVFP4_IDX, OUT_T); \ + } else { \ + LAUNCH_RG(4, HAS_INDEX, INSERT, PROCESS_INDEX, FP8, NVFP4_IDX, OUT_T); \ + } \ + } while (0) #else // ROCm: standard kernel launch syntax (no PDL/stream serialization). // clang-format off - #define LAUNCH(HAS_INDEX, INSERT, PROCESS_INDEX, FP8, OUT_T) \ + #define LAUNCH(HAS_INDEX, INSERT, PROCESS_INDEX, FP8, NVFP4_IDX, OUT_T) \ fusedMiniMaxM3QNormRopeKVInsertKernel< \ scalar_t, cache_t, kv_dt, kNvfp4, OUT_T, HAS_INDEX, INSERT, \ - PROCESS_INDEX, FP8><<>>( \ + PROCESS_INDEX, FP8, NVFP4_IDX><<>>( \ qkv, q_out, q_fp8_out, reinterpret_cast(index_q_out), \ q_norm_w, k_norm_w, iq_norm_w, ik_norm_w, cos_sin_cache, positions, \ slot_mapping, index_slot_mapping, kv_cache, \ reinterpret_cast(index_cache), eps, q_fp8_inv_scale, \ rotary_dim, num_tokens, nq, nkv, niq, block_size, kv_s_block, \ - kv_s_head, kv_s_token, kv_s_dim, kv_k_scale, kv_v_scale) + kv_s_head, kv_s_token, kv_s_dim, idx_s_block, kv_k_scale, \ + kv_v_scale) // clang-format on #endif if constexpr (kNvfp4) { // NVFP4 main cache: sparse serving only (the host checks insert_kv). if (!process_index) { - LAUNCH(true, true, false, false, scalar_t); + LAUNCH(true, true, false, false, false, scalar_t); + } else if (nvfp4_idx) { + LAUNCH(true, true, true, true, true, uint8_t); } else if (fp8_idx) { - LAUNCH(true, true, true, true, uint8_t); + LAUNCH(true, true, true, true, false, uint8_t); } else { - LAUNCH(true, true, true, false, scalar_t); + LAUNCH(true, true, true, false, false, scalar_t); } } else if (has_index) { if (!process_index) { if (insert_kv) { - LAUNCH(true, true, false, false, scalar_t); + LAUNCH(true, true, false, false, false, scalar_t); } else { - LAUNCH(true, false, false, false, scalar_t); + LAUNCH(true, false, false, false, false, scalar_t); } } else if (insert_kv) { - if (fp8_idx) { - LAUNCH(true, true, true, true, + if (nvfp4_idx) { + LAUNCH(true, true, true, true, true, + uint8_t); // sparse serving, NVFP4 index-K, fp8 index_q + } else if (fp8_idx) { + LAUNCH(true, true, true, true, false, uint8_t); // sparse serving, fp8 index outputs } else { - LAUNCH(true, true, true, false, scalar_t); // sparse serving, bf16 + LAUNCH(true, true, true, false, false, + scalar_t); // sparse serving, bf16 } } else { if (fp8_idx) { - LAUNCH(true, false, true, true, + LAUNCH(true, false, true, true, false, uint8_t); // sparse profiling, fp8 index_q } else { - LAUNCH(true, false, true, false, scalar_t); // sparse profiling, bf16 + LAUNCH(true, false, true, false, false, + scalar_t); // sparse profiling, bf16 } } + } else if (insert_kv) { + LAUNCH(false, true, false, false, false, scalar_t); // dense, KV insert } else { - // Dense layer: never has an index branch and never inserts here (the - // generic Attention layer owns the KV insert). - LAUNCH(false, false, false, false, scalar_t); + LAUNCH(false, false, false, false, false, scalar_t); // dense } #undef LAUNCH +#undef LAUNCH_RG +#undef LAUNCH_ARGS +} + +#ifndef USE_ROCM +// ──────────────────────────────────────────────────────────────────────────── +// DSpark draft context K/V: every draft layer in one launch (bf16 in). +// ──────────────────────────────────────────────────────────────────────────── +// The draft projects the target's context states to the K/V of all its layers +// with one GEMM, ``all_kv [N, L, 2, nkv, 128]``. blockIdx.z picks the layer, +// blockIdx.y the head, blockIdx.x a run of tokens. Eight lanes own one token; +// lane l holds two 8-dim chunks of K and of V, A = dims [8l, 8l + 8) and +// B = dims [64 + 8l, 64 + 8l + 8), so with rotary_dim == 64 every lane rotates +// its A chunk (partner lane l ^ 4) and no lane idles through the RoPE. K gets +// the layer's Gemma k-norm (rounded to bf16 like the unfused norm) and partial +// NeoX RoPE (fp32 math rounded once, like rotary_embedding); K and V go into +// the token's cache row like reshape_and_cache_flash (fp8: e4m3(bf16(x) * +// (1 / scale)); a unit scale converts the bf16 pairs directly). +// Layer caches are separate allocations of one layout, logical shape +// [nb, nkv, bs, 2*128] with K at [..., :128] and V at [..., 128:]; they are +// addressed as layer 0's cache plus a per-layer element offset (likewise the +// fp8 k/v scales from layer 0's k scale), so the launch is CUDA-graph safe. +constexpr int kCtxLanesPerRow = 8; +constexpr int kCtxElems = 8; // bf16 per 16-byte chunk +constexpr int kCtxHalfVecs = kHeadDim / 2 / kCtxElems; // 8: chunk B offset +constexpr int kCtxBlockSize = 256; +constexpr int kCtxRowsPerBlock = kCtxBlockSize / kCtxLanesPerRow; // 32 + +__device__ __forceinline__ float bf16lo(uint32_t v) { + return __uint_as_float(v << 16); +} +__device__ __forceinline__ float bf16hi(uint32_t v) { + return __uint_as_float(v & 0xffff0000u); +} +__device__ __forceinline__ uint32_t packBf16(float lo, float hi) { + __nv_bfloat162 const r = __floats2bfloat162_rn(lo, hi); + return *reinterpret_cast(&r); +} +// Loads of what the PDL predecessor may still be writing: coherent (an .nc +// load may be hoisted above griddepcontrol.wait). +__device__ __forceinline__ uint4 ldgV4(uint4 const* p) { + uint4 r; + asm volatile("ld.global.v4.u32 {%0, %1, %2, %3}, [%4];" + : "=r"(r.x), "=r"(r.y), "=r"(r.z), "=r"(r.w) + : "l"(p)); + return r; +} +// Low 32 bits of an int64 (positions and slots fit). +__device__ __forceinline__ int ldgLo32(int64_t const* p) { + int r; + asm volatile("ld.global.b32 %0, [%1];" : "=r"(r) : "l"(p)); + return r; +} +// Same approximation as flashinfer's gemma_rmsnorm. +__device__ __forceinline__ float rsqrtApproxFtz(float x) { + float r; + asm("rsqrt.approx.ftz.f32 %0, %1;" : "=f"(r) : "f"(x)); + return r; +} +// e4m3 of four floats, low byte first. +__device__ __forceinline__ uint32_t packE4m3(float a, float b, float c, + float d) { + #if defined(__CUDA_ARCH__) && __CUDA_ARCH__ >= 890 + uint32_t r; + asm("{\n" + " .reg .b16 lo, hi;\n" + " cvt.rn.satfinite.e4m3x2.f32 lo, %2, %1;\n" + " cvt.rn.satfinite.e4m3x2.f32 hi, %4, %3;\n" + " mov.b32 %0, {lo, hi};\n" + "}" + : "=r"(r) + : "f"(a), "f"(b), "f"(c), "f"(d)); + return r; + #else + uint32_t const lo = + __nv_cvt_float2_to_fp8x2(make_float2(a, b), __NV_SATFINITE, __NV_E4M3); + uint32_t const hi = + __nv_cvt_float2_to_fp8x2(make_float2(c, d), __NV_SATFINITE, __NV_E4M3); + return lo | (hi << 16); + #endif +} +// Eight bf16 (4 packed pairs) * inv_scale -> e4m3, low byte first. +__device__ __forceinline__ uint2 bf16x8ToE4m3(uint4 const p, + float const inv_scale) { + uint32_t const w[4] = {p.x, p.y, p.z, p.w}; + uint32_t r[2]; + #pragma unroll + for (int i = 0; i < 2; i++) { + r[i] = packE4m3(bf16lo(w[2 * i]) * inv_scale, bf16hi(w[2 * i]) * inv_scale, + bf16lo(w[2 * i + 1]) * inv_scale, + bf16hi(w[2 * i + 1]) * inv_scale); + } + return make_uint2(r[0], r[1]); +} + #if ((defined(__CUDA_ARCH_FAMILY_SPECIFIC__) && \ + __CUDA_ARCH_FAMILY_SPECIFIC__ >= 1000) || \ + (defined(__CUDA_ARCH_SPECIFIC__) && __CUDA_ARCH_SPECIFIC__ >= 1000)) && \ + defined(CUDART_VERSION) && CUDART_VERSION >= 13010 + #define VLLM_CTX_KV_BF16X2_TO_E4M3X2 + #endif +// Eight bf16 -> e4m3 with a unit scale (exact); one convert per pair where +// the arch and toolkit (PTX ISA 9.1+) convert bf16x2 -> e4m3x2. +__device__ __forceinline__ uint2 bf16x8ToE4m3(uint4 const p) { + #ifdef VLLM_CTX_KV_BF16X2_TO_E4M3X2 + uint32_t r[2]; + uint32_t const w[4] = {p.x, p.y, p.z, p.w}; + #pragma unroll + for (int i = 0; i < 2; i++) { + asm("{\n" + " .reg .b16 lo, hi;\n" + " cvt.rn.satfinite.e4m3x2.bf16x2 lo, %1;\n" + " cvt.rn.satfinite.e4m3x2.bf16x2 hi, %2;\n" + " mov.b32 %0, {lo, hi};\n" + "}" + : "=r"(r[i]) + : "r"(w[2 * i]), "r"(w[2 * i + 1])); + } + return make_uint2(r[0], r[1]); + #else + return bf16x8ToE4m3(p, 1.0f); + #endif } +template +__global__ void __launch_bounds__(kCtxBlockSize) + fusedMiniMaxM3DSparkContextKVKernel( + uint4 const* __restrict__ all_kv, // bf16 [N, L, 2, nkv, 128] + uint4 const* __restrict__ k_norm_w, // bf16 [L, 128] + uint4 const* __restrict__ cos_sin_cache, // bf16 [max_pos, rot] + int64_t const* __restrict__ positions, // [N] + int64_t const* __restrict__ slot_mapping, // [N], < 0: no write + uint8_t* __restrict__ kv_cache, // layer 0's cache, bytes + int64_t const* __restrict__ cache_offsets, // [L] elements from it + float const* __restrict__ scales, // layer 0's k scale + int64_t const* __restrict__ scale_offsets, // [2, L] k, v; floats + float const eps, int const rotary_dim, int const num_tokens, + int const num_layers, int const block_shift, int const s_token, + int const s_layer, int const s_side, int const s_head, + int const kv_s_block, int const kv_s_head, int const kv_s_token) { + // Strides are in 16-byte (8 x bf16) units for all_kv / cos_sin, elements + // for the cache. + constexpr int kElemBytes = kFp8 ? 1 : 2; + int const row0 = blockIdx.x * kCtxRowsPerBlock; + // Warps with no token exit; the others run their tail rows too (the + // shuffles need every lane) and skip the store. + if (row0 + threadIdx.x / 32 * (32 / kCtxLanesPerRow) >= num_tokens) return; + int const head = blockIdx.y; + int const layer = blockIdx.z; + int const lane = threadIdx.x % kCtxLanesPerRow; + int const dim0 = lane * kCtxElems; // chunk A; chunk B is 64 dims later + int const t = row0 + threadIdx.x / kCtxLanesPerRow; + + int const half = rotary_dim / 2; + bool const in_rope = dim0 < rotary_dim; + bool const first_half = dim0 < half; + int const cs_vec = + in_rope ? (first_half ? dim0 : dim0 - half) / kCtxElems : 0; + int const lane_xor = half / kCtxElems; + int const rot_vecs = rotary_dim / kCtxElems; + // x * cos - partner * sin in the first half, + in the second. + uint32_t const sin_sign = first_half ? 0x80008000u : 0u; + + uint8_t* const cache = + kv_cache + (cache_offsets[layer] + head * kv_s_head + dim0) * kElemBytes; + float k_scale = 1.0f, v_scale = 1.0f; + if constexpr (kFp8) { + int64_t const k_off = scale_offsets[layer]; + int64_t const v_off = scale_offsets[num_layers + layer]; + k_scale = scales[k_off]; + v_scale = scales[v_off]; + } + float g[2][kCtxElems]; // 1 + w, chunks A and B + #pragma unroll + for (int c = 0; c < 2; c++) { + uint4 const w = + k_norm_w[layer * (kHeadDim / kCtxElems) + c * kCtxHalfVecs + lane]; + uint32_t const wp[4] = {w.x, w.y, w.z, w.w}; + #pragma unroll + for (int i = 0; i < 4; i++) { + g[c][2 * i] = 1.0f + bf16lo(wp[i]); + g[c][2 * i + 1] = 1.0f + bf16hi(wp[i]); + } + } + + #if defined(__CUDA_ARCH__) && (__CUDA_ARCH__ >= 900) + cudaGridDependencySynchronize(); + #endif + + bool const valid = t < num_tokens; + int const ts = valid ? t : 0; + int const pos = ldgLo32(positions + ts); + int const slot = valid ? ldgLo32(slot_mapping + ts) : -1; + uint4 const* const src = + all_kv + ts * s_token + layer * s_layer + head * s_head + lane; + uint4 kr[2], vr[2]; + #pragma unroll + for (int c = 0; c < 2; c++) { + kr[c] = ldgV4(src + c * kCtxHalfVecs); + vr[c] = ldgV4(src + s_side + c * kCtxHalfVecs); + } + // Lanes past the rotary dims load (and later discard) a valid row too. + uint4 const* const cs = cos_sin_cache + pos * rot_vecs + cs_vec; + uint4 const cr = cs[0]; + uint4 const sr = cs[lane_xor]; + + bool const store = slot >= 0; + // Power-of-two block size: shift and mask, not division. + uint8_t* const dst = + cache + (static_cast(slot >> block_shift) * kv_s_block + + (slot & ((1 << block_shift) - 1)) * kv_s_token) * + kElemBytes; + constexpr int kChunkB = kHeadDim / 2 * kElemBytes; + constexpr int kV = kHeadDim * kElemBytes; + float const k_inv = __fdividef(1.0f, k_scale); + float const v_inv = __fdividef(1.0f, v_scale); + #ifdef VLLM_CTX_KV_BF16X2_TO_E4M3X2 + bool const unit_scale = k_scale == 1.0f && v_scale == 1.0f; + #else + constexpr bool unit_scale = false; + #endif + // Eight bf16 -> the cache row at byte offset `off`. + auto put = [&](int const off, uint4 const x, float const inv_scale) { + if constexpr (kFp8) { + uint2 const y = unit_scale ? bf16x8ToE4m3(x) : bf16x8ToE4m3(x, inv_scale); + if (store) *reinterpret_cast(dst + off) = y; + } else { + if (store) *reinterpret_cast(dst + off) = x; + } + }; + + // V first, so its registers free up before the K math. + #pragma unroll + for (int c = 0; c < 2; c++) put(kV + c * kChunkB, vr[c], v_inv); + + // K: Gemma RMSNorm over the 8 lanes of the row. + float x[2][kCtxElems]; + float sumsq = 0.0f; + #pragma unroll + for (int c = 0; c < 2; c++) { + uint32_t const kp[4] = {kr[c].x, kr[c].y, kr[c].z, kr[c].w}; + #pragma unroll + for (int j = 0; j < 4; j++) { + x[c][2 * j] = bf16lo(kp[j]); + x[c][2 * j + 1] = bf16hi(kp[j]); + sumsq = fmaf(x[c][2 * j], x[c][2 * j], sumsq); + sumsq = fmaf(x[c][2 * j + 1], x[c][2 * j + 1], sumsq); + } + } + #pragma unroll + for (int mask = kCtxLanesPerRow / 2; mask > 0; mask >>= 1) { + sumsq += __shfl_xor_sync(FINAL_MASK, sumsq, mask, 32); + } + float const rr = rsqrtApproxFtz(sumsq * (1.0f / kHeadDim) + eps); + // bf16-rounded norm output, packed. + uint32_t kn[2][4]; + #pragma unroll + for (int c = 0; c < 2; c++) { + #pragma unroll + for (int j = 0; j < 4; j++) { + kn[c][j] = packBf16(x[c][2 * j] * rr * g[c][2 * j], + x[c][2 * j + 1] * rr * g[c][2 * j + 1]); + } + } + // RoPE on chunk A; the partner chunk is half / 8 lanes away. + uint32_t const cp[4] = {cr.x, cr.y, cr.z, cr.w}; + uint32_t const sp[4] = {sr.x ^ sin_sign, sr.y ^ sin_sign, sr.z ^ sin_sign, + sr.w ^ sin_sign}; + #pragma unroll + for (int j = 0; j < 4; j++) { + uint32_t const pn = __shfl_xor_sync(FINAL_MASK, kn[0][j], lane_xor, 32); + uint32_t const rot = + packBf16(__fmaf_rn(bf16lo(kn[0][j]), bf16lo(cp[j]), + __fmul_rn(bf16lo(pn), bf16lo(sp[j]))), + __fmaf_rn(bf16hi(kn[0][j]), bf16hi(cp[j]), + __fmul_rn(bf16hi(pn), bf16hi(sp[j])))); + kn[0][j] = in_rope ? rot : kn[0][j]; + } + #pragma unroll + for (int c = 0; c < 2; c++) { + put(c * kChunkB, make_uint4(kn[c][0], kn[c][1], kn[c][2], kn[c][3]), k_inv); + } + + #if defined(__CUDA_ARCH__) && (__CUDA_ARCH__ >= 900) + cudaTriggerProgrammaticLaunchCompletion(); + #endif +} + +template +void launchFusedMiniMaxM3DSparkContextKV( + uint4 const* all_kv, uint4 const* k_norm_w, uint4 const* cos_sin_cache, + int64_t const* positions, int64_t const* slot_mapping, uint8_t* kv_cache, + int64_t const* cache_offsets, float const* scales, + int64_t const* scale_offsets, float const eps, int const rotary_dim, + int const num_tokens, int const num_layers, int const nkv, + int const block_shift, int const s_token, int const s_layer, + int const s_side, int const s_head, int const kv_s_block, + int const kv_s_head, int const kv_s_token, cudaStream_t stream) { + if (num_tokens == 0) return; + static int const sm_version = getSMVersion(); + cudaLaunchConfig_t config; + config.gridDim = dim3((num_tokens + kCtxRowsPerBlock - 1) / kCtxRowsPerBlock, + nkv, num_layers); + config.blockDim = dim3(kCtxBlockSize); + config.dynamicSmemBytes = 0; + config.stream = stream; + cudaLaunchAttribute attrs[1]; + attrs[0].id = cudaLaunchAttributeProgrammaticStreamSerialization; + attrs[0].val.programmaticStreamSerializationAllowed = 1; + config.attrs = attrs; + config.numAttrs = (sm_version >= 90) ? 1 : 0; + cudaLaunchKernelEx(&config, fusedMiniMaxM3DSparkContextKVKernel, all_kv, + k_norm_w, cos_sin_cache, positions, slot_mapping, kv_cache, + cache_offsets, scales, scale_offsets, eps, rotary_dim, + num_tokens, num_layers, block_shift, s_token, s_layer, + s_side, s_head, kv_s_block, kv_s_head, kv_s_token); +} +#endif // !USE_ROCM + } // namespace minimax_m3_fused_ops } // namespace vllm @@ -948,8 +2187,9 @@ void launchFusedMiniMaxM3( static_cast(eps), 1.0f / static_cast(q_fp8_scale), \ static_cast(rotary_dim), num_tokens, nq, nkv, niq, \ static_cast(block_size), kv_s_block, kv_s_head, kv_s_token, \ - kv_s_dim, has_index, insert_kv, process_index, fp8_idx, kv_k_scale_ptr, \ - kv_v_scale_ptr, stream) + kv_s_dim, idx_s_block, has_index, insert_kv, process_index, fp8_idx, \ + nvfp4_idx, kv_k_scale_ptr, kv_v_scale_ptr, write_qkv, slots_fit_int32, \ + stream) #define CALL_FUSED_MINIMAX_M3(_RAW_T, CACHE_T, KV_DTYPE) \ CALL_FUSED_MINIMAX_M3_IMPL(CACHE_T, KV_DTYPE, false) // clang-format on @@ -980,7 +2220,7 @@ void fused_minimax_m3_qknorm_rope_kv_insert( q_fp8_out, // [N, nq*128] contiguous E4M3 double q_fp8_scale, std::optional kv_k_scale, // [] f32, NVFP4 only - std::optional kv_v_scale) { + std::optional kv_v_scale, bool write_qkv) { STD_TORCH_CHECK(qkv.is_cuda() && qkv.is_contiguous(), "qkv must be contiguous CUDA"); STD_TORCH_CHECK( @@ -1032,11 +2272,8 @@ void fused_minimax_m3_qknorm_rope_kv_insert( " + num_index_heads + 1) * 128 for sparse, " "(num_heads + 2*num_kv_heads) * 128 for dense"); - // Only the sparse layer inserts here (dense lets the generic Attention layer - // own the KV write); there is no dense+insert kernel instantiation. - STD_TORCH_CHECK( - !insert_kv || has_index, - "insert mode (kv_cache) requires the index branch (sparse layer)"); + STD_TORCH_CHECK(!nvfp4_kv || has_index, + "nvfp4 kv_cache requires the index branch (sparse layer)"); STD_TORCH_CHECK(has_index || !skip_index_branch, "skip_index_branch requires sparse qkv rows"); if (process_index) { @@ -1084,14 +2321,18 @@ void fused_minimax_m3_qknorm_rope_kv_insert( kv_cache->scalar_type() == torch::headeronly::ScalarType::Byte, "fp8 kv_cache must use uint8 storage"); } - // Indexer index-K cache: independent dtype -- qkv dtype or fp8 e4m3. + // Indexer index-K cache: independent dtype -- qkv dtype, fp8 e4m3, or + // uint8 packed NVFP4 pages. if (process_index) { STD_TORCH_CHECK( index_cache.has_value() && (index_cache->scalar_type() == qkv.scalar_type() || index_cache->scalar_type() == - torch::headeronly::ScalarType::Float8_e4m3fn), - "insert mode requires index_cache matching qkv dtype or fp8 e4m3"); + torch::headeronly::ScalarType::Float8_e4m3fn || + index_cache->scalar_type() == + torch::headeronly::ScalarType::Byte), + "insert mode requires index_cache matching qkv dtype, fp8 e4m3, " + "or uint8 (nvfp4)"); } STD_TORCH_CHECK(kv_cache->dim() == 4 && kv_cache->stride(3) == 1, "kv_cache must be [nb,nkv,bs,2*head_dim] with contiguous " @@ -1150,6 +2391,9 @@ void fused_minimax_m3_qknorm_rope_kv_insert( STD_TORCH_CHECK(std::isfinite(q_fp8_scale) && q_fp8_scale > 0.0, "q_fp8_scale must be finite and positive"); } + STD_TORCH_CHECK(write_qkv || (!q_out.has_value() && q_fp8_out.has_value()), + "write_qkv=False writes q only to q_fp8_out: pass q_fp8_out " + "and no q_out"); if (index_q_out.has_value()) { STD_TORCH_CHECK(process_index, "index_q_out requires index branch processing"); @@ -1166,16 +2410,36 @@ void fused_minimax_m3_qknorm_rope_kv_insert( } // fp8 index path: the index-K cache and index-Q outputs are e4m3 bytes while - // q/k/v + q_out stay qkv dtype. Both index outputs must agree. + // q/k/v + q_out stay qkv dtype. Both index outputs must agree. The NVFP4 + // index path keeps e4m3 index-Q and stores index-K as packed NVFP4 pages. auto const kFp8 = torch::headeronly::ScalarType::Float8_e4m3fn; + bool const nvfp4_idx = + process_index && insert_kv && + index_cache->scalar_type() == torch::headeronly::ScalarType::Byte; bool const fp8_idx = process_index && - ((index_cache.has_value() && index_cache->scalar_type() == kFp8) || + (nvfp4_idx || + (index_cache.has_value() && index_cache->scalar_type() == kFp8) || (index_q_out.has_value() && index_q_out->scalar_type() == kFp8)); - if (fp8_idx) { + int64_t idx_s_block = 0; + if (nvfp4_idx) { +#ifdef USE_ROCM + STD_TORCH_CHECK(false, "nvfp4 index_cache is not supported on ROCm"); +#endif + int64_t const page_width = kHeadDim / 2 + kHeadDim / 16; STD_TORCH_CHECK( - !index_cache.has_value() || index_cache->scalar_type() == kFp8, - "fp8 index path: index_cache must be fp8 e4m3"); + index_cache->dim() == 3 && index_cache->size(1) == block_size && + index_cache->size(2) == page_width && index_cache->stride(2) == 1 && + index_cache->stride(1) == page_width && + index_cache->stride(0) >= block_size * page_width, + "nvfp4 index_cache must be [nb, block_size, 72] uint8 with " + "contiguous pages"); + idx_s_block = index_cache->stride(0); + } + if (fp8_idx) { + STD_TORCH_CHECK(!index_cache.has_value() || nvfp4_idx || + index_cache->scalar_type() == kFp8, + "fp8 index path: index_cache must be fp8 e4m3"); STD_TORCH_CHECK( !index_q_out.has_value() || index_q_out->scalar_type() == kFp8, "fp8 index path: index_q_out must be fp8 e4m3"); @@ -1188,6 +2452,12 @@ void fused_minimax_m3_qknorm_rope_kv_insert( float const* kv_v_scale_ptr = nvfp4_kv ? kv_v_scale->const_data_ptr() : nullptr; + // The row-group kernel indexes cache slots in 32 bits. + bool const slots_fit_int32 = + !insert_kv || + (kv_cache->size(0) * block_size <= INT32_MAX && + (!process_index || index_cache->size(0) * block_size <= INT32_MAX)); + const torch::stable::accelerator::DeviceGuard device_guard( qkv.get_device_index()); auto stream = get_current_cuda_stream(qkv.get_device_index()); @@ -1216,3 +2486,122 @@ void fused_minimax_m3_qknorm_rope_kv_insert( #undef CALL_FUSED_MINIMAX_M3 #undef CALL_FUSED_MINIMAX_M3_IMPL + +// ──────────────────────────────────────────────────────────────────────────── +// DSpark context K/V torch op +// ──────────────────────────────────────────────────────────────────────────── +void fused_minimax_m3_dspark_context_kv( + torch::stable::Tensor const& all_kv, // bf16 [N, L, 2, nkv, 128] + torch::stable::Tensor const& k_norm_weight, // bf16 [L, 128] + torch::stable::Tensor const& cos_sin_cache, // bf16 [max_pos, rotary_dim] + torch::stable::Tensor const& positions, // [N] i64 + torch::stable::Tensor const& slot_mapping, // [N] i64 + torch::stable::Tensor& kv_cache, // layer 0's [nb, nkv, bs, 2*128] + torch::stable::Tensor const& cache_offsets, // [L] i64, elements + torch::stable::Tensor const& k_scale, // layer 0's [] f32 k scale + torch::stable::Tensor const& scale_offsets, // [2, L] i64, floats + int64_t rotary_dim, double eps, const std::string& kv_cache_dtype) { +#ifdef USE_ROCM + STD_TORCH_CHECK(false, "fused_minimax_m3_dspark_context_kv is CUDA-only"); +#else + using torch::headeronly::ScalarType; + int const kHeadDim = vllm::minimax_m3_fused_ops::kHeadDim; + STD_TORCH_CHECK(all_kv.is_cuda() && all_kv.dim() == 5 && + all_kv.size(2) == 2 && all_kv.size(4) == kHeadDim && + all_kv.stride(4) == 1 && + all_kv.scalar_type() == ScalarType::BFloat16, + "all_kv must be bf16 CUDA [N, L, 2, nkv, 128], last dim " + "contiguous"); + for (int d = 0; d < 4; d++) { + STD_TORCH_CHECK(all_kv.stride(d) % 8 == 0, + "all_kv strides must be multiples of 8 elements"); + } + STD_TORCH_CHECK(reinterpret_cast(all_kv.data_ptr()) % 16 == 0, + "all_kv must be 16-byte aligned"); + int const num_tokens = static_cast(all_kv.size(0)); + int const num_layers = static_cast(all_kv.size(1)); + int const nkv = static_cast(all_kv.size(3)); + STD_TORCH_CHECK(k_norm_weight.is_contiguous() && + k_norm_weight.scalar_type() == ScalarType::BFloat16 && + k_norm_weight.numel() == num_layers * kHeadDim, + "k_norm_weight must be contiguous bf16 [L, 128]"); + STD_TORCH_CHECK(cos_sin_cache.is_contiguous() && + cos_sin_cache.scalar_type() == ScalarType::BFloat16 && + cos_sin_cache.dim() == 2 && + cos_sin_cache.size(1) == rotary_dim, + "cos_sin_cache must be contiguous bf16 [max_pos, " + "rotary_dim]"); + STD_TORCH_CHECK(rotary_dim == 16 || rotary_dim == 32 || rotary_dim == 64, + "rotary_dim must be 16, 32 or 64"); + for (auto const* t : {&positions, &slot_mapping}) { + STD_TORCH_CHECK(t->is_cuda() && t->is_contiguous() && + t->scalar_type() == ScalarType::Long && + t->numel() == num_tokens, + "positions and slot_mapping must be contiguous int64 [N]"); + } + STD_TORCH_CHECK(cache_offsets.is_cuda() && cache_offsets.is_contiguous() && + cache_offsets.scalar_type() == ScalarType::Long && + cache_offsets.numel() == num_layers, + "cache_offsets must be int64 [L]"); + STD_TORCH_CHECK(scale_offsets.is_cuda() && scale_offsets.is_contiguous() && + scale_offsets.scalar_type() == ScalarType::Long && + scale_offsets.numel() == 2 * num_layers, + "scale_offsets must be int64 [2, L]"); + STD_TORCH_CHECK( + k_scale.is_cuda() && k_scale.scalar_type() == ScalarType::Float, + "k_scale must be a float32 CUDA tensor"); + bool const fp8 = kv_cache_dtype == "fp8" || kv_cache_dtype == "fp8_e4m3"; + STD_TORCH_CHECK(fp8 || kv_cache_dtype == "auto", + "kv_cache_dtype must be auto, fp8 or fp8_e4m3"); + STD_TORCH_CHECK(fp8 ? kv_cache.scalar_type() == ScalarType::Byte + : kv_cache.scalar_type() == ScalarType::BFloat16, + "kv_cache must be uint8 (fp8) or bf16 (auto)"); + STD_TORCH_CHECK( + kv_cache.dim() == 4 && kv_cache.size(1) == nkv && + kv_cache.size(3) == 2 * kHeadDim && kv_cache.stride(3) == 1 && + kv_cache.stride(0) % 16 == 0 && kv_cache.stride(1) % 16 == 0 && + kv_cache.stride(2) % 16 == 0, + "kv_cache must be [nb, nkv, bs, 256] with a contiguous last dim"); + int64_t const block_size = kv_cache.size(2); + STD_TORCH_CHECK(block_size > 0 && (block_size & (block_size - 1)) == 0, + "kv_cache block size must be a power of two"); + int block_shift = 0; + while ((int64_t{1} << block_shift) < block_size) block_shift++; + int64_t constexpr kIntMax = std::numeric_limits::max(); + STD_TORCH_CHECK(all_kv.numel() / 8 <= kIntMax && + kv_cache.stride(0) <= kIntMax && + kv_cache.size(0) * block_size <= kIntMax, + "all_kv / kv_cache too large for 32-bit indexing"); + + const torch::stable::accelerator::DeviceGuard device_guard( + all_kv.get_device_index()); + auto stream = get_current_cuda_stream(all_kv.get_device_index()); + // all_kv strides in 16-byte vectors. + auto launch = [&](auto fp8_tag) { + constexpr bool kFp8 = decltype(fp8_tag)::value; + vllm::minimax_m3_fused_ops::launchFusedMiniMaxM3DSparkContextKV( + reinterpret_cast(all_kv.data_ptr()), + reinterpret_cast(k_norm_weight.data_ptr()), + reinterpret_cast(cos_sin_cache.data_ptr()), + reinterpret_cast(positions.data_ptr()), + reinterpret_cast(slot_mapping.data_ptr()), + reinterpret_cast(kv_cache.data_ptr()), + reinterpret_cast(cache_offsets.data_ptr()), + reinterpret_cast(k_scale.data_ptr()), + reinterpret_cast(scale_offsets.data_ptr()), + static_cast(eps), static_cast(rotary_dim), num_tokens, + num_layers, nkv, block_shift, static_cast(all_kv.stride(0) / 8), + static_cast(all_kv.stride(1) / 8), + static_cast(all_kv.stride(2) / 8), + static_cast(all_kv.stride(3) / 8), + static_cast(kv_cache.stride(0)), + static_cast(kv_cache.stride(1)), + static_cast(kv_cache.stride(2)), stream); + }; + if (fp8) { + launch(std::true_type{}); + } else { + launch(std::false_type{}); + } +#endif +} diff --git a/csrc/fused_mqa_topk/candidates.cuh b/csrc/fused_mqa_topk/candidates.cuh new file mode 100644 index 000000000000..30afdec28efd --- /dev/null +++ b/csrc/fused_mqa_topk/candidates.cuh @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright contributors to the vLLM project +// +// SPDX-License-Identifier: MIT +// Copyright (c) 2025 DeepSeek +// +// 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. +// +// Candidate records of the gated scan: a uint16 value (the low 16 bits of the +// fp24 score code) and an int32 word (key index in bits 0-19, the code's high +// 8 bits in bits 20-27). +#pragma once +#include +#include + +#include "common.cuh" +#include "constants.h" + +namespace fused_mqa_topk { +using CandidateValue = uint16_t; +constexpr uint32_t kCandidateIndexBits = 20; +constexpr uint32_t kCandidateIndexMask = (1u << kCandidateIndexBits) - 1u; +static_assert(kMaxKeys <= (1u << kCandidateIndexBits), + "key indices must fit the candidate index field"); + +CUTLASS_DEVICE uint32_t candidate_pack_index(const uint32_t payload, + const uint32_t kv_index) { + return (kv_index & kCandidateIndexMask) | + ((payload >> 16) << kCandidateIndexBits); +} + +CUTLASS_DEVICE uint32_t candidate_fp24_code(const float value) { + return ordered_code(value) >> 8; +} + +CUTLASS_DEVICE uint32_t candidate_load_score_code(const CandidateValue value, + const int32_t packed_idx) { + return (static_cast(packed_idx) >> kCandidateIndexBits) << 16 | + static_cast(value); +} + +CUTLASS_DEVICE int32_t candidate_decode_index(const int32_t packed_idx) { + return static_cast(static_cast(packed_idx) & + kCandidateIndexMask); +} + +// Stores a candidate as its fp24 code (low 16 bits in value_dst, high 8 bits +// in bits 20-27 of the index word) and key index. +CUTLASS_DEVICE void store_candidate_record(CandidateValue* value_dst, + int32_t* index_dst, + const uint32_t payload, + const uint32_t kv_index) { + __stcg(value_dst, static_cast(payload)); + __stcg(index_dst, + static_cast(candidate_pack_index(payload, kv_index))); +} + +CUTLASS_DEVICE void store_candidate(CandidateValue* value_dst, + int32_t* index_dst, const float bq, + const uint32_t kv_index) { + store_candidate_record(value_dst, index_dst, candidate_fp24_code(bq), + kv_index); +} + +} // namespace fused_mqa_topk diff --git a/csrc/fused_mqa_topk/common.cuh b/csrc/fused_mqa_topk/common.cuh new file mode 100644 index 000000000000..860bed7c3dcc --- /dev/null +++ b/csrc/fused_mqa_topk/common.cuh @@ -0,0 +1,130 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright contributors to the vLLM project +// +// Small device helpers shared by the persistent kernel, its gate sample and +// the exact rescan. +#pragma once + +#include +#include + +#include + +#include "constants.h" + +namespace fused_mqa_topk { + +// Warp roles of both pipelines (persistent kernel and dense rescan): warps 0-7 +// are the math warps (two warpgroups of 4 query rows each), warps 8-11 the +// producer / UMMA / drain warps. +constexpr uint32_t kMathThreads = 256, kMathWgThreads = 128, + kMathWarps = kMathThreads / 32; +// Query rows of a block each math warpgroup scores. +constexpr uint32_t kWgRows = kBlockRows / 2; +// Register split: ptxas gives each of the 384 threads 168 registers +// (65536 / 384, rounded down to a multiple of 8); setmaxnreg moves them from +// the producer / UMMA / drain warpgroup to the two math warpgroups. +constexpr uint32_t kMathRegs = 224, kSpecRegs = 168 * 3 - 2 * kMathRegs; + +// Named barrier ids (cutlass::arch::NamedBarrier offsets them past the ids it +// reserves). kBarMath / kBarSelect: the 256 math threads; kBarDrain: the math +// threads and the two drain warps; kBarDenseMath: the dense rescan's math +// threads. +constexpr uint32_t kBarDenseMath = 0, kBarMath = 2, kBarDrain = 4, + kBarSelect = 5; +constexpr uint32_t kDrainSyncThreads = kMathThreads + 2 * 32; + +// Barrier of the 256 math threads (warps 0-7); the compiler barriers keep +// shared-memory accesses on their side of it. +__device__ __forceinline__ void math_wg_sync() { + asm volatile("" ::: "memory"); + cutlass::arch::NamedBarrier(kMathThreads, kBarMath).sync(); + asm volatile("" ::: "memory"); +} + +// Shared-memory histograms pad one word per 32 bins (bank conflicts): +// bin b lives at padded_bin(b), a histogram of n bins takes padded_bins(n). +__host__ __device__ constexpr uint32_t padded_bins(uint32_t n) { + return n + n / 32; +} +__device__ __forceinline__ uint32_t padded_bin(uint32_t b) { + return b + b / 32; +} + +// Mask of all 32 lanes for the warp-synchronous intrinsics. +constexpr uint32_t kFullMask = 0xffffffffu; + +// Inclusive prefix sum of v over the warp's lanes. +__device__ __forceinline__ uint32_t warp_inclusive_scan(uint32_t v, + uint32_t lane) { +#pragma unroll + for (uint32_t d = 1; d < 32; d *= 2) { + const uint32_t u = __shfl_up_sync(kFullMask, v, d); + if (lane >= d) v += u; + } + return v; +} + +__device__ __forceinline__ void mbar_inval(const void* b) { + asm volatile("mbarrier.inval.shared::cta.b64 [%0];" ::"r"( + static_cast(__cvta_generic_to_shared(b))) + : "memory"); +} + +// Reads `x` with a volatile load, so the compiler re-reads it from (shared) +// memory instead of keeping it live in a register. +template +__device__ __forceinline__ T volatile_load(const T& x) { + return *reinterpret_cast(&x); +} + +// Order-preserving uint32 code of a float (higher code = larger value) and +// its inverse. +__device__ __forceinline__ uint32_t ordered_code(float x) { + const uint32_t u = __float_as_uint(x); + return (u & 0x80000000u) ? ~u : (u ^ 0x80000000u); +} +__device__ __forceinline__ float ordered_float(uint32_t code) { + return __uint_as_float((code & 0x80000000u) ? (code ^ 0x80000000u) : ~code); +} + +// Histogram bin of a score: its fp16 value (rounded down) as an +// order-preserving 16-bit code, >> Shift. half_bin_floor is the smallest value +// in bin `b`. +template +__device__ __forceinline__ uint32_t half_bin(float x) { + const uint32_t u = __half_as_ushort(__float2half_rd(x)); + return ((u & 0x8000u) ? ((~u) & 65535u) : (u ^ 0x8000u)) >> Shift; +} +template +__device__ __forceinline__ float half_bin_floor(uint32_t b) { + const uint32_t o = b << Shift, + u = (o & 0x8000u) ? (o ^ 0x8000u) : ((~o) & 65535u); + return __half2float(__ushort_as_half(u)); +} + +// Weighted head reduction of one row's 32 head scores s_h in two FFMA2 chains +// seeded with sum_0 / sum_1: sum_h w_h * |s_h| (kWithLinear = false: the +// persistent kernel adds the linear part on the tensor core) or +// sum_h w_h * (s_h + |s_h|) (kWithLinear = true); relu(s) = (s + |s|) / 2. +// Adapted from DeepGEMM's sm100_fp8_mqa_logits.cuh at 88965b078186. +template +__device__ __forceinline__ float reduce_heads(const float* s, const float* w, + float2 sum_0 = {}, + float2 sum_1 = {}) { + auto term = [&](uint32_t at, float2 sum) { + const auto abs_s = make_float2(fabsf(s[at]), fabsf(s[at + 1])); + const auto x = + kWithLinear ? __fadd2_rn(make_float2(s[at], s[at + 1]), abs_s) : abs_s; + return __ffma2_rn(x, make_float2(w[at], w[at + 1]), sum); + }; +#pragma unroll + for (uint32_t j = 0; j < kNumHeads; j += 4) { + sum_0 = term(j, sum_0); + sum_1 = term(j + 2, sum_1); + } + const auto sum = __fadd2_rn(sum_0, sum_1); + return sum.x + sum.y; +} + +} // namespace fused_mqa_topk diff --git a/csrc/fused_mqa_topk/constants.h b/csrc/fused_mqa_topk/constants.h new file mode 100644 index 000000000000..c831bbfcfa6e --- /dev/null +++ b/csrc/fused_mqa_topk/constants.h @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright contributors to the vLLM project +// +// Shapes and workspace layout shared by the fused MQA-logits + top-k kernel +// and its host wrapper. The Python side mirrors the workspace sizes in +// vllm/model_executor/kernels/attention/dsa/fused_mqa_topk.py (the host wrapper +// checks them). +#pragma once + +#include + +namespace fused_mqa_topk { + +// Indexer shape: 32 heads of dimension 128. +constexpr uint32_t kNumHeads = 32, kHeadDim = 128; +// Query rows per block (the unit a CTA claims), threads per CTA, keys per +// scan tile. +constexpr uint32_t kBlockRows = 8, kNumThreads = 384, kTileKeys = 256; +// Largest key range per call. +constexpr uint32_t kMaxKeys = 1u << 20; +// Per-row workspace: the gated candidate slab (a row with more candidates is +// rescanned exactly) and the exact rescan's retained set. +constexpr uint32_t kCandidatesPerRow = 16384, kRetainedPerRow = 4096; +// Candidate / retained slots (of kBlockRows rows each) per persistent CTA: one +// block in selection, one claimed ahead. +constexpr uint32_t kSlotsPerCta = 2; + +// Top-k sizes the kernel is compiled for. +constexpr bool is_supported_topk(int64_t k) { + return k == 512 || k == 1024 || k == 2048; +} + +} // namespace fused_mqa_topk diff --git a/csrc/fused_mqa_topk/deep_gemm_compat.cuh b/csrc/fused_mqa_topk/deep_gemm_compat.cuh new file mode 100644 index 000000000000..ae13a12c3d57 --- /dev/null +++ b/csrc/fused_mqa_topk/deep_gemm_compat.cuh @@ -0,0 +1,180 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright contributors to the vLLM project +// +// Adapted from DeepGEMM (https://github.com/deepseek-ai/DeepGEMM) at +// 88965b078186ee7510ab9fc4f1d5ebc19adfa8d1, the last release before its SM100 +// MQA-logits kernels went MX-only. FP8 MQA-logits building blocks: the +// shared-storage layout with fp32 per-key scales and fp32 weights, the +// grid-stride contiguous-KV scheduler and the 2D/3D TMA load. Trimmed to what +// the fused kernel instantiates (FP8 only, no metadata schedule, no multicast, +// no paging). +#pragma once + +#include +#include + +#include +#include +#include +#include + +namespace fused_mqa_topk::dg { +namespace math = deep_gemm::math; + +// FP8 Q/K (one byte per element), fp32 per-key scales, fp32 weights: the +// DeepGEMM layout without its MX / FP4 variants. +template +struct MQALogitsSharedStorage { + using Barrier = cutlass::arch::ClusterTransactionBarrier; + DG_STATIC_ASSERT(sizeof(qk_dtype_t) == 1, "FP8 Q/K only"); + + static constexpr uint32_t BLOCK_QH = BLOCK_Q * kNumHeads; + static constexpr uint32_t UMMA_N = math::constexpr_align(BLOCK_QH, 8u); + // Align to one 8-row Q/K swizzle tile (head_dim bytes per FP8 token). + static constexpr uint32_t kSwizzleAlignment = 8 * kHeadDim; + static constexpr uint32_t kNumQElementsPerStage = UMMA_N * kHeadDim; + static constexpr uint32_t kNumKVElementsPerStage = SPLIT_KV * kHeadDim; + // TMA destinations in shared memory must be 128-byte aligned. + static constexpr uint32_t kTmaAlignment = 128; + static constexpr uint32_t kNumWeightBytesPerRow = math::constexpr_align( + kNumHeads * static_cast(sizeof(reduce_dtype_t)), 16u); + static constexpr uint32_t kNumWeightBytesPerStage = + math::constexpr_align(BLOCK_Q * kNumWeightBytesPerRow, kTmaAlignment); + static constexpr uint32_t kNumWeightElementsPerStage = + kNumWeightBytesPerStage / static_cast(sizeof(reduce_dtype_t)); + + DG_STATIC_ASSERT(kNumQElementsPerStage % kSwizzleAlignment == 0, + "Unaligned TMA swizzling"); + DG_STATIC_ASSERT(kNumKVElementsPerStage % kSwizzleAlignment == 0, + "Unaligned TMA swizzling"); + DG_STATIC_ASSERT(kSwizzleAlignment % 128 == 0, + "TMA destination must be 128-byte aligned"); + + alignas(kSwizzleAlignment) + qk_dtype_t smem_q[kNumQStages][kNumQElementsPerStage]; + alignas(kSwizzleAlignment) + qk_dtype_t smem_kv[kNumKVStages][kNumKVElementsPerStage]; + alignas(kTmaAlignment) float smem_sf_kv[kNumKVStages][SPLIT_KV]; + alignas(kTmaAlignment) + reduce_dtype_t smem_weights[kNumQStages][kNumWeightElementsPerStage]; + // Barriers require 8-byte alignment, already guaranteed by the preceding + // TMA-aligned arrays. + Barrier full_q_barriers[kNumQStages]; + Barrier empty_q_barriers[kNumQStages]; + Barrier full_kv_barriers[kNumKVStages]; + Barrier empty_kv_barriers[kNumKVStages]; + Barrier full_tmem_barriers[kNumTmemStages]; + Barrier empty_tmem_barriers[kNumTmemStages]; + uint32_t tmem_ptr_in_smem; +}; + +// Grid-stride contiguous-KV scheduler: SM `s` owns Q blocks `s`, `s + kNumSMs`, +// ..., each visiting the KV range [min k_start, max k_end) of its rows. +template +struct SM100MQALogitsScheduler { + uint32_t current_q_block_idx; + uint32_t num_q_blocks; + uint32_t num_q_tokens; + uint32_t num_kv_tokens; + const uint32_t* cu_seq_len_k_start; + const uint32_t* cu_seq_len_k_end; + + CUTLASS_DEVICE SM100MQALogitsScheduler(const uint32_t& sm_idx, + const uint32_t& num_q_tokens, + const uint32_t& num_kv_tokens, + const uint32_t* cu_seq_len_k_start, + const uint32_t* cu_seq_len_k_end) + : current_q_block_idx(sm_idx), + num_q_blocks(math::ceil_div(num_q_tokens, BLOCK_Q)), + num_q_tokens(num_q_tokens), + num_kv_tokens(num_kv_tokens), + cu_seq_len_k_start(cu_seq_len_k_start), + cu_seq_len_k_end(cu_seq_len_k_end) { + DG_STATIC_ASSERT(kNumSMs > 0, "Invalid SM count"); + } + + // NOTE: Out-of-range rows are clamped to the last valid row + CUTLASS_DEVICE bool next_q_block(uint32_t& q_block_idx, + uint32_t& kv_token_base, + uint32_t& num_kv_splits) { + if (current_q_block_idx >= num_q_blocks) return false; + q_block_idx = current_q_block_idx; + current_q_block_idx += kNumSMs; + uint32_t start = cute::numeric_limits::max(); + uint32_t end = cute::numeric_limits::min(); +#pragma unroll 8 + for (uint32_t token_idx = 0; token_idx < BLOCK_Q; ++token_idx) { + const auto row_idx = + cute::min(q_block_idx * BLOCK_Q + token_idx, num_q_tokens - 1); + start = cute::min(start, + cute::min(cu_seq_len_k_start[row_idx], num_kv_tokens)); + end = cute::max(end, cute::min(cu_seq_len_k_end[row_idx], num_kv_tokens)); + } + kv_token_base = start / 4 * 4; + num_kv_splits = math::ceil_div(end - kv_token_base, SPLIT_KV); + return true; + } + + CUTLASS_DEVICE uint32_t + get_q_tma_token_base(const uint32_t& q_block_idx) const { + return q_block_idx * BLOCK_Q; + } + + CUTLASS_DEVICE static uint32_t get_kv_tma_offset( + const uint32_t& kv_token_base, const uint32_t& kv_split_idx) { + return kv_token_base + kv_split_idx * SPLIT_KV; + } + + CUTLASS_DEVICE static uint32_t get_logits_row(const uint32_t& q_block_idx, + const uint32_t& token_idx) { + return q_block_idx * BLOCK_Q + token_idx; + } + + CUTLASS_DEVICE static uint32_t get_logits_col( + const uint32_t& kv_token_base, const uint32_t& kv_split_idx, + const uint32_t& math_thread_idx) { + return kv_token_base + kv_split_idx * SPLIT_KV + math_thread_idx; + } +}; + +// Single-CTA TMA load of a BLOCK_OUTER x BLOCK_INNER box (3D adds a batch +// coordinate). +template +CUTLASS_DEVICE void tma_copy( + void const* desc_ptr, cutlass::arch::ClusterTransactionBarrier* barrier_ptr, + dtype_t* smem_ptr, const uint32_t& inner_idx, const uint32_t& outer_idx, + const uint32_t& batch_idx = 0) { + constexpr auto kCacheHint = + static_cast(cute::TMA::CacheHintSm90::EVICT_NORMAL); + constexpr uint32_t kPackFactor = deep_gemm::get_smem_pack_factor(); + DG_STATIC_ASSERT(kPackFactor == 1 or sizeof(dtype_t) == 1, + "Packing expects a 1-byte storage type"); + constexpr uint32_t BLOCK_INNER_ATOM_STORAGE = + kSwizzleMode == 0 ? BLOCK_INNER / kPackFactor + : kSwizzleMode / sizeof(dtype_t); + constexpr uint32_t BLOCK_INNER_ATOM_LOGICAL = + BLOCK_INNER_ATOM_STORAGE * kPackFactor; + DG_STATIC_ASSERT(BLOCK_INNER % BLOCK_INNER_ATOM_LOGICAL == 0, + "TMA inner block must contain whole atoms"); + +#pragma unroll + for (uint32_t i = 0; i < BLOCK_INNER / BLOCK_INNER_ATOM_LOGICAL; ++i) { + if constexpr (kIs3DTMA) { + cute::SM90_TMA_LOAD_3D::copy( + desc_ptr, reinterpret_cast(barrier_ptr), kCacheHint, + smem_ptr + i * BLOCK_OUTER * BLOCK_INNER_ATOM_STORAGE, + inner_idx + i * BLOCK_INNER_ATOM_LOGICAL, outer_idx, batch_idx); + } else { + cute::SM90_TMA_LOAD_2D::copy( + desc_ptr, reinterpret_cast(barrier_ptr), kCacheHint, + smem_ptr + i * BLOCK_OUTER * BLOCK_INNER_ATOM_STORAGE, + inner_idx + i * BLOCK_INNER_ATOM_LOGICAL, outer_idx); + } + } +} + +} // namespace fused_mqa_topk::dg diff --git a/csrc/fused_mqa_topk/dense_scan.cuh b/csrc/fused_mqa_topk/dense_scan.cuh new file mode 100644 index 000000000000..395cb84f183e --- /dev/null +++ b/csrc/fused_mqa_topk/dense_scan.cuh @@ -0,0 +1,289 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright contributors to the vLLM project +// +// Adapted from DeepGEMM (https://github.com/deepseek-ai/DeepGEMM) at +// 88965b078186ee7510ab9fc4f1d5ebc19adfa8d1: +// deep_gemm/include/deep_gemm/impls/sm100_fp8_mqa_logits.cuh. +// +// Dense FP8 MQA scoring of one 8-row block over its whole KV range: DeepGEMM's +// SM100 MQA logits pipeline (contiguous KV, 32 heads, D = 128) with the logits +// store replaced by a per-score policy. Used by the persistent kernel's exact +// recovery rescan (rescan.cuh). +// +// Roles (384 threads): warps 0-7 reduce the 32 heads (4 rows per warpgroup), +// warp 8 loads Q and the weights, warp 9 the KV tiles and their scales, warp 10 +// issues the UMMAs, warp 11 is idle. +// +// Policy interface (all called by the math warps): +// kWeightScale: score = (sum_h kWeightScale * w_h * (s_h + |s_h|)) * scale_kv +// begin_block(sched, q_block, query_base): after the block's Q / weights +// landed +// begin_split(split): before the split's scores +// score(i, kv_offset, kv_lane, split, score): row query_base + i of the +// warpgroup +// end_split(split, num_splits, kv_base): after the split's scores +// end_block(sched, q_block): before the block's Q stage is released +#pragma once + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "common.cuh" +#include "constants.h" +#include "deep_gemm_compat.cuh" + +namespace fused_mqa_topk::dense { +using namespace deep_gemm; + +constexpr uint32_t BLOCK_Q = kBlockRows, SPLIT_KV = kTileKeys; +constexpr uint32_t kNumKVStages = 2, kNumTmemStages = 2; +constexpr uint32_t UMMA_M = 128, UMMA_N = BLOCK_Q * kNumHeads, UMMA_K = 32; +// The rescan scores exactly one 8-row block: the scheduler is built with that +// block as its `sm_idx`, and its grid stride (kNumSMs) exceeds any block count +// (rows < 2^26), so next_q_block yields that block only. A smaller stride would +// also score block + stride, outside the rescan's row bounds and output rows. +constexpr uint32_t kSingleBlockStride = 1u << 31; +using Scheduler = + dg::SM100MQALogitsScheduler; +using SharedStorage = + dg::MQALogitsSharedStorage; + +// The policy's state lives in dynamic shared memory right after the pipeline +// storage. Barrier words are zeroed before init: the persistent kernel reuses +// this memory between rescans. +template +CUTLASS_DEVICE void score_block(const Scheduler& block, Policy& policy, + const cute::TmaDescriptor& tensor_map_q, + const cute::TmaDescriptor& tensor_map_kv, + const cute::TmaDescriptor& tensor_map_sf_kv, + const cute::TmaDescriptor& tensor_map_weights) { + const auto warp_idx = cutlass::canonical_warp_idx_sync(); + const auto lane_idx = ptx::get_lane_idx(); + constexpr uint32_t kSpecWarpStart = 8; + constexpr uint32_t SMEM_Q_SIZE_PER_STAGE = BLOCK_Q * kNumHeads * kHeadDim; + constexpr uint32_t SMEM_KV_SIZE_PER_STAGE = SPLIT_KV * kHeadDim; + constexpr uint32_t SMEM_SF_KV_SIZE_PER_STAGE = SPLIT_KV * sizeof(float); + constexpr uint32_t SMEM_WEIGHT_SIZE_PER_STAGE = + BLOCK_Q * kNumHeads * sizeof(float); + constexpr uint32_t kNumTmemCols = + utils::get_num_aligned_tmem_cols(); + + if (warp_idx == kSpecWarpStart) { + cute::prefetch_tma_descriptor(&tensor_map_q); + cute::prefetch_tma_descriptor(&tensor_map_weights); + cute::prefetch_tma_descriptor(&tensor_map_kv); + cute::prefetch_tma_descriptor(&tensor_map_sf_kv); + } + + extern __shared__ __align__(SharedStorage::kSwizzleAlignment) + uint8_t smem_buffer[]; + auto& smem = *reinterpret_cast(smem_buffer); + if (warp_idx == kSpecWarpStart + 1 and cute::elect_one_sync()) { + const auto init = [](auto& barrier, uint32_t count) { + *reinterpret_cast(&barrier) = 0; + barrier.init(count); + }; + init(smem.full_q_barriers[0], 1); + init(smem.empty_q_barriers[0], kMathThreads + 32); // + the UMMA warp +#pragma unroll + for (uint32_t i = 0; i < kNumKVStages; ++i) { + init(smem.full_kv_barriers[i], 1); + init(smem.empty_kv_barriers[i], kMathThreads); + } +#pragma unroll + for (uint32_t i = 0; i < kNumTmemStages; ++i) { + init(smem.full_tmem_barriers[i], 1); + init(smem.empty_tmem_barriers[i], kMathThreads); + } + cutlass::arch::fence_barrier_init(); + } + __syncwarp(); + if (warp_idx == kSpecWarpStart + 2) + cute::TMEM::Allocator1Sm().allocate(kNumTmemCols, &smem.tmem_ptr_in_smem); + __syncthreads(); + + RingPipeline<1> q_pipeline; + RingPipeline kv_pipeline; + RingPipeline tmem_pipeline; + + uint32_t q_block_idx, kv_base, num_kv_splits; + if (warp_idx == kSpecWarpStart) { + cutlass::arch::warpgroup_reg_dealloc(); + if (cute::elect_one_sync()) { + Scheduler scheduler = block; + while (scheduler.next_q_block(q_block_idx, kv_base, num_kv_splits)) { + CUTE_TIE_DECL(q_pipeline.advance(), q_stage_idx, q_phase); + smem.empty_q_barriers[q_stage_idx].wait(q_phase ^ 1); + const uint32_t q_token_base = + scheduler.get_q_tma_token_base(q_block_idx); + dg::tma_copy( + &tensor_map_q, &smem.full_q_barriers[q_stage_idx], + smem.smem_q[q_stage_idx], 0, q_token_base * kNumHeads); + dg::tma_copy( + &tensor_map_weights, &smem.full_q_barriers[q_stage_idx], + smem.smem_weights[q_stage_idx], 0, q_token_base); + smem.full_q_barriers[q_stage_idx].arrive_and_expect_tx( + SMEM_Q_SIZE_PER_STAGE + SMEM_WEIGHT_SIZE_PER_STAGE); + } + } + __syncwarp(); + } else if (warp_idx == kSpecWarpStart + 1) { + cutlass::arch::warpgroup_reg_dealloc(); + Scheduler scheduler = block; + while (scheduler.next_q_block(q_block_idx, kv_base, num_kv_splits)) { +#pragma unroll 1 + for (uint32_t kv_split_idx = 0; kv_split_idx < num_kv_splits; + ++kv_split_idx) { + if (cute::elect_one_sync()) { + CUTE_TIE_DECL(kv_pipeline.advance(), kv_stage_idx, kv_phase); + smem.empty_kv_barriers[kv_stage_idx].wait(kv_phase ^ 1); + const uint32_t kv_tma_offset = + scheduler.get_kv_tma_offset(kv_base, kv_split_idx); + dg::tma_copy( + &tensor_map_kv, &smem.full_kv_barriers[kv_stage_idx], + smem.smem_kv[kv_stage_idx], 0, kv_tma_offset); + dg::tma_copy( + &tensor_map_sf_kv, &smem.full_kv_barriers[kv_stage_idx], + smem.smem_sf_kv[kv_stage_idx], kv_tma_offset, 0); + smem.full_kv_barriers[kv_stage_idx].arrive_and_expect_tx( + SMEM_KV_SIZE_PER_STAGE + SMEM_SF_KV_SIZE_PER_STAGE); + } + __syncwarp(); + } + } + } else if (warp_idx == kSpecWarpStart + 2) { + cutlass::arch::warpgroup_reg_dealloc(); + DG_TRAP_ONLY_DEVICE_ASSERT(ptx::ld_shared(&smem.tmem_ptr_in_smem) == 0); + auto instr_desc = cute::UMMA::make_instr_desc< + cutlass::float_e4m3_t, cutlass::float_e4m3_t, float, UMMA_M, UMMA_N, + cute::UMMA::Major::K, cute::UMMA::Major::K>(); + auto runtime_instr_desc = cute::UMMA::make_runtime_instr_desc(instr_desc); + Scheduler scheduler = block; + while (scheduler.next_q_block(q_block_idx, kv_base, num_kv_splits)) { + CUTE_TIE_DECL(q_pipeline.advance(), q_stage_idx, q_phase); + smem.full_q_barriers[q_stage_idx].wait(q_phase); + for (uint32_t kv_split_idx = 0; kv_split_idx < num_kv_splits; + ++kv_split_idx) { + CUTE_TIE_DECL(kv_pipeline.advance(), kv_stage_idx, kv_phase); + smem.full_kv_barriers[kv_stage_idx].wait(kv_phase); + if (cute::elect_one_sync()) { + // One UMMA per math warpgroup: KV rows [128 i, 128 i + 128) x all 256 + // Q rows. +#pragma unroll + for (uint32_t i = 0; i < 2; ++i) { + CUTE_TIE_DECL(tmem_pipeline.advance(), tmem_stage_idx, tmem_phase); + const uint32_t tmem_addr = tmem_stage_idx * UMMA_N; + smem.empty_tmem_barriers[tmem_stage_idx].wait(tmem_phase ^ 1); + ptx::tcgen05_after_thread_sync(); +#pragma unroll + for (uint32_t k = 0; k < kHeadDim / UMMA_K; ++k) { + auto a_desc = mma::sm100::make_umma_desc( + smem.smem_kv[kv_stage_idx], i * UMMA_M, k * UMMA_K); + auto b_desc = mma::sm100::make_umma_desc( + smem.smem_q[q_stage_idx], 0, k * UMMA_K); + ptx::SM100_MMA_F8F6F4_SS::fma(a_desc, b_desc, tmem_addr, k, + runtime_instr_desc); + } + ptx::umma_arrive_no_elect(smem.full_tmem_barriers[tmem_stage_idx]); + } + } + __syncwarp(); + } + smem.empty_q_barriers[q_stage_idx].arrive(); + } + } else if (warp_idx == kSpecWarpStart + 3) { + cutlass::arch::warpgroup_reg_dealloc(); + } else { + cutlass::arch::warpgroup_reg_alloc(); + const uint32_t math_thread_idx = warp_idx * 32 + lane_idx; + const uint32_t query_base = (warp_idx / 4) * 4; + float weights[4][kNumHeads]; + Scheduler scheduler = block; + while (scheduler.next_q_block(q_block_idx, kv_base, num_kv_splits)) { + CUTE_TIE_DECL(q_pipeline.advance(), q_stage_idx, q_phase); + smem.full_q_barriers[q_stage_idx].wait(q_phase); +#pragma unroll + for (uint32_t i = 0; i < 4; ++i) +#pragma unroll + for (uint32_t j = 0; j < kNumHeads; ++j) + weights[i][j] = Policy::kWeightScale * + ptx::ld_shared(smem.smem_weights[q_stage_idx] + + (query_base + i) * kNumHeads + j); + policy.begin_block(scheduler, q_block_idx, query_base); + for (uint32_t kv_split_idx = 0; kv_split_idx < num_kv_splits; + ++kv_split_idx) { + policy.begin_split(kv_split_idx); + CUTE_TIE_DECL(kv_pipeline.advance(), kv_stage_idx, kv_phase); + smem.full_kv_barriers[kv_stage_idx].wait(kv_phase); +#pragma unroll + for (uint32_t kv_half = 0; kv_half < 2; ++kv_half) { + const uint32_t kv_lane = + (math_thread_idx % kMathWgThreads) + kv_half * kMathWgThreads; + const uint32_t kv_offset = + scheduler.get_logits_col(kv_base, kv_split_idx, kv_lane); + const float scale_kv = + ptx::ld_shared(smem.smem_sf_kv[kv_stage_idx] + kv_lane); + CUTE_TIE_DECL(tmem_pipeline.advance(), tmem_stage_idx, tmem_phase); + smem.full_tmem_barriers[tmem_stage_idx].wait(tmem_phase); + ptx::tcgen05_after_thread_sync(); + // Both KV half-MMAs have committed before the shared stage is freed. + if (kv_half == 1) { + cutlass::arch::fence_view_async_shared(); + smem.empty_kv_barriers[kv_stage_idx].arrive(); + } + float accum_pair[2][kNumHeads]; +#pragma unroll + for (uint32_t pair = 0; pair < 4; pair += 2) { + // Two query rows at a time keep the live accumulator registers low. + ptx::tmem_load_32dp32b<2 * kNumHeads>( + tmem_stage_idx * UMMA_N + (query_base + pair) * kNumHeads, + reinterpret_cast(accum_pair)); + cutlass::arch::fence_view_async_tmem_load(); + if (pair == 2) { + ptx::tcgen05_before_thread_sync(); + smem.empty_tmem_barriers[tmem_stage_idx].arrive(); + } +#pragma unroll + for (uint32_t r = 0; r < 2; ++r) + policy.score( + pair + r, kv_offset, kv_lane, kv_split_idx, + reduce_heads(accum_pair[r], weights[pair + r]) * + scale_kv); + } + } + policy.end_split(kv_split_idx, num_kv_splits, kv_base); + } + policy.end_block(scheduler, q_block_idx); + cutlass::arch::fence_view_async_shared(); + smem.empty_q_barriers[q_stage_idx].arrive(); + } + cutlass::arch::NamedBarrier(kMathThreads, kBarDenseMath).sync(); + if (warp_idx == 0) cute::TMEM::Allocator1Sm().free(0, kNumTmemCols); + } +} + +// Invalidates the pipeline's barriers so the next score_block can re-initialise +// the same shared memory (mbarrier.inval before reuse). +CUTLASS_DEVICE void invalidate_barriers(SharedStorage& smem) { + for (auto& b : smem.full_q_barriers) mbar_inval(&b); + for (auto& b : smem.empty_q_barriers) mbar_inval(&b); + for (auto& b : smem.full_kv_barriers) mbar_inval(&b); + for (auto& b : smem.empty_kv_barriers) mbar_inval(&b); + for (auto& b : smem.full_tmem_barriers) mbar_inval(&b); + for (auto& b : smem.empty_tmem_barriers) mbar_inval(&b); +} + +} // namespace fused_mqa_topk::dense diff --git a/csrc/fused_mqa_topk/fused_mqa_topk.cu b/csrc/fused_mqa_topk/fused_mqa_topk.cu new file mode 100644 index 000000000000..652453494fbe --- /dev/null +++ b/csrc/fused_mqa_topk/fused_mqa_topk.cu @@ -0,0 +1,394 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright contributors to the vLLM project +// +// Single-launch fused FP8 MQA-logits + top-k (k = 512, 1024, 2048) prefill +// selection (SM100 family): one persistent kernel (persistent_kernel.cuh, one +// CTA per SM; sampled gate, candidate emission and selection, exact rescan of +// out-of-range blocks) scores every visible key of a row and selects its top-k +// keys without materializing the [rows, keys] logits matrix. +// This file is the libtorch-stable host wrapper. All scratch memory is passed +// in by the caller (no allocation here), so the op is CUDA-graph friendly. + +#include "../torch_utils.h" + +#include + +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "constants.h" +#include "persistent_kernel.cuh" + +namespace { + +using fused_mqa_topk::kBlockRows; +using fused_mqa_topk::kCandidatesPerRow; +using fused_mqa_topk::kHeadDim; +using fused_mqa_topk::kMaxKeys; +using fused_mqa_topk::kNumHeads; +using fused_mqa_topk::kNumThreads; +using fused_mqa_topk::kRetainedPerRow; +using fused_mqa_topk::kSlotsPerCta; +using fused_mqa_topk::kTileKeys; +using torch::headeronly::ScalarType; +namespace persist = fused_mqa_topk::persist; + +void check_cuda(cudaError_t err) { + STD_TORCH_CHECK(err == cudaSuccess, cudaGetErrorString(err)); +} + +// cuTensorMapEncodeTiled from the driver, resolved once through the runtime +// (thread-safe static initialisation; no link-time libcuda dependency). +PFN_cuTensorMapEncodeTiled_v12000 encode_tiled() { + static const PFN_cuTensorMapEncodeTiled_v12000 fn = [] { + void* ptr = nullptr; + cudaDriverEntryPointQueryResult status; + const cudaError_t err = cudaGetDriverEntryPointByVersion( + "cuTensorMapEncodeTiled", &ptr, 12000, cudaEnableDefault, &status); + STD_TORCH_CHECK(err == cudaSuccess && status == cudaDriverEntryPointSuccess, + "failed to resolve cuTensorMapEncodeTiled"); + return reinterpret_cast(ptr); + }(); + return fn; +} + +// SM count of `device`, queried once per device. +int sm_count(int device) { + static std::mutex mutex; + static std::map cache; + const std::lock_guard lock(mutex); + auto [it, inserted] = cache.try_emplace(device, 0); + if (inserted) { + const cudaError_t err = cudaDeviceGetAttribute( + &it->second, cudaDevAttrMultiProcessorCount, device); + if (err != cudaSuccess) cache.erase(it); + check_cuda(err); + } + return it->second; +} + +// Adapted from DeepGEMM's make_tma_2d_desc +// (csrc/jit_kernels/impls/runtime_utils.hpp at 88965b078186) +CUtensorMap make_2d(void* ptr, CUtensorMapDataType dt, int elem_size, + int gmem_inner, int gmem_outer, int smem_inner, + int smem_outer, long gmem_outer_stride, int swizzle_mode) { + if (swizzle_mode != 0) smem_inner = swizzle_mode / elem_size; + CUtensorMap tm; + const cuuint64_t gdims[2] = {(cuuint64_t)gmem_inner, (cuuint64_t)gmem_outer}; + const cuuint32_t sdims[2] = {(cuuint32_t)smem_inner, (cuuint32_t)smem_outer}; + const cuuint64_t gstrides[1] = {(cuuint64_t)(gmem_outer_stride * elem_size)}; + const cuuint32_t estrides[2] = {1, 1}; + CUtensorMapSwizzle swizzle = swizzle_mode == 128 ? CU_TENSOR_MAP_SWIZZLE_128B + : swizzle_mode == 64 ? CU_TENSOR_MAP_SWIZZLE_64B + : swizzle_mode == 32 + ? CU_TENSOR_MAP_SWIZZLE_32B + : CU_TENSOR_MAP_SWIZZLE_NONE; + CUresult r = encode_tiled()(&tm, dt, 2, ptr, gdims, gstrides, sdims, estrides, + CU_TENSOR_MAP_INTERLEAVE_NONE, swizzle, + CU_TENSOR_MAP_L2_PROMOTION_L2_256B, + CU_TENSOR_MAP_FLOAT_OOB_FILL_NONE); + STD_TORCH_CHECK(r == CUDA_SUCCESS, "cuTensorMapEncodeTiled failed: ", (int)r); + return tm; +} + +// KV [keys,128] bytes viewed as {128, groups, stride} with strides +// {128*stride, 128}: box {128, 256, 1} at (0, 0, o) loads keys o, o+stride, ... +// with 128B swizzle. The view spans groups * stride keys, which can exceed +// `keys` by < stride / 2; that is safe only because the kernel loads offset +// o = stride / 2, whose last key ((groups - 1) * stride + stride / 2) is +// always < keys (launch_config sizes `groups` for that). +CUtensorMap make_strided_kv(void* ptr, uint32_t groups, uint32_t stride) { + CUtensorMap tm; + constexpr uint64_t kRowBytes = kHeadDim; + const cuuint64_t gdims[3] = {kRowBytes, groups, stride}; + const cuuint64_t gstrides[2] = {kRowBytes * stride, kRowBytes}; + const cuuint32_t box[3] = {kRowBytes, kTileKeys, 1}; + const cuuint32_t estrides[3] = {1, 1, 1}; + CUresult r = encode_tiled()( + &tm, CU_TENSOR_MAP_DATA_TYPE_UINT8, 3, ptr, gdims, gstrides, box, + estrides, CU_TENSOR_MAP_INTERLEAVE_NONE, CU_TENSOR_MAP_SWIZZLE_128B, + CU_TENSOR_MAP_L2_PROMOTION_L2_256B, CU_TENSOR_MAP_FLOAT_OOB_FILL_NONE); + STD_TORCH_CHECK(r == CUDA_SUCCESS, + "strided KV cuTensorMapEncodeTiled failed: ", (int)r); + return tm; +} + +// -------- launch configuration -------- +// Keys per row above which the long-context instantiations (Interval 64, 128) +// and gate settings apply. +constexpr uint32_t kLongContextKeys = 131072; + +using PersistentKernel = + decltype(&persist::fused_mqa_topk_fp8_prefill_kernel<16, 2048>); + +// vLLM dispatches chunks of >= 32K keys per row here (fused_mqa_topk_min_keys); +// smaller key counts are supported but take the same <= 32K configuration. +template +PersistentKernel persistent_kernel(uint32_t keys) { + return keys <= 32768 ? persist::fused_mqa_topk_fp8_prefill_kernel<16, TopK> + : keys <= kLongContextKeys + ? persist::fused_mqa_topk_fp8_prefill_kernel<32, TopK> + : keys <= 524288 + ? persist::fused_mqa_topk_fp8_prefill_kernel<64, TopK> + : persist::fused_mqa_topk_fp8_prefill_kernel<128, TopK>; +} + +struct LaunchConfig { + PersistentKernel kernel; + int smem; // dynamic shared memory bytes + uint32_t phase_bytes; // selection scratch bytes past the Q / weights region + // Gate sample: sample_tiles tiles of 256 keys on a uniform grid (stride + // sample_stride). + uint32_t sample_target, sample_stride, sample_groups, sample_tiles; +}; + +// Dynamic shared memory of `kernel` on `device`: everything past its static +// shared memory up to the device's opt-in limit (232448 bytes on SM100; the +// staged selection uses it all), at least the exact rescan's layout. Sets +// the opt-in attribute; both are computed once per device and kernel. +cudaError_t kernel_smem(int device, PersistentKernel kernel, int& smem) { + // The exact rescan reuses the same dynamic shared memory with its own layout. + constexpr int kRescanSmem = int(sizeof(fused_mqa_topk::dense::SharedStorage) + + sizeof(persist::RescanShared)); + constexpr int kMinSmem = int( + sizeof(persist::KernelSmem) + + std::max(sizeof(persist::HandoffRing), sizeof(persist::SampleShared))); + static std::mutex mutex; + static std::map, int> cache; + const std::lock_guard lock(mutex); + if (const auto it = cache.find({device, kernel}); it != cache.end()) { + smem = it->second; + return cudaSuccess; + } + int max_smem = 0; + if (const cudaError_t e = cudaDeviceGetAttribute( + &max_smem, cudaDevAttrMaxSharedMemoryPerBlockOptin, device); + e != cudaSuccess) + return e; + cudaFuncAttributes attr; + if (const cudaError_t e = + cudaFuncGetAttributes(&attr, reinterpret_cast(kernel)); + e != cudaSuccess) + return e; + const int bytes = std::max(kRescanSmem, max_smem - int(attr.sharedSizeBytes)); + if (bytes < kMinSmem) return cudaErrorInvalidConfiguration; + if (const cudaError_t e = cudaFuncSetAttribute( + reinterpret_cast(kernel), + cudaFuncAttributeMaxDynamicSharedMemorySize, bytes); + e != cudaSuccess) + return e; + cache.emplace(std::make_pair(device, kernel), bytes); + smem = bytes; + return cudaSuccess; +} + +// Picks the kernel instantiation, its shared memory and the gate sample for +// `keys` keys and top-`topk` rows on `device` (the current device). +cudaError_t launch_config(int device, uint32_t keys, uint32_t topk, + LaunchConfig& c) { + // The gate aims for `target` candidates per row and sizes the sample so its + // quantile rank is `rank` (count error ~1/sqrt(rank)); a row whose count + // falls below the top-k is rescanned exactly. Up to 128K keys the candidate + // stream dominates, so smaller top-ks aim lower with a coarser sample. Above + // 128K the sample is clamped to 16-32 tiles (rank ~32 at 1M keys): a sample + // tile costs about two scan tiles, so a larger sample costs more than a + // smaller candidate set saves. Tuned on B300 real captures. + struct Gate { + uint32_t target, rank; + }; + constexpr Gate kGates[3][2] = { + // <= 128K > 128K + {{1024, 32}, {1536, 64}}, // top-512 + {{1536, 48}, {2048, 48}}, // top-1024 + {{3072, 64}, {4096, 64}}, // top-2048 + }; + const bool long_ctx = keys > kLongContextKeys; + const Gate gate = + kGates[topk == 512 ? 0 : (topk == 1024 ? 1 : 2)][long_ctx ? 1 : 0]; + c.sample_target = gate.target; + const uint32_t rank = gate.rank; + const uint64_t tile = kTileKeys; + const uint32_t rank_tiles = std::max( + 1u, uint32_t((uint64_t(rank) * keys + tile * c.sample_target - 1) / + (tile * c.sample_target))); + c.sample_tiles = + long_ctx ? std::min(32u, std::max(16u, rank_tiles)) : rank_tiles; + c.sample_stride = std::max(1u, uint32_t((keys + tile * c.sample_tiles - 1) / + (tile * c.sample_tiles))); + c.sample_groups = + (keys - c.sample_stride / 2 + c.sample_stride - 1) / c.sample_stride; + c.kernel = topk == 512 ? persistent_kernel<512>(keys) + : topk == 1024 ? persistent_kernel<1024>(keys) + : persistent_kernel<2048>(keys); + if (const cudaError_t e = kernel_smem(device, c.kernel, c.smem); + e != cudaSuccess) + return e; + c.phase_bytes = uint32_t(c.smem) - persist::kSelBase; + return cudaSuccess; +} + +void check_tensor(const torch::stable::Tensor& t, int32_t device, + ScalarType dtype, const char* name) { + STD_TORCH_CHECK(t.is_cuda() && t.get_device_index() == device, name, + " must be on the same CUDA device as q"); + STD_TORCH_CHECK(t.scalar_type() == dtype, name, " has the wrong dtype"); + STD_TORCH_CHECK(t.is_contiguous(), name, " must be contiguous"); +} + +// TMA tensor maps need 16-byte aligned base addresses. +void check_aligned(const torch::stable::Tensor& t, const char* name) { + STD_TORCH_CHECK(reinterpret_cast(t.data_ptr()) % 16 == 0, name, + " must be 16-byte aligned"); +} + +} // namespace + +// Inputs: q [rows,32,128] fp8e4m3, k [keys,128] fp8e4m3, scales [keys] fp32, +// weights [rows,32] fp32, starts/ends [rows] int32 (the visible key range +// [start,end) of each row), with 0 < rows < 2^26 and 0 < keys <= 1048576 (the +// last 8-row block may be partial). starts/ends are expected in [0, keys] with +// start <= end; they are read as uint32_t and clamped to keys, so a negative +// value means `keys` and an end before its start means an empty row. +// +// Output: out [rows,K] int32 with K (the top-k) 512, 1024 or 2048: key indices +// relative to the row's start (key - start, the top_k_per_row_prefill +// convention), unordered; -1 padded when fewer than K keys are visible. Rows +// that see at most K keys get them all, in order. Exact up to ties: the gated +// path ranks fp24 score codes (the low 8 mantissa bits dropped), the exact +// rescan ranks fp32 scores. +// +// All tensors are contiguous; q, k, scales, weights and out are 16-byte aligned +// (TMA / vector stores). +// +// Workspace (caller-owned, reused across calls), with B = ceil(rows / 8) and +// S >= 2 slots per CTA of the persistent grid (one CTA per SM): +// counter [2] int32 (zero between calls; the kernel leaves it zeroed, so a +// workspace must not be shared by launches that can run concurrently, e.g. +// on two streams), +// values [8S,16384] uint16 (low 16 bits of the fp24 candidate codes), +// indices [8S,16384] int32 (key index in bits 0-19, the code's high 8 bits +// in bits 20-27), +// retained [8S,4096] fp32, retained_indices [8S,4096] int32, +// counts [>=8B] int32, defer [>=B] int32. +void fused_mqa_topk_prefill( + torch::stable::Tensor q, torch::stable::Tensor k, + torch::stable::Tensor scales, torch::stable::Tensor weights, + torch::stable::Tensor starts, torch::stable::Tensor ends, + torch::stable::Tensor out, torch::stable::Tensor counter, + torch::stable::Tensor values, torch::stable::Tensor indices, + torch::stable::Tensor retained, torch::stable::Tensor retained_indices, + torch::stable::Tensor counts, torch::stable::Tensor defer) { + STD_TORCH_CHECK(q.is_cuda(), "q must be a CUDA tensor"); + const int32_t dev = q.get_device_index(); + check_tensor(q, dev, ScalarType::Float8_e4m3fn, "q"); + check_tensor(k, dev, ScalarType::Float8_e4m3fn, "k"); + check_tensor(scales, dev, ScalarType::Float, "scales"); + check_tensor(weights, dev, ScalarType::Float, "weights"); + check_tensor(starts, dev, ScalarType::Int, "starts"); + check_tensor(ends, dev, ScalarType::Int, "ends"); + check_tensor(out, dev, ScalarType::Int, "out"); + check_tensor(counter, dev, ScalarType::Int, "counter"); + check_tensor(values, dev, ScalarType::UInt16, "values"); + check_tensor(indices, dev, ScalarType::Int, "indices"); + check_tensor(retained, dev, ScalarType::Float, "retained"); + check_tensor(retained_indices, dev, ScalarType::Int, "retained_indices"); + check_tensor(counts, dev, ScalarType::Int, "counts"); + check_tensor(defer, dev, ScalarType::Int, "defer"); + + STD_TORCH_CHECK( + q.dim() == 3 && q.size(1) == kNumHeads && q.size(2) == kHeadDim, + "q must be [rows, 32, 128]"); + STD_TORCH_CHECK(k.dim() == 2 && k.size(1) == kHeadDim, + "k must be [keys, 128]"); + const int64_t rows = q.size(0), keys = k.size(0); + // TMA row coordinates (rows * 32 Q rows) are int32. + STD_TORCH_CHECK(rows > 0 && rows < (int64_t(1) << 26), + "rows must be in [1, 2^26)"); + STD_TORCH_CHECK(keys > 0 && keys <= kMaxKeys, "keys must be in [1, 1048576]"); + const int64_t blocks = (rows + kBlockRows - 1) / kBlockRows; + STD_TORCH_CHECK(scales.numel() == keys, "scales must be [keys]"); + STD_TORCH_CHECK(weights.dim() == 2 && weights.size(0) == rows && + weights.size(1) == kNumHeads, + "weights must be [rows, 32]"); + STD_TORCH_CHECK(starts.numel() == rows && ends.numel() == rows, + "starts/ends must be [rows]"); + STD_TORCH_CHECK(out.dim() == 2 && out.size(0) == rows && + fused_mqa_topk::is_supported_topk(out.size(1)), + "out must be [rows, K] with K in {512, 1024, 2048}"); + check_aligned(q, "q"); + check_aligned(k, "k"); + check_aligned(scales, "scales"); + check_aligned(weights, "weights"); + check_aligned(out, "out"); + + torch::stable::accelerator::DeviceGuard guard(dev); + const cudaStream_t stream = get_current_cuda_stream(dev); + const uint32_t urows = static_cast(rows), + ukeys = static_cast(keys), + ublocks = static_cast(blocks), + topk = static_cast(out.size(1)); + const auto* starts_p = static_cast(starts.data_ptr()); + const auto* ends_p = static_cast(ends.data_ptr()); + auto* out_p = static_cast(out.data_ptr()); + // Q: rows * 32 head rows of 128 bytes, a block's 256 per box; K: keys rows of + // 128 bytes, a tile's 256 per box (both 128B-swizzled); per-key scales: a + // tile's 256; weights: a block's 8 rows of 32. + const int head_rows = int(rows) * int(kNumHeads); + auto tq = make_2d(q.data_ptr(), CU_TENSOR_MAP_DATA_TYPE_UINT8, 1, kHeadDim, + head_rows, kHeadDim, kBlockRows * kNumHeads, kHeadDim, 128); + auto tk = make_2d(k.data_ptr(), CU_TENSOR_MAP_DATA_TYPE_UINT8, 1, kHeadDim, + int(keys), kHeadDim, kTileKeys, kHeadDim, 128); + auto ts = make_2d(scales.data_ptr(), CU_TENSOR_MAP_DATA_TYPE_FLOAT32, 4, + int(keys), 1, kTileKeys, 1, 0, 0); + auto tw = make_2d(weights.data_ptr(), CU_TENSOR_MAP_DATA_TYPE_FLOAT32, 4, + kNumHeads, int(rows), kNumHeads, kBlockRows, kNumHeads, 0); + + STD_TORCH_CHECK(counter.numel() == 2, "counter must be [2]"); + constexpr int64_t kCtaRows = kSlotsPerCta * kBlockRows; + STD_TORCH_CHECK(values.dim() == 2 && values.size(1) == kCandidatesPerRow && + values.size(0) % kCtaRows == 0 && indices.dim() == 2 && + indices.size(0) == values.size(0) && + indices.size(1) == kCandidatesPerRow, + "values/indices must be [8 * slots, 16384], slots even"); + STD_TORCH_CHECK(values.size(0) > 0, "the candidate workspace is empty"); + STD_TORCH_CHECK(retained.dim() == 2 && retained.size(1) == kRetainedPerRow && + retained.size(0) == values.size(0) && + retained_indices.dim() == 2 && + retained_indices.size(0) == retained.size(0) && + retained_indices.size(1) == kRetainedPerRow, + "retained/retained_indices must be [8 * slots, 4096]"); + STD_TORCH_CHECK( + counts.numel() >= blocks * kBlockRows && defer.numel() >= blocks, + "per-row workspace too small"); + // One persistent CTA per SM (capped by the blocks and the workspace slots); + // CTA c owns candidate slots 2c and 2c + 1. + const uint32_t grid = static_cast(std::min( + {int64_t(sm_count(dev)), blocks, values.size(0) / kCtaRows})); + LaunchConfig c; + const cudaError_t config_err = launch_config(dev, ukeys, topk, c); + STD_TORCH_CHECK(config_err != cudaErrorInvalidConfiguration, + "fused_mqa_topk_prefill needs more shared memory per block " + "than this GPU allows"); + check_cuda(config_err); + auto tks = make_strided_kv(k.data_ptr(), c.sample_groups, c.sample_stride); + const fused_mqa_topk::rescan::RescanBuffers rescan{ + static_cast(retained.data_ptr()), + static_cast(retained_indices.data_ptr())}; + c.kernel<<>>( + urows, ukeys, starts_p, ends_p, out_p, + static_cast(values.data_ptr()), + static_cast(indices.data_ptr()), + static_cast(counts.data_ptr()), rescan, c.phase_bytes, + c.sample_target, c.sample_stride, c.sample_groups, c.sample_tiles, + static_cast(scales.data_ptr()), tks, tq, ts, tk, tw, + ublocks, static_cast(counter.data_ptr()), + static_cast(defer.data_ptr())); + check_cuda(cudaGetLastError()); +} diff --git a/csrc/fused_mqa_topk/persistent_epoch.inc b/csrc/fused_mqa_topk/persistent_epoch.inc new file mode 100644 index 000000000000..1af6a1d2088b --- /dev/null +++ b/csrc/fused_mqa_topk/persistent_epoch.inc @@ -0,0 +1,742 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright contributors to the vLLM project +// +// Body of one persistent epoch (included twice by persistent_kernel.cuh: the +// first epoch is straight-line code so the hot loops are not nested in an outer +// loop). +if (warp_idx == 9 && cute::elect_one_sync()) { + if (epoch) + for (uint32_t i = 0; i < BarIdx::total; ++i) mbar_inval(bar_words + i); + for (uint32_t i = 0; i < BarIdx::total; ++i) + reinterpret_cast(bar_words)[i] = 0; + full_q.init(1); + empty_q.init(kMathThreads + 32); // math threads + the UMMA warp + for (uint32_t i = 0; i < kKVStages; ++i) { + full_kv[i].init(kFullKvArrivals); + empty_kv[i].init(kMathThreads); + } + for (uint32_t i = 0; i < BarIdx::kTmemSlots; ++i) { + full_tmem[i].init(1); + empty_tmem[i].init(kMathWgThreads); + } + full_lin->init(32); + // Every reader lane arrives on claim_empty. + for (uint32_t i = 0; i < 2; ++i) { + claim_full[i].init(32); + claim_empty[i].init(kClaimConsumers * 32); + } + sel_done.init(kMathWarps); // one lane per math warp + for (uint32_t i = 0; i < 2 * kMathWarps; ++i) { + h_full[i].init(32); + h_empty[i].init(32); + } + cutlass::arch::fence_barrier_init(); + unsafe_rows = 0; +} +__syncwarp(); +if (warp_idx == 10) cute::TMEM::Allocator1Sm().allocate(512, &tmem_ptr); +__syncthreads(); + +if (warp_idx == 8) { + // -------- claimer + Q/weights producer + drain of WG1 rows -------- + cutlass::arch::warpgroup_reg_dealloc(); + // n_blk: blocks this CTA processed (all-short blocks excluded). + uint32_t n_blk = 0, drain_interval = 0; + for (uint32_t k = 0;; ++k) { + if (lane == 0) { + empty_q.wait((n_blk & 1) ^ 1); + } + __syncwarp(); + // Every lane waits: lanes 0-7 write the entry. + claim_empty[k & 1].wait(((k >> 1) & 1) ^ 1); + uint32_t b = 0; + if (lane == 0) + b = atomicOr(&pause_req, 0u) ? kPause : atomicAdd(counter, 1u); + b = __shfl_sync(kFullMask, b, 0); + ClaimEntry& e = entries[k & 1]; + if (b >= nblocks) { + if (lane == 0) { + const uint32_t kind = b == kPause ? kPause : kTerm; + e.vblock = kind; + epoch_kind = kind; + } + __syncwarp(); + claim_full[k & 1].arrive(); // every writer lane arrives + break; + } + // Blocks are claimed heaviest (last rows, longest context) first. + const uint32_t vblock = nblocks - 1 - b, row_base = vblock * kBlockRows; + uint32_t st = 0, en = 0; + if (lane < kBlockRows) { + const uint32_t r = row_base + lane; + if (r < rows) { + st = min(starts_g[r], keys); + en = max(min(ends_g[r], keys), st); + } + e.starts[lane] = st; + e.ends[lane] = en; + } + const bool all_short = + __ballot_sync(kFullMask, lane < kBlockRows && en - st > topk) == 0; + const bool gated = + __ballot_sync(kFullMask, + lane < kBlockRows && en - st > sample_target) != 0; + // KV range: rows past the end are clamped to the last valid row (DeepGEMM + // scheduler). + const uint32_t src = min(row_base + (lane & 7), rows - 1) - row_base; + uint32_t lo = __shfl_sync(kFullMask, st, src), + hi = __shfl_sync(kFullMask, en, src); +#pragma unroll + for (int d = 4; d >= 1; d >>= 1) { + lo = min(lo, __shfl_xor_sync(kFullMask, lo, d)); + hi = max(hi, __shfl_xor_sync(kFullMask, hi, d)); + } + const uint32_t kv_base = lo / 4 * 4, + splits = (hi - kv_base + kTileKeys - 1) / kTileKeys; + // CTA-private slot pair: block n of this CTA uses slot 2 * cta + (n & 1); + // at most two blocks (one in selection, one claimed ahead) are in flight, + // so no claim atomics. + const uint32_t slot = kSlotsPerCta * blockIdx.x + (n_blk & 1); + if (!all_short && lane < kBlockRows) row_counts[row_base + lane] = 0; + if (lane == 0) { + e.vblock = vblock; + e.all_short = all_short; + e.kv_base = kv_base; + e.splits = splits; + e.samples = gated ? sample_tiles : 0u; + e.slot = slot; + } + __syncwarp(); + claim_full[k & 1].arrive(); // every writer lane arrives + if (all_short) continue; + if (lane == 0) { + dg::tma_copy(&tq, &full_q, sm.q, 0, + row_base * kNumHeads); + dg::tma_copy(&tw, &full_q, sm.weights, 0, + row_base); + full_q.arrive_and_expect_tx(kQBytes + kWBytes); + } + __syncwarp(); + ++n_blk; + const int64_t row_delta = slot_row_delta(slot, row_base); + for (uint32_t base = 0; base < splits; base += Interval, ++drain_interval) { + for (uint32_t p = kMathWarps / 2; p < kMathWarps; ++p) + drain_handoff(hdata, h_full, h_empty, p, drain_interval, row_base + 4, + row_counts, base + Interval >= splits, + [&](uint32_t row, uint32_t slot2, uint32_t record, + uint32_t batch_base, uint32_t source) { + store_ring_record(vals, indices, row, row_delta, slot2, + record, kv_base, batch_base, + source % kMathWgThreads); + }); + } + cutlass::arch::NamedBarrier(kDrainSyncThreads, kBarDrain).arrive(); + } +} else if (warp_idx == 9) { + // -------- KV producer: sample tiles then scan tiles, one ring -------- + cutlass::arch::warpgroup_reg_dealloc(); + RingPipeline kv_pipe; + uint32_t n_blk = 0; + for (uint32_t k = 0;; ++k) { + ClaimHeader claim; + if (!consume_claim(k, claim_full, claim_empty, entries, claim)) break; + if (claim.all_short) continue; + const uint32_t kv_base = claim.kv_base, splits = claim.splits, + samples = claim.samples; + { + // The whole warp stages the strided sample scales into sf_kv[stage] + // (prefetched one tile ahead in registers); lane 0 issues the KV TMA. + if (lane == 0 && n_blk > 0) sel_done.wait((n_blk - 1) & 1); + __syncwarp(); + const uint32_t center = sample_stride / 2; + // Per-key scales of one sample tile, kScalesPerLane per lane. + constexpr uint32_t kScalesPerLane = kTileKeys / 32; + float pf[kScalesPerLane]; +#pragma unroll + for (uint32_t j = 0; j < kScalesPerLane; ++j) { + const uint32_t g = j * 32 + lane; + pf[j] = g < sample_groups ? __ldg(scales + g * sample_stride + center) + : 0.0f; + } +#pragma unroll 1 + for (uint32_t t = 0; t < samples; ++t) { + // Only lane 0's ring state is maintained across blocks (the scan loop + // runs on lane 0). + CUTE_TIE_DECL(kv_pipe.advance(), s0, ph); + const uint32_t s = __shfl_sync(kFullMask, s0, 0); + // Every writing lane acquires and releases the stage itself. + empty_kv[s].wait(__shfl_sync(kFullMask, ph, 0) ^ 1); +#pragma unroll + for (uint32_t j = 0; j < kScalesPerLane; ++j) + sm.sf_kv[s][j * 32 + lane] = pf[j]; + // Generic writes before later TMA writes of this stage. + cutlass::arch::fence_view_async_shared(); + if (lane == 0) { + full_kv[s].expect_transaction(kKVBytes); + dg::tma_copy( + &tks, &full_kv[s], sm.kv[s], 0, t * kTileKeys, center); + } + full_kv[s].arrive(); +#pragma unroll + for (uint32_t j = 0; j < kScalesPerLane; ++j) { + const uint32_t g = (t + 1) * kTileKeys + j * 32 + lane; + pf[j] = g < sample_groups ? __ldg(scales + g * sample_stride + center) + : 0.0f; + } + } + } + if (lane == 0) { +#pragma unroll 1 + for (uint32_t t = 0; t < splits; ++t) { + CUTE_TIE_DECL(kv_pipe.advance(), s, ph); + empty_kv[s].wait(ph ^ 1); + const uint32_t off = kv_base + t * kTileKeys; + dg::tma_copy(&tk, &full_kv[s], sm.kv[s], 0, + off); + dg::tma_copy(&ts, &full_kv[s], sm.sf_kv[s], off, 0); + mbar_arrive_expect_tx(&full_kv[s], kKVBytes + kScaleBytes, + kFullKvArrivals); + } + } + __syncwarp(); + ++n_blk; + } +} else if (warp_idx == 10) { + // -------- UMMA issuer -------- + cutlass::arch::warpgroup_reg_dealloc(); + RingPipeline<1> q_pipe; + RingPipeline kv_pipe; + RingPipeline tmem_pipe; + auto instr_desc = + cute::UMMA::make_instr_desc(); + auto runtime_instr_desc = cute::UMMA::make_runtime_instr_desc(instr_desc); + for (uint32_t k = 0;; ++k) { + ClaimHeader claim; + if (!consume_claim(k, claim_full, claim_empty, entries, claim)) break; + if (claim.all_short) continue; + const uint32_t splits = claim.splits, samples = claim.samples; + CUTE_TIE_DECL(q_pipe.advance(), q_stage, q_phase); + (void)q_stage; + full_q.wait(q_phase); + full_lin->wait(q_phase); // this block's linear-term rows (warp 11) + ptx::tcgen05_after_thread_sync(); + const uint32_t total = samples + splits; +#pragma unroll 1 + for (uint32_t t = 0; t < total; ++t) { + CUTE_TIE_DECL(kv_pipe.advance(), s, ph); + full_kv[s].wait(ph); + if (cute::elect_one_sync()) { + // Per KV half, one N=144 MMA per math warpgroup into its own TMEM slot + // (ring of 3; WG0 takes even, WG1 odd positions of the issue sequence). +#pragma unroll + for (uint32_t i = 0; i < 2; ++i) { +#pragma unroll + for (uint32_t g = 0; g < 2; ++g) { + CUTE_TIE_DECL(tmem_pipe.advance(), ts_idx, ts_ph); + const uint32_t tmem_addr = ts_idx * kSlotCols; + empty_tmem[ts_idx].wait(ts_ph ^ 1); + ptx::tcgen05_after_thread_sync(); + cutlass::float_e4m3_t* b_base = + g == 0 ? reinterpret_cast(sm.lin_lo) + : sm.q + 4 * kNumHeads * kHeadDim; // WG1's rows +#pragma unroll + for (uint32_t kk = 0; kk < 4; ++kk) { + auto a_desc = + mma::sm100::make_umma_desc( + sm.kv[s], i * 128, kk * 32); + auto b_desc = + mma::sm100::make_umma_desc( + b_base, 0, kk * 32); + ptx::SM100_MMA_F8F6F4_SS::fma(a_desc, b_desc, tmem_addr, kk, + runtime_instr_desc); + } + ptx::umma_arrive_no_elect(full_tmem[ts_idx]); + } + } + } + __syncwarp(); + } + empty_q.arrive(); + } +} else if (warp_idx == 11) { + // -------- linear-term rows + drain of WG0 rows -------- + cutlass::arch::warpgroup_reg_dealloc(); + uint32_t drain_interval = 0, n_lin = 0; + for (uint32_t k = 0;; ++k) { + ClaimHeader claim; + if (!consume_claim(k, claim_full, claim_empty, entries, claim)) break; + if (claim.all_short) continue; + const uint32_t vblock = claim.vblock, kv_base = claim.kv_base, + splits = claim.splits, slot = claim.slot; + // Q of block k lands while the math warps still select block k-1, so this + // is off the critical path except for a CTA's first block. + full_q.wait(n_lin & 1); + ++n_lin; + compute_lin_rows(sm, lane); + // Generic smem writes -> UMMA (async proxy). + cutlass::arch::fence_view_async_shared(); + full_lin->arrive(); // every lane arrives (count 32) + const uint32_t row_base = vblock * kBlockRows; + const int64_t row_delta = slot_row_delta(slot, row_base); + for (uint32_t base = 0; base < splits; base += Interval, ++drain_interval) { + for (uint32_t p = 0; p < kMathWarps / 2; ++p) + drain_handoff(hdata, h_full, h_empty, p, drain_interval, row_base, + row_counts, base + Interval >= splits, + [&](uint32_t row, uint32_t slot2, uint32_t record, + uint32_t batch_base, uint32_t source) { + store_ring_record(vals, indices, row, row_delta, slot2, + record, kv_base, batch_base, + source % kMathWgThreads); + }); + } + cutlass::arch::NamedBarrier(kDrainSyncThreads, kBarDrain).arrive(); + } +} else { + // -------- math warps 0-7 -------- + cutlass::arch::warpgroup_reg_alloc(); + const uint32_t math_tid = threadIdx.x; + const uint32_t query_base = (warp_idx / 4) * kWgRows; // 4 warps per WG + RingPipeline<1> q_pipe; + RingPipeline kv_pipe; + RingPipeline tmem_pipe; + // Each warpgroup consumes every second TMEM slot position (WG1 starts at 1). + constexpr uint32_t kTmemStep = 2; + if (warp_idx >= 4) tmem_pipe.advance(1); + const uint32_t heads_col = warp_idx < 4 ? 16u : 0u; + const uint32_t lin_col = warp_idx < 4 ? 0u : 128u; + uint32_t produce_interval = 0; + for (uint32_t k = 0;; ++k) { + claim_full[k & 1].wait((k >> 1) & 1); + const ClaimEntry& e = entries[k & 1]; + const uint32_t vblock = e.vblock; + if (vblock >= kPause) break; + const uint32_t row_base = vblock * kBlockRows; + if (e.all_short) { + for (uint32_t r = 0; r < kBlockRows && row_base + r < rows; ++r) + write_all_keys(out + (uint64_t)(row_base + r) * topk, topk, + e.ends[r] - e.starts[r], math_tid, kMathThreads); + __syncwarp(); + claim_empty[k & 1].arrive(); + continue; + } + // ---- block setup: Q, weights, sample histogram ---- + { + CUTE_TIE_DECL(q_pipe.advance(), q_stage, q_phase); + (void)q_stage; + full_q.wait(q_phase); + full_lin->wait(q_phase); + } + static_assert(sizeof(SampleShared) % 16 == 0); + for (uint32_t j = math_tid; j < sizeof(SampleShared) / 16; + j += kMathThreads) + reinterpret_cast(sample_smem.hist)[j] = make_uint4(0, 0, 0, 0); + float weights[kWgRows][kNumHeads]; + float row_s[kWgRows]; // pow2 row scale of the linear term + uint32_t seq_k_start[kWgRows], seq_k_end[kWgRows]; +#pragma unroll + for (uint32_t i = 0; i < kWgRows; ++i) { + seq_k_start[i] = e.starts[query_base + i]; + seq_k_end[i] = e.ends[query_base + i]; + // Weights in units of 1 / s_r (exact: s_r is a power of two). + row_s[i] = sm.lin_scale[query_base + i]; + const float inv_s = 1.0f / row_s[i]; +#pragma unroll + for (uint32_t j = 0; j < kNumHeads; j += 4) { + const float4 w4 = *reinterpret_cast( + sm.weights + (query_base + i) * kNumHeads + j); + weights[i][j] = w4.x * 0.5f * inv_s; + weights[i][j + 1] = w4.y * 0.5f * inv_s; + weights[i][j + 2] = w4.z * 0.5f * inv_s; + weights[i][j + 3] = w4.w * 0.5f * inv_s; + } + } + { + // Weights that are subnormal, or become subnormal or infinite once + // scaled, take the exact recovery path. + const float w = sm.weights[warp_idx * kNumHeads + lane]; // lane = head + // 2^-125: the smallest magnitude whose half is still a normal float. + constexpr float kMinScalable = 0x1p-125f; + bool bad_w = w != 0.0f && fabsf(w) < kMinScalable; + const float wr = w * 0.5f * (1.0f / sm.lin_scale[warp_idx]); + bad_w |= (wr != 0.0f && fabsf(wr) < kMinScalable) || isinf(wr); + const uint32_t bad = __ballot_sync(kFullMask, bad_w); + if (lane == 0 && bad) atomicOr(&unsafe_rows, 1u << warp_idx); + } + math_wg_sync(); + // ---- sample tiles ---- + // The first tile's per-row max is the histogram reference (see + // SampleShared): tile 0 keeps its scores until the reference is known. + float ref[kWgRows] = {}; +#pragma unroll 1 + for (uint32_t t = 0; t < e.samples; ++t) { + CUTE_TIE_DECL(kv_pipe.advance(), kv_stage, kv_phase); + full_kv[kv_stage].wait(kv_phase); + uint32_t first_code[kWgRows] = {}; +#pragma unroll + for (uint32_t kv_half = 0; kv_half < 2; ++kv_half) { + const uint32_t kv_lane = + (math_tid % kMathWgThreads) + kv_half * kMathWgThreads; + const uint32_t group = t * kTileKeys + kv_lane; + const uint32_t kv_offset = group * sample_stride + sample_stride / 2; + const bool in_view = group < sample_groups; + // 0 outside the view (staged by the KV producer). + const float scale_kv = ptx::ld_shared(sm.sf_kv[kv_stage] + kv_lane); + CUTE_TIE_DECL(tmem_pipe.advance(kTmemStep), tmem_stage, tmem_phase); + full_tmem[tmem_stage].wait(tmem_phase); + ptx::tcgen05_after_thread_sync(); + if (kv_half == 1) { + cutlass::arch::fence_view_async_shared(); + empty_kv[kv_stage].arrive(); + } + // One query row (32 TMEM columns) at a time keeps the sample loop's + // live registers low enough to carry the scan weights through it. +#pragma unroll + for (uint32_t i = 0; i < kWgRows; ++i) { + float acc[kNumHeads]; + float lin[4]; + ptx::tmem_load_32dp32b<32>( + tmem_stage * kSlotCols + heads_col + i * kNumHeads, + reinterpret_cast(acc)); + ptx::tmem_load_32dp32b<4>(tmem_stage * kSlotCols + lin_col + i * 4, + reinterpret_cast(lin)); + cutlass::arch::fence_view_async_tmem_load(); + if (i == kWgRows - 1) { + ptx::tcgen05_before_thread_sync(); + empty_tmem[tmem_stage].arrive(); + } + const float bq = reduce_heads_with_lin(acc, weights[i], lin) * + (row_s[i] * scale_kv); + const bool valid = in_view && kv_offset >= seq_k_start[i] && + kv_offset < seq_k_end[i] && isfinite(bq); + if (t == 0) { + sample_smem.first[kv_half][i][math_tid] = + valid ? bq : -CUDART_INF_F; + if (valid) first_code[i] = max(first_code[i], ordered_code(bq)); + } else if (valid) + add_sample(sample_smem.hist[query_base + i], bq - ref[i]); + } + } + if (t == 0) { +#pragma unroll + for (uint32_t i = 0; i < kWgRows; ++i) { + const uint32_t code = __reduce_max_sync(kFullMask, first_code[i]); + if (lane == 0) atomicMax(&sample_smem.ref_code[query_base + i], code); + } + math_wg_sync(); +#pragma unroll + for (uint32_t i = 0; i < kWgRows; ++i) { + ref[i] = sample_ref(sample_smem.ref_code[query_base + i]); +#pragma unroll + for (uint32_t h = 0; h < 2; ++h) { + const float x = sample_smem.first[h][i][math_tid]; + if (isfinite(x)) + add_sample(sample_smem.hist[query_base + i], x - ref[i]); + } + } + } + } + math_wg_sync(); + finish_sample_histogram( + sample_smem.hist[warp_idx], sample_ref(sample_smem.ref_code[warp_idx]), + gate_s + warp_idx, e.ends[warp_idx] - e.starts[warp_idx], sample_target, + lane); + math_wg_sync(); + // The block index is re-read from the claim entry (volatile) instead of + // kept live across the sample loop. + const uint32_t scan_row_base = volatile_load(e.vblock) * kBlockRows; + + // ---- scan tiles ---- + const uint32_t kv_base = e.kv_base, splits = e.splits; + const int64_t cand_row_delta = slot_row_delta(e.slot, scan_row_base); + float gate_score[kWgRows]; +#pragma unroll + for (uint32_t i = 0; i < kWgRows; ++i) { + gate_score[i] = gate_s[query_base + i]; + if ((volatile_load(unsafe_rows) >> (query_base + i)) & 1u) { + // Unsafe weights: no key passes (+inf gate), and a count above the + // candidate cap sends the block to the exact rescan. + gate_score[i] = CUDART_INF_F; + if (math_tid % kMathWgThreads == 0) + atomicAdd(row_counts + scan_row_base + query_base + i, + static_cast(kCandidatesPerRow) + 1); + } + // Compare S * scale_kv against gate / row_s (row_s is a power of two -> + // exact). + gate_score[i] *= 1.0f / row_s[i]; + } + uint32_t lane_counts[kWgRows] = {}; + uint32_t flush_base = 0; +#pragma unroll 1 + for (uint32_t kv_split_idx = 0; kv_split_idx < splits; ++kv_split_idx) { + if ((kv_split_idx & (Interval - 1)) == 0) { + h_empty[(produce_interval & 1) * kMathWarps + warp_idx].wait( + ((produce_interval >> 1) & 1) ^ 1); + __syncwarp(); + } + CUTE_TIE_DECL(kv_pipe.advance(), kv_stage, kv_phase); + full_kv[kv_stage].wait(kv_phase); +#pragma unroll + for (uint32_t kv_half = 0; kv_half < 2; ++kv_half) { + const uint32_t kv_lane = + (math_tid % kMathWgThreads) + kv_half * kMathWgThreads; + const uint32_t kv_offset = kv_base + kv_split_idx * kTileKeys + kv_lane; + const float scale_kv = ptx::ld_shared(sm.sf_kv[kv_stage] + kv_lane); + CUTE_TIE_DECL(tmem_pipe.advance(kTmemStep), tmem_stage, tmem_phase); + full_tmem[tmem_stage].wait(tmem_phase); + ptx::tcgen05_after_thread_sync(); + if (kv_half == 1) { + cutlass::arch::fence_view_async_shared(); + empty_kv[kv_stage].arrive(); + } + float accum_pair[2][kNumHeads]; + float lin_pair[2][4]; + float + bp4[kWgRows]; // row scores of the slot, in units of the row scale +#pragma unroll + for (uint32_t pair = 0; pair < kWgRows; pair += 2) { + const uint32_t slot_base = tmem_stage * kSlotCols; + ptx::tmem_load_32dp32b<2 * kNumHeads>( + slot_base + heads_col + pair * kNumHeads, + reinterpret_cast(accum_pair)); + ptx::tmem_load_32dp32b<8>(slot_base + lin_col + pair * 4, + reinterpret_cast(lin_pair)); + cutlass::arch::fence_view_async_tmem_load(); + if (pair + 2 >= kWgRows) { + ptx::tcgen05_before_thread_sync(); + empty_tmem[tmem_stage].arrive(); + } + // The linear term (sum_t term_t . k, in units of the row scale; terms + // 2, 3 in units of s * 2^-8) seeds the sums. +#pragma unroll + for (uint32_t r = 0; r < 2; ++r) + bp4[pair + r] = reduce_heads_with_lin( + accum_pair[r], weights[pair + r], lin_pair[r]) * + scale_kv; + } + // One warp vote per slot: most keys fail every gate, so the common path + // is 4 compares + 1 vote + 1 uniform branch. + const bool any_pass = + (bp4[0] > gate_score[0]) | (bp4[1] > gate_score[1]) | + (bp4[2] > gate_score[2]) | (bp4[3] > gate_score[3]); + if (__any_sync(kFullMask, any_pass)) [[unlikely]] { +#pragma unroll + for (uint32_t i = 0; i < kWgRows; ++i) { + const float bp = bp4[i]; + // Gate first; range and finiteness only for passing keys. + if (bp > gate_score[i]) { + const float bq = bp * row_s[i]; + const uint32_t row = scan_row_base + query_base + i; + if (kv_offset >= seq_k_start[i] && kv_offset < seq_k_end[i] && + isfinite(bq)) { + const uint32_t count = lane_counts[i]; + if (count < kRingSlots) { + const uint32_t code = candidate_fp24_code(bq); + hdata + .records[produce_interval & 1][warp_idx][i][count][lane] = + (code << 8) | ((kv_split_idx - flush_base) * 2 + kv_half); + lane_counts[i] = count + 1; + } else { + const int slot = atomicAdd(row_counts + row, 1); + if (slot < static_cast(kCandidatesPerRow)) { + const uint64_t off = + candidate_offset(row, cand_row_delta, slot); + store_candidate(vals + off, indices + off, bq, kv_offset); + } + } + } + } + } + } + } + if (((kv_split_idx + 1) & (Interval - 1)) == 0 && + kv_split_idx + 1 != splits) { +#pragma unroll + for (uint32_t i = 0; i < kWgRows; ++i) { + hdata.num_records[produce_interval & 1][warp_idx][i][lane] = + lane_counts[i]; + lane_counts[i] = 0; + } + if (lane == 0) hdata.bases[produce_interval & 1][warp_idx] = flush_base; + __syncwarp(); + h_full[(produce_interval & 1) * kMathWarps + warp_idx].arrive(); + ++produce_interval; + flush_base = kv_split_idx + 1; + } + } + { + // Final interval: every math warp stores its own ring records (8 warps in + // parallel instead of 2 drain warps after the scan); the drain warps only + // cycle the barriers. + const uint32_t bank = produce_interval & 1, + rbase = scan_row_base + query_base; + static_assert(sizeof(hdata.records[0][0]) == + kWgRows * kRingSlots * 32 * sizeof(uint32_t), + "ring record layout"); + self_drain_last(&hdata.records[bank][warp_idx][0][0][0], lane_counts[0], + lane_counts[1], lane_counts[2], lane_counts[3], + row_counts, rbase, kv_base + (math_tid % kMathWgThreads), + flush_base, cand_row_delta, vals, indices); +#pragma unroll + for (uint32_t row = 0; row < kWgRows; ++row) lane_counts[row] = 0; + __syncwarp(); + h_full[bank * kMathWarps + warp_idx].arrive(); + ++produce_interval; + } + cutlass::arch::fence_view_async_shared(); + empty_q.arrive(); + // Drains of this block done. + cutlass::arch::NamedBarrier(kDrainSyncThreads, kBarDrain).sync(); + // ---- selection ---- + // Re-read as well (not kept live across the scan). + const uint32_t sel_vblock = volatile_load(e.vblock), + sel_row_base = sel_vblock * kBlockRows; + __threadfence_block(); + cutlass::arch::NamedBarrier(kMathThreads, kBarSelect).sync(); + const ClaimEntry& sel_entry = entries[k & 1]; + if (math_tid == 0) { + uint32_t recover = 0, short_rows = 0; + row_offset[0] = 0; + for (uint32_t r = 0; r < kBlockRows; ++r) { + const bool is_short = sel_entry.ends[r] - sel_entry.starts[r] <= topk; + short_rows |= uint32_t(is_short) << r; + const int c = __ldcg(row_counts + sel_row_base + r); + recover |= !is_short && (c < int(topk) || c > int(kCandidatesPerRow)); + // Rows start 16-byte aligned for select_staged_row's vector loads. + row_offset[r + 1] = + row_offset[r] + (is_short ? 0 : (uint32_t(max(c, 0)) + 3u) & ~3u); + } + // 32-bit byte arithmetic (sizeof is a 64-bit size_t). + constexpr uint32_t kWordBytes = sizeof(uint32_t); + // Stage all 8 rows' candidates at once (1), in two rounds of 4 (2), or, + // when they exceed the scratch, rescan exactly. + const uint32_t half = max(row_offset[kWgRows], + row_offset[kBlockRows] - row_offset[kWgRows]); + const uint32_t staged = + recover ? 0u + : ((kBlockRows * selection::kBins + row_offset[kBlockRows]) * + kWordBytes <= + phase_bytes + ? 1u + : ((kWgRows * selection::kBins + half) * kWordBytes <= + phase_bytes + ? 2u + : 0u)); + if (staged == 0) recover = 1; + if (recover) { + defer_next[sel_vblock] = int(defer_head); + defer_head = sel_vblock; + atomicExch(&pause_req, 1u); + } + sel_recover = recover; + sel_staged = staged; + sel_short = short_rows; + unsafe_rows = 0; + } + cutlass::arch::NamedBarrier(kMathThreads, kBarSelect).sync(); + const uint32_t recover = sel_recover, short_rows = sel_short; + const auto write_short = [&](uint32_t r, uint32_t t, uint32_t stride) { + if (r >= rows) return; + write_all_keys( + out + (uint64_t)r * topk, topk, + sel_entry.ends[r - sel_row_base] - sel_entry.starts[r - sel_row_base], + t, stride); + }; + if (!recover) { + const int64_t row_delta = slot_row_delta(sel_entry.slot, sel_row_base); + const uint32_t per_round = sel_staged == 1 ? kBlockRows : kWgRows; + for (uint32_t round = 0; round < kBlockRows / per_round; ++round) { + if (warp_idx < per_round) { + const uint32_t local = round * per_round + warp_idx, + first = round * per_round; + uint32_t* hist = reinterpret_cast(smem_buffer + kSelBase) + + warp_idx * selection::kBins; + uint32_t* codes = + reinterpret_cast(smem_buffer + kSelBase) + + per_round * selection::kBins + + (row_offset[local] - row_offset[first]); + const uint32_t r = sel_row_base + local; + const uint64_t base = candidate_offset(r, row_delta, 0); + if ((short_rows >> local) & 1u) + write_short(r, lane, 32); + else { + const uint32_t cnt = __ldcg(row_counts + r); + int* o = out + (uint64_t)r * topk; + if (!selection::select_staged_row(codes, cnt, vals + base, + indices + base, o, topk, hist)) + selection::select_row(codes, cnt, vals + base, indices + base, o, + topk, hist); + } + } + cutlass::arch::NamedBarrier(kMathThreads, kBarSelect).sync(); + } + } + // Every row is final here: the last selection round ends with a barrier, + // and a recovered block skipped the rounds right after the barrier above. + // Short rows were written relative already. + if (!recover) + make_relative(out, topk, rows, sel_row_base, starts_g, math_tid, + kMathThreads, [&](uint32_t local) { + return sel_entry.ends[local] - sel_entry.starts[local] <= + topk; + }); + // The selection scratch aliases kv[] and sf_kv[]. Order its generic + // accesses before the next block's TMA writes, which have no proxy fence + // of their own when that block has no sample tiles. + cutlass::arch::fence_view_async_shared(); + __syncwarp(); + if (lane == 0) sel_done.arrive(); + claim_empty[k & 1].arrive(); + } +} + +// -------- exact rescan of the blocks queued by the selection -------- +// The rescan's first TMA loads overwrite the epoch's generic shared memory. +cutlass::arch::fence_view_async_shared(); +__syncthreads(); +if (warp_idx == 0) cute::TMEM::Allocator1Sm().free(0, 512); +if (defer_head != kTerm) { + // The rescan expects the 224 / 56 register split of the math warps. + __syncthreads(); + for (uint32_t cur = defer_head; cur != kTerm;) { + const uint32_t vblock = cur, row_base = vblock * kBlockRows; + if (threadIdx.x < kBlockRows) { + const uint32_t r = row_base + threadIdx.x; + row_bounds[0][threadIdx.x] = r < rows ? min(starts_g[r], keys) : 0u; + row_bounds[1][threadIdx.x] = + r < rows ? max(min(ends_g[r], keys), row_bounds[0][threadIdx.x]) : 0u; + } + __syncthreads(); + const uint32_t* const starts = row_bounds[0] - row_base; + const uint32_t* const ends = row_bounds[1] - row_base; + // No block is in flight during the rescan: use the CTA's first slot. + const int64_t row_delta = + slot_row_delta(kSlotsPerCta * blockIdx.x, row_base); + const RescanBuffers block_buffers{ + rescan.retained + row_delta * kRetainedPerRow, + rescan.retained_indices + row_delta * kRetainedPerRow}; + rescan::rescan_block(dense::Scheduler(vblock, rows, keys, starts, ends), + block_buffers, out, topk, tq, tk, ts, tw); + __syncthreads(); + if (threadIdx.x == 0) + dense::invalidate_barriers( + *reinterpret_cast(smem_buffer)); + __syncthreads(); + // The rescan writes absolute indices for every row of the block. + make_relative(out, topk, rows, row_base, starts_g, threadIdx.x, kNumThreads, + [](uint32_t) { return false; }); + cur = uint32_t(__ldcg(defer_next + vblock)); + // Order the generic mbarrier.inval above before the TMA writes of the + // next rescan block or the next epoch. + cutlass::arch::fence_view_async_shared(); + __syncthreads(); + } + // The loop ran at least once and ended with a barrier. + if (threadIdx.x == 0) { + defer_head = kTerm; + pause_req = 0; + } +} // deferred blocks +more = epoch_kind != kTerm; +__syncthreads(); diff --git a/csrc/fused_mqa_topk/persistent_kernel.cuh b/csrc/fused_mqa_topk/persistent_kernel.cuh new file mode 100644 index 000000000000..86063b08fbac --- /dev/null +++ b/csrc/fused_mqa_topk/persistent_kernel.cuh @@ -0,0 +1,592 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright contributors to the vLLM project +// +// Persistent fused FP8 MQA logits + per-row top-K (K = 512, 1024, 2048; SM100), +// DeepGEMM/FA4-style: one CTA per SM, barriers and TMEM initialised once, every +// warp role runs its own loop over blocks claimed (heaviest first) from a +// global counter, and the sample and scan tiles of a block flow through ONE +// continuous Q/KV/TMEM pipeline whose stage/phase state is carried across +// blocks. +// +// Roles (384 threads): +// warp 8 claimer + Q/weights TMA + drain of math WG1 rows +// warp 9 KV TMA (sample tiles via the strided map, then the scan tiles) +// warp 10 UMMA issuer +// warp 11 linear-term rows + drain of math WG0 rows +// warps 0-7 math: sample histogram -> gate -> scan/emit -> selection +// A block is rescanned exactly (rescan.cuh) when a row's candidate count +// leaves [K, kCandidatesPerRow], a row's weights are unsafe to scale, or the +// block's candidates exceed the selection scratch (~11K+ per row): the role +// loops pause, the CTA rescans its queued blocks, and the loops resume with +// freshly initialised barriers. +#pragma once +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "candidates.cuh" +#include "common.cuh" +#include "constants.h" +#include "deep_gemm_compat.cuh" +#include "rescan.cuh" +#include "select.cuh" + +namespace fused_mqa_topk::persist { +using namespace deep_gemm; +using rescan::RescanBuffers; +using rescan::RescanShared; +using TBar = cutlass::arch::ClusterTransactionBarrier; + +constexpr uint32_t kTerm = 0xffffffffu, kPause = 0xfffffffeu; +// Bytes of a block's Q, of one KV tile and its per-key scales, of a block's +// weights. +constexpr uint32_t kQBytes = kBlockRows * kNumHeads * kHeadDim, + kKVBytes = kTileKeys * kHeadDim, + kScaleBytes = kTileKeys * sizeof(float), + kWBytes = kBlockRows * kNumHeads * sizeof(float); +// KV (and per-key scale) pipeline stages. +constexpr uint32_t kKVStages = 3; +// Linear-term B rows per math warpgroup: 4 query rows x 4 FP8 terms. +constexpr uint32_t kLinRows = 16; +// Terms 2 and 3 of the linear term are stored in units of s * 2^-8. +constexpr float kLinLoScale = 0x1p-8f; +// TMEM: one UMMA per math warpgroup and KV half writes 4 rows x 32 heads plus +// the kLinRows linear-term columns (N = 144). Slots are 160 columns apart (144 +// rounded up to a multiple of 32); the 3 slots take 480 of the 512 allocated +// columns. +constexpr uint32_t kUmmaN = 4 * kNumHeads + kLinRows, kSlotCols = 160; +static_assert(kSlotCols >= kUmmaN && kSlotCols % 32 == 0); + +// Rebalanced scan: the linear-term rows of each math warpgroup bracket Q, so +// WG0's N=144 UMMA reads B = [lin WG0 | Q rows 0..127] (D = [lin | heads]) and +// WG1's reads B = [Q rows 128..255 | lin WG1] (D = [heads | lin]). +struct KernelSmem { + alignas(1024) uint8_t lin_lo[kLinRows * kHeadDim]; + cutlass::float_e4m3_t q[kQBytes]; + uint8_t lin_hi[kLinRows * kHeadDim]; + alignas(128) float weights[kBlockRows * kNumHeads]; + float lin_scale[kBlockRows]; + alignas(1024) cutlass::float_e4m3_t kv[kKVStages][kKVBytes]; + alignas(128) float sf_kv[kKVStages][kTileKeys]; +}; +static_assert(offsetof(KernelSmem, q) == kLinRows * kHeadDim && + offsetof(KernelSmem, lin_hi) == kLinRows * kHeadDim + kQBytes, + "lin rows must bracket Q"); +// Selection scratch starts after lin + Q + lin + weights + row scales, so the +// next block's Q and linear term land during this block's selection. +constexpr uint32_t kSelBase = offsetof(KernelSmem, kv); + +// Byte offset of byte `col` of row `row` in a 128B-swizzled K-major tile of +// 128-byte rows (the UMMA B layout of Q and the linear-term rows). +__device__ __forceinline__ uint32_t swizzle128_offset(uint32_t row, + uint32_t col) { + return row * 128u + (((col >> 4) ^ (row & 7u)) << 4) + (col & 15u); +} + +// q'_r = sum_h (w_rh / 2) q_rh, split into 4 FP8 e4m3 terms: terms 0,1 scale +// s_r, terms 2,3 s_r * 2^-8 (lin_scale[r]); term t of row r is B row (r % 4) * +// 4 + t of lin_lo (rows 0-3) / lin_hi (4-7), 128B-swizzled K-major like Q. One +// warp; lane l owns dims 4l..4l+3. +__device__ __forceinline__ void compute_lin_rows(KernelSmem& sm, + uint32_t lane) { + const uint8_t* qbytes = reinterpret_cast(sm.q); +#pragma unroll 1 + for (uint32_t r = 0; r < kBlockRows; ++r) { + float acc[4] = {0.f, 0.f, 0.f, 0.f}; +#pragma unroll 8 + for (uint32_t h = 0; h < kNumHeads; ++h) { + const uint32_t off = swizzle128_offset(r * kNumHeads + h, lane * 4u); + const uint32_t packed = + ptx::ld_shared(reinterpret_cast(qbytes + off)); + const float wv = ptx::ld_shared(sm.weights + r * kNumHeads + h) * 0.5f; + const __half2_raw lo = __nv_cvt_fp8x2_to_halfraw2( + static_cast<__nv_fp8x2_storage_t>(packed & 0xffffu), __NV_E4M3); + const __half2_raw hi = __nv_cvt_fp8x2_to_halfraw2( + static_cast<__nv_fp8x2_storage_t>(packed >> 16), __NV_E4M3); + const float2 f0 = __half22float2(*reinterpret_cast(&lo)); + const float2 f1 = __half22float2(*reinterpret_cast(&hi)); + acc[0] = fmaf(wv, f0.x, acc[0]); + acc[1] = fmaf(wv, f0.y, acc[1]); + acc[2] = fmaf(wv, f1.x, acc[2]); + acc[3] = fmaf(wv, f1.y, acc[3]); + } + float m = fmaxf(fmaxf(fabsf(acc[0]), fabsf(acc[1])), + fmaxf(fabsf(acc[2]), fabsf(acc[3]))); +#pragma unroll + for (uint32_t o = 16; o > 0; o >>= 1) + m = fmaxf(m, __shfl_xor_sync(kFullMask, m, o)); + int e = -120; // pow2 scale s >= m / 448, clamped so 1/s stays finite + if (m > 0.f && isfinite(m)) { + frexpf(m * (1.0f / 448.0f), &e); + e = max(e, -120); + } + const float s = ldexpf(1.0f, e), inv_s = ldexpf(1.0f, -e); + float res[4]; +#pragma unroll + for (uint32_t dd = 0; dd < 4; ++dd) res[dd] = acc[dd] * inv_s; + uint8_t* lin_base = r < kWgRows ? sm.lin_lo : sm.lin_hi; +#pragma unroll + for (uint32_t tt = 0; tt < 4; ++tt) { + const __nv_fp8x2_storage_t c01 = __nv_cvt_float2_to_fp8x2( + make_float2(res[0], res[1]), __NV_SATFINITE, __NV_E4M3); + const __nv_fp8x2_storage_t c23 = __nv_cvt_float2_to_fp8x2( + make_float2(res[2], res[3]), __NV_SATFINITE, __NV_E4M3); + const __half2_raw d01 = __nv_cvt_fp8x2_to_halfraw2(c01, __NV_E4M3); + const __half2_raw d23 = __nv_cvt_fp8x2_to_halfraw2(c23, __NV_E4M3); + const float2 g01 = + __half22float2(*reinterpret_cast(&d01)); + const float2 g23 = + __half22float2(*reinterpret_cast(&d23)); + res[0] -= g01.x; + res[1] -= g01.y; + res[2] -= g23.x; + res[3] -= g23.y; + // Terms 2, 3 in units of s * 2^-8: the residual after two terms is <= + // 0.5, so x256 <= 128 (no saturation), and the e4m3 subnormal floor drops + // from s * 2^-10 to s * 2^-18 per dim. + if (tt == 1) { + res[0] *= 1.0f / kLinLoScale; + res[1] *= 1.0f / kLinLoScale; + res[2] *= 1.0f / kLinLoScale; + res[3] *= 1.0f / kLinLoScale; + } + const uint32_t off = + swizzle128_offset((r % kWgRows) * 4u + tt, lane * 4u); + ptx::st_shared( + reinterpret_cast(lin_base + off), + static_cast(c01) | (static_cast(c23) << 16)); + } + if (lane == 0) sm.lin_scale[r] = s; + } +} + +// One row's weighted head reduction seeded with its linear-term columns +// (terms 2, 3 in units of s * 2^-8). +__device__ __forceinline__ float reduce_heads_with_lin(const float* s, + const float* w, + const float* lin) { + return reduce_heads(s, w, make_float2(lin[0], lin[1]), + __fmul2_rn(make_float2(lin[2], lin[3]), + make_float2(kLinLoScale, kLinLoScale))); +} + +// Gate sample state: each math warp histograms its row's sampled scores +// relative to the row's reference `ref` (its max over the first sample tile): +// half_bin<5>(x - ref), kSampleBins bins, 32 per octave of the distance, so +// the bins near the row's top stay fine whatever the scores' common offset +// (absolute bins are ~2 wide at |x| ~ 100, too coarse for top tails that +// narrow). +constexpr uint32_t kSampleShift = 5, kSampleBins = 65536 >> kSampleShift; +struct alignas(16) SampleShared { + uint32_t hist[kBlockRows][padded_bins(kSampleBins)]; + // Ordered code of each row's max over the first sample tile (0: none). + uint32_t ref_code[kBlockRows]; + // The first tile's scores, held until the reference is known (in registers + // they cost the hot loops spills). + float first[2][kWgRows][kMathThreads]; +}; + +__device__ __forceinline__ void add_sample(uint32_t* hist, float distance) { + atomicAdd(hist + padded_bin(half_bin(distance)), 1u); +} +__device__ __forceinline__ uint32_t sample_count(const uint32_t* hist, + uint32_t bin) { + return hist[padded_bin(bin)]; +} +// The row reference from its ordered code; 0 when the row had no valid sample +// in the first tile (then the bins are absolute). +__device__ __forceinline__ float sample_ref(uint32_t code) { + const float ref = ordered_float(code); + return isfinite(ref) ? ref : 0.0f; +} + +// The gate aims kGateSlack16 / 16 above the target count, so that few rows fall +// below the top-k (and into the exact rescan) when the sampled estimate runs +// low. On the B300 captures 17/16 cuts the rescanned blocks by ~85% at about +// the candidate counts of the coarser absolute bins. +constexpr uint32_t kGateSlack16 = 17; + +// Gate: the k-th largest sample (k = count * target / visible, count = the +// histogram total) of one row from its histogram; one warp per row. -inf when +// the row sees at most `target` keys or has no samples. +__device__ __noinline__ inline void finish_sample_histogram( + const uint32_t* hist, float ref, float* gate, uint32_t visible, + uint32_t target, uint32_t lane) { + if (visible <= target) { + if (lane == 0) *gate = -CUDART_INF_F; + __syncwarp(); + return; + } + // Lane l owns kChunksPerLane 32-bin chunks, descending with the lane: chunk + // kChunks - 1 - kChunksPerLane * l - j for j < kChunksPerLane. + constexpr uint32_t kChunks = kSampleBins / 32, kChunksPerLane = kChunks / 32; + uint32_t sum = 0; +#pragma unroll + for (uint32_t j = 0; j < kChunksPerLane; ++j) { + const uint32_t c = kChunks - 1 - kChunksPerLane * lane - j; +#pragma unroll 8 + for (uint32_t b = 0; b < 32; ++b) sum += hist[padded_bin(c * 32 + b)]; + } + const uint32_t incl = warp_inclusive_scan(sum, lane); + const uint32_t tot = __shfl_sync(kFullMask, incl, 31); + if (tot == 0) { + if (lane == 0) *gate = -CUDART_INF_F; + __syncwarp(); + return; + } + const uint32_t rank = max( + 1u, min(tot, (tot * target * kGateSlack16 / 16 + visible - 1) / visible)); + const uint32_t w = __ffs(__ballot_sync(kFullMask, incl >= rank)) - 1; + uint32_t above = __shfl_sync(kFullMask, incl - sum, w), chosen = 0; +#pragma unroll 1 + for (uint32_t cc = 0; cc < kChunksPerLane; ++cc) { + const uint32_t c = kChunks - 1 - kChunksPerLane * w - cc; + // Lane 0 = highest bin of the chunk. + const uint32_t v = + warp_inclusive_scan(sample_count(hist, c * 32 + 31 - lane), lane); + const uint32_t mask = __ballot_sync(kFullMask, above + v >= rank); + if (mask) { + chosen = c * 32 + 31 - (__ffs(mask) - 1); + break; + } + above += __shfl_sync(kFullMask, v, 31); + } + if (lane == 0) *gate = ref + half_bin_floor(chosen); + __syncwarp(); +} + +// Candidate records each lane of a math warp buffers per row and handoff +// interval before it falls back to direct global stores. +constexpr uint32_t kRingSlots = 8; +// The drain's prefix sum packs two 16-bit per-row counts into one word. +static_assert(32 * kRingSlots < 65536, + "per-warp record count must fit 16 bits"); + +// Handoff ring: [bank][math warp][row][slot][lane] records, per-lane record +// counts and the interval's first scan tile. +struct alignas(16) HandoffRing { + uint32_t records[2][kMathWarps][kWgRows][kRingSlots][32]; + uint32_t num_records[2][kMathWarps][kWgRows][32]; + uint32_t bases[2][kMathWarps]; +}; + +// samples: gate sample tiles of the block (0 when no row sees more keys than +// the gate target, so every gate is -inf). +struct ClaimHeader { + uint32_t vblock, all_short, kv_base, splits, slot, samples; +}; +struct ClaimEntry : ClaimHeader { + uint32_t starts[kBlockRows], ends[kBlockRows]; +}; + +// Claim k in a producer role (warps 9-11): waits for the entry, copies its +// header and releases the entry (every lane arrives). False once the claimer +// ends the epoch (kTerm / kPause). +__device__ __forceinline__ bool consume_claim(uint32_t k, TBar* claim_full, + TBar* claim_empty, + const ClaimEntry* entries, + ClaimHeader& claim) { + claim_full[k & 1].wait((k >> 1) & 1); + claim = entries[k & 1]; + __syncwarp(); + if (claim.vblock >= kPause) return false; + claim_empty[k & 1].arrive(); + return true; +} + +// Barrier words (static shared, never aliased with data). +struct BarIdx { + // TMEM slots, shared by the two math warpgroups (WG0 even, WG1 odd + // positions). + static constexpr uint32_t kTmemSlots = 3; + static constexpr uint32_t full_q = 0, empty_q = 1, full_kv = 2, + empty_kv = 2 + kKVStages, + full_tmem = 2 + 2 * kKVStages, + empty_tmem = full_tmem + kTmemSlots, + claim_full = empty_tmem + kTmemSlots, + claim_empty = claim_full + 2, + sel_done = claim_empty + 2, h_full = sel_done + 1, + h_empty = h_full + 2 * kMathWarps, + full_lin = h_empty + 2 * kMathWarps, + total = full_lin + 1; +}; + +// Expect `bytes` of TMA transactions and arrive `count` times on one barrier. +__device__ __forceinline__ void mbar_arrive_expect_tx(const void* b, + uint32_t bytes, + uint32_t count) { + const uint32_t a = static_cast(__cvta_generic_to_shared(b)); + asm volatile( + "mbarrier.expect_tx.relaxed.cta.shared::cta.b64 [%0], %1;" ::"r"(a), + "r"(bytes) + : "memory"); + asm volatile( + "mbarrier.arrive.release.cta.shared::cta.b64 _, [%0], %1;" ::"r"(a), + "r"(count) + : "memory"); +} + +// Candidate slabs: a block's 8 rows live in its CTA slot's 8 slab rows, so row +// r maps to slab row r + slot_row_delta(slot, row_base). +__device__ __forceinline__ int64_t slot_row_delta(uint32_t slot, + uint32_t row_base) { + return int64_t(slot) * kBlockRows - int64_t(row_base); +} +// Offset of entry `index` of row `row` in the candidate slabs. +__device__ __forceinline__ uint64_t candidate_offset(uint32_t row, + int64_t row_delta, + uint32_t index) { + return static_cast(int64_t(row) + row_delta) * kCandidatesPerRow + + index; +} +// Stores ring record `record` (fp24 code << 8 | tile offset * 2 + KV half, +// tile offset relative to the interval's first tile `interval_base`) of KV +// lane `kv_lane` as entry `index` of row `row`. +__device__ __forceinline__ void store_ring_record( + uint16_t* vals, int32_t* indices, uint32_t row, int64_t row_delta, + uint32_t index, uint32_t record, uint32_t kv_base, uint32_t interval_base, + uint32_t kv_lane) { + const uint32_t key = + kv_base + (interval_base * 2 + (record & 255u)) * 128 + kv_lane; + const uint64_t off = candidate_offset(row, row_delta, index); + store_candidate_record(vals + off, indices + off, record >> 8, key); +} + +// Output row `out` of a row that sees at most top-k keys: its `visible` keys +// in order (0 .. visible - 1, relative to the row start), then -1 padding. +// Threads t, t + stride, ... write the slots. +__device__ __forceinline__ void write_all_keys(int* out, uint32_t topk, + uint32_t visible, uint32_t t, + uint32_t stride) { + for (uint32_t j = t; j < topk; j += stride) + out[j] = j < visible ? int(j) : -1; +} + +// Rewrites the output rows of the block at row_base in place from absolute key +// indices to indices relative to the row's start (the top_k_per_row_prefill +// convention; -1 padding stays). Rows that start at 0 and rows for which +// skip(local row) holds are left as they are. Threads t, t + stride, ... each +// rewrite 4 slots. +template +__device__ __forceinline__ void make_relative(int* output, uint32_t topk, + uint32_t rows, uint32_t row_base, + const uint32_t* starts_g, + uint32_t t, uint32_t stride, + Skip skip) { + const uint32_t quads = topk / 4; + for (uint32_t i = t; i < kBlockRows * quads; i += stride) { + const uint32_t r = row_base + i / quads; + if (r >= rows || skip(i / quads)) continue; + const int s = int(starts_g[r]); + if (s == 0) continue; + int4* p = reinterpret_cast(output + (uint64_t)r * topk) + i % quads; + int4 v = *p; + v.x = v.x < 0 ? -1 : v.x - s; + v.y = v.y < 0 ? -1 : v.y - s; + v.z = v.z < 0 ? -1 : v.z - s; + v.w = v.w < 0 ? -1 : v.w - s; + *p = v; + } +} + +// Stores one warp's ring records of a handoff interval in its rows' candidate +// slabs. recs = [4 rows][kRingSlots records][32 lanes]; lane l holds n[row] +// records of each row. Each row's slots are reserved with one atomic on +// row_counts[row_base + row]; records past kCandidatesPerRow are dropped (the +// row's count then exceeds the cap, so its block is rescanned exactly). +// store(row, slot, record) writes one record. +// Written for a warpgroup's 4 rows (two 16-bit counts per word in the scan). +static_assert(kWgRows == 4, "store_ring_records packs 4 row counts"); +template +__device__ __forceinline__ void store_ring_records(const uint32_t* recs, + const uint32_t (&n)[4], + int* row_counts, + uint32_t row_base, + Store store) { + const uint32_t lane = threadIdx.x & 31; + uint32_t my_total = 0; +#pragma unroll + for (uint32_t row = 0; row < 4; ++row) { + const uint32_t total = __reduce_add_sync(kFullMask, n[row]); + if (lane == row) my_total = total; + } + uint32_t my_start = 0; + if (lane < 4 && my_total) + my_start = atomicAdd(row_counts + row_base + lane, my_total); + // Two 16-bit per-row counts per word: one scan covers two rows. + uint32_t p01 = n[0] | (n[1] << 16), p23 = n[2] | (n[3] << 16); +#pragma unroll + for (int dd = 1; dd < 32; dd *= 2) { + const uint32_t a = __shfl_up_sync(kFullMask, p01, dd), + b = __shfl_up_sync(kFullMask, p23, dd); + if (lane >= dd) { + p01 += a; + p23 += b; + } + } + const uint32_t prefix[4] = {p01 & 65535u, p01 >> 16, p23 & 65535u, p23 >> 16}; +#pragma unroll + for (uint32_t row = 0; row < 4; ++row) { + const uint32_t start = __shfl_sync(kFullMask, my_start, row); + for (uint32_t j = 0; j < n[row]; ++j) { + const uint32_t slot = start + prefix[row] - n[row] + j; + const uint32_t record = recs[(row * kRingSlots + j) * 32 + lane]; + if (slot < kCandidatesPerRow) store(row_base + row, slot, record); + } + } +} + +// Self-drain, out of line: stores this warp's ring records of the last +// handoff interval. recs = records[bank][warp]. +__device__ __noinline__ inline void self_drain_last( + const uint32_t* __restrict__ recs, uint32_t n0, uint32_t n1, uint32_t n2, + uint32_t n3, int* __restrict__ row_counts, uint32_t rbase, uint32_t lane_kv, + uint32_t flush_base, int64_t row_delta, uint16_t* __restrict__ vals, + int32_t* __restrict__ indices) { + const uint32_t n[4] = {n0, n1, n2, n3}; + store_ring_records(recs, n, row_counts, rbase, + [&](uint32_t row, uint32_t slot, uint32_t record) { + store_ring_record(vals, indices, row, row_delta, slot, + record, lane_kv, flush_base, 0); + }); + __syncwarp(); +} + +// Drain warp: waits for producer warp `producer`'s handoff interval, stores its +// records (unless the producer self-drained it) and frees the ring bank. +template +__device__ __forceinline__ void drain_handoff( + HandoffRing& d, TBar* hfull, TBar* hempty, int producer, uint32_t interval, + uint32_t row_base, int* row_counts, bool skip, Store store) { + const int lane = threadIdx.x & 31, bank = interval & 1; + hfull[bank * kMathWarps + producer].wait((interval >> 1) & 1); + __syncwarp(); + if (skip) { // self-drained by the producer + hempty[bank * kMathWarps + producer].arrive(); + return; + } + const uint32_t base = d.bases[bank][producer]; + uint32_t n[4]; +#pragma unroll + for (uint32_t row = 0; row < 4; ++row) + n[row] = d.num_records[bank][producer][row][lane]; + store_ring_records(&d.records[bank][producer][0][0][0], n, row_counts, + row_base, + [&](uint32_t row, uint32_t slot, uint32_t record) { + store(row, slot, record, base, producer * 32 + lane); + }); + __syncwarp(); + hempty[bank * kMathWarps + producer].arrive(); +} + +// Interval: scan tiles per candidate handoff to the drain warps. +template +__global__ +__launch_bounds__(kNumThreads, 1) void fused_mqa_topk_fp8_prefill_kernel( + uint32_t rows, uint32_t keys, const uint32_t* starts_g, + const uint32_t* ends_g, int* out, uint16_t* vals, int* indices, + int* row_counts, RescanBuffers rescan, uint32_t phase_bytes, + uint32_t sample_target, uint32_t sample_stride, uint32_t sample_groups, + uint32_t sample_tiles, const float* scales, + const __grid_constant__ cute::TmaDescriptor tks, + const __grid_constant__ cute::TmaDescriptor tq, + const __grid_constant__ cute::TmaDescriptor ts, + const __grid_constant__ cute::TmaDescriptor tk, + const __grid_constant__ cute::TmaDescriptor tw, uint32_t nblocks, + uint32_t* counter, int* defer_next) { + // Compile-time: a runtime top-k costs ~0.5% in the scan (codegen). + constexpr uint32_t topk = TopK; + static_assert(BarIdx::kTmemSlots * kSlotCols <= 512, + "TMEM slots exceed 512 columns"); + // A ring record's low byte holds (tile - interval start) * 2 + half, and the + // handoff points use `& (Interval - 1)`. + static_assert((Interval & (Interval - 1)) == 0 && Interval <= 128, + "Interval must be a power of two <= 128"); + extern __shared__ __align__(1024) uint8_t smem_buffer[]; + auto& sm = *reinterpret_cast(smem_buffer); + uint8_t* const region = smem_buffer + sizeof(KernelSmem); + auto& sample_smem = *reinterpret_cast(region); + auto& hdata = *reinterpret_cast(region); + + __shared__ alignas(8) uint64_t bar_words[BarIdx::total]; + __shared__ uint32_t tmem_ptr; + __shared__ ClaimEntry entries[2]; + __shared__ float gate_s[kBlockRows]; + __shared__ uint32_t sel_recover, sel_staged, sel_short, + row_offset[kBlockRows + 1]; + __shared__ uint32_t defer_head, unsafe_rows, pause_req, epoch_kind; + + TBar* const bars = reinterpret_cast(bar_words); + TBar& full_q = bars[BarIdx::full_q]; + TBar& empty_q = bars[BarIdx::empty_q]; + TBar* const full_kv = bars + BarIdx::full_kv; + // The 32 producer lanes stage the sample scales, so each arrives on full_kv. + constexpr uint32_t kFullKvArrivals = 32; + TBar* const empty_kv = bars + BarIdx::empty_kv; + TBar* const full_tmem = bars + BarIdx::full_tmem; + TBar* const empty_tmem = bars + BarIdx::empty_tmem; + TBar* const claim_full = bars + BarIdx::claim_full; + TBar* const claim_empty = bars + BarIdx::claim_empty; + TBar& sel_done = bars[BarIdx::sel_done]; + TBar* const h_full = bars + BarIdx::h_full; + TBar* const h_empty = bars + BarIdx::h_empty; + TBar* const full_lin = bars + BarIdx::full_lin; + + const uint32_t warp_idx = cutlass::canonical_warp_idx_sync(); + const uint32_t lane = ptx::get_lane_idx(); + constexpr uint32_t kClaimConsumers = 11; // warps 9, 10, 11, 0-7 + + if (warp_idx == 8 && lane == 0) { + cute::prefetch_tma_descriptor(&tq); + cute::prefetch_tma_descriptor(&tw); + cute::prefetch_tma_descriptor(&tk); + cute::prefetch_tma_descriptor(&ts); + cute::prefetch_tma_descriptor(&tks); + } + if (warp_idx == 9 && cute::elect_one_sync()) { + defer_head = kTerm; + pause_req = 0; + } + // Epochs: the persistent role loops run until the work runs out (kTerm) or a + // block needs the exact rescan (kPause); rare blocks are then rescanned + // exactly (rescan.cuh) and the loops resume with freshly initialised + // barriers. The first epoch is emitted outside any loop (nesting the role + // loops in an epoch loop costs ~1.5 percent in the scan: uniform address math + // is rematerialised every tile). Later epochs only run after a recovery + // pause. + __shared__ uint32_t row_bounds[2][kBlockRows]; + bool more = false; + { + constexpr uint32_t epoch = 0; +#include "persistent_epoch.inc" + } +#pragma unroll 1 + for (uint32_t epoch = 1; more; ++epoch) { +#include "persistent_epoch.inc" + } + // counter = {next block, finished CTAs}: the last CTA to finish (every claim + // is done by then) zeroes both, so the caller's workspace is ready for the + // next launch / graph replay. + if (threadIdx.x == 0) { + __threadfence(); + if (atomicAdd(counter + 1, 1u) == gridDim.x - 1) { + // Acquire: every other CTA's last claim on counter happens before the + // reset below. + __threadfence(); + atomicExch(counter, 0u); + atomicExch(counter + 1, 0u); + } + } +} + +} // namespace fused_mqa_topk::persist diff --git a/csrc/fused_mqa_topk/rescan.cuh b/csrc/fused_mqa_topk/rescan.cuh new file mode 100644 index 000000000000..80d9dc57370f --- /dev/null +++ b/csrc/fused_mqa_topk/rescan.cuh @@ -0,0 +1,350 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright contributors to the vLLM project +// +// Exact recovery for the blocks the persistent kernel cannot select from its +// gated candidates (see persistent_kernel.cuh): the block is rescanned densely, +// and every 1024-key segment of fp32 scores is merged into a running per-row +// top set (kept <= kRetainedPerRow, compacted to K). +#pragma once +#include +#include +#include +#include +#include + +#include "common.cuh" +#include "constants.h" +#include "dense_scan.cuh" + +namespace fused_mqa_topk::rescan { +using dense::Scheduler; + +// Keys whose scores are merged into the retained sets at a time. +constexpr uint32_t kSegmentKeys = 1024; +// Per-row retained top set of the exact rescan (scores and key indices). +struct RescanBuffers { + float* retained; + int* retained_indices; +}; +// Per-row segment histograms: half_bin<6> of a score (1024 bins); the radix +// levels of compact use 256 bins. +constexpr uint32_t kSegmentBins = 1024, kRadixBins = 256; +struct alignas(16) RescanShared { + float ring[2][kBlockRows][kSegmentKeys]; + uint32_t hist[kBlockRows][padded_bins(kSegmentBins)], gates[kBlockRows], + kept[kBlockRows]; + uint32_t work[padded_bins(kRadixBins)], prefix, rank, emit_counts[kMathWarps]; + __device__ void init(uint32_t tid) { + for (uint32_t j = tid; j < sizeof(hist) / sizeof(uint32_t); + j += kNumThreads) + reinterpret_cast(hist)[j] = 0; + if (tid < kBlockRows) { + gates[tid] = __float_as_uint(-CUDART_INF_F); + kept[tid] = 0; + } + } +}; +// What compact_retained's emit pass keeps. +enum class Emit { kAboveGate, kAboveCode, kTies }; + +__device__ inline void compact_retained(RescanShared& shared, + RescanBuffers buffers, uint32_t topk, + uint32_t local, uint32_t row, + uint32_t tid, + bool compact_to_k = false) { + constexpr uint32_t kThreads = kMathThreads, + kItems = kRetainedPerRow / kThreads; + uint32_t lane = tid & 31, count = shared.kept[local]; + uint64_t base = (uint64_t)row * kRetainedPerRow; + float values[kItems]; + int indices[kItems]; +#pragma unroll + for (uint32_t i = 0; i < kItems; ++i) { + uint32_t j = tid + i * kThreads; + values[i] = j < count ? buffers.retained[base + j] : -CUDART_INF_F; + indices[i] = j < count ? buffers.retained_indices[base + j] : -1; + } + // Emits the values >= gate, the codes above threshold, or the codes equal + // to it (ties, up to K) to the retained set from base_offset on. + auto emit = [&](float gate, uint32_t threshold, Emit mode, + uint32_t base_offset) { + const auto passes = [&](float v) { + return isfinite(v) && + (mode == Emit::kAboveGate + ? v >= gate + : (mode == Emit::kAboveCode ? ordered_code(v) > threshold + : ordered_code(v) == threshold)); + }; + uint32_t total = 0; +#pragma unroll + for (uint32_t i = 0; i < kItems; ++i) total += passes(values[i]); + uint32_t warp_total = __reduce_add_sync(kFullMask, total); + if (lane == 0) shared.emit_counts[tid / 32] = warp_total; + math_wg_sync(); + uint32_t start = base_offset; + for (uint32_t w = 0; w < tid / 32; ++w) start += shared.emit_counts[w]; + uint32_t all = 0; + for (uint32_t w = 0; w < kMathWarps; ++w) all += shared.emit_counts[w]; +#pragma unroll + for (uint32_t i = 0; i < kItems; ++i) { + const float v = values[i]; + const bool pass = passes(v); + uint32_t mask = __ballot_sync(kFullMask, pass), + pos = start + __popc(mask & ((1u << lane) - 1)); + if (pass && (mode != Emit::kTies || pos < topk)) { + buffers.retained[base + pos] = v; + buffers.retained_indices[base + pos] = indices[i]; + } + start += __popc(mask); + } + math_wg_sync(); + return all; + }; + if (!compact_to_k) { + if (tid == 0) { + shared.rank = 0; + } + math_wg_sync(); + float gate = __uint_as_float(shared.gates[local]); + uint32_t kept = 0; +#pragma unroll + for (uint32_t i = 0; i < kItems; ++i) + kept += isfinite(values[i]) && values[i] >= gate; + kept = __reduce_add_sync(kFullMask, kept); + if (lane == 0) atomicAdd(&shared.rank, kept); + math_wg_sync(); + kept = shared.rank; + if (kept >= topk && kept <= kRetainedPerRow - kSegmentKeys) { + emit(gate, 0, Emit::kAboveGate, 0); + math_wg_sync(); + if (tid == 0) shared.kept[local] = kept; + math_wg_sync(); + return; + } + } + if (tid == 0) { + shared.prefix = 0; + shared.rank = topk; + } + math_wg_sync(); + for (int shift = 24; shift >= 0; shift -= 8) { + for (uint32_t j = tid; j < padded_bins(kRadixBins); j += kThreads) + shared.work[j] = 0; + math_wg_sync(); + uint32_t prefix = shared.prefix; +#pragma unroll + for (uint32_t i = 0; i < kItems; ++i) { + uint32_t code = ordered_code(values[i]); + bool valid = isfinite(values[i]) && + (shift == 24 || (code >> (shift + 8)) == prefix); + uint32_t mask = __ballot_sync(kFullMask, valid); + if (valid) { + uint32_t bin = (code >> shift) & 255u, + peers = __match_any_sync(mask, bin); + if (lane == uint32_t(__ffs(peers) - 1)) + atomicAdd(shared.work + padded_bin(bin), __popc(peers)); + } + } + math_wg_sync(); + if (tid < 32) { + uint32_t sum = 0; + for (uint32_t j = 0; j < 8; ++j) { + uint32_t bin = 255 - lane * 8 - j; + sum += shared.work[padded_bin(bin)]; + } + const uint32_t inclusive = warp_inclusive_scan(sum, lane); + uint32_t before = inclusive - sum, rank = shared.rank, + crossing = + __ballot_sync(kFullMask, before < rank && inclusive >= rank), + chosen_lane = __ffs(crossing) - 1, chosen = 0, above = before; + if (lane == chosen_lane) { + for (uint32_t j = 0; j < 8; ++j) { + uint32_t bin = 255 - lane * 8 - j; + if (above + shared.work[padded_bin(bin)] >= rank) { + chosen = bin; + break; + } + above += shared.work[padded_bin(bin)]; + } + } + chosen = __shfl_sync(kFullMask, chosen, chosen_lane); + above = __shfl_sync(kFullMask, above, chosen_lane); + __syncwarp(); // every lane read shared.rank above + if (lane == 0) { + shared.prefix = (prefix << 8) | chosen; + shared.rank = rank - above; + } + } + math_wg_sync(); + } + uint32_t threshold = shared.prefix; + uint32_t greater = emit(0, threshold, Emit::kAboveCode, 0); + emit(0, threshold, Emit::kTies, greater); + if (tid == 0) { + shared.kept[local] = topk; + float gate = + fmaxf(__uint_as_float(shared.gates[local]), ordered_float(threshold)); + atomicExch(shared.gates + local, __float_as_uint(gate)); + } + math_wg_sync(); +} + +__device__ inline void merge_segment(RescanShared& shared, + RescanBuffers buffers, uint32_t topk, + uint32_t local, uint32_t row, + uint32_t segment, uint32_t first, + uint32_t n, uint32_t lane) { + constexpr uint32_t kItems = kSegmentKeys / 32; + const uint32_t bank = segment & 1; + float values[kItems], gate = __uint_as_float(shared.gates[local]); + uint32_t count = 0; +#pragma unroll + for (uint32_t i = 0; i < kItems; ++i) { + uint32_t j = lane + i * 32; + float v = j < n ? shared.ring[bank][local][j] : -CUDART_INF_F; + if (v <= gate) v = -CUDART_INF_F; + values[i] = v; + bool valid = isfinite(v); + count += valid; + uint32_t mask = __ballot_sync(kFullMask, valid); + if (valid) { + uint32_t bin = half_bin<6>(v), peers = __match_any_sync(mask, bin); + if (lane == uint32_t(__ffs(peers) - 1)) + atomicAdd(&shared.hist[local][padded_bin(bin)], __popc(peers)); + } + } + count = __reduce_add_sync(kFullMask, count); + __syncwarp(); + if (count) { + uint32_t sum = 0; +#pragma unroll + for (uint32_t j = 0; j < 32; ++j) + sum += shared.hist[local][padded_bin((31 - lane) * 32 + j)]; + uint32_t inclusive = warp_inclusive_scan(sum, lane); + uint32_t before = inclusive - sum, + mask = + __ballot_sync(kFullMask, before < topk && inclusive >= topk); + if (mask) { + uint32_t chosen = __ffs(mask) - 1, group = 31 - chosen, + rank = topk - __shfl_sync(kFullMask, before, chosen), + bin = group * 32 + 31 - lane; + sum = shared.hist[local][padded_bin(bin)]; + inclusive = warp_inclusive_scan(sum, lane); + before = inclusive - sum; + chosen = + __ffs(__ballot_sync(kFullMask, before < rank && inclusive >= rank)) - + 1; + bin = __shfl_sync(kFullMask, bin, chosen); + gate = fmaxf(gate, half_bin_floor<6>(bin)); + if (lane == 0) atomicExch(shared.gates + local, __float_as_uint(gate)); + } + uint32_t written = shared.kept[local]; + uint64_t base = (uint64_t)row * kRetainedPerRow; +#pragma unroll + for (uint32_t i = 0; i < kItems; ++i) { + float v = values[i]; + bool pass = isfinite(v) && v >= gate; + uint32_t mask = __ballot_sync(kFullMask, pass), + pos = written + __popc(mask & ((1u << lane) - 1)); + if (pass) { + buffers.retained[base + pos] = v; + buffers.retained_indices[base + pos] = first + lane + i * 32; + } + written += __popc(mask); + } + __syncwarp(); + if (lane == 0) shared.kept[local] = written; + __syncwarp(); + } +} + +// Streams each row's scores (above its running gate) into the segment ring and +// merges every full segment into the row's retained top set; the final set is +// written to the output. +struct RescanPolicy { + static constexpr float kWeightScale = 0.5f; + static constexpr uint32_t kSplitsPerSegment = kSegmentKeys / kTileKeys; + RescanShared& shared; + RescanBuffers buffers; + int* out; // [rows, topk] + uint32_t topk, query_base, row_base, segment = 0; + uint32_t seq_k_start[kWgRows], seq_k_end[kWgRows]; + float gate[kWgRows]; + CUTLASS_DEVICE void begin_block(const Scheduler& sched, uint32_t q_block, + uint32_t base) { + query_base = base; + row_base = sched.get_logits_row(q_block, 0); +#pragma unroll + for (uint32_t i = 0; i < kWgRows; ++i) { + const uint32_t row = sched.get_logits_row(q_block, base + i); + gate[i] = -CUDART_INF_F; + seq_k_start[i] = min(sched.cu_seq_len_k_start[row], sched.num_kv_tokens); + seq_k_end[i] = min(sched.cu_seq_len_k_end[row], sched.num_kv_tokens); + } + } + CUTLASS_DEVICE void begin_split(uint32_t split) { + if (split % kSplitsPerSegment == 0) { + __syncwarp(); + for (uint32_t i = 0; i < kWgRows; ++i) + gate[i] = __uint_as_float(atomicAdd(shared.gates + query_base + i, 0u)); + } + } + CUTLASS_DEVICE void score(uint32_t i, uint32_t kv_offset, uint32_t kv_lane, + uint32_t split, float bq) { + const bool valid = kv_offset >= seq_k_start[i] && + kv_offset < seq_k_end[i] && isfinite(bq) && bq > gate[i]; + shared.ring[segment & 1][query_base + i] + [(split % kSplitsPerSegment) * kTileKeys + kv_lane] = + valid ? bq : -CUDART_INF_F; + } + CUTLASS_DEVICE void end_split(uint32_t split, uint32_t num_splits, + uint32_t kv_base) { + if ((split + 1) % kSplitsPerSegment != 0 && split + 1 != num_splits) return; + __threadfence_block(); + math_wg_sync(); + const uint32_t tid = threadIdx.x; + for (uint32_t local = 0; local < kBlockRows; ++local) + if (shared.kept[local] > kRetainedPerRow - kSegmentKeys) + compact_retained(shared, buffers, topk, local, row_base + local, tid); + math_wg_sync(); + const uint32_t local = tid / 32; + merge_segment(shared, buffers, topk, local, row_base + local, segment, + kv_base + (split / kSplitsPerSegment) * kSegmentKeys, + (split % kSplitsPerSegment + 1) * kTileKeys, tid % 32); + math_wg_sync(); + if (split + 1 == num_splits) + for (uint32_t l = 0; l < kBlockRows; ++l) + if (shared.kept[l] > topk) + compact_retained(shared, buffers, topk, l, row_base + l, tid, true); + math_wg_sync(); + ++segment; + } + CUTLASS_DEVICE void end_block(const Scheduler& sched, uint32_t q_block) { + const uint32_t local = threadIdx.x / 32, lane = threadIdx.x % 32; + const uint32_t row = sched.get_logits_row(q_block, local), + count = shared.kept[local]; + if (row < sched.num_q_tokens) + for (uint32_t j = lane; j < topk; j += 32) + out[(uint64_t)row * topk + j] = + j < count + ? buffers.retained_indices[(uint64_t)row * kRetainedPerRow + j] + : -1; + } +}; + +// Rescans one block exactly and writes its output rows of `out`; `buffers` +// rows are addressed by absolute row (callers offset them). Out of line: the +// rare rescan must not add register pressure to the caller's hot loops. +__device__ __noinline__ inline void rescan_block( + const Scheduler& block, RescanBuffers buffers, int* out, uint32_t topk, + const cute::TmaDescriptor& tq, const cute::TmaDescriptor& tk, + const cute::TmaDescriptor& ts, const cute::TmaDescriptor& tw) { + extern __shared__ __align__(1024) uint8_t smem_buffer[]; + auto& shared = *reinterpret_cast(smem_buffer + + sizeof(dense::SharedStorage)); + shared.init(threadIdx.x); + RescanPolicy policy{shared, buffers, out, topk}; + dense::score_block(block, policy, tq, tk, ts, tw); +} + +} // namespace fused_mqa_topk::rescan diff --git a/csrc/fused_mqa_topk/select.cuh b/csrc/fused_mqa_topk/select.cuh new file mode 100644 index 000000000000..759ec7fce46a --- /dev/null +++ b/csrc/fused_mqa_topk/select.cuh @@ -0,0 +1,452 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright contributors to the vLLM project +// +// Per-row top-K selection of the persistent kernel's candidates, one warp per +// row (K = topk, the row's top-k: 512, 1024 or 2048, a runtime value). +// +// select_staged_row: bracket the K-th largest fp24 code with sampled codes, +// place the codes above the bracket directly, and resolve the bracket with +// histogram levels on register-resident codes. +// +// select_row: the fallback of select_staged_row when its sampled bracket +// misses. Candidate codes (higher code = higher score, staged once in shared +// memory) span a narrow range (gate .. row max), so each level histograms the +// range-relative code into at most kBins bins: collisions are rare, plain +// shared atomics suffice and each lane keeps kUnroll independent elements in +// flight. Levels repeat inside the boundary bin until it is one code wide; +// codes above it are emitted, then ties until K. +#pragma once +#include + +#include "candidates.cuh" +#include "common.cuh" +#include "constants.h" + +namespace fused_mqa_topk::selection { +constexpr uint32_t kBinsLog2 = 10, kBins = 1u << kBinsLog2; +// Elements each lane keeps in flight. +constexpr uint32_t kUnroll = 8; + +// Bin (descending) where the cumulative count first reaches `rank`; +// `above` = elements in higher bins. Lane l owns Bins / 32 bins, descending +// from bin Bins - 1 - l * Bins / 32. +template +__device__ __forceinline__ uint32_t find_bin_n(const uint32_t* hist, + uint32_t rank, uint32_t& above) { + const uint32_t lane = threadIdx.x & 31; + constexpr uint32_t kPerLane = Bins / 32; + uint32_t sum = 0; +#pragma unroll + for (uint32_t j = 0; j < kPerLane; ++j) + sum += hist[Bins - 1 - lane * kPerLane - j]; + const uint32_t inclusive = warp_inclusive_scan(sum, lane); + const uint32_t before = inclusive - sum; + const uint32_t winner = + __ffs(__ballot_sync(kFullMask, before < rank && inclusive >= rank)) - 1; + uint32_t chosen = 0, acc = before; + if (lane == winner) { + for (uint32_t j = 0; j < kPerLane; ++j) { + const uint32_t bin = Bins - 1 - lane * kPerLane - j, h = hist[bin]; + if (acc + h >= rank) { + chosen = bin; + break; + } + acc += h; + } + } + above = __shfl_sync(kFullMask, acc, winner); + return __shfl_sync(kFullMask, chosen, winner); +} + +// Requires count >= topk (callers recover rows with fewer candidates). +__device__ inline void select_row(uint32_t* codes, uint32_t count, + const uint16_t* values, const int32_t* packed, + int32_t* out, uint32_t topk, uint32_t* hist) { + const uint32_t lane = threadIdx.x & 31; + uint32_t lo = 0xffffffffu, hi = 0; + uint32_t j = lane; + for (; j + 32 * (kUnroll - 1) < count; j += 32 * kUnroll) { + uint16_t v[kUnroll]; + int32_t p[kUnroll]; +#pragma unroll + for (uint32_t u = 0; u < kUnroll; ++u) { + v[u] = values[j + 32 * u]; + p[u] = packed[j + 32 * u]; + } +#pragma unroll + for (uint32_t u = 0; u < kUnroll; ++u) { + const uint32_t c = candidate_load_score_code(v[u], p[u]); + codes[j + 32 * u] = c; + lo = min(lo, c); + hi = max(hi, c); + } + } + for (; j < count; j += 32) { + const uint32_t c = candidate_load_score_code(values[j], packed[j]); + codes[j] = c; + lo = min(lo, c); + hi = max(hi, c); + } + lo = __reduce_min_sync(kFullMask, lo); + hi = __reduce_max_sync(kFullMask, hi); + __syncwarp(); + + // Current bucket: codes in [lo, lo + width). rank = elements still needed + // from it. + uint32_t width = hi - lo + 1, rank = topk; +#pragma unroll 1 + while (width > 1) { + const uint32_t spread = (width - 1) >> kBinsLog2; + const uint32_t shift = spread ? 32 - __clz(spread) : 0; + for (uint32_t b = lane; b < kBins; b += 32) hist[b] = 0; + __syncwarp(); + const uint32_t top = lo + width - 1; + uint32_t i = lane; + for (; i + 32 * (kUnroll - 1) < count; i += 32 * kUnroll) { + uint32_t c[kUnroll]; +#pragma unroll + for (uint32_t u = 0; u < kUnroll; ++u) c[u] = codes[i + 32 * u]; +#pragma unroll + for (uint32_t u = 0; u < kUnroll; ++u) + if (c[u] >= lo && c[u] <= top) + atomicAdd(hist + ((c[u] - lo) >> shift), 1u); + } + for (; i < count; i += 32) { + const uint32_t c = codes[i]; + if (c >= lo && c <= top) atomicAdd(hist + ((c - lo) >> shift), 1u); + } + __syncwarp(); + uint32_t above; + const uint32_t chosen = find_bin_n(hist, rank, above); + rank -= above; + lo += chosen << shift; + width = min(1u << shift, top - lo + 1); + __syncwarp(); + } + + // lo is now the K-th largest code: emit codes above it, then ties until K. + const uint32_t threshold = lo, lt = (1u << lane) - 1u; + uint32_t pos = 0; +#pragma unroll 1 + for (uint32_t tie = 0; tie < 2; ++tie) { + for (uint32_t j0 = 0; j0 < count && pos < topk; j0 += 32 * kUnroll) { + bool pass[kUnroll]; + int32_t p[kUnroll]; +#pragma unroll + for (uint32_t u = 0; u < kUnroll; ++u) { + const uint32_t e = j0 + 32 * u + lane; + const uint32_t c = e < count ? codes[e] : 0; + pass[u] = e < count && (tie ? c == threshold : c > threshold); + p[u] = pass[u] ? packed[e] : 0; + } +#pragma unroll + for (uint32_t u = 0; u < kUnroll; ++u) { + const uint32_t mask = __ballot_sync(kFullMask, pass[u]), + at = pos + __popc(mask & lt); + if (pass[u] && at < topk) out[at] = candidate_decode_index(p[u]); + pos += __popc(mask); + } + } + } +} + +// Register samples per lane for the sampled bracket of select_staged_row. +constexpr uint32_t kSamplesPerLane = 16; +// Largest bracket (elements between the two sampled bounds) that +// select_staged_row resolves in registers; wider brackets fall back to +// select_row. +constexpr uint32_t kMidCap = 1024; +// Bins of the histogram levels that resolve the exact threshold inside a +// bracket (select_staged_row). +constexpr uint32_t kMidBins = 512; + +// Bracket element ids are stored as uint16 (midbuf). +static_assert(kCandidatesPerRow <= 65536, "candidate ids must fit 16 bits"); +// Per-warp scratch of select_staged_row is kBins words: radix_bracket's 768, +// or the midbuf (kMidCap uint16) plus the kMidBins-bin histogram. +static_assert(768 <= kBins && kMidCap / 2 + kMidBins <= kBins, + "selection scratch exceeds kBins words"); + +// r-th largest (1-based) of the kNumSamples * 32 register samples (kNumSamples +// per lane) for two ranks at once +// (r_hi <= r_lo), via a 256-bin hi8 level and two 256-bin lo8 levels. h8: 768 +// words of scratch. +template +__device__ __forceinline__ void radix_bracket( + const uint32_t (&smp)[kNumSamples], uint32_t r_hi, uint32_t r_lo, + uint32_t* h8, uint32_t& h_hi, uint32_t& h_lo) { + const uint32_t lane = threadIdx.x & 31; +#pragma unroll + for (uint32_t b = lane; b < 192; b += 32) + reinterpret_cast(h8)[b] = make_uint4(0, 0, 0, 0); + __syncwarp(); +#pragma unroll + for (uint32_t u = 0; u < kNumSamples; ++u) atomicAdd(h8 + (smp[u] >> 8), 1u); + __syncwarp(); + uint32_t ab_hi, ab_lo; + const uint32_t b_hi = find_bin_n<256>(h8, r_hi, ab_hi); + const uint32_t b_lo = find_bin_n<256>(h8, r_lo, ab_lo); + uint32_t* const lh = h8 + 256; + uint32_t* const ll = h8 + 512; +#pragma unroll + for (uint32_t u = 0; u < kNumSamples; ++u) { + const uint32_t b = smp[u] >> 8; + if (b == b_hi) atomicAdd(lh + (smp[u] & 255u), 1u); + if (b == b_lo) atomicAdd(ll + (smp[u] & 255u), 1u); + } + __syncwarp(); + uint32_t d0, d1; + h_hi = b_hi << 8 | find_bin_n<256>(lh, r_hi - ab_hi, d0); + h_lo = b_lo << 8 | find_bin_n<256>(ll, r_lo - ab_lo, d1); + __syncwarp(); // h8 is reused as the output stage +} + +// Brackets the topk-th largest of the `count` staged codes between the hi16 +// codes of sample ranks expect -+ margin, where the kPerLane * 32 samples sit +// at evenly spaced quantiles and expect is the topk-th largest's expected rank +// among them. +template +__device__ __forceinline__ void sampled_bracket(const uint32_t* codes, + uint32_t count, uint32_t topk, + uint32_t margin, uint32_t* hist, + uint32_t& h_hi, + uint32_t& h_lo) { + constexpr uint32_t kSamples = kPerLane * 32; + const uint32_t lane = threadIdx.x & 31; + uint32_t smp[kPerLane]; +#pragma unroll + for (uint32_t u = 0; u < kPerLane; ++u) + smp[u] = codes[((u * 32 + lane) * 2 + 1) * count / (2 * kSamples)] >> 8; + const uint32_t expect = (topk * kSamples + count / 2) / count; + const uint32_t r_hi = expect > margin ? expect - margin : 1, + r_lo = min(expect + margin, kSamples); + radix_bracket(smp, r_hi, r_lo, hist, h_hi, h_lo); +} + +// find_bin_n with each lane's kPer contiguous bins loaded as 16-byte vectors +// and searched in registers (needs a 16-byte aligned histogram and kPer % 4 == +// 0; find_bin_n reads the bins one at a time and has neither requirement). +template +__device__ __forceinline__ uint32_t find_bin_v(const uint32_t* hist, + uint32_t rank, uint32_t& above) { + const uint32_t lane = threadIdx.x & 31; + constexpr uint32_t kPer = Bins / 32, kVec = kPer / 4; + static_assert(kPer % 4 == 0); + // lane l owns bins [Bins - (l + 1) * kPer, Bins - l * kPer), highest first. + const uint4* hv = + reinterpret_cast(hist + Bins - (lane + 1) * kPer); + uint32_t h[kPer]; +#pragma unroll + for (uint32_t v = 0; v < kVec; ++v) { + const uint4 x = hv[v]; + h[4 * v] = x.x; + h[4 * v + 1] = x.y; + h[4 * v + 2] = x.z; + h[4 * v + 3] = x.w; + } + uint32_t sum = 0; +#pragma unroll + for (uint32_t j = 0; j < kPer; ++j) sum += h[j]; + const uint32_t inclusive = warp_inclusive_scan(sum, lane); + const uint32_t before = inclusive - sum; + const uint32_t winner = + __ffs(__ballot_sync(kFullMask, before < rank && inclusive >= rank)) - 1; + // Descending walk inside the lane: bin index Bins - lane * kPer - 1 - j <-> + // h[kPer - 1 - j]. + uint32_t acc = before, chosen = 0, found = 0, acc_at = before; +#pragma unroll + for (uint32_t j = 0; j < kPer; ++j) { + const uint32_t x = h[kPer - 1 - j]; + const bool hit = !found && acc + x >= rank; + if (hit) { + chosen = Bins - lane * kPer - 1 - j; + acc_at = acc; + found = 1; + } + acc += x; + } + above = __shfl_sync(kFullMask, acc_at, winner); + return __shfl_sync(kFullMask, chosen, winner); +} + +// One row's global-memory candidates, staged to shared memory as +// fp24 codes (codes must be 16-byte aligned, i.e. row offsets rounded up to 4). +// Register budget: ptxas compiles the whole kernel to 168 regs, so every pass +// keeps <= ~32 regs of arrays live (more spills into the scan loop). +// Passes: stage (16-byte global loads of values / packed indices, two chunks of +// 8 in flight per lane, to fp24 codes); hi16 bracket (radix levels over 512 +// shared-memory samples); count above / inside the bracket (16-byte shared +// loads); collect (codes above the bracket to out, bracket element ids to +// midbuf); histogram levels on register codes; emit. +// Returns false before writing out; the caller's select_row then restages and +// runs the levels. +__device__ __noinline__ inline bool select_staged_row( + uint32_t* __restrict__ codes, uint32_t count, + const uint16_t* __restrict__ values, const int32_t* __restrict__ packed, + int32_t* __restrict__ out, uint32_t topk, uint32_t* hist) { + const uint32_t lane = threadIdx.x & 31; + if (count <= topk) return false; + const uint32_t chunks = (count + 7) / 8; + constexpr uint32_t kChunksInFlight = 2; + const uint4* v4p = reinterpret_cast(values); + const uint4* p4p = reinterpret_cast(packed); +#pragma unroll 1 + for (uint32_t c0i = lane; c0i < chunks; c0i += 32 * kChunksInFlight) { + uint4 vv[kChunksInFlight], pp0[kChunksInFlight], pp1[kChunksInFlight]; +#pragma unroll + for (uint32_t u = 0; u < kChunksInFlight; ++u) { + const uint32_t c = min(c0i + 32 * u, chunks - 1); + vv[u] = __ldcg(v4p + c); + pp0[u] = __ldcg(p4p + 2 * c); + pp1[u] = __ldcg(p4p + 2 * c + 1); + } +#pragma unroll + for (uint32_t u = 0; u < kChunksInFlight; ++u) { + const uint32_t c = c0i + 32 * u; + if (c < chunks) { + const uint32_t vw[4] = {vv[u].x, vv[u].y, vv[u].z, vv[u].w}; + const uint32_t pw[8] = {pp0[u].x, pp0[u].y, pp0[u].z, pp0[u].w, + pp1[u].x, pp1[u].y, pp1[u].z, pp1[u].w}; + uint32_t cw[8]; +#pragma unroll + for (uint32_t k = 0; k < 8; ++k) + cw[k] = candidate_load_score_code( + uint16_t(vw[k / 2] >> (16 * (k & 1))), int32_t(pw[k])); + if (8 * c + 8 <= count) { + reinterpret_cast(codes)[2 * c] = + make_uint4(cw[0], cw[1], cw[2], cw[3]); + reinterpret_cast(codes)[2 * c + 1] = + make_uint4(cw[4], cw[5], cw[6], cw[7]); + } else { +#pragma unroll + for (uint32_t k = 0; k < 8; ++k) + if (8 * c + k < count) codes[8 * c + k] = cw[k]; + } + } + } + } + __syncwarp(); + // The bracket is the code range between the sampled ranks expect -+ margin + // (expect = the K-th largest's expected rank among the samples). Rows up to + // kFewCandidates use 512 samples, +-40 ranks. Long-context rows (4-6K + // candidates) would get a bracket of ~16% of the row that way, over kMidCap + // for 10-20% of the rows at 512K-1M: 1024 samples, +-48 (~3 sd), give ~9%. + constexpr uint32_t kFewCandidates = 3584, kMargin = 40, kWideMargin = 48; + uint32_t h_hi, h_lo; + if (count <= kFewCandidates) + sampled_bracket(codes, count, topk, kMargin, hist, h_hi, + h_lo); + else + sampled_bracket<2 * kSamplesPerLane>(codes, count, topk, kWideMargin, hist, + h_hi, h_lo); + const uint4* c4p = reinterpret_cast(codes); + const uint32_t quads = (count + 3) / 4; + const uint32_t lt = (1u << lane) - 1u; + uint16_t* midbuf = reinterpret_cast(hist); + uint32_t* h512 = hist + kMidCap / 2; + // Collect with warp-ballot placement: passing lanes of one step write + // consecutive slots, so the global stores of the above elements are + // coalesced. Writes are bounded; the + // bracket check follows (out may be partially written on failure: the + // fallback select_row rewrites all K slots). + uint32_t above = 0, mid = 0; + constexpr uint32_t kQuadsInFlight = 4; +#pragma unroll 1 + for (uint32_t q0 = 0; q0 < quads; q0 += 32 * kQuadsInFlight) { + uint4 pp[kQuadsInFlight], cc[kQuadsInFlight]; +#pragma unroll + for (uint32_t u = 0; u < kQuadsInFlight; ++u) { + const uint32_t q = min(q0 + 32 * u + lane, quads - 1); + pp[u] = __ldcg(p4p + q); + cc[u] = c4p[q]; + } +#pragma unroll + for (uint32_t u = 0; u < kQuadsInFlight; ++u) { + const uint32_t q = q0 + 32 * u + lane; + const uint32_t w[4] = {cc[u].x, cc[u].y, cc[u].z, cc[u].w}; + const uint32_t p[4] = {pp[u].x, pp[u].y, pp[u].z, pp[u].w}; +#pragma unroll + for (uint32_t k = 0; k < 4; ++k) { + const uint32_t e = 4 * q + k, h = w[k] >> 8; + const bool valid = e < count; + const bool a = valid && h > h_hi, m = valid && !a && h >= h_lo; + const uint32_t ma = __ballot_sync(kFullMask, a), + mm = __ballot_sync(kFullMask, m); + const uint32_t at = above + __popc(ma & lt), mt = mid + __popc(mm & lt); + if (a && at < topk) out[at] = candidate_decode_index(int32_t(p[k])); + if (m && mt < kMidCap) midbuf[mt] = uint16_t(e); + above += __popc(ma); + mid += __popc(mm); + } + } + } + __syncwarp(); + if (above >= topk || above + mid < topk || mid > kMidCap) return false; + // Bracket codes per lane (register-resident). + constexpr uint32_t kMidPerLane = kMidCap / 32; + uint32_t mc[kMidPerLane]; +#pragma unroll + for (uint32_t u = 0; u < kMidPerLane; ++u) { + const uint32_t i = lane + 32 * u; + mc[u] = 0; + if (i < mid) mc[u] = codes[midbuf[i]]; + } + uint32_t rank = topk - above, base = h_lo << 8, top = h_hi << 8 | 0xffu; +#pragma unroll 1 + while (top > base) { + const uint32_t width = top - base + 1, spread = (width - 1) / kMidBins; + const uint32_t shift = spread ? 32 - __clz(spread) : 0; +#pragma unroll + for (uint32_t b = lane; b < kMidBins / 4; b += 32) + reinterpret_cast(h512)[b] = make_uint4(0, 0, 0, 0); + __syncwarp(); +#pragma unroll + for (uint32_t u = 0; u < kMidPerLane; ++u) { + if (u * 32 >= mid) break; + if (lane + 32 * u < mid && mc[u] >= base && mc[u] <= top) + atomicAdd(h512 + ((mc[u] - base) >> shift), 1u); + } + __syncwarp(); + uint32_t above_bin; + const uint32_t chosen = find_bin_v(h512, rank, above_bin); + rank -= above_bin; + base += chosen << shift; + top = min(base + (1u << shift) - 1, top); + __syncwarp(); + } + const uint32_t th = base; + { + // Ballot placement (coalesced stores): greater codes fill [above, K - + // rank), ties [K - rank, K). + uint32_t pg = above, pt = topk - rank; +#pragma unroll + for (uint32_t h = 0; h < 2; ++h) { + if (h * 16 * 32 >= mid) break; + int32_t pk[16]; +#pragma unroll + for (uint32_t j = 0; j < 16; ++j) { + const uint32_t u = h * 16 + j; + pk[j] = (lane + 32 * u < mid && mc[u] >= th) + ? __ldcg(packed + midbuf[lane + 32 * u]) + : 0; + } +#pragma unroll + for (uint32_t j = 0; j < 16; ++j) { + const uint32_t u = h * 16 + j; + if (u * 32 >= mid) break; + const bool v = lane + 32 * u < mid; + const bool g = v && mc[u] > th, tq = v && mc[u] == th; + const uint32_t mg = __ballot_sync(kFullMask, g), + mt = __ballot_sync(kFullMask, tq); + const uint32_t ag = pg + __popc(mg & lt), at = pt + __popc(mt & lt); + if (g) out[ag] = candidate_decode_index(pk[j]); + if (tq && at < topk) out[at] = candidate_decode_index(pk[j]); + pg += __popc(mg); + pt += __popc(mt); + } + } + } + return true; +} + +} // namespace fused_mqa_topk::selection diff --git a/csrc/hisparse_kernels.cu b/csrc/hisparse_kernels.cu index 74df6553d020..cae27cb44a48 100644 --- a/csrc/hisparse_kernels.cu +++ b/csrc/hisparse_kernels.cu @@ -156,13 +156,25 @@ __device__ __forceinline__ int warp_inclusive_scan(int32_t* s_data, int lane_id, return __shfl_sync(0xffffffff, val, 31); } +// Non-negative x only. Block sizes are powers of two in practice, and integer +// division would otherwise dominate the resolver's per-position instructions. +__device__ __forceinline__ int32_t block_div(int32_t x, int32_t block_size) { + return (block_size & (block_size - 1)) == 0 ? x >> (__ffs(block_size) - 1) + : x / block_size; +} + +__device__ __forceinline__ int32_t block_mod(int32_t x, int32_t block_size) { + return (block_size & (block_size - 1)) == 0 ? x & (block_size - 1) + : x % block_size; +} + __device__ __forceinline__ int64_t get_physical_hot_row(const int32_t* hot_block_table, int32_t row, int64_t table_stride, int32_t block_size, int32_t slot) { const int32_t block = hot_block_table[static_cast(row) * table_stride + - slot / block_size]; - return static_cast(block) * block_size + slot % block_size; + block_div(slot, block_size)]; + return static_cast(block) * block_size + block_mod(slot, block_size); } __device__ __forceinline__ void store_hot_index( @@ -174,8 +186,8 @@ __device__ __forceinline__ void store_hot_index( attention_indices[index] = physical_row < 0 ? -1 - : (physical_row / hot_block_size) * attention_block_stride + - physical_row % hot_block_size; + : block_div(physical_row, hot_block_size) * attention_block_stride + + block_mod(physical_row, hot_block_size); } } @@ -189,6 +201,8 @@ constexpr int kResidencyCounters = 5; // Per-request table entries per referenced top-k position, within the // device's shared-memory budget. constexpr int kHashLoadInverse = 4; +// Top-k positions per thread the resolver loads before its request lookups. +constexpr int kPrefetchedTopK = 2; // Translate a request-relative top-k position to its resident GPU row (-1 when // the page is not resident) and returns its host row (-1 when not host-backed). @@ -205,7 +219,7 @@ __device__ __forceinline__ int32_t translate_topk_entry( resident_row = -1; if (source_block_table != nullptr) { const int32_t source_block = - token_index >= 0 ? token_index / source_block_size : -1; + token_index >= 0 ? block_div(token_index, source_block_size) : -1; if (request_row >= 0 && request_row < source_num_reqs && source_block >= 0 && source_block < source_num_blocks) { const int32_t physical_block = @@ -213,14 +227,14 @@ __device__ __forceinline__ int32_t translate_topk_entry( source_bt_stride + source_block]; g = physical_block > 0 ? physical_block * source_block_size + - token_index % source_block_size + block_mod(token_index, source_block_size) : -1; } else { g = -1; } if (resident_block_table != nullptr) { const int32_t resident_block = - token_index >= 0 ? token_index / resident_block_size : -1; + token_index >= 0 ? block_div(token_index, resident_block_size) : -1; if (state_row >= 0 && state_row < resident_num_reqs && resident_block >= 0 && resident_block < resident_num_blocks) { const int32_t physical_block = @@ -229,7 +243,7 @@ __device__ __forceinline__ int32_t translate_topk_entry( resident_block]; if (physical_block != resident_null_block && physical_block >= 0) { resident_row = physical_block * resident_block_size + - token_index % resident_block_size; + block_mod(token_index, resident_block_size); } } } @@ -298,7 +312,9 @@ __device__ __forceinline__ void write_entry_index( // s_done[nbc] hot slots holding a union row (phase 2), then // positions phase 3 resolved, one word per chunk // s_lru_out[hot_size] int16, compacted slots: [hits fwd | evict bwd] -// Valid global ids must be unique within each row. +// Valid global ids must be unique within each row. A nonzero kBlockSize fixes +// every block size at compile time. +template __global__ __launch_bounds__(1024) void hisparse_resolve_residency_kernel( const int32_t* __restrict__ hot_block_table, // [max_rows, hot_blocks] const int32_t* __restrict__ global_indices, // global or request-relative @@ -319,69 +335,93 @@ __global__ __launch_bounds__(1024) void hisparse_resolve_residency_kernel( const int32_t* __restrict__ request_state_indices, // [num_requests] or // nullptr const int32_t request_state_count, const int64_t host_rows, - const int64_t hot_table_stride, const int32_t hot_block_size, + const int64_t hot_table_stride, const int32_t hot_block_size_arg, const int32_t top_k, const int32_t hot_size, const int32_t hash_size, const int32_t max_union_rows, const int64_t region_stride, const int64_t attention_block_stride, const int64_t source_bt_stride, const int32_t source_num_reqs, const int32_t source_num_blocks, - const int32_t source_block_size, const int64_t resident_bt_stride, + const int32_t source_block_size_arg, const int64_t resident_bt_stride, const int32_t resident_num_reqs, const int32_t resident_num_blocks, - const int32_t resident_block_size, const int32_t resident_null_block, + const int32_t resident_block_size_arg, const int32_t resident_null_block, const int64_t input_row_stride, const int64_t attention_row_stride, - const int64_t valid_count_stride) { + const int64_t valid_count_stride, + const int32_t* __restrict__ num_valid_rows) { // [1] or nullptr + const int32_t hot_block_size = + kBlockSize > 0 ? kBlockSize : hot_block_size_arg; + const int32_t source_block_size = + kBlockSize > 0 ? kBlockSize : source_block_size_arg; + const int32_t resident_block_size = + kBlockSize > 0 ? kBlockSize : resident_block_size_arg; const int NUM_WARPS = blockDim.x / kWarpSize; const int num_buffer_chunks = (hot_size + kWarpSize - 1) / kWarpSize; - // The first row of each contiguous run of a request's rows leads it. FULL - // CUDA-graph padding rows map to request 0 after the real rows, forming a - // second run, but carry no valid top-k: that run returns before phase 2 - // touches the request's shared state. + const int tid = threadIdx.x; + const int warp_id = tid / kWarpSize; + const int lane_id = tid % kWarpSize; + const unsigned int lanes_before = ((unsigned int)1 << lane_id) - 1; const int first_row = blockIdx.x; - const int request_row = - request_ids != nullptr ? request_ids[first_row] : first_row; - if (request_ids != nullptr && first_row > 0 && - request_ids[first_row - 1] == request_row) { - return; - } - int end_row = first_row + 1; - if (request_ids != nullptr) { - while (end_row < static_cast(gridDim.x) && - request_ids[end_row] == request_row) { - ++end_row; - } + + // Issued before the request lookups resolve so that an all-resident step + // pays few dependent memory round trips. + int32_t first_topk[kPrefetchedTopK]; +#pragma unroll + for (int k = 0; k < kPrefetchedTopK; ++k) { + const int i = tid + k * blockDim.x; + first_topk[k] = i < top_k ? global_indices[static_cast(first_row) * + input_row_stride + + i] + : -1; } + // FULL CUDA-graph padding rows past num_valid_rows belong to no request. + const int valid_rows = num_valid_rows != nullptr + ? min(*num_valid_rows, static_cast(gridDim.x)) + : static_cast(gridDim.x); + const auto request_of = [&](const int row) -> int32_t { + if (request_ids == nullptr) return row; + const int32_t request = request_ids[row]; + return row < valid_rows ? request : -1; + }; + const int request_row = request_of(first_row); const int state_row = request_state_indices != nullptr && request_row >= 0 && request_row < request_state_count ? request_state_indices[request_row] : (request_state_indices == nullptr ? request_row : -1); - // V2 publishes -1 for CUDA-graph padding rows. + // V2 publishes -1 for CUDA-graph padding rows. They share no request state, + // so every block masks its own row. if (state_row < 0) { - for (int row = first_row; row < end_row; ++row) { - for (int i = threadIdx.x; i < top_k; i += blockDim.x) { - const int64_t index = static_cast(row) * top_k + i; - hot_indices[index] = -1; - if (attention_indices != nullptr) { - attention_indices[static_cast(row) * attention_row_stride + - i] = -1; - } - if (resolved_global_indices != nullptr) { - resolved_global_indices[index] = -1; - } - } - if (valid_counts != nullptr && threadIdx.x == 0) { - valid_counts[static_cast(row) * valid_count_stride] = 0; + for (int i = tid; i < top_k; i += blockDim.x) { + const int64_t index = static_cast(first_row) * top_k + i; + hot_indices[index] = -1; + if (attention_indices != nullptr) { + attention_indices[static_cast(first_row) * + attention_row_stride + + i] = -1; } - if (swap_counts != nullptr && threadIdx.x == 0) { - swap_counts[row] = 0; + if (resolved_global_indices != nullptr) { + resolved_global_indices[index] = -1; } } + if (valid_counts != nullptr && tid == 0) { + valid_counts[static_cast(first_row) * valid_count_stride] = 0; + } + if (swap_counts != nullptr && tid == 0) { + swap_counts[first_row] = 0; + } return; } - const int tid = threadIdx.x; - const int warp_id = tid / kWarpSize; - const int lane_id = tid % kWarpSize; - const unsigned int lanes_before = ((unsigned int)1 << lane_id) - 1; + // The first row of each contiguous run of a request's rows leads it. + if (request_ids != nullptr && first_row > 0 && + request_of(first_row - 1) == request_row) { + return; + } + int end_row = first_row + 1; + if (request_ids != nullptr) { + while (end_row < static_cast(gridDim.x) && + request_of(end_row) == request_row) { + ++end_row; + } + } int32_t* row_dgi = device_global_indices + static_cast(state_row) * region_stride; @@ -411,7 +451,8 @@ __global__ __launch_bounds__(1024) void hisparse_resolve_residency_kernel( // s_chunk_off holds per-row valid counts until phase 2. int32_t* s_row_valid = s_chunk_off; const int num_positions = (end_row - first_row) * top_k; - for (int position = tid; position < num_positions; position += blockDim.x) { + const auto resolve_position = [&](const int position, + const int32_t token_index) { const int row = first_row + position / top_k; const int i = position % top_k; int32_t* row_out = hot_indices + static_cast(row) * top_k; @@ -422,12 +463,11 @@ __global__ __launch_bounds__(1024) void hisparse_resolve_residency_kernel( : nullptr; int32_t resident_row; const int32_t g = translate_topk_entry( - global_indices[static_cast(row) * input_row_stride + i], - request_row, state_row, source_block_table, resident_block_table, - host_rows, source_bt_stride, source_num_reqs, source_num_blocks, - source_block_size, resident_bt_stride, resident_num_reqs, - resident_num_blocks, resident_block_size, resident_null_block, - resident_row); + token_index, request_row, state_row, source_block_table, + resident_block_table, host_rows, source_bt_stride, source_num_reqs, + source_num_blocks, source_block_size, resident_bt_stride, + resident_num_reqs, resident_num_blocks, resident_block_size, + resident_null_block, resident_row); if (resolved_global_indices != nullptr) { resolved_global_indices[static_cast(row) * top_k + i] = g; } @@ -446,6 +486,27 @@ __global__ __launch_bounds__(1024) void hisparse_resolve_residency_kernel( row_out[i] = encode_host_row(g); atomicAdd(&s_counters[3], 1); } + }; +#pragma unroll + for (int k = 0; k < kPrefetchedTopK; ++k) { + const int position = tid + k * blockDim.x; + if (position < num_positions) { + resolve_position(position, + position < top_k + ? first_topk[k] + : global_indices[static_cast( + first_row + position / top_k) * + input_row_stride + + position % top_k]); + } + } + for (int position = tid + kPrefetchedTopK * blockDim.x; + position < num_positions; position += blockDim.x) { + resolve_position( + position, + global_indices[static_cast(first_row + position / top_k) * + input_row_stride + + position % top_k]); } __syncthreads(); for (int row = first_row + tid; row < end_row; row += blockDim.x) { @@ -879,30 +940,42 @@ __global__ void hisparse_gather_compact_kernel( } } +// One block per (written token, index group): drop the group's hot copy of +// the token's rewritten host slot. __global__ void hisparse_invalidate_written_slots_kernel( - int32_t* __restrict__ device_global_indices, + int32_t* const* __restrict__ device_global_indices, // [num_groups] const int32_t* __restrict__ request_state_indices, const int32_t* __restrict__ req_id_per_token, - const int64_t* __restrict__ written_slots, const int64_t num_tokens, - const int64_t num_request_ids, const int64_t num_state_rows, - const int64_t region_stride) { + const int64_t* __restrict__ written_slots, const int64_t num_request_ids, + const int64_t num_state_rows, const int64_t region_stride) { const int64_t token_idx = blockIdx.x; - if (token_idx >= num_tokens) { - return; - } const int32_t req_idx = req_id_per_token[token_idx]; if (req_idx < 0 || req_idx >= num_request_ids) { return; } const int32_t state_idx = request_state_indices[req_idx]; const int64_t written_slot = written_slots[token_idx]; - if (state_idx < 0 || state_idx >= num_state_rows || written_slot < 0) { + if (state_idx < 0 || state_idx >= num_state_rows || written_slot < 0 || + written_slot > INT32_MAX) { + return; + } + const int32_t slot = static_cast(written_slot); + int32_t* row = device_global_indices[blockIdx.y] + state_idx * region_stride; + if (region_stride % 4 == 0 && (reinterpret_cast(row) & 15) == 0) { + const int4* row4 = reinterpret_cast(row); + for (int64_t offset = threadIdx.x; offset < region_stride / 4; + offset += blockDim.x) { + const int4 v = row4[offset]; + if (v.x == slot) row[4 * offset] = -1; + if (v.y == slot) row[4 * offset + 1] = -1; + if (v.z == slot) row[4 * offset + 2] = -1; + if (v.w == slot) row[4 * offset + 3] = -1; + } return; } - int32_t* row = device_global_indices + state_idx * region_stride; for (int64_t offset = threadIdx.x; offset < region_stride; offset += blockDim.x) { - if (row[offset] == written_slot) { + if (row[offset] == slot) { row[offset] = -1; } } @@ -920,20 +993,22 @@ int64_t check_2d_rows(const torch::stable::Tensor& t, const char* name, } // namespace void hisparse_invalidate_written_slots( - torch::stable::Tensor& device_global_indices, + torch::stable::Tensor const& device_global_indices_ptrs, torch::stable::Tensor const& request_state_indices, torch::stable::Tensor const& req_id_per_token, - torch::stable::Tensor const& written_slots) { - STD_TORCH_CHECK(device_global_indices.is_cuda() && + torch::stable::Tensor const& written_slots, int64_t num_state_rows, + int64_t region_stride) { + STD_TORCH_CHECK(device_global_indices_ptrs.is_cuda() && request_state_indices.is_cuda() && req_id_per_token.is_cuda() && written_slots.is_cuda(), "HiSparse invalidation tensors must be on CUDA"); STD_TORCH_CHECK( - device_global_indices.scalar_type() == - torch::headeronly::ScalarType::Int && - device_global_indices.dim() == 2 && - device_global_indices.is_contiguous(), - "device_global_indices must be a contiguous 2D int32 CUDA tensor"); + device_global_indices_ptrs.scalar_type() == + torch::headeronly::ScalarType::Long && + device_global_indices_ptrs.dim() == 1 && + device_global_indices_ptrs.is_contiguous(), + "device_global_indices_ptrs must be a contiguous 1D int64 CUDA tensor " + "of [max_num_reqs, region_stride] int32 table addresses"); STD_TORCH_CHECK( request_state_indices.scalar_type() == torch::headeronly::ScalarType::Int && @@ -950,7 +1025,7 @@ void hisparse_invalidate_written_slots( "written_slots must be a contiguous 1D int64 CUDA tensor"); STD_TORCH_CHECK(req_id_per_token.numel() == written_slots.numel(), "req_id_per_token and written_slots must have equal length"); - const int device_index = device_global_indices.get_device_index(); + const int device_index = device_global_indices_ptrs.get_device_index(); STD_TORCH_CHECK( request_state_indices.get_device_index() == device_index && req_id_per_token.get_device_index() == device_index && @@ -958,24 +1033,25 @@ void hisparse_invalidate_written_slots( "HiSparse invalidation tensors must be on the same CUDA device"); const int64_t num_tokens = written_slots.numel(); - const int64_t num_state_rows = device_global_indices.size(0); - const int64_t region_stride = device_global_indices.size(1); - if (num_tokens == 0 || num_state_rows == 0 || region_stride == 0) { + const int64_t num_groups = device_global_indices_ptrs.numel(); + if (num_tokens == 0 || num_groups == 0 || num_state_rows == 0 || + region_stride == 0) { return; } - STD_TORCH_CHECK(num_tokens <= INT32_MAX, + STD_TORCH_CHECK(num_tokens <= INT32_MAX && num_groups <= 65535, "HiSparse invalidation grid exceeds CUDA limits"); constexpr int kBlockSize = 256; const torch::stable::accelerator::DeviceGuard device_guard(device_index); const cudaStream_t stream = get_current_cuda_stream(); - hisparse_invalidate_written_slots_kernel<<(num_tokens), - kBlockSize, 0, stream>>>( - device_global_indices.mutable_data_ptr(), + const dim3 grid(static_cast(num_tokens), + static_cast(num_groups)); + hisparse_invalidate_written_slots_kernel<<>>( + static_cast(device_global_indices_ptrs.const_data_ptr()), request_state_indices.const_data_ptr(), req_id_per_token.const_data_ptr(), - written_slots.const_data_ptr(), num_tokens, - request_state_indices.numel(), num_state_rows, region_stride); + written_slots.const_data_ptr(), request_state_indices.numel(), + num_state_rows, region_stride); const cudaError_t launch_error = cudaGetLastError(); STD_TORCH_CHECK(launch_error == cudaSuccess, "HiSparse invalidation kernel launch failed: ", @@ -1004,7 +1080,8 @@ void hisparse_resolve_residency( std::optional const& swap_device_physical_rows, std::optional const& swap_counts, std::optional const& resident_block_table, - int64_t resident_block_size, int64_t resident_null_block) { + int64_t resident_block_size, int64_t resident_null_block, + std::optional const& num_valid_rows) { STD_TORCH_CHECK( host_cache.device().is_cpu() && is_pinned_cpu_tensor(host_cache), "host_cache must be pinned CPU memory"); @@ -1098,6 +1175,18 @@ void hisparse_resolve_residency( source_num_reqs = static_cast(table.size(0)); source_num_blocks = static_cast(table.size(1)); } + const int32_t* num_valid_rows_ptr = nullptr; + if (num_valid_rows.has_value()) { + auto const& valid_rows = num_valid_rows.value(); + STD_TORCH_CHECK(request_ids.has_value(), + "num_valid_rows requires request_ids"); + STD_TORCH_CHECK( + valid_rows.is_cuda() && + valid_rows.scalar_type() == torch::headeronly::ScalarType::Int && + valid_rows.numel() == 1, + "num_valid_rows must be a one-element int32 CUDA tensor"); + num_valid_rows_ptr = valid_rows.const_data_ptr(); + } const int32_t required_hot_rows = request_ids_ptr != nullptr ? source_num_reqs : launch_rows; STD_TORCH_CHECK(hot_block_table.size(0) >= required_hot_rows, @@ -1278,20 +1367,31 @@ void hisparse_resolve_residency( attention_indices.has_value() ? attention_indices.value().stride(0) : 0; const int64_t valid_count_stride = valid_counts.has_value() ? valid_counts.value().stride(0) : 0; - auto kernel = hisparse_resolve_residency_kernel; + // Block arithmetic dominates the all-resident path; specialize the + // ubiquitous 64-row block. + constexpr int kFixedBlockSize = 64; + const bool fixed_block_size = hot_block_size == kFixedBlockSize && + (!source_block_table.has_value() || + source_block_size == kFixedBlockSize) && + (!resident_block_table.has_value() || + resident_block_size == kFixedBlockSize); + auto kernel = fixed_block_size + ? hisparse_resolve_residency_kernel + : hisparse_resolve_residency_kernel<0>; if (smem_bytes > 48 * 1024) { - static std::vector configured_smem_bytes; + static std::vector configured_smem_bytes[2]; + std::vector& configured = configured_smem_bytes[fixed_block_size]; const size_t device_index = hot_cache.get_device_index(); - if (configured_smem_bytes.size() <= device_index) { - configured_smem_bytes.resize(device_index + 1, 0); + if (configured.size() <= device_index) { + configured.resize(device_index + 1, 0); } - if (configured_smem_bytes[device_index] < smem_bytes) { + if (configured[device_index] < smem_bytes) { const cudaError_t attribute_error = cudaFuncSetAttribute( kernel, cudaFuncAttributeMaxDynamicSharedMemorySize, smem_bytes); STD_TORCH_CHECK(attribute_error == cudaSuccess, "failed to configure HiSparse swap-in shared memory: ", cudaGetErrorString(attribute_error)); - configured_smem_bytes[device_index] = smem_bytes; + configured[device_index] = smem_bytes; } } kernel<<>>( @@ -1311,7 +1411,7 @@ void hisparse_resolve_residency( resident_bt_stride, resident_num_reqs, resident_num_blocks, static_cast(resident_block_size), static_cast(resident_null_block), global_indices.stride(0), - attention_row_stride, valid_count_stride); + attention_row_stride, valid_count_stride, num_valid_rows_ptr); const cudaError_t launch_error = cudaGetLastError(); STD_TORCH_CHECK(launch_error == cudaSuccess, "HiSparse residency kernel launch failed: ", diff --git a/csrc/moe/grouped_topk_kernels.cu b/csrc/moe/grouped_topk_kernels.cu index ea89aa38a1f0..443cdd14dc69 100644 --- a/csrc/moe/grouped_topk_kernels.cu +++ b/csrc/moe/grouped_topk_kernels.cu @@ -479,9 +479,9 @@ __device__ void topk_with_k2(T* output, T const* input, BiasT const* bias, template __global__ void grouped_topk_fused_kernel( T* scores, float* topk_values, IdxT* topk_indices, BiasT const* bias, - int64_t const num_tokens, int64_t const num_experts, int64_t const n_group, - int64_t const topk_group, int64_t const topk, bool renormalize, - double routed_scaling_factor) { + bool const* is_padding, int64_t const num_tokens, int64_t const num_experts, + int64_t const n_group, int64_t const topk_group, int64_t const topk, + bool renormalize, double routed_scaling_factor) { int32_t const token_id = static_cast(blockIdx.x); if (token_id >= num_tokens) { return; @@ -564,8 +564,11 @@ __global__ void grouped_topk_fused_kernel( if (!proceed) { for (int i = lane_id; i < topk_i32; i += WARP_SIZE) { - topk_indices[i] = static_cast(i); - topk_values[i] = 1.0f / static_cast(topk_i32); + topk_indices[i] = + is_padding && is_padding[token_id] ? -1 : static_cast(i); + topk_values[i] = is_padding && is_padding[token_id] + ? 0.0f + : 1.0f / static_cast(topk_i32); } #if (defined(__CUDA_ARCH__) && (__CUDA_ARCH__ >= 900)) cudaTriggerProgrammaticLaunchCompletion(); @@ -624,8 +627,9 @@ __global__ void grouped_topk_fused_kernel( } if (lane_id < topk_i32) { - topk_indices[lane_id] = lane_idx; - topk_values[lane_id] = lane_unbiased * scale; + topk_indices[lane_id] = is_padding && is_padding[token_id] ? -1 : lane_idx; + topk_values[lane_id] = + is_padding && is_padding[token_id] ? 0.0f : lane_unbiased * scale; } #if (defined(__CUDA_ARCH__) && (__CUDA_ARCH__ >= 900)) @@ -638,8 +642,8 @@ template __global__ void grouped_topk_fused_small_expert_count_kernel( T* scores, float* topkValues, IdxT* topkIndices, BiasT const* routingBias, - int64_t const numTokens, int64_t const numGroup, int64_t const topkGroup, - int64_t const topk, int64_t const numExperts, + bool const* is_padding, int64_t const numTokens, int64_t const numGroup, + int64_t const topkGroup, int64_t const topk, int64_t const numExperts, int64_t const numExpertsPerGroup, bool const renormalize, double const routedScalingFactor) { #if (defined(__CUDA_ARCH__) && (__CUDA_ARCH__ >= 900)) @@ -838,8 +842,10 @@ __global__ void grouped_topk_fused_small_expert_count_kernel( } // store the topk scores and experts to output if (laneIdx < topk) { - topkValues[laneIdx] = finalScore; - topkIndices[laneIdx] = expertIdx; + topkValues[laneIdx] = + is_padding && is_padding[blockIdx.x] ? 0.0f : finalScore; + topkIndices[laneIdx] = + is_padding && is_padding[blockIdx.x] ? -1 : expertIdx; } } @@ -890,7 +896,8 @@ __device__ __forceinline__ void write_outputs( cg::thread_block_tile const& warp, float lane_selection_score, float lane_unbiased, int32_t lane_expert, int32_t lane, int32_t token, int32_t topk, float* topk_values, IdxT* topk_indices, bool renormalize, - float routed_scaling_factor) { + float routed_scaling_factor, bool const* is_padding) { + bool const padded = is_padding && is_padding[token]; bool const finite_selection = lane < topk && lane_selection_score != InvalidScore; lane_unbiased = finite_selection ? lane_unbiased : 0.0F; @@ -912,8 +919,8 @@ __device__ __forceinline__ void write_outputs( } int64_t const output_index = int64_t{token} * topk + lane; - topk_values[output_index] = output; - topk_indices[output_index] = static_cast(lane_expert); + topk_values[output_index] = padded ? 0.0F : output; + topk_indices[output_index] = static_cast(padded ? -1 : lane_expert); } } @@ -922,8 +929,8 @@ template = 900) @@ -1095,9 +1102,9 @@ template ::BlockDim) single_group_topk_warp_kernel(T const* scores, float* topk_values, IdxT* topk_indices, BiasT const* bias, - int64_t num_tokens, int64_t num_experts, - int64_t topk, bool renormalize, - float routed_scaling_factor, + bool const* is_padding, int64_t num_tokens, + int64_t num_experts, int64_t topk, + bool renormalize, float routed_scaling_factor, bool enable_pdl) { static constexpr int NumChunks = (MaxNumExperts + WARP_SIZE - 1) / WARP_SIZE; static constexpr int WarpBlockDim = @@ -1166,7 +1173,7 @@ __global__ void __launch_bounds__(WarpTopKLaunchConfig::BlockDim) } write_outputs(warp, lane_score, lane_unbiased, lane_expert, lane, token, topk_i32, topk_values, topk_indices, renormalize, - routed_scaling_factor); + routed_scaling_factor, is_padding); } #if defined(__CUDA_ARCH__) && (__CUDA_ARCH__ >= 900) @@ -1198,9 +1205,10 @@ using PrecomputedSoftmaxBiasTiers = template void launch(T* scores, float* topk_values, IdxT* topk_indices, - BiasT const* bias, int64_t num_tokens, int64_t num_experts, - int64_t topk, bool renormalize, double routed_scaling_factor, - bool enable_pdl, cudaLaunchConfig_t& config) { + BiasT const* bias, bool const* is_padding, int64_t num_tokens, + int64_t num_experts, int64_t topk, bool renormalize, + double routed_scaling_factor, bool enable_pdl, + cudaLaunchConfig_t& config) { config.dynamicSmemBytes = 0; bool const use_block_kernel = UseTunedBlockPath || @@ -1213,8 +1221,8 @@ void launch(T* scores, float* topk_values, IdxT* topk_indices, &config, &single_group_topk_block_kernel, - scores, topk_values, topk_indices, bias, num_experts, topk, renormalize, - static_cast(routed_scaling_factor), enable_pdl); + scores, topk_values, topk_indices, bias, is_padding, num_experts, topk, + renormalize, static_cast(routed_scaling_factor), enable_pdl); } else { using WarpConfig = WarpTopKLaunchConfig; config.gridDim = WarpConfig::grid_dim(num_tokens); @@ -1223,54 +1231,58 @@ void launch(T* scores, float* topk_values, IdxT* topk_indices, &config, &single_group_topk_warp_kernel, - scores, topk_values, topk_indices, bias, num_tokens, num_experts, topk, - renormalize, static_cast(routed_scaling_factor), enable_pdl); + scores, topk_values, topk_indices, bias, is_padding, num_tokens, + num_experts, topk, renormalize, + static_cast(routed_scaling_factor), enable_pdl); } } template -bool dispatch(TierList<>*, T*, float*, IdxT*, BiasT const*, int64_t, int64_t, - int64_t, bool, double, bool, cudaLaunchConfig_t&) { +bool dispatch(TierList<>*, T*, float*, IdxT*, BiasT const*, bool const*, + int64_t, int64_t, int64_t, bool, double, bool, + cudaLaunchConfig_t&) { return false; } template bool dispatch(TierList*, T* scores, float* topk_values, - IdxT* topk_indices, BiasT const* bias, int64_t num_tokens, - int64_t num_experts, int64_t topk, bool renormalize, - double routed_scaling_factor, bool enable_pdl, + IdxT* topk_indices, BiasT const* bias, bool const* is_padding, + int64_t num_tokens, int64_t num_experts, int64_t topk, + bool renormalize, double routed_scaling_factor, bool enable_pdl, cudaLaunchConfig_t& config) { if (num_experts <= First::kExperts && topk <= First::kTopK) { launch( - scores, topk_values, topk_indices, bias, num_tokens, num_experts, topk, - renormalize, routed_scaling_factor, enable_pdl, config); + scores, topk_values, topk_indices, bias, is_padding, num_tokens, + num_experts, topk, renormalize, routed_scaling_factor, enable_pdl, + config); return true; } return dispatch( static_cast*>(nullptr), scores, topk_values, - topk_indices, bias, num_tokens, num_experts, topk, renormalize, - routed_scaling_factor, enable_pdl, config); + topk_indices, bias, is_padding, num_tokens, num_experts, topk, + renormalize, routed_scaling_factor, enable_pdl, config); } } // namespace detail template bool invoke(T* scores, float* topk_values, IdxT* topk_indices, - BiasT const* bias, int64_t num_tokens, int64_t num_experts, - int64_t topk, bool renormalize, double routed_scaling_factor, - bool enable_pdl, cudaLaunchConfig_t& config) { + BiasT const* bias, bool const* is_padding, int64_t num_tokens, + int64_t num_experts, int64_t topk, bool renormalize, + double routed_scaling_factor, bool enable_pdl, + cudaLaunchConfig_t& config) { static_assert(SF == SCORING_NONE || SF == SCORING_SIGMOID); if constexpr (SF == SCORING_SIGMOID) { return detail::dispatch( static_cast(nullptr), scores, topk_values, - topk_indices, bias, num_tokens, num_experts, topk, renormalize, - routed_scaling_factor, enable_pdl, config); + topk_indices, bias, is_padding, num_tokens, num_experts, topk, + renormalize, routed_scaling_factor, enable_pdl, config); } else { return detail::dispatch( static_cast(nullptr), scores, - topk_values, topk_indices, bias, num_tokens, num_experts, topk, - renormalize, routed_scaling_factor, enable_pdl, config); + topk_values, topk_indices, bias, is_padding, num_tokens, num_experts, + topk, renormalize, routed_scaling_factor, enable_pdl, config); } } @@ -1278,10 +1290,11 @@ bool invoke(T* scores, float* topk_values, IdxT* topk_indices, template void invokeNoAuxTc(T* scores, float* topk_values, IdxT* topk_indices, - BiasT const* bias, int64_t const num_tokens, - int64_t const num_experts, int64_t const n_group, - int64_t const topk_group, int64_t const topk, - bool const renormalize, double const routed_scaling_factor, + BiasT const* bias, bool const* is_padding, + int64_t const num_tokens, int64_t const num_experts, + int64_t const n_group, int64_t const topk_group, + int64_t const topk, bool const renormalize, + double const routed_scaling_factor, const bool enable_pdl = false, cudaStream_t const stream = 0) { cudaLaunchConfig_t config; @@ -1293,8 +1306,9 @@ void invokeNoAuxTc(T* scores, float* topk_values, IdxT* topk_indices, config.attrs = attrs; if (n_group == 1 && topk_group == 1 && single_group_topk::invoke( - scores, topk_values, topk_indices, bias, num_tokens, num_experts, - topk, renormalize, routed_scaling_factor, enable_pdl, config)) { + scores, topk_values, topk_indices, bias, is_padding, num_tokens, + num_experts, topk, renormalize, routed_scaling_factor, enable_pdl, + config)) { return; } @@ -1346,9 +1360,9 @@ void invokeNoAuxTc(T* scores, float* topk_values, IdxT* topk_indices, config.blockDim = num_threads; config.dynamicSmemBytes = 0; cudaLaunchKernelEx(&config, kernel_instance, scores, topk_values, - topk_indices, bias, num_tokens, n_group, topk_group, - topk, num_experts, num_experts / n_group, renormalize, - routed_scaling_factor); + topk_indices, bias, is_padding, num_tokens, n_group, + topk_group, topk, num_experts, num_experts / n_group, + renormalize, routed_scaling_factor); } else { auto* kernel_instance = &grouped_topk_fused_kernel; // One block per token; one warp per group. @@ -1366,18 +1380,20 @@ void invokeNoAuxTc(T* scores, float* topk_values, IdxT* topk_indices, size_t const extra_bytes = 16 + static_cast(n_group) * sizeof(T); config.dynamicSmemBytes = internal_bytes + extra_bytes; cudaLaunchKernelEx(&config, kernel_instance, scores, topk_values, - topk_indices, bias, num_tokens, num_experts, n_group, - topk_group, topk, renormalize, routed_scaling_factor); + topk_indices, bias, is_padding, num_tokens, num_experts, + n_group, topk_group, topk, renormalize, + routed_scaling_factor); } } #define INSTANTIATE_NOAUX_TC(T, BiasT, IdxT, SF) \ template void invokeNoAuxTc( \ T * scores, float* topk_values, IdxT* topk_indices, BiasT const* bias, \ - int64_t const num_tokens, int64_t const num_experts, \ - int64_t const n_group, int64_t const topk_group, int64_t const topk, \ - bool const renormalize, double const routed_scaling_factor, \ - const bool enable_pdl, cudaStream_t const stream); + bool const* is_padding, int64_t const num_tokens, \ + int64_t const num_experts, int64_t const n_group, \ + int64_t const topk_group, int64_t const topk, bool const renormalize, \ + double const routed_scaling_factor, const bool enable_pdl, \ + cudaStream_t const stream); INSTANTIATE_NOAUX_TC(float, float, int32_t, SCORING_SIGMOID); INSTANTIATE_NOAUX_TC(float, half, int32_t, SCORING_SIGMOID); @@ -1403,7 +1419,8 @@ INSTANTIATE_NOAUX_TC(__nv_bfloat16, __nv_bfloat16, int32_t, SCORING_NONE); std::tuple grouped_topk( torch::stable::Tensor const& scores, int64_t n_group, int64_t topk_group, int64_t topk, bool renormalize, double routed_scaling_factor, - torch::stable::Tensor const& bias, int64_t scoring_func = 0) { + torch::stable::Tensor const& bias, int64_t scoring_func, + const std::optional& is_padding) { const auto data_type = scores.scalar_type(); const auto bias_type = bias.scalar_type(); STD_TORCH_CHECK(scores.dim() == 2, "scores must be a 2D Tensor"); @@ -1426,6 +1443,20 @@ std::tuple grouped_topk( scoring_func == vllm::moe::SCORING_SIGMOID, "scoring_func must be SCORING_NONE (0) or SCORING_SIGMOID (1)"); + const bool* is_padding_ptr = nullptr; + if (is_padding.has_value()) { + STD_TORCH_CHECK( + is_padding->scalar_type() == torch::headeronly::ScalarType::Bool, + "is_padding must be bool"); + STD_TORCH_CHECK(is_padding->dim() == 1 && is_padding->size(0) == num_tokens, + "is_padding must have one entry per token"); + STD_TORCH_CHECK(is_padding->is_contiguous(), + "is_padding must be contiguous"); + STD_TORCH_CHECK(is_padding->device() == scores.device(), + "is_padding must be on the scores device"); + is_padding_ptr = is_padding->const_data_ptr(); + } + // Always output float32 for topk_values (eliminates Python-side conversion) auto topk_values = torch::stable::new_empty( scores, {num_tokens, topk}, torch::headeronly::ScalarType::Float); @@ -1447,8 +1478,8 @@ std::tuple grouped_topk( reinterpret_cast(scores.mutable_data_ptr()), \ reinterpret_cast(topk_values.mutable_data_ptr()), \ reinterpret_cast(topk_indices.mutable_data_ptr()), \ - reinterpret_cast(bias.data_ptr()), num_tokens, \ - num_experts, n_group, topk_group, topk, renormalize, \ + reinterpret_cast(bias.data_ptr()), is_padding_ptr, \ + num_tokens, num_experts, n_group, topk_group, topk, renormalize, \ routed_scaling_factor, pdl_flag, stream); \ break; \ case vllm::moe::SCORING_SIGMOID: \ @@ -1456,8 +1487,8 @@ std::tuple grouped_topk( reinterpret_cast(scores.mutable_data_ptr()), \ reinterpret_cast(topk_values.mutable_data_ptr()), \ reinterpret_cast(topk_indices.mutable_data_ptr()), \ - reinterpret_cast(bias.data_ptr()), num_tokens, \ - num_experts, n_group, topk_group, topk, renormalize, \ + reinterpret_cast(bias.data_ptr()), is_padding_ptr, \ + num_tokens, num_experts, n_group, topk_group, topk, renormalize, \ routed_scaling_factor, pdl_flag, stream); \ break; \ default: \ diff --git a/csrc/moe/moe_ops.h b/csrc/moe/moe_ops.h index 8ab24fcb33b8..484ae2a86957 100644 --- a/csrc/moe/moe_ops.h +++ b/csrc/moe/moe_ops.h @@ -20,6 +20,15 @@ void topk_sigmoid(torch::stable::Tensor& topk_weights, double routed_scaling_factor, std::optional is_padding); +#ifdef USE_CUDA +void muse_spark_routing(torch::stable::Tensor& topk_weights, + torch::stable::Tensor& topk_ids, + const torch::stable::Tensor& logits, + const torch::stable::Tensor& bias, double multiplier, + double route_scale, double division_eps, + const std::optional& is_padding); +#endif + void topk_softplus_sqrt( torch::stable::Tensor& topk_weights, torch::stable::Tensor& topk_indices, torch::stable::Tensor& token_expert_indices, @@ -82,7 +91,8 @@ torch::stable::Tensor moe_wna16_gemm( std::tuple grouped_topk( const torch::stable::Tensor& scores, int64_t n_group, int64_t topk_group, int64_t topk, bool renormalize, double routed_scaling_factor, - const torch::stable::Tensor& bias, int64_t scoring_func); + const torch::stable::Tensor& bias, int64_t scoring_func, + const std::optional& is_padding); #endif bool moe_permute_unpermute_supported(); diff --git a/csrc/moe/muse_spark_routing.cu b/csrc/moe/muse_spark_routing.cu new file mode 100644 index 000000000000..0c77237be32f --- /dev/null +++ b/csrc/moe/muse_spark_routing.cu @@ -0,0 +1,346 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright contributors to the vLLM project +// Adapted from SGLang's Muse Spark CUDA routing implementation. + +#include +#include +#include +#include +#include +#include +#include + +#include "dispatch_utils.h" +#include "torch_utils.h" + +namespace vllm { +namespace moe { + +__device__ __forceinline__ float warp_max(float value) { +#if defined(__CUDA_ARCH_FAMILY_SPECIFIC__) && __CUDA_ARCH__ >= 1000 && \ + __CUDA_ARCH__ < 1100 + float result; + asm("redux.sync.max.f32 %0, %1, %2;" + : "=f"(result) + : "f"(value), "r"(0xffffffffu)); + return result; +#else + #pragma unroll + for (int offset = 16; offset; offset /= 2) + value = fmaxf(value, __shfl_xor_sync(0xffffffffu, value, offset)); + return value; +#endif +} + +inline constexpr float kNegInf = -__builtin_huge_valf(); +// fp32(log2(e)) == 0x3FB8AA3B, the constant Triton folds into tl.exp. +inline constexpr float kLog2e = 1.4426950408889634f; + +__device__ __forceinline__ float ex2_approx(float x) { + float r; + asm("ex2.approx.f32 %0, %1;" : "=f"(r) : "f"(x)); + return r; +} + +// Triton lowers `a / b` on fp32 to div.full.f32, not the div.rn.f32 nvcc emits. +__device__ __forceinline__ float div_full(float a, float b) { + float r; + asm("div.full.f32 %0, %1, %2;" : "=f"(r) : "f"(a), "f"(b)); + return r; +} + +// triton/language/standard.py:50 -- `1 / (1 + exp(-x))`, lowered as +// sub.f32 / mul.f32 / ex2.approx.f32 / add.f32 / div.full.f32. +__device__ __forceinline__ float triton_sigmoid(float x) { + return div_full(1.0f, ex2_approx((0.0f - x) * kLog2e) + 1.0f); +} + +// kExpertsPerLane must be a multiple of 4 (128-bit row loads) and covers +// kExpertsPerLane * 32 experts. Lane `l` holds expert v*128 + l*4 + j at +// register slot v*4 + j, monotone in the slot -- both the stable sort and the +// group-ballot tie-break rely on that to reproduce tl.argmax. +template +struct RoutingTrait { + static constexpr int kTopK = 8; + static constexpr int kExpertsPerLane = 8; + static constexpr int kVecsPerLane = 2; + static constexpr int kWarpsPerBlock = kWarpsPerBlock_; + static constexpr int kMaxExperts = 256; +}; + +template +struct RoutingParams { + const float* __restrict__ gating_output; + const float* __restrict__ bias; + float* __restrict__ topk_weights; + Index* __restrict__ topk_ids; + const bool* __restrict__ is_padding; + int64_t padding_stride; + int num_tokens; + int num_experts; + int64_t stride_gm; + int64_t stride_wm; + int64_t stride_im; + float router_output_multiplier; + float route_scale; + float division_eps; +}; + +template +__global__ void routing_kernel( + const RoutingParams __grid_constant__ params) { + constexpr int kEPL = Trait::kExpertsPerLane; + constexpr int kTopK = Trait::kTopK; + constexpr int kGroupExperts = 4 * 32; + + const int lane = threadIdx.x % 32; + const int token = + blockIdx.x * Trait::kWarpsPerBlock + static_cast(threadIdx.x / 32); + if (token >= params.num_tokens) return; + +#if defined(__CUDA_ARCH__) && __CUDA_ARCH__ >= 900 + if constexpr (kUsePDL) cudaGridDependencySynchronize(); +#endif + if (params.is_padding != nullptr && + params.is_padding[token * params.padding_stride]) { + if (lane < kTopK) { + params.topk_ids[token * params.stride_im + lane] = -1; + params.topk_weights[token * params.stride_wm + lane] = 0.0f; + } + return; + } + + const float* __restrict__ row = + params.gating_output + static_cast(token) * params.stride_gm; + float score[kEPL]; + int expert[kEPL]; + +#pragma unroll + for (int v = 0; v < Trait::kVecsPerLane; ++v) { + const int base = v * kGroupExperts + lane * 4; + float4 g = make_float4(kNegInf, kNegInf, kNegInf, kNegInf); + if (base < params.num_experts) + g = *reinterpret_cast(row + base); + score[v * 4 + 0] = g.x; + score[v * 4 + 1] = g.y; + score[v * 4 + 2] = g.z; + score[v * 4 + 3] = g.w; +#pragma unroll + for (int j = 0; j < 4; ++j) expert[v * 4 + j] = base + j; + } + +#pragma unroll + for (int s = 0; s < kEPL; ++s) score[s] *= params.router_output_multiplier; + +#pragma unroll + for (int s = 0; s < kEPL; ++s) score[s] = triton_sigmoid(score[s]); + + float biased[kEPL]; +#pragma unroll + for (int v = 0; v < Trait::kVecsPerLane; ++v) { + const int base = v * kGroupExperts + lane * 4; + float4 b = make_float4(0.0f, 0.0f, 0.0f, 0.0f); + if (base < params.num_experts) + b = *reinterpret_cast(params.bias + base); + biased[v * 4 + 0] = b.x; + biased[v * 4 + 1] = b.y; + biased[v * 4 + 2] = b.z; + biased[v * 4 + 3] = b.w; + } + +#if defined(__CUDA_ARCH__) && __CUDA_ARCH__ >= 900 + if constexpr (kUsePDL) cudaTriggerProgrammaticLaunchCompletion(); +#endif + + float key[kEPL]; +#pragma unroll + for (int s = 0; s < kEPL; ++s) { + if (expert[s] < params.num_experts) { + key[s] = biased[s] + score[s]; + } else { + score[s] = 0.0f; + key[s] = kNegInf; + } + } + + // Must be a stable sort: equal keys keep their slot order and slots are + // monotone in expert id, which is tl.argmax's lowest-index-wins tie-break. +#pragma unroll + for (int r = 0; r < kEPL; ++r) { +#pragma unroll + for (int i = r % 2; i + 1 < kEPL; i += 2) { + const bool swap = key[i + 1] > key[i]; + const float k0 = swap ? key[i + 1] : key[i]; + const float k1 = swap ? key[i] : key[i + 1]; + const int e0 = swap ? expert[i + 1] : expert[i]; + const int e1 = swap ? expert[i] : expert[i + 1]; + const float s0 = swap ? score[i + 1] : score[i]; + const float s1 = swap ? score[i] : score[i + 1]; + key[i] = k0; + key[i + 1] = k1; + expert[i] = e0; + expert[i + 1] = e1; + score[i] = s0; + score[i + 1] = s1; + } + } + + float score_sum = 0.0f; + int out_id = 0; + float out_score = 0.0f; + +#pragma unroll + for (int k = 0; k < kTopK; ++k) { + const float round_max = warp_max(key[0]); + const bool is_head = key[0] == round_max; + + // Lowest expert id among the tied heads: within one kGroupExperts-wide + // group the id is monotone in the lane, so the lowest set bit of the lowest + // non-empty group's ballot owns the round. + unsigned owners = 0; +#pragma unroll + for (int g = Trait::kVecsPerLane - 1; g >= 0; --g) { + const unsigned m = + __ballot_sync(0xffffffffu, is_head && expert[0] / kGroupExperts == g); + owners = (m != 0u) ? m : owners; + } + const int owner = __ffs(owners) - 1; + const int winner = __shfl_sync(0xffffffffu, expert[0], owner); + const float selected = __shfl_sync(0xffffffffu, score[0], owner); + + // Tapered pop: after round k a lane can be popped at most kTopK-1-k more + // times, so slots past that can never surface and need not be maintained. + const bool pop = expert[0] == winner; +#pragma unroll + for (int s = 0; s + 1 < kEPL; ++s) + if (s + k <= kTopK - 2) { + key[s] = pop ? key[s + 1] : key[s]; + expert[s] = pop ? expert[s + 1] : expert[s]; + score[s] = pop ? score[s + 1] : score[s]; + } + // Only reachable when kTopK > kEPL: a lane popped kEPL times is exhausted + // and must stop winning rounds. + if (kEPL - 1 + k <= kTopK - 2) + key[kEPL - 1] = pop ? kNegInf : key[kEPL - 1]; + + score_sum += selected; + if (lane == k) { + out_id = winner; + out_score = selected; + } + } + + score_sum += params.division_eps; + const float inv_sum_scaled = div_full(params.route_scale, score_sum); + if (lane < kTopK) { + const Index id = out_id; + const float weight = out_score * inv_sum_scaled; + params.topk_ids[token * params.stride_im + lane] = id; + params.topk_weights[token * params.stride_wm + lane] = weight; + } +} + +} // namespace moe +} // namespace vllm + +namespace { +using Tensor = torch::stable::Tensor; +using ScalarType = torch::headeronly::ScalarType; + +template +void launch_routing(const vllm::moe::RoutingParams& params, + cudaStream_t stream) { + using Trait = vllm::moe::RoutingTrait; + cudaLaunchConfig_t config{}; + config.gridDim = dim3((params.num_tokens + Warps - 1) / Warps); + config.blockDim = dim3(32 * Warps); + config.stream = stream; + cudaLaunchAttribute attr{}; + if constexpr (PDL) { + attr.id = cudaLaunchAttributeProgrammaticStreamSerialization; + attr.val.programmaticStreamSerializationAllowed = 1; + config.attrs = &attr; + config.numAttrs = 1; + } + auto err = cudaLaunchKernelEx( + &config, vllm::moe::routing_kernel, params); + STD_TORCH_CHECK(err == cudaSuccess, "Muse Spark routing launch failed: ", + cudaGetErrorString(err)); +} +} // namespace + +void muse_spark_routing(Tensor& topk_weights, Tensor& topk_ids, + const Tensor& logits, const Tensor& bias, + double multiplier, double route_scale, + double division_eps, + const std::optional& is_padding) { + STD_TORCH_CHECK(logits.device().is_cuda() && logits.dim() == 2 && + logits.scalar_type() == ScalarType::Float && + logits.size(1) == 256 && logits.stride(1) == 1 && + logits.stride(0) >= 256 && logits.stride(0) % 4 == 0 && + reinterpret_cast(logits.data_ptr()) % 16 == 0, + "logits must be aligned FP32 CUDA rows of 256 experts"); + STD_TORCH_CHECK(logits.size(0) <= std::numeric_limits::max(), + "Too many routing tokens"); + const int tokens = logits.size(0); + STD_TORCH_CHECK(bias.device() == logits.device() && bias.dim() == 1 && + bias.size(0) == 256 && + bias.scalar_type() == ScalarType::Float && + bias.stride(0) == 1 && + reinterpret_cast(bias.data_ptr()) % 16 == 0, + "bias must be an aligned FP32 tensor of 256 experts on the " + "logits device"); + for (const auto* output : {&topk_weights, &topk_ids}) { + STD_TORCH_CHECK( + output->device() == logits.device() && output->dim() == 2 && + output->size(0) == tokens && output->size(1) == 8 && + output->stride(1) == 1 && output->stride(0) >= 8, + "routing outputs must have shape [tokens, 8] with contiguous rows"); + } + STD_TORCH_CHECK( + topk_weights.scalar_type() == ScalarType::Float && + (topk_ids.scalar_type() == ScalarType::Int || + topk_ids.scalar_type() == ScalarType::Long), + "routing outputs must be FP32 weights and int32 or int64 IDs"); + if (is_padding) { + STD_TORCH_CHECK(is_padding->device() == logits.device() && + is_padding->scalar_type() == ScalarType::Bool && + is_padding->dim() == 1 && is_padding->size(0) == tokens, + "is_padding must be a bool tensor of shape [tokens] on the " + "logits device"); + } + if (tokens == 0) return; + torch::stable::accelerator::DeviceGuard guard(logits.get_device_index()); + const cudaStream_t stream = + get_current_cuda_stream(logits.get_device_index()); + VLLM_STABLE_DISPATCH_IDX_TYPES( + topk_ids.scalar_type(), "muse_spark_routing", [&] { + vllm::moe::RoutingParams params{ + .gating_output = static_cast(logits.data_ptr()), + .bias = static_cast(bias.data_ptr()), + .topk_weights = static_cast(topk_weights.data_ptr()), + .topk_ids = static_cast(topk_ids.data_ptr()), + .is_padding = is_padding + ? static_cast(is_padding->data_ptr()) + : nullptr, + .padding_stride = is_padding ? is_padding->stride(0) : 0, + .num_tokens = tokens, + .num_experts = 256, + .stride_gm = logits.stride(0), + .stride_wm = topk_weights.stride(0), + .stride_im = topk_ids.stride(0), + .router_output_multiplier = static_cast(multiplier), + .route_scale = static_cast(route_scale), + .division_eps = static_cast(division_eps), + }; + if (tokens <= 2048) { + const bool pdl = get_device_prop()->major >= 9; + if (pdl) + launch_routing<1, true>(params, stream); + else + launch_routing<1, false>(params, stream); + } else { + launch_routing<4, false>(params, stream); + } + }); +} diff --git a/csrc/moe/topk_softmax_kernels.cu b/csrc/moe/topk_softmax_kernels.cu index abacf6502b1c..b687e95f75e8 100644 --- a/csrc/moe/topk_softmax_kernels.cu +++ b/csrc/moe/topk_softmax_kernels.cu @@ -627,6 +627,8 @@ void topkGatingLauncherHelper(const InputType* input, const bool* finished, floa dim3 block_dim(WARP_SIZE_PARAM, WARPS_PER_TB); #ifndef USE_ROCM + // Pre-SM90 cannot use PDL, but the attribute still adds a programmatic edge to captured CUDA graphs. + static const bool use_pdl = get_device_prop()->major >= 9; cudaLaunchConfig_t config{}; config.gridDim = dim3(num_blocks); config.blockDim = block_dim; @@ -635,7 +637,7 @@ void topkGatingLauncherHelper(const InputType* input, const bool* finished, floa attr.id = cudaLaunchAttributeProgrammaticStreamSerialization; attr.val.programmaticStreamSerializationAllowed = 1; config.attrs = &attr; - config.numAttrs = 1; + config.numAttrs = use_pdl ? 1 : 0; auto err = cudaLaunchKernelEx( &config, topkGating, diff --git a/csrc/moe/torch_bindings.cpp b/csrc/moe/torch_bindings.cpp index 9e8907afd714..3e2ee68a5eaa 100644 --- a/csrc/moe/torch_bindings.cpp +++ b/csrc/moe/torch_bindings.cpp @@ -23,6 +23,13 @@ STABLE_TORCH_LIBRARY_FRAGMENT(_moe_C, m) { "bias, Tensor? input_ids, Tensor? tid2eid, Tensor? is_padding, " "Tensor? bias_vl=None, int image_sentinel_lo=0) -> ()"); +#ifdef USE_CUDA + m.def( + "muse_spark_routing(Tensor! topk_weights, Tensor! topk_ids, " + "Tensor logits, Tensor bias, float multiplier, float route_scale, " + "float division_eps, Tensor? is_padding) -> ()"); +#endif + // Calculate the result of moe by summing up the partial results // from all selected experts. topk_ids/expert_map are optional and, when // both given, enable pad-aware reduce that skips (token, expert) @@ -144,7 +151,8 @@ STABLE_TORCH_LIBRARY_FRAGMENT(_moe_C, m) { m.def( "grouped_topk(Tensor scores, int n_group, int " "topk_group, int topk, bool renormalize, float " - "routed_scaling_factor, Tensor bias, int scoring_func) -> (Tensor, " + "routed_scaling_factor, Tensor bias, int scoring_func, Tensor? " + "is_padding=None) -> (Tensor, " "Tensor)"); #endif } @@ -152,6 +160,9 @@ STABLE_TORCH_LIBRARY_FRAGMENT(_moe_C, m) { STABLE_TORCH_LIBRARY_IMPL(_moe_C, CUDA, m) { m.impl("topk_softmax", TORCH_BOX(&topk_softmax)); m.impl("topk_sigmoid", TORCH_BOX(&topk_sigmoid)); +#ifdef USE_CUDA + m.impl("muse_spark_routing", TORCH_BOX(&muse_spark_routing)); +#endif m.impl("topk_softplus_sqrt", TORCH_BOX(&topk_softplus_sqrt)); m.impl("moe_sum", TORCH_BOX(&moe_sum)); m.impl("moe_align_block_size", TORCH_BOX(&moe_align_block_size)); diff --git a/csrc/ops.h b/csrc/ops.h index 401f6b4996ab..802de7f550e7 100644 --- a/csrc/ops.h +++ b/csrc/ops.h @@ -413,7 +413,18 @@ void fused_minimax_m3_qknorm_rope_kv_insert( const std::string& kv_cache_dtype, bool skip_index_branch, std::optional q_fp8_out, double q_fp8_scale, std::optional kv_k_scale, - std::optional kv_v_scale); + std::optional kv_v_scale, bool write_qkv); + +void fused_minimax_m3_dspark_context_kv( + torch::stable::Tensor const& all_kv, + torch::stable::Tensor const& k_norm_weight, + torch::stable::Tensor const& cos_sin_cache, + torch::stable::Tensor const& positions, + torch::stable::Tensor const& slot_mapping, torch::stable::Tensor& kv_cache, + torch::stable::Tensor const& cache_offsets, + torch::stable::Tensor const& k_scale, + torch::stable::Tensor const& scale_offsets, int64_t rotary_dim, double eps, + const std::string& kv_cache_dtype); #ifdef VLLM_ENABLE_FUSED_KDA_DECODE void fused_kda_decode( @@ -746,13 +757,15 @@ void hisparse_resolve_residency( std::optional const& swap_device_physical_rows, std::optional const& swap_counts, std::optional const& resident_block_table, - int64_t resident_block_size, int64_t resident_null_block); + int64_t resident_block_size, int64_t resident_null_block, + std::optional const& num_valid_rows); void hisparse_invalidate_written_slots( - torch::stable::Tensor& device_global_indices, + torch::stable::Tensor const& device_global_indices_ptrs, torch::stable::Tensor const& request_state_indices, torch::stable::Tensor const& req_id_per_token, - torch::stable::Tensor const& written_slots); + torch::stable::Tensor const& written_slots, int64_t num_state_rows, + int64_t region_stride); void hisparse_gather_plan( torch::stable::Tensor const& host_cache, torch::stable::Tensor& hot_cache, @@ -871,3 +884,12 @@ void ngram_compute_n_gram_ids( torch::stable::Tensor& exclusive_req_len_sums, torch::stable::Tensor& ne_token_table, torch::stable::Tensor& row_indices, torch::stable::Tensor& column_starts, torch::stable::Tensor& n_gram_ids); + +void fused_mqa_topk_prefill( + torch::stable::Tensor q, torch::stable::Tensor k, + torch::stable::Tensor scales, torch::stable::Tensor weights, + torch::stable::Tensor starts, torch::stable::Tensor ends, + torch::stable::Tensor out, torch::stable::Tensor counter, + torch::stable::Tensor values, torch::stable::Tensor indices, + torch::stable::Tensor retained, torch::stable::Tensor retained_indices, + torch::stable::Tensor counts, torch::stable::Tensor defer); diff --git a/csrc/persistent_topk.cuh b/csrc/persistent_topk.cuh index bd18483b48d0..30ffed044561 100644 --- a/csrc/persistent_topk.cuh +++ b/csrc/persistent_topk.cuh @@ -65,8 +65,9 @@ struct RadixRowState { // Kernel parameters // ============================================================================ +template struct PersistentTopKParams { - const float* __restrict__ input; // [num_rows, stride] + const InT* __restrict__ input; // [num_rows, stride] int32_t* __restrict__ output; // [num_rows, top_k] const int32_t* __restrict__ lengths; // [num_rows] RadixRowState* row_states; // large path: per-group state @@ -150,9 +151,9 @@ __device__ __forceinline__ void select_coarse_bin( // kernel // Returns where exact recovery resumes when the threshold bin overflows the // candidate buffer; .needed is false otherwise. -template +template __device__ __noinline__ overflow::MediumResume histogram_medium_topk( - const float* __restrict__ logits, int* __restrict__ output_indices, + const InT* __restrict__ logits, int* __restrict__ output_indices, int seq_len) { // All shared state lives in dynamic shared memory to avoid static extern __shared__ char medium_smem[]; @@ -291,7 +292,8 @@ __device__ __noinline__ overflow::MediumResume histogram_medium_topk( if (remaining_k == 0) { for (int i = thread_id; i < num_buffered; i += kThreadsPerBlock) { const int idx = buffered_indices[src_buffer][i]; - const uint32_t fp32_bits = convert_to_uint32_v2(logits[idx]); + const uint32_t fp32_bits = + convert_to_uint32_v2(topk_histogram_4096::to_float(logits[idx])); const int bin = (fp32_bits >> bit_offset) & 0xFF; if (bin > threshold_bin) { const int output_pos = atomicAdd(&shared_output_count, 1); @@ -310,7 +312,7 @@ __device__ __noinline__ overflow::MediumResume histogram_medium_topk( for (int i = thread_id; i < num_buffered; i += kThreadsPerBlock) { const int idx = buffered_indices[src_buffer][i]; - const float logit_value = logits[idx]; + const float logit_value = topk_histogram_4096::to_float(logits[idx]); const uint32_t fp32_bits = convert_to_uint32_v2(logit_value); const int bin = (fp32_bits >> bit_offset) & 0xFF; if (bin > threshold_bin) { @@ -400,8 +402,8 @@ __device__ __forceinline__ void wait_ge(int* ptr, int target_val, // Adapted from https://github.com/flashinfer-ai/flashinfer/pull/2215 // ============================================================================ -template -__device__ void radix_topk(const float* __restrict__ row_input, +template +__device__ void radix_topk(const InT* __restrict__ row_input, int32_t* __restrict__ row_output, uint32_t seq_len, uint32_t my_chunk_start, uint32_t chunk_size, uint32_t* local_histogram, uint32_t* suffix_sum, @@ -421,24 +423,26 @@ __device__ void radix_topk(const float* __restrict__ row_input, for (uint32_t i = tx * VEC_SIZE; i < aligned_size; i += kThreadsPerBlock * VEC_SIZE) { - const float* src = row_input + my_chunk_start + i; + const InT* src = row_input + my_chunk_start + i; if constexpr (VEC_SIZE == 4) { - float4 v = *reinterpret_cast(src); + float4 v = topk_histogram_4096::load_score4(src); shared_ordered[i] = convert_to_uint32_v2(v.x); shared_ordered[i + 1] = convert_to_uint32_v2(v.y); shared_ordered[i + 2] = convert_to_uint32_v2(v.z); shared_ordered[i + 3] = convert_to_uint32_v2(v.w); } else if constexpr (VEC_SIZE == 2) { - float2 v = *reinterpret_cast(src); + float2 v = topk_histogram_4096::load_score2(src); shared_ordered[i] = convert_to_uint32_v2(v.x); shared_ordered[i + 1] = convert_to_uint32_v2(v.y); } else { - shared_ordered[i] = convert_to_uint32_v2(*src); + shared_ordered[i] = + convert_to_uint32_v2(topk_histogram_4096::to_float(*src)); } } for (uint32_t i = aligned_size + tx; i < actual_chunk_size; i += kThreadsPerBlock) { - shared_ordered[i] = convert_to_uint32_v2(row_input[my_chunk_start + i]); + shared_ordered[i] = convert_to_uint32_v2( + topk_histogram_4096::to_float(row_input[my_chunk_start + i])); } } __syncthreads(); @@ -609,9 +613,9 @@ __device__ void radix_topk(const float* __restrict__ row_input, // Persistent kernel — short/medium/large paths with RadixTopK. // ============================================================================ -template +template __global__ void __launch_bounds__(kThreadsPerBlock, 2) - persistent_topk_kernel(PersistentTopKParams params) { + persistent_topk_kernel(PersistentTopKParams params) { const uint32_t tx = threadIdx.x; extern __shared__ uint8_t smem_raw[]; @@ -675,7 +679,7 @@ __global__ void __launch_bounds__(kThreadsPerBlock, 2) const uint32_t seq_len = non_negative_len < row_bound ? non_negative_len : row_bound; int32_t* row_output = params.output + row_idx * params.top_k; - const float* row_input = params.input + row_idx * params.stride; + const InT* row_input = params.input + row_idx * params.stride; if (seq_len <= RADIX_THRESHOLD) { if (cta_in_group == 0) { @@ -736,10 +740,11 @@ struct vec_t { FLASHINFER_INLINE T& operator[](size_t i) { return data[i]; } FLASHINFER_INLINE const T& operator[](size_t i) const { return data[i]; } - FLASHINFER_INLINE void cast_load(const T* ptr) { + template + FLASHINFER_INLINE void cast_load(const InT* ptr) { #pragma unroll for (size_t i = 0; i < N; ++i) { - data[i] = ptr[i]; + data[i] = topk_histogram_4096::to_float(ptr[i]); } } }; @@ -802,7 +807,7 @@ __device__ __forceinline__ bool filtered_topk_row( extern __shared__ __align__(16) uint8_t dynamic_smem[]; constexpr uint32_t BLOCK_SIZE = FILTERED_TOPK_BLOCK_THREADS; constexpr int SMEM_INPUT_SIZE = FILTERED_TOPK_SMEM_INPUT_SIZE; - using Traits = FilteredTopKTraits; + using Traits = FilteredTopKTraits; constexpr int RADIX = 1 << Traits::REFINE_BITS; static_assert(RADIX == FilteredTopKStorage::RADIX); constexpr int COARSE_RADIX = Traits::COARSE_RADIX; @@ -853,7 +858,7 @@ __device__ __forceinline__ bool filtered_topk_row( } __syncthreads(); - vec_t score_vec; + vec_t score_vec; const int aligned_length = (length / VEC_SIZE) * VEC_SIZE; #pragma unroll 2 @@ -868,7 +873,7 @@ __device__ __forceinline__ bool filtered_topk_row( } // Handle tail for (int i = aligned_length + tx; i < length; i += BLOCK_SIZE) { - const auto bin = Traits::ToCoarseKey(score[i]); + const auto bin = Traits::ToCoarseKey(hist4096::to_float(score[i])); atomicAdd(&s_coarse_histogram[bin], 1); } __syncthreads(); @@ -920,7 +925,8 @@ __device__ __forceinline__ bool filtered_topk_row( } // Handle tail for (int i = aligned_length + tx; i < length; i += BLOCK_SIZE) { - const auto bin = static_cast(Traits::ToCoarseKey(score[i])); + const auto bin = + static_cast(Traits::ToCoarseKey(hist4096::to_float(score[i]))); if (bin > threshold_bin) { const auto pos = atomicAdd(&s_counter, 1); s_indices[pos] = i; @@ -972,7 +978,7 @@ __device__ __forceinline__ bool filtered_topk_row( } // Handle tail for (int i = aligned_length + tx; i < length; i += BLOCK_SIZE) { - filter_and_add_to_histogram(score[i], i); + filter_and_add_to_histogram(hist4096::to_float(score[i]), i); } __syncthreads(); @@ -1007,7 +1013,8 @@ __device__ __forceinline__ bool filtered_topk_row( for (int i = tx; i < num_input; i += BLOCK_SIZE) { const auto idx = s_input_idx[r_idx][i]; const auto bin = - (Traits::ToOrdered(score[idx]) >> offset) & (RADIX - 1); + (Traits::ToOrdered(hist4096::to_float(score[idx])) >> offset) & + (RADIX - 1); if (static_cast(bin) > threshold) { const auto pos = atomicAdd(&s_counter, 1); s_indices[pos] = idx; @@ -1023,7 +1030,7 @@ __device__ __forceinline__ bool filtered_topk_row( __syncthreads(); for (int i = tx; i < num_input; i += BLOCK_SIZE) { const auto idx = s_input_idx[r_idx][i]; - const auto raw_input = score[idx]; + const float raw_input = hist4096::to_float(score[idx]); const auto bin = (Traits::ToOrdered(raw_input) >> offset) & (RADIX - 1); if (static_cast(bin) > threshold) { @@ -1100,7 +1107,7 @@ constexpr uint32_t gcd(uint32_t a, uint32_t b) { // Returns 1, 2, 4, or 8 template constexpr int ComputeFilteredTopKVecSize(uint32_t max_len) { - constexpr int MAX_VEC = 16 / sizeof(DType); // 4 for float32, 8 for fp16/bf16 + constexpr int MAX_VEC = 4; // Use GCD to find largest power-of-2 divisor const uint32_t g = gcd(max_len, static_cast(MAX_VEC)); return static_cast(g); @@ -1114,7 +1121,7 @@ cudaError_t FilteredTopKRaggedTransform(const DType* input, uint32_t max_len, cudaStream_t stream = 0) { constexpr size_t smem_size = FILTERED_TOPK_SMEM_DYNAMIC; - constexpr int MAX_VEC = 16 / sizeof(DType); + constexpr int MAX_VEC = 4; dim3 grid(num_rows); dim3 block(FILTERED_TOPK_BLOCK_THREADS); diff --git a/csrc/persistent_topk_overflow.cuh b/csrc/persistent_topk_overflow.cuh index 9ae116998e89..306dcfe263a5 100644 --- a/csrc/persistent_topk_overflow.cuh +++ b/csrc/persistent_topk_overflow.cuh @@ -19,10 +19,10 @@ struct MediumResume { uint32_t remaining = 0; }; -template -__device__ __noinline__ void recover_medium(const float* scores, - int32_t* output, uint32_t length, - void* smem, uint32_t prefix, +template +__device__ __noinline__ void recover_medium(const InT* scores, int32_t* output, + uint32_t length, void* smem, + uint32_t prefix, uint32_t remaining) { topk_histogram_4096::exact_topk_rescan( scores, output, length, smem, prefix, remaining); @@ -70,7 +70,9 @@ __device__ bool try_sampled_exact_topk(const DType* __restrict__ scores, IdType* __restrict__ output, uint32_t length, uint32_t top_k, void* dynamic_smem, int* scratch) { - using Traits = FilteredTopKTraits; + using Traits = + FilteredTopKTraits; using Radix = SampledRadixDigits; constexpr uint32_t kItemsPerThread = 2; constexpr uint32_t kSampleSize = BlockSize * kItemsPerThread; @@ -92,7 +94,8 @@ __device__ bool try_sampled_exact_topk(const DType* __restrict__ scores, const uint32_t score_idx = static_cast( (static_cast(2 * sample_idx + 1) * length) / (2 * kSampleSize)); - sample_keys[i] = Traits::ToOrdered(scores[score_idx]); + sample_keys[i] = + Traits::ToOrdered(topk_histogram_4096::to_float(scores[score_idx])); } auto* sort_smem = @@ -127,14 +130,14 @@ __device__ bool try_sampled_exact_topk(const DType* __restrict__ scores, static_cast(CandidateCapacity) * kSampleSize) { return false; } - const auto collect_candidate = [&](uint32_t idx, DType score) { + const auto collect_candidate = [&](uint32_t idx, float score) { if (Traits::ToOrdered(score) >= proposed_pivot) { const int pos = atomicAdd(candidate_count, 1); if (pos < static_cast(CandidateCapacity)) candidates[pos] = idx; } }; - vec_t score_vec; + vec_t score_vec; const uint32_t aligned_length = length / VecSize * VecSize; for (uint32_t base = tx * VecSize; base < aligned_length; base += BlockSize * VecSize) { @@ -145,7 +148,7 @@ __device__ bool try_sampled_exact_topk(const DType* __restrict__ scores, } } for (uint32_t idx = aligned_length + tx; idx < length; idx += BlockSize) { - collect_candidate(idx, scores[idx]); + collect_candidate(idx, topk_histogram_4096::to_float(scores[idx])); } __syncthreads(); @@ -157,8 +160,8 @@ __device__ bool try_sampled_exact_topk(const DType* __restrict__ scores, auto* candidate_keys = candidates + CandidateCapacity; for (int i = tx; i < num_candidates; i += BlockSize) { - candidate_keys[i] = - static_cast(Traits::ToOrdered(scores[candidates[i]])); + candidate_keys[i] = static_cast(Traits::ToOrdered( + topk_histogram_4096::to_float(scores[candidates[i]]))); } if (tx == 0) { diff --git a/csrc/quantization/fp4/activation_nvfp4_quant_fusion_kernels.cu b/csrc/quantization/fp4/activation_nvfp4_quant_fusion_kernels.cu index 2064775111fb..269e50cfdf4a 100644 --- a/csrc/quantization/fp4/activation_nvfp4_quant_fusion_kernels.cu +++ b/csrc/quantization/fp4/activation_nvfp4_quant_fusion_kernels.cu @@ -38,7 +38,7 @@ namespace vllm { template __global__ void __launch_bounds__(512, VLLM_BLOCKS_PER_SM(512)) silu_mul_cvt_fp16_to_fp4(int32_t numRows, int32_t numCols, - int32_t num_packed_cols, + int32_t num_packed_cols, int32_t num_padded_cols, Type const* __restrict__ in, float const* __restrict__ SFScale, uint32_t* __restrict__ out, @@ -49,9 +49,16 @@ __global__ void __launch_bounds__(512, VLLM_BLOCKS_PER_SM(512)) static_assert(sizeof(PackedVec) == sizeof(Type) * CVT_FP4_ELTS_PER_THREAD, "Vec size is not matched."); +#if (defined(__CUDA_ARCH__) && (__CUDA_ARCH__ >= 900)) + cudaGridDependencySynchronize(); + cudaTriggerProgrammaticLaunchCompletion(); +#endif + // Precompute SF layout parameter (constant for entire kernel). int32_t const numKTiles = (numCols + 63) / 64; + int const sf_m = round_up(numRows, 128); + // Get the global scaling factor, which will be applied to the SF. // Note SFScale is the same as next GEMM's alpha, which is // (448.f / (Alpha_A / 6.f)). @@ -60,9 +67,10 @@ __global__ void __launch_bounds__(512, VLLM_BLOCKS_PER_SM(512)) int32_t const colIdx = blockDim.x * blockIdx.y + threadIdx.x; int elem_idx = colIdx * CVT_FP4_ELTS_PER_THREAD; - // Input tensor row/col loops. - for (int rowIdx = blockIdx.x; rowIdx < numRows; rowIdx += gridDim.x) { - if (colIdx < num_packed_cols) { + // Iterate over all rows and cols including padded ones - + // ensures we visit every single scale factor address to initialize it. + for (int rowIdx = blockIdx.x; rowIdx < sf_m; rowIdx += gridDim.x) { + if (colIdx < num_padded_cols) { PackedVec in_vec; PackedVec in_vec2; int64_t inOffset = @@ -70,7 +78,8 @@ __global__ void __launch_bounds__(512, VLLM_BLOCKS_PER_SM(512)) int64_t inOffset2 = rowIdx * (numCols * 2 / CVT_FP4_ELTS_PER_THREAD) + numCols / CVT_FP4_ELTS_PER_THREAD + colIdx; - bool valid = (rowIdx < numRows) && (elem_idx < numCols); + // If we are outside valid rows/columns, feed zeros + bool valid = (rowIdx < numRows) && (colIdx < num_packed_cols); if constexpr (CVT_FP4_PACK16) { ld256_cg_or_zero(reinterpret_cast(in_vec), &reinterpret_cast(in)[inOffset * 8], @@ -95,9 +104,17 @@ __global__ void __launch_bounds__(512, VLLM_BLOCKS_PER_SM(512)) CVT_FP4_NUM_THREADS_PER_SF>( rowIdx, colIdx, numKTiles, SFout); - auto out_val = - cvt_warp_fp16_to_fp4( - out_silu_mul, SFScaleVal, sf_out); + fp4_packed_t out_val{}; + if (valid) { + out_val = + cvt_warp_fp16_to_fp4( + out_silu_mul, SFScaleVal, sf_out); + } else if (sf_out != nullptr) { + // The swizzled scale tensor may be allocated with torch.empty. + // Explicitly initialize padded rows and columns in this kernel so + // downstream NVFP4 GEMMs never observe stale scale factors. + *sf_out = 0; + } if (valid) { if constexpr (CVT_FP4_PACK16) { @@ -108,7 +125,7 @@ __global__ void __launch_bounds__(512, VLLM_BLOCKS_PER_SM(512)) } else { int64_t outOffset = rowIdx * (numCols / CVT_FP4_ELTS_PER_THREAD) + colIdx; - out[outOffset] = out_val; + reinterpret_cast(out)[outOffset] = out_val; } } } @@ -146,18 +163,41 @@ void silu_and_mul_nvfp4_quant_sm1xxa( int num_packed_cols = int(n / CVT_FP4_ELTS_PER_THREAD); - int grid_y = vllm::div_round_up(num_packed_cols, static_cast(block.x)); - int grid_x = std::min( - int(m), std::max(1, (multiProcessorCount * numBlocksPerSM) / grid_y)); + // The swizzled scale tensor is padded to 128 rows x 4 scale columns; the + // grid covers the padding so the kernel can initialize it. + int sf_n_int = int(vllm::round_up(n / CVT_FP4_SF_VEC_SIZE, 4) / 4); + int num_padded_cols = + sf_n_int * 4 * CVT_FP4_SF_VEC_SIZE / CVT_FP4_ELTS_PER_THREAD; + + int grid_y = vllm::div_round_up(num_padded_cols, static_cast(block.x)); + int grid_x = + std::min(vllm::computeEffectiveRows(m), + std::max(1, (multiProcessorCount * numBlocksPerSM) / grid_y)); dim3 grid(grid_x, grid_y); + // PDL launch pays off at small M; at large M the early-launched blocks + // slow the chain down, so keep the serialized launch there. + bool const use_pdl = + int64_t(m) * num_packed_cols <= int64_t(multiProcessorCount) * 4096; + VLLM_STABLE_DISPATCH_HALF_TYPES( input.scalar_type(), "silu_and_mul_nvfp4_quant_kernel", [&] { using cuda_type = vllm::CUDATypeConverter::Type; auto input_ptr = static_cast(input.data_ptr()); - vllm::silu_mul_cvt_fp16_to_fp4<<>>( - m, n, num_packed_cols, input_ptr, input_sf_ptr, - reinterpret_cast(output_ptr), - reinterpret_cast(sf_out)); + cudaLaunchConfig_t config = {}; + config.gridDim = grid; + config.blockDim = block; + config.dynamicSmemBytes = 0; + config.stream = stream; + cudaLaunchAttribute attrs[1]; + attrs[0].id = cudaLaunchAttributeProgrammaticStreamSerialization; + attrs[0].val.programmaticStreamSerializationAllowed = 1; + config.numAttrs = use_pdl ? 1 : 0; + config.attrs = attrs; + cudaLaunchKernelEx(&config, vllm::silu_mul_cvt_fp16_to_fp4, + m, n, num_packed_cols, num_padded_cols, input_ptr, + input_sf_ptr, + reinterpret_cast(output_ptr), + reinterpret_cast(sf_out)); }); } diff --git a/csrc/quantization/w8a8/fp8/common.cu b/csrc/quantization/w8a8/fp8/common.cu index 471499786977..837948fdd2a7 100644 --- a/csrc/quantization/w8a8/fp8/common.cu +++ b/csrc/quantization/w8a8/fp8/common.cu @@ -95,7 +95,7 @@ __global__ void scaled_fp8_quant_kernel_strided_group_shape( int size, float inv_scale) { vectorize_with_alignment( in, out, size, tid, blockDim.x, - [=] __device__(fp8_type & dst, const scalar_t& src) { + [=] __device__(fp8_type& dst, const scalar_t& src) { dst = scaled_fp8_conversion(static_cast(src), inv_scale); }); @@ -194,7 +194,7 @@ __global__ void scaled_fp8_quant_kernel_strided_dynamic( const float reciprocal_scale = 1.0f / (*scale); vectorize_with_alignment<16>( token_in, token_out, hidden_size, tid, blockDim.x, - [=] __device__(fp8_type & dst, const scalar_t& src) { + [=] __device__(fp8_type& dst, const scalar_t& src) { dst = scaled_fp8_conversion(static_cast(src), reciprocal_scale); }); @@ -238,7 +238,7 @@ __global__ void dynamic_per_token_scaled_fp8_quant_kernel_strided( // 2) quantize vectorize_with_alignment<16>( token_in, token_out, hidden_size, tid, blockDim.x, - [=] __device__(fp8_type & dst, const scalar_t& src) { + [=] __device__(fp8_type& dst, const scalar_t& src) { dst = scaled_fp8_conversion(static_cast(src), token_scale); }); diff --git a/csrc/quantization/w8a8/fp8/per_token_group_quant.cu b/csrc/quantization/w8a8/fp8/per_token_group_quant.cu index 40d3f92efb39..162a2d879f67 100644 --- a/csrc/quantization/w8a8/fp8/per_token_group_quant.cu +++ b/csrc/quantization/w8a8/fp8/per_token_group_quant.cu @@ -49,7 +49,7 @@ __device__ __forceinline__ float ComputeGroupScale( constexpr int vec_size = 16 / sizeof(T); // copy global -> shared & compute absmax - auto scalar_op_cache = [&] __device__(T & dst, const T& src) { + auto scalar_op_cache = [&] __device__(T& dst, const T& src) { float abs_v = fabsf(static_cast(src)); local_absmax = fmaxf(local_absmax, abs_v); dst = src; @@ -85,7 +85,7 @@ __device__ __forceinline__ void QuantizeGroup( const float inv_y = 1.0f / y_s; // quantize shared -> global 8-bit - auto scalar_op_quant = [&] __device__(DST_DTYPE & dst, const T& src) { + auto scalar_op_quant = [&] __device__(DST_DTYPE& dst, const T& src) { float q = fminf(fmaxf(static_cast(src) * inv_y, min_8bit), max_8bit); dst = DST_DTYPE(q); }; diff --git a/csrc/rocm/.gitignore b/csrc/rocm/.gitignore new file mode 100644 index 000000000000..afdfc475e0f1 --- /dev/null +++ b/csrc/rocm/.gitignore @@ -0,0 +1,7 @@ +# Generated at configure time by generate_paged_attn.py. +attention_paged_attn_*.cu +attention_paged_attn_entries.inc +attention_paged_attn_dispatch.inc +# Record of what is on disk (the script hash and the list of files written), so a +# second build directory regenerates instead of silently reusing stale units. +.paged_attn_generated diff --git a/csrc/rocm/attention_dispatch.cu b/csrc/rocm/attention_dispatch.cu index 53859c77e15e..3bb6a7ca6faf 100644 --- a/csrc/rocm/attention_dispatch.cu +++ b/csrc/rocm/attention_dispatch.cu @@ -17,31 +17,23 @@ // Runtime dispatch for the ROCm paged attention kernels. // // The kernels themselves (and the expensive template instantiation they -// trigger) live in attention_paged_attn_*.cu, one translation unit per -// (dtype, kv-cache dtype, head size) combination, so ninja compiles them in -// parallel instead of serializing the whole attention kernel set onto a single -// core. +// trigger) live in the generated attention_paged_attn_*.cu translation units, +// one per (dtype, kv-cache dtype, head size, block size) combination, so ninja +// compiles them in parallel instead of serializing the whole attention kernel +// set onto a single core. Those translation units, their prototypes and the +// dispatch below are all emitted from the variant list in +// csrc/rocm/generate_paged_attn.py. #include #include "attention_kernels.cuh" #include "ops.h" -// One prototype per entry point. Adding one here is useless unless -// paged_attention() below also dispatches it (checked by CMakeLists.txt). -ATTN_ENTRY(paged_attention_h64_f16_auto); -ATTN_ENTRY(paged_attention_h128_f16_auto); -ATTN_ENTRY(paged_attention_h64_bf16_auto); -ATTN_ENTRY(paged_attention_h128_bf16_auto); -ATTN_ENTRY(paged_attention_h64_f16_fp8_f16); -ATTN_ENTRY(paged_attention_h128_f16_fp8_f16); -ATTN_ENTRY(paged_attention_h64_f16_fp8_fp8); -ATTN_ENTRY(paged_attention_h128_f16_fp8_fp8); -ATTN_ENTRY(paged_attention_h64_bf16_fp8_f16); -ATTN_ENTRY(paged_attention_h128_bf16_fp8_f16); -ATTN_ENTRY(paged_attention_h64_bf16_fp8_fp8); -ATTN_ENTRY(paged_attention_h128_bf16_fp8_fp8); +// One prototype per generated entry point. +#include "attention_paged_attn_entries.inc" -// gfx11/gfx12 (RDNA) need a different kernel set than gfx9 (CDNA). +// gfx11/gfx12 (RDNA) need a different kernel set than gfx9 (CDNA). Keep this +// predicate in sync with the arch filter in CMakeLists.txt, which defines +// VLLM_ROCM_ATTN_NO_NAVI when no Navi arch is targeted. bool is_navi_gpu() { static bool is_cached = false; static bool result; @@ -60,22 +52,6 @@ bool is_navi_gpu() { return result; } -// Each ATTN_HEAD_SWITCH() call below is a dispatch arm: it names the entry -// point that paged_attention() calls, which is what makes the corresponding -// attention_paged_attn_*.cu file reachable. -#define ATTN_HEAD_SWITCH(FN64, FN128) \ - switch (head_size) { \ - case 64: \ - FN64(ATTN_FWD); \ - break; \ - case 128: \ - FN128(ATTN_FWD); \ - break; \ - default: \ - TORCH_CHECK(false, "Unsupported head size: ", head_size); \ - break; \ - } - // clang-format off void paged_attention( torch::Tensor& out, // [num_seqs, num_heads, head_size] @@ -100,43 +76,11 @@ void paged_attention( // clang-format on bool is_navi = is_navi_gpu(); const int head_size = query.size(2); - if (kv_cache_dtype == "auto") { - if (query.dtype() == at::ScalarType::Half) { - ATTN_HEAD_SWITCH(paged_attention_h64_f16_auto, - paged_attention_h128_f16_auto); - } else if (query.dtype() == at::ScalarType::BFloat16) { - ATTN_HEAD_SWITCH(paged_attention_h64_bf16_auto, - paged_attention_h128_bf16_auto); - } else { - TORCH_CHECK(false, "Unsupported data type: ", query.dtype()); - } - } else if (kv_cache_dtype == "fp8" || kv_cache_dtype == "fp8_e4m3") { - if (query.dtype() == at::ScalarType::Half) { - if (mfma_type == "fp8") { - ATTN_HEAD_SWITCH(paged_attention_h64_f16_fp8_fp8, - paged_attention_h128_f16_fp8_fp8); - } else { - ATTN_HEAD_SWITCH(paged_attention_h64_f16_fp8_f16, - paged_attention_h128_f16_fp8_f16); - } - } else if (query.dtype() == at::ScalarType::BFloat16) { - if (mfma_type == "fp8") { - ATTN_HEAD_SWITCH(paged_attention_h64_bf16_fp8_fp8, - paged_attention_h128_bf16_fp8_fp8); - } else { - ATTN_HEAD_SWITCH(paged_attention_h64_bf16_fp8_f16, - paged_attention_h128_bf16_fp8_f16); - } - } else { - TORCH_CHECK(false, "Unsupported data type: ", query.dtype()); - } - } else { - TORCH_CHECK(false, "Unsupported KV cache dtype: ", kv_cache_dtype); - } + // clang-format off + #include "attention_paged_attn_dispatch.inc" + // clang-format on } -#undef ATTN_HEAD_SWITCH #undef ATTN_ENTRY -#undef ATTN_LAUNCH #undef ATTN_FWD #undef ATTN_PARAMS diff --git a/csrc/rocm/attention_kernels.cuh b/csrc/rocm/attention_kernels.cuh index 42539faa0d73..d17acc74d808 100644 --- a/csrc/rocm/attention_kernels.cuh +++ b/csrc/rocm/attention_kernels.cuh @@ -16,9 +16,16 @@ #pragma once -#include +// Only the ATen/c10 surface is needed here (at::Tensor, the CUDA stream/guard +// helpers and TORCH_CHECK). also drags in autograd, nn and jit, +// which is ~7 s of the ~9.5 s frontend of every attention translation unit. +#include +#include #include #include +#include + +#include #include #include #include "core/cuda_compat.h" @@ -3433,13 +3440,13 @@ template void paged_attention_custom_launcher( - torch::Tensor& out, torch::Tensor& exp_sums, torch::Tensor& max_logits, - torch::Tensor& tmp_out, torch::Tensor& query, torch::Tensor& key_cache, - torch::Tensor& value_cache, const int num_kv_heads, float scale, - torch::Tensor& block_tables, torch::Tensor& seq_lens, - const std::optional& query_start_loc, int max_seq_len, - const std::optional& alibi_slopes, torch::Tensor& k_scale, - torch::Tensor& v_scale, const std::optional& fp8_out_scale) { + at::Tensor& out, at::Tensor& exp_sums, at::Tensor& max_logits, + at::Tensor& tmp_out, at::Tensor& query, at::Tensor& key_cache, + at::Tensor& value_cache, const int num_kv_heads, float scale, + at::Tensor& block_tables, at::Tensor& seq_lens, + const std::optional& query_start_loc, int max_seq_len, + const std::optional& alibi_slopes, at::Tensor& k_scale, + at::Tensor& v_scale, const std::optional& fp8_out_scale) { int num_seqs = block_tables.size(0); int num_heads = query.size(1); int head_size = query.size(2); @@ -3589,13 +3596,13 @@ template void paged_attention_custom_launcher_navi( - torch::Tensor& out, torch::Tensor& exp_sums, torch::Tensor& max_logits, - torch::Tensor& tmp_out, torch::Tensor& query, torch::Tensor& key_cache, - torch::Tensor& value_cache, const int num_kv_heads, float scale, - torch::Tensor& block_tables, torch::Tensor& seq_lens, - const std::optional& query_start_loc, int max_seq_len, - const std::optional& alibi_slopes, torch::Tensor& k_scale, - torch::Tensor& v_scale) { + at::Tensor& out, at::Tensor& exp_sums, at::Tensor& max_logits, + at::Tensor& tmp_out, at::Tensor& query, at::Tensor& key_cache, + at::Tensor& value_cache, const int num_kv_heads, float scale, + at::Tensor& block_tables, at::Tensor& seq_lens, + const std::optional& query_start_loc, int max_seq_len, + const std::optional& alibi_slopes, at::Tensor& k_scale, + at::Tensor& v_scale) { int num_seqs = block_tables.size(0); int num_heads = query.size(1); int head_size = query.size(2); @@ -3766,19 +3773,46 @@ void paged_attention_custom_launcher_navi( // compatibility; a TORCH_CHECK prevents dispatch to NAVI with interleaved V. // TODO: implement interleaved V-cache support in the NAVI (GFX11/GFX12) // kernels if shuffle KV cache layout is needed on RDNA GPUs. -#define CALL_CUSTOM_LAUNCHER(T, KVT, KV_DTYPE, BLK_SIZE, HEAD_SIZE, OUTT, \ - PSIZE, ALIBI_ENABLED, MFMA_TYPE) \ - if (use_interleaved_v_cache) { \ - TORCH_CHECK(!is_navi, \ - "Reading interleaved V-cache layout from paged decode " \ - "attention not supported on NAVI GPUs"); \ - paged_attention_custom_launcher( \ - out, exp_sums, max_logits, tmp_out, query, key_cache, value_cache, \ - num_kv_heads, scale, block_tables, seq_lens, query_start_loc, \ - max_seq_len, alibi_slopes, k_scale, v_scale, fp8_out_scale); \ - } else { \ +// +// BLK_SIZE/HEAD_SIZE are baked into the generated entry points (see +// generate_paged_attn.py); ALIBI_ENABLED, the out dtype, +// use_interleaved_v_cache and is_navi stay runtime branches, see the NOTE +// below. +#define CALL_CUSTOM_LAUNCHER(T, KVT, KV_DTYPE, BLK_SIZE, HEAD_SIZE, OUTT, \ + PSIZE, ALIBI_ENABLED, MFMA_TYPE) \ + if (use_interleaved_v_cache) { \ + TORCH_CHECK(!is_navi, \ + "Reading interleaved V-cache layout from paged decode " \ + "attention not supported on NAVI GPUs"); \ + paged_attention_custom_launcher( \ + out, exp_sums, max_logits, tmp_out, query, key_cache, value_cache, \ + num_kv_heads, scale, block_tables, seq_lens, query_start_loc, \ + max_seq_len, alibi_slopes, k_scale, v_scale, fp8_out_scale); \ + } else { \ + ATTN_CALL_NON_INTERLEAVED(T, KVT, KV_DTYPE, BLK_SIZE, HEAD_SIZE, OUTT, \ + PSIZE, ALIBI_ENABLED, MFMA_TYPE) \ + } + +// The non-interleaved path picks the CDNA or the Navi launcher at runtime. With +// no gfx11/gfx12 arch CMake defines VLLM_ROCM_ATTN_NO_NAVI: is_navi_gpu() can +// then never be true, so the Navi set is dead weight and is not instantiated. +#ifdef VLLM_ROCM_ATTN_NO_NAVI + #define ATTN_CALL_NON_INTERLEAVED(T, KVT, KV_DTYPE, BLK_SIZE, HEAD_SIZE, \ + OUTT, PSIZE, ALIBI_ENABLED, MFMA_TYPE) \ + TORCH_CHECK(!is_navi, \ + "Navi (gfx11/gfx12) GPUs are not supported by this build: " \ + "its arch list has no Navi code objects"); \ + paged_attention_custom_launcher( \ + out, exp_sums, max_logits, tmp_out, query, key_cache, value_cache, \ + num_kv_heads, scale, block_tables, seq_lens, query_start_loc, \ + max_seq_len, alibi_slopes, k_scale, v_scale, fp8_out_scale); +#else + #define ATTN_CALL_NON_INTERLEAVED(T, KVT, KV_DTYPE, BLK_SIZE, HEAD_SIZE, \ + OUTT, PSIZE, ALIBI_ENABLED, MFMA_TYPE) \ if (!is_navi) { \ paged_attention_custom_launcher&query_start_loc, int64_t max_seq_len, \ - const std::optional&alibi_slopes, torch::Tensor &k_scale, \ - torch::Tensor &v_scale, \ - const std::optional&fp8_out_scale, int64_t block_size, \ +// the generated attention_paged_attn_*.cu files. The names must match what +// CALL_CUSTOM_LAUNCHER* expect to find in scope. block_size is not part of it: +// it is part of the entry point name, so that each translation unit only +// instantiates the kernels of one block size and paged_attention() switches on +// it. +#define ATTN_PARAMS \ + at::Tensor &out, at::Tensor &exp_sums, at::Tensor &max_logits, \ + at::Tensor &tmp_out, at::Tensor &query, at::Tensor &key_cache, \ + at::Tensor &value_cache, int64_t num_kv_heads, double scale, \ + at::Tensor &block_tables, at::Tensor &seq_lens, \ + const std::optional&query_start_loc, int64_t max_seq_len, \ + const std::optional&alibi_slopes, at::Tensor &k_scale, \ + at::Tensor &v_scale, const std::optional&fp8_out_scale, \ bool is_navi, bool use_interleaved_v_cache -#define ATTN_FWD \ - out, exp_sums, max_logits, tmp_out, query, key_cache, value_cache, \ - num_kv_heads, scale, block_tables, seq_lens, query_start_loc, \ - max_seq_len, alibi_slopes, k_scale, v_scale, fp8_out_scale, block_size, \ - is_navi, use_interleaved_v_cache +#define ATTN_FWD \ + out, exp_sums, max_logits, tmp_out, query, key_cache, value_cache, \ + num_kv_heads, scale, block_tables, seq_lens, query_start_loc, \ + max_seq_len, alibi_slopes, k_scale, v_scale, fp8_out_scale, is_navi, \ + use_interleaved_v_cache -// Declare/define one fixed-head-size entry point. A definition in one of the -// attention_paged_attn_*.cu files is reachable only if attention_dispatch.cu -// names it in one of its ATTN_HEAD_SWITCH() calls (CMakeLists.txt fails if one -// is missing). +// Declare/define one entry point. The generated attention_paged_attn_*.cu files +// define one, and attention_paged_attn_entries.inc declares them; both are +// emitted from the same variant list, so an entry point cannot be compiled +// without being dispatched. #define ATTN_ENTRY(fn) void fn(ATTN_PARAMS) -// Instantiate one entry point (one per attention_paged_attn_*.cu file), for -// every supported block size. Kept as an alias of CALL_CUSTOM_LAUNCHER_BLK so -// the block-size switch has a single definition; the head-size switch lives in -// attention_dispatch.cu. -#define ATTN_LAUNCH(T, KVT, KV_DTYPE, HEAD_SIZE, MFMA_TYPE) \ - CALL_CUSTOM_LAUNCHER_BLK(T, KVT, KV_DTYPE, HEAD_SIZE, MFMA_TYPE) - #undef WARP_SIZE #undef MAX #undef MIN diff --git a/csrc/rocm/attention_paged_attn_h128_bf16_auto.cu b/csrc/rocm/attention_paged_attn_h128_bf16_auto.cu deleted file mode 100644 index 536af4557185..000000000000 --- a/csrc/rocm/attention_paged_attn_h128_bf16_auto.cu +++ /dev/null @@ -1,9 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// SPDX-FileCopyrightText: Copyright contributors to the vLLM project - -#include "attention_kernels.cuh" - -ATTN_ENTRY(paged_attention_h128_bf16_auto) { - ATTN_LAUNCH(__hip_bfloat16, __hip_bfloat16, vllm::Fp8KVCacheDataType::kAuto, - 128, MFMAType::F16); -} diff --git a/csrc/rocm/attention_paged_attn_h128_bf16_fp8_f16.cu b/csrc/rocm/attention_paged_attn_h128_bf16_fp8_f16.cu deleted file mode 100644 index b183a378a861..000000000000 --- a/csrc/rocm/attention_paged_attn_h128_bf16_fp8_f16.cu +++ /dev/null @@ -1,9 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// SPDX-FileCopyrightText: Copyright contributors to the vLLM project - -#include "attention_kernels.cuh" - -ATTN_ENTRY(paged_attention_h128_bf16_fp8_f16) { - ATTN_LAUNCH(__hip_bfloat16, uint8_t, vllm::Fp8KVCacheDataType::kFp8E4M3, 128, - MFMAType::F16); -} diff --git a/csrc/rocm/attention_paged_attn_h128_bf16_fp8_fp8.cu b/csrc/rocm/attention_paged_attn_h128_bf16_fp8_fp8.cu deleted file mode 100644 index 913d9f018963..000000000000 --- a/csrc/rocm/attention_paged_attn_h128_bf16_fp8_fp8.cu +++ /dev/null @@ -1,9 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// SPDX-FileCopyrightText: Copyright contributors to the vLLM project - -#include "attention_kernels.cuh" - -ATTN_ENTRY(paged_attention_h128_bf16_fp8_fp8) { - ATTN_LAUNCH(__hip_bfloat16, uint8_t, vllm::Fp8KVCacheDataType::kFp8E4M3, 128, - MFMAType::Fp8); -} diff --git a/csrc/rocm/attention_paged_attn_h128_f16_auto.cu b/csrc/rocm/attention_paged_attn_h128_f16_auto.cu deleted file mode 100644 index 1a38303f4d7f..000000000000 --- a/csrc/rocm/attention_paged_attn_h128_f16_auto.cu +++ /dev/null @@ -1,9 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// SPDX-FileCopyrightText: Copyright contributors to the vLLM project - -#include "attention_kernels.cuh" - -ATTN_ENTRY(paged_attention_h128_f16_auto) { - ATTN_LAUNCH(_Float16, _Float16, vllm::Fp8KVCacheDataType::kAuto, 128, - MFMAType::F16); -} diff --git a/csrc/rocm/attention_paged_attn_h128_f16_fp8_f16.cu b/csrc/rocm/attention_paged_attn_h128_f16_fp8_f16.cu deleted file mode 100644 index 62fb1928f2af..000000000000 --- a/csrc/rocm/attention_paged_attn_h128_f16_fp8_f16.cu +++ /dev/null @@ -1,9 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// SPDX-FileCopyrightText: Copyright contributors to the vLLM project - -#include "attention_kernels.cuh" - -ATTN_ENTRY(paged_attention_h128_f16_fp8_f16) { - ATTN_LAUNCH(_Float16, uint8_t, vllm::Fp8KVCacheDataType::kFp8E4M3, 128, - MFMAType::F16); -} diff --git a/csrc/rocm/attention_paged_attn_h128_f16_fp8_fp8.cu b/csrc/rocm/attention_paged_attn_h128_f16_fp8_fp8.cu deleted file mode 100644 index 21afad1cbebd..000000000000 --- a/csrc/rocm/attention_paged_attn_h128_f16_fp8_fp8.cu +++ /dev/null @@ -1,9 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// SPDX-FileCopyrightText: Copyright contributors to the vLLM project - -#include "attention_kernels.cuh" - -ATTN_ENTRY(paged_attention_h128_f16_fp8_fp8) { - ATTN_LAUNCH(_Float16, uint8_t, vllm::Fp8KVCacheDataType::kFp8E4M3, 128, - MFMAType::Fp8); -} diff --git a/csrc/rocm/attention_paged_attn_h64_bf16_auto.cu b/csrc/rocm/attention_paged_attn_h64_bf16_auto.cu deleted file mode 100644 index 9d33b250df8d..000000000000 --- a/csrc/rocm/attention_paged_attn_h64_bf16_auto.cu +++ /dev/null @@ -1,9 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// SPDX-FileCopyrightText: Copyright contributors to the vLLM project - -#include "attention_kernels.cuh" - -ATTN_ENTRY(paged_attention_h64_bf16_auto) { - ATTN_LAUNCH(__hip_bfloat16, __hip_bfloat16, vllm::Fp8KVCacheDataType::kAuto, - 64, MFMAType::F16); -} diff --git a/csrc/rocm/attention_paged_attn_h64_bf16_fp8_f16.cu b/csrc/rocm/attention_paged_attn_h64_bf16_fp8_f16.cu deleted file mode 100644 index 10c74d77b479..000000000000 --- a/csrc/rocm/attention_paged_attn_h64_bf16_fp8_f16.cu +++ /dev/null @@ -1,9 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// SPDX-FileCopyrightText: Copyright contributors to the vLLM project - -#include "attention_kernels.cuh" - -ATTN_ENTRY(paged_attention_h64_bf16_fp8_f16) { - ATTN_LAUNCH(__hip_bfloat16, uint8_t, vllm::Fp8KVCacheDataType::kFp8E4M3, 64, - MFMAType::F16); -} diff --git a/csrc/rocm/attention_paged_attn_h64_bf16_fp8_fp8.cu b/csrc/rocm/attention_paged_attn_h64_bf16_fp8_fp8.cu deleted file mode 100644 index 8992c7143232..000000000000 --- a/csrc/rocm/attention_paged_attn_h64_bf16_fp8_fp8.cu +++ /dev/null @@ -1,9 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// SPDX-FileCopyrightText: Copyright contributors to the vLLM project - -#include "attention_kernels.cuh" - -ATTN_ENTRY(paged_attention_h64_bf16_fp8_fp8) { - ATTN_LAUNCH(__hip_bfloat16, uint8_t, vllm::Fp8KVCacheDataType::kFp8E4M3, 64, - MFMAType::Fp8); -} diff --git a/csrc/rocm/attention_paged_attn_h64_f16_auto.cu b/csrc/rocm/attention_paged_attn_h64_f16_auto.cu deleted file mode 100644 index b65c197de732..000000000000 --- a/csrc/rocm/attention_paged_attn_h64_f16_auto.cu +++ /dev/null @@ -1,9 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// SPDX-FileCopyrightText: Copyright contributors to the vLLM project - -#include "attention_kernels.cuh" - -ATTN_ENTRY(paged_attention_h64_f16_auto) { - ATTN_LAUNCH(_Float16, _Float16, vllm::Fp8KVCacheDataType::kAuto, 64, - MFMAType::F16); -} diff --git a/csrc/rocm/attention_paged_attn_h64_f16_fp8_f16.cu b/csrc/rocm/attention_paged_attn_h64_f16_fp8_f16.cu deleted file mode 100644 index 6c5897ece5e3..000000000000 --- a/csrc/rocm/attention_paged_attn_h64_f16_fp8_f16.cu +++ /dev/null @@ -1,9 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// SPDX-FileCopyrightText: Copyright contributors to the vLLM project - -#include "attention_kernels.cuh" - -ATTN_ENTRY(paged_attention_h64_f16_fp8_f16) { - ATTN_LAUNCH(_Float16, uint8_t, vllm::Fp8KVCacheDataType::kFp8E4M3, 64, - MFMAType::F16); -} diff --git a/csrc/rocm/attention_paged_attn_h64_f16_fp8_fp8.cu b/csrc/rocm/attention_paged_attn_h64_f16_fp8_fp8.cu deleted file mode 100644 index 573a58f19f47..000000000000 --- a/csrc/rocm/attention_paged_attn_h64_f16_fp8_fp8.cu +++ /dev/null @@ -1,9 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// SPDX-FileCopyrightText: Copyright contributors to the vLLM project - -#include "attention_kernels.cuh" - -ATTN_ENTRY(paged_attention_h64_f16_fp8_fp8) { - ATTN_LAUNCH(_Float16, uint8_t, vllm::Fp8KVCacheDataType::kFp8E4M3, 64, - MFMAType::Fp8); -} diff --git a/csrc/rocm/generate_paged_attn.py b/csrc/rocm/generate_paged_attn.py new file mode 100644 index 000000000000..802c48565b76 --- /dev/null +++ b/csrc/rocm/generate_paged_attn.py @@ -0,0 +1,435 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +"""Generate the ROCm paged attention translation units and their dispatch. + +Instantiating these kernels dominates the _rocm_C build: one file holding every +(dtype, kv-cache dtype, head size, block size, ...) combination serialized +~10 min of device code generation onto one core. + +All outputs derive from VARIANTS: + + * attention_paged_attn__bs.cu: one unit per (variant, + block size), each instantiating only its slice (~48 s per unit, vs ~95 s for + a whole set), so ninja runs them in parallel, + * attention_paged_attn_entries.inc: entry-point prototypes (file scope), + * attention_paged_attn_dispatch.inc: paged_attention()'s dispatch (function + body). + +So a unit cannot be compiled without being dispatched - what the configure-time +guard in CMakeLists.txt used to regex for. Outputs land next to the kernels (like +marlin's generate_kernels.py) and are gitignored. +""" + +import glob +import hashlib +import os +import sys + +# Block sizes the kernels are instantiated for (see CALL_CUSTOM_LAUNCHER in +# attention_kernels.cuh). One translation unit is generated per (variant, block +# size). +BLOCK_SIZES = [16, 32] + +# ALIBI_ENABLED and the out dtype stay runtime branches (CALL_CUSTOM_LAUNCHER_OUT) +# instead of getting units of their own: see the NOTE in attention_kernels.cuh. + +# One entry per supported (query dtype, kv-cache dtype, head size) kernel set: +# query_type/kv_type: C++ types of the query/out and of the key/value cache +# (template parameters T and KVT), written with the HIP +# spellings hipify produces; `__half` is a distinct type +# from `_Float16` for the MFMA builtins. +# kv_cache_enum/head_size/mfma: Fp8KVCacheDataType, head size and MFMAType +# query_scalar_type: at::ScalarType of the query this variant serves +# kv_cache_dtype/mfma_type: the runtime strings this variant serves; mfma_type +# is None when the kv-cache dtype does not split on it. +VARIANTS = [ + { + "query_type": "_Float16", + "kv_type": "_Float16", + "kv_cache_enum": "vllm::Fp8KVCacheDataType::kAuto", + "head_size": 64, + "mfma": "MFMAType::F16", + "query_scalar_type": "Half", + "kv_cache_dtype": "auto", + "mfma_type": None, + }, + { + "query_type": "_Float16", + "kv_type": "_Float16", + "kv_cache_enum": "vllm::Fp8KVCacheDataType::kAuto", + "head_size": 128, + "mfma": "MFMAType::F16", + "query_scalar_type": "Half", + "kv_cache_dtype": "auto", + "mfma_type": None, + }, + { + "query_type": "__hip_bfloat16", + "kv_type": "__hip_bfloat16", + "kv_cache_enum": "vllm::Fp8KVCacheDataType::kAuto", + "head_size": 64, + "mfma": "MFMAType::F16", + "query_scalar_type": "BFloat16", + "kv_cache_dtype": "auto", + "mfma_type": None, + }, + { + "query_type": "__hip_bfloat16", + "kv_type": "__hip_bfloat16", + "kv_cache_enum": "vllm::Fp8KVCacheDataType::kAuto", + "head_size": 128, + "mfma": "MFMAType::F16", + "query_scalar_type": "BFloat16", + "kv_cache_dtype": "auto", + "mfma_type": None, + }, + { + "query_type": "_Float16", + "kv_type": "uint8_t", + "kv_cache_enum": "vllm::Fp8KVCacheDataType::kFp8E4M3", + "head_size": 64, + "mfma": "MFMAType::F16", + "query_scalar_type": "Half", + "kv_cache_dtype": "fp8", + "mfma_type": "f16", + }, + { + "query_type": "_Float16", + "kv_type": "uint8_t", + "kv_cache_enum": "vllm::Fp8KVCacheDataType::kFp8E4M3", + "head_size": 128, + "mfma": "MFMAType::F16", + "query_scalar_type": "Half", + "kv_cache_dtype": "fp8", + "mfma_type": "f16", + }, + { + "query_type": "_Float16", + "kv_type": "uint8_t", + "kv_cache_enum": "vllm::Fp8KVCacheDataType::kFp8E4M3", + "head_size": 64, + "mfma": "MFMAType::Fp8", + "query_scalar_type": "Half", + "kv_cache_dtype": "fp8", + "mfma_type": "fp8", + }, + { + "query_type": "_Float16", + "kv_type": "uint8_t", + "kv_cache_enum": "vllm::Fp8KVCacheDataType::kFp8E4M3", + "head_size": 128, + "mfma": "MFMAType::Fp8", + "query_scalar_type": "Half", + "kv_cache_dtype": "fp8", + "mfma_type": "fp8", + }, + { + "query_type": "__hip_bfloat16", + "kv_type": "uint8_t", + "kv_cache_enum": "vllm::Fp8KVCacheDataType::kFp8E4M3", + "head_size": 64, + "mfma": "MFMAType::F16", + "query_scalar_type": "BFloat16", + "kv_cache_dtype": "fp8", + "mfma_type": "f16", + }, + { + "query_type": "__hip_bfloat16", + "kv_type": "uint8_t", + "kv_cache_enum": "vllm::Fp8KVCacheDataType::kFp8E4M3", + "head_size": 128, + "mfma": "MFMAType::F16", + "query_scalar_type": "BFloat16", + "kv_cache_dtype": "fp8", + "mfma_type": "f16", + }, + { + "query_type": "__hip_bfloat16", + "kv_type": "uint8_t", + "kv_cache_enum": "vllm::Fp8KVCacheDataType::kFp8E4M3", + "head_size": 64, + "mfma": "MFMAType::Fp8", + "query_scalar_type": "BFloat16", + "kv_cache_dtype": "fp8", + "mfma_type": "fp8", + }, + { + "query_type": "__hip_bfloat16", + "kv_type": "uint8_t", + "kv_cache_enum": "vllm::Fp8KVCacheDataType::kFp8E4M3", + "head_size": 128, + "mfma": "MFMAType::Fp8", + "query_scalar_type": "BFloat16", + "kv_cache_dtype": "fp8", + "mfma_type": "fp8", + }, +] + + +def variant_name(variant): + """File and entry-point suffix, derived so it cannot disagree with the fields.""" + parts = [ + f"h{variant['head_size']}", + {"_Float16": "f16", "__hip_bfloat16": "bf16"}[variant["query_type"]], + variant["kv_cache_dtype"], + ] + if variant["mfma_type"] is not None: + parts.append(variant["mfma_type"]) + return "_".join(parts) + + +for variant in VARIANTS: + variant["name"] = variant_name(variant) + +# kv_cache_dtype values accepted per VARIANTS["kv_cache_dtype"], in dispatch +# order (the first matching arm wins). +KV_CACHE_DTYPE_PREDICATES = { + "auto": 'kv_cache_dtype == "auto"', + "fp8": 'kv_cache_dtype == "fp8" || kv_cache_dtype == "fp8_e4m3"', +} + +OUTPUT_DIR = os.path.dirname(os.path.abspath(__file__)) + +# MD5 of this script, recorded in the stamp so CMake can tell whether the files +# on disk were produced by the current version (same MD5 as `file(MD5 ...)`). +# CMake passes the key it compares against so the format is defined in one place; +# a manual run falls back to the hash above. +with open(__file__, "rb") as _self: + # A cache key only: a FIPS-enforcing host rejects md5 without this flag. + SCRIPT_HASH = hashlib.md5(_self.read(), usedforsecurity=False).hexdigest() +KEY = sys.argv[1] if len(sys.argv) > 1 else SCRIPT_HASH +STAMP_NAME = ".paged_attn_generated" + +# Whether the Navi (gfx11/gfx12) kernel set is instantiated is a compile +# definition decided by CMake (`VLLM_ROCM_ATTN_NO_NAVI`, see CMakeLists.txt), so +# the units written here are identical for every build directory and arch list. + +FILE_HEAD = """\ +// auto generated by generate_paged_attn.py +// clang-format off +""" + +TU_HEAD = ( + FILE_HEAD + + """ +#include "attention_kernels.cuh" +""" +) + +TU_ENTRY = """ +ATTN_ENTRY(paged_attention_{name}_bs{block_size}) {{ + CALL_CUSTOM_LAUNCHER_OUT({query_type}, {kv_type}, {kv_cache_enum}, + {block_size}, {head_size}, {mfma}); +}} +""" + +ENTRIES_HEAD = ( + FILE_HEAD + + """ +// Prototypes of the entry points defined in the generated +// attention_paged_attn_*.cu translation units. +""" +) + +DISPATCH_HEAD = ( + FILE_HEAD + + """ + // Dispatch on kv_cache_dtype, query dtype, mfma_type, block size and head + // size, mirroring the template parameters of the generated entry points. +""" +) + + +def entry_point(variant, block_size): + return f"paged_attention_{variant['name']}_bs{block_size}" + + +def block_size_switch(variants, indent_level): + """Switch on block_size then head_size for one dispatch group. + + `variants` is the list of variants sharing (kv_cache_dtype, query dtype, + mfma_type); they differ in head size only. + """ + indent = " " * indent_level + inner = " " * (indent_level + 4) + lines = [f"{indent}switch (block_size) {{"] + for block_size in BLOCK_SIZES: + lines.append(f"{indent} case {block_size}:") + lines.append(f"{inner}switch (head_size) {{") + for variant in variants: + lines.append(f"{inner} case {variant['head_size']}:") + lines.append(f"{inner} {entry_point(variant, block_size)}(ATTN_FWD);") + lines.append(f"{inner} break;") + lines.append(f"{inner} default:") + lines.append( + f'{inner} TORCH_CHECK(false, "Unsupported head size: ", head_size);' + ) + lines.append(f"{inner} break;") + lines.append(f"{inner}}}") + lines.append(f"{indent} break;") + lines.append(f"{indent} default:") + lines.append( + f'{indent} TORCH_CHECK(false, "Unsupported block size: ", block_size);' + ) + lines.append(f"{indent} break;") + lines.append(f"{indent}}}") + return lines + + +def group_variants(): + """Group VARIANTS by (kv_cache_dtype, query dtype, mfma_type), in order.""" + groups = {} + for variant in VARIANTS: + key = ( + variant["kv_cache_dtype"], + variant["query_scalar_type"], + variant["mfma_type"], + ) + groups.setdefault(key, []).append(variant) + return groups + + +def generate_dispatch(): + groups = group_variants() + # kv_cache_dtype -> query dtype -> mfma_type -> variants + by_kv_cache_dtype = {} + for (kv_cache_dtype, query_scalar_type, mfma_type), variants in groups.items(): + by_kv_cache_dtype.setdefault(kv_cache_dtype, {}).setdefault( + query_scalar_type, {} + )[mfma_type] = variants + + lines = [DISPATCH_HEAD.rstrip("\n")] + kv_cache_dtypes = list(by_kv_cache_dtype) + for index, kv_cache_dtype in enumerate(kv_cache_dtypes): + keyword = "if" if index == 0 else "else if" + predicate = KV_CACHE_DTYPE_PREDICATES[kv_cache_dtype] + lines.append(f" {keyword} ({predicate}) {{") + + by_query_dtype = by_kv_cache_dtype[kv_cache_dtype] + for dtype_index, (query_scalar_type, by_mfma) in enumerate( + by_query_dtype.items() + ): + dtype_keyword = "if" if dtype_index == 0 else "else if" + lines.append( + f" {dtype_keyword} (query.dtype() == " + f"at::ScalarType::{query_scalar_type}) {{" + ) + + mfma_types = list(by_mfma) + # A lone arm is the catch-all and takes the no-split path: None when + # mfma_type is not a dimension here, "f16" when it is the only arm left + # (a bare `else {` would otherwise open a block with no `if`). + if mfma_types in ([None], ["f16"]): + lines.extend(block_size_switch(by_mfma[mfma_types[0]], 6)) + else: + # ``mfma_type`` is matched with a plain ``else`` on the last arm, + # as the hand-written dispatch used to: any value other than the + # recognised ones keeps selecting that arm instead of failing. + if "f16" not in mfma_types: + raise SystemExit( + f"the {mfma_types} group needs an f16 arm, which is " + "emitted as the catch-all else" + ) + ordered = sorted(mfma_types, key=lambda m: m == "f16") # f16 last + for mfma_index, mfma_type in enumerate(ordered): + if mfma_index == len(ordered) - 1: + lines.append(" else {") + else: + mfma_keyword = "if" if mfma_index == 0 else "else if" + lines.append( + f' {mfma_keyword} (mfma_type == "{mfma_type}") {{' + ) + lines.extend(block_size_switch(by_mfma[mfma_type], 8)) + lines.append(" }") + lines.append(" }") + + lines.append(" else {") + lines.append( + ' TORCH_CHECK(false, "Unsupported data type: ", query.dtype());' + ) + lines.append(" }") + lines.append(" }") + + lines.append(" else {") + lines.append( + ' TORCH_CHECK(false, "Unsupported KV cache dtype: ", kv_cache_dtype);' + ) + lines.append(" }") + return "\n".join(lines) + "\n" + + +def generate_entries(): + lines = [ENTRIES_HEAD.rstrip("\n")] + for variant in VARIANTS: + for block_size in BLOCK_SIZES: + lines.append(f"ATTN_ENTRY({entry_point(variant, block_size)});") + return "\n".join(lines) + "\n" + + +def generate_tu(variant, block_size): + return TU_HEAD + TU_ENTRY.format(block_size=block_size, **variant) + + +def remove_old_files(): + # Only remove files this script produced: a name match alone would delete a + # developer's scratch copy in the same directory. + for path in glob.glob(os.path.join(OUTPUT_DIR, "attention_paged_attn_*.cu")): + with open(path) as fd: + if fd.readline() != FILE_HEAD.splitlines()[0] + "\n": + continue + os.remove(path) + for name in ( + "attention_paged_attn_entries.inc", + "attention_paged_attn_dispatch.inc", + ): + path = os.path.join(OUTPUT_DIR, name) + if os.path.exists(path): + os.remove(path) + + +def write(name, content): + with open(os.path.join(OUTPUT_DIR, name), "w") as fd: + fd.write(content) + + +def check_dispatch_covers_entries(entries, dispatch): + # Cheap replacement for the configure-time guard that used to regex + # attention_dispatch.cu: every instantiated entry point must be named by the + # generated dispatch, otherwise its unit is compiled and linked for nothing. + for name in entries: + if f"{name}(" not in dispatch: + raise SystemExit( + f"{name} is instantiated but never dispatched: add it to " + "VARIANTS ordering/generate_dispatch(), or drop it." + ) + + +def main(): + remove_old_files() + outputs = [] + entries = [] + for variant in VARIANTS: + for block_size in BLOCK_SIZES: + name = f"attention_paged_attn_{variant['name']}_bs{block_size}.cu" + write(name, generate_tu(variant, block_size)) + outputs.append(name) + entries.append(entry_point(variant, block_size)) + entries_inc = generate_entries() + dispatch_inc = generate_dispatch() + check_dispatch_covers_entries(entries, dispatch_inc) + for name, content in ( + ("attention_paged_attn_entries.inc", entries_inc), + ("attention_paged_attn_dispatch.inc", dispatch_inc), + ): + write(name, content) + outputs.append(name) + # Record what is on disk next to the outputs: CMake takes this list as its + # source list and compares the key (this script's hash) so that units left by + # another version of the script are regenerated. The key is arch-independent. + write(STAMP_NAME, KEY + "\n" + "\n".join(outputs) + "\n") + print(f"Generated {len(outputs)} paged attention files.") + + +if __name__ == "__main__": + main() diff --git a/csrc/sampled_topk.cuh b/csrc/sampled_topk.cuh index c25f8ed8e731..9923dcbf417e 100644 --- a/csrc/sampled_topk.cuh +++ b/csrc/sampled_topk.cuh @@ -69,9 +69,8 @@ __device__ void select_bin(Storage& s) { // Find a coarse bin, then refine its full FP32 keys one byte at a time. // Read shared candidates normally, or the full row for exact fallback. -template -__device__ uint32_t threshold(Storage& s, const float* row, int count, - int rank) { +template +__device__ uint32_t threshold(Storage& s, const InT* row, int count, int rank) { const int tid = threadIdx.x; if (tid == 0) { s.remaining = rank; @@ -81,7 +80,8 @@ __device__ uint32_t threshold(Storage& s, const float* row, int count, __syncthreads(); for (int i = tid; i < count; i += kThreads) { const uint32_t key = - Buffered ? s.candidates[i].y : hist4096::convert_to_uint32_v2(row[i]); + Buffered ? s.candidates[i].y + : hist4096::convert_to_uint32_v2(hist4096::to_float(row[i])); atomicAdd(&s.histogram[coarse(key)], 1); } __syncthreads(); @@ -94,7 +94,8 @@ __device__ uint32_t threshold(Storage& s, const float* row, int count, const uint32_t prefix = s.prefix; for (int i = tid; i < count; i += kThreads) { const uint32_t key = - Buffered ? s.candidates[i].y : hist4096::convert_to_uint32_v2(row[i]); + Buffered ? s.candidates[i].y + : hist4096::convert_to_uint32_v2(hist4096::to_float(row[i])); if (coarse(key) == coarse_bin && (key & mask) == prefix) { atomicAdd(&s.histogram[(key >> shift) & 255], 1); } @@ -118,8 +119,8 @@ __device__ __forceinline__ int reserve(bool hit, int* counter) { return base + __popc(mask & ((1u << lane) - 1)); } -template -__device__ void emit(Storage& s, const float* row, int32_t* dst, int count, +template +__device__ void emit(Storage& s, const InT* row, int32_t* dst, int count, uint32_t cutoff) { const int tid = threadIdx.x; if (tid == 0) s.count = 0; @@ -127,10 +128,10 @@ __device__ void emit(Storage& s, const float* row, int32_t* dst, int count, for (int base = 0; base < count; base += kThreads) { const int i = base + tid; const uint2 pair = - i < count - ? (Buffered ? s.candidates[i] - : make_uint2(i, hist4096::convert_to_uint32_v2(row[i]))) - : make_uint2(0, 0); + i < count ? (Buffered ? s.candidates[i] + : make_uint2(i, hist4096::convert_to_uint32_v2( + hist4096::to_float(row[i])))) + : make_uint2(0, 0); bool keep = i < count && pair.y > cutoff; if (i < count && pair.y == cutoff) { keep = atomicSub(&s.remaining, 1) > 0; @@ -140,9 +141,9 @@ __device__ void emit(Storage& s, const float* row, int32_t* dst, int count, } } -template +template __global__ void __launch_bounds__(kThreads) - sampled_topk_kernel(const float* __restrict__ input, + sampled_topk_kernel(const InT* __restrict__ input, const int32_t* __restrict__ lengths, int32_t* __restrict__ output, int64_t stride, int max_length) { @@ -150,7 +151,7 @@ __global__ void __launch_bounds__(kThreads) auto& s = *reinterpret_cast(smem); const int tid = threadIdx.x; const int length = max(0, min(lengths[blockIdx.x], max_length)); - const float* row = input + blockIdx.x * stride; + const InT* row = input + blockIdx.x * stride; int32_t* dst = output + blockIdx.x * K; // Graph allocations can hold rows much shorter than the host length bound. if (length < kMinSampledLength) { @@ -158,11 +159,11 @@ __global__ void __launch_bounds__(kThreads) bool selected; if ((stride & 3) == 0 && (reinterpret_cast(row) & 15) == 0) { selected = - filtered_topk::filtered_topk_row( + filtered_topk::filtered_topk_row( row, dst, length, K, fallback_storage); } else { selected = - filtered_topk::filtered_topk_row( + filtered_topk::filtered_topk_row( row, dst, length, K, fallback_storage); } if (!selected) { @@ -177,7 +178,8 @@ __global__ void __launch_bounds__(kThreads) // Sample contiguous warps spread across the row to keep loads coalesced. for (int i = tid; i < kSample; i += kThreads) { const int index = (i / 32) * (length / (kSample / 32)) + i % 32; - atomicAdd(&s.histogram[coarse(hist4096::convert_to_uint32_v2(row[index]))], + atomicAdd(&s.histogram[coarse(hist4096::convert_to_uint32_v2( + hist4096::to_float(row[index])))], 1); } __syncthreads(); @@ -200,7 +202,7 @@ __global__ void __launch_bounds__(kThreads) #pragma unroll for (int j = 0; j < kItems / 4; ++j) { const float4 values = - reinterpret_cast(row + base)[tid + j * kThreads]; + hist4096::load_score4(row + base + (tid + j * kThreads) * 4); keys[4 * j] = hist4096::convert_to_uint32_v2(values.x); keys[4 * j + 1] = hist4096::convert_to_uint32_v2(values.y); keys[4 * j + 2] = hist4096::convert_to_uint32_v2(values.z); @@ -210,7 +212,10 @@ __global__ void __launch_bounds__(kThreads) #pragma unroll for (int j = 0; j < kItems; ++j) { const int i = base + tid * 4 + (j / 4) * kThreads * 4 + j % 4; - keys[j] = i < length ? hist4096::convert_to_uint32_v2(row[i]) : 0; + keys[j] = + i < length + ? hist4096::convert_to_uint32_v2(hist4096::to_float(row[i])) + : 0; } } #pragma unroll diff --git a/csrc/topk.cu b/csrc/topk.cu index d5ff2a8386eb..1322ca3b0dd4 100644 --- a/csrc/topk.cu +++ b/csrc/topk.cu @@ -15,7 +15,7 @@ namespace { #ifndef USE_ROCM -template +template void launch_persistent_topk(const torch::stable::Tensor& logits, const torch::stable::Tensor& lengths, torch::stable::Tensor& output, @@ -28,6 +28,7 @@ void launch_persistent_topk(const torch::stable::Tensor& logits, const int64_t num_rows = logits.size(0); const int64_t stride = logits.stride(0); const cudaStream_t stream = get_current_cuda_stream(); + const auto* input = reinterpret_cast(logits.const_data_ptr()); static int num_sms = 0; static int max_smem_per_block = 0; @@ -37,11 +38,15 @@ void launch_persistent_topk(const torch::stable::Tensor& logits, max_smem_per_block = device_prop->sharedMemPerBlockOptin; } + STD_TORCH_CHECK( + sizeof(InT) == sizeof(float) || max_smem_per_block >= 128 * 1024, + "bfloat16 persistent_topk requires 128 KiB shared memory"); + // Allow static fallback storage in addition to the 128 KiB dynamic buffer. if (num_rows > 64 && max_seq_len >= vllm::sampled_topk::kMinSampledLength && max_smem_per_block >= 144 * 1024) { - auto kernel = vllm::sampled_topk::sampled_topk_kernel; + auto kernel = vllm::sampled_topk::sampled_topk_kernel; constexpr size_t smem_size = vllm::filtered_topk::FILTERED_TOPK_SMEM_DYNAMIC; cudaError_t status = cudaFuncSetAttribute( @@ -49,15 +54,14 @@ void launch_persistent_topk(const torch::stable::Tensor& logits, STD_TORCH_CHECK(status == cudaSuccess, "sampled_topk smem failed: ", cudaGetErrorString(status)); kernel<<>>( - logits.const_data_ptr(), lengths.const_data_ptr(), + input, lengths.const_data_ptr(), output.mutable_data_ptr(), stride, static_cast(std::min(max_seq_len, logits.size(1)))); } else if (num_rows > 32 && max_smem_per_block >= 128 * 1024) { - cudaError_t status = - vllm::FilteredTopKRaggedTransform( - logits.const_data_ptr(), output.mutable_data_ptr(), - lengths.const_data_ptr(), static_cast(num_rows), - static_cast(TopK), static_cast(stride), stream); + cudaError_t status = vllm::FilteredTopKRaggedTransform( + input, output.mutable_data_ptr(), + lengths.const_data_ptr(), static_cast(num_rows), + static_cast(TopK), static_cast(stride), stream); STD_TORCH_CHECK(status == cudaSuccess, "FilteredTopK failed: ", cudaGetErrorString(status)); } else { @@ -83,8 +87,7 @@ void launch_persistent_topk(const torch::stable::Tensor& logits, static_cast(available_for_ordered / sizeof(uint32_t)); // Views like x[:, 1:] keep the stride but shift the base. - const auto base = - reinterpret_cast(logits.const_data_ptr()); + const auto base = reinterpret_cast(input); uint32_t vec_size = 1; if (stride % 4 == 0 && base % 16 == 0) vec_size = 4; @@ -112,16 +115,16 @@ void launch_persistent_topk(const torch::stable::Tensor& logits, cudaError_t occ_err = cudaSuccess; if (vec_size == 4) { occ_err = cudaOccupancyMaxActiveBlocksPerMultiprocessor( - &occupancy, P::persistent_topk_kernel, P::kThreadsPerBlock, - smem_size); + &occupancy, P::persistent_topk_kernel, + P::kThreadsPerBlock, smem_size); } else if (vec_size == 2) { occ_err = cudaOccupancyMaxActiveBlocksPerMultiprocessor( - &occupancy, P::persistent_topk_kernel, P::kThreadsPerBlock, - smem_size); + &occupancy, P::persistent_topk_kernel, + P::kThreadsPerBlock, smem_size); } else { occ_err = cudaOccupancyMaxActiveBlocksPerMultiprocessor( - &occupancy, P::persistent_topk_kernel, P::kThreadsPerBlock, - smem_size); + &occupancy, P::persistent_topk_kernel, + P::kThreadsPerBlock, smem_size); } STD_TORCH_CHECK(occ_err == cudaSuccess, "persistent_topk occupancy query failed: ", @@ -162,9 +165,8 @@ void launch_persistent_topk(const torch::stable::Tensor& logits, return; } cudaError_t status = - vllm::FilteredTopKRaggedTransform( - logits.const_data_ptr(), - output.mutable_data_ptr(), + vllm::FilteredTopKRaggedTransform( + input, output.mutable_data_ptr(), lengths.const_data_ptr(), static_cast(num_rows), static_cast(TopK), static_cast(stride), stream); @@ -209,8 +211,8 @@ void launch_persistent_topk(const torch::stable::Tensor& logits, "row_states memset failed: ", cudaGetErrorString(mz_err)); } - P::PersistentTopKParams params; - params.input = logits.const_data_ptr(); + P::PersistentTopKParams params; + params.input = input; params.output = output.mutable_data_ptr(); params.lengths = lengths.const_data_ptr(); params.num_rows = static_cast(num_rows); @@ -224,7 +226,7 @@ void launch_persistent_topk(const torch::stable::Tensor& logits, #define LAUNCH_PERSISTENT(TOPK_VAL, VS) \ do { \ - auto kernel = &P::persistent_topk_kernel; \ + auto kernel = &P::persistent_topk_kernel; \ cudaError_t err = cudaFuncSetAttribute( \ kernel, cudaFuncAttributeMaxDynamicSharedMemorySize, smem_size); \ STD_TORCH_CHECK(err == cudaSuccess, \ @@ -246,6 +248,21 @@ void launch_persistent_topk(const torch::stable::Tensor& logits, STD_TORCH_CHECK(err == cudaSuccess, "persistent_topk failed: ", cudaGetErrorString(err)); } + +template +void launch_persistent_topk_dtype(const torch::stable::Tensor& logits, + const torch::stable::Tensor& lengths, + torch::stable::Tensor& output, + torch::stable::Tensor& workspace, + int64_t max_seq_len) { + if (logits.scalar_type() == torch::headeronly::ScalarType::BFloat16) { + launch_persistent_topk(logits, lengths, output, + workspace, max_seq_len); + } else { + launch_persistent_topk(logits, lengths, output, workspace, + max_seq_len); + } +} #endif } // anonymous namespace @@ -259,8 +276,14 @@ void persistent_topk(const torch::stable::Tensor& logits, STD_TORCH_CHECK(logits.is_cuda(), "logits must be CUDA tensor"); STD_TORCH_CHECK(lengths.is_cuda(), "lengths must be CUDA tensor"); STD_TORCH_CHECK(output.is_cuda(), "output must be CUDA tensor"); - STD_TORCH_CHECK(logits.scalar_type() == torch::headeronly::ScalarType::Float, - "Only float32 supported"); + STD_TORCH_CHECK( + logits.scalar_type() == torch::headeronly::ScalarType::Float || + logits.scalar_type() == torch::headeronly::ScalarType::BFloat16, + "logits must be float32 or bfloat16"); + STD_TORCH_CHECK( + logits.scalar_type() != torch::headeronly::ScalarType::BFloat16 || + logits.stride(1) == 1, + "bfloat16 logits columns must be contiguous"); STD_TORCH_CHECK(lengths.scalar_type() == torch::headeronly::ScalarType::Int, "lengths must be int32"); STD_TORCH_CHECK(output.scalar_type() == torch::headeronly::ScalarType::Int, @@ -284,14 +307,14 @@ void persistent_topk(const torch::stable::Tensor& logits, logits.get_device_index()); if (k == 512) { - launch_persistent_topk<512>(logits, lengths, output, workspace, - max_seq_len); + launch_persistent_topk_dtype<512>(logits, lengths, output, workspace, + max_seq_len); } else if (k == 1024) { - launch_persistent_topk<1024>(logits, lengths, output, workspace, - max_seq_len); + launch_persistent_topk_dtype<1024>(logits, lengths, output, workspace, + max_seq_len); } else { - launch_persistent_topk<2048>(logits, lengths, output, workspace, - max_seq_len); + launch_persistent_topk_dtype<2048>(logits, lengths, output, workspace, + max_seq_len); } #else STD_TORCH_CHECK(false, "persistent_topk is not supported on ROCm"); diff --git a/csrc/topk_histogram_4096.cuh b/csrc/topk_histogram_4096.cuh index b5aeb2d78cd6..86e7b680461e 100644 --- a/csrc/topk_histogram_4096.cuh +++ b/csrc/topk_histogram_4096.cuh @@ -5,6 +5,7 @@ #ifndef TOPK_HISTOGRAM_4096_CUH_ #define TOPK_HISTOGRAM_4096_CUH_ +#include #include #include #include @@ -66,6 +67,39 @@ __device__ __forceinline__ void load_float4_predicated(const float* ptr, v3 = __uint_as_float(r3); } +__device__ __forceinline__ float to_float(float x) { return x; } +__device__ __forceinline__ float to_float(__nv_bfloat16 x) { + return __bfloat162float(x); +} + +__device__ __forceinline__ float4 load_score4(const float* ptr) { + return *reinterpret_cast(ptr); +} +__device__ __forceinline__ float4 load_score4(const __nv_bfloat16* ptr) { + const uint2 raw = *reinterpret_cast(ptr); + return make_float4( + __uint_as_float(raw.x << 16), __uint_as_float(raw.x & 0xFFFF0000u), + __uint_as_float(raw.y << 16), __uint_as_float(raw.y & 0xFFFF0000u)); +} +__device__ __forceinline__ float2 load_score2(const float* ptr) { + return *reinterpret_cast(ptr); +} +__device__ __forceinline__ float2 load_score2(const __nv_bfloat16* ptr) { + const uint32_t raw = *reinterpret_cast(ptr); + return make_float2(__uint_as_float(raw << 16), + __uint_as_float(raw & 0xFFFF0000u)); +} + +__device__ __forceinline__ void load_float4_predicated(const __nv_bfloat16* ptr, + int base, int seq_len, + float& v0, float& v1, + float& v2, float& v3) { + v0 = base < seq_len ? to_float(ptr[0]) : __uint_as_float(0xFF800000u); + v1 = base + 1 < seq_len ? to_float(ptr[1]) : __uint_as_float(0xFF800000u); + v2 = base + 2 < seq_len ? to_float(ptr[2]) : __uint_as_float(0xFF800000u); + v3 = base + 3 < seq_len ? to_float(ptr[3]) : __uint_as_float(0xFF800000u); +} + // converts the float32 score to a 32-bit ordered unsigned integer — the full // precision key for radix sorting __device__ __forceinline__ auto convert_to_uint32_v2(float x) -> uint32_t { @@ -333,9 +367,9 @@ struct Histogram4096Smem { // Defined in topk_histogram_4096_overflow.cuh. Only kFp32Rescan instantiations // call it, and their callers include that header. -template +template __device__ __noinline__ void exact_topk_rescan_cold( - const float* __restrict__ scores, int32_t* __restrict__ output, + const InT* __restrict__ scores, int32_t* __restrict__ output, uint32_t length, void* smem); // kRegisterValues refines from the values already held in registers and @@ -349,16 +383,17 @@ __device__ __forceinline__ bool exact_topk_refine_registers( uint32_t length, uint32_t coarse_bin, uint32_t num_above, void* _smem); template -__device__ void exact_topk_rescan(const float* __restrict__ scores, + uint32_t StartRound, uint32_t FinalCandidateCapacity, typename InT> +__device__ void exact_topk_rescan(const InT* __restrict__ scores, int32_t* __restrict__ output, uint32_t length, void* _smem, uint32_t initial_prefix, uint32_t initial_remaining); template -__device__ void histogram_4096_topk(const float* __restrict__ scores, + bool UsePredicatedLoads = false, OverflowRecovery Recovery, + typename InT> +__device__ void histogram_4096_topk(const InT* __restrict__ scores, int32_t* __restrict__ output, uint32_t length, void* _smem) { constexpr uint32_t HIST_BINS = 1 << HIST_BITS; @@ -388,14 +423,14 @@ __device__ void histogram_4096_topk(const float* __restrict__ scores, smem->counter_gt = 0; smem->counter_eq = 0; } - if constexpr (UsePredicatedLoads) { + if constexpr (UsePredicatedLoads || sizeof(InT) == 2) { const bool row_aligned = (reinterpret_cast(scores) & 0xFu) == 0; #pragma unroll for (uint32_t v = 0; v < VECS_PER_THREAD; v++) { const uint32_t base = (tx + v * kBlockSize) * 4; if (base < length) { if (row_aligned && base + 3 < length) { - vecs[v] = *reinterpret_cast(scores + base); + vecs[v] = load_score4(scores + base); } else { load_float4_predicated(scores + base, static_cast(base), static_cast(length), vecs[v].x, vecs[v].y, @@ -408,7 +443,7 @@ __device__ void histogram_4096_topk(const float* __restrict__ scores, for (uint32_t v = 0; v < VECS_PER_THREAD; v++) { const uint32_t base = (tx + v * kBlockSize) * 4; if (base < length) { - vecs[v] = *reinterpret_cast(scores + base); + vecs[v] = load_score4(scores + base); } } } @@ -596,9 +631,9 @@ __device__ void histogram_4096_topk(const float* __restrict__ scores, template + OverflowRecovery Recovery, typename InT> __device__ __noinline__ void histogram_4096_topk_predicated( - const float* __restrict__ scores, int32_t* __restrict__ output, + const InT* __restrict__ scores, int32_t* __restrict__ output, uint32_t length, void* _smem) { histogram_4096_topk( scores, output, length, _smem); diff --git a/csrc/topk_histogram_4096_overflow.cuh b/csrc/topk_histogram_4096_overflow.cuh index 7433a0362acf..f788cadac2c9 100644 --- a/csrc/topk_histogram_4096_overflow.cuh +++ b/csrc/topk_histogram_4096_overflow.cuh @@ -9,15 +9,15 @@ namespace vllm { namespace topk_histogram_4096 { // Visit a row with vectorized loads and a scalar tail. -template -__device__ __forceinline__ void scan_scores(const float* __restrict__ scores, +template +__device__ __forceinline__ void scan_scores(const InT* __restrict__ scores, uint32_t length, Visit visit) { static_assert(VecSize == 1 || VecSize == 2 || VecSize == 4); const uint32_t tx = threadIdx.x; if constexpr (VecSize == 1) { for (uint32_t idx = tx; idx < length; idx += BlockSize) { - visit(idx, scores[idx]); + visit(idx, to_float(scores[idx])); } return; } @@ -27,20 +27,20 @@ __device__ __forceinline__ void scan_scores(const float* __restrict__ scores, for (uint32_t base = tx * VecSize; base < aligned_length; base += BlockSize * VecSize) { if constexpr (VecSize == 4) { - const float4 values = *reinterpret_cast(scores + base); + const float4 values = load_score4(scores + base); visit(base, values.x); visit(base + 1, values.y); visit(base + 2, values.z); visit(base + 3, values.w); } else { - const float2 values = *reinterpret_cast(scores + base); + const float2 values = load_score2(scores + base); visit(base, values.x); visit(base + 1, values.y); } } for (uint32_t idx = aligned_length + tx; idx < length; idx += BlockSize) { - visit(idx, scores[idx]); + visit(idx, to_float(scores[idx])); } } @@ -70,8 +70,9 @@ constexpr uint32_t kExactCandidateOffset = 16 * 1024; // resume at StartRound from a known prefix, and with FinalCandidateCapacity // the last round stages its candidates so the output pass skips a row scan. template -__device__ void exact_topk_rescan(const float* __restrict__ scores, + uint32_t StartRound = 0, uint32_t FinalCandidateCapacity = 0, + typename InT> +__device__ void exact_topk_rescan(const InT* __restrict__ scores, int32_t* __restrict__ output, uint32_t length, void* _smem, uint32_t initial_prefix = 0, uint32_t initial_remaining = TopK) { @@ -215,7 +216,7 @@ __device__ void exact_topk_rescan(const float* __restrict__ scores, if (candidate_count <= FinalCandidateCapacity) { for (uint32_t i = tx; i < candidate_count; i += BlockSize) { const uint32_t idx = final_candidates[i]; - collect(idx, scores[idx]); + collect(idx, to_float(scores[idx])); } } else if (smem->pivot_count == candidate_count) { // Every key under the final prefix equals the pivot (heavy ties), so @@ -240,9 +241,9 @@ __device__ void exact_topk_rescan(const float* __restrict__ scores, __syncthreads(); } -template +template __device__ __noinline__ void exact_topk_rescan_cold( - const float* __restrict__ scores, int32_t* __restrict__ output, + const InT* __restrict__ scores, int32_t* __restrict__ output, uint32_t length, void* smem) { exact_topk_rescan(scores, output, length, smem); } diff --git a/csrc/torch_bindings.cpp b/csrc/torch_bindings.cpp index fef3076056ee..9969f6309b8f 100644 --- a/csrc/torch_bindings.cpp +++ b/csrc/torch_bindings.cpp @@ -8,6 +8,17 @@ // Note: We register under namespace "_C" so ops are accessible as // torch.ops._C. for compatibility with existing code. STABLE_TORCH_LIBRARY_FRAGMENT(_C, ops) { +#ifdef VLLM_ENABLE_FUSED_MQA_TOPK + // Single-launch fused FP8 MQA-logits + top-k (512/1024/2048) prefill + // selection (SM100 family). Workspace tensors are caller-owned; counter must + // be zero. + ops.def( + "fused_mqa_topk_prefill(Tensor q, Tensor k, Tensor scales, " + "Tensor weights, Tensor starts, Tensor ends, Tensor! out, " + "Tensor! counter, Tensor! values, Tensor! indices, Tensor! retained, " + "Tensor! retained_indices, Tensor! counts, Tensor! defer) -> ()"); +#endif + // Compute per-token-group FP8 quantized tensor and scaling factor. // The dummy arguments are here so we can correctly fuse with RMSNorm. ops.def( @@ -538,7 +549,17 @@ STABLE_TORCH_LIBRARY_FRAGMENT(_C, ops) { "int block_size, Tensor!? q_out, Tensor!? index_q_out, " "str kv_cache_dtype, bool skip_index_branch=False, " "Tensor!? q_fp8_out=None, float q_fp8_scale=1.0, " - "Tensor? kv_k_scale=None, Tensor? kv_v_scale=None) -> ()"); + "Tensor? kv_k_scale=None, Tensor? kv_v_scale=None, " + "bool write_qkv=True) -> ()"); + + // MiniMax-M3 DSpark draft: Gemma k-norm + partial NeoX RoPE + K/V cache + // insert for every draft layer's context K/V in one launch. + ops.def( + "fused_minimax_m3_dspark_context_kv(" + "Tensor all_kv, Tensor k_norm_weight, Tensor cos_sin_cache, " + "Tensor positions, Tensor slot_mapping, Tensor! kv_cache, " + "Tensor cache_offsets, Tensor k_scale, Tensor scale_offsets, " + "int rotary_dim, float eps, str kv_cache_dtype) -> ()"); #ifdef VLLM_ENABLE_FUSED_KDA_DECODE ops.def( @@ -782,6 +803,10 @@ STABLE_TORCH_LIBRARY_FRAGMENT(_C, ops) { } STABLE_TORCH_LIBRARY_IMPL(_C, CUDA, ops) { +#ifdef VLLM_ENABLE_FUSED_MQA_TOPK + ops.impl("fused_mqa_topk_prefill", TORCH_BOX(&fused_mqa_topk_prefill)); +#endif + // LongCat n-gram embedding index kernel. ops.impl("ngram_compute_n_gram_ids", TORCH_BOX(&ngram_compute_n_gram_ids)); ops.impl("vocab_parallel_embedding", TORCH_BOX(&vocab_parallel_embedding)); @@ -879,6 +904,8 @@ STABLE_TORCH_LIBRARY_IMPL(_C, CUDA, ops) { TORCH_BOX(&fused_muse_spark_qknorm_rope_kv_insert)); ops.impl("fused_minimax_m3_qknorm_rope_kv_insert", TORCH_BOX(&fused_minimax_m3_qknorm_rope_kv_insert)); + ops.impl("fused_minimax_m3_dspark_context_kv", + TORCH_BOX(&fused_minimax_m3_dspark_context_kv)); #ifdef VLLM_ENABLE_FUSED_KDA_DECODE ops.impl("fused_kda_decode", TORCH_BOX(&fused_kda_decode)); #endif @@ -1064,13 +1091,16 @@ STABLE_TORCH_LIBRARY_FRAGMENT(_C_cache_ops, ops) { " Tensor(h!)? swap_counts=None," " Tensor? resident_block_table=None," " int resident_block_size=0," - " int resident_null_block=0) -> ()"); + " int resident_null_block=0," + " Tensor? num_valid_rows=None) -> ()"); ops.def( - "hisparse_invalidate_written_slots(Tensor! device_global_indices," + "hisparse_invalidate_written_slots(Tensor device_global_indices_ptrs," " Tensor request_state_indices," " Tensor req_id_per_token," - " Tensor written_slots) -> ()"); + " Tensor written_slots," + " int num_state_rows," + " int region_stride) -> ()"); ops.def( "hisparse_gather_plan(Tensor host_cache," diff --git a/docker/Dockerfile b/docker/Dockerfile index f3cac42652e9..ff5b273c56fd 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -93,11 +93,6 @@ ARG UV_KEYRING_PROVIDER=${PIP_KEYRING_PROVIDER} # Flag enables built-in KV-connector dependency libs into docker images ARG INSTALL_KV_CONNECTORS=false -# Flag builds NIXL, including its NIXL EP extension for the installed PyTorch, -# from source and installs it in place of the nixl packages from the KV-connector -# requirements -ARG BUILD_NIXL=false - #################### BASE BUILD IMAGE #################### # prepare basic build environment FROM ${BUILD_BASE_IMAGE} AS base @@ -253,8 +248,13 @@ RUN --mount=type=cache,target=/opt/uv/cache \ && uv pip install --python /opt/venv/bin/python3 -r requirements/cuda.txt \ --extra-index-url ${PYTORCH_CUDA_INDEX_BASE_URL}/nightly/cu$(echo $CUDA_VERSION | cut -d. -f1,2 | tr -d '.'); \ else \ + # Keep the CUDA-specific PyTorch index first. Using it as an extra + # index with unsafe-best-match can select a higher, incompatible + # torch build from the base image (for example cu130 in a cu129 image). uv pip install --python /opt/venv/bin/python3 -r requirements/cuda.txt \ - --extra-index-url ${PYTORCH_CUDA_INDEX_BASE_URL}/cu$(echo $CUDA_VERSION | cut -d. -f1,2 | tr -d '.'); \ + --index-url ${PYTORCH_CUDA_INDEX_BASE_URL}/cu$(echo $CUDA_VERSION | cut -d. -f1,2 | tr -d '.') \ + --extra-index-url https://pypi.org/simple \ + --index-strategy unsafe-first-match; \ fi # Track PyTorch lib versions used during build and match in downstream instances. @@ -372,7 +372,9 @@ RUN --mount=type=cache,target=/opt/uv/cache \ else \ echo "Installing build requirements..." \ && uv pip install --python /opt/venv/bin/python3 -r requirements/build/cuda.txt \ - --extra-index-url ${PYTORCH_CUDA_INDEX_BASE_URL}/cu$(echo $CUDA_VERSION | cut -d. -f1,2 | tr -d '.'); \ + --index-url ${PYTORCH_CUDA_INDEX_BASE_URL}/cu$(echo $CUDA_VERSION | cut -d. -f1,2 | tr -d '.') \ + --extra-index-url https://pypi.org/simple \ + --index-strategy unsafe-first-match; \ fi WORKDIR /workspace @@ -561,24 +563,6 @@ RUN --mount=type=cache,target=/opt/uv/cache \ (cd "${TRITON_WORKSPACE}/dist" \ && sha256sum build-state > wheel.sha256); \ fi - -# Build NIXL and its NIXL EP extension against the PyTorch installed here when -# requested. The wheels are installed by the final image, which has no compiler. -ARG BUILD_NIXL -COPY tools/build_nixl_with_torch215_for_rubin.py /tmp/build_nixl_for_rubin.py -RUN --mount=type=bind,source=requirements,target=/tmp/vllm-requirements,ro \ - set -eux; \ - NIXL_WORKSPACE=/tmp/nixl_workspace; \ - mkdir -p "${NIXL_WORKSPACE}/dist"; \ - if [ "${BUILD_NIXL}" = "true" ]; then \ - dnf install -y --setopt=install_weak_deps=False autoconf automake libtool; \ - dnf clean all; \ - rm -rf /var/cache/dnf; \ - NIXL_REQUIREMENTS_FILE=/tmp/vllm-requirements/kv_connectors.txt \ - WHEELS_CACHE_HOME="${NIXL_WORKSPACE}/dist" \ - MAX_JOBS="${max_jobs:-}" \ - python3 /tmp/build_nixl_for_rubin.py --skip-system-deps --build-only; \ - fi #################### EXTENSIONS BUILD IMAGE #################### #################### WHEEL BUILD IMAGE #################### @@ -732,7 +716,9 @@ RUN --mount=type=cache,target=/opt/uv/cache \ else \ echo "Installing dev requirements..." \ && uv pip install --python /opt/venv/bin/python3 -r requirements/dev.txt \ - --extra-index-url ${PYTORCH_CUDA_INDEX_BASE_URL}/cu$(echo $CUDA_VERSION | cut -d. -f1,2 | tr -d '.'); \ + --index-url ${PYTORCH_CUDA_INDEX_BASE_URL}/cu$(echo $CUDA_VERSION | cut -d. -f1,2 | tr -d '.') \ + --extra-index-url https://pypi.org/simple \ + --index-strategy unsafe-first-match; \ fi #################### DEV IMAGE #################### @@ -941,7 +927,9 @@ RUN --mount=type=cache,target=/opt/uv/cache \ else \ uv pip install --system \ -r /tmp/requirements-cuda.txt \ - --extra-index-url ${PYTORCH_CUDA_INDEX_BASE_URL}/cu$(echo $CUDA_VERSION | cut -d. -f1,2 | tr -d '.'); \ + --index-url ${PYTORCH_CUDA_INDEX_BASE_URL}/cu$(echo $CUDA_VERSION | cut -d. -f1,2 | tr -d '.') \ + --extra-index-url https://pypi.org/simple \ + --index-strategy unsafe-first-match; \ fi && \ rm /tmp/requirements-cuda.txt /tmp/common.txt @@ -976,7 +964,7 @@ RUN set -eux; \ # From versions.json: .openai_server_extras.timm, .openai_server_extras.runai_model_streamer, # .openai_server_extras.gguf (NVFP4 GGUF checkpoints, e.g. RecursiveSI/flint-nvfp4-gguf) ARG TIMM_VERSION=">=1.0.17" -ARG RUNAI_MODEL_STREAMER_VERSION=">=0.15.7" +ARG RUNAI_MODEL_STREAMER_VERSION=">=0.17.1" ARG GGUF_VERSION=">=0.19.0" RUN --mount=type=cache,target=/opt/uv/cache \ uv pip install --system accelerate 'modelscope<1.38' \ @@ -1017,7 +1005,9 @@ RUN --mount=type=bind,from=build,src=/workspace/dist,target=/vllm-workspace/dist else \ echo "Installing vLLM..." \ && uv pip install --system dist/*.whl --verbose \ - --extra-index-url ${PYTORCH_CUDA_INDEX_BASE_URL}/cu$(echo $CUDA_VERSION | cut -d. -f1,2 | tr -d '.'); \ + --index-url ${PYTORCH_CUDA_INDEX_BASE_URL}/cu$(echo $CUDA_VERSION | cut -d. -f1,2 | tr -d '.') \ + --extra-index-url https://pypi.org/simple \ + --index-strategy unsafe-first-match; \ fi # Expose the Rust frontend bundled in the wheel on the container's PATH. @@ -1140,7 +1130,9 @@ RUN --mount=type=bind,from=build,src=/workspace/dist,target=/vllm-workspace/dist else \ echo "Installing vLLM..." \ && uv pip install --system dist/*.whl --verbose \ - --extra-index-url ${PYTORCH_CUDA_INDEX_BASE_URL}/cu$(echo $CUDA_VERSION | cut -d. -f1,2 | tr -d '.'); \ + --index-url ${PYTORCH_CUDA_INDEX_BASE_URL}/cu$(echo $CUDA_VERSION | cut -d. -f1,2 | tr -d '.') \ + --extra-index-url https://pypi.org/simple \ + --index-strategy unsafe-first-match; \ fi # Pytorch now installs NVSHMEM, setting LD_LIBRARY_PATH @@ -1150,7 +1142,9 @@ COPY --from=build /tmp/ep_kernels_workspace/dist/wheels.sha256 /tmp/ep-kernels-w RUN --mount=type=bind,from=build,src=/tmp/ep_kernels_workspace/dist,target=/vllm-workspace/ep_kernels/dist \ --mount=type=cache,target=/opt/uv/cache \ uv pip install --system ep_kernels/dist/*.whl --verbose \ - --extra-index-url ${PYTORCH_CUDA_INDEX_BASE_URL}/cu$(echo $CUDA_VERSION | cut -d. -f1,2 | tr -d '.') + --index-url ${PYTORCH_CUDA_INDEX_BASE_URL}/cu$(echo $CUDA_VERSION | cut -d. -f1,2 | tr -d '.') \ + --extra-index-url https://pypi.org/simple \ + --index-strategy unsafe-first-match # See the matching comment in the vllm-base stage. ENV LD_LIBRARY_PATH=/usr/local/nvidia/lib64:${LD_LIBRARY_PATH} @@ -1293,25 +1287,6 @@ RUN --mount=type=cache,target=/opt/uv/cache \ /tmp/install_triton_from_source.sh \ /tmp/triton-source-wheel.sha256 -# Install source-built NIXL last, replacing the nixl packages installed above, so -# the image has a NIXL EP build for its PyTorch version. There is no GPU driver -# at build time, so check for the extension instead of importing it. -ARG BUILD_NIXL -RUN --mount=type=cache,target=/opt/uv/cache \ - --mount=type=bind,from=extensions-build,source=/tmp/nixl_workspace/dist,target=/tmp/nixl-dist,ro \ - set -eu; \ - if [ "${BUILD_NIXL}" = "true" ]; then \ - ls /tmp/nixl-dist/*.whl; \ - uv pip uninstall --system nixl nixl-cu12 nixl-cu13; \ - uv pip install --system --no-deps /tmp/nixl-dist/*.whl; \ - python3 -c "import glob, importlib.util as u, os, torch; \ - spec = u.find_spec('nixl_ep_cu' + os.environ['CUDA_VERSION'].split('.')[0]); \ - mm = ''.join(torch.__version__.split('.')[:2]); \ - ext = glob.glob(os.path.join(spec.submodule_search_locations[0], 'nixl_ep_cpp_torch' + mm + '.*')) if spec else []; \ - assert ext, 'NIXL EP has no build for torch ' + torch.__version__; \ - print('NIXL EP extension:', ext[0])"; \ - fi - ENV VLLM_USAGE_SOURCE production-docker-image ENV VLLM_BUILD_COMMIT=${VLLM_BUILD_COMMIT:-unknown} \ VLLM_BUILD_PIPELINE=${VLLM_BUILD_PIPELINE:-local} \ diff --git a/docker/Dockerfile.cpu b/docker/Dockerfile.cpu index db146475c830..b8fb7e07ccba 100644 --- a/docker/Dockerfile.cpu +++ b/docker/Dockerfile.cpu @@ -446,6 +446,10 @@ ADD ./tools/ ./tools/ # ship either file, and this stage does not inherit the vllm-src tree. ADD ./setup.py ./setup.py ADD ./requirements/cpu.txt ./requirements/cpu.txt +# tests/v1/core/test_prompt_block_hash_vectors.py checks the engine against the +# prompt block hash vectors that the Rust frontend's unit tests also use. +ADD ./rust/src/engine-core-client/src/tests/prompt_block_hash_vectors.json \ + ./rust/src/engine-core-client/src/tests/prompt_block_hash_vectors.json # install development dependencies (for testing) RUN --mount=type=cache,target=/root/.cache/uv \ diff --git a/docker/Dockerfile.rocm b/docker/Dockerfile.rocm index d1a9c99803ac..30de51609dd0 100644 --- a/docker/Dockerfile.rocm +++ b/docker/Dockerfile.rocm @@ -1,409 +1,40 @@ -# default base image -ARG REMOTE_VLLM="0" -ARG VLLM_REPO="https://github.com/vllm-project/vllm.git" -ARG VLLM_BRANCH="main" -ARG COMMON_WORKDIR=/app -ARG BASE_IMAGE=rocm/vllm-dev:base -ARG CI_BASE_IMAGE=rocm/vllm-dev:ci_base -ARG ROCM_TRITON_KERNELS_COMMIT=669b31acc1dd1b3fd93286afd5db67f65d9f7557 -ARG INSTALL_LMCACHE=false -# NIC backend for MoRI RDMA support. -# By default (all), drivers and userspace libraries for all supported NIC types -# (ainic and bnxt) are installed; MoRI selects the appropriate one at runtime. -# To install drivers for a single NIC type only, set NIC_BACKEND explicitly: -# --build-arg NIC_BACKEND=ainic # AMD AINIC (Pensando) only -# --build-arg NIC_BACKEND=bnxt # Broadcom Thor-2 only -# --build-arg NIC_BACKEND=none # Install nothing. -ARG NIC_BACKEND=all -# AMD AINIC apt repo settings -# Users can specify a custom version compatible with their host drivers. -# The default version has been tested with ionic-dkms=26.03.3.001 -# (libionic1=54.0-187-1), required for MoRI-EP/NIXL on Pollara/AINIC. -ARG AINIC_VERSION=1.117.5-a-77 -ARG UBUNTU_CODENAME=jammy - -# Sccache configuration. Release builds use this today; CI can opt in when a -# shared S3-compatible cache backend is available. -ARG USE_SCCACHE -ARG SCCACHE_DOWNLOAD_URL -ARG SCCACHE_ENDPOINT -ARG SCCACHE_BUCKET_NAME=vllm-build-sccache -ARG SCCACHE_REGION_NAME=us-west-2 -ARG SCCACHE_S3_NO_CREDENTIALS=0 - -FROM ${BASE_IMAGE} AS base - -ARG ARG_PYTORCH_ROCM_ARCH -ENV PYTORCH_ROCM_ARCH=${ARG_PYTORCH_ROCM_ARCH:-${PYTORCH_ROCM_ARCH}} - -# Install build dependencies and utilities -RUN apt-get update -q -y && apt-get install -q -y \ +# syntax=docker/dockerfile:1.27@sha256:4edf897a3ffa55b89f906fc8cc78afdb3f1834cc9c7083565e611a8a7d5fe99e +# vLLM on ROCm. Build it with docker/docker-bake-rocm.hcl, which provides the +# `stack` named context (the `final` stage of docker/Dockerfile.rocm_base) +# and optionally prebuilt `runtime` and `test-deps` images. +# Everything above the per-commit marker is hashed into the runtime key by .buildkite/scripts/rocm/build.sh. + +FROM stack AS dev +# Build tools plus the runtime system packages (RDMA userspace, FFmpeg for torchcodec). +# mold is installed but not the default linker: some packages JIT-compile at runtime +# with flags mold does not support. Build stages opt in via LDFLAGS="-fuse-ld=mold". +RUN apt-get update -q -y -o Acquire::Retries=3 && apt-get install -q -y -o Acquire::Retries=3 \ sqlite3 libsqlite3-dev libfmt-dev libmsgpack-dev libsuitesparse-dev \ - apt-transport-https ca-certificates wget curl \ - libnuma-dev ccache mold + apt-transport-https ca-certificates wget curl libnuma-dev ccache mold \ + librdmacm1 libibverbs1 ibverbs-providers ibverbs-utils util-linux unzip libsodium23 \ + pkg-config ffmpeg libavcodec-dev libavformat-dev libavutil-dev \ + libswscale-dev libavdevice-dev libavfilter-dev libswresample-dev \ + && rm -rf /var/lib/apt/lists/* RUN --mount=type=cache,target=/root/.cache/pip \ python3 -m pip install --upgrade pip -# Note: mold is installed but not set as the system default linker because -# some packages use JIT compilation at runtime with flags mold does not support. -# Build stages opt in via LDFLAGS="-fuse-ld=mold". -# Remove sccache only if not using sccache (it exists in base image from Dockerfile.rocm_base) -ARG USE_SCCACHE -RUN if [ "$USE_SCCACHE" != "1" ]; then \ - apt-get purge -y sccache || true; \ - python3 -m pip uninstall -y sccache || true; \ - rm -f "$(which sccache)" || true; \ - fi - -# Install UV — download first, then run, so a curl failure is not masked by the pipe +# Download first, then run, so a curl failure is not masked by the pipe RUN curl -LsSf --retry 3 --retry-delay 5 https://astral.sh/uv/install.sh -o /tmp/uv-install.sh \ && env UV_INSTALL_DIR="/usr/local/bin" sh /tmp/uv-install.sh \ && rm -f /tmp/uv-install.sh \ && uv --version - -# This timeout (in seconds) is necessary when installing some dependencies via uv since it's likely to time out -# Reference: https://github.com/astral-sh/uv/pull/1694 +# Some dependencies time out with uv's default; see https://github.com/astral-sh/uv/pull/1694 ENV UV_HTTP_TIMEOUT=500 ENV UV_INDEX_STRATEGY="unsafe-best-match" # Use copy mode to avoid hardlink failures with Docker cache mounts ENV UV_LINK_MODE=copy -# ccache directory - persisted across layer rebuilds via cache mounts. ENV CCACHE_DIR=/root/.cache/ccache ENV CCACHE_COMPILERCHECK=content -# Empty by default so build steps fall back to $(nproc); CI can override. -ARG max_jobs -ENV MAX_JOBS=${max_jobs} - -# Install sccache if USE_SCCACHE is enabled (for release builds) -ARG USE_SCCACHE -ARG SCCACHE_DOWNLOAD_URL -ARG SCCACHE_ENDPOINT -ARG SCCACHE_BUCKET_NAME -ARG SCCACHE_REGION_NAME -ARG SCCACHE_S3_NO_CREDENTIALS -RUN if [ "$USE_SCCACHE" = "1" ]; then \ - if command -v sccache >/dev/null 2>&1; then \ - echo "sccache already installed, skipping installation"; \ - sccache --version; \ - else \ - echo "Installing sccache..." \ - && SCCACHE_ARCH="x86_64" \ - && SCCACHE_VERSION="v0.8.1" \ - && SCCACHE_DL_URL="${SCCACHE_DOWNLOAD_URL:-https://github.com/mozilla/sccache/releases/download/${SCCACHE_VERSION}/sccache-${SCCACHE_VERSION}-${SCCACHE_ARCH}-unknown-linux-musl.tar.gz}" \ - && curl -L -o /tmp/sccache.tar.gz ${SCCACHE_DL_URL} \ - && tar -xzf /tmp/sccache.tar.gz -C /tmp \ - && mv /tmp/sccache-${SCCACHE_VERSION}-${SCCACHE_ARCH}-unknown-linux-musl/sccache /usr/bin/sccache \ - && chmod +x /usr/bin/sccache \ - && rm -rf /tmp/sccache.tar.gz /tmp/sccache-${SCCACHE_VERSION}-${SCCACHE_ARCH}-unknown-linux-musl \ - && sccache --version; \ - fi; \ - fi - -# Set sccache environment variables only when USE_SCCACHE=1 -# This prevents S3 config from leaking into images when sccache is not used -ARG USE_SCCACHE -ENV SCCACHE_BUCKET=${USE_SCCACHE:+${SCCACHE_BUCKET_NAME}} -ENV SCCACHE_REGION=${USE_SCCACHE:+${SCCACHE_REGION_NAME}} -ENV SCCACHE_S3_NO_CREDENTIALS=${USE_SCCACHE:+${SCCACHE_S3_NO_CREDENTIALS}} -ENV SCCACHE_IDLE_TIMEOUT=${USE_SCCACHE:+0} - -ARG COMMON_WORKDIR -WORKDIR ${COMMON_WORKDIR} - - -# ----------------------- -# vLLM fetch stages -FROM base AS fetch_vllm_0 -ONBUILD COPY ./ vllm/ -FROM base AS fetch_vllm_1 -ARG VLLM_REPO -ARG VLLM_BRANCH -ONBUILD RUN git clone ${VLLM_REPO} \ - && cd vllm \ - && git fetch -v --prune -- origin ${VLLM_BRANCH} \ - && git checkout FETCH_HEAD \ - && if [ ${VLLM_REPO} != "https://github.com/vllm-project/vllm.git" ] ; then \ - git remote add upstream "https://github.com/vllm-project/vllm.git" \ - && git fetch upstream ; fi -FROM fetch_vllm_${REMOTE_VLLM} AS fetch_vllm - -# Resolve the device-independent source version once for every downstream -# artifact. setuptools-scm reads Git metadata for local/remote sources and the -# archive metadata generated for canonical CI contexts. -FROM fetch_vllm AS vllm-version -ARG COMMON_WORKDIR RUN --mount=type=cache,id=vllm-rocm-uv,target=/root/.cache/uv \ - cd ${COMMON_WORKDIR}/vllm \ - && uv pip install --system -r requirements/build/rust.txt \ - && python3 -c \ - "from setuptools_scm import get_version; print(get_version(git_describe_command=['git', 'describe', '--dirty', '--tags', '--long', '--abbrev=40', '--match', 'v[0-9]*']))" \ - > /vllm-version.txt - -# ----------------------- -# Rust toolchain inputs -# -# Keep these separate from Rust source inputs so Rust source-only changes do not -# invalidate the long-lived ci_base image that carries the toolchain. -FROM scratch AS rust_toolchain_input_0 -COPY rust-toolchain.toml /rust-toolchain-input/vllm/rust-toolchain.toml - -FROM base AS rust_toolchain_input_1 -ARG VLLM_REPO -ARG VLLM_BRANCH -RUN git clone --no-checkout --filter=blob:none ${VLLM_REPO} /rust-toolchain-input/vllm \ - && cd /rust-toolchain-input/vllm \ - && git fetch -v --prune -- origin ${VLLM_BRANCH} \ - && git sparse-checkout init --no-cone \ - && git sparse-checkout set \ - rust-toolchain.toml \ - && git checkout FETCH_HEAD - -FROM rust_toolchain_input_${REMOTE_VLLM} AS rust-toolchain-input - -# ----------------------- -# Rust build inputs -# -# REMOTE_VLLM=0: use only the local files that can affect Rust artifacts. -# REMOTE_VLLM=1: fetch only those same files from the requested remote ref. -FROM scratch AS rust_input_0 -COPY requirements/build/rust.txt /rust-input/vllm/requirements/build/rust.txt -COPY rust/Cargo.lock rust/Cargo.toml /rust-input/vllm/rust/ -COPY rust/proto /rust-input/vllm/rust/proto -COPY rust/src /rust-input/vllm/rust/src -COPY rust-toolchain.toml /rust-input/vllm/rust-toolchain.toml -COPY tools/build_rust.py /rust-input/vllm/tools/build_rust.py -COPY tools/build_rust.sh /rust-input/vllm/tools/build_rust.sh - -FROM base AS rust_input_1 -ARG VLLM_REPO -ARG VLLM_BRANCH -RUN git clone --no-checkout --filter=blob:none ${VLLM_REPO} /rust-input/vllm \ - && cd /rust-input/vllm \ - && git fetch -v --prune -- origin ${VLLM_BRANCH} \ - && git sparse-checkout init --no-cone \ - && git sparse-checkout set \ - requirements/build/rust.txt \ - rust/Cargo.lock \ - rust/Cargo.toml \ - rust/proto/** \ - rust/src/** \ - rust-toolchain.toml \ - tools/build_rust.py \ - tools/build_rust.sh \ - && git checkout FETCH_HEAD - -FROM rust_input_${REMOTE_VLLM} AS rust-input - -# ----------------------- -# Rust toolchain -FROM base AS rust-toolchain - -ENV CARGO_HOME=/root/.cargo -ENV RUSTUP_HOME=/root/.rustup -ENV PATH=${CARGO_HOME}/bin:${PATH} - -RUN apt-get update -q -y && apt-get install -q -y --no-install-recommends \ - ca-certificates curl \ - && rm -rf /var/lib/apt/lists/* - -COPY --from=rust-toolchain-input /rust-toolchain-input/vllm/rust-toolchain.toml /tmp/rust-toolchain.toml -RUN TOOLCHAIN="$(grep '^channel' /tmp/rust-toolchain.toml | sed 's/.*= *"\(.*\)"/\1/')" \ - && if ! command -v rustup >/dev/null 2>&1; then \ - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \ - | sh -s -- -y --default-toolchain none; \ - fi \ - && rustup toolchain install "${TOOLCHAIN}" \ - && rustup default "${TOOLCHAIN}" \ - && rustc --version \ - && cargo --version - -# ----------------------- -# Rust build stage -# Builds the `vllm-rs` frontend in a dedicated stage so the wheel build stages -# don't need the rust toolchain. -FROM rust-toolchain AS rust-build -ARG COMMON_WORKDIR -ARG USE_SCCACHE - -# Cap cargo parallelism to avoid exhausting the AMD CI host's open-file limit -# (rustc spawns enough concurrent processes to hit RLIMIT_NOFILE otherwise). -ENV CARGO_BUILD_JOBS=4 -ENV CARGO_NET_RETRY=10 -ENV RUSTUP_MAX_RETRIES=10 - -WORKDIR ${COMMON_WORKDIR}/vllm - -COPY --from=rust-input /rust-input/vllm/requirements/build/rust.txt requirements/build/rust.txt - -# BuildKit can run this stage in parallel with ROCm native builds. Keep Rust on -# a separate local sccache daemon while sharing the same remote cache backend. -ENV SCCACHE_SERVER_PORT=4227 - -RUN --mount=type=cache,id=vllm-rocm-uv,target=/root/.cache/uv \ - uv pip install --system -r requirements/build/rust.txt - -COPY --from=rust-input /rust-input/vllm/rust rust -COPY --from=rust-input /rust-input/vllm/rust-toolchain.toml rust-toolchain.toml -COPY --from=rust-input /rust-input/vllm/tools/build_rust.py tools/build_rust.py -COPY --from=rust-input /rust-input/vllm/tools/build_rust.sh tools/build_rust.sh -COPY --from=vllm-version /vllm-version.txt /tmp/vllm-version.txt - -# Build the release binary. Cargo's registry/git caches can be written by -# concurrent BuildKit jobs on shared workers, so lock those cache mounts while -# keeping the cache benefit. Do not cache target/, because stale target metadata -# can outlive source updates across BuildKit cache reuse. -RUN --mount=type=cache,id=vllm-rocm-cargo-registry,target=/root/.cargo/registry,sharing=locked \ - --mount=type=cache,id=vllm-rocm-cargo-git,target=/root/.cargo/git,sharing=locked \ - if [ "$USE_SCCACHE" = "1" ]; then \ - export RUSTC_WRAPPER=sccache \ - && sccache --show-stats; \ - fi \ - && export SETUPTOOLS_SCM_PRETEND_VERSION="$(cat /tmp/vllm-version.txt)" \ - && bash tools/build_rust.sh \ - && test -x vllm/vllm-rs \ - && if [ "$USE_SCCACHE" = "1" ]; then \ - sccache --show-stats; \ - fi - -# ----------------------- -# Shared vLLM build dependencies -# -# Install requirements once, before bringing source-specific inputs into either -# consumer. Native compilation and wheel packaging then share this expensive -# layer while retaining their independent source cache boundaries. -FROM base AS build_vllm_dependencies -ARG COMMON_WORKDIR -ENV VLLM_TARGET_DEVICE=rocm -WORKDIR ${COMMON_WORKDIR}/vllm - -COPY --from=fetch_vllm \ - ${COMMON_WORKDIR}/vllm/requirements/common.txt \ - ${COMMON_WORKDIR}/vllm/requirements/rocm.txt \ - requirements/ - -# Use constraints file to protect triton, pybind11, and torch dependencies. -RUN --mount=type=cache,id=vllm-rocm-uv,target=/root/.cache/uv \ - uv pip install --system -c /etc/rocm-constraints.txt -r requirements/rocm.txt - -# Fetch only the pinned ROCm Triton kernels package. Keeping this before the -# vLLM source COPYs makes the download reusable when native sources change. -FROM base AS rocm-triton-kernels -ARG ROCM_TRITON_KERNELS_COMMIT -ADD https://codeload.github.com/ROCm/triton/tar.gz/${ROCM_TRITON_KERNELS_COMMIT} \ - /tmp/rocm-triton.tar.gz -RUN mkdir -p /opt/rocm-triton-kernels \ - && tar --extract --gzip --file /tmp/rocm-triton.tar.gz \ - --directory /opt/rocm-triton-kernels --strip-components=4 \ - --no-same-owner \ - "triton-${ROCM_TRITON_KERNELS_COMMIT}/python/triton_kernels/triton_kernels" \ - && test -f /opt/rocm-triton-kernels/__init__.py \ - && rm /tmp/rocm-triton.tar.gz - -# ----------------------- -# vLLM native build stages -# -# csrc-build intentionally copies only files that affect ROCm native extension -# compilation. That keeps unrelated CI/test/docs edits from invalidating the -# expensive HIP/C++ build layer. -FROM build_vllm_dependencies AS csrc-build -ARG ROCM_TRITON_KERNELS_COMMIT -COPY --from=rocm-triton-kernels \ - /opt/rocm-triton-kernels /opt/rocm-triton-kernels -ENV TRITON_KERNELS_SRC_DIR=/opt/rocm-triton-kernels - -# pyproject.toml is bind-mounted so it is not persisted in the csrc-build image, -# but it still participates in the BuildKit cache key for this RUN step. -COPY setup.py CMakeLists.txt ./ -COPY tools/build_rust.py tools/build_rust.py -COPY cmake cmake/ -COPY csrc csrc/ -COPY vllm/envs.py vllm/envs.py -COPY vllm/__init__.py vllm/__init__.py - -ENV SETUPTOOLS_SCM_PRETEND_VERSION="0.0.0+rocm.csrc.build" - -RUN --mount=type=bind,source=pyproject.toml,target=${COMMON_WORKDIR}/vllm/pyproject.toml \ - --mount=type=cache,id=vllm-rocm-ccache,target=/root/.cache/ccache \ - { grep -Fq "set(TRITON_KERNELS_TAG \"${ROCM_TRITON_KERNELS_COMMIT}\")" \ - cmake/external_projects/triton_kernels.cmake \ - || echo "WARNING: Dockerfile.rocm ROCM_TRITON_KERNELS_COMMIT (${ROCM_TRITON_KERNELS_COMMIT}) does not match cmake TRITON_KERNELS_TAG; the Docker build uses the Dockerfile pin, cmake governs only non-Docker builds"; } \ - && export CCACHE_BASEDIR="$PWD" \ - && echo "=== ccache stats before ROCm native build ===" \ - && (ccache --show-stats || true) \ - && (ccache --zero-stats || true) \ - && EFFECTIVE_MAX_JOBS="${MAX_JOBS:-$(nproc)}" \ - && echo "Building ROCm native extension wheel with MAX_JOBS=${EFFECTIVE_MAX_JOBS}" \ - && PYTHONDONTWRITEBYTECODE=1 LDFLAGS="-fuse-ld=mold" MAX_JOBS="${EFFECTIVE_MAX_JOBS}" python3 setup.py bdist_wheel --dist-dir=dist \ - && test -d dist \ - && ls dist/*.whl >/dev/null \ - && echo "=== ccache stats after ROCm native build ===" \ - && (ccache --show-stats || true) - -FROM csrc-build AS kernel-symbol-map-build -ARG VLLM_KERNEL_SYMBOL_MAP=0 -ARG buildkite_commit="" -COPY tools/ci/kernel_symbol_map.py tools/ci/rocm_kernel_symbol_map.py tools/ci/ -RUN if [ "${VLLM_KERNEL_SYMBOL_MAP}" = "1" ]; then \ - uv venv --system-site-packages /tmp/kernel-map/.venv \ - && /tmp/kernel-map/.venv/bin/python tools/ci/kernel_symbol_map.py \ - --backend rocm --build-root build --source-root . \ - --commit "${buildkite_commit}" \ - --out dist/kernel_symbol_map.rocm.json.gz \ - && rm -rf /tmp/kernel-map; \ - fi - -FROM scratch AS kernel-symbol-map-rocm -ARG COMMON_WORKDIR -COPY --from=kernel-symbol-map-build ${COMMON_WORKDIR}/vllm/dist/kernel_symbol_map.rocm.json.gz / - -# Build the full vLLM ROCm wheel by reusing the native extension wheel from -# csrc-build. This stage still rebuilds for Python/package changes, but skips -# the expensive dependency installation and HIP/C++ compilation when their -# respective inputs are unchanged. -FROM build_vllm_dependencies AS build_vllm -COPY --from=fetch_vllm ${COMMON_WORKDIR}/vllm ${COMMON_WORKDIR}/vllm -COPY --from=csrc-build ${COMMON_WORKDIR}/vllm/dist /precompiled-wheels -COPY --from=vllm-version /vllm-version.txt /tmp/vllm-version.txt - -# Drop the pre-built Rust artifacts into the source tree. setup.py detects -# them and ships them as-is, skipping the local Rust build. -COPY --from=rust-build ${COMMON_WORKDIR}/vllm/vllm/vllm-rs ${COMMON_WORKDIR}/vllm/vllm/vllm-rs -COPY --from=rust-build ${COMMON_WORKDIR}/vllm/vllm/_rust_*.so ${COMMON_WORKDIR}/vllm/vllm/ - -RUN cd "${COMMON_WORKDIR}/vllm" \ - && export SETUPTOOLS_SCM_PRETEND_VERSION="$(cat /tmp/vllm-version.txt)" \ - && export VLLM_USE_PRECOMPILED=1 \ - && export VLLM_PRECOMPILED_WHEEL_LOCATION="$(ls /precompiled-wheels/*.whl)" \ - && export VLLM_DOCKER_BUILD_CONTEXT=1 \ - && echo "Packaging vLLM ROCm wheel using precompiled extensions from ${VLLM_PRECOMPILED_WHEEL_LOCATION}" \ - && PYTHONDONTWRITEBYTECODE=1 python3 setup.py bdist_wheel --dist-dir=dist \ - && test -d dist \ - && ls dist/*.whl >/dev/null -FROM scratch AS export_vllm -ARG COMMON_WORKDIR -COPY --from=build_vllm ${COMMON_WORKDIR}/vllm/dist/*.whl / -COPY --from=build_vllm ${COMMON_WORKDIR}/vllm/requirements /requirements -COPY --from=build_vllm ${COMMON_WORKDIR}/vllm/benchmarks /benchmarks -COPY --from=build_vllm ${COMMON_WORKDIR}/vllm/tests /tests -COPY --from=build_vllm ${COMMON_WORKDIR}/vllm/examples /examples -COPY --from=build_vllm ${COMMON_WORKDIR}/vllm/tools/benchmark_helion_kernels.py /tools/benchmark_helion_kernels.py -COPY --from=build_vllm ${COMMON_WORKDIR}/vllm/tools/install_torchcodec_rocm.sh /tools/install_torchcodec_rocm.sh -# tests/tools/test_kernel_symbol_map.py loads the symbol-map producers from here. -COPY --from=build_vllm ${COMMON_WORKDIR}/vllm/tools/ci /tools/ci -COPY --from=build_vllm ${COMMON_WORKDIR}/vllm/docker/Dockerfile /docker/Dockerfile -COPY --from=build_vllm ${COMMON_WORKDIR}/vllm/docker/Dockerfile.cpu /docker/Dockerfile.cpu -COPY --from=build_vllm ${COMMON_WORKDIR}/vllm/docker/Dockerfile.rocm /docker/ -COPY --from=build_vllm ${COMMON_WORKDIR}/vllm/docker/Dockerfile.rocm_base /docker/Dockerfile.rocm_base -COPY --from=build_vllm ${COMMON_WORKDIR}/vllm/docker/ci-rocm.hcl /docker/ci-rocm.hcl -COPY --from=build_vllm ${COMMON_WORKDIR}/vllm/docker/docker-bake.hcl /docker/docker-bake.hcl -COPY --from=build_vllm ${COMMON_WORKDIR}/vllm/docker/docker-bake-rocm.hcl /docker/docker-bake-rocm.hcl -COPY --from=build_vllm ${COMMON_WORKDIR}/vllm/.buildkite /.buildkite -COPY --from=build_vllm ${COMMON_WORKDIR}/vllm/pyproject.toml /pyproject.toml -COPY --from=build_vllm ${COMMON_WORKDIR}/vllm/rust /rust -COPY --from=build_vllm ${COMMON_WORKDIR}/vllm/rust-toolchain.toml /rust-toolchain.toml -COPY --from=build_vllm ${COMMON_WORKDIR}/vllm/vllm/v1 /vllm_v1 + uv pip install --system -c /etc/rocm-constraints.txt cmake ninja "setuptools<80" setuptools-scm \ + setuptools-rust wheel jinja2 packaging regex # NIXL/UCX build stages -FROM base AS build_nixl +FROM dev AS build_nixl ARG NIXL_BRANCH="1683cf3b7f3d11674c03c5e861cea22339876c96" ARG NIXL_REPO="https://github.com/ai-dynamo/nixl.git" ARG UCX_BRANCH="2ff0b47ce7626584cb9b56c647d08f712c6c3edf" @@ -428,11 +59,11 @@ RUN apt-get -y update && apt-get -y install autoconf libtool pkg-config \ ibverbs-providers \ && rm -rf /var/lib/apt/lists/* -RUN --mount=type=cache,target=/root/.cache/uv \ +RUN --mount=type=cache,id=vllm-rocm-uv,target=/root/.cache/uv \ uv pip install --system meson meson-python pyyaml types-PyYAML \ auditwheel build patchelf pytest tomlkit "setuptools>=80.9.0" -RUN --mount=type=cache,target=/root/.cache/ccache \ +RUN --mount=type=cache,id=vllm-rocm-ccache,target=/root/.cache/ccache \ cd /usr/local/src && \ git clone ${UCX_REPO} && \ cd ucx && \ @@ -457,7 +88,7 @@ RUN --mount=type=cache,target=/root/.cache/ccache \ ENV PATH=/usr/local/ucx/bin:$PATH ENV LD_LIBRARY_PATH=${UCX_HOME}/lib:${LD_LIBRARY_PATH} -RUN --mount=type=cache,target=/root/.cache/ccache \ +RUN --mount=type=cache,id=vllm-rocm-ccache,target=/root/.cache/ccache \ git clone ${NIXL_REPO} /opt/nixl && \ cd /opt/nixl && \ git checkout ${NIXL_BRANCH} && \ @@ -506,12 +137,12 @@ RUN cd /opt/nixl && \ cp /tmp/nixl_wheels/repaired/*.whl /app/install # Fetch and verify pinned LMCache source. -FROM base AS lmcache_source +FROM dev AS lmcache_source ARG LMCACHE_REPO="https://github.com/LMCache/LMCache.git" ARG LMCACHE_REF="140819c9d57a975dbc5678a6459a218e544cb58b" ARG LMCACHE_VERSION="0.5.3" -# Remove NVIDIA-only dependencies and accept the release image's NumPy version. +# Remove NVIDIA-only dependencies and accept the runtime's NumPy version. RUN printf '%s\n' "${LMCACHE_REF}" | grep -Eq '^[0-9a-f]{40}$' \ && git clone --depth=1 --branch "v${LMCACHE_VERSION}" \ "${LMCACHE_REPO}" /app/lmcache \ @@ -526,7 +157,7 @@ RUN printf '%s\n' "${LMCACHE_REF}" | grep -Eq '^[0-9a-f]{40}$' \ /app/lmcache/requirements/common.txt \ && rm -rf /app/lmcache/.git -FROM base AS build_lmcache +FROM dev AS build_lmcache COPY --from=lmcache_source \ /app/lmcache/requirements/build.txt /tmp/lmcache-build-requirements.txt RUN --mount=type=cache,id=vllm-rocm-uv,target=/root/.cache/uv \ @@ -534,7 +165,6 @@ RUN --mount=type=cache,id=vllm-rocm-uv,target=/root/.cache/uv \ ARG LMCACHE_VERSION="0.5.3" ARG LMCACHE_ROCM_ARCH="gfx942;gfx950" -ARG USE_SCCACHE COPY --from=lmcache_source /app/lmcache /app/lmcache WORKDIR /app/lmcache @@ -545,9 +175,6 @@ RUN --mount=type=cache,id=vllm-rocm-ccache,target=/root/.cache/ccache \ | grep -Eq '^gfx[0-9a-z]+(;gfx[0-9a-z]+)*$'; \ offload_flags="$(printf '%s' "${LMCACHE_ROCM_ARCH}" \ | sed 's/;/ --offload-arch=/g; s/^/--offload-arch=/')"; \ - if [ "${USE_SCCACHE:-0}" = "1" ]; then \ - export HIP_CLANG_PATH=/opt/sccache-wrappers; \ - fi; \ mkdir -p /app/install; \ SETUPTOOLS_SCM_PRETEND_VERSION="${LMCACHE_VERSION}" \ PYTORCH_ROCM_ARCH="${LMCACHE_ROCM_ARCH}" \ @@ -570,10 +197,8 @@ RUN --mount=type=tmpfs,target=/tmp/lmcache-wheel \ exit 1; \ fi -# DeepEP build stage. ROCShmem is provided by TheRock SDK -# ROCSHMEM_DIR set to the SDK install path so DeepEP finds the headers, etc. -FROM base AS build_deepep -ENV ROCSHMEM_DIR=${ROCM_PATH} +# DeepEP build stage. The stack sets ROCSHMEM_DIR via the TheRock SDK. +FROM dev AS build_deepep ARG DEEPEP_BRANCH="0f63d3e9" ARG DEEPEP_REPO="https://github.com/ROCm/DeepEP.git" ARG DEEPEP_NIC="cx7" @@ -583,20 +208,53 @@ ARG DEEPEP_NIC="cx7" # We pass --rocm-gfx942-fp8-fnuz-max 224 to align the DeepEP dispatch FP8 # quantization bound with vLLM's own clamp # (vllm/model_executor/layers/quantization/utils/fp8_utils.py): 224 for E4M3FNUZ -RUN --mount=type=cache,target=/root/.cache/ccache \ +RUN --mount=type=cache,id=vllm-rocm-ccache,target=/root/.cache/ccache \ export PYTORCH_ROCM_ARCH="gfx942;gfx950" \ && git clone ${DEEPEP_REPO} \ && cd DeepEP \ && git checkout ${DEEPEP_BRANCH} \ && LDFLAGS="-fuse-ld=mold" MAX_JOBS="${MAX_JOBS:-$(nproc)}" python3 setup.py --variant rocm --rocm-explicit-ctx --nic ${DEEPEP_NIC} --rocm-gfx942-fp8-fnuz-max 224 bdist_wheel --dist-dir=/app/deep_install -# MoRI runtime dependencies live in Dockerfile.rocm so NIC backend changes do -# not force users to rebuild the long-lived Dockerfile.rocm_base image. -FROM base AS mori_base -ARG NIC_BACKEND -ARG AINIC_VERSION -ARG UBUNTU_CODENAME -RUN /bin/bash -lc 'set -euo pipefail; \ +# mamba-ssm and causal-conv1d, the fast path transformers uses for the hybrid-model references in the +# language model tests. CI only. Their setup.py targets HIP_ARCHITECTURES (default: the local GPU). +FROM dev AS build_mamba +ARG MAMBA_COMMIT="269cbe654d10883374736eb936e61d3ee5625695" +ARG CAUSAL_CONV1D_COMMIT="da6dbaa9fd5a919967f14d3fd031da1288ad5025" +ARG HIP_ARCHITECTURES="gfx942,gfx950" +RUN MAMBA_FORCE_BUILD=TRUE CAUSAL_CONV1D_FORCE_BUILD=TRUE \ + python3 -m pip wheel --no-build-isolation --no-deps -w /app/mamba_install \ + "git+https://github.com/AndreasKaratzas/mamba@${MAMBA_COMMIT}" \ + "git+https://github.com/Dao-AILab/causal-conv1d@${CAUSAL_CONV1D_COMMIT}" \ + && for w in /app/mamba_install/*.whl; do python3 -m zipfile -e "$w" /tmp/whl; done \ + && for so in /tmp/whl/*.so; do for a in $(echo "$HIP_ARCHITECTURES" | tr , ' '); do \ + llvm-objdump --offloading "$so" | grep -qw "$a" || { echo "$so lacks $a" >&2; exit 1; }; done; done \ + && rm -rf /tmp/whl + +# rdma-core recent enough for DeepEP's rocshmem. CI only: it replaces the distro libibverbs, which +# the vendor NIC providers in the runtime (AINIC, Broadcom bnxt_re) are built against. +FROM dev AS build_rdma_core +RUN git clone --branch v62.0 --depth 1 https://github.com/linux-rdma/rdma-core.git /tmp/rdma-core \ + && cd /tmp/rdma-core && mkdir build && cd build \ + && cmake -GNinja -DCMAKE_INSTALL_PREFIX=/usr -DNO_MAN_PAGES=1 .. \ + && ninja && DESTDIR=/out ninja install + +# The runtime: stack + every compiled component + all pip dependencies + all env. Published as base-nightly. +FROM dev AS runtime +# NIC backend for MoRI RDMA support. +# By default (all), drivers and userspace libraries for all supported NIC types +# (ainic and bnxt) are installed; MoRI selects the appropriate one at runtime. +# To install drivers for a single NIC type only, set NIC_BACKEND explicitly: +# --build-arg NIC_BACKEND=ainic # AMD AINIC (Pensando) only +# --build-arg NIC_BACKEND=bnxt # Broadcom Thor-2 only +# --build-arg NIC_BACKEND=none # Install nothing. +ARG NIC_BACKEND=all +# AMD AINIC apt repo settings +# Users can specify a custom version compatible with their host drivers. +# The default version has been tested with ionic-dkms=26.03.3.001 +# (libionic1=54.0-187-1), required for MoRI-EP/NIXL on Pollara/AINIC. +ARG AINIC_VERSION=1.117.5-a-77 +ARG UBUNTU_CODENAME=jammy +RUN --mount=type=cache,target=/root/.cache/pip /bin/bash -lc 'set -euo pipefail; \ \ install_ainic() { \ apt-get update && apt-get install -y --no-install-recommends ca-certificates curl gnupg apt-transport-https; \ @@ -629,9 +287,7 @@ RUN /bin/bash -lc 'set -euo pipefail; \ rm -rf /var/lib/apt/lists/*; \ }; \ \ - echo "[MORI] Install MoRI proxy deps"; \ - pip install --quiet --ignore-installed blinker && \ - pip install --quiet quart msgpack aiohttp pyzmq; \ + pip install --quiet --ignore-installed blinker; \ echo "[MORI] NIC_BACKEND=${NIC_BACKEND}"; \ \ # NIC backend deps — mori auto-detects NIC at runtime (MORI_DEVICE_NIC env var override). \ @@ -643,395 +299,297 @@ RUN /bin/bash -lc 'set -euo pipefail; \ bnxt) install_bnxt ;; \ *) echo "ERROR: unknown NIC_BACKEND=${NIC_BACKEND}. Use one of: none, ainic, bnxt, all"; exit 2 ;; \ esac' - -# ----------------------- -# vLLM wheel release build stage (for building distributable wheels) -# This stage pins dependencies to custom ROCm wheel versions and handles version detection -FROM fetch_vllm AS build_vllm_wheel_release - -ARG COMMON_WORKDIR -ARG ROCM_TRITON_KERNELS_COMMIT -COPY --from=vllm-version /vllm-version.txt /tmp/vllm-version.txt -COPY --from=rocm-triton-kernels \ - /opt/rocm-triton-kernels /opt/rocm-triton-kernels -ENV TRITON_KERNELS_SRC_DIR=/opt/rocm-triton-kernels - -# Drop the pre-built Rust artifacts into the source tree. setup.py detects -# them and ships them as-is, skipping the local Rust build. -COPY --from=rust-build ${COMMON_WORKDIR}/vllm/vllm/vllm-rs ${COMMON_WORKDIR}/vllm/vllm/vllm-rs -COPY --from=rust-build ${COMMON_WORKDIR}/vllm/vllm/_rust_*.so ${COMMON_WORKDIR}/vllm/vllm/ - -# Create /install directory for custom wheels -RUN mkdir -p /install - -# Copy custom ROCm wheels from docker/context if they exist -# COPY ensures Docker cache is invalidated when wheels change -# .keep file ensures directory always exists for COPY to work -COPY docker/context/base-wheels/ /tmp/base-wheels/ -# This is how we know if we are building for a wheel release or not. -# If there are not wheels found there, we are not building for a wheel release. -# So we exit with an error. To skip this stage. -RUN if [ -n "$(ls /tmp/base-wheels/*.whl 2>/dev/null)" ]; then \ - echo "Found custom wheels - copying to /install"; \ - cp /tmp/base-wheels/*.whl /install/ && \ - echo "Copied custom wheels:"; \ - ls -lh /install/; \ - else \ - echo "ERROR: No custom wheels found in docker/context/base-wheels/"; \ - echo "Wheel releases require pre-built ROCm wheels."; \ - exit 1; \ - fi - -# GIT_REPO_CHECK: Verify repo is clean and tags are available (for release builds) -# This matches CUDA's Dockerfile behavior for proper version detection via setuptools_scm -ARG GIT_REPO_CHECK=0 -RUN if [ "$GIT_REPO_CHECK" != "0" ]; then \ - echo "Running repository checks..."; \ - cd vllm && bash tools/check_repo.sh; \ - fi - -# Install the packaging helpers before modifying requirements/rocm.txt. -RUN --mount=type=cache,target=/root/.cache/uv \ - { grep -Fq "set(TRITON_KERNELS_TAG \"${ROCM_TRITON_KERNELS_COMMIT}\")" \ - vllm/cmake/external_projects/triton_kernels.cmake \ - || echo "WARNING: Dockerfile.rocm ROCM_TRITON_KERNELS_COMMIT (${ROCM_TRITON_KERNELS_COMMIT}) does not match cmake TRITON_KERNELS_TAG; the Docker build uses the Dockerfile pin, cmake governs only non-Docker builds"; } \ - && cd vllm \ - && uv pip install --system setuptools_scm regex - -# Fail if git-based package dependencies are found in requirements files -# (uv doesn't handle git+ URLs well, and packages should be distributed on PyPI) -# Extra notes: pip install is able to handle git+ URLs, but uv doesn't. -RUN echo "Checking for git-based packages in requirements files..." \ - && echo "Checking common.txt for git-based packages:" \ - && if grep -q 'git+' ${COMMON_WORKDIR}/vllm/requirements/common.txt; then \ - echo "ERROR: Git-based packages found in common.txt:"; \ - grep 'git+' ${COMMON_WORKDIR}/vllm/requirements/common.txt; \ - echo "Please publish these packages to PyPI instead of using git dependencies."; \ - exit 1; \ - else \ - echo " ✓ No git-based packages found in common.txt"; \ - fi \ - && echo "Checking rocm.txt for git-based packages:" \ - && if grep -q 'git+' ${COMMON_WORKDIR}/vllm/requirements/rocm.txt; then \ - echo "ERROR: Git-based packages found in rocm.txt:"; \ - grep 'git+' ${COMMON_WORKDIR}/vllm/requirements/rocm.txt; \ - echo "Please publish these packages to PyPI instead of using git dependencies."; \ - exit 1; \ - else \ - echo " ✓ No git-based packages found in rocm.txt"; \ - fi \ - && echo "All requirements files are clean - no git-based packages found" - -# Pin vLLM dependencies to exact versions of custom ROCm wheels -# This ensures 'pip install vllm' automatically installs correct torch/triton/torchvision/amdsmi -COPY tools/vllm-rocm/pin_rocm_dependencies.py /tmp/pin_rocm_dependencies.py -RUN echo "Pinning vLLM dependencies to custom wheel versions..." \ - && python3 /tmp/pin_rocm_dependencies.py /install ${COMMON_WORKDIR}/vllm/requirements/rocm.txt - -# Install dependencies using custom wheels from /install -RUN --mount=type=cache,target=/root/.cache/uv \ - cd vllm \ - && echo "Building vLLM with custom wheels from /install" \ - && uv pip install --system --find-links /install -r requirements/rocm.txt - -# Build wheel using the shared source version to avoid dirty state from modified requirements/rocm.txt -# (setup.py auto-detects ccache/sccache in PATH) -RUN --mount=type=cache,id=vllm-rocm-ccache,target=/root/.cache/ccache \ - cd vllm \ - && export CCACHE_BASEDIR="$PWD" \ - && export SETUPTOOLS_SCM_PRETEND_VERSION="$(cat /tmp/vllm-version.txt)" \ - && echo "Building wheel with version: ${SETUPTOOLS_SCM_PRETEND_VERSION}" \ - && MAX_JOBS="${MAX_JOBS:-$(nproc)}" python3 setup.py bdist_wheel --dist-dir=dist - -FROM scratch AS export_vllm_wheel_release -ARG COMMON_WORKDIR -COPY --from=build_vllm_wheel_release ${COMMON_WORKDIR}/vllm/dist/*.whl / -COPY --from=build_vllm_wheel_release ${COMMON_WORKDIR}/vllm/requirements /requirements -COPY --from=build_vllm_wheel_release ${COMMON_WORKDIR}/vllm/benchmarks /benchmarks -COPY --from=build_vllm_wheel_release ${COMMON_WORKDIR}/vllm/tests /tests -COPY --from=build_vllm_wheel_release ${COMMON_WORKDIR}/vllm/examples /examples -COPY --from=build_vllm_wheel_release ${COMMON_WORKDIR}/vllm/tools/benchmark_helion_kernels.py /tools/benchmark_helion_kernels.py -COPY --from=build_vllm_wheel_release ${COMMON_WORKDIR}/vllm/tools/install_torchcodec_rocm.sh /tools/install_torchcodec_rocm.sh -COPY --from=build_vllm_wheel_release ${COMMON_WORKDIR}/vllm/docker/Dockerfile /docker/Dockerfile -COPY --from=build_vllm_wheel_release ${COMMON_WORKDIR}/vllm/docker/Dockerfile.cpu /docker/Dockerfile.cpu -COPY --from=build_vllm_wheel_release ${COMMON_WORKDIR}/vllm/docker/Dockerfile.rocm /docker/ -COPY --from=build_vllm_wheel_release ${COMMON_WORKDIR}/vllm/docker/Dockerfile.rocm_base /docker/Dockerfile.rocm_base -COPY --from=build_vllm_wheel_release ${COMMON_WORKDIR}/vllm/docker/ci-rocm.hcl /docker/ci-rocm.hcl -COPY --from=build_vllm_wheel_release ${COMMON_WORKDIR}/vllm/docker/docker-bake.hcl /docker/docker-bake.hcl -COPY --from=build_vllm_wheel_release ${COMMON_WORKDIR}/vllm/docker/docker-bake-rocm.hcl /docker/docker-bake-rocm.hcl -COPY --from=build_vllm_wheel_release ${COMMON_WORKDIR}/vllm/.buildkite /.buildkite -COPY --from=build_vllm_wheel_release ${COMMON_WORKDIR}/vllm/pyproject.toml /pyproject.toml -COPY --from=build_vllm_wheel_release ${COMMON_WORKDIR}/vllm/rust /rust -COPY --from=build_vllm_wheel_release ${COMMON_WORKDIR}/vllm/rust-toolchain.toml /rust-toolchain.toml -COPY --from=build_vllm_wheel_release ${COMMON_WORKDIR}/vllm/vllm/v1 /vllm_v1 - -# ----------------------- -# CI base image (Tier 1) - stable, rarely changing CI dependencies. -# Per-PR test builds pull this as CI_BASE_IMAGE so the test stage only layers -# in the vLLM artifacts for the current commit. -FROM mori_base AS ci_base -ARG COMMON_WORKDIR - -ENV CARGO_HOME=/root/.cargo -ENV RUSTUP_HOME=/root/.rustup -ENV PATH=${CARGO_HOME}/bin:${PATH} - -# Bake the Rust toolchain into ci_base so Rust build/test steps do not -# need to fetch it on each per-commit image build. -COPY --from=rust-toolchain /root/.cargo /root/.cargo -COPY --from=rust-toolchain /root/.rustup /root/.rustup - -# Update rdma-core to support latest rocshmem. -ARG DEEPEP_NIC -RUN if [ "${DEEPEP_NIC}" = "cx7" ] || [ "${DEEPEP_NIC}" = "io" ]; then \ - git clone --branch v62.0 --depth 1 https://github.com/linux-rdma/rdma-core.git /tmp/rdma-core && \ - cd /tmp/rdma-core && \ - mkdir -p build && cd build && \ - cmake -GNinja -DCMAKE_INSTALL_PREFIX=/usr -DNO_MAN_PAGES=1 .. && \ - ninja && ninja install && ldconfig && rm -rf /tmp/rdma-core; \ -fi - -# Install NIXL + DeepEP wheels. -RUN --mount=type=bind,from=build_nixl,src=/app/install,target=/nixl_install \ - --mount=type=bind,from=build_deepep,src=/app/deep_install,target=/deep_install \ - uv pip install --system /nixl_install/*.whl /deep_install/*.whl - -# RDMA userspace libraries plus FFmpeg dev libs needed by torchcodec. -RUN timeout --kill-after=30s 5m apt-get update -q -y --error-on=any \ - -o Acquire::Retries=3 \ - -o Acquire::http::Timeout=30 \ - -o Acquire::https::Timeout=30 \ - && apt-get install -q -y --no-install-recommends \ - -o Acquire::Retries=3 \ - -o Acquire::http::Timeout=30 \ - -o Acquire::https::Timeout=30 \ - librdmacm1 \ - libibverbs1 \ - ibverbs-providers \ - ibverbs-utils \ - util-linux \ - unzip \ - libsodium23 \ - pkg-config ffmpeg libavcodec-dev libavformat-dev libavutil-dev \ - libswscale-dev libavdevice-dev libavfilter-dev libswresample-dev \ - && rm -rf /var/lib/apt/lists/* +RUN echo "MORI_NIC_BACKEND=${NIC_BACKEND}" >> /app/versions.txt \ + && echo "AINIC_VERSION=${AINIC_VERSION}" >> /app/versions.txt # Install torchcodec from source for ROCm/torch ABI compatibility. -COPY tools/install_torchcodec_rocm.sh /tmp/install_torchcodec.sh -RUN --mount=type=cache,target=/root/.cache/uv \ +RUN --mount=type=bind,source=tools/install_torchcodec_rocm.sh,target=/tmp/install_torchcodec.sh \ + --mount=type=cache,id=vllm-rocm-uv,target=/root/.cache/uv \ --mount=type=cache,target=/root/.cache/pip \ - --mount=type=cache,target=/root/.cache/torchcodec-wheels \ - bash /tmp/install_torchcodec.sh \ - && rm /tmp/install_torchcodec.sh \ - && apt-get clean && rm -rf /var/lib/apt/lists/* - -# Pre-install shared ROCm runtime dependencies. -COPY requirements/common.txt requirements/rocm.txt /tmp/ci-base-requirements/ -RUN --mount=type=cache,target=/root/.cache/uv \ - uv pip install --system -c /etc/rocm-constraints.txt \ - -r /tmp/ci-base-requirements/rocm.txt \ - && rm -rf /tmp/ci-base-requirements - -# Enable fast and less brittle model downloads in tests. -ENV HF_XET_HIGH_PERFORMANCE=1 -ENV HF_HUB_DOWNLOAD_TIMEOUT=60 - -# Keep torch.cuda.is_available() fork-safe (see vllm/env_override.py). -ENV PYTORCH_NVML_BASED_CUDA_CHECK=1 - -# Pre-install vLLM test dependencies. -COPY requirements/test/rocm.txt /tmp/rocm-test-reqs.txt -RUN --mount=type=cache,target=/root/.cache/uv \ - uv pip install --system -r /tmp/rocm-test-reqs.txt - + --mount=type=tmpfs,target=/root/.cache/torchcodec-wheels \ + ldconfig && bash /tmp/install_torchcodec.sh +# All Python dependencies, at the versions CI tests (the test lock is a constraint). +# quart, msgpack, aiohttp, pyzmq: MoRI proxy dependencies (quart: not in the test lock). +RUN --mount=type=bind,source=requirements/common.txt,target=/r/common.txt \ + --mount=type=bind,source=requirements/rocm.txt,target=/r/rocm.txt \ + --mount=type=bind,source=requirements/test/rocm.txt,target=/r/test.txt \ + --mount=type=bind,from=build_nixl,src=/app/install,target=/nixl \ + --mount=type=bind,from=build_lmcache,src=/app/install,target=/lmcache \ + --mount=type=cache,id=vllm-rocm-uv,target=/root/.cache/uv \ + uv pip install --system -c /etc/rocm-constraints.txt -c /r/test.txt -r /r/rocm.txt \ + /nixl/*.whl /lmcache/*.whl quart==0.22.0 msgpack aiohttp pyzmq \ + && python3 -c "import lmcache, lmcache.c_ops; print('lmcache', lmcache.__version__)" # Rebuild fastsafetensors from source so its C++ extension is compiled with # USE_ROCM and can detect libamdhip64.so at runtime. -RUN --mount=type=cache,target=/root/.cache/pip \ - FASTSAFETENSORS_REQ="$(grep -E '^fastsafetensors(==| @ )' /tmp/rocm-test-reqs.txt | head -1)" \ +RUN --mount=type=bind,source=requirements/test/rocm.txt,target=/r/test.txt \ + --mount=type=cache,target=/root/.cache/pip \ + FASTSAFETENSORS_REQ="$(grep -E '^fastsafetensors(==| @ )' /r/test.txt | head -1)" \ && test -n "${FASTSAFETENSORS_REQ}" \ && python3 -m pip install --force-reinstall --no-deps \ - --no-binary fastsafetensors "${FASTSAFETENSORS_REQ}" \ - && rm /tmp/rocm-test-reqs.txt + --no-binary fastsafetensors "${FASTSAFETENSORS_REQ}" +# ROCm profiler limits workaround. +RUN echo "ROCTRACER_MAX_EVENTS=10000000" > /app/libkineto.conf +ENV KINETO_CONFIG="/app/libkineto.conf" # Set MIOPEN ENVS to resolve performance regressions in MIOpen 3D convolution kernel. # See: https://github.com/pytorch/pytorch/issues/169857 ENV MIOPEN_DEBUG_CONV_DIRECT=0 ENV MIOPEN_DEBUG_CONV_GEMM=0 - # Use legacy IPC mode for HSA to avoid GPU memory pinning issues with UCX rocm_ipc. # See: https://github.com/ROCm/rocm-libraries/issues/6266 ENV HSA_ENABLE_IPC_MODE_LEGACY=1 ENV UCX_ROCM_COPY_SIGPOOL_MAX_ELEMS=inf ENV UCX_ROCM_IPC_SIGPOOL_MAX_ELEMS=inf - -# ROCm profiler limits workaround. -RUN echo "ROCTRACER_MAX_EVENTS=10000000" > ${COMMON_WORKDIR}/libkineto.conf -ENV KINETO_CONFIG="${COMMON_WORKDIR}/libkineto.conf" - -# Install vllm_test_utils in ci_base for ci_base + wheel parity. -COPY tests/vllm_test_utils /tmp/vllm_test_utils -RUN --mount=type=cache,target=/root/.cache/uv \ - uv pip install --system /tmp/vllm_test_utils \ - && rm -rf /tmp/vllm_test_utils - -RUN --mount=type=bind,from=build_lmcache,src=/app/install,target=/lmcache_install \ - --mount=type=bind,source=requirements/test/rocm.txt,target=/tmp/lmcache-constraints.txt \ - --mount=type=cache,target=/root/.cache/uv \ - uv pip install --system --constraint /tmp/lmcache-constraints.txt \ - /lmcache_install/*.whl \ - && python3 -c "import lmcache, lmcache.c_ops; print('lmcache', lmcache.__version__)" - -# ----------------------- -# Test vLLM image (Tier 2) - vLLM-only layer on top of ci_base. -FROM ${CI_BASE_IMAGE} AS test -ARG COMMON_WORKDIR - -# Install the vLLM wheel (--no-deps: all deps already in ci_base). -RUN --mount=type=bind,from=export_vllm,src=/,target=/install \ - --mount=type=cache,target=/root/.cache/uv \ - cd /install \ - && uv pip install --system --no-deps *.whl - -# Store the vLLM wheel in the image for python-only install tests. -COPY --from=export_vllm /*.whl /opt/vllm-wheels/ - -WORKDIR /vllm-workspace -COPY --from=build_vllm ${COMMON_WORKDIR}/vllm /vllm-workspace - -# Copy in the v1 package (for python-only install test group). -COPY --from=export_vllm /vllm_v1 /usr/local/lib/python${PYTHON_VERSION}/dist-packages/vllm/v1 - -# Hide source under src/ so it won't shadow the installed package in tests. -RUN mkdir src && mv vllm src/vllm - -# Catch GPU<->CPU syncs in execute_model/sample_tokens during tests. -ENV VLLM_GPU_SYNC_CHECK=error - -FROM test AS test_smoke -ARG ROCM_SMOKE_ID -RUN --network=none \ - bash /vllm-workspace/.buildkite/scripts/rocm/smoke-test-image.sh --inside \ - && printf '%s\n' "${ROCM_SMOKE_ID:-local}" > /vllm-smoke-ok - -FROM scratch AS export_test_smoke -COPY --from=test_smoke /vllm-smoke-ok / - -# ----------------------- -# Final vLLM image -FROM mori_base AS final_common -ENV UV_COMPILE_BYTECODE=1 - -RUN python3 -m pip install --upgrade pip && rm -rf /var/lib/apt/lists/* - -# Clean up sccache from release image (not needed at runtime) -# This removes the binary and wrappers that may have been installed during build -RUN rm -f /usr/bin/sccache || true \ - && rm -rf /opt/sccache-wrappers || true - -# Unset sccache environment variables for the release image -# This prevents S3 bucket config from leaking into production images -ENV SCCACHE_BUCKET= -ENV SCCACHE_REGION= -ENV SCCACHE_ENDPOINT= -ENV SCCACHE_S3_NO_CREDENTIALS= -ENV SCCACHE_IDLE_TIMEOUT= - -# Error related to odd state for numpy 1.20.3 where there is no METADATA etc, but an extra LICENSES_bundled.txt. -# Manually remove it so that later steps of numpy upgrade can continue -RUN case "$(which python3)" in \ - *"/opt/conda/envs/py_3.9"*) \ - rm -rf /opt/conda/envs/py_3.9/lib/python3.9/site-packages/numpy-1.20.3.dist-info/;; \ - *) ;; esac - -RUN --mount=type=cache,target=/root/.cache/uv \ - uv pip install --system --upgrade huggingface-hub - -# Install vLLM using uv (inherited from base stage) -# Note: No -U flag to avoid upgrading PyTorch ROCm to CUDA version -# Keep OpenTelemetry within LMCache's cap. -RUN --mount=type=bind,from=export_vllm,src=/,target=/install \ - --mount=type=cache,target=/root/.cache/uv \ - cd /install \ - && echo "opentelemetry-api<=1.40.0" > /tmp/otel-constraints.txt \ - && uv pip install --system -c /etc/rocm-constraints.txt -c /tmp/otel-constraints.txt -r requirements/rocm.txt \ - && pip uninstall -y vllm \ - && uv pip install --system -c /etc/rocm-constraints.txt -c /tmp/otel-constraints.txt *.whl \ - && rm /tmp/otel-constraints.txt - -# Install NIXL ROCm wheel -RUN --mount=type=bind,from=build_nixl,src=/app/install,target=/nixl_install \ - uv pip install --system /nixl_install/*.whl - -# Recompute the base's triton fingerprint to detect resolver interference. -RUN EXPECTED=$(grep '^TRITON_SHA256: ' /app/versions.txt | cut -d' ' -f2) \ - && ACTUAL=$(find $(python3 -c 'import triton, os; print(os.path.dirname(triton.__file__))') \ - -type f \( -name '*.py' -o -name '*.so' \) | sort | xargs sha256sum | sha256sum | cut -d' ' -f1) \ - && echo "triton sha256 expected=${EXPECTED} actual=${ACTUAL}" \ - && test -n "${EXPECTED}" \ - && test "${EXPECTED}" = "${ACTUAL}" \ - && python3 -c "import torch; assert torch.version.hip, f'non-ROCm torch: {torch.__version__}'; print(f'torch={torch.__version__}')" - -# Recompute the base's pybind11 fingerprint to detect resolver interference. -RUN EXPECTED=$(grep '^PYBIND11_SHA256: ' /app/versions.txt | cut -d' ' -f2) \ - && EXPECTED_ABI=$(grep '^PYBIND11_INTERNALS_VERSION: ' /app/versions.txt | cut -d' ' -f2) \ - && INC=$(python3 -c 'import pybind11; print(pybind11.get_include())') \ - && ACTUAL=$(find "${INC}" -type f -name '*.h' | sort | xargs sha256sum | sha256sum | cut -d' ' -f1) \ - && ACTUAL_ABI=$(grep -rhE '^#[[:space:]]*define PYBIND11_INTERNALS_VERSION' "${INC}" | awk '{print $NF}') \ - && echo "pybind11 $(pip show pybind11 | awk '/^Version:/ {print $2}')" \ - "sha256 expected=${EXPECTED} actual=${ACTUAL}" \ - "internals expected=${EXPECTED_ABI} actual=${ACTUAL_ABI}" \ - && test -n "${EXPECTED}" \ - && test "${EXPECTED}" = "${ACTUAL}" \ - && test "${EXPECTED_ABI}" = "${ACTUAL_ABI}" - -ARG COMMON_WORKDIR -ARG BASE_IMAGE -ARG NIC_BACKEND -ARG AINIC_VERSION - -# Copy over the benchmark scripts as well -COPY --from=export_vllm /benchmarks ${COMMON_WORKDIR}/vllm/benchmarks -COPY --from=export_vllm /examples ${COMMON_WORKDIR}/vllm/examples -COPY --from=export_vllm /docker ${COMMON_WORKDIR}/vllm/docker - -# Use legacy IPC mode for HSA to avoid GPU memory pinning issues with UCX rocm_ipc -# See: https://github.com/ROCm/rocm-libraries/issues/6266 -ENV HSA_ENABLE_IPC_MODE_LEGACY=1 -ENV UCX_ROCM_COPY_SIGPOOL_MAX_ELEMS=inf -ENV UCX_ROCM_IPC_SIGPOOL_MAX_ELEMS=inf - ENV TOKENIZERS_PARALLELISM=false - # ENV that can improve safe tensor loading, and end-to-end time ENV SAFETENSORS_FAST_GPU=1 - # Performance environment variable. ENV HIP_FORCE_DEV_KERNARG=1 - # Keep torch.cuda.is_available() fork-safe (see vllm/env_override.py). ENV PYTORCH_NVML_BASED_CUDA_CHECK=1 +ARG STACK +LABEL ai.vllm.rocm.stack=${STACK} -# Workaround for ROCm profiler limits -RUN echo "ROCTRACER_MAX_EVENTS=10000000" > ${COMMON_WORKDIR}/libkineto.conf -ENV KINETO_CONFIG="${COMMON_WORKDIR}/libkineto.conf" -RUN echo "VLLM_BASE_IMAGE=${BASE_IMAGE}" >> ${COMMON_WORKDIR}/versions.txt \ - && echo "MORI_NIC_BACKEND=${NIC_BACKEND}" >> ${COMMON_WORKDIR}/versions.txt \ - && echo "AINIC_VERSION=${AINIC_VERSION}" >> ${COMMON_WORKDIR}/versions.txt +FROM dev AS rust-toolchain +ENV CARGO_HOME=/root/.cargo +ENV RUSTUP_HOME=/root/.rustup +ENV PATH=${CARGO_HOME}/bin:${PATH} +RUN --mount=type=bind,source=rust-toolchain.toml,target=/tmp/rust-toolchain.toml \ + TOOLCHAIN="$(grep '^channel' /tmp/rust-toolchain.toml | sed 's/.*= *"\(.*\)"/\1/')" \ + && curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain none \ + && rustup toolchain install "${TOOLCHAIN}" \ + && rustup default "${TOOLCHAIN}" \ + && rustc --version \ + && cargo --version -CMD ["/bin/bash"] +# Test-only dependencies, a content-keyed image like the runtime (build.sh: test-deps--), +# so every build of a key reuses these layers by digest instead of rebuilding them from cache. +FROM runtime AS test-deps +COPY --from=rust-toolchain /root/.cargo /root/.cargo +COPY --from=rust-toolchain /root/.rustup /root/.rustup +ENV CARGO_HOME=/root/.cargo RUSTUP_HOME=/root/.rustup PATH=/root/.cargo/bin:$PATH +# DeepEP and the rdma-core its rocshmem needs: CI only, as before (see build_rdma_core). +COPY --from=build_rdma_core /out / +RUN --mount=type=bind,from=build_deepep,src=/app/deep_install,target=/deepep \ + --mount=type=cache,id=vllm-rocm-uv,target=/root/.cache/uv \ + ldconfig && uv pip install --system -c /etc/rocm-constraints.txt /deepep/*.whl +RUN --mount=type=bind,source=requirements/test/rocm.txt,target=/t.txt --mount=type=bind,source=tests/vllm_test_utils,target=/tu,rw \ + --mount=type=cache,id=vllm-rocm-uv,target=/root/.cache/uv \ + sp="$(python3 -c 'import sysconfig; print(sysconfig.get_paths()["purelib"])')" \ + && pip freeze --path "$sp" | sort >/tmp/shipped && uv pip install --system -c /etc/rocm-constraints.txt -r /t.txt /tu \ + && ! pip freeze --path "$sp" | sort | comm -23 /tmp/shipped - | grep . # a test dep changed a shipped package (apt's python3-* may be shadowed) +RUN --mount=type=bind,from=build_mamba,src=/app/mamba_install,target=/mamba \ + --mount=type=cache,id=vllm-rocm-uv,target=/root/.cache/uv \ + uv pip install --system --no-deps /mamba/*.whl +ENV HF_XET_HIGH_PERFORMANCE=1 -FROM final_common AS final_lmcache_false +# ==== per-commit stages: nothing below is part of the runtime key ==== -FROM final_common AS final_lmcache_true -RUN --mount=type=bind,from=build_lmcache,src=/app/install,target=/lmcache_install \ - --mount=type=cache,target=/root/.cache/uv \ - uv pip install --system /lmcache_install/*.whl \ - && test "$(python3 -c 'import numpy; print(numpy.__version__)')" = "2.3.5" \ - && uv pip check --system \ - && python3 -c "import lmcache, lmcache.c_ops; print('lmcache', lmcache.__version__)" +# The pin lives below the marker: a bump rebuilds csrc, not the runtime. +FROM dev AS triton-kernels +ARG ROCM_TRITON_KERNELS_COMMIT=669b31acc1dd1b3fd93286afd5db67f65d9f7557 +ADD https://codeload.github.com/ROCm/triton/tar.gz/${ROCM_TRITON_KERNELS_COMMIT} \ + /tmp/rocm-triton.tar.gz +RUN mkdir -p /opt/rocm-triton-kernels \ + && tar --extract --gzip --file /tmp/rocm-triton.tar.gz \ + --directory /opt/rocm-triton-kernels --strip-components=4 \ + --no-same-owner \ + "triton-${ROCM_TRITON_KERNELS_COMMIT}/python/triton_kernels/triton_kernels" \ + && test -f /opt/rocm-triton-kernels/__init__.py \ + && rm /tmp/rocm-triton.tar.gz +ENV TRITON_KERNELS_SRC_DIR=/opt/rocm-triton-kernels ROCM_TRITON_KERNELS_COMMIT=${ROCM_TRITON_KERNELS_COMMIT} + +# The version every per-commit artifact carries. build.sh builds from an export of HEAD (no .git) +# and writes .git_archival.txt from the host's `git describe`; without it, e.g. locally, .git and a +# v* tag are used. +FROM dev AS version-calc +RUN --mount=type=bind,target=/src,rw cd /src \ + && if [ -f .git_archival.txt ]; then rm -rf .git; \ + else git config --global --add safe.directory '*'; fi \ + && python3 -c "from setuptools_scm import get_version; print(get_version(git_describe_command=['git', 'describe', '--dirty', '--tags', '--long', '--abbrev=40', '--match', 'v[0-9]*']))" \ + > /vllm-version.txt \ + && cat /vllm-version.txt \ + && if grep -q '^0\.1\.dev' /vllm-version.txt; then echo "no v* tag reachable from HEAD: git fetch --tags" >&2; exit 1; fi +FROM scratch AS version +COPY --from=version-calc /vllm-version.txt / + +# Compiles the `vllm-rs` frontend once per change to the rust sources, so the wheel build does not +# need the rust toolchain. The crates, their downloads and target/ stay in this stage's layers (no +# cache mounts), so rust-build always continues from the target/ of these exact sources. +FROM rust-toolchain AS rust-deps +# Cap cargo parallelism to avoid exhausting the AMD CI host's open-file limit +# (rustc spawns enough concurrent processes to hit RLIMIT_NOFILE otherwise). +ENV CARGO_BUILD_JOBS=4 +ENV CARGO_NET_RETRY=10 +ENV RUSTUP_MAX_RETRIES=10 +WORKDIR /app/vllm +COPY requirements/build/rust.txt requirements/build/rust.txt +RUN --mount=type=cache,id=vllm-rocm-uv,target=/root/.cache/uv \ + uv pip install --system -r requirements/build/rust.txt +COPY rust/Cargo.lock rust/Cargo.toml rust/ +COPY rust/proto rust/proto +COPY rust/src rust/src +COPY rust-toolchain.toml rust-toolchain.toml +COPY tools/build_rust.py tools/build_rust.sh tools/ +RUN SETUPTOOLS_SCM_PRETEND_VERSION=0.0.0 bash tools/build_rust.sh + +# Per commit: vllm-rs embeds the vLLM version, so cargo recompiles only the crates that read it and +# relinks (~30 s). Those few rustc processes stay far from the open-file limit, and the job count +# also caps the thin-LTO threads, so lift the cap. +FROM rust-deps AS rust-build +COPY --from=version /vllm-version.txt /tmp/vllm-version.txt +RUN CARGO_BUILD_JOBS="$(nproc)" SETUPTOOLS_SCM_PRETEND_VERSION="$(cat /tmp/vllm-version.txt)" \ + bash tools/build_rust.sh \ + && test -x vllm/vllm-rs + +# Native extensions only: copies just the files that affect the HIP/C++ build (and the map tool). +FROM triton-kernels AS csrc +WORKDIR /app/vllm +COPY setup.py CMakeLists.txt pyproject.toml ./ +COPY tools/build_rust.py tools/build_rust.py +COPY cmake cmake/ +COPY csrc csrc/ +COPY vllm/envs.py vllm/__init__.py vllm/ +# Empty: the stack's full PYTORCH_ROCM_ARCH (release); CI passes the arches it can test. +ARG ARCH +# setup.py reads requirements/rocm.txt for install_requires. This wheel only carries the .so files +# the wheel stage repackages with the real requirements, so empty files keep a requirements bump +# from recompiling HIP. +# CI builds (ARCH set) also write the kernel symbol map (CI test selection: which source file each +# GPU kernel came from). It reads the build tree, so it runs here, before the tree is removed, and +# an edit to its tool rebuilds this stage. It never fails the build. +# Release (SCCACHE_BUCKET_NAME, us-west-2): its agents run one job each, so the ccache mount starts +# empty; sccache shares per-file results in S3 through the agent's instance role. setup.py prefers +# sccache when it is on PATH; without S3 access it is removed and ccache compiles as in CI. +ARG SCCACHE_BUCKET_NAME +RUN --mount=type=cache,id=vllm-rocm-ccache,target=/root/.cache/ccache \ + --mount=type=bind,source=tools/ci/rocm_kernel_symbol_map.py,target=/tmp/rocm_kernel_symbol_map.py \ + mkdir -p requirements && : > requirements/rocm.txt \ + && { grep -Fq "set(TRITON_KERNELS_TAG \"${ROCM_TRITON_KERNELS_COMMIT}\")" \ + cmake/external_projects/triton_kernels.cmake \ + || echo "WARNING: Dockerfile.rocm ROCM_TRITON_KERNELS_COMMIT (${ROCM_TRITON_KERNELS_COMMIT}) does not match cmake TRITON_KERNELS_TAG; the Docker build uses the Dockerfile pin, cmake governs only non-Docker builds"; } \ + && { [ -z "${SCCACHE_BUCKET_NAME}" ] \ + || { export SCCACHE_BUCKET="${SCCACHE_BUCKET_NAME}" SCCACHE_REGION=us-west-2 SCCACHE_IDLE_TIMEOUT=0 \ + && curl -fsSL --retry 3 -o /tmp/sccache.tgz https://github.com/mozilla/sccache/releases/download/v0.8.1/sccache-v0.8.1-x86_64-unknown-linux-musl.tar.gz \ + && echo "e0ee621fb16b6940666cd770b091c62cadafd3e062dd12e3a49d9caaff3b795f /tmp/sccache.tgz" | sha256sum -c - \ + && tar -xzf /tmp/sccache.tgz -C /usr/local/bin --strip-components=1 --wildcards '*/sccache' \ + && rm /tmp/sccache.tgz && sccache --start-server; } \ + || { echo "WARNING: no S3 sccache; compiling with ccache only"; rm -f /usr/local/bin/sccache /tmp/sccache.tgz; }; } \ + && CCACHE_BASEDIR="$PWD" PYTORCH_ROCM_ARCH="${ARCH:-$PYTORCH_ROCM_ARCH}" \ + SETUPTOOLS_SCM_PRETEND_VERSION=0.0.0+rocm.csrc.build PYTHONDONTWRITEBYTECODE=1 \ + LDFLAGS="-fuse-ld=mold" MAX_JOBS="${MAX_JOBS:-$(nproc)}" python3 setup.py bdist_wheel --dist-dir=/dist \ + && { sccache --stop-server 2>/dev/null || true; } \ + && mkdir /kernel-map \ + && { [ -z "${ARCH}" ] \ + || timeout 30m python3 /tmp/rocm_kernel_symbol_map.py --build-root build --source-root . \ + --out /kernel-map/kernel_symbol_map.rocm.json.gz \ + || echo "WARNING: no kernel symbol map; CI test selection falls back to its static rule"; } \ + && rm -rf build + +# The map, stamped with this build's commit (CI selection only uses a map of the commit it ran). A +# csrc layer reused from another commit was built from the same sources, so its map holds here. +FROM dev AS kernel-symbol-map-stamp +ARG BUILDKITE_COMMIT +RUN --mount=type=bind,from=csrc,src=/kernel-map,target=/in python3 - "${BUILDKITE_COMMIT}" <<'EOF' +import gzip, json, os, sys +os.makedirs("/out") +if os.path.exists(src := "/in/kernel_symbol_map.rocm.json.gz"): + with gzip.open(src, "rt") as f: + m = json.load(f) + m["commit"] = sys.argv[1] + with gzip.open("/out/kernel_symbol_map.rocm.json.gz", "wt") as f: + json.dump(m, f, separators=(",", ":")) +EOF +FROM scratch AS kernel-symbol-map +COPY --from=kernel-symbol-map-stamp /out/ / + +# The full vLLM wheel, repackaged around the csrc extensions and the rust artifacts. +FROM dev AS wheel +COPY --from=csrc /dist /precompiled +COPY --from=rust-build /app/vllm/vllm/vllm-rs /app/vllm/vllm/_rust_*.so /rust/ +COPY --from=version /vllm-version.txt / +# The published wheel pins the exact stack builds (torch, triton, ...) it was compiled against. +RUN --mount=type=bind,target=/src,rw cd /src && cp /rust/* vllm/ \ + && ! grep -n 'git+' requirements/common.txt requirements/rocm.txt \ + && pip list --format=freeze \ + | grep -iE '^(torch|triton|torchvision|torchaudio|amdsmi|flash[-_]attn|amd[-_]aiter)==' >> requirements/rocm.txt \ + && SETUPTOOLS_SCM_PRETEND_VERSION="$(cat /vllm-version.txt)" VLLM_USE_PRECOMPILED=1 \ + VLLM_PRECOMPILED_WHEEL_LOCATION="$(ls /precompiled/*.whl)" VLLM_DOCKER_BUILD_CONTEXT=1 \ + PYTHONDONTWRITEBYTECODE=1 python3 setup.py bdist_wheel --dist-dir=/dist + +# The installed csrc extensions (~700 MB), as their own layer: it depends only on the csrc stage, +# so a commit that leaves csrc alone reuses the layer and pulls only the Python one below. +FROM dev AS native +RUN --mount=type=bind,from=csrc,src=/dist,target=/csrc \ + sp="$(python3 -c 'import sysconfig; print(sysconfig.get_paths()["purelib"])')" \ + && mkdir -p "/out$sp" && unzip -q /csrc/*.whl 'vllm/*.so' -d "/out$sp" + +# Bytecode compiled as if installed at / (not /out), so tracebacks name the real paths. +# The csrc extensions come from the native layer instead. +FROM runtime AS install +RUN --mount=type=bind,from=wheel,src=/dist,target=/dist \ + --mount=type=bind,from=csrc,src=/dist,target=/csrc \ + pip install --no-deps --no-compile --root /out /dist/*.whl \ + && sp="$(python3 -c 'import sysconfig; print(sysconfig.get_paths()["purelib"])')" \ + && unzip -Z1 /csrc/*.whl 'vllm/*.so' | sed "s|^|/out$sp/|" | xargs rm \ + && python3 -m compileall -q -j 0 -s /out -p / /out + +FROM runtime AS vllm-openai +COPY --link --from=native /out / +COPY --link --from=install /out / +COPY --link benchmarks /app/vllm/benchmarks +COPY --link examples /app/vllm/examples +ARG CI_ARCH +LABEL ai.vllm.rocm.ci-arch=${CI_ARCH} +ARG VLLM_VERSION +LABEL org.opencontainers.image.version=${VLLM_VERSION} +ENTRYPOINT ["vllm", "serve"] -FROM final_lmcache_${INSTALL_LMCACHE} AS final +# Release check (cacheonly): vLLM imports offline and _C carries every arch of PYTORCH_ROCM_ARCH. +FROM vllm-openai AS openai-smoke +RUN --network=none so="$(python3 -c 'import os, vllm, vllm._C; print(os.path.dirname(vllm.__file__))')/_C.abi3.so" \ + && for a in $(echo "$PYTORCH_ROCM_ARCH" | tr ';' ' '); do \ + llvm-objdump --offloading "$so" | grep -qw "$a" || { echo "_C lacks $a" >&2; exit 1; }; done + +FROM test-deps AS test-base +# AITER nightly: replace the stack's aiter with the newest nightly wheel that fits this image's ROCm, +# torch and Python (never cached, see the bake file). AITER_SPEC pins a pip spec instead. +ARG AITER_NIGHTLY +ARG AITER_SPEC +RUN --mount=type=bind,source=.buildkite/scripts/rocm/select_aiter_nightly_wheel.py,target=/tmp/select.py \ + set -e; spec="$AITER_SPEC"; \ + if [ -z "$spec" ] && [ "$AITER_NIGHTLY" = 1 ]; then \ + out="$(python3 -P /tmp/select.py)" || { echo "$out" >&2; exit 1; }; \ + echo "$out"; spec="$(echo "$out" | head -n 1)"; \ + fi; \ + if [ -n "$spec" ]; then \ + pip install --no-cache-dir --pre --no-deps --force-reinstall "$spec" || exit 1; \ + # Locate aiter without importing it: import needs a GPU, which a build has not. A wheel built + # against a newer glibc or libstdc++ than this image installs fine but fails every test that + # loads its modules; fail here instead. + aiter="$(python3 -c 'import importlib.util as u; print(u.find_spec("aiter").origin)')"; \ + ! find "${aiter%/*}" -name '*.so' -exec ldd {} + 2>&1 | grep "version .* not found"; \ + fi -#Set entrypoint for vllm-openai official images -FROM final AS vllm-openai -ENTRYPOINT ["vllm", "serve"] +FROM test-base AS test +COPY --link --from=native /out / +COPY --link --from=install /out / +COPY --link --exclude=vllm --exclude=.git . /vllm-workspace/ +# Source under src/ so it does not shadow the installed package in tests. +COPY --link vllm /vllm-workspace/src/vllm +WORKDIR /vllm-workspace + +FROM test AS test-smoke +RUN --network=none python3 -c "import vllm, vllm._C, vllm._rocm_C; print(vllm.__version__)" + +FROM scratch AS wheels +COPY --link --from=stack-wheels / / +COPY --link --from=wheel /dist/ / + +# CI: the vLLM wheel alone (a build artifact for the python-only install test), and the expensive +# compile stages, whose cache PR builds export. +FROM scratch AS vllm-wheel +COPY --link --from=wheel /dist/ / +FROM scratch AS compile +COPY --link --from=csrc /dist /csrc +COPY --link --from=rust-deps /app/vllm/rust/Cargo.lock /rust/ diff --git a/docker/Dockerfile.rocm_72 b/docker/Dockerfile.rocm_72 deleted file mode 100644 index eeaa9d6c1b18..000000000000 --- a/docker/Dockerfile.rocm_72 +++ /dev/null @@ -1,1018 +0,0 @@ -# default base image -ARG REMOTE_VLLM="0" -ARG VLLM_REPO="https://github.com/vllm-project/vllm.git" -ARG VLLM_BRANCH="main" -ARG COMMON_WORKDIR=/app -ARG BASE_IMAGE=vllm/vllm-openai-rocm:base-nightly-rocm72 -ARG CI_BASE_IMAGE=rocm/vllm-dev:ci_base -ARG ROCM_TRITON_KERNELS_COMMIT=669b31acc1dd1b3fd93286afd5db67f65d9f7557 -ARG INSTALL_LMCACHE=false -# NIC backend for MoRI RDMA support. -# By default (all), drivers and userspace libraries for all supported NIC types -# (ainic and bnxt) are installed; MoRI selects the appropriate one at runtime. -# To install drivers for a single NIC type only, set NIC_BACKEND explicitly: -# --build-arg NIC_BACKEND=ainic # AMD AINIC (Pensando) only -# --build-arg NIC_BACKEND=bnxt # Broadcom Thor-2 only -# --build-arg NIC_BACKEND=none # Install nothing. -ARG NIC_BACKEND=all -# AMD AINIC apt repo settings -# Users can specify a custom version compatible with their host drivers. -# The default version has been tested with ionic-dkms=26.03.3.001 -# (libionic1=54.0-187-1), required for MoRI-EP/NIXL on Pollara/AINIC. -ARG AINIC_VERSION=1.117.5-a-77 -ARG UBUNTU_CODENAME=jammy - -# Sccache configuration. Release builds use this today; CI can opt in when a -# shared S3-compatible cache backend is available. -ARG USE_SCCACHE -ARG SCCACHE_DOWNLOAD_URL -ARG SCCACHE_ENDPOINT -ARG SCCACHE_BUCKET_NAME=vllm-build-sccache -ARG SCCACHE_REGION_NAME=us-west-2 -ARG SCCACHE_S3_NO_CREDENTIALS=0 - -FROM ${BASE_IMAGE} AS base - -ARG ARG_PYTORCH_ROCM_ARCH -ENV PYTORCH_ROCM_ARCH=${ARG_PYTORCH_ROCM_ARCH:-${PYTORCH_ROCM_ARCH}} - -# Install build dependencies and utilities -RUN apt-get update -q -y && apt-get install -q -y \ - sqlite3 libsqlite3-dev libfmt-dev libmsgpack-dev libsuitesparse-dev \ - apt-transport-https ca-certificates wget curl \ - libnuma-dev ccache mold -RUN --mount=type=cache,target=/root/.cache/pip \ - python3 -m pip install --upgrade pip -# Note: mold is installed but not set as the system default linker because -# some packages use JIT compilation at runtime with flags mold does not support. -# Build stages opt in via LDFLAGS="-fuse-ld=mold". -# Remove sccache only if not using sccache (it exists in base image from Dockerfile.rocm_72_base) -ARG USE_SCCACHE -RUN if [ "$USE_SCCACHE" != "1" ]; then \ - apt-get purge -y sccache || true; \ - python3 -m pip uninstall -y sccache || true; \ - rm -f "$(which sccache)" || true; \ - fi - -# Install UV — download first, then run, so a curl failure is not masked by the pipe -RUN curl -LsSf --retry 3 --retry-delay 5 https://astral.sh/uv/install.sh -o /tmp/uv-install.sh \ - && env UV_INSTALL_DIR="/usr/local/bin" sh /tmp/uv-install.sh \ - && rm -f /tmp/uv-install.sh \ - && uv --version - -# This timeout (in seconds) is necessary when installing some dependencies via uv since it's likely to time out -# Reference: https://github.com/astral-sh/uv/pull/1694 -ENV UV_HTTP_TIMEOUT=500 -ENV UV_INDEX_STRATEGY="unsafe-best-match" -# Use copy mode to avoid hardlink failures with Docker cache mounts -ENV UV_LINK_MODE=copy -# ccache directory - persisted across layer rebuilds via cache mounts. -ENV CCACHE_DIR=/root/.cache/ccache -ENV CCACHE_COMPILERCHECK=content -# Empty by default so build steps fall back to $(nproc); CI can override. -ARG max_jobs -ENV MAX_JOBS=${max_jobs} - -# Install sccache if USE_SCCACHE is enabled (for release builds) -ARG USE_SCCACHE -ARG SCCACHE_DOWNLOAD_URL -ARG SCCACHE_ENDPOINT -ARG SCCACHE_BUCKET_NAME -ARG SCCACHE_REGION_NAME -ARG SCCACHE_S3_NO_CREDENTIALS -RUN if [ "$USE_SCCACHE" = "1" ]; then \ - if command -v sccache >/dev/null 2>&1; then \ - echo "sccache already installed, skipping installation"; \ - sccache --version; \ - else \ - echo "Installing sccache..." \ - && SCCACHE_ARCH="x86_64" \ - && SCCACHE_VERSION="v0.8.1" \ - && SCCACHE_DL_URL="${SCCACHE_DOWNLOAD_URL:-https://github.com/mozilla/sccache/releases/download/${SCCACHE_VERSION}/sccache-${SCCACHE_VERSION}-${SCCACHE_ARCH}-unknown-linux-musl.tar.gz}" \ - && curl -L -o /tmp/sccache.tar.gz ${SCCACHE_DL_URL} \ - && tar -xzf /tmp/sccache.tar.gz -C /tmp \ - && mv /tmp/sccache-${SCCACHE_VERSION}-${SCCACHE_ARCH}-unknown-linux-musl/sccache /usr/bin/sccache \ - && chmod +x /usr/bin/sccache \ - && rm -rf /tmp/sccache.tar.gz /tmp/sccache-${SCCACHE_VERSION}-${SCCACHE_ARCH}-unknown-linux-musl \ - && sccache --version; \ - fi; \ - fi - -# Set sccache environment variables only when USE_SCCACHE=1 -# This prevents S3 config from leaking into images when sccache is not used -ARG USE_SCCACHE -ENV SCCACHE_BUCKET=${USE_SCCACHE:+${SCCACHE_BUCKET_NAME}} -ENV SCCACHE_REGION=${USE_SCCACHE:+${SCCACHE_REGION_NAME}} -ENV SCCACHE_S3_NO_CREDENTIALS=${USE_SCCACHE:+${SCCACHE_S3_NO_CREDENTIALS}} -ENV SCCACHE_IDLE_TIMEOUT=${USE_SCCACHE:+0} - -ARG COMMON_WORKDIR -WORKDIR ${COMMON_WORKDIR} - - -# ----------------------- -# vLLM fetch stages -FROM base AS fetch_vllm_0 -ONBUILD COPY ./ vllm/ -FROM base AS fetch_vllm_1 -ARG VLLM_REPO -ARG VLLM_BRANCH -ONBUILD RUN git clone ${VLLM_REPO} \ - && cd vllm \ - && git fetch -v --prune -- origin ${VLLM_BRANCH} \ - && git checkout FETCH_HEAD \ - && if [ ${VLLM_REPO} != "https://github.com/vllm-project/vllm.git" ] ; then \ - git remote add upstream "https://github.com/vllm-project/vllm.git" \ - && git fetch upstream ; fi -FROM fetch_vllm_${REMOTE_VLLM} AS fetch_vllm - -# Resolve the device-independent source version once for every downstream -# artifact. setuptools-scm reads Git metadata for local/remote sources and the -# archive metadata generated for canonical CI contexts. -FROM fetch_vllm AS vllm-version -ARG COMMON_WORKDIR -RUN --mount=type=cache,id=vllm-rocm-uv,target=/root/.cache/uv \ - cd ${COMMON_WORKDIR}/vllm \ - && uv pip install --system -r requirements/build/rust.txt \ - && python3 -c \ - "from setuptools_scm import get_version; print(get_version(git_describe_command=['git', 'describe', '--dirty', '--tags', '--long', '--abbrev=40', '--match', 'v[0-9]*']))" \ - > /vllm-version.txt - -# ----------------------- -# Rust toolchain inputs -# -# Keep these separate from Rust source inputs so Rust source-only changes do not -# invalidate the long-lived ci_base image that carries the toolchain. -FROM scratch AS rust_toolchain_input_0 -COPY rust-toolchain.toml /rust-toolchain-input/vllm/rust-toolchain.toml - -FROM base AS rust_toolchain_input_1 -ARG VLLM_REPO -ARG VLLM_BRANCH -RUN git clone --no-checkout --filter=blob:none ${VLLM_REPO} /rust-toolchain-input/vllm \ - && cd /rust-toolchain-input/vllm \ - && git fetch -v --prune -- origin ${VLLM_BRANCH} \ - && git sparse-checkout init --no-cone \ - && git sparse-checkout set \ - rust-toolchain.toml \ - && git checkout FETCH_HEAD - -FROM rust_toolchain_input_${REMOTE_VLLM} AS rust-toolchain-input - -# ----------------------- -# Rust build inputs -# -# REMOTE_VLLM=0: use only the local files that can affect Rust artifacts. -# REMOTE_VLLM=1: fetch only those same files from the requested remote ref. -FROM scratch AS rust_input_0 -COPY requirements/build/rust.txt /rust-input/vllm/requirements/build/rust.txt -COPY rust/Cargo.lock rust/Cargo.toml /rust-input/vllm/rust/ -COPY rust/proto /rust-input/vllm/rust/proto -COPY rust/src /rust-input/vllm/rust/src -COPY rust-toolchain.toml /rust-input/vllm/rust-toolchain.toml -COPY tools/build_rust.py /rust-input/vllm/tools/build_rust.py -COPY tools/build_rust.sh /rust-input/vllm/tools/build_rust.sh - -FROM base AS rust_input_1 -ARG VLLM_REPO -ARG VLLM_BRANCH -RUN git clone --no-checkout --filter=blob:none ${VLLM_REPO} /rust-input/vllm \ - && cd /rust-input/vllm \ - && git fetch -v --prune -- origin ${VLLM_BRANCH} \ - && git sparse-checkout init --no-cone \ - && git sparse-checkout set \ - requirements/build/rust.txt \ - rust/Cargo.lock \ - rust/Cargo.toml \ - rust/proto/** \ - rust/src/** \ - rust-toolchain.toml \ - tools/build_rust.py \ - tools/build_rust.sh \ - && git checkout FETCH_HEAD - -FROM rust_input_${REMOTE_VLLM} AS rust-input - -# ----------------------- -# Rust toolchain -FROM base AS rust-toolchain - -ENV CARGO_HOME=/root/.cargo -ENV RUSTUP_HOME=/root/.rustup -ENV PATH=${CARGO_HOME}/bin:${PATH} - -RUN apt-get update -q -y && apt-get install -q -y --no-install-recommends \ - ca-certificates curl \ - && rm -rf /var/lib/apt/lists/* - -COPY --from=rust-toolchain-input /rust-toolchain-input/vllm/rust-toolchain.toml /tmp/rust-toolchain.toml -RUN TOOLCHAIN="$(grep '^channel' /tmp/rust-toolchain.toml | sed 's/.*= *"\(.*\)"/\1/')" \ - && if ! command -v rustup >/dev/null 2>&1; then \ - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \ - | sh -s -- -y --default-toolchain none; \ - fi \ - && rustup toolchain install "${TOOLCHAIN}" \ - && rustup default "${TOOLCHAIN}" \ - && rustc --version \ - && cargo --version - -# ----------------------- -# Rust build stage -# Builds the `vllm-rs` frontend in a dedicated stage so the wheel build stages -# don't need the rust toolchain. -FROM rust-toolchain AS rust-build -ARG COMMON_WORKDIR -ARG USE_SCCACHE - -# Cap cargo parallelism to avoid exhausting the AMD CI host's open-file limit -# (rustc spawns enough concurrent processes to hit RLIMIT_NOFILE otherwise). -ENV CARGO_BUILD_JOBS=4 -ENV CARGO_NET_RETRY=10 -ENV RUSTUP_MAX_RETRIES=10 - -WORKDIR ${COMMON_WORKDIR}/vllm - -COPY --from=rust-input /rust-input/vllm/requirements/build/rust.txt requirements/build/rust.txt - -# BuildKit can run this stage in parallel with ROCm native builds. Keep Rust on -# a separate local sccache daemon while sharing the same remote cache backend. -ENV SCCACHE_SERVER_PORT=4227 - -RUN --mount=type=cache,id=vllm-rocm-uv,target=/root/.cache/uv \ - uv pip install --system -r requirements/build/rust.txt - -COPY --from=rust-input /rust-input/vllm/rust rust -COPY --from=rust-input /rust-input/vllm/rust-toolchain.toml rust-toolchain.toml -COPY --from=rust-input /rust-input/vllm/tools/build_rust.py tools/build_rust.py -COPY --from=rust-input /rust-input/vllm/tools/build_rust.sh tools/build_rust.sh -COPY --from=vllm-version /vllm-version.txt /tmp/vllm-version.txt - -# Build the release binary. Cargo's registry/git caches can be written by -# concurrent BuildKit jobs on shared workers, so lock those cache mounts while -# keeping the cache benefit. Do not cache target/, because stale target metadata -# can outlive source updates across BuildKit cache reuse. -RUN --mount=type=cache,id=vllm-rocm-cargo-registry,target=/root/.cargo/registry,sharing=locked \ - --mount=type=cache,id=vllm-rocm-cargo-git,target=/root/.cargo/git,sharing=locked \ - if [ "$USE_SCCACHE" = "1" ]; then \ - export RUSTC_WRAPPER=sccache \ - && sccache --show-stats; \ - fi \ - && export SETUPTOOLS_SCM_PRETEND_VERSION="$(cat /tmp/vllm-version.txt)" \ - && bash tools/build_rust.sh \ - && test -x vllm/vllm-rs \ - && if [ "$USE_SCCACHE" = "1" ]; then \ - sccache --show-stats; \ - fi - -# ----------------------- -# Shared vLLM build dependencies -# -# Install requirements once, before bringing source-specific inputs into either -# consumer. Native compilation and wheel packaging then share this expensive -# layer while retaining their independent source cache boundaries. -FROM base AS build_vllm_dependencies -ARG COMMON_WORKDIR -ENV VLLM_TARGET_DEVICE=rocm -WORKDIR ${COMMON_WORKDIR}/vllm - -COPY --from=fetch_vllm \ - ${COMMON_WORKDIR}/vllm/requirements/common.txt \ - ${COMMON_WORKDIR}/vllm/requirements/rocm.txt \ - requirements/ - -RUN --mount=type=cache,id=vllm-rocm-uv,target=/root/.cache/uv \ - uv pip install --system -r requirements/rocm.txt - -# Fetch only the pinned ROCm Triton kernels package. Keeping this before the -# vLLM source COPYs makes the download reusable when native sources change. -FROM base AS rocm-triton-kernels -ARG ROCM_TRITON_KERNELS_COMMIT -ADD https://codeload.github.com/ROCm/triton/tar.gz/${ROCM_TRITON_KERNELS_COMMIT} \ - /tmp/rocm-triton.tar.gz -RUN mkdir -p /opt/rocm-triton-kernels \ - && tar --extract --gzip --file /tmp/rocm-triton.tar.gz \ - --directory /opt/rocm-triton-kernels --strip-components=4 \ - --no-same-owner \ - "triton-${ROCM_TRITON_KERNELS_COMMIT}/python/triton_kernels/triton_kernels" \ - && test -f /opt/rocm-triton-kernels/__init__.py \ - && rm /tmp/rocm-triton.tar.gz - -# ----------------------- -# vLLM native build stages -# -# csrc-build intentionally copies only files that affect ROCm native extension -# compilation. That keeps unrelated CI/test/docs edits from invalidating the -# expensive HIP/C++ build layer. -FROM build_vllm_dependencies AS csrc-build -ARG ROCM_TRITON_KERNELS_COMMIT -COPY --from=rocm-triton-kernels \ - /opt/rocm-triton-kernels /opt/rocm-triton-kernels -ENV TRITON_KERNELS_SRC_DIR=/opt/rocm-triton-kernels - -# pyproject.toml is bind-mounted so it is not persisted in the csrc-build image, -# but it still participates in the BuildKit cache key for this RUN step. -COPY setup.py CMakeLists.txt ./ -COPY tools/build_rust.py tools/build_rust.py -COPY cmake cmake/ -COPY csrc csrc/ -COPY vllm/envs.py vllm/envs.py -COPY vllm/__init__.py vllm/__init__.py - -ENV SETUPTOOLS_SCM_PRETEND_VERSION="0.0.0+rocm.csrc.build" - -RUN --mount=type=bind,source=pyproject.toml,target=${COMMON_WORKDIR}/vllm/pyproject.toml \ - --mount=type=cache,id=vllm-rocm-ccache,target=/root/.cache/ccache \ - grep -Fq "set(TRITON_KERNELS_TAG \"${ROCM_TRITON_KERNELS_COMMIT}\")" \ - cmake/external_projects/triton_kernels.cmake \ - && export CCACHE_BASEDIR="$PWD" \ - && echo "=== ccache stats before ROCm native build ===" \ - && (ccache --show-stats || true) \ - && (ccache --zero-stats || true) \ - && EFFECTIVE_MAX_JOBS="${MAX_JOBS:-$(nproc)}" \ - && echo "Building ROCm native extension wheel with MAX_JOBS=${EFFECTIVE_MAX_JOBS}" \ - && PYTHONDONTWRITEBYTECODE=1 LDFLAGS="-fuse-ld=mold" MAX_JOBS="${EFFECTIVE_MAX_JOBS}" python3 setup.py bdist_wheel --dist-dir=dist \ - && test -d dist \ - && ls dist/*.whl >/dev/null \ - && echo "=== ccache stats after ROCm native build ===" \ - && (ccache --show-stats || true) - -# Build the full vLLM ROCm wheel by reusing the native extension wheel from -# csrc-build. This stage still rebuilds for Python/package changes, but skips -# the expensive dependency installation and HIP/C++ compilation when their -# respective inputs are unchanged. -FROM build_vllm_dependencies AS build_vllm -COPY --from=fetch_vllm ${COMMON_WORKDIR}/vllm ${COMMON_WORKDIR}/vllm -COPY --from=csrc-build ${COMMON_WORKDIR}/vllm/dist /precompiled-wheels -COPY --from=vllm-version /vllm-version.txt /tmp/vllm-version.txt - -# Drop the pre-built Rust artifacts into the source tree. setup.py detects -# them and ships them as-is, skipping the local Rust build. -COPY --from=rust-build ${COMMON_WORKDIR}/vllm/vllm/vllm-rs ${COMMON_WORKDIR}/vllm/vllm/vllm-rs -COPY --from=rust-build ${COMMON_WORKDIR}/vllm/vllm/_rust_*.so ${COMMON_WORKDIR}/vllm/vllm/ - -RUN cd "${COMMON_WORKDIR}/vllm" \ - && export SETUPTOOLS_SCM_PRETEND_VERSION="$(cat /tmp/vllm-version.txt)" \ - && export VLLM_USE_PRECOMPILED=1 \ - && export VLLM_PRECOMPILED_WHEEL_LOCATION="$(ls /precompiled-wheels/*.whl)" \ - && export VLLM_DOCKER_BUILD_CONTEXT=1 \ - && echo "Packaging vLLM ROCm wheel using precompiled extensions from ${VLLM_PRECOMPILED_WHEEL_LOCATION}" \ - && PYTHONDONTWRITEBYTECODE=1 python3 setup.py bdist_wheel --dist-dir=dist \ - && test -d dist \ - && ls dist/*.whl >/dev/null -FROM scratch AS export_vllm -ARG COMMON_WORKDIR -COPY --from=build_vllm ${COMMON_WORKDIR}/vllm/dist/*.whl / -COPY --from=build_vllm ${COMMON_WORKDIR}/vllm/requirements /requirements -COPY --from=build_vllm ${COMMON_WORKDIR}/vllm/benchmarks /benchmarks -COPY --from=build_vllm ${COMMON_WORKDIR}/vllm/tests /tests -COPY --from=build_vllm ${COMMON_WORKDIR}/vllm/examples /examples -COPY --from=build_vllm ${COMMON_WORKDIR}/vllm/tools /tools -COPY --from=build_vllm ${COMMON_WORKDIR}/vllm/docker/Dockerfile /docker/Dockerfile -COPY --from=build_vllm ${COMMON_WORKDIR}/vllm/docker/Dockerfile.cpu /docker/Dockerfile.cpu -COPY --from=build_vllm ${COMMON_WORKDIR}/vllm/docker/Dockerfile.rocm_72 /docker/ -COPY --from=build_vllm ${COMMON_WORKDIR}/vllm/docker/Dockerfile.rocm_72_base /docker/ -COPY --from=build_vllm ${COMMON_WORKDIR}/vllm/docker/ci-rocm.hcl /docker/ci-rocm.hcl -COPY --from=build_vllm ${COMMON_WORKDIR}/vllm/docker/docker-bake.hcl /docker/docker-bake.hcl -COPY --from=build_vllm ${COMMON_WORKDIR}/vllm/docker/docker-bake-rocm.hcl /docker/docker-bake-rocm.hcl -COPY --from=build_vllm ${COMMON_WORKDIR}/vllm/.buildkite /.buildkite -COPY --from=build_vllm ${COMMON_WORKDIR}/vllm/pyproject.toml /pyproject.toml -COPY --from=build_vllm ${COMMON_WORKDIR}/vllm/rust /rust -COPY --from=build_vllm ${COMMON_WORKDIR}/vllm/rust-toolchain.toml /rust-toolchain.toml -COPY --from=build_vllm ${COMMON_WORKDIR}/vllm/vllm/v1 /vllm_v1 - -# NIXL/UCX build stages -FROM base AS build_nixl -ARG NIXL_BRANCH="1683cf3b7f3d11674c03c5e861cea22339876c96" -ARG NIXL_REPO="https://github.com/ai-dynamo/nixl.git" -ARG UCX_BRANCH="2ff0b47ce7626584cb9b56c647d08f712c6c3edf" -ARG UCX_REPO="https://github.com/openucx/ucx.git" -ENV ROCM_PATH=/opt/rocm -ENV UCX_HOME=/usr/local/ucx -ENV NIXL_HOME=/usr/local/nixl - -# NIXL build system dependencies and RDMA support -RUN apt-get -y update && apt-get -y install autoconf libtool pkg-config \ - libgrpc-dev \ - libgrpc++-dev \ - libprotobuf-dev \ - protobuf-compiler-grpc \ - libcpprest-dev \ - libaio-dev \ - librdmacm1 \ - librdmacm-dev \ - libibverbs1 \ - libibverbs-dev \ - ibverbs-utils \ - rdmacm-utils \ - ibverbs-providers \ - && rm -rf /var/lib/apt/lists/* - -RUN --mount=type=cache,target=/root/.cache/uv \ - uv pip install --system meson meson-python pyyaml types-PyYAML \ - auditwheel build patchelf pytest tomlkit "setuptools>=80.9.0" - -RUN --mount=type=cache,target=/root/.cache/ccache \ - cd /usr/local/src && \ - git clone ${UCX_REPO} && \ - cd ucx && \ - git checkout ${UCX_BRANCH} && \ - ./autogen.sh && \ - mkdir build && cd build && \ - CC="ccache gcc" CXX="ccache g++" \ - ../configure \ - --prefix=/usr/local/ucx \ - --enable-shared \ - --disable-static \ - --disable-doxygen-doc \ - --enable-optimizations \ - --enable-devel-headers \ - --with-rocm=${ROCM_PATH} \ - --with-verbs \ - --with-dm \ - --enable-mt && \ - make -j$(nproc) && \ - make install - -ENV PATH=/usr/local/ucx/bin:$PATH -ENV LD_LIBRARY_PATH=${UCX_HOME}/lib:${LD_LIBRARY_PATH} - -RUN --mount=type=cache,target=/root/.cache/ccache \ - git clone ${NIXL_REPO} /opt/nixl && \ - cd /opt/nixl && \ - git checkout ${NIXL_BRANCH} && \ - CC="ccache gcc" CXX="ccache g++" \ - meson setup build --prefix=${NIXL_HOME} \ - -Ducx_path=${UCX_HOME} \ - -Dwheel_variant=rocm \ - -Dbuild_tests=false \ - -Dbuild_examples=false && \ - cd build && \ - ninja -j$(nproc) && \ - ninja install && \ - echo "${NIXL_HOME}/lib/$(uname -m)-linux-gnu" \ - > /etc/ld.so.conf.d/nixl.conf && \ - echo "${NIXL_HOME}/lib/$(uname -m)-linux-gnu/plugins" \ - >> /etc/ld.so.conf.d/nixl.conf && \ - ldconfig - -# Generate the ROCm NIXL wheel. Upstream's generic wheel helper detects CUDA, -# so configure the ROCm wheel variant directly through Meson. -# Exclude libcore and libpull from auditwheel: transitive dependencies -# that are not shipped in the wheel and vary across base images. -# Tag manylinux_2_35 to match the Ubuntu 22.04 base: NIXL >= 1.5 links -# GLIBCXX_3.4.30 symbols, which manylinux_2_34 does not allow. -RUN cd /opt/nixl && \ - ./contrib/tomlutil.py --wheel-name nixl-rocm pyproject.toml && \ - CC="ccache gcc" CXX="ccache g++" \ - uv build --wheel --no-build-isolation --out-dir /tmp/nixl_wheels \ - --python ${PYTHON_VERSION} \ - -Csetup-args=-Ducx_path=${UCX_HOME} \ - -Csetup-args=-Dwheel_variant=rocm \ - -Csetup-args=-Dbuild_tests=false \ - -Csetup-args=-Dbuild_examples=false && \ - mkdir -p /tmp/nixl_wheels/repaired /app/install && \ - auditwheel repair \ - --exclude 'libamdhip64*' \ - --exclude 'libcore*' \ - --exclude 'libpull*' \ - /tmp/nixl_wheels/nixl_rocm*.whl \ - --plat manylinux_2_35_$(uname -m) \ - --wheel-dir /tmp/nixl_wheels/repaired && \ - ./contrib/wheel_add_ucx_plugins.py \ - --ucx-plugins-dir ${UCX_HOME}/lib/ucx \ - --nixl-plugins-dir ${NIXL_HOME}/lib/$(uname -m)-linux-gnu/plugins \ - /tmp/nixl_wheels/repaired/*.whl && \ - cp /tmp/nixl_wheels/repaired/*.whl /app/install - -# Fetch and verify pinned LMCache source. -FROM base AS lmcache_source -ARG LMCACHE_REPO="https://github.com/LMCache/LMCache.git" -ARG LMCACHE_REF="140819c9d57a975dbc5678a6459a218e544cb58b" -ARG LMCACHE_VERSION="0.5.3" - -# Remove NVIDIA-only dependencies and accept the release image's NumPy version. -RUN printf '%s\n' "${LMCACHE_REF}" | grep -Eq '^[0-9a-f]{40}$' \ - && git clone --depth=1 --branch "v${LMCACHE_VERSION}" \ - "${LMCACHE_REPO}" /app/lmcache \ - && test "$(git -C /app/lmcache rev-parse HEAD)" = "${LMCACHE_REF}" \ - && test "$(grep -Ec '^(cufile-python|nvtx)([<=>[:space:]]|$)' \ - /app/lmcache/requirements/common.txt)" -eq 2 \ - && test "$(grep -Fxc 'numpy<=2.2.6' \ - /app/lmcache/requirements/common.txt)" -eq 1 \ - && sed -i -E \ - -e '/^(cufile-python|nvtx)([<=>[:space:]]|$)/d' \ - -e 's/^numpy<=2\.2\.6$/numpy<=2.3.5/' \ - /app/lmcache/requirements/common.txt \ - && rm -rf /app/lmcache/.git - -FROM base AS build_lmcache -COPY --from=lmcache_source \ - /app/lmcache/requirements/build.txt /tmp/lmcache-build-requirements.txt -RUN --mount=type=cache,id=vllm-rocm-uv,target=/root/.cache/uv \ - uv pip install --system -r /tmp/lmcache-build-requirements.txt - -ARG LMCACHE_VERSION="0.5.3" -ARG LMCACHE_ROCM_ARCH="gfx942;gfx950" -ARG USE_SCCACHE - -COPY --from=lmcache_source /app/lmcache /app/lmcache -WORKDIR /app/lmcache - -RUN --mount=type=cache,id=vllm-rocm-ccache,target=/root/.cache/ccache \ - set -eu; \ - printf '%s\n' "${LMCACHE_ROCM_ARCH}" \ - | grep -Eq '^gfx[0-9a-z]+(;gfx[0-9a-z]+)*$'; \ - offload_flags="$(printf '%s' "${LMCACHE_ROCM_ARCH}" \ - | sed 's/;/ --offload-arch=/g; s/^/--offload-arch=/')"; \ - if [ "${USE_SCCACHE:-0}" = "1" ]; then \ - export HIP_CLANG_PATH=/opt/sccache-wrappers; \ - fi; \ - mkdir -p /app/install; \ - SETUPTOOLS_SCM_PRETEND_VERSION="${LMCACHE_VERSION}" \ - PYTORCH_ROCM_ARCH="${LMCACHE_ROCM_ARCH}" \ - HIPCC_COMPILE_FLAGS_APPEND="${offload_flags}" \ - BUILD_WITH_HIP=1 CXX=hipcc \ - python3 setup.py bdist_wheel --dist-dir=/app/install; \ - test "$(find /app/install -maxdepth 1 -name '*.whl' | wc -l)" -eq 1 - -RUN --mount=type=tmpfs,target=/tmp/lmcache-wheel \ - set -eu; \ - python3 -m zipfile -e /app/install/*.whl /tmp/lmcache-wheel; \ - c_ops="$(find /tmp/lmcache-wheel -type f -name 'c_ops*.so')"; \ - test "$(find /tmp/lmcache-wheel -type f -name 'c_ops*.so' | wc -l)" -eq 1; \ - expected="$(printf '%s' "${LMCACHE_ROCM_ARCH}" | tr ';' '\n' | sort -u)"; \ - actual="$(/opt/rocm/llvm/bin/llvm-objdump --offloading "${c_ops}" \ - | grep -oE 'gfx[0-9a-z]+' | sort -u)"; \ - if [ "${actual}" != "${expected}" ]; then \ - printf 'LMCache code objects mismatch\nexpected:\n%s\nactual:\n%s\n' \ - "${expected}" "${actual}" >&2; \ - exit 1; \ - fi - -# ROCShmem build stage - split from DeepEP so changing DEEPEP_BRANCH does not -# invalidate the slow ROCShmem build. -FROM base AS build_rocshmem -ARG ROCSHMEM_BRANCH="f0acb0c6" -ARG ROCSHMEM_REPO="https://github.com/ROCm/rocm-systems.git" -# DeepEP only supports gfx942 and gfx950; build ROCShmem for the same set so -# it can be linked against DeepEP without arch mismatches. -ARG DEEPEP_ROCM_ARCH="gfx942;gfx950" -ENV ROCM_PATH=/opt/rocm -ENV ROCSHMEM_DIR=/opt/rocshmem - -RUN --mount=type=cache,target=/root/.cache/ccache \ - git clone --no-checkout --filter=blob:none ${ROCSHMEM_REPO} \ - && cd rocm-systems \ - && git sparse-checkout set --cone projects/rocshmem \ - && git checkout ${ROCSHMEM_BRANCH} \ - && mkdir -p projects/rocshmem/build \ - && cd projects/rocshmem/build \ - && CC="ccache gcc" CXX="ccache g++" INSTALL_PREFIX=${ROCSHMEM_DIR} \ - bash ../scripts/build_configs/all_backends \ - -DROCM_PATH=${ROCM_PATH} \ - -DGPU_TARGETS="${DEEPEP_ROCM_ARCH}" \ - -DUSE_EXTERNAL_MPI=OFF - -# DeepEP build stage - depends on ROCShmem, builds the HIP kernel wheel. -FROM build_rocshmem AS build_deepep -ARG DEEPEP_BRANCH="0f63d3e9" -ARG DEEPEP_REPO="https://github.com/ROCm/DeepEP.git" -ARG DEEPEP_NIC="cx7" - -# Build DeepEP wheel. DeepEP looks for rocshmem at ROCSHMEM_DIR. -# DeepEP only supports gfx942 and gfx950, so avoid gfx90a in the default list. -# We pass --rocm-gfx942-fp8-fnuz-max 224 to align the DeepEP dispatch FP8 -# quantization bound with vLLM's own clamp -# (vllm/model_executor/layers/quantization/utils/fp8_utils.py): 224 for E4M3FNUZ -RUN --mount=type=cache,target=/root/.cache/ccache \ - export PYTORCH_ROCM_ARCH="gfx942;gfx950" \ - && git clone ${DEEPEP_REPO} \ - && cd DeepEP \ - && git checkout ${DEEPEP_BRANCH} \ - && LDFLAGS="-fuse-ld=mold" MAX_JOBS="${MAX_JOBS:-$(nproc)}" python3 setup.py --variant rocm --rocm-explicit-ctx --nic ${DEEPEP_NIC} --rocm-gfx942-fp8-fnuz-max 224 bdist_wheel --dist-dir=/app/deep_install - -# MoRI runtime dependencies live in Dockerfile.rocm_72 so NIC backend changes do -# not force users to rebuild the long-lived Dockerfile.rocm_72_base image. -FROM base AS mori_base -ARG NIC_BACKEND -ARG AINIC_VERSION -ARG UBUNTU_CODENAME -RUN /bin/bash -lc 'set -euo pipefail; \ - \ - install_ainic() { \ - apt-get update && apt-get install -y --no-install-recommends ca-certificates curl gnupg apt-transport-https; \ - rm -rf /var/lib/apt/lists/*; \ - mkdir -p /etc/apt/keyrings; \ - curl -fsSL https://repo.radeon.com/rocm/rocm.gpg.key | gpg --dearmor > /etc/apt/keyrings/amdainic.gpg; \ - echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/amdainic.gpg] https://repo.radeon.com/amdainic/pensando/ubuntu/${AINIC_VERSION} ${UBUNTU_CODENAME} main" \ - > /etc/apt/sources.list.d/amdainic.list; \ - apt-get update && apt-get install -y --no-install-recommends \ - libionic-dev \ - ionic-common \ - ; \ - rm -rf /var/lib/apt/lists/*; \ - }; \ - \ - # NOTE: requires FW 235.2.86.0 and kernel drivers on the host: \ - # bnxt-en-dkms=1.10.3.235.2.86.0 bnxt-re-dkms=235.2.86.0 (from packages.broadcom.com PPA) \ - install_bnxt() { \ - install -m 0755 -d /etc/apt/keyrings; \ - curl -fsSL https://packages.broadcom.com/artifactory/api/security/keypair/PackagesKey/public \ - -o /etc/apt/keyrings/broadcom-nic.asc; \ - chmod a+r /etc/apt/keyrings/broadcom-nic.asc; \ - echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/broadcom-nic.asc] https://packages.broadcom.com/artifactory/ethernet-nic-debian-public jammy main" \ - > /etc/apt/sources.list.d/broadcom-nic.list; \ - apt-get update && apt-get install -y --no-install-recommends \ - bnxt-rocelib=235.2.86.0 \ - ; \ - cp -a /usr/local/lib/x86_64-linux-gnu/libbnxt_re* /usr/local/lib/; \ - ldconfig; \ - rm -rf /var/lib/apt/lists/*; \ - }; \ - \ - echo "[MORI] Install MoRI proxy deps"; \ - pip install --quiet --ignore-installed blinker && \ - pip install --quiet quart msgpack aiohttp pyzmq; \ - echo "[MORI] NIC_BACKEND=${NIC_BACKEND}"; \ - \ - # NIC backend deps — mori auto-detects NIC at runtime (MORI_DEVICE_NIC env var override). \ - # Only vendor packages are installed here for dlopen; no compile-time flags needed. \ - case "${NIC_BACKEND}" in \ - none) ;; \ - all) install_ainic; install_bnxt ;; \ - ainic) install_ainic ;; \ - bnxt) install_bnxt ;; \ - *) echo "ERROR: unknown NIC_BACKEND=${NIC_BACKEND}. Use one of: none, ainic, bnxt, all"; exit 2 ;; \ - esac' - -# ----------------------- -# vLLM wheel release build stage (for building distributable wheels) -# This stage pins dependencies to custom ROCm wheel versions and handles version detection -FROM fetch_vllm AS build_vllm_wheel_release - -ARG COMMON_WORKDIR -ARG ROCM_TRITON_KERNELS_COMMIT -COPY --from=vllm-version /vllm-version.txt /tmp/vllm-version.txt -COPY --from=rocm-triton-kernels \ - /opt/rocm-triton-kernels /opt/rocm-triton-kernels -ENV TRITON_KERNELS_SRC_DIR=/opt/rocm-triton-kernels - -# Drop the pre-built Rust artifacts into the source tree. setup.py detects -# them and ships them as-is, skipping the local Rust build. -COPY --from=rust-build ${COMMON_WORKDIR}/vllm/vllm/vllm-rs ${COMMON_WORKDIR}/vllm/vllm/vllm-rs -COPY --from=rust-build ${COMMON_WORKDIR}/vllm/vllm/_rust_*.so ${COMMON_WORKDIR}/vllm/vllm/ - -# Create /install directory for custom wheels -RUN mkdir -p /install - -# Copy custom ROCm wheels from docker/context if they exist -# COPY ensures Docker cache is invalidated when wheels change -# .keep file ensures directory always exists for COPY to work -COPY docker/context/base-wheels/ /tmp/base-wheels/ -# This is how we know if we are building for a wheel release or not. -# If there are not wheels found there, we are not building for a wheel release. -# So we exit with an error. To skip this stage. -RUN if [ -n "$(ls /tmp/base-wheels/*.whl 2>/dev/null)" ]; then \ - echo "Found custom wheels - copying to /install"; \ - cp /tmp/base-wheels/*.whl /install/ && \ - echo "Copied custom wheels:"; \ - ls -lh /install/; \ - else \ - echo "ERROR: No custom wheels found in docker/context/base-wheels/"; \ - echo "Wheel releases require pre-built ROCm wheels."; \ - exit 1; \ - fi - -# GIT_REPO_CHECK: Verify repo is clean and tags are available (for release builds) -# This matches CUDA's Dockerfile behavior for proper version detection via setuptools_scm -ARG GIT_REPO_CHECK=0 -RUN if [ "$GIT_REPO_CHECK" != "0" ]; then \ - echo "Running repository checks..."; \ - cd vllm && bash tools/check_repo.sh; \ - fi - -# Install the packaging helpers before modifying requirements/rocm.txt. -RUN --mount=type=cache,target=/root/.cache/uv \ - grep -Fq "set(TRITON_KERNELS_TAG \"${ROCM_TRITON_KERNELS_COMMIT}\")" \ - vllm/cmake/external_projects/triton_kernels.cmake \ - && cd vllm \ - && uv pip install --system setuptools_scm regex - -# Fail if git-based package dependencies are found in requirements files -# (uv doesn't handle git+ URLs well, and packages should be distributed on PyPI) -# Extra notes: pip install is able to handle git+ URLs, but uv doesn't. -RUN echo "Checking for git-based packages in requirements files..." \ - && echo "Checking common.txt for git-based packages:" \ - && if grep -q 'git+' ${COMMON_WORKDIR}/vllm/requirements/common.txt; then \ - echo "ERROR: Git-based packages found in common.txt:"; \ - grep 'git+' ${COMMON_WORKDIR}/vllm/requirements/common.txt; \ - echo "Please publish these packages to PyPI instead of using git dependencies."; \ - exit 1; \ - else \ - echo " ✓ No git-based packages found in common.txt"; \ - fi \ - && echo "Checking rocm.txt for git-based packages:" \ - && if grep -q 'git+' ${COMMON_WORKDIR}/vllm/requirements/rocm.txt; then \ - echo "ERROR: Git-based packages found in rocm.txt:"; \ - grep 'git+' ${COMMON_WORKDIR}/vllm/requirements/rocm.txt; \ - echo "Please publish these packages to PyPI instead of using git dependencies."; \ - exit 1; \ - else \ - echo " ✓ No git-based packages found in rocm.txt"; \ - fi \ - && echo "All requirements files are clean - no git-based packages found" - -# Pin vLLM dependencies to exact versions of custom ROCm wheels -# This ensures 'pip install vllm' automatically installs correct torch/triton/torchvision/amdsmi -COPY tools/vllm-rocm/pin_rocm_dependencies.py /tmp/pin_rocm_dependencies.py -RUN echo "Pinning vLLM dependencies to custom wheel versions..." \ - && python3 /tmp/pin_rocm_dependencies.py /install ${COMMON_WORKDIR}/vllm/requirements/rocm.txt - -# Install dependencies using custom wheels from /install -RUN --mount=type=cache,target=/root/.cache/uv \ - cd vllm \ - && echo "Building vLLM with custom wheels from /install" \ - && uv pip install --system --find-links /install -r requirements/rocm.txt - -# Build wheel using the shared source version to avoid dirty state from modified requirements/rocm.txt -# (setup.py auto-detects ccache/sccache in PATH) -RUN --mount=type=cache,id=vllm-rocm-ccache,target=/root/.cache/ccache \ - cd vllm \ - && export CCACHE_BASEDIR="$PWD" \ - && export SETUPTOOLS_SCM_PRETEND_VERSION="$(cat /tmp/vllm-version.txt)" \ - && echo "Building wheel with version: ${SETUPTOOLS_SCM_PRETEND_VERSION}" \ - && MAX_JOBS="${MAX_JOBS:-$(nproc)}" python3 setup.py bdist_wheel --dist-dir=dist - -FROM scratch AS export_vllm_wheel_release -ARG COMMON_WORKDIR -COPY --from=build_vllm_wheel_release ${COMMON_WORKDIR}/vllm/dist/*.whl / -COPY --from=build_vllm_wheel_release ${COMMON_WORKDIR}/vllm/requirements /requirements -COPY --from=build_vllm_wheel_release ${COMMON_WORKDIR}/vllm/benchmarks /benchmarks -COPY --from=build_vllm_wheel_release ${COMMON_WORKDIR}/vllm/tests /tests -COPY --from=build_vllm_wheel_release ${COMMON_WORKDIR}/vllm/examples /examples -COPY --from=build_vllm_wheel_release ${COMMON_WORKDIR}/vllm/tools /tools -COPY --from=build_vllm_wheel_release ${COMMON_WORKDIR}/vllm/docker/Dockerfile /docker/Dockerfile -COPY --from=build_vllm_wheel_release ${COMMON_WORKDIR}/vllm/docker/Dockerfile.cpu /docker/Dockerfile.cpu -COPY --from=build_vllm_wheel_release ${COMMON_WORKDIR}/vllm/docker/Dockerfile.rocm_72 /docker/ -COPY --from=build_vllm_wheel_release ${COMMON_WORKDIR}/vllm/docker/Dockerfile.rocm_72_base /docker/ -COPY --from=build_vllm_wheel_release ${COMMON_WORKDIR}/vllm/docker/ci-rocm.hcl /docker/ci-rocm.hcl -COPY --from=build_vllm_wheel_release ${COMMON_WORKDIR}/vllm/docker/docker-bake.hcl /docker/docker-bake.hcl -COPY --from=build_vllm_wheel_release ${COMMON_WORKDIR}/vllm/docker/docker-bake-rocm.hcl /docker/docker-bake-rocm.hcl -COPY --from=build_vllm_wheel_release ${COMMON_WORKDIR}/vllm/.buildkite /.buildkite -COPY --from=build_vllm_wheel_release ${COMMON_WORKDIR}/vllm/pyproject.toml /pyproject.toml -COPY --from=build_vllm_wheel_release ${COMMON_WORKDIR}/vllm/rust /rust -COPY --from=build_vllm_wheel_release ${COMMON_WORKDIR}/vllm/rust-toolchain.toml /rust-toolchain.toml -COPY --from=build_vllm_wheel_release ${COMMON_WORKDIR}/vllm/vllm/v1 /vllm_v1 - -# ----------------------- -# CI base image (Tier 1) - stable, rarely changing CI dependencies. -# Per-PR test builds pull this as CI_BASE_IMAGE so the test stage only layers -# in the vLLM artifacts for the current commit. -FROM mori_base AS ci_base -ARG COMMON_WORKDIR - -ENV CARGO_HOME=/root/.cargo -ENV RUSTUP_HOME=/root/.rustup -ENV PATH=${CARGO_HOME}/bin:${PATH} - -# Bake the Rust toolchain into ci_base so Rust build/test steps do not -# need to fetch it on each per-commit image build. -COPY --from=rust-toolchain /root/.cargo /root/.cargo -COPY --from=rust-toolchain /root/.rustup /root/.rustup - -# Update rdma-core to support latest rocshmem. -ARG DEEPEP_NIC -RUN if [ "${DEEPEP_NIC}" = "cx7" ] || [ "${DEEPEP_NIC}" = "io" ]; then \ - git clone --branch v62.0 --depth 1 https://github.com/linux-rdma/rdma-core.git /tmp/rdma-core && \ - cd /tmp/rdma-core && \ - mkdir -p build && cd build && \ - cmake -GNinja -DCMAKE_INSTALL_PREFIX=/usr -DNO_MAN_PAGES=1 .. && \ - ninja && ninja install && ldconfig && rm -rf /tmp/rdma-core; \ -fi - -# Install NIXL + DeepEP wheels. -RUN --mount=type=bind,from=build_nixl,src=/app/install,target=/nixl_install \ - --mount=type=bind,from=build_deepep,src=/app/deep_install,target=/deep_install \ - uv pip install --system /nixl_install/*.whl /deep_install/*.whl - -# Copy ROCShmem runtime libraries. -COPY --from=build_rocshmem /opt/rocshmem /opt/rocshmem - -# RDMA userspace libraries plus FFmpeg dev libs needed by torchcodec. -RUN timeout --kill-after=30s 5m apt-get update -q -y --error-on=any \ - -o Acquire::Retries=3 \ - -o Acquire::http::Timeout=30 \ - -o Acquire::https::Timeout=30 \ - && apt-get install -q -y --no-install-recommends \ - -o Acquire::Retries=3 \ - -o Acquire::http::Timeout=30 \ - -o Acquire::https::Timeout=30 \ - librdmacm1 \ - libibverbs1 \ - ibverbs-providers \ - ibverbs-utils \ - util-linux \ - unzip \ - libsodium23 \ - pkg-config ffmpeg libavcodec-dev libavformat-dev libavutil-dev \ - libswscale-dev libavdevice-dev libavfilter-dev libswresample-dev \ - && rm -rf /var/lib/apt/lists/* - -# Install torchcodec from source for ROCm/torch ABI compatibility. -COPY tools/install_torchcodec_rocm.sh /tmp/install_torchcodec.sh -RUN --mount=type=cache,target=/root/.cache/uv \ - --mount=type=cache,target=/root/.cache/pip \ - --mount=type=cache,target=/root/.cache/torchcodec-wheels \ - bash /tmp/install_torchcodec.sh \ - && rm /tmp/install_torchcodec.sh \ - && apt-get clean && rm -rf /var/lib/apt/lists/* - -# Pre-install shared ROCm runtime dependencies. -COPY requirements/common.txt requirements/rocm.txt /tmp/ci-base-requirements/ -RUN --mount=type=cache,target=/root/.cache/uv \ - uv pip install --system -r /tmp/ci-base-requirements/rocm.txt \ - && rm -rf /tmp/ci-base-requirements - -# Enable fast and less brittle model downloads in tests. -ENV HF_XET_HIGH_PERFORMANCE=1 -ENV HF_HUB_DOWNLOAD_TIMEOUT=60 - -# Keep torch.cuda.is_available() fork-safe (see vllm/env_override.py). -ENV PYTORCH_NVML_BASED_CUDA_CHECK=1 - -# Pre-install vLLM test dependencies. -COPY requirements/test/rocm.txt /tmp/rocm-test-reqs.txt -RUN --mount=type=cache,target=/root/.cache/uv \ - uv pip install --system -r /tmp/rocm-test-reqs.txt - -# Rebuild fastsafetensors from source so its C++ extension is compiled with -# USE_ROCM and can detect libamdhip64.so at runtime. -RUN --mount=type=cache,target=/root/.cache/pip \ - FASTSAFETENSORS_REQ="$(grep -E '^fastsafetensors(==| @ )' /tmp/rocm-test-reqs.txt | head -1)" \ - && test -n "${FASTSAFETENSORS_REQ}" \ - && python3 -m pip install --force-reinstall --no-deps \ - --no-binary fastsafetensors "${FASTSAFETENSORS_REQ}" \ - && rm /tmp/rocm-test-reqs.txt - -# Set MIOPEN ENVS to resolve performance regressions in MIOpen 3D convolution kernel. -# See: https://github.com/pytorch/pytorch/issues/169857 -ENV MIOPEN_DEBUG_CONV_DIRECT=0 -ENV MIOPEN_DEBUG_CONV_GEMM=0 - -# Use legacy IPC mode for HSA to avoid GPU memory pinning issues with UCX rocm_ipc. -# See: https://github.com/ROCm/rocm-libraries/issues/6266 -ENV HSA_ENABLE_IPC_MODE_LEGACY=1 -ENV UCX_ROCM_COPY_SIGPOOL_MAX_ELEMS=inf -ENV UCX_ROCM_IPC_SIGPOOL_MAX_ELEMS=inf - -# ROCm profiler limits workaround. -RUN echo "ROCTRACER_MAX_EVENTS=10000000" > ${COMMON_WORKDIR}/libkineto.conf -ENV KINETO_CONFIG="${COMMON_WORKDIR}/libkineto.conf" - -# Install vllm_test_utils in ci_base for ci_base + wheel parity. -COPY tests/vllm_test_utils /tmp/vllm_test_utils -RUN --mount=type=cache,target=/root/.cache/uv \ - uv pip install --system /tmp/vllm_test_utils \ - && rm -rf /tmp/vllm_test_utils - -RUN --mount=type=bind,from=build_lmcache,src=/app/install,target=/lmcache_install \ - --mount=type=bind,source=requirements/test/rocm.txt,target=/tmp/lmcache-constraints.txt \ - --mount=type=cache,target=/root/.cache/uv \ - uv pip install --system --constraint /tmp/lmcache-constraints.txt \ - /lmcache_install/*.whl \ - && python3 -c "import lmcache, lmcache.c_ops; print('lmcache', lmcache.__version__)" - -# ----------------------- -# Test vLLM image (Tier 2) - vLLM-only layer on top of ci_base. -FROM ${CI_BASE_IMAGE} AS test -ARG COMMON_WORKDIR - -# Install the vLLM wheel (--no-deps: all deps already in ci_base). -RUN --mount=type=bind,from=export_vllm,src=/,target=/install \ - --mount=type=cache,target=/root/.cache/uv \ - cd /install \ - && uv pip install --system --no-deps *.whl - -# Store the vLLM wheel in the image for python-only install tests. -COPY --from=export_vllm /*.whl /opt/vllm-wheels/ - -WORKDIR /vllm-workspace -COPY --from=build_vllm ${COMMON_WORKDIR}/vllm /vllm-workspace - -# Copy in the v1 package (for python-only install test group). -COPY --from=export_vllm /vllm_v1 /usr/local/lib/python${PYTHON_VERSION}/dist-packages/vllm/v1 - -# Hide source under src/ so it won't shadow the installed package in tests. -RUN mkdir src && mv vllm src/vllm - -# Catch GPU<->CPU syncs in execute_model/sample_tokens during tests. -ENV VLLM_GPU_SYNC_CHECK=error - -# Run the structural smoke test inside BuildKit, where the test image layers -# are already available. Export only a build-scoped success marker. -FROM test AS test_smoke -ARG ROCM_SMOKE_ID -RUN --network=none \ - bash /vllm-workspace/.buildkite/scripts/rocm/smoke-test-image.sh --inside \ - && printf '%s\n' "${ROCM_SMOKE_ID:-local}" > /vllm-smoke-ok - -FROM scratch AS export_test_smoke -COPY --from=test_smoke /vllm-smoke-ok / - -# ----------------------- -# Final vLLM image -FROM mori_base AS final_common -ENV UV_COMPILE_BYTECODE=1 - -RUN python3 -m pip install --upgrade pip && rm -rf /var/lib/apt/lists/* - -# Clean up sccache from release image (not needed at runtime) -# This removes the binary and wrappers that may have been installed during build -RUN rm -f /usr/bin/sccache || true \ - && rm -rf /opt/sccache-wrappers || true - -# Unset sccache environment variables for the release image -# This prevents S3 bucket config from leaking into production images -ENV SCCACHE_BUCKET= -ENV SCCACHE_REGION= -ENV SCCACHE_ENDPOINT= -ENV SCCACHE_S3_NO_CREDENTIALS= -ENV SCCACHE_IDLE_TIMEOUT= - -# Error related to odd state for numpy 1.20.3 where there is no METADATA etc, but an extra LICENSES_bundled.txt. -# Manually remove it so that later steps of numpy upgrade can continue -RUN case "$(which python3)" in \ - *"/opt/conda/envs/py_3.9"*) \ - rm -rf /opt/conda/envs/py_3.9/lib/python3.9/site-packages/numpy-1.20.3.dist-info/;; \ - *) ;; esac - -RUN --mount=type=cache,target=/root/.cache/uv \ - uv pip install --system --upgrade huggingface-hub - -# Install vLLM using uv (inherited from base stage) -# Note: No -U flag to avoid upgrading PyTorch ROCm to CUDA version -# Keep OpenTelemetry within LMCache's cap. -RUN --mount=type=bind,from=export_vllm,src=/,target=/install \ - --mount=type=cache,target=/root/.cache/uv \ - cd /install \ - && echo "opentelemetry-api<=1.40.0" > /tmp/otel-constraints.txt \ - && uv pip install --system -c /tmp/otel-constraints.txt -r requirements/rocm.txt \ - && pip uninstall -y vllm \ - && uv pip install --system -c /tmp/otel-constraints.txt *.whl \ - && rm /tmp/otel-constraints.txt - -# Install NIXL ROCm wheel -RUN --mount=type=bind,from=build_nixl,src=/app/install,target=/nixl_install \ - uv pip install --system /nixl_install/*.whl - -ARG COMMON_WORKDIR -ARG BASE_IMAGE -ARG NIC_BACKEND -ARG AINIC_VERSION - -# Copy over the benchmark scripts as well -COPY --from=export_vllm /benchmarks ${COMMON_WORKDIR}/vllm/benchmarks -COPY --from=export_vllm /examples ${COMMON_WORKDIR}/vllm/examples -COPY --from=export_vllm /docker ${COMMON_WORKDIR}/vllm/docker - -# Use legacy IPC mode for HSA to avoid GPU memory pinning issues with UCX rocm_ipc -# See: https://github.com/ROCm/rocm-libraries/issues/6266 -ENV HSA_ENABLE_IPC_MODE_LEGACY=1 -ENV UCX_ROCM_COPY_SIGPOOL_MAX_ELEMS=inf -ENV UCX_ROCM_IPC_SIGPOOL_MAX_ELEMS=inf - -ENV TOKENIZERS_PARALLELISM=false - -# ENV that can improve safe tensor loading, and end-to-end time -ENV SAFETENSORS_FAST_GPU=1 - -# Performance environment variable. -ENV HIP_FORCE_DEV_KERNARG=1 - -# Keep torch.cuda.is_available() fork-safe (see vllm/env_override.py). -ENV PYTORCH_NVML_BASED_CUDA_CHECK=1 - -# Workaround for ROCm profiler limits -RUN echo "ROCTRACER_MAX_EVENTS=10000000" > ${COMMON_WORKDIR}/libkineto.conf -ENV KINETO_CONFIG="${COMMON_WORKDIR}/libkineto.conf" -RUN echo "VLLM_BASE_IMAGE=${BASE_IMAGE}" >> ${COMMON_WORKDIR}/versions.txt \ - && echo "MORI_NIC_BACKEND=${NIC_BACKEND}" >> ${COMMON_WORKDIR}/versions.txt \ - && echo "AINIC_VERSION=${AINIC_VERSION}" >> ${COMMON_WORKDIR}/versions.txt - -CMD ["/bin/bash"] - -FROM final_common AS final_lmcache_false - -FROM final_common AS final_lmcache_true -RUN --mount=type=bind,from=build_lmcache,src=/app/install,target=/lmcache_install \ - --mount=type=cache,target=/root/.cache/uv \ - uv pip install --system /lmcache_install/*.whl \ - && test "$(python3 -c 'import numpy; print(numpy.__version__)')" = "2.3.5" \ - && uv pip check --system \ - && python3 -c "import lmcache, lmcache.c_ops; print('lmcache', lmcache.__version__)" - -FROM final_lmcache_${INSTALL_LMCACHE} AS final - -#Set entrypoint for vllm-openai official images -FROM final AS vllm-openai -ENTRYPOINT ["vllm", "serve"] diff --git a/docker/Dockerfile.rocm_72_base b/docker/Dockerfile.rocm_72_base deleted file mode 100644 index 22b75d2941ea..000000000000 --- a/docker/Dockerfile.rocm_72_base +++ /dev/null @@ -1,479 +0,0 @@ -ARG BASE_IMAGE=rocm/dev-ubuntu-22.04:7.2.3-complete -ARG TRITON_BRANCH="669b31a" # release/internal/3.8.x as of 09/17 -ARG TRITON_REPO="https://github.com/ROCm/triton.git" -ARG PYTORCH_BRANCH="733fca1" # release/2.13 as of 09/17 -ARG PYTORCH_REPO="https://github.com/ROCm/pytorch.git" -ARG PYTORCH_VISION_BRANCH="v0.28.0" -ARG PYTORCH_VISION_REPO="https://github.com/pytorch/vision.git" -ARG PYTORCH_AUDIO_BRANCH="v2.11.0" -ARG PYTORCH_AUDIO_REPO="https://github.com/pytorch/audio.git" -ARG FA_BRANCH="0e60e394" -ARG FA_REPO="https://github.com/Dao-AILab/flash-attention.git" -ARG AITER_BRANCH="v0.1.26" -ARG AITER_REPO="https://github.com/ROCm/aiter.git" -ARG MORI_BRANCH="v1.2.3.post1" -ARG MORI_REPO="https://github.com/ROCm/mori.git" -ARG ROCM_SYSTEMS_REPO="https://github.com/ROCm/rocm-systems.git" -ARG ROCPROFILER_SDK_COMMIT="2b22ab0195cc1461cd9abf3b969e9dd7c10af350" -ARG ROCM_RUNTIME_COMMIT="af70edc91f1dc5d228fd4c076df99e65e49c4521" - -# Sccache configuration (only used in release pipeline) -ARG USE_SCCACHE -ARG SCCACHE_DOWNLOAD_URL -ARG SCCACHE_ENDPOINT -ARG SCCACHE_BUCKET_NAME=vllm-build-sccache -ARG SCCACHE_REGION_NAME=us-west-2 -ARG SCCACHE_S3_NO_CREDENTIALS=0 - - -FROM ${BASE_IMAGE} AS base - -ENV PATH=/opt/rocm/llvm/bin:/opt/rocm/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin -ENV ROCM_PATH=/opt/rocm -ENV LD_LIBRARY_PATH=/opt/rocm/lib:/usr/local/lib: - -ARG PYTORCH_ROCM_ARCH=gfx90a;gfx942;gfx950;gfx1100;gfx1101;gfx1200;gfx1201;gfx1150;gfx1151 -ENV PYTORCH_ROCM_ARCH=${PYTORCH_ROCM_ARCH} -ENV AITER_ROCM_ARCH=gfx942;gfx950 -# Note: Do not set MORI_GPU_ARCHS here, it is automatically inferred at runtime - -# Required for RCCL in ROCm7.1 -ENV HSA_NO_SCRATCH_RECLAIM=1 - -ARG PYTHON_VERSION=3.12 -ENV PYTHON_VERSION=${PYTHON_VERSION} - -RUN mkdir -p /app -WORKDIR /app -ENV DEBIAN_FRONTEND=noninteractive - -# Install Python and other dependencies (plus gcc-13, selected later in build_pytorch). -RUN apt-get update -y \ - && apt-get install -y software-properties-common git curl sudo vim less libgfortran5 libopenmpi-dev libpci-dev liblzma-dev pkg-config \ - && for i in 1 2 3; do \ - add-apt-repository -y ppa:deadsnakes/ppa && add-apt-repository -y ppa:ubuntu-toolchain-r/test && break || \ - { echo "Attempt $i failed, retrying in 5s..."; sleep 5; }; \ - done \ - && apt-get update -y \ - && apt-get install -y python${PYTHON_VERSION} python${PYTHON_VERSION}-dev python${PYTHON_VERSION}-venv \ - python${PYTHON_VERSION}-lib2to3 python-is-python3 gcc-13 g++-13 \ - && update-alternatives --install /usr/bin/python3 python3 /usr/bin/python${PYTHON_VERSION} 1 \ - && update-alternatives --set python3 /usr/bin/python${PYTHON_VERSION} \ - && ln -sf /usr/bin/python${PYTHON_VERSION}-config /usr/bin/python3-config \ - && curl -sS https://bootstrap.pypa.io/get-pip.py | python${PYTHON_VERSION} \ - && python3 --version && python3 -m pip --version - -RUN pip install -U packaging 'cmake<4' ninja wheel 'setuptools<80' 'pybind11<3.1.0' Cython -RUN apt-get update && apt-get install -y libjpeg-dev libsox-dev libsox-fmt-all sox && rm -rf /var/lib/apt/lists/* - -# Install sccache if USE_SCCACHE is enabled (for release builds) -ARG USE_SCCACHE -ARG SCCACHE_DOWNLOAD_URL -ARG SCCACHE_ENDPOINT -ARG SCCACHE_BUCKET_NAME -ARG SCCACHE_REGION_NAME -ARG SCCACHE_S3_NO_CREDENTIALS -RUN if [ "$USE_SCCACHE" = "1" ]; then \ - echo "Installing sccache..." \ - && SCCACHE_ARCH="x86_64" \ - && SCCACHE_VERSION="v0.8.1" \ - && SCCACHE_DL_URL="${SCCACHE_DOWNLOAD_URL:-https://github.com/mozilla/sccache/releases/download/${SCCACHE_VERSION}/sccache-${SCCACHE_VERSION}-${SCCACHE_ARCH}-unknown-linux-musl.tar.gz}" \ - && curl -L -o /tmp/sccache.tar.gz ${SCCACHE_DL_URL} \ - && tar -xzf /tmp/sccache.tar.gz -C /tmp \ - && mv /tmp/sccache-${SCCACHE_VERSION}-${SCCACHE_ARCH}-unknown-linux-musl/sccache /usr/bin/sccache \ - && chmod +x /usr/bin/sccache \ - && rm -rf /tmp/sccache.tar.gz /tmp/sccache-${SCCACHE_VERSION}-${SCCACHE_ARCH}-unknown-linux-musl \ - && sccache --version; \ - fi - -# Setup sccache for HIP compilation via HIP_CLANG_PATH -# This creates wrapper scripts in a separate directory and points HIP to use them -# This avoids modifying the original ROCm binaries which can break detection -# NOTE: HIP_CLANG_PATH is NOT set as ENV to avoid affecting downstream images (Dockerfile.rocm_72) -# Instead, each build stage should export HIP_CLANG_PATH=/opt/sccache-wrappers if USE_SCCACHE=1 -RUN if [ "$USE_SCCACHE" = "1" ]; then \ - echo "Setting up sccache wrappers for HIP compilation..." \ - && mkdir -p /opt/sccache-wrappers \ - && printf '#!/bin/bash\nexec sccache /opt/rocm/lib/llvm/bin/clang++ "$@"\n' > /opt/sccache-wrappers/clang++ \ - && chmod +x /opt/sccache-wrappers/clang++ \ - && printf '#!/bin/bash\nexec sccache /opt/rocm/lib/llvm/bin/clang "$@"\n' > /opt/sccache-wrappers/clang \ - && chmod +x /opt/sccache-wrappers/clang \ - && echo "sccache wrappers created in /opt/sccache-wrappers"; \ - fi - -# Set sccache environment variables only when USE_SCCACHE=1 -# This prevents S3 config from leaking into images when sccache is not used -ARG USE_SCCACHE -ENV SCCACHE_BUCKET=${USE_SCCACHE:+${SCCACHE_BUCKET_NAME}} -ENV SCCACHE_REGION=${USE_SCCACHE:+${SCCACHE_REGION_NAME}} -ENV SCCACHE_S3_NO_CREDENTIALS=${USE_SCCACHE:+${SCCACHE_S3_NO_CREDENTIALS}} -ENV SCCACHE_IDLE_TIMEOUT=${USE_SCCACHE:+0} - - -### -### Triton Build -### -FROM base AS build_triton -ARG TRITON_BRANCH -ARG TRITON_REPO -RUN git clone ${TRITON_REPO} -RUN cd triton \ - && git checkout ${TRITON_BRANCH} \ - && git config --global user.email "you@example.com" && git config --global user.name "Your Name" \ - && if [ ! -f setup.py ]; then cd python; fi \ - && MAX_JOBS=16 python3 setup.py bdist_wheel --dist-dir=dist \ - && mkdir -p /app/install && cp dist/*.whl /app/install - - -### -### AMD SMI Build -### -FROM base AS build_amdsmi -RUN cd /opt/rocm/share/amd_smi \ - && pip wheel . --wheel-dir=dist -RUN mkdir -p /app/install && cp /opt/rocm/share/amd_smi/dist/*.whl /app/install - - -### -### Pytorch build -### -FROM base AS build_pytorch -ARG PYTORCH_BRANCH -ARG PYTORCH_REPO -ARG USE_SCCACHE - -RUN apt-get update && apt-get install -y pkg-config liblzma-dev -# torch 2.12 emits float->__bf16 libcalls (__truncsfbf2) absent from gcc-11 libgcc; build with gcc-13. -RUN update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-13 130 --slave /usr/bin/g++ g++ /usr/bin/g++-13 -RUN git clone ${PYTORCH_REPO} pytorch -RUN cd pytorch && git checkout ${PYTORCH_BRANCH} -RUN cd pytorch \ - && pip install -r requirements.txt && git submodule update --init --recursive -RUN cd pytorch && python3 tools/amd_build/build_amd.py \ - && if [ "$USE_SCCACHE" = "1" ]; then \ - export HIP_CLANG_PATH=/opt/sccache-wrappers \ - && export CMAKE_C_COMPILER_LAUNCHER=sccache \ - && export CMAKE_CXX_COMPILER_LAUNCHER=sccache \ - && sccache --show-stats; \ - fi \ - && CMAKE_PREFIX_PATH=$(python3 -c 'import sys; print(sys.prefix)') python3 setup.py bdist_wheel --dist-dir=dist \ - && if [ "$USE_SCCACHE" = "1" ]; then sccache --show-stats; fi -RUN mkdir -p /app/install && cp /app/pytorch/dist/*.whl /app/install - - -### -### Shared PyTorch runtime for dependent builds -### -FROM base AS build_pytorch_runtime -RUN update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-13 130 --slave /usr/bin/g++ g++ /usr/bin/g++-13 -RUN --mount=type=bind,from=build_pytorch,src=/app/install/,target=/install,ro \ - pip install /install/*.whl - - -### -### TorchVision build -### -FROM build_pytorch_runtime AS build_torchvision -ARG PYTORCH_VISION_BRANCH -ARG PYTORCH_VISION_REPO -ARG USE_SCCACHE - -RUN git clone ${PYTORCH_VISION_REPO} vision -RUN cd vision && git checkout ${PYTORCH_VISION_BRANCH} \ - && if [ "$USE_SCCACHE" = "1" ]; then \ - export HIP_CLANG_PATH=/opt/sccache-wrappers \ - && export CMAKE_C_COMPILER_LAUNCHER=sccache \ - && export CMAKE_CXX_COMPILER_LAUNCHER=sccache; \ - fi \ - && python3 setup.py bdist_wheel --dist-dir=dist \ - && if [ "$USE_SCCACHE" = "1" ]; then sccache --show-stats; fi -RUN mkdir -p /app/install && cp /app/vision/dist/*.whl /app/install - - -### -### TorchAudio build -### -FROM build_pytorch_runtime AS build_torchaudio -ARG PYTORCH_AUDIO_BRANCH -ARG PYTORCH_AUDIO_REPO -ARG USE_SCCACHE - -RUN git clone ${PYTORCH_AUDIO_REPO} audio -RUN cd audio && git checkout ${PYTORCH_AUDIO_BRANCH} \ - && git submodule update --init --recursive -RUN cd audio && pip install -r requirements.txt -RUN cd audio \ - && if [ "$USE_SCCACHE" = "1" ]; then \ - export HIP_CLANG_PATH=/opt/sccache-wrappers \ - && export CMAKE_C_COMPILER_LAUNCHER=sccache \ - && export CMAKE_CXX_COMPILER_LAUNCHER=sccache; \ - fi \ - && python3 setup.py bdist_wheel --dist-dir=dist \ - && if [ "$USE_SCCACHE" = "1" ]; then sccache --show-stats; fi -RUN mkdir -p /app/install && cp /app/audio/dist/*.whl /app/install - - -### -### MORI Build -### -FROM base AS build_mori -ARG MORI_BRANCH -ARG MORI_REPO -RUN --mount=type=bind,from=build_pytorch,src=/app/install/,target=/install \ - pip install /install/*.whl -RUN git clone ${MORI_REPO} -RUN cd mori \ - && git checkout ${MORI_BRANCH} \ - && git submodule update --init --recursive \ - && python3 setup.py bdist_wheel --dist-dir=dist && ls /app/mori/dist/*.whl -RUN mkdir -p /app/install && cp /app/mori/dist/*.whl /app/install - - -### -### FlashAttention Build -### -FROM base AS build_fa -ARG FA_BRANCH -ARG FA_REPO -ARG USE_SCCACHE -RUN --mount=type=bind,from=build_pytorch,src=/app/install/,target=/install \ - pip install /install/*.whl -RUN git clone ${FA_REPO} -RUN cd flash-attention \ - && git checkout ${FA_BRANCH} \ - && git submodule update --init \ - && if [ "$USE_SCCACHE" = "1" ]; then \ - export HIP_CLANG_PATH=/opt/sccache-wrappers \ - && sccache --show-stats; \ - fi \ - && GPU_ARCHS=$(echo ${PYTORCH_ROCM_ARCH} | sed -e 's/;gfx1[0-9]\{3\}//g') python3 setup.py bdist_wheel --dist-dir=dist \ - && if [ "$USE_SCCACHE" = "1" ]; then sccache --show-stats; fi -RUN mkdir -p /app/install && cp /app/flash-attention/dist/*.whl /app/install - - -### -### AITER Build -### -FROM base AS build_aiter -ARG AITER_BRANCH -ARG AITER_REPO -ARG USE_SCCACHE -RUN --mount=type=bind,from=build_pytorch,src=/app/install/,target=/install \ - pip install /install/*.whl -RUN git clone --recursive --branch ${AITER_BRANCH} ${AITER_REPO} -RUN cd aiter \ - && git submodule update --init --recursive \ - && pip install -r requirements.txt -RUN pip install pyyaml && cd aiter \ - && if [ "$USE_SCCACHE" = "1" ]; then \ - export HIP_CLANG_PATH=/opt/sccache-wrappers \ - && sccache --show-stats; \ - fi \ - && PREBUILD_KERNELS=1 AITER_USE_SYSTEM_TRITON=1 GPU_ARCHS=${AITER_ROCM_ARCH} python3 setup.py bdist_wheel --dist-dir=dist \ - && if [ "$USE_SCCACHE" = "1" ]; then sccache --show-stats; fi \ - && ls /app/aiter/dist/*.whl -RUN mkdir -p /app/install && cp /app/aiter/dist/*.whl /app/install - - -### -### Final Build -### - -# Wheel release stage - -# only includes dependencies used by wheel release pipeline -FROM base AS debs_wheel_release -RUN mkdir /app/debs -RUN --mount=type=bind,from=build_triton,src=/app/install/,target=/install \ - cp /install/*.whl /app/debs -RUN --mount=type=bind,from=build_fa,src=/app/install/,target=/install \ - cp /install/*.whl /app/debs -RUN --mount=type=bind,from=build_amdsmi,src=/app/install/,target=/install \ - cp /install/*.whl /app/debs -RUN --mount=type=bind,from=build_pytorch,src=/app/install/,target=/install \ - cp /install/*.whl /app/debs -RUN --mount=type=bind,from=build_torchvision,src=/app/install/,target=/install \ - cp /install/*.whl /app/debs -RUN --mount=type=bind,from=build_torchaudio,src=/app/install/,target=/install \ - cp /install/*.whl /app/debs -RUN --mount=type=bind,from=build_aiter,src=/app/install/,target=/install \ - cp /install/*.whl /app/debs - -# Full debs stage - includes Mori (used by Docker releases) -FROM base AS debs -RUN mkdir /app/debs -RUN --mount=type=bind,from=build_triton,src=/app/install/,target=/install \ - cp /install/*.whl /app/debs -RUN --mount=type=bind,from=build_fa,src=/app/install/,target=/install \ - cp /install/*.whl /app/debs -RUN --mount=type=bind,from=build_amdsmi,src=/app/install/,target=/install \ - cp /install/*.whl /app/debs -RUN --mount=type=bind,from=build_pytorch,src=/app/install/,target=/install \ - cp /install/*.whl /app/debs -RUN --mount=type=bind,from=build_torchvision,src=/app/install/,target=/install \ - cp /install/*.whl /app/debs -RUN --mount=type=bind,from=build_torchaudio,src=/app/install/,target=/install \ - cp /install/*.whl /app/debs -RUN --mount=type=bind,from=build_aiter,src=/app/install/,target=/install \ - cp /install/*.whl /app/debs -RUN --mount=type=bind,from=build_mori,src=/app/install/,target=/install \ - cp /install/*.whl /app/debs - -### -### ROCr + CLR build -### -# Pull in updates to ROCr + clr from ROCm 7.2.4 and beyond. -FROM base AS build_rocm_runtime -ARG ROCM_SYSTEMS_REPO -ARG ROCM_RUNTIME_COMMIT -RUN apt-get update \ - && apt-get install -y --no-install-recommends g++ \ - libelf-dev libdrm-dev libnuma-dev libdw-dev xxd \ - && rm -rf /var/lib/apt/lists/* -RUN pip install --no-cache-dir CppHeaderParser -RUN git init -q /src && cd /src \ - && git remote add origin ${ROCM_SYSTEMS_REPO} \ - && git config core.sparseCheckout true \ - && git config remote.origin.promisor true \ - && git config remote.origin.partialclonefilter blob:none \ - && git sparse-checkout init --cone \ - && git sparse-checkout set projects/rocr-runtime projects/clr projects/hip shared cmake \ - && git fetch --filter=blob:none --depth 1 origin ${ROCM_RUNTIME_COMMIT} \ - && git checkout -q FETCH_HEAD -# ROCr's trap_handler and blit_shaders call find_package(Clang/LLVM REQUIRED) only to get -# IMPORTED executables. The ROCm dev image ships the binaries but no CMake package config, so -# supply one pointing at them. -RUN mkdir -p /opt/rocm-cmake-shim \ - && printf 'if(NOT TARGET clang)\n add_executable(clang IMPORTED GLOBAL)\n set_target_properties(clang PROPERTIES IMPORTED_LOCATION "/opt/rocm/llvm/bin/clang")\nendif()\nset(Clang_PACKAGE_VERSION "rocm-image-shim")\n' > /opt/rocm-cmake-shim/ClangConfig.cmake \ - && printf 'if(NOT TARGET llvm-objcopy)\n add_executable(llvm-objcopy IMPORTED GLOBAL)\n set_target_properties(llvm-objcopy PROPERTIES IMPORTED_LOCATION "/opt/rocm/llvm/bin/llvm-objcopy")\nendif()\nset(LLVM_FOUND TRUE)\nset(LLVM_PACKAGE_VERSION "rocm-image-shim")\n' > /opt/rocm-cmake-shim/LLVMConfig.cmake -# ROCr first, and installed into /opt/rocm, because the clr build below needs its headers -RUN cd /src/projects/rocr-runtime \ - && cmake -B build -G Ninja \ - -DCMAKE_BUILD_TYPE=Release \ - -DCMAKE_PREFIX_PATH=/opt/rocm \ - -DCMAKE_INSTALL_PREFIX=/opt/rocm \ - -DClang_DIR=/opt/rocm-cmake-shim \ - -DLLVM_DIR=/opt/rocm-cmake-shim \ - && cmake --build build --parallel 16 \ - && cmake --install build \ - && cmake --install build --prefix /rocr-install --strip -RUN cd /src/projects/clr \ - && cmake -B build -G Ninja \ - -DCMAKE_BUILD_TYPE=Release \ - -DCLR_BUILD_HIP=ON \ - -DCLR_BUILD_OCL=OFF \ - -DHIP_COMMON_DIR=/src/projects/hip \ - -DROCM_PATH=/opt/rocm \ - -DCMAKE_PREFIX_PATH=/opt/rocm \ - -DCMAKE_INSTALL_PREFIX=/opt/rocm \ - -DLLVM_DIR=/opt/rocm-cmake-shim \ - -DHIP_LLVM_ROOT=/opt/rocm/llvm \ - && cmake --build build --parallel 16 \ - && cmake --install build --prefix /clr-install --strip -RUN mkdir -p /staging/lib \ - && cp -P /rocr-install/lib/libhsa-runtime64.so* /staging/lib/ \ - && cp -P /clr-install/lib/libamdhip64.so* /staging/lib/ \ - && ls -l /staging/lib - -# rocprofiler-sdk 1.3.2 fixes torch 2.12 profiler on ROCm 7.2.3 -FROM base AS build_rocprofiler_sdk -ARG ROCM_SYSTEMS_REPO -ARG ROCPROFILER_SDK_COMMIT -ARG USE_SCCACHE -RUN apt-get update \ - && apt-get install -y --no-install-recommends libdw-dev libelf-dev libsqlite3-dev libzstd-dev \ - && rm -rf /var/lib/apt/lists/* -RUN git init -q /src && cd /src \ - && git remote add origin ${ROCM_SYSTEMS_REPO} \ - && git config core.sparseCheckout true \ - && git config remote.origin.promisor true \ - && git config remote.origin.partialclonefilter blob:none \ - && git sparse-checkout init --cone \ - && git sparse-checkout set projects/rocprofiler-sdk shared cmake \ - && git fetch --filter=blob:none --depth 1 origin ${ROCPROFILER_SDK_COMMIT} \ - && git checkout -q FETCH_HEAD -# Patch the ROCm 7.2.3 build of rocprofiler-sdk to include queue-interposition fixes that are -# absent from the pinned 7.14 build. This resolves hangs on torch.profiler on ROCm. -# TODO: Drop these once ${ROCPROFILER_SDK_COMMIT} is bumped to a build that already -# contains them, such as ROCm 10.0. See https://github.com/vllm-project/vllm/issues/54087. -ARG ROCPROFILER_SDK_PR_7796=fae7fd57ffb8a76e811d6410b8a0cee1e94d710a -ARG ROCPROFILER_SDK_PR_7924=f8db22adbe17686a9b05ae03367dd1ec73011653 -RUN cd /src \ - && HSA_PATHS='projects/rocprofiler-sdk/source/lib/rocprofiler-sdk/hsa/*' \ - && curl -fsSL -o /tmp/qi-7796.patch "${ROCM_SYSTEMS_REPO%.git}/commit/${ROCPROFILER_SDK_PR_7796}.patch" \ - && curl -fsSL -o /tmp/qi-7924.patch "${ROCM_SYSTEMS_REPO%.git}/commit/${ROCPROFILER_SDK_PR_7924}.patch" \ - && git apply -p1 --include="$HSA_PATHS" --verbose /tmp/qi-7796.patch \ - && git apply -p1 --include="$HSA_PATHS" --verbose /tmp/qi-7924.patch -RUN cd /src/projects/rocprofiler-sdk \ - && if [ "$USE_SCCACHE" = "1" ]; then \ - export CMAKE_C_COMPILER_LAUNCHER=sccache \ - && export CMAKE_CXX_COMPILER_LAUNCHER=sccache; \ - fi \ - && cmake -B build -G Ninja \ - -DCMAKE_BUILD_TYPE=Release \ - -DCMAKE_PREFIX_PATH=/opt/rocm \ - -DCMAKE_INSTALL_PREFIX=/opt/rocm \ - -DROCPROFILER_BUILD_TESTS=OFF \ - -DROCPROFILER_BUILD_SAMPLES=OFF \ - -DROCPROFILER_BUILD_DOCS=OFF \ - && cmake --build build --parallel "$(nproc)" \ - && cmake --install build --prefix /staging --strip - -FROM base AS final -RUN --mount=type=bind,from=debs,src=/app/debs,target=/install \ - pip install /install/*.whl - -COPY --from=build_rocprofiler_sdk /staging/ /opt/rocm/ -ENV ROCPROFILER_QUEUE_INTERPOSITION=0 - -COPY --from=build_rocm_runtime /staging/ /opt/rocm/ -# Hard link the patched library onto every stock file name so that both the -# SONAME lookup and the hard coded paths resolve to it. -RUN set -eux; \ - cd /opt/rocm/lib; \ - ours_hsa="$(readlink -f libhsa-runtime64.so.1)"; \ - ours_hip="$(readlink -f libamdhip64.so.7)"; \ - for f in libhsa-runtime64.so.1.*; do \ - [ "$f" = "${ours_hsa##*/}" ] || ln -f "$ours_hsa" "$f"; \ - done; \ - for f in libamdhip64.so.7.*; do \ - [ "$f" = "${ours_hip##*/}" ] || ln -f "$ours_hip" "$f"; \ - done - -RUN ldconfig - -ARG BASE_IMAGE -ARG TRITON_BRANCH -ARG TRITON_REPO -ARG PYTORCH_BRANCH -ARG PYTORCH_VISION_BRANCH -ARG PYTORCH_REPO -ARG PYTORCH_VISION_REPO -ARG PYTORCH_AUDIO_BRANCH -ARG PYTORCH_AUDIO_REPO -ARG FA_BRANCH -ARG FA_REPO -ARG AITER_BRANCH -ARG AITER_REPO -ARG MORI_BRANCH -ARG MORI_REPO -ARG ROCM_SYSTEMS_REPO -ARG ROCPROFILER_SDK_COMMIT -ARG ROCM_RUNTIME_COMMIT -RUN echo "BASE_IMAGE: ${BASE_IMAGE}" > /app/versions.txt \ - && echo "ROCM_SYSTEMS_REPO: ${ROCM_SYSTEMS_REPO}" >> /app/versions.txt \ - && echo "ROCPROFILER_SDK_COMMIT: ${ROCPROFILER_SDK_COMMIT}" >> /app/versions.txt \ - && echo "ROCM_RUNTIME_COMMIT: ${ROCM_RUNTIME_COMMIT}" >> /app/versions.txt \ - && echo "TRITON_BRANCH: ${TRITON_BRANCH}" >> /app/versions.txt \ - && echo "TRITON_REPO: ${TRITON_REPO}" >> /app/versions.txt \ - && echo "PYTORCH_BRANCH: ${PYTORCH_BRANCH}" >> /app/versions.txt \ - && echo "PYTORCH_VISION_BRANCH: ${PYTORCH_VISION_BRANCH}" >> /app/versions.txt \ - && echo "PYTORCH_REPO: ${PYTORCH_REPO}" >> /app/versions.txt \ - && echo "PYTORCH_VISION_REPO: ${PYTORCH_VISION_REPO}" >> /app/versions.txt \ - && echo "PYTORCH_AUDIO_BRANCH: ${PYTORCH_AUDIO_BRANCH}" >> /app/versions.txt \ - && echo "PYTORCH_AUDIO_REPO: ${PYTORCH_AUDIO_REPO}" >> /app/versions.txt \ - && echo "FA_BRANCH: ${FA_BRANCH}" >> /app/versions.txt \ - && echo "FA_REPO: ${FA_REPO}" >> /app/versions.txt \ - && echo "AITER_BRANCH: ${AITER_BRANCH}" >> /app/versions.txt \ - && echo "AITER_REPO: ${AITER_REPO}" >> /app/versions.txt \ - && echo "MORI_BRANCH: ${MORI_BRANCH}" >> /app/versions.txt \ - && echo "MORI_REPO: ${MORI_REPO}" >> /app/versions.txt diff --git a/docker/Dockerfile.rocm_base b/docker/Dockerfile.rocm_base index 997af6183805..9561b4a8e24c 100644 --- a/docker/Dockerfile.rocm_base +++ b/docker/Dockerfile.rocm_base @@ -1,4 +1,6 @@ -ARG BASE_IMAGE=ubuntu:22.04 +ARG BASE_IMAGE=ubuntu:22.04@sha256:5ec03bb3441e8b0bf3b4f9cd4629a1ae763010dc3035bb8da3ae6cf026486401 +# Bump to rebuild the stack (changes its key and busts every layer); upstream drift alone never does. +ARG REFRESH=2026-10-12 ARG PYTHON_VERSION=3.12 ARG TORCH_VERSION=2.13.0+rocm10.0.0 ARG TORCHVISION_VERSION=0.28.0+rocm10.0.0 @@ -8,7 +10,7 @@ ARG TRITON_VERSION=3.8.0+git669b31ac ARG TRITON_REPO="https://github.com/ROCm/triton.git" ARG FA_BRANCH="0e60e394" ARG FA_REPO="https://github.com/Dao-AILab/flash-attention.git" -ARG AITER_BRANCH="v0.1.26" +ARG AITER_BRANCH="v0.1.27" ARG AITER_REPO="https://github.com/ROCm/aiter.git" ARG MORI_BRANCH="v1.2.3.post1" ARG MORI_REPO="https://github.com/ROCm/mori.git" @@ -26,14 +28,6 @@ ARG ROCM_SDK_VERSION=10.0.0 ARG PYTORCH_ROCM_ARCH="gfx90a;gfx942;gfx950;gfx1100;gfx1101;gfx1200;gfx1201;gfx1150;gfx1151" ARG AITER_ROCM_ARCH="gfx942;gfx950" -# Sccache configuration (only used in release pipeline) -ARG USE_SCCACHE -ARG SCCACHE_DOWNLOAD_URL -ARG SCCACHE_ENDPOINT -ARG SCCACHE_BUCKET_NAME=vllm-build-sccache -ARG SCCACHE_REGION_NAME=us-west-2 -ARG SCCACHE_S3_NO_CREDENTIALS=0 - FROM ${BASE_IMAGE} AS base ARG PYTORCH_ROCM_ARCH @@ -52,6 +46,7 @@ ENV ROCM_PATH="$SDK_DEV" \ HIP_DEVICE_LIB_PATH="$SDK_CORE/lib/llvm/amdgcn/bitcode" ENV LD_LIBRARY_PATH="$SDK_DEV/lib:$SDK_DEV/lib/rocm_sysdeps/lib:$SDK_CORE/lib:$SDK_CORE/lib/rocm_sysdeps/lib" ENV PATH="$SDK_DEV/bin:$SDK_DEV/lib/llvm/bin:$SDK_CORE/bin:$PATH" +ENV ROCSHMEM_DIR="$SDK_DEV" # Required for RCCL in ROCm7.1 ENV HSA_NO_SCRATCH_RECLAIM=1 @@ -59,12 +54,16 @@ ENV HSA_NO_SCRATCH_RECLAIM=1 ARG PYTHON_VERSION ENV PYTHON_VERSION=${PYTHON_VERSION} -RUN mkdir -p /app +ARG REFRESH +RUN echo "$REFRESH" >/etc/vllm-rocm-refresh && mkdir -p /app WORKDIR /app ENV DEBIAN_FRONTEND=noninteractive # Install Python and other dependencies -RUN apt-get update -y \ +# pip runs with a cache mount, so its downloads (several GB of SDK +# and torch wheels) never land in an image layer. +RUN --mount=type=cache,target=/root/.cache/pip \ + apt-get update -y \ && apt-get install -y software-properties-common build-essential git curl sudo vim less libgfortran5 libopenmpi-dev libpci-dev liblzma-dev pkg-config libdrm-dev libnuma-dev kmod \ && for i in 1 2 3; do \ add-apt-repository -y ppa:deadsnakes/ppa && break || \ @@ -79,14 +78,16 @@ RUN apt-get update -y \ && curl -sS https://bootstrap.pypa.io/get-pip.py | python${PYTHON_VERSION} \ && python3 --version && python3 -m pip --version -RUN pip install -U packaging 'cmake<4' ninja wheel 'setuptools<80' pybind11==3.0.4 Cython +RUN --mount=type=cache,target=/root/.cache/pip \ + pip install -U packaging 'cmake<4' ninja wheel==0.48.0 'setuptools<80' pybind11==3.0.4 Cython RUN apt-get update && apt-get install -y libjpeg-dev libsox-dev libsox-fmt-all sox && rm -rf /var/lib/apt/lists/* # Install ROCm SDK via TheRock ARG ROCM_RELEASE_WHEELS_MULTIARCH_URL ARG ROCM_NIGHTLY_WHEELS_MULTIARCH_URL ARG ROCM_SDK_VERSION -RUN pip install --index-url ${ROCM_RELEASE_WHEELS_MULTIARCH_URL} \ +RUN --mount=type=cache,target=/root/.cache/pip \ + pip install --index-url ${ROCM_RELEASE_WHEELS_MULTIARCH_URL} \ --extra-index-url ${ROCM_NIGHTLY_WHEELS_MULTIARCH_URL} \ "rocm[libraries,devel]${ROCM_SDK_VERSION:+==${ROCM_SDK_VERSION}}" \ && rocm-sdk init @@ -95,7 +96,8 @@ RUN pip install --index-url ${ROCM_RELEASE_WHEELS_MULTIARCH_URL} \ ARG TORCH_VERSION ARG TORCHAUDIO_VERSION ARG TORCHVISION_VERSION -RUN pip install \ +RUN --mount=type=cache,target=/root/.cache/pip \ + pip install \ --index-url ${ROCM_RELEASE_WHEELS_MULTIARCH_URL} \ --extra-index-url ${ROCM_NIGHTLY_WHEELS_MULTIARCH_URL} \ "torch==${TORCH_VERSION}" \ @@ -112,7 +114,8 @@ RUN pip install \ # For each arch in PYTORCH_ROCM_ARCH, install the precompiled GPU code that # PyTorch needs to run on that specific chip. SHELL ["/bin/bash", "-c"] -RUN TORCH_VER=$(python3 -c "import torch; print(torch.__version__)") \ +RUN --mount=type=cache,target=/root/.cache/pip \ + TORCH_VER=$(python3 -c "import torch; print(torch.__version__)") \ && for arch in $(echo "${PYTORCH_ROCM_ARCH}" | tr ';' ' '); do \ echo "Installing device packages for ${arch}" \ && pip install --index-url ${ROCM_RELEASE_WHEELS_MULTIARCH_URL} \ @@ -126,7 +129,8 @@ RUN TORCH_VER=$(python3 -c "import torch; print(torch.__version__)") \ RUN rm -f "${SITE_PACKAGES}/torch/.kpack/torch_gfx90a:xnack+.kpack" \ "${SITE_PACKAGES}/torch/.kpack/torch_gfx90a:xnack-.kpack" -RUN cd $SITE_PACKAGES/_rocm_sdk_core/share/amd_smi && pip install . +RUN --mount=type=cache,target=/root/.cache/pip \ + cd $SITE_PACKAGES/_rocm_sdk_core/share/amd_smi && pip install . ENV PYTHONPATH=$SITE_PACKAGES/_rocm_sdk_core/share/amd_smi # TheRock runtime fixes @@ -156,61 +160,28 @@ RUN if [ -f "${SITE_PACKAGES}/rocm_sdk/__init__.py" ]; then \ done \ && ldconfig -# Install sccache if USE_SCCACHE is enabled (for release builds) -ARG USE_SCCACHE -ARG SCCACHE_DOWNLOAD_URL -ARG SCCACHE_ENDPOINT -ARG SCCACHE_BUCKET_NAME -ARG SCCACHE_REGION_NAME -ARG SCCACHE_S3_NO_CREDENTIALS -RUN if [ "$USE_SCCACHE" = "1" ]; then \ - echo "Installing sccache..." \ - && SCCACHE_ARCH="x86_64" \ - && SCCACHE_VERSION="v0.8.1" \ - && SCCACHE_DL_URL="${SCCACHE_DOWNLOAD_URL:-https://github.com/mozilla/sccache/releases/download/${SCCACHE_VERSION}/sccache-${SCCACHE_VERSION}-${SCCACHE_ARCH}-unknown-linux-musl.tar.gz}" \ - && curl -L -o /tmp/sccache.tar.gz ${SCCACHE_DL_URL} \ - && tar -xzf /tmp/sccache.tar.gz -C /tmp \ - && mv /tmp/sccache-${SCCACHE_VERSION}-${SCCACHE_ARCH}-unknown-linux-musl/sccache /usr/bin/sccache \ - && chmod +x /usr/bin/sccache \ - && rm -rf /tmp/sccache.tar.gz /tmp/sccache-${SCCACHE_VERSION}-${SCCACHE_ARCH}-unknown-linux-musl \ - && sccache --version; \ - fi - -# Setup sccache for HIP compilation via HIP_CLANG_PATH -# This creates wrapper scripts in a separate directory and points HIP to use them -# This avoids modifying the original ROCm binaries which can break detection -# NOTE: HIP_CLANG_PATH is NOT set as ENV to avoid affecting downstream images (Dockerfile.rocm) -# Instead, each build stage should export HIP_CLANG_PATH=/opt/sccache-wrappers if USE_SCCACHE=1 -RUN if [ "$USE_SCCACHE" = "1" ]; then \ - echo "Setting up sccache wrappers for HIP compilation..." \ - && mkdir -p /opt/sccache-wrappers \ - && printf '#!/bin/bash\nexec sccache %s/lib/llvm/bin/clang++ "$@"\n' "${ROCM_PATH}" > /opt/sccache-wrappers/clang++ \ - && chmod +x /opt/sccache-wrappers/clang++ \ - && printf '#!/bin/bash\nexec sccache %s/lib/llvm/bin/clang "$@"\n' "${ROCM_PATH}" > /opt/sccache-wrappers/clang \ - && chmod +x /opt/sccache-wrappers/clang \ - && echo "sccache wrappers created in /opt/sccache-wrappers"; \ - fi - -ENV SCCACHE_BUCKET=${USE_SCCACHE:+${SCCACHE_BUCKET_NAME}} -ENV SCCACHE_REGION=${USE_SCCACHE:+${SCCACHE_REGION_NAME}} -ENV SCCACHE_S3_NO_CREDENTIALS=${USE_SCCACHE:+${SCCACHE_S3_NO_CREDENTIALS}} -ENV SCCACHE_IDLE_TIMEOUT=${USE_SCCACHE:+0} - ### ### MORI Build ### FROM base AS build_mori +# mori silently builds without UMBP when gRPC or Protobuf is missing. +RUN apt-get update \ + && apt-get install -y --no-install-recommends libgrpc++-dev libprotobuf-dev \ + protobuf-compiler protobuf-compiler-grpc \ + && rm -rf /var/lib/apt/lists/* ARG MORI_BRANCH ARG MORI_REPO RUN git clone ${MORI_REPO} -RUN cd mori \ +RUN --mount=type=cache,target=/root/.cache/pip cd mori \ && git checkout ${MORI_BRANCH} \ && git submodule update --init --recursive \ && ln -sfn ${ROCM_PATH} /opt/rocm \ && pip install "setuptools_scm>=6.2" \ && CMAKE_PREFIX_PATH="${ROCM_PATH}:${ROCM_PATH}/lib/rocm_sysdeps" \ - python3 setup.py bdist_wheel --dist-dir=dist && ls /app/mori/dist/*.whl + python3 setup.py bdist_wheel --dist-dir=dist \ + && grep -qx 'BUILD_UMBP:BOOL=ON' build/CMakeCache.txt \ + && ls /app/mori/dist/*.whl RUN mkdir -p /app/install && cp /app/mori/dist/*.whl /app/install ### @@ -219,17 +190,11 @@ RUN mkdir -p /app/install && cp /app/mori/dist/*.whl /app/install FROM base AS build_fa ARG FA_BRANCH ARG FA_REPO -ARG USE_SCCACHE RUN git clone ${FA_REPO} RUN cd flash-attention \ && git checkout ${FA_BRANCH} \ && git submodule update --init \ - && if [ "$USE_SCCACHE" = "1" ]; then \ - export HIP_CLANG_PATH=/opt/sccache-wrappers \ - && sccache --show-stats; \ - fi \ - && GPU_ARCHS=$(echo ${PYTORCH_ROCM_ARCH} | sed -e 's/;gfx1[0-9]\{3\}//g') python3 setup.py bdist_wheel --dist-dir=dist \ - && if [ "$USE_SCCACHE" = "1" ]; then sccache --show-stats; fi + && GPU_ARCHS=$(echo ${PYTORCH_ROCM_ARCH} | sed -e 's/;gfx1[0-9]\{3\}//g') python3 setup.py bdist_wheel --dist-dir=dist RUN mkdir -p /app/install && cp /app/flash-attention/dist/*.whl /app/install @@ -239,20 +204,14 @@ RUN mkdir -p /app/install && cp /app/flash-attention/dist/*.whl /app/install FROM base AS build_aiter ARG AITER_BRANCH ARG AITER_REPO -ARG USE_SCCACHE RUN git clone --recursive --branch ${AITER_BRANCH} ${AITER_REPO} -RUN cd aiter \ +RUN --mount=type=cache,target=/root/.cache/pip cd aiter \ && git submodule update --init --recursive \ && pip install -r requirements.txt # AITER_USE_SYSTEM_TRITON keeps setup.py from resolving its own triton, which would # displace the pinned one already installed in base. -RUN pip install pyyaml && cd aiter \ - && if [ "$USE_SCCACHE" = "1" ]; then \ - export HIP_CLANG_PATH=/opt/sccache-wrappers \ - && sccache --show-stats; \ - fi \ +RUN --mount=type=cache,target=/root/.cache/pip pip install pyyaml && cd aiter \ && PREBUILD_KERNELS=1 AITER_USE_SYSTEM_TRITON=1 GPU_ARCHS=${AITER_ROCM_ARCH} python3 setup.py bdist_wheel --dist-dir=dist \ - && if [ "$USE_SCCACHE" = "1" ]; then sccache --show-stats; fi \ && ls /app/aiter/dist/*.whl RUN mkdir -p /app/install && cp /app/aiter/dist/*.whl /app/install @@ -268,7 +227,10 @@ RUN cd triton \ && git checkout ${TRITON_BRANCH} \ && if [ ! -f setup.py ]; then cd python; fi \ && python3 setup.py bdist_wheel --dist-dir=dist \ - && mkdir -p /app/install && cp dist/*.whl /app/install + && python3 -m wheel unpack dist/*.whl -d /tmp/triton-wheel \ + && find /tmp/triton-wheel -name '*.so' -exec strip --strip-debug {} + \ + && mkdir -p /app/install \ + && python3 -m wheel pack /tmp/triton-wheel/* -d /app/install ### @@ -330,9 +292,8 @@ RUN mkdir -p /staging/lib \ ### Final Build ### -# Wheel release stage - -# only includes dependencies used by wheel release pipeline -FROM base AS debs_wheel_release +# The stack's half of the release wheel index. +FROM base AS wheels-build RUN mkdir /app/debs RUN --mount=type=bind,from=build_triton,src=/app/install/,target=/install \ cp /install/*.whl /app/debs @@ -348,7 +309,8 @@ ARG ROCM_RELEASE_WHEELS_MULTIARCH_URL ARG ROCM_NIGHTLY_WHEELS_MULTIARCH_URL COPY tools/vllm-rocm/therock_wheels.py /tmp/therock_wheels.py # regex and pybase64 aren't installed here; fall back to the stdlib modules. -RUN sed -i -e 's/import regex as re/import re/' \ +RUN --mount=type=cache,target=/root/.cache/pip \ + sed -i -e 's/import regex as re/import re/' \ -e 's/import pybase64 as base64/import base64/' /tmp/therock_wheels.py \ && pip download --no-deps --dest /tmp/torch-dl \ --index-url ${ROCM_RELEASE_WHEELS_MULTIARCH_URL} \ @@ -363,19 +325,12 @@ RUN sed -i -e 's/import regex as re/import re/' \ && rm -rf /tmp/torch-dl /tmp/amd_smi /tmp/therock_wheels.py \ && ls -l /app/debs -FROM base AS debs -RUN mkdir /app/debs -RUN --mount=type=bind,from=build_triton,src=/app/install/,target=/install \ - cp /install/*.whl /app/debs -RUN --mount=type=bind,from=build_fa,src=/app/install/,target=/install \ - cp /install/*.whl /app/debs -RUN --mount=type=bind,from=build_aiter,src=/app/install/,target=/install \ - cp /install/*.whl /app/debs -RUN --mount=type=bind,from=build_mori,src=/app/install/,target=/install \ - cp /install/*.whl /app/debs +FROM scratch AS wheels +COPY --from=wheels-build /app/debs / FROM base AS final ARG TRITON_VERSION +LABEL ai.vllm.rocm.variant=rocm100 # torch 2.12 implicitly attaches rocprofiler-sdk; its queue interposition hangs at HSA # teardown, so disable it. TODO: drop once ROCm's stock rocprofiler carries the fix. @@ -409,8 +364,17 @@ RUN TORCH_DIST_INFO=$(ls -d ${SITE_PACKAGES}/torch-*.dist-info) \ && sed -i '/^Requires-Dist: triton/d' "$TORCH_DIST_INFO/METADATA" \ && sed -i "s|^\(${TORCH_DIST_INFO##*/}/METADATA\),.*|\1,,|" "$TORCH_DIST_INFO/RECORD" -RUN --mount=type=bind,from=debs,src=/app/debs,target=/install \ - pip install /install/*.whl \ +# Runtime libraries of mori's UMBP extension. +RUN apt-get update \ + && apt-get install -y --no-install-recommends libgrpc++1 libprotobuf23 \ + && rm -rf /var/lib/apt/lists/* + +RUN --mount=type=bind,from=build_triton,src=/app/install,target=/i/triton \ + --mount=type=bind,from=build_fa,src=/app/install,target=/i/fa \ + --mount=type=bind,from=build_aiter,src=/app/install,target=/i/aiter \ + --mount=type=bind,from=build_mori,src=/app/install,target=/i/mori \ + --mount=type=cache,target=/root/.cache/pip \ + pip install /i/*/*.whl \ && ! pip check 2>&1 | grep -iE '^(torch|triton) ' \ && test "$(pip show triton | awk '/^Version:/ {print $2}')" = "${TRITON_VERSION}" \ # Create a constraints file to protect the versions we need, especially @@ -422,8 +386,16 @@ RUN --mount=type=bind,from=debs,src=/app/debs,target=/install \ && grep -q '^pybind11==3\.0\.' /etc/rocm-constraints.txt \ && python3 -c "import torch; assert torch.version.hip, f'non-ROCm torch: {torch.__version__}'; print(f'torch={torch.__version__}')" \ && python3 -c "import triton; print(f'triton={triton.__version__}')" \ + && python3 -c "from mori.cpp import UMBPClient" \ && pip list 2>/dev/null | grep -iE '^(torch|triton) ' +# Fail if a pip call above ran without the cache mount and left its downloads in the image. +RUN if [ -n "$(ls -A /root/.cache/pip 2>/dev/null)" ]; then \ + du -sh /root/.cache/pip; \ + echo "ERROR: /root/.cache/pip is not empty; add --mount=type=cache,target=/root/.cache/pip" >&2; \ + exit 1; \ + fi + ARG BASE_IMAGE ARG PYTHON_VERSION ARG ROCM_SDK_VERSION @@ -443,7 +415,6 @@ ARG MORI_REPO ARG ROCM_SYSTEMS_REPO ARG ROCM_RUNTIME_COMMIT ARG TRITON_VERSION -# Record hashes for Triton and pybind11 so we can verify later. RUN echo "BASE_IMAGE: ${BASE_IMAGE}" > /app/versions.txt \ && echo "PYTHON_VERSION: ${PYTHON_VERSION}" >> /app/versions.txt \ && echo "ROCM_SDK_VERSION: ${ROCM_SDK_VERSION}" >> /app/versions.txt \ @@ -462,15 +433,5 @@ RUN echo "BASE_IMAGE: ${BASE_IMAGE}" > /app/versions.txt \ && echo "MORI_REPO: ${MORI_REPO}" >> /app/versions.txt \ && echo "ROCM_SYSTEMS_REPO: ${ROCM_SYSTEMS_REPO}" >> /app/versions.txt \ && echo "ROCM_RUNTIME_COMMIT: ${ROCM_RUNTIME_COMMIT}" >> /app/versions.txt \ - # Fingerprint our triton so Dockerfile.rocm can prove it survived the build. && echo "TRITON_VERSION: $(pip show triton | awk '/^Version:/ {print $2}')" >> /app/versions.txt \ - && echo "TRITON_SHA256: $(find $(python3 -c 'import triton, os; print(os.path.dirname(triton.__file__))') \ - -type f \( -name '*.py' -o -name '*.so' \) | sort | xargs sha256sum | sha256sum | cut -d' ' -f1)" \ - >> /app/versions.txt \ - && echo "PYBIND11_VERSION: $(pip show pybind11 | awk '/^Version:/ {print $2}')" >> /app/versions.txt \ - && echo "PYBIND11_INTERNALS_VERSION: $(grep -rhE '^#[[:space:]]*define PYBIND11_INTERNALS_VERSION' \ - $(python3 -c 'import pybind11; print(pybind11.get_include())') | awk '{print $NF}')" \ - >> /app/versions.txt \ - && echo "PYBIND11_SHA256: $(find $(python3 -c 'import pybind11; print(pybind11.get_include())') \ - -type f -name '*.h' | sort | xargs sha256sum | sha256sum | cut -d' ' -f1)" \ - >> /app/versions.txt + && echo "PYBIND11_VERSION: $(pip show pybind11 | awk '/^Version:/ {print $2}')" >> /app/versions.txt diff --git a/docker/Dockerfile.rocm_gfx1250 b/docker/Dockerfile.rocm_gfx1250 index 634e522e0d62..dd22d0c27685 100644 --- a/docker/Dockerfile.rocm_gfx1250 +++ b/docker/Dockerfile.rocm_gfx1250 @@ -2,7 +2,7 @@ ARG REMOTE_VLLM="0" ARG COMMON_WORKDIR=/app ARG BASE_IMAGE=rocm/vllm-dev:base -ARG CI_BASE_IMAGE=rocm/vllm-dev:ci_base +ARG CI_BASE_IMAGE=ci_base # NIC backend for MoRI RDMA support. # By default (all), drivers and userspace libraries for all supported NIC types # (ainic and bnxt) are installed; MoRI selects the appropriate one at runtime. diff --git a/docker/Dockerfile.xpu b/docker/Dockerfile.xpu index 5e1e2984f4a4..fbde67b4e6d3 100644 --- a/docker/Dockerfile.xpu +++ b/docker/Dockerfile.xpu @@ -171,7 +171,7 @@ FROM vllm-base AS ucx-nixl-build # NIXL_VERSION (nixl contrib/Dockerfile.manylinux: ARG UCX_REF), so XPU runs the # same UCX commit as the released nixl-cu* wheels rather than an untested pairing. ARG UCX_VERSION=v1.23.x -ARG NIXL_VERSION=v1.5.0 +ARG NIXL_VERSION=v1.5.2 # Build-time only: compiler, autotools, and verbs dev headers RUN apt-get update -y && apt-get install -y --no-install-recommends \ diff --git a/docker/ci-rocm.hcl b/docker/ci-rocm.hcl deleted file mode 100644 index 31c8ad6405c5..000000000000 --- a/docker/ci-rocm.hcl +++ /dev/null @@ -1,424 +0,0 @@ -# ci-rocm.hcl - CI-specific configuration for vLLM ROCm Docker builds -# -# This file lives in the vLLM repo at docker/ci-rocm.hcl so ROCm Docker -# build mechanics can evolve with Dockerfile.rocm and docker-bake-rocm.hcl. -# Used with: docker buildx bake -f docker/docker-bake-rocm.hcl -f docker/ci-rocm.hcl test-rocm-ci -# -# Registry cache: Docker Hub (rocm/vllm-ci-cache) is used exclusively. -# AMD build agents already have Docker Hub credentials (they push the test -# image to rocm/vllm-ci), so no additional credential setup is required. -# ROCm CI uses Docker Hub for BuildKit layer cache by default. A separate -# compiler cache can be enabled with USE_SCCACHE=1 when AMD provides a shared -# S3-compatible cache endpoint. - -# CI metadata - -variable "BUILDKITE_COMMIT" { - default = "" -} - -variable "BUILDKITE_BUILD_NUMBER" { - default = "" -} - -variable "BUILDKITE_BUILD_ID" { - default = "" -} - -variable "VLLM_KERNEL_SYMBOL_MAP" { - default = "0" -} - -variable "PARENT_COMMIT" { - default = "" -} - -# Merge-base of HEAD with main - provides a more stable cache fallback than -# parent commit for long-lived PRs. Mirrors the VLLM_MERGE_BASE_COMMIT -# pattern used in the shared ci.hcl file. Auto-computed by ci-bake-rocm.sh -# when unset. -variable "VLLM_MERGE_BASE_COMMIT" { - default = "" -} - -# Bridge to vLLM's COMMIT variable for OCI labels -variable "COMMIT" { - default = BUILDKITE_COMMIT -} - -# Image tags (set by CI) - -variable "IMAGE_TAG" { - default = "" -} - -variable "IMAGE_TAG_LATEST" { - default = "" -} - -# ROCm-specific GPU architecture targets - -variable "PYTORCH_ROCM_ARCH" { - default = "gfx90a;gfx942;gfx950" -} - -# Pre-built CI base image (Tier 1). Per-PR builds pull this instead of -# rebuilding NIXL/DeepEP/torchcodec from scratch. The ci_base stage in -# Dockerfile.rocm inherits from base, so CI_BASE_IMAGE only affects the test -# stage and is irrelevant when building --target ci_base itself. -variable "CI_BASE_IMAGE" { - default = "rocm/vllm-dev:ci_base" -} - -# Leave CI_MAX_JOBS empty so the Dockerfile falls back to $(nproc) and uses -# the full builder parallelism. Operators can still override this per build. -variable "CI_MAX_JOBS" { - default = "" -} - -# Upstream dependency commit pins -- extracted from Dockerfile.rocm by -# ci-bake-rocm.sh at build time. Empty defaults are safe: the cache -# functions produce no entries when the variable is empty. -variable "NIXL_BRANCH" { - default = "" -} - -variable "UCX_BRANCH" { - default = "" -} - -variable "DEEPEP_BRANCH" { - default = "" -} - -variable "NIXL_CACHE_KEY" { - default = "" -} - -variable "DEEPEP_CACHE_KEY" { - default = "" -} - -# Docker Hub registry cache for AMD builds. -# -# A separate repo (rocm/vllm-ci-cache) is used for BuildKit layer cache. -# Final-image cache exports use mode=min to reduce the volume of data pushed. -# Source-scoped csrc cache exports default to mode=max so fresh workers can -# recover more of the native build graph when ROCm extension inputs change. -# NOTE: mode=min still includes all layers referenced by the final image -# manifest, including inherited base layers (~7.25GB ROCm runtime). -# Docker Hub auto-creates the repo on first push. -# -# Final-image cache stays commit-scoped. Branch-to-branch reuse for the test -# image comes from importing the parent and merge-base commit cache refs. -# -# The source-scoped native cache is exported both per-commit and per-branch so -# ROCm extension rebuilds are shareable within the same commit reruns and across -# consecutive commits on the same branch without depending on a single global -# latest tag. - -variable "DOCKERHUB_CACHE_REPO" { - default = "rocm/vllm-ci-cache" -} - -variable "DOCKERHUB_CACHE_TO" { - default = "" -} - -variable "ROCM_CACHE_BRANCH_TAG" { - default = "" -} - -variable "ROCM_CACHE_UPSTREAM_BRANCH_TAG" { - default = "" -} - -variable "ROCM_CSRC_CACHE_TO_MODE" { - default = "max" -} - -variable "ROCM_RUST_CACHE_TO_MODE" { - default = "max" -} - -variable "ROCM_FINAL_CACHE_TO_MODE" { - default = "min" -} - -# Functions - -function "get_cache_from_rocm" { - params = [] - result = compact([ - # Exact commit hit - fastest cache on re-runs of the same commit - BUILDKITE_COMMIT != "" ? "type=registry,ref=${DOCKERHUB_CACHE_REPO}:rocm-${BUILDKITE_COMMIT}" : "", - # Parent commit - useful cache for incremental changes - PARENT_COMMIT != "" ? "type=registry,ref=${DOCKERHUB_CACHE_REPO}:rocm-${PARENT_COMMIT}" : "", - # Merge-base with main - stable fallback for long-lived or rebased PRs; - # maps to a real main-branch commit whose cache layers are likely warm - VLLM_MERGE_BASE_COMMIT != "" ? "type=registry,ref=${DOCKERHUB_CACHE_REPO}:rocm-${VLLM_MERGE_BASE_COMMIT}" : "", - # Import the source-scoped native build cache as well so builds whose - # Python/package layers changed can still reuse compiled ROCm objects. - BUILDKITE_COMMIT != "" ? "type=registry,ref=${DOCKERHUB_CACHE_REPO}:csrc-rocm-${BUILDKITE_COMMIT}" : "", - PARENT_COMMIT != "" ? "type=registry,ref=${DOCKERHUB_CACHE_REPO}:csrc-rocm-${PARENT_COMMIT}" : "", - VLLM_MERGE_BASE_COMMIT != "" ? "type=registry,ref=${DOCKERHUB_CACHE_REPO}:csrc-rocm-${VLLM_MERGE_BASE_COMMIT}" : "", - ROCM_CACHE_BRANCH_TAG != "" ? "type=registry,ref=${DOCKERHUB_CACHE_REPO}:csrc-rocm-branch-${ROCM_CACHE_BRANCH_TAG}" : "", - ROCM_CACHE_UPSTREAM_BRANCH_TAG != "" ? "type=registry,ref=${DOCKERHUB_CACHE_REPO}:csrc-rocm-branch-${ROCM_CACHE_UPSTREAM_BRANCH_TAG}" : "", - # Import the source-scoped Rust frontend cache so non-Rust changes do not - # force a fresh cargo release build. - BUILDKITE_COMMIT != "" ? "type=registry,ref=${DOCKERHUB_CACHE_REPO}:rust-rocm-${BUILDKITE_COMMIT}" : "", - PARENT_COMMIT != "" ? "type=registry,ref=${DOCKERHUB_CACHE_REPO}:rust-rocm-${PARENT_COMMIT}" : "", - VLLM_MERGE_BASE_COMMIT != "" ? "type=registry,ref=${DOCKERHUB_CACHE_REPO}:rust-rocm-${VLLM_MERGE_BASE_COMMIT}" : "", - ROCM_CACHE_BRANCH_TAG != "" ? "type=registry,ref=${DOCKERHUB_CACHE_REPO}:rust-rocm-branch-${ROCM_CACHE_BRANCH_TAG}" : "", - ROCM_CACHE_UPSTREAM_BRANCH_TAG != "" ? "type=registry,ref=${DOCKERHUB_CACHE_REPO}:rust-rocm-branch-${ROCM_CACHE_UPSTREAM_BRANCH_TAG}" : "", - # Branch-scoped full image cache - fallback when parent-commit cache is evicted - ROCM_CACHE_BRANCH_TAG != "" ? "type=registry,ref=${DOCKERHUB_CACHE_REPO}:rocm-branch-${ROCM_CACHE_BRANCH_TAG}" : "", - ROCM_CACHE_UPSTREAM_BRANCH_TAG != "" ? "type=registry,ref=${DOCKERHUB_CACHE_REPO}:rocm-branch-${ROCM_CACHE_UPSTREAM_BRANCH_TAG}" : "", - ]) -} - -function "get_cache_to_rocm" { - params = [] - result = compact([ - # Commit-scoped cache for exact re-runs. - BUILDKITE_COMMIT != "" ? "type=registry,ref=${DOCKERHUB_CACHE_REPO}:rocm-${BUILDKITE_COMMIT},mode=${ROCM_FINAL_CACHE_TO_MODE}" : "", - # Branch-scoped cache so later commits on the same branch can reuse the full - # image layers when the parent-commit cache is evicted. Unlike the old - # rocm-latest tag (which caused duplicate exporter 400s), this is per-branch. - ROCM_CACHE_BRANCH_TAG != "" ? "type=registry,ref=${DOCKERHUB_CACHE_REPO}:rocm-branch-${ROCM_CACHE_BRANCH_TAG},mode=${ROCM_FINAL_CACHE_TO_MODE}" : "", - ]) -} - -function "get_cache_from_rocm_csrc" { - params = [] - result = compact([ - BUILDKITE_COMMIT != "" ? "type=registry,ref=${DOCKERHUB_CACHE_REPO}:csrc-rocm-${BUILDKITE_COMMIT}" : "", - PARENT_COMMIT != "" ? "type=registry,ref=${DOCKERHUB_CACHE_REPO}:csrc-rocm-${PARENT_COMMIT}" : "", - VLLM_MERGE_BASE_COMMIT != "" ? "type=registry,ref=${DOCKERHUB_CACHE_REPO}:csrc-rocm-${VLLM_MERGE_BASE_COMMIT}" : "", - ROCM_CACHE_BRANCH_TAG != "" ? "type=registry,ref=${DOCKERHUB_CACHE_REPO}:csrc-rocm-branch-${ROCM_CACHE_BRANCH_TAG}" : "", - ROCM_CACHE_UPSTREAM_BRANCH_TAG != "" ? "type=registry,ref=${DOCKERHUB_CACHE_REPO}:csrc-rocm-branch-${ROCM_CACHE_UPSTREAM_BRANCH_TAG}" : "", - ]) -} - -function "get_cache_to_rocm_csrc" { - params = [] - result = compact([ - # Export the exact-commit native cache for same-commit reruns. - BUILDKITE_COMMIT != "" ? "type=registry,ref=${DOCKERHUB_CACHE_REPO}:csrc-rocm-${BUILDKITE_COMMIT},mode=${ROCM_CSRC_CACHE_TO_MODE}" : "", - # Export the branch-scoped native cache so later commits on the same branch - # can reuse compiled ROCm objects even when the exact parent cache is absent. - ROCM_CACHE_BRANCH_TAG != "" ? "type=registry,ref=${DOCKERHUB_CACHE_REPO}:csrc-rocm-branch-${ROCM_CACHE_BRANCH_TAG},mode=${ROCM_CSRC_CACHE_TO_MODE}" : "", - ]) -} - -function "get_cache_from_rocm_rust" { - params = [] - result = compact([ - BUILDKITE_COMMIT != "" ? "type=registry,ref=${DOCKERHUB_CACHE_REPO}:rust-rocm-${BUILDKITE_COMMIT}" : "", - PARENT_COMMIT != "" ? "type=registry,ref=${DOCKERHUB_CACHE_REPO}:rust-rocm-${PARENT_COMMIT}" : "", - VLLM_MERGE_BASE_COMMIT != "" ? "type=registry,ref=${DOCKERHUB_CACHE_REPO}:rust-rocm-${VLLM_MERGE_BASE_COMMIT}" : "", - ROCM_CACHE_BRANCH_TAG != "" ? "type=registry,ref=${DOCKERHUB_CACHE_REPO}:rust-rocm-branch-${ROCM_CACHE_BRANCH_TAG}" : "", - ROCM_CACHE_UPSTREAM_BRANCH_TAG != "" ? "type=registry,ref=${DOCKERHUB_CACHE_REPO}:rust-rocm-branch-${ROCM_CACHE_UPSTREAM_BRANCH_TAG}" : "", - ]) -} - -function "get_cache_to_rocm_rust" { - params = [] - result = compact([ - # Export exact-commit and branch-scoped Rust caches. A content-addressed - # cache ref is appended by ci-bake-rocm.sh when that wrapper is used. - BUILDKITE_COMMIT != "" ? "type=registry,ref=${DOCKERHUB_CACHE_REPO}:rust-rocm-${BUILDKITE_COMMIT},mode=${ROCM_RUST_CACHE_TO_MODE}" : "", - ROCM_CACHE_BRANCH_TAG != "" ? "type=registry,ref=${DOCKERHUB_CACHE_REPO}:rust-rocm-branch-${ROCM_CACHE_BRANCH_TAG},mode=${ROCM_RUST_CACHE_TO_MODE}" : "", - ]) -} - -# Cache functions for upstream dependency stages (NIXL/UCX, DeepEP). -# These stages are pinned to specific upstream commit hashes, so cache keys use -# those hashes rather than the Buildkite commit. This means the cache persists -# across all vLLM commits as long as the upstream dependency pins don't change. - -function "get_cache_from_rocm_deps" { - params = [] - result = compact([ - NIXL_CACHE_KEY != "" ? "type=registry,ref=${DOCKERHUB_CACHE_REPO}:nixl-rocm-${NIXL_CACHE_KEY}" : (NIXL_BRANCH != "" ? "type=registry,ref=${DOCKERHUB_CACHE_REPO}:nixl-rocm-${NIXL_BRANCH}-ucx-${UCX_BRANCH}" : ""), - DEEPEP_CACHE_KEY != "" ? "type=registry,ref=${DOCKERHUB_CACHE_REPO}:deepep-rocm-${DEEPEP_CACHE_KEY}" : (DEEPEP_BRANCH != "" ? "type=registry,ref=${DOCKERHUB_CACHE_REPO}:deepep-rocm-${DEEPEP_BRANCH}" : ""), - ]) -} - -function "get_cache_to_rocm_nixl" { - params = [] - result = compact([ - NIXL_CACHE_KEY != "" ? "type=registry,ref=${DOCKERHUB_CACHE_REPO}:nixl-rocm-${NIXL_CACHE_KEY},mode=min" : (NIXL_BRANCH != "" ? "type=registry,ref=${DOCKERHUB_CACHE_REPO}:nixl-rocm-${NIXL_BRANCH}-ucx-${UCX_BRANCH},mode=min" : ""), - ]) -} - -function "get_cache_to_rocm_deepep" { - params = [] - result = compact([ - DEEPEP_CACHE_KEY != "" ? "type=registry,ref=${DOCKERHUB_CACHE_REPO}:deepep-rocm-${DEEPEP_CACHE_KEY},mode=min" : (DEEPEP_BRANCH != "" ? "type=registry,ref=${DOCKERHUB_CACHE_REPO}:deepep-rocm-${DEEPEP_BRANCH},mode=min" : ""), - ]) -} - -# CI targets - -target "_ci-rocm" { - annotations = [ - "manifest:vllm.buildkite.build_number=${BUILDKITE_BUILD_NUMBER}", - "manifest:vllm.buildkite.build_id=${BUILDKITE_BUILD_ID}", - ] - args = { - ARG_PYTORCH_ROCM_ARCH = PYTORCH_ROCM_ARCH - CI_BASE_IMAGE = CI_BASE_IMAGE - ROCM_SMOKE_ID = BUILDKITE_BUILD_ID - max_jobs = CI_MAX_JOBS - VLLM_KERNEL_SYMBOL_MAP = VLLM_KERNEL_SYMBOL_MAP - buildkite_commit = BUILDKITE_COMMIT - } -} - -target "test-rocm-ci" { - inherits = ["_common-rocm", "_ci-rocm", "_labels"] - target = "test" - cache-from = get_cache_from_rocm() - cache-to = get_cache_to_rocm() - tags = compact([ - IMAGE_TAG, - IMAGE_TAG_LATEST, - ]) - output = ["type=registry"] -} - -# Validate the test image in the shared BuildKit graph and export only the -# success marker. This avoids pulling the multi-GB image into the host daemon. -target "smoke-test-rocm-ci" { - inherits = ["_common-rocm", "_ci-rocm"] - target = "export_test_smoke" - cache-from = get_cache_from_rocm() - output = ["type=local,dest=./build/rocm-smoke-export"] -} - -# Cache-only target for the source-scoped ROCm native build stage. -# This persists the csrc-build stage in the registry cache even though the -# final test image only consumes it indirectly while packaging the wheel. -target "csrc-rocm-ci" { - inherits = ["_common-rocm", "_ci-rocm"] - target = "csrc-build" - cache-from = get_cache_from_rocm_csrc() - cache-to = get_cache_to_rocm_csrc() - output = ["type=cacheonly"] -} - -target "kernel-symbol-map-rocm" { - inherits = ["_common-rocm", "_ci-rocm"] - target = "kernel-symbol-map-rocm" - cache-from = get_cache_from_rocm_csrc() - output = ["type=local,dest=./kernel-symbol-map-rocm"] -} - -# Cache-only target for the Rust frontend build stage. Final-image cache -# exports use mode=min and do not reliably persist intermediate cargo layers, -# so Rust gets its own source-scoped cache target. -target "rust-rocm-ci" { - inherits = ["_common-rocm", "_ci-rocm"] - target = "rust-build" - cache-from = get_cache_from_rocm_rust() - cache-to = get_cache_to_rocm_rust() - output = ["type=cacheonly"] -} - -# Keep wheel export on the same CI graph as the test image build so the -# shared build_vllm/export_vllm stages resolve identically within one bake -# invocation. Without this, export-wheel-rocm uses the plain local target -# args while test-rocm-ci uses CI-only args, which can lead to separate -# cache lineages and inconsistent export_vllm results. -target "export-wheel-rocm" { - inherits = ["_common-rocm", "_ci-rocm"] - target = "export_vllm" - cache-from = get_cache_from_rocm() - cache-to = get_cache_to_rocm() - output = ["type=local,dest=./wheel-export"] -} - -# Artifact-only vLLM build. GPU test jobs consume this artifact on top of -# ci_base, avoiding a per-commit multi-GB image push/pull. -group "test-rocm-ci-with-artifacts" { - targets = ["rust-rocm-ci", "csrc-rocm-ci", "export-wheel-rocm"] -} - -# Full test image + wheel export. Kept for fallback/debugging when a pushed, -# build-scoped image is useful. -group "test-rocm-ci-with-wheel" { - targets = [ - "rust-rocm-ci", - "csrc-rocm-ci", - "test-rocm-ci", - "smoke-test-rocm-ci", - "export-wheel-rocm", - ] -} - -# Primary output tag for the ci_base build. In Buildkite this is a unique, -# build-scoped tag; ci-bake-rocm.sh validates it before creating content and -# stable aliases. -variable "CI_BASE_IMAGE_TAG" { - default = "rocm/vllm-dev:ci_base" -} - -# Versioned, content-addressed trusted ref. Preview builds import it read-only; -# the wrapper creates their source-scoped content alias after validation. -variable "CI_BASE_TRUSTED_CONTENT_REF" { - default = "" -} - -# Cache-only targets for upstream dependency stages. These persist each stage -# in the registry cache keyed by its upstream commit hash. When ci_base rebuilds -# (e.g., requirements change), these stages are cache hits if their upstream -# pins haven't changed -- saving ~35min of compilation. -target "nixl-rocm-ci" { - inherits = ["_common-rocm", "_ci-rocm"] - target = "build_nixl" - cache-from = get_cache_from_rocm_deps() - cache-to = get_cache_to_rocm_nixl() - output = ["type=cacheonly"] -} - -target "deepep-rocm-ci" { - inherits = ["_common-rocm", "_ci-rocm"] - target = "build_deepep" - cache-from = get_cache_from_rocm_deps() - cache-to = get_cache_to_rocm_deepep() - output = ["type=cacheonly"] -} - -# Builds only the ci_base stage (NIXL, DeepEP, torchcodec, etc.) -# Invoked by the ensure-ci-base step when the content hash of ci_base-affecting -# files drifts from the remote image label. Per-PR builds then pull the result -# as CI_BASE_IMAGE instead of rebuilding those slow layers on every commit. -# Uses inline cache metadata on the ci_base image itself instead of exporting a -# separate registry cache artifact. -target "ci-base-rocm-ci" { - inherits = ["_common-rocm"] - target = "ci_base" - args = { - max_jobs = CI_MAX_JOBS - } - cache-from = concat( - compact([ - CI_BASE_IMAGE_TAG != "" ? "type=registry,ref=${CI_BASE_IMAGE_TAG}" : "", - CI_BASE_TRUSTED_CONTENT_REF != "" ? "type=registry,ref=${CI_BASE_TRUSTED_CONTENT_REF}" : "", - ]), - # Import upstream dependency caches so NIXL/DeepEP stages - # are cache hits even when ci_base itself needs rebuilding. - get_cache_from_rocm_deps(), - ) - cache-to = ["type=inline"] - tags = compact([CI_BASE_IMAGE_TAG]) - attest = ["type=provenance,disabled=true"] - output = ["type=registry"] -} - -# Group for ci_base builds -- exports dependency stage caches alongside the -# ci_base image so future rebuilds can reuse them independently. -group "ci-base-rocm-ci-with-deps" { - targets = ["nixl-rocm-ci", "deepep-rocm-ci", "ci-base-rocm-ci"] -} diff --git a/docker/context/base-wheels/.keep b/docker/context/base-wheels/.keep new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/docker/docker-bake-rocm.hcl b/docker/docker-bake-rocm.hcl index 2eb1d5a43076..75c0ebe0cb02 100644 --- a/docker/docker-bake-rocm.hcl +++ b/docker/docker-bake-rocm.hcl @@ -1,143 +1,162 @@ -# docker-bake-rocm.hcl - vLLM ROCm Docker build configuration -# -# This file lives in the vLLM repo at docker/docker-bake-rocm.hcl -# Equivalent of docker-bake.hcl for ROCm builds. -# -# Usage: -# docker buildx bake -f docker/docker-bake-rocm.hcl # Build test (default) -# docker buildx bake -f docker/docker-bake-rocm.hcl final-rocm # Build final image -# docker buildx bake -f docker/docker-bake-rocm.hcl --print # Show resolved config -# -# CI usage (with the vLLM-owned CI overlay): -# docker buildx bake -f docker/docker-bake-rocm.hcl -f docker/ci-rocm.hcl test-rocm-ci - -variable "MAX_JOBS" { - # Empty string lets the Dockerfile fall back to $(nproc) via - # MAX_JOBS="${MAX_JOBS:-$(nproc)}" in each RUN step, which uses all - # available cores on whatever machine the build runs on. - # Override with --set '*.args.max_jobs=8' for local builds on small machines. - default = "" -} - -variable "PYTORCH_ROCM_ARCH" { - default = "gfx90a;gfx942;gfx950" -} - -variable "COMMIT" { - default = "" -} - -# Content hash of ci_base-affecting files. Computed by ci-bake-rocm.sh and -# embedded as a label so future builds can compare without rebuilding. -variable "CI_BASE_CONTENT_HASH" { - default = "" -} - -variable "CI_BASE_DOCKERFILE" { - default = "docker/Dockerfile.rocm" -} - -# REMOTE_VLLM=0: use local source via Docker build context (ONBUILD COPY ./ vllm/) -# REMOTE_VLLM=1: clone from GitHub at VLLM_BRANCH (standalone builds without local source) -variable "REMOTE_VLLM" { - default = "0" -} - -variable "VLLM_BRANCH" { - default = "main" -} - -# CI_BASE_IMAGE: pre-built ci_base image for per-PR test builds. -# Defaults to the local "ci_base" stage for standalone/local builds. -# CI overrides this to "rocm/vllm-dev:ci_base" via environment variable. -variable "CI_BASE_IMAGE" { - default = "rocm/vllm-dev:ci_base" -} - -# Upstream dependency commit pins. Plain local bake builds use the Dockerfile -# ARG defaults. ci-bake-rocm.sh resolves those defaults (plus any env -# overrides) and writes a small HCL override before invoking CI targets. -variable "NIXL_BRANCH" { - default = "" -} - -variable "UCX_BRANCH" { - default = "" -} - -variable "DEEPEP_BRANCH" { - default = "" -} - -group "default" { - targets = ["test-rocm"] -} - -target "_common-rocm" { - dockerfile = CI_BASE_DOCKERFILE - context = "." - args = { - max_jobs = MAX_JOBS - ARG_PYTORCH_ROCM_ARCH = PYTORCH_ROCM_ARCH - REMOTE_VLLM = REMOTE_VLLM - VLLM_BRANCH = VLLM_BRANCH - CI_BASE_IMAGE = CI_BASE_IMAGE - } -} - -target "_labels" { - labels = { - "org.opencontainers.image.source" = "https://github.com/vllm-project/vllm" - "org.opencontainers.image.vendor" = "vLLM" - "org.opencontainers.image.title" = "vLLM ROCm" - "org.opencontainers.image.description" = "vLLM: A high-throughput and memory-efficient inference and serving engine for LLMs (ROCm)" - "org.opencontainers.image.licenses" = "Apache-2.0" - "org.opencontainers.image.revision" = COMMIT - } - annotations = [ - "manifest:org.opencontainers.image.revision=${COMMIT}", - ] -} - -target "test-rocm" { - inherits = ["_common-rocm", "_labels"] +# ROCm images (docker/Dockerfile.rocm). CI/release drive this via .buildkite/scripts/rocm/build.sh. Locally: +# docker buildx bake -f docker/docker-bake-rocm.hcl # test image, everything from source +# STACK_IMAGE=vllm/vllm-openai-rocm:base-nightly RUNTIME=vllm/vllm-openai-rocm:base-nightly \ +# docker buildx bake -f docker/docker-bake-rocm.hcl vllm-openai # compile only vLLM +variable "STACK" { default = "rocm" } # rocm: TheRock ROCm 10 +variable "STACK_IMAGE" { default = "" } # prebuilt stack ref@digest; empty: build it in this graph +variable "STACK_WHEELS" { default = "" } # that stack's -wheels image; empty: build it in this graph +variable "RUNTIME" { default = "" } # prebuilt runtime ref@digest; empty: build it in this graph +variable "TEST_DEPS" { default = "" } # prebuilt test-deps ref@digest; empty: build it in this graph +variable "CI_ARCH" { default = "gfx90a;gfx942;gfx950" } # MI250, MI300/325, MI355: what CI can test +variable "TAG" { default = "vllm-rocm:dev" } +variable "PUSH" { default = false } +variable "CACHE" { default = "" } # trusted cache repo (rocm/vllm-ci-cache): every CI build reads it +variable "CACHE_TO" { default = "" } # ref prefix this build writes ("" = none); build.sh sets it per repo +variable "AITER_SPEC" { default = "" } # pins the AITER nightly wheel (pip spec) +variable "AITER_NIGHTLY" { default = "" } # "1": test the newest AITER nightly wheel +variable "VLLM_VERSION" { default = "" } # release: image version label (build.sh reuse check) +variable "CACHE_CI" { default = true } # false (PR builds): skip the full ci cache; compile-cache still exports +variable "BUILDKITE_COMMIT" { default = "" } # the commit the kernel symbol map is stamped with +variable "CONTEXT" { default = "." } # build.sh: .rocm-src, an export of HEAD +variable "SCCACHE_BUCKET_NAME" { default = "" } # release: S3 sccache for the csrc compile (release-pipeline.yaml) + +function "cfrom" { # trusted cache (CI only), plus this build's own cache when CACHE_TO points elsewhere + # (without its "ci" when CACHE_CI is false: nothing writes that) + params = [names] + result = flatten([for p in distinct(compact([CACHE == "" ? "" : "${CACHE}:", CACHE_TO])) : + [for n in names : "type=registry,ref=${p}${STACK}-${n}" if CACHE_CI || n != "ci" || p != CACHE_TO]]) +} +function "cto" { + params = [name] + result = CACHE_TO == "" ? [] : ["type=registry,ref=${CACHE_TO}${STACK}-${name},mode=max,image-manifest=true,ignore-error=true,compression=zstd,compression-level=3,force-compression=true"] +} +# Images and cache layers are zstd: on the CI fleet, unpacking dominates pull time (vllm#60258, +# vllm#28656). Pulling needs Docker >= 23 or containerd >= 1.5. force-compression keeps every layer +# zstd: without it, a builder exports whichever copy of a layer it holds, e.g. a gzip one imported +# from an older cache. File mtimes stay as built: clamping them would leave every timestamp-based +# .pyc (pip's, and those written by imports during the build) stale in each new container. +function "out" { + params = [] + result = PUSH ? "type=registry,compression=zstd,compression-level=3,force-compression=true,oci-mediatypes=true" : "type=docker" +} +function "ctx" { # named contexts: prebuilt refs when given, else in-graph targets + params = [] + result = merge( + { stack = STACK_IMAGE == "" ? "target:stack-${STACK}" : "docker-image://${STACK_IMAGE}" }, + RUNTIME == "" ? {} : { runtime = "docker-image://${RUNTIME}" }, + TEST_DEPS == "" ? {} : { test-deps = "docker-image://${TEST_DEPS}" }) +} + +target "_rocm" { + context = CONTEXT + args = { SOURCE_DATE_EPOCH = "0", STACK = STACK } + attest = ["type=provenance,disabled=true"] # provenance would change the pushed digest on every build + output = [out()] +} +target "_stack" { + inherits = ["_rocm"] + target = "final" + tags = [TAG] + cache-from = cfrom(["stack"]) + cache-to = cto("stack") +} +# literal dockerfile paths: ci-infra ci_selector (images.py) only follows `dockerfile = "..."` literals +target "stack-rocm" { + inherits = ["_stack"] + dockerfile = "docker/Dockerfile.rocm_base" +} +target "stack-wheels" { + inherits = ["stack-${STACK}"] + target = "wheels" + tags = ["${TAG}-wheels"] + cache-to = [] +} +target "runtime" { + inherits = ["_rocm"] + dockerfile = "docker/Dockerfile.rocm" + target = "runtime" + contexts = ctx() + tags = [TAG] + cache-from = cfrom(["runtime"]) + cache-to = cto("runtime") +} +target "test-deps" { # content-keyed like the runtime (build.sh base mode) + inherits = ["_rocm"] + dockerfile = "docker/Dockerfile.rocm" + target = "test-deps" + contexts = ctx() + tags = [TAG] + cache-from = cfrom(["test-deps", "runtime", "ci"]) + cache-to = cto("test-deps") +} +target "_app" { + inherits = ["_rocm"] + dockerfile = "docker/Dockerfile.rocm" + contexts = ctx() + args = { ARCH = "", AITER_SPEC = AITER_SPEC, AITER_NIGHTLY = AITER_NIGHTLY, VLLM_VERSION = VLLM_VERSION, + SCCACHE_BUCKET_NAME = SCCACHE_BUCKET_NAME, CI_ARCH = CI_ARCH } + # dev/csrc records are shared. Only release (CACHE = "") writes "release"; only CI "ci" and "compile". + cache-from = cfrom(CACHE == "" ? ["runtime", "release"] : ["runtime", "ci", "compile"]) +} +target "test" { + inherits = ["_app"] target = "test" - tags = ["rocm/vllm:test"] - output = ["type=docker"] -} - -# CI base image target - builds only the ci_base stage (NIXL, DeepEP, -# torchcodec, requirements, etc.). Used by the weekly scheduled build and -# the auto-rebuild trigger when requirements change in a PR. -target "ci-base-rocm" { - inherits = ["_common-rocm", "_labels"] - target = "ci_base" - labels = { - "vllm.ci_base.content_hash" = CI_BASE_CONTENT_HASH - } - tags = ["rocm/vllm-dev:ci_base"] - output = ["type=docker"] -} - -# Wheel export target - extracts the built vLLM wheel + test workspace -# to local disk. Used by CI to upload the wheel as a Buildkite artifact -# so test jobs can assemble images locally from ci_base + wheel instead -# of pulling the full large image from Docker Hub. -# -# Usage: -# docker buildx bake -f docker/docker-bake-rocm.hcl export-wheel-rocm -# # Creates ./wheel-export/*.whl, ./wheel-export/requirements/, etc. -# -# After a full bake build, BuildKit cache makes this nearly instant. -target "export-wheel-rocm" { - inherits = ["_common-rocm"] - target = "export_vllm" - output = ["type=local,dest=./wheel-export"] -} - -target "final-rocm" { - inherits = ["_common-rocm", "_labels"] - target = "final" - tags = ["rocm/vllm:latest"] - output = ["type=docker"] -} + args = { ARCH = CI_ARCH } + tags = [TAG] + cache-to = CACHE_CI ? cto("ci") : [] + no-cache-filter = AITER_SPEC == "" && AITER_NIGHTLY != "1" ? [] : ["test-base"] # AITER nightly: always fetch the newest wheel +} +target "test-smoke" { + inherits = ["test"] + target = "test-smoke" + output = ["type=cacheonly"] + cache-to = [] +} +target "compile-cache" { # csrc + rust-deps on their own: a fresh builder (or a new PR) reuses them + inherits = ["_app"] + target = "compile" + args = { ARCH = CI_ARCH } + output = ["type=cacheonly"] + cache-to = cto("compile") +} +target "vllm-wheel" { # the python-only install test's wheel, uploaded as a build artifact + inherits = ["test"] + target = "vllm-wheel" + output = ["type=local,dest=.rocm-wheel"] + cache-to = [] +} +target "kernel-symbol-map" { # CI test selection (build.sh ci, VLLM_KERNEL_SYMBOL_MAP=1): from test's csrc + inherits = ["test"] + target = "kernel-symbol-map" + args = { BUILDKITE_COMMIT = BUILDKITE_COMMIT } + tags = [] + output = ["type=local,dest=.rocm-kernel-map"] + cache-to = [] +} +target "vllm-openai" { + inherits = ["_app"] + target = "vllm-openai" + tags = [TAG] +} +# cacheonly checks +target "openai-smoke" { + inherits = ["_app"] + target = "openai-smoke" + output = ["type=cacheonly"] +} +target "wheels" { + inherits = ["_app"] + target = "wheels" + tags = ["${TAG}-wheels"] + contexts = merge(ctx(), { stack-wheels = STACK_WHEELS == "" ? "target:stack-wheels" : "docker-image://${STACK_WHEELS}" }) + cache-to = cto("release") # the all-arch compile, for the next release +} +target "version" { + inherits = ["_app"] + target = "version" + output = ["type=local,dest=.rocm-version"] +} + +group "default" { targets = ["test"] } +group "base" { targets = ["stack-${STACK}", "stack-wheels"] } # build.sh pushes runtime separately (own key) +group "ci" { targets = ["test", "test-smoke", "compile-cache", "vllm-wheel"] } diff --git a/docker/versions.json b/docker/versions.json index 03a9babdc0d9..84e4ac65d915 100644 --- a/docker/versions.json +++ b/docker/versions.json @@ -37,9 +37,6 @@ "INSTALL_KV_CONNECTORS": { "default": "false" }, - "BUILD_NIXL": { - "default": "false" - }, "SCCACHE_BUCKET_NAME": { "default": "vllm-build-sccache" }, @@ -83,7 +80,7 @@ "default": ">=1.0.17" }, "RUNAI_MODEL_STREAMER_VERSION": { - "default": ">=0.15.7" + "default": ">=0.17.1" }, "GGUF_VERSION": { "default": ">=0.19.0" diff --git a/docs/.nav.yml b/docs/.nav.yml index 783c40bb5c6d..0b470f200019 100644 --- a/docs/.nav.yml +++ b/docs/.nav.yml @@ -30,7 +30,7 @@ nav: - models/extensions - Hardware Supported Models: - models/hardware_supported_models/* - - TPU: https://docs.vllm.ai/projects/tpu/en/latest/recommended_models_features/ + - TPU: https://docs.vllm.ai/projects/tpu/en/latest/recommended_models/ - Features: features - Developer Guide: - contributing/README.md diff --git a/docs/benchmarking/cli.md b/docs/benchmarking/cli.md index 1ea961fefa4f..533b89816971 100644 --- a/docs/benchmarking/cli.md +++ b/docs/benchmarking/cli.md @@ -14,6 +14,29 @@ It's a living document, updated as new features and datasets become available. For exact-batch model timing, including steady-state pipeline-parallel forwards, see [vllm bench forward](../cli/README.md#forward). +For skewed synthetic MoE expert loads, use `--routing zipf`: + +```bash +vllm bench forward --model Qwen/Qwen3-30B-A3B \ + --routing zipf --case decode=6*1@8192 --case prefill=2048 +``` + +Each token independently selects distinct experts without replacement, weighted +by popularity rank to the power of `-1.25`. Expert popularity is randomly +permuted per layer to avoid systematically assigning hot experts to the lowest +EP ranks. +The fixture is seeded by `--seed`, fixed across timing replays, and generated +outside the timed forward. Sequence-parallel TP ranks use their corresponding +token rows. Zipf allows EP load imbalance; `--routing balanced` gives equal +load per EP rank. Neither synthetic mode supports grouped top-k routing; +Zipf also does not support prefill context parallelism. + +The default `--routing auto` selects Zipf with dummy weights and real routing +otherwise. When the model or parallel layout does not support the synthetic +fixture, `auto` warns and falls back to real routing. Use `--routing balanced` +to explicitly request equal EP load. Zipf is a synthetic workload, not a fit +to a particular model's routing or speculative-token correlations. + ## Attention Kernel Benchmark Run the attention kernel smoke benchmark from the repository root using the diff --git a/docs/cli/README.md b/docs/cli/README.md index a8880f493d3c..116cd12f9c58 100644 --- a/docs/cli/README.md +++ b/docs/cli/README.md @@ -183,8 +183,10 @@ Use `--exclude-drafter` to measure only the target forward. Verification assumes all draft tokens are accepted and uses a fixed bonus token; target sampling and rejection sampling are excluded. Both paths use CUDA graphs and restore the batch's state before each replay. -Balanced MoE routing fixtures apply to the target model; the drafter uses its -model routing, so use real weights when measuring a MoE drafter. +Synthetic MoE routing fixtures (`--routing balanced` or `--routing zipf`) apply +to the target model; the drafter uses its model routing, so use real weights +when measuring a MoE drafter. For the Zipf distribution and its limitations, +see [Model Forward Benchmark](../benchmarking/cli.md#model-forward-benchmark). ```bash vllm bench forward --model Qwen/Qwen3-1.7B \ diff --git a/docs/configuration/optimization.md b/docs/configuration/optimization.md index 18122ea2c8bc..fea8977b2ede 100644 --- a/docs/configuration/optimization.md +++ b/docs/configuration/optimization.md @@ -156,10 +156,47 @@ GPU. vLLM can pin each worker with `numactl` before the Python subprocess starts so the interpreter, imports, and early allocator state are created with the desired NUMA policy from the beginning. -Use `--numa-bind` to enable the feature. By default, vLLM auto-detects the -GPU-to-NUMA mapping and uses `--cpunodebind= --membind=` for each -worker. When you need a custom CPU policy, add `--numa-bind-cpus` and vLLM will -switch to `--physcpubind= --membind=`. +By default, vLLM binds automatically when the environment supports it: +CUDA-alike platform, multiple NUMA nodes, `numactl` installed, permission to +set NUMA memory policy, unrestricted CPU affinity, a default NUMA memory policy, +the `spawn` multiprocessing start method (the CLI default), and a local `mp` or +`uni` executor with multiprocessing DP, including supported prefill context +parallelism (PCP) launches. Ray, external launchers and `--device-ids` remapping +are excluded from automatic binding. +Otherwise vLLM starts unbound. Use +`--numa-bind` to make a missing `numactl` or undetectable GPU-to-NUMA topology a +startup error, or `--no-numa-bind` to disable binding. Passing +`--numa-bind-nodes` or `--numa-bind-cpus` explicitly enables binding even without +`--numa-bind`: either list requests strict `--membind`, not automatic soft memory +preference or CPU-only binding. Neither can be combined with `--no-numa-bind`. +Automatic binding preserves an existing non-default memory policy, such as +interleave, preferred-node, or bind. Explicit enablement may override it. + +Automatic binding uses `--cpunodebind= --preferred=` for each worker: +memory allocations prefer the GPU-local node but can fall back to other nodes. +EngineCore prefers its shard's nodes with `--preferred-many=` when the +shard spans multiple NUMA nodes, or `--preferred=` for a single-node shard. +Explicit `--numa-bind` or binding lists retain strict `--membind=`. +When you need a custom CPU policy, add `--numa-bind-cpus` and vLLM will switch +to `--physcpubind=`. + +Automatic eligibility and topology validation happen in each local launcher +before starting its first bound subprocess, using freshly discovered topology. +The mapping must cover all visible GPUs and local workers, and each mapped NUMA +node must have CPUs. The resolved decision and mapping are passed to spawned +children, so an already-bound EngineCore does not repeat detection under its +restricted CPU affinity. +PCP workers use the full TP × PCP × PP shard size for local DP GPU offsets; +EngineCore's CPU affinity covers all workers in its shard. +Multi-node multiprocessing launchers resolve their own host-local mapping; +NUMA node IDs are never interpreted as cluster-wide IDs. + +Before each bound launch, vLLM probes the selected policy with +`numactl true`. If CPU-and-memory binding is rejected, it retries with +CPU-only binding; if that is also rejected, the subprocess starts unbound. +Logs report the selected policy or fallback. If `--preferred-many` is unsupported, +automatic mode falls back to CPU-only binding, never strict memory binding. +These probes test policy acceptance, not future allocation capacity. These `--numa-bind*` options only apply to GPU execution processes. They do not configure the CPU backend's separate thread-affinity controls. Automatic @@ -174,7 +211,7 @@ order as the GPU indices. Each CPU list must use `numactl --physcpubind` syntax such as `0-3`, `0,2,4-7`, or `16-31,48-63`. ```bash -# Auto-detect NUMA nodes for visible GPUs +# Require binding, auto-detecting NUMA nodes for visible GPUs vllm serve meta-llama/Llama-3.1-8B-Instruct \ --tensor-parallel-size 4 \ --numa-bind @@ -195,10 +232,12 @@ vllm serve meta-llama/Llama-3.1-8B-Instruct \ Notes: -- CLI usage forces multiprocessing to use the `spawn` method automatically. If you enable NUMA binding through the Python API, also set `VLLM_WORKER_MULTIPROC_METHOD=spawn`. -- Automatic detection relies on NVML and NUMA support from the host. If it cannot determine the mapping reliably, pass `--numa-bind-nodes` explicitly. +- The CLI defaults to `spawn` when `VLLM_WORKER_MULTIPROC_METHOD` is unset. Automatic NUMA binding never changes the start method: if it is `fork`, default binding stays off. Python API usage normally defaults to `fork`; set `VLLM_WORKER_MULTIPROC_METHOD=spawn` to allow binding and follow Python's `if __name__ == "__main__":` requirements. +- Automatic memory preference permits allocations on other NUMA nodes when local memory is unavailable. Explicit `--numa-bind` still uses `--membind`, which restricts host allocations to the selected nodes; for host-memory-heavy setups (e.g. large CPU KV offload), avoid explicit strict binding if workers need memory from other nodes. The policy probes do not check allocation capacity or container memory limits. +- Automatic detection relies on NVML (CUDA) or amdsmi (ROCm) and NUMA support from the host. If it cannot determine the mapping reliably, default mode starts unbound; pass `--numa-bind-nodes` to provide a mapping explicitly. - Explicit `--numa-bind-nodes` and `--numa-bind-cpus` values must be valid `numactl` inputs. vLLM does a small amount of validation, but the effective binding semantics are still determined by `numactl`. - The current implementation binds GPU execution processes such as `EngineCore` and multiprocessing workers. It does not apply NUMA binding to frontend API server processes or the DP coordinator. +- Ray actors are not bound by the multiprocessing executable wrapper. Enabling automatic NUMA binding for Ray requires a separate actor-launch implementation. - In containerized environments, NUMA policy syscalls may require extra permissions, such as `--cap-add SYS_NICE` when running via `docker run`. ### CPU Backend Thread Affinity @@ -380,6 +419,11 @@ A processed item larger than this budget is served uncached (with a warning) instead of failing engine startup; raise `mm_processor_cache_gb` if you want those items cached. +With processor-only caching, a request can contain more processed multi-modal +data than the cache budget. Cached tensors and prompt updates needed by the +request remain referenced while its inputs are merged, even if they are evicted +from the LRU. The budget limits cache storage, not transient per-request memory. + If you do not benefit much from the cache, you can disable both IPC and processor caching completely via `mm_processor_cache_gb=0`. diff --git a/docs/contributing/README.md b/docs/contributing/README.md index d665116e47b5..c97b5bd6c17a 100644 --- a/docs/contributing/README.md +++ b/docs/contributing/README.md @@ -103,6 +103,13 @@ integration, use the configuration files in `tools/pre_commit/`: pre-commit run -a # runs on all files (short for --all-files) ``` + Hook revisions in `.pre-commit-config.yaml` are pinned to commit SHAs. To bump them, use: + + ```bash + pre-commit autoupdate --freeze # bumps all hooks + pre-commit autoupdate --freeze --repo # bumps one hook + ``` + ### Documentation MkDocs is a fast, simple and downright gorgeous static site generator that's geared towards building project documentation. Documentation source files are written in Markdown, and configured with a single YAML configuration file, [mkdocs.yaml](../../mkdocs.yaml). diff --git a/docs/contributing/dockerfile/dockerfile.md b/docs/contributing/dockerfile/dockerfile.md index b5c8b8001a2f..307d98b9a0eb 100644 --- a/docs/contributing/dockerfile/dockerfile.md +++ b/docs/contributing/dockerfile/dockerfile.md @@ -29,11 +29,6 @@ The `extensions-build` stage can also produce an optional source-built Triton wheel. `vllm-openai-base` installs that wheel after its other Python dependencies so dependency resolution cannot restore an older Triton version. -With `BUILD_NIXL=true`, `extensions-build` also builds NIXL, including its NIXL -EP extension for the installed PyTorch, with `tools/build_nixl_with_torch215_for_rubin.py`. -`vllm-openai-base` then replaces the NIXL packages installed from the KV-connector -requirements with these wheels. - The AWS EFA images are built separately by `docker/Dockerfile.efa` on top of a `vllm-openai` image built with `INSTALL_KV_CONNECTORS=true`. It adds a minimal AWS EFA userspace install (libfabric, aws-ofi-nccl and hwloc), the EFA Mooncake diff --git a/docs/contributing/model/transcription.md b/docs/contributing/model/transcription.md index f82bafa44d52..4fdab121cd85 100644 --- a/docs/contributing/model/transcription.md +++ b/docs/contributing/model/transcription.md @@ -66,7 +66,11 @@ This is for controlling general behavior of the API when serving your model: See [Audio preprocessing and chunking](#audio-preprocessing-and-chunking) for what each field controls. -Implement the prompt construction via [get_generation_prompt][vllm.model_executor.models.interfaces.SupportsTranscription.get_generation_prompt]. The server builds a [SpeechToTextParams][vllm.config.speech_to_text.SpeechToTextParams] object that bundles the resampled waveform, task parameters, and request-specific options. Your model receives this single object and returns a valid [PromptType][vllm.inputs.llm.PromptType]. There are two common patterns: +Implement the prompt construction via [get_generation_prompt][vllm.model_executor.models.interfaces.SupportsTranscription.get_generation_prompt]. The server builds a [SpeechToTextParams][vllm.config.speech_to_text.SpeechToTextParams] object that bundles the resampled waveform, task parameters, and request-specific options. Your model receives this single object and returns a valid [PromptType][vllm.inputs.llm.PromptType]. + +For transcription requests, `stt_params.extra_args` contains the optional `vllm_xargs` dictionary, which is also passed to `SamplingParams.extra_args`. Models can read request-specific frontend or prompt options here and should validate the options they consume. An omitted dictionary is `None`. Multipart clients can send `vllm_xargs` as a JSON string. These options apply to the request; do not use them to mutate the shared model configuration. + +There are two common patterns: #### Multimodal LLM with audio embeddings (e.g., Voxtral, Gemma3n) @@ -194,7 +198,7 @@ Provide a fast duration→token estimate to improve streaming usage statistics: The API server takes care of basic audio I/O and optional chunking before building prompts: -- Resampling: Input audio is resampled to `SpeechToTextConfig.sample_rate` using `AudioResampler`. +- Resampling: Input audio is resampled to `SpeechToTextConfig.sample_rate`. Models can set specific `audio_resample_method` (e.g., `pyav`, `scipy`, `torchaudio`, etc) to decode at the native sample rate, then resample with `AudioResampler` before duration calculation and chunking. This can match a model's reference audio frontend without changing other models' defaults. - Chunking: If `SpeechToTextConfig.allow_audio_chunking` is True and the duration exceeds `max_audio_clip_s`, the server splits the audio into chunks and generates a prompt per chunk. There is no overlap between chunks, overlap_chunk_second controls the size of the search window used to find the split point. - Energy-aware splitting: When `min_energy_split_window_size` is set, the server finds low-energy regions to minimize cutting within words. diff --git a/docs/deployment/integrations/dynamo.md b/docs/deployment/integrations/dynamo.md index 8d0a0dcb0c84..f22e9f520cbc 100644 --- a/docs/deployment/integrations/dynamo.md +++ b/docs/deployment/integrations/dynamo.md @@ -2,6 +2,6 @@ [NVIDIA Dynamo](https://github.com/ai-dynamo/dynamo) is an open-source framework for distributed LLM inference that can run vLLM on Kubernetes with flexible serving architectures (e.g. aggregated/disaggregated, optional router/planner). -For Kubernetes deployment instructions and examples (including vLLM), see the [Deploying Dynamo on Kubernetes](https://github.com/ai-dynamo/dynamo/blob/main/docs/kubernetes/README.md) guide. +For Kubernetes deployment instructions and examples (including vLLM), see the [Deploying Dynamo on Kubernetes](https://docs.nvidia.com/dynamo/kubernetes/getting-started/introduction) guide. Background reading: InfoQ news coverage — [NVIDIA Dynamo simplifies Kubernetes deployment for LLM inference](https://www.infoq.com/news/2025/12/nvidia-dynamo-kubernetes/). diff --git a/docs/deployment/integrations/k8s-aibom.md b/docs/deployment/integrations/k8s-aibom.md new file mode 100644 index 000000000000..8f1cf1ac25a9 --- /dev/null +++ b/docs/deployment/integrations/k8s-aibom.md @@ -0,0 +1,29 @@ +# k8s-aibom + +[k8s-aibom](https://github.com/GoogleCloudPlatform/k8s-aibom) is an open-source Kubernetes controller that keeps an inventory of the AI workloads actually serving in a cluster. For every vLLM deployment it produces a [CycloneDX ML-BOM](https://cyclonedx.org/capabilities/mlbom/): the vLLM image and resolved digest, the model the server was started with, and, when the model is signed, the verified signer identity. Each attribute carries the evidence it was read from and a confidence tier (declared, inferred, or unresolved). + +It is read-only toward your cluster: it watches workloads through the Kubernetes API, adds no sidecars or init containers, runs unprivileged, and never changes scheduling or serving behavior. vLLM is detected whether you run it as a plain Deployment or StatefulSet, as a LeaderWorkerSet or DisaggregatedSet, under [KubeAI](kubeai.md), [KServe](kserve.md), [KubeRay](kuberay.md), [llm-d](llm-d.md), or the [production stack](production-stack.md), which ships it as an optional chart dependency. + +## Install + +```bash +helm install k8s-aibom oci://ghcr.io/googlecloudplatform/charts/k8s-aibom \ + --version 1.6.0 \ + --namespace k8s-aibom-system --create-namespace +``` + +Then opt in each namespace that serves models: + +```bash +kubectl label namespace aibom.k8saibom.dev/enabled=true +``` + +Documents appear as `AIBOM` resources next to the workloads they describe. The `kubectl aibom` plugin (`kubectl krew install aibom`) summarizes and searches them: + +```bash +kubectl aibom summary -A +kubectl aibom find --runtime vllm -A +kubectl aibom find --image vllm/vllm-openai -A +``` + +See the [k8s-aibom documentation](https://github.com/GoogleCloudPlatform/k8s-aibom#readme) for the document format, signature verification, external sinks, and the full list of detected runtimes. diff --git a/docs/deployment/integrations/llm-d.md b/docs/deployment/integrations/llm-d.md index dc335edb0399..d125a8dc034a 100644 --- a/docs/deployment/integrations/llm-d.md +++ b/docs/deployment/integrations/llm-d.md @@ -9,10 +9,10 @@ It is a [CNCF Sandbox project](https://www.cncf.io/blog/2026/03/24/welcome-llm-d A single vLLM server is fast, but at scale the picture changes: across many replicas, cache locality breaks under round-robin load balancing, long prompts inflate time-to-first-token, and accelerators sit underused. llm-d adds the cluster-level layer that vLLM does not aim to provide on its own: - **[Prefix-aware routing](https://llm-d.ai/docs/well-lit-paths/foundations/precise-prefix-cache-routing).** Instead of round-robin, llm-d reads vLLM's KV-cache events and routes each request to the replica that already holds its prefix, reusing cache instead of recomputing it. -- **[Distributed KV-cache management](https://llm-d.ai/docs/architecture/advanced/kv-management).** A global index tracks which token blocks live on which replica, and [tiered offloading](https://llm-d.ai/docs/well-lit-paths/foundations/tiered-prefix-cache) spills cache to CPU memory or local SSD, extending the working set beyond accelerator HBM. +- **[Distributed KV-cache management](https://llm-d.ai/docs/architecture/kv-management).** A global index tracks which token blocks live on which replica, and [tiered offloading](https://llm-d.ai/docs/well-lit-paths/foundations/tiered-prefix-cache) spills cache to CPU memory or local SSD, extending the working set beyond accelerator HBM. - **[Prefill/decode disaggregation](https://llm-d.ai/docs/well-lit-paths/foundations/pd-disaggregation).** Prompt processing and token generation run on separate vLLM workers, with KV-cache moved over the vLLM [NIXL connector](https://docs.vllm.ai/en/latest/features/nixl_connector_usage/), lowering TTFT and steadying per-token latency on long prompts. - **[Wide expert-parallelism](https://llm-d.ai/docs/well-lit-paths/foundations/wide-expert-parallelism).** Serve large Mixture-of-Experts models such as DeepSeek-R1 and GPT-OSS across nodes with combined data and expert parallelism, for more KV-cache capacity and throughput. -- **SLO-aware [autoscaling](https://llm-d.ai/docs/well-lit-paths/foundations/workload-autoscaling) and [flow control](https://llm-d.ai/docs/well-lit-paths/foundations/flow-control).** Scale vLLM pools on real inference signals (queue depth, true demand) rather than raw GPU utilization, with multi-tenant fairness and priority dispatch. +- **SLO-aware [autoscaling](https://llm-d.ai/docs/operations/autoscaling) and [flow control](https://llm-d.ai/docs/operations/traffic/flow-control).** Scale vLLM pools on real inference signals (queue depth, true demand) rather than raw GPU utilization, with multi-tenant fairness and priority dispatch. These are composable. Most teams start by adding prefix-aware routing over an existing vLLM pool, then layer in the rest as specific bottlenecks appear. diff --git a/docs/deployment/k8s.md b/docs/deployment/k8s.md index e7d0853e9f49..8b10baab23d4 100644 --- a/docs/deployment/k8s.md +++ b/docs/deployment/k8s.md @@ -431,6 +431,43 @@ You can also verify the health service manually with `grpcurl`: grpcurl -plaintext localhost:50051 grpc.health.v1.Health/Check ``` +### Rust frontend gRPC health checks + +The Rust frontend is a separate server from `--grpc` above. Start it with `VLLM_USE_RUST_FRONTEND=1 vllm serve --grpc-port 50051` or `vllm-rs serve --grpc-port 50051`. Its gRPC port serves `grpc.health.v1.Health` and reports engine liveness and inference readiness under separate service names, so a paused engine can leave the load balancer without being restarted: + +| Service | Reports `NOT_SERVING` when | +| --- | --- | +| `liveness` | The engine has failed. Pause, sleep, weight updates, and orderly shutdown do not affect it until the listener closes. | +| `readiness` and `vllm.Inference` | The engine has failed, shutdown has started, the engine is paused (including for a process checkpoint), asleep, or partially awake, or a weight update has not finished successfully. | +| `vllm.Control` and `""` | The engine has failed or shutdown has started. | + +Point the startup and liveness probes at `liveness` and the readiness probe at `readiness`. A liveness probe on `readiness` or `vllm.Inference` restarts an engine that stays paused longer than the probe's failure threshold. The listener stops accepting connections when shutdown starts, so probes see the endpoint as unavailable while `--shutdown-timeout` drains in-flight requests. + +```yaml +startupProbe: + grpc: + port: 50051 + service: liveness + periodSeconds: 10 + failureThreshold: 120 # Allow for model loading. +livenessProbe: + grpc: + port: 50051 + service: liveness +readinessProbe: + grpc: + port: 50051 + service: readiness +``` + +A paused or sleeping pod leaves the endpoints of its Services, so send resume, wake-up, and weight-update calls to the pod directly, for example through a headless Service with `publishNotReadyAddresses: true`. After a process checkpoint fails, the engine stays paused and `liveness` stays `SERVING`, so whatever drives the checkpoint must restart the pod when `/checkpoint/status` reports the failure. + +This server does not enable gRPC reflection, so check a service manually with [`grpc_health_probe`](https://github.com/grpc-ecosystem/grpc-health-probe): + +```bash +grpc_health_probe -addr=localhost:50051 -service=readiness +``` + ## Troubleshooting ### Startup Probe or Readiness Probe Failure, container log contains "KeyboardInterrupt: terminated" diff --git a/docs/design/arch_overview.md b/docs/design/arch_overview.md index 711e9a2668eb..f7695d4a3d92 100644 --- a/docs/design/arch_overview.md +++ b/docs/design/arch_overview.md @@ -131,6 +131,58 @@ A data parallel deployment with 8 GPUs (`vllm serve -tp=2 -dp=4`) has: For CPU resource sizing recommendations, see [CPU Resources for GPU Deployments](../configuration/optimization.md#cpu-resources-for-gpu-deployments). +## Scheduler + +The scheduler runs inside the **Engine Core Process** and decides which +requests execute each step. The code can be found in +[vllm/v1/core/sched/scheduler.py](../../vllm/v1/core/sched/scheduler.py). + +The scheduler is **token-budget-based**: each step, it fills a budget of +`max_num_batched_tokens` across both running and waiting requests. There are no +separate prefill and decode phases. Each request tracks how many of its tokens +have been computed, and the scheduler assigns it tokens to close the gap. This +single loop handles chunked prefill, prefix caching and speculative decoding. + +### Scheduling Policies + +Two policies are supported via `--scheduling-policy`: + +- **FCFS** (default): requests are scheduled first-come, first-served. When + the KV cache is exhausted, the last-admitted running request is preempted. +- **Priority**: requests carry an integer `priority` field. When the KV cache + is exhausted, the running request with the lowest priority (highest + `priority` value, then latest arrival time) is preempted. + +### Preemption + +When a request cannot be allocated new KV cache blocks, the scheduler +**preempts** running requests, chosen by the scheduling policy, until enough +blocks are free: + +1. The preempted request's KV cache blocks are freed. +2. Its progress is reset and it returns to the waiting queue. Under FCFS it + goes to the front of the queue; under Priority it is reinserted by + `(priority, arrival_time)`. +3. When re-admitted, it is recomputed rather than swapped back from CPU + memory. Prefix caching or a KV connector can restore part of its progress + immediately. + +For how to reduce preemption, see +[Preemption](../configuration/optimization.md#preemption). + +### Async Scheduling + +With async scheduling, +[vllm/v1/core/sched/async_scheduler.py](../../vllm/v1/core/sched/async_scheduler.py) +overlaps CPU scheduling with GPU execution: while the GPU executes step *N*, +the CPU schedules step *N+1*, using placeholders for the tokens that step *N* +will produce. + +If a request is preempted while it still has output in flight, that output is +normally still delivered when it arrives. It is dropped instead when delivering +it would be invalid, for example when `reset_prefix_cache` preempts and resumes +requests within the same step. + ## LLM Engine The `LLMEngine` and `AsyncLLMEngine` classes are central to the functioning of diff --git a/docs/design/custom_op.md b/docs/design/custom_op.md index f99c8043fa04..84faeaeff42e 100644 --- a/docs/design/custom_op.md +++ b/docs/design/custom_op.md @@ -264,7 +264,7 @@ Currently, thanks to [vLLM's hardware-plugin mechanism](./plugin_system.md), the - **Official device plugins:** [vllm-ascend](https://github.com/vllm-project/vllm-ascend) (for Huawei Ascend NPU), [vllm-spyre](https://github.com/vllm-project/vllm-spyre) (for Spyre), [vllm-gaudi](https://github.com/vllm-project/vllm-gaudi) (for Intel Gaudi), [vllm-neuron](https://github.com/vllm-project/vllm-neuron) (for AWS Neuron), [vllm-meta](https://github.com/vllm-project/vllm-metal) (for Apple Silicon), etc. -- **Non-official device plugins:** [vllm-metax](https://github.com/MetaX-MACA/vLLM-metax) (for MetaX GPU), [vllm-kunlun](https://github.com/baidu/vLLM-Kunlun) (for Baidu Kunlun XPU), [vllm-musa](https://github.com/MooreThreads/vllm-musa) (for Moore Threads GPU), etc. +- **Non-official device plugins:** [vllm-metax](https://github.com/MetaX-MACA/vLLM-metax) (for MetaX GPU), [vllm-kunlun](https://github.com/baidu/vLLM-Kunlun) (for Baidu Kunlun XPU), [vllm-musa](https://github.com/MooreThreads/vllm-musa) (for Moore Threads GPU), [vllm-iluvatar](https://github.com/Deep-Spark/vllm-iluvatar) (for Iluvatar CoreX GPU), etc. In this case, `CustomOp` can enable these hardware manufacturers to seamlessly replace vLLM's operations with their deep-optimized kernels for specific devices at runtime, by just registering an OOT `CustomOp` and implementing the `forward_oot()` method. @@ -314,3 +314,46 @@ In addition, you can also register all your `CustomOp` at one place for better m for op_name, op_cls in REGISTERED_CUSTOM_OPS.items(): CustomOp.register_oot(_decorated_op_cls=op_cls, name=op_name) ``` + +#### Opt in to the hw-agnostic layers + +With `VLLM_USE_HW_AGNOSTIC=1`, the Transformers modeling backend builds layers from `vllm.model_executor.hw_agnostic.layers` where a hw-agnostic implementation exists, and from the in-tree `vllm.model_executor.layers` otherwise. These are separate classes with separate `op_registry_oot`s, so an override of the in-tree class does not reach the hw-agnostic one. + +To override the class the Transformers backend builds on either path, obtain it with `hw_agnostic.resolve` instead of importing it, and register through that class's `register_oot`: + +??? code + + ```python + from vllm.model_executor import hw_agnostic + + SiluAndMul = hw_agnostic.resolve("activation", "SiluAndMul") + + + @SiluAndMul.register_oot + class CustomSiluAndMul(SiluAndMul): + + def forward_oot(...): + # Call optimized device-specific kernels. + ... + ``` + +`resolve` returns the hw-agnostic class when `VLLM_USE_HW_AGNOSTIC` is set and one exists, and the in-tree class otherwise, so the plugin code is the same for both paths. Do not register through `vllm.model_executor.custom_op.CustomOp.register_oot`, which always writes to the in-tree registry. On the hw-agnostic path, vLLM warns about an override registered only for the in-tree class, and raises if an override does not derive from the class it replaces. + +`VLLM_USE_HW_AGNOSTIC` only affects the Transformers backend. Models with a native vLLM implementation build the in-tree class even when it is set, so a plugin that also serves those models registers its override for the in-tree class as well. + +The Transformers backend builds its norms as `TPAwareRMSNorm` and `TPAwareGemmaRMSNorm`, subclasses of `RMSNorm` and `GemmaRMSNorm`. Overrides are keyed on the exact class name, so an `RMSNorm` or `GemmaRMSNorm` override does not reach them yet. + +Type checkers do not accept a variable as a base class, so plugins that run mypy can import the in-tree class for type checking only: + +??? code + + ```python + from typing import TYPE_CHECKING + + from vllm.model_executor import hw_agnostic + + if TYPE_CHECKING: + from vllm.model_executor.layers.activation import SiluAndMul + else: + SiluAndMul = hw_agnostic.resolve("activation", "SiluAndMul") + ``` diff --git a/docs/design/hisparse.md b/docs/design/hisparse.md index f5fab3c0f5d2..64aab6f52715 100644 --- a/docs/design/hisparse.md +++ b/docs/design/hisparse.md @@ -221,9 +221,16 @@ hot row? ──────── yes ──► existing hot physical row + upda choose GPU LRU victim ──► copy pinned host row ──► hot physical row ``` -ROCm is not currently supported because the fused HiSparse cache operations are -implemented only by CUDA kernels. A future platform-specific worker may provide -the same command, output, and cache-resolution boundaries. +Device operations are selected by `Platform.get_hisparse_backend_cls()`. The base +platform returns `None`; CUDA selects `CUDAHiSparseBackend` in `platforms/cuda.py`. +Hardware plugins can provide a `HiSparseBackend` for host registration, cache +operations, IPC events, capture queries, and transfers while reusing the common +runtime and connector. HiSparse validates availability and cache formats before +allocation. Streams, ordinary events, current-stream queries, and synchronization +use `torch.Stream`, `torch.Event`, and `torch.accelerator` directly. The backend +provides IPC event creation/import/export only for shared host pools. +Without HiSparse, sparse attention keeps its normal stream and event path. +ROCm does not currently provide a HiSparse backend. ## Main classes @@ -260,6 +267,5 @@ the same command, output, and cache-resolution boundaries. The command/result and attention-layer boundaries can be shared. The host allocator, copy implementation, hot layout, and replacement policy should stay platform-specific. NVIDIA uses the current accelerator LRU and fused host/hot -kernel. ROCm is not currently supported; AMD or other accelerator backends can -implement their own worker without forcing NVIDIA's policy into the shared -boundary. +kernel. Other accelerators can implement `HiSparseBackend` and return its class +path from their platform override without changing the shared control flow. diff --git a/docs/design/hybrid_kv_cache_manager.md b/docs/design/hybrid_kv_cache_manager.md index 3984000c7d8f..50e87cbdd2da 100644 --- a/docs/design/hybrid_kv_cache_manager.md +++ b/docs/design/hybrid_kv_cache_manager.md @@ -131,13 +131,17 @@ Some architectures (e.g., Bamba, Jamba, Minimax) interleave standard attention l The current algorithm is: 1. Increase the `block_size` of attention layers until + $$ \text{block_size} \times \text{kv_hidden_size}_{\text{att}} \ge \text{state_size}_{\text{mamba}} $$ + 2. Pad the mamba state per layer to + $$ \text{block_size} \times \text{kv_hidden_size}_{\text{att}} $$ + 3. Apply the grouping strategy in case 3. !!! note diff --git a/docs/design/metrics.md b/docs/design/metrics.md index 5d9fa42c11c3..b063939ed142 100644 --- a/docs/design/metrics.md +++ b/docs/design/metrics.md @@ -597,7 +597,7 @@ A common use case for our metrics is to support automated scaling of vLLM instances. For related discussion from the -[Kubernetes Serving Working Group](https://github.com/kubernetes/community/tree/master/wg-serving), +[Kubernetes Serving Working Group](https://github.com/kubernetes/community/tree/main/archive/wg-serving), see: - [Standardizing Large Model Server Metrics in Kubernetes](https://docs.google.com/document/d/1SpSp1E6moa4HSrJnS4x3NpLuj88sMXr2tbofKlzTZpk) diff --git a/docs/design/mm_processing.md b/docs/design/mm_processing.md index 20226171a912..530e1f903284 100644 --- a/docs/design/mm_processing.md +++ b/docs/design/mm_processing.md @@ -90,6 +90,9 @@ GPU-side fusion is controlled by `multimodal_config.mm_device_do_normalize`: | `llama-nemotron-vl-embed` | `LlamaNemotronVLForEmbedding` | `nvidia/llama-nemotron-embed-vl-1b-v2` | | `llama-nemotron-vl-rerank` | `LlamaNemotronVLForSequenceClassification` | `nvidia/llama-nemotron-rerank-vl-1b-v2` | | `mistral3` | `Mistral3ForConditionalGeneration` | `mistralai/Mistral-Small-3.2-24B-Instruct-2506`, etc. | +| `nano-nemotron-vl` | `NemotronH_Nano_VL_V2` | `nvidia/NVIDIA-Nemotron-Nano-12B-v2-VL-BF16` | +| `nano-nemotron-omni` | `NemotronH_Nano_Omni_Reasoning_V3` | `nvidia/Nemotron-3-Nano-Omni-30B-A3B-Reasoning-NVFP4` | +| `nemotron-omni` | `NemotronH_Omni_Reasoning_V3` | `nvidia/NVIDIA-Nemotron-3.5-Super-VL-120B-A12B-NVFP4` | | `nemotron-parse` | `NemotronParseForConditionalGeneration` | `nvidia/NVIDIA-Nemotron-Parse-2.0`, etc. | | `qwen2-vl` | `Qwen2VLForConditionalGeneration` | `Qwen/Qwen2-VL-2B-Instruct`, etc. | | `qwen2.5-vl` | `Qwen2_5_VLForConditionalGeneration` | `Qwen/Qwen2.5-VL-3B-Instruct`, etc. | diff --git a/docs/design/nixl_kv_push_connector.md b/docs/design/nixl_kv_push_connector.md index 621b0f7c9dd7..afdf84a9bd02 100644 --- a/docs/design/nixl_kv_push_connector.md +++ b/docs/design/nixl_kv_push_connector.md @@ -6,6 +6,17 @@ prefill completes. `NixlPushConnector` adds a **push-based** alternative in which P writes the KV blocks directly into D's pre-allocated memory via `NIXL WRITE`. +Enable push with `"kv_connector": "NixlPushConnector"`. Alternatively, use +`"kv_connector": "NixlConnector"` and set +`"kv_connector_extra_config": {"transfer_mode": "push"}`. + +For the built-in `NixlConnector`, `transfer_mode` defaults to `"pull"` and accepts +only `"pull"` or `"push"`; other values raise `ValueError`. +Explicit `NixlPullConnector` and `NixlPushConnector` names ignore this option. +`kv_connector_module_path` takes precedence over built-in selection. +Under `MultiConnector`, set `transfer_mode` in the NIXL child's +`kv_connector_extra_config`. + This document describes the threading, queues, and scheduling interactions specific to the push design. The pull-mode design is unchanged; the push connector reuses the same handshake, NIXL agent @@ -183,31 +194,40 @@ accounting code is unchanged. ## Pipeline parallelism and hybrid KV caches -The pull connector requires the local and remote workers to expose a -congruent list of KV regions — region *i* here corresponds to region -*i* there. That assumption breaks under pipeline parallelism (PP) -combined with a hybrid (HMA) KV layout: +A peer's KV regions do not line up with the local ones by index under +pipeline parallelism (PP) or a hybrid (HMA) KV layout: * a PP-sharded prefiller (**P**) holds only a slice of the model's layers while the `PP=1` decoder (**D**) holds them all, so region counts differ; -* with HMA, several layer names are pooled into one region and the layer - that represents a pooled region can differ between P and D. - -`NixlPushConnector` handles this for PP-sharded producers by routing -**by layer name** instead of by region index. Each -worker advertises which layer names back each of its NIXL regions in the -handshake metadata (`NixlAgentMetadata.region_members`). A producer that -needs layer routing derives its transfer layer order once, when it -registers its KV caches, and every transfer it issues uses that order. -`add_remote_agent` then selects exactly the remote regions this stage -owns and reorders them to match, so both sides stay paired regardless of -how each remote rank happens to order its metadata. - -Addresses, block lengths, strides, and per-region capacities follow the same -layer order. Descriptor offsets use each layer's region capacity, so P and -D need not allocate the same number of blocks. Physical allocations are still -registered once, even when multiple layers share them. +* with HMA, layers of several cache groups can share one region, and + which layers share a region can differ between P and D. + +Both connectors therefore pair regions **by layer**, for every peer. Each +worker advertises which layers back each of its NIXL regions in the +handshake metadata (`NixlAgentMetadata.region_members`). On the +handshake, `_pair_regions` looks each layer up on both sides and records +one pair (remote region, local region, cache group) per layer, +deduplicated; a layer registered as two regions (K and V apart) pairs +them in order. The remote descriptor list keeps only the remote regions +that pair, and a transfer reads or writes each pair at its cache group's +blocks. A region shared by several groups thus transfers once per group +it holds a layer of, from one descriptor list. + +```text +D (PP=1): region 0 = [L0, L0.swa] region 1 = [L1] region 2 = [L2] +P stage 1: region 0 = [L2] + +pairs (remote, local, group) on P: (2, 0, 0) -> D's region 2 only +``` + +Region capacities stay per region on both sides, so P and D need not +allocate the same number of blocks. Physical allocations are still +registered once, even when multiple layers share them. Mamba/SSM hybrids +pair the same way: each SSM region's conv and SSM-state descriptors are +built from the peer region its layers pair with, so P and D may group the +same layers into regions differently. A layer in different cache groups on +P and D is refused at the handshake for these models. Packed MLA caches (block-outermost layouts such as DeepSeek-V4's) store the pages of every layer for one block in a single row. A PP stage holds only its @@ -219,24 +239,27 @@ P (stage 2), block b: | L3 | L4 | L5 | ``` `PP=1` peers transfer whole rows. They also advertise where each layer's page -sits in the row (`NixlAgentMetadata.packed_member_layouts`), so a PP producer -writes each of its pages to the matching slot in D's row. Page sizes must -match; offsets, row strides, and block counts may differ. Because PP stages can -list the same attention backends in a different order, packed push compares the -sorted set of backend names during the handshake. Pull mode is unchanged. - -Invariants enforced when the remote regions are aligned: - -* every locally owned layer must be advertised exactly once by the - remote; a missing layer fails the handshake rather than silently - leaving that layer's KV stale, and remote-only layers (owned by other - PP stages) are ignored; -* a remote that omits layer metadata while the local layout requires - layer routing fails the handshake instead of falling back to - region-index routing; -* layer order is a property of the local layout alone, so the same local - source descriptors serve every remote engine and TP rank; only the - remote descriptor list is rebuilt per rank. +sits in the row (`NixlAgentMetadata.packed_member_layouts`). A PP producer +registers one region per layer, so it views each of D's rows as one region per +layer and pairs its pages with those. Page sizes must match; offsets, row +strides, and block counts may differ. Because PP stages can list the same +attention backends in a different order, packed push compares the sorted set of +backend names during the handshake. + +Invariants enforced when pairing: + +* the remote must hold every local layer; a missing layer fails the + handshake rather than silently leaving that layer's KV stale. Layers + only the remote holds are skipped on a PP stage (other stages own + them) and fail the handshake otherwise; +* a remote that does not advertise its layers fails the handshake + instead of falling back to pairing by index; +* a layer must span the same number of regions on both sides; +* every TP rank of one remote engine must pair the same way; +* a layer in different cache groups on P and D is refused by push. Each + worker advertises every layer's cache group + (`NixlAgentMetadata.layer_group_ids`), so this is caught even when the + layer's region is shared by several groups. Decode-side PP is unsupported because completions are counted per consumer rank. Mamba/SSM hybrids are unsupported under PP. Packed PP push diff --git a/docs/features/README.md b/docs/features/README.md index 1df8960ee354..969d5f33a0d9 100644 --- a/docs/features/README.md +++ b/docs/features/README.md @@ -1,5 +1,9 @@ # Features +## KV Cache Offloading + +- [UMBPStoreConnector](umbp_store_connector_usage.md) + ## Compatibility Matrix The tables below show mutually exclusive features and the support on some hardware. @@ -78,4 +82,4 @@ th:not(:first-child) { | beam-search | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | !!! note - For information on feature support on Google TPU, please refer to the [TPU-Inference Recommended Models and Features](https://docs.vllm.ai/projects/tpu/en/latest/recommended_models_features/) documentation. + For information on feature support on Google TPU, please refer to the [TPU-Inference Feature Matrix](https://docs.vllm.ai/projects/tpu/en/latest/features/) documentation. diff --git a/docs/features/custom_logitsprocs.md b/docs/features/custom_logitsprocs.md index 8c1c25939928..53bb258d6078 100644 --- a/docs/features/custom_logitsprocs.md +++ b/docs/features/custom_logitsprocs.md @@ -33,7 +33,7 @@ Custom logits processors must subclass `vllm.v1.worker.gpu.sample.logits_process * Return whether this processor modifies logits for the request. The sampler skips the logits-processing pipeline for batches in which no request needs it * `apply_staged_writes(self) -> None` (optional): - * Flush host-side writes staged by `add_request()` to the device; called once per step before the forward pass + * Flush host-side writes staged by `add_request()` to the device; called before the forward pass of every step that admits new requests, so only state set in `add_request()` is flushed. Read data that changes every step from `ctx` in `apply()` * `validate_params(cls, sampling_params) -> None` (optional classmethod): * Raise `ValueError` for invalid per-request arguments (especially custom arguments); runs at request admission, so invalid arguments fail the request instead of reaching the sampler @@ -70,7 +70,7 @@ The contrived example below implements a custom logits processor which masks out def __init__(self, vllm_config: "VllmConfig", req_states: LogitsProcRequestState): # Per-slot target; -1 means disabled. Staged on the host and - # flushed to the device once per step in apply_staged_writes(). + # flushed to the device in apply_staged_writes(). self.target_token = torch.full( (req_states.max_num_reqs,), -1, dtype=torch.int64 ) @@ -264,6 +264,6 @@ The examples below show how a user would pass a custom argument (`target_token`) 2. **The conditions under which the logits processor is or is not enabled on a per-request basis.** Unless your intention is for the custom logits processor to act on all requests all the time, you should write your logits processor in such a way that it is possible to disable the logits processor for a given request, i.e. by defaulting an argument to `None` or by passing in a specific do-nothing argument value i.e. `0.0`. Return `False` from `add_request()` for such requests so that the sampler can skip the logits-processing pipeline for batches in which no request needs it -* Stage host-side writes in `add_request()` and flush them once per step in `apply_staged_writes()`, instead of writing device tensors directly; this is the discipline every built-in sampler state follows +* Stage host-side writes in `add_request()` and flush them in `apply_staged_writes()`, instead of writing device tensors directly; this is the discipline every built-in sampler state follows * Since there is no removal hook, `add_request()` must fully (re)initialize the slot's state; do not rely on state left behind by the slot's previous occupant diff --git a/docs/features/ec_cpu_connector.md b/docs/features/ec_cpu_connector.md index 18274b6dd970..b895d8dcbb0c 100644 --- a/docs/features/ec_cpu_connector.md +++ b/docs/features/ec_cpu_connector.md @@ -71,7 +71,7 @@ vllm serve --ec-transfer-config '{ ### Protocol -- **Control plane**: ZMQ. The producer binds a `ROUTER` socket on `VLLM_EC_SIDE_CHANNEL_HOST:VLLM_EC_SIDE_CHANNEL_PORT`; each consumer opens one `DEALER` connection per producer peer, with ZMQ heartbeating (2s interval, 4s timeout, 8s TTL) to detect a dead peer. `XferReq`/`XferAck` are `msgspec` msgpack structs, versioned by `EC_CONNECTOR_VERSION` (currently `1`) — a version mismatch is NACKed. +- **Control plane**: ZMQ. By default, each producer binds a `ROUTER` socket on the local IP returned by vLLM's `get_ip()` with an OS-assigned port and advertises the bound port through `ec_transfer_params.peer_port`. Optional host and port overrides are listed below. Each consumer opens one `DEALER` connection per producer peer, with ZMQ heartbeating (2s interval, 4s timeout, 8s TTL) to detect a dead peer. `XferReq`/`XferAck` are `msgspec` msgpack structs, versioned by `EC_CONNECTOR_VERSION` (currently `1`) — a version mismatch is NACKed. - **Compatibility check**: every `XferReq` carries a SHA-256 hash over `(vllm_version, model, dtype, block_size_bytes)`; the producer NACKs (`NACK_INCOMPAT`) any peer whose hash differs. - **Ack statuses**: `OK`, `NACK_MISSING` (the producer no longer holds the encoding), `NACK_NOT_READY` (held, but its save has not landed), `NACK_INCOMPAT`, `NACK_VERSION`, `NACK_INTERNAL`. Only `NACK_NOT_READY` is retryable — `RETRYABLE_NACKS` in `cpu/protocol.py` is what both ends consult, so the classification lives with the wire vocabulary rather than at each call site. - **Data plane**: NIXL, `UCX` backend, consumer-initiated `READ` — the consumer pulls bytes directly out of the producer's registered mmap region; the producer never pushes. @@ -98,14 +98,14 @@ EC transfer is configured via `--ec-transfer-config` (CLI) or the `ec_transfer_c | `consumer_ack_timeout_s` | `float` | No (default `2.0`) | How long a consumer waits for an `XferAck` before giving up on a read. The producer answers `XferReq`s from its own scheduler step, so its reply latency scales with the encoder's `--max-num-batched-tokens`: a loaded encoder whose steps run longer than this makes consumers abandon reads the producer is about to grant. Raise it for large encoder batches. | | `ec_cpu_bytes` | `int` | Yes | Total size, in bytes, of the shared CPU mmap region. `ECCPUConnector` raises `ValueError` if unset. Block count = `ec_cpu_bytes // block_size_bytes`, where each block holds one LLM input-embedding vector. Entries allocate enough blocks for their modality-specific output shape, including visual DeepStack features. | -### Environment Variables +The control address uses vLLM's standard IP discovery. Set `VLLM_HOST_IP` to select a specific network interface when automatic discovery is unsuitable. -| Variable | Default | Description | +| Environment variable | Default | Description | | --- | --- | --- | -| `VLLM_EC_SIDE_CHANNEL_HOST` | `localhost` | Host the producer's ZMQ `ROUTER` socket binds to. Set to a routable address (e.g. the pod IP) for multi-instance/multi-node P2P — the default only works when producer and consumer share a host. | -| `VLLM_EC_SIDE_CHANNEL_PORT` | `5601` | Port for the same ZMQ `ROUTER` socket. | +| `VLLM_EC_SIDE_CHANNEL_HOST` | Unset | Override the EC control host instead of using `get_ip()`. Must be reachable by consumers; wildcard addresses are rejected. | +| `VLLM_EC_SIDE_CHANNEL_PORT` | `0` | `0` selects an OS-assigned port per producer. A nonzero value binds `base_port + data_parallel_index`; an occupied port fails startup. | -Both are read only when `ec_enable_nixl=True` on a producer (`ec_role="ec_producer"` or `"ec_both"`). +For fixed ports, assign non-overlapping ranges to independent instances sharing a host. Consumers use the advertised address and do not need matching port configuration. ## Limitations diff --git a/docs/features/engram.md b/docs/features/engram.md index 023477b34559..e3bba1ca0806 100644 --- a/docs/features/engram.md +++ b/docs/features/engram.md @@ -96,6 +96,41 @@ vllm serve deepseek-ai/DeepSeek-V4.1-Flash \ CPU offload requires a GPU with UVA support; vLLM fails fast when it is unavailable. +## Disk offload (Qwen4Exp) + +`disk_offload` keeps Qwen4Exp PLE tables in the checkpoint files: no table +is allocated in GPU or pinned host memory, and nothing is copied to an offload +directory. Each step reads that step's rows from the safetensors files on the +host. Use it where neither `cpu_offload` nor a resident table fits, for example +on unified-memory GPUs such as DGX Spark, where pinned host memory comes out of +the same pool as the weights and the KV cache: + +```bash +vllm serve --engram-config '{"disk_offload": true}' +``` + +It applies only to Qwen4Exp on CUDA and takes precedence over `cpu_offload`. + +Rows are read with `preadv` and copied to fixed device buffers before the +forward, so CUDA graphs replay unchanged. They stay in the checkpoint format +(BF16, FP8 or NVFP4) and are decoded on the GPU, so an unquantized table must +match `--dtype`. Rows that were read recently are served from the OS page +cache, which is shared by co-located ranks, not counted in the worker's RSS, +and reclaimed by the kernel under memory pressure. + +Costs and limits: + +- The read needs the step's token ids, so it waits for the previous step on + the GPU, and every TP rank reads all of its rows. +- A row that is not in the page cache is read from disk. For a fast first + request, read the PLE shard files once after the server has started. +- The checkpoint files must not change while the server runs. Truncation stops + the engine with an error naming the file; deleting or renaming is safe. +- Weight reload and elastic EP are unsupported. Loading fails when tensors are + not file-backed safetensors views (`.bin`, runai, `fastsafetensors`, + `instanttensor`, or `--safetensors-load-strategy eager`); + `--load-format dummy` serves zeros. + ## Data-parallel topologies (DeepSeek V4.1) By default (`embedding_across_dp: false`), each DP replica keeps its own @@ -115,7 +150,8 @@ vllm serve deepseek-ai/DeepSeek-V4.1-Flash \ ``` `embedding_across_dp` is not supported with elastic expert parallelism yet. -Qwen4Exp PLE tables are ETP-sharded instead and honor `cpu_offload` only. +Qwen4Exp PLE tables are ETP-sharded instead and honor only `cpu_offload` and +`disk_offload`. ## Sharing host tables across DP replicas diff --git a/docs/features/kv_offloading_usage.md b/docs/features/kv_offloading_usage.md index 7450e0dacc13..6a938dfc0cd8 100644 --- a/docs/features/kv_offloading_usage.md +++ b/docs/features/kv_offloading_usage.md @@ -95,6 +95,8 @@ vllm serve \ }' ``` +Secondary tiers are not supported yet on engines whose workers span several nodes; such an engine fails at startup. + ## `kv_connector_extra_config` Reference | Key | Required | Default | Scope | Notes | @@ -111,7 +113,7 @@ vllm serve \ | `tier_poll_interval_s` | no | `0.001` | multi-tier | Pause between tier-poll rounds for tiers that must be serviced between engine steps (currently `p2p`). See [Threading model](#threading-model). Set to `0` to disable the thread and service those tiers once per engine step instead. | | `offload_prompt_only` | no | `true` | both | If `true`, only prompt (prefill) blocks are offloaded; decode blocks are skipped. | | `sync_load_budget_bytes` | no | `0` | both | Per-step budget of KV bytes (per GPU) loaded within the same step, so those requests run immediately instead of waiting a step for an async load. Hits that don't fit the remaining budget, or would leave nothing to compute, stay async. `0` disables. | -| `self_describing_kv_events` | no | `false` | both | Opt-in. When `true` *and* KV cache events are enabled (`--kv-events-config` with `enable_kv_cache_events`), the connector emits self-describing block-granular `BlockStored`/`BlockRemoved` payloads (constituent block hashes, whole-chunk `token_ids`, per-block `block_size`, parent hash, LoRA + group/cache-spec metadata) instead of the placeholder fallback, so external KV-event consumers can index offloaded blocks. Inert unless events are enabled. With `TieringOffloadingSpec`, a CPU promotion is self-describing when a local request observes its primary-tier `HIT` before event translation; otherwise its stored event may retain the placeholder, while a later `HIT` can backfill metadata for removal. Pending-removal/re-promotion races and externally initiated promotions may also produce placeholders, and consumers must ignore removals for unknown hashes. Partial recurrent tails emit the hash-aligned portion from the physical block start through the tail boundary. Other sliding-window/SSM chunks keep the placeholder fallback. In chunk mode (`block_size` > GPU block size, or `blocks_per_chunk` > 1), overlapping chunks re-announce shared per-block hashes, so consumers must reference-count (deduplicate) repeated store/remove announcements. | +| `self_describing_kv_events` | no | `false` | both | Opt-in. When `true` *and* KV cache events are enabled (`--kv-events-config` with `enable_kv_cache_events`), the connector emits self-describing block-granular `BlockStored`/`BlockRemoved` payloads (constituent block hashes, whole-chunk `token_ids`, per-block `block_size`, parent hash, LoRA + group/cache-spec metadata) instead of the placeholder fallback, so external KV-event consumers can index offloaded blocks. Inert unless events are enabled. With `TieringOffloadingSpec`, a CPU promotion is self-describing when a local request observes its primary-tier `HIT` before event translation; otherwise its stored event may retain the placeholder, while a later `HIT` can backfill metadata for removal. Pending-removal/re-promotion races and externally initiated promotions may also produce placeholders, and consumers must ignore removals for unknown hashes. Recurrent (Mamba / linear-attention) state groups announce state checkpoints, with `kv_cache_spec_kind` `mamba`: an offloaded chunk or partial tail holds one recurrent state, valid at its last hash, so its `BlockStored` carries that single hash, its parent hash and an empty `token_ids`. Consumers align it by hash with the attention group's blocks; a hybrid prefix is reusable only up to a boundary where every group has a residency. Sliding-window and chunked-local attention chunks keep the placeholder fallback. In chunk mode (`block_size` > GPU block size, or `blocks_per_chunk` > 1), overlapping chunks re-announce shared per-block hashes, so consumers must reference-count (deduplicate) repeated store/remove announcements. | | `spec_module_path` | no | — | both | Python import path for a custom `OffloadingSpec` not in the built-in registry. Required only when `spec_name` is not built-in (advanced). | | `canonical_layout` | no | `false` | both | Store each offloaded block in a common canonical representation instead of each worker's local GPU page (enables sharing across TP sizes and single-copy storage of replicated KV). See [Canonical Layout](#canonical-layout). | @@ -193,6 +195,11 @@ context provides each key's token position through `get_offload_key_position(key so a policy can establish prefix ordering during insertion. Policies that do not use request-specific insertion ordering can accept and ignore `req_context`. +Custom policies must also implement `on_request_finished`, which receives the +request's keys per KV cache group in head-to-tail order and applies the request's +cache access once, when it finishes. `CachePolicy` no longer has a `touch` method; +recency updates belong in this hook. + `eviction_policy` is checked against the built-in registry first; if it isn't a registered name, vLLM imports `cache_policy_module_path` and looks up `eviction_policy` as a class name in that module — the same fallback `spec_module_path` provides for a custom `OffloadingSpec`. No import or registration call needs to run before the server starts. ### Registering a friendly short name (in-process only) @@ -224,9 +231,9 @@ Most tiers only ever see the scheduler thread, which holds the lock for the leng Two consequences worth knowing: - Any tier can be reached from that thread indirectly, because serving a peer looks keys up through the tiering manager, which fans out to the other tiers. Tier state must therefore not assume a particular thread, only that the manager lock is held. -- Work the thread deliberately leaves alone is batched per step: a promotion it initiates is submitted at the next `on_schedule_end()`, not immediately. +- Work the thread deliberately leaves alone is batched per step: a promotion it initiates is submitted at the next `on_schedule_end()`, not immediately. Because only an engine step submits that promotion and collects its completion, a multi-tier setup that includes an opted-in tier (for example `p2p` alongside `fs`) keeps the engine stepping even when idle, so pause and graceful shutdown wait there too. A setup whose only secondary tier is the opted-in one still goes idle, since a peer's lookup cannot promote out of it. Work the thread can finish by itself, such as serving a peer's lookup and fetch, does not count as pending, so a producer that peers keep fetching from still goes idle. Pause does not stop the P2P server either: it stops scheduling and quiesces the device, and a paused engine keeps serving peers from its CPU tier. -Setting `tier_poll_interval_s` to `0` disables the thread; opted-in tiers then get serviced once per engine step, as they were before it existed. +Setting `tier_poll_interval_s` to `0` disables the thread; opted-in tiers then get serviced once per engine step, as they were before it existed. With the thread disabled the engine keeps stepping for such a tier's lifetime, so an idle engine does not report itself idle and pause and graceful shutdown do not complete on their own. A round that keeps failing does not stop the thread: after 30 s of consecutive failures it backs off to a slower interval — the larger of 1 s and `tier_poll_interval_s` — and keeps retrying, so peers are still served while the engine is idle. Failing rounds log at most one message every 30 s: a full traceback for the first failure of a run, a one-line summary for the rest. ### Filesystem (FS) diff --git a/docs/features/mooncake_connector_usage.md b/docs/features/mooncake_connector_usage.md index 42c1dcd676ce..e0df25436ca2 100644 --- a/docs/features/mooncake_connector_usage.md +++ b/docs/features/mooncake_connector_usage.md @@ -66,6 +66,7 @@ cache), see [ECMooncakeConnector Usage Guide](mooncake_ec_connector_usage.md). - `VLLM_MOONCAKE_ABORT_REQUEST_TIMEOUT`: Timeout (in seconds) for automatically releasing the prefiller’s KV cache for a particular request. (Optional) - Default: 480 - If a request is aborted and the decoder has not yet notified the prefiller, the prefill instance will release its KV-cache blocks after this timeout to avoid holding them indefinitely. + - When `kv_lease_duration` is set, the lease decides when those blocks are released instead. This timeout still bounds how long a pull waits for a prefill that has not finished. ## KV Transfer Config @@ -80,6 +81,10 @@ cache), see [ECMooncakeConnector Usage Guide](mooncake_ec_connector_usage.md). - **num_workers**: Size of thread pool for one prefiller worker to transfer KV caches by mooncake. (default 10) - **mooncake_protocol**: Mooncake connector protocol. (default "rdma") - **device_name**: Comma-separated whitelist of RDMA devices (e.g. `"mlx5_0,mlx5_1"`) to restrict topology discovery to. Empty discovers every device. Useful on hosts exposing a mix of InfiniBand and RoCE ports, where both peers must settle on the same link layer. +- **kv_lease_duration**: Seconds the prefiller holds a finished request's KV blocks before the decoder must renew them. (default unset) + - Unset keeps the fixed `VLLM_MOONCAKE_ABORT_REQUEST_TIMEOUT`. + - When set, the decoder renews the lease every `kv_lease_duration / 6` seconds from the time the request arrives until its pull settles, and each renewal extends it to at least `kv_lease_duration * 2 / 3` seconds from then, as in [NixlConnector](../design/nixl_kv_cache_lease.md). Blocks of a decoder that crashed are freed within about `kv_lease_duration` seconds, while requests queued on a busy decoder keep their blocks. + - Set the same value on prefill and decode instances. A decoder without it does not renew, so its requests that wait longer than the lease lose their prefilled KV; a prefiller without it rejects renewals, and the decoder logs a warning. Prefill instances from releases without this option log an error for each renewal they receive, so upgrade them before enabling it on decode instances. - **bootstrap_server_address**: Optional `"host:port"` of an already running Mooncake bootstrap server. All prefiller ranks register with this endpoint instead of launching an embedded server. The caller owns the server lifetime and must configure the proxy to use the same endpoint. The example proxy assumes the bootstrap server is on the prefiller API host; a different host requires a proxy that supplies that address in `remote_bootstrap_addr`. Omit this option to use the embedded server and `VLLM_MOONCAKE_BOOTSTRAP_PORT`. ## Prometheus metrics @@ -102,8 +107,9 @@ The following Prometheus metrics are exported when MooncakeConnector is active, !!! tip High `vllm:mooncake_num_kv_expired_reqs` indicates that the prefiller's - lease duration is too short for your network or workload. Increase it via - `VLLM_MOONCAKE_ABORT_REQUEST_TIMEOUT`. + lease duration is too short for your network or workload. Increase + `VLLM_MOONCAKE_ABORT_REQUEST_TIMEOUT`, or set `kv_lease_duration` on both + instances so that queued requests keep their blocks. ## Example Scripts/Code diff --git a/docs/features/mooncake_store_connector_usage.md b/docs/features/mooncake_store_connector_usage.md index ac5f432e1bc7..a46642f335cc 100644 --- a/docs/features/mooncake_store_connector_usage.md +++ b/docs/features/mooncake_store_connector_usage.md @@ -177,13 +177,56 @@ remain unchanged. A decoder configured with `"save_decode_cache": true` uses the same Store TP for decode KV from every prefiller. The list may contain positive integer TP sizes. Sharing requires a Store TP that -is at least the local TP and divisible by it, an LBHNC or LBNHC local KV cache, -and the existing topology and KV-head constraints. The Store namespace includes -the attention backend's selected layout. Different layouts use separate Store -entries. Malformed lists and unsupported endpoints use an isolated rank-local -key layout. When -`enable_store_tp_lcm` is absent or false, `prefill_tp_sizes` has no effect and -the existing `store_tp_size` behavior is unchanged. +is at least the local TP and divisible by it. LBHNC, LBNHC, BLHNC, BLNHC, LHBNC, +and BHLNC local KV cache layouts are supported. The connector normalizes the +four head-major layouts to one Store value order and the two token-major +layouts to another. Physical layouts with the same value order can share +entries. + +In hybrid models, local Attention block sizes can vary with TP size. Shared +Store chunks have the same size on every endpoint; a local block can contain +several chunks. Hybrid sharing requires the same explicit `--prefix-match-unit` +on every endpoint, which also sets the default Attention Store chunk size. +A larger `store_chunk_size` can reduce Store object and metadata overhead: + +```json +{ + "kv_connector_extra_config": { + "store_tp_size": 8, + "store_chunk_size": 512 + } +} +``` + +The configured size applies to Attention Store objects. It must be a multiple +of `prefix_match_unit`, divide every local Attention page, and align with each +layer's stored states. Sharing endpoints must use the same Store chunk size. + +Mamba and GDN entries contain complete state snapshots. Both `DS` and `SD` +convolution state layouts are supported; sharing endpoints use the same layout. +Incremental offloads save intervals aligned across all participating cache +groups. Partial-tail checkpoints use the core's state-block handoff and matching +Attention KV. Store jobs hold their source blocks until all workers finish. + +Lookup reports a reusable prefix only when every required cache group is +present. For each Attention page, this includes every Store chunk covering its +reusable tokens, across all Store shards. + +Attention groups are planned from their per-layer cache specs, including specs +wrapped by `UniformTypeKVCacheSpecs`. The Store TP and global head-slot count +must divide one another; MQA and replicated GQA use the smaller head-slot count +as the physical Store shard count. `MambaSpec` state segments map in complete +state units; a replicated local unit can serve multiple Store shards. All +endpoints use the same PP size and Store TP; PCP, DCP, and cross-layer blocks +remain rank-local. Each group namespace records its Store value format and +normalized value schema. Incompatible configurations use an isolated rank-local +namespace. + +Cache groups marked `prefix_cacheable=False` are excluded from shared-layout +validation, Store transfers, and prefix lookup. Unsupported layouts in participating +groups use the rank-local fallback. + +When `enable_store_tp_lcm` is absent or false, `prefill_tp_sizes` has no effect. **Proxy:** @@ -285,7 +328,8 @@ Strict isolation requires a Mooncake master started with `--enable_multi_tenants incompatible with sleep mode and the CuMem allocator. - `cache_prefix` (str): Namespace prepended to every store key. Lets separate deployments share one Mooncake master without polluting each other — instances configured with different prefixes never see each other's cached blocks, even for identical prompts. All instances that should share a prefix cache must use the same value. Default: `""` (no prefix; keys are byte-identical to the unprefixed format). - `save_decode_cache` (bool): Enable offloading decode tokens' KV cache. A `kv_consumer` does not save during prefill; when decode starts, it fills any missing block-aligned prompt prefix before appending completed decode blocks. Default: `false`. -- `store_tp_size` (int): Common Store TP for endpoints with different local TP sizes. It supports LBHNC and LBNHC local KV caches, with `store_tp_size >= local_tp_size` and `store_tp_size % local_tp_size == 0`. The current topology is one full-attention cache group, PCP/DCP disabled, and cross-layer blocks disabled. For GQA and MHA, the total KV-head count must be divisible by `store_tp_size`. Store shards contain fixed global KV-head ranges in the local layout. Shared endpoints use the same KV cache layout, pipeline-parallel size, and Store TP. The Store namespace includes the layout and PP size. Unsupported configurations use a topology-specific rank-local namespace. +- `store_tp_size` (int): Common Store TP for divisible heterogeneous-TP + sharing. See [Sharing one Store across multiple Prefill TP sizes](#sharing-one-store-across-multiple-prefill-tp-sizes). LBHNC/HND is strongly recommended for TP-sharded Store when supported. LBNHC/NHD creates many transfer segments and may significantly reduce PUT/GET performance. @@ -294,13 +338,10 @@ For example, with prefill TP 4, decode TP 2, and eight KV heads, set `store_tp_size` to 4 on both instances. Each decode rank reads and writes two of the four Store shards. -MQA with one total KV head uses a replicated-head layout. For the supported -prefill TP 4 to decode TP 2 case, every rank uses the -same rank-0 key namespace. The four prefill replicas stripe block PUTs so each -object is stored once, while both decode ranks GET every block into their local -KV replica. `store_tp_size` does not appear in MQA keys, so identical MQA -objects written at different store TP sizes share the same pool entry when PP -sizes match. +MQA with one total KV head uses one physical Store shard. For prefill TP 4 and +decode TP 2, every rank uses the same shard key. Prefill replicas stripe block +PUTs so each object is stored once, while both decode ranks GET every block into +their local KV replica. Tensor-parallel collectives and low-precision arithmetic are not bitwise invariant across TP sizes, so heterogeneous-TP reuse does not guarantee the diff --git a/docs/features/moriio_connector_usage.md b/docs/features/moriio_connector_usage.md index 352022c7aa6b..d806e04cde8c 100644 --- a/docs/features/moriio_connector_usage.md +++ b/docs/features/moriio_connector_usage.md @@ -142,7 +142,7 @@ WRITE mode supports hybrid attention plus Mamba/KDA models, including DSpark. Us For DSpark, configure the same draft model on both instances through `--speculative-config`. Target and draft attention groups retain their own block IDs and token spans; only the running recurrent state is transferred, not speculative scratch slots. The producer transfers the state after the first N-1 prompt tokens, and the consumer recomputes the last prompt token. Convolution and SSM transfers must both finish before the layer is complete. -Hybrid READ keeps its existing single-transferable-attention-group requirement. Plain-attention WRITE retains its existing notification format. +Hybrid READ transfers every attention group, so drafters that share the target's attention groups (such as MTP) also work. Plain-attention WRITE retains its existing notification format. ### Kimi-K3 specialized decode diff --git a/docs/features/nixl_connector_compatibility.md b/docs/features/nixl_connector_compatibility.md index 05084e58966b..54462dd5b785 100644 --- a/docs/features/nixl_connector_compatibility.md +++ b/docs/features/nixl_connector_compatibility.md @@ -108,11 +108,10 @@ By default, a **compatibility hash** is checked during handshake. P and D instan The default (pull) NixlConnector does **not** support `pipeline-parallel-size > 1` together with a hybrid KV cache (HMA) -layout: region indices are not a stable identity across the -prefill/decode layer split, so the connector raises at startup. Use the -push connector (`NixlPushConnector`) for pipeline parallelism with -hybrid KV caches — it routes transfers by layer-name (member) identity, -so a PP-sharded prefiller can write into a `PP=1` decoder. See +layout and raises at startup. Use the push connector +(`NixlPushConnector`) for pipeline parallelism with hybrid KV caches. +Both connectors pair KV regions with a peer's by the layers each region +holds, so a PP-sharded prefiller can write into a `PP=1` decoder. See [NIXL push-mode KV transfer](../design/nixl_kv_push_connector.md). Current push PP + HMA limitations: @@ -120,7 +119,7 @@ Current push PP + HMA limitations: - Only the prefiller (producer) may be PP-sharded; decode-side PP is not supported. - Hybrid SSM/Mamba layouts are not supported under PP. - HMA requires the same block size on P and D. -- Non-MLA attention-HMA layer routing requires decode TP to be no greater than prefill TP. +- Non-MLA attention under HMA requires decode TP to be no greater than prefill TP. - Packed layouts under PP require MLA caches. Local and remote packed block strides may differ; layer pages must have equal sizes. MLA supports decode TP greater than prefill TP because its KV is replicated. ### Quantized KV cache diff --git a/docs/features/nixl_connector_usage.md b/docs/features/nixl_connector_usage.md index 4402095b2ec1..00f5860af4c2 100644 --- a/docs/features/nixl_connector_usage.md +++ b/docs/features/nixl_connector_usage.md @@ -63,6 +63,19 @@ vllm serve \ !!! note Backend availability depends on how NIXL was built and what plugins are present in your environment. Refer to the [NIXL repository](https://github.com/ai-dynamo/nixl) for available backends and build instructions. +#### UCX backend parameters + +`kv_connector_extra_config.ucx_backend_params` passes extra init parameters to NIXL's UCX backend. Values are strings. For example, NIXL creates UCX with `ucx_error_handling_mode=peer` by default; UCX then uses only transports that can detect peer failure, which can exclude intra-node GPU transports such as `rocm_ipc`. To trade that error handling for those transports: + +```bash +vllm serve \ + --kv-transfer-config '{ + "kv_connector":"NixlConnector", + "kv_role":"kv_both", + "kv_connector_extra_config":{"ucx_backend_params":{"ucx_error_handling_mode":"none"}} + }' +``` + ## Basic Usage (on the same host) ### Producer (Prefiller) Configuration diff --git a/docs/features/preload.md b/docs/features/preload.md index 5787fdb51c1c..92dab0418286 100644 --- a/docs/features/preload.md +++ b/docs/features/preload.md @@ -281,10 +281,18 @@ threat model. - Each GPU's socket path is guarded by an exclusive lock file, so a second daemon for the same GPU fails fast instead of clobbering the live socket. -- `vllm preload` reports readiness once every rank is serving; if any rank - dies during startup, the remaining ranks are terminated and the command - exits with that rank's exit code. +- `vllm preload` reports readiness once every rank is serving. If any rank + exits during startup or serving, the launcher terminates and reaps the + remaining ranks, stops its health endpoint, and exits with a nonzero status. + The failing rank's nonzero exit code is preserved. Signal failures use + `128 + signal number` (for example, 137 for `SIGKILL`); an unexpected clean + rank exit is reported as status 1. Restart the entire preload group before + attaching new engines. - `SIGINT`/`SIGTERM` to the `vllm preload` process terminates all daemon - ranks, which unlinks their sockets and releases the GPU memory. + ranks and exits with status 130/143. +- In either shutdown path, ranks that do not stop within ten seconds are + killed and reaped, releasing their GPU memory. A forcibly + terminated rank may leave a stale socket; the next daemon replaces it + after acquiring the per-GPU lock. See [vllm preload](../cli/preload.md) for the full CLI reference. diff --git a/docs/features/prompt_embeds.md b/docs/features/prompt_embeds.md index 14b4e56afa5c..4721294a16c9 100644 --- a/docs/features/prompt_embeds.md +++ b/docs/features/prompt_embeds.md @@ -59,6 +59,8 @@ Prompt embeddings can be included as content parts in chat messages, interleaved Each `prompt_embeds` content part contains a `data` field with a base64-encoded `torch.Tensor` of shape `(num_tokens, hidden_size)`. Multiple `prompt_embeds` parts can appear in any message, in any position relative to text parts. The server expands each part into the correct number of placeholder tokens during chat template rendering, then splices the pre-computed embeddings into the model's input at the corresponding positions. +For multimodal models, `prompt_embeds` parts can also be mixed with image content parts. Their placeholder token IDs are replaced with safe dummy IDs before model execution; the supplied embeddings replace the dummy embeddings at those positions. + Unlike the Completions API, a `prompt_embeds` content part should encode **only** the content, not a templated conversation. The server wraps the chat template around the embedded content at request time, the same way it would for a plain text `content` string. Embedding a full templated conversation here would double-apply the template and produce incorrect inputs to the model. Assistant messages can also pass a single `prompt_embeds` part as `reasoning` instead of a string: diff --git a/docs/features/quantization/README.md b/docs/features/quantization/README.md index ba1a5b86fe45..87c7afe9cde6 100644 --- a/docs/features/quantization/README.md +++ b/docs/features/quantization/README.md @@ -83,7 +83,7 @@ th:not(:first-child) { - *Turing does not support Marlin MXFP4. !!! note - For information on quantization support on Google TPU, please refer to the [TPU-Inference Recommended Models and Features](https://docs.vllm.ai/projects/tpu/en/latest/recommended_models_features/) documentation. + For information on quantization support on Google TPU, please refer to the [TPU-Inference Feature Matrix](https://docs.vllm.ai/projects/tpu/en/latest/features/#quantization-support) documentation. !!! note This compatibility chart is subject to change as vLLM continues to evolve and expand its support for different hardware platforms and quantization methods. diff --git a/docs/features/quantization/modelopt.md b/docs/features/quantization/modelopt.md index 6d59a23565f0..36440e19c38c 100644 --- a/docs/features/quantization/modelopt.md +++ b/docs/features/quantization/modelopt.md @@ -49,12 +49,23 @@ module prefix and decoder layer indices before constructing quantized layers. use `--linear-backend flashinfer_trtllm` to select FlashInfer's TensorRT-LLM GEMM backend. +For Muse Spark NVFP4 on NVIDIA, the prepared-input FlashInfer TRT-LLM path +skips CUDA-graph padding rows when routing runs outside the expert kernel. +The router writes expert IDs of `-1` and zero weights in its existing fused +kernel. This requires a padding mask matching the routed token layout; +other dispatch paths retain their backend-specific behavior. +Set `VLLM_MOE_SKIP_PADDING=0` before starting the engine to disable this +optimization for comparisons. + ## Benchmarking with dummy weights For performance experiments with random weights, use `--load-format dummy`. ModelOpt NVFP4 MoE layers initialize positive quantization scales with matching gate/up global scales, including for FlashInfer MegaMoE (`--moe-backend flashinfer_moe_ep_cutedsl`). +Muse Spark's NVIDIA `deep_gemm_mega_moe` backend also supports dummy loading: +it initializes valid NVFP4 scales and finalizes its expert weights and residual +constants before the first forward. Enable expert parallelism for this backend. Dummy weights are for benchmarking, not model quality evaluation. Normal checkpoint loading uses the checkpoint's scales. diff --git a/docs/features/reasoning_outputs.md b/docs/features/reasoning_outputs.md index 5e59644def9b..729325e29c36 100644 --- a/docs/features/reasoning_outputs.md +++ b/docs/features/reasoning_outputs.md @@ -308,6 +308,8 @@ curl http://localhost:8000/v1/chat/completions \ }' ``` +The Responses API (`/v1/responses`) accepts the same `thinking_token_budget` field. + ### Offline Inference ```python diff --git a/docs/features/sleep_mode.md b/docs/features/sleep_mode.md index bb6c6559228c..8c73dd16a624 100644 --- a/docs/features/sleep_mode.md +++ b/docs/features/sleep_mode.md @@ -242,3 +242,134 @@ curl -X POST 'http://localhost:8000/wake_up?tags=kv_cache' ## Limitation On ROCm, the virtual memory allocation on ROCm is done through chunked memory allocation. You can control the chunk size through `VLLM_ROCM_SLEEP_MEM_CHUNK_SIZE` (in MB). The default value is set at 256MB. The larger the chunk size the faster the performance. However, setting it too large will cause OOM. So if you encounter OOM when using sleep mode. Try reducing the chunk size. It is recommended to define the chunk size as a power of 2. + +## Experimental CUDA checkpoint executor + +This capability is experimental and disabled by default. Selecting the executor +below checkpoints each CUDA worker after ordinary level-1 sleep, +then restores all workers before waking allocator-managed memory. This preserves +the remaining CUDA state, including captured graph executable objects, without +recapturing graphs. Ordinary level-1 semantics still apply: KV cache contents are +discarded. This is not a durable application checkpoint or model switching API. + +Set `VLLM_USE_V2_MODEL_RUNNER=1` before starting the engine: + +```python +from vllm import LLM + +llm = LLM( + "Qwen/Qwen3-0.6B", + enable_sleep_mode=True, + distributed_executor_backend=( + "vllm.v1.executor.cuda_checkpoint_executor.CUDACheckpointExecutor" + ), +) +llm.sleep(level=1) +llm.wake_up() +``` + +This experimental implementation requires Linux, NVIDIA driver checkpoint APIs, +and dedicated worker processes. It uses an external helper to call +`cuCheckpointProcessLock`, `cuCheckpointProcessCheckpoint`, +`cuCheckpointProcessRestore`, and `cuCheckpointProcessUnlock`. CPU threads remain +alive, but worker CUDA calls cannot execute while checkpointed. The helper uses +the same process namespace and user as the executor. Driver permission and +resource restrictions still apply; see the +[CUDA checkpoint API documentation](https://docs.nvidia.com/cuda/archive/13.0.0/cuda-driver-api/group__CUDA__CHECKPOINT.html). + +The initial validation used NVIDIA H20 with driver 580.126.09, NCCL 2.29.7, +and FlashInfer 0.6.18.post1. See [PR #58183](https://github.com/vllm-project/vllm/pull/58183) +for versioned test results and communication performance comparisons. Support +for other GPU, driver, and communication-library combinations is not established. + +Only single-node tensor parallelism is accepted. Tensor parallel sizes 1, 2, +and 4 have been GPU-tested; other sizes remain unvalidated. Use +`VLLM_USE_V2_MODEL_RUNNER=1`, the standard GPU worker and the `cumem` sleep +backend. Pipeline, data, context and expert parallelism, external cache or +weight transfer, custom worker classes, and sleep level 2 are rejected before +worker startup (sleep level 2 is rejected when requested). Worker extensions +that allocate additional CUDA IPC resources are outside the validated scope. +Level 0 remains +a scheduler pause and does not perform a CUDA checkpoint. + +For tensor parallel sizes greater than 1, use NCCL 2.29.7 or later with memory +suspension support, set `enable_nccl_comm_suspend=True`, and pass +`disable_custom_all_reduce=True`. The following explicit configuration retains +NCCL GPU peer-to-peer or legacy shared-memory communication, selected by NCCL. +`NCCL_P2P_DISABLE` and `NCCL_SHM_DISABLE` need not be set when communicator +suspension is enabled. NCCL CUDA host-memory allocation remains disabled; it +is not equivalent to legacy shared-memory transport. Before driver checkpoint, +ordinary worker +sleep suspends NCCL mappings; worker wake restores them at their reserved +addresses. The executor checks each rank's actual suspension state. + +```bash +export NCCL_IB_DISABLE=1 +export NCCL_CUMEM_ENABLE=1 NCCL_CUMEM_HOST_ENABLE=0 NCCL_NVLS_ENABLE=0 +export VLLM_ALLREDUCE_USE_SYMM_MEM=0 VLLM_USE_NCCL_SYMM_MEM=0 +export VLLM_ALLREDUCE_USE_FLASHINFER_PCIE_IPC=0 +export VLLM_ALLREDUCE_USE_FLASHINFER=1 +export VLLM_FLASHINFER_ALLREDUCE_BACKEND=trtllm +``` + +FlashInfer must provide stable-address workspace `checkpoint_prepare` and +`checkpoint_restore` methods. The executor calls existing worker hooks around +the driver checkpoint. Disabling standalone FlashInfer all-reduce with +`VLLM_ALLREDUCE_USE_FLASHINFER=0` does not disable the compiler +`fuse_allreduce_rms` pass, which can allocate FlashInfer workspaces independently. +When that pass is enabled, the same backend restriction and workspace hooks +apply. A configuration using only NCCL must also disable that compiler pass. +The FlashInfer `mnnvl` backend is not accepted, and the NCCL suspend configuration +requires NVLS to be disabled. Live multicast +allocations can block the combined sleep/checkpoint flow even on newer +user-mode drivers. Keep these restrictions when upgrading the driver. The +helper must use the same user-mode driver as the workers; mixed versions are +unsupported. Performance costs of the restrictions must be measured against +the workload's default communication +configuration; retaining GPU peer-to-peer communication alone does not establish +unchanged inference performance. + +A more restrictive Socket-only configuration is also accepted without +`enable_nccl_comm_suspend`: set `NCCL_P2P_DISABLE=1`, `NCCL_SHM_DISABLE=1`, +`NCCL_CUMEM_ENABLE=0`, and +`VLLM_ALLREDUCE_USE_FLASHINFER=0`, retaining the other communication restrictions +above. This configuration is primarily useful for compatibility comparisons. +Do not create additional CUDA IPC resources or fall back to an independently +created PyTorch NCCL process group while using the checkpoint executor. + +Provide host memory for both the ordinary weight backup and the driver's +remaining CUDA state backup. Restore needs sufficient free GPU memory; release +any temporary GPU workload before waking. Checkpoint and restore add latency to +ordinary sleep/wake. The helper itself can retain a small driver allocation, so +worker offload does not imply a zero device-wide memory reading. + +All ranks are locked before any is checkpointed. All ranks are restored before +any is unlocked. Operations within each stage run concurrently across workers. +The whole stage has a 30-second response deadline (each lock also has a +10-second driver timeout). Worker sleep/wake and FlashInfer workspace +hooks have a 120-second response deadline. Failure terminates the workers rather than +serving with a partially restored rank set; restart the engine after failure. +Shutdown terminates the dedicated worker and helper processes without restoring +their CUDA state or requiring free GPU memory for the model. + +Repeated level-1 sleep is idempotent. A first partial wake, such as +`wake_up(tags=["weights"])`, restores the **whole CUDA process** before restoring +the selected allocator tags. It cannot selectively restore graph or driver +state. Ordinary worker RPCs are rejected while checkpointed. A checkpointed +worker cannot execute another model until restored. + +Serialize sleep, wake, pause, and resume calls across all clients, including +while level-1 sleep is draining requests. Wait for each call to complete before +issuing the next. A partial wake does not automatically resume scheduling; wake +all allocator tags before explicitly resuming or submitting new inference. +Do not use an explicit resume call to bypass sleeping memory. + +If EngineCore exits during a management call, the client may remain waiting even +after the workers terminate. Use an external timeout and restart the failed +engine; a timeout does not establish that the operation completed. Do not retry +control operations on an engine whose checkpoint outcome is unknown. + +As with the ordinary executor, `sleep()` is a no-op while weights remain asleep. +For example, after `wake_up(tags=["kv_cache"])`, another `sleep()` does not +checkpoint the process again. Wake the remaining weights before starting a new +full sleep/checkpoint cycle. diff --git a/docs/features/speculative_decoding/README.md b/docs/features/speculative_decoding/README.md index 568308e4ef67..cf7d442b6f9a 100644 --- a/docs/features/speculative_decoding/README.md +++ b/docs/features/speculative_decoding/README.md @@ -16,7 +16,7 @@ vLLM supports a variety of methods of speculative decoding. Model-based methods - [N-Gram](n_gram.md) - [Suffix Decoding](suffix.md) - [Hidden State Extraction](extract_hidden_states.md) -- [Custom Proposer Backend (Experimental)](#custom-proposer-backend-experimental) +- [Custom Speculators (Experimental)](#custom-speculators-experimental) - [Dynamic Speculative Decoding](dynamic_speculative_decoding.md) - [Adaptive Verification](adaptive_verification.md) - [FLy Verification (Lossy)](fly.md) @@ -43,10 +43,80 @@ For reproducible measurements in your environment, use [`examples/features/speculative_decoding/spec_decode_offline.py`](../../../examples/features/speculative_decoding/spec_decode_offline.py) or the [benchmark CLI guide](../../benchmarking/cli.md). -## Custom Proposer Backend (Experimental) +## Custom Speculators (Experimental) + +You can plug in your own speculator with the V2 model runner by registering it +as a speculative method from a [vLLM plugin](../../design/plugin_system.md), and then using it +with `"method": ""`. Custom speculative methods require the V2 model +runner. + +A custom speculator consists of: + +- The speculator itself: a subclass of + `vllm.v1.worker.gpu.spec_decode.speculator.BaseSpeculator`, typically of + `DraftModelSpeculator` (for a speculator that runs its own draft model, which + the model runner then loads and allocates draft KV cache for) or + `TargetDependentARSpeculator` (for EAGLE/MTP-style drafting from the target + model's hidden states). It's constructed with `(vllm_config, device)`. +- Its config: a subclass of + `vllm.v1.worker.gpu.spec_decode.speculator_config.SpeculatorConfig`, which + names the speculator class and describes how it behaves (KV lookahead slots, + whether it needs the target's hidden states, which features it supports, + ...). It's used in every vLLM process, including those that never build the + speculator, so its module shouldn't import the speculator. A speculator that + behaves like a built-in one should subclass that speculator's config (e.g. + `Eagle3SpeculatorConfig`), which also determines how its draft model is + configured from `model`. + +```python +# my_package/config.py +from vllm.v1.worker.gpu.spec_decode.eagle.config import Eagle3SpeculatorConfig + + +class MySpeculatorConfig(Eagle3SpeculatorConfig): + speculator_class = "my_package.speculator.MySpeculator" + # Accept options in `speculator_options` (validated in `apply_options`). + supported_options = frozenset({"speculator_options"}) + + @classmethod + def apply_options(cls, spec): + options = spec.speculator_options or {} + if unknown := options.keys() - {"my_option"}: + raise ValueError(f"Unknown speculator_options: {sorted(unknown)}") + + +# my_package/__init__.py, registered as a `vllm.general_plugins` entry point +def register(): + from vllm.v1.worker.gpu.spec_decode.speculator_config import ( + register_speculative_method, + ) + + register_speculative_method("my_spec", "my_package.config.MySpeculatorConfig") +``` + +```bash +vllm serve \ + --speculative-config '{ + "method": "my_spec", + "model": "", + "num_speculative_tokens": 3, + "speculator_options": {"my_option": 1} + }' +``` + +The speculator reads its options from +`vllm_config.speculative_config.speculator_options`. Once set up, +`speculative_config.custom_method` is the registered name (`"my_spec"`), and +`speculative_config.method` is the built-in method its config is based on +(`"eagle3"`), so vLLM treats it like that built-in speculator. + +### V1 Custom Proposer Backend -You can plug in your own custom proposer class for speculative decoding by setting the method to `custom_class` and providing the full module path to your class. -Your custom class must accept a `VllmConfig` upon instantiation and implement a `propose` method. +With the V1 model runner, you can plug in your own custom proposer class by +setting the method to `custom_class` and providing the full module path to your +class. Your custom class must accept a `VllmConfig` upon instantiation and +implement a `propose` method. This is not supported by the V2 model runner, and +will be removed together with the V1 model runner. **Example configuration:** diff --git a/docs/features/umbp_store_connector_usage.md b/docs/features/umbp_store_connector_usage.md new file mode 100644 index 000000000000..fa6e1af69ba9 --- /dev/null +++ b/docs/features/umbp_store_connector_usage.md @@ -0,0 +1,75 @@ +# UMBPStoreConnector Usage Guide + +`UMBPStoreConnector` offloads KV cache blocks to host DRAM managed by UMBP, +the unified memory buffer pool of [MORI](https://github.com/ROCm/mori). It +restores them when a later request shares a prefix that has left GPU memory. + +## Requirements + +- MORI built with UMBP (`BUILD_UMBP=ON`), importable as `mori`. +- Prefix caching enabled. + +## Embedded mode + +Embedded mode is the default. Each vLLM worker owns a private DRAM pool in its +own process. The engine that stored a prefix can restore it; independent +engines do not share these pools. + +```bash +vllm serve \ + --enable-prefix-caching \ + --kv-transfer-config '{ + "kv_connector": "UMBPStoreConnector", + "kv_role": "kv_both", + "kv_load_failure_policy": "recompute", + "kv_connector_extra_config": { + "mode": "embedded", + "total_capacity_bytes": 68719476736 + } + }' +``` + +### Embedded options + +Set these options in `kv_connector_extra_config`: + +| Option | Default | Meaning | +| --- | --- | --- | +| `capacity_bytes` | 64 GiB | Pool size per worker. | +| `total_capacity_bytes` | | Pool size per engine, split evenly across its workers. Exclusive with `capacity_bytes`. | +| `lookup_instance` | | Distinguishes independent engines serving the same model on one host. Data-parallel ranks are already distinguished. | +| `lookup_dir` | `/tmp` | Directory of the per-worker lookup sockets. | +| `dram_high_watermark`, `dram_low_watermark` | MORI defaults | Pool occupancy at which eviction starts and stops. | +| `dram_use_hugepages`, `dram_hugepage_size`, `dram_numa_node`, `dram_prefault` | MORI defaults | Allocation of each worker's pool. | + +## Shared options + +Set these options in `kv_connector_extra_config`: + +| Option | Default | Meaning | +| --- | --- | --- | +| `load_async` | `true` | Load in the background while the request waits. Models with Mamba-style or sparse-attention indexer layers always load asynchronously, since those layers read restored state before a synchronous load could have landed. | +| `lookup_async` | `false` | Look up the pool off the scheduler thread; the request waits until the lookup completes. | +| `kv_event_max_objects` | `65536` | Maximum objects tracked for KV events. When full, new stored-object notifications are suppressed until tracked objects leave the pool. Storage and lookup continue normally. | +| `enable_lookup` | `true` | Restore from the pool. With `false` the connector only stores. | +| `save_decode_cache` | `false` | Also store blocks produced by decoding. | +| `enable_partial_hash_hits` | `false` | Allow hits that end inside a block, at core's prefix-match unit (`cache_config.prefix_match_unit`). | +| `num_workers`, `timeout_ms` | `4`, `30000` | Transfer threads per worker, and the transfer timeout. A synchronous wait that times out fails the step rather than reusing its blocks. | +| `key_namespace` | derived | Overrides the namespace derived from the model, revision, KV layout, cache groups, parallel topology and draft model. | + +Use `kv_load_failure_policy: "recompute"` in `kv_transfer_config`, as in the +example above. An object can be evicted between lookup and load; the default +`fail` policy fails the request instead of recomputing it. + +With `kv_role: "kv_consumer"` the connector only restores and never stores. + +## Limitations + +- Pipeline parallelism is rejected. +- Stores are submitted once per step after the forward pass. + +With KV events enabled, the scheduler verifies residency on every rank before +publishing a stored-object event. It periodically rechecks tracked objects +during engine steps to report removals. These checks do not delay the release +of GPU source blocks after a completed transfer. With events disabled, no +historical object identities are retained for event reporting. diff --git a/docs/getting_started/installation/gpu.cuda.inc.md b/docs/getting_started/installation/gpu.cuda.inc.md index 19a2432442fe..2166e1a856d2 100644 --- a/docs/getting_started/installation/gpu.cuda.inc.md +++ b/docs/getting_started/installation/gpu.cuda.inc.md @@ -458,11 +458,6 @@ BuildKit does not automatically invalidate cached layers when a mutable Git ref changes. Use `--no-cache-filter extensions-build` to refresh an empty, branch, or tag revision. -Set `BUILD_NIXL=true` to build NIXL from source. NIXL's release wheels do not -include the NIXL EP extension for the PyTorch nightly used by the Rubin build. -The build uses the NIXL version pinned in `requirements/kv_connectors.txt` and -replaces the NIXL packages installed from the KV-connector requirements. - For `FINAL_BASE_IMAGE`, use the public, multi-arch `nvidia/cuda:13.4.1-base-ubuntu24.04` image. Set `NCCL_VERSION` to 2.32.3 or newer, the first NCCL release with Rubin (SM107) support. diff --git a/docs/getting_started/installation/gpu.rocm.inc.md b/docs/getting_started/installation/gpu.rocm.inc.md index d8debbab3ea3..69274c8ac350 100644 --- a/docs/getting_started/installation/gpu.rocm.inc.md +++ b/docs/getting_started/installation/gpu.rocm.inc.md @@ -1,7 +1,7 @@ --8<-- [start:installation] -vLLM supports AMD GPUs with ROCm 6.3 or above. Pre-built wheels are available for ROCm 10.0 (the default) and ROCm 7.2. +vLLM supports AMD GPUs with ROCm 6.3 or above. Pre-built wheels are available for ROCm 10.0; ROCm 7.2 wheels remain available for earlier releases. #### Prebuilt Wheels @@ -9,7 +9,7 @@ vLLM supports AMD GPUs with ROCm 6.3 or above. Pre-built wheels are available fo | ------------ | -------------- | ------------ | ----------------- | ------------------ | | `rocm700` | 3.12 | 7.0 | >= 2.35 | `0.14.0` to `0.18.0` | | `rocm721` | 3.12 | 7.2.1 | >= 2.35 | Nightly releases after commit `171775f306a333a9cf105bfd533bf3e113d401d9` | -| `rocm72` | 3.12 | 7.2 | >= 2.39 | Nightly releases on the legacy ROCm 7.2 stack | +| `rocm72` | 3.12 | 7.2 | >= 2.39 | Earlier nightly releases on the legacy ROCm 7.2 stack (no longer built) | | `rocm100` | 3.12 | 10.0 | >= 2.35 | Nightly releases (default) | --8<-- [end:installation] @@ -51,7 +51,7 @@ uv pip install vllm --extra-index-url https://wheels.vllm.ai/rocm/ --upgrade !!! tip The ROCm 10.0 wheels install the ROCm SDK from pip ([TheRock](https://github.com/ROCm/TheRock)), including the GPU kernels for every supported architecture. - Wheels for the legacy ROCm 7.2 stack, which use a system ROCm installation, remain available at `https://wheels.vllm.ai/rocm/${VLLM_VERSION}/rocm72`. + Wheels for the legacy ROCm 7.2 stack, which use a system ROCm installation, remain available for earlier releases at `https://wheels.vllm.ai/rocm/${VLLM_VERSION}/rocm72`. !!! tip You can find out about which ROCm version the latest vLLM supports by checking the `vllm` package in index in extra-index-url at [https://wheels.vllm.ai/rocm/vllm](https://wheels.vllm.ai/rocm/vllm) . @@ -234,7 +234,6 @@ You can find more information about vLLM's wheels in !!! tip - The steps below follow [docker/Dockerfile.rocm_base](https://github.com/vllm-project/vllm/blob/main/docker/Dockerfile.rocm_base), which pins the validated version of every component. If a step does not work for you, refer to it; a Dockerfile is a form of installation steps. - - The legacy ROCm 7.2 stack uses a system ROCm installation instead; its steps are in [docker/Dockerfile.rocm_72_base](https://github.com/vllm-project/vllm/blob/main/docker/Dockerfile.rocm_72_base). 0. Install the ROCm SDK and PyTorch (skip if you are already in an environment or docker image with them installed, e.g. `vllm/vllm-openai-rocm:base-nightly`). @@ -371,7 +370,10 @@ The images can be used to run OpenAI compatible server and are available on Dock - `vllm/vllm-openai-rocm:latest` — stable release, built on ROCm 10.0 (also tagged `latest-rocm100`) - `vllm/vllm-openai-rocm:nightly` — preview build from the latest development branch on ROCm 10.0 (also tagged `nightly-rocm100`), use this if you want the latest features and fixes -- `vllm/vllm-openai-rocm:latest-rocm72` / `nightly-rocm72` — the same builds on the legacy ROCm 7.2 stack, kept for a transition period +- `vllm/vllm-openai-rocm:latest-rocm72` / `nightly-rocm72` — earlier builds on the legacy ROCm 7.2 stack, no longer updated +- `vllm/vllm-openai-rocm:base-nightly` / `latest-base` (and their `-rocm100` forms) — the same image without vLLM itself: the ROCm stack plus all of vLLM's dependencies. Earlier builds of these tags held only the ROCm stack ([docker/Dockerfile.rocm_base](https://github.com/vllm-project/vllm/blob/main/docker/Dockerfile.rocm_base)) + +ROCm 10.0 images use zstd-compressed layers; pulling them needs Docker 23 or newer (or containerd 1.5 or newer). ```bash docker run --rm \ @@ -422,57 +424,19 @@ AMD also offered nightly prebuilt docker image from [Docker Hub](https://hub.doc --8<-- [end:pre-built-images] --8<-- [start:build-image-from-source] -You can build and run vLLM from source via the provided [docker/Dockerfile.rocm](https://github.com/vllm-project/vllm/blob/main/docker/Dockerfile.rocm). - -??? info "(Optional) Build an image with ROCm software stack" - - Build a docker image from [docker/Dockerfile.rocm_base](https://github.com/vllm-project/vllm/blob/main/docker/Dockerfile.rocm_base) which setup ROCm software stack needed by the vLLM. - **This step is optional as this rocm_base image is usually prebuilt and store at [Docker Hub](https://hub.docker.com/r/rocm/vllm-dev) under tag `rocm/vllm-dev:base` to speed up user experience.** For the legacy ROCm 7.2 stack, the base image is built from [docker/Dockerfile.rocm_72_base](https://github.com/vllm-project/vllm/blob/main/docker/Dockerfile.rocm_72_base) and published as `vllm/vllm-openai-rocm:base-nightly-rocm72`. - If you choose to build this rocm_base image yourself, the steps are as follows. +You can build and run vLLM from source via the provided [docker/Dockerfile.rocm](https://github.com/vllm-project/vllm/blob/main/docker/Dockerfile.rocm), driven by [docker/docker-bake-rocm.hcl](https://github.com/vllm-project/vllm/blob/main/docker/docker-bake-rocm.hcl) (requires Docker Buildx). +It uses ROCm 10.0 (installed from TheRock wheels, [docker/Dockerfile.rocm_base](https://github.com/vllm-project/vllm/blob/main/docker/Dockerfile.rocm_base)). Older vLLM branches support ROCm 5.7 through 7.0. - It is important that the user kicks off the docker build using buildkit. Either the user put `DOCKER_BUILDKIT=1` as environment variable when calling docker build command, or the user needs to set up buildkit in the docker daemon configuration `/etc/docker/daemon.json` as follows and restart the daemon: - - ```json - { - "features": { - "buildkit": true - } - } - ``` - - To build vllm on ROCm 10.0 for all supported GPUs, you can use the default: - - ```bash - DOCKER_BUILDKIT=1 docker build \ - -f docker/Dockerfile.rocm_base \ - -t rocm/vllm-dev:base . - ``` - -First, build a docker image from [docker/Dockerfile.rocm](https://github.com/vllm-project/vllm/blob/main/docker/Dockerfile.rocm) and launch a docker container from the image. -It is important that the user kicks off the docker build using buildkit. Either the user put `DOCKER_BUILDKIT=1` as environment variable when calling docker build command, or the user needs to set up buildkit in the docker daemon configuration /etc/docker/daemon.json as follows and restart the daemon: - -```json -{ - "features": { - "buildkit": true - } -} -``` - -[docker/Dockerfile.rocm](https://github.com/vllm-project/vllm/blob/main/docker/Dockerfile.rocm) uses ROCm 10.0 (installed from TheRock wheels) by default. The legacy ROCm 7.2 stack is available as [docker/Dockerfile.rocm_72](https://github.com/vllm-project/vllm/blob/main/docker/Dockerfile.rocm_72) with [docker/Dockerfile.rocm_72_base](https://github.com/vllm-project/vllm/blob/main/docker/Dockerfile.rocm_72_base); older vLLM branches support ROCm 5.7 through 7.0. -It provides flexibility to customize the build of docker image using the following arguments: - -- `BASE_IMAGE`: specifies the base image used when running `docker build`. The default value `rocm/vllm-dev:base` is an image published and maintained by AMD. It is being built using [docker/Dockerfile.rocm_base](https://github.com/vllm-project/vllm/blob/main/docker/Dockerfile.rocm_base). For [docker/Dockerfile.rocm_72](https://github.com/vllm-project/vllm/blob/main/docker/Dockerfile.rocm_72), the default is `vllm/vllm-openai-rocm:base-nightly-rocm72` -- `ARG_PYTORCH_ROCM_ARCH`: Allows to override the gfx architecture values from the base docker image - -Their values can be passed in when running `docker build` with `--build-arg` options. - -To build vllm on ROCm 10.0 for all supported GPUs, you can use the default (which build a docker image with `vllm serve` as entrypoint): +To build vllm on ROCm 10.0 for all supported GPUs (an image with `vllm serve` as entrypoint), reusing the published ROCm stack and dependencies (`vllm/vllm-openai-rocm:base-nightly`) so that only vLLM itself is compiled: ```bash -DOCKER_BUILDKIT=1 docker build -f docker/Dockerfile.rocm -t vllm/vllm-openai-rocm . +STACK_IMAGE=vllm/vllm-openai-rocm:base-nightly RUNTIME=vllm/vllm-openai-rocm:base-nightly \ + TAG=vllm/vllm-openai-rocm docker buildx bake -f docker/docker-bake-rocm.hcl vllm-openai ``` +Without `STACK_IMAGE` and `RUNTIME`, the ROCm stack and every dependency are built from source as well, which takes several hours. +To compile vLLM only for your GPU, add for example `--set vllm-openai.args.ARCH=gfx942`. + To run vLLM with the custom-built Docker image: ```bash diff --git a/docs/models/extensions/tensorizer.md b/docs/models/extensions/tensorizer.md index 1e0d5da81e6e..75639574ccfe 100644 --- a/docs/models/extensions/tensorizer.md +++ b/docs/models/extensions/tensorizer.md @@ -1,6 +1,6 @@ # Loading models with CoreWeave's Tensorizer -vLLM supports loading models with [CoreWeave's Tensorizer](https://docs.coreweave.com/coreweave-machine-learning-and-ai/inference/tensorizer). +vLLM supports loading models with [CoreWeave's Tensorizer](https://github.com/coreweave/tensorizer). vLLM model tensors that have been serialized to disk, an HTTP/HTTPS endpoint, or S3 endpoint can be deserialized at runtime extremely quickly directly to the GPU, resulting in significantly shorter Pod startup times and CPU memory usage. Tensor encryption is also supported. diff --git a/docs/models/pooling_models/token_embed.md b/docs/models/pooling_models/token_embed.md index b0a09c0f887d..3823d5560527 100644 --- a/docs/models/pooling_models/token_embed.md +++ b/docs/models/pooling_models/token_embed.md @@ -82,6 +82,15 @@ If your model is not in the above list, we will try to automatically convert the jina-reranker-v3 is a listwise document reranker model with a novel `last but not late interaction` architecture. More information can be found at: [examples/pooling/token_embed/jina_reranker_v3_offline.py](../../../examples/pooling/token_embed/jina_reranker_v3_offline.py) +For `jinaai/jina-reranker-v3`, `truncate_prompt_tokens` is unsupported. This applies to +`LLM.encode(..., pooling_task="token_embed")`, `LLM.score`, `/pooling`, `/score`, and +`/rerank`, even when the limit would leave the prompt unchanged. Requests that set +it fail before inference; HTTP APIs return status 400 with +`param="truncate_prompt_tokens"`. For scoring, use `max_tokens_per_query` and +`max_tokens_per_doc` to truncate the query and documents separately. With `LLM.score`, +pass these limits through `PoolingParams(extra_kwargs={...})`. For `/pooling` and +`LLM.encode`, truncate each input text on the client before submitting the request. + --8<-- [end:supported-token-embed-models] ## Offline Inference diff --git a/docs/models/structured_decisions_models.md b/docs/models/structured_decisions_models.md index 8d2d09f3b6d0..2ec032a3c3ba 100644 --- a/docs/models/structured_decisions_models.md +++ b/docs/models/structured_decisions_models.md @@ -200,6 +200,11 @@ The response adds a `joint` object, `null` when each question was read on its ow | `joint` | optional boolean, default `false`; see [Joint reads](#joint-reads). DiffusionGemma only, other models return 400 | | `images`, `audio`, `videos` | optional media URLs or data URLs, for models that take them ([Clef-Flash](#clef-flash), images/video; [Clef-Omni](#clef-omni); [Decision 3.0](#decision-30), images/video; DiffusionGemma, images only, with or without `joint`; [Liquid d1](#liquid-d1), images only; multimodal `NextTokenStrategy` models such as Qwen3.5, images only) | +By default, `/v1/systemone` rejects `chat_template_kwargs.chat_template` with +HTTP 400. Start the server with `--trust-request-chat-template` to allow +request-provided chat templates. Other template kwargs, such as +`enable_thinking`, do not require this flag. + Unknown top-level fields, and question fields other than `type`, `instructions` and `criteria`, are rejected with a 400. diff --git a/docs/models/supported_models.md b/docs/models/supported_models.md index 8520a3347fda..6719e661b8ff 100644 --- a/docs/models/supported_models.md +++ b/docs/models/supported_models.md @@ -200,6 +200,7 @@ Some model architectures are supported via vLLM plugins. These plugins extend vL | `BartForConditionalGeneration` | BART | [bart-plugin](https://github.com/vllm-project/bart-plugin) | | `Florence2ForConditionalGeneration` | Florence-2 | [bart-plugin](https://github.com/vllm-project/bart-plugin) | | `Nemotron3_5AsrForRNNT` | NVIDIA Nemotron 3.5 ASR | [vllm-nemotron-asr-plugin](https://github.com/Sohaib-Ahmed21/vllm-nemotron-asr-plugin) | +| `ParakeetForTDT` | NVIDIA Parakeet TDT | [vllm-parakeet-plugin](https://github.com/Alex-Wengg/vllm-parakeet-plugin) | For other model architectures not natively supported, in particular for Encoder-Decoder models, we recommend following a similar pattern by implementing support through the plugin system. @@ -451,7 +452,6 @@ th { | `NemotronForCausalLM` | Nemotron-3, Nemotron-4, Minitron | `nvidia/Minitron-8B-Base`, `mgoin/Nemotron-4-340B-Base-hf-FP8`, etc. | ✅︎ | ✅︎ | | `NemotronHForCausalLM` | Nemotron-H | `nvidia/Nemotron-H-8B-Base-8K`, `nvidia/Nemotron-H-47B-Base-8K`, `nvidia/Nemotron-H-56B-Base-8K`, etc. | ✅︎ | ✅︎ | | `OlmoHybridForCausalLM` | OLMo Hybrid | `allenai/Olmo-Hybrid-7B` | ✅︎ | ✅︎ | -| `OlmoeForCausalLM` | OLMoE | `allenai/OLMoE-1B-7B-0924`, `allenai/OLMoE-1B-7B-0924-Instruct`, etc. | | ✅︎ | | `OPTForCausalLM` | OPT, OPT-IML | `facebook/opt-66b`, `facebook/opt-iml-max-30b`, etc. | ✅︎ | ✅︎ | | `OrionForCausalLM` | Orion | `OrionStarAI/Orion-14B-Base`, `OrionStarAI/Orion-14B-Chat`, etc. | | ✅︎ | | `PanguEmbeddedForCausalLM` | openPangu-Embedded-7B | `FreedomIntelligence/openPangu-Embedded-7B-V1.1` | ✅︎ | ✅︎ | @@ -497,6 +497,7 @@ Some models are supported only via the [Transformers modeling backend](#transfor | `OlmoForCausalLM` | OLMo | `allenai/OLMo-1B-hf`, `allenai/OLMo-7B-hf`, etc. | ✅︎ | ✅︎ | | `Olmo2ForCausalLM` | OLMo2 | `allenai/OLMo-2-0425-1B`, etc. | ✅︎ | ✅︎ | | `Olmo3ForCausalLM` | OLMo3 | `allenai/Olmo-3-7B-Instruct`, `allenai/Olmo-3-32B-Think`, etc. | ✅︎ | ✅︎ | +| `OlmoeForCausalLM` | OLMoE | `allenai/OLMoE-1B-7B-0924`, `allenai/OLMoE-1B-7B-0924-Instruct`, etc. | ✅︎ | ✅︎ | | `PhiForCausalLM` | Phi | `microsoft/phi-1_5`, `microsoft/phi-2`, etc. | ✅︎ | ✅︎ | | `SeedOssForCausalLM` | SeedOss | `ByteDance-Seed/Seed-OSS-36B-Instruct`, etc. | ✅︎ | ✅︎ | | `SmolLM3ForCausalLM` | SmolLM3 | `HuggingFaceTB/SmolLM3-3B` | ✅︎ | ✅︎ | @@ -581,8 +582,6 @@ These models primarily accept the [`LLM.generate`](./generative_models.md#llmgen | `InternVLChatModel` | InternVL 3.5, InternVL 3.0, InternVideo 2.5, InternVL 2.5, InternVL 2.0 | T + IE+ + (VE+) | `OpenGVLab/InternVL3_5-14B`, `OpenGVLab/InternVL3-9B`, `OpenGVLab/InternVideo2_5_Chat_8B`, `OpenGVLab/InternVL2_5-4B`, `OpenGVLab/InternVL2-4B`, etc. | ✅︎ | ✅︎ | | `InternVLForConditionalGeneration` | InternVL 3.0 (HF format) | T + IE+ + VE+ | `OpenGVLab/InternVL3-1B-hf`, etc. | ✅︎ | ✅︎ | | `KananaVForConditionalGeneration` | Kanana-V | T + I+ | `kakaocorp/kanana-1.5-v-3b-instruct`, etc. | | ✅︎ | -| `KeyeForConditionalGeneration` | Keye-VL-8B-Preview | T + IE+ + VE+ | `Kwai-Keye/Keye-VL-8B-Preview` | ✅︎ | ✅︎ | -| `KeyeVL1_5ForConditionalGeneration` | Keye-VL-1_5-8B | T + IE+ + VE+ | `Kwai-Keye/Keye-VL-1_5-8B` | ✅︎ | ✅︎ | | `KimiAudioForConditionalGeneration` | Kimi-Audio | T + A+ | `moonshotai/Kimi-Audio-7B-Instruct` | | ✅︎ | | `KimiK25ForConditionalGeneration` | Kimi-K2.5 | T + I+ | `moonshotai/Kimi-K2.5` | | ✅︎ | | `KimiK3ForConditionalGeneration` | Kimi-K3 | T + I+ | `moonshotai/Kimi-K3` | | ✅︎ | @@ -610,7 +609,9 @@ These models primarily accept the [`LLM.generate`](./generative_models.md#llmgen | `Moondream3ForCausalLM` | Moondream3 | T + I | `moondream/moondream3-preview` | | ✅︎ | | `MuseGlimmerForCausalLM`, `MuseGlimmerForConditionalGeneration` | Muse Glimmer | T + I+ + V+ | `meta-models/Muse-Glimmer-30B` | ✅︎ | ✅︎ | | `MuseSparkForConditionalGeneration` | Muse Spark Open | T + I+ + V+ + A+ | `meta-models/Muse-Spark-Open-816B-A42B-NVFP4`, etc. | ✅︎ | ✅︎ | +| `Nemotron3_5TranscribeForConditionalGeneration` | Nemotron-3.5-Transcribe | T + A | `nvidia/NVIDIA-Nemotron-3.5-Transcribe-30B-A4B`, `nvidia/NVIDIA-Nemotron-3.5-Transcribe-30B-A4B-FP8`, `nvidia/NVIDIA-Nemotron-3.5-Transcribe-30B-A4B-NVFP4` | | ✅︎ | | `NemotronH_Nano_Omni_Reasoning_V3`, `NemotronH_Nano_VL_V2` | Nemotron Nano 3 Omni, Nemotron Nano V2 VL | T + IE+ + V+ + A* | `nvidia/Nemotron-3-Nano-Omni-30B-A3B-Reasoning-BF16` | ✅︎ | | +| `NemotronH_Omni_Reasoning_V3` | Nemotron 3.5 Super VL | T + IE+ + V+ | `nvidia/NVIDIA-Nemotron-3.5-Super-VL-120B-A12B-BF16`, `nvidia/NVIDIA-Nemotron-3.5-Super-VL-120B-A12B-NVFP4`, etc. | ✅︎ | | | `RadioMBartPrefixForCausalLM` | Nemotron Parse 3.5 | T + I | `nvidia/Nemotron-3.5-Parse` | | | | `NVLM_D_Model` | NVLM-D 1.0 | T + I+ | `nvidia/NVLM-D-72B`, etc. | | ✅︎ | | `OpenCUAForConditionalGeneration` | OpenCUA-7B | T + IE+ | `xlangai/OpenCUA-7B` | ✅︎ | ✅︎ | @@ -723,10 +724,20 @@ Some models are supported only via the [Transformers modeling backend](#transfor its [DFlash](../features/speculative_decoding/README.md) path. !!! note - Both Nemotron Nano architecture names map to the same vLLM implementation. + Both Nemotron Nano architecture names and `NemotronH_Omni_Reasoning_V3` map to + the same vLLM implementation. Audio inputs are only supported by the Omni variants; `NemotronH_Nano_VL_V2` checkpoints such as `nvidia/NVIDIA-Nemotron-Nano-12B-v2-VL-BF16` accept text, - image and video only. + image and video only, as do the Nemotron 3.5 Super VL checkpoints. + `NemotronH_Omni_Reasoning_V3` checkpoints load without `--trust-remote-code` + on `transformers>=5.18.0`; older versions still require the flag. + +!!! note + On ROCm, `Step5ForConditionalGeneration` runs its MoE layers on Triton. With + `VLLM_ROCM_USE_AITER=1`, also pass `--moe-backend triton`: AITER's MoE does not + apply Step-5's SwiGLU clamp. An FP8 KV cache (`--kv-cache-dtype fp8`) needs + `--block-size 32`. The checkpoint ships three MTP layers, enabled with + `--speculative-config '{"method":"mtp","num_speculative_tokens":3}'`. !!! note The official `openbmb/MiniCPM-V-2` doesn't work yet, so we need to use a fork (`HwwwH/MiniCPM-V-2`) for now. @@ -738,6 +749,7 @@ Speech2Text models trained specifically for Automatic Speech Recognition. | Architecture | Models | Example HF Models | [LoRA](../features/lora.md) | [PP](../serving/parallelism_scaling.md) | | ------------ | ------ | ----------------- | -------------------- | ------------------------- | +| `CanaryForConditionalGeneration` | Canary | `nvidia/canary-1b-v2` | | | | `CohereAsrForConditionalGeneration` | Cohere-Transcribe | `CohereLabs/cohere-transcribe-03-2026` | | | | `FireRedASR2ForConditionalGeneration` | FireRedASR2 | `allendou/FireRedASR2-LLM-vllm`, etc. | | | | `FunASRForConditionalGeneration` | FunASR | `FunAudioLLM/Fun-ASR-Nano-2512-vllm`, etc. | | | @@ -745,12 +757,33 @@ Speech2Text models trained specifically for Automatic Speech Recognition. | `GlmAsrForConditionalGeneration` | GLM-ASR | `zai-org/GLM-ASR-Nano-2512` | ✅︎ | ✅︎ | | `GraniteSpeechForConditionalGeneration` | Granite Speech | `ibm-granite/granite-4.0-1b-speech`, `ibm-granite/granite-speech-3.3-2b`, etc. | ✅︎ | ✅︎ | | `GraniteSpeechPlusForConditionalGeneration` | Granite Speech Plus | `ibm-granite/granite-speech-4.1-2b-plus` | ✅︎ | ✅︎ | +| `MingV3ASRForConditionalGeneration` | Ming-3.0-ASR | `inclusionAI/Ming-3.0-asr-tiny` | | | | `MossTranscribeDiarizeForConditionalGeneration` | MOSS-Transcribe-Diarize | `OpenMOSS-Team/MOSS-Transcribe-Diarize` | | ✅︎ | +| `Nemotron3_5TranscribeForConditionalGeneration` | Nemotron-3.5-Transcribe | `nvidia/NVIDIA-Nemotron-3.5-Transcribe-30B-A4B`, `nvidia/NVIDIA-Nemotron-3.5-Transcribe-30B-A4B-FP8`, `nvidia/NVIDIA-Nemotron-3.5-Transcribe-30B-A4B-NVFP4` | | ✅︎ | | `Qwen3ASRForConditionalGeneration` | Qwen3-ASR | `Qwen/Qwen3-ASR-1.7B`, etc. | ✅︎ | ✅︎ | | `Qwen3OmniMoeThinkerForConditionalGeneration` | Qwen3-Omni | `Qwen/Qwen3-Omni-30B-A3B-Instruct`, etc. | | ✅︎ | | `VoxtralForConditionalGeneration` | Voxtral (Mistral format) | `mistralai/Voxtral-Mini-3B-2507`, `mistralai/Voxtral-Small-24B-2507`, etc. | ✅︎ | ✅︎ | | `WhisperForConditionalGeneration` | Whisper | `openai/whisper-small`, `openai/whisper-large-v3-turbo`, etc. | | | +!!! note + `Nemotron3_5TranscribeForConditionalGeneration` transcribes with speaker tags. Its chat template turns + thinking on by default, so transcription servers should set + `--default-chat-template-kwargs '{"enable_thinking": false}'`. It supports MTP and DFlash2 speculative + decoding. + + Long audio: by default vLLM refuses audio longer than 600 s and transcription uploads larger than 25 MB + (`VLLM_MAX_AUDIO_DECODE_DURATION_S`, `VLLM_MAX_AUDIO_CLIP_FILESIZE_MB`). A chat request takes one clip of + up to one hour, the model's training limit (`--limit-mm-per-prompt '{"audio": {"length": }}'` + changes it), and `/v1/audio/transcriptions` splits longer audio into pieces of at most one hour, shorter + when `--max-model-len` is below 65,536. + + Audio tower: the FP8 and NVFP4 checkpoints quantize the linear layers of its ASR encoder to FP8 through + their `quantization_config`. FP8 encoder attention (`--mm-encoder-attn-dtype fp8`) is not supported and + raises an error when the model is built. The tower attends through vLLM's multimodal encoder attention + backend (FlashAttention on CUDA GPUs that support it; `--mm-encoder-attn-backend` selects another). In + bfloat16, its output can depend on that backend and on the other clips in the batch: small rounding + differences can flip the thresholded speaker features on some frames. + !!! note `VoxtralForConditionalGeneration` requires `mistral-common[audio]` to be installed. diff --git a/docs/pre_run_check.sh b/docs/pre_run_check.sh index 04f4a642ef07..9ccfc85b35e9 100644 --- a/docs/pre_run_check.sh +++ b/docs/pre_run_check.sh @@ -29,7 +29,7 @@ DOCS_PATHS=( requirements/docs.txt # Affects build process requirements/docs.in # Affects build process ) -if git diff --quiet origin/main -- "${DOCS_PATHS[@]}"; then +if git diff --quiet origin/main...HEAD -- "${DOCS_PATHS[@]}"; then echo "No docs-affecting files changed vs origin/main; cancelling build." # See https://docs.readthedocs.com/platform/latest/guides/build/skip-build.html for info on exit code exit 183 diff --git a/docs/serving/expert_parallel_deployment.md b/docs/serving/expert_parallel_deployment.md index f3b329610216..0c6d08560eb0 100644 --- a/docs/serving/expert_parallel_deployment.md +++ b/docs/serving/expert_parallel_deployment.md @@ -29,6 +29,7 @@ vLLM provides multiple communication backends for EP. Use `--all2all-backend` to | `flashinfer_nvlink_one_sided` | MNNVL systems | FlashInfer's one-sided A2A strategy for multi-node NVLink | High-throughput workloads | | `passthrough` | Any | Internal, not accepted on the CLI: bound automatically for MoE backends that dispatch and combine themselves, so no all-to-all runs in the framework | `--moe-backend flashinfer_moe_ep_*` (FlashInfer MoE-EP megakernels) | | `flashinfer_nvlink_two_sided` | MNNVL systems | FlashInfer's two-sided A2A strategy for multi-node NVLink | Systems with NVLink across nodes | +| `mori` | ROCm | MoRI EP: EPv2 (dispatch/combine over a CCO symmetric-memory window) where supported, otherwise EPv1 | AMD Instinct; see [MoRI EP](#mori-ep-rocm) | ## Single Node Deployment @@ -80,6 +81,48 @@ vllm serve deepseek-ai/DeepSeek-V3-0324 \ --enable-expert-parallel # Enable expert parallelism ``` +### MoRI EP (ROCm) + +`--all2all-backend mori` moves MoE tokens with MoRI. It runs MoRI EPv2 (`mori.ops.dispatch_combine_v2`, which reads and writes peer GPU memory directly through a MoRI CCO symmetric-memory window) where EPv2 is supported, and MoRI EPv1 otherwise. + +```bash +VLLM_ROCM_USE_AITER=1 VLLM_ROCM_USE_AITER_MOE=1 \ +VLLM_ROCM_USE_AITER_FUSION_SHARED_EXPERTS=0 \ +vllm serve deepseek-ai/DeepSeek-V4-Pro \ + --tensor-parallel-size 1 \ + --data-parallel-size 8 \ + --enable-expert-parallel \ + --all2all-backend mori +``` + +`VLLM_MORI_EP_V2` selects the MoRI version: + +| `VLLM_MORI_EP_V2` | Behavior | +| ----------------- | -------- | +| unset | EPv2, falling back to EPv1 with a warning when MoRI is older than 1.2.4, the GPU is not gfx950 or gfx1250, EP spans hosts (except on gfx1250, whose UALink team can span hosts), `MORI_ENABLE_SDMA=1`, `--moe-backend aiter_mega_moe`, the model is not BF16, or MoE activations use per-tensor FP8 | +| `1` | Always EPv2. The checks above only log a warning; startup fails if the EP ranks are not in one CCO-LSA team | +| `0` | EPv1 | + +On EPv1, `mori` uses the IntraNode kernel within one node. Across nodes it uses InterNodeV1LL when the per-rank token budget (`--max-num-batched-tokens`) is at most `VLLM_MORI_DISPATCH_INTER_KERNEL_SWITCH_THRESHOLD` (default 256), and InterNodeV1 otherwise. `mori_high_throughput` and `mori_low_latency` always run EPv1 and pin the inter-node kernel to InterNodeV1 and InterNodeV1LL respectively. + +Choosing a MoRI backend: use EPv2 wherever it applies, and EPv1 for deployments EPv2 does not support. EPv2 cuts its receive buffers to the rows a step can actually receive and lets AITER fused MoE write its output straight into the combine buffer, so it does less work per step than EPv1. + +| Deployment | Backend | +| ---------- | ------- | +| EP within one node (or one gfx1250 UALink fabric), BF16 model, AITER fused MoE | `mori` (runs EPv2) | +| EP within one node, with experts that take activations quantized before dispatch (e.g. FP8 or INT8 Triton experts) | EPv2 rejects these experts at startup. Enable AITER fused MoE (`VLLM_ROCM_USE_AITER=1 VLLM_ROCM_USE_AITER_MOE=1`) so `mori` runs EPv2 with AITER experts; MoRI EPv1 (`VLLM_MORI_EP_V2=0`) also needs AITER fused MoE. To keep Triton experts without AITER, use a non-MoRI backend such as `allgather_reducescatter` | +| EP across nodes | `mori` (falls back to EPv1; picks InterNodeV1LL when the per-rank token budget is at most `VLLM_MORI_DISPATCH_INTER_KERNEL_SWITCH_THRESHOLD`, else InterNodeV1) | + +Within one node, all EPv1 options run the same IntraNode kernel. To pin the inter-node kernel, set the threshold: `0` always picks InterNodeV1, and a value at least the per-rank token budget always picks InterNodeV1LL. `mori_high_throughput` and `mori_low_latency` do the same pinning but are deprecated and will be removed in a future release; use `mori` instead. + +EPv2 limitations: + +- All EP ranks must be in one MoRI CCO-LSA team, i.e. GPUs that can load and store each other's memory: one node, or one UALink fabric. Startup fails otherwise; there is no multi-node RDMA path yet. +- Activations are dispatched in BF16, so the model dtype must be BF16 and the experts must take unquantized inputs (AITER fused MoE, which quantizes after the all-to-all, or BF16 experts). Experts that expect activations quantized before dispatch, such as FP8 or INT8 Triton experts, are rejected at startup. Block FP8 and MXFP4 weights work; per-tensor FP8 activations (`--quantization fp8` without blocks, ModelOpt FP8) do not, since AITER cannot quantize the received rows per tensor. +- AITER shared-expert fusion is not supported: set `VLLM_ROCM_USE_AITER_FUSION_SHARED_EXPERTS=0`. +- Install a MoRI build that ships `mori.ops.dispatch_combine_v2`. On NICs that limit a registered memory region to 2 GiB, the window registration fails once the dispatch arena outgrows that limit (DeepSeek-V4-Pro at EP8 already does at the default 16k batched tokens). +- Older MoRI builds leave a stale HIP error behind when the fabric-handle probe fails (e.g. on a single MI355X node), and the next PyTorch allocation raises `operation not supported`. + ## Multi-Node Deployment For multi-node deployment, use the DeepEP communication kernel with one of two modes (see [Backend Selection Guide](#backend-selection-guide) above). @@ -156,7 +199,7 @@ Configure EPLB with the `--eplb-config` argument, which accepts a JSON string. T | `num_redundant_experts` | Additional global experts per EP rank beyond equal distribution | `0` | | `use_async` | Use non-blocking EPLB for reduced latency overhead | `true` | | `policy` | The policy type for expert parallel load balancing | `"default"` | -| `communicator` | Backend for expert weight transfers: `"torch_nccl"`, `"torch_gloo"`, `"pynccl"`, `"nixl"`, or `null` (auto) | `null` | +| `communicator` | Backend for expert weight transfers: `"torch_nccl"`, `"torch_gloo"`, `"pynccl"`, `"nixl"`, `"symm_mem"`, or `null` (auto) | `null` | For example: @@ -189,6 +232,8 @@ EPLB uses redundant experts that need to fit in GPU memory. This means that EPLB This overhead equals `NUM_MOE_LAYERS * BYTES_PER_EXPERT * (NUM_TOTAL_EXPERTS + NUM_REDUNDANT_EXPERTS) ÷ NUM_EP_RANKS`. For DeepSeekV3, this is approximately `2.4 GB` for one redundant expert per EP rank. +The `symm_mem` communicator requires all EP ranks in one NVLink domain (MNNVL across nodes). It allocates its one-layer receive buffer in symmetric memory instead of regular memory, and senders write into it directly, so it uses no more memory than the other backends. + ### Example Command Single node deployment with EPLB enabled: @@ -319,3 +364,44 @@ except Exception as e: - To simulate the decode deployment of disaggregated serving, pass `--kv-transfer-config '{"kv_connector":"DecodeBenchConnector","kv_role":"kv_both"}'` to the `vllm serve` invocation. The connector populates KV cache with random values so decode can be profiled in isolation. Add `"kv_connector_extra_config":{"startup_fill":true}` to fill the whole KV cache once at startup instead of in the decode steps that admit new requests (see the connector docstring for its limitations). - **CUDAGraph capture**: Use `--compilation_config '{"cudagraph_mode": "FULL_DECODE_ONLY"}'` to enable CUDA graph capture for decode only and save KV cache. + +## Fault tolerance and scale-down recovery + +Scale-down reloads every surviving physical replica using the recovered EPLB +placement. The fused GPT-OSS MXFP4 checkpoint loader is not supported for this +reload path; recovery rejects it rather than loading weights into the wrong slots. + +Fault tolerance lets vLLM recover from certain distributed engine failures without +restarting the entire deployment. An external orchestrator detects failures and +coordinates recovery across the engines. + +### Configuring recovery + +Both retry and scale-down require `--enable-fault-tolerance`, +`--enable-expert-parallel`, an FT-capable `--all2all-backend` (`nixl_ep` or +`deepep_low_latency`), and the v2 model runner (`VLLM_USE_V2_MODEL_RUNNER=1`). +Retry can recover from a fault when all workers remain alive. + +For scale-down recovery, also enable `--enable-eplb` and configure enough +redundant expert slots with `--eplb-config.num_redundant_experts` to fit all +logical experts on the surviving ranks. The original checkpoint must remain +accessible to survivors so they can reload expert weights. +The orchestrator selects the recovery mode when it sends the recovery request. + +### Coordinating recovery + +Send recovery instructions to `POST /v1/fault_tolerance/apply`, then poll +`GET /v1/fault_tolerance/status` on each participating engine. Use a new, +nonempty `request_id` for each recovery round and send the same ID to every +engine in that round. An HTTP 202 response means the instruction was accepted; +wait until all participating engines report `healthy` before resuming traffic. + +Use `retry` when all workers are still alive. To remove a failed DP rank, stop +routing traffic to it and send `scale_down` to every surviving engine once they +report `unhealthy`. Include the original rank IDs in `removed_dp_ranks`. If the +removed rank hosted the DP store, also provide a reachable surviving +`dp_master_ip` and a free `dp_store_port`. + +Scale-down restores service with fewer ranks; it does not restart the removed +engines or restore their capacity. The previous `/fault_tolerance/apply` +endpoint is no longer registered. diff --git a/docs/serving/integrations/claude_code.md b/docs/serving/integrations/claude_code.md index 99a89a076769..6f8f8f387a49 100644 --- a/docs/serving/integrations/claude_code.md +++ b/docs/serving/integrations/claude_code.md @@ -77,4 +77,6 @@ If the model responds correctly, your setup is working. You can now use Claude C **Tool calls not working**: Verify that your model supports tool calling and that you've enabled it with the correct `--tool-call-parser` flag. See [Tool Calling](../../features/tool_calling.md). +**WebSearch fails with `tool type 'web_search_...' is not supported`**: Claude Code's WebSearch tool relies on Anthropic's server-side web search tool, which runs on Anthropic's servers. vLLM does not run server tools and rejects requests that include them with a 400. Use an MCP server that provides web search instead. + **Model not found**: Ensure the `--served-model-name` matches the model names in your environment variables. You cannot use model names with `/` in them, such as `openai/gpt-oss-120b` directly from Huggingface, so beware of that limitation with Claude Code. diff --git a/docs/serving/online_serving/derenderer.md b/docs/serving/online_serving/derenderer.md index 65b1d0daf8cf..09e96a4ba205 100644 --- a/docs/serving/online_serving/derenderer.md +++ b/docs/serving/online_serving/derenderer.md @@ -8,6 +8,8 @@ This closes the loop for a token-in / token-out engine in disaggregated serving: - **Parser parity**: The derenderer reuses vLLM's tool and reasoning parsers, so a disaggregated deployment produces the same `content`/`reasoning`/ `tool_calls` split as a standard `vllm serve` server - **Streaming and one-shot parsing**: Non-streaming calls send a complete `GenerateResponse` with all token IDs present and get one-shot parsing. Both endpoints also accept `stream: true`, taking one `GenerateStreamResponse` delta plus a client carried `stream_state` and returning `{chunk, stream_state}`. The chat endpoint's streaming path supports reasoning and tool call parsing while emitting the same `reasoning`/`content`/`tool_calls` deltas the generate streaming path would for the same token IDs (see [Streaming](#streaming)) +For clients that already use `GenerateRequest`, `output_mode: "derender"` on `/inference/v1/generate` parses inline and supports `stream: true` with no per-chunk hop (see [Token In Token Out](token_in_token_out.md)). + Both endpoints are hosted by the GPU less rendering server started with [`vllm launch render`](../../cli/launch/render.md), alongside the `/render` endpoints. diff --git a/docs/serving/online_serving/openai_compatible_server.md b/docs/serving/online_serving/openai_compatible_server.md index 7b4e84a17fee..b87903412d22 100644 --- a/docs/serving/online_serving/openai_compatible_server.md +++ b/docs/serving/online_serving/openai_compatible_server.md @@ -174,6 +174,8 @@ see our [Multimodal Inputs](../../features/multimodal_inputs.md) guide for more Code example: [examples/basic/online_serving/openai_chat_completion_client.py](../../../examples/basic/online_serving/openai_chat_completion_client.py) +With `--enable-prompt-tokens-details`, `/v1/chat/completions/batch` sums `cached_tokens`, `created_cache_tokens` and per-modality `multimodal_tokens` across conversations in `usage.prompt_tokens_details`; an unreported cache count makes that total `null`, and disabling the flag leaves the details `null`. + #### Extra parameters The following [sampling parameters](../../api/README.md#inference-parameters) are supported. diff --git a/docs/serving/online_serving/token_in_token_out.md b/docs/serving/online_serving/token_in_token_out.md index f71c5e78fb83..6e4d78bb4c69 100644 --- a/docs/serving/online_serving/token_in_token_out.md +++ b/docs/serving/online_serving/token_in_token_out.md @@ -93,9 +93,17 @@ print(response["choices"][0]["token_ids"]) - `parse_context` is ignored at other levels, so a rendered request can go to a prefill leg unchanged. - `logprobs` follow coupled chat. `top_logprobs` is cut to `sampling_params.logprobs`, `logprob_token_ids` is honored. When a parser hides the reasoning with `include_reasoning: false`, `logprobs`, `token_ids` and `sampling_mask` are dropped so they don't expose it. - `finish_reason` follows coupled chat too. It's `tool_calls` when the message has tool calls except with a named `tool_choice` or a `length` cut. -- `stream: true` isn't supported yet. +- With `stream: true`, each choice carries a chat `delta` (`role`, `content`, `reasoning`, `tool_calls`) instead of `text`, and `finish_reason` follows coupled chat. `role` is set on the first chunk of each choice, `n > 1` is supported, and `null` fields are left out of `delta`. A chunk is sent whenever the engine output carries a parsed delta, visible token IDs, logprobs or a `finish_reason`. Parsing runs on the API server event loop, so `--api-server-count` scales it. - Cohere grounding citations aren't returned. The parser strips the citation spans from `content`, and the document map can't go over JSON in `parse_context`. +A streamed tool call looks like this, with the usage chunk and `data: [DONE]` framed as at the other levels: + +```text +data: {"request_id": "...", "output_mode": "derender", "choices": [{"index": 0, "token_ids": [151657], "delta": {"role": "assistant", "tool_calls": [{"index": 0, "id": "chatcmpl-tool-...", "type": "function", "function": {"name": "get_weather", "arguments": ""}}]}, "finish_reason": null, ...}]} + +data: {"request_id": "...", "output_mode": "derender", "choices": [{"index": 0, "token_ids": [], "delta": {}, "finish_reason": "tool_calls", ...}]} +``` + ### Logprobs With `output_mode: "tokens"`, logprobs are `GenerateLogProbs` with an integer `token_id` and `rank` per entry and no `token` or `bytes` (see [Generate Output Logprobs](renderer.md#generate-output-logprobs)). With `output_mode: "text"`, they are `ChatCompletionLogProbs` carrying the decoded token strings and their UTF-8 `bytes` for the sampled token and every entry in `top_logprobs`; a server started with `--return-tokens-as-token-ids` returns `token_id:N` placeholders there instead, as `/v1/completions` does. @@ -106,7 +114,6 @@ The server returns a 400 for: - `output_mode: "text"` or `"derender"` on a server without a tokenizer (`--tokens-only` or `--skip-tokenizer-init`). - `output_mode: "text"` or `"derender"` with `sampling_params.detokenize: false`. -- `output_mode: "derender"` with `stream: true`. - `output_mode: "derender"` without `parse_context` on a server with a tool or reasoning parser or with a named `tool_choice` in `parse_context` that matches none of its `tools`. This is checked before generating. - An unsupported `output_mode` value. diff --git a/docs/serving/online_serving/trace_replay.md b/docs/serving/online_serving/trace_replay.md index 86ed13644c7a..9acd09b57ad6 100644 --- a/docs/serving/online_serving/trace_replay.md +++ b/docs/serving/online_serving/trace_replay.md @@ -44,6 +44,7 @@ When `trace_decode_token_ids` is set: - The trace is truncated if it does not fit within `max_model_len` given the prompt length. - All stop conditions (EOS, stop strings, stop token IDs) are disabled. - Generation produces exactly the trace tokens, then stops. +- With speculative decoding, a draft token is accepted only if it equals the trace token at its position, so a step emits the matching drafts plus one trace token, up to the end of the trace. The returned logprobs are those of the trace tokens, but each verification step scores several positions in one forward pass, so they are bitwise equal to the logprobs without speculative decoding only with batch invariance (`VLLM_BATCH_INVARIANT=1`). ## Limitations @@ -53,7 +54,6 @@ When `trace_decode_token_ids` is set: | --- | --- | | `n > 1` | Trace replay produces a single deterministic sequence | | `prompt_logprobs` | Expanded logit layout conflicts with trace kernel indexing | -| Speculative decoding | Multi-token speculation conflicts with single-token trace stepping | | Structured outputs | Grammar constraints conflict with forced token injection | | `repetition_detection` | Would terminate the request on repeated patterns in the trace | | `thinking_token_budget` | Logit masking corrupts logprobs for trace tokens | diff --git a/docs/training/weight_transfer/README.md b/docs/training/weight_transfer/README.md index 9c946eba0f13..73b56c6cc2fc 100644 --- a/docs/training/weight_transfer/README.md +++ b/docs/training/weight_transfer/README.md @@ -131,10 +131,11 @@ parameter is usually itself a collective among its owners (an FSDP `full_tensor()` all-gather, a Megatron export) that would deadlock if some of them skipped it. Which ranks own which parameter is declared in [`ParamMeta.owners`](base.md#parammeta-ownership-and-layout). -Only [sharded RDT](sharded_rdt.md) accepts ranks that own different parameters, -such as pipeline stages. Pipeline-parallel support for NCCL and IPC is in -progress; until then they, like NCCL M2N, need every parameter owned by rank 0 -or by every rank (see each backend's Limitations). +Only [sharded RDT](sharded_rdt.md) and [NCCL](nccl.md#broadcasting-ranks) +accept ranks that own different parameters, such as pipeline stages. +Pipeline-parallel support for IPC is in progress; until then it, like NCCL M2N, +needs every parameter owned by rank 0 or by every rank (see each backend's +Limitations). Pass each process's own `rank` on the init info. It is explicit rather than read from a global process group, which is ambiguous once several groups (FSDP / TP / diff --git a/docs/training/weight_transfer/nccl.md b/docs/training/weight_transfer/nccl.md index 8ada8ed870f9..279ba3f21e93 100644 --- a/docs/training/weight_transfer/nccl.md +++ b/docs/training/weight_transfer/nccl.md @@ -10,7 +10,7 @@ The NCCL weight transfer engine uses [NCCL](https://developer.nvidia.com/nccl) b ## How It Works -1. The trainer and all inference workers join a shared NCCL process group using `StatelessProcessGroup` (vLLM's torch.distributed-independent group abstraction). The trainer is rank 0; workers start at `rank_offset` (1). +1. The trainer's broadcasting ranks and all inference workers join a shared NCCL process group using `StatelessProcessGroup` (vLLM's torch.distributed-independent group abstraction). Trainer rank 0 is group rank 0; with [several broadcasters](#broadcasting-ranks) they take `[0, rank_offset)` and workers follow them (`rank_offset` is 1 otherwise). 2. The trainer broadcasts weights to all workers simultaneously. Each worker receives and loads the weights. 3. Optionally, **packed tensor broadcasting** batches multiple small tensors into larger buffers with double/triple buffering and CUDA stream overlap for higher throughput. This implementation is based on [NeMo-RL's packed tensor](https://github.com/NVIDIA-NeMo/RL/blob/main/nemo_rl/utils/packed_tensor.py). @@ -52,7 +52,7 @@ engine = WeightTransferTrainerFactory.trainer_init( init_info=NCCLTrainerInitInfo( master_address=master_address, master_port=master_port, - world_size=world_size, # trainer + all inference workers + world_size=world_size, # broadcasting trainer ranks + all inference workers rank=0, # this trainer rank; rank 0 is the sender packed=True, ), @@ -66,7 +66,8 @@ engine.send_weights() # once per sync `trainer_init` drives the whole handshake: it kicks off the inference side's `init_weight_transfer_engine` (through the client) on a side thread while opening the trainer's own rank-0 endpoint, since both ends must rendezvous together. It -builds the worker's init info itself, with `rank_offset=1` and the same packed +builds the worker's init info itself, with `rank_offset` set to the number of +broadcasting ranks (1 unless the source is partitioned) and the same packed params, so the two sides cannot disagree. `send_weights()` then drives `start_weight_update`, `update_weights` — run @@ -79,7 +80,7 @@ once every transfer has drained. | ----- | ------- | ----------- | | `master_address` | — | Rendezvous host | | `master_port` | — | Rendezvous port | -| `world_size` | — | Full trainer + worker NCCL group size | +| `world_size` | — | Transfer NCCL group size: the [broadcasting trainer ranks](#broadcasting-ranks) plus the inference workers (`1 + num_workers` for a single sender). Other trainer ranks are not counted | | `rank` | — | Keyword-only. This trainer process's rank; **0 is the sender** | | `packed` | `True` | Use packed broadcasting | | `packed_buffer_size_bytes` | 1 GiB | Packed buffer size | @@ -124,13 +125,37 @@ rather than letting it reach the wire. `ModuleSource` satisfies this by construction. If you write a custom source — a Megatron export, an MoE re-fusing pass — this is the invariant to test first. -### One sender - -Each parameter is sent by its first owner, and this backend has a single -sender, trainer rank 0. Every parameter's `owners` must therefore start at rank -0 or be unset; a source whose pipeline stages own different parameters is -rejected at `trainer_init`. Other owners (FSDP peers of rank 0) only -drive `iter_full()` to take part in the gathers. +### Broadcasting ranks + +Each parameter is broadcast by its first owner (rank 0 when `owners` is unset). +When that is rank 0 for every parameter, rank 0 is the only broadcaster. Other +owners (FSDP peers of rank 0) only drive `iter_full()` to take part in the +gathers. + +A partitioned source, such as pipeline stages declaring `owners=(stage +ranks...)`, has several broadcasters. They join the transfer group at group +ranks `[0, rank_offset)` in trainer-rank order, rank 0 always included. Every +rank derives this plan from `metadata`, which is identical on every rank, so +the broadcasters need no extra coordination. In `send_weights()`, rank 0 sends +one `update_weights` per broadcaster, in order, each tagged with that +broadcaster's group rank as `src`. Each broadcaster sends its own parameters +during its turn and receives and discards the others' to stay in the +collective. Initialize and call `send_weights()` on every trainer rank +concurrently. + +Every parameter a rank owns must share its first owner, since each broadcaster +gathers and sends in its own turn. So with several broadcasters every parameter +must declare `owners`. Rank 0 is always in the transfer group and only gathers +in its own turn, so if it owns a parameter it must be that parameter's first +owner. `trainer_init` raises on every rank otherwise. + +A failure on any broadcaster during an update is fatal for the transfer. That +rank raises immediately, but the other trainer ranks and the inference workers +can stay blocked in that update's broadcasts, so tear down the trainer job and +restart or re-initialize the inference engine before transferring again. + +[`rlhf_nccl_multi_source.py`](../../../examples/rl/rlhf_nccl_multi_source.py) +shows two pipeline stages updating one tensor-parallel inference engine. ### Metadata is sent once, then reused @@ -142,7 +167,7 @@ round. So the worker remembers the list: | `update_info` you send | What the worker loads | | ---------------------- | --------------------- | | `names`, `dtype_names`, `shapes` filled in | Exactly these parameters. The worker also saves this list. | -| All three lists empty (or `{}`) | The list saved by the previous `update_weights` call on the same model. | +| All three lists empty (or `{}`) | The list saved by the previous `update_weights` call on the same model from the same `src`. | | All three lists empty, nothing saved yet | Nothing. | Rules to remember: @@ -195,6 +220,9 @@ while rank 0 broadcasts in `names` order, then `POST /finish_weight_update`. checkpoint shapes. From the second sync on, send `{"update_info": {}}` if the parameters did not change; see [Metadata is sent once, then reused](#metadata-is-sent-once-then-reused). +An optional `src` (default `0`, below `rank_offset`) selects the trainer rank +that broadcasts that update, so one start/finish lifecycle can carry updates +from several trainer ranks. - Unpacked: one broadcast per tensor. - Packed: concatenate `t.contiguous().view(-1).view(torch.uint8)` in `names` @@ -275,11 +303,9 @@ Sparse NCCL sends only `O(nnz)` indices and values. Checkpoint application still ## Limitations -- **Pipeline-parallel trainers are not supported yet (work in progress).** Rank 0 - is the only sender, so it must own every parameter (see - [One sender](#one-sender)). A source whose ranks own different parameters is - rejected at `trainer_init`. -- **Full tensors on the wire.** Rank 0 materializes each parameter in full and +- **A broadcaster failure is fatal.** See + [Broadcasting ranks](#broadcasting-ranks). +- **Full tensors on the wire.** Each broadcaster materializes its parameters in full and every worker receives all of it, whatever its tensor-parallel shard. For large or expert-parallel models, see [sharded RDT](sharded_rdt.md) or [NCCL M2N](m2n.md). @@ -288,6 +314,7 @@ Sparse NCCL sends only `O(nnz)` indices and values. Checkpoint application still ## Examples - [RLHF with NCCL weight syncing (`vllm serve`, HTTP)](../../../examples/rl/rlhf_http_nccl.py) - **Start here.** Trainer on one GPU, 2x tensor-parallel fp8 server on two others; HTTP control plane, NCCL data plane. Launches and tears down its own server +- [Multi-source NCCL weight syncing](../../../examples/rl/rlhf_nccl_multi_source.py) - Two pipeline stages each broadcast their own parameters to one TP2 inference engine within a single lifecycle - [NCCL weight syncing from a trainer that does not import vLLM](../../../examples/rl/rlhf_http_nccl_no_vllm.py) - `torch` + `requests` only, `nccl_unique_id_b64` rendezvous; see [Trainer Without vLLM over HTTP](#trainer-without-vllm-over-http) - [RLHF with NCCL + FSDP2 and expert parallelism](../../../examples/rl/rlhf_nccl_fsdp_ep.py) - Multi-rank trainer: every FSDP rank builds an engine and joins the `full_tensor()` gather while only rank 0 touches the wire - [RLHF with sparse NCCL weight syncing (offline, Ray)](../../../examples/rl/rlhf_sparse_nccl.py) - Qwen3 MoE per-expert checkpoint updates with one trainer GPU and two TP2/EP2 inference GPUs diff --git a/docs/usage/security.md b/docs/usage/security.md index ed10de055890..88f248a38f1e 100644 --- a/docs/usage/security.md +++ b/docs/usage/security.md @@ -324,6 +324,7 @@ vLLM enforces several configurable limits to prevent this: | `VLLM_P2P_MAX_PEERS` | `128` | Maximum concurrent P2P peer sessions. New peers (both outbound and inbound) are rejected when this limit is reached. | | `VLLM_P2P_HANDSHAKE_TIMEOUT_S` | `30` | Seconds to wait for a newly connected peer to complete the P2P handshake. Sessions that remain connected but not ready after this deadline are torn down. | | `VLLM_P2P_IDLE_TIMEOUT_S` | `300` | Seconds a P2P session may remain idle (no pending work, no messages) before it is eligible for eviction. Idle eviction runs only when remaining peer slots are within 8 of `VLLM_P2P_MAX_PEERS`. Set to `0` to disable idle eviction. | +| `VLLM_P2P_TRANSFER_STALL_TIMEOUT_S` | `20` | Seconds a P2P WRITE may stay in progress before its peer is treated as faulted: no new WRITEs to it, and the session is torn down. The stalled transfer's memory stays held until it stops. Set to `0` to disable. | Connections that fail (ZMQ socket errors, handshake failures) are handled gracefully — the request falls back to local prefill instead of crashing the engine. @@ -494,7 +495,7 @@ driver-side credentials from worker-side processes within the same Ray cluster. This assumption is consistent with -[Ray's own security model](https://docs.ray.io/en/latest/ray-core/security.html), +[Ray's own security model](https://docs.ray.io/en/latest/ray-security/index.html), which states that any user who can connect to a Ray cluster can run arbitrary code on any node in that cluster. In other words, Ray cluster access already implies full code execution on worker nodes, so restricting environment diff --git a/docs/usage/v1_guide.md b/docs/usage/v1_guide.md index d16db468955f..bdf3b4b2a139 100644 --- a/docs/usage/v1_guide.md +++ b/docs/usage/v1_guide.md @@ -139,6 +139,7 @@ Whisper is supported natively. Other encoder-decoder models are supported via th - **BART**: `BartForConditionalGeneration` is supported via the official [bart-plugin](https://github.com/vllm-project/bart-plugin). - **Florence-2**: `Florence2ForConditionalGeneration` is supported via the official [bart-plugin](https://github.com/vllm-project/bart-plugin). - **Nemotron 3.5 ASR**: `Nemotron3_5AsrForRNNT` is supported via [vllm-nemotron-asr-plugin](https://github.com/Sohaib-Ahmed21/vllm-nemotron-asr-plugin). +- **Parakeet TDT**: `ParakeetForTDT` is supported via [vllm-parakeet-plugin](https://github.com/Alex-Wengg/vllm-parakeet-plugin). For other encoder-decoder models (e.g., `MllamaForConditionalGeneration`), we recommend following a similar pattern by implementing support through the [plugin system](../design/plugin_system.md). diff --git a/examples/features/openai_batch/README.md b/examples/features/openai_batch/README.md index c1bb32937688..fff4acc2e601 100644 --- a/examples/features/openai_batch/README.md +++ b/examples/features/openai_batch/README.md @@ -13,9 +13,9 @@ The OpenAI batch file format consists of a series of json objects on new lines. Each line represents a separate request. See the [OpenAI package reference](https://platform.openai.com/docs/api-reference/batch/requestInput) for more details. ```{note} -We currently support `/v1/chat/completions`, `/v1/embeddings`, `/score`, `/rerank`, -`/v1/audio/transcriptions`, and `/v1/audio/translations` endpoints (`/v1/completions` -coming soon). Score and rerank match any URL ending in `/score` or `/rerank`, such as +We currently support `/v1/chat/completions`, `/v1/completions`, `/v1/embeddings`, +`/score`, `/rerank`, `/v1/audio/transcriptions`, and `/v1/audio/translations` +endpoints. Score and rerank match any URL ending in `/score` or `/rerank`, such as `/v1/score`. ``` diff --git a/examples/generate/multimodal/audio_language_offline.py b/examples/generate/multimodal/audio_language_offline.py index 8b2215d8c1eb..5866101973d0 100644 --- a/examples/generate/multimodal/audio_language_offline.py +++ b/examples/generate/multimodal/audio_language_offline.py @@ -251,6 +251,26 @@ def run_midashenglm(question: str, audio_count: int): ) +# Ming3-ASR +def run_ming_v3_asr(question: str, audio_count: int) -> ModelRequestData: + from vllm.model_executor.models.ming_v3_asr import ASR_INSTRUCTION + + assert audio_count == 1, "Ming3-ASR only supports single audio input per prompt" + engine_args = EngineArgs( + model="inclusionAI/Ming-3.0-asr-tiny", + trust_remote_code=True, + dtype="bfloat16", + max_model_len=8192, + max_num_seqs=4, + limit_mm_per_prompt={"audio": audio_count}, + enforce_eager=True, + ) + return ModelRequestData( + engine_args=engine_args, + prompt=ASR_INSTRUCTION + "", + ) + + # MiniCPM-O def run_minicpmo(question: str, audio_count: int) -> ModelRequestData: model_name = "openbmb/MiniCPM-o-2_6" @@ -503,6 +523,7 @@ def run_whisper(question: str, audio_count: int) -> ModelRequestData: "granite_speech": run_granite_speech, "kimi_audio": run_kimi_audio, "midashenglm": run_midashenglm, + "ming_v3_asr": run_ming_v3_asr, "minicpmo": run_minicpmo, "phi4_mm": run_phi4mm, "qwen2_audio": run_qwen2_audio, diff --git a/examples/generate/multimodal/vision_language_multi_image_offline.py b/examples/generate/multimodal/vision_language_multi_image_offline.py index 7565f1e42b51..d829503750dc 100644 --- a/examples/generate/multimodal/vision_language_multi_image_offline.py +++ b/examples/generate/multimodal/vision_language_multi_image_offline.py @@ -454,80 +454,6 @@ def load_internvl(question: str, image_urls: list[str]) -> ModelRequestData: ) -def load_keye_vl(question: str, image_urls: list[str]) -> ModelRequestData: - model_name = "Kwai-Keye/Keye-VL-8B-Preview" - - engine_args = EngineArgs( - model=model_name, - trust_remote_code=True, - max_model_len=8192, - max_num_seqs=5, - limit_mm_per_prompt={"image": len(image_urls)}, - ) - - placeholders = [{"type": "image", "image": url} for url in image_urls] - messages = [ - { - "role": "user", - "content": [ - *placeholders, - {"type": "text", "text": question}, - ], - }, - ] - - processor = AutoProcessor.from_pretrained(model_name, trust_remote_code=True) - - prompt = processor.apply_chat_template( - messages, tokenize=False, add_generation_prompt=True - ) - - image_data = [fetch_image(url) for url in image_urls] - - return ModelRequestData( - engine_args=engine_args, - prompt=prompt, - image_data=image_data, - ) - - -def load_keye_vl1_5(question: str, image_urls: list[str]) -> ModelRequestData: - model_name = "Kwai-Keye/Keye-VL-1_5-8B" - - engine_args = EngineArgs( - model=model_name, - trust_remote_code=True, - max_model_len=32768, - max_num_seqs=5, - limit_mm_per_prompt={"image": len(image_urls)}, - ) - - placeholders = [{"type": "image", "image": url} for url in image_urls] - messages = [ - { - "role": "user", - "content": [ - *placeholders, - {"type": "text", "text": question}, - ], - }, - ] - - processor = AutoProcessor.from_pretrained(model_name, trust_remote_code=True) - - prompt = processor.apply_chat_template( - messages, tokenize=False, add_generation_prompt=True - ) - - image_data = [fetch_image(url) for url in image_urls] - - return ModelRequestData( - engine_args=engine_args, - prompt=prompt, - image_data=image_data, - ) - - def load_kimi_vl(question: str, image_urls: list[str]) -> ModelRequestData: model_name = "moonshotai/Kimi-VL-A3B-Instruct" @@ -1349,8 +1275,6 @@ def load_molmo2(question: str, image_urls: list[str]) -> ModelRequestData: "idefics3": load_idefics3, "interns1": load_interns1, "internvl_chat": load_internvl, - "keye_vl": load_keye_vl, - "keye_vl1_5": load_keye_vl1_5, "kimi_vl": load_kimi_vl, "llama4": load_llama4, "llava": load_llava, diff --git a/examples/generate/multimodal/vision_language_offline.py b/examples/generate/multimodal/vision_language_offline.py index c1448f1cbe87..02602f3e4fda 100644 --- a/examples/generate/multimodal/vision_language_offline.py +++ b/examples/generate/multimodal/vision_language_offline.py @@ -21,6 +21,7 @@ from vllm.lora.request import LoRARequest from vllm.multimodal.image import convert_image_mode from vllm.platforms import current_platform +from vllm.transformers_utils.chat_templates import get_chat_template_fallback_path from vllm.utils.argparse_utils import FlexibleArgumentParser @@ -935,80 +936,6 @@ def run_kanana_v(questions: list[str], modality: str) -> ModelRequestData: ) -# Keye-VL -def run_keye_vl(questions: list[str], modality: str) -> ModelRequestData: - model_name = "Kwai-Keye/Keye-VL-8B-Preview" - - mm_limit = {"image": 1, "video": 1} if modality == "image+video" else {modality: 1} - engine_args = EngineArgs( - model=model_name, - max_model_len=8192, - trust_remote_code=True, - limit_mm_per_prompt=mm_limit, - ) - - image_placeholder = "<|vision_start|><|image_pad|><|vision_end|>" - video_placeholder = "<|vision_start|><|video_pad|><|vision_end|>" - - if modality == "image": - placeholder = image_placeholder - elif modality == "video": - placeholder = video_placeholder - elif modality == "image+video": - placeholder = image_placeholder + video_placeholder - - prompts = [ - ( - f"<|im_start|>user\n{placeholder}" - f"{question}<|im_end|>\n" - "<|im_start|>assistant\n" - ) - for question in questions - ] - - return ModelRequestData( - engine_args=engine_args, - prompts=prompts, - ) - - -# Keye-VL-1.5 -def run_keye_vl1_5(questions: list[str], modality: str) -> ModelRequestData: - model_name = "Kwai-Keye/Keye-VL-1.5-8B" - - mm_limit = {"image": 1, "video": 1} if modality == "image+video" else {modality: 1} - engine_args = EngineArgs( - model=model_name, - max_model_len=8192, - trust_remote_code=True, - limit_mm_per_prompt=mm_limit, - ) - - image_placeholder = "<|vision_start|><|image_pad|><|vision_end|>" - video_placeholder = "<|vision_start|><|video_pad|><|vision_end|>" - - if modality == "image": - placeholder = image_placeholder - elif modality == "video": - placeholder = video_placeholder - elif modality == "image+video": - placeholder = image_placeholder + video_placeholder - - prompts = [ - ( - f"<|im_start|>user\n{placeholder}" - f"{question}<|im_end|>\n" - "<|im_start|>assistant\n" - ) - for question in questions - ] - - return ModelRequestData( - engine_args=engine_args, - prompts=prompts, - ) - - # Kimi-VL def run_kimi_vl(questions: list[str], modality: str) -> ModelRequestData: assert modality == "image" @@ -1528,6 +1455,36 @@ def run_openpangu_vl(questions: list[str], modality: str) -> ModelRequestData: ) +# OpenVLA +def run_openvla(questions: list[str], modality: str) -> ModelRequestData: + assert modality == "image" + + model_name = "openvla/openvla-7b" + chat_template = get_chat_template_fallback_path("openvla", model_name) + assert chat_template is not None + + tokenizer = AutoTokenizer.from_pretrained(model_name, trust_remote_code=True) + messages = [{"role": "user", "content": "pick up the red block"}] + prompt = tokenizer.apply_chat_template( + messages, + chat_template=chat_template.read_text(), + tokenize=False, + ).removeprefix(tokenizer.bos_token) + prompts = [prompt for _ in questions] + engine_args = EngineArgs( + model=model_name, + trust_remote_code=True, + max_model_len=512, + limit_mm_per_prompt={modality: 1}, + ) + + return ModelRequestData( + engine_args=engine_args, + prompts=prompts, + sampling_params=SamplingParams(temperature=0.0, max_tokens=7), + ) + + # Ovis def run_ovis(questions: list[str], modality: str) -> ModelRequestData: assert modality == "image" @@ -2235,8 +2192,6 @@ def run_step_vl(questions: list[str], modality: str) -> ModelRequestData: "interns1_pro": run_interns1_pro, "internvl_chat": run_internvl, "kanana_v": run_kanana_v, - "keye_vl": run_keye_vl, - "keye_vl1_5": run_keye_vl1_5, "kimi_vl": run_kimi_vl, "kimi_k25": run_kimi_k25, "lightonocr": run_lightonocr, @@ -2255,6 +2210,7 @@ def run_step_vl(questions: list[str], modality: str) -> ModelRequestData: "nemotron_vl": run_nemotron_vl, "NVLM_D": run_nvlm_d, "openpangu_vl": run_openpangu_vl, + "openvla": run_openvla, "ovis": run_ovis, "ovis2_5": run_ovis2_5, "paddleocr_vl": run_paddleocr_vl, diff --git a/examples/pooling/embed/openai_embedding_long_text/README.md b/examples/pooling/embed/openai_embedding_long_text/README.md index 2ed04f1d91f2..cafe4975a58b 100644 --- a/examples/pooling/embed/openai_embedding_long_text/README.md +++ b/examples/pooling/embed/openai_embedding_long_text/README.md @@ -46,7 +46,7 @@ The key parameters for chunked processing are in the `--pooler-config`: ```json { - "pooling_type": "auto", + "pooling_type": "MEAN", "use_activation": true, "enable_chunked_processing": true, "max_embed_len": 3072000 @@ -54,7 +54,7 @@ The key parameters for chunked processing are in the `--pooler-config`: ``` !!! note - `pooling_type` sets the model's own pooling strategy for processing within each chunk. The cross-chunk aggregation automatically uses MEAN strategy when input exceeds the model's native maximum length. + `pooling_type` sets the model's own pooling strategy for processing within each chunk and must be one of `CLS`, `LAST` or `MEAN`. `service.sh` accepts `POOLING_TYPE=auto` and resolves it to one of these before building the config. The cross-chunk aggregation automatically uses MEAN strategy when input exceeds the model's native maximum length. #### Chunked Processing Behavior diff --git a/examples/pooling/embed/openai_embedding_long_text/service.sh b/examples/pooling/embed/openai_embedding_long_text/service.sh index 68950d04ee14..1c68b00f9177 100644 --- a/examples/pooling/embed/openai_embedding_long_text/service.sh +++ b/examples/pooling/embed/openai_embedding_long_text/service.sh @@ -20,8 +20,7 @@ API_KEY=${API_KEY:-"your-api-key"} # Enhanced pooling configuration with model-specific defaults POOLING_TYPE=${POOLING_TYPE:-"auto"} # auto, MEAN, CLS, LAST -export VLLM_ENABLE_CHUNKED_PROCESSING=true -export CUDA_VISIBLE_DEVICES=2,3,4,5 +ENABLE_CHUNKED_PROCESSING=true echo "🚀 Starting vLLM Embedding Server with Enhanced Chunked Processing" echo "==================================================================" @@ -68,7 +67,7 @@ echo "📋 Configuration:" echo " - Model: $MODEL_NAME" echo " - Port: $PORT" echo " - GPU Count: $GPU_COUNT" -echo " - Enhanced Chunked Processing: ${VLLM_ENABLE_CHUNKED_PROCESSING}" +echo " - Enhanced Chunked Processing: ${ENABLE_CHUNKED_PROCESSING}" echo " - Max Embed Length: ${MAX_EMBED_LEN} tokens" echo " - Native Pooling Type: $POOLING_TYPE + Normalization" echo " - Cross-chunk Aggregation: MEAN (automatic)" @@ -96,7 +95,7 @@ echo "" echo "🔧 Starting server with enhanced chunked processing configuration..." # Build pooler config JSON -POOLER_CONFIG="{\"pooling_type\": \"$POOLING_TYPE\", \"use_activation\": true, \"enable_chunked_processing\": ${VLLM_ENABLE_CHUNKED_PROCESSING}, \"max_embed_len\": ${MAX_EMBED_LEN}}" +POOLER_CONFIG="{\"pooling_type\": \"$POOLING_TYPE\", \"use_activation\": true, \"enable_chunked_processing\": ${ENABLE_CHUNKED_PROCESSING}, \"max_embed_len\": ${MAX_EMBED_LEN}}" # Start vLLM server with enhanced chunked processing vllm serve "$MODEL_NAME" \ diff --git a/examples/rl/rlhf_nccl_multi_source.py b/examples/rl/rlhf_nccl_multi_source.py new file mode 100644 index 000000000000..730d4443528c --- /dev/null +++ b/examples/rl/rlhf_nccl_multi_source.py @@ -0,0 +1,207 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +"""Dense NCCL weight transfer from two disjoint trainer sources. + +The example uses four GPUs: + +* GPUs 0-1 run two trainer actors. Each actor exposes the parameters owned by + one logical pipeline stage. +* GPUs 2-3 run one tensor-parallel vLLM engine. + +Each stage declares itself the owner of its parameters in `ParamMeta.owners`, +so both trainer ranks join the weight-transfer communicator and each +broadcasts its own parameters. Rank 0 drives the control plane: one +`update_weights` per stage, within one start/finish lifecycle. + +For simplicity, both trainer actors load the small model before filtering their +parameter streams. A real pipeline-parallel trainer would already hold only its +stage-local parameters; the transfer behavior is the same. + +This example was written for four GPUs with at least 8 GiB each. +""" + +import os +import time +from functools import cached_property + +import ray +import torch +from huggingface_hub import snapshot_download +from transformers import AutoModelForCausalLM + +from vllm import LLM, SamplingParams +from vllm.config import WeightTransferConfig +from vllm.distributed.weight_transfer import ( + ParamMeta, + RayVLLMWeightSyncClient, + WeightSource, + WeightTransferTrainerFactory, +) +from vllm.distributed.weight_transfer.nccl_engine import NCCLTrainerInitInfo +from vllm.utils.network_utils import get_ip, get_open_port + +MODEL_NAME = "Qwen/Qwen3-0.6B" +NUM_SOURCES = 2 +INFERENCE_TP_SIZE = 2 + + +class StageSource(WeightSource): + """Expose the checkpoint parameters assigned to one logical stage.""" + + def __init__( + self, + model: torch.nn.Module, + stage_rank: int, + num_stages: int, + num_layers: int, + ) -> None: + self.model = model + self.stage_rank = stage_rank + self.num_stages = num_stages + self.num_layers = num_layers + + def _owner(self, name: str) -> int: + parts = name.split(".") + for index, part in enumerate(parts[:-1]): + if part == "layers" and parts[index + 1].isdigit(): + layer = int(parts[index + 1]) + return min( + layer * self.num_stages // self.num_layers, self.num_stages - 1 + ) + return 0 if "embed_tokens" in name else self.num_stages - 1 + + @cached_property + def metadata(self) -> list[ParamMeta]: + return [ + ParamMeta( + name, param.dtype, tuple(param.shape), owners=(self._owner(name),) + ) + for name, param in self.model.named_parameters() + ] + + def iter_full(self): + for name, param in self.model.named_parameters(): + if self._owner(name) == self.stage_rank: + yield name, param + + +class MyLLM(LLM): + """Allow the vLLM Ray executor to allocate its own worker GPUs.""" + + def __init__(self, *args, **kwargs): + os.environ.pop("CUDA_VISIBLE_DEVICES", None) + super().__init__(*args, **kwargs) + + def ready(self): + return True + + +@ray.remote(num_cpus=1, num_gpus=1) +class TrainerSource: + def __init__(self, model_path: str, source_rank: int): + self.source_rank = source_rank + torch.accelerator.set_device_index(0) + self.model = AutoModelForCausalLM.from_pretrained( + model_path, torch_dtype=torch.bfloat16 + ).cuda() + self.engine = None + + def ready(self): + return self.source_rank + + def setup_engine( + self, + llm_handle, + master_address: str, + master_port: int, + world_size: int, + ): + source = StageSource( + self.model, + self.source_rank, + NUM_SOURCES, + self.model.config.num_hidden_layers, + ) + self.engine = WeightTransferTrainerFactory.trainer_init( + init_info=NCCLTrainerInitInfo( + master_address=master_address, + master_port=master_port, + world_size=world_size, + rank=self.source_rank, + packed=True, + ), + client=RayVLLMWeightSyncClient(llm_handle), + source=source, + ) + + def send_weights(self): + self.engine.send_weights() + + +def generate(llm, prompts: list[str]) -> list[list[int]]: + outputs = ray.get( + llm.generate.remote( + prompts, + SamplingParams(temperature=0, max_tokens=16), + ) + ) + return [output.outputs[0].token_ids for output in outputs] + + +def main(): + ray.init() + model_path = snapshot_download(MODEL_NAME) + + sources = [ + TrainerSource.remote(model_path, source_rank) + for source_rank in range(NUM_SOURCES) + ] + ray.get([source.ready.remote() for source in sources]) + + llm = ray.remote(num_cpus=0, num_gpus=0)(MyLLM).remote( + model=model_path, + enforce_eager=True, + tensor_parallel_size=INFERENCE_TP_SIZE, + distributed_executor_backend="ray", + weight_transfer_config=WeightTransferConfig(backend="nccl"), + load_format="dummy", + ) + ray.get(llm.ready.remote()) + num_workers = ray.get(llm.get_world_size.remote()) + + prompts = ["The capital of France is", "The future of AI is"] + before = generate(llm, prompts) + + master_address = get_ip() + master_port = get_open_port() + world_size = NUM_SOURCES + num_workers + t0 = time.perf_counter() + ray.get( + [ + source.setup_engine.remote( + llm, + master_address, + master_port, + world_size, + ) + for source in sources + ] + ) + print(f"[sync] engine setup took {time.perf_counter() - t0:.2f}s") + + t0 = time.perf_counter() + ray.get([source.send_weights.remote() for source in sources]) + print(f"[sync] iter 0 took {time.perf_counter() - t0:.3f}s") + after = generate(llm, prompts) + t0 = time.perf_counter() + ray.get([source.send_weights.remote() for source in sources]) + print(f"[sync] iter 1 took {time.perf_counter() - t0:.3f}s") + repeated = generate(llm, prompts) + + assert after != before, "generation did not change after weight transfer" + assert repeated == after, "repeating the same transfer changed generation" + print("Multi-source NCCL weight transfer completed successfully.") + + +if __name__ == "__main__": + main() diff --git a/pyproject.toml b/pyproject.toml index 46c836043ca2..386a029f1e74 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -226,8 +226,15 @@ FoPE = "FoPE" k_ot = "k_ot" view_seperator = "view_seperator" inverse_std_variences = "inverse_std_variences" +# Muse Spark K3 monokernel: threads an expert +TPE = "TPE" +stride_bse = "stride_bse" [tool.typos.default.extend-words] +# Muse Spark monokernel: shuffled-weight / scale tags, o_proj flag region +shs = "shs" +shw = "shw" +opf = "opf" certifi = "certifi" MoNE = "MoNE" mone = "mone" @@ -254,10 +261,15 @@ pard = "pard" AKS = "AKS" ba = "ba" fo = "fo" +ot = "ot" nd = "nd" eles = "eles" datas = "datas" ser = "ser" +# numpy flags.writeable +writeable = "writeable" +# HF CLIP weight name +layrnorm = "layrnorm" ure = "ure" VALU = "VALU" # Walsh-Hadamard Transform diff --git a/recipes/README.md b/recipes/README.md index d96c9096ce96..5fd08935b4fc 100644 --- a/recipes/README.md +++ b/recipes/README.md @@ -9,9 +9,10 @@ The site, the command builder, and the JSON API are built by [vllm-project/recipes](https://github.com/vllm-project/recipes). That repository owns the schema and the shared vocabulary (hardware profiles, serving strategies, KV-offload deployments) and pins this directory to a -commit. Omni (image, video, audio) recipes stay in vllm-project/recipes for -now (moving to vllm-omni), and Ascend NPU content is inline in these recipes -(moving to vllm-ascend overlays). +commit. Omni (image, video, audio) recipes live in vllm-project/vllm-omni, and +Ascend NPU content lives in vllm-project/vllm-ascend as overlays that merge into +these recipes when the site is built. Don't add Ascend hardware here: open those +changes against `recipes/overlays/` in vllm-ascend. ## Editing a recipe diff --git a/recipes/models/MiniMaxAI/MiniMax-M3.1-Flash.yaml b/recipes/models/MiniMaxAI/MiniMax-M3.1-Flash.yaml index 25ea0663eff1..14a88fdf6251 100644 --- a/recipes/models/MiniMaxAI/MiniMax-M3.1-Flash.yaml +++ b/recipes/models/MiniMaxAI/MiniMax-M3.1-Flash.yaml @@ -4,7 +4,7 @@ meta: provider: "MiniMax" description: "MiniMax M3.1-Flash vision-language MoE (~428B total / 23B active) with MSA sparse attention throughout and a built-in DSpark draft; quantization-aware trained, MXFP8 dense layers and NVFP4 experts. Served TP4 on NVIDIA Blackwell or AMD MI355X, prefill/decode disaggregated as TP4 + TP4 on MI355X or DEP2 + DEP4 on GB200/GB300, and TP8 on NVIDIA Hopper." date_added: 2026-10-23 - date_updated: 2026-10-27 + date_updated: 2026-10-30 tasks: - text - multimodal @@ -378,6 +378,625 @@ strategy_overrides: VLLM_KV_CACHE_LAYOUT: "BLHNC" presets: + # GB300 AgentX under Dynamo: aggregated TP4 workers, and DEP2 prefill + # workers feeding one DEP decode worker. Each preset restates its engine + # flags, so it does not depend on the pd_cluster GB300 block above. + gb300_agentx_agg: + label: "AgentX TP4 (Dynamo)" + hardware: gb300 + strategy: single_node_tp + variants: [default] + workload: "Agentic (InferenceX AgentX)" + description: "Aggregated TP4 vLLM workers, one per 4-GPU GB300 tray, behind the Dynamo KV router with session affinity. Seven-token DSpark draft on FLASH_ATTN and prefill CUDA graphs up to 8192 tokens. KV stays in HBM, or spills to host DRAM through SimpleCPUOffloadConnector." + source: { label: "InferenceX #4631", href: "https://github.com/SemiAnalysisAI/InferenceX/pull/4631", key: "minimaxm31flash-fp4-gb300-dynamo-vllm-agentic-dspark-agg", revision: 5d3aa21b680eb8fb66a72386bd8e85e435e15a8c } + image: + vllm: "vllm/vllm-openai:minimax-m31-flash-arm64-cu134" + dynamo: "1.6.0.dev20261023" + pip: ["ai-dynamo-runtime==1.6.0.dev20261023", "ai-dynamo==1.6.0.dev20261023"] + tp: 4 + select: + features: { text_only: true } + args: + - "--enable-prefix-caching" + - "--max-model-len" + - "1048576" + - "--max-num-batched-tokens" + - "32768" + env: + # Pins the prefix-block hashes, so they match across processes and restarts. + PYTHONHASHSEED: "0" + # A cold start (JIT builds, graph capture, autotune) takes more than 10 minutes. + VLLM_ENGINE_READY_TIMEOUT_S: "7200" + VLLM_HTTP_TIMEOUT_KEEP_ALIVE: "600" + VLLM_CHAT_TEMPLATE_RENDER_TIMEOUT: "600" + json_args: + "--compilation-config": { cudagraph_capture_sizes: [8, 16, 24, 32, 40, 48, 56, 64, 72, 80, 88, 96, 104, 112, 120, 128, 136, 144, 152, 160, 168, 176, 184, 192, 200, 208, 216, 224, 232, 240, 248, 256, 320, 384, 448, 512, 576, 640, 704, 768, 832, 896, 960, 1024, 1536, 2048, 3072, 4096, 6144, 8192] } + "--speculative-config": { attention_backend: "FLASH_ATTN" } + # --max-num-seqs is about twice the sessions a worker serves. + points: + gpu_kv: + label: "GPU-resident KV" + description: "KV cache in HBM only: --max-num-seqs 32 for about 16 sessions per worker, 64 for up to 32." + default: true + args: ["--max-num-seqs", "32", "--gpu-memory-utilization", "0.9"] + select: { kv_offload: "off" } + dram_kv: + label: "Host-DRAM KV offload" + description: "Adds a host-DRAM KV tier (860 GB per worker) for about 40-56 sessions per worker." + args: ["--max-num-seqs", "96", "--gpu-memory-utilization", "0.86"] + env: { VLLM_USE_SIMPLE_KV_OFFLOAD: "1" } + select: { kv_offload: simple } + json_args: + "--kv-transfer-config": { kv_connector_extra_config: { cpu_bytes_to_use_per_rank: null, cpu_bytes_to_use: 860000000000, lazy_offload: true } } + runbook: | + The GB300 aggregated AgentX lane in [SemiAnalysis InferenceX](https://inferencex.semianalysis.com/) (`minimaxm31flash-fp4-gb300-dynamo-vllm-agentic-dspark-agg`, [SemiAnalysisAI/InferenceX#4631](https://github.com/SemiAnalysisAI/InferenceX/pull/4631)) serves the default checkpoint text-only, with one TP4 `dynamo.vllm` worker per 4-GPU tray behind the Dynamo frontend. Add trays to scale out: the KV router spreads conversations across them. The lane measured 29.4k tok/s/GPU at 417 tok/s/user with 16 sessions per tray (GPU-resident KV, `--max-num-seqs 32`), and 96.6k tok/s/GPU at 185 tok/s/user with 56 sessions per tray (host-DRAM KV offload, `--max-num-seqs 96`). + + **Image and versions.** The lane used an arm64 CUDA 13 build of the `vllm/vllm-openai:minimax-m31-flash` image (tag `minimax-m31-flash-arm64-cu134`); any build of that image family for GB300 will do. Dynamo `1.6.0.dev20261023` was installed on top: `pip install --extra-index-url https://pypi.nvidia.com ai-dynamo-runtime==1.6.0.dev20261023 ai-dynamo==1.6.0.dev20261023`. + + **Tuning rules.** + + - `--max-num-seqs` is about twice the sessions one worker serves: 32 up to 16 sessions, 64 at 32-48, 96 at 56. + - **GPU-resident KV** (`--gpu-memory-utilization 0.9`) is enough up to about 32 sessions per tray. Beyond that, multi-turn contexts outgrow HBM and evicted prefixes are recomputed. + - **Host-DRAM KV offload** adds `SimpleCPUOffloadConnector` with lazy offload (`VLLM_USE_SIMPLE_KV_OFFLOAD=1`). `cpu_bytes_to_use` is the budget for the whole worker, split across its four ranks: 860 GB pins 215 GB per rank. Size it from the host DRAM that is free after the engine processes are up. Lower `--gpu-memory-utilization` to 0.86 to leave room for the transfer buffers. + - The CUDA graph sizes go up to 8192 tokens, so prefill chunks of up to 8192 tokens also replay graphs. `--max-num-batched-tokens 32768` still allows larger mixed steps, which run piecewise. + + **Running under Dynamo.** Start etcd and NATS (32 MB max payload) once: + + ```bash + etcd --listen-client-urls http://0.0.0.0:2379 --advertise-client-urls http://:2379 + printf 'max_payload: 33554432\n' > nats.conf && nats-server -c nats.conf + ``` + + On each tray, export `ETCD_ENDPOINTS=http://:2379` and `NATS_SERVER=nats://:4222` together with the generated environment and `DYN_TCP_CONNECT_TIMEOUT=120`, `DYN_BACKEND_ADMISSION_ENGINE_REQUEST_LIMIT=1024`, `DYN_BACKEND_ADMISSION_ENGINE_REQUEST_WAITING_LIMIT=1024` and `DYN_BACKEND_ADMISSION_REQUEST_QUEUE_LIMIT=2048`. Then run the generated flags with `python3 -m dynamo.vllm --model MiniMaxAI/MiniMax-M3.1-Flash --served-model-name MiniMaxAI/MiniMax-M3.1-Flash --trust-remote-code` in place of `vllm serve MiniMaxAI/MiniMax-M3.1-Flash`. `dynamo.vllm` rejects vLLM's API-server flags, so leave out `--tool-call-parser minimax_m3 --enable-auto-tool-choice` and add: + + ```bash + --dyn-tool-call-parser minimax_m3 --dyn-reasoning-parser minimax_m3 \ + --kv-events-config '{"publisher":"zmq","topic":"kv-events","enable_kv_cache_events":true,"endpoint":"tcp://*:5200"}' + ``` + + Start the frontend: + + ```bash + export ETCD_ENDPOINTS=http://:2379 NATS_SERVER=nats://:4222 + export DYN_ROUTER_ACTIVE_REQUEST_EXPIRY_SECS=3600 DYN_TCP_CONNECT_TIMEOUT=120 DYN_TOKENIZER=fastokens + python3 -m dynamo.frontend --http-port 8000 --router-mode kv --router-session-affinity-ttl-secs 14400 + ``` + + `DYN_TOKENIZER=fastokens` switches the frontend to the fastokens tokenizer backend that ships with Dynamo. With 100K+ token agentic prompts, the default tokenizer spends about 150 ms per request on the frontend (up to 600 ms at p90), all of it added to TTFT; fastokens takes about 1 ms. + + The KV router uses the workers' KV events to send each request to the worker that holds its prefix. Session affinity keeps a conversation on that worker for up to four hours; send a stable conversation ID in the `X-Dynamo-Session-ID` request header. + + The InferenceX throughput points replaced block verification with synthetic rejection at the committed golden DSpark acceptance length (4.04), so every point is measured at the same acceptance rate. Production serving keeps the default rejection sampler (`rejection_sample_method: block`), as in the command above. + + gb300_agentx_pd: + label: "AgentX PD (Dynamo)" + hardware: gb300 + strategy: pd_cluster + variants: [default] + workload: "Agentic (InferenceX AgentX)" + description: "DEP2 prefill workers, two per GB300 tray, feed one DEP8 decode worker (throughput) or DEP16 (lower latency) over NIXL. Both roles share a Mooncake KV store in host DRAM. The Dynamo KV router routes with session affinity. Seven-token DSpark draft on FLASHINFER_TS, DeepGEMM MegaMoE on both roles." + source: { label: "InferenceX #4631", href: "https://github.com/SemiAnalysisAI/InferenceX/pull/4631", key: "minimaxm31flash-fp4-gb300-dynamo-vllm-agentic-dspark-disagg", revision: 5d3aa21b680eb8fb66a72386bd8e85e435e15a8c } + image: + vllm: "vllm/vllm-openai:minimax-m31-flash-arm64-cu134" + dynamo: "1.6.0.dev20261023" + pip: ["ai-dynamo-runtime==1.6.0.dev20261023", "ai-dynamo==1.6.0.dev20261023", "mooncake-transfer-engine-cuda13==0.3.13.post1"] + # Dynamo workers take engine flags only; the tool-call parser moves to + # --dyn-tool-call-parser (see the runbook). + select: + pd_router: dynamo + features: { text_only: true, tool_calling: false } + args: + - "--enable-expert-parallel" + - "--moe-backend" + - "deep_gemm_mega_moe" + - "--numa-bind" + - "--enable-prefix-caching" + - "--max-model-len" + - "1048576" + # The RDMA names are a GB300 tray's four NICs; use the host's own. + env: + PYTHONHASHSEED: "0" + VLLM_USE_V2_MODEL_RUNNER: "1" + VLLM_ENGINE_READY_TIMEOUT_S: "7200" + VLLM_HTTP_TIMEOUT_KEEP_ALIVE: "600" + NCCL_NET_GDR_C2C: "1" + NCCL_IB_HCA: "mlx5_0,mlx5_1,mlx5_2,mlx5_3" + NVSHMEM_REMOTE_TRANSPORT: "none" + NVSHMEM_ENABLE_NIC_PE_MAPPING: "1" + NVSHMEM_HCA_PE_MAPPING: "mlx5_0:1:1,mlx5_1:1:1,mlx5_2:1:1,mlx5_3:1:1" + UCX_TLS: "rc,cuda_copy" + UCX_NET_DEVICES: "mlx5_0:1,mlx5_1:1,mlx5_2:1,mlx5_3:1" + UCX_CUDA_IPC_ENABLE_MNNVL: "n" + UCX_MEMTYPE_CACHE: "n" + UCX_MEMTYPE_REG_WHOLE: "n" + UCX_RNDV_PIPELINE_ERROR_HANDLING: "y" + NIXL_UCX_SGL_ENABLE: "1" + WITH_NVIDIA_PEERMEM: "0" + MC_SLICE_SIZE: "1048576" + MC_MAX_MR_SIZE: "4294967296" + MC_ENABLE_DEST_DEVICE_AFFINITY: "1" + DYN_TCP_CONNECT_TIMEOUT: "120" + DYN_BACKEND_ADMISSION_ENGINE_REQUEST_LIMIT: "1024" + DYN_BACKEND_ADMISSION_ENGINE_REQUEST_WAITING_LIMIT: "1024" + DYN_BACKEND_ADMISSION_REQUEST_QUEUE_LIMIT: "2048" + roles: + prefill: + # One DEP2 worker on two GPUs; two per tray, as many trays as the load needs. + nodes: 1 + parallelism: dep + args: + - "--data-parallel-size" + - "2" + - "--data-parallel-size-local" + - "2" + - "--max-num-seqs" + - "64" + - "--max-num-batched-tokens" + - "32768" + - "--long-prefill-token-threshold" + - "4096" + - "--gpu-memory-utilization" + - "0.85" + - "--compilation-config" + - '{"cudagraph_mode":"FULL_AND_PIECEWISE"}' + - "--max-cudagraph-capture-size" + - "2048" + - "--kv-events-config" + - '{"publisher":"zmq","topic":"kv-events","enable_kv_cache_events":true,"endpoint":"tcp://*:5200"}' + - "--kv-transfer-config" + - '{"kv_connector":"MultiConnector","kv_role":"kv_both","kv_load_failure_policy":"recompute","kv_connector_extra_config":{"connectors":[{"kv_connector":"NixlConnector","kv_role":"kv_both","kv_load_failure_policy":"fail","kv_buffer_device":"cuda","kv_connector_extra_config":{"backends":["UCX"],"enforce_handshake_compat":false,"kv_lease_duration":120}},{"kv_connector":"MooncakeStoreConnector","kv_role":"kv_both","kv_load_failure_policy":"recompute","kv_connector_extra_config":{"load_async":true,"lookup_async":true,"enable_offload":false}}]}}' + env: + VLLM_MOONCAKE_LOAD_RECV_THREADS: "8" + decode: + nodes: 2 + parallelism: dep + # 48 sequences per DP rank; decode graphs cover 48 x (1 + 7) tokens. + # Decode also writes the KV it generates into the Mooncake store. + remove_args: ["--no-enable-prefix-caching"] + args: + - "--max-num-seqs" + - "48" + - "--max-num-batched-tokens" + - "8192" + - "--watermark" + - "0.05" + - "--gpu-memory-utilization" + - "0.95" + - "--compilation-config" + - '{"cudagraph_mode":"FULL_DECODE_ONLY"}' + - "--max-cudagraph-capture-size" + - "384" + - "--numa-bind-nodes" + - "0" + - "0" + - "1" + - "1" + - "--kv-transfer-config" + - '{"kv_connector":"MultiConnector","kv_role":"kv_both","kv_load_failure_policy":"recompute","kv_connector_extra_config":{"connectors":[{"kv_connector":"NixlConnector","kv_role":"kv_both","kv_load_failure_policy":"fail","kv_buffer_device":"cuda","kv_connector_extra_config":{"backends":["UCX"],"enforce_handshake_compat":false,"kv_lease_duration":120}},{"kv_connector":"MooncakeStoreConnector","kv_role":"kv_both","kv_load_failure_policy":"recompute","kv_connector_extra_config":{"load_async":true,"lookup_async":true,"enable_offload":false,"save_decode_cache":true}}]}}' + env: + VLLM_MOONCAKE_LOAD_RECV_THREADS: "4" + VLLM_MAX_ASYNC_KV_ADMITS_PER_STEP: "4" + VLLM_RANDOMIZE_DP_DUMMY_INPUTS: "1" + VLLM_ALLREDUCE_USE_SYMM_MEM: "0" + NCCL_CUMEM_ENABLE: "1" + NCCL_MNNVL_ENABLE: "1" + NCCL_NVLS_ENABLE: "1" + points: + dep8: + label: "DEP8 decode (throughput)" + description: "One DEP8 decode worker across two trays." + default: true + dep16: + label: "DEP16 decode (lower latency)" + description: "One DEP16 decode worker across four trays." + roles: { decode: { nodes: 4 } } + runbook: | + The GB300 disaggregated AgentX lane in [SemiAnalysis InferenceX](https://inferencex.semianalysis.com/) (`minimaxm31flash-fp4-gb300-dynamo-vllm-agentic-dspark-disagg`, [SemiAnalysisAI/InferenceX#4631](https://github.com/SemiAnalysisAI/InferenceX/pull/4631)) serves the default checkpoint text-only with Dynamo and vLLM. Six prefill workers, each DEP2 on two GPUs (two per 4-GPU tray, three trays), feed one decode worker that is DEP8 across two trays or DEP16 across four. KV moves from prefill to decode over NIXL (UCX over RDMA), and every rank contributes host DRAM to a shared Mooncake KV store, so a returning conversation's prefix is loaded from the store instead of recomputed. Both roles use expert parallelism with DeepGEMM MegaMoE, attention data parallelism, the V2 model runner and the seven-token DSpark draft on `FLASHINFER_TS`. + + | Prefill workers (DEP2) | Decode | GPUs | Sessions | tok/s/GPU | tok/s/user | TTFT p90 | E2E latency p90 | + |---:|---|---:|---:|---:|---:|---:|---:| + | 6 | DEP8 | 20 | 280 | 93.1k | 240 | 2.7 s | 12.2 s | + | 6 | DEP8 | 20 | 320 | 100.5k | 229 | 3.3 s | 13.8 s | + | 6 | DEP8 | 20 | 360 | 107.9k | 224 | 4.2 s | 15.9 s | + | 6 | DEP8 | 20 | 400 | 114.5k | 219 | 5.9 s | 18.4 s | + | 6 | DEP16 | 28 | 300 | 75.0k | 296 | 1.8 s | 9.3 s | + | 6 | DEP16 | 28 | 360 | 85.8k | 285 | 2.4 s | 10.3 s | + + The preset renders one prefill worker and the decode worker. Run more prefill workers with the same command. Two workers share a tray on GPUs 0-1 and 2-3 (`--device-ids 0,1` and `--device-ids 2,3` on `dynamo.vllm`). Give each its own `--data-parallel-rpc-port`, `VLLM_NIXL_SIDE_CHANNEL_PORT` and `--kv-events-config` endpoint port. + + **Image and versions.** The lane used an arm64 CUDA 13 build of the `vllm/vllm-openai:minimax-m31-flash` image (tag `minimax-m31-flash-arm64-cu134`), with Dynamo `1.6.0.dev20261023` and Mooncake `mooncake-transfer-engine-cuda13==0.3.13.post1` installed on every worker and on the Mooncake master host: `pip install --extra-index-url https://pypi.nvidia.com ai-dynamo-runtime==1.6.0.dev20261023 ai-dynamo==1.6.0.dev20261023 mooncake-transfer-engine-cuda13==0.3.13.post1`. + + ### Tuning rules + + - **Prefill pool.** The prefill pool sets TTFT, so scale prefill workers with load. The lane's points kept TTFT p90 between 1.8 and 5.9 s with one DEP2 prefill worker per 47-67 concurrent agentic sessions, so 93-133 sessions per prefill tray (six workers at 280-400 sessions). Add prefill workers when TTFT p90 climbs while decode still has room. Two DEP2 workers per tray give the router twice as many prefill targets as one DEP4 worker. + - **DEP8 or DEP16 decode.** DEP8 gives the most tokens per GPU: up to 50 sessions per decode GPU (400 sessions) at 219 tok/s/user. DEP16 halves the batch per rank and gives about 23% more tok/s/user (296 at 300 sessions, against 240 at 280 on DEP8) at about 19% fewer tokens per GPU, and keeps E2E latency p90 near 10 s up to 360 sessions. `--max-num-seqs 48` per DP rank is a cap, not the operating point. The decode graph cap is `--max-num-seqs` x (1 + 7 draft tokens) = 384. `--watermark 0.05` keeps 5% of the decode KV cache free, so extra requests wait in the queue instead of preempting running ones. Keep decode `--max-num-batched-tokens` at 8192: a preempted request recomputes its context in chunks of that size. + - **Prefill batching.** `--long-prefill-token-threshold 4096` chunks long prompts into 4096-token pieces. Short prompts then share each step with them instead of waiting behind a 100K-token context, which lowers the TTFT tail. The recipe's 1P1D section above uses 8192. `--max-num-batched-tokens 32768` bounds a prefill step. Prefill captures CUDA graphs up to 2048 tokens. + - **Mooncake pool size.** `global_segment_size` is registered by every DP rank, prefill and decode alike, so a tray pins four times that value. The lane used 180 GB per rank (720 GB per tray), which leaves 80-100 GiB of a GB300 tray's host DRAM free with the engines up; 200 GB per rank ran trays out of host memory. A larger pool raises the hit rate at high session counts: going from 160 to 180 GB per rank, with the services moved off the engine trays, lowered TTFT p90 by 0.4-0.8 s at 280-360 sessions. Size it as (host DRAM free per tray after the engines are up - headroom for NIXL and the 4 GB local buffers) / ranks per tray. The decode ranks also write the KV they generate into the store (`save_decode_cache: true`), so the next turn's prefill can load the previous answer instead of recomputing it. + - **Routing.** The KV router (`--router-mode kv`) reads the prefill workers' KV events and sends each request to the worker that already holds its prefix. Session affinity (`--router-session-affinity-ttl-secs 14400`) keeps a conversation on that worker for up to four hours. Send a stable conversation ID in the `X-Dynamo-Session-ID` header. The Mooncake store catches the prefixes that miss after a reroute. + - **Transport.** Cross-tray NIXL needs `UCX_TLS=rc,cuda_copy` with `UCX_NET_DEVICES` set to the tray's RDMA ports. Without them, UCX can fall back to TCP and transfers stall. Point `UCX_MODULE_DIR` at the UCX bundled with the NIXL wheel (`nixl_cu13.libs/ucx` in the image's `dist-packages`). `kv_lease_duration: 120` holds a prefill's blocks for up to 120 s until decode reads them (the default is 30 s). `VLLM_MAX_ASYNC_KV_ADMITS_PER_STEP=4` caps how many requests with finished KV loads decode admits per step. + - **NUMA.** `--numa-bind` pins each rank to its GPU's NUMA node. Decode passes the tray's map, `--numa-bind-nodes 0 0 1 1`, explicitly. + - `--moe-backend deep_gemm_mega_moe` is the same as the lane's `--kernel-config '{"moe_backend":"deep_gemm_mega_moe"}'`. `--prefill-schedule-interval 1`, also set by the lane, is vLLM's default. + + ### Services + + Start etcd, NATS and the Mooncake master once, on a host that runs no engine workers, together with the frontend. The engine trays' host DRAM then goes to the Mooncake pool. + + ```bash + etcd --listen-client-urls http://0.0.0.0:2379 --advertise-client-urls http://:2379 + printf 'max_payload: 33554432\n' > nats.conf && nats-server -c nats.conf + mooncake_master --rpc_port=50051 \ + --default_kv_lease_ttl=60000 --eviction_high_watermark_ratio=0.95 --eviction_ratio=0.10 + ``` + + Every worker reads the store configuration from the JSON file that `MOONCAKE_CONFIG_PATH` points to (export it with the rest of the environment): + + ```json + {"master_server_address": ":50051", "metadata_server": "P2PHANDSHAKE", + "global_segment_size": "180GB", "local_buffer_size": "4GB", "protocol": "rdma", + "device_name": "mlx5_0,mlx5_1,mlx5_2,mlx5_3", "enable_offload": false} + ``` + + ### Workers + + Export `ETCD_ENDPOINTS=http://:2379`, `NATS_SERVER=nats://:4222` and the generated environment on every tray, then run the generated `python3 -m dynamo.vllm` commands. Add `--served-model-name MiniMaxAI/MiniMax-M3.1-Flash --trust-remote-code --dyn-tool-call-parser minimax_m3 --dyn-reasoning-parser minimax_m3` to both roles. `dynamo.vllm` rejects vLLM's API-server flags `--tool-call-parser` and `--enable-auto-tool-choice`. Multi-node DEP decode runs one process per tray with `--data-parallel-start-rank` set to 0, 4, 8 and so on, and a shared `--data-parallel-address` and `--data-parallel-rpc-port`. `--data-parallel-hybrid-lb` lets every tray's Dynamo runtime register its own DP ranks with the frontend. + + ### Frontend + + ```bash + export ETCD_ENDPOINTS=http://:2379 NATS_SERVER=nats://:4222 + export DYN_ROUTER_ACTIVE_REQUEST_EXPIRY_SECS=3600 DYN_TCP_CONNECT_TIMEOUT=120 DYN_TOKENIZER=fastokens + python3 -m dynamo.frontend --http-port 8000 --router-mode kv --router-session-affinity-ttl-secs 14400 + ``` + + `DYN_TOKENIZER=fastokens` switches the frontend to the fastokens tokenizer backend that ships with Dynamo. With 100K+ token agentic prompts, the default tokenizer spends about 150 ms per request on the frontend (up to 600 ms at p90), all of it added to TTFT; fastokens takes about 1 ms. + + The InferenceX throughput points replaced block verification with synthetic rejection at the committed golden DSpark acceptance length (4.04), so every point is measured at the same acceptance rate. Production serving keeps the default rejection sampler (`rejection_sample_method: block`), as in the command above. + + # GB300 AgentX on one 4-GPU tray without Dynamo: TP4 engines, a DEP4 engine + # behind vllm-router, and TP2 engines with host-DRAM KV offload. + gb300_agentx_tp4: + label: "AgentX TP4" + hardware: gb300 + strategy: single_node_tp + variants: [default] + workload: "Agentic (InferenceX AgentX)" + description: "One TP4 engine on a 4-GPU GB300 tray with the seven-token DSpark draft on FLASHINFER_TS; GPU-resident KV, with per-concurrency scheduling and CUDA graph settings." + source: { label: "InferenceX #4662", href: "https://github.com/SemiAnalysisAI/InferenceX/pull/4662", key: "minimaxm31flash-fp4-gb300-vllm-agentic-dspark", revision: 85956a00e240c2b7428e1e9406f25c731ddb0470 } + image: "vllm/vllm-openai:minimax-m31-flash-arm64-cu134@sha256:5d76347628973fb80519a919a2f2740b6645b62cba3fe2bd87b25f111bd0e948" + tp: 4 + select: + features: { text_only: true } + args: + - "--enable-prefix-caching" + - "--max-model-len" + - "1048576" + - "--max-num-batched-tokens" + - "8192" + env: + VLLM_ENGINE_READY_TIMEOUT_S: "7200" + VLLM_CHAT_TEMPLATE_RENDER_TIMEOUT: "600" + points: + c1: { label: "1", concurrency: 1, args: ["--max-num-seqs", "2"] } + c4: { label: "4", concurrency: 4, args: ["--max-num-seqs", "8"] } + c8: + label: "8" + concurrency: 8 + args: ["--max-num-seqs", "16"] + json_args: + "--compilation-config": { cudagraph_capture_sizes: [1, 2, 4, 8, 16, 24, 32, 40, 48, 56, 64, 72, 80, 88, 96, 104, 112, 120, 128, 136, 144, 152, 160, 168, 176, 184, 192, 200, 208, 216, 224, 232, 240, 248, 256, 264, 272, 280, 288, 296, 304, 312, 320, 328, 336, 344, 352, 360, 368, 376, 384, 392, 400, 408, 416, 424, 432, 440, 448, 456, 464, 472, 480, 488, 496, 504, 512, 640, 768, 896, 1024, 1152, 1280, 1408, 1536, 1664, 1792, 1920, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4608, 5120, 5632, 6144, 6656, 7168, 7680, 8192] } + c16: + label: "16" + concurrency: 16 + args: ["--max-num-seqs", "64"] + json_args: + "--compilation-config": { cudagraph_capture_sizes: [1, 2, 4, 8, 16, 24, 32, 40, 48, 56, 64, 72, 80, 88, 96, 104, 112, 120, 128, 136, 144, 152, 160, 168, 176, 184, 192, 200, 208, 216, 224, 232, 240, 248, 256, 264, 272, 280, 288, 296, 304, 312, 320, 328, 336, 344, 352, 360, 368, 376, 384, 392, 400, 408, 416, 424, 432, 440, 448, 456, 464, 472, 480, 488, 496, 504, 512, 640, 768, 896, 1024, 1152, 1280, 1408, 1536, 1664, 1792, 1920, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4608, 5120, 5632, 6144, 6656, 7168, 7680, 8192] } + c24: + label: "24" + concurrency: 24 + args: + - "--max-num-seqs" + - "64" + - "--max-num-batched-tokens" + - "32768" + - "--long-prefill-token-threshold" + - "8192" + - "--prefill-schedule-interval" + - "16" + - "--cudagraph-capture-sizes" + - "1" + - "2" + - "4" + - "8" + - "16" + - "24" + - "32" + - "40" + - "48" + - "56" + - "64" + - "72" + - "80" + - "88" + - "96" + - "104" + - "112" + - "120" + - "128" + - "136" + - "144" + - "152" + - "160" + - "168" + - "176" + - "184" + - "192" + - "200" + - "208" + - "216" + - "224" + - "232" + - "240" + - "248" + - "256" + - "272" + - "288" + - "304" + - "320" + - "336" + - "352" + - "368" + - "384" + - "400" + - "416" + - "432" + - "448" + - "464" + - "480" + - "496" + - "512" + - "528" + - "544" + - "560" + - "576" + - "592" + - "608" + - "624" + - "640" + - "656" + - "672" + - "688" + - "704" + - "720" + - "736" + - "752" + - "768" + - "784" + - "800" + - "816" + - "832" + - "848" + - "864" + - "880" + - "896" + - "912" + - "928" + - "944" + - "960" + - "976" + - "992" + - "1008" + - "1024" + - "1280" + - "1536" + - "1792" + - "2048" + - "2304" + - "2560" + - "2816" + - "3072" + - "3328" + - "3584" + - "3840" + - "4096" + - "4608" + - "5120" + - "5632" + - "6144" + - "6656" + - "7168" + - "7680" + - "8192" + env: { VLLM_HTTP_TIMEOUT_KEEP_ALIVE: "600" } + c32: { label: "32", concurrency: 32, args: ["--max-num-seqs", "64"] } + c40: + label: "40" + concurrency: 40 + args: + - "--max-num-seqs" + - "64" + - "--long-prefill-token-threshold" + - "8192" + - "--gpu-memory-utilization" + - "0.95" + - "--watermark" + - "0.05" + - "--cudagraph-capture-sizes" + - "1" + - "2" + - "4" + - "8" + - "16" + - "24" + - "32" + - "40" + - "48" + - "56" + - "64" + - "72" + - "80" + - "88" + - "96" + - "104" + - "112" + - "120" + - "128" + - "136" + - "144" + - "152" + - "160" + - "168" + - "176" + - "184" + - "192" + - "200" + - "208" + - "216" + - "224" + - "232" + - "240" + - "248" + - "256" + - "272" + - "288" + - "304" + - "320" + - "336" + - "352" + - "368" + - "384" + - "400" + - "416" + - "432" + - "448" + - "464" + - "480" + - "496" + - "512" + - "528" + - "544" + - "560" + - "576" + - "592" + - "608" + - "624" + - "640" + - "656" + - "672" + - "688" + - "704" + - "720" + - "736" + - "752" + - "768" + - "784" + - "800" + - "816" + - "832" + - "848" + - "864" + - "880" + - "896" + - "912" + - "928" + - "944" + - "960" + - "976" + - "992" + - "1008" + - "1024" + - "1280" + - "1536" + - "1792" + - "2048" + - "2304" + - "2560" + - "2816" + - "3072" + - "3328" + - "3584" + - "3840" + - "4096" + - "4608" + - "5120" + - "5632" + - "6144" + - "6656" + - "7168" + - "7680" + - "8192" + env: { VLLM_HTTP_TIMEOUT_KEEP_ALIVE: "600" } + runbook: | + The GB300 TP4 points of the AgentX lane in [SemiAnalysis InferenceX](https://inferencex.semianalysis.com/) (`minimaxm31flash-fp4-gb300-vllm-agentic-dspark`, [SemiAnalysisAI/InferenceX#4662](https://github.com/SemiAnalysisAI/InferenceX/pull/4662)) serve the default checkpoint text-only on one TP4 engine per 4-GPU tray, with `--max-num-seqs` set per concurrency. + + - **Concurrency 1, 4 and 32** use the default CUDA graph capture sizes. + - **Concurrency 8 and 16** capture CUDA graphs for every size up to 8192 tokens. + - **Concurrency 24** raises `--max-num-batched-tokens` to 32768 and schedules prefill every 16 steps; **concurrency 40** keeps 8192 and raises `--gpu-memory-utilization` to 0.95. Both cap prefill chunks at 8192 tokens and capture graphs to 8192. + + gb300_agentx_dep4: + label: "AgentX DEP4" + hardware: gb300 + strategy: single_node_dep + variants: [default] + workload: "Agentic (InferenceX AgentX)" + description: "One data-parallel, expert-parallel engine across the four GPUs of a GB300 tray behind vllm-router with session affinity. Three-token DSpark draft on FLASHINFER_TS." + source: { label: "InferenceX #4662", href: "https://github.com/SemiAnalysisAI/InferenceX/pull/4662", key: "minimaxm31flash-fp4-gb300-vllm-agentic-dspark", revision: 85956a00e240c2b7428e1e9406f25c731ddb0470 } + image: "vllm/vllm-openai:minimax-m31-flash-arm64-cu134@sha256:5d76347628973fb80519a919a2f2740b6645b62cba3fe2bd87b25f111bd0e948" + select: + features: { text_only: true } + args: + - "--enable-prefix-caching" + - "--max-model-len" + - "1048576" + - "--max-num-batched-tokens" + - "8192" + env: + VLLM_ENGINE_READY_TIMEOUT_S: "7200" + VLLM_CHAT_TEMPLATE_RENDER_TIMEOUT: "600" + json_args: + "--speculative-config": { num_speculative_tokens: 3 } + points: + c64: { label: "64", concurrency: 64, args: ["--max-num-seqs", "32"] } + c96: { label: "96", concurrency: 96, args: ["--max-num-seqs", "48"] } + c128: { label: "128", concurrency: 128, args: ["--max-num-seqs", "64"] } + runbook: | + The DEP4 points of the GB300 AgentX lane in [SemiAnalysis InferenceX](https://inferencex.semianalysis.com/) (`minimaxm31flash-fp4-gb300-vllm-agentic-dspark`, [SemiAnalysisAI/InferenceX#4662](https://github.com/SemiAnalysisAI/InferenceX/pull/4662)) run one engine with `--data-parallel-size 4` and `--enable-expert-parallel`; `--max-num-seqs` is the per-rank limit. + + The router uses the `consistent_hash` policy so a conversation stays on the rank that holds its prefix. The lane also runs it with `--request-timeout-secs 14400` and `--disable-retries`. + + gb300_agentx_tp2: + label: "AgentX TP2 + KV offload" + hardware: gb300 + strategy: single_node_tp + variants: [default] + workload: "Agentic (InferenceX AgentX)" + description: "One TP2 engine on two of the tray's four GB300 GPUs with the seven-token DSpark draft on FLASHINFER_TS and host-DRAM KV offload (482 GB per engine)." + source: { label: "InferenceX #4662", href: "https://github.com/SemiAnalysisAI/InferenceX/pull/4662", key: "minimaxm31flash-fp4-gb300-vllm-agentic-dspark", revision: 85956a00e240c2b7428e1e9406f25c731ddb0470 } + image: "vllm/vllm-openai:minimax-m31-flash-arm64-cu134@sha256:5d76347628973fb80519a919a2f2740b6645b62cba3fe2bd87b25f111bd0e948" + tp: 2 + select: + features: { text_only: true } + kv_offload: simple + args: + - "--enable-prefix-caching" + - "--max-model-len" + - "1048576" + - "--max-num-batched-tokens" + - "32768" + env: + VLLM_ENGINE_READY_TIMEOUT_S: "7200" + VLLM_CHAT_TEMPLATE_RENDER_TIMEOUT: "600" + VLLM_HTTP_TIMEOUT_KEEP_ALIVE: "600" + VLLM_USE_SIMPLE_KV_OFFLOAD: "1" + json_args: + "--compilation-config": { cudagraph_capture_sizes: [1, 2, 4, 8, 16, 24, 32, 40, 48, 56, 64, 72, 80, 88, 96, 104, 112, 120, 128, 136, 144, 152, 160, 168, 176, 184, 192, 200, 208, 216, 224, 232, 240, 248, 256, 264, 272, 280, 288, 296, 304, 312, 320, 328, 336, 344, 352, 360, 368, 376, 384, 392, 400, 408, 416, 424, 432, 440, 448, 456, 464, 472, 480, 488, 496, 504, 512, 640, 768, 896, 1024, 1152, 1280, 1408, 1536, 1664, 1792, 1920, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4608, 5120, 5632, 6144, 6656, 7168, 7680, 8192] } + "--kv-transfer-config": { kv_connector_extra_config: { cpu_bytes_to_use_per_rank: null, cpu_bytes_to_use: 482000000000, lazy_offload: true } } + points: + c28: { label: "28", concurrency: 28, args: ["--max-num-seqs", "42"] } + c32: { label: "32", concurrency: 32, args: ["--max-num-seqs", "48"] } + c36: { label: "36", concurrency: 36, args: ["--max-num-seqs", "54"] } + runbook: | + The TP2 points of the GB300 AgentX lane in [SemiAnalysis InferenceX](https://inferencex.semianalysis.com/) (`minimaxm31flash-fp4-gb300-vllm-agentic-dspark`, [SemiAnalysisAI/InferenceX#4662](https://github.com/SemiAnalysisAI/InferenceX/pull/4662)) add a host-DRAM KV tier through `SimpleCPUOffloadConnector` with lazy offload. `cpu_bytes_to_use` (482 GB) is the budget for the whole engine, about half of the tray's host DRAM, so the engine needs that much free host RAM. `--max-num-seqs` is 1.5 times the concurrency. + b300_agentx_tp4: label: "AgentX TP4" hardware: b300 @@ -488,6 +1107,386 @@ presets: The InferenceX throughput points replaced block verification with synthetic rejection at the committed golden DSpark acceptance length (4.04) so every point is measured at the same acceptance rate. The command above keeps block verification. + # GB200 AgentX under Dynamo: aggregated TP4 workers, and DEP4 prefill trays + # feeding one DEP decode worker. Each preset restates its engine flags, so it + # does not depend on the pd_cluster GB200 block above. + gb200_agentx_agg: + label: "AgentX TP4 (Dynamo)" + hardware: gb200 + strategy: single_node_tp + variants: [default] + workload: "Agentic (InferenceX AgentX)" + description: "Aggregated TP4 vLLM workers, one per 4-GPU GB200 tray, behind the Dynamo KV router with session affinity. Seven-token DSpark draft on FLASH_ATTN and prefill CUDA graphs up to 8192 tokens. KV stays in HBM, or spills to host DRAM through SimpleCPUOffloadConnector." + source: { label: "InferenceX #4632", href: "https://github.com/SemiAnalysisAI/InferenceX/pull/4632", key: "minimaxm31flash-fp4-gb200-dynamo-vllm-agentic-dspark-agg", revision: 936213146b79b55e8a11ffbc1d6010145995c6a6 } + image: + vllm: "vllm/vllm-openai:minimax-m31-flash-arm64-cu134" + dynamo: "1.6.0.dev20261023" + pip: ["ai-dynamo-runtime==1.6.0.dev20261023", "ai-dynamo==1.6.0.dev20261023"] + tp: 4 + select: + features: { text_only: true } + args: + - "--enable-prefix-caching" + - "--max-model-len" + - "1048576" + - "--max-num-batched-tokens" + - "32768" + env: + # Pins the prefix-block hashes, so they match across processes and restarts. + PYTHONHASHSEED: "0" + # A cold start (JIT builds, graph capture, autotune) takes more than 10 minutes. + VLLM_ENGINE_READY_TIMEOUT_S: "7200" + json_args: + "--compilation-config": { cudagraph_capture_sizes: [8, 16, 24, 32, 40, 48, 56, 64, 72, 80, 88, 96, 104, 112, 120, 128, 136, 144, 152, 160, 168, 176, 184, 192, 200, 208, 216, 224, 232, 240, 248, 256, 320, 384, 448, 512, 576, 640, 704, 768, 832, 896, 960, 1024, 1536, 2048, 3072, 4096, 6144, 8192] } + "--speculative-config": { attention_backend: "FLASH_ATTN" } + # --max-num-seqs is about twice the sessions a worker serves. + points: + gpu_kv: + label: "GPU-resident KV" + description: "KV cache in HBM only, for up to about 16 sessions per worker." + default: true + args: ["--max-num-seqs", "32", "--gpu-memory-utilization", "0.88"] + select: { kv_offload: "off" } + dram_kv: + label: "Host-DRAM KV offload" + description: "Adds a host-DRAM KV tier (360 GB per worker) for about 32-40 sessions per worker." + args: ["--max-num-seqs", "64", "--gpu-memory-utilization", "0.82", "--kv-cache-metrics"] + select: { kv_offload: simple } + json_args: + "--kv-transfer-config": { kv_connector_extra_config: { cpu_bytes_to_use_per_rank: null, cpu_bytes_to_use: 360000000000, lazy_offload: true } } + runbook: | + The GB200 aggregated AgentX lane in [SemiAnalysis InferenceX](https://inferencex.semianalysis.com/) (`minimaxm31flash-fp4-gb200-dynamo-vllm-agentic-dspark-agg`, [SemiAnalysisAI/InferenceX#4632](https://github.com/SemiAnalysisAI/InferenceX/pull/4632)) serves the default checkpoint text-only, with one TP4 `dynamo.vllm` worker per 4-GPU tray behind the Dynamo frontend. Add trays to scale out: the KV router spreads conversations across them. At 16 sessions per tray (GPU-resident KV) the lane measured 29.3k tok/s/GPU at 402 tok/s/user. + + **Image and versions.** The lane used an arm64 CUDA 13 build of the `vllm/vllm-openai:minimax-m31-flash` image (tag `minimax-m31-flash-arm64-cu134`); any build of that image family for GB200 will do. Dynamo `1.6.0.dev20261023` was installed on top: `pip install --extra-index-url https://pypi.nvidia.com ai-dynamo-runtime==1.6.0.dev20261023 ai-dynamo==1.6.0.dev20261023`. + + **Tuning rules.** + + - `--max-num-seqs` is about twice the sessions one worker serves: 32 up to 16 sessions, 64 at 32-40. + - **GPU-resident KV** (`--gpu-memory-utilization 0.88`) is enough up to about 16 sessions per tray. Beyond that, multi-turn contexts outgrow HBM and evicted prefixes are recomputed. + - **Host-DRAM KV offload** adds `SimpleCPUOffloadConnector` with lazy offload. `cpu_bytes_to_use` is the budget for the whole worker, split across its four ranks: 360 GB pins 90 GB per rank. Size it from the host DRAM that is free after the engine processes are up. Lower `--gpu-memory-utilization` to 0.82 to leave room for the transfer buffers. `--kv-cache-metrics` exports KV residency metrics, which help with that sizing; it can be left out. + - The CUDA graph sizes go up to 8192 tokens, so prefill chunks of up to 8192 tokens also replay graphs. `--max-num-batched-tokens 32768` still allows larger mixed steps, which run piecewise. + + **Running under Dynamo.** Start etcd and NATS (32 MB max payload) once: + + ```bash + etcd --listen-client-urls http://0.0.0.0:2379 --advertise-client-urls http://:2379 + printf 'max_payload: 33554432\n' > nats.conf && nats-server -c nats.conf + ``` + + On each tray, export `ETCD_ENDPOINTS=http://:2379` and `NATS_SERVER=nats://:4222` together with the generated environment and `DYN_TCP_CONNECT_TIMEOUT=120`, `DYN_BACKEND_ADMISSION_ENGINE_REQUEST_LIMIT=1024`, `DYN_BACKEND_ADMISSION_ENGINE_REQUEST_WAITING_LIMIT=1024` and `DYN_BACKEND_ADMISSION_REQUEST_QUEUE_LIMIT=2048`. Then run the generated flags with `python3 -m dynamo.vllm --model MiniMaxAI/MiniMax-M3.1-Flash --served-model-name MiniMaxAI/MiniMax-M3.1-Flash --trust-remote-code` in place of `vllm serve MiniMaxAI/MiniMax-M3.1-Flash`. `dynamo.vllm` rejects vLLM's API-server flags, so leave out `--tool-call-parser minimax_m3 --enable-auto-tool-choice` and add: + + ```bash + --dyn-tool-call-parser minimax_m3 --dyn-reasoning-parser minimax_m3 \ + --kv-events-config '{"publisher":"zmq","topic":"kv-events","enable_kv_cache_events":true,"endpoint":"tcp://*:5200"}' + ``` + + Start the frontend: + + ```bash + export ETCD_ENDPOINTS=http://:2379 NATS_SERVER=nats://:4222 + export DYN_ROUTER_ACTIVE_REQUEST_EXPIRY_SECS=3600 DYN_TCP_CONNECT_TIMEOUT=120 DYN_TOKENIZER=fastokens + python3 -m dynamo.frontend --http-port 8000 --router-mode kv --router-session-affinity-ttl-secs 14400 + ``` + + `DYN_TOKENIZER=fastokens` switches the frontend to the fastokens tokenizer backend that ships with Dynamo. With 100K+ token agentic prompts, the default tokenizer spends about 150 ms per request on the frontend (up to 600 ms at p90), all of it added to TTFT; fastokens takes about 1 ms. + + The KV router uses the workers' KV events to send each request to the worker that holds its prefix. Session affinity keeps a conversation on that worker for up to four hours; send a stable conversation ID in the `X-Dynamo-Session-ID` request header. + + The InferenceX throughput points replaced block verification with synthetic rejection at the committed golden DSpark acceptance length (4.04), so every point is measured at the same acceptance rate. Production serving keeps the default rejection sampler (`rejection_sample_method: block`), as in the command above. + + gb200_agentx_pd: + label: "AgentX PD (Dynamo)" + hardware: gb200 + strategy: pd_cluster + variants: [default] + workload: "Agentic (InferenceX AgentX)" + description: "DEP4 prefill workers, one per GB200 tray, feed one DEP8 decode worker (throughput) or DEP16 (lower latency) over NIXL. Both roles share a Mooncake KV store in host DRAM. The Dynamo KV router routes with session affinity. Seven-token DSpark draft on FLASHINFER_TS, DeepGEMM MegaMoE on both roles." + source: { label: "InferenceX #4632", href: "https://github.com/SemiAnalysisAI/InferenceX/pull/4632", key: "minimaxm31flash-fp4-gb200-dynamo-vllm-agentic-dspark-disagg", revision: 936213146b79b55e8a11ffbc1d6010145995c6a6 } + image: + vllm: "vllm/vllm-openai:minimax-m31-flash-arm64-cu134" + dynamo: "1.6.0.dev20261023" + pip: ["ai-dynamo-runtime==1.6.0.dev20261023", "ai-dynamo==1.6.0.dev20261023", "mooncake-transfer-engine-cuda13==0.3.13.post1"] + # Dynamo workers take engine flags only; the tool-call parser moves to + # --dyn-tool-call-parser (see the runbook). + select: + pd_router: dynamo + features: { text_only: true, tool_calling: false } + args: + - "--enable-expert-parallel" + - "--moe-backend" + - "deep_gemm_mega_moe" + - "--numa-bind" + - "--enable-prefix-caching" + - "--max-model-len" + - "1048576" + - "--load-format" + - "fastsafetensors" + # The RDMA names are a GB200 tray's four NICs; use the host's own. + env: + PYTHONHASHSEED: "0" + VLLM_USE_V2_MODEL_RUNNER: "1" + VLLM_ENGINE_READY_TIMEOUT_S: "7200" + VLLM_HTTP_TIMEOUT_KEEP_ALIVE: "600" + VLLM_RANDOMIZE_DP_DUMMY_INPUTS: "1" + NCCL_NET_GDR_C2C: "1" + NCCL_IB_HCA: "mlx5_0,mlx5_1,mlx5_2,mlx5_3" + NVSHMEM_REMOTE_TRANSPORT: "none" + NVSHMEM_ENABLE_NIC_PE_MAPPING: "1" + NVSHMEM_HCA_PE_MAPPING: "mlx5_0:1:1,mlx5_1:1:1,mlx5_2:1:1,mlx5_3:1:1" + UCX_TLS: "rc,cuda_copy" + UCX_NET_DEVICES: "mlx5_0:1,mlx5_1:1,mlx5_2:1,mlx5_3:1" + UCX_CUDA_IPC_ENABLE_MNNVL: "n" + UCX_MEMTYPE_CACHE: "n" + UCX_MEMTYPE_REG_WHOLE: "n" + UCX_RNDV_PIPELINE_ERROR_HANDLING: "y" + NIXL_UCX_SGL_ENABLE: "1" + WITH_NVIDIA_PEERMEM: "0" + MC_SLICE_SIZE: "1048576" + MC_MAX_MR_SIZE: "4294967296" + MC_ENABLE_DEST_DEVICE_AFFINITY: "1" + DYN_TCP_CONNECT_TIMEOUT: "120" + DYN_BACKEND_ADMISSION_ENGINE_REQUEST_LIMIT: "1024" + DYN_BACKEND_ADMISSION_ENGINE_REQUEST_WAITING_LIMIT: "1024" + DYN_BACKEND_ADMISSION_REQUEST_QUEUE_LIMIT: "2048" + roles: + prefill: + # One DEP4 worker per tray; run as many trays as the load needs. + nodes: 1 + parallelism: dep + args: + - "--data-parallel-size" + - "4" + - "--data-parallel-size-local" + - "4" + - "--max-num-seqs" + - "64" + - "--max-num-batched-tokens" + - "16384" + - "--long-prefill-token-threshold" + - "4096" + - "--gpu-memory-utilization" + - "0.92" + - "--compilation-config" + - '{"cudagraph_mode":"FULL_AND_PIECEWISE"}' + - "--max-cudagraph-capture-size" + - "2048" + - "--kv-events-config" + - '{"publisher":"zmq","topic":"kv-events","enable_kv_cache_events":true,"endpoint":"tcp://*:5200"}' + - "--kv-transfer-config" + - '{"kv_connector":"MultiConnector","kv_role":"kv_both","kv_load_failure_policy":"recompute","kv_connector_extra_config":{"connectors":[{"kv_connector":"NixlConnector","kv_role":"kv_both","kv_load_failure_policy":"fail","kv_buffer_device":"cuda","kv_connector_extra_config":{"backends":["UCX"],"enforce_handshake_compat":false,"kv_lease_duration":120}},{"kv_connector":"MooncakeStoreConnector","kv_role":"kv_both","kv_load_failure_policy":"recompute","kv_connector_extra_config":{"load_async":true,"lookup_async":true,"enable_offload":false}}]}}' + env: + VLLM_MOONCAKE_LOAD_RECV_THREADS: "8" + decode: + nodes: 2 + parallelism: dep + # 64 sequences per DP rank; decode graphs cover 64 x (1 + 7) tokens. + # The decode ranks only add capacity to the Mooncake store. + remove_args: ["--no-enable-prefix-caching"] + args: + - "--max-num-seqs" + - "64" + - "--max-num-batched-tokens" + - "8192" + - "--watermark" + - "0.05" + - "--gpu-memory-utilization" + - "0.88" + - "--compilation-config" + - '{"cudagraph_mode":"FULL_DECODE_ONLY"}' + - "--max-cudagraph-capture-size" + - "512" + - "--numa-bind-nodes" + - "0" + - "0" + - "1" + - "1" + - "--kv-transfer-config" + - '{"kv_connector":"MultiConnector","kv_role":"kv_both","kv_load_failure_policy":"recompute","kv_connector_extra_config":{"connectors":[{"kv_connector":"NixlConnector","kv_role":"kv_both","kv_load_failure_policy":"fail","kv_buffer_device":"cuda","kv_connector_extra_config":{"backends":["UCX"],"enforce_handshake_compat":false,"kv_lease_duration":120}},{"kv_connector":"MooncakeStoreConnector","kv_role":"kv_consumer","kv_load_failure_policy":"recompute","kv_connector_extra_config":{"load_async":true,"lookup_async":true,"enable_offload":false,"save_decode_cache":false,"enable_lookup":false}}]}}' + env: + VLLM_MOONCAKE_LOAD_RECV_THREADS: "4" + VLLM_ALLREDUCE_USE_SYMM_MEM: "0" + NCCL_CUMEM_ENABLE: "1" + NCCL_MNNVL_ENABLE: "1" + NCCL_NVLS_ENABLE: "1" + points: + dep8: + label: "DEP8 decode (throughput)" + description: "One DEP8 decode worker across two trays." + default: true + dep16: + label: "DEP16 decode (lower latency)" + description: "One DEP16 decode worker across four trays." + roles: { decode: { nodes: 4 } } + runbook: | + The GB200 disaggregated AgentX lane in [SemiAnalysis InferenceX](https://inferencex.semianalysis.com/) (`minimaxm31flash-fp4-gb200-dynamo-vllm-agentic-dspark-disagg`, [SemiAnalysisAI/InferenceX#4632](https://github.com/SemiAnalysisAI/InferenceX/pull/4632)) serves the default checkpoint text-only with Dynamo and vLLM. Several prefill workers, each DEP4 on one 4-GPU tray, feed one decode worker that is DEP8 across two trays or DEP16 across four. KV moves from prefill to decode over NIXL (UCX over RDMA), and every rank contributes host DRAM to a shared Mooncake KV store, so a returning conversation's prefix is loaded from the store instead of recomputed. Both roles use expert parallelism with DeepGEMM MegaMoE, attention data parallelism, the V2 model runner and the seven-token DSpark draft on `FLASHINFER_TS`. + + | Prefill workers (DEP4) | Decode | GPUs | Sessions | tok/s/GPU | tok/s/user | TTFT p90 | E2E latency p90 | + |---:|---|---:|---:|---:|---:|---:|---:| + | 2 | DEP8 | 16 | 224 | 94.2k | 250 | 3.0 s | 12.2 s | + | 2 | DEP8 | 16 | 256 | 98.8k | 244 | 5.3 s | 15.7 s | + | 3 | DEP8 | 20 | 256 | 86.4k | 240 | 2.4 s | 12.1 s | + | 3 | DEP8 | 20 | 288 | 93.2k | 230 | 3.5 s | 13.3 s | + | 3 | DEP16 | 28 | 280 | 70.4k | 302 | 1.9 s | 9.4 s | + | 3 | DEP16 | 28 | 320 | 77.3k | 291 | 2.2 s | 10.0 s | + | 3 | DEP16 | 28 | 360 | 85.4k | 284 | 2.5 s | 10.6 s | + | 4 | DEP16 | 32 | 352 | 74.5k | 284 | 1.8 s | 9.9 s | + + The preset renders one prefill worker and the decode worker. Run more prefill workers with the same command, one per tray. + + **Image and versions.** The lane used an arm64 CUDA 13 build of the `vllm/vllm-openai:minimax-m31-flash` image (tag `minimax-m31-flash-arm64-cu134`), with Dynamo `1.6.0.dev20261023` and Mooncake `mooncake-transfer-engine-cuda13==0.3.13.post1` installed on every worker and on the Mooncake master host: `pip install --extra-index-url https://pypi.nvidia.com ai-dynamo-runtime==1.6.0.dev20261023 ai-dynamo==1.6.0.dev20261023 mooncake-transfer-engine-cuda13==0.3.13.post1`. + + ### Tuning rules + + - **Prefill pool.** The prefill pool sets TTFT, so scale prefill workers with load. The lane's points kept TTFT p90 between 1.8 and 5.3 s with one DEP4 prefill tray per 85-128 concurrent agentic sessions (2 trays at 224-256 sessions, 3 at 256-360, 4 at 352). Two prefill trays per DEP8 decode give the most tokens per GPU; a third keeps TTFT p90 near 3.5 s up to 288 sessions. Add a prefill tray when TTFT p90 climbs while decode still has room. + - **DEP8 or DEP16 decode.** DEP8 gives the most tokens per GPU: 28-36 sessions per decode GPU at 230-250 tok/s/user. DEP16 halves the batch per rank and gives about 25% more tok/s/user (302 at 280 sessions) and holds E2E latency p90 near 10 s up to 360 sessions, at about 10-20% fewer tokens per GPU. `--max-num-seqs 64` per DP rank is a cap, not the operating point. The decode graph cap is `--max-num-seqs` x (1 + 7 draft tokens) = 512. `--watermark 0.05` keeps 5% of the decode KV cache free, so extra requests wait in the queue instead of preempting running ones. Keep decode `--max-num-batched-tokens` at 8192: a preempted request recomputes its context in chunks of that size. + - **Prefill batching.** `--long-prefill-token-threshold 4096` chunks long prompts into 4096-token pieces. Short prompts then share each step with them instead of waiting behind a 100K-token context, which lowers the TTFT tail. The recipe's 1P1D section above uses 8192. `--max-num-batched-tokens 16384` bounds a prefill step. Prefill captures CUDA graphs up to 2048 tokens. Prefill runs at `--gpu-memory-utilization 0.92`, decode at 0.88. + - **Mooncake pool size.** `global_segment_size` is registered by every DP rank, prefill and decode alike, so a tray pins four times that value. The lane used 190 GB per rank (760 GB per tray), which lets three DEP4 prefill trays serve up to 288 sessions with DEP8 and 360 with DEP16; 210 GB per rank ran the prefill trays out of host memory. Size it as (host DRAM free per tray after the engines are up - headroom for NIXL and the 4 GB local buffers) / ranks per tray. The decode ranks only add capacity to the pool (`kv_consumer` with `enable_lookup: false` and `save_decode_cache: false`): the prefill workers write and read every block. + - **Routing.** The KV router (`--router-mode kv`) reads the prefill workers' KV events and sends each request to the worker that already holds its prefix. Session affinity (`--router-session-affinity-ttl-secs 14400`) keeps a conversation on that worker for up to four hours. Send a stable conversation ID in the `X-Dynamo-Session-ID` header. The Mooncake store catches the prefixes that miss after a reroute. + - **Transport.** Cross-tray NIXL needs `UCX_TLS=rc,cuda_copy` with `UCX_NET_DEVICES` set to the tray's RDMA ports. Without them, UCX can fall back to TCP and transfers stall. Point `UCX_MODULE_DIR` at the UCX bundled with the NIXL wheel (`nixl_cu13.libs/ucx` in the image's `dist-packages`). `kv_lease_duration: 120` holds a prefill's blocks for up to 120 s until decode reads them (the default is 30 s). + - **NUMA.** `--numa-bind` pins each rank to its GPU's NUMA node. Decode passes the tray's map, `--numa-bind-nodes 0 0 1 1`, explicitly. + - `--moe-backend deep_gemm_mega_moe` is the same as the lane's `--kernel-config '{"moe_backend":"deep_gemm_mega_moe"}'`. `--prefill-schedule-interval 1`, also set by the lane, is vLLM's default. + + ### Services + + Start etcd, NATS and the Mooncake master once, on a host that runs no engine workers, together with the frontend. The engine trays' host DRAM then goes to the Mooncake pool. + + ```bash + etcd --listen-client-urls http://0.0.0.0:2379 --advertise-client-urls http://:2379 + printf 'max_payload: 33554432\n' > nats.conf && nats-server -c nats.conf + mooncake_master --rpc_port=50051 \ + --default_kv_lease_ttl=60000 --eviction_high_watermark_ratio=0.95 --eviction_ratio=0.10 + ``` + + Every worker reads the store configuration from the JSON file that `MOONCAKE_CONFIG_PATH` points to (export it with the rest of the environment): + + ```json + {"master_server_address": ":50051", "metadata_server": "P2PHANDSHAKE", + "global_segment_size": "190GB", "local_buffer_size": "4GB", "protocol": "rdma", + "device_name": "mlx5_0,mlx5_1,mlx5_2,mlx5_3", "enable_offload": false} + ``` + + ### Workers + + Export `ETCD_ENDPOINTS=http://:2379`, `NATS_SERVER=nats://:4222` and the generated environment on every tray, then run the generated `python3 -m dynamo.vllm` commands. Add `--served-model-name MiniMaxAI/MiniMax-M3.1-Flash --trust-remote-code --dyn-tool-call-parser minimax_m3 --dyn-reasoning-parser minimax_m3` to both roles. `dynamo.vllm` rejects vLLM's API-server flags `--tool-call-parser` and `--enable-auto-tool-choice`. Multi-node DEP decode runs one process per tray with `--data-parallel-start-rank` set to 0, 4, 8 and so on, and a shared `--data-parallel-address` and `--data-parallel-rpc-port`. `--data-parallel-hybrid-lb` lets every tray's Dynamo runtime register its own DP ranks with the frontend. + + ### Frontend + + ```bash + export ETCD_ENDPOINTS=http://:2379 NATS_SERVER=nats://:4222 + export DYN_ROUTER_ACTIVE_REQUEST_EXPIRY_SECS=3600 DYN_TCP_CONNECT_TIMEOUT=120 DYN_TOKENIZER=fastokens + python3 -m dynamo.frontend --http-port 8000 --router-mode kv --router-session-affinity-ttl-secs 14400 + ``` + + `DYN_TOKENIZER=fastokens` switches the frontend to the fastokens tokenizer backend that ships with Dynamo. With 100K+ token agentic prompts, the default tokenizer spends about 150 ms per request on the frontend (up to 600 ms at p90), all of it added to TTFT; fastokens takes about 1 ms. + + The InferenceX throughput points replaced block verification with synthetic rejection at the committed golden DSpark acceptance length (4.04), so every point is measured at the same acceptance rate. Production serving keeps the default rejection sampler (`rejection_sample_method: block`), as in the command above. + + mi355x_agentx: + label: "AgentX TP4" + hardware: mi355x + strategy: single_node_tp + variants: [mxfp4] + workload: "Agentic (InferenceX AgentX)" + description: "One TP4 engine on four of the node's eight MI355X GPUs, serving the Quark MXFP4 checkpoint: seven-token DSpark with mono decode and an FP8 KV cache up to concurrency 8, seven-token DSpark with DBO and an NVFP4 KV cache at 16 and 32, and three-token DSpark with DBO and a host-DRAM KV tier at 64." + source: { label: "InferenceX #4671", href: "https://github.com/SemiAnalysisAI/InferenceX/pull/4671", key: "minimaxm31flash-fp4-mi355x-vllm-agentic-dspark", revision: a328b443582c45e45eef7f0c4f133f3e173f0211 } + image: "vllm/vllm-openai-rocm:minimax-m31-flash@sha256:ae8681212aa5291d8c27f06e90016435e3fca144a68fd5c439231f6475f74569" + tp: 4 + select: + features: { text_only: true, spec_decoding: true } + args: + - "--enable-prefix-caching" + env: + VLLM_HTTP_TIMEOUT_KEEP_ALIVE: "900" + json_args: + "--speculative-config": { num_speculative_tokens: 7, rejection_sample_method: block } + # The DBO points pass the DBO flags directly: the DBO / SP prefill feature's + # modes set their own thresholds (8192, or 16384 with SP on the default + # checkpoint only), and this lane runs the 16384-token threshold without SP. + points: + c1: + label: "1 (mono)" + concurrency: 1 + select: { features: { mono_decode: mono } } + args: ["--max-num-seqs", "2", "--max-num-batched-tokens", "8192"] + c2: + label: "2 (mono)" + concurrency: 2 + select: { features: { mono_decode: mono } } + args: ["--max-num-seqs", "4", "--max-num-batched-tokens", "8192"] + c4: + label: "4 (mono)" + concurrency: 4 + select: { features: { mono_decode: mono } } + args: ["--max-num-seqs", "8", "--max-num-batched-tokens", "8192"] + c8: + label: "8 (mono)" + concurrency: 8 + select: { features: { mono_decode: mono } } + args: ["--max-num-seqs", "16", "--max-num-batched-tokens", "8192"] + c16: + label: "16 (DBO)" + concurrency: 16 + args: ["--max-num-seqs", "32", "--max-num-batched-tokens", "32768", "--gpu-memory-utilization", "0.85", "--enable-dbo", "--dbo-prefill-token-threshold", "16384", "--dbo-decode-token-threshold", "32768"] + env: { VLLM_USE_V2_MODEL_RUNNER: "1", DEBUG_HIP_DYNAMIC_QUEUES: "0" } + c32: + label: "32 (DBO)" + concurrency: 32 + args: ["--max-num-seqs", "64", "--max-num-batched-tokens", "32768", "--gpu-memory-utilization", "0.85", "--enable-dbo", "--dbo-prefill-token-threshold", "16384", "--dbo-decode-token-threshold", "32768"] + env: { VLLM_USE_V2_MODEL_RUNNER: "1", DEBUG_HIP_DYNAMIC_QUEUES: "0" } + c64: + label: "64 (DBO, KV offload)" + concurrency: 64 + select: { kv_offload: simple } + args: ["--max-num-seqs", "128", "--max-num-batched-tokens", "32768", "--gpu-memory-utilization", "0.9", "--enable-dbo", "--dbo-prefill-token-threshold", "16384", "--dbo-decode-token-threshold", "32768"] + env: { VLLM_USE_V2_MODEL_RUNNER: "1", DEBUG_HIP_DYNAMIC_QUEUES: "0", PYTHONHASHSEED: "42" } + json_args: + "--speculative-config": { num_speculative_tokens: 3 } + "--kv-transfer-config": { kv_connector_extra_config: { cpu_bytes_to_use_per_rank: null, cpu_bytes_to_use: 1349000000000, lazy_offload: false } } + runbook: | + The MI355X AgentX lane in [SemiAnalysis InferenceX](https://inferencex.semianalysis.com/) + (`minimaxm31flash-fp4-mi355x-vllm-agentic-dspark`, + [SemiAnalysisAI/InferenceX#4671](https://github.com/SemiAnalysisAI/InferenceX/pull/4671)), + on the Quark MXFP4 checkpoint with `--max-num-seqs` at twice the concurrency + (c64 admits 128). Throughput runs replaced block verification with synthetic + rejection at the committed golden DSpark acceptance length (4.04 for seven + tokens, 2.88 for three); the commands above keep block verification. + + - **Concurrency 1-8** run mono decode with an FP8 KV cache and no DBO: a decode + step of up to 16 tokens runs each layer as one fused kernel, and prefill steps + stay at 8192 tokens. + - **Concurrency 16 and 32** run seven-token DSpark with DBO and an NVFP4 KV cache + at 0.85 memory utilization, leaving headroom for DBO prefill. + - **Concurrency 64** runs three-token DSpark with DBO at 0.90 and adds a host-DRAM + KV tier through `SimpleCPUOffloadConnector`, so cached prefixes are not lost + once the working set outgrows the GPU KV cache (14.6M tokens at 0.90). + `cpu_bytes_to_use` (1,349 GB) is the budget for the whole engine. + + [InferenceX run 41003224750](https://github.com/SemiAnalysisAI/InferenceX/actions/runs/41003224750) + passed all seven performance points and GSM8K at concurrency 8, 32 and 64 at + `a328b4435` (single run): + + | Concurrency | Interactivity p50 / p90 (tokens/s/user) | Total tokens/s per GPU | + | --- | ---: | ---: | + | 1 | 551.0 / 515.7 | 6,766 | + | 2 | 548.9 / 410.4 | 6,918 | + | 4 | 546.3 / 395.6 | 9,494 | + | 8 | 407.4 / 240.0 | 15,411 | + | 16 | 228.6 / 168.3 | 27,275 | + | 32 | 172.3 / 115.1 | 54,158 | + | 64 | 71.4 / 47.3 | 75,434 | + + GSM8K exact match (strict) over 1319 questions was 0.964 at concurrency 8, 0.959 at + 32 and 0.957 at 64. + + - **Concurrency 64 needed a rerun.** The first attempt hung at its first load from + the host KV tier (23 fetches in progress, none completing; `sample_tokens` RPC + timeout) and was rerun once, which passed. Treat the offload point as less + proven than the others. + - **Sequence-parallel prefill does not apply to this checkpoint.** InferenceX also + set `VLLM_ROCM_MINIMAX_M3_SP_PREFILL=1` at concurrency 64, and the server logged + sequence-parallel prefill as off; it is omitted here. SP prefill is available on + the default checkpoint through the DBO / SP prefill feature. + guide: | ## Overview diff --git a/recipes/models/Qwen/Qwen3-30B-A3B.yaml b/recipes/models/Qwen/Qwen3-30B-A3B.yaml index ae97eb6fa1f7..b07ad144c2ae 100644 --- a/recipes/models/Qwen/Qwen3-30B-A3B.yaml +++ b/recipes/models/Qwen/Qwen3-30B-A3B.yaml @@ -12,7 +12,6 @@ meta: - "Qwen/Qwen3-235B-A22B-Instruct-2507" hardware: xeon6: verified - ascend_910c: verified arc_pro_b70: verified model: @@ -48,59 +47,12 @@ features: opt_in_features: - long_context -hardware_opt_in_features: - ascend_910c: - - tool_calling - - reasoning - variants: default: precision: bf16 vram_minimum_gb: 74 description: "Full precision BF16" supported_hardware: [xeon6, arc_pro_b70] - ascend_w8a8: - model_id: "Eco-Tech/Qwen3-30B-A3B-w8a8" - precision: int8 - label: "W8A8 (Ascend A3)" - vram_minimum_gb: 37 - description: "ModelSlim INT8 W8A8 checkpoint for Ascend 910C, verified with TP4 + expert parallelism." - supported_hardware: [ascend_910c] - tp: 4 - min_vllm_version: "0.23.0" - extra_args: - - "--trust-remote-code" - - "--max-num-seqs" - - "100" - - "--max-model-len" - - "40960" - - "--max-num-batched-tokens" - - "16384" - - "--enable-expert-parallel" - - "--quantization" - - "ascend" - - "--distributed_executor_backend" - - "mp" - - "--no-enable-prefix-caching" - - "--async-scheduling" - - "--compilation-config" - - '{"cudagraph_mode": "FULL_DECODE_ONLY"}' - - "--additional-config" - - '{"enable_flashcomm1": true, "weight_nz_mode": 2}' - - "--gpu-memory-utilization" - - "0.95" - extra_env: - VLLM_USE_MODELSCOPE: "True" - HCCL_OP_EXPANSION_MODE: "AIV" - HCCL_BUFFSIZE: "1024" - OMP_PROC_BIND: "false" - OMP_NUM_THREADS: "1" - TASK_QUEUE_ENABLE: "1" - PYTORCH_NPU_ALLOC_CONF: "expandable_segments:True" - VLLM_USE_RUST_FRONTEND: "1" - hardware_overrides: - ascend_910c: - docker_image: "swr.cn-southwest-2.myhuaweicloud.com/base_image/ascend-ci/vllm-ascend:v0.23.0-a3" compatible_strategies: - single_node_tp diff --git a/recipes/models/Qwen/Qwen3.5-397B-A17B.yaml b/recipes/models/Qwen/Qwen3.5-397B-A17B.yaml index 061dfa53f14d..d46f798ea66d 100644 --- a/recipes/models/Qwen/Qwen3.5-397B-A17B.yaml +++ b/recipes/models/Qwen/Qwen3.5-397B-A17B.yaml @@ -4,7 +4,7 @@ meta: provider: "Qwen" description: "Multimodal MoE model with gated delta networks architecture, 397B total / 17B active parameters, up to 262K context" date_added: 2026-02-16 - date_updated: 2026-10-07 + date_updated: 2026-10-29 tasks: - multimodal - text @@ -18,7 +18,6 @@ meta: mi325x: verified mi350x: verified mi355x: verified - ascend_950dt: verified model: model_id: "Qwen/Qwen3.5-397B-A17B" @@ -142,100 +141,6 @@ variants: - "ROCM_AITER_FA" - "--max-num-seqs" - "64" - ascend_w8a8_mtp: - model_id: "Eco-Tech/Qwen3.5-397B-A17B-w8a8-mtp" - precision: int8 - label: "W8A8 (Ascend)" - vram_minimum_gb: 477 - supported_hardware: [ascend_950dt] - tp: 8 - min_vllm_version: "0.23.0" - default_modes: - spec_decoding: qwen3_5_mtp - extra_args: - - "--distributed-executor-backend" - - "mp" - - "--data-parallel-size" - - "1" - - "--quantization" - - "ascend" - - "--served-model-name" - - "qwen3.5" - - "--max-num-seqs" - - "128" - - "--max-model-len" - - "133000" - - "--max-num-batched-tokens" - - "8192" - - "--gpu-memory-utilization" - - "0.95" - - "--enable-prefix-caching" - - "--async-scheduling" - - "--compilation-config" - - '{"cudagraph_mode":"FULL_DECODE_ONLY"}' - - "--additional-config" - - '{"enable_cpu_binding":true}' - extra_env: - VLLM_USE_MODELSCOPE: "True" - PYTORCH_NPU_ALLOC_CONF: "expandable_segments:True" - HCCL_OP_EXPANSION_MODE: "AIV" - HCCL_BUFFSIZE: "400" - OMP_PROC_BIND: "false" - OMP_NUM_THREADS: "100" - TASK_QUEUE_ENABLE: "1" - VLLM_ASCEND_ENABLE_PREFETCH_MLP: "1" - HCCL_INTRA_PCIE_ENABLE: "1" - HCCL_INTRA_ROCE_ENABLE: "0" - hardware_overrides: - ascend_950dt: - docker_image: "quay.io/ascend/vllm-ascend:v0.23.0-950DT" - ascend_w4a4_mxfp4: - model_id: "Eco-Tech/Qwen3.5-397B-A17B-w4a4-mxfp4" - precision: mxfp4 - label: "W4A4 (Ascend)" - vram_minimum_gb: 239 - supported_hardware: [ascend_950dt] - tp: 8 - min_vllm_version: "0.23.0" - default_modes: - spec_decoding: qwen3_5_mtp - extra_args: - - "--distributed-executor-backend" - - "mp" - - "--data-parallel-size" - - "1" - - "--quantization" - - "ascend" - - "--served-model-name" - - "qwen3.5" - - "--max-num-seqs" - - "128" - - "--max-model-len" - - "133000" - - "--max-num-batched-tokens" - - "8192" - - "--gpu-memory-utilization" - - "0.95" - - "--enable-prefix-caching" - - "--async-scheduling" - - "--compilation-config" - - '{"cudagraph_mode":"FULL_DECODE_ONLY"}' - - "--additional-config" - - '{"enable_cpu_binding":true}' - extra_env: - VLLM_USE_MODELSCOPE: "True" - PYTORCH_NPU_ALLOC_CONF: "expandable_segments:True" - HCCL_OP_EXPANSION_MODE: "AIV" - HCCL_BUFFSIZE: "400" - OMP_PROC_BIND: "false" - OMP_NUM_THREADS: "100" - TASK_QUEUE_ENABLE: "1" - VLLM_ASCEND_ENABLE_PREFETCH_MLP: "1" - HCCL_INTRA_PCIE_ENABLE: "1" - HCCL_INTRA_ROCE_ENABLE: "0" - hardware_overrides: - ascend_950dt: - docker_image: "quay.io/ascend/vllm-ascend:v0.23.0-950DT" compatible_strategies: - single_node_tp @@ -265,6 +170,57 @@ strategy_overrides: env: VLLM_SSM_CONV_STATE_LAYOUT: "DS" +presets: + b200_tp2_kv_offload: + label: "TP2 + KV offload (multi-turn agentic)" + hardware: b200 + strategy: single_node_tp + variants: [nvfp4] + workload: "Multi-turn agentic coding, ~64k input, KV-reuse-bound (mean ITL under 20 ms)" + description: "One TP2 engine with expert parallelism on two of the node's eight B200 GPUs: MTP with three draft tokens, FP8 KV cache, float16 Mamba SSM cache, async scheduling, and a 160 GiB host KV tier so prefixes survive across turns; admission is capped at 48 sequences to hold mean ITL under 20 ms." + source: { label: "vLLM #64607", href: "https://github.com/vllm-project/vllm/pull/64607" } + image: "vllm/vllm-openai:v0.32.0" + tp: 2 + select: + frontend: python + features: { spec_decoding: mtp, encoder_parallel: false } + args: + - "--enable-expert-parallel" + - "--async-scheduling" + - "--block-size" + - "64" + - "--mamba-ssm-cache-dtype" + - "float16" + - "--max-num-batched-tokens" + - "16384" + # Admission stays at 48 sequences so mean ITL holds under 20 ms up to 64 clients; the rest queue. + - "--max-num-seqs" + - "48" + # Qwen3.5 mixes Gated DeltaNet and attention layers; the hybrid manager keeps prefix caching across both. + - "--no-disable-hybrid-kv-cache-manager" + # vLLM sums --kv-offloading-size over the TP ranks: 80 GiB of host KV per rank. + - "--kv-offloading-backend" + - "native" + - "--kv-offloading-size" + - "160" + runbook: | + Measured by NVIDIA on vLLM v0.32.0 (`vllm/vllm-openai:v0.32.0`) on two B200 SXM 180 GB with a + 942-request multi-turn agentic coding trace (about 64k mean input tokens, most prefix tokens + reused across turns, outputs capped at 12k tokens), replayed at fixed concurrency with real + MTP acceptance (mean acceptance length 3.2 to 3.3). Host: 512 GiB RAM and 200 GiB /dev/shm per + server for the 160 GiB offload tier. + + | Concurrency | Output tok/s/GPU | Mean ITL | Mean TTFT | + |---|---:|---:|---:| + | 16 | 936 | 8.7 ms | 0.70 s | + + Further concurrencies and the comparison against this recipe's baseline command on the same + image and trace are being collected and will follow in a separate update. + + The 48-sequence cap trades time to first token for inter-token latency at 64 clients; raise + `--max-num-seqs` if TTFT matters more than ITL for your traffic. The offload tier only helps + workloads that revisit long prefixes; drop the `--kv-offloading-*` flags for single-turn traffic. + guide: | ## Overview diff --git a/recipes/models/Qwen/Qwen3.6-27B.yaml b/recipes/models/Qwen/Qwen3.6-27B.yaml index 2129e22b2c63..63e4418af82c 100644 --- a/recipes/models/Qwen/Qwen3.6-27B.yaml +++ b/recipes/models/Qwen/Qwen3.6-27B.yaml @@ -16,8 +16,6 @@ meta: rtx_pro_6000: verified dgx_station_gb300: verified rtx_5090: verified - ascend_950dt: verified - ascend_910c: verified arc_pro_b60: verified arc_pro_b70: verified mi300x: verified @@ -102,39 +100,11 @@ variants: precision: bf16 vram_minimum_gb: 65 description: "Full precision BF16 — fits on 1x H200, 2x H100, 4x Intel Arc Pro B60, 2x Intel Arc Pro B70, or an Ascend A3 node (TP2+DP8)" - tp: - ascend_910c: 2 hardware_overrides: rtx_pro_6000: extra_args: - "--max-num-seqs" - "8" - ascend_910c: - docker_image: "swr.cn-southwest-2.myhuaweicloud.com/base_image/ascend-ci/vllm-ascend:v0.23.0-a3" - extra_args: - - "--data-parallel-size" - - "8" - - "--served-model-name" - - "qwen3.6" - - "--max-num-seqs" - - "32" - - "--max-model-len" - - "133000" - - "--max-num-batched-tokens" - - "16384" - - "--gpu-memory-utilization" - - "0.90" - - "--no-enable-prefix-caching" - - "--compilation-config" - - '{"cudagraph_mode":"FULL_DECODE_ONLY"}' - - "--async-scheduling" - extra_env: - VLLM_USE_MODELSCOPE: "True" - PYTORCH_NPU_ALLOC_CONF: "expandable_segments:True" - HCCL_BUFFSIZE: "512" - OMP_PROC_BIND: "false" - OMP_NUM_THREADS: "1" - TASK_QUEUE_ENABLE: "1" fp8: model_id: "Qwen/Qwen3.6-27B-FP8" precision: fp8 @@ -247,90 +217,6 @@ variants: label: "FP8 (RedHat)" vram_minimum_gb: 33 description: "RedHatAI FP8 dynamic quantization" - ascend_w8a8_mxfp8: - model_id: "Eco-Tech/Qwen3.6-27B-w8a8-mxfp8" - precision: mxfp8 - label: "W8A8 (Ascend)" - vram_minimum_gb: 34 - supported_hardware: [ascend_950dt] - tp: 1 - min_vllm_version: "0.23.0" - default_modes: - spec_decoding: qwen3_5_mtp - extra_args: - - "--distributed-executor-backend" - - "mp" - - "--data-parallel-size" - - "8" - - "--quantization" - - "ascend" - - "--served-model-name" - - "qwen3.6" - - "--max-num-seqs" - - "32" - - "--max-model-len" - - "262144" - - "--max-num-batched-tokens" - - "16384" - - "--gpu-memory-utilization" - - "0.90" - - "--no-enable-prefix-caching" - - "--compilation-config" - - '{"cudagraph_mode":"FULL_DECODE_ONLY"}' - - "--additional-config" - - '{"enable_cpu_binding":true}' - extra_env: - VLLM_USE_MODELSCOPE: "True" - PYTORCH_NPU_ALLOC_CONF: "expandable_segments:True" - HCCL_BUFFSIZE: "512" - OMP_PROC_BIND: "false" - OMP_NUM_THREADS: "1" - TASK_QUEUE_ENABLE: "1" - hardware_overrides: - ascend_950dt: - docker_image: "quay.io/ascend/vllm-ascend:v0.23.0-a5" - ascend_w8a8: - model_id: "Eco-Tech/Qwen3.6-27B-w8a8" - precision: int8 - label: "W8A8 (Ascend A3)" - vram_minimum_gb: 33 - supported_hardware: [ascend_910c] - tp: 2 - min_vllm_version: "0.23.0" - default_modes: - spec_decoding: qwen3_5_mtp - extra_args: - - "--data-parallel-size" - - "8" - - "--quantization" - - "ascend" - - "--served-model-name" - - "qwen3.6" - - "--max-num-seqs" - - "32" - - "--max-model-len" - - "133000" - - "--max-num-batched-tokens" - - "16384" - - "--gpu-memory-utilization" - - "0.90" - - "--no-enable-prefix-caching" - - "--compilation-config" - - '{"cudagraph_mode":"FULL_DECODE_ONLY"}' - - "--additional-config" - - '{"enable_cpu_binding":true}' - - "--async-scheduling" - extra_env: - VLLM_USE_MODELSCOPE: "True" - PYTORCH_NPU_ALLOC_CONF: "expandable_segments:True" - HCCL_BUFFSIZE: "512" - OMP_PROC_BIND: "false" - OMP_NUM_THREADS: "1" - TASK_QUEUE_ENABLE: "1" - hardware_overrides: - ascend_910c: - docker_image: "swr.cn-southwest-2.myhuaweicloud.com/base_image/ascend-ci/vllm-ascend:v0.23.0-a3" - description: "ModelSlim INT8 W8A8 checkpoint for one Atlas 800 A3 node (16x Ascend 910C), using TP2+DP8. Requires --quantization ascend." compatible_strategies: - single_node_tp diff --git a/recipes/models/Qwen/Qwen3.8-27B.yaml b/recipes/models/Qwen/Qwen3.8-27B.yaml index 1f58d069472a..bed45a33b168 100644 --- a/recipes/models/Qwen/Qwen3.8-27B.yaml +++ b/recipes/models/Qwen/Qwen3.8-27B.yaml @@ -28,11 +28,6 @@ meta: rtx_5090_2x: verified rtx_pro_6000: verified dgx_spark_gb10: verified - # Huawei Ascend NPU, verified 2026-08-18 on vLLM Ascend 0.23.0 (image - # quay.io/ascend/vllm-ascend:qwen3.8-a5). The `ascend_w8a8` variant booted on a - # single 950PR at TP1 with the in-checkpoint MTP head and FULL_DECODE_ONLY - # ACLGraph and answered a long-form prompt correctly. - ascend_950pr: verified arc_pro_b60: verified arc_pro_b70: verified @@ -189,14 +184,7 @@ variants: # (quantization_config.weight_block_size [128, 128]). vram_minimum_gb: 38 description: "Official block-scaled FP8 checkpoint — 28.7 GiB: 1 GPU." - # ascend_950pr added 2026-08-26: vllm-project/vllm-ascend#14852 dispatches - # `quant_method: fp8` + `weight_block_size` in AscendFp8Config.get_quant_method and - # re-quantizes the block-scaled tiles to MXFP8 at load time on 950. No - # `--quantization ascend` — the scheme is picked up from the checkpoint config. - supported_hardware: [h100, h200, b200, b300, gb200, gb300, mi300x, mi325x, mi355x, rtx_5090, rtx_5090_2x, ascend_950pr] - hardware_overrides: - ascend_950pr: - docker_image: "quay.io/ascend/vllm-ascend:nightly-main-a5" + supported_hardware: [h100, h200, b200, b300, gb200, gb300, mi300x, mi325x, mi355x, rtx_5090, rtx_5090_2x] nvfp4: model_id: "Inferact/Qwen3.8-27B-NVFP4" precision: nvfp4 @@ -276,40 +264,6 @@ variants: supported_hardware: [h100, h200, b200, b300, gb200, gb300, rtx_5090, rtx_5090_2x] description: "Red Hat AI compressed-tensors MXFP4 build — 19.8 GB: 1 GPU. True W4A4 on Blackwell (SM100+); W4A16 via Marlin on Hopper and elsewhere." - ascend_w8a8: - model_id: "Eco-Tech/Qwen3.8-27B-w8a8" - precision: int8 - label: "W8A8 (Ascend)" - # 32,128,768,176 B across 10 shards = 32.1 GB. ModelSlim keeps the vision tower in BF16 - # and stores FP32 per-channel scales, so this is not a uniform 8-bit build. - vram_minimum_gb: 39 - supported_hardware: [ascend_950pr] - default_modes: - spec_decoding: qwen3_5_mtp - # `--quantization ascend` is what selects the ModelSlim scheme; without it the loader - # treats the checkpoint as float. ACLGraph replays decode only. CPU binding cuts - # cross-core scheduling overhead on the Kunpeng host. - extra_args: - - "--quantization" - - "ascend" - - "--compilation-config" - - '{"cudagraph_mode":"FULL_DECODE_ONLY"}' - - "--additional-config" - - '{"enable_cpu_binding":true}' - - "--max-num-batched-tokens" - - "16384" - extra_env: - PYTORCH_NPU_ALLOC_CONF: "expandable_segments:True" - HCCL_BUFFSIZE: "512" - OMP_PROC_BIND: "false" - OMP_NUM_THREADS: "1" - # `computeDockerMeta` has no Huawei brand, so a bare string `docker_image` would only pin - # NVIDIA. The exact variant+hardware override lands regardless of brand. - hardware_overrides: - ascend_950pr: - docker_image: "quay.io/ascend/vllm-ascend:qwen3.8-a5" - description: "ModelSlim INT8 W8A8 for Ascend NPU — 32.1 GB: 1 NPU. Requires --quantization ascend." - # Dense model: no routed experts, so none of the expert-parallel or PD strategies apply. # # `multi_node_tp` is deliberately ABSENT, unlike the 2.4T sibling. Multi-node TP sets @@ -457,74 +411,7 @@ guide: | official vLLM XPU image `vllm/vllm-openai-xpu:latest`. `--enforce-eager` is required on this backend. Select Arc Pro B60/B70 in the hardware picker above for the exact command. - ### Huawei Ascend 950PR-w8a8 (vLLM Ascend) - - Verified on a single 950PR, TP1, vLLM Ascend 0.23.0 from the - `quay.io/ascend/vllm-ascend:qwen3.8-a5` image. - - The ModelSlim INT8 build is the checkpoint to use here. `--quantization ascend` is what - selects the ModelSlim scheme — without it the loader treats the weights as float. The MTP - head inside the checkpoint works and is spelled `qwen3_5_mtp` on this backend. - - Both the weight and the image come from ModelScope, not HuggingFace: set - `VLLM_USE_MODELSCOPE=True` or pass a local path. - - ```bash - export VLLM_USE_MODELSCOPE=True - export PYTORCH_NPU_ALLOC_CONF=expandable_segments:True - export HCCL_BUFFSIZE=512 - export OMP_PROC_BIND=false - export OMP_NUM_THREADS=1 - - vllm serve Eco-Tech/Qwen3.8-27B-w8a8 \ - --tensor-parallel-size 1 \ - --quantization ascend \ - --max-num-batched-tokens 16384 \ - --enable-prefix-caching \ - --reasoning-parser qwen3 \ - --tool-call-parser qwen3_coder \ - --mm-encoder-tp-mode data \ - --speculative-config '{"method":"qwen3_5_mtp","num_speculative_tokens":3,"enforce_eager":true}' \ - --compilation-config '{"cudagraph_mode":"FULL_DECODE_ONLY"}' \ - --additional-config '{"enable_cpu_binding":true}' - ``` - - `--reasoning-parser qwen3` is not optional in practice: the chat template opens every - assistant turn with ``, so without it the entire reasoning block lands in - `message.content` and a 2048-token budget can be spent before the answer starts. - - Measured single-stream decode: 64 tok/s (2048-token completion, one request, MTP on). - Read MTP acceptance from `vllm:spec_decode_num_{accepted,draft}_tokens_total` before - tuning `num_speculative_tokens` — throughput alone cannot tell a working drafter from - one that loaded and was ignored. - - ### Huawei Ascend 950PR-Native block-scaled FP8 (vLLM Ascend) - - Verified on a single 950PR, TP1, vLLM Ascend 0.23.0 from the - `quay.io/ascend/vllm-ascend:nightly-main-a5` image. - - The official `Qwen/Qwen3.8-27B-FP8` checkpoint serves directly on a 950PR — no offline - re-quantization and **no `--quantization ascend`**. vLLM Ascend reads `quant_method: fp8` - and `weight_block_size` straight from the checkpoint, expands the `weight_scale_inv` - tiles at load time and re-quantizes to MXFP8 on 950. - - Requires a vLLM Ascend build that includes - [vllm-ascend#14852](https://github.com/vllm-project/vllm-ascend/pull/14852) (merged - 2026-08-26); no tagged release carries it yet. The W8A8 path above is unaffected. - - ```bash - vllm serve Qwen/Qwen3.8-27B-FP8 \ - --tensor-parallel-size 1 \ - --max-model-len 128000 \ - --max-num-seqs 16 \ - --gpu-memory-utilization 0.85 \ - --trust-remote-code \ - --enable-prefix-caching \ - --reasoning-parser qwen3 \ - --tool-call-parser qwen3_coder \ - --mm-encoder-tp-mode data \ - --speculative-config '{"method": "qwen3_5_mtp", "num_speculative_tokens": 3, "enforce_eager": true}' - ``` + ### DFlash2 (opt-in) diff --git a/recipes/models/autotrust/JEV-27B-VL.yaml b/recipes/models/autotrust/JEV-27B-VL.yaml new file mode 100644 index 000000000000..61a0ae1327ac --- /dev/null +++ b/recipes/models/autotrust/JEV-27B-VL.yaml @@ -0,0 +1,200 @@ +# yaml-language-server: $schema=https://recipes.vllm.ai/recipe.schema.json + +meta: + title: "JEV-27B-VL" + slug: "jev-27b-vl" + provider: "AutoTrust" + description: "Dense 27B vision-language decision model on a Qwen3.8-27B backbone, with a rank-32 LoRA (jev-decision) for calibrated choices over text and images." + date_added: 2026-10-28 + date_updated: 2026-10-28 + tasks: + - multimodal + - text + performance_headline: "Qwen3.8-27B VL backbone; System 1 decisions through the jev-decision LoRA" + related_recipes: + - "Qwen/Qwen3.8-27B" + +model: + model_id: "autotrust/JEV-27B-VL" + # Model author: this checkpoint only works on vLLM >= 0.32.0. + # Qwen3_5ForConditionalGeneration is registered in that release. + min_vllm_version: "0.32.0" + architecture: dense + # safetensors parameter total is 27,781,427,952 BF16 (vision tower included). + parameter_count: "27B" + active_parameters: "27B" + # text_config.max_position_embeddings. The validated serve line caps the + # window at 32768 via --max-model-len; 256K is the backbone native length. + context_length: 262144 + base_args: + - "--served-model-name" + - "autotrust/JEV-27B-VL" + - "--enable-lora" + - "--max-lora-rank" + - "32" + - "--lora-modules" + - "jev-decision=JEV-27B-VL/adapter_vllm" + - "--logprobs-mode" + - "processed_logprobs" + - "--max-model-len" + - "32768" + - "--enable-prefix-caching" + - "--mamba-cache-mode" + - "align" + - "--limit-mm-per-prompt" + - '{"image": 8}' + - "--max-num-seqs" + - "8" + - "--trust-request-chat-template" + base_env: {} + +# Host checkout for the LoRA. vLLM serve still pulls weights by model_id; +# --lora-modules needs adapter_vllm on disk. Same subdirectory on the +# ModelScope checkout. install_modes includes docker because the image +# cannot fetch this relative path for you. +dependencies: + - note: "Local layout for LoRA module jev-decision. After this download the adapter is JEV-27B-VL/adapter_vllm; a ModelScope checkout uses the same adapter_vllm directory under whatever folder you chose." + command: "hf download autotrust/JEV-27B-VL --local-dir JEV-27B-VL" + install_modes: [pip, docker] + +features: + text_only: + description: "Skip the vision encoder for text-only workloads and free its memory for KV cache." + args: + - "--language-model-only" + encoder_parallel: + description: "Run the vision encoder data-parallel across the tensor-parallel ranks instead of sharding it." + args: + - "--mm-encoder-tp-mode" + - "data" + +opt_in_features: + - text_only + - encoder_parallel + +variants: + default: + precision: bf16 + # 55,562,855,904 bytes on disk; ceil(55.563 GB × 1.2) = 67. + # Uniform BF16, so params × 2 × 1.2 lands on the same budget. + vram_minimum_gb: 67 + description: "BF16 weights (about 52–56 GB). The model card's quick start targets one GPU with 80 GB or more; this recipe does not mark any GPU verified." + +compatible_strategies: + - single_node_tp + - multi_node_tp + - multi_node_tp_pp + +# serve.sh does not set tensor-parallel-size: one GPU. 67 GB fits a single +# high-memory GPU, so the single-node default stays TP 1 instead of the +# whole node. +strategy_overrides: + single_node_tp: + tp: 1 + +guide: | + ## Overview + + [JEV-27B-VL](https://huggingface.co/autotrust/JEV-27B-VL) is AutoTrust's + vision-language decision model. The checkpoint is a dense BF16 + `Qwen3_5ForConditionalGeneration` model (about 27.8B parameters, vision + tower included) continued from + [Qwen/Qwen3.8-27B](https://huggingface.co/Qwen/Qwen3.8-27B). System 2 is + the served checkpoint, named `autotrust/JEV-27B-VL`. System 1 is the + rank-32 LoRA `jev-decision` (`adapter_vllm/`, `r` 32, `lora_alpha` 64), + used for calibrated choice probabilities over text or images. + + This recipe has no `verified` hardware entry. The model card's quick start + asks for one GPU with 80 GB or more, and the card's published latency + figures are from a B200. Those runs are the author's, not a badge on this + page. + + ## Prerequisites + + - **vLLM:** >= 0.32.0. Earlier releases are not supported for this + checkpoint. + - **Weights:** `hf download autotrust/JEV-27B-VL --local-dir JEV-27B-VL` + so the LoRA path below exists. The same `adapter_vllm` directory is in + the [ModelScope](https://modelscope.cn/models/autotrust/JEV-27B-VL) + checkout; if that folder is not named `JEV-27B-VL`, point the module at + `/adapter_vllm` (the HF snapshot path ends in + `snapshots//adapter_vllm`). + - **Docker:** the image only bind-mounts the Hugging Face cache. Mount the + checkout into the container and keep `jev-decision` pointed at that + `adapter_vllm` directory. + - **Context:** `text_config.max_position_embeddings` is 262144. The + validated command uses `--max-model-len 32768` (the `serve.sh` default). + The card says a full 256K prompt needs about 17 GB of KV cache on top of + the weights, and suggests `--max-model-len 131072` on an 80 GB GPU. + + ## Decision LoRA and serve flags + + These flags are the model card's validated `vllm serve` line. They are + base arguments, not optional toggles. + + - **`jev-decision`** — `--enable-lora`, `--max-lora-rank 32`, and + `--lora-modules jev-decision=JEV-27B-VL/adapter_vllm`. The module name + stays `jev-decision`. The path is the local checkout + `JEV-27B-VL/adapter_vllm`, or the `adapter_vllm` directory inside an + HF or ModelScope checkout. + - **`--mamba-cache-mode align`** — the Qwen3.8 hybrid backbone mixes + full attention with Mamba-style layers; the card serves it in `align` + mode. + - **`--limit-mm-per-prompt '{"image": 8}'`** — at most 8 images per + request. + - **`--max-num-seqs 8`** — required. The card says that with more than 8 + sequences in one batch, vLLM's LoRA path for this multimodal class + returns wrong System 1 probabilities. Extra requests queue. + - **`--logprobs-mode processed_logprobs`** — System 1 reads post-processor + logprobs. On `/v1/chat/completions` or `/v1/completions` for + `jev-decision`, the card also says to send `top_k: 0` and `top_p: 1.0`: + `generation_config.json` sets `top_k` 20 and `top_p` 0.95, and those + defaults would truncate the returned distribution. + - **`--trust-request-chat-template`** — image decisions send a raw + decision chat template on the request. System 2 keeps the checkpoint's + own template. + - **`--served-model-name autotrust/JEV-27B-VL`** — public name for + System 2. `--enable-prefix-caching` matches the card's serve line. + + `POST /v1/decide` is not part of stock `vllm serve`. The checkpoint ships + `serve_decide.py`, which starts the OpenAI server with these same flags + and adds that route. Stock `vllm serve` still exposes System 2 as + `autotrust/JEV-27B-VL` and System 1 as the LoRA model `jev-decision`. + + ## Client usage + + System 2 (the base checkpoint), with an image: + + ```python + client.chat.completions.create( + model="autotrust/JEV-27B-VL", + messages=[{ + "role": "user", + "content": [ + {"type": "image_url", "image_url": {"url": image_url}}, + {"type": "text", "text": "What does this image show?"}, + ], + }], + ) + ``` + + System 1 through the checkpoint's `/v1/decide` route (`serve_decide.py`): + + ```bash + curl localhost:8000/v1/decide -H 'Content-Type: application/json' -d '{ + "kind": "choice", + "state": "Customer: my card was charged twice for one coffee.", + "question": "Which team should handle this?", + "options": ["billing", "shipping", "tech support"]}' + ``` + + Text-only System 1 calls the LoRA directly (`model` `jev-decision`). The + card says those text decisions match + [autotrust/JEV-27B](https://huggingface.co/autotrust/JEV-27B). + + ## References + + - [Hugging Face model card](https://huggingface.co/autotrust/JEV-27B-VL) + - [ModelScope (autotrust/JEV-27B-VL)](https://modelscope.cn/models/autotrust/JEV-27B-VL) + - [ModelScope mirror listed on the card (AutoTrust-Labs/JEV-27B-VL)](https://www.modelscope.ai/models/AutoTrust-Labs/JEV-27B-VL) + - [Base model: Qwen3.8-27B](https://huggingface.co/Qwen/Qwen3.8-27B) diff --git a/recipes/models/deepseek-ai/DeepSeek-V4-Flash-Vision-Exp.yaml b/recipes/models/deepseek-ai/DeepSeek-V4-Flash-Vision-Exp.yaml index 0337d41783ca..eb45f98f8129 100644 --- a/recipes/models/deepseek-ai/DeepSeek-V4-Flash-Vision-Exp.yaml +++ b/recipes/models/deepseek-ai/DeepSeek-V4-Flash-Vision-Exp.yaml @@ -17,10 +17,6 @@ meta: # NVIDIA hardware with a published end-to-end run on that branch: TP4 + EP # on one NVL4 tray, FP8 KV cache, block size 256, DSpark k=3, full # 1000-sample OCRBench at 83.5% with zero request errors. - # Atlas A5 8-card mixed serving of the native FP4+FP8 Vision-Exp checkpoint - # (DP4+EP, TP=1, no speculative decoding). 950PR verified 2026-09-10 on - # vLLM Ascend; 950DT uses the same command (4 of 8 NPUs). See the hardware - # note for the required hash-router bias fix. # AMD MI350X (gfx950): ROCm vision merged in vllm-project/vllm#55107 (2026-09-11) # on the #54566 base. Verified text+vision on stock ROCm nightly at single-node # TP2 and TP4, with DSpark k=3 (needs VLLM_USE_V2_MODEL_RUNNER=1). See @@ -29,8 +25,6 @@ meta: hardware: gb200: verified mi350x: verified - ascend_950dt_8x: verified - ascend_950pr_8x: verified model: model_id: "deepseek-ai/DeepSeek-V4-Flash-Vision-Exp" @@ -74,13 +68,6 @@ features: - "deepseek_v4" - "--reasoning-config" - '{"reasoning_parser":"deepseek_v4","reasoning_start_str":"","reasoning_end_str":""}' - # Empty start/end strings make the Ascend chat path emit content=null - # for a full completion. Keep the parser, drop the override. - hardware_overrides: - npu: - args: - - "--reasoning-parser" - - "deepseek_v4" # Single method, so the boolean form (not `modes`): the checkpoint's fused # DSpark module is the only drafter with a published run on this branch. Left # OUT of opt_in_features on purpose — the verified GB200 configuration had @@ -92,9 +79,6 @@ features: args: - "--speculative-config" - '{"method":"dspark","model":"deepseek-ai/DeepSeek-V4-Flash-Vision-Exp","num_speculative_tokens":3,"draft_sample_method":"probabilistic","enable_adaptive_verification":true}' - # 950 has no working MTP or DSpark path yet. Empty args so a toggle-on - # still emits no --speculative-config. Default-off via - # hardware_opt_in_features below — GB200 keeps DSpark on. hardware_overrides: # ROCm: the DeepseekV4 indexer backend rejects adaptive verification, so it # is forced false (DSpark still drafts k=3). VLLM_USE_V2_MODEL_RUNNER=1 @@ -103,15 +87,6 @@ features: args: - "--speculative-config" - '{"method":"dspark","model":"deepseek-ai/DeepSeek-V4-Flash-Vision-Exp","num_speculative_tokens":3,"draft_sample_method":"probabilistic","enable_adaptive_verification":false}' - npu: - args: [] - -# Spec decoding stays default-on everywhere except the A5 8-card pills. -hardware_opt_in_features: - ascend_950pr_8x: - - spec_decoding - ascend_950dt_8x: - - spec_decoding variants: default: @@ -120,18 +95,6 @@ variants: # underestimates: sized from the real checkpoint (167.8 GB x 1.2). vram_minimum_gb: 202 description: "Native FP4+FP8 mixed checkpoint (MoE experts FP4, attention/norm/router FP8) with a BF16 vision tower, aligner, and the fused DSpark draft module." - hardware_overrides: - # Exact-GPU pin: computeDockerMeta has no Huawei brand, so the model-level - # docker_image string only affects NVIDIA. Hide pip — official wheels - # have no NPU kernels. - ascend_950pr_8x: - docker_image: "quay.io/ascend/vllm-ascend:v0.23.0-a5" - install: - pip: false - ascend_950dt_8x: - docker_image: "quay.io/ascend/vllm-ascend:v0.23.0-a5" - install: - pip: false compatible_strategies: - single_node_tp @@ -143,23 +106,6 @@ compatible_strategies: # TP + EP is the verified layout (TP4 + EP on one GB200 NVL4 tray). default_strategy: single_node_tep -# Atlas A5 mixed serving is DP+EP only. The native checkpoint has 64 -# attention heads; 950's FP8 DSA operator accepts local num_heads_q of -# 64 or 128, so TP must stay 1. TEP/TP and intra-node PD are not offered. -strategy_hardware: - single_node_tp: - npu: unsupported - single_node_tep: - npu: unsupported - multi_node_tep: - npu: unsupported - multi_node_dep: - npu: unsupported - -# Shared by 950DT 8x (8×96 GB) and 950PR 8x (8×128 GB). Mixed serving uses -# 4 of the 8 NPUs (DP4+EP, TP=1). HCCL_BUFFSIZE=1024 is required for MC2 -# dispatch. last-wins shadows the DEP builder's --data-parallel-size 8 -# and the recipe-level --block-size 256 / --kv-cache-dtype fp8. hardware_overrides: amd: extra_args: @@ -173,49 +119,6 @@ hardware_overrides: VLLM_ROCM_USE_AITER: "1" VLLM_USE_BREAKABLE_CUDAGRAPH: "1" VLLM_USE_V2_MODEL_RUNNER: "1" - npu: - extra_args: - - "--data-parallel-size" - - "4" - - "--block-size" - - "128" - - "--kv-cache-dtype" - - "auto" - - "--tokenizer-mode" - - "deepseek_v4" - - "--safetensors-load-strategy" - - "prefetch" - - "--async-scheduling" - - "--max-model-len" - - "8192" - - "--max-num-seqs" - - "16" - - "--max-num-batched-tokens" - - "2048" - - "--gpu-memory-utilization" - - "0.9" - - "--api-server-count" - - "1" - - "--compilation-config" - - '{"cudagraph_mode":"FULL_DECODE_ONLY"}' - - "--additional-config" - - '{"enable_cpu_binding": true, "multistream_overlap_shared_expert": true, "enable_shared_expert_dp": true}' - extra_env: - PYTORCH_NPU_ALLOC_CONF: "expandable_segments:True" - OMP_PROC_BIND: "false" - OMP_NUM_THREADS: "10" - HCCL_BUFFSIZE: "1024" - HCCL_OP_EXPANSION_MODE: "AIV" - TASK_QUEUE_ENABLE: "1" - ASCEND_RT_VISIBLE_DEVICES: "0,1,2,3" - -hardware_notes: - ascend_950dt_8x: - title: "vLLM Ascend A5 image required" - body: "Mixed serving on one 8-card 950DT node uses 4 of 8 NPUs (DP4+EP, TP=1). Same command as 950PR. Pin quay.io/ascend/vllm-ascend:v0.23.0-a5 — the NVIDIA pip wheel cannot run here. Set HCCL_BUFFSIZE=1024 before launch. Native Flash-Vision checkpoints need the hash-router bias skip in vllm-project/vllm-ascend#16259 — without it load_weights aborts on model.layers.0.mlp.gate.e_score_correction_bias." - ascend_950pr_8x: - title: "vLLM Ascend A5 image required" - body: "Mixed serving on one 8-card 950PR node uses 4 of 8 NPUs (DP4+EP, TP=1). Same command as 950DT. Pin quay.io/ascend/vllm-ascend:v0.23.0-a5 and set HCCL_BUFFSIZE=1024 before launch. Native Flash-Vision checkpoints need the hash-router bias skip in vllm-project/vllm-ascend#16259 — without it load_weights aborts on model.layers.0.mlp.gate.e_score_correction_bias." guide: | ## Overview diff --git a/recipes/models/deepseek-ai/DeepSeek-V4-Flash.yaml b/recipes/models/deepseek-ai/DeepSeek-V4-Flash.yaml index 2ccd6acacbaa..ccbbdf16c9ba 100644 --- a/recipes/models/deepseek-ai/DeepSeek-V4-Flash.yaml +++ b/recipes/models/deepseek-ai/DeepSeek-V4-Flash.yaml @@ -21,17 +21,6 @@ meta: mi300x: verified mi325x: verified mi355x: verified - # Atlas A5 8-card mixed serving of FP8 (Preview). 950PR verified 2026-08-29 - # on vLLM Ascend 0.23.0 (DP4+EP, MTP k=1). 950DT matches the v0.23.0 - # vLLM Ascend mixed recipe (same command, 4 of 8 NPUs). - # 950PR additionally verified 2026-09-09 on FP8 (0731) with DSpark k=5, - # same DP4+EP command; 950DT follows it. See the hardware note for the - # required fix. - ascend_950dt_8x: verified - ascend_950pr_8x: verified - # Atlas 800 A3 (16× Ascend 910C, 64 GB each). W8A8 quantized checkpoint, - # DP2+TP8 on all 16 NPUs. Verified 2026-09-23 on vLLM Ascend v0.23.0-a3. - ascend_910c: verified model: model_id: "deepseek-ai/DeepSeek-V4-Flash" @@ -94,13 +83,6 @@ features: args: - "--speculative-config" - '{"method":"mtp","num_speculative_tokens":1}' - # Preview MTP head is next-1. vLLM Ascend 0.23.0 runs the draft eager - # while the target keeps FULL_DECODE_ONLY ACLGraph. DSpark is not in - # this release; do not raise k or switch method on the preview weights. - npu: - args: - - "--speculative-config" - - '{"method":"mtp","num_speculative_tokens":1,"enforce_eager":true}' dspark: label: "DSpark" description: "DSpark drafting, 7 draft tokens, probabilistic sampling. Requires the DSpark or 0731 checkpoint (Variant row)." @@ -117,13 +99,6 @@ features: args: - "--speculative-config" - '{"method":"dspark","num_speculative_tokens":5,"draft_sample_method":"probabilistic","attention_backend":"B12X_MLA_SPARSE"}' - # 950PR was verified at k=5 with an eager draft while the target keeps - # its FULL_DECODE_ONLY ACLGraph. vLLM Ascend does not implement - # probabilistic draft sampling, so that key is left off. - npu: - args: - - "--speculative-config" - - '{"method":"dspark","num_speculative_tokens":5,"enforce_eager":true}' dflash: label: "DFlash" @@ -145,12 +120,7 @@ variants: precision: fp8 vram_minimum_gb: 200 description: "Official DeepSeek-V4-Flash release (2026-07-31) superseding the preview — substantially stronger agentic performance, with the DSpark draft module attached." - # 950PR does load this checkpoint's DSpark draft module: the v0.23.0-a5 tag - # currently ships vLLM Ascend main (0.1.dev1+g6e6aad57e), not the 0.23.0 - # release. 950DT is offered on the same grounds the FP8 (Preview) row - # already uses — one A5 profile, one command, 4 of 8 NPUs — and its 4x96 GB - # clears this variant's 200 GB minimum. 0731 itself was run on 950PR. - supported_hardware: [arch:sm90, arch:sm100, arch:sm103, arch:gfx942, mi355x, rtx_pro_6000_8x, dgx_spark_gb10, ascend_950pr_8x, ascend_950dt_8x] + supported_hardware: [arch:sm90, arch:sm100, arch:sm103, arch:gfx942, mi355x, rtx_pro_6000_8x, dgx_spark_gb10] min_vllm_version: "0.25.0" docker_image: "vllm/vllm-openai:v0.28.0" default_modes: @@ -158,16 +128,6 @@ variants: hardware_overrides: rtx_pro_6000_8x: docker_image: "vllm/vllm-openai:nightly" - # Same pinning as the FP8 (Preview) variant below: the NVIDIA wheel - # carries no NPU kernels, so the pip tab is hidden here. - ascend_950pr_8x: - docker_image: "quay.io/ascend/vllm-ascend:v0.23.0-a5" - install: - pip: false - ascend_950dt_8x: - docker_image: "quay.io/ascend/vllm-ascend:v0.23.0-a5" - install: - pip: false dgx_spark_gb10: docker_image: "eugr/spark-vllm-b12x:latest" # No wheel path here: the B12X args and env below ship only in the @@ -296,17 +256,6 @@ variants: extra_args: - "--moe-backend" - "deep_gemm_mega_moe" - # `computeDockerMeta` has no Huawei brand, so a bare string docker_image - # would only pin NVIDIA. Exact-GPU overrides land regardless of brand. - # The pip tab is hidden: the official vLLM wheel has no NPU kernels. - ascend_950dt_8x: - docker_image: "quay.io/ascend/vllm-ascend:v0.23.0-a5" - install: - pip: false - ascend_950pr_8x: - docker_image: "quay.io/ascend/vllm-ascend:v0.23.0-a5" - install: - pip: false nvfp4: model_id: "nvidia/DeepSeek-V4-Flash-NVFP4" precision: nvfp4 @@ -451,59 +400,6 @@ variants: extra_args: - "--moe-backend" - "deep_gemm_mega_moe" - ascend_w8a8: - # W8A8 quantized checkpoint for Atlas 800 A3 (16× Ascend 910C, 64 GB each). - # TP8 shards the 284B weights across 8 NPUs (35.5 GB each), fitting within - # the 64 GB per-NPU limit. DP2 uses all 16 NPUs (2 replicas × 8 NPUs each). - model_id: "Eco-Tech/DeepSeek-V4-Flash-w8a8-mtp" - label: "W8A8 (Ascend A3)" - precision: int8 - vram_minimum_gb: 341 - description: "ModelSlim INT8 W8A8 checkpoint for one Atlas 800 A3 node (16× Ascend 910C), using DP2+TP8. Requires --quantization ascend." - supported_hardware: [ascend_910c] - tp: - ascend_910c: 8 - min_vllm_version: "0.23.0" - extra_args: - - "--data-parallel-size" - - "2" - - "--quantization" - - "ascend" - - "--max-num-seqs" - - "32" - - "--max-num-batched-tokens" - - "10240" - - "--gpu-memory-utilization" - - "0.90" - - "--block-size" - - "128" - - "--compilation-config" - - '{"cudagraph_mode":"FULL_DECODE_ONLY"}' - - "--async-scheduling" - - "--api-server-count" - - "1" - - "--safetensors-load-strategy" - - "prefetch" - - "--additional-config" - - '{"ascend_compilation_config":{"enable_npugraph_ex":true,"enable_static_kernel":false},"enable_cpu_binding":true,"multistream_overlap_shared_expert":true}' - extra_env: - HCCL_OP_EXPANSION_MODE: "AIV" - HCCL_BUFFSIZE: "2560" - PYTORCH_NPU_ALLOC_CONF: "expandable_segments:True" - OMP_PROC_BIND: "false" - OMP_NUM_THREADS: "10" - TASK_QUEUE_ENABLE: "1" - LD_PRELOAD: "/usr/lib/aarch64-linux-gnu/libjemalloc.so.2:$LD_PRELOAD" - # Override the npu generation-level "0,1,2,3" (for A5) — A3 uses all 16 NPUs. - ASCEND_RT_VISIBLE_DEVICES: "0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15" - # Pull W8A8 weights from ModelScope (Eco-Tech checkpoint). - VLLM_USE_MODELSCOPE: "True" - hardware_overrides: - ascend_910c: - docker_image: "quay.io/ascend/vllm-ascend:v0.23.0-a3" - # Ascend recommends Docker; no official pip wheel for NPU. - install: - pip: false compatible_strategies: - single_node_tp @@ -534,21 +430,8 @@ strategy_hardware: # hardware; this makes the Strategy row agree. multi_node_dep needs no entry: # the Nodes row is pinned to 1 here, which already hides every multi_node # strategy. - # Atlas A5 mixed serving is DP+EP only (vLLM Ascend 0.23.0). TP-only does not - # partition the V4 MoE layers on 950; TEP and intra-node PD are documented - # separately in the vLLM Ascend 1P1D tutorial, not this builder. - single_node_tp: - npu: unsupported - single_node_tep: - npu: unsupported - multi_node_dep: - npu: unsupported pd_cluster: rtx_pro_6000_8x: unsupported - npu: unsupported - # Atlas 800 A3 PD separation (1P1D or multi-node) is documented in the - # vllm-ascend tutorial separately from the A5 single-node mixed path. - ascend_910c: supported hardware_overrides: blackwell: @@ -571,16 +454,6 @@ hardware_overrides: - '{"mode": 3, "cudagraph_mode": "FULL_DECODE_ONLY"}' extra_env: VLLM_ROCM_USE_AITER: "1" - # Shared by 950DT 8x (8×96 GB) and 950PR 8x (8×128 GB). Mixed serving uses 4 of the - # 8 NPUs (DP4+EP). HCCL_BUFFSIZE=1024 is required for MC2 dispatch; 200 MB - # default fails profile_run with aclnnMoeDistributeDispatchV4 561002. - npu: - extra_env: - PYTORCH_NPU_ALLOC_CONF: "expandable_segments:True" - OMP_PROC_BIND: "false" - OMP_NUM_THREADS: "10" - HCCL_BUFFSIZE: "1024" - ASCEND_RT_VISIBLE_DEVICES: "0,1,2,3" strategy_overrides: single_node_tp: @@ -645,38 +518,6 @@ strategy_overrides: extra_args: - "--gpu-memory-utilization" - "0.85" - npu: - # A per-strategy generation override replaces the recipe-level one, and - # DEP is the only strategy the A5 pills can select — so hardware_overrides.npu - # never reaches the command and its env has to be repeated here. Without - # HCCL_BUFFSIZE the MC2 dispatch fails profile_run with - # aclnnMoeDistributeDispatchV4 561002. - extra_env: - PYTORCH_NPU_ALLOC_CONF: "expandable_segments:True" - OMP_PROC_BIND: "false" - OMP_NUM_THREADS: "10" - HCCL_BUFFSIZE: "1024" - ASCEND_RT_VISIBLE_DEVICES: "0,1,2,3" - extra_args: - - "--block-size" - - "32" - - "--kv-cache-dtype" - - "auto" - - "--safetensors-load-strategy" - - "prefetch" - - "--async-scheduling" - - "--max-num-seqs" - - "64" - - "--max-num-batched-tokens" - - "4096" - - "--gpu-memory-utilization" - - "0.9" - - "--api-server-count" - - "1" - - "--compilation-config" - - '{"cudagraph_mode":"FULL_DECODE_ONLY"}' - - "--additional-config" - - '{"enable_cpu_binding": true, "multistream_overlap_shared_expert": true, "enable_shared_expert_dp": true}' multi_node_dep: extra_args: - "--compilation-config" @@ -742,24 +583,6 @@ hardware_notes: link: label: "DGX Spark cluster guide" href: "#dgx-spark-gb10-cluster" - ascend_950dt_8x: - title: "vLLM Ascend A5 image required" - body: "Mixed serving on one 8-card 950DT node uses 4 of 8 NPUs (DP4+EP). FP8 (Preview) runs MTP k=1; FP8 (0731) runs DSpark k=5, verified on 950PR and offered here on the same A5 command. Pin quay.io/ascend/vllm-ascend:v0.23.0-a5 — the NVIDIA pip wheel cannot run here. Set HCCL_BUFFSIZE=1024 before launch. DeepSeek-V4 on 950 also needs the indexer quant_mode fix in vllm-project/vllm-ascend#16104 — without it the image the tag currently ships aborts during warmup with QuantLightningIndexerV2 EZ0021." - link: - label: "Ascend 950DT 8x mixed" - href: "#huawei-ascend-950dt-8x" - ascend_950pr_8x: - title: "vLLM Ascend A5 image required" - body: "Mixed serving on one 8-card 950PR node uses 4 of 8 NPUs (DP4+EP). FP8 (Preview) runs MTP k=1, same command as 950DT; FP8 (0731) runs DSpark k=5. Pin quay.io/ascend/vllm-ascend:v0.23.0-a5 and set HCCL_BUFFSIZE=1024 before launch. DeepSeek-V4 on 950 also needs the indexer quant_mode fix in vllm-project/vllm-ascend#16104 — without it the image the tag currently ships aborts during warmup with QuantLightningIndexerV2 EZ0021." - link: - label: "Ascend 950PR 8x mixed" - href: "#huawei-ascend-950pr-8x" - ascend_910c: - title: "vLLM Ascend A3 image required" - body: "Single-node DP2+TP8 on all 16 NPUs using the W8A8 quantized checkpoint from Eco-Tech. TP8 shards the 284B weights to 35.5 GB per NPU (within the 64 GB limit). Pin quay.io/ascend/vllm-ascend:v0.23.0-a3 — the NVIDIA pip wheel cannot run here. Set HCCL_BUFFSIZE=2560 before launch. The A3 path uses ModelScope to download the W8A8 weights (pip install modelscope). Multi-node PD separation is also available; see the vllm-ascend tutorial." - link: - label: "Ascend 910C A3 reference" - href: "https://docs.vllm.ai/projects/ascend/en/latest/tutorials/models/DeepSeek-V4-Flash.html" guide: | ## Overview diff --git a/recipes/models/deepseek-ai/DeepSeek-V4-Pro.yaml b/recipes/models/deepseek-ai/DeepSeek-V4-Pro.yaml index d57a3fc7c916..6b3f63e2fbc5 100644 --- a/recipes/models/deepseek-ai/DeepSeek-V4-Pro.yaml +++ b/recipes/models/deepseek-ai/DeepSeek-V4-Pro.yaml @@ -4,7 +4,7 @@ meta: provider: "DeepSeek" description: "DeepSeek V4 flagship MoE (1.6T total / 49B active) with hybrid CSA+HCA attention, manifold-constrained hyper-connections, Muon-trained on 32T+ tokens, and three-tier reasoning." date_added: 2026-04-24 - date_updated: 2026-10-13 + date_updated: 2026-10-28 tasks: - text performance_headline: "Frontier 1.6T/49B reasoning MoE with native FP4+FP8 weights, MTP and DSpark speculative decoding, and 1M-token context" @@ -171,6 +171,10 @@ compatible_strategies: # latency-oriented alternative. default_strategy: single_node_tep +strategy_hardware: + single_node_dpa_tp: + mi355x: supported + hardware_overrides: hopper: # H200 (141 GB × 8 = 1128 GB) leaves KV cache tight at 1M context with @@ -275,6 +279,547 @@ strategy_overrides: - "--enable-sleep-mode" env: {} +# Tuned presets (InferenceX AgentX lanes); see SCHEMA.md "presets". +presets: + b200_agentx_tp: + label: "AgentX TP8" + hardware: b200 + strategy: single_node_tp + tp: 8 + variants: [default] + workload: "Agentic (InferenceX AgentX)" + description: "TP8 on one 8-GPU B200 node with DSpark (six draft tokens), FlashInfer sparse MLA and a fixed 50 GiB KV cache per GPU; no host KV offload." + source: { label: "InferenceX #4554", href: "https://github.com/SemiAnalysisAI/InferenceX/pull/4554", key: "dsv4-fp4-b200-vllm-agentic-mtp", revision: 2c044c3 } + image: "vllm/vllm-openai:nightly-8e2beaa3bff595577a069d49d96bde9074294ec2@sha256:3207cb99c0f59b92f7d47a74da8c6eb5467565d811a4f8d9df51df5188f59c70" + select: + features: { spec_decoding: dspark } + # The lane's FlashInfer sparse MLA attention config replaces the builder's + # standalone MXFP4 indexer flag; DSpark drafts six tokens, not seven; the pinned + # image predates --disable-flashinfer-autotune, so keep the --no-enable form. + remove_args: ["--attention_config.indexer_kv_dtype=mxfp4", "--disable-flashinfer-autotune"] + args: + - "--no-enable-flashinfer-autotune" + - "--max-model-len" + - "1048576" + - "--gpu-memory-utilization" + - "0.90" + - "--max-num-batched-tokens" + - "16384" + - "--numa-bind" + - "--enable-cumem-allocator" + - "--disable-uvicorn-access-log" + - "--no-disable-hybrid-kv-cache-manager" + - "--attention-config" + - '{"backend":"FLASHINFER_MLA_SPARSE_DSV4","use_prefill_query_quantization":true,"indexer_kv_dtype":"mxfp4"}' + json_args: + "--speculative-config": { num_speculative_tokens: 6 } + env: + VLLM_USE_V2_MODEL_RUNNER: "1" + VLLM_FLOAT32_MATMUL_PRECISION: "high" + TORCH_CUDA_ARCH_LIST: "10.0" + VLLM_ENGINE_READY_TIMEOUT_S: "7200" + VLLM_RPC_TIMEOUT: "600000" + # --max-num-seqs equals the concurrency. Graphs capture every decode batch of up to + # that many sequences (target plus six drafts each) and mixed batches of 100-500 tokens. + points: + c1: + label: "1" + concurrency: 1 + args: + - "--max-num-seqs" + - "1" + - "--kv-cache-memory-bytes" + - "53687091200" + - "--compilation-config" + - '{"cudagraph_mode":"FULL_AND_PIECEWISE","cudagraph_capture_sizes":[7,100,200,300,400,500]}' + env: + VLLM_DSV4_MEGA_FP8_COMBINE: "1" + c4: + label: "4 (low latency)" + concurrency: 4 + image: "vllm/vllm-openai@sha256:d428631bb917a33d22308265d1b8be91522f862bf9fd133bee62dde3313664ae" + args: + - "--max-num-seqs" + - "4" + - "--kv-cache-memory-bytes" + - "42949672960" + - "--compilation-config" + - '{"cudagraph_mode":"FULL_AND_PIECEWISE","cudagraph_capture_sizes":[7,14,21,28,100,200,300,400,500,640,800,1024,1280,1600,2048,2560,3200,4096,5120,6400,8192,10240,12800,16384],"max_cudagraph_capture_size":16384}' + - "--sparse-indexer-topk-backend" + - "deep_select" + env: + VLLM_DSV4_MEGA_FP8_COMBINE: "1" + c6: + label: "6" + concurrency: 6 + args: + - "--max-num-seqs" + - "6" + - "--kv-cache-memory-bytes" + - "53687091200" + - "--compilation-config" + - '{"cudagraph_mode":"FULL_AND_PIECEWISE","cudagraph_capture_sizes":[7,14,21,28,35,42,100,200,300,400,500]}' + env: + VLLM_DSV4_MEGA_FP8_COMBINE: "1" + c10: + label: "10" + concurrency: 10 + image: "vllm/vllm-openai@sha256:d428631bb917a33d22308265d1b8be91522f862bf9fd133bee62dde3313664ae" + args: + - "--max-num-seqs" + - "10" + - "--kv-cache-memory-bytes" + - "53687091200" + - "--compilation-config" + - '{"cudagraph_mode":"FULL_AND_PIECEWISE","cudagraph_capture_sizes":[7,14,21,28,35,42,49,56,63,70,100,200,300,400,500]}' + - "--prefix-cache-retention-interval" + - "32768" + - "--prefill-schedule-interval" + - "64" + json_args: + "--attention-config": { indexer_tp_shard_min_rows: 4096 } + c14: + label: "14" + concurrency: 14 + args: + - "--max-num-seqs" + - "14" + - "--kv-cache-memory-bytes" + - "53687091200" + - "--compilation-config" + - '{"cudagraph_mode":"FULL_AND_PIECEWISE","cudagraph_capture_sizes":[7,14,21,28,35,42,49,56,63,70,77,84,91,98,100,200,300,400,500]}' + env: + VLLM_DSV4_MEGA_FP8_COMBINE: "1" + runbook: | + The TP8 points of the B200 AgentX lane (`dsv4-fp4-b200-vllm-agentic-mtp` and its low-concurrency + companion, [SemiAnalysisAI/InferenceX#4554](https://github.com/SemiAnalysisAI/InferenceX/pull/4554)) serve the + 0813 checkpoint with DSpark (six draft tokens) as one aggregated vLLM worker on one 8-GPU B200 node: TP8, no + expert parallelism, no host KV offload. Each GPU holds a fixed 50 GiB KV cache and `--max-num-seqs` equals the + concurrency. + + Concurrency 4 is the low-latency variant: a 40 GiB KV cache, the `deep_select` sparse indexer top-k backend and + graph capture up to 16384 tokens. Concurrency 10 sets `--prefix-cache-retention-interval 32768` and + `--prefill-schedule-interval 64`, shards the indexer at 4096 rows (`indexer_tp_shard_min_rows`) and leaves + `VLLM_DSV4_MEGA_FP8_COMBINE` unset. Concurrency 4 and 10 were measured on a second pinned image, shown on + those points. + + Left out of the command: the lane also exported `VLLM_PREFIX_CACHE_RETENTION_INTERVAL`, which vLLM removed in + v0.30.0 ([vllm#55353](https://github.com/vllm-project/vllm/pull/55353)) in favour of the flag above, so it had + no effect on the image it ran on; and `--served-model-name`, which the builder replaces with the checkpoint ID. + The throughput points in InferenceX swap block verification for synthetic rejection at a committed + acceptance length so every point is measured at the same acceptance rate; the command does not set it. + + b200_agentx_dep: + label: "AgentX DEP" + hardware: b200 + strategy: single_node_dep + variants: [default] + workload: "Agentic (InferenceX AgentX)" + description: "Data-parallel ranks with expert parallelism and a Simple CPU KV tier behind a consistent-hash vLLM Router: DEP8 for concurrency 64 to 192, TP4 x DP2 at 32 and TP2 x DP4 at 48 and 64." + source: { label: "InferenceX #4554", href: "https://github.com/SemiAnalysisAI/InferenceX/pull/4554", key: "dsv4-fp4-b200-vllm-agentic-mtp", revision: 2c044c3 } + image: "vllm/vllm-openai:nightly-8e2beaa3bff595577a069d49d96bde9074294ec2@sha256:3207cb99c0f59b92f7d47a74da8c6eb5467565d811a4f8d9df51df5188f59c70" + select: + kv_offload: simple + features: { spec_decoding: dspark } + remove_args: ["--attention_config.indexer_kv_dtype=mxfp4"] + args: + - "--max-model-len" + - "1048576" + - "--gpu-memory-utilization" + - "0.90" + - "--numa-bind" + - "--enable-cumem-allocator" + - "--disable-uvicorn-access-log" + - "--enable-ep-weight-filter" + - "--no-disable-hybrid-kv-cache-manager" + - "--no-enable-flashinfer-autotune" + - "--prefill-schedule-interval" + - "16" + - "--long-prefill-token-threshold" + - "512" + - "--moe-backend" + - "deep_gemm_mega_moe" + - "--attention-config" + - '{"backend":"FLASHINFER_MLA_SPARSE_DSV4","use_prefill_query_quantization":true,"indexer_kv_dtype":"mxfp4"}' + json_args: + "--speculative-config": { num_speculative_tokens: 6 } + "--kv-transfer-config": { kv_connector_extra_config: { cpu_bytes_to_use_per_rank: 241500000000, enable_cross_layers_blocks: "true", lazy_offload: false } } + env: + VLLM_USE_V2_MODEL_RUNNER: "1" + VLLM_FLOAT32_MATMUL_PRECISION: "high" + TORCH_CUDA_ARCH_LIST: "10.0" + VLLM_ENGINE_READY_TIMEOUT_S: "7200" + VLLM_RPC_TIMEOUT: "600000" + PYTORCH_ALLOC_CONF: "expandable_segments:True" + PYTHONHASHSEED: "42" + # --max-num-seqs is per DP rank; --max-num-batched-tokens is an 8192-token prefill budget + # plus seven verification slots per sequence; graphs capture decode batches of 7 to 7 x seqs. + points: + dep8_c64: + label: "64 (DEP8)" + concurrency: 64 + args: + - "--tensor-parallel-size" + - "1" + - "--data-parallel-size" + - "8" + - "--max-num-seqs" + - "16" + - "--max-num-batched-tokens" + - "8304" + - "--kv-cache-memory-bytes" + - "37580963840" + - "--compilation-config" + - '{"cudagraph_mode":"FULL_DECODE_ONLY","cudagraph_capture_sizes":[7,14,21,28,35,42,49,56,63,70,77,84,91,98,105,112],"mode":0}' + env: + VLLM_DSV4_MEGA_FP8_COMBINE: "1" + dep8_c96: + label: "96 (DEP8)" + concurrency: 96 + args: + - "--tensor-parallel-size" + - "1" + - "--data-parallel-size" + - "8" + - "--max-num-seqs" + - "24" + - "--max-num-batched-tokens" + - "8360" + - "--kv-cache-memory-bytes" + - "33285996544" + - "--compilation-config" + - '{"cudagraph_mode":"FULL_DECODE_ONLY","cudagraph_capture_sizes":[7,14,21,28,35,42,49,56,63,70,77,84,91,98,105,112,119,126,133,140,147,154,161,168],"mode":0}' + env: + VLLM_DSV4_MEGA_FP8_COMBINE: "1" + dep8_c128: + label: "128 (DEP8)" + concurrency: 128 + args: + - "--tensor-parallel-size" + - "1" + - "--data-parallel-size" + - "8" + - "--max-num-seqs" + - "32" + - "--max-num-batched-tokens" + - "8416" + - "--kv-cache-memory-bytes" + - "33285996544" + - "--compilation-config" + - '{"cudagraph_mode":"FULL_DECODE_ONLY","cudagraph_capture_sizes":[7,14,21,28,35,42,49,56,63,70,77,84,91,98,105,112,119,126,133,140,147,154,161,168,175,182,189,196,203,210,217,224],"mode":0}' + env: + VLLM_DSV4_MEGA_FP8_COMBINE: "1" + dep8_c160: + label: "160 (DEP8)" + concurrency: 160 + args: + - "--tensor-parallel-size" + - "1" + - "--data-parallel-size" + - "8" + - "--max-num-seqs" + - "40" + - "--max-num-batched-tokens" + - "8472" + - "--kv-cache-memory-bytes" + - "32212254720" + - "--compilation-config" + - '{"cudagraph_mode":"FULL_DECODE_ONLY","cudagraph_capture_sizes":[7,14,21,28,35,42,49,56,63,70,77,84,91,98,105,112,119,126,133,140,147,154,161,168,175,182,189,196,203,210,217,224,231,238,245,252,259,266,273,280],"mode":0}' + env: + VLLM_DSV4_MEGA_FP8_COMBINE: "1" + dep8_c192: + label: "192 (DEP8)" + concurrency: 192 + args: + - "--tensor-parallel-size" + - "1" + - "--data-parallel-size" + - "8" + - "--max-num-seqs" + - "48" + - "--max-num-batched-tokens" + - "8528" + - "--kv-cache-memory-bytes" + - "32212254720" + - "--compilation-config" + - '{"cudagraph_mode":"FULL_DECODE_ONLY","cudagraph_capture_sizes":[7,14,21,28,35,42,49,56,63,70,77,84,91,98,105,112,119,126,133,140,147,154,161,168,175,182,189,196,203,210,217,224,231,238,245,252,259,266,273,280,287,294,301,308,315,322,329,336],"mode":0}' + env: + VLLM_DSV4_MEGA_FP8_COMBINE: "1" + tp4dp2_c32: + label: "32 (TP4 x DP2)" + concurrency: 32 + args: + - "--tensor-parallel-size" + - "4" + - "--data-parallel-size" + - "2" + - "--max-num-seqs" + - "32" + - "--max-num-batched-tokens" + - "8416" + - "--kv-cache-memory-bytes" + - "51539607552" + - "--compilation-config" + - '{"cudagraph_mode":"FULL_DECODE_ONLY","cudagraph_capture_sizes":[7,14,21,28,35,42,49,56,63,70,77,84,91,98,105,112,119,126,133,140,147,154,161,168,175,182,189,196,203,210,217,224],"mode":0}' + tp2dp4_c48: + label: "48 (TP2 x DP4)" + concurrency: 48 + image: "vllm/vllm-openai@sha256:d428631bb917a33d22308265d1b8be91522f862bf9fd133bee62dde3313664ae" + args: + - "--tensor-parallel-size" + - "2" + - "--data-parallel-size" + - "4" + - "--max-num-seqs" + - "24" + - "--max-num-batched-tokens" + - "8360" + - "--kv-cache-memory-bytes" + - "37580963840" + - "--compilation-config" + - '{"cudagraph_mode":"FULL_DECODE_ONLY","cudagraph_capture_sizes":[7,14,21,28,35,42,49,56,63,70,77,84,91,98,105,112,119,126,133,140,147,154,161,168],"mode":0}' + tp2dp4_c64: + label: "64 (TP2 x DP4)" + concurrency: 64 + image: "vllm/vllm-openai@sha256:d428631bb917a33d22308265d1b8be91522f862bf9fd133bee62dde3313664ae" + args: + - "--tensor-parallel-size" + - "2" + - "--data-parallel-size" + - "4" + - "--max-num-seqs" + - "32" + - "--max-num-batched-tokens" + - "8416" + - "--kv-cache-memory-bytes" + - "37580963840" + - "--compilation-config" + - '{"cudagraph_mode":"FULL_DECODE_ONLY","cudagraph_capture_sizes":[7,14,21,28,35,42,49,56,63,70,77,84,91,98,105,112,119,126,133,140,147,154,161,168,175,182,189,196,203,210,217,224],"mode":0}' + runbook: | + The data-parallel points of the B200 AgentX lane (`dsv4-fp4-b200-vllm-agentic-mtp` and its low-concurrency + companion, [SemiAnalysisAI/InferenceX#4554](https://github.com/SemiAnalysisAI/InferenceX/pull/4554)) serve the + 0813 checkpoint with DSpark (six draft tokens) as one engine on one 8-GPU B200 node: DEP8 (TP1 x DP8) for + concurrency 64 to 192, TP4 x DP2 at 32 and TP2 x DP4 at 48 and 64, all with expert parallelism and the Simple + CPU KV tier. Each rank gets 241.5 GB of host memory (1932 GB across the node), so size it to the machine. + `--max-num-seqs` is per DP rank, and `--max-num-batched-tokens` is an 8192-token prefill budget plus seven + verification slots per sequence. The TP4 x DP2 and TP2 x DP4 points leave `VLLM_DSV4_MEGA_FP8_COMBINE` unset. + TP2 x DP4 was measured on a second pinned image, shown on those points. + + The lane fronts the server with vLLM Router 0.1.14 (`--policy consistent_hash`, `--intra-node-data-parallel-size` + equal to the server's DP size, as on the router tab), plus `--request-timeout-secs 14400` so long agentic + requests outlive the default and `--disable-retries` so a failed request is not replayed. Clients send a stable + per-conversation `X-Session-ID` so the router keeps a conversation on the rank that holds its prefix. + + Left out of the command: `VLLM_PREFIX_CACHE_RETENTION_INTERVAL` (removed in vLLM v0.30.0, + [vllm#55353](https://github.com/vllm-project/vllm/pull/55353), so it had no effect), `--served-model-name` + (the builder replaces it with the checkpoint ID) and the router's Prometheus port. The throughput points in + InferenceX swap block verification for synthetic rejection at a committed acceptance length so every point is + measured at the same acceptance rate; the command does not set it. + + mi355x_agentx_tp: + label: "AgentX TP8 + monokernel" + hardware: mi355x + strategy: single_node_tp + variants: [default] + workload: "Agentic coding (InferenceX AgentX)" + description: "Source reproduction: DSpark K6 and monokernel on eight MI355X GPUs; draft communication precision requires resolution." + source: + label: "InferenceX #4526" + href: "https://github.com/SemiAnalysisAI/InferenceX/pull/4526" + key: dsv4-fp4-mi355x-vllm-agentic-mtp + revision: 5ec3c995988e5d3632c1ff53e8ae39da3e799892 + image: + vllm: "vllm/vllm-openai-rocm:nightly-e4a5b5537344b103ed10de7764378ef09604ef3d" + model_revision: 72e1d3230f6c080a530b0a1d46f8eb4602340597 + pip: [Pillow, fastapi, uvicorn] + tp: 8 + select: + frontend: python + features: {tool_calling: true, reasoning: true, spec_decoding: dspark} + kv_offload: off + args: + - "--revision" + - "72e1d3230f6c080a530b0a1d46f8eb4602340597" + - "--tokenizer-revision" + - "72e1d3230f6c080a530b0a1d46f8eb4602340597" + - "--async-scheduling" + - "--data-parallel-size" + - "1" + - "--gpu-memory-utilization" + - "0.86" + - "--moe-backend" + - "aiter" + - "--monokernel" + - "--enable-prefix-caching" + - "--no-disable-hybrid-kv-cache-manager" + env: + VLLM_ENGINE_READY_TIMEOUT_S: "10800" + VLLM_USE_V2_MODEL_RUNNER: "1" + VLLM_ROCM_USE_AITER: "1" + VLLM_ROCM_QUICK_REDUCE_QUANTIZATION: "INT4" + VLLM_ROCM_USE_AITER_MOE: "1" + VLLM_ROCM_USE_AITER_FUSION_SHARED_EXPERTS: "0" + OMP_NUM_THREADS: "1" + json_args: + --compilation-config: {mode: 3, cudagraph_mode: FULL_AND_PIECEWISE} + --speculative-config: {num_speculative_tokens: 6} + points: + c1: {label: "1", concurrency: 1, args: ["--max-num-seqs", "2"]} + c4: {label: "4", concurrency: 4, args: ["--max-num-seqs", "8"]} + c8: {label: "8", concurrency: 8, args: ["--max-num-seqs", "16"]} + c16: {label: "16", concurrency: 16, args: ["--max-num-seqs", "32"]} + c32: {label: "32", concurrency: 32, args: ["--max-num-seqs", "64"]} + c40: {label: "40", concurrency: 40, args: ["--max-num-seqs", "80"]} + c48: {label: "48", concurrency: 48, args: ["--max-num-seqs", "96"]} + c56: {label: "56", concurrency: 56, args: ["--max-num-seqs", "112"]} + c64: {label: "64", concurrency: 64, args: ["--max-num-seqs", "128"]} + runbook: | + This opt-in profile serves [DeepSeek-V4-Pro-0813](https://huggingface.co/deepseek-ai/DeepSeek-V4-Pro-0813) + on one 8× MI355X node. The monokernel is enabled only for the TP8 arm; + [vLLM #61482](https://github.com/vllm-project/vllm/pull/61482) added this + DSpark-compatible path. These presets do not change the generic defaults. + + **Qualification blocked:** the source enables nondefault INT4 QuickReduce + for draft collectives too. For example, 32 active K6 requests produce at + least 192 draft rows, making the BF16 attention-output reduction eligible + for INT4 communication in the pinned image. This conflicts with + [InferenceX's draft-precision rule](https://github.com/SemiAnalysisAI/InferenceX/blob/67de4b34ee1419b2a31c0e7200964a6e78015be4/CONTRIBUTING.md#draft-model-precision). + These are faithful source profiles, pending a source correction and + qualifying validation; they are not a production recommendation. Real + target verification and passing GSM8K do not resolve that precision issue. + + InferenceX installs `Pillow fastapi uvicorn` before engine startup. When + using the pinned image interactively, install these prerequisites inside + the container, then run the generated Python command in that container: + + ```bash + uv pip install Pillow fastapi uvicorn + ``` + + The generated Docker launch does not perform this setup step; provide the + dependencies in its container before using that launch form. + + The source pins the target and tokenizer revisions, but supplies no separate + DSpark draft revision. DSpark loads the native draft from the same model ID + through the image's default mixed FP8/MXFP4 handling; no draft dtype or + quantization override is added. INT4 QuickReduce is shared by eligible + standard-path target and draft collectives, rather than being target-only. + The KV cache is FP8. The image is recorded as a tag, without a source-pinned + digest. Keep inherited environment settings consistent with the preset. + + The generated serving commands use real target verification. For the + **InferenceX throughput sweep only**, merge the following keys into their + speculative config, retaining DSpark K6 and probabilistic drafting: + + ```json + {"rejection_sample_method":"synthetic","synthetic_acceptance_length":3.77} + ``` + + `3.77` comes from the committed + [DeepSeek-V4-Pro thinking-on K6 golden curve](https://github.com/SemiAnalysisAI/InferenceX/blob/5ec3c995988e5d3632c1ff53e8ae39da3e799892/inferencex-e2e/infx/golden_al_distribution/dsv4-pro-0813-dspark.yaml). + Synthetic acceptance is a measurement control; do not use it for production + or accuracy evaluation. The generic Bench command is a smoke benchmark, + not an AgentX replay; reproduce AgentX with the linked InferenceX harness. + + The TP8 scheduler caps are twice the listed client concurrency for AgentX + fan-out. No host-memory offload is enabled. The source's obsolete + `VLLM_PREFIX_CACHE_RETENTION_INTERVAL=32768` has no effect in this image; + the presets retain the effective CLI default of `0` (semantic checkpoints). + + Evidence: [sweep 40260921924, attempt 3](https://github.com/SemiAnalysisAI/InferenceX/actions/runs/40260921924/attempts/3) + for source head `882db2060239f4d849f8b1c93f3b8b192ad3637e` covers all twelve TP8/DPA + throughput points and two real-verification GSM8K evals. The current-head + [run 40626241537](https://github.com/SemiAnalysisAI/InferenceX/actions/runs/40626241537) + only reuses that sweep; its performance/eval matrices are skipped. + Its actual CI merge `f301906060dcfd3b63ac6f77a735698f731f65b1` and the cited + head produce identical native recipe expansion for this lane. + This documentation change does not rerun GPU inference or establish private + agentic accuracy evaluation. + mi355x_agentx_dpa: + label: "AgentX DPA8 + TP experts" + hardware: mi355x + strategy: single_node_dpa_tp + variants: [default] + workload: "Agentic coding (InferenceX AgentX)" + description: "TP1 attention / DP8 with tensor-sharded experts, DSpark K6 and session-affine routing; monokernel disabled." + source: + label: "InferenceX #4526" + href: "https://github.com/SemiAnalysisAI/InferenceX/pull/4526" + key: dsv4-fp4-mi355x-vllm-agentic-mtp + revision: 5ec3c995988e5d3632c1ff53e8ae39da3e799892 + image: + vllm: "vllm/vllm-openai-rocm:nightly-e4a5b5537344b103ed10de7764378ef09604ef3d" + model_revision: 72e1d3230f6c080a530b0a1d46f8eb4602340597 + pip: [Pillow, fastapi, uvicorn, "vllm-router==0.1.14"] + select: + frontend: python + features: {tool_calling: true, reasoning: true, spec_decoding: dspark} + kv_offload: off + args: + - "--revision" + - "72e1d3230f6c080a530b0a1d46f8eb4602340597" + - "--tokenizer-revision" + - "72e1d3230f6c080a530b0a1d46f8eb4602340597" + - "--async-scheduling" + - "--gpu-memory-utilization" + - "0.86" + - "--moe-backend" + - "aiter" + - "--enable-prefix-caching" + - "--no-disable-hybrid-kv-cache-manager" + - "--prefill-schedule-interval" + - "8" + - "--long-prefill-token-threshold" + - "16384" + env: + VLLM_ENGINE_READY_TIMEOUT_S: "10800" + VLLM_USE_V2_MODEL_RUNNER: "1" + VLLM_ROCM_USE_AITER: "1" + VLLM_ROCM_QUICK_REDUCE_QUANTIZATION: "INT4" + VLLM_ROCM_USE_AITER_MOE: "1" + VLLM_ROCM_USE_AITER_FUSION_SHARED_EXPERTS: "0" + OMP_NUM_THREADS: "1" + json_args: + --compilation-config: {mode: 3, cudagraph_mode: FULL_AND_PIECEWISE} + --speculative-config: {num_speculative_tokens: 6} + points: + c64: {label: "64", concurrency: 64, args: ["--max-num-seqs", "64"]} + c128: {label: "128", concurrency: 128, args: ["--max-num-seqs", "128"]} + c192: {label: "192", concurrency: 192, args: ["--max-num-seqs", "192"]} + runbook: | + This is the DP-attention arm of the + [TP8 profile](#preset-mi355x_agentx_tp), using the same checkpoint, image, + runtime prerequisites and real-versus-synthetic verification distinction. + The source qualification blocker described there remains unresolved. + Runtime flags are TP1/DP8 with no expert-parallel flag: experts remain + tensor-sharded across the eight ranks. The source's matrix label TP8/EP1 + describes the aggregate topology, not the attention TP argument. + Monokernel remains disabled (the image default); TP8-only monokernel + eligibility does not extend to this arm. + + Each DP scheduler caps sequences at the listed client concurrency. + The measured prefill interval is `8`; the `16384` long-prefill threshold + exceeds the `8192` token budget and does not bind. + + For exact source routing, replace the generic router companion with this + pinned setup in another shell on the engine's host/container network: + + ```bash + uv pip install vllm-router==0.1.14 + vllm-router \ + --host 0.0.0.0 --port 8000 \ + --worker-urls http://localhost:8100 \ + --intra-node-data-parallel-size 8 \ + --policy consistent_hash \ + --request-timeout-secs 14400 \ + --disable-retries + ``` + + Clients connect to the router on port `8000`, retaining an `X-Session-ID` + across turns of each conversation. InferenceX maps correlation IDs to + this header; `AIPERF_HTTP_X_SESSION_ID_FROM_CORRELATION_ID=1` is a benchmark + client setting, not an engine environment variable. + guide: | ## Overview @@ -726,81 +1271,25 @@ guide: | -d '{"model":"deepseek-ai/DeepSeek-V4-Pro-0813","messages":[{"role":"user","content":"Hello"}],"max_tokens":32}' ``` - ## Agentic benchmark reproduction (InferenceX MI355X sweep) - - The command above is the default serving configuration. The SemiAnalysis InferenceX - MI355X benchmark lane for this checkpoint is a separate high-concurrency benchmark - configuration — not the recipe default. To reproduce that sweep, start from the - default command above and apply these deltas: - - **Source configuration:** - - ```yaml - image: vllm/vllm-openai-rocm:nightly-eed1f3d0c6043bd494424a22443ee198dd56f657 - ``` - - **Applied to both arms:** + ## MI355X AgentX profiles (InferenceX #4526) - - Enable `VLLM_ROCM_QUICK_REDUCE_QUANTIZATION=INT4` (previously commented out). - - Disable `VLLM_ROCM_USE_AITER_FUSION_SHARED_EXPERTS=0` for this nightly. - DSv4-Pro is a mixed checkpoint (MXFP4 routed experts, FP8 shared expert), and - this image fails while loading incompatible scales and shapes when the fused - shared-expert path is enabled. - - Raise `--gpu-memory-utilization` from `0.8` to `0.86`. - - Pin `--max-num-batched-tokens 8192` instead of the nightly default of `16384`. - On the TP8 initialization check this raised GPU KV-cache capacity from `4,730,981` - to `8,524,228` tokens and reduced peak activation memory from `11.44 GiB` to - `8.9 GiB`. + The opt-in **AgentX TP8 + monokernel** and **AgentX DPA8 + TP experts** presets + cover the [InferenceX #4526](https://github.com/SemiAnalysisAI/InferenceX/pull/4526) + agentic-coding lane for + [DeepSeek-V4-Pro-0813](https://huggingface.co/deepseek-ai/DeepSeek-V4-Pro-0813) + on one eight-GPU MI355X node. Both use native DSpark K6, mixed FP8/MXFP4 + weights and FP8 KV cache; only the TP8 arm enables the monokernel. They pin the + source nightly and target/tokenizer revisions, leaving the generic recipe and + the separate fixed-sequence lane unchanged. - **Applied to the DP-attention arm only:** - - - Cap `--max-num-seqs` at `CONC` rather than `2*CONC`. The limit is per scheduler - and DP-attention runs one scheduler per rank. The pure TP8 arm keeps the existing - `2*CONC` headroom for AgentX subagent fan-out. - - Add `--prefill-schedule-interval 8` and `--long-prefill-token-threshold 16384`. - - With `--max-num-batched-tokens 8192` now pinned for both arms, - `--long-prefill-token-threshold 16384` sits above the token budget and therefore - never binds. It is inert rather than harmful, and the DP-attention point was - measured with it present. - - `--compilation-config '{"mode":3,"cudagraph_mode":"FULL_AND_PIECEWISE"}'` is - unchanged; it was already in the checked-in recipe. - - Export the AITER and INT4 quantized all-reduce environment variables before launch: - - ```bash - export VLLM_ROCM_USE_AITER=1 - export VLLM_ROCM_QUICK_REDUCE_QUANTIZATION=INT4 - export VLLM_ROCM_USE_AITER_MOE=1 - export VLLM_ROCM_USE_AITER_FUSION_SHARED_EXPERTS=0 - export VLLM_ENGINE_READY_TIMEOUT_S=10800 - - vllm serve "$MODEL_PATH" --served-model-name "$MODEL" \ - --host 0.0.0.0 \ - --port "$VLLM_BACKEND_PORT" \ - --trust-remote-code \ - --async-scheduling \ - --distributed-executor-backend mp \ - --kv-cache-dtype fp8 \ - --max-num-batched-tokens 8192 \ - --gpu-memory-utilization 0.86 \ - --moe-backend aiter \ - --compilation-config '{"mode":3,"cudagraph_mode":"FULL_AND_PIECEWISE"}' \ - --speculative-config '{"method": "mtp", "num_speculative_tokens": 3, "rejection_sample_method": "synthetic", "synthetic_acceptance_length": 2.49}' \ - --tokenizer-mode deepseek_v4 \ - --tool-call-parser deepseek_v4 \ - --reasoning-parser deepseek_v4 \ - --enable-auto-tool-choice \ - --enable-prefix-caching \ - --prefix-cache-retention-interval 32768 \ - --no-disable-hybrid-kv-cache-manager \ - --max-num-seqs "8" - ``` + These source profiles await resolution of the shared INT4 draft-communication + precision issue described in their runbooks before production qualification. - `--prefix-cache-retention-interval` replaces the `VLLM_PREFIX_CACHE_RETENTION_INTERVAL` - env var, removed in vLLM 0.30.0 - ([vllm-project/vllm#55353](https://github.com/vllm-project/vllm/pull/55353)). + Their generated commands retain real target verification for serving and eval. + The preset runbooks record runtime package prerequisites, the session-affine + DP router, all twelve concurrency points, and the separate throughput-only + synthetic acceptance overlay (`3.77`). The source sweep and real GSM8K evals + supply external validation; no GPU run was performed for this recipe update. ## Fixed-sequence benchmark reproduction (InferenceX MI355X 8K/1K) @@ -947,8 +1436,8 @@ guide: | | Workload | ISL 8192 / OSL 1024, conc 4–512 | agentic-coding traces | | `--gpu-memory-utilization` | `0.8` | `0.86` | | Prefix caching | `--no-enable-prefix-caching` | `--enable-prefix-caching` | - | Speculation | none (STP) or real MTP, 2 tokens | synthetic MTP, 3 tokens | - | `--max-num-seqs` | not passed | `8` | + | Speculation | none (STP) or real MTP, 2 tokens | real DSpark K6 for serving/eval; synthetic AL 3.77 for throughput | + | `--max-num-seqs` | not passed | per point: twice concurrency for TP8, concurrency for DPA8 | | `--max-num-batched-tokens` | not passed | `8192` | The Agentic lane's synthetic acceptance length is a benchmark-comparability diff --git a/recipes/models/deepseek-ai/DeepSeek-V4.1-Flash.yaml b/recipes/models/deepseek-ai/DeepSeek-V4.1-Flash.yaml index ccf9b38f4f83..43d127bd6ad3 100644 --- a/recipes/models/deepseek-ai/DeepSeek-V4.1-Flash.yaml +++ b/recipes/models/deepseek-ai/DeepSeek-V4.1-Flash.yaml @@ -892,6 +892,213 @@ presets: plus `--request-timeout-secs 14400` so long agentic requests outlive the 1800 s default and `--disable-retries` so a failed request is not replayed. + mi325x_agentx: + label: "AgentX TP8 / TP4 / TP2" + hardware: mi325x + strategy: single_node_tp + variants: [fp8] + workload: "Agentic (InferenceX AgentX)" + description: "TP8 with resident Engram tables, TP4 with monokernel and host Engram, and TP2 with host Engram and smaller scheduler and graph budgets; six concurrency points per topology." + source: { label: "InferenceX #4617", href: "https://github.com/SemiAnalysisAI/InferenceX/pull/4617", key: "dsv41flash-fp4-mi325x-vllm-agentic-dspark", revision: 7c375027a3cd50f63153222fa6eb7069ee86d3b4 } + image: "vllm/vllm-openai-rocm:nightly-rocm100-edad8b03c5285a1c12e7e4f83c1af709b319a01b@sha256:009169c114af5b15e25eddc808afb3c1fc3f12c94e746472b16f39fcf39cd716" + select: + frontend: rust + features: { tool_calling: true, reasoning: true, spec_decoding: true, text_only: true, encoder_parallel: false } + dcp: 1 + kv_offload: off + args: + - "--served-model-name" + - "deepseek-ai/DeepSeek-V4.1-Flash" + - "--max-model-len" + - "1048576" + - "--max-num-seqs" + - "128" + - "--max-num-batched-tokens" + - "16384" + - "--max-cudagraph-capture-size" + - "1024" + - "--disable-uvicorn-access-log" + - "--monokernel" + env: + OMP_NUM_THREADS: "1" + PYTHONUNBUFFERED: "1" + json_args: + "--speculative-config": + num_speculative_tokens: 5 + draft_sample_method: probabilistic + rejection_sample_method: block + enable_adaptive_verification: false + "--compilation-config": { cudagraph_mode: FULL_DECODE_ONLY } + points: + tp8_c1: + label: "1 (TP8)" + concurrency: 1 + tp: 8 + remove_args: ["--monokernel"] + json_args: + "--engram-config": { cpu_offload: false } + tp8_c2: + label: "2 (TP8)" + concurrency: 2 + tp: 8 + remove_args: ["--monokernel"] + json_args: + "--engram-config": { cpu_offload: false } + tp8_c4: + label: "4 (TP8)" + concurrency: 4 + tp: 8 + remove_args: ["--monokernel"] + json_args: + "--engram-config": { cpu_offload: false } + tp8_c8: + label: "8 (TP8)" + concurrency: 8 + tp: 8 + remove_args: ["--monokernel"] + json_args: + "--engram-config": { cpu_offload: false } + tp8_c16: + label: "16 (TP8)" + concurrency: 16 + tp: 8 + remove_args: ["--monokernel"] + json_args: + "--engram-config": { cpu_offload: false } + tp8_c32: + label: "32 (TP8)" + concurrency: 32 + tp: 8 + remove_args: ["--monokernel"] + json_args: + "--engram-config": { cpu_offload: false } + tp4_c1: + label: "1 (TP4)" + concurrency: 1 + tp: 4 + tp4_c2: + label: "2 (TP4)" + concurrency: 2 + tp: 4 + tp4_c4: + label: "4 (TP4)" + concurrency: 4 + default: true + tp: 4 + tp4_c8: + label: "8 (TP4)" + concurrency: 8 + tp: 4 + tp4_c16: + label: "16 (TP4)" + concurrency: 16 + tp: 4 + tp4_c32: + label: "32 (TP4)" + concurrency: 32 + tp: 4 + tp2_c1: + label: "1 (TP2)" + concurrency: 1 + tp: 2 + remove_args: ["--monokernel"] + args: + - "--max-num-seqs" + - "16" + - "--max-num-batched-tokens" + - "8192" + - "--max-cudagraph-capture-size" + - "512" + tp2_c2: + label: "2 (TP2)" + concurrency: 2 + tp: 2 + remove_args: ["--monokernel"] + args: + - "--max-num-seqs" + - "16" + - "--max-num-batched-tokens" + - "8192" + - "--max-cudagraph-capture-size" + - "512" + tp2_c4: + label: "4 (TP2)" + concurrency: 4 + tp: 2 + remove_args: ["--monokernel"] + args: + - "--max-num-seqs" + - "16" + - "--max-num-batched-tokens" + - "8192" + - "--max-cudagraph-capture-size" + - "512" + tp2_c8: + label: "8 (TP2)" + concurrency: 8 + tp: 2 + remove_args: ["--monokernel"] + args: + - "--max-num-seqs" + - "16" + - "--max-num-batched-tokens" + - "8192" + - "--max-cudagraph-capture-size" + - "512" + tp2_c16: + label: "16 (TP2)" + concurrency: 16 + tp: 2 + remove_args: ["--monokernel"] + args: + - "--max-num-seqs" + - "32" + - "--max-num-batched-tokens" + - "8192" + - "--max-cudagraph-capture-size" + - "512" + tp2_c32: + label: "32 (TP2)" + concurrency: 32 + tp: 2 + remove_args: ["--monokernel"] + args: + - "--max-num-seqs" + - "64" + - "--max-num-batched-tokens" + - "8192" + - "--max-cudagraph-capture-size" + - "512" + runbook: | + These points serve the 0909 launch checkpoint with five-token DSpark and + real block verification. TP8 keeps Engram in HBM; TP4 and TP2 offload it + to host memory. TP2 uses smaller scheduler, prefill and graph budgets. + Decode uses full graphs, prefill runs eagerly, and decoder SWA bounded + replay keeps the pinned image's enabled default. + + The TP4 monokernel runs eligible decode steps of at most 42 rows, then + falls back to the ordinary path for larger steps. On this image it also + quantizes DSpark draft FFN inputs from BF16 to MXFP8. The profile records + the measured configuration; its [InferenceX draft-precision qualification](https://github.com/SemiAnalysisAI/InferenceX/blob/main/CONTRIBUTING.md#draft-model-precision) + remains unresolved. TP8 and TP2 use the ordinary FFN path. Adaptive + verification remains disabled for the gfx942 startup issue tracked in + [vLLM #63816](https://github.com/vllm-project/vllm/pull/63816). + + [Run 40744174186](https://github.com/SemiAnalysisAI/InferenceX/actions/runs/40744174186) + passed all 18 performance points and the TP8 c32 GSM8K evaluation at + `7c375027a3cd`. The separate [TP4 c4 evaluation](https://github.com/SemiAnalysisAI/InferenceX/actions/runs/40718348873) + at `3df335bfe629` confirmed active mono execution. Both GSM8K evaluations + scored 97.42% strict exact match over 1,319 samples. The effective serving + configuration is unchanged at reviewed source head `c8befe0ed1f0`. + + InferenceX throughput measurement separately substitutes synthetic + acceptance length 3.51 from its committed DSpark thinking-on curve. + This control is excluded from the preset's serving and accuracy commands; + the generic Bench action is not an AgentX trace replay. Concurrency labels + identify the measured workload, while the generated sequence cap controls + admission and may queue additional sessions. No independent accuracy + evaluation was performed at every concurrency or on the 1017 checkpoint. + mi355x_agentx: label: "AgentX TP2" hardware: mi355x @@ -1067,6 +1274,55 @@ presets: - '{"cudagraph_mode":"FULL_DECODE_ONLY","cudagraph_capture_sizes":[6,12,18,24,30,36,42,48,54,60,66,72,78,84,90,96,102,108,114,120,126,132,138,144,150,156,162,168,174,180,186,192,198,204,210,216,222,228,234,240],"max_cudagraph_capture_size":240}' - "--no-enable-prefix-caching" + mi355x_agentx_tp8: + label: "AgentX TP8 conc 1" + hardware: mi355x + strategy: single_node_tp + # Same checkpoint note as mi355x_agentx: the lane served the 0909 launch + # checkpoint, and 1017 shares its format and memory layout. + variants: [fp8, default] + workload: "Agentic (InferenceX AgentX)" + description: "TP8 at concurrency 1 with mono decode and resident Engram tables: one sequence at a time and 32768-token prefill chunks, as the TP4 concurrency-1 point, on a nightly that carries mono decode at TP8." + source: { label: "InferenceX #4666", href: "https://github.com/SemiAnalysisAI/InferenceX/pull/4666", key: "dsv41flash-fp4-mi355x-vllm-agentic-dspark-tp8-c1", revision: fa9d48627d37b236ca6401505da2626bfe469cb2 } + image: "vllm/vllm-openai-rocm:nightly-335dd77b09161c5d63139b4e7ae51bb9928305b1@sha256:3c634cc2c51eda7eb840a236b1f09a5ecf6a4b0cb92c4b8486e2158770507291" + tp: 8 + select: + frontend: rust + features: { spec_decoding: true, text_only: true, encoder_parallel: false } + args: + - "--max-num-seqs" + - "1" + - "--max-num-batched-tokens" + - "32768" + - "--max-cudagraph-capture-size" + - "8" + env: + OMP_NUM_THREADS: "1" + json_args: + "--engram-config": { cpu_offload: false } + points: + tp8_c1: + label: "1" + concurrency: 1 + runbook: | + `--monokernel` runs at TP8 only on builds that carry + [vllm-project/vllm#64101](https://github.com/vllm-project/vllm/pull/64101) + (merged 2026-10-29); earlier builds refuse it outside TP2 and TP4. The pinned + nightly is 17 commits past that merge. + + TP8 mono is a concurrency-1 configuration. #64101 measured one TP8 server at + concurrency 4 with 26% lower throughput than two TP4 replicas, so serve TP4 or + TP2 replicas, as the `mi355x_agentx` preset does, once more than one request is + in flight. + + [InferenceX run 41098938196](https://github.com/SemiAnalysisAI/InferenceX/actions/runs/41098938196) + passed its concurrency-1 performance point and GSM8K evaluation at `fa9d48627`. + The performance point completed 362 of 373 requests with interactivity p50 696.9 + and p90 685.5 tokens/s/user and TTFT p50 0.17 s (single run). The evaluation + scored 0.972 exact match (strict) over 1319 questions. Throughput runs use + DSpark's synthetic golden acceptance length (3.51); the preset keeps real block + rejection for ordinary serving and accuracy evaluation. + hardware_notes: dgx_station_gb300: title: "Weights in Grace memory (UVA offload)" diff --git a/recipes/models/inclusionAI/Ming-3.0-asr-tiny.yaml b/recipes/models/inclusionAI/Ming-3.0-asr-tiny.yaml new file mode 100644 index 000000000000..0a43c7029aba --- /dev/null +++ b/recipes/models/inclusionAI/Ming-3.0-asr-tiny.yaml @@ -0,0 +1,244 @@ +meta: + title: "Ming3-ASR-tiny" + slug: "ming-3-0-asr-tiny" + provider: "inclusionAI" + description: "Chinese, English, and dialect speech recognition with topic, hotword, and dialogue context." + date_added: 2026-10-26 + date_updated: 2026-10-27 + tasks: + - multimodal + performance_headline: "Context-aware Chinese, English, and dialect ASR; single-GPU serving" + +model: + model_id: "inclusionAI/Ming-3.0-asr-tiny" + min_vllm_version: "nightly" + nightly_required: true + install: + pip: + note: "Install from main for now." + command: | + git clone --branch main https://github.com/vllm-project/vllm.git + cd vllm + uv venv --python 3.12 + source .venv/bin/activate + VLLM_USE_PRECOMPILED=1 uv pip install --editable ".[audio]" --torch-backend=auto + vllm download-kernels + docker: false + architecture: moe + parameter_count: "8.55B" + active_parameters: "1.3B" + context_length: 8192 + base_args: + - "--trust-remote-code" + base_env: {} + +dependencies: + - note: "Ming uses torchaudio's sinc resampler; install a build compatible with PyTorch." + command: "uv pip install torchaudio --torch-backend=auto" + +features: + no_repeat_ngram: + description: "Load the Model Runner V2 plugin for request-level full-history n-gram blocking." + args: + - "--logits-processors" + - "vllm.model_executor.models.ming_v3_asr_logits_processor:NoRepeatNGramLogitsProcessor" + +opt_in_features: [] + +variants: + default: + precision: bf16 + vram_minimum_gb: 22 + description: "BF16 checkpoint; the capacity estimate includes the audio tower and is not a measured minimum." + +compatible_strategies: + - single_node_tp + +hardware_overrides: {} + +strategy_overrides: + single_node_tp: + tp: 1 + +guide: | + ## Overview + + [Ming-3.0-asr-tiny](https://huggingface.co/inclusionAI/Ming-3.0-asr-tiny) supports Chinese, English, and dialect transcription with optional topic, hotword, and dialogue context. + This guide covers single-GPU serving, transcription, streaming, Context ASR, and offline inference with vLLM. + + ## Public example audio + + Download the three clips from the official inference repository: + + ```bash + MING_AUDIO_BASE=https://raw.githubusercontent.com/inclusionAI/Ming-3.0-asr/6141e8c552815db724d8df0f72c9d865c547a1ea/examples/cases/audio + curl -fL "$MING_AUDIO_BASE/english_common_voice.wav" -o english.wav + curl -fL "$MING_AUDIO_BASE/chinese_common_voice.wav" -o chinese.wav + curl -fL "$MING_AUDIO_BASE/context_same_sentence_pcm16.wav" -o context.wav + ``` + + | Clip | Observed transcription | + | --- | --- | + | English | `same way you did` | + | Mandarin | `漓江和湘江有秦代开凿的灵渠沟通` | + | Context clip with the hints below | Contains the film title `同囚` | + + The Context clip illustrates correction of `铜球` to `同囚` with a relevant topic and hotwords. These are sample-specific observations, not a general Context ASR accuracy claim. Audio provenance and licenses are documented in the [official examples](https://github.com/inclusionAI/Ming-3.0-asr/blob/6141e8c552815db724d8df0f72c9d865c547a1ea/examples/cases/README.md). + + For accuracy comparisons, keep batch settings and attention backends consistent; greedy transcription outputs can differ across these configurations. + + ## Transcription API + + Use the audio requests below to verify speech recognition. The catalog's generic text-only cURL and benchmark shortcuts do not exercise ASR. + + ```bash + curl http://localhost:8000/v1/audio/transcriptions \ + -F model=inclusionAI/Ming-3.0-asr-tiny \ + -F file=@chinese.wav \ + -F temperature=0 \ + -F max_completion_tokens=256 \ + -F 'vllm_xargs={"no_repeat_ngram_size":10}' + ``` + + Omit `language` for automatic recognition, or use `language=zh` / `language=en` to seed the known language. These are the supported language-hint codes; dialect transcription does not introduce additional hint codes. The response contains the transcript without the generated language label. `response_format=json` and `response_format=text` are supported. + + ### OpenAI Python client + + ```python + import json + from openai import OpenAI + + client = OpenAI(base_url="http://localhost:8000/v1", api_key="EMPTY") + with open("english.wav", "rb") as audio: + result = client.audio.transcriptions.create( + model="inclusionAI/Ming-3.0-asr-tiny", + file=audio, + temperature=0, + extra_body={ + "max_completion_tokens": 256, + "vllm_xargs": json.dumps({"no_repeat_ngram_size": 10}), + }, + ) + print(result.text) + ``` + + `vllm_xargs` is JSON-encoded for the multipart request, as in the cURL examples. + + ### Context ASR + + ```bash + curl http://localhost:8000/v1/audio/transcriptions \ + -F model=inclusionAI/Ming-3.0-asr-tiny \ + -F file=@context.wav \ + -F language=zh \ + -F temperature=0 \ + -F max_completion_tokens=256 \ + -F 'prompt=This is an audio about 同囚.' \ + -F 'hotwords=同囚,黄国权,游学修,关楚耀,赵永洪,赵咏瑶,狱警,长镜头,吞玻璃戏,糖化玻璃,真实案例数据.' \ + -F 'vllm_xargs={"no_repeat_ngram_size":10,"context_chunks":true}' + ``` + + `prompt` accepts topic or preceding dialogue appended to the official ASR instruction, or the full official instruction followed by context. Context and hotwords must be single-line text without audio placeholders. To reproduce the vendor's full prompt exactly, put its hotword text in `prompt` and omit the separate `hotwords` field. + + ### Streaming + + ```bash + curl -N http://localhost:8000/v1/audio/transcriptions \ + -F model=inclusionAI/Ming-3.0-asr-tiny \ + -F file=@english.wav \ + -F language=en \ + -F temperature=0 \ + -F stream=true \ + -F max_completion_tokens=256 \ + -F 'vllm_xargs={"no_repeat_ngram_size":10}' + ``` + + Streaming sends transcript deltas after the complete audio upload. It is not live microphone input. The English clip produces `same way you did` without leaking the language prefix into the transcript. + + ## Chunking and repetition control + + Audio is downmixed and resampled to 16 kHz using torchaudio. The processor extracts the entire clip's log-mel features before splitting encoder chunks. Chunk settings are request-local and are included in multimodal cache keys. + + | Option in `vllm_xargs` | Behavior | + | --- | --- | + | No chunk option | Use 3,000-mel-frame encoder chunks, approximately 30 seconds each. | + | `"context_chunks": true` | Use 3,000 frames up to 30 seconds and 6,000 frames for 30–60 seconds; reject longer clips. | + | `"audio_chunk_frames": 6000` | Explicit chunk size; any integer from 1 through 6,000 is accepted. | + | `"no_repeat_ngram_size": 10` | Block repeated 10-grams over prompt plus generated tokens. | + + `context_chunks` and an explicit `audio_chunk_frames` are mutually exclusive. Ordinary chunking can handle clips beyond 60 seconds within the shared 8,192-token input/output budget and audio-upload size limit. Internal encoder chunking does not create separate transcription requests. + + The no-repeat feature loads a plugin; each request must still set a nonzero `no_repeat_ngram_size` to enable blocking. Omitted values and `0` leave ordinary sampling in effect. Blocking uses the full prompt/output history, and requires Model Runner V2. Speculative decoding and MTP are unsupported with this plugin. + + ## Offline inference + + From the same vLLM source checkout, use the shared [audio language model example](https://github.com/vllm-project/vllm/blob/main/examples/generate/multimodal/audio_language_offline.py) with its built-in audio assets: + + ```bash + python examples/generate/multimodal/audio_language_offline.py --model-type ming_v3_asr + ``` + + This uses the shared example's sampling defaults (`temperature=0.2`, `max_tokens=64`) and prints raw model output, including the language label. For the downloaded Context clip, use the native prompt builder with greedy decoding and 10-gram blocking: + + ```python + from vllm import LLM, SamplingParams + from vllm.config.speech_to_text import SpeechToTextParams + from vllm.model_executor.models.ming_v3_asr import ( + MingV3ASRForConditionalGeneration as MingASR, + ) + from vllm.multimodal.audio import AudioResampler + from vllm.multimodal.media.audio import load_audio + + def main(): + llm = LLM( + model="inclusionAI/Ming-3.0-asr-tiny", + trust_remote_code=True, + dtype="bfloat16", + max_model_len=8192, + max_num_seqs=4, + enforce_eager=True, + logits_processors=[ + "vllm.model_executor.models.ming_v3_asr_logits_processor:" + "NoRepeatNGramLogitsProcessor", + ], + ) + audio, rate = load_audio("context.wav", sr=None, mono=True) + audio = AudioResampler(16000).resample(audio, orig_sr=rate) + params = SpeechToTextParams( + audio=audio, + stt_config=MingASR.get_speech_to_text_config(llm.model_config, "transcribe"), + model_config=llm.model_config, + language="zh", + request_prompt="This is an audio about 同囚.", + hotwords=( + "同囚,黄国权,游学修,关楚耀,赵永洪,赵咏瑶,狱警,长镜头," + "吞玻璃戏,糖化玻璃,真实案例数据." + ), + extra_args={"context_chunks": True}, + ) + outputs = llm.generate( + MingASR.get_generation_prompt(params), + SamplingParams( + temperature=0, + max_tokens=256, + extra_args={"no_repeat_ngram_size": 10}, + ), + ) + print(MingASR.post_process_output(outputs[0].outputs[0].text)) + + + if __name__ == "__main__": + main() + ``` + + ## Limitations + + The [official model card](https://huggingface.co/inclusionAI/Ming-3.0-asr-tiny#usage-notes) describes transcription text output without word-level timestamps or speaker diarization. + + The current vLLM integration does not support translation, segment timestamps, or live audio input. Translation and timestamp requests are rejected. The `context_chunks=true` policy accepts clips up to 60 seconds; ordinary chunking, including requests with context prompts, can accept longer clips subject to the model context and upload limits. Longer recordings can produce incomplete transcripts; accepting an upload does not guarantee complete transcription. + + ## References + + - [model card](https://huggingface.co/inclusionAI/Ming-3.0-asr-tiny) + - [Official inference repository](https://github.com/inclusionAI/Ming-3.0-asr) + - [vLLM speech-to-text API](https://docs.vllm.ai/en/latest/serving/online_serving/speech_to_text.html) diff --git a/recipes/models/meta-models/Muse-Spark-Open-816B-A42B.yaml b/recipes/models/meta-models/Muse-Spark-Open-816B-A42B.yaml index c69c65a57f59..dc6c11f7de50 100644 --- a/recipes/models/meta-models/Muse-Spark-Open-816B-A42B.yaml +++ b/recipes/models/meta-models/Muse-Spark-Open-816B-A42B.yaml @@ -4,7 +4,7 @@ meta: provider: "Muse (Meta)" description: "Sparse MoE reasoning model with 816B total / 42B active parameters and a 1M-token context, built for long-horizon agentic coding. Native image and video perception, channel-scoped reasoning and tool calls via the `muse` parsers, and an optional DSpark draft model for speculative decoding." date_added: 2026-10-15 - date_updated: 2026-10-16 + date_updated: 2026-10-23 tasks: - text - multimodal @@ -381,6 +381,19 @@ guide: | keep `--max-num-seqs 32`. For throughput beyond about 64 concurrent requests, use expert parallelism without DSpark. + ### Monokernel decode (MI355X, manual opt-in) + + On MI355X, `--monokernel` runs decoder layers 1-60 of the DSpark verify + steps of 1 to 16 requests as persistent FlyDSL launches: one launch a layer + for one or two requests, three for 3-16 requests, with the verify attention + fused into the launches for up to four requests. Add it to the MXFP4 + command with **Tensor + Expert Parallel** (TP8 + `--enable-expert-parallel`) + and the seven-token DSpark profile above (AITER FA, bf16 KV cache). vLLM + refuses other deployments at startup; prefill and larger steps keep the + regular path. The launches keep the expert activations in bf16 (MXFP4 + weights x bf16 activations) instead of the checkpoint's W4A4, so outputs + are close to, but not bit-identical with, the regular path. + ### Adaptive DSpark (manual opt-in, Triton attention) Adaptive verification captures variable-length decode CUDA graphs, which diff --git a/recipes/models/mistralai/Mistral-Large-4-1T-A52B.yaml b/recipes/models/mistralai/Mistral-Large-4-1T-A52B.yaml new file mode 100644 index 000000000000..c554a74c2792 --- /dev/null +++ b/recipes/models/mistralai/Mistral-Large-4-1T-A52B.yaml @@ -0,0 +1,956 @@ +# yaml-language-server: $schema=https://recipes.vllm.ai/recipe.schema.json +# +# Every flag below comes from a launcher that ran on GB300, B300, B200, H200 or +# MI350X; untested options are left out. + +meta: + title: "Mistral-Large-4-1T-A52B" + slug: "mistral-large-4-1t-a52b" + provider: "Mistral AI" + description: "Mistral Large 4: a 1T-total / 49B-active multimodal MoE with sparse MLA attention, a built-in DSpark drafter, tool calling and reasoning, in FP8, NVFP4 and AMD MXFP4 checkpoints." + date_added: 2026-10-28 + date_updated: 2026-10-30 + tasks: + - multimodal + performance_headline: "8x GB300, NVFP4 + DSpark: TP8 c1 TPOT 3.1 ms; 1P1D (4+4) P DP4/EP → D TP4 reaches 241 output tok/s/GPU at 32k input" + related_recipes: + - mistralai/Mistral-Large-3-675B-Instruct-2512 + default_hardware: gb300 + # NVFP4 doesn't run on ROCm: AMD defaults to the FP8 checkpoint. + default_variant: + amd: fp8 + hardware: + gb300: verified + b300: verified + b200: verified + h200: verified + mi350x: verified + mi355x: verified + +model: + model_id: "mistralai/Mistral-Large-4-1T-A52B" + min_vllm_version: "0.34.0" + docker_image: + nvidia: "vllm/vllm-openai:mistral-large-4" + amd: "vllm/vllm-openai-rocm:mistral-large-4" + install: + docker: + note: "Mistral Large 4 support ships in these images; no vLLM release or nightly wheel includes it yet. The ROCm image also carries the MXFP4 expert path and the AITER tuning tables." + pip: false + architecture: moe + parameter_count: "1T" + active_parameters: "49B" + context_length: 1048576 + supports_dcp: true + base_args: + - "--tokenizer-mode" + - "mistral" + - "--config-format" + - "mistral" + - "--load-format" + - "mistral" + # Validated up to 256k; the checkpoint advertises 1,048,576 positions. + - "--max-model-len" + - "262144" + # Every validated run used these; current vLLM defaults on B200/GB300 + # (16384 / 1024) are untested with this model. + - "--max-num-batched-tokens" + - "8192" + - "--max-num-seqs" + - "128" + - "--gpu-memory-utilization" + - "0.85" + base_env: + # Cold starts (weight load + DeepGEMM warmup + CUDA graph capture) took + # 1300-1800 s, past the 600 s default. + VLLM_ENGINE_READY_TIMEOUT_S: "6000" + +dependencies: + - note: "Mistral tokenizer and image processing for Mistral Large 4" + command: 'uv pip install -U "mistral_common[image]>=1.13.0"' + - note: "Only for the MegaMoE kernel (flashinfer_moe_ep_cutedsl) used by the P/D presets with expert parallelism" + command: 'uv pip install "nvshmem4py-cu13==0.4.1"' + optional: true + brand: NVIDIA + +features: + tool_calling: + description: "Mistral tool-call parser with automatic tool choice." + args: + - "--enable-auto-tool-choice" + - "--tool-call-parser" + - "mistral" + reasoning: + description: "Mistral reasoning parser: thinking goes to the `reasoning` field instead of `content`." + args: + - "--reasoning-parser" + - "mistral" + spec_decoding: + description: "DSpark speculative decoding with the drafter shipped in both checkpoints. Depth 3 is fixed by the checkpoint." + # Not validated with pipeline parallelism. + strategies: + - single_node_tp + - multi_node_tp + - multi_node_dep + - pd_cluster + default_mode: dspark + modes: + dspark: + label: "DSpark" + description: "Greedy drafting (default). Same acceptance as probabilistic at T ≤ 1 with top-p." + args: + - "--speculative-config" + - '{"method":"dspark","num_speculative_tokens":3}' + dspark_probabilistic: + label: "DSpark (probabilistic)" + description: "Probabilistic drafting for sampled serving at T ≥ 1 without top-p: AL 2.04 → 2.36–2.53 at T=1.5." + args: + - "--speculative-config" + - '{"method":"dspark","num_speculative_tokens":3,"draft_sample_method":"probabilistic"}' + text_only: + description: "Skip the Pixtral vision encoder for text-only serving. Required by prefill context parallelism (PCP)." + args: + - "--language-model-only" + +opt_in_features: + - text_only + +variants: + # NVFP4 is the default on Blackwell; H200 falls back to FP8. + default: + model_id: "mistralai/Mistral-Large-4-1T-A52B-NVFP4" + precision: nvfp4 + # ModelOpt mixed precision: size from the 610.1 GB checkpoint, × 1.2 + vram_minimum_gb: 733 + supported_hardware: [gb300, b300, b200] + description: "ModelOpt NVFP4 experts with FP8 attention and shared experts (~76 GiB per GPU on 8 GPUs), with the DSpark drafter. ~1.5x the KV capacity of FP8. Validated on GB300, B300 and B200." + fp8: + precision: fp8 + # ceil(1060.6 GB checkpoint × 1.2) + vram_minimum_gb: 1273 + supported_hardware: [gb300, b300, h200, mi350x, mi355x] + description: "FP8 block-scaled weights with the DSpark drafter (~128 GiB of weights per GPU on 8 GPUs). On AMD the routed experts are re-quantized to MXFP4 at load, which also fits TP4. Validated on GB300, B300, H200 and MI350X." + hardware_overrides: + amd: + extra_env: + # FP8 → MXFP4 routed experts, layer by layer at load (peak memory: + # the MXFP4 model plus one layer of FP8 experts). a8w4 = FP8 + # activations; a4w4 (MXFP4 activations) measured level with it at TP4. + VLLM_FP8_MOE_REQUANT: "mxfp4" + VLLM_FP8_MOE_REQUANT_ACT: "a8w4" + # The ROCm image carries ML4 kernels and AITER tables a wheel doesn't. + mi350x: + install: + pip: false + docker_serve_command: "vllm serve" + mi355x: + install: + pip: false + docker_serve_command: "vllm serve" + mxfp4: + model_id: "amd/Mistral-Large-4-1T-A52B-Quark-MXFP4" + precision: mxfp4 + label: "MXFP4 (Quark)" + # ceil(578 GB checkpoint × 1.2) + vram_minimum_gb: 694 + supported_hardware: [mi350x, mi355x] + hardware_overrides: + # The ROCm image carries ML4 kernels and AITER tables a wheel doesn't. + mi350x: + install: + pip: false + docker_serve_command: "vllm serve" + mi355x: + install: + pip: false + docker_serve_command: "vllm serve" + description: "AMD Quark OCP MXFP4 routed experts (static weight, dynamic activation scales); attention, shared experts, dense layers, router, head, vision encoder and the DSpark drafter keep the original precision. ~578 GB, fits four MI350 Series GPUs." + +# Only the validated layouts are offered: +# GB300: one 4-GPU tray (TP4, NVFP4), 2 trays (TP8, DP8/EP) and 1P1D with +# one tray per side +# B300: one 8-GPU node (TP8, NVFP4 and FP8) +# B200: one 8-GPU node (TP8) and 1P1D with one node per side +# H200: 2 x 8-GPU nodes (TP16, PP2 x TP8) +compatible_strategies: [] + +strategy_hardware: + single_node_tp: + gb300: supported + b300: supported + b200: supported + mi350x: supported + mi355x: supported + multi_node_tp: + gb300: supported + h200: supported + multi_node_dep: + gb300: supported + multi_node_tp_pp: + h200: supported + pd_cluster: + gb300: supported + b200: supported + +default_strategy: single_node_tp + +strategy_min_gpus: + h200: 16 + +hardware_overrides: + amd: + extra_env: + VLLM_ROCM_USE_AITER: "1" + # AITER's Triton sparse MLA, and AITER custom all-reduce up to 128 MiB + # (large prefill steps): +2–5% output tok/s at TP4. + VLLM_ROCM_USE_AITER_TRITON_SPARSE_MLA: "1" + AITER_CUSTOM_AR_MAX_SIZE: "134217728" + +strategy_overrides: + multi_node_tp_pp: + env: + # Each stage must own layer 35 (the KV-sharing source) and the indexer + # layers its shared-index layers reuse. Valid for PP2 only. + VLLM_PP_LAYER_PARTITION: "35,37" + pd_cluster: + # Validated: P DP/EP on one node → D TP on one node + # (4+4 GB300, 8+8 B200). + prefill: + nodes: 1 + parallelism: dep + decode: + nodes: 1 + parallelism: tp + +presets: + gb300_dcp8: + label: "TP8 DCP8 (long context)" + hardware: gb300 + strategy: multi_node_tp + workload: "Long context, KV-capacity-bound" + description: "Decode context parallelism shards the latent KV cache across the 8 ranks: ~8x KV capacity at a higher per-step latency. Works with DSpark." + select: + nodes: 2 + args: + - "--decode-context-parallel-size" + - "8" + runbook: | + Measured on 2x GB300 (8 GPUs), NVFP4 revision 4359ec6, `--max-model-len 65536`. + + | | TP8 | TP8 DCP8 | + |---|---:|---:| + | KV cache, NVFP4 (no spec) | 3.60M tokens | 28.76M tokens | + | KV cache, FP8 (no spec) | 2.38M tokens | 18.88M tokens | + | GSM8K-200 (NVFP4, no spec / DSpark) | 0.970 / 0.965 | 0.975 / 0.970 | + | c1 TPOT p50, 32k input, DSpark | 4.02 ms | 5.44 ms | + + On NVFP4, 28.76M tokens is room for ~27 concurrent 1M-token requests + (KV only; serving at 1M context is not validated, the recipe caps + `--max-model-len` at 262144). With DSpark the draft's per-block + admission bound costs some of that capacity (~0.69x of plain DCP8 at + 64k context; it doesn't bind at `--max-num-seqs 128` below ~99k + average context). + + DCP8 only pays off when KV capacity binds: at `--max-num-seqs 128` + and ≤60k input its TPOT is worse than TP8 at every load we measured. + On the final stack, TP8 DCP8 FP8 at 60k input / 1k output measured + c1 TTFT 4.73 s, c1 TPOT 12.50 ms and 204 output tok/s at c64; with + DSpark c1 TPOT is 6.16 ms (AL 2.81). + gb300_pcp8_dcp8: + label: "TP1×PCP8×DCP8 + EP (lowest TTFT)" + hardware: gb300 + strategy: multi_node_tp + workload: "Long prompts, low load" + description: "Prefill context parallelism splits each prompt across the 8 ranks: ~3x lower c1 TTFT than TP8 on long prompts, but the lowest throughput. Text-only for now." + select: + nodes: 2 + features: + text_only: true + args: + - "--tensor-parallel-size" + - "1" + - "--prefill-context-parallel-size" + - "8" + - "--decode-context-parallel-size" + - "8" + - "--enable-expert-parallel" + runbook: | + Measured on 2x GB300 (8 GPUs), NVFP4 revision 4359ec6, DSpark, real-text + prompts, 1k output tokens, `--max-model-len 65536`, without + `--enable-expert-parallel`. The preset adds EP, as used by the PCP + prefill side of the P/D presets; it was not benchmarked in this layout. + + | c1 TTFT p50 / TPOT p50 | 8k | 32k | 60k | + |---|---|---|---| + | TP1×PCP8×DCP8 + DSpark | 0.16 s / 8.75 ms | 0.54 s / 8.77 ms | 1.08 s / 9.35 ms | + | TP8 + DSpark | 0.45 s / 3.45 ms | 1.87 s / 4.02 ms | 3.65 s / 3.82 ms | + + Under load (DSpark, p50 / p99 TTFT): 60k at ~0.1 req/s 3.30 / 5.91 s + (TP8: 3.70 / 7.28 s). Best throughput is 86.2 output tok/s/GPU at 8k + (c16, 47 tok/s/user), vs 178.3 for TP8 + DSpark. GSM8K-200 0.965–0.970 + with DSpark, 0.980 without. KV capacity 24.48M tokens without spec. + + PCP requires `--language-model-only` (text only) for now. + h200_dcp8: + label: "TP16 DCP8 (long context)" + hardware: h200 + strategy: multi_node_tp + workload: "Long context, KV-capacity-bound" + description: "Decode context parallelism shards the latent KV cache across 8 ranks of the TP16 group. Validated with DSpark." + select: + nodes: 2 + args: + - "--decode-context-parallel-size" + - "8" + runbook: | + Served correctly on 2x 8 H200 with the FP8 checkpoint, TP16 + DCP8 with + DSpark (3 tokens). TP16 without DCP scored 0.960 on GSM8K (500 + questions, 5-shot, chat template, greedy). No performance numbers were + taken on H200. + gb300_pd_dep_tp: + label: "P DP4/EP → D TP4" + hardware: gb300 + strategy: pd_cluster + variants: [default] + workload: "Throughput at 32k/60k input" + default: true + description: "Default 1P1D layout (4+4 GPUs): the best throughput at 32k and 60k input. MegaMoE kernel on the expert-parallel prefill side." + roles: + prefill: + nodes: 1 + parallelism: dep + args: + - "--kernel-config" + - '{"moe_backend":"flashinfer_moe_ep_cutedsl"}' + json_args: + --kv-transfer-config: { NixlConnector: { kv_role: kv_both, kv_load_failure_policy: null } } + decode: + nodes: 1 + parallelism: tp + remove_args: ["--compilation-config"] + json_args: + --kv-transfer-config: { NixlConnector: { kv_role: kv_both, kv_load_failure_policy: null } } + runbook: | + Measured 1P1D on 4+4 GB300 (one NVL72 rack), NVFP4 revision 4359ec6, DSpark + depth 3 on both sides, CUDA graphs, no prefix caching, `--max-model-len 65536`. + Workload: 320 unique real-text prompts per input length (8k / 32k / 60k + tokens), 1,024 output tokens, greedy, EOS ignored. Routed with the NIXL + toy proxy (`tests/v1/kv_connector/nixl_integration/toy_proxy_server.py`), + not vllm-router. + + Best output tok/s per GPU (8 GPUs) at a minimum per-user speed: + + | | 8k ≥40 | 8k ≥100 | 32k ≥40 | 32k ≥100 | 60k ≥40 / ≥100 | + |---|---:|---:|---:|---:|---:| + | P DP4/EP (MegaMoE) → D TP4 | 596 | 200 | 241 | 154 | 129 / 129 | + | P DP4/EP (default MoE) → D TP4 | 588 | 192 | 206 | 149 | not run | + + c1 TTFT 0.43 / 1.78 / 3.48 s and c1 TPOT 3.9–4.2 ms at 8k / 32k / 60k. + Max 4.65 / 1.88 / 1.01 req/s. GSM8K-200 through the proxy ≥ 0.965. + DSpark acceptance ~2.98 / 2.85 / 2.79 at 8k / 32k / 60k. + + MegaMoE (`flashinfer_moe_ep_cutedsl`) needs `nvshmem4py-cu13`; drop the + prefill `--kernel-config` to use the default MoE backend. + gb300_pd_pcp_tp: + label: "P PCP4×DCP4+EP → D TP4" + hardware: gb300 + strategy: pd_cluster + variants: [default] + workload: "Lowest TTFT, ≥100 tok/s/user at 32k" + description: "Prefill with TP1×PCP4×DCP4 + EP and a 32k-token batch: 2–3.5x lower TTFT than P DP4/EP, within 7–9% of its throughput. Text only." + roles: + prefill: + nodes: 1 + parallelism: tp + args: + - "--tensor-parallel-size" + - "1" + - "--prefill-context-parallel-size" + - "4" + - "--decode-context-parallel-size" + - "4" + - "--cp-kv-cache-interleave-size" + - "64" + - "--enable-expert-parallel" + - "--max-num-batched-tokens" + - "32768" + - "--kernel-config" + - '{"moe_backend":"flashinfer_moe_ep_cutedsl"}' + - "--language-model-only" + json_args: + --kv-transfer-config: { NixlConnector: { kv_role: kv_producer, kv_load_failure_policy: null } } + decode: + nodes: 1 + parallelism: tp + remove_args: ["--compilation-config"] + json_args: + --kv-transfer-config: { NixlConnector: { kv_role: kv_both, kv_load_failure_policy: null } } + runbook: | + Same setup as the default P/D preset (4+4 GB300, NVFP4 4359ec6, DSpark on + both sides, toy proxy). + + | | 8k ≥40 | 8k ≥100 | 32k ≥40 | 32k ≥100 | 60k ≥40 / ≥100 | c1 TTFT 8k / 32k / 60k | + |---|---:|---:|---:|---:|---:|---| + | P PCP4×DCP4+EP → D TP4 | 596 | 201 | 223 | 174 | 118 / 118 | 0.35 / 0.64 / 1.21 s | + | P DP4/EP → D TP4 (default) | 596 | 200 | 241 | 154 | 129 / 129 | 0.43 / 1.78 / 3.48 s | + + Fixed-QPS TTFT p50 / p99: 0.76 / 1.73 s at 32k and 0.87 req/s (default + preset: 2.53 / 4.80 s); 1.35 / 4.47 s at 60k and 0.46 req/s (default: + 4.77 / 8.33 s). + + `--max-num-batched-tokens` was PCP's real limit: at 8192 the 32k / 60k + req/s drops to 0.74 / 0.40 and c1 TTFT rises to 1.43 / 2.62 s. 65536 + adds nothing over 32768. + gb300_pd_dep_dep: + label: "P DP4/EP → D DP4/EP" + hardware: gb300 + strategy: pd_cluster + variants: [default] + workload: "Short inputs, high concurrency" + description: "Expert-parallel decode: the highest throughput at 8k input and high concurrency, with higher per-user latency. MegaMoE on both sides; the decode side keeps a 256-token batch so the kernel uses its low-latency profile." + roles: + prefill: + nodes: 1 + parallelism: dep + args: + - "--kernel-config" + - '{"moe_backend":"flashinfer_moe_ep_cutedsl"}' + json_args: + --kv-transfer-config: { NixlConnector: { kv_role: kv_both, kv_load_failure_policy: null } } + decode: + nodes: 1 + parallelism: dep + args: + - "--max-num-batched-tokens" + - "256" + - "--kernel-config" + - '{"moe_backend":"flashinfer_moe_ep_cutedsl"}' + remove_args: ["--compilation-config"] + json_args: + --kv-transfer-config: { NixlConnector: { kv_role: kv_both, kv_load_failure_policy: null } } + runbook: | + Same setup as the default P/D preset (4+4 GB300, NVFP4 4359ec6, DSpark on + both sides, toy proxy). + + | | 8k ≥40 | 8k ≥100 | 32k ≥40 | 32k ≥100 | 60k ≥40 / ≥100 | c1 TPOT | + |---|---:|---:|---:|---:|---:|---| + | P DP4/EP → D DP4/EP (D mnbt 256) | 766 | 98 | 238 | 83 | 128 / 68 | 6.0–6.5 ms | + + Max 5.98 req/s at 8k. With the default MoE on both sides: 723 / 178 + output tok/s/GPU at 8k (≥40 / ≥100 tok/s/user). MegaMoE on the decode + side picks its low-latency profile only when `--max-num-batched-tokens` + is below 512; at 8192 it lost 10% at 8k. Use session-sticky routing to + the DP ranks when prefix caching is on. + gb300_pd_agentic: + label: "Agentic: P PCP4×DCP4+EP → D TP4 DCP4" + hardware: gb300 + strategy: pd_cluster + variants: [default] + workload: "Agentic, KV-capacity-bound (AgentX traces)" + description: "No spec decoding, prefix caching on: DCP4 on the decode node gives 4x its KV capacity, so long multi-turn sessions keep their cache. Text only." + select: + features: + spec_decoding: false + roles: + prefill: + nodes: 1 + parallelism: tp + args: + - "--tensor-parallel-size" + - "1" + - "--prefill-context-parallel-size" + - "4" + - "--decode-context-parallel-size" + - "4" + - "--cp-kv-cache-interleave-size" + - "64" + - "--enable-expert-parallel" + - "--max-num-batched-tokens" + - "32768" + - "--kernel-config" + - '{"moe_backend":"flashinfer_moe_ep_cutedsl"}' + - "--language-model-only" + json_args: + --kv-transfer-config: { NixlConnector: { kv_role: kv_producer, kv_load_failure_policy: null } } + decode: + nodes: 1 + parallelism: tp + args: + - "--decode-context-parallel-size" + - "4" + - "--cp-kv-cache-interleave-size" + - "64" + remove_args: ["--compilation-config"] + json_args: + --kv-transfer-config: { NixlConnector: { kv_role: kv_both, kv_load_failure_policy: null } } + runbook: | + Measured 1P1D on 4+4 GB300 with `agentx-harness==1.0.7` on + `semianalysisai/cc-traces-weka-062126-256k` (393 sessions, ISL mean + 100.9k / max 255.8k, OSL p50 376), 900 s per point, NVFP4 4359ec6, + no spec decoding, prefix caching on, `--max-model-len 262144`. + + | | KV tokens (D) | c16 out tok/s/GPU | c32 TTFT p50 | c32 out tok/s/GPU | c32 req/s | + |---|---:|---:|---:|---:|---:| + | P PCP4×DCP4 → D TP4 DCP4 | 7.81M | 57.6 | 0.72 s | 92.5 | 1.185 | + | P PCP4×DCP4 → D TP4 | 1.96M | 60.9 | 14.95 s | 78.3 | 0.939 | + | P DP4/EP → D TP4 | 1.96M | 61.0 | 12.66 s | 78.9 | 0.969 | + | P DP4/EP → D DP4/EP | 4 x 1.51M | 54.3 | 0.98 s | 89.7 | 1.155 | + + At ~32 sessions D TP4 runs out of KV (local hit rate 17–33%) and TTFT + explodes; DCP4 on D fixes it. DCP4 decode costs 12–25% ITL, so at ≤16 + sessions P PCP4×DCP4 → D TP4 is better (it is the "P PCP4×DCP4+EP → D + TP4" preset with spec decoding switched off). Everything is past the + knee at c64. + + Route each session to the same DP rank on DP sides (our proxy set the + `X-data-parallel-rank` header): without it the hit rate drops 14–22 + points and TTFT p99 is 2–3x worse. DSpark on a DCP decode node is not + supported yet. + b200_pd_dep_tp: + label: "P DP8/EP → D TP8" + hardware: b200 + strategy: pd_cluster + variants: [default] + workload: "Throughput at 32k/60k input" + default: true + description: "1P1D on one 8-GPU B200 node per side: the best B200 layout we measured." + roles: + prefill: + nodes: 1 + parallelism: dep + json_args: + --kv-transfer-config: { NixlConnector: { kv_role: kv_both, kv_load_failure_policy: null } } + decode: + nodes: 1 + parallelism: tp + remove_args: ["--compilation-config"] + json_args: + --kv-transfer-config: { NixlConnector: { kv_role: kv_both, kv_load_failure_policy: null } } + runbook: | + Measured 1P1D on 8+8 B200 (one node per side, IB between them), NVFP4 + revision 4359ec6, DSpark depth 3 on both sides, default MoE backend, + `--max-model-len 65536`, no prefix caching, NIXL toy proxy. Unique + real-text prompts of 32k / 60k tokens, 1,024 output tokens, greedy. + + Best output tok/s per GPU over all 16 GPUs (concurrency, tok/s/user): + + | P → D | 32k | 60k | c1 TPOT 32k / 60k | + |---|---|---|---| + | P DP8/EP → D TP8 | 164.4 (c256, 100) | 82.8 (c128, 150) | 3.95 / 4.11 ms | + | P DP8/EP → D DP8/EP | 162.9 (c256, 104) | 84.8 (c256, 112) | 6.77 / 5.87 ms | + | P TP8 → D TP8 | 31.7 (c32, 229) | 15.8 (c16, 243) | 4.19 / 3.75 ms | + + Both DP8/EP prefill layouts are prefill-bound at 32k and 60k. GSM8K-200 + (vLLM `tests/evals/gsm8k`, through the proxy) 0.855–0.900, within noise + of colocated TP8 + DSpark (0.890 / 0.885). B200 per-GPU throughput is + ~0.8x GB300 in the same layouts. + + mi350x_tp4: + label: "TP4 (two replicas per node)" + hardware: mi350x + strategy: single_node_tp + variants: [fp8] + workload: "Interactive and mid concurrency" + select: + features: + spec_decoding: dspark + description: "MXFP4 experts fit four MI350X GPUs, so one 8-GPU node runs two TP4 replicas (run the second in its own container on GPUs 4-7, e.g. `docker run -e ROCR_VISIBLE_DEVICES=4,5,6,7`, on another port). Validated with DSpark and the FP8 KV cache." + tp: 4 + runbook: | + Measured on MI350X, TP4, FP8 checkpoint with MXFP4 experts at load + (a4w4; the preset's default a8w4 measured level), FP8 KV cache, DSpark depth 3. 200 real prompts (80 MT-Bench + first turns + 120 GSM8K questions), greedy, ≤ 512 output tokens. + + | DSpark vs no spec | c1 | c8 | c32 | + |---|---:|---:|---:| + | TPOT p50 speedup | 2.7x | 1.9x | 1.8x | + | Output tok/s | 2.1x | 1.6x | 1.4x | + + AL 3.1 at every concurrency. a8w4 and a4w4 experts measured level + (within 3% tok/s, same AL). KV cache 1.79–1.90M tokens per replica + without spec, 1.02M with DSpark at `--max-model-len 32768`. + + The prompt set is not the one behind the NVIDIA acceptance numbers, so + don't compare AL across hardware. + mi350x_tp4_specialized: + label: "TP4 specialized decode (lowest latency)" + hardware: mi350x + strategy: single_node_tp + variants: [fp8] + workload: "1–4 concurrent requests" + description: "Runs every MoE layer of a 1-, 2- or 4-request decode step as one persistent FlyDSL kernel (attention, indexer, router, MXFP4 experts and both all-reduces in one launch). Larger batches and prefill run the standard path. Needs TP4 and a4w4 experts; refuses to start otherwise." + tp: 4 + args: + - "--additional-config" + - '{"decode_backend":"latency_specialized"}' + - "--max-num-seqs" + - "64" + env: + VLLM_FP8_MOE_REQUANT_ACT: "a4w4" + runbook: | + Measured on MI350X, TP4, a4w4 experts, FP8 KV cache, against the + standard decode on the same GPUs. + + | TPOT speedup vs standard decode | c1 | c4 | + |---|---:|---:| + | Specialized decode (8k in / 1k out) | 1.5x | 1.1x | + | Specialized decode + DSpark, vs no spec (real prompts) | 2.9x | n/a | + + With DSpark only single-request verify blocks run fused: at c1 it is + 1.09x faster per token than DSpark on the standard decode, at c8 equal. + mi350x_mxfp4_tp4: + label: "TP4 (two replicas per node)" + hardware: mi350x + strategy: single_node_tp + variants: [mxfp4] + workload: "Interactive and mid concurrency" + select: + features: + spec_decoding: dspark + description: "MXFP4 experts fit four MI350X GPUs, so one 8-GPU node runs two TP4 replicas (run the second in its own container on GPUs 4-7, e.g. `docker run -e ROCR_VISIBLE_DEVICES=4,5,6,7`, on another port)." + tp: 4 + mi350x_mxfp4_tp4_specialized: + label: "TP4 specialized decode (lowest latency)" + hardware: mi350x + strategy: single_node_tp + variants: [mxfp4] + workload: "1–4 concurrent requests" + description: "Runs every MoE layer of a 1-, 2- or 4-request decode step as one persistent FlyDSL kernel (attention, indexer, router, MXFP4 experts and both all-reduces in one launch). Larger batches and prefill run the standard path. Needs TP4 and a4w4 experts; refuses to start otherwise." + tp: 4 + args: + - "--additional-config" + - '{"decode_backend":"latency_specialized"}' + - "--max-num-seqs" + - "64" + mi355x_tp4: + label: "TP4 (two replicas per node)" + hardware: mi355x + strategy: single_node_tp + variants: [fp8] + workload: "Interactive and mid concurrency" + select: + features: + spec_decoding: dspark + description: "MXFP4 experts fit four MI355X GPUs, so one 8-GPU node runs two TP4 replicas (run the second in its own container on GPUs 4-7, e.g. `docker run -e ROCR_VISIBLE_DEVICES=4,5,6,7`, on another port). Validated with DSpark and the FP8 KV cache." + tp: 4 + runbook: | + Measured on MI355X, a8w4 experts, FP8 KV cache, DSpark: c1 / c4 TPOT p50 + 3.75 / 6.26 ms on real prompts. + mi355x_tp4_specialized: + label: "TP4 specialized decode (lowest latency)" + hardware: mi355x + strategy: single_node_tp + variants: [fp8] + workload: "1–4 concurrent requests" + description: "Runs every MoE layer of a 1-, 2- or 4-request decode step as one persistent FlyDSL kernel (attention, indexer, router, MXFP4 experts and both all-reduces in one launch). Larger batches and prefill run the standard path. Needs TP4 and a4w4 experts; refuses to start otherwise." + tp: 4 + args: + - "--additional-config" + - '{"decode_backend":"latency_specialized"}' + env: + VLLM_FP8_MOE_REQUANT_ACT: "a4w4" + runbook: | + Measured on MI355X, a4w4 experts, FP8 KV cache, DSpark: c1 / c4 TPOT p50 + 3.56 / 6.14 ms on real prompts (1.05x / 1.02x vs the TP4 preset). + mi355x_mxfp4_tp4: + label: "TP4 (two replicas per node)" + hardware: mi355x + strategy: single_node_tp + variants: [mxfp4] + workload: "Interactive and mid concurrency" + select: + features: + spec_decoding: dspark + description: "MXFP4 experts fit four MI355X GPUs, so one 8-GPU node runs two TP4 replicas (run the second in its own container on GPUs 4-7, e.g. `docker run -e ROCR_VISIBLE_DEVICES=4,5,6,7`, on another port). Validated with DSpark and the FP8 KV cache." + tp: 4 + runbook: | + Measured on MI355X, FP8 KV cache, DSpark: c1 / c4 TPOT p50 3.85 / 6.05 ms + on real prompts. + mi355x_mxfp4_tp4_specialized: + label: "TP4 specialized decode (lowest latency)" + hardware: mi355x + strategy: single_node_tp + variants: [mxfp4] + workload: "1–4 concurrent requests" + description: "Runs every MoE layer of a 1-, 2- or 4-request decode step as one persistent FlyDSL kernel (attention, indexer, router, MXFP4 experts and both all-reduces in one launch). Larger batches and prefill run the standard path. Needs TP4 and a4w4 experts; refuses to start otherwise." + tp: 4 + args: + - "--additional-config" + - '{"decode_backend":"latency_specialized"}' + runbook: | + Measured on MI355X, FP8 KV cache, DSpark: c1 / c4 TPOT p50 3.60 / 6.03 ms + on real prompts (1.07x / 1.00x vs the TP4 preset). + +notes: + - title: "Two GB300 trays per 8-GPU instance" + when: + hardware: [gb300] + strategies: [multi_node_tp] + body: | + The colocated configurations were validated on 8 GPUs: two 4-GPU GB300 + trays in one NVL72 rack. Node 1 runs `--headless`; send requests to node 0. + - title: "DP8/EP load balancing" + when: + hardware: [gb300] + strategies: [multi_node_dep] + body: | + DP8/EP over two GB300 trays was validated with vLLM's internal load + balancer: node 0 served the API for all 8 DP ranks, and node 1 ran with + `--headless --data-parallel-start-rank 4` instead of + `--data-parallel-hybrid-lb`. Measured with DSpark: 209.8 output tok/s/GPU + at 8k input (c32, 55 tok/s/user) and 50.6 at 32k (c8, 58 tok/s/user), + GSM8K-200 0.975, 1.97M KV tokens per DP rank. + - title: "Use NVFP4 for P/D on GB300" + when: + hardware: [gb300] + strategies: [pd_cluster] + variants: [fp8] + body: | + The P/D presets run one 4-GPU tray per side, which holds the NVFP4 weights + but not the FP8 ones (~128 GiB per GPU at 8 GPUs). Pick the NVFP4 variant. + - title: "Probabilistic drafting" + when: + features: [spec_decoding] + body: | + For sampled serving at T ≥ 1 without top-p, pick **DSpark (probabilistic)**: + at T=1.5 it raises AL from 2.04 to 2.36–2.53 (+12–22% throughput). At + T=1.0 with top-p 0.95 both modes give the same acceptance (AL 2.81 vs 2.80 + at c8), and greedy is ~1% faster per step. Both are lossless. + - title: "HiSparse: set the TRTLLM all-reduce backend" + when: + hardware: [gb300, b300, b200] + strategies: [single_node_tp] + body: | + With HiSparse KV offload (`--kv-transfer-config '{"kv_connector":"HiSparseConnector",...}'`), + also set `VLLM_FLASHINFER_ALLREDUCE_BACKEND=trtllm`. The default single-node + FlashInfer all-reduce (MNNVL) crashes the engine with `CUDA error: unspecified + launch failure` once HiSparse spills KV to host + ([vllm#64327](https://github.com/vllm-project/vllm/issues/64327); fix in + [flashinfer#7782](https://github.com/flashinfer-ai/flashinfer/pull/7782)). + Not needed without HiSparse. + - title: "H200: two 8-GPU nodes" + when: + hardware: [h200] + body: | + FP8 only (NVFP4 needs Blackwell). The ~1.06 TB of FP8 weights need two + 8x H200 nodes: TP16 is the primary layout, PP2 x TP8 the alternative. + FlashMLA sparse with the `fp8_ds_mla` KV cache runs on SM90 as is (DCP + top-k compaction is disabled automatically), so no extra flags are needed. + Node 1 runs `--headless`; send requests to node 0. + + TP16 with DSpark (3 tokens), `fp8_ds_mla` and CUDA graphs scored 0.960 on + GSM8K (500 questions, 5-shot, chat template, greedy). No performance + numbers were taken on H200. + - title: "AMD: MXFP4 experts and AITER" + when: + hardware: [amd] + body: | + ROCm has no NVFP4 kernels. Serve the FP8 checkpoint, whose routed + experts are re-quantized to OCP MXFP4 at load + (`VLLM_FP8_MOE_REQUANT=mxfp4`; expert weights drop from ~125 to ~69 GiB + per GPU at TP8), or the AMD Quark MXFP4 checkpoint. + `VLLM_FP8_MOE_REQUANT_ACT` picks the activation format: `a8w4` (FP8, the + default) or `a4w4` (MXFP4, required by the specialized decode). + `VLLM_ROCM_USE_AITER=1` runs the MoE, GEMMs, MLA decode, RMSNorm and the + TP all-reduce on AITER; every AMD number here was measured with it on. + The latent KV cache is FP8 (E4M3) by default on ROCm, 1.86x the capacity + of bf16; `--kv-cache-dtype bfloat16` opts out. + - title: "AMD: TP8 with DSpark not yet validated" + when: + hardware: [amd] + features: [spec_decoding] + body: | + DSpark was measured at TP4 on MI350X (see the TP4 presets). TP8 serves + without spec decoding; TP8 with DSpark has not been run yet. + - title: "Pipeline parallelism needs a layer partition" + when: + strategies: [multi_node_tp_pp] + body: | + `VLLM_PP_LAYER_PARTITION=35,37` is required for PP2 so each stage owns + layer 35 (the source of the cross-layer KV sharing) and the indexer + layers its shared-index layers reuse. PP2 x TP8 on 2x 8 H200 scored + GSM8K 0.960, the same as TP16. DSpark is not offered with PP. + +troubleshooting: + - title: "NIXL_ERR_BACKEND / UCX \"Invalid value for SCOPY_\" at startup" + when: + strategies: [pd_cluster] + body: | + NCCL auto-loaded the HPC-X net plugin, which loads a second UCX into the + process next to NIXL's bundled UCX. Set `NCCL_NET_PLUGIN=none` on the + prefill and decode nodes (fine when each side is a single node), and keep + HPC-X out of `LD_LIBRARY_PATH`. + - title: "ModuleNotFoundError: nvshmem" + body: | + The MegaMoE backend (`--kernel-config '{"moe_backend":"flashinfer_moe_ep_cutedsl"}'`) + needs `nvshmem4py-cu13`. Install `nvshmem4py-cu13==0.4.1` (the optional + dependency above), or drop the `--kernel-config` to use the default MoE backend. + - title: "SIGBUS in FlashInfer JIT or autotune" + body: | + Seen on shared filesystems. Give each job a private + `FLASHINFER_WORKSPACE_BASE`, or add + `--kernel-config '{"enable_flashinfer_autotune": false}'`. + - title: "DSpark on a DCP decode node" + when: + strategies: [pd_cluster] + features: [spec_decoding] + body: | + Not supported yet: a P/D decode node with `--decode-context-parallel-size` + can't run DSpark. DCP prefill → TP decode with DSpark on both sides works, + and colocated DCP works with DSpark. + - title: "Mistral Large 4 specialized decode unsupported: …" + when: + hardware: [amd] + body: | + The specialized decode is default-deny: it starts only at TP4 on + gfx950, with MXFP4 a4w4 experts (`VLLM_FP8_MOE_REQUANT=mxfp4 + VLLM_FP8_MOE_REQUANT_ACT=a4w4`), Model Runner V2, an FP8 (E4M3) or bf16 + latent KV cache and no context, data, pipeline or expert parallelism. + The error lists every reason; fix them or drop `--additional-config`. + +guide: | + ## Overview + + Mistral Large 4 is Mistral AI's frontier mixture-of-experts multimodal model: 1T + parameters in total, 49B active per token (52B including embeddings and output + layers). It has 72 layers, 128 routed experts (top-4) plus one shared expert, MLA + attention with a DSA sparse indexer (top-2048 keys), cross-layer KV sharing, a + Pixtral vision encoder and a built-in DSpark drafter. It supports tool calling and + reasoning (`reasoning_effort`). + + | Variant | Checkpoint | Weights | Validated on | + |---|---|---|---| + | NVFP4 (default) | [`mistralai/Mistral-Large-4-1T-A52B-NVFP4`](https://huggingface.co/mistralai/Mistral-Large-4-1T-A52B-NVFP4) | ModelOpt NVFP4 experts, FP8 attention / shared experts, ~610 GB | GB300, B200 | + | FP8 (default on AMD) | [`mistralai/Mistral-Large-4-1T-A52B`](https://huggingface.co/mistralai/Mistral-Large-4-1T-A52B) | FP8 block-scaled, ~1.06 TB (on AMD: MXFP4 experts at load) | GB300, H200, MI350X | + | MXFP4 (Quark) | [`amd/Mistral-Large-4-1T-A52B-Quark-MXFP4`](https://huggingface.co/amd/Mistral-Large-4-1T-A52B-Quark-MXFP4) | AMD Quark OCP MXFP4 routed experts, rest unchanged, ~578 GB | MI350 Series | + + Both checkpoints ship the DSpark drafter. The speculative depth is fixed at 3 by + the checkpoint. + + vLLM serves the model on model runner V2 (the default) with breakable CUDA graphs, + FlashMLA sparse attention and an FP8 latent KV cache (`fp8_ds_mla`) by default, so + no attention or KV-cache flags are needed. The checkpoint advertises 1,048,576 + positions; we validated up to 256k, which is the `--max-model-len` set here. + + ## Prerequisites + + - **vLLM**: the `vllm/vllm-openai:mistral-large-4` image. No vLLM release or + nightly wheel includes Mistral Large 4 support yet. + - **AMD**: the ROCm image `vllm/vllm-openai-rocm:mistral-large-4`. + - **mistral_common**: `mistral_common[image] >= 1.13.0` (included in both images). + - **MegaMoE** (P/D presets only): the image does not include `nvshmem4py-cu13`; + install it in the container, or drop the `--kernel-config` flag. + - **Hardware**: validated on NVIDIA GB300 (one 4-GPU tray with NVFP4, or 8 GPUs as + two trays with NVFP4 and FP8), 8x B300 (NVFP4 and FP8), 8x B200 (NVFP4) and + 2x 8 H200 (FP8, TP16 or PP2 x TP8). FP8 needs ~128 GiB of weights per GPU on + 8 GPUs, NVFP4 ~76 GiB. AMD Instinct MI350X (8 GPUs, TP8 or two TP4 replicas) + with the ROCm image. + + ## Choosing a configuration + + All numbers are NVFP4 on 8x GB300 with DSpark unless stated; the presets carry + the full tables. On H200 use TP16 (or TP16 DCP8 for long context), with PP2 x TP8 + as the alternative; we took no performance numbers there. + + | Goal | Configuration | Why | + |---|---|---| + | Default, best interactivity | TP8 + DSpark | c1 TPOT 3.13 ms (NVFP4) / 3.56 ms (FP8), AL ~2.9; 1.9–2.2x output throughput vs no spec | + | Throughput, short/medium inputs | DP8 + EP + DSpark | 209.8 output tok/s/GPU at 8k (c32, 55 tok/s/user), 50.6 at 32k | + | Long context, KV-bound | TP8 DCP8 (+ DSpark) | 28.76M KV tokens vs 3.60M on TP8 (~27 concurrent 1M-token requests by KV size) | + | Lowest TTFT on long prompts | TP1×PCP8×DCP8 + EP (+ DSpark) | c1 TTFT 0.16 / 0.54 / 1.08 s at 8k / 32k / 60k, ~3x faster than TP8; text only | + | Disaggregated throughput | P DP4/EP → D TP4 | 241 output tok/s/GPU at 32k (≥40 tok/s/user), 129 at 60k | + | Disaggregated, lowest TTFT | P PCP4×DCP4+EP → D TP4 | c1 TTFT 0.35 / 0.64 / 1.21 s at 8k / 32k / 60k | + | Disaggregated, short inputs | P DP4/EP → D DP4/EP | 766 output tok/s/GPU at 8k | + | Agentic, KV-bound, no spec | P PCP4×DCP4+EP → D TP4 DCP4 | at 32 sessions TTFT p50 0.72 s vs 14.95 s with D TP4 | + + Colocated TP8 + DSpark at ≥100 tok/s/user: 110.2 output tok/s/GPU at 8k (c8), + 30.5 at 32k (c2), 21.4 at 60k (c2). DCP8 doesn't pay off colocated until KV + capacity binds. Disaggregated 4+4 beats colocated 8-GPU serving by 1.8–2.2x at 8k + and 4–5x at 32k/60k, because colocated decode shares steps with prefill chunks. + + Tuning knobs used by the presets: + + - `--kernel-config '{"moe_backend":"flashinfer_moe_ep_cutedsl"}'` (MegaMoE) with + expert parallelism, +14–17% prefill req/s at 32k. Needs `nvshmem4py-cu13`. On EP + decode nodes only with `--max-num-batched-tokens` below 512 (256 here). + - `--cp-kv-cache-interleave-size 64` on the side that runs DCP in P/D. + - Prefix caching (on by default) for agentic workloads, with session-sticky routing + to the DP ranks of DP sides. + + NVFP4 vs FP8 at TP8: 1.09–1.18x faster per token without spec and 1.14–1.22x with + DSpark, and ~1.5x the KV capacity. + + ## AMD (ROCm) + + Mistral Large 4 runs on AMD Instinct MI350 Series GPUs (gfx950) with the ROCm build + of vLLM (image `vllm/vllm-openai-rocm:mistral-large-4`). The routed experts run as + OCP MXFP4 on AITER's fused MoE, either from the + AMD Quark MXFP4 checkpoint or from the FP8 checkpoint re-quantized at load + (`VLLM_FP8_MOE_REQUANT=mxfp4`, layer by layer, so peak load memory stays low). With + MXFP4 experts the model fits four GPUs, so one 8-GPU node serves two TP4 replicas. + The sparse MLA runs on AITER's MLA decode kernels (`ROCM_AITER_MLA_SPARSE`), the + DSA indexer on AITER's gfx950 indexer kernels, and the latent KV cache is plain FP8 + (E4M3) by default, 1.86x the capacity of bf16. The builder adds the AITER + environment on AMD. + + | Goal | Configuration | Why | + |---|---|---| + | Default | TP8 | one replica per node; DSpark at TP8 not yet validated on ROCm | + | Interactive, more replicas | TP4 + DSpark (preset) | AL 3.1; TPOT 2.7x / 1.9x / 1.8x lower than no spec at c1 / c8 / c32 | + | Lowest latency, 1–4 users | TP4 specialized decode (preset) | one fused kernel per MoE layer: TPOT 1.5x lower at c1; 2.9x with DSpark | + + MXFP4 vs FP8 experts at TP8: +22–71% output tok/s at 1–64 concurrent requests. + + ## Accuracy + + Validated against Mistral's reference implementation on 2x GB300 TP8: per-layer + parity and 256k long-context perplexity equal to the reference. GSM8K (1319 + questions, 5-shot) scores 0.909 on the FP8 checkpoint and 0.918 with DSpark, which + sits at the base server's own noise floor. All P/D and colocated configurations + scored 0.965–0.980 on GSM8K-200 (zero-shot chat). + + ## Client usage + + ```bash + curl http://localhost:8000/v1/chat/completions -H "Content-Type: application/json" -d '{ + "model": "mistralai/Mistral-Large-4-1T-A52B", + "messages": [{"role": "user", "content": "Explain sparse attention in two sentences."}], + "reasoning_effort": "high" + }' + ``` + + Image input (allow more images per prompt with `--limit-mm-per-prompt '{"image": N}'`): + + ```python + from openai import OpenAI + + client = OpenAI(api_key="EMPTY", base_url="http://localhost:8000/v1") + response = client.chat.completions.create( + model="mistralai/Mistral-Large-4-1T-A52B", + messages=[{ + "role": "user", + "content": [ + {"type": "text", "text": "What does this chart show?"}, + {"type": "image_url", "image_url": {"url": "https://example.com/chart.png"}}, + ], + }], + ) + print(response.choices[0].message.reasoning, response.choices[0].message.content) + ``` + + ## Limitations + + - Prefill context parallelism is text-only (`--language-model-only`). + - DSpark doesn't run on a DCP decode node in P/D yet. + - DSpark wasn't validated with pipeline parallelism. + - AMD: NVFP4 checkpoints, DCP / PCP, NIXL P/D and HiSparse aren't supported on + ROCm yet; TP8 with DSpark and MI300 Series (gfx942) are not validated. + + ## References + + - [Mistral-Large-4-1T-A52B (FP8)](https://huggingface.co/mistralai/Mistral-Large-4-1T-A52B) + - [Mistral-Large-4-1T-A52B-NVFP4](https://huggingface.co/mistralai/Mistral-Large-4-1T-A52B-NVFP4) + - [Mistral-Large-4-1T-A52B-Quark-MXFP4](https://huggingface.co/amd/Mistral-Large-4-1T-A52B-Quark-MXFP4) + - [Cross-layer MLA KV sharing (vllm#64095)](https://github.com/vllm-project/vllm/pull/64095) + - [DSpark sliding-window MLA drafts (vllm#63795)](https://github.com/vllm-project/vllm/pull/63795) and [DSpark under DCP (vllm#63796)](https://github.com/vllm-project/vllm/pull/63796) + - [NIXL connector](https://docs.vllm.ai/en/latest/features/nixl_connector_usage.html) diff --git a/recipes/models/moonshotai/Kimi-K3.yaml b/recipes/models/moonshotai/Kimi-K3.yaml index a4f61b63639f..fd8440e1cca3 100644 --- a/recipes/models/moonshotai/Kimi-K3.yaml +++ b/recipes/models/moonshotai/Kimi-K3.yaml @@ -4,7 +4,7 @@ meta: provider: "Moonshot AI" description: "Pre-release 2.8T-parameter native multimodal MoE with Kimi Delta Attention, Gated MLA, Attention Residuals, and a 1M-token context window" date_added: 2026-07-27 - date_updated: 2026-10-26 + date_updated: 2026-10-29 tasks: - multimodal - text @@ -20,7 +20,6 @@ meta: gb200: verified gb300: verified mi355x: verified - ascend_910c: verified model: model_id: "moonshotai/Kimi-K3" @@ -94,10 +93,6 @@ features: args: - "--speculative-config" - '{"model":"RedHatAI/Kimi-K3-speculator.dspark", "num_speculative_tokens":8, "method": "dspark", "attention_backend":"TOKENSPEED_MLA", "draft_sample_method": "probabilistic", "rejection_sample_method": "block"}' - npu: - args: - - "--speculative-config" - - '{"method":"dspark","model":"RedHatAI/Kimi-K3-speculator.dspark","num_speculative_tokens":7,"enforce_eager":true}' text_only: description: "Skip the vision encoder for text-only workloads. Mutually exclusive with encoder_parallel." args: @@ -127,49 +122,6 @@ variants: model_id: "RedHatAI/Kimi-K3-NVFP4" description: "RedHatAI compressed-tensors NVFP4 quantization; MoE layers quantized to NVFP4, attention and embeddings at full precision" - ascend_w4a8: - model_id: "Eco-Tech/Kimi-K3-w4a8" - precision: int4 - label: "W4A8 (Ascend)" - vram_minimum_gb: 1711 - supported_hardware: [ascend_910c] - extra_args: - - "--quantization" - - "ascend" - - "--enable-prefix-caching" - - "--enable-expert-parallel" - - "--max-num-seqs" - - "16" - - "--max-model-len" - - "131072" - - "--max-num-batched-tokens" - - "24576" - - "--gpu-memory-utilization" - - "0.9" - - "--compilation-config" - - '{"cudagraph_mode":"FULL_DECODE_ONLY"}' - - "--mm-processor-cache-gb" - - "0" - - "--additional-config" - - '{"enable_cpu_binding":true, "enable_flashcomm1":true}' - - "--mm-encoder-tp-mode" - - "data" - - "--limit-mm-per-prompt" - - '{"vision_chunk": 2}' - - "--allowed-local-media-path" - - "/" - extra_env: - PYTORCH_NPU_ALLOC_CONF: "expandable_segments:True" - OMP_PROC_BIND: "false" - OMP_NUM_THREADS: "1" - TASK_QUEUE_ENABLE: "1" - HCCL_BUFFSIZE: "800" - VLLM_ENGINE_READY_TIMEOUT_S: "7200" - hardware_overrides: - ascend_910c: - docker_image: "quay.io/ascend/vllm-ascend:kimi-k3-a3" - description: "ModelSlim INT4 W4A8 for Ascend NPU. Requires --quantization ascend. Validated on 4× 910C mixed P/D." - fp8: model_id: "RedHatAI/Kimi-K3-FP8-BLOCK" precision: fp8 @@ -194,19 +146,9 @@ strategy_hardware: h200: supported gb200: supported gb300: supported - ascend_910c: supported multi_node_dep: h100: unsupported h200: unsupported - ascend_910c: unsupported - multi_node_tp: - ascend_910c: unsupported - multi_node_tep: - ascend_910c: unsupported - multi_node_tp_pp: - ascend_910c: unsupported - pd_cluster: - ascend_910c: unsupported default_strategy: single_node_tp @@ -230,7 +172,6 @@ strategy_min_gpus: # https://vllm-project.github.io/2026/07/27/k3.html#performance-optimizations gb200: multi_node_tp: 16 - ascend_910c: 64 hardware_overrides: hopper: @@ -705,10 +646,10 @@ presets: strategy: pd_cluster workload: "Agentic (InferenceX AgentX)" default: true - description: "TP8 + DCP8 prefill and decode workers across two GB300 trays each, served by Dynamo with the DSpark K4 draft." - source: { label: "InferenceX #4047", href: "https://github.com/SemiAnalysisAI/InferenceX/pull/4047", key: "kimik3-fp4-gb300-dynamo-vllm-agentic-dspark-mooncake-dcp8-disagg" } + description: "TP8 + DCP8 prefill and decode workers across two GB300 trays each, served by Dynamo with the DSpark K4 draft: latency-specialized decode at 16-32 clients, KV-balanced decode attention at 112-264." + source: { label: "InferenceX #4638", href: "https://github.com/SemiAnalysisAI/InferenceX/pull/4638", key: "kimik3-fp4-gb300-dynamo-vllm-agentic-dspark-mooncake-dcp8-disagg", revision: bc4d49f5df307680be5a1567e01b838b83b31b62 } image: - vllm: "vllm/vllm-openai:nightly-e4a5b5537344b103ed10de7764378ef09604ef3d" + vllm: "vllm/vllm-openai:nightly-3966924d566340e971a6da313e790d05492aa505@sha256:f255c4fad2f30ff5ee80db14d3a03afe9ae0f10d3470339864ff04745fe44cb3" dynamo: "a0de541ea8c861d5ac01d15953ca529bea68637b" # Served by Dynamo with the Rust frontend; tool-call flags off under Dynamo, # and the K4 draft is in args (specialized decode rejects the toggle's K8). @@ -716,17 +657,19 @@ presets: pd_router: dynamo frontend: rust features: { tool_calling: false, spec_decoding: false } + # NIXL P/D plus the shared Mooncake store in host DRAM (embedded mode). + kv_offload: kv_store_distributed_mooncake + # The lane's connector options on the composed MultiConnector. + json_args: + "--kv-transfer-config": + kv_load_failure_policy: recompute + NixlConnector: { kv_buffer_device: cuda, kv_connector_extra_config: { enforce_handshake_compat: false, enable_cross_layers_blocks: false } } + MooncakeStoreConnector: { kv_load_failure_policy: recompute, kv_connector_extra_config: { enable_cross_layers_blocks: false, enable_offload: false } } # GB300: the InferenceX AgentX xPyD lane (SemiAnalysisAI/InferenceX#4047, - # refreshed in #4516), TP8 + DCP8 pools across two trays with the DSpark K4 - # draft. The pd_cluster - # Blackwell block above replaces the recipe Blackwell baseline, so the - # shared serving flags are restated here; role blocks below add the rest. - # It renders the low-concurrency specialized-decode profile (#4373: c16-c32), - # so both roles keep FP32 KV-cache SSM state, pin the DCP KV interleave to 1 - # and leave FlashInfer autotune off. NIXL P/D would otherwise raise the - # interleave to the KV block size after the specialized decode has read it, - # and its spec-verify causal mask would use the stale value. The uncapped - # c48+ points keep BF16 state, NIXL's interleave and autotune on (see the guide). + # refreshed in #4516 and #4638), TP8 + DCP8 pools across two trays with the + # DSpark K4 draft. The pd_cluster Blackwell block above replaces the recipe + # Blackwell baseline, so the shared serving flags are restated here. Roles + # add what every point shares; each point adds its decode profile. args: - "--decode-context-parallel-size" - "8" @@ -736,19 +679,12 @@ presets: - "TOKENSPEED_MLA" - "--kv-cache-dtype" - "fp8" - - "--mamba-ssm-cache-dtype" - - "float32" - - "--cp-kv-cache-interleave-size" - - "1" - "--enable-prefix-caching" - "--prefix-match-unit" - "128" - "--gpu-memory-utilization" - "0.95" - - "--max-cudagraph-capture-size" - - "512" - "--enable-cumem-allocator" - - "--no-enable-flashinfer-autotune" - "--load-format" - "fastsafetensors" - "--stream-interval" @@ -761,9 +697,9 @@ presets: VLLM_USE_DIRECT_DCP_Q_GATHER: "1" VLLM_USE_DIRECT_DCP_KV_GATHER: "1" VLLM_ALLREDUCE_USE_FLASHINFER: "1" + VLLM_KIMI_K3_SHARD_SP_SHARED_EXPERT: "0" VLLM_USE_NCCL_SYMM_MEM: "0" VLLM_USE_V2_MODEL_RUNNER: "1" - VLLM_PREFIX_CACHE_RETENTION_INTERVAL: "0" NCCL_CUMEM_ENABLE: "1" NCCL_MNNVL_ENABLE: "1" NCCL_NVLS_ENABLE: "1" @@ -780,15 +716,16 @@ presets: # The qualified workers left UCX on its default transports; this # replaces the Blackwell PD block's rc,cuda_copy restriction. UCX_TLS: "all" + DYN_TCP_REQUEST_TIMEOUT: "120" + ETCD_LEASE_TTL: "600" # The pd_cluster Blackwell block replaces the recipe baseline, so the # startup timeouts are restated: loading and autotune take ~25 min. VLLM_ENGINE_READY_TIMEOUT_S: "3600" VLLM_EXECUTE_MODEL_TIMEOUT_SECONDS: "1800" - VLLM_RPC_TIMEOUT: "600000" roles: prefill: nodes: 2 - # Prefill captures CUDA graphs up to 8192 tokens (#4516). + # TP8 + EP8 with MegaMoE; CUDA graphs up to 8192 tokens. args: - "--moe-backend" - "deep_gemm_mega_moe" @@ -809,47 +746,156 @@ presets: # GB300 decode is a TP8 + DCP8 pool without expert parallelism (#4047). parallelism: tp nodes: 2 - # Undo the DEP-only decode flags below for the TP pool. Hybrid LB is - # invalid on the --headless follower; 16384 is vLLM's API-server default - # on >=160 GB GPUs, which the qualified decode ran with. - # FULL_AND_PIECEWISE is vLLM's V1 default; it replaces the strategy's - # FULL_DECODE_ONLY, which the qualified decode did not set. - # Specialized decode (InferenceX #4373, c16-c32), decode only: it needs no - # expert parallelism and FULL decode CUDA graphs. --max-num-seqs is - # 64 // (K + 1) = 12 for the K4 draft, and graphs are captured for every - # batch size up to 64. The lane also drops tokenspeed_mla_min_split_kv. + # Drop the DEP-only decode flags for the TP pool: hybrid LB is invalid + # on the --headless follower, and the lane keeps prefix caching on. + # 16384 is vLLM's API-server default on >=160 GB GPUs, which the + # qualified decode ran with. + remove_args: ["--data-parallel-hybrid-lb", "--no-enable-prefix-caching"] args: - - "--no-data-parallel-hybrid-lb" - "--max-num-batched-tokens" - "16384" - - "--additional-config" - - '{"decode_backend":"latency_specialized"}' - - "--max-num-seqs" - - "12" - - "--max-cudagraph-capture-size" - - "64" - - "--compilation-config" - - '{"cudagraph_mode":"FULL_AND_PIECEWISE","cudagraph_capture_sizes":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64]}' - "--attention-config" - '{"mla_prefill_backend":"TRTLLM_RAGGED","use_prefill_query_quantization":true}' env: VLLM_ENABLE_GEMM_RS: "0" + # One point per measured operating point. The builder renders one prefill + # and one decode worker; the label and description give the lane's worker + # counts (each worker is two trays), which the schema cannot express. + # + # Specialized decode (c16-c32): the latency_specialized backend on decode + # needs FULL decode CUDA graphs, FP32 SSM state and no expert parallelism. + # --max-num-seqs is 64 // (K + 1) = 12 for the K4 draft, with graphs for + # every batch size up to 64. Both roles pin the DCP KV interleave to 1 and + # keep FlashInfer autotune off: otherwise NIXL P/D raises the interleave to + # the KV block size after the specialized decode has read it, and its + # spec-verify causal mask uses the stale value. + # + # Regular decode (c112-c264): BF16 SSM state and FlashInfer autotune on both + # roles; decode runs target and draft attention on KIMI_K3_KV_BALANCED_MLA + # (prefill stays on TOKENSPEED_MLA) with 128 sequences, ReplaySSM and graphs + # up to 512 tokens in vLLM's default FULL_AND_PIECEWISE mode. + points: + 1p3d_c16: + label: "16 (1P3D)" + concurrency: 16 + description: "One prefill and three decode workers (32 GPUs), specialized decode." + args: &k3_gb300_spec_args + - "--mamba-ssm-cache-dtype" + - "float32" + - "--cp-kv-cache-interleave-size" + - "1" + - "--no-enable-flashinfer-autotune" + roles: &k3_gb300_spec_roles + decode: + args: + - "--additional-config" + - '{"decode_backend":"latency_specialized"}' + - "--max-num-seqs" + - "12" + - "--max-cudagraph-capture-size" + - "64" + - "--compilation-config" + - '{"cudagraph_mode":"FULL_AND_PIECEWISE","cudagraph_capture_sizes":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64]}' + 1p2d_c24: + label: "24 (1P2D)" + concurrency: 24 + default: true + description: "One prefill and two decode workers (24 GPUs), specialized decode." + args: *k3_gb300_spec_args + roles: *k3_gb300_spec_roles + 1p2d_c32: + label: "32 (1P2D)" + concurrency: 32 + description: "One prefill and two decode workers (24 GPUs), specialized decode." + args: *k3_gb300_spec_args + roles: *k3_gb300_spec_roles + 2p4d_c112: + label: "112 (2P4D)" + concurrency: 112 + description: "Two prefill and four decode workers (48 GPUs), KV-balanced decode." + remove_args: &k3_gb300_reg_remove ["--disable-flashinfer-autotune"] + args: &k3_gb300_reg_args + - "--mamba-ssm-cache-dtype" + - "bfloat16" + - "--enable-flashinfer-autotune" + roles: &k3_gb300_reg_roles + decode: + # vLLM's default CUDA graph mode, as the lane ran it. + remove_args: ["--compilation-config"] + args: + - "--attention-backend" + - "KIMI_K3_KV_BALANCED_MLA" + - "--max-num-seqs" + - "128" + - "--max-cudagraph-capture-size" + - "512" + - "--use-replayssm" + json_args: + "--attention-config": { tokenspeed_mla_min_split_kv: 8 } + "--speculative-config": { attention_backend: "KIMI_K3_KV_BALANCED_MLA" } + 2p4d_c128: + label: "128 (2P4D)" + concurrency: 128 + description: "Two prefill and four decode workers (48 GPUs), KV-balanced decode." + remove_args: *k3_gb300_reg_remove + args: *k3_gb300_reg_args + roles: *k3_gb300_reg_roles + 2p3d_c128: + label: "128 (2P3D)" + concurrency: 128 + description: "Two prefill and three decode workers (40 GPUs), KV-balanced decode." + remove_args: *k3_gb300_reg_remove + args: *k3_gb300_reg_args + roles: *k3_gb300_reg_roles + 2p3d_c152: + label: "152 (2P3D)" + concurrency: 152 + description: "Two prefill and three decode workers (40 GPUs), KV-balanced decode." + remove_args: *k3_gb300_reg_remove + args: *k3_gb300_reg_args + roles: *k3_gb300_reg_roles + 3p4d_c240: + label: "240 (3P4D)" + concurrency: 240 + description: "Three prefill and four decode workers (56 GPUs), KV-balanced decode." + remove_args: *k3_gb300_reg_remove + args: *k3_gb300_reg_args + roles: *k3_gb300_reg_roles + 3p3d_c240: + label: "240 (3P3D)" + concurrency: 240 + description: "Three prefill and three decode workers (48 GPUs), KV-balanced decode." + remove_args: *k3_gb300_reg_remove + args: *k3_gb300_reg_args + roles: *k3_gb300_reg_roles + 3p3d_c264: + label: "264 (3P3D)" + concurrency: 264 + description: "Three prefill and three decode workers (48 GPUs), KV-balanced decode." + remove_args: *k3_gb300_reg_remove + args: *k3_gb300_reg_args + roles: *k3_gb300_reg_roles runbook: | - The GB300 AgentX disaggregated lane in [SemiAnalysis InferenceX](https://inferencex.semianalysis.com/) (`kimik3-fp4-gb300-dynamo-vllm-agentic-dspark-mooncake-dcp8-disagg`, [SemiAnalysisAI/InferenceX#4047](https://github.com/SemiAnalysisAI/InferenceX/pull/4047), points refined in [#4373](https://github.com/SemiAnalysisAI/InferenceX/pull/4373) and [#4516](https://github.com/SemiAnalysisAI/InferenceX/pull/4516)) runs Dynamo + vLLM with several prefill and decode workers. Prefill and decode workers are both TP8 + DCP8 across two 4-GPU GB300 trays; prefill workers also use EP8 with MegaMoE, and decode workers use no expert parallelism. KV moves over NIXL, and all workers share an RDMA Mooncake store. Image `vllm/vllm-openai:nightly-e4a5b5537344b103ed10de7764378ef09604ef3d`, Dynamo `a0de541ea8c861d5ac01d15953ca529bea68637b`. + The GB300 AgentX disaggregated lane in [SemiAnalysis InferenceX](https://inferencex.semianalysis.com/) (`kimik3-fp4-gb300-dynamo-vllm-agentic-dspark-mooncake-dcp8-disagg`, [SemiAnalysisAI/InferenceX#4047](https://github.com/SemiAnalysisAI/InferenceX/pull/4047), points refined in [#4373](https://github.com/SemiAnalysisAI/InferenceX/pull/4373), [#4516](https://github.com/SemiAnalysisAI/InferenceX/pull/4516) and [#4638](https://github.com/SemiAnalysisAI/InferenceX/pull/4638)) runs Dynamo + vLLM with several prefill and decode workers. Every worker is TP8 + DCP8 across two 4-GPU GB300 trays; prefill workers also use EP8 with MegaMoE, and decode workers use no expert parallelism. KV moves over NIXL, and all workers share an RDMA Mooncake store in host DRAM. Image `vllm/vllm-openai:nightly-3966924d566340e971a6da313e790d05492aa505` (vLLM `0.33.0rc1.dev229+g3966924d5`), Dynamo built from `a0de541ea8c861d5ac01d15953ca529bea68637b`, Mooncake `0.3.13.post1` from the image. - | Clients | Prefill workers | Decode workers | GPUs | Decode profile | Decode `--max-num-seqs` | - |---:|---:|---:|---:|---|---:| - | 16 | 1 | 3 | 32 | [Specialized](#specialized-decode-low-concurrency) | 12 | - | 24-32 | 1 | 2 | 24 | [Specialized](#specialized-decode-low-concurrency) | 12 | - | 48-64 | 1 | 2 | 24 | Regular | 128 | - | 240 | 2 | 3 | 40 | Regular | 128 | - | 240 | 3 | 4 | 56 | Regular | 128 | + | Clients | Prefill workers | Decode workers | GPUs | Decode profile | Decode attention | Decode `--max-num-seqs` | + |---:|---:|---:|---:|---|---|---:| + | 16 | 1 | 3 | 32 | Specialized | `TOKENSPEED_MLA` | 12 | + | 24, 32 | 1 | 2 | 24 | Specialized | `TOKENSPEED_MLA` | 12 | + | 112, 128 | 2 | 4 | 48 | Regular | `KIMI_K3_KV_BALANCED_MLA` | 128 | + | 128, 152 | 2 | 3 | 40 | Regular | `KIMI_K3_KV_BALANCED_MLA` | 128 | + | 240 | 3 | 4 | 56 | Regular | `KIMI_K3_KV_BALANCED_MLA` | 128 | + | 240, 264 | 3 | 3 | 48 | Regular | `KIMI_K3_KV_BALANCED_MLA` | 128 | - Prefill workers always use `--max-num-seqs 32`. The regular profile is the Workers commands below; the specialized one changes both roles as described under [Specialized decode](#specialized-decode-low-concurrency). + Each point renders one prefill worker and one decode worker; start the table's number of workers per role with the same commands, two trays each. Run every worker command on both of its trays with the same `--master-addr` (the first tray's address), `--nnodes 2` and `--node-rank 0` or `1`. Prefill workers always use `--max-num-seqs 32` and target and draft attention on `TOKENSPEED_MLA`. - The preset renders one prefill pool and one decode pool of the specialized profile (the 1P2D c24-c32 row); its decode uses `--max-num-seqs 12`. For the regular points use the Workers commands below. It turns **Spec decoding** off, because the toggle's generic `RedHatAI/Kimi-K3-speculator.dspark` K8 draft was not qualified and specialized decode allows at most 7 draft tokens, and **Tool calling** off (see the parser note under Workers). This preset's DCP8 pools do not combine with a DEP decode pool, which renders TP1 with `--decode-context-parallel-size 8` that vLLM rejects; the DEP16 decode lane is the [AgentX 3P1D DEP16 preset](#preset-gb300_agentx_pd_dep16). + **Specialized decode (16-32 clients).** The decode workers use the `latency_specialized` decode backend from the [GB300 low-latency preset](#preset-gb300_low_latency). Its requirements are listed there: V2 model runner, FULL decode CUDA graphs, BF16 convolution state with FP32 recurrent state, DSpark with at most 7 draft tokens, no pipeline or expert parallelism, and DCP of 1 or TP. Both roles pin `--cp-kv-cache-interleave-size 1`: left unset, NIXL P/D raises the DCP KV interleave to the KV block size after the specialized decode has read it, so its spec-verify causal mask uses the stale value. The prefill workers do not set `--additional-config`, because the backend rejects the expert parallelism they use. - Documented here only: worker counts beyond one pool per role, the Mooncake store connector (the builder's Mooncake option renders a plain `MooncakeStoreConnector` without the qualified NIXL MultiConnector, recompute policy and load options), the Mooncake services, prefill `--kv-events-config`, and the frontend's router mode and session affinity (the builder renders round-robin). + **Regular decode (112-264 clients).** The decode workers run the target and the DSpark draft on `KIMI_K3_KV_BALANCED_MLA`, an FP8 MLA decode backend that plans its split-KV work on the device; it is never selected automatically, and prefill steps use the configured MLA prefill backend. These points cap decode at 128 sequences with ReplaySSM, so TTFT is not traded for ITL by a standing decode queue. + + The preset turns **Spec decoding** off, because the toggle's generic `RedHatAI/Kimi-K3-speculator.dspark` K8 draft was not qualified and specialized decode allows at most 7 draft tokens, and **Tool calling** off: `python3 -m dynamo.vllm` parses the remaining arguments as vLLM engine arguments only, so the vLLM API-server flags `--enable-auto-tool-choice` and `--tool-call-parser` are rejected there. For tool calling and reasoning output, add `--dyn-tool-call-parser kimi_k3 --dyn-reasoning-parser kimi_k3 --dyn-enable-structural-tag` to both roles. This preset's DCP8 pools do not combine with a DEP decode pool, which renders TP1 with `--decode-context-parallel-size 8` that vLLM rejects; the DEP16 decode lane is the [AgentX 3P1D DEP16 preset](#preset-gb300_agentx_pd_dep16). + + The preset selects the distributed (embedded) Mooncake store on the **KV Offload** row, which wraps NIXL and the `MooncakeStoreConnector` in one `MultiConnector` on both roles; the preset sets the lane's connector options on it (recompute on a failed load, offload off). Documented here only: the worker counts, the lane's Mooncake master options and store sizing (the builder's Mooncake option shows its own defaults), and the frontend's router mode and session affinity (the builder renders round-robin). ### Services @@ -862,96 +908,14 @@ presets: --default_kv_lease_ttl=60000 --eviction_high_watermark_ratio=0.95 --eviction_ratio=0.10 ``` - The workers' Mooncake store uses P2P handshake metadata, RDMA over the node's NICs, a 160GB global segment and a 4GB local buffer, with offload disabled. - - ### Environment (every worker) + Every worker reads the Mooncake store configuration from the JSON file that `MOONCAKE_CONFIG_PATH` points to. The lane registered 160GB per rank: - ```bash - export VLLM_USE_DIRECT_DCP_A2A=1 VLLM_USE_DIRECT_DCP_Q_GATHER=1 VLLM_USE_DIRECT_DCP_KV_GATHER=1 - export VLLM_ALLREDUCE_USE_FLASHINFER=1 VLLM_KIMI_K3_SHARD_SP_SHARED_EXPERT=0 VLLM_USE_NCCL_SYMM_MEM=0 - export NCCL_CUMEM_ENABLE=1 NCCL_MNNVL_ENABLE=1 NCCL_NVLS_ENABLE=1 NCCL_P2P_LEVEL=NVL - export VLLM_USE_V2_MODEL_RUNNER=1 VLLM_USE_RUST_FRONTEND=1 VLLM_SSM_CONV_STATE_LAYOUT=DS - export VLLM_PREFIX_CACHE_RETENTION_INTERVAL=0 - export VLLM_MOONCAKE_LOAD_RECV_THREADS=4 MC_SLICE_SIZE=1048576 MC_MAX_MR_SIZE=4294967296 MC_ENABLE_DEST_DEVICE_AFFINITY=1 - export UCX_MEMTYPE_CACHE=n UCX_MEMTYPE_REG_WHOLE=n UCX_RCACHE_MAX_UNRELEASED=1024 UCX_TCP_AF_PRIO=inet - export TOKENSPEED_MLA_FP16_PARTIALS=0 - export DYN_REQUEST_PLANE=tcp DYN_TCP_REQUEST_TIMEOUT=120 ETCD_LEASE_TTL=600 - export VLLM_ENGINE_READY_TIMEOUT_S=3600 VLLM_EXECUTE_MODEL_TIMEOUT_SECONDS=1800 VLLM_RPC_TIMEOUT=600000 + ```json + {"master_server_address": ":", "metadata_server": "P2PHANDSHAKE", + "global_segment_size": "160GB", "local_buffer_size": "4GB", "protocol": "rdma", + "device_name": "", "mode": "embedded", "enable_offload": false} ``` - Set `VLLM_ENABLE_GEMM_RS=1` on prefill workers and `VLLM_ENABLE_GEMM_RS=0` on decode workers. Prefill workers also set `DYN_BACKEND_ADMISSION_ENGINE_REQUEST_LIMIT=1024` and `DYN_BACKEND_ADMISSION_ENGINE_REQUEST_WAITING_LIMIT=1024`. Loading the 2.8T checkpoint across trays and autotuning takes a while, which is why the engine-ready, execute-model and RPC timeouts are raised. - - ### Workers - - Each worker spans two trays. Run the command on both trays with the same `--master-addr` (the first tray's address), `--nnodes 2` and `--node-rank 0` or `1`. - - Prefill: - - ```bash - python3 -m dynamo.vllm --model moonshotai/Kimi-K3 --served-model-name moonshotai/Kimi-K3 \ - --disaggregation-mode prefill --request-plane tcp \ - --nnodes 2 --node-rank <0|1> --master-addr --device-ids 0,1,2,3 \ - --tensor-parallel-size 8 --decode-context-parallel-size 8 --dcp-comm-backend a2a \ - --enable-expert-parallel --moe-backend deep_gemm_mega_moe \ - --attention-backend TOKENSPEED_MLA \ - --attention-config '{"mla_prefill_backend":"TRTLLM_RAGGED","use_prefill_query_quantization":true,"trtllm_ragged_backend":"cute-dsl"}' \ - --speculative-config '{"model":"Inferact/Kimi-K3-DSpark","attention_backend":"TOKENSPEED_MLA","method":"dspark","num_speculative_tokens":4,"draft_sample_method":"probabilistic","rejection_sample_method":"block"}' \ - --kv-cache-dtype fp8 --mamba-ssm-cache-dtype bfloat16 \ - --enable-prefix-caching --prefix-match-unit 128 \ - --gpu-memory-utilization 0.95 --max-num-seqs 32 --max-cudagraph-capture-size 8192 \ - --compilation-config '{"cudagraph_mode":"FULL_AND_PIECEWISE","cudagraph_capture_sizes":[4,8,12,16,20,24,32,40,48,64,80,96,128,160,256,384,512,640,768,896,1024,1152,1280,1408,1536,1664,1792,1920,2048,2304,2560,2816,3072,3328,3584,3840,4096,4608,5120,5632,6144,6656,7168,7680,8192]}' \ - --enable-cumem-allocator --enable-flashinfer-autotune \ - --load-format fastsafetensors --stream-interval 10 --language-model-only --trust-remote-code \ - --kv-events-config '{"publisher":"zmq","topic":"kv-events","enable_kv_cache_events":true,"endpoint":"tcp://*:5200"}' \ - --kv-transfer-config '{"kv_connector":"MultiConnector","kv_role":"kv_both","kv_load_failure_policy":"recompute","kv_connector_extra_config":{"connectors":[{"kv_connector":"NixlConnector","kv_role":"kv_both","kv_load_failure_policy":"fail","kv_buffer_device":"cuda","kv_connector_extra_config":{"enforce_handshake_compat":false,"enable_cross_layers_blocks":false}},{"kv_connector":"MooncakeStoreConnector","kv_role":"kv_both","kv_load_failure_policy":"recompute","kv_connector_extra_config":{"load_async":true,"lookup_async":true,"enable_cross_layers_blocks":false,"enable_offload":false}}]}}' - ``` - - Decode (regular profile, `--max-num-seqs 128`): - - ```bash - python3 -m dynamo.vllm --model moonshotai/Kimi-K3 --served-model-name moonshotai/Kimi-K3 \ - --disaggregation-mode decode --request-plane tcp \ - --nnodes 2 --node-rank <0|1> --master-addr --device-ids 0,1,2,3 \ - --tensor-parallel-size 8 --decode-context-parallel-size 8 --dcp-comm-backend a2a \ - --attention-backend TOKENSPEED_MLA \ - --attention-config '{"mla_prefill_backend":"TRTLLM_RAGGED","use_prefill_query_quantization":true,"tokenspeed_mla_min_split_kv":8}' \ - --speculative-config '{"model":"Inferact/Kimi-K3-DSpark","attention_backend":"TOKENSPEED_MLA","method":"dspark","num_speculative_tokens":4,"draft_sample_method":"probabilistic","rejection_sample_method":"block"}' \ - --kv-cache-dtype fp8 --mamba-ssm-cache-dtype bfloat16 \ - --enable-prefix-caching --prefix-match-unit 128 \ - --gpu-memory-utilization 0.95 --max-num-seqs 128 --max-cudagraph-capture-size 512 --use-replayssm \ - --enable-cumem-allocator --enable-flashinfer-autotune \ - --load-format fastsafetensors --stream-interval 10 --language-model-only --trust-remote-code \ - --kv-transfer-config '{"kv_connector":"MultiConnector","kv_role":"kv_both","kv_load_failure_policy":"recompute","kv_connector_extra_config":{"connectors":[{"kv_connector":"NixlConnector","kv_role":"kv_both","kv_load_failure_policy":"fail","kv_buffer_device":"cuda","kv_connector_extra_config":{"enforce_handshake_compat":false,"enable_cross_layers_blocks":false}},{"kv_connector":"MooncakeStoreConnector","kv_role":"kv_both","kv_load_failure_policy":"recompute","kv_connector_extra_config":{"load_async":true,"lookup_async":true,"enable_cross_layers_blocks":false,"enable_offload":false}}]}}' - ``` - - For tool calling and reasoning output, add `--reasoning-parser kimi_k3 --dyn-tool-call-parser kimi_k3 --dyn-reasoning-parser kimi_k3 --dyn-enable-structural-tag` to both roles. `python3 -m dynamo.vllm` parses the remaining arguments as vLLM engine arguments only, so the vLLM API-server flags `--enable-auto-tool-choice` and `--tool-call-parser` are rejected there. - - ### Specialized decode (low concurrency) - - The c16-c32 points use the `latency_specialized` decode backend from [GB300 low-latency preset](#preset-gb300_low_latency) on the decode workers (1P3D for c16, 1P2D for c24-c32), with the Workers commands above changed as follows. The backend's requirements are listed there: V2 model runner (set in Environment), FULL decode CUDA graphs, BF16 convolution state with FP32 recurrent state, DSpark with at most 7 draft tokens, no pipeline or expert parallelism, and DCP of 1 or TP. The decode workers already meet the last two. - - Both roles: - - ```bash - --mamba-ssm-cache-dtype float32 --cp-kv-cache-interleave-size 1 --no-enable-flashinfer-autotune - # replacing --mamba-ssm-cache-dtype bfloat16 and --enable-flashinfer-autotune - ``` - - Pin `--cp-kv-cache-interleave-size 1` on both roles. Left unset, NIXL P/D raises the DCP KV interleave to the KV block size after the specialized decode has read it, so its spec-verify causal mask uses the stale value. - - Decode only: - - ```bash - --additional-config '{"decode_backend":"latency_specialized"}' \ - --attention-config '{"mla_prefill_backend":"TRTLLM_RAGGED","use_prefill_query_quantization":true}' \ - --max-num-seqs 12 --max-cudagraph-capture-size 64 \ - --compilation-config "{\"cudagraph_mode\":\"FULL_AND_PIECEWISE\",\"cudagraph_capture_sizes\":[$(seq -s, 1 64)]}" - # replacing the regular decode's --attention-config (it also sets tokenspeed_mla_min_split_kv), - # --max-num-seqs 128 and --max-cudagraph-capture-size 512, and without --use-replayssm - ``` - - `--max-num-seqs` is `64 // (K + 1)` with the K4 draft, so a full verify batch (12 x 5 = 60 tokens) fits the 64-token graph, and CUDA graphs are captured for every batch size up to 64. The prefill workers do not set `--additional-config`: the backend rejects the expert parallelism they use. The uncapped c48+ points keep the regular profile, so TTFT is not traded for ITL by a standing decode queue. - ### Frontend ```bash @@ -967,10 +931,10 @@ presets: hardware: gb300 strategy: pd_cluster workload: "Agentic (InferenceX AgentX)" - description: "Three TP8 + EP8 prefill workers across two GB300 trays each and one DEP16 decode worker across four trays, served by Dynamo with the DSpark K4 draft and online MXFP8 target projections." - source: { label: "InferenceX #4516", href: "https://github.com/SemiAnalysisAI/InferenceX/pull/4516", key: "kimik3-fp4-gb300-dynamo-vllm-agentic-dspark-mooncake-dep16-disagg" } + description: "Three TP8 + EP8 prefill workers across two GB300 trays each and one DEP16 decode worker across four trays on KV-balanced decode attention, served by Dynamo with the DSpark K4 draft and online MXFP8 target projections." + source: { label: "InferenceX #4638", href: "https://github.com/SemiAnalysisAI/InferenceX/pull/4638", key: "kimik3-fp4-gb300-dynamo-vllm-agentic-dspark-mooncake-dep16-disagg", revision: bc4d49f5df307680be5a1567e01b838b83b31b62 } image: - vllm: "vllm/vllm-openai:nightly-e4a5b5537344b103ed10de7764378ef09604ef3d" + vllm: "vllm/vllm-openai:nightly-3966924d566340e971a6da313e790d05492aa505@sha256:f255c4fad2f30ff5ee80db14d3a03afe9ae0f10d3470339864ff04745fe44cb3" dynamo: "a0de541ea8c861d5ac01d15953ca529bea68637b" # Served by Dynamo with the Rust frontend; tool-call flags off under Dynamo, # and the K4 draft is in args instead of the toggle's generic K8 draft. @@ -978,10 +942,19 @@ presets: pd_router: dynamo frontend: rust features: { tool_calling: false, spec_decoding: false } - # GB300: the InferenceX AgentX 3P1D lane (SemiAnalysisAI/InferenceX#4516), - # TP8 + EP8 prefill pools (no DCP) and one DEP16 decode pool with MegaMoE. - # The pd_cluster Blackwell block replaces the recipe Blackwell baseline, so - # the shared serving flags are restated here; role blocks add the rest. + # NIXL P/D plus the shared Mooncake store in host DRAM (embedded mode). + kv_offload: kv_store_distributed_mooncake + # The lane's connector options on the composed MultiConnector. + json_args: + "--kv-transfer-config": + kv_load_failure_policy: recompute + NixlConnector: { kv_buffer_device: cuda, kv_connector_extra_config: { enforce_handshake_compat: false, enable_cross_layers_blocks: false } } + MooncakeStoreConnector: { kv_load_failure_policy: recompute, kv_connector_extra_config: { enable_cross_layers_blocks: false, enable_offload: false } } + # GB300: the InferenceX AgentX 3P1D lane (SemiAnalysisAI/InferenceX#4516, + # refreshed in #4638), TP8 + EP8 prefill pools (no DCP) and one DEP16 + # decode pool with MegaMoE. The pd_cluster Blackwell block replaces the + # recipe Blackwell baseline, so the shared serving flags are restated here; + # role blocks add the rest. args: - "--attention-backend" - "TOKENSPEED_MLA" @@ -1034,9 +1007,10 @@ presets: # The qualified workers left UCX on its default transports; this # replaces the Blackwell PD block's rc,cuda_copy restriction. UCX_TLS: "all" + DYN_TCP_REQUEST_TIMEOUT: "120" + ETCD_LEASE_TTL: "600" VLLM_ENGINE_READY_TIMEOUT_S: "3600" VLLM_EXECUTE_MODEL_TIMEOUT_SECONDS: "1800" - VLLM_RPC_TIMEOUT: "600000" roles: prefill: parallelism: tep @@ -1063,9 +1037,11 @@ presets: # One DEP16 pool (TP1 x DP16 with expert parallelism) across four trays. parallelism: dep nodes: 4 - # Decode also quantizes o_proj and the shared experts online to MXFP8. + # Decode also quantizes o_proj and the shared experts online to MXFP8, + # and runs target and draft attention on KIMI_K3_KV_BALANCED_MLA. # FULL_AND_PIECEWISE is vLLM's V1 default, which the qualified decode - # ran with; it replaces the DEP strategy's FULL_DECODE_ONLY. + # ran with; it replaces the DEP strategy's FULL_DECODE_ONLY. The decode + # ranks only add capacity to the Mooncake store (kv_consumer, no lookup). args: - "--compilation-config" - '{"cudagraph_mode":"FULL_AND_PIECEWISE"}' @@ -1076,81 +1052,44 @@ presets: - "--max-cudagraph-capture-size" - "512" - "--use-replayssm" + - "--attention-backend" + - "KIMI_K3_KV_BALANCED_MLA" - "--attention-config" - '{"mla_prefill_backend":"TRTLLM_RAGGED","use_prefill_query_quantization":true,"tokenspeed_mla_min_split_kv":8}' + json_args: + "--speculative-config": { attention_backend: "KIMI_K3_KV_BALANCED_MLA" } + "--kv-transfer-config": { MooncakeStoreConnector: { kv_role: kv_consumer, kv_connector_extra_config: { enable_lookup: false } } } + # The lane ran one configuration at three client counts. + points: + c256: + label: "256 (3P1D)" + concurrency: 256 + description: "Three prefill workers and one DEP16 decode worker (40 GPUs)." + c288: + label: "288 (3P1D)" + concurrency: 288 + description: "Three prefill workers and one DEP16 decode worker (40 GPUs)." + c336: + label: "336 (3P1D)" + concurrency: 336 + default: true + description: "Three prefill workers and one DEP16 decode worker (40 GPUs)." runbook: | - The GB300 AgentX 3P1D lane in [SemiAnalysis InferenceX](https://inferencex.semianalysis.com/) (`kimik3-fp4-gb300-dynamo-vllm-agentic-dspark-mooncake-dep16-disagg`, [SemiAnalysisAI/InferenceX#4516](https://github.com/SemiAnalysisAI/InferenceX/pull/4516)) runs Dynamo + vLLM with three prefill workers and one decode worker at 336 clients (40 GPUs). Each prefill worker is TP8 + EP8 across two 4-GPU GB300 trays, without DCP. The decode worker is DEP16 (`--tensor-parallel-size 1 --data-parallel-size 16` with expert parallelism) across four trays. Both roles use MegaMoE, `--max-num-seqs 32` (per data-parallel rank on decode) and online MXFP8 for selected target projections. KV moves over NIXL, and all workers share an RDMA Mooncake store. Image `vllm/vllm-openai:nightly-e4a5b5537344b103ed10de7764378ef09604ef3d`, Dynamo `a0de541ea8c861d5ac01d15953ca529bea68637b`. + The GB300 AgentX 3P1D lane in [SemiAnalysis InferenceX](https://inferencex.semianalysis.com/) (`kimik3-fp4-gb300-dynamo-vllm-agentic-dspark-mooncake-dep16-disagg`, [SemiAnalysisAI/InferenceX#4516](https://github.com/SemiAnalysisAI/InferenceX/pull/4516), refreshed in [#4638](https://github.com/SemiAnalysisAI/InferenceX/pull/4638)) runs Dynamo + vLLM with three prefill workers and one decode worker at 256, 288 and 336 clients (40 GPUs), with the same configuration at every point. Each prefill worker is TP8 + EP8 across two 4-GPU GB300 trays, without DCP. The decode worker is DEP16 (`--tensor-parallel-size 1 --data-parallel-size 16` with expert parallelism) across four trays and runs the target and the DSpark draft on `KIMI_K3_KV_BALANCED_MLA`; prefill stays on `TOKENSPEED_MLA`. Both roles use MegaMoE, `--max-num-seqs 32` (per data-parallel rank on decode) and online MXFP8 for selected target projections. KV moves over NIXL, and all workers share an RDMA Mooncake store in host DRAM. Image `vllm/vllm-openai:nightly-3966924d566340e971a6da313e790d05492aa505` (vLLM `0.33.0rc1.dev229+g3966924d5`), Dynamo built from `a0de541ea8c861d5ac01d15953ca529bea68637b`, Mooncake `0.3.13.post1` from the image. - The preset renders one prefill pool and the decode pool. It turns **Spec decoding** off, because the toggle's generic `RedHatAI/Kimi-K3-speculator.dspark` K8 draft was not qualified, and **Tool calling** off (see the parser note under Workers). - - Documented here only: the three prefill workers, the Mooncake store connector (the builder renders a plain NIXL connector, without the qualified MultiConnector, recompute policy and load options), the Mooncake services, prefill `--kv-events-config`, `NVSHMEM_HCA_LIST`, and the frontend's router mode and session affinity (the builder renders round-robin). - - **Target quantization:** `--quantization-config` quantizes these target layers online to MXFP8: the KDA `in_proj_qkvgfab` and MLA `fused_qkv_a_g_proj` projections and the latent-MoE `routed_expert_up_proj`/`routed_expert_down_proj` projections on both roles, plus `o_proj` and the shared experts (`gate_up_proj`, `down_proj`) on decode. The DSpark draft is not quantized: vLLM builds the named draft from its own unquantized config, and the lane's logs show the online quantization pass only for the target layers. - - ### Services - - Start etcd, NATS and a Mooncake master once, as for the [AgentX xPyD preset](#preset-gb300_agentx_pd): `mooncake_master --enable_http_metadata_server=true --default_kv_lease_ttl=60000 --eviction_high_watermark_ratio=0.95 --eviction_ratio=0.10`. The workers' Mooncake store uses P2P handshake metadata, RDMA over the node's NICs, a 160GB global segment and a 4GB local buffer, with offload disabled. - - ### Environment (every worker) - - ```bash - export VLLM_ALLREDUCE_USE_FLASHINFER=1 VLLM_ENABLE_GEMM_RS=1 VLLM_USE_NCCL_SYMM_MEM=0 - export NCCL_CUMEM_ENABLE=1 NCCL_MNNVL_ENABLE=1 NCCL_NVLS_ENABLE=1 NCCL_P2P_LEVEL=NVL - export NVSHMEM_REMOTE_TRANSPORT=none NVSHMEM_DISABLE_MNNVL=0 NVSHMEM_IB_ENABLE_IBGDA=0 NVSHMEM_ENABLE_NIC_PE_MAPPING=1 - export NVSHMEM_HCA_LIST= - export VLLM_USE_V2_MODEL_RUNNER=1 VLLM_USE_RUST_FRONTEND=1 VLLM_SSM_CONV_STATE_LAYOUT=DS - export VLLM_MOONCAKE_LOAD_RECV_THREADS=4 MC_SLICE_SIZE=1048576 MC_MAX_MR_SIZE=4294967296 MC_ENABLE_DEST_DEVICE_AFFINITY=1 - export UCX_MEMTYPE_CACHE=n UCX_MEMTYPE_REG_WHOLE=n UCX_RCACHE_MAX_UNRELEASED=1024 UCX_TCP_AF_PRIO=inet - export DYN_REQUEST_PLANE=tcp DYN_TCP_REQUEST_TIMEOUT=120 ETCD_LEASE_TTL=600 - export VLLM_ENGINE_READY_TIMEOUT_S=3600 VLLM_EXECUTE_MODEL_TIMEOUT_SECONDS=1800 VLLM_RPC_TIMEOUT=600000 - ``` + Each point renders one prefill worker and the decode worker; start three prefill workers with the same command. Run each prefill worker on its two trays with the same `--master-addr` (the first tray's address), `--nnodes 2` and `--node-rank 0` or `1`, and the decode worker on each of its four trays with `--data-parallel-start-rank` 0, 4, 8 and 12 and the same `--data-parallel-address`. Set `NVSHMEM_HCA_LIST` to the node's RDMA HCAs on every worker. - Prefill workers also set `VLLM_KIMI_K3_SHARD_SP_SHARED_EXPERT=0`, `DYN_BACKEND_ADMISSION_ENGINE_REQUEST_LIMIT=1024` and `DYN_BACKEND_ADMISSION_ENGINE_REQUEST_WAITING_LIMIT=1024`. + `KIMI_K3_KV_BALANCED_MLA` is an FP8 MLA decode backend that plans its split-KV work on the device; it is never selected automatically, and prefill steps use the configured MLA prefill backend. - ### Workers + The preset turns **Spec decoding** off, because the toggle's generic `RedHatAI/Kimi-K3-speculator.dspark` K8 draft was not qualified, and **Tool calling** off; for tool calling and reasoning output under Dynamo, add `--dyn-tool-call-parser kimi_k3 --dyn-reasoning-parser kimi_k3 --dyn-enable-structural-tag` to both roles (`python3 -m dynamo.vllm` rejects the vLLM API-server flags `--enable-auto-tool-choice` and `--tool-call-parser`). - Prefill (three workers; run each on its two trays with the same `--master-addr`, the first tray's address, `--nnodes 2` and `--node-rank 0` or `1`): + The preset selects the distributed (embedded) Mooncake store on the **KV Offload** row with the same connector options as the [AgentX xPyD preset](#preset-gb300_agentx_pd); the decode ranks only add capacity to the store (`kv_consumer` with `enable_lookup: false`). Documented here only: the three prefill workers, the lane's Mooncake master options and store sizing, `NVSHMEM_HCA_LIST`, and the frontend's router mode and session affinity (the builder renders round-robin). - ```bash - python3 -m dynamo.vllm --model moonshotai/Kimi-K3 --served-model-name moonshotai/Kimi-K3 \ - --disaggregation-mode prefill --request-plane tcp \ - --nnodes 2 --node-rank <0|1> --master-addr --device-ids 0,1,2,3 \ - --tensor-parallel-size 8 --enable-expert-parallel --enable-ep-weight-filter --moe-backend deep_gemm_mega_moe \ - --quantization-config '{"targets":{"*.in_proj_qkvgfab":"mxfp8","*.fused_qkv_a_g_proj":"mxfp8","*.routed_expert_down_proj":"mxfp8","*.routed_expert_up_proj":"mxfp8"}}' \ - --attention-backend TOKENSPEED_MLA \ - --attention-config '{"mla_prefill_backend":"TRTLLM_RAGGED","use_prefill_query_quantization":true,"trtllm_ragged_backend":"cute-dsl"}' \ - --speculative-config '{"model":"Inferact/Kimi-K3-DSpark","attention_backend":"TOKENSPEED_MLA","method":"dspark","num_speculative_tokens":4,"draft_sample_method":"probabilistic","rejection_sample_method":"block"}' \ - --kv-cache-dtype fp8 --mamba-ssm-cache-dtype bfloat16 \ - --enable-prefix-caching --prefix-match-unit 128 \ - --gpu-memory-utilization 0.95 --max-num-seqs 32 --max-num-batched-tokens 16384 --max-cudagraph-capture-size 8192 \ - --compilation-config '{"cudagraph_mode":"FULL_AND_PIECEWISE","cudagraph_capture_sizes":[4,8,12,16,20,24,32,40,48,64,80,96,128,160,256,384,512,640,768,896,1024,1152,1280,1408,1536,1664,1792,1920,2048,2304,2560,2816,3072,3328,3584,3840,4096,4608,5120,5632,6144,6656,7168,7680,8192]}' \ - --enable-cumem-allocator --enable-flashinfer-autotune \ - --load-format fastsafetensors --stream-interval 10 --language-model-only --trust-remote-code \ - --kv-events-config '{"publisher":"zmq","topic":"kv-events","enable_kv_cache_events":true,"endpoint":"tcp://*:5200"}' \ - --kv-transfer-config '{"kv_connector":"MultiConnector","kv_role":"kv_both","kv_load_failure_policy":"recompute","kv_connector_extra_config":{"connectors":[{"kv_connector":"NixlConnector","kv_role":"kv_both","kv_load_failure_policy":"fail","kv_buffer_device":"cuda","kv_connector_extra_config":{"enforce_handshake_compat":false,"enable_cross_layers_blocks":false}},{"kv_connector":"MooncakeStoreConnector","kv_role":"kv_both","kv_load_failure_policy":"recompute","kv_connector_extra_config":{"load_async":true,"lookup_async":true,"enable_cross_layers_blocks":false,"enable_offload":false}}]}}' - ``` - - Decode (one DEP16 worker; run on each of its four trays with `--data-parallel-start-rank` 0, 4, 8 and 12 and the same `--data-parallel-address`, the first tray's address): + **Target quantization:** `--quantization-config` quantizes these target layers online to MXFP8: the KDA `in_proj_qkvgfab` and MLA `fused_qkv_a_g_proj` projections and the latent-MoE `routed_expert_up_proj`/`routed_expert_down_proj` projections on both roles, plus `o_proj` and the shared experts (`gate_up_proj`, `down_proj`) on decode. The DSpark draft is not quantized: vLLM builds the named draft from its own unquantized config, and the lane's logs show the online quantization pass only for the target layers. - ```bash - python3 -m dynamo.vllm --model moonshotai/Kimi-K3 --served-model-name moonshotai/Kimi-K3 \ - --disaggregation-mode decode --request-plane tcp --device-ids 0,1,2,3 \ - --tensor-parallel-size 1 --data-parallel-size 16 --data-parallel-size-local 4 \ - --data-parallel-start-rank <0|4|8|12> --data-parallel-address --data-parallel-rpc-port \ - --data-parallel-hybrid-lb --enable-expert-parallel --enable-ep-weight-filter --moe-backend deep_gemm_mega_moe \ - --quantization-config '{"targets":{"*.in_proj_qkvgfab":"mxfp8","*.fused_qkv_a_g_proj":"mxfp8","*.routed_expert_down_proj":"mxfp8","*.routed_expert_up_proj":"mxfp8","*.o_proj":"mxfp8","*.shared_experts.gate_up_proj":"mxfp8","*.shared_experts.down_proj":"mxfp8"}}' \ - --attention-backend TOKENSPEED_MLA \ - --attention-config '{"mla_prefill_backend":"TRTLLM_RAGGED","use_prefill_query_quantization":true,"tokenspeed_mla_min_split_kv":8}' \ - --speculative-config '{"model":"Inferact/Kimi-K3-DSpark","attention_backend":"TOKENSPEED_MLA","method":"dspark","num_speculative_tokens":4,"draft_sample_method":"probabilistic","rejection_sample_method":"block"}' \ - --kv-cache-dtype fp8 --mamba-ssm-cache-dtype bfloat16 \ - --no-enable-prefix-caching --prefix-match-unit 128 \ - --gpu-memory-utilization 0.95 --max-num-seqs 32 --max-num-batched-tokens 512 --max-cudagraph-capture-size 512 --use-replayssm \ - --enable-cumem-allocator --enable-flashinfer-autotune \ - --load-format fastsafetensors --stream-interval 10 --language-model-only --trust-remote-code \ - --kv-transfer-config '{"kv_connector":"MultiConnector","kv_role":"kv_both","kv_load_failure_policy":"recompute","kv_connector_extra_config":{"connectors":[{"kv_connector":"NixlConnector","kv_role":"kv_both","kv_load_failure_policy":"fail","kv_buffer_device":"cuda","kv_connector_extra_config":{"enforce_handshake_compat":false,"enable_cross_layers_blocks":false}},{"kv_connector":"MooncakeStoreConnector","kv_role":"kv_consumer","kv_load_failure_policy":"recompute","kv_connector_extra_config":{"load_async":true,"lookup_async":true,"enable_lookup":false,"enable_cross_layers_blocks":false,"enable_offload":false}}]}}' - ``` + ### Services - For tool calling and reasoning output, add `--reasoning-parser kimi_k3 --dyn-tool-call-parser kimi_k3 --dyn-reasoning-parser kimi_k3 --dyn-enable-structural-tag` to both roles; `python3 -m dynamo.vllm` rejects the vLLM API-server flags `--enable-auto-tool-choice` and `--tool-call-parser`. + Start etcd, NATS and a Mooncake master once, and give every worker the Mooncake store configuration through `MOONCAKE_CONFIG_PATH`, as for the [AgentX xPyD preset](#preset-gb300_agentx_pd). The store uses P2P handshake metadata, RDMA over the node's NICs, a 160GB global segment and a 4GB local buffer, with offload disabled. ### Frontend @@ -1238,6 +1177,129 @@ presets: Make the same target checkpoint available at the same local path on every node, and make `Inferact/Kimi-K3-DSpark` available through the mounted Hugging Face cache. Run the preset's commands inside the nightly image on every allocated node, with four visible GPUs, host networking, sufficient shared memory, and the devices and network access required by your NVLink/RDMA deployment. Use the reachable address of rank 0 for `MASTER_ADDR` on every node and a distinct `NODE_RANK` from `0` through `NODES - 1`. The lane served the checkpoint from a local path with `--served-model-name moonshotai/Kimi-K3` and `--device-ids 0,1,2,3`; the builder serves the Hugging Face id. + b300_agentx_agg: + label: "AgentX aggregated (Mooncake)" + hardware: b300 + strategy: single_node_tp + workload: "Agentic (InferenceX AgentX)" + description: "TP8 + DCP8 on one B300 node with the Inferact DSpark draft (K7 at concurrency 1, K5 at 4-8, K3 at 12-28) and an embedded RDMA Mooncake DRAM store." + source: { label: "InferenceX #4381", href: "https://github.com/SemiAnalysisAI/InferenceX/pull/4381", key: "kimik3-fp4-b300-vllm-agentic-dspark", revision: 809f7045172ea5effde45cef7cbe02918f25f968 } + image: "vllm/vllm-openai@sha256:8c1434bfee7d7626eef78ef70600434ffba4a1b8813cb1d1c8707ad25f47035e" + # vLLM's own Python frontend. The draft is in args (TOKENSPEED_MLA attention, + # Inferact checkpoint), so the generic Spec decoding toggle stays off. + select: + frontend: python + kv_offload: kv_store_distributed_mooncake + features: { spec_decoding: false, text_only: true, tool_calling: true, reasoning: true } + # Lane settings that hold at every concurrency. The Blackwell baseline already + # supplies fastsafetensors, 1M context, fp8 KV, prefix caching, TOKENSPEED_MLA + # decode and --prefix-match-unit 128. The recipe has no DCP selector, so the + # lane's DCP8 pairing (a2a, TRTLLM_RAGGED prefill) is written out here. + args: + - "--decode-context-parallel-size" + - "8" + - "--dcp-comm-backend" + - "a2a" + - "--max-num-batched-tokens" + - "16384" + - "--enable-flashinfer-autotune" + - "--enable-cumem-allocator" + - "--mamba-ssm-cache-dtype" + - "bfloat16" + - "--stream-interval" + - "10" + - "--gpu-memory-utilization" + - "0.92" + - "--moe-backend" + - "auto" + - "--speculative-config" + - '{"model":"Inferact/Kimi-K3-DSpark","attention_backend":"TOKENSPEED_MLA","method":"dspark","num_speculative_tokens":3,"draft_sample_method":"probabilistic","rejection_sample_method":"block"}' + remove_args: + - "--no-enable-flashinfer-autotune" + - "--disable-flashinfer-autotune" + env: + VLLM_ENABLE_K3_LATENT_MOE_TAIL_FUSION: "1" + VLLM_USE_DIRECT_DCP_A2A: "1" + VLLM_USE_DIRECT_DCP_Q_GATHER: "1" + VLLM_USE_DIRECT_DCP_KV_GATHER: "1" + VLLM_RPC_TIMEOUT: "600000" + VLLM_EXECUTE_MODEL_TIMEOUT_SECONDS: "1800" + VLLM_PREFIX_CACHE_RETENTION_INTERVAL: "0" + VLLM_MEMORY_PROFILER_ESTIMATE_CUDAGRAPHS: "0" + VLLM_HTTP_TIMEOUT_KEEP_ALIVE: "900" + VLLM_MOONCAKE_LOAD_RECV_THREADS: "4" + TORCH_CUDA_ARCH_LIST: "10.0" + PYTHONHASHSEED: "42" + MC_STORE_MEMCPY: "1" + MC_ENABLE_DEST_DEVICE_AFFINITY: "1" + MC_SLICE_SIZE: "1048576" + MC_WORKERS_PER_CTX: "4" + json_args: + "--attention-config": { mla_prefill_backend: "TRTLLM_RAGGED", use_prefill_query_quantization: true } + "--compilation-config": { cudagraph_mode: "FULL_AND_PIECEWISE" } + "--kv-transfer-config": { kv_load_failure_policy: "recompute", kv_connector_extra_config: { load_async: true, lookup_async: true, enable_offload: false } } + # One point per measured concurrency. S is --max-num-seqs (2x concurrency); the + # graphs capture (1 + K) x 1..S tokens, then the shared 256..8192 tail. + points: + c1: + label: "1" + concurrency: 1 + args: ["--max-num-seqs", "2"] + json_args: + "--speculative-config": { num_speculative_tokens: 7 } + "--compilation-config": { cudagraph_capture_sizes: [8, 16, 64, 128, 256, 512, 1024, 2048, 4096, 8192] } + c4: + label: "4" + concurrency: 4 + args: ["--max-num-seqs", "8"] + json_args: + "--speculative-config": { num_speculative_tokens: 5 } + "--compilation-config": { cudagraph_capture_sizes: [6, 12, 18, 24, 30, 36, 42, 48, 54, 60, 66, 72, 78, 84, 90, 96, 128, 256, 512, 1024, 2048, 4096, 8192] } + c8: + label: "8" + concurrency: 8 + args: ["--max-num-seqs", "16"] + json_args: + "--speculative-config": { num_speculative_tokens: 5 } + "--compilation-config": { cudagraph_capture_sizes: [6, 12, 18, 24, 30, 36, 42, 48, 54, 60, 66, 72, 78, 84, 90, 96, 128, 256, 512, 1024, 2048, 4096, 8192] } + c12: + label: "12" + concurrency: 12 + args: ["--max-num-seqs", "24"] + json_args: + "--compilation-config": { cudagraph_capture_sizes: [4, 8, 12, 16, 20, 24, 28, 32, 36, 40, 44, 48, 52, 56, 60, 64, 68, 72, 76, 80, 84, 88, 92, 96, 100, 104, 108, 112, 128, 256, 512, 1024, 2048, 4096, 8192] } + c14: + label: "14" + concurrency: 14 + args: ["--max-num-seqs", "28"] + json_args: + "--compilation-config": { cudagraph_capture_sizes: [4, 8, 12, 16, 20, 24, 28, 32, 36, 40, 44, 48, 52, 56, 60, 64, 68, 72, 76, 80, 84, 88, 92, 96, 100, 104, 108, 112, 128, 256, 512, 1024, 2048, 4096, 8192] } + c16: + label: "16" + concurrency: 16 + default: true + args: ["--max-num-seqs", "32"] + json_args: + "--compilation-config": { cudagraph_capture_sizes: [4, 8, 12, 16, 20, 24, 28, 32, 36, 40, 44, 48, 52, 56, 60, 64, 68, 72, 76, 80, 84, 88, 92, 96, 100, 104, 108, 112, 116, 120, 124, 128, 256, 512, 1024, 2048, 4096, 8192] } + c24: + label: "24" + concurrency: 24 + args: ["--max-num-seqs", "48"] + json_args: + "--compilation-config": { cudagraph_capture_sizes: [4, 8, 12, 16, 20, 24, 28, 32, 36, 40, 44, 48, 52, 56, 60, 64, 68, 72, 76, 80, 84, 88, 92, 96, 100, 104, 108, 112, 116, 120, 124, 128, 132, 136, 140, 144, 148, 152, 156, 160, 164, 168, 172, 176, 180, 184, 188, 192, 196, 200, 204, 208, 212, 216, 220, 224, 228, 232, 236, 240, 244, 248, 252, 256, 512, 1024, 2048, 4096, 8192] } + c28: + label: "28" + concurrency: 28 + args: ["--max-num-seqs", "56"] + json_args: + "--compilation-config": { cudagraph_capture_sizes: [4, 8, 12, 16, 20, 24, 28, 32, 36, 40, 44, 48, 52, 56, 60, 64, 68, 72, 76, 80, 84, 88, 92, 96, 100, 104, 108, 112, 116, 120, 124, 128, 132, 136, 140, 144, 148, 152, 156, 160, 164, 168, 172, 176, 180, 184, 188, 192, 196, 200, 204, 208, 212, 216, 220, 224, 228, 232, 236, 240, 244, 248, 252, 256, 512, 1024, 2048, 4096, 8192] } + runbook: | + The B300 AgentX lane in [SemiAnalysis InferenceX](https://inferencex.semianalysis.com/) (`kimik3-fp4-b300-vllm-agentic-dspark`, [SemiAnalysisAI/InferenceX#4381](https://github.com/SemiAnalysisAI/InferenceX/pull/4381), pinned to [`809f704`](https://github.com/SemiAnalysisAI/InferenceX/tree/809f7045172ea5effde45cef7cbe02918f25f968/inferencex-e2e/benchmarks/single_node/srt-slurm-recipes/kimik3/vllm/b300-fp4-mtp)) serves the default `moonshotai/Kimi-K3` checkpoint (MXFP4 weights) on one 8-GPU B300 node with DSpark speculative decoding and an embedded Mooncake DRAM tier. It is a benchmark configuration; the settings in this preset are its production-useful parts. Draft length falls with concurrency (K7 at 1, K5 at 4-8, K3 at 12-28), and each point sets `--max-num-seqs` to twice the concurrency with CUDA graphs covering `(1 + K) x 1..max-num-seqs` tokens. Throughput runs use InferenceX's golden acceptance lengths for DSpark (3.84, 3.62 and 3.00 for K7, K5 and K3). + + **Mooncake store.** The builder's **Mooncake** option renders the master and store config with a 100GB segment, which is the default. The lane gives each of the 8 TP ranks a 256GB `global_segment_size` and a 4GB `local_buffer_size`, 2,080 GiB of DRAM across the node in total, so raise the segment above the default only when the node has that much pinned memory to spare. The lane's master sets `--eviction_high_watermark_ratio=0.95 --eviction_ratio=0.10`, matching the builder, and leaves the lease TTL at the Mooncake default. The store runs embedded over RDMA with `P2PHANDSHAKE` metadata. Pin `device_name` to one ACTIVE InfiniBand port driven by `mlx5_core`: this lane's ranks use a single rail because the rails are isolated from each other, so a port that is down or an EFA `rdmap*` device (no RC queue pairs) fails registration. An InfiniBand port addresses peers with GID index 0 (`MC_GID_INDEX=0`). + + **Kept out of the preset.** `--disable-uvicorn-access-log`, `PYTHONNOUSERSITE=1` and the benchmark client variables are InferenceX harness or logging controls with no serving effect. `--moe-backend auto` is vLLM's default selection and is stated only to match the lane. The lane's `RDMAV_DRIVERS` and `WITH_NVIDIA_PEERMEM=0` are specific to the DSXE cluster (a host-library mount for its `libmlx5` verbs provider, and peermem not loaded) and apply only on nodes with the same setup. + mi355x_agentx: label: "AgentX TP8" hardware: mi355x diff --git a/recipes/models/zai-org/GLM-5.3-Flash.yaml b/recipes/models/zai-org/GLM-5.3-Flash.yaml index 875be29630a5..de4d7213128a 100644 --- a/recipes/models/zai-org/GLM-5.3-Flash.yaml +++ b/recipes/models/zai-org/GLM-5.3-Flash.yaml @@ -4,7 +4,7 @@ meta: provider: "GLM (Z-AI)" description: "GLM-5.3-Flash is a 320B-total / 18B-active multimodal MoE with hybrid KDA and sparse MLA attention, native FP8 weights, MTP, and a 1M-token context window." date_added: 2026-08-27 - date_updated: 2026-10-08 + date_updated: 2026-10-19 tasks: - text - multimodal @@ -14,23 +14,26 @@ meta: b200: verified gb200: verified mi355x: verified - ascend_950pr_8x: verified model: model_id: "zai-org/GLM-5.3-Flash" min_vllm_version: "0.29.0" nightly_required: true + default_frontend: rust docker_image: nvidia: "vllm/vllm-openai:nightly" amd: "vllm/vllm-openai-rocm:nightly" install: docker: - note: "NVIDIA: use vllm/vllm-openai:glm53-flash until support lands in the standard image. AMD: use the official vllm/vllm-openai-rocm:nightly image, which includes merged GLM-5.3-Flash support. Ascend 950PR: pin quay.io/ascend/vllm-ascend:nightly-main-a5 (same as GLM-5.3) with GLM-5.3-Flash overlays vLLM#53906 and vllm-ascend#15127." + note: "Native GLM5Next rendering requires an image containing vllm-project/vllm#62424, including its Rust binary. Use NVIDIA/ROCm nightly images only once they include this change. Ascend 950PR also requires the nightly-main-a5 platform support and GLM-5.3-Flash overlays vLLM#53906 and vllm-ascend#15127." pip: false architecture: moe parameter_count: "321B" active_parameters: "18B" context_length: 1048576 + base_args: + - "--tokenizer-mode" + - "glm5_next" base_env: VLLM_ENGINE_READY_TIMEOUT_S: "3600" @@ -76,13 +79,6 @@ variants: vram_minimum_gb: 386 description: "Default native FP8 checkpoint (zai-org/GLM-5.3-Flash)" hardware_overrides: - # Exact-GPU pin: computeDockerMeta has no Huawei brand, so a model-level - # docker_image string would only affect NVIDIA. Hide pip — official - # wheels have no NPU kernels. - ascend_950pr_8x: - docker_image: "quay.io/ascend/vllm-ascend:nightly-main-a5" - install: - pip: false mi350x: &gfx950_aiter extra_env: VLLM_ROCM_USE_AITER_FUSION_SHARED_EXPERTS: "1" @@ -116,22 +112,6 @@ kv_offload_support: offloading_cpu: verified offloading_fs: verified -strategy_hardware: - single_node_tep: - npu: unsupported - single_node_dep: - npu: unsupported - multi_node_tp: - npu: unsupported - multi_node_tp_pp: - npu: unsupported - multi_node_tep: - npu: unsupported - multi_node_dep: - npu: unsupported - pd_cluster: - npu: unsupported - hardware_overrides: hopper: extra_args: @@ -149,34 +129,12 @@ hardware_overrides: - "ROCM_AITER_MLA_SPARSE" extra_env: VLLM_ROCM_USE_AITER: "1" - # Shared by 950PR 8x (8×128 GB). SFA / kpool indexer needs --block-size 512. - npu: - extra_args: - - "--trust-remote-code" - - "--block-size" - - "512" - - "--max-model-len" - - "131072" - - "--max-num-seqs" - - "16" - - "--max-num-batched-tokens" - - "8192" - - "--gpu-memory-utilization" - - "0.90" - extra_env: - PYTORCH_NPU_ALLOC_CONF: "expandable_segments:True" - HCCL_BUFFSIZE: "512" - HCCL_OP_EXPANSION_MODE: "AIV" - HCCL_CONNECT_TIMEOUT: "7200" hardware_notes: mi350x: &cdna4_indexer_topk_notes title: "Long-context indexer top-k opt-in" body: "When serving long contexts (>=64k), add `--sparse-indexer-topk-backend aiter` for improved decode performance." mi355x: *cdna4_indexer_topk_notes - ascend_950pr_8x: - title: "vLLM Ascend A5 image required" - body: "Single-node TP8 on 8×950PR. Pin quay.io/ascend/vllm-ascend:nightly-main-a5 and overlay vLLM#53906 / vllm-ascend#15127. Do not pass --quantization ascend. --block-size 512 is required (kpool indexer). EP is not available on this interconnect." strategy_overrides: pd_cluster: @@ -219,7 +177,7 @@ guide: | - **Weights:** about 306 GiB for the default native FP8 checkpoint before runtime and KV-cache overhead; the BF16 variant requires roughly twice the weight memory - - **vLLM:** use docker before the integration is included in the public repo + - **vLLM:** an image containing [native GLM5Next support (#62424)](https://github.com/vllm-project/vllm/pull/62424), including the matching Rust frontend binary. Older images need an updated build for the commands below. - **FlashInfer:** 0.6.17 or newer is required for NoPE sparse MLA The default model ID, `zai-org/GLM-5.3-Flash`, is the FP8 checkpoint. Select the BF16 @@ -227,12 +185,26 @@ guide: | `RedHatAI/GLM-5.3-Flash-NVFP4`. Its MoE experts are quantized to 4-bit (NVFP4), and it requires NVIDIA Blackwell GPUs. + ## Frontends + + The command builder defaults to **Rust** and adds `--tokenizer-mode glm5_next`. + With `VLLM_USE_RUST_FRONTEND=1`, this selects native GLM5Next chat rendering for + the GLM-5.3-Flash template, including tool calls and reasoning options. Image and + video inputs are not yet supported on the Rust frontend; use the Python frontend + for multimodal requests. + + Select **Python** in the command builder, or set `VLLM_USE_RUST_FRONTEND=0`, + to use the Hugging Face renderer and processor on the same build. On older builds + that do not recognize `glm5_next`, also replace `--tokenizer-mode glm5_next` with + `--tokenizer-mode auto`. + ## Launching the server ### FP8 with TP4 and MTP on one GB200 tray ```bash - vllm serve zai-org/GLM-5.3-Flash \ + VLLM_USE_RUST_FRONTEND=1 vllm serve zai-org/GLM-5.3-Flash \ + --tokenizer-mode glm5_next \ --tensor-parallel-size 4 \ --kv-cache-dtype fp8 \ --speculative-config '{"method":"mtp","num_speculative_tokens":5}' \ @@ -259,7 +231,9 @@ guide: | VLLM_KV_CACHE_LAYOUT=HND \ UCX_NET_DEVICES=all \ VLLM_NIXL_SIDE_CHANNEL_PORT=5557 \ + VLLM_USE_RUST_FRONTEND=1 \ vllm serve zai-org/GLM-5.3-Flash \ + --tokenizer-mode glm5_next \ --port 8001 \ --tensor-parallel-size 4 \ --kv-transfer-config '{"kv_connector":"NixlConnector","kv_role":"kv_producer"}' \ @@ -275,7 +249,9 @@ guide: | VLLM_KV_CACHE_LAYOUT=HND \ UCX_NET_DEVICES=all \ VLLM_NIXL_SIDE_CHANNEL_PORT=5558 \ + VLLM_USE_RUST_FRONTEND=1 \ vllm serve zai-org/GLM-5.3-Flash \ + --tokenizer-mode glm5_next \ --port 8002 \ --tensor-parallel-size 4 \ --kv-transfer-config '{"kv_connector":"NixlConnector","kv_role":"kv_consumer"}' \ @@ -433,15 +409,16 @@ guide: | ## Running on AMD Instinct - Use the official `vllm/vllm-openai-rocm:nightly` image. GLM-5.3-Flash support - landed in [vLLM #53906](https://github.com/vllm-project/vllm/pull/53906). + Use a `vllm/vllm-openai-rocm:nightly` build containing #62424 for native Rust + rendering. Model support landed in [vLLM #53906](https://github.com/vllm-project/vllm/pull/53906). ```bash export VLLM_ROCM_USE_AITER=1 # gfx950 (MI350X/MI355X) only export VLLM_ROCM_USE_AITER_FUSION_SHARED_EXPERTS=1 export VLLM_ROCM_USE_AITER_TRITON_SPARSE_MLA=1 - vllm serve zai-org/GLM-5.3-Flash \ + VLLM_USE_RUST_FRONTEND=1 vllm serve zai-org/GLM-5.3-Flash \ + --tokenizer-mode glm5_next \ --served-model-name zai-org/GLM-5.3-Flash \ --tensor-parallel-size 4 \ --max-num-seqs 512 \ @@ -466,7 +443,8 @@ guide: | (MI350X/MI355X), using the topk indexer backend from AITER is recommended: ```bash - vllm serve zai-org/GLM-5.3-Flash \ + VLLM_USE_RUST_FRONTEND=1 vllm serve zai-org/GLM-5.3-Flash \ + --tokenizer-mode glm5_next \ --attention-backend ROCM_AITER_MLA_SPARSE \ --sparse-indexer-topk-backend aiter ``` diff --git a/recipes/models/zai-org/GLM-5.3.yaml b/recipes/models/zai-org/GLM-5.3.yaml index 3ac7e3ca2a8e..2570d061215a 100644 --- a/recipes/models/zai-org/GLM-5.3.yaml +++ b/recipes/models/zai-org/GLM-5.3.yaml @@ -4,7 +4,7 @@ meta: provider: "GLM (Z-AI)" description: "GLM-5.3 — Frontier Coding with Emergent Cyber Capabilities" date_added: 2026-08-28 - date_updated: 2026-10-15 + date_updated: 2026-10-19 tasks: - text performance_headline: "Latest GLM-5 series MoE — native FP8 by default, 5-token MTP, 1M context" @@ -20,9 +20,6 @@ meta: mi325x: verified # FP8 + 5-token MTP validated 2026-10-03 on 8×MI350X (TP8). mi350x: verified - # Validated 2026-08-30 on one Atlas A5 node (8×950PR, TP8, graph mode). - # Native FP8 path needs vllm-ascend#15346. - ascend_950pr_8x: verified # NVFP4 + --cpu-offload-gb on 1× GB300 (252 GB HBM + unified host RAM). # Do not raise taxonomy vram_gb — host RAM is recipe-specific, not HBM. dgx_station_gb300: verified @@ -37,11 +34,18 @@ model: default_frontend: rust docker_image: amd: "vllm/vllm-openai-rocm:v0.29.0" + install: + pip: + note: "Native GLM5Next rendering requires a build containing vllm-project/vllm#62424, including its Rust binary. The minimum version alone does not include this support." + docker: + note: "Use an image containing vllm-project/vllm#62424, including its Rust binary. Older hardware-specific image pins require an updated build for --tokenizer-mode glm5_next." architecture: moe parameter_count: "743B" active_parameters: "39B" context_length: 1048576 base_args: + - "--tokenizer-mode" + - "glm5_next" - "--kv-cache-dtype" - "fp8_e4m3" base_env: {} @@ -69,10 +73,6 @@ features: - "--speculative-config" - '{"method":"mtp","num_speculative_tokens":5}' hardware_overrides: - npu: - args: - - "--speculative-config" - - '{"method":"mtp","num_speculative_tokens":1,"enforce_eager":true}' dgx_station_gb300: args: - "--speculative-config" @@ -83,7 +83,6 @@ features: variants: [default] hardware: amd: unsupported - npu: unsupported args: - "--speculative-config" - '{"method":"dflash","model":"incoai/GLM-5.3-DFlash2","num_speculative_tokens":7}' @@ -111,13 +110,6 @@ variants: extra_args: - "--max-model-len" - "262144" - # Exact-GPU pin: computeDockerMeta has no Huawei brand, so a model-level - # docker_image string would only affect NVIDIA. Hide pip — official - # wheels have no NPU kernels. - ascend_950pr_8x: - docker_image: "quay.io/ascend/vllm-ascend:nightly-main-a5" - install: - pip: false nvfp4: model_id: "Inferact/GLM-5.3-NVFP4" precision: nvfp4 @@ -186,22 +178,6 @@ compatible_strategies: - multi_node_dep - pd_cluster -# EP/MC2 cannot acquire HCCL links on Atlas 350 (950PR). Multi-node and PD -# were not validated. Only single-node TP8 is offered on NPU. -strategy_hardware: - single_node_tep: - npu: unsupported - multi_node_tp: - npu: unsupported - multi_node_tp_pp: - npu: unsupported - multi_node_tep: - npu: unsupported - multi_node_dep: - npu: unsupported - pd_cluster: - npu: unsupported - hardware_overrides: hopper: # H100/H200 use plain fp8 KV cache; Blackwell keeps base_args' fp8_e4m3. @@ -218,35 +194,10 @@ hardware_overrides: extra_env: VLLM_ROCM_USE_AITER: "1" VLLM_ROCM_USE_AITER_FUSION_SHARED_EXPERTS: "1" - # Shared by 950PR 8x (8×128 GB). last-wins shadows --kv-cache-dtype fp8_e4m3 - # from base_args; SFA on 950 needs --block-size 512. - npu: - extra_args: - - "--trust-remote-code" - - "--kv-cache-dtype" - - "auto" - - "--block-size" - - "512" - - "--max-model-len" - - "131072" - - "--max-num-seqs" - - "16" - - "--gpu-memory-utilization" - - "0.9" - extra_env: - PYTORCH_NPU_ALLOC_CONF: "expandable_segments:True" - OMP_PROC_BIND: "false" - OMP_NUM_THREADS: "1" - TASK_QUEUE_ENABLE: "1" - HCCL_BUFFSIZE: "1024" - HCCL_OP_EXPANSION_MODE: "AIV" strategy_overrides: {} hardware_notes: - ascend_950pr_8x: - title: "vLLM Ascend A5 image required" - body: "Single-node TP8 on 8×950PR. Pin quay.io/ascend/vllm-ascend:nightly-main-a5 and overlay vllm-ascend#15346 for native FP8. EP is not available on this interconnect. Set HCCL_BUFFSIZE=1024 before launch." dgx_station_gb300: title: "CPU weight offload (NVFP4)" body: "1× GB300 (252 GB HBM) plus unified host RAM. NVFP4 offloads MoE expert weights (w13_weight, w2_weight) with --offload-backend uva and --cpu-offload-gb 222. Set VLLM_WEIGHT_OFFLOADING_DISABLE_PIN_MEMORY=1. Context is capped at 262K / 8 seqs. FP8/BF16 still do not fit this box." @@ -255,32 +206,41 @@ guide: | ## Overview **GLM-5.3** is the newest model in the GLM-5 series — a ~743B-parameter MoE (39B active) from **Z-AI**. - Architecture and serving flags are identical to GLM-5.2; the packaging change is that the - **default `zai-org/GLM-5.3` checkpoint is now native FP8** — BF16 weights live under the suffixed `zai-org/GLM-5.3-BF16` repo. + The **default `zai-org/GLM-5.3` checkpoint is native FP8** — BF16 weights live under the suffixed `zai-org/GLM-5.3-BF16` repo. The FP8 checkpoint fits on a single 8xH200 / 8xH20 node and — with FP8 KV cache — reaches the full 1M-token context on 8xB200. An NVFP4 checkpoint for Blackwell is published by Inferact (`Inferact/GLM-5.3-NVFP4`). ## Prerequisites - - **vLLM 0.28.0** or newer. + - **vLLM:** a build containing [native GLM5Next support (#62424)](https://github.com/vllm-project/vllm/pull/62424), including the matching Rust frontend binary. + - **Transformers:** 5.15.0 or newer. - **GPU:** 8xH200 or 8xH20 (141 GB each) for single-node FP8; 8xB200 (180 GB each) for the full 1M context. ## Installation - ```bash - uv venv - source .venv/bin/activate - uv pip install "vllm==0.28.0" --torch-backend=auto - uv pip install "transformers>=5.15.0" - ``` + Use a wheel or container built with #62424 for the Rust commands below. + The older release and hardware-specific image pins record prior model validation; + they need an updated build to use `--tokenizer-mode glm5_next`. + + ## Frontends + + The command builder defaults to **Rust** and adds `--tokenizer-mode glm5_next`. + With `VLLM_USE_RUST_FRONTEND=1`, this selects native GLM5Next chat rendering for + GLM-5.3, including tool calls and reasoning options. + + Select **Python** in the command builder, or set `VLLM_USE_RUST_FRONTEND=0`, + to use the Hugging Face renderer on the same build. On older builds that do not + recognize `glm5_next`, also replace `--tokenizer-mode glm5_next` with + `--tokenizer-mode auto`. ## Launching the server ### FP8 on 8xH200 (standard) ```bash - vllm serve zai-org/GLM-5.3 \ + VLLM_USE_RUST_FRONTEND=1 vllm serve zai-org/GLM-5.3 \ + --tokenizer-mode glm5_next \ --kv-cache-dtype fp8 \ --tensor-parallel-size 8 \ --speculative-config.method mtp \ @@ -296,7 +256,9 @@ guide: | ```bash VLLM_ROCM_USE_AITER=1 \ VLLM_ROCM_USE_AITER_FUSION_SHARED_EXPERTS=1 \ + VLLM_USE_RUST_FRONTEND=1 \ vllm serve zai-org/GLM-5.3 \ + --tokenizer-mode glm5_next \ --kv-cache-dtype fp8_e4m3 \ --tensor-parallel-size 8 \ --speculative-config.method mtp \ @@ -311,7 +273,7 @@ guide: | --moe-backend aiter ``` - On 8x MI325X with `vllm/vllm-openai-rocm:v0.29.0`, this command serves the full + Earlier validation on 8x MI325X with `vllm/vllm-openai-rocm:v0.29.0` served `--max-model-len 524288` at `--gpu-memory-utilization 0.80`, reporting a 1,477,168-token KV cache and 2.82x maximum concurrency. MTP is healthy there: a mean acceptance length of 2.77 with 5 tokens drafted per step on a ~500-token greedy generation. @@ -328,7 +290,8 @@ guide: | (`--kv-cache-dtype fp8_e4m3`, already in the base flags) roughly halves that budget. ```bash - vllm serve zai-org/GLM-5.3 \ + VLLM_USE_RUST_FRONTEND=1 vllm serve zai-org/GLM-5.3 \ + --tokenizer-mode glm5_next \ --kv-cache-dtype fp8_e4m3 \ --tensor-parallel-size 8 \ --speculative-config.method mtp \ @@ -351,7 +314,8 @@ guide: | Select the **NVFP4** variant above (Blackwell-only) or run: ```bash - vllm serve Inferact/GLM-5.3-NVFP4 \ + VLLM_USE_RUST_FRONTEND=1 vllm serve Inferact/GLM-5.3-NVFP4 \ + --tokenizer-mode glm5_next \ --tensor-parallel-size 8 \ --enable-expert-parallel \ --reasoning-parser glm47 \ @@ -368,7 +332,8 @@ guide: | with eight speculative tokens: ```bash - vllm serve zai-org/GLM-5.3 \ + VLLM_USE_RUST_FRONTEND=1 vllm serve zai-org/GLM-5.3 \ + --tokenizer-mode glm5_next \ --tensor-parallel-size 8 \ --speculative-config '{"method":"dspark","model":"RedHatAI/GLM-5.3-speculator.dspark","num_speculative_tokens":8}' ``` diff --git a/requirements/build/rock.txt b/requirements/build/rock.txt index 5ca5ada2af06..932057f11737 100644 --- a/requirements/build/rock.txt +++ b/requirements/build/rock.txt @@ -15,5 +15,5 @@ setuptools-rust>=1.9.0 wheel jinja2>=3.1.6 amdsmi==7.0.2 -timm>=1.0.17 +timm>=1.0.30 tilelang==0.1.10 diff --git a/requirements/build/rocm.txt b/requirements/build/rocm.txt deleted file mode 100644 index 3c9b6c179613..000000000000 --- a/requirements/build/rocm.txt +++ /dev/null @@ -1,19 +0,0 @@ -# Common dependencies --r ../common.txt - - ---extra-index-url https://repo.amd.com/rocm/whl-multi-arch/ -torch==2.12.0+rocm7.14.0 -torchvision==0.27.0+rocm7.14.0 -torchaudio==2.11.0+rocm7.14.0 -triton==3.7.1+git0263a6a6 -cmake>=3.26.1,<4 -packaging>=26.2 -setuptools>=78.1.1,<80.0.0 -setuptools-scm>=8 -setuptools-rust>=1.9.0 -wheel -jinja2>=3.1.6 -amdsmi==7.0.2 -timm>=1.0.17 -tilelang==0.1.10 diff --git a/requirements/common.txt b/requirements/common.txt index d235d9e8bd81..5dab173e4c12 100644 --- a/requirements/common.txt +++ b/requirements/common.txt @@ -8,9 +8,9 @@ certifi >= 2026.10.19 tqdm blake3 py-cpuinfo -transformers >= 5.16.1, < 5.21.0 -huggingface_hub >= 1.31.0 # Required for the `huggingface_hub.utils.httpx` re-export -tokenizers >= 0.23.2 # Required for fast incremental detokenization. +transformers >= 5.16.1, < 5.23.0 +huggingface_hub >= 2.0.0 +tokenizers >= 0.23.3 # Required for fast incremental detokenization and huggingface_hub 2. safetensors >= 0.6.2 # MXFP4/MXFP6 dtype support (F8_E8M0, F4) added in 0.6.0: https://github.com/huggingface/safetensors/pull/611 protobuf >= 5.29.6, !=6.30.*, !=6.31.*, !=6.32.*, !=6.33.0.*, !=6.33.1.*, !=6.33.2.*, !=6.33.3.*, !=6.33.4.*; platform_machine != "s390x" protobuf == 7.36.1; platform_machine == "s390x" @@ -36,7 +36,7 @@ partial-json-parser # used for parsing partial JSON outputs jsonschema >= 4.23.0 # required for MiniMax M3 tool schema validation pyzmq >= 25.0.0 msgspec -mistral_common[image] >= 1.11.6 +mistral_common[image] >= 1.13.0 opencv-python-headless >= 4.13.0 # required for video IO pyyaml six>=1.16.0; python_version > '3.11' # transitive dependency of pandas that needs to be the latest version for python 3.12 diff --git a/requirements/kv_connectors.txt b/requirements/kv_connectors.txt index baddf1cd633c..2c5593c3c6a0 100644 --- a/requirements/kv_connectors.txt +++ b/requirements/kv_connectors.txt @@ -2,7 +2,7 @@ lmcache >= 0.5.6 # CuPy 14.1.0 imports pytest from cupy.testing._random, which breaks runtime # images. 14.1.1 fixes the root cause, so only 14.1.0 is excluded. cupy-cuda13x != 14.1.0 -nixl == 1.5.1 +nixl == 1.5.2 # CUDA 12 build. On the CUDA 13 image install-kv-connectors.sh swaps this for # the mooncake-transfer-engine-cuda13 variant of the same version. mooncake-transfer-engine >= 0.3.12 diff --git a/requirements/kv_connectors_cu12.txt b/requirements/kv_connectors_cu12.txt index 60bb194b32ec..e1c6b58af3c6 100644 --- a/requirements/kv_connectors_cu12.txt +++ b/requirements/kv_connectors_cu12.txt @@ -3,5 +3,5 @@ lmcache == 0.5.6 cuda-python >= 12, < 13 cupy-cuda12x != 14.1.0 -nixl == 1.5.1 +nixl == 1.5.2 mooncake-transfer-engine >= 0.3.12 diff --git a/requirements/rocm.txt b/requirements/rocm.txt index cd7a11472157..a36e798ab072 100644 --- a/requirements/rocm.txt +++ b/requirements/rocm.txt @@ -16,9 +16,9 @@ packaging>=24.2 setuptools>=78.1.1,<80.0.0 setuptools-scm>=8 setuptools-rust>=1.9.0 -runai-model-streamer[s3,gcs,azure]==0.15.7 +runai-model-streamer[s3,gcs,azure]==0.17.1 conch-triton-kernels==1.2.1 -timm>=1.0.17 +timm>=1.0.30 # amd-quark: required for Quark quantization on ROCm # To be consistent with test_quark.py amd-quark==0.12.post1 diff --git a/requirements/rubin-prerelease.txt b/requirements/rubin-prerelease.txt index 720c08843159..157c8f421a3a 100644 --- a/requirements/rubin-prerelease.txt +++ b/requirements/rubin-prerelease.txt @@ -11,4 +11,4 @@ torchaudio==2.11.0.dev20261009+cu134 cuda-python==13.4.1 cuda-bindings==13.4.1 -nixl==1.4.1 +nixl==1.5.2 diff --git a/requirements/test/cpu.txt b/requirements/test/cpu.txt index 1fe62a75d014..ee94974a116f 100644 --- a/requirements/test/cpu.txt +++ b/requirements/test/cpu.txt @@ -347,7 +347,6 @@ httpx==0.27.2 # cohere # fastapi # fastapi-cloud-cli - # huggingface-hub # model-hosting-container-standards # openai # perceptron @@ -355,8 +354,9 @@ httpx==0.27.2 httpx2==2.12.0 # via # -r requirements/test/cuda.in + # huggingface-hub # mcp -huggingface-hub==1.31.0 +huggingface-hub==2.4.0 # via # -r requirements/test/../common.txt # accelerate @@ -508,7 +508,7 @@ mcp-types==2.3.0 # via mcp mdurl==0.1.2 # via markdown-it-py -mistral-common==1.11.6 +mistral-common==1.13.0 # via # -r requirements/test/../common.txt # -r requirements/test/cuda.in @@ -1010,13 +1010,13 @@ rpds-py==0.20.1 # referencing rsa==4.9.1 # via google-auth -runai-model-streamer==0.15.7 +runai-model-streamer==0.17.1 # via -r requirements/test/cuda.in -runai-model-streamer-azure==0.15.7 +runai-model-streamer-azure==0.17.1 # via runai-model-streamer -runai-model-streamer-gcs==0.15.7 +runai-model-streamer-gcs==0.17.1 # via runai-model-streamer -runai-model-streamer-s3==0.15.7 +runai-model-streamer-s3==0.17.1 # via runai-model-streamer s3transfer==0.10.3 # via boto3 @@ -1159,12 +1159,12 @@ tiktoken==0.12.0 # gpt-oss # lm-eval # mistral-common -timm==1.0.17 +timm==1.0.30 # via # -r requirements/test/cuda.in # open-clip-torch # segmentation-models-pytorch -tokenizers==0.23.2 +tokenizers==0.23.3 # via # -r requirements/test/../common.txt # cohere @@ -1227,7 +1227,7 @@ traitlets==5.16.1 # via # jupyter-client # jupyter-core -transformers==5.20.2 +transformers==5.22.0 # via # -r requirements/test/../common.txt # -r requirements/test/cuda.in @@ -1286,7 +1286,6 @@ typing-extensions==4.16.0 # lm-eval # mcp # mcp-types - # mistral-common # mteb # openai # opentelemetry-api diff --git a/requirements/test/cuda.in b/requirements/test/cuda.in index 299fee0864b5..0c129abcad8e 100644 --- a/requirements/test/cuda.in +++ b/requirements/test/cuda.in @@ -27,20 +27,20 @@ sentence-transformers>=5.6.0 # required for embedding tests soundfile # required for audio tests jiwer # required for audio tests tblib # for pickling test exceptions -timm >=1.0.17 # required for internvl and gemma3n-mm test +timm >=1.0.30 # required for internvl and gemma3n-mm test torch==2.14.0 torchaudio==2.11.0 torchvision==0.29.0 transformers_stream_generator # required for qwen-vl test matplotlib # required for qwen-vl test -mistral_common[image,audio] >= 1.11.6 # required for voxtral test +mistral_common[image,audio] >= 1.13.0 # required for voxtral test num2words # required for smolvlm test open_clip_torch==2.32.0 # Required for nemotron_vl test, Nemotron Parse in test_common.py datamodel_code_generator>=0.81.0 # required for minicpm3 test nltk>=3.10.3 # pin transitive rouge-score/lm-eval dep for known GHSAs lm-eval[api]>=0.4.12 # required for model evaluation test mteb[bm25s]>=2, <3 # required for mteb test -transformers==5.20.2 +transformers==5.22.0 schemathesis>=4.0.0 # Required for openai schema test. # quantization buildkite-test-collector==0.1.9 @@ -55,7 +55,7 @@ grpcio-reflection==1.78.0 arctic-inference == 0.1.1; platform_machine == "x86_64" # Required for suffix decoding test numba == 0.65.0 # Required for N-gram speculative decoding -runai-model-streamer[s3,gcs,azure]==0.15.7 +runai-model-streamer[s3,gcs,azure]==0.17.1 fastsafetensors>=0.3.3 instanttensor>=0.2.2; platform_machine == "x86_64" decord==0.6.0; platform_machine == "x86_64" diff --git a/requirements/test/cuda.txt b/requirements/test/cuda.txt index 58706a39d07e..49209ea1adab 100644 --- a/requirements/test/cuda.txt +++ b/requirements/test/cuda.txt @@ -367,7 +367,6 @@ httpx==0.27.2 # cohere # fastapi # fastapi-cloud-cli - # huggingface-hub # model-hosting-container-standards # openai # perceptron @@ -375,8 +374,9 @@ httpx==0.27.2 httpx2==2.12.0 # via # -r requirements/test/cuda.in + # huggingface-hub # mcp -huggingface-hub==1.31.0 +huggingface-hub==2.4.0 # via # -c requirements/common.txt # -r requirements/test/../common.txt @@ -540,7 +540,7 @@ mcp-types==2.3.0 # via mcp mdurl==0.1.2 # via markdown-it-py -mistral-common==1.11.6 +mistral-common==1.13.0 # via # -c requirements/common.txt # -r requirements/test/../common.txt @@ -1109,13 +1109,13 @@ rpds-py==0.20.1 # referencing rsa==4.9.1 # via google-auth -runai-model-streamer==0.15.7 +runai-model-streamer==0.17.1 # via -r requirements/test/cuda.in -runai-model-streamer-azure==0.15.7 +runai-model-streamer-azure==0.17.1 # via runai-model-streamer -runai-model-streamer-gcs==0.15.7 +runai-model-streamer-gcs==0.17.1 # via runai-model-streamer -runai-model-streamer-s3==0.15.7 +runai-model-streamer-s3==0.17.1 # via runai-model-streamer s3transfer==0.10.3 # via boto3 @@ -1263,12 +1263,12 @@ tiktoken==0.12.0 # gpt-oss # lm-eval # mistral-common -timm==1.0.17 +timm==1.0.30 # via # -r requirements/test/cuda.in # open-clip-torch # segmentation-models-pytorch -tokenizers==0.23.2 +tokenizers==0.23.3 # via # -c requirements/common.txt # -r requirements/test/../common.txt @@ -1337,7 +1337,7 @@ traitlets==5.16.1 # via # jupyter-client # jupyter-core -transformers==5.20.2 +transformers==5.22.0 # via # -c requirements/common.txt # -r requirements/test/../common.txt @@ -1400,7 +1400,6 @@ typing-extensions==4.16.0 # lm-eval # mcp # mcp-types - # mistral-common # mteb # openai # opentelemetry-api diff --git a/requirements/test/nightly-torch.txt b/requirements/test/nightly-torch.txt index ed4ad5f011a9..708d8097ae84 100644 --- a/requirements/test/nightly-torch.txt +++ b/requirements/test/nightly-torch.txt @@ -23,13 +23,13 @@ jiwer # required for audio tests timm # required for internvl test transformers_stream_generator # required for qwen-vl test matplotlib # required for qwen-vl test -mistral_common[image,audio] >= 1.11.6 # required for voxtral test +mistral_common[image,audio] >= 1.13.0 # required for voxtral test num2words # required for smolvlm test opencv-python-headless >= 4.13.0 # required for video test datamodel_code_generator>=0.81.0 # required for minicpm3 test lm-eval[api]>=0.4.12 # required for model evaluation test mteb[bm25s]>=2, <3 # required for mteb test -transformers==5.20.2 +transformers==5.22.0 schemathesis>=4.0.0 # Required for openai schema test. # quantization buildkite-test-collector==0.1.9 @@ -40,7 +40,7 @@ tritonclient>=2.51.0 numba == 0.65.0 # Required for N-gram speculative decoding numpy -runai-model-streamer[s3,gcs,azure]==0.15.7 +runai-model-streamer[s3,gcs,azure]==0.17.1 fastsafetensors>=0.3.3 instanttensor>=0.2.2 pydantic>=2.12 # 2.11 leads to error on python 3.13 diff --git a/requirements/test/rocm.in b/requirements/test/rocm.in index 8d090b29b3c5..8e279eba7218 100644 --- a/requirements/test/rocm.in +++ b/requirements/test/rocm.in @@ -26,17 +26,17 @@ sentence-transformers>=5.6.0 # required for embedding tests soundfile # required for audio tests jiwer # required for audio tests tblib # for pickling test exceptions -timm>=1.0.17 # required for internvl and gemma3n-mm test +timm>=1.0.30 # required for internvl and gemma3n-mm test transformers_stream_generator # required for qwen-vl test matplotlib # required for qwen-vl test -mistral_common[image,audio]>=1.11.6 # required for voxtral test +mistral_common[image,audio]>=1.13.0 # required for voxtral test num2words # required for smolvlm test open_clip_torch==2.32.0 # Required for nemotron_vl test, Nemotron Parse in test_common.py datamodel_code_generator>=0.81.0 # required for minicpm3 test nltk>=3.10.3 # pin transitive rouge-score/lm-eval dep for known GHSAs lm-eval[api]>=0.4.12 # required for model evaluation test mteb[bm25s]>=2, <3 # required for mteb test -transformers==5.20.2 +transformers==5.22.0 schemathesis>=4.0.0 # Required for openai schema test # quantization bitsandbytes==0.50.0 @@ -52,7 +52,7 @@ grpcio-reflection==1.78.0 arctic-inference==0.1.1 # Required for suffix decoding test numba==0.65.0 # Required for N-gram speculative decoding -runai-model-streamer[s3,gcs,azure]==0.15.7 +runai-model-streamer[s3,gcs,azure]==0.17.1 fastsafetensors>=0.3.3 instanttensor>=0.1.9 decord==0.6.0 diff --git a/requirements/test/rocm.txt b/requirements/test/rocm.txt index ba87f1c3a336..d9db92d4bb6a 100644 --- a/requirements/test/rocm.txt +++ b/requirements/test/rocm.txt @@ -1,5 +1,5 @@ # This file was autogenerated by uv via the following command: -# uv pip compile requirements/test/rocm.in -c requirements/rocm.txt -o requirements/test/rocm.txt --index-strategy unsafe-best-match --python-platform x86_64-manylinux_2_28 --python-version 3.12 --no-emit-package torch --no-emit-package torchvision --no-emit-package torchaudio --no-emit-package triton --no-emit-package cuda-bindings --no-emit-package cuda-pathfinder --no-emit-package cuda-toolkit --no-emit-package cupy-cuda12x --no-emit-package nvidia-cublas --no-emit-package nvidia-cuda-cupti --no-emit-package nvidia-cuda-nvrtc --no-emit-package nvidia-cuda-runtime --no-emit-package nvidia-cudnn --no-emit-package nvidia-cufft --no-emit-package nvidia-cufile --no-emit-package nvidia-curand --no-emit-package nvidia-cusolver --no-emit-package nvidia-cusparse --no-emit-package nvidia-cusparselt --no-emit-package nvidia-nccl --no-emit-package nvidia-nvjitlink --no-emit-package nvidia-nvshmem --no-emit-package nvidia-nvtx --no-emit-package nvidia-cublas-cu12 --no-emit-package nvidia-cuda-cupti-cu12 --no-emit-package nvidia-cuda-nvrtc-cu12 --no-emit-package nvidia-cuda-runtime-cu12 --no-emit-package nvidia-cudnn-cu12 --no-emit-package nvidia-cufft-cu12 --no-emit-package nvidia-cufile-cu12 --no-emit-package nvidia-curand-cu12 --no-emit-package nvidia-cusolver-cu12 --no-emit-package nvidia-cusparse-cu12 --no-emit-package nvidia-cusparselt-cu12 --no-emit-package nvidia-nccl-cu12 --no-emit-package nvidia-nvjitlink-cu12 --no-emit-package nvidia-nvshmem-cu12 --no-emit-package nvidia-nvtx-cu12 --no-emit-package nvidia-cublas-cu13 --no-emit-package nvidia-cuda-cupti-cu13 --no-emit-package nvidia-cuda-nvrtc-cu13 --no-emit-package nvidia-cuda-runtime-cu13 --no-emit-package nvidia-cudnn-cu13 --no-emit-package nvidia-cufft-cu13 --no-emit-package nvidia-cufile-cu13 --no-emit-package nvidia-curand-cu13 --no-emit-package nvidia-cusolver-cu13 --no-emit-package nvidia-cusparse-cu13 --no-emit-package nvidia-cusparselt-cu13 --no-emit-package nvidia-nccl-cu13 --no-emit-package nvidia-nvjitlink-cu13 --no-emit-package nvidia-nvshmem-cu13 --no-emit-package nvidia-nvtx-cu13 +# uv pip compile requirements/test/rocm.in -c requirements/rocm.txt -o requirements/test/rocm.txt --index-strategy unsafe-best-match --python-platform x86_64-manylinux_2_28 --python-version 3.12 --no-emit-package torch --no-emit-package torchvision --no-emit-package torchaudio --no-emit-package triton --no-emit-package cuda-bindings --no-emit-package cuda-pathfinder --no-emit-package cuda-toolkit --no-emit-package cupy-cuda12x --no-emit-package nvidia-cublas --no-emit-package nvidia-cuda-cccl --no-emit-package nvidia-cuda-cupti --no-emit-package nvidia-cuda-nvrtc --no-emit-package nvidia-cuda-runtime --no-emit-package nvidia-cudnn --no-emit-package nvidia-cufft --no-emit-package nvidia-cufile --no-emit-package nvidia-curand --no-emit-package nvidia-cusolver --no-emit-package nvidia-cusparse --no-emit-package nvidia-cusparselt --no-emit-package nvidia-nccl --no-emit-package nvidia-nvjitlink --no-emit-package nvidia-nvshmem --no-emit-package nvidia-nvtx --no-emit-package nvidia-cublas-cu12 --no-emit-package nvidia-cuda-cupti-cu12 --no-emit-package nvidia-cuda-nvrtc-cu12 --no-emit-package nvidia-cuda-runtime-cu12 --no-emit-package nvidia-cudnn-cu12 --no-emit-package nvidia-cufft-cu12 --no-emit-package nvidia-cufile-cu12 --no-emit-package nvidia-curand-cu12 --no-emit-package nvidia-cusolver-cu12 --no-emit-package nvidia-cusparse-cu12 --no-emit-package nvidia-cusparselt-cu12 --no-emit-package nvidia-nccl-cu12 --no-emit-package nvidia-nvjitlink-cu12 --no-emit-package nvidia-nvshmem-cu12 --no-emit-package nvidia-nvtx-cu12 --no-emit-package nvidia-cublas-cu13 --no-emit-package nvidia-cuda-cupti-cu13 --no-emit-package nvidia-cuda-nvrtc-cu13 --no-emit-package nvidia-cuda-runtime-cu13 --no-emit-package nvidia-cudnn-cu13 --no-emit-package nvidia-cufft-cu13 --no-emit-package nvidia-cufile-cu13 --no-emit-package nvidia-curand-cu13 --no-emit-package nvidia-cusolver-cu13 --no-emit-package nvidia-cusparse-cu13 --no-emit-package nvidia-cusparselt-cu13 --no-emit-package nvidia-nccl-cu13 --no-emit-package nvidia-nvjitlink-cu13 --no-emit-package nvidia-nvshmem-cu13 --no-emit-package nvidia-nvtx-cu13 absl-py==2.4.0 # via rouge-score accelerate==1.13.0 @@ -363,7 +363,6 @@ httpx==0.27.2 # cohere # fastapi # fastapi-cloud-cli - # huggingface-hub # model-hosting-container-standards # openai # perceptron @@ -371,8 +370,9 @@ httpx==0.27.2 httpx2==2.12.0 # via # -r requirements/test/rocm.in + # huggingface-hub # mcp -huggingface-hub==1.31.0 +huggingface-hub==2.4.0 # via # -c requirements/common.txt # -r requirements/test/../common.txt @@ -538,7 +538,7 @@ mcp-types==2.3.0 # via mcp mdurl==0.1.2 # via markdown-it-py -mistral-common==1.11.6 +mistral-common==1.13.0 # via # -c requirements/common.txt # -r requirements/test/../common.txt @@ -1083,15 +1083,15 @@ rpds-py==0.30.0 # via # jsonschema # referencing -runai-model-streamer==0.15.7 +runai-model-streamer==0.17.1 # via # -c requirements/rocm.txt # -r requirements/test/rocm.in -runai-model-streamer-azure==0.15.7 +runai-model-streamer-azure==0.17.1 # via runai-model-streamer -runai-model-streamer-gcs==0.15.7 +runai-model-streamer-gcs==0.17.1 # via runai-model-streamer -runai-model-streamer-s3==0.15.7 +runai-model-streamer-s3==0.17.1 # via runai-model-streamer s3transfer==0.16.0 # via boto3 @@ -1248,13 +1248,13 @@ tilelang==0.1.10 # via # -c requirements/rocm.txt # -r requirements/test/rocm.in -timm==1.0.17 +timm==1.0.30 # via # -c requirements/rocm.txt # -r requirements/test/rocm.in # open-clip-torch # segmentation-models-pytorch -tokenizers==0.23.2 +tokenizers==0.23.3 # via # -c requirements/common.txt # -r requirements/test/../common.txt @@ -1287,7 +1287,7 @@ traitlets==5.16.1 # via # jupyter-client # jupyter-core -transformers==5.20.2 +transformers==5.22.0 # via # -c requirements/common.txt # -r requirements/test/../common.txt @@ -1348,7 +1348,6 @@ typing-extensions==4.16.0 # lm-eval # mcp # mcp-types - # mistral-common # mteb # openai # opentelemetry-api @@ -1446,12 +1445,12 @@ zipp==3.23.0 # cuda-pathfinder # cuda-toolkit # nvidia-cublas +# nvidia-cuda-cccl # nvidia-cuda-cupti # nvidia-cuda-nvrtc # nvidia-cuda-runtime # nvidia-cufft # nvidia-cufile -# nvidia-curand # nvidia-cusolver # nvidia-cusparse # nvidia-nvjitlink diff --git a/requirements/test/xpu.in b/requirements/test/xpu.in index 333cfb132d0b..f16f1a5de031 100644 --- a/requirements/test/xpu.in +++ b/requirements/test/xpu.in @@ -19,9 +19,9 @@ arctic-inference lm_eval[api]>=0.4.12 nltk>=3.10.3 # pin transitive rouge-score/lm-eval dep for known GHSAs modelscope<1.38 -runai-model-streamer[s3,gcs,azure]>=0.15.7 +runai-model-streamer[s3,gcs,azure]>=0.17.1 tensorizer==2.10.1 -transformers==5.20.2 +transformers==5.22.0 # --- Audio Processing --- av @@ -46,12 +46,12 @@ num2words pqdm # --- Vision & Multimodal --- -timm>=1.0.17 # required for internvl and gemma3n-mm test +timm>=1.0.30 # required for internvl and gemma3n-mm test albumentations ftfy matplotlib decord==0.6.0; platform_machine == "x86_64" -mistral-common[image,audio]>=1.11.6 # required for voxtral test +mistral-common[image,audio]>=1.13.0 # required for voxtral test open_clip_torch==2.32.0 # Required for nemotron_vl test, Nemotron Parse in test_common.py diff --git a/requirements/test/xpu.txt b/requirements/test/xpu.txt index 3d026574702b..39c9ab930002 100644 --- a/requirements/test/xpu.txt +++ b/requirements/test/xpu.txt @@ -147,7 +147,7 @@ dataproperty==1.1.1 # via # pytablewriter # tabledata -datasets==5.0.1 +datasets==5.1.0 # via # evaluate # lm-eval @@ -302,17 +302,16 @@ httpx==0.28.1 # via # anthropic # cohere - # datasets # fastapi # fastapi-cloud-cli - # huggingface-hub # model-hosting-container-standards httpx2==2.13.1 # via # -r requirements/test/xpu.in + # huggingface-hub # mcp # openai -huggingface-hub==1.31.0 +huggingface-hub==2.4.0 # via # -c requirements/common.txt # -r requirements/test/../common.txt @@ -489,7 +488,7 @@ mcp-types==2.3.0 # via mcp mdurl==0.1.2 # via markdown-it-py -mistral-common==1.11.7 +mistral-common==1.13.0 # via # -c requirements/common.txt # -r requirements/test/../common.txt @@ -930,7 +929,6 @@ requests==2.34.2 # -r requirements/test/../common.txt # azure-core # cohere - # datasets # docker # evaluate # google-api-core @@ -964,13 +962,13 @@ rpds-py==2026.6.3 # via # jsonschema # referencing -runai-model-streamer==0.16.1 +runai-model-streamer==0.17.1 # via -r requirements/test/xpu.in -runai-model-streamer-azure==0.16.1 +runai-model-streamer-azure==0.17.1 # via runai-model-streamer -runai-model-streamer-gcs==0.16.1 +runai-model-streamer-gcs==0.17.1 # via runai-model-streamer -runai-model-streamer-s3==0.16.1 +runai-model-streamer-s3==0.17.1 # via runai-model-streamer s3transfer==0.19.2 # via boto3 @@ -1105,11 +1103,11 @@ tiktoken==0.14.0 # gpt-oss # lm-eval # mistral-common -timm==1.0.28 +timm==1.0.30 # via # -r requirements/test/xpu.in # open-clip-torch -tokenizers==0.23.2 +tokenizers==0.23.3 # via # -c requirements/common.txt # -r requirements/test/../common.txt @@ -1156,7 +1154,7 @@ traitlets==5.16.1 # via # jupyter-client # jupyter-core -transformers==5.20.2 +transformers==5.22.0 # via # -c requirements/common.txt # -r requirements/test/../common.txt @@ -1212,7 +1210,6 @@ typing-extensions==4.16.0 # lm-eval # mcp # mcp-types - # mistral-common # mteb # openai # opentelemetry-api diff --git a/requirements/xpu.txt b/requirements/xpu.txt index 14372071f0b5..14a9cdcc4d1d 100644 --- a/requirements/xpu.txt +++ b/requirements/xpu.txt @@ -20,4 +20,5 @@ torchvision torchcodec >= 0.14 # Required for the torchcodec video decoding backend auto_round_lib==0.16.0 -vllm_xpu_kernels==0.1.15.4 +--extra-index-url=https://vllm-project.github.io/vllm-xpu-kernels/whl +vllm_xpu_kernels==0.1.16 diff --git a/rust/Cargo.lock b/rust/Cargo.lock index a722d362cb52..da33ca59188a 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -5726,6 +5726,7 @@ dependencies = [ "serde_repr", "serde_tuple", "serde_with", + "sha2", "socket2", "task-local", "tempfile", diff --git a/rust/README.md b/rust/README.md index 42d6c3e16a05..6f94d24d859d 100644 --- a/rust/README.md +++ b/rust/README.md @@ -64,6 +64,12 @@ SMG `VllmEngine` gRPC server in place of HTTP. The Rust listener exposes the `vllm.Inference` and `vllm.Control` services alongside HTTP. These are different gRPC APIs; `--grpc` and `--grpc-port` are mutually exclusive. +### gRPC health checks + +The gRPC listener also serves `grpc.health.v1.Health`. Kubernetes probes should +use the `liveness` service for liveness and `readiness` for readiness; see +[Rust frontend gRPC health checks](../docs/deployment/k8s.md#rust-frontend-grpc-health-checks). + ### RL weight synchronization With `VLLM_SERVER_DEV_MODE=1`, the Rust frontend supports the HTTP weight-transfer @@ -107,6 +113,23 @@ To build the `vllm-rs` in isolation: ./tools/build_rust.sh ``` +### Hybrid and External DP Load Balancing + +Like `vllm serve` (see [Data Parallel Deployment](../docs/serving/data_parallel_deployment.md)), `vllm-rs serve` can run one Rust frontend per node that serves only the node's data parallel ranks, with an upstream load balancer spreading requests across nodes. Every node passes the global `--data-parallel-size`, the address of the node that owns rank 0 as `--data-parallel-address`, and the same explicit `--data-parallel-rpc-port`, because all engines join the handshake of the rank-0 frontend. For MoE models, the rank-0 frontend also runs the DP coordinator. + +```bash +# Node 0: ranks 0-1 +vllm-rs serve \ + --data-parallel-size 4 --data-parallel-size-local 2 --data-parallel-hybrid-lb \ + --data-parallel-address 10.0.0.1 --data-parallel-rpc-port 13345 +# Node 1: ranks 2-3 +vllm-rs serve \ + --data-parallel-size 4 --data-parallel-size-local 2 --data-parallel-start-rank 2 \ + --data-parallel-address 10.0.0.1 --data-parallel-rpc-port 13345 +``` + +For external load balancing, start one `vllm-rs serve` per rank with `--data-parallel-rank `. A request can be pinned to one of a frontend's ranks with the `X-data-parallel-rank` header, which takes the global rank. Unlike `vllm serve`, `--data-parallel-rpc-port` has no default here and ranks are not inferred from `--nnodes`/`--node-rank`. + ### Engine-free renderer `vllm-rs render` serves text-only request preprocessing without starting or diff --git a/rust/src/bench/src/backends/mod.rs b/rust/src/bench/src/backends/mod.rs index b5400c5d0f49..da23eb46a0ab 100644 --- a/rust/src/bench/src/backends/mod.rs +++ b/rust/src/bench/src/backends/mod.rs @@ -52,6 +52,21 @@ pub struct ChatDelta { #[derive(Deserialize)] pub struct ChunkUsage { pub completion_tokens: Option, + pub prompt_tokens: Option, +} + +impl ChunkUsage { + /// Record the server-reported token counts on `output`. A zero + /// `prompt_tokens` keeps the client estimate: some OpenAI-compatible + /// servers send placeholder zeros in usage chunks. + pub fn apply(&self, output: &mut RequestFuncOutput) { + if let Some(ct) = self.completion_tokens { + output.output_tokens = ct as usize; + } + if let Some(pt) = self.prompt_tokens.filter(|&pt| pt > 0) { + output.prompt_len = pt as usize; + } + } } use crate::cli::BackendKind; @@ -246,6 +261,21 @@ pub fn build_headers( mod tests { use super::*; + /// A placeholder zero `prompt_tokens` must not replace the client estimate. + #[test] + fn chunk_usage_ignores_zero_prompt_tokens() { + let mut output = RequestFuncOutput { + prompt_len: 100, + ..Default::default() + }; + ChunkUsage { + completion_tokens: Some(3), + prompt_tokens: Some(0), + } + .apply(&mut output); + assert_eq!((output.prompt_len, output.output_tokens), (100, 3)); + } + #[test] fn test_update_server_metrics_converts_ms_to_seconds() { let chunk: CompletionChunk = serde_json::from_value(serde_json::json!({ diff --git a/rust/src/bench/src/backends/openai_chat.rs b/rust/src/bench/src/backends/openai_chat.rs index 30c6093296d0..36ce961abf81 100644 --- a/rust/src/bench/src/backends/openai_chat.rs +++ b/rust/src/bench/src/backends/openai_chat.rs @@ -130,10 +130,8 @@ impl OpenAIChatBackend { } // Separate `if` (not `else if`) — Dynamo may send // both choices and usage in the same chunk. - if let Some(ref usage) = data.usage - && let Some(ct) = usage.completion_tokens - { - output.output_tokens = ct as usize; + if let Some(usage) = &data.usage { + usage.apply(&mut output); } } } @@ -363,7 +361,11 @@ mod tests { /// Serve one streaming chat response over a raw socket: `num_tokens` /// content chunks back-to-back, then `usage_delay` of silence, then the /// choice-less usage chunk and `[DONE]`. Returns the endpoint URL. - fn spawn_sse_server(num_tokens: usize, usage_delay: std::time::Duration) -> String { + fn spawn_sse_server( + num_tokens: usize, + usage_delay: std::time::Duration, + prompt_tokens: Option, + ) -> String { use std::io::{BufRead, BufReader, Read, Write}; let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap(); @@ -393,8 +395,10 @@ mod tests { send("data: {\"choices\":[{\"delta\":{\"content\":\"a\"}}]}\n\n"); } std::thread::sleep(usage_delay); + let prompt_tokens = + prompt_tokens.map_or(String::new(), |pt| format!(",\"prompt_tokens\":{pt}")); send(&format!( - "data: {{\"choices\":[],\"usage\":{{\"completion_tokens\":{num_tokens}}}}}\n\n" + "data: {{\"choices\":[],\"usage\":{{\"completion_tokens\":{num_tokens}{prompt_tokens}}}}}\n\n" )); send("data: [DONE]\n\n"); }); @@ -411,7 +415,7 @@ mod tests { // real delay is needed here; there is no observable event to wait on. let usage_delay = std::time::Duration::from_millis(150); let input = RequestFuncInput { - api_url: spawn_sse_server(3, usage_delay), + api_url: spawn_sse_server(3, usage_delay, None), model: "test-model".to_string(), output_len: 3, ..Default::default() @@ -442,4 +446,42 @@ mod tests { // keys already set above must not be overridden by extra_body assert_eq!(v["stream"], true); } + + /// The server's `usage.prompt_tokens` replaces the client-side estimate, + /// so input-token totals count what the server actually processed + /// (chat template, multimodal tokens). Mirrors Python #38654. + #[tokio::test] + async fn test_usage_prompt_tokens_override_prompt_len() { + let input = RequestFuncInput { + api_url: spawn_sse_server(3, std::time::Duration::ZERO, Some(128)), + model: "test-model".to_string(), + prompt_len: 100, + output_len: 3, + ..Default::default() + }; + + let output = OpenAIChatBackend.send_request(&input, &reqwest::Client::new()).await.unwrap(); + + assert!(output.success, "{}", output.error); + assert_eq!(output.prompt_len, 128); + } + + /// Servers that omit `usage.prompt_tokens` keep the client-side estimate, + /// and `completion_tokens` is still recorded. + #[tokio::test] + async fn test_missing_prompt_tokens_keeps_client_prompt_len() { + let input = RequestFuncInput { + api_url: spawn_sse_server(3, std::time::Duration::ZERO, None), + model: "test-model".to_string(), + prompt_len: 100, + output_len: 3, + ..Default::default() + }; + + let output = OpenAIChatBackend.send_request(&input, &reqwest::Client::new()).await.unwrap(); + + assert!(output.success, "{}", output.error); + assert_eq!(output.prompt_len, 100); + assert_eq!(output.output_tokens, 3); + } } diff --git a/rust/src/bench/src/backends/openai_completions.rs b/rust/src/bench/src/backends/openai_completions.rs index 0b62d342bcde..049988191318 100644 --- a/rust/src/bench/src/backends/openai_completions.rs +++ b/rust/src/bench/src/backends/openai_completions.rs @@ -156,10 +156,8 @@ impl OpenAICompletionsBackend { } // Separate `if` (not `else if`) — Dynamo may send // both choices and usage in the same chunk. - if let Some(ref usage) = data.usage - && let Some(ct) = usage.completion_tokens - { - output.output_tokens = ct as usize; + if let Some(usage) = &data.usage { + usage.apply(&mut output); } } } @@ -201,3 +199,69 @@ impl OpenAICompletionsBackend { Ok(output) } } + +#[cfg(test)] +mod tests { + use super::*; + + /// Serve `body` as one streaming completions response over a raw socket. + /// Returns the endpoint URL. + fn spawn_sse_server(body: &'static str) -> String { + use std::io::{BufRead, BufReader, Read, Write}; + + let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap(); + let addr = listener.local_addr().unwrap(); + std::thread::spawn(move || { + let (mut stream, _) = listener.accept().unwrap(); + let mut reader = BufReader::new(stream.try_clone().unwrap()); + let mut content_length = 0; + loop { + let mut line = String::new(); + reader.read_line(&mut line).unwrap(); + if let Some(v) = line.to_ascii_lowercase().strip_prefix("content-length:") { + content_length = v.trim().parse().unwrap(); + } + if line == "\r\n" { + break; + } + } + reader.read_exact(&mut vec![0; content_length]).unwrap(); + stream + .write_all( + format!( + "HTTP/1.1 200 OK\r\ncontent-type: text/event-stream\r\n\ + connection: close\r\n\r\n{body}" + ) + .as_bytes(), + ) + .unwrap(); + }); + format!("http://{addr}/v1/completions") + } + + /// The server's `usage.prompt_tokens` replaces the client-side estimate, + /// as in the chat backend. + #[tokio::test] + async fn test_usage_prompt_tokens_override_prompt_len() { + let input = RequestFuncInput { + api_url: spawn_sse_server(concat!( + "data: {\"choices\":[{\"text\":\"a\"}]}\n\n", + "data: {\"choices\":[{\"text\":\"b\"}]}\n\n", + "data: {\"choices\":[],\"usage\":{\"completion_tokens\":2,\"prompt_tokens\":128}}\n\n", + "data: [DONE]\n\n", + )), + model: "test-model".to_string(), + prompt_len: 100, + output_len: 2, + ..Default::default() + }; + + let output = OpenAICompletionsBackend + .send_request(&input, &reqwest::Client::new()) + .await + .unwrap(); + + assert!(output.success, "{}", output.error); + assert_eq!((output.prompt_len, output.output_tokens), (128, 2)); + } +} diff --git a/rust/src/bench/src/backends/openai_responses.rs b/rust/src/bench/src/backends/openai_responses.rs index 2a9982636137..34d519e3ef95 100644 --- a/rust/src/bench/src/backends/openai_responses.rs +++ b/rust/src/bench/src/backends/openai_responses.rs @@ -27,6 +27,7 @@ struct ResponsesEventResponse { #[derive(serde::Deserialize)] struct ResponsesUsage { + input_tokens: Option, output_tokens: Option, } @@ -156,9 +157,17 @@ impl ResponsesStream { }; match event.event_type.as_deref() { - // Reasoning deltas count toward TTFT and ITL because the server is - // already decoding. Only output text is collected. - Some(kind @ ("response.output_text.delta" | "response.reasoning_text.delta")) => { + // Reasoning and tool call deltas count toward TTFT and ITL because + // the server is already decoding; a response that only calls a tool + // emits nothing else. Only output text is collected. + Some( + kind @ ("response.output_text.delta" + | "response.reasoning_text.delta" + | "response.function_call_arguments.delta" + | "response.custom_tool_call_input.delta" + | "response.mcp_call_arguments.delta" + | "response.code_interpreter_call_code.delta"), + ) => { if self.first_token_received { self.output .itl @@ -180,8 +189,14 @@ impl ResponsesStream { // with `status: "incomplete"`; the spec also has `response.incomplete`. Some("response.completed" | "response.incomplete") => { self.saw_terminal_event = true; - if let Some(n) = event.response.and_then(|r| r.usage?.output_tokens) { - self.output.output_tokens = n as usize; + if let Some(usage) = event.response.and_then(|r| r.usage) { + if let Some(n) = usage.output_tokens { + self.output.output_tokens = n as usize; + } + // Same rule as `ChunkUsage::apply`: a zero keeps the client estimate. + if let Some(n) = usage.input_tokens.filter(|&n| n > 0) { + self.output.prompt_len = n as usize; + } } } kind if matches!(kind, Some("error" | "response.failed")) || event.error.is_some() => { @@ -253,6 +268,21 @@ mod tests { stream.finish() } + /// The terminal event's `usage.input_tokens` replaces the client estimate, + /// as in Python's `async_request_openai_responses`. + #[test] + fn test_terminal_usage_input_tokens_override_prompt_len() { + let output = run(&[ + text_delta("Hi"), + event( + "response.completed", + ",\"response\":{\"usage\":{\"input_tokens\":128,\"output_tokens\":1}}", + ), + ]); + assert!(output.success, "{}", output.error); + assert_eq!((output.prompt_len, output.output_tokens), (128, 1)); + } + #[test] fn test_reasoning_starts_ttft_and_latency_ends_at_last_token() { let output = run(&[ @@ -271,6 +301,48 @@ mod tests { assert_eq!(output.output_tokens, 3); } + /// A forced tool call streams only argument deltas. They are generated + /// tokens, as `tool_calls` deltas are on `openai-chat`. + #[test] + fn test_tool_call_only_response_is_timed_and_counted() { + let args = |delta: &str| { + event( + "response.function_call_arguments.delta", + &format!(",\"delta\":\"{delta}\""), + ) + }; + let output = run(&[ + event("response.output_item.added", ""), + args("{\\\"city\\\": "), + args("\\\"Paris\\\"}"), + event("response.function_call_arguments.done", ""), + completed(6), + ]); + + assert!(output.success, "{}", output.error); + assert_eq!(output.ttft, 0.02); + assert_eq!(output.itl, vec![0.01]); + assert_eq!(output.latency, 0.03); + // Tool arguments are not output text. + assert_eq!(output.generated_text, ""); + assert_eq!(output.output_tokens, 6); + } + + #[test] + fn test_every_tool_delta_event_starts_the_clock() { + for kind in [ + "response.function_call_arguments.delta", + "response.custom_tool_call_input.delta", + "response.mcp_call_arguments.delta", + "response.code_interpreter_call_code.delta", + ] { + let delta = event(kind, ",\"delta\":\"x\""); + let output = run(&[delta.clone(), delta, completed(2)]); + assert!(output.success, "{kind}: {}", output.error); + assert_eq!(output.itl.len(), 1, "{kind}"); + } + } + #[test] fn test_event_line_split_from_data_line() { let delta = text_delta("Hi"); diff --git a/rust/src/bench/src/benchmark.rs b/rust/src/bench/src/benchmark.rs index 5833a3bdcea5..1f6b0567ac2f 100644 --- a/rust/src/bench/src/benchmark.rs +++ b/rust/src/bench/src/benchmark.rs @@ -645,7 +645,7 @@ pub async fn run_benchmark(config: &BenchConfig) -> Result { let total_output_tokens: usize = input_requests.iter().map(|r| r.expected_output_len).sum(); println!("Dry run stats:"); println!(" Total prompts: {}", input_requests.len()); - println!(" Total input tokens: {total_input_tokens}"); + println!(" Estimated total input tokens: {total_input_tokens}"); println!(" Total expected output tokens: {total_output_tokens}"); println!( " Avg input tokens: {:.1}", @@ -1118,7 +1118,6 @@ pub async fn run_benchmark(config: &BenchConfig) -> Result { (m, Vec::new()) } else { calculate_metrics( - &input_requests, &outputs, benchmark_duration, &config.selected_percentiles, @@ -1148,7 +1147,6 @@ pub async fn run_benchmark(config: &BenchConfig) -> Result { ) { let ss = steady_state::compute( &outputs, - &input_requests, &window, &config.selected_percentiles, config.backend.is_pooling(), @@ -1392,7 +1390,7 @@ pub(crate) async fn send_profile_request( Ok(resp.status().is_success()) } -/// Parse `vllm:num_requests_running` from the server's `/metrics` Prometheus endpoint. +/// Fetch total `vllm:num_requests_running` across engines from `/metrics`. pub(crate) async fn fetch_num_requests_running( client: &reqwest::Client, base_url: &str, @@ -1400,18 +1398,35 @@ pub(crate) async fn fetch_num_requests_running( let url = format!("{base_url}/metrics"); let resp = client.get(&url).send().await.ok()?; let text = resp.text().await.ok()?; + + parse_num_requests_running(&text) +} + +/// Parse total running requests across the engine series in `/metrics`. +fn parse_num_requests_running(text: &str) -> Option { + let mut total: Option = None; for line in text.lines() { - if let Some(rest) = line.strip_prefix("vllm:num_requests_running") { - // Line format: `vllm:num_requests_running{model_name="..."} 42` - // The value is after the last space. - if let Some(val_str) = rest.rsplit(' ').next() - && let Ok(v) = val_str.parse::() - { - return Some(v as usize); - } + let Some(rest) = line.trim().strip_prefix("vllm:num_requests_running") else { + continue; + }; + let sample = if rest.trim_start().starts_with('{') { + let Some((_, sample)) = rest.rsplit_once('}') else { + continue; + }; + sample + } else if rest.starts_with(char::is_whitespace) { + rest + } else { + continue; + }; + if let Some(value) = sample.split_whitespace().next().and_then(|s| s.parse::().ok()) + && value.is_finite() + && value >= 0.0 + { + total = Some(total.unwrap_or(0).saturating_add(value as usize)); } } - None + total } /// Poll `/metrics` until `num_requests_running >= threshold`, then start the @@ -1923,6 +1938,44 @@ async fn sample_verify_prompts( mod tests { use super::*; + #[test] + fn test_parse_num_requests_running_sums_dp_engines() { + let metrics: String = (0..8) + .map(|engine| { + format!("vllm:num_requests_running{{model_name=\"m\",engine=\"{engine}\"}} 16.0\n") + }) + .collect(); + + assert_eq!(parse_num_requests_running(&metrics), Some(128)); + } + + #[test] + fn test_parse_num_requests_running_matches_exact_metric_name() { + let metrics = "\ +vllm:num_requests_running_total{engine=\"0\"} 1024 +vllm:num_requests_running{engine=\"0\"} 16 +vllm:num_requests_waiting{model_name=\"vllm:num_requests_running\"} 256 +vllm:num_requests_running{engine=\"1\"} 16 +"; + + assert_eq!(parse_num_requests_running(metrics), Some(32)); + } + + #[test] + fn test_parse_num_requests_running_distinguishes_zero_and_missing() { + for (metrics, expected) in [("vllm:num_requests_running 0\n", Some(0)), ("", None)] { + assert_eq!(parse_num_requests_running(metrics), expected); + } + } + + #[test] + fn test_parse_num_requests_running_reads_sample_before_timestamp() { + let metrics = + "vllm:num_requests_running {model_name=\"model with spaces\"}\t4.2e1 123456\n"; + + assert_eq!(parse_num_requests_running(metrics), Some(42)); + } + fn make_metrics( drafts: u64, draft_tokens: u64, diff --git a/rust/src/bench/src/cli.rs b/rust/src/bench/src/cli.rs index 850fcb35ddfb..693f7f6585ba 100644 --- a/rust/src/bench/src/cli.rs +++ b/rust/src/bench/src/cli.rs @@ -406,10 +406,11 @@ pub struct BenchServeArgs { #[arg(long, default_value_t = false)] pub profile: bool, - /// Minimum server batch size (num_requests_running) before starting the - /// profiler. When set, profiling is deferred until the /metrics endpoint - /// reports at least this many running requests, then captures for - /// --profile-duration seconds. Requires --profile. + /// Minimum total running requests across engines reported by /metrics + /// before starting the profiler. When set, profiling is deferred until + /// this threshold is reached, then captures for --profile-duration seconds. + /// Only engines exposed by the /metrics endpoint at --base-url are counted. + /// Requires --profile. #[arg(long)] pub profile_batch_threshold: Option, diff --git a/rust/src/bench/src/config.rs b/rust/src/bench/src/config.rs index 3e180cfb3085..ed8f8f7d36cd 100644 --- a/rust/src/bench/src/config.rs +++ b/rust/src/bench/src/config.rs @@ -246,6 +246,17 @@ impl BenchConfig { )); } + if args.max_concurrency == Some(0) { + return Err(BenchError::Config( + "--max-concurrency must be greater than 0".into(), + )); + } + if args.multi_turn_concurrency == Some(0) { + return Err(BenchError::Config( + "--multi-turn-concurrency must be greater than 0".into(), + )); + } + let base_url = args.resolve_base_url(); let api_url = args.resolve_api_url(); @@ -1398,6 +1409,60 @@ mod tests { assert!(BenchConfig::from_args(&args).is_err()); } + + #[test] + fn test_zero_max_concurrency_fails() { + let args = parse_args(vec![ + "vllm-bench", + "--model", + "test-model", + "--max-concurrency", + "0", + ]); + let err = BenchConfig::from_args(&args).expect_err("0 concurrency must be rejected"); + assert!( + err.to_string().contains("--max-concurrency must be greater than 0"), + "{}", + err + ); + } + + #[test] + fn test_positive_max_concurrency_passes() { + for value in ["1", "16"] { + let args = parse_args(vec![ + "vllm-bench", + "--model", + "test-model", + "--max-concurrency", + value, + ]); + assert!( + BenchConfig::from_args(&args).is_ok(), + "value {value} should pass" + ); + } + } + + #[test] + fn test_zero_multi_turn_concurrency_fails() { + let args = parse_args(vec![ + "vllm-bench", + "--model", + "test-model", + "--multi-turn", + "--multi-turn-concurrency", + "0", + ]); + let err = + BenchConfig::from_args(&args).expect_err("0 multi-turn concurrency must be rejected"); + assert!( + err.to_string().contains("--multi-turn-concurrency must be greater than 0"), + "{}", + err + ); + } + #[test] fn test_range_ratio_parse_float() { let rr = RangeRatio::parse("0.2").unwrap(); diff --git a/rust/src/bench/src/metrics/calculator.rs b/rust/src/bench/src/metrics/calculator.rs index 7e3fc77565ce..b55ed1cbde6d 100644 --- a/rust/src/bench/src/metrics/calculator.rs +++ b/rust/src/bench/src/metrics/calculator.rs @@ -3,7 +3,6 @@ use crate::backends::RequestFuncOutput; use crate::config::GoodputConfig; -use crate::datasets::SampleRequest; use crate::metrics::{BenchmarkMetrics, MultiTurnMetrics}; use crate::multi_turn::ConversationOutput; @@ -27,7 +26,6 @@ fn log_failed_requests(outputs: &[RequestFuncOutput]) { /// /// Mirrors Python's `calculate_metrics()` from serve.py:392-599. pub fn calculate_metrics( - input_requests: &[SampleRequest], outputs: &[RequestFuncOutput], dur_s: f64, selected_percentiles: &[f64], @@ -44,7 +42,7 @@ pub fn calculate_metrics( let mut ttfts: Vec = Vec::new(); let mut e2els: Vec = Vec::new(); - for (i, output) in outputs.iter().enumerate() { + for output in outputs.iter() { if output.success { let output_len = if output.output_tokens > 0 { output.output_tokens @@ -57,7 +55,7 @@ pub fn calculate_metrics( }; actual_output_lens.push(output_len); - total_input += input_requests[i].prompt_len; + total_input += output.prompt_len; if output_len > 1 { let latency_minus_ttft = output.latency - output.ttft; @@ -283,7 +281,6 @@ fn percentiles_from_sorted(sorted: &[f64], percentiles: &[f64]) -> Vec<(f64, f64 /// /// Mirrors Python's `calculate_metrics_for_embeddings()` from serve.py. /// Key differences from `calculate_metrics()`: -/// - Uses `output.prompt_len` (server-reported) instead of `input_requests[i].prompt_len` /// - Only computes E2EL (no TTFT/TPOT/ITL since pooling is non-streaming) /// - `total_output` is 0 (pooling produces no output tokens) /// - `total_token_throughput` = `total_input / dur_s` (input tokens only) @@ -380,7 +377,7 @@ pub fn calculate_embedding_metrics( /// Calculate multi-turn benchmark metrics with per-turn breakdown. /// -/// 1. Flatten all turns into SampleRequest/RequestFuncOutput pairs → overall metrics +/// 1. Flatten all turns' outputs → overall metrics /// 2. Group by turn_index → per-turn metrics /// 3. Conversation-level stats pub fn calculate_multi_turn_metrics( @@ -391,24 +388,15 @@ pub fn calculate_multi_turn_metrics( max_turn_count: usize, ) -> MultiTurnMetrics { // Flatten all turns for overall metrics - let mut all_requests: Vec = Vec::new(); let mut all_outputs: Vec = Vec::new(); for conv in conversation_outputs { for turn in &conv.turns { - all_requests.push(SampleRequest { - prompt: std::sync::Arc::from(""), - prompt_len: turn.request_output.prompt_len, - expected_output_len: turn.request_output.output_tokens, - request_id: None, - ..Default::default() - }); all_outputs.push(turn.request_output.clone()); } } let (overall, _) = calculate_metrics( - &all_requests, &all_outputs, dur_s, selected_percentiles, @@ -419,25 +407,16 @@ pub fn calculate_multi_turn_metrics( // Per-turn breakdown let mut per_turn: Vec = Vec::new(); for turn_idx in 0..max_turn_count { - let mut turn_requests: Vec = Vec::new(); let mut turn_outputs: Vec = Vec::new(); for conv in conversation_outputs { if let Some(turn) = conv.turns.get(turn_idx) { - turn_requests.push(SampleRequest { - prompt: std::sync::Arc::from(""), - prompt_len: turn.request_output.prompt_len, - expected_output_len: turn.request_output.output_tokens, - request_id: None, - ..Default::default() - }); turn_outputs.push(turn.request_output.clone()); } } if !turn_outputs.is_empty() { let (turn_metrics, _) = calculate_metrics( - &turn_requests, &turn_outputs, dur_s, selected_percentiles, @@ -509,6 +488,26 @@ mod tests { assert!((p99 - 98.01).abs() < 0.1); } + /// Input-token totals come from each output's `prompt_len` (server-reported + /// via `usage.prompt_tokens`), not the client-side request estimate. + /// Mirrors Python's `serve.py`. + #[test] + fn test_total_input_uses_output_prompt_len() { + let outputs = vec![ + RequestFuncOutput { + success: true, + prompt_len: 128, + output_tokens: 3, + ..Default::default() + }; + 2 + ]; + + let (metrics, _) = calculate_metrics(&outputs, 1.0, &[], false, &GoodputConfig::default()); + + assert_eq!(metrics.total_input, 256); + } + #[test] fn test_multi_turn_metrics_use_request_prompt_lengths() { let turn = |turn_index, prompt_len, output_tokens| crate::multi_turn::TurnOutput { diff --git a/rust/src/bench/src/metrics/steady_state.rs b/rust/src/bench/src/metrics/steady_state.rs index a403275b7ac6..f108bb7c8501 100644 --- a/rust/src/bench/src/metrics/steady_state.rs +++ b/rust/src/bench/src/metrics/steady_state.rs @@ -170,16 +170,12 @@ pub fn detect_window( }) } -use crate::datasets::SampleRequest; - -/// Compute steady-state metrics from per-request outputs, the detected window, -/// and the original request list (for input-token attribution). +/// Compute steady-state metrics from per-request outputs and the detected window. /// /// `is_pooling` gates off TTFT because pooling backends write `ttft = latency` /// as a placeholder; reporting TTFT over that would silently be E2EL. pub fn compute( outputs: &[RequestFuncOutput], - input_requests: &[SampleRequest], window: &SteadyStateWindow, percentiles: &[f64], is_pooling: bool, @@ -200,7 +196,7 @@ pub fn compute( let mut ttfts_in_window: Vec = Vec::new(); let mut tpots_in_window: Vec = Vec::new(); - for (idx, o) in outputs.iter().enumerate() { + for o in outputs.iter() { if !o.success { continue; } @@ -213,7 +209,7 @@ pub fn compute( } if started_in { - input_tokens_in_window += input_requests[idx].prompt_len; + input_tokens_in_window += o.prompt_len; if !is_pooling { ttfts_in_window.push(o.ttft); // Per-request TPOT: (latency - ttft) / (output_tokens - 1). @@ -417,10 +413,6 @@ mod tests { assert!((w.end_s - 10.0).abs() < 1e-9); } - use std::sync::Arc; - - use crate::datasets::SampleRequest; - fn mk_full( start: f64, latency: f64, @@ -440,16 +432,6 @@ mod tests { } } - fn mk_sample(prompt_len: usize) -> SampleRequest { - SampleRequest { - prompt: Arc::from(""), - prompt_len, - expected_output_len: 0, - request_id: None, - ..Default::default() - } - } - #[test] fn compute_constant_concurrency_matches_full_run() { // 3 identical requests, each 10s, each emitting 5 tokens uniformly via itl=2s. @@ -459,13 +441,10 @@ mod tests { mk_full(0.0, 10.0, 2.0, itl.clone(), 5), mk_full(0.0, 10.0, 2.0, itl.clone(), 5), ]; - let requests: Vec = - outputs.iter().map(|o| mk_sample(o.prompt_len)).collect(); let w = detect_window(&outputs, Some(3), 1.0, 0.1, 10.0).unwrap(); let m = compute( &outputs, - &requests, &w, &[99.0], false, // is_pooling @@ -502,12 +481,10 @@ mod tests { let late_itl = vec![0.02; 999]; // 1000 tokens; emission starts after end_s let late = mk_full(9.9, 30.0, 0.05, late_itl, 1000); outputs.push(late); - let requests: Vec = - outputs.iter().map(|o| mk_sample(o.prompt_len)).collect(); // Target = 2 so threshold_abs = 2; window opens when both are in flight. let w = detect_window(&outputs, Some(2), 1.0, 0.1, 40.0).unwrap(); - let m = compute(&outputs, &requests, &w, &[99.0], false); + let m = compute(&outputs, &w, &[99.0], false); // Late request started in window but most of its tokens are emitted AFTER end_s. // Only a handful (≤3) should count. @@ -534,10 +511,8 @@ mod tests { mk_full(0.0, 5.0, 5.0, Vec::new(), 0), mk_full(0.0, 5.0, 5.0, Vec::new(), 0), ]; - let requests: Vec = - outputs.iter().map(|o| mk_sample(o.prompt_len)).collect(); let w = detect_window(&outputs, Some(2), 1.0, 0.1, 10.0).unwrap(); - let m = compute(&outputs, &requests, &w, &[99.0], /* is_pooling */ true); + let m = compute(&outputs, &w, &[99.0], /* is_pooling */ true); assert_eq!(m.mean_ttft_ms, 0.0); assert_eq!(m.median_ttft_ms, 0.0); assert!(m.percentiles_ttft_ms.is_empty()); @@ -560,10 +535,8 @@ mod tests { mk_full(0.0, 10.0, 2.0, itl.clone(), 5), mk_full(0.0, 10.0, 2.0, itl.clone(), 5), ]; - let requests: Vec = - outputs.iter().map(|o| mk_sample(o.prompt_len)).collect(); let w = detect_window(&outputs, Some(3), 1.0, 0.1, 10.0).unwrap(); - let m = compute(&outputs, &requests, &w, &[99.0], false); + let m = compute(&outputs, &w, &[99.0], false); assert!((m.mean_tpot_ms - 2000.0).abs() < 1e-6); assert!((m.median_tpot_ms - 2000.0).abs() < 1e-6); @@ -574,10 +547,8 @@ mod tests { #[test] fn json_roundtrip_some() { let outputs = vec![mk_full(0.0, 10.0, 2.0, vec![2.0; 4], 5); 3]; - let requests: Vec = - outputs.iter().map(|o| mk_sample(o.prompt_len)).collect(); let w = detect_window(&outputs, Some(3), 1.0, 0.1, 10.0).unwrap(); - let m = compute(&outputs, &requests, &w, &[99.0], false); + let m = compute(&outputs, &w, &[99.0], false); let s = serde_json::to_string(&m).unwrap(); let parsed: SteadyStateMetrics = serde_json::from_str(&s).unwrap(); assert!((parsed.output_throughput - m.output_throughput).abs() < 1e-12); @@ -605,14 +576,13 @@ mod tests { let itl = vec![1.0; 9]; let outputs: Vec = (0..5).map(|i| mk_full(i as f64, 20.0, 1.0, itl.clone(), 10)).collect(); - let requests: Vec = (0..5).map(|_| mk_sample(100)).collect(); let w = detect_window(&outputs, Some(5), 1.0, 1.0, 25.0).unwrap(); assert_eq!(w.threshold_abs, 5); assert!((w.start_s - 4.0).abs() < 1e-9); assert!((w.end_s - 20.0).abs() < 1e-9); - let m = compute(&outputs, &requests, &w, &[50.0, 99.0], false); + let m = compute(&outputs, &w, &[50.0, 99.0], false); // Round-trip through JSON. let s = serde_json::to_string(&m).unwrap(); diff --git a/rust/src/bench/src/mm_processor.rs b/rust/src/bench/src/mm_processor.rs index 3454dc7993c7..1f7668a0e9e5 100644 --- a/rust/src/bench/src/mm_processor.rs +++ b/rust/src/bench/src/mm_processor.rs @@ -135,6 +135,9 @@ pub struct MmProcessorArgs { /// Entry point for `vllm-bench mm-processor`. pub async fn run_mm_processor(args: MmProcessorArgs, timing: RequestTimingStats) -> Result<()> { + if args.max_concurrency == 0 { + anyhow::bail!("--max-concurrency must be greater than 0"); + } if args.engine.data_parallel_size_local == Some(0) { anyhow::bail!( "`--data-parallel-size-local 0` is not supported; the benchmark requires \ @@ -157,6 +160,7 @@ pub async fn run_mm_processor(args: MmProcessorArgs, timing: RequestTimingStats) handshake_host: args.engine.handshake_host.clone(), handshake_port, data_parallel_size: args.engine.data_parallel_size, + client_handshake_address: None, python_args, }; eprintln!( @@ -215,14 +219,15 @@ async fn run_mm_processor_with_engine( } // Connect to the engine over the same transport the serving frontend uses. + let engine_count = + args.engine.data_parallel_size_local.unwrap_or(args.engine.data_parallel_size); let client = EngineCoreClient::connect(EngineCoreClientConfig { transport_mode: TransportMode::HandshakeOwner { handshake_address: format!("tcp://{}:{}", args.engine.handshake_host, handshake_port), advertised_host: args.engine.handshake_host.clone(), - engine_count: args - .engine - .data_parallel_size_local - .unwrap_or(args.engine.data_parallel_size), + engine_start_index: 0, + engine_count, + data_parallel_size: engine_count, ready_timeout: ENGINE_READY_TIMEOUT, local_input_address: None, local_output_address: None, diff --git a/rust/src/bench/src/sweep.rs b/rust/src/bench/src/sweep.rs index 367e7adfc34e..7fb6a6129e00 100644 --- a/rust/src/bench/src/sweep.rs +++ b/rust/src/bench/src/sweep.rs @@ -376,9 +376,15 @@ impl SummaryColumn { pub fn parse_concurrency_values(s: &str) -> Result> { s.split(',') .map(|v| { - v.trim().parse::().map_err(|_| { + let value = v.trim().parse::().map_err(|_| { BenchError::Config(format!("Invalid concurrency value: '{}'", v.trim())) - }) + })?; + if value == 0 { + return Err(BenchError::Config( + "--sweep-max-concurrency values must be greater than 0, got 0".into(), + )); + } + Ok(value) }) .collect() } @@ -561,4 +567,25 @@ mod tests { "expected at least 2 dashes, got {dash_count}: {row}" ); } + + #[test] + fn parse_concurrency_values_rejects_zero() { + for input in ["0", "0,1", "1,0", "1,0,16"] { + let err = parse_concurrency_values(input).expect_err("0 must be rejected"); + let msg = err.to_string(); + assert!( + msg.contains("--sweep-max-concurrency") && msg.contains("greater than 0"), + "{input}: {msg}" + ); + } + } + + #[test] + fn parse_concurrency_values_accepts_positive() { + assert_eq!( + parse_concurrency_values("1,10,50").unwrap(), + vec![1, 10, 50] + ); + assert_eq!(parse_concurrency_values(" 8 , 16 ").unwrap(), vec![8, 16]); + } } diff --git a/rust/src/chat/examples/external_engine_chat_qwen.rs b/rust/src/chat/examples/external_engine_chat_qwen.rs index 1ba4d8a71a48..d210580d1da7 100644 --- a/rust/src/chat/examples/external_engine_chat_qwen.rs +++ b/rust/src/chat/examples/external_engine_chat_qwen.rs @@ -62,7 +62,9 @@ async fn main() -> Result<()> { transport_mode: TransportMode::HandshakeOwner { handshake_address: args.handshake_address.clone(), advertised_host: args.host.clone(), + engine_start_index: 0, engine_count: args.engine_count, + data_parallel_size: args.engine_count, ready_timeout, local_input_address: None, local_output_address: None, diff --git a/rust/src/chat/src/backend/hf.rs b/rust/src/chat/src/backend/hf.rs index f0da863534c7..a0b3d3451b5b 100644 --- a/rust/src/chat/src/backend/hf.rs +++ b/rust/src/chat/src/backend/hf.rs @@ -16,7 +16,7 @@ use crate::backend::{ ChatBackend, DynChatBackend, LoadModelBackendsOptions, LoadedModelBackends, NewChatOutputProcessorOptions, }; -use crate::error::Result; +use crate::error::{Error, Result}; use crate::multimodal::{MultimodalConfigFiles, MultimodalModelInfo}; use crate::output::{ DefaultChatOutputProcessor, HarmonyChatOutputProcessor, validate_harmony_parser_overrides, @@ -25,7 +25,7 @@ use crate::parser::unified::{HfTemplateError, ResponseTemplate}; use crate::renderer::hf::{HfChatRenderer, MultimodalRenderInfo}; use crate::renderer::{ DeepSeekV4ChatRenderer, DeepSeekV32ChatRenderer, DeepSeekV41ChatRenderer, DynChatRenderer, - HarmonyChatRenderer, InklingChatRenderer, KimiK3ChatRenderer, + Glm5NextChatRenderer, HarmonyChatRenderer, InklingChatRenderer, KimiK3ChatRenderer, }; use crate::request::ChatRequest; use crate::{DynChatOutputProcessor, RendererSelection}; @@ -72,6 +72,10 @@ impl HfChatBackend { let response_template = load_response_template(&files)?; let renderer = options.renderer.resolve(model_type); + // Only the HF renderer renders Jinja templates. + if renderer != RendererSelection::Hf && options.chat_template.is_some() { + return Err(Error::UnsupportedChatTemplateOption { renderer }); + } let chat_renderer: DynChatRenderer = match renderer { RendererSelection::Auto => unreachable!("renderer auto should be resolved above"), RendererSelection::Hf => Arc::new(HfChatRenderer::load( @@ -99,6 +103,11 @@ impl HfChatBackend { tokenizer.clone(), options.default_chat_template_kwargs, )), + RendererSelection::Glm5Next => Arc::new( + Glm5NextChatRenderer::new(options.default_chat_template_kwargs) + .with_multimodal(model_type == "glm5_next") + .with_tokenizer(tokenizer.clone())?, + ), }; info!( @@ -237,8 +246,8 @@ mod tests { use super::HfChatBackend; use crate::backend::{ChatBackend, LoadModelBackendsOptions, NewChatOutputProcessorOptions}; - use crate::request::{ChatContent, ChatMessage, ChatRequest}; - use crate::{ParserSelection, RendererSelection}; + use crate::request::{ChatContent, ChatMessage, ChatRequest, ResolvedToolContext}; + use crate::{ChatTool, ParserSelection, RendererSelection}; fn request_with_user_text(text: &str) -> ChatRequest { ChatRequest { @@ -282,6 +291,15 @@ mod tests { config_json: &str, tokenizer_config_json: &str, ) -> HfChatBackend { + load_backend(renderer, config_json, tokenizer_config_json, None).unwrap() + } + + fn load_backend( + renderer: RendererSelection, + config_json: &str, + tokenizer_config_json: &str, + chat_template: Option<&str>, + ) -> crate::Result { HfChatBackend::from_resolved_model_files( resolved_files(config_json, tokenizer_config_json), "test-model".to_string(), @@ -294,14 +312,13 @@ mod tests { renderer, language_model_only: false, chat_template_content_format: Default::default(), - chat_template: None, + chat_template: chat_template.map(str::to_string), trust_request_chat_template: false, default_chat_template_kwargs: HashMap::new(), limit_mm_per_prompt: HashMap::new(), }, test_tokenizer(), ) - .unwrap() } fn render_prompt( @@ -343,6 +360,72 @@ mod tests { assert_eq!(prompt, "hello"); } + #[test] + fn hf_renderer_selects_named_tokenizer_config_templates_like_transformers() { + let config = r#"{"model_type":"llama"}"#; + let named_templates = r#"{"chat_template":[ + {"name":"default","template":"default:{{ messages[0].content }}"}, + {"name":"tool_use","template":"tool_use:{{ tools[0].function.name }}"} + ]}"#; + let load = |files, chat_template: Option<&str>| { + HfChatBackend::from_resolved_model_files( + files, + "test-model".to_string(), + LoadModelBackendsOptions { + chat_template: chat_template.map(str::to_owned), + ..Default::default() + }, + test_tokenizer(), + ) + .unwrap() + }; + + let tokenizer_config_only = load(resolved_files(config, named_templates), None); + // A configured or dedicated template replaces every named template, + // `tool_use` included. + let configured = load( + resolved_files(config, named_templates), + Some("configured:{{ messages[0].content }}"), + ); + let mut files = resolved_files(config, named_templates); + let dedicated_path = + files.config_path.as_ref().unwrap().with_file_name("chat_template.jinja"); + std::fs::write(&dedicated_path, "dedicated:{{ messages[0].content }}").unwrap(); + files.chat_template_path = Some(dedicated_path); + let dedicated = load(files, None); + + let request = request_with_user_text("hello"); + let mut with_tools = request.clone(); + with_tools.tool_context = ResolvedToolContext::new( + &request.messages, + vec![ChatTool { + name: "lookup".to_string(), + description: None, + parameters: serde_json::json!({"type": "object"}), + strict: None, + defer_loading: None, + }], + None, + true, + ) + .unwrap(); + let rendered = [tokenizer_config_only, configured, dedicated].map(|backend| { + [&request, &with_tools].map(|request| { + backend.chat_renderer().render(request).unwrap().prompt.into_text().unwrap() + }) + }); + + assert_eq!( + rendered, + [ + ["default:hello", "tool_use:lookup"], + ["configured:hello", "configured:hello"], + ["dedicated:hello", "dedicated:hello"], + ] + .map(|row| row.map(str::to_owned)) + ); + } + #[test] fn native_renderer_inherits_deployment_reasoning_below_request_controls() { let backend = HfChatBackend::from_resolved_model_files( @@ -404,6 +487,43 @@ mod tests { ); } + #[tokio::test] + async fn only_hf_renderer_accepts_chat_template() { + use RendererSelection::Auto; + + const HF_CONFIG: &str = r#"{"model_type":"llama"}"#; + const NATIVE_CONFIG: &str = r#"{"model_type":"deepseek_v4"}"#; + const TOKENIZER_CONFIG: &str = r#"{"chat_template":"{{ messages[0].content }}"}"#; + const TEMPLATE: &str = "{{ messages[0].content }}!"; + + load_backend(Auto, HF_CONFIG, TOKENIZER_CONFIG, Some(TEMPLATE)).unwrap(); + let Err(error) = load_backend(Auto, NATIVE_CONFIG, TOKENIZER_CONFIG, Some(TEMPLATE)) else { + panic!("native renderers should reject --chat-template"); + }; + expect_test::expect!["--chat-template is not supported by the `deepseek_v4` renderer; use --tokenizer-mode hf to render a Jinja chat template"] + .assert_eq(&error.to_report_string()); + + let prepare = async |config_json| { + let backend = backend_for_selection(Auto, config_json, TOKENIZER_CONFIG); + let mut request = request_with_user_text("hello"); + request.chat_options.chat_template = Some(TEMPLATE.to_string()); + crate::ChatRequestProcessor::render_only(Arc::new(backend)) + .prepare_for_tokenization(request) + .await + .unwrap_err() + }; + // The HF renderer goes on to its trust check. + assert!(matches!( + prepare(HF_CONFIG).await, + crate::Error::UntrustedRequestChatTemplate + )); + let error = prepare(NATIVE_CONFIG).await; + assert!(error.is_request_validation_error()); + assert_eq!(error.param(), Some("chat_template")); + expect_test::expect!["chat_template is not supported by this chat renderer"] + .assert_eq(&error.to_report_string()); + } + #[test] fn auto_uses_harmony_renderer_and_output_processor_for_gpt_oss_model_type() { let backend = backend_for_selection( diff --git a/rust/src/chat/src/error.rs b/rust/src/chat/src/error.rs index 7245a07b1339..3e48d242ee89 100644 --- a/rust/src/chat/src/error.rs +++ b/rust/src/chat/src/error.rs @@ -4,6 +4,8 @@ use thiserror::Error; use thiserror_ext::{AsReport as _, Macro}; +use crate::RendererSelection; + type BoxedError = Box; #[derive(Debug, Error, Macro)] @@ -30,10 +32,21 @@ pub enum Error { Refused request with untrusted chat template." )] UntrustedRequestChatTemplate, + #[error("chat_template is not supported by this chat renderer")] + UnsupportedRequestChatTemplate, + #[error( + "--chat-template is not supported by the `{renderer}` renderer; use --tokenizer-mode hf to render a Jinja chat template" + )] + UnsupportedChatTemplateOption { renderer: RendererSelection }, #[error("multimodal input is not supported by this chat renderer")] UnsupportedMultimodalRenderer, #[error("unsupported multimodal content: {0}")] UnsupportedMultimodalContent(&'static str), + #[error( + "multimodal content in a non-leading system or developer message is not supported \ + when the chat template does not support the `developer` role" + )] + UnsupportedMultimodalSystemFallback, #[error("`{modality}` input is not supported by this model")] UnsupportedModality { modality: String }, #[error("At most {limit} {modality}(s) may be provided in one prompt.")] @@ -120,10 +133,12 @@ impl Error { | Self::ToolChoiceRequiresTools | Self::ToolChoiceFunctionNotFound { .. } | Self::UnsupportedChatRole { .. } - | Self::UntrustedRequestChatTemplate => true, + | Self::UntrustedRequestChatTemplate + | Self::UnsupportedRequestChatTemplate => true, Self::Text(error) => error.is_request_validation_error(), Self::UnsupportedMultimodalRenderer | Self::UnsupportedMultimodalContent(_) + | Self::UnsupportedMultimodalSystemFallback | Self::UnsupportedModality { .. } | Self::InvalidPreprocessedMultimodal { .. } | Self::MmLimitExceeded { .. } => true, @@ -137,6 +152,7 @@ impl Error { pub fn param(&self) -> Option<&'static str> { match self { Self::Text(error) => error.param(), + Self::UnsupportedRequestChatTemplate => Some("chat_template"), _ => None, } } diff --git a/rust/src/chat/src/event.rs b/rust/src/chat/src/event.rs index bbcec8121c79..0302684a5e03 100644 --- a/rust/src/chat/src/event.rs +++ b/rust/src/chat/src/event.rs @@ -6,7 +6,7 @@ use std::sync::Arc; use serde::{Deserialize, Serialize}; use vllm_engine_core_client::protocol::output::RequestSpecDecodeMetrics; -use vllm_llm::{RequestTimestamps, TokenUsage}; +use vllm_llm::{RequestTimestamps, TokenUsage, WeightVersionSpan}; use vllm_text::{DecodedLogprobs, DecodedPromptLogprobs}; use crate::FinishReason; @@ -248,5 +248,7 @@ pub enum ChatEvent { /// Connector-specific encoder cache transfer parameters for /// disaggregated serving. ec_transfer_params: Option>, + /// Weight-version spans over all generated token positions. + weight_versions: Option>, }, } diff --git a/rust/src/chat/src/lib.rs b/rust/src/chat/src/lib.rs index 47d813aae66a..a8f67c4ff680 100644 --- a/rust/src/chat/src/lib.rs +++ b/rust/src/chat/src/lib.rs @@ -221,21 +221,34 @@ impl ChatRequestProcessor { Ok(Some(features)) } - async fn prepare_text_request(&self, mut request: ChatRequest) -> Result { + /// Render one request into a text request, also returning the rendered + /// prompt text when the request asked for it and the renderer produced + /// text. + async fn prepare_text_request( + &self, + mut request: ChatRequest, + ) -> Result<(TextRequest, Option)> { // Stamp before rendering so render and tokenize count toward TTFT/e2e. let arrival_time = vllm_llm::current_unix_timestamp_secs(); if self.exclude_tools_when_tool_choice_none { request.tool_context.exclude_initial_tools_if_none(); } let renderer = self.backend.chat_renderer(); + if request.chat_options.chat_template.is_some() && !renderer.renders_request_chat_template() + { + return Err(Error::UnsupportedRequestChatTemplate); + } let (mut rendered, request) = tokio::task::spawn_blocking(move || { renderer.render(&request).map(|rendered| (rendered, request)) }) .await .map_err(|e| Error::ChatTemplate(format!("render task join error: {e}")))??; let chat_template_kwargs = std::mem::take(&mut rendered.effective_template_kwargs); + // Taken before media placeholders are expanded, like Python. + let prompt_text = + request.return_prompt_text.then(|| rendered.prompt.as_text().cloned()).flatten(); let (prompt, mm_features) = self.finalize_rendered_prompt(&request, rendered).await?; - Ok(TextRequest { + let text_request = TextRequest { request_id: request.request_id, prompt, mm_features, @@ -255,14 +268,16 @@ impl ChatRequestProcessor { reasoning_ended: None, lora_request: request.lora_request, arrival_time: Some(arrival_time), - }) + }; + Ok((text_request, prompt_text)) } /// Prepare one chat request for tokenization without constructing an output processor. pub async fn prepare_for_tokenization(&self, mut request: ChatRequest) -> Result { request.validate()?; self.map_reasoning_effort(&mut request); - self.prepare_text_request(request).await + let (text_request, _) = self.prepare_text_request(request).await?; + Ok(text_request) } /// Prepare one chat request without submitting it to an engine. @@ -275,7 +290,7 @@ impl ChatRequestProcessor { &self, mut request: ChatRequest, text_processor: &TextRequestProcessor, - ) -> Result<(TextRequest, DynChatOutputProcessor)> { + ) -> Result { request.validate()?; self.map_reasoning_effort(&mut request); @@ -287,7 +302,7 @@ impl ChatRequestProcessor { tool_strict_level: self.tool_strict_level, }, )?; - let mut text_request = self.prepare_text_request(request).await?; + let (mut text_request, prompt_text) = self.prepare_text_request(request).await?; // Initialize the output processor from the final prompt token IDs and apply // the output grammar to the request, if any. @@ -311,10 +326,25 @@ impl ChatRequestProcessor { } } - Ok((text_request, output_processor)) + Ok(PreparedChatRequest { + text_request, + output_processor, + prompt_text, + }) } } +/// One chat request rendered, tokenized, and ready for submission. +pub struct PreparedChatRequest { + /// The tokenized text request, with any output grammar applied. + pub text_request: TextRequest, + /// Request-scoped processor for the decoded output. + pub output_processor: DynChatOutputProcessor, + /// The rendered prompt text, when the request asked for it and the + /// renderer produced text. + pub prompt_text: Option, +} + /// Structured chat facade above [`TextLlm`]. /// /// This layer stays above raw text semantics: it takes care of chat-template @@ -446,14 +476,17 @@ impl ChatLlm { /// Render, tokenize, and submit one chat request. pub async fn chat(&self, request: ChatRequest) -> Result { - let (text_request, output_processor) = - self.processor.prepare(request, self.text.request_processor()).await?; + let PreparedChatRequest { + text_request, + output_processor, + prompt_text, + } = self.processor.prepare(request, self.text.request_processor()).await?; let request_id = text_request.request_id.clone(); let decoded_stream = self.text.generate(text_request).await?.map_err(Error::from).boxed(); let structured_stream = output_processor.process(decoded_stream)?; - Ok(ChatEventStream::new(request_id, structured_stream)) + Ok(ChatEventStream::new(request_id, structured_stream).with_prompt_text(prompt_text)) } /// Abort in-flight requests by their external (user-supplied) request ids. diff --git a/rust/src/chat/src/output/default/mod.rs b/rust/src/chat/src/output/default/mod.rs index 8e125b0679d1..3b4bb0617a33 100644 --- a/rust/src/chat/src/output/default/mod.rs +++ b/rust/src/chat/src/output/default/mod.rs @@ -410,6 +410,7 @@ mod tests { ec_transfer_params: None, sampling_mask: None, spec_decode_metrics: None, + weight_versions: None, timestamps: Default::default(), })), })]); diff --git a/rust/src/chat/src/output/default/unified.rs b/rust/src/chat/src/output/default/unified.rs index da4f16efc075..cd1208888e05 100644 --- a/rust/src/chat/src/output/default/unified.rs +++ b/rust/src/chat/src/output/default/unified.rs @@ -282,6 +282,7 @@ pub(crate) async fn unified_event_stream( finish_reason: finished.finish_reason, kv_transfer_params: finished.kv_transfer_params, ec_transfer_params: finished.ec_transfer_params, + weight_versions: finished.weight_versions, }) .await; } @@ -422,6 +423,7 @@ mod tests { ec_transfer_params: None, sampling_mask: None, spec_decode_metrics: None, + weight_versions: None, timestamps: Default::default(), })), } @@ -751,6 +753,7 @@ mod tests { finish_reason: crate::FinishReason::Stop(None), kv_transfer_params: None, ec_transfer_params: None, + weight_versions: None, }, ] ); @@ -793,6 +796,7 @@ mod tests { finish_reason: crate::FinishReason::Stop(None), kv_transfer_params: None, ec_transfer_params: None, + weight_versions: None, }, ] ); @@ -827,6 +831,7 @@ mod tests { finish_reason: crate::FinishReason::Stop(None), kv_transfer_params: None, ec_transfer_params: None, + weight_versions: None, }, ] ); @@ -881,6 +886,7 @@ mod tests { finish_reason: crate::FinishReason::Stop(None), kv_transfer_params: None, ec_transfer_params: None, + weight_versions: None, }, ] ); @@ -929,6 +935,7 @@ mod tests { finish_reason: crate::FinishReason::Stop(None), kv_transfer_params: None, ec_transfer_params: None, + weight_versions: None, }, ] ); diff --git a/rust/src/chat/src/output/harmony/mod.rs b/rust/src/chat/src/output/harmony/mod.rs index d4227041a7e3..2dc1b2e18739 100644 --- a/rust/src/chat/src/output/harmony/mod.rs +++ b/rust/src/chat/src/output/harmony/mod.rs @@ -15,7 +15,7 @@ use vllm_text::output::{DecodedTextEvent, SampledDelta}; use crate::Result as ChatResult; use crate::error::{Error, Result}; -use crate::event::AssistantBlockKind; +use crate::event::{AssistantBlockKind, ChatTokenUsage}; use crate::output::{ AssistantEvent, ChatOutputProcessor, DynChatEventStream, DynDecodedTextEventStream, generate_tool_call_id, @@ -47,6 +47,8 @@ struct HarmonyGroupKey { struct HarmonyGroup { key: HarmonyGroupKey, text: String, + /// Reasoning tokens attributed to this group's text. + reasoning_tokens: usize, } #[derive(Debug)] @@ -57,6 +59,8 @@ struct OpenHarmonyToolCall { struct HarmonyState { /// Incremental Harmony parser over assistant token IDs. parser: StreamableParser, + /// Harmony encoding, used to tell control tokens from content tokens. + encoding: &'static HarmonyEncoding, /// Whether tool-call content should surface as structured tool events. tool_calls_enabled: bool, /// Count of completed visible assistant messages for newline insertion. @@ -67,6 +71,13 @@ struct HarmonyState { current_text_group: Option, /// The currently open Harmony tool recipient, if any. open_tool_call: Option, + /// Total generated tokens counted as reasoning, reported in the final + /// usage. + reasoning_tokens: usize, + /// Reasoning tokens of the current message not yet attributed to an + /// emitted reasoning delta, such as a token whose content delta is still + /// empty. + unattributed_reasoning_tokens: usize, } impl HarmonyChatOutputProcessor { @@ -119,18 +130,48 @@ impl ChatOutputProcessor for HarmonyChatOutputProcessor { impl HarmonyState { /// Create one fresh Harmony streaming state for a new assistant response. - fn new(encoding: HarmonyEncoding, tool_calls_enabled: bool) -> Result { + fn new(encoding: &'static HarmonyEncoding, tool_calls_enabled: bool) -> Result { Ok(Self { - parser: StreamableParser::new(encoding, Some(Role::Assistant)) + parser: StreamableParser::new(encoding.clone(), Some(Role::Assistant)) .map_err(harmony_output_parsing_error)?, + encoding, tool_calls_enabled, completed_visible_messages: 0, completed_reasoning_messages: 0, current_text_group: None, open_tool_call: None, + reasoning_tokens: 0, + unattributed_reasoning_tokens: 0, }) } + /// Count the token the parser just consumed as reasoning if it is content + /// of an `analysis` message or of a `commentary` message with a recipient, + /// excluding Harmony control tokens, like Python's + /// `HarmonyParser._is_reasoning_token`. + fn count_reasoning_token(&mut self, token_id: u32) { + let is_reasoning_channel = match self.parser.current_channel().as_deref() { + Some("analysis") => true, + Some("commentary") => self.parser.current_recipient().is_some(), + _ => false, + }; + if is_reasoning_channel && !self.encoding.tokenizer().is_special_token(token_id) { + self.reasoning_tokens += 1; + self.unattributed_reasoning_tokens += 1; + } + } + + /// Take the reasoning tokens not yet attributed if `key` is a reasoning + /// text group. + fn take_reasoning_tokens_for(&mut self, key: &HarmonyGroupKey) -> usize { + match text_block_kind(key.channel.as_deref(), key.recipient.as_deref()) { + Some(AssistantBlockKind::Reasoning) => { + std::mem::take(&mut self.unattributed_reasoning_tokens) + } + _ => 0, + } + } + fn process_token_ids(&mut self, token_ids: &[u32]) -> Result> { let mut events = Vec::new(); let mut pending_group: Option = None; @@ -139,6 +180,7 @@ impl HarmonyState { let completed_before = self.parser.messages().len(); self.parser.process(token_id).map_err(harmony_output_parsing_error)?; let completed_after = self.parser.messages().len(); + self.count_reasoning_token(token_id); if let Some(delta) = self .parser @@ -151,14 +193,22 @@ impl HarmonyState { channel: self.parser.current_channel(), recipient: self.parser.current_recipient(), }; + let reasoning_tokens = self.take_reasoning_tokens_for(&key); match pending_group.as_mut() { - Some(group) if group.key == key => group.text.push_str(&delta), + Some(group) if group.key == key => { + group.text.push_str(&delta); + group.reasoning_tokens += reasoning_tokens; + } _ => { if let Some(group) = pending_group.take() { self.emit_group(group, &mut events); } - pending_group = Some(HarmonyGroup { key, text: delta }); + pending_group = Some(HarmonyGroup { + key, + text: delta, + reasoning_tokens, + }); } } } @@ -214,10 +264,12 @@ impl HarmonyState { let final_text = harmony_message_text(final_message); let tail = final_text.strip_prefix(&pending_content).unwrap_or(final_text).to_string(); if !tail.is_empty() { + let reasoning_tokens = self.take_reasoning_tokens_for(&pending_key); self.emit_group( HarmonyGroup { key: pending_key, text: tail, + reasoning_tokens, }, &mut events, ); @@ -258,8 +310,6 @@ impl HarmonyState { events.push(AssistantEvent::TextDelta { kind, delta: "\n".to_string(), - // TODO: measure reasoning tokens from native - // analysis-channel token counts. token_count: None, }); } @@ -267,12 +317,12 @@ impl HarmonyState { self.current_text_group = Some(group.key.clone()); } + let token_count = + (kind == AssistantBlockKind::Reasoning).then_some(group.reasoning_tokens); events.push(AssistantEvent::TextDelta { kind, delta: group.text, - // TODO: measure reasoning tokens from native - // analysis-channel token counts. - token_count: None, + token_count, }); return; } @@ -307,6 +357,10 @@ impl HarmonyState { /// Update newline and open-tool state after one Harmony message completes. fn handle_completed_message(&mut self, key: HarmonyGroupKey) { + // Reasoning tokens of a tool-call message are never attributed to a + // delta; they reach the client through the final usage only. + self.unattributed_reasoning_tokens = 0; + if self.current_text_group.as_ref() == Some(&key) { self.current_text_group = None; } @@ -334,7 +388,7 @@ async fn harmony_assistant_event_stream( tool_calls_enabled: bool, mut y: TryYielder, ) -> Result<()> { - let mut state = HarmonyState::new(encoding.clone(), tool_calls_enabled)?; + let mut state = HarmonyState::new(encoding, tool_calls_enabled)?; futures::pin_mut!(decoded); while let Some(event) = decoded.next().await.transpose()? { @@ -378,12 +432,16 @@ async fn harmony_assistant_event_stream( if let Some(finished) = finished { y.yield_ok(AssistantEvent::Done { - usage: finished.usage.into(), + usage: ChatTokenUsage { + engine: finished.usage, + reasoning_tokens: state.reasoning_tokens, + }, timestamps: finished.timestamps, spec_decode_metrics: finished.spec_decode_metrics, finish_reason: finished.finish_reason, kv_transfer_params: finished.kv_transfer_params, ec_transfer_params: finished.ec_transfer_params, + weight_versions: finished.weight_versions, }) .await; } diff --git a/rust/src/chat/src/output/harmony/tests.rs b/rust/src/chat/src/output/harmony/tests.rs index e05a15bfd53a..1c8a91c713ef 100644 --- a/rust/src/chat/src/output/harmony/tests.rs +++ b/rust/src/chat/src/output/harmony/tests.rs @@ -11,6 +11,7 @@ use vllm_text::output::{ }; use super::*; +use crate::event::AssistantBlockKind; use crate::output::ChatOutputProcessor; use crate::request::{ChatRequest, ChatTool, ChatToolChoice, ResolvedToolContext}; use crate::{AssistantMessageExt, ChatEvent, ChatTokenUsage, FinishReason}; @@ -60,6 +61,7 @@ fn finished() -> Finished { ec_transfer_params: None, sampling_mask: None, spec_decode_metrics: None, + weight_versions: None, timestamps: Default::default(), } } @@ -139,6 +141,7 @@ fn interrupted_final_message_is_preserved() { finish_reason: FinishReason::stop_eos(), kv_transfer_params: None, ec_transfer_params: None, + weight_versions: None, }) ); } @@ -182,16 +185,20 @@ fn interrupted_analysis_message_is_preserved() { text: "think".to_string(), }], }, - usage: ChatTokenUsage::from(vllm_llm::TokenUsage { - prompt_token_count: 0, - output_token_count: 0, - prompt_cache_usage: Default::default(), - }), + usage: ChatTokenUsage { + engine: vllm_llm::TokenUsage { + prompt_token_count: 0, + output_token_count: 0, + prompt_cache_usage: Default::default(), + }, + reasoning_tokens: 1, + }, timestamps: Default::default(), spec_decode_metrics: None, finish_reason: FinishReason::stop_eos(), kv_transfer_params: None, ec_transfer_params: None, + weight_versions: None, }) ); } @@ -286,6 +293,124 @@ fn tool_calls_stream_arguments_and_finish_with_local_id_shape() { assert_eq!(end.arguments, r#"{"city":"Paris"}"#); } +fn content_token_count(text: &str) -> usize { + harmony_encoding().unwrap().tokenizer().encode_ordinary(text).len() +} + +fn reasoning_token_counts(events: &[ChatEvent]) -> (Vec, usize) { + let deltas = events + .iter() + .filter_map(|event| match event { + ChatEvent::BlockDelta { + kind: AssistantBlockKind::Reasoning, + token_count, + .. + // The newline separating two reasoning messages has no tokens. + } => Some(token_count.unwrap_or_default()), + _ => None, + }) + .collect(); + let ChatEvent::Done { usage, .. } = events.last().unwrap() else { + panic!("expected done"); + }; + (deltas, usage.reasoning_tokens) +} + +#[test] +fn reasoning_tokens_count_analysis_and_tool_call_content() { + let analysis = "Let me check the weather."; + let arguments = r#"{"city":"Paris"}"#; + let tokens = completion_tokens(&[ + text_message("analysis", analysis), + tool_message("get_weather", arguments, "commentary"), + ]); + let events = block_on(collect_events( + HarmonyChatOutputProcessor::new(&request_with_tools()).unwrap(), + vec![ + decoded_start(), + decoded_tokens(tokens, None, Some(finished())), + ], + )); + + // Like Python, content of `analysis` and of `commentary` with a recipient + // counts, Harmony control and header tokens do not. Tool-call tokens are + // only in the final usage. + let (deltas, total) = reasoning_token_counts(&events); + assert_eq!(deltas.iter().sum::(), content_token_count(analysis)); + assert_eq!( + total, + content_token_count(analysis) + content_token_count(arguments) + ); +} + +#[test] +fn reasoning_tokens_exclude_final_and_preamble_content() { + let tokens = completion_tokens(&[ + text_message("commentary", "Let me check."), + text_message("final", "Hello"), + ]); + let events = block_on(collect_events( + HarmonyChatOutputProcessor::new(&ChatRequest::for_test()).unwrap(), + vec![ + decoded_start(), + decoded_tokens(tokens, None, Some(finished())), + ], + )); + + assert_eq!(reasoning_token_counts(&events), (vec![], 0)); +} + +fn stream_one_token_per_update(messages: &[Message]) -> Vec { + let tokens = completion_tokens(messages); + let last = tokens.len() - 1; + let mut decoded = vec![decoded_start()]; + for (i, token) in tokens.into_iter().enumerate() { + decoded.push(decoded_tokens( + vec![token], + None, + (i == last).then(finished), + )); + } + block_on(collect_events( + HarmonyChatOutputProcessor::new(&ChatRequest::for_test()).unwrap(), + decoded, + )) +} + +#[test] +fn reasoning_tokens_are_attributed_to_their_streamed_deltas() { + let first = "Let me think about this."; + let second = "Then answer."; + let events = stream_one_token_per_update(&[ + text_message("analysis", first), + text_message("analysis", second), + text_message("final", "Hi"), + ]); + + // One delta per content token, and the separating newline carries none. + let mut expected = vec![1; content_token_count(first)]; + expected.push(0); + expected.extend(vec![1; content_token_count(second)]); + let total = expected.iter().sum(); + assert_eq!(reasoning_token_counts(&events), (expected, total)); +} + +#[test] +fn reasoning_tokens_of_a_split_character_join_its_delta() { + // Each of these characters spans several byte-level tokens, so the first + // tokens of a character produce no text delta of their own. + let text = "\u{1D518}\u{1D52B}"; + let tokens = content_token_count(text); + assert!(tokens > text.chars().count()); + let events = + stream_one_token_per_update(&[text_message("analysis", text), text_message("final", "Hi")]); + + let (deltas, total) = reasoning_token_counts(&events); + assert_eq!(deltas.len(), text.chars().count()); + assert_eq!(deltas.iter().sum::(), tokens); + assert_eq!(total, tokens); +} + #[test] fn semantic_events_precede_same_update_logprobs() { let tokens = completion_tokens(&[text_message("final", "hello")]); diff --git a/rust/src/chat/src/output/mod.rs b/rust/src/chat/src/output/mod.rs index 6e6ad4909e2a..e06f70dabaf6 100644 --- a/rust/src/chat/src/output/mod.rs +++ b/rust/src/chat/src/output/mod.rs @@ -8,7 +8,7 @@ use futures::Stream; use trait_set::trait_set; use uuid::Uuid; use vllm_engine_core_client::protocol::output::RequestSpecDecodeMetrics; -use vllm_llm::RequestTimestamps; +use vllm_llm::{RequestTimestamps, WeightVersionSpan}; use vllm_parser::output_grammar::BuiltOutputGrammar; use vllm_text::output::{DecodedLogprobs, DecodedPromptLogprobs, DecodedTextEvent}; @@ -64,6 +64,8 @@ pub(crate) enum AssistantEvent { /// Connector-specific encoder cache transfer parameters for /// disaggregated serving. ec_transfer_params: Option>, + /// Weight-version spans over all generated token positions. + weight_versions: Option>, }, } diff --git a/rust/src/chat/src/output/structured.rs b/rust/src/chat/src/output/structured.rs index 583d8e78f487..aca2d7b61423 100644 --- a/rust/src/chat/src/output/structured.rs +++ b/rust/src/chat/src/output/structured.rs @@ -11,7 +11,7 @@ use asynk_strim_attr::{TryYielder, try_stream}; use futures::{StreamExt as _, pin_mut}; use vllm_engine_core_client::protocol::output::RequestSpecDecodeMetrics; -use vllm_llm::RequestTimestamps; +use vllm_llm::{RequestTimestamps, WeightVersionSpan}; use vllm_text::DecodedLogprobs; use super::{AssistantEvent, AssistantEventStream}; @@ -157,6 +157,7 @@ impl StructuredEventState { finish_reason: FinishReason, kv_transfer_params: Option>, ec_transfer_params: Option>, + weight_versions: Option>, ) -> Result> { let mut events = Vec::new(); self.close_open_text_block(&mut events); @@ -169,6 +170,7 @@ impl StructuredEventState { finish_reason, kv_transfer_params, ec_transfer_params, + weight_versions, }); Ok(events) } @@ -324,6 +326,7 @@ pub(crate) async fn structured_chat_event_stream( finish_reason, kv_transfer_params, ec_transfer_params, + weight_versions, } => { for next in state.finish( usage, @@ -332,6 +335,7 @@ pub(crate) async fn structured_chat_event_stream( finish_reason, kv_transfer_params, ec_transfer_params, + weight_versions, )? { y.yield_ok(next).await; } @@ -372,6 +376,7 @@ mod tests { finish_reason: FinishReason::stop_eos(), kv_transfer_params: None, ec_transfer_params: None, + weight_versions: None, }), ]); @@ -429,6 +434,7 @@ mod tests { finish_reason: FinishReason::stop_eos(), kv_transfer_params: None, ec_transfer_params: None, + weight_versions: None, }), ]); @@ -484,6 +490,7 @@ mod tests { finish_reason: FinishReason::stop_eos(), kv_transfer_params: None, ec_transfer_params: None, + weight_versions: None, }), ]); @@ -539,6 +546,7 @@ mod tests { finish_reason: FinishReason::stop_eos(), kv_transfer_params: None, ec_transfer_params: None, + weight_versions: None, }), ]); @@ -609,6 +617,7 @@ mod tests { finish_reason: FinishReason::stop_eos(), kv_transfer_params: None, ec_transfer_params: None, + weight_versions: None, }), ]); @@ -662,6 +671,7 @@ mod tests { finish_reason: FinishReason::stop_eos(), kv_transfer_params: None, ec_transfer_params: None, + weight_versions: None, }), ]); diff --git a/rust/src/chat/src/renderer/glm5_next/encoding.rs b/rust/src/chat/src/renderer/glm5_next/encoding.rs new file mode 100644 index 000000000000..3fa988a5fbe0 --- /dev/null +++ b/rust/src/chat/src/renderer/glm5_next/encoding.rs @@ -0,0 +1,535 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright contributors to the vLLM project + +use std::collections::{HashMap, HashSet}; +use std::fmt::Write as _; +use std::io::{self, Write}; + +use serde::Serialize; +use serde_json::ser::{Formatter, Serializer}; +use serde_json::{Value, json}; +use thiserror_ext::AsReport as _; + +use super::writer::Glm5PromptWriter; +use crate::error::{Error, Result, bail_chat_template_thrown, chat_template_thrown}; +use crate::renderer::MediaPartSource; +use crate::request::{ChatContent, ChatContentPart, ChatMessage, ChatTool, GenerationPromptMode}; +use crate::{AssistantContentBlock, AssistantMessageExt}; + +const TOOLS_PREFIX: &str = "<|system|>\n# Tools\n\nYou may call one or more functions to assist with the user query.\n\nYou are provided with function signatures within XML tags:\n\n"; +const TOOLS_SUFFIX: &str = "\n\nFor each function call, output the function name and arguments within the following XML format:\n{function-name}{arg-key-1}{arg-value-1}{arg-key-2}{arg-value-2}..."; + +pub(super) fn truthy(value: &Value) -> bool { + match value { + Value::Null => false, + Value::Bool(value) => *value, + Value::Number(value) => value.as_f64() != Some(0.0), + Value::String(value) => !value.is_empty(), + Value::Array(value) => !value.is_empty(), + Value::Object(value) => !value.is_empty(), + } +} + +fn text(value: &Value) -> String { + match value { + Value::Null => "None".into(), + Value::Bool(true) => "True".into(), + Value::Bool(false) => "False".into(), + Value::String(value) => value.clone(), + _ => value.to_string(), + } +} + +#[derive(Default)] +struct MediaContext { + enabled: bool, + order: Vec, +} + +fn visible_text( + out: &mut Glm5PromptWriter<'_>, + value: &Value, + media: &mut MediaContext, +) -> Result<()> { + match value { + Value::Null => {} + Value::String(value) => out.ordinary(value), + Value::Array(parts) => { + for part in parts { + if let Some(part) = part.as_str() { + out.ordinary(part); + continue; + } + match part["type"].as_str().unwrap_or_default() { + "text" => out.ordinary(&part.get("text").map(text).unwrap_or_default()), + kind @ ("image" | "image_url" | "video" | "video_url" | "audio" + | "audio_url" | "input_audio") => { + let kind = kind.replace("_url", "").replace("input_", ""); + if media.enabled { + out.control(&format!("<|begin_of_{kind}|>"))?; + if kind != "audio" { + out.control(&format!("<|{kind}|>"))?; + } + out.control(&format!("<|end_of_{kind}|>"))?; + if let (Some(message_index), Some(content_part_index)) = ( + part["message_index"].as_u64(), + part["content_part_index"].as_u64(), + ) { + media.order.push(MediaPartSource { + message_index: message_index as usize, + content_part_index: content_part_index as usize, + }); + } + } else { + out.control(&format!("You are unable to process this {kind} because you don't have multi-modal input ability. Try different methods."))?; + } + } + _ => {} + } + } + } + _ => out.ordinary(&text(value)), + } + Ok(()) +} + +fn tool_definition(tool: &Value) -> Result { + let Some(fields) = tool.as_object() else { + bail_chat_template_thrown!("Each tool must be a JSON object"); + }; + let mut out = String::from("{"); + for (key, value) in fields { + if key == "strict" || key == "defer_loading" { + continue; + } + if out.len() > 1 { + out.push_str(", "); + } + write!(out, "\"{key}\": {}", json(value)?).unwrap(); + } + out.push('}'); + Ok(out) +} + +fn unwrap_tool(tool: &Value) -> &Value { + tool.get("function").unwrap_or(tool) +} + +fn tool_references(value: &Value) -> bool { + value + .as_array() + .and_then(|parts| parts.first()) + .is_some_and(|part| part["type"] == "tool_reference") +} + +fn reference_response(out: &mut Glm5PromptWriter<'_>, refs: &Value, tools: &[Value]) -> Result<()> { + if tools.is_empty() { + bail_chat_template_thrown!("Rendering a tool_reference requires tools"); + } + out.control("\n")?; + for reference in refs.as_array().into_iter().flatten() { + for tool in tools.iter().map(unwrap_tool) { + if tool.get("name") == reference.get("name") { + out.ordinary(&tool_definition(tool)?); + out.control("\n")?; + } + } + } + out.control("") +} + +fn response( + out: &mut Glm5PromptWriter<'_>, + value: &Value, + tools: &[Value], + media: &mut MediaContext, +) -> Result<()> { + if tool_references(value) { + reference_response(out, value, tools) + } else { + out.control("")?; + visible_text(out, value, media)?; + out.control("") + } +} + +fn bundled_outputs(value: &Value) -> Result { + if !truthy(value) || value.is_string() { + return Ok(false); + } + let Some(parts) = value.as_array() else { + bail_chat_template_thrown!("A tool message content must be a string or a list"); + }; + Ok(parts.first().is_some_and(|part| part.get("output").is_some())) +} + +fn tool_response( + out: &mut Glm5PromptWriter<'_>, + message: &Value, + tools: &[Value], + media: &mut MediaContext, +) -> Result<()> { + let value = &message["content"]; + if tool_references(value) { + return reference_response(out, value, tools); + } + if bundled_outputs(value)? { + for part in value.as_array().unwrap() { + response(out, &part["output"], tools, media)?; + } + Ok(()) + } else { + response(out, value, tools, media) + } +} + +fn id(value: &Value) -> String { + ["tool_call_id", "id"] + .into_iter() + .filter_map(|key| value.get(key)) + .find(|value| truthy(value)) + .map(text) + .unwrap_or_default() +} + +fn ordered_tool_block( + out: &mut Glm5PromptWriter<'_>, + block: &[Value], + assistant: Option<&Value>, + tools: &[Value], + media: &mut MediaContext, +) -> Result<()> { + let calls = assistant + .filter(|message| message["role"] == "assistant") + .and_then(|message| message["tool_calls"].as_array()) + .filter(|calls| !calls.is_empty()); + if let Some(calls) = calls { + let mut can_sort = true; + let mut responses = HashMap::new(); + for message in block { + if bundled_outputs(&message["content"])? { + for entry in message["content"].as_array().unwrap() { + let key = id(entry); + if key.is_empty() || responses.insert(key, &entry["output"]).is_some() { + can_sort = false; + } + } + } else { + let key = id(message); + if key.is_empty() || responses.insert(key, &message["content"]).is_some() { + can_sort = false; + } + } + } + let call_ids: Vec<_> = calls.iter().map(id).collect(); + let mut seen = HashSet::new(); + for key in &call_ids { + if key.is_empty() || !seen.insert(key) { + can_sort = false; + } + } + if can_sort && responses.keys().all(|key| seen.contains(key)) { + for value in call_ids.iter().filter_map(|key| responses.get(key)) { + response(out, value, tools, media)?; + } + return Ok(()); + } + } + for message in block { + tool_response(out, message, tools, media)?; + } + Ok(()) +} + +fn tool_call(out: &mut Glm5PromptWriter<'_>, call: &Value) -> Result<()> { + let call = call.get("function").filter(|value| truthy(value)).unwrap_or(call); + let Some(name) = call["name"].as_str() else { + bail_chat_template_thrown!("Each tool_call needs a string name"); + }; + let parsed; + let arguments = if let Some(raw) = call["arguments"].as_str() { + parsed = serde_json::from_str::(raw).map_err(|error| { + chat_template_thrown!("Invalid tool_call arguments: {}", error.as_report()) + })?; + &parsed + } else { + &call["arguments"] + }; + let Some(arguments) = arguments.as_object() else { + bail_chat_template_thrown!("tool_call arguments must be a JSON object"); + }; + out.control("")?; + out.ordinary(name); + for (key, value) in arguments { + out.control("")?; + out.ordinary(key); + out.control("")?; + if let Some(value) = value.as_str() { + out.ordinary(value); + } else { + out.ordinary(&json(value)?); + } + out.control("")?; + } + out.control("") +} + +#[cfg(test)] +pub(super) fn render( + messages: &[Value], + tools: &[Value], + mode: GenerationPromptMode, + kwargs: &HashMap, +) -> Result { + let mut out = Glm5PromptWriter::new(None); + render_into(&mut out, messages, tools, mode, kwargs, false)?; + Ok(out.text().to_owned()) +} + +/// Render into `out`, returning media parts in placeholder order. +pub(super) fn render_into( + out: &mut Glm5PromptWriter<'_>, + messages: &[Value], + tools: &[Value], + mode: GenerationPromptMode, + kwargs: &HashMap, + multimodal: bool, +) -> Result> { + let mut media = MediaContext { + enabled: multimodal, + order: Vec::new(), + }; + render_inner(out, messages, tools, mode, kwargs, &mut media)?; + Ok(media.order) +} + +fn render_inner( + out: &mut Glm5PromptWriter<'_>, + messages: &[Value], + tools: &[Value], + mode: GenerationPromptMode, + kwargs: &HashMap, + media: &mut MediaContext, +) -> Result<()> { + let effort = match kwargs.get("reasoning_effort").and_then(Value::as_str) { + Some("low") => "Low", + Some("high") => "High", + _ => "Max", + }; + let clear_thinking = kwargs.get("clear_thinking").is_some_and(truthy); + out.control(&format!("[gMASK]<|system|>Reasoning Effort: {effort}"))?; + if !tools.is_empty() { + out.control(TOOLS_PREFIX)?; + for tool in tools.iter().map(unwrap_tool) { + if !truthy(&tool["defer_loading"]) { + out.ordinary(&tool_definition(tool)?); + out.control("\n")?; + } + } + out.control(TOOLS_SUFFIX)?; + } + let last_user = messages.iter().rposition(|message| message["role"] == "user"); + let mut index = 0; + while index < messages.len() { + let message = &messages[index]; + match message["role"].as_str().unwrap_or_default() { + "user" | "system" => { + out.control(&format!("<|{}|>", message["role"].as_str().unwrap()))?; + visible_text(out, &message["content"], media)?; + } + "assistant" => { + out.control("<|assistant|>")?; + let mut content = { + let mut text = Glm5PromptWriter::new(None); + visible_text(&mut text, &message["content"], media)?; + text.text().to_owned() + }; + let original = content.clone(); + let mut reasoning = message["reasoning_content"].as_str().map(str::to_owned); + if reasoning.is_none() && content.contains("") { + reasoning = Some( + content + .split("") + .next() + .unwrap() + .rsplit("") + .next() + .unwrap() + .to_owned(), + ); + content = content.rsplit("").next().unwrap().to_owned(); + } + if (!clear_thinking || last_user.is_none_or(|last| index > last)) + && let Some(reasoning) = reasoning + { + out.ordinary(&reasoning); + } + out.control("")?; + let trim = |ch: char| ch.is_whitespace() || ('\u{1c}'..='\u{1f}').contains(&ch); + if mode == GenerationPromptMode::ContinueFinalAssistant + && index + 1 == messages.len() + { + // Match HF continuation, which truncates the template output + // at the end of the final content: tool calls are dropped, + // trailing whitespace is trimmed, and content the template + // rewrote is rejected. + out.ordinary(content.trim_matches(trim)); + let original = original.trim_matches(trim); + if !out.text().contains(original) { + return Err(Error::ChatTemplate(format!( + "continue_final_message is set but the final message does not appear \ + in the prompt after applying the chat template! This can happen if \ + the chat template deletes portions of the final message. Final \ + message to continue: {original}" + ))); + } + break; + } + out.ordinary(content.trim_matches(trim)); + for call in message["tool_calls"].as_array().into_iter().flatten() { + tool_call(out, call)?; + } + } + "tool" => { + let start = index; + while index + 1 < messages.len() && messages[index + 1]["role"] == "tool" { + index += 1; + } + out.control("<|observation|>")?; + ordered_tool_block( + out, + &messages[start..=index], + start.checked_sub(1).map(|i| &messages[i]), + tools, + media, + )?; + } + _ => {} + } + index += 1; + } + if mode == GenerationPromptMode::StartNewAssistant { + out.control("<|assistant|>")?; + } + Ok(()) +} + +// TODO: Expose tool_reference and bundled outputs in ChatContentPart. + +fn content(value: &ChatContent, message_index: usize) -> Value { + match value { + ChatContent::Text(text) => json!(text), + ChatContent::Parts(parts) => Value::Array( + parts + .iter() + .enumerate() + .map(|(content_part_index, part)| match part { + ChatContentPart::Text { text } => json!({"type": "text", "text": text}), + ChatContentPart::ImageUrl { .. } => json!({"type": "image", "message_index": message_index, "content_part_index": content_part_index}), + ChatContentPart::VideoUrl { .. } => json!({"type": "video", "message_index": message_index, "content_part_index": content_part_index}), + ChatContentPart::InputAudio { .. } | ChatContentPart::AudioUrl { .. } => { + json!({"type": "audio", "message_index": message_index, "content_part_index": content_part_index}) + } + }) + .collect(), + ), + } +} + +pub(super) fn messages(messages: &[ChatMessage]) -> Vec { + messages.iter().enumerate().map(|(index, message)| match message { + ChatMessage::System { content: value } => json!({"role": "system", "content": content(value, index)}), + ChatMessage::Developer { content: value, .. } => json!({"role": "developer", "content": content(value, index)}), + ChatMessage::User { content: value } => json!({"role": "user", "content": content(value, index)}), + ChatMessage::Custom { role, content: value } => json!({"role": role, "content": content(value, index)}), + ChatMessage::ToolResponse { content: value, tool_call_id } => { + json!({"role": "tool", "content": content(value, index), "tool_call_id": tool_call_id}) + } + ChatMessage::Assistant { content } => { + let mut message = json!({"role": "assistant", "content": content.text()}); + if content.iter().any(|block| matches!(block, AssistantContentBlock::Reasoning { .. })) { + message["reasoning_content"] = json!(content.iter().filter_map(|block| match block { + AssistantContentBlock::Reasoning { text } => Some(text.as_str()), + _ => None, + }).collect::()); + } + if content.has_tool_calls() { + message["tool_calls"] = Value::Array(content.tool_calls().map(|call| { + json!({"id": call.id, "function": {"name": call.name, "arguments": call.arguments}}) + }).collect()); + } + message + } + }).collect() +} + +pub(super) fn tools(tools: &[ChatTool]) -> Vec { + tools + .iter() + .map(|tool| { + json!({"function": { + "name": tool.name, + "description": tool.description, + "parameters": tool.parameters, + "defer_loading": tool.defer_loading, + }}) + }) + .collect() +} + +struct PythonFormatter; + +impl Formatter for PythonFormatter { + fn begin_array_value( + &mut self, + writer: &mut W, + first: bool, + ) -> io::Result<()> { + if !first { + writer.write_all(b", ")?; + } + Ok(()) + } + + fn begin_object_key( + &mut self, + writer: &mut W, + first: bool, + ) -> io::Result<()> { + if !first { + writer.write_all(b", ")?; + } + Ok(()) + } + + fn begin_object_value(&mut self, writer: &mut W) -> io::Result<()> { + writer.write_all(b": ") + } + + fn write_f64(&mut self, writer: &mut W, value: f64) -> io::Result<()> { + let magnitude = value.abs(); + if magnitude != 0.0 && !(1e-4..1e16).contains(&magnitude) { + let scientific = format!("{value:e}"); + let (mantissa, exponent) = scientific.split_once('e').unwrap(); + let exponent: i32 = exponent.parse().unwrap(); + write!(writer, "{mantissa}e{exponent:+03}") + } else { + let decimal = value.to_string(); + writer.write_all(decimal.as_bytes())?; + if !decimal.contains('.') { + writer.write_all(b".0")?; + } + Ok(()) + } + } +} + +fn json(value: &Value) -> Result { + let mut bytes = Vec::new(); + value + .serialize(&mut Serializer::with_formatter(&mut bytes, PythonFormatter)) + .map_err(|error| { + chat_template_thrown!("GLM JSON serialization failed: {}", error.as_report()) + })?; + Ok(String::from_utf8(bytes).expect("JSON serializer must write UTF-8")) +} diff --git a/rust/src/chat/src/renderer/glm5_next/fixtures/glm_5_3.jinja b/rust/src/chat/src/renderer/glm5_next/fixtures/glm_5_3.jinja new file mode 100644 index 000000000000..55fd3945292b --- /dev/null +++ b/rust/src/chat/src/renderer/glm5_next/fixtures/glm_5_3.jinja @@ -0,0 +1,251 @@ +[gMASK] +{%- set effective_reasoning_effort = reasoning_effort if reasoning_effort is defined and reasoning_effort in ['low', 'high'] else 'max' -%} +{%- if effective_reasoning_effort is not none -%}<|system|>Reasoning Effort: {{ effective_reasoning_effort | capitalize }}{%- endif -%} +{%- set clear_thinking = clear_thinking if clear_thinking is defined else false -%} +{%- if tools -%} +{%- macro tool_to_json(tool) -%} + {%- set ns_tool = namespace(first=true) -%} + {{ '{' -}} + {%- for k, v in tool.items() -%} + {%- if k != 'defer_loading' and k != 'strict' -%} + {%- if not ns_tool.first -%}{{- ', ' -}}{%- endif -%} + {%- set ns_tool.first = false -%} + "{{ k }}": {{ v | tojson(ensure_ascii=False) }} + {%- endif -%} + {%- endfor -%} + {{- '}' -}} +{%- endmacro -%} +{%- macro tool_references_to_response(refs) -%} + {{- '\n' -}} + {%- for tr in refs -%} + {%- for tool in tools -%} + {%- if 'function' in tool -%} + {%- set tool = tool['function'] -%} + {%- endif -%} + {%- if tool.name == tr.name -%} + {{- tool_to_json(tool) + '\n' -}} + {%- endif -%} + {%- endfor -%} + {%- endfor -%} + {{- '' -}} +{%- endmacro -%} +<|system|> +# Tools + +You may call one or more functions to assist with the user query. + +You are provided with function signatures within XML tags: + +{% for tool in tools %} +{%- if 'function' in tool -%} + {%- set tool = tool['function'] -%} +{%- endif -%} +{% if tool.defer_loading is not defined or not tool.defer_loading %} +{{ tool_to_json(tool) }} +{% endif %} +{% endfor %} + + +For each function call, output the function name and arguments within the following XML format: +{function-name}{arg-key-1}{arg-value-1}{arg-key-2}{arg-value-2}...{%- endif -%} +{%- macro visible_text(content) -%} + {%- if content is string -%} + {{- content }} + {%- elif content is iterable and content is not mapping -%} + {%- for item in content -%} + {%- if item is mapping and item.type == 'text' -%} + {{- item.text }} + {%- elif item is string -%} + {{- item }} + {%- elif item is mapping and item.type in ['image', 'image_url', 'video', 'video_url', 'audio', 'audio_url', 'input_audio'] -%} + {%- set media_type = item.type | replace('_url', '') | replace('input_', '') -%} + {{- "You are unable to process this " ~ media_type ~ " because you don't have multi-modal input ability. Try different methods." }} + {%- endif -%} + {%- endfor -%} + {%- elif content is not none -%} + {{- content }} + {%- endif -%} +{%- endmacro -%} +{%- macro tool_response(text) -%} +{{- '' + text + '' -}} +{%- endmacro -%} +{%- macro render_tool_response(m) -%} +{%- if m.content is string -%} + {{- tool_response(m.content) -}} +{%- elif m.content and m.content is not mapping and m.content.0.type == "tool_reference" -%} + {{- tool_references_to_response(m.content) -}} +{%- elif is_list_of_outputs(m) -%} + {%- for tr in m.content -%} + {%- if tr.output is iterable and tr.output is not string and tr.output is not mapping and tr.output and tr.output.0.type == "tool_reference" -%} + {{- tool_references_to_response(tr.output) -}} + {%- else -%} + {{- tool_response(visible_text(tr.output)) -}} + {%- endif -%} + {%- endfor -%} +{%- else -%} + {{- tool_response(visible_text(m.content)) -}} +{%- endif -%} +{%- endmacro -%} +{%- macro id_of(obj) -%} + {%- if obj.tool_call_id -%} + {{- obj.tool_call_id -}} + {%- elif obj.id -%} + {{- obj.id -}} + {%- endif -%} +{%- endmacro -%} +{%- macro is_list_of_outputs(m) -%} + {%- if m.content and m.content.0.output is defined -%}1{%- endif -%} +{%- endmacro -%} +{%- macro has_dup_tool_result_id(lo, hi, target) -%} + {%- set ns_cnt = namespace(n=0) -%} + {%- for k in range(lo, hi + 1) -%} + {%- set m = messages[k] -%} + {%- if is_list_of_outputs(m) -%} + {%- for entry in m.content -%} + {%- if id_of(entry) == target -%} + {%- set ns_cnt.n = ns_cnt.n + 1 -%} + {%- endif -%} + {%- endfor -%} + {%- elif id_of(m) == target -%} + {%- set ns_cnt.n = ns_cnt.n + 1 -%} + {%- endif -%} + {%- if ns_cnt.n > 1 -%}{%- break -%}{%- endif -%} + {%- endfor -%} + {%- if ns_cnt.n > 1 -%}1{%- endif -%} +{%- endmacro -%} +{%- macro tc_id_exists(tcs, target) -%} + {%- set ns_f = namespace(found=false) -%} + {%- for tc in tcs -%} + {%- if id_of(tc) == target -%} + {%- set ns_f.found = true -%} + {%- break -%} + {%- endif -%} + {%- endfor -%} + {%- if ns_f.found -%}1{%- endif -%} +{%- endmacro -%} +{%- set ns = namespace(last_user_index=-1) -%} +{%- for m in messages %} + {%- if m.role == 'user' %} + {%- set ns.last_user_index = loop.index0 -%} + {%- endif %} +{%- endfor %} +{%- for m in messages -%} +{%- if m.role == 'user' -%}<|user|>{{ visible_text(m.content) }} +{%- elif m.role == 'assistant' -%} +<|assistant|> +{%- set content = visible_text(m.content) %} +{%- if m.reasoning_content is string %} + {%- set reasoning_content = m.reasoning_content %} +{%- elif '' in content %} + {%- set reasoning_content = content.split('')[0].split('')[-1] %} + {%- set content = content.split('')[-1] %} +{%- endif %} +{%- if (not clear_thinking or loop.index0 > ns.last_user_index) and reasoning_content is defined -%} +{{ '' + reasoning_content + ''}} +{%- else -%} +{{ '' }} +{%- endif -%} +{%- if content.strip() -%} +{{ content.strip() }} +{%- endif -%} +{% if m.tool_calls %} +{% for tc in m.tool_calls %} +{%- if tc.function %} + {%- set tc = tc.function %} +{%- endif %} +{{- '' + tc.name -}} +{% set _args = tc.arguments %}{% for k, v in _args.items() %}{{ k }}{{ v | tojson(ensure_ascii=False) if v is not string else v }}{% endfor %}{% endfor %} +{% endif %} +{%- elif m.role == 'tool' -%} +{%- if loop.first or (messages[loop.index0 - 1].role != "tool") %} + {{- '<|observation|>' -}} + {%- set block_start = loop.index0 -%} + {%- set ns_blk = namespace(end=block_start) -%} + {%- for j in range(block_start, messages|length) -%} + {%- if messages[j].role == 'tool' -%} + {%- set ns_blk.end = j -%} + {%- else -%} + {%- break -%} + {%- endif -%} + {%- endfor -%} + {%- set ns_a = namespace(tool_calls=none) -%} + {%- if block_start > 0 and messages[block_start - 1].role == 'assistant' and messages[block_start - 1].tool_calls -%} + {%- set ns_a.tool_calls = messages[block_start - 1].tool_calls -%} + {%- endif -%} + {%- set ns_chk = namespace(can_sort=true, res=[], mk=[], ek=[], tcs=[]) -%} + {%- for k in range(block_start, ns_blk.end + 1) -%} + {%- set m = messages[k] -%} + {%- if is_list_of_outputs(m) -%} + {%- for entry in m.content -%} + {%- set ns_chk.res = ns_chk.res + [id_of(entry)] -%} + {%- set ns_chk.mk = ns_chk.mk + [k] -%} + {%- set ns_chk.ek = ns_chk.ek + [loop.index0] -%} + {%- endfor -%} + {%- else -%} + {%- set ns_chk.res = ns_chk.res + [id_of(m)] -%} + {%- set ns_chk.mk = ns_chk.mk + [k] -%} + {%- set ns_chk.ek = ns_chk.ek + [-1] -%} + {%- endif -%} + {%- endfor -%} + {%- if not ns_a.tool_calls -%} + {%- set ns_chk.can_sort = false -%} + {%- else -%} + {%- for tc in ns_a.tool_calls -%} + {%- set ns_chk.tcs = ns_chk.tcs + [id_of(tc)] -%} + {%- endfor -%} + {%- if '' in ns_chk.res or '' in ns_chk.tcs -%} + {%- set ns_chk.can_sort = false -%} + {%- elif ns_chk.res | unique(case_sensitive=true) | list | length != ns_chk.res | length -%} + {%- set ns_chk.can_sort = false -%} + {%- elif ns_chk.tcs | unique(case_sensitive=true) | list | length != ns_chk.tcs | length -%} + {%- set ns_chk.can_sort = false -%} + {%- else -%} + {%- for eid in ns_chk.res -%} + {%- if eid not in ns_chk.tcs -%} + {%- set ns_chk.can_sort = false -%} + {%- break -%} + {%- endif -%} + {%- endfor -%} + {%- endif -%} + {%- endif -%} + {%- if ns_chk.can_sort -%} + {%- set ns_ord = namespace(res=[], tcs=[], j=0, pairs=[]) -%} + {%- for eid in ns_chk.res -%} + {%- set ns_ord.res = ns_ord.res + [{'id': eid, 'p': loop.index0}] -%} + {%- endfor -%} + {%- for tc_id in ns_chk.tcs -%} + {%- set ns_ord.tcs = ns_ord.tcs + [{'id': tc_id, 'i': loop.index0}] -%} + {%- endfor -%} + {%- set ns_ord.res = ns_ord.res | sort(attribute='id', case_sensitive=true) -%} + {%- for tc in ns_ord.tcs | sort(attribute='id', case_sensitive=true) -%} + {%- if ns_ord.j < ns_ord.res | length and ns_ord.res[ns_ord.j].id == tc.id -%} + {%- set ns_ord.pairs = ns_ord.pairs + [{'i': tc.i, 'p': ns_ord.res[ns_ord.j].p}] -%} + {%- set ns_ord.j = ns_ord.j + 1 -%} + {%- endif -%} + {%- endfor -%} + {%- for pair in ns_ord.pairs | sort(attribute='i') -%} + {%- set m = messages[ns_chk.mk[pair.p]] -%} + {%- if ns_chk.ek[pair.p] >= 0 -%} + {%- set entry = m.content[ns_chk.ek[pair.p]] -%} + {%- if entry.output is iterable and entry.output is not string and entry.output is not mapping and entry.output and entry.output.0.type == "tool_reference" -%} + {{- tool_references_to_response(entry.output) -}} + {%- else -%} + {{- tool_response(visible_text(entry.output)) -}} + {%- endif -%} + {%- else -%} + {{- render_tool_response(m) -}} + {%- endif -%} + {%- endfor -%} + {%- else -%} + {%- for k in range(block_start, ns_blk.end + 1) -%} + {{- render_tool_response(messages[k]) -}} + {%- endfor -%} + {%- endif -%} +{% endif -%} +{%- elif m.role == 'system' -%} +<|system|>{{ visible_text(m.content) }} +{%- endif -%} +{%- endfor -%} +{%- if add_generation_prompt -%} + <|assistant|>{{- '' -}} +{%- endif -%} diff --git a/rust/src/chat/src/renderer/glm5_next/fixtures/glm_5_3_flash.jinja b/rust/src/chat/src/renderer/glm5_next/fixtures/glm_5_3_flash.jinja new file mode 100644 index 000000000000..14180451f166 --- /dev/null +++ b/rust/src/chat/src/renderer/glm5_next/fixtures/glm_5_3_flash.jinja @@ -0,0 +1,257 @@ +[gMASK] +{%- set effective_reasoning_effort = reasoning_effort if reasoning_effort is defined and reasoning_effort in ['low', 'high'] else 'max' -%} +{%- if effective_reasoning_effort is not none -%}<|system|>Reasoning Effort: {{ effective_reasoning_effort | capitalize }}{%- endif -%} +{%- set clear_thinking = clear_thinking if clear_thinking is defined else false -%} +{%- if tools -%} +{%- macro tool_to_json(tool) -%} + {%- set ns_tool = namespace(first=true) -%} + {{ '{' -}} + {%- for k, v in tool.items() -%} + {%- if k != 'defer_loading' and k != 'strict' -%} + {%- if not ns_tool.first -%}{{- ', ' -}}{%- endif -%} + {%- set ns_tool.first = false -%} + "{{ k }}": {{ v | tojson(ensure_ascii=False) }} + {%- endif -%} + {%- endfor -%} + {{- '}' -}} +{%- endmacro -%} +{%- macro tool_references_to_response(refs) -%} + {{- '\n' -}} + {%- for tr in refs -%} + {%- for tool in tools -%} + {%- if 'function' in tool -%} + {%- set tool = tool['function'] -%} + {%- endif -%} + {%- if tool.name == tr.name -%} + {{- tool_to_json(tool) + '\n' -}} + {%- endif -%} + {%- endfor -%} + {%- endfor -%} + {{- '' -}} +{%- endmacro -%} +<|system|> +# Tools + +You may call one or more functions to assist with the user query. + +You are provided with function signatures within XML tags: + +{% for tool in tools %} +{%- if 'function' in tool -%} + {%- set tool = tool['function'] -%} +{%- endif -%} +{% if tool.defer_loading is not defined or not tool.defer_loading %} +{{ tool_to_json(tool) }} +{% endif %} +{% endfor %} + + +For each function call, output the function name and arguments within the following XML format: +{function-name}{arg-key-1}{arg-value-1}{arg-key-2}{arg-value-2}...{%- endif -%} +{%- macro emit_image() -%}<|begin_of_image|><|image|><|end_of_image|>{%- endmacro -%} +{%- macro emit_video() -%}<|begin_of_video|><|video|><|end_of_video|>{%- endmacro -%} +{%- macro emit_audio() -%}<|begin_of_audio|><|end_of_audio|>{%- endmacro -%} +{%- macro visible_text(content) -%} + {%- if content is string -%} + {{- content -}} + {%- elif content is iterable and content is not mapping -%} + {%- for item in content -%} + {%- if item is mapping and item.type == 'text' -%} + {{- item.text -}} + {%- elif item is string -%} + {{- item -}} + {%- elif item is mapping and item.type in ['image', 'image_url'] -%} + {{- emit_image() -}} + {%- elif item is mapping and item.type in ['video', 'video_url'] -%} + {{- emit_video() -}} + {%- elif item is mapping and item.type in ['audio', 'audio_url', 'input_audio'] -%} + {{- emit_audio() -}} + {%- endif -%} + {%- endfor -%} + {%- elif content is not none -%} + {{- content -}} + {%- endif -%} +{%- endmacro -%} +{%- macro tool_response(text) -%} +{{- '' + text + '' -}} +{%- endmacro -%} +{%- macro render_tool_response(m) -%} +{%- if m.content is string -%} + {{- tool_response(m.content) -}} +{%- elif m.content and m.content is not mapping and m.content.0.type == "tool_reference" -%} + {{- tool_references_to_response(m.content) -}} +{%- elif is_list_of_outputs(m) -%} + {%- for tr in m.content -%} + {%- if tr.output is iterable and tr.output is not string and tr.output is not mapping and tr.output and tr.output.0.type == "tool_reference" -%} + {{- tool_references_to_response(tr.output) -}} + {%- else -%} + {{- tool_response(visible_text(tr.output)) -}} + {%- endif -%} + {%- endfor -%} +{%- else -%} + {{- tool_response(visible_text(m.content)) -}} +{%- endif -%} +{%- endmacro -%} +{%- macro id_of(obj) -%} + {%- if obj.tool_call_id -%} + {{- obj.tool_call_id -}} + {%- elif obj.id -%} + {{- obj.id -}} + {%- endif -%} +{%- endmacro -%} +{%- macro is_list_of_outputs(m) -%} + {%- if m.content and m.content.0.output is defined -%}1{%- endif -%} +{%- endmacro -%} +{%- macro has_dup_tool_result_id(lo, hi, target) -%} + {%- set ns_cnt = namespace(n=0) -%} + {%- for k in range(lo, hi + 1) -%} + {%- set m = messages[k] -%} + {%- if is_list_of_outputs(m) -%} + {%- for entry in m.content -%} + {%- if id_of(entry) == target -%} + {%- set ns_cnt.n = ns_cnt.n + 1 -%} + {%- endif -%} + {%- endfor -%} + {%- elif id_of(m) == target -%} + {%- set ns_cnt.n = ns_cnt.n + 1 -%} + {%- endif -%} + {%- if ns_cnt.n > 1 -%}{%- break -%}{%- endif -%} + {%- endfor -%} + {%- if ns_cnt.n > 1 -%}1{%- endif -%} +{%- endmacro -%} +{%- macro tc_id_exists(tcs, target) -%} + {%- set ns_f = namespace(found=false) -%} + {%- for tc in tcs -%} + {%- if id_of(tc) == target -%} + {%- set ns_f.found = true -%} + {%- break -%} + {%- endif -%} + {%- endfor -%} + {%- if ns_f.found -%}1{%- endif -%} +{%- endmacro -%} +{%- set ns = namespace(last_user_index=-1) -%} +{%- for m in messages %} + {%- if m.role == 'user' %} + {%- set ns.last_user_index = loop.index0 -%} + {%- endif %} +{%- endfor %} +{%- for m in messages -%} +{%- if m.role == 'user' -%}<|user|>{{ visible_text(m.content) }} +{%- elif m.role == 'assistant' -%} +<|assistant|> +{%- set content = visible_text(m.content) %} +{%- if m.reasoning_content is string %} + {%- set reasoning_content = m.reasoning_content %} +{%- elif '' in content %} + {%- set reasoning_content = content.split('')[0].split('')[-1] %} + {%- set content = content.split('')[-1] %} +{%- endif %} +{%- if (not clear_thinking or loop.index0 > ns.last_user_index) and reasoning_content is defined -%} +{{ '' + reasoning_content + ''}} +{%- else -%} +{{ '' }} +{%- endif -%} +{%- if content.strip() -%} +{{ content.strip() }} +{%- endif -%} +{% if m.tool_calls %} +{% for tc in m.tool_calls %} +{%- if tc.function %} + {%- set tc = tc.function %} +{%- endif %} +{{- '' ~ tc.name -}} +{% set _args = tc.arguments %}{% for k, v in _args.items() %}{{ k }}{{ v | tojson(ensure_ascii=False) if v is not string else v }}{% endfor %}{% endfor %} +{% endif %} +{%- elif m.role == 'tool' -%} +{%- if loop.first or (messages[loop.index0 - 1].role != "tool") %} + {{- '<|observation|>' -}} + {%- set block_start = loop.index0 -%} + {%- set ns_blk = namespace(end=block_start) -%} + {%- for j in range(block_start, messages|length) -%} + {%- if messages[j].role == 'tool' -%} + {%- set ns_blk.end = j -%} + {%- else -%} + {%- break -%} + {%- endif -%} + {%- endfor -%} + {%- set ns_a = namespace(tool_calls=none) -%} + {%- if block_start > 0 and messages[block_start - 1].role == 'assistant' and messages[block_start - 1].tool_calls -%} + {%- set ns_a.tool_calls = messages[block_start - 1].tool_calls -%} + {%- endif -%} + {%- set ns_chk = namespace(can_sort=true, res=[], mk=[], ek=[], tcs=[]) -%} + {%- for k in range(block_start, ns_blk.end + 1) -%} + {%- set m = messages[k] -%} + {%- if is_list_of_outputs(m) -%} + {%- for entry in m.content -%} + {%- set ns_chk.res = ns_chk.res + [id_of(entry)] -%} + {%- set ns_chk.mk = ns_chk.mk + [k] -%} + {%- set ns_chk.ek = ns_chk.ek + [loop.index0] -%} + {%- endfor -%} + {%- else -%} + {%- set ns_chk.res = ns_chk.res + [id_of(m)] -%} + {%- set ns_chk.mk = ns_chk.mk + [k] -%} + {%- set ns_chk.ek = ns_chk.ek + [-1] -%} + {%- endif -%} + {%- endfor -%} + {%- if not ns_a.tool_calls -%} + {%- set ns_chk.can_sort = false -%} + {%- else -%} + {%- for tc in ns_a.tool_calls -%} + {%- set ns_chk.tcs = ns_chk.tcs + [id_of(tc)] -%} + {%- endfor -%} + {%- if '' in ns_chk.res or '' in ns_chk.tcs -%} + {%- set ns_chk.can_sort = false -%} + {%- elif ns_chk.res | unique(case_sensitive=true) | list | length != ns_chk.res | length -%} + {%- set ns_chk.can_sort = false -%} + {%- elif ns_chk.tcs | unique(case_sensitive=true) | list | length != ns_chk.tcs | length -%} + {%- set ns_chk.can_sort = false -%} + {%- else -%} + {%- for eid in ns_chk.res -%} + {%- if eid not in ns_chk.tcs -%} + {%- set ns_chk.can_sort = false -%} + {%- break -%} + {%- endif -%} + {%- endfor -%} + {%- endif -%} + {%- endif -%} + {%- if ns_chk.can_sort -%} + {%- set ns_ord = namespace(res=[], tcs=[], j=0, pairs=[]) -%} + {%- for eid in ns_chk.res -%} + {%- set ns_ord.res = ns_ord.res + [{'id': eid, 'p': loop.index0}] -%} + {%- endfor -%} + {%- for tc_id in ns_chk.tcs -%} + {%- set ns_ord.tcs = ns_ord.tcs + [{'id': tc_id, 'i': loop.index0}] -%} + {%- endfor -%} + {%- set ns_ord.res = ns_ord.res | sort(attribute='id', case_sensitive=true) -%} + {%- for tc in ns_ord.tcs | sort(attribute='id', case_sensitive=true) -%} + {%- if ns_ord.j < ns_ord.res | length and ns_ord.res[ns_ord.j].id == tc.id -%} + {%- set ns_ord.pairs = ns_ord.pairs + [{'i': tc.i, 'p': ns_ord.res[ns_ord.j].p}] -%} + {%- set ns_ord.j = ns_ord.j + 1 -%} + {%- endif -%} + {%- endfor -%} + {%- for pair in ns_ord.pairs | sort(attribute='i') -%} + {%- set m = messages[ns_chk.mk[pair.p]] -%} + {%- if ns_chk.ek[pair.p] >= 0 -%} + {%- set entry = m.content[ns_chk.ek[pair.p]] -%} + {%- if entry.output is iterable and entry.output is not string and entry.output is not mapping and entry.output and entry.output.0.type == "tool_reference" -%} + {{- tool_references_to_response(entry.output) -}} + {%- else -%} + {{- tool_response(visible_text(entry.output)) -}} + {%- endif -%} + {%- else -%} + {{- render_tool_response(m) -}} + {%- endif -%} + {%- endfor -%} + {%- else -%} + {%- for k in range(block_start, ns_blk.end + 1) -%} + {{- render_tool_response(messages[k]) -}} + {%- endfor -%} + {%- endif -%} +{% endif -%} +{%- elif m.role == 'system' -%} +<|system|>{{ visible_text(m.content) }} +{%- endif -%} +{%- endfor -%} +{%- if add_generation_prompt -%} + <|assistant|>{{- '' -}} +{%- endif -%} diff --git a/rust/src/chat/src/renderer/glm5_next/fixtures/test_cases.json b/rust/src/chat/src/renderer/glm5_next/fixtures/test_cases.json new file mode 100644 index 000000000000..ac18c322edad --- /dev/null +++ b/rust/src/chat/src/renderer/glm5_next/fixtures/test_cases.json @@ -0,0 +1,3001 @@ +{ + "cases": [ + { + "name": "basic", + "messages": [ + { + "role": "user", + "content": "你好,GLM!" + } + ], + "tools": [], + "add_generation_prompt": true, + "kwargs": {}, + "expected": "[gMASK]<|system|>Reasoning Effort: Max<|user|>你好,GLM!<|assistant|>" + }, + { + "name": "no_generation_prompt", + "messages": [ + { + "role": "user", + "content": "你好,GLM!" + } + ], + "tools": [], + "add_generation_prompt": false, + "kwargs": {}, + "expected": "[gMASK]<|system|>Reasoning Effort: Max<|user|>你好,GLM!" + }, + { + "name": "effort_None", + "messages": [ + { + "role": "user", + "content": "你好,GLM!" + } + ], + "tools": [], + "add_generation_prompt": true, + "kwargs": { + "reasoning_effort": null + }, + "expected": "[gMASK]<|system|>Reasoning Effort: Max<|user|>你好,GLM!<|assistant|>" + }, + { + "name": "effort_low", + "messages": [ + { + "role": "user", + "content": "你好,GLM!" + } + ], + "tools": [], + "add_generation_prompt": true, + "kwargs": { + "reasoning_effort": "low" + }, + "expected": "[gMASK]<|system|>Reasoning Effort: Low<|user|>你好,GLM!<|assistant|>" + }, + { + "name": "effort_high", + "messages": [ + { + "role": "user", + "content": "你好,GLM!" + } + ], + "tools": [], + "add_generation_prompt": true, + "kwargs": { + "reasoning_effort": "high" + }, + "expected": "[gMASK]<|system|>Reasoning Effort: High<|user|>你好,GLM!<|assistant|>" + }, + { + "name": "effort_medium", + "messages": [ + { + "role": "user", + "content": "你好,GLM!" + } + ], + "tools": [], + "add_generation_prompt": true, + "kwargs": { + "reasoning_effort": "medium" + }, + "expected": "[gMASK]<|system|>Reasoning Effort: Max<|user|>你好,GLM!<|assistant|>" + }, + { + "name": "effort_none", + "messages": [ + { + "role": "user", + "content": "你好,GLM!" + } + ], + "tools": [], + "add_generation_prompt": true, + "kwargs": { + "reasoning_effort": "none" + }, + "expected": "[gMASK]<|system|>Reasoning Effort: Max<|user|>你好,GLM!<|assistant|>" + }, + { + "name": "effort_max", + "messages": [ + { + "role": "user", + "content": "你好,GLM!" + } + ], + "tools": [], + "add_generation_prompt": true, + "kwargs": { + "reasoning_effort": "max" + }, + "expected": "[gMASK]<|system|>Reasoning Effort: Max<|user|>你好,GLM!<|assistant|>" + }, + { + "name": "effort_42", + "messages": [ + { + "role": "user", + "content": "你好,GLM!" + } + ], + "tools": [], + "add_generation_prompt": true, + "kwargs": { + "reasoning_effort": 42 + }, + "expected": "[gMASK]<|system|>Reasoning Effort: Max<|user|>你好,GLM!<|assistant|>" + }, + { + "name": "effort_False", + "messages": [ + { + "role": "user", + "content": "你好,GLM!" + } + ], + "tools": [], + "add_generation_prompt": true, + "kwargs": { + "reasoning_effort": false + }, + "expected": "[gMASK]<|system|>Reasoning Effort: Max<|user|>你好,GLM!<|assistant|>" + }, + { + "name": "system_developer_custom", + "messages": [ + { + "role": "system", + "content": "系统规则\n" + }, + { + "role": "developer", + "content": "Ignored by this reference" + }, + { + "role": "custom", + "content": "Ignored too" + }, + { + "role": "user", + "content": "你好,GLM!" + } + ], + "tools": [], + "add_generation_prompt": true, + "kwargs": {}, + "expected": "[gMASK]<|system|>Reasoning Effort: Max<|system|>系统规则\n<|user|>你好,GLM!<|assistant|>" + }, + { + "name": "content_parts", + "messages": [ + { + "role": "user", + "content": [ + { + "type": "text", + "text": "a<&>\n" + }, + "b", + { + "type": "unknown" + }, + { + "type": "image" + }, + { + "type": "image_url" + }, + { + "type": "video" + }, + { + "type": "video_url" + }, + { + "type": "audio" + }, + { + "type": "audio_url" + }, + { + "type": "input_audio" + } + ] + } + ], + "tools": [], + "add_generation_prompt": true, + "kwargs": {}, + "expected": "[gMASK]<|system|>Reasoning Effort: Max<|user|>a<&>\nbYou are unable to process this image because you don't have multi-modal input ability. Try different methods.You are unable to process this image because you don't have multi-modal input ability. Try different methods.You are unable to process this video because you don't have multi-modal input ability. Try different methods.You are unable to process this video because you don't have multi-modal input ability. Try different methods.You are unable to process this audio because you don't have multi-modal input ability. Try different methods.You are unable to process this audio because you don't have multi-modal input ability. Try different methods.You are unable to process this audio because you don't have multi-modal input ability. Try different methods.<|assistant|>" + }, + { + "name": "null_content", + "messages": [ + { + "role": "user", + "content": null + }, + { + "role": "assistant", + "content": null + } + ], + "tools": [], + "add_generation_prompt": true, + "kwargs": {}, + "expected": "[gMASK]<|system|>Reasoning Effort: Max<|user|><|assistant|><|assistant|>" + }, + { + "name": "missing_content", + "messages": [ + { + "role": "user" + }, + { + "role": "assistant" + } + ], + "tools": [], + "add_generation_prompt": true, + "kwargs": {}, + "expected": "[gMASK]<|system|>Reasoning Effort: Max<|user|><|assistant|><|assistant|>" + }, + { + "name": "python_whitespace", + "messages": [ + { + "role": "user", + "content": "你好,GLM!" + }, + { + "role": "assistant", + "content": "\u001c\u001d\u001e\u001f… answer\u001c\u001f" + } + ], + "tools": [], + "add_generation_prompt": true, + "kwargs": {}, + "expected": "[gMASK]<|system|>Reasoning Effort: Max<|user|>你好,GLM!<|assistant|>answer<|assistant|>" + }, + { + "name": "clear_False", + "messages": [ + { + "role": "user", + "content": "你好,GLM!" + }, + { + "role": "assistant", + "content": " 答案 \n", + "reasoning_content": "旧思考" + }, + { + "role": "user", + "content": "继续" + }, + { + "role": "assistant", + "content": "", + "reasoning_content": "新思考" + } + ], + "tools": [], + "add_generation_prompt": true, + "kwargs": { + "clear_thinking": false + }, + "expected": "[gMASK]<|system|>Reasoning Effort: Max<|user|>你好,GLM!<|assistant|>旧思考答案<|user|>继续<|assistant|>新思考<|assistant|>" + }, + { + "name": "clear_True", + "messages": [ + { + "role": "user", + "content": "你好,GLM!" + }, + { + "role": "assistant", + "content": " 答案 \n", + "reasoning_content": "旧思考" + }, + { + "role": "user", + "content": "继续" + }, + { + "role": "assistant", + "content": "", + "reasoning_content": "新思考" + } + ], + "tools": [], + "add_generation_prompt": true, + "kwargs": { + "clear_thinking": true + }, + "expected": "[gMASK]<|system|>Reasoning Effort: Max<|user|>你好,GLM!<|assistant|>答案<|user|>继续<|assistant|>新思考<|assistant|>" + }, + { + "name": "clear_None", + "messages": [ + { + "role": "user", + "content": "你好,GLM!" + }, + { + "role": "assistant", + "content": " 答案 \n", + "reasoning_content": "旧思考" + }, + { + "role": "user", + "content": "继续" + }, + { + "role": "assistant", + "content": "", + "reasoning_content": "新思考" + } + ], + "tools": [], + "add_generation_prompt": true, + "kwargs": { + "clear_thinking": null + }, + "expected": "[gMASK]<|system|>Reasoning Effort: Max<|user|>你好,GLM!<|assistant|>旧思考答案<|user|>继续<|assistant|>新思考<|assistant|>" + }, + { + "name": "clear_", + "messages": [ + { + "role": "user", + "content": "你好,GLM!" + }, + { + "role": "assistant", + "content": " 答案 \n", + "reasoning_content": "旧思考" + }, + { + "role": "user", + "content": "继续" + }, + { + "role": "assistant", + "content": "", + "reasoning_content": "新思考" + } + ], + "tools": [], + "add_generation_prompt": true, + "kwargs": { + "clear_thinking": "" + }, + "expected": "[gMASK]<|system|>Reasoning Effort: Max<|user|>你好,GLM!<|assistant|>旧思考答案<|user|>继续<|assistant|>新思考<|assistant|>" + }, + { + "name": "clear_yes", + "messages": [ + { + "role": "user", + "content": "你好,GLM!" + }, + { + "role": "assistant", + "content": " 答案 \n", + "reasoning_content": "旧思考" + }, + { + "role": "user", + "content": "继续" + }, + { + "role": "assistant", + "content": "", + "reasoning_content": "新思考" + } + ], + "tools": [], + "add_generation_prompt": true, + "kwargs": { + "clear_thinking": "yes" + }, + "expected": "[gMASK]<|system|>Reasoning Effort: Max<|user|>你好,GLM!<|assistant|>答案<|user|>继续<|assistant|>新思考<|assistant|>" + }, + { + "name": "clear_0", + "messages": [ + { + "role": "user", + "content": "你好,GLM!" + }, + { + "role": "assistant", + "content": " 答案 \n", + "reasoning_content": "旧思考" + }, + { + "role": "user", + "content": "继续" + }, + { + "role": "assistant", + "content": "", + "reasoning_content": "新思考" + } + ], + "tools": [], + "add_generation_prompt": true, + "kwargs": { + "clear_thinking": 0 + }, + "expected": "[gMASK]<|system|>Reasoning Effort: Max<|user|>你好,GLM!<|assistant|>旧思考答案<|user|>继续<|assistant|>新思考<|assistant|>" + }, + { + "name": "clear_1", + "messages": [ + { + "role": "user", + "content": "你好,GLM!" + }, + { + "role": "assistant", + "content": " 答案 \n", + "reasoning_content": "旧思考" + }, + { + "role": "user", + "content": "继续" + }, + { + "role": "assistant", + "content": "", + "reasoning_content": "新思考" + } + ], + "tools": [], + "add_generation_prompt": true, + "kwargs": { + "clear_thinking": 1 + }, + "expected": "[gMASK]<|system|>Reasoning Effort: Max<|user|>你好,GLM!<|assistant|>答案<|user|>继续<|assistant|>新思考<|assistant|>" + }, + { + "name": "clear_[]", + "messages": [ + { + "role": "user", + "content": "你好,GLM!" + }, + { + "role": "assistant", + "content": " 答案 \n", + "reasoning_content": "旧思考" + }, + { + "role": "user", + "content": "继续" + }, + { + "role": "assistant", + "content": "", + "reasoning_content": "新思考" + } + ], + "tools": [], + "add_generation_prompt": true, + "kwargs": { + "clear_thinking": [] + }, + "expected": "[gMASK]<|system|>Reasoning Effort: Max<|user|>你好,GLM!<|assistant|>旧思考答案<|user|>继续<|assistant|>新思考<|assistant|>" + }, + { + "name": "clear_[1]", + "messages": [ + { + "role": "user", + "content": "你好,GLM!" + }, + { + "role": "assistant", + "content": " 答案 \n", + "reasoning_content": "旧思考" + }, + { + "role": "user", + "content": "继续" + }, + { + "role": "assistant", + "content": "", + "reasoning_content": "新思考" + } + ], + "tools": [], + "add_generation_prompt": true, + "kwargs": { + "clear_thinking": [ + 1 + ] + }, + "expected": "[gMASK]<|system|>Reasoning Effort: Max<|user|>你好,GLM!<|assistant|>答案<|user|>继续<|assistant|>新思考<|assistant|>" + }, + { + "name": "no_user_preserves_reasoning", + "messages": [ + { + "role": "assistant", + "content": " 答案 \n", + "reasoning_content": "旧思考" + } + ], + "tools": [], + "add_generation_prompt": true, + "kwargs": { + "clear_thinking": true + }, + "expected": "[gMASK]<|system|>Reasoning Effort: Max<|assistant|>旧思考答案<|assistant|>" + }, + { + "name": "embedded_reasoning_None", + "messages": [ + { + "role": "user", + "content": "你好,GLM!" + }, + { + "role": "assistant", + "content": "prefixfirstdiscard answer " + } + ], + "tools": [], + "add_generation_prompt": true, + "kwargs": {}, + "expected": "[gMASK]<|system|>Reasoning Effort: Max<|user|>你好,GLM!<|assistant|>firstanswer<|assistant|>" + }, + { + "name": "embedded_reasoning_", + "messages": [ + { + "role": "user", + "content": "你好,GLM!" + }, + { + "role": "assistant", + "content": "prefixfirstdiscard answer ", + "reasoning_content": "" + } + ], + "tools": [], + "add_generation_prompt": true, + "kwargs": {}, + "expected": "[gMASK]<|system|>Reasoning Effort: Max<|user|>你好,GLM!<|assistant|>prefixfirstdiscard answer<|assistant|>" + }, + { + "name": "embedded_reasoning_explicit", + "messages": [ + { + "role": "user", + "content": "你好,GLM!" + }, + { + "role": "assistant", + "content": "prefixfirstdiscard answer ", + "reasoning_content": "explicit" + } + ], + "tools": [], + "add_generation_prompt": true, + "kwargs": {}, + "expected": "[gMASK]<|system|>Reasoning Effort: Max<|user|>你好,GLM!<|assistant|>explicitprefixfirstdiscard answer<|assistant|>" + }, + { + "name": "tools", + "messages": [ + { + "role": "user", + "content": "你好,GLM!" + } + ], + "tools": [ + { + "type": "function", + "function": { + "name": "lookup", + "description": "查找<&>", + "parameters": { + "type": "object", + "properties": { + "q": { + "type": "string" + } + }, + "required": [ + "q" + ] + }, + "strict": true + } + }, + { + "function": { + "name": "hidden", + "description": null, + "parameters": {}, + "defer_loading": true + } + } + ], + "add_generation_prompt": true, + "kwargs": {}, + "expected": "[gMASK]<|system|>Reasoning Effort: Max<|system|>\n# Tools\n\nYou may call one or more functions to assist with the user query.\n\nYou are provided with function signatures within XML tags:\n\n{\"name\": \"lookup\", \"description\": \"查找<&>\", \"parameters\": {\"type\": \"object\", \"properties\": {\"q\": {\"type\": \"string\"}}, \"required\": [\"q\"]}}\n\n\nFor each function call, output the function name and arguments within the following XML format:\n{function-name}{arg-key-1}{arg-value-1}{arg-key-2}{arg-value-2}...<|user|>你好,GLM!<|assistant|>" + }, + { + "name": "flat_tools", + "messages": [ + { + "role": "user", + "content": "你好,GLM!" + } + ], + "tools": [ + { + "name": "lookup", + "description": "查找<&>", + "parameters": { + "type": "object", + "properties": { + "q": { + "type": "string" + } + }, + "required": [ + "q" + ] + }, + "strict": true + }, + { + "name": "hidden", + "description": null, + "parameters": {}, + "defer_loading": true + } + ], + "add_generation_prompt": true, + "kwargs": {}, + "expected": "[gMASK]<|system|>Reasoning Effort: Max<|system|>\n# Tools\n\nYou may call one or more functions to assist with the user query.\n\nYou are provided with function signatures within XML tags:\n\n{\"name\": \"lookup\", \"description\": \"查找<&>\", \"parameters\": {\"type\": \"object\", \"properties\": {\"q\": {\"type\": \"string\"}}, \"required\": [\"q\"]}}\n\n\nFor each function call, output the function name and arguments within the following XML format:\n{function-name}{arg-key-1}{arg-value-1}{arg-key-2}{arg-value-2}...<|user|>你好,GLM!<|assistant|>" + }, + { + "name": "all_deferred", + "messages": [ + { + "role": "user", + "content": "你好,GLM!" + } + ], + "tools": [ + { + "function": { + "name": "hidden", + "description": null, + "parameters": {}, + "defer_loading": true + } + } + ], + "add_generation_prompt": true, + "kwargs": {}, + "expected": "[gMASK]<|system|>Reasoning Effort: Max<|system|>\n# Tools\n\nYou may call one or more functions to assist with the user query.\n\nYou are provided with function signatures within XML tags:\n\n\n\nFor each function call, output the function name and arguments within the following XML format:\n{function-name}{arg-key-1}{arg-value-1}{arg-key-2}{arg-value-2}...<|user|>你好,GLM!<|assistant|>" + }, + { + "name": "raw_schema_key", + "messages": [ + { + "role": "user", + "content": "你好,GLM!" + } + ], + "tools": [ + { + "name": "odd", + "a\"b": "value" + } + ], + "add_generation_prompt": true, + "kwargs": {}, + "expected": "[gMASK]<|system|>Reasoning Effort: Max<|system|>\n# Tools\n\nYou may call one or more functions to assist with the user query.\n\nYou are provided with function signatures within XML tags:\n\n{\"name\": \"odd\", \"a\"b\": \"value\"}\n\n\nFor each function call, output the function name and arguments within the following XML format:\n{function-name}{arg-key-1}{arg-value-1}{arg-key-2}{arg-value-2}...<|user|>你好,GLM!<|assistant|>" + }, + { + "name": "tool_order", + "messages": [ + { + "role": "user", + "content": "你好,GLM!" + }, + { + "role": "assistant", + "content": "", + "reasoning_content": "search", + "tool_calls": [ + { + "id": "a", + "function": { + "name": "lookup", + "arguments": { + "q": "中文<&>\n", + "list": [ + 1, + true, + null + ], + "object": { + "b": 2, + "a": "x" + } + } + } + }, + { + "id": "b", + "function": { + "name": "lookup", + "arguments": { + "q": "second", + "float": 1.5 + } + } + } + ] + }, + { + "role": "tool", + "tool_call_id": "b", + "content": "B" + }, + { + "role": "tool", + "tool_call_id": "a", + "content": "A" + } + ], + "tools": [ + { + "type": "function", + "function": { + "name": "lookup", + "description": "查找<&>", + "parameters": { + "type": "object", + "properties": { + "q": { + "type": "string" + } + }, + "required": [ + "q" + ] + }, + "strict": true + } + }, + { + "function": { + "name": "hidden", + "description": null, + "parameters": {}, + "defer_loading": true + } + } + ], + "add_generation_prompt": true, + "kwargs": {}, + "expected": "[gMASK]<|system|>Reasoning Effort: Max<|system|>\n# Tools\n\nYou may call one or more functions to assist with the user query.\n\nYou are provided with function signatures within XML tags:\n\n{\"name\": \"lookup\", \"description\": \"查找<&>\", \"parameters\": {\"type\": \"object\", \"properties\": {\"q\": {\"type\": \"string\"}}, \"required\": [\"q\"]}}\n\n\nFor each function call, output the function name and arguments within the following XML format:\n{function-name}{arg-key-1}{arg-value-1}{arg-key-2}{arg-value-2}...<|user|>你好,GLM!<|assistant|>searchlookupq中文<&>\nlist[1, true, null]object{\"b\": 2, \"a\": \"x\"}lookupqsecondfloat1.5<|observation|>AB<|assistant|>" + }, + { + "name": "string_arguments", + "messages": [ + { + "role": "user", + "content": "你好,GLM!" + }, + { + "role": "assistant", + "content": "", + "reasoning_content": "search", + "tool_calls": [ + { + "id": "a", + "function": { + "name": "lookup", + "arguments": "{\"q\": \"\\u4e2d\\u6587<&>\\n\", \"list\": [1, true, null], \"object\": {\"b\": 2, \"a\": \"x\"}}" + } + }, + { + "id": "b", + "function": { + "name": "lookup", + "arguments": "{\"q\": \"second\", \"float\": 1.5}" + } + } + ] + }, + { + "role": "tool", + "tool_call_id": "b", + "content": "B" + }, + { + "role": "tool", + "tool_call_id": "a", + "content": "A" + } + ], + "tools": [ + { + "type": "function", + "function": { + "name": "lookup", + "description": "查找<&>", + "parameters": { + "type": "object", + "properties": { + "q": { + "type": "string" + } + }, + "required": [ + "q" + ] + }, + "strict": true + } + }, + { + "function": { + "name": "hidden", + "description": null, + "parameters": {}, + "defer_loading": true + } + } + ], + "add_generation_prompt": true, + "kwargs": {}, + "expected": "[gMASK]<|system|>Reasoning Effort: Max<|system|>\n# Tools\n\nYou may call one or more functions to assist with the user query.\n\nYou are provided with function signatures within XML tags:\n\n{\"name\": \"lookup\", \"description\": \"查找<&>\", \"parameters\": {\"type\": \"object\", \"properties\": {\"q\": {\"type\": \"string\"}}, \"required\": [\"q\"]}}\n\n\nFor each function call, output the function name and arguments within the following XML format:\n{function-name}{arg-key-1}{arg-value-1}{arg-key-2}{arg-value-2}...<|user|>你好,GLM!<|assistant|>searchlookupq中文<&>\nlist[1, true, null]object{\"b\": 2, \"a\": \"x\"}lookupqsecondfloat1.5<|observation|>AB<|assistant|>" + }, + { + "name": "duplicate_response", + "messages": [ + { + "role": "user", + "content": "你好,GLM!" + }, + { + "role": "assistant", + "content": "", + "reasoning_content": "search", + "tool_calls": [ + { + "id": "a", + "function": { + "name": "lookup", + "arguments": { + "q": "中文<&>\n", + "list": [ + 1, + true, + null + ], + "object": { + "b": 2, + "a": "x" + } + } + } + }, + { + "id": "b", + "function": { + "name": "lookup", + "arguments": { + "q": "second", + "float": 1.5 + } + } + } + ] + }, + { + "role": "tool", + "tool_call_id": "b", + "content": "B" + }, + { + "role": "tool", + "tool_call_id": "a", + "content": "A" + }, + { + "role": "tool", + "tool_call_id": "b", + "content": "B" + } + ], + "tools": [ + { + "type": "function", + "function": { + "name": "lookup", + "description": "查找<&>", + "parameters": { + "type": "object", + "properties": { + "q": { + "type": "string" + } + }, + "required": [ + "q" + ] + }, + "strict": true + } + }, + { + "function": { + "name": "hidden", + "description": null, + "parameters": {}, + "defer_loading": true + } + } + ], + "add_generation_prompt": true, + "kwargs": {}, + "expected": "[gMASK]<|system|>Reasoning Effort: Max<|system|>\n# Tools\n\nYou may call one or more functions to assist with the user query.\n\nYou are provided with function signatures within XML tags:\n\n{\"name\": \"lookup\", \"description\": \"查找<&>\", \"parameters\": {\"type\": \"object\", \"properties\": {\"q\": {\"type\": \"string\"}}, \"required\": [\"q\"]}}\n\n\nFor each function call, output the function name and arguments within the following XML format:\n{function-name}{arg-key-1}{arg-value-1}{arg-key-2}{arg-value-2}...<|user|>你好,GLM!<|assistant|>searchlookupq中文<&>\nlist[1, true, null]object{\"b\": 2, \"a\": \"x\"}lookupqsecondfloat1.5<|observation|>BAB<|assistant|>" + }, + { + "name": "unknown_id", + "messages": [ + { + "role": "user", + "content": "你好,GLM!" + }, + { + "role": "assistant", + "content": "", + "reasoning_content": "search", + "tool_calls": [ + { + "id": "a", + "function": { + "name": "lookup", + "arguments": { + "q": "中文<&>\n", + "list": [ + 1, + true, + null + ], + "object": { + "b": 2, + "a": "x" + } + } + } + }, + { + "id": "b", + "function": { + "name": "lookup", + "arguments": { + "q": "second", + "float": 1.5 + } + } + } + ] + }, + { + "role": "tool", + "tool_call_id": "b", + "content": "B" + }, + { + "role": "tool", + "tool_call_id": "a", + "content": "A" + }, + { + "role": "tool", + "tool_call_id": "unknown", + "content": "?" + } + ], + "tools": [ + { + "type": "function", + "function": { + "name": "lookup", + "description": "查找<&>", + "parameters": { + "type": "object", + "properties": { + "q": { + "type": "string" + } + }, + "required": [ + "q" + ] + }, + "strict": true + } + }, + { + "function": { + "name": "hidden", + "description": null, + "parameters": {}, + "defer_loading": true + } + } + ], + "add_generation_prompt": true, + "kwargs": {}, + "expected": "[gMASK]<|system|>Reasoning Effort: Max<|system|>\n# Tools\n\nYou may call one or more functions to assist with the user query.\n\nYou are provided with function signatures within XML tags:\n\n{\"name\": \"lookup\", \"description\": \"查找<&>\", \"parameters\": {\"type\": \"object\", \"properties\": {\"q\": {\"type\": \"string\"}}, \"required\": [\"q\"]}}\n\n\nFor each function call, output the function name and arguments within the following XML format:\n{function-name}{arg-key-1}{arg-value-1}{arg-key-2}{arg-value-2}...<|user|>你好,GLM!<|assistant|>searchlookupq中文<&>\nlist[1, true, null]object{\"b\": 2, \"a\": \"x\"}lookupqsecondfloat1.5<|observation|>BA?<|assistant|>" + }, + { + "name": "missing_id", + "messages": [ + { + "role": "user", + "content": "你好,GLM!" + }, + { + "role": "assistant", + "content": "", + "reasoning_content": "search", + "tool_calls": [ + { + "id": "a", + "function": { + "name": "lookup", + "arguments": { + "q": "中文<&>\n", + "list": [ + 1, + true, + null + ], + "object": { + "b": 2, + "a": "x" + } + } + } + }, + { + "id": "b", + "function": { + "name": "lookup", + "arguments": { + "q": "second", + "float": 1.5 + } + } + } + ] + }, + { + "role": "tool", + "tool_call_id": "b", + "content": "B" + }, + { + "role": "tool", + "tool_call_id": "a", + "content": "A" + }, + { + "role": "tool", + "content": "missing" + } + ], + "tools": [ + { + "type": "function", + "function": { + "name": "lookup", + "description": "查找<&>", + "parameters": { + "type": "object", + "properties": { + "q": { + "type": "string" + } + }, + "required": [ + "q" + ] + }, + "strict": true + } + }, + { + "function": { + "name": "hidden", + "description": null, + "parameters": {}, + "defer_loading": true + } + } + ], + "add_generation_prompt": true, + "kwargs": {}, + "expected": "[gMASK]<|system|>Reasoning Effort: Max<|system|>\n# Tools\n\nYou may call one or more functions to assist with the user query.\n\nYou are provided with function signatures within XML tags:\n\n{\"name\": \"lookup\", \"description\": \"查找<&>\", \"parameters\": {\"type\": \"object\", \"properties\": {\"q\": {\"type\": \"string\"}}, \"required\": [\"q\"]}}\n\n\nFor each function call, output the function name and arguments within the following XML format:\n{function-name}{arg-key-1}{arg-value-1}{arg-key-2}{arg-value-2}...<|user|>你好,GLM!<|assistant|>searchlookupq中文<&>\nlist[1, true, null]object{\"b\": 2, \"a\": \"x\"}lookupqsecondfloat1.5<|observation|>BAmissing<|assistant|>" + }, + { + "name": "empty_id", + "messages": [ + { + "role": "user", + "content": "你好,GLM!" + }, + { + "role": "assistant", + "content": "", + "reasoning_content": "search", + "tool_calls": [ + { + "id": "a", + "function": { + "name": "lookup", + "arguments": { + "q": "中文<&>\n", + "list": [ + 1, + true, + null + ], + "object": { + "b": 2, + "a": "x" + } + } + } + }, + { + "id": "b", + "function": { + "name": "lookup", + "arguments": { + "q": "second", + "float": 1.5 + } + } + } + ] + }, + { + "role": "tool", + "tool_call_id": "", + "content": "empty" + }, + { + "role": "tool", + "tool_call_id": "b", + "content": "B" + }, + { + "role": "tool", + "tool_call_id": "a", + "content": "A" + } + ], + "tools": [ + { + "type": "function", + "function": { + "name": "lookup", + "description": "查找<&>", + "parameters": { + "type": "object", + "properties": { + "q": { + "type": "string" + } + }, + "required": [ + "q" + ] + }, + "strict": true + } + }, + { + "function": { + "name": "hidden", + "description": null, + "parameters": {}, + "defer_loading": true + } + } + ], + "add_generation_prompt": true, + "kwargs": {}, + "expected": "[gMASK]<|system|>Reasoning Effort: Max<|system|>\n# Tools\n\nYou may call one or more functions to assist with the user query.\n\nYou are provided with function signatures within XML tags:\n\n{\"name\": \"lookup\", \"description\": \"查找<&>\", \"parameters\": {\"type\": \"object\", \"properties\": {\"q\": {\"type\": \"string\"}}, \"required\": [\"q\"]}}\n\n\nFor each function call, output the function name and arguments within the following XML format:\n{function-name}{arg-key-1}{arg-value-1}{arg-key-2}{arg-value-2}...<|user|>你好,GLM!<|assistant|>searchlookupq中文<&>\nlist[1, true, null]object{\"b\": 2, \"a\": \"x\"}lookupqsecondfloat1.5<|observation|>emptyBA<|assistant|>" + }, + { + "name": "id_alias", + "messages": [ + { + "role": "user", + "content": "你好,GLM!" + }, + { + "role": "assistant", + "content": "", + "reasoning_content": "search", + "tool_calls": [ + { + "id": "a", + "function": { + "name": "lookup", + "arguments": { + "q": "中文<&>\n", + "list": [ + 1, + true, + null + ], + "object": { + "b": 2, + "a": "x" + } + } + } + }, + { + "id": "b", + "function": { + "name": "lookup", + "arguments": { + "q": "second", + "float": 1.5 + } + } + } + ] + }, + { + "role": "tool", + "id": "b", + "content": "B" + }, + { + "role": "tool", + "id": "a", + "content": "A" + } + ], + "tools": [ + { + "type": "function", + "function": { + "name": "lookup", + "description": "查找<&>", + "parameters": { + "type": "object", + "properties": { + "q": { + "type": "string" + } + }, + "required": [ + "q" + ] + }, + "strict": true + } + }, + { + "function": { + "name": "hidden", + "description": null, + "parameters": {}, + "defer_loading": true + } + } + ], + "add_generation_prompt": true, + "kwargs": {}, + "expected": "[gMASK]<|system|>Reasoning Effort: Max<|system|>\n# Tools\n\nYou may call one or more functions to assist with the user query.\n\nYou are provided with function signatures within XML tags:\n\n{\"name\": \"lookup\", \"description\": \"查找<&>\", \"parameters\": {\"type\": \"object\", \"properties\": {\"q\": {\"type\": \"string\"}}, \"required\": [\"q\"]}}\n\n\nFor each function call, output the function name and arguments within the following XML format:\n{function-name}{arg-key-1}{arg-value-1}{arg-key-2}{arg-value-2}...<|user|>你好,GLM!<|assistant|>searchlookupq中文<&>\nlist[1, true, null]object{\"b\": 2, \"a\": \"x\"}lookupqsecondfloat1.5<|observation|>AB<|assistant|>" + }, + { + "name": "unanswered_call", + "messages": [ + { + "role": "user", + "content": "你好,GLM!" + }, + { + "role": "assistant", + "content": "", + "reasoning_content": "search", + "tool_calls": [ + { + "id": "a", + "function": { + "name": "lookup", + "arguments": { + "q": "中文<&>\n", + "list": [ + 1, + true, + null + ], + "object": { + "b": 2, + "a": "x" + } + } + } + }, + { + "id": "b", + "function": { + "name": "lookup", + "arguments": { + "q": "second", + "float": 1.5 + } + } + } + ] + }, + { + "role": "tool", + "tool_call_id": "b", + "content": "B" + } + ], + "tools": [ + { + "type": "function", + "function": { + "name": "lookup", + "description": "查找<&>", + "parameters": { + "type": "object", + "properties": { + "q": { + "type": "string" + } + }, + "required": [ + "q" + ] + }, + "strict": true + } + }, + { + "function": { + "name": "hidden", + "description": null, + "parameters": {}, + "defer_loading": true + } + } + ], + "add_generation_prompt": true, + "kwargs": {}, + "expected": "[gMASK]<|system|>Reasoning Effort: Max<|system|>\n# Tools\n\nYou may call one or more functions to assist with the user query.\n\nYou are provided with function signatures within XML tags:\n\n{\"name\": \"lookup\", \"description\": \"查找<&>\", \"parameters\": {\"type\": \"object\", \"properties\": {\"q\": {\"type\": \"string\"}}, \"required\": [\"q\"]}}\n\n\nFor each function call, output the function name and arguments within the following XML format:\n{function-name}{arg-key-1}{arg-value-1}{arg-key-2}{arg-value-2}...<|user|>你好,GLM!<|assistant|>searchlookupq中文<&>\nlist[1, true, null]object{\"b\": 2, \"a\": \"x\"}lookupqsecondfloat1.5<|observation|>B<|assistant|>" + }, + { + "name": "null_result", + "messages": [ + { + "role": "user", + "content": "你好,GLM!" + }, + { + "role": "assistant", + "content": "", + "reasoning_content": "search", + "tool_calls": [ + { + "id": "a", + "function": { + "name": "lookup", + "arguments": { + "q": "中文<&>\n", + "list": [ + 1, + true, + null + ], + "object": { + "b": 2, + "a": "x" + } + } + } + }, + { + "id": "b", + "function": { + "name": "lookup", + "arguments": { + "q": "second", + "float": 1.5 + } + } + } + ] + }, + { + "role": "tool", + "tool_call_id": "a", + "content": null + } + ], + "tools": [ + { + "type": "function", + "function": { + "name": "lookup", + "description": "查找<&>", + "parameters": { + "type": "object", + "properties": { + "q": { + "type": "string" + } + }, + "required": [ + "q" + ] + }, + "strict": true + } + }, + { + "function": { + "name": "hidden", + "description": null, + "parameters": {}, + "defer_loading": true + } + } + ], + "add_generation_prompt": true, + "kwargs": {}, + "expected": "[gMASK]<|system|>Reasoning Effort: Max<|system|>\n# Tools\n\nYou may call one or more functions to assist with the user query.\n\nYou are provided with function signatures within XML tags:\n\n{\"name\": \"lookup\", \"description\": \"查找<&>\", \"parameters\": {\"type\": \"object\", \"properties\": {\"q\": {\"type\": \"string\"}}, \"required\": [\"q\"]}}\n\n\nFor each function call, output the function name and arguments within the following XML format:\n{function-name}{arg-key-1}{arg-value-1}{arg-key-2}{arg-value-2}...<|user|>你好,GLM!<|assistant|>searchlookupq中文<&>\nlist[1, true, null]object{\"b\": 2, \"a\": \"x\"}lookupqsecondfloat1.5<|observation|><|assistant|>" + }, + { + "name": "missing_result", + "messages": [ + { + "role": "user", + "content": "你好,GLM!" + }, + { + "role": "assistant", + "content": "", + "reasoning_content": "search", + "tool_calls": [ + { + "id": "a", + "function": { + "name": "lookup", + "arguments": { + "q": "中文<&>\n", + "list": [ + 1, + true, + null + ], + "object": { + "b": 2, + "a": "x" + } + } + } + }, + { + "id": "b", + "function": { + "name": "lookup", + "arguments": { + "q": "second", + "float": 1.5 + } + } + } + ] + }, + { + "role": "tool", + "tool_call_id": "a" + } + ], + "tools": [ + { + "type": "function", + "function": { + "name": "lookup", + "description": "查找<&>", + "parameters": { + "type": "object", + "properties": { + "q": { + "type": "string" + } + }, + "required": [ + "q" + ] + }, + "strict": true + } + }, + { + "function": { + "name": "hidden", + "description": null, + "parameters": {}, + "defer_loading": true + } + } + ], + "add_generation_prompt": true, + "kwargs": {}, + "expected": "[gMASK]<|system|>Reasoning Effort: Max<|system|>\n# Tools\n\nYou may call one or more functions to assist with the user query.\n\nYou are provided with function signatures within XML tags:\n\n{\"name\": \"lookup\", \"description\": \"查找<&>\", \"parameters\": {\"type\": \"object\", \"properties\": {\"q\": {\"type\": \"string\"}}, \"required\": [\"q\"]}}\n\n\nFor each function call, output the function name and arguments within the following XML format:\n{function-name}{arg-key-1}{arg-value-1}{arg-key-2}{arg-value-2}...<|user|>你好,GLM!<|assistant|>searchlookupq中文<&>\nlist[1, true, null]object{\"b\": 2, \"a\": \"x\"}lookupqsecondfloat1.5<|observation|><|assistant|>" + }, + { + "name": "bundled", + "messages": [ + { + "role": "user", + "content": "你好,GLM!" + }, + { + "role": "assistant", + "content": "", + "reasoning_content": "search", + "tool_calls": [ + { + "id": "a", + "function": { + "name": "lookup", + "arguments": { + "q": "中文<&>\n", + "list": [ + 1, + true, + null + ], + "object": { + "b": 2, + "a": "x" + } + } + } + }, + { + "id": "b", + "function": { + "name": "lookup", + "arguments": { + "q": "second", + "float": 1.5 + } + } + } + ] + }, + { + "role": "tool", + "content": [ + { + "id": "b", + "output": "B" + }, + { + "id": "a", + "output": [ + { + "type": "text", + "text": "A" + } + ] + } + ] + } + ], + "tools": [ + { + "type": "function", + "function": { + "name": "lookup", + "description": "查找<&>", + "parameters": { + "type": "object", + "properties": { + "q": { + "type": "string" + } + }, + "required": [ + "q" + ] + }, + "strict": true + } + }, + { + "function": { + "name": "hidden", + "description": null, + "parameters": {}, + "defer_loading": true + } + } + ], + "add_generation_prompt": true, + "kwargs": {}, + "expected": "[gMASK]<|system|>Reasoning Effort: Max<|system|>\n# Tools\n\nYou may call one or more functions to assist with the user query.\n\nYou are provided with function signatures within XML tags:\n\n{\"name\": \"lookup\", \"description\": \"查找<&>\", \"parameters\": {\"type\": \"object\", \"properties\": {\"q\": {\"type\": \"string\"}}, \"required\": [\"q\"]}}\n\n\nFor each function call, output the function name and arguments within the following XML format:\n{function-name}{arg-key-1}{arg-value-1}{arg-key-2}{arg-value-2}...<|user|>你好,GLM!<|assistant|>searchlookupq中文<&>\nlist[1, true, null]object{\"b\": 2, \"a\": \"x\"}lookupqsecondfloat1.5<|observation|>AB<|assistant|>" + }, + { + "name": "references", + "messages": [ + { + "role": "user", + "content": "你好,GLM!" + }, + { + "role": "assistant", + "content": "", + "reasoning_content": "search", + "tool_calls": [ + { + "id": "a", + "function": { + "name": "lookup", + "arguments": { + "q": "中文<&>\n", + "list": [ + 1, + true, + null + ], + "object": { + "b": 2, + "a": "x" + } + } + } + }, + { + "id": "b", + "function": { + "name": "lookup", + "arguments": { + "q": "second", + "float": 1.5 + } + } + } + ] + }, + { + "role": "tool", + "tool_call_id": "a", + "content": [ + { + "type": "tool_reference", + "name": "hidden" + } + ] + } + ], + "tools": [ + { + "type": "function", + "function": { + "name": "lookup", + "description": "查找<&>", + "parameters": { + "type": "object", + "properties": { + "q": { + "type": "string" + } + }, + "required": [ + "q" + ] + }, + "strict": true + } + }, + { + "function": { + "name": "hidden", + "description": null, + "parameters": {}, + "defer_loading": true + } + } + ], + "add_generation_prompt": true, + "kwargs": {}, + "expected": "[gMASK]<|system|>Reasoning Effort: Max<|system|>\n# Tools\n\nYou may call one or more functions to assist with the user query.\n\nYou are provided with function signatures within XML tags:\n\n{\"name\": \"lookup\", \"description\": \"查找<&>\", \"parameters\": {\"type\": \"object\", \"properties\": {\"q\": {\"type\": \"string\"}}, \"required\": [\"q\"]}}\n\n\nFor each function call, output the function name and arguments within the following XML format:\n{function-name}{arg-key-1}{arg-value-1}{arg-key-2}{arg-value-2}...<|user|>你好,GLM!<|assistant|>searchlookupq中文<&>\nlist[1, true, null]object{\"b\": 2, \"a\": \"x\"}lookupqsecondfloat1.5<|observation|>\n{\"name\": \"hidden\", \"description\": null, \"parameters\": {}}\n<|assistant|>" + }, + { + "name": "bundled_references", + "messages": [ + { + "role": "user", + "content": "你好,GLM!" + }, + { + "role": "assistant", + "content": "", + "reasoning_content": "search", + "tool_calls": [ + { + "id": "a", + "function": { + "name": "lookup", + "arguments": { + "q": "中文<&>\n", + "list": [ + 1, + true, + null + ], + "object": { + "b": 2, + "a": "x" + } + } + } + }, + { + "id": "b", + "function": { + "name": "lookup", + "arguments": { + "q": "second", + "float": 1.5 + } + } + } + ] + }, + { + "role": "tool", + "content": [ + { + "id": "a", + "output": [ + { + "type": "tool_reference", + "name": "hidden" + } + ] + } + ] + } + ], + "tools": [ + { + "type": "function", + "function": { + "name": "lookup", + "description": "查找<&>", + "parameters": { + "type": "object", + "properties": { + "q": { + "type": "string" + } + }, + "required": [ + "q" + ] + }, + "strict": true + } + }, + { + "function": { + "name": "hidden", + "description": null, + "parameters": {}, + "defer_loading": true + } + } + ], + "add_generation_prompt": true, + "kwargs": {}, + "expected": "[gMASK]<|system|>Reasoning Effort: Max<|system|>\n# Tools\n\nYou may call one or more functions to assist with the user query.\n\nYou are provided with function signatures within XML tags:\n\n{\"name\": \"lookup\", \"description\": \"查找<&>\", \"parameters\": {\"type\": \"object\", \"properties\": {\"q\": {\"type\": \"string\"}}, \"required\": [\"q\"]}}\n\n\nFor each function call, output the function name and arguments within the following XML format:\n{function-name}{arg-key-1}{arg-value-1}{arg-key-2}{arg-value-2}...<|user|>你好,GLM!<|assistant|>searchlookupq中文<&>\nlist[1, true, null]object{\"b\": 2, \"a\": \"x\"}lookupqsecondfloat1.5<|observation|>\n{\"name\": \"hidden\", \"description\": null, \"parameters\": {}}\n<|assistant|>" + }, + { + "name": "orphan_tool_block", + "messages": [ + { + "role": "tool", + "tool_call_id": "b", + "content": "B" + }, + { + "role": "tool", + "tool_call_id": "a", + "content": "A" + } + ], + "tools": [ + { + "type": "function", + "function": { + "name": "lookup", + "description": "查找<&>", + "parameters": { + "type": "object", + "properties": { + "q": { + "type": "string" + } + }, + "required": [ + "q" + ] + }, + "strict": true + } + }, + { + "function": { + "name": "hidden", + "description": null, + "parameters": {}, + "defer_loading": true + } + } + ], + "add_generation_prompt": true, + "kwargs": {}, + "expected": "[gMASK]<|system|>Reasoning Effort: Max<|system|>\n# Tools\n\nYou may call one or more functions to assist with the user query.\n\nYou are provided with function signatures within XML tags:\n\n{\"name\": \"lookup\", \"description\": \"查找<&>\", \"parameters\": {\"type\": \"object\", \"properties\": {\"q\": {\"type\": \"string\"}}, \"required\": [\"q\"]}}\n\n\nFor each function call, output the function name and arguments within the following XML format:\n{function-name}{arg-key-1}{arg-value-1}{arg-key-2}{arg-value-2}...<|observation|>BA<|assistant|>" + }, + { + "name": "duplicate_call", + "messages": [ + { + "role": "user", + "content": "你好,GLM!" + }, + { + "role": "assistant", + "content": "", + "reasoning_content": "search", + "tool_calls": [ + { + "id": "a", + "function": { + "name": "lookup", + "arguments": { + "q": "中文<&>\n", + "list": [ + 1, + true, + null + ], + "object": { + "b": 2, + "a": "x" + } + } + } + }, + { + "id": "a", + "function": { + "name": "lookup", + "arguments": { + "q": "second", + "float": 1.5 + } + } + } + ] + }, + { + "role": "tool", + "tool_call_id": "b", + "content": "B" + }, + { + "role": "tool", + "tool_call_id": "a", + "content": "A" + } + ], + "tools": [ + { + "type": "function", + "function": { + "name": "lookup", + "description": "查找<&>", + "parameters": { + "type": "object", + "properties": { + "q": { + "type": "string" + } + }, + "required": [ + "q" + ] + }, + "strict": true + } + }, + { + "function": { + "name": "hidden", + "description": null, + "parameters": {}, + "defer_loading": true + } + } + ], + "add_generation_prompt": true, + "kwargs": {}, + "expected": "[gMASK]<|system|>Reasoning Effort: Max<|system|>\n# Tools\n\nYou may call one or more functions to assist with the user query.\n\nYou are provided with function signatures within XML tags:\n\n{\"name\": \"lookup\", \"description\": \"查找<&>\", \"parameters\": {\"type\": \"object\", \"properties\": {\"q\": {\"type\": \"string\"}}, \"required\": [\"q\"]}}\n\n\nFor each function call, output the function name and arguments within the following XML format:\n{function-name}{arg-key-1}{arg-value-1}{arg-key-2}{arg-value-2}...<|user|>你好,GLM!<|assistant|>searchlookupq中文<&>\nlist[1, true, null]object{\"b\": 2, \"a\": \"x\"}lookupqsecondfloat1.5<|observation|>BA<|assistant|>" + }, + { + "name": "missing_call", + "messages": [ + { + "role": "user", + "content": "你好,GLM!" + }, + { + "role": "assistant", + "content": "", + "reasoning_content": "search", + "tool_calls": [ + { + "id": "a", + "function": { + "name": "lookup", + "arguments": { + "q": "中文<&>\n", + "list": [ + 1, + true, + null + ], + "object": { + "b": 2, + "a": "x" + } + } + } + }, + { + "id": null, + "function": { + "name": "lookup", + "arguments": { + "q": "second", + "float": 1.5 + } + } + } + ] + }, + { + "role": "tool", + "tool_call_id": "b", + "content": "B" + }, + { + "role": "tool", + "tool_call_id": "a", + "content": "A" + } + ], + "tools": [ + { + "type": "function", + "function": { + "name": "lookup", + "description": "查找<&>", + "parameters": { + "type": "object", + "properties": { + "q": { + "type": "string" + } + }, + "required": [ + "q" + ] + }, + "strict": true + } + }, + { + "function": { + "name": "hidden", + "description": null, + "parameters": {}, + "defer_loading": true + } + } + ], + "add_generation_prompt": true, + "kwargs": {}, + "expected": "[gMASK]<|system|>Reasoning Effort: Max<|system|>\n# Tools\n\nYou may call one or more functions to assist with the user query.\n\nYou are provided with function signatures within XML tags:\n\n{\"name\": \"lookup\", \"description\": \"查找<&>\", \"parameters\": {\"type\": \"object\", \"properties\": {\"q\": {\"type\": \"string\"}}, \"required\": [\"q\"]}}\n\n\nFor each function call, output the function name and arguments within the following XML format:\n{function-name}{arg-key-1}{arg-value-1}{arg-key-2}{arg-value-2}...<|user|>你好,GLM!<|assistant|>searchlookupq中文<&>\nlist[1, true, null]object{\"b\": 2, \"a\": \"x\"}lookupqsecondfloat1.5<|observation|>BA<|assistant|>" + }, + { + "name": "multiple_blocks", + "messages": [ + { + "role": "user", + "content": "你好,GLM!" + }, + { + "role": "assistant", + "content": "", + "reasoning_content": "search", + "tool_calls": [ + { + "id": "a", + "function": { + "name": "lookup", + "arguments": { + "q": "中文<&>\n", + "list": [ + 1, + true, + null + ], + "object": { + "b": 2, + "a": "x" + } + } + } + }, + { + "id": "b", + "function": { + "name": "lookup", + "arguments": { + "q": "second", + "float": 1.5 + } + } + } + ] + }, + { + "role": "tool", + "tool_call_id": "b", + "content": "B" + }, + { + "role": "tool", + "tool_call_id": "a", + "content": "A" + }, + { + "role": "user", + "content": "你好,GLM!" + }, + { + "role": "assistant", + "content": "", + "reasoning_content": "search", + "tool_calls": [ + { + "id": "a", + "function": { + "name": "lookup", + "arguments": { + "q": "中文<&>\n", + "list": [ + 1, + true, + null + ], + "object": { + "b": 2, + "a": "x" + } + } + } + }, + { + "id": "b", + "function": { + "name": "lookup", + "arguments": { + "q": "second", + "float": 1.5 + } + } + } + ] + }, + { + "role": "tool", + "tool_call_id": "b", + "content": "B" + }, + { + "role": "tool", + "tool_call_id": "a", + "content": "A" + } + ], + "tools": [ + { + "type": "function", + "function": { + "name": "lookup", + "description": "查找<&>", + "parameters": { + "type": "object", + "properties": { + "q": { + "type": "string" + } + }, + "required": [ + "q" + ] + }, + "strict": true + } + }, + { + "function": { + "name": "hidden", + "description": null, + "parameters": {}, + "defer_loading": true + } + } + ], + "add_generation_prompt": true, + "kwargs": {}, + "expected": "[gMASK]<|system|>Reasoning Effort: Max<|system|>\n# Tools\n\nYou may call one or more functions to assist with the user query.\n\nYou are provided with function signatures within XML tags:\n\n{\"name\": \"lookup\", \"description\": \"查找<&>\", \"parameters\": {\"type\": \"object\", \"properties\": {\"q\": {\"type\": \"string\"}}, \"required\": [\"q\"]}}\n\n\nFor each function call, output the function name and arguments within the following XML format:\n{function-name}{arg-key-1}{arg-value-1}{arg-key-2}{arg-value-2}...<|user|>你好,GLM!<|assistant|>searchlookupq中文<&>\nlist[1, true, null]object{\"b\": 2, \"a\": \"x\"}lookupqsecondfloat1.5<|observation|>AB<|user|>你好,GLM!<|assistant|>searchlookupq中文<&>\nlist[1, true, null]object{\"b\": 2, \"a\": \"x\"}lookupqsecondfloat1.5<|observation|>AB<|assistant|>" + }, + { + "name": "numeric_and_string_serialization", + "messages": [ + { + "role": "user", + "content": "你好,GLM!" + }, + { + "role": "assistant", + "content": "", + "reasoning_content": "search", + "tool_calls": [ + { + "id": "a", + "function": { + "name": "lookup", + "arguments": { + "floats": [ + 1e-07, + 1e-06, + 1e-05, + 1000000000000000.0, + 1e+16, + 1e+20, + -0.0, + 0.0001, + 1.2345678901234567, + 1.234567890123456e-100, + 5e-324, + 2.2250738585072014e-308 + ], + "unicode": "中文 😀 \\ \" \t\n" + } + } + }, + { + "id": "b", + "function": { + "name": "lookup", + "arguments": { + "q": "second", + "float": 1.5 + } + } + } + ] + } + ], + "tools": [ + { + "type": "function", + "function": { + "name": "lookup", + "description": "查找<&>", + "parameters": { + "type": "object", + "properties": { + "q": { + "type": "string" + } + }, + "required": [ + "q" + ] + }, + "strict": true + } + }, + { + "function": { + "name": "hidden", + "description": null, + "parameters": {}, + "defer_loading": true + } + } + ], + "add_generation_prompt": true, + "kwargs": {}, + "expected": "[gMASK]<|system|>Reasoning Effort: Max<|system|>\n# Tools\n\nYou may call one or more functions to assist with the user query.\n\nYou are provided with function signatures within XML tags:\n\n{\"name\": \"lookup\", \"description\": \"查找<&>\", \"parameters\": {\"type\": \"object\", \"properties\": {\"q\": {\"type\": \"string\"}}, \"required\": [\"q\"]}}\n\n\nFor each function call, output the function name and arguments within the following XML format:\n{function-name}{arg-key-1}{arg-value-1}{arg-key-2}{arg-value-2}...<|user|>你好,GLM!<|assistant|>searchlookupfloats[1e-07, 1e-06, 1e-05, 1000000000000000.0, 1e+16, 1e+20, -0.0, 0.0001, 1.2345678901234567, 1.234567890123456e-100, 5e-324, 2.2250738585072014e-308]unicode中文 😀 \\ \" \t\nlookupqsecondfloat1.5<|assistant|>" + }, + { + "name": "bad_arguments_None", + "messages": [ + { + "role": "user", + "content": "你好,GLM!" + }, + { + "role": "assistant", + "content": "", + "reasoning_content": "search", + "tool_calls": [ + { + "id": "a", + "function": { + "name": "lookup", + "arguments": null + } + }, + { + "id": "b", + "function": { + "name": "lookup", + "arguments": { + "q": "second", + "float": 1.5 + } + } + } + ] + } + ], + "tools": [ + { + "type": "function", + "function": { + "name": "lookup", + "description": "查找<&>", + "parameters": { + "type": "object", + "properties": { + "q": { + "type": "string" + } + }, + "required": [ + "q" + ] + }, + "strict": true + } + }, + { + "function": { + "name": "hidden", + "description": null, + "parameters": {}, + "defer_loading": true + } + } + ], + "add_generation_prompt": true, + "kwargs": {}, + "error": "ValueError" + }, + { + "name": "bad_arguments_[]", + "messages": [ + { + "role": "user", + "content": "你好,GLM!" + }, + { + "role": "assistant", + "content": "", + "reasoning_content": "search", + "tool_calls": [ + { + "id": "a", + "function": { + "name": "lookup", + "arguments": [] + } + }, + { + "id": "b", + "function": { + "name": "lookup", + "arguments": { + "q": "second", + "float": 1.5 + } + } + } + ] + } + ], + "tools": [ + { + "type": "function", + "function": { + "name": "lookup", + "description": "查找<&>", + "parameters": { + "type": "object", + "properties": { + "q": { + "type": "string" + } + }, + "required": [ + "q" + ] + }, + "strict": true + } + }, + { + "function": { + "name": "hidden", + "description": null, + "parameters": {}, + "defer_loading": true + } + } + ], + "add_generation_prompt": true, + "kwargs": {}, + "error": "ValueError" + }, + { + "name": "bad_arguments_invalid", + "messages": [ + { + "role": "user", + "content": "你好,GLM!" + }, + { + "role": "assistant", + "content": "", + "reasoning_content": "search", + "tool_calls": [ + { + "id": "a", + "function": { + "name": "lookup", + "arguments": "invalid" + } + }, + { + "id": "b", + "function": { + "name": "lookup", + "arguments": { + "q": "second", + "float": 1.5 + } + } + } + ] + } + ], + "tools": [ + { + "type": "function", + "function": { + "name": "lookup", + "description": "查找<&>", + "parameters": { + "type": "object", + "properties": { + "q": { + "type": "string" + } + }, + "required": [ + "q" + ] + }, + "strict": true + } + }, + { + "function": { + "name": "hidden", + "description": null, + "parameters": {}, + "defer_loading": true + } + } + ], + "add_generation_prompt": true, + "kwargs": {}, + "error": "ValueError" + }, + { + "name": "bad_arguments_[]", + "messages": [ + { + "role": "user", + "content": "你好,GLM!" + }, + { + "role": "assistant", + "content": "", + "reasoning_content": "search", + "tool_calls": [ + { + "id": "a", + "function": { + "name": "lookup", + "arguments": "[]" + } + }, + { + "id": "b", + "function": { + "name": "lookup", + "arguments": { + "q": "second", + "float": 1.5 + } + } + } + ] + } + ], + "tools": [ + { + "type": "function", + "function": { + "name": "lookup", + "description": "查找<&>", + "parameters": { + "type": "object", + "properties": { + "q": { + "type": "string" + } + }, + "required": [ + "q" + ] + }, + "strict": true + } + }, + { + "function": { + "name": "hidden", + "description": null, + "parameters": {}, + "defer_loading": true + } + } + ], + "add_generation_prompt": true, + "kwargs": {}, + "error": "ValueError" + }, + { + "name": "bad_arguments_5", + "messages": [ + { + "role": "user", + "content": "你好,GLM!" + }, + { + "role": "assistant", + "content": "", + "reasoning_content": "search", + "tool_calls": [ + { + "id": "a", + "function": { + "name": "lookup", + "arguments": 5 + } + }, + { + "id": "b", + "function": { + "name": "lookup", + "arguments": { + "q": "second", + "float": 1.5 + } + } + } + ] + } + ], + "tools": [ + { + "type": "function", + "function": { + "name": "lookup", + "description": "查找<&>", + "parameters": { + "type": "object", + "properties": { + "q": { + "type": "string" + } + }, + "required": [ + "q" + ] + }, + "strict": true + } + }, + { + "function": { + "name": "hidden", + "description": null, + "parameters": {}, + "defer_loading": true + } + } + ], + "add_generation_prompt": true, + "kwargs": {}, + "error": "ValueError" + }, + { + "name": "reference_without_tools", + "messages": [ + { + "role": "tool", + "content": [ + { + "type": "tool_reference", + "name": "hidden" + } + ] + } + ], + "tools": [], + "add_generation_prompt": true, + "kwargs": {}, + "error": "ValueError" + }, + { + "name": "bad_tool_content", + "messages": [ + { + "role": "tool", + "content": { + "bad": 1 + } + } + ], + "tools": [ + { + "type": "function", + "function": { + "name": "lookup", + "description": "查找<&>", + "parameters": { + "type": "object", + "properties": { + "q": { + "type": "string" + } + }, + "required": [ + "q" + ] + }, + "strict": true + } + }, + { + "function": { + "name": "hidden", + "description": null, + "parameters": {}, + "defer_loading": true + } + } + ], + "add_generation_prompt": true, + "kwargs": {}, + "error": "ValueError" + }, + { + "name": "bad_tool_definition", + "messages": [ + { + "role": "user", + "content": "你好,GLM!" + } + ], + "tools": [ + "bad" + ], + "add_generation_prompt": true, + "kwargs": {}, + "error": "ValueError" + }, + { + "name": "continue_plain", + "messages": [ + { + "role": "user", + "content": "你好,GLM!" + }, + { + "role": "assistant", + "content": "部分回答" + } + ], + "tools": [], + "add_generation_prompt": false, + "continue_final_message": true, + "kwargs": {}, + "expected": "[gMASK]<|system|>Reasoning Effort: Max<|user|>你好,GLM!<|assistant|>部分回答" + }, + { + "name": "continue_spaces", + "messages": [ + { + "role": "user", + "content": "你好,GLM!" + }, + { + "role": "assistant", + "content": " partial answer \n" + } + ], + "tools": [], + "add_generation_prompt": false, + "continue_final_message": true, + "kwargs": {}, + "expected": "[gMASK]<|system|>Reasoning Effort: Max<|user|>你好,GLM!<|assistant|>partial answer" + }, + { + "name": "continue_unicode_spaces", + "messages": [ + { + "role": "user", + "content": "你好,GLM!" + }, + { + "role": "assistant", + "content": " x …" + } + ], + "tools": [], + "add_generation_prompt": false, + "continue_final_message": true, + "kwargs": {}, + "expected": "[gMASK]<|system|>Reasoning Effort: Max<|user|>你好,GLM!<|assistant|>x" + }, + { + "name": "continue_python_whitespace", + "messages": [ + { + "role": "user", + "content": "你好,GLM!" + }, + { + "role": "assistant", + "content": "\u001c\u001d answer\u001e\u001f" + } + ], + "tools": [], + "add_generation_prompt": false, + "continue_final_message": true, + "kwargs": {}, + "expected": "[gMASK]<|system|>Reasoning Effort: Max<|user|>你好,GLM!<|assistant|>answer" + }, + { + "name": "continue_inline_think", + "messages": [ + { + "role": "user", + "content": "你好,GLM!" + }, + { + "role": "assistant", + "content": "inlinepartial" + } + ], + "tools": [], + "add_generation_prompt": false, + "continue_final_message": true, + "kwargs": {}, + "expected": "[gMASK]<|system|>Reasoning Effort: Max<|user|>你好,GLM!<|assistant|>inlinepartial" + }, + { + "name": "continue_inline_close_only", + "messages": [ + { + "role": "user", + "content": "你好,GLM!" + }, + { + "role": "assistant", + "content": "partialmore" + } + ], + "tools": [], + "add_generation_prompt": false, + "continue_final_message": true, + "kwargs": {}, + "expected": "[gMASK]<|system|>Reasoning Effort: Max<|user|>你好,GLM!<|assistant|>partialmore" + }, + { + "name": "continue_inline_think_with_spaces", + "messages": [ + { + "role": "user", + "content": "你好,GLM!" + }, + { + "role": "assistant", + "content": "a b" + } + ], + "tools": [], + "add_generation_prompt": false, + "continue_final_message": true, + "kwargs": {}, + "error": "ValueError" + }, + { + "name": "continue_reasoning_text_tool", + "messages": [ + { + "role": "user", + "content": "你好,GLM!" + }, + { + "role": "assistant", + "content": "partial", + "reasoning_content": "reasoning", + "tool_calls": [ + { + "id": "call_0", + "function": { + "name": "lookup", + "arguments": { + "q": "x", + "n": 1 + } + } + } + ] + } + ], + "tools": [], + "add_generation_prompt": false, + "continue_final_message": true, + "kwargs": {}, + "expected": "[gMASK]<|system|>Reasoning Effort: Max<|user|>你好,GLM!<|assistant|>reasoningpartial" + }, + { + "name": "continue_reasoning_only", + "messages": [ + { + "role": "user", + "content": "你好,GLM!" + }, + { + "role": "assistant", + "content": "", + "reasoning_content": "r" + } + ], + "tools": [], + "add_generation_prompt": false, + "continue_final_message": true, + "kwargs": {}, + "expected": "[gMASK]<|system|>Reasoning Effort: Max<|user|>你好,GLM!<|assistant|>r" + }, + { + "name": "continue_empty_reasoning", + "messages": [ + { + "role": "user", + "content": "你好,GLM!" + }, + { + "role": "assistant", + "content": "x ", + "reasoning_content": "" + } + ], + "tools": [], + "add_generation_prompt": false, + "continue_final_message": true, + "kwargs": {}, + "expected": "[gMASK]<|system|>Reasoning Effort: Max<|user|>你好,GLM!<|assistant|>x" + }, + { + "name": "continue_tool_call_only", + "messages": [ + { + "role": "user", + "content": "你好,GLM!" + }, + { + "role": "assistant", + "content": "", + "tool_calls": [ + { + "id": "call_0", + "function": { + "name": "lookup", + "arguments": { + "q": "x", + "n": 1 + } + } + } + ] + } + ], + "tools": [], + "add_generation_prompt": false, + "continue_final_message": true, + "kwargs": {}, + "expected": "[gMASK]<|system|>Reasoning Effort: Max<|user|>你好,GLM!<|assistant|>" + }, + { + "name": "continue_whitespace_only", + "messages": [ + { + "role": "user", + "content": "你好,GLM!" + }, + { + "role": "assistant", + "content": " " + } + ], + "tools": [], + "add_generation_prompt": false, + "continue_final_message": true, + "kwargs": {}, + "expected": "[gMASK]<|system|>Reasoning Effort: Max<|user|>你好,GLM!<|assistant|>" + }, + { + "name": "continue_no_user", + "messages": [ + { + "role": "assistant", + "content": "partial", + "reasoning_content": "r" + } + ], + "tools": [], + "add_generation_prompt": false, + "continue_final_message": true, + "kwargs": {}, + "expected": "[gMASK]<|system|>Reasoning Effort: Max<|assistant|>rpartial" + }, + { + "name": "continue_clear_thinking", + "messages": [ + { + "role": "user", + "content": "你好,GLM!" + }, + { + "role": "assistant", + "content": "old", + "reasoning_content": "old r" + }, + { + "role": "user", + "content": "你好,GLM!" + }, + { + "role": "assistant", + "content": "new", + "reasoning_content": "new r" + } + ], + "tools": [], + "add_generation_prompt": false, + "continue_final_message": true, + "kwargs": { + "clear_thinking": true + }, + "expected": "[gMASK]<|system|>Reasoning Effort: Max<|user|>你好,GLM!<|assistant|>old<|user|>你好,GLM!<|assistant|>new rnew" + }, + { + "name": "continue_after_tool_response", + "messages": [ + { + "role": "user", + "content": "你好,GLM!" + }, + { + "role": "assistant", + "content": "", + "reasoning_content": "r", + "tool_calls": [ + { + "id": "call_0", + "function": { + "name": "lookup", + "arguments": { + "q": "x", + "n": 1 + } + } + } + ] + }, + { + "role": "tool", + "content": "结果", + "tool_call_id": "call_0" + }, + { + "role": "assistant", + "content": "根据结果" + } + ], + "tools": [], + "add_generation_prompt": false, + "continue_final_message": true, + "kwargs": { + "reasoning_effort": "low" + }, + "expected": "[gMASK]<|system|>Reasoning Effort: Low<|user|>你好,GLM!<|assistant|>rlookupqxn1<|observation|>结果<|assistant|>根据结果" + } + ] +} diff --git a/rust/src/chat/src/renderer/glm5_next/mod.rs b/rust/src/chat/src/renderer/glm5_next/mod.rs new file mode 100644 index 000000000000..10b2960db4f1 --- /dev/null +++ b/rust/src/chat/src/renderer/glm5_next/mod.rs @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright contributors to the vLLM project + +mod encoding; +mod writer; + +use std::collections::HashMap; +use std::sync::Arc; + +use serde_json::Value; +use vllm_text::tokenizer::DynTokenizer; + +use super::{ChatRenderer, RenderedPrompt}; +use crate::error::Result; +use crate::request::ChatRequest; + +#[derive(Clone, Default)] +pub struct Glm5NextChatRenderer { + default_template_kwargs: HashMap, + multimodal: bool, + tokenizer: Option>, +} + +impl Glm5NextChatRenderer { + pub fn new(default_template_kwargs: HashMap) -> Self { + Self { + default_template_kwargs, + multimodal: false, + tokenizer: None, + } + } + + /// Render token IDs: control tokens use added-vocabulary IDs and literal + /// text uses the base vocabulary. + pub fn with_tokenizer(mut self, tokenizer: DynTokenizer) -> Result { + self.tokenizer = Some(Arc::new(writer::ControlTokenizer::new(tokenizer)?)); + Ok(self) + } + + pub fn with_multimodal(mut self, enabled: bool) -> Self { + self.multimodal = enabled; + self + } +} + +impl ChatRenderer for Glm5NextChatRenderer { + fn render(&self, request: &ChatRequest) -> Result { + request.validate()?; + let mut kwargs = self.default_template_kwargs.clone(); + kwargs.extend(request.chat_options.template_kwargs.clone()); + if let Some(effort) = &request.chat_options.reasoning_effort { + kwargs.insert("reasoning_effort".into(), serde_json::json!(effort)); + } + let messages = encoding::messages(&request.messages); + let tools = encoding::tools(request.tools()); + let mut out = writer::Glm5PromptWriter::new(self.tokenizer.as_deref()); + let media_order = encoding::render_into( + &mut out, + &messages, + &tools, + request.chat_options.generation_prompt_mode, + &kwargs, + self.multimodal, + )?; + Ok(RenderedPrompt { + prompt: out.finish()?, + media_order: Some(media_order), + effective_template_kwargs: kwargs, + }) + } +} + +#[cfg(test)] +mod tests; diff --git a/rust/src/chat/src/renderer/glm5_next/tests.rs b/rust/src/chat/src/renderer/glm5_next/tests.rs new file mode 100644 index 000000000000..fa87129db685 --- /dev/null +++ b/rust/src/chat/src/renderer/glm5_next/tests.rs @@ -0,0 +1,492 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright contributors to the vLLM project + +use std::collections::HashMap; +use std::sync::Arc; + +use serde::Deserialize; +use serde_json::{Value, json}; +use vllm_text::Prompt; +use vllm_tokenizer::{DynTokenizer, HuggingFaceTokenizer}; + +use super::writer::{ControlTokenizer, Glm5PromptWriter}; +use super::{Glm5NextChatRenderer, encoding}; +use crate::renderer::hf::{ChatTemplateContentFormatOption, HfChatRenderer, MultimodalRenderInfo}; +use crate::request::{ + ChatMessage, ChatRequest, ChatTool, GenerationPromptMode, ResolvedToolContext, +}; +use crate::{AssistantContentBlock, AssistantToolCall, ChatRenderer, EffortValue}; + +#[derive(Deserialize)] +struct TestCase { + name: String, + messages: Vec, + tools: Vec, + add_generation_prompt: bool, + #[serde(default)] + continue_final_message: bool, + kwargs: HashMap, + expected: Option, + error: Option, +} + +#[derive(Deserialize)] +struct TestCases { + cases: Vec, +} + +#[test] +fn renders_expected_prompts_and_rejects_invalid_input() { + let fixtures: TestCases = + serde_json::from_str(include_str!("fixtures/test_cases.json")).unwrap(); + for case in fixtures.cases { + let result = encoding::render( + &case.messages, + &case.tools, + if case.continue_final_message { + GenerationPromptMode::ContinueFinalAssistant + } else if case.add_generation_prompt { + GenerationPromptMode::StartNewAssistant + } else { + GenerationPromptMode::NoGenerationPrompt + }, + &case.kwargs, + ); + if case.error.is_some() { + assert!( + result.is_err(), + "{}: expected rejection, got {result:?}", + case.name + ); + } else { + assert_eq!(result.unwrap(), case.expected.unwrap(), "{}", case.name); + } + } +} + +fn tool_request(count: usize) -> ChatRequest { + let calls = (0..count) + .map(|index| { + AssistantContentBlock::ToolCall(AssistantToolCall { + id: format!("call_{index}"), + name: "lookup".into(), + arguments: json!({"q": format!("城市 {index}"), "values": [true, null, 1.5]}) + .to_string(), + }) + }) + .collect(); + let mut messages = vec![ + ChatMessage::system("助手"), + ChatMessage::user("查询天气"), + ChatMessage::assistant_blocks(calls), + ]; + messages.extend( + (0..count).rev().map(|index| { + ChatMessage::tool_response(format!("结果 {index}"), format!("call_{index}")) + }), + ); + let tools = vec![ChatTool { + name: "lookup".into(), + description: Some("查询天气".into()), + parameters: json!({"type": "object", "properties": {"q": {"type": "string"}}}), + strict: Some(true), + defer_loading: None, + }]; + let tool_context = ResolvedToolContext::new(&messages, tools, None, true).unwrap(); + ChatRequest { + messages, + tool_context, + ..ChatRequest::for_test() + } +} + +const CHECKPOINT_TEMPLATES: [(&str, &str); 2] = [ + ("GLM-5.3", include_str!("fixtures/glm_5_3.jinja")), + ( + "GLM-5.3-Flash", + include_str!("fixtures/glm_5_3_flash.jinja"), + ), +]; + +fn hf_renderer(template: &str) -> HfChatRenderer { + HfChatRenderer::new( + Some(template.into()), + HashMap::new(), + ChatTemplateContentFormatOption::OpenAi, + ) + .unwrap() +} + +#[test] +fn typed_requests_match_existing_hf_renderer() { + let native = Glm5NextChatRenderer::default(); + for (name, template) in CHECKPOINT_TEMPLATES { + let hf = hf_renderer(template); + for count in [1, 2, 8] { + let mut request = tool_request(count); + for clear in [false, true] { + request + .chat_options + .template_kwargs + .insert("clear_thinking".into(), json!(clear)); + assert_eq!( + native.render(&request).unwrap().prompt, + hf.render(&request).unwrap().prompt, + "{name}" + ); + } + request.tool_context.tool_choice = crate::request::ChatToolChoice::None; + assert_eq!( + native.render(&request).unwrap().prompt, + hf.render(&request).unwrap().prompt + ); + } + } +} + +#[test] +fn flash_media_matches_checkpoint_template() { + let hf = hf_renderer(CHECKPOINT_TEMPLATES[1].1).with_multimodal(Some(MultimodalRenderInfo { + image_token: Some("<|image|>".into()), + video_token: Some("<|video|>".into()), + audio_token: Some("<|begin_of_audio|>".into()), + })); + let mut request = tool_request(2); + request.messages.push(ChatMessage::user(vec![ + crate::request::ChatContentPart::image_url("unused"), + ])); + let expected = hf.render(&request).unwrap(); + let renderer = Glm5NextChatRenderer::default().with_multimodal(true); + let actual = renderer.render(&request).unwrap(); + assert_eq!(actual.prompt, expected.prompt); + assert_eq!( + actual.media_order.as_ref().unwrap(), + &[crate::renderer::MediaPartSource { + message_index: 5, + content_part_index: 0 + }] + ); + assert!( + actual + .prompt + .into_text() + .unwrap() + .contains("<|begin_of_image|><|image|><|end_of_image|>") + ); +} + +#[test] +fn media_order_tracks_reordered_tool_responses() { + use crate::renderer::MediaPartSource; + use crate::request::ChatContentPart; + let mut request = tool_request(2); + request.messages[3] = + ChatMessage::tool_response(vec![ChatContentPart::image_url("second")], "call_1"); + request.messages[4] = + ChatMessage::tool_response(vec![ChatContentPart::image_url("first")], "call_0"); + let native = Glm5NextChatRenderer::default().with_multimodal(true).render(&request).unwrap(); + let hf = hf_renderer(CHECKPOINT_TEMPLATES[1].1) + .with_multimodal(Some(MultimodalRenderInfo { + image_token: Some("<|image|>".into()), + video_token: Some("<|video|>".into()), + audio_token: None, + })) + .render(&request) + .unwrap(); + assert_eq!(native.prompt, hf.prompt); + assert_eq!( + native.media_order.unwrap(), + vec![ + MediaPartSource { + message_index: 4, + content_part_index: 0 + }, + MediaPartSource { + message_index: 3, + content_part_index: 0 + } + ] + ); +} + +#[test] +fn defaults_request_kwargs_and_typed_effort_have_defined_precedence() { + let renderer = Glm5NextChatRenderer::new(HashMap::from([ + ("reasoning_effort".into(), json!("low")), + ("clear_thinking".into(), json!(true)), + ])); + let mut request = ChatRequest::for_test(); + request + .chat_options + .template_kwargs + .insert("reasoning_effort".into(), json!("high")); + let rendered = renderer.render(&request).unwrap(); + assert!(rendered.prompt.into_text().unwrap().contains("Reasoning Effort: High")); + request.chat_options.reasoning_effort = Some(EffortValue::from("low")); + let rendered = renderer.render(&request).unwrap(); + assert!(rendered.prompt.into_text().unwrap().contains("Reasoning Effort: Low")); + assert_eq!( + rendered.effective_template_kwargs["reasoning_effort"], + "low" + ); + assert_eq!(rendered.effective_template_kwargs["clear_thinking"], true); +} + +#[test] +fn empty_explicit_reasoning_suppresses_embedded_tag_parsing() { + let request = ChatRequest { + messages: vec![ChatMessage::assistant_blocks(vec![ + AssistantContentBlock::Reasoning { + text: String::new(), + }, + AssistantContentBlock::Text { + text: "literalanswer".into(), + }, + ])], + ..ChatRequest::for_test() + }; + let rendered = Glm5NextChatRenderer::default().render(&request).unwrap(); + assert_eq!( + rendered.prompt.into_text().unwrap(), + "[gMASK]<|system|>Reasoning Effort: Max<|assistant|>literalanswer<|assistant|>" + ); +} + +#[test] +fn continue_final_message_matches_existing_hf_renderer() { + let final_messages = [ + ChatMessage::assistant_text(" partial answer \n"), + ChatMessage::assistant_text("inlinepartial"), + ChatMessage::assistant_blocks(vec![ + AssistantContentBlock::Reasoning { + text: "reasoning".into(), + }, + AssistantContentBlock::Text { + text: "partial".into(), + }, + AssistantContentBlock::ToolCall(AssistantToolCall { + id: "call_0".into(), + name: "lookup".into(), + arguments: json!({"q": "x"}).to_string(), + }), + ]), + ]; + let native = Glm5NextChatRenderer::default(); + for (name, template) in CHECKPOINT_TEMPLATES { + let hf = hf_renderer(template); + for final_message in &final_messages { + let mut request = tool_request(1); + request.messages.push(ChatMessage::user("继续")); + request.messages.push(final_message.clone()); + request.chat_options.generation_prompt_mode = + GenerationPromptMode::ContinueFinalAssistant; + assert_eq!( + native.render(&request).unwrap().prompt, + hf.render(&request).unwrap().prompt, + "{name}: {final_message:?}" + ); + } + } +} + +#[test] +fn media_parts_render_reminders_without_multimodal_inputs() { + let request = ChatRequest { + messages: vec![ChatMessage::user(vec![ + crate::request::ChatContentPart::image_url("unused"), + ])], + ..ChatRequest::for_test() + }; + let rendered = Glm5NextChatRenderer::default().render(&request).unwrap(); + assert!(rendered.prompt.into_text().unwrap().contains("unable to process this image")); + assert_eq!(rendered.media_order, Some(Vec::new())); +} + +/// Exercise BPE merges, added-token recognition and Unicode without a checkpoint download. +fn tokenizer_fixture() -> Value { + let vocab: serde_json::Map = (0u32..256) + .map(|byte| { + let ch = match byte { + 33..=126 | 161..=172 | 174..=255 => char::from_u32(byte).unwrap(), + _ => { + let index = (0..byte) + .filter(|b| !matches!(b, 33..=126 | 161..=172 | 174..=255)) + .count(); + char::from_u32(256 + index as u32).unwrap() + } + }; + (ch.to_string(), json!(byte)) + }) + .chain([("ab".into(), json!(256)), ("abc".into(), json!(257))]) + .collect(); + let tokens = [ + "[gMASK]", + "", + "<|system|>", + "<|user|>", + "<|assistant|>", + "<|observation|>", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "<|image|>", + "<|video|>", + "<|begin_of_image|>", + "<|end_of_image|>", + "<|begin_of_video|>", + "<|end_of_video|>", + "<|begin_of_audio|>", + "<|end_of_audio|>", + ]; + json!({ + "version": "1.0", "truncation": null, "padding": null, "normalizer": null, + "added_tokens": tokens.iter().enumerate().map(|(i, content)| json!({ + "id": 258 + i, "content": content, "single_word": false, + "lstrip": false, "rstrip": false, "normalized": false, "special": i < 6, + })).collect::>(), + "pre_tokenizer": {"type": "Sequence", "pretokenizers": [ + {"type": "Split", "pattern": {"Regex": " ?\\p{L}+| ?[^\\s\\p{L}]+|\\s+"}, + "behavior": "Isolated", "invert": false}, + {"type": "ByteLevel", "add_prefix_space": false, + "trim_offsets": true, "use_regex": false} + ]}, + "post_processor": {"type": "ByteLevel", "add_prefix_space": true, + "trim_offsets": false, "use_regex": true}, + "decoder": {"type": "ByteLevel", "add_prefix_space": false, + "trim_offsets": false, "use_regex": false}, + "model": {"type": "BPE", "dropout": null, "unk_token": null, + "continuing_subword_prefix": null, "end_of_word_suffix": null, + "fuse_unk": false, "byte_fallback": false, "ignore_merges": false, + "vocab": vocab, "merges": [["a", "b"], ["ab", "c"]]} + }) +} + +/// Load `VLLM_GLM_TOKENIZER_JSON`, or the fixture, with both tokenizer backends. +fn tokenizers(dir: &std::path::Path) -> [DynTokenizer; 2] { + let path = std::env::var_os("VLLM_GLM_TOKENIZER_JSON") + .map(std::path::PathBuf::from) + .unwrap_or_else(|| { + let path = dir.join("tokenizer.json"); + std::fs::write(&path, serde_json::to_vec(&tokenizer_fixture()).unwrap()).unwrap(); + path + }); + [ + HuggingFaceTokenizer::new_hf, + HuggingFaceTokenizer::new_fastokens, + ] + .map(|constructor| Arc::new(constructor(&path).unwrap()) as DynTokenizer) +} + +#[test] +fn token_ids_match_whole_prompt_encoding() { + let dir = tempfile::tempdir().unwrap(); + for tokenizer in tokenizers(dir.path()) { + let mut request = tool_request(2); + request.messages.push(ChatMessage::user(vec![ + crate::request::ChatContentPart::text("abc 中文 e\u{301}🙂\r\n"), + crate::request::ChatContentPart::image_url("unused"), + ])); + for mode in [ + GenerationPromptMode::StartNewAssistant, + GenerationPromptMode::NoGenerationPrompt, + GenerationPromptMode::ContinueFinalAssistant, + ] { + request.chat_options.generation_prompt_mode = mode; + if mode == GenerationPromptMode::ContinueFinalAssistant { + request.messages.push(ChatMessage::assistant_text("abc ")); + } + for multimodal in [false, true] { + let renderer = Glm5NextChatRenderer::default().with_multimodal(multimodal); + let text = renderer.render(&request).unwrap(); + let tokens = + renderer.with_tokenizer(tokenizer.clone()).unwrap().render(&request).unwrap(); + let ids = tokenizer.encode(&text.prompt.into_text().unwrap(), false).unwrap(); + assert_eq!(tokens.prompt, Prompt::TokenIds(ids)); + assert_eq!(tokens.media_order, text.media_order); + } + } + } +} + +#[test] +fn literal_control_token_spellings_use_the_base_vocabulary() { + let dir = tempfile::tempdir().unwrap(); + for tokenizer in tokenizers(dir.path()) { + let request = ChatRequest { + messages: vec![ChatMessage::user("<|user|>x")], + ..ChatRequest::for_test() + }; + let text = Glm5NextChatRenderer::default().render(&request).unwrap().prompt; + let Prompt::TokenIds(ids) = Glm5NextChatRenderer::default() + .with_tokenizer(tokenizer.clone()) + .unwrap() + .render(&request) + .unwrap() + .prompt + else { + panic!("expected token IDs"); + }; + assert_eq!( + tokenizer.decode(&ids, false).unwrap(), + text.into_text().unwrap() + ); + let user = tokenizer.token_to_id("<|user|>").unwrap(); + assert_eq!(ids.iter().filter(|&&id| id == user).count(), 1); + } +} + +#[test] +fn fixture_token_ids_decode_to_rendered_prompts() { + let dir = tempfile::tempdir().unwrap(); + let fixtures: TestCases = + serde_json::from_str(include_str!("fixtures/test_cases.json")).unwrap(); + for tokenizer in tokenizers(dir.path()) { + let control = ControlTokenizer::new(tokenizer.clone()).unwrap(); + for case in fixtures.cases.iter().filter(|case| case.expected.is_some()) { + let mode = if case.continue_final_message { + GenerationPromptMode::ContinueFinalAssistant + } else if case.add_generation_prompt { + GenerationPromptMode::StartNewAssistant + } else { + GenerationPromptMode::NoGenerationPrompt + }; + let mut out = Glm5PromptWriter::new(Some(&control)); + encoding::render_into( + &mut out, + &case.messages, + &case.tools, + mode, + &case.kwargs, + false, + ) + .unwrap(); + let Prompt::TokenIds(ids) = out.finish().unwrap() else { + panic!("expected token IDs"); + }; + assert_eq!( + &tokenizer.decode(&ids, false).unwrap(), + case.expected.as_ref().unwrap(), + "{}", + case.name + ); + } + } +} + +#[test] +fn tokenizer_without_control_tokens_is_rejected() { + let dir = tempfile::tempdir().unwrap(); + let mut config = tokenizer_fixture(); + config["added_tokens"].as_array_mut().unwrap().pop(); + let path = dir.path().join("tokenizer.json"); + std::fs::write(&path, serde_json::to_vec(&config).unwrap()).unwrap(); + let tokenizer: DynTokenizer = Arc::new(HuggingFaceTokenizer::new_hf(&path).unwrap()); + assert!(Glm5NextChatRenderer::default().with_tokenizer(tokenizer).is_err()); +} diff --git a/rust/src/chat/src/renderer/glm5_next/writer.rs b/rust/src/chat/src/renderer/glm5_next/writer.rs new file mode 100644 index 000000000000..e81d2e67d91b --- /dev/null +++ b/rust/src/chat/src/renderer/glm5_next/writer.rs @@ -0,0 +1,129 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright contributors to the vLLM project + +use vllm_text::Prompt; +use vllm_text::tokenizer::DynTokenizer; + +use crate::error::{Result, chat_template_thrown}; + +/// Control tokens emitted by the GLM5Next template. +const CONTROL_TOKENS: [&str; 24] = [ + "[gMASK]", + "", + "<|system|>", + "<|user|>", + "<|assistant|>", + "<|observation|>", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "<|begin_of_image|>", + "<|image|>", + "<|end_of_image|>", + "<|begin_of_video|>", + "<|video|>", + "<|end_of_video|>", + "<|begin_of_audio|>", + "<|end_of_audio|>", +]; + +/// Tokenizer with the added-vocabulary IDs of [`CONTROL_TOKENS`]. +pub(super) struct ControlTokenizer { + tokenizer: DynTokenizer, + ids: [u32; CONTROL_TOKENS.len()], +} + +impl ControlTokenizer { + pub(super) fn new(tokenizer: DynTokenizer) -> Result { + let mut ids = [0; CONTROL_TOKENS.len()]; + for (id, token) in ids.iter_mut().zip(CONTROL_TOKENS) { + *id = tokenizer + .added_vocab() + .iter() + .find_map(|(text, id)| (text == token).then_some(*id)) + .ok_or_else(|| { + chat_template_thrown!("GLM tokenizer lacks added token {token:?}") + })?; + } + Ok(Self { tokenizer, ids }) + } +} + +/// GLM prompt writer: control tokens map to added-vocabulary IDs and the text +/// between them is encoded once with the base vocabulary. +pub(super) struct Glm5PromptWriter<'a> { + text: String, + tokens: Option<(&'a ControlTokenizer, Vec)>, + pending: usize, +} + +impl<'a> Glm5PromptWriter<'a> { + pub(super) fn new(tokenizer: Option<&'a ControlTokenizer>) -> Self { + Self { + text: String::new(), + tokens: tokenizer.map(|tokenizer| (tokenizer, Vec::new())), + pending: 0, + } + } + + pub(super) fn text(&self) -> &str { + &self.text + } + + /// Write template text, recognizing the GLM control tokens it contains. + pub(super) fn control(&mut self, mut text: &str) -> Result<()> { + if self.tokens.is_none() { + self.text.push_str(text); + return Ok(()); + } + while let Some(start) = text.find(['<', '[']) { + let rest = &text[start..]; + let Some(index) = CONTROL_TOKENS.iter().position(|token| rest.starts_with(token)) + else { + self.text.push_str(&text[..=start]); + text = &text[start + 1..]; + continue; + }; + self.text.push_str(&text[..start]); + self.flush()?; + let (tokenizer, token_ids) = self.tokens.as_mut().unwrap(); + token_ids.push(tokenizer.ids[index]); + self.text.push_str(CONTROL_TOKENS[index]); + self.pending = self.text.len(); + text = &rest[CONTROL_TOKENS[index].len()..]; + } + self.text.push_str(text); + Ok(()) + } + + /// Write literal text; control-token spellings in it stay plain text. + pub(super) fn ordinary(&mut self, text: &str) { + self.text.push_str(text); + } + + fn flush(&mut self) -> Result<()> { + if let Some((tokenizer, token_ids)) = &mut self.tokens + && self.pending < self.text.len() + { + let text = &self.text[self.pending..]; + token_ids.extend(tokenizer.tokenizer.encode_ordinary(text)?); + self.pending = self.text.len(); + } + Ok(()) + } + + pub(super) fn finish(mut self) -> Result { + self.flush()?; + Ok(match self.tokens { + Some((_, token_ids)) => Prompt::TokenIds(token_ids), + None => Prompt::Text(self.text), + }) + } +} diff --git a/rust/src/chat/src/renderer/hf/mod.rs b/rust/src/chat/src/renderer/hf/mod.rs index dfa6a1d2165a..083f7d30f639 100644 --- a/rust/src/chat/src/renderer/hf/mod.rs +++ b/rust/src/chat/src/renderer/hf/mod.rs @@ -9,7 +9,7 @@ use thiserror_ext::AsReport as _; use tracing::{info, trace, warn}; use vllm_text::Prompt; use vllm_text::backend::hf::{ - HfSpecialTokens, HfTokenizerConfig, ResolvedModelFiles, load_tokenizer_config, + HfChatTemplate, HfSpecialTokens, HfTokenizerConfig, ResolvedModelFiles, load_tokenizer_config, }; use self::format::{ @@ -73,6 +73,9 @@ pub struct MultimodalRenderInfo { /// state. pub struct HfChatRenderer { default_template: Option, + /// The model's template for requests with tools, from a list-form + /// `chat_template` entry named `tool_use`; falls back to the default. + tool_use_template: Option, default_template_kwargs: HashMap, content_format: ContentFormatOption, special_tokens: Option, @@ -97,6 +100,7 @@ impl HfChatRenderer { .map_err(|error| Error::ChatTemplate(error.to_report_string())) }) .transpose()?, + tool_use_template: None, default_template_kwargs, content_format, special_tokens: None, @@ -109,6 +113,18 @@ impl HfChatRenderer { }) } + /// Use a separate model template for requests with tools, as Transformers + /// selects a `tool_use` template when tools are passed. + pub fn with_tool_use_template(mut self, template: Option) -> Result { + self.tool_use_template = template + .map(|template| { + CompiledChatTemplate::new(template, self.content_format) + .map_err(|error| Error::ChatTemplate(error.to_report_string())) + }) + .transpose()?; + Ok(self) + } + pub fn with_special_tokens(mut self, special_tokens: Option) -> Self { self.special_tokens = special_tokens; self @@ -137,7 +153,16 @@ impl HfChatRenderer { chat_template, .. } = load_tokenizer_config(files.tokenizer_config_path.as_deref())?; - let mut template = chat_template; + let mut template = chat_template + .as_ref() + .and_then(HfChatTemplate::default_template) + .map(str::to_owned); + // A configured template or a dedicated template file replaces every + // tokenizer_config template, `tool_use` included. + let mut tool_use_template = chat_template + .as_ref() + .and_then(HfChatTemplate::tool_use_template) + .map(str::to_owned); let special_tokens = (!special_tokens.is_empty()).then_some(special_tokens); if let Some(configured_template) = options.chat_template.as_deref() { @@ -145,6 +170,7 @@ impl HfChatRenderer { resolve_chat_template(configured_template) .map_err(|error| Error::ChatTemplate(error.to_report_string()))?, ); + tool_use_template = None; info!("using configured chat template override"); } else if let Some(chat_template_path) = files.chat_template_path.as_deref() { // If independent chat template file(s) exist and contain non-empty content, @@ -158,6 +184,7 @@ impl HfChatRenderer { "loaded dedicated chat template file, overriding tokenizer_config chat_template" ); template = file_template; + tool_use_template = None; } else { warn!( path = %chat_template_path.display(), @@ -171,6 +198,7 @@ impl HfChatRenderer { options.default_chat_template_kwargs, options.chat_template_content_format, )? + .with_tool_use_template(tool_use_template)? .with_special_tokens(special_tokens) .with_multimodal(multimodal) .with_trust_request_chat_template(options.trust_request_chat_template)) @@ -183,7 +211,9 @@ impl HfChatRenderer { /// temporary template is compiled from that string and used instead of /// the model's default. Overrides are refused unless request templates /// are trusted, and render under a fuel budget; the model's default - /// template has none, as in the Python frontend. + /// template has none, as in the Python frontend. Without an override, a + /// request with tools uses the model's `tool_use` template if it has one, + /// as Transformers' `get_chat_template` does. fn apply_chat_template(&self, request: &ChatRequest) -> Result { if request.chat_options.chat_template.is_some() && !self.trust_request_chat_template { return Err(Error::UntrustedRequestChatTemplate); @@ -198,9 +228,15 @@ impl HfChatRenderer { .map_err(|error| Error::ChatTemplate(error.to_report_string())) }) .transpose()?; + let model_template = if request.tools().is_empty() { + None + } else { + self.tool_use_template.as_ref() + } + .or(self.default_template.as_ref()); let template = override_template .as_ref() - .or(self.default_template.as_ref()) + .or(model_template) .ok_or(Error::MissingChatTemplate)?; self.apply_chat_template_inner(template, request) @@ -215,7 +251,28 @@ impl HfChatRenderer { &request.messages, effective_template.content_format(), self.multimodal.as_ref(), + effective_template.supports_developer_role(), )?; + let has_developer_messages = request + .messages + .iter() + .any(|message| matches!(message, ChatMessage::Developer { .. })); + if has_developer_messages && !effective_template.supports_developer_role() { + // Consolidation moves every non-leading system message to the front, + // ahead of earlier media. Media data stays in message order, so the + // rendered placeholders would no longer match it. + let moves_media = request.messages.iter().skip(1).any(|message| { + matches!( + message, + ChatMessage::System { content } | ChatMessage::Developer { content, .. } + if content.has_multimodal() + ) + }); + if moves_media { + return Err(Error::UnsupportedMultimodalSystemFallback); + } + messages = consolidate_system_messages(messages); + } // Handling of `continue_final_message`: // Append a sentinel tag to the final message content, render as usual, then @@ -280,6 +337,10 @@ impl ChatRenderer for HfChatRenderer { fn render(&self, request: &ChatRequest) -> Result { self.apply_chat_template(request) } + + fn renders_request_chat_template(&self) -> bool { + true + } } /// Chat message in the JSON shape expected by Jinja chat templates. @@ -320,6 +381,91 @@ enum TemplateContentPart { Audio, } +/// Match Python vLLM's fallback for templates that do not support `developer`. +fn consolidate_system_messages<'a>(messages: Vec>) -> Vec> { + let needs_consolidation = messages + .iter() + .enumerate() + .any(|(index, message)| message.role == "system" && index > 0) + || messages.iter().filter(|message| message.role == "system").count() > 1; + if !needs_consolidation { + return messages; + } + + let mut system_contents = Vec::new(); + let mut non_system = Vec::with_capacity(messages.len()); + for message in messages { + if message.role == "system" { + system_contents.push(message.content); + } else { + non_system.push(message); + } + } + + let mut consolidated = Vec::with_capacity(non_system.len() + 1); + consolidated.push(TemplateMessage { + role: "system", + content: merge_system_content(system_contents), + tools: None, + reasoning: None, + reasoning_content: None, + tool_calls: None, + tool_call_id: None, + }); + consolidated.extend(non_system); + consolidated +} + +/// Merge system/developer content without changing the format selected by the +/// chat template. OpenAI content arrays retain all text and multimodal parts. +fn merge_system_content(contents: Vec) -> TemplateContent { + let mut contents = contents.into_iter(); + let Some(mut merged) = contents.next() else { + return TemplateContent::String(String::new()); + }; + + for content in contents { + merged = match (merged, content) { + (TemplateContent::String(mut left), TemplateContent::String(right)) => { + if !left.is_empty() && !right.is_empty() { + left.push_str("\n\n"); + } + left.push_str(&right); + TemplateContent::String(left) + } + (TemplateContent::OpenAi(mut left), TemplateContent::OpenAi(mut right)) => { + if !left.is_empty() && !right.is_empty() { + left.push(TemplateContentPart::Text { + text: "\n\n".to_string(), + }); + } + left.append(&mut right); + TemplateContent::OpenAi(left) + } + (left, right) => { + let mut left = into_openai_parts(left); + let mut right = into_openai_parts(right); + if !left.is_empty() && !right.is_empty() { + left.push(TemplateContentPart::Text { + text: "\n\n".to_string(), + }); + } + left.append(&mut right); + TemplateContent::OpenAi(left) + } + }; + } + + merged +} + +fn into_openai_parts(content: TemplateContent) -> Vec { + match content { + TemplateContent::String(text) => vec![TemplateContentPart::Text { text }], + TemplateContent::OpenAi(parts) => parts, + } +} + #[derive(Debug, Serialize)] struct TemplateToolCall { id: String, @@ -356,10 +502,13 @@ fn to_template_messages<'a>( messages: &'a [ChatMessage], content_format: ChatTemplateContentFormat, multimodal: Option<&MultimodalRenderInfo>, + supports_developer_role: bool, ) -> Result>> { messages .iter() - .map(|message| to_template_message(message, content_format, multimodal)) + .map(|message| { + to_template_message(message, content_format, multimodal, supports_developer_role) + }) .collect() } @@ -367,6 +516,7 @@ fn to_template_message<'a>( message: &'a ChatMessage, content_format: ChatTemplateContentFormat, multimodal: Option<&MultimodalRenderInfo>, + supports_developer_role: bool, ) -> Result> { Ok(match message { ChatMessage::System { content } => TemplateMessage { @@ -379,9 +529,15 @@ fn to_template_message<'a>( tool_call_id: None, }, ChatMessage::Developer { content, tools } => TemplateMessage { - role: "developer", + role: if supports_developer_role { + "developer" + } else { + "system" + }, content: to_template_content(content, content_format, multimodal)?, - tools: tools.as_deref().map(to_template_tools), + tools: supports_developer_role + .then(|| tools.as_deref().map(to_template_tools)) + .flatten(), reasoning: None, reasoning_content: None, tool_calls: None, @@ -520,31 +676,29 @@ fn to_template_string_content( match content { ChatContent::Text(text) => Ok(text.clone()), ChatContent::Parts(parts) => { - let mut out = String::new(); - for part in parts { - match part { - ChatContentPart::Text { text } => out.push_str(text), - ChatContentPart::ImageUrl { .. } => { - let image_token = multimodal - .and_then(|multimodal| multimodal.image_token.as_ref()) - .ok_or(Error::UnsupportedMultimodalContent("image_url"))?; - out.push_str(image_token); - } - ChatContentPart::VideoUrl { .. } => { - let video_token = multimodal - .and_then(|multimodal| multimodal.video_token.as_ref()) - .ok_or(Error::UnsupportedMultimodalContent("video_url"))?; - out.push_str(video_token); - } + // Same as Python with `--interleave-mm-strings`: each part becomes one segment, + // media as its modality's placeholder in place, empty text parts are dropped, and + // the segments are joined with "\n". Python's default moves placeholders to the + // front instead. + let segments: Vec<&str> = parts + .iter() + .filter(|part| !part.is_empty_text()) + .map(|part| match part { + ChatContentPart::Text { text } => Ok(text.as_str()), + ChatContentPart::ImageUrl { .. } => multimodal + .and_then(|multimodal| multimodal.image_token.as_deref()) + .ok_or(Error::UnsupportedMultimodalContent("image_url")), + ChatContentPart::VideoUrl { .. } => multimodal + .and_then(|multimodal| multimodal.video_token.as_deref()) + .ok_or(Error::UnsupportedMultimodalContent("video_url")), ChatContentPart::InputAudio { .. } | ChatContentPart::AudioUrl { .. } => { - let audio_token = multimodal - .and_then(|multimodal| multimodal.audio_token.as_ref()) - .ok_or(Error::UnsupportedMultimodalContent("audio"))?; - out.push_str(audio_token); + multimodal + .and_then(|multimodal| multimodal.audio_token.as_deref()) + .ok_or(Error::UnsupportedMultimodalContent("audio")) } - } - } - Ok(out) + }) + .collect::>()?; + Ok(segments.join("\n")) } } } @@ -690,7 +844,7 @@ mod tests { }; let rendered = render( - Some("{{ messages|length }}:{{ messages[0].role }}:{{ messages[0].tools[0].function.name }}:{{ tools[0].function.name }}"), + Some("{% set developer_role = 'developer' %}{{ messages|length }}:{{ messages[0].role }}:{{ messages[0].tools[0].function.name }}:{{ tools[0].function.name }}"), &request, ) .unwrap(); @@ -779,7 +933,7 @@ mod tests { ) .unwrap(); - assert_eq!(rendered.prompt, Prompt::Text("ab".to_string())); + assert_eq!(rendered.prompt, Prompt::Text("a\n\nb".to_string())); } #[test] @@ -791,7 +945,7 @@ mod tests { ) .unwrap(); - assert_eq!(rendered.prompt, Prompt::Text("a