diff --git a/.agents/skills/triton-kernel-writing/SKILL.md b/.agents/skills/triton-kernel-writing/SKILL.md index 6dd6a8520be0..a41de016b78d 100644 --- a/.agents/skills/triton-kernel-writing/SKILL.md +++ b/.agents/skills/triton-kernel-writing/SKILL.md @@ -22,10 +22,14 @@ description: Write or review Triton kernels for vLLM, with practical guidance fo a small, legible heuristic when workloads need different choices. Use `triton.autotune` only when tuning is critical to performance, such as for a matrix multiplication. Otherwise prioritize simple code and fast startup. -- Be careful to avoid unintended runtime JIT compilation. For example, put - unimportant runtime integer scalars in `do_not_specialize`, especially those - that may alternate between values such as 0 and 1, which can produce - different specialization keys. +- Avoid unintended runtime JIT compilation. Put unimportant runtime integer + scalars in `do_not_specialize`, especially values that alternate between 0 + and 1. Pointer alignment also affects specialization: an offset tensor view + can trigger a new variant after warmup used an aligned allocation. For + scalar-only pointers whose alignment provides no useful optimization, use + `@triton.jit(do_not_specialize_on_alignment=["end_ptr"])`. Retain alignment + specialization when vectorized accesses benefit from it. See the + [Triton JIT options](https://triton-lang.org/main/python-api/generated/triton.jit.html). - The Triton compiler does not guarantee safe ordering when a kernel writes to a pointer and subsequently reads from the same pointer. This pattern must have a `tl.debug_barrier()` between the write and read. The barrier @@ -49,7 +53,9 @@ description: Write or review Triton kernels for vLLM, with practical guidance fo ## Validation - Check correctness at boundary shapes and at sizes that exercise masks and - large offsets. + large offsets. Include offset tensor views, not only fresh allocations. + Warm aligned buffers, then exercise offset views with vLLM's + `--jit-monitor-mode error --jit-monitor-verbose` to catch missing variants. - Choose accumulation and intermediate dtypes explicitly. Test numerically difficult inputs, not only random, well-scaled tensors. - Use `$kernel-microbenchmark` for benchmark construction, measurement, and diff --git a/.buildkite/hardware_tests/cpu.yaml b/.buildkite/hardware_tests/cpu.yaml index 769e3d4adc3a..b50a082727f5 100644 --- a/.buildkite/hardware_tests/cpu.yaml +++ b/.buildkite/hardware_tests/cpu.yaml @@ -52,6 +52,52 @@ steps: tests/v1/sample/test_topk_topp_sampler.py::TestTritonTopkTopp" parallelism: 2 +# Speculative decoding under a CPU platform. The unit tests already pass on +# CPU but no CPU job runs them; the e2e tests gate the engine path itself, +# which unit tests cannot reach. +- label: CPU-Spec Decode Tests + key: cpu-spec-decode-tests + depends_on: [] + soft_fail: true + device: intel_cpu + no_plugin: true + source_file_dependencies: + - vllm/v1/spec_decode/ + - vllm/v1/worker/cpu/ + - vllm/v1/worker/cpu_model_runner.py + - vllm/v1/worker/gpu/spec_decode/ + - vllm/v1/attention/backends/cpu_attn.py + - vllm/config/speculative.py + - vllm/model_executor/models/qwen3_5.py + - vllm/model_executor/models/qwen3_5_mtp.py + - vllm/model_executor/models/llama_eagle3.py + - vllm/model_executor/layers/mamba/gdn/qwen_gdn_linear_attn.py + - vllm/model_executor/layers/mamba/mamba_mixer2.py + - vllm/model_executor/layers/mamba/ops/cpu/ + - tests/v1/e2e/test_cpu_spec_decode.py + - tests/v1/spec_decode/ + - tests/v1/worker/ + commands: + - | + bash .buildkite/scripts/hardware_ci/run-cpu-test.sh 30m " + pytest -x -v -s \ + tests/v1/worker/test_gpu_autoregressive_speculator.py \ + tests/v1/worker/test_gpu_extract_hidden_states_speculator.py \ + tests/v1/worker/test_gpu_batch_ordering.py \ + tests/v1/worker/test_gpu_rejection_sampler_chunking.py \ + tests/v1/worker/test_eagle3_aux_hidden_states_pp.py \ + tests/v1/worker/test_spec_decode_embed_sharing_pp.py \ + tests/v1/spec_decode/test_adaptive_verification.py \ + tests/v1/spec_decode/test_dflash2.py \ + tests/v1/spec_decode/test_dflash_causality.py \ + tests/v1/spec_decode/test_eagle_draft_attn_metadata.py \ + tests/v1/spec_decode/test_draft_attention_backend_override.py \ + tests/v1/spec_decode/test_draft_moe_backend_override.py \ + tests/v1/spec_decode/test_ngram.py \ + tests/v1/spec_decode/test_llm_base_proposer_sampling.py + unset VLLM_CPU_CI_ENV + pytest -x -v -s tests/v1/e2e/test_cpu_spec_decode.py" + # Note: SDE can't be downloaded from CI host because of AWS WAF # - label: CPU-Compatibility Tests # depends_on: [] diff --git a/.buildkite/image_build/image_build.sh b/.buildkite/image_build/image_build.sh index b81cd4ec0cbe..6d22acd5b16c 100755 --- a/.buildkite/image_build/image_build.sh +++ b/.buildkite/image_build/image_build.sh @@ -79,6 +79,54 @@ setup_buildx_builder() { docker buildx ls | grep -E '^\*|^NAME' || docker buildx ls } +download_ci_hcl() { + # Returns non-zero instead of exiting: the optional export path must be + # able to give up without failing a build whose image already exists. + echo "--- :arrow_down: Downloading ci.hcl" + curl -sSfL -o "${CI_HCL_PATH}" "${CI_HCL_URL}" || return $? + echo "Downloaded to ${CI_HCL_PATH}" + + if [[ ! -f "${CI_HCL_PATH}" ]]; then + echo "Error: ci.hcl not found at ${CI_HCL_PATH}" + return 1 + fi +} + +export_kernel_symbol_map_from_cache() { + # The existing-image path: everything here is best effort. Called as + # `... || echo`, so errexit is suspended inside and any failure just + # returns; the build keeps the image it already has. + local rc=0 + download_ci_hcl \ + && setup_buildx_builder \ + && resolve_parent_commit \ + && export PARENT_COMMIT \ + && BUILD_TMP_DIR="$(mktemp -d)" \ + && export_kernel_symbol_map || rc=$? + rm -rf -- "${BUILD_TMP_DIR:-}" + return "${rc}" +} + +export_kernel_symbol_map() { + # Only when the build asked for it (nightly/post-merge). The map was + # produced inside the csrc-build stage during the main bake; this second + # bake of the scratch stage is a cache hit that just writes the file out. + if [[ "${VLLM_KERNEL_SYMBOL_MAP:-0}" != "1" ]]; then + return 0 + fi + echo "--- :world_map: Exporting kernel symbol map" + local out_dir="${BUILD_TMP_DIR}/kernel-symbol-map" + mkdir -p "${out_dir}" + if docker buildx bake -f "${VLLM_BAKE_FILE_PATH}" -f "${CI_HCL_PATH}" --progress plain \ + --set "kernel-symbol-map.output=type=local,dest=${out_dir}" kernel-symbol-map \ + && [[ -s "${out_dir}/kernel_symbol_map.json.gz" ]]; then + ls -la "${out_dir}" + (cd "${out_dir}" && buildkite-agent artifact upload "kernel_symbol_map.json.gz") + else + echo "kernel symbol map export failed; continuing without it" >&2 + fi +} + annotate_image_tags() { .buildkite/scripts/annotate-image-build.sh \ "${IMAGE_TAG:-}" "${IMAGE_TAG_LATEST:-}" @@ -90,6 +138,15 @@ check_and_skip_if_image_exists() { if docker manifest inspect "${IMAGE_TAG}" >/dev/null 2>&1; then echo "Image already exists: ${IMAGE_TAG}" echo "Skipping build" + if [[ "${VLLM_KERNEL_SYMBOL_MAP:-0}" == "1" ]]; then + # The image is reused, but the symbol map is an artifact of + # this build. Bake just the export target: its layers come + # from the registry cache, and the map step itself reruns + # if the cached image was built without the arg. Never + # fails the build: the image is already there. + export_kernel_symbol_map_from_cache \ + || echo "kernel symbol map: export from the cached image failed; continuing" >&2 + fi annotate_image_tags exit 0 fi @@ -154,7 +211,7 @@ print_bake_config() { local bake_tmp bake_tmp="$(mktemp -d)" BAKE_CONFIG_FILE="${bake_tmp}/bake-config-build-${BUILDKITE_BUILD_NUMBER:-local}.json" - docker buildx bake -f "${VLLM_BAKE_FILE_PATH}" -f "${CI_HCL_PATH}" --print "${TARGET}" | tee "${BAKE_CONFIG_FILE}" || true + docker buildx bake "${BAKE_FILES[@]}" --print "${TARGET}" | tee "${BAKE_CONFIG_FILE}" || true echo "Saved bake config to ${BAKE_CONFIG_FILE}" echo "--- :arrow_down: Uploading bake config to Buildkite" (cd "$(dirname "${BAKE_CONFIG_FILE}")" && buildkite-agent artifact upload "$(basename "${BAKE_CONFIG_FILE}")") @@ -230,8 +287,10 @@ TARGET="test-ci" VLLM_BAKE_FILE_PATH="${VLLM_BAKE_FILE_PATH:-docker/docker-bake.hcl}" BUILDER_NAME="${BUILDER_NAME:-vllm-builder}" CI_HCL_URL="${CI_HCL_URL:-https://raw.githubusercontent.com/vllm-project/ci-infra/main/docker/ci.hcl}" -CI_HCL_PATH="/tmp/ci.hcl" +CI_HCL_PATH="${CI_HCL_PATH:-/tmp/ci.hcl}" +ZSTD_HCL_PATH="${ZSTD_HCL_PATH:-.buildkite/image_build/zstd.hcl}" BUILDKIT_SOCKET="/run/buildkit/buildkitd.sock" +BAKE_FILES=(-f "${VLLM_BAKE_FILE_PATH}" -f "${CI_HCL_PATH}" -f "${ZSTD_HCL_PATH}") prepare_cache_tags ecr_login @@ -288,14 +347,7 @@ if [[ ! -f "${VLLM_BAKE_FILE_PATH}" ]]; then exit 1 fi -echo "--- :arrow_down: Downloading ci.hcl" -curl -sSfL -o "${CI_HCL_PATH}" "${CI_HCL_URL}" -echo "Downloaded to ${CI_HCL_PATH}" - -if [[ ! -f "${CI_HCL_PATH}" ]]; then - echo "Error: ci.hcl not found at ${CI_HCL_PATH}" - exit 1 -fi +download_ci_hcl || exit 1 setup_buildx_builder @@ -309,7 +361,7 @@ BUILD_TMP_DIR="$(mktemp -d)" trap 'rm -rf -- "${BUILD_TMP_DIR}"' EXIT BUILD_METADATA_FILE="${BUILD_TMP_DIR}/build-metadata.json" BUILD_STATUS=0 -docker --debug buildx bake -f "${VLLM_BAKE_FILE_PATH}" -f "${CI_HCL_PATH}" \ +docker --debug buildx bake "${BAKE_FILES[@]}" \ --progress plain --metadata-file "${BUILD_METADATA_FILE}" "${TARGET}" || BUILD_STATUS=$? record_buildkit_trace "${BUILD_METADATA_FILE}" || true @@ -319,5 +371,6 @@ if [[ "${BUILD_STATUS}" -ne 0 ]]; then fi echo "--- :white_check_mark: Build complete" +export_kernel_symbol_map || true annotate_image_tags diff --git a/.buildkite/image_build/image_build_torch_nightly.sh b/.buildkite/image_build/image_build_torch_nightly.sh index 8aa313d0e6d4..56c7116e14ac 100755 --- a/.buildkite/image_build/image_build_torch_nightly.sh +++ b/.buildkite/image_build/image_build_torch_nightly.sh @@ -49,7 +49,16 @@ echo "Image not found, proceeding with build..." # Nightly CI uses CUDA 13.0 while regular CI stays on CUDA 12.9 NIGHTLY_CUDA_VERSION="13.0.2" NIGHTLY_BUILD_BASE_IMAGE="pytorch/manylinux2_28-builder:cuda13.0-78e737ad29420ffc4800e677c51e2a852caf8359" -NIGHTLY_FINAL_BASE_IMAGE="nvidia/cuda:${NIGHTLY_CUDA_VERSION}-base-ubuntu22.04" +# Must track ARG UBUNTU_VERSION in docker/Dockerfile. The nightly lane exists to +# run the normal pipeline against nightly torch, so it has to differ from the +# regular image in torch only -- not in the OS underneath it. This was pinned to +# 22.04 while everything else moved to 24.04, and the runtime dependency list in +# tools/install_snapshot_runtime.sh is 24.04-only (libgnutls30t64 is the 64-bit +# time_t rename and does not exist on jammy), so the nightly build failed with +# "E: Unable to locate package libgnutls30t64" while regular CI stayed green on +# a cached layer. +NIGHTLY_UBUNTU_VERSION="24.04" +NIGHTLY_FINAL_BASE_IMAGE="nvidia/cuda:${NIGHTLY_CUDA_VERSION}-base-ubuntu${NIGHTLY_UBUNTU_VERSION}" echo "--- :docker: Building torch nightly image (CUDA ${NIGHTLY_CUDA_VERSION})" docker buildx build --file docker/Dockerfile \ diff --git a/.buildkite/image_build/zstd.hcl b/.buildkite/image_build/zstd.hcl new file mode 100644 index 000000000000..1f795c5f9150 --- /dev/null +++ b/.buildkite/image_build/zstd.hcl @@ -0,0 +1,3 @@ +target "test-ci" { + output = ["type=registry,compression=zstd,compression-level=3,force-compression=true,oci-mediatypes=true"] +} diff --git a/.buildkite/intel_jobs/basic_correctness_intel.yaml b/.buildkite/intel_jobs/basic_correctness_intel.yaml index 292920c47ea1..173baf40f62c 100644 --- a/.buildkite/intel_jobs/basic_correctness_intel.yaml +++ b/.buildkite/intel_jobs/basic_correctness_intel.yaml @@ -3,6 +3,7 @@ depends_on: - image-build-xpu steps: - label: XPU Sleep Mode + key: xpu-sleep-mode timeout_in_minutes: 45 device: intel_gpu agent_tags: diff --git a/.buildkite/intel_jobs/engine_intel.yaml b/.buildkite/intel_jobs/engine_intel.yaml index d0aabd6d3299..98218f23b785 100644 --- a/.buildkite/intel_jobs/engine_intel.yaml +++ b/.buildkite/intel_jobs/engine_intel.yaml @@ -41,6 +41,7 @@ steps: pytest -v -s engine/test_arg_utils.py test_sequence.py test_logger.py test_vllm_port.py jit_monitor/test_hooks.py' - label: Engine (1 GPU) + key: engine-1-gpu timeout_in_minutes: 30 device: intel_gpu agent_tags: @@ -62,9 +63,10 @@ steps: bash .buildkite/scripts/hardware_ci/run-intel-test.sh 'cd tests && pytest -v -s v1/engine --ignore v1/engine/test_preprocess_error_handling.py && - VLLM_XPU_ENABLE_XPU_GRAPH=1 pytest -v -s test_config.py' + pytest -v -s test_config.py' - label: V1 e2e (2 GPUs) + key: v1-e2e-2-gpus timeout_in_minutes: 30 device: intel_gpu agent_tags: diff --git a/.buildkite/intel_jobs/entrypoints_intel.yaml b/.buildkite/intel_jobs/entrypoints_intel.yaml index efed04c1b383..e8006bac8f66 100644 --- a/.buildkite/intel_jobs/entrypoints_intel.yaml +++ b/.buildkite/intel_jobs/entrypoints_intel.yaml @@ -134,7 +134,8 @@ steps: 'cd tests && pytest -v -s tool_use --deselect="tests/tool_use/mistral/test_mistral_tool_calls.py::test_tool_call_none_with_tools[ministral-3b]" - --deselect="tests/tool_use/test_chat_completions.py::test_chat_completion_with_tools[granite-3.0-8b]" && + --deselect="tests/tool_use/test_chat_completions.py::test_chat_completion_with_tools[granite-3.0-8b]" + --deselect="tests/tool_use/test_chat_completions.py::test_chat_completion_with_tools[llama3.2]" && pytest -v -s entrypoints/tool_parsers && pytest -v -s entrypoints/generate && pytest -v -s entrypoints/anthropic && diff --git a/.buildkite/intel_jobs/kernels_intel.yaml b/.buildkite/intel_jobs/kernels_intel.yaml index e914c10d8bfe..da6086c1dc43 100644 --- a/.buildkite/intel_jobs/kernels_intel.yaml +++ b/.buildkite/intel_jobs/kernels_intel.yaml @@ -3,6 +3,7 @@ depends_on: - image-build-xpu steps: - label: vLLM IR Tests + key: vllm-ir-tests timeout_in_minutes: 30 device: intel_gpu agent_tags: diff --git a/.buildkite/intel_jobs/lm_eval_intel.yaml b/.buildkite/intel_jobs/lm_eval_intel.yaml index 966ced8156d9..e1f649ad91c7 100644 --- a/.buildkite/intel_jobs/lm_eval_intel.yaml +++ b/.buildkite/intel_jobs/lm_eval_intel.yaml @@ -23,6 +23,30 @@ steps: commands: - >- bash .buildkite/scripts/hardware_ci/run-intel-test.sh - 'pip install "gpt-oss[eval]==0.0.5" && + 'uv pip install "gpt-oss[eval]==0.0.5" && cd tests && pytest -s -v evals/gpt_oss/test_gpqa_correctness.py --config-list-file=configs/models-xpu.txt' +- label: LM Eval Large Models(2 GPUs) + key: lm-eval-large-models-2-gpus + timeout_in_minutes: 45 + device: intel_gpu + agent_tags: + label: production + gpu: 2+ + mem: 24+ + no_plugin: true + optional: true + working_dir: "." + env: + REGISTRY: "public.ecr.aws/q9t5s3a7" + REPO: "vllm-ci-test-repo" + VLLM_TEST_DEVICE: "xpu" + source_file_dependencies: + - vllm/ + - .buildkite/lm-eval-harness/ + commands: + - >- + bash .buildkite/scripts/hardware_ci/run-intel-test.sh + 'cd .buildkite/lm-eval-harness && + export VLLM_USE_DEEP_GEMM=0 && + pytest -s -v test_lm_eval_correctness.py --config-list-file=configs/models-large-xpu-fp8.txt --tp-size=2' diff --git a/.buildkite/intel_jobs/lora_intel.yaml b/.buildkite/intel_jobs/lora_intel.yaml index cab0cb66aa45..2cecbf18f773 100644 --- a/.buildkite/intel_jobs/lora_intel.yaml +++ b/.buildkite/intel_jobs/lora_intel.yaml @@ -3,6 +3,7 @@ depends_on: - image-build-xpu steps: - label: LoRA Runtime + Utils + key: lora-runtime-utils timeout_in_minutes: 45 device: intel_gpu agent_tags: @@ -36,6 +37,7 @@ steps: pytest -v -s lora/test_worker.py' - label: LoRA Fused/MoE Kernels + key: lora-fused-moe-kernels timeout_in_minutes: 45 device: intel_gpu agent_tags: @@ -60,6 +62,7 @@ steps: 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]"' - label: LoRA Punica Kernels + key: lora-punica-kernels timeout_in_minutes: 45 device: intel_gpu agent_tags: @@ -86,6 +89,7 @@ steps: pytest -v -s lora/test_punica_ops.py::test_add_lora_fused_moe_early_exit' - label: LoRA Punica FP8/XPU Ops + key: lora-punica-fp8-xpu-ops timeout_in_minutes: 60 device: intel_gpu agent_tags: @@ -110,6 +114,7 @@ steps: pytest -v -s lora/test_punica_xpu_ops.py' - label: LoRA Models + key: lora-models timeout_in_minutes: 45 device: intel_gpu agent_tags: @@ -137,6 +142,7 @@ steps: pytest -s -v lora/test_minicpmv_tp.py' - label: LoRA Multimodal + key: lora-multimodal timeout_in_minutes: 45 device: intel_gpu agent_tags: diff --git a/.buildkite/intel_jobs/misc_intel.yaml b/.buildkite/intel_jobs/misc_intel.yaml index 752870abe36b..5fa4d833d7f0 100644 --- a/.buildkite/intel_jobs/misc_intel.yaml +++ b/.buildkite/intel_jobs/misc_intel.yaml @@ -2,7 +2,36 @@ group: Miscellaneous Intel depends_on: - image-build-xpu steps: +- label: XPU Batch Invariance + key: xpu-batch-invariance + timeout_in_minutes: 60 + num_devices: 2 + device: intel_gpu + agent_tags: + label: production + gpu: 2+ + mem: 24+ + no_plugin: true + working_dir: "." + env: + REGISTRY: "public.ecr.aws/q9t5s3a7" + REPO: "vllm-ci-test-repo" + VLLM_TEST_DEVICE: "xpu" + source_file_dependencies: + - vllm/ + - tests/v1/determinism/test_xpu_batch_invariant_ut.py + - .buildkite/scripts/hardware_ci/run-xpu-batch-invariance.sh + - .buildkite/intel_jobs/misc_intel.yaml + commands: + - >- + bash .buildkite/scripts/hardware_ci/run-intel-test.sh + 'export VLLM_BATCH_INVARIANT=1 && + cd tests && + pytest -v -s v1/determinism/test_xpu_batch_invariant_ut.py && + bash ../.buildkite/scripts/hardware_ci/run-xpu-batch-invariance.sh' + - label: V1 Core + KV + Metrics + key: v1-core-kv-metrics-intel timeout_in_minutes: 45 device: intel_gpu agent_tags: @@ -32,9 +61,10 @@ steps: export VLLM_WORKER_MULTIPROC_METHOD=spawn && cd tests && pytest -v -s v1/executor && - VLLM_BATCH_INVARIANT=1 pytest -v -s -m 'not cpu_test' v1/ec_connector/unit' + VLLM_BATCH_INVARIANT=1 pytest -v -s -m "not cpu_test" v1/ec_connector/unit' - label: V1 Sample + Logits + key: v1-sample-logits-intel timeout_in_minutes: 90 device: intel_gpu agent_tags: @@ -67,8 +97,7 @@ steps: commands: - >- bash .buildkite/scripts/hardware_ci/run-intel-test.sh - 'pip install lm_eval[api]>=0.4.12 && - export VLLM_WORKER_MULTIPROC_METHOD=spawn && + 'export VLLM_WORKER_MULTIPROC_METHOD=spawn && cd tests && pytest -v -s v1/logits_processors --ignore=v1/logits_processors/test_custom_online.py --ignore=v1/logits_processors/test_custom_offline.py && pytest -v -s v1/test_oracle.py v1/test_request.py v1/test_outputs.py && @@ -80,6 +109,7 @@ steps: parallelism: 4 - label: Basic Models Tests (Initialization) + key: basic-models-tests-initialization timeout_in_minutes: 60 device: intel_gpu agent_tags: @@ -104,6 +134,7 @@ steps: pytest -v -s models/test_initialization.py::test_can_initialize_large_subset[Eagle3MiniMaxM2ForCausalLM]' - label: XPU CPU Offload + key: xpu-cpu-offload timeout_in_minutes: 60 device: intel_gpu agent_tags: @@ -131,6 +162,7 @@ steps: pytest -v -s v1/kv_connector/unit/test_offloading_connector.py' - label: NixlConnector PD accuracy (4 GPUs) + key: nixlconnector-pd-accuracy-4-gpus timeout_in_minutes: 60 num_devices: 4 device: intel_gpu @@ -188,8 +220,7 @@ steps: commands: - >- bash .buildkite/scripts/hardware_ci/run-intel-test.sh - 'pip install modelscope\<1.38 && - cd tests && + 'cd tests && pytest -v -s test_regression.py' - label: Metrics, Tracing (2 GPUs) @@ -224,8 +255,7 @@ steps: commands: - >- bash .buildkite/scripts/hardware_ci/run-intel-test.sh - '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 && - cd tests && + 'cd tests && pytest -v -s v1/tracing' - label: Async Engine, Inputs, Utils, Worker @@ -263,12 +293,12 @@ steps: - >- bash .buildkite/scripts/hardware_ci/run-intel-test.sh 'cd tests && - pip install av && pytest -v -s detokenizer && pytest -v -s -m "not cpu_test" ./multimodal && pytest -v -s utils_ --ignore=utils_/test_mem_utils.py' - label: Fusion Unit Tests + key: fusion-unit-tests timeout_in_minutes: 30 device: intel_gpu agent_tags: @@ -288,4 +318,4 @@ steps: - >- bash .buildkite/scripts/hardware_ci/run-intel-test.sh 'cd tests && - pytest -v -s compile/passes/test_qk_norm_rope_fusion.py' \ No newline at end of file + pytest -v -s compile/passes/test_qk_norm_rope_fusion.py' diff --git a/.buildkite/intel_jobs/model_executor_intel.yaml b/.buildkite/intel_jobs/model_executor_intel.yaml index 14a853544eca..d7c683f8cea3 100644 --- a/.buildkite/intel_jobs/model_executor_intel.yaml +++ b/.buildkite/intel_jobs/model_executor_intel.yaml @@ -25,8 +25,6 @@ steps: - >- bash .buildkite/scripts/hardware_ci/run-intel-test.sh 'apt-get update && apt-get install -y curl libsodium23 && - pip3 install tensorizer==2.10.1 && - pip3 install runai-model-streamer[s3,gcs,azure]\>=0.15.7 && export VLLM_WORKER_MULTIPROC_METHOD=spawn && export PYTHONFAULTHANDLER=1 && cd tests && diff --git a/.buildkite/intel_jobs/model_runner_v2_intel.yaml b/.buildkite/intel_jobs/model_runner_v2_intel.yaml deleted file mode 100644 index 1f68333e976f..000000000000 --- a/.buildkite/intel_jobs/model_runner_v2_intel.yaml +++ /dev/null @@ -1,116 +0,0 @@ -group: Model Runner V2 Intel -depends_on: - - image-build-xpu -steps: -- label: Model Runner V2 Core Tests (Intel) - timeout_in_minutes: 45 - device: intel_gpu - agent_tags: - label: production - gpu: 2+ - mem: 24+ - 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/worker/gpu/ - - vllm/v1/worker/gpu_worker.py - - vllm/v1/core/sched/ - - vllm/v1/attention/ - - tests/v1/engine/test_llm_engine.py - - tests/v1/e2e/ - commands: - - >- - bash .buildkite/scripts/hardware_ci/run-intel-test.sh - 'export VLLM_USE_V2_MODEL_RUNNER=1 && - cd tests && - pytest -v -s v1/engine/test_llm_engine.py -k "not test_engine_metrics" && - pytest -v -s v1/e2e/general/test_context_length.py && - ENFORCE_EAGER=1 pytest -v -s v1/e2e/general/test_async_scheduling.py -k "not ngram" && - pytest -v -s entrypoints/llm/test_struct_output_generate.py -k "xgrammar and not speculative_config6 and not speculative_config7 and not speculative_config8 and not speculative_config0" && - pytest -v -s v1/e2e/general/test_min_tokens.py' - -- label: Model Runner V2 Examples (Intel) - timeout_in_minutes: 45 - device: intel_gpu - agent_tags: - label: production - gpu: 1+ - mem: 24+ - 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/worker/gpu/ - - vllm/v1/core/sched/ - - vllm/v1/worker/gpu_worker.py - - examples/basic/offline_inference/ - - examples/generate/multimodal/ - - examples/features/ - commands: - - >- - bash .buildkite/scripts/hardware_ci/run-intel-test.sh - 'export VLLM_USE_V2_MODEL_RUNNER=1 && - cd examples && - python3 basic/offline_inference/chat.py && - python3 basic/offline_inference/generate.py --model facebook/opt-125m && - python3 generate/multimodal/vision_language_offline.py --seed 0 && - python3 features/automatic_prefix_caching/prefix_caching_offline.py' - -- label: Model Runner V2 Distributed (2 GPUs) - timeout_in_minutes: 50 - device: intel_gpu - agent_tags: - label: production - gpu: 2+ - 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/worker/gpu/ - - vllm/v1/worker/gpu_worker.py - - tests/basic_correctness/test_basic_correctness.py - - tests/v1/distributed/test_async_llm_dp.py - - tests/v1/distributed/test_eagle_dp.py - commands: - - >- - bash .buildkite/scripts/hardware_ci/run-intel-test.sh - 'export VLLM_USE_V2_MODEL_RUNNER=1 && - cd tests && - TARGET_TEST_SUITE=L4 pytest -v -s basic_correctness/test_basic_correctness.py -m "distributed\(num_gpus=2\)" -k "not ray and not True"' - -- label: Model Runner V2 Spec Decode - timeout_in_minutes: 50 - device: intel_gpu - agent_tags: - label: production - gpu: 1+ - mem: 24+ - 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/worker/gpu/ - - vllm/v1/worker/gpu_worker.py - - tests/v1/spec_decode/test_max_len.py - - tests/v1/spec_decode/test_rejection_sampler_utils.py - - tests/v1/spec_decode/test_synthetic_rejection_sampler_utils.py - commands: - - >- - bash .buildkite/scripts/hardware_ci/run-intel-test.sh - 'export VLLM_USE_V2_MODEL_RUNNER=1 && - cd tests && - pytest -v -s v1/spec_decode/test_synthetic_rejection_sampler_utils.py' diff --git a/.buildkite/intel_jobs/models_multimodal_intel.yaml b/.buildkite/intel_jobs/models_multimodal_intel.yaml index d99a7debb0b5..58ecb2d28209 100644 --- a/.buildkite/intel_jobs/models_multimodal_intel.yaml +++ b/.buildkite/intel_jobs/models_multimodal_intel.yaml @@ -22,8 +22,7 @@ steps: commands: - >- bash .buildkite/scripts/hardware_ci/run-intel-test.sh - 'pip install av && - cd tests && + 'cd tests && pytest -v -s models/multimodal/generation/test_common.py -m core_model -k "qwen2" && pytest -v -s models/multimodal/generation/test_ultravox.py -m core_model' @@ -96,8 +95,7 @@ steps: commands: - >- bash .buildkite/scripts/hardware_ci/run-intel-test.sh - 'pip install av && - cd tests && + 'cd tests && pytest -v -s models/multimodal -m core_model --ignore models/multimodal/generation/test_common.py --ignore models/multimodal/generation/test_ultravox.py --ignore models/multimodal/generation/test_qwen2_5_vl.py --ignore models/multimodal/generation/test_qwen2_vl.py --ignore models/multimodal/generation/test_whisper.py --ignore models/multimodal/generation/test_memory_leak.py --ignore models/multimodal/processing' - label: Multi-Modal Processor # 44min @@ -121,10 +119,9 @@ steps: commands: - >- bash .buildkite/scripts/hardware_ci/run-intel-test.sh - 'pip install av matplotlib ftfy && - pip install open-clip-torch --no-deps && - cd tests && + 'cd tests && pytest -v -s models/multimodal/processing/test_tensor_schema.py + --deselect="tests/models/multimodal/processing/test_tensor_schema.py::test_model_tensor_schema[deepseek-ai/DeepSeek-V4.1-Flash]" --num-shards=$$BUILDKITE_PARALLEL_JOB_COUNT --shard-id=$$BUILDKITE_PARALLEL_JOB' parallelism: 4 diff --git a/.buildkite/intel_jobs/test-intel.yaml b/.buildkite/intel_jobs/test-intel.yaml index 0bc25261a2b3..e15b20ea405e 100644 --- a/.buildkite/intel_jobs/test-intel.yaml +++ b/.buildkite/intel_jobs/test-intel.yaml @@ -15,9 +15,10 @@ steps: - exit_status: -10 # Agent was lost limit: 2 - label: "XPU example Test" + key: xpu-example-test depends_on: - image-build-xpu - timeout_in_minutes: 50 + timeout_in_minutes: 70 device: intel_gpu agent_tags: label: production @@ -33,8 +34,7 @@ steps: commands: - >- bash .buildkite/scripts/hardware_ci/run-intel-test.sh - 'pip install tblib==3.1.0 && - python3 examples/basic/offline_inference/generate.py --model facebook/opt-125m --block-size 64 --enforce-eager && + 'python3 examples/basic/offline_inference/generate.py --model facebook/opt-125m --block-size 64 --enforce-eager && python3 examples/basic/offline_inference/generate.py --model facebook/opt-125m --block-size 64 -O3 -cc.cudagraph_mode=NONE && python3 examples/basic/offline_inference/generate.py --model facebook/opt-125m --block-size 64 --enforce-eager -tp 2 --distributed-executor-backend mp && python3 examples/basic/offline_inference/generate.py --model facebook/opt-125m --block-size 64 --enforce-eager --attention-backend=TRITON_ATTN && @@ -47,9 +47,10 @@ steps: python3 examples/basic/offline_inference/generate.py --model ibm-research/PowerMoE-3b --block-size 64 --enforce-eager -tp 2 --enable-expert-parallel && python3 examples/basic/offline_inference/generate.py --model superjob/Qwen3-4B-Instruct-2507-GPTQ-Int4 --max-model-len 8192 && VLLM_XPU_FUSED_MOE_USE_REF=1 python3 examples/basic/offline_inference/generate.py --model Qwen/Qwen3-30B-A3B-Instruct-2507-FP8 --enforce-eager -tp 2 --max-model-len 8192 && - python3 examples/basic/offline_inference/generate.py --model INCModel/Qwen3-30B-A3B-Instruct-2507-MXFP4-LLMC --enforce-eager -tp 2 --max-model-len 8192 + python3 examples/basic/offline_inference/generate.py --model INCModel/Qwen3-30B-A3B-Instruct-2507-MXFP4-LLMC -tp 2 --max-model-len 8192 ' - label: "XPU W8A8 FP8 Linear Examples" + key: xpu-w8a8-fp8-linear-examples depends_on: - image-build-xpu timeout_in_minutes: 60 @@ -77,6 +78,7 @@ steps: python3 examples/basic/offline_inference/generate.py --linear-backend torch --model meta-llama/Llama-3.2-1B-Instruct --quantization fp8 --enforce-eager --max-model-len 4096 ' - label: "XPU V1 test" + key: xpu-v1-test depends_on: - image-build-xpu timeout_in_minutes: 70 @@ -95,7 +97,7 @@ steps: commands: - >- bash .buildkite/scripts/hardware_ci/run-intel-test.sh - 'cd tests && + 'cd tests && pip install dpctl && pytest -v -s v1/core --ignore=v1/core/test_reset_prefix_cache_e2e.py --ignore=v1/core/test_scheduler_e2e.py && pytest -v -s v1/engine --ignore=v1/engine/test_output_processor.py && pytest -v -s v1/sample --ignore=v1/sample/test_logprobs.py --ignore=v1/sample/test_logprobs_e2e.py -k "not test_topk_only and not test_topp_only and not test_topk_and_topp" && @@ -103,10 +105,11 @@ steps: pytest -v -s v1/structured_output && pytest -v -s v1/test_serial_utils.py && pytest -v -s v1/e2e/general/test_correctness_sliding_window.py && - pytest -v -s v1/spec_decode --ignore=v1/spec_decode/test_max_len.py --ignore=v1/spec_decode/test_speculators_eagle3.py --ignore=v1/spec_decode/test_acceptance_length.py --ignore=v1/spec_decode/test_speculators_correctness.py && - pytest -v -s v1/kv_connector/unit --ignore=v1/kv_connector/unit/test_lmcache_integration.py --ignore=v1/kv_connector/unit/test_hf3fs_client.py --ignore=v1/kv_connector/unit/test_hf3fs_connector.py --ignore=v1/kv_connector/unit/test_hf3fs_metadata_server.py --ignore=v1/kv_connector/unit/test_offloading_connector.py' + pytest -v -s v1/spec_decode --ignore=v1/spec_decode/test_max_len.py --ignore=v1/spec_decode/test_speculators_eagle3.py --ignore=v1/spec_decode/test_acceptance_length.py --ignore=v1/spec_decode/test_speculators_correctness.py --deselect=tests/v1/spec_decode/test_mtp.py::test_glm_mtp_defers_lm_head && + pytest -v -s v1/kv_connector/unit --ignore=v1/kv_connector/unit/test_lmcache_integration.py --ignore=v1/kv_connector/unit/test_hf3fs_client.py --ignore=v1/kv_connector/unit/test_hf3fs_connector.py --ignore=v1/kv_connector/unit/test_hf3fs_metadata_server.py --ignore=v1/kv_connector/unit/test_offloading_connector.py --deselect "v1/kv_connector/unit/test_nixl_connector.py::test_abort_timeout_on_prefiller[ray]" --ignore=v1/kv_connector/unit/test_nixl_connector_hma.py' parallelism: 2 - label: "XPU server test" + key: xpu-server-test depends_on: - image-build-xpu timeout_in_minutes: 45 @@ -125,11 +128,11 @@ steps: commands: - >- bash .buildkite/scripts/hardware_ci/run-intel-test.sh - 'pip install av && - cd tests && + 'cd tests && pytest -v -s entrypoints/multimodal/openai/chat_completion/test_audio_in_video.py && pytest -v -s benchmarks/test_serve_cli.py' - label: "XPU quantization test" + key: xpu-quantization-test depends_on: - image-build-xpu timeout_in_minutes: 30 @@ -145,19 +148,14 @@ steps: source_file_dependencies: - vllm/ - .buildkite/intel_jobs/test-intel.yaml - # TODO: XPU fused MoE + model runner V2 currently hangs on some - # pre-quantized models, e.g. - # quantization/test_online.py::test_online_quantization_loads_real_weights[nm-testing/tinysmokeqwen3moe-W4A16-first-only-CTstable] - # Remove the VLLM_USE_V2_MODEL_RUNNER=0 override below once the hang is - # root-caused and fixed. commands: - >- bash .buildkite/scripts/hardware_ci/run-intel-test.sh 'cd tests && - export VLLM_USE_V2_MODEL_RUNNER=0 && pytest -v -s quantization/test_auto_round.py && pytest -v -s quantization/test_online.py' - label: "XPU compressed tensors FP8 test" + key: xpu-compressed-tensors-fp8-test depends_on: - image-build-xpu timeout_in_minutes: 60 @@ -182,6 +180,7 @@ steps: pytest -v -s quantization/test_compressed_tensors.py::test_compressed_tensors_fp8 && pytest -v -s quantization/test_compressed_tensors.py::test_compressed_tensors_fp8_block_enabled' - label: "XPU Graph" + key: xpu-graph depends_on: - image-build-xpu timeout_in_minutes: 60 @@ -197,16 +196,20 @@ steps: VLLM_TEST_DEVICE: "xpu" source_file_dependencies: - vllm/compilation/ + - vllm/config/compilation.py - vllm/platforms/xpu.py + - vllm/v1/cudagraph_dispatcher.py + - vllm/v1/worker/encoder_cudagraph.py + - vllm/v1/worker/encoder_cudagraph_defs.py - vllm/v1/worker/xpu_model_runner.py - vllm/v1/worker/xpu_worker.py + - tests/v1/cudagraph - .buildkite/intel_jobs/test-intel.yaml commands: - >- bash .buildkite/scripts/hardware_ci/run-intel-test.sh - 'export VLLM_XPU_ENABLE_XPU_GRAPH=1 && - python3 examples/basic/offline_inference/generate.py --model Qwen/Qwen3-0.6B && - python3 examples/basic/offline_inference/generate.py --model Qwen/Qwen3-0.6B --kv-cache-dtype fp8 && - python3 examples/basic/offline_inference/generate.py --model INCModel/Qwen3-30B-A3B-Instruct-2507-MXFP4-CT-AutoRound --max-model-len 4096' - # Temporarily disabled (multi-GPU/MXFP8 not yet supported with XPU Graph): - # python3 examples/basic/offline_inference/generate.py --model INCModel/Qwen3-30B-A3B-Instruct-2507-MXFP8-CT-AutoRound -tp 2 --max-model-len 4096 + 'cd tests && + pytest -v -s v1/cudagraph/test_cudagraph_dispatch.py && + pytest -v -s v1/cudagraph/test_cudagraph_mode.py && + pytest -v -s v1/cudagraph/test_breakable_cudagraph.py && + pytest -v -s v1/cudagraph/test_encoder_cudagraph.py' diff --git a/.buildkite/lm-eval-harness/configs/NVIDIA-Nemotron-3-Nano-30B-A3B-FP8-xpu.yaml b/.buildkite/lm-eval-harness/configs/NVIDIA-Nemotron-3-Nano-30B-A3B-FP8-xpu.yaml new file mode 100644 index 000000000000..72ba8611953c --- /dev/null +++ b/.buildkite/lm-eval-harness/configs/NVIDIA-Nemotron-3-Nano-30B-A3B-FP8-xpu.yaml @@ -0,0 +1,16 @@ +model_name: "nvidia/NVIDIA-Nemotron-3-Nano-30B-A3B-FP8" +tasks: +- name: "gsm8k" + metrics: + - name: "exact_match,strict-match" + value: 0.7142 + - name: "exact_match,flexible-extract" + value: 0.4579 +limit: 256 +num_fewshot: 5 +max_model_len: 262144 +kv_cache_dtype: fp8 +enforce_eager: true +apply_chat_template: true +fewshot_as_multiturn: true +trust_remote_code: true diff --git a/.buildkite/lm-eval-harness/configs/models-large-xpu-fp8.txt b/.buildkite/lm-eval-harness/configs/models-large-xpu-fp8.txt new file mode 100644 index 000000000000..73b28f062c3e --- /dev/null +++ b/.buildkite/lm-eval-harness/configs/models-large-xpu-fp8.txt @@ -0,0 +1 @@ +NVIDIA-Nemotron-3-Nano-30B-A3B-FP8-xpu.yaml \ No newline at end of file diff --git a/.buildkite/lm-eval-harness/test_lm_eval_correctness.py b/.buildkite/lm-eval-harness/test_lm_eval_correctness.py index fe9e332bdfa7..5576c84b05cb 100644 --- a/.buildkite/lm-eval-harness/test_lm_eval_correctness.py +++ b/.buildkite/lm-eval-harness/test_lm_eval_correctness.py @@ -1,7 +1,6 @@ # SPDX-License-Identifier: Apache-2.0 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project -""" -LM eval harness on model to compare vs HF baseline computed offline. +"""LM eval harness on model to compare vs HF baseline computed offline. Configs are found in configs/$MODEL.yaml pytest -s -v test_lm_eval_correctness.py \ diff --git a/.buildkite/performance-benchmarks/scripts/compare-json-results.py b/.buildkite/performance-benchmarks/scripts/compare-json-results.py index c9f8139fe62f..823c7488ec59 100644 --- a/.buildkite/performance-benchmarks/scripts/compare-json-results.py +++ b/.buildkite/performance-benchmarks/scripts/compare-json-results.py @@ -83,8 +83,7 @@ def compare_data_columns( drop_column: str, debug: bool = False, ): - """ - Align concatenation by keys derived from info_cols instead of row order. + """Align concatenation by keys derived from info_cols instead of row order. - Pick one canonical key list: subset of info_cols present in ALL files. - For each file: set index to those keys, aggregate duplicates (mean for metric, first for names). @@ -363,8 +362,7 @@ def _apply_two_decimals( # Export helpers (Excel + CSV) # ----------------------------- def _sanitize_sheet_name(name: str) -> str: - """ - Excel sheet constraints: + """Excel sheet constraints: - max 31 chars - cannot contain: : \ / ? * [ ] - cannot be empty diff --git a/.buildkite/performance-benchmarks/scripts/convert-results-json-to-markdown.py b/.buildkite/performance-benchmarks/scripts/convert-results-json-to-markdown.py index e694c38780cb..deb14a65e4a8 100644 --- a/.buildkite/performance-benchmarks/scripts/convert-results-json-to-markdown.py +++ b/.buildkite/performance-benchmarks/scripts/convert-results-json-to-markdown.py @@ -93,8 +93,7 @@ def results_to_json(latency, throughput, serving): def get_size_with_unit(bytes, suffix="B"): - """ - Scale bytes to its proper format + """Scale bytes to its proper format e.g: 1253656 => '1.20MB' 1253656678 => '1.17GB' diff --git a/.buildkite/release-pipeline.yaml b/.buildkite/release-pipeline.yaml index 4af4eaf56ebc..2b90fef8201c 100644 --- a/.buildkite/release-pipeline.yaml +++ b/.buildkite/release-pipeline.yaml @@ -316,6 +316,66 @@ steps: - "docker push public.ecr.aws/q9t5s3a7/vllm-release-repo:$BUILDKITE_COMMIT-$(uname -m)-cu129" - 'bash .buildkite/scripts/annotate-build-artifact.sh "$$BUILDKITE_LABEL" "public.ecr.aws/q9t5s3a7/vllm-release-repo:$BUILDKITE_COMMIT-$(uname -m)-cu129" release-images' + - label: "Build release image - x86_64 - Rubin CUDA 13.4" + depends_on: ~ + id: build-release-image-x86-rubin-cuda-13-4 + if: build.env("NIGHTLY") == "1" + agents: + queue: cpu_queue_release + commands: + - "aws ecr-public get-login-password --region us-east-1 | docker login --username AWS --password-stdin public.ecr.aws/q9t5s3a7" + - | + DOCKER_BUILDKIT=1 docker buildx build --load \ + $(bash .buildkite/scripts/docker-build-metadata-args.sh cu134) \ + --platform linux/amd64 \ + --build-arg max_jobs=16 \ + --build-arg USE_SCCACHE=1 \ + --build-arg GIT_REPO_CHECK=1 \ + --build-arg CUDA_VERSION=13.4 \ + --build-arg torch_cuda_arch_list="${CUDA_ARCH_X86}" \ + --build-arg INSTALL_KV_CONNECTORS=true \ + --build-arg INSTALL_RUBIN_PRERELEASE=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 \ + --build-arg BUILD_BASE_IMAGE=pytorch/manylinux2_28-builder:cuda13.4 \ + --build-arg FINAL_BASE_IMAGE=nvcr.io/nvidia/cuda-dl-base:26.08-cuda13.4-devel-ubuntu24.04 \ + --target vllm-openai \ + --progress plain \ + -f docker/Dockerfile . + - "docker push public.ecr.aws/q9t5s3a7/vllm-release-repo:$BUILDKITE_COMMIT-$(uname -m)-cu134" + - 'bash .buildkite/scripts/annotate-build-artifact.sh "$$BUILDKITE_LABEL" "public.ecr.aws/q9t5s3a7/vllm-release-repo:$BUILDKITE_COMMIT-$(uname -m)-cu134" release-images' + + - label: "Build release image - aarch64 - Rubin CUDA 13.4" + depends_on: ~ + id: build-release-image-arm64-rubin-cuda-13-4 + if: build.env("NIGHTLY") == "1" + agents: + 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" + - | + DOCKER_BUILDKIT=1 docker buildx build --load \ + $(bash .buildkite/scripts/docker-build-metadata-args.sh cu134) \ + --platform linux/arm64 \ + --build-arg max_jobs=16 \ + --build-arg USE_SCCACHE=1 \ + --build-arg GIT_REPO_CHECK=1 \ + --build-arg CUDA_VERSION=13.4 \ + --build-arg torch_cuda_arch_list="${CUDA_ARCH_AARCH64}" \ + --build-arg INSTALL_KV_CONNECTORS=true \ + --build-arg INSTALL_RUBIN_PRERELEASE=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 \ + --build-arg BUILD_BASE_IMAGE=pytorch/manylinuxaarch64-builder:cuda13.4 \ + --build-arg FINAL_BASE_IMAGE=nvcr.io/nvidia/cuda-dl-base:26.08-cuda13.4-devel-ubuntu24.04 \ + --target vllm-openai \ + --progress plain \ + -f docker/Dockerfile . + - "docker push public.ecr.aws/q9t5s3a7/vllm-release-repo:$BUILDKITE_COMMIT-$(uname -m)-cu134" + - 'bash .buildkite/scripts/annotate-build-artifact.sh "$$BUILDKITE_LABEL" "public.ecr.aws/q9t5s3a7/vllm-release-repo:$BUILDKITE_COMMIT-$(uname -m)-cu134" release-images' + - label: "Build release image - x86_64 - CUDA 13.0 - Ubuntu 24.04" depends_on: ~ id: build-release-image-x86-ubuntu2404 @@ -513,6 +573,20 @@ steps: - "docker manifest push public.ecr.aws/q9t5s3a7/vllm-release-repo:$BUILDKITE_COMMIT-cu129" - 'bash .buildkite/scripts/annotate-build-artifact.sh "Manifest: CUDA 12.9" "public.ecr.aws/q9t5s3a7/vllm-release-repo:$BUILDKITE_COMMIT-cu129" release-manifests' + - label: "Create multi-arch manifest - Rubin CUDA 13.4" + depends_on: + - build-release-image-x86-rubin-cuda-13-4 + - build-release-image-arm64-rubin-cuda-13-4 + id: create-multi-arch-manifest-rubin-cuda-13-4 + if: build.env("NIGHTLY") == "1" + agents: + queue: small_cpu_queue_release + commands: + - "aws ecr-public get-login-password --region us-east-1 | docker login --username AWS --password-stdin public.ecr.aws/q9t5s3a7" + - "docker manifest create public.ecr.aws/q9t5s3a7/vllm-release-repo:$BUILDKITE_COMMIT-cu134 public.ecr.aws/q9t5s3a7/vllm-release-repo:$BUILDKITE_COMMIT-x86_64-cu134 public.ecr.aws/q9t5s3a7/vllm-release-repo:$BUILDKITE_COMMIT-aarch64-cu134 --amend" + - "docker manifest push public.ecr.aws/q9t5s3a7/vllm-release-repo:$BUILDKITE_COMMIT-cu134" + - 'bash .buildkite/scripts/annotate-build-artifact.sh "Manifest: Rubin CUDA 13.4" "public.ecr.aws/q9t5s3a7/vllm-release-repo:$BUILDKITE_COMMIT-cu134" release-manifests' + - label: "Create multi-arch manifest - CUDA 13.0 - Ubuntu 24.04" depends_on: - build-release-image-x86-ubuntu2404 @@ -585,6 +659,24 @@ steps: DOCKER_BUILDKIT: "1" DOCKERHUB_USERNAME: "vllmbot" + - label: "Publish nightly multi-arch Rubin image to DockerHub - CUDA 13.4" + depends_on: + - create-multi-arch-manifest-rubin-cuda-13-4 + if: build.env("NIGHTLY") == "1" + agents: + queue: small_cpu_queue_release + commands: + - "bash .buildkite/scripts/push-nightly-builds.sh cu134" + # Clean up old nightly builds (keep only last 14) + - "bash .buildkite/scripts/cleanup-nightly-builds.sh cu134-nightly-" + plugins: + - docker-login#v3.0.0: + username: vllmbot + password-env: DOCKERHUB_TOKEN + env: + DOCKER_BUILDKIT: "1" + DOCKERHUB_USERNAME: "vllmbot" + - label: "Publish nightly CPU multi-arch image to DockerHub" depends_on: - build-cpu-release-image-x86 diff --git a/.buildkite/scripts/ci-bake-rocm.sh b/.buildkite/scripts/ci-bake-rocm.sh index 15f8274fc62b..0a9e6e7589fb 100644 --- a/.buildkite/scripts/ci-bake-rocm.sh +++ b/.buildkite/scripts/ci-bake-rocm.sh @@ -418,7 +418,7 @@ validate_ci_build_context_source() { describe_ci_revision() { git -C "$1" describe --tags --long --abbrev=10 \ - --match '*[0-9]*' "$2" 2>/dev/null + --match 'v[0-9]*' "$2" 2>/dev/null } write_ci_git_archival_metadata() { @@ -540,6 +540,26 @@ prepare_ci_build_context() { 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}}" + if [[ " ${stages} " != *" build_rocshmem "* ]]; then + unset ROCSHMEM_BRANCH ROCSHMEM_CACHE_KEY DEEPEP_CACHE_KEY + fi +} + compose_dependency_cache_key() { local prefix="$1" local material="$2" @@ -569,11 +589,11 @@ hash_dockerfile_stages() { } emit = 0 } - $1 == "FROM" { + toupper($1) == "FROM" { stage = "" for (idx = 1; idx <= NF; idx++) { if (tolower($idx) == "as" && idx < NF) { - stage = $(idx + 1) + stage = tolower($(idx + 1)) } } emit = (stage in wanted) @@ -613,11 +633,11 @@ discover_dockerfile_stage_args() { } { line = $0 - if ($1 == "FROM") { + if (toupper($1) == "FROM") { stage = "" for (idx = 1; idx <= NF; idx++) { if (tolower($idx) == "as" && idx < NF) { - stage = $(idx + 1) + stage = tolower($(idx + 1)) } } emit = (stage in wanted) @@ -630,7 +650,7 @@ discover_dockerfile_stage_args() { for (idx = 1; idx <= line_count; idx++) { line = lines[idx] arg_name = line - sub(/^[[:space:]]*ARG[[:space:]]+/, "", arg_name) + 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_]*$/) { @@ -716,7 +736,7 @@ extract_dockerfile_arg_default() { if [[ -n "${ROCM_BUILD_CONTEXT_ROOT:-}" && "${dockerfile}" != /* ]]; then physical_dockerfile="${ROCM_BUILD_CONTEXT_ROOT}/${dockerfile}" fi - sed -n -E "s/^[[:space:]]*ARG[[:space:]]+${arg_name}=\"?([^\"[:space:]]+)\"?.*/\\1/p" \ + sed -n -E "s/^[[:space:]]*[Aa][Rr][Gg][[:space:]]+${arg_name}=\"?([^\"[:space:]]+)\"?.*/\\1/p" \ "${physical_dockerfile}" | head -1 } @@ -1000,7 +1020,21 @@ create_and_bootstrap_builder() { } 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}}}" @@ -1010,10 +1044,12 @@ init_config() { 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}}" - CI_BASE_DOCKERFILE_STAGES="${CI_BASE_DOCKERFILE_STAGES:-${DEFAULT_CI_BASE_DOCKERFILE_STAGES}}" + 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 + 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" @@ -1042,6 +1078,11 @@ print_header() { } 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" @@ -1069,6 +1110,11 @@ load_ci_hcl() { 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() { @@ -1804,7 +1850,7 @@ compute_rocm_csrc_content_hash() { local -a content_args=() bake_dir=$(dirname "${VLLM_BAKE_FILE}") - dockerfile_rocm="${bake_dir}/Dockerfile.rocm" + dockerfile_rocm="${CI_BASE_DOCKERFILE:-${bake_dir}/Dockerfile.rocm}" read -r -a content_paths <<< "${content_files}" mapfile -t content_args < <( get_content_arg_names "${dockerfile_rocm}" "${stages}" "${ROCM_CSRC_CONTENT_ARGS:-}" @@ -1853,7 +1899,7 @@ compute_rocm_rust_content_hash() { local -a content_args=() bake_dir=$(dirname "${VLLM_BAKE_FILE}") - dockerfile_rocm="${bake_dir}/Dockerfile.rocm" + dockerfile_rocm="${CI_BASE_DOCKERFILE:-${bake_dir}/Dockerfile.rocm}" read -r -a content_paths <<< "${content_files}" mapfile -t content_args < <( get_content_arg_names \ @@ -1943,7 +1989,7 @@ write_rocm_build_arg_override() { local arg_value="" bake_dir=$(dirname "${VLLM_BAKE_FILE}") - dockerfile_rocm="${bake_dir}/Dockerfile.rocm" + dockerfile_rocm="${CI_BASE_DOCKERFILE:-${bake_dir}/Dockerfile.rocm}" mapfile -t arg_names < <( { get_content_arg_names \ @@ -2259,7 +2305,7 @@ extract_dependency_pins() { local val="" bake_dir=$(dirname "${VLLM_BAKE_FILE}") - dockerfile_rocm="${bake_dir}/Dockerfile.rocm" + 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}" @@ -2275,12 +2321,12 @@ extract_dependency_pins() { fi val=$( - sed -n -E "s/^[[:space:]]*ARG[[:space:]]+${var}=\"?([^\"[:space:]]+)\"?.*/\\1/p" \ + 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" + echo "Extracted ${var}=${val} from ${dockerfile_rocm}" fi done } @@ -2297,7 +2343,7 @@ compute_dependency_cache_keys() { local deepep_material="" bake_dir=$(dirname "${VLLM_BAKE_FILE}") - dockerfile_rocm="${bake_dir}/Dockerfile.rocm" + 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") rocshmem_branch=$(resolve_dockerfile_arg_value "${dockerfile_rocm}" "ROCSHMEM_BRANCH") @@ -2687,6 +2733,7 @@ main() { 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 @@ -2697,6 +2744,7 @@ main() { # 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 diff --git a/.buildkite/scripts/ci-clean-log.sh b/.buildkite/scripts/ci-clean-log.sh index e2e21483d546..2fe45f3b7745 100644 --- a/.buildkite/scripts/ci-clean-log.sh +++ b/.buildkite/scripts/ci-clean-log.sh @@ -10,11 +10,20 @@ fi INPUT_FILE="$1" +# GNU sed uses "-i [-r]" while BSD/macOS sed needs "-i '' [-E]"; detect once. +if sed --version >/dev/null 2>&1; then + sedi() { sed -i "$@"; } + SED_ERE=-r +else + sedi() { sed -i '' "$@"; } + SED_ERE=-E +fi + # Strip timestamps -sed -i 's/^\[[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}T[0-9]\{2\}:[0-9]\{2\}:[0-9]\{2\}Z\] //' "$INPUT_FILE" +sedi 's/^\[[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}T[0-9]\{2\}:[0-9]\{2\}:[0-9]\{2\}Z\] //' "$INPUT_FILE" # Strip Buildkite inline timestamp markers (ESC _bk;t= BEL) -sed -i 's/\x1B_bk;t=[0-9]*\x07//g' "$INPUT_FILE" +sedi 's/\x1B_bk;t=[0-9]*\x07//g' "$INPUT_FILE" # Strip colorization -sed -i -r 's/\x1B\[[0-9;]*[mK]//g' "$INPUT_FILE" +sedi "$SED_ERE" 's/\x1B\[[0-9;]*[mK]//g' "$INPUT_FILE" diff --git a/.buildkite/scripts/ci-otel/README.md b/.buildkite/scripts/ci-otel/README.md new file mode 100644 index 000000000000..318cec5f2169 --- /dev/null +++ b/.buildkite/scripts/ci-otel/README.md @@ -0,0 +1,51 @@ +# CI timing and GPU samples + +The pipeline generator activates these helpers for trusted, traced CI commands. +`ci_otel.sh` records command spans and `ci_pytest.sh` injects the pytest timing +plugin. `ci_gpu.py` runs once per command, outside pytest workers, when +`nvidia-smi` is available. It queries up to 16 container-visible NVIDIA devices +once per second without importing PyTorch or creating a CUDA context. +Set `CI_INFRA_GPU_SAMPLING=0` in the job environment to disable sampling. + +Samples contain device UUID, index, name, utilization percent, and used/total +device memory in bytes. They are `ci.gpu.sample` OTLP events inside +`ci.gpu.samples` spans, parented to the command span and authenticated using the +existing job-scoped Buildkite OIDC identity. Batches upload every 30 seconds; +the final batch is spooled for the shell's normal job-end upload. No host agent +or long-lived dashboard token is needed. The dashboard companion change adds +GPU controls alongside jobs, commands, and pytest rows in the Builds timeline. + +Device queries and periodic uploads each have a two-second timeout. Three +consecutive query failures stop sampling. Failed periodic uploads are dropped; +memory stays bounded, and the dashboard shows the missing intervals as gaps. +Uploads can delay the next poll. The shell stops and joins the sampler before +flushing traces, allows at most three seconds for shutdown, and preserves the +original command exit status. Samples already uploaded survive an interrupted +job; the last unflushed batch may be lost on forced termination. + +These are device readings during each test's time interval, not process-level +kernel attribution. Other processes and overlapping tests can contribute to +the readings. Subsecond tests may have no sample. Missing readings never become +zero utilization. MIG parent-device memory and utilization are omitted because +they cannot be attributed to the job's partition. When the environment identifies +an assigned MIG UUID, the collector queries that instance's used/total memory +through vLLM's standalone bundled NVML binding in a bounded subprocess. +The binding is loaded directly to avoid vLLM/Torch import side effects. Numeric +`CUDA_VISIBLE_DEVICES` indices are resolved against explicit MIG UUIDs in +`NVIDIA_VISIBLE_DEVICES`. For CDI assignments without an explicit UUID, a bounded +helper initializes the CUDA driver and reads only CUDA-visible device UUIDs, +without creating CUDA contexts or importing Torch. NVML verifies each MIG handle. +Discovery or permission failures remain unknown; NVML sibling instances are never +enumerated. MIG utilization stays absent. +CPU jobs and AMD mirrors do not collect GPU samples. + +Run the helper tests without loading vLLM or requiring a GPU: + +```bash +PATH="$PWD/.venv/bin:$PATH" .venv/bin/python -m pytest \ + .buildkite/scripts/ci-otel/tests/test_ci_otel.py -q +``` + +The suite uses a fake `nvidia-smi` to exercise the shell lifecycle, device-query +failure handling, disable switch, event encoding, and bounded uploads. Real +hardware sampling overhead still needs a canary run before broad deployment. diff --git a/.buildkite/scripts/ci-otel/ci_gpu.py b/.buildkite/scripts/ci-otel/ci_gpu.py new file mode 100644 index 000000000000..0c77f5170b19 --- /dev/null +++ b/.buildkite/scripts/ci-otel/ci_gpu.py @@ -0,0 +1,303 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project + +"""Best-effort, container-visible NVIDIA device samples for command timelines.""" + +from __future__ import annotations + +import csv +import ctypes +import importlib.util +import io +import json +import math +import os +import secrets +import signal +import subprocess +import sys +import threading +import time +import uuid as uuid_module +from pathlib import Path + +from ci_otel import Span, export_spans, record_spans + +INTERVAL_SECONDS = 1.0 +BATCH_SECONDS = 30 +MAX_DEVICES = 16 +QUERY_TIMEOUT_SECONDS = 2 +QUERY = "index,uuid,name,utilization.gpu,memory.used,memory.total,mig.mode.current" + + +def _number(value: str, scale: int = 1, maximum: int | None = None) -> int | None: + try: + number = float(value) + if not math.isfinite(number) or number < 0: + return None + if maximum is not None and number > maximum: + return None + return round(number * scale) + except ValueError: + return None + + +def parse_samples(output: str, timestamp_ns: int) -> list[dict]: + events = [] + for row in csv.reader(io.StringIO(output)): + if len(row) != 7 or len(events) >= MAX_DEVICES: + continue + index, uuid, name, util, used, total, mig = (value.strip() for value in row) + if not index.isdigit() or not uuid.startswith("GPU-"): + continue + attributes: dict[str, str | int | bool] = { + "gpu.uuid": uuid, + "gpu.index": int(index), + "gpu.name": name, + } + # Parent-device memory on a MIG GPU is not the job's allocated memory. + if mig.lower() == "enabled": + attributes["gpu.status"] = "unsupported_mig" + else: + for key, value in ( + ("gpu.utilization", _number(util, maximum=100)), + ("gpu.memory.used", _number(used, 1024**2)), + ("gpu.memory.total", _number(total, 1024**2)), + ): + if value is not None: + attributes[key] = value + events.append( + {"time_ns": timestamp_ns, "name": "ci.gpu.sample", "attributes": attributes} + ) + return events + + +def visible_mig_devices() -> list[str]: + """Resolve explicit MIG assignments without enumerating sibling instances.""" + nvidia = os.environ.get("NVIDIA_VISIBLE_DEVICES", "").split(",") + nvidia = [value.strip() for value in nvidia] + cuda = os.environ.get("CUDA_VISIBLE_DEVICES") + devices = nvidia if cuda is None else [value.strip() for value in cuda.split(",")] + if devices and all(value.isdigit() for value in devices): + if not all(value.startswith("MIG-") for value in nvidia): + return [] + try: + devices = [nvidia[int(value)] for value in devices] + except IndexError: + return [] + if not devices or not all(value.startswith("MIG-") for value in devices): + return [] + return list(dict.fromkeys(devices))[:MAX_DEVICES] + + +def load_nvml(): + """Load the standalone bundled binding without importing vLLM or Torch.""" + # Test images install vLLM as a wheel and keep only tests/helpers in the + # checkout. Finding the top-level package does not execute its __init__. + package = importlib.util.find_spec("vllm") + if package is None or package.origin is None: + raise ImportError("Installed vLLM package unavailable") + path = Path(package.origin).parent / "third_party/pynvml.py" + spec = importlib.util.spec_from_file_location("ci_nvml", path) + if spec is None or spec.loader is None: + raise ImportError("Bundled NVML binding unavailable") + module = importlib.util.module_from_spec(spec) + sys.modules[spec.name] = module + spec.loader.exec_module(module) + return module + + +def cuda_visible_mig_devices() -> list[str]: + """Discover CDI assignments in a subprocess without creating CUDA contexts.""" + pynvml = load_nvml() + + driver = ctypes.CDLL("libcuda.so.1") + count = ctypes.c_int() + if driver.cuInit(0) or driver.cuDeviceGetCount(ctypes.byref(count)): + return [] + devices = [] + pynvml.nvmlInit() + try: + for index in range(min(count.value, MAX_DEVICES)): + device = ctypes.c_int() + identifier = (ctypes.c_ubyte * 16)() + if driver.cuDeviceGet(ctypes.byref(device), index): + continue + # The v2 entry point returns the compute instance UUID under MIG. + if driver.cuDeviceGetUuid_v2(ctypes.byref(identifier), device): + continue + uuid = "MIG-" + str(uuid_module.UUID(bytes=bytes(identifier))) + try: + handle = pynvml.nvmlDeviceGetHandleByUUID(uuid) + if pynvml.nvmlDeviceIsMigDeviceHandle(handle): + devices.append(uuid) + except pynvml.NVMLError: + continue + finally: + pynvml.nvmlShutdown() + return devices + + +def query_mig_samples(devices: list[str], timestamp_ns: int) -> list[dict]: + # Use the binding already shipped with vLLM; never initialize CUDA/Torch. + pynvml = load_nvml() + + events = [] + pynvml.nvmlInit() + try: + for index, uuid in enumerate(devices[:MAX_DEVICES]): + if not uuid.startswith("MIG-"): + continue + attributes: dict[str, str | int | bool] = { + "gpu.uuid": uuid, + "gpu.index": index, + "gpu.name": "NVIDIA MIG", + "gpu.status": "unavailable_mig_memory", + } + try: + handle = pynvml.nvmlDeviceGetHandleByUUID(uuid) + if not pynvml.nvmlDeviceIsMigDeviceHandle(handle): + continue + memory = pynvml.nvmlDeviceGetMemoryInfo(handle) + used = _number(str(memory.used)) + total = _number(str(memory.total)) + if used is not None and total and used <= total: + attributes.update( + { + "gpu.memory.used": used, + "gpu.memory.total": total, + "gpu.status": "mig_memory_only", + } + ) + # NVML does not support per-MIG utilization on these devices. + except pynvml.NVMLError: + pass + events.append( + { + "time_ns": timestamp_ns, + "name": "ci.gpu.sample", + "attributes": attributes, + } + ) + finally: + pynvml.nvmlShutdown() + return events + + +def sample_span(events: list[dict]) -> Span: + return Span( + trace_id=os.environ["CI_INFRA_TRACE_ID"], + span_id=secrets.token_hex(8), + parent_span_id=os.environ["CI_INFRA_COMMAND_SPAN_ID"], + name="ci.gpu.samples", + start_ns=events[0]["time_ns"], + end_ns=events[-1]["time_ns"], + attributes={ + "ci.span.kind": "gpu-samples", + "gpu.sample.interval_ms": int(INTERVAL_SECONDS * 1000), + "gpu.scope": "container-visible-devices", + }, + events=events, + ) + + +def collect(parent_pid: int, stop: threading.Event) -> None: + events: list[dict] = [] + batch_started = time.monotonic() + failures = 0 + discovery_attempted = False + mig_devices = visible_mig_devices() + query = ( + [sys.executable, __file__, "--query-mig", *mig_devices] + if mig_devices + else [ + "nvidia-smi", + f"--query-gpu={QUERY}", + "--format=csv,noheader,nounits", + ] + ) + try: + while not stop.is_set() and os.getppid() == parent_pid: + started = time.monotonic() + try: + result = subprocess.run( + query, + capture_output=True, + text=True, + timeout=QUERY_TIMEOUT_SECONDS, + check=True, + ) + if stop.is_set(): + break + samples = ( + json.loads(result.stdout) + if mig_devices + else parse_samples(result.stdout, time.time_ns()) + ) + if ( + not mig_devices + and not discovery_attempted + and any( + sample["attributes"].get("gpu.status") == "unsupported_mig" + for sample in samples + ) + ): + discovery_attempted = True + # CDI can assign a MIG device without exporting its UUID. + # Only CUDA-visible devices are inspected, never NVML siblings. + try: + discovered = subprocess.run( + [sys.executable, __file__, "--discover-mig"], + capture_output=True, + text=True, + timeout=QUERY_TIMEOUT_SECONDS, + check=True, + ) + mig_devices = json.loads(discovered.stdout) + except (OSError, subprocess.SubprocessError, json.JSONDecodeError): + mig_devices = [] + if mig_devices: + query = [sys.executable, __file__, "--query-mig", *mig_devices] + continue # Do not create an extra parent-device series. + if not samples: + break + events.extend(samples) + failures = 0 + except (OSError, subprocess.SubprocessError, json.JSONDecodeError): + failures += 1 + if failures >= 3: + print( + "CI GPU sampling stopped: device query unavailable", + file=sys.stderr, + ) + break + if events and time.monotonic() - batch_started >= BATCH_SECONDS: + # Bounded memory and upload time even if the receiver is down. + # Dropped batches appear as gaps, never as zero utilization. + export_spans([sample_span(events)], timeout_seconds=2) + events = [] + batch_started = time.monotonic() + stop.wait(max(0, INTERVAL_SECONDS - (time.monotonic() - started))) + finally: + # The shell joins this process before the ordinary job-end spool flush. + if events: + record_spans([sample_span(events)]) + + +def main() -> None: + stop = threading.Event() + signal.signal(signal.SIGTERM, lambda *_: stop.set()) + signal.signal(signal.SIGINT, lambda *_: stop.set()) + try: + collect(int(sys.argv[1]), stop) + except Exception: + print("CI GPU sampling skipped", file=sys.stderr) + + +if __name__ == "__main__": + if len(sys.argv) > 1 and sys.argv[1] == "--discover-mig": + print(json.dumps(cuda_visible_mig_devices())) + elif len(sys.argv) > 1 and sys.argv[1] == "--query-mig": + print(json.dumps(query_mig_samples(sys.argv[2:], time.time_ns()))) + else: + main() diff --git a/.buildkite/scripts/ci-otel/ci_otel.py b/.buildkite/scripts/ci-otel/ci_otel.py index 0d25594bec19..cec3f4130986 100644 --- a/.buildkite/scripts/ci-otel/ci_otel.py +++ b/.buildkite/scripts/ci-otel/ci_otel.py @@ -14,12 +14,13 @@ import time import urllib.request from collections.abc import Iterable -from dataclasses import asdict, dataclass +from dataclasses import asdict, dataclass, field from pathlib import Path ENDPOINT = os.getenv("CI_INFRA_OTEL_ENDPOINT", "https://ci.vllm.ai/api/otel/v1/traces") AUDIENCE = os.getenv("CI_INFRA_OTEL_AUDIENCE", "https://ci.vllm.ai/api/otel") MAX_BATCH_SIZE = 2_000 +MAX_BATCH_BYTES = 1024 * 1024 TEST_SPOOL_BATCH_SIZE = 100 @@ -56,6 +57,7 @@ class Span: end_ns: int attributes: dict[str, str | int | bool] status_code: int = 1 + events: list[dict] = field(default_factory=list) def _varint(value: int) -> bytes: @@ -137,6 +139,12 @@ def _encode_span(span: Span) -> bytes: ) for name, value in attributes.items(): encoded += _bytes_field(9, _attribute(name, value)) + for event in span.events: + encoded_event = _fixed64_field(1, int(event["time_ns"])) + encoded_event += _string_field(2, event["name"]) + for name, value in event["attributes"].items(): + encoded_event += _bytes_field(3, _attribute(name, value)) + encoded += _bytes_field(11, encoded_event) return encoded + _bytes_field(15, _varint_field(3, span.status_code)) @@ -482,10 +490,24 @@ def export_spans(spans: list[Span], timeout_seconds: float = 3.0) -> bool: try: deadline = time.monotonic() + max(timeout_seconds, 0.1) token = _oidc_token(deadline) - for offset in range(0, len(spans), MAX_BATCH_SIZE): + batches: list[list[Span]] = [] + batch: list[Span] = [] + size = 0 + for span in spans: + span_size = len(_encode_span(span)) + if batch and ( + size + span_size > MAX_BATCH_BYTES or len(batch) >= MAX_BATCH_SIZE + ): + batches.append(batch) + batch, size = [], 0 + batch.append(span) + size += span_size + if batch: + batches.append(batch) + for batch in batches: request = urllib.request.Request( ENDPOINT, - data=encode_request(spans[offset : offset + MAX_BATCH_SIZE]), + data=encode_request(batch), method="POST", headers={ "Authorization": f"Bearer {token}", diff --git a/.buildkite/scripts/ci-otel/ci_otel.sh b/.buildkite/scripts/ci-otel/ci_otel.sh index feec16fa6528..7f1b700fe6bd 100755 --- a/.buildkite/scripts/ci-otel/ci_otel.sh +++ b/.buildkite/scripts/ci-otel/ci_otel.sh @@ -26,7 +26,8 @@ if [ -z "${CI_INFRA_OTEL_RUNTIME_DIR}" ] || [ -z "${_CI_INFRA_OTEL_PYTHON}" ] || [ ! -f "${CI_INFRA_OTEL_DIR}/ci_otel.py" ] || ! mkdir -p "${CI_INFRA_OTEL_SPOOL_DIR}" || - ! PYTHONPATH="${CI_INFRA_OTEL_DIR}${PYTHONPATH:+:${PYTHONPATH}}" \ + ! PYTHONDONTWRITEBYTECODE=1 \ + PYTHONPATH="${CI_INFRA_OTEL_DIR}${PYTHONPATH:+:${PYTHONPATH}}" \ "${_CI_INFRA_OTEL_PYTHON}" -c "import ci_otel" >/dev/null 2>&1; then echo "vLLM CI OTel: tracing unavailable; test command will run normally" >&2 || : if [ "${_CI_INFRA_OTEL_OWNS_RUNTIME}" = "1" ]; then @@ -58,12 +59,45 @@ fi _ci_otel_python() { if command -v timeout >/dev/null 2>&1; then - timeout 2s "${_CI_INFRA_OTEL_PYTHON}" "$@" + PYTHONDONTWRITEBYTECODE=1 timeout 2s "${_CI_INFRA_OTEL_PYTHON}" "$@" else - "${_CI_INFRA_OTEL_PYTHON}" "$@" + PYTHONDONTWRITEBYTECODE=1 "${_CI_INFRA_OTEL_PYTHON}" "$@" fi } +# The Docker test commands run as root, while /workdir is owned by the +# Buildkite checkout user. Run the GPU sampler as that unprivileged identity so +# any future project-code import cannot leave root-owned files in the checkout. +_ci_otel_prepare_gpu_user() { + _CI_INFRA_GPU_UID="" + _CI_INFRA_GPU_GID="" + [ "$(id -u 2>/dev/null || :)" = "0" ] || return 0 + command -v setpriv >/dev/null 2>&1 || return 1 + + _CI_INFRA_OTEL_WORKSPACE_DIR="${CI_INFRA_OTEL_WORKSPACE_DIR:-/workdir}" + [ -d "${_CI_INFRA_OTEL_WORKSPACE_DIR}" ] || return 1 + # The owner fields are numeric and whitespace-free on GNU stat. + # shellcheck disable=SC2046 + set -- $(stat -c '%u %g' -- "${_CI_INFRA_OTEL_WORKSPACE_DIR}" 2>/dev/null || :) + [ "$#" -eq 2 ] || return 1 + case "$1:$2" in + *[!0-9:]* | 0:* | *:0) return 1 ;; + esac + + _CI_INFRA_GPU_UID="$1" + _CI_INFRA_GPU_GID="$2" + chown "${_CI_INFRA_GPU_UID}:${_CI_INFRA_GPU_GID}" \ + "${CI_INFRA_OTEL_SPOOL_DIR}" || return 1 + chmod 0700 "${CI_INFRA_OTEL_SPOOL_DIR}" || return 1 + if [ "${_CI_INFRA_OTEL_OWNS_RUNTIME}" = "1" ]; then + # Permit traversal to the private child spool, but not directory listing. + chmod 0711 "${CI_INFRA_OTEL_RUNTIME_DIR}" || return 1 + fi + setpriv --reuid="${_CI_INFRA_GPU_UID}" \ + --regid="${_CI_INFRA_GPU_GID}" --clear-groups --no-new-privs \ + -- test -w "${CI_INFRA_OTEL_SPOOL_DIR}" +} + ci_otel_start() { _CI_INFRA_OTEL_COMMAND_INDEX="$1" _CI_INFRA_OTEL_COMMAND_LABEL="${2:-command ${_CI_INFRA_OTEL_COMMAND_INDEX}}" @@ -82,6 +116,28 @@ ci_otel_start() { CI_INFRA_COMMAND_SPAN_ID="${_CI_INFRA_OTEL_COMMAND_SPAN_ID}" export CI_INFRA_TRACE_ID CI_INFRA_COMMAND_SPAN_ID _CI_INFRA_OTEL_ACTIVE=1 + _CI_INFRA_GPU_PID="" + if [ "${CI_INFRA_GPU_SAMPLING:-1}" != "0" ] && + [ -f "${CI_INFRA_OTEL_DIR}/ci_gpu.py" ] && + command -v nvidia-smi >/dev/null 2>&1; then + if _ci_otel_prepare_gpu_user; then + if [ -n "${_CI_INFRA_GPU_UID}" ]; then + PYTHONDONTWRITEBYTECODE=1 setpriv \ + --reuid="${_CI_INFRA_GPU_UID}" --regid="${_CI_INFRA_GPU_GID}" \ + --clear-groups --no-new-privs -- \ + "${_CI_INFRA_OTEL_PYTHON}" "${CI_INFRA_OTEL_DIR}/ci_gpu.py" "$$" \ + &2 || : + fi + fi } ci_otel_finish() { @@ -93,6 +149,18 @@ ci_otel_finish() { "${_CI_INFRA_OTEL_PARENT_SPAN_ID}" "${_CI_INFRA_OTEL_START_NS}" \ "${_CI_INFRA_OTEL_COMMAND_INDEX}" "${_CI_INFRA_OTEL_COMMAND_STATUS}" \ "${_CI_INFRA_OTEL_COMMAND_LABEL}" || : + if [ -n "${_CI_INFRA_GPU_PID:-}" ]; then + kill -TERM "${_CI_INFRA_GPU_PID}" 2>/dev/null || : + _CI_INFRA_GPU_WAIT=0 + while kill -0 "${_CI_INFRA_GPU_PID}" 2>/dev/null && + [ "${_CI_INFRA_GPU_WAIT}" -lt 30 ]; do + sleep 0.1 + _CI_INFRA_GPU_WAIT=$((_CI_INFRA_GPU_WAIT + 1)) + done + kill -KILL "${_CI_INFRA_GPU_PID}" 2>/dev/null || : + wait "${_CI_INFRA_GPU_PID}" 2>/dev/null || : + _CI_INFRA_GPU_PID="" + fi CI_INFRA_TRACE_ID="" CI_INFRA_COMMAND_SPAN_ID="" export CI_INFRA_TRACE_ID CI_INFRA_COMMAND_SPAN_ID @@ -118,9 +186,11 @@ _ci_otel_on_exit() { trap - 0 ci_otel_finish "${_CI_INFRA_OTEL_EXIT_STATUS}" || : if command -v timeout >/dev/null 2>&1; then - timeout 4s "${_CI_INFRA_OTEL_PYTHON}" "${CI_INFRA_OTEL_DIR}/ci_otel.py" flush || : + PYTHONDONTWRITEBYTECODE=1 timeout 4s \ + "${_CI_INFRA_OTEL_PYTHON}" "${CI_INFRA_OTEL_DIR}/ci_otel.py" flush || : else - "${_CI_INFRA_OTEL_PYTHON}" "${CI_INFRA_OTEL_DIR}/ci_otel.py" flush || : + PYTHONDONTWRITEBYTECODE=1 \ + "${_CI_INFRA_OTEL_PYTHON}" "${CI_INFRA_OTEL_DIR}/ci_otel.py" flush || : fi if [ "${_CI_INFRA_OTEL_OWNS_RUNTIME}" = "1" ]; then rm -rf -- "${CI_INFRA_OTEL_RUNTIME_DIR}" || : diff --git a/.buildkite/scripts/ci-otel/tests/test_ci_otel.py b/.buildkite/scripts/ci-otel/tests/test_ci_otel.py index ff279de953a5..c66530989f97 100644 --- a/.buildkite/scripts/ci-otel/tests/test_ci_otel.py +++ b/.buildkite/scripts/ci-otel/tests/test_ci_otel.py @@ -6,16 +6,20 @@ import json import os import shlex +import shutil import subprocess import sys +import threading import time from pathlib import Path +from types import SimpleNamespace import pytest SCRIPTS_DIR = Path(__file__).resolve().parents[1] sys.path.insert(0, str(SCRIPTS_DIR)) +import ci_gpu # noqa: E402 import ci_otel # noqa: E402 from ci_otel import Span # noqa: E402 @@ -251,9 +255,15 @@ def test_export_failure_is_soft_and_bounded(monkeypatch): assert time.monotonic() - started < 0.5 -def test_export_batches_with_one_oidc_token(monkeypatch): +@pytest.mark.parametrize("byte_limited", [False, True]) +def test_export_batches_with_one_oidc_token(monkeypatch, byte_limited): monkeypatch.setenv("BUILDKITE", "true") - monkeypatch.setattr(ci_otel, "MAX_BATCH_SIZE", 2) + if byte_limited: + monkeypatch.setattr( + ci_otel, "MAX_BATCH_BYTES", len(ci_otel._encode_span(_span())) * 2 + ) + else: + monkeypatch.setattr(ci_otel, "MAX_BATCH_SIZE", 2) token_calls = [] requests = [] @@ -387,6 +397,35 @@ def test_shell_wrapper_preserves_failure_status(tmp_path): assert result.returncode == 1 +def test_shell_wrapper_does_not_write_bytecode_to_helper_tree(tmp_path): + helper_dir = tmp_path / "ci-otel" + shutil.copytree( + SCRIPTS_DIR, + helper_dir, + ignore=shutil.ignore_patterns("__pycache__"), + ) + shell = ( + f'. "{helper_dir / "ci_otel.sh"}"; ' + f"ci_otel_start 1 {_quoted('true')}; ci_otel_finish 0" + ) + + result = subprocess.run( + ["/bin/sh", "-c", shell], + check=False, + capture_output=True, + text=True, + env={ + **os.environ, + "CI_INFRA_OTEL_DIR": str(helper_dir), + "CI_INFRA_GPU_SAMPLING": "0", + "CI_INFRA_OTEL_SPOOL_DIR": str(tmp_path / "spans"), + }, + ) + + assert result.returncode == 0, result.stderr + assert list(helper_dir.rglob("*.pyc")) == [] + + def test_ci_otel_run_records_command_and_preserves_status(tmp_path): shell = ( f'. "{SCRIPTS_DIR / "ci_otel.sh"}"; ' @@ -786,3 +825,398 @@ def test_pytest_plugin_failure_cannot_fail_pytest(tmp_path): assert result.returncode == 0, result.stderr assert "1 passed" in result.stdout + + +def test_gpu_samples_preserve_zero_and_omit_unsupported_metrics(): + events = ci_gpu.parse_samples( + "0, GPU-a, H200, 0, 1024, 2048, Disabled\n" + "1, GPU-b, H200, [N/A], [N/A], 2048, Disabled\n" + "2, GPU-c, H200, 50, 1024, 2048, Enabled\n" + "3, GPU-d, H200, NaN, -1, inf, Disabled\n", + 1234, + ) + assert len(events) == 4 + assert events[0]["attributes"]["gpu.utilization"] == 0 + assert events[0]["attributes"]["gpu.memory.used"] == 1024**3 + for event in events[1:]: + assert "gpu.utilization" not in event["attributes"] + assert "gpu.memory.used" not in event["attributes"] + assert events[2]["attributes"]["gpu.status"] == "unsupported_mig" + assert "gpu.memory.total" not in events[2]["attributes"] + + +@pytest.mark.parametrize( + "cuda,nvidia,expected", + [ + ("MIG-a", "GPU-parent", ["MIG-a"]), + ("0", "MIG-a", ["MIG-a"]), + ("1,0", "MIG-a,MIG-b", ["MIG-b", "MIG-a"]), + (None, "MIG-a", ["MIG-a"]), + ("", "MIG-a", []), + ("-1", "MIG-a", []), + ("2", "MIG-a", []), + ("GPU-parent", "MIG-a", []), + ("0", "all", []), + ], +) +def test_mig_assignment_obeys_cuda_visibility(monkeypatch, cuda, nvidia, expected): + monkeypatch.setenv("NVIDIA_VISIBLE_DEVICES", nvidia) + monkeypatch.delenv("CUDA_VISIBLE_DEVICES", raising=False) + if cuda is not None: + monkeypatch.setenv("CUDA_VISIBLE_DEVICES", cuda) + assert ci_gpu.visible_mig_devices() == expected + + +@pytest.mark.parametrize("used", [0, 3 * 1024**3]) +def test_mig_memory_uses_assigned_handle_without_parent_metrics(monkeypatch, used): + handles = [] + closed = [] + + def get_handle(uuid): + handles.append(uuid) + return "slice-handle" + + def get_memory(handle): + assert handle == "slice-handle" + return SimpleNamespace(used=used, total=16 * 1024**3) + + nvml = SimpleNamespace( + nvmlInit=lambda: None, + nvmlShutdown=lambda: closed.append(True), + nvmlDeviceGetHandleByUUID=get_handle, + nvmlDeviceIsMigDeviceHandle=lambda handle: handle == "slice-handle", + nvmlDeviceGetMemoryInfo=get_memory, + NVMLError=RuntimeError, + ) + monkeypatch.setattr(ci_gpu, "load_nvml", lambda: nvml) + samples = ci_gpu.query_mig_samples(["MIG-a"], 1234) + assert handles == ["MIG-a"] + assert closed == [True] + assert samples[0]["time_ns"] == 1234 + attributes = samples[0]["attributes"] + assert attributes["gpu.uuid"] == "MIG-a" + assert attributes["gpu.memory.used"] == used + assert attributes["gpu.memory.total"] == 16 * 1024**3 + assert "gpu.utilization" not in attributes + + +@pytest.mark.parametrize("is_mig", [True, False]) +def test_mig_memory_never_substitutes_parent_or_permission_failure(monkeypatch, is_mig): + def unavailable(handle): + if not is_mig: + pytest.fail("queried parent memory") + raise RuntimeError("Insufficient Permissions") + + nvml = SimpleNamespace( + nvmlInit=lambda: None, + nvmlShutdown=lambda: None, + nvmlDeviceGetHandleByUUID=lambda uuid: "handle", + nvmlDeviceIsMigDeviceHandle=lambda handle: is_mig, + nvmlDeviceGetMemoryInfo=unavailable, + NVMLError=RuntimeError, + ) + monkeypatch.setattr(ci_gpu, "load_nvml", lambda: nvml) + samples = ci_gpu.query_mig_samples(["MIG-a"], 1234) + if is_mig: + assert samples[0]["attributes"]["gpu.status"] == "unavailable_mig_memory" + assert "gpu.memory.used" not in samples[0]["attributes"] + else: + assert samples == [] + + +def test_gpu_batches_keep_command_identity_and_survive_spool_roundtrip( + monkeypatch, tmp_path +): + monkeypatch.setenv("CI_INFRA_TRACE_ID", "01" * 16) + monkeypatch.setenv("CI_INFRA_COMMAND_SPAN_ID", "02" * 8) + monkeypatch.setenv("CI_INFRA_OTEL_SPOOL_DIR", str(tmp_path)) + span = ci_gpu.sample_span( + ci_gpu.parse_samples("0, GPU-a, H200, 90, 1, 2, Disabled", 1234) + ) + assert span.parent_span_id == "02" * 8 + assert span.trace_id == "01" * 16 + assert ci_otel.record_spans([span]) + assert ci_otel.load_spans() == [span] + assert b"ci.gpu.sample" in ci_otel.encode_request([span]) + + +@pytest.mark.parametrize("mig", [False, True]) +def test_gpu_query_failures_are_bounded_and_never_synthesize_idle_samples( + monkeypatch, mig +): + monkeypatch.setenv("CUDA_VISIBLE_DEVICES", "MIG-a" if mig else "0") + calls = [] + + def query(*args, **kwargs): + calls.append(kwargs["timeout"]) + raise subprocess.TimeoutExpired("nvidia-smi", kwargs["timeout"]) + + monkeypatch.setattr(ci_gpu.subprocess, "run", query) + monkeypatch.setattr(ci_gpu, "INTERVAL_SECONDS", 0) + monkeypatch.setattr( + ci_gpu, "record_spans", lambda _: pytest.fail("fabricated sample") + ) + ci_gpu.collect(os.getppid(), threading.Event()) + assert calls == [2, 2, 2] + + +def test_gpu_periodic_batches_are_bounded_even_when_upload_fails(monkeypatch): + monkeypatch.setenv("CI_INFRA_TRACE_ID", "01" * 16) + monkeypatch.setenv("CI_INFRA_COMMAND_SPAN_ID", "02" * 8) + monkeypatch.setattr(ci_gpu, "BATCH_SECONDS", 0) + monkeypatch.setattr(ci_gpu, "INTERVAL_SECONDS", 0) + monkeypatch.setattr( + ci_gpu.subprocess, + "run", + lambda *a, **k: subprocess.CompletedProcess( + a, 0, "0, GPU-a, H200, 50, 1, 2, Disabled" + ), + ) + stop = threading.Event() + batches = [] + + def upload(spans, timeout_seconds): + batches.append(spans) + assert timeout_seconds == 2 + if len(batches) == 3: + stop.set() + return False + + monkeypatch.setattr(ci_gpu, "export_spans", upload) + ci_gpu.collect(os.getppid(), stop) + assert [len(batch[0].events) for batch in batches] == [1, 1, 1] + + +def _write_executable(path: Path, body: str) -> None: + path.write_text(f"#!/bin/sh\n{body}") + path.chmod(0o755) + + +@pytest.mark.parametrize( + "workspace_owner,expect_sampler", + [("997 996", True), ("0 0", False)], +) +def test_root_shell_runs_gpu_sampler_as_checkout_owner( + tmp_path, workspace_owner, expect_sampler +): + bin_dir = tmp_path / "bin" + workspace = tmp_path / "workdir" + runtime = tmp_path / "runtime" + log = tmp_path / "calls.log" + bin_dir.mkdir() + workspace.mkdir() + + _write_executable( + bin_dir / "id", + '[ "$1" = "-u" ] && { echo 0; exit 0; }; exec /usr/bin/id "$@"\n', + ) + _write_executable( + bin_dir / "stat", + f"printf '%s\\n' '{workspace_owner}'\n", + ) + for command in ("chown", "chmod"): + _write_executable(bin_dir / command, "exit 0\n") + _write_executable( + bin_dir / "setpriv", + 'printf "setpriv %s\\n" "$*" >> "$CI_OTEL_TEST_LOG"\n' + 'while [ "$#" -gt 0 ] && [ "$1" != "--" ]; do shift; done\n' + '[ "$#" -gt 0 ] && shift\n' + 'exec "$@"\n', + ) + _write_executable( + bin_dir / "python3", + '[ "$1" = "-c" ] && exit 0\n' + '[ "$2" = "new-context" ] && {\n' + ' echo "01010101010101010101010101010101 0202020202020202 - 1"\n' + " exit 0\n" + "}\n" + 'case "$1" in *ci_gpu.py) echo gpu >> "$CI_OTEL_TEST_LOG";; esac\n' + "exit 0\n", + ) + _write_executable(bin_dir / "nvidia-smi", "exit 0\n") + shell = ( + f'. "{SCRIPTS_DIR / "ci_otel.sh"}"; ' + f"ci_otel_start 1 {_quoted('true')}; ci_otel_finish 0" + ) + + result = subprocess.run( + ["/bin/sh", "-c", shell], + check=False, + capture_output=True, + text=True, + env={ + **os.environ, + "PATH": f"{bin_dir}:{os.environ['PATH']}", + "CI_INFRA_OTEL_DIR": str(SCRIPTS_DIR), + "CI_INFRA_OTEL_RUNTIME_DIR": str(runtime), + "CI_INFRA_OTEL_WORKSPACE_DIR": str(workspace), + "CI_OTEL_TEST_LOG": str(log), + }, + ) + + assert result.returncode == 0, result.stderr + calls = log.read_text() if log.exists() else "" + if expect_sampler: + expected = "--reuid=997 --regid=996 --clear-groups --no-new-privs --" + assert calls.count("setpriv") == 2 + assert calls.count(expected) == 2 + assert "gpu" in calls + else: + assert calls == "" + assert "non-root GPU sampler unavailable" in result.stderr + + +@pytest.mark.parametrize("sampling", ["1", "0"]) +def test_gpu_sampler_stops_with_command_and_preserves_failure_status( + tmp_path, sampling +): + bin_dir = tmp_path / "bin" + bin_dir.mkdir() + smi = bin_dir / "nvidia-smi" + smi.write_text('#!/bin/sh\necho "0, GPU-a, H200, 70, 1024, 2048, Disabled"\n') + smi.chmod(0o755) + # Keep the spool for inspection, and avoid any network upload. + shell = ( + f'. "{SCRIPTS_DIR / "ci_otel.sh"}"\n' + "ci_otel_run 1 workload sh -c 'sleep 1.3; exit 7'\n" + "exit $?" + ) + result = subprocess.run( + ["/bin/sh", "-c", shell], + capture_output=True, + text=True, + timeout=8, + env={ + **os.environ, + "PATH": f"{bin_dir}:{os.environ['PATH']}", + "BUILDKITE": "false", + "CI_INFRA_GPU_SAMPLING": sampling, + "CI_INFRA_OTEL_DIR": str(SCRIPTS_DIR), + "CI_INFRA_OTEL_RUNTIME_DIR": str(tmp_path / "runtime"), + "CI_INFRA_OTEL_SPOOL_DIR": str(tmp_path / "spans"), + }, + ) + assert result.returncode == 7, result.stderr + records = [ + json.loads(line) + for path in (tmp_path / "spans").glob("spans-*.jsonl") + for line in path.read_text().splitlines() + ] + command = next(record for record in records if record["name"] == "ci.command") + batches = [record for record in records if record["name"] == "ci.gpu.samples"] + assert bool(batches) == (sampling == "1") + if batches: + assert batches[0]["parent_span_id"] == command["span_id"] + assert all( + command["start_ns"] <= event["time_ns"] <= command["end_ns"] + for event in batches[0]["events"] + ) + + +@pytest.mark.parametrize("is_mig", [True, False]) +def test_cdi_discovery_uses_only_cuda_visible_uuids(monkeypatch, is_mig): + identifiers = [] + + def get_count(output): + output._obj.value = 1 + return 0 + + def get_device(output, index): + assert index == 0 + output._obj.value = 7 + return 0 + + def get_uuid(output, device): + assert device.value == 7 + output._obj[:] = bytes(range(16)) + return 0 + + driver = SimpleNamespace( + cuInit=lambda flags: 0, + cuDeviceGetCount=get_count, + cuDeviceGet=get_device, + cuDeviceGetUuid_v2=get_uuid, + ) + monkeypatch.setattr(ci_gpu.ctypes, "CDLL", lambda _: driver) + nvml = SimpleNamespace( + nvmlInit=lambda: None, + nvmlShutdown=lambda: None, + nvmlDeviceGetHandleByUUID=lambda value: identifiers.append(value) or "handle", + nvmlDeviceIsMigDeviceHandle=lambda handle: is_mig, + NVMLError=RuntimeError, + ) + monkeypatch.setattr(ci_gpu, "load_nvml", lambda: nvml) + expected = "MIG-00010203-0405-0607-0809-0a0b0c0d0e0f" + assert ci_gpu.cuda_visible_mig_devices() == ([expected] if is_mig else []) + assert identifiers == [expected] + + +@pytest.mark.parametrize("discovery_fails", [False, True]) +def test_cdi_sampling_switches_to_slice_or_keeps_unknown(monkeypatch, discovery_fails): + monkeypatch.setenv("CI_INFRA_TRACE_ID", "01" * 16) + monkeypatch.setenv("CI_INFRA_COMMAND_SPAN_ID", "02" * 8) + monkeypatch.delenv("CUDA_VISIBLE_DEVICES", raising=False) + monkeypatch.setenv("NVIDIA_VISIBLE_DEVICES", "void") + monkeypatch.setattr(ci_gpu, "INTERVAL_SECONDS", 0) + stop = threading.Event() + commands = [] + records = [] + + def query(command, **kwargs): + assert kwargs["timeout"] == ci_gpu.QUERY_TIMEOUT_SECONDS + commands.append(command) + if "--discover-mig" in command: + if discovery_fails: + raise subprocess.TimeoutExpired(command, 2) + return SimpleNamespace(stdout='["MIG-assigned"]') + if len(commands) == 4: + stop.set() + if "--query-mig" in command: + assert command[-1] == "MIG-assigned" + return SimpleNamespace( + stdout=json.dumps( + [ + { + "time_ns": 2, + "name": "ci.gpu.sample", + "attributes": { + "gpu.uuid": "MIG-assigned", + "gpu.memory.used": 123, + }, + } + ] + ) + ) + return SimpleNamespace(stdout="0, GPU-parent, H200, 99, 100, 200, Enabled") + + monkeypatch.setattr(ci_gpu.subprocess, "run", query) + monkeypatch.setattr(ci_gpu, "record_spans", lambda spans: records.extend(spans)) + ci_gpu.collect(os.getppid(), stop) + assert sum("--discover-mig" in command for command in commands) == 1 + attributes = [event["attributes"] for span in records for event in span.events] + if discovery_fails: + assert all("gpu.memory.used" not in event for event in attributes) + else: + assert any(event.get("gpu.memory.used") == 123 for event in attributes) + assert all(event["gpu.uuid"] == "MIG-assigned" for event in attributes) + + +def test_bundled_nvml_load_does_not_import_vllm_or_torch(tmp_path): + # Match test images: the collector is outside the installed vLLM package. + helper = tmp_path / "ci_gpu_relocated.py" + helper.write_text((SCRIPTS_DIR / "ci_gpu.py").read_text()) + result = subprocess.run( + [ + sys.executable, + "-c", + "import sys; import ci_gpu_relocated as ci_gpu; " + "binding = ci_gpu.load_nvml(); " + "assert callable(binding.nvmlDeviceGetMemoryInfo); " + "assert 'vllm' not in sys.modules; assert 'torch' not in sys.modules", + ], + env={**os.environ, "PYTHONPATH": f"{tmp_path}{os.pathsep}{SCRIPTS_DIR}"}, + capture_output=True, + text=True, + timeout=5, + ) + assert result.returncode == 0, result.stderr diff --git a/.buildkite/scripts/cleanup-nightly-builds.sh b/.buildkite/scripts/cleanup-nightly-builds.sh index b324778b1f79..41b04996e455 100755 --- a/.buildkite/scripts/cleanup-nightly-builds.sh +++ b/.buildkite/scripts/cleanup-nightly-builds.sh @@ -56,8 +56,8 @@ get_all_tags() { set -x # Get both last_updated timestamp and tag name, separated by | - # Exclude nightly-dev tags from cleanup - local tags=$(echo "$response" | jq -r --arg prefix "$TAG_PREFIX" '.results[] | select(.name | startswith($prefix)) | select(.name | startswith("nightly-dev") | not) | "\(.last_updated)|\(.name)"') + # Exclude mutable architecture tags and nightly-dev tags from cleanup + local tags=$(echo "$response" | jq -r --arg prefix "$TAG_PREFIX" '.results[] | select(.name | startswith($prefix)) | select(.name != ($prefix + "x86_64") and .name != ($prefix + "aarch64")) | select(.name | startswith("nightly-dev") | not) | "\(.last_updated)|\(.name)"') if [ -z "$tags" ]; then break diff --git a/.buildkite/scripts/crcr-report.sh b/.buildkite/scripts/crcr-report.sh index e19146fddbed..3b12d451cdb8 100644 --- a/.buildkite/scripts/crcr-report.sh +++ b/.buildkite/scripts/crcr-report.sh @@ -40,8 +40,7 @@ fi TOKEN_SECRET_KEY="${CRCR_BUILDKITE_TOKEN_SECRET_KEY:-CRCR_BUILDKITE_API_TOKEN}" BK_TOKEN="${BUILDKITE_API_TOKEN:-}" if [[ -z "${BK_TOKEN}" ]]; then - # Not in the job environment, so read it from a Buildkite secret. The agent - # redacts values fetched this way from the log. + # Not in the job environment, so read it from a Buildkite secret. # # Report why a lookup failed. Swallowing stderr made a missing secret, a # denied policy and an unusable agent indistinguishable, all surfacing as the @@ -50,14 +49,9 @@ if [[ -z "${BK_TOKEN}" ]]; then echo "buildkite-agent is not on PATH; cannot read secret '${TOKEN_SECRET_KEY}'" else secret_err="$(mktemp)" - # Deliberately not passing --skip-redaction. On the deployed agent - # (v3.73.1) the flag cannot help: secret_get.go creates the Job API - # client unconditionally and only checks SkipRedaction afterwards, so - # under the docker plugin -- which does not expose the Job API socket to - # the container -- it fails before the flag is read. That ordering was - # only fixed in v3.107.0. Skipping redaction would also stop the token - # being registered with the log redactor, for no gain here. - if BK_TOKEN="$(buildkite-agent secret get "${TOKEN_SECRET_KEY}" 2>"${secret_err}")"; then + # Requires agent >= 3.107.0: the Docker plugin does not mount the Job API + # socket needed for redaction. Capture the value without logging it. + if BK_TOKEN="$(buildkite-agent secret get --skip-redaction "${TOKEN_SECRET_KEY}" 2>"${secret_err}")"; then if [[ -z "${BK_TOKEN}" ]]; then echo "secret '${TOKEN_SECRET_KEY}' resolved but is empty" fi @@ -77,7 +71,8 @@ if [[ -z "${BK_TOKEN}" ]]; then fi AUDIENCE="pytorch-cross-repo-ci-relay" -OIDC_TOKEN="$(buildkite-agent oidc request-token --audience "${AUDIENCE}" 2>/dev/null)" +# OIDC redaction also requires the unavailable Job API socket. +OIDC_TOKEN="$(buildkite-agent oidc request-token --skip-redaction --audience "${AUDIENCE}" 2>/dev/null)" if [[ -z "${OIDC_TOKEN}" ]]; then echo "could not mint a Buildkite OIDC token -- skipping report" exit 0 diff --git a/.buildkite/scripts/generate-nightly-index.py b/.buildkite/scripts/generate-nightly-index.py index 67adf9c92b80..8f92996ff9ab 100644 --- a/.buildkite/scripts/generate-nightly-index.py +++ b/.buildkite/scripts/generate-nightly-index.py @@ -18,8 +18,7 @@ def normalize_package_name(name: str) -> str: - """ - Normalize package name according to PEP 503. + """Normalize package name according to PEP 503. https://peps.python.org/pep-0503/#normalized-names Replace runs of underscores, hyphens, and periods with a single hyphen, @@ -55,17 +54,18 @@ class WheelFileInfo: def parse_from_filename(file: str) -> WheelFileInfo: - """ - Parse wheel file name to extract metadata. + """Parse wheel file name to extract metadata. The format of wheel names: {package_name}-{version}(-{build_tag})?-{python_tag}-{abi_tag}-{platform_tag}.whl All versions could contain a variant like '+cu129' or '.cpu' or `.rocm` (or not). + Example: vllm-0.11.0-cp38-abi3-manylinux1_x86_64.whl vllm-0.10.2rc2+cu129-cp38-abi3-manylinux2014_aarch64.whl vllm-0.11.1rc8.dev14+gaa384b3c0-cp38-abi3-manylinux2014_aarch64.whl vllm-0.11.1rc8.dev14+gaa384b3c0.cu130-cp38-abi3-manylinux1_x86_64.whl + """ wheel_file_re = re.compile( r"^(?P.+)-(?P[^-]+?)(-(?P[^-]+))?-(?P[^-]+)-(?P[^-]+)-(?P[^-]+)\.whl$" @@ -111,9 +111,7 @@ def parse_from_filename(file: str) -> WheelFileInfo: def generate_project_list(subdir_names: list[str], comment: str = "") -> str: - """ - Generate project list HTML content linking to each project & variant subdirectory. - """ + """Generate project list HTML content linking to each project & variant subdirectory.""" href_tags = [] for name in sorted(subdir_names): name = name.strip("/").strip(".") @@ -127,9 +125,7 @@ def generate_package_index_and_metadata( index_base_dir: Path, comment: str = "", ) -> tuple[str, str]: - """ - Generate package index HTML content for a specific package, linking to actual wheel files. - """ + """Generate package index HTML content for a specific package, linking to actual wheel files.""" href_tags = [] metadata = [] for file in sorted(wheel_files, key=lambda x: x.filename): @@ -156,8 +152,7 @@ def generate_index_and_metadata( alias_to_default: str | None = None, comment: str = "", ): - """ - Generate index for all wheel files. + """Generate index for all wheel files. Args: whl_files (list[str]): List of wheel files (must be directly under `wheel_base_dir`). @@ -216,8 +211,8 @@ def generate_index_and_metadata( }, ... ] - """ + """ parsed_files = [parse_from_filename(f) for f in whl_files] if not parsed_files: diff --git a/.buildkite/scripts/hardware_ci/run-amd-test.sh b/.buildkite/scripts/hardware_ci/run-amd-test.sh index 62fda27bf0be..2705c3b87e7b 100755 --- a/.buildkite/scripts/hardware_ci/run-amd-test.sh +++ b/.buildkite/scripts/hardware_ci/run-amd-test.sh @@ -34,6 +34,9 @@ ############################################################################### 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}" @@ -106,6 +109,10 @@ clear_ci_orchestration_env() { 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 \ @@ -225,6 +232,9 @@ prepare_artifact_image() { 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" @@ -1602,6 +1612,13 @@ 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 @@ -1759,6 +1776,7 @@ else -e "VLLM_CACHE_ROOT=${CONTAINER_CACHE_ROOT}/vllm" \ -e "XDG_CACHE_HOME=${CONTAINER_CACHE_ROOT}/xdg" \ -e "PYTORCH_ROCM_ARCH=" \ + "${cpu_platform_env[@]}" \ "${standalone_merge_base_env[@]}" \ --name "${container_name}" \ "${image_name}" \ diff --git a/.buildkite/scripts/hardware_ci/run-cpu-test-arm.sh b/.buildkite/scripts/hardware_ci/run-cpu-test-arm.sh index 052063f2f69e..689ca92d6a05 100755 --- a/.buildkite/scripts/hardware_ci/run-cpu-test-arm.sh +++ b/.buildkite/scripts/hardware_ci/run-cpu-test-arm.sh @@ -30,7 +30,7 @@ remove_docker_container docker pull "$IMAGE" # Run the image -docker run -itd --cpuset-cpus="$CORE_RANGE" --entrypoint /bin/bash -v ~/.cache/huggingface:/root/.cache/huggingface -e HF_TOKEN --env VLLM_CPU_KVCACHE_SPACE=16 --env VLLM_CPU_CI_ENV=1 -e E2E_OMP_THREADS="$OMP_CORE_RANGE" --shm-size=4g --name "$CONTAINER_NAME" "$IMAGE" +docker run -itd --cpuset-cpus="$CORE_RANGE" --entrypoint /bin/bash -v ~/.cache/huggingface:/root/.cache/huggingface -e HF_TOKEN --env VLLM_CPU_KVCACHE_SPACE=16 --env VLLM_CPU_CI_ENV=1 -e E2E_OMP_THREADS="$OMP_CORE_RANGE" -e TERM=xterm-256color -e PY_COLORS=1 -e FORCE_COLOR=1 -e CLICOLOR_FORCE=1 --shm-size=4g --name "$CONTAINER_NAME" "$IMAGE" print_packages() { docker exec "$CONTAINER_NAME" bash -c " @@ -112,7 +112,9 @@ print_packages export CONTAINER_NAME export -f kernel_tests model_tests serving_tests case "$SHARD_ID" in - 0) timeout 30m bash -c kernel_tests ;; + # The kernel shard contains several large parametrized suites and can exceed + # 30 minutes on otherwise healthy Arm runners. + 0) timeout 60m bash -c kernel_tests ;; 1) timeout 30m bash -c model_tests ;; 2) timeout 30m bash -c serving_tests ;; esac diff --git a/.buildkite/scripts/hardware_ci/run-cpu-test.sh b/.buildkite/scripts/hardware_ci/run-cpu-test.sh index e47cc22c8fea..eecf70ea2bde 100644 --- a/.buildkite/scripts/hardware_ci/run-cpu-test.sh +++ b/.buildkite/scripts/hardware_ci/run-cpu-test.sh @@ -59,7 +59,7 @@ docker pull ubuntu:25.04 || true # building the docker image echo "--- :docker: Building Docker image" -BUILD_RETRY_PATTERN='dial tcp|i/o timeout|failed to authorize|TLS handshake timeout|connection reset|PROTOCOL_ERROR|Could not resolve host|Temporary failure in name resolution|dns error: failed to lookup address information|client error \(Connect\)|error sending request for url|Failed to fetch:' +BUILD_RETRY_PATTERN='dial tcp|i/o timeout|failed to authorize|TLS handshake timeout|connection reset|PROTOCOL_ERROR|Could not resolve host|Temporary failure in name resolution|dns error: failed to lookup address information|client error \(Connect\)|error sending request for url|Failed to fetch:|The read operation timed out|BrokenPipeError:.*Broken pipe|HTTP/[0-9.]+ stream [0-9]+ was not closed cleanly|fetch-pack: unexpected disconnect|fatal: early EOF|invalid index-pack output' BUILD_MAX_ATTEMPTS=4 # 1 initial + 3 retries BUILD_RETRY_WAITS=(10 20 40) # seconds to wait before retry 1/2/3 build_log="$(mktemp)" @@ -83,11 +83,23 @@ rm -f "$build_log" # Run the image, setting --shm-size=4g for tensor parallel. Default to # HF_HUB_OFFLINE so a warm ~/.cache/huggingface doesn't hit the network; -# retry once online if the cache is missing something. -OFFLINE_RETRY_PATTERN='huggingface_hub\.errors\.(LocalEntryNotFoundError|OfflineModeIsEnabled)' +# retry once online if the cache is missing something. vllm wraps offline +# cache-miss errors in ways that don't preserve the raw huggingface_hub +# exception name: get_config() re-raises as a generic ValueError +# (transformers_utils/config.py), and default_loader.py raises its own +# RuntimeError when a snapshot dir has a config but no weight files. Match +# those messages too or the fallback never triggers for those cache misses. +# +# Also mount ~/.cache/vllm: multimodal test fixtures (e.g. VideoAsset, via +# vllm/assets/video.py) download into VLLM_ASSETS_CACHE (~/.cache/vllm/assets +# by default), a directory distinct from the HF hub cache above. Without a +# persistent mount there, those assets never survive past one container's +# lifetime, so every run re-fetches them from the network on the "offline" +# attempt and unconditionally falls back to the online retry. +OFFLINE_RETRY_PATTERN='huggingface_hub\.errors\.(LocalEntryNotFoundError|OfflineModeIsEnabled)|Invalid repository ID or local directory specified|Cannot find any model weights with' run_test() { local hf_offline=$1 - docker run --rm --cpuset-cpus="$CORE_RANGE" --cpuset-mems="$NUMA_NODE" -v ~/.cache/huggingface:/root/.cache/huggingface --privileged=true -e HF_TOKEN -e VLLM_CPU_KVCACHE_SPACE=16 -e VLLM_CPU_CI_ENV=1 -e VLLM_CPU_SIM_MULTI_NUMA=1 -e VLLM_CPU_ATTN_SPLIT_KV=0 -e HF_HUB_OFFLINE="$hf_offline" -e HF_DATASETS_OFFLINE="$hf_offline" --shm-size=4g "$IMAGE_NAME" \ + docker run --rm --cpuset-cpus="$CORE_RANGE" --cpuset-mems="$NUMA_NODE" -v ~/.cache/huggingface:/root/.cache/huggingface -v ~/.cache/vllm:/root/.cache/vllm --privileged=true -e HF_TOKEN -e VLLM_CPU_KVCACHE_SPACE=16 -e VLLM_CPU_CI_ENV=1 -e VLLM_CPU_SIM_MULTI_NUMA=1 -e VLLM_CPU_ATTN_SPLIT_KV=0 -e HF_HUB_OFFLINE="$hf_offline" -e HF_DATASETS_OFFLINE="$hf_offline" -e TERM=xterm-256color -e PY_COLORS=1 -e FORCE_COLOR=1 -e CLICOLOR_FORCE=1 --shm-size=4g "$IMAGE_NAME" \ timeout "$TIMEOUT_VAL" bash -c "set -euox pipefail; echo \"--- Print packages\"; pip list; echo \"--- Running tests\"; ${TEST_COMMAND}" } diff --git a/.buildkite/scripts/hardware_ci/run-intel-ci-test.sh b/.buildkite/scripts/hardware_ci/run-intel-ci-test.sh index 58c0403b17a5..905ce0a96373 100644 --- a/.buildkite/scripts/hardware_ci/run-intel-ci-test.sh +++ b/.buildkite/scripts/hardware_ci/run-intel-ci-test.sh @@ -43,7 +43,7 @@ case "${test_suite}" in pytest -v -s v1/structured_output pytest -v -s v1/test_serial_utils.py pytest -v -s v1/e2e/general/test_correctness_sliding_window.py - pytest -v -s v1/spec_decode --ignore=v1/spec_decode/test_max_len.py --ignore=v1/spec_decode/test_speculators_eagle3.py --ignore=v1/spec_decode/test_acceptance_length.py --ignore=v1/spec_decode/test_speculators_correctness.py + pytest -v -s v1/spec_decode --ignore=v1/spec_decode/test_max_len.py --ignore=v1/spec_decode/test_speculators_eagle3.py --ignore=v1/spec_decode/test_acceptance_length.py --ignore=v1/spec_decode/test_speculators_correctness.py --deselect=tests/v1/spec_decode/test_mtp.py::test_glm_mtp_defers_lm_head pytest -v -s v1/kv_connector/unit --ignore=v1/kv_connector/unit/test_multi_connector.py --ignore=v1/kv_connector/unit/test_example_connector.py --ignore=v1/kv_connector/unit/test_lmcache_integration.py --ignore=v1/kv_connector/unit/test_hf3fs_client.py --ignore=v1/kv_connector/unit/test_hf3fs_connector.py --ignore=v1/kv_connector/unit/test_hf3fs_metadata_server.py --ignore=v1/kv_connector/unit/test_offloading_connector.py ;; server) @@ -65,8 +65,6 @@ case "${test_suite}" in pytest -v -s quantization/test_compressed_tensors.py::test_compressed_tensors_fp8 ;; graph) - export VLLM_XPU_ENABLE_XPU_GRAPH=1 - python3 examples/basic/offline_inference/generate.py --model Qwen/Qwen3-0.6B python3 examples/basic/offline_inference/generate.py --model Qwen/Qwen3-0.6B --kv-cache-dtype fp8 python3 examples/basic/offline_inference/generate.py --model INCModel/Qwen3-30B-A3B-Instruct-2507-MXFP4-CT-AutoRound --max-model-len 4096 diff --git a/.buildkite/scripts/hardware_ci/run-npu-test.sh b/.buildkite/scripts/hardware_ci/run-npu-test.sh index f7227095ac2e..d0c973f6daff 100644 --- a/.buildkite/scripts/hardware_ci/run-npu-test.sh +++ b/.buildkite/scripts/hardware_ci/run-npu-test.sh @@ -76,21 +76,12 @@ FROM ${BASE_IMAGE_NAME} ENV DEBIAN_FRONTEND=noninteractive ENV SOC_VERSION="ascend910b1" -# Install uv and configure uv to use internal PyPI mirror -RUN curl -LsSf https://astral.sh/uv/install.sh | sh -ENV PATH="/root/.local/bin:$PATH" -ENV UV_SYSTEM_PYTHON=true -ENV UV_INDEX_URL="http://cache-service-vllm.nginx-pypi-cache.svc.cluster.local:${PYPI_CACHE_PORT}/pypi/simple" -ENV UV_EXTRA_INDEX_URL="https://mirrors.huaweicloud.com/ascend/repos/pypi" - -RUN apt-get update -y && \ - apt-get install -y python3-pip git vim wget net-tools gcc g++ cmake libnuma-dev && \ - rm -rf /var/cache/apt/* && \ - rm -rf /var/lib/apt/lists/* +RUN pip config set global.index-url http://cache-service-vllm.nginx-pypi-cache.svc.cluster.local:${PYPI_CACHE_PORT}/pypi/simple && \ + pip config set global.trusted-host cache-service-vllm.nginx-pypi-cache.svc.cluster.local # Install for pytest to make the docker build cache layer always valid -RUN --mount=type=cache,target=/root/.cache/uv \ - uv pip install pytest>=6.0 'modelscope<1.38' --quiet +RUN --mount=type=cache,target=/root/.cache/pip \ + pip install pytest>=6.0 WORKDIR /workspace/vllm @@ -167,5 +158,5 @@ docker run \ "${image_name}" \ bash -c ' set -e - pytest -v -s tests/e2e/vllm_interface/test_vllm_pr_interface_compatibility.py + pytest --noconftest -v -s tests/e2e/vllm_interface/test_vllm_pr_interface_compatibility.py ' diff --git a/.buildkite/scripts/hardware_ci/run-xpu-batch-invariance.sh b/.buildkite/scripts/hardware_ci/run-xpu-batch-invariance.sh new file mode 100755 index 000000000000..4a8e41d580b7 --- /dev/null +++ b/.buildkite/scripts/hardware_ci/run-xpu-batch-invariance.sh @@ -0,0 +1,243 @@ +#!/usr/bin/env bash +set -euo pipefail + +repo_root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../../.." && pwd)" + +model="ibm-research/PowerMoE-3b" +port=8000 +num_decode_tokens=128 +server_pid= +server_log= + +# XPU MoE kernels may not safely handle the -1 routes emitted for padding. +export VLLM_MOE_SKIP_PADDING=0 + +cd "$repo_root" + +cleanup_server() { + local attempt + + if [[ -n "$server_pid" ]] && kill -0 "$server_pid" 2>/dev/null; then + kill "$server_pid" 2>/dev/null || true + + for ((attempt = 0; attempt < 30; attempt++)); do + if ! kill -0 "$server_pid" 2>/dev/null; then + break + fi + sleep 1 + done + + if kill -0 "$server_pid" 2>/dev/null; then + echo "vLLM did not stop after 30 seconds; sending SIGKILL" >&2 + kill -KILL "$server_pid" 2>/dev/null || true + fi + + wait "$server_pid" 2>/dev/null || true + fi + + server_pid= +} + +on_exit() { + local status=$? + + cleanup_server + if [[ "$status" -ne 0 && -n "$server_log" && -f "$server_log" ]]; then + if command -v buildkite-agent >/dev/null 2>&1; then + buildkite-agent artifact upload "$server_log" || + echo "Failed to upload $server_log as a Buildkite artifact" >&2 + fi + fi + [[ -z "$server_log" ]] || rm -f "$server_log" + exit "$status" +} +trap on_exit EXIT + +wait_for_server() { + for _ in $(seq 1 600); do + if curl -sf "http://127.0.0.1:$port/health" >/dev/null; then + return + fi + if ! kill -0 "$server_pid" 2>/dev/null; then + echo "vLLM exited before becoming healthy; see the server log above." >&2 + return 1 + fi + sleep 1 + done + echo "Timed out waiting for vLLM server health check" >&2 + return 1 +} + +request_outputs() { + local prompts_json=$1 + local expected_choices=$2 + local response + + response=$(printf \ + '{"model":"%s","prompt":%s,"temperature":0.7,"top_p":0.95,"seed":0,"max_tokens":%s,"logprobs":64,"ignore_eos":true,"return_token_ids":true}' \ + "$model" "$prompts_json" "$num_decode_tokens" | + curl --fail --silent --show-error \ + --max-time 1200 \ + "http://127.0.0.1:$port/v1/completions" \ + -H "Content-Type: application/json" \ + --data-binary @-) + + local -a token_id_arrays logprob_arrays + mapfile -t token_id_arrays < <( + grep -o '"token_ids":\[[^]]*\]' <<<"$response" | + sed 's/^"token_ids":\[//; s/\]$//' + ) + mapfile -t logprob_arrays < <( + grep -o '"token_logprobs":\[[^]]*\]' <<<"$response" | + sed 's/^"token_logprobs":\[//; s/\]$//' + ) + if [[ "${#token_id_arrays[@]}" -ne "$expected_choices" ]] || + [[ "${#logprob_arrays[@]}" -ne "$expected_choices" ]]; then + echo "Expected $expected_choices choices with token IDs and logits" >&2 + return 1 + fi + local index + local array + local value + local -a values + for index in "${!logprob_arrays[@]}"; do + array="${logprob_arrays[index]}" + IFS=, read -r -a values <<<"$array" + if [[ "${#values[@]}" -ne "$num_decode_tokens" ]]; then + echo "Completion returned an unexpected number of decode steps" >&2 + return 1 + fi + IFS=, read -r -a values <<<"${token_id_arrays[index]}" + if [[ "${#values[@]}" -ne "$num_decode_tokens" ]]; then + echo "Completion returned an unexpected number of generated tokens" >&2 + return 1 + fi + for value in "${values[@]}"; do + if [[ ! "$value" =~ ^[0-9]+$ ]]; then + echo "Completion returned an invalid token ID" >&2 + return 1 + fi + done + IFS=, read -r -a values <<<"$array" + for value in "${values[@]}"; do + if [[ ! "$value" =~ ^-?([0-9]+([.][0-9]*)?|[.][0-9]+)([eE][+-]?[0-9]+)?$ ]]; then + echo "Completion returned non-finite raw logits" >&2 + return 1 + fi + done + printf '%s|%s\n' "${token_id_arrays[index]}" "${logprob_arrays[index]}" + done +} + +make_prompt() { + local length=$1 + local offset=${2:-0} + local i + local prompt="Use the following system-design context to answer the question. Scenario $offset:" + for ((i = 0; i < length; i++)); do + prompt+=" context" + done + prompt+="\\n\\nQuestion: How should batch-invariant inference behave when this same request is decoded alongside unrelated requests?\\nAnswer:" + printf '"%s"' "$prompt" +} + +first_difference() { + local actual=$1 + local expected=$2 + local position=$3 + local actual_tokens=${actual%%|*} + local expected_tokens=${expected%%|*} + local actual_logits=${actual#*|} + local expected_logits=${expected#*|} + local -a actual_token_values + local -a expected_token_values + local -a actual_logit_values + local -a expected_logit_values + local step + + IFS=, read -r -a actual_token_values <<<"$actual_tokens" + IFS=, read -r -a expected_token_values <<<"$expected_tokens" + IFS=, read -r -a actual_logit_values <<<"$actual_logits" + IFS=, read -r -a expected_logit_values <<<"$expected_logits" + for ((step = 0; step < num_decode_tokens; step++)); do + if [[ "${actual_token_values[step]}" != "${expected_token_values[step]}" ]]; then + printf 'batch position %s: step=%s, baseline token=%s, actual token=%s' \ + "$position" \ + "$step" \ + "${expected_token_values[step]}" \ + "${actual_token_values[step]}" + return + fi + if [[ "${actual_logit_values[step]}" != "${expected_logit_values[step]}" ]]; then + printf 'batch position %s: step=%s, token=%s, baseline logit=%s, actual logit=%s' \ + "$position" \ + "$step" \ + "${expected_token_values[step]}" \ + "${expected_logit_values[step]}" \ + "${actual_logit_values[step]}" + return + fi + done +} + +run_e2e() { + local needle + local batch + local baseline_output + local batch_response + local difference + local position + local -a fillers + local -a batch_outputs + local -a differences=() + + echo "Testing PowerMoE-3b: TP=2, VLLM_BATCH_INVARIANT=1, VLLM_MOE_SKIP_PADDING=0" + server_log=$(mktemp "${TMPDIR:-/tmp}/xpu-batch-invariance.XXXXXX.log") + VLLM_BATCH_INVARIANT=1 vllm serve "$model" \ + --tensor-parallel-size 2 \ + --max-model-len 1024 \ + --max-num-seqs 16 \ + --max-num-batched-tokens 512 \ + --enable-chunked-prefill \ + --no-enable-prefix-caching \ + --enforce-eager \ + --seed 0 \ + --max-logprobs 64 \ + --logprobs-mode raw_logits \ + --port "$port" > >(tee "$server_log") 2>&1 & + server_pid=$! + wait_for_server + + needle=$(make_prompt 257) + local filler_lengths=(7 31 63 95 127 159 191 223 255 287 319 383 447) + for position in "${!filler_lengths[@]}"; do + fillers+=( "$(make_prompt "${filler_lengths[position]}" "$((position + 1))")" ) + done + batch="[$needle,${fillers[0]},${fillers[1]},${fillers[2]},${fillers[3]},${fillers[4]},${fillers[5]},$needle,${fillers[6]},${fillers[7]},${fillers[8]},${fillers[9]},${fillers[10]},${fillers[11]},${fillers[12]},$needle]" + + echo "Requesting BS=1 baseline ($num_decode_tokens decode tokens)..." + baseline_output=$(request_outputs "$needle" 1) + echo "Received BS=1 baseline." + echo "Requesting BS=16 batch ($((16 * num_decode_tokens)) decode tokens)..." + batch_response=$(request_outputs "$batch" 16) + mapfile -t batch_outputs <<<"$batch_response" + echo "Received BS=16 batch; comparing generated tokens and logits..." + + for position in 0 7 15; do + difference=$(first_difference "${batch_outputs[position]}" "$baseline_output" "$position") + if [[ -n "$difference" ]]; then + differences+=("$difference") + fi + done + + if [[ "${#differences[@]}" -ne 0 ]]; then + echo "Batch-invariant PowerMoE-3b output changed: ${differences[*]}" >&2 + return 1 + fi + echo "PowerMoE-3b batch invariance check passed" + cleanup_server + rm -f "$server_log" + server_log= +} + +run_e2e diff --git a/.buildkite/scripts/initialized-snapshot-e2e.sh b/.buildkite/scripts/initialized-snapshot-e2e.sh new file mode 100755 index 000000000000..cd0511ffdd9a --- /dev/null +++ b/.buildkite/scripts/initialized-snapshot-e2e.sh @@ -0,0 +1,311 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +set -Eeuo pipefail +readonly MODEL="Qwen/Qwen3-0.6B" +readonly MODEL_REVISION="c1899de289a04d12100db370d81485cdf75e47ca" +readonly EXPECTED_TOKEN_ID="12095" +readonly EXPECTED_TEXT=" Paris" +readonly CLEANUP_RESERVE_S=300 +RUN_LIMIT_S="${SNAPSHOT_E2E_RUN_LIMIT_S:-3300}" +[[ "$RUN_LIMIT_S" =~ ^[1-9][0-9]{2,3}$ ]] \ + || { echo "SNAPSHOT_E2E_RUN_LIMIT_S must be a bounded decimal integer" >&2; exit 1; } +(( RUN_LIMIT_S >= CLEANUP_RESERVE_S + 60 && RUN_LIMIT_S <= 3300 )) \ + || { echo "SNAPSHOT_E2E_RUN_LIMIT_S must be between 360 and 3300" >&2; exit 1; } +readonly RUN_LIMIT_S +readonly HOST_HF_CACHE="${SNAPSHOT_E2E_HOST_HF_CACHE:-}" +HF_CACHE_MOUNT=() +if [[ -n "$HOST_HF_CACHE" ]]; then + [[ "$HOST_HF_CACHE" == /* && -d "$HOST_HF_CACHE" ]] \ + || { echo "SNAPSHOT_E2E_HOST_HF_CACHE must be an existing absolute directory" >&2; exit 1; } + HF_CACHE_MOUNT=(--mount "type=bind,source=$HOST_HF_CACHE,target=/e2e/hf,readonly") + PREPARE_ARTIFACT_ROOT='install -d -m 0700 /e2e' +else + PREPARE_ARTIFACT_ROOT='mkdir -m 0700 /e2e && mkdir -m 0700 /e2e/hf' +fi +readonly PREPARE_ARTIFACT_ROOT +if (( $# != 2 )); then + echo "usage: $0 IMAGE_TAG BUILDKITE_COMMIT" >&2 + exit 2 +fi +readonly IMAGE_TAG="$1" +readonly EXPECTED_COMMIT="$2" +readonly DEADLINE_EPOCH="$(( $(date +%s) + RUN_LIMIT_S ))" +RUN_ID="" CONTAINER_NAME="" CONTAINER_ID="" +GPU_UUID="" PORT_ONE="" PORT_TWO="" +LINK_REMAP_BASELINE="" +SNAPSHOT_PIDS=() +die() { + echo "initialized snapshot E2E: $*" >&2 + exit 1 +} + +run() { + local label="$1" maximum_s="$2" available limit + shift 2 + available="$((DEADLINE_EPOCH - $(date +%s) - CLEANUP_RESERVE_S))" + (( available > 0 )) || die "no time remains before the cleanup reserve" + (( available < maximum_s )) && limit="$available" || limit="$maximum_s" + echo "--- ${label} (timeout ${limit}s)" >&2 + timeout --signal=TERM --kill-after=30s "${limit}s" "$@" +} + +link_remaps() { + find /dev/shm -maxdepth 1 -name 'link_remap.*' \ + -printf '%p|%D|%i|%m|%s\n' | sort +} + +gpu_pids() { + nvidia-smi --id="$GPU_UUID" --query-compute-apps=pid \ + --format=csv,noheader,nounits | sed '/^[[:space:]]*$/d' +} + +port_listeners() { + ss -H -ltn "sport = :$1" +} + +state_clean() { + local pid port output + for pid in "${SNAPSHOT_PIDS[@]}"; do + [[ ! -e "/proc/$pid" ]] || return 1 + done + output="$(gpu_pids)" || return 1 + [[ -z "$output" ]] || return 1 + output="$(link_remaps)" || return 1 + [[ "$output" == "$LINK_REMAP_BASELINE" ]] || return 1 + for port in "$PORT_ONE" "$PORT_TWO"; do + [[ -n "$port" ]] || continue + output="$(port_listeners "$port")" || return 1 + [[ -z "$output" ]] || return 1 + done +} + +wait_clean() { + local _attempt + for ((_attempt = 0; _attempt < 60; _attempt++)); do + state_clean && return 0 + sleep 1 + done + return 1 +} + +container_names() { + timeout 15s docker container ls --all --format '{{.Names}}' +} + +cleanup() { + local failed=0 names="" actual="" expected="" + if [[ -n "$CONTAINER_NAME" ]]; then + names="$(container_names 2>/dev/null)" || return 1 + if grep -Fxq "$CONTAINER_NAME" <<< "$names"; then + actual="$(docker container inspect \ + --format '{{.Id}}|{{index .Config.Labels "ai.vllm.snapshot.e2e.run"}}' \ + "$CONTAINER_NAME" 2>/dev/null)" || return 1 + expected="${CONTAINER_ID:-${actual%%|*}}|$RUN_ID" + if [[ "$actual" != "$expected" ]]; then + echo "refusing to remove a container with mismatched identity" >&2 + failed=1 + else + timeout 60s docker rm --force "$CONTAINER_NAME" >/dev/null \ + || failed=1 + fi + fi + names="$(container_names 2>/dev/null)" || return 1 + grep -Fxq "$CONTAINER_NAME" <<< "$names" && failed=1 + fi + if [[ -n "$PORT_TWO" ]] && ! wait_clean; then + echo "process, GPU, port, or link-remap residue remained" >&2 + failed=1 + fi + return "$failed" +} + +on_exit() { + local status=$? + trap - EXIT INT TERM + set +e + cleanup || status=1 + exit "$status" +} + +trap on_exit EXIT +trap 'exit 130' INT +trap 'exit 143' TERM + +parse_inspect() { + timeout 30s python3 -c ' +import json, math, sys +data = json.load(sys.stdin) +model, revision, gpu_uuid, token_id, text = sys.argv[1:] +expected = { + "boundary": "post-engine-init-reloadable-state-released", + "model": model, "model_revision": revision, + "gpu_uuid": gpu_uuid, "oracle_token_ids": [int(token_id)], + "oracle_text": text, +} +bad = next((field for field, value in expected.items() if data.get(field) != value), None) +if bad: + raise SystemExit(f"inspect field {bad} mismatch: {data.get(bad)!r}") +logprob = data.get("oracle_sampled_token_logprob") +if type(logprob) is not float or not math.isfinite(logprob): + raise SystemExit("inspect logprob is not finite") +pids = data.get("process_tree") +if not pids: + raise SystemExit("inspect process_tree is empty") +for pid in pids: + print(f"pid={pid}") +print(f"oracle_logprob={logprob}") +' "$MODEL" "$MODEL_REVISION" "$GPU_UUID" "$EXPECTED_TOKEN_ID" \ + "$EXPECTED_TEXT" +} + +assert_oracle() { + local port="$1" ordinal="$2" response + response="$(run "restore ${ordinal} public completion" 120 curl \ + --fail --silent --show-error --max-time 120 \ + --header 'Content-Type: application/json' \ + --data "{\"model\":\"${MODEL}\",\"prompt\":\"The capital of France is\",\"temperature\":0,\"seed\":0,\"min_tokens\":1,\"max_tokens\":1,\"logprobs\":0,\"return_token_ids\":true}" \ + "http://127.0.0.1:${port}/v1/completions")" + printf '%s' "$response" | timeout 30s python3 -c ' +import json, math, sys +choice = json.load(sys.stdin)["choices"][0] +token_id, text, expected, ordinal = sys.argv[1:] +values = choice["logprobs"]["token_logprobs"] +actual = float(values[0]) if len(values) == 1 else math.nan +if (choice["token_ids"] != [int(token_id)] or choice["text"] != text + or not math.isfinite(actual) or abs(actual - float(expected)) > 1e-3): + raise SystemExit(f"public oracle mismatch: {choice!r}") +print(f"restore={ordinal} token_id={token_id} text={text!r} logprob={actual}") +' "$EXPECTED_TOKEN_ID" "$EXPECTED_TEXT" "$INSPECT_LOGPROB" "$ordinal" +} + +[[ "$(uname -s)" == "Linux" && "$(uname -m)" == "x86_64" ]] \ + || die "host must be Linux x86_64" +[[ "$EXPECTED_COMMIT" =~ ^[0-9a-f]{40}$ ]] \ + || die "BUILDKITE_COMMIT must be a full lowercase commit hash" +[[ "$(< /proc/sys/kernel/io_uring_disabled)" == "2" ]] \ + || die "kernel.io_uring_disabled must already equal 2" +run "Docker preflight" 60 docker info >/dev/null + +VISIBLE_GPU="${NVIDIA_VISIBLE_DEVICES:-}" +if [[ -z "$VISIBLE_GPU" || "$VISIBLE_GPU" == "all" || "$VISIBLE_GPU" == "void" ]]; then + VISIBLE_GPU="${CUDA_VISIBLE_DEVICES:-}" +fi +if [[ -z "$VISIBLE_GPU" || "$VISIBLE_GPU" == "all" || "$VISIBLE_GPU" == "void" ]]; then + CDI_GPU="${BUILDKITE_PLUGIN_DOCKER_DEVICES_0:-}" + if [[ "$CDI_GPU" == nvidia.com/gpu=* ]]; then + VISIBLE_GPU="${CDI_GPU#nvidia.com/gpu=}" + else + VISIBLE_GPU="" + fi +fi +[[ -n "$VISIBLE_GPU" && "$VISIBLE_GPU" != all && "$VISIBLE_GPU" != void \ + && "$VISIBLE_GPU" != *","* ]] \ + || die "the runner must expose exactly one selected GPU" +GPU_ROW="$(nvidia-smi --id="$VISIBLE_GPU" \ + --query-gpu=name,uuid,mig.mode.current,memory.total \ + --format=csv,noheader,nounits)" +IFS=',' read -r GPU_NAME GPU_UUID MIG_MODE GPU_MEMORY_MIB <<< "$GPU_ROW" +for variable in GPU_NAME GPU_UUID MIG_MODE GPU_MEMORY_MIB; do + printf -v "$variable" '%s' "${!variable#"${!variable%%[![:space:]]*}"}" +done +[[ "$GPU_NAME" == *H200* && "$GPU_UUID" == GPU-* && "$MIG_MODE" == Disabled ]] \ + || die "selected device is not a full non-MIG H200: $GPU_ROW" +(( GPU_MEMORY_MIB >= 130000 )) || die "selected H200 does not expose full memory" +GPU_PIDS="$(gpu_pids)" || die "failed to inspect the selected GPU" +[[ -z "$GPU_PIDS" ]] || die "selected H200 is not idle" + +RUN_ID="snapshot-e2e-$(date +%s)-$$-$RANDOM" +CONTAINER_NAME="vllm-$RUN_ID" +read -r PORT_ONE PORT_TWO < <(python3 -c 'import socket; s=[socket.socket(),socket.socket()]; [x.bind(("127.0.0.1",0)) for x in s]; print(*(x.getsockname()[1] for x in s))') +LINK_REMAP_BASELINE="$(link_remaps)" + +run "pull exact candidate image" 900 docker pull --platform linux/amd64 \ + "$IMAGE_TAG" >/dev/null +IFS='|' read -r IMAGE_ID IMAGE_PLATFORM OCI_COMMIT VLLM_COMMIT < <( + docker image inspect \ + --format '{{.Id}}|{{.Os}}/{{.Architecture}}|{{index .Config.Labels "org.opencontainers.image.revision"}}|{{index .Config.Labels "ai.vllm.build.commit"}}' \ + "$IMAGE_TAG" +) +[[ "$IMAGE_ID" =~ ^sha256:[0-9a-f]{64}$ && "$IMAGE_PLATFORM" == linux/amd64 ]] \ + || die "candidate image identity is invalid" +[[ "$OCI_COMMIT" == "$EXPECTED_COMMIT" && "$VLLM_COMMIT" == "$EXPECTED_COMMIT" ]] \ + || die "candidate image commit labels do not match" +CONTAINER_ID="$(run "start exact candidate container" 90 docker run --detach \ + --name "$CONTAINER_NAME" --label "ai.vllm.snapshot.e2e.run=$RUN_ID" \ + --gpus "device=$GPU_UUID" --user 0 --privileged --pid=host --ipc=host \ + --network=host --env "CUDA_VISIBLE_DEVICES=$GPU_UUID" --env HF_HUB_DISABLE_TELEMETRY=1 \ + --env VLLM_NO_USAGE_STATS=1 --env VLLM_USE_V2_MODEL_RUNNER=1 \ + ${HF_CACHE_MOUNT[@]+"${HF_CACHE_MOUNT[@]}"} \ + --entrypoint sleep "$IMAGE_ID" infinity)" +[[ "$CONTAINER_ID" =~ ^[0-9a-f]{64}$ ]] || die "invalid container ID" +run "prepare private artifact root" 60 docker exec "$CONTAINER_NAME" sh -c \ + "$PREPARE_ARTIFACT_ROOT && df -Pk / | awk 'NR == 2 && \$4 >= 10485760 {ok=1} END {exit !ok}'" +run "verify snapshot runtime" 60 docker exec "$CONTAINER_NAME" sh -c \ + 'criu --version; test -x /usr/local/sbin/cuda-checkpoint; test -f /usr/local/lib/criu/cuda_plugin.so' +run "verify selected CUDA device" 60 docker exec "$CONTAINER_NAME" python3 -c ' +import sys, torch +if torch.cuda.device_count() != 1: + raise SystemExit("expected exactly one visible CUDA device") +actual = str(torch.cuda.get_device_properties(0).uuid).removeprefix("GPU-") +if actual != sys.argv[1].removeprefix("GPU-"): + raise SystemExit(f"CUDA device {actual} does not match {sys.argv[1]}") +print(f"selected_cuda_uuid=GPU-{actual}") +' "$GPU_UUID" +OFFLINE_EXEC=(docker exec --env HF_HOME=/e2e/hf --env HF_HUB_OFFLINE=1 \ + --env TRANSFORMERS_OFFLINE=1 --env VLLM_SNAPSHOT_TIMEOUT_S=900 \ + "$CONTAINER_NAME") +if [[ -n "$HOST_HF_CACHE" ]]; then + run "validate pinned model in read-only host cache" 120 \ + "${OFFLINE_EXEC[@]}" python3 -c \ + 'from pathlib import Path; from huggingface_hub import snapshot_download; import sys; p=snapshot_download(repo_id=sys.argv[1], revision=sys.argv[2], local_files_only=True); assert Path(p).name == sys.argv[2]' \ + "$MODEL" "$MODEL_REVISION" +else + run "prefetch pinned public model" 900 docker exec --env HF_HOME=/e2e/hf \ + "$CONTAINER_NAME" python3 -c \ + 'from pathlib import Path; from huggingface_hub import snapshot_download; import sys; p=snapshot_download(repo_id=sys.argv[1], revision=sys.argv[2]); assert Path(p).name == sys.argv[2]' \ + "$MODEL" "$MODEL_REVISION" +fi + +run "create compact initialized snapshot" 1200 "${OFFLINE_EXEC[@]}" \ + vllm snapshot create "$MODEL" --revision "$MODEL_REVISION" \ + --tokenizer-revision "$MODEL_REVISION" --snapshot-dir /e2e/artifact \ + --dtype float16 --max-model-len 512 --gpu-memory-utilization 0.50 +wait_clean || die "create left process, GPU, port, or link-remap residue" + +INSPECT_JSON="$(run "inspect compact initialized snapshot" 120 \ + "${OFFLINE_EXEC[@]}" vllm snapshot inspect /e2e/artifact)" +INSPECT_RECEIPT="$(printf '%s' "$INSPECT_JSON" | parse_inspect)" +mapfile -t SNAPSHOT_PIDS < <(sed -n 's/^pid=//p' <<< "$INSPECT_RECEIPT") +INSPECT_LOGPROB="$(sed -n 's/^oracle_logprob=//p' <<< "$INSPECT_RECEIPT")" +(( ${#SNAPSHOT_PIDS[@]} > 0 )) || die "inspect returned no PIDs" +state_clean || die "create left recorded state alive" +echo "$INSPECT_RECEIPT" + +run "restore compact snapshot 1" 900 "${OFFLINE_EXEC[@]}" \ + vllm snapshot restore /e2e/artifact --host 127.0.0.1 --port "$PORT_ONE" +PORT_LISTENERS="$(port_listeners "$PORT_ONE")" || die "failed to inspect restore 1 port" +[[ -n "$PORT_LISTENERS" ]] || die "restore 1 did not bind its port" +GPU_PIDS="$(gpu_pids)" || die "failed to inspect the selected GPU" +[[ -n "$GPU_PIDS" ]] || die "restore 1 did not engage the selected GPU" +assert_oracle "$PORT_ONE" 1 +run "stop container after restore 1" 60 docker stop --time 20 "$CONTAINER_NAME" >/dev/null +[[ "$(docker container inspect --format '{{.Id}}' "$CONTAINER_NAME")" == "$CONTAINER_ID" ]] \ + || die "docker stop replaced the container" +wait_clean || die "restore 1 teardown left residue" + +run "start the same container" 60 docker start "$CONTAINER_NAME" >/dev/null +[[ "$(docker container inspect --format '{{.Id}}' "$CONTAINER_NAME")" == "$CONTAINER_ID" ]] \ + || die "docker start replaced the container" +run "restore compact snapshot 2" 900 "${OFFLINE_EXEC[@]}" \ + vllm snapshot restore /e2e/artifact --host 127.0.0.1 --port "$PORT_TWO" +PORT_LISTENERS="$(port_listeners "$PORT_TWO")" || die "failed to inspect restore 2 port" +[[ -n "$PORT_LISTENERS" ]] || die "restore 2 did not bind its port" +GPU_PIDS="$(gpu_pids)" || die "failed to inspect the selected GPU" +[[ -n "$GPU_PIDS" ]] || die "restore 2 did not engage the selected GPU" +assert_oracle "$PORT_TWO" 2 +run "stop container after restore 2" 60 docker stop --time 20 "$CONTAINER_NAME" >/dev/null +wait_clean || die "restore 2 teardown left residue" +cleanup || die "exact cleanup failed" +echo "container_id=$CONTAINER_ID restore_1_port=$PORT_ONE restore_2_port=$PORT_TWO" +echo "result=PASS" diff --git a/.buildkite/scripts/install-kv-connectors.sh b/.buildkite/scripts/install-kv-connectors.sh index ef47c3625f44..8733a21347d3 100755 --- a/.buildkite/scripts/install-kv-connectors.sh +++ b/.buildkite/scripts/install-kv-connectors.sh @@ -9,9 +9,20 @@ if python3 -c "import torch; raise SystemExit(0 if torch.version.hip is not None exit 0 fi -REQUIREMENTS_FILE="${KV_CONNECTORS_REQUIREMENTS:-/vllm-workspace/requirements/kv_connectors.txt}" +# Select requirements before optional packages can change the installed Torch. +CUDA_MAJOR=$(python3 -c 'import torch; print(torch.version.cuda.split(".", 1)[0])') +DEFAULT_REQUIREMENTS=/vllm-workspace/requirements/kv_connectors.txt +if [ "${CUDA_MAJOR}" = "12" ]; then + DEFAULT_REQUIREMENTS=/vllm-workspace/requirements/kv_connectors_cu12.txt +fi +REQUIREMENTS_FILE="${KV_CONNECTORS_REQUIREMENTS:-${DEFAULT_REQUIREMENTS}}" -uv pip install --system -r "${REQUIREMENTS_FILE}" +CONSTRAINTS_DIR=$(mktemp -d) +trap 'rm -rf "${CONSTRAINTS_DIR}"' EXIT +uv pip freeze --system > "${CONSTRAINTS_DIR}/installed.txt" +grep -E '^(torch|torchaudio|torchvision|triton|cuda-bindings|cuda-python|cuda-toolkit)==' \ + "${CONSTRAINTS_DIR}/installed.txt" > "${CONSTRAINTS_DIR}/constraints.txt" +uv pip install --system -c "${CONSTRAINTS_DIR}/constraints.txt" -r "${REQUIREMENTS_FILE}" KV_METADATA=$(python3 - <<'PY' import importlib.metadata as metadata @@ -56,5 +67,5 @@ PY # build first to avoid a clash. if [ "${CUDA_MAJOR}" = "13" ] && [ -n "${MOONCAKE_VERSION}" ]; then uv pip uninstall --system mooncake-transfer-engine 2>/dev/null || true - uv pip install --system "mooncake-transfer-engine-cuda13==${MOONCAKE_VERSION}" + uv pip install --system -c "${CONSTRAINTS_DIR}/constraints.txt" "mooncake-transfer-engine-cuda13==${MOONCAKE_VERSION}" fi diff --git a/.buildkite/scripts/install-kv-offload.sh b/.buildkite/scripts/install-kv-offload.sh new file mode 100644 index 000000000000..46d756b621b1 --- /dev/null +++ b/.buildkite/scripts/install-kv-offload.sh @@ -0,0 +1,18 @@ +#!/bin/bash +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project + +set -euo pipefail + +if [ -z "${CUDA_VERSION:-}" ]; then + exit 0 +fi + +# TODO: Use the PyPI wheel once it is published. +uv pip install --system \ + "kvcr @ git+https://github.com/ai-dynamo/kvcr.git@v0.1.0" + +# Keep only the NIXL wheel matching the CI image's CUDA runtime. +NIXL_VERSION=$(uv pip show --system nixl | sed -n 's/^Version: //p') +uv pip uninstall --system nixl-cu12 nixl-cu13 +uv pip install --system --no-deps "nixl-cu${CUDA_VERSION%%.*}==${NIXL_VERSION}" diff --git a/.buildkite/scripts/publish-release-images.sh b/.buildkite/scripts/publish-release-images.sh index 2fa334241fce..dec366390b00 100755 --- a/.buildkite/scripts/publish-release-images.sh +++ b/.buildkite/scripts/publish-release-images.sh @@ -61,6 +61,13 @@ if target_enabled cuda-13-0; then docker manifest create "vllm/vllm-openai:v${RELEASE_VERSION}" "vllm/vllm-openai:v${RELEASE_VERSION}-x86_64" "vllm/vllm-openai:v${RELEASE_VERSION}-aarch64" docker manifest push vllm/vllm-openai:latest docker manifest push "vllm/vllm-openai:v${RELEASE_VERSION}" + + ZSTD_DIGEST=$(docker buildx imagetools inspect \ + "public.ecr.aws/q9t5s3a7/vllm-release-repo:${COMMIT}-x86_64" \ + --format '{{json .Manifest.Digest}}' | tr -d '"') + .buildkite/scripts/publish-zstd-image.sh \ + "public.ecr.aws/q9t5s3a7/vllm-release-repo@${ZSTD_DIGEST}" \ + "vllm/vllm-openai:v${RELEASE_VERSION}-x86_64-zstd" fi # ---- CUDA 12.9 ---- diff --git a/.buildkite/scripts/publish-zstd-image.sh b/.buildkite/scripts/publish-zstd-image.sh new file mode 100755 index 000000000000..bd3d2f99d65c --- /dev/null +++ b/.buildkite/scripts/publish-zstd-image.sh @@ -0,0 +1,45 @@ +#!/bin/bash +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project + +set -euo pipefail + +if [ "$#" -ne 2 ]; then + echo "Usage: $0 SOURCE@sha256:DIGEST DESTINATION-zstd" >&2 + exit 2 +fi + +SOURCE=$1 +DESTINATION=$2 +if [[ ! "$SOURCE" =~ ^[a-zA-Z0-9][a-zA-Z0-9._:/-]*@sha256:[a-f0-9]{64}$ ]] || + [[ ! "$DESTINATION" =~ ^[a-zA-Z0-9][a-zA-Z0-9._:/-]*:[a-zA-Z0-9_][a-zA-Z0-9_.-]*-zstd$ ]]; then + echo "ERROR: source must use an immutable sha256 digest and destination must end in -zstd" >&2 + exit 2 +fi + +BUILDER= +cleanup() { + status=$? + trap - EXIT INT TERM + if [ -n "$BUILDER" ] && ! docker buildx rm --force "$BUILDER"; then + echo "ERROR: failed to remove Buildx builder $BUILDER" >&2 + [ "$status" -ne 0 ] || status=1 + fi + exit "$status" +} +trap cleanup EXIT +trap 'exit 130' INT +trap 'exit 143' TERM + +BUILDER=$(docker buildx create \ + --driver docker-container \ + --driver-opt network=host \ + --driver-opt 'image=moby/buildkit:v0.32.2@sha256:28a898719c18a33f4e8000685287fa36fd0dd9560c6440227d3a732d79bb41d8') + +printf '# check=error=true\n\nFROM %s\n' "$SOURCE" | docker buildx build \ + --builder "$BUILDER" \ + --platform linux/amd64 \ + --progress plain \ + --provenance=mode=min \ + --output "type=image,name=${DESTINATION},push=true,compression=zstd,compression-level=3,force-compression=true,oci-mediatypes=true" \ + - diff --git a/.buildkite/scripts/rocm/build-config.sh b/.buildkite/scripts/rocm/build-config.sh new file mode 100644 index 000000000000..ad0179b16359 --- /dev/null +++ b/.buildkite/scripts/rocm/build-config.sh @@ -0,0 +1,85 @@ +#!/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 Rock, set CI_ROCM_DOCKERFILE_BASE=docker/Dockerfile.rock_base and +# CI_ROCM_DOCKERFILE=docker/Dockerfile.rock. +# 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/refresh-base-image.sh b/.buildkite/scripts/rocm/refresh-base-image.sh index 23d10492a99c..10e594577a87 100755 --- a/.buildkite/scripts/rocm/refresh-base-image.sh +++ b/.buildkite/scripts/rocm/refresh-base-image.sh @@ -4,6 +4,10 @@ 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}}" @@ -12,7 +16,12 @@ DEFAULT_ROCM_BASE_METADATA_VERSION="2" DEFAULT_ROCM_BASE_CONTENT_FILES="${DOCKERFILE}" ROCM_BASE_LAYER_CACHE_REF="" -ROCM_BASE_TRUSTED_LAYER_CACHE_REF="${CACHE_REPO}:rocm-base-main" +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=() @@ -120,7 +129,7 @@ configure_rocm_base_layer_cache() { fi scope=$(rocm_base_layer_cache_scope) - ROCM_BASE_LAYER_CACHE_REF="${CACHE_REPO}:rocm-base-${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}" ) @@ -139,7 +148,7 @@ configure_rocm_base_layer_cache() { extract_arg_default() { local arg_name="$1" - sed -n -E "s/^[[:space:]]*ARG[[:space:]]+${arg_name}=\"?([^\"[:space:]]+)\"?.*/\\1/p" \ + sed -n -E "s/^[[:space:]]*[Aa][Rr][Gg][[:space:]]+${arg_name}=\"?([^\"[:space:]]+)\"?.*/\\1/p" \ "${DOCKERFILE}" | head -1 } @@ -384,6 +393,7 @@ build_base_image() { 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="" @@ -418,11 +428,16 @@ build_base_image() { 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="$(rocm_version_from_base_image "${base_image_arg}")" + 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)" @@ -431,6 +446,7 @@ build_base_image() { 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 @@ -471,7 +487,8 @@ build_base_image() { if [[ "${trusted_content_tag}" == "${scoped_content_tag}" ]]; then immutable_ref=$(find_matching_base_content_ref \ "${base_hash}" "${metadata_version}" \ - "${trusted_content_tag}" "${stable_tag}") || reuse_status=$? + "${trusted_content_tag}" "${preview_content_tag}" \ + "${stable_tag}") || reuse_status=$? else immutable_ref=$(find_matching_base_content_ref \ "${base_hash}" "${metadata_version}" \ @@ -490,8 +507,9 @@ build_base_image() { if [[ ${reuse_status} -eq 0 && -n "${immutable_ref}" ]]; then echo "Reusing ROCm base image with matching content: ${immutable_ref}" - if [[ "${immutable_ref%@*}" == "${stable_tag}" \ - && "${writable_content_tag}" != "${stable_tag}" ]]; then + 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}" @@ -565,6 +583,13 @@ build_base_image() { } 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 diff --git a/.buildkite/test-amd.yaml b/.buildkite/test-amd.yaml index 9561fec5e04e..326fa3a2cee2 100644 --- a/.buildkite/test-amd.yaml +++ b/.buildkite/test-amd.yaml @@ -114,6 +114,7 @@ steps: - label: ":computer: (CPU) Basic Models Other" # TBD timeout_in_minutes: 35 mirror_hardwares: [amdexperimental, amdproduction, amdgfx90anightly, amdmi250] + dind: false agent_pool: mi250_1 no_gpu: true optional: true @@ -124,12 +125,15 @@ steps: - tests/models/test_vision.py - tests/models/test_adapters.py - tests/models/transformers/fusers/ + - tests/model_executor/layers/test_activation.py + - tests/models/test_qwen3_5_mtp_config.py commands: - - pytest -v -s models/test_utils.py models/test_vision.py models/test_adapters.py models/transformers/fusers/ + - pytest -v -s models/test_utils.py models/test_vision.py models/test_adapters.py models/test_qwen3_5_mtp_config.py models/transformers/fusers/ model_executor/layers/test_activation.py - label: ":computer: (CPU) Multimodal Processor Shard %N" # TBD timeout_in_minutes: 90 mirror_hardwares: [amdexperimental, amdproduction, amdgfx90anightly, amdmi250] + dind: false agent_pool: mi250_1 no_gpu: true parallelism: 6 @@ -145,6 +149,7 @@ steps: - label: ":computer: (CPU) V1 Others" # TBD timeout_in_minutes: 45 mirror_hardwares: [amdexperimental, amdproduction, amdgfx90anightly, amdmi250] + dind: false agent_pool: mi250_1 no_gpu: true optional: true @@ -165,6 +170,7 @@ steps: - label: ":computer: (CPU) Async Engine, Inputs, Utils, Worker, Config" # TBD timeout_in_minutes: 80 mirror_hardwares: [amdexperimental, amdproduction, amdgfx90anightly, amdmi250] + dind: false agent_pool: mi250_1 no_gpu: true optional: true @@ -204,6 +210,7 @@ steps: - label: ":computer: (CPU) Rust Frontend Cargo Style + Clippy" # TBD timeout_in_minutes: 35 mirror_hardwares: [amdexperimental, amdproduction, amdgfx90anightly, amdmi250] + dind: false agent_pool: mi250_1 no_gpu: true optional: true @@ -214,11 +221,12 @@ steps: - .buildkite/test_areas/rust_frontend_cargo.yaml - .buildkite/scripts/run-rust-frontend-cargo-ci.sh commands: - - bash .buildkite/scripts/run-rust-frontend-cargo-ci.sh style-clippy + - .buildkite/scripts/run-rust-frontend-cargo-ci.sh style-clippy - label: ":computer: (CPU) Rust Frontend Cargo" # TBD timeout_in_minutes: 35 mirror_hardwares: [amdexperimental, amdproduction, amdgfx90anightly, amdmi250] + dind: false agent_pool: mi250_1 no_gpu: true optional: true @@ -229,11 +237,12 @@ steps: - .buildkite/test_areas/rust_frontend_cargo.yaml - .buildkite/scripts/run-rust-frontend-cargo-ci.sh commands: - - bash .buildkite/scripts/run-rust-frontend-cargo-ci.sh test + - .buildkite/scripts/run-rust-frontend-cargo-ci.sh test - label: ":computer: (CPU) Docker Build Metadata" # TBD timeout_in_minutes: 35 mirror_hardwares: [amdexperimental, amdproduction, amdgfx90anightly, amdmi250] + dind: false agent_pool: mi250_1 no_gpu: true optional: true @@ -264,6 +273,7 @@ steps: - label: ":amd: (MI250) PyTorch Fullgraph Smoke" # TBD timeout_in_minutes: 65 mirror_hardwares: [amdexperimental, amdproduction, amdgfx90anightly, amdmi250] + dind: false agent_pool: mi250_1 optional: true working_dir: "/vllm-workspace/tests" @@ -281,8 +291,9 @@ steps: #-------------------------------------------------------- mi250 · distributed --------------------------------------------------------# - label: ":amd: (MI250) Pipeline + Context Parallelism" # TBD - timeout_in_minutes: 80 + timeout_in_minutes: 90 mirror_hardwares: [amdexperimental, amdproduction, amdgfx90anightly, amdmi250] + dind: false agent_pool: mi250_4 num_gpus: 4 working_dir: "/vllm-workspace/tests" @@ -297,15 +308,20 @@ steps: - tests/distributed/ - vllm/_aiter_ops.py - vllm/platforms/rocm.py + - tests/v1/distributed/test_pp_dp_v2.py + optional: true commands: - pytest -v -s distributed/test_pp_cudagraph.py - pytest -v -s distributed/test_pipeline_parallel.py + - pytest -v -s v1/distributed/test_pp_dp_v2.py #---------------------------------------------------------- mi250 · kernels ----------------------------------------------------------# -- label: ":amd: (MI250) Helion Kernels" # TBD - timeout_in_minutes: 40 +- label: ":amd: (MI250) Helion Kernels Shard %N" + timeout_in_minutes: 45 + parallelism: 2 mirror_hardwares: [amdexperimental, amdproduction, amdgfx90anightly, amdmi250] + dind: false agent_pool: mi250_1 optional: true working_dir: "/vllm-workspace/tests" @@ -316,26 +332,46 @@ steps: - vllm/platforms/rocm.py commands: - pip install helion==1.4.0 - - pytest -v -s kernels/helion/ + - pytest -v -s kernels/helion/ --shard-id=$$BUILDKITE_PARALLEL_JOB --num-shards=$$BUILDKITE_PARALLEL_JOB_COUNT #----------------------------------------------------- mi250 · models / language -----------------------------------------------------# - label: ":amd: (MI250) Language Models (PPL)" # TBD - timeout_in_minutes: 45 + timeout_in_minutes: 65 mirror_hardwares: [amdexperimental, amdproduction, amdgfx90anightly, amdmi250] + dind: false agent_pool: mi250_1 working_dir: "/vllm-workspace/tests" source_file_dependencies: - vllm/ - tests/models/language/generation_ppl_test + - vllm/model_executor/models/qwen3_5.py + - vllm/model_executor/models/qwen3_5_mtp.py + - vllm/transformers_utils/configs/qwen3_5.py + - vllm/transformers_utils/configs/qwen3_5_moe.py + - vllm/model_executor/models/qwen2.py + - vllm/model_executor/models/qwen3.py + - vllm/model_executor/models/qwen3_next.py + - vllm/model_executor/models/qwen3_next_mtp.py + - vllm/model_executor/layers/fla/ops/ + - vllm/_aiter_ops.py + - vllm/v1/attention/backends/triton_attn.py + - vllm/v1/attention/backends/rocm_attn.py + - vllm/v1/attention/backends/rocm_aiter_unified_attn.py + - vllm/v1/attention/backends/rocm_aiter_fa.py + - vllm/v1/attention/backends/flex_attention.py + - vllm/v1/attention/ops/ + - vllm/platforms/rocm.py + optional: true commands: - pytest -v -s models/language/generation_ppl_test #---------------------------------------------------- mi250 · models / multimodal ----------------------------------------------------# - label: ":amd: (MI250) Multimodal Models (Standard) 3: llava + qwen2_vl" # TBD - timeout_in_minutes: 45 + timeout_in_minutes: 55 mirror_hardwares: [amdexperimental, amdproduction, amdgfx90anightly, amdmi250] + dind: false agent_pool: mi250_1 optional: true working_dir: "/vllm-workspace/tests" @@ -351,6 +387,7 @@ steps: - label: ":amd: (MI250) Batch Invariance" # TBD timeout_in_minutes: 35 mirror_hardwares: [amdexperimental, amdproduction, amdgfx90anightly, amdmi250] + dind: false agent_pool: mi250_1 optional: true working_dir: "/vllm-workspace/tests" @@ -367,8 +404,9 @@ steps: - pytest -v -s v1/determinism/test_rms_norm_batch_invariant.py - label: ":amd: (MI250) CUDAGraph" # TBD - timeout_in_minutes: 45 + timeout_in_minutes: 55 mirror_hardwares: [amdexperimental, amdproduction, amdgfx90anightly, amdmi250] + dind: false agent_pool: mi250_1 optional: true working_dir: "/vllm-workspace/tests" @@ -378,13 +416,18 @@ steps: - vllm/config/compilation.py - vllm/compilation - vllm/platforms/rocm.py + - vllm/v1/worker/encoder_cudagraph.py + - vllm/v1/worker/encoder_cudagraph_defs.py commands: - pytest -v -s v1/cudagraph/test_cudagraph_dispatch.py - pytest -v -s v1/cudagraph/test_cudagraph_mode.py + - pytest -v -s v1/cudagraph/test_breakable_cudagraph.py + - pytest -v -s v1/cudagraph/test_encoder_cudagraph.py - label: ":amd: (MI250) E2E Core" # TBD timeout_in_minutes: 50 mirror_hardwares: [amdexperimental, amdproduction, amdgfx90anightly, amdmi250] + dind: false agent_pool: mi250_1 optional: true working_dir: "/vllm-workspace/tests" @@ -393,11 +436,12 @@ steps: - tests/v1/e2e/general/ - vllm/platforms/rocm.py commands: - - pytest -v -s v1/e2e/general --ignore v1/e2e/general/test_async_scheduling.py + - pytest -v -s v1/e2e/general --ignore v1/e2e/general/test_async_scheduling.py --ignore v1/e2e/general/test_kv_sharing_fast_prefill.py -k "not test_mamba_prefix_cache_mrv2" - label: ":amd: (MI250) E2E Scheduling" # TBD timeout_in_minutes: 55 mirror_hardwares: [amdexperimental, amdproduction, amdgfx90anightly, amdmi250] + dind: false agent_pool: mi250_1 optional: true working_dir: "/vllm-workspace/tests" @@ -411,6 +455,7 @@ steps: - label: ":amd: (MI250) V1 Engine" # TBD timeout_in_minutes: 45 mirror_hardwares: [amdexperimental, amdproduction, amdgfx90anightly, amdmi250] + dind: false agent_pool: mi250_1 optional: true working_dir: "/vllm-workspace/tests" @@ -423,6 +468,7 @@ steps: - vllm/engine/arg_utils.py - vllm/transformers_utils/config.py - vllm/platforms/rocm.py + - vllm/v1/engine/ commands: - pytest -v -s v1/engine/test_preprocess_error_handling.py - pytest -v -s v1/engine --ignore v1/engine/test_preprocess_error_handling.py @@ -432,6 +478,7 @@ steps: - label: ":amd: (MI250) Spec Decode Draft Model" # TBD timeout_in_minutes: 60 mirror_hardwares: [amdexperimental, amdproduction, amdgfx90anightly, amdmi250] + dind: false agent_pool: mi250_1 optional: true working_dir: "/vllm-workspace/tests" @@ -447,8 +494,9 @@ steps: - pytest -v -s v1/e2e/spec_decode/draft_model/ - label: ":amd: (MI250) Spec Decode Speculators + MTP" # TBD - timeout_in_minutes: 65 + timeout_in_minutes: 75 mirror_hardwares: [amdexperimental, amdproduction, amdgfx90anightly, amdmi250] + dind: false agent_pool: mi250_1 optional: true working_dir: "/vllm-workspace/tests" @@ -461,6 +509,7 @@ steps: - vllm/transformers_utils/configs/speculators/ - tests/v1/e2e/spec_decode/ - vllm/platforms/rocm.py + - vllm/v1/attention/backends/ commands: - pytest -v -s v1/e2e/spec_decode/speculators/ - pytest -v -s v1/e2e/spec_decode/mtp/ @@ -468,7 +517,7 @@ steps: #------------------------------------------------------------- mi250 · misc ------------------------------------------------------------# - label: ":amd: (MI300) Python-only Installation" # TBD - timeout_in_minutes: 25 + timeout_in_minutes: 55 mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300] dind: false agent_pool: mi300_1 @@ -501,14 +550,16 @@ steps: - tests/basic_correctness/test_basic_correctness - tests/basic_correctness/test_cpu_offload - tests/basic_correctness/test_mem.py + optional: true commands: - export VLLM_WORKER_MULTIPROC_METHOD=spawn - pytest -v -s basic_correctness/test_mem.py - VLLM_TARGET_TEST_SUITE=MI300 pytest -v -s basic_correctness/test_basic_correctness.py - pytest -v -s basic_correctness/test_cpu_offload.py -- label: ":amd: (MI300) Distributed Models" # TBD +- label: ":amd: (MI300) Distributed Models Shard %N" timeout_in_minutes: 80 + parallelism: 3 mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300] dind: false agent_pool: mi300_2 @@ -516,6 +567,11 @@ steps: optional: true working_dir: "/vllm-workspace/tests" source_file_dependencies: + - vllm/models/hy_v4/ + - vllm/config/ + - vllm/v1/worker/gpu/ + - vllm/v1/attention/ops/ + - vllm/v1/attention/backend.py - vllm/model_executor/model_loader/sharded_state_loader.py - vllm/model_executor/models/ - vllm/model_executor/layers/ @@ -526,20 +582,17 @@ steps: - tests/basic_correctness/ - tests/model_executor/model_loader/test_sharded_state_loader.py - tests/models/ + - vllm/v1/worker/ + - tests/conftest.py + - tests/utils.py + - tests/models/test_vision.py commands: - - TARGET_TEST_SUITE=MI300 pytest basic_correctness/ -v -s -m 'distributed(num_gpus=2)' - - HIP_VISIBLE_DEVICES=0,1 pytest -v -s model_executor/model_loader/test_sharded_state_loader.py -m '(not slow_test)' - - pytest models/transformers/test_backend.py -v -s -m 'distributed(num_gpus=2)' - - pytest models/language -v -s -m 'distributed(num_gpus=2)' - - pytest models/multimodal -v -s -m 'distributed(num_gpus=2)' --ignore models/multimodal/generation/test_whisper.py --ignore models/multimodal/generation/test_phi4siglip.py - - pytest models/multimodal/generation/test_phi4siglip.py -v -s -m 'distributed(num_gpus=2)' - - VLLM_WORKER_MULTIPROC_METHOD=spawn pytest models/multimodal/generation/test_whisper.py -v -s -m 'distributed(num_gpus=2)' - - pytest -v -s models/test_vision.py -m 'distributed(num_gpus=2)' + - case "$$BUILDKITE_PARALLEL_JOB" in 0) TARGET_TEST_SUITE=MI300 pytest basic_correctness/ -v -s -m 'distributed(num_gpus=2)' && HIP_VISIBLE_DEVICES=0,1 pytest -v -s model_executor/model_loader/test_sharded_state_loader.py -m '(not slow_test)' ;; 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)' ;; 2) pytest models/multimodal -v -s -m 'distributed(num_gpus=2)' --ignore models/multimodal/generation/test_whisper.py --ignore models/multimodal/generation/test_phi4siglip.py && pytest models/multimodal/generation/test_phi4siglip.py -v -s -m 'distributed(num_gpus=2)' && VLLM_WORKER_MULTIPROC_METHOD=spawn pytest models/multimodal/generation/test_whisper.py -v -s -m 'distributed(num_gpus=2)' && pytest -v -s models/test_vision.py -m 'distributed(num_gpus=2)' ;; *) exit 2 ;; esac #-------------------------------------------------------- mi300 · benchmarks ---------------------------------------------------------# - label: ":amd: (MI300) Benchmarks CLI" # TBD - timeout_in_minutes: 35 + timeout_in_minutes: 40 mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300] dind: false agent_pool: mi300_1 @@ -548,13 +601,14 @@ steps: source_file_dependencies: - vllm/ - tests/benchmarks/ + - rust/src/bench/tests/python_serve_flags.txt commands: - pytest -v -s benchmarks/ #---------------------------------------------------------- mi300 · compile ----------------------------------------------------------# - label: ":amd: (MI300) PyTorch Compilation" # TBD - timeout_in_minutes: 45 + timeout_in_minutes: 90 mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300] dind: false agent_pool: mi300_1 @@ -570,6 +624,29 @@ steps: - csrc/ - tests/compile - vllm/platforms/rocm.py + - vllm/__init__.py + - vllm/_aiter_ops.py + - vllm/_custom_ops.py + - vllm/config/ + - vllm/distributed/ + - vllm/engine/ + - vllm/env_override.py + - vllm/envs.py + - vllm/forward_context.py + - vllm/inputs/ + - vllm/ir/ + - vllm/kernels/ + - vllm/logger.py + - vllm/model_executor/ + - vllm/multimodal/ + - vllm/platforms/ + - vllm/plugins/ + - vllm/sampling_params.py + - vllm/sequence.py + - vllm/transformers_utils/ + - vllm/triton_utils/ + - vllm/utils/ + - vllm/v1/ commands: - "find compile/ -maxdepth 1 -name 'test_*.py' -print0 | xargs -0 -n1 -I{} pytest -s -v '{}'" @@ -590,6 +667,7 @@ steps: - tests/compile/fusions_e2e/ - vllm/_aiter_ops.py - vllm/platforms/rocm.py + optional: true commands: - rocm-smi - pytest -v -s tests/compile/fusions_e2e/test_tp1_quant.py -k "llama-3" @@ -609,6 +687,7 @@ steps: - tests/compile/fusions_e2e/ - vllm/_aiter_ops.py - vllm/platforms/rocm.py + optional: true commands: - rocm-smi # Run all models and attn backends but only Inductor partition and native custom ops @@ -625,11 +704,37 @@ steps: source_file_dependencies: - vllm/ - tests/compile/passes + - vllm/platforms/rocm.py + - vllm/__init__.py + - vllm/_aiter_ops.py + - vllm/_custom_ops.py + - vllm/compilation/ + - vllm/config/ + - vllm/distributed/ + - vllm/engine/ + - vllm/env_override.py + - vllm/envs.py + - vllm/forward_context.py + - vllm/inputs/ + - vllm/ir/ + - vllm/kernels/ + - vllm/logger.py + - vllm/model_executor/ + - vllm/multimodal/ + - vllm/platforms/ + - vllm/plugins/ + - vllm/sampling_params.py + - vllm/sequence.py + - vllm/transformers_utils/ + - vllm/triton_utils/ + - vllm/utils/ + - vllm/v1/ + optional: true commands: - pytest -s -v compile/passes --ignore compile/passes/distributed - label: ":amd: (MI300) PyTorch Fullgraph" # TBD - timeout_in_minutes: 35 + timeout_in_minutes: 90 mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300] dind: false agent_pool: mi300_1 @@ -643,11 +748,33 @@ steps: - csrc/ - tests/compile - vllm/platforms/rocm.py + - vllm/__init__.py + - vllm/_aiter_ops.py + - vllm/_custom_ops.py + - vllm/config/ + - vllm/distributed/ + - vllm/engine/ + - vllm/env_override.py + - vllm/envs.py + - vllm/forward_context.py + - vllm/inputs/ + - vllm/ir/ + - vllm/kernels/ + - vllm/logger.py + - vllm/multimodal/ + - vllm/platforms/ + - vllm/plugins/ + - vllm/sampling_params.py + - vllm/sequence.py + - vllm/transformers_utils/ + - vllm/triton_utils/ + - vllm/utils/ + - vllm/v1/ commands: - - pytest -v -s compile/fullgraph/test_full_graph.py + - find compile/fullgraph/ -name 'test_*.py' -not -name 'test_full_cudagraph.py' -print0 | xargs -0 -n1 -I{} pytest -s -v '{}' - label: ":amd: (MI300) PyTorch Nightly Dependency Override Check" # TBD - timeout_in_minutes: 25 + timeout_in_minutes: 30 mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300] dind: false agent_pool: mi300_1 @@ -698,7 +825,7 @@ steps: - pytest -v -s tests/compile/fusions_e2e/test_tp2_ar_rms.py::test_tp2_ar_rms_fusions -k "dynamo_partition" - label: ":amd: (MI300) Distributed Compile + RPC" # TBD - timeout_in_minutes: 45 + timeout_in_minutes: 65 mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300] dind: false agent_pool: mi300_2 @@ -716,9 +843,9 @@ steps: - tests/compile/test_wrapper.py - tests/entrypoints/llm/test_collective_rpc.py - vllm/platforms/rocm.py + optional: true commands: - pytest -v -s entrypoints/llm/test_collective_rpc.py - - pytest -v -s ./compile/fullgraph/test_basic_correctness.py - pytest -v -s ./compile/test_wrapper.py #----------------------------------------------------------- mi300 · cuda ------------------------------------------------------------# @@ -732,9 +859,16 @@ steps: source_file_dependencies: - vllm/ - tests/cuda + - vllm/envs.py + - vllm/logger.py + - vllm/platforms/ + - vllm/plugins/ + - vllm/utils/ + optional: true commands: - pytest -v -s cuda/test_cuda_context.py - pytest -v -s cuda/test_platform_no_cuda_init.py + - pytest -v -s cuda/test_cuda_compatibility_path.py #-------------------------------------------------------- mi300 · detokenizer --------------------------------------------------------# @@ -750,6 +884,19 @@ steps: - tests/detokenizer - tests/multimodal - tests/utils_ + - vllm/assets/ + - vllm/config/ + - vllm/distributed/ + - vllm/engine/ + - vllm/inputs/ + - vllm/model_executor/ + - vllm/multimodal/ + - vllm/platforms/ + - vllm/sampling_params.py + - vllm/tokenizers/ + - vllm/transformers_utils/ + - vllm/utils/ + - vllm/v1/ commands: - pytest -v -s detokenizer - pytest -v -s -m 'not cpu_test' multimodal @@ -776,7 +923,7 @@ steps: - pytest -v -s distributed/test_shm_storage.py - label: ":amd: (MI300) EPLB Algorithm" # TBD - timeout_in_minutes: 25 + timeout_in_minutes: 30 mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300] dind: false agent_pool: mi300_1 @@ -808,7 +955,7 @@ steps: - pytest -v -s distributed/test_eplb_spec_decode.py - label: ":amd: (MI300) Distributed Features" # TBD - timeout_in_minutes: 45 + timeout_in_minutes: 90 mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300] dind: false agent_pool: mi300_2 @@ -825,6 +972,26 @@ steps: - examples/features/data_parallel/data_parallel_offline.py - vllm/_aiter_ops.py - vllm/platforms/rocm.py + - csrc/custom_quickreduce.cu + - csrc/ops.h + - csrc/torch_bindings.cpp + - vllm/model_executor/layers/ + - vllm/entrypoints/llm.py + - vllm/config/parallel.py + - vllm/v1/engine/ + - vllm/v1/executor/ + - vllm/v1/worker/ + - vllm/_custom_ops.py + - vllm/envs.py + - tests/distributed/test_rocm_aiter_custom_ar.py + - tests/distributed/test_rocm_quick_reduce.py + - tests/distributed/test_quick_all_reduce.py + - tests/v1/e2e/general/test_rocm_aiter_custom_ar.py + - tests/utils.py + - examples/rl/rlhf_async_new_apis.py + - tests/distributed/test_weight_transfer.py + - tests/distributed/test_packed_tensor.py + optional: true commands: - pytest -v -s tests/distributed/test_context_parallel.py - VLLM_ALLOW_INSECURE_SERIALIZATION=1 python3 examples/rl/rlhf_async_new_apis.py @@ -833,6 +1000,10 @@ steps: - pytest -v -s tests/v1/distributed/test_dbo.py - VLLM_ALLOW_INSECURE_SERIALIZATION=1 pytest -v -s tests/distributed/test_weight_transfer.py - pytest -v -s tests/distributed/test_packed_tensor.py + - pytest -v -s tests/distributed/test_rocm_aiter_custom_ar.py + - pytest -v -s tests/v1/e2e/general/test_rocm_aiter_custom_ar.py + - pytest -v -s tests/distributed/test_rocm_quick_reduce.py + - pytest -v -s tests/distributed/test_quick_all_reduce.py - label: ":amd: (MI300) Distributed" # TBD timeout_in_minutes: 40 @@ -987,7 +1158,7 @@ steps: #---------------------------------------------------------- mi300 · engine -----------------------------------------------------------# - label: ":amd: (MI300) Engine Core" # TBD - timeout_in_minutes: 35 + timeout_in_minutes: 40 mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300] dind: false agent_pool: mi300_1 @@ -1002,6 +1173,17 @@ steps: - tests/test_vllm_port - tests/jit_monitor/test_hooks.py - tests/jit_monitor/test_hooks_gpu.py + - vllm/compilation/ + - vllm/config/ + - vllm/engine/ + - vllm/entrypoints/logger.py + - vllm/envs.py + - vllm/logger.py + - vllm/logging_utils/ + - vllm/platforms/ + - vllm/sequence.py + - vllm/triton_utils/ + - vllm/utils/ commands: - pytest -v -s engine test_sequence.py test_config.py test_logger.py test_vllm_port.py jit_monitor/test_hooks.py jit_monitor/test_hooks_gpu.py @@ -1043,8 +1225,9 @@ steps: - pytest -v -s entrypoints/llm/test_generate.py # it needs a clean process - pytest -v -s entrypoints/llm/offline_mode # Needs to avoid interference with other tests -- label: ":amd: (MI300) Entrypoints Integration (API Server)" # TBD +- label: ":amd: (MI300) Entrypoints Integration (API Server) %N" timeout_in_minutes: 65 + parallelism: 4 mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300] dind: false agent_pool: mi300_1 @@ -1057,12 +1240,12 @@ steps: - tests/entrypoints/scale_out commands: - export VLLM_WORKER_MULTIPROC_METHOD=spawn - - pytest -v -s entrypoints/serve --ignore=entrypoints/serve/dev/rpc - - PYTHONPATH=/vllm-workspace pytest -v -s entrypoints/serve/dev/rpc - - pytest -v -s entrypoints/scale_out + - pytest -v -s entrypoints/serve --ignore=entrypoints/serve/dev/rpc --num-shards=$$BUILDKITE_PARALLEL_JOB_COUNT --shard-id=$$BUILDKITE_PARALLEL_JOB + - if [ "$$BUILDKITE_PARALLEL_JOB" = "1" ]; then PYTHONPATH=/vllm-workspace pytest -v -s entrypoints/serve/dev/rpc; fi + - pytest -v -s entrypoints/scale_out --num-shards=$$BUILDKITE_PARALLEL_JOB_COUNT --shard-id=$$BUILDKITE_PARALLEL_JOB -- label: ":amd: (MI300) Entrypoints Integration (API Server OpenAI - Part 1)" # TBD - timeout_in_minutes: 40 +- label: ":amd: (MI300) Entrypoints Integration (OpenAI API completion)" # TBD + timeout_in_minutes: 65 mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300] dind: false agent_pool: mi300_1 @@ -1074,10 +1257,11 @@ steps: - tests/entrypoints/openai commands: - export VLLM_WORKER_MULTIPROC_METHOD=spawn - - pytest -v -s entrypoints/openai/ --ignore=entrypoints/openai/completion --ignore=entrypoints/openai/chat_completion --ignore=entrypoints/openai/responses --ignore=entrypoints/openai/correctness + - pytest -v -s entrypoints/openai/completion --ignore=entrypoints/openai/completion/test_tensorizer_entrypoint.py + - pytest -v -s entrypoints/openai --ignore=entrypoints/openai/completion --ignore=entrypoints/openai/chat_completion --ignore=entrypoints/openai/responses --ignore=entrypoints/openai/correctness -- label: ":amd: (MI300) Entrypoints Integration (API Server OpenAI - Part 2)" # TBD - timeout_in_minutes: 55 +- label: ":amd: (MI300) Entrypoints Integration (OpenAI API chat_completion)" # TBD + timeout_in_minutes: 70 mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300] dind: false agent_pool: mi300_1 @@ -1090,7 +1274,6 @@ steps: commands: - export VLLM_WORKER_MULTIPROC_METHOD=spawn - pytest -v -s entrypoints/openai/chat_completion - - pytest -v -s entrypoints/openai/completion --ignore=entrypoints/openai/completion/test_tensorizer_entrypoint.py - label: ":amd: (MI300) Entrypoints Integration (API Server Generate)" # TBD timeout_in_minutes: 65 @@ -1108,7 +1291,6 @@ steps: - tests/entrypoints/cohere - tests/entrypoints/generate commands: - - export VLLM_WORKER_MULTIPROC_METHOD=spawn - pytest -v -s tool_use - pytest -v -s entrypoints/tool_parsers - pytest -v -s entrypoints/generate @@ -1116,7 +1298,7 @@ steps: - pytest -v -s entrypoints/cohere - label: ":amd: (MI300) Entrypoints Integration (Responses API)" # TBD - timeout_in_minutes: 45 + timeout_in_minutes: 50 mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300] dind: false agent_pool: mi300_1 @@ -1127,7 +1309,6 @@ steps: - vllm/ - tests/entrypoints/openai/responses commands: - - export VLLM_WORKER_MULTIPROC_METHOD=spawn - pytest -v -s entrypoints/openai/responses - label: ":amd: (MI300) Entrypoints Integration (Speech to Text)" # TBD @@ -1140,6 +1321,7 @@ steps: source_file_dependencies: - vllm/ - tests/entrypoints/speech_to_text + optional: true commands: - export VLLM_WORKER_MULTIPROC_METHOD=spawn - pytest -v -s entrypoints/speech_to_text @@ -1159,23 +1341,8 @@ steps: - export VLLM_WORKER_MULTIPROC_METHOD=spawn - pytest -v -s entrypoints/multimodal -- label: ":amd: (MI300) Entrypoints Integration (Pooling)" # TBD - timeout_in_minutes: 65 - mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300] - dind: false - agent_pool: mi300_1 - optional: true - fast_check: true - working_dir: "/vllm-workspace/tests" - source_file_dependencies: - - vllm/ - - tests/entrypoints/pooling - commands: - - export VLLM_WORKER_MULTIPROC_METHOD=spawn - - pytest -v -s entrypoints/pooling - - label: ":amd: (MI300) OpenAI API Correctness" # TBD - timeout_in_minutes: 25 + timeout_in_minutes: 30 mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300] dind: false agent_pool: mi300_1 @@ -1194,6 +1361,23 @@ steps: - bash ../tools/install_torchcodec_rocm.sh || exit 1 - pytest -s entrypoints/openai/correctness/ +- label: ":amd: (MI300) Scale-out EC Connector E2E" + timeout_in_minutes: 60 + 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/ec_transfer/ + - vllm/entrypoints/scale_out/ + - tests/entrypoints/scale_out/ec_integration/ + - vllm/platforms/rocm.py + commands: + - export VLLM_WORKER_MULTIPROC_METHOD=spawn + - bash entrypoints/scale_out/ec_integration/run_scale_out_ec_e2e_test.sh + #----------------------------------------------------------- mi300 · evals -----------------------------------------------------------# - label: ":amd: (MI300) DeepSeek V2-Lite Prefetch Offload Accuracy" # TBD @@ -1214,11 +1398,12 @@ steps: - .buildkite/scripts/scheduled_integration_test/ - vllm/_aiter_ops.py - vllm/platforms/rocm.py + optional: true commands: - bash .buildkite/scripts/scheduled_integration_test/deepseek_v2_lite_prefetch_offload.sh 0.25 200 8030 - label: ":amd: (MI300) LM Eval Small Models" # TBD - timeout_in_minutes: 40 + timeout_in_minutes: 45 mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300] dind: false agent_pool: mi300_1 @@ -1232,11 +1417,12 @@ steps: - vllm/v1/attention/selector.py - vllm/_aiter_ops.py - vllm/platforms/rocm.py + optional: true commands: - pytest -s -v evals/gsm8k/test_gsm8k_correctness.py --config-list-file=configs/models-small.txt - label: ":amd: (MI300) MRCR Eval Small Models" # TBD - timeout_in_minutes: 35 + timeout_in_minutes: 40 mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300] dind: false agent_pool: mi300_1 @@ -1269,11 +1455,12 @@ steps: - vllm/v1/attention/selector.py - vllm/_aiter_ops.py - vllm/platforms/rocm.py + - .buildkite/lm-eval-harness/ commands: - pytest -s -v test_lm_eval_correctness.py --config-list-file=configs/models-small-rocm.txt - label: ":amd: (MI300) Multimodal Accuracy Eval (Small Models)" # TBD - timeout_in_minutes: 30 + timeout_in_minutes: 35 mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300] dind: false agent_pool: mi300_1 @@ -1289,7 +1476,7 @@ steps: - pytest -s -v test_lm_eval_correctness.py --config-list-file=configs/models-mm-small.txt --tp-size=1 - label: ":amd: (MI300) GPQA Eval (GPT-OSS)" # TBD - timeout_in_minutes: 30 + timeout_in_minutes: 40 mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300] dind: false agent_pool: mi300_2 @@ -1311,7 +1498,7 @@ steps: - uv pip install --system 'gpt-oss[eval]==0.0.5' - pytest -s -v evals/gpt_oss/test_gpqa_correctness.py --config-list-file=configs/models-gfx942.txt -- label: ":amd: (MI300) LM Eval Small Models FP8 + Mixed" # TBD +- label: ":amd: (MI300) LM Eval Small Models Distributed" timeout_in_minutes: 55 mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300] dind: false @@ -1327,6 +1514,8 @@ steps: - vllm/v1/attention/selector.py - vllm/_aiter_ops.py - vllm/platforms/rocm.py + - tests/evals/gsm8k/configs/models-mi3xx-fp8-and-mixed.txt + optional: true commands: - pytest -s -v evals/gsm8k/test_gsm8k_correctness.py --config-list-file=configs/models-mi3xx-fp8-and-mixed.txt @@ -1511,7 +1700,7 @@ steps: #--------------------------------------------------------- mi300 · examples ----------------------------------------------------------# - label: ":amd: (MI300) Examples" # TBD - timeout_in_minutes: 50 + timeout_in_minutes: 75 mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300] dind: false agent_pool: mi300_1 @@ -1523,27 +1712,27 @@ steps: - examples/ - vllm/platforms/rocm.py commands: - - pip install tensorizer # Basic - - python3 basic/offline_inference/chat.py - - python3 basic/offline_inference/generate.py --model facebook/opt-125m - - python3 basic/offline_inference/generate.py --model meta-llama/Llama-2-13b-chat-hf --cpu-offload-gb 10 - - python3 basic/offline_inference/classify.py - - python3 basic/offline_inference/embed.py - - python3 basic/offline_inference/score.py # Multi-modal models - - python3 generate/multimodal/audio_language_offline.py --seed 0 - - python3 generate/multimodal/vision_language_offline.py --seed 0 - - python3 generate/multimodal/vision_language_multi_image_offline.py --seed 0 - - python3 generate/multimodal/encoder_decoder_multimodal_offline.py --model-type whisper --seed 0 # Pooling models - - python3 pooling/embed/vision_embedding_offline.py --seed 0 # Features demo - - python3 features/automatic_prefix_caching/prefix_caching_offline.py - - python3 deployment/llm_engine_example.py - - python3 features/tensorize_vllm_model.py --model facebook/opt-125m serialize --serialized-directory /tmp/ --suffix v1 && python3 features/tensorize_vllm_model.py --model facebook/opt-125m deserialize --path-to-tensors /tmp/vllm/facebook/opt-125m/v1/model.tensors - - python3 features/speculative_decoding/spec_decode_offline.py --test --method eagle --num_spec_tokens 3 --dataset-name hf --dataset-path philschmid/mt-bench --num-prompts 80 --temp 0 --top-p 1.0 --top-k -1 --tp 1 --enable-chunked-prefill --max-model-len 2048 - - python3 features/speculative_decoding/spec_decode_offline.py --test --method eagle3 --num_spec_tokens 3 --dataset-name hf --dataset-path philschmid/mt-bench --num-prompts 80 --temp 0 --top-p 1.0 --top-k -1 --tp 1 --enable-chunked-prefill --max-model-len 1536 + - pip install --no-deps tensorizer + - python3 basic/offline_inference/chat.py + - python3 basic/offline_inference/generate.py --model facebook/opt-125m + - python3 basic/offline_inference/generate.py --model meta-llama/Llama-2-13b-chat-hf --cpu-offload-gb 10 + - python3 basic/offline_inference/classify.py + - python3 basic/offline_inference/embed.py + - python3 basic/offline_inference/score.py + - python3 generate/multimodal/audio_language_offline.py --seed 0 + - python3 generate/multimodal/vision_language_offline.py --seed 0 + - python3 generate/multimodal/vision_language_multi_image_offline.py --seed 0 + - python3 generate/multimodal/encoder_decoder_multimodal_offline.py --model-type whisper --seed 0 + - python3 pooling/embed/vision_embedding_offline.py --seed 0 + - python3 features/automatic_prefix_caching/prefix_caching_offline.py + - python3 deployment/llm_engine_example.py + - python3 features/tensorize_vllm_model.py --model facebook/opt-125m serialize --serialized-directory /tmp/ --suffix v1 && python3 features/tensorize_vllm_model.py --model facebook/opt-125m deserialize --path-to-tensors /tmp/vllm/facebook/opt-125m/v1/model.tensors + - python3 features/speculative_decoding/spec_decode_offline.py --test --method eagle --num_spec_tokens 3 --dataset-name hf --dataset-path philschmid/mt-bench --num-prompts 80 --temp 0 --top-p 1.0 --top-k -1 --tp 1 --enable-chunked-prefill --max-model-len 2048 + - python3 features/speculative_decoding/spec_decode_offline.py --test --method eagle3 --num_spec_tokens 3 --dataset-name hf --dataset-path philschmid/mt-bench --num-prompts 80 --temp 0 --top-p 1.0 --top-k -1 --tp 1 --enable-chunked-prefill --max-model-len 1536 #---------------------------------------------------------- mi300 · kernels ----------------------------------------------------------# @@ -1559,34 +1748,14 @@ steps: - vllm/kernels - vllm/_aiter_ops.py - vllm/platforms/rocm.py + - tests/ir/ + - tests/kernels/ir/ commands: - pytest -v -s tests/ir - pytest -v -s tests/kernels/ir -- label: ":amd: (MI300) MLA Kernels" # TBD - timeout_in_minutes: 25 - mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300] - dind: false - agent_pool: mi300_1 - working_dir: "/vllm-workspace/" - source_file_dependencies: - - vllm/v1/attention/backends/mla/rocm_aiter_mla.py - - vllm/v1/attention/selector.py - - vllm/platforms/rocm.py - - vllm/_aiter_ops.py - - tests/kernels/attention/test_rocm_aiter_mla_decode.py - - tests/kernels/attention/test_rocm_aiter_mla_decode_metadata.py - - tests/kernels/attention/test_rocm_aiter_mla_fp8_support.py - - tests/kernels/attention/test_rocm_aiter_mla_op_registration.py - commands: - - rocm-smi - - pytest -v -s tests/kernels/attention/test_rocm_aiter_mla_decode_metadata.py - - pytest -v -s tests/kernels/attention/test_rocm_aiter_mla_decode.py - - pytest -v -s tests/kernels/attention/test_rocm_aiter_mla_fp8_support.py - - pytest -v -s tests/kernels/attention/test_rocm_aiter_mla_op_registration.py - - label: ":amd: (MI300) Attention Kernels Shard %N" # TBD - timeout_in_minutes: 70 + timeout_in_minutes: 90 mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300] dind: false agent_pool: mi300_1 @@ -1601,26 +1770,9 @@ steps: - vllm/_aiter_ops.py - vllm/envs.py - vllm/platforms/rocm.py + - vllm/utils/flashinfer.py commands: - - pytest -v -s kernels/attention --shard-id=$$BUILDKITE_PARALLEL_JOB --num-shards=$$BUILDKITE_PARALLEL_JOB_COUNT - -- label: ":amd: (MI300) Core Operation Kernels Shard %N" # TBD - timeout_in_minutes: 40 - mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300] - dind: false - agent_pool: mi300_1 - parallelism: 3 - working_dir: "/vllm-workspace/tests" - source_file_dependencies: - - csrc/ - - tests/kernels/core - - tests/kernels/test_top_k_per_row.py - - tests/kernels/test_concat_mla_q.py - - vllm/model_executor/layers/rotary_embedding/ - - vllm/_aiter_ops.py - - vllm/platforms/rocm.py - commands: - - pytest -v -s kernels/core --ignore=kernels/core/test_minimax_reduce_rms.py kernels/test_concat_mla_q.py kernels/test_top_k_per_row.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 --shard-id=$$BUILDKITE_PARALLEL_JOB --num-shards=$$BUILDKITE_PARALLEL_JOB_COUNT - label: ":amd: (MI300) MiniMax Reduce RMS Kernels" # TBD timeout_in_minutes: 45 @@ -1637,6 +1789,12 @@ steps: - vllm/platforms/rocm.py - tests/kernels/core/test_minimax_reduce_rms.py - tests/utils.py + - csrc/libtorch_stable/minimax_reduce_rms_kernel.cu + - csrc/libtorch_stable/minimax_reduce_rms_kernel.h + - csrc/libtorch_stable/ops.h + - csrc/libtorch_stable/torch_bindings.cpp + - vllm/model_executor/layers/mamba/linear/minimax_linear_attn.py + optional: true commands: - pytest -v -s kernels/core/test_minimax_reduce_rms.py @@ -1660,8 +1818,50 @@ steps: commands: - pytest -v -s models/kimi_k3/test_kda.py models/kimi_k3/test_kda_metadata.py +- label: ":amd: (MI355) Kimi K3" # TBD + timeout_in_minutes: 60 + mirror_hardwares: [amdexperimental, amdproduction, amdgfx950nightly, amdmi355] + dind: false + agent_pool: mi355_1 + num_gpus: 1 + optional: true + working_dir: "/vllm-workspace/tests" + source_file_dependencies: + - vllm/models/kimi_k3/ + - csrc/libtorch_stable/kimi_k3/ + - tests/models/kimi_k3/ + - vllm/platforms/rocm.py + commands: + # Mirrors the ":amd: (MI355) Kimi K3" step generated from + # .buildkite/test_areas/models_basic.yaml. The ROCm KDA/AttnRes kernels are + # gated on gfx950, so this has to be MI355. The two kernels/ files the NVIDIA + # step runs are CUDA-only (they import cuda.bindings.driver), so they are + # deliberately left out here. + - pytest -v -s models/kimi_k3 + +- label: ":amd: (MI355) Kimi K3 Multi-GPU" # TBD + 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/models/kimi_k3/ + - tests/models/kimi_k3/ + - vllm/platforms/rocm.py + commands: + # The single-GPU step above skips every @multi_gpu_test case. `-m "distributed"` + # selects exactly those: multi_gpu_test() applies pytest.mark.distributed + # alongside its skipif (tests/utils.py:2080), so this picks up a new + # multi-GPU test anywhere in the directory without editing this list. + # Four GPUs run the two tp4 cases in test_amd_latent_moe_runner.py; the tp8 + # case and the 8/16-GPU cases in test_latent_moe_tail.py still skip. + - pytest -v -s -m "distributed" models/kimi_k3 + - label: ":amd: (MI300) Mamba Kernels" # TBD - timeout_in_minutes: 50 + timeout_in_minutes: 55 mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300] dind: false agent_pool: mi300_1 @@ -1670,13 +1870,15 @@ steps: source_file_dependencies: - csrc/mamba/ - tests/kernels/mamba + - vllm/model_executor/layers/mamba/gdn - vllm/model_executor/layers/mamba/ops + - vllm/third_party/flash_linear_attention/ - vllm/platforms/rocm.py commands: - pytest -v -s kernels/mamba - label: ":amd: (MI300) MoE Kernels Shard %N" # TBD - timeout_in_minutes: 50 + timeout_in_minutes: 55 mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300] dind: false agent_pool: mi300_1 @@ -1698,11 +1900,13 @@ steps: - vllm/_aiter_ops.py - vllm/platforms/rocm.py 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 --ignore=kernels/moe/test_modular_oai_triton_moe.py --ignore=kernels/moe/test_modular_kernel_combinations.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 + - VLLM_ROCM_USE_AITER=1 VLLM_ROCM_USE_AITER_MOE=1 pytest -v -s kernels/moe/test_modular_kernel_combinations.py --shard-id=$$BUILDKITE_PARALLEL_JOB --num-shards=$$BUILDKITE_PARALLEL_JOB_COUNT -- label: ":amd: (MI300) Quantization Kernels" # TBD - timeout_in_minutes: 55 +- label: ":amd: (MI300) Quantization Kernels Shard %N" + timeout_in_minutes: 120 + parallelism: 6 mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300] dind: false agent_pool: mi300_1 @@ -1722,8 +1926,10 @@ steps: - vllm/platforms/rocm.py - vllm/model_executor/kernels/ - vllm/v1/attention/backends/rocm_aiter_fa.py + - vllm/config/ + - tests/kernels/quantization/test_rocm_skinny_gemms.py commands: - - pytest -v -s kernels/quantization + - pytest -v -s kernels/quantization -k "not test_nvfp4_dense_emulation" --shard-id=$$BUILDKITE_PARALLEL_JOB --num-shards=$$BUILDKITE_PARALLEL_JOB_COUNT - label: ":amd: (MI300) DeepEP FP8 MoE Kernels" # TBD timeout_in_minutes: 55 @@ -1747,7 +1953,7 @@ steps: #----------------------------------------------------------- mi300 · lora ------------------------------------------------------------# - label: ":amd: (MI300) LoRA Shard %N" # TBD - timeout_in_minutes: 40 + timeout_in_minutes: 85 mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300] dind: false agent_pool: mi300_1 @@ -1761,8 +1967,9 @@ steps: commands: - pytest -v -s lora --shard-id=$$BUILDKITE_PARALLEL_JOB --num-shards=$$BUILDKITE_PARALLEL_JOB_COUNT --ignore=lora/test_chatglm3_tp.py --ignore=lora/test_llama_tp.py --ignore=lora/test_qwen3_with_multi_loras.py --ignore=lora/test_olmoe_tp.py --ignore=lora/test_deepseekv2_tp.py --ignore=lora/test_gptoss_tp.py --ignore=lora/test_qwen3moe_tp.py --ignore=lora/test_qwen35_densemodel_lora.py -- label: ":amd: (MI300) LoRA TP (Distributed)" # TBD +- label: ":amd: (MI300) LoRA TP (Distributed) %N" timeout_in_minutes: 65 + parallelism: 4 mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300] dind: false agent_pool: mi300_4 @@ -1773,19 +1980,22 @@ steps: - vllm/model_executor/layers/fused_moe/ - tests/lora - vllm/platforms/rocm.py + optional: true commands: - - pytest -v -s -x lora/test_chatglm3_tp.py - - pytest -v -s -x lora/test_llama_tp.py - - pytest -v -s -x lora/test_qwen3_with_multi_loras.py - - pytest -v -s -x lora/test_olmoe_tp.py - - pytest -v -s -x lora/test_gptoss_tp.py - - pytest -v -s -x lora/test_qwen35_densemodel_lora.py - - pytest -v -s -x lora/test_gemma4_tp.py + - export VLLM_WORKER_MULTIPROC_METHOD=spawn + - case "$$BUILDKITE_PARALLEL_JOB" in 0|1|2|3) ;; *) echo "unexpected BUILDKITE_PARALLEL_JOB=$$BUILDKITE_PARALLEL_JOB" && exit 1;; esac + - if [ "$$BUILDKITE_PARALLEL_JOB" = "3" ]; then pytest -v -s -x lora/test_chatglm3_tp.py; fi + - if [ "$$BUILDKITE_PARALLEL_JOB" = "1" ]; then pytest -v -s -x lora/test_llama_tp.py; fi + - if [ "$$BUILDKITE_PARALLEL_JOB" = "3" ]; then pytest -v -s -x lora/test_qwen3_with_multi_loras.py; fi + - if [ "$$BUILDKITE_PARALLEL_JOB" = "2" ]; then pytest -v -s -x lora/test_olmoe_tp.py; fi + - if [ "$$BUILDKITE_PARALLEL_JOB" = "0" ]; then pytest -v -s -x lora/test_gptoss_tp.py; fi + - if [ "$$BUILDKITE_PARALLEL_JOB" = "1" ]; then pytest -v -s -x lora/test_qwen35_densemodel_lora.py; fi + - if [ "$$BUILDKITE_PARALLEL_JOB" = "2" ]; then pytest -v -s -x lora/test_gemma4_tp.py; fi #------------------------------------------------------ mi300 · model_executor -------------------------------------------------------# - label: ":amd: (MI300) Model Executor" # TBD - timeout_in_minutes: 60 + timeout_in_minutes: 75 mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300] dind: false agent_pool: mi300_1 @@ -1799,160 +2009,19 @@ steps: - tests/entrypoints/openai/completion/test_tensorizer_entrypoint.py - vllm/_aiter_ops.py - vllm/platforms/rocm.py + - vllm/model_executor/warmup + - tests/model_executor/test_jit_warmup.py + - tests/model_executor/test_jit_warmup_cutedsl_launcher.py + - tests/model_executor/test_jit_warmup_triton_launcher.py commands: - apt-get update && apt-get install -y curl libsodium23 - export VLLM_WORKER_MULTIPROC_METHOD=spawn - - pytest -v -s model_executor -m '(not slow_test)' - - pytest -v -s entrypoints/openai/completion/test_tensorizer_entrypoint.py - -#---------------------------------------------------- mi300 · model_runner_v2 -------------------------------------------------------# - -- label: ":amd: (MI300) Model Runner V2 Core" # TBD - timeout_in_minutes: 55 - mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300] - dind: false - agent_pool: mi300_1 - optional: true - working_dir: "/vllm-workspace/tests" - source_file_dependencies: - - vllm/v1/worker/gpu/ - - vllm/v1/worker/gpu_worker.py - - vllm/v1/core/sched/ - - vllm/v1/attention/ - - tests/v1/engine/test_llm_engine.py - - tests/v1/e2e/ - - tests/entrypoints/llm/test_struct_output_generate.py - - vllm/platforms/rocm.py - commands: - - set -x - - export VLLM_USE_V2_MODEL_RUNNER=1 - - pytest -v -s v1/engine/test_llm_engine.py -k "not test_engine_metrics" - - pytest -v -s v1/e2e/general/test_async_scheduling.py -k "not ngram" - - pytest -v -s v1/e2e/general/test_context_length.py - - pytest -v -s v1/e2e/general/test_min_tokens.py - - pytest -v -s entrypoints/llm/test_struct_output_generate.py -k "xgrammar and not speculative_config6 and not speculative_config7 and not speculative_config8 and not speculative_config0" - -- label: ":amd: (MI300) Model Runner V2 Examples" # TBD - timeout_in_minutes: 40 - mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300] - dind: false - agent_pool: mi300_1 - optional: true - working_dir: "/vllm-workspace/examples" - source_file_dependencies: - - vllm/v1/worker/gpu/ - - vllm/v1/core/sched/ - - vllm/v1/worker/gpu_worker.py - - examples/basic/offline_inference/ - - examples/generate/multimodal/ - - examples/features/ - - examples/pooling/embed/vision_embedding_offline.py - - examples/features/tensorize_vllm_model.py - - examples/deployment/llm_engine_example.py - - vllm/platforms/rocm.py - commands: - - set -x - - export VLLM_USE_V2_MODEL_RUNNER=1 - - pip install tensorizer - - python3 basic/offline_inference/chat.py - - python3 basic/offline_inference/generate.py --model facebook/opt-125m - - python3 generate/multimodal/audio_language_offline.py --seed 0 - - python3 generate/multimodal/vision_language_offline.py --seed 0 - - python3 generate/multimodal/vision_language_multi_image_offline.py --seed 0 - - python3 generate/multimodal/encoder_decoder_multimodal_offline.py --model-type whisper --seed 0 - - python3 pooling/embed/vision_embedding_offline.py --seed 0 - - python3 features/automatic_prefix_caching/prefix_caching_offline.py - - python3 deployment/llm_engine_example.py - - python3 features/tensorize_vllm_model.py --model facebook/opt-125m serialize --serialized-directory /tmp/ --suffix v1 && python3 features/tensorize_vllm_model.py --model facebook/opt-125m deserialize --path-to-tensors /tmp/vllm/facebook/opt-125m/v1/model.tensors - - python3 features/speculative_decoding/spec_decode_offline.py --test --method eagle --num_spec_tokens 3 --dataset-name hf --dataset-path philschmid/mt-bench --num-prompts 80 --temp 0 --top-p 1.0 --top-k -1 --tp 1 --enable-chunked-prefill --max-model-len 2048 - - python3 features/speculative_decoding/spec_decode_offline.py --test --method eagle3 --num_spec_tokens 3 --dataset-name hf --dataset-path philschmid/mt-bench --num-prompts 80 --temp 0 --top-p 1.0 --top-k -1 --tp 1 --enable-chunked-prefill --max-model-len 1536 - -- label: ":amd: (MI300) Model Runner V2 Distributed" # TBD - timeout_in_minutes: 45 - 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/v1/worker/gpu/ - - vllm/v1/worker/gpu_worker.py - - tests/basic_correctness/test_basic_correctness.py - - tests/v1/distributed/test_async_llm_dp.py - - tests/v1/distributed/test_eagle_dp.py - - vllm/platforms/rocm.py - commands: - - set -x - - export VLLM_USE_V2_MODEL_RUNNER=1 - - TARGET_TEST_SUITE=MI300 pytest -v -s basic_correctness/test_basic_correctness.py -m 'distributed(num_gpus=2)' -k "not ray and not True" - - TP_SIZE=1 DP_SIZE=2 pytest -v -s v1/distributed/test_async_llm_dp.py -k "not ray" - - TP_SIZE=1 DP_SIZE=2 pytest -v -s v1/distributed/test_eagle_dp.py - -- label: ":amd: (MI300) Model Runner V2 Pipeline Parallelism" # TBD - timeout_in_minutes: 65 - 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/v1/worker/gpu/ - - vllm/v1/worker/gpu_worker.py - - tests/distributed/test_pipeline_parallel.py - - tests/distributed/test_pp_cudagraph.py - - vllm/platforms/rocm.py - commands: - - set -x - - export VLLM_USE_V2_MODEL_RUNNER=1 - - pytest -v -s distributed/test_pipeline_parallel.py -k "not ray" - - pytest -v -s distributed/test_pp_cudagraph.py -k "not ray" - -- label: ":amd: (MI300) Model Runner V2 Spec Decode" # TBD - timeout_in_minutes: 90 - mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300] - dind: false - agent_pool: mi300_1 - optional: true - working_dir: "/vllm-workspace/tests" - source_file_dependencies: - - vllm/v1/worker/gpu/ - - vllm/v1/worker/gpu_worker.py - - tests/v1/spec_decode/test_max_len.py - - tests/v1/spec_decode/test_rejection_sampler_utils.py - - tests/v1/spec_decode/test_synthetic_rejection_sampler_utils.py - - tests/v1/e2e/spec_decode/ - - vllm/platforms/rocm.py - commands: - - set -x - - export VLLM_USE_V2_MODEL_RUNNER=1 - - pytest -v -s v1/spec_decode/test_max_len.py -k "eagle or mtp" - - pytest -v -s v1/spec_decode/test_rejection_sampler_utils.py - - pytest -v -s v1/spec_decode/test_synthetic_rejection_sampler_utils.py - - pytest -v -s v1/e2e/spec_decode/eagle/ - - pytest -v -s v1/e2e/spec_decode/speculators/ - - pytest -v -s v1/e2e/spec_decode/mtp/ + - export PYTHONFAULTHANDLER=1 + - pytest -v -s model_executor -m '(not slow_test)' --timeout=900 --timeout-method=thread + - pytest -v -s entrypoints/openai/completion/test_tensorizer_entrypoint.py --timeout=900 --timeout-method=thread #------------------------------------------------------ mi300 · models / basic -------------------------------------------------------# -- label: ":amd: (MI300) Basic Models (Extra Initialization) Shard %N" # TBD - timeout_in_minutes: 60 - mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300] - dind: false - agent_pool: mi300_1 - parallelism: 6 - working_dir: "/vllm-workspace/tests" - source_file_dependencies: - - vllm/model_executor/models/ - - vllm/model_executor/layers/ - - tests/models/test_initialization.py - - tests/models/registry.py - - vllm/_aiter_ops.py - - vllm/platforms/rocm.py - commands: - - pytest -v -s models/test_initialization.py -k 'not test_can_initialize_small_subset' --num-shards=$$BUILDKITE_PARALLEL_JOB_COUNT --shard-id=$$BUILDKITE_PARALLEL_JOB - - label: ":amd: (MI300) Basic Models (Initialization)" # TBD timeout_in_minutes: 30 mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300] @@ -1963,11 +2032,12 @@ steps: - vllm/ - tests/models/test_initialization.py - tests/models/registry.py + optional: true commands: - pytest -v -s models/test_initialization.py::test_can_initialize_small_subset - label: ":amd: (MI300) Basic Models (Other)" # TBD - timeout_in_minutes: 55 + timeout_in_minutes: 65 mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300] dind: false agent_pool: mi300_1 @@ -1978,9 +2048,12 @@ steps: - tests/models/test_terratorch.py - tests/models/transformers/test_backend.py - tests/models/test_registry.py + - tests/models/test_hyv4_rocm.py - tests/models/test_vision.py + - tests/models/test_deepseek_v4_vl_rocm.py commands: - - pytest -v -s models/test_terratorch.py models/transformers/test_backend.py models/test_registry.py + - pytest -v -s models/test_terratorch.py models/transformers/test_backend.py models/test_registry.py models/test_deepseek_v4_vl_rocm.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 #----------------------------------------------------- mi300 · models / language -----------------------------------------------------# @@ -1998,8 +2071,9 @@ steps: commands: - pytest -v -s models/language/pooling_mteb_test -- label: ":amd: (MI300) Language Models (Extended Pooling)" # TBD - timeout_in_minutes: 80 +- label: ":amd: (MI300) Language Models (Extended Pooling) Shard %N" + timeout_in_minutes: 95 + parallelism: 4 mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300] dind: false agent_pool: mi300_1 @@ -2009,10 +2083,10 @@ steps: - vllm/ - tests/models/language/pooling commands: - - pytest -v -s models/language/pooling -m 'not core_model' + - pytest -v -s models/language/pooling -m 'not core_model' --num-shards=$$BUILDKITE_PARALLEL_JOB_COUNT --shard-id=$$BUILDKITE_PARALLEL_JOB - label: ":amd: (MI300) Language Models (Standard)" # TBD - timeout_in_minutes: 45 + timeout_in_minutes: 60 mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300] dind: false agent_pool: mi300_1 @@ -2063,22 +2137,6 @@ steps: - 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: (MI300) Language Models (Hybrid) Shard %N" # TBD - timeout_in_minutes: 65 - mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300] - dind: false - agent_pool: mi300_1 - parallelism: 2 - optional: true - working_dir: "/vllm-workspace/tests" - source_file_dependencies: - - 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 hybrid_model --num-shards=$$BUILDKITE_PARALLEL_JOB_COUNT --shard-id=$$BUILDKITE_PARALLEL_JOB - #---------------------------------------------------- mi300 · models / multimodal ----------------------------------------------------# - label: ":amd: (MI300) Multimodal Models (Extended Generation 1)" # TBD @@ -2096,8 +2154,9 @@ steps: - pytest -v -s models/multimodal/generation -m 'not core_model' --ignore models/multimodal/generation/test_common.py - pytest -v -s models/multimodal/test_mapping.py -- label: ":amd: (MI300) Multimodal Models (Extended Generation 2)" # TBD - timeout_in_minutes: 65 +- label: ":amd: (MI300) Multimodal Models (Extended Generation 2) Shard %N" + timeout_in_minutes: 100 + parallelism: 4 mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300] dind: false agent_pool: mi300_1 @@ -2107,11 +2166,11 @@ steps: - vllm/ - tests/models/multimodal/generation commands: - - pytest -v -s models/multimodal/generation/test_common.py -m 'split(group=0) and not core_model' + - pytest -v -s models/multimodal/generation/test_common.py -m 'split(group=0) and not core_model' --num-shards=$$BUILDKITE_PARALLEL_JOB_COUNT --shard-id=$$BUILDKITE_PARALLEL_JOB - label: ":amd: (MI300) Multimodal Models (Extended Generation 3)" # TBD - timeout_in_minutes: 55 + timeout_in_minutes: 90 mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300] dind: false agent_pool: mi300_1 @@ -2124,7 +2183,7 @@ steps: - pytest -v -s models/multimodal/generation/test_common.py -m 'split(group=1) and not core_model' - label: ":amd: (MI300) Multimodal Models (Standard) 1: qwen2" # TBD - timeout_in_minutes: 45 + timeout_in_minutes: 65 mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300] dind: false agent_pool: mi300_1 @@ -2138,7 +2197,7 @@ steps: - pytest -v -s models/multimodal/generation/test_ultravox.py -m core_model - label: ":amd: (MI300) Multimodal Models (Standard) 3: llava + qwen2_vl" # TBD - timeout_in_minutes: 40 + timeout_in_minutes: 55 mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300] dind: false agent_pool: mi300_1 @@ -2148,38 +2207,25 @@ steps: - vllm/ - tests/models/multimodal/generation - tests/models/multimodal/test_mapping.py + - tests/models/multimodal commands: - pytest -v -s models/multimodal/generation/test_common.py -m core_model -k "not qwen2 and not qwen3 and not gemma" - pytest -v -s models/multimodal/generation/test_qwen2_vl.py -m core_model -- label: ":amd: (MI300) Multimodal Models (Standard) 4: other + whisper" # TBD - timeout_in_minutes: 70 - mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300] - dind: false - agent_pool: mi300_1 - working_dir: "/vllm-workspace/tests" - source_file_dependencies: - - vllm/ - - tests/models/multimodal - commands: - - pytest -v -s models/multimodal -m core_model --ignore models/multimodal/generation/test_common.py --ignore models/multimodal/generation/test_ultravox.py --ignore models/multimodal/generation/test_qwen2_5_vl.py --ignore models/multimodal/generation/test_qwen2_vl.py --ignore models/multimodal/generation/test_whisper.py --ignore models/multimodal/generation/test_mm_prefix_lm.py --ignore models/multimodal/generation/test_memory_leak.py --ignore models/multimodal/generation/test_vit_cudagraph.py --ignore models/multimodal/processing - - pytest -v -s models/multimodal/generation/test_vit_cudagraph.py -m core_model - - pytest -v -s models/multimodal/generation/test_memory_leak.py -m core_model - - cd .. && VLLM_WORKER_MULTIPROC_METHOD=spawn pytest -v -s tests/models/multimodal/generation/test_whisper.py -m core_model - -- label: ":amd: (MI300) Multimodal Processor" # 1h 42m - timeout_in_minutes: 110 +- label: ":amd: (MI300) Multimodal Processor Shard %N" # TBD + timeout_in_minutes: 115 mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300] dind: false agent_pool: mi300_1 optional: true + parallelism: 4 working_dir: "/vllm-workspace/tests" source_file_dependencies: - vllm/ - tests/models/multimodal - tests/models/registry.py commands: - - pytest -v -s models/multimodal/processing/test_tensor_schema.py + - pytest -v -s models/multimodal/processing/test_tensor_schema.py --num-shards=$$BUILDKITE_PARALLEL_JOB_COUNT --shard-id=$$BUILDKITE_PARALLEL_JOB - label: ":amd: (MI300) Multimodal Models (Extended Pooling)" # TBD timeout_in_minutes: 75 @@ -2195,7 +2241,7 @@ steps: - pytest -v -s models/multimodal/pooling -m 'not core_model' - label: ":amd: (MI300) Multimodal Models (Standard) 2: qwen3 + gemma" # TBD - timeout_in_minutes: 50 + timeout_in_minutes: 55 mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300] dind: false agent_pool: mi300_1 @@ -2223,7 +2269,9 @@ steps: - vllm/platforms/rocm.py - tests/models/quantization - vllm/model_executor/model_loader/ + optional: true commands: + - unset VLLM_USE_V2_MODEL_RUNNER - pytest -v -s models/quantization #-------------------------------------------------- mi300 · models / transformers ---------------------------------------------------# @@ -2301,7 +2349,7 @@ steps: #---------------------------------------------------------- mi300 · plugins ----------------------------------------------------------# - label: ":amd: (MI300) Plugin Integration" # TBD - timeout_in_minutes: 35 + timeout_in_minutes: 50 mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300] dind: false agent_pool: mi300_2 @@ -2314,43 +2362,43 @@ steps: - vllm/platforms/rocm.py commands: # BEGIN: platform plugin and general plugin tests, all the code in-between runs on dummy platform + # END: platform plugin tests + # BEGIN: `io_processor` plugins test, all the code in between uses the `prithvi_io_processor` plugin + # END: `io_processor` plugins test + # BEGIN: `bge_m3_sparse io_processor` test + # END: `bge_m3_sparse io_processor` test + # BEGIN: `colbert_query io_processor` test + # END: `colbert_query io_processor` test + # BEGIN: `stat_logger` plugins test + # END: `stat_logger` plugins test + # BEGIN: `endpoint` plugins test + # END: `endpoint` plugins test + # BEGIN: other tests - pip install -e ./plugins/vllm_add_dummy_platform - pytest -v -s plugins_tests/test_platform_plugins.py - pip uninstall vllm_add_dummy_platform -y - # END: platform plugin tests - # BEGIN: `io_processor` plugins test, all the code in between uses the `prithvi_io_processor` plugin + - pytest -v -s ./plugins_tests/test_io_processor_plugins.py - pip install -e ./plugins/prithvi_io_processor_plugin - - pytest -v -s plugins_tests/test_io_processor_plugins.py - pytest -v -s plugins_tests/test_terratorch_io_processor_plugins.py - pip uninstall prithvi_io_processor_plugin -y - # END: `io_processor` plugins test - # BEGIN: `bge_m3_sparse io_processor` test - pip install -e ./plugins/bge_m3_sparse_plugin - pytest -v -s plugins_tests/test_bge_m3_sparse_io_processor_plugins.py - pip uninstall bge_m3_sparse_plugin -y - # END: `bge_m3_sparse io_processor` test - # BEGIN: `colbert_query io_processor` test - pip install -e ./plugins/colbert_query_plugin - pytest -v -s plugins_tests/test_colbert_query_io_processor_plugins.py - pip uninstall colbert_query_plugin -y - # END: `colbert_query io_processor` test - # BEGIN: `stat_logger` plugins test - pip install -e ./plugins/vllm_add_dummy_stat_logger - pytest -v -s plugins_tests/test_stats_logger_plugins.py - pip uninstall dummy_stat_logger -y - # END: `stat_logger` plugins test - # BEGIN: `endpoint` plugins test - pip install -e ./plugins/vllm_add_dummy_endpoint_plugin - pytest -v -s plugins_tests/test_endpoint_plugins.py - pip uninstall vllm_add_dummy_endpoint_plugin -y - # END: `endpoint` plugins test - # BEGIN: other tests - pytest -v -s plugins_tests/test_scheduler_plugins.py - pip install -e ./plugins/vllm_add_dummy_model - pytest -v -s distributed/test_distributed_oot.py - - pytest -v -s plugins_tests/test_oot_registration_online.py # it needs a clean process - - pytest -v -s plugins_tests/test_oot_registration_offline.py # it needs a clean process - - pytest -v -s plugins_tests/lora_resolvers # unit tests for in-tree lora resolver plugins + - pytest -v -s plugins_tests/test_oot_registration_online.py + - pytest -v -s plugins_tests/test_oot_registration_offline.py + - pytest -v -s plugins_tests/lora_resolvers - label: ":amd: (MI300) GGUF Plugin" # TBD timeout_in_minutes: 45 @@ -2392,18 +2440,24 @@ steps: - tests/entrypoints/serve/instrumentator/test_uds.py - tests/v1/sample/test_logprobs_e2e.py - vllm/platforms/rocm.py + - tests/entrypoints/openai/chat_completion/test_include_reasoning.py + - tests/entrypoints/openai/chat_completion/test_serving_chat.py + - tests/entrypoints/openai/completion/test_shutdown.py + - tests/entrypoints/openai/test_uds.py commands: - export VLLM_USE_RUST_FRONTEND=1 - export VLLM_WORKER_MULTIPROC_METHOD=spawn - pytest -v -s benchmarks/test_serve_cli.py -k "not insecure and not (test_bench_serve and not test_bench_serve_chat)" - pytest -v -s entrypoints/openai/chat_completion/test_chat_completion.py -k "not test_invalid_json_schema and not test_invalid_regex and not test_kv_transfer_prompt_token_ids_round_trip and not test_kv_transfer_prompt_token_ids_streaming" + - pytest -v -s entrypoints/openai/chat_completion/test_include_reasoning.py - pytest -v -s entrypoints/openai/chat_completion/test_chat_logit_bias_validation.py -k "not multiple" + - pytest -v -s entrypoints/openai/chat_completion/test_serving_chat.py -k "test_chat_per_request_metrics_follow_server_flag or test_streaming_reasoning_usage_counts_across_deltas or test_completion_tokens_details" - pytest -v -s entrypoints/launchers/test_shutdown.py -k "not engine_failure and not test_abort_timeout_exits_quickly" - pytest -v -s entrypoints/openai/test_return_token_ids.py -k "not test_comparison" - pytest -v -s entrypoints/serve/instrumentator/test_uds.py - pytest -v -s v1/sample/test_logprobs_e2e.py -k "test_prompt_logprobs_e2e_server" -- label: ":amd: (MI300) Rust Frontend Serve + Admin Coverage" # TBD +- label: ":amd: (MI300) Rust Frontend Serve-Admin Coverage" timeout_in_minutes: 30 mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300] dind: false @@ -2499,8 +2553,9 @@ steps: #------------------------------------------------------- mi300 · quantization --------------------------------------------------------# -- label: ":amd: (MI300) Quantization" # TBD +- label: ":amd: (MI300) Quantization Shard %N" timeout_in_minutes: 115 + parallelism: 4 mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300] dind: false agent_pool: mi300_1 @@ -2514,19 +2569,19 @@ steps: - vllm/model_executor/layers/fused_moe/unquantized_fused_moe_method.py - tests/quantization - tests/rocm/test_moe_weight_replay.py + optional: true commands: - # temporary install here since we need nightly, will move to requirements/test.in # after torchao 0.12 release, and pin a working version of torchao nightly here - # since torchao nightly is only compatible with torch nightly currently # https://github.com/pytorch/ao/issues/2919, we'll have to skip new torchao tests for now # we can only upgrade after this is resolved # TODO(jerryzh168): resolve the above comment - - uv pip install --system torchao==0.17.0 + - unset VLLM_USE_V2_MODEL_RUNNER + - uv pip install --system torchao==0.18.0 - uv pip install --system conch-triton-kernels - - VLLM_TEST_FORCE_LOAD_FORMAT=auto pytest -v -s quantization/ --ignore quantization/test_blackwell_moe.py - - pytest -v -s rocm/test_moe_weight_replay.py + - VLLM_TEST_FORCE_LOAD_FORMAT=auto pytest -v -s quantization/ --ignore quantization/test_blackwell_moe.py --shard-id=$$BUILDKITE_PARALLEL_JOB --num-shards=$$BUILDKITE_PARALLEL_JOB_COUNT + - if [ "$$BUILDKITE_PARALLEL_JOB" = "0" ]; then pytest -v -s rocm/test_moe_weight_replay.py; fi #--------------------------------------------------------- mi300 · samplers ----------------------------------------------------------# @@ -2546,8 +2601,9 @@ steps: - tests/conftest.py - vllm/_aiter_ops.py - vllm/platforms/rocm.py + - vllm/entrypoints/generate/beam_search commands: - - pytest -v -s samplers + - pytest -v -s samplers -k "not test_beam_search_passes_multimodal_data" #------------------------------------------------------------ mi300 · misc ------------------------------------------------------------# @@ -2560,6 +2616,18 @@ steps: source_file_dependencies: - vllm/ - tests/test_regression + - vllm/config/ + - vllm/distributed/ + - vllm/engine/ + - vllm/inputs/ + - vllm/model_executor/ + - vllm/multimodal/ + - vllm/platforms/ + - vllm/sampling_params.py + - vllm/transformers_utils/ + - vllm/utils/ + - vllm/v1/ + optional: true commands: - pip install 'modelscope<1.38' - pytest -v -s test_regression.py @@ -2576,6 +2644,8 @@ steps: - requirements/ - setup.py - vllm/platforms/rocm.py + - .buildkite/scripts/check-ray-compatibility.sh + optional: true commands: - bash /vllm-workspace/.buildkite/scripts/check-ray-compatibility.sh @@ -2598,7 +2668,7 @@ steps: - pytest -v -s v1/spec_decode/test_acceptance_length.py -m slow_test - label: ":amd: (MI300) E2E Scheduling" # TBD - timeout_in_minutes: 45 + timeout_in_minutes: 55 mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300] dind: false agent_pool: mi300_1 @@ -2612,7 +2682,7 @@ steps: - pytest -v -s v1/e2e/general/test_async_scheduling.py - label: ":amd: (MI300) V1 Engine" # TBD - timeout_in_minutes: 40 + timeout_in_minutes: 45 mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300] dind: false agent_pool: mi300_1 @@ -2622,12 +2692,14 @@ steps: - vllm/v1/engine/ - tests/v1/engine/ - vllm/platforms/rocm.py + - tests/v1/test_tensor_ipc_queue.py commands: - pytest -v -s v1/engine/test_preprocess_error_handling.py - pytest -v -s v1/engine --ignore v1/engine/test_preprocess_error_handling.py + - pytest -v -s v1/test_tensor_ipc_queue.py - label: ":amd: (MI300) Spec Decode Draft Model" # TBD - timeout_in_minutes: 45 + timeout_in_minutes: 60 mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300] dind: false agent_pool: mi300_1 @@ -2644,7 +2716,7 @@ steps: commands: - pytest -v -s v1/e2e/spec_decode/draft_model/ -- label: ":amd: (MI300) Spec Decode Eagle" # TBD +- label: ":amd: (MI300) Spec Decode Eagle 1: DeepSeek + Qwen" timeout_in_minutes: 60 mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300] dind: false @@ -2660,15 +2732,15 @@ steps: - tests/v1/e2e/spec_decode/ - vllm/platforms/rocm.py commands: - - pytest -v -s v1/e2e/spec_decode/eagle/ + - pytest -v -s v1/e2e/spec_decode/eagle/ -k "deepseek_eagle or qwen3_eagle3" -- label: ":amd: (MI300) Spec Decode N-Gram + Suffix" # TBD - timeout_in_minutes: 35 +- label: ":amd: (MI300) Spec Decode Eagle 2: Llama 3 + Qwen VL + Other" + timeout_in_minutes: 60 mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300] dind: false agent_pool: mi300_1 optional: true - working_dir: "/vllm-workspace/tests" + working_dir: /vllm-workspace/tests source_file_dependencies: - vllm/v1/spec_decode/ - vllm/v1/worker/gpu/spec_decode/ @@ -2676,14 +2748,33 @@ steps: - vllm/v1/sample/ - vllm/model_executor/layers/ - tests/v1/e2e/spec_decode/ - - tests/spec_decode/ - vllm/platforms/rocm.py commands: - - pytest -v -s v1/e2e/spec_decode/ngram_suffix/ + - pytest -v -s v1/e2e/spec_decode/eagle/ -k "not deepseek_eagle and not qwen3_eagle3" + + +- label: ":amd: (MI300) Spec Decode N-Gram + Suffix" # TBD + timeout_in_minutes: 35 + mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300] + dind: false + agent_pool: mi300_1 + optional: true + working_dir: "/vllm-workspace/tests" + source_file_dependencies: + - vllm/v1/spec_decode/ + - vllm/v1/worker/gpu/spec_decode/ + - vllm/model_executor/model_loader/ + - vllm/v1/sample/ + - vllm/model_executor/layers/ + - tests/v1/e2e/spec_decode/ + - tests/spec_decode/ + - vllm/platforms/rocm.py + commands: + - pytest -v -s v1/e2e/spec_decode/ngram_suffix/ - python3 spec_decode/test_custom_proposer.py - label: ":amd: (MI300) Spec Decode Speculators + MTP" # TBD - timeout_in_minutes: 50 + timeout_in_minutes: 75 mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300] dind: false agent_pool: mi300_1 @@ -2698,12 +2789,13 @@ steps: - vllm/transformers_utils/configs/speculators/ - tests/v1/e2e/spec_decode/ - vllm/platforms/rocm.py + - vllm/v1/attention/backends/ commands: - pytest -v -s v1/e2e/spec_decode/speculators/ - pytest -v -s v1/e2e/spec_decode/mtp/ -- label: ":amd: (MI300) Speculators Correctness" # TBD - timeout_in_minutes: 35 +- label: ":amd: (MI300) Speculators Correctness Nightly" + timeout_in_minutes: 40 mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300] dind: false agent_pool: mi300_1 @@ -2730,6 +2822,7 @@ steps: - tests/evals/gsm8k/ - tests/v1/spec_decode/test_speculators_correctness.py - vllm/platforms/rocm.py + - vllm/v1/worker/gpu/spec_decode/dflash/ commands: - export VLLM_ALLOW_INSECURE_SERIALIZATION=1 - pytest -v -s v1/spec_decode/test_speculators_correctness.py -m slow_test @@ -2743,6 +2836,17 @@ steps: source_file_dependencies: - vllm/ - tests/v1/spec_decode + - vllm/config/ + - vllm/distributed/ + - vllm/inputs/ + - vllm/model_executor/ + - vllm/models/qwen4_exp/ + - vllm/platforms/ + - vllm/sampling_params.py + - vllm/transformers_utils/ + - vllm/utils/ + - vllm/v1/ + optional: true commands: - export VLLM_WORKER_MULTIPROC_METHOD=spawn - pytest -v -s -m 'not slow_test' v1/spec_decode @@ -2753,7 +2857,7 @@ steps: dind: false agent_pool: mi300_2 num_gpus: 2 - working_dir: "/vllm-workspace/" + working_dir: /vllm-workspace source_file_dependencies: - vllm/config/speculative.py - vllm/distributed/kv_transfer/kv_connector/ @@ -2775,12 +2879,15 @@ steps: - vllm/_aiter_ops.py - tests/v1/kv_connector/extract_hidden_states_integration/ - vllm/platforms/rocm.py + - vllm/distributed/kv_transfer/kv_connector/v1/example_hidden_states_connector.py + - tests/v1/kv_connector/extract_hidden_states_integration + optional: true commands: - export VLLM_WORKER_MULTIPROC_METHOD=spawn - - pytest -v -s tests/v1/kv_connector/extract_hidden_states_integration + - pytest -v -s -m 'distributed' tests/v1/kv_connector/extract_hidden_states_integration - label: ":amd: (MI300) V1 Attention Shard %N" # TBD - timeout_in_minutes: 35 + timeout_in_minutes: 125 mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300] dind: false agent_pool: mi300_1 @@ -2798,7 +2905,7 @@ steps: commands: - pytest -v -s v1/attention --shard-id=$$BUILDKITE_PARALLEL_JOB --num-shards=$$BUILDKITE_PARALLEL_JOB_COUNT -- label: ":amd: (MI300) V1 Core + KV + Metrics" # TBD +- label: ":amd: (MI300) V1 Core" timeout_in_minutes: 60 mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300] dind: false @@ -2816,23 +2923,204 @@ steps: - tests/v1/ec_connector/unit - tests/v1/metrics - tests/entrypoints/openai/correctness/test_lmeval.py + - vllm/config/ + - vllm/distributed/ + - vllm/engine/ + - vllm/entrypoints/pooling/ + - vllm/inputs/ + - vllm/lora/ + - vllm/model_executor/ + - vllm/multimodal/ + - vllm/outputs.py + - vllm/platforms/ + - vllm/pooling_params.py + - vllm/profiler/ + - vllm/sampling_params.py + - vllm/tokenizers/ + - vllm/transformers_utils/ + - vllm/utils/ + - vllm/v1/ + optional: true commands: - - uv pip install --system -r /vllm-workspace/requirements/kv_connectors_rocm.txt + # - export HSA_NO_SCRATCH_RECLAIM=1 - export VLLM_WORKER_MULTIPROC_METHOD=spawn - pytest -v -s -m 'not cpu_test' v1/core + +- label: ":amd: (MI300) V1 Executor + Worker" + timeout_in_minutes: 60 + mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300] + dind: false + agent_pool: mi300_1 + working_dir: /vllm-workspace/tests + source_file_dependencies: + - vllm/ + - tests/v1/core + - tests/v1/executor + - tests/v1/kv_offload + - tests/v1/simple_kv_offload + - tests/v1/worker + - tests/v1/streaming_input + - tests/v1/kv_connector/unit + - tests/v1/ec_connector/unit + - tests/v1/metrics + - tests/entrypoints/openai/correctness/test_lmeval.py + - vllm/config/ + - vllm/distributed/ + - vllm/engine/ + - vllm/entrypoints/pooling/ + - vllm/inputs/ + - vllm/lora/ + - vllm/model_executor/ + - vllm/multimodal/ + - vllm/outputs.py + - vllm/platforms/ + - vllm/pooling_params.py + - vllm/profiler/ + - vllm/sampling_params.py + - vllm/tokenizers/ + - vllm/transformers_utils/ + - vllm/utils/ + - vllm/v1/ + commands: + - export VLLM_WORKER_MULTIPROC_METHOD=spawn - pytest -v -s v1/executor - - pytest -v -s v1/kv_offload - - pytest -v -s v1/simple_kv_offload - pytest -v -s v1/worker - pytest -v -s v1/streaming_input - - pytest -v -s -m 'not cpu_test' v1/kv_connector/unit - - pytest -v -s -m 'not cpu_test' v1/ec_connector/unit + optional: true + +- label: ":amd: (MI300) V1 KV Offload" + timeout_in_minutes: 60 + mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300] + dind: false + agent_pool: mi300_1 + working_dir: /vllm-workspace/tests + source_file_dependencies: + - vllm/ + - tests/v1/core + - tests/v1/executor + - tests/v1/kv_offload + - tests/v1/simple_kv_offload + - tests/v1/worker + - tests/v1/streaming_input + - tests/v1/kv_connector/unit + - tests/v1/ec_connector/unit + - tests/v1/metrics + - tests/entrypoints/openai/correctness/test_lmeval.py + - vllm/config/ + - vllm/distributed/ + - vllm/engine/ + - vllm/entrypoints/pooling/ + - vllm/inputs/ + - vllm/lora/ + - vllm/model_executor/ + - vllm/multimodal/ + - vllm/outputs.py + - vllm/platforms/ + - vllm/pooling_params.py + - vllm/profiler/ + - vllm/sampling_params.py + - vllm/tokenizers/ + - vllm/transformers_utils/ + - vllm/utils/ + - vllm/v1/ + commands: + - export VLLM_WORKER_MULTIPROC_METHOD=spawn + - bash /vllm-workspace/.buildkite/scripts/install-kv-offload.sh + - pytest -v -s v1/kv_offload + - pytest -v -s v1/simple_kv_offload + optional: true + +- label: ":amd: (MI300) V1 KV Connectors Shard %N" + timeout_in_minutes: 60 + parallelism: 4 + mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300] + dind: false + agent_pool: mi300_1 + working_dir: /vllm-workspace/tests + source_file_dependencies: + - vllm/ + - tests/v1/core + - tests/v1/executor + - tests/v1/kv_offload + - tests/v1/simple_kv_offload + - tests/v1/worker + - tests/v1/streaming_input + - tests/v1/kv_connector/unit + - tests/v1/ec_connector/unit + - tests/v1/metrics + - tests/entrypoints/openai/correctness/test_lmeval.py + - vllm/config/ + - vllm/distributed/ + - vllm/engine/ + - vllm/entrypoints/pooling/ + - vllm/inputs/ + - vllm/lora/ + - vllm/model_executor/ + - vllm/multimodal/ + - vllm/outputs.py + - vllm/platforms/ + - vllm/pooling_params.py + - vllm/profiler/ + - vllm/sampling_params.py + - vllm/tokenizers/ + - vllm/transformers_utils/ + - vllm/utils/ + - vllm/v1/ + commands: + - bash /vllm-workspace/.buildkite/scripts/install-kv-connectors.sh + - export VLLM_WORKER_MULTIPROC_METHOD=spawn + - pytest -v -s -m 'not cpu_test' v1/kv_connector/unit --num-shards=$$BUILDKITE_PARALLEL_JOB_COUNT --shard-id=$$BUILDKITE_PARALLEL_JOB + - pytest -v -s -m 'not cpu_test' v1/ec_connector/unit --num-shards=$$BUILDKITE_PARALLEL_JOB_COUNT --shard-id=$$BUILDKITE_PARALLEL_JOB + optional: true + +- label: ":amd: (MI300) V1 Metrics + LM Eval" + timeout_in_minutes: 65 + mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300] + dind: false + agent_pool: mi300_1 + working_dir: /vllm-workspace/tests + source_file_dependencies: + - vllm/ + - tests/v1/core + - tests/v1/executor + - tests/v1/kv_offload + - tests/v1/simple_kv_offload + - tests/v1/worker + - tests/v1/streaming_input + - tests/v1/kv_connector/unit + - tests/v1/ec_connector/unit + - tests/v1/metrics + - tests/entrypoints/openai/correctness/test_lmeval.py + - vllm/config/ + - vllm/distributed/ + - vllm/engine/ + - vllm/entrypoints/pooling/ + - vllm/inputs/ + - vllm/lora/ + - vllm/model_executor/ + - vllm/multimodal/ + - vllm/outputs.py + - vllm/platforms/ + - vllm/pooling_params.py + - vllm/profiler/ + - vllm/sampling_params.py + - vllm/tokenizers/ + - vllm/transformers_utils/ + - vllm/utils/ + - vllm/v1/ + commands: + - export GIT_CONFIG_COUNT=1 + - export GIT_CONFIG_KEY_0=http.version + - export GIT_CONFIG_VALUE_0=HTTP/1.1 + - export GIT_TERMINAL_PROMPT=0 + - export VLLM_WORKER_MULTIPROC_METHOD=spawn - pytest -v -s -m 'not cpu_test' v1/metrics - pip install -U git+https://github.com/vllm-project/lm-evaluation-harness.git@streaming-api - # - export HSA_NO_SCRATCH_RECLAIM=1 - pytest -v -s entrypoints/openai/correctness/test_lmeval.py::test_lm_eval_accuracy_v1_engine + optional: true -- label: ":amd: (MI300) V1 Sample + Logits" # TBD + +- label: ":amd: (MI300) V1 Sample" timeout_in_minutes: 70 mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300] dind: false @@ -2846,41 +3134,56 @@ steps: - tests/v1/test_oracle.py - tests/v1/test_request.py - tests/v1/test_outputs.py + - vllm/config/ + - vllm/distributed/ + - vllm/engine/ + - vllm/inputs/ + - vllm/logger.py + - vllm/model_executor/ + - vllm/platforms/ + - vllm/sampling_params.py + - vllm/transformers_utils/ + - vllm/utils/ + - vllm/v1/ commands: - export VLLM_WORKER_MULTIPROC_METHOD=spawn - pytest -v -s v1/sample - - pytest -v -s v1/logits_processors - - pytest -v -s v1/test_oracle.py - - pytest -v -s v1/test_request.py - - pytest -v -s v1/test_outputs.py -- label: ":amd: (MI300) Distributed DP Basic" # TBD - timeout_in_minutes: 45 +- label: ":amd: (MI300) V1 Logits + Oracle" + timeout_in_minutes: 70 mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300] dind: false - agent_pool: mi300_2 + agent_pool: mi300_1 optional: true - num_gpus: 2 - working_dir: "/vllm-workspace/tests" + working_dir: /vllm-workspace/tests source_file_dependencies: + - vllm/ + - tests/v1/sample + - tests/v1/logits_processors + - tests/v1/test_oracle.py + - tests/v1/test_request.py + - tests/v1/test_outputs.py + - vllm/config/ - vllm/distributed/ - vllm/engine/ - - vllm/executor/ - - vllm/worker/worker_base.py - - vllm/v1/engine/ - - vllm/v1/worker/ - - tests/v1/distributed - - tests/entrypoints/launchers/api_server/test_multi_api_servers.py - - vllm/platforms/rocm.py + - vllm/inputs/ + - vllm/logger.py + - vllm/model_executor/ + - vllm/platforms/ + - vllm/sampling_params.py + - vllm/transformers_utils/ + - vllm/utils/ + - vllm/v1/ commands: - - export NCCL_CUMEM_HOST_ENABLE=0 - - TP_SIZE=1 DP_SIZE=2 pytest -v -s v1/distributed/test_async_llm_dp.py - - TP_SIZE=1 DP_SIZE=2 pytest -v -s v1/distributed/test_eagle_dp.py - - TP_SIZE=1 DP_SIZE=2 pytest -v -s v1/distributed/test_external_lb_dp.py - - DP_SIZE=2 pytest -v -s entrypoints/launchers/api_server/test_multi_api_servers.py + - export VLLM_WORKER_MULTIPROC_METHOD=spawn + - pytest -v -s v1/logits_processors + - pytest -v -s v1/test_oracle.py + - pytest -v -s v1/test_request.py + - pytest -v -s v1/test_outputs.py + - label: ":amd: (MI300) Metrics, Tracing" # TBD - timeout_in_minutes: 25 + timeout_in_minutes: 30 mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300] dind: false agent_pool: mi300_2 @@ -2891,6 +3194,18 @@ steps: - 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' \ @@ -2901,7 +3216,7 @@ steps: - pytest -v -s tracing - label: ":amd: (MI300) V1 E2E" # TBD - timeout_in_minutes: 25 + timeout_in_minutes: 30 mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300] dind: false agent_pool: mi300_2 @@ -2911,31 +3226,84 @@ steps: source_file_dependencies: - vllm/ - tests/v1/e2e + - vllm/compilation/ + - vllm/config/ + - vllm/distributed/ + - vllm/engine/ + - vllm/envs.py + - vllm/forward_context.py + - vllm/inputs/ + - vllm/logger.py + - vllm/logging_utils/ + - vllm/model_executor/ + - vllm/multimodal/ + - vllm/platforms/ + - vllm/sampling_params.py + - vllm/transformers_utils/ + - vllm/triton_utils/ + - vllm/utils/ + - vllm/v1/ + - tests/v1/e2e/spec_decode/ commands: - >- 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 -- label: ":amd: (MI300) NixlConnector PD + Spec Decode acceptance" # TBD - timeout_in_minutes: 45 +- label: ":amd: (MI300) Mooncake EC TCP E2E" + timeout_in_minutes: 60 mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300] dind: false agent_pool: mi300_2 num_gpus: 2 optional: true - working_dir: "/vllm-workspace/tests" + working_dir: "/vllm-workspace" 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/distributed/ec_transfer/ + - vllm/config/ec_transfer.py + - vllm/config/multimodal.py + - vllm/config/vllm.py + - vllm/multimodal/ + - vllm/v1/core/encoder_cache_manager.py + - vllm/v1/core/sched/ + - vllm/v1/engine/ + - vllm/v1/worker/ec_connector_model_runner_mixin.py + - vllm/v1/worker/gpu_model_runner.py + - vllm/v1/worker/gpu_worker.py + - vllm/v1/worker/gpu/ec_connector.py + - vllm/v1/worker/gpu/model_runner.py + - vllm/v1/worker/mm_encoder_model_runner.py + - vllm/v1/worker/encoder_cudagraph.py + - vllm/v1/worker/encoder_cudagraph_defs.py + - vllm/v1/attention/ops/vit_attn_wrappers.py + - vllm/v1/worker/gpu/model_states/ + - vllm/v1/worker/gpu/mm/ + - tests/v1/cudagraph/test_encoder_cudagraph.py + - tests/models/multimodal/generation/test_vit_cudagraph.py + - examples/disaggregated/disaggregated_encoder/ + - tests/v1/ec_connector/ + - requirements/kv_connectors.txt - vllm/platforms/rocm.py + - requirements/rocm.txt 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 + - export VLLM_WORKER_MULTIPROC_METHOD=spawn + - export PYTHON_BIN='python' + - export PYTHONUNBUFFERED='1' + - export VLLM_HOST_IP='127.0.0.1' + - export VLLM_USE_V2_MODEL_RUNNER='1' + - export E_CUDAGRAPH_MM_ENCODER='1' + - export E_MM_ENCODER_ATTN_BACKEND='TRITON_ATTN' + - export MOONCAKE_EC_PROTOCOL='tcp' + - export USE_MM_PROMPTS='1' + - export SKIP_BASELINE='0' + - export CONCURRENCY='3' + - export REPEAT='2' + - export LOG_PATH='/tmp/mooncake-ec-e2e' + - export BASELINE_FILE='/tmp/mooncake-ec-e2e/baseline.json' + - bash tests/v1/ec_connector/integration/run_epd_mooncake_ec_full_pipeline.sh - label: ":amd: (MI300) CrossLayer KV layout Distributed NixlConnector PD accuracy" # TBD - timeout_in_minutes: 45 + timeout_in_minutes: 60 mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300] dind: false agent_pool: mi300_4 @@ -2951,7 +3319,7 @@ steps: - 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: 60 + timeout_in_minutes: 75 mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300] dind: false agent_pool: mi300_4 @@ -2974,38 +3342,6 @@ steps: - pytest -v -s v1/engine/test_engine_core_client.py::test_kv_cache_events_dp - pytest -v -s distributed/test_utils.py -- 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) 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) Hybrid SSM NixlConnector PD accuracy" # TBD timeout_in_minutes: 60 mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300] @@ -3079,48 +3415,21 @@ steps: - ATTENTION_BACKEND=TRITON_ATTN bash v1/kv_connector/nixl_integration/run_multi_connector_edge_case_test.sh - label: ":amd: (MI300) V1 E2E Hybrid Chunked Prefill" # TBD - timeout_in_minutes: 30 + timeout_in_minutes: 35 mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300] dind: false agent_pool: mi300_4 num_gpus: 4 optional: true source_file_dependencies: - - vllm/v1/attention/backends/utils.py - - vllm/v1/worker/gpu_model_runner.py - - tests/v1/e2e/test_hybrid_chunked_prefill.py + - vllm/v1/attention/backends/utils.py + - vllm/v1/worker/gpu_model_runner.py + - tests/v1/e2e/test_hybrid_chunked_prefill.py + - vllm/v1/core/ + working_dir: /vllm-workspace/tests commands: - pytest -v -s v1/e2e/test_hybrid_chunked_prefill.py -#------------------------------------------------------ mi300 · weight_loading -------------------------------------------------------# - -- label: ":amd: (MI300) Weight Loading Multi-GPU" # TBD - timeout_in_minutes: 70 - mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300] - dind: false - agent_pool: mi300_2 - num_gpus: 2 - working_dir: "/vllm-workspace/tests" - source_file_dependencies: - - vllm/ - - tests/weight_loading - commands: - - bash weight_loading/run_model_weight_loading_test.sh -c weight_loading/models-amd.txt - -- label: ":amd: (MI300) Weight Loading Multi-GPU (Large Models)" # TBD - timeout_in_minutes: 35 - 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/ - - tests/weight_loading - commands: - - bash weight_loading/run_model_weight_loading_test.sh -c weight_loading/models-large-amd.txt - ######################################################################################################################################### # # # MI355 (gfx950) tests # @@ -3144,9 +3453,49 @@ steps: commands: - python3 benchmarks/attention_benchmarks/benchmark.py --backends ROCM_ATTN ROCM_AITER_FA ROCM_AITER_UNIFIED_ATTN --batch-specs "8q1s1k" -#-------------------------------------------------------- mi355 · distributed --------------------------------------------------------# +#---------------------------------------------------------- mi355 · compile ----------------------------------------------------------# -- label: ":amd: (MI355) Distributed Features" # TBD +- label: ":amd: (MI355) PyTorch Compilation H100 Cases" + timeout_in_minutes: 45 + mirror_hardwares: [amdexperimental, amdproduction, amdgfx950nightly, amdmi355] + dind: false + agent_pool: mi355_1 + num_gpus: 1 + optional: true + working_dir: "/vllm-workspace/tests" + source_file_dependencies: + - vllm/__init__.py + - vllm/_aiter_ops.py + - vllm/_custom_ops.py + - vllm/compilation/ + - vllm/config/ + - vllm/distributed/ + - "!vllm/distributed/kv_transfer/" + - vllm/engine/ + - vllm/env_override.py + - vllm/envs.py + - vllm/forward_context.py + - vllm/inputs/ + - vllm/ir/ + - vllm/kernels/ + - vllm/logger.py + - vllm/model_executor/ + - vllm/multimodal/ + - vllm/platforms/ + - vllm/plugins/ + - vllm/sampling_params.py + - vllm/sequence.py + - vllm/transformers_utils/ + - vllm/triton_utils/ + - vllm/utils/ + - vllm/v1/ + - tests/compile/h100/ + - tests/utils.py + - csrc/ + commands: + - pytest -v -s compile/h100/test_startup.py + +- label: ":amd: (MI355) Fusion and Compile FP8" timeout_in_minutes: 45 mirror_hardwares: [amdexperimental, amdproduction, amdgfx950nightly, amdmi355] dind: false @@ -3155,162 +3504,444 @@ steps: optional: true working_dir: "/vllm-workspace/" source_file_dependencies: - - csrc/custom_quickreduce.cu - - csrc/ops.h - - csrc/torch_bindings.cpp - - vllm/distributed/ + - vllm/compilation/ - vllm/model_executor/layers/ - - vllm/entrypoints/llm.py - - vllm/config/parallel.py - - vllm/model_executor/layers/fused_moe/ - - vllm/v1/engine/ - - vllm/v1/executor/ - - vllm/v1/worker/ - - vllm/v1/distributed/ - - vllm/v1/attention/backends/ - - vllm/v1/attention/selector.py + - vllm/v1/attention/ + - vllm/distributed/ - vllm/_aiter_ops.py - - vllm/_custom_ops.py - - vllm/platforms/rocm.py - - vllm/envs.py - - examples/offline_inference/data_parallel.py - - tests/distributed/test_context_parallel.py - - tests/distributed/test_rocm_aiter_custom_ar.py - - tests/distributed/test_rocm_quick_reduce.py - - tests/distributed/test_quick_all_reduce.py - - tests/v1/e2e/general/test_rocm_aiter_custom_ar.py - - tests/v1/distributed/test_dbo.py - - tests/utils.py + - tests/compile/passes/ + - tests/compile/correctness_e2e/test_attn_quant.py + - vllm/model_executor/kernels/linear/ + - vllm/model_executor/models/llama.py commands: - - pytest -v -s tests/distributed/test_context_parallel.py - - pytest -v -s tests/distributed/test_rocm_aiter_custom_ar.py - - pytest -v -s tests/v1/e2e/general/test_rocm_aiter_custom_ar.py - - pytest -v -s tests/distributed/test_rocm_quick_reduce.py - - pytest -v -s tests/distributed/test_quick_all_reduce.py - - pytest -v -s tests/v1/distributed/test_dbo.py + - amd-smi + - pytest -v -s tests/compile/passes/test_fusion_attn.py tests/compile/passes/test_silu_mul_quant_fusion.py tests/compile/passes/test_silu_mul_quant_manual_fusion.py + - pytest -v -s tests/compile/correctness_e2e/test_attn_quant.py -#-------------------------------------------------------- mi355 · entrypoints --------------------------------------------------------# +#-------------------------------------------------------- mi355 · distributed --------------------------------------------------------# -- label: ":amd: (MI355) Entrypoints Integration (API Server)" # TBD - timeout_in_minutes: 55 +- label: ":amd: (MI355) Sharded RDT Weight Transfer" + timeout_in_minutes: 45 mirror_hardwares: [amdexperimental, amdproduction, amdgfx950nightly, amdmi355] dind: false agent_pool: mi355_1 + num_gpus: 1 optional: true - fast_check: true working_dir: "/vllm-workspace/tests" source_file_dependencies: - - vllm/ - - tests/entrypoints/serve - - tests/entrypoints/scale_out + - vllm/distributed/weight_transfer/ + - tests/distributed/test_sharded_rdt_plan.py + - tests/distributed/test_sharded_rdt_producer.py + - tests/distributed/test_sharded_rdt_trainer.py + - vllm/platforms/rocm.py commands: - export VLLM_WORKER_MULTIPROC_METHOD=spawn - - pytest -v -s entrypoints/serve --ignore=entrypoints/serve/dev/rpc - - PYTHONPATH=/vllm-workspace pytest -v -s entrypoints/serve/dev/rpc - - pytest -v -s entrypoints/scale_out + - uv pip install --system --no-deps 'ray==2.56.1' + - pytest -v -s distributed/test_sharded_rdt_plan.py + - pytest -v -s distributed/test_sharded_rdt_producer.py + - pytest -v -s distributed/test_sharded_rdt_trainer.py -- label: ":amd: (MI355) Entrypoints Integration (API Server OpenAI - Part 1)" # TBD - timeout_in_minutes: 35 +- label: ":amd: (MI355) Distributed DP Basic" # TBD + timeout_in_minutes: 45 mirror_hardwares: [amdexperimental, amdproduction, amdgfx950nightly, amdmi355] dind: false - agent_pool: mi355_1 - fast_check: true + agent_pool: mi355_2 optional: true + num_gpus: 2 working_dir: "/vllm-workspace/tests" source_file_dependencies: - - vllm/ - - tests/entrypoints/openai + - vllm/distributed/ + - vllm/engine/ + - vllm/executor/ + - vllm/worker/worker_base.py + - vllm/v1/engine/ + - vllm/v1/worker/ + - tests/v1/distributed + - tests/entrypoints/launchers/api_server/test_multi_api_servers.py + - vllm/platforms/rocm.py + - tests/v1/e2e/general/test_sharded_sampling.py + - tests/v1/e2e/spec_decode/test_sharded_sampling.py + - tests/v1/e2e/spec_decode/utils.py commands: - - export VLLM_WORKER_MULTIPROC_METHOD=spawn - - pytest -v -s entrypoints/openai --ignore=entrypoints/openai/completion --ignore=entrypoints/openai/chat_completion --ignore=entrypoints/openai/responses --ignore=entrypoints/openai/correctness + - export NCCL_CUMEM_HOST_ENABLE=0 + - TP_SIZE=1 DP_SIZE=2 pytest -v -s v1/distributed/test_async_llm_dp.py + - TP_SIZE=1 DP_SIZE=2 pytest -v -s v1/distributed/test_eagle_dp.py + - TP_SIZE=1 DP_SIZE=2 pytest -v -s v1/distributed/test_external_lb_dp.py + - DP_SIZE=2 pytest -v -s entrypoints/launchers/api_server/test_multi_api_servers.py + - pytest -v -s v1/e2e/general/test_sharded_sampling.py + - pytest -v -s v1/e2e/spec_decode/test_sharded_sampling.py -- label: ":amd: (MI355) Entrypoints Integration (API Server OpenAI - Part 2)" # TBD - timeout_in_minutes: 50 +- label: ":amd: (MI355) MoRI EP Healthy Baseline" + timeout_in_minutes: 10 mirror_hardwares: [amdexperimental, amdproduction, amdgfx950nightly, amdmi355] dind: false - agent_pool: mi355_1 - fast_check: true - optional: true + agent_pool: mi355_2 + num_gpus: 2 working_dir: "/vllm-workspace/tests" source_file_dependencies: - - vllm/ - - tests/entrypoints/openai + - csrc/moe/ + - csrc/rocm/ + - tests/kernels/moe/ + - vllm/model_executor/layers/fused_moe/ + - vllm/model_executor/layers/quantization/ + - vllm/distributed/ + - vllm/config/ + - vllm/forward_context.py + - vllm/v1/worker/workspace.py + - vllm/utils/import_utils.py + - vllm/utils/math_utils.py + - vllm/utils/torch_utils.py + - vllm/platforms/ + - vllm/_aiter_ops.py + optional: true + # TODO: Add fault injection, detection and recovery once MoRI supports them. + # This job currently checks healthy EP execution only. commands: - - export VLLM_WORKER_MULTIPROC_METHOD=spawn - - pytest -v -s entrypoints/openai/chat_completion - - pytest -v -s entrypoints/openai/completion --ignore=entrypoints/openai/completion/test_tensorizer_entrypoint.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]' -- label: ":amd: (MI355) Entrypoints Integration (API Server Generate)" # TBD - timeout_in_minutes: 40 +- label: ":amd: (MI355) NixlConnector PD + Spec Decode acceptance" # TBD + timeout_in_minutes: 45 mirror_hardwares: [amdexperimental, amdproduction, amdgfx950nightly, amdmi355] dind: false - agent_pool: mi355_1 - fast_check: true + agent_pool: mi355_2 + num_gpus: 2 optional: true working_dir: "/vllm-workspace/tests" source_file_dependencies: - - vllm/ - - tests/tool_use - - tests/entrypoints/tool_parsers - - tests/entrypoints/anthropic - - tests/entrypoints/generate + - 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: - - export VLLM_WORKER_MULTIPROC_METHOD=spawn - - pytest -v -s tool_use - - pytest -v -s entrypoints/tool_parsers - - pytest -v -s entrypoints/generate - - pytest -v -s entrypoints/anthropic + - 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) Entrypoints Integration (Speech to Text)" # TBD - timeout_in_minutes: 50 - mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi355] +- label: ":amd: (MI355) Distributed NixlConnector PD accuracy" # TBD + timeout_in_minutes: 60 + mirror_hardwares: [amdexperimental, amdproduction, amdgfx950nightly, amdmi355] dind: false - agent_pool: mi355_1 - fast_check: true + agent_pool: mi355_4 + num_gpus: 4 + optional: true working_dir: "/vllm-workspace/tests" source_file_dependencies: - - vllm/ - - tests/entrypoints/speech_to_text + - vllm/distributed/kv_transfer/kv_connector/v1/nixl/ + - tests/v1/kv_connector/nixl_integration/ + - vllm/platforms/rocm.py commands: - - export VLLM_WORKER_MULTIPROC_METHOD=spawn - - pytest -v -s entrypoints/speech_to_text + - 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) Entrypoints Integration (Multimodal)" - timeout_in_minutes: 40 - mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi355] +- label: ":amd: (MI355) Distributed AITER NixlConnector PD accuracy" + timeout_in_minutes: 60 + mirror_hardwares: [amdexperimental, amdproduction, amdgfx950nightly, amdmi355] dind: false - agent_pool: mi355_1 - fast_check: true + agent_pool: mi355_4 + num_gpus: 4 + optional: true working_dir: "/vllm-workspace/tests" source_file_dependencies: - - vllm/ - - tests/entrypoints/multimodal + - 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: - - export VLLM_WORKER_MULTIPROC_METHOD=spawn - - pytest -v -s entrypoints/multimodal + - 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) Entrypoints Integration (Pooling)" # TBD - timeout_in_minutes: 55 +- label: ":amd: (MI355) Distributed MooncakeConnector HIP PD accuracy" + timeout_in_minutes: 30 mirror_hardwares: [amdexperimental, amdproduction, amdgfx950nightly, amdmi355] dind: false - agent_pool: mi355_1 - fast_check: true + agent_pool: mi355_4 + num_gpus: 4 + optional: true working_dir: "/vllm-workspace/tests" source_file_dependencies: - - vllm/ - - tests/entrypoints/pooling + - 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: - - export VLLM_WORKER_MULTIPROC_METHOD=spawn - - pytest -v -s entrypoints/pooling - -#----------------------------------------------------------- mi355 · evals -----------------------------------------------------------# + - 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) GPQA Eval (GPT-OSS)" # TBD +- 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_2 - num_gpus: 2 + 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) 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,--mamba-cache-mode align,--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) Distributed Features" # TBD + timeout_in_minutes: 90 + mirror_hardwares: [amdexperimental, amdproduction, amdgfx950nightly, amdmi355] + dind: false + agent_pool: mi355_2 + num_gpus: 2 + optional: true + working_dir: "/vllm-workspace/" + source_file_dependencies: + - csrc/custom_quickreduce.cu + - csrc/ops.h + - csrc/torch_bindings.cpp + - vllm/distributed/ + - vllm/model_executor/layers/ + - vllm/entrypoints/llm.py + - vllm/config/parallel.py + - vllm/model_executor/layers/fused_moe/ + - vllm/v1/engine/ + - vllm/v1/executor/ + - vllm/v1/worker/ + - vllm/v1/distributed/ + - vllm/v1/attention/backends/ + - vllm/v1/attention/selector.py + - vllm/_aiter_ops.py + - vllm/_custom_ops.py + - vllm/platforms/rocm.py + - vllm/envs.py + - examples/offline_inference/data_parallel.py + - tests/distributed/test_context_parallel.py + - tests/distributed/test_rocm_aiter_custom_ar.py + - tests/distributed/test_rocm_quick_reduce.py + - tests/distributed/test_quick_all_reduce.py + - tests/v1/e2e/general/test_rocm_aiter_custom_ar.py + - tests/v1/distributed/test_dbo.py + - tests/utils.py + - examples/features/data_parallel/data_parallel_offline.py + - examples/rl/rlhf_async_new_apis.py + - tests/distributed/test_weight_transfer.py + - tests/distributed/test_packed_tensor.py + commands: + - pytest -v -s tests/distributed/test_context_parallel.py + - VLLM_ALLOW_INSECURE_SERIALIZATION=1 python3 examples/rl/rlhf_async_new_apis.py + - VLLM_LOGGING_LEVEL=DEBUG python3 examples/features/data_parallel/data_parallel_offline.py --model=Qwen/Qwen1.5-MoE-A2.7B -tp=1 -dp=2 --max-model-len=2048 --all2all-backend=deepep_high_throughput + - VLLM_LOGGING_LEVEL=DEBUG python3 examples/features/data_parallel/data_parallel_offline.py --model=Qwen/Qwen1.5-MoE-A2.7B -tp=1 -dp=2 --max-model-len=2048 --all2all-backend=allgather_reducescatter --disable-nccl-for-dp-synchronization + - pytest -v -s tests/v1/distributed/test_dbo.py + - VLLM_ALLOW_INSECURE_SERIALIZATION=1 pytest -v -s tests/distributed/test_weight_transfer.py + - pytest -v -s tests/distributed/test_packed_tensor.py + - pytest -v -s tests/distributed/test_rocm_aiter_custom_ar.py + - pytest -v -s tests/v1/e2e/general/test_rocm_aiter_custom_ar.py + - pytest -v -s tests/distributed/test_rocm_quick_reduce.py + - pytest -v -s tests/distributed/test_quick_all_reduce.py + +- label: ":amd: (MI355) Fusion E2E TP2 Quick" # TBD + timeout_in_minutes: 60 + mirror_hardwares: [amdexperimental, amdproduction, amdgfx950nightly, amdmi355] + dind: false + agent_pool: mi355_2 + num_gpus: 2 + optional: true + working_dir: "/vllm-workspace/" + source_file_dependencies: + - csrc/quantization/ + - vllm/compilation/ + - vllm/distributed/ + - vllm/model_executor/ + - vllm/v1/attention/ + - tests/compile/fusions_e2e/ + - vllm/_aiter_ops.py + - vllm/platforms/rocm.py + commands: + - export VLLM_TEST_CLEAN_GPU_MEMORY=1 + - >- + VLLM_ROCM_USE_AITER=1 + VLLM_ROCM_USE_AITER_CUSTOM_AR=1 + pytest -v -s tests/compile/fusions_e2e/test_tp2_ar_rms.py + -k 'test_tp2_ar_rms_fp8_fusions and inductor_partition and + not +quant_fp8 and not +rms_norm and Llama-3 and + ROCM_AITER_UNIFIED_ATTN' + - >- + VLLM_ROCM_USE_AITER=1 + VLLM_ROCM_USE_AITER_CUSTOM_AR=1 + pytest -v -s tests/compile/fusions_e2e/test_tp2_ar_rms.py + -k 'test_tp2_ar_rms_fp8_fusions and inductor_partition and + +quant_fp8 and +rms_norm and qwen3 and + ROCM_AITER_UNIFIED_ATTN' + - >- + VLLM_ROCM_USE_AITER=1 + VLLM_ROCM_USE_AITER_CUSTOM_AR=1 + pytest -v -s tests/compile/fusions_e2e/test_tp2_ar_rms.py + -k 'test_tp2_ar_rms_fp8_fusions and inductor_partition and + not +quant_fp8 and not +rms_norm and DeepSeek-Coder and + ROCM_AITER_MLA' + +#-------------------------------------------------------- mi355 · entrypoints --------------------------------------------------------# + +- label: ":amd: (MI355) Entrypoints Integration (API Server) %N" + timeout_in_minutes: 65 + parallelism: 4 + mirror_hardwares: [amdexperimental, amdproduction, amdgfx950nightly, amdmi355] + dind: false + agent_pool: mi355_1 + optional: true + fast_check: true + working_dir: "/vllm-workspace/tests" + source_file_dependencies: + - vllm/ + - tests/entrypoints/serve + - tests/entrypoints/scale_out + commands: + - export VLLM_WORKER_MULTIPROC_METHOD=spawn + - pytest -v -s entrypoints/serve --ignore=entrypoints/serve/dev/rpc --num-shards=$$BUILDKITE_PARALLEL_JOB_COUNT --shard-id=$$BUILDKITE_PARALLEL_JOB + - if [ "$$BUILDKITE_PARALLEL_JOB" = "1" ]; then PYTHONPATH=/vllm-workspace pytest -v -s entrypoints/serve/dev/rpc; fi + - pytest -v -s entrypoints/scale_out --num-shards=$$BUILDKITE_PARALLEL_JOB_COUNT --shard-id=$$BUILDKITE_PARALLEL_JOB + +- label: ":amd: (MI355) Entrypoints Integration (OpenAI API completion)" # TBD + timeout_in_minutes: 65 + mirror_hardwares: [amdexperimental, amdproduction, amdgfx950nightly, amdmi355] + dind: false + agent_pool: mi355_1 + fast_check: true + optional: true + working_dir: "/vllm-workspace/tests" + source_file_dependencies: + - vllm/ + - tests/entrypoints/openai + commands: + - export VLLM_WORKER_MULTIPROC_METHOD=spawn + - pytest -v -s entrypoints/openai/completion --ignore=entrypoints/openai/completion/test_tensorizer_entrypoint.py + - pytest -v -s entrypoints/openai --ignore=entrypoints/openai/completion --ignore=entrypoints/openai/chat_completion --ignore=entrypoints/openai/responses --ignore=entrypoints/openai/correctness + +- label: ":amd: (MI355) Entrypoints Integration (OpenAI API chat_completion)" # TBD + timeout_in_minutes: 70 + mirror_hardwares: [amdexperimental, amdproduction, amdgfx950nightly, amdmi355] + dind: false + agent_pool: mi355_1 + fast_check: true + optional: true + working_dir: "/vllm-workspace/tests" + source_file_dependencies: + - vllm/ + - tests/entrypoints/openai + commands: + - export VLLM_WORKER_MULTIPROC_METHOD=spawn + - pytest -v -s entrypoints/openai/chat_completion + +- label: ":amd: (MI355) Entrypoints Integration (API Server Generate)" # TBD + timeout_in_minutes: 65 + mirror_hardwares: [amdexperimental, amdproduction, amdgfx950nightly, amdmi355] + dind: false + agent_pool: mi355_1 + fast_check: true + optional: true + working_dir: "/vllm-workspace/tests" + source_file_dependencies: + - vllm/ + - tests/tool_use + - tests/entrypoints/tool_parsers + - tests/entrypoints/anthropic + - tests/entrypoints/generate + - tests/entrypoints/cohere + commands: + - pytest -v -s tool_use + - pytest -v -s entrypoints/tool_parsers + - pytest -v -s entrypoints/generate + - pytest -v -s entrypoints/anthropic + - pytest -v -s entrypoints/cohere + +- label: ":amd: (MI355) Entrypoints Integration (Speech to Text)" # TBD + timeout_in_minutes: 60 + mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi355] + dind: false + agent_pool: mi355_1 + fast_check: true + working_dir: "/vllm-workspace/tests" + source_file_dependencies: + - vllm/ + - tests/entrypoints/speech_to_text + optional: true + commands: + - export VLLM_WORKER_MULTIPROC_METHOD=spawn + - pytest -v -s entrypoints/speech_to_text + +- label: ":amd: (MI355) Entrypoints Integration (Multimodal)" + timeout_in_minutes: 55 + mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi355] + dind: false + agent_pool: mi355_1 + fast_check: true + working_dir: "/vllm-workspace/tests" + source_file_dependencies: + - vllm/ + - tests/entrypoints/multimodal + optional: true + commands: + - export VLLM_WORKER_MULTIPROC_METHOD=spawn + - pytest -v -s entrypoints/multimodal + +- label: ":amd: (MI355) Entrypoints Integration (Pooling) Shard %N" # TBD + timeout_in_minutes: 65 + mirror_hardwares: [amdexperimental, amdproduction, amdgfx950nightly, amdmi355] + dind: false + agent_pool: mi355_1 + fast_check: true + parallelism: 4 + working_dir: "/vllm-workspace/tests" + source_file_dependencies: + - vllm/ + - tests/entrypoints/pooling + optional: true + commands: + - export VLLM_WORKER_MULTIPROC_METHOD=spawn + - pytest -v -s entrypoints/pooling --num-shards=$$BUILDKITE_PARALLEL_JOB_COUNT --shard-id=$$BUILDKITE_PARALLEL_JOB + +#----------------------------------------------------------- mi355 · evals -----------------------------------------------------------# + +- label: ":amd: (MI355) GPQA Eval (GPT-OSS)" # TBD + timeout_in_minutes: 40 + 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: @@ -3328,6 +3959,51 @@ steps: - uv pip install --system 'gpt-oss[eval]==0.0.5' - pytest -s -v evals/gpt_oss/test_gpqa_correctness.py --config-list-file=configs/models-gfx950.txt +- label: ":amd: (MI355) LM Eval Watermarking" + timeout_in_minutes: 45 + mirror_hardwares: [amdexperimental, amdproduction, amdgfx950nightly, amdmi355] + dind: false + agent_pool: mi355_1 + num_gpus: 1 + optional: true + working_dir: "/vllm-workspace/tests" + source_file_dependencies: + - .buildkite/test_areas/lm_eval.yaml + - tests/evals/gsm8k/ + - vllm/config/watermarking.py + - vllm/sampling_params.py + - vllm/v1/watermarking/ + - vllm/v1/worker/gpu/ + - vllm/platforms/rocm.py + commands: + - export VLLM_WORKER_MULTIPROC_METHOD=spawn + - pytest -s -v evals/gsm8k/test_gsm8k_correctness.py --config-list-file=configs/models-watermark.txt + +- label: ":amd: (MI355) LM Eval Watermark Feature Combination" + timeout_in_minutes: 60 + 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: + - .buildkite/test_areas/lm_eval.yaml + - tests/evals/gsm8k/ + - vllm/config/speculative.py + - vllm/config/vllm.py + - vllm/config/watermarking.py + - vllm/sampling_params.py + - vllm/model_executor/models/qwen3_dspark.py + - vllm/model_executor/models/qwen3_dflash.py + - vllm/model_executor/warmup/ + - vllm/v1/watermarking/ + - vllm/v1/worker/gpu/ + - vllm/platforms/rocm.py + commands: + - export VLLM_WORKER_MULTIPROC_METHOD=spawn + - pytest -s -v evals/gsm8k/test_gsm8k_correctness.py --config-list-file=configs/models-watermark-dspark-tp2.txt + - label: ":amd: (MI355) LM Eval Qwen3-5 Models" # TBD timeout_in_minutes: 40 mirror_hardwares: [amdexperimental, amdproduction, amdgfx950nightly, amdmi355] @@ -3349,10 +4025,136 @@ steps: - vllm/_aiter_ops.py - vllm/platforms/rocm.py commands: - - pytest -s -v evals/gsm8k/test_gsm8k_correctness.py --config-list-file=configs/models-qwen35-mi355.txt + - pytest -s -v evals/gsm8k/test_gsm8k_correctness.py --config-list-file=configs/models-qwen35-mi355.txt + +- label: ":amd: (MI355) Qwen3-8-Flash-Next-FP8 Accuracy Eval" + 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: + - csrc/libtorch_stable/gdn/ + - tests/evals/qwen4_exp/ + - vllm/model_executor/layers/mamba/ + - vllm/models/qwen4_exp/ + - vllm/transformers_utils/configs/qwen4_exp.py + - vllm/v1/attention/backends/short_conv_attn.py + - vllm/platforms/rocm.py + commands: + - export VLLM_WORKER_MULTIPROC_METHOD=spawn + - uv pip install --system 'evalscope==1.10.0' + - pytest -s -v evals/qwen4_exp/test_accuracy.py --config-list-file=configs/models-rocm.txt + +- label: ":amd: (MI355) LM Eval TurboQuant k8v4" + timeout_in_minutes: 30 + mirror_hardwares: [amdexperimental, amdproduction, amdgfx950nightly, amdmi355] + dind: false + agent_pool: mi355_1 + num_gpus: 1 + optional: true + working_dir: "/vllm-workspace/tests" + source_file_dependencies: + - tests/evals/gsm8k/ + - tests/utils.py + - tests/conftest.py + - vllm/model_executor/layers/quantization/turboquant/ + - vllm/v1/attention/backends/turboquant_attn.py + - vllm/v1/attention/backends/triton_attn.py + - vllm/v1/attention/ops/triton_unified_attention.py + - vllm/v1/attention/selector.py + - vllm/v1/attention/ops/triton_turboquant_decode.py + - vllm/v1/attention/ops/triton_turboquant_store.py + - vllm/v1/attention/ops/flydsl_turboquant_decode.py + - vllm/v1/attention/ops/flydsl_kernels/ + - vllm/v1/attention/ops/turboquant_soa/ + - vllm/platforms/rocm.py + commands: + - pytest -s -v evals/gsm8k/test_gsm8k_correctness.py --config-list-file=evals/gsm8k/configs/models-turboquant-k8v4-rocm.txt + +- label: ":amd: (MI355) LM Eval TurboQuant t4nc" + timeout_in_minutes: 30 + mirror_hardwares: [amdexperimental, amdproduction, amdgfx950nightly, amdmi355] + dind: false + agent_pool: mi355_1 + num_gpus: 1 + optional: true + working_dir: "/vllm-workspace/tests" + source_file_dependencies: + - tests/evals/gsm8k/ + - tests/utils.py + - tests/conftest.py + - vllm/model_executor/layers/quantization/turboquant/ + - vllm/v1/attention/backends/turboquant_attn.py + - vllm/v1/attention/backends/triton_attn.py + - vllm/v1/attention/ops/triton_unified_attention.py + - vllm/v1/attention/selector.py + - vllm/v1/attention/ops/triton_turboquant_decode.py + - vllm/v1/attention/ops/triton_turboquant_store.py + - vllm/v1/attention/ops/flydsl_turboquant_decode.py + - vllm/v1/attention/ops/flydsl_kernels/ + - vllm/v1/attention/ops/turboquant_soa/ + - vllm/platforms/rocm.py + commands: + - pytest -s -v evals/gsm8k/test_gsm8k_correctness.py --config-list-file=evals/gsm8k/configs/models-turboquant-t4nc-rocm.txt + +- label: ":amd: (MI355) LM Eval TurboQuant k3v4nc" + timeout_in_minutes: 30 + mirror_hardwares: [amdexperimental, amdproduction, amdgfx950nightly, amdmi355] + dind: false + agent_pool: mi355_1 + num_gpus: 1 + optional: true + working_dir: "/vllm-workspace/tests" + source_file_dependencies: + - tests/evals/gsm8k/ + - tests/utils.py + - tests/conftest.py + - vllm/model_executor/layers/quantization/turboquant/ + - vllm/v1/attention/backends/turboquant_attn.py + - vllm/v1/attention/backends/triton_attn.py + - vllm/v1/attention/ops/triton_unified_attention.py + - vllm/v1/attention/selector.py + - vllm/v1/attention/ops/triton_turboquant_decode.py + - vllm/v1/attention/ops/triton_turboquant_store.py + - vllm/v1/attention/ops/flydsl_turboquant_decode.py + - vllm/v1/attention/ops/flydsl_kernels/ + - vllm/v1/attention/ops/turboquant_soa/ + - vllm/platforms/rocm.py + commands: + - pytest -s -v evals/gsm8k/test_gsm8k_correctness.py --config-list-file=evals/gsm8k/configs/models-turboquant-k3v4nc-rocm.txt + +- label: ":amd: (MI355) LM Eval TurboQuant t3nc" + timeout_in_minutes: 30 + mirror_hardwares: [amdexperimental, amdproduction, amdgfx950nightly, amdmi355] + dind: false + agent_pool: mi355_1 + num_gpus: 1 + optional: true + working_dir: "/vllm-workspace/tests" + source_file_dependencies: + - .buildkite/test-amd.yaml + - tests/evals/gsm8k/ + - tests/utils.py + - tests/conftest.py + - vllm/model_executor/layers/quantization/turboquant/ + - vllm/v1/attention/backends/turboquant_attn.py + - vllm/v1/attention/backends/triton_attn.py + - vllm/v1/attention/ops/triton_unified_attention.py + - vllm/v1/attention/selector.py + - vllm/v1/attention/ops/triton_turboquant_decode.py + - vllm/v1/attention/ops/triton_turboquant_store.py + - vllm/v1/attention/ops/flydsl_turboquant_decode.py + - vllm/v1/attention/ops/flydsl_kernels/ + - vllm/v1/attention/ops/turboquant_soa/ + - vllm/platforms/rocm.py + commands: + - pytest -s -v evals/gsm8k/test_gsm8k_correctness.py --config-list-file=evals/gsm8k/configs/models-turboquant-t3nc-rocm.txt -- label: ":amd: (MI355) LM Eval Small Models" # TBD - timeout_in_minutes: 45 +- label: ":amd: (MI355) LM Eval Small Models Distributed" + timeout_in_minutes: 55 mirror_hardwares: [amdexperimental, amdproduction, amdgfx950nightly, amdmi355] dind: false agent_pool: mi355_2 @@ -3368,9 +4170,29 @@ steps: - vllm/v1/attention/selector.py - vllm/_aiter_ops.py - vllm/platforms/rocm.py + - tests/evals/gsm8k/configs/models-mi3xx-fp8-and-mixed.txt commands: - pytest -s -v evals/gsm8k/test_gsm8k_correctness.py --config-list-file=configs/models-mi3xx-fp8-and-mixed.txt +- label: ":amd: (MI355) MoE Refactor Integration Shard %N" + timeout_in_minutes: 105 + mirror_hardwares: [amdexperimental, amdproduction, amdgfx950nightly, amdmi355] + dind: false + agent_pool: mi355_2 + num_gpus: 2 + parallelism: 4 + optional: true + working_dir: "/vllm-workspace/tests" + source_file_dependencies: + - vllm/platforms/rocm.py + - tests/evals/gsm8k/ + - vllm/model_executor/layers/fused_moe/ + - vllm/model_executor/layers/quantization/ + - vllm/_aiter_ops.py + commands: + - export VLLM_WORKER_MULTIPROC_METHOD=spawn + - pytest -s -v evals/gsm8k/test_gsm8k_correctness.py --config-list-file=evals/gsm8k/configs/moe-refactor/config-rocm-shard-$$BUILDKITE_PARALLEL_JOB.txt + - label: ":amd: (MI355) Qwen3-30B-A3B-FP8-block Sync EPLB Accuracy" # TBD timeout_in_minutes: 25 mirror_hardwares: [amdexperimental, amdproduction, amdgfx950nightly, amdmi355] @@ -3414,7 +4236,7 @@ steps: - pytest -s -v test_lm_eval_correctness.py --config-list-file=configs/models-large-rocm-tp4.txt --tp-size=4 - label: ":amd: (MI355) LM Eval Large Models" # TBD - timeout_in_minutes: 35 + timeout_in_minutes: 65 mirror_hardwares: [amdexperimental, amdproduction, amdgfx950nightly, amdmi355] dind: false agent_pool: mi355_8 @@ -3439,7 +4261,7 @@ steps: #--------------------------------------------------------- mi355 · examples ----------------------------------------------------------# - label: ":amd: (MI355) Examples" # TBD - timeout_in_minutes: 40 + timeout_in_minutes: 75 mirror_hardwares: [amdexperimental, amdproduction, amdgfx950nightly, amdmi355] dind: false agent_pool: mi355_1 @@ -3449,23 +4271,24 @@ steps: - vllm/multimodal - examples/ - vllm/platforms/rocm.py + optional: true commands: - - pip install tensorizer # Basic + # Multi-modal models + # Pooling models + # Features demo + - pip install --no-deps tensorizer - python3 basic/offline_inference/chat.py - python3 basic/offline_inference/generate.py --model facebook/opt-125m - python3 basic/offline_inference/generate.py --model meta-llama/Llama-2-13b-chat-hf --cpu-offload-gb 10 - python3 basic/offline_inference/classify.py - python3 basic/offline_inference/embed.py - python3 basic/offline_inference/score.py - # Multi-modal models - python3 generate/multimodal/audio_language_offline.py --seed 0 - python3 generate/multimodal/vision_language_offline.py --seed 0 - python3 generate/multimodal/vision_language_multi_image_offline.py --seed 0 - python3 generate/multimodal/encoder_decoder_multimodal_offline.py --model-type whisper --seed 0 - # Pooling models - python3 pooling/embed/vision_embedding_offline.py --seed 0 - # Features demo - python3 features/automatic_prefix_caching/prefix_caching_offline.py - python3 deployment/llm_engine_example.py - python3 features/tensorize_vllm_model.py --model facebook/opt-125m serialize --serialized-directory /tmp/ --suffix v1 && python3 features/tensorize_vllm_model.py --model facebook/opt-125m deserialize --path-to-tensors /tmp/vllm/facebook/opt-125m/v1/model.tensors @@ -3474,6 +4297,23 @@ steps: #---------------------------------------------------------- mi355 · kernels ----------------------------------------------------------# +- label: ":amd: (MI355) Attention DiffKV Kernels" + timeout_in_minutes: 35 + mirror_hardwares: [amdexperimental, amdproduction, amdgfx950nightly, amdmi355] + dind: false + agent_pool: mi355_1 + num_gpus: 1 + optional: true + working_dir: "/vllm-workspace/tests" + source_file_dependencies: + - vllm/v1/attention/ops/triton_unified_attention_diffkv.py + - vllm/v1/attention/backends/triton_attn_diffkv.py + - tests/kernels/attention/test_triton_unified_attention.py + - tests/kernels/attention/test_triton_unified_attention_diffkv.py + - vllm/platforms/rocm.py + commands: + - pytest -v -s kernels/attention/test_triton_unified_attention_diffkv.py + - label: ":amd: (MI355) Core Operation Kernels Shard %N" # TBD timeout_in_minutes: 40 mirror_hardwares: [amdexperimental, amdproduction, amdgfx950nightly, amdmi355] @@ -3492,8 +4332,10 @@ steps: - vllm/model_executor/layers/rotary_embedding/ - vllm/_aiter_ops.py - vllm/platforms/rocm.py + - tests/kernels/test_fused_qk_norm_rope_gate.py + optional: true commands: - - pytest -v -s kernels/core --ignore=kernels/core/test_minimax_reduce_rms.py kernels/test_concat_mla_q.py kernels/test_top_k_per_row.py kernels/test_rocm_fp32_router_gemm.py --shard-id=$$BUILDKITE_PARALLEL_JOB --num-shards=$$BUILDKITE_PARALLEL_JOB_COUNT + - pytest -v -s kernels/core --ignore=kernels/core/test_minimax_reduce_rms.py kernels/test_fused_qk_norm_rope_gate.py kernels/test_concat_mla_q.py kernels/test_top_k_per_row.py kernels/test_rocm_fp32_router_gemm.py --shard-id=$$BUILDKITE_PARALLEL_JOB --num-shards=$$BUILDKITE_PARALLEL_JOB_COUNT - label: ":amd: (MI355) MiniMax Reduce RMS Kernels" # TBD timeout_in_minutes: 45 @@ -3510,6 +4352,12 @@ steps: - vllm/platforms/rocm.py - tests/kernels/core/test_minimax_reduce_rms.py - tests/utils.py + - csrc/libtorch_stable/minimax_reduce_rms_kernel.cu + - csrc/libtorch_stable/minimax_reduce_rms_kernel.h + - csrc/libtorch_stable/ops.h + - csrc/libtorch_stable/torch_bindings.cpp + - vllm/model_executor/layers/mamba/linear/minimax_linear_attn.py + optional: true commands: - pytest -v -s kernels/core/test_minimax_reduce_rms.py @@ -3533,17 +4381,21 @@ steps: - vllm/v1/attention/selector.py - vllm/platforms/rocm.py - vllm/_aiter_ops.py + - examples/basic/offline_inference/chat.py + - tests/kernels/attention/test_attention_selector.py + optional: true commands: - - rocm-smi + - amd-smi - python3 examples/basic/offline_inference/chat.py --attention-backend TRITON_ATTN - pytest -v -s tests/kernels/attention/test_attention_selector.py - - pytest -v -s tests/kernels/attention/test_rocm_aiter_mla_decode_metadata.py -- label: ":amd: (MI355) MLA Kernels" # TBD - timeout_in_minutes: 25 +- label: ":amd: (MI355) MLA Kernels" + timeout_in_minutes: 40 mirror_hardwares: [amdexperimental, amdproduction, amdgfx950nightly, amdmi355] dind: false agent_pool: mi355_1 + num_gpus: 1 + optional: true working_dir: "/vllm-workspace/" source_file_dependencies: - vllm/v1/attention/backends/mla/rocm_aiter_mla.py @@ -3555,14 +4407,30 @@ steps: - tests/kernels/attention/test_rocm_aiter_mla_fp8_support.py - tests/kernels/attention/test_rocm_aiter_mla_op_registration.py commands: - - rocm-smi + - amd-smi - pytest -v -s tests/kernels/attention/test_rocm_aiter_mla_decode_metadata.py - pytest -v -s tests/kernels/attention/test_rocm_aiter_mla_decode.py - pytest -v -s tests/kernels/attention/test_rocm_aiter_mla_fp8_support.py - pytest -v -s tests/kernels/attention/test_rocm_aiter_mla_op_registration.py +- label: ":amd: (MI355) DeepSeek V4 Fused Kernels" + timeout_in_minutes: 60 + mirror_hardwares: [amdexperimental, amdproduction, amdgfx950nightly, amdmi355] + dind: false + agent_pool: mi355_1 + num_gpus: 1 + optional: true + working_dir: "/vllm-workspace/tests" + source_file_dependencies: + - csrc/libtorch_stable/fused_deepseek_v4_qnorm_rope_kv_insert_kernel.cu + - vllm/models/deepseek_v4/common/ops/ + - tests/kernels/test_fused_deepseek_v4_qnorm_rope_kv_insert.py + - vllm/platforms/rocm.py + commands: + - pytest -v -s kernels/test_fused_deepseek_v4_qnorm_rope_kv_insert.py + - label: ":amd: (MI355) Attention Kernels Shard %N" # TBD - timeout_in_minutes: 40 + timeout_in_minutes: 90 mirror_hardwares: [amdexperimental, amdproduction, amdgfx950nightly, amdmi355] dind: false agent_pool: mi355_1 @@ -3576,11 +4444,13 @@ steps: - vllm/_aiter_ops.py - vllm/envs.py - vllm/platforms/rocm.py + - vllm/utils/flashinfer.py + optional: true commands: - - pytest -v -s kernels/attention --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 --shard-id=$$BUILDKITE_PARALLEL_JOB --num-shards=$$BUILDKITE_PARALLEL_JOB_COUNT - label: ":amd: (MI355) MoE Kernels Shard %N" # TBD - timeout_in_minutes: 45 + timeout_in_minutes: 55 mirror_hardwares: [amdexperimental, amdproduction, amdgfx950nightly, amdmi355] dind: false agent_pool: mi355_1 @@ -3600,6 +4470,7 @@ steps: - vllm/config - vllm/_aiter_ops.py - vllm/platforms/rocm.py + optional: true commands: - pytest -v -s kernels/moe --ignore=kernels/moe/test_modular_oai_triton_moe.py @@ -3636,11 +4507,45 @@ steps: - vllm/utils/torch_utils.py - vllm/platforms/ - vllm/_aiter_ops.py + - csrc/quantization/cutlass_w8a8/moe/ + - vllm/distributed/device_communicators/ + - vllm/config commands: - - pytest -v -s kernels/moe/test_moe_layer.py + # MoRI graph cases run in the Fault Tolerance group's healthy EP baseline. + - pytest -v -s kernels/moe/test_moe_layer.py -k "not test_moe_layer_mori_graph" -- label: ":amd: (MI355) Quantization Kernels" # TBD - timeout_in_minutes: 50 +- label: ":amd: (MI355) MoRI EP Numerics" + timeout_in_minutes: 20 + mirror_hardwares: [amdexperimental, amdproduction, amdgfx950nightly, amdmi355] + dind: false + agent_pool: mi355_2 + num_gpus: 2 + working_dir: "/vllm-workspace/tests" + source_file_dependencies: + - tests/kernels/moe/test_mori_moe.py + - tests/kernels/moe/utils.py + - tests/kernels/moe/modular_kernel_tools/parallel_utils.py + - tests/kernels/utils.py + - tests/utils.py + - tests/conftest.py + - vllm/utils/torch_utils.py + - vllm/model_executor/layers/fused_moe/ + - vllm/model_executor/layers/quantization/ + - vllm/distributed/ + - vllm/config/ + - vllm/forward_context.py + - vllm/v1/worker/workspace.py + - vllm/platforms/ + - vllm/_aiter_ops.py + - requirements/rocm-test.txt + optional: true + commands: + - amd-smi + - VLLM_ROCM_USE_AITER=1 VLLM_ROCM_USE_AITER_MOE=1 VLLM_ROCM_USE_AITER_FUSION_SHARED_EXPERTS=0 pytest -v -s kernels/moe/test_mori_moe.py + +- label: ":amd: (MI355) Quantization Kernels Shard %N" + timeout_in_minutes: 120 + parallelism: 6 mirror_hardwares: [amdexperimental, amdproduction, amdgfx950nightly, amdmi355] dind: false agent_pool: mi355_1 @@ -3659,8 +4564,34 @@ steps: - vllm/platforms/rocm.py - vllm/model_executor/kernels/ - vllm/v1/attention/backends/rocm_aiter_fa.py + - vllm/config/ + - tests/kernels/quantization/test_rocm_skinny_gemms.py + optional: true + commands: + - pytest -v -s kernels/quantization -k "not test_nvfp4_dense_emulation" --shard-id=$$BUILDKITE_PARALLEL_JOB --num-shards=$$BUILDKITE_PARALLEL_JOB_COUNT + +- label: ":amd: (MI355) NVFP4 Dense Emulation Shard %N" + timeout_in_minutes: 10 + parallelism: 3 + mirror_hardwares: [amdexperimental, amdproduction, amdgfx950nightly, amdmi355] + dind: false + agent_pool: mi355_1 + working_dir: "/vllm-workspace/tests" + source_file_dependencies: + - tests/kernels/quantization/test_nvfp4_emulation.py + - tests/kernels/quantization/nvfp4_utils.py + - tests/conftest.py + - vllm/model_executor/kernels/linear/ + - vllm/model_executor/layers/quantization/utils/nvfp4_emulation_utils.py + - vllm/config/kernel.py + - vllm/platforms/ + - vllm/envs.py + - vllm/utils/torch_utils.py + - requirements/rocm-test.txt + optional: true commands: - - pytest -v -s kernels/quantization + - amd-smi + - pytest -v -s kernels/quantization/test_nvfp4_emulation.py -k test_nvfp4_dense_emulation --shard-id=$$BUILDKITE_PARALLEL_JOB --num-shards=$$BUILDKITE_PARALLEL_JOB_COUNT - label: ":amd: (MI355) FP8 MoE Kernels" # TBD timeout_in_minutes: 40 @@ -3675,6 +4606,13 @@ steps: - vllm/_aiter_ops.py - vllm/platforms/rocm.py - vllm/envs.py + - tests/kernels/moe/test_gpt_oss_triton_kernels.py + - tests/kernels/moe/test_modular_oai_triton_moe.py + - tests/kernels/moe/test_moe.py + - tests/kernels/moe/test_block_int8.py + - tests/kernels/moe/test_triton_moe_no_act_mul.py + - tests/kernels/moe/test_triton_moe_ptpc_fp8.py + optional: true commands: - pytest -v -s kernels/moe/test_gpt_oss_triton_kernels.py - pytest -v -s kernels/moe/test_modular_oai_triton_moe.py @@ -3684,7 +4622,7 @@ steps: - pytest -v -s kernels/moe/test_triton_moe_ptpc_fp8.py - label: ":amd: (MI355) DeepEP FP8 MoE Kernels" # TBD - timeout_in_minutes: 45 + timeout_in_minutes: 55 mirror_hardwares: [amdexperimental, amdproduction, amdgfx950nightly, amdmi355] dind: false agent_pool: mi355_2 @@ -3702,24 +4640,100 @@ steps: commands: - pytest -v -s kernels/moe/test_deepep_moe.py +#------------------------------------------------------ mi355 · models / basic -------------------------------------------------------# + +- label: ":amd: (MI355) GLM5Next" + timeout_in_minutes: 35 + mirror_hardwares: [amdexperimental, amdproduction, amdgfx950nightly, amdmi355] + dind: false + agent_pool: mi355_1 + num_gpus: 1 + optional: true + working_dir: "/vllm-workspace/tests" + source_file_dependencies: + - vllm/models/glm5next/ + - tests/models/glm5next/ + - vllm/platforms/rocm.py + commands: + - pytest -v -s models/glm5next + +- label: ":amd: (MI355) Basic Models (Extra Initialization) Shard %N" # TBD + timeout_in_minutes: 60 + mirror_hardwares: [amdexperimental, amdproduction, amdgfx950nightly, amdmi355] + dind: false + agent_pool: mi355_1 + parallelism: 6 + working_dir: "/vllm-workspace/tests" + source_file_dependencies: + - vllm/model_executor/models/ + - vllm/model_executor/layers/ + - tests/models/test_initialization.py + - tests/models/registry.py + - vllm/_aiter_ops.py + - vllm/platforms/rocm.py + optional: true + commands: + - pytest -v -s models/test_initialization.py -k 'not test_can_initialize_small_subset' --num-shards=$$BUILDKITE_PARALLEL_JOB_COUNT --shard-id=$$BUILDKITE_PARALLEL_JOB + #----------------------------------------------------- mi355 · models / language -----------------------------------------------------# -- label: ":amd: (MI355) Language Models (Extended Generation)" # TBD +- label: ":amd: (MI355) Language Models (Hybrid) Shard %N" # TBD timeout_in_minutes: 65 mirror_hardwares: [amdexperimental, amdproduction, amdgfx950nightly, amdmi355] dind: false agent_pool: mi355_1 + parallelism: 2 + optional: true + working_dir: "/vllm-workspace/tests" + source_file_dependencies: + - 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) Granite Language Model Compatibility" + timeout_in_minutes: 80 + mirror_hardwares: [amdexperimental, amdproduction, amdgfx950nightly, amdmi355] + dind: false + agent_pool: mi355_1 + num_gpus: 1 + optional: true + working_dir: "/vllm-workspace/tests" + source_file_dependencies: + - vllm/ + - "!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) Language Models (Extended Generation)" # TBD + timeout_in_minutes: 95 + mirror_hardwares: [amdexperimental, amdproduction, amdgfx950nightly, amdmi355] + dind: false + agent_pool: mi355_1 working_dir: "/vllm-workspace/tests" source_file_dependencies: - vllm/ - 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) Language Models (Extended Pooling)" # TBD - timeout_in_minutes: 65 +- label: ":amd: (MI355) Language Models (Extended Pooling) Shard %N" + timeout_in_minutes: 95 + parallelism: 4 mirror_hardwares: [amdexperimental, amdproduction, amdgfx950nightly, amdmi355] dind: false agent_pool: mi355_1 @@ -3729,10 +4743,10 @@ steps: - vllm/ - tests/models/language/pooling commands: - - pytest -v -s models/language/pooling -m 'not core_model' + - pytest -v -s models/language/pooling -m 'not core_model' --num-shards=$$BUILDKITE_PARALLEL_JOB_COUNT --shard-id=$$BUILDKITE_PARALLEL_JOB - label: ":amd: (MI355) Language Models (PPL)" # TBD - timeout_in_minutes: 35 + timeout_in_minutes: 65 mirror_hardwares: [amdexperimental, amdproduction, amdgfx950nightly, amdmi355] dind: false agent_pool: mi355_1 @@ -3757,6 +4771,8 @@ steps: - vllm/v1/attention/ops/ - vllm/platforms/rocm.py - tests/models/language/generation_ppl_test + - vllm/model_executor/layers/fla/ops/ + - vllm/ commands: - pytest -v -s models/language/generation_ppl_test @@ -3769,6 +4785,7 @@ steps: source_file_dependencies: - vllm/ - tests/models/language + optional: true commands: - pip freeze | grep -E 'torch' - pytest -v -s models/language -m 'core_model and (not slow_test)' @@ -3776,7 +4793,7 @@ steps: #---------------------------------------------------- mi355 · models / multimodal ----------------------------------------------------# - label: ":amd: (MI355) Multimodal Models (Extended Generation 1)" # TBD - timeout_in_minutes: 60 + timeout_in_minutes: 90 mirror_hardwares: [amdexperimental, amdproduction, amdgfx950nightly, amdmi355] dind: false agent_pool: mi355_1 @@ -3791,7 +4808,7 @@ steps: - pytest -v -s models/multimodal/test_mapping.py - label: ":amd: (MI355) Multimodal Models (Extended Generation 3)" # TBD - timeout_in_minutes: 40 + timeout_in_minutes: 90 mirror_hardwares: [amdexperimental, amdproduction, amdgfx950nightly, amdmi355] dind: false agent_pool: mi355_1 @@ -3804,7 +4821,7 @@ steps: - pytest -v -s models/multimodal/generation/test_common.py -m 'split(group=1) and not core_model' - label: ":amd: (MI355) Multimodal Models (Extended Pooling)" # TBD - timeout_in_minutes: 40 + timeout_in_minutes: 75 mirror_hardwares: [amdexperimental, amdproduction, amdgfx950nightly, amdmi355] dind: false agent_pool: mi355_1 @@ -3817,7 +4834,7 @@ steps: - pytest -v -s models/multimodal/pooling -m 'not core_model' - label: ":amd: (MI355) Multimodal Models (Standard) 1: qwen2" # TBD - timeout_in_minutes: 35 + timeout_in_minutes: 65 mirror_hardwares: [amdexperimental, amdproduction, amdgfx950nightly, amdmi355] dind: false agent_pool: mi355_1 @@ -3831,7 +4848,7 @@ steps: - pytest -v -s models/multimodal/generation/test_ultravox.py -m core_model - label: ":amd: (MI355) Multimodal Models (Standard) 4: other + whisper" # TBD - timeout_in_minutes: 55 + timeout_in_minutes: 70 mirror_hardwares: [amdexperimental, amdproduction, amdgfx950nightly, amdmi355] dind: false agent_pool: mi355_1 @@ -3843,13 +4860,13 @@ steps: commands: - pytest -v -s models/multimodal -m core_model --ignore models/multimodal/generation/test_common.py --ignore models/multimodal/generation/test_ultravox.py --ignore models/multimodal/generation/test_qwen2_5_vl.py --ignore models/multimodal/generation/test_qwen2_vl.py --ignore models/multimodal/generation/test_whisper.py --ignore models/multimodal/generation/test_mm_prefix_lm.py --ignore models/multimodal/generation/test_memory_leak.py --ignore models/multimodal/generation/test_vit_cudagraph.py --ignore models/multimodal/processing - pytest -v -s models/multimodal/generation/test_vit_cudagraph.py -m core_model - - pytest -v -s models/multimodal/generation/test_memory_leak.py -m core_model + - pytest models/multimodal/generation/test_memory_leak.py -m core_model - cd .. && VLLM_WORKER_MULTIPROC_METHOD=spawn pytest -v -s tests/models/multimodal/generation/test_whisper.py -m core_model #----------------------------------------------------- mi355 · models / quantized -----------------------------------------------------# - label: ":amd: (MI355) Quantized Models" # TBD - timeout_in_minutes: 50 + timeout_in_minutes: 80 mirror_hardwares: [amdexperimental, amdproduction, amdgfx950nightly, amdmi355] dind: false agent_pool: mi355_1 @@ -3860,13 +4877,16 @@ steps: - vllm/_aiter_ops.py - vllm/platforms/rocm.py - vllm/model_executor/model_loader/ + optional: true commands: + - unset VLLM_USE_V2_MODEL_RUNNER - pytest -v -s models/quantization #------------------------------------------------------- mi355 · quantization --------------------------------------------------------# -- label: ":amd: (MI355) Quantization" # TBD - timeout_in_minutes: 80 +- label: ":amd: (MI355) Quantization Shard %N" + timeout_in_minutes: 115 + parallelism: 4 mirror_hardwares: [amdexperimental, amdproduction, amdgfx950nightly, amdmi355] dind: false agent_pool: mi355_1 @@ -3877,10 +4897,16 @@ steps: - tests/quantization - vllm/_aiter_ops.py - vllm/platforms/rocm.py + - vllm/model_executor/layers/fused_moe/oracle/unquantized.py + - vllm/model_executor/layers/fused_moe/unquantized_fused_moe_method.py + - tests/rocm/test_moe_weight_replay.py + optional: true commands: + - unset VLLM_USE_V2_MODEL_RUNNER - uv pip install --system torchao==0.17.0 - uv pip install --system conch-triton-kernels - - VLLM_TEST_FORCE_LOAD_FORMAT=auto pytest -v -s quantization/ --ignore quantization/test_blackwell_moe.py + - VLLM_TEST_FORCE_LOAD_FORMAT=auto pytest -v -s quantization/ --ignore quantization/test_blackwell_moe.py --shard-id=$$BUILDKITE_PARALLEL_JOB --num-shards=$$BUILDKITE_PARALLEL_JOB_COUNT + - if [ "$$BUILDKITE_PARALLEL_JOB" = "0" ]; then pytest -v -s rocm/test_moe_weight_replay.py; fi # - label: Quantized MoE Test (B200-MI355) # TBD # timeout_in_minutes: 180 @@ -3909,10 +4935,67 @@ steps: # commands: # - pytest -s -v tests/quantization/test_gfx950_moe.py +#--------------------------------------------------------- mi355 · samplers ----------------------------------------------------------# + +- label: ":amd: (MI355) Samplers Multimodal Beam Search" + timeout_in_minutes: 45 + mirror_hardwares: [amdexperimental, amdproduction, amdgfx950nightly, amdmi355] + dind: false + agent_pool: mi355_1 + num_gpus: 1 + optional: true + working_dir: "/vllm-workspace/tests" + source_file_dependencies: + - vllm/model_executor/layers + - vllm/sampling_metadata.py + - vllm/v1/sample/ + - vllm/entrypoints/generate/beam_search/ + - tests/samplers + - tests/conftest.py + - vllm/_aiter_ops.py + - vllm/platforms/rocm.py + commands: + - pytest -v -s samplers/test_beam_search.py::test_beam_search_passes_multimodal_data + #------------------------------------------------------------ mi355 · v1 -------------------------------------------------------------# +- label: ":amd: (MI355) E2E Core Large Memory" + timeout_in_minutes: 65 + mirror_hardwares: [amdexperimental, amdproduction, amdgfx950nightly, amdmi355] + dind: false + agent_pool: mi355_1 + num_gpus: 1 + optional: true + working_dir: "/vllm-workspace/tests" + source_file_dependencies: + - vllm/v1/ + - tests/v1/e2e/general/ + - vllm/platforms/rocm.py + commands: + - pytest -v -s v1/e2e/general/test_kv_sharing_fast_prefill.py + - pytest -v -s v1/e2e/general/test_mamba_prefix_cache.py::test_mamba_prefix_cache_mrv2 v1/e2e/general/test_mamba_prefix_cache.py::test_mamba_prefix_cache_mrv2_async + +- label: ":amd: (MI355) Extract Hidden States Integration Single GPU" + timeout_in_minutes: 45 + mirror_hardwares: [amdexperimental, amdproduction, amdgfx950nightly, amdmi355] + dind: false + agent_pool: mi355_1 + num_gpus: 1 + optional: true + working_dir: "/vllm-workspace" + source_file_dependencies: + - vllm/v1/spec_decode/extract_hidden_states.py + - vllm/model_executor/models/extract_hidden_states.py + - vllm/transformers_utils/configs/extract_hidden_states.py + - vllm/distributed/kv_transfer/kv_connector/v1/example_hidden_states_connector.py + - tests/v1/kv_connector/extract_hidden_states_integration + - vllm/platforms/rocm.py + commands: + - export VLLM_WORKER_MULTIPROC_METHOD=spawn + - pytest -v -s -m 'not distributed' tests/v1/kv_connector/extract_hidden_states_integration + - label: ":amd: (MI355) V1 Attention Shard %N" # TBD - timeout_in_minutes: 35 + timeout_in_minutes: 125 mirror_hardwares: [amdexperimental, amdproduction, amdgfx950nightly, amdmi355] dind: false agent_pool: mi355_1 @@ -3926,11 +5009,12 @@ steps: - vllm/_aiter_ops.py - vllm/envs.py - vllm/platforms/rocm.py + optional: true commands: - pytest -v -s v1/attention --shard-id=$$BUILDKITE_PARALLEL_JOB --num-shards=$$BUILDKITE_PARALLEL_JOB_COUNT -- label: ":amd: (MI355) V1 Core + KV + Metrics" # TBD - timeout_in_minutes: 55 +- label: ":amd: (MI355) V1 Core" + timeout_in_minutes: 60 mirror_hardwares: [amdexperimental, amdproduction, amdgfx950nightly, amdmi355] dind: false agent_pool: mi355_1 @@ -3946,24 +5030,243 @@ steps: - tests/v1/kv_connector/unit - tests/v1/metrics - tests/entrypoints/openai/correctness/test_lmeval.py + - vllm/config/ + - vllm/distributed/ + - vllm/engine/ + - vllm/entrypoints/pooling/ + - vllm/inputs/ + - vllm/lora/ + - vllm/model_executor/ + - vllm/multimodal/ + - vllm/outputs.py + - vllm/platforms/ + - vllm/pooling_params.py + - vllm/profiler/ + - vllm/sampling_params.py + - vllm/tokenizers/ + - vllm/transformers_utils/ + - vllm/utils/ + - vllm/v1/ + - tests/v1/simple_kv_offload + - tests/v1/streaming_input + - tests/v1/ec_connector/unit commands: - - uv pip install --system -r /vllm-workspace/requirements/kv_connectors_rocm.txt + - export VLLM_WORKER_MULTIPROC_METHOD=spawn - pytest -v -s -m 'not cpu_test' v1/core + +- label: ":amd: (MI355) V1 Executor + Worker" + 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/ + - tests/v1/core + - tests/v1/executor + - tests/v1/kv_offload + - tests/v1/worker + - tests/v1/cudagraph + - tests/v1/kv_connector/unit + - tests/v1/metrics + - tests/entrypoints/openai/correctness/test_lmeval.py + - vllm/config/ + - vllm/distributed/ + - vllm/engine/ + - vllm/entrypoints/pooling/ + - vllm/inputs/ + - vllm/lora/ + - vllm/model_executor/ + - vllm/multimodal/ + - vllm/outputs.py + - vllm/platforms/ + - vllm/pooling_params.py + - vllm/profiler/ + - vllm/sampling_params.py + - vllm/tokenizers/ + - vllm/transformers_utils/ + - vllm/utils/ + - vllm/v1/ + - tests/v1/simple_kv_offload + - tests/v1/streaming_input + - tests/v1/ec_connector/unit + commands: + - export VLLM_WORKER_MULTIPROC_METHOD=spawn - pytest -v -s v1/executor - - pytest -v -s v1/kv_offload - pytest -v -s v1/worker - - pytest -v -s v1/cudagraph/test_encoder_cudagraph.py - - pytest -v -s -m 'not cpu_test' v1/kv_connector/unit + - pytest -v -s v1/streaming_input + +- label: ":amd: (MI355) V1 KV Offload" + 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/ + - tests/v1/core + - tests/v1/executor + - tests/v1/kv_offload + - tests/v1/worker + - tests/v1/cudagraph + - tests/v1/kv_connector/unit + - tests/v1/metrics + - tests/entrypoints/openai/correctness/test_lmeval.py + - vllm/config/ + - vllm/distributed/ + - vllm/engine/ + - vllm/entrypoints/pooling/ + - vllm/inputs/ + - vllm/lora/ + - vllm/model_executor/ + - vllm/multimodal/ + - vllm/outputs.py + - vllm/platforms/ + - vllm/pooling_params.py + - vllm/profiler/ + - vllm/sampling_params.py + - vllm/tokenizers/ + - vllm/transformers_utils/ + - vllm/utils/ + - vllm/v1/ + - tests/v1/simple_kv_offload + - tests/v1/streaming_input + - tests/v1/ec_connector/unit + commands: + - export VLLM_WORKER_MULTIPROC_METHOD=spawn + - bash /vllm-workspace/.buildkite/scripts/install-kv-offload.sh + - pytest -v -s v1/kv_offload + - pytest -v -s v1/simple_kv_offload + +- label: ":amd: (MI355) V1 KV Connectors Shard %N" + timeout_in_minutes: 60 + parallelism: 4 + mirror_hardwares: [amdexperimental, amdproduction, amdgfx950nightly, amdmi355] + dind: false + agent_pool: mi355_1 + optional: true + working_dir: /vllm-workspace/tests + source_file_dependencies: + - vllm/ + - tests/v1/core + - tests/v1/executor + - tests/v1/kv_offload + - tests/v1/worker + - tests/v1/cudagraph + - tests/v1/kv_connector/unit + - tests/v1/metrics + - tests/entrypoints/openai/correctness/test_lmeval.py + - vllm/config/ + - vllm/distributed/ + - vllm/engine/ + - vllm/entrypoints/pooling/ + - vllm/inputs/ + - vllm/lora/ + - vllm/model_executor/ + - vllm/multimodal/ + - vllm/outputs.py + - vllm/platforms/ + - vllm/pooling_params.py + - vllm/profiler/ + - vllm/sampling_params.py + - vllm/tokenizers/ + - vllm/transformers_utils/ + - vllm/utils/ + - vllm/v1/ + - tests/v1/simple_kv_offload + - tests/v1/streaming_input + - tests/v1/ec_connector/unit + commands: + - bash /vllm-workspace/.buildkite/scripts/install-kv-connectors.sh + - export VLLM_WORKER_MULTIPROC_METHOD=spawn + - pytest -v -s -m 'not cpu_test' v1/kv_connector/unit --num-shards=$$BUILDKITE_PARALLEL_JOB_COUNT --shard-id=$$BUILDKITE_PARALLEL_JOB + - pytest -v -s -m 'not cpu_test' v1/ec_connector/unit --num-shards=$$BUILDKITE_PARALLEL_JOB_COUNT --shard-id=$$BUILDKITE_PARALLEL_JOB + +- label: ":amd: (MI355) V1 Metrics + LM Eval" + timeout_in_minutes: 65 + mirror_hardwares: [amdexperimental, amdproduction, amdgfx950nightly, amdmi355] + dind: false + agent_pool: mi355_1 + optional: true + working_dir: /vllm-workspace/tests + source_file_dependencies: + - vllm/ + - tests/v1/core + - tests/v1/executor + - tests/v1/kv_offload + - tests/v1/worker + - tests/v1/cudagraph + - tests/v1/kv_connector/unit + - tests/v1/metrics + - tests/entrypoints/openai/correctness/test_lmeval.py + - vllm/config/ + - vllm/distributed/ + - vllm/engine/ + - vllm/entrypoints/pooling/ + - vllm/inputs/ + - vllm/lora/ + - vllm/model_executor/ + - vllm/multimodal/ + - vllm/outputs.py + - vllm/platforms/ + - vllm/pooling_params.py + - vllm/profiler/ + - vllm/sampling_params.py + - vllm/tokenizers/ + - vllm/transformers_utils/ + - vllm/utils/ + - vllm/v1/ + - tests/v1/simple_kv_offload + - tests/v1/streaming_input + - tests/v1/ec_connector/unit + commands: + - export GIT_CONFIG_COUNT=1 + - export GIT_CONFIG_KEY_0=http.version + - export GIT_CONFIG_VALUE_0=HTTP/1.1 + - export GIT_TERMINAL_PROMPT=0 + - export VLLM_WORKER_MULTIPROC_METHOD=spawn - pytest -v -s -m 'not cpu_test' v1/metrics - pip install -U git+https://github.com/vllm-project/lm-evaluation-harness.git@streaming-api - pytest -v -s entrypoints/openai/correctness/test_lmeval.py::test_lm_eval_accuracy_v1_engine -- label: ":amd: (MI355) V1 Sample + Logits" # TBD +- label: ":amd: (MI355) CUDAGraph" timeout_in_minutes: 55 mirror_hardwares: [amdexperimental, amdproduction, amdgfx950nightly, amdmi355] dind: false agent_pool: mi355_1 optional: true + working_dir: /vllm-workspace/tests + source_file_dependencies: + - vllm/ + - tests/v1/core + - tests/v1/executor + - tests/v1/kv_offload + - tests/v1/worker + - tests/v1/cudagraph + - tests/v1/kv_connector/unit + - tests/v1/metrics + - tests/entrypoints/openai/correctness/test_lmeval.py + - vllm/v1/cudagraph_dispatcher.py + - vllm/config/compilation.py + - vllm/compilation + - vllm/platforms/rocm.py + - vllm/v1/worker/encoder_cudagraph.py + - vllm/v1/worker/encoder_cudagraph_defs.py + commands: + - pytest -v -s v1/cudagraph/test_cudagraph_dispatch.py + - pytest -v -s v1/cudagraph/test_cudagraph_mode.py + - pytest -v -s v1/cudagraph/test_breakable_cudagraph.py + - pytest -v -s v1/cudagraph/test_encoder_cudagraph.py + + +- label: ":amd: (MI355) V1 Sample" + timeout_in_minutes: 70 + mirror_hardwares: [amdexperimental, amdproduction, amdgfx950nightly, amdmi355] + dind: false + agent_pool: mi355_1 + optional: true working_dir: "/vllm-workspace/tests" source_file_dependencies: - vllm/ @@ -3972,15 +5275,56 @@ steps: - tests/v1/test_oracle.py - tests/v1/test_request.py - tests/v1/test_outputs.py + - vllm/config/ + - vllm/distributed/ + - vllm/engine/ + - vllm/inputs/ + - vllm/logger.py + - vllm/model_executor/ + - vllm/platforms/ + - vllm/sampling_params.py + - vllm/transformers_utils/ + - vllm/utils/ + - vllm/v1/ commands: + - export VLLM_WORKER_MULTIPROC_METHOD=spawn - pytest -v -s v1/sample + +- label: ":amd: (MI355) V1 Logits + Oracle" + timeout_in_minutes: 70 + mirror_hardwares: [amdexperimental, amdproduction, amdgfx950nightly, amdmi355] + dind: false + agent_pool: mi355_1 + optional: true + working_dir: /vllm-workspace/tests + source_file_dependencies: + - vllm/ + - tests/v1/sample + - tests/v1/logits_processors + - tests/v1/test_oracle.py + - tests/v1/test_request.py + - tests/v1/test_outputs.py + - vllm/config/ + - vllm/distributed/ + - vllm/engine/ + - vllm/inputs/ + - vllm/logger.py + - vllm/model_executor/ + - vllm/platforms/ + - vllm/sampling_params.py + - vllm/transformers_utils/ + - vllm/utils/ + - vllm/v1/ + commands: + - export VLLM_WORKER_MULTIPROC_METHOD=spawn - pytest -v -s v1/logits_processors - pytest -v -s v1/test_oracle.py - pytest -v -s v1/test_request.py - pytest -v -s v1/test_outputs.py + - label: ":amd: (MI355) V1 Spec Decode" # TBD - timeout_in_minutes: 45 + timeout_in_minutes: 50 mirror_hardwares: [amdexperimental, amdproduction, amdgfx950nightly, amdmi355] dind: false agent_pool: mi355_1 @@ -3988,11 +5332,24 @@ steps: source_file_dependencies: - vllm/ - tests/v1/spec_decode + - vllm/config/ + - vllm/distributed/ + - vllm/inputs/ + - vllm/model_executor/ + - vllm/models/qwen4_exp/ + - vllm/platforms/ + - vllm/sampling_params.py + - vllm/transformers_utils/ + - vllm/utils/ + - vllm/v1/ + optional: true commands: + - export VLLM_WORKER_MULTIPROC_METHOD=spawn - pytest -v -s -m 'not slow_test' v1/spec_decode -- label: ":amd: (MI355) Spec Decode AL MTP + Other Acceptance" # TBD - timeout_in_minutes: 180 +- label: ":amd: (MI355) Spec Decode AL MTP + Other Acceptance Nightly" + timeout_in_minutes: 55 + parallelism: 3 mirror_hardwares: [amdexperimental, amdproduction, amdgfx950nightly, amdmi355] dind: false agent_pool: mi355_1 @@ -4016,50 +5373,90 @@ steps: - tests/utils.py - tests/v1/e2e/spec_decode/ - vllm/platforms/rocm.py + - vllm/v1/worker/gpu/spec_decode/ + - tests/v1/e2e/spec_decode/conftest.py commands: - - pytest -v -s v1/e2e/spec_decode/acceptance_rates/mtp_other/ + - if [ "$$BUILDKITE_PARALLEL_JOB" = "0" ]; then pytest -v -s v1/e2e/spec_decode/acceptance_rates/mtp_other/ --deselect "tests/v1/e2e/spec_decode/acceptance_rates/mtp_other/test_mtp.py::test_gemma4_mtp_acceptance_lengths[False]" --deselect "tests/v1/e2e/spec_decode/acceptance_rates/mtp_other/test_mtp.py::test_gemma4_mtp_acceptance_lengths[True]"; fi + - if [ "$$BUILDKITE_PARALLEL_JOB" = "1" ]; then pytest -v -s "v1/e2e/spec_decode/acceptance_rates/mtp_other/test_mtp.py::test_gemma4_mtp_acceptance_lengths[False]"; fi + - if [ "$$BUILDKITE_PARALLEL_JOB" = "2" ]; then pytest -v -s "v1/e2e/spec_decode/acceptance_rates/mtp_other/test_mtp.py::test_gemma4_mtp_acceptance_lengths[True]"; fi -#------------------------------------------------------ mi355 · weight_loading -------------------------------------------------------# +#----------------------------------------------------------- mi355 · misc ------------------------------------------------------------# -- label: ":amd: (MI355) Weight Loading Multi-GPU" # TBD - timeout_in_minutes: 30 +- label: ":amd: (MI355) Regression" # TBD + timeout_in_minutes: 25 mirror_hardwares: [amdexperimental, amdproduction, amdgfx950nightly, amdmi355] dind: false - agent_pool: mi355_2 - num_gpus: 2 + agent_pool: mi355_1 + optional: true working_dir: "/vllm-workspace/tests" source_file_dependencies: - vllm/ - - tests/weight_loading + - tests/test_regression + - vllm/config/ + - vllm/distributed/ + - vllm/engine/ + - vllm/inputs/ + - vllm/model_executor/ + - vllm/multimodal/ + - vllm/platforms/ + - vllm/sampling_params.py + - vllm/transformers_utils/ + - vllm/utils/ + - vllm/v1/ commands: - - bash weight_loading/run_model_weight_loading_test.sh -c weight_loading/models-amd.txt + - pip install 'modelscope<1.38' + - pytest -v -s test_regression.py -- label: ":amd: (MI355) Weight Loading Multi-GPU (Large Models)" # TBD +- label: ':amd: (MI355) Fusion E2E TP2 AsyncTP BF16 Config Sweep' timeout_in_minutes: 30 - mirror_hardwares: [amdexperimental, amdproduction, amdgfx950nightly, amdmi355] + working_dir: /vllm-workspace/ + source_file_dependencies: + - vllm/compilation/ + - vllm/model_executor/layers/linear.py + - vllm/model_executor/layers/utils.py + - vllm/distributed/ + - vllm/v1/attention/backends/triton_attn.py + - tests/compile/fusions_e2e/ + - tests/compile/passes/distributed/test_async_tp.py + - tests/compile/rocm/test_async_tp.py + - tests/compile/correctness_e2e/test_async_tp.py + - vllm/model_executor/kernels/linear/ + - vllm/model_executor/models/qwen3.py + commands: + - rocm-smi + - pytest -v -s tests/compile/rocm/test_async_tp.py + - pytest -v -s tests/compile/fusions_e2e/test_tp2_rocm_async_tp.py + - pytest -v -s tests/compile/correctness_e2e/test_async_tp.py::test_rocm_async_tp_bf16_output_correctness dind: false agent_pool: mi355_2 - working_dir: "/vllm-workspace/tests" num_gpus: 2 + mirror_hardwares: + - amdexperimental + - amdproduction + - amdgfx950nightly + - amdmi355 optional: true - source_file_dependencies: - - vllm/ - - tests/weight_loading - commands: - - bash weight_loading/run_model_weight_loading_test.sh -c weight_loading/models-large-amd.txt - -#----------------------------------------------------------- mi355 · misc ------------------------------------------------------------# -- label: ":amd: (MI355) Regression" # TBD - timeout_in_minutes: 25 +- label: ":amd: (MI355) MRV2 Sampler JIT Warmup" + timeout_in_minutes: 10 mirror_hardwares: [amdexperimental, amdproduction, amdgfx950nightly, amdmi355] dind: false agent_pool: mi355_1 + num_gpus: 1 optional: true - working_dir: "/vllm-workspace/tests" + working_dir: "/vllm-workspace/" source_file_dependencies: - - vllm/ - - tests/test_regression + - tests/jit_monitor/ + - tests/models/utils.py + - tests/models/registry.py + - tests/utils.py + - vllm/v1/worker/gpu/ + - vllm/v1/worker/gpu_worker.py + - vllm/model_executor/warmup/ + - vllm/v1/sample/ops/topk_topp_sampler.py + - vllm/utils/jit_monitor.py + - vllm/config/observability.py + - vllm/platforms/rocm.py + - vllm/envs.py commands: - - pip install 'modelscope<1.38' - - pytest -v -s test_regression.py + - pytest -v -s tests/jit_monitor/test_no_runtime_jit_rocm.py --timeout=300 diff --git a/.buildkite/test_areas/benchmarks.yaml b/.buildkite/test_areas/benchmarks.yaml index 45a9e02fc840..bfa86e5597a4 100644 --- a/.buildkite/test_areas/benchmarks.yaml +++ b/.buildkite/test_areas/benchmarks.yaml @@ -34,3 +34,18 @@ steps: - vllm/v1/attention/ commands: - python3 benchmarks/attention_benchmarks/benchmark.py --backends flash flashinfer --batch-specs "8q1s1k" + mirror: + amd: + label: ":amd: (MI355) Attention Benchmark Smoke" + dind: false + device: mi355_2 + timeout_in_minutes: 25 + depends_on: + - image-build-amd + source_file_dependencies: + - benchmarks/attention_benchmarks/ + - vllm/v1/attention/ + - vllm/_aiter_ops.py + - vllm/platforms/rocm.py + commands: + - python3 benchmarks/attention_benchmarks/benchmark.py --backends ROCM_ATTN ROCM_AITER_FA ROCM_AITER_UNIFIED_ATTN --batch-specs "8q1s1k" diff --git a/.buildkite/test_areas/compile.yaml b/.buildkite/test_areas/compile.yaml index 8eea80068646..2f314aa13893 100644 --- a/.buildkite/test_areas/compile.yaml +++ b/.buildkite/test_areas/compile.yaml @@ -67,6 +67,31 @@ steps: - pytest -v -s tests/compile/passes/test_silu_mul_quant_manual_fusion.py # this runner has 2 GPUs available even though num_devices=2 is not set - pytest -v -s tests/compile/passes/distributed/test_fusion_all_reduce.py + mirror: + amd: + label: ":amd: (MI355) Fusion and Compile FP8" + dind: false + device: mi355_2 + num_devices: 2 + working_dir: "/vllm-workspace/" + timeout_in_minutes: 45 + depends_on: + - image-build-amd + source_file_dependencies: + - vllm/compilation/ + - vllm/model_executor/layers/ + - vllm/v1/attention/ + - vllm/distributed/ + - vllm/_aiter_ops.py + - tests/compile/passes/ + - tests/compile/correctness_e2e/test_attn_quant.py + - vllm/model_executor/kernels/linear/ + - vllm/model_executor/models/llama.py + commands: + - amd-smi + # MLA static FP8 and distributed AR/RMS are owned by the existing ROCm jobs. + - pytest -v -s tests/compile/passes/test_fusion_attn.py tests/compile/passes/test_silu_mul_quant_fusion.py tests/compile/passes/test_silu_mul_quant_manual_fusion.py + - pytest -v -s tests/compile/correctness_e2e/test_attn_quant.py - label: ":nvidia: (H100) Fusion E2E Quick" key: fusion-e2e-quick-h100 @@ -92,7 +117,7 @@ steps: dind: false device: mi355_1 working_dir: "/vllm-workspace/" - timeout_in_minutes: 25 + timeout_in_minutes: 30 depends_on: - image-build-amd source_file_dependencies: @@ -133,7 +158,7 @@ steps: dind: false device: mi355_1 working_dir: "/vllm-workspace/" - timeout_in_minutes: 30 + timeout_in_minutes: 40 depends_on: - image-build-amd source_file_dependencies: @@ -181,6 +206,47 @@ steps: # Run all models and attn backends but only Inductor partition and native custom ops - 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))" - pytest -v -s tests/compile/fusions_e2e/test_tp2_async_tp.py -k "inductor_partition and not +rms_norm and (not +quant_fp8 or +quant_fp8 and (qwen3 or deepseek))" + mirror: + amd: + label: ":amd: (MI355) Fusion E2E TP2 Quick" + dind: false + device: mi355_2 + timeout_in_minutes: 60 + depends_on: + - image-build-amd + working_dir: /vllm-workspace/ + source_file_dependencies: + - csrc/quantization/ + - vllm/compilation/ + - vllm/distributed/ + - vllm/model_executor/ + - vllm/v1/attention/ + - tests/compile/fusions_e2e/ + - vllm/_aiter_ops.py + - vllm/platforms/rocm.py + commands: + - export VLLM_TEST_CLEAN_GPU_MEMORY=1 + - >- + VLLM_ROCM_USE_AITER=1 + VLLM_ROCM_USE_AITER_CUSTOM_AR=1 + pytest -v -s tests/compile/fusions_e2e/test_tp2_ar_rms.py + -k 'test_tp2_ar_rms_fp8_fusions and inductor_partition and + not +quant_fp8 and not +rms_norm and Llama-3 and + ROCM_AITER_UNIFIED_ATTN' + - >- + VLLM_ROCM_USE_AITER=1 + VLLM_ROCM_USE_AITER_CUSTOM_AR=1 + pytest -v -s tests/compile/fusions_e2e/test_tp2_ar_rms.py + -k 'test_tp2_ar_rms_fp8_fusions and inductor_partition and + +quant_fp8 and +rms_norm and qwen3 and + ROCM_AITER_UNIFIED_ATTN' + - >- + VLLM_ROCM_USE_AITER=1 + VLLM_ROCM_USE_AITER_CUSTOM_AR=1 + pytest -v -s tests/compile/fusions_e2e/test_tp2_ar_rms.py + -k 'test_tp2_ar_rms_fp8_fusions and inductor_partition and + not +quant_fp8 and not +rms_norm and DeepSeek-Coder and + ROCM_AITER_MLA' - label: ":nvidia: (H100) Fusion E2E TP2 AR-RMS Config Sweep" key: fusion-e2e-tp2-ar-rms-config-sweep-h100 @@ -221,6 +287,33 @@ steps: - nvidia-smi # Run just llama3 (fp8 & bf16) for all config combinations - pytest -v -s tests/compile/fusions_e2e/test_tp2_async_tp.py -k "llama-3" + mirror: + amd: + label: ':amd: (MI355) Fusion E2E TP2 AsyncTP BF16 Config Sweep' + dind: false + device: mi355_2 + num_devices: 2 + working_dir: /vllm-workspace/ + timeout_in_minutes: 30 + depends_on: + - image-build-amd + source_file_dependencies: + - vllm/compilation/ + - vllm/model_executor/layers/linear.py + - vllm/model_executor/layers/utils.py + - vllm/distributed/ + - vllm/v1/attention/backends/triton_attn.py + - tests/compile/fusions_e2e/ + - tests/compile/passes/distributed/test_async_tp.py + - tests/compile/rocm/test_async_tp.py + - tests/compile/correctness_e2e/test_async_tp.py + - vllm/model_executor/kernels/linear/ + - vllm/model_executor/models/qwen3.py + commands: + - rocm-smi + - pytest -v -s tests/compile/rocm/test_async_tp.py + - pytest -v -s tests/compile/fusions_e2e/test_tp2_rocm_async_tp.py + - pytest -v -s tests/compile/correctness_e2e/test_async_tp.py::test_rocm_async_tp_bf16_output_correctness - label: ":nvidia: (B200) Fusion E2E TP2" key: fusion-e2e-tp2-b200 diff --git a/.buildkite/test_areas/cuda.yaml b/.buildkite/test_areas/cuda.yaml index f0b1f4af2b80..8ac8e1960ade 100644 --- a/.buildkite/test_areas/cuda.yaml +++ b/.buildkite/test_areas/cuda.yaml @@ -23,15 +23,15 @@ steps: dind: false device: mi355_1 working_dir: "/vllm-workspace/tests" - timeout_in_minutes: 20 + timeout_in_minutes: 25 depends_on: - image-build-amd source_file_dependencies: - vllm/ - tests/cuda -- label: ":nvidia: (H200 MIG 35GB) CUDAGraph" - device: h200_35gb +- label: ":nvidia: (H200 MIG 18GB) CUDAGraph" + device: h200_18gb key: cudagraph timeout_in_minutes: 30 source_file_dependencies: @@ -49,8 +49,9 @@ steps: mirror: amd: label: ":amd: (MI250) CUDAGraph" + dind: false device: mi250_1 - timeout_in_minutes: 45 + timeout_in_minutes: 55 depends_on: - image-build-amd source_file_dependencies: diff --git a/.buildkite/test_areas/disaggregated.yaml b/.buildkite/test_areas/disaggregated.yaml index 0100b99993ef..1258336009d7 100644 --- a/.buildkite/test_areas/disaggregated.yaml +++ b/.buildkite/test_areas/disaggregated.yaml @@ -16,9 +16,9 @@ steps: - bash v1/kv_connector/nixl_integration/config_sweep_accuracy_test.sh mirror: amd: - label: ":amd: (MI300) Distributed NixlConnector PD accuracy" + label: ":amd: (MI355) Distributed NixlConnector PD accuracy" dind: false - device: mi300_4 + device: mi355_4 timeout_in_minutes: 60 depends_on: - image-build-amd @@ -42,6 +42,29 @@ steps: commands: - bash /vllm-workspace/.buildkite/scripts/install-kv-connectors.sh - FLASHINFER=1 bash v1/kv_connector/nixl_integration/config_sweep_accuracy_test.sh + mirror: + amd: + label: ":amd: (MI355) Distributed AITER NixlConnector PD accuracy" + dind: false + device: mi355_4 + num_devices: 4 + timeout_in_minutes: 60 + working_dir: "/vllm-workspace/tests" + depends_on: + - image-build-amd + 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 + # Preserve all six model/TP pairs with native AITER attention. + - pytest -v -s v1/kv_connector/nixl_integration/test_accuracy_rocm.py - label: ":nvidia: (L4) Push NixlConnector PP prefill PD accuracy" key: push-nixlconnector-pp-prefill-pd-accuracy-4-gpus @@ -71,9 +94,9 @@ steps: - DP_EP=1 bash v1/kv_connector/nixl_integration/config_sweep_accuracy_test.sh mirror: amd: - label: ":amd: (MI300) DP EP Distributed NixlConnector PD accuracy" + label: ":amd: (MI355) DP EP Distributed NixlConnector PD accuracy" dind: false - device: mi300_4 + device: mi355_4 timeout_in_minutes: 40 depends_on: - image-build-amd @@ -130,7 +153,7 @@ steps: label: ":amd: (MI300) Hybrid SSM NixlConnector PD accuracy" dind: false device: mi300_4 - timeout_in_minutes: 55 + timeout_in_minutes: 60 depends_on: - image-build-amd source_file_dependencies: @@ -172,6 +195,23 @@ steps: commands: - bash /vllm-workspace/.buildkite/scripts/install-kv-connectors.sh - bash v1/kv_connector/nixl_integration/run_mamba_prefix_cache_test.sh + mirror: + amd: + label: ":amd: (MI355) Hybrid SSM NixlConnector PD prefix cache" + dind: false + device: mi355_2 + timeout_in_minutes: 25 + depends_on: + - image-build-amd + 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: ":nvidia: (L4) MultiConnector (Nixl+Offloading) PD accuracy" key: multiconnector-nixl-offloading-pd-accuracy-2-gpus @@ -188,6 +228,25 @@ steps: commands: - bash /vllm-workspace/.buildkite/scripts/install-kv-connectors.sh - bash v1/kv_connector/nixl_integration/run_multi_connector_accuracy_test.sh + mirror: + amd: + label: ":amd: (MI355) MultiConnector (Nixl+Offloading) PD accuracy" + dind: false + device: mi355_2 + timeout_in_minutes: 40 + depends_on: + - image-build-amd + 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/ + - requirements/kv_connectors_rocm.txt + - 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: ":nvidia: (A100) NixlConnector PD + Spec Decode acceptance" key: nixlconnector-pd-spec-decode-acceptance-2-gpus @@ -204,9 +263,9 @@ steps: - bash v1/kv_connector/nixl_integration/config_sweep_spec_decode_test.sh mirror: amd: - label: ":amd: (MI300) NixlConnector PD + Spec Decode acceptance" + label: ":amd: (MI355) NixlConnector PD + Spec Decode acceptance" dind: false - device: mi300_2 + device: mi355_2 timeout_in_minutes: 45 depends_on: - image-build-amd @@ -234,6 +293,24 @@ steps: commands: - bash /vllm-workspace/.buildkite/scripts/install-kv-connectors.sh - bash v1/kv_connector/nixl_integration/run_multi_connector_edge_case_test.sh + mirror: + amd: + label: ":amd: (MI355) MultiConnector (Nixl+Offloading) PD edge cases" + dind: false + device: mi355_2 + timeout_in_minutes: 25 + depends_on: + - image-build-amd + 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 # P TP 4 - D DPEP 4 test case for DSv4-Flash - label: ":nvidia: (H200) DSv4-Flash Disaggregated DP EP" @@ -252,6 +329,7 @@ steps: 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" # The V4 indexer only supports block-outermost layouts; let it pick. PREFILLER_KV_LAYOUT: "" @@ -275,7 +353,31 @@ steps: PREFILLER_TP_SIZE: "2" DECODER_TP_SIZE: "2" MODEL_NAMES: "moonshotai/Kimi-Linear-48B-A3B-Instruct" + # Keep TP=2 prefill and TP=1 decode above both hybrid page-size minima. + PREFILL_BLOCK_SIZE: "2048" + DECODE_BLOCK_SIZE: "2048" VLLM_SERVE_EXTRA_ARGS: "--trust-remote-code,--mamba-cache-mode align,--max-model-len 32768" commands: - bash /vllm-workspace/.buildkite/scripts/install-kv-connectors.sh - bash v1/kv_connector/nixl_integration/run_accuracy_test.sh + mirror: + amd: + label: ":amd: (MI355) Kimi-Linear-48B-A3B Disaggregated DP EP" + dind: false + device: mi355_4 + num_devices: 4 + timeout_in_minutes: 105 + working_dir: /vllm-workspace/tests + depends_on: + - image-build-amd + 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 + env: + VLLM_WORKER_MULTIPROC_METHOD: "spawn" + commands: + - uv pip install --system -r /vllm-workspace/requirements/kv_connectors_rocm.txt + - bash v1/kv_connector/nixl_integration/run_accuracy_test.sh diff --git a/.buildkite/test_areas/disaggregated_ec.yaml b/.buildkite/test_areas/disaggregated_ec.yaml index 9100ad0a431e..67bfebaf8cac 100644 --- a/.buildkite/test_areas/disaggregated_ec.yaml +++ b/.buildkite/test_areas/disaggregated_ec.yaml @@ -14,3 +14,22 @@ steps: - tests/entrypoints/scale_out/ec_integration/ commands: - bash entrypoints/scale_out/ec_integration/run_scale_out_ec_e2e_test.sh + mirror: + amd: + label: ":amd: (MI300) Scale-out EC Connector E2E" + dind: false + device: mi300_2 + num_devices: 2 + timeout_in_minutes: 60 + working_dir: /vllm-workspace/tests + depends_on: + - image-build-amd + source_file_dependencies: + - vllm/distributed/ec_transfer/ + - vllm/entrypoints/scale_out/ + - tests/entrypoints/scale_out/ec_integration/ + - vllm/platforms/rocm.py + env: + VLLM_WORKER_MULTIPROC_METHOD: "spawn" + commands: + - bash entrypoints/scale_out/ec_integration/run_scale_out_ec_e2e_test.sh diff --git a/.buildkite/test_areas/disaggregated_mooncake.yaml b/.buildkite/test_areas/disaggregated_mooncake.yaml index 8c061a95ad8f..030351ba5763 100644 --- a/.buildkite/test_areas/disaggregated_mooncake.yaml +++ b/.buildkite/test_areas/disaggregated_mooncake.yaml @@ -19,6 +19,30 @@ steps: commands: - bash /vllm-workspace/.buildkite/scripts/install-kv-connectors.sh - bash v1/kv_connector/mooncake_integration/config_sweep_accuracy_test.sh + mirror: + amd: + label: ":amd: (MI355) Distributed MooncakeConnector HIP PD accuracy" + dind: false + device: mi355_4 + num_devices: 4 + timeout_in_minutes: 30 + working_dir: "/vllm-workspace/tests" + depends_on: + - image-build-amd + 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 + # Same full accuracy/TP sweep, using same-host HIP GPU transport. + - pytest -v -s v1/kv_connector/mooncake_integration/test_accuracy_rocm.py - label: ":nvidia: (L4) Mooncake EC TCP E2E" key: mooncake-ec-tcp-e2e-2-gpus @@ -40,7 +64,14 @@ steps: - vllm/v1/worker/gpu_worker.py - vllm/v1/worker/gpu/ec_connector.py - vllm/v1/worker/gpu/model_runner.py + - vllm/v1/worker/mm_encoder_model_runner.py + - vllm/v1/worker/encoder_cudagraph.py + - vllm/v1/worker/encoder_cudagraph_defs.py + - vllm/v1/attention/ops/vit_attn_wrappers.py + - vllm/v1/worker/gpu/model_states/ - vllm/v1/worker/gpu/mm/ + - tests/v1/cudagraph/test_encoder_cudagraph.py + - tests/models/multimodal/generation/test_vit_cudagraph.py - examples/disaggregated/disaggregated_encoder/ - tests/v1/ec_connector/ - requirements/kv_connectors.txt @@ -49,6 +80,9 @@ steps: PYTHONUNBUFFERED: "1" VLLM_HOST_IP: "127.0.0.1" VLLM_USE_V2_MODEL_RUNNER: "1" + E_CUDAGRAPH_MM_ENCODER: "1" + # FA padding NaNs are tracked separately in #57136. + E_MM_ENCODER_ATTN_BACKEND: "TRITON_ATTN" MOONCAKE_EC_PROTOCOL: "tcp" USE_MM_PROMPTS: "1" SKIP_BASELINE: "0" @@ -73,3 +107,45 @@ steps: PY )" - bash tests/v1/ec_connector/integration/run_epd_mooncake_ec_full_pipeline.sh + mirror: + amd: + label: ":amd: (MI300) Mooncake EC TCP E2E" + dind: false + device: mi300_2 + num_devices: 2 + timeout_in_minutes: 60 + working_dir: /vllm-workspace + depends_on: + - image-build-amd + source_file_dependencies: + - vllm/distributed/ec_transfer/ + - vllm/config/ec_transfer.py + - vllm/config/multimodal.py + - vllm/config/vllm.py + - vllm/multimodal/ + - vllm/v1/core/encoder_cache_manager.py + - vllm/v1/core/sched/ + - vllm/v1/engine/ + - vllm/v1/worker/ec_connector_model_runner_mixin.py + - vllm/v1/worker/gpu_model_runner.py + - vllm/v1/worker/gpu_worker.py + - vllm/v1/worker/gpu/ec_connector.py + - vllm/v1/worker/gpu/model_runner.py + - vllm/v1/worker/mm_encoder_model_runner.py + - vllm/v1/worker/encoder_cudagraph.py + - vllm/v1/worker/encoder_cudagraph_defs.py + - vllm/v1/attention/ops/vit_attn_wrappers.py + - vllm/v1/worker/gpu/model_states/ + - vllm/v1/worker/gpu/mm/ + - tests/v1/cudagraph/test_encoder_cudagraph.py + - tests/models/multimodal/generation/test_vit_cudagraph.py + - examples/disaggregated/disaggregated_encoder/ + - tests/v1/ec_connector/ + - requirements/kv_connectors.txt + - vllm/platforms/rocm.py + - requirements/rocm.txt + env: + VLLM_WORKER_MULTIPROC_METHOD: "spawn" + PYTHON_BIN: "python" + commands: + - bash tests/v1/ec_connector/integration/run_epd_mooncake_ec_full_pipeline.sh diff --git a/.buildkite/test_areas/distributed.yaml b/.buildkite/test_areas/distributed.yaml index 44e96ce63264..15966f8bd1fc 100644 --- a/.buildkite/test_areas/distributed.yaml +++ b/.buildkite/test_areas/distributed.yaml @@ -33,6 +33,29 @@ steps: - pytest -v -s distributed/test_sharded_rdt_plan.py - pytest -v -s distributed/test_sharded_rdt_producer.py - pytest -v -s distributed/test_sharded_rdt_trainer.py + mirror: + amd: + label: ":amd: (MI355) Sharded RDT Weight Transfer" + dind: false + device: mi355_1 + num_devices: 1 + timeout_in_minutes: 45 + working_dir: /vllm-workspace/tests + depends_on: + - image-build-amd + source_file_dependencies: + - vllm/distributed/weight_transfer/ + - tests/distributed/test_sharded_rdt_plan.py + - tests/distributed/test_sharded_rdt_producer.py + - tests/distributed/test_sharded_rdt_trainer.py + - vllm/platforms/rocm.py + env: + VLLM_WORKER_MULTIPROC_METHOD: "spawn" + commands: + - uv pip install --system --no-deps 'ray==2.56.1' + - pytest -v -s distributed/test_sharded_rdt_plan.py + - pytest -v -s distributed/test_sharded_rdt_producer.py + - pytest -v -s distributed/test_sharded_rdt_trainer.py - label: ":nvidia: (L4) Distributed DP Basic" key: distributed-dp-tests-2-gpus @@ -65,9 +88,9 @@ steps: - pytest -v -s v1/e2e/spec_decode/test_sharded_sampling.py mirror: amd: - label: ":amd: (MI300) Distributed DP Basic" + label: ":amd: (MI355) Distributed DP Basic" dind: false - device: mi300_2 + device: mi355_2 timeout_in_minutes: 45 depends_on: - image-build-amd @@ -105,6 +128,29 @@ steps: - export NCCL_CUMEM_HOST_ENABLE=0 - pytest -v -s entrypoints/llm/test_collective_rpc.py - pytest -v -s ./compile/test_wrapper.py + mirror: + amd: + label: ":amd: (MI355) Distributed Compile + RPC" + dind: false + device: mi355_2 + timeout_in_minutes: 65 + depends_on: + - image-build-amd + source_file_dependencies: + - vllm/compilation/ + - vllm/distributed/ + - "!vllm/distributed/kv_transfer/" + - vllm/engine/ + - vllm/executor/ + - vllm/worker/worker_base.py + - vllm/v1/engine/ + - vllm/v1/worker/ + - tests/compile/test_wrapper.py + - tests/entrypoints/llm/test_collective_rpc.py + - vllm/platforms/rocm.py + commands: + - pytest -v -s entrypoints/llm/test_collective_rpc.py + - pytest -v -s ./compile/test_wrapper.py - label: ":nvidia: (L4) Distributed Torchrun + Shutdown" key: distributed-torchrun-shutdown-tests-2-gpus @@ -173,6 +219,41 @@ steps: # default does not fit an L4. - bash /vllm-workspace/.buildkite/scripts/install-kv-connectors.sh - RDT_MODEL=hmellor/tiny-random-DeepseekV2ForCausalLM python3 examples/rl/rlhf_sharded_rdt_small_ep.py + mirror: + amd: + label: ":amd: (MI355) Distributed Torchrun + Examples" + dind: false + device: mi355_4 + timeout_in_minutes: 45 + depends_on: + - image-build-amd + source_file_dependencies: + - vllm/distributed/ + - "!vllm/distributed/kv_transfer/" + - tests/distributed/test_torchrun_example.py + - tests/distributed/test_torchrun_example_moe.py + - examples/rl/ + - examples/features/data_parallel/data_parallel_offline.py + - .buildkite/scripts/install-kv-connectors.sh + - requirements/kv_connectors_rocm.txt + - requirements/test/rocm.in + - requirements/test/rocm.txt + - vllm/platforms/rocm.py + commands: + # Sharded RDT needs Ray >= 2.56; keep the ROCm image's dependencies. + - uv pip install --system --no-deps 'ray==2.56.1' + - export NCCL_CUMEM_HOST_ENABLE=0 + - torchrun --nproc-per-node=4 tests/distributed/test_torchrun_example.py + - PP_SIZE=2 torchrun --nproc-per-node=4 tests/distributed/test_torchrun_example.py + - TP_SIZE=4 torchrun --nproc-per-node=4 tests/distributed/test_torchrun_example_moe.py + - PP_SIZE=2 TP_SIZE=2 torchrun --nproc-per-node=4 tests/distributed/test_torchrun_example_moe.py + - DP_SIZE=4 ENABLE_EP=1 torchrun --nproc-per-node=4 tests/distributed/test_torchrun_example_moe.py + - TP_SIZE=2 DP_SIZE=2 ENABLE_EP=1 torchrun --nproc-per-node=4 tests/distributed/test_torchrun_example_moe.py + - python3 examples/features/data_parallel/data_parallel_offline.py --enforce-eager + - VLLM_ALLOW_INSECURE_SERIALIZATION=1 python3 examples/rl/rlhf_http_nccl.py + - VLLM_ALLOW_INSECURE_SERIALIZATION=1 python3 examples/rl/rlhf_http_ipc.py + - bash /vllm-workspace/.buildkite/scripts/install-kv-connectors.sh + - RDT_MODEL=hmellor/tiny-random-DeepseekV2ForCausalLM python3 examples/rl/rlhf_sharded_rdt_small_ep.py - label: ":nvidia: (L4) Distributed DP Extended" key: distributed-dp-tests-4-gpus @@ -186,6 +267,9 @@ steps: - tests/v1/distributed - tests/v1/engine/test_engine_core_client.py - tests/distributed/test_utils + - tests/distributed/test_engram_dp_shard.py + - vllm/config/engram.py + - vllm/models/deepseek_v41/ commands: # https://github.com/NVIDIA/nccl/issues/1838 - export NCCL_CUMEM_HOST_ENABLE=0 @@ -197,6 +281,7 @@ steps: - TP_SIZE=1 DP_SIZE=4 pytest -v -s v1/distributed/test_hybrid_lb_dp.py - pytest -v -s v1/engine/test_engine_core_client.py::test_kv_cache_events_dp - pytest -v -s distributed/test_utils.py + - pytest -v -s distributed/test_engram_dp_shard.py - label: ":nvidia: (L4) Distributed Compile + Comm" key: distributed-compile-comm-4-gpus @@ -241,6 +326,23 @@ steps: - 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 + mirror: + amd: + label: ":amd: (MI355) Distributed DP + EP" + dind: false + device: mi355_8 + timeout_in_minutes: 50 + depends_on: + - image-build-amd + source_file_dependencies: + - examples/features/torchrun/torchrun_dp_example_offline.py + - vllm/config/parallel.py + - vllm/distributed/ + - "!vllm/distributed/kv_transfer/" + - vllm/v1/engine/llm_engine.py + - vllm/v1/executor/uniproc_executor.py + - vllm/v1/worker/gpu_worker.py + - vllm/platforms/rocm.py - label: ":nvidia: (A100) Distributed" key: distributed-tests-4xa100 @@ -272,6 +374,57 @@ steps: - pytest -v -s tests/v1/distributed/test_dbo.py - VLLM_ALLOW_INSECURE_SERIALIZATION=1 pytest -v -s tests/distributed/test_weight_transfer.py - pytest -v -s tests/distributed/test_packed_tensor.py + mirror: + amd: + label: ':amd: (MI355) Distributed Features' + dind: false + device: mi355_2 + timeout_in_minutes: 90 + working_dir: /vllm-workspace/ + depends_on: + - image-build-amd + source_file_dependencies: + - vllm/distributed/ + - vllm/v1/distributed/ + - vllm/model_executor/layers/fused_moe/ + - vllm/v1/attention/backends/ + - vllm/v1/attention/selector.py + - tests/v1/distributed/test_dbo.py + - tests/distributed/test_context_parallel.py + - examples/features/data_parallel/data_parallel_offline.py + - vllm/_aiter_ops.py + - vllm/platforms/rocm.py + - csrc/custom_quickreduce.cu + - csrc/ops.h + - csrc/torch_bindings.cpp + - vllm/model_executor/layers/ + - vllm/entrypoints/llm.py + - vllm/config/parallel.py + - vllm/v1/engine/ + - vllm/v1/executor/ + - vllm/v1/worker/ + - vllm/_custom_ops.py + - vllm/envs.py + - tests/distributed/test_rocm_aiter_custom_ar.py + - tests/distributed/test_rocm_quick_reduce.py + - tests/distributed/test_quick_all_reduce.py + - tests/v1/e2e/general/test_rocm_aiter_custom_ar.py + - tests/utils.py + - examples/rl/rlhf_async_new_apis.py + - tests/distributed/test_weight_transfer.py + - tests/distributed/test_packed_tensor.py + commands: + - pytest -v -s tests/distributed/test_context_parallel.py + - VLLM_ALLOW_INSECURE_SERIALIZATION=1 python3 examples/rl/rlhf_async_new_apis.py + - VLLM_LOGGING_LEVEL=DEBUG python3 examples/features/data_parallel/data_parallel_offline.py --model=Qwen/Qwen1.5-MoE-A2.7B -tp=1 -dp=2 --max-model-len=2048 --all2all-backend=deepep_high_throughput + - VLLM_LOGGING_LEVEL=DEBUG python3 examples/features/data_parallel/data_parallel_offline.py --model=Qwen/Qwen1.5-MoE-A2.7B -tp=1 -dp=2 --max-model-len=2048 --all2all-backend=allgather_reducescatter --disable-nccl-for-dp-synchronization + - pytest -v -s tests/v1/distributed/test_dbo.py + - VLLM_ALLOW_INSECURE_SERIALIZATION=1 pytest -v -s tests/distributed/test_weight_transfer.py + - pytest -v -s tests/distributed/test_packed_tensor.py + - pytest -v -s tests/distributed/test_rocm_aiter_custom_ar.py + - pytest -v -s tests/v1/e2e/general/test_rocm_aiter_custom_ar.py + - pytest -v -s tests/distributed/test_rocm_quick_reduce.py + - pytest -v -s tests/distributed/test_quick_all_reduce.py - label: ":nvidia: (B200) Distributed" key: distributed-tests-2xb200 @@ -286,17 +439,18 @@ steps: - pytest -v -s tests/v1/distributed/test_dbo.py - pytest -v -s tests/distributed/test_mnnvl_alltoall.py -- label: ":nvidia: (B200) Kimi-K3 GEMM-RS/AR" - key: kimi-k3-gemm-rs-ar-2xb200 +- label: ":nvidia: (B200) GEMM-RS/AR" + key: gemm-rs-ar-2xb200 timeout_in_minutes: 20 device: b200-k8s working_dir: "/vllm-workspace/" num_devices: 2 source_file_dependencies: - - vllm/models/kimi_k3/nvidia/ops/cute_dsl/gemm_rs_ar.py - - tests/kernels/test_kimi_k3_gemm_rs_ar.py + - vllm/model_executor/kernels/linear/cute_dsl/gemm_rs_ar.py + - vllm/cute_utils/ + - tests/kernels/test_gemm_rs_ar.py commands: - - pytest -v -s tests/kernels/test_kimi_k3_gemm_rs_ar.py + - pytest -v -s tests/kernels/test_gemm_rs_ar.py - label: ":nvidia: (L4) Distributed 2-Node" key: 2-node-test-4-gpus @@ -335,6 +489,26 @@ steps: - pytest -v -s distributed/test_pp_cudagraph.py - pytest -v -s distributed/test_pipeline_parallel.py - pytest -v -s v1/distributed/test_pp_dp_v2.py + mirror: + amd: + label: ":amd: (MI250) Pipeline + Context Parallelism" + dind: false + device: mi250_4 + timeout_in_minutes: 90 + depends_on: + - image-build-amd + source_file_dependencies: + - vllm/distributed/ + - "!vllm/distributed/kv_transfer/" + - vllm/engine/ + - vllm/executor/ + - vllm/model_executor/models/ + - vllm/model_executor/layers/ + - vllm/v1/attention/backends/ + - vllm/v1/attention/selector.py + - tests/distributed/ + - vllm/_aiter_ops.py + - vllm/platforms/rocm.py - label: ":nvidia: (L4) RayExecutorV2" key: rayexecutorv2-4-gpus diff --git a/.buildkite/test_areas/docker.yaml b/.buildkite/test_areas/docker.yaml index e88f19ad9793..4c80f26ca52e 100644 --- a/.buildkite/test_areas/docker.yaml +++ b/.buildkite/test_areas/docker.yaml @@ -3,6 +3,7 @@ depends_on: - image-build-cpu steps: - label: ":computer: (CPU) Docker Build Metadata" + key: docker-build-metadata timeout_in_minutes: 20 device: cpu-small source_file_dependencies: @@ -17,6 +18,7 @@ steps: mirror: amd: label: ":amd: (MI250) Docker Build Metadata" + dind: false device: mi250_1 no_gpu: true timeout_in_minutes: 35 diff --git a/.buildkite/test_areas/e2e_integration.yaml b/.buildkite/test_areas/e2e_integration.yaml index 02ba409e3433..12ebee893011 100644 --- a/.buildkite/test_areas/e2e_integration.yaml +++ b/.buildkite/test_areas/e2e_integration.yaml @@ -11,6 +11,28 @@ steps: working_dir: "/vllm-workspace" commands: - bash .buildkite/scripts/scheduled_integration_test/deepseek_v2_lite_ep_eplb.sh 0.25 200 8010 + mirror: + amd: + label: ":amd: (MI355) DeepSeek V2-Lite Sync EPLB Accuracy" + dind: false + device: mi355_4 + timeout_in_minutes: 40 + env: + VLLM_ENGINE_READY_TIMEOUT_S: "1800" + depends_on: + - image-build-amd + source_file_dependencies: + - vllm/model_executor/models/ + - vllm/model_executor/model_loader/ + - vllm/distributed/eplb + - vllm/model_executor/layers/fused_moe/ + - vllm/model_executor/layers/quantization/ + - vllm/v1/attention/backends/ + - vllm/v1/attention/backends/mla/ + - vllm/v1/attention/selector.py + - .buildkite/scripts/scheduled_integration_test/ + - vllm/_aiter_ops.py + - vllm/platforms/rocm.py - label: ":nvidia: (H100) Qwen3-30B-A3B-FP8-block Sync EPLB Accuracy" key: qwen3-30b-a3b-fp8-block-sync-eplb-accuracy-4xh100 @@ -21,6 +43,25 @@ steps: working_dir: "/vllm-workspace" commands: - bash .buildkite/scripts/scheduled_integration_test/qwen30b_a3b_fp8_block_ep_eplb.sh 0.8 200 8020 + mirror: + amd: + label: ":amd: (MI355) Qwen3-30B-A3B-FP8-block Sync EPLB Accuracy TP2" + dind: false + device: mi355_4 + timeout_in_minutes: 30 + depends_on: + - image-build-amd + source_file_dependencies: + - vllm/model_executor/models/ + - vllm/model_executor/model_loader/ + - vllm/model_executor/layers/quantization/ + - vllm/distributed/eplb + - vllm/model_executor/layers/fused_moe/ + - vllm/v1/attention/backends/ + - vllm/v1/attention/selector.py + - .buildkite/scripts/scheduled_integration_test/ + - vllm/_aiter_ops.py + - vllm/platforms/rocm.py - label: ":nvidia: (B200) Qwen3-30B-A3B-FP8-block Sync EPLB Accuracy" key: qwen3-30b-a3b-fp8-block-sync-eplb-accuracy-2xb200 @@ -31,6 +72,25 @@ steps: working_dir: "/vllm-workspace" commands: - bash .buildkite/scripts/scheduled_integration_test/qwen30b_a3b_fp8_block_ep_eplb.sh 0.8 200 8020 2 1 + mirror: + amd: + label: ":amd: (MI355) Qwen3-30B-A3B-FP8-block Sync EPLB Accuracy TP1" + dind: false + device: mi355_2 + timeout_in_minutes: 25 + depends_on: + - image-build-amd + source_file_dependencies: + - vllm/model_executor/models/ + - vllm/model_executor/model_loader/ + - vllm/model_executor/layers/quantization/ + - vllm/model_executor/layers/fused_moe/ + - vllm/distributed/eplb + - vllm/v1/attention/backends/ + - vllm/v1/attention/selector.py + - .buildkite/scripts/scheduled_integration_test/ + - vllm/_aiter_ops.py + - vllm/platforms/rocm.py - label: ":nvidia: (H100) Qwen3-30B-A3B-FP8 DP4 Async EPLB Accuracy" key: qwen3-30b-a3b-fp8-dp4-async-eplb-accuracy @@ -41,6 +101,25 @@ steps: working_dir: "/vllm-workspace" commands: - bash .buildkite/scripts/scheduled_integration_test/qwen30b_a3b_fp8_dp4_async_eplb.sh 0.8 200 8050 + mirror: + amd: + label: ":amd: (MI355) Qwen3-30B-A3B-FP8 DP4 Async EPLB Accuracy" + dind: false + device: mi355_4 + timeout_in_minutes: 30 + depends_on: + - image-build-amd + source_file_dependencies: + - vllm/model_executor/models/ + - vllm/model_executor/model_loader/ + - vllm/model_executor/layers/quantization/ + - vllm/distributed/eplb + - vllm/model_executor/layers/fused_moe/ + - vllm/v1/attention/backends/ + - vllm/v1/attention/selector.py + - .buildkite/scripts/scheduled_integration_test/ + - vllm/_aiter_ops.py + - vllm/platforms/rocm.py - label: ":nvidia: (H100) DeepSeek V2-Lite Prefetch Offload Accuracy" key: deepseek-v2-lite-prefetch-offload-accuracy-h100 @@ -57,7 +136,7 @@ steps: dind: false device: mi355_1 working_dir: "/vllm-workspace" - timeout_in_minutes: 20 + timeout_in_minutes: 30 depends_on: - image-build-amd source_file_dependencies: diff --git a/.buildkite/test_areas/engine.yaml b/.buildkite/test_areas/engine.yaml index 4384484c0954..e38cd925732d 100644 --- a/.buildkite/test_areas/engine.yaml +++ b/.buildkite/test_areas/engine.yaml @@ -52,10 +52,13 @@ steps: mirror: amd: label: ":amd: (MI250) V1 Engine" + dind: false device: mi250_1 timeout_in_minutes: 45 depends_on: - image-build-amd + source_file_dependencies: + - vllm/platforms/rocm.py - label: ":nvidia: (H200 MIG 18GB) E2E Scheduling" key: e2e-scheduling-1-gpu @@ -69,23 +72,29 @@ steps: mirror: amd: label: ":amd: (MI250) E2E Scheduling" + dind: false device: mi250_1 timeout_in_minutes: 55 depends_on: - image-build-amd + source_file_dependencies: + - vllm/platforms/rocm.py -- label: ":nvidia: (H200 MIG 35GB) E2E Core" - device: h200_35gb +- label: ":nvidia: (H200 MIG 18GB) E2E Core" + device: h200_18gb key: e2e-core-1-gpu - timeout_in_minutes: 40 + timeout_in_minutes: 65 source_file_dependencies: - vllm/v1/ - tests/v1/e2e/general/ commands: - - pytest -v -s v1/e2e/general --ignore v1/e2e/general/test_async_scheduling.py + - pytest -v -s v1/e2e/general --ignore v1/e2e/general/test_async_scheduling.py --ignore v1/e2e/general/test_kv_sharing_fast_prefill.py -k "not test_mamba_prefix_cache_mrv2" mirror: amd: label: ":amd: (MI250) E2E Core" + dind: false + commands: + - pytest -v -s v1/e2e/general --ignore v1/e2e/general/test_async_scheduling.py --ignore v1/e2e/general/test_kv_sharing_fast_prefill.py -k "not test_mamba_prefix_cache_mrv2" device: mi250_1 timeout_in_minutes: 50 depends_on: @@ -95,6 +104,35 @@ steps: - tests/v1/e2e/general/ - vllm/platforms/rocm.py +# Keep the large Gemma-3n and Mamba MRV2 warmup allocations on 35GB slices. +- label: ":nvidia: (H200 MIG 35GB) E2E Core Large Memory" + device: h200_35gb + key: e2e-core-large-memory + timeout_in_minutes: 50 + source_file_dependencies: + - vllm/v1/ + - tests/v1/e2e/general/ + commands: + - pytest -v -s v1/e2e/general/test_kv_sharing_fast_prefill.py + - pytest -v -s v1/e2e/general/test_mamba_prefix_cache.py::test_mamba_prefix_cache_mrv2 v1/e2e/general/test_mamba_prefix_cache.py::test_mamba_prefix_cache_mrv2_async + mirror: + amd: + label: ":amd: (MI355) E2E Core Large Memory" + dind: false + device: mi355_1 + num_devices: 1 + timeout_in_minutes: 65 + working_dir: /vllm-workspace/tests + depends_on: + - image-build-amd + source_file_dependencies: + - vllm/v1/ + - tests/v1/e2e/general/ + - vllm/platforms/rocm.py + commands: + - pytest -v -s v1/e2e/general/test_kv_sharing_fast_prefill.py + - pytest -v -s v1/e2e/general/test_mamba_prefix_cache.py::test_mamba_prefix_cache_mrv2 v1/e2e/general/test_mamba_prefix_cache.py::test_mamba_prefix_cache_mrv2_async + - label: ":nvidia: (L4) V1 E2E" key: v1-e2e-2-gpus timeout_in_minutes: 25 # TODO: Fix timeout after we have more confidence in the test stability diff --git a/.buildkite/test_areas/entrypoints.yaml b/.buildkite/test_areas/entrypoints.yaml index 6ad579f82ad3..075b3733caf8 100644 --- a/.buildkite/test_areas/entrypoints.yaml +++ b/.buildkite/test_areas/entrypoints.yaml @@ -2,6 +2,27 @@ group: Entrypoints depends_on: - image-build steps: +- label: ":nvidia: (H200) Initialized Snapshot E2E" + key: initialized-snapshot-e2e + device: h200 + num_devices: 1 + no_plugin: true + optional: true + timeout_in_minutes: 60 + working_dir: "." + env: + SNAPSHOT_E2E_RUN_LIMIT_S: "1200" + NVIDIA_VISIBLE_DEVICES: "0" + source_file_dependencies: + - .buildkite/scripts/initialized-snapshot-e2e.sh + - .buildkite/test_areas/entrypoints.yaml + - docker/Dockerfile + - tools/install_snapshot_runtime.sh + - vllm/entrypoints/cli/ + - vllm/snapshot/ + commands: + - bash .buildkite/scripts/initialized-snapshot-e2e.sh "$IMAGE_TAG" "$BUILDKITE_COMMIT" + - label: ":nvidia: (H200 MIG 35GB) Entrypoints Unit" device: h200_35gb key: entrypoints-unit-tests @@ -21,9 +42,11 @@ steps: label: ":amd: (MI355) Entrypoints Unit" dind: false device: mi355_1 - timeout_in_minutes: 35 + timeout_in_minutes: 50 depends_on: - image-build-amd + source_file_dependencies: + - vllm/platforms/rocm.py - label: ":nvidia: (H200 MIG 35GB) Entrypoints Integration (LLM)" device: h200_35gb @@ -44,7 +67,7 @@ steps: label: ":amd: (MI300) Entrypoints Integration (LLM)" dind: false device: mi300_1 - timeout_in_minutes: 55 + timeout_in_minutes: 90 depends_on: - image-build-amd @@ -73,9 +96,9 @@ steps: depends_on: - image-build-amd -- label: ":nvidia: (H200 MIG 35GB) Entrypoints Integration (API Server OpenAI - Part 1)" +- label: ":nvidia: (H200 MIG 35GB) Entrypoints Integration (OpenAI API completion)" device: h200_35gb - key: entrypoints-integration-api-server-openai-part-1 + key: entrypoints-integration-api-server-openai-completion timeout_in_minutes: 68 working_dir: "/vllm-workspace/tests" source_file_dependencies: @@ -84,19 +107,20 @@ steps: - tests/entrypoints/openai commands: - export VLLM_WORKER_MULTIPROC_METHOD=spawn + - pytest -v -s entrypoints/openai/completion --ignore=entrypoints/openai/completion/test_tensorizer_entrypoint.py - pytest -v -s entrypoints/openai --ignore=entrypoints/openai/completion --ignore=entrypoints/openai/chat_completion --ignore=entrypoints/openai/responses --ignore=entrypoints/openai/correctness mirror: amd: - label: ":amd: (MI300) Entrypoints Integration (API Server OpenAI - Part 1)" + label: ":amd: (MI300) Entrypoints Integration (OpenAI API completion)" dind: false device: mi300_1 timeout_in_minutes: 65 depends_on: - image-build-amd -- label: ":nvidia: (H200 MIG 35GB) Entrypoints Integration (API Server OpenAI - Part 2)" +- label: ":nvidia: (H200 MIG 35GB) Entrypoints Integration (OpenAI API chat_completion)" device: h200_35gb - key: entrypoints-integration-api-server-openai-part-2 + key: entrypoints-integration-api-server-openai-chat_completion timeout_in_minutes: 83 working_dir: "/vllm-workspace/tests" source_file_dependencies: @@ -106,10 +130,9 @@ steps: commands: - export VLLM_WORKER_MULTIPROC_METHOD=spawn - pytest -v -s entrypoints/openai/chat_completion - - pytest -v -s entrypoints/openai/completion --ignore=entrypoints/openai/completion/test_tensorizer_entrypoint.py mirror: amd: - label: ":amd: (MI300) Entrypoints Integration (API Server OpenAI - Part 2)" + label: ":amd: (MI300) Entrypoints Integration (OpenAI API chat_completion)" dind: false device: mi300_1 timeout_in_minutes: 70 @@ -176,6 +199,15 @@ steps: commands: - export VLLM_WORKER_MULTIPROC_METHOD=spawn - pytest -v -s entrypoints/speech_to_text + mirror: + amd: + label: ":amd: (MI355) Entrypoints Integration (Speech to Text)" + dind: false + device: mi355_1 + timeout_in_minutes: 60 + working_dir: "/vllm-workspace/tests" + depends_on: + - image-build-amd - label: ":nvidia: (H200 MIG 35GB) Entrypoints Integration (Multimodal)" device: h200_35gb @@ -210,6 +242,15 @@ steps: commands: - export VLLM_WORKER_MULTIPROC_METHOD=spawn - pytest -v -s entrypoints/pooling + mirror: + amd: + label: ":amd: (MI355) Entrypoints Integration (Pooling)" + dind: false + device: mi355_1 + timeout_in_minutes: 65 + working_dir: "/vllm-workspace/tests" + depends_on: + - image-build-amd - label: ":nvidia: (H200 MIG 18GB) OpenAI API Correctness" key: openai-api-correctness diff --git a/.buildkite/test_areas/expert_parallelism.yaml b/.buildkite/test_areas/expert_parallelism.yaml index f74b1adf2e2a..b9ce4f59240c 100644 --- a/.buildkite/test_areas/expert_parallelism.yaml +++ b/.buildkite/test_areas/expert_parallelism.yaml @@ -40,10 +40,23 @@ steps: commands: - pytest -v -s distributed/test_eplb_execute.py - pytest -v -s distributed/test_eplb_spec_decode.py + mirror: + amd: + label: ":amd: (MI355) EPLB Execution" + dind: false + device: mi355_4 + timeout_in_minutes: 35 + depends_on: + - image-build-amd + source_file_dependencies: + - vllm/distributed/eplb + - tests/distributed/test_eplb_execute.py + - tests/distributed/test_eplb_spec_decode.py + - vllm/platforms/rocm.py - label: ":nvidia: (H100) Elastic EP Scaling" key: elastic-ep-scaling-test - timeout_in_minutes: 30 + timeout_in_minutes: 40 device: h100 working_dir: "/vllm-workspace/tests" num_devices: 4 diff --git a/.buildkite/test_areas/fault_tolerance.yaml b/.buildkite/test_areas/fault_tolerance.yaml index 2e85a1c3f856..a578be14eded 100644 --- a/.buildkite/test_areas/fault_tolerance.yaml +++ b/.buildkite/test_areas/fault_tolerance.yaml @@ -24,3 +24,33 @@ steps: # https://github.com/NVIDIA/nccl/issues/1838 - export NCCL_CUMEM_HOST_ENABLE=0 - pytest -v -s v1/fault_tolerance/test_fault_tolerance_e2e.py + mirror: + amd: + label: ":amd: (MI355) MoRI EP Healthy Baseline" + dind: false + device: mi355_2 + num_devices: 2 + timeout_in_minutes: 10 + working_dir: "/vllm-workspace/tests" + depends_on: + - image-build-amd + source_file_dependencies: + - csrc/moe/ + - csrc/rocm/ + - tests/kernels/moe/ + - vllm/model_executor/layers/fused_moe/ + - vllm/model_executor/layers/quantization/ + - vllm/distributed/ + - vllm/config/ + - vllm/forward_context.py + - vllm/v1/worker/workspace.py + - vllm/utils/import_utils.py + - vllm/utils/math_utils.py + - vllm/utils/torch_utils.py + - vllm/platforms/ + - vllm/_aiter_ops.py + # TODO: Add fault injection, detection and recovery once MoRI supports them. + # This mirror currently checks healthy EP execution only. + commands: + - 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]' diff --git a/.buildkite/test_areas/jit_monitor.yaml b/.buildkite/test_areas/jit_monitor.yaml index 3355470c1712..27de713fa2f0 100644 --- a/.buildkite/test_areas/jit_monitor.yaml +++ b/.buildkite/test_areas/jit_monitor.yaml @@ -20,3 +20,24 @@ steps: # instead of running until the build timeout. - export PYTHONFAULTHANDLER=1 - pytest -v -s jit_monitor/test_no_runtime_jit.py --timeout=900 --timeout-method=thread + mirror: + amd: + # Focused MRV2 regression; the broader model/backend matrix is pending. + label: ':amd: (MI355) MRV2 Sampler JIT Warmup' + dind: false + device: mi355_1 + num_devices: 1 + timeout_in_minutes: 10 + working_dir: /vllm-workspace/ + depends_on: + - image-build-amd + source_file_dependencies: + - tests/jit_monitor/ + - tests/models/utils.py + - tests/utils.py + - vllm/v1/worker/gpu/ + - vllm/v1/sample/ops/topk_topp_sampler.py + - vllm/platforms/rocm.py + - vllm/envs.py + commands: + - pytest -v -s tests/jit_monitor/test_no_runtime_jit_rocm.py --timeout=300 diff --git a/.buildkite/test_areas/kernels.yaml b/.buildkite/test_areas/kernels.yaml index 2b43938060e9..ba683d58adf3 100644 --- a/.buildkite/test_areas/kernels.yaml +++ b/.buildkite/test_areas/kernels.yaml @@ -13,6 +13,22 @@ steps: commands: - pytest -v -s tests/ir - pytest -v -s tests/kernels/ir + mirror: + amd: + label: ":amd: (MI355) vLLM IR" + dind: false + device: mi355_1 + timeout_in_minutes: 50 + working_dir: "/vllm-workspace/" + depends_on: + - image-build-amd + source_file_dependencies: + - tests/ir/ + - tests/kernels/ir/ + - vllm/ir + - vllm/kernels + - vllm/_aiter_ops.py + - vllm/platforms/rocm.py - label: ":nvidia: (H200 MIG 35GB) Core Operation Kernels Shard %N" device: h200_35gb @@ -26,6 +42,29 @@ steps: commands: - pytest -v -s kernels/core --ignore=kernels/core/test_minimax_reduce_rms.py kernels/test_concat_mla_q.py kernels/test_fused_qk_norm_rope_gate.py --shard-id=$$BUILDKITE_PARALLEL_JOB --num-shards=$$BUILDKITE_PARALLEL_JOB_COUNT parallelism: 3 + mirror: + amd: + label: ":amd: (MI355) Core Operation Kernels Shard %N" + dind: false + device: mi355_1 + timeout_in_minutes: 40 + working_dir: "/vllm-workspace/tests" + depends_on: + - image-build-amd + source_file_dependencies: + - csrc/ + - tests/kernels/core + - tests/kernels/test_top_k_per_row.py + - tests/kernels/test_concat_mla_q.py + - tests/kernels/test_rocm_fp32_router_gemm.py + - vllm/model_executor/layers/fused_moe/router/gate_linear.py + - vllm/model_executor/layers/fused_moe/router/rocm_fp32_router_gemm.py + - vllm/model_executor/layers/rotary_embedding/ + - vllm/_aiter_ops.py + - vllm/platforms/rocm.py + - tests/kernels/test_fused_qk_norm_rope_gate.py + commands: + - pytest -v -s kernels/core --ignore=kernels/core/test_minimax_reduce_rms.py kernels/test_fused_qk_norm_rope_gate.py kernels/test_concat_mla_q.py kernels/test_top_k_per_row.py kernels/test_rocm_fp32_router_gemm.py --shard-id=$$BUILDKITE_PARALLEL_JOB --num-shards=$$BUILDKITE_PARALLEL_JOB_COUNT - label: ":nvidia: (H100) MiniMax Reduce RMS Kernels" key: kernels-minimax-reduce-rms-test-2-gpus @@ -72,6 +111,23 @@ steps: commands: - pytest -v -s kernels/test_fused_deepseek_v4_*.py - pytest -v -s kernels/test_top_k_per_row.py + mirror: + amd: + label: ":amd: (MI355) DeepSeek V4 Fused Kernels" + dind: false + device: mi355_1 + num_devices: 1 + timeout_in_minutes: 60 + working_dir: /vllm-workspace/tests + depends_on: + - image-build-amd + source_file_dependencies: + - csrc/libtorch_stable/fused_deepseek_v4_qnorm_rope_kv_insert_kernel.cu + - vllm/models/deepseek_v4/common/ops/ + - tests/kernels/test_fused_deepseek_v4_qnorm_rope_kv_insert.py + - vllm/platforms/rocm.py + commands: + - pytest -v -s kernels/test_fused_deepseek_v4_qnorm_rope_kv_insert.py - label: ":nvidia: (B200) DeepSeek V4 Kernels" key: deepseek-v4-kernel-test-b200 @@ -81,6 +137,7 @@ steps: - csrc/fused_deepseek_v4_qnorm_rope_kv_insert_kernel.cu - vllm/models/deepseek_v4/common/ops/ - vllm/models/deepseek_v4/nvidia/ + - vllm/utils/deep_gemm.py - tests/kernels/test_fused_deepseek_v4_qnorm_rope_kv_insert.py - tests/models/test_deepseek_v4_mega_moe.py commands: @@ -102,6 +159,8 @@ steps: - "!vllm/distributed/kv_transfer/" - tests/kernels/ commands: + # Isolate mHC from FlashInfer's import-time ptxas override. + - pytest -v -s kernels/test_mhc_kernels.py - pytest -v -s kernels/ --ignore=kernels/attention --ignore=kernels/core @@ -117,6 +176,7 @@ steps: --ignore=kernels/test_kda.py --ignore=kernels/test_bf16x3_router_gemm_cutedsl.py --ignore=kernels/test_ll_bf16_gemm.py + --ignore=kernels/test_mhc_kernels.py --ignore=kernels/test_shuffle_rows.py # BROKEN on main, pending kernel fixes (B200): # test_shuffle_rows.py (1: test_shuffle_rows_edge_cases) @@ -142,6 +202,7 @@ steps: dind: false device: mi300_1 timeout_in_minutes: 90 + parallelism: 6 depends_on: - image-build-amd source_file_dependencies: @@ -152,6 +213,8 @@ steps: - vllm/_aiter_ops.py - 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 - label: ":nvidia: (H100) Attention DiffKV Kernels" key: kernels-attention-diffkv-test-h100 @@ -165,6 +228,24 @@ steps: - tests/kernels/attention/test_triton_unified_attention_diffkv.py commands: - pytest -v -s kernels/attention/test_triton_unified_attention_diffkv.py + mirror: + amd: + label: ":amd: (MI355) Attention DiffKV Kernels" + dind: false + device: mi355_1 + num_devices: 1 + timeout_in_minutes: 35 + working_dir: /vllm-workspace/tests + depends_on: + - image-build-amd + source_file_dependencies: + - vllm/v1/attention/ops/triton_unified_attention_diffkv.py + - vllm/v1/attention/backends/triton_attn_diffkv.py + - tests/kernels/attention/test_triton_unified_attention.py + - tests/kernels/attention/test_triton_unified_attention_diffkv.py + - vllm/platforms/rocm.py + commands: + - pytest -v -s kernels/attention/test_triton_unified_attention_diffkv.py - label: ":nvidia: (H100) MLA Kernel Test" key: kernels-flashmla-test-h100 @@ -187,6 +268,31 @@ steps: - pytest -v -s kernels/attention/test_flashmla_sparse.py - pytest -v -s kernels/attention/test_flashinfer_mla_decode.py - pytest -v -s kernels/attention/test_mla_cross_layer_kernel_equivalence.py + mirror: + amd: + label: ":amd: (MI355) MLA Kernels" + dind: false + device: mi355_1 + num_devices: 1 + timeout_in_minutes: 40 + working_dir: /vllm-workspace/ + depends_on: + - image-build-amd + source_file_dependencies: + - vllm/v1/attention/backends/mla/rocm_aiter_mla.py + - vllm/v1/attention/selector.py + - vllm/platforms/rocm.py + - vllm/_aiter_ops.py + - tests/kernels/attention/test_rocm_aiter_mla_decode.py + - tests/kernels/attention/test_rocm_aiter_mla_decode_metadata.py + - tests/kernels/attention/test_rocm_aiter_mla_fp8_support.py + - tests/kernels/attention/test_rocm_aiter_mla_op_registration.py + commands: + - amd-smi + - pytest -v -s tests/kernels/attention/test_rocm_aiter_mla_decode_metadata.py + - pytest -v -s tests/kernels/attention/test_rocm_aiter_mla_decode.py + - pytest -v -s tests/kernels/attention/test_rocm_aiter_mla_fp8_support.py + - pytest -v -s tests/kernels/attention/test_rocm_aiter_mla_op_registration.py - label: ":nvidia: (L4) Quantization Kernels Shard %N" key: kernels-quantization-test @@ -205,9 +311,6 @@ steps: label: ":amd: (MI300) Quantization Kernels Shard %N" dind: false device: mi300_1 - # Pin the AMD mirror at its current 2 shards so raising NVIDIA - # parallelism does not multiply AMD copies (needs ci-infra #473). - parallelism: 2 timeout_in_minutes: 120 source_file_dependencies: - csrc/quantization/ @@ -219,8 +322,16 @@ steps: - vllm/kernels/aiter_ops.py - vllm/platforms/rocm.py - vllm/model_executor/kernels/ + - csrc/rocm/ + - vllm/v1/attention/backends/rocm_aiter_fa.py + - tests/kernels/quant_utils.py + - tests/kernels/utils.py + - vllm/_custom_ops.py + - vllm/envs.py depends_on: - image-build-amd + 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" key: kernels-moe-test @@ -255,6 +366,10 @@ steps: - vllm/config - vllm/_aiter_ops.py - vllm/platforms/rocm.py + - csrc/rocm/ + - tests/kernels/utils.py + - vllm/model_executor/layers/quantization/mxfp4.py + - vllm/model_executor/layers/quantization/utils/quant_utils.py depends_on: - image-build-amd @@ -265,16 +380,21 @@ steps: source_file_dependencies: - csrc/mamba/ - tests/kernels/mamba + - vllm/model_executor/layers/mamba/gdn - vllm/model_executor/layers/mamba/ops commands: - pytest -v -s kernels/mamba mirror: amd: label: ":amd: (MI250) Mamba Kernels" + dind: false device: mi250_1 timeout_in_minutes: 55 depends_on: - image-build-amd + source_file_dependencies: + - vllm/platforms/rocm.py + - vllm/third_party/flash_linear_attention/ - label: ":nvidia: (H100) DeepGEMM Kernels" key: kernels-deepgemm-test-h100 @@ -314,6 +434,7 @@ steps: - csrc/quantization/fp4/ - csrc/libtorch_stable/nvfp4_kv_cache_kernels.cu - csrc/libtorch_stable/quantization/fp4/ + - csrc/libtorch_stable/quantization/w8a8/cutlass/ - csrc/attention/mla/ - csrc/quantization/cutlass_w8a8/moe/ - vllm/model_executor/layers/fusion/ @@ -355,6 +476,7 @@ steps: - tests/kernels/test_ll_bf16_gemm.py - tests/kernels/test_top_k_per_row.py - tests/kernels/quantization/test_silu_mul_nvfp4_quant.py + - tests/kernels/quantization/test_block_fp8.py - tests/models/quantization/test_nvfp4.py commands: - nvidia-smi @@ -389,6 +511,7 @@ steps: pytest -v -s tests/kernels/attention/test_flashinfer.py -k 'not num_heads2' pytest -v -s tests/kernels/attention/test_cutlass_mla_decode.py pytest -v -s tests/kernels/quantization/test_cutlass_scaled_mm.py -k 'fp8' + pytest -v -s tests/kernels/quantization/test_block_fp8.py::test_w8a8_block_fp8_cutlass_matmul pytest -v -s tests/kernels/quantization/test_nvfp4_quant.py pytest -v -s tests/kernels/quantization/test_nvfp4_scaled_mm.py pytest -v -s tests/kernels/quantization/test_mxfp4_qutlass.py @@ -402,6 +525,32 @@ steps: exit 1 ;; esac + mirror: + amd: + label: ":amd: (MI355) NVFP4 Dense Emulation Shard %N" + dind: false + device: mi355_1 + num_devices: 1 + timeout_in_minutes: 10 + working_dir: "/vllm-workspace/tests" + depends_on: + - image-build-amd + source_file_dependencies: + - tests/kernels/quantization/test_nvfp4_emulation.py + - tests/kernels/quantization/nvfp4_utils.py + - tests/conftest.py + - vllm/model_executor/kernels/linear/ + - vllm/model_executor/layers/quantization/utils/nvfp4_emulation_utils.py + - vllm/config/kernel.py + - vllm/platforms/ + - vllm/envs.py + - vllm/utils/torch_utils.py + - requirements/rocm-test.txt + # W4A4 format numerics and replay via emulation. TODO: native packed + # activation/KV-cache layouts and CUDA-only kernels need AMD backends. + commands: + - amd-smi + - pytest -v -s kernels/quantization/test_nvfp4_emulation.py -k test_nvfp4_dense_emulation --shard-id=$$BUILDKITE_PARALLEL_JOB --num-shards=$$BUILDKITE_PARALLEL_JOB_COUNT - label: ":nvidia: (DGX) Spark b12x Linear Kernels Nightly" key: b12x-linear-kernels-dgx-spark-nightly @@ -456,10 +605,14 @@ steps: mirror: amd: label: ":amd: (MI250) Helion Kernels Shard %N" + dind: false device: mi250_1 timeout_in_minutes: 45 depends_on: - image-build-amd + source_file_dependencies: + - vllm/platforms/rocm.py + - scripts/benchmark_helion_kernels.py - label: ":nvidia: (H100) FP8 MoE Kernels" @@ -488,6 +641,25 @@ steps: commands: - pytest -v -s kernels/moe/test_deepep_deepgemm_moe.py - pytest -v -s kernels/moe/test_deepep_moe.py + mirror: + amd: + label: ":amd: (MI355) DeepEP FP8 MoE Kernels" + dind: false + device: mi355_2 + timeout_in_minutes: 55 + working_dir: "/vllm-workspace/tests" + depends_on: + - image-build-amd + source_file_dependencies: + - csrc/moe/ + - csrc/quantization/w8a8/cutlass/moe/ + - vllm/model_executor/layers/fused_moe/ + - tests/kernels/moe/test_deepep_moe.py + - vllm/_aiter_ops.py + - vllm/platforms/rocm.py + - vllm/envs.py + commands: + - pytest -v -s kernels/moe/test_deepep_moe.py - label: ":nvidia: (B200) FP4 MoE Kernels" key: kernels-fp4-moe-test-b200 @@ -517,7 +689,33 @@ steps: - vllm/config commands: - pytest -v -s kernels/moe/test_moe_layer.py - + mirror: + amd: + label: ":amd: (MI355) FusedMoE Layer Kernels" + dind: false + device: mi355_2 + timeout_in_minutes: 35 + working_dir: "/vllm-workspace/tests" + depends_on: + - image-build-amd + source_file_dependencies: + - csrc/moe/ + - csrc/rocm/ + - tests/kernels/moe + - vllm/model_executor/layers/fused_moe/ + - vllm/model_executor/layers/quantization/ + - vllm/distributed/ + - vllm/config/ + - vllm/forward_context.py + - vllm/v1/worker/workspace.py + - vllm/utils/import_utils.py + - vllm/utils/math_utils.py + - vllm/utils/torch_utils.py + - vllm/platforms/ + - vllm/_aiter_ops.py + commands: + # MoRI graph cases run in the Fault Tolerance group's healthy EP baseline. + - pytest -v -s kernels/moe/test_moe_layer.py -k "not test_moe_layer_mori_graph" - label: ":nvidia: (B200) FusedMoE Layer Kernels" key: kernels-fusedmoe-layer-test-2-b200s @@ -535,3 +733,95 @@ steps: commands: - pytest -v -s kernels/moe/test_moe_layer.py - pytest -v -s kernels/moe/test_deepep_v2_moe.py + mirror: + amd: + label: ":amd: (MI355) MoRI EP Numerics" + dind: false + device: mi355_2 + num_devices: 2 + timeout_in_minutes: 20 + working_dir: "/vllm-workspace/tests" + depends_on: + - image-build-amd + source_file_dependencies: + - tests/kernels/moe/test_mori_moe.py + - tests/kernels/moe/utils.py + - tests/kernels/moe/modular_kernel_tools/parallel_utils.py + - tests/kernels/utils.py + - tests/utils.py + - tests/conftest.py + - vllm/utils/torch_utils.py + - vllm/model_executor/layers/fused_moe/ + - vllm/model_executor/layers/quantization/ + - vllm/distributed/ + - vllm/config/ + - vllm/forward_context.py + - vllm/v1/worker/workspace.py + - vllm/platforms/ + - vllm/_aiter_ops.py + - requirements/rocm-test.txt + # Same-node MoRI/AITER BF16 and per-token/channel FP8. TODO: the + # DeepEP v2 FlashInfer backend matrix has no direct AMD implementation. + commands: + - amd-smi + - VLLM_ROCM_USE_AITER=1 VLLM_ROCM_USE_AITER_MOE=1 VLLM_ROCM_USE_AITER_FUSION_SHARED_EXPERTS=0 pytest -v -s kernels/moe/test_mori_moe.py + +- label: ":amd: (MI355) FP8 MoE Kernels" + key: amd-fp8-moe-kernels-mi355 + dind: false + device: mi355_1 + num_devices: 1 + timeout_in_minutes: 40 + working_dir: /vllm-workspace/tests + depends_on: + - image-build-amd + source_file_dependencies: + - csrc/moe/ + - vllm/model_executor/layers/fused_moe/ + - tests/kernels/moe/test_deepep_moe.py + - vllm/_aiter_ops.py + - vllm/platforms/rocm.py + - vllm/envs.py + - tests/kernels/moe/test_gpt_oss_triton_kernels.py + - tests/kernels/moe/test_modular_oai_triton_moe.py + - tests/kernels/moe/test_moe.py + - tests/kernels/moe/test_block_int8.py + - tests/kernels/moe/test_triton_moe_no_act_mul.py + - tests/kernels/moe/test_triton_moe_ptpc_fp8.py + commands: + - pytest -v -s kernels/moe/test_gpt_oss_triton_kernels.py + - pytest -v -s kernels/moe/test_modular_oai_triton_moe.py + - pytest -v -s kernels/moe/test_moe.py + - pytest -v -s kernels/moe/test_block_int8.py + - pytest -v -s kernels/moe/test_triton_moe_no_act_mul.py + - pytest -v -s kernels/moe/test_triton_moe_ptpc_fp8.py + +- label: ":amd: (MI355) Kernels" + key: amd-kernels-mi355 + dind: false + device: mi355_1 + num_devices: 1 + timeout_in_minutes: 25 + working_dir: /vllm-workspace/ + depends_on: + - image-build-amd + source_file_dependencies: + - csrc/quantization/fp4/ + - csrc/attention/mla/ + - csrc/quantization/cutlass_w8a8/moe/ + - vllm/model_executor/layers/fused_moe/cutlass_moe.py + - vllm/v1/attention/backends/triton_attn.py + - vllm/v1/attention/backends/rocm_attn.py + - vllm/v1/attention/backends/rocm_aiter_fa.py + - vllm/v1/attention/backends/rocm_aiter_unified_attn.py + - vllm/v1/attention/backends/mla/aiter_triton_mla.py + - vllm/v1/attention/backends/mla/rocm_aiter_mla.py + - vllm/v1/attention/selector.py + - vllm/platforms/rocm.py + - vllm/_aiter_ops.py + - examples/basic/offline_inference/chat.py + - tests/kernels/attention/test_attention_selector.py + commands: + - amd-smi + - python3 examples/basic/offline_inference/chat.py --attention-backend TRITON_ATTN + - pytest -v -s tests/kernels/attention/test_attention_selector.py diff --git a/.buildkite/test_areas/lm_eval.yaml b/.buildkite/test_areas/lm_eval.yaml index 0863dfd2767e..dccf57f2e2e5 100644 --- a/.buildkite/test_areas/lm_eval.yaml +++ b/.buildkite/test_areas/lm_eval.yaml @@ -30,6 +30,65 @@ steps: - vllm/_aiter_ops.py - vllm/platforms/rocm.py +- label: ":amd: (MI300) LM Eval Small Models Harness" + key: amd-lm-eval-small-models-harness + dind: false + device: mi300_1 + timeout_in_minutes: 35 + depends_on: + - image-build-amd + working_dir: /vllm-workspace/.buildkite/lm-eval-harness + source_file_dependencies: + - csrc/ + - vllm/model_executor/layers/quantization + - vllm/model_executor/models/ + - vllm/model_executor/model_loader/ + - vllm/v1/attention/backends/ + - vllm/v1/attention/selector.py + - vllm/_aiter_ops.py + - vllm/platforms/rocm.py + - .buildkite/lm-eval-harness/ + commands: + - pytest -s -v test_lm_eval_correctness.py --config-list-file=configs/models-small-rocm.txt + +- label: ":nvidia: (H200 MIG 35GB) LM Eval Watermarking" + device: h200_35gb + key: lm-eval-watermarking + # Recorded runs took 80–140 seconds + timeout_in_minutes: 10 + source_file_dependencies: + - .buildkite/test_areas/lm_eval.yaml + - tests/evals/gsm8k/ + - vllm/config/watermarking.py + - vllm/sampling_params.py + - vllm/v1/watermarking/ + - vllm/v1/worker/gpu/ + autorun_on_main: true + commands: + - pytest -s -v evals/gsm8k/test_gsm8k_correctness.py --config-list-file=configs/models-watermark.txt + mirror: + amd: + label: ":amd: (MI355) LM Eval Watermarking" + dind: false + device: mi355_1 + num_devices: 1 + timeout_in_minutes: 45 + working_dir: /vllm-workspace/tests + depends_on: + - image-build-amd + source_file_dependencies: + - .buildkite/test_areas/lm_eval.yaml + - tests/evals/gsm8k/ + - vllm/config/watermarking.py + - vllm/sampling_params.py + - vllm/v1/watermarking/ + - vllm/v1/worker/gpu/ + - vllm/platforms/rocm.py + env: + VLLM_WORKER_MULTIPROC_METHOD: "spawn" + 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 @@ -55,6 +114,27 @@ steps: commands: - export VLLM_USE_DEEP_GEMM=0 # We found Triton is faster than DeepGEMM for H100 - pytest -s -v test_lm_eval_correctness.py --config-list-file=configs/models-large-hopper.txt --tp-size=4 + mirror: + amd: + label: ":amd: (MI355) LM Eval Large Models FP8" + dind: false + device: mi355_4 + timeout_in_minutes: 45 + depends_on: + - image-build-amd + source_file_dependencies: + - csrc/ + - vllm/model_executor/layers/quantization + - vllm/model_executor/models/ + - vllm/model_executor/model_loader/ + - vllm/v1/attention/backends/ + - vllm/v1/attention/selector.py + - .buildkite/lm-eval-harness/ + - vllm/_aiter_ops.py + - vllm/platforms/rocm.py + commands: + - export VLLM_USE_DEEP_GEMM=0 + - pytest -s -v test_lm_eval_correctness.py --config-list-file=configs/models-large-rocm-tp4.txt --tp-size=4 - label: ":nvidia: (B200) LM Eval Small Models" key: lm-eval-small-models-1xb200 @@ -79,6 +159,44 @@ steps: autorun_on_main: true commands: - pytest -s -v evals/gsm8k/test_gsm8k_correctness.py --config-list-file=configs/models-small-tp.txt + mirror: + amd: + label: ":amd: (MI355) LM Eval Small Models Distributed" + dind: false + device: mi355_2 + timeout_in_minutes: 55 + depends_on: + - image-build-amd + source_file_dependencies: + - csrc/ + - vllm/model_executor/layers/quantization + - vllm/model_executor/models/ + - vllm/model_executor/model_loader/ + - vllm/v1/attention/backends/ + - vllm/v1/attention/selector.py + - tests/evals/gsm8k/configs/models-mi3xx-fp8-and-mixed.txt + - vllm/_aiter_ops.py + - vllm/platforms/rocm.py + commands: + - pytest -s -v evals/gsm8k/test_gsm8k_correctness.py --config-list-file=configs/models-mi3xx-fp8-and-mixed.txt + +- label: ":nvidia: (B200) LM Eval HiSparse Nightly" + key: lm-eval-hisparse-nightly-4xb200 + timeout_in_minutes: 60 + device: b200-k8s + num_devices: 4 + optional: true + source_file_dependencies: + - .buildkite/test_areas/lm_eval.yaml + - tests/evals/gsm8k/ + - vllm/v1/hisparse/ + - vllm/distributed/kv_transfer/kv_connector/v1/hisparse/ + - vllm/v1/attention/backends/mla/ + - vllm/model_executor/layers/attention/sparse_mla_attention.py + - vllm/v1/worker/gpu/ + - csrc/libtorch_stable/hisparse_kernels.cu + commands: + - pytest -s -v evals/gsm8k/test_gsm8k_correctness.py --config-list-file=configs/models-hisparse.txt - label: ":nvidia: (B200) LM Eval PCP" key: lm-eval-pcp-4xb200 @@ -90,18 +208,72 @@ steps: - csrc/ - tests/evals/gsm8k/configs/GLM-5.2-NVFP4-TP2-PCP2-EP.yaml - tests/evals/gsm8k/configs/GLM-5.2-NVFP4-TP1-PCP4-EP.yaml + - tests/evals/gsm8k/configs/GLM-5.2-NVFP4-TP1-PCP4-DCP4-EP.yaml - tests/evals/gsm8k/configs/models-pcp.txt - vllm/model_executor/layers/quantization - vllm/config/parallel.py - vllm/distributed/parallel_state.py - vllm/model_executor/layers/attention/mla_attention.py - vllm/model_executor/layers/attention/pcp.py + - vllm/model_executor/layers/attention/sparse_mla_attention.py + - vllm/model_executor/layers/sparse_attn_indexer.py + - vllm/v1/attention/backends/mla/flashmla_sparse.py + - vllm/v1/attention/backends/mla/indexer.py - vllm/v1/worker/gpu/model_runner.py - vllm/v1/worker/gpu/pcp_manager.py autorun_on_main: true commands: - pytest -s -v evals/gsm8k/test_gsm8k_correctness.py --config-list-file=configs/models-pcp.txt +- label: ":nvidia: (H100) LM Eval Watermark Feature Combination" + key: lm-eval-dspark-watermark-2xh100 + device: h100 + num_devices: 2 + # Local TP2 runs took 84–379 seconds, including startup. + timeout_in_minutes: 15 + source_file_dependencies: + - .buildkite/test_areas/lm_eval.yaml + - tests/evals/gsm8k/ + - vllm/config/speculative.py + - vllm/config/vllm.py + - vllm/config/watermarking.py + - vllm/sampling_params.py + - vllm/model_executor/models/qwen3_dspark.py + - vllm/model_executor/models/qwen3_dflash.py + - vllm/model_executor/warmup/ + - vllm/v1/watermarking/ + - vllm/v1/worker/gpu/ + autorun_on_main: true + commands: + - pytest -s -v evals/gsm8k/test_gsm8k_correctness.py --config-list-file=configs/models-watermark-dspark-tp2.txt + mirror: + amd: + label: ":amd: (MI355) LM Eval Watermark Feature Combination" + dind: false + device: mi355_2 + num_devices: 2 + timeout_in_minutes: 60 + working_dir: /vllm-workspace/tests + depends_on: + - image-build-amd + source_file_dependencies: + - .buildkite/test_areas/lm_eval.yaml + - tests/evals/gsm8k/ + - vllm/config/speculative.py + - vllm/config/vllm.py + - vllm/config/watermarking.py + - vllm/sampling_params.py + - vllm/model_executor/models/qwen3_dspark.py + - vllm/model_executor/models/qwen3_dflash.py + - vllm/model_executor/warmup/ + - vllm/v1/watermarking/ + - vllm/v1/worker/gpu/ + - vllm/platforms/rocm.py + env: + VLLM_WORKER_MULTIPROC_METHOD: "spawn" + commands: + - pytest -s -v evals/gsm8k/test_gsm8k_correctness.py --config-list-file=configs/models-watermark-dspark-tp2.txt + - label: ":nvidia: (B200) LM Eval Spec Decode" key: lm-eval-spec-decode-4xb200 timeout_in_minutes: 120 @@ -191,27 +363,94 @@ steps: - vllm/third_party/flash_linear_attention/ops/ commands: - pytest -s -v evals/gsm8k/test_gsm8k_correctness.py --config-list-file=configs/models-qwen35-blackwell.txt - -- label: ":nvidia: (H200) LM Eval Large Models" - key: lm-eval-large-models-8xh200 - timeout_in_minutes: 50 - device: h200 - optional: true - num_devices: 8 - commands: - - pytest -s -v evals/gsm8k/test_gsm8k_correctness.py --config-list-file=configs/models-h200.txt mirror: amd: - label: ":amd: (MI300) LM Eval Large Models" + label: ":amd: (MI355) LM Eval Qwen3.5 Models" dind: false - device: mi300_8 + device: mi355_2 timeout_in_minutes: 40 depends_on: - image-build-amd + source_file_dependencies: + - vllm/model_executor/models/qwen3_5.py + - vllm/model_executor/models/qwen3_5_mtp.py + - vllm/transformers_utils/configs/qwen3_5.py + - vllm/transformers_utils/configs/qwen3_5_moe.py + - vllm/model_executor/models/qwen2.py + - vllm/model_executor/models/qwen3.py + - vllm/model_executor/models/qwen3_next.py + - vllm/model_executor/models/qwen3_next_mtp.py + - vllm/third_party/flash_linear_attention/ops/ + - tests/evals/gsm8k/configs/models-qwen35-mi355.txt + - tests/evals/gsm8k/configs/Qwen3.5-35B-A3B-DEP2.yaml + - tests/evals/gsm8k/configs/Qwen3.5-35B-A3B-MXFP4-AITER-TP2.yaml + - tests/evals/gsm8k/configs/Qwen3.5-35B-A3B-MXFP4-EMU-TP2.yaml + - tests/evals/gsm8k/test_gsm8k_correctness.py + - vllm/_aiter_ops.py + - vllm/platforms/rocm.py commands: - - export VLLM_WORKER_MULTIPROC_METHOD=spawn - - export PYTORCH_ROCM_ARCH=gfx942 # Limit Quark compilation to save time - - pytest -s -v evals/gsm8k/test_gsm8k_correctness.py --config-list-file=configs/models-mi3xx.txt + - pytest -s -v evals/gsm8k/test_gsm8k_correctness.py --config-list-file=configs/models-qwen35-mi355.txt + +- label: ":nvidia: (H200) LM Eval DeepSeek R1 TP" + key: lm-eval-deepseek-r1-tp-8xh200 + timeout_in_minutes: 35 + device: h200 + optional: true + num_devices: 8 + commands: + - pytest -s -v evals/gsm8k/test_gsm8k_correctness.py --config-list-file=configs/models-h200-deepseek-r1-tp.txt + +- label: ":nvidia: (H200) LM Eval DeepSeek R1 DP" + key: lm-eval-deepseek-r1-dp-8xh200 + timeout_in_minutes: 35 + device: h200 + optional: true + num_devices: 8 + commands: + - pytest -s -v evals/gsm8k/test_gsm8k_correctness.py --config-list-file=configs/models-h200-deepseek-r1-dp.txt + +- label: ":nvidia: (H200) LM Eval DeepSeek V3.2 TP" + key: lm-eval-deepseek-v32-tp-8xh200 + timeout_in_minutes: 35 + device: h200 + optional: true + num_devices: 8 + commands: + - pytest -s -v evals/gsm8k/test_gsm8k_correctness.py --config-list-file=configs/models-h200-deepseek-v32-tp.txt + +- label: ":nvidia: (H200) LM Eval DeepSeek V3.2 DP" + key: lm-eval-deepseek-v32-dp-8xh200 + timeout_in_minutes: 35 + device: h200 + optional: true + num_devices: 8 + commands: + - pytest -s -v evals/gsm8k/test_gsm8k_correctness.py --config-list-file=configs/models-h200-deepseek-v32-dp.txt + +- label: ":nvidia: (H200) LM Eval Nemotron 3 Super" + key: lm-eval-nemotron-3-super-8xh200 + timeout_in_minutes: 35 + device: h200 + optional: true + num_devices: 8 + commands: + - pytest -s -v evals/gsm8k/test_gsm8k_correctness.py --config-list-file=configs/models-h200-nemotron-3-super.txt + +- label: ":amd: (MI300) LM Eval Large Models" + key: amd-lm-eval-large-models + dind: false + device: mi300_8 + optional: true + timeout_in_minutes: 65 + depends_on: + - image-build-amd + commands: + - export VLLM_WORKER_MULTIPROC_METHOD=spawn + - export PYTORCH_ROCM_ARCH=gfx942 # Limit Quark compilation to save time + - pytest -s -v evals/gsm8k/test_gsm8k_correctness.py --config-list-file=configs/models-mi3xx.txt + source_file_dependencies: + - vllm/_aiter_ops.py + - vllm/platforms/rocm.py - label: ":nvidia: (H100) MoE Refactor Integration TEMPORARY" key: moe-refactor-integration-test-h100-temporary @@ -224,7 +463,27 @@ steps: # build 83851): shards ~12.4/13.6/12.4/14.5m of eval time. New configs must # be added to exactly one config-h100-shard-*.txt. - pytest -s -v evals/gsm8k/test_gsm8k_correctness.py --config-list-file=evals/gsm8k/configs/moe-refactor/config-h100-shard-$$BUILDKITE_PARALLEL_JOB.txt - + mirror: + amd: + label: ":amd: (MI355) MoE Refactor Integration Shard %N" + dind: false + device: mi355_2 + num_devices: 2 + timeout_in_minutes: 105 + working_dir: /vllm-workspace/tests + depends_on: + - image-build-amd + source_file_dependencies: + - vllm/platforms/rocm.py + - tests/evals/gsm8k/ + - vllm/model_executor/layers/fused_moe/ + - vllm/model_executor/layers/quantization/ + - vllm/_aiter_ops.py + env: + VLLM_WORKER_MULTIPROC_METHOD: "spawn" + commands: + - pytest -s -v evals/gsm8k/test_gsm8k_correctness.py --config-list-file=evals/gsm8k/configs/moe-refactor/config-rocm-shard-$$BUILDKITE_PARALLEL_JOB.txt + - label: ":nvidia: (B200) MoE Refactor Integration TEMPORARY Shard %N" key: moe-refactor-integration-test-b200-temporary device: b200-k8s @@ -242,47 +501,67 @@ steps: commands: - pytest -s -v evals/gsm8k/test_gsm8k_correctness.py --config-list-file=evals/gsm8k/configs/moe-refactor-dp-ep/config-b200.txt -- label: ":nvidia: (A100) LM Eval Humming FP16 TEMPORARY Shard %N" - key: lm-eval-humming-f16-a100 +- label: ":nvidia: (L4) LM Eval Humming TEMPORARY" + key: lm-eval-humming-l4 + optional: true + timeout_in_minutes: 90 + device: l4 + num_devices: 1 + source_file_dependencies: + - .buildkite/test_areas/lm_eval.yaml + - tests/evals/gsm8k/configs/humming/ + - requirements/cuda.txt + - vllm/utils/humming.py + - 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-l4.txt + +- label: ":nvidia: (A100) LM Eval Humming Default TEMPORARY Shard %N" + key: lm-eval-humming-a100-default + optional: true timeout_in_minutes: 75 device: a100 - optional: true num_devices: 1 parallelism: 3 source_file_dependencies: - vllm/model_executor/layers/quantization/humming.py - - vllm/model_executor/layers/quantization/utils/humming_utils.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) LM Eval Humming Activation INT8 TEMPORARY" - key: lm-eval-humming-act-a100 +- label: ":nvidia: (A100) LM Eval Humming Activation INT8 TEMPORARY Shard %N" + key: lm-eval-humming-a100-act + optional: true timeout_in_minutes: 45 device: a100 - optional: true num_devices: 1 + parallelism: 2 source_file_dependencies: - vllm/model_executor/layers/quantization/humming.py - - vllm/model_executor/layers/quantization/utils/humming_utils.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-act-int8.txt + - 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) LM Eval Humming FP16 TEMPORARY Shard %N" - key: lm-eval-humming-f16-h100 +- label: ":nvidia: (H100) LM Eval Humming Default TEMPORARY Shard %N" + key: lm-eval-humming-h100-default + optional: true timeout_in_minutes: 70 device: h100 - optional: true num_devices: 1 parallelism: 3 source_file_dependencies: - vllm/model_executor/layers/quantization/humming.py - - vllm/model_executor/layers/quantization/utils/humming_utils.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/ @@ -290,30 +569,50 @@ steps: - pytest -s -v evals/gsm8k/test_gsm8k_correctness.py --config-list-file=evals/gsm8k/configs/humming/config-h100-shard-$$BUILDKITE_PARALLEL_JOB.txt - label: ":nvidia: (H100) LM Eval Humming Activation FP8/INT8 TEMPORARY" - key: lm-eval-humming-act-h100 - timeout_in_minutes: 70 - device: h100 + key: lm-eval-humming-h100-act optional: true + timeout_in_minutes: 90 + device: h100 num_devices: 1 source_file_dependencies: - vllm/model_executor/layers/quantization/humming.py - - vllm/model_executor/layers/quantization/utils/humming_utils.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-act-fp8.txt - # - pytest -s -v evals/gsm8k/test_gsm8k_correctness.py --config-list-file=evals/gsm8k/configs/humming/config-act-int8.txt + - pytest -s -v evals/gsm8k/test_gsm8k_correctness.py --config-list-file=evals/gsm8k/configs/humming/config-act-int8.txt + +- label: ":nvidia: (H100) LM Eval Humming DeepEP TEMPORARY" + key: lm-eval-humming-h100-deepep + optional: true + timeout_in_minutes: 60 + device: h100 + num_devices: 2 + source_file_dependencies: + - .buildkite/test_areas/lm_eval.yaml + - tests/evals/gsm8k/configs/humming/ + - requirements/cuda.txt + - vllm/utils/humming.py + - vllm/model_executor/layers/quantization/humming.py + - vllm/model_executor/layers/quantization/utils/humming/ + - vllm/model_executor/layers/fused_moe/ + - vllm/model_executor/kernels/linear/ + - vllm/distributed/ + - csrc/moe/ + commands: + - pytest -s -v evals/gsm8k/test_gsm8k_correctness.py --config-list-file=evals/gsm8k/configs/humming/config-deepep.txt -- label: ":nvidia: (B200) LM Eval Humming FP16 TEMPORARY" - key: lm-eval-humming-f16-b200 +- label: ":nvidia: (B200) LM Eval Humming Default TEMPORARY" + key: lm-eval-humming-b200-default + optional: true timeout_in_minutes: 50 device: b200-k8s - optional: true num_devices: 1 source_file_dependencies: - vllm/model_executor/layers/quantization/humming.py - - vllm/model_executor/layers/quantization/utils/humming_utils.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/ @@ -321,14 +620,14 @@ steps: - pytest -s -v evals/gsm8k/test_gsm8k_correctness.py --config-list-file=evals/gsm8k/configs/humming/config.txt - label: ":nvidia: (B200) LM Eval Humming Activation FP8/INT8 TEMPORARY" - key: lm-eval-humming-act-b200 + key: lm-eval-humming-b200-act + optional: true timeout_in_minutes: 50 device: b200-k8s - optional: true num_devices: 1 source_file_dependencies: - vllm/model_executor/layers/quantization/humming.py - - vllm/model_executor/layers/quantization/utils/humming_utils.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/ @@ -336,9 +635,9 @@ steps: - pytest -s -v evals/gsm8k/test_gsm8k_correctness.py --config-list-file=evals/gsm8k/configs/humming/config-act-fp8.txt - pytest -s -v evals/gsm8k/test_gsm8k_correctness.py --config-list-file=evals/gsm8k/configs/humming/config-act-int8.txt -- label: ":nvidia: (H200 MIG 18GB) LM Eval TurboQuant KV Cache" - key: lm-eval-turboquant-kv-cache - timeout_in_minutes: 55 +- label: ":nvidia: (H200 MIG 18GB) LM Eval TurboQuant k8v4" + key: lm-eval-turboquant-k8v4 + timeout_in_minutes: 30 device: h200_18gb source_file_dependencies: - vllm/model_executor/layers/quantization/turboquant/ @@ -346,7 +645,155 @@ steps: - vllm/v1/attention/ops/triton_turboquant_decode.py - vllm/v1/attention/ops/triton_turboquant_store.py commands: - - pytest -s -v evals/gsm8k/test_gsm8k_correctness.py --config-list-file=evals/gsm8k/configs/models-turboquant.txt + - pytest -s -v evals/gsm8k/test_gsm8k_correctness.py --config-list-file=evals/gsm8k/configs/models-turboquant-k8v4.txt + mirror: + amd: + label: ":amd: (MI355) LM Eval TurboQuant k8v4" + dind: false + device: mi355_1 + num_devices: 1 + timeout_in_minutes: 30 + working_dir: /vllm-workspace/tests + depends_on: + - image-build-amd + source_file_dependencies: + - .buildkite/test_areas/lm_eval.yaml + - tests/evals/gsm8k/ + - tests/utils.py + - tests/conftest.py + - vllm/model_executor/layers/quantization/turboquant/ + - vllm/v1/attention/backends/turboquant_attn.py + - vllm/v1/attention/backends/triton_attn.py + - vllm/v1/attention/ops/triton_unified_attention.py + - vllm/v1/attention/selector.py + - vllm/v1/attention/ops/triton_turboquant_decode.py + - vllm/v1/attention/ops/triton_turboquant_store.py + - vllm/v1/attention/ops/flydsl_turboquant_decode.py + - vllm/v1/attention/ops/flydsl_kernels/ + - vllm/v1/attention/ops/turboquant_soa/ + - vllm/platforms/rocm.py + commands: + - pytest -s -v evals/gsm8k/test_gsm8k_correctness.py --config-list-file=evals/gsm8k/configs/models-turboquant-k8v4-rocm.txt + +- label: ":nvidia: (H200 MIG 18GB) LM Eval TurboQuant t4nc" + key: lm-eval-turboquant-t4nc + timeout_in_minutes: 30 + device: h200_18gb + source_file_dependencies: + - vllm/model_executor/layers/quantization/turboquant/ + - vllm/v1/attention/backends/turboquant_attn.py + - vllm/v1/attention/ops/triton_turboquant_decode.py + - vllm/v1/attention/ops/triton_turboquant_store.py + commands: + - pytest -s -v evals/gsm8k/test_gsm8k_correctness.py --config-list-file=evals/gsm8k/configs/models-turboquant-t4nc.txt + mirror: + amd: + label: ":amd: (MI355) LM Eval TurboQuant t4nc" + dind: false + device: mi355_1 + num_devices: 1 + timeout_in_minutes: 30 + working_dir: /vllm-workspace/tests + depends_on: + - image-build-amd + source_file_dependencies: + - .buildkite/test_areas/lm_eval.yaml + - tests/evals/gsm8k/ + - tests/utils.py + - tests/conftest.py + - vllm/model_executor/layers/quantization/turboquant/ + - vllm/v1/attention/backends/turboquant_attn.py + - vllm/v1/attention/backends/triton_attn.py + - vllm/v1/attention/ops/triton_unified_attention.py + - vllm/v1/attention/selector.py + - vllm/v1/attention/ops/triton_turboquant_decode.py + - vllm/v1/attention/ops/triton_turboquant_store.py + - vllm/v1/attention/ops/flydsl_turboquant_decode.py + - vllm/v1/attention/ops/flydsl_kernels/ + - vllm/v1/attention/ops/turboquant_soa/ + - vllm/platforms/rocm.py + commands: + - pytest -s -v evals/gsm8k/test_gsm8k_correctness.py --config-list-file=evals/gsm8k/configs/models-turboquant-t4nc-rocm.txt + +- label: ":nvidia: (H200 MIG 18GB) LM Eval TurboQuant k3v4nc" + key: lm-eval-turboquant-k3v4nc + timeout_in_minutes: 30 + device: h200_18gb + source_file_dependencies: + - vllm/model_executor/layers/quantization/turboquant/ + - vllm/v1/attention/backends/turboquant_attn.py + - vllm/v1/attention/ops/triton_turboquant_decode.py + - vllm/v1/attention/ops/triton_turboquant_store.py + commands: + - pytest -s -v evals/gsm8k/test_gsm8k_correctness.py --config-list-file=evals/gsm8k/configs/models-turboquant-k3v4nc.txt + mirror: + amd: + label: ":amd: (MI355) LM Eval TurboQuant k3v4nc" + dind: false + device: mi355_1 + num_devices: 1 + timeout_in_minutes: 30 + working_dir: /vllm-workspace/tests + depends_on: + - image-build-amd + source_file_dependencies: + - .buildkite/test_areas/lm_eval.yaml + - tests/evals/gsm8k/ + - tests/utils.py + - tests/conftest.py + - vllm/model_executor/layers/quantization/turboquant/ + - vllm/v1/attention/backends/turboquant_attn.py + - vllm/v1/attention/backends/triton_attn.py + - vllm/v1/attention/ops/triton_unified_attention.py + - vllm/v1/attention/selector.py + - vllm/v1/attention/ops/triton_turboquant_decode.py + - vllm/v1/attention/ops/triton_turboquant_store.py + - vllm/v1/attention/ops/flydsl_turboquant_decode.py + - vllm/v1/attention/ops/flydsl_kernels/ + - vllm/v1/attention/ops/turboquant_soa/ + - vllm/platforms/rocm.py + commands: + - pytest -s -v evals/gsm8k/test_gsm8k_correctness.py --config-list-file=evals/gsm8k/configs/models-turboquant-k3v4nc-rocm.txt + +- label: ":nvidia: (H200 MIG 18GB) LM Eval TurboQuant t3nc" + key: lm-eval-turboquant-t3nc + timeout_in_minutes: 30 + device: h200_18gb + source_file_dependencies: + - vllm/model_executor/layers/quantization/turboquant/ + - vllm/v1/attention/backends/turboquant_attn.py + - vllm/v1/attention/ops/triton_turboquant_decode.py + - vllm/v1/attention/ops/triton_turboquant_store.py + commands: + - pytest -s -v evals/gsm8k/test_gsm8k_correctness.py --config-list-file=evals/gsm8k/configs/models-turboquant-t3nc.txt + mirror: + amd: + label: ":amd: (MI355) LM Eval TurboQuant t3nc" + dind: false + device: mi355_1 + num_devices: 1 + timeout_in_minutes: 30 + working_dir: /vllm-workspace/tests + depends_on: + - image-build-amd + source_file_dependencies: + - .buildkite/test_areas/lm_eval.yaml + - tests/evals/gsm8k/ + - tests/utils.py + - tests/conftest.py + - vllm/model_executor/layers/quantization/turboquant/ + - vllm/v1/attention/backends/turboquant_attn.py + - vllm/v1/attention/backends/triton_attn.py + - vllm/v1/attention/ops/triton_unified_attention.py + - vllm/v1/attention/selector.py + - vllm/v1/attention/ops/triton_turboquant_decode.py + - vllm/v1/attention/ops/triton_turboquant_store.py + - vllm/v1/attention/ops/flydsl_turboquant_decode.py + - vllm/v1/attention/ops/flydsl_kernels/ + - vllm/v1/attention/ops/turboquant_soa/ + - vllm/platforms/rocm.py + commands: + - pytest -s -v evals/gsm8k/test_gsm8k_correctness.py --config-list-file=evals/gsm8k/configs/models-turboquant-t3nc-rocm.txt - label: ":nvidia: (H100) GPQA Eval (GPT-OSS)" key: gpqa-eval-gpt-oss-2xh100 @@ -375,6 +822,28 @@ steps: commands: - uv pip install --system 'gpt-oss[eval]==0.0.5' - pytest -s -v evals/gpt_oss/test_gpqa_correctness.py --config-list-file=configs/models-b200.txt + mirror: + amd: + label: ":amd: (MI355) GPQA Eval (GPT-OSS)" + dind: false + device: mi355_2 + timeout_in_minutes: 40 + depends_on: + - image-build-amd + source_file_dependencies: + - csrc/ + - vllm/model_executor/layers/quantization + - vllm/model_executor/models/ + - vllm/model_executor/model_loader/ + - vllm/v1/attention/backends/ + - vllm/v1/attention/selector.py + - vllm/model_executor/layers/fused_moe/ + - tests/evals/gpt_oss/ + - vllm/_aiter_ops.py + - vllm/platforms/rocm.py + commands: + - uv pip install --system 'gpt-oss[eval]==0.0.5' + - pytest -s -v evals/gpt_oss/test_gpqa_correctness.py --config-list-file=configs/models-gfx950.txt - label: ":nvidia: (B200) Qwen3.8-Flash-Next-FP8 Accuracy Eval" key: accuracy-eval-qwen3-8-flash-next-fp8-b200 @@ -411,6 +880,29 @@ steps: commands: - uv pip install --system 'evalscope==1.10.0' - pytest -s -v evals/qwen4_exp/test_accuracy.py --config-list-file=configs/models-h200.txt + mirror: + amd: + label: ":amd: (MI355) Qwen3.8-Flash-Next-FP8 Accuracy Eval" + dind: false + device: mi355_4 + num_devices: 4 + timeout_in_minutes: 105 + working_dir: /vllm-workspace/tests + depends_on: + - image-build-amd + source_file_dependencies: + - csrc/libtorch_stable/gdn/ + - tests/evals/qwen4_exp/ + - vllm/model_executor/layers/mamba/ + - vllm/models/qwen4_exp/ + - vllm/transformers_utils/configs/qwen4_exp.py + - vllm/v1/attention/backends/short_conv_attn.py + - vllm/platforms/rocm.py + env: + VLLM_WORKER_MULTIPROC_METHOD: "spawn" + commands: + - uv pip install --system 'evalscope==1.10.0' + - pytest -s -v evals/qwen4_exp/test_accuracy.py --config-list-file=configs/models-rocm.txt - label: ":nvidia: (DGX) Spark GPQA Eval (GPT-OSS)" key: gpqa-eval-gpt-oss-spark @@ -470,6 +962,7 @@ steps: - pytest -s -v evals/gsm8k/test_gsm8k_offloading.py -k "deepseek-v4-flash" - label: ":nvidia: (H200 MIG 35GB) MRCR Eval Small Models" + key: mrcr-eval-small-models device: h200_35gb timeout_in_minutes: 25 source_file_dependencies: diff --git a/.buildkite/test_areas/lora.yaml b/.buildkite/test_areas/lora.yaml index d63fb567eaf1..28f695f391a9 100644 --- a/.buildkite/test_areas/lora.yaml +++ b/.buildkite/test_areas/lora.yaml @@ -54,13 +54,14 @@ steps: - test "$$BUILDKITE_PARALLEL_JOB" != "0" || pytest -v -s -x lora/test_gptoss_tp.py - test "$$BUILDKITE_PARALLEL_JOB" != "1" || pytest -v -s -x lora/test_qwen35_densemodel_lora.py - test "$$BUILDKITE_PARALLEL_JOB" != "2" || pytest -v -s -x lora/test_gemma4_tp.py + - test "$$BUILDKITE_PARALLEL_JOB" != "0" || pytest -v -s -x lora/test_sequence_classification.py::test_batched_loras_tp mirror: amd: label: ":amd: (MI300) LoRA TP (Distributed) %N" dind: false device: mi300_4 working_dir: "/vllm-workspace/tests" - timeout_in_minutes: 25 + timeout_in_minutes: 65 depends_on: - image-build-amd source_file_dependencies: @@ -72,10 +73,11 @@ steps: - export VLLM_WORKER_MULTIPROC_METHOD=spawn # expandable_segments breaks custom all-reduce IPC on ROCm. - case "$$BUILDKITE_PARALLEL_JOB" in 0|1|2|3) ;; *) echo "unexpected BUILDKITE_PARALLEL_JOB=$$BUILDKITE_PARALLEL_JOB" && exit 1;; esac - - test "$$BUILDKITE_PARALLEL_JOB" != "3" || pytest -v -s -x lora/test_chatglm3_tp.py - - test "$$BUILDKITE_PARALLEL_JOB" != "1" || pytest -v -s -x lora/test_llama_tp.py - - test "$$BUILDKITE_PARALLEL_JOB" != "3" || pytest -v -s -x lora/test_qwen3_with_multi_loras.py - - test "$$BUILDKITE_PARALLEL_JOB" != "2" || pytest -v -s -x lora/test_olmoe_tp.py - - test "$$BUILDKITE_PARALLEL_JOB" != "0" || pytest -v -s -x lora/test_gptoss_tp.py - - test "$$BUILDKITE_PARALLEL_JOB" != "1" || pytest -v -s -x lora/test_qwen35_densemodel_lora.py - - test "$$BUILDKITE_PARALLEL_JOB" != "2" || pytest -v -s -x lora/test_gemma4_tp.py + - if [ "$$BUILDKITE_PARALLEL_JOB" = "3" ]; then pytest -v -s -x lora/test_chatglm3_tp.py; fi + - if [ "$$BUILDKITE_PARALLEL_JOB" = "1" ]; then pytest -v -s -x lora/test_llama_tp.py; fi + - if [ "$$BUILDKITE_PARALLEL_JOB" = "3" ]; then pytest -v -s -x lora/test_qwen3_with_multi_loras.py; fi + - if [ "$$BUILDKITE_PARALLEL_JOB" = "2" ]; then pytest -v -s -x lora/test_olmoe_tp.py; fi + - if [ "$$BUILDKITE_PARALLEL_JOB" = "0" ]; then pytest -v -s -x lora/test_gptoss_tp.py; fi + - if [ "$$BUILDKITE_PARALLEL_JOB" = "1" ]; then pytest -v -s -x lora/test_qwen35_densemodel_lora.py; fi + - if [ "$$BUILDKITE_PARALLEL_JOB" = "2" ]; then pytest -v -s -x lora/test_gemma4_tp.py; fi + - if [ "$$BUILDKITE_PARALLEL_JOB" = "0" ]; then pytest -v -s -x lora/test_sequence_classification.py::test_batched_loras_tp; fi diff --git a/.buildkite/test_areas/misc.yaml b/.buildkite/test_areas/misc.yaml index 5ae3bcf73ecb..ad1c58f08717 100644 --- a/.buildkite/test_areas/misc.yaml +++ b/.buildkite/test_areas/misc.yaml @@ -99,12 +99,12 @@ steps: label: ":amd: (MI300) V1 Core" dind: false device: mi300_1 - timeout_in_minutes: 45 + timeout_in_minutes: 60 depends_on: - image-build-amd -- label: ":nvidia: (H200 MIG 35GB) V1 Executor + Worker" - device: h200_35gb +- label: ":nvidia: (H200 MIG 18GB) V1 Executor + Worker" + device: h200_18gb key: v1-executor-worker timeout_in_minutes: 45 source_file_dependencies: *v1-core-kv-metrics-deps @@ -119,7 +119,7 @@ steps: label: ":amd: (MI300) V1 Executor + Worker" dind: false device: mi300_1 - timeout_in_minutes: 45 + timeout_in_minutes: 60 depends_on: - image-build-amd @@ -130,6 +130,7 @@ steps: source_file_dependencies: *v1-core-kv-metrics-deps commands: - export VLLM_WORKER_MULTIPROC_METHOD=spawn + - bash /vllm-workspace/.buildkite/scripts/install-kv-offload.sh - pytest -v -s v1/kv_offload - pytest -v -s v1/simple_kv_offload mirror: @@ -137,26 +138,27 @@ steps: label: ":amd: (MI300) V1 KV Offload" dind: false device: mi300_1 - timeout_in_minutes: 45 + timeout_in_minutes: 60 depends_on: - image-build-amd -- label: ":nvidia: (H200 MIG 35GB) V1 KV Connectors" +- label: ":nvidia: (H200 MIG 35GB) V1 KV Connectors Shard %N" device: h200_35gb key: v1-kv-connectors timeout_in_minutes: 45 + parallelism: 4 source_file_dependencies: *v1-core-kv-metrics-deps commands: - bash /vllm-workspace/.buildkite/scripts/install-kv-connectors.sh - export VLLM_WORKER_MULTIPROC_METHOD=spawn - - pytest -v -s -m 'not cpu_test' v1/kv_connector/unit - - pytest -v -s -m 'not cpu_test' v1/ec_connector/unit + - pytest -v -s -m 'not cpu_test' v1/kv_connector/unit --num-shards=$$BUILDKITE_PARALLEL_JOB_COUNT --shard-id=$$BUILDKITE_PARALLEL_JOB + - pytest -v -s -m 'not cpu_test' v1/ec_connector/unit --num-shards=$$BUILDKITE_PARALLEL_JOB_COUNT --shard-id=$$BUILDKITE_PARALLEL_JOB mirror: amd: - label: ":amd: (MI300) V1 KV Connectors" + label: ":amd: (MI300) V1 KV Connectors Shard %N" dind: false device: mi300_1 - timeout_in_minutes: 45 + timeout_in_minutes: 60 depends_on: - image-build-amd @@ -235,6 +237,27 @@ steps: commands: - export VLLM_WORKER_MULTIPROC_METHOD=spawn - pytest -v -s tests/v1/kv_connector/extract_hidden_states_integration + mirror: + amd: + label: ":amd: (MI355) Extract Hidden States Integration Single GPU" + dind: false + device: mi355_1 + num_devices: 1 + timeout_in_minutes: 45 + working_dir: /vllm-workspace + depends_on: + - image-build-amd + source_file_dependencies: + - vllm/v1/spec_decode/extract_hidden_states.py + - vllm/model_executor/models/extract_hidden_states.py + - vllm/transformers_utils/configs/extract_hidden_states.py + - vllm/distributed/kv_transfer/kv_connector/v1/example_hidden_states_connector.py + - tests/v1/kv_connector/extract_hidden_states_integration + - vllm/platforms/rocm.py + env: + VLLM_WORKER_MULTIPROC_METHOD: "spawn" + commands: + - pytest -v -s -m 'not distributed' tests/v1/kv_connector/extract_hidden_states_integration - label: ":nvidia: (L4) Extract Hidden States Integration" key: extract-hidden-states-integration-2-gpus @@ -251,6 +274,35 @@ steps: commands: - export VLLM_WORKER_MULTIPROC_METHOD=spawn - pytest -v -s -m 'distributed' tests/v1/kv_connector/extract_hidden_states_integration + mirror: + amd: + label: ":amd: (MI355) Extract Hidden States Integration" + dind: false + device: mi355_2 + timeout_in_minutes: 30 + depends_on: + - image-build-amd + source_file_dependencies: + - vllm/config/speculative.py + - vllm/distributed/kv_transfer/kv_connector/ + - vllm/model_executor/layers/attention/ + - vllm/model_executor/layers/mamba/ + - vllm/model_executor/model_loader/ + - vllm/model_executor/models/extract_hidden_states.py + - vllm/model_executor/models/llama.py + - vllm/model_executor/models/qwen3_5.py + - vllm/model_executor/models/qwen3_next.py + - vllm/model_executor/models/registry.py + - vllm/transformers_utils/configs/extract_hidden_states.py + - vllm/transformers_utils/configs/qwen3_5.py + - vllm/v1/attention/backends/ + - vllm/v1/attention/selector.py + - vllm/v1/kv_cache_interface.py + - vllm/v1/spec_decode/extract_hidden_states.py + - vllm/v1/worker/gpu_model_runner.py + - vllm/_aiter_ops.py + - tests/v1/kv_connector/extract_hidden_states_integration/ + - vllm/platforms/rocm.py - label: ":nvidia: (H200 MIG 18GB) Regression" key: regression @@ -274,6 +326,15 @@ steps: - pip install 'modelscope<1.38' - pytest -v -s test_regression.py working_dir: "/vllm-workspace/tests" # optional + mirror: + amd: + label: ":amd: (MI355) Regression" + dind: false + device: mi355_1 + timeout_in_minutes: 25 + depends_on: + - image-build-amd + working_dir: /vllm-workspace/tests - label: ":nvidia: (H200 MIG 35GB) Examples" device: h200_35gb @@ -376,7 +437,6 @@ steps: dind: false device: mi300_1 timeout_in_minutes: 55 - soft_fail: true depends_on: - image-build-amd source_file_dependencies: @@ -415,9 +475,15 @@ steps: label: ":amd: (MI355) Async Engine, Inputs, Utils, Worker" dind: false device: mi355_1 - timeout_in_minutes: 40 + timeout_in_minutes: 50 depends_on: - image-build-amd + commands: + - pytest -v -s detokenizer + - pytest -v -s -m 'not cpu_test' multimodal + # test_cache.py marks every test cpu_test, including this GPU regression. + - pytest -v -s multimodal/test_cache.py::test_sleep_wake_preserves_mm_cache_consistency + - pytest -v -s utils_ - label: ":computer: (CPU) Params, Env, Tokenizers, Parser" key: cpu-params-env-tokenizers-parser @@ -452,6 +518,7 @@ steps: - vllm/v1/ - tests/test_envs.py - tests/test_outputs.py + - tests/test_pcp_dp.py - tests/test_pooling_params.py - tests/test_ray_env.py - tests/test_sampling_params.py @@ -459,6 +526,9 @@ steps: - tests/renderers - tests/standalone_tests/lazy_imports.py - tests/reasoning + - tools/pre_commit/check_test_tethering.py + - tools/pre_commit/test_tethering_allowlist.txt + - tests/tools/test_check_test_tethering.py - tests/tool_parsers - tests/tokenizers_ - tests/parser @@ -469,6 +539,7 @@ steps: - python3 standalone_tests/lazy_imports.py - pytest -v -s test_envs.py - pytest -v -s test_outputs.py + - pytest -v -s test_pcp_dp.py - pytest -v -s test_pooling_params.py - pytest -v -s test_ray_env.py - pytest -v -s test_sampling_params.py @@ -486,6 +557,7 @@ steps: commands: - pytest -v -s -m 'cpu_test' multimodal - pytest -v -s config + - pytest -v -s tools/test_check_test_tethering.py - label: ":computer: (CPU) Reasoning + Renderers" key: cpu-reasoning-renderers @@ -537,6 +609,25 @@ steps: - pytest -v -s v1/determinism/test_rms_norm_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: (MI250) Batch Invariance" + dind: false + device: mi250_1 + timeout_in_minutes: 35 + depends_on: + - image-build-amd + source_file_dependencies: + - vllm/v1/attention + - vllm/model_executor/layers + - 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 - label: ":nvidia: (B200) Batch Invariance" key: batch-invariance-b200 @@ -574,3 +665,17 @@ steps: commands: - export VLLM_ALLOW_INSECURE_SERIALIZATION=1 - pytest -v -s v1/spec_decode/test_acceptance_length.py -m slow_test + mirror: + amd: + label: ":amd: (MI355) Acceptance Length (Large Models)" + dind: false + device: mi355_1 + timeout_in_minutes: 45 + depends_on: + - image-build-amd + working_dir: /vllm-workspace/tests + source_file_dependencies: + - vllm/v1/spec_decode/ + - vllm/model_executor/models/mlp_speculator.py + - tests/v1/spec_decode/test_acceptance_length.py + - vllm/platforms/rocm.py diff --git a/.buildkite/test_areas/model_executor.yaml b/.buildkite/test_areas/model_executor.yaml index 17818cc15260..f6905f98ef35 100644 --- a/.buildkite/test_areas/model_executor.yaml +++ b/.buildkite/test_areas/model_executor.yaml @@ -33,7 +33,7 @@ steps: label: ":amd: (MI300) Model Executor" dind: false device: mi300_1 - timeout_in_minutes: 60 + timeout_in_minutes: 75 depends_on: - image-build-amd source_file_dependencies: diff --git a/.buildkite/test_areas/models_basic.yaml b/.buildkite/test_areas/models_basic.yaml index 93d6962868a4..6f570b3459ef 100644 --- a/.buildkite/test_areas/models_basic.yaml +++ b/.buildkite/test_areas/models_basic.yaml @@ -19,7 +19,7 @@ steps: label: ":amd: (MI355) Basic Models (Initialization)" dind: false device: mi355_1 - timeout_in_minutes: 25 + timeout_in_minutes: 30 depends_on: - image-build-amd @@ -37,27 +37,51 @@ steps: # test.) Also run if model initialization test file is modified - pytest -v -s models/test_initialization.py -k 'not test_can_initialize_small_subset' --num-shards=$$BUILDKITE_PARALLEL_JOB_COUNT --shard-id=$$BUILDKITE_PARALLEL_JOB parallelism: 14 + mirror: + amd: + label: ":amd: (MI355) Basic Models (Extra Initialization) Shard %N" + dind: false + device: mi355_1 + timeout_in_minutes: 60 + parallelism: 10 + working_dir: "/vllm-workspace/tests" + depends_on: + - image-build-amd + source_file_dependencies: + - vllm/model_executor/models/ + - vllm/model_executor/layers/ + - tests/models/test_initialization.py + - tests/models/registry.py + - vllm/_aiter_ops.py + - vllm/platforms/rocm.py - label: ":nvidia: (H200 MIG 35GB) Basic Models (Other)" device: h200_35gb key: basic-models-tests-other - timeout_in_minutes: 35 + timeout_in_minutes: 38 source_file_dependencies: - vllm/ - "!vllm/distributed/kv_transfer/" - tests/models/test_terratorch.py - tests/models/transformers/test_backend.py - tests/models/test_registry.py + - tests/models/test_deepseek_v4_vl_rocm.py commands: - - pytest -v -s models/test_terratorch.py models/transformers/test_backend.py models/test_registry.py + - pytest -v -s models/test_terratorch.py models/transformers/test_backend.py models/test_registry.py models/test_deepseek_v4_vl_rocm.py mirror: amd: label: ":amd: (MI300) Basic Models (Other)" dind: false device: mi300_1 - timeout_in_minutes: 50 + timeout_in_minutes: 65 depends_on: - image-build-amd + source_file_dependencies: + - tests/models/test_deepseek_v4_vl_rocm.py + - tests/models/test_hyv4_rocm.py + commands: + - pytest -v -s models/test_terratorch.py models/transformers/test_backend.py models/test_registry.py models/test_deepseek_v4_vl_rocm.py + - VLLM_ROCM_USE_AITER=1 pytest -v -s models/test_hyv4_rocm.py -m 'not distributed' - label: ":nvidia: (B200) Inkling" key: inkling-unit-tests-b200 @@ -85,6 +109,23 @@ steps: commands: # The native NVIDIA Kimi K3 kernels require the SM100 family. - pytest -v -s models/kimi_k3 kernels/attention/test_kimi_k3_mla_fused_epilogue.py kernels/test_bf16_skinny_gemm.py + mirror: + amd: + label: ":amd: (MI355) Kimi K3" + dind: false + device: mi355_1 + num_devices: 1 + timeout_in_minutes: 60 + working_dir: "/vllm-workspace/tests" + depends_on: + - image-build-amd + source_file_dependencies: + - vllm/platforms/rocm.py + commands: + # The ROCm KDA/AttnRes kernels are gated on gfx950, so this has to be MI355. + # The two kernels/ files the NVIDIA job adds are CUDA-only (they import + # cuda.bindings.driver), so they are deliberately left out here. + - pytest -v -s models/kimi_k3 - label: ":nvidia: (H200) GLM5Next" key: glm5next-unit-tests @@ -95,6 +136,17 @@ steps: - tests/models/glm5next/ commands: - pytest -v -s models/glm5next + mirror: + amd: + label: ":amd: (MI355) GLM5Next" + dind: false + device: mi355_1 + timeout_in_minutes: 35 + working_dir: "/vllm-workspace/tests" + depends_on: + - image-build-amd + source_file_dependencies: + - vllm/platforms/rocm.py - label: ":nvidia: (H200) Qwen4 Exp" key: qwen4-exp-unit-tests @@ -146,6 +198,17 @@ steps: - tests/models/test_adapters.py - tests/models/test_qwen3_5_mtp_config.py - tests/models/transformers/fusers/ + - tests/model_executor/layers/test_activation.py device: cpu-small commands: - - pytest -v -s models/test_utils.py models/test_vision.py models/test_adapters.py models/test_qwen3_5_mtp_config.py models/transformers/fusers/ + - pytest -v -s models/test_utils.py models/test_vision.py models/test_adapters.py models/test_qwen3_5_mtp_config.py models/transformers/fusers/ model_executor/layers/test_activation.py + mirror: + amd: + label: ":amd: (MI250) Basic Models Other" + dind: false + device: mi250_1 + no_gpu: true + working_dir: "/vllm-workspace/tests" + timeout_in_minutes: 35 + depends_on: + - image-build-amd diff --git a/.buildkite/test_areas/models_distributed.yaml b/.buildkite/test_areas/models_distributed.yaml index cf7623ce3ec8..a054c00db860 100644 --- a/.buildkite/test_areas/models_distributed.yaml +++ b/.buildkite/test_areas/models_distributed.yaml @@ -38,10 +38,17 @@ steps: label: ":amd: (MI300) Distributed Models Shard %N" dind: false device: mi300_2 - timeout_in_minutes: 35 + timeout_in_minutes: 80 depends_on: - image-build-amd source_file_dependencies: + - vllm/models/hy_v4/ + - vllm/config/ + - vllm/v1/worker/ + - vllm/v1/attention/ops/ + - vllm/v1/attention/backend.py + - tests/conftest.py + - tests/utils.py - vllm/model_executor/model_loader/sharded_state_loader.py - vllm/model_executor/models/ - vllm/model_executor/layers/ @@ -56,16 +63,17 @@ steps: - | case "$$BUILDKITE_PARALLEL_JOB" in 0) - TARGET_TEST_SUITE=MI300 pytest basic_correctness/ -v -s -m 'distributed(num_gpus=2)' + TARGET_TEST_SUITE=MI300 pytest basic_correctness/ -v -s -m 'distributed(num_gpus=2)' && HIP_VISIBLE_DEVICES=0,1 pytest -v -s model_executor/model_loader/test_sharded_state_loader.py -m '(not slow_test)' ;; 1) - pytest models/transformers/test_backend.py -v -s -m 'distributed(num_gpus=2)' - pytest models/language -v -s -m 'distributed(num_gpus=2)' + 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)' ;; 2) - pytest models/multimodal -v -s -m 'distributed(num_gpus=2)' --ignore models/multimodal/generation/test_whisper.py --ignore models/multimodal/generation/test_phi4siglip.py - pytest models/multimodal/generation/test_phi4siglip.py -v -s -m 'distributed(num_gpus=2)' + pytest models/multimodal -v -s -m 'distributed(num_gpus=2)' --ignore models/multimodal/generation/test_whisper.py --ignore models/multimodal/generation/test_phi4siglip.py && + pytest models/multimodal/generation/test_phi4siglip.py -v -s -m 'distributed(num_gpus=2)' && VLLM_WORKER_MULTIPROC_METHOD=spawn pytest models/multimodal/generation/test_whisper.py -v -s -m 'distributed(num_gpus=2)' ;; *) exit 2 ;; diff --git a/.buildkite/test_areas/models_language.yaml b/.buildkite/test_areas/models_language.yaml index 68f1cdd429ce..c8473e7f0b60 100644 --- a/.buildkite/test_areas/models_language.yaml +++ b/.buildkite/test_areas/models_language.yaml @@ -19,7 +19,7 @@ steps: label: ":amd: (MI300) Language Models (Standard)" dind: false device: mi300_1 - timeout_in_minutes: 45 + timeout_in_minutes: 60 depends_on: - image-build-amd @@ -82,16 +82,16 @@ steps: parallelism: 2 mirror: amd: - label: ":amd: (MI300) Language Models (Hybrid) Shard %N" + label: ":amd: (MI355) Language Models (Hybrid) Shard %N" dind: false - device: mi300_1 - timeout_in_minutes: 60 + device: mi355_1 + timeout_in_minutes: 65 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 --num-shards=$$BUILDKITE_PARALLEL_JOB_COUNT --shard-id=$$BUILDKITE_PARALLEL_JOB + - 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 # autotuning (https://github.com/vllm-project/vllm/issues/25194). Keep this one @@ -111,6 +111,27 @@ steps: - rm -rf /tmp/mamba-src && git clone --depth 1 --branch v2.3.0 https://github.com/state-spaces/mamba /tmp/mamba-src && sed -i 's/-std=c++17/-std=c++20/g' /tmp/mamba-src/setup.py && MAMBA_FORCE_BUILD=TRUE uv pip install --system --no-build-isolation /tmp/mamba-src - 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' + mirror: + amd: + label: ":amd: (MI355) Granite Language Model Compatibility" + dind: false + device: mi355_1 + num_devices: 1 + timeout_in_minutes: 80 + working_dir: /vllm-workspace/tests + depends_on: + - image-build-amd + source_file_dependencies: + - vllm/ + - "!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) Language Models (Extended Generation)" device: h200_35gb @@ -133,7 +154,7 @@ steps: label: ":amd: (MI355) Language Models (Extended Generation)" dind: false device: mi355_1 - timeout_in_minutes: 70 + timeout_in_minutes: 95 depends_on: - image-build-amd commands: @@ -155,6 +176,7 @@ steps: mirror: amd: label: ":amd: (MI250) Language Models (PPL)" + dind: false device: mi250_1 timeout_in_minutes: 65 depends_on: @@ -197,11 +219,8 @@ steps: dind: false device: mi300_1 timeout_in_minutes: 95 - parallelism: 2 depends_on: - image-build-amd - commands: - - pytest -v -s models/language/pooling -m 'not core_model' --num-shards=$$BUILDKITE_PARALLEL_JOB_COUNT --shard-id=$$BUILDKITE_PARALLEL_JOB - label: ":nvidia: (H200 MIG 18GB) Language Models (MTEB)" key: language-models-test-mteb @@ -217,6 +236,7 @@ steps: mirror: amd: label: ":amd: (MI250) Language Models (MTEB)" + dind: false device: mi250_1 timeout_in_minutes: 60 depends_on: diff --git a/.buildkite/test_areas/models_multimodal.yaml b/.buildkite/test_areas/models_multimodal.yaml index 9893383bff1b..779801a2ea2b 100644 --- a/.buildkite/test_areas/models_multimodal.yaml +++ b/.buildkite/test_areas/models_multimodal.yaml @@ -57,6 +57,7 @@ steps: mirror: amd: label: ":amd: (MI250) Multimodal Models (Standard) 3: llava + qwen2_vl" + dind: false device: mi250_1 timeout_in_minutes: 55 depends_on: @@ -77,9 +78,9 @@ steps: - cd .. && VLLM_WORKER_MULTIPROC_METHOD=spawn pytest -v -s tests/models/multimodal/generation/test_whisper.py -m core_model # Otherwise, mp_method="spawn" doesn't work mirror: amd: - label: ":amd: (MI300) Multimodal Models (Standard) 4: other + whisper" + label: ":amd: (MI355) Multimodal Models (Standard) 4: other + whisper" dind: false - device: mi300_1 + device: mi355_1 timeout_in_minutes: 70 depends_on: - image-build-amd @@ -98,23 +99,36 @@ steps: commands: - pytest -v -s models/multimodal/processing --ignore models/multimodal/processing/test_tensor_schema.py --num-shards=$$BUILDKITE_PARALLEL_JOB_COUNT --shard-id=$$BUILDKITE_PARALLEL_JOB parallelism: 4 + mirror: + amd: + label: ":amd: (MI250) Multimodal Processor Shard %N" + dind: false + device: mi250_1 + no_gpu: true + working_dir: "/vllm-workspace/tests" + timeout_in_minutes: 90 + parallelism: 6 + depends_on: + - image-build-amd -- label: ":nvidia: (H200 MIG 18GB) Multimodal Processor" +- label: ":nvidia: (H200 MIG 18GB) Multimodal Processor Shard %N" key: multi-modal-processor timeout_in_minutes: 98 device: h200_18gb + parallelism: 4 source_file_dependencies: - vllm/ - "!vllm/distributed/kv_transfer/" - tests/models/multimodal - tests/models/registry.py commands: - - pytest -v -s models/multimodal/processing/test_tensor_schema.py + - pytest -v -s models/multimodal/processing/test_tensor_schema.py --num-shards=$$BUILDKITE_PARALLEL_JOB_COUNT --shard-id=$$BUILDKITE_PARALLEL_JOB mirror: amd: - label: ":amd: (MI355) Multimodal Processor" + label: ":amd: (MI355) Multimodal Processor Shard %N" dind: false device: mi355_1 + parallelism: 4 timeout_in_minutes: 115 depends_on: - image-build-amd diff --git a/.buildkite/test_areas/plugins.yaml b/.buildkite/test_areas/plugins.yaml index 65e7a0b258a0..6b901d8f490b 100644 --- a/.buildkite/test_areas/plugins.yaml +++ b/.buildkite/test_areas/plugins.yaml @@ -53,6 +53,7 @@ steps: mirror: amd: label: ":amd: (MI250) Plugin Integration" + dind: false device: mi250_2 timeout_in_minutes: 50 depends_on: @@ -75,6 +76,20 @@ steps: commands: - pip install "vllm-gguf-plugin >= 0.0.2" - pytest -v -s plugins_tests/gguf + mirror: + amd: + label: ":amd: (MI355) GGUF Plugin" + dind: false + device: mi355_1 + timeout_in_minutes: 45 + working_dir: "/vllm-workspace/tests" + depends_on: + - image-build-amd + source_file_dependencies: + - vllm/model_executor/layers/quantization + - tests/plugins_tests/test_gguf_plugin.py + - tests/plugins_tests/gguf + - vllm/platforms/rocm.py - label: ":nvidia: (H200 MIG 18GB) BitsAndBytes Plugin" key: bitsandbytes-plugin diff --git a/.buildkite/test_areas/pytorch.yaml b/.buildkite/test_areas/pytorch.yaml index eb5b8407033c..8204fb9670a2 100644 --- a/.buildkite/test_areas/pytorch.yaml +++ b/.buildkite/test_areas/pytorch.yaml @@ -87,6 +87,20 @@ steps: - tests/compile/h100/ commands: - "find compile/h100/ -name 'test_*.py' -print0 | xargs -0 -n1 -I{} pytest -s -v '{}'" + mirror: + amd: + label: ":amd: (MI355) PyTorch Compilation H100 Cases" + dind: false + device: mi355_1 + timeout_in_minutes: 45 + working_dir: "/vllm-workspace/tests" + depends_on: + - image-build-amd + source_file_dependencies: + - tests/utils.py + - csrc/ + commands: + - pytest -v -s compile/h100/test_startup.py - label: ":nvidia: (L4) PyTorch Compilation Passes" key: pytorch-compilation-passes-unit-tests @@ -122,6 +136,21 @@ steps: - tests/compile/passes commands: - pytest -s -v compile/passes --ignore compile/passes/distributed + mirror: + amd: + label: ':amd: (MI300) PyTorch Compilation Passes' + dind: false + device: mi300_1 + timeout_in_minutes: 80 + working_dir: /vllm-workspace/tests + depends_on: + - image-build-amd + source_file_dependencies: + - vllm/ + - tests/compile/passes + - vllm/platforms/rocm.py + commands: + - pytest -s -v compile/passes --ignore compile/passes/distributed - label: ":nvidia: (H200 MIG 35GB) PyTorch Fullgraph" device: h200_35gb @@ -154,12 +183,21 @@ steps: - vllm/utils/ - vllm/v1/ - 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 '{}'" + mirror: + amd: + label: ":amd: (MI355) PyTorch Fullgraph" + dind: false + device: mi355_1 + timeout_in_minutes: 90 + depends_on: + - image-build-amd # Hopper-only DeepSeek-V2-Lite cases in this file require two 29.3-GiB model # instances and cannot fit a 35GB MIG slice. L4 retains the original coverage: diff --git a/.buildkite/test_areas/quantization.yaml b/.buildkite/test_areas/quantization.yaml index 1d895e1f2f90..702bbe702452 100644 --- a/.buildkite/test_areas/quantization.yaml +++ b/.buildkite/test_areas/quantization.yaml @@ -20,6 +20,31 @@ steps: # The SM90-only checkpoint currently contains a removed weight_chan_scale # parameter. It was not exercised by the previous L4 job. - VLLM_TEST_FORCE_LOAD_FORMAT=auto pytest -v -s quantization/ --ignore quantization/test_blackwell_moe.py -k 'not test_compressed_tensors_w4a8_fp8' --shard-id=$$BUILDKITE_PARALLEL_JOB --num-shards=$$BUILDKITE_PARALLEL_JOB_COUNT + mirror: + amd: + label: ":amd: (MI355) Quantization Shard %N" + dind: false + device: mi355_1 + timeout_in_minutes: 115 + working_dir: "/vllm-workspace/tests" + depends_on: + - image-build-amd + source_file_dependencies: + - csrc/ + - vllm/model_executor/layers/quantization + - tests/quantization + - vllm/_aiter_ops.py + - vllm/platforms/rocm.py + - vllm/model_executor/layers/fused_moe/oracle/unquantized.py + - vllm/model_executor/layers/fused_moe/unquantized_fused_moe_method.py + - tests/rocm/test_moe_weight_replay.py + commands: + # Use the ROCm default instead of the NVIDIA runner override. + - unset VLLM_USE_V2_MODEL_RUNNER + - uv pip install --system torchao==0.17.0 + - uv pip install --system conch-triton-kernels + - VLLM_TEST_FORCE_LOAD_FORMAT=auto pytest -v -s quantization/ --ignore quantization/test_blackwell_moe.py --shard-id=$$BUILDKITE_PARALLEL_JOB --num-shards=$$BUILDKITE_PARALLEL_JOB_COUNT + - if [ "$$BUILDKITE_PARALLEL_JOB" = "0" ]; then pytest -v -s rocm/test_moe_weight_replay.py; fi - label: ":nvidia: (H200 MIG 35GB) Quantized Fusions" device: h200_35gb @@ -63,3 +88,22 @@ steps: - tests/models/quantization commands: - pytest -v -s models/quantization + mirror: + amd: + label: ":amd: (MI355) Quantized Models" + dind: false + device: mi355_1 + timeout_in_minutes: 80 + working_dir: "/vllm-workspace/tests" + depends_on: + - image-build-amd + source_file_dependencies: + - vllm/model_executor/layers/quantization + - tests/models/quantization + - vllm/_aiter_ops.py + - vllm/platforms/rocm.py + - vllm/model_executor/model_loader/ + commands: + # Use the ROCm default instead of the NVIDIA runner override. + - unset VLLM_USE_V2_MODEL_RUNNER + - pytest -v -s models/quantization diff --git a/.buildkite/test_areas/ray_compat.yaml b/.buildkite/test_areas/ray_compat.yaml index d24c38430b8d..dc8571b26045 100644 --- a/.buildkite/test_areas/ray_compat.yaml +++ b/.buildkite/test_areas/ray_compat.yaml @@ -16,3 +16,19 @@ steps: - setup.py commands: - bash /vllm-workspace/.buildkite/scripts/check-ray-compatibility.sh + mirror: + amd: + label: ":amd: (MI355) Ray Dependency Compatibility Check" + dind: false + device: mi355_1 + timeout_in_minutes: 25 + working_dir: "/" + depends_on: + - image-build-amd + source_file_dependencies: + - .buildkite/scripts/check-ray-compatibility.sh + - requirements/ + - setup.py + - vllm/platforms/rocm.py + commands: + - bash /vllm-workspace/.buildkite/scripts/check-ray-compatibility.sh diff --git a/.buildkite/test_areas/rust_frontend.yaml b/.buildkite/test_areas/rust_frontend.yaml index 8ccc0c50e926..b8d0e58f5810 100644 --- a/.buildkite/test_areas/rust_frontend.yaml +++ b/.buildkite/test_areas/rust_frontend.yaml @@ -3,6 +3,7 @@ depends_on: - image-build steps: - label: ":nvidia: (H200 MIG 18GB) Rust Frontend OpenAI Coverage" + key: rust-frontend-openai-coverage timeout_in_minutes: 30 device: h200_18gb working_dir: "/vllm-workspace/tests" @@ -67,6 +68,7 @@ steps: - vllm/platforms/rocm.py - label: ":nvidia: (H200 MIG 18GB) Rust Frontend Serve/Admin Coverage" + key: rust-frontend-serve-admin-coverage timeout_in_minutes: 25 device: h200_18gb working_dir: "/vllm-workspace/tests" @@ -116,6 +118,7 @@ steps: - vllm/platforms/rocm.py - label: ":nvidia: (H200 MIG 18GB) Rust Frontend Core Correctness" + key: rust-frontend-core-correctness timeout_in_minutes: 20 device: h200_18gb working_dir: "/vllm-workspace/tests" @@ -128,8 +131,23 @@ steps: - export VLLM_USE_RUST_FRONTEND=1 - export VLLM_WORKER_MULTIPROC_METHOD=spawn - pytest -s entrypoints/openai/correctness/test_lmeval.py::test_lm_eval_accuracy_v1_engine + mirror: + amd: + label: ":amd: (MI355) Rust Frontend Core Correctness" + dind: false + device: mi355_1 + timeout_in_minutes: 25 + depends_on: + - image-build-amd + source_file_dependencies: + - rust/ + - vllm/entrypoints/openai/ + - tests/utils.py + - tests/entrypoints/openai/correctness/test_lmeval.py + - vllm/platforms/rocm.py - label: ":nvidia: (H200 MIG 35GB) Rust Frontend Tool Use" + key: rust-frontend-tool-use device: h200_35gb timeout_in_minutes: 25 working_dir: "/vllm-workspace/tests" @@ -143,8 +161,24 @@ steps: - export VLLM_USE_RUST_FRONTEND=1 - export VLLM_WORKER_MULTIPROC_METHOD=spawn - pytest -v -s tool_use --ignore=tool_use/mistral --models llama3.2 -k "not test_response_format_with_tool_choice_required and not test_parallel_tool_calls_false and not test_tool_call_and_choice" + mirror: + amd: + label: ":amd: (MI355) Rust Frontend Tool Use" + dind: false + device: mi355_1 + timeout_in_minutes: 30 + depends_on: + - image-build-amd + source_file_dependencies: + - rust/ + - vllm/entrypoints/openai/ + - vllm/tool_parsers/ + - tests/utils.py + - tests/tool_use/ + - vllm/platforms/rocm.py - label: ":nvidia: (L4) Rust Frontend Distributed" + key: rust-frontend-distributed timeout_in_minutes: 25 device: l4 num_devices: 4 @@ -170,3 +204,27 @@ steps: - TP_SIZE=1 DP_SIZE=4 pytest -v -s v1/distributed/test_internal_lb_dp.py -k "not 4 and not server_info" - TP_SIZE=1 DP_SIZE=2 pytest -v -s v1/distributed/test_external_lb_dp.py -k "not 4 and not server_info" - TP_SIZE=1 DP_SIZE=4 pytest -v -s v1/distributed/test_hybrid_lb_dp.py -k "not 4 and not server_info" + mirror: + amd: + label: ":amd: (MI355) Rust Frontend Distributed" + dind: false + device: mi355_4 + timeout_in_minutes: 45 + env: + VLLM_ENGINE_READY_TIMEOUT_S: "1800" + depends_on: + - image-build-amd + source_file_dependencies: + - rust/ + - vllm/distributed/ + - "!vllm/distributed/kv_transfer/" + - vllm/engine/ + - vllm/v1/executor/ + - vllm/v1/engine/ + - vllm/v1/worker/ + - tests/utils.py + - tests/v1/distributed/test_dense_dp_world_size.py + - tests/v1/distributed/test_external_lb_dp.py + - tests/v1/distributed/test_hybrid_lb_dp.py + - tests/v1/distributed/test_internal_lb_dp.py + - vllm/platforms/rocm.py diff --git a/.buildkite/test_areas/rust_frontend_cargo.yaml b/.buildkite/test_areas/rust_frontend_cargo.yaml index 11bca6252667..53d1da7edf41 100644 --- a/.buildkite/test_areas/rust_frontend_cargo.yaml +++ b/.buildkite/test_areas/rust_frontend_cargo.yaml @@ -14,6 +14,16 @@ steps: - .buildkite/scripts/run-rust-frontend-cargo-ci.sh commands: - .buildkite/scripts/run-rust-frontend-cargo-ci.sh style-clippy + mirror: + amd: + label: ":amd: (MI250) Rust Frontend Cargo Style + Clippy" + dind: false + device: mi250_1 + no_gpu: true + working_dir: "/vllm-workspace" + timeout_in_minutes: 35 + depends_on: + - image-build-amd - label: ":computer: (CPU) Rust Frontend Cargo" key: rust-frontend-cargo-tests @@ -28,3 +38,13 @@ steps: - .buildkite/scripts/run-rust-frontend-cargo-ci.sh commands: - .buildkite/scripts/run-rust-frontend-cargo-ci.sh test + mirror: + amd: + label: ":amd: (MI250) Rust Frontend Cargo" + dind: false + device: mi250_1 + no_gpu: true + working_dir: "/vllm-workspace" + timeout_in_minutes: 35 + depends_on: + - image-build-amd diff --git a/.buildkite/test_areas/samplers.yaml b/.buildkite/test_areas/samplers.yaml index cfde3717c860..084583c8a732 100644 --- a/.buildkite/test_areas/samplers.yaml +++ b/.buildkite/test_areas/samplers.yaml @@ -2,10 +2,10 @@ group: Samplers depends_on: - image-build steps: -- label: ":nvidia: (H200 MIG 35GB) Samplers" - device: h200_35gb +- label: ":nvidia: (H200 MIG 18GB) Samplers" + device: h200_18gb key: samplers-test - timeout_in_minutes: 40 + timeout_in_minutes: 60 source_file_dependencies: - vllm/model_executor/layers - vllm/sampling_metadata.py @@ -15,11 +15,12 @@ steps: commands: # VLLM_USE_FLASHINFER_SAMPLER defaults to 1 now, so we need to pin both # values explicitly to still cover the PyTorch-native (Triton) path. - - VLLM_USE_FLASHINFER_SAMPLER=0 pytest -v -s samplers - - VLLM_USE_FLASHINFER_SAMPLER=1 pytest -v -s samplers + - VLLM_USE_FLASHINFER_SAMPLER=0 pytest -v -s samplers -k "not test_beam_search_passes_multimodal_data" + - VLLM_USE_FLASHINFER_SAMPLER=1 pytest -v -s samplers -k "not test_beam_search_passes_multimodal_data" mirror: amd: label: ":amd: (MI250) Samplers" + dind: false device: mi250_1 timeout_in_minutes: 40 depends_on: @@ -34,4 +35,40 @@ steps: - vllm/_aiter_ops.py - vllm/platforms/rocm.py commands: - - pytest -v -s samplers + - pytest -v -s samplers -k "not test_beam_search_passes_multimodal_data" + +# Qwen2-Audio-7B leaves no room for KV cache on a 16 GiB slice. +- label: ":nvidia: (H200 MIG 35GB) Samplers Multimodal Beam Search" + device: h200_35gb + key: samplers-multimodal-beam-search + timeout_in_minutes: 30 + source_file_dependencies: + - vllm/model_executor/layers + - vllm/sampling_metadata.py + - tests/samplers + - tests/conftest.py + - vllm/entrypoints/generate/beam_search + commands: + - VLLM_USE_FLASHINFER_SAMPLER=0 pytest -v -s samplers/test_beam_search.py::test_beam_search_passes_multimodal_data + - VLLM_USE_FLASHINFER_SAMPLER=1 pytest -v -s samplers/test_beam_search.py::test_beam_search_passes_multimodal_data + mirror: + amd: + label: ":amd: (MI355) Samplers Multimodal Beam Search" + dind: false + device: mi355_1 + num_devices: 1 + timeout_in_minutes: 45 + working_dir: /vllm-workspace/tests + depends_on: + - image-build-amd + source_file_dependencies: + - vllm/model_executor/layers + - vllm/sampling_metadata.py + - vllm/v1/sample/ + - vllm/entrypoints/generate/beam_search/ + - tests/samplers + - tests/conftest.py + - vllm/_aiter_ops.py + - vllm/platforms/rocm.py + commands: + - pytest -v -s samplers/test_beam_search.py::test_beam_search_passes_multimodal_data diff --git a/.buildkite/test_areas/spec_decode.yaml b/.buildkite/test_areas/spec_decode.yaml index 838727035980..fb0130600058 100644 --- a/.buildkite/test_areas/spec_decode.yaml +++ b/.buildkite/test_areas/spec_decode.yaml @@ -47,7 +47,7 @@ steps: label: ":amd: (MI300) Spec Decode Eagle 1: DeepSeek + Qwen" dind: false device: mi300_1 - timeout_in_minutes: 35 + timeout_in_minutes: 60 depends_on: - image-build-amd source_file_dependencies: @@ -74,7 +74,7 @@ steps: label: ":amd: (MI300) Spec Decode Eagle 2: Llama 3 + Qwen VL + Other" dind: false device: mi300_1 - timeout_in_minutes: 25 + timeout_in_minutes: 60 depends_on: - image-build-amd source_file_dependencies: @@ -172,22 +172,24 @@ steps: - tests/v1/e2e/spec_decode/ - vllm/platforms/rocm.py -- label: ":nvidia: (H200 MIG 18GB) Spec Decode Draft Model" +- label: ":nvidia: (H200 MIG 18GB) Spec Decode Draft Model Shard %N" key: spec-decode-draft-model - timeout_in_minutes: 45 + timeout_in_minutes: 60 device: h200_18gb + parallelism: 4 source_file_dependencies: - vllm/v1/spec_decode/ - vllm/v1/worker/gpu/spec_decode/ - tests/v1/e2e/spec_decode/ commands: - - pytest -v -s v1/e2e/spec_decode/draft_model/ + - pytest -v -s v1/e2e/spec_decode/draft_model/ --num-shards=$$BUILDKITE_PARALLEL_JOB_COUNT --shard-id=$$BUILDKITE_PARALLEL_JOB mirror: amd: - label: ":amd: (MI300) Spec Decode Draft Model" + label: ":amd: (MI300) Spec Decode Draft Model Shard %N" dind: false device: mi300_1 - timeout_in_minutes: 55 + timeout_in_minutes: 60 + parallelism: 2 depends_on: - image-build-amd source_file_dependencies: @@ -233,6 +235,9 @@ steps: timeout_in_minutes: 40 depends_on: - image-build-amd + source_file_dependencies: + - vllm/_aiter_ops.py + - vllm/platforms/rocm.py - label: ":nvidia: (B200) Spec Decode DeepSeek MTP Parallel Load" key: spec-decode-deepseek-mtp-parallel-load-2xb200-2xmi300 @@ -280,6 +285,8 @@ steps: - vllm/v1/spec_decode/ - vllm/v1/worker/gpu/spec_decode/ - vllm/model_executor/models/qwen3_dflash.py + - vllm/model_executor/models/qwen3_dflash2.py + - vllm/model_executor/models/qwen3_5.py - vllm/model_executor/models/laguna_dflash.py - tests/v1/e2e/spec_decode/ commands: @@ -290,6 +297,8 @@ steps: dind: false device: mi300_1 timeout_in_minutes: 120 + commands: + - pytest -v -s v1/e2e/spec_decode/acceptance_rates/dflash/ -k "not dflash2" depends_on: - image-build-amd source_file_dependencies: @@ -362,6 +371,33 @@ steps: # Each Gemma4 parametrization is an indivisible ~23m floor. Keep them in # separate shards and put the remaining tests in shard 0, where new tests # land by default. - - test "$$BUILDKITE_PARALLEL_JOB" != "0" || pytest -v -s v1/e2e/spec_decode/acceptance_rates/mtp_other/ --deselect "tests/v1/e2e/spec_decode/acceptance_rates/mtp_other/test_mtp.py::test_gemma4_mtp_acceptance_lengths[False]" --deselect "tests/v1/e2e/spec_decode/acceptance_rates/mtp_other/test_mtp.py::test_gemma4_mtp_acceptance_lengths[True]" - - test "$$BUILDKITE_PARALLEL_JOB" != "1" || pytest -v -s "v1/e2e/spec_decode/acceptance_rates/mtp_other/test_mtp.py::test_gemma4_mtp_acceptance_lengths[False]" - - test "$$BUILDKITE_PARALLEL_JOB" != "2" || pytest -v -s "v1/e2e/spec_decode/acceptance_rates/mtp_other/test_mtp.py::test_gemma4_mtp_acceptance_lengths[True]" + - if [ "$$BUILDKITE_PARALLEL_JOB" = "0" ]; then pytest -v -s v1/e2e/spec_decode/acceptance_rates/mtp_other/ --deselect "tests/v1/e2e/spec_decode/acceptance_rates/mtp_other/test_mtp.py::test_gemma4_mtp_acceptance_lengths[False]" --deselect "tests/v1/e2e/spec_decode/acceptance_rates/mtp_other/test_mtp.py::test_gemma4_mtp_acceptance_lengths[True]"; fi + - if [ "$$BUILDKITE_PARALLEL_JOB" = "1" ]; then pytest -v -s "v1/e2e/spec_decode/acceptance_rates/mtp_other/test_mtp.py::test_gemma4_mtp_acceptance_lengths[False]"; fi + - if [ "$$BUILDKITE_PARALLEL_JOB" = "2" ]; then pytest -v -s "v1/e2e/spec_decode/acceptance_rates/mtp_other/test_mtp.py::test_gemma4_mtp_acceptance_lengths[True]"; fi + mirror: + amd: + label: ":amd: (MI355) Spec Decode AL MTP + Other Acceptance Nightly" + dind: false + device: mi355_1 + timeout_in_minutes: 55 + depends_on: + - image-build-amd + working_dir: /vllm-workspace/tests + source_file_dependencies: + - vllm/v1/spec_decode/ + - vllm/v1/sample/ + - vllm/v1/worker/gpu/ + - vllm/v1/worker/gpu_model_runner.py + - vllm/v1/worker/gpu_worker.py + - vllm/model_executor/model_loader/ + - vllm/model_executor/models/gemma4.py + - vllm/model_executor/models/gemma4_mm.py + - vllm/model_executor/models/gemma4_mtp.py + - vllm/model_executor/models/llama.py + - vllm/model_executor/models/llama_eagle3.py + - vllm/model_executor/models/medusa.py + - vllm/model_executor/models/registry.py + - tests/evals/gsm8k/ + - tests/utils.py + - tests/v1/e2e/spec_decode/ + - vllm/platforms/rocm.py diff --git a/.buildkite/test_areas/torch_abi.yaml b/.buildkite/test_areas/torch_abi.yaml index 9e8375587adb..201f9b6093a3 100644 --- a/.buildkite/test_areas/torch_abi.yaml +++ b/.buildkite/test_areas/torch_abi.yaml @@ -4,7 +4,7 @@ depends_on: steps: - label: ":nvidia: (L4) Torch Stable ABI Audit" key: torch-stable-abi-audit - timeout_in_minutes: 5 + timeout_in_minutes: 10 source_file_dependencies: - .buildkite/check-torch-abi.py - csrc/ diff --git a/.buildkite/test_areas/weight_loading.yaml b/.buildkite/test_areas/weight_loading.yaml deleted file mode 100644 index 154b355778b2..000000000000 --- a/.buildkite/test_areas/weight_loading.yaml +++ /dev/null @@ -1,38 +0,0 @@ -group: Weight Loading -depends_on: - - image-build -steps: -- label: ":nvidia: (L4) Weight Loading Multi-GPU" - key: weight-loading-multiple-gpu - timeout_in_minutes: 50 - working_dir: "/vllm-workspace/tests" - device: l4 - num_devices: 2 - optional: true - source_file_dependencies: - - vllm/ - - "!vllm/distributed/kv_transfer/" - - tests/weight_loading - commands: - - bash weight_loading/run_model_weight_loading_test.sh -c weight_loading/models.txt - mirror: - amd: - label: ":amd: (MI355) Weight Loading Multi-GPU" - dind: false - device: mi355_2 - timeout_in_minutes: 40 - depends_on: - - image-build-amd - commands: - - bash weight_loading/run_model_weight_loading_test.sh -c weight_loading/models-amd.txt - -# - label: Weight Loading Multiple GPU - Large Models # optional -# working_dir: "/vllm-workspace/tests" -# num_devices: 2 -# device: a100 -# optional: true -# source_file_dependencies: -# - vllm/ -# - tests/weight_loading -# commands: -# - bash weight_loading/run_model_weight_loading_test.sh -c weight_loading/models-large.txt diff --git a/.gemini/config.yaml b/.gemini/config.yaml deleted file mode 100644 index 2499d3f09510..000000000000 --- a/.gemini/config.yaml +++ /dev/null @@ -1,6 +0,0 @@ -# https://developers.google.com/gemini-code-assist/docs/customize-gemini-behavior-github -have_fun: false # Just review the code -code_review: - comment_severity_threshold: HIGH # Reduce quantity of comments - pull_request_opened: - summary: false # Don't summarize the PR in a separate comment diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 1f605db84a7e..f64cf5f56107 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -34,9 +34,9 @@ # Entrypoints /vllm/entrypoints/anthropic @mgoin @DarkLight1337 -/vllm/entrypoints/cli @hmellor @mgoin @DarkLight1337 @russellb +/vllm/entrypoints/cli @hmellor @mgoin @DarkLight1337 /vllm/entrypoints/mcp @heheda12345 -/vllm/entrypoints/openai @aarnphm @chaunceyjiang @DarkLight1337 @russellb +/vllm/entrypoints/openai @aarnphm @chaunceyjiang @DarkLight1337 /vllm/entrypoints/speech_to_text/realtime @njhill /vllm/entrypoints/speech_to_text @NickLucche /vllm/entrypoints/pooling @noooop @@ -73,7 +73,7 @@ /vllm/v1/core @WoosukKwon @robertgshaw2-redhat @njhill @ywang96 @alexm-redhat @heheda12345 @ApostaC @orozery @ivanium /vllm/v1/sample @22quinn @houseroad @njhill /vllm/v1/spec_decode @benchislett @luccafong @MatthewBonanni -/vllm/v1/structured_output @mgoin @russellb @aarnphm @benchislett +/vllm/v1/structured_output @mgoin @aarnphm @benchislett @arpera /vllm/v1/kv_cache_interface.py @heheda12345 @ivanium /vllm/v1/kv_offload @ApostaC @orozery /vllm/v1/simple_kv_offload @ivanium @@ -107,10 +107,9 @@ /tests/multimodal @DarkLight1337 @ywang96 @NickLucche @shen-shanshan /tests/multimodal/media @Isotr0py /tests/quantization @mgoin @robertgshaw2-redhat @yewentao256 @pavanimajety @zyongye @AndreasKaratzas -/tests/entrypoints/llm/test_struct_output_generate.py @mgoin @russellb @aarnphm -/tests/v1/structured_output @mgoin @russellb @aarnphm +/tests/entrypoints/llm/test_struct_output_generate.py @mgoin @aarnphm @arpera +/tests/v1/structured_output @mgoin @aarnphm @arpera /tests/v1/core @WoosukKwon @robertgshaw2-redhat @njhill @ywang96 @alexm-redhat @heheda12345 @ApostaC @orozery @ivanium -/tests/weight_loading @mgoin @youkaichao @yewentao256 /tests/lora @jeejeelee /tests/models/language/generation/test_hybrid.py @tdoublep @tomeras91 /tests/v1/kv_connector/nixl_integration @NickLucche @@ -232,6 +231,6 @@ mkdocs.yaml @hmellor /docs/training/weight_transfer @aoshen02 # Security guide and policies -/docs/usage/security.md @russellb @jperezdealgaba -/SECURITY.md @russellb @jperezdealgaba -/docs/contributing/vulnerability_management.md @russellb @jperezdealgaba +/docs/usage/security.md @jperezdealgaba +/SECURITY.md @jperezdealgaba +/docs/contributing/vulnerability_management.md @jperezdealgaba diff --git a/.github/mergify.yml b/.github/mergify.yml index 04a623a499d1..1267f0264dfe 100644 --- a/.github/mergify.yml +++ b/.github/mergify.yml @@ -330,12 +330,31 @@ pull_request_rules: - label != stale - or: - files~=^vllm/models/deepseek_v4/ - - title~=(?i)(?:\bDSv4\b|deepseek[-\s]?v4) + # Trailing (?![._]?1) keeps V4.1 out: DeepseekV41ForCausalLM is a + # distinct architecture with its own tree and label (DSv4.1). + - title~=(?i)(?:\bDSv4\b|deepseek[-\s_]?v4)(?![._]?1) actions: label: add: - DSv4 +- name: label-dsv4-1 + description: Automatically apply DSv4.1 label + conditions: + - label != stale + - or: + # The 4.1 sources use deepseek_v41 and deepseekv41 (tool parsers). + - files~=^vllm/models/deepseek_v41/ + - files~=^vllm/(?:parser|tokenizers|reasoning|transformers_utils/configs)/deepseek_v41.*\.py + - files~=^vllm/tool_parsers/deepseekv41.*\.py + - files~=^rust/src/(?:chat|parser)/src/.*deepseek_v41 + - files~=^tests/.*deepseek_?v41 + - title~=(?i)(?:\bDSv4[._]?1\b|deepseek[-\s_]?v4[._]?1) + actions: + label: + add: + - DSv4.1 + - name: label-nvidia description: Automatically apply nvidia label conditions: diff --git a/.github/workflows/buf.yml b/.github/workflows/buf.yml index fda4add1d726..dd695d28d529 100644 --- a/.github/workflows/buf.yml +++ b/.github/workflows/buf.yml @@ -5,12 +5,6 @@ on: paths: - "rust/proto/**" - ".github/workflows/buf.yml" - push: - tags: - - "v*" - schedule: - - cron: "0 8 * * *" - workflow_dispatch: permissions: contents: read @@ -21,37 +15,14 @@ concurrency: jobs: proto: - if: github.repository_owner == 'vllm-project' runs-on: ubuntu-latest steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - fetch-depth: 0 - uses: bufbuild/buf-action@8c6a16e16f12ba20b6470afa9c2ba9b5ba8c97c3 # v1.5.0 with: version: 1.72.0 input: rust/proto - token: ${{ github.repository == 'vllm-project/vllm' && github.event_name != 'pull_request' && secrets.BUF_TOKEN || '' }} format: false breaking: false pr_comment: false push: false - - name: Publish nightly schema - if: github.repository == 'vllm-project/vllm' && (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') - run: >- - buf push rust/proto - --create - --create-default-label main - --create-visibility public - --label nightly - --source-control-url "${{ github.server_url }}/${{ github.repository }}/commit/${{ github.sha }}" - - name: Publish release schema - if: github.repository == 'vllm-project/vllm' && github.event_name == 'push' - run: >- - buf push rust/proto - --create - --create-default-label main - --create-visibility public - --label main - --label "${GITHUB_REF_NAME}" - --source-control-url "${{ github.server_url }}/${{ github.repository }}/commit/${{ github.sha }}" diff --git a/.github/workflows/issue_autolabel.yml b/.github/workflows/issue_autolabel.yml index b2392e87ce5c..fc6e4921ef83 100644 --- a/.github/workflows/issue_autolabel.yml +++ b/.github/workflows/issue_autolabel.yml @@ -197,9 +197,12 @@ jobs: deepseek: { keywords: [{ term: "DeepSeek", searchIn: "title" }], }, + // DSv4 and DSv4.1 are distinct architectures; these keep them exclusive. DSv4: { - keywords: [{ term: "DSv4", searchIn: "title" }], - substrings: [{ term: "deepseek-v4", searchIn: "title" }], + regexPatterns: [{ pattern: "(?:\\bDSv4\\b|deepseek[-\\s_]?v4)(?![._]?1)", flags: "gi", searchIn: "title" }], + }, + "DSv4.1": { + regexPatterns: [{ pattern: "(?:\\bDSv4[._]?1\\b|deepseek[-\\s_]?v4[._]?1)", flags: "gi", searchIn: "title" }], }, llama: { keywords: [{ term: "Llama", searchIn: "title" }], diff --git a/.github/workflows/proto-crate.yml b/.github/workflows/proto-crate.yml new file mode 100644 index 000000000000..43be5c8c9214 --- /dev/null +++ b/.github/workflows/proto-crate.yml @@ -0,0 +1,70 @@ +name: Rust proto crate + +on: + pull_request: + paths: + - "rust/proto/**" + - "rust/Cargo.toml" + - "rust/Cargo.lock" + - "rust-toolchain.toml" + - ".github/workflows/proto-crate.yml" + push: + tags: + - "proto-v*" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +jobs: + semver: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: obi1kenobi/cargo-semver-checks-action@6b69fcf40e9b5fb17adeb57e4b6ecd020649a239 # v2 + with: + manifest-path: rust/proto/Cargo.toml + package: vllm-proto + + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - run: rustup show active-toolchain + - name: Check release version + if: github.event_name == 'push' + run: | + version=$(cargo metadata --manifest-path rust/Cargo.toml --no-deps --format-version 1 | + jq -er '.packages[] | select(.name == "vllm-proto") | .version') + if [[ "$GITHUB_REF_NAME" != "proto-v$version" ]]; then + echo "Tag $GITHUB_REF_NAME does not match vllm-proto version $version" >&2 + exit 1 + fi + - name: Verify package + run: cargo publish --manifest-path rust/proto/Cargo.toml --locked --dry-run + + publish: + needs: [check, semver] + if: github.repository == 'vllm-project/vllm' && github.event_name == 'push' + runs-on: ubuntu-latest + environment: crates-io + permissions: + contents: read + id-token: write + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + 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 + id: auth + - name: Publish + env: + CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }} + run: cargo publish --manifest-path rust/proto/Cargo.toml --locked diff --git a/.github/workflows/run-ci-command.yml b/.github/workflows/run-ci-command.yml index 747831c7d4c8..3435148ea4e3 100644 --- a/.github/workflows/run-ci-command.yml +++ b/.github/workflows/run-ci-command.yml @@ -21,11 +21,17 @@ jobs: (github.event.comment.body == '/ci run' || github.event.comment.body == '/ci run all' || github.event.comment.body == '/ci run nightly' || + github.event.comment.body == '/ci run --allow-stale' || + github.event.comment.body == '/ci run all --allow-stale' || + github.event.comment.body == '/ci run nightly --allow-stale' || github.event.comment.body == '/ci retry' || github.event.comment.body == '/ci cancel' || github.event.comment.body == '/amd-ci run' || github.event.comment.body == '/amd-ci run all' || github.event.comment.body == '/amd-ci run nightly' || + github.event.comment.body == '/amd-ci run --allow-stale' || + github.event.comment.body == '/amd-ci run all --allow-stale' || + github.event.comment.body == '/amd-ci run nightly --allow-stale' || github.event.comment.body == '/amd-ci retry' || github.event.comment.body == '/amd-ci cancel') runs-on: [self-hosted, linux, x64, vllm-runners] diff --git a/.github/workflows/scripts/run_ci_command.py b/.github/workflows/scripts/run_ci_command.py index 6454f041ff06..530bd697d008 100644 --- a/.github/workflows/scripts/run_ci_command.py +++ b/.github/workflows/scripts/run_ci_command.py @@ -1,6 +1,7 @@ # SPDX-License-Identifier: Apache-2.0 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project +import http.client import json import os import random @@ -22,6 +23,7 @@ COMMAND_RUN_AMD_CI_NIGHTLY = "/amd-ci run nightly" COMMAND_RETRY_AMD_FAILED = "/amd-ci retry" COMMAND_CANCEL_AMD_CI = "/amd-ci cancel" +ALLOW_STALE_SUFFIX = " --allow-stale" RUN_CI_COMMAND_ENV = { COMMAND_RUN_CI: {}, COMMAND_RUN_CI_ALL: {"RUN_ALL": "1"}, @@ -30,20 +32,25 @@ COMMAND_RUN_AMD_CI_ALL: {"RUN_ALL": "1"}, COMMAND_RUN_AMD_CI_NIGHTLY: {"RUN_ALL": "1", "NIGHTLY": "1"}, } +ALLOW_STALE_COMMANDS = frozenset( + f"{command}{ALLOW_STALE_SUFFIX}" for command in RUN_CI_COMMAND_ENV +) +RUN_CI_COMMAND_ENV.update( + { + command: RUN_CI_COMMAND_ENV[command.removesuffix(ALLOW_STALE_SUFFIX)] + for command in ALLOW_STALE_COMMANDS + } +) UPSTREAM_CI_COMMANDS = frozenset( { - COMMAND_RUN_CI, - COMMAND_RUN_CI_ALL, - COMMAND_RUN_CI_NIGHTLY, + *(command for command in RUN_CI_COMMAND_ENV if command.startswith("/ci ")), COMMAND_RETRY_FAILED, COMMAND_CANCEL_CI, } ) AMD_CI_COMMANDS = frozenset( { - COMMAND_RUN_AMD_CI, - COMMAND_RUN_AMD_CI_ALL, - COMMAND_RUN_AMD_CI_NIGHTLY, + *(command for command in RUN_CI_COMMAND_ENV if command.startswith("/amd-ci ")), COMMAND_RETRY_AMD_FAILED, COMMAND_CANCEL_AMD_CI, } @@ -79,6 +86,10 @@ def __init__(self, status: int | None, message: str) -> None: self.status = status +class CiPreparationError(RuntimeError): + pass + + def rate_limit_jitter() -> float: return random.uniform(1, 5) @@ -137,6 +148,8 @@ def request( None, f"API request failed: {error.reason}", ) from error + except (OSError, http.client.HTTPException) as error: + raise ApiError(None, f"API request failed: {error}") from error if not response_body: return None @@ -234,6 +247,17 @@ def _paginate(self, path: str) -> list[dict[str, Any]]: def get_pr(self, number: int) -> dict[str, Any]: return self._request(self._repo_path(f"/pulls/{number}")) + def get_commits_behind_base(self, base_ref: str, head_sha: str) -> int: + base_ref = urllib.parse.quote(f"refs/heads/{base_ref}", safe="") + head_sha = urllib.parse.quote(head_sha, safe="") + response = self._request( + self._repo_path(f"/compare/{base_ref}...{head_sha}?per_page=1") + ) + behind = response.get("behind_by") if isinstance(response, dict) else None + if type(behind) is not int or behind < 0: + raise ApiError(None, "GitHub returned an invalid commits-behind count.") + return behind + def list_pulls_for_commit(self, commit: str) -> list[dict[str, Any]]: commit = urllib.parse.quote(commit, safe="") response = self._request(self._repo_path(f"/commits/{commit}/pulls")) @@ -747,6 +771,10 @@ def notify_authorized( ( f"✅ @{author}, CI is now available for this PR.\n\n" "- `/ci run` starts upstream CI; `/amd-ci run` starts AMD CI only.\n" + "- Your branch must contain every commit currently on its upstream " + "target branch. Merge or rebase onto the latest target branch, then " + "rerun the command. Append `--allow-stale` to a run command to test " + "an outdated branch at your own risk.\n" "- `/ci retry` retries failed jobs in the CI build for the current " "PR head. If the current head has no CI build, it starts a new CI " "build for the current head containing only jobs that failed in " @@ -794,6 +822,56 @@ def find_matching_pr( return find_matching_pr(github.list_pulls_for_commit(head_sha)) +def prepare_pr_for_ci( + github: GitHubClient, + pr: Mapping[str, Any], + command: str, +) -> tuple[dict[str, Any], str]: + base_ref = pr["base"]["ref"] + no_action = "No new CI build was started." + try: + behind = github.get_commits_behind_base(base_ref, pr["head"]["sha"]) + current_pr = github.get_pr(pr["number"]) + except ApiError as error: + raise CiPreparationError( + f"Could not check the PR against upstream `{base_ref}`. " + f"{no_action} Comment `{command}` again. {error}" + ) from error + if ( + current_pr["state"] != "open" + or current_pr["head"]["sha"] != pr["head"]["sha"] + or current_pr["base"]["ref"] != pr["base"]["ref"] + ): + raise CiPreparationError( + f"The PR changed while checking its branch. {no_action} " + f"Comment `{command}` again." + ) + commits = "commit" if behind == 1 else "commits" + lag = f"This PR is {behind} {commits} behind upstream `{base_ref}`." + if command in ALLOW_STALE_COMMANDS: + warning = "" + if behind: + run_command = command.removesuffix(ALLOW_STALE_SUFFIX) + warning = ( + f"⚠️ {lag} Running CI at your own risk because " + "`--allow-stale` was requested; outdated CI configuration may " + "cause failures. Before merging, merge or rebase onto the latest " + f"`{base_ref}`, then rerun `{run_command}` on the latest PR commit." + ) + print(warning) + return current_pr, warning + + if behind: + raise CiPreparationError( + f"{lag} Your branch must contain every commit currently on upstream " + f"`{base_ref}`. {no_action} " + f"Merge or rebase onto the latest `{base_ref}`, then rerun `{command}`." + " To test this branch at your own risk, " + f"use `{command}{ALLOW_STALE_SUFFIX}`." + ) + return current_pr, "" + + def handle_run_ci( *, actor: str, @@ -828,12 +906,7 @@ def handle_run_ci( f"{ci_name} is already running for this commit: {active_build['web_url']}" ) - current_pr = github.get_pr(pr["number"]) - if current_pr["state"] != "open" or current_pr["head"]["sha"] != pr["head"]["sha"]: - return ( - "The PR head changed while processing the command. " - f"Comment `{command}` again." - ) + current_pr, warning = prepare_pr_for_ci(github, pr, command) build = buildkite.create_build( create_build_payload( @@ -846,6 +919,7 @@ def handle_run_ci( return ( f"Triggered [Buildkite {ci_name} #{build['number']}]({build['web_url']}) " f"for commit `{current_pr['head']['sha'][:12]}`." + + (f"\n\n{warning}" if warning else "") ) @@ -1099,6 +1173,12 @@ def run( raise ValueError(f"Unsupported CI command: {command}") add_reaction_safely(github, comment_id, "rocket") github.add_comment(issue_number, f"✅ {message}") + except CiPreparationError as error: + add_reaction_safely(github, comment_id, "-1") + github.add_comment( + issue_number, + f"❌ {error}\n\n{command_comment_marker(comment_id)}", + ) except Exception: add_reaction_safely(github, comment_id, "confused") raise diff --git a/.github/workflows/scripts/test_run_ci_command.py b/.github/workflows/scripts/test_run_ci_command.py index ef93ea8559c8..9750cefaa394 100644 --- a/.github/workflows/scripts/test_run_ci_command.py +++ b/.github/workflows/scripts/test_run_ci_command.py @@ -22,8 +22,10 @@ COMMAND_RUN_CI_ALL, COMMAND_RUN_CI_NIGHTLY, RETRY_STATES, + RUN_CI_COMMAND_ENV, ApiError, BuildkiteClient, + GitHubClient, HttpTransport, authorize, create_build_payload, @@ -85,6 +87,7 @@ def __init__( comments: list[str] | None = None, pulls_for_commit: list[dict[str, Any]] | None = None, prs: dict[int, dict[str, Any]] | None = None, + behind: int = 0, ) -> None: self.comments = comments or [] self.permission = permission @@ -95,6 +98,12 @@ def __init__( self.reviews = reviews or [] self.pulls_for_commit = pulls_for_commit or [] self.prs = prs or {self.pr["number"]: self.pr} + self.behind = behind + self.lag_queries: list[tuple[str, str]] = [] + + def get_commits_behind_base(self, base_ref: str, head_sha: str) -> int: + self.lag_queries.append((base_ref, head_sha)) + return self.behind def get_pr(self, number: int) -> dict[str, Any]: try: @@ -322,6 +331,11 @@ def test_only_exact_ci_commands_are_accepted(self) -> None: for command in commands: with self.subTest(command=command): self.assertEqual(parse_command(command), command) + override = f"{command} --allow-stale" + if " run" in command: + self.assertEqual(parse_command(override), override) + else: + self.assertIsNone(parse_command(override)) self.assertIsNone(parse_command("/ci run please")) self.assertIsNone(parse_command("/ci run all please")) @@ -331,6 +345,8 @@ def test_only_exact_ci_commands_are_accepted(self) -> None: self.assertIsNone(parse_command("/amd-ci retry ")) self.assertIsNone(parse_command("/AMD-CI run")) self.assertIsNone(parse_command("/amdci run")) + self.assertIsNone(parse_command("/ci run --allow-stale all")) + self.assertIsNone(parse_command("/amd-ci run --allow-stale please")) def test_commands_select_only_their_configured_pipeline(self) -> None: cases = ( @@ -530,6 +546,177 @@ def test_write_reviewer_runs_ci_without_delegation(self) -> None: self.assertTrue(github.comments[0].startswith("✅ ")) self.assertIn("Buildkite CI #123", github.comments[0]) + def test_run_variants_require_current_base_without_changing_commit(self) -> None: + for command in RUN_CI_COMMAND_ENV: + if command.endswith(" --allow-stale"): + continue + for behind in (0, 1, 5): + with self.subTest(command=command, behind=behind): + github = FakeGitHub( + behind=behind, pr=make_pr(base={"ref": "release/1.0"}) + ) + buildkite = FakeBuildkite([[], []]) + run(make_event(command), github, buildkite) + + self.assertEqual( + github.lag_queries, + [("release/1.0", github.pr["head"]["sha"])], + ) + if behind == 0: + self.assertEqual( + buildkite.created_builds[0]["commit"], + github.pr["head"]["sha"], + ) + self.assertIn( + "](https://buildkite.example/builds/123)", + github.comments[0], + ) + else: + self.assertEqual(buildkite.created_builds, []) + self.assertTrue(github.comments[0].startswith("❌ ")) + self.assertIn(str(behind), github.comments[0]) + self.assertIn("upstream `release/1.0`", github.comments[0]) + self.assertIn("every commit", github.comments[0]) + self.assertIn( + "No new CI build was started.", github.comments[0] + ) + self.assertIn(command, github.comments[0]) + + def test_allow_stale_preserves_author_permissions_and_run_configuration( + self, + ) -> None: + cases = ( + (COMMAND_RUN_CI, "ci", {}), + (COMMAND_RUN_CI_ALL, "ci", {"RUN_ALL": "1"}), + (COMMAND_RUN_CI_NIGHTLY, "ci", {"RUN_ALL": "1", "NIGHTLY": "1"}), + (COMMAND_RUN_AMD_CI, "amd-ci", {}), + (COMMAND_RUN_AMD_CI_ALL, "amd-ci", {"RUN_ALL": "1"}), + ( + COMMAND_RUN_AMD_CI_NIGHTLY, + "amd-ci", + {"RUN_ALL": "1", "NIGHTLY": "1"}, + ), + ) + for original_command, pipeline, expected_env in cases: + command = f"{original_command} --allow-stale" + for ready, behind in ((False, 100), (True, 0), (True, 100)): + with self.subTest(command=command, ready=ready, behind=behind): + github = FakeGitHub( + permission="read", + behind=behind, + pr=make_pr( + base={"ref": "feature/stack"}, + labels=[{"name": "ready"}] if ready else [], + ), + ) + buildkite = FakeBuildkite([[], []]) + + run(make_event(command, "author"), github, buildkite) + + self.assertEqual(pipeline_for_command(command), pipeline) + if not ready: + self.assertEqual(buildkite.created_builds, []) + self.assertEqual(buildkite.list_calls, []) + self.assertEqual(github.lag_queries, []) + self.assertTrue(github.comments[0].startswith("❌ ")) + continue + self.assertEqual(len(buildkite.created_builds), 1) + payload = buildkite.created_builds[0] + self.assertEqual(payload["commit"], github.pr["head"]["sha"]) + self.assertEqual( + payload["pull_request_base_branch"], "feature/stack" + ) + self.assertEqual(payload["message"], f"PR #42 {command} by @author") + self.assertEqual( + payload["env"], + { + "VLLM_CI_GITHUB_COMMENT_ID": "99", + "VLLM_CI_TRIGGERED_BY": "author", + **expected_env, + }, + ) + self.assertEqual( + github.lag_queries, + [("feature/stack", github.pr["head"]["sha"])], + ) + self.assertIn( + "Buildkite AMD CI #123" + if pipeline == "amd-ci" + else "Buildkite CI #123", + github.comments[0], + ) + if behind: + self.assertIn("100 commits behind", github.comments[0]) + self.assertIn("upstream `feature/stack`", github.comments[0]) + self.assertIn("Before merging", github.comments[0]) + self.assertIn("latest PR commit", github.comments[0]) + self.assertIn(original_command, github.comments[0]) + + def test_base_lag_query_qualifies_branch_refs_and_validates_count(self) -> None: + for base_ref, encoded_ref in ( + ("main", "main"), + ("release/1.0", "release%2F1.0"), + ("feature/stack", "feature%2Fstack"), + ): + for count in (0, 5, None, -1, True, "5"): + with self.subTest(base_ref=base_ref, count=count): + transport = FakeTransport( + {} if count is None else {"behind_by": count} + ) + github = GitHubClient("token", "vllm-project/vllm", transport) + if type(count) is int and count >= 0: + self.assertEqual( + github.get_commits_behind_base(base_ref, "head-sha"), count + ) + else: + with self.assertRaises(ApiError): + github.get_commits_behind_base(base_ref, "head-sha") + self.assertEqual( + transport.calls[0]["url"], + "https://api.github.com/repos/vllm-project/vllm/compare/" + f"refs%2Fheads%2F{encoded_ref}...head-sha?per_page=1", + ) + self.assertEqual(transport.calls[0]["method"], "GET") + + def test_unknown_lag_or_changed_pr_prevents_runs(self) -> None: + for command in ( + COMMAND_RUN_CI, + COMMAND_RUN_AMD_CI, + f"{COMMAND_RUN_CI} --allow-stale", + f"{COMMAND_RUN_AMD_CI} --allow-stale", + ): + for fault in ("api", "head", "closed", "base"): + with self.subTest(command=command, fault=fault): + github = FakeGitHub() + buildkite = FakeBuildkite([[], []]) + + def check( + base_ref: str, + sha: str, + fault: str = fault, + github: FakeGitHub = github, + ) -> int: + if fault == "api": + raise ApiError(503, "Unavailable") + changed = make_pr() + if fault == "head": + changed["head"]["sha"] = "another-head" + elif fault == "closed": + changed["state"] = "closed" + else: + changed["base"]["ref"] = "another-base" + github.prs[42] = changed + return 0 + + with patch.object( + github, "get_commits_behind_base", side_effect=check + ): + run(make_event(command), github, buildkite) + self.assertEqual(buildkite.created_builds, []) + self.assertEqual(buildkite.retry_calls, []) + self.assertTrue(github.comments[0].startswith("❌ ")) + self.assertNotIn("Triggered", github.comments[0]) + def test_amd_run_ignores_blocked_builds(self) -> None: for metadata in ({}, {"github-comment-id": "98"}): with self.subTest(metadata=metadata): @@ -627,6 +814,7 @@ def test_unapproved_authors_are_denied_without_buildkite(self) -> None: run(make_event(COMMAND_RUN_CI, "author"), github, buildkite) self.assertEqual(buildkite.list_calls, []) + self.assertEqual(github.lag_queries, []) self.assertEqual(github.reactions, ["eyes"]) self.assertTrue(github.comments[0].startswith("❌ ")) self.assertIn("approve the PR", github.comments[0]) @@ -848,6 +1036,7 @@ def test_notification_skips_draft_prs(self) -> None: def test_ci_retry_retries_failed_jobs_while_build_is_running(self) -> None: github = FakeGitHub( + behind=100, permission="read", pr=make_pr(labels=[{"name": "ready"}]), ) @@ -867,10 +1056,12 @@ def test_ci_retry_retries_failed_jobs_while_build_is_running(self) -> None: run(make_event(COMMAND_RETRY_FAILED, "author"), github, buildkite) self.assertEqual(buildkite.retry_calls, [(123, RETRY_STATES)]) + self.assertEqual(github.lag_queries, []) self.assertIn("Queued 3 failed job", github.comments[0]) def test_amd_ci_retry_retries_only_the_current_head_build(self) -> None: github = FakeGitHub( + behind=100, permission="read", pr=make_pr(labels=[{"name": "ready"}]), ) @@ -891,6 +1082,7 @@ def test_amd_ci_retry_retries_only_the_current_head_build(self) -> None: run(make_event(COMMAND_RETRY_AMD_FAILED, "author"), github, buildkite) self.assertEqual(buildkite.retry_calls, [(321, RETRY_STATES)]) + self.assertEqual(github.lag_queries, []) self.assertIn("Buildkite AMD CI #321", github.comments[0]) def test_amd_ci_retry_requires_a_build_for_the_current_head(self) -> None: @@ -912,6 +1104,7 @@ def test_amd_ci_retry_requires_a_build_for_the_current_head(self) -> None: def test_ci_retry_creates_filtered_build_for_new_head(self) -> None: github = FakeGitHub( + behind=100, permission="read", pr=make_pr(labels=[{"name": "ready"}]), ) @@ -953,6 +1146,7 @@ def test_ci_retry_creates_filtered_build_for_new_head(self) -> None: self.assertEqual(len(buildkite.created_builds), 1) payload = buildkite.created_builds[0] self.assertEqual(payload["commit"], "0123456789abcdef") + self.assertEqual(github.lag_queries, []) self.assertEqual(payload["message"], "PR #42 /ci retry by @author") self.assertEqual( json.loads(payload["env"]["VLLM_CI_ONLY_STEP_KEYS"]), @@ -1104,6 +1298,7 @@ def test_ci_cancel_cancels_active_builds_for_pr_branch(self) -> None: run(make_event(COMMAND_CANCEL_CI), github, buildkite) self.assertEqual(buildkite.cancel_calls, [123, 124, 126]) + self.assertEqual(github.lag_queries, []) self.assertEqual( [request["branch"] for request in buildkite.list_requests], ["contributor:feature", "feature"], @@ -1152,6 +1347,7 @@ def test_amd_ci_cancel_handles_command_and_fork_webhook_branches(self) -> None: run(make_event(COMMAND_CANCEL_AMD_CI), github, buildkite) self.assertEqual(buildkite.cancel_calls, [322, 321]) + self.assertEqual(github.lag_queries, []) self.assertEqual( [request["branch"] for request in buildkite.list_requests], ["contributor:feature", "feature"], diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0954c53cb290..4823f788dc3a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -177,6 +177,21 @@ repos: files: ^\.github/mergify\.yml$ pass_filenames: false additional_dependencies: [PyYAML, regex] + - id: check-buildkite-step-keys + name: Check every Buildkite step declares an explicit key + language: python + entry: python tools/pre_commit/check_buildkite_step_keys.py + files: ^\.buildkite/.*\.ya?ml$ + pass_filenames: false + additional_dependencies: [PyYAML] + - id: check-test-tethering + name: Check new tests are wired into Buildkite CI + language: python + entry: python tools/pre_commit/check_test_tethering.py + files: ^(tests/.*\.py|\.buildkite/(test_areas|hardware_tests|intel_jobs)/.*\.yaml|\.buildkite/(test-amd|ci_config[^/]*)\.yaml|tools/pre_commit/test_tethering_allowlist\.txt)$ + additional_dependencies: [PyYAML, regex] + verbose: true # full-scan `note:` lines are advisory, so the hook passes + require_serial: true # one full scan, not one per pre-commit file batch - id: mypy-3.10 # TODO: Use https://github.com/pre-commit/mirrors-mypy when mypy setup is less awkward name: Run mypy for Python 3.10 entry: python tools/pre_commit/mypy.py "3.10" @@ -285,7 +300,7 @@ repos: pass_filenames: false require_serial: true stages: [pre-commit] # Only run locally as Buildkite will cover this - files: ^rust/(Cargo\.toml|src/.*/Cargo\.toml)$ + files: ^rust/(Cargo\.toml|proto/Cargo\.toml|src/.*/Cargo\.toml)$ - id: rust-cargo-sort name: Rust - Sort Cargo manifest sections entry: tools/pre_commit/rust-check.sh sort --workspace @@ -293,7 +308,7 @@ repos: pass_filenames: false require_serial: true stages: [pre-commit] # Only run locally as Buildkite will cover this - files: ^rust/(Cargo\.toml|src/.*/Cargo\.toml)$ + files: ^rust/(Cargo\.toml|proto/Cargo\.toml|src/.*/Cargo\.toml)$ - id: rust-cargo-fmt name: Rust - Format code entry: tools/pre_commit/rust-check.sh fmt diff --git a/CMakeLists.txt b/CMakeLists.txt index a7d40ae58603..58921b93690d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -460,6 +460,8 @@ if(VLLM_GPU_LANG STREQUAL "CUDA" OR VLLM_GPU_LANG STREQUAL "HIP") DEFINED CMAKE_CUDA_COMPILER_VERSION AND CMAKE_CUDA_COMPILER_VERSION VERSION_GREATER_EQUAL 12.0) + list(APPEND VLLM_STABLE_EXT_SRC "csrc/libtorch_stable/all_reduce_mhc.cu") + if(${CMAKE_CUDA_COMPILER_VERSION} VERSION_GREATER_EQUAL 13.0) cuda_archs_loose_intersection(COOPERATIVE_TOPK_ARCHS "9.0a;10.0f;10.1f;10.3f;10.7f;11.0f;12.0f;12.1f" "${CUDA_ARCHS}") @@ -476,6 +478,7 @@ if(VLLM_GPU_LANG STREQUAL "CUDA" OR VLLM_GPU_LANG STREQUAL "HIP") if(VLLM_GPU_LANG STREQUAL "CUDA") list(APPEND VLLM_STABLE_EXT_SRC + "csrc/libtorch_stable/hisparse_kernels.cu" "csrc/libtorch_stable/attention/dcp_utils/dcp_direct_a2a_lse_reduce.cu" "csrc/libtorch_stable/attention/dcp_utils/dcp_direct_kv_gather.cu" "csrc/libtorch_stable/attention/dcp_utils/dcp_direct_q_gather.cu") @@ -774,22 +777,6 @@ if(VLLM_GPU_LANG STREQUAL "CUDA" OR VLLM_GPU_LANG STREQUAL "HIP") "(requires SM90+ and CUDA >= 12.0).") endif() - # Only build AllSpark kernels if we are building for at least some compatible archs. - cuda_archs_loose_intersection(ALLSPARK_ARCHS "8.0;8.6;8.7;8.9" "${CUDA_ARCHS}") - if (ALLSPARK_ARCHS) - set(ALLSPARK_SRCS - "csrc/libtorch_stable/quantization/gptq_allspark/allspark_repack.cu" - "csrc/libtorch_stable/quantization/gptq_allspark/allspark_qgemm_w8a16.cu") - set_gencode_flags_for_srcs( - SRCS "${ALLSPARK_SRCS}" - CUDA_ARCHS "${ALLSPARK_ARCHS}") - list(APPEND VLLM_STABLE_EXT_SRC "${ALLSPARK_SRCS}") - message(STATUS "Building AllSpark kernels for archs: ${ALLSPARK_ARCHS}") - else() - message(STATUS "Not building AllSpark kernels as no compatible archs found" - " in CUDA target architectures") - endif() - # # CUTLASS scaled_mm kernels (moved from _C to _C_stable_libtorch) # @@ -1571,6 +1558,7 @@ 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) include(cmake/external_projects/flashkda.cmake) diff --git a/benchmarks/attention_benchmarks/batch_spec.py b/benchmarks/attention_benchmarks/batch_spec.py index 9f15f1d8096e..85c689b48944 100644 --- a/benchmarks/attention_benchmarks/batch_spec.py +++ b/benchmarks/attention_benchmarks/batch_spec.py @@ -1,8 +1,7 @@ # SPDX-License-Identifier: Apache-2.0 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project -""" -Simplified batch specification grammar for attention benchmarks. +"""Simplified batch specification grammar for attention benchmarks. Grammar (underscore-separated segments): Format: (?) q(k?) (s(k?))? @@ -24,6 +23,7 @@ q4s1k -> [(4, 1024)] # 4-token extend (spec decode) 2q1k_32q1s1k -> [(1024, 1024)] * 2 + [(1, 1024)] * 32 # Mixed batch 16q4s1k -> [(4, 1024)] * 16 # 16 spec decode requests + """ from collections import Counter @@ -71,8 +71,7 @@ def _parse_size(size_str: str, k_suffix: str) -> int: def parse_batch_spec(spec: str) -> list[BatchRequest]: - """ - Parse batch specification string into list of BatchRequest objects. + """Parse batch specification string into list of BatchRequest objects. Grammar: (?) q(k?) (s(k?))? @@ -84,6 +83,7 @@ def parse_batch_spec(spec: str) -> list[BatchRequest]: Raises: ValueError: If spec format is invalid + """ requests = [] @@ -103,8 +103,7 @@ def parse_batch_spec(spec: str) -> list[BatchRequest]: def format_batch_spec(requests: list[BatchRequest]) -> str: - """ - Format list of BatchRequest into human-readable string. + """Format list of BatchRequest into human-readable string. Groups requests by type and provides counts and sizes. @@ -113,6 +112,7 @@ def format_batch_spec(requests: list[BatchRequest]) -> str: Returns: Formatted string describing the batch + """ kinds = { "prefill": [], @@ -157,8 +157,7 @@ def format_batch_spec(requests: list[BatchRequest]) -> str: def reorder_for_flashinfer(requests: list[BatchRequest]) -> list[BatchRequest]: - """ - Reorder requests for FlashInfer: decode first, then prefill. + """Reorder requests for FlashInfer: decode first, then prefill. FlashInfer expects decode requests before prefill requests for optimal performance. @@ -168,6 +167,7 @@ def reorder_for_flashinfer(requests: list[BatchRequest]) -> list[BatchRequest]: Returns: Reordered list with decode requests first + """ decodes = [r for r in requests if r.is_decode] non_decodes = [r for r in requests if not r.is_decode] @@ -177,14 +177,14 @@ def reorder_for_flashinfer(requests: list[BatchRequest]) -> list[BatchRequest]: def split_by_type( requests: list[BatchRequest], ) -> dict[str, list[BatchRequest]]: - """ - Split requests by type for analysis. + """Split requests by type for analysis. Args: requests: List of BatchRequest Returns: Dict with keys: 'decode', 'prefill', 'extend' + """ result = { "decode": [], @@ -204,14 +204,14 @@ def split_by_type( def get_batch_stats(requests: list[BatchRequest]) -> dict: - """ - Compute statistics about a batch. + """Compute statistics about a batch. Args: requests: List of BatchRequest Returns: Dict with batch statistics + """ by_type = split_by_type(requests) @@ -232,8 +232,7 @@ def get_batch_stats(requests: list[BatchRequest]) -> dict: def get_batch_type(batch_spec: str, spec_decode_threshold: int = 8) -> str: - """ - Classify a batch spec into a type string. + """Classify a batch spec into a type string. Args: batch_spec: Batch specification string (e.g., "q2k", "8q1s1k", "2q2k_8q1s1k") @@ -241,6 +240,7 @@ def get_batch_type(batch_spec: str, spec_decode_threshold: int = 8) -> str: Returns: Type string: "prefill", "decode", "spec-decode", "extend", or "mixed (types...)" + """ requests = parse_batch_spec(batch_spec) diff --git a/benchmarks/attention_benchmarks/benchmark.py b/benchmarks/attention_benchmarks/benchmark.py index 9c1ea1add70d..943133252be0 100644 --- a/benchmarks/attention_benchmarks/benchmark.py +++ b/benchmarks/attention_benchmarks/benchmark.py @@ -2,8 +2,7 @@ # SPDX-License-Identifier: Apache-2.0 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project -""" -Universal vLLM Attention Benchmark +"""Universal vLLM Attention Benchmark. Benchmark any attention backend with the extended grammar. Supports standard attention (Flash/Triton/FlashInfer) and MLA backends. @@ -23,6 +22,7 @@ # Parameter sweep (YAML config - recommended) python benchmark.py --config configs/cutlass_numsplits.yaml + """ import argparse @@ -84,8 +84,7 @@ def run_mla_benchmark(config: BenchmarkConfig, **kwargs) -> BenchmarkResult: def run_benchmark(config: BenchmarkConfig, **kwargs) -> BenchmarkResult: - """ - Run a single benchmark with proper backend selection. + """Run a single benchmark with proper backend selection. Args: config: BenchmarkConfig with backend, batch_spec, and model params @@ -93,6 +92,7 @@ def run_benchmark(config: BenchmarkConfig, **kwargs) -> BenchmarkResult: Returns: BenchmarkResult (may have error field set on failure) + """ try: if is_mla_backend(config.backend): @@ -119,8 +119,7 @@ def run_model_parameter_sweep( sweep: ModelParameterSweep, console: Console, ) -> list[BenchmarkResult]: - """ - Run model parameter sweep for given backends and batch specs. + """Run model parameter sweep for given backends and batch specs. Args: backends: List of backend names @@ -131,6 +130,7 @@ def run_model_parameter_sweep( Returns: List of BenchmarkResult objects + """ all_results = [] @@ -304,8 +304,7 @@ def run_parameter_sweep( sweep: ParameterSweep, console: Console, ) -> list[BenchmarkResult]: - """ - Run parameter sweep for given backends and batch specs. + """Run parameter sweep for given backends and batch specs. Args: backends: List of backend names @@ -316,6 +315,7 @@ def run_parameter_sweep( Returns: List of BenchmarkResult objects + """ all_results = [] @@ -395,8 +395,7 @@ def load_config_from_yaml(config_path: str) -> dict: def generate_batch_specs_from_ranges(ranges: list[dict]) -> list[str]: - """ - Generate batch specs from range specifications. + """Generate batch specs from range specifications. Args: ranges: List of range specifications, each containing: @@ -420,6 +419,7 @@ def generate_batch_specs_from_ranges(ranges: list[dict]) -> list[str]: } ] Returns: ["q1kv1k", "q2kv1k", ..., "q16kv1k"] + """ all_specs = [] diff --git a/benchmarks/attention_benchmarks/common.py b/benchmarks/attention_benchmarks/common.py index 45858f9148b3..b6fd26c80344 100644 --- a/benchmarks/attention_benchmarks/common.py +++ b/benchmarks/attention_benchmarks/common.py @@ -21,8 +21,7 @@ def batch_spec_sort_key(spec: str) -> tuple[int, int, int]: - """ - Extract sorting key from batch spec: (batch_size, max_q_len, max_kv_len). + """Extract sorting key from batch spec: (batch_size, max_q_len, max_kv_len). This ensures results are sorted by batch size first, then query length, then sequence length, rather than alphabetically. @@ -354,13 +353,13 @@ def print_table( backends: list[str], compare_to_fastest: bool = True, ): - """ - Print results as a rich table. + """Print results as a rich table. Args: results: List of BenchmarkResult backends: List of backend names being compared compare_to_fastest: Show percentage comparison to fastest + """ # Group by batch spec, preserving first-occurrence order by_spec = {} @@ -489,14 +488,14 @@ def save_json(self, results: list[BenchmarkResult], path: str): def setup_mla_dims(model_name: str = "deepseek-v3") -> dict: - """ - Get MLA dimensions for known models. + """Get MLA dimensions for known models. Args: model_name: Model identifier Returns: Dict with MLA dimension configuration + """ configs = { "deepseek-v2": { @@ -542,8 +541,7 @@ def get_attention_scale(head_dim: int) -> float: def is_mla_backend(backend: str) -> bool: - """ - Check if backend is an MLA backend using the AttentionBackendEnum. + """Check if backend is an MLA backend using the AttentionBackendEnum. Args: backend: Backend name matching AttentionBackendEnum exactly @@ -551,6 +549,7 @@ def is_mla_backend(backend: str) -> bool: Returns: True if the backend is an MLA backend, False otherwise + """ from vllm.v1.attention.backends.registry import AttentionBackendEnum diff --git a/benchmarks/attention_benchmarks/mla_runner.py b/benchmarks/attention_benchmarks/mla_runner.py index 790b2f89442b..04dcd73719a3 100644 --- a/benchmarks/attention_benchmarks/mla_runner.py +++ b/benchmarks/attention_benchmarks/mla_runner.py @@ -1,8 +1,7 @@ # SPDX-License-Identifier: Apache-2.0 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project -""" -MLA benchmark runner - shared utilities for MLA benchmarks. +"""MLA benchmark runner - shared utilities for MLA benchmarks. This module provides helpers for running MLA backends without needing full VllmConfig integration. @@ -93,8 +92,7 @@ def _ensure_single_rank_model_parallel() -> None: def _add_mock_methods_to_model_config(model_config: ModelConfig) -> None: - """ - Add mock methods for layer-specific queries to ModelConfig. + """Add mock methods for layer-specific queries to ModelConfig. These methods are needed by metadata builders but aren't normally present on ModelConfig when used in benchmark contexts. @@ -125,14 +123,15 @@ def create_minimal_vllm_config( kv_cache_dtype: str = "auto", sparse_mla_force_mqa: bool = False, ) -> VllmConfig: - """ - Create minimal VllmConfig for MLA benchmarks. + """Create minimal VllmConfig for MLA benchmarks. Args: model_name: Model name (deepseek-v2, deepseek-v3, etc.) - used if mla_dims not provided block_size: KV cache block size max_num_seqs: Maximum number of sequences + max_num_batched_tokens: Maximum number of batched tokens + max_model_len: Maximum model context length mla_dims: Optional custom MLA dimensions dict. If not provided, uses setup_mla_dims(model_name) index_topk: Optional topk value for sparse MLA backends. If provided, @@ -140,11 +139,13 @@ def create_minimal_vllm_config( prefill_backend: Prefill backend name (e.g., "fa3", "fa4", "flashinfer", "trtllm"). Configures the attention config to force the specified prefill backend. + kv_cache_dtype: KV cache dtype, e.g. "auto" or "fp8" sparse_mla_force_mqa: If True, forces all sparse MLA tokens through forward_mqa (even prefill tokens). Returns: VllmConfig for benchmarking + """ # Get MLA dimensions - use provided or load from model name if mla_dims is None: @@ -320,8 +321,7 @@ def get_prefill_backend_config(prefill_backend: str) -> dict: def _get_backend_config(backend: str) -> dict: - """ - Get backend configuration from AttentionBackendEnum. + """Get backend configuration from AttentionBackendEnum. Uses the registry to get the backend class and extract configuration from its methods (get_impl_cls, get_builder_cls, is_sparse, etc.). @@ -332,6 +332,7 @@ def _get_backend_config(backend: str) -> dict: Returns: Dict with backend configuration + """ from vllm.v1.attention.backend import MultipleOf from vllm.v1.attention.backends.registry import AttentionBackendEnum @@ -381,8 +382,7 @@ def _build_attention_metadata( device: torch.device, builder_instance, ) -> tuple: - """ - Build attention metadata from batch requests. + """Build attention metadata from batch requests. Args: requests: List of BatchRequest objects @@ -392,6 +392,7 @@ def _build_attention_metadata( Returns: Tuple of (metadata, kv_cache_num_blocks) + """ q_lens = [r.q_len for r in requests] kv_lens = [r.kv_len for r in requests] @@ -409,10 +410,6 @@ def _build_attention_metadata( seq_lens_cpu = torch.tensor(kv_lens, dtype=torch.int32) seq_lens_gpu = seq_lens_cpu.to(device) - # Build num_computed_tokens (context length for each request) - context_lens = [kv_len - q_len for q_len, kv_len in zip(q_lens, kv_lens)] - num_computed_tokens_cpu = torch.tensor(context_lens, dtype=torch.int32) - # Build block table num_blocks_per_req = [(kv + block_size - 1) // block_size for kv in kv_lens] max_num_blocks = max(num_blocks_per_req) @@ -454,8 +451,6 @@ def _build_attention_metadata( query_start_loc_cpu=q_start_cpu, seq_lens=seq_lens_gpu, seq_lens_cpu_upper_bound=seq_lens_cpu, - _seq_lens_cpu=seq_lens_cpu, - _num_computed_tokens_cpu=num_computed_tokens_cpu, slot_mapping=slot_mapping, block_table_tensor=block_table_gpu, dcp_local_seq_lens=None, @@ -478,8 +473,7 @@ def _create_input_tensors( device: torch.device, dtype: torch.dtype, ): - """ - Create input tensors for both decode and prefill modes. + """Create input tensors for both decode and prefill modes. MLA requires different tensor formats for decode vs prefill: - Decode: Uses kv_lora_rank (512) dimension @@ -496,6 +490,7 @@ def _create_input_tensors( Tuple of (decode_inputs, prefill_inputs) - decode_inputs: Query tensor(s) for decode mode - prefill_inputs: Dict with 'q', 'k_c_normed', 'k_pe', 'k_scale' for prefill + """ if query_format == "tuple": # Decode mode format: (q_nope, q_pe) where q_nope has kv_lora_rank dim @@ -589,8 +584,7 @@ def _create_backend_impl( index_topk: int | None = None, kv_cache_dtype: str = "auto", ): - """ - Create backend implementation instance. + """Create backend implementation instance. Args: backend_cfg: Backend configuration dict from _get_backend_config() @@ -599,9 +593,11 @@ def _create_backend_impl( device: Target device max_num_tokens: Maximum number of tokens for sparse indexer buffer index_topk: Topk value for sparse MLA backends + kv_cache_dtype: KV cache dtype, e.g. "auto" or "fp8" Returns: Tuple of (impl, layer, builder_instance, indexer) + """ # Get classes from backend config (already resolved by _get_backend_config) impl_class = backend_cfg["impl_class"] @@ -707,14 +703,14 @@ def _create_backend_impl( def _extract_mla_dims_from_config(config) -> dict | None: - """ - Extract MLA dimensions from BenchmarkConfig if all required fields are present. + """Extract MLA dimensions from BenchmarkConfig if all required fields are present. Args: config: BenchmarkConfig instance Returns: Dict with MLA dimensions if all fields are provided, None otherwise + """ # Check if all MLA-specific fields are provided if all( @@ -768,8 +764,7 @@ def _run_single_benchmark( output_scale: float | None = None, fuse_quant_op: bool = False, ) -> BenchmarkResult: - """ - Run a single benchmark iteration. + """Run a single benchmark iteration. Args: config: BenchmarkConfig instance @@ -780,6 +775,7 @@ def _run_single_benchmark( mla_dims: MLA dimension configuration device: Target device indexer: Optional MockIndexer for sparse backends + kv_cache_dtype: KV cache dtype. None keeps the backend default. output_scale: Static per-tensor FP8 scale for prefill output. None keeps the plain bf16 output (no quantization). fuse_quant_op: With output_scale set, True lets the prefill kernel write @@ -788,6 +784,7 @@ def _run_single_benchmark( Returns: BenchmarkResult with timing statistics + """ # Parse batch spec requests = parse_batch_spec(config.batch_spec) @@ -1050,8 +1047,7 @@ def _run_mla_benchmark_batched( output_scale: float | None = None, fuse_quant_op: bool = False, ) -> list[BenchmarkResult]: - """ - Unified batched MLA benchmark runner for all backends. + """Unified batched MLA benchmark runner for all backends. Works for: flashattn_mla, flashmla, flashinfer_mla, cutlass_mla, flashinfer_mla_sparse, flashmla_sparse @@ -1067,11 +1063,16 @@ def _run_mla_benchmark_batched( index_topk: Topk value for sparse MLA backends (default 2048) prefill_backend: Prefill backend name (e.g., "fa3", "fa4"). When set, forces the specified FlashAttention version for prefill. + output_scale: Static per-tensor FP8 scale for prefill output. None + keeps the plain bf16 output (no quantization). + fuse_quant_op: With output_scale set, True lets the prefill kernel write + FP8 directly; False runs bf16 attention then a standalone quant. sparse_mla_force_mqa: If True, forces all sparse MLA tokens through forward_mqa (even prefill tokens). Returns: List of BenchmarkResult objects + """ if not configs_with_params: return [] @@ -1283,8 +1284,7 @@ def run_mla_benchmark( output_scale: float | None = None, fuse_quant_op: bool = False, ) -> BenchmarkResult | list[BenchmarkResult]: - """ - Unified MLA benchmark runner for all backends. + """Unified MLA benchmark runner for all backends. Works for: flashattn_mla, flashmla, flashinfer_mla, cutlass_mla, flashinfer_mla_sparse, flashmla_sparse @@ -1309,6 +1309,7 @@ def run_mla_benchmark( Returns: BenchmarkResult (single mode) or list of BenchmarkResult (batched mode) + """ # Normalize to batched mode: (config, threshold, num_splits) if isinstance(config, list): diff --git a/benchmarks/attention_benchmarks/runner.py b/benchmarks/attention_benchmarks/runner.py index 01fd558043b7..a1b7d045050e 100644 --- a/benchmarks/attention_benchmarks/runner.py +++ b/benchmarks/attention_benchmarks/runner.py @@ -1,8 +1,7 @@ # SPDX-License-Identifier: Apache-2.0 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project -""" -Standard attention benchmark runner - shared utilities for non-MLA benchmarks. +"""Standard attention benchmark runner - shared utilities for non-MLA benchmarks. This module provides helpers for running standard attention backends (FlashAttention, Triton, FlashInfer) with real vLLM integration. @@ -57,8 +56,7 @@ def _get_backend_config(backend: str) -> dict: - """ - Get backend configuration from AttentionBackendEnum. + """Get backend configuration from AttentionBackendEnum. Args: backend: Backend name matching AttentionBackendEnum exactly @@ -66,6 +64,7 @@ def _get_backend_config(backend: str) -> dict: Returns: Dict with backend_class + """ from vllm.v1.attention.backends.registry import AttentionBackendEnum @@ -409,6 +408,7 @@ def _run_single_benchmark( Returns: (timing_stats, mem_stats) where timing_stats is a dict with mean/std/min/max in seconds per layer. + """ total_q = q_list[0].shape[0] v_dim = config.v_head_dim if config.v_head_dim is not None else config.head_dim @@ -458,8 +458,7 @@ def benchmark_fn(): def run_attention_benchmark(config: BenchmarkConfig) -> BenchmarkResult: - """ - Run standard attention benchmark with real kernels. + """Run standard attention benchmark with real kernels. Supports: FLASH_ATTN, TRITON_ATTN, FLASHINFER @@ -468,6 +467,7 @@ def run_attention_benchmark(config: BenchmarkConfig) -> BenchmarkResult: Returns: BenchmarkResult with timing and memory statistics + """ device = torch.device(config.device) torch.accelerator.set_device_index(device) diff --git a/benchmarks/backend_request_func.py b/benchmarks/backend_request_func.py index 6349095ad729..b8e5f5be4c13 100644 --- a/benchmarks/backend_request_func.py +++ b/benchmarks/backend_request_func.py @@ -105,7 +105,7 @@ async def async_request_tgi( timestamp = time.perf_counter() # First token if ttft == 0.0: - ttft = time.perf_counter() - st + ttft = timestamp - st output.ttft = ttft # Decoding phase @@ -330,8 +330,7 @@ async def async_request_openai_completions( # First token if not first_chunk_received: first_chunk_received = True - ttft = time.perf_counter() - st - output.ttft = ttft + output.ttft = timestamp - st # Decoding phase else: @@ -416,7 +415,7 @@ async def async_request_openai_chat_completions( output.prompt_len = request_func_input.prompt_len generated_text = "" - ttft = 0.0 + first_chunk_received = False st = time.perf_counter() most_recent_timestamp = st try: @@ -443,22 +442,30 @@ async def async_request_openai_chat_completions( if choices := data.get("choices"): content = choices[0]["delta"].get("content") # First token - if ttft == 0.0: - ttft = timestamp - st - output.ttft = ttft + if not first_chunk_received: + first_chunk_received = True + output.ttft = timestamp - st # Decoding phase else: output.itl.append(timestamp - most_recent_timestamp) generated_text += content or "" + # Only token chunks advance the request end; + # the trailing usage chunk carries no token. + most_recent_timestamp = timestamp elif usage := data.get("usage"): output.output_tokens = usage.get("completion_tokens") - most_recent_timestamp = timestamp - output.generated_text = generated_text - output.success = True + if first_chunk_received: + output.success = True + else: + output.success = False + output.error = ( + "Never received a valid chunk to calculate TTFT." + "This response will be marked as failed!" + ) output.latency = most_recent_timestamp - st else: output.error = response.reason or "" @@ -530,7 +537,7 @@ def to_bytes(y, sr): output.prompt_len = request_func_input.prompt_len generated_text = "" - ttft = 0.0 + first_chunk_received = False st = time.perf_counter() most_recent_timestamp = st try: @@ -551,9 +558,9 @@ def to_bytes(y, sr): if choices := data.get("choices"): content = choices[0]["delta"].get("content") # First token - if ttft == 0.0: - ttft = timestamp - st - output.ttft = ttft + if not first_chunk_received: + first_chunk_received = True + output.ttft = timestamp - st # Decoding phase else: @@ -562,15 +569,24 @@ def to_bytes(y, sr): ) generated_text += content or "" + # Only token chunks advance the request + # end; the trailing usage chunk carries no + # token. + most_recent_timestamp = timestamp elif usage := data.get("usage"): output.output_tokens = usage.get( "completion_tokens" ) - most_recent_timestamp = timestamp - output.generated_text = generated_text - output.success = True + if first_chunk_received: + output.success = True + else: + output.success = False + output.error = ( + "Never received a valid chunk to calculate TTFT." + "This response will be marked as failed!" + ) output.latency = most_recent_timestamp - st else: output.error = response.reason or "" @@ -614,10 +630,6 @@ def get_tokenizer( pretrained_model_name_or_path ): pretrained_model_name_or_path = get_model(pretrained_model_name_or_path) - if tokenizer_mode == "slow": - if kwargs.get("use_fast", False): - raise ValueError("Cannot use the fast tokenizer in slow tokenizer mode.") - kwargs["use_fast"] = False if tokenizer_mode == "mistral": try: from vllm.tokenizers.mistral import MistralTokenizer diff --git a/benchmarks/benchmark_batch_invariance.py b/benchmarks/benchmark_batch_invariance.py index 7473a41e5140..dd22523ce201 100755 --- a/benchmarks/benchmark_batch_invariance.py +++ b/benchmarks/benchmark_batch_invariance.py @@ -1,8 +1,7 @@ #!/usr/bin/env python3 # SPDX-License-Identifier: Apache-2.0 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project -""" -Benchmark to measure the performance overhead of VLLM_BATCH_INVARIANT mode. +"""Benchmark to measure the performance overhead of VLLM_BATCH_INVARIANT mode. This benchmark runs the same workload twice: 1. With VLLM_BATCH_INVARIANT=0 (baseline) @@ -96,8 +95,7 @@ def run_benchmark_with_batch_invariant( batch_invariant: bool, seed: int = 12345, ) -> dict: - """ - Run the benchmark with the specified configuration. + """Run the benchmark with the specified configuration. Returns a dict with timing and throughput metrics. """ diff --git a/benchmarks/benchmark_hash.py b/benchmarks/benchmark_hash.py index 08cdc012d652..c5af4da9998b 100644 --- a/benchmarks/benchmark_hash.py +++ b/benchmarks/benchmark_hash.py @@ -1,7 +1,6 @@ # SPDX-License-Identifier: Apache-2.0 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project -""" -Micro benchmark comparing built-in hash(), SHA-256, and xxHash. +"""Micro benchmark comparing built-in hash(), SHA-256, and xxHash. This focuses on a single test payload shaped like the prefix-cache hash input: (32-byte bytes object, 32-int tuple) diff --git a/benchmarks/benchmark_hidden_state_extraction.py b/benchmarks/benchmark_hidden_state_extraction.py index f0a35a0cf150..878be2c29347 100644 --- a/benchmarks/benchmark_hidden_state_extraction.py +++ b/benchmarks/benchmark_hidden_state_extraction.py @@ -1,7 +1,6 @@ # SPDX-License-Identifier: Apache-2.0 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project -""" -Benchmark hidden state extraction throughput. +"""Benchmark hidden state extraction throughput. Measures two modes: 1. Baseline: bulk inference with max_tokens=1, no extraction. diff --git a/benchmarks/benchmark_long_document_qa_throughput.py b/benchmarks/benchmark_long_document_qa_throughput.py index 6106b9014952..1f5b5f5504b0 100644 --- a/benchmarks/benchmark_long_document_qa_throughput.py +++ b/benchmarks/benchmark_long_document_qa_throughput.py @@ -1,7 +1,6 @@ # SPDX-License-Identifier: Apache-2.0 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project -""" -Offline benchmark to test the long document QA throughput. +"""Offline benchmark to test the long document QA throughput. Example usage: # This workload samples 8 different prompts with a default input @@ -49,14 +48,14 @@ def test_long_document_qa(llm=None, sampling_params=None, prompts=None): - """ - Test long document QA with the given prompts and sampling parameters. + """Test long document QA with the given prompts and sampling parameters. Print the time spent in processing all the prompts. Args: llm: The language model used for generating responses. sampling_params: Sampling parameter used to generate the response. prompts: A list of prompt strings to be processed by the LLM. + """ start_time = time.time() llm.generate(prompts, sampling_params=sampling_params) @@ -65,8 +64,7 @@ def test_long_document_qa(llm=None, sampling_params=None, prompts=None): def repeat_prompts(prompts, repeat_count, mode: str): - """ - Repeat each prompt in the list for a specified number of times. + """Repeat each prompt in the list for a specified number of times. The order of prompts in the output list depends on the mode. Args: @@ -84,6 +82,7 @@ def repeat_prompts(prompts, repeat_count, mode: str): Raises: ValueError: If an invalid mode is provided. + """ print("Repeat mode: ", mode) if mode == "random": diff --git a/benchmarks/benchmark_prefix_block_hash.py b/benchmarks/benchmark_prefix_block_hash.py index 8bcd8af0d310..522bf9d1ab31 100644 --- a/benchmarks/benchmark_prefix_block_hash.py +++ b/benchmarks/benchmark_prefix_block_hash.py @@ -1,11 +1,11 @@ # SPDX-License-Identifier: Apache-2.0 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project -""" -Simple benchmark to compare prefix-cache block hashing algorithms. +"""Simple benchmark to compare prefix-cache block hashing algorithms. Example: python benchmark_prefix_block_hash.py --num-blocks 20000 --block-size 32 + """ from __future__ import annotations diff --git a/benchmarks/benchmark_prefix_caching.py b/benchmarks/benchmark_prefix_caching.py index 0145f6b7c8e5..5abdd65b2aaf 100644 --- a/benchmarks/benchmark_prefix_caching.py +++ b/benchmarks/benchmark_prefix_caching.py @@ -1,7 +1,6 @@ # SPDX-License-Identifier: Apache-2.0 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project -""" -Benchmark the efficiency of prefix caching. +"""Benchmark the efficiency of prefix caching. This script allows you to benchmark the performance of a model with and without prefix caching using either fixed prompts diff --git a/benchmarks/benchmark_serving_structured_output.py b/benchmarks/benchmark_serving_structured_output.py index 664fa58dd49f..dc8e30268e7f 100644 --- a/benchmarks/benchmark_serving_structured_output.py +++ b/benchmarks/benchmark_serving_structured_output.py @@ -102,6 +102,7 @@ class SampleRequest: images). prompt_len: The length of the prompt in tokens. expected_output_len: The expected length of the output in tokens. + """ prompt: str @@ -305,8 +306,7 @@ async def get_request( request_rate: float, burstiness: float = 1.0, ) -> AsyncGenerator[tuple[int, SampleRequest], None]: - """ - Asynchronously generates requests at a specified rate + """Asynchronously generates requests at a specified rate with OPTIONAL burstiness. Args: @@ -322,6 +322,7 @@ async def get_request( A lower burstiness value (0 < burstiness < 1) results in more bursty requests, while a higher burstiness value (burstiness > 1) results in a more uniform arrival of requests. + """ input_requests = iter(input_requests) diff --git a/benchmarks/benchmark_topk_topp.py b/benchmarks/benchmark_topk_topp.py index f727f16ea29c..14111f733b9a 100644 --- a/benchmarks/benchmark_topk_topp.py +++ b/benchmarks/benchmark_topk_topp.py @@ -1,8 +1,7 @@ #!/usr/bin/env python3 # SPDX-License-Identifier: Apache-2.0 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project -""" -Benchmark comparing Triton vs PyTorch sort-based top-k/top-p implementations. +"""Benchmark comparing Triton vs PyTorch sort-based top-k/top-p implementations. Compares: - apply_top_k_top_p_triton (Triton binary search) @@ -47,8 +46,7 @@ def calculate_ops_pct( vocab_size: int, batch_size: int, ) -> float: - """ - Calculate the percentage of active top-k and top-p operations. + """Calculate the percentage of active top-k and top-p operations. Returns percentage where 100% = batch_size ops. E.g., if all rows have both top-k and top-p active, returns 200%. @@ -117,8 +115,7 @@ def benchmark_function( warmup_iters: int = 5, benchmark_iters: int = 20, ) -> tuple[float, int]: - """ - Benchmark a function and return (avg_time_ms, peak_memory_bytes). + """Benchmark a function and return (avg_time_ms, peak_memory_bytes). Returns average time in milliseconds and peak memory usage. """ diff --git a/benchmarks/cutlass_benchmarks/utils.py b/benchmarks/cutlass_benchmarks/utils.py deleted file mode 100644 index 659c68bb11d7..000000000000 --- a/benchmarks/cutlass_benchmarks/utils.py +++ /dev/null @@ -1,39 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 -# SPDX-FileCopyrightText: Copyright contributors to the vLLM project - -# Cutlass bench utils - -import torch - - -def to_fp8(tensor: torch.Tensor) -> torch.Tensor: - finfo = torch.finfo(torch.float8_e4m3fn) - return torch.round(tensor.clamp(min=finfo.min, max=finfo.max)).to( - dtype=torch.float8_e4m3fn - ) - - -def to_int8(tensor: torch.Tensor) -> torch.Tensor: - return torch.round(tensor.clamp(min=-128, max=127)).to(dtype=torch.int8) - - -def to_bf16(tensor: torch.Tensor) -> torch.Tensor: - return tensor.to(dtype=torch.bfloat16) - - -def to_fp16(tensor: torch.Tensor) -> torch.Tensor: - return tensor.to(dtype=torch.float16) - - -def make_rand_tensors( - dtype: torch.dtype, m: int, n: int, k: int -) -> tuple[torch.Tensor, torch.Tensor]: - a = torch.randn((m, k), device="cuda") * 5 - b = torch.randn((n, k), device="cuda").t() * 5 - - if dtype == torch.int8: - return to_int8(a), to_int8(b) - if dtype == torch.float8_e4m3fn: - return to_fp8(a), to_fp8(b) - - raise ValueError("unsupported dtype") diff --git a/benchmarks/cutlass_benchmarks/w8a8_benchmarks.py b/benchmarks/cutlass_benchmarks/w8a8_benchmarks.py deleted file mode 100644 index f7325ddd2cbb..000000000000 --- a/benchmarks/cutlass_benchmarks/w8a8_benchmarks.py +++ /dev/null @@ -1,372 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 -# SPDX-FileCopyrightText: Copyright contributors to the vLLM project - -import argparse -import copy -import itertools -import pickle as pkl -import time -from collections.abc import Callable, Iterable - -import torch -import torch.utils.benchmark as TBenchmark -from torch.utils.benchmark import Measurement as TMeasurement -from utils import make_rand_tensors -from weight_shapes import WEIGHT_SHAPES - -from vllm import _custom_ops as ops -from vllm.model_executor.layers.quantization.utils.fp8_utils import ( - w8a8_triton_block_scaled_mm, -) -from vllm.utils.argparse_utils import FlexibleArgumentParser -from vllm.utils.math_utils import cdiv - -DEFAULT_MODELS = list(WEIGHT_SHAPES.keys()) -DEFAULT_BATCH_SIZES = [1, 16, 32, 64, 128, 256, 512] -DEFAULT_TP_SIZES = [1] - - -# bench -def bench_fn( - label: str, sub_label: str, description: str, fn: Callable, *args, **kwargs -) -> TMeasurement: - min_run_time = 1 - - globals = { - "args": args, - "kwargs": kwargs, - "fn": fn, - } - return TBenchmark.Timer( - stmt="fn(*args, **kwargs)", - globals=globals, - label=label, - sub_label=sub_label, - description=description, - ).blocked_autorange(min_run_time=min_run_time) - - -def bench_int8( - dtype: torch.dtype, - m: int, - k: int, - n: int, - label: str, - sub_label: str, - bench_kernels: list[str] | None = None, -) -> Iterable[TMeasurement]: - """Benchmark INT8-based kernels.""" - assert dtype == torch.int8 - a, b = make_rand_tensors(torch.int8, m, n, k) - scale_a = torch.tensor(1.0, device="cuda", dtype=torch.float32) - scale_b = torch.tensor(1.0, device="cuda", dtype=torch.float32) - bias = torch.zeros((n,), device="cuda", dtype=torch.bfloat16) - azp = torch.zeros((m,), device="cuda", dtype=torch.int32) - azp_adj = torch.zeros((n,), device="cuda", dtype=torch.int32) - - bench_fns = { - "pytorch_bf16_bf16_bf16_matmul-no-scales": lambda: torch.mm( - a.to(dtype=torch.bfloat16), b.to(dtype=torch.bfloat16) - ), - "pytorch_fp16_fp16_fp16_matmul-no-scales": lambda: torch.mm( - a.to(dtype=torch.float16), b.to(dtype=torch.float16) - ), - "cutlass_i8_i8_bf16_scaled_mm": lambda: ops.cutlass_scaled_mm( - a, b, scale_a, scale_b, torch.bfloat16 - ), - "cutlass_i8_i8_bf16_scaled_mm_bias": lambda: ops.cutlass_scaled_mm( - a, b, scale_a, scale_b, torch.bfloat16, bias - ), - "cutlass_i8_i8_bf16_scaled_mm_azp": lambda: ops.cutlass_scaled_mm_azp( - a, b, scale_a, scale_b, torch.bfloat16, azp_adj - ), - "cutlass_i8_i8_bf16_scaled_mm_azp_bias": lambda: ops.cutlass_scaled_mm_azp( - a, b, scale_a, scale_b, torch.bfloat16, azp_adj, None, bias - ), - "cutlass_i8_i8_bf16_scaled_mm_azp_pt": lambda: ops.cutlass_scaled_mm_azp( - a, b, scale_a, scale_b, torch.bfloat16, azp_adj, azp - ), - "cutlass_i8_i8_bf16_scaled_mm_azp_pt_bias": lambda: ops.cutlass_scaled_mm_azp( - a, b, scale_a, scale_b, torch.bfloat16, azp_adj, azp, bias - ), - } - - timers = [] - for name, fn in bench_fns.items(): - # If bench_kernels is None, run all. Otherwise, run only exact matches. - if bench_kernels is None or name in bench_kernels: - print(f"Running {name}") - timers.append(bench_fn(label, sub_label, name, fn)) - - return timers - - -def bench_fp8( - dtype: torch.dtype, - m: int, - k: int, - n: int, - label: str, - sub_label: str, - bench_kernels: list[str] | None = None, -) -> Iterable[TMeasurement]: - """Benchmark FP8-based kernels.""" - assert dtype == torch.float8_e4m3fn - a, b = make_rand_tensors(torch.float8_e4m3fn, m, n, k) - a_cont = a.contiguous() - scale_a = torch.tensor(1.0, device="cuda", dtype=torch.float32) - scale_b = torch.tensor(1.0, device="cuda", dtype=torch.float32) - - block_scale_a = torch.rand((m, cdiv(k, 128)), device="cuda", dtype=torch.float32) - block_scale_b = torch.rand( - cdiv(k, 128), cdiv(n, 128), device="cuda", dtype=torch.float32 - ) - block_scale_a_M_major = block_scale_a.t().contiguous().t() - block_scale_b_K_major = block_scale_b.t().contiguous().t() - bias = torch.zeros((n,), device="cuda", dtype=torch.bfloat16) - - print(m, k, n) - - bench_fns = { - "pytorch_bf16_bf16_bf16_matmul-no-scales": lambda: torch.mm( - a.to(dtype=torch.bfloat16), b.to(dtype=torch.bfloat16) - ), - "pytorch_fp16_fp16_fp16_matmul-no-scales": lambda: torch.mm( - a.to(dtype=torch.float16), b.to(dtype=torch.float16) - ), - "pytorch_fp8_fp8_fp16_scaled_mm": lambda: torch._scaled_mm( - a, b, scale_a, scale_b, out_dtype=torch.float16 - ), - "pytorch_fp8_fp8_fp16_scaled_mm_fast_accum": lambda: torch._scaled_mm( - a, b, scale_a, scale_b, out_dtype=torch.float16, use_fast_accum=True - ), - "pytorch_fp8_fp8_bf16_scaled_mm": lambda: torch._scaled_mm( - a, b, scale_a, scale_b, out_dtype=torch.bfloat16 - ), - "pytorch_fp8_fp8_bf16_scaled_mm_fast_accum": lambda: torch._scaled_mm( - a, b, scale_a, scale_b, out_dtype=torch.bfloat16, use_fast_accum=True - ), - "cutlass_fp8_fp8_bf16_scaled_mm": lambda: ops.cutlass_scaled_mm( - a, b, scale_a, scale_b, torch.bfloat16 - ), - "cutlass_fp8_fp8_fp16_scaled_mm": lambda: ops.cutlass_scaled_mm( - a, b, scale_a, scale_b, torch.float16 - ), - "cutlass_fp8_fp8_bf16_scaled_mm_bias": lambda: ops.cutlass_scaled_mm( - a, b, scale_a, scale_b, torch.bfloat16, bias - ), - "cutlass_fp8_fp8_fp16_scaled_mm_bias": lambda: ops.cutlass_scaled_mm( - a, b, scale_a, scale_b, torch.float16, bias.to(dtype=torch.float16) - ), - "triton_fp8_fp8_fp16_scaled_mm_blockwise": lambda: w8a8_triton_block_scaled_mm( - a_cont, b.t(), block_scale_a, block_scale_b.t(), (128, 128) - ), - "cutlass_fp8_fp8_fp16_scaled_mm_blockwise": lambda: ops.cutlass_scaled_mm( - a, b, block_scale_a_M_major, block_scale_b_K_major, torch.float16 - ), - } - - timers = [] - for name, fn in bench_fns.items(): - # If bench_kernels is None, run all. Otherwise, run only exact matches. - if bench_kernels is None or name in bench_kernels: - print(f"Running {name}") - timers.append(bench_fn(label, sub_label, name, fn)) - - return timers - - -def bench( - dtype: torch.dtype, - m: int, - k: int, - n: int, - label: str, - sub_label: str, - bench_kernels: list[str] | None = None, -) -> Iterable[TMeasurement]: - if dtype == torch.int8: - return bench_int8(dtype, m, k, n, label, sub_label, bench_kernels) - if dtype == torch.float8_e4m3fn: - return bench_fp8(dtype, m, k, n, label, sub_label, bench_kernels) - raise ValueError("unsupported type") - - -# runner -def print_timers(timers: Iterable[TMeasurement]): - compare = TBenchmark.Compare(timers) - compare.print() - - -def run( - dtype: torch.dtype, - MKNs: Iterable[tuple[int, int, int]], - bench_kernels: list[str] | None = None, -) -> Iterable[TMeasurement]: - results = [] - for m, k, n in MKNs: - timers = bench( - dtype, - m, - k, - n, - f"scaled-{dtype}-gemm", - f"MKN=({m}x{k}x{n})", - bench_kernels=bench_kernels, - ) - print_timers(timers) - results.extend(timers) - return results - - -def make_output( - data: Iterable[TMeasurement], - MKNs: Iterable[tuple[int, int, int]], - base_description: str, - timestamp=None, -): - print(f"== All Results {base_description} ====") - print_timers(data) - - # pickle all the results - timestamp = int(time.time()) if timestamp is None else timestamp - with open(f"{base_description}-{timestamp}.pkl", "wb") as f: - pkl.dump(data, f) - - -def run_square_bench(args): - dim_sizes = list(range(args.dim_start, args.dim_end + 1, args.dim_increment)) - MKNs = list(zip(dim_sizes, dim_sizes, dim_sizes)) - data = run(args.dtype, MKNs, bench_kernels=args.kernels) - make_output(data, MKNs, f"square_bench-{args.dtype}") - - -def run_range_bench(args): - dim_sizes = list(range(args.dim_start, args.dim_end, args.dim_increment)) - n = len(dim_sizes) - Ms = [args.m_constant] * n if args.m_constant is not None else dim_sizes - Ks = [args.k_constant] * n if args.k_constant is not None else dim_sizes - Ns = [args.n_constant] * n if args.n_constant is not None else dim_sizes - MKNs = list(zip(Ms, Ks, Ns)) - data = run(args.dtype, MKNs, bench_kernels=args.kernels) - make_output(data, MKNs, f"range_bench-{args.dtype}") - - -def run_model_bench(args): - print("Benchmarking models:") - for i, model in enumerate(args.models): - print(f"[{i}] {model}") - - def model_shapes(model_name: str, tp_size: int) -> list[tuple[int, int]]: - KNs = [] - for KN, tp_split_dim in copy.deepcopy(WEIGHT_SHAPES[model_name]): - KN[tp_split_dim] = KN[tp_split_dim] // tp_size - KNs.append(KN) - return KNs - - model_bench_data = [] - models_tps = list(itertools.product(args.models, args.tp_sizes)) - for model, tp_size in models_tps: - Ms = args.batch_sizes - KNs = model_shapes(model, tp_size) - MKNs = [] - for m in Ms: - for k, n in KNs: - MKNs.append((m, k, n)) - - data = run(args.dtype, MKNs, bench_kernels=args.kernels) - model_bench_data.append(data) - - # Print all results - for data, model_tp in zip(model_bench_data, models_tps): - model, tp_size = model_tp - print(f"== Results {args.dtype} {model}-TP{tp_size} ====") - print_timers(data) - - timestamp = int(time.time()) - - all_data = [] - for d in model_bench_data: - all_data.extend(d) - # pickle all data - with open(f"model_bench-{args.dtype}-{timestamp}.pkl", "wb") as f: - pkl.dump(all_data, f) - - -if __name__ == "__main__": - - def to_torch_dtype(dt): - if dt == "int8": - return torch.int8 - if dt == "fp8": - return torch.float8_e4m3fn - raise ValueError("unsupported dtype") - - parser = FlexibleArgumentParser( - description=""" -Benchmark Cutlass GEMM. - - To run square GEMMs: - python3 ./benchmarks/cutlass_benchmarks/w8a8_benchmarks.py --dtype fp8 square_bench --dim-start 128 --dim-end 512 --dim-increment 64 - - To run constant N and K and sweep M: - python3 ./benchmarks/cutlass_benchmarks/w8a8_benchmarks.py --dtype fp8 range_bench --dim-start 128 --dim-end 512 --dim-increment 64 --n-constant 16384 --k-constant 16384 - - To run dimensions from a model: - python3 ./benchmarks/cutlass_benchmarks/w8a8_benchmarks.py --dtype fp8 model_bench --models meta-llama/Llama-2-7b-hf --batch-sizes 16 --tp-sizes 1 - - Output: - - a .pkl file, that is a list of raw torch.benchmark.utils.Measurements for the pytorch and cutlass implementations for the various GEMMs. - """, # noqa: E501 - formatter_class=argparse.RawTextHelpFormatter, - ) - - parser.add_argument( - "--dtype", - type=to_torch_dtype, - required=True, - help="Available options are ['int8', 'fp8']", - ) - parser.add_argument( - "--kernels", - nargs="+", - type=str, - default=None, - help="Exact names of the kernels to benchmark. If not set, runs all kernels.", - ) - - subparsers = parser.add_subparsers(dest="cmd") - - square_parser = subparsers.add_parser("square_bench") - square_parser.add_argument("--dim-start", type=int, required=True) - square_parser.add_argument("--dim-end", type=int, required=True) - square_parser.add_argument("--dim-increment", type=int, required=True) - square_parser.set_defaults(func=run_square_bench) - - range_parser = subparsers.add_parser("range_bench") - range_parser.add_argument("--dim-start", type=int, required=True) - range_parser.add_argument("--dim-end", type=int, required=True) - range_parser.add_argument("--dim-increment", type=int, required=True) - range_parser.add_argument("--m-constant", type=int, default=None) - range_parser.add_argument("--n-constant", type=int, default=None) - range_parser.add_argument("--k-constant", type=int, default=None) - range_parser.set_defaults(func=run_range_bench) - - model_parser = subparsers.add_parser("model_bench") - model_parser.add_argument( - "--models", - nargs="+", - type=str, - default=DEFAULT_MODELS, - choices=WEIGHT_SHAPES.keys(), - ) - model_parser.add_argument( - "--tp-sizes", nargs="+", type=int, default=DEFAULT_TP_SIZES - ) - model_parser.add_argument( - "--batch-sizes", nargs="+", type=int, default=DEFAULT_BATCH_SIZES - ) - model_parser.set_defaults(func=run_model_bench) - - args = parser.parse_args() - args.func(args) diff --git a/benchmarks/cutlass_benchmarks/weight_shapes.py b/benchmarks/cutlass_benchmarks/weight_shapes.py deleted file mode 100644 index 25b96ef56620..000000000000 --- a/benchmarks/cutlass_benchmarks/weight_shapes.py +++ /dev/null @@ -1,46 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 -# SPDX-FileCopyrightText: Copyright contributors to the vLLM project - -# Weight Shapes are in the format -# ([K, N], TP_SPLIT_DIM) -# Example: -# A shape of ([14336, 4096], 0) indicates the following GEMM shape, -# - TP1 : K = 14336, N = 4096 -# - TP2 : K = 7168, N = 4096 -# A shape of ([4096, 6144], 1) indicates the following GEMM shape, -# - TP1 : K = 4096, N = 6144 -# - TP4 : K = 4096, N = 1536 - -# TP1 shapes -WEIGHT_SHAPES = { - "mistralai/Mistral-7B-v0.1": [ - ([4096, 6144], 1), - ([4096, 4096], 0), - ([4096, 28672], 1), - ([14336, 4096], 0), - ], - "meta-llama/Llama-2-7b-hf": [ - ([4096, 12288], 1), - ([4096, 4096], 0), - ([4096, 22016], 1), - ([11008, 4096], 0), - ], - "meta-llama/Llama-3-8b": [ - ([4096, 6144], 1), - ([4096, 4096], 0), - ([4096, 28672], 1), - ([14336, 4096], 0), - ], - "meta-llama/Llama-2-13b-hf": [ - ([5120, 15360], 1), - ([5120, 5120], 0), - ([5120, 27648], 1), - ([13824, 5120], 0), - ], - "meta-llama/Llama-2-70b-hf": [ - ([8192, 10240], 1), - ([8192, 8192], 0), - ([8192, 57344], 1), - ([28672, 8192], 0), - ], -} diff --git a/benchmarks/fused_kernels/merge_attn_states_benchmarks.py b/benchmarks/fused_kernels/merge_attn_states_benchmarks.py index 26b04299b353..61dfd7ea127c 100644 --- a/benchmarks/fused_kernels/merge_attn_states_benchmarks.py +++ b/benchmarks/fused_kernels/merge_attn_states_benchmarks.py @@ -1,7 +1,6 @@ # SPDX-License-Identifier: Apache-2.0 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project -""" -Benchmark: Fused FP8 output quantization in merge_attn_states +"""Benchmark: Fused FP8 output quantization in merge_attn_states. Compares fused vs unfused approaches for producing FP8-quantized merged attention output: diff --git a/benchmarks/kernels/bench_concat_mla_q.py b/benchmarks/kernels/bench_concat_mla_q.py index 8d940484d6b3..c528ab86b295 100644 --- a/benchmarks/kernels/bench_concat_mla_q.py +++ b/benchmarks/kernels/bench_concat_mla_q.py @@ -24,9 +24,9 @@ def make_inputs(num_tokens, dtype): """Create inputs matching the real code path. Args: - contiguous_nope: If False, simulate the transposed BMM output - (non-contiguous nope with stride pattern from - [N,B,L].transpose(0,1)). + num_tokens: Number of tokens to generate inputs for. + dtype: dtype of the generated tensors. + """ # Simulate: bmm output [N, B, L].transpose(0, 1) -> [B, N, L] raw = torch.randn(NUM_HEADS, num_tokens, NOPE_DIM, dtype=dtype, device="cuda") diff --git a/benchmarks/kernels/benchmark_2d_silu_mul_fp8_quant.py b/benchmarks/kernels/benchmark_2d_silu_mul_fp8_quant.py index 0dd5c6d84882..d0880b3f03cd 100644 --- a/benchmarks/kernels/benchmark_2d_silu_mul_fp8_quant.py +++ b/benchmarks/kernels/benchmark_2d_silu_mul_fp8_quant.py @@ -106,8 +106,7 @@ def make_impl_kwargs(self, impl_type: ImplType) -> dict[str, Any]: def reference_quant(x: torch.Tensor, quant_out: torch.Tensor, use_ue8m0: bool): - """ - Reference triton quant kernel from, + """Reference triton quant kernel from, vllm.model_executor.layers.quantization.utils.fp8_utils """ assert quant_out.size() == x.size() diff --git a/benchmarks/kernels/benchmark_cutlass_moe_fp8.py b/benchmarks/kernels/benchmark_cutlass_moe_fp8.py index 03d7fb386f74..bff4f73e2c0f 100644 --- a/benchmarks/kernels/benchmark_cutlass_moe_fp8.py +++ b/benchmarks/kernels/benchmark_cutlass_moe_fp8.py @@ -1,7 +1,6 @@ # SPDX-License-Identifier: Apache-2.0 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project -""" -Benchmark the performance of the cutlass_moe_fp8 kernel vs the triton_moe +"""Benchmark the performance of the cutlass_moe_fp8 kernel vs the triton_moe kernel. Both kernels take in fp8 quantized weights and 16-bit activations, but use different quantization strategies and backends. """ @@ -190,7 +189,7 @@ def bench_run( torch.accelerator.synchronize() def bench_cuda_graph(graph, num_warmup=5, num_iters=100): - """Benchmark CUDA graph using events like benchmark_moe.py""" + """Benchmark CUDA graph using events like benchmark_moe.py.""" # Warmup for _ in range(num_warmup): graph.replay() diff --git a/benchmarks/kernels/benchmark_cutlass_moe_nvfp4.py b/benchmarks/kernels/benchmark_cutlass_moe_nvfp4.py index 7379bf858889..a14242732a48 100644 --- a/benchmarks/kernels/benchmark_cutlass_moe_nvfp4.py +++ b/benchmarks/kernels/benchmark_cutlass_moe_nvfp4.py @@ -1,7 +1,6 @@ # SPDX-License-Identifier: Apache-2.0 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project -""" -Benchmark the performance of the cutlass_moe_fp4 kernel vs the triton_moe +"""Benchmark the performance of the cutlass_moe_fp4 kernel vs the triton_moe kernel. The cutlass_moe_fp4 kernel takes in fp4 quantized weights and 16-bit activations. The triton_moe kernel takes in fp8 weights(tensor scaled to fp8) and 16-bit activations. diff --git a/benchmarks/kernels/benchmark_device_communicators.py b/benchmarks/kernels/benchmark_device_communicators.py index 24e22023b91d..9f9a150471af 100644 --- a/benchmarks/kernels/benchmark_device_communicators.py +++ b/benchmarks/kernels/benchmark_device_communicators.py @@ -2,8 +2,7 @@ # SPDX-License-Identifier: Apache-2.0 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project -""" -Benchmark script for device communicators: +"""Benchmark script for device communicators: CustomAllreduce (oneshot, twoshot), PyNcclCommunicator, and SymmMemCommunicator (multimem, two-shot). @@ -17,6 +16,7 @@ Example: torchrun --nproc_per_node=2 benchmark_device_communicators.py --sequence-lengths 512 1024 2048 --num-warmup 10 --num-trials 100 + """ import json @@ -29,6 +29,10 @@ import torch.distributed as dist from torch.distributed import ProcessGroup +from vllm._aiter_ops import rocm_aiter_ops +from vllm.distributed.device_communicators.aiter_custom_all_reduce import ( + AiterCustomAllreduce, +) from vllm.distributed.device_communicators.custom_all_reduce import CustomAllreduce from vllm.distributed.device_communicators.flashinfer_all_reduce import ( FlashInferAllReduce, @@ -42,6 +46,7 @@ ) from vllm.distributed.device_communicators.symm_mem import SymmMemCommunicator from vllm.logger import init_logger +from vllm.platforms import current_platform from vllm.utils.argparse_utils import FlexibleArgumentParser logger = init_logger(__name__) @@ -56,6 +61,30 @@ # CUDA graph settings CUDA_GRAPH_CAPTURE_CYCLES = 10 +# All-gather and reduce-scatter are distinct collectives from all-reduce, so +# results are compared within a collective (each against its own NCCL/RCCL +# baseline) rather than across collectives. +COMM_COLLECTIVE = { + "ca_1stage": "all-reduce", + "ca_2stage": "all-reduce", + "pynccl": "all-reduce", + "pynccl-symm": "all-reduce", + "symm_mem_multimem": "all-reduce", + "symm_mem_two_shot": "all-reduce", + "flashinfer_trtllm": "all-reduce", + "flashinfer_mnnvl": "all-reduce", + "aiter_ag": "all-gather", + "pynccl_ag": "all-gather", + "aiter_rs": "reduce-scatter", + "pynccl_rs": "reduce-scatter", +} +COLLECTIVE_ORDER = ["all-reduce", "all-gather", "reduce-scatter"] +COLLECTIVE_BASELINE = { + "all-reduce": "pynccl", + "all-gather": "pynccl_ag", + "reduce-scatter": "pynccl_rs", +} + class CommunicatorBenchmark: """Benchmark class for testing device communicators.""" @@ -77,6 +106,11 @@ def __init__( max_seq_len = max(sequence_lengths) max_tensor_elements = max_seq_len * HIDDEN_SIZE self.max_size_override = max_tensor_elements * BENCHMARK_DTYPE.itemsize + 1 + # AITER has effective max size of half the max size, so we double it, and + # account for AG output size (which is world_size larger) + self.aiter_max_size = ( + 2 * world_size * max_tensor_elements * BENCHMARK_DTYPE.itemsize + 1 + ) # Initialize communicators self.custom_allreduce = None @@ -85,6 +119,7 @@ def __init__( self.symm_mem_comm_multimem = None self.symm_mem_comm_two_shot = None self.fi_ar_comm = None + self.aiter_comm = None self._init_communicators() @@ -181,11 +216,31 @@ def _init_communicators(self): ) self.fi_ar_comm = None + if current_platform.is_rocm() and rocm_aiter_ops.is_custom_all_reduce_enabled(): + try: + # Also used for AG/RS for DP + self.aiter_comm = AiterCustomAllreduce( + group=self.cpu_group, + device=self.device, + max_size=self.aiter_max_size, + ) + if not self.aiter_comm.disabled: + logger.info("Rank %s: AITER custom AG/RS initialized", self.rank) + else: + logger.info("Rank %s: AITER custom AG/RS disabled", self.rank) + self.aiter_comm = None + except Exception as e: + logger.warning( + "Rank %s: Failed to initialize AITER custom AG/RS: %s", + self.rank, + e, + ) + self.aiter_comm = None + def benchmark_allreduce( self, sequence_length: int, num_warmup: int, num_trials: int ) -> dict[str, float]: """Benchmark allreduce operations for all available communicators.""" - results = {} # Define communicators with their benchmark functions @@ -385,29 +440,116 @@ def benchmark_allreduce_single( f"CUDA graph benchmark failed for communicator: {e}" ) from e + def benchmark_ag_rs( + self, sequence_length: int, num_warmup: int, num_trials: int + ) -> dict[str, float]: + """Benchmark all-gather and reduce-scatter: custom (AITER) vs NCCL/RCCL. + + Each collective is measured for both the AITER custom kernel and the + PyNccl baseline so they compare like-for-like (print_results groups + columns by collective). All-gather maps ``(seq, hidden)`` -> + ``(world_size * seq, hidden)``; reduce-scatter maps ``(seq, hidden)`` -> + ``(seq // world_size, hidden)`` and so only runs when ``seq`` divides + evenly across ranks. Both reuse ``benchmark_allreduce_single`` timed on a + ``(seq, hidden)`` input; baselines write into a pre-allocated buffer. + """ + results: dict[str, float] = {} + entries = [] + + # ---- all-gather: (seq, hidden) -> (world_size * seq, hidden) ---- + if self.aiter_comm is not None: + c = self.aiter_comm + entries.append( + ( + "aiter_ag", + lambda t, c=c: c.custom_all_gather(t, dim=0), + lambda t, c=c: c.should_custom_ag(t), + c.capture(), + ) + ) + if self.pynccl_comm is not None: + c = self.pynccl_comm + ag_out = torch.empty( + sequence_length * self.world_size, + HIDDEN_SIZE, + dtype=BENCHMARK_DTYPE, + device=self.device, + ) + entries.append( + ( + "pynccl_ag", + lambda t, c=c, o=ag_out: c.all_gather(o, t), + lambda t: True, + nullcontext(), + ) + ) + + # ---- reduce-scatter: (seq, hidden) -> (seq // world_size, hidden) ---- + if sequence_length % self.world_size == 0: + rs_shape = (sequence_length // self.world_size, HIDDEN_SIZE) + if self.aiter_comm is not None: + c = self.aiter_comm + rs_out = torch.empty( + rs_shape, dtype=BENCHMARK_DTYPE, device=self.device + ) + entries.append( + ( + "aiter_rs", + lambda t, c=c, o=rs_out: c.custom_reduce_scatter(t, o, dim=0), + lambda t, c=c: c.should_custom_rs(t, dim=0), + c.capture(), + ) + ) + if self.pynccl_comm is not None: + c = self.pynccl_comm + rs_out = torch.empty( + rs_shape, dtype=BENCHMARK_DTYPE, device=self.device + ) + entries.append( + ( + "pynccl_rs", + lambda t, c=c, o=rs_out: c.reduce_scatter(o, t), + lambda t: True, + nullcontext(), + ) + ) + + for name, fn, should_use_fn, context in entries: + latency = self.benchmark_allreduce_single( + sequence_length, fn, should_use_fn, context, num_warmup, num_trials + ) + if latency is not None: + results[name] = latency + + return results + -def _calculate_speedup_info(comm_results: dict[str, float]) -> str: - """Calculate speedup information for a single tensor size.""" +def _calculate_speedup_info(comm_results: dict[str, float], baseline: str) -> str: + """Fastest comm in a collective group and its speedup vs ``baseline``.""" if not comm_results: return "N/A" - # Find the fastest communicator fastest_comm = min(comm_results.keys(), key=lambda k: comm_results[k]) fastest_time = comm_results[fastest_comm] - # Calculate speedup vs PyNccl if available - if "pynccl" in comm_results: - pynccl_time = comm_results["pynccl"] - speedup = pynccl_time / fastest_time + if baseline in comm_results and fastest_time > 0: + speedup = comm_results[baseline] / fastest_time return f"{fastest_comm} ({speedup:.2f}x)" - else: - return f"{fastest_comm} (N/A)" + return f"{fastest_comm} (N/A)" def print_results( - results: dict[str, dict[str, float]], sequence_lengths: list[int], world_size: int + results: dict[int, dict[str, float]], sequence_lengths: list[int], world_size: int ): - """Print benchmark results in a formatted table.""" + """Print benchmark results as one table per collective. + + All-reduce, all-gather and reduce-scatter are different collectives, so each + gets its own table and its speedup is computed against that collective's + NCCL/RCCL baseline rather than across collectives. + """ + present: set[str] = set() + for size_results in results.values(): + present.update(size_results.keys()) print(f"\n{'=' * 130}") print("Device Communicator Benchmark Results") @@ -415,53 +557,45 @@ def print_results( f"World Size: {world_size}, Data Type: {BENCHMARK_DTYPE}, " f"Hidden Size: {HIDDEN_SIZE}" ) + print("Tensor Shape is the per-rank input; times are ms per operation.") print(f"{'=' * 130}") - # Get all communicator names - all_comms = set() - for size_results in results.values(): - all_comms.update(size_results.keys()) - - all_comms = sorted(list(all_comms)) - - # Print header - header = f"{'Tensor Shape':<20}{'Tensor Size':<15}" - for comm in all_comms: - header += f"{comm:<20}" - header += f"{'Best (Speedup vs PyNccl)':<30}" - print(header) - print("-" * len(header)) - - # Print results for each sequence length - for seq_len in sequence_lengths: - if seq_len in results: - # Calculate tensor size in elements and bytes - tensor_elements = seq_len * HIDDEN_SIZE - tensor_bytes = tensor_elements * BENCHMARK_DTYPE.itemsize - - # Format tensor size (MB) - tensor_size_mb = tensor_bytes / (1024 * 1024) - tensor_size_str = f"{tensor_size_mb:.2f} MB" - - # Format tensor shape + for collective in COLLECTIVE_ORDER: + comms = sorted(c for c in present if COMM_COLLECTIVE.get(c) == collective) + if not comms: + continue + baseline = COLLECTIVE_BASELINE[collective] + + print(f"\n{collective} (speedup vs {baseline})") + header = f"{'Tensor Shape':<20}{'Tensor Size':<15}" + for comm in comms: + header += f"{comm:<20}" + header += f"{'Best':<30}" + print(header) + print("-" * len(header)) + + for seq_len in sequence_lengths: + if seq_len not in results: + continue + row_results = { + c: results[seq_len][c] for c in comms if c in results[seq_len] + } + + tensor_bytes = seq_len * HIDDEN_SIZE * BENCHMARK_DTYPE.itemsize + tensor_size_str = f"{tensor_bytes / (1024 * 1024):.2f} MB" tensor_shape = f"({seq_len}, {HIDDEN_SIZE})" row = f"{tensor_shape:<20}{tensor_size_str:<15}" - for comm in all_comms: - if comm in results[seq_len]: - row += f"{results[seq_len][comm]:<20.3f}" + for comm in comms: + if comm in row_results: + row += f"{row_results[comm]:<20.3f}" else: row += f"{'N/A':<20}" - - # Calculate speedup information - speedup_info = _calculate_speedup_info(results[seq_len]) - row += f"{speedup_info:<30}" - + row += f"{_calculate_speedup_info(row_results, baseline):<30}" print(row) - print(f"{'=' * 130}") - print("All times are in milliseconds (ms) per allreduce operation") - print("Speedup column shows: fastest_algorithm (speedup_vs_pynccl)") + print(f"\n{'=' * 130}") + print("All times are in milliseconds (ms) per operation") def main(): @@ -526,6 +660,13 @@ def main(): num_warmup=args.num_warmup, num_trials=args.num_trials, ) + results.update( + benchmark.benchmark_ag_rs( + sequence_length=seq_len, + num_warmup=args.num_warmup, + num_trials=args.num_trials, + ) + ) all_results[seq_len] = results @@ -538,12 +679,23 @@ def main(): # Save to JSON if requested if args.output_json: - # Add speedup information to results + # Add per-collective speedup information to results. enhanced_results = {} for seq_len, comm_results in all_results.items(): + speedup_info = {} + for collective in COLLECTIVE_ORDER: + group = { + k: v + for k, v in comm_results.items() + if COMM_COLLECTIVE.get(k) == collective + } + if group: + speedup_info[collective] = _calculate_speedup_info( + group, COLLECTIVE_BASELINE[collective] + ) enhanced_results[seq_len] = { "timings": comm_results, - "speedup_info": _calculate_speedup_info(comm_results), + "speedup_info": speedup_info, } output_data = { diff --git a/benchmarks/kernels/benchmark_dsv41_mega_attn.py b/benchmarks/kernels/benchmark_dsv41_mega_attn.py new file mode 100644 index 000000000000..1b3b5d9c3848 --- /dev/null +++ b/benchmarks/kernels/benchmark_dsv41_mega_attn.py @@ -0,0 +1,272 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +"""FlashMLA mega attention vs the split-KV decode pipeline (DeepSeek V4.1). + +Times the full set of per-layer ops each path runs in a decode step, captured +in a CUDA graph -- eager timing is dominated by launch overhead and flatters +whichever path launches fewer kernels. + + mega: fused_qnorm_rope_kv_rope_quant_insert (Q pad + KV insert) + + mega kernel + split-KV: fused_qnorm_rope_kv_rope_quant_insert (Q RoPE + pad + KV insert) + + flash_mla_with_kvcache + fused_inv_rope_fp8_quant + +Both arms fuse their Q preparation into the KV insert, so that op is charged +to both; the mega arm's Q side is only a zero-pad (nothing at all when the +shard is already at the kernel head count) because its kernel does the Q RoPE, +the inverse RoPE and the FP8 cast. + +``--local-heads`` is the variable that decides the outcome: the mega kernel +absorbs work proportional to the live head count while paying for the padded +one, so it wins at 64 live heads and is a wash at 16 (TP4 on a 64-head model). + +Both arms use the same KV records so the comparison is kernel-for-kernel: +a V4.1 fp8 (528 B) sliding-window cache and a compressed cache that is either +V4.1 fp8 or V4.1 NVFP4 (288 B) -- FlashMLA's SM100 sparse decode reads the +NVFP4 compressed record in both the fused and unfused kernels. + +Run: .venv/bin/python benchmarks/kernels/benchmark_dsv41_mega_attn.py +""" + +import argparse + +import torch + +import vllm.v1.attention.ops.flashmla as fm +from vllm.models.deepseek_v4.common.ops.fused_inv_rope_fp8_quant import ( + fused_inv_rope_fp8_quant, +) +from vllm.models.deepseek_v41.common.ops import quantize_and_insert_k_cache +from vllm.models.deepseek_v41.common.ops.fused_compress_quant_cache import ( + rope_quant_insert, +) +from vllm.models.deepseek_v41.common.ops.fused_layout import permute_q_to_fused +from vllm.models.deepseek_v41.nvidia.flash_mla_mega_attn import ( + alloc_mega_attn_output, + is_flashmla_mega_attn_supported, +) +from vllm.utils.math_utils import round_up + +HEAD_DIM, ROPE_DIM = 512, 64 +V41_BYTES, V41_FP4_BYTES = 528, 288 +SWA_BLOCK, COMPRESSED_BLOCK = 32, 128 + + +def make_cos_sin_cache(max_pos, device): + inv_freq = 1.0 / ( + 10000 ** (torch.arange(0, ROPE_DIM, 2, device=device).float() / ROPE_DIM) + ) + freqs = torch.outer(torch.arange(max_pos, device=device).float(), inv_freq) + return torch.cat([freqs.cos(), freqs.sin()], -1) + + +def empty_paged_cache(num_rows, block_size, bytes_per_token, device): + """A zeroed paged cache view [num_blocks, block_size, bytes_per_token].""" + num_blocks = (num_rows + block_size - 1) // block_size + 1 + page = round_up(block_size * bytes_per_token, 512) + backing = torch.zeros(num_blocks, page, dtype=torch.uint8, device=device) + return backing, backing.as_strided( + (num_blocks, block_size, bytes_per_token), (page, bytes_per_token, 1) + ) + + +def fill_cache(view, bytes_per_token, cos_sin, device): + n = view.shape[0] * view.shape[1] + rows = torch.randn(n, HEAD_DIM, device=device, dtype=torch.bfloat16) + slots = torch.arange(n, dtype=torch.int64, device=device) + if bytes_per_token == V41_FP4_BYTES: + # A page-aligned cache has more slots than the cos_sin table has rows; + # wrap so the insert never indexes past it. + positions = slots % cos_sin.shape[0] + rope_quant_insert(rows, positions, cos_sin, view, slots, 1) + else: + quantize_and_insert_k_cache( + rows, + view.reshape(view.shape[0], -1), + slots, + block_size=view.shape[1], + bytes_per_token=bytes_per_token, + ) + + +def time_graph(fn, iters=100, warmup=5): + """Capture ``fn`` in a CUDA graph and time its replay.""" + s = torch.cuda.Stream() + s.wait_stream(torch.cuda.current_stream()) + with torch.cuda.stream(s): + for _ in range(3): + fn() + torch.cuda.current_stream().wait_stream(s) + g = torch.cuda.CUDAGraph() + with torch.cuda.graph(g): + fn() + for _ in range(warmup): + g.replay() + torch.accelerator.synchronize() + start = torch.cuda.Event(enable_timing=True) + end = torch.cuda.Event(enable_timing=True) + start.record() + for _ in range(iters): + g.replay() + end.record() + torch.accelerator.synchronize() + return start.elapsed_time(end) * 1000 / iters + + +def bench_decode(s_q, device, topk_extra, extra_bytes, local_heads): + padded_heads, scale = 64, HEAD_DIM**-0.5 + 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( + s_q, local_heads, HEAD_DIM, device=device, dtype=torch.bfloat16 + ) + q_fused = permute_q_to_fused(q_local) + kv = torch.randn(s_q, HEAD_DIM, device=device, dtype=torch.bfloat16) + + n_swa = s_q * 128 + 128 + swa_backing, swa = empty_paged_cache(n_swa, SWA_BLOCK, V41_BYTES, device) + fill_cache(swa, V41_BYTES, cos_sin, device) + _, extra = empty_paged_cache(65536, COMPRESSED_BLOCK, extra_bytes, device) + fill_cache(extra, extra_bytes, cos_sin, device) + + slot_mapping = torch.arange(s_q, dtype=torch.int64, device=device) + swa_idx = torch.randint(0, n_swa, (s_q, 128), device=device, dtype=torch.int32) + swa_len = torch.full((s_q,), 128, device=device, dtype=torch.int32) + ex_idx = torch.randint( + 0, 65536, (s_q, topk_extra), device=device, dtype=torch.int32 + ) + ex_len = torch.full((s_q,), topk_extra, device=device, dtype=torch.int32) + sink = torch.zeros(padded_heads, device=device) + sched = fm.FlashMLASchedMeta() + out = alloc_mega_attn_output(s_q, n_wv_group, device) + swa4 = swa.unsqueeze(-2) + extra4 = extra.unsqueeze(-2) + swa_2d = swa_backing + + def mega(): + # One launch zero-pads the fused-layout Q and inserts the SWA KV. + q_pad = torch.ops._C.fused_deepseek_v4_qnorm_rope_kv_rope_quant_insert( + q_fused, + kv, + swa_2d, + slot_mapping, + positions, + cos_sin, + 0 if padded_heads == local_heads else padded_heads, + 1e-6, + SWA_BLOCK, + False, # apply_q_norm + True, # kv_mxfp8 + False, # apply_q_rope: the mega kernel rotates Q itself + True, # is_q_interleaved + ) + torch.ops._flashmla_C.fused_norm_rope_attn_rope_cast_decode( + q_fused if padded_heads == local_heads else q_pad, + swa4, + swa_idx, + scale, + HEAD_DIM, + sink, + swa_len, + extra4, + ex_idx, + ex_len, + False, + 0.0, + pos32, + False, + 64, + cos_sin, + n_wv_group, + 32, + True, + True, + True, + out.data, + out.scale, + ) + + def split_kv(): + q_pad = torch.ops._C.fused_deepseek_v4_qnorm_rope_kv_rope_quant_insert( + q_local, + kv, + swa_2d, + slot_mapping, + positions, + cos_sin, + padded_heads, + 1e-6, + SWA_BLOCK, + False, + True, + ) + o = fm.flash_mla_with_kvcache( + q=q_pad.unsqueeze(1), + k_cache=swa4, + block_table=None, + head_dim_v=HEAD_DIM, + tile_scheduler_metadata=sched, + cache_seqlens=None, + is_fp8_kvcache=True, + indices=swa_idx.view(s_q, 1, -1), + topk_length=swa_len, + softmax_scale=scale, + attn_sink=sink, + extra_k_cache=extra4, + extra_indices_in_kvcache=ex_idx.view(s_q, 1, -1), + extra_topk_length=ex_len, + )[0] + fused_inv_rope_fp8_quant( + o.squeeze(1)[:, :local_heads], + positions, + cos_sin, + n_groups=local_heads // 8, + heads_per_group=8, + quant_group_size=32, + tma_aligned_scales=True, + ) + + return time_graph(mega), time_graph(split_kv) + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument( + "--decode-s-q", + nargs="+", + type=int, + default=[1, 2, 4, 8, 16, 32, 64, 128, 256, 512], + ) + parser.add_argument("--topk-extra", type=int, default=512) + parser.add_argument("--local-heads", type=int, default=16) + parser.add_argument( + "--extra-bytes", + type=int, + choices=[V41_BYTES, V41_FP4_BYTES], + default=V41_BYTES, + help="compressed-cache record: 528 (V4.1 fp8) or 288 (V4.1 NVFP4)", + ) + args = parser.parse_args() + ok, reason = is_flashmla_mega_attn_supported() + if not ok: + raise SystemExit(reason) + device = torch.device("cuda") + torch.manual_seed(0) + print( + f"decode, cudagraph (topk_swa=128, topk_extra={args.topk_extra}, " + f"local_heads={args.local_heads}, extra={args.extra_bytes}B)" + ) + print(f"{'s_q':>6} {'mega_us':>9} {'split_kv_us':>12} {'speedup':>8}") + for s_q in args.decode_s_q: + m, s = bench_decode( + s_q, device, args.topk_extra, args.extra_bytes, args.local_heads + ) + print(f"{s_q:>6d} {m:9.1f} {s:12.1f} {s / m:7.2f}x") + + +if __name__ == "__main__": + main() diff --git a/benchmarks/kernels/benchmark_fused_collective.py b/benchmarks/kernels/benchmark_fused_collective.py index c999c16021bf..aaa39fd9fe06 100644 --- a/benchmarks/kernels/benchmark_fused_collective.py +++ b/benchmarks/kernels/benchmark_fused_collective.py @@ -1,8 +1,7 @@ # SPDX-License-Identifier: Apache-2.0 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project -""" -Benchmark for FlashInfer fused collective operations vs standard operations. +"""Benchmark for FlashInfer fused collective operations vs standard operations. This benchmark compares: 1. FlashInfer's allreduce_fusion with trtllm backend @@ -446,9 +445,15 @@ def run_benchmarks( """Run all benchmarks for given configuration. Args: + num_tokens: Number of tokens in the benchmark input. + hidden_dim: Hidden dimension of the benchmark input. + dtype: dtype of the benchmark input. + use_residual: Whether to include a residual add in the fused op. + no_oneshot: Skip the one-shot allreduce variants. allreduce_params: Shared parameters for FlashInfer fused allreduce. workspaces: Dict mapping backend name ("trtllm", "mnnvl") to workspace. quant_modes: Set of quantization modes: "none", "fp8", "fp4". + """ ( input_tensor, diff --git a/benchmarks/kernels/benchmark_gelu_and_mul_sparse.py b/benchmarks/kernels/benchmark_gelu_and_mul_sparse.py new file mode 100644 index 000000000000..9f8d97612161 --- /dev/null +++ b/benchmarks/kernels/benchmark_gelu_and_mul_sparse.py @@ -0,0 +1,90 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project + +import itertools + +import torch + +import vllm.kernels # noqa: F401 +from vllm import ir +from vllm.benchmarks.lib.utils import default_vllm_config +from vllm.triton_utils import triton +from vllm.utils.argparse_utils import FlexibleArgumentParser +from vllm.utils.torch_utils import STR_DTYPE_TO_TORCH_DTYPE, set_random_seed + +NUM_TOKENS = [1, 2, 8, 32, 128, 512, 2048] +INTERMEDIATE_SIZES = [2048, 4096, 8192, 13824, 16384, 32768] +CONFIGS = list(itertools.product(NUM_TOKENS, INTERMEDIATE_SIZES)) +STD_MULTIPLIER = 1.6448536269514722 + + +@default_vllm_config() +def benchmark_gelu_and_mul_sparse( + num_tokens: int, + intermediate_size: int, + provider: str, + dtype: torch.dtype, +): + set_random_seed(0) + torch.set_default_device("cuda") + x = torch.randn(num_tokens, 2 * intermediate_size, dtype=dtype) + native = ir.ops.gelu_and_mul_sparse.impls["native"].impl_fn + triton_impl = ir.ops.gelu_and_mul_sparse.impls["triton"].impl_fn + args = (x, STD_MULTIPLIER, "tanh") + + expected = native(*args) + actual = triton_impl(*args) + torch.testing.assert_close( + actual, + expected, + **ir.ops.gelu_and_mul_sparse.get_tolerance(actual.dtype), + ) + + if provider == "triton": + fn = lambda: triton_impl(*args) + elif provider == "native_compiled": + compiled = torch.compile(native, fullgraph=True) + compiled(*args) + fn = lambda: compiled(*args) + else: + fn = lambda: native(*args) + + ms, min_ms, max_ms = triton.testing.do_bench_cudagraph( + fn, quantiles=[0.5, 0.2, 0.8] + ) + return ms, max_ms, min_ms + + +if __name__ == "__main__": + parser = FlexibleArgumentParser( + description="Benchmark the Gemma3n sparse GELU activation." + ) + parser.add_argument( + "--dtype", + type=str, + choices=["half", "bfloat16", "float"], + default="bfloat16", + ) + args = parser.parse_args() + dtype = STR_DTYPE_TO_TORCH_DTYPE[args.dtype] + + perf_report = triton.testing.perf_report( + triton.testing.Benchmark( + x_names=["num_tokens", "intermediate_size"], + x_vals=CONFIGS, + line_arg="provider", + line_vals=["triton", "native_compiled", "native_eager"], + line_names=["Triton", "Native (torch.compile)", "Native (eager)"], + styles=[("blue", "-"), ("green", "-"), ("red", "-")], + ylabel="ms", + plot_name="gelu-and-mul-sparse-performance", + args={}, + ) + ) + perf_report( + lambda num_tokens, intermediate_size, provider: ( + benchmark_gelu_and_mul_sparse( + num_tokens, intermediate_size, provider, dtype + ) + ) + ).run(print_data=True) diff --git a/benchmarks/kernels/benchmark_gemm_rs_ar.py b/benchmarks/kernels/benchmark_gemm_rs_ar.py new file mode 100644 index 000000000000..3e47f61c74f0 --- /dev/null +++ b/benchmarks/kernels/benchmark_gemm_rs_ar.py @@ -0,0 +1,407 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +"""Benchmark the SM100 GEMM-RS/AR kernel against the model's unfused path. + +The baseline is what a model runs without the fusion: the projection's own +linear kernel followed by vLLM's collective (``sp_reduce_scatter`` for RS, +``tensor_model_parallel_all_reduce`` for AR), with the collective backend +chosen by vLLM's dispatch exactly as in serving. The same two kernels are also +timed bare (the GEMM alone, the collective alone on a precomputed partial); +their max is the floor a perfectly overlapped fused kernel could reach. + +All ranks must belong to one NVLink domain. Run a TP8 sweep over the Kimi-K3 +projections with: + + torchrun --nproc-per-node=8 -- benchmarks/kernels/benchmark_gemm_rs_ar.py + +or the DeepSeek-V4.1 MXFP8 ``wo_b`` projection at TP4 with: + + torchrun --nproc-per-node=4 -- benchmarks/kernels/benchmark_gemm_rs_ar.py \ + --model deepseek_v41 --backend flashinfer_cutedsl +""" + +import argparse +import json +import os +import statistics +from collections.abc import Callable + +import torch +import torch.distributed as dist + +from vllm.config import ModelConfig, VllmConfig, set_current_vllm_config +from vllm.distributed import ( + cleanup_dist_env_and_memory, + tensor_model_parallel_all_reduce, +) +from vllm.distributed.parallel_state import ( + get_tp_group, + init_distributed_environment, + initialize_model_parallel, +) +from vllm.model_executor.kernels.linear.cute_dsl.gemm_rs_ar import GemmRsAr +from vllm.model_executor.layers.linear import RowParallelLinear +from vllm.models.common.ops.sequence_parallel import sp_reduce_scatter + +# 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). +_MODEL_PROJECTIONS = { + "kimi_k3": (7168, (6144, 12288)), + "deepseek_v41": (5120, (8192,)), +} + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser() + parser.add_argument( + "--mode", + choices=("rs", "ar"), + default="rs", + help="Collective mode to benchmark.", + ) + parser.add_argument( + "--m", + type=int, + nargs="+", + default=[128, 512, 2048, 8192, 32768], + help="Global token counts to benchmark.", + ) + parser.add_argument( + "--model", + choices=tuple(_MODEL_PROJECTIONS), + default="kimi_k3", + help="Model whose projection shapes set the default --n and --k.", + ) + parser.add_argument( + "--backend", + choices=("bf16", "flashinfer_cutlass", "flashinfer_cutedsl"), + default="bf16", + help="Weight dtype/layout: BF16 or an online-MXFP8 FlashInfer kernel.", + ) + parser.add_argument( + "--k", + type=int, + nargs="+", + help=( + "Per-rank input dimensions. By default, derive the selected " + "model's projection dimensions from the TP world size." + ), + ) + parser.add_argument( + "--n", + type=int, + help="Output width. Defaults to the selected model's hidden size.", + ) + parser.add_argument( + "--num-workspaces", + type=int, + default=10, + help="Pointer-distinct inputs and CUDA graphs to rotate.", + ) + parser.add_argument("--warmup-replays", type=int, default=5) + parser.add_argument("--samples", type=int, default=20) + parser.add_argument("--json", type=str, help="Also write the results here.") + return parser.parse_args() + + +def make_projection( + gemm_rs_ar: GemmRsAr, + weight: torch.Tensor, + world_size: int, + backend: str, +) -> RowParallelLinear: + """Wrap a BF16 [N, K] weight the way the model would hand it to GEMM-RS. + + For the MXFP8 backends this quantizes online through the FlashInfer + kernel's post-load step, so the fused kernel and the unfused baseline see + the production weight and scale layouts. + """ + N, K = weight.shape + quant_config = None + if backend != "bf16": + from vllm.config.quantization import QuantizationConfigArgs + from vllm.model_executor.layers.quantization.online.base import ( + OnlineQuantizationConfig, + ) + + quant_config = OnlineQuantizationConfig(QuantizationConfigArgs(linear="mxfp8")) + with torch.device(weight.device): + linear = RowParallelLinear( + K * world_size, + N, + bias=False, + params_dtype=weight.dtype, + quant_config=quant_config, + # The baseline reduces the local partial itself, like the models. + reduce_results=False, + return_bias=False, + ) + # Select the fused path before online quantization replaces the weights. + assert gemm_rs_ar.can_run(linear) + linear.weight = torch.nn.Parameter(weight, requires_grad=False) + linear.quant_method.process_weights_after_loading(linear) + return linear + + +def capture_graph( + op: Callable[[], torch.Tensor], + stream: torch.cuda.Stream, + cpu_group: dist.ProcessGroup, +) -> torch.cuda.CUDAGraph: + stream.wait_stream(torch.cuda.current_stream()) + dist.barrier(group=cpu_group) + with torch.cuda.stream(stream): + for _ in range(3): + op() + stream.synchronize() + dist.barrier(group=cpu_group) + + graph = torch.cuda.CUDAGraph() + with torch.cuda.graph(graph, stream=stream): + op() + torch.cuda.current_stream().wait_stream(stream) + dist.barrier(group=cpu_group) + return graph + + +def benchmark_graphs( + candidate_graphs: dict[str, list[torch.cuda.CUDAGraph]], + warmup_replays: int, + samples: int, + device_group: dist.ProcessGroup, + device_barrier: Callable[[], None], +) -> dict[str, float]: + """Median over samples of the slowest rank's replay time, in us.""" + names = list(candidate_graphs) + for round_index in range(warmup_replays): + for offset in range(len(names)): + name = names[(round_index + offset) % len(names)] + graphs = candidate_graphs[name] + device_barrier() + graphs[round_index % len(graphs)].replay() + torch.accelerator.synchronize() + + timings: dict[str, list[float]] = {name: [] for name in names} + start = torch.cuda.Event(enable_timing=True) + end = torch.cuda.Event(enable_timing=True) + for sample_index in range(samples): + for offset in range(len(names)): + name = names[(sample_index + offset) % len(names)] + graphs = candidate_graphs[name] + device_barrier() + start.record() + graphs[sample_index % len(graphs)].replay() + end.record() + end.synchronize() + + elapsed = torch.tensor( + start.elapsed_time(end) * 1000, + dtype=torch.float64, + device=torch.accelerator.current_device_index(), + ) + dist.all_reduce(elapsed, op=dist.ReduceOp.MAX, group=device_group) + timings[name].append(elapsed.item()) + return {name: statistics.median(values) for name, values in timings.items()} + + +def benchmark_shape( + gemm_rs_ar: GemmRsAr, + mode: str, + backend: str, + M: int, + N: int, + K: int, + num_workspaces: int, + warmup_replays: int, + samples: int, + device_group: dist.ProcessGroup, + cpu_group: dist.ProcessGroup, + device_barrier: Callable[[], None], +) -> dict[str, float | int | str]: + all_reduce = mode == "ar" + world_size = dist.get_world_size(device_group) + rank = dist.get_rank(device_group) + device = torch.device("cuda", torch.accelerator.current_device_index()) + local_M = (M + world_size - 1) // world_size + + rng = torch.Generator(device=device) + rng.manual_seed(1000 + rank * 10 + M + K) + inputs = [ + torch.randn(M, K, dtype=torch.bfloat16, device=device, generator=rng) + for _ in range(num_workspaces) + ] + projections = [ + make_projection( + gemm_rs_ar, + torch.randn(N, K, dtype=torch.bfloat16, device=device, generator=rng), + world_size, + backend, + ) + for _ in range(num_workspaces) + ] + + def collective(partial: torch.Tensor) -> torch.Tensor: + if all_reduce: + return tensor_model_parallel_all_reduce(partial) + return sp_reduce_scatter(partial) + + def gemm(x: torch.Tensor, linear: RowParallelLinear) -> torch.Tensor: + return linear(x) + + def unfused(x: torch.Tensor, linear: RowParallelLinear) -> torch.Tensor: + return collective(linear(x)) + + def fused(x: torch.Tensor, linear: RowParallelLinear) -> torch.Tensor: + return gemm_rs_ar.apply(x, linear) + + # The bare collective runs on the GEMM's own output so it sees the same + # message size and layout the unfused path hands to the collective. + partials = [linear(x) for x, linear in zip(inputs, projections)] + + expected = unfused(inputs[0], projections[0]) + actual = fused(inputs[0], projections[0]) + torch.accelerator.synchronize(device) + rows = M if all_reduce else min(max(M - rank * local_M, 0), local_M) + torch.testing.assert_close( + actual[:rows], expected[:rows], rtol=5e-2, atol=4.0, msg=f"M={M}, K={K}" + ) + + candidates: dict[str, list[Callable[[], torch.Tensor]]] = { + "gemm": [lambda x=x, w=w: gemm(x, w) for x, w in zip(inputs, projections)], + "collective": [lambda p=p: collective(p) for p in partials], + "vllm": [lambda x=x, w=w: unfused(x, w) for x, w in zip(inputs, projections)], + "gemm_rs_ar": [ + lambda x=x, w=w: fused(x, w) for x, w in zip(inputs, projections) + ], + } + + candidate_graphs = {} + keepalive: list[object] = [] + for name, ops in candidates.items(): + stream = torch.cuda.Stream() + candidate_graphs[name] = [capture_graph(op, stream, cpu_group) for op in ops] + keepalive.append(stream) + + times = benchmark_graphs( + candidate_graphs, warmup_replays, samples, device_group, device_barrier + ) + floor = max(times["gemm"], times["collective"]) + return { + "mode": mode.upper(), + "backend": backend, + "M": M, + "N": N, + "K": K, + "gemm_us": times["gemm"], + "collective_us": times["collective"], + "floor_us": floor, + "vllm_us": times["vllm"], + "gemm_rs_ar_us": times["gemm_rs_ar"], + "latency_change": (times["gemm_rs_ar"] - times["vllm"]) / times["vllm"], + "floor_gap": (times["gemm_rs_ar"] - floor) / floor, + } + + +def print_results(results: list[dict[str, float | int | str]]) -> None: + collective = results[0]["mode"] + backend = results[0]["backend"] + print(f"### GEMM-{collective} vs vLLM unfused path ({backend} weights)") + print( + f"| M | N | K | GEMM (us) | {collective} (us) | floor (us) | vLLM (us) | " + f"GEMM-{collective} (us) | vs vLLM | vs floor |" + ) + print("|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|") + for r in results: + print( + f"| {r['M']} | {r['N']} | {r['K']} | {r['gemm_us']:.1f} | " + f"{r['collective_us']:.1f} | {r['floor_us']:.1f} | {r['vllm_us']:.1f} | " + f"{r['gemm_rs_ar_us']:.1f} | {r['latency_change']:+.1%} | " + f"{r['floor_gap']:+.1%} |" + ) + + +def main() -> None: + args = parse_args() + default_n, default_k = _MODEL_PROJECTIONS[args.model] + if args.n is None: + args.n = default_n + assert args.m and min(args.m) > 0 + assert args.n % 128 == 0 + assert args.num_workspaces > 0 + assert args.warmup_replays >= 0 + assert args.samples > 0 + + local_rank = int(os.environ["LOCAL_RANK"]) + torch.accelerator.set_device_index(local_rank) + init_distributed_environment() + world_size = dist.get_world_size() + if args.k is None: + assert all(K % world_size == 0 for K in default_k) + K_values = [K // world_size for K in default_k] + else: + K_values = args.k + k_alignment = 64 if args.backend == "bf16" else 128 + assert all(K % k_alignment == 0 for K in K_values) + + config = VllmConfig() + config.model_config = ModelConfig(dtype="bfloat16") + # Pin the online-MXFP8 kernel the projections quantize through. + config.kernel_config.linear_backend = ( + "auto" if args.backend == "bf16" else args.backend + ) + with set_current_vllm_config(config): + # Builds the TP communicator as in serving, so the baseline's + # collective is whatever vLLM's dispatch selects (FlashInfer all-reduce + # first, then NCCL symmetric memory, custom all-reduce, pynccl). + initialize_model_parallel(tensor_model_parallel_size=world_size) + + tp_group = get_tp_group() + pynccl_comm = tp_group.device_communicator.pynccl_comm + assert pynccl_comm is not None + sync_input = torch.zeros(1, device=torch.accelerator.current_device_index()) + sync_output = torch.empty_like(sync_input) + + def device_barrier() -> None: + # Order the timed launch after a device-side rank rendezvous without + # including the rendezvous itself in the measured event interval. + pynccl_comm.all_reduce(sync_input, sync_output) + + gemm_rs_ar = GemmRsAr( + max_M=max(args.m), + N=args.n, + all_reduce=args.mode == "ar", + ) + with set_current_vllm_config(config): + results = [ + benchmark_shape( + gemm_rs_ar, + args.mode, + args.backend, + M, + args.n, + K, + args.num_workspaces, + args.warmup_replays, + args.samples, + tp_group.device_group, + tp_group.cpu_group, + device_barrier, + ) + for K in K_values + for M in args.m + ] + del gemm_rs_ar + + if tp_group.rank_in_group == 0: + print_results(results) + if args.json: + with open(args.json, "w") as f: + json.dump(results, f, indent=2) + + dist.barrier(group=tp_group.cpu_group) + cleanup_dist_env_and_memory() + + +if __name__ == "__main__": + main() diff --git a/benchmarks/kernels/benchmark_kimi_k3_attn_res.py b/benchmarks/kernels/benchmark_kimi_k3_attn_res.py index c8cf6f9acc77..dba82a872226 100644 --- a/benchmarks/kernels/benchmark_kimi_k3_attn_res.py +++ b/benchmarks/kernels/benchmark_kimi_k3_attn_res.py @@ -10,6 +10,7 @@ .venv/bin/python benchmarks/kernels/benchmark_kimi_k3_attn_res.py \ --output /tmp/attn_res.json + """ from __future__ import annotations diff --git a/benchmarks/kernels/benchmark_kimi_k3_gemm_rs_ar.py b/benchmarks/kernels/benchmark_kimi_k3_gemm_rs_ar.py deleted file mode 100644 index e4daea26bbce..000000000000 --- a/benchmarks/kernels/benchmark_kimi_k3_gemm_rs_ar.py +++ /dev/null @@ -1,469 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 -# SPDX-FileCopyrightText: Copyright contributors to the vLLM project -"""Benchmark the SM100 Kimi-K3 GEMM-RS/AR kernel. - -All ranks must belong to one NVLink domain. For example, run a TP8 sweep with: - - torchrun --nproc-per-node=8 \ - benchmarks/kernels/benchmark_kimi_k3_gemm_rs_ar.py -""" - -import argparse -import os -import statistics -from collections.abc import Callable -from dataclasses import dataclass - -import pandas as pd -import torch -import torch.distributed as dist -import torch.distributed._symmetric_memory as symm_mem - -from vllm.config import VllmConfig, set_current_vllm_config -from vllm.distributed import cleanup_dist_env_and_memory -from vllm.distributed.parallel_state import ( - get_tp_group, - init_distributed_environment, - initialize_model_parallel, -) -from vllm.models.kimi_k3.nvidia.ops.cute_dsl.gemm_rs_ar import GemmRsAr - -# Shared-expert down-proj and attention O-proj. -_KIMI_K3_PROJECTION_K = (6144, 12288) - - -@dataclass -class Candidate: - name: str - runs: list[Callable[[], torch.Tensor]] - check_correctness: bool = True - - -def parse_args() -> argparse.Namespace: - parser = argparse.ArgumentParser() - parser.add_argument( - "--mode", - choices=("rs", "ar"), - default="rs", - help="Collective mode to benchmark.", - ) - parser.add_argument( - "--m", - type=int, - nargs="+", - default=[128, 512, 2048, 8192, 32768], - help="Global token counts to benchmark.", - ) - parser.add_argument( - "--k", - type=int, - nargs="+", - help=( - "Per-rank input dimensions. By default, derive the Kimi-K3 " - "shared-expert down-proj and O-proj dimensions from the TP " - "world size." - ), - ) - parser.add_argument("--n", type=int, default=7168) - parser.add_argument( - "--num-workspaces", - type=int, - default=10, - help="Pointer-distinct inputs and CUDA graphs to rotate.", - ) - parser.add_argument("--warmup-replays", type=int, default=5) - parser.add_argument("--samples", type=int, default=20) - return parser.parse_args() - - -def capture_graph( - op: Callable[[], torch.Tensor], - stream: torch.cuda.Stream, - cpu_group: dist.ProcessGroup, -) -> tuple[torch.cuda.CUDAGraph, list[torch.Tensor | None]]: - result: list[torch.Tensor | None] = [None] - stream.wait_stream(torch.cuda.current_stream()) - dist.barrier(group=cpu_group) - with torch.cuda.stream(stream): - for _ in range(3): - result[0] = op() - stream.synchronize() - dist.barrier(group=cpu_group) - - graph = torch.cuda.CUDAGraph() - with torch.cuda.graph(graph, stream=stream): - result[0] = op() - torch.cuda.current_stream().wait_stream(stream) - dist.barrier(group=cpu_group) - return graph, result - - -def benchmark_graphs( - candidate_graphs: dict[str, list[torch.cuda.CUDAGraph]], - warmup_replays: int, - samples: int, - device_group: dist.ProcessGroup, - device_barrier: Callable[[], None], -) -> dict[str, float]: - candidate_names = list(candidate_graphs) - for round_index in range(warmup_replays): - for candidate_index in range(len(candidate_names)): - candidate_id = (round_index + candidate_index) % len(candidate_names) - name = candidate_names[candidate_id] - graphs = candidate_graphs[name] - device_barrier() - graphs[round_index % len(graphs)].replay() - torch.accelerator.synchronize() - - timings: dict[str, list[float]] = {name: [] for name in candidate_names} - start = torch.cuda.Event(enable_timing=True) - end = torch.cuda.Event(enable_timing=True) - for sample_index in range(samples): - for candidate_index in range(len(candidate_names)): - candidate_id = (sample_index + candidate_index) % len(candidate_names) - name = candidate_names[candidate_id] - graphs = candidate_graphs[name] - device_barrier() - start.record() - graphs[sample_index % len(graphs)].replay() - end.record() - end.synchronize() - - elapsed = torch.tensor( - start.elapsed_time(end) * 1000, - dtype=torch.float64, - device=torch.accelerator.current_device_index(), - ) - dist.all_reduce(elapsed, op=dist.ReduceOp.MAX, group=device_group) - timings[name].append(elapsed.item()) - return {name: statistics.median(values) for name, values in timings.items()} - - -def valid_rows(M: int, local_M: int, rank: int) -> int: - return min(max(M - rank * local_M, 0), local_M) - - -def benchmark_shape( - gemm_rs_ar: GemmRsAr, - mode: str, - M: int, - N: int, - K: int, - num_workspaces: int, - warmup_replays: int, - samples: int, - device_group: dist.ProcessGroup, - cpu_group: dist.ProcessGroup, - device_barrier: Callable[[], None], -) -> dict[str, float | int | str]: - all_reduce = mode == "ar" - world_size = dist.get_world_size(device_group) - rank = dist.get_rank(device_group) - device = torch.device("cuda", torch.accelerator.current_device_index()) - padded_M = (M + world_size - 1) // world_size * world_size - local_M = padded_M // world_size - - rng = torch.Generator(device=device) - rng.manual_seed(1000 + rank * 10 + M + K) - inputs = [ - torch.randn(M, K, dtype=torch.bfloat16, device=device, generator=rng) - for _ in range(num_workspaces) - ] - weights = [ - torch.randn(N, K, dtype=torch.bfloat16, device=device, generator=rng) - for _ in range(num_workspaces) - ] - - partial = torch.empty((padded_M, N), dtype=torch.bfloat16, device=device) - symm_partial = symm_mem.empty((padded_M, N), dtype=torch.bfloat16, device=device) - symm_partial_handle = symm_mem.rendezvous(symm_partial, device_group) - collective_inputs = [torch.empty_like(partial) for _ in range(num_workspaces)] - symm_collective_inputs = [] - symm_collective_handles = [] - for _ in range(num_workspaces): - collective_input = symm_mem.empty( - (padded_M, N), - dtype=torch.bfloat16, - device=device, - ) - symm_collective_inputs.append(collective_input) - symm_collective_handles.append( - symm_mem.rendezvous(collective_input, device_group) - ) - torch_output = torch.empty((local_M, N), dtype=torch.bfloat16, device=device) - symm_output = torch.empty_like(torch_output) - gemm_output = torch.empty((M, N), dtype=torch.bfloat16, device=device) - - if padded_M > M: - partial[M:].zero_() - symm_partial[M:].zero_() - - def make_torch_ring_ll_gemm_collective( - x: torch.Tensor, weight: torch.Tensor - ) -> Callable[[], torch.Tensor]: - def run() -> torch.Tensor: - torch.mm(x, weight.T, out=partial[:M]) - if all_reduce: - dist.all_reduce(partial, group=device_group) - return partial[:M] - dist.reduce_scatter_single(torch_output, partial, group=device_group) - return torch_output - - return run - - def make_torch_ldmc_gemm_collective( - x: torch.Tensor, weight: torch.Tensor - ) -> Callable[[], torch.Tensor]: - def run() -> torch.Tensor: - torch.mm(x, weight.T, out=symm_partial[:M]) - if all_reduce: - dist.all_reduce(symm_partial, group=device_group) - return symm_partial[:M] - dist.reduce_scatter_single( - symm_output, - symm_partial, - group=device_group, - ) - return symm_output - - return run - - def make_fused_gemm_collective( - x: torch.Tensor, weight: torch.Tensor - ) -> Callable[[], torch.Tensor]: - def run() -> torch.Tensor: - return gemm_rs_ar(x, weight) - - return run - - def make_torch_gemm( - x: torch.Tensor, - weight: torch.Tensor, - ) -> Callable[[], torch.Tensor]: - def run() -> torch.Tensor: - return torch.mm(x, weight.T, out=gemm_output) - - return run - - def make_ring_ll_collective( - collective_input: torch.Tensor, - ) -> Callable[[], torch.Tensor]: - def run() -> torch.Tensor: - if all_reduce: - dist.all_reduce(collective_input, group=device_group) - return collective_input - dist.reduce_scatter_single( - torch_output, collective_input, group=device_group - ) - return torch_output - - return run - - def make_ldmc_collective( - collective_input: torch.Tensor, - ) -> Callable[[], torch.Tensor]: - def run() -> torch.Tensor: - if all_reduce: - dist.all_reduce(collective_input, group=device_group) - return collective_input - dist.reduce_scatter_single( - symm_output, collective_input, group=device_group - ) - return symm_output - - return run - - candidates = ( - Candidate( - "ring_ll_us", - [make_torch_ring_ll_gemm_collective(x, w) for x, w in zip(inputs, weights)], - ), - Candidate( - "ldmc_us", - [make_torch_ldmc_gemm_collective(x, w) for x, w in zip(inputs, weights)], - ), - Candidate( - "gemm_rs_ar_us", - [make_fused_gemm_collective(x, w) for x, w in zip(inputs, weights)], - ), - Candidate( - "torch_gemm_us", - [make_torch_gemm(x, w) for x, w in zip(inputs, weights)], - check_correctness=False, - ), - Candidate( - "ring_ll_collective_us", - [make_ring_ll_collective(x) for x in collective_inputs], - check_correctness=False, - ), - Candidate( - "ldmc_collective_us", - [make_ldmc_collective(x) for x in symm_collective_inputs], - check_correctness=False, - ), - ) - - expected = candidates[0].runs[0]() - rows = M if all_reduce else valid_rows(M, local_M, rank) - for candidate in candidates[1:]: - if not candidate.check_correctness: - continue - actual = candidate.runs[0]() - torch.accelerator.synchronize(device) - torch.testing.assert_close( - actual[:rows], - expected[:rows], - rtol=5e-2, - atol=4.0, - ) - - candidate_graphs = {} - graph_keepalive: list[object] = [symm_partial_handle, *symm_collective_handles] - for candidate in candidates: - stream = torch.cuda.Stream() - bundles = [capture_graph(run, stream, cpu_group) for run in candidate.runs] - candidate_graphs[candidate.name] = [graph for graph, _ in bundles] - graph_keepalive.extend(bundles) - graph_keepalive.append(stream) - - times = benchmark_graphs( - candidate_graphs, - warmup_replays, - samples, - device_group, - device_barrier, - ) - - best_nccl_us = min(times["ring_ll_collective_us"], times["ldmc_collective_us"]) - return { - "mode": mode.upper(), - "M": M, - "N": N, - "K": K, - **times, - "best_nccl_us": best_nccl_us, - "speedup_vs_ring_ll": times["ring_ll_us"] / times["gemm_rs_ar_us"], - "speedup_vs_ldmc": times["ldmc_us"] / times["gemm_rs_ar_us"], - } - - -def print_results(results: list[dict[str, float | int | str]]) -> None: - results_df = pd.DataFrame(results) - collective = str(results_df["mode"].iloc[0]) - end_to_end = ( - results_df[ - [ - "M", - "N", - "K", - "ring_ll_us", - "ldmc_us", - "gemm_rs_ar_us", - "speedup_vs_ring_ll", - "speedup_vs_ldmc", - ] - ] - .rename( - columns={ - "ring_ll_us": f"Torch GEMM + NCCL {collective} (RING_LL) (us)", - "ldmc_us": f"Torch GEMM + NCCL {collective} (LDMC) (us)", - "gemm_rs_ar_us": f"GEMM-{collective} (us)", - "speedup_vs_ring_ll": "Speedup vs RING_LL", - "speedup_vs_ldmc": "Speedup vs LDMC", - } - ) - .round(3) - ) - components = ( - results_df[["M", "N", "K", "torch_gemm_us", "best_nccl_us", "gemm_rs_ar_us"]] - .rename( - columns={ - "torch_gemm_us": "Torch GEMM (us)", - "best_nccl_us": f"NCCL {collective} (best) (us)", - "gemm_rs_ar_us": f"GEMM-{collective} (us)", - } - ) - .round(2) - ) - - print(f"### {collective} end-to-end latency") - print(end_to_end.to_markdown(index=False)) - print(f"\n### {collective} component latency") - print(components.to_markdown(index=False)) - print( - f"\nNCCL {collective} (best) is the faster of RING_LL and LDMC " - "for each shape.\n" - ) - - -def main() -> None: - args = parse_args() - assert args.m and min(args.m) > 0 - assert args.n % 256 == 0 - assert args.num_workspaces > 0 - assert args.warmup_replays >= 0 - assert args.samples > 0 - - local_rank = int(os.environ["LOCAL_RANK"]) - torch.accelerator.set_device_index(local_rank) - init_distributed_environment() - world_size = dist.get_world_size() - if args.k is None: - assert all(K % world_size == 0 for K in _KIMI_K3_PROJECTION_K) - K_values = [K // world_size for K in _KIMI_K3_PROJECTION_K] - else: - K_values = args.k - assert all(K % 64 == 0 for K in K_values) - # Reserve symmetric memory for the NCCL-managed benchmark allocations. - os.environ["VLLM_ALLREDUCE_USE_SYMM_MEM"] = "0" - # NCCL-managed symmetric allocations select the NVLS/LDMC collective path. - symm_mem.set_backend("NCCL") - with set_current_vllm_config(VllmConfig()): - initialize_model_parallel(tensor_model_parallel_size=world_size) - - tp_group = get_tp_group() - group_warmup = torch.zeros(1, device=torch.accelerator.current_device_index()) - dist.all_reduce(group_warmup, group=tp_group.device_group) - pynccl_comm = tp_group.device_communicator.pynccl_comm - assert pynccl_comm is not None - sync_input = torch.zeros(1, device=torch.accelerator.current_device_index()) - sync_output = torch.empty_like(sync_input) - - def device_barrier() -> None: - # Order the timed launch after a device-side rank rendezvous without - # including the rendezvous itself in the measured event interval. - pynccl_comm.all_reduce(sync_input, sync_output) - - gemm_rs_ar = GemmRsAr( - max_M=max(args.m), - N=args.n, - all_reduce=args.mode == "ar", - ) - results = [ - benchmark_shape( - gemm_rs_ar, - args.mode, - M, - args.n, - K, - args.num_workspaces, - args.warmup_replays, - args.samples, - tp_group.device_group, - tp_group.cpu_group, - device_barrier, - ) - for K in K_values - for M in args.m - ] - del gemm_rs_ar - - if tp_group.rank_in_group == 0: - print_results(results) - - dist.barrier(group=tp_group.cpu_group) - cleanup_dist_env_and_memory() - - -if __name__ == "__main__": - main() diff --git a/benchmarks/kernels/benchmark_kimi_k3_kda_decode.py b/benchmarks/kernels/benchmark_kimi_k3_kda_decode.py index fcce64015256..6b6d944455e9 100644 --- a/benchmarks/kernels/benchmark_kimi_k3_kda_decode.py +++ b/benchmarks/kernels/benchmark_kimi_k3_kda_decode.py @@ -16,6 +16,7 @@ Example: python benchmarks/kernels/benchmark_kimi_k3_kda_decode.py \ --tokens 1 8 32 64 128 --heads 12 + """ import argparse diff --git a/benchmarks/kernels/benchmark_kimi_k3_latent_moe_tail.py b/benchmarks/kernels/benchmark_kimi_k3_latent_moe_tail.py index 2669dadb348d..855769263d84 100644 --- a/benchmarks/kernels/benchmark_kimi_k3_latent_moe_tail.py +++ b/benchmarks/kernels/benchmark_kimi_k3_latent_moe_tail.py @@ -27,6 +27,7 @@ For multi-node runs, launch one ``torchrun`` agent per node and use a shared rendezvous endpoint. + """ from __future__ import annotations diff --git a/benchmarks/kernels/benchmark_lora.py b/benchmarks/kernels/benchmark_lora.py index ab930c59d219..606741d0474e 100644 --- a/benchmarks/kernels/benchmark_lora.py +++ b/benchmarks/kernels/benchmark_lora.py @@ -102,9 +102,7 @@ def make_rand_tensors( num_slices: int, device: str = "cuda", ) -> tuple[torch.Tensor, list[torch.Tensor], torch.Tensor]: - """ - Make LoRA input/output matrices. - """ + """Make LoRA input/output matrices.""" A = torch.rand(a_shape, dtype=a_dtype).to(device) # LoRA weights column major @@ -117,8 +115,7 @@ def make_rand_tensors( def make_prompt_lora_mapping( num_prompts: int, num_active_loras: int, sort_by_lora_id: bool, device: str ) -> torch.Tensor: - """ - All prompts are mapped to a LoRA ID in range [0, num_active_loras). + """All prompts are mapped to a LoRA ID in range [0, num_active_loras). where 0 refers to first lora, 1 refers to second lora and so on. """ assert num_active_loras > 0 @@ -147,9 +144,7 @@ def make_token_lora_mapping( seq_len_tensor: torch.Tensor, device: str, ): - """ - Make token_lora_mapping from prompt_lora_mapping and seq_lens_tensor - """ + """Make token_lora_mapping from prompt_lora_mapping and seq_lens_tensor.""" assert prompt_lora_mapping.shape[0] == num_prompts # token to lora index mapping @@ -174,8 +169,7 @@ def ref_group_gemm( scaling: float, add_inputs: bool | None, ): - """ - Torch group gemm reference implementation to test correctness of + """Torch group gemm reference implementation to test correctness of benchmarking operations. """ batches = seq_lens_cpu.size(0) @@ -198,9 +192,7 @@ def ref_group_gemm( class OpType(Enum): - """ - LoRA Ops to benchmark and its properties. - """ + """LoRA Ops to benchmark and its properties.""" LORA_SHRINK = auto() LORA_EXPAND = auto() @@ -291,9 +283,7 @@ def mkn( def matmul_dtypes( self, op_dtype: torch.dtype ) -> tuple[torch.dtype, torch.dtype, torch.dtype]: - """ - return a type, b type and c type for A x B = C - """ + """Return a type, b type and c type for A x B = C.""" if self.is_shrink_fn(): return op_dtype, op_dtype, torch.float32 elif self.is_expand_fn(): @@ -338,8 +328,7 @@ def matmul_shapes( top_k_num: int | None = None, num_experts: int | None = None, ) -> tuple[tuple[int, ...], tuple[int, ...], tuple[int, ...]]: - """ - Given num_slices, return the shapes of the A, B, and C matrices + """Given num_slices, return the shapes of the A, B, and C matrices in A x B = C, for the op_type """ m, k, n = self.mkn(batch_size, seq_length, hidden_size, lora_rank) @@ -419,9 +408,7 @@ def run_ref_group_gemm( @dataclass class BenchmarkContext: - """ - LoRA benchmark context - """ + """LoRA benchmark context.""" batch_size: int hidden_size: int @@ -467,9 +454,7 @@ def bench_sublabel(self, op_type: OpType) -> str: @dataclass class BenchmarkTensors: - """ - Input/Output tensors used for benchmarks - """ + """Input/Output tensors used for benchmarks.""" # matmul tensors input: torch.Tensor @@ -555,9 +540,7 @@ def make( ) def sanity_check(self, ctx: BenchmarkContext, op_type: OpType) -> None: - """ - Fails asserts when non-conformality is detected. - """ + """Fails asserts when non-conformality is detected.""" num_tokens = ( self.input.shape[1] if op_type.is_fused_moe_lora_expand_fn() @@ -577,9 +560,7 @@ def sanity_check(self, ctx: BenchmarkContext, op_type: OpType) -> None: ) def to_device(self, device: str): - """ - Transfer tensors to device if the tensors aren't already on the device - """ + """Transfer tensors to device if the tensors aren't already on the device.""" def to_device(tensor: torch.Tensor): if tensor.device != device: @@ -604,9 +585,7 @@ def to_device(tensor: torch.Tensor): ) def metadata(self, ctx: BenchmarkContext, op_type: OpType) -> tuple[int, int, int]: - """ - Return num_seqs, num_tokens and max_seq_len - """ + """Return num_seqs, num_tokens and max_seq_len.""" num_seqs = self.seq_lens.shape[0] num_tokens = self.get_num_tokens( self.lora_kernel_meta.token_lora_mapping.shape[0], ctx.top_k_num, op_type @@ -630,8 +609,7 @@ def moe_lora_align_block_size( expert_map: torch.Tensor | None = None, pad_sorted_ids: bool = False, ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: - """ - Aligns tokens and experts into block-sized chunks for LoRA-based + """Aligns tokens and experts into block-sized chunks for LoRA-based mixture-of-experts (MoE) execution. """ max_num_tokens_padded = topk_ids.numel() + num_experts * (block_size - 1) @@ -959,8 +937,7 @@ def bench_fn_kwargs( def test_correctness( self, op_type: OpType, expand_fn_add_inputs: bool | None ) -> bool: - """ - Test correctness of op_type implementation against a grouped gemm + """Test correctness of op_type implementation against a grouped gemm reference implementation. """ seq_lens_cpu = self.seq_lens.to(device="cpu") @@ -1070,15 +1047,13 @@ def bench_torch_mm( op_type: OpType, cuda_graph_nops: int | None = None, ) -> TMeasurement: - """ - Benchmark basic torch.mm as a roofline. + """Benchmark basic torch.mm as a roofline. When all the input tokens have the same LoRA ID, the LoRA kernels are just a matmul. This torch.mm benchmark serves as a roofline for that case. input op_type is used in determining the m, k, n dimensions for the matmul. """ - batch_size, hidden_size, lora_rank, seq_length, dtype = ( ctx.batch_size, ctx.hidden_size, diff --git a/benchmarks/kernels/benchmark_marlin.py b/benchmarks/kernels/benchmark_marlin.py index 751211993ad3..f2330a4ff0f0 100644 --- a/benchmarks/kernels/benchmark_marlin.py +++ b/benchmarks/kernels/benchmark_marlin.py @@ -6,10 +6,6 @@ from benchmark_shapes import WEIGHT_SHAPES from vllm import _custom_ops as ops -from vllm.model_executor.layers.quantization.utils.allspark_utils import ( - ALLSPARK_AMPERE_M_CUBLAS_THRESHOLD, - ALLSPARK_SUPPORTED_QUANT_TYPES, -) from vllm.model_executor.layers.quantization.utils.marlin_utils import ( GPTQ_MARLIN_MAX_PARALLEL, GPTQ_MARLIN_MIN_THREAD_N, @@ -31,7 +27,6 @@ from vllm.model_executor.layers.quantization.utils.quant_utils import ( gptq_pack, gptq_quantize_weights, - quantize_weights, ) from vllm.scalar_type import ScalarType, scalar_types from vllm.utils.argparse_utils import FlexibleArgumentParser @@ -62,9 +57,6 @@ def bench_run( return repack_supported = group_size in MARLIN_SUPPORTED_GROUP_SIZES - allspark_supported = ( - quant_type in ALLSPARK_SUPPORTED_QUANT_TYPES and group_size == -1 - ) def gen_marlin_params(): # Marlin quant @@ -104,35 +96,6 @@ def gen_repack_params(): q_w_gptq = gptq_pack(q_w, quant_type.size_bits, size_k, size_n) return q_w_gptq - def gen_allspark_params(): - qw_reorder = s_reorder = zp_reorder = sm_count = sm_version = ( - CUBLAS_M_THRESHOLD - ) = None - nonlocal allspark_supported - if allspark_supported: - properties = torch.cuda.get_device_properties(b.device.index) - sm_count = properties.multi_processor_count - sm_version = properties.major * 10 + properties.minor - - supported_arch = sm_version >= 80 and sm_version < 90 - allspark_supported = allspark_supported and supported_arch - if supported_arch: - w_ref, qw, s, zp = quantize_weights(b, quant_type, group_size, has_zp) - qw = qw.to(torch.uint8) - - qw_reorder, s_reorder, zp_reorder = ops.allspark_repack_weight( - qw, s, zp, has_zp - ) - CUBLAS_M_THRESHOLD = ALLSPARK_AMPERE_M_CUBLAS_THRESHOLD - return ( - qw_reorder, - s_reorder, - zp_reorder, - sm_count, - sm_version, - CUBLAS_M_THRESHOLD, - ) - ( marlin_w_ref, marlin_q_w, @@ -141,9 +104,6 @@ def gen_allspark_params(): marlin_zp, ) = gen_marlin_params() q_w_gptq = gen_repack_params() - qw_reorder, s_reorder, zp_reorder, sm_count, sm_version, CUBLAS_M_THRESHOLD = ( - gen_allspark_params() - ) # Prepare marlin_workspace = MarlinWorkspace( @@ -167,17 +127,9 @@ def gen_allspark_params(): "marlin_workspace": marlin_workspace, # GPTQ params "q_w_gptq": q_w_gptq, - # AllSpark W8A16 params - "qw_reorder": qw_reorder, - "s_reorder": s_reorder, - "zp_reorder": zp_reorder, - "sm_count": sm_count, - "sm_version": sm_version, - "CUBLAS_M_THRESHOLD": CUBLAS_M_THRESHOLD, # Kernels "marlin_gemm": ops.marlin_gemm, "gptq_marlin_repack": ops.gptq_marlin_repack, - "allspark_w8a16_gemm": ops.allspark_w8a16_gemm, } min_run_time = 1 @@ -227,17 +179,6 @@ def gen_allspark_params(): ).blocked_autorange(min_run_time=min_run_time) ) - if allspark_supported: - results.append( - benchmark.Timer( - stmt="output = allspark_w8a16_gemm(a, qw_reorder, s_reorder, zp_reorder, size_n, group_size, sm_count, sm_version, CUBLAS_M_THRESHOLD, False, True)", # noqa: E501 - globals=globals, - label=label, - sub_label=sub_label, - description="allspark_w8a16_gemm_fp32", - ).blocked_autorange(min_run_time=min_run_time) - ) - def main(args): print("Benchmarking models:") diff --git a/benchmarks/kernels/benchmark_mla_k_concat.py b/benchmarks/kernels/benchmark_mla_k_concat.py index 7debf3634804..3cae613e82c7 100644 --- a/benchmarks/kernels/benchmark_mla_k_concat.py +++ b/benchmarks/kernels/benchmark_mla_k_concat.py @@ -1,7 +1,6 @@ # SPDX-License-Identifier: Apache-2.0 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project -""" -Benchmark script comparing torch.cat vs direct copy for k_nope/k_pe concatenation +"""Benchmark script comparing torch.cat vs direct copy for k_nope/k_pe concatenation in MLA (Multi-head Latent Attention) prefill. This validates that the optimization from commit 8d4142bd is beneficial across diff --git a/benchmarks/kernels/benchmark_moe_align_block_size.py b/benchmarks/kernels/benchmark_moe_align_block_size.py index a340500379a0..7ddf5b3ecf7c 100644 --- a/benchmarks/kernels/benchmark_moe_align_block_size.py +++ b/benchmarks/kernels/benchmark_moe_align_block_size.py @@ -5,6 +5,7 @@ import torch +import vllm._custom_ops as ops from vllm.model_executor.layers.fused_moe.moe_align_block_size import ( moe_align_block_size, ) @@ -68,8 +69,87 @@ def benchmark(num_tokens, num_experts, topk, ep_size, provider): return 1000 * ms, 1000 * max_ms, 1000 * min_ms +batched_configs = list( + itertools.product( + [8, 32, 64, 256], + [16, 256, 512, 2048], + [8, 32, 48, 64], + ["uniform", "skewed", "mostly_empty"], + ) +) + + +@triton.testing.perf_report( + triton.testing.Benchmark( + x_names=[ + "num_batches", + "max_tokens_per_batch", + "block_size", + "distribution", + ], + x_vals=batched_configs, + line_arg="provider", + line_vals=["vllm"], + line_names=["vLLM"], + plot_name="batched-moe-align-block-size-performance", + args={}, + ) +) +def benchmark_batched( + num_batches, + max_tokens_per_batch, + block_size, + distribution, + provider, +): + set_random_seed(0) + if distribution == "uniform": + expert_num_tokens = torch.full( + (num_batches,), + max_tokens_per_batch, + dtype=torch.int32, + device="cuda", + ) + elif distribution == "skewed": + expert_num_tokens = torch.ones((num_batches,), dtype=torch.int32, device="cuda") + expert_num_tokens[0] = max_tokens_per_batch + else: + expert_num_tokens = torch.zeros( + (num_batches,), dtype=torch.int32, device="cuda" + ) + expert_num_tokens[::8] = max_tokens_per_batch + + max_num_tokens_padded = ( + num_batches + * ((max_tokens_per_batch + block_size - 1) // block_size) + * block_size + ) + sorted_ids = torch.empty((max_num_tokens_padded,), dtype=torch.int32, device="cuda") + expert_ids = torch.empty( + (max_num_tokens_padded // block_size,), dtype=torch.int32, device="cuda" + ) + num_tokens_post_pad = torch.empty((1,), dtype=torch.int32, device="cuda") + + quantiles = [0.5, 0.2, 0.8] + if provider == "vllm": + ms, min_ms, max_ms = triton.testing.do_bench( + lambda: ops.batched_moe_align_block_size( + max_tokens_per_batch, + block_size, + expert_num_tokens, + sorted_ids, + expert_ids, + num_tokens_post_pad, + ), + quantiles=quantiles, + ) + + return 1000 * ms, 1000 * max_ms, 1000 * min_ms + + if __name__ == "__main__": parser = argparse.ArgumentParser() + parser.add_argument("--batched", action="store_true") parser.add_argument( "--num_experts", type=int, @@ -85,4 +165,5 @@ def benchmark(num_tokens, num_experts, topk, ep_size, provider): ) args = parser.parse_args() - benchmark.run(print_data=True, show_plots=True) + selected_benchmark = benchmark_batched if args.batched else benchmark + selected_benchmark.run(print_data=True, show_plots=True) diff --git a/benchmarks/kernels/benchmark_moe_defaults.py b/benchmarks/kernels/benchmark_moe_defaults.py index f6ad59366dca..00c6606db009 100644 --- a/benchmarks/kernels/benchmark_moe_defaults.py +++ b/benchmarks/kernels/benchmark_moe_defaults.py @@ -1,8 +1,7 @@ #!/usr/bin/env python3 # SPDX-License-Identifier: Apache-2.0 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project -""" -Benchmark comparing old vs new default fused MoE configs. +"""Benchmark comparing old vs new default fused MoE configs. Runs the triton fused_moe kernel with three configurations for each scenario: 1. Tuned config (from JSON file, if available) — the target to match diff --git a/benchmarks/kernels/benchmark_per_token_group_quant.py b/benchmarks/kernels/benchmark_per_token_group_quant.py index f2195a6d780b..fda3d2a652b4 100644 --- a/benchmarks/kernels/benchmark_per_token_group_quant.py +++ b/benchmarks/kernels/benchmark_per_token_group_quant.py @@ -15,7 +15,7 @@ @contextmanager def _triton_mode(): - """Temporarily force the Triton fallback path""" + """Temporarily force the Triton fallback path.""" with patch("vllm.platforms.current_platform.is_cuda", return_value=False): yield diff --git a/benchmarks/kernels/benchmark_per_token_quant_fp8.py b/benchmarks/kernels/benchmark_per_token_quant_fp8.py index 6ce97e30368b..0069b06c6456 100644 --- a/benchmarks/kernels/benchmark_per_token_quant_fp8.py +++ b/benchmarks/kernels/benchmark_per_token_quant_fp8.py @@ -16,7 +16,7 @@ def with_triton_mode(fn): - """Temporarily force the Triton fallback path""" + """Temporarily force the Triton fallback path.""" def wrapped(*args, **kwargs): with patch("vllm.platforms.current_platform.is_cuda", return_value=False): @@ -124,8 +124,7 @@ def compute_geomean_speedups( speedup_cols: list[str], groupby_cols: list[str] | None = None, ) -> pd.DataFrame: - """ - Compute geometric mean speedups over a baseline column. + """Compute geometric mean speedups over a baseline column. Args: df: Input dataframe @@ -135,6 +134,7 @@ def compute_geomean_speedups( Returns: pd.DataFrame with geometric mean speedups + """ from scipy.stats import gmean diff --git a/benchmarks/kernels/benchmark_persistent_topk.py b/benchmarks/kernels/benchmark_persistent_topk.py new file mode 100644 index 000000000000..a77a04cfd625 --- /dev/null +++ b/benchmarks/kernels/benchmark_persistent_topk.py @@ -0,0 +1,134 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +"""Benchmark sparse-indexer decode top-k on variable-length FP32 rows. + +Run on a reserved GPU, for example: + chg run -- .venv/bin/python benchmarks/kernels/benchmark_persistent_topk.py + +Use --capacity to emulate graphs captured at a larger maximum context length. +Run the same command on the base and candidate builds to compare JSON results. +""" + +import argparse +import itertools +import json +import statistics +from pathlib import Path + +import torch +from flashinfer.testing import bench_gpu_time_with_cupti + +import vllm._custom_ops # noqa: F401 +from vllm.platforms import current_platform + + +def benchmark(rows: int, width: int, k: int, args) -> dict: + capacity = args.capacity or width + if capacity < width: + raise ValueError("Capacity must be at least the valid-length upper bound") + torch.manual_seed(args.seed) + logits = torch.randn(rows, capacity, device="cuda", dtype=torch.float32) + if args.full_length: + lengths = torch.full((rows,), width, device="cuda", dtype=torch.int32) + else: + lengths = torch.randint( + int(width * 0.8), width + 1, (rows,), device="cuda", dtype=torch.int32 + ) + invalid = torch.arange(capacity, device="cuda")[None] >= lengths[:, None] + logits.masked_fill_(invalid, float("nan")) + out = torch.empty((rows, k), device="cuda", dtype=torch.int32) + workspace = torch.empty(1024 * 1024, device="cuda", dtype=torch.uint8) + cooperative = ( + args.backend == "auto" + and rows <= 64 + and capacity % 4 == 0 + and current_platform.has_device_capability(90) + and not current_platform.is_device_capability_family(120) + ) + op = torch.ops._C.cooperative_topk if cooperative else torch.ops._C.persistent_topk + + def run(): + op(logits, lengths, out, workspace, k, capacity) + + run() + valid = torch.arange(k, device="cuda")[None] < lengths[:, None] + assert torch.all(out[~valid] == -1) + assert torch.all(((out >= 0) & (out < lengths[:, None])) == valid) + sorted_indices = out.sort(dim=1).values + assert torch.all( + (sorted_indices[:, 1:] != sorted_indices[:, :-1]) + | (sorted_indices[:, 1:] == -1) + ) + selected = logits.gather(1, out.clamp_min(0).long()).masked_fill( + ~valid, -float("inf") + ) + reference = logits.masked_fill(invalid, -float("inf")).topk(k, dim=1).values + torch.testing.assert_close( + selected.sort(dim=1, descending=True).values, reference, atol=0, rtol=0 + ) + trials = [ + statistics.median( + bench_gpu_time_with_cupti( + run, + use_cuda_graph=True, + cold_l2_cache=not args.warm, + dry_run_iters=5, + repeat_iters=args.repeat, + ) + ) + * 1000 + for _ in range(args.trials) + ] + us = statistics.median(trials) + return dict( + rows=rows, + width=width, + capacity=capacity, + k=k, + backend="cooperative" if cooperative else "persistent", + us=us, + trials_us=trials, + # Minimum useful traffic: one valid-score read and one index write. + effective_gbps=(int(lengths.sum()) * 4 + out.numel() * 4) / (us * 1000), + ) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--rows", type=int, nargs="+", default=[1, 32, 128, 256, 1024]) + parser.add_argument("--widths", type=int, nargs="+", default=[8192, 65536, 163840]) + parser.add_argument("--top-k", type=int, nargs="+", default=[512, 2048]) + parser.add_argument("--capacity", type=int) + parser.add_argument( + "--full-length", action="store_true", help="Use exactly --widths valid scores" + ) + parser.add_argument("--backend", choices=["auto", "persistent"], default="auto") + parser.add_argument( + "--warm", action="store_true", help="Keep L2 warm between replays" + ) + parser.add_argument("--repeat", type=int, default=60) + parser.add_argument("--trials", type=int, default=3) + parser.add_argument("--seed", type=int, default=123) + parser.add_argument("--output", type=Path) + args = parser.parse_args() + metadata = dict( + gpu=str(torch.cuda.get_device_properties(0)), + torch=torch.__version__, + cuda=torch.version.cuda, + args=vars(args), + ) + print(json.dumps(metadata, default=str)) + results = [] + for rows, width, k in itertools.product(args.rows, args.widths, args.top_k): + result = benchmark(rows, width, k, args) + results.append(result) + print(json.dumps(result), flush=True) + if args.output: + args.output.write_text( + json.dumps(dict(metadata=metadata, results=results), indent=2, default=str) + + "\n" + ) + + +if __name__ == "__main__": + main() diff --git a/benchmarks/kernels/benchmark_rdna_hybrid_w4a16_gemm.py b/benchmarks/kernels/benchmark_rdna_hybrid_w4a16_gemm.py index 4ecd917f31ac..2d5ec80c6aae 100644 --- a/benchmarks/kernels/benchmark_rdna_hybrid_w4a16_gemm.py +++ b/benchmarks/kernels/benchmark_rdna_hybrid_w4a16_gemm.py @@ -1,7 +1,6 @@ # SPDX-License-Identifier: Apache-2.0 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project -""" -Benchmark the RDNAHybridW4A16LinearKernel across decode and prefill shapes. +"""Benchmark the RDNAHybridW4A16LinearKernel across decode and prefill shapes. Usage: python benchmark_int4_gemm.py diff --git a/benchmarks/kernels/benchmark_reshape_and_cache.py b/benchmarks/kernels/benchmark_reshape_and_cache.py index 97af4ac976ee..f49bf61036d0 100644 --- a/benchmarks/kernels/benchmark_reshape_and_cache.py +++ b/benchmarks/kernels/benchmark_reshape_and_cache.py @@ -32,7 +32,6 @@ 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.") diff --git a/benchmarks/kernels/benchmark_reshape_and_cache_flash.py b/benchmarks/kernels/benchmark_reshape_and_cache_flash.py index 55c203725186..84d9cc707c40 100644 --- a/benchmarks/kernels/benchmark_reshape_and_cache_flash.py +++ b/benchmarks/kernels/benchmark_reshape_and_cache_flash.py @@ -37,7 +37,6 @@ 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.") diff --git a/benchmarks/kernels/benchmark_rmsnorm.py b/benchmarks/kernels/benchmark_rmsnorm.py deleted file mode 100644 index d8d7f5bcf9da..000000000000 --- a/benchmarks/kernels/benchmark_rmsnorm.py +++ /dev/null @@ -1,255 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 -# SPDX-FileCopyrightText: Copyright contributors to the vLLM project - -import itertools - -import torch -from flashinfer.norm import fused_add_rmsnorm, rmsnorm -from torch import nn - -from vllm import _custom_ops as vllm_ops -from vllm.triton_utils import triton - - -class HuggingFaceRMSNorm(nn.Module): - def __init__(self, hidden_size: int, eps: float = 1e-6) -> None: - super().__init__() - self.weight = nn.Parameter(torch.ones(hidden_size)) - self.variance_epsilon = eps - - def forward( - self, - x: torch.Tensor, - residual: torch.Tensor | None = None, - ) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]: - orig_dtype = x.dtype - x = x.to(torch.float32) - if residual is not None: - x = x + residual.to(torch.float32) - residual = x.to(orig_dtype) - - variance = x.pow(2).mean(dim=-1, keepdim=True) - x = x * torch.rsqrt(variance + self.variance_epsilon) - x = x.to(orig_dtype) * self.weight - if residual is None: - return x - else: - return x, residual - - -def rmsnorm_naive( - x: torch.Tensor, - weight: torch.Tensor, - residual: torch.Tensor | None = None, - eps: float = 1e-6, -): - naive_norm = HuggingFaceRMSNorm(x.shape[-1], eps=eps) - naive_norm.weight = nn.Parameter(weight) - naive_norm = naive_norm.to(x.device) - - orig_shape = x.shape - x = x.view(-1, x.shape[-1]) - if residual is not None: - residual = residual.view(-1, residual.shape[-1]) - - output = naive_norm(x, residual) - - if isinstance(output, tuple): - output = (output[0].view(orig_shape), output[1].view(orig_shape)) - else: - output = output.view(orig_shape) - return output - - -def rmsnorm_flashinfer( - x: torch.Tensor, - weight: torch.Tensor, - residual: torch.Tensor | None = None, - eps: float = 1e-6, -): - orig_shape = x.shape - x = x.view(-1, x.shape[-1]) - if residual is not None: - residual = residual.view(-1, residual.shape[-1]) - - if residual is not None: - fused_add_rmsnorm(x, residual, weight, eps) - output = (x, residual) - else: - output = rmsnorm(x, weight, eps) - - if isinstance(output, tuple): - output = (output[0].view(orig_shape), output[1].view(orig_shape)) - else: - output = output.view(orig_shape) - return output - - -def rmsnorm_vllm( - x: torch.Tensor, - weight: torch.Tensor, - residual: torch.Tensor | None = None, - eps: float = 1e-6, -): - orig_shape = x.shape - x = x.view(-1, x.shape[-1]) - if residual is not None: - residual = residual.view(-1, residual.shape[-1]) - - if residual is not None: - vllm_ops.fused_add_rms_norm(x, residual, weight, eps) - output = (x, residual) - else: - out = torch.empty_like(x) - vllm_ops.rms_norm(out, x, weight, eps) - output = out - - if isinstance(output, tuple): - output = (output[0].view(orig_shape), output[1].view(orig_shape)) - else: - output = output.view(orig_shape) - return output - - -def calculate_diff(batch_size, seq_len, hidden_size, use_residual=True): - dtype = torch.bfloat16 - x = torch.randn(batch_size, seq_len, hidden_size, dtype=dtype, device="cuda") - weight = torch.ones(hidden_size, dtype=dtype, device="cuda") - residual = torch.randn_like(x) if use_residual else None - - output_naive = rmsnorm_naive( - x.clone(), weight, residual.clone() if residual is not None else None - ) - output_flashinfer = rmsnorm_flashinfer( - x.clone(), weight, residual.clone() if residual is not None else None - ) - output_vllm = rmsnorm_vllm( - x.clone(), weight, residual.clone() if residual is not None else None - ) - - if use_residual: - output_naive = output_naive[0] - output_flashinfer = output_flashinfer[0] - output_vllm = output_vllm[0] - - print(f"Naive output={output_naive}") - print(f"FlashInfer output={output_flashinfer}") - print(f"vLLM output={output_vllm}") - - if torch.allclose( - output_naive, output_flashinfer, atol=1e-2, rtol=1e-2 - ) and torch.allclose(output_naive, output_vllm, atol=1e-2, rtol=1e-2): - print("✅ All implementations match") - else: - print("❌ Implementations differ") - - -batch_size_range = [2**i for i in range(0, 7, 2)] -seq_length_range = [2**i for i in range(6, 11, 1)] -head_num_range = [32, 48] -configs = list(itertools.product(head_num_range, batch_size_range, seq_length_range)) - - -def get_benchmark(use_residual): - @triton.testing.perf_report( - triton.testing.Benchmark( - x_names=["head_num", "batch_size", "seq_len"], - x_vals=[list(_) for _ in configs], - line_arg="provider", - line_vals=["huggingface", "flashinfer", "vllm"], - line_names=["HuggingFace", "FlashInfer", "vLLM"], - styles=[("blue", "-"), ("green", "-"), ("red", "-")], - ylabel="us", - plot_name=f"rmsnorm-perf-{'with' if use_residual else 'without'}-residual", - args={}, - ) - ) - def benchmark(head_num, batch_size, seq_len, provider): - dtype = torch.bfloat16 - hidden_size = head_num * 128 # assuming head_dim = 128 - - x = torch.randn(batch_size, seq_len, hidden_size, dtype=dtype, device="cuda") - weight = torch.ones(hidden_size, dtype=dtype, device="cuda") - residual = torch.randn_like(x) if use_residual else None - - quantiles = [0.5, 0.2, 0.8] - - if provider == "huggingface": - ms, min_ms, max_ms = triton.testing.do_bench( - lambda: rmsnorm_naive( - x.clone(), - weight, - residual.clone() if residual is not None else None, - ), - quantiles=quantiles, - ) - elif provider == "flashinfer": - ms, min_ms, max_ms = triton.testing.do_bench( - lambda: rmsnorm_flashinfer( - x.clone(), - weight, - residual.clone() if residual is not None else None, - ), - quantiles=quantiles, - ) - else: - ms, min_ms, max_ms = triton.testing.do_bench( - lambda: rmsnorm_vllm( - x.clone(), - weight, - residual.clone() if residual is not None else None, - ), - quantiles=quantiles, - ) - - return 1000 * ms, 1000 * max_ms, 1000 * min_ms - - return benchmark - - -if __name__ == "__main__": - import argparse - - parser = argparse.ArgumentParser() - parser.add_argument( - "--batch-size", - type=int, - default=4, - help="Batch size", - ) - parser.add_argument( - "--seq-len", - type=int, - default=128, - help="Sequence length", - ) - parser.add_argument( - "--hidden-size", - type=int, - default=4096, - help="Hidden size (2nd dimension) of the sequence", - ) - parser.add_argument( - "--use-residual", action="store_true", help="Whether to use residual connection" - ) - parser.add_argument( - "--save-path", - type=str, - default="./configs/rmsnorm/", - help="Path to save rmsnorm benchmark results", - ) - - args = parser.parse_args() - - # Run correctness test - calculate_diff( - batch_size=args.batch_size, - seq_len=args.seq_len, - hidden_size=args.hidden_size, - use_residual=args.use_residual, - ) - - # Get the benchmark function with proper use_residual setting - benchmark = get_benchmark(args.use_residual) - # Run performance benchmark - benchmark.run(print_data=True, save_path=args.save_path) diff --git a/benchmarks/kernels/benchmark_selective_state_update.py b/benchmarks/kernels/benchmark_selective_state_update.py index ddf1efd018c8..476dce7dff8d 100644 --- a/benchmarks/kernels/benchmark_selective_state_update.py +++ b/benchmarks/kernels/benchmark_selective_state_update.py @@ -1,8 +1,7 @@ #!/usr/bin/env python3 # SPDX-License-Identifier: Apache-2.0 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project -""" -Benchmark and tuning script for the Mamba selective_state_update kernel. +"""Benchmark and tuning script for the Mamba selective_state_update kernel. Mirrors the fused MoE tuning workflow: sweeps (BLOCK_SIZE_M, num_warps) across an effective_batch grid for a given (headdim, dstate, ngroups, cache_dtype) and @@ -124,8 +123,7 @@ def benchmark_config( num_warmup: int = 20, graph_batch_size: int = 10, ) -> float | None: - """ - Time one (BLOCK_SIZE_M, num_warps) config for selective_state_update. + """Time one (BLOCK_SIZE_M, num_warps) config for selective_state_update. Returns elapsed time in microseconds, or None on error. Uses accelerator graph capture-and-replay to isolate kernel time from @@ -356,8 +354,7 @@ def validate_configs( rtol: float = 1e-2, state_dtype: torch.dtype | None = None, ) -> dict[int, bool]: - """ - For every (effective_batch, batch, nheads) in *active* that has a tuned + """For every (effective_batch, batch, nheads) in *active* that has a tuned config, run the kernel with that config and compare against the reference. Returns {effective_batch: passed}. """ diff --git a/benchmarks/kernels/benchmark_silu_mul_fp8_quant.py b/benchmarks/kernels/benchmark_silu_mul_fp8_quant.py index 9fcf278f2ef3..22ef378b0763 100644 --- a/benchmarks/kernels/benchmark_silu_mul_fp8_quant.py +++ b/benchmarks/kernels/benchmark_silu_mul_fp8_quant.py @@ -1,8 +1,7 @@ # SPDX-License-Identifier: Apache-2.0 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project -""" -Comprehensive 3-way SiLU Benchmark Suite +"""Comprehensive 3-way SiLU Benchmark Suite. This benchmark compares three SiLU implementations: 1. SiLU V2 (CUDA) - Optimized CUDA kernel implementation @@ -110,7 +109,7 @@ def silu_mul_fp8_quant_deep_gemm_triton( eps: float = 1e-10, expert_offsets: torch.Tensor = None, ) -> tuple[torch.Tensor, torch.Tensor]: - """Quantize silu(y[..., :H]) * y[..., H:] to FP8 with group per-token scales + """Quantize silu(y[..., :H]) * y[..., H:] to FP8 with group per-token scales. y has shape (E, T, 2*H). The first half of the last dimension is silu-activated, multiplied by the second half, then quantized into FP8. @@ -206,7 +205,7 @@ def benchmark( iterations_per_run: int = 20, ): def generate_data(seed_offset=0): - """Generate input data with given seed offset""" + """Generate input data with given seed offset.""" set_random_seed(42 + seed_offset) y = torch.rand((E, T, 2 * H), dtype=torch.bfloat16, device="cuda").contiguous() diff --git a/benchmarks/kernels/benchmark_vit_aiter_fp8_attn.py b/benchmarks/kernels/benchmark_vit_aiter_fp8_attn.py index 6bb504b7f090..29ff156b6035 100644 --- a/benchmarks/kernels/benchmark_vit_aiter_fp8_attn.py +++ b/benchmarks/kernels/benchmark_vit_aiter_fp8_attn.py @@ -10,6 +10,7 @@ Example: python benchmarks/kernels/benchmark_vit_aiter_fp8_attn.py \ --seq-lens 2304 4096 8192 16384 --head-dim 72 + """ from types import SimpleNamespace diff --git a/benchmarks/kernels/benchmark_w8a8_block_fp8.py b/benchmarks/kernels/benchmark_w8a8_block_fp8.py index 590d4cfdc6d9..3ad7ec888728 100644 --- a/benchmarks/kernels/benchmark_w8a8_block_fp8.py +++ b/benchmarks/kernels/benchmark_w8a8_block_fp8.py @@ -57,10 +57,12 @@ def w8a8_block_matmul( Bs: The per-block quantization scale for `B`. block_size: The block size for per-block quantization. It should be 2-dim, e.g., [128, 128]. + config: Triton kernel launch config, e.g. tile sizes and num_warps. output_dtype: The dtype of the returned tensor. Returns: torch.Tensor: The result of matmul. + """ assert len(block_size) == 2 block_n, block_k = block_size[0], block_size[1] diff --git a/benchmarks/kernels/ir/bench_ir_ops.py b/benchmarks/kernels/ir/bench_ir_ops.py index b23c4e8ae327..80fdd87acb21 100644 --- a/benchmarks/kernels/ir/bench_ir_ops.py +++ b/benchmarks/kernels/ir/bench_ir_ops.py @@ -1,8 +1,7 @@ #!/usr/bin/env python3 # SPDX-License-Identifier: Apache-2.0 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project -""" -Generic benchmark harness for vLLM IR ops. +"""Generic benchmark harness for vLLM IR ops. Usage: python benchmarks/kernels/ir/bench_ir_ops.py diff --git a/benchmarks/kernels/ir/shapes.py b/benchmarks/kernels/ir/shapes.py index 6cc44cf6cec1..40f5f8947621 100644 --- a/benchmarks/kernels/ir/shapes.py +++ b/benchmarks/kernels/ir/shapes.py @@ -1,8 +1,6 @@ # SPDX-License-Identifier: Apache-2.0 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project -""" -Shape configurations for IR op benchmarks. -""" +"""Shape configurations for IR op benchmarks.""" import torch @@ -20,10 +18,11 @@ # Each entry maps an op name to a list of kwarg dicts that will be passed # to that op's registered input generator via op.generate_inputs(**kwargs). SHAPE_CONFIGS: dict[str, list[dict]] = { - "rms_norm": [ + op_name: [ {"num_tokens": n, "hidden_size": d, "dtype": dtype} for dtype in [torch.float16, torch.bfloat16, torch.float32] for d in COMMON_HIDDEN_SIZES for n in NUM_TOKENS - ], + ] + for op_name in ("rms_norm", "fused_add_rms_norm") } diff --git a/benchmarks/kernels/utils.py b/benchmarks/kernels/utils.py index a9af811bbe9c..64d23691bdbb 100644 --- a/benchmarks/kernels/utils.py +++ b/benchmarks/kernels/utils.py @@ -17,8 +17,7 @@ class CudaGraphBenchParams: @dataclasses.dataclass class ArgPool: - """ - When some argument of the benchmarking function is annotated with this type, + """When some argument of the benchmarking function is annotated with this type, the benchmarking class (BenchMM) will collapse the argument to a pick a single value from the given list of values, during function invocation. For every invocation during a benchmarking run, it will choose a diff --git a/benchmarks/multi_turn/bench_dataset.py b/benchmarks/multi_turn/bench_dataset.py index 8cb8a2f386a9..9b159a9ee999 100644 --- a/benchmarks/multi_turn/bench_dataset.py +++ b/benchmarks/multi_turn/bench_dataset.py @@ -148,8 +148,7 @@ def __init__( def _generate_lognormal_by_median( target_average: int, median_ratio: float ) -> tuple[float, float]: - """ - Compute (mu, sigma) for a lognormal distribution given: + """Compute (mu, sigma) for a lognormal distribution given: - a target average (mean of the distribution) - a ratio of median / mean (controls skewness), assume mean > median diff --git a/benchmarks/multi_turn/benchmark_serving_multi_turn.py b/benchmarks/multi_turn/benchmark_serving_multi_turn.py index 5a60d9c66883..f084a4d84c5f 100644 --- a/benchmarks/multi_turn/benchmark_serving_multi_turn.py +++ b/benchmarks/multi_turn/benchmark_serving_multi_turn.py @@ -32,8 +32,11 @@ from bench_utils import TEXT_SEPARATOR, Color, logger from transformers import AutoTokenizer # type: ignore +from vllm.benchmarks.lib.utils import redact_sensitive_namespace + NUM_TOKENS_FROM_DATASET = 0 TERM_SIGNAL = None +_SENSITIVE_ARG_FIELDS = ("api_key", "header") class ConversationSampling(str, Enum): @@ -1548,7 +1551,7 @@ async def main() -> None: args = parser.parse_args() - logger.info(args) + logger.info(redact_sensitive_namespace(args, _SENSITIVE_ARG_FIELDS)) logger.info(f"{Color.GREEN}Input parameters:{Color.RESET}") logger.info(f"url={args.url}") diff --git a/benchmarks/multi_turn/convert_sharegpt_to_openai.py b/benchmarks/multi_turn/convert_sharegpt_to_openai.py index fccab4d0ce21..b415744873b8 100644 --- a/benchmarks/multi_turn/convert_sharegpt_to_openai.py +++ b/benchmarks/multi_turn/convert_sharegpt_to_openai.py @@ -1,7 +1,6 @@ # SPDX-License-Identifier: Apache-2.0 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project -""" -Download dataset from: +"""Download dataset from: https://huggingface.co/datasets/philschmid/sharegpt-raw/blob/main/sharegpt_20230401_clean_lang_split.json Convert to OpenAI API: diff --git a/benchmarks/overheads/benchmark_hashing.py b/benchmarks/overheads/benchmark_hashing.py deleted file mode 100644 index 178599952d5c..000000000000 --- a/benchmarks/overheads/benchmark_hashing.py +++ /dev/null @@ -1,64 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 -# SPDX-FileCopyrightText: Copyright contributors to the vLLM project - -import cProfile -import pstats - -from vllm import LLM, SamplingParams -from vllm.utils.argparse_utils import FlexibleArgumentParser - -# A very long prompt, total number of tokens is about 15k. -LONG_PROMPT = ["You are an expert in large language models, aren't you?"] * 1000 -LONG_PROMPT = " ".join(LONG_PROMPT) - - -def main(args): - llm = LLM( - model=args.model, - enforce_eager=True, - enable_prefix_caching=True, - tensor_parallel_size=args.tensor_parallel_size, - ) - - sampling_params = SamplingParams(temperature=0, max_tokens=args.output_len) - profiler = cProfile.Profile() - - print("------warm up------") - for i in range(3): - output = llm.generate(LONG_PROMPT, sampling_params) - print(output[0].outputs[0].text) - - print("------start generating------") - for i in range(3): - profiler.runctx( - "llm.generate(LONG_PROMPT, sampling_params)", globals(), locals() - ) - - # analyze the runtime of hashing function - stats = pstats.Stats(profiler) - stats.sort_stats("cumulative") - total_time = 0 - total_calls = 0 - for func in stats.stats: - if "hash_of_block" in func[2]: - total_time = stats.stats[func][3] - total_calls = stats.stats[func][0] - percentage = (total_time / stats.total_tt) * 100 - print( - f"Hashing took {total_time:.2f} seconds,{percentage:.2f}% of the total runtime." - ) - - -if __name__ == "__main__": - parser = FlexibleArgumentParser( - description="Benchmark the performance of hashing function in" - "automatic prefix caching." - ) - parser.add_argument("--model", type=str, default="lmsys/longchat-7b-16k") - parser.add_argument("--tensor-parallel-size", "-tp", type=int, default=1) - parser.add_argument("--output-len", type=int, default=10) - parser.add_argument( - "--enable-prefix-caching", action="store_true", help="enable prefix caching" - ) - args = parser.parse_args() - main(args) diff --git a/benchmarks/replayssm/e2e_decode_speedup.py b/benchmarks/replayssm/e2e_decode_speedup.py index b7084a21315b..ff7c30669a12 100644 --- a/benchmarks/replayssm/e2e_decode_speedup.py +++ b/benchmarks/replayssm/e2e_decode_speedup.py @@ -15,6 +15,7 @@ python e2e_decode_speedup.py --model-id nvidia/NVIDIA-Nemotron-3-Nano-4B-BF16 python e2e_decode_speedup.py --dtype auto --buffer-len 16 \ --model-id nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-NVFP4 # B300 NVFP4 + """ import argparse diff --git a/cmake/cpu_extension.cmake b/cmake/cpu_extension.cmake index ff2911dc11c8..414fbc5e238c 100644 --- a/cmake/cpu_extension.cmake +++ b/cmake/cpu_extension.cmake @@ -173,6 +173,18 @@ if (CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|amd64" OR ENABLE_X86_ISA) set(AMX_FP8_SUPPORTED FALSE) message(STATUS "AMX-FP8 disabled by ENABLE_AMX_FP8=OFF") endif() + + # AVX10.2 arrived in GCC 15, so the target attribute, the fp8 convert + # intrinsics and __builtin_cpu_supports("avx10.2") are all hard errors on + # the 12.3+ the backend otherwise supports. Probe instead of assuming. + check_cxx_compiler_flag("-mavx10.2" COMPILER_SUPPORTS_AVX10_2_FLAG) + if (COMPILER_SUPPORTS_AVX10_2_FLAG) + set(AVX10_2_SUPPORTED TRUE) + else() + set(AVX10_2_SUPPORTED FALSE) + message(STATUS "AVX10.2 disabled: compiler does not support -mavx10.2 (compiler: ${CMAKE_CXX_COMPILER_ID} ${CMAKE_CXX_COMPILER_VERSION})") + endif() + list(APPEND CXX_COMPILE_FLAGS_AVX2 "-mavx2") elseif (POWER9_FOUND OR POWER10_FOUND OR POWER11_FOUND) @@ -488,6 +500,7 @@ set(VLLM_EXT_SRC "csrc/moe/dynamic_4bit_int_moe_cpu.cpp" "csrc/cpu/cpu_fused_moe.cpp" "csrc/cpu/cpu_attn.cpp" + "csrc/cpu/cpu_isa.cpp" "csrc/cpu/torch_bindings.cpp") if (CMAKE_SYSTEM_PROCESSOR MATCHES "riscv64" AND VLLM_RVV_VLEN AND @@ -522,6 +535,12 @@ if (POWER9_FOUND OR POWER10_FOUND OR POWER11_FOUND) ${VLLM_EXT_SRC}) endif() +if (POWER10_FOUND OR POWER11_FOUND) + set(VLLM_EXT_SRC + "csrc/cpu/cpu_fused_moe_int8.cpp" + ${VLLM_EXT_SRC}) +endif() + if(USE_ONEDNN) set(VLLM_EXT_SRC "csrc/cpu/dnnl_kernels.cpp" @@ -540,11 +559,13 @@ if (ENABLE_X86_ISA) "csrc/cpu/sgl-kernels/gemm.cpp" "csrc/cpu/sgl-kernels/gemm_int8.cpp" "csrc/cpu/sgl-kernels/gemm_fp8.cpp" + "csrc/cpu/sgl-kernels/gemm_fp8_w8a8.cpp" "csrc/cpu/sgl-kernels/gemm_int4.cpp" "csrc/cpu/sgl-kernels/moe.cpp" "csrc/cpu/sgl-kernels/moe_int8.cpp" "csrc/cpu/sgl-kernels/moe_int4.cpp" "csrc/cpu/sgl-kernels/moe_fp8.cpp" + "csrc/cpu/sgl-kernels/moe_fp8_w8a8.cpp" "csrc/cpu/sgl-kernels/bmm.cpp" "csrc/cpu/sgl-kernels/decode.cpp" "csrc/cpu/sgl-kernels/extend.cpp" @@ -565,6 +586,7 @@ if (ENABLE_X86_ISA) "csrc/cpu/utils.cpp" "csrc/cpu/spec_decode_utils.cpp" "csrc/cpu/cpu_attn.cpp" + "csrc/cpu/cpu_isa.cpp" "csrc/cpu/dnnl_kernels.cpp" "csrc/cpu/mamba_cpu.cpp" "csrc/cpu/torch_bindings.cpp" @@ -581,6 +603,7 @@ if (ENABLE_X86_ISA) "csrc/cpu/utils.cpp" "csrc/cpu/spec_decode_utils.cpp" "csrc/cpu/cpu_attn.cpp" + "csrc/cpu/cpu_isa.cpp" "csrc/cpu/mamba_cpu.cpp" "csrc/cpu/dnnl_kernels.cpp" "csrc/cpu/torch_bindings.cpp" @@ -618,6 +641,12 @@ if (ENABLE_X86_ISA) message(STATUS "AMX-FP8 (Diamond Rapids) enabled") endif() + # For the sgl-kernels AVX10.2 fp8 quantize paths + if (AVX10_2_SUPPORTED) + target_compile_definitions(_C PRIVATE "-DCPU_CAPABILITY_AVX10_2") + message(STATUS "AVX10.2 enabled") + endif() + # AVX512F define_extension_target( _C_AVX512 diff --git a/cmake/external_projects/deepgemm.cmake b/cmake/external_projects/deepgemm.cmake index 72f7f5eb66a8..a17031cc5e0f 100644 --- a/cmake/external_projects/deepgemm.cmake +++ b/cmake/external_projects/deepgemm.cmake @@ -28,9 +28,12 @@ if(DEEPGEMM_SRC_DIR) message(STATUS "DeepGEMM using local DEEPGEMM_SRC_DIR: ${deepgemm_SOURCE_DIR}") else() # Keep in sync with tools/install_deepgemm.sh - set(_DEEPGEMM_UPSTREAM_REPO "https://github.com/deepseek-ai/DeepGEMM.git") - # Pinned to the tip of the nv_dev branch (SM120 support). - set(_DEEPGEMM_UPSTREAM_TAG "8b1392b978f5a03c828dd1711090d7fb50958b8a") + set(_DEEPGEMM_UPSTREAM_REPO "https://github.com/vllm-project/DeepGEMM.git") + # Pinned to the fork's dev branch: upstream 2.8.0 plus the SM120 + # port, the SM90 paged-MQA kv_block=32/next_n=4 port, configurable SwiGLU + # alpha/beta, and SiTU for FP8/FP4 Mega MoE. Also includes the CUDA 12.x + # layout header fix from vllm-project/DeepGEMM#12. + set(_DEEPGEMM_UPSTREAM_TAG "e1f418c2a4f20818221f6b0e578b4c2f634d4c3f") set(_deepgemm_fc_root "${FETCHCONTENT_BASE_DIR}") if(NOT _deepgemm_fc_root) @@ -51,7 +54,7 @@ else() BINARY_DIR "${_deepgemm_bin}" GIT_REPOSITORY "${_DEEPGEMM_UPSTREAM_REPO}" GIT_TAG "${_DEEPGEMM_UPSTREAM_TAG}" - GIT_SUBMODULES "third-party/cutlass" "third-party/fmt" + GIT_SUBMODULES "third-party/cutlass" "third-party/deep_jit" GIT_PROGRESS TRUE ) endif() @@ -119,13 +122,15 @@ if(DEEPGEMM_ARCHS) message(STATUS "DeepGEMM _C will be built for: ${_dg_pythons}") # add_custom_command does no implicit header scanning; glob explicitly so - # header-only edits in DeepGEMM/cutlass/fmt re-trigger the rebuild. + # header-only edits in DeepGEMM/cutlass/deep_jit re-trigger the rebuild. file(GLOB_RECURSE _dg_headers "${deepgemm_SOURCE_DIR}/csrc/*.h" "${deepgemm_SOURCE_DIR}/csrc/*.hpp" "${deepgemm_SOURCE_DIR}/deep_gemm/include/*.h" "${deepgemm_SOURCE_DIR}/deep_gemm/include/*.hpp" - "${deepgemm_SOURCE_DIR}/deep_gemm/include/*.cuh") + "${deepgemm_SOURCE_DIR}/deep_gemm/include/*.cuh" + "${deepgemm_SOURCE_DIR}/third-party/deep_jit/include/*.h" + "${deepgemm_SOURCE_DIR}/third-party/deep_jit/include/*.hpp") set(_dg_markers) set(_dg_seen_soabis) diff --git a/cmake/external_projects/deepselect.cmake b/cmake/external_projects/deepselect.cmake new file mode 100644 index 000000000000..e2a5592e6167 --- /dev/null +++ b/cmake/external_projects/deepselect.cmake @@ -0,0 +1,97 @@ +include(FetchContent) + +# If DEEPSELECT_SRC_DIR is set, DeepSelect is built from that directory +# instead of downloading. +# It can be set as an environment variable or passed as a cmake argument. +# The environment variable takes precedence. +if (DEFINED ENV{DEEPSELECT_SRC_DIR}) + set(DEEPSELECT_SRC_DIR $ENV{DEEPSELECT_SRC_DIR}) +endif() + +if(DEEPSELECT_SRC_DIR) + FetchContent_Declare( + deepselect + SOURCE_DIR ${DEEPSELECT_SRC_DIR} + CONFIGURE_COMMAND "" + BUILD_COMMAND "" + ) +else() + FetchContent_Declare( + deepselect + GIT_REPOSITORY https://github.com/vllm-project/DeepSelect.git + GIT_TAG d96d33afe1fab0d6066da49cdc91e64c2bee65ea + GIT_PROGRESS TRUE + CONFIGURE_COMMAND "" + BUILD_COMMAND "" + ) +endif() + +FetchContent_MakeAvailable(deepselect) +message(STATUS "DeepSelect is available at ${deepselect_SOURCE_DIR}") + +# DeepSelect kernels require CUDA 12.9+; 10.0f (family) covers SM100 and SM103. +set(DEEPSELECT_SUPPORT_ARCHS) +if(${CMAKE_CUDA_COMPILER_VERSION} VERSION_GREATER_EQUAL 12.9) + list(APPEND DEEPSELECT_SUPPORT_ARCHS "10.0f") +endif() + +cuda_archs_loose_intersection(DEEPSELECT_ARCHS "${DEEPSELECT_SUPPORT_ARCHS}" "${CUDA_ARCHS}") +if(DEEPSELECT_ARCHS) + message(STATUS "DeepSelect CUDA architectures: ${DEEPSELECT_ARCHS}") + + # All explicit template instantiations must be compiled: api.cpp + # references every dispatch combination. + file(GLOB DeepSelect_SOURCES CONFIGURE_DEPENDS + ${deepselect_SOURCE_DIR}/csrc/cuda_kernels/v3/instantiations/*.cu + ${deepselect_SOURCE_DIR}/csrc/cuda_kernels/v3_fp32/instantiations/*.cu + ${deepselect_SOURCE_DIR}/csrc/cuda_kernels/v3_cluster/instantiations/*.cu + ) + list(PREPEND DeepSelect_SOURCES ${deepselect_SOURCE_DIR}/csrc/api.cpp) + + set(DeepSelect_INCLUDES + ${deepselect_SOURCE_DIR}/csrc + ${deepselect_SOURCE_DIR}/csrc/3rdparty/cutlass/include + ${deepselect_SOURCE_DIR}/csrc/3rdparty/kerutils/include + ) + + set_gencode_flags_for_srcs( + SRCS "${DeepSelect_SOURCES}" + CUDA_ARCHS "${DEEPSELECT_ARCHS}") + + set(VLLM_DEEPSELECT_GPU_FLAGS ${VLLM_GPU_FLAGS}) + list(APPEND VLLM_DEEPSELECT_GPU_FLAGS + "-O3" "--expt-relaxed-constexpr" "--expt-extended-lambda" + "--use_fast_math" "--ftz=false" + "-U__CUDA_NO_HALF_OPERATORS__" "-U__CUDA_NO_HALF_CONVERSIONS__" + "-U__CUDA_NO_HALF2_OPERATORS__" "-U__CUDA_NO_BFLOAT16_CONVERSIONS__") + + define_extension_target( + _deepselect_C + DESTINATION vllm + LANGUAGE ${VLLM_GPU_LANG} + SOURCES ${DeepSelect_SOURCES} + COMPILE_FLAGS ${VLLM_DEEPSELECT_GPU_FLAGS} + ARCHITECTURES ${VLLM_GPU_ARCHES} + INCLUDE_DIRECTORIES ${DeepSelect_INCLUDES} + USE_SABI 3 + WITH_SOABI) + + # Only use C-shim APIs available in PyTorch 2.10. + # _deepselect_C is abi compatible with PyTorch >= TORCH_TARGET_VERSION. + target_compile_definitions(_deepselect_C PRIVATE + TORCH_TARGET_VERSION=0x020A000000000000ULL) + + # Needed to use cuda APIs from C-shim + if(VLLM_GPU_LANG STREQUAL "CUDA") + target_compile_definitions(_deepselect_C PRIVATE USE_CUDA) + endif() + + # DeepSelect requires C++20 + target_compile_options(_deepselect_C PRIVATE + $<$:-std=c++20> + $<$:-std=c++20>) +else() + message(STATUS "DeepSelect will not compile: unsupported CUDA architecture ${CUDA_ARCHS}") + # Create an empty target for setup.py on unsupported systems + add_custom_target(_deepselect_C) +endif() diff --git a/cmake/external_projects/flashkda.cmake b/cmake/external_projects/flashkda.cmake index 6ffb46e6a793..5ac4f4b415ea 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 3b225bf26bb8e218928a1fe14751cb48cf31d11b + GIT_TAG b59532f1f464fbd536272780e30df5bf6a2ccc02 GIT_PROGRESS TRUE GIT_SUBMODULES cutlass ) diff --git a/cmake/external_projects/flashmla.cmake b/cmake/external_projects/flashmla.cmake index 4cf1e421f0a5..cfa8243f726c 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 6bc49418c5ead572ff0339191ddf3b155749e183 + GIT_TAG 0eee43b12f034b657133cf2afca6a72ebb6efccf GIT_PROGRESS TRUE CONFIGURE_COMMAND "" BUILD_COMMAND "" @@ -76,43 +76,77 @@ if(FLASH_MLA_ARCHS) set(FlashMLA_SOURCES ${flashmla_SOURCE_DIR}/csrc/api/api.cpp + ${flashmla_SOURCE_DIR}/csrc/api/sparse_prefill.cpp + ${flashmla_SOURCE_DIR}/csrc/api/sparse_decode.cpp + ${flashmla_SOURCE_DIR}/csrc/api/dense_decode.cpp + ${flashmla_SOURCE_DIR}/csrc/api/fused_norm_rope_attn_rope_cast_fwd.cpp # Misc kernels for decoding - ${flashmla_SOURCE_DIR}/csrc/smxx/decode/get_decoding_sched_meta/get_decoding_sched_meta.cu - ${flashmla_SOURCE_DIR}/csrc/smxx/decode/combine/combine.cu + ${flashmla_SOURCE_DIR}/csrc/kernels/smxx/decode/get_decoding_sched_meta/get_decoding_sched_meta.cu + ${flashmla_SOURCE_DIR}/csrc/kernels/smxx/decode/combine/combine.cu # sm90 dense decode - ${flashmla_SOURCE_DIR}/csrc/sm90/decode/dense/instantiations/fp16.cu - ${flashmla_SOURCE_DIR}/csrc/sm90/decode/dense/instantiations/bf16.cu + ${flashmla_SOURCE_DIR}/csrc/kernels/sm90/decode/dense/instantiations/fp16.cu + ${flashmla_SOURCE_DIR}/csrc/kernels/sm90/decode/dense/instantiations/bf16.cu # sm90 sparse decode - ${flashmla_SOURCE_DIR}/csrc/sm90/decode/sparse_fp8/instantiations/model1_persistent_h64.cu - ${flashmla_SOURCE_DIR}/csrc/sm90/decode/sparse_fp8/instantiations/model1_persistent_h128.cu - ${flashmla_SOURCE_DIR}/csrc/sm90/decode/sparse_fp8/instantiations/v32_persistent_h64.cu - ${flashmla_SOURCE_DIR}/csrc/sm90/decode/sparse_fp8/instantiations/v32_persistent_h128.cu + ${flashmla_SOURCE_DIR}/csrc/kernels/sm90/decode/sparse/instantiations/v4_persistent_h64.cu + ${flashmla_SOURCE_DIR}/csrc/kernels/sm90/decode/sparse/instantiations/v4_persistent_h128.cu + ${flashmla_SOURCE_DIR}/csrc/kernels/sm90/decode/sparse/instantiations/v32_persistent_h64.cu + ${flashmla_SOURCE_DIR}/csrc/kernels/sm90/decode/sparse/instantiations/v32_persistent_h128.cu # sm90 sparse prefill - ${flashmla_SOURCE_DIR}/csrc/sm90/prefill/sparse/fwd.cu - ${flashmla_SOURCE_DIR}/csrc/sm90/prefill/sparse/instantiations/phase1_k512.cu - ${flashmla_SOURCE_DIR}/csrc/sm90/prefill/sparse/instantiations/phase1_k512_topklen.cu - ${flashmla_SOURCE_DIR}/csrc/sm90/prefill/sparse/instantiations/phase1_k576.cu - ${flashmla_SOURCE_DIR}/csrc/sm90/prefill/sparse/instantiations/phase1_k576_topklen.cu + ${flashmla_SOURCE_DIR}/csrc/kernels/sm90/prefill/sparse/instantiations/phase1_k512.cu + ${flashmla_SOURCE_DIR}/csrc/kernels/sm90/prefill/sparse/instantiations/phase1_k512_topklen.cu + ${flashmla_SOURCE_DIR}/csrc/kernels/sm90/prefill/sparse/instantiations/phase1_k576.cu + ${flashmla_SOURCE_DIR}/csrc/kernels/sm90/prefill/sparse/instantiations/phase1_k576_topklen.cu - # sm100 dense prefill & backward - ${flashmla_SOURCE_DIR}/csrc/sm100/prefill/dense/fmha_cutlass_fwd_sm100.cu + # sm100 dense prefill (inference-only: dense backward stays out) + ${flashmla_SOURCE_DIR}/csrc/kernels/sm100/prefill/dense/fmha_cutlass_fwd_sm100.cu # sm100 sparse prefill - ${flashmla_SOURCE_DIR}/csrc/sm100/prefill/sparse/fwd/head64/instantiations/phase1_k512.cu - ${flashmla_SOURCE_DIR}/csrc/sm100/prefill/sparse/fwd/head64/instantiations/phase1_k576.cu - ${flashmla_SOURCE_DIR}/csrc/sm100/prefill/sparse/fwd/head128/instantiations/phase1_k512.cu - ${flashmla_SOURCE_DIR}/csrc/sm100/prefill/sparse/fwd/head128/instantiations/phase1_k576.cu - ${flashmla_SOURCE_DIR}/csrc/sm100/prefill/sparse/fwd_for_small_topk/head128/instantiations/phase1_prefill_k512.cu + ${flashmla_SOURCE_DIR}/csrc/kernels/sm100/prefill/sparse/fwd/head64/instantiations/phase1_h64_k512.cu + ${flashmla_SOURCE_DIR}/csrc/kernels/sm100/prefill/sparse/fwd/head64/instantiations/phase1_h64_k576.cu + ${flashmla_SOURCE_DIR}/csrc/kernels/sm100/prefill/sparse/fwd/head128/instantiations/phase1_k512.cu + ${flashmla_SOURCE_DIR}/csrc/kernels/sm100/prefill/sparse/fwd/head128/instantiations/phase1_k576.cu + ${flashmla_SOURCE_DIR}/csrc/kernels/sm100/prefill/sparse/fwd_for_small_topk/head128/instantiations/phase1_k512.cu + + # sm100 fused norm + rope + attn + rope + cast + ${flashmla_SOURCE_DIR}/csrc/kernels/sm100/prefill/sparse/fused_norm_rope_attn_rope_cast_fwd/core_attn/instantiations/v4_h64_prefill_norm.cu + ${flashmla_SOURCE_DIR}/csrc/kernels/sm100/prefill/sparse/fused_norm_rope_attn_rope_cast_fwd/core_attn/instantiations/v4_h64_prefill_nonorm.cu + ${flashmla_SOURCE_DIR}/csrc/kernels/sm100/prefill/sparse/fused_norm_rope_attn_rope_cast_fwd/core_attn/instantiations/v4_h128_prefill_norm.cu + ${flashmla_SOURCE_DIR}/csrc/kernels/sm100/prefill/sparse/fused_norm_rope_attn_rope_cast_fwd/core_attn/instantiations/v4_h128_prefill_nonorm.cu + ${flashmla_SOURCE_DIR}/csrc/kernels/sm100/prefill/sparse/fused_norm_rope_attn_rope_cast_fwd/core_attn/instantiations/v4_h64_decode_norm.cu + ${flashmla_SOURCE_DIR}/csrc/kernels/sm100/prefill/sparse/fused_norm_rope_attn_rope_cast_fwd/core_attn/instantiations/v4_h64_decode_nonorm.cu + ${flashmla_SOURCE_DIR}/csrc/kernels/sm100/prefill/sparse/fused_norm_rope_attn_rope_cast_fwd/core_attn/instantiations/v4_h128_decode_norm.cu + ${flashmla_SOURCE_DIR}/csrc/kernels/sm100/prefill/sparse/fused_norm_rope_attn_rope_cast_fwd/core_attn/instantiations/v4_h128_decode_nonorm.cu + ${flashmla_SOURCE_DIR}/csrc/kernels/sm100/prefill/sparse/fused_norm_rope_attn_rope_cast_fwd/core_attn/instantiations/v41_h64_decode_norm.cu + ${flashmla_SOURCE_DIR}/csrc/kernels/sm100/prefill/sparse/fused_norm_rope_attn_rope_cast_fwd/core_attn/instantiations/v41_h64_decode_nonorm.cu + ${flashmla_SOURCE_DIR}/csrc/kernels/sm100/prefill/sparse/fused_norm_rope_attn_rope_cast_fwd/core_attn/instantiations/v41_h128_decode_norm.cu + ${flashmla_SOURCE_DIR}/csrc/kernels/sm100/prefill/sparse/fused_norm_rope_attn_rope_cast_fwd/core_attn/instantiations/v41_h128_decode_nonorm.cu + ${flashmla_SOURCE_DIR}/csrc/kernels/sm100/prefill/sparse/fused_norm_rope_attn_rope_cast_fwd/core_attn/instantiations/v41fp4_h64_decode_norm.cu + ${flashmla_SOURCE_DIR}/csrc/kernels/sm100/prefill/sparse/fused_norm_rope_attn_rope_cast_fwd/core_attn/instantiations/v41fp4_h64_decode_nonorm.cu + ${flashmla_SOURCE_DIR}/csrc/kernels/sm100/prefill/sparse/fused_norm_rope_attn_rope_cast_fwd/core_attn/instantiations/v41fp4_h128_decode_norm.cu + ${flashmla_SOURCE_DIR}/csrc/kernels/sm100/prefill/sparse/fused_norm_rope_attn_rope_cast_fwd/core_attn/instantiations/v41fp4_h128_decode_nonorm.cu + ${flashmla_SOURCE_DIR}/csrc/kernels/sm100/prefill/sparse/fused_norm_rope_attn_rope_cast_fwd/permute_q_b_proj/kernel.cu + ${flashmla_SOURCE_DIR}/csrc/kernels/sm100/prefill/sparse/fused_norm_rope_attn_rope_cast_fwd/permute_wv_proj/kernel.cu # sm100 sparse decode - ${flashmla_SOURCE_DIR}/csrc/sm100/decode/head64/instantiations/v32.cu - ${flashmla_SOURCE_DIR}/csrc/sm100/decode/head64/instantiations/model1.cu - ${flashmla_SOURCE_DIR}/csrc/sm100/decode/head64/instantiations/v32_nvfp4_fp8rope.cu - ${flashmla_SOURCE_DIR}/csrc/sm100/prefill/sparse/fwd_for_small_topk/head128/instantiations/phase1_decode_k512.cu + ${flashmla_SOURCE_DIR}/csrc/kernels/sm100/decode/sparse/head64/instantiations/v32_h64.cu + ${flashmla_SOURCE_DIR}/csrc/kernels/sm100/decode/sparse/head64/instantiations/v32_h64_no_split.cu + ${flashmla_SOURCE_DIR}/csrc/kernels/sm100/decode/sparse/head64/instantiations/v4_h64.cu + ${flashmla_SOURCE_DIR}/csrc/kernels/sm100/decode/sparse/head64/instantiations/v4_h64_no_split.cu + ${flashmla_SOURCE_DIR}/csrc/kernels/sm100/decode/sparse/head64/instantiations/v41_h64.cu + ${flashmla_SOURCE_DIR}/csrc/kernels/sm100/decode/sparse/head64/instantiations/v41_h64_no_split.cu + ${flashmla_SOURCE_DIR}/csrc/kernels/sm100/decode/sparse/head64/instantiations/v41fp4_h64.cu + ${flashmla_SOURCE_DIR}/csrc/kernels/sm100/decode/sparse/head64/instantiations/v41fp4_h64_no_split.cu + ${flashmla_SOURCE_DIR}/csrc/kernels/sm100/decode/sparse/nvfp4_head64/instantiations/v32_nvfp4_fp8rope.cu + ${flashmla_SOURCE_DIR}/csrc/kernels/sm100/prefill/sparse/fwd_for_small_topk/head128/instantiations/phase1_decode_k512.cu + ${flashmla_SOURCE_DIR}/csrc/kernels/sm100/prefill/sparse/fwd_for_small_topk/head128/instantiations/phase1_decode_k512_splitkv.cu + ${flashmla_SOURCE_DIR}/csrc/kernels/sm100/prefill/sparse/fwd_for_small_topk/head128/instantiations/phase1_decode_k512_v41.cu + ${flashmla_SOURCE_DIR}/csrc/kernels/sm100/prefill/sparse/fwd_for_small_topk/head128/instantiations/phase1_decode_k512_v41_splitkv.cu + ${flashmla_SOURCE_DIR}/csrc/kernels/sm100/prefill/sparse/fwd_for_small_topk/head128/instantiations/phase1_decode_k512_v41fp4.cu + ${flashmla_SOURCE_DIR}/csrc/kernels/sm100/prefill/sparse/fwd_for_small_topk/head128/instantiations/phase1_decode_k512_v41fp4_splitkv.cu ) set(FlashMLA_Extension_SOURCES @@ -125,7 +159,6 @@ if(FLASH_MLA_ARCHS) set(FlashMLA_INCLUDES ${flashmla_SOURCE_DIR}/csrc ${flashmla_SOURCE_DIR}/csrc/kerutils/include - ${flashmla_SOURCE_DIR}/csrc/sm90 ${flashmla_SOURCE_DIR}/csrc/cutlass/include ${flashmla_SOURCE_DIR}/csrc/cutlass/tools/util/include ) @@ -186,6 +219,15 @@ if(FLASH_MLA_ARCHS) if(VLLM_GPU_LANG STREQUAL "CUDA") target_compile_definitions(_flashmla_extension_C PRIVATE USE_CUDA) endif() + + # FlashMLA's sources use M_LOG2E from . MSVC's UCRT only defines the + # M_* constants when _USE_MATH_DEFINES is set before is first + # included, so define it for both targets on MSVC. Scoped to MSVC so the + # compile flags (and build caches) on other platforms stay unchanged. + if(MSVC) + target_compile_definitions(_flashmla_C PRIVATE _USE_MATH_DEFINES) + target_compile_definitions(_flashmla_extension_C PRIVATE _USE_MATH_DEFINES) + endif() else() message(STATUS "FlashMLA will not compile: unsupported CUDA architecture ${CUDA_ARCHS}") # Create empty targets for setup.py on unsupported systems diff --git a/cmake/external_projects/fmha_sm100.cmake b/cmake/external_projects/fmha_sm100.cmake index 442097dec7d3..ae5bfec7b47d 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 087c161814d4d9c735b46c21212a09e5f8eb92fa + GIT_TAG f355c37eb4e1413f21ee2ad8bbad25079e6bef9d GIT_PROGRESS TRUE CONFIGURE_COMMAND "" BUILD_COMMAND "" diff --git a/cmake/external_projects/qutlass.cmake b/cmake/external_projects/qutlass.cmake index cd3665c889e9..b6df33c170b8 100644 --- a/cmake/external_projects/qutlass.cmake +++ b/cmake/external_projects/qutlass.cmake @@ -142,9 +142,9 @@ if(${CMAKE_CUDA_COMPILER_VERSION} VERSION_GREATER_EQUAL 12.8 AND QUTLASS_ARCHS) target_compile_definitions(_qutlass_C PRIVATE QUTLASS_MINIMAL_BUILD=1 + QUTLASS_DISABLE_PYBIND=1 TARGET_CUDA_ARCH=${QUTLASS_TARGET_CC} CUTLASS_ENABLE_DIRECT_CUDA_DRIVER_CALL=1 - TORCH_TARGET_VERSION=0x020B000000000000ULL USE_CUDA) set_property(SOURCE ${QUTLASS_SOURCES} APPEND PROPERTY COMPILE_OPTIONS diff --git a/cmake/external_projects/tml_fa4.cmake b/cmake/external_projects/tml_fa4.cmake index a965295e2bbd..c6ddff82c339 100644 --- a/cmake/external_projects/tml_fa4.cmake +++ b/cmake/external_projects/tml_fa4.cmake @@ -14,7 +14,7 @@ else() FetchContent_Declare( tml_fa4 GIT_REPOSITORY https://github.com/vllm-project/tml-fa4.git - GIT_TAG b206834606ed5b5f21f8eed6b0683f528ea9cf7d + GIT_TAG 75765e76a9c2c012c1f6ecd64577eb646eb4d303 GIT_PROGRESS TRUE CONFIGURE_COMMAND "" BUILD_COMMAND "") diff --git a/cmake/external_projects/vllm_flash_attn.cmake b/cmake/external_projects/vllm_flash_attn.cmake index 1b3be244de54..2a26023f740b 100644 --- a/cmake/external_projects/vllm_flash_attn.cmake +++ b/cmake/external_projects/vllm_flash_attn.cmake @@ -39,7 +39,7 @@ else() FetchContent_Declare( vllm-flash-attn GIT_REPOSITORY https://github.com/vllm-project/flash-attention.git - GIT_TAG 506341a143fcabd4bb79052a7605ada727d6b3f5 + GIT_TAG 9cd61de38763d712bb6ce56e2a02cc2bf718c89f GIT_PROGRESS TRUE # Don't share the vllm-flash-attn build between build types BINARY_DIR ${CMAKE_BINARY_DIR}/vllm-flash-attn diff --git a/csrc/cpu/cpu_attn.cpp b/csrc/cpu/cpu_attn.cpp index b31a6faa0104..13e430fb4192 100644 --- a/csrc/cpu/cpu_attn.cpp +++ b/csrc/cpu/cpu_attn.cpp @@ -1,16 +1,7 @@ #include "cpu_attn_dispatch_generated.h" -#if defined(__x86_64__) || defined(_M_X64) - #include -// Runtime check for AMX-FP8 (Diamond Rapids): CPUID leaf 7, subleaf 1, EAX[21]. -static bool runtime_has_amx_fp8() { - unsigned int eax = 0, ebx = 0, ecx = 0, edx = 0; - if (__get_cpuid_count(7, 1, &eax, &ebx, &ecx, &edx)) { - return (eax >> 21) & 1u; - } - return false; -} -#endif // x86_64 +// Runtime check for AMX-FP8, implemented in cpu_isa.cpp. +extern bool runtime_has_amx_fp8(); // Maps kv_cache_dtype string to Fp8KVCacheDataType enum. // "auto" -> kAuto(0); "fp8"/"fp8_e4m3" -> kFp8E4M3; "fp8_e5m2" -> kFp8E5M2. @@ -252,7 +243,19 @@ void cpu_attention_with_kv_cache( input.block_table = block_table.data_ptr(); input.alibi_slopes = alibi_slopes.has_value() ? alibi_slopes->data_ptr() : nullptr; - input.s_aux = s_aux.has_value() ? s_aux->data_ptr() : nullptr; + // Attention sinks may be bf16 (native path) or fp32. Anything that is not + // bf16 must be provided as fp32 and is executed in full float precision. + if (s_aux.has_value()) { + TORCH_CHECK(s_aux->scalar_type() == at::ScalarType::BFloat16 || + s_aux->scalar_type() == at::ScalarType::Float, + "cpu_attention_with_kv_cache: s_aux (attention sinks) dtype ", + s_aux->scalar_type(), " must be bfloat16 or float32"); + input.s_aux = s_aux->data_ptr(); + input.s_aux_is_bf16 = s_aux->scalar_type() == at::ScalarType::BFloat16; + } else { + input.s_aux = nullptr; + input.s_aux_is_bf16 = false; + } input.dynamic_causal = dynamic_causal.has_value() ? dynamic_causal->data_ptr() : nullptr; input.scale = scale; diff --git a/csrc/cpu/cpu_attn_impl.hpp b/csrc/cpu/cpu_attn_impl.hpp index e9ddaacea573..d914e150636c 100644 --- a/csrc/cpu/cpu_attn_impl.hpp +++ b/csrc/cpu/cpu_attn_impl.hpp @@ -879,7 +879,11 @@ struct AttentionInput { int32_t* seq_lens; int32_t* block_table; float* alibi_slopes; - c10::BFloat16* s_aux; + // Attention sinks pointer. May reference bf16 or fp32 data depending on + // `s_aux_is_bf16`. bf16 sinks keep the native bf16 path; anything else is + // provided as fp32 and executed in full float precision. + const void* s_aux; + bool s_aux_is_bf16; bool* dynamic_causal; float scale; bool causal; @@ -956,6 +960,28 @@ void print_logits(const char* name, T* ptr, int32_t row, int32_t col, std::printf("%s", ss.str().c_str()); } +// This allows us to store probabilities in a packed format +// for GEMM implementations that need packed A. +template +struct ProbabilityTokenStore { + using prob_buffer_t = typename attention_impl_t::prob_buffer_t; + static constexpr int32_t TokenStride = 1; + + FORCE_INLINE static void store_probabilities( + prob_buffer_t* __restrict__ probability, const vec_op::FP32Vec16& values, + const int32_t, const int64_t) { + using prob_buffer_vec_t = typename VecTypeTrait::vec_t; + prob_buffer_vec_t output_vec(values); + output_vec.save(probability); + } +}; + +template +struct ProbabilityTokenStore< + attention_impl_t, + std::void_t> + : attention_impl_t::ProbabilityTokenStore {}; + template class AttentionMainLoop { public: @@ -966,6 +992,7 @@ class AttentionMainLoop { using partial_output_buffer_t = typename attention_impl_t::partial_output_buffer_t; using prob_buffer_t = typename attention_impl_t::prob_buffer_t; + using probability_token_store_t = ProbabilityTokenStore; static constexpr int64_t max_q_head_num_per_iter = attention_impl_t::MaxQHeadNumPerIteration; @@ -974,6 +1001,8 @@ class AttentionMainLoop { static constexpr int64_t headdim_alignment = attention_impl_t::HeadDimAlignment; static constexpr int64_t head_dim = attention_impl_t::HeadDim; + static constexpr int32_t probability_token_stride = + probability_token_store_t::TokenStride; static constexpr ISA ISAType = attention_impl_t::ISAType; static constexpr bool scale_on_logits = attention_impl_t::scale_on_logits; // apply scale on logits, otherwise @@ -1152,8 +1181,6 @@ class AttentionMainLoop { ? kv_tile_token_num : kv_tile_token_num * (sizeof(logits_buffer_t) / sizeof(prob_buffer_t)); - constexpr int64_t prob_buffer_elem_size = - prequantize_probabilities ? sizeof(uint8_t) : sizeof(prob_buffer_t); partial_output_buffer_t* curr_partial_q_buffer = partial_q_buffer; bool accum_c = !is_first_iter; for (int32_t block_idx = start_block_idx; block_idx < end_block_idx; @@ -1187,9 +1214,7 @@ class AttentionMainLoop { remaining_group_num -= curr_group_num_in_block; curr_group_offset = 0; curr_group_num_in_block = token_group_num_per_block; - curr_prob_buffer = reinterpret_cast( - reinterpret_cast(curr_prob_buffer) + - curr_token_num * prob_buffer_elem_size); + curr_prob_buffer += curr_token_num * probability_token_stride; curr_partial_q_buffer = partial_q_buffer; accum_c = true; } @@ -1262,7 +1287,6 @@ class AttentionMainLoop { std::is_same_v; #endif - using prob_buffer_vec_t = typename VecTypeTrait::vec_t; constexpr bool prequantize_probabilities = []() { if constexpr (requires { tile_gemm_t::prequantize_probabilities; }) { return tile_gemm_t::prequantize_probabilities; @@ -1331,12 +1355,6 @@ class AttentionMainLoop { vec = fast_exp(vec); } - if constexpr (prequantize_probabilities) { - vec.save(curr_logits_buffer_iter); - } else { - prob_buffer_vec_t output_vec(vec); - output_vec.save(curr_prob_buffer_iter); - } #else vec.save(curr_logits_buffer_iter); for (int32_t k = 0; k < 16; ++k) { @@ -1345,10 +1363,19 @@ class AttentionMainLoop { vec = vec_op::FP32Vec16(curr_logits_buffer_iter); #endif + if constexpr (prequantize_probabilities) { + vec.save(curr_logits_buffer_iter); + } else { + probability_token_store_t::store_probabilities( + curr_prob_buffer_iter, vec, i, + logits_buffer_stride * sizeof(logits_buffer_t) / + sizeof(prob_buffer_t)); + } + sum_vec = sum_vec + vec; curr_logits_buffer_iter += 16; - curr_prob_buffer_iter += 16; + curr_prob_buffer_iter += 16 * probability_token_stride; } if constexpr (prequantize_probabilities) { tile_gemm_t::quantize_probability_row( @@ -1542,7 +1569,8 @@ class AttentionMainLoop { const float scale = input->scale; const float softcap_scale = input->softcap; const float* alibi_slopes = input->alibi_slopes; - const c10::BFloat16* s_aux = input->s_aux; + const void* s_aux = input->s_aux; + const bool s_aux_is_bf16 = input->s_aux_is_bf16; const bool* dynamic_causal = input->dynamic_causal; const bool is_dynamic_causal = dynamic_causal != nullptr; @@ -1750,9 +1778,6 @@ class AttentionMainLoop { const float* curr_alibi_slopes = (alibi_slopes != nullptr ? alibi_slopes + q_head_start_idx : nullptr); - const c10::BFloat16* curr_s_aux = - (s_aux != nullptr ? s_aux + q_head_start_idx : nullptr); - // copy the Q tile to q_buffer, the logical layout of q_buffer is // [actual_q_token_num, curr_q_heads_per_kv, head_dim] { @@ -1764,10 +1789,23 @@ class AttentionMainLoop { if (use_sink) { alignas(64) float s_aux_fp32[16]; - // All other platforms have BF16Vec16 available - vec_op::BF16Vec16 vec_bf16(curr_s_aux); - vec_op::FP32Vec16 vec_fp32(vec_bf16); - vec_fp32.save(s_aux_fp32); + // Sinks may be provided as bf16 or fp32. Either way they are + // loaded into fp32: bf16 sinks are up-converted (native + // BF16Vec16 path, available on all platforms), while fp32 + // sinks are loaded directly and executed in full precision. + if (s_aux_is_bf16) { + const c10::BFloat16* curr_s_aux = + static_cast(s_aux) + + q_head_start_idx; + vec_op::BF16Vec16 vec_bf16(curr_s_aux); + vec_op::FP32Vec16 vec_fp32(vec_bf16); + vec_fp32.save(s_aux_fp32); + } else { + const float* curr_s_aux = + static_cast(s_aux) + q_head_start_idx; + vec_op::FP32Vec16 vec_fp32(curr_s_aux); + vec_fp32.save(s_aux_fp32); + } float* __restrict__ curr_sum_buffer = sum_buffer; float* __restrict__ curr_max_buffer = max_buffer; diff --git a/csrc/cpu/cpu_attn_neon.hpp b/csrc/cpu/cpu_attn_neon.hpp index db4c5df2e88d..eed749944496 100644 --- a/csrc/cpu/cpu_attn_neon.hpp +++ b/csrc/cpu/cpu_attn_neon.hpp @@ -11,7 +11,7 @@ namespace cpu_attention { namespace { -#define BLOCK_SIZE_ALIGNMENT 32 +#define BLOCK_SIZE_ALIGNMENT 16 #define HEAD_SIZE_ALIGNMENT 32 #define MAX_Q_HEAD_NUM_PER_ITER 16 @@ -387,7 +387,7 @@ class AttentionImpl { }; #ifdef ARM_BF16_SUPPORT -// For BF16 on Arm, reuse the BFMMLA kernels with 32-token alignment. +// For BF16 on Arm, reuse the BFMMLA kernels with the same token alignment. template class AttentionImpl : public AttentionImplNEONBFMMLA #include +#include +#include #include -#include namespace cpu_attention { -namespace { - -// BFMMLA tile dimensions -constexpr int32_t TILE_ROWS = 2; // M dimension -constexpr int32_t TILE_K = 4; // K reduction -constexpr int32_t TILE_COLS = 2; // N dimension (column-pair) - -// Derived constants -constexpr int32_t OUTPUT_COLS_PER_BLOCK = 8; // 4 column-pairs -constexpr int32_t K_TOKENS_PER_GROUP = 8; // Tokens grouped in K cache -constexpr int32_t V_TOKENS_PER_ROW_BLOCK = 4; // Tokens per V cache row block -constexpr int32_t K_INNER_STRIDE = K_TOKENS_PER_GROUP * TILE_K; -constexpr int32_t V_INNER_STRIDE = V_TOKENS_PER_ROW_BLOCK * TILE_COLS; -constexpr int32_t PACK_ELEMENTS_PER_K_CHUNK = TILE_ROWS * TILE_K; // A packing - -// Matrix Packing and Accumulator -// Reshape two rows of Q into BFMMLA-friendly interleaved -// Input: row0 = [a0,a1,a2,a3], row1 = [b0,b1,b2,b3] -// Output: [a0,a1,a2,a3,b0,b1,b2,b3, a4,a5,a6,a7,b4,b5,b6,b7] -// For K tail (K % TILE_K != 0): pads with zeros to complete the final chunk -FORCE_INLINE void reshape_Q_2xK_for_bfmmla(const c10::BFloat16* __restrict r0, - const c10::BFloat16* __restrict r1, - c10::BFloat16* __restrict dst, - int32_t K) { - const uint16_t* s0 = reinterpret_cast(r0); - const uint16_t* s1 = reinterpret_cast(r1); - uint16_t* d = reinterpret_cast(dst); - - // Process TILE_K elements at a time (PACK_ELEMENTS_PER_K_CHUNK output) - int32_t k = 0; - for (; k + TILE_K <= K; k += TILE_K, d += PACK_ELEMENTS_PER_K_CHUNK) { - vst1q_u16(d, vcombine_u16(vld1_u16(s0 + k), vld1_u16(s1 + k))); - } - - // Handle K tail: pack remaining elements with zero-padding - const int32_t tail = K - k; - if (tail > 0) { - // Pack remaining tail elements: [r0[k..k+tail-1], pad, r1[k..k+tail-1], - // pad] - for (int32_t t = 0; t < tail; ++t) { - d[t] = s0[k + t]; - d[t + TILE_K] = s1[k + t]; - } - // Zero-pad the rest - for (int32_t t = tail; t < TILE_K; ++t) { - d[t] = 0; - d[t + TILE_K] = 0; +class BfmmlaGemm { + public: + static constexpr int32_t KTile = 4; + static constexpr int32_t NTile = 8; + static constexpr int32_t MaxRows = 8; + + FORCE_INLINE static void gemm(const c10::BFloat16* __restrict__ a, + const c10::BFloat16* __restrict__ b, + float* __restrict__ c, const int32_t m, + const int32_t n, const int32_t k, + const int64_t a_pair_stride, + const int64_t b_n_group_stride, + const int64_t b_k_group_stride, + const int64_t ldc, const bool accumulate) { + const auto* a_ptr = reinterpret_cast(a); + const auto* b_ptr = reinterpret_cast(b); + + for (int32_t n_idx = 0; n_idx < n; n_idx += 16) { + const auto* b_panel = b_ptr + (n_idx / NTile) * b_n_group_stride; + float* c_panel = c + n_idx; + + // Preserve this range so the inactive row pair is optimized away. + if (m <= 2) { + gemm_4x16(a_ptr, b_panel, c_panel, m, k, a_pair_stride, + b_n_group_stride, b_k_group_stride, ldc, accumulate); + } else if (m <= 4) { + gemm_4x16(a_ptr, b_panel, c_panel, m, k, a_pair_stride, + b_n_group_stride, b_k_group_stride, ldc, accumulate); + } else { + gemm_8x8(a_ptr, b_panel, c_panel, m, k, a_pair_stride, b_k_group_stride, + ldc, accumulate); + gemm_8x8(a_ptr, b_panel + b_n_group_stride, c_panel + NTile, m, k, + a_pair_stride, b_k_group_stride, ldc, accumulate); + } } } -} - -// 2x2 accumulator load/store with compile-time row count -template -FORCE_INLINE float32x4_t load_acc_2x2(float* base, int64_t ldc, int col_off) { - static_assert(m_rows == 1 || m_rows == 2); - float32x2_t row0 = vld1_f32(base + col_off); - float32x2_t row1 = - (m_rows == 2) ? vld1_f32(base + ldc + col_off) : vdup_n_f32(0.f); - return vcombine_f32(row0, row1); -} - -template -FORCE_INLINE void store_acc_2x2(float32x4_t acc, float* base, int64_t ldc, - int col_off) { - static_assert(m_rows == 1 || m_rows == 2); - vst1_f32(base + col_off, vget_low_f32(acc)); - if constexpr (m_rows == 2) { - vst1_f32(base + ldc + col_off, vget_high_f32(acc)); - } -} - -// Initialize 4 column-pair accumulators for 2 rows (8 columns total) -#define INIT_ACC_ROWPAIR_4(a0, a1, a2, a3, Crow, ldc, m_rows, accum) \ - do { \ - if (accum) { \ - if (m_rows == 2) { \ - a0 = load_acc_2x2<2>(Crow, ldc, 0); \ - a1 = load_acc_2x2<2>(Crow, ldc, 2); \ - a2 = load_acc_2x2<2>(Crow, ldc, 4); \ - a3 = load_acc_2x2<2>(Crow, ldc, 6); \ - } else { \ - a0 = load_acc_2x2<1>(Crow, ldc, 0); \ - a1 = load_acc_2x2<1>(Crow, ldc, 2); \ - a2 = load_acc_2x2<1>(Crow, ldc, 4); \ - a3 = load_acc_2x2<1>(Crow, ldc, 6); \ - } \ - } else { \ - a0 = a1 = a2 = a3 = vdupq_n_f32(0.f); \ - } \ - } while (0) - -// Store 4 column-pair accumulators back to C matrix -#define STORE_ACC_ROWPAIR_4(a0, a1, a2, a3, Crow, ldc, m_rows) \ - do { \ - if (m_rows == 2) { \ - store_acc_2x2<2>(a0, Crow, ldc, 0); \ - store_acc_2x2<2>(a1, Crow, ldc, 2); \ - store_acc_2x2<2>(a2, Crow, ldc, 4); \ - store_acc_2x2<2>(a3, Crow, ldc, 6); \ - } else { \ - store_acc_2x2<1>(a0, Crow, ldc, 0); \ - store_acc_2x2<1>(a1, Crow, ldc, 2); \ - store_acc_2x2<1>(a2, Crow, ldc, 4); \ - store_acc_2x2<1>(a3, Crow, ldc, 6); \ - } \ - } while (0) - -// Perform 4 BFMMLA operations: acc += A @ B for 4 column-pairs -#define BFMMLA_COMPUTE_4(r0, r1, r2, r3, a, b0, b1, b2, b3) \ - do { \ - r0 = vbfmmlaq_f32(r0, a, b0); \ - r1 = vbfmmlaq_f32(r1, a, b1); \ - r2 = vbfmmlaq_f32(r2, a, b2); \ - r3 = vbfmmlaq_f32(r3, a, b3); \ - } while (0) - -// Micro-kernel: updates a small fixed tile using BFMMLA. -// RP = number of row-pairs (1,2,4) -// Computes C[TILE_ROWS*RP, OUTPUT_COLS_PER_BLOCK] += A_packed @ B. -// A_packed interleaves RP row-pairs; B layout is driven by the attention phase: -// - AttentionGemmPhase::QK -> token-column layout (Q @ K^T) -// - AttentionGemmPhase::PV -> token-row layout (P @ V) -// K_static < 0 enables runtime K (PV only) -template -FORCE_INLINE void gemm_rowpairs_x8_bfmmla_neon( - const bfloat16_t* const* __restrict A_packed_rp, - const int32_t* __restrict m_rows_rp, const bfloat16_t* __restrict B_blk, - float* __restrict C, int64_t ldc, bool accumulate, int64_t b_stride, - int32_t K_runtime = 0) { - static_assert(RP == 1 || RP == 2 || RP == 4, "RP must be 1,2,4"); - static_assert(K_static < 0 || K_static % TILE_K == 0, - "K must be divisible by TILE_K"); - static_assert(K_static >= 0 || phase == AttentionGemmPhase::PV, - "Runtime K only supported for PV"); - - constexpr bool runtime_k = (K_static < 0); - const int32_t K_iters = - runtime_k ? (K_runtime / TILE_K) : (K_static / TILE_K); - const int32_t K_tail = runtime_k ? (K_runtime % TILE_K) : 0; - - if (!runtime_k) { - // Help the compiler fold away unused K_runtime when K is compile-time - (void)K_runtime; - } - - auto* C_al = C; - const auto* B_al = B_blk; - // Setup A pointers - const bfloat16_t* a_ptr[4] = { - A_packed_rp[0], - (RP >= 2) ? A_packed_rp[1] : nullptr, - (RP >= 4) ? A_packed_rp[2] : nullptr, - (RP >= 4) ? A_packed_rp[3] : nullptr, - }; - - // Setup B pointers based on layout - const bfloat16_t* b_ptr[4]; - if constexpr (phase == AttentionGemmPhase::PV) { - b_ptr[0] = B_blk + 0 * b_stride; - b_ptr[1] = B_blk + 1 * b_stride; - b_ptr[2] = B_blk + 2 * b_stride; - b_ptr[3] = B_blk + 3 * b_stride; + private: + FORCE_INLINE static float32x4_t zip_low_pairs(const float32x4_t a, + const float32x4_t b) { + return vreinterpretq_f32_f64( + vzip1q_f64(vreinterpretq_f64_f32(a), vreinterpretq_f64_f32(b))); } - float32x4_t acc[4][4]; - -// Initialize accumulators -#define INIT_RP(rp) \ - if constexpr (RP > rp) { \ - INIT_ACC_ROWPAIR_4(acc[rp][0], acc[rp][1], acc[rp][2], acc[rp][3], \ - C_al + (rp * 2) * ldc, ldc, m_rows_rp[rp], accumulate); \ + FORCE_INLINE static float32x4_t zip_high_pairs(const float32x4_t a, + const float32x4_t b) { + return vreinterpretq_f32_f64( + vzip2q_f64(vreinterpretq_f64_f32(a), vreinterpretq_f64_f32(b))); } - INIT_RP(0); - INIT_RP(1); - INIT_RP(2); - INIT_RP(3); -#undef INIT_RP - - // Main compute loop - for (int32_t ki = 0; ki < K_iters; ++ki) { - bfloat16x8_t b0, b1, b2, b3; - if constexpr (phase == AttentionGemmPhase::PV) { - b0 = vld1q_bf16(b_ptr[0] + ki * V_INNER_STRIDE); - b1 = vld1q_bf16(b_ptr[1] + ki * V_INNER_STRIDE); - b2 = vld1q_bf16(b_ptr[2] + ki * V_INNER_STRIDE); - b3 = vld1q_bf16(b_ptr[3] + ki * V_INNER_STRIDE); - } else { - const bfloat16_t* b_base = B_al + ki * b_stride; - b0 = vld1q_bf16(b_base + 0 * V_INNER_STRIDE); - b1 = vld1q_bf16(b_base + 1 * V_INNER_STRIDE); - b2 = vld1q_bf16(b_base + 2 * V_INNER_STRIDE); - b3 = vld1q_bf16(b_base + 3 * V_INNER_STRIDE); + + FORCE_INLINE static void init_accumulators( + float32x4_t& acc01, float32x4_t& acc23, float32x4_t& acc45, + float32x4_t& acc67, const float* __restrict__ c, const int64_t ldc, + const int32_t rows, const bool accumulate) { + if (!accumulate || rows == 0) { + acc01 = vdupq_n_f32(0.0f); + acc23 = vdupq_n_f32(0.0f); + acc45 = vdupq_n_f32(0.0f); + acc67 = vdupq_n_f32(0.0f); + return; } -#define COMPUTE_RP(rp) \ - if constexpr (RP > rp) { \ - bfloat16x8_t a = vld1q_bf16(a_ptr[rp] + ki * PACK_ELEMENTS_PER_K_CHUNK); \ - BFMMLA_COMPUTE_4(acc[rp][0], acc[rp][1], acc[rp][2], acc[rp][3], a, b0, \ - b1, b2, b3); \ - } - COMPUTE_RP(0); - COMPUTE_RP(1); - COMPUTE_RP(2); - COMPUTE_RP(3); -#undef COMPUTE_RP + const float32x4_t row0_0123 = vld1q_f32(c); + const float32x4_t row0_4567 = vld1q_f32(c + 4); + const float32x4_t row1_0123 = + (rows == 2) ? vld1q_f32(c + ldc) : vdupq_n_f32(0.0f); + const float32x4_t row1_4567 = + (rows == 2) ? vld1q_f32(c + ldc + 4) : vdupq_n_f32(0.0f); + + acc01 = zip_low_pairs(row0_0123, row1_0123); + acc23 = zip_high_pairs(row0_0123, row1_0123); + acc45 = zip_low_pairs(row0_4567, row1_4567); + acc67 = zip_high_pairs(row0_4567, row1_4567); } - // K tail for runtime PV: fallback path - if constexpr (runtime_k) { - if (K_tail > 0) { - const int32_t tail_offset = K_iters * V_INNER_STRIDE; - const int32_t a_tail_offset = K_iters * PACK_ELEMENTS_PER_K_CHUNK; - for (int32_t kt = 0; kt < K_tail; ++kt) { - float32x4_t b_vecs[4]; - for (int32_t p = 0; p < 4; ++p) { - const bfloat16_t* bp = b_ptr[p] + tail_offset + kt * TILE_COLS; - const float b0 = vcvtah_f32_bf16(bp[0]); - const float b1 = vcvtah_f32_bf16(bp[1]); - const float32x2_t b_pair = vset_lane_f32(b1, vdup_n_f32(b0), 1); - b_vecs[p] = vcombine_f32(b_pair, b_pair); - } + FORCE_INLINE static void store_accumulators( + const float32x4_t acc01, const float32x4_t acc23, const float32x4_t acc45, + const float32x4_t acc67, float* __restrict__ c, const int64_t ldc, + const int32_t rows) { + if (rows == 0) { + return; + } -#define TAIL_RP(rp) \ - if constexpr (RP > rp) { \ - const bfloat16_t* ap = A_packed_rp[rp] + a_tail_offset; \ - float a_row0 = vcvtah_f32_bf16(ap[kt]); \ - float a_row1 = \ - (m_rows_rp[rp] == 2) ? vcvtah_f32_bf16(ap[kt + TILE_K]) : 0.0f; \ - const float32x4_t a_vec = \ - vcombine_f32(vdup_n_f32(a_row0), vdup_n_f32(a_row1)); \ - for (int32_t p = 0; p < 4; ++p) { \ - acc[rp][p] = vmlaq_f32(acc[rp][p], a_vec, b_vecs[p]); \ - } \ - } - TAIL_RP(0); - TAIL_RP(1); - TAIL_RP(2); - TAIL_RP(3); -#undef TAIL_RP - } + vst1q_f32(c, zip_low_pairs(acc01, acc23)); + vst1q_f32(c + 4, zip_low_pairs(acc45, acc67)); + if (rows == 2) { + vst1q_f32(c + ldc, zip_high_pairs(acc01, acc23)); + vst1q_f32(c + ldc + 4, zip_high_pairs(acc45, acc67)); } } - // Store results -#define STORE_RP(rp) \ - if constexpr (RP > rp) { \ - STORE_ACC_ROWPAIR_4(acc[rp][0], acc[rp][1], acc[rp][2], acc[rp][3], \ - C_al + (rp * 2) * ldc, ldc, m_rows_rp[rp]); \ - } - STORE_RP(0); - STORE_RP(1); - STORE_RP(2); - STORE_RP(3); -#undef STORE_RP -} - -// Meso-kernel: packs a small MBxK slice of A, then tiles over N and calls the -// micro-kernel for each OUTPUT_COLS_PER_BLOCK chunk. K_static < 0 enables -// runtime K (PV only). -template -FORCE_INLINE void gemm_packA_compute_MB_xN( - const c10::BFloat16* __restrict A, const c10::BFloat16* __restrict B, - float* __restrict C, int32_t K_runtime, int64_t lda, int64_t ldc, - int64_t b_layout_stride, int64_t b_reduction_stride, bool accumulate) { - static_assert(MB >= 1 && MB <= 8, "MB must be in [1,8]"); - static_assert(N % OUTPUT_COLS_PER_BLOCK == 0, - "N must be a multiple of OUTPUT_COLS_PER_BLOCK"); - static_assert(K_static < 0 || K_static % TILE_K == 0, - "K must be divisible by TILE_K"); - static_assert(K_static >= 0 || phase == AttentionGemmPhase::PV, - "Runtime K only supported for PV"); - - constexpr bool runtime_k = (K_static < 0); - const int32_t K_val = runtime_k ? K_runtime : K_static; - - // Keep small packs on-stack to avoid heap churn - constexpr int32_t STACK_PACK_STRIDE = - (1024 / TILE_K) * PACK_ELEMENTS_PER_K_CHUNK; - - constexpr int32_t ROW_PAIRS = (MB + 1) / TILE_ROWS; - const int32_t pack_stride = - runtime_k ? ((K_val + TILE_K - 1) / TILE_K) * PACK_ELEMENTS_PER_K_CHUNK - : (K_static / TILE_K) * PACK_ELEMENTS_PER_K_CHUNK; - - alignas(64) c10::BFloat16 A_packed_stack[ROW_PAIRS * STACK_PACK_STRIDE]; - std::vector A_packed_heap; - c10::BFloat16* A_packed = - (pack_stride <= STACK_PACK_STRIDE) - ? A_packed_stack - : (A_packed_heap.resize(ROW_PAIRS * pack_stride), - A_packed_heap.data()); - - for (int32_t rp = 0; rp < ROW_PAIRS; ++rp) { - const int32_t m = rp * TILE_ROWS; - const int32_t m_rows = (m + 1 < MB) ? TILE_ROWS : 1; - const c10::BFloat16* A0 = A + m * lda; - const c10::BFloat16* A1 = (m_rows == TILE_ROWS) ? (A + (m + 1) * lda) : A0; - reshape_Q_2xK_for_bfmmla(A0, A1, A_packed + rp * pack_stride, K_val); + FORCE_INLINE static bfloat16x8_t load_a_pair(const bfloat16_t* __restrict__ a, + const int32_t rows) { + if (rows == 0) { + return vdupq_n_bf16(bfloat16_t{}); + } + // Packed A reserves both rows for an M tail. + return vld1q_bf16(a); } - for (int32_t n = 0; n < N; n += OUTPUT_COLS_PER_BLOCK) { - const c10::BFloat16* B_blk_c10 = - (phase == AttentionGemmPhase::PV) - ? (B + (n / TILE_COLS) * b_layout_stride) - : (B + (n / OUTPUT_COLS_PER_BLOCK) * b_layout_stride); - const bfloat16_t* B_blk = reinterpret_cast(B_blk_c10); - - // Process row-pairs in groups of 4, 2, then 1 - int32_t row_pair_idx = 0; - -#define PROCESS_RP_GROUP(group_size) \ - for (; row_pair_idx + (group_size - 1) < ROW_PAIRS; \ - row_pair_idx += group_size) { \ - const bfloat16_t* Ap[group_size]; \ - int32_t mr[group_size]; \ - for (int32_t i = 0; i < group_size; ++i) { \ - Ap[i] = reinterpret_cast( \ - A_packed + (row_pair_idx + i) * pack_stride); \ - mr[i] = (((row_pair_idx + i) * TILE_ROWS + 1) < MB) ? TILE_ROWS : 1; \ - } \ - float* C_blk = C + (row_pair_idx * TILE_ROWS) * ldc + n; \ - if constexpr (runtime_k) { \ - gemm_rowpairs_x8_bfmmla_neon( \ - Ap, mr, B_blk, C_blk, ldc, accumulate, b_layout_stride, K_val); \ - } else { \ - gemm_rowpairs_x8_bfmmla_neon( \ - Ap, mr, B_blk, C_blk, ldc, accumulate, \ - (phase == AttentionGemmPhase::PV) ? b_layout_stride \ - : b_reduction_stride); \ - } \ - } + FORCE_INLINE static void gemm_4x16(const bfloat16_t* __restrict__ a, + const bfloat16_t* __restrict__ b, + float* __restrict__ c, const int32_t m, + const int32_t k, + const int64_t a_pair_stride, + const int64_t b_n_group_stride, + const int64_t b_k_group_stride, + const int64_t ldc, const bool accumulate) { + const int32_t rows01 = std::min(2, std::max(0, m)); + const int32_t rows23 = std::min(2, std::max(0, m - 2)); + float32x4_t acc0101, acc0123, acc0145, acc0167; + float32x4_t acc2301, acc2323, acc2345, acc2367; + float32x4_t acc0189, acc011011, acc011213, acc011415; + float32x4_t acc2389, acc231011, acc231213, acc231415; + init_accumulators(acc0101, acc0123, acc0145, acc0167, c, ldc, rows01, + accumulate); + init_accumulators(acc2301, acc2323, acc2345, acc2367, c + 2 * ldc, ldc, + rows23, accumulate); + init_accumulators(acc0189, acc011011, acc011213, acc011415, c + 8, ldc, + rows01, accumulate); + init_accumulators(acc2389, acc231011, acc231213, acc231415, c + 2 * ldc + 8, + ldc, rows23, accumulate); + + const bfloat16_t* a01 = a; + const bfloat16_t* a23 = a + a_pair_stride; + const bfloat16_t* b0 = b; + const bfloat16_t* b1 = b + b_n_group_stride; + +#pragma GCC unroll 4 + for (int32_t k_idx = 0; k_idx < k; k_idx += KTile) { + const bfloat16x8_t av01 = load_a_pair(a01, rows01); + const bfloat16x8_t av23 = load_a_pair(a23, rows23); + const bfloat16x8_t b01 = vld1q_bf16(b0); + const bfloat16x8_t b23 = vld1q_bf16(b0 + NTile); + const bfloat16x8_t b45 = vld1q_bf16(b0 + 2 * NTile); + const bfloat16x8_t b67 = vld1q_bf16(b0 + 3 * NTile); + const bfloat16x8_t b89 = vld1q_bf16(b1); + const bfloat16x8_t b1011 = vld1q_bf16(b1 + NTile); + const bfloat16x8_t b1213 = vld1q_bf16(b1 + 2 * NTile); + const bfloat16x8_t b1415 = vld1q_bf16(b1 + 3 * NTile); + + acc0101 = vbfmmlaq_f32(acc0101, av01, b01); + acc2301 = vbfmmlaq_f32(acc2301, av23, b01); + acc0123 = vbfmmlaq_f32(acc0123, av01, b23); + acc2323 = vbfmmlaq_f32(acc2323, av23, b23); + acc0145 = vbfmmlaq_f32(acc0145, av01, b45); + acc2345 = vbfmmlaq_f32(acc2345, av23, b45); + acc0167 = vbfmmlaq_f32(acc0167, av01, b67); + acc2367 = vbfmmlaq_f32(acc2367, av23, b67); + acc0189 = vbfmmlaq_f32(acc0189, av01, b89); + acc2389 = vbfmmlaq_f32(acc2389, av23, b89); + acc011011 = vbfmmlaq_f32(acc011011, av01, b1011); + acc231011 = vbfmmlaq_f32(acc231011, av23, b1011); + acc011213 = vbfmmlaq_f32(acc011213, av01, b1213); + acc231213 = vbfmmlaq_f32(acc231213, av23, b1213); + acc011415 = vbfmmlaq_f32(acc011415, av01, b1415); + acc231415 = vbfmmlaq_f32(acc231415, av23, b1415); + + a01 += 2 * KTile; + a23 += 2 * KTile; + b0 += b_k_group_stride; + b1 += b_k_group_stride; + } - PROCESS_RP_GROUP(4); - PROCESS_RP_GROUP(2); - PROCESS_RP_GROUP(1); -#undef PROCESS_RP_GROUP + store_accumulators(acc0101, acc0123, acc0145, acc0167, c, ldc, rows01); + store_accumulators(acc2301, acc2323, acc2345, acc2367, c + 2 * ldc, ldc, + rows23); + store_accumulators(acc0189, acc011011, acc011213, acc011415, c + 8, ldc, + rows01); + store_accumulators(acc2389, acc231011, acc231213, acc231415, + c + 2 * ldc + 8, ldc, rows23); } -} - -// Macro-kernel: iterates over M in MB={8,4,2,1} chunks. -// Supports compile-time K specialization when K >= 0; otherwise uses runtime K -// (runtime K path is only supported for PV). -template -FORCE_INLINE void gemm_macro_neon_bfmmla( - const c10::BFloat16* __restrict A, const c10::BFloat16* __restrict B, - float* __restrict C, int32_t M, int32_t K_runtime, int64_t lda, int64_t ldc, - int64_t b_layout_stride, int64_t b_reduction_stride, bool accumulate) { - static_assert(N % OUTPUT_COLS_PER_BLOCK == 0, - "N must be a multiple of OUTPUT_COLS_PER_BLOCK"); - - if constexpr (K >= 0) { - static_assert(K % TILE_K == 0, "K must be divisible by TILE_K"); - for (int32_t m = 0; m < M;) { - const int32_t rem = M - m; - const c10::BFloat16* A_blk = A + m * lda; - float* C_blk = C + m * ldc; - -#define DISPATCH_MB(mb) \ - gemm_packA_compute_MB_xN(A_blk, B, C_blk, 0, lda, ldc, \ - b_layout_stride, \ - b_reduction_stride, accumulate) - - if (rem >= 8) { - DISPATCH_MB(8); - m += 8; - } else if (rem >= 4) { - DISPATCH_MB(4); - m += 4; - } else if (rem >= 2) { - DISPATCH_MB(2); - m += 2; - } else { - DISPATCH_MB(1); - m += 1; - } -#undef DISPATCH_MB - } - } else { - static_assert(phase == AttentionGemmPhase::PV, - "Runtime K specialization only supported for PV."); - const int32_t K_val = K_runtime; - - for (int32_t m = 0; m < M;) { - const int32_t rem = M - m; - const c10::BFloat16* A_blk = A + m * lda; - float* C_blk = C + m * ldc; - -#define DISPATCH_MB_RUNTIME(mb) \ - gemm_packA_compute_MB_xN(A_blk, B, C_blk, K_val, lda, ldc, \ - b_layout_stride, \ - b_reduction_stride, accumulate) - - if (rem >= 8) { - DISPATCH_MB_RUNTIME(8); - m += 8; - } else if (rem >= 4) { - DISPATCH_MB_RUNTIME(4); - m += 4; - } else if (rem >= 2) { - DISPATCH_MB_RUNTIME(2); - m += 2; - } else { - DISPATCH_MB_RUNTIME(1); - m += 1; - } -#undef DISPATCH_MB_RUNTIME + + FORCE_INLINE static void gemm_8x8(const bfloat16_t* __restrict__ a, + const bfloat16_t* __restrict__ b, + float* __restrict__ c, const int32_t m, + const int32_t k, + const int64_t a_pair_stride, + const int64_t b_k_group_stride, + const int64_t ldc, const bool accumulate) { + const int32_t rows01 = std::min(2, std::max(0, m)); + const int32_t rows23 = std::min(2, std::max(0, m - 2)); + const int32_t rows45 = std::min(2, std::max(0, m - 4)); + const int32_t rows67 = std::min(2, std::max(0, m - 6)); + float32x4_t acc0101, acc0123, acc0145, acc0167; + float32x4_t acc2301, acc2323, acc2345, acc2367; + float32x4_t acc4501, acc4523, acc4545, acc4567; + float32x4_t acc6701, acc6723, acc6745, acc6767; + init_accumulators(acc0101, acc0123, acc0145, acc0167, c, ldc, rows01, + accumulate); + init_accumulators(acc2301, acc2323, acc2345, acc2367, c + 2 * ldc, ldc, + rows23, accumulate); + init_accumulators(acc4501, acc4523, acc4545, acc4567, c + 4 * ldc, ldc, + rows45, accumulate); + init_accumulators(acc6701, acc6723, acc6745, acc6767, c + 6 * ldc, ldc, + rows67, accumulate); + + const bfloat16_t* a01 = a; + const bfloat16_t* a23 = a + a_pair_stride; + const bfloat16_t* a45 = a + 2 * a_pair_stride; + const bfloat16_t* a67 = a + 3 * a_pair_stride; + const bfloat16_t* b_ptr = b; + +#pragma GCC unroll 4 + for (int32_t k_idx = 0; k_idx < k; k_idx += KTile) { + const bfloat16x8_t av01 = load_a_pair(a01, rows01); + const bfloat16x8_t av23 = load_a_pair(a23, rows23); + const bfloat16x8_t av45 = load_a_pair(a45, rows45); + const bfloat16x8_t av67 = load_a_pair(a67, rows67); + const bfloat16x8_t b01 = vld1q_bf16(b_ptr); + const bfloat16x8_t b23 = vld1q_bf16(b_ptr + NTile); + const bfloat16x8_t b45 = vld1q_bf16(b_ptr + 2 * NTile); + const bfloat16x8_t b67 = vld1q_bf16(b_ptr + 3 * NTile); + + acc0101 = vbfmmlaq_f32(acc0101, av01, b01); + acc2301 = vbfmmlaq_f32(acc2301, av23, b01); + acc4501 = vbfmmlaq_f32(acc4501, av45, b01); + acc6701 = vbfmmlaq_f32(acc6701, av67, b01); + acc0123 = vbfmmlaq_f32(acc0123, av01, b23); + acc2323 = vbfmmlaq_f32(acc2323, av23, b23); + acc4523 = vbfmmlaq_f32(acc4523, av45, b23); + acc6723 = vbfmmlaq_f32(acc6723, av67, b23); + acc0145 = vbfmmlaq_f32(acc0145, av01, b45); + acc2345 = vbfmmlaq_f32(acc2345, av23, b45); + acc4545 = vbfmmlaq_f32(acc4545, av45, b45); + acc6745 = vbfmmlaq_f32(acc6745, av67, b45); + acc0167 = vbfmmlaq_f32(acc0167, av01, b67); + acc2367 = vbfmmlaq_f32(acc2367, av23, b67); + acc4567 = vbfmmlaq_f32(acc4567, av45, b67); + acc6767 = vbfmmlaq_f32(acc6767, av67, b67); + + a01 += 2 * KTile; + a23 += 2 * KTile; + a45 += 2 * KTile; + a67 += 2 * KTile; + b_ptr += b_k_group_stride; } + + store_accumulators(acc0101, acc0123, acc0145, acc0167, c, ldc, rows01); + store_accumulators(acc2301, acc2323, acc2345, acc2367, c + 2 * ldc, ldc, + rows23); + store_accumulators(acc4501, acc4523, acc4545, acc4567, c + 4 * ldc, ldc, + rows45); + store_accumulators(acc6701, acc6723, acc6745, acc6767, c + 6 * ldc, ldc, + rows67); } -} +}; + +namespace { -#undef INIT_ACC_ROWPAIR_4 -#undef STORE_ACC_ROWPAIR_4 -#undef BFMMLA_COMPUTE_4 +constexpr int32_t TILE_K = BfmmlaGemm::KTile; +constexpr int32_t TILE_COLS = 2; +constexpr int32_t OUTPUT_COLS_PER_BLOCK = BfmmlaGemm::NTile; +constexpr int32_t K_TOKENS_PER_GROUP = 8; +constexpr int32_t V_TOKENS_PER_ROW_BLOCK = 4; +constexpr int32_t K_CACHE_K_GROUP_STRIDE = K_TOKENS_PER_GROUP * TILE_K; +constexpr int32_t B_COL_PAIR_STRIDE = V_TOKENS_PER_ROW_BLOCK * TILE_COLS; } // namespace -// TileGemm Adapter for Attention - template class TileGemmNEONBFMMLA { public: @@ -443,49 +286,31 @@ class TileGemmNEONBFMMLA { [[maybe_unused]] const int32_t block_size, [[maybe_unused]] const int32_t dynamic_k_size, const bool accum_c) { - static_assert(BlockTokens % OUTPUT_COLS_PER_BLOCK == 0); - // BFMMLA kernels require compile-time head_dim; keep head_dim_ct only for - // API parity with other tile_gemm implementations. + static_assert(BlockTokens % 16 == 0); if constexpr (head_dim_ct >= 0) { - static_assert(head_dim_ct == HeadDim, - "BFMMLA expects head_dim_ct to match HeadDim; PV passes " - "-1 for API parity."); + static_assert(head_dim_ct == HeadDim); } + const auto* a = reinterpret_cast(a_tile); if constexpr (phase == AttentionGemmPhase::QK) { - const int64_t b_reduction_stride = K_INNER_STRIDE; - const int64_t b_token_block_stride = (HeadDim / TILE_K) * K_INNER_STRIDE; - - gemm_macro_neon_bfmmla( - reinterpret_cast(a_tile), b_tile, c_tile, - m_size, 0, lda, ldc, b_token_block_stride, b_reduction_stride, - accum_c); + constexpr int64_t b_n_group_stride = + (HeadDim / BfmmlaGemm::KTile) * K_CACHE_K_GROUP_STRIDE; + + for (int32_t row = 0; row < m_size; row += BfmmlaGemm::MaxRows) { + const int32_t panel_m = std::min(BfmmlaGemm::MaxRows, m_size - row); + BfmmlaGemm::gemm(a + row * HeadDim, b_tile, c_tile + row * ldc, panel_m, + BlockTokens, HeadDim, 2 * HeadDim, b_n_group_stride, + K_CACHE_K_GROUP_STRIDE, ldc, accum_c); + } } else { - const int64_t b_pair_stride = - (block_size / V_TOKENS_PER_ROW_BLOCK) * V_INNER_STRIDE; - - // PV gemm with runtime K specialization - switch (dynamic_k_size) { - case 32: - gemm_macro_neon_bfmmla( - reinterpret_cast(a_tile), b_tile, c_tile, - m_size, 32, lda, ldc, b_pair_stride, 0, accum_c); - break; - case 128: - gemm_macro_neon_bfmmla( - reinterpret_cast(a_tile), b_tile, c_tile, - m_size, 128, lda, ldc, b_pair_stride, 0, accum_c); - break; - case 256: - gemm_macro_neon_bfmmla( - reinterpret_cast(a_tile), b_tile, c_tile, - m_size, 256, lda, ldc, b_pair_stride, 0, accum_c); - break; - default: - gemm_macro_neon_bfmmla( - reinterpret_cast(a_tile), b_tile, c_tile, - m_size, dynamic_k_size, lda, ldc, b_pair_stride, 0, accum_c); - break; + const int64_t b_n_group_stride = + (block_size / V_TOKENS_PER_ROW_BLOCK) * K_CACHE_K_GROUP_STRIDE; + + for (int32_t row = 0; row < m_size; row += BfmmlaGemm::MaxRows) { + const int32_t panel_m = std::min(BfmmlaGemm::MaxRows, m_size - row); + BfmmlaGemm::gemm(a + row * lda, b_tile, c_tile + row * ldc, panel_m, + HeadDim, dynamic_k_size, 2 * lda, b_n_group_stride, + K_CACHE_K_GROUP_STRIDE, ldc, accum_c); } } } @@ -512,9 +337,11 @@ class AttentionImplNEONBFMMLA { static constexpr int64_t HeadDim = head_dim; static constexpr ISA ISAType = isa_type; static constexpr bool scale_on_logits = false; + static constexpr int64_t VCacheNGroup = OUTPUT_COLS_PER_BLOCK; + static constexpr int64_t VCacheKGroupStride = VCacheNGroup * TILE_K; - static_assert(HeadDim % OUTPUT_COLS_PER_BLOCK == 0); - static_assert(BlockSizeAlignment % OUTPUT_COLS_PER_BLOCK == 0); + static_assert(HeadDim % (2 * OUTPUT_COLS_PER_BLOCK) == 0); + static_assert(BlockSizeAlignment % K_TOKENS_PER_GROUP == 0); static_assert(HeadDim % TILE_K == 0, "HeadDim must be a multiple of TILE_K"); public: @@ -527,19 +354,36 @@ class AttentionImplNEONBFMMLA { attention_iteration(CPU_ATTENTION_PARAMS); } + struct ProbabilityTokenStore { + static constexpr int32_t TokenStride = 2; + + FORCE_INLINE static void store_probabilities( + c10::BFloat16* __restrict__ probability, + const vec_op::FP32Vec16& values, const int32_t row, + const int64_t row_stride) { + const int32_t row_in_pair = row & 1; + auto* dst = reinterpret_cast( + probability + row_in_pair * (BfmmlaGemm::KTile - row_stride)); + vst1_bf16(dst, vcvt_bf16_f32(values.reg.val[0])); + vst1_bf16(dst + 2 * BfmmlaGemm::KTile, vcvt_bf16_f32(values.reg.val[1])); + vst1_bf16(dst + 4 * BfmmlaGemm::KTile, vcvt_bf16_f32(values.reg.val[2])); + vst1_bf16(dst + 6 * BfmmlaGemm::KTile, vcvt_bf16_f32(values.reg.val[3])); + } + }; + // Key cache stride per token group (TokenColumn layout; QK) static constexpr int64_t k_cache_token_group_stride( [[maybe_unused]] const int32_t block_size) { static_assert(BlockSizeAlignment % K_TOKENS_PER_GROUP == 0); return (BlockSizeAlignment / K_TOKENS_PER_GROUP) * - ((head_dim / TILE_K) * K_INNER_STRIDE); + ((head_dim / TILE_K) * K_CACHE_K_GROUP_STRIDE); } // Value cache stride per token group (TokenRow layout; PV) static constexpr int64_t v_cache_token_group_stride( [[maybe_unused]] const int32_t block_size) { static_assert(BlockSizeAlignment % V_TOKENS_PER_ROW_BLOCK == 0); - return (BlockSizeAlignment / V_TOKENS_PER_ROW_BLOCK) * V_INNER_STRIDE; + return (BlockSizeAlignment / V_TOKENS_PER_ROW_BLOCK) * VCacheKGroupStride; } // The stride to move to the "next" head_dim group @@ -550,7 +394,7 @@ class AttentionImplNEONBFMMLA { return head_dim * block_size; } - // Convert Q heads to BF16 and apply scale factor using native BF16 intrinsics + // Scale Q and write row pairs in BFMMLA reduction order. static void copy_q_heads_tile(c10::BFloat16* __restrict__ src, c10::BFloat16* __restrict__ q_buffer, const int32_t q_num, @@ -559,25 +403,42 @@ class AttentionImplNEONBFMMLA { const int64_t q_head_stride, float scale) { constexpr int32_t dim = static_cast(head_dim); const float32x4_t scale_vec = vdupq_n_f32(scale); - - for (int32_t qi = 0; qi < q_num; ++qi) { - for (int32_t hi = 0; hi < q_heads_per_kv; ++hi) { - c10::BFloat16* __restrict__ curr_q = - src + qi * q_num_stride + hi * q_head_stride; - c10::BFloat16* __restrict__ dst = - q_buffer + qi * q_heads_per_kv * head_dim + hi * head_dim; - - for (int32_t i = 0; i < dim; i += OUTPUT_COLS_PER_BLOCK) { - bfloat16x8_t in8 = - vld1q_bf16(reinterpret_cast(curr_q + i)); - float32x4_t lo = vmulq_f32(vcvtq_low_f32_bf16(in8), scale_vec); - float32x4_t hi = vmulq_f32(vcvtq_high_f32_bf16(in8), scale_vec); - - bfloat16x4_t lo_b = vcvt_bf16_f32(lo); - bfloat16x4_t hi_b = vcvt_bf16_f32(hi); - bfloat16x8_t out = vcombine_bf16(lo_b, hi_b); - vst1q_bf16(reinterpret_cast(dst + i), out); + const bfloat16x4_t zero = vdup_n_bf16(bfloat16_t{}); + const int32_t row_num = q_num * q_heads_per_kv; + + for (int32_t row = 0; row < row_num; row += 2) { + const int32_t q0 = row / q_heads_per_kv; + const int32_t h0 = row % q_heads_per_kv; + const auto* row0 = reinterpret_cast( + src + q0 * q_num_stride + h0 * q_head_stride); + const bool has_row1 = row + 1 < row_num; + const int32_t q1 = (row + 1) / q_heads_per_kv; + const int32_t h1 = (row + 1) % q_heads_per_kv; + const auto* row1 = has_row1 + ? reinterpret_cast( + src + q1 * q_num_stride + h1 * q_head_stride) + : nullptr; + auto* dst = reinterpret_cast(q_buffer + row * head_dim); + + for (int32_t k = 0; k < dim; k += OUTPUT_COLS_PER_BLOCK) { + const bfloat16x8_t in0 = vld1q_bf16(row0 + k); + const bfloat16x4_t out0_lo = + vcvt_bf16_f32(vmulq_f32(vcvtq_low_f32_bf16(in0), scale_vec)); + const bfloat16x4_t out0_hi = + vcvt_bf16_f32(vmulq_f32(vcvtq_high_f32_bf16(in0), scale_vec)); + + bfloat16x4_t out1_lo = zero; + bfloat16x4_t out1_hi = zero; + if (has_row1) { + const bfloat16x8_t in1 = vld1q_bf16(row1 + k); + out1_lo = + vcvt_bf16_f32(vmulq_f32(vcvtq_low_f32_bf16(in1), scale_vec)); + out1_hi = + vcvt_bf16_f32(vmulq_f32(vcvtq_high_f32_bf16(in1), scale_vec)); } + + vst1q_bf16(dst + 2 * k, vcombine_bf16(out0_lo, out1_lo)); + vst1q_bf16(dst + 2 * k + 8, vcombine_bf16(out0_hi, out1_hi)); } } } @@ -585,11 +446,12 @@ class AttentionImplNEONBFMMLA { public: // Reshape and cache K/V into BFMMLA-optimized layouts // K cache: - // [block_size/K_TOKENS_PER_GROUP][head_dim/TILE_K][K_INNER_STRIDE] + // [block_size/K_TOKENS_PER_GROUP][head_dim/TILE_K] + // [K_CACHE_K_GROUP_STRIDE] // - TokenColumn // V cache: - // [head_dim/TILE_COLS][block_size/V_TOKENS_PER_ROW_BLOCK][V_INNER_STRIDE] - // - TokenRows + // [head_dim/VCacheNGroup][block_size/V_TOKENS_PER_ROW_BLOCK] + // [VCacheKGroupStride] static void reshape_and_cache( const c10::BFloat16* __restrict__ key, const c10::BFloat16* __restrict__ value, @@ -604,13 +466,13 @@ class AttentionImplNEONBFMMLA { const int64_t block_size, [[maybe_unused]] const int64_t block_size_stride, const float /*k_inv*/ = 0.0f, const float /*v_inv*/ = 0.0f) { - const int64_t k_block_stride = (head_dim / TILE_K) * K_INNER_STRIDE; - const int64_t v_pair_stride = - (block_size / V_TOKENS_PER_ROW_BLOCK) * V_INNER_STRIDE; + const int64_t k_block_stride = (head_dim / TILE_K) * K_CACHE_K_GROUP_STRIDE; + const int64_t v_n_group_stride = + (block_size / V_TOKENS_PER_ROW_BLOCK) * VCacheKGroupStride; -#pragma omp parallel for - for (int64_t head_idx = 0; head_idx < head_num; ++head_idx) { - for (int64_t token_idx = 0; token_idx < token_num; ++token_idx) { +#pragma omp parallel for collapse(2) + for (int64_t token_idx = 0; token_idx < token_num; ++token_idx) { + for (int64_t head_idx = 0; head_idx < head_num; ++head_idx) { const int64_t pos = slot_mapping[token_idx]; if (pos < 0) continue; @@ -636,12 +498,12 @@ class AttentionImplNEONBFMMLA { key_base + block_in_block * k_block_stride; for (int64_t hd4 = 0; hd4 < head_dim / TILE_K; ++hd4) { - uint16_t* dst_u16 = reinterpret_cast( - block_base + hd4 * K_INNER_STRIDE + - pair_in_block * V_INNER_STRIDE + lane_base); - const uint16_t* src_u16 = - reinterpret_cast(key_src + hd4 * TILE_K); - vst1_u16(dst_u16, vld1_u16(src_u16)); + bfloat16_t* dst = reinterpret_cast( + block_base + hd4 * K_CACHE_K_GROUP_STRIDE + + pair_in_block * B_COL_PAIR_STRIDE + lane_base); + const bfloat16_t* src = + reinterpret_cast(key_src + hd4 * TILE_K); + vst1_bf16(dst, vld1_bf16(src)); } } @@ -655,22 +517,33 @@ class AttentionImplNEONBFMMLA { value_cache + block_idx * num_blocks_stride + head_idx * cache_head_num_stride; - const int64_t row_block = block_offset / V_TOKENS_PER_ROW_BLOCK; + const int64_t token_group = block_offset / V_TOKENS_PER_ROW_BLOCK; const int64_t lane = block_offset & (V_TOKENS_PER_ROW_BLOCK - 1); - c10::BFloat16* __restrict row_block_base = - value_base + row_block * V_INNER_STRIDE; - - for (int64_t hd2 = 0; hd2 < head_dim / TILE_COLS; ++hd2) { - c10::BFloat16* __restrict dst_val = - row_block_base + hd2 * v_pair_stride; - - const uint16_t* src_u16 = - reinterpret_cast(value_src); - uint16_t* dst_u16 = reinterpret_cast(dst_val); - dst_u16[lane] = src_u16[hd2 * TILE_COLS + 0]; - dst_u16[lane + V_TOKENS_PER_ROW_BLOCK] = - src_u16[hd2 * TILE_COLS + 1]; + const auto* src = reinterpret_cast(value_src); + auto* dst = reinterpret_cast(value_base); + for (int64_t hd8 = 0; hd8 < head_dim / OUTPUT_COLS_PER_BLOCK; ++hd8) { + const bfloat16x8_t values = + vld1q_bf16(src + hd8 * OUTPUT_COLS_PER_BLOCK); + const bfloat16x4_t low = vget_low_bf16(values); + const bfloat16x4_t high = vget_high_bf16(values); + bfloat16_t* group = + dst + hd8 * v_n_group_stride + token_group * VCacheKGroupStride; + + vst1_lane_bf16(group + lane, low, 0); + vst1_lane_bf16(group + V_TOKENS_PER_ROW_BLOCK + lane, low, 1); + vst1_lane_bf16(group + B_COL_PAIR_STRIDE + lane, low, 2); + vst1_lane_bf16( + group + B_COL_PAIR_STRIDE + V_TOKENS_PER_ROW_BLOCK + lane, low, + 3); + vst1_lane_bf16(group + 2 * B_COL_PAIR_STRIDE + lane, high, 0); + vst1_lane_bf16( + group + 2 * B_COL_PAIR_STRIDE + V_TOKENS_PER_ROW_BLOCK + lane, + high, 1); + vst1_lane_bf16(group + 3 * B_COL_PAIR_STRIDE + lane, high, 2); + vst1_lane_bf16( + group + 3 * B_COL_PAIR_STRIDE + V_TOKENS_PER_ROW_BLOCK + lane, + high, 3); } } } @@ -680,4 +553,4 @@ class AttentionImplNEONBFMMLA { } // namespace cpu_attention -#endif // CPU_ATTN_ASIMD_BFMMLA_HPP +#endif // CPU_ATTN_NEON_BFMMLA_HPP diff --git a/csrc/cpu/cpu_fused_moe_int8.cpp b/csrc/cpu/cpu_fused_moe_int8.cpp index 649317b4786a..27d0f6a7d539 100644 --- a/csrc/cpu/cpu_fused_moe_int8.cpp +++ b/csrc/cpu/cpu_fused_moe_int8.cpp @@ -26,10 +26,23 @@ #define NEON_DISPATCH(SCALAR_TYPE, ...) case cpu_utils::ISA::NEON: #endif +#if defined(__powerpc64__) + #include "cpu/micro_gemm/cpu_micro_gemm_int8_vsx.hpp" + #define VSX_DISPATCH(SCALAR_TYPE, ...) \ + case cpu_utils::ISA::VSX: { \ + using gemm_t = \ + cpu_micro_gemm::MicroGemmINT8; \ + return __VA_ARGS__(); \ + } +#else + #define VSX_DISPATCH(SCALAR_TYPE, ...) case cpu_utils::ISA::VSX: +#endif + #define CPU_INT8_ISA_DISPATCH_IMPL(ISA_TYPE, SCALAR_TYPE, ...) \ [&] { \ switch (ISA_TYPE) { \ NEON_DISPATCH(SCALAR_TYPE, __VA_ARGS__) \ + VSX_DISPATCH(SCALAR_TYPE, __VA_ARGS__) \ default: { \ TORCH_CHECK(false, "Invalid CPU ISA type."); \ } \ @@ -81,9 +94,9 @@ void fused_moe_int8_impl( const int32_t thread_num = cpu_utils::get_max_threads(); const int32_t w13_input_buffer_size = cpu_utils::round_up<64>( - gemm_m_tile_size * input_size_13 * sizeof(int8_t)); - const int32_t w2_input_buffer_size = - cpu_utils::round_up<64>(gemm_m_tile_size * input_size_2 * sizeof(int8_t)); + (gemm_m_tile_size * input_size_13 + 64) * sizeof(int8_t)); + const int32_t w2_input_buffer_size = cpu_utils::round_up<64>( + (gemm_m_tile_size * input_size_2 + 64) * sizeof(int8_t)); const int32_t w13_n_tile_size = [&]() { const int64_t cache_size = cpu_utils::get_available_l2_size(); diff --git a/csrc/cpu/cpu_isa.cpp b/csrc/cpu/cpu_isa.cpp new file mode 100644 index 000000000000..1c4a8faf1918 --- /dev/null +++ b/csrc/cpu/cpu_isa.cpp @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright contributors to the vLLM project + +// AMX-FP8 is reported at CPUID leaf 0x1E, subleaf 1, EAX[4]. +#if defined(__x86_64__) || defined(_M_X64) + #include + +bool runtime_has_amx_fp8() { + unsigned int eax = 0, ebx = 0, ecx = 0, edx = 0; + if (__get_cpuid_max(0, nullptr) < 0x1e) return false; + if (__get_cpuid_count(0x1e, 1, &eax, &ebx, &ecx, &edx)) { + return (eax >> 4) & 1u; + } + return false; +} + +bool cpu_has_amx_fp8() { return runtime_has_amx_fp8(); } +#else +bool cpu_has_amx_fp8() { return false; } +#endif diff --git a/csrc/cpu/cpu_types_vsx.hpp b/csrc/cpu/cpu_types_vsx.hpp index d46906130e92..eaea921d98b8 100644 --- a/csrc/cpu/cpu_types_vsx.hpp +++ b/csrc/cpu/cpu_types_vsx.hpp @@ -13,10 +13,10 @@ namespace vec_op { struct fp8_e4m3_tag {}; struct fp8_e5m2_tag {}; -#define VLLM_DISPATCH_CASE_FLOATING_TYPES(...) \ - AT_DISPATCH_CASE(at::ScalarType::Float, __VA_ARGS__) \ - AT_DISPATCH_CASE(at::ScalarType::BFloat16, __VA_ARGS__) \ - AT_DISPATCH_CASE(at::ScalarType::Half, __VA_ARGS__) +#define VLLM_DISPATCH_CASE_FLOATING_TYPES(...) \ + AT_DISPATCH_CASE(at::ScalarType::Float, __VA_ARGS__) \ + AT_DISPATCH_CASE(at::ScalarType::BFloat16, __VA_ARGS__) +// Note: FP16 (Half) is not supported on POWER VSX — no FP16Vec16 type. #define VLLM_DISPATCH_FLOATING_TYPES(TYPE, NAME, ...) \ AT_DISPATCH_SWITCH(TYPE, NAME, VLLM_DISPATCH_CASE_FLOATING_TYPES(__VA_ARGS__)) @@ -416,6 +416,7 @@ struct FP32Vec8 : public Vec { tmp.val[0] = two_x; tmp.val[1] = two_x; FP32Vec8 temp_vec(tmp); + FP32Vec8 exp_vec = temp_vec.exp(); vector float e = temp_vec.exp().reg.val[0]; vector float num = vec_sub(e, one); @@ -634,6 +635,16 @@ struct FP32Vec16 : public Vec { reg.val[3] = vec_ctf(v.reg.val[3], 0); } + explicit FP32Vec16(const float* __restrict__ base, const INT32Vec16& index) { + INT32Vec16::AliasReg idx; + idx.reg = index.reg; + AliasReg ar; + for (int i = 0; i < VEC_ELEM_NUM; ++i) { + ar.values[i] = base[idx.values[i]]; + } + reg = ar.reg; + } + FP32Vec16 operator*(const FP32Vec16& b) const { return FP32Vec16(f32x4x4_t({vec_mul(reg.val[0], b.reg.val[0]), vec_mul(reg.val[1], b.reg.val[1]), @@ -648,6 +659,13 @@ struct FP32Vec16 : public Vec { vec_add(reg.val[3], b.reg.val[3])})); } + FP32Vec16 operator-() const { + const __vector float zero = vec_splats(0.0f); + return FP32Vec16( + f32x4x4_t({vec_sub(zero, reg.val[0]), vec_sub(zero, reg.val[1]), + vec_sub(zero, reg.val[2]), vec_sub(zero, reg.val[3])})); + } + FP32Vec16 operator-(const FP32Vec16& b) const { return FP32Vec16(f32x4x4_t({vec_sub(reg.val[0], b.reg.val[0]), vec_sub(reg.val[1], b.reg.val[1]), @@ -655,11 +673,6 @@ struct FP32Vec16 : public Vec { vec_sub(reg.val[3], b.reg.val[3])})); } - FP32Vec16 operator-() const { - return FP32Vec16( - f32x4x4_t({-reg.val[0], -reg.val[1], -reg.val[2], -reg.val[3]})); - } - FP32Vec16 operator/(const FP32Vec16& b) const { return FP32Vec16(f32x4x4_t({vec_div(reg.val[0], b.reg.val[0]), vec_div(reg.val[1], b.reg.val[1]), diff --git a/csrc/cpu/dnnl_helper.cpp b/csrc/cpu/dnnl_helper.cpp index 14c136dcbbf0..1ac0401f540d 100644 --- a/csrc/cpu/dnnl_helper.cpp +++ b/csrc/cpu/dnnl_helper.cpp @@ -172,7 +172,8 @@ struct hash { size_t operator()( const MatMulPrimitiveHandler::ClassMatmulCacheKey& val) const { return hash()(val.b_n_size) ^ hash()(val.b_k_size) ^ - hash()(static_cast(val.b_type)); + hash()(static_cast(val.b_type)) ^ + hash()(static_cast(val.c_type)); } }; @@ -202,7 +203,7 @@ bool operator==(const W8A8MatMulPrimitiveHandler::MSizeCacheKey& l, bool operator==(const MatMulPrimitiveHandler::ClassMatmulCacheKey& l, const MatMulPrimitiveHandler::ClassMatmulCacheKey& r) { return l.b_n_size == r.b_n_size && l.b_k_size == r.b_k_size && - l.b_type == r.b_type; + l.b_type == r.b_type && l.c_type == r.c_type; } bool operator==(const MatMulPrimitiveHandler::MSizeCacheKey& l, @@ -482,8 +483,10 @@ void MatMulPrimitiveHandler::execute(ExecArgs& args) { dnnl::matmul MatMulPrimitiveHandler::get_matmul_cache( const MSizeCacheKey& key) { if (m_size_cache_.get() == nullptr) { - ClassMatmulCacheKey class_key = { - .b_n_size = b_n_size_, .b_k_size = b_k_size_, .b_type = b_type_}; + ClassMatmulCacheKey class_key = {.b_n_size = b_n_size_, + .b_k_size = b_k_size_, + .b_type = b_type_, + .c_type = c_type_}; m_size_cache_ = get_matul_class_primitive_cache(class_key, primitive_cache_size_); } diff --git a/csrc/cpu/dnnl_helper.h b/csrc/cpu/dnnl_helper.h index b841ed73058e..05fb116ecd1e 100644 --- a/csrc/cpu/dnnl_helper.h +++ b/csrc/cpu/dnnl_helper.h @@ -176,6 +176,7 @@ class MatMulPrimitiveHandler : public DNNLMatMulPrimitiveHandler { dnnl_dim_t b_n_size; dnnl_dim_t b_k_size; dnnl::memory::data_type b_type; + dnnl::memory::data_type c_type; friend bool operator==(const ClassMatmulCacheKey& l, const ClassMatmulCacheKey& r); @@ -203,6 +204,8 @@ class MatMulPrimitiveHandler : public DNNLMatMulPrimitiveHandler { public: MatMulPrimitiveHandler(const Args& args); + dnnl::memory::data_type get_output_type() const { return c_type_; } + void execute(ExecArgs& args); private: diff --git a/csrc/cpu/dnnl_kernels.cpp b/csrc/cpu/dnnl_kernels.cpp index 6dda09296160..e81d365628c0 100644 --- a/csrc/cpu/dnnl_kernels.cpp +++ b/csrc/cpu/dnnl_kernels.cpp @@ -513,6 +513,17 @@ int64_t create_onednn_mm_handler(const torch::Tensor& b, args.ab_type = get_dnnl_type(); }); +#ifdef VLLM_USE_ACL + // TODO(fadara01): this is a hot-fix, remove once problem is addressed in the + // AArch64 stack. hybrid kernels in ACL which are the fastest for LM head + // layers are only enabled for BF16 x BF16 -> FP32 problem desc as they can't + // convert FP32->BF16 internally. In this case do: oneDNN matmul with + // bf16:bf16:fp32 then cast output down to bf16. + if (b.scalar_type() == at::kBFloat16 && b.size(1) >= 64000) { + args.c_type = dnnl::memory::data_type::f32; + } +#endif + return reinterpret_cast(new MatMulPrimitiveHandler(args)); } @@ -527,12 +538,22 @@ void onednn_mm(torch::Tensor& c, // [M, OC], row-major MatMulPrimitiveHandler* ptr = reinterpret_cast(handler_tensor.item()); + torch::Tensor dnnl_output = c; + // AArch64 specific case where we do a bf16 x bf16 -> fp32 matmul + const bool convert_output = + ptr->get_output_type() == dnnl::memory::data_type::f32 && + c.scalar_type() != at::kFloat; + if (convert_output) { + dnnl_output = torch::empty(c.sizes(), c.options().dtype(at::kFloat)); + } + // ACL matmuls expect contiguous source tensors #ifdef VLLM_USE_ACL torch::Tensor a_contig = a.contiguous(); #endif MatMulPrimitiveHandler::ExecArgs exec_args; + exec_args.c_ptr = dnnl_output.data_ptr(); #ifdef VLLM_USE_ACL exec_args.a_m_size = a_contig.size(0); @@ -548,7 +569,7 @@ void onednn_mm(torch::Tensor& c, // [M, OC], row-major #ifdef VLLM_USE_ACL // ACL matmuls in oneDNN do not support a bias. // We handle a matmul with bias by doing: c = bias; c += matmul(a, b) - c.copy_(bias.value()); + dnnl_output.copy_(bias.value()); #else exec_args.bias_ptr = bias->data_ptr(); #endif @@ -563,8 +584,11 @@ void onednn_mm(torch::Tensor& c, // [M, OC], row-major exec_args.a_ptr = a.data_ptr(); #endif - exec_args.c_ptr = c.data_ptr(); ptr->execute(exec_args); }); + + if (convert_output) { + c.copy_(dnnl_output); + } } diff --git a/csrc/cpu/generate_cpu_attn_dispatch.py b/csrc/cpu/generate_cpu_attn_dispatch.py index a82a27b844a4..12a14ef58774 100644 --- a/csrc/cpu/generate_cpu_attn_dispatch.py +++ b/csrc/cpu/generate_cpu_attn_dispatch.py @@ -1,9 +1,7 @@ #!/usr/bin/env python3 # SPDX-License-Identifier: Apache-2.0 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project -""" -Generate CPU attention dispatch switch cases and kernel instantiations. -""" +"""Generate CPU attention dispatch switch cases and kernel instantiations.""" import os diff --git a/csrc/cpu/micro_gemm/cpu_micro_gemm_int8_vsx.hpp b/csrc/cpu/micro_gemm/cpu_micro_gemm_int8_vsx.hpp new file mode 100644 index 000000000000..60333537281d --- /dev/null +++ b/csrc/cpu/micro_gemm/cpu_micro_gemm_int8_vsx.hpp @@ -0,0 +1,287 @@ +#ifndef CPU_MICRO_GEMM_INT8_VSX_HPP +#define CPU_MICRO_GEMM_INT8_VSX_HPP + +#include +#include +#include +#include +#include + +#include "cpu/micro_gemm/cpu_micro_gemm_impl.hpp" + +#include +#include +#include + +namespace cpu_micro_gemm { + +namespace vsx_int8 { + +constexpr int32_t K = 4; +constexpr int32_t Mr = 4; +constexpr int32_t Nr = 32; + +// --------------------------------------------------------------------------- +// 8-Accumulator MMA 4x32 micro-GEMM (POWER10 xvi8ger4pp). +// +// Computes 4 rows x 32 columns (128 outputs) using all 8 hardware accumulators. +// Operates on pre-biased uint8 weights (B + 128) and subtracts 128 * a_sums. +// Works for all m in [1, 4] with zero scalar loop overhead. +// --------------------------------------------------------------------------- +__attribute__((target("cpu=power10"))) FORCE_INLINE void gemm_mma_4x32( + const int8_t* __restrict__ a_packed, const uint8_t* __restrict__ b_panel, + const int32_t* __restrict__ a_sums, int32_t* __restrict__ c_ptr, + const int32_t m, const int32_t k_size, const int64_t ldc) { + TORCH_CHECK(m > 0 && m <= Mr); + TORCH_CHECK(k_size % K == 0); + + __vector_quad acc0, acc1, acc2, acc3, acc4, acc5, acc6, acc7; + __builtin_mma_xxsetaccz(&acc0); + __builtin_mma_xxsetaccz(&acc1); + __builtin_mma_xxsetaccz(&acc2); + __builtin_mma_xxsetaccz(&acc3); + __builtin_mma_xxsetaccz(&acc4); + __builtin_mma_xxsetaccz(&acc5); + __builtin_mma_xxsetaccz(&acc6); + __builtin_mma_xxsetaccz(&acc7); + + const int8_t* __restrict__ ap = a_packed; + const uint8_t* __restrict__ bp = b_panel; + +#pragma GCC unroll 4 + for (int32_t k = 0; k < k_size; k += 4) { + const __vector unsigned char avec = + vec_xl(0, reinterpret_cast(ap)); + __builtin_mma_xvi8ger4pp(&acc0, avec, vec_xl(0, bp)); + __builtin_mma_xvi8ger4pp(&acc1, avec, vec_xl(16, bp)); + __builtin_mma_xvi8ger4pp(&acc2, avec, vec_xl(32, bp)); + __builtin_mma_xvi8ger4pp(&acc3, avec, vec_xl(48, bp)); + __builtin_mma_xvi8ger4pp(&acc4, avec, vec_xl(64, bp)); + __builtin_mma_xvi8ger4pp(&acc5, avec, vec_xl(80, bp)); + __builtin_mma_xvi8ger4pp(&acc6, avec, vec_xl(96, bp)); + __builtin_mma_xvi8ger4pp(&acc7, avec, vec_xl(112, bp)); + ap += 16; + bp += 128; + } + + __vector signed int res[4]; +#define STORE_ACC(ACC, CG_IDX) \ + { \ + __builtin_mma_disassemble_acc(res, &ACC); \ + for (int r = 0; r < m; ++r) { \ + const __vector signed int vcorr = vec_splats(128 * a_sums[r]); \ + const __vector signed int val = vec_sub(res[r], vcorr); \ + vec_xst(val, 0, \ + reinterpret_cast(c_ptr + r * ldc + (CG_IDX) * 4)); \ + } \ + } + STORE_ACC(acc0, 0); + STORE_ACC(acc1, 1); + STORE_ACC(acc2, 2); + STORE_ACC(acc3, 3); + STORE_ACC(acc4, 4); + STORE_ACC(acc5, 5); + STORE_ACC(acc6, 6); + STORE_ACC(acc7, 7); +#undef STORE_ACC +} + +} // namespace vsx_int8 + +// ============================================================================= +// MicroGemmINT8 specialisation for VSX (POWER10+) +// ============================================================================= +template +class MicroGemmINT8 { + public: + static constexpr int32_t K = vsx_int8::K; // 4 + static constexpr int32_t Mr = vsx_int8::Mr; // 4 + static constexpr int32_t Nr = vsx_int8::Nr; // 32 + static constexpr int32_t NrGemv = Nr; + static constexpr int32_t MaxMSize = 8; + static constexpr int32_t NSize = Nr; + static constexpr int32_t WeightOCGroupSize = Nr; + static_assert(MaxMSize % Mr == 0); + + // ------------------------------------------------------------------------- + // quantize_row: scalar_t -> INT8 + per-token scale + // Single-pass 16-element vector conversion with aligned 16-byte stores. + // ------------------------------------------------------------------------- + static FORCE_INLINE void quantize_row(const scalar_t* input, int8_t* output, + float& scale, const int32_t size) { + TORCH_CHECK_EQ(size % 16, 0); + + const __vector signed short z = vec_splats((signed short)0); + + auto load_lo_hi = + [&](int32_t i) -> std::pair<__vector float, __vector float> { + if constexpr (std::is_same_v) { + const float* p = reinterpret_cast(input) + i; + return {vec_xl(0, p), vec_xl(16, p)}; + } else { + const signed short* p = + reinterpret_cast(input + i); + const __vector signed short raw = (__vector signed short)vec_xl(0, p); + return {(__vector float)vec_mergeh(z, raw), + (__vector float)vec_mergel(z, raw)}; + } + }; + + __vector float max_v = vec_splats(0.0f); + for (int32_t i = 0; i < size; i += 8) { + auto [lo, hi] = load_lo_hi(i); + max_v = vec_max(max_v, vec_abs(lo)); + max_v = vec_max(max_v, vec_abs(hi)); + } + __vector float t = vec_max(max_v, vec_sld(max_v, max_v, 8)); + t = vec_max(t, vec_sld(t, t, 4)); + const float abs_max = std::max(vec_extract(t, 0), 1.0e-7f); + scale = abs_max / 127.0f; + const __vector float inv_scale = vec_splats(127.0f / abs_max); + + for (int32_t i = 0; i < size; i += 16) { + auto [lo0, hi0] = load_lo_hi(i); + auto [lo1, hi1] = load_lo_hi(i + 8); + + const __vector signed int i0 = + vec_cts(vec_round(vec_mul(lo0, inv_scale)), 0); + const __vector signed int i1 = + vec_cts(vec_round(vec_mul(hi0, inv_scale)), 0); + const __vector signed int i2 = + vec_cts(vec_round(vec_mul(lo1, inv_scale)), 0); + const __vector signed int i3 = + vec_cts(vec_round(vec_mul(hi1, inv_scale)), 0); + + const __vector signed short s0 = vec_packs(i0, i1); + const __vector signed short s1 = vec_packs(i2, i3); + const __vector signed char s8 = vec_packs(s0, s1); + + vec_xst(s8, 0, reinterpret_cast(output + i)); + } + } + + // ------------------------------------------------------------------------- + // dequantize_tile: INT32 accumulator -> FP32 scaled output + // ------------------------------------------------------------------------- + static FORCE_INLINE void dequantize_tile( + int32_t* input, float* output, const float* __restrict__ input_scales, + const float* __restrict__ weight_scales, const int32_t m, const int32_t n, + const int32_t stride) { + TORCH_CHECK_EQ(n % 4, 0); + for (int32_t mi = 0; mi < m; ++mi) { + const __vector float a_scale = vec_splats(input_scales[mi]); + for (int32_t ni = 0; ni < n; ni += 4) { + const __vector signed int i32 = vec_xl(0, input + mi * stride + ni); + const __vector float wscale = vec_xl(0, weight_scales + ni); + vec_xst(vec_mul(vec_ctf(i32, 0), vec_mul(a_scale, wscale)), 0, + output + mi * stride + ni); + } + } + } + + // ------------------------------------------------------------------------- + // pack_input_from_rows: pack rows into 4-row MMA tiles and compute row sums + // Header: 64 bytes (contains int32 a_sums[0..7]) + // Data: a_packed + 64 (contains 4x4 tiles of A) + // ------------------------------------------------------------------------- + static void pack_input_from_rows(const int8_t* const* __restrict__ rows, + int8_t* __restrict__ a_packed, + const int32_t m, const int32_t k) { + TORCH_CHECK(m > 0 && m <= MaxMSize); + TORCH_CHECK_EQ(k % K, 0); + + int32_t* a_sums = reinterpret_cast(a_packed); + for (int32_t r = 0; r < 8; ++r) a_sums[r] = 0; + + int8_t* a_data = a_packed + 64; + + for (int32_t row_base = 0; row_base < m; row_base += Mr) { + const int32_t panel_m = std::min(Mr, m - row_base); + const int8_t* const* panel_rows = rows + row_base; + int32_t* panel_sums = a_sums + row_base; + int8_t* dst_panel = a_data + (row_base / Mr) * (k * Mr); + + for (int32_t ki = 0; ki < k; ki += 4) { + int8_t* dst = dst_panel + (ki / 4) * 16; + for (int32_t r = 0; r < 4; ++r) { + if (r < panel_m) { + const int8_t* src = panel_rows[r] + ki; + dst[r * 4 + 0] = src[0]; + dst[r * 4 + 1] = src[1]; + dst[r * 4 + 2] = src[2]; + dst[r * 4 + 3] = src[3]; + panel_sums[r] += + static_cast(src[0]) + src[1] + src[2] + src[3]; + } else { + dst[r * 4 + 0] = 0; + dst[r * 4 + 1] = 0; + dst[r * 4 + 2] = 0; + dst[r * 4 + 3] = 0; + } + } + } + } + } + + // ------------------------------------------------------------------------- + // pack_weight: [output_size, input_size] INT8 -> packed layout + // Pre-biases with +128 in [N/32][K/4][8 col-groups][4 cols x 4 k] layout + // ------------------------------------------------------------------------- + static void pack_weight(const int8_t* __restrict__ weight, + int8_t* __restrict__ packed_weight, + const int32_t output_size, const int32_t input_size) { + TORCH_CHECK_EQ(output_size % NSize, 0); + TORCH_CHECK_EQ(input_size % K, 0); + + uint8_t* dst_base = reinterpret_cast(packed_weight); + + for (int32_t o = 0; o < output_size; o += Nr) { + uint8_t* dst_panel = dst_base + o * input_size; + for (int32_t k = 0; k < input_size; k += 4) { + for (int32_t cg = 0; cg < 8; ++cg) { + uint8_t* dst = dst_panel + (k / 4) * 128 + cg * 16; + for (int32_t col = 0; col < 4; ++col) { + for (int32_t ki = 0; ki < 4; ++ki) { + const int8_t w = weight[(o + cg * 4 + col) * input_size + k + ki]; + dst[col * 4 + ki] = + static_cast(static_cast(w) + 128); + } + } + } + } + } + } + + // ------------------------------------------------------------------------- + // gemm: 8-accumulator MMA GEMM over 32-column tiles + // ------------------------------------------------------------------------- + void gemm(const int8_t* __restrict__ a_packed, + const int8_t* __restrict__ b_packed, int32_t* __restrict__ c, + const int32_t m, const int32_t k, const int64_t b_n_group_stride, + const int64_t ldc) const { + TORCH_CHECK(m > 0 && m <= MaxMSize); + TORCH_CHECK_EQ(k % K, 0); + (void)b_n_group_stride; + + const int32_t* a_sums = reinterpret_cast(a_packed); + const int8_t* a_data = a_packed + 64; + const uint8_t* b_u8 = reinterpret_cast(b_packed); + + for (int32_t n_idx = 0; n_idx < NSize; n_idx += Nr) { + const uint8_t* b_panel = b_u8 + n_idx * k; + + for (int32_t row_base = 0; row_base < m; row_base += Mr) { + const int32_t panel_m = std::min(Mr, m - row_base); + const int8_t* a_panel = a_data + (row_base / Mr) * (k * Mr); + const int32_t* panel_sums = a_sums + row_base; + + vsx_int8::gemm_mma_4x32(a_panel, b_panel, panel_sums, + c + row_base * ldc + n_idx, panel_m, k, ldc); + } + } + } +}; + +} // namespace cpu_micro_gemm + +#endif // CPU_MICRO_GEMM_INT8_VSX_HPP diff --git a/csrc/cpu/sgl-kernels/common.h b/csrc/cpu/sgl-kernels/common.h index 6f27ba2d5c10..acf75912819f 100644 --- a/csrc/cpu/sgl-kernels/common.h +++ b/csrc/cpu/sgl-kernels/common.h @@ -12,11 +12,45 @@ #include #include #include +#include #if defined(_OPENMP) #include #endif +// Adapted from sglang: dispatch utilities for FP8 W8A8 kernels +#define AT_DISPATCH_OUT_TYPES(TYPE, NAME, ...) \ + AT_DISPATCH_SWITCH( \ + TYPE, \ + NAME, \ + AT_PRIVATE_CASE_TYPE_USING_HINT(at::ScalarType::Float, out_t, __VA_ARGS__) \ + AT_PRIVATE_CASE_TYPE_USING_HINT(at::ScalarType::BFloat16, out_t, __VA_ARGS__) \ + AT_PRIVATE_CASE_TYPE_USING_HINT(at::ScalarType::Half, out_t, __VA_ARGS__)) + +#define AT_DISPATCH_CASE_ENUM_NO_RETURN(VALUE, TYPE, HINT, ...) \ + case VALUE: { \ + constexpr TYPE HINT = VALUE; \ + __VA_ARGS__; \ + break; \ + } + +#define AT_DISPATCH_BOOL_NO_RETURN(VALUE, NAME, HINT, ...) \ + [&]() { \ + switch (VALUE) { \ + AT_DISPATCH_CASE_ENUM_NO_RETURN(true, bool, HINT, __VA_ARGS__) \ + AT_DISPATCH_CASE_ENUM_NO_RETURN(false, bool, HINT, __VA_ARGS__) \ + } \ + }() + +#define AT_DISPATCH_QUANT_MODE_NO_RETURN(MODE, NAME, HINT, ...) \ + [&]() { \ + switch (MODE) { \ + AT_DISPATCH_CASE_ENUM_NO_RETURN(PER_TENSOR, int, HINT, __VA_ARGS__) \ + AT_DISPATCH_CASE_ENUM_NO_RETURN(PER_ROW, int, HINT, __VA_ARGS__) \ + AT_DISPATCH_CASE_ENUM_NO_RETURN(PER_GROUP, int, HINT, __VA_ARGS__) \ + } \ + }() + namespace { // dispatch bool @@ -246,26 +280,10 @@ inline int get_thread_num() { // balance payload across each thread template inline void balance211(T n, T nth, T ith, T& n_start, T& n_end) { -#if 0 - // onednn partition pattern - T& n_my = n_end; - if (nth <= 1 || n == 0) { - n_start = 0; - n_my = n; - } else { - T n1 = div_up(n, nth); - T n2 = n1 - 1; - T T1 = n - n2 * nth; - n_my = ith < T1 ? n1 : n2; - n_start = ith <= T1 ? ith*n1 : T1 * n1 + (ith - T1) * n2; - } - n_end += n_start; -#else // pytorch aten partition pattern T n_my = div_up(n, nth); n_start = ith * n_my; n_end = std::min(n_start + n_my, n); -#endif } template @@ -472,4 +490,60 @@ inline T* conditional_data_ptr(const std::optional& opt) { return opt.has_value() ? opt.value().data_ptr() : nullptr; } +// Adapted from sglang: M blocking utilities for FP8 W8A8 kernels +inline int64_t get_m_block(int64_t M) { + if (M <= 48) { + return M; + } else if (M < 64) { + return 32; + } else if (M < 96) { + return 64; + } else { + return 128; + } +} + +inline std::tuple get_m_blocking(int64_t M) { + bool parallel_on_M = M > 128; + int64_t block_m = get_m_block(M); + int64_t Mc = (M + block_m - 1) / block_m; + int64_t Mc_parallel = parallel_on_M ? Mc : 1; + return std::make_tuple(parallel_on_M, block_m, Mc, Mc_parallel); +} + +#if defined(CPU_CAPABILITY_AVX512) +template +void zero_buffer(T* data, int64_t size) { + const int32_t vec_size = at::vec::Vectorized::size(); + auto zero_vec = at::vec::Vectorized(0); + int64_t d = 0; + for (; d < size - (size % vec_size); d += vec_size) { + zero_vec.store(data + d); + } + if (d < size) { + zero_vec.store(data + d, size - d); + } +} +#else +template +void zero_buffer(T* data, int64_t size) { + memset(data, 0, sizeof(T) * size); +} +#endif + +#if defined(__x86_64__) +// CPU_CAPABILITY_AVX10_2 is defined by cmake only when the compiler accepts +// -mavx10.2; without it the builtin below is not merely absent but a hard +// error, so the AVX512 paths have to stand in. +bool avx10_2_available() { +#ifdef CPU_CAPABILITY_AVX10_2 + // __builtin_cpu_supports returns the masked feature bit rather than 0/1. + static const bool available = __builtin_cpu_supports("avx10.2"); + return available; +#else + return false; +#endif +} +#endif + } // anonymous namespace diff --git a/csrc/cpu/sgl-kernels/gemm.h b/csrc/cpu/sgl-kernels/gemm.h index db7ad884d09a..dc5f9c585d5d 100644 --- a/csrc/cpu/sgl-kernels/gemm.h +++ b/csrc/cpu/sgl-kernels/gemm.h @@ -94,7 +94,7 @@ enum class CPUActMethod : int { gelu_and_mul = 2, }; -enum class CPUQuantMethod : int64_t { BF16 = 0, INT8_W8A8 = 1, FP8_W8A16 = 2, INT4_W4A8 = 3, MXFP4 = 4 }; +enum class CPUQuantMethod : int64_t { BF16 = 0, INT8_W8A8 = 1, FP8_W8A16 = 2, INT4_W4A8 = 3, MXFP4 = 4, FP8_W8A8 = 5 }; constexpr bool operator==(CPUQuantMethod a, int64_t b) { return static_cast(a) == b; @@ -395,3 +395,55 @@ void tinygemm_kernel( bool brg, int64_t block_size_K, bool do_unpack = true); + +// FP8 W8A8 tinygemm kernel (defined in gemm_fp8_w8a8.cpp) +void tinygemm_kernel( + float* C, + const at::Float8_e4m3fn* A, + const float* scales_a, + const at::Float8_e4m3fn* B, + const float* scales_b, + int64_t M, + int64_t K, + int64_t lda, + int64_t ldc, + int64_t ldsa, + float* ukernel_buf, + at::BFloat16* dqA_buf, + at::BFloat16* dqB_buf); + +// FP8 W8A8 quantize activation (defined in gemm_fp8_w8a8.cpp) +std::tuple quantize_fp8e4m3_vec( + const at::Tensor& t, + bool channelwise, + c10::optional scale_opt); + +// FP8 W8A8 fused MoE kernel (defined in moe_fp8_w8a8.cpp) +template +void fused_experts_fp8_a8_kernel_impl( + scalar_t* __restrict__ output, + scalar_t* __restrict__ ic0, + scalar_t* __restrict__ ic1, + scalar_t* __restrict__ ic2, + at::Float8_e4m3fn* __restrict__ A_tmp, + scalar_t* __restrict__ B_tmp, + float* __restrict__ C_tmp, + float* __restrict__ Ukernel_tmp, + const at::Float8_e4m3fn* __restrict__ input, + const at::Float8_e4m3fn* __restrict__ packed_w1, + const at::Float8_e4m3fn* __restrict__ packed_w2, + const float* __restrict__ As, + const float* __restrict__ w1s, + const float* __restrict__ w2s, + int64_t block_size_N, + int64_t block_size_K, + const float* __restrict__ topk_weights, + const int32_t* __restrict__ sorted_ids, + const int32_t* __restrict__ expert_ids, + const int32_t* __restrict__ offsets, + int64_t M, + int64_t N, + int64_t K, + int64_t E, + int64_t topk, + int64_t num_tokens_post_pad); diff --git a/csrc/cpu/sgl-kernels/gemm_fp8_w8a8.cpp b/csrc/cpu/sgl-kernels/gemm_fp8_w8a8.cpp new file mode 100644 index 000000000000..2014a29fa295 --- /dev/null +++ b/csrc/cpu/sgl-kernels/gemm_fp8_w8a8.cpp @@ -0,0 +1,1099 @@ +// Adapted from +// https://github.com/sgl-project/sglang/tree/main/sgl-kernel/csrc/cpu + +// clang-format off + +#include +#include +#include + +#include "common.h" +#include "vec.h" + +// Runtime check for AMX-FP8, implemented in cpu_isa.cpp. +extern bool cpu_has_amx_fp8(); + +namespace { + +#define BLOCK_N 32 + +#define PER_TENSOR 1 +#define PER_ROW 2 +#define PER_GROUP 3 + +static bool cpublas_checked = false; +static bool cpublas_can_pack = false; + +bool cpublas_could_pack() { + // the could_pack check requires AMX support implicitly + if (cpublas_checked) { + return cpublas_can_pack; + } + cpublas_can_pack = cpu_has_amx_fp8() && at::native::cpublas::could_pack(at::kFloat8_e4m3fn); + cpublas_checked = true; + return cpublas_can_pack; +} + +#if defined(CPU_CAPABILITY_AVX512) +static void cvt_f8e4m3_to_bf16( + const at::Float8_e4m3fn* __restrict__ in, at::BFloat16* out, int64_t rows, int64_t cols, int64_t stride) { + if (stride == cols) { + // A contiguous buffer + size_t len = rows * cols; + size_t i = 0; + for (; i < len; i += 32) { + __m256i fp8_vec = _mm256_loadu_si256((__m256i*)&in[i]); + __m512bh bf16_vec = cvt_e4m3_bf16_intrinsic_no_nan(fp8_vec); + _mm512_storeu_si512((__m512i*)(out + i), (__m512i)bf16_vec); + } + for (; i < len; ++i) { + out[i] = (at::BFloat16)in[i]; + } + } else { + // Non-contiguous. Access each row with stride + TORCH_CHECK(stride > cols); + for (int r = 0; r < rows; ++r) { + size_t i = 0; + size_t vec_len = cols / 32 * 32; + for (; i < vec_len; i += 32) { + __m256i fp8_vec = _mm256_loadu_si256((__m256i*)&in[r * stride + i]); + __m512bh bf16_vec = cvt_e4m3_bf16_intrinsic_no_nan(fp8_vec); + _mm512_storeu_si512((__m512i*)(out + r * cols + i), (__m512i)bf16_vec); + } + for (; i < cols; ++i) { + out[r * cols + i] = (at::BFloat16)in[r * stride + i]; + } + } + } +} + +// accumulate and store result to buffer +// if act/wei are per_group quantized, apply scales +template +static void _accumulate_result( + float* __restrict__ output, + const float* __restrict__ input, + const float* __restrict__ scale_a, + const float* __restrict__ scale_b, + int M, + int ldi, + int ldo, + int ldsa = 1) { + float a_scale, b_scale; + __m512 va_scale; + __m512 vb_scale; + for (int m = 0; m < M; ++m) { + if constexpr (act_quant_mode == PER_GROUP) { + a_scale = *(scale_a + m * ldsa); + va_scale = _mm512_set1_ps(a_scale); + } + constexpr int N_UNROLL = N / 16; + c10::ForcedUnroll{}([&](auto i) { + constexpr int n = i * 16; + __m512 vc_f = _mm512_loadu_ps(input + m * ldi + n); + if constexpr (act_quant_mode == PER_GROUP) { + vc_f = _mm512_mul_ps(vc_f, va_scale); + } + if constexpr (wei_quant_mode == PER_GROUP) { + vb_scale = _mm512_loadu_ps(scale_b + n); + vc_f = _mm512_mul_ps(vc_f, vb_scale); + } + if constexpr (accum) { + __m512 vo = _mm512_loadu_ps(output + m * ldo + n); + _mm512_storeu_ps(output + m * ldo + n, _mm512_add_ps(vo, vc_f)); + } else { + _mm512_storeu_ps(output + m * ldo + n, vc_f); + } + }); + constexpr int tail_start = N / 16 * 16; + for (int n = tail_start; n < N; ++n) { + float dq_val = input[m * ldi + n]; + if constexpr (act_quant_mode == PER_GROUP) { + dq_val = dq_val * a_scale; + } + if constexpr (wei_quant_mode == PER_GROUP) { + b_scale = scale_b[n]; + dq_val = dq_val * b_scale; + } + if constexpr (accum) { + output[m * ldo + n] += dq_val; + } else { + output[m * ldo + n] = dq_val; + } + } + } +} + +// Store result to output buffer with dtype conversion +// If act/wei are per_row or per_tensor quantized, apply scales +// If bias is not null, add bias +template +inline void store_out( + const float* y_buf, + out_dtype* c_ptr, + int64_t M, + int64_t lda, + const float* scales_a, + const float* scales_b, + const float* bias) { + float a_scale = 1.0, b_scale = 1.0; + __m512 va_scale, vb_scale; + if constexpr (act_quant_mode == PER_TENSOR) { + a_scale = *scales_a; + } + if constexpr (wei_quant_mode == PER_TENSOR) { + b_scale = *scales_b; + vb_scale = _mm512_set1_ps(b_scale); + } + for (int i = 0; i < M; ++i) { + if constexpr (act_quant_mode == PER_ROW) { + a_scale = *(scales_a + i); + } + if constexpr (act_quant_mode != PER_GROUP) { + va_scale = _mm512_set1_ps(a_scale); + } + constexpr int N_UNROLL = N / 16; + c10::ForcedUnroll{}([&](auto idx) { + constexpr int j = idx * 16; + __m512 y_vec = _mm512_loadu_ps(y_buf + i * N + j); + __m512 bias_vec = bias ? _mm512_loadu_ps(bias + j) : _mm512_setzero_ps(); + if constexpr (act_quant_mode != PER_GROUP) { + y_vec = _mm512_mul_ps(y_vec, va_scale); + } + if constexpr (wei_quant_mode == PER_ROW) { + vb_scale = _mm512_loadu_ps(scales_b + j); + } + if constexpr (wei_quant_mode != PER_GROUP) { + y_vec = _mm512_mul_ps(y_vec, vb_scale); + } + y_vec = _mm512_add_ps(y_vec, bias_vec); + if constexpr (std::is_same::value) { + _mm512_storeu_ps(c_ptr + i * lda + j, y_vec); + } else if constexpr (std::is_same::value) { + __m256i y_bf16_vec = at::vec::cvtfp32_bf16(y_vec); + _mm256_storeu_si256(reinterpret_cast<__m256i*>(c_ptr + i * lda + j), y_bf16_vec); + } else if constexpr (std::is_same::value) { + __m256i y_fp16_vec = at::vec::cvtfp32_fp16(y_vec); + _mm256_storeu_si256(reinterpret_cast<__m256i*>(c_ptr + i * lda + j), y_fp16_vec); + } else { + TORCH_CHECK(false, "Unsupported output dtype"); + } + }); + constexpr int tail_start = N / 16 * 16; + for (int j = tail_start; j < N; ++j) { + if constexpr (wei_quant_mode == PER_ROW) { + b_scale = scales_b[j]; + } + c_ptr[i * lda + j] = static_cast(y_buf[i * N + j] * a_scale * b_scale); + } + } // for M +} + +#else // no AVX512 + +static void cvt_f8e4m3_to_bf16( + const at::Float8_e4m3fn* __restrict__ in, at::BFloat16* out, int64_t rows, int64_t cols, int64_t stride) { + for (int r = 0; r < rows; ++r) { + for (int c = 0; c < cols; ++c) { + out[r * cols + c] = (at::BFloat16)in[r * stride + c]; + } + } +} + +// Store result to output buffer with dtype conversion +// If act/wei are per_row or per_tensor quantized, apply scales +// If bias is not null, add bias +template +inline void store_out( + const float* y_buf, + out_dtype* c_ptr, + int64_t M, + int64_t lda, + const float* scales_a, + const float* scales_b, + const float* bias) { + float a_scale = 1.0, b_scale = 1.0; + if constexpr (act_quant_mode == PER_TENSOR) { + a_scale = *scales_a; + } + if constexpr (wei_quant_mode == PER_TENSOR) { + b_scale = *scales_b; + } + for (int i = 0; i < M; ++i) { + if constexpr (act_quant_mode == PER_ROW) { + a_scale = *(scales_a + i); + } + for (int j = 0; j < N; ++j) { + if constexpr (wei_quant_mode == PER_ROW) { + b_scale = scales_b[j]; + } + c_ptr[i * lda + j] = static_cast(y_buf[i * N + j] * a_scale * b_scale); + } + } // for M +} + +#endif // CPU_CAPABILITY_AVX512 + +template +void _micro_gemm( + float* C, + const at::Float8_e4m3fn* A, + const float* scales_a, + const at::Float8_e4m3fn* B, + const float* scales_b, + int64_t M, + int64_t K, + int64_t lda, + int64_t ldc, + int64_t ldsa, + float* ukernel_buf, + at::BFloat16* dqA_buf, + at::BFloat16* dqB_buf) { + // If FP8 brgemm is not available, convert A/B to bf16 for computation + // Compute GEMM fp8 * fp8 -> fp32 (or bf16 * bf16 -> fp32) + // If per_group quant, apply scales. Otherwise, don't apply scales here + // Finally accumulate and store results +#if defined(CPU_CAPABILITY_AVX512) + if constexpr (cpublas_can_pack) { +#ifdef CPUBLAS_BRGEMM_F8F8F32 + at::native::cpublas::brgemm( + M, N, K, lda /*lda*/, N /*ldb*/, N /*ldc*/, false /* add_C */, A, B, ukernel_buf, true /* is_vnni */); +#else + cvt_f8e4m3_to_bf16(A, dqA_buf, M, K, lda); + cvt_f8e4m3_to_bf16(B, dqB_buf, K, N, N); + at::native::cpublas::brgemm( + M, N, K, K /*lda*/, N /*ldb*/, N /*ldc*/, false /* add_C */, dqA_buf, dqB_buf, ukernel_buf, true /* is_vnni */); +#endif + _mm_prefetch(B + N * (K + 128), _MM_HINT_T0); + _mm_prefetch(A + K + 128, _MM_HINT_T0); + _accumulate_result( + C, ukernel_buf, scales_a, scales_b, M, N /*ldi*/, ldc, ldsa); + } else +#endif + { + for (int64_t i = 0; i < M; ++i) { + for (int64_t j = 0; j < N; ++j) { + float sum = 0; + for (int64_t k = 0; k < K; ++k) { + sum += ((float)A[i * lda + k] * (float)B[k * N + j]); + } + if constexpr (act_quant_mode == PER_GROUP) { + sum *= scales_a[i * ldsa]; + } + if constexpr (wei_quant_mode == PER_GROUP) { + sum *= scales_b[j]; + } + C[i * ldc + j] += sum; + } + } + } +} + +template +void _float8_linear_impl( + const at::Tensor& input, + const at::Tensor& input_scales, + const at::Tensor& weight, + const at::Tensor& weight_scales, + const std::optional& bias, + at::Tensor& output) { + // input shape = [..., K] + // input is per token quantized + int64_t K = input.size(-1); + auto input_view = input.view({-1, K}); + int64_t M = input_view.size(0); + + // weight shape = [Nc, Kc, block_k, block_n] + // scales shape = [Nc, G, block_n] + int64_t Nc = weight.size(0); + int64_t Kc = wei_quant_mode != PER_GROUP ? 1 : weight.size(1); + int64_t block_k = wei_quant_mode != PER_GROUP ? weight.size(1) * weight.size(2) : weight.size(2); + constexpr int64_t block_n = BLOCK_N; + TORCH_CHECK(weight.size(3) == block_n, "Float8 linear: unexpected weight shape"); + int64_t N = Nc * block_n; + TORCH_CHECK(K == Kc * block_k, "Float8 linear: weight and input shapes mismatch"); + auto [parallel_on_M, block_m, Mc, Mc_parallel] = get_m_blocking(M); + + // scales shape = [Nc, G, block_n] + int64_t num_groups = wei_quant_mode == PER_TENSOR ? 1 : weight_scales.size(1); + TORCH_CHECK(K % num_groups == 0, "K should be divisible by num_groups"); + int64_t group_size = K / num_groups; + TORCH_CHECK(group_size % block_k == 0, "Float8 linear: group_size should be divisible by block_k"); + int64_t block_per_group = group_size / block_k; + TORCH_CHECK( + input_scales.numel() == 1 || input_scales.numel() == M || input_scales.numel() == M * num_groups, + "Float8 linear: unexpected input scales shape"); + auto ldsa = act_quant_mode == PER_TENSOR ? 0 : act_quant_mode == PER_ROW ? 1 : num_groups; + + const at::Float8_e4m3fn* a_ptr = input_view.data_ptr(); + const float* a_scales_ptr = input_scales.data_ptr(); + const at::Float8_e4m3fn* b_ptr = weight.data_ptr(); + const float* b_scales_ptr = weight_scales.data_ptr(); + out_dtype* c_ptr = output.data_ptr(); + const float* bias_ptr = bias.has_value() ? bias.value().data_ptr() : nullptr; + + int64_t block_size = block_m * block_n; + int64_t num_thread = at::get_num_threads(); + at::Tensor y_buffer = at::empty({num_thread, block_size}, output.options().dtype(at::kFloat)); + // Create buffer for brgemm output and dqA/dqB (optional) +#if defined(CPU_CAPABILITY_AVX512) + // buffer for brgemm output in float32 + int64_t buffer_size = block_size * 2; // float32 = bfloat16 * 2 +#ifndef CPUBLAS_BRGEMM_F8F8F32 + // buffers for dqA & dqB in bf16 + buffer_size += (block_k * block_n + block_m * block_k); +#endif + at::Tensor micro_gemm_buffer = at::empty({num_thread, buffer_size}, output.options().dtype(at::kBFloat16)); +#endif + + parallel_2d(Mc, Nc, [&](int64_t mc0, int64_t mc1, int64_t nc0, int64_t nc1) { + int tid = get_thread_num(); + float* y_buf = y_buffer.data_ptr() + tid * block_size; + at::BFloat16 *dqA_buffer = nullptr, *dqB_buffer = nullptr; + float* ukernel_buf = nullptr; +#if defined(CPU_CAPABILITY_AVX512) + at::BFloat16* micro_gemm_buf = micro_gemm_buffer.data_ptr() + tid * buffer_size; + ukernel_buf = reinterpret_cast(micro_gemm_buf); +#ifndef CPUBLAS_BRGEMM_F8F8F32 + dqA_buffer = micro_gemm_buf; + dqB_buffer = micro_gemm_buf + block_m * block_k; + ukernel_buf = reinterpret_cast(micro_gemm_buf + block_m * block_k + block_k * block_n); +#endif +#endif + + loop_2d(mc0, mc1, nc0, nc1, block_n * K, [&](int64_t mci, int64_t nc, int64_t) { + int64_t m_size = mci * block_m + block_m > M ? M - mci * block_m : block_m; + zero_buffer(y_buf, m_size * block_n); + for (int kci = 0; kci < Kc; ++kci) { + auto scales_a = a_scales_ptr + mci * block_m * num_groups + kci / block_per_group; + auto scales_b = b_scales_ptr + nc * block_n * num_groups + kci / block_per_group * block_n; + _micro_gemm( + /* C */ y_buf, + /* A */ a_ptr + mci * block_m * K + kci * block_k, + /* scales_a */ scales_a, + /* B */ b_ptr + (nc * Kc + kci) * block_n * block_k, + /* scales_b */ scales_b, + /* M */ m_size, + /* K */ block_k, + /* lda */ K, + /* ldc */ block_n, + /* ldsa */ ldsa, + /* ukernel_buf */ ukernel_buf, + /* dqA_buf */ dqA_buffer, + /* dqB_buf */ dqB_buffer); + } + auto scales_a = act_quant_mode == PER_TENSOR ? a_scales_ptr + : act_quant_mode == PER_ROW ? a_scales_ptr + mci * block_m + : nullptr; + auto scales_b = wei_quant_mode == PER_TENSOR ? b_scales_ptr + : wei_quant_mode == PER_ROW ? b_scales_ptr + nc * block_n + : nullptr; + auto bias_data = bias_ptr ? bias_ptr + nc * block_n : nullptr; + store_out( + y_buf, c_ptr + mci * block_m * N + nc * block_n, m_size, N /*lda*/, scales_a, scales_b, bias_data); + }); + + if constexpr (cpublas_can_pack) { + at::native::cpublas::brgemm_release(); + } + }); +} + +} // anonymous namespace + +void tinygemm_kernel( + float* C, + const at::Float8_e4m3fn* A, + const float* scales_a, + const at::Float8_e4m3fn* B, + const float* scales_b, + int64_t M, + int64_t K, + int64_t lda, + int64_t ldc, + int64_t ldsa, + float* ukernel_buf, + at::BFloat16* dqA_buf, + at::BFloat16* dqB_buf) { + // act_quant_mode = per row (2), wei_quant_mode = per group (3) + if (cpublas_could_pack()) { + _micro_gemm(C, A, scales_a, B, scales_b, M, K, lda, ldc, ldsa, ukernel_buf, dqA_buf, dqB_buf); + } else { + _micro_gemm(C, A, scales_a, B, scales_b, M, K, lda, ldc, ldsa, ukernel_buf, dqA_buf, dqB_buf); + } +} + +#define INSTANTIATE_TINYGEMM_TEMPLATE() \ + void tinygemm_kernel( \ + float* C, \ + const at::Float8_e4m3fn* A, \ + const float* scales_a, \ + const at::Float8_e4m3fn* B, \ + const float* scales_b, \ + int64_t M, \ + int64_t K, \ + int64_t lda, \ + int64_t ldc, \ + int64_t ldsa, \ + float* ukernel_buf, \ + at::BFloat16* dqA_buf, \ + at::BFloat16* dqB_buf) + +INSTANTIATE_TINYGEMM_TEMPLATE(); +/* +return: packed_weight, packed_scales +*/ +std::tuple float8_linear_prepack_impl(const at::Tensor& weight, const at::Tensor& scales) { + // weight shape = [N, K] + // scales shape = [N, G] + TORCH_CHECK(weight.dim() == 2, "Float8 linear CPU: Weight should be a 2D tensor for packing"); + TORCH_CHECK(weight.size(1) % 2 == 0, "Float8 linear CPU: Weight should have even number of columns for packing"); + + auto new_scales = scales; + if (new_scales.dim() == 1) { + new_scales.unsqueeze_(1); + } + new_scales = new_scales.to(at::kFloat); + int N = weight.size(0); + int K = weight.size(1); + int G = scales.size(1); + int group_size = K / G; + int block_k = group_size > 128 ? 128 : group_size; + while (K % block_k != 0) { + block_k /= 2; + } + TORCH_CHECK( + block_k > 0 && block_k <= group_size, "Float8 linear CPU: Invalid block_k size, should be in (0, group_size]"); + constexpr int block_n = BLOCK_N; + int Nc = N / block_n; + int Kc = K / block_k; + + // Reorder weight to [N/block_n, K/block_k, block_k, block_n] + // Reorder scales to [N/block_n, G, block_n] + auto weight_view = weight.view({Nc, block_n, Kc, block_k}); + at::Tensor weight_reordered = weight_view.permute({0, 2, 3, 1}).contiguous(); + at::Tensor blocked_weight; + at::Tensor blocked_scales = new_scales.view({Nc, block_n, G}).permute({0, 2, 1}).contiguous(); + +#if defined(CPU_CAPABILITY_AVX512) + if (cpublas_could_pack()) { +#ifdef CPUBLAS_BRGEMM_F8F8F32 + constexpr int vnni_size = 4; // for fp8 +#else + constexpr int vnni_size = 2; // for float16 +#endif + blocked_weight = at::empty({Nc, Kc, block_k, block_n}, weight.options()); + auto weight_ptr = reinterpret_cast(weight_reordered.data_ptr()); + auto blocked_weight_ptr = reinterpret_cast(blocked_weight.data_ptr()); + int64_t num_blocks = Nc * Kc; + at::parallel_for(0, num_blocks, 1, [&](int64_t begin, int64_t end) { + for (const auto i : c10::irange(begin, end)) { + auto in_ptr = weight_ptr + i * block_k * block_n; + auto out_ptr = blocked_weight_ptr + i * block_k * block_n; + + // Reorder weight block to VNNI + // plain shape = [block_k, block_n] + // packed shape = [block_k / VNNI_SIZE, block_n, VNNI_SIZE] viewed as [block_k, block_n] + constexpr int n_group_size = 8; + constexpr int n_group = block_n / n_group_size; // 4 + for (int nb = 0; nb < n_group; ++nb) { + for (int k = 0; k < block_k; k += vnni_size) { + for (int ni = 0; ni < n_group_size; ++ni) { + for (int ki = 0; ki < vnni_size; ++ki) { + int src_idx = nb * n_group_size + ni + (k + ki) * block_n; + int dst_idx = (nb * n_group_size + ni) * vnni_size + k * block_n + ki; + *(out_ptr + dst_idx) = *(in_ptr + src_idx); + } + } + } + } + } + }); + } else +#endif + { + blocked_weight = weight_reordered; + } + + return std::make_tuple(std::move(blocked_weight), std::move(blocked_scales)); +} + +// AVX512 optimized channel-wise max computation +inline void +compute_channel_max_avx512(const float* data, float* max_vals, int64_t num_channels, int64_t elements_per_channel) { + at::parallel_for(0, num_channels, 1, [&](int64_t start, int64_t end) { + for (int64_t c = start; c < end; ++c) { + const float* channel_data = data + c * elements_per_channel; + float max_val = 0.0f; + + int64_t i = 0; + // Process 16 elements at a time + __m512 max_vec = _mm512_setzero_ps(); + + for (; i <= elements_per_channel - 16; i += 16) { + __m512 data_vec = _mm512_loadu_ps(&channel_data[i]); + __m512 abs_vec = _mm512_abs_ps(data_vec); + max_vec = _mm512_max_ps(max_vec, abs_vec); + } + + // Horizontal max of the vector + float hmax = _mm512_reduce_max_ps(max_vec); + max_val = std::max(max_val, hmax); + + // Handle remaining elements + for (; i < elements_per_channel; ++i) { + max_val = std::max(max_val, std::abs(channel_data[i])); + } + + max_vals[c] = max_val; + } + }); +} + +// AVX512 optimized scaling and clamping for channel-wise quantization +inline void scale_clamp_channelwise_avx512( + const float* src, + float* dst, + const float* scales, + int64_t num_channels, + int64_t elements_per_channel, + float quant_max, + float neg_quant_max) { + at::parallel_for(0, num_channels, 1, [&](int64_t start, int64_t end) { + for (int64_t c = start; c < end; ++c) { + float scale_val = scales[c]; + float scale_reciprocal = 1.0f / scale_val; + + const float* channel_src = src + c * elements_per_channel; + float* channel_dst = dst + c * elements_per_channel; + + int64_t i = 0; + const __m512 scale_recip_vec = _mm512_set1_ps(scale_reciprocal); + const __m512 quant_max_vec = _mm512_set1_ps(quant_max); + const __m512 neg_quant_max_vec = _mm512_set1_ps(neg_quant_max); + + for (; i <= elements_per_channel - 16; i += 16) { + __m512 src_vec = _mm512_loadu_ps(&channel_src[i]); + __m512 scaled_vec = _mm512_mul_ps(src_vec, scale_recip_vec); + __m512 clamped_vec = _mm512_min_ps(_mm512_max_ps(scaled_vec, neg_quant_max_vec), quant_max_vec); + _mm512_storeu_ps(&channel_dst[i], clamped_vec); + } + + // Handle remaining elements + for (; i < elements_per_channel; ++i) { + float scaled = channel_src[i] * scale_reciprocal; + channel_dst[i] = std::clamp(scaled, neg_quant_max, quant_max); + } + } + }); +} + +// AVX512 optimized scaling and clamping for global quantization +inline void scale_clamp_global_avx512( + const float* src, float* dst, int64_t size, float scale_reciprocal, float quant_max, float neg_quant_max) { + if (size <= 0) return; + + const __m512 scale_recip_vec = _mm512_set1_ps(scale_reciprocal); + const __m512 quant_max_vec = _mm512_set1_ps(quant_max); + const __m512 neg_quant_max_vec = _mm512_set1_ps(neg_quant_max); + + at::parallel_for(0, size, 4096, [&](int64_t start, int64_t end) { + int64_t block_size = end - start; + const float* block_src = src + start; + float* block_dst = dst + start; + + int64_t i = 0; + for (; i <= block_size - 16; i += 16) { + __m512 src_vec = _mm512_loadu_ps(&block_src[i]); + __m512 scaled_vec = _mm512_mul_ps(src_vec, scale_recip_vec); + __m512 clamped_vec = _mm512_min_ps(_mm512_max_ps(scaled_vec, neg_quant_max_vec), quant_max_vec); + _mm512_storeu_ps(&block_dst[i], clamped_vec); + } + + // Handle remaining elements in block + for (; i < block_size; ++i) { + float scaled = block_src[i] * scale_reciprocal; + block_dst[i] = std::clamp(scaled, neg_quant_max, quant_max); + } + }); +} + +std::tuple +_quantize_fp8e4m3(const at::Tensor& t, bool channelwise, c10::optional scale_opt = c10::nullopt) { + constexpr float quant_max = 448.0f; // torch.finfo(torch.float8_e4m3fn).max + constexpr float eps = std::numeric_limits::epsilon(); + + // Ensure input is contiguous and in float32 + auto t_float = t.to(at::ScalarType::Float).contiguous(); + at::Tensor qt; + at::Tensor scale_tensor; + + if (channelwise) { + // Channel-wise quantization with AVX512 optimization + int64_t num_channels = t_float.size(0); + int64_t elements_per_channel = t_float.numel() / num_channels; + + if (elements_per_channel * num_channels != t_float.numel()) { + throw std::runtime_error("Tensor must be divisible by number of channels for channel-wise quantization"); + } + + // Allocate scale tensor + scale_tensor = at::empty({num_channels}, t_float.options()); + float* scale_data = scale_tensor.data_ptr(); + + // Compute channel-wise max using AVX512 + compute_channel_max_avx512(t_float.data_ptr(), scale_data, num_channels, elements_per_channel); + + // Apply quant_max and EPS + at::parallel_for(0, num_channels, 1, [&](int64_t start, int64_t end) { + for (int64_t c = start; c < end; ++c) { + scale_data[c] = std::max(scale_data[c] / quant_max, eps); + } + }); + + // Create output tensor for quantized values + qt = at::empty_like(t_float); + + // Scale and clamp using AVX512 + scale_clamp_channelwise_avx512( + t_float.data_ptr(), + qt.data_ptr(), + scale_data, + num_channels, + elements_per_channel, + quant_max, + -quant_max); + } else { + // Global quantization with AVX512 optimization + if (!scale_opt.has_value()) { + throw std::runtime_error("Scale must be provided for non-channelwise quantization in AVX512 version"); + } + + scale_tensor = scale_opt.value().to(at::ScalarType::Float).contiguous(); + + // Handle scalar scale case + float scale_val; + if (scale_tensor.numel() == 1) { + scale_val = std::max(scale_tensor.item(), eps); + } else { + // Compute max of scale tensor if it's not scalar + float* scale_data = scale_tensor.data_ptr(); + __m512 max_vec = _mm512_set1_ps(eps); + + int64_t i = 0; + for (; i <= scale_tensor.numel() - 16; i += 16) { + __m512 scale_vec = _mm512_loadu_ps(&scale_data[i]); + max_vec = _mm512_max_ps(max_vec, scale_vec); + } + + float hmax = _mm512_reduce_max_ps(max_vec); + for (; i < scale_tensor.numel(); ++i) { + hmax = std::max(hmax, scale_data[i]); + } + scale_val = std::max(hmax, eps); + } + + scale_tensor = at::tensor({scale_val}, scale_tensor.options()); + float scale_reciprocal = 1.0f / scale_val; + + // Create output tensor + qt = at::empty_like(t_float); + + // Scale and clamp using AVX512 + scale_clamp_global_avx512( + t_float.data_ptr(), qt.data_ptr(), t_float.numel(), scale_reciprocal, quant_max, -quant_max); + } + + // Final conversion to FP8 E4M3FN using ATen's native conversion + qt = at::_to_copy(qt, at::ScalarType::Float8_e4m3fn); + + return std::make_tuple(qt, scale_tensor); +} + +inline __m128i cvtfp32_fp8e4m3(__m512& src) { + // cvt 16x32 from fp32 to fp8 e4m3 + const __m512i sign_mask = _mm512_set1_epi32(0x80000000); + const __m512i fp8_max = _mm512_set1_epi32(UINT32_C(1087) << 20); + const __m512i denorm_thresh = _mm512_set1_epi32(UINT32_C(121) << 23); + const __m512i denorm_mask = _mm512_set1_epi32(UINT32_C(141) << 23); + const __m512i bias_part1 = _mm512_set1_epi32((uint32_t)(7 - 127) << 23); + const __m512i rounding_bias = _mm512_set1_epi32(0x7FFFF); + __m512i f_bits = _mm512_castps_si512(src); + // Extract and save sign + __m512i sign = _mm512_and_epi32(f_bits, sign_mask); + f_bits = _mm512_xor_epi32(f_bits, sign); + + // Prepare result containers + __m512i result = _mm512_setzero_si512(); + + // Step 1: Handle case of overflow + // (f_bits >= fp8_max): set result = 0x7f + __mmask16 overflow_mask = _mm512_cmpge_epu32_mask(f_bits, fp8_max); + if (overflow_mask) { + result = _mm512_mask_set1_epi32(result, overflow_mask, 0x7f); + } + + // Step 2: Handle small numbers (denormals) + // Small numbers (f_bits < denorm_thresh) + __mmask16 denorm_thresh_mask = _mm512_cmplt_epu32_mask(f_bits, denorm_thresh); + + if (denorm_thresh_mask) { + __m512 small_input = _mm512_castsi512_ps(f_bits); + __m512 small_denorm = _mm512_add_ps(small_input, _mm512_castsi512_ps(denorm_mask)); + __m512i small_denorm_bits = _mm512_castps_si512(small_denorm); + __m512i small_result = _mm512_sub_epi32(small_denorm_bits, denorm_mask); + result = _mm512_mask_mov_epi32(result, denorm_thresh_mask, small_result); + } + + // Step 3: Handle normal numbers + __mmask16 normal_mask = ~(overflow_mask | denorm_thresh_mask); + + if (normal_mask) { + // mant_odd = (f_bits >> 20) & 1 + __m512i mant_odd = _mm512_and_epi32(_mm512_srli_epi32(f_bits, 20), _mm512_set1_epi32(1)); + // f_bits += bias_part1 + rounding_bias + __m512i rounded = _mm512_add_epi32(f_bits, bias_part1); + rounded = _mm512_add_epi32(rounded, rounding_bias); + // Add mant_odd + rounded = _mm512_add_epi32(rounded, mant_odd); + // Shift right by 20 bits + __m512i normal_result = _mm512_srli_epi32(rounded, 20); + result = _mm512_mask_mov_epi32(result, normal_mask, normal_result); + } + + // Merge back the sign + __m512i sign_shifted = _mm512_srli_epi32(sign, 24); + result = _mm512_or_epi32(result, sign_shifted); + + // Now result is 16 x 32-bit integers, but we only need 8-bit for each + __m512i packed = _mm512_and_si512(result, _mm512_set1_epi32(0xFF)); + + // Narrow 32-bit integers to 8-bit + return _mm512_cvtepi32_epi8(packed); +} + +#ifdef CPU_CAPABILITY_AVX10_2 +__attribute__((target("avx10.2"))) inline __m128i cvtfp32_fp8e4m3_avx10_2(__m512& src) { + __m256i f16_vec = _mm512_cvt_roundps_ph(src, _MM_FROUND_TO_NEAREST_INT | _MM_FROUND_NO_EXC); + return _mm256_cvtph_hf8(_mm256_castsi256_ph(f16_vec)); +} + +// Carries the whole loop rather than just the conversion, as GCC cannot inline an avx10.2 +// function into a caller lacking that target, so branching per vector is costly. +__attribute__((target("avx10.2"))) void quantize_bf16_block_avx10_2( + const at::BFloat16* __restrict__ src, at::Float8_e4m3fn* __restrict__ dst, int64_t count, float scale_reciprocal) { + constexpr float quant_max = 448.0f; // torch.finfo(torch.float8_e4m3fn).max + const __m512 scale_recip_vec = _mm512_set1_ps(scale_reciprocal); + const __m512 quant_max_vec = _mm512_set1_ps(quant_max); + const __m512 neg_quant_max_vec = _mm512_set1_ps(-quant_max); + + for (int64_t i = 0; i <= count - 32; i += 32) { + __m256i src_vec1 = _mm256_loadu_si256(reinterpret_cast(&src[i])); + __m256i src_vec2 = _mm256_loadu_si256(reinterpret_cast(&src[i + 16])); + + __m512 fp32_vec1 = _mm512_castsi512_ps(_mm512_slli_epi32(_mm512_cvtepu16_epi32(src_vec1), 16)); + __m512 fp32_vec2 = _mm512_castsi512_ps(_mm512_slli_epi32(_mm512_cvtepu16_epi32(src_vec2), 16)); + + __m512 scaled_vec1 = _mm512_mul_ps(fp32_vec1, scale_recip_vec); + __m512 clamped_vec1 = _mm512_min_ps(_mm512_max_ps(scaled_vec1, neg_quant_max_vec), quant_max_vec); + + __m512 scaled_vec2 = _mm512_mul_ps(fp32_vec2, scale_recip_vec); + __m512 clamped_vec2 = _mm512_min_ps(_mm512_max_ps(scaled_vec2, neg_quant_max_vec), quant_max_vec); + + __m128i fp8_vec1 = cvtfp32_fp8e4m3_avx10_2(clamped_vec1); + __m128i fp8_vec2 = cvtfp32_fp8e4m3_avx10_2(clamped_vec2); + _mm_storeu_si128(reinterpret_cast<__m128i*>(dst + i), fp8_vec1); + _mm_storeu_si128(reinterpret_cast<__m128i*>(dst + i + 16), fp8_vec2); + } +} +#endif // CPU_CAPABILITY_AVX10_2 + +std::tuple _quantize_fp8e4m3_bf16_per_tensor_no_scale(const at::Tensor& t) { + constexpr float quant_max = 448.0f; // torch.finfo(torch.float8_e4m3fn).max + constexpr float eps = std::numeric_limits::epsilon(); + + // Input validation and preparation + assert(t.scalar_type() == at::ScalarType::BFloat16); + auto t_bf16 = t.contiguous(); + int64_t num_channels = t_bf16.size(0); + int64_t elements_per_channel = t_bf16.numel() / num_channels; + assert(elements_per_channel % 32 == 0); // do not consider tile currently + at::Tensor quant_t = at::empty(t_bf16.sizes(), t_bf16.options().dtype(at::kFloat8_e4m3fn)); + + // Allocate output tensors + at::Tensor scale_tensor = at::empty({num_channels}, t_bf16.options().dtype(at::ScalarType::Float)); + float* scale_data = scale_tensor.data_ptr(); + + // Unified processing: compute max, apply scale, and quantize in single pass + at::parallel_for(0, num_channels, 1, [&](int64_t start, int64_t end) { + for (int64_t c = start; c < end; ++c) { + const at::BFloat16* channel_src = t_bf16.data_ptr() + c * elements_per_channel; + at::Float8_e4m3fn* quant_dst = quant_t.data_ptr() + c * elements_per_channel; + + // Step 1: Compute channel-wise max using AVX512 + float channel_max = 0.0f; + int64_t i = 0; + + // Process 32 elements at a time for max computation + for (; i <= elements_per_channel - 32; i += 32) { + // Load 32 BF16 values (2x 256-bit vectors) + __m256i src_vec1 = _mm256_loadu_si256(reinterpret_cast(&channel_src[i])); + __m256i src_vec2 = _mm256_loadu_si256(reinterpret_cast(&channel_src[i + 16])); + + // Convert BF16 to FP32 + __m512i fp32_int1 = _mm512_cvtepu16_epi32(src_vec1); + __m512 fp32_vec1 = _mm512_castsi512_ps(_mm512_slli_epi32(fp32_int1, 16)); + + __m512i fp32_int2 = _mm512_cvtepu16_epi32(src_vec2); + __m512 fp32_vec2 = _mm512_castsi512_ps(_mm512_slli_epi32(fp32_int2, 16)); + + // Compute absolute values + __m512 abs_vec1 = _mm512_abs_ps(fp32_vec1); + __m512 abs_vec2 = _mm512_abs_ps(fp32_vec2); + + // Find max in each vector + float max1 = _mm512_reduce_max_ps(abs_vec1); + float max2 = _mm512_reduce_max_ps(abs_vec2); + channel_max = std::max(channel_max, std::max(max1, max2)); + } + + // Step 2: Apply quant_max and EPS to compute scale + float scale_val = std::max(channel_max / quant_max, eps); + float scale_reciprocal = 1.0f / scale_val; + scale_data[c] = scale_val; + + // Step 3: Scale and clamp using AVX512 (reuse the same loop structure) +#ifdef CPU_CAPABILITY_AVX10_2 + if (avx10_2_available()) { + quantize_bf16_block_avx10_2(channel_src, quant_dst, elements_per_channel, scale_reciprocal); + continue; + } +#endif + i = 0; + const __m512 scale_recip_vec = _mm512_set1_ps(scale_reciprocal); + const __m512 quant_max_vec = _mm512_set1_ps(quant_max); + const __m512 neg_quant_max_vec = _mm512_set1_ps(-quant_max); + + for (; i <= elements_per_channel - 32; i += 32) { + // Load 32 BF16 values + __m256i src_vec1 = _mm256_loadu_si256(reinterpret_cast(&channel_src[i])); + __m256i src_vec2 = _mm256_loadu_si256(reinterpret_cast(&channel_src[i + 16])); + + // Convert BF16 to FP32 + __m512 fp32_vec1 = _mm512_castsi512_ps(_mm512_slli_epi32(_mm512_cvtepu16_epi32(src_vec1), 16)); + __m512 fp32_vec2 = _mm512_castsi512_ps(_mm512_slli_epi32(_mm512_cvtepu16_epi32(src_vec2), 16)); + + // Scale and clamp + __m512 scaled_vec1 = _mm512_mul_ps(fp32_vec1, scale_recip_vec); + __m512 clamped_vec1 = _mm512_min_ps(_mm512_max_ps(scaled_vec1, neg_quant_max_vec), quant_max_vec); + + __m512 scaled_vec2 = _mm512_mul_ps(fp32_vec2, scale_recip_vec); + __m512 clamped_vec2 = _mm512_min_ps(_mm512_max_ps(scaled_vec2, neg_quant_max_vec), quant_max_vec); + + __m128i fp8_vec1 = cvtfp32_fp8e4m3(clamped_vec1); + __m128i fp8_vec2 = cvtfp32_fp8e4m3(clamped_vec2); + _mm_storeu_si128(reinterpret_cast<__m128i*>(quant_dst + i), fp8_vec1); + _mm_storeu_si128(reinterpret_cast<__m128i*>(quant_dst + i + 16), fp8_vec2); + } + } + }); + + return std::make_tuple(quant_t, scale_tensor); +} + +std::tuple +_quantize_fp8e4m3_bf16_per_tensor_with_scale(const at::Tensor& t, at::Tensor& scale_tensor) { + constexpr float quant_max = 448.0f; // torch.finfo(torch.float8_e4m3fn).max + constexpr float eps = std::numeric_limits::epsilon(); + + // Input validation + assert(t.scalar_type() == at::ScalarType::BFloat16); + + auto t_bf16 = t.contiguous(); + auto scale_tensor_contig = scale_tensor.contiguous().to(at::ScalarType::Float); + + // Get scale value (handle scalar or tensor) + float scale_val; + if (scale_tensor_contig.numel() == 1) { + scale_val = scale_tensor_contig.item(); + } else { + // Take max if scale is a tensor (though should be scalar for global quantization) + scale_val = scale_tensor_contig.max().item(); + } + + // Apply EPS to scale + scale_val = std::max(scale_val, eps); + float scale_reciprocal = 1.0f / scale_val; + + at::Tensor scale_output = at::tensor({scale_val}, scale_tensor_contig.options()); + + // Apply scale and clamp using AVX512 + const at::BFloat16* src_data = t_bf16.data_ptr(); + at::Tensor quant_t = at::empty(t_bf16.sizes(), t_bf16.options().dtype(at::kFloat8_e4m3fn)); + int64_t total_elements = t_bf16.numel(); + const __m512 scale_recip_vec = _mm512_set1_ps(scale_reciprocal); + const __m512 quant_max_vec = _mm512_set1_ps(quant_max); + const __m512 neg_quant_max_vec = _mm512_set1_ps(-quant_max); + int64_t num_channels = t_bf16.size(0); + int64_t elements_per_channel = total_elements / num_channels; + assert(elements_per_channel % 32 == 0); // do not consider tile currently + // Process in parallel blocks + at::parallel_for(0, num_channels, 1, [&](int64_t start, int64_t end) { + for (int64_t c = start; c < end; ++c) { + const at::BFloat16* src_data = t_bf16.data_ptr() + c * elements_per_channel; + at::Float8_e4m3fn* quant_t_data = quant_t.data_ptr() + c * elements_per_channel; + +#ifdef CPU_CAPABILITY_AVX10_2 + if (avx10_2_available()) { + quantize_bf16_block_avx10_2(src_data, quant_t_data, elements_per_channel, scale_reciprocal); + continue; + } +#endif + + int64_t i = 0; + // Process 32 elements at a time using AVX512 + for (; i <= elements_per_channel - 32; i += 32) { + // Load 32 BF16 values (2x 256-bit vectors) + __m256i src_vec1 = _mm256_loadu_si256(reinterpret_cast(&src_data[i])); + __m256i src_vec2 = _mm256_loadu_si256(reinterpret_cast(&src_data[i + 16])); + + // Convert BF16 to FP32 + __m512 fp32_vec1 = _mm512_castsi512_ps(_mm512_slli_epi32(_mm512_cvtepu16_epi32(src_vec1), 16)); + __m512 fp32_vec2 = _mm512_castsi512_ps(_mm512_slli_epi32(_mm512_cvtepu16_epi32(src_vec2), 16)); + + // Scale and clamp + __m512 scaled_vec1 = _mm512_mul_ps(fp32_vec1, scale_recip_vec); + __m512 clamped_vec1 = _mm512_min_ps(_mm512_max_ps(scaled_vec1, neg_quant_max_vec), quant_max_vec); + + __m512 scaled_vec2 = _mm512_mul_ps(fp32_vec2, scale_recip_vec); + __m512 clamped_vec2 = _mm512_min_ps(_mm512_max_ps(scaled_vec2, neg_quant_max_vec), quant_max_vec); + + __m128i fp8_vec1 = cvtfp32_fp8e4m3(clamped_vec1); + __m128i fp8_vec2 = cvtfp32_fp8e4m3(clamped_vec2); + _mm_storeu_si128(reinterpret_cast<__m128i*>(quant_t_data + i), fp8_vec1); + _mm_storeu_si128(reinterpret_cast<__m128i*>(quant_t_data + i + 16), fp8_vec2); + } + } + }); + + return std::make_tuple(quant_t, scale_output); +} + +std::tuple +_quantize_fp8e4m3_vec(const at::Tensor& t, bool channelwise, c10::optional scale_opt) { + if (channelwise) { + return _quantize_fp8e4m3_bf16_per_tensor_no_scale(t); + } else { + assert(scale_opt.has_value()); + return _quantize_fp8e4m3_bf16_per_tensor_with_scale(t, scale_opt.value()); + } +} + +// Public wrapper for torch op registration +std::tuple +quantize_fp8e4m3_vec(const at::Tensor& t, bool channelwise, c10::optional scale_opt) { + return _quantize_fp8e4m3_vec(t, channelwise, scale_opt); +} + +at::Tensor fp8_scaled_mm_with_quant( + const at::Tensor& act, + const std::optional& act_scales, + bool channelwise, + const at::Tensor& weight, + const at::Tensor& weight_scales, + const std::optional& bias, + at::ScalarType output_dtype) { + // TODO: refine here to use tensor ptr and in below dispatch api. + std::tuple quant_act = act.scalar_type() == at::ScalarType::BFloat16 + ? _quantize_fp8e4m3_vec(act, channelwise, act_scales) + : _quantize_fp8e4m3(act, channelwise, act_scales); + auto input = std::get<0>(quant_act); + auto input_scales = std::get<1>(quant_act); + int64_t N = weight.dim() == 4 ? weight.size(0) * weight.size(-1) : weight.size(0); + int act_quant_mode = input_scales.numel() == 1 ? PER_TENSOR + : input_scales.numel() == input.numel() / input.size(-1) ? PER_ROW + : PER_GROUP; + int wei_quant_mode = weight_scales.numel() == 1 ? PER_TENSOR : weight_scales.numel() == N ? PER_ROW : PER_GROUP; + // Case to fall back + if (weight.dim() == 2) { + TORCH_CHECK( + act_quant_mode != PER_GROUP && wei_quant_mode != PER_GROUP, + "FP8 linear: Per-group quantization is not supported in the fallback path"); + auto y_fp32 = at::linear(input.to(at::kFloat).mul_(input_scales), weight.to(at::kFloat).mul_(weight_scales), bias); + return y_fp32.to(output_dtype); + } + + static bool cpublas_can_pack = cpublas_could_pack(); + auto out_sizes = input.sizes().vec(); + out_sizes.back() = N; + auto output = at::empty(out_sizes, input.options().dtype(output_dtype)); + +#define AT_DISPATCH_FP8_LINEAR_KERNEL(OUT_DTYPE, CAN_PACK, A_QUANT_MODE, B_QUANT_MODE, ...) \ + AT_DISPATCH_BOOL_NO_RETURN( \ + CAN_PACK, \ + "cpublas_can_pack", \ + can_pack, \ + AT_DISPATCH_QUANT_MODE_NO_RETURN( \ + A_QUANT_MODE, \ + "act_quant_mode", \ + a_quant_mode, \ + AT_DISPATCH_QUANT_MODE_NO_RETURN( \ + B_QUANT_MODE, \ + "wei_quant_mode", \ + b_quant_mode, \ + AT_DISPATCH_OUT_TYPES(OUT_DTYPE, "out_dtype", __VA_ARGS__)))) + + AT_DISPATCH_FP8_LINEAR_KERNEL(output_dtype, cpublas_can_pack, act_quant_mode, wei_quant_mode, [&]() { + _float8_linear_impl( + input, input_scales, weight, weight_scales, bias, output); + }); + return output; +} + +at::Tensor float8_linear_impl( + const at::Tensor& input, + const at::Tensor& input_scales, + const at::Tensor& weight, + const at::Tensor& weight_scales, + const std::optional& bias, + at::ScalarType output_dtype) { + int64_t N = weight.dim() == 4 ? weight.size(0) * weight.size(-1) : weight.size(0); + int act_quant_mode = input_scales.numel() == 1 ? PER_TENSOR + : input_scales.numel() == input.numel() / input.size(-1) ? PER_ROW + : PER_GROUP; + int wei_quant_mode = weight_scales.numel() == 1 ? PER_TENSOR : weight_scales.numel() == N ? PER_ROW : PER_GROUP; + // Case to fall back + if (weight.dim() == 2) { + TORCH_CHECK( + act_quant_mode != PER_GROUP && wei_quant_mode != PER_GROUP, + "FP8 linear: Per-group quantization is not supported in the fallback path"); + auto y_fp32 = at::linear(input.to(at::kFloat).mul_(input_scales), weight.to(at::kFloat).mul_(weight_scales), bias); + return y_fp32.to(output_dtype); + } + + static bool cpublas_can_pack = cpublas_could_pack(); + auto out_sizes = input.sizes().vec(); + out_sizes.back() = N; + auto output = at::empty(out_sizes, input.options().dtype(output_dtype)); + +#define AT_DISPATCH_FP8_LINEAR_KERNEL(OUT_DTYPE, CAN_PACK, A_QUANT_MODE, B_QUANT_MODE, ...) \ + AT_DISPATCH_BOOL_NO_RETURN( \ + CAN_PACK, \ + "cpublas_can_pack", \ + can_pack, \ + AT_DISPATCH_QUANT_MODE_NO_RETURN( \ + A_QUANT_MODE, \ + "act_quant_mode", \ + a_quant_mode, \ + AT_DISPATCH_QUANT_MODE_NO_RETURN( \ + B_QUANT_MODE, \ + "wei_quant_mode", \ + b_quant_mode, \ + AT_DISPATCH_OUT_TYPES(OUT_DTYPE, "out_dtype", __VA_ARGS__)))) + + AT_DISPATCH_FP8_LINEAR_KERNEL(output_dtype, cpublas_can_pack, act_quant_mode, wei_quant_mode, [&]() { + _float8_linear_impl( + input, input_scales, weight, weight_scales, bias, output); + }); + return output; +} \ No newline at end of file diff --git a/csrc/cpu/sgl-kernels/moe.cpp b/csrc/cpu/sgl-kernels/moe.cpp index 3afdc1bd6791..09a170eb6d4c 100644 --- a/csrc/cpu/sgl-kernels/moe.cpp +++ b/csrc/cpu/sgl-kernels/moe.cpp @@ -836,6 +836,11 @@ static inline void check_moe_scales( TORCH_CHECK(w2_scale.has_value(), "missing w2_scale for mxfp4."); TORCH_CHECK(w1_scale.value().scalar_type() == at::kByte, "expect w1_scale to be uint8."); TORCH_CHECK(w2_scale.value().scalar_type() == at::kByte, "expect w2_scale to be uint8."); + } else if constexpr (quant == CPUQuantMethod::FP8_W8A8) { + TORCH_CHECK(w1_scale.has_value(), "missing w1_scale for fp8 w8a8."); + TORCH_CHECK(w2_scale.has_value(), "missing w2_scale for fp8 w8a8."); + TORCH_CHECK(block_size.has_value(), "missing block_size for fp8 w8a8."); + TORCH_CHECK(block_size.value().size() == 2, "expect block_size.size() to be 2."); } } @@ -850,9 +855,14 @@ static inline void check_moe_scales( check_moe_scales(w1_scale, w2_scale, block_size); } else if (moe_comp_method == CPUQuantMethod::MXFP4) { check_moe_scales(w1_scale, w2_scale, block_size); + } else if (moe_comp_method == CPUQuantMethod::FP8_W8A8) { + check_moe_scales(w1_scale, w2_scale, block_size); } } +// CHECK_MOE_SCALES_FP8: validate scale shapes for FP8_W8A16. +// w1s shape [E, 2N/block_N, K/block_K, block_N]: DIM0=1, DIM1=2 +// w2s shape [E, K/block_N, N/block_K]: DIM0=1, DIM1=2 #define CHECK_MOE_SCALES_FP8(DIM0, DIM1) \ auto w1s = w1_scale.value(); \ auto w2s = w2_scale.value(); \ @@ -864,6 +874,17 @@ static inline void check_moe_scales( TORCH_CHECK(w2s.size(DIM0) == div_up(K, block_size_N)); \ TORCH_CHECK(w2s.size(DIM1) == div_up(N, block_size_K)) +// CHECK_MOE_SCALES_FP8_W8A8: extract scale info for FP8_W8A8 without shape +// validation, because w1s is pre-packed by float8_linear_prepack_cpu with +// shape [E, 2N/BLOCK_N, G, BLOCK_N] where BLOCK_N=32 != block_size_N(128). +// This matches the sglang upstream behavior (checks are commented out there). +#define CHECK_MOE_SCALES_FP8_W8A8(DIM0, DIM1) \ + auto w1s = w1_scale.value(); \ + auto w2s = w2_scale.value(); \ + auto block_size_val = block_size.value(); \ + int64_t block_size_N = block_size_val[0]; \ + int64_t block_size_K = block_size_val[1] + // hidden_states: [M, K] // w1: [E, 2N, K] or [E, 2N, K / 2] for uint8 // w2: [E, K, N] or [E, K, N / 2] for uint8 @@ -883,6 +904,7 @@ void fused_experts_cpu( const std::optional& w2_scale, const std::optional& w1_zero, const std::optional& w2_zero, + const std::optional& a1_scale, const std::optional> block_size, const std::optional& w1_bias, const std::optional& w2_bias, @@ -895,7 +917,10 @@ void fused_experts_cpu( constexpr int64_t BLOCK_M = block_size_m(); constexpr int64_t BLOCK_N = block_size_n(); - const auto st = hidden_states.scalar_type(); + // FP8_W8A8 hidden_states input is FP8; scalar_t should be BF16 for output + const auto st = (moe_comp_method == CPUQuantMethod::FP8_W8A8) + ? at::kBFloat16 + : hidden_states.scalar_type(); // TODO: fused_topk_torch_native (CPU fallback for models like MiniMax) // returns int64 topk_ids; fused_experts_cpu requires int32. Remove the typecast after topk kernel is provided auto topk_ids_ = topk_ids.scalar_type() == at::kInt ? topk_ids : topk_ids.to(at::kInt); @@ -908,6 +933,9 @@ void fused_experts_cpu( if (moe_comp_method == CPUQuantMethod::INT4_W4A8 && is_vnni) { CHECK_DIM(4, w1); CHECK_DIM(4, w2); + } else if (moe_comp_method == CPUQuantMethod::FP8_W8A8) { + // w1 is prepackaged by float8_linear_prepack_cpu (5D), skip w1 dim check + CHECK_DIM(3, w2); } else { CHECK_DIM(3, w1); CHECK_DIM(3, w2); @@ -924,8 +952,9 @@ void fused_experts_cpu( int64_t M = hidden_states.size(0); int64_t K = hidden_states.size(1); - int64_t N = moe_comp_method == CPUQuantMethod::INT4_W4A8 ? w1_scale.value().size(1) * w1_scale.value().size(3) / 2 - : w1.size(1) / 2; + int64_t N = moe_comp_method == CPUQuantMethod::INT4_W4A8 + ? w1_scale.value().size(1) * w1_scale.value().size(3) / 2 + : (moe_comp_method == CPUQuantMethod::FP8_W8A8 ? w2.size(2) : w1.size(1) / 2); int64_t E = w1.size(0); int64_t topk = topk_weights_.size(1); @@ -935,7 +964,10 @@ void fused_experts_cpu( // check weight shapes CHECK_EQ(w2.size(0), E); - if (!(moe_comp_method == CPUQuantMethod::INT4_W4A8)) { + if (moe_comp_method == CPUQuantMethod::FP8_W8A8) { + CHECK_EQ(w2.size(1), K); + CHECK_EQ(packed_w2.size(2), packed_N); + } else if (!(moe_comp_method == CPUQuantMethod::INT4_W4A8)) { CHECK_EQ(w2.size(1), K); CHECK_EQ(packed_w1.size(2), packed_K / (moe_comp_method == CPUQuantMethod::INT4_W4A8 ? 2 : 1)); CHECK_EQ(packed_w2.size(2), packed_N / (moe_comp_method == CPUQuantMethod::INT4_W4A8 ? 2 : 1)); @@ -993,22 +1025,35 @@ void fused_experts_cpu( // 5. Aq_tmp : [M, K] or [M * topk, N] // 6. As_tmp : [M * topk] // - // for fp8 w8a16 and mxfp4: + // for fp8 w8a16, mxfp4, and fp8 w8a8: // 7. intermediate_cache0 : [M * topk, 2N] // 8. B_tmp : [T, MAX_CACHE_BLOCK_SIZE, BLOCK_N, std::max(K, N)] // + // for fp8 w8a8 additionally: + // 9. Ukernel_tmp : [T, 2 * BLOCK_M * BLOCK_N] (float32 for FP8×FP8 brgemm) + // int64_t buffer_size_nbytes = M * topk * N * 2 + M * topk * K * 2 + num_threads * BLOCK_M * K * - (moe_comp_method == CPUQuantMethod::INT8_W8A8 | moe_comp_method == CPUQuantMethod::INT4_W4A8 ? 1 : 2) + + (moe_comp_method == CPUQuantMethod::INT8_W8A8 | moe_comp_method == CPUQuantMethod::INT4_W4A8 + | moe_comp_method == CPUQuantMethod::FP8_W8A8 ? 1 : 2) + num_threads * 2 * BLOCK_M * BLOCK_N * sizeof(float); if (moe_comp_method == CPUQuantMethod::INT8_W8A8) { buffer_size_nbytes += std::max(M * K, M * topk * N) + M * topk * sizeof(float); } - if (moe_comp_method == CPUQuantMethod::FP8_W8A16 || moe_comp_method == CPUQuantMethod::MXFP4) { + if (moe_comp_method == CPUQuantMethod::FP8_W8A16 || moe_comp_method == CPUQuantMethod::MXFP4 + || moe_comp_method == CPUQuantMethod::FP8_W8A8) { buffer_size_nbytes += M * topk * 2 * N * 2 + num_threads * MAX_CACHE_BLOCK_SIZE * BLOCK_N * std::max(K, N) * 2; } + if (moe_comp_method == CPUQuantMethod::FP8_W8A8) { + // Ukernel buffer: float32 workspace per thread for FP8×FP8 brgemm + buffer_size_nbytes += num_threads * 2 * BLOCK_M * BLOCK_N * sizeof(float); +#ifndef CPUBLAS_BRGEMM_F8F8F32 + // BF16 fallback: also need dqA [T, BLOCK_M, BLOCK_K] + dqB [T, BLOCK_K, BLOCK_N] + buffer_size_nbytes += num_threads * (BLOCK_M * BLOCK_K + BLOCK_K * BLOCK_N) * 2; +#endif + } if (moe_comp_method == CPUQuantMethod::INT4_W4A8) { buffer_size_nbytes += M * topk * 2 * N * 2 + std::max(M * K, M * topk * N) + M * topk * sizeof(float) + num_threads * 2 * get_4bit_block_k_size(K / w1_scale.value().size(2)) * BLOCK_N; @@ -1181,6 +1226,47 @@ void fused_experts_cpu( E, topk, num_tokens_post_pad); + } else if (moe_comp_method == CPUQuantMethod::FP8_W8A8) { + // Stage 1: FP8×FP8 GEMM (hidden_states is FP8 input) + // Stage 2: BF16×FP8 GEMM (W8A16 path for down-projection) + at::Float8_e4m3fn* __restrict__ A_tmp = + (at::Float8_e4m3fn*)((void*)(intermediate_cache2 + M * topk * K)); + float* __restrict__ C_tmp = (float*)((void*)(A_tmp + num_threads * BLOCK_M * K)); + scalar_t* __restrict__ intermediate_cache0 = + (scalar_t*)((void*)(C_tmp + num_threads * 2 * BLOCK_M * BLOCK_N)); + scalar_t* __restrict__ B_tmp = + (scalar_t*)((void*)(intermediate_cache0 + M * topk * 2 * N)); + float* __restrict__ Ukernel_tmp = + (float*)((void*)(B_tmp + num_threads * MAX_CACHE_BLOCK_SIZE * BLOCK_N * std::max(K, N))); + + CHECK_MOE_SCALES_FP8_W8A8(1, 2); + fused_experts_fp8_a8_kernel_impl( + out_hidden_states.data_ptr(), + intermediate_cache0, + intermediate_cache1, + intermediate_cache2, + A_tmp, + B_tmp, + C_tmp, + Ukernel_tmp, + hidden_states.data_ptr(), + packed_w1.data_ptr(), + packed_w2.data_ptr(), + a1_scale.has_value() ? a1_scale.value().data_ptr() : nullptr, + w1s.data_ptr(), + w2s.data_ptr(), + block_size_N, + block_size_K, + topk_weights_.data_ptr(), + sorted_ids, + expert_ids, + offsets, + M, + N, + K, + E, + topk, + num_tokens_post_pad); } else { scalar_t* __restrict__ A_tmp = intermediate_cache2 + M * topk * K; float* __restrict__ C_tmp = (float*)((void*)(A_tmp + num_threads * BLOCK_M * K)); diff --git a/csrc/cpu/sgl-kernels/moe_fp8_w8a8.cpp b/csrc/cpu/sgl-kernels/moe_fp8_w8a8.cpp new file mode 100644 index 000000000000..0f393a34a853 --- /dev/null +++ b/csrc/cpu/sgl-kernels/moe_fp8_w8a8.cpp @@ -0,0 +1,432 @@ +// Adapted from +// https://github.com/sgl-project/sglang/tree/main/sgl-kernel/csrc/cpu + +// clang-format off + +#include + +#include "common.h" +#include "gemm.h" +#include "moe.h" + +namespace { + +// Byte-wise copy for FP8 elements (no remainder assumption) +inline void copy_stub2( + at::Float8_e4m3fn* __restrict__ out, + const at::Float8_e4m3fn* __restrict__ input, + int64_t size) { + for (int64_t d = 0; d < size; d++) { + out[d] = input[d]; + } +} + +#define PER_TENSOR 1 +#define PER_ROW 2 +#define PER_GROUP 3 + +// Store float32 GEMM result to output buffer with dequantization and dtype +// conversion. Activations are PER_ROW quantized; weights are PER_GROUP quantized. +// Uses AVX512 on capable platforms. +#if defined(CPU_CAPABILITY_AVX512) + +template +inline void store_out( + const float* y_buf, + out_dtype* c_ptr, + int64_t M, + int64_t lda, + const float* scales_a, + const float* scales_b, + const float* bias) { + float a_scale = 1.0, b_scale = 1.0; + __m512 va_scale, vb_scale; + if constexpr (act_quant_mode == PER_TENSOR) { + a_scale = *scales_a; + } + if constexpr (wei_quant_mode == PER_TENSOR) { + b_scale = *scales_b; + vb_scale = _mm512_set1_ps(b_scale); + } + for (int i = 0; i < M; ++i) { + if constexpr (act_quant_mode == PER_ROW) { + a_scale = *(scales_a + i); + } + if constexpr (act_quant_mode != PER_GROUP) { + va_scale = _mm512_set1_ps(a_scale); + } + constexpr int N_UNROLL = N / 16; + c10::ForcedUnroll{}([&](auto idx) { + constexpr int j = idx * 16; + __m512 y_vec = _mm512_loadu_ps(y_buf + i * N + j); + __m512 bias_vec = bias ? _mm512_loadu_ps(bias + j) : _mm512_setzero_ps(); + if constexpr (act_quant_mode != PER_GROUP) { + y_vec = _mm512_mul_ps(y_vec, va_scale); + } + if constexpr (wei_quant_mode == PER_ROW) { + vb_scale = _mm512_loadu_ps(scales_b + j); + } + if constexpr (wei_quant_mode != PER_GROUP) { + y_vec = _mm512_mul_ps(y_vec, vb_scale); + } + y_vec = _mm512_add_ps(y_vec, bias_vec); + if constexpr (std::is_same::value) { + _mm512_storeu_ps(c_ptr + i * lda + j, y_vec); + } else if constexpr (std::is_same::value) { + __m256i y_bf16_vec = at::vec::cvtfp32_bf16(y_vec); + _mm256_storeu_si256(reinterpret_cast<__m256i*>(c_ptr + i * lda + j), y_bf16_vec); + } else if constexpr (std::is_same::value) { + __m256i y_fp16_vec = at::vec::cvtfp32_fp16(y_vec); + _mm256_storeu_si256(reinterpret_cast<__m256i*>(c_ptr + i * lda + j), y_fp16_vec); + } else { + TORCH_CHECK(false, "Unsupported output dtype"); + } + }); + constexpr int tail_start = N / 16 * 16; + for (int j = tail_start; j < N; ++j) { + if constexpr (wei_quant_mode == PER_ROW) { + b_scale = scales_b[j]; + } + c_ptr[i * lda + j] = static_cast(y_buf[i * N + j] * a_scale * b_scale); + } + } // for M +} + +#else // no AVX512 + +template +inline void store_out( + const float* y_buf, + out_dtype* c_ptr, + int64_t M, + int64_t lda, + const float* scales_a, + const float* scales_b, + const float* bias) { + float a_scale = 1.0, b_scale = 1.0; + if constexpr (act_quant_mode == PER_TENSOR) { + a_scale = *scales_a; + } + if constexpr (wei_quant_mode == PER_TENSOR) { + b_scale = *scales_b; + } + for (int i = 0; i < M; ++i) { + if constexpr (act_quant_mode == PER_ROW) { + a_scale = *(scales_a + i); + } + for (int j = 0; j < N; ++j) { + if constexpr (wei_quant_mode == PER_ROW) { + b_scale = scales_b[j]; + } + float val = y_buf[i * N + j] * a_scale * b_scale; + if (bias) val += bias[j]; + c_ptr[i * lda + j] = static_cast(val); + } + } +} + +#endif // CPU_CAPABILITY_AVX512 + +} // anonymous namespace + +// FP8 W8A8 fused MoE kernel. +// +// hidden_states: [M, K] FP8 E4M3, pre-quantized activation +// packed_w1: [E, 2*Nc, Kc, BLOCK_K, BLOCK_N] FP8 W8A8 prepack (float8_linear_prepack_cpu) +// packed_w2: [E, K, N_packed] FP8 W8A16 VNNI pack (convert_weight_packed) +// As: [M] float32 per-token activation scales +// w1s: [E, num_groups, 2N] float32 per-group weight scales for w1 +// w2s: [E, ...] float32 per-group weight scales for w2 +// +// Stage 1: hidden_states(FP8) @ w1(FP8) → ic0(BF16) [M*topk, 2N] +// Uses FP8×FP8 GEMM (tinygemm_kernel with FP8 inputs) +// Stage 1.5: ic1 = silu(ic0[:,:N]) * ic0[:,N:] [M*topk, N] +// Stage 2: ic1(BF16) @ w2(FP8) → ic2(BF16) [M*topk, K] +// Uses BF16×FP8 GEMM (W8A16 path, tinygemm_kernel) +// Stage 3: out = weighted_sum(ic2, topk_weights) [M, K] + +template +void fused_experts_fp8_a8_kernel_impl( + scalar_t* __restrict__ output, + scalar_t* __restrict__ ic0, + scalar_t* __restrict__ ic1, + scalar_t* __restrict__ ic2, + at::Float8_e4m3fn* __restrict__ A_tmp, + scalar_t* __restrict__ B_tmp, + float* __restrict__ C_tmp, + float* __restrict__ Ukernel_tmp, + const at::Float8_e4m3fn* __restrict__ input, + const at::Float8_e4m3fn* __restrict__ packed_w1, + const at::Float8_e4m3fn* __restrict__ packed_w2, + const float* __restrict__ As, + const float* __restrict__ w1s, + const float* __restrict__ w2s, + int64_t block_size_N, + int64_t block_size_K, + const float* __restrict__ topk_weights, + const int32_t* __restrict__ sorted_ids, + const int32_t* __restrict__ expert_ids, + const int32_t* __restrict__ offsets, + int64_t M, + int64_t N, + int64_t K, + int64_t E, + int64_t topk, + int64_t num_tokens_post_pad) { + // 1. intermediate_cache1 : [M * topk, N] + // 2. intermediate_cache2 : [M * topk, K] + // 3. A_tmp : [T, BLOCK_M * K] FP8 + // 4. C_tmp : [T, 2 * BLOCK_M * BLOCK_N] float32 + // 5. intermediate_cache0 : [M * topk, 2N] + // 6. B_tmp : [T, MAX_CACHE_BLOCK_SIZE, BLOCK_N, max(K, N)] scalar_t + // 7. Ukernel_tmp: [T, 2 * BLOCK_M * BLOCK_N] float32 + // 8. (non-FP8-brgemm only) dqA [T, BLOCK_M * BLOCK_K] + dqB [T, BLOCK_K * BLOCK_N] in BF16 + // appended after Ukernel_tmp; see moe.cpp FP8_W8A8 buffer allocation + + constexpr int64_t BLOCK_M = block_size_m(); + constexpr int64_t BLOCK_N_VAL = block_size_n(); // = 32 + const int64_t KB = K / BLOCK_K; // K blocks + + int64_t B_tmp_size_per_thread = MAX_CACHE_BLOCK_SIZE * BLOCK_N_VAL * std::max(K, N); + + // stage 1: intermediate_cache0 = hidden_states @ w1 + const int64_t MB = div_up(num_tokens_post_pad, BLOCK_M); + const int64_t NB = div_up(N, BLOCK_N_VAL); + + int64_t scale_size_N = div_up(2 * N, block_size_N); + int64_t scale_size_K = div_up(K, block_size_K); + int64_t blocks_n_per_group = block_size_N / BLOCK_N_VAL; + int64_t num_groups = div_up(K, block_size_K); // G + int64_t blocks_k_per_group = block_size_K / BLOCK_K; + const int64_t stride_e = 2 * N * K; // per-expert stride for w1 (total elements) + bool use_brgemm = true; + int64_t num_thread = at::get_num_threads(); + + // For the BF16 fallback path (no native FP8 brgemm), tinygemm_kernel needs temporary + // BF16 buffers to dequantize FP8 inputs before running AMX-BF16 brgemm. + // These are placed after the Ukernel_tmp region (see moe.cpp FP8_W8A8 buffer allocation). +#ifndef CPUBLAS_BRGEMM_F8F8F32 + // dqA: [T, BLOCK_M, BLOCK_K] BF16 + // dqB: [T, BLOCK_K, BLOCK_N] BF16 + // Layout: immediately after Ukernel_tmp[num_thread * 2 * BLOCK_M * BLOCK_N_VAL] + at::BFloat16* __restrict__ dq_base = reinterpret_cast( + Ukernel_tmp + num_thread * 2 * BLOCK_M * BLOCK_N_VAL); + const int64_t dq_per_thread = BLOCK_M * BLOCK_K + BLOCK_K * BLOCK_N_VAL; +#endif + + at::parallel_for(0, MB * NB, 1, [&](int64_t begin, int64_t end) { + int tid = get_thread_num(); + // A_tmp: FP8 activation buffer for this thread [BLOCK_M, K] + at::Float8_e4m3fn* __restrict__ A = A_tmp + tid * BLOCK_M * K; + float* __restrict__ C0 = C_tmp + tid * 2 * BLOCK_M * BLOCK_N_VAL; + float* __restrict__ C1 = C0 + BLOCK_M * BLOCK_N_VAL; + alignas(64) float As_[BLOCK_M]; + float* __restrict__ ukernel_buf_1 = Ukernel_tmp + tid * 2 * BLOCK_M * BLOCK_N_VAL; +#ifndef CPUBLAS_BRGEMM_F8F8F32 + at::BFloat16* __restrict__ dqA_buf = dq_base + tid * dq_per_thread; + at::BFloat16* __restrict__ dqB_buf = dqA_buf + BLOCK_M * BLOCK_K; +#else + at::BFloat16* __restrict__ dqA_buf = nullptr; + at::BFloat16* __restrict__ dqB_buf = nullptr; +#endif + float* __restrict__ ukernel_buf_2 = ukernel_buf_1 + BLOCK_M * BLOCK_N_VAL; + auto ldsa = 1; // activation per-row quantized + + for (int64_t i = begin; i < end; ++i) { + int64_t mb = i / NB; + int64_t nb = i % NB; + int64_t nb1 = nb + NB; // gate is first N columns, up is second N columns + + int64_t n_size = std::min(N - nb * BLOCK_N_VAL, BLOCK_N_VAL); + + // B (weight) for expert expert_id: gate+up fused [2N, K] prepackaged as FP8 + int32_t expert_id = expert_ids[mb]; + const at::Float8_e4m3fn* __restrict__ B = packed_w1 + expert_id * stride_e; + const float* __restrict__ Bs = w1s + expert_id * num_groups * (2 * N); + + // Load activation tile for this M-block + const int32_t* A_ids = sorted_ids + mb * BLOCK_M; + int64_t m_size = offsets[mb + 1] - offsets[mb]; + + for (int64_t m = 0; m < m_size; ++m) { + int32_t index = A_ids[m] / topk; + copy_stub2(A + m * K, input + index * K, K); + As_[m] = As[index]; + } + + const int64_t offset = offsets[mb]; + + // Gate path (nb): w1[:N, :] + zero_buffer(C0, BLOCK_M * BLOCK_N_VAL); + for (int kci = 0; kci < KB; ++kci) { + // FP8×FP8 tinygemm: act_quant=PER_ROW(2), wei_quant=PER_GROUP(3) + tinygemm_kernel( + /* C */ C0, + /* A */ A + kci * BLOCK_K, + /* scales_a*/ As_ + kci / blocks_k_per_group, + /* B */ B + (nb * KB + kci) * BLOCK_K * BLOCK_N_VAL, + /* scales_b*/ Bs + nb * BLOCK_N_VAL * num_groups + kci / blocks_k_per_group * BLOCK_N_VAL, + /* M */ m_size, + /* K */ BLOCK_K, + /* lda */ K, + /* ldc */ BLOCK_N_VAL, + /* ldsa */ ldsa, + /* ukernel */ ukernel_buf_1, + /* dqA_buf */ dqA_buf, + /* dqB_buf */ dqB_buf); + } + store_out( + C0, + ic0 + offset * 2 * N + nb * BLOCK_N_VAL, + m_size, + 2 * N, // lda + As_, + nullptr, + nullptr); + + // Up path (nb1): w1[N:, :] + zero_buffer(C1, BLOCK_M * BLOCK_N_VAL); + for (int kci = 0; kci < KB; ++kci) { + tinygemm_kernel( + /* C */ C1, + /* A */ A + kci * BLOCK_K, + /* scales_a*/ As_ + kci / blocks_k_per_group, + /* B */ B + (nb1 * KB + kci) * BLOCK_K * BLOCK_N_VAL, + /* scales_b*/ Bs + nb1 * BLOCK_N_VAL * num_groups + kci / blocks_k_per_group * BLOCK_N_VAL, + /* M */ m_size, + /* K */ BLOCK_K, + /* lda */ K, + /* ldc */ BLOCK_N_VAL, + /* ldsa */ ldsa, + /* ukernel */ ukernel_buf_2, + /* dqA_buf */ dqA_buf, + /* dqB_buf */ dqB_buf); + } + store_out( + C1, + ic0 + offset * 2 * N + nb1 * BLOCK_N_VAL, + m_size, + 2 * N, // lda + As_, + nullptr, + nullptr); + } + if (use_brgemm) { + at::native::cpublas::brgemm_release(); + } + }); + + // stage 1.5: intermediate_cache1 = silu(ic0[:, :N]) * ic0[:, N:] + at::parallel_for(0, M * topk, 0, [&](int64_t begin, int64_t end) { + for (int64_t m = begin; m < end; ++m) { + silu_and_mul_stub(ic1 + m * N, ic0 + m * 2 * N, ic0 + m * 2 * N + N, N); + } + }); + + // stage 2: intermediate_cache2 = intermediate_cache1 @ w2 + // w2: [E, K, N] FP8, VNNI packed; treated as W8A16 (BF16 act × FP8 weight) + const int64_t OC = K; // output channels (hidden dim) + const int64_t IC = N; // input channels (expert intermediate dim) + const int64_t MB2 = MB; + const int64_t NB2 = div_up(OC, BLOCK_N_VAL); + scale_size_N = div_up(K, block_size_N); + scale_size_K = div_up(N, block_size_K); + const int64_t stride_e2 = OC * IC; // per-expert stride for w2 + const int64_t stride_oc = IC; + + int64_t avg_M = std::max(int64_t(1), M * topk / E); + bool use_brgemm2 = can_use_brgemm(avg_M); + + parallel_2d(MB2, NB2, [&](int64_t mb0, int64_t mb1, int64_t nb0, int64_t nb1) { + int tid = get_thread_num(); + alignas(64) scalar_t C[BLOCK_M * BLOCK_K]; + + loop_2d(mb0, mb1, nb0, nb1, BLOCK_N_VAL * IC, [&](int64_t mb, int64_t nb, int64_t nb_offset) { + int64_t m_size = offsets[mb + 1] - offsets[mb]; + int64_t n_size = std::min(OC - nb * BLOCK_N_VAL, BLOCK_N_VAL); + + // A from ic1: [M*topk, N] in sorted order (contiguous after stage 1.5) + const scalar_t* __restrict__ A2 = ic1 + offsets[mb] * N; + const int32_t* A_ids = sorted_ids + mb * BLOCK_M; + + // B from w2: [E, K, N] VNNI packed, offset to expert + output block + int32_t expert_id = expert_ids[mb]; + const at::Float8_e4m3fn* __restrict__ B2 = + packed_w2 + expert_id * stride_e2 + nb * BLOCK_N_VAL * stride_oc; + const float* __restrict__ Bs2 = + w2s + expert_id * scale_size_N * scale_size_K + (nb / blocks_n_per_group) * scale_size_K; + + // Only unpack B on first visit or new expert + int32_t pre_expert_id = mb == 0 ? -1 : expert_ids[mb - 1]; + bool do_unpack = (mb == mb0) || (expert_id != pre_expert_id); + + tinygemm_kernel( + /* A */ A2, + /* B */ B2, + /* C */ C, + /* Btmp */ B_tmp + tid * B_tmp_size_per_thread + nb_offset * BLOCK_N_VAL * IC, + /* Ctmp */ C_tmp + tid * 2 * BLOCK_M * BLOCK_N_VAL, + /* Bbias */ nullptr, + /* scale */ Bs2, + /* M */ m_size, + /* N */ n_size, + /* K */ IC, + /* lda */ IC, + /* ldb */ n_size, + /* ldc */ BLOCK_N_VAL, + /* brg */ use_brgemm2, + /* block_sz_K */ block_size_K, + /* do_unpack */ do_unpack); + + // Scatter ic2 in original token order, weighting by topk_weights + for (int64_t m = 0; m < m_size; ++m) { + int32_t index = A_ids[m]; + float weight = topk_weights[index]; + copy_mul_stub(ic2 + index * K + nb * BLOCK_N_VAL, C + m * BLOCK_N_VAL, weight, n_size); + } + }); + + if (use_brgemm2) { + at::native::cpublas::brgemm_release(); + } + }); + + // stage 3: output = sum(ic2, dim=topk) [M, K] + at::parallel_for(0, M, 0, [&](int64_t begin, int64_t end) { + for (int64_t m = begin; m < end; ++m) { + sum_stub(output + m * K, ic2 + m * topk * K, topk, K); + } + }); +} + +#define INSTANTIATE_MOE_FP8_A8_TEMPLATE(TYPE) \ + template void fused_experts_fp8_a8_kernel_impl( \ + TYPE* __restrict__ output, \ + TYPE* __restrict__ ic0, \ + TYPE* __restrict__ ic1, \ + TYPE* __restrict__ ic2, \ + at::Float8_e4m3fn* __restrict__ A_tmp, \ + TYPE* __restrict__ B_tmp, \ + float* __restrict__ C_tmp, \ + float* __restrict__ Ukernel_tmp, \ + const at::Float8_e4m3fn* __restrict__ input, \ + const at::Float8_e4m3fn* __restrict__ packed_w1, \ + const at::Float8_e4m3fn* __restrict__ packed_w2, \ + const float* __restrict__ As, \ + const float* __restrict__ w1s, \ + const float* __restrict__ w2s, \ + int64_t block_size_N, \ + int64_t block_size_K, \ + const float* __restrict__ topk_weights, \ + const int32_t* __restrict__ sorted_ids, \ + const int32_t* __restrict__ expert_ids, \ + const int32_t* __restrict__ offsets, \ + int64_t M, \ + int64_t N, \ + int64_t K, \ + int64_t E, \ + int64_t topk, \ + int64_t num_tokens_post_pad) + +INSTANTIATE_MOE_FP8_A8_TEMPLATE(at::BFloat16); +INSTANTIATE_MOE_FP8_A8_TEMPLATE(at::Half); diff --git a/csrc/cpu/torch_bindings.cpp b/csrc/cpu/torch_bindings.cpp index 8f663a93c36d..accd5fcec354 100644 --- a/csrc/cpu/torch_bindings.cpp +++ b/csrc/cpu/torch_bindings.cpp @@ -73,6 +73,7 @@ void fused_experts_cpu(at::Tensor& out, at::Tensor& hidden_states, const std::optional& w2_scale, const std::optional& w1_zero, const std::optional& w2_zero, + const std::optional& a1_scale, const std::optional> block_size, const std::optional& w1_bias, const std::optional& w2_bias, @@ -91,6 +92,18 @@ at::Tensor fp8_scaled_mm_cpu(at::Tensor& mat1, at::Tensor& mat2, const std::optional& bias, at::ScalarType out_dtype, bool is_vnni); +// Adapted from sglang: FP8 W8A8 kernels +std::tuple float8_linear_prepack_impl( + const at::Tensor& weight, const at::Tensor& scales); +at::Tensor fp8_scaled_mm_with_quant(const at::Tensor& act, + const std::optional& act_scales, + bool channelwise, const at::Tensor& weight, + const at::Tensor& weight_scales, + const std::optional& bias, + at::ScalarType output_dtype); +std::tuple quantize_fp8e4m3_vec( + const at::Tensor& t, bool channelwise, c10::optional scale_opt); + // Adapted from sglang: MLA CPU kernels (AMX-only) void decode_attention_cpu(at::Tensor& query, at::Tensor& k_buffer, at::Tensor& v_buffer, at::Tensor& output, @@ -281,6 +294,7 @@ void activation_lut_bf16(torch::Tensor& out, torch::Tensor& input, const std::string& activation); bool cpu_attn_has_isa(const std::string& isa); +bool cpu_has_amx_fp8(); torch::Tensor get_scheduler_metadata( const int64_t num_req, const int64_t num_heads_q, @@ -611,7 +625,7 @@ TORCH_LIBRARY_EXPAND(TORCH_EXTENSION_NAME, ops) { "fused_experts_cpu(Tensor(a0!) out, Tensor hidden_states, Tensor w1, " "Tensor w2, Tensor topk_weights, Tensor topk_ids, " "int moe_comp_method, Tensor? w1_scale, Tensor? w2_scale, " - "Tensor? w1_zero, Tensor? w2_zero, int[]? block_size, " + "Tensor? w1_zero, Tensor? w2_zero, Tensor? a1_scale, int[]? block_size, " "Tensor? w1_bias, Tensor? w2_bias, float? alpha, float? limit, " "bool is_vnni) -> " "()"); @@ -629,7 +643,23 @@ TORCH_LIBRARY_EXPAND(TORCH_EXTENSION_NAME, ops) { "bool is_vnni) -> Tensor"); ops.impl("fp8_scaled_mm_cpu", torch::kCPU, &fp8_scaled_mm_cpu); - // Adapted from sglang: casual_conv1d kernels + // Adapted from sglang: FP8 W8A8 kernels + ops.def( + "float8_linear_prepack_cpu(Tensor weight, Tensor scales) -> (Tensor, " + "Tensor)"); + ops.impl("float8_linear_prepack_cpu", torch::kCPU, + &float8_linear_prepack_impl); + ops.def( + "fp8_scaled_mm_with_quant(Tensor act, Tensor? act_scales, bool " + "channelwise, " + "Tensor weight, Tensor weight_scales, Tensor? bias, ScalarType " + "output_dtype) -> Tensor"); + ops.impl("fp8_scaled_mm_with_quant", torch::kCPU, &fp8_scaled_mm_with_quant); + ops.def( + "quantize_fp8e4m3_vec(Tensor input, bool channelwise, Tensor? scale_opt) " + "-> (Tensor, Tensor)"); + ops.impl("quantize_fp8e4m3_vec", torch::kCPU, &quantize_fp8e4m3_vec); + ops.def("causal_conv1d_weight_pack(Tensor weight) -> Tensor"); ops.impl("causal_conv1d_weight_pack", torch::kCPU, &causal_conv1d_weight_pack); @@ -832,6 +862,9 @@ TORCH_LIBRARY_EXPAND(TORCH_EXTENSION_NAME, ops) { "-> (Tensor, Tensor)"); ops.impl("fused_gdn_gating_cpu", torch::kCPU, &fused_gdn_gating_cpu); + // Pure runtime CPU ISA capability check. + ops.def("cpu_has_amx_fp8() -> bool", &cpu_has_amx_fp8); + // CPU attention kernels ops.def("cpu_attn_has_isa(str isa) -> bool", &cpu_attn_has_isa); ops.def( @@ -883,8 +916,10 @@ TORCH_LIBRARY_EXPAND(TORCH_EXTENSION_NAME, ops) { "bool skip_weighted, " "str act, str isa) -> ()"); ops.impl("cpu_fused_moe", torch::kCPU, &cpu_fused_moe); -#if defined(ARM_I8MM_SUPPORT) && defined(ARM_BF16_SUPPORT) && \ - !defined(__APPLE__) + +#if (defined(ARM_I8MM_SUPPORT) && defined(ARM_BF16_SUPPORT) && \ + !defined(__APPLE__)) || \ + defined(__powerpc64__) ops.def( "prepack_moe_weight_int8(Tensor weight, Tensor(a1!) packed_weight, " "str isa) -> ()"); @@ -895,8 +930,7 @@ TORCH_LIBRARY_EXPAND(TORCH_EXTENSION_NAME, ops) { "Tensor? w2_bias, Tensor topk_weights, Tensor topk_id, bool " "skip_weighted, str act, str isa) -> ()"); ops.impl("cpu_fused_moe_int8", torch::kCPU, &cpu_fused_moe_int8); -#endif // #if defined(ARM_I8MM_SUPPORT) && defined(ARM_BF16_SUPPORT) && - // !defined(__APPLE__) +#endif // ARM_I8MM+BF16 or __powerpc64__ ops.def( "mla_decode_kvcache(" " Tensor! out, Tensor query, Tensor kv_cache," diff --git a/csrc/libtorch_stable/all_reduce_mhc.cu b/csrc/libtorch_stable/all_reduce_mhc.cu new file mode 100644 index 000000000000..dc58371156a2 --- /dev/null +++ b/csrc/libtorch_stable/all_reduce_mhc.cu @@ -0,0 +1,212 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright contributors to the vLLM project + +#include "torch_utils.h" +#include "custom_all_gather_reduce_scatter.cuh" +#include +#include + +namespace { +using Tensor = torch::stable::Tensor; +using Pack = vllm::packed_t::P; +constexpr int kHidden = 5120; +constexpr int kRanks = 4; +constexpr int kThreads = 128; +constexpr int kCluster = 5; +constexpr int kPacks = kHidden / Pack::size; + +__global__ __launch_bounds__(kThreads) + __cluster_dims__(1, kCluster, 1) void all_reduce_mhc_kernel( + const Pack* input, const Pack* residual, const float* post, + const float* comb, const float* pre, const Pack* weight, Pack* output, + Pack* normalized, Pack* local, Pack* multicast, uint32_t* epochs, + int rank, int stage_size, float eps) { +#if defined(__CUDA_ARCH__) && __CUDA_ARCH__ >= 900 + cudaGridDependencySynchronize(); + const int token = blockIdx.x; + const int column = blockIdx.y * kThreads + threadIdx.x; + const int idx = token * kPacks + column; + const int size = gridDim.x * kPacks; + const int stage = epochs[0] % 3; + const int dirty = vllm::mnnvl_lamport_dirty_stage(stage); + const int dirty_size = epochs[2 + dirty]; + Pack value = input[idx]; + vllm::store_multimem_lamport_payload( + multicast + stage * stage_size + rank * size + idx, + vllm::sanitize_lamport_payload(value)); + // Multi-token forwards benefit from launching dependents before mHC finishes. + if (gridDim.x > 1) cudaTriggerProgrammaticLaunchCompletion(); + vllm::lamport_cta_arrive(epochs + 1); + for (int i = idx; i < dirty_size; i += size) { + local[dirty * stage_size + i] = vllm::lamport_sentinel(); + } + Pack peers[kRanks]; + bool pending; + do { + vllm::LamportPack packets[kRanks]; + #pragma unroll + for (int peer = 0; peer < kRanks; ++peer) { + auto ptr = local + stage * stage_size + idx + peer * size; + asm volatile("ld.relaxed.sys.global.v4.u32 {%0,%1,%2,%3}, [%4];" + : "=r"(packets[peer].words[0]), "=r"(packets[peer].words[1]), + "=r"(packets[peer].words[2]), "=r"(packets[peer].words[3]) + : "l"(ptr) + : "memory"); + } + pending = false; + #pragma unroll + for (int peer = 0; peer < kRanks; ++peer) { + pending |= vllm::is_lamport_dirty(packets[peer]); + peers[peer] = packets[peer].packed; + } + } while (pending); + auto sum = vllm::upcast(peers[0]); + #pragma unroll + for (int r = 1; r < kRanks; ++r) { + vllm::packed_assign_add(sum, vllm::upcast(peers[r])); + } + // Keep the BF16 boundary between the collective and mHC post. + auto reduced = vllm::upcast(vllm::downcast(sum)); + decltype(sum) streams[4]; + #pragma unroll + for (int r = 0; r < 4; ++r) { + streams[r] = vllm::upcast(residual[(token * 4 + r) * kPacks + column]); + } + decltype(sum) collapsed{}; + #pragma unroll + for (int c = 0; c < 4; ++c) { + decltype(sum) mixed; + #pragma unroll + for (int e = 0; e < Pack::size; ++e) { + // Match post*x followed by the four residual FMAs. + float acc = __fmul_rn(post[token * 4 + c], reduced.data[e]); + #pragma unroll + for (int r = 0; r < 4; ++r) { + acc = fmaf(comb[token * 16 + r * 4 + c], streams[r].data[e], acc); + } + mixed.data[e] = acc; + } + Pack rounded = vllm::downcast(mixed); + output[(token * 4 + c) * kPacks + column] = rounded; + auto as_float = vllm::upcast(rounded); + #pragma unroll + for (int e = 0; e < Pack::size; ++e) { + collapsed.data[e] = + fmaf(pre[token * 4 + c], as_float.data[e], collapsed.data[e]); + } + } + // RMSNorm consumes the rounded BF16 collapse in the unfused path. + collapsed = vllm::upcast(vllm::downcast(collapsed)); + float sqr = 0; + #pragma unroll + for (int e = 0; e < Pack::size; ++e) { + sqr = fmaf(collapsed.data[e], collapsed.data[e], sqr); + } + #pragma unroll + for (int delta = 16; delta; delta /= 2) { + sqr += __shfl_xor_sync(0xffffffff, sqr, delta); + } + __shared__ float warp_sums[kCluster][kThreads / 32]; + auto cluster = cooperative_groups::this_cluster(); + const int lane = threadIdx.x % 32; + if (lane < kCluster) { + *cluster.map_shared_rank(&warp_sums[blockIdx.y][threadIdx.x / 32], lane) = + sqr; + } + cluster.sync(); + float total = 0; + #pragma unroll + for (int c = 0; c < kCluster; ++c) { + #pragma unroll + for (int w = 0; w < kThreads / 32; ++w) total += warp_sums[c][w]; + } + const float scale = rsqrtf(__fadd_rn(total / kHidden, eps)); + auto weights = vllm::upcast(weight[column]); + #pragma unroll + for (int e = 0; e < Pack::size; ++e) { + collapsed.data[e] = collapsed.data[e] * scale * weights.data[e]; + } + normalized[idx] = vllm::downcast(collapsed); + if (idx == 0) { + while (*reinterpret_cast(epochs + 1) < + gridDim.x * gridDim.y) { + } + epochs[2 + stage] = kRanks * size; + epochs[0] = vllm::mnnvl_lamport_next_stage(stage); + epochs[1] = 0; + } + if (gridDim.x == 1) cudaTriggerProgrammaticLaunchCompletion(); +#else + asm volatile("trap;"); +#endif +} + +void all_reduce_mhc(Tensor input, Tensor residual, Tensor post, Tensor comb, + Tensor pre, Tensor weight, Tensor output, Tensor normalized, + int64_t local, int64_t multicast, Tensor epochs, + int64_t rank, int64_t stage_bytes, double eps) { + using torch::headeronly::ScalarType; + const torch::stable::accelerator::DeviceGuard guard(input.get_device_index()); + const int64_t n = input.size(0); + STD_TORCH_CHECK(input.dim() == 2 && input.size(1) == kHidden && n > 0 && + n <= 16); + for (const auto& t : {input, residual, weight, output, normalized}) { + STD_TORCH_CHECK(t.is_contiguous() && + t.scalar_type() == ScalarType::BFloat16); + STD_TORCH_CHECK(t.get_device_index() == input.get_device_index()); + STD_TORCH_CHECK(reinterpret_cast(t.const_data_ptr()) % 16 == 0); + } + for (const auto& t : {post, comb, pre}) { + STD_TORCH_CHECK(t.is_contiguous() && t.scalar_type() == ScalarType::Float); + STD_TORCH_CHECK(t.get_device_index() == input.get_device_index()); + } + STD_TORCH_CHECK(residual.numel() == n * 4 * kHidden && + output.numel() == residual.numel() && + normalized.numel() == input.numel() && + weight.numel() == kHidden); + STD_TORCH_CHECK(post.numel() == n * 4 && pre.numel() == n * 4 && + comb.numel() == n * 16); + STD_TORCH_CHECK(epochs.is_contiguous() && + epochs.scalar_type() == ScalarType::Int && + epochs.numel() >= 5 && + epochs.get_device_index() == input.get_device_index()); + STD_TORCH_CHECK(local && multicast && rank >= 0 && rank < kRanks && + stage_bytes % 16 == 0 && + stage_bytes >= n * kHidden * 2 * kRanks); + STD_TORCH_CHECK(get_device_prop()->major == 10); + cudaLaunchAttribute attr{}; + attr.id = cudaLaunchAttributeProgrammaticStreamSerialization; + attr.val.programmaticStreamSerializationAllowed = 1; + cudaLaunchConfig_t config{ + .gridDim = dim3(n, kCluster), + .blockDim = dim3(kThreads), + .dynamicSmemBytes = 0, + .stream = get_current_cuda_stream(input.get_device_index()), + .attrs = &attr, + .numAttrs = 1}; + STD_CUDA_CHECK(cudaLaunchKernelEx( + &config, all_reduce_mhc_kernel, + static_cast(input.const_data_ptr()), + static_cast(residual.const_data_ptr()), + static_cast(post.const_data_ptr()), + static_cast(comb.const_data_ptr()), + static_cast(pre.const_data_ptr()), + static_cast(weight.const_data_ptr()), + static_cast(output.mutable_data_ptr()), + static_cast(normalized.mutable_data_ptr()), + reinterpret_cast(local), reinterpret_cast(multicast), + static_cast(epochs.mutable_data_ptr()), static_cast(rank), + static_cast(stage_bytes / sizeof(Pack)), static_cast(eps))); +} +} // namespace + +STABLE_TORCH_LIBRARY_FRAGMENT(_C_custom_ar, mhc) { + mhc.def( + "all_reduce_mhc(Tensor input, Tensor residual, Tensor post, Tensor comb, " + "Tensor pre, Tensor weight, Tensor! output, Tensor! normalized, " + "int local, int multicast, Tensor! epochs, int rank, int stage_bytes, " + "float eps) -> ()"); +} +STABLE_TORCH_LIBRARY_IMPL(_C_custom_ar, CUDA, mhc) { + mhc.impl("all_reduce_mhc", TORCH_BOX(&all_reduce_mhc)); +} diff --git a/csrc/libtorch_stable/cache_kernels.cu b/csrc/libtorch_stable/cache_kernels.cu index 79aa63a5b83a..9130adf87d69 100644 --- a/csrc/libtorch_stable/cache_kernels.cu +++ b/csrc/libtorch_stable/cache_kernels.cu @@ -19,7 +19,6 @@ #include #include -#include #include #ifdef USE_ROCM @@ -84,7 +83,9 @@ void swap_blocks(torch::stable::Tensor& src, torch::stable::Tensor& dst, namespace { // ROCm hipMemcpyBatchAsync faults for count > 8192 (MI350X/gfx950), so chunk // at that ceiling. +#if defined(USE_ROCM) constexpr int64_t kRocmDefaultMaxBatchDescriptors = 8192; +#endif constexpr const char* kMaxBatchDescriptorsEnv = "VLLM_KV_OFFLOAD_MAX_BATCH_DESCRIPTORS"; @@ -456,14 +457,14 @@ __global__ void concat_and_cache_mla_kernel( // Grouped variant of concat_and_cache_mla: inserts the context K/V for every // draft layer in a single launch. Grid is (num_tokens, num_layers); each layer // reads its own cache base pointer from kv_cache_ptrs (same pointer-array -// pattern as copy_blocks_kernel). bf16 only, so it is a raw 16-bit copy with no -// scaling or quantization; scalar_t is uint16_t for portability. -template +// pattern as copy_blocks_kernel). +template __global__ void concat_and_cache_mla_grouped_kernel( const scalar_t* __restrict__ kv_c, // [num_layers, num_tokens, // kv_lora_rank] const scalar_t* __restrict__ k_pe, // [num_layers, num_tokens, pe_dim] const int64_t* __restrict__ kv_cache_ptrs, // [num_layers] + const float* __restrict__ kv_scales, // [num_layers] or nullptr const int64_t* __restrict__ slot_mapping, // [num_layers, num_tokens] const int64_t kv_c_layer_stride, const int64_t kv_c_token_stride, const int64_t k_pe_layer_stride, const int64_t k_pe_token_stride, @@ -481,12 +482,16 @@ __global__ void concat_and_cache_mla_grouped_kernel( const int64_t block_idx = slot_idx / block_size; const int64_t block_offset = slot_idx % block_size; - scalar_t* __restrict__ kv_cache = - reinterpret_cast(kv_cache_ptrs[layer_idx]); + cache_t* __restrict__ kv_cache = + reinterpret_cast(kv_cache_ptrs[layer_idx]); const scalar_t* __restrict__ kv_c_layer = kv_c + layer_idx * kv_c_layer_stride; const scalar_t* __restrict__ k_pe_layer = k_pe + layer_idx * k_pe_layer_stride; + float scale = 0.0f; + if constexpr (kv_dt != Fp8KVCacheDataType::kAuto) { + scale = kv_scales[layer_idx]; + } auto copy = [&](const scalar_t* __restrict__ src, int64_t src_token_stride, int size, int offset) { @@ -494,7 +499,12 @@ __global__ void concat_and_cache_mla_grouped_kernel( const int64_t src_idx = token_idx * src_token_stride + i; const int64_t dst_idx = block_idx * block_stride + block_offset * entry_stride + i + offset; - kv_cache[dst_idx] = src[src_idx]; + if constexpr (kv_dt == Fp8KVCacheDataType::kAuto) { + kv_cache[dst_idx] = src[src_idx]; + } else { + kv_cache[dst_idx] = + fp8::scaled_convert(src[src_idx], scale); + } } }; @@ -539,16 +549,20 @@ __global__ void concat_and_cache_ds_mla_kernel( scalar_t* kv_cache_16bit = reinterpret_cast(&kv_cache[dst_idx_start]); - // The last warp handles the RoPE part + // Zero the reserved RoPE tail for NoPE rows. if (threadIdx.x >= 64) { // Each thread handles two elements of RoPE const int8_t pe_idx_start = (threadIdx.x - 64) * 2; - const int64_t src_idx = token_idx * k_pe_stride + pe_idx_start; - // Vectorized load of two 16-bit values, performed as one 32-bit load - const int32_t vals = *reinterpret_cast(&k_pe[src_idx]); // RoPE values start after the packed 8-bit NoPE values and the // 32-bit scales const int64_t dst_idx = kv_lora_rank / 2 + 8 + pe_idx_start; + if (pe_dim == 0) { + *reinterpret_cast(&kv_cache_16bit[dst_idx]) = 0; + return; + } + const int64_t src_idx = token_idx * k_pe_stride + pe_idx_start; + // Vectorized load of two 16-bit values, performed as one 32-bit load + const int32_t vals = *reinterpret_cast(&k_pe[src_idx]); // Vectorized store of two 16-bit values, performed as one 32-bit store *reinterpret_cast(&kv_cache_16bit[dst_idx]) = vals; return; @@ -578,8 +592,9 @@ __global__ void concat_and_cache_ds_mla_kernel( max_abs = fmaxf(max_abs, VLLM_SHFL_XOR_SYNC_WIDTH(max_abs, offset, 16)); } - // Compute the scale for the tile - float tile_scale = fmaxf(max_abs / kFp8ScaleDivisor, FLT_MIN); + // Both SM90 and SM100 readers preserve power-of-two fp32 scales exactly. + float tile_scale = fmaxf(max_abs / kFp8ScaleDivisor, 1e-4f); + tile_scale = exp2f(ceilf(log2f(tile_scale))); // The first lane of each half-warp writes the scale to kv_cache if ((lane_idx == 0) || (lane_idx == 16)) { @@ -589,7 +604,7 @@ __global__ void concat_and_cache_ds_mla_kernel( } // Now all threads in the block scale and write their elements - // NoPE data is packed in the first kv_lora_rank/2 bytes (first 256 bytes) + // NoPE data occupies the first kv_lora_rank bytes (512 bytes). const int64_t dst_idx_base = dst_idx_start + (threadIdx.x * 8); uint8_t result[8]; @@ -925,7 +940,8 @@ void concat_and_cache_mla( if (kv_cache_dtype == "fp8_ds_mla") { STD_TORCH_CHECK(kv_lora_rank == 512, "kv_lora_rank must be 512 for fp8_ds_mla"); - STD_TORCH_CHECK(pe_dim == 64, "pe_dim must be 64 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(kv_c.element_size() == 2, @@ -1001,20 +1017,55 @@ void concat_and_cache_mla_grouped( torch::stable::Tensor& k_pe, // [num_layers, num_tokens, pe_dim] torch::stable::Tensor& kv_cache_ptrs, // [num_layers] int64, on device torch::stable::Tensor& slot_mapping, // [num_layers, num_tokens] int64 - int64_t block_size, int64_t block_stride, int64_t entry_stride) { - int num_layers = kv_c.size(0); - int num_tokens = kv_c.size(1); - int kv_lora_rank = kv_c.size(2); - int pe_dim = k_pe.size(2); + int64_t block_size, int64_t block_stride, int64_t entry_stride, + std::optional kv_scales, // [num_layers] or None + const std::string& kv_cache_dtype) { + const bool use_fp8 = kv_cache_dtype == "fp8" || + kv_cache_dtype == "fp8_e4m3" || + kv_cache_dtype == "fp8_e5m2"; +#ifdef USE_ROCM + STD_TORCH_CHECK(kv_cache_dtype != "fp8_e5m2", + "concat_and_cache_mla_grouped does not support fp8_e5m2 " + "KV cache on ROCm"); +#endif + STD_TORCH_CHECK( + use_fp8 || kv_cache_dtype == "auto" || kv_cache_dtype == "bfloat16", + "concat_and_cache_mla_grouped only supports BF16 and plain " + "FP8 KV cache; got ", + kv_cache_dtype); STD_TORCH_CHECK( kv_c.scalar_type() == torch::headeronly::ScalarType::BFloat16 && k_pe.scalar_type() == torch::headeronly::ScalarType::BFloat16, - "concat_and_cache_mla_grouped only supports a bf16 KV cache; got kv_c=", + "concat_and_cache_mla_grouped requires BF16 inputs; got kv_c=", kv_c.scalar_type(), ", k_pe=", k_pe.scalar_type()); STD_TORCH_CHECK( - kv_cache_ptrs.scalar_type() == torch::headeronly::ScalarType::Long, - "kv_cache_ptrs must be int64"); + kv_cache_ptrs.scalar_type() == torch::headeronly::ScalarType::Long && + slot_mapping.scalar_type() == torch::headeronly::ScalarType::Long, + "cache pointers and slot mapping must be int64"); + + const int num_layers = kv_c.size(0); + const int num_tokens = kv_c.size(1); + const int kv_lora_rank = kv_c.size(2); + const int pe_dim = k_pe.size(2); + const float* kv_scales_ptr = nullptr; + if (use_fp8) { + STD_TORCH_CHECK(kv_scales.has_value(), + "FP8 grouped cache insert requires kv_scales"); + STD_TORCH_CHECK( + kv_scales->scalar_type() == torch::headeronly::ScalarType::Float, + "kv_scales must be float32"); + STD_TORCH_CHECK(kv_scales->numel() == num_layers, + "kv_scales must contain one scale per layer"); + STD_TORCH_CHECK(kv_scales->is_cuda() && kv_scales->get_device_index() == + kv_c.get_device_index(), + "kv_scales must be on the same CUDA device as kv_c"); + STD_TORCH_CHECK(kv_scales->is_contiguous(), "kv_scales must be contiguous"); + kv_scales_ptr = kv_scales->const_data_ptr(); + } else { + STD_TORCH_CHECK(!kv_scales.has_value(), + "BF16 grouped cache insert does not use kv_scales"); + } if (num_tokens == 0 || num_layers == 0) { return; @@ -1030,17 +1081,40 @@ void concat_and_cache_mla_grouped( kv_c.get_device_index()); const cudaStream_t stream = get_current_cuda_stream(); - dim3 grid(num_tokens, num_layers); - dim3 block(std::min(kv_lora_rank, 512)); - vllm::concat_and_cache_mla_grouped_kernel - <<>>( - reinterpret_cast(kv_c.data_ptr()), - reinterpret_cast(k_pe.data_ptr()), - kv_cache_ptrs.const_data_ptr(), - slot_mapping.const_data_ptr(), kv_c_layer_stride, - kv_c_token_stride, k_pe_layer_stride, k_pe_token_stride, - slot_layer_stride, block_stride, entry_stride, kv_lora_rank, pe_dim, - block_size); + const dim3 grid(num_tokens, num_layers); + const dim3 block(std::min(kv_lora_rank, 512)); + + if (!use_fp8) { + vllm::concat_and_cache_mla_grouped_kernel + <<>>( + reinterpret_cast(kv_c.data_ptr()), + reinterpret_cast(k_pe.data_ptr()), + kv_cache_ptrs.const_data_ptr(), nullptr, + slot_mapping.const_data_ptr(), kv_c_layer_stride, + kv_c_token_stride, k_pe_layer_stride, k_pe_token_stride, + slot_layer_stride, block_stride, entry_stride, kv_lora_rank, pe_dim, + block_size); + return; + } + +#define LAUNCH_GROUPED_FP8(KV_DTYPE) \ + vllm::concat_and_cache_mla_grouped_kernel<__nv_bfloat16, uint8_t, KV_DTYPE> \ + <<>>( \ + reinterpret_cast(kv_c.data_ptr()), \ + reinterpret_cast(k_pe.data_ptr()), \ + kv_cache_ptrs.const_data_ptr(), kv_scales_ptr, \ + slot_mapping.const_data_ptr(), kv_c_layer_stride, \ + kv_c_token_stride, k_pe_layer_stride, k_pe_token_stride, \ + slot_layer_stride, block_stride, entry_stride, kv_lora_rank, pe_dim, \ + block_size) + + if (kv_cache_dtype == "fp8_e5m2") { + LAUNCH_GROUPED_FP8(vllm::Fp8KVCacheDataType::kFp8E5M2); + } else { + LAUNCH_GROUPED_FP8(vllm::Fp8KVCacheDataType::kFp8E4M3); + } +#undef LAUNCH_GROUPED_FP8 } namespace vllm { @@ -1391,7 +1465,11 @@ __global__ void cp_gather_and_upconvert_fp8_kv_cache_page( const int32_t block_size, const int32_t total_tokens, const int64_t block_table_stride, const int64_t cache_block_stride, const int64_t cache_entry_stride, const int64_t dst_entry_stride, - const int32_t* __restrict__ seq_starts) { + const int32_t* __restrict__ seq_starts, + const uint8_t* __restrict__ host_cache, + const int32_t* __restrict__ host_row_ids, + const int32_t* __restrict__ device_row_ids, + const int64_t host_entry_stride) { constexpr int32_t warps_per_cta = 16; __shared__ GatherPageTask page; __shared__ int32_t physical_block; @@ -1417,8 +1495,22 @@ __global__ void cp_gather_and_upconvert_fp8_kv_cache_page( page_token < page.page_token_end; page_token += warps_per_cta) { const int32_t output_token = page.output_token_begin + page_token - page.page_token_begin; - const uint8_t* token_ptr = src_cache + physical_block * cache_block_stride + - page_token * cache_entry_stride; + const int32_t source_row = physical_block * block_size + page_token; + const uint8_t* token_ptr; + if (device_row_ids == nullptr) { + token_ptr = src_cache + physical_block * cache_block_stride + + page_token * cache_entry_stride; + } else { + const int32_t device_row = device_row_ids[source_row]; + if (device_row >= 0) { + token_ptr = src_cache + (device_row / block_size) * cache_block_stride + + (device_row % block_size) * cache_entry_stride; + } else { + token_ptr = + host_cache + + static_cast(host_row_ids[source_row]) * host_entry_stride; + } + } __nv_bfloat16* dst_ptr = dst + output_token * dst_entry_stride; gather_and_upconvert_fp8_token(token_ptr, dst_ptr, lane_id); } @@ -1627,7 +1719,10 @@ void cp_gather_and_upconvert_fp8_kv_cache( torch::stable::Tensor const& block_table, // [BATCH, BLOCK_INDICES] torch::stable::Tensor const& workspace_starts, // [BATCH] int64_t batch_size, - std::optional seq_starts = std::nullopt) { + std::optional seq_starts = std::nullopt, + std::optional host_cache = std::nullopt, + std::optional host_row_ids = std::nullopt, + std::optional device_row_ids = std::nullopt) { torch::stable::accelerator::DeviceGuard device_guard( src_cache.get_device_index()); const cudaStream_t stream = get_current_cuda_stream(); @@ -1647,6 +1742,12 @@ void cp_gather_and_upconvert_fp8_kv_cache( "seq_starts must be int32"); } + const bool has_host_rows = host_cache.has_value(); + STD_TORCH_CHECK( + has_host_rows == host_row_ids.has_value() && + has_host_rows == device_row_ids.has_value(), + "host_cache, host_row_ids, and device_row_ids must be provided together"); + STD_TORCH_CHECK(src_cache.device() == dst.device(), "src_cache and dst must be on the same device"); STD_TORCH_CHECK(src_cache.device() == block_table.device(), @@ -1657,6 +1758,43 @@ void cp_gather_and_upconvert_fp8_kv_cache( STD_TORCH_CHECK(src_cache.device() == seq_starts.value().device(), "src_cache and seq_starts must be on the same device"); } + const uint8_t* host_cache_ptr = nullptr; + const int32_t* host_row_ids_ptr = nullptr; + const int32_t* device_row_ids_ptr = nullptr; + int64_t host_entry_stride = 0; + if (has_host_rows) { + auto const& host = host_cache.value(); + auto const& host_rows = host_row_ids.value(); + auto const& device_rows = device_row_ids.value(); + STD_TORCH_CHECK( + host.device().is_cpu() && + host.scalar_type() == torch::headeronly::ScalarType::Byte && + host.dim() == 2 && host.is_contiguous() && + host.size(1) == src_cache.size(2), + "host_cache must be a contiguous uint8 CPU row matrix matching " + "src_cache"); + cudaPointerAttributes attributes{}; + const cudaError_t pointer_status = + cudaPointerGetAttributes(&attributes, host.const_data_ptr()); + if (pointer_status != cudaSuccess) { + cudaGetLastError(); + } + STD_TORCH_CHECK( + pointer_status == cudaSuccess && attributes.type == cudaMemoryTypeHost, + "host_cache must be CUDA-accessible pinned memory"); + STD_TORCH_CHECK( + host_rows.is_cuda() && device_rows.is_cuda() && + host_rows.scalar_type() == torch::headeronly::ScalarType::Int && + device_rows.scalar_type() == torch::headeronly::ScalarType::Int && + host_rows.is_contiguous() && device_rows.is_contiguous() && + host_rows.numel() == device_rows.numel() && host_rows.dim() <= 2 && + device_rows.dim() <= 2, + "host/device row maps must be matching contiguous CUDA int32 tensors"); + host_cache_ptr = reinterpret_cast(host.const_data_ptr()); + host_row_ids_ptr = host_rows.const_data_ptr(); + device_row_ids_ptr = device_rows.const_data_ptr(); + host_entry_stride = host.stride(0); + } auto dtype = src_cache.scalar_type(); STD_TORCH_CHECK( dtype == torch::headeronly::ScalarType::Byte || // uint8 @@ -1699,7 +1837,8 @@ void cp_gather_and_upconvert_fp8_kv_cache( workspace_starts.const_data_ptr(), static_cast(batch_size), block_size, total_tokens, block_table_stride, cache_block_stride, cache_entry_stride, - dst_entry_stride, seq_starts_ptr); + dst_entry_stride, seq_starts_ptr, host_cache_ptr, host_row_ids_ptr, + device_row_ids_ptr, host_entry_stride); } void cp_gather_and_upconvert_nvfp4_kv_cache( diff --git a/csrc/libtorch_stable/cuda_vec_utils.cuh b/csrc/libtorch_stable/cuda_vec_utils.cuh index ec6e60724e65..c962c99ec255 100644 --- a/csrc/libtorch_stable/cuda_vec_utils.cuh +++ b/csrc/libtorch_stable/cuda_vec_utils.cuh @@ -320,10 +320,6 @@ __host__ __device__ __forceinline__ bool is_16byte_aligned(const void* ptr) { return (reinterpret_cast(ptr) & 15) == 0; } -__host__ __device__ __forceinline__ bool is_32byte_aligned(const void* ptr) { - return (reinterpret_cast(ptr) & 31) == 0; -} - // ============================================================ // Packed type conversion and arithmetic // ============================================================ diff --git a/csrc/libtorch_stable/fused_deepseek_v4_qnorm_rope_kv_insert_kernel.cu b/csrc/libtorch_stable/fused_deepseek_v4_qnorm_rope_kv_insert_kernel.cu index 06fe7108bd61..5a5caacce7a5 100644 --- a/csrc/libtorch_stable/fused_deepseek_v4_qnorm_rope_kv_insert_kernel.cu +++ b/csrc/libtorch_stable/fused_deepseek_v4_qnorm_rope_kv_insert_kernel.cu @@ -3,7 +3,10 @@ * SPDX-FileCopyrightText: Copyright contributors to the vLLM project * * Horizontally-fused DeepseekV4-MLA kernel: - * - Q side: optional per-head RMSNorm + GPT-J RoPE on last ROPE_DIM + * - Q side: optional per-head RMSNorm (apply_q_norm) and GPT-J RoPE on the + * last ROPE_DIM (apply_q_rope), plus the zero-pad to + * q_head_padded, in the head-major layout or (is_q_interleaved) + * FlashMLA's mega-attention chunk-interleaved one * - KV side: GPT-J RoPE on last ROPE_DIM + UE8M0 FP8 quant on NoPE + paged * cache insert * @@ -23,9 +26,15 @@ * cos_sin_cache layout [max_pos, rope_dim] = cos || sin (cos first, sin * second along last dim; each half is rope_dim/2 = 32 values) * - * Cache layout per paged-cache block (block_size tokens): - * [0, bs*576): token data, 448 fp8 + 128 bf16 each - * [bs*576, bs*576 + bs*8): UE8M0 scales, 7 real + 1 pad per token + * Cache layout per paged-cache block (block_size tokens), selected by the + * `kv_mxfp8` argument: + * V4 (584 B/token, kv_mxfp8=false) + * [0, bs*576): token data, 448 fp8 + 128 bf16 each + * [bs*576, bs*576 + bs*8): UE8M0 scales, 7 real + 1 pad per token + * V4.1 (528 B/token, kv_mxfp8=true) -- RoPE is quantized as well, so the + * whole row is MXFP8 (fp8 e4m3 with one UE8M0 scale per 32 dims) + * [0, bs*512): token data, 512 fp8 each + * [bs*512, bs*512 + bs*16): UE8M0 scales, 16 per token */ #include "torch_utils.h" @@ -91,6 +100,10 @@ constexpr int kQuantBlock = 64; constexpr int kNumQuantBlocks = kNopeDim / kQuantBlock; // 7 constexpr int kScaleBytesPerToken = kNumQuantBlocks + 1; // 8 (7 real + 1 pad) constexpr int kTokenDataBytes = kNopeDim + kRopeDim * 2; // 448 + 128 = 576 +// V4.1 MXFP8 record: every dim quantized, one UE8M0 scale per 32 dims. +constexpr int kMxfp8QuantBlock = 32; +constexpr int kMxfp8NumQuantBlocks = kHeadDim / kMxfp8QuantBlock; // 16 +constexpr int kMxfp8TokenDataBytes = kHeadDim; // 512 // FNUZ on gfx942 / OCP elsewhere. FNUZ uses 224.0 (not the dtype's raw // 240.0) to match the rest of vLLM's FNUZ pipeline. #if defined(USE_ROCM) && defined(__gfx942__) @@ -141,6 +154,11 @@ __device__ __forceinline__ uint4 packFp8E4M3x16(float const* values, // ──────────────────────────────────────────────────────────────────────────── // Small inline helpers // ──────────────────────────────────────────────────────────────────────────── +__device__ __forceinline__ float warp2MaxAbs(float val) { + // Reduce absolute max across 2 consecutive lanes (32 dims = one MXFP8 block). + return fmaxf(val, __shfl_xor_sync(FINAL_MASK, val, 1)); +} + __device__ __forceinline__ float warp4MaxAbs(float val) { // Reduce absolute max across 4 consecutive lanes (lane id & 3 group). float peer = __shfl_xor_sync(FINAL_MASK, val, 1); @@ -159,6 +177,30 @@ __device__ __forceinline__ float warpSum(float val) { return val; } +// Offset of the 16 elements a lane owns inside a Q tensor of `num_heads` +// heads. The default layout is head-major: a head's 512 dims are contiguous. +// `Q_INTERLEAVED` is FlashMLA's mega-attention layout, 32 chunks of +// [num_heads, 16] per token, which changes the address only -- lane L still +// owns dims [16L, 16L+16) of its head either way, so `dim_base` stays the +// true dim offset and the norm and RoPE below are equally valid in both. +// What it does change is where the padding heads land: at the tail of every +// chunk rather than after the live heads. +template +__device__ __forceinline__ int64_t qElemOffset(int const tokenIdx, + int const slotIdx, + int const laneId, + int const dim_base, + int const num_heads) { + if constexpr (Q_INTERLEAVED) { + return static_cast(tokenIdx) * num_heads * kHeadDim + + static_cast(laneId) * num_heads * kElemsPerLane + + static_cast(slotIdx) * kElemsPerLane; + } else { + return (static_cast(tokenIdx) * num_heads + slotIdx) * kHeadDim + + dim_base; + } +} + // ──────────────────────────────────────────────────────────────────────────── // Per-slot inner pipeline // ──────────────────────────────────────────────────────────────────────────── @@ -176,7 +218,17 @@ __device__ __forceinline__ float warpSum(float val) { // slot == kNumHeadsQPadded → KV (RoPE + UE8M0 quant // + paged-cache // insert) -template +// +// `kv_mxfp8` and `apply_q_rope` are grid-uniform, so they stay runtime +// arguments rather than multiplying every (head count, q-norm, q-layout) +// instantiation of a kernel whose hot path is the Q branch. `Q_INTERLEAVED` +// cannot: it picks the address arithmetic, which has to fold. +// +// The three Q knobs are independent. The mega-attention layer uses +// Q_INTERLEAVED with both norm and RoPE off, because its attention kernel +// applies those itself -- not because the layout forbids them. +template __device__ __forceinline__ void processDeepseekV4Slot( uint4 v0, uint4 v1, int const tokenIdx, int const slotIdx, int const dim_base, int const laneId, int const num_heads_q, @@ -184,7 +236,7 @@ __device__ __forceinline__ void processDeepseekV4Slot( uint8_t* __restrict__ k_cache, int64_t const* __restrict__ slot_mapping, int64_t const* __restrict__ position_ids, float const* __restrict__ cos_sin_cache, int const cache_block_size, - int const kv_block_stride) { + int const kv_block_stride, bool const kv_mxfp8, bool const apply_q_rope) { using Converter = vllm::_typeConvert; bool const isKV = (slotIdx == kNumHeadsQPadded); bool const isPadQ = !isKV && (slotIdx >= num_heads_q); @@ -194,10 +246,8 @@ __device__ __forceinline__ void processDeepseekV4Slot( // zero literal is correct. Matches the live-Q branch's vectorized store. if (isPadQ) { scalar_t_in* dst = - q_out + - (static_cast(tokenIdx) * kNumHeadsQPadded + slotIdx) * - kHeadDim + - dim_base; + q_out + qElemOffset(tokenIdx, slotIdx, laneId, dim_base, + kNumHeadsQPadded); uint4 const zero4 = {0u, 0u, 0u, 0u}; *reinterpret_cast(dst) = zero4; *reinterpret_cast(dst + 8) = zero4; @@ -245,7 +295,9 @@ __device__ __forceinline__ void processDeepseekV4Slot( // ── GPT-J RoPE on dims [NOPE_DIM, HEAD_DIM) ───────────────────────────── // All math in fp32. cos_sin_cache is loaded as fp32 (its native storage). - bool const is_rope_lane = dim_base >= kNopeDim; + // KV is always rotated; Q only when the caller has not already had it + // rotated elsewhere (the mega-attention kernel does its own). + bool const is_rope_lane = dim_base >= kNopeDim && (isKV || apply_q_rope); if (is_rope_lane) { int64_t const pos = position_ids[tokenIdx]; constexpr int kHalfRope = kRopeDim / 2; @@ -295,26 +347,29 @@ __device__ __forceinline__ void processDeepseekV4Slot( make_float2(elements[8 + 2 * i], elements[8 + 2 * i + 1])); } scalar_t_in* dst = - q_out + - (static_cast(tokenIdx) * kNumHeadsQPadded + slotIdx) * - kHeadDim + - dim_base; + q_out + qElemOffset(tokenIdx, slotIdx, laneId, dim_base, + kNumHeadsQPadded); *reinterpret_cast(dst) = out0; *reinterpret_cast(dst + 8) = out1; } else { - // ── KV: FP8 quant on NoPE + bf16 store on RoPE + cache insert. + // ── KV: FP8 quant + cache insert. V4 keeps the RoPE dims in bf16; + // V4.1 (kv_mxfp8) quantizes them too. int64_t const slot_id = slot_mapping[tokenIdx]; if (slot_id >= 0) { + int const token_data_bytes = + kv_mxfp8 ? kMxfp8TokenDataBytes : kTokenDataBytes; + int const scale_bytes = + kv_mxfp8 ? kMxfp8NumQuantBlocks : kScaleBytesPerToken; int64_t const block_idx = slot_id / cache_block_size; int64_t const pos_in_block = slot_id % cache_block_size; uint8_t* block_base = k_cache + block_idx * static_cast(kv_block_stride); - uint8_t* token_fp8_ptr = block_base + pos_in_block * kTokenDataBytes; + uint8_t* token_fp8_ptr = block_base + pos_in_block * token_data_bytes; uint8_t* token_bf16_ptr = token_fp8_ptr + kNopeDim; uint8_t* token_scale_ptr = block_base + - static_cast(cache_block_size) * kTokenDataBytes + - pos_in_block * kScaleBytesPerToken; + static_cast(cache_block_size) * token_data_bytes + + pos_in_block * scale_bytes; #pragma unroll for (int i = 0; i < kElemsPerLane; i++) { @@ -326,11 +381,13 @@ __device__ __forceinline__ void processDeepseekV4Slot( for (int i = 0; i < kElemsPerLane; i++) { local_absmax = fmaxf(local_absmax, fabsf(elements[i])); } - float const absmax = fmaxf(warp4MaxAbs(local_absmax), 1e-4f); + float const absmax = fmaxf( + kv_mxfp8 ? warp2MaxAbs(local_absmax) : warp4MaxAbs(local_absmax), + 1e-4f); float const exponent = ceilf(log2f(absmax / kFp8Max)); float const inv_scale = exp2f(-exponent); - if (!is_rope_lane) { + if (kv_mxfp8 || !is_rope_lane) { uint8_t out_bytes[kElemsPerLane]; #pragma unroll for (int i = 0; i < kElemsPerLane; i++) { @@ -347,13 +404,19 @@ __device__ __forceinline__ void processDeepseekV4Slot( *reinterpret_cast(token_fp8_ptr + dim_base) = *reinterpret_cast(out_bytes); - if ((laneId & 3) == 0) { - int const q_block_idx = laneId >> 2; - float encoded = fmaxf(fminf(exponent + 127.0f, 255.0f), 0.0f); - token_scale_ptr[q_block_idx] = static_cast(encoded); - } - if (laneId == 0) { - token_scale_ptr[kNumQuantBlocks] = 0; + float const encoded = fmaxf(fminf(exponent + 127.0f, 255.0f), 0.0f); + if (kv_mxfp8) { + // One scale per 32 dims = one per lane pair. + if ((laneId & 1) == 0) { + token_scale_ptr[laneId >> 1] = static_cast(encoded); + } + } else { + if ((laneId & 3) == 0) { + token_scale_ptr[laneId >> 2] = static_cast(encoded); + } + if (laneId == 0) { + token_scale_ptr[kNumQuantBlocks] = 0; + } } } else { uint4 out0, out1; @@ -400,14 +463,17 @@ __device__ __forceinline__ void processDeepseekV4Slot( // `kNumHeadsQPadded` is a template parameter (compile-time constant) so the // divisions in the grid math and the KV-sentinel comparison fold to fast // constant operations. The launch wrapper dispatches the runtime value to -// the matching instantiation. +// the matching instantiation. `kNumHeadsQPadded == 0` leaves one slot per +// token and is the KV-insert-only instantiation, for a Q that is already in +// the shape its attention kernel wants. // // With DP padding, q/kv/position_ids can have more rows than slot_mapping. // The live-Q and pad-Q branches cover all `num_tokens_full` rows (downstream // attention uses them). The KV branch only inserts the first // `num_tokens_insert` tokens (= slot_mapping length) into the paged cache. // -template +template __global__ void fusedDeepseekV4QNormRopeKVRopeQuantInsertKernel( scalar_t_in const* __restrict__ q_in, // [N, num_heads_q, 512] scalar_t_in* __restrict__ q_out, // [N, kNumHeadsQPadded, 512] @@ -421,7 +487,9 @@ __global__ void fusedDeepseekV4QNormRopeKVRopeQuantInsertKernel( int const num_tokens_insert, // = slot_mapping.size(0), ≤ num_tokens_full int const num_heads_q, // live Q heads (input layout) int const cache_block_size, // tokens per paged-cache block - int const kv_block_stride) { // bytes per paged-cache block + int const kv_block_stride, // bytes per paged-cache block + bool const kv_mxfp8, // V4.1 all-dims MXFP8 record + bool const apply_q_rope) { // rotate Q too, not just KV #if (!defined(__CUDA_ARCH__) || __CUDA_ARCH__ < 800) && !defined(USE_ROCM) // BF16 _typeConvert specialization is unavailable on pre-Ampere. The // DeepseekV4 kernel only runs with bf16 inputs in practice, so compile a @@ -464,20 +532,18 @@ __global__ void fusedDeepseekV4QNormRopeKVRopeQuantInsertKernel( if (isKV) { src_ptr = kv_in + static_cast(tokenIdx) * kHeadDim + dim_base; } else { - int64_t const q_row_offset = - (static_cast(tokenIdx) * num_heads_q + slotIdx) * - kHeadDim + - dim_base; - src_ptr = q_in + q_row_offset; + src_ptr = q_in + qElemOffset(tokenIdx, slotIdx, laneId, + dim_base, num_heads_q); } v0 = *reinterpret_cast(src_ptr); v1 = *reinterpret_cast(src_ptr + 8); } - processDeepseekV4Slot( + processDeepseekV4Slot( v0, v1, tokenIdx, slotIdx, dim_base, laneId, num_heads_q, eps, q_out, k_cache, slot_mapping, position_ids, cos_sin_cache, cache_block_size, - kv_block_stride); + kv_block_stride, kv_mxfp8, apply_q_rope); #if defined(__CUDA_ARCH__) && (__CUDA_ARCH__ >= 900) cudaTriggerProgrammaticLaunchCompletion(); @@ -497,7 +563,8 @@ __global__ void fusedDeepseekV4QNormRopeKVRopeQuantInsertKernel( // Q branch (optional RMSNorm + RoPE, in place) head_slot == num_heads_q // KV branch (RoPE + UE8M0 quant + insert) // -template +template __global__ void fusedDeepseekV4QNormRopeKVRopeQuantInsertKernelReducedGrid( scalar_t_in const* __restrict__ q_in, scalar_t_in* __restrict__ q_out, scalar_t_in const* __restrict__ kv_in, uint8_t* __restrict__ k_cache, @@ -506,7 +573,7 @@ __global__ void fusedDeepseekV4QNormRopeKVRopeQuantInsertKernelReducedGrid( float const* __restrict__ cos_sin_cache, float const eps, int const num_tokens_full, int const num_tokens_insert, int const num_heads_q, int const cache_block_size, - int const kv_block_stride) { + int const kv_block_stride, bool const kv_mxfp8, bool const apply_q_rope) { #if (!defined(__CUDA_ARCH__) || __CUDA_ARCH__ < 800) && !defined(USE_ROCM) if constexpr (std::is_same_v) { return; @@ -536,11 +603,8 @@ __global__ void fusedDeepseekV4QNormRopeKVRopeQuantInsertKernelReducedGrid( if (s == kNumHeadsQPadded) { src = kv_in + static_cast(tokenIdx) * kHeadDim + dim_base; } else { - src = q_in + - (static_cast(tokenIdx) * num_heads_q + - static_cast(s)) * - kHeadDim + - dim_base; + src = q_in + qElemOffset(tokenIdx, s, laneId, dim_base, + num_heads_q); } va = *reinterpret_cast(src); vb = *reinterpret_cast(src + 8); @@ -561,10 +625,12 @@ __global__ void fusedDeepseekV4QNormRopeKVRopeQuantInsertKernelReducedGrid( load_slot(next_slot, v0_next, v1_next); } - processDeepseekV4Slot( + processDeepseekV4Slot( v0_curr, v1_curr, tokenIdx, curr_slot, dim_base, laneId, num_heads_q, eps, q_out, k_cache, slot_mapping, position_ids, - cos_sin_cache, cache_block_size, kv_block_stride); + cos_sin_cache, cache_block_size, kv_block_stride, kv_mxfp8, + apply_q_rope); // ── Buffer rotation: hand the prefetched LDGs to the next iter. v0_curr = v0_next; @@ -584,14 +650,15 @@ __global__ void fusedDeepseekV4QNormRopeKVRopeQuantInsertKernelReducedGrid( // ──────────────────────────────────────────────────────────────────────────── // Launch wrapper // ──────────────────────────────────────────────────────────────────────────── -template +template static void launchFusedDeepseekV4Templated( scalar_t_in const* q_in, scalar_t_in* q_out, scalar_t_in const* kv_in, uint8_t* k_cache, int64_t const* slot_mapping, int64_t const* position_ids, float const* cos_sin_cache, float const eps, int const num_tokens_full, int const num_tokens_insert, int const num_heads_q, - int const cache_block_size, int const kv_block_stride, - cudaStream_t stream) { + int const cache_block_size, int const kv_block_stride, bool const kv_mxfp8, + bool const apply_q_rope, cudaStream_t stream) { constexpr int kBlockSize = 256; constexpr int kWarpsPerBlock = kBlockSize / 32; int64_t const total_warps = @@ -624,37 +691,46 @@ static void launchFusedDeepseekV4Templated( config.attrs = attrs; config.numAttrs = (sm_version >= 90) ? 1 : 0; - if (num_tokens_full < NUM_TOKEN_CUTOFF) { - cudaLaunchKernelEx(&config, - fusedDeepseekV4QNormRopeKVRopeQuantInsertKernel< - scalar_t_in, kNumHeadsQPadded, APPLY_Q_NORM>, - q_in, q_out, kv_in, k_cache, slot_mapping, position_ids, - cos_sin_cache, eps, num_tokens_full, num_tokens_insert, - num_heads_q, cache_block_size, kv_block_stride); + // The reduced grid pays off by iterating a token's Q heads inside one CTA. + // With zero query heads there is nothing to iterate, so one warp per block + // would do all the work and the other seven idle; keep the warp-per-slot + // grid instead. Only reachable above NUM_TOKEN_CUTOFF tokens in a single + // insert, where it is worth ~2x. + if (kNumHeadsQPadded == 0 || num_tokens_full < NUM_TOKEN_CUTOFF) { + cudaLaunchKernelEx( + &config, + fusedDeepseekV4QNormRopeKVRopeQuantInsertKernel< + scalar_t_in, kNumHeadsQPadded, APPLY_Q_NORM, Q_INTERLEAVED>, + q_in, q_out, kv_in, k_cache, slot_mapping, position_ids, cos_sin_cache, + eps, num_tokens_full, num_tokens_insert, num_heads_q, cache_block_size, + kv_block_stride, kv_mxfp8, apply_q_rope); } else { config.gridDim = dim3(num_tokens_full); cudaLaunchKernelEx( &config, fusedDeepseekV4QNormRopeKVRopeQuantInsertKernelReducedGrid< - scalar_t_in, kNumHeadsQPadded, APPLY_Q_NORM>, + scalar_t_in, kNumHeadsQPadded, APPLY_Q_NORM, Q_INTERLEAVED>, q_in, q_out, kv_in, k_cache, slot_mapping, position_ids, cos_sin_cache, eps, num_tokens_full, num_tokens_insert, num_heads_q, cache_block_size, - kv_block_stride); + kv_block_stride, kv_mxfp8, apply_q_rope); } #else // ROCm: use standard kernel launch syntax (no PDL/stream serialization) // clang-format off fusedDeepseekV4QNormRopeKVRopeQuantInsertKernel< - scalar_t_in, kNumHeadsQPadded, APPLY_Q_NORM> + scalar_t_in, kNumHeadsQPadded, APPLY_Q_NORM, Q_INTERLEAVED> <<>>( q_in, q_out, kv_in, k_cache, slot_mapping, position_ids, cos_sin_cache, eps, num_tokens_full, num_tokens_insert, num_heads_q, - cache_block_size, kv_block_stride); + cache_block_size, kv_block_stride, kv_mxfp8, apply_q_rope); #endif } // Runtime dispatch into one of the precompiled `kNumHeadsQPadded` -// instantiations. Supported padded head counts: 8, 16, 32, 64, 128. +// instantiations. Supported padded head counts: 8, 16, 32, 64, 128, plus 0 +// for a KV-only launch that writes no Q at all. `apply_q_norm` and +// `is_q_interleaved` are template parameters, `apply_q_rope` is not (see +// processDeepseekV4Slot), so each head count carries four instantiations. template void launchFusedDeepseekV4QNormRopeKVRopeQuantInsert( scalar_t_in const* q_in, scalar_t_in* q_out, scalar_t_in const* kv_in, @@ -663,22 +739,32 @@ void launchFusedDeepseekV4QNormRopeKVRopeQuantInsert( int const num_tokens_full, int const num_tokens_insert, int const num_heads_q, int const num_heads_q_padded, int const cache_block_size, int const kv_block_stride, - bool const apply_q_norm, cudaStream_t stream) { -#define DISPATCH(N) \ - case N: \ - if (apply_q_norm) { \ - launchFusedDeepseekV4Templated( \ - q_in, q_out, kv_in, k_cache, slot_mapping, position_ids, \ - cos_sin_cache, eps, num_tokens_full, num_tokens_insert, \ - num_heads_q, cache_block_size, kv_block_stride, stream); \ - } else { \ - launchFusedDeepseekV4Templated( \ - q_in, q_out, kv_in, k_cache, slot_mapping, position_ids, \ - cos_sin_cache, eps, num_tokens_full, num_tokens_insert, \ - num_heads_q, cache_block_size, kv_block_stride, stream); \ - } \ + bool const apply_q_norm, bool const kv_mxfp8, bool const apply_q_rope, + bool const is_q_interleaved, cudaStream_t stream) { +#define LAUNCH(N, NORM, INTERLEAVED) \ + launchFusedDeepseekV4Templated( \ + q_in, q_out, kv_in, k_cache, slot_mapping, position_ids, cos_sin_cache, \ + eps, num_tokens_full, num_tokens_insert, num_heads_q, cache_block_size, \ + kv_block_stride, kv_mxfp8, apply_q_rope, stream) +#define DISPATCH(N) \ + case N: \ + if (is_q_interleaved) { \ + if (apply_q_norm) { \ + LAUNCH(N, true, true); \ + } else { \ + LAUNCH(N, false, true); \ + } \ + } else if (apply_q_norm) { \ + LAUNCH(N, true, false); \ + } else { \ + LAUNCH(N, false, false); \ + } \ return; + if (num_heads_q_padded == 0) { + LAUNCH(0, false, false); + return; + } switch (num_heads_q_padded) { DISPATCH(8) DISPATCH(16) @@ -690,9 +776,10 @@ void launchFusedDeepseekV4QNormRopeKVRopeQuantInsert( "fused_deepseek_v4_qnorm_rope_kv_rope_quant_insert: " "unsupported num_heads_q_padded=", num_heads_q_padded, - " (compiled instantiations: 8, 16, 32, 64, 128)."); + " (compiled instantiations: 0, 8, 16, 32, 64, 128)."); } #undef DISPATCH +#undef LAUNCH } // ──────────────────────────────────────────────────────────────────────────── @@ -959,6 +1046,91 @@ static void launchFullCacheKernel( // ──────────────────────────────────────────────────────────────────────────── // Torch op wrapper // ──────────────────────────────────────────────────────────────────────────── +void fused_deepseek_v4_kv_rope_insert( + torch::stable::Tensor const& kv, torch::stable::Tensor& k_cache, + torch::stable::Tensor const& slot_mapping, + torch::stable::Tensor const& position_ids, + torch::stable::Tensor const& cos_sin_cache, int64_t cache_block_size, + std::optional fp8_scale, bool kv_mxfp8) { + using torch::headeronly::ScalarType; + STD_TORCH_CHECK(kv.device().is_cuda() && kv.is_contiguous() && + kv.dim() == 2 && kv.size(1) == 512 && + kv.scalar_type() == ScalarType::BFloat16, + "kv must be contiguous CUDA bfloat16 [tokens, 512]"); + auto const device = kv.get_device_index(); + STD_TORCH_CHECK(k_cache.device().is_cuda() && + k_cache.get_device_index() == device && + k_cache.dim() == 3 && cache_block_size > 0 && + k_cache.size(1) == cache_block_size && + k_cache.stride(2) == 1, + "k_cache must be a paged cache on the KV device"); + for (auto const* tensor : {&slot_mapping, &position_ids}) { + STD_TORCH_CHECK(tensor->device().is_cuda() && + tensor->get_device_index() == device && + tensor->scalar_type() == ScalarType::Long && + tensor->dim() == 1 && tensor->is_contiguous(), + "slots and positions must be contiguous int64 on the KV device"); + } + STD_TORCH_CHECK(position_ids.size(0) == kv.size(0) && + slot_mapping.size(0) <= kv.size(0), + "positions must match KV rows; slots may omit padded rows"); + STD_TORCH_CHECK(cos_sin_cache.device().is_cuda() && + cos_sin_cache.get_device_index() == device && + cos_sin_cache.scalar_type() == ScalarType::Float && + cos_sin_cache.dim() == 2 && + cos_sin_cache.size(1) == 64 && cos_sin_cache.is_contiguous(), + "cos_sin_cache must be contiguous float32 [max_pos, 64]"); + bool const packed = k_cache.scalar_type() == ScalarType::Byte; + bool const fp8 = k_cache.scalar_type() == ScalarType::Float8_e4m3fn; + STD_TORCH_CHECK(packed || fp8 || + k_cache.scalar_type() == ScalarType::BFloat16, + "cache must be uint8, bfloat16 or float8_e4m3fn"); + STD_TORCH_CHECK(k_cache.size(2) == (packed ? (kv_mxfp8 ? 528 : 584) : 512), + "cache row width does not match its dtype"); + if (fp8) { + STD_TORCH_CHECK(fp8_scale.has_value() && fp8_scale->device().is_cuda() && + fp8_scale->get_device_index() == device && + fp8_scale->scalar_type() == ScalarType::Float && + fp8_scale->numel() == 1, + "fp8 cache requires one float32 KV scale on the KV device"); + } + int const num_tokens = static_cast(slot_mapping.size(0)); + if (num_tokens == 0) return; + const torch::stable::accelerator::DeviceGuard device_guard(device); + const cudaStream_t stream = get_current_cuda_stream(device); + using scalar_t = c10::BFloat16; + auto const* input = reinterpret_cast(kv.const_data_ptr()); + auto* cache = reinterpret_cast(k_cache.mutable_data_ptr()); + auto const* slots = slot_mapping.const_data_ptr(); + auto const* positions = position_ids.const_data_ptr(); + auto const* cos_sin = cos_sin_cache.const_data_ptr(); + // Zero query heads schedules only the existing KV branch, preserving its + // RoPE rounding and quantization without allocating any query tensors. + if (packed) { + // With zero query heads no Q slot is scheduled, so neither Q knob is + // reachable: Q_INTERLEAVED picks addressing nothing uses, and apply_q_rope + // gates a branch only a Q slot takes (KV is rotated unconditionally). + vllm::deepseek_v4_fused_ops::launchFusedDeepseekV4Templated( + nullptr, nullptr, input, cache, slots, positions, cos_sin, 0.0f, + num_tokens, num_tokens, 0, static_cast(cache_block_size), + static_cast(k_cache.stride(0)), kv_mxfp8, /*apply_q_rope=*/true, + stream); + } else if (fp8) { + vllm::deepseek_v4_fused_ops::launchFullCacheKernel( + nullptr, nullptr, 0, 0, input, cache, slots, positions, cos_sin, + fp8_scale->const_data_ptr(), nullptr, 0.0f, num_tokens, num_tokens, + 0, static_cast(cache_block_size), k_cache.stride(0), + k_cache.stride(1), false, "fused_deepseek_v4_kv_rope_insert", stream); + } else { + vllm::deepseek_v4_fused_ops::launchFullCacheKernel( + nullptr, nullptr, 0, 0, input, cache, slots, positions, cos_sin, nullptr, + nullptr, 0.0f, num_tokens, num_tokens, 0, static_cast(cache_block_size), + k_cache.stride(0) * 2, k_cache.stride(1) * 2, false, + "fused_deepseek_v4_kv_rope_insert", stream); + } +} + torch::stable::Tensor fused_deepseek_v4_qnorm_rope_kv_rope_quant_insert( torch::stable::Tensor const& q_in, // [N, num_heads_q, 512] bf16 torch::stable::Tensor const& kv, // [N, 512] bf16 (read-only) @@ -966,8 +1138,9 @@ torch::stable::Tensor fused_deepseek_v4_qnorm_rope_kv_rope_quant_insert( torch::stable::Tensor const& slot_mapping, // [N] int64 torch::stable::Tensor const& position_ids, // [N] int64 torch::stable::Tensor const& cos_sin_cache, // [max_pos, rope_dim] bf16 - int64_t q_head_padded, // padded Q head count for output - double eps, int64_t cache_block_size, bool apply_q_norm) { + int64_t q_head_padded, // padded Q head count for output, 0 for no Q + double eps, int64_t cache_block_size, bool apply_q_norm, bool kv_mxfp8, + bool apply_q_rope, bool is_q_interleaved) { STD_TORCH_CHECK(q_in.device().is_cuda() && q_in.is_contiguous(), "q_in must be contiguous CUDA"); STD_TORCH_CHECK(kv.device().is_cuda() && kv.is_contiguous(), @@ -987,8 +1160,11 @@ torch::stable::Tensor fused_deepseek_v4_qnorm_rope_kv_rope_quant_insert( STD_TORCH_CHECK(kv.dim() == 2 && kv.size(1) == 512, "kv shape [N, 512]"); STD_TORCH_CHECK(q_in.scalar_type() == kv.scalar_type(), "q_in and kv dtype must match"); - STD_TORCH_CHECK(q_head_padded >= q_in.size(1), - "q_head_padded must be >= q_in.size(1) (num_heads_q)"); + // q_head_padded == 0 asks for the KV insert alone: Q is left untouched and + // an empty tensor comes back, for a caller whose Q is already in the shape + // its attention kernel reads. + STD_TORCH_CHECK(q_head_padded == 0 || q_head_padded >= q_in.size(1), + "q_head_padded must be 0 or >= q_in.size(1) (num_heads_q)"); STD_TORCH_CHECK(k_cache.scalar_type() == torch::headeronly::ScalarType::Byte, "k_cache must be uint8"); STD_TORCH_CHECK(cos_sin_cache.dim() == 2 && cos_sin_cache.size(1) == 64, @@ -1019,8 +1195,12 @@ torch::stable::Tensor fused_deepseek_v4_qnorm_rope_kv_rope_quant_insert( // Allocate the padded q output. The kernel writes every element (live // region gets optional RMSNorm+RoPE; pad region gets zeros), so `empty` is // safe. - auto q_out = torch::stable::new_empty( - q_in, {q_in.size(0), q_head_padded, q_in.size(2)}, q_in.scalar_type()); + auto q_out = + q_head_padded == 0 + ? torch::stable::new_empty(q_in, {int64_t{0}}, q_in.scalar_type()) + : torch::stable::new_empty( + q_in, {q_in.size(0), q_head_padded, q_in.size(2)}, + q_in.scalar_type()); VLLM_STABLE_DISPATCH_HALF_TYPES( q_in.scalar_type(), "fused_deepseek_v4_qnorm_rope_kv_insert", [&] { @@ -1036,7 +1216,8 @@ torch::stable::Tensor fused_deepseek_v4_qnorm_rope_kv_rope_quant_insert( cos_sin_cache.const_data_ptr(), static_cast(eps), num_tokens_full, num_tokens_insert, num_heads_q, num_heads_q_padded, cache_block_size_i, kv_block_stride, - apply_q_norm, stream); + apply_q_norm, kv_mxfp8, apply_q_rope, is_q_interleaved, + stream); }); return q_out; } diff --git a/csrc/libtorch_stable/fused_kimi_k3_mla_key_concat_kv_cache_kernel.cu b/csrc/libtorch_stable/fused_kimi_k3_mla_key_concat_kv_cache_kernel.cu index 05c7bfc6d13e..3892790c4040 100644 --- a/csrc/libtorch_stable/fused_kimi_k3_mla_key_concat_kv_cache_kernel.cu +++ b/csrc/libtorch_stable/fused_kimi_k3_mla_key_concat_kv_cache_kernel.cu @@ -35,8 +35,9 @@ * * fp8_ds_mla (fused_kimi_k3_mla_key_concat_ds_mla_insert): * - full key concat (bf16), and cache insert in DeepSeek's 656-byte - * block-scaled layout (NoPE fp8 in 4 tiles of 128 with per-tile dynamic - * scales, RoPE bf16), bit-compatible with concat_and_cache_ds_mla_kernel. + * block-scaled layout (NoPE fp8 in 4 tiles of 128 with per-tile + * power-of-two scales, RoPE bf16), bit-compatible with + * concat_and_cache_ds_mla_kernel. * * Both use Programmatic Dependent Launch (PDL) to overlap the tail of the * producing GEMMs on sm_90+, and are structured after @@ -68,7 +69,6 @@ #include "../quantization/w8a8/fp8/amd/quant_utils.cuh" #endif #include -#include #include #ifdef USE_ROCM @@ -111,9 +111,9 @@ constexpr float kFp8Max = 224.0f; #else constexpr float kFp8Max = 448.0f; #endif -// Divisor for fp8_ds_mla per-tile dynamic scales (matches cache_kernels.cu). -// fp8_ds_mla 656B entry: [0,512) NoPE fp8 (4 tiles of 128), [512,528) 4 fp32 -// tile scales, [528,656) RoPE 64 bf16. +// Divisor for fp8_ds_mla per-tile power-of-two scales (matches +// cache_kernels.cu). fp8_ds_mla 656B entry: [0,512) NoPE fp8 (4 tiles of 128), +// [512,528) 4 fp32 tile scales, [528,656) RoPE 64 bf16. constexpr float kFp8ScaleDivisor = kFp8Max; // Copy 8 source elements (one uint4 of bf16/fp16) to `dst`. FP8=false stores a @@ -325,7 +325,7 @@ __device__ __forceinline__ void writeLatent576( } // Write [kv_c | k_pe] into the fp8_ds_mla 656B entry using one warp: NoPE 512 -// as fp8 in 4 tiles of 128 (per-tile dynamic absmax scale, 4 fp32 scales at +// as fp8 in 4 tiles of 128 (per-tile power-of-two scale, 4 fp32 scales at // [512,528)), RoPE 64 as bf16 at [528,656). Bit-compatible with // concat_and_cache_ds_mla_kernel. template @@ -349,7 +349,8 @@ __device__ __forceinline__ void writeDsMlaCache( for (int offset = 4; offset > 0; offset /= 2) { max_abs = fmaxf(max_abs, VLLM_SHFL_XOR_SYNC_WIDTH(max_abs, offset, 8)); } - float const tile_scale = fmaxf(max_abs / kFp8ScaleDivisor, FLT_MIN); + float tile_scale = fmaxf(max_abs / kFp8ScaleDivisor, 1e-4f); + tile_scale = exp2f(ceilf(log2f(tile_scale))); if ((laneId & 7) == 0) { reinterpret_cast(row)[kKvLoraRank / 4 + tile] = tile_scale; } @@ -598,7 +599,7 @@ __global__ void fusedKimiK3MLAKVConcatPackKernel( // ds_mla variant: concat full key (bf16) + fp8_ds_mla latent cache insert // // Cache entry (656 bytes), matching concat_and_cache_ds_mla_kernel: -// [0, 512) NoPE 512 vals as fp8, 4 tiles of 128, each dynamically scaled +// [0, 512) NoPE 512 vals as fp8, 4 tiles of 128, power-of-two scaled // [512, 528) 4 fp32 per-tile scales // [528, 656) RoPE 64 vals as bf16 (unquantized) // The cache slot uses one warp: lane L quantizes NoPE elems [L*16, L*16+16) diff --git a/csrc/libtorch_stable/hisparse_kernels.cu b/csrc/libtorch_stable/hisparse_kernels.cu new file mode 100644 index 000000000000..97ce9246cc75 --- /dev/null +++ b/csrc/libtorch_stable/hisparse_kernels.cu @@ -0,0 +1,1244 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright contributors to the vLLM project +// +// HiSparse decode hot-buffer kernels for sparse MLA (GLM-5 / DeepSeek-V3.2). +// +// The swap-in algorithm is a port of SGLang's hisparse +// load_cache_to_device_buffer kernel (sgl jit_kernel/csrc/hisparse.cuh), +// adapted to vLLM addressing: +// - tokens are keyed by their global KV slot id (block_table-converted +// indexer output) instead of in-request positions, so no per-request +// host-location table is needed: host pool row i is global slot i. +// - each batch row owns a fixed region of `region_stride` hot rows; +// slots [0, hot_size) are LRU-managed, slot `hot_size` holds the row's +// newest token (written directly by the KV-cache update). +// +// The kernels only move bytes; they are dtype agnostic. + +#include "torch_utils.h" +#include "ops.h" +#include "../cuda_utils.h" + +#include +#include +#include + +#include +#include +#include + +namespace { + +constexpr int kWarpSize = 32; +// Sentinel in the shared top-k scratch: entry already resolved (hit / +// newest / invalid), no miss handling needed. +constexpr int32_t kTokenDone = -1; +constexpr int32_t kHashEmpty = -1; + +bool is_pinned_cpu_tensor(const torch::stable::Tensor& tensor) { + cudaPointerAttributes attributes{}; + const auto status = + cudaPointerGetAttributes(&attributes, tensor.const_data_ptr()); + if (status != cudaSuccess) { + cudaGetLastError(); + return false; + } + return attributes.type == cudaMemoryTypeHost; +} + +__device__ __forceinline__ int32_t hash_slot(int32_t key, int32_t hash_size) { + // Knuth multiplicative hash for the open-addressing table. + return static_cast((static_cast(key) * 2654435761u) % + static_cast(hash_size)); +} + +// Copy one row of `row_bytes` bytes with a single warp. MLA rows take the +// 16-byte vectorized path; packed indexer rows use the scalar tail-safe path. +__device__ __forceinline__ void copy_row_warp(int lane_id, const char* src, + char* dst, int64_t row_bytes) { + const auto alignment = reinterpret_cast(src) | + reinterpret_cast(dst) | + static_cast(row_bytes); + if ((alignment & 15) == 0) { + const int64_t num_vec = row_bytes / 16; + const uint4* src4 = reinterpret_cast(src); + uint4* dst4 = reinterpret_cast(dst); + for (int64_t j = lane_id; j < num_vec; j += kWarpSize) { + __stcg(dst4 + j, __ldcg(src4 + j)); + } + return; + } + + if ((alignment & 3) == 0) { + const int64_t num_words = row_bytes / 4; + const unsigned int* src_words = reinterpret_cast(src); + unsigned int* dst_words = reinterpret_cast(dst); + for (int64_t j = lane_id; j < num_words; j += kWarpSize) { + __stcg(dst_words + j, __ldcg(src_words + j)); + } + return; + } + + for (int64_t j = lane_id; j < row_bytes; j += kWarpSize) { + __stcg(dst + j, __ldcg(src + j)); + } +} + +// Zero one row with a single warp (16B vectorized, L2-only stores), same +// addressing contract as copy_row_warp. +__device__ __forceinline__ void zero_row_warp(int lane_id, char* dst, + int64_t row_bytes) { + const auto alignment = + reinterpret_cast(dst) | static_cast(row_bytes); + if ((alignment & 15) == 0) { + const int64_t num_vec = row_bytes / 16; + uint64_t* dst8 = reinterpret_cast(dst); + for (int64_t j = lane_id; j < num_vec; j += kWarpSize) { + uint64_t* d = dst8 + j * 2; + asm volatile("st.global.cg.v2.b64 [%0],{%1,%2};" ::"l"(d), "l"(0ULL), + "l"(0ULL) + : "memory"); + } + return; + } + + if ((alignment & 3) == 0) { + const int64_t num_words = row_bytes / 4; + unsigned int* dst_words = reinterpret_cast(dst); + for (int64_t j = lane_id; j < num_words; j += kWarpSize) { + __stcg(dst_words + j, 0u); + } + return; + } + + for (int64_t j = lane_id; j < row_bytes; j += kWarpSize) { + __stcg(dst + j, static_cast(0)); + } +} + +__device__ __forceinline__ char* cache_row_ptr(char* cache, int64_t row, + int32_t block_size, + int64_t block_stride, + int64_t row_bytes) { + return cache + (row / block_size) * block_stride + + (row % block_size) * row_bytes; +} + +__device__ __forceinline__ void zero_cache_row_warp(int lane_id, char* cache, + int64_t row, + int32_t block_size, + int64_t block_stride, + int64_t row_bytes) { + zero_row_warp(lane_id, + cache_row_ptr(cache, row, block_size, block_stride, row_bytes), + row_bytes); +} + +// In-place inclusive scan over s_data[offset, count) performed by warp 0, +// carrying `accumulator` across calls. Returns the running total. +__device__ __forceinline__ int warp_inclusive_scan(int32_t* s_data, int lane_id, + int offset, int count, + int accumulator) { + int idx = lane_id + offset; + int val = (idx < count) ? s_data[idx] : 0; +#pragma unroll + for (int i = 1; i < 32; i *= 2) { + int n = __shfl_up_sync(0xffffffff, val, i); + if (lane_id >= i) val += n; + } + val += accumulator; + if (idx < count) { + s_data[idx] = val; + } + return __shfl_sync(0xffffffff, val, 31); +} + +__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; +} + +__device__ __forceinline__ void store_hot_index( + int32_t* hot_indices, int32_t* attention_indices, int32_t index, + int32_t physical_row, int32_t hot_block_size, + int64_t attention_block_stride) { + hot_indices[index] = physical_row; + if (attention_indices != nullptr) { + attention_indices[index] = + physical_row < 0 + ? -1 + : (physical_row / hot_block_size) * attention_block_stride + + physical_row % hot_block_size; + } +} + +// One block per batch row. +// +// Shared memory layout (int32 region followed by int16 region): +// s_topk[top_k] top-k global ids; reused as miss scratch +// s_chunk_off[nbc + 1] prefix sums for hit (then miss) compaction +// s_evict_off[nbc + 1] prefix sums for evictable compaction +// s_hash_keys[hash_size] open addressing: global id -> top-k index +// s_counters[3] hits, phase-1 resolved, valid count +// s_lru_out[hot_size] int16, compacted slots: [hits fwd | evict bwd] +// s_hash_vals[hash_size] int16 hash values (top-k index) +// Valid global ids must be unique within each row. +__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 + const int32_t* __restrict__ request_ids, // [num_rows] or nullptr + const int32_t* __restrict__ source_block_table, // [num_reqs, max_blocks] + const int32_t* __restrict__ resident_block_table, + int32_t* __restrict__ resolved_global_indices, // [num_rows, top_k] + int32_t* __restrict__ valid_counts, // [num_rows] or nullptr + int32_t* __restrict__ swap_host_physical_rows, // [num_rows, top_k] + int32_t* __restrict__ swap_device_physical_rows, // [num_rows, top_k] + int32_t* __restrict__ swap_counts, // [num_rows] + int32_t* __restrict__ hot_indices, // [num_rows, top_k] + int32_t* __restrict__ attention_indices, // [num_rows, top_k] + int32_t* __restrict__ miss_mask, // [num_rows, top_k] or nullptr + int32_t* __restrict__ device_global_indices, // [max_rows, region_stride] + int16_t* __restrict__ lru_slots, // [max_rows, hot_size] + unsigned long long* __restrict__ stats, // [2] hits,misses or nullptr + 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 int32_t top_k, const int32_t hot_size, const int32_t hash_size, + 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 resident_num_reqs, + const int32_t resident_num_blocks, const int32_t resident_block_size, + 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 int NUM_WARPS = blockDim.x / kWarpSize; + const int num_buffer_chunks = (hot_size + kWarpSize - 1) / kWarpSize; + const int num_token_chunks = (top_k + kWarpSize - 1) / kWarpSize; + + const int batch_row = blockIdx.x; + const int request_row = + request_ids != nullptr ? request_ids[batch_row] : batch_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. + if (state_row < 0) { + for (int i = threadIdx.x; i < top_k; i += blockDim.x) { + const int64_t index = static_cast(batch_row) * top_k + i; + hot_indices[index] = -1; + if (attention_indices != nullptr) { + attention_indices[static_cast(batch_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(batch_row) * valid_count_stride] = 0; + } + if (swap_counts != nullptr && threadIdx.x == 0) { + swap_counts[batch_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; + + const int32_t* row_topk = + global_indices + static_cast(batch_row) * input_row_stride; + int32_t* row_out = hot_indices + static_cast(batch_row) * top_k; + int32_t* row_attention = + attention_indices != nullptr + ? attention_indices + + static_cast(batch_row) * attention_row_stride + : nullptr; + int32_t* row_miss = (miss_mask != nullptr) + ? miss_mask + static_cast(batch_row) * top_k + : nullptr; + int32_t* row_dgi = + device_global_indices + static_cast(state_row) * region_stride; + int16_t* row_lru = lru_slots + static_cast(state_row) * hot_size; + + extern __shared__ char smem_raw[]; + int32_t* s_topk = reinterpret_cast(smem_raw); + int32_t* s_chunk_off = s_topk + top_k; + int32_t* s_evict_off = s_chunk_off + (num_buffer_chunks + 1); + int32_t* s_hash_keys = s_evict_off + (num_buffer_chunks + 1); + int32_t* s_counters = s_hash_keys + hash_size; + int16_t* s_lru_out = reinterpret_cast(s_counters + 3); + int16_t* s_hash_vals = s_lru_out + hot_size; + + if (tid < 3) { + s_counters[tid] = 0; + } + __syncthreads(); + + // Phase 1: translate request-relative positions and resolve resident rows. + for (int i = tid; i < top_k; i += blockDim.x) { + const int32_t token_index = row_topk[i]; + int32_t g = token_index; + int32_t resident_row = -1; + if (source_block_table != nullptr) { + const int32_t request_id = request_ids[batch_row]; + const int32_t source_block = + token_index >= 0 ? token_index / source_block_size : -1; + if (request_id >= 0 && request_id < source_num_reqs && + source_block >= 0 && source_block < source_num_blocks) { + const int32_t physical_block = + source_block_table[static_cast(request_id) * + source_bt_stride + + source_block]; + g = physical_block > 0 ? physical_block * source_block_size + + 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; + if (request_id >= 0 && request_id < resident_num_reqs && + resident_block >= 0 && resident_block < resident_num_blocks) { + const int32_t physical_block = + resident_block_table[static_cast(request_id) * + resident_bt_stride + + resident_block]; + if (physical_block != resident_null_block && physical_block >= 0) { + resident_row = physical_block * resident_block_size + + token_index % resident_block_size; + } + } + } + } + if (g >= host_rows) { + g = -1; + } + if (resolved_global_indices != nullptr) { + resolved_global_indices[static_cast(batch_row) * top_k + i] = g; + } + if (resident_row >= 0 || g >= 0) atomicAdd(&s_counters[2], 1); + if (row_miss != nullptr) row_miss[i] = 0; + if (resident_row >= 0) { + store_hot_index(row_out, row_attention, i, resident_row, hot_block_size, + attention_block_stride); + s_topk[i] = kTokenDone; + atomicAdd(&s_counters[1], 1); + } else if (g < 0) { + store_hot_index(row_out, row_attention, i, -1, hot_block_size, + attention_block_stride); + s_topk[i] = kTokenDone; + atomicAdd(&s_counters[1], 1); + } else { + s_topk[i] = g; + } + } + __syncthreads(); + if (valid_counts != nullptr && tid == 0) { + valid_counts[static_cast(batch_row) * valid_count_stride] = + s_counters[2]; + } + // Fully resident rows need only request-relative page translation. Avoid + // scanning or rewriting the hot LRU when no selected row can consult it. + if (s_counters[1] == top_k) { + if (tid == 0 && swap_counts != nullptr) { + swap_counts[batch_row] = 0; + } + return; + } + + for (int i = tid; i < hash_size; i += blockDim.x) { + s_hash_keys[i] = kHashEmpty; + } + for (int i = tid; i < num_buffer_chunks + 1; i += blockDim.x) { + s_chunk_off[i] = 0; + s_evict_off[i] = 0; + } + __syncthreads(); + + for (int i = tid; i < top_k; i += blockDim.x) { + const int32_t g = s_topk[i]; + if (g == kTokenDone) { + continue; + } + int slot = hash_slot(g, hash_size); + while (true) { + const int32_t old = atomicCAS(&s_hash_keys[slot], kHashEmpty, g); + if (old == kHashEmpty || old == g) { + s_hash_vals[slot] = static_cast(i); + break; + } + slot = (slot + 1) % hash_size; + } + } + __syncthreads(); + + // Phase 2: walk hot slots in LRU order, classify hit / evictable, and + // compact them (hits forward, evictables backward) into s_lru_out. + const int iters_buffer = (num_buffer_chunks + NUM_WARPS - 1) / NUM_WARPS; + int total_hit_count = 0; + int total_evict_count = 0; + for (int iter = 0; iter < iters_buffer; iter++) { + const int chunk_idx = warp_id + iter * NUM_WARPS; + const bool has_valid_chunk = chunk_idx < num_buffer_chunks; + + const int pos = chunk_idx * kWarpSize + lane_id; + const bool has_valid_pos = has_valid_chunk && (pos < hot_size); + const int16_t slot = has_valid_pos ? row_lru[pos] : int16_t(-1); + // Corruption tripwire: lru/dgi are long-lived device state; if some + // external writer (e.g. stray RDMA into reused VRAM) corrupts a slot + // out of [0, region_stride), treat it as no-hit so it degrades to a re-miss + // instead of an unbounded dgi read (phases 3/5 bound the write side). + const int32_t cached_g = + (slot >= 0 && slot < region_stride) ? row_dgi[slot] : -1; + + int found_topk_idx = -1; + if (cached_g >= 0) { + int h = hash_slot(cached_g, hash_size); + while (true) { + const int32_t k = s_hash_keys[h]; + if (k == cached_g) { + found_topk_idx = static_cast(s_hash_vals[h]); + break; + } + if (k == kHashEmpty) break; + h = (h + 1) % hash_size; + } + } + const bool is_hit = found_topk_idx >= 0; + const bool is_evictable = has_valid_pos && !is_hit; + + if (is_hit) { + s_topk[found_topk_idx] = kTokenDone; + store_hot_index(row_out, row_attention, found_topk_idx, + static_cast(get_physical_hot_row( + hot_block_table, request_row, hot_table_stride, + hot_block_size, slot)), + hot_block_size, attention_block_stride); + } + + int local_hit_off = 0; + int local_evict_off = 0; + if (has_valid_chunk) { + const unsigned int hit_mask = __ballot_sync(0xFFFFFFFF, is_hit); + const unsigned int evict_mask = __ballot_sync(0xFFFFFFFF, is_evictable); + local_hit_off = __popc(hit_mask & lanes_before); + local_evict_off = __popc(evict_mask & lanes_before); + if (lane_id == 0) { + s_chunk_off[chunk_idx + 1] = __popc(hit_mask); + s_evict_off[chunk_idx + 1] = __popc(evict_mask); + } + } + __syncthreads(); + + if (warp_id == 0) { + total_hit_count = + warp_inclusive_scan(s_chunk_off, lane_id, chunk_idx + 1, + num_buffer_chunks + 1, total_hit_count); + total_evict_count = + warp_inclusive_scan(s_evict_off, lane_id, chunk_idx + 1, + num_buffer_chunks + 1, total_evict_count); + if (tid == 0) { + s_counters[0] = total_hit_count; + } + } + __syncthreads(); + + if (is_hit) { + const int off = s_chunk_off[chunk_idx] + local_hit_off; + s_lru_out[off] = slot; + } + if (is_evictable) { + const int off = s_evict_off[chunk_idx] + local_evict_off; + s_lru_out[hot_size - 1 - off] = slot; + } + } + __syncthreads(); + + // Reset prefix sums for the miss compaction (token chunks <= buffer + // chunks because hot_size >= top_k). + for (int i = tid; i < num_token_chunks + 1; i += blockDim.x) { + s_chunk_off[i] = 0; + } + __syncthreads(); + + // Phase 3: compact misses, assign them eviction slots (oldest first) and + // record the new ownership in device_global_indices. + const int iters_token = (num_token_chunks + NUM_WARPS - 1) / NUM_WARPS; + int miss_running_total = 0; + for (int iter = 0; iter < iters_token; iter++) { + const int chunk_idx = warp_id + iter * NUM_WARPS; + const bool has_valid_chunk = chunk_idx < num_token_chunks; + + const int i = chunk_idx * kWarpSize + lane_id; + const bool has_valid_token = has_valid_chunk && (i < top_k); + + int32_t g = 0; + bool is_miss = false; + if (has_valid_token) { + is_miss = s_topk[i] != kTokenDone; + if (is_miss) { + g = s_topk[i]; + } + } + + int local_miss_off = 0; + if (has_valid_chunk) { + const unsigned int miss_mask = __ballot_sync(0xFFFFFFFF, is_miss); + local_miss_off = __popc(miss_mask & lanes_before); + if (lane_id == 0) { + s_chunk_off[chunk_idx + 1] = __popc(miss_mask); + } + } + __syncthreads(); + + if (warp_id == 0) { + miss_running_total = + warp_inclusive_scan(s_chunk_off, lane_id, chunk_idx + 1, + num_token_chunks + 1, miss_running_total); + } + __syncthreads(); + + if (is_miss) { + const int m = s_chunk_off[chunk_idx] + local_miss_off; + const int16_t evict_slot = s_lru_out[hot_size - 1 - m]; + if (evict_slot < 0 || evict_slot >= region_stride) { + // Corruption tripwire: an out-of-range slot (corrupted lru state, + // see phase 2) must not become a hot-cache/dgi write. Resolve the + // entry as invalid (-1, masked by attention, not in the miss set); + // it re-misses on a later step. Phase 5 re-checks the same + // s_lru_out value, so its copy is skipped consistently. + store_hot_index(row_out, row_attention, i, -1, hot_block_size, + attention_block_stride); + if (swap_host_physical_rows != nullptr) { + const int64_t compact_index = + static_cast(batch_row) * top_k + m; + swap_host_physical_rows[compact_index] = g; + swap_device_physical_rows[compact_index] = -1; + } + } else { + // Reuse s_topk as compacted miss scratch: m < i always holds (done + // entries are skipped), so writes never overrun pending reads. + s_topk[m] = g; + const int32_t physical_row = static_cast( + get_physical_hot_row(hot_block_table, request_row, hot_table_stride, + hot_block_size, evict_slot)); + store_hot_index(row_out, row_attention, i, physical_row, hot_block_size, + attention_block_stride); + if (swap_host_physical_rows != nullptr) { + const int64_t compact_index = + static_cast(batch_row) * top_k + m; + swap_host_physical_rows[compact_index] = g; + swap_device_physical_rows[compact_index] = physical_row; + } + if (row_miss != nullptr) row_miss[i] = 1; + row_dgi[evict_slot] = g; + } + } + } + __syncthreads(); + + const int total_hits = s_counters[0]; + const int total_misses = top_k - total_hits - s_counters[1]; + if (swap_counts != nullptr && tid == 0) { + swap_counts[batch_row] = total_misses; + } + if (stats != nullptr && tid == 0) { + atomicAdd(&stats[0], static_cast(total_hits)); + atomicAdd(&stats[1], static_cast(total_misses)); + } + + // Phase 4: write back the LRU order: stale evictables at the front, + // freshly loaded misses next, then hits at MRU. + const int total_evictable = hot_size - total_hits; + const int remaining_evictable = total_evictable - total_misses; + for (int i = tid; i < hot_size; i += blockDim.x) { + if (i < remaining_evictable) { + row_lru[i] = s_lru_out[hot_size - 1 - total_misses - i]; + } else if (i < remaining_evictable + total_misses) { + row_lru[i] = s_lru_out[hot_size - 1 - (i - remaining_evictable)]; + } else { + row_lru[i] = s_lru_out[i - remaining_evictable - total_misses]; + } + } +} + +// Shared-layer plan replay. Given a plan (hot_indices + miss_mask) already +// computed by the group's index-producing layer, gather THIS +// layer's own missed KV rows into the planned hot slots. No LRU resolution: +// index-sharing shared layers see identical global_indices/hot_indices, so the +// slot assignment is identical -- only the per-layer bytes differ. Fixed shape +// (num_rows x top_k), so it is CUDA-graph-capture safe. +__global__ void hisparse_gather_plan_kernel( + const char* __restrict__ host_cache, // [host_rows, row_bytes] + char* __restrict__ hot_cache, // [hot_rows, row_bytes] + const int32_t* __restrict__ global_indices, // [num_rows, top_k] + const int32_t* __restrict__ hot_indices, // [num_rows, top_k] abs hot rows + const int32_t* __restrict__ miss_mask, // [num_rows, top_k] + int32_t* __restrict__ attention_indices, // [num_rows, top_k] + const int32_t* __restrict__ request_state_indices, // [num_rows] or nullptr + const int64_t host_rows, const int64_t hot_rows, const int64_t row_bytes, + const int64_t hot_block_stride, const int32_t hot_block_size, + const int32_t top_k, const int64_t attention_block_stride) { + const int NUM_WARPS = blockDim.x / kWarpSize; + // Columns are interleaved across gridDim.y blocks per row so few-row + // launches (local-prefill staging's single-row layout, small decode + // batches) still fill the device with outstanding host reads instead of + // starving on one block per row. + const int row = blockIdx.x; + const bool is_padding = + request_state_indices != nullptr && request_state_indices[row] < 0; + const int warp_id = threadIdx.x / kWarpSize; + const int lane_id = threadIdx.x % kWarpSize; + const int col_start = blockIdx.y * NUM_WARPS + warp_id; + const int col_stride = gridDim.y * NUM_WARPS; + const int64_t base = static_cast(row) * top_k; + for (int col = col_start; col < top_k; col += col_stride) { + const int32_t dst = hot_indices[base + col]; + if (lane_id == 0 && attention_indices != nullptr) { + attention_indices[base + col] = + is_padding + ? -1 + : (dst < 0 ? -1 + : (dst / hot_block_size) * attention_block_stride + + dst % hot_block_size); + } + if (is_padding) { + continue; + } + if (miss_mask[base + col] == 0) { + continue; + } + const int32_t g = global_indices[base + col]; + if (g < 0 || dst < 0 || dst >= hot_rows) { + continue; + } + if (g < host_rows) { + copy_row_warp(lane_id, host_cache + static_cast(g) * row_bytes, + cache_row_ptr(hot_cache, dst, hot_block_size, + hot_block_stride, row_bytes), + row_bytes); + } else { + // No source row for g: zero the planned slot rather than serving + // whatever bytes it held (see the swap-in kernel's phase 5). + zero_cache_row_warp(lane_id, hot_cache, dst, hot_block_size, + hot_block_stride, row_bytes); + } + } +} + +// Copy the compact swap rows produced by the residency resolver. +__global__ void hisparse_gather_compact_kernel( + const char* __restrict__ host_cache, char* __restrict__ hot_cache, + const int32_t* __restrict__ miss_global_indices, + const int32_t* __restrict__ miss_hot_indices, + const int32_t* __restrict__ miss_counts, const int64_t host_rows, + const int64_t hot_rows, const int64_t row_bytes, + const int64_t hot_block_stride, const int32_t hot_block_size, + const int32_t top_k) { + const int NUM_WARPS = blockDim.x / kWarpSize; + const int row = blockIdx.x; + const int warp_id = threadIdx.x / kWarpSize; + const int lane_id = threadIdx.x % kWarpSize; + const int miss_count = min(max(miss_counts[row], 0), top_k); + const int col_start = blockIdx.y * NUM_WARPS + warp_id; + const int col_stride = gridDim.y * NUM_WARPS; + const int64_t base = static_cast(row) * top_k; + for (int col = col_start; col < miss_count; col += col_stride) { + const int32_t g = miss_global_indices[base + col]; + const int32_t dst = miss_hot_indices[base + col]; + if (g < 0 || dst < 0 || dst >= hot_rows) { + continue; + } + if (g < host_rows) { + copy_row_warp(lane_id, host_cache + static_cast(g) * row_bytes, + cache_row_ptr(hot_cache, dst, hot_block_size, + hot_block_stride, row_bytes), + row_bytes); + } else { + zero_cache_row_warp(lane_id, hot_cache, dst, hot_block_size, + hot_block_stride, row_bytes); + } + } +} + +__global__ void hisparse_invalidate_written_slots_kernel( + int32_t* __restrict__ device_global_indices, + 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 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) { + 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) { + row[offset] = -1; + } + } +} + +int64_t check_2d_rows(const torch::stable::Tensor& t, const char* name, + int64_t row_bytes) { + STD_TORCH_CHECK(t.dim() == 2, name, " must be 2D"); + STD_TORCH_CHECK(t.is_contiguous(), name, " must be contiguous"); + STD_TORCH_CHECK(t.size(1) * t.element_size() == row_bytes, name, + " row width mismatch"); + return t.size(0); +} + +} // namespace + +void hisparse_invalidate_written_slots( + torch::stable::Tensor& device_global_indices, + 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() && + 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"); + STD_TORCH_CHECK( + request_state_indices.scalar_type() == + torch::headeronly::ScalarType::Int && + request_state_indices.dim() == 1 && + request_state_indices.is_contiguous(), + "request_state_indices must be a contiguous 1D int32 CUDA tensor"); + STD_TORCH_CHECK( + req_id_per_token.scalar_type() == torch::headeronly::ScalarType::Int && + req_id_per_token.dim() == 1 && req_id_per_token.is_contiguous(), + "req_id_per_token must be a contiguous 1D int32 CUDA tensor"); + STD_TORCH_CHECK( + written_slots.scalar_type() == torch::headeronly::ScalarType::Long && + written_slots.dim() == 1 && written_slots.is_contiguous(), + "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(); + STD_TORCH_CHECK( + request_state_indices.get_device_index() == device_index && + req_id_per_token.get_device_index() == device_index && + written_slots.get_device_index() == device_index, + "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) { + return; + } + STD_TORCH_CHECK(num_tokens <= INT32_MAX, + "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(), + 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); + const cudaError_t launch_error = cudaGetLastError(); + STD_TORCH_CHECK(launch_error == cudaSuccess, + "HiSparse invalidation kernel launch failed: ", + cudaGetErrorString(launch_error)); +} + +void hisparse_resolve_residency( + torch::stable::Tensor const& host_cache, torch::stable::Tensor& hot_cache, + torch::stable::Tensor const& hot_block_table, + torch::stable::Tensor const& global_indices, + torch::stable::Tensor& hot_indices, + torch::stable::Tensor& device_global_indices, + torch::stable::Tensor& lru_slots, + std::optional const& request_state_indices, + int64_t region_stride, + std::optional const& miss_mask, + std::optional const& stats, + std::optional const& attention_indices, + int64_t attention_block_stride, + std::optional const& request_ids, + std::optional const& source_block_table, + int64_t source_block_size, + std::optional const& resolved_global_indices, + std::optional const& valid_counts, + std::optional const& swap_host_physical_rows, + 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) { + STD_TORCH_CHECK( + host_cache.device().is_cpu() && is_pinned_cpu_tensor(host_cache), + "host_cache must be pinned CPU memory"); + STD_TORCH_CHECK(hot_cache.is_cuda(), "hot_cache must be on CUDA"); + STD_TORCH_CHECK( + hot_block_table.is_cuda() && + hot_block_table.scalar_type() == torch::headeronly::ScalarType::Int && + hot_block_table.dim() == 2, + "hot_block_table must be a 2D int32 CUDA tensor"); + STD_TORCH_CHECK(global_indices.is_cuda() && hot_indices.is_cuda() && + device_global_indices.is_cuda() && lru_slots.is_cuda(), + "index tensors must be on CUDA"); + STD_TORCH_CHECK( + global_indices.scalar_type() == torch::headeronly::ScalarType::Int && + hot_indices.scalar_type() == torch::headeronly::ScalarType::Int, + "global_indices/hot_indices must be int32"); + STD_TORCH_CHECK( + device_global_indices.scalar_type() == torch::headeronly::ScalarType::Int, + "device_global_indices must be int32"); + STD_TORCH_CHECK( + lru_slots.scalar_type() == torch::headeronly::ScalarType::Short, + "lru_slots must be int16"); + STD_TORCH_CHECK(global_indices.dim() == 2 && global_indices.stride(1) == 1, + "global_indices must be row-major 2D"); + const int64_t num_rows = global_indices.size(0); + STD_TORCH_CHECK(num_rows >= 0 && num_rows <= INT32_MAX, + "num_rows must fit the CUDA grid"); + STD_TORCH_CHECK(hot_indices.size(0) == num_rows && + hot_indices.size(1) == global_indices.size(1) && + hot_indices.is_contiguous(), + "hot_indices must have one contiguous row per launch row"); + STD_TORCH_CHECK( + device_global_indices.dim() == 2 && device_global_indices.is_contiguous(), + "device_global_indices must be contiguous 2D"); + STD_TORCH_CHECK( + lru_slots.dim() == 2 && + lru_slots.size(0) == device_global_indices.size(0) && + lru_slots.is_contiguous(), + "lru_slots must be contiguous with one row per request state"); + STD_TORCH_CHECK(hot_cache.dim() == 3, + "hot_cache must be [num_blocks, block_size, row_width]"); + const int64_t row_bytes = hot_cache.size(-1) * hot_cache.element_size(); + STD_TORCH_CHECK(row_bytes % 16 == 0, "KV rows must be 16-byte aligned"); + STD_TORCH_CHECK(hot_cache.stride(1) * hot_cache.element_size() == row_bytes, + "hot-cache rows must be contiguous"); + const int64_t hot_block_size = hot_cache.size(1); + const int64_t hot_rows = hot_cache.size(0) * hot_block_size; + const int64_t hot_block_stride = + hot_cache.stride(0) * hot_cache.element_size(); + const int64_t host_rows = check_2d_rows(host_cache, "host_cache", row_bytes); + const auto launch_rows = static_cast(num_rows); + const auto top_k = static_cast(global_indices.size(1)); + const auto hot_size = static_cast(lru_slots.size(1)); + STD_TORCH_CHECK(hot_size >= top_k, "hot buffer size must be >= top_k"); + STD_TORCH_CHECK(hot_size <= 32768, "hot buffer size must fit int16 slots"); + STD_TORCH_CHECK(region_stride == hot_size, + "region_stride must match the LRU size"); + STD_TORCH_CHECK(device_global_indices.size(1) == region_stride, + "device_global_indices must cover the full hot region"); + STD_TORCH_CHECK(hot_block_table.size(1) >= + (region_stride + hot_block_size - 1) / hot_block_size, + "hot_block_table has too few columns"); + STD_TORCH_CHECK(hot_rows < INT32_MAX, "hot indices must fit int32"); + + const int32_t* request_ids_ptr = nullptr; + const int32_t* source_block_table_ptr = nullptr; + int64_t source_bt_stride = 0; + int32_t source_num_reqs = 0; + int32_t source_num_blocks = 0; + STD_TORCH_CHECK( + request_ids.has_value() == source_block_table.has_value(), + "request_ids and source_block_table must be provided together"); + if (source_block_table.has_value()) { + auto const& req = request_ids.value(); + auto const& table = source_block_table.value(); + STD_TORCH_CHECK( + req.is_cuda() && + req.scalar_type() == torch::headeronly::ScalarType::Int && + req.numel() >= launch_rows && req.is_contiguous(), + "request_ids must be contiguous int32 on CUDA with one entry per row"); + STD_TORCH_CHECK( + table.is_cuda() && + table.scalar_type() == torch::headeronly::ScalarType::Int && + table.dim() == 2 && table.stride(1) == 1, + "source_block_table must be a row-major 2D int32 CUDA tensor"); + STD_TORCH_CHECK(source_block_size > 0, + "source_block_size must be positive"); + request_ids_ptr = req.const_data_ptr(); + source_block_table_ptr = table.const_data_ptr(); + source_bt_stride = table.stride(0); + source_num_reqs = static_cast(table.size(0)); + source_num_blocks = static_cast(table.size(1)); + } + 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, + "hot_block_table has too few rows"); + + const int32_t* resident_block_table_ptr = nullptr; + int64_t resident_bt_stride = 0; + int32_t resident_num_reqs = 0; + int32_t resident_num_blocks = 0; + if (resident_block_table.has_value()) { + auto const& table = resident_block_table.value(); + STD_TORCH_CHECK( + source_block_table.has_value() && request_ids.has_value(), + "resident lookup requires request_ids and source_block_table"); + STD_TORCH_CHECK( + table.is_cuda() && + table.scalar_type() == torch::headeronly::ScalarType::Int && + table.dim() == 2 && table.stride(1) == 1, + "resident_block_table must be a row-major 2D int32 CUDA tensor"); + STD_TORCH_CHECK(resident_block_size == hot_block_size, + "resident and hot block sizes must match"); + STD_TORCH_CHECK(resident_null_block >= 0, + "resident null block must be non-negative"); + resident_block_table_ptr = table.const_data_ptr(); + resident_bt_stride = table.stride(0); + resident_num_reqs = static_cast(table.size(0)); + resident_num_blocks = static_cast(table.size(1)); + } + + int32_t* resolved_global_indices_ptr = nullptr; + if (resolved_global_indices.has_value()) { + auto const& resolved = resolved_global_indices.value(); + STD_TORCH_CHECK( + resolved.is_cuda() && + resolved.scalar_type() == torch::headeronly::ScalarType::Int && + resolved.dim() == 2 && resolved.size(0) == launch_rows && + resolved.size(1) == global_indices.size(1) && + resolved.is_contiguous(), + "resolved_global_indices must be contiguous int32 matching indices"); + resolved_global_indices_ptr = resolved.mutable_data_ptr(); + } + + int32_t* valid_counts_ptr = nullptr; + if (valid_counts.has_value()) { + auto const& counts = valid_counts.value(); + STD_TORCH_CHECK( + counts.is_cuda() && + counts.scalar_type() == torch::headeronly::ScalarType::Int && + counts.dim() == 1 && counts.size(0) == launch_rows, + "valid_counts must be int32 on CUDA with one entry per row"); + valid_counts_ptr = counts.mutable_data_ptr(); + } + + int32_t* swap_host_physical_rows_ptr = nullptr; + int32_t* swap_device_physical_rows_ptr = nullptr; + int32_t* swap_counts_ptr = nullptr; + const bool has_compact_swaps = swap_host_physical_rows.has_value(); + STD_TORCH_CHECK(has_compact_swaps == swap_device_physical_rows.has_value() && + has_compact_swaps == swap_counts.has_value(), + "compact swap tensors must be provided together"); + if (has_compact_swaps) { + auto const& globals = swap_host_physical_rows.value(); + auto const& hots = swap_device_physical_rows.value(); + auto const& counts = swap_counts.value(); + STD_TORCH_CHECK( + globals.is_cuda() && hots.is_cuda() && counts.is_cuda() && + globals.scalar_type() == torch::headeronly::ScalarType::Int && + hots.scalar_type() == torch::headeronly::ScalarType::Int && + counts.scalar_type() == torch::headeronly::ScalarType::Int && + globals.dim() == 2 && hots.dim() == 2 && + globals.size(0) == launch_rows && + globals.size(1) == global_indices.size(1) && + hots.size(0) == launch_rows && + hots.size(1) == global_indices.size(1) && + counts.numel() >= launch_rows && globals.is_contiguous() && + hots.is_contiguous() && counts.is_contiguous(), + "compact swaps must be contiguous int32 matching indices"); + swap_host_physical_rows_ptr = globals.mutable_data_ptr(); + swap_device_physical_rows_ptr = hots.mutable_data_ptr(); + swap_counts_ptr = counts.mutable_data_ptr(); + } + + const int32_t* request_state_ptr = nullptr; + int32_t request_state_count = 0; + if (request_state_indices.has_value()) { + auto const& state_indices = request_state_indices.value(); + STD_TORCH_CHECK( + state_indices.is_cuda() && + state_indices.scalar_type() == torch::headeronly::ScalarType::Int && + state_indices.dim() == 1 && state_indices.is_contiguous() && + state_indices.numel() <= INT32_MAX, + "request_state_indices must be contiguous 1D int32 on CUDA"); + request_state_ptr = state_indices.const_data_ptr(); + request_state_count = static_cast(state_indices.numel()); + } else { + STD_TORCH_CHECK(device_global_indices.size(0) >= launch_rows, + "device_global_indices has too few rows"); + } + + // Optional output: 1 at columns requiring a host-to-device swap, 0 elsewhere. + int32_t* miss_mask_ptr = nullptr; + if (miss_mask.has_value()) { + auto const& mm = miss_mask.value(); + STD_TORCH_CHECK( + mm.is_cuda() && + mm.scalar_type() == torch::headeronly::ScalarType::Int && + mm.dim() == 2 && mm.is_contiguous() && mm.size(0) == launch_rows && + mm.size(1) == global_indices.size(1), + "miss_mask must be a contiguous int32 CUDA tensor matching " + "global_indices"); + miss_mask_ptr = mm.mutable_data_ptr(); + } + + unsigned long long* stats_ptr = nullptr; + if (stats.has_value()) { + auto const& st = stats.value(); + STD_TORCH_CHECK( + st.is_cuda() && st.is_contiguous() && st.dim() == 1 && + st.scalar_type() == torch::headeronly::ScalarType::UInt64 && + st.numel() == 2, + "stats must be a contiguous two-element uint64 CUDA tensor"); + stats_ptr = static_cast(st.mutable_data_ptr()); + } + + int32_t* attention_indices_ptr = nullptr; + if (attention_indices.has_value()) { + auto const& indices = attention_indices.value(); + STD_TORCH_CHECK( + indices.is_cuda() && + indices.scalar_type() == torch::headeronly::ScalarType::Int && + indices.dim() == 2 && indices.size(0) == launch_rows && + indices.stride(1) == 1 && indices.size(1) == global_indices.size(1), + "attention_indices must be row-major int32 matching global_indices"); + STD_TORCH_CHECK(attention_block_stride >= hot_block_size, + "attention block stride must cover one hot block"); + attention_indices_ptr = indices.mutable_data_ptr(); + } + + if (launch_rows == 0 || top_k == 0) { + return; + } + + constexpr int kBlockSize = 1024; + const int hash_size = 2 * top_k; + const int num_buffer_chunks = (hot_size + kWarpSize - 1) / kWarpSize; + const size_t smem_bytes = + sizeof(int32_t) * (top_k + 2 * (num_buffer_chunks + 1) + hash_size + 3) + + sizeof(int16_t) * (hot_size + hash_size); + + const torch::stable::accelerator::DeviceGuard device_guard( + hot_cache.get_device_index()); + const cudaStream_t stream = get_current_cuda_stream(); + const int64_t attention_row_stride = + 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; + if (smem_bytes > 48 * 1024) { + 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)); + } + kernel<<>>( + hot_block_table.const_data_ptr(), + global_indices.const_data_ptr(), request_ids_ptr, + source_block_table_ptr, resident_block_table_ptr, + resolved_global_indices_ptr, valid_counts_ptr, + swap_host_physical_rows_ptr, swap_device_physical_rows_ptr, + swap_counts_ptr, hot_indices.mutable_data_ptr(), + attention_indices_ptr, miss_mask_ptr, + device_global_indices.mutable_data_ptr(), + lru_slots.mutable_data_ptr(), stats_ptr, request_state_ptr, + request_state_count, host_rows, hot_block_table.stride(0), hot_block_size, + top_k, hot_size, hash_size, region_stride, attention_block_stride, + source_bt_stride, source_num_reqs, source_num_blocks, + static_cast(source_block_size), 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); + const cudaError_t launch_error = cudaGetLastError(); + STD_TORCH_CHECK(launch_error == cudaSuccess, + "HiSparse residency kernel launch failed: ", + cudaGetErrorString(launch_error)); +} + +void hisparse_gather_plan( + torch::stable::Tensor const& host_cache, torch::stable::Tensor& hot_cache, + torch::stable::Tensor const& global_indices, + torch::stable::Tensor const& hot_indices, + torch::stable::Tensor const& miss_mask, + std::optional const& request_state_indices, + std::optional const& attention_indices, + int64_t attention_block_stride) { + STD_TORCH_CHECK( + host_cache.device().is_cpu() && is_pinned_cpu_tensor(host_cache), + "host_cache must be pinned CPU memory"); + STD_TORCH_CHECK(hot_cache.is_cuda(), "hot_cache must be on CUDA"); + STD_TORCH_CHECK( + global_indices.is_cuda() && hot_indices.is_cuda() && miss_mask.is_cuda(), + "plan tensors must be on CUDA"); + STD_TORCH_CHECK( + global_indices.scalar_type() == torch::headeronly::ScalarType::Int && + hot_indices.scalar_type() == torch::headeronly::ScalarType::Int && + miss_mask.scalar_type() == torch::headeronly::ScalarType::Int, + "plan tensors must be int32"); + STD_TORCH_CHECK(global_indices.dim() == 2 && global_indices.is_contiguous(), + "global_indices must be contiguous 2D"); + STD_TORCH_CHECK( + hot_indices.size(0) == global_indices.size(0) && + hot_indices.size(1) == global_indices.size(1) && + miss_mask.size(0) == global_indices.size(0) && + miss_mask.size(1) == global_indices.size(1) && + hot_indices.is_contiguous() && miss_mask.is_contiguous(), + "hot_indices/miss_mask must match contiguous 2D global_indices"); + + STD_TORCH_CHECK(hot_cache.dim() == 2 || hot_cache.dim() == 3, + "hot_cache must be a 2D staging buffer or paged 3D cache"); + const int64_t row_bytes = hot_cache.size(-1) * hot_cache.element_size(); + STD_TORCH_CHECK(row_bytes % 16 == 0, "KV rows must be 16-byte aligned"); + STD_TORCH_CHECK( + hot_cache.stride(hot_cache.dim() - 2) * hot_cache.element_size() == + row_bytes, + "hot-cache rows must be contiguous"); + const int64_t host_rows = check_2d_rows(host_cache, "host_cache", row_bytes); + + const auto num_rows = static_cast(global_indices.size(0)); + const auto top_k = static_cast(global_indices.size(1)); + + const int32_t* request_state_ptr = nullptr; + if (request_state_indices.has_value()) { + auto const& state_indices = request_state_indices.value(); + STD_TORCH_CHECK( + state_indices.is_cuda() && + state_indices.scalar_type() == torch::headeronly::ScalarType::Int && + state_indices.numel() >= num_rows, + "request_state_indices must be int32 on CUDA with one entry per row"); + request_state_ptr = state_indices.const_data_ptr(); + } + + int32_t* attention_indices_ptr = nullptr; + if (attention_indices.has_value()) { + auto const& indices = attention_indices.value(); + STD_TORCH_CHECK( + indices.is_cuda() && + indices.scalar_type() == torch::headeronly::ScalarType::Int && + indices.dim() == 2 && indices.is_contiguous() && + indices.size(0) == global_indices.size(0) && + indices.size(1) == global_indices.size(1), + "attention_indices must be a contiguous int32 CUDA tensor matching " + "global_indices"); + attention_indices_ptr = indices.mutable_data_ptr(); + } + + if (num_rows == 0 || top_k == 0) { + return; + } + + // Match the swap-in kernel's block size: the gather serves 3 of every 4 + // layers' misses (index-sharing replay), so per-row copy parallelism is + // the throughput limiter on cold rows. + constexpr int kBlockSize = 1024; + constexpr int kNumWarps = kBlockSize / kWarpSize; + // Interleave columns over enough blocks per row to cover the device even + // for few-row launches (local-prefill staging's single-row layout, small + // decode batches), keeping >= 1 column per warp. + constexpr int kTargetBlocks = 256; + const int max_chunks = std::max(1, (top_k + kNumWarps - 1) / kNumWarps); + const int num_chunks = + std::min(max_chunks, std::max(1, kTargetBlocks / num_rows)); + const dim3 grid(num_rows, num_chunks); + const int32_t hot_block_size = + hot_cache.dim() == 3 ? static_cast(hot_cache.size(1)) : 1; + if (attention_indices.has_value()) { + STD_TORCH_CHECK(attention_block_stride >= hot_block_size, + "attention block stride must cover one hot block"); + } + const int64_t hot_rows = hot_cache.size(0) * hot_block_size; + const int64_t hot_block_stride = + hot_cache.stride(0) * hot_cache.element_size(); + const torch::stable::accelerator::DeviceGuard device_guard( + hot_cache.get_device_index()); + const cudaStream_t stream = get_current_cuda_stream(); + hisparse_gather_plan_kernel<<>>( + static_cast(host_cache.const_data_ptr()), + static_cast(hot_cache.mutable_data_ptr()), + global_indices.const_data_ptr(), + hot_indices.const_data_ptr(), + miss_mask.const_data_ptr(), attention_indices_ptr, + request_state_ptr, host_rows, hot_rows, row_bytes, hot_block_stride, + hot_block_size, top_k, attention_block_stride); +} + +void hisparse_gather_compact(torch::stable::Tensor const& host_cache, + torch::stable::Tensor& hot_cache, + torch::stable::Tensor const& miss_global_indices, + torch::stable::Tensor const& miss_hot_indices, + torch::stable::Tensor const& miss_counts) { + STD_TORCH_CHECK( + host_cache.device().is_cpu() && is_pinned_cpu_tensor(host_cache), + "host_cache must be pinned CPU memory"); + STD_TORCH_CHECK(hot_cache.is_cuda(), "hot_cache must be on CUDA"); + STD_TORCH_CHECK( + miss_global_indices.is_cuda() && miss_hot_indices.is_cuda() && + miss_counts.is_cuda() && + miss_global_indices.scalar_type() == + torch::headeronly::ScalarType::Int && + miss_hot_indices.scalar_type() == + torch::headeronly::ScalarType::Int && + miss_counts.scalar_type() == torch::headeronly::ScalarType::Int && + miss_global_indices.dim() == 2 && miss_hot_indices.dim() == 2 && + miss_global_indices.size(0) == miss_hot_indices.size(0) && + miss_global_indices.size(1) == miss_hot_indices.size(1) && + miss_counts.numel() >= miss_global_indices.size(0) && + miss_global_indices.is_contiguous() && + miss_hot_indices.is_contiguous() && miss_counts.is_contiguous(), + "compact miss plan must be matching contiguous int32 CUDA tensors"); + STD_TORCH_CHECK(hot_cache.dim() == 3, + "hot_cache must be [num_blocks, block_size, row_width]"); + const int64_t row_bytes = hot_cache.size(-1) * hot_cache.element_size(); + STD_TORCH_CHECK(row_bytes % 16 == 0, "KV rows must be 16-byte aligned"); + STD_TORCH_CHECK(hot_cache.stride(1) * hot_cache.element_size() == row_bytes, + "hot-cache rows must be contiguous"); + const int64_t host_rows = check_2d_rows(host_cache, "host_cache", row_bytes); + const auto num_rows = static_cast(miss_global_indices.size(0)); + const auto top_k = static_cast(miss_global_indices.size(1)); + if (num_rows == 0 || top_k == 0) { + return; + } + + constexpr int kBlockSize = 512; + constexpr int kTargetBlocks = 64; + const int num_chunks = std::min(8, std::max(1, kTargetBlocks / num_rows)); + const dim3 grid(num_rows, num_chunks); + const int32_t hot_block_size = static_cast(hot_cache.size(1)); + const int64_t hot_rows = hot_cache.size(0) * hot_block_size; + const int64_t hot_block_stride = + hot_cache.stride(0) * hot_cache.element_size(); + const torch::stable::accelerator::DeviceGuard device_guard( + hot_cache.get_device_index()); + const cudaStream_t stream = get_current_cuda_stream(); + hisparse_gather_compact_kernel<<>>( + static_cast(host_cache.const_data_ptr()), + static_cast(hot_cache.mutable_data_ptr()), + miss_global_indices.const_data_ptr(), + miss_hot_indices.const_data_ptr(), + miss_counts.const_data_ptr(), host_rows, hot_rows, row_bytes, + hot_block_stride, hot_block_size, top_k); +} diff --git a/csrc/libtorch_stable/moe/moe_align_sum_kernels.cu b/csrc/libtorch_stable/moe/moe_align_sum_kernels.cu index 98a04de783d4..5c9a522feb7b 100644 --- a/csrc/libtorch_stable/moe/moe_align_sum_kernels.cu +++ b/csrc/libtorch_stable/moe/moe_align_sum_kernels.cu @@ -23,20 +23,18 @@ namespace batched_moe_align_block_size { // Note num_threads needs to be 1024 for BlockScan Reduction in the kernel. static constexpr int32_t num_threads = 1024; static constexpr int32_t num_blocks = 1; +template __global__ void batched_moe_align_block_size_kernel( int32_t const num_batches, int32_t const max_tokens_per_batch, int32_t const block_size, int32_t const* __restrict__ batch_num_tokens, int32_t* __restrict__ sorted_ids, int32_t* __restrict__ block_ids, int32_t* __restrict__ num_tokens_post_pad) { - // TODO(varun): This is a naive implementation. Could be optimized. - - size_t const batch_id = threadIdx.x; size_t const stride = blockDim.x * gridDim.x; int32_t const num_blocks_per_batch = CEILDIV(max_tokens_per_batch, block_size); - int32_t const sorted_ids_size = - num_blocks_per_batch * num_batches * block_size; - int32_t const block_ids_size = sorted_ids_size / block_size; + size_t const sorted_ids_size = + static_cast(num_blocks_per_batch) * num_batches * block_size; + size_t const block_ids_size = sorted_ids_size / block_size; int32_t const SENTINEL = num_batches * max_tokens_per_batch; // To denote invalid entries. // Initialize sorted_ids @@ -49,8 +47,8 @@ __global__ void batched_moe_align_block_size_kernel( } int32_t b_num_tokens = 0; - if (batch_id < num_batches) { - b_num_tokens = batch_num_tokens[batch_id]; + if (threadIdx.x < num_batches) { + b_num_tokens = batch_num_tokens[threadIdx.x]; } int32_t const ceil_b_num_tokens = CEILDIV(b_num_tokens, block_size) * block_size; @@ -62,20 +60,56 @@ __global__ void batched_moe_align_block_size_kernel( BlockScan(temp_storage).ExclusiveSum(ceil_b_num_tokens, cumsum_val); __syncthreads(); - bool const is_last_batch = batch_id == (num_batches - 1); - if (is_last_batch) { + if (threadIdx.x == num_batches - 1) { *num_tokens_post_pad = cumsum_val + ceil_b_num_tokens; } - if (batch_id < num_batches) { - int32_t const batch_offset = batch_id * max_tokens_per_batch; + if constexpr (cooperative_writes) { + __shared__ int32_t batch_cumsum[num_threads]; + __shared__ int32_t valid_tokens[num_threads]; + __shared__ int32_t batch_num_blocks[num_threads]; + if (threadIdx.x < num_batches) { + batch_cumsum[threadIdx.x] = cumsum_val; + valid_tokens[threadIdx.x] = b_num_tokens; + batch_num_blocks[threadIdx.x] = ceil_b_num_tokens / block_size; + } + __syncthreads(); + + int32_t const max_num_groups = blockDim.x / WARP_SIZE; + int32_t num_groups = 1; + while (num_groups < num_batches && num_groups < max_num_groups) { + num_groups *= 2; + } + int32_t const threads_per_batch = blockDim.x / num_groups; + int32_t const group_id = threadIdx.x / threads_per_batch; + int32_t const group_offset = threadIdx.x % threads_per_batch; + + // Assign at least one warp to each batch when possible. + for (int32_t batch_id = group_id; batch_id < num_batches; + batch_id += num_groups) { + size_t const batch_offset = + static_cast(batch_id) * max_tokens_per_batch; + size_t const cumsum = batch_cumsum[batch_id]; + for (size_t i = group_offset; i < valid_tokens[batch_id]; + i += threads_per_batch) { + sorted_ids[cumsum + i] = static_cast(batch_offset + i); + } + + size_t const block_start = cumsum / block_size; + for (size_t i = group_offset; i < batch_num_blocks[batch_id]; + i += threads_per_batch) { + block_ids[block_start + i] = batch_id; + } + } + } else if (threadIdx.x < num_batches) { + size_t const batch_id = threadIdx.x; + size_t const batch_offset = batch_id * max_tokens_per_batch; for (size_t i = 0; i < b_num_tokens; ++i) { - sorted_ids[cumsum_val + i] = batch_offset + i; + sorted_ids[cumsum_val + i] = static_cast(batch_offset + i); } - int32_t const block_start = cumsum_val / block_size; - int32_t const num_blocks = ceil_b_num_tokens / block_size; - for (size_t i = 0; i < num_blocks; ++i) { + size_t const block_start = cumsum_val / block_size; + for (size_t i = 0; i < ceil_b_num_tokens / block_size; ++i) { block_ids[block_start + i] = batch_id; } } @@ -202,7 +236,7 @@ __device__ void _moe_align_block_size_small_batch_expert( int32_t* __restrict__ expert_map, int32_t num_experts, int32_t block_size, size_t numel, int32_t max_num_tokens_padded, int32_t max_num_m_blocks, int32_t inactive_expert_id, int32_t model_offset, int32_t topk_num, - int32_t* token_mask, bool has_expert_map) { + int32_t* token_mask, bool has_expert_map, int32_t* scatter_idx = nullptr) { // Compute input buffer offsets. Typically these will all be 0, except when // using Multi LoRA. int sorted_token_ids_offset = max_num_tokens_padded * model_offset; @@ -284,9 +318,14 @@ __device__ void _moe_align_block_size_small_batch_expert( } for (size_t i = tid; i < numel; i += stride) { - if (int expert_id = get_local_expert_id(i, topk_ids, expert_map, - num_experts, has_expert_map); - expert_id != -1) { + int expert_id = get_local_expert_id(i, topk_ids, expert_map, num_experts, + has_expert_map); + bool valid = + expert_id != -1 && (token_mask == nullptr || token_mask[i / topk_num]); + if (scatter_idx != nullptr) { + scatter_idx[i] = valid ? static_cast(i) : -1; + } + if (expert_id != -1) { int32_t rank_post_pad = tokens_cnts[tid * num_experts + expert_id] + cumsum[expert_id]; @@ -304,14 +343,20 @@ __device__ void _count_and_sort_expert_tokens( int32_t* __restrict__ sorted_token_ids, int32_t* __restrict__ cumsum_buffer, int32_t* __restrict__ expert_map, size_t numel, int32_t num_experts, int32_t max_num_tokens_padded, int32_t* __restrict__ token_mask, - int32_t model_offset, int32_t topk_num, bool has_expert_map) { + int32_t model_offset, int32_t topk_num, bool has_expert_map, + int32_t* scatter_idx = nullptr) { const size_t tid = blockIdx.y * blockDim.x + threadIdx.x; const size_t stride = blockDim.x * gridDim.y; for (size_t i = tid; i < numel; i += stride) { - if (int expert_id = get_local_expert_id(i, topk_ids, expert_map, - num_experts, has_expert_map); - expert_id != -1) { + int expert_id = get_local_expert_id(i, topk_ids, expert_map, num_experts, + has_expert_map); + bool valid = + expert_id != -1 && (token_mask == nullptr || token_mask[i / topk_num]); + if (scatter_idx != nullptr) { + scatter_idx[i] = valid ? static_cast(i) : -1; + } + if (expert_id != -1) { if (token_mask == nullptr || token_mask[i / topk_num]) { int32_t rank_post_pad = atomicAdd( &cumsum_buffer[(model_offset * (num_experts + 1)) + expert_id], 1); @@ -343,10 +388,11 @@ __global__ void count_and_sort_expert_tokens_kernel( const scalar_t* __restrict__ topk_ids, int32_t* __restrict__ sorted_token_ids, int32_t* __restrict__ cumsum_buffer, int32_t* __restrict__ expert_map, size_t numel, int32_t num_experts, - int32_t max_num_tokens_padded, int32_t topk_num, bool has_expert_map) { + int32_t max_num_tokens_padded, int32_t topk_num, bool has_expert_map, + int32_t* scatter_idx) { _count_and_sort_expert_tokens( topk_ids, sorted_token_ids, cumsum_buffer, expert_map, numel, num_experts, - max_num_tokens_padded, nullptr, 0, topk_num, has_expert_map); + max_num_tokens_padded, nullptr, 0, topk_num, has_expert_map, scatter_idx); } // Reduce the topk expert outputs per token (summed in fp32). The output is @@ -499,12 +545,12 @@ __global__ void moe_align_block_size_small_batch_expert_kernel( int32_t* __restrict__ total_tokens_post_pad, int32_t* __restrict__ expert_map, int32_t num_experts, int32_t block_size, size_t numel, int32_t max_num_tokens_padded, int32_t topk_num, - bool has_expert_map) { + bool has_expert_map, int32_t* scatter_idx) { _moe_align_block_size_small_batch_expert( topk_ids, sorted_token_ids, expert_ids, total_tokens_post_pad, expert_map, num_experts, block_size, numel, max_num_tokens_padded, CEILDIV(max_num_tokens_padded, block_size), -1, 0, topk_num, nullptr, - has_expert_map); + has_expert_map, scatter_idx); } template @@ -624,16 +670,31 @@ __global__ void moe_lora_align_block_size_small_batch_expert_kernel( // taken from // https://github.com/sgl-project/sglang/blob/8b5f83ed3b7d2a49ad5c5cd5aa61c5d502f47dbc -void moe_align_block_size( - torch::stable::Tensor topk_ids, int64_t num_experts, int64_t block_size, - torch::stable::Tensor sorted_token_ids, torch::stable::Tensor experts_ids, - torch::stable::Tensor num_tokens_post_pad, - std::optional maybe_expert_map) { +void moe_align_block_size(torch::stable::Tensor topk_ids, int64_t num_experts, + int64_t block_size, + torch::stable::Tensor sorted_token_ids, + torch::stable::Tensor experts_ids, + torch::stable::Tensor num_tokens_post_pad, + std::optional maybe_expert_map, + std::optional scatter_idx) { const torch::stable::accelerator::DeviceGuard device_guard( topk_ids.get_device_index()); const cudaStream_t stream = get_current_cuda_stream(topk_ids.get_device_index()); + int32_t* scatter_ptr = nullptr; + if (scatter_idx.has_value()) { + STD_TORCH_CHECK( + scatter_idx->scalar_type() == torch::headeronly::ScalarType::Int, + "scatter_idx must be int32"); + STD_TORCH_CHECK(scatter_idx->device() == topk_ids.device() && + scatter_idx->is_contiguous() && + scatter_idx->numel() == topk_ids.numel(), + "scatter_idx must be contiguous, on the same device, and " + "match topk_ids.numel()"); + scatter_ptr = reinterpret_cast(scatter_idx->mutable_data_ptr()); + } + int64_t padded_num_experts = ((num_experts + WARP_SIZE - 1) / WARP_SIZE) * WARP_SIZE; int experts_per_warp = WARP_SIZE; @@ -679,7 +740,8 @@ void moe_align_block_size( num_tokens_post_pad.mutable_data_ptr()), reinterpret_cast(expert_map.mutable_data_ptr()), num_experts, block_size, topk_ids.numel(), - sorted_token_ids.size(0), topk_ids.size(1), has_expert_map); + sorted_token_ids.size(0), topk_ids.size(1), has_expert_map, + scatter_ptr); } else { torch::stable::Tensor cumsum_buffer = torch::stable::new_empty( topk_ids, {num_experts + 1}, torch::headeronly::ScalarType::Int); @@ -719,7 +781,7 @@ void moe_align_block_size( reinterpret_cast(cumsum_buffer.mutable_data_ptr()), reinterpret_cast(expert_map.mutable_data_ptr()), topk_ids.numel(), num_experts, sorted_token_ids.size(0), - topk_ids.size(1), has_expert_map); + topk_ids.size(1), has_expert_map, scatter_ptr); } }); } @@ -747,8 +809,16 @@ void batched_moe_align_block_size(int64_t max_tokens_per_batch, STD_TORCH_CHECK(num_tokens_post_pad.size(0) == 1); STD_TORCH_CHECK(B <= batched_kernel::num_threads); - batched_kernel::batched_moe_align_block_size_kernel<<< - batched_kernel::num_blocks, batched_kernel::num_threads, 0, stream>>>( + // Avoid coordination overhead for small capacities or many batches. + int64_t const cooperative_threshold = std::max(256, 8 * B); + bool const use_cooperative_writes = + max_tokens_per_batch >= cooperative_threshold; + auto kernel = + use_cooperative_writes + ? batched_kernel::batched_moe_align_block_size_kernel + : batched_kernel::batched_moe_align_block_size_kernel; + kernel<<>>( B, max_tokens_per_batch, block_size, reinterpret_cast(batch_num_tokens.const_data_ptr()), reinterpret_cast(sorted_ids.mutable_data_ptr()), diff --git a/csrc/libtorch_stable/moe/moe_ops.h b/csrc/libtorch_stable/moe/moe_ops.h index 03057c3b1421..549a19e98148 100644 --- a/csrc/libtorch_stable/moe/moe_ops.h +++ b/csrc/libtorch_stable/moe/moe_ops.h @@ -36,11 +36,13 @@ void moe_sum(torch::stable::Tensor& input, torch::stable::Tensor& output, std::optional topk_ids, std::optional expert_map); -void moe_align_block_size( - torch::stable::Tensor topk_ids, int64_t num_experts, int64_t block_size, - torch::stable::Tensor sorted_token_ids, torch::stable::Tensor experts_ids, - torch::stable::Tensor num_tokens_post_pad, - std::optional maybe_expert_map); +void moe_align_block_size(torch::stable::Tensor topk_ids, int64_t num_experts, + int64_t block_size, + torch::stable::Tensor sorted_token_ids, + torch::stable::Tensor experts_ids, + torch::stable::Tensor num_tokens_post_pad, + std::optional maybe_expert_map, + std::optional scatter_idx); void batched_moe_align_block_size( int64_t max_tokens_per_batch, int64_t block_size, diff --git a/csrc/libtorch_stable/moe/moe_permute_unpermute_op.cu b/csrc/libtorch_stable/moe/moe_permute_unpermute_op.cu index 251097456f9e..957cd60e49de 100644 --- a/csrc/libtorch_stable/moe/moe_permute_unpermute_op.cu +++ b/csrc/libtorch_stable/moe/moe_permute_unpermute_op.cu @@ -56,20 +56,18 @@ int64_t moe_permute_sort_workspace_size(int64_t num_expanded_rows, CubKeyValueSorter::getWorkspaceSize(num_expanded_rows, n_expert)); } -void moe_permute_impl( - const torch::stable::Tensor& input, // [n_token, hidden] - const torch::stable::Tensor& topk_ids, // [n_token, topk] - const torch::stable::Tensor& token_expert_indices, // [n_token, topk] +torch::stable::Tensor moe_sort_routing( + const torch::stable::Tensor& topk_ids, // [n_token, topk] + const torch::stable::Tensor& token_expert_indices, // [n_token, topk] const std::optional& expert_map, // [n_expert] int64_t n_expert, int64_t n_local_expert, int64_t topk, - torch::stable::Tensor& permuted_input, // [permuted_size, hidden] torch::stable::Tensor& expert_first_token_offset, // [n_local_expert + 1] torch::stable::Tensor& inv_permuted_idx, // [n_token, topk] - torch::stable::Tensor& permuted_idx, // [permute_size] const std::optional& maybe_sort_workspace, const std::optional& maybe_permuted_experts_id, const std::optional& maybe_sorted_row_idx, - const std::optional& maybe_topk_ids_for_sort) { + const std::optional& maybe_topk_ids_for_sort, + bool build_inverse) { STD_TORCH_CHECK(expert_first_token_offset.scalar_type() == torch::headeronly::ScalarType::Long, "expert_first_token_offset must be int64"); @@ -88,12 +86,11 @@ void moe_permute_impl( "token_expert_indices shape must be same as inv_permuted_idx"); const torch::stable::accelerator::DeviceGuard device_guard( - input.get_device_index()); - auto device = input.device(); - auto n_token = input.sizes()[0]; - auto n_hidden = input.sizes()[1]; + topk_ids.get_device_index()); + auto device = topk_ids.device(); + auto n_token = topk_ids.sizes()[0]; auto expanded_rows = n_token * topk; - auto stream = get_current_cuda_stream(input.get_device_index()); + auto stream = get_current_cuda_stream(topk_ids.get_device_index()); auto sorter_size = moe_permute_sort_workspace_size(expanded_rows, n_expert); auto sort_workspace = maybe_allocate_tensor( @@ -107,13 +104,11 @@ void moe_permute_impl( torch::headeronly::ScalarType::Int, device, "sorted_row_idx"); CubKeyValueSorter sorter{}; - int64_t* valid_num_ptr = nullptr; torch::stable::Tensor topk_ids_for_sort = topk_ids; - if (expert_map.has_value()) { - const int* expert_map_ptr = get_ptr(expert_map.value()); - valid_num_ptr = - get_ptr(expert_first_token_offset) + n_local_expert; + if (expert_map.has_value() || build_inverse) { + const int* expert_map_ptr = + expert_map.has_value() ? get_ptr(expert_map.value()) : nullptr; topk_ids_for_sort = maybe_allocate_tensor( maybe_topk_ids_for_sort, topk_ids.sizes(), torch::headeronly::ScalarType::Int, device, "topk_ids_for_sort"); @@ -126,7 +121,39 @@ void moe_permute_impl( get_ptr(topk_ids_for_sort), get_ptr(token_expert_indices), get_ptr(permuted_experts_id), get_ptr(sorted_row_idx), get_ptr(expert_first_token_offset), n_token, n_expert, - n_local_expert, topk, sorter, get_ptr(sort_workspace), stream); + n_local_expert, topk, sorter, get_ptr(sort_workspace), stream, + build_inverse ? get_ptr(inv_permuted_idx) : nullptr); + return sorted_row_idx; +} + +void moe_permute_impl( + const torch::stable::Tensor& input, // [n_token, hidden] + const torch::stable::Tensor& topk_ids, // [n_token, topk] + const torch::stable::Tensor& token_expert_indices, // [n_token, topk] + const std::optional& expert_map, // [n_expert] + int64_t n_expert, int64_t n_local_expert, int64_t topk, + torch::stable::Tensor& permuted_input, // [permuted_size, hidden] + torch::stable::Tensor& expert_first_token_offset, // [n_local_expert + 1] + torch::stable::Tensor& inv_permuted_idx, // [n_token, topk] + torch::stable::Tensor& permuted_idx, // [permute_size] + const std::optional& maybe_sort_workspace, + const std::optional& maybe_permuted_experts_id, + const std::optional& maybe_sorted_row_idx, + const std::optional& maybe_topk_ids_for_sort) { + const torch::stable::accelerator::DeviceGuard device_guard( + input.get_device_index()); + auto sorted_row_idx = moe_sort_routing( + topk_ids, token_expert_indices, expert_map, n_expert, n_local_expert, + topk, expert_first_token_offset, inv_permuted_idx, maybe_sort_workspace, + maybe_permuted_experts_id, maybe_sorted_row_idx, maybe_topk_ids_for_sort, + false); + auto stream = get_current_cuda_stream(input.get_device_index()); + auto n_token = input.size(0); + auto n_hidden = input.size(1); + auto valid_num_ptr = + expert_map.has_value() + ? get_ptr(expert_first_token_offset) + n_local_expert + : nullptr; MOE_DISPATCH(input.scalar_type(), [&] { expandInputRowsKernelLauncher( @@ -171,6 +198,22 @@ void moe_permute_with_scratch( topk_ids_for_sort); } +void moe_prepare_scatter(const torch::stable::Tensor& topk_ids, + const torch::stable::Tensor& token_expert_indices, + const std::optional& expert_map, + int64_t n_expert, int64_t n_local_expert, + torch::stable::Tensor& expert_first_token_offset, + torch::stable::Tensor& scatter_idx, + torch::stable::Tensor& sort_workspace, + torch::stable::Tensor& sorted_experts, + torch::stable::Tensor& sorted_rows, + torch::stable::Tensor& topk_ids_for_sort) { + moe_sort_routing(topk_ids, token_expert_indices, expert_map, n_expert, + n_local_expert, topk_ids.size(1), expert_first_token_offset, + scatter_idx, sort_workspace, sorted_experts, sorted_rows, + topk_ids_for_sort, true); +} + void moe_unpermute( const torch::stable::Tensor& permuted_hidden_states, // [n_token * topk, hidden] @@ -315,6 +358,19 @@ void moe_unpermute( STD_TORCH_CHECK(false, "moe_unpermute is not supported on CUDA < 12.0"); } +void moe_prepare_scatter(const torch::stable::Tensor& topk_ids, + const torch::stable::Tensor& token_expert_indices, + const std::optional& expert_map, + int64_t n_expert, int64_t n_local_expert, + torch::stable::Tensor& expert_first_token_offset, + torch::stable::Tensor& scatter_idx, + torch::stable::Tensor& sort_workspace, + torch::stable::Tensor& sorted_experts, + torch::stable::Tensor& sorted_rows, + torch::stable::Tensor& topk_ids_for_sort) { + STD_TORCH_CHECK(false, "moe_prepare_scatter is not supported on CUDA < 12.0"); +} + #endif bool moe_permute_unpermute_supported() { @@ -328,5 +384,6 @@ bool moe_permute_unpermute_supported() { STABLE_TORCH_LIBRARY_IMPL(_moe_C, CUDA, m) { m.impl("moe_permute", TORCH_BOX(&moe_permute)); m.impl("moe_permute_with_scratch", TORCH_BOX(&moe_permute_with_scratch)); + m.impl("moe_prepare_scatter", TORCH_BOX(&moe_prepare_scatter)); m.impl("moe_unpermute", TORCH_BOX(&moe_unpermute)); } diff --git a/csrc/libtorch_stable/moe/moe_wna16_utils.h b/csrc/libtorch_stable/moe/moe_wna16_utils.h index 8ef03f0e6052..6ce52ee5b1d8 100644 --- a/csrc/libtorch_stable/moe/moe_wna16_utils.h +++ b/csrc/libtorch_stable/moe/moe_wna16_utils.h @@ -34,10 +34,6 @@ class ScalarType { static __host__ __device__ float2 inline num22float2(const half2 x) { return __half22float2(x); } - - static __host__ __device__ half2 inline float22num2(const float2 x) { - return __float22half2_rn(x); - } }; template <> @@ -71,10 +67,6 @@ class ScalarType { static __host__ __device__ float2 inline num22float2(const nv_bfloat162 x) { return __bfloat1622float2(x); } - - static __host__ __device__ nv_bfloat162 inline float22num2(const float2 x) { - return __float22bfloat162_rn(x); - } #endif }; diff --git a/csrc/libtorch_stable/moe/permute_unpermute_kernels/moe_permute_unpermute_kernel.cu b/csrc/libtorch_stable/moe/permute_unpermute_kernels/moe_permute_unpermute_kernel.cu index ec7851da3d3d..8d8bdf8baaf6 100644 --- a/csrc/libtorch_stable/moe/permute_unpermute_kernels/moe_permute_unpermute_kernel.cu +++ b/csrc/libtorch_stable/moe/permute_unpermute_kernels/moe_permute_unpermute_kernel.cu @@ -82,34 +82,40 @@ __device__ inline int64_t findTotalEltsLessThanTarget(T const* sorted_indices, return target_location + 1; } -// Calculates the start offset of the tokens for a given expert. The last -// element is the total number of valid tokens -__global__ void computeExpertFirstTokenOffsetKernel( +// Computes expert offsets (ending with the valid row count) and optional +// inverse indices. +__global__ void computeExpertOffsetsAndInverseKernel( int const* sorted_experts, int64_t const sorted_experts_len, - int const num_experts, int64_t* expert_first_token_offset) { - // First, compute the global tid. We only need 1 thread per expert. - int const expert = blockIdx.x * blockDim.x + threadIdx.x; + int const num_experts, int64_t* expert_first_token_offset, + int const* sorted_rows, int* inverse) { + int const index = blockIdx.x * blockDim.x + threadIdx.x; // Note that expert goes [0, num_experts] (inclusive) because we want a count // for the total number of active tokens at the end of the scan. - if (expert >= num_experts + 1) { - return; + if (index < num_experts + 1) { + expert_first_token_offset[index] = + findTotalEltsLessThanTarget(sorted_experts, sorted_experts_len, index); + } + if (inverse != nullptr && index < sorted_experts_len) { + inverse[sorted_rows[index]] = index; } - expert_first_token_offset[expert] = - findTotalEltsLessThanTarget(sorted_experts, sorted_experts_len, expert); } -void computeExpertFirstTokenOffset(int const* sorted_indices, - int const total_indices, - int const num_experts, - int64_t* expert_first_token_offset, - cudaStream_t stream) { - int const num_entries = num_experts + 1; - int const threads = std::min(1024, num_entries); +void computeExpertOffsetsAndInverse(int const* sorted_indices, + int const total_indices, + int const num_experts, + int64_t* expert_first_token_offset, + int const* sorted_rows, int* inverse, + cudaStream_t stream) { + int const num_entries = inverse != nullptr + ? std::max(num_experts + 1, total_indices) + : num_experts + 1; + int const threads = inverse != nullptr ? 256 : std::min(1024, num_entries); int const blocks = (num_entries + threads - 1) / threads; - computeExpertFirstTokenOffsetKernel<<>>( - sorted_indices, total_indices, num_experts, expert_first_token_offset); + computeExpertOffsetsAndInverseKernel<<>>( + sorted_indices, total_indices, num_experts, expert_first_token_offset, + sorted_rows, inverse); } void sortAndScanExpert(const int* expert_for_source_row, const int* source_rows, @@ -117,7 +123,7 @@ void sortAndScanExpert(const int* expert_for_source_row, const int* source_rows, int64_t* expert_first_token_offset, int num_rows, int num_experts, int num_experts_per_node, int k, CubKeyValueSorter& sorter, void* sorter_ws, - cudaStream_t stream) { + cudaStream_t stream, int* inverse) { int64_t const expanded_num_rows = static_cast(k) * num_rows; // We need to use the full num_experts because that is the sentinel value used // by topk for disabled experts @@ -127,9 +133,9 @@ void sortAndScanExpert(const int* expert_for_source_row, const int* source_rows, sorter.run((void*)sorter_ws, sorter_ws_size_bytes, expert_for_source_row, permuted_experts, source_rows, permuted_rows, expanded_num_rows, stream); - computeExpertFirstTokenOffset(permuted_experts, expanded_num_rows, - num_experts_per_node, expert_first_token_offset, - stream); + computeExpertOffsetsAndInverse( + permuted_experts, expanded_num_rows, num_experts_per_node, + expert_first_token_offset, permuted_rows, inverse, stream); } __global__ void preprocessTopkIdKernel(int* topk_id_ptr, int size, @@ -142,7 +148,7 @@ __global__ void preprocessTopkIdKernel(int* topk_id_ptr, int size, extern __shared__ int smem_expert_map[]; // store expert_map in smem for (int i = tidx; i < num_experts; i += blockDim.x) { - smem_expert_map[i] = expert_map_ptr[i]; + smem_expert_map[i] = expert_map_ptr ? expert_map_ptr[i] : i; } __syncthreads(); diff --git a/csrc/libtorch_stable/moe/permute_unpermute_kernels/moe_permute_unpermute_kernel.h b/csrc/libtorch_stable/moe/permute_unpermute_kernels/moe_permute_unpermute_kernel.h index 89c278a4ed4b..53afad2f5d41 100644 --- a/csrc/libtorch_stable/moe/permute_unpermute_kernels/moe_permute_unpermute_kernel.h +++ b/csrc/libtorch_stable/moe/permute_unpermute_kernels/moe_permute_unpermute_kernel.h @@ -43,18 +43,19 @@ class CubKeyValueSorter { int num_bits_; }; -void computeExpertFirstTokenOffset(int const* sorted_indices, - int const total_indices, - int const num_experts, - int64_t* expert_first_token_offset, - cudaStream_t stream); +void computeExpertOffsetsAndInverse(int const* sorted_indices, + int const total_indices, + int const num_experts, + int64_t* expert_first_token_offset, + int const* sorted_rows, int* inverse, + cudaStream_t stream); void sortAndScanExpert(const int* expert_for_source_row, const int* source_rows, int* permuted_experts, int* permuted_rows, int64_t* expert_first_token_offset, int num_rows, int num_experts, int num_experts_per_node, int k, CubKeyValueSorter& sorter, void* sorter_ws, - cudaStream_t stream); + cudaStream_t stream, int* inverse = nullptr); template void expandInputRowsKernelLauncher( diff --git a/csrc/libtorch_stable/moe/torch_bindings.cpp b/csrc/libtorch_stable/moe/torch_bindings.cpp index c0e966b2be9c..2feec3838993 100644 --- a/csrc/libtorch_stable/moe/torch_bindings.cpp +++ b/csrc/libtorch_stable/moe/torch_bindings.cpp @@ -39,7 +39,8 @@ STABLE_TORCH_LIBRARY_FRAGMENT(_moe_C, m) { " int block_size, Tensor! sorted_token_ids," " Tensor! experts_ids," " Tensor! num_tokens_post_pad," - " Tensor? maybe_expert_map) -> ()"); + " Tensor? maybe_expert_map," + " Tensor(a!)? scatter_idx=None) -> ()"); // Aligning the number of tokens to be processed by each expert such // that it is divisible by the block size, but for the batched case. @@ -105,6 +106,13 @@ STABLE_TORCH_LIBRARY_FRAGMENT(_moe_C, m) { "permuted_idx, Tensor! sort_workspace, Tensor! permuted_experts_id, " "Tensor! sorted_row_idx, Tensor! topk_ids_for_sort)->()"); + m.def( + "moe_prepare_scatter(Tensor topk_ids, Tensor token_expert_indices, " + "Tensor? expert_map, int n_expert, int n_local_expert, " + "Tensor! expert_first_token_offset, Tensor! scatter_idx, " + "Tensor! sort_workspace, Tensor! sorted_experts, Tensor! sorted_rows, " + "Tensor! topk_ids_for_sort)->()"); + m.def( "moe_unpermute(Tensor permuted_hidden_states, Tensor topk_weights," "Tensor inv_permuted_idx, Tensor? expert_first_token_offset, " diff --git a/csrc/libtorch_stable/nvfp4_ds_mla_cache_kernels.cu b/csrc/libtorch_stable/nvfp4_ds_mla_cache_kernels.cu index c2faf7b76023..b1f369e5ab00 100644 --- a/csrc/libtorch_stable/nvfp4_ds_mla_cache_kernels.cu +++ b/csrc/libtorch_stable/nvfp4_ds_mla_cache_kernels.cu @@ -109,9 +109,6 @@ __device__ __forceinline__ void nvfp4_unpack16_e2m1(uint64_t raw, #endif } -// Rounded UP to the next e4m3 value so amax / sf never exceeds the payload -// range: round-to-nearest can land up to 33% low near e4m3's 2^-9 floor, which -// would saturate the tile's largest values. __device__ __forceinline__ float nvfp4_tile_scale(const float* vals, float max_val, uint8_t* sf_out) { @@ -121,15 +118,8 @@ __device__ __forceinline__ float nvfp4_tile_scale(const float* vals, amax = fmaxf(amax, fabsf(vals[i])); } const float sf_f = fmaxf(amax / max_val, 0.001953125f); // 2^-9 - __nv_fp8_e4m3 sf8(sf_f); - uint8_t sf_bits = *reinterpret_cast(&sf8); - if (float(sf8) < sf_f && sf_bits < 0x7E) { - // Bump to the next e4m3 value (bit patterns of positive e4m3 values are - // monotonic; 0x7E = 448 is the max finite value). - sf_bits += 1; - sf8 = *reinterpret_cast(&sf_bits); - } - *sf_out = sf_bits; + const __nv_fp8_e4m3 sf8(sf_f); + *sf_out = *reinterpret_cast(&sf8); return float(sf8); } diff --git a/csrc/libtorch_stable/ops.h b/csrc/libtorch_stable/ops.h index a8beef5cda3d..331cd1fbb87f 100644 --- a/csrc/libtorch_stable/ops.h +++ b/csrc/libtorch_stable/ops.h @@ -175,9 +175,6 @@ torch::stable::Tensor awq_dequantize(torch::stable::Tensor _kernel, // DSV3 fused A GEMM: conditionally compiled so declaration and impl // registration are in the source file (dsv3_fused_a_gemm.cu) -// AllSpark ops: declarations are in the source files -// (allspark_repack.cu and allspark_qgemm_w8a16.cu) - #endif // CPU tensor -> CUDA UVA view (shared CUDA/ROCm) @@ -259,12 +256,20 @@ void fused_qk_norm_rope(torch::stable::Tensor& qkv, int64_t num_heads_q, torch::stable::Tensor& position_ids, int64_t forced_token_heads_per_warp); +void fused_deepseek_v4_kv_rope_insert( + torch::stable::Tensor const& kv, torch::stable::Tensor& k_cache, + torch::stable::Tensor const& slot_mapping, + torch::stable::Tensor const& position_ids, + torch::stable::Tensor const& cos_sin_cache, int64_t cache_block_size, + std::optional fp8_scale, bool kv_mxfp8); + torch::stable::Tensor fused_deepseek_v4_qnorm_rope_kv_rope_quant_insert( torch::stable::Tensor const& q_in, torch::stable::Tensor const& kv, torch::stable::Tensor& k_cache, torch::stable::Tensor const& slot_mapping, torch::stable::Tensor const& position_ids, torch::stable::Tensor const& cos_sin_cache, int64_t q_head_padded, - double eps, int64_t cache_block_size, bool apply_q_norm); + double eps, int64_t cache_block_size, bool apply_q_norm, bool kv_mxfp8, + bool apply_q_rope, bool is_q_interleaved); void fused_deepseek_v4_qnorm_rope_kv_rope_full_cache_bf16_insert( torch::stable::Tensor& q, torch::stable::Tensor const& kv, @@ -654,12 +659,61 @@ void concat_and_cache_mla(torch::stable::Tensor& kv_c, const std::string& kv_cache_dtype, torch::stable::Tensor& scale); -void concat_and_cache_mla_grouped(torch::stable::Tensor& kv_c, - torch::stable::Tensor& k_pe, - torch::stable::Tensor& kv_cache_ptrs, - torch::stable::Tensor& slot_mapping, - int64_t block_size, int64_t block_stride, - int64_t entry_stride); +void concat_and_cache_mla_grouped( + torch::stable::Tensor& kv_c, torch::stable::Tensor& k_pe, + torch::stable::Tensor& kv_cache_ptrs, torch::stable::Tensor& slot_mapping, + int64_t block_size, int64_t block_stride, int64_t entry_stride, + std::optional kv_scales, + const std::string& kv_cache_dtype); + +#ifndef USE_ROCM +// HiSparse kernels use raw PTX in the row copy; CUDA-only. +void hisparse_resolve_residency( + torch::stable::Tensor const& host_cache, torch::stable::Tensor& hot_cache, + torch::stable::Tensor const& hot_block_table, + torch::stable::Tensor const& global_indices, + torch::stable::Tensor& hot_indices, + torch::stable::Tensor& device_global_indices, + torch::stable::Tensor& lru_slots, + std::optional const& request_state_indices, + int64_t region_stride, + std::optional const& miss_mask, + std::optional const& stats, + std::optional const& attention_indices, + int64_t attention_block_stride, + std::optional const& request_ids, + std::optional const& source_block_table, + int64_t source_block_size, + std::optional const& resolved_global_indices, + std::optional const& valid_counts, + std::optional const& swap_host_physical_rows, + 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); + +void hisparse_invalidate_written_slots( + torch::stable::Tensor& device_global_indices, + torch::stable::Tensor const& request_state_indices, + torch::stable::Tensor const& req_id_per_token, + torch::stable::Tensor const& written_slots); + +void hisparse_gather_plan( + torch::stable::Tensor const& host_cache, torch::stable::Tensor& hot_cache, + torch::stable::Tensor const& global_indices, + torch::stable::Tensor const& hot_indices, + torch::stable::Tensor const& miss_mask, + std::optional const& request_state_indices, + std::optional const& attention_indices, + int64_t attention_block_stride); + +void hisparse_gather_compact(torch::stable::Tensor const& host_cache, + torch::stable::Tensor& hot_cache, + torch::stable::Tensor const& miss_global_indices, + torch::stable::Tensor const& miss_hot_indices, + torch::stable::Tensor const& miss_counts); + +#endif // !USE_ROCM // NOTE: k_pe and kv_c order is flipped compared to concat_and_cache_mla void concat_and_cache_mla_rope_fused( @@ -704,7 +758,10 @@ void cp_gather_and_upconvert_fp8_kv_cache( torch::stable::Tensor const& block_table, // [BATCH, BLOCK_INDICES] torch::stable::Tensor const& workspace_starts, // [BATCH] int64_t batch_size, - std::optional seq_starts = std::nullopt); + std::optional seq_starts = std::nullopt, + std::optional host_cache = std::nullopt, + std::optional host_row_ids = std::nullopt, + std::optional device_row_ids = std::nullopt); // Gather and upconvert an nvfp4_ds_mla KV cache to a BF16 workspace void cp_gather_and_upconvert_nvfp4_kv_cache( diff --git a/csrc/libtorch_stable/persistent_topk.cuh b/csrc/libtorch_stable/persistent_topk.cuh index 9ae0fd4bcbaa..bf5b46ab1875 100644 --- a/csrc/libtorch_stable/persistent_topk.cuh +++ b/csrc/libtorch_stable/persistent_topk.cuh @@ -1030,41 +1030,34 @@ constexpr uint32_t FILTERED_TOPK_SMEM_INPUT_SIZE = constexpr size_t FILTERED_TOPK_SMEM_DYNAMIC = sizeof(int) * 2 * FILTERED_TOPK_SMEM_INPUT_SIZE; // 128KB -/*! - * \brief Filtered Top-K kernel for ragged sequences. - * - * \tparam DType Data type (float, half, nv_bfloat16) - * \tparam IdType Index type (int32_t) - * \tparam VEC_SIZE Vector size for input loads (1, 2, 4, or 8) - */ -template -__global__ void __launch_bounds__(FILTERED_TOPK_BLOCK_THREADS) - FilteredTopKUnifiedKernel(const DType* __restrict__ input, - IdType* __restrict__ output, - const IdType* __restrict__ lengths, - uint32_t num_rows, uint32_t top_k, - uint32_t max_len) { +template +struct FilteredTopKStorage { + alignas(128) int histogram[2][256 + 128]; + alignas(128) int counter; + alignas(128) int threshold_bin; + alignas(128) int num_input[2]; + alignas(128) int indices[MAX_K]; + int last_remain; +}; + +// With CheckOverflow, return false before using a truncated stash so the caller +// can retry with exact full-row selection. +template +__device__ __forceinline__ bool filtered_topk_row( + const DType* score, IdType* dst, int length, uint32_t top_k, + FilteredTopKStorage& storage) { constexpr uint32_t BLOCK_SIZE = FILTERED_TOPK_BLOCK_THREADS; constexpr int RADIX = 256; constexpr int SMEM_INPUT_SIZE = FILTERED_TOPK_SMEM_INPUT_SIZE; - - const uint32_t bid = blockIdx.x; const int tx = threadIdx.x; - if (bid >= num_rows) return; - - const int length = - (lengths != nullptr) ? lengths[bid] : static_cast(max_len); - const DType* score = input + bid * max_len; - IdType* dst = output + bid * top_k; - // Trivial case: length <= top_k if (length <= static_cast(top_k)) { for (int i = tx; i < static_cast(top_k); i += BLOCK_SIZE) { dst[i] = (i < length) ? static_cast(i) : static_cast(-1); } - return; + return true; } // Short path @@ -1077,16 +1070,14 @@ __global__ void __launch_bounds__(FILTERED_TOPK_BLOCK_THREADS) hist4096::histogram_4096_topk(score, dst, length, _smem_reg); } - return; + return true; } - // Static shared memory - alignas(128) __shared__ int s_histogram_buf[2][RADIX + 128]; - alignas(128) __shared__ int s_counter; - alignas(128) __shared__ int s_threshold_bin_id; - alignas(128) __shared__ int s_num_input[2]; - alignas(128) __shared__ int s_indices[MAX_K]; - + auto& s_histogram_buf = storage.histogram; + auto& s_counter = storage.counter; + auto& s_threshold_bin_id = storage.threshold_bin; + auto& s_num_input = storage.num_input; + auto& s_indices = storage.indices; auto& s_histogram = s_histogram_buf[0]; // Dynamic shared memory for input double buffer @@ -1213,10 +1204,13 @@ __global__ void __launch_bounds__(FILTERED_TOPK_BLOCK_THREADS) // Stage 2: refine with 8bit radix passes #pragma unroll for (int round = 0; round < NUM_ROUNDS; ++round) { - __shared__ int s_last_remain; + auto& s_last_remain = storage.last_remain; const auto r_idx = round % 2; const auto _raw_num_input = s_num_input[r_idx]; + if constexpr (CheckOverflow) { + if (_raw_num_input > SMEM_INPUT_SIZE) return false; + } const auto num_input = (_raw_num_input < SMEM_INPUT_SIZE) ? _raw_num_input : SMEM_INPUT_SIZE; @@ -1284,6 +1278,30 @@ __global__ void __launch_bounds__(FILTERED_TOPK_BLOCK_THREADS) const int idx = s_indices[base]; dst[base] = static_cast(idx); } + return true; +} + +/*! + * \brief Filtered Top-K kernel for ragged sequences. + * + * \tparam DType Data type (float, half, nv_bfloat16) + * \tparam IdType Index type (int32_t) + * \tparam VEC_SIZE Vector size for input loads (1, 2, 4, or 8) + */ +template +__global__ void __launch_bounds__(FILTERED_TOPK_BLOCK_THREADS) + FilteredTopKUnifiedKernel(const DType* __restrict__ input, + IdType* __restrict__ output, + const IdType* __restrict__ lengths, + uint32_t num_rows, uint32_t top_k, + uint32_t max_len) { + const uint32_t bid = blockIdx.x; + if (bid >= num_rows) return; + const int length = lengths ? lengths[bid] : static_cast(max_len); + __shared__ FilteredTopKStorage storage; + filtered_topk_row( + input + bid * max_len, output + bid * top_k, length, top_k, storage); } // Helper to compute GCD for VEC_SIZE selection diff --git a/csrc/libtorch_stable/quantization/gptq/matrix_view.cuh b/csrc/libtorch_stable/quantization/gptq/matrix_view.cuh index 3d82e4393166..abe3368cb16f 100644 --- a/csrc/libtorch_stable/quantization/gptq/matrix_view.cuh +++ b/csrc/libtorch_stable/quantization/gptq/matrix_view.cuh @@ -74,18 +74,9 @@ class MatrixView_half_rw { const int width) : data(data), height(height), width(width) {} - __device__ __forceinline__ half item(int row, int column) const { - return data[row * width + column]; - } __device__ __forceinline__ half* item_ptr(int row, int column) { return &data[row * width + column]; } - __device__ __forceinline__ void set(int row, int column, half value) { - data[row * width + column] = value; - } - __device__ __forceinline__ void set_half2(int row, int column, half2 value) { - ((half2*)data)[(row * width + column) / 2] = value; - } __device__ __forceinline__ void set4(int row, int column, half v0, half v1, half v2, half v3) { @@ -113,14 +104,6 @@ class MatrixView_q4_row { return (data[row * width / 8 + column / 8] >> shift) & 0x0f; } - __device__ __forceinline__ void item2(int (&items)[2], int row, - int column) const { - int shift = (column & 0x07) * 4; - uint32_t d = data[row * width / 8 + column / 8] >> shift; - items[0] = d & 0x0f; - items[1] = (d >> 4) & 0x0f; - } - __device__ __forceinline__ void item4(int (&items)[4], int row, int column) const { int shift = (column & 0x07) * 4; @@ -148,14 +131,6 @@ class MatrixView_q2_row { return (data[row * width / 16 + column / 16] >> shift) & 0x03; } - __device__ __forceinline__ void item2(int (&items)[2], int row, - int column) const { - int shift = (column & 0x0f) * 2; - uint32_t d = data[row * width / 16 + column / 16] >> shift; - items[0] = d & 0x03; - items[1] = (d >> 2) & 0x03; - } - __device__ __forceinline__ void item4(int (&items)[4], int row, int column) const { int shift = (column & 0x0f) * 2; @@ -237,14 +212,6 @@ class MatrixView_q8_row { return (data[row * width / 4 + column / 4] >> shift) & 0xff; } - __device__ __forceinline__ void item2(int (&items)[2], int row, - int column) const { - int shift = (column & 0x03) * 8; - uint32_t d = data[row * width / 4 + column / 4] >> shift; - items[0] = d & 0xff; - items[1] = (d >> 8) & 0xff; - } - __device__ __forceinline__ void item4(int (&items)[4], int row, int column) const { int shift = (column & 0x03) * 2; diff --git a/csrc/libtorch_stable/quantization/gptq_allspark/allspark_qgemm_w8a16.cu b/csrc/libtorch_stable/quantization/gptq_allspark/allspark_qgemm_w8a16.cu deleted file mode 100644 index 96dc3ecfc860..000000000000 --- a/csrc/libtorch_stable/quantization/gptq_allspark/allspark_qgemm_w8a16.cu +++ /dev/null @@ -1,1024 +0,0 @@ -#include "allspark_utils.cuh" - -#include -#include -#include -#include - -#include - -#include "core/registration.h" -#include "libtorch_stable/torch_utils.h" - -torch::stable::Tensor as_g_workspace; - -#if defined(__CUDA_ARCH__) && __CUDA_ARCH__ < 800 - -torch::stable::Tensor allspark_w8a16_gemm( - torch::stable::Tensor const& a, torch::stable::Tensor const& b_qweight, - torch::stable::Tensor const& b_scales, - std::optional const& b_qzeros, int64_t n, - int64_t group_size, int64_t sm_count, int64_t sm_version, - int64_t CUBLAS_M_THRESHOLD, bool has_zp, bool n32k16_reorder) { - STD_TORCH_CHECK_NOT_IMPLEMENTED( - false, "allspark_w8a16_gemm(..) requires CUDA_ARCH >= 8.0"); - return torch::stable::empty({1, 1}); -} - -#else -namespace allspark { -/* - * GemmTile manage data movement from Global Memory to Shared Memory - * requiring N % 8 == 0, K % 16 == 0 by loading uint - * BN is obtained by padding the original N to a multiple of 32 - * weight B is rearranged as N32K16 order, - * i.e. a initial data block of size 32(n)x16(k) is reordered as n8k4n4k4, - * in order to put data loaded by the same thread of 32x16 data block together - * continuously (see - * https://docs.nvidia.com/cuda/parallel-thread-execution/index.html#matrix-fragments-for-mma-m16n8k16-with-floating-point-type) - */ -template -struct GmemTile_W8A16_PerC_MtilexNtilex32_multistage_SM8x_SplitK { - // element num loaded by a LDG inst. - static constexpr int LDG_ELEMENT_CNT_A = 8; - static constexpr int LDG_ELEMENT_CNT_B = 16; - static constexpr int WARP_SIZE = 32; - static constexpr int M_SIZE_ONE_LOAD = (BLOCK * LDG_ELEMENT_CNT_A) / 32; - static constexpr int N_SIZE_ONE_LOAD = (BLOCK * LDG_ELEMENT_CNT_B) / 32; - - __device__ GmemTile_W8A16_PerC_MtilexNtilex32_multistage_SM8x_SplitK( - const SM8x_GEMM_W8A16_Splitk_Params& k_params, - const uint32_t& A_smem_addr, const uint32_t& BQ_smem_addr, - const uint32_t& A_stage_stride, const uint32_t& BQ_stage_stride) - : params(k_params), - A_smem_base_addr(A_smem_addr), - BQ_smem_base_addr(BQ_smem_addr), - A_smem_stage_stride(A_stage_stride), - BQ_smem_stage_stride(BQ_stage_stride) { - this_block_A_base_ptr = params.A_ptr + blockIdx.x * Mtile * params.K + - blockIdx.z * params.SplitK; - // here B is rearranged as N32K16 order, i.e. 4 continuous N-direction - // 8(N)x16(K) size data blocks are packed together - this_block_B_base_ptr = params.B_ptr + blockIdx.y * Ntile * params.K + - blockIdx.z * params.SplitK * 4; - - const auto lane_id = threadIdx.x % WARP_SIZE; - - // For matrix A, a block load/store Mtile(row) x 32(col) elements in - // multiple iters, 8x4 warp load/store 8(row) x 32(col) elements per iter - const auto Aldg_row_base_idx = threadIdx.x / 4; - Aldg_col_idx = (threadIdx.x % 4) * LDG_ELEMENT_CNT_A; - const int Aldg_base_offset = Aldg_row_base_idx * params.K + Aldg_col_idx; - - // For matrix B, a block load/store elements of (Ntile / 4) row x 128 col - // elements of N32K16 packing in multiple iters, 4x8 warp load/store 4(row) - // * 128(col) per iter - Bldg_col_idx = (threadIdx.x % 8) * LDG_ELEMENT_CNT_B; - const auto Bldg_row_base_idx = threadIdx.x / 8; - const int Bldg_base_offset = - Bldg_row_base_idx * params.K * 4 + Bldg_col_idx; - - this_block_A_base_ptr += Aldg_base_offset; - this_block_B_base_ptr += Bldg_base_offset; - - const int sts_a_base_offset = - (threadIdx.x / 4) * 32 + - ((lane_id % 4) ^ ((lane_id / 4) % 4) ^ ((lane_id / 4) / 4)) * - LDG_ELEMENT_CNT_A; - const int sts_bq_base_offset = - Bldg_row_base_idx * 32 * 4 + - ((threadIdx.x % 8) ^ (((threadIdx.x / 8) % 2) * 4)) * LDG_ELEMENT_CNT_B; - - A_smem_base_addr += sts_a_base_offset * sizeof(FType); - BQ_smem_base_addr += sts_bq_base_offset * sizeof(uint8_t); - - A_ldg_guard = 0; - B_ldg_guard = 0; - #pragma unroll - for (int i = 0; i < (Mtile + M_SIZE_ONE_LOAD - 1) / M_SIZE_ONE_LOAD; ++i) { - auto m_idx = blockIdx.x * Mtile + Aldg_row_base_idx + i * M_SIZE_ONE_LOAD; - if (m_idx < params.M) { - A_ldg_guard |= (1u << i); - } - } - - const int N_padded = (params.N + 31) / 32 * 32; - #pragma unroll - for (int i = 0; i < (Ntile + N_SIZE_ONE_LOAD - 1) / N_SIZE_ONE_LOAD; ++i) { - auto n_idx = blockIdx.y * Ntile + (Bldg_row_base_idx / 8) * 32 + - i * N_SIZE_ONE_LOAD; - if (n_idx < N_padded) { - B_ldg_guard |= (1u << i); - } - } - } - - __device__ void ldgsts_first_ktiles(const int& first_k_tile, - const int& k_tiles) { - // load first k_tile - // load A - const int A_src_size = Aldg_col_idx < first_k_tile ? 16 : 0; - #pragma unroll - for (int i = 0; i < (Mtile + M_SIZE_ONE_LOAD - 1) / M_SIZE_ONE_LOAD; ++i) { - cp_async<16>( - A_smem_base_addr + (i * M_SIZE_ONE_LOAD * 32) * sizeof(FType), - this_block_A_base_ptr + i * M_SIZE_ONE_LOAD * params.K, A_src_size, - (A_ldg_guard & (1u << i)) != 0); - } - - // load B - const int B_src_size = (Bldg_col_idx / 4) < first_k_tile ? 16 : 0; - #pragma unroll - for (int i = 0; i < (Ntile + N_SIZE_ONE_LOAD - 1) / N_SIZE_ONE_LOAD; ++i) { - cp_async<16>( - BQ_smem_base_addr + (i * N_SIZE_ONE_LOAD * 32) * sizeof(uint8_t), - this_block_B_base_ptr + i * N_SIZE_ONE_LOAD * params.K, B_src_size, - (B_ldg_guard & (1u << i)) != 0); - } - - cp_async_commit_group(); - this_block_A_base_ptr += first_k_tile; - this_block_B_base_ptr += (first_k_tile * 4); - - // load second to (N-stage - 1) k_tiles - for (int stage_idx = 1; stage_idx < NStage - 1; ++stage_idx) { - if (stage_idx < k_tiles) { - #pragma unroll - for (int i = 0; i < (Mtile + M_SIZE_ONE_LOAD - 1) / M_SIZE_ONE_LOAD; - ++i) { - cp_async<16>(A_smem_base_addr + stage_idx * A_smem_stage_stride + - (i * M_SIZE_ONE_LOAD * 32) * sizeof(FType), - this_block_A_base_ptr + i * M_SIZE_ONE_LOAD * params.K, - 16, (A_ldg_guard & (1u << i)) != 0); - } - - #pragma unroll - for (int i = 0; i < (Ntile + N_SIZE_ONE_LOAD - 1) / N_SIZE_ONE_LOAD; - ++i) { - cp_async<16>(BQ_smem_base_addr + stage_idx * BQ_smem_stage_stride + - (i * N_SIZE_ONE_LOAD * 32) * sizeof(uint8_t), - this_block_B_base_ptr + i * N_SIZE_ONE_LOAD * params.K, - 16, (B_ldg_guard & (1u << i)) != 0); - } - - this_block_A_base_ptr += 32; - this_block_B_base_ptr += (32 * 4); - } - cp_async_commit_group(); - } - } - - __device__ void ldgsts(const int& sts_stage_idx) { - const int a_stage_offset = sts_stage_idx * A_smem_stage_stride; - const int bq_stage_offset = sts_stage_idx * BQ_smem_stage_stride; - #pragma unroll - for (int i = 0; i < (Mtile + M_SIZE_ONE_LOAD - 1) / M_SIZE_ONE_LOAD; ++i) { - cp_async<16>(A_smem_base_addr + a_stage_offset + - (i * M_SIZE_ONE_LOAD * 32) * sizeof(FType), - this_block_A_base_ptr + i * M_SIZE_ONE_LOAD * params.K, 16, - (A_ldg_guard & (1u << i)) != 0); - } - - #pragma unroll - for (int i = 0; i < (Ntile + N_SIZE_ONE_LOAD - 1) / N_SIZE_ONE_LOAD; ++i) { - cp_async<16>(BQ_smem_base_addr + bq_stage_offset + - (i * N_SIZE_ONE_LOAD * 32) * sizeof(uint8_t), - this_block_B_base_ptr + i * N_SIZE_ONE_LOAD * params.K, 16, - (B_ldg_guard & (1u << i)) != 0); - } - - cp_async_commit_group(); - this_block_A_base_ptr += 32; - this_block_B_base_ptr += (32 * 4); - } - - const FType* this_block_A_base_ptr = nullptr; - const QType* this_block_B_base_ptr = nullptr; - - int Aldg_col_idx; - int Bldg_col_idx; - - uint32_t A_ldg_guard; - uint32_t B_ldg_guard; - - uint32_t A_smem_base_addr, BQ_smem_base_addr; - const uint32_t A_smem_stage_stride, BQ_smem_stage_stride; - - const SM8x_GEMM_W8A16_Splitk_Params& params; -}; - -/* - * requiring N % 8 == 0 - */ -template -struct ComputeTile_W8A16_PerC_MtilexNtilex32_multistage_SM8x_SplitK { - static constexpr int WARP_SIZE = 32; - static constexpr int WARP_CNT = BLOCK / WARP_SIZE; - static constexpr int WARP_NTILE = Ntile / WARP_CNT; - static constexpr int WARP_NITER = WARP_NTILE / 8; // hmma16816 - static_assert(WARP_NTILE == 32 or WARP_NTILE == 64, - "now only support WARP_NTILE = 32 or 64!"); - - __device__ ComputeTile_W8A16_PerC_MtilexNtilex32_multistage_SM8x_SplitK( - const SM8x_GEMM_W8A16_Splitk_Params& k_params, - const uint32_t& A_smem_addr, const uint32_t& BQ_smem_addr, - const uint32_t& A_stage_stride, const uint32_t& BQ_stage_stride) - : params(k_params), - A_smem_base_addr(A_smem_addr), - BQ_smem_base_addr(BQ_smem_addr), - A_smem_stage_stride(A_stage_stride), - BQ_smem_stage_stride(BQ_stage_stride) { - warp_id = threadIdx.x / WARP_SIZE; - lane_id = threadIdx.x % WARP_SIZE; - - load_a_base_offset[0] = - (lane_id % 16) * 32 + - ((lane_id / 16) ^ (lane_id % 4) ^ ((lane_id / 4) % 2)) * 8; - load_a_base_offset[1] = - (lane_id % 16) * 32 + - ((lane_id / 16 + 2) ^ (lane_id % 4) ^ ((lane_id / 4) % 2)) * 8; - - load_b_base_offset[0] = - (lane_id / 4 + warp_id * (WARP_NTILE / 4)) * 32 * 4 + - (lane_id % 4) * 16 + ((lane_id / 4) % 2) * 16 * 4; - load_b_base_offset[1] = - (lane_id / 4 + warp_id * (WARP_NTILE / 4)) * 32 * 4 + - (lane_id % 4) * 16 + (((lane_id / 4) % 2) ^ 1) * 16 * 4; - - sts_c_base_offset = warp_id * Mtile * WARP_NTILE + - (lane_id / 4) * WARP_NTILE + (lane_id % 4) * 2; - - if (EnableFuse) { - this_block_C_base_ptr = - params.C_ptr + blockIdx.x * Mtile * params.N + blockIdx.y * Ntile; - } else { - this_block_C_base_ptr = - params.C_split_ptr + blockIdx.z * params.M * params.N + - blockIdx.x * Mtile * params.N + blockIdx.y * Ntile; - } - int store_thds_in_row = WARP_NTILE / 8; - store_c_row_base_idx = lane_id / store_thds_in_row; - store_c_col_idx = warp_id * WARP_NTILE + (lane_id % store_thds_in_row) * 8; - store_c_base_offset = store_c_row_base_idx * params.N + store_c_col_idx; - - #pragma unroll - for (int i = 0; i < Mtile / 16; ++i) { - #pragma unroll - for (int j = 0; j < WARP_NITER; ++j) { - #pragma unroll - for (int k = 0; k < 4; ++k) { - C_frag[i][j][k] = 0.f; - } - } - } - params_n_idx = - blockIdx.y * Ntile + warp_id * WARP_NTILE + (lane_id / 4) * 4; - } - - __device__ void lds(const int& smem_stage_idx, const int& reg_buf_idx, - const int& k_phase_idx) { - uint32_t A_smem_addr = - A_smem_base_addr + A_smem_stage_stride * smem_stage_idx; - uint32_t B_smem_addr = - BQ_smem_base_addr + BQ_smem_stage_stride * smem_stage_idx; - - #pragma unroll - for (int i = 0; i < Mtile / 16; ++i) { - ldsm_4(A_frag[reg_buf_idx][i][0], A_frag[reg_buf_idx][i][1], - A_frag[reg_buf_idx][i][2], A_frag[reg_buf_idx][i][3], - A_smem_addr + (load_a_base_offset[k_phase_idx] + i * 16 * 32) * - sizeof(FType)); - } - #pragma unroll - for (int i = 0; i < WARP_NTILE / 32; ++i) { - lds128(BQ_frag[reg_buf_idx][4 * i + 0], BQ_frag[reg_buf_idx][4 * i + 1], - BQ_frag[reg_buf_idx][4 * i + 2], BQ_frag[reg_buf_idx][4 * i + 3], - B_smem_addr + (load_b_base_offset[k_phase_idx] + i * 32 * 32) * - sizeof(uint8_t)); - } - - // dequant B - #pragma unroll - for (int i = 0; i < WARP_NITER / 2; ++i) { - cvt_8bx4_to_16bx4_bias128(BQ_frag[reg_buf_idx][2 * i], - BF_frag[reg_buf_idx][2 * i]); - if (has_zp) { - BF_frag[reg_buf_idx][2 * i][0] = - __hsub2(BF_frag[reg_buf_idx][2 * i][0], num2num2(B_zero[i].x)); - BF_frag[reg_buf_idx][2 * i][1] = - __hsub2(BF_frag[reg_buf_idx][2 * i][1], num2num2(B_zero[i].x)); - } - - BF_frag[reg_buf_idx][2 * i][0] = - __hmul2(BF_frag[reg_buf_idx][2 * i][0], num2num2(B_scale[i].x)); - BF_frag[reg_buf_idx][2 * i][1] = - __hmul2(BF_frag[reg_buf_idx][2 * i][1], num2num2(B_scale[i].x)); - - cvt_8bx4_to_16bx4_bias128(BQ_frag[reg_buf_idx][2 * i + 1], - BF_frag[reg_buf_idx][2 * i + 1]); - if (has_zp) { - BF_frag[reg_buf_idx][2 * i + 1][0] = - __hsub2(BF_frag[reg_buf_idx][2 * i + 1][0], num2num2(B_zero[i].y)); - BF_frag[reg_buf_idx][2 * i + 1][1] = - __hsub2(BF_frag[reg_buf_idx][2 * i + 1][1], num2num2(B_zero[i].y)); - } - - BF_frag[reg_buf_idx][2 * i + 1][0] = - __hmul2(BF_frag[reg_buf_idx][2 * i + 1][0], num2num2(B_scale[i].y)); - BF_frag[reg_buf_idx][2 * i + 1][1] = - __hmul2(BF_frag[reg_buf_idx][2 * i + 1][1], num2num2(B_scale[i].y)); - } - } - - __device__ void ldg_params() { - const int N_padded = (params.N + 31) / 32 * 32; - // load B scale and zero_point - #pragma unroll - for (int i = 0; i < WARP_NTILE / 32; ++i) { - ldg64_ca(B_scale[2 * i + 0], B_scale[2 * i + 1], - params.B_scale_ptr + params_n_idx + i * 32, - (params_n_idx + i * 32) < N_padded); - if (has_zp) { - ldg64_ca(B_zero[2 * i + 0], B_zero[2 * i + 1], - params.B_zero_ptr + params_n_idx + i * 32, - (params_n_idx + i * 32) < N_padded); - } - } - } - - __device__ void mma(const int& reg_buf_idx) { - #pragma unroll - for (int m_idx = 0; m_idx < Mtile / 16; ++m_idx) { - #pragma unroll - for (int n_idx = 0; n_idx < WARP_NITER; ++n_idx) { - hmma16816_f32( - C_frag[m_idx][n_idx], A_frag[reg_buf_idx][m_idx], - reinterpret_cast(BF_frag[reg_buf_idx][n_idx])); - } - } - } - - __device__ void fused_splitk_reduce() { - // need splitk-reduce if enable splitk - if (gridDim.z > 1) { - auto blk_red_idx = blockIdx.x * gridDim.y + blockIdx.y; - // Wait for all previous blocks in the splitk direction to accumulate the - // results into C_tmp - if (threadIdx.x == 0) { - uint32_t* red_count_ptr = params.red_count_ptr + blk_red_idx; - uint32_t count; - do { - // make sure the ld.cg inside the do-wile loop - __threadfence_block(); - asm volatile("ld.global.cg.b32 %0, [%1];" - : "=r"(count) - : "l"(red_count_ptr)); - } while (count != blockIdx.z); - } - __syncthreads(); - - auto C_tmp_base_offset = blk_red_idx * Mtile * Ntile + threadIdx.x * 4; - if (blockIdx.z != 0) { - // expecting that temporary register here reuses the previous A&B frag - // register - float temp_frag[Mtile / 16][WARP_NITER][4]; - #pragma unroll - for (int m_idx = 0; m_idx < Mtile / 16; ++m_idx) { - #pragma unroll - for (int n_idx = 0; n_idx < WARP_NITER; ++n_idx) { - int offset = - C_tmp_base_offset + (m_idx * WARP_NITER + n_idx) * BLOCK * 4; - *reinterpret_cast(temp_frag[m_idx][n_idx]) = - *reinterpret_cast(params.C_tmp_ptr + offset); - } - } - #pragma unroll - for (int m_idx = 0; m_idx < Mtile / 16; ++m_idx) { - #pragma unroll - for (int n_idx = 0; n_idx < WARP_NITER; ++n_idx) { - #pragma unroll - for (int idx = 0; idx < 4; ++idx) { - C_frag[m_idx][n_idx][idx] += temp_frag[m_idx][n_idx][idx]; - } - } - } - } - - // first splitk - 1 blocks need to write partial results into C_tmp - if (blockIdx.z != gridDim.z - 1) { - #pragma unroll - for (int m_idx = 0; m_idx < Mtile / 16; ++m_idx) { - #pragma unroll - for (int n_idx = 0; n_idx < WARP_NITER; ++n_idx) { - int offset = - C_tmp_base_offset + (m_idx * WARP_NITER + n_idx) * BLOCK * 4; - asm volatile( - "{st.global.cg.v4.b32 [%0], {%1, %2, %3, %4};}\n" - : - : "l"(params.C_tmp_ptr + offset), "f"(C_frag[m_idx][n_idx][0]), - "f"(C_frag[m_idx][n_idx][1]), "f"(C_frag[m_idx][n_idx][2]), - "f"(C_frag[m_idx][n_idx][3])); - } - } - __threadfence(); - __syncthreads(); - if (threadIdx.x == 0) { - uint32_t* red_count_ptr = params.red_count_ptr + blk_red_idx; - atomicInc(red_count_ptr, gridDim.z); - } - } - } - } - - __device__ void stg(char* smem) { - if (EnableFuse) { - if (blockIdx.z != gridDim.z - 1) return; - } - uint32_t* C_sts_ptr = - reinterpret_cast(smem + sts_c_base_offset * sizeof(FType)); - // C_tile sts - #pragma unroll - for (int m_idx = 0; m_idx < Mtile / 16; ++m_idx) { - #pragma unroll - for (int n_idx = 0; n_idx < WARP_NITER; ++n_idx) { - #pragma unroll - for (int k_idx = 0; k_idx < 2; ++k_idx) { - FType low16 = MarlinScalarType2::float2num( - C_frag[m_idx][n_idx][k_idx * 2]); - FType high16 = MarlinScalarType2::float2num( - C_frag[m_idx][n_idx][k_idx * 2 + 1]); - uint32_t tmp = (reinterpret_cast(low16) & 0xffff) | - (reinterpret_cast(high16) << 16); - int sts_offset = - m_idx * 16 * (WARP_NTILE / 2) + - (((lane_id / (32 / WARP_NITER)) + n_idx) % WARP_NITER) * (8 / 2) + - k_idx * 8 * (WARP_NTILE / 2); - C_sts_ptr[sts_offset] = tmp; - } - } - } - - __syncthreads(); - - FType* C_base_ptr = this_block_C_base_ptr + store_c_base_offset; - // C_tile lds and stg - auto m_base_idx = store_c_row_base_idx + blockIdx.x * Mtile; - bool n_guard = (store_c_col_idx + blockIdx.y * Ntile) < params.N; - if (WARP_NTILE == 32) { - int lds_c_base_offset = warp_id * Mtile * WARP_NTILE + - (lane_id / 4) * WARP_NTILE + - ((lane_id % 4 + lane_id / 8) % 4) * 8; - uint4* C_lds_ptr = - reinterpret_cast(smem + lds_c_base_offset * sizeof(FType)); - #pragma unroll - for (int i = 0; i < (Mtile / 16) * (WARP_NITER / 2); ++i) { - uint4 stg_reg = C_lds_ptr[i * 8 * 4]; - stg128(stg_reg.x, stg_reg.y, stg_reg.z, stg_reg.w, - C_base_ptr + i * 8 * params.N, - (m_base_idx + i * 8) < params.M && n_guard); - } - } else if (WARP_NTILE == 64) { - int lds_c_base_offset = - warp_id * Mtile * WARP_NTILE + (lane_id / 8) * WARP_NTILE; - #pragma unroll - for (int i = 0; i < (Mtile / 16) * (WARP_NITER / 2); ++i) { - int lds_c_offset = lds_c_base_offset + i * 4 * WARP_NTILE + - ((lane_id % 8 + lane_id / 8 + (i % 2) * 4) % 8) * 8; - uint4 stg_reg = - *reinterpret_cast(smem + lds_c_offset * sizeof(FType)); - stg128(stg_reg.x, stg_reg.y, stg_reg.z, stg_reg.w, - C_base_ptr + i * 4 * params.N, - (m_base_idx + i * 4) < params.M && n_guard); - } - } - } - - const SM8x_GEMM_W8A16_Splitk_Params& params; - - int load_a_base_offset[2]; - int load_b_base_offset[2]; - int sts_c_base_offset; - - int store_c_base_offset; - - int store_c_row_base_idx, store_c_col_idx; - FType* this_block_C_base_ptr = nullptr; - - int params_n_idx; - const uint32_t A_smem_base_addr, BQ_smem_base_addr; - const uint32_t A_smem_stage_stride, BQ_smem_stage_stride; - - int lane_id; - int warp_id; - // first 2 denotes double buffer, second dim denotes M direction - uint32_t A_frag[2][Mtile / 16][4]; - - typename HalfType::T2 B_scale[WARP_NITER / 2]; - typename HalfType::T2 B_zero[WARP_NITER / 2]; - uint32_t BQ_frag[2][WARP_NITER]; - // first 2 denotes double buffer, second dim denotes N direction, last 2 - // denotes K direction - typename HalfType::T2 BF_frag[2][WARP_NITER][2]; - // first dim denotes M direction, second dim denotes N direction - float C_frag[Mtile / 16][WARP_NITER][4]; -}; - -/* - * @brief W8A16 Perchannel Quantization GEMM, - * requires N % 8 == 0, K % 16 == 0 - * accumulator precision: FP32 - * @tparam FType: DataType for A, B_scale, B_zero, and C, supports half or - * nv_bfloat16 - * @tparam QType: DataType for B, support uint8(bias128) - * @tparam Mtile: M-dimensional size of the gemm block tile, supports 16, 32, - * 48 or 64 - * @tparam Ntile: N-dimensional size of the gemm block tile, supports 128 or - * 256 - * @tparam NStage: Num of stages for async copy - * @tparam BLOCK: BLOCK size - * @tparam EnableFuse: If true, use fused splitk-reduce, otherwise use - * non-fused splitk-reduce - * @tparam has_zp: whether to use zero_point - * - * @fparam params struct consists of following parameters: - * @param A_ptr: Matrix A value ptr, A = (M, K) - * @param B_ptr: Matrix B value ptr, B = (N32_align, K) (N32K16 special - * format), N32_align = (N + 32 - 1) / 32 * 32 - * @param B_scale_ptr: B_scale value ptr, B_scale = (N32_align,) (N32K16 - * special format) - * @param B_zero_ptr: B_zero value ptr, B_zero = (N32_align,) (N32K16 - * special format) - * @param C_ptr: Matrix C value ptr, C = (M, N) - * @param M: dimnesion m - * @param N: dimnesion n - * @param K: dimnesion k - * @param SplitK: split size along K-dimension - * @param C_split_ptr: Matrix C_split value ptr, used only in non-fused - * splitk-reduce - * @param C_tmp_ptr: Matrix C_tmp value ptr, used only in fused - * splitk-reduce - * @param red_count_ptr: 1-D red_count value ptr, used only in fused - * splitk-reduce - */ -template -__global__ void __launch_bounds__(BLOCK) - ampere_hgemm_W8A16_perc_f16_f16_MtilexNtilex32_hmma16816_multistage_AN_BTN32K16_CN_splitk_kernel( - const SM8x_GEMM_W8A16_Splitk_Params params) { - // A smem size = 64 * 32 * 2B/elem * 4(stage) = 16KB - // B smem size = 128 * 32 * 1B/elem * 4(stage) = 16KB - constexpr int smem_size_one_stage = Mtile * 32 * 2 + Ntile * 32; - __shared__ char smem[NStage * smem_size_one_stage]; - char* A_smem = smem; - char* BQ_smem = smem + Mtile * 32 * 2 * NStage; - - uint32_t A_smem_addr = smem_u32addr(A_smem); - uint32_t BQ_smem_addr = smem_u32addr(BQ_smem); - uint32_t A_smem_stage_stride = Mtile * 32 * 2; - uint32_t BQ_smem_stage_stride = Ntile * 32; - - // initialize the data move process from GM to SMEM for this block - GmemTile_W8A16_PerC_MtilexNtilex32_multistage_SM8x_SplitK< - FType, QType, Mtile, Ntile, NStage, BLOCK> - gmem_tile(params, A_smem_addr, BQ_smem_addr, A_smem_stage_stride, - BQ_smem_stage_stride); - - int sts_stage_idx = 0; - int lds_stage_idx = 0; - - auto tb_k_slice = blockIdx.z * params.SplitK + params.SplitK <= params.K - ? params.SplitK - : params.K - blockIdx.z * params.SplitK; - int k_tiles = (tb_k_slice + 31) / 32; - int first_k_tile = tb_k_slice - (k_tiles - 1) * 32; - - // load first three tiles to shared memory - gmem_tile.ldgsts_first_ktiles(first_k_tile, k_tiles); - sts_stage_idx += (NStage - 2); - ComputeTile_W8A16_PerC_MtilexNtilex32_multistage_SM8x_SplitK< - FType, QType, Mtile, Ntile, BLOCK, EnableFuse, has_zp> - compute_tile(params, A_smem_addr, BQ_smem_addr, A_smem_stage_stride, - BQ_smem_stage_stride); - compute_tile.ldg_params(); - cp_asyc_wait_group(); - __syncthreads(); - - compute_tile.lds(lds_stage_idx, 0, 0); - int reg_buf_idx = 1; - - // main loop - for (; k_tiles > NStage - 1; --k_tiles) { - // load next A&B tile - sts_stage_idx = sts_stage_idx < NStage - 1 ? sts_stage_idx + 1 : 0; - gmem_tile.ldgsts(sts_stage_idx); - - #pragma unroll - for (int k_phase_idx = 0; k_phase_idx < 2; k_phase_idx++) { - // dequantize next B tile - if (k_phase_idx == 1) { - cp_asyc_wait_group(); - __syncthreads(); - lds_stage_idx = lds_stage_idx < NStage - 1 ? lds_stage_idx + 1 : 0; - } - - compute_tile.lds(lds_stage_idx, reg_buf_idx, (k_phase_idx + 1) % 2); - - compute_tile.mma(reg_buf_idx ^ 1); - reg_buf_idx ^= 1; - } - } - - // last NStage-1 tiles - for (; k_tiles > 0; --k_tiles) { - cp_async_commit_group(); - #pragma unroll - for (int k_phase_idx = 0; k_phase_idx < 2; k_phase_idx++) { - // dequantize next B tile - if (k_phase_idx == 1) { - cp_asyc_wait_group(); - __syncthreads(); - lds_stage_idx = lds_stage_idx < NStage - 1 ? lds_stage_idx + 1 : 0; - } - - compute_tile.lds(lds_stage_idx, reg_buf_idx, (k_phase_idx + 1) % 2); - - compute_tile.mma(reg_buf_idx ^ 1); - reg_buf_idx ^= 1; - } - } - - if (EnableFuse) { - compute_tile.fused_splitk_reduce(); - } - compute_tile.stg(smem); -} - - #define __CALL_IF(MTILE, NTILE, NUM_THREADS, ENABLE_FUSE, HAS_ZP) \ - else if (Mtile == MTILE && Ntile == NTILE && BLOCK == NUM_THREADS && \ - enable_fuse == ENABLE_FUSE && has_zp == HAS_ZP) { \ - ampere_hgemm_W8A16_perc_f16_f16_MtilexNtilex32_hmma16816_multistage_AN_BTN32K16_CN_splitk_kernel< \ - FType, QType, MTILE, NTILE, 4, NUM_THREADS, ENABLE_FUSE, HAS_ZP> \ - <<>>(params); \ - } - -template -void ampere_hgemm_W8A16_perc_f16_f16_MtilexNtilex32_mma16816_multistage_AN_BTN32K16_CN_splitk( - const FType* A, const QType* B, const FType* B_scale, const FType* B_zero, - FType* C, const int M, const int N, const int K, void* workspace, - const int sm_version, const BlockTileSplitkParams& fused_gemm_params, - cudaStream_t stream) { - int Mtile = fused_gemm_params.Mtile; - int grid_x = (M + Mtile - 1) / Mtile; - int Ntile = fused_gemm_params.Ntile; - int grid_y = (N + Ntile - 1) / Ntile; - int SplitK = fused_gemm_params.SplitK; - int grid_z = (K + SplitK - 1) / SplitK; - - int BLOCK = (Ntile == 256) ? 256 : 128; - - dim3 grid(grid_x, grid_y, grid_z); - dim3 block(BLOCK); - - bool enable_fuse = fused_gemm_params.EnableFuse; - bool has_zp = B_zero != nullptr; - if (enable_fuse) { - float* C_tmp = reinterpret_cast(workspace); - uint32_t* red_count = reinterpret_cast( - (char*)workspace + grid_x * Mtile * grid_y * Ntile * sizeof(float)); - CHECK_CUDA(cudaMemsetAsync(red_count, 0, grid_x * grid_y * sizeof(uint32_t), - stream)); - SM8x_GEMM_W8A16_Splitk_Params params{ - A, B, B_scale, B_zero, C, M, N, - K, SplitK, 0, -1, nullptr, C_tmp, red_count}; - - if (false) { - } - // Select the template parameters for kernel launch - // according to the above settings. Tuning is not supported. - __CALL_IF(16, 256, 256, true, false) - __CALL_IF(32, 256, 256, true, false) - __CALL_IF(48, 256, 256, true, false) - __CALL_IF(64, 128, 128, true, false) - __CALL_IF(64, 256, 256, true, false) - __CALL_IF(16, 256, 256, true, true) - __CALL_IF(32, 256, 256, true, true) - __CALL_IF(48, 256, 256, true, true) - __CALL_IF(64, 128, 128, true, true) - __CALL_IF(64, 256, 256, true, true) - } else { - FType* C_split = reinterpret_cast(workspace); - SM8x_GEMM_W8A16_Splitk_Params params{ - A, B, B_scale, B_zero, C, M, N, - K, SplitK, 0, -1, C_split, nullptr, nullptr}; - - if (false) { - } - // Select the template parameters for kernel launch - // according to the above settings. Tuning is not supported. - __CALL_IF(16, 256, 256, false, false) - __CALL_IF(32, 256, 256, false, false) - __CALL_IF(48, 256, 256, false, false) - __CALL_IF(64, 128, 128, false, false) - __CALL_IF(64, 256, 256, false, false) - __CALL_IF(16, 256, 256, false, true) - __CALL_IF(32, 256, 256, false, true) - __CALL_IF(48, 256, 256, false, true) - __CALL_IF(64, 128, 128, false, true) - __CALL_IF(64, 256, 256, false, true) - - // SplitK reduce - f16_gemm_splitk_reduce(C_split, C, M, N, grid_z, stream); - } -} - -size_t allspark_qgemm_w8a16_perc_n32k16_ampere_workspace_size( - int m, int n, int k, int sm_count, - BlockTileSplitkParams& fused_gemm_params) { - // Determine the block tile and splitk strategy - int m16_times = (m + 16 - 1) / 16; - int Mtile = m16_times <= 4 ? m16_times * 16 : 64; - int grid_x = (m + Mtile - 1) / Mtile; - int Ntile = - (float(grid_x * ((n + 127) / 128)) / sm_count > 10) || (Mtile < 64) ? 256 - : 128; - int grid_y = (n + Ntile - 1) / Ntile; - int grid_z; - - // split-k - const float SPLIT_THRESHOLD = 0.8; - int n_slice; - for (n_slice = 1; n_slice < k / 256; ++n_slice) { - int n_block = grid_x * grid_y * n_slice; - if (n_block >= sm_count * SPLIT_THRESHOLD && - (n_block % sm_count == 0 || n_block % sm_count >= sm_count * 0.5)) { - break; - } - } - - int k_slice = - (k / n_slice) % 32 == 0 ? k / n_slice : k / n_slice / 32 * 32 + 32; - grid_z = (k + k_slice - 1) / k_slice; - bool enable_fuse = float(grid_x * grid_y) / sm_count >= 0.5 ? 1 : 0; - - size_t ws_size; - if (enable_fuse) { - ws_size = grid_x * Mtile * grid_y * Ntile * sizeof(float) // For C_tmp - + grid_x * grid_y * sizeof(uint32_t); // For red_count - } else { - ws_size = grid_z * m * n * sizeof(__half); - } - - fused_gemm_params.Mtile = Mtile; - fused_gemm_params.Ntile = Ntile; - fused_gemm_params.SplitK = k_slice; - fused_gemm_params.EnableFuse = enable_fuse; - return ws_size; -} - -// restore from N32K16 order to original N-major order -// K % 16 == 0, N % 8 == 0 -// each block process 64(k) * 32(n) result elements -template -__global__ void restore_N32_K16_dequantize_rhs_w8a16_perc_kernel( - const QT* qdata, const FT* scales, const FT* zeros, FT* fdata, - const int N_32align, const int N, const int K) { - __shared__ FT smem[64 * 32]; - auto warp_id = threadIdx.x / 32; - auto lane_id = threadIdx.x % 32; - const auto src_row_idx = blockIdx.x * 8 + lane_id / 4; - const int src_col_idx = - blockIdx.y * 64 * 4 + warp_id * 16 * 4 + (lane_id % 4) * 16; - const int src_offset = src_row_idx * K * 4 + src_col_idx; - auto params_nidx = blockIdx.x * 32 + (lane_id / 4) * 4; - - QT qval_reg[16]; - const QT* pdata = qdata + src_offset; - if (src_col_idx < (K * 4)) { - *(reinterpret_cast(qval_reg)) = - *(reinterpret_cast(qdata + src_offset)); - } - FT scale_reg[4]; - *(reinterpret_cast(scale_reg)) = - *(reinterpret_cast(scales + params_nidx)); - FT zero_reg[4]; - if (zeros != nullptr) { - *(reinterpret_cast(zero_reg)) = - *(reinterpret_cast(zeros + params_nidx)); - } - FT fval_reg[16]; - - const int sts_base_offset = - (warp_id * 16 + (lane_id % 4) * 2) * 32 + lane_id / 4; - #pragma unroll - for (int ni = 0; ni < 4; ++ni) { - cvt_8bx4_to_16bx4_bias128( - *reinterpret_cast(&qval_reg[ni * 4]), - reinterpret_cast::T2*>(&(fval_reg[ni * 4]))); - #pragma unroll - for (int ki = 0; ki < 4; ++ki) { - if (zeros != nullptr) { - fval_reg[ni * 4 + ki] = __hsub(fval_reg[ni * 4 + ki], zero_reg[ni]); - } - fval_reg[ni * 4 + ki] = __hmul(fval_reg[ni * 4 + ki], scale_reg[ni]); - int sts_offset = sts_base_offset + ((ki / 2) * 8 + (ki % 2)) * 32 + - ((ni + lane_id % 4) % 4) * 8; - smem[sts_offset] = fval_reg[ni * 4 + ki]; - } - } - __syncthreads(); - - const int lds_base_offset = - (threadIdx.x / 4) * 32 + ((threadIdx.x % 4 + threadIdx.x / 8) % 4) * 8; - #pragma unroll - for (int i = 0; i < 2; ++i) { - *reinterpret_cast(fval_reg + i * 8) = - *reinterpret_cast(smem + lds_base_offset + i * 32 * 32); - } - - const auto dst_row_base_kidx = blockIdx.y * 64 + threadIdx.x / 4; - const auto dst_col_nidx = blockIdx.x * 32 + (threadIdx.x % 4) * 8; - #pragma unroll - for (int i = 0; i < 2; ++i) { - int dst_row_kidx = dst_row_base_kidx + i * 32; - int dst_offset = dst_row_kidx * N + dst_col_nidx; - if (dst_row_kidx < K && dst_col_nidx < N) { - *reinterpret_cast(fdata + dst_offset) = - *reinterpret_cast(fval_reg + i * 8); - } - } -} - -template -void restore_N32_K16_dequantize_rhs_w8a16(const QT* qdata, const FT* scales, - const FT* zeros, FT* fdata, - const int N_32align, const int N, - const int K, const int GroupSize, - cudaStream_t stream) { - STD_TORCH_CHECK(N % 8 == 0 && K % 16 == 0 && N_32align % 32 == 0, - "Unsupported shape"); - if (GroupSize == -1) { - const int BLOCK = 128; - dim3 grid(N_32align / 32, ((K / 16) + 3) / 4); - restore_N32_K16_dequantize_rhs_w8a16_perc_kernel - <<>>(qdata, scales, zeros, fdata, N_32align, N, - K); - } - // TODO: Support SubChannel - else { - STD_TORCH_CHECK(false, "Now only support PerChannel"); - } -} - -template -void w8a16_gemm_dq_cublas(const FT* in, const QT* rhs_qdata_ptr, - const FT* rhs_scales_ptr, const FT* rhs_zeros_ptr, - FT* out, void* workspace, const int M, - const int N_32align, const int N, const int K, - const int group_size, cudaStream_t stream, - cublasHandle_t handle) { - static_assert( - std::is_same::value || std::is_same::value, - "only float16 and bfloat16 is supported"); - // Dequant - FT* rhs_fdata_ptr = static_cast(workspace); - restore_N32_K16_dequantize_rhs_w8a16(rhs_qdata_ptr, rhs_scales_ptr, - rhs_zeros_ptr, rhs_fdata_ptr, N_32align, - N, K, group_size, stream); - // cuBLAS GEMM - int lda = K; - int ldb = N; - int ldc = N; - const float alpha = 1.0f; - const float beta = 0.0f; - cudaDataType_t cuda_type; - if (std::is_same::value) { - cuda_type = CUDA_R_16F; - } else { - cuda_type = CUDA_R_16BF; - } - CHECK_CUBLAS(cublasGemmEx(handle, CUBLAS_OP_N, CUBLAS_OP_N, N, M, K, &alpha, - rhs_fdata_ptr, cuda_type, ldb, in, cuda_type, lda, - &beta, out, cuda_type, ldc, CUDA_R_32F, - CUBLAS_GEMM_DEFAULT_TENSOR_OP)); -} - -template -void allspark_qgemm_w8a16_perc_ampere( - const FType* A, const QType* B, const FType* B_scale, const FType* B_zero, - FType* C, const int M, const int N_32align, const int N, const int K, - void* workspace, const BlockTileSplitkParams& fused_gemm_params, - const int group_size, int CUBLAS_M_THRESHOLD, const int sm_version, - cudaStream_t stream, cublasHandle_t handle) { - if (M > CUBLAS_M_THRESHOLD) { - w8a16_gemm_dq_cublas(A, B, B_scale, B_zero, C, workspace, M, - N_32align, N, K, group_size, stream, - handle); - } else { - ampere_hgemm_W8A16_perc_f16_f16_MtilexNtilex32_mma16816_multistage_AN_BTN32K16_CN_splitk< - FType, QType>(A, B, B_scale, B_zero, C, M, N, K, workspace, sm_version, - fused_gemm_params, stream); - } -} - -} // namespace allspark - -torch::stable::Tensor allspark_w8a16_gemm( - torch::stable::Tensor const& a, torch::stable::Tensor const& b_qweight, - torch::stable::Tensor const& b_scales, - std::optional const& b_qzeros, int64_t n, - int64_t group_size, int64_t sm_count, int64_t sm_version, - int64_t CUBLAS_M_THRESHOLD, bool has_zp, bool n32k16_reorder) { - // Verify device and strides - STD_TORCH_CHECK(a.device().is_cuda(), "A is not on GPU"); - STD_TORCH_CHECK(a.is_contiguous(), "A is not contiguous"); - - STD_TORCH_CHECK(b_qweight.device().is_cuda(), "b_qweight is not on GPU"); - STD_TORCH_CHECK(b_qweight.is_contiguous(), "b_qweight is not contiguous"); - - STD_TORCH_CHECK(b_scales.device().is_cuda(), "b_scales is not on GPU"); - STD_TORCH_CHECK(b_scales.is_contiguous(), "b_scales is not contiguous"); - - if (has_zp) { - STD_TORCH_CHECK(b_qzeros.value().device().is_cuda(), - "b_qzeros is not on GPU"); - STD_TORCH_CHECK(b_qzeros.value().is_contiguous(), - "b_qzeros is not contiguous"); - } - - int m = a.size(0); - int n_32align = (n + 32 - 1) / 32 * 32; - int k = a.size(1); - - // Verify shape - STD_TORCH_CHECK(b_qweight.size(0) == n_32align, - "Shape mismatch: b_qweight.size(0) = ", b_qweight.size(0), - ", n_32align = ", n_32align); - STD_TORCH_CHECK(b_qweight.size(1) == k, - "Shape mismatch: b_qweight.size(1) = ", b_qweight.size(1), - ", k = ", k); - - STD_TORCH_CHECK(group_size == -1, "Currently only supports group_size = -1"); - - const torch::stable::accelerator::DeviceGuard device_guard( - a.get_device_index()); - const void* a_ptr = reinterpret_cast(a.data_ptr()); - const uint8_t* b_ptr = reinterpret_cast(b_qweight.data_ptr()); - const void* b_scale_ptr = reinterpret_cast(b_scales.data_ptr()); - const void* b_zero_ptr = nullptr; - if (b_qzeros.has_value()) { - b_zero_ptr = reinterpret_cast(b_qzeros.value().data_ptr()); - } - - auto c = - torch::stable::empty({m, n}, a.scalar_type(), std::nullopt, a.device()); - void* c_ptr = reinterpret_cast(c.mutable_data_ptr()); - - cudaStream_t stream = get_current_cuda_stream(); - cublasHandle_t handle = get_current_cuda_blas_handle(); - - allspark::BlockTileSplitkParams fused_gemm_params; - - size_t ws_size = 0; - if (m > CUBLAS_M_THRESHOLD) { - ws_size = k * n * 2; // sizeof(f16)==2 - } else { - ws_size = allspark::allspark_qgemm_w8a16_perc_n32k16_ampere_workspace_size( - m, n, k, sm_count, fused_gemm_params); - } - - if (as_g_workspace.numel() < - ws_size) { // ws_options: kChar, so numel() is bytes - as_g_workspace = torch::stable::empty({static_cast(ws_size)}, - torch::headeronly::ScalarType::Char, - std::nullopt, a.device()); - } - void* ws = reinterpret_cast(as_g_workspace.data_ptr()); - - if (a.scalar_type() == torch::headeronly::ScalarType::Half) { - allspark::allspark_qgemm_w8a16_perc_ampere<__half, uint8_t>( - reinterpret_cast(a_ptr), b_ptr, - reinterpret_cast(b_scale_ptr), - reinterpret_cast(b_zero_ptr), - reinterpret_cast<__half*>(c_ptr), m, n_32align, n, k, ws, - fused_gemm_params, group_size, CUBLAS_M_THRESHOLD, sm_version, stream, - handle); - } else if (a.scalar_type() == torch::headeronly::ScalarType::BFloat16) { - allspark::allspark_qgemm_w8a16_perc_ampere<__nv_bfloat16, uint8_t>( - reinterpret_cast(a_ptr), b_ptr, - reinterpret_cast(b_scale_ptr), - reinterpret_cast(b_zero_ptr), - reinterpret_cast<__nv_bfloat16*>(c_ptr), m, n_32align, n, k, ws, - fused_gemm_params, group_size, CUBLAS_M_THRESHOLD, sm_version, stream, - handle); - } - - return c; -} - -#endif - -STABLE_TORCH_LIBRARY_IMPL(_C, CUDA, m) { - m.impl("allspark_w8a16_gemm", TORCH_BOX(&allspark_w8a16_gemm)); -} diff --git a/csrc/libtorch_stable/quantization/gptq_allspark/allspark_repack.cu b/csrc/libtorch_stable/quantization/gptq_allspark/allspark_repack.cu deleted file mode 100644 index 9a00f66241b5..000000000000 --- a/csrc/libtorch_stable/quantization/gptq_allspark/allspark_repack.cu +++ /dev/null @@ -1,176 +0,0 @@ -#include "allspark_utils.cuh" - -#include -#include -#include - -#include "core/registration.h" -#include "libtorch_stable/torch_utils.h" - -namespace allspark { - -// Rearrange B to facilitate Ampere Tensor Core load data -// reorder B from (K, N) to (N_32align / 4, K * 4) -// K % 16 == 0, N % 16 == 0, N_32align % 32 == 0 -template -__global__ void __launch_bounds__(128) - rearrange_kn_weight_as_n32k16_order_ldg16_kernel( - const uint8_t* B, const FType* B_scale, const FType* B_zero, - uint8_t* B_result, FType* B_scale_result, FType* B_zero_result, - const int K, const int N, const int N_32align) { - const auto lane_id = threadIdx.x % 32; - const auto warp_id = threadIdx.x / 32; - - if (blockIdx.x != gridDim.x - 1) { - // Load B - // per block process 64(k) * 128(n) B elements - // per warp process 16(k) * 128 B elements - const int src_row_base_idx = - blockIdx.x * 64 + warp_id * 16 + ((lane_id % 8) / 2) * 2; - const int src_col_idx = - blockIdx.y * 128 + (lane_id / 8) * 32 + (lane_id % 2) * 16; - uint8_t B_frag[4][16]; -#pragma unroll - for (int i = 0; i < 4; ++i) { - int src_row_idx = src_row_base_idx + (i / 2) * 8 + (i % 2); - int src_offset = src_row_idx * N + src_col_idx; - bool guard = src_row_idx < K && src_col_idx < N; - ldg128_cg_0(*reinterpret_cast(B_frag[i]), - *(reinterpret_cast(B_frag[i]) + 1), - *(reinterpret_cast(B_frag[i]) + 2), - *(reinterpret_cast(B_frag[i]) + 3), B + src_offset, - guard); - } - - // reorder B - uint8_t B_reorder_frag[8][8]; -#pragma unroll - for (int i = 0; i < 4; ++i) { -#pragma unroll - for (int j = 0; j < 16; ++j) { - int dst_i = j % 8; - int dst_j = i + (j / 8) * 4; - B_reorder_frag[dst_i][dst_j] = B_frag[i][j]; - } - } - - // Store B - const auto dst_row_base_idx = blockIdx.y * (128 / 4) + (lane_id / 8) * 8; - const int dst_col_idx = - blockIdx.x * (64 * 4) + warp_id * 64 + (lane_id % 8) * 8; - for (int i = 0; i < 8; ++i) { - int dst_row_idx = dst_row_base_idx + i; - int dst_offset = dst_row_idx * K * 4 + dst_col_idx; - bool guard = (dst_row_base_idx < N_32align / 4) && (dst_col_idx < K * 4); - if (guard) { - *reinterpret_cast(B_result + dst_offset) = - *reinterpret_cast(B_reorder_frag[i]); - } - } - } else { - // Load B_scale and B_zero - FType b_scale_reg, b_zero_reg; - auto src_offset = blockIdx.y * 128 + threadIdx.x; - ldg16_cg_0(b_scale_reg, B_scale + src_offset, src_offset < N); - if (B_zero != nullptr) - ldg16_cg_0(b_zero_reg, B_zero + src_offset, src_offset < N); - int dst_offset = - blockIdx.y * 128 + warp_id * 32 + (lane_id % 8) * 4 + lane_id / 8; - if (dst_offset < N_32align) { - B_scale_result[dst_offset] = b_scale_reg; - if (B_zero != nullptr) B_zero_result[dst_offset] = b_zero_reg; - } - } -} - -template -void rearrange_kn_weight_as_n32k16_order_ldg16( - const uint8_t* B, const FType* B_scale, const FType* B_zero, - uint8_t* B_result, FType* B_scale_result, FType* B_zero_result, - const int64_t K, const int64_t N, const int64_t N_32align, - cudaStream_t stream) { - if (N % 16 != 0 || K % 16 != 0) { - std::cerr << "Now only support N and K is multiples of 16" << std::endl; - } - const int BLOCK = 128; - int grid_x = (K + 64 - 1) / 64 + 1; - int grid_y = (N + 128 - 1) / 128; - dim3 grid(grid_x, grid_y); - - rearrange_kn_weight_as_n32k16_order_ldg16_kernel - <<>>(B, B_scale, B_zero, B_result, B_scale_result, - B_zero_result, K, N, N_32align); -} -} // namespace allspark - -void rearrange_kn_weight_as_n32k16_order( - torch::stable::Tensor const& b_qweight, - torch::stable::Tensor const& b_scales, - std::optional const& b_zeros, bool has_zp, - torch::stable::Tensor& b_qweight_reorder, - torch::stable::Tensor& b_scales_reorder, - std::optional const& b_zeros_reorder, - const int64_t K, const int64_t N, const int64_t N_32align) { - // Verify device and strides - STD_TORCH_CHECK(b_qweight.device().is_cuda(), "b_qweight is not on GPU"); - STD_TORCH_CHECK(b_qweight.is_contiguous(), "b_qweight is not contiguous"); - - STD_TORCH_CHECK(b_scales.device().is_cuda(), "b_scales is not on GPU"); - STD_TORCH_CHECK(b_scales.is_contiguous(), "b_scales is not contiguous"); - - STD_TORCH_CHECK(b_qweight_reorder.device().is_cuda(), - "b_qweight_reorder is not on GPU"); - STD_TORCH_CHECK(b_qweight_reorder.is_contiguous(), - "b_qweight_reorder is not contiguous"); - - STD_TORCH_CHECK(b_scales_reorder.device().is_cuda(), - "b_scales_reorder is not on GPU"); - STD_TORCH_CHECK(b_scales_reorder.is_contiguous(), - "b_scales_reorder is not contiguous"); - - if (has_zp) { - STD_TORCH_CHECK(b_zeros.value().device().is_cuda(), - "b_zeros is not on GPU"); - STD_TORCH_CHECK(b_zeros.value().is_contiguous(), - "b_zeros is not contiguous"); - - STD_TORCH_CHECK(b_zeros_reorder.value().device().is_cuda(), - "b_zeros_reorder is not on GPU"); - STD_TORCH_CHECK(b_zeros_reorder.value().is_contiguous(), - "b_zeros_reorder is not contiguous"); - } - - const uint8_t* matB = reinterpret_cast(b_qweight.data_ptr()); - const void* b_scale = b_scales.data_ptr(); - const void* b_zero = has_zp ? b_zeros.value().data_ptr() : nullptr; - - uint8_t* matB_reorder = - reinterpret_cast(b_qweight_reorder.mutable_data_ptr()); - void* b_scale_reorder = b_scales_reorder.mutable_data_ptr(); - void* b_zero_reorder = - has_zp ? b_zeros_reorder.value().mutable_data_ptr() : nullptr; - - const torch::stable::accelerator::DeviceGuard device_guard( - b_qweight.get_device_index()); - cudaStream_t stream = get_current_cuda_stream(); - if (b_scales.scalar_type() == torch::headeronly::ScalarType::Half) { - allspark::rearrange_kn_weight_as_n32k16_order_ldg16<__half>( - matB, reinterpret_cast(b_scale), - reinterpret_cast(b_zero), matB_reorder, - reinterpret_cast<__half*>(b_scale_reorder), - reinterpret_cast<__half*>(b_zero_reorder), K, N, N_32align, stream); - } else if (b_scales.scalar_type() == - torch::headeronly::ScalarType::BFloat16) { - allspark::rearrange_kn_weight_as_n32k16_order_ldg16<__nv_bfloat16>( - matB, reinterpret_cast(b_scale), - reinterpret_cast(b_zero), matB_reorder, - reinterpret_cast<__nv_bfloat16*>(b_scale_reorder), - reinterpret_cast<__nv_bfloat16*>(b_zero_reorder), K, N, N_32align, - stream); - } -} - -STABLE_TORCH_LIBRARY_IMPL(_C, CUDA, m) { - m.impl("rearrange_kn_weight_as_n32k16_order", - TORCH_BOX(&rearrange_kn_weight_as_n32k16_order)); -} diff --git a/csrc/libtorch_stable/quantization/gptq_allspark/allspark_utils.cuh b/csrc/libtorch_stable/quantization/gptq_allspark/allspark_utils.cuh deleted file mode 100644 index 628f6551ea88..000000000000 --- a/csrc/libtorch_stable/quantization/gptq_allspark/allspark_utils.cuh +++ /dev/null @@ -1,387 +0,0 @@ -#pragma once - -#include -#include -#include - -#include - -#include "libtorch_stable/quantization/marlin/marlin_dtypes.cuh" -using marlin::MarlinScalarType2; - -namespace allspark { - -#define CHECK_CUDA(cmd) \ - do { \ - cudaError_t cuda_status = cmd; \ - if (cuda_status != cudaSuccess) { \ - std::string err_str = cudaGetErrorString(cuda_status); \ - std::cerr << "Failed: " << __FILE__ << ":" << __LINE__ << " " \ - << err_str; \ - exit(-1); \ - } \ - } while (0) - -#define CHECK_CUBLAS(cmd) \ - do { \ - cublasStatus_t cublas_status = cmd; \ - if (cublas_status != CUBLAS_STATUS_SUCCESS) { \ - std::cerr << "Failed: " << __FILE__ << ":" << __LINE__ << " " \ - << cublas_status << std::endl; \ - exit(-1); \ - } \ - } while (0) - -template -struct SM8x_GEMM_W8A16_Splitk_Params { - const FType* A_ptr; - const QType* B_ptr; - const FType* B_scale_ptr; - const FType* B_zero_ptr; - FType* C_ptr; - int M; - int N; - int K; - int SplitK; - int GroupCnt; - int GroupSize; - FType* C_split_ptr; // for non-fused splitk reduce - float* C_tmp_ptr; // for fused splitk reduce - uint32_t* red_count_ptr; // for fused splitk reduce -}; - -struct alignas(16) BlockTileSplitkParams { - int Mtile; - int Ntile; - int SplitK; - bool EnableFuse; -}; - -template -__global__ void f16_gemm_splitk_reduce_kernel(const FType* C_split, FType* C, - uint32_t n, uint32_t n_matrix, - uint32_t matrix_size) { - auto idx = blockIdx.x * BLOCK + threadIdx.x; - - if (idx >= matrix_size) { - return; - } - - float sum = 0.f; - - int n_mat = N_MATRIX > 0 ? N_MATRIX : (int)n_matrix; - for (int i = 0; i < n_mat; ++i) { - sum += MarlinScalarType2::num2float(C_split[idx + i * matrix_size]); - } - - C[idx] = MarlinScalarType2::float2num(sum); -} - -template -void f16_gemm_splitk_reduce(const FType* C_split, FType* C, const uint32_t m, - const uint32_t n, const uint32_t n_matrix, - cudaStream_t stream) { - const int BLOCK = 128; - uint32_t matrix_size = m * n; - int grid = (matrix_size + BLOCK - 1) / BLOCK; - - void (*kernel)(const FType*, FType*, uint32_t, uint32_t, uint32_t) = nullptr; - - switch (n_matrix) { - case 4: - kernel = f16_gemm_splitk_reduce_kernel; - break; - case 5: - kernel = f16_gemm_splitk_reduce_kernel; - break; - case 6: - kernel = f16_gemm_splitk_reduce_kernel; - break; - case 7: - kernel = f16_gemm_splitk_reduce_kernel; - break; - case 8: - kernel = f16_gemm_splitk_reduce_kernel; - break; - case 9: - kernel = f16_gemm_splitk_reduce_kernel; - break; - case 10: - kernel = f16_gemm_splitk_reduce_kernel; - break; - case 11: - kernel = f16_gemm_splitk_reduce_kernel; - break; - case 12: - kernel = f16_gemm_splitk_reduce_kernel; - break; - default: - kernel = f16_gemm_splitk_reduce_kernel; - break; - } - - kernel<<>>(C_split, C, n, n_matrix, matrix_size); -} - -template -struct HalfType; -template <> -struct HalfType { - using T1 = __half; - using T2 = __half2; -}; -template <> -struct HalfType<__nv_bfloat16> { - using T1 = __nv_bfloat16; - using T2 = __nv_bfloat162; -}; - -// convert 64-bit pointer to 32-bit smem addr -__device__ __forceinline__ uint32_t smem_u32addr(const void* smem_ptr) { - uint32_t addr; - asm("{.reg .u64 u64addr;\n" - " cvta.to.shared.u64 u64addr, %1;\n" - " cvt.u32.u64 %0, u64addr;}\n" - : "=r"(addr) - : "l"(smem_ptr)); - - return addr; -} - -template -__device__ __forceinline__ void ldg16_cg_0(T& r0, const void* ptr, bool guard) { - static_assert(sizeof(T) == 2, "ldg16_cg_0: invalid T"); - - asm volatile( - "{.reg .pred p;\n" - " setp.ne.b32 p, %2, 0;\n" - " @!p mov.b16 %0, 0;\n" -#if __CUDACC_VER_MAJOR__ >= 11 && __CUDACC_VER_MINOR__ >= 4 && \ - __CUDA_ARCH__ >= 750 - " @p ld.global.cg.L2::128B.b16 {%0}, [%1];}\n" -#else - " @p ld.global.ca.b16 {%0}, [%1];}\n" -#endif - : "=h"(reinterpret_cast(r0)) - : "l"(ptr), "r"((int)guard)); -} - -template -__device__ __forceinline__ void ldg64_ca(T& r0, T& r1, const void* ptr, - bool guard) { - static_assert(sizeof(T) == 4, "ldg64_ca: invalid T"); - - asm volatile( - "{.reg .pred p;\n" - " setp.ne.b32 p, %3, 0;\n" -#if __CUDACC_VER_MAJOR__ >= 11 && __CUDACC_VER_MINOR__ >= 4 && \ - __CUDA_ARCH__ >= 750 - " @p ld.global.ca.L2::128B.v2.b32 {%0, %1}, [%2];}\n" -#else - " @p ld.global.ca.v2.b32 {%0, %1}, [%2];}\n" -#endif - : "=r"(reinterpret_cast(r0)), - "=r"(reinterpret_cast(r1)) - : "l"(ptr), "r"((int)guard)); -} - -template -__device__ __forceinline__ void ldg128_cg_0(T& r0, T& r1, T& r2, T& r3, - const void* ptr, bool guard) { - static_assert(sizeof(T) == 4, "ldg128_cg_0: invalid T"); - - asm volatile( - "{.reg .pred p;\n" - " setp.ne.b32 p, %5, 0;\n" - " @!p mov.b32 %0, 0;\n" - " @!p mov.b32 %1, 0;\n" - " @!p mov.b32 %2, 0;\n" - " @!p mov.b32 %3, 0;\n" -#if __CUDACC_VER_MAJOR__ >= 11 && __CUDACC_VER_MINOR__ >= 4 && \ - __CUDA_ARCH__ >= 750 - " @p ld.global.cg.L2::128B.v4.b32 {%0, %1, %2, %3}, [%4];}\n" -#else - " @p ld.global.cg.v4.b32 {%0, %1, %2, %3}, [%4];}\n" -#endif - : "=r"(reinterpret_cast(r0)), - "=r"(reinterpret_cast(r1)), - "=r"(reinterpret_cast(r2)), - "=r"(reinterpret_cast(r3)) - : "l"(ptr), "r"((int)guard)); -} - -template -__device__ __forceinline__ void lds128(T& reg0, T& reg1, T& reg2, T& reg3, - const uint32_t addr) { - static_assert(sizeof(T) == 4, "lds128: invalid T"); - - asm volatile("ld.shared.v4.b32 {%0, %1, %2, %3}, [%4];\n" - : "=r"(reinterpret_cast(reg0)), - "=r"(reinterpret_cast(reg1)), - "=r"(reinterpret_cast(reg2)), - "=r"(reinterpret_cast(reg3)) - : "r"(addr)); -} - -template -__device__ __forceinline__ void stg128(const T& r0, const T& r1, const T& r2, - const T& r3, const void* ptr, - bool guard) { - static_assert(sizeof(T) == 4, "stg128: invalid T"); - - asm volatile( - "{.reg .pred p;\n" - " setp.ne.b32 p, %1, 0;\n" - " @p st.global.v4.b32 [%0], {%2, %3, %4, %5};}\n" - : - : "l"(ptr), "r"((int)guard), "r"(reinterpret_cast(r0)), - "r"(reinterpret_cast(r1)), - "r"(reinterpret_cast(r2)), - "r"(reinterpret_cast(r3))); -} - -template -__device__ __forceinline__ void ldsm_4(T& r0, T& r1, T& r2, T& r3, - const uint32_t& addr) { - static_assert(sizeof(T) == 4, "ldsm_4: invalid T"); -#if (__CUDA_ARCH__ >= 750) && (__CUDACC_VER_MAJOR__ >= 11) - asm volatile( - "ldmatrix.sync.aligned.m8n8.x4.shared.b16 {%0, %1, %2, %3}, [%4];\n" - : "=r"(reinterpret_cast(r0)), - "=r"(reinterpret_cast(r1)), - "=r"(reinterpret_cast(r2)), - "=r"(reinterpret_cast(r3)) - : "r"(addr)); -#endif -} - -template -__device__ __forceinline__ void hmma16816_f32(float (&d)[4], - const uint32_t (&a)[4], - const uint32_t (&b)[2]); - -template <> -__device__ __forceinline__ void hmma16816_f32<__half>(float (&d)[4], - const uint32_t (&a)[4], - const uint32_t (&b)[2]) { -#if (__CUDA_ARCH__ >= 800) && (__CUDACC_VER_MAJOR__ >= 11) - asm volatile( - "mma.sync.aligned.m16n8k16.row.col.f32.f16.f16.f32 {%0, %1, %2, %3}, " - "{%4, %5, %6, %7}, {%8, %9}, {%0, %1, %2, %3};\n" - : "+f"(d[0]), "+f"(d[1]), "+f"(d[2]), "+f"(d[3]) - : "r"(a[0]), "r"(a[1]), "r"(a[2]), "r"(a[3]), "r"(b[0]), "r"(b[1])); -#endif -} - -template <> -__device__ __forceinline__ void hmma16816_f32<__nv_bfloat16>( - float (&d)[4], const uint32_t (&a)[4], const uint32_t (&b)[2]) { -#if (__CUDA_ARCH__ >= 800) && (__CUDACC_VER_MAJOR__ >= 11) - asm volatile( - "mma.sync.aligned.m16n8k16.row.col.f32.bf16.bf16.f32 {%0, %1, %2, %3}, " - "{%4, %5, %6, %7}, {%8, %9}, {%0, %1, %2, %3};\n" - : "+f"(d[0]), "+f"(d[1]), "+f"(d[2]), "+f"(d[3]) - : "r"(a[0]), "r"(a[1]), "r"(a[2]), "r"(a[3]), "r"(b[0]), "r"(b[1])); -#endif -} - -template -__device__ __forceinline__ void cp_async(const uint32_t smem_addr, - const void* gmem_ptr, - const int src_in_bytes, bool guard) { - static_assert( - (SIZE_IN_BYTES == 4 || SIZE_IN_BYTES == 8 || SIZE_IN_BYTES == 16), - "Size is not supported"); -#if __CUDACC_VER_MAJOR__ >= 11 && __CUDA_ARCH__ >= 800 - asm volatile( - "{.reg.pred p;\n" - " setp.ne.b32 p, %4, 0;\n" - #if __CUDACC_VER_MINOR__ >= 4 - " @p cp.async.cg.shared.global.L2::256B [%0], [%1], %2, %3;}\n" - #else - " @p cp.async.cg.shared.global [%0], [%1], %2, %3;}\n" - #endif - ::"r"(smem_addr), - "l"(gmem_ptr), "n"(SIZE_IN_BYTES), "r"(src_in_bytes), "r"((int)guard)); -#endif -} - -__device__ __forceinline__ void cp_async_commit_group() { -#if __CUDACC_VER_MAJOR__ >= 11 && __CUDA_ARCH__ >= 800 - asm volatile("cp.async.commit_group;\n"); -#endif -} - -template -__device__ __forceinline__ void cp_asyc_wait_group() { -#if __CUDACC_VER_MAJOR__ >= 11 && __CUDA_ARCH__ >= 800 - asm volatile("cp.async.wait_group %0;\n" : : "n"(N)); -#endif -} - -template -__device__ __forceinline__ void cvt_8bx4_to_16bx4_bias128(const uint32_t& idata, - T* fdata); - -template <> -// fast conversion: 4xuint8 to 4xhalf, subtracting bias = 128 -__device__ __forceinline__ void cvt_8bx4_to_16bx4_bias128<__half2>( - const uint32_t& idata, __half2* fdata) { - uint32_t i10, i32; - asm volatile( - "prmt.b32 %0, %2, 0x64, 0x4140;" - "prmt.b32 %1, %2, 0x64, 0x4342;" - : "=r"(i10), "=r"(i32) - : "r"(idata)); - - static constexpr uint32_t MAGIC_NUM = 0x64806480; - fdata[0] = __hsub2(reinterpret_cast(i10), - reinterpret_cast(MAGIC_NUM)); - fdata[1] = __hsub2(reinterpret_cast(i32), - reinterpret_cast(MAGIC_NUM)); -} - -template <> -// fast conversion: 4xuint8 to 4xbfloat16, subtracting bias = 128 -// reference from marlin fast implementation -__device__ __forceinline__ void cvt_8bx4_to_16bx4_bias128<__nv_bfloat162>( - const uint32_t& idata, __nv_bfloat162* fdata) { - float fp32_imd[4]; - uint32_t* fp32_imd_casted = reinterpret_cast(fp32_imd); - asm volatile( - "prmt.b32 %0, %4, 0x4B000000, 0x7650;" - "prmt.b32 %1, %4, 0x4B000000, 0x7651;" - "prmt.b32 %2, %4, 0x4B000000, 0x7652;" - "prmt.b32 %3, %4, 0x4B000000, 0x7653;" - : "=r"(fp32_imd_casted[0]), "=r"(fp32_imd_casted[1]), - "=r"(fp32_imd_casted[2]), "=r"(fp32_imd_casted[3]) - : "r"(idata)); - - fp32_imd[0] -= 8388736.f; - fp32_imd[1] -= 8388736.f; - fp32_imd[2] -= 8388736.f; - fp32_imd[3] -= 8388736.f; - - uint32_t* bf16_res = reinterpret_cast(fdata); - asm volatile( - "prmt.b32 %0, %2, %3, 0x7632;" - "prmt.b32 %1, %4, %5, 0x7632;" - : "=r"(bf16_res[0]), "=r"(bf16_res[1]) - : "r"(fp32_imd_casted[0]), "r"(fp32_imd_casted[1]), - "r"(fp32_imd_casted[2]), "r"(fp32_imd_casted[3])); -} - -static __device__ nv_bfloat162 inline num2num2(const nv_bfloat16 x) { -#if defined(__CUDA_ARCH__) && __CUDA_ARCH__ < 800 - assert(false); -#else - return __bfloat162bfloat162(x); -#endif - __builtin_unreachable(); // Suppress missing return statement warning -} - -static __device__ half2 inline num2num2(const half x) { - return __half2half2(x); -} - -} // namespace allspark diff --git a/csrc/libtorch_stable/quantization/hadamard/hadacore/hadamard_transform_cuda.cu b/csrc/libtorch_stable/quantization/hadamard/hadacore/hadamard_transform_cuda.cu index 665585caa46c..a5d199db2737 100644 --- a/csrc/libtorch_stable/quantization/hadamard/hadacore/hadamard_transform_cuda.cu +++ b/csrc/libtorch_stable/quantization/hadamard/hadacore/hadamard_transform_cuda.cu @@ -796,7 +796,7 @@ torch::stable::Tensor hadacore_transform(torch::stable::Tensor& x, bool inplace) VLLM_STABLE_DISPATCH_HALF_TYPES(x.scalar_type(), "hadacore_transform_runfht", [&] { auto constexpr SCALAR_TYPE = torch::headeronly::CppTypeToScalarType::value; - hadacore::run_fht(x.data_ptr(), x.data_ptr(), x.numel(), had_size, stream); + hadacore::run_fht(x.data_ptr(), out.data_ptr(), x.numel(), had_size, stream); }); if (numel % 256 != 0) { diff --git a/csrc/libtorch_stable/quantization/marlin/marlin.cuh b/csrc/libtorch_stable/quantization/marlin/marlin.cuh index bfb65e874b3d..af4d4730ba3a 100644 --- a/csrc/libtorch_stable/quantization/marlin/marlin.cuh +++ b/csrc/libtorch_stable/quantization/marlin/marlin.cuh @@ -67,14 +67,6 @@ __device__ inline void cp_async2_ca_pred(void* smem_ptr, const void* glob_ptr, } } -__device__ inline void cp_async4_ca_pred(void* smem_ptr, const void* glob_ptr, - bool pred = true) { - if (pred) { - reinterpret_cast(smem_ptr)[0] = - reinterpret_cast(glob_ptr)[0]; - } -} - __device__ inline void cp_async4_pred(void* smem_ptr, const void* glob_ptr, bool pred = true) { if (pred) { @@ -121,19 +113,6 @@ __device__ inline void cp_async2_ca_pred(void* smem_ptr, const void* glob_ptr, "r"(smem), "l"(glob_ptr), "n"(BYTES)); } -__device__ inline void cp_async4_ca_pred(void* smem_ptr, const void* glob_ptr, - bool pred = true) { - const int BYTES = 16; - uint32_t smem = static_cast(__cvta_generic_to_shared(smem_ptr)); - asm volatile( - "{\n" - " .reg .pred p;\n" - " setp.ne.b32 p, %0, 0;\n" - " @p cp.async.ca.shared.global [%1], [%2], %3;\n" - "}\n" ::"r"((int)pred), - "r"(smem), "l"(glob_ptr), "n"(BYTES)); -} - __device__ inline void cp_async4_pred(void* smem_ptr, const void* glob_ptr, bool pred = true) { const int BYTES = 16; @@ -170,4 +149,4 @@ __device__ inline void cp_async_wait() { } // namespace MARLIN_NAMESPACE_NAME -#endif \ No newline at end of file +#endif diff --git a/csrc/libtorch_stable/quantization/w8a8/cutlass/c3x/scaled_mm_blockwise_sm100_fp8_dispatch.cuh b/csrc/libtorch_stable/quantization/w8a8/cutlass/c3x/scaled_mm_blockwise_sm100_fp8_dispatch.cuh index 8f74f2991469..fa2add918213 100644 --- a/csrc/libtorch_stable/quantization/w8a8/cutlass/c3x/scaled_mm_blockwise_sm100_fp8_dispatch.cuh +++ b/csrc/libtorch_stable/quantization/w8a8/cutlass/c3x/scaled_mm_blockwise_sm100_fp8_dispatch.cuh @@ -152,6 +152,29 @@ void cutlass_gemm_caller_blockwise(torch::stable::Tensor& out, torch::stable::Te int32_t m = a.size(0), n = b.size(1), k = a.size(1); + // The SM100 blockwise SF copy requires the SFA M extent to be a multiple + // of 4 (4-row copy atom). For misaligned M on the non-swapAB path, pad only + // the per-token activation scales (column-major (m, k/128), 1/64 the size + // of the bf16 activation) and build layout_SFA from the padded extent. + int32_t m_sf = m; + std::optional a_scales_pad; + void const* a_scales_data = a_scales.data_ptr(); + if constexpr (!swap_ab) { + if (m % 4 != 0) { + m_sf = (m + 3) & ~3; + int64_t num_groups = a_scales.numel() / m; + a_scales_pad = torch::stable::new_empty( + a_scales, {num_groups * m_sf}, + torch::headeronly::ScalarType::Float); + cudaMemcpy2DAsync(a_scales_pad->data_ptr(), m_sf * sizeof(float), + a_scales.data_ptr(), m * sizeof(float), + m * sizeof(float), num_groups, + cudaMemcpyDeviceToDevice, + get_current_cuda_stream(a.get_device())); + a_scales_data = a_scales_pad->data_ptr(); + } + } + StrideA a_stride; StrideB b_stride; StrideC c_stride; @@ -162,16 +185,16 @@ void cutlass_gemm_caller_blockwise(torch::stable::Tensor& out, torch::stable::Te c_stride = cutlass::make_cute_packed_stride(StrideC{}, swap_ab ? cute::make_shape(n, m, 1) : cute::make_shape(m, n, 1)); - LayoutSFA layout_SFA = swap_ab ? + LayoutSFA layout_SFA = swap_ab ? ScaleConfig::tile_atom_to_shape_SFA(make_shape(n, m, k, 1)) : - ScaleConfig::tile_atom_to_shape_SFA(make_shape(m, n, k, 1)); + ScaleConfig::tile_atom_to_shape_SFA(make_shape(m_sf, n, k, 1)); LayoutSFB layout_SFB = swap_ab ? ScaleConfig::tile_atom_to_shape_SFB(make_shape(n, m, k, 1)) : ScaleConfig::tile_atom_to_shape_SFB(make_shape(m, n, k, 1)); auto a_ptr = static_cast(a.data_ptr()); auto b_ptr = static_cast(b.data_ptr()); - auto a_scales_ptr = static_cast(a_scales.data_ptr()); + auto a_scales_ptr = static_cast(a_scales_data); auto b_scales_ptr = static_cast(b_scales.data_ptr()); typename GemmKernel::MainloopArguments mainloop_args{}; @@ -212,7 +235,9 @@ void cutlass_gemm_blockwise_sm100_fp8_dispatch(torch::stable::Tensor& out, constexpr int TILE_K = 128; // TODO: better heuristics - bool swap_ab = (m < 16) || (m % 4 != 0); + // swapAB only wins for small M; misaligned M >= 65 is faster on the + // non-swapAB path with padded activation scales (measured on GB300). + bool swap_ab = (m <= 64); bool use_tma_epilogue = (m * n) % 4 == 0; if (!swap_ab) { constexpr int TILE_N = 128; diff --git a/csrc/libtorch_stable/quantization/w8a8/fp8/per_token_group_quant.cu b/csrc/libtorch_stable/quantization/w8a8/fp8/per_token_group_quant.cu index 06fa29eaba29..cfd1fab1ad00 100644 --- a/csrc/libtorch_stable/quantization/w8a8/fp8/per_token_group_quant.cu +++ b/csrc/libtorch_stable/quantization/w8a8/fp8/per_token_group_quant.cu @@ -96,7 +96,8 @@ __device__ __forceinline__ void QuantizeGroup( } template + bool SCALE_UE8M0 = false, typename scale_packed_t = float, + bool CHECK_BOUNDS = false> __global__ void per_token_group_quant_8bit_kernel( const T* __restrict__ input, void* __restrict__ output_q, scale_packed_t* __restrict__ output_s, const int group_size, @@ -114,49 +115,54 @@ __global__ void per_token_group_quant_8bit_kernel( using scale_element_t = float; static_assert(sizeof(scale_packed_t) % sizeof(scale_element_t) == 0); - const T* group_input = input + block_group_offset; - DST_DTYPE* group_output = - static_cast(output_q) + block_group_offset; - scale_element_t* scale_output; - #if (defined(__CUDA_ARCH__) && (__CUDA_ARCH__ >= 900)) cudaGridDependencySynchronize(); #endif - if constexpr (IS_COLUMN_MAJOR) { - const int num_elems_per_pack = - static_cast(sizeof(scale_packed_t) / sizeof(scale_element_t)); - const int scale_num_rows_element = scale_num_rows * num_elems_per_pack; - const int row_idx = global_group_id / scale_num_rows_element; - const int col_idx_raw = global_group_id % scale_num_rows_element; - const int col_idx = col_idx_raw / num_elems_per_pack; - const int pack_idx = col_idx_raw % num_elems_per_pack; - scale_output = reinterpret_cast(output_s) + - (col_idx * scale_stride * num_elems_per_pack + - row_idx * num_elems_per_pack + pack_idx); - } else { - scale_output = output_s + global_group_id; - } - // shared memory to cache each group's data to avoid double DRAM reads. extern __shared__ __align__(16) char smem_raw[]; T* smem = reinterpret_cast(smem_raw); T* smem_group = smem + local_group_id * group_size; + const bool valid_group = !CHECK_BOUNDS || global_group_id < num_groups; + float y_s = 1.0f; + if (valid_group) { + const T* group_input = input + block_group_offset; + scale_element_t* scale_output; + + if constexpr (IS_COLUMN_MAJOR) { + const int num_elems_per_pack = + static_cast(sizeof(scale_packed_t) / sizeof(scale_element_t)); + const int scale_num_rows_element = scale_num_rows * num_elems_per_pack; + const int row_idx = global_group_id / scale_num_rows_element; + const int col_idx_raw = global_group_id % scale_num_rows_element; + const int col_idx = col_idx_raw / num_elems_per_pack; + const int pack_idx = col_idx_raw % num_elems_per_pack; + scale_output = reinterpret_cast(output_s) + + (col_idx * scale_stride * num_elems_per_pack + + row_idx * num_elems_per_pack + pack_idx); + } else { + scale_output = output_s + global_group_id; + } - const float y_s = ComputeGroupScale( - group_input, smem_group, group_size, lane_id, threads_per_group, eps, - max_8bit); + y_s = ComputeGroupScale(group_input, smem_group, group_size, + lane_id, threads_per_group, eps, + max_8bit); - scale_element_t y_s_quant = y_s; + scale_element_t y_s_quant = y_s; - if (lane_id == 0) { - *scale_output = y_s_quant; + if (lane_id == 0) { + *scale_output = y_s_quant; + } } __syncthreads(); - QuantizeGroup(smem_group, group_output, group_size, lane_id, - threads_per_group, y_s, min_8bit, max_8bit); + if (valid_group) { + DST_DTYPE* group_output = + static_cast(output_q) + block_group_offset; + QuantizeGroup(smem_group, group_output, group_size, lane_id, + threads_per_group, y_s, min_8bit, max_8bit); + } #if (defined(__CUDA_ARCH__) && (__CUDA_ARCH__ >= 900)) cudaTriggerProgrammaticLaunchCompletion(); @@ -209,10 +215,20 @@ void per_token_group_quant_8bit(const torch::stable::Tensor& input, constexpr int THREADS_PER_GROUP = 16; - const int groups_per_block = GetGroupsPerBlock(num_groups); + if (num_groups == 0) { + return; + } + STD_TORCH_CHECK(input.size(-1) % group_size == 0); + const int64_t groups_per_row = input.size(-1) / group_size; + const int64_t num_rows = num_groups / groups_per_row; + // Odd row counts inherit the next even row count's launch geometry. + const int64_t policy_num_groups = + (num_rows + (num_rows & 1)) * groups_per_row; + const int groups_per_block = GetGroupsPerBlock(policy_num_groups); + const bool needs_bounds_check = num_groups % groups_per_block != 0; auto dst_type = output_q.scalar_type(); - const int num_blocks = num_groups / groups_per_block; + const int num_blocks = (num_groups + groups_per_block - 1) / groups_per_block; const int num_threads = groups_per_block * THREADS_PER_GROUP; const bool is_column_major = output_s.stride(0) < output_s.stride(1); @@ -220,38 +236,51 @@ void per_token_group_quant_8bit(const torch::stable::Tensor& input, const int scale_stride = output_s.stride(1); #ifndef USE_ROCM - #define LAUNCH_KERNEL_INST(T, DST_DTYPE, COL_MAJOR, UE8M0, SMEM_BYTES) \ - do { \ - cudaLaunchConfig_t config = {}; \ - config.gridDim = dim3(num_blocks); \ - config.blockDim = dim3(num_threads); \ - config.dynamicSmemBytes = (SMEM_BYTES); \ - config.stream = stream; \ - cudaLaunchAttribute attrs[1]; \ - attrs[0].id = cudaLaunchAttributeProgrammaticStreamSerialization; \ - attrs[0].val.programmaticStreamSerializationAllowed = 1; \ - config.numAttrs = 1; \ - config.attrs = attrs; \ - cudaLaunchKernelEx( \ - &config, \ - per_token_group_quant_8bit_kernel, \ - static_cast(input.data_ptr()), output_q.data_ptr(), \ - static_cast(output_s.data_ptr()), group_size, num_groups, \ - groups_per_block, (float)eps, (float)min_8bit, (float)max_8bit, \ - scale_num_rows, scale_stride); \ + #define LAUNCH_KERNEL_IMPL(T, DST_DTYPE, COL_MAJOR, UE8M0, CHECK_BOUNDS, \ + SMEM_BYTES) \ + do { \ + cudaLaunchConfig_t config = {}; \ + config.gridDim = dim3(num_blocks); \ + config.blockDim = dim3(num_threads); \ + config.dynamicSmemBytes = (SMEM_BYTES); \ + config.stream = stream; \ + cudaLaunchAttribute attrs[1]; \ + attrs[0].id = cudaLaunchAttributeProgrammaticStreamSerialization; \ + attrs[0].val.programmaticStreamSerializationAllowed = 1; \ + config.numAttrs = 1; \ + config.attrs = attrs; \ + cudaLaunchKernelEx( \ + &config, \ + per_token_group_quant_8bit_kernel, \ + static_cast(input.data_ptr()), output_q.data_ptr(), \ + static_cast(output_s.data_ptr()), group_size, num_groups, \ + groups_per_block, (float)eps, (float)min_8bit, (float)max_8bit, \ + scale_num_rows, scale_stride); \ } while (0) #else - #define LAUNCH_KERNEL_INST(T, DST_DTYPE, COL_MAJOR, UE8M0, SMEM_BYTES) \ - do { \ - per_token_group_quant_8bit_kernel \ - <<>>( \ - static_cast(input.data_ptr()), output_q.data_ptr(), \ - static_cast(output_s.data_ptr()), group_size, \ - num_groups, groups_per_block, (float)eps, (float)min_8bit, \ - (float)max_8bit, scale_num_rows, scale_stride); \ + #define LAUNCH_KERNEL_IMPL(T, DST_DTYPE, COL_MAJOR, UE8M0, CHECK_BOUNDS, \ + SMEM_BYTES) \ + do { \ + per_token_group_quant_8bit_kernel \ + <<>>( \ + static_cast(input.data_ptr()), output_q.data_ptr(), \ + static_cast(output_s.data_ptr()), group_size, \ + num_groups, groups_per_block, (float)eps, (float)min_8bit, \ + (float)max_8bit, scale_num_rows, scale_stride); \ } while (0) #endif +#define LAUNCH_KERNEL_INST(T, DST_DTYPE, COL_MAJOR, UE8M0, SMEM_BYTES) \ + do { \ + if (needs_bounds_check) { \ + LAUNCH_KERNEL_IMPL(T, DST_DTYPE, COL_MAJOR, UE8M0, true, SMEM_BYTES); \ + } else { \ + LAUNCH_KERNEL_IMPL(T, DST_DTYPE, COL_MAJOR, UE8M0, false, SMEM_BYTES); \ + } \ + } while (0) + #define LAUNCH_KERNEL(T, DST_DTYPE) \ do { \ size_t smem_bytes = \ @@ -284,6 +313,7 @@ void per_token_group_quant_8bit(const torch::stable::Tensor& input, #undef LAUNCH_KERNEL #undef LAUNCH_KERNEL_INST +#undef LAUNCH_KERNEL_IMPL } // Register-resident fast path for group_size==128. diff --git a/csrc/libtorch_stable/sampled_topk.cuh b/csrc/libtorch_stable/sampled_topk.cuh new file mode 100644 index 000000000000..867abbd63542 --- /dev/null +++ b/csrc/libtorch_stable/sampled_topk.cuh @@ -0,0 +1,253 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright contributors to the vLLM project + +// Sampled top-k for long FP32 sparse-indexer rows, inspired by DeepSelect: +// https://github.com/deepseek-ai/DeepSelect +// A sample estimates a cutoff, then survivors are compacted in shared memory +// for exact FP32 selection. Too few or too many candidates trigger an exact +// full-row fallback. + +#pragma once + +#include +#include +#include + +#include "persistent_topk.cuh" + +namespace vllm::sampled_topk { + +constexpr int kThreads = 1024; +// Conservative crossover bounds from the B300 batch/length sweep. +template +constexpr int kMinSampledLength = K == 512 ? 98304 : 65536; +// Target half the buffer to leave room for sampling error. +constexpr int kSample = 4096; +constexpr int kCapacity = 8192; + +struct Storage { + uint2 candidates[kCapacity]; + int histogram[2048]; + typename cub::BlockScan::TempStorage scan; + int count; + int remaining; + int bin; + uint32_t prefix; +}; + +__device__ __forceinline__ uint32_t ordered(float value) { + const uint32_t bits = __float_as_uint(value); + return (bits & 0x80000000u) ? ~bits : (bits | 0x80000000u); +} + +__device__ __forceinline__ uint32_t coarse(uint32_t key) { + const uint32_t bits = (key & 0x80000000u) ? (key ^ 0x80000000u) : ~key; + return topk_histogram_4096::extract_coarse_bin_N<11>(__uint_as_float(bits)); +} + +// Select the bin containing the rank and update the rank within that bin. +template +__device__ void select_bin(Storage& s) { + constexpr int kItems = (Bins + kThreads - 1) / kThreads; + int sums[kItems]; +#pragma unroll + for (int j = 0; j < kItems; ++j) { + const int bin = Bins - 1 - (threadIdx.x * kItems + j); + sums[j] = bin >= 0 ? s.histogram[bin] : 0; + } + cub::BlockScan(s.scan).InclusiveSum(sums, sums); + const int remaining = s.remaining; + __syncthreads(); +#pragma unroll + for (int j = 0; j < kItems; ++j) { + const int bin = Bins - 1 - (threadIdx.x * kItems + j); + if (bin >= 0 && sums[j] >= remaining && + sums[j] - s.histogram[bin] < remaining) { + s.bin = bin; + s.remaining = remaining - (sums[j] - s.histogram[bin]); + } + } + __syncthreads(); +} + +// 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) { + const int tid = threadIdx.x; + if (tid == 0) { + s.remaining = rank; + s.prefix = 0; + } + for (int i = tid; i < 2048; i += kThreads) s.histogram[i] = 0; + __syncthreads(); + for (int i = tid; i < count; i += kThreads) { + const uint32_t key = Buffered ? s.candidates[i].y : ordered(row[i]); + atomicAdd(&s.histogram[coarse(key)], 1); + } + __syncthreads(); + select_bin<2048>(s); + const int coarse_bin = s.bin; + uint32_t mask = 0; + for (int shift = 24; shift >= 0; shift -= 8) { + if (tid < 256) s.histogram[tid] = 0; + __syncthreads(); + const uint32_t prefix = s.prefix; + for (int i = tid; i < count; i += kThreads) { + const uint32_t key = Buffered ? s.candidates[i].y : ordered(row[i]); + if (coarse(key) == coarse_bin && (key & mask) == prefix) { + atomicAdd(&s.histogram[(key >> shift) & 255], 1); + } + } + __syncthreads(); + select_bin<256>(s); + if (tid == 0) s.prefix = prefix | (static_cast(s.bin) << shift); + mask |= 255u << shift; + __syncthreads(); + } + return s.prefix; +} + +__device__ __forceinline__ int reserve(bool hit, int* counter) { + const uint32_t mask = __ballot_sync(0xffffffffu, hit); + if (mask == 0) return 0; + const int lane = threadIdx.x & 31; + int base = 0; + if (lane == 0) base = atomicAdd(counter, __popc(mask)); + base = __shfl_sync(0xffffffffu, base, 0); + return base + __popc(mask & ((1u << lane) - 1)); +} + +template +__device__ void emit(Storage& s, const float* row, int32_t* dst, int count, + uint32_t cutoff) { + const int tid = threadIdx.x; + if (tid == 0) s.count = 0; + __syncthreads(); + 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, ordered(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; + } + const int offset = reserve(keep, &s.count); + if (keep) dst[offset] = pair.x; + } +} + +template +__global__ void __launch_bounds__(kThreads) + sampled_topk_kernel(const float* __restrict__ input, + const int32_t* __restrict__ lengths, + int32_t* __restrict__ output, int64_t stride, + int max_length) { + extern __shared__ __align__(16) unsigned char smem[]; + 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; + int32_t* dst = output + blockIdx.x * K; + // Graph allocations can hold rows much shorter than the host length bound. + if (length < kMinSampledLength) { + __shared__ filtered_topk::FilteredTopKStorage fallback_storage; + bool selected; + if ((stride & 3) == 0 && (reinterpret_cast(row) & 15) == 0) { + selected = + filtered_topk::filtered_topk_row( + row, dst, length, K, fallback_storage); + } else { + selected = + filtered_topk::filtered_topk_row( + row, dst, length, K, fallback_storage); + } + if (!selected) { + const uint32_t exact_cutoff = threshold(s, row, length, K); + emit(s, row, dst, length, exact_cutoff); + } + return; + } + if (tid == 0) s.remaining = max(1, (kCapacity / 2) * kSample / length); + for (int i = tid; i < 2048; i += kThreads) s.histogram[i] = 0; + __syncthreads(); + // 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(ordered(row[index]))], 1); + } + __syncthreads(); + select_bin<2048>(s); + const uint16_t half_key = s.bin << 5; + const uint16_t half_bits = + (half_key & 0x8000u) ? (half_key ^ 0x8000u) : ~half_key; + const uint32_t cutoff = ordered(__half2float(__ushort_as_half(half_bits))); + if (tid == 0) s.count = 0; + __syncthreads(); + constexpr int kItems = 16; + // Compact survivors with one reservation per warp per tile. Keep counting + // beyond capacity so overflow triggers fallback instead of using a subset. + for (int base = 0; base < length; base += kItems * kThreads) { + uint32_t keys[kItems]; + uint32_t hit_mask = 0; + if ((reinterpret_cast(row) & 15) == 0 && + base + kItems * kThreads <= length) { +#pragma unroll + for (int j = 0; j < kItems / 4; ++j) { + const float4 values = + reinterpret_cast(row + base)[tid + j * kThreads]; + keys[4 * j] = ordered(values.x); + keys[4 * j + 1] = ordered(values.y); + keys[4 * j + 2] = ordered(values.z); + keys[4 * j + 3] = ordered(values.w); + } + } else { +#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 ? ordered(row[i]) : 0; + } + } +#pragma unroll + for (int j = 0; j < kItems; ++j) { + const int i = base + tid * 4 + (j / 4) * kThreads * 4 + j % 4; + if (i < length && keys[j] >= cutoff) hit_mask |= 1u << j; + } + const int lane = tid & 31; + const int hits = __popc(hit_mask); + int inclusive = hits; +#pragma unroll + for (int delta = 1; delta < 32; delta *= 2) { + const int previous = __shfl_up_sync(0xffffffffu, inclusive, delta); + if (lane >= delta) inclusive += previous; + } + const int total = __shfl_sync(0xffffffffu, inclusive, 31); + int warp_base = 0; + if (lane == 31 && total != 0) warp_base = atomicAdd(&s.count, total); + warp_base = __shfl_sync(0xffffffffu, warp_base, 31); + int offset = warp_base + inclusive - hits; +#pragma unroll + for (int j = 0; j < kItems; ++j) { + if (hit_mask & (1u << j)) { + if (offset < kCapacity) { + s.candidates[offset] = make_uint2( + base + tid * 4 + (j / 4) * kThreads * 4 + j % 4, keys[j]); + } + ++offset; + } + } + } + __syncthreads(); + const int count = s.count; + if (count >= K && count <= kCapacity) { + const uint32_t exact_cutoff = threshold(s, row, count, K); + emit(s, row, dst, count, exact_cutoff); + } else { + const uint32_t exact_cutoff = threshold(s, row, length, K); + emit(s, row, dst, length, exact_cutoff); + } +} + +} // namespace vllm::sampled_topk diff --git a/csrc/libtorch_stable/sampler.cu b/csrc/libtorch_stable/sampler.cu index ada1c4ba39c0..703a748ce686 100644 --- a/csrc/libtorch_stable/sampler.cu +++ b/csrc/libtorch_stable/sampler.cu @@ -45,11 +45,6 @@ __global__ void apply_repetition_penalties_kernel( } } -__device__ __forceinline__ auto convert_to_uint32(float x) -> uint32_t { - uint32_t bits = __float_as_uint(x); - return (bits & 0x80000000) ? bits : ~bits & 0x7fffffff; -} - template static inline __device__ uint32_t extractBinIdx(float x) { if constexpr (step == 0) { @@ -154,7 +149,8 @@ __device__ void vectorized_process(size_t thread_rank, size_t num_threads, } template __device__ bool processHistogramStep( const int* indices, const float* logits, int rowEnd, uint32_t& logitPattern, @@ -180,10 +176,15 @@ __device__ bool processHistogramStep( << patternShift; } + int negativeInfinityCount = 0; auto distributeToBins = [&](float logit, int /* idx */ = 0) { if (isPartialMatch(logit, logitPattern)) { uint32_t binIdx = extractBinIdx(logit); - atomicAdd(&smemFinal.histo.data[binIdx], 1); + if (useTopK512Optimization && __float_as_uint(logit) == 0xff800000u) { + ++negativeInfinityCount; + } else { + atomicAdd(&smemFinal.histo.data[binIdx], 1); + } } }; @@ -198,6 +199,12 @@ __device__ bool processHistogramStep( distributeToBins(logit, idx); } } + if constexpr (useTopK512Optimization) { + if (negativeInfinityCount > 0) { + const auto bin = extractBinIdx(-INFINITY); + atomicAdd(&smemFinal.histo.data[bin], negativeInfinityCount); + } + } // Make sure the histogram is ready. __syncthreads(); @@ -254,6 +261,12 @@ __device__ bool processHistogramStep( // The threshold bin. thresholdBinIdx = smemThresholdBinIdx[0]; + // Resolve a -inf cutoff with exact tie emission, keeping sort padding out. + const bool finalBinFits = + smemFinalBinSize[0] <= kNumFinalItems && + !(useTopK512Optimization && step < 3 && + isPartialMatch(-INFINITY, logitPattern) && + thresholdBinIdx == extractBinIdx(-INFINITY)); auto processBins = [&](float logit, int idx) { if (isPartialMatch(logit, logitPattern)) { @@ -262,8 +275,7 @@ __device__ bool processHistogramStep( // 1. This is step 0 and the threshold bin is small enough (no step 1) // 2. This is step >= 1 (where pattern matching filters correctly) // This prevents duplicates when step 0 and step 1 both run. - bool shouldWriteDirectly = - (step == 0 && smemFinalBinSize[0] <= kNumFinalItems) || (step >= 1); + bool shouldWriteDirectly = (step == 0 && finalBinFits) || (step >= 1); if (binIdx < thresholdBinIdx && shouldWriteDirectly) { // The element is part of the top-k selection int dstIdx = atomicAdd(&smemFoundTopKValues[0], 1); @@ -279,8 +291,7 @@ __device__ bool processHistogramStep( } if constexpr (step < 3) { // Only fill the final items for sorting if the threshold bin fits - if (binIdx == thresholdBinIdx && - smemFinalBinSize[0] <= kNumFinalItems) { + if (binIdx == thresholdBinIdx && finalBinFits) { int dstIdx = atomicAdd(&smemFinalDstIdx[0], 1); smemFinal.items.logits[dstIdx] = logit; if constexpr (mergeBlocks) { @@ -325,20 +336,21 @@ __device__ bool processHistogramStep( __syncthreads(); // Check if we should continue to next step - return smemFinalBinSize[0] > kNumFinalItems; + return !finalBinFits; } // Follows half - 11 - 11 - 10 bit iterations // Keep the adaptive kernel's device instantiation separate from legacy calls. template + bool deviceLengthAware = false, bool useTopK512Optimization = false> static __device__ void topKPerRowJob(const int* indices, const float* logits, int rowStart, int rowEnd, int* outIndices, float* outLogits, int stride1, int topK) { static_assert(!deviceLengthAware || multipleBlocksPerRow != mergeBlocks); + static_assert(!useTopK512Optimization || kNumThreadsPerBlock == 1024); // The number of slots for the final pass. - static constexpr int kNumFinalItems = 2048; + static constexpr int kNumFinalItems = useTopK512Optimization ? 1024 : 2048; // The number of elements per thread for the final sort. static constexpr int kNumFinalItemsPerThread = kNumFinalItems / kNumThreadsPerBlock; @@ -404,7 +416,7 @@ static __device__ void topKPerRowJob(const int* indices, const float* logits, rowIt += kNumThreadsPerBlock) { outIndices[rowIt] = -1; if constexpr (multipleBlocksPerRow) { - outLogits[rowIt] = -FLT_MAX; + outLogits[rowIt] = useTopK512Optimization ? -INFINITY : -FLT_MAX; } } @@ -422,7 +434,8 @@ static __device__ void topKPerRowJob(const int* indices, const float* logits, // Step 0: Process first 11 bits of half representation bool continueToNextStep = processHistogramStep<0, kNumThreadsPerBlock, kNumBins, kNumFinalItems, - multipleBlocksPerRow, mergeBlocks>( + multipleBlocksPerRow, mergeBlocks, + useTopK512Optimization>( indices, logits, rowEnd, logitPattern, thresholdBinIdx, smemOutput, smemThresholdBinIdx, smemFinalDstIdx, smemFinalBinSize, smemFoundTopKValues, smemFinal, stride1, rowStart, topK); @@ -431,7 +444,8 @@ static __device__ void topKPerRowJob(const int* indices, const float* logits, // Step 1: Process next 11 bits continueToNextStep = processHistogramStep<1, kNumThreadsPerBlock, kNumBins, kNumFinalItems, - multipleBlocksPerRow, mergeBlocks>( + multipleBlocksPerRow, mergeBlocks, + useTopK512Optimization>( indices, logits, rowEnd, logitPattern, thresholdBinIdx, smemOutput, smemThresholdBinIdx, smemFinalDstIdx, smemFinalBinSize, smemFoundTopKValues, smemFinal, stride1, rowStart, topK); @@ -441,7 +455,8 @@ static __device__ void topKPerRowJob(const int* indices, const float* logits, // Step 2: Process next 11 bits continueToNextStep = processHistogramStep<2, kNumThreadsPerBlock, kNumBins, kNumFinalItems, - multipleBlocksPerRow, mergeBlocks>( + multipleBlocksPerRow, mergeBlocks, + useTopK512Optimization>( indices, logits, rowEnd, logitPattern, thresholdBinIdx, smemOutput, smemThresholdBinIdx, smemFinalDstIdx, smemFinalBinSize, smemFoundTopKValues, smemFinal, stride1, rowStart, topK); @@ -450,7 +465,8 @@ static __device__ void topKPerRowJob(const int* indices, const float* logits, if (continueToNextStep) { // Step 3: Process last 10 bits processHistogramStep<3, kNumThreadsPerBlock, kNumBins, kNumFinalItems, - multipleBlocksPerRow, mergeBlocks>( + multipleBlocksPerRow, mergeBlocks, + useTopK512Optimization>( indices, logits, rowEnd, logitPattern, thresholdBinIdx, smemOutput, smemThresholdBinIdx, smemFinalDstIdx, smemFinalBinSize, smemFoundTopKValues, smemFinal, stride1, rowStart, topK); @@ -460,50 +476,7 @@ static __device__ void topKPerRowJob(const int* indices, const float* logits, // The histogram did not proceed to the final 10 bits, therefore we need to // sort the final items The logits of the elements to be sorted in the final // pass. - if constexpr (useRadixSort) { - // Sorting with radix sort - float finalLogits[kNumFinalItemsPerThread]; - // The indices of the elements to be sorted in the final pass. - int finalIndices[kNumFinalItemsPerThread]; - -#pragma unroll - for (int ii = 0; ii < kNumFinalItemsPerThread; ++ii) { - finalLogits[ii] = -FLT_MAX; - } - - // Read the elements from SMEM. -#pragma unroll - for (int ii = 0; ii < kNumFinalItemsPerThread; ++ii) { - int srcIdx = ii * kNumThreadsPerBlock + threadIdx.x; - if (srcIdx < smemFinalDstIdx[0]) { - finalLogits[ii] = smemFinal.items.logits[srcIdx]; - finalIndices[ii] = smemFinal.items.indices[srcIdx]; - } - } - // Make sure the shared memory has been read. - __syncthreads(); - - // Sort the elements. - FinalSort(smemFinal.finalSort) - .SortDescendingBlockedToStriped(finalLogits, finalIndices); - - // Copy the data back to the shared memory storage. - int baseIdx = smemFoundTopKValues[0]; - -#pragma unroll - for (int ii = 0; ii < kNumFinalItemsPerThread; ++ii) { - int srcIdx = ii * kNumThreadsPerBlock + threadIdx.x; - int dstIdx = baseIdx + srcIdx; - - if (dstIdx < topK) { - smemOutput[dstIdx] = finalIndices[ii]; - if constexpr (multipleBlocksPerRow) { - reinterpret_cast(smemOutput + topK)[dstIdx] = - finalLogits[ii]; - } - } - } - } else { + auto insertionSort = [&]() { // Sorting with insertion sort auto baseIdx = smemFoundTopKValues[0]; for (int i = threadIdx.x; i < smemFinalDstIdx[0]; @@ -525,6 +498,57 @@ static __device__ void topKPerRowJob(const int* indices, const float* logits, } } } + }; + if constexpr (useRadixSort) { + if (useTopK512Optimization && smemFinalDstIdx[0] <= 128) { + insertionSort(); + } else { + // Sorting with radix sort + float finalLogits[kNumFinalItemsPerThread]; + // The indices of the elements to be sorted in the final pass. + int finalIndices[kNumFinalItemsPerThread]; + +#pragma unroll + for (int ii = 0; ii < kNumFinalItemsPerThread; ++ii) { + finalLogits[ii] = useTopK512Optimization ? -INFINITY : -FLT_MAX; + if constexpr (useTopK512Optimization) finalIndices[ii] = -1; + } + + // Read the elements from SMEM. +#pragma unroll + for (int ii = 0; ii < kNumFinalItemsPerThread; ++ii) { + int srcIdx = ii * kNumThreadsPerBlock + threadIdx.x; + if (srcIdx < smemFinalDstIdx[0]) { + finalLogits[ii] = smemFinal.items.logits[srcIdx]; + finalIndices[ii] = smemFinal.items.indices[srcIdx]; + } + } + // Make sure the shared memory has been read. + __syncthreads(); + + // Sort the elements. + FinalSort(smemFinal.finalSort) + .SortDescendingBlockedToStriped(finalLogits, finalIndices); + + // Copy the data back to the shared memory storage. + int baseIdx = smemFoundTopKValues[0]; + +#pragma unroll + for (int ii = 0; ii < kNumFinalItemsPerThread; ++ii) { + int srcIdx = ii * kNumThreadsPerBlock + threadIdx.x; + int dstIdx = baseIdx + srcIdx; + + if (dstIdx < topK) { + smemOutput[dstIdx] = finalIndices[ii]; + if constexpr (multipleBlocksPerRow) { + reinterpret_cast(smemOutput + topK)[dstIdx] = + finalLogits[ii]; + } + } + } + } + } else { + insertionSort(); } __syncthreads(); } @@ -669,6 +693,70 @@ __launch_bounds__(kNumThreadsPerBlock) void topKPerRowDecodeDeviceLengthAware( stride1, topK); } +#ifdef USE_ROCM +// The grid and workspace stay fixed while graph replays change device lengths. +__device__ int gfx950TopK512ActiveBlocks(int rowLength, int numRows, + int maxBlocks) { + int activeBlocks = 1; + if (numRows <= 32 && rowLength > 16 * 1024) { + activeBlocks = rowLength <= 128 * 1024 ? 8 + : rowLength <= 256 * 1024 ? 10 + : 16; + } else if (numRows <= 128 && rowLength > 64 * 1024) { + activeBlocks = rowLength <= 512 * 1024 ? 4 : 10; + } else if (numRows > 128 && numRows <= 256 && rowLength > 256 * 1024) { + activeBlocks = numRows <= 160 ? 3 : 2; + } + // Every block in a merged row must contain at least k real indices. + return min(min(activeBlocks, maxBlocks), max(1, rowLength / 512)); +} + +template +static __global__ +__launch_bounds__(kNumThreadsPerBlock) void topKPerRowDecode512DeviceLengthAware( + const float* logits, const int* seqLens, int* outIndices, + int* outIndicesAux, float* outLogitsAux, int stride0, int next_n, + int seqLensIs2D, int maxBlocks) { + #if defined(__gfx950__) + constexpr int topK = 512; + constexpr int kNumBins = 2048; + const int rowIdx = blockIdx.x; + const int rowEnd = + seqLensIs2D + ? max(0, seqLens[rowIdx]) + : max(0, seqLens[rowIdx / next_n] - next_n + rowIdx % next_n + 1); + const int activeBlocks = + gfx950TopK512ActiveBlocks(rowEnd, gridDim.x, maxBlocks); + outIndices += static_cast(rowIdx) * topK; + if constexpr (mergeBlocks) { + if (activeBlocks == 1) return; + const int64_t offset = static_cast(rowIdx) * maxBlocks * topK; + topKPerRowJob(outIndicesAux + offset, outLogitsAux + offset, 0, + activeBlocks * topK, outIndices, nullptr, 1, topK); + } else { + if (blockIdx.y >= activeBlocks) return; + logits += static_cast(rowIdx) * stride0; + if (activeBlocks == 1) { + topKPerRowJob(nullptr, logits, 0, rowEnd, outIndices, nullptr, 1, + topK); + return; + } + const int blockSize = rowEnd / activeBlocks; + const int rowStart = blockSize * blockIdx.y; + const int blockRowEnd = + blockIdx.y + 1 == activeBlocks ? rowEnd : rowStart + blockSize; + const int64_t offset = + (static_cast(rowIdx) * maxBlocks + blockIdx.y) * topK; + topKPerRowJob(nullptr, logits, rowStart, blockRowEnd, + outIndicesAux + offset, outLogitsAux + offset, 1, topK); + } + #endif +} +#endif + } // namespace vllm void apply_repetition_penalties_( @@ -731,6 +819,60 @@ void top_k_per_row_decode(const torch::stable::Tensor& logits, int64_t next_n, // the same seq_len and the kernel computes the per-row offset itself. int seqLensIs2D = seqLens.dim() == 2 ? 1 : 0; +#ifdef USE_ROCM + const bool useGfx950DeviceLengthAwareTopK512 = + topK == 512 && stride1 == 1 && numRows > 0 && numRows <= 384 && + numColumns <= 1024 * 1024 && + std::strncmp(get_device_prop()->gcnArchName, "gfx950", 6) == 0; + if (useGfx950DeviceLengthAwareTopK512) { + int multipleBlocksPerRowConfig = 1; + if (numRows <= 32 && numColumns > 16 * 1024) { + multipleBlocksPerRowConfig = 16; + } else if (numRows <= 128 && numColumns > 64 * 1024) { + multipleBlocksPerRowConfig = 10; + } else if (numRows <= 160 && numColumns > 256 * 1024) { + multipleBlocksPerRowConfig = 3; + } else if (numRows <= 256 && numColumns > 256 * 1024) { + multipleBlocksPerRowConfig = 2; + } + + constexpr int kNumThreadsPerSplitBlock = 1024; + if (multipleBlocksPerRowConfig == 1) { + vllm::topKPerRowDecode512DeviceLengthAware + <<>>(logits.const_data_ptr(), + seqLens.const_data_ptr(), + indices.mutable_data_ptr(), nullptr, nullptr, + static_cast(stride0), static_cast(next_n), + seqLensIs2D, multipleBlocksPerRowConfig); + return; + } + const auto outIndicesAux = torch::stable::empty( + {numRows, multipleBlocksPerRowConfig, topK}, + torch::headeronly::ScalarType::Int, std::nullopt, logits.device()); + const auto outLogitsAux = torch::stable::empty( + {numRows, multipleBlocksPerRowConfig, topK}, + torch::headeronly::ScalarType::Float, std::nullopt, logits.device()); + vllm::topKPerRowDecode512DeviceLengthAware + <<>>( + logits.const_data_ptr(), seqLens.const_data_ptr(), + indices.mutable_data_ptr(), + outIndicesAux.mutable_data_ptr(), + outLogitsAux.mutable_data_ptr(), static_cast(stride0), + static_cast(next_n), seqLensIs2D, multipleBlocksPerRowConfig); + constexpr int kNumThreadsPerBlockMerge = 1024; + vllm::topKPerRowDecode512DeviceLengthAware + <<>>( + nullptr, seqLens.const_data_ptr(), + indices.mutable_data_ptr(), + outIndicesAux.mutable_data_ptr(), + outLogitsAux.mutable_data_ptr(), 0, static_cast(next_n), + seqLensIs2D, multipleBlocksPerRowConfig); + return; + } +#endif + if (numColumns < kSortingAlgorithmThreshold) { // Use insertion sort vllm::topKPerRowDecode diff --git a/csrc/libtorch_stable/topk.cu b/csrc/libtorch_stable/topk.cu index b3e1a8623f3e..fb3d946b96b0 100644 --- a/csrc/libtorch_stable/topk.cu +++ b/csrc/libtorch_stable/topk.cu @@ -9,6 +9,7 @@ #ifndef USE_ROCM #include "persistent_topk.cuh" + #include "sampled_topk.cuh" #endif namespace { @@ -36,7 +37,22 @@ void launch_persistent_topk(const torch::stable::Tensor& logits, max_smem_per_block = device_prop->sharedMemPerBlockOptin; } - if (num_rows > 32 && max_smem_per_block >= 128 * 1024) { + // 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; + constexpr size_t smem_size = + vllm::filtered_topk::FILTERED_TOPK_SMEM_DYNAMIC; + cudaError_t status = cudaFuncSetAttribute( + kernel, cudaFuncAttributeMaxDynamicSharedMemorySize, smem_size); + STD_TORCH_CHECK(status == cudaSuccess, + "sampled_topk smem failed: ", cudaGetErrorString(status)); + kernel<<>>( + logits.const_data_ptr(), 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(), diff --git a/csrc/libtorch_stable/torch_bindings.cpp b/csrc/libtorch_stable/torch_bindings.cpp index 3781eeaebf26..3833c95fde00 100644 --- a/csrc/libtorch_stable/torch_bindings.cpp +++ b/csrc/libtorch_stable/torch_bindings.cpp @@ -327,21 +327,6 @@ STABLE_TORCH_LIBRARY_FRAGMENT(_C, ops) { // BF16/FP32 x FP32 -> FP32 router GEMM for H=3072, E=256, M<=32 (SM90+). // conditionally compiled so impl registration is in source file ops.def("fp32_router_gemm(Tensor! output, Tensor mat_a, Tensor mat_b) -> ()"); - - // reorder weight for AllSpark Ampere W8A16 Fused Gemm kernel - ops.def( - "rearrange_kn_weight_as_n32k16_order(Tensor b_qweight, Tensor b_scales, " - "Tensor? b_zeros, " - "bool has_zp, Tensor! b_qweight_reorder, Tensor! b_scales_reorder, " - "Tensor!? b_zeros_reorder, " - "int K, int N, int N_32align) -> ()"); - - // AllSpark quantization ops - ops.def( - "allspark_w8a16_gemm(Tensor a, Tensor b_qweight, Tensor b_scales, " - "Tensor? b_qzeros, " - "SymInt n, SymInt group_size, SymInt sm_count, SymInt sm_version, SymInt " - "CUBLAS_M_THRESHOLD, bool has_zp, bool n32k16_reorder) -> Tensor"); #endif // Merge attn states @@ -425,12 +410,25 @@ STABLE_TORCH_LIBRARY_FRAGMENT(_C, ops) { "bool is_neox, Tensor position_ids, " "int forced_token_heads_per_warp=-1) -> ()"); + // q_head_padded is the padded Q head count of the returned tensor, or 0 to + // do the KV insert alone and return an empty tensor. The Q knobs are + // independent: apply_q_norm and apply_q_rope each drop that step for Q + // alone (KV is always rotated), and is_q_interleaved reads and writes Q in + // FlashMLA's mega-attention chunk-interleaved layout, which moves the + // padding heads to the tail of every head-dim chunk. + ops.def( + "fused_deepseek_v4_kv_rope_insert(" + "Tensor kv, Tensor! k_cache, Tensor slot_mapping, Tensor position_ids, " + "Tensor cos_sin_cache, int cache_block_size, Tensor? fp8_scale=None, " + "bool kv_mxfp8=False) -> " + "()"); ops.def( "fused_deepseek_v4_qnorm_rope_kv_rope_quant_insert(" "Tensor q_in, Tensor kv, Tensor! k_cache, " "Tensor slot_mapping, Tensor position_ids, Tensor cos_sin_cache, " "int q_head_padded, float eps, int cache_block_size, " - "bool apply_q_norm=True) -> Tensor"); + "bool apply_q_norm=True, bool kv_mxfp8=False, bool apply_q_rope=True, " + "bool is_q_interleaved=False) -> Tensor"); // FlashInfer V4 full-cache variants: write Q in place (bf16) or to a separate // FP8 tensor, and KV into a contiguous 512-wide token-strided cache. @@ -781,9 +779,6 @@ STABLE_TORCH_LIBRARY_IMPL(_C, CUDA, ops) { // DSV3 fused A GEMM: conditionally compiled so impl registration is in // source file (dsv3_fused_a_gemm.cu) - - // AllSpark ops: conditionally compiled so impl registrations are in source - // files (allspark_repack.cu and allspark_qgemm_w8a16.cu) #endif ops.impl("merge_attn_states", TORCH_BOX(&merge_attn_states)); @@ -807,6 +802,8 @@ STABLE_TORCH_LIBRARY_IMPL(_C, CUDA, ops) { // Positional encoding kernels (shared CUDA/ROCm) ops.impl("rotary_embedding", TORCH_BOX(&rotary_embedding)); ops.impl("fused_qk_norm_rope", TORCH_BOX(&fused_qk_norm_rope)); + ops.impl("fused_deepseek_v4_kv_rope_insert", + TORCH_BOX(&fused_deepseek_v4_kv_rope_insert)); ops.impl("fused_deepseek_v4_qnorm_rope_kv_rope_quant_insert", TORCH_BOX(&fused_deepseek_v4_qnorm_rope_kv_rope_quant_insert)); ops.impl( @@ -976,14 +973,68 @@ STABLE_TORCH_LIBRARY_FRAGMENT(_C_cache_ops, ops) { " str kv_cache_dtype," " Tensor scale) -> ()"); - // Grouped concat_and_cache_mla across all layers (bf16 only). Each - // layer's cache base pointer is read from kv_cache_ptrs. + // Grouped concat_and_cache_mla across all layers. Each layer's cache base + // pointer and optional plain-FP8 scale are read from device tensors. ops.def( "concat_and_cache_mla_grouped(Tensor kv_c, Tensor k_pe," " Tensor kv_cache_ptrs," " Tensor slot_mapping," " int block_size, int block_stride," - " int entry_stride) -> ()"); + " int entry_stride," + " Tensor? kv_scales=None," + " str kv_cache_dtype='auto') -> ()"); + +#ifndef USE_ROCM + ops.def( + "hisparse_resolve_residency(Tensor host_cache," + " Tensor! hot_cache," + " Tensor hot_block_table," + " Tensor global_indices," + " Tensor! hot_indices," + " Tensor! device_global_indices," + " Tensor! lru_slots," + " Tensor? request_state_indices," + " int region_stride," + " Tensor(a!)? miss_mask=None," + " Tensor(b!)? stats=None," + " Tensor(c!)? attention_indices=None," + " int attention_block_stride=0," + " Tensor? request_ids=None," + " Tensor? source_block_table=None," + " int source_block_size=0," + " Tensor(d!)? resolved_global_indices=None," + " Tensor(e!)? valid_counts=None," + " Tensor(f!)? swap_host_physical_rows=None," + " Tensor(g!)? swap_device_physical_rows=None," + " Tensor(h!)? swap_counts=None," + " Tensor? resident_block_table=None," + " int resident_block_size=0," + " int resident_null_block=0) -> ()"); + + ops.def( + "hisparse_invalidate_written_slots(Tensor! device_global_indices," + " Tensor request_state_indices," + " Tensor req_id_per_token," + " Tensor written_slots) -> ()"); + + ops.def( + "hisparse_gather_plan(Tensor host_cache," + " Tensor! hot_cache," + " Tensor global_indices," + " Tensor hot_indices," + " Tensor miss_mask," + " Tensor? request_state_indices," + " Tensor(a!)? attention_indices=None," + " int attention_block_stride=0) -> ()"); + + ops.def( + "hisparse_gather_compact(Tensor host_cache," + " Tensor! hot_cache," + " Tensor miss_global_indices," + " Tensor miss_hot_indices," + " Tensor miss_counts) -> ()"); + +#endif // !USE_ROCM // Rotate Q and K, then write to kv cache for MLA ops.def( @@ -1021,7 +1072,8 @@ STABLE_TORCH_LIBRARY_FRAGMENT(_C_cache_ops, ops) { ops.def( "cp_gather_and_upconvert_fp8_kv_cache(Tensor src_cache, Tensor! dst, " "Tensor block_table, Tensor workspace_starts, int batch_size, Tensor? " - "seq_starts) -> ()"); + "seq_starts, Tensor? host_cache=None, Tensor? host_row_ids=None, Tensor? " + "device_row_ids=None) -> ()"); ops.def( "cp_gather_and_upconvert_nvfp4_kv_cache(Tensor src_cache, Tensor! dst, " @@ -1089,6 +1141,15 @@ STABLE_TORCH_LIBRARY_IMPL(_C_cache_ops, CUDA, ops) { ops.impl("concat_and_cache_mla", TORCH_BOX(&concat_and_cache_mla)); ops.impl("concat_and_cache_mla_grouped", TORCH_BOX(&concat_and_cache_mla_grouped)); + +#ifndef USE_ROCM + ops.impl("hisparse_resolve_residency", + TORCH_BOX(&hisparse_resolve_residency)); + ops.impl("hisparse_invalidate_written_slots", + TORCH_BOX(&hisparse_invalidate_written_slots)); + ops.impl("hisparse_gather_plan", TORCH_BOX(&hisparse_gather_plan)); + ops.impl("hisparse_gather_compact", TORCH_BOX(&hisparse_gather_compact)); +#endif // !USE_ROCM ops.impl("concat_and_cache_mla_rope_fused", TORCH_BOX(&concat_and_cache_mla_rope_fused)); ops.impl("convert_fp8", TORCH_BOX(&convert_fp8)); diff --git a/csrc/quantization/w8a8/fp8/amd/quant_utils.cuh b/csrc/quantization/w8a8/fp8/amd/quant_utils.cuh index 58d19de73494..32925c0c3c95 100644 --- a/csrc/quantization/w8a8/fp8/amd/quant_utils.cuh +++ b/csrc/quantization/w8a8/fp8/amd/quant_utils.cuh @@ -67,104 +67,6 @@ using fp8_type = __hip_fp8_e4m3_fnuz; using fp8x2_type = __hip_fp8x2_e4m3_fnuz; #endif -// fp8 -> half -template <> -__inline__ __device__ uint16_t -vec_conversion(const uint8_t& a) { - return __hip_cvt_fp8_to_halfraw(a, fp8_type::__default_interpret).x; -} - -// fp8x2 -> half2 -template <> -__inline__ __device__ uint32_t -vec_conversion(const uint16_t& a) { - union { - __half2_raw h2r; - uint32_t ui32; - } tmp; - tmp.h2r = __hip_cvt_fp8x2_to_halfraw2(a, fp8_type::__default_interpret); - return tmp.ui32; -} - -// fp8x4 -> half2x2 -template <> -__inline__ __device__ uint2 vec_conversion(const uint32_t& a) { - union { - uint2 u32x2; - uint32_t u32[2]; - } tmp; - tmp.u32[0] = vec_conversion((uint16_t)a); - tmp.u32[1] = vec_conversion((uint16_t)(a >> 16U)); - return tmp.u32x2; -} - -// fp8x8 -> half2x4 -template <> -__inline__ __device__ uint4 vec_conversion(const uint2& a) { - union { - uint4 u64x2; - uint2 u64[2]; - } tmp; - tmp.u64[0] = vec_conversion(a.x); - tmp.u64[1] = vec_conversion(a.y); - return tmp.u64x2; -} - -using __nv_bfloat16 = __hip_bfloat16; - -// fp8 -> __nv_bfloat16 -template <> -__inline__ __device__ __nv_bfloat16 -vec_conversion<__nv_bfloat16, uint8_t>(const uint8_t& a) { - fp8_type f8; - f8.__x = a; - return __float2bfloat16(static_cast(f8)); -} - -using __nv_bfloat162 = __hip_bfloat162; - -// fp8x2 -> __nv_bfloat162 -template <> -__inline__ __device__ __nv_bfloat162 -vec_conversion<__nv_bfloat162, uint16_t>(const uint16_t& a) { - __nv_bfloat162 res; - res.x = vec_conversion<__nv_bfloat16, uint8_t>((uint8_t)a); - res.y = vec_conversion<__nv_bfloat16, uint8_t>((uint8_t)(a >> 8U)); - return res; -} - -// fp8x4 -> bf16_4_t -template <> -__inline__ __device__ bf16_4_t -vec_conversion(const uint32_t& a) { - bf16_4_t res; - res.x = vec_conversion<__nv_bfloat162, uint16_t>((uint16_t)a); - res.y = vec_conversion<__nv_bfloat162, uint16_t>((uint16_t)(a >> 16U)); - return res; -} - -// fp8x8 -> bf16_8_t -template <> -__inline__ __device__ bf16_8_t vec_conversion(const uint2& a) { - bf16_4_t tmp1, tmp2; - tmp1 = vec_conversion(a.x); - tmp2 = vec_conversion(a.y); - bf16_8_t res; - res.x = tmp1.x; - res.y = tmp1.y; - res.z = tmp2.x; - res.w = tmp2.y; - return res; -} - -// fp8 -> float -template <> -__inline__ __device__ float vec_conversion(const uint8_t& a) { - fp8_type f8; - f8.__x = a; - return static_cast(f8); -} - // fp8x2 -> float2 template <> __inline__ __device__ float2 @@ -193,138 +95,6 @@ vec_conversion(const uint32_t& a) { return res; } -// fp8x8 -> float8 -template <> -__inline__ __device__ Float8_ vec_conversion(const uint2& a) { - Float4_ tmp1, tmp2; - tmp1 = vec_conversion(a.x); - tmp2 = vec_conversion(a.y); - Float8_ res; - res.x = tmp1.x; - res.y = tmp1.y; - res.z = tmp2.x; - res.w = tmp2.y; - return res; -} - -// half -> fp8 -template <> -__inline__ __device__ uint8_t -vec_conversion(const uint16_t& a) { - __half_raw tmp; - tmp.x = a; - return __hip_cvt_halfraw_to_fp8(tmp, fp8_type::__default_saturation, - fp8_type::__default_interpret); -} - -template <> -__inline__ __device__ uint16_t -vec_conversion(const uint32_t& a) { - union { - uint32_t ui32; - __half2_raw h2r; - } tmp; - tmp.ui32 = a; - return __hip_cvt_halfraw2_to_fp8x2(tmp.h2r, fp8_type::__default_saturation, - fp8_type::__default_interpret); -} - -// bf16 -> fp8 -template <> -__inline__ __device__ uint8_t -vec_conversion(const __nv_bfloat16& a) { - return __hip_cvt_float_to_fp8(__bfloat162float(a), - fp8_type::__default_saturation, - fp8_type::__default_interpret); -} - -// float -> fp8 -template <> -__inline__ __device__ uint8_t vec_conversion(const float& a) { - return __hip_cvt_float_to_fp8(a, fp8_type::__default_saturation, - fp8_type::__default_interpret); -} - -// float2 -> half2 -template <> -__inline__ __device__ uint32_t -vec_conversion(const float2& a) { - union { - half2 float16; - uint32_t uint32; - }; - - float16 = __float22half2_rn(a); - return uint32; -} - -// Float4 -> half2x2 -template <> -__inline__ __device__ uint2 vec_conversion(const Float4_& a) { - uint2 b; - float2 val; - val.x = a.x.x; - val.y = a.x.y; - b.x = vec_conversion(val); - - val.x = a.y.x; - val.y = a.y.y; - b.y = vec_conversion(val); - return b; -} - -// Float4 -> float4 -template <> -__inline__ __device__ float4 vec_conversion(const Float4_& a) { - float4 b; - b.x = a.x.x; - b.y = a.x.y; - b.z = a.y.x; - b.w = a.y.y; - return b; -} - -// Float8 -> half2x4 -template <> -__inline__ __device__ uint4 vec_conversion(const Float8_& a) { - uint4 b; - b.x = vec_conversion(a.x); - b.y = vec_conversion(a.y); - b.z = vec_conversion(a.z); - b.w = vec_conversion(a.w); - return b; -} - -// float2 -> bfloat162 -template <> -__inline__ __device__ __nv_bfloat162 -vec_conversion<__nv_bfloat162, float2>(const float2& a) { - __nv_bfloat162 b = __float22bfloat162_rn(a); - return b; -} - -// Float4 -> bfloat162x2 -template <> -__inline__ __device__ bf16_4_t -vec_conversion(const Float4_& a) { - bf16_4_t b; - b.x = __float22bfloat162_rn(a.x); - b.y = __float22bfloat162_rn(a.y); - return b; -} - -// Float8 -> bfloat162x4 -template <> -__inline__ __device__ bf16_8_t -vec_conversion(const Float8_& a) { - bf16_8_t b; - b.x = __float22bfloat162_rn(a.x); - b.y = __float22bfloat162_rn(a.y); - b.z = __float22bfloat162_rn(a.z); - b.w = __float22bfloat162_rn(a.w); - return b; -} - /* Scaled and vectorized conversions, for data exchange between high and low precision domains @@ -335,6 +105,7 @@ vec_conversion(const Float8_& a) { */ using __nv_bfloat16 = __hip_bfloat16; +using __nv_bfloat162 = __hip_bfloat162; // fp8 -> __nv_bfloat16 template <> @@ -613,17 +384,6 @@ scaled_vec_conversion(const float4& a, float scale) { } #endif // ENABLE_FP8 -template -__inline__ __device__ Tout convert(const Tin& x) { - #ifdef ENABLE_FP8 - if constexpr (kv_dt == Fp8KVCacheDataType::kFp8E4M3) { - return vec_conversion(x); - } - #endif - assert(false); - return {}; // Squash missing return statement warning -} - template __inline__ __device__ Tout scaled_convert(const Tin& x, const float scale) { #ifdef ENABLE_FP8 diff --git a/csrc/quantization/w8a8/fp8/nvidia/quant_utils.cuh b/csrc/quantization/w8a8/fp8/nvidia/quant_utils.cuh index 11a9b0818dc6..7fcf541fd64d 100644 --- a/csrc/quantization/w8a8/fp8/nvidia/quant_utils.cuh +++ b/csrc/quantization/w8a8/fp8/nvidia/quant_utils.cuh @@ -282,19 +282,6 @@ __inline__ __device__ float4 scaled_vec_conversion( } #endif // ENABLE_FP8 -template -__inline__ __device__ Tout convert(const Tin& x) { - #if 0 // Disable the following code to reduce the binary size. - if constexpr (kv_dt == Fp8KVCacheDataType::kFp8E4M3) { - return vec_conversion(x, __NV_E4M3); - } else if constexpr (kv_dt == Fp8KVCacheDataType::kFp8E5M2) { - return vec_conversion(x, __NV_E5M2); - } - #endif - assert(false); - __builtin_unreachable(); // Suppress missing return statement warning -} - template __inline__ __device__ Tout scaled_convert(const Tin& x, const float scale) { #ifdef ENABLE_FP8 diff --git a/csrc/rocm/attention.cu b/csrc/rocm/attention.cu index b747b870e23f..2a11653a58d7 100644 --- a/csrc/rocm/attention.cu +++ b/csrc/rocm/attention.cu @@ -342,22 +342,6 @@ typedef union u64_cvt { int64_t i64; } _T8x8; -__device__ __forceinline__ _B8x8 convert_b16x8(const _B16x8& input, - _T8x8& Mtemp) { - _T8x8 Qtmp8x8; - - for (int i = 0; i < 2; i++) { - floatx4 q_out = {0, 0, 0, 0}; - q_out = gcn_mfma16x16x16_instr<_Float16, 0, 0, 0>(Mtemp.b64, input.xy[i], - q_out); - Qtmp8x8.b16x4[i * 2] = - __builtin_amdgcn_cvt_pk_fp8_f32(q_out[0], q_out[1], 0, false); - Qtmp8x8.b16x4[i * 2 + 1] = - __builtin_amdgcn_cvt_pk_fp8_f32(q_out[2], q_out[3], 0, false); - } - return Qtmp8x8.b8x8; -} - __device__ float warpReduceMax(float val) { for (int offset = warpSize / 2; offset > 0; offset /= 2) { val = max( @@ -2583,23 +2567,6 @@ __device__ __forceinline__ float to_float(const T& inp) { } } -template -__device__ __forceinline__ float to_float_b16(const bit16_t& inp) { - union tmpcvt { - bit16_t u; - _Float16 f; - __hip_bfloat16 b; - } t16; - t16.u = inp; - if constexpr (std::is_same::value) { - return (float)t16.f; - } else if constexpr (std::is_same::value) { - return __bfloat162float(t16.b); - } else { - static_assert(false, "unsupported 16b dtype"); - } -} - template __device__ __forceinline__ T from_float(const float& inp) { if constexpr (std::is_same::value) { diff --git a/csrc/rocm/moe_q_gemm_rdna3.cu b/csrc/rocm/moe_q_gemm_rdna3.cu index 6c25ed7e4bc0..61ffbb9987e9 100644 --- a/csrc/rocm/moe_q_gemm_rdna3.cu +++ b/csrc/rocm/moe_q_gemm_rdna3.cu @@ -67,21 +67,6 @@ __forceinline__ __device__ float dot22_8_f(half2 (&dq)[4], const half* a_ptr) { return result; } -__forceinline__ __device__ float dot22_8_f(float (&dq)[8], - const bf16_t* a_ptr) { - float result = 0.0f; - #pragma unroll - for (int i = 0; i < 4; i++) { - uint32_t aw; - __builtin_memcpy(&aw, a_ptr + 2 * i, sizeof(uint32_t)); - float a_x = __uint_as_float((aw & 0xFFFFu) << 16); - float a_y = __uint_as_float(aw & 0xFFFF0000u); - result = __fmaf_rn(dq[2 * i + 0], a_x, result); - result = __fmaf_rn(dq[2 * i + 1], a_y, result); - } - return result; -} - __forceinline__ __device__ void atomic_add_pk4_f16(half* addr, half2 v01, half2 v23) { unsigned long long* addr_u = reinterpret_cast(addr); diff --git a/csrc/rocm/q_gemm_rdna3.cu b/csrc/rocm/q_gemm_rdna3.cu index 104af66d1cc5..bf2368269964 100644 --- a/csrc/rocm/q_gemm_rdna3.cu +++ b/csrc/rocm/q_gemm_rdna3.cu @@ -115,61 +115,6 @@ __forceinline__ __device__ float dot22_8_f(half2 (&dq)[4], const half* a_ptr) { return result; } -__forceinline__ __device__ float dot22_8_f(bf162_t (&dq)[4], - const bf16_t* a_ptr) { - // RDNA3 (gfx1100) lacks a packed bf16 FMA: there is no v_pk_fma_bf16 in - // the gfx11 ISA (it only landed on CDNA3+ / gfx94x and later). hipcc - // therefore lowers __hfma2(bf162_t, bf162_t, bf162_t) to a serialised - // fallback (single-element FMAs or fp32 round-trips), which empirically - // runs ~2× the cycle count of v_pk_fma_f16 on the same VALU. The bf16 - // decode path was paying that tax in full, scaling linearly with M (the - // fp16 path scales sub-linearly because its v_pk_fma_f16 is full rate - // and the kernel becomes memory-bound). - // - // Fix: widen bf16 → fp32 explicitly (a left-shift by 16, free in VGPRs) - // and accumulate with v_fma_f32, which IS full rate on RDNA3. Same FMA - // count, but each FMA is fast. Bonus: the accumulator is now fp32 - // throughout instead of bf16, which is also numerically more accurate - // (no compounding bf16-rounding inside the dot loop). - float result = 0.0f; - #pragma unroll - for (int i = 0; i < 4; i++) { - uint32_t aw, dw; - __builtin_memcpy(&aw, a_ptr + 2 * i, sizeof(uint32_t)); - __builtin_memcpy(&dw, &dq[i], sizeof(uint32_t)); - // bf16 in low 16 bits → fp32 by left-shifting into the upper half. - // bf16 in high 16 bits → already aligned with fp32's upper half. - float a_x = __uint_as_float((aw & 0xFFFFu) << 16); - float a_y = __uint_as_float(aw & 0xFFFF0000u); - float d_x = __uint_as_float((dw & 0xFFFFu) << 16); - float d_y = __uint_as_float(dw & 0xFFFF0000u); - result = __fmaf_rn(d_x, a_x, result); - result = __fmaf_rn(d_y, a_y, result); - } - return result; -} - -// fp32-input dot product: paired with dequant_4bit_8_bf16_f32 which already -// produces fp32 dq[8]. Saves the bf16→fp32 widening that the bf162_t -// overload above does for dq (still need to widen A from bf16). Wins more -// at high N: the bf162_t version's per-call widening cost scales with the -// number of dequants × M_COUNT × 4 dot calls; the fp32 version pays only -// for A widening (M_COUNT × 4 × 4 widens, half as many). -__forceinline__ __device__ float dot22_8_f(float (&dq)[8], - const bf16_t* a_ptr) { - float result = 0.0f; - #pragma unroll - for (int i = 0; i < 4; i++) { - uint32_t aw; - __builtin_memcpy(&aw, a_ptr + 2 * i, sizeof(uint32_t)); - float a_x = __uint_as_float((aw & 0xFFFFu) << 16); - float a_y = __uint_as_float(aw & 0xFFFF0000u); - result = __fmaf_rn(dq[2 * i + 0], a_x, result); - result = __fmaf_rn(dq[2 * i + 1], a_y, result); - } - return result; -} - // --------------------------------------------------------------------------- // Packed atomic-add via CAS-loop on a 64-bit word (4 fp16/bf16 lanes per CAS). // RDNA3 (gfx11) does NOT have native v_global_atomic_pk_add_f16 / _bf16 (those @@ -327,9 +272,8 @@ __global__ void gemm_q4_kernel_rdna3(const T* __restrict__ a, // Per-column dequant constants. We hold one set of (z, y) pairs per column. // fp16 uses the exllama (z1z16, y1y16) double-pair to enable the upper- - // nibble-*16 trick. bf16 uses fp32 scalars (z, y) because the dequant - // produces fp32 directly — see prep_zero_scale_bf16_f32 / the FMA - // bypass for the missing v_pk_fma_bf16 on gfx11. + // nibble-*16 trick. bf16 uses fp32 scalars (z, y) for the inline v_dot2 + // bias correction. half2 z1z16_h[4][2], y1y16_h[4][2]; float z_b_f[4], y_b_f[4]; diff --git a/csrc/rocm/qdq_4_rdna3.cuh b/csrc/rocm/qdq_4_rdna3.cuh index f4be668c1f08..43a7b33ba153 100644 --- a/csrc/rocm/qdq_4_rdna3.cuh +++ b/csrc/rocm/qdq_4_rdna3.cuh @@ -113,63 +113,6 @@ __forceinline__ __device__ void dequant_4bit_8_fp16(uint32_t qa, half2 (&dq)[4], // bf16 path // --------------------------------------------------------------------------- -// Bit-trick magic for bf16: -// bf16(128) == 0x4300 (sign 0, exp 134, mantissa 0). -// For nibble n in [0..15], bits [3:0] of mantissa hold n exactly because -// bf16's ULP at 128 is 1 (mantissa step = 2^(7-7) = 1). So -// ((qa & 0x000F000F) | 0x43004300) bitcasts to bfloat162(128+n_lo, 128+n_hi). -// -// Because bf16's mantissa is only 7 bits, we cannot use the fp16 "upper nibble -// * 16" trick. Instead each pair of nibbles is shifted down to [3:0]/[19:16] -// via a single 4/8/12-bit right-shift before the OR. That costs one extra -// shift per pair vs fp16, but keeps the FMA structure identical. -__forceinline__ __device__ void prep_zero_scale_bf16(uint32_t zero, - bf16_t scale, - bf162_t& z_prep, - bf162_t& y_prep) { - // z = scale * -(128 + zero); y = scale. - float scale_f = __bfloat162float(scale); - float zf = -(128.0f + (float)zero) * scale_f; - bf16_t zb = __float2bfloat16(zf); - z_prep = __bfloat162bfloat162(zb); - y_prep = __bfloat162bfloat162(scale); -} - -__forceinline__ __device__ void dequant_4bit_8_bf16(uint32_t qa, - bf162_t (&dq)[4], - bf162_t z_prep, - bf162_t y_prep) { - const uint32_t c0 = 0x43004300; - - union { - uint32_t u; - bf162_t b2; - } q0, q1, q2, q3; - q0.u = ((qa >> 0) & 0x000F000F) | c0; // bf162(128+q[0], 128+q[1]) - q1.u = ((qa >> 4) & 0x000F000F) | c0; // bf162(128+q[2], 128+q[3]) - q2.u = ((qa >> 8) & 0x000F000F) | c0; // bf162(128+q[4], 128+q[5]) - q3.u = ((qa >> 12) & 0x000F000F) | c0; // bf162(128+q[6], 128+q[7]) - - // dq = q_b * scale + (-(128+zero)*scale) = (q - zero) * scale - dq[0] = __hfma2(q0.b2, y_prep, z_prep); - dq[1] = __hfma2(q1.b2, y_prep, z_prep); - dq[2] = __hfma2(q2.b2, y_prep, z_prep); - dq[3] = __hfma2(q3.b2, y_prep, z_prep); -} - -// --------------------------------------------------------------------------- -// bf16-input → fp32-output dequant (RDNA3 scalar path). -// -// RDNA3 (gfx1100) has no v_pk_fma_bf16; packed bf16 FMA lowers to a slow -// fallback. Rather than computing dq in bf16 and widening at FMA time in -// the dot product, we widen to fp32 here once (a free left-shift by 16) and -// emit the (q - zero) * scale FMA directly in fp32. This: -// * Replaces 4× slow bf16 packed FMA with 8× fast fp32 FMA per int32. -// * Eliminates 4× bf16→fp32 widens that the dot product would do. -// * Keeps the dot product accumulator in fp32 without a roundtrip. -// -// Output: fp32 dq[8], one element per K position (consumed by the -// fp32-overload of dot22_8_f in q_gemm_rdna3.cu). __forceinline__ __device__ void prep_zero_scale_bf16_f32(uint32_t zero, bf16_t scale, float& z_prep, @@ -179,60 +122,6 @@ __forceinline__ __device__ void prep_zero_scale_bf16_f32(uint32_t zero, y_prep = scale_f; } -// Pure-q dequant for the M_COUNT=1 factored path: outputs the unscaled fp32 -// values 128+nibble, without folding scale/zero. The caller folds scale/zb -// into the accumulator outside the inner loop using a precomputed sum_a, -// which saves ~27% of the FMA count vs the per-col-dequant approach above -// (only beneficial at M_COUNT=1; break-even at M_COUNT=2). -// -// Cost: 0 FMAs (pure bit-trick + as_float reinterprets). -__forceinline__ __device__ void dequant_4bit_8_bf16_q_only(uint32_t qa, - float (&q_f32)[8]) { - const uint32_t c0 = 0x43004300; - const uint32_t q0 = ((qa >> 0) & 0x000F000F) | c0; - const uint32_t q1 = ((qa >> 4) & 0x000F000F) | c0; - const uint32_t q2 = ((qa >> 8) & 0x000F000F) | c0; - const uint32_t q3 = ((qa >> 12) & 0x000F000F) | c0; - q_f32[0] = __uint_as_float((q0 & 0xFFFFu) << 16); - q_f32[1] = __uint_as_float(q0 & 0xFFFF0000u); - q_f32[2] = __uint_as_float((q1 & 0xFFFFu) << 16); - q_f32[3] = __uint_as_float(q1 & 0xFFFF0000u); - q_f32[4] = __uint_as_float((q2 & 0xFFFFu) << 16); - q_f32[5] = __uint_as_float(q2 & 0xFFFF0000u); - q_f32[6] = __uint_as_float((q3 & 0xFFFFu) << 16); - q_f32[7] = __uint_as_float(q3 & 0xFFFF0000u); -} - -__forceinline__ __device__ void dequant_4bit_8_bf16_f32(uint32_t qa, - float (&dq)[8], - float z_prep, - float y_prep) { - const uint32_t c0 = 0x43004300; - const uint32_t q0 = ((qa >> 0) & 0x000F000F) | c0; - const uint32_t q1 = ((qa >> 4) & 0x000F000F) | c0; - const uint32_t q2 = ((qa >> 8) & 0x000F000F) | c0; - const uint32_t q3 = ((qa >> 12) & 0x000F000F) | c0; - // bf16(128+nibble) bits → fp32(128+nibble) bits via left-shift by 16 - // (just zero-extends the mantissa from 7 to 23 bits; exponent preserved). - const float q0x = __uint_as_float((q0 & 0xFFFFu) << 16); - const float q0y = __uint_as_float(q0 & 0xFFFF0000u); - const float q1x = __uint_as_float((q1 & 0xFFFFu) << 16); - const float q1y = __uint_as_float(q1 & 0xFFFF0000u); - const float q2x = __uint_as_float((q2 & 0xFFFFu) << 16); - const float q2y = __uint_as_float(q2 & 0xFFFF0000u); - const float q3x = __uint_as_float((q3 & 0xFFFFu) << 16); - const float q3y = __uint_as_float(q3 & 0xFFFF0000u); - // dq[i] = q_f32 * scale + (-(128+zero)*scale) = (nibble - zero) * scale - dq[0] = __fmaf_rn(q0x, y_prep, z_prep); - dq[1] = __fmaf_rn(q0y, y_prep, z_prep); - dq[2] = __fmaf_rn(q1x, y_prep, z_prep); - dq[3] = __fmaf_rn(q1y, y_prep, z_prep); - dq[4] = __fmaf_rn(q2x, y_prep, z_prep); - dq[5] = __fmaf_rn(q2y, y_prep, z_prep); - dq[6] = __fmaf_rn(q3x, y_prep, z_prep); - dq[7] = __fmaf_rn(q3y, y_prep, z_prep); -} - } // namespace gptq_rdna3 } // namespace vllm diff --git a/csrc/rocm/skinny_gemms.cu b/csrc/rocm/skinny_gemms.cu index 615cdabed585..79af0814550d 100644 --- a/csrc/rocm/skinny_gemms.cu +++ b/csrc/rocm/skinny_gemms.cu @@ -8,12 +8,20 @@ #include #include +#include +#include +#include #include "../cuda_compat.h" #include "dispatch_utils.h" #include "quantization/w8a8/fp8/common.cuh" #include "core/batch_invariant.hpp" +// Number of streams the pre-allocated split-K pool covers. ~7.5 MiB per slot. +static constexpr int64_t kWvSlots = 8; +// Slots held back for capturing streams, which cannot allocate one themselves. +static constexpr int64_t kWvCaptureSlots = 8; + // TODO(rasmith): The kernels in this file are susceptible to integer overflow // issues, do not take strides, and are unable to handle PyTorch tensors that // return is_contiguous() as False (the tensors may actually be contiguous @@ -1364,9 +1372,18 @@ __global__ void __launch_bounds__(WvPrGrp* THRDS) }; using big4 = __attribute__((__vector_size__(4 * sizeof(bigType)))) __bf16; - __shared__ scalar_t stg[WvPrGrp * WVLDS / GrpsShrB]; + // The main-loop staging areas are separated from the split-K readback by a + // __syncthreads(), so the readback may reuse all of LDS. + __shared__ union { + struct { + scalar_t stg[WvPrGrp * WVLDS / GrpsShrB]; + scalar_t s[max_lds_len - WvPrGrp * WVLDS / GrpsShrB]; + } mainloop; + scalar_t all[max_lds_len]; + } lds; + auto& stg = lds.mainloop.stg; + auto& s = lds.mainloop.s; unsigned int* myStg = (unsigned int*)(&stg[WVLDS * (threadIdx.y / GrpsShrB)]); - __shared__ scalar_t s[max_lds_len - WvPrGrp * WVLDS / GrpsShrB]; #ifndef WVSPLITKRC_1KPASS constexpr int TUC_ = (THRDS * UNRL * A_CHUNK); @@ -1682,33 +1699,69 @@ __global__ void __launch_bounds__(WvPrGrp* THRDS) if (my_cntr + 1 == k_rnd) { cntr[adr_] = 0; // clear for next round if constexpr (DTRMNSTC) { + constexpr uint32_t NT = N / NTILE / GrpsShrB; + // The readback stages k-shard partials through LDS, which bounds how + // many shards may be in flight at once. + constexpr uint32_t LDS_F4 = + max_lds_len * sizeof(scalar_t) / sizeof(float4); + constexpr uint32_t KSBMAX = LDS_F4 / (THRDS * 4 * NT); + static_assert(KSBMAX >= 1, "LDS cannot stage even one k-shard"); + const uint32_t KSB = (k_rnd < KSBMAX) ? k_rnd : KSBMAX; + + // First batch is peeled; it is the only batch for most shapes. #pragma unroll - for (int ks = 0; ks < k_rnd; ks++) { - for (uint32_t nt = 0; nt < N / NTILE / GrpsShrB; nt++) { + for (uint32_t kb = 0; kb < KSB; kb++) { + for (uint32_t nt = 0; nt < NT; nt++) { int g_nindx = (nt * NTILE + (N / GrpsShrB) * (threadIdx.y % GrpsShrB)) / 4; int g_adr = g_mindx * 4 + 0 + M * g_nindx * 4; __builtin_amdgcn_global_load_lds( - (float4*)(&glbl[g_adr + M * N * ks]), - &(((float4*)s)[(threadIdx.y * THRDS) + ks * THRDS * 4 + - nt * THRDS * 4 * k_rnd]), + (float4*)(&glbl[g_adr + M * N * kb]), + &(((float4*)lds.all)[(threadIdx.y * THRDS) + kb * THRDS * 4 + + nt * THRDS * 4 * KSB]), 16, 0, 0); } } - if (BIAS) - for (uint32_t nt = 0; nt < N / NTILE / GrpsShrB; nt++) { + if (BIAS) // overlaps the DMAs above + for (uint32_t nt = 0; nt < NT; nt++) { for (uint32_t j = 0; j < 4; j++) { int nindx = (j + (threadIdx.x / 16) * 4) + nt * NTILE + (N / GrpsShrB) * (threadIdx.y % GrpsShrB); biases[nt][j] = BIAS[(mindx % Bx) + (nindx % By) * Bx]; } } - asm volatile("s_waitcnt 0"); - for (int ks = 0; ks < k_rnd; ks++) { - for (uint32_t nt = 0; nt < N / NTILE / GrpsShrB; nt++) { - float4 eval = ((float4*)s)[(threadIdx.x + threadIdx.y * THRDS) + - ks * THRDS * 4 + nt * THRDS * 4 * k_rnd]; - vals[nt].f4 += eval; + asm volatile("s_waitcnt vmcnt(0)" ::: "memory"); + for (uint32_t kb = 0; kb < KSB; kb++) { + for (uint32_t nt = 0; nt < NT; nt++) { + vals[nt].f4 += + ((float4*)lds.all)[(threadIdx.x + threadIdx.y * THRDS) + + kb * THRDS * 4 + nt * THRDS * 4 * KSB]; + } + } + + for (uint32_t ks0 = KSB; ks0 < k_rnd; ks0 += KSB) { + const uint32_t ksn = (k_rnd - ks0 < KSB) ? (k_rnd - ks0) : KSB; + // prior reads must retire before the staging is overwritten + asm volatile("s_waitcnt lgkmcnt(0)" ::: "memory"); + for (uint32_t kb = 0; kb < ksn; kb++) { + for (uint32_t nt = 0; nt < NT; nt++) { + int g_nindx = + (nt * NTILE + (N / GrpsShrB) * (threadIdx.y % GrpsShrB)) / 4; + int g_adr = g_mindx * 4 + 0 + M * g_nindx * 4; + __builtin_amdgcn_global_load_lds( + (float4*)(&glbl[g_adr + M * N * (ks0 + kb)]), + &(((float4*)lds.all)[(threadIdx.y * THRDS) + kb * THRDS * 4 + + nt * THRDS * 4 * KSB]), + 16, 0, 0); + } + } + asm volatile("s_waitcnt vmcnt(0)" ::: "memory"); + for (uint32_t kb = 0; kb < ksn; kb++) { + for (uint32_t nt = 0; nt < NT; nt++) { + vals[nt].f4 += + ((float4*)lds.all)[(threadIdx.x + threadIdx.y * THRDS) + + kb * THRDS * 4 + nt * THRDS * 4 * KSB]; + } } } } else { @@ -1771,7 +1824,7 @@ __global__ void wvSplitKrc_(const int actlN, const int K, const int Kap, torch::Tensor wvSplitKrc(const at::Tensor& in_a, const at::Tensor& in_b, const std::optional& in_bias, const int64_t CuCount) { - int _DTRMNSTC = 1; // vllm::vllm_is_batch_invariant(); + static constexpr bool _DTRMNSTC = true; auto M_in = in_b.size(0); auto N_in = in_a.size(0); @@ -1819,26 +1872,157 @@ torch::Tensor wvSplitKrc(const at::Tensor& in_a, const at::Tensor& in_b, if (CuNeeded > CuCount) throw std::runtime_error("Invalid wvSplitKrc size"); - // Can we increase SplitK by shrinking the K-shared to 256? - int chunkk = (CuNeeded * 2 <= CuCount) ? 2 : 1; - - static torch::Tensor axl_glbl = - torch::zeros( - 128 * 1024 * (_DTRMNSTC ? 12 : 1), - torch::TensorOptions().dtype(torch::kFloat32).device(in_a.device())) - .detach(); - static torch::Tensor axl_cntr = - torch::zeros( - 128 * 1024 * (_DTRMNSTC ? 12 : 1) / 4, - torch::TensorOptions().dtype(torch::kInt).device(in_a.device())) - .detach(); - auto glbl = axl_glbl.data_ptr(); - auto cntr = axl_cntr.data_ptr(); + // Can we increase SplitK by shrinking the K-shard to 256? That doubles both + // the CUs needed and the k-shards to read back, and the readback is serial in + // the shard count, so it pays only up to a crossover. The crossover is not a + // constant -- it falls as M grows -- so one cap cannot sit on it for every + // shape; this value minimizes total time over the admitted shapes. Retune it + // if the readback cost changes, and note the directions are not symmetric: + // capping too high costs about twice what capping too low does. + // Mirrored by rocm_unquantized_gemm_impl() in + // vllm/model_executor/layers/utils.py; both must pick the same chunkk or the + // host-side fit check bounds the wrong k_rnd. + constexpr int64_t CHUNKK2_MAX_SHARDS = 11; + const int64_t shards_chunkk2 = (K_in + 256 - 1) / 256; // 256 == 512 / 2 + const int chunkk = (N_p2 != 16 && CuNeeded * 2 <= CuCount && + shards_chunkk2 <= CHUNKK2_MAX_SHARDS) + ? 2 + : 1; + + // One fp32 partial per (M, N, k-shard) must fit the split-K workspace. + const int64_t k_rnd = (K_in + 512 / chunkk - 1) / (512 / chunkk); + constexpr int64_t glbl_cap = 128 * 1024 * (_DTRMNSTC ? 12 : 1); + const int64_t glbl_needed = + _DTRMNSTC ? (int64_t)N_p2 * M_in * k_rnd : (int64_t)N_p2 * M_in; + TORCH_CHECK(glbl_needed <= glbl_cap, + "wvSplitKrc: split-K workspace too small", " (need ", glbl_needed, + " > ", glbl_cap, " floats) for N=", N_in, " K=", K_in, + " M=", M_in); + + // Split-K workspaces: one pre-partitioned pool per device. + // These must be zeroed before first use, and the kernel re-zeros them after + // each use. + // To avoid unnecessary overhead, we do not allocate them zeroed + // per-invocation. However, a simple static allocation does not work with + // multi-streams, because concurrent streams can alias into the same + // workspace. Solution: pre-allocate kWvSlots = 8 slots per device, plus + // kWvCaptureSlots reserved for capturing streams, and zero the whole pool + // once at creation. On first use, a stream takes one of the slots if any are + // still available. On subsequent uses, it re-uses the slot. If no slots are + // available, fall back to allocating a zeroed one per call, which is safe but + // adds overhead. + // A capturing stream must neither allocate nor enqueue anything -- either + // would be captured into the graph -- so it is only handed a reserved slot. + struct WvSplitKrcPool { + torch::Tensor glbl, cntr; + int64_t glbl_stride = 0, cntr_stride = 0; + int next_slot = 0; + int next_capture_slot = 0; + std::map slot_of; + std::map capture_slot_of; + }; + // mutex not strictly necessary if caller is always single-threaded (Python) + // but this preserves Torch thread_local semantics for the C++ API + static std::mutex wv_pool_mu; + static std::map wv_pools; + + float* glbl; + int* cntr; + // Fallback space + torch::Tensor spill_glbl, spill_cntr; + { + std::lock_guard wv_lk(wv_pool_mu); + const int wv_dev = static_cast(in_a.device().index()); + WvSplitKrcPool& P = wv_pools[wv_dev]; + constexpr int64_t wv_gf = 128 * 1024 * (_DTRMNSTC ? 12 : 1); + constexpr int64_t wv_ci = wv_gf / 4; + constexpr int64_t wv_all_slots = kWvSlots + kWvCaptureSlots; + + // A stream whose capture was already invalidated reports an error; treat + // that as not capturing, since the capture is lost either way. + cudaStreamCaptureStatus capture_status = cudaStreamCaptureStatusNone; + unsigned long long capture_id = 0; + if (cudaStreamGetCaptureInfo(stream, &capture_status, &capture_id) != + cudaSuccess) { + capture_status = cudaStreamCaptureStatusNone; + } + const bool wv_capturing = capture_status != cudaStreamCaptureStatusNone; + + if (!P.glbl.defined()) { + TORCH_CHECK(!wv_capturing, + "wvSplitKrc: the split-K workspace pool cannot be allocated " + "during graph capture. Call " + "warmup_rocm_skinny_gemm_workspaces() " + "(vllm/model_executor/layers/utils.py) eagerly first."); + P.glbl_stride = wv_gf; + P.cntr_stride = wv_ci; + P.glbl = torch::zeros(wv_gf * wv_all_slots, torch::TensorOptions() + .dtype(torch::kFloat32) + .device(in_a.device())) + .detach(); + P.cntr = + torch::zeros( + wv_ci * wv_all_slots, + torch::TensorOptions().dtype(torch::kInt).device(in_a.device())) + .detach(); + // The mutex orders host access to the map, not device execution, so it + // alone would let another stream be handed a slot the fill above is + // still clearing. Synchronizing here, inside the lock, is what makes + // every slot safe to use without any further per-slot zeroing. + const cudaError_t wv_err = cudaStreamSynchronize(stream); + TORCH_CHECK(wv_err == cudaSuccess, + "wvSplitKrc: failed to zero the split-K workspace pool: ", + cudaGetErrorString(wv_err)); + } + + int wv_slot = -1; + if (wv_capturing) { + // One workspace per capturing stream, so graphs captured on the same + // stream share it -- safe because at most one of them replays at a time + // (vLLM captures one graph per padded batch size, and runs one per step). + auto cap_it = P.capture_slot_of.find(stream); + if (cap_it == P.capture_slot_of.end()) { + TORCH_CHECK(P.next_capture_slot < kWvCaptureSlots, + "wvSplitKrc: no free capture workspace slot; more than ", + kWvCaptureSlots, + " distinct streams have captured a graph containing " + "wvSplitKrc. Raise kWvCaptureSlots in " + "csrc/rocm/skinny_gemms.cu."); + cap_it = P.capture_slot_of + .emplace(stream, static_cast(kWvSlots) + + P.next_capture_slot++) + .first; + } + wv_slot = cap_it->second; + } else { + auto slot_it = P.slot_of.find(stream); + if (slot_it != P.slot_of.end()) { + wv_slot = slot_it->second; + } else if (P.next_slot < kWvSlots) { + wv_slot = P.next_slot++; + P.slot_of.emplace(stream, wv_slot); + } + } + + if (wv_slot >= 0) { + glbl = P.glbl.data_ptr() + wv_slot * P.glbl_stride; + cntr = P.cntr.data_ptr() + wv_slot * P.cntr_stride; + } else { + spill_glbl = torch::zeros( + wv_gf, + torch::TensorOptions().dtype(torch::kFloat32).device(in_a.device())); + spill_cntr = torch::zeros( + wv_ci, + torch::TensorOptions().dtype(torch::kInt).device(in_a.device())); + glbl = spill_glbl.data_ptr(); + cntr = spill_cntr.data_ptr(); + } + } #define WVSPLITKrc(_N, _GrpsShrB, _CHUNKK) \ { \ dim3 block(64, 4); \ - if (_DTRMNSTC) \ + if constexpr (_DTRMNSTC) \ wvSplitKrc_ \ <<>>(N_in, K_in, Kap_in, M_in, Bx_in, By_in, \ af4, bf4, biasf4, glbl, cntr, c, \ diff --git a/csrc/rocm/skinny_gemms_int4.cu b/csrc/rocm/skinny_gemms_int4.cu index 8b7077d14ed0..a20ca41d413e 100644 --- a/csrc/rocm/skinny_gemms_int4.cu +++ b/csrc/rocm/skinny_gemms_int4.cu @@ -102,6 +102,18 @@ __device__ inline unsigned int min__(uint32_t a, uint32_t b) { return min(a, b); } +// Reads row `row` of packed zero points [M/8, num_groups] uint32. +// +// The parameters must be signed and the indexing must use shift/mask: with +// unsigned parameters and row/8, row%8, the (A_CHUNK=32, UNRL=8) +// instantiation needs 256 VGPRs and 68 bytes of scratch instead of 239. +__device__ __forceinline__ uint32_t zp_nibble(const uint32_t* zp_packed, + const int row, const int group, + const int num_groups) { + const uint32_t w = zp_packed[(row >> 3) * num_groups + group]; + return (w >> (4 * (row & 7))) & 0xFu; +} + // W4A16 skinny GEMM kernel: packed int4 weights, fp16/bf16 activations // Targets the "sml" case where activations fit in LDS. // A_CHUNK: number of K-elements processed per thread per step. @@ -116,7 +128,7 @@ __global__ void __launch_bounds__(WvPrGrp* THRDS) wvSplitK_int4_hf_sml_(const int K, const int M, const int Bx, const int By, const uint8_t* B_packed, const scalar_t* __restrict__ A, const scalar_t* scale, - const scalar_t* zero_points, + const uint32_t* zero_points, const scalar_t* __restrict__ BIAS, scalar_t* C, const int _WvPrGrp, const int CuCount) { constexpr int max_lds_len = LDS_SIZE / 2; @@ -257,7 +269,8 @@ __global__ void __launch_bounds__(WvPrGrp* THRDS) if constexpr (!std::is_same_v) { if constexpr (HAS_ZERO_POINTS && GROUP_SIZE > 0) { uint32_t group_idx = k_ / GROUP_SIZE; - scalar_t zp = zero_points[(m + y) * num_groups + group_idx]; + scalar_t zp = __float2s(static_cast( + zp_nibble(zero_points, m + y, group_idx, num_groups))); #pragma unroll for (uint32_t b = 0; b < A_CHUNK; b++) { cvtB.h[b] = cvtB.h[b] - zp; @@ -280,8 +293,8 @@ __global__ void __launch_bounds__(WvPrGrp* THRDS) } if constexpr (HAS_ZERO_POINTS) { uint32_t group_idx_zp = k_ / GROUP_SIZE; - float zp_f = __s2float( - zero_points[(m + y) * num_groups + group_idx_zp]); + float zp_f = static_cast( + zp_nibble(zero_points, m + y, group_idx_zp, num_groups)); partial -= (128.0f + zp_f) * act_sum; } else { partial -= 136.0f * act_sum; @@ -335,7 +348,7 @@ __global__ void wvSplitK_int4_hf_sml_(const int K, const int M, const int Bx, const int By, const uint8_t* B_packed, const scalar_t* __restrict__ A, const scalar_t* scale, - const scalar_t* zero_points, + const uint32_t* zero_points, const scalar_t* __restrict__ BIAS, scalar_t* C, const int _WvPrGrp, const int CuCount) { @@ -352,7 +365,7 @@ template ) { if constexpr (HAS_ZERO_POINTS && GROUP_SIZE > 0) { uint32_t group_idx = k_ / GROUP_SIZE; - scalar_t zp = zero_points[(m + y) * num_groups + group_idx]; + scalar_t zp = __float2s(static_cast( + zp_nibble(zero_points, m + y, group_idx, num_groups))); #pragma unroll for (uint32_t b = 0; b < A_CHUNK; b++) { cvtB.h[b] = cvtB.h[b] - zp; @@ -531,8 +545,8 @@ __global__ void __launch_bounds__(WvPrGrp* THRDS) } if constexpr (HAS_ZERO_POINTS) { uint32_t group_idx_zp = k_ / GROUP_SIZE; - float zp_f = __s2float( - zero_points[(m + y) * num_groups + group_idx_zp]); + float zp_f = static_cast( + zp_nibble(zero_points, m + y, group_idx_zp, num_groups)); partial -= (128.0f + zp_f) * act_sum; } else { partial -= 136.0f * act_sum; @@ -596,7 +610,7 @@ __global__ void wvSplitK_int4_hf_(const int K, const int M, const int Bx, const int By, const uint8_t* B_packed, const scalar_t* __restrict__ A, const scalar_t* scale, - const scalar_t* zero_points, + const uint32_t* zero_points, const scalar_t* __restrict__ BIAS, scalar_t* C, const int _WvPrGrp, const int CuCount) { @@ -621,7 +635,8 @@ static int mindiv_int4(int N, int div1, int div2) { // in_a: packed int4 weights [M, K/2] (int8) or [M, K/8] (int32) // in_b: activations [N, K] (fp16/bf16) // in_scale: group scales [M, K/group_size] (fp16/bf16) -// in_zero_points: optional raw zero points [M, K/group_size] (fp16/bf16) +// in_zero_points: optional raw zero points [M/8, K/group_size] (int32, +// 8 uint4 per word along dim 0) // If provided, kernel dequants as (nibble - zp_raw) * scale (asymmetric). // If absent, kernel dequants as (nibble - 8) * scale (symmetric uint4b8). // group_size: 32, 64, or 128 @@ -664,14 +679,22 @@ torch::Tensor wvSplitK_int4_g(const at::Tensor& in_a, const at::Tensor& in_b, "Scale must be [M, K/group_size] = [", M_in, ", ", num_groups, "] but got [", in_scale.size(0), ", ", in_scale.size(1), "]"); if (in_zero_points.has_value()) { - TORCH_CHECK(in_zero_points->dtype() == in_b.dtype(), - "Zero points dtype must match activation dtype"); + // The kernel reads the words as uint32, so either signedness is accepted. + TORCH_CHECK(in_zero_points->dtype() == at::kInt || + in_zero_points->dtype() == at::kUInt32, + "Zero points must be int32 or uint32 (packed 8x uint4 along " + "dim 0), got ", + in_zero_points->dtype()); TORCH_CHECK(in_zero_points->dim() == 2, - "Zero points must be 2D [M, K/group_size], got shape ", + "Zero points must be 2D [M/8, K/group_size], got shape ", in_zero_points->sizes()); - TORCH_CHECK(in_zero_points->size(0) == M_in && + TORCH_CHECK(M_in % 8 == 0, + "M must be divisible by 8 for packed zero points, got ", M_in); + TORCH_CHECK(in_zero_points->is_contiguous(), + "Zero points must be contiguous"); + TORCH_CHECK(in_zero_points->size(0) == M_in / 8 && in_zero_points->size(1) == num_groups, - "Zero points must be [M, K/group_size] = [", M_in, ", ", + "Zero points must be [M/8, K/group_size] = [", M_in / 8, ", ", num_groups, "] but got [", in_zero_points->size(0), ", ", in_zero_points->size(1), "]"); } @@ -769,9 +792,9 @@ torch::Tensor wvSplitK_int4_g(const at::Tensor& in_a, const at::Tensor& in_b, const fptype* aptr = reinterpret_cast(in_b.data_ptr()); const fptype* sptr = reinterpret_cast(in_scale.data_ptr()); - const fptype* zpptr = + const uint32_t* zpptr = in_zero_points.has_value() - ? reinterpret_cast(in_zero_points->data_ptr()) + ? reinterpret_cast(in_zero_points->data_ptr()) : nullptr; const fptype* biasptr = (in_bias.has_value() && in_bias->numel() > 0) diff --git a/csrc/rocm/torch_bindings.cpp b/csrc/rocm/torch_bindings.cpp index a8114ce1969a..14d1822707ea 100644 --- a/csrc/rocm/torch_bindings.cpp +++ b/csrc/rocm/torch_bindings.cpp @@ -31,7 +31,8 @@ TORCH_LIBRARY_EXPAND(TORCH_EXTENSION_NAME, rocm_ops) { rocm_ops.impl("wvSplitK", torch::kCUDA, &wvSplitK); // W4A16 grouped skinny GEMM: packed int4 weights, per-group scales, - // optional zero points for asymmetric quantization + // optional zero points [M/8, K/group_size] int32 for asymmetric + // quantization rocm_ops.def( "wvSplitK_int4_g(Tensor in_a, Tensor in_b, Tensor in_scale, " "Tensor? in_zero_points, Tensor? in_bias, int CuCount, " diff --git a/docker/Dockerfile b/docker/Dockerfile index 9b344552c5ae..26b615688100 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -131,6 +131,7 @@ RUN dnf install -y --setopt=install_weak_deps=False \ git \ curl \ sudo \ + elfutils-devel \ rdma-core-devel \ && dnf clean all \ && rm -rf /var/cache/dnf @@ -230,10 +231,12 @@ RUN --mount=type=cache,target=/opt/uv/cache \ echo "Installing Rubin prerelease requirements..." \ && /opt/venv/bin/python3 use_existing_torch.py --prefix \ && uv pip install --python /opt/venv/bin/python3 \ + --prerelease=allow --index-strategy unsafe-first-match \ -r /tmp/vllm-requirements/rubin-prerelease.txt \ && uv pip install --python /opt/venv/bin/python3 \ -r requirements/cuda.txt \ && uv pip install --python /opt/venv/bin/python3 \ + --prerelease=allow --index-strategy unsafe-first-match \ -r /tmp/vllm-requirements/rubin-prerelease.txt; \ elif [ "${PYTORCH_NIGHTLY}" = "1" ]; then \ echo "Installing torch nightly..." \ @@ -474,6 +477,24 @@ RUN --mount=type=cache,target=/root/.cache/ccache \ #################### EXTENSIONS BUILD IMAGE #################### # Build DeepEP - runs in PARALLEL with csrc-build # This stage is independent and doesn't affect csrc cache +# Kernel symbol map for CI test selection: which source file produced each GPU +# kernel symbol, read from the objects this stage just compiled. Only when +# VLLM_KERNEL_SYMBOL_MAP=1 (nightly), and placed after the compile layers so +# their cache is untouched. Never fails the build; an empty map is a signal. +ARG VLLM_KERNEL_SYMBOL_MAP="" +ARG buildkite_commit="" +COPY tools/ci/kernel_symbol_map.py tools/ci/kernel_symbol_map.py +RUN if [ "${VLLM_KERNEL_SYMBOL_MAP}" = "1" ]; then \ + mkdir -p dist && python3 tools/ci/kernel_symbol_map.py \ + --build-root build --source-root . --commit "${buildkite_commit}" \ + --out dist/kernel_symbol_map.json.gz; \ + else echo "Skipping kernel symbol map (VLLM_KERNEL_SYMBOL_MAP != 1)"; fi + +# Exported on its own by the CI bake target of the same name (ci-infra +# docker/ci.hcl) with output=type=local, so the map becomes a build artifact. +FROM scratch AS kernel-symbol-map +COPY --from=csrc-build /workspace/dist/kernel_symbol_map.json.gz /kernel_symbol_map.json.gz + FROM base AS extensions-build ARG TARGETARCH ARG CUDA_VERSION @@ -640,7 +661,7 @@ RUN --mount=type=cache,target=/opt/uv/cache \ python3 use_existing_torch.py --prefix; \ fi \ && if [ "${INSTALL_RUBIN_PRERELEASE}" = "true" ]; then \ - sed -Ei '/^[[:space:]]*(flashinfer-python|nvidia-cutlass-dsl\[cu13\])[[:space:]]*==/Id' \ + sed -Ei '/^[[:space:]]*(flashinfer-python|nvidia-cutlass-dsl\[cu13\]|quack-kernels)[[:space:]]*==/Id' \ requirements/cuda.txt; \ fi @@ -732,6 +753,8 @@ RUN --mount=type=cache,target=/opt/uv/cache \ FROM ${FINAL_BASE_IMAGE} AS vllm-runtime-base ARG INSTALL_RUBIN_PRERELEASE +ARG TARGETPLATFORM +ARG max_jobs=2 ARG CUDA_VERSION ARG PYTHON_VERSION ARG NCCL_VERSION @@ -851,6 +874,20 @@ RUN if [ "${INSTALL_RUBIN_PRERELEASE}" = "true" ]; then \ && ldconfig; \ fi +# Install the initialized-snapshot runtime on the architecture supported by +# `vllm snapshot`. CRIU and its CUDA plugin are built from the same pinned +# source. The `cuda-checkpoint`-compatible helper is built from NVIDIA Dynamo's +# Apache-2.0 source against the public CUDA Driver checkpoint APIs instead of +# redistributing NVIDIA's proprietary cuda-checkpoint binary. +COPY tools/install_snapshot_runtime.sh /usr/local/bin/install_snapshot_runtime.sh +RUN if [ "${TARGETPLATFORM}" = "linux/amd64" ]; then \ + MAX_JOBS="${max_jobs}" /usr/local/bin/install_snapshot_runtime.sh; \ + else \ + echo "Initialized snapshots are not installed for ${TARGETPLATFORM}"; \ + fi + +ENV CRIU_CUDA_PLUGIN_DIR=/usr/local/lib/criu + # Install uv for faster pip installs RUN python3 -m pip install uv @@ -940,11 +977,13 @@ RUN --mount=type=cache,target=/opt/uv/cache \ fi && \ if [ "${INSTALL_RUBIN_PRERELEASE}" = "true" ]; then \ uv pip install --system \ + --prerelease=allow --index-strategy unsafe-first-match \ -r /tmp/vllm-requirements/rubin-prerelease.txt \ && sed -Ei '/^[[:space:]]*(torch|torchvision|torchaudio)[[:space:]]*==/Id' \ /tmp/requirements-cuda.txt \ && uv pip install --system -r /tmp/requirements-cuda.txt \ && uv pip install --system \ + --prerelease=allow --index-strategy unsafe-first-match \ -r /tmp/vllm-requirements/rubin-prerelease.txt \ && printf '%s\n' \ "/usr/local/lib/python${PYTHON_VERSION}/dist-packages/nvpl/lib" \ @@ -960,7 +999,7 @@ RUN --mount=type=cache,target=/opt/uv/cache \ # Install FlashInfer JIT cache (requires CUDA-version-specific index URL) # https://docs.flashinfer.ai/installation.html # From versions.json: .flashinfer.version -ARG FLASHINFER_VERSION=0.6.18.post1 +ARG FLASHINFER_VERSION=0.7.0 RUN --mount=type=cache,target=/opt/uv/cache \ if [ "${INSTALL_RUBIN_PRERELEASE}" != "true" ]; then \ uv pip install --system flashinfer-jit-cache==${FLASHINFER_VERSION} \ @@ -1042,6 +1081,10 @@ RUN --mount=type=bind,from=build,src=/workspace/dist,target=/vllm-workspace/dist --extra-index-url ${PYTORCH_CUDA_INDEX_BASE_URL}/cu$(echo $CUDA_VERSION | cut -d. -f1,2 | tr -d '.'); \ fi +# Expose the Rust frontend bundled in the wheel on the container's PATH. +RUN ln -s "$(python3 -c 'import sysconfig; print(sysconfig.get_path("platlib"))')/vllm/vllm-rs" /usr/local/bin/vllm-rs \ + && vllm-rs --version + RUN --mount=type=cache,target=/opt/uv/cache \ . /etc/environment && \ uv pip list @@ -1221,7 +1264,8 @@ ENV UV_HTTP_TIMEOUT=500 ARG torch_cuda_arch_list='7.5 8.0 8.6 8.9 9.0 10.0 11.0 12.0' ENV TORCH_CUDA_ARCH_LIST=${torch_cuda_arch_list} RUN --mount=type=cache,target=/opt/uv/cache \ - --mount=type=bind,source=requirements/kv_connectors.txt,target=/tmp/kv_connectors.txt,ro \ + --mount=type=bind,source=requirements,target=/tmp/vllm-requirements,ro \ + set -eu; \ CUDA_MAJOR="${CUDA_VERSION%%.*}"; \ CUDA_VERSION_DASH=$(echo $CUDA_VERSION | cut -d. -f1,2 | tr '.' '-'); \ CUDA_HOME=/usr/local/cuda; \ @@ -1230,11 +1274,19 @@ RUN --mount=type=cache,target=/opt/uv/cache \ libcublas-dev-${CUDA_VERSION_DASH} \ libcusolver-dev-${CUDA_VERSION_DASH}"; \ if [ "$INSTALL_KV_CONNECTORS" = "true" ]; then \ - uv pip install --system -r /tmp/kv_connectors.txt --no-build || ( \ + KV_REQUIREMENTS=/tmp/vllm-requirements/kv_connectors.txt; \ + if [ "${CUDA_MAJOR}" = "12" ]; then \ + KV_REQUIREMENTS=/tmp/vllm-requirements/kv_connectors_cu12.txt; \ + fi; \ + # Preserve the stack used to compile vLLM when resolving optional dependencies. \ + uv pip freeze --system > /tmp/kv-installed.txt; \ + grep -E '^(torch|torchaudio|torchvision|triton|cuda-bindings|cuda-python|cuda-toolkit)==' \ + /tmp/kv-installed.txt > /tmp/kv-constraints.txt; \ + uv pip install --system -c /tmp/kv-constraints.txt -r "${KV_REQUIREMENTS}" --no-build || ( \ # if the above fails, install from source apt-get update -y && \ apt-get install -y --no-install-recommends --allow-change-held-packages ${BUILD_PKGS} && \ - uv pip install --system -r /tmp/kv_connectors.txt --no-build-isolation && \ + uv pip install --system -c /tmp/kv-constraints.txt -r "${KV_REQUIREMENTS}" --no-build-isolation && \ apt-get purge -y ${BUILD_PKGS} && \ # clean up -dev packages, keep runtime libraries rm -rf /var/lib/apt/lists/* \ @@ -1245,8 +1297,9 @@ RUN --mount=type=cache,target=/opt/uv/cache \ MOONCAKE_VERSION=$(python3 -c "import importlib.metadata as m; print(m.version('mooncake-transfer-engine'))" 2>/dev/null || true) && \ if [ "${CUDA_MAJOR}" = "13" ] && [ -n "${MOONCAKE_VERSION}" ]; then \ uv pip uninstall --system mooncake-transfer-engine 2>/dev/null || true; \ - uv pip install --system "mooncake-transfer-engine-cuda13==${MOONCAKE_VERSION}"; \ + uv pip install --system -c /tmp/kv-constraints.txt "mooncake-transfer-engine-cuda13==${MOONCAKE_VERSION}"; \ fi; \ + rm -f /tmp/kv-installed.txt /tmp/kv-constraints.txt; \ fi # Mooncake registers GPU memory for RDMA via nvidia-peermem by default. Hosts @@ -1261,6 +1314,7 @@ RUN --mount=type=cache,target=/opt/uv/cache \ if [ "${INSTALL_RUBIN_PRERELEASE}" = "true" ]; then \ echo "Installing Rubin prerelease requirements..." \ && uv pip install --system \ + --prerelease=allow --index-strategy unsafe-first-match \ -r /tmp/vllm-requirements/rubin-prerelease.txt; \ fi diff --git a/docker/Dockerfile.cpu b/docker/Dockerfile.cpu index f81ae880966c..05d337fa1361 100644 --- a/docker/Dockerfile.cpu +++ b/docker/Dockerfile.cpu @@ -107,55 +107,6 @@ ENV UV_LINK_MODE="copy" ENV TARGETARCH=${TARGETARCH} -######################### TRITON-CPU BUILD IMAGE ######################### -# Branches off base-common directly (before requirements/cpu.txt is installed -# below) so a torch/etc. version bump doesn't invalidate this stage's cache -- -# it builds triton-cpu in an isolated `uv build` environment and never touches -# these packages. -FROM base-common AS vllm-triton-cpu-build - -# Support for cross-compilation with x86 ISA including AVX2 and AVX512: docker build --build-arg VLLM_CPU_X86="true" ... -# Re-declared here because this stage is `FROM base-common` (not `vllm-build`), -# so it does not inherit the ARG/ENV defined there. Without it, the guard -# below would see an empty value and build triton-cpu on non-x86 targets -# (e.g. arm64). -ARG VLLM_CPU_X86=0 - -ARG USE_SCCACHE -ARG SCCACHE_ENDPOINT -ARG SCCACHE_LOCAL_ONLY=0 - -WORKDIR /vllm-workspace - -RUN mkdir dist - -RUN --mount=type=cache,target=/root/.cache/uv \ - --mount=type=cache,target=/root/.cache/ccache \ - --mount=type=cache,target=/root/.cache/sccache-local,sharing=locked \ - --mount=type=cache,target=/vllm-workspace/.deps,sharing=locked \ - --mount=type=cache,target=/root/.triton \ - --mount=type=secret,id=aws-credentials,target=/root/.aws/credentials,required=false \ - set -ex; \ - if [ "$TARGETARCH" = "amd64" ] || [ "$VLLM_CPU_X86" != "0" ]; then \ - git clone --recurse-submodules --shallow-submodules --filter=blob:none "https://github.com/triton-lang/triton-cpu.git"; \ - cd triton-cpu; \ - git checkout "270e696d"; \ - if [ "$USE_SCCACHE" = "1" ]; then \ - if [ "$SCCACHE_LOCAL_ONLY" = "1" ]; then \ - unset SCCACHE_BUCKET SCCACHE_REGION; \ - export SCCACHE_DIR=/root/.cache/sccache-local SCCACHE_CACHE_SIZE=20G; \ - elif [ -n "${SCCACHE_ENDPOINT}" ]; then \ - export SCCACHE_ENDPOINT="${SCCACHE_ENDPOINT}"; \ - fi; \ - export TRITON_APPEND_CMAKE_ARGS="-DCMAKE_C_COMPILER_LAUNCHER=sccache -DCMAKE_CXX_COMPILER_LAUNCHER=sccache"; \ - sccache --show-stats; \ - fi; \ - uv build --wheel --out-dir=../dist; \ - if [ "$USE_SCCACHE" = "1" ]; then sccache --show-stats; fi; \ - cd ..; \ - rm -rf triton-cpu; \ - fi - ######################### COMMON BASE IMAGE + PYTHON DEPS ######################### FROM base-common AS base-common-pydeps @@ -379,9 +330,12 @@ WORKDIR /vllm-workspace COPY requirements/test/cpu.txt requirements/test/cpu.txt # cpu.txt is compiled for x86_64, so platform markers are resolved away. Drop -# packages unavailable on aarch64 (decord, terratorch) for arm builds. +# packages unavailable on aarch64 (decord, terratorch) for arm builds. Also +# drop the generic PyPI triton pulled in transitively (via xgrammar) on x86 +# builds, so it doesn't clobber the triton-cpu wheel from requirements/cpu.txt. RUN case "$(uname -m)" in \ aarch64|arm64) sed -i '/^decord==/d; /^terratorch==/d' requirements/test/cpu.txt ;; \ + x86_64) sed -i '/^triton==/d' requirements/test/cpu.txt ;; \ esac # pytrec-eval-terrier bundles pre-C23 trec_eval sources. GCC 15 defaults to C23; @@ -434,14 +388,6 @@ RUN --mount=type=cache,target=/root/.cache/uv \ fi && \ pre-commit install --hook-type pre-commit --hook-type commit-msg -# Installed after requirements/test/cpu.txt (which pins triton==3.6.0) so the -# triton-cpu wheel isn't overwritten by that pin. -RUN --mount=type=cache,target=/root/.cache/uv \ - --mount=type=bind,from=vllm-triton-cpu-build,src=/vllm-workspace/dist,target=dist \ - if ls dist/*.whl >/dev/null 2>&1; then \ - uv pip install dist/*.whl; \ - fi - ENTRYPOINT ["bash"] ######################### TEST IMAGE ######################### @@ -453,18 +399,16 @@ RUN --mount=type=cache,target=/root/.cache/uv \ --mount=type=bind,from=vllm-build,src=/vllm-workspace/dist,target=dist \ uv pip install dist/*.whl -RUN --mount=type=cache,target=/root/.cache/uv \ - --mount=type=bind,from=vllm-triton-cpu-build,src=/vllm-workspace/dist,target=dist \ - if ls dist/*.whl >/dev/null 2>&1; then \ - uv pip install dist/*.whl; \ - fi - ADD ./tests/ ./tests/ ADD ./examples/ ./examples/ ADD ./benchmarks/ ./benchmarks/ ADD ./vllm/collect_env.py . ADD ./docker/ ./docker/ ADD ./.buildkite/ ./.buildkite/ +# tools/ carries repo-tooling code that a few tests exercise directly +# (e.g. tests/tools/test_check_test_tethering.py imports +# tools.pre_commit.check_test_tethering). +ADD ./tools/ ./tools/ # install development dependencies (for testing) RUN --mount=type=cache,target=/root/.cache/uv \ @@ -484,13 +428,7 @@ WORKDIR /vllm-workspace RUN --mount=type=cache,target=/root/.cache/uv \ --mount=type=cache,target=/root/.cache/ccache \ --mount=type=bind,from=vllm-build,src=/vllm-workspace/dist,target=dist \ - uv pip install "$(realpath dist/*.whl)[audio]" - -RUN --mount=type=cache,target=/root/.cache/uv \ - --mount=type=bind,from=vllm-triton-cpu-build,src=/vllm-workspace/dist,target=dist \ - if ls dist/*.whl >/dev/null 2>&1; then \ - uv pip install dist/*.whl; \ - fi + uv pip install "$(realpath dist/*.whl)[audio,bench]" # Add labels to document build configuration LABEL org.opencontainers.image.title="vLLM CPU" diff --git a/docker/Dockerfile.rock b/docker/Dockerfile.rock index c3ce785c679d..16e01ad723b9 100644 --- a/docker/Dockerfile.rock +++ b/docker/Dockerfile.rock @@ -5,7 +5,7 @@ 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=0f380657dbf3ee86eb57558ff71df24f03b5d4e7 +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 @@ -17,8 +17,9 @@ ARG INSTALL_LMCACHE=false 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 ioinic-dkms=25.11.1.001 -ARG AINIC_VERSION=1.117.3-hydra +# 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 @@ -322,8 +323,9 @@ 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}\")" \ + { grep -Fq "set(TRITON_KERNELS_TAG \"${ROCM_TRITON_KERNELS_COMMIT}\")" \ cmake/external_projects/triton_kernels.cmake \ + || echo "WARNING: Dockerfile.rock 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) \ @@ -669,8 +671,9 @@ RUN if [ "$GIT_REPO_CHECK" != "0" ]; then \ # 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}\")" \ + { grep -Fq "set(TRITON_KERNELS_TAG \"${ROCM_TRITON_KERNELS_COMMIT}\")" \ vllm/cmake/external_projects/triton_kernels.cmake \ + || echo "WARNING: Dockerfile.rock 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 @@ -875,6 +878,15 @@ 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 @@ -902,7 +914,7 @@ RUN case "$(which python3)" in \ *) ;; esac RUN --mount=type=cache,target=/root/.cache/uv \ - uv pip install --system --upgrade huggingface-hub[cli] + 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 diff --git a/docker/Dockerfile.rock_base b/docker/Dockerfile.rock_base index 1b1d08988123..79896933e7eb 100644 --- a/docker/Dockerfile.rock_base +++ b/docker/Dockerfile.rock_base @@ -3,10 +3,12 @@ ARG PYTHON_VERSION=3.12 ARG TORCH_VERSION=2.12.0+rocm10.0.0 ARG TORCHVISION_VERSION=0.27.0+rocm10.0.0 ARG TORCHAUDIO_VERSION=2.11.0+rocm10.0.0 -ARG TRITON_VERSION=3.8.0+git4cff872c.rocm10.0.0 +ARG TRITON_BRANCH="c0a142ffcbb42f9a67a56bf791cad472be1d6425" # release/internal/3.8.x as of 9/21/2026 +ARG TRITON_VERSION=3.8.0+gitc0a142ff +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.21.post1" +ARG AITER_BRANCH="v0.1.22.post1" ARG AITER_REPO="https://github.com/ROCm/aiter.git" ARG MORI_BRANCH="f95cd5e0ca7b730f92f9ffdfb358484d6965d9f7" # TheRock compatible v1.1.0 ARG MORI_REPO="https://github.com/ROCm/mori.git" @@ -90,19 +92,15 @@ RUN pip install --index-url ${ROCM_RELEASE_WHEELS_MULTIARCH_URL} \ && rocm-sdk init # Install PyTorch wheels (nightly for latest torch, release as fallback) -# Triton is pinned here rather than left to torch's Requires-Dist, so a wheel bump that -# moves the triton torch wants fails resolution instead of quietly changing compilers. ARG TORCH_VERSION ARG TORCHAUDIO_VERSION ARG TORCHVISION_VERSION -ARG TRITON_VERSION RUN pip install \ --index-url ${ROCM_RELEASE_WHEELS_MULTIARCH_URL} \ --extra-index-url ${ROCM_NIGHTLY_WHEELS_MULTIARCH_URL} \ "torch==${TORCH_VERSION}" \ "torchaudio==${TORCHAUDIO_VERSION}" \ - "torchvision==${TORCHVISION_VERSION}" \ - "triton==${TRITON_VERSION}" + "torchvision==${TORCHVISION_VERSION}" # Install the AMD-compiled device kernels corresponding to PYTORCH_ROCM_ARCH: @@ -146,6 +144,15 @@ RUN if [ -f "${SITE_PACKAGES}/rocm_sdk/__init__.py" ]; then \ ln -sfn "${ROCM_PATH}/lib/${bn}" "$f"; \ fi; \ done \ + && for f in librocprofiler-sdk.so.1 librocprofiler-register.so.0 \ + librocprofiler-sdk-roctx.so.1; do \ + if [ -e "${SITE_PACKAGES}/_rocm_sdk_core/lib/${f}" ] \ + && [ -e "${SITE_PACKAGES}/_rocm_sdk_devel/lib/${f}" ]; then \ + rm -f "${SITE_PACKAGES}/_rocm_sdk_core/lib/${f}"; \ + ln -sfn "${SITE_PACKAGES}/_rocm_sdk_devel/lib/${f}" \ + "${SITE_PACKAGES}/_rocm_sdk_core/lib/${f}"; \ + fi; \ + done \ && ldconfig # Install sccache if USE_SCCACHE is enabled (for release builds) @@ -248,6 +255,20 @@ RUN pip install pyyaml && cd aiter \ RUN mkdir -p /app/install && cp /app/aiter/dist/*.whl /app/install +### +### 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} \ + && 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 + + ### ### ROCr + CLR build ### @@ -311,6 +332,8 @@ RUN mkdir -p /staging/lib \ # 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_aiter,src=/app/install/,target=/install \ @@ -318,6 +341,8 @@ RUN --mount=type=bind,from=build_aiter,src=/app/install/,target=/install \ 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 \ @@ -326,6 +351,7 @@ RUN --mount=type=bind,from=build_mori,src=/app/install/,target=/install \ cp /install/*.whl /app/debs FROM base AS final +ARG TRITON_VERSION # 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. @@ -350,7 +376,15 @@ RUN set -eux; \ RUN ldconfig -ARG TRITON_VERSION +# This is to prevent resolvers from checking METADATA and RECORD and clobbering +# the Triton source build we installed. Constraints files are unable to help +# with this. However, these entries are optional and can be blanked which +# solves the problem. +RUN TORCH_DIST_INFO=$(ls -d ${SITE_PACKAGES}/torch-*.dist-info) \ + && grep -q '^Requires-Dist: triton' "$TORCH_DIST_INFO/METADATA" \ + && 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 \ && ! pip check 2>&1 | grep -iE '^(torch|triton) ' \ @@ -374,6 +408,8 @@ ARG TORCHAUDIO_VERSION ARG TORCHVISION_VERSION ARG PYTORCH_ROCM_ARCH ARG AITER_ROCM_ARCH +ARG TRITON_BRANCH +ARG TRITON_REPO ARG FA_BRANCH ARG FA_REPO ARG AITER_BRANCH @@ -382,6 +418,7 @@ ARG MORI_BRANCH 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 \ @@ -391,6 +428,8 @@ RUN echo "BASE_IMAGE: ${BASE_IMAGE}" > /app/versions.txt \ && echo "TORCHVISION_VERSION: ${TORCHVISION_VERSION}" >> /app/versions.txt \ && echo "PYTORCH_ROCM_ARCH: ${PYTORCH_ROCM_ARCH}" >> /app/versions.txt \ && echo "AITER_ROCM_ARCH: ${AITER_ROCM_ARCH}" >> /app/versions.txt \ + && echo "TRITON_BRANCH: ${TRITON_BRANCH}" >> /app/versions.txt \ + && echo "TRITON_REPO: ${TRITON_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 \ diff --git a/docker/Dockerfile.rocm b/docker/Dockerfile.rocm index 8d5f4f8f408a..cfd0b4557cd0 100644 --- a/docker/Dockerfile.rocm +++ b/docker/Dockerfile.rocm @@ -19,7 +19,7 @@ ARG NIC_BACKEND=all # 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 +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 @@ -367,7 +367,7 @@ 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/scripts/benchmark_helion_kernels.py /scripts/benchmark_helion_kernels.py -COPY --from=build_vllm ${COMMON_WORKDIR}/vllm/tools/install_torchcodec_rocm.sh /tools/install_torchcodec_rocm.sh +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 /docker/ @@ -750,7 +750,7 @@ COPY --from=build_vllm_wheel_release ${COMMON_WORKDIR}/vllm/benchmarks /benchmar 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/scripts/benchmark_helion_kernels.py /scripts/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/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 /docker/ @@ -938,7 +938,7 @@ RUN case "$(which python3)" in \ *) ;; esac RUN --mount=type=cache,target=/root/.cache/uv \ - uv pip install --system --upgrade huggingface-hub[cli] + 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 diff --git a/docker/Dockerfile.rocm_base b/docker/Dockerfile.rocm_base index d842a7b07e68..eb4bb5cc9750 100644 --- a/docker/Dockerfile.rocm_base +++ b/docker/Dockerfile.rocm_base @@ -9,13 +9,13 @@ 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.21.post2" +ARG AITER_BRANCH="v0.1.22.post1" ARG AITER_REPO="https://github.com/ROCm/aiter.git" ARG MORI_BRANCH="v1.1.0" 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="b539bf7eebfd99ad0a69668caa1f4037034d501f" +ARG ROCM_RUNTIME_COMMIT="af70edc91f1dc5d228fd4c076df99e65e49c4521" # Sccache configuration (only used in release pipeline) ARG USE_SCCACHE diff --git a/docker/Dockerfile.rocm_gfx1250 b/docker/Dockerfile.rocm_gfx1250 index 95dad539136d..318bf702720b 100644 --- a/docker/Dockerfile.rocm_gfx1250 +++ b/docker/Dockerfile.rocm_gfx1250 @@ -15,7 +15,6 @@ ARG NIC_BACKEND=all # 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. -# TODO (jmathias) : Check if we can revert this Pinned AINIC version ARG AINIC_VERSION=1.117.5-a-77 ARG UBUNTU_CODENAME=jammy @@ -672,7 +671,7 @@ RUN case "$(which python3)" in \ *) ;; esac RUN --mount=type=cache,target=/root/.cache/uv \ - uv pip install --system --upgrade huggingface-hub[cli] + 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 diff --git a/docker/Dockerfile.s390x b/docker/Dockerfile.s390x index 9517ff371850..6faba540a92b 100644 --- a/docker/Dockerfile.s390x +++ b/docker/Dockerfile.s390x @@ -169,8 +169,6 @@ ENV PKG_CONFIG_PATH="/opt/rh/gcc-toolset-14/root/usr/lib64/pkgconfig:/usr/local/ ENV PATH="${VIRTUAL_ENV:+${VIRTUAL_ENV}/bin}:/opt/rh/gcc-toolset-14/root/usr/bin:/usr/local/bin:$CARGO_HOME/bin:$RUSTUP_HOME/bin:$PATH" ENV PIP_EXTRA_INDEX_URL=${PIP_EXTRA_INDEX_URL} ENV UV_EXTRA_INDEX_URL=${PIP_EXTRA_INDEX_URL} -# Force pure Python protobuf to avoid s390x C++ extension crashes -ENV PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION=python COPY . /workspace/vllm WORKDIR /workspace/vllm @@ -214,10 +212,6 @@ RUN --mount=type=cache,target=/root/.cache/uv \ VLLM_TARGET_DEVICE=cpu VLLM_CPU_MOE_PREPACK=0 python setup.py bdist_wheel && \ uv pip install "$(echo dist/*.whl)[tensorizer]" -# Remove protobuf C++ extension that crashes on s390x -RUN rm -rf /opt/vllm/lib64/python${PYTHON_VERSION}/site-packages/google/_upb/*.so \ - /opt/vllm/lib64/python${PYTHON_VERSION}/site-packages/google/protobuf/pyext/*.so 2>/dev/null || true - # setup non-root user for vllm RUN umask 002 && \ /usr/sbin/useradd --uid 2000 --gid 0 vllm && \ diff --git a/docker/Dockerfile.xpu b/docker/Dockerfile.xpu index ee4c2a0d3b5a..3b0df0082643 100644 --- a/docker/Dockerfile.xpu +++ b/docker/Dockerfile.xpu @@ -87,11 +87,14 @@ RUN mkdir neo && \ # Install XPU-Manager payloads without registering conflicting libze package # dependencies in dpkg. +# Note that ocl-icd-opencl-dev is required by DLE upgrade but xpu-smi, should +# can be removed in future. RUN apt-get update -y && \ apt-get install -y --no-install-recommends software-properties-common && \ add-apt-repository -y ppa:kobuk-team/intel-graphics && \ apt-get update -y && \ apt-get install -y --no-install-recommends libhwloc15 libpciaccess0 libigsc1 intel-metrics-library && \ + apt-get install -y ocl-icd-opencl-dev && \ mkdir xpu-m && \ cd xpu-m && \ wget https://github.com/intel/xpumanager/releases/download/v2.1.0/xpu-smi_2.1.0+26.33.6468cec-1.24.04_amd64.deb && \ @@ -264,4 +267,19 @@ RUN --mount=type=cache,target=/root/.cache/uv \ RUN --mount=type=cache,target=/root/.cache/uv \ uv pip install -e tests/vllm_test_utils +# Replace 2026.1.2 DLE packages to 2026.1.3 +RUN --mount=type=cache,target=/root/.cache/uv \ + --mount=type=bind,source=.git,target=.git \ + uv pip install \ + "intel-cmplr-lib-rt==2026.1.1" \ + "intel-cmplr-lib-ur==2026.1.1" \ + "intel-cmplr-lic-rt==2026.1.1" \ + "intel-sycl-rt==2026.1.1" \ + "oneccl-devel==2022.1.2; platform_system == 'Linux' and platform_machine == 'x86_64'" \ + "oneccl==2022.1.2; platform_system == 'Linux' and platform_machine == 'x86_64'" \ + "dpcpp-cpp-rt==2026.1.1" \ + "intel-opencl-rt==2026.1.1" \ + "intel-openmp==2026.1.1" \ + "intel-pti==1.1.0" + ENTRYPOINT ["vllm", "serve"] diff --git a/docker/docker-bake-rocm.hcl b/docker/docker-bake-rocm.hcl index 9d73a6505eab..ab523a296a93 100644 --- a/docker/docker-bake-rocm.hcl +++ b/docker/docker-bake-rocm.hcl @@ -33,6 +33,10 @@ 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" { @@ -74,7 +78,7 @@ group "default" { } target "_common-rocm" { - dockerfile = "docker/Dockerfile.rocm" + dockerfile = CI_BASE_DOCKERFILE context = "." args = { max_jobs = MAX_JOBS diff --git a/docker/versions.json b/docker/versions.json index 7a5b9f44abd7..ec23b95186ac 100644 --- a/docker/versions.json +++ b/docker/versions.json @@ -71,7 +71,7 @@ "default": "true" }, "FLASHINFER_VERSION": { - "default": "0.6.18.post1" + "default": "0.7.0" }, "GDRCOPY_CUDA_VERSION": { "default": "12.8" diff --git a/docs/assets/contributing/dockerfile-stages-dependency.png b/docs/assets/contributing/dockerfile-stages-dependency.png index 9eca3ae91357..8ab91038016e 100644 Binary files a/docs/assets/contributing/dockerfile-stages-dependency.png and b/docs/assets/contributing/dockerfile-stages-dependency.png differ diff --git a/docs/benchmarking/cli.md b/docs/benchmarking/cli.md index d46ab04b0e2a..e7ee794da6f1 100644 --- a/docs/benchmarking/cli.md +++ b/docs/benchmarking/cli.md @@ -147,7 +147,7 @@ With standard decoding, each streamed output usually contains one token, so ITL and TPOT are typically similar. With speculative decoding, one streamed output can contain multiple tokens, -such as several accepted draft tokens within a single engine tstep. ITL records +such as several accepted draft tokens within a single engine step. ITL records only the gaps between streamed outputs; it does not add zero-duration gaps for tokens in the same output. TPOT instead amortizes the request's decoding time over every output token. @@ -451,7 +451,7 @@ vllm bench serve \ --num-prompts -1 ``` -Available categories include `[high_entropy, mixed, low_entropy]`, where high entropy data contains unstructued data such as creative writing while low entropy data contains more structured data such as coding, more details are in the dataset card. +Available categories include `[high_entropy, mixed, low_entropy]`, where high entropy data contains unstructured data such as creative writing while low entropy data contains more structured data such as coding, more details are in the dataset card. #### BFCL (Tool-Calling) Benchmark diff --git a/docs/benchmarking/dashboard.md b/docs/benchmarking/dashboard.md index 44effc078e35..4c1132c17fd1 100644 --- a/docs/benchmarking/dashboard.md +++ b/docs/benchmarking/dashboard.md @@ -36,7 +36,7 @@ When run, benchmark script generates results under **benchmark/results** folder, - `ON_CPU`: set the value to '1' on Intel® Xeon® and Arm® Neoverse™ Processors. Default value is 0. - `SERVING_JSON`: JSON file to use for the serving tests. Default value is empty string (use default file). - `LATENCY_JSON`: JSON file to use for the latency tests. Default value is empty string (use default file). -- `THROUGHPUT_JSON`: JSON file to use for the throughout tests. Default value is empty string (use default file). +- `THROUGHPUT_JSON`: JSON file to use for the throughput tests. Default value is empty string (use default file). - `REMOTE_HOST`: IP for the remote vLLM service to benchmark. Default value is empty string. - `REMOTE_PORT`: Port for the remote vLLM service to benchmark. Default value is empty string. - `PROMPTS_PER_CONCURRENCY`: Multiplier to compute `num_prompts` for serving tests (`num_prompts = max_concurrency × value`). Overrides JSON `num_prompts`. Default is NULL. diff --git a/docs/contributing/README.md b/docs/contributing/README.md index 55966f2854fd..e4b86f99aed5 100644 --- a/docs/contributing/README.md +++ b/docs/contributing/README.md @@ -50,7 +50,7 @@ To rebuild only the Rust frontend binary: ./build_rust.sh --debug # faster build for development ``` -If you are developing vLLM's Python and CUDA/C++ code, install Pytorch first: +If you are developing vLLM's Python and CUDA/C++ code, install PyTorch first: ```bash uv pip install torch torchvision torchaudio --extra-index-url https://download.pytorch.org/whl/cu129 @@ -315,6 +315,34 @@ review process: the `ready` label, the PR author can use `/ci run`, `/ci retry`, `/ci cancel`, or the corresponding `/amd-ci` variants. New commits do not start upstream CI automatically. +- Before creating a Buildkite build, `/ci run` and `/amd-ci run`, including + `all` and `nightly`, check the requested PR commit against its target branch + in the upstream repository. Your branch must contain every commit currently + on that branch: **zero commits behind**. This also applies to PRs targeting + release branches or another PR's branch. +- If the PR is behind, the bot reports the count without creating a new build. + Merge or rebase onto the latest target branch, then rerun the command. If the + target branch advances in the meantime, update your branch and try again. +- To run CI on an outdated branch at your own risk, append `--allow-stale` to + any run command, for example `/ci run --allow-stale` or + `/amd-ci run all --allow-stale`. The same authorization requirements apply. + The bot reports the lag and warns that outdated CI configuration may cause + failures. Before merging, merge or rebase onto the latest target branch and + rerun CI without `--allow-stale` on the latest PR commit. +- Retrying or cancelling builds does not check branch freshness. +- These commands do not modify your branch or enforce merge requirements. + Direct Buildkite launches are outside this workflow. No additional token is + needed. + +Maintainers must configure required Buildkite status checks on the latest PR +commit and enable GitHub's +[Require branches to be up to date before merging](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches/about-protected-branches#require-status-checks-before-merging) +protection on the relevant target branches to enforce the merge policy. Select +statuses that confirm the required tests completed, using Buildkite as their +expected source. An aggregate status that reports "passed and blocked" does not +by itself guarantee those tests ran; verify the pipeline's required and optional +blocked steps before choosing statuses or changing blocked-build reporting. +The CI command workflow does not configure these repository or pipeline settings. ### Pull Request Limits and Escalation diff --git a/docs/contributing/model/multimodal.md b/docs/contributing/model/multimodal.md index 951d25587ad9..d374a5ecbc27 100644 --- a/docs/contributing/model/multimodal.md +++ b/docs/contributing/model/multimodal.md @@ -454,17 +454,17 @@ return a schema of the tensors outputted by the HF processor that are related to ??? code ```python - def _get_hf_processor_text(self, mm_counts: Mapping[str, int]) -> str: + def _get_hf_mm_text(self, mm_counts: Mapping[str, int]) -> str: # Mistral3Processor requires text corresponding to the images return self.dummy_inputs.get_dummy_text(mm_counts) def _postprocess_hf_mm_data( self, - mm_data: Mapping[str, object], - hf_processor_mm_kwargs: Mapping[str, object], + hf_data: Mapping[str, object], + hf_kwargs: Mapping[str, object], processed_data: BatchFeature, ) -> BatchFeature: - if not mm_data: + if not hf_data: return processed_data pixel_values = processed_data.get("pixel_values") @@ -486,25 +486,36 @@ return a schema of the tensors outputted by the HF processor that are related to calls the HF processor on the multi-modal data without passing any text. If the HF processor instead requires text corresponding to the multi-modal items, you should override - [_get_hf_processor_text][vllm.multimodal.processing.BaseMultiModalProcessor._get_hf_processor_text] + [_get_hf_mm_text][vllm.multimodal.processing.BaseMultiModalProcessor._get_hf_mm_text] to return the dummy text from [BaseDummyInputsBuilder.get_dummy_text][vllm.multimodal.processing.BaseDummyInputsBuilder.get_dummy_text] like in the example above. If the HF processor expects the multi-modal data - under different keys than those provided by the multi-modal items - (e.g. `audio` instead of `audios`), or requires additional keyword arguments + under different keys than those provided by the multi-modal items, + or requires additional keyword arguments (e.g. `sampling_rate`), you should override - [_preprocess_hf_mm_data][vllm.multimodal.processing.BaseMultiModalProcessor._preprocess_hf_mm_data]. + [_get_hf_mm_inputs][vllm.multimodal.processing.BaseMultiModalProcessor._get_hf_mm_inputs]. If you need to modify the output of the HF processor, you should override [_postprocess_hf_mm_data][vllm.multimodal.processing.BaseMultiModalProcessor._postprocess_hf_mm_data]. + Passthrough fields have already been merged into `processed_data` when this + hook is called. + If you need to change how the normalized inputs are passed to the HF + processor (for example, to call a method other than `__call__`, or to use + different kwargs to construct and invoke it), override + [_call_hf_processor][vllm.multimodal.processing.BaseMultiModalProcessor._call_hf_processor]. If the HF processor applies additional transformations to the prompt tokens that are not reflected in its multi-modal outputs (e.g. prepending a BOS token), you should override [_postprocess_prompt][vllm.multimodal.processing.BaseMultiModalProcessor._postprocess_prompt] to replicate them. - For even more control over how the HF processor is called, you can override + Only processors that require multiple HF calls or manually construct the + complete output should override [_apply_hf_processor_main][vllm.multimodal.processing.BaseMultiModalProcessor._apply_hf_processor_main] - directly. + directly. Such implementations must obtain their normalized inputs through + [_get_hf_mm_inputs][vllm.multimodal.processing.BaseMultiModalProcessor._get_hf_mm_inputs] + and return through + [_finalize_hf_mm_data][vllm.multimodal.processing.BaseMultiModalProcessor._finalize_hf_mm_data] + so passthrough fields and model-specific postprocessing are applied consistently. Since `pixel_values` is now a list with one tensor per image, we can override [_get_mm_fields_config][vllm.multimodal.processing.BaseMultiModalProcessor._get_mm_fields_config] as follows: @@ -724,7 +735,7 @@ Examples: ### Custom HF processor -Some models don't define an HF processor class on HF Hub. In that case, you can define a custom HF processor that has the same call signature as HF processors and return it from [BaseProcessingInfo.get_hf_processor][vllm.multimodal.processing.BaseProcessingInfo.get_hf_processor]. It is then applied to the multi-modal data via [InputProcessingContext.call_hf_processor][vllm.multimodal.processing.InputProcessingContext.call_hf_processor] inside [_apply_hf_processor_main][vllm.multimodal.processing.BaseMultiModalProcessor._apply_hf_processor_main]. +Some models don't define an HF processor class on HF Hub. In that case, you can define a custom HF processor that has the same call signature as HF processors and return it from [BaseProcessingInfo.get_hf_processor][vllm.multimodal.processing.BaseProcessingInfo.get_hf_processor]. The default [_call_hf_processor][vllm.multimodal.processing.BaseMultiModalProcessor._call_hf_processor] applies it to the normalized multi-modal data via [InputProcessingContext.call_hf_processor][vllm.multimodal.processing.InputProcessingContext.call_hf_processor]. Override `_call_hf_processor` if the custom processor instead exposes a different callable interface. Examples: diff --git a/docs/contributing/profiling.md b/docs/contributing/profiling.md index e420a3aa68e1..86bd68acef0c 100644 --- a/docs/contributing/profiling.md +++ b/docs/contributing/profiling.md @@ -22,6 +22,10 @@ To use the `torch.profiler` module, set the `profiler` entry to `'torch'` and `t - `torch_profiler_with_flops` to enable recording FLOPs, off by default - `torch_profiler_use_gzip` to control gzip-compressing profiling files, on by default - `torch_profiler_dump_cuda_time_total` to control dumping and printing the aggregated CUDA self time table, on by default +- `torch_profiler_activities` to select worker activities. Defaults are + platform-specific: `["CPU"]` on CPU, `["CPU", "CUDA"]` on NVIDIA GPUs, and + `["CPU", "XPU"]` on Intel GPUs. Selecting `["CUDA"]` omits CPU annotations + and the AsyncLLM CPU trace to reduce profiling overhead and trace size. When using `vllm bench serve`, you can enable profiling by passing the `--profile` flag. @@ -90,18 +94,18 @@ $ curl -X POST http://localhost:8000/stop_profile is Triton's GPU profiler. It can collect a low-overhead aggregate tree or a Chrome trace and works through the same vLLM profiling controls as the PyTorch and CUDA profilers. Proton currently supports NVIDIA GPUs through CUPTI and -requires eager execution. +supports CUDA graph attribution. -Start a server with a local output directory: +Start a server with a local output directory and graph attribution: ```bash vllm serve meta-llama/Llama-3.1-8B-Instruct \ - --enforce-eager \ --profiler-config '{ "profiler": "proton", "proton_profiler_dir": "./proton_profile", "proton_output_format": "hatchet", - "proton_hook": "triton" + "proton_hook": "triton", + "proton_graph_attribution": true }' ``` @@ -111,7 +115,7 @@ rank-qualified output name, such as `proton_dp0_pp0_tp0_dcp0_ep0_rank0_pid1234_0123456789abcdef0123456789abcdef_run0.hatchet`, so distributed workers, restarted servers, and repeated profiling runs do not overwrite one another. A `profile_prefix` is included when supplied. Each -profile is finalized by `/stop_profile` and is ready to inspect immediately. +profile is written by `/stop_profile` and is ready to inspect immediately. The Proton-specific options are: @@ -121,21 +125,39 @@ The Proton-specific options are: - `proton_mode`: an optional backend mode string - `proton_hook`: `triton` to record Triton launch metadata, or unset - `proton_output_format`: `hatchet`, `hatchet_msgpack`, `chrome_trace`, or unset +- `proton_graph_attribution`: observe CUDA graph capture for replay attribution; + disabled by default and requires `proton_data: "tree"` `hatchet` and `hatchet_msgpack` require `proton_data: "tree"`, while -`chrome_trace` requires `proton_data: "trace"`. CUDA graph profiling is not yet -supported, so `--enforce-eager` is required for every Proton mode. +`chrome_trace` requires `proton_data: "trace"`. Automatic backend selection is recommended. vLLM currently supports Proton's `cupti` backend on NVIDIA GPUs. ROCm support is not yet available. vLLM does not expose Proton's experimental instrumentation backend because current upstream -Triton builds can produce profiles without timing metrics. Backend-specific -modes, including `pcsampling`, can be selected with `proton_mode`. - -Triton 3.6 supports explicit `hatchet` and `chrome_trace` output. The -`hatchet_msgpack` format and `periodic_flushing` mode require Triton 3.7 or -newer; vLLM rejects these options with the detected version before starting a -session. +can produce profiles without timing metrics. When `proton_graph_attribution` is +enabled, Proton observes vLLM's CUDA graph capture with the configured profiling +session active, then deactivates that same session until profiling starts. This +lets later profiles attribute replayed kernels without retaining model-startup +activity. Backend-specific modes can be selected with `proton_mode`; +`pcsampling` synchronizes the CUDA context and therefore requires +`--enforce-eager`. When CUDA graphs are enabled (including encoder graphs), +Proton requires `proton_graph_attribution: true` to collect replayed kernels. +For Chrome traces, disable CUDA graphs with `--enforce-eager`. + +CUDA graph-attributed profiles support repeated `start_profile`/`stop_profile` +runs. Each stop flushes and writes one tree-data phase while preserving the +graph-aware session. Without `proton_graph_attribution`, each `stop_profile` +instead finalizes and writes an independent Proton session. + +CUDA graph attribution requires Triton 3.7 or newer. It uses the phase data API +to discard graph-capture activity and separate profiling runs. The `hatchet_msgpack` +output format and `periodic_flushing` mode also require Triton 3.7 or newer. +`periodic_flushing` cannot be combined with graph attribution because both +manage the session's data phases. Ordinary Proton profiling remains available +with Triton 3.6. + +Graph attribution retains capture metadata for the worker lifetime. With eager +execution or no graphs to capture, profiling uses ordinary independent sessions. Inspect tree profiles with: diff --git a/docs/contributing/vulnerability_management.md b/docs/contributing/vulnerability_management.md index 8dd908a7ce5d..a73d451a6217 100644 --- a/docs/contributing/vulnerability_management.md +++ b/docs/contributing/vulnerability_management.md @@ -33,9 +33,7 @@ on GitHub. However, if you need to contact the VMT directly for an urgent issue, you may contact the following individuals: - Simon Mo - -- Russell Bryant - - Juan Pérez de Algaba - -- Huzaifa Sidhpurwala - ## Slack Discussion diff --git a/docs/deployment/frameworks/nebius.md b/docs/deployment/frameworks/nebius.md new file mode 100644 index 000000000000..94d66c1de8b7 --- /dev/null +++ b/docs/deployment/frameworks/nebius.md @@ -0,0 +1,148 @@ +# Nebius Serverless AI + +[Nebius Serverless AI](https://docs.nebius.com/serverless/overview) runs GPU containers as persistent HTTP endpoints or finite jobs. This guide deploys the vLLM OpenAI-compatible server as a single-GPU Endpoint using the Nebius CLI. + +The Endpoint keeps running until you stop or delete it. This recipe uses a regular GPU instance; it does not configure autoscaling or automatic scale-to-zero. + +## Prerequisites + +- A Nebius project with permission to create and delete Serverless Endpoints, and sufficient GPU, VM, disk, and networking quotas. +- The [Nebius CLI](https://docs.nebius.com/cli/install), installed and authenticated for that project. +- A subnet in `eu-north1` with outbound access to Docker Hub and Hugging Face. The example uses the `gpu-l40s-a` platform and `1gpu-8vcpu-32gb` preset; check availability in your project before starting. +- `curl`, `jq`, and `openssl` on your local machine. + +The example model, `Qwen/Qwen3-0.6B`, does not require a Hugging Face token. Other models may require accepting their license and supplying a token through Nebius [secret environment variables](https://docs.nebius.com/serverless/endpoints/manage). + +## Create the Endpoint + +The commands below were tested with Nebius CLI `0.12.265`. Set your project and subnet IDs explicitly. Use the same CLI profile throughout the guide. + +```bash +export PROJECT_ID="" +export SUBNET_ID="" +export ENDPOINT_NAME="vllm-qwen-$(openssl rand -hex 6)" +export AUTH_TOKEN="$(openssl rand -hex 32)" +export MODEL_ID="Qwen/Qwen3-0.6B" +export MODEL_REVISION="c1899de289a04d12100db370d81485cdf75e47ca" +# vllm/vllm-openai:v0.19.1, Linux/amd64 image digest. +export VLLM_IMAGE="vllm/vllm-openai@sha256:89c1d0629d377daa3f7f369cbea6167a7b48ea89aaacd12555e2b0b2f7f740d3" + +nebius ai endpoint create \ + --parent-id "$PROJECT_ID" \ + --subnet-id "$SUBNET_ID" \ + --name "$ENDPOINT_NAME" \ + --image "$VLLM_IMAGE" \ + --container-command vllm \ + --args "serve $MODEL_ID --revision $MODEL_REVISION --tokenizer-revision $MODEL_REVISION --host 0.0.0.0 --port 8000 --tensor-parallel-size 1 --max-model-len 4096 --gpu-memory-utilization 0.8" \ + --platform gpu-l40s-a \ + --preset 1gpu-8vcpu-32gb \ + --container-port 8000/http \ + --auth token --token "$AUTH_TOKEN" \ + --disk-size 250Gi --shm-size 16Gi \ + --public=false --preemptible=false \ + --retries 1 +``` + +The image and model revisions are pinned for reproducibility. When changing the image, check its CUDA/driver requirements against the selected Nebius platform. The model downloads into the container disk on startup; this example does not configure persistent model storage. + +The Endpoint token is separate from your Nebius CLI credentials. Keep it private. Nebius token authentication requires exactly one HTTP port. This example relies on authentication at the managed HTTPS URL; it does not set a second vLLM API key. Access from inside the subnet must be restricted to trusted clients. + +A public VM IP is not required to use the managed HTTPS URL. The subnet still needs outbound connectivity for image and model downloads. + +Save the ID of the Endpoint you created: + +```bash +export ENDPOINT_ID="$(nebius ai endpoint get-by-name \ + --parent-id "$PROJECT_ID" --name "$ENDPOINT_NAME" \ + --format jsonpath='{.metadata.id}')" +``` + +If creation times out or your terminal disconnects, use the same project and name to find the Endpoint before attempting another creation. A local timeout does not mean that provisioning was cancelled. + +## Check readiness + +Inspect the state and recent logs: + +```bash +nebius ai endpoint get "$ENDPOINT_ID" +nebius ai endpoint logs "$ENDPOINT_ID" --tail 100 --timestamps +``` + +Wait for `RUNNING`, then select the managed HTTPS URL. The URL can appear while the Endpoint is still `STARTING`, when ingress may return HTTP 404. The `jq` expression requires exactly one HTTPS URL and preserves its scheme: + +```bash +ENDPOINT_URL="$(nebius ai endpoint get "$ENDPOINT_ID" --format json \ + | jq -er '[.status.public_endpoints[]? | select(startswith("https://"))] + | if length == 1 then .[0] else error("Expected one HTTPS URL") end')" +export ENDPOINT_URL="${ENDPOINT_URL%/}" + +curl --fail-with-body --silent --show-error --max-time 10 \ + "$ENDPOINT_URL/health" -H "Authorization: Bearer $AUTH_TOKEN" +curl --fail-with-body --silent --show-error --max-time 10 \ + "$ENDPOINT_URL/v1/models" -H "Authorization: Bearer $AUTH_TOKEN" | jq +``` + +`RUNNING` alone does not establish that the model has finished loading. Wait for `/health` to succeed and `/v1/models` to list `Qwen/Qwen3-0.6B` before sending a chat request. While waiting, inspect logs instead of creating another Endpoint. If startup does not succeed within your chosen time or spending limit, delete the Endpoint and investigate the error. + +## Send a chat request + +```bash +curl --fail-with-body --silent --show-error --max-time 120 \ + "$ENDPOINT_URL/v1/chat/completions" \ + -H "Authorization: Bearer $AUTH_TOKEN" \ + -H 'Content-Type: application/json' \ + -d '{ + "model": "Qwen/Qwen3-0.6B", + "messages": [{"role": "user", "content": "Say hello in one short sentence."}], + "max_tokens": 128, + "temperature": 0, + "chat_template_kwargs": {"enable_thinking": false} + }' | jq +``` + +Expect a chat completion containing an assistant message. The Qwen-specific template option disables thinking for this short example; it is not a universal option for other models. + +For streaming, add `"stream": true` to the JSON and use `curl -N` without piping into `jq`. A successful stream contains completion chunks followed by `data: [DONE]`. For an OpenAI-compatible client, use `${ENDPOINT_URL}/v1` as the base URL and the Endpoint token as the API key. + +Verify that the same inference URL rejects requests without the token: + +```bash +curl --silent --show-error --max-time 10 --output /dev/null \ + --write-out '%{http_code}\n' "$ENDPOINT_URL/v1/models" +``` + +Expect HTTP `401` or `403`. Do not share an Endpoint that accepts unauthenticated inference unexpectedly. + +## Stop or delete the Endpoint + +To stop serving while preserving the Endpoint configuration: + +```bash +nebius ai endpoint stop "$ENDPOINT_ID" +nebius ai endpoint get "$ENDPOINT_ID" +``` + +Let the synchronous stop command complete and confirm `STOPPED` before starting again. To start it again, run `nebius ai endpoint start "$ENDPOINT_ID"`, retrieve its current URL, and repeat the readiness checks. Starting again can require fresh capacity and new image/model downloads; do not rely on the container disk as persistent storage. + +When finished, delete the Endpoint and confirm that getting the same ID returns `NotFound`: + +```bash +nebius ai endpoint delete "$ENDPOINT_ID" +nebius ai endpoint get "$ENDPOINT_ID" +unset AUTH_TOKEN +``` + +Nebius deletes the managed VM and container disk with the Endpoint. Separately mounted storage has its own lifecycle and billing. Closing your terminal or interrupting a request does not stop the Endpoint. If deletion fails, keep the ID and retry cleanup; a generic connection or authentication error is not confirmation of deletion. + +## Troubleshooting + +| Symptom | Check | +| --- | --- | +| `PROVISIONING` or `NotEnoughResources` | Inspect quota, region, platform and preset. Capacity availability can differ from quota. | +| `RUNNING`, but HTTP 502/503 | Check model-download and engine-start logs, subnet egress, host binding and port 8000. | +| HTTP 401/403 | Use the Endpoint token, not the Nebius control-plane token. | +| CUDA or out-of-memory error | Check image/driver compatibility, the model's memory requirements and maximum context length. | +| Model not found | Verify the requested model ID and pinned revision. | +| Interrupted stream | Inspect ingress and server logs. A partial response should not be automatically replayed as a new generation. | + +See the [Nebius vLLM cookbook example](https://github.com/nebius/serverless-ai-cookbook/tree/main/inference/vllm-endpoint) and [Endpoint management documentation](https://docs.nebius.com/serverless/endpoints/manage) for further details. diff --git a/docs/deployment/integrations/kthena.md b/docs/deployment/integrations/kthena.md index 0285b733c6b8..73dcb3dafd72 100644 --- a/docs/deployment/integrations/kthena.md +++ b/docs/deployment/integrations/kthena.md @@ -2,7 +2,9 @@ [**Kthena**](https://github.com/volcano-sh/kthena) is a Kubernetes-native LLM inference platform that transforms how organizations deploy and manage Large Language Models in production. Built with declarative model lifecycle management and intelligent request routing, it provides high performance and enterprise-grade scalability for LLM inference workloads. -This guide shows how to deploy a production-grade, **multi-node vLLM** service on Kubernetes. +Kthena manages inference workloads through `ModelServing` and provides model-aware request routing through `ModelServer` and `ModelRoute`. + +This guide shows how to deploy a **multi-node vLLM** service on Kubernetes using `ModelServing` and expose it through a Kubernetes Service. We’ll: @@ -18,7 +20,7 @@ You need: - A Kubernetes cluster with **GPU nodes**. - `kubectl` access with cluster-admin or equivalent permissions. -- **Volcano** installed for gang scheduling. +- **Volcano 1.14 or later** for the role-level gang scheduling used in this example. - **Kthena** installed with the `ModelServing` CRD available. - A valid **Hugging Face token** if loading models from Hugging Face Hub. @@ -30,21 +32,22 @@ helm repo update helm install volcano volcano-sh/volcano -n volcano-system --create-namespace ``` -This provides the gang-scheduling and network topology features used by Kthena. +Volcano provides the gang scheduling used in this example. It is optional for Kthena deployments that do not use Volcano scheduling. ### 1.2 Install Kthena ```bash -helm install kthena oci://ghcr.io/volcano-sh/charts/kthena --version v0.1.0 --namespace kthena-system --create-namespace +helm install kthena oci://ghcr.io/volcano-sh/charts/kthena --version v1.0.0 --namespace kthena-system --create-namespace ``` - The `kthena-system` namespace is created. -- Kthena controllers and CRDs, including `ModelServing`, are installed and healthy. +- Kthena controllers and CRDs, including `ModelServing`, are installed. Validate: ```bash -kubectl get crd | grep modelserving +kubectl get pods -n kthena-system +kubectl get crd modelservings.workload.serving.volcano.sh ``` You should see: @@ -57,7 +60,7 @@ modelservings.workload.serving.volcano.sh ... ## 2. The Multi-Node vLLM `ModelServing` Example -Kthena provides an example manifest to deploy a **multi-node vLLM cluster running Llama**. Conceptually this is equivalent to the vLLM production stack Helm deployment, but expressed with `ModelServing`. +Kthena provides an example manifest to deploy a **multi-node vLLM cluster running Llama**. Each role replica contains an entry pod and its worker pods. A simplified version of the example (`llama-multinode`) looks like: @@ -116,10 +119,9 @@ Commands: - -c - > bash /vllm-workspace/examples/ray_serving/multi-node-serving.sh - leader --ray_cluster_size=2; python3 -m - vllm.entrypoints.launchers.api_server.entry --port 8080 --model - meta-llama/Llama-3.1-405B-Instruct --tensor-parallel-size 8 - --pipeline-parallel-size 2 + leader --ray_cluster_size=2; + vllm serve meta-llama/Llama-3.1-405B-Instruct --port 8080 --tensor-parallel-size 8 + --pipeline-parallel-size 2 --distributed-executor-backend ray ``` Worker: @@ -166,9 +168,9 @@ Save one of the following manifests to `modelserving.yaml`: restartGracePeriodSeconds: 60 gangPolicy: minRoleReplicas: - 405b: 1 + llama-405b: 1 roles: - - name: 405b + - name: llama-405b replicas: 2 entryTemplate: spec: @@ -256,9 +258,9 @@ Save one of the following manifests to `modelserving.yaml`: restartGracePeriodSeconds: 60 gangPolicy: minRoleReplicas: - 405b: 1 + llama-405b: 1 roles: - - name: 405b + - name: llama-405b replicas: 2 entryTemplate: spec: @@ -275,7 +277,7 @@ Save one of the following manifests to `modelserving.yaml`: - sh - -c - "bash /vllm-workspace/examples/ray_serving/multi-node-serving.sh leader --ray_cluster_size=2; - vllm serve meta-llama/Llama-3.1-405B-Instruct --port 8080 --tensor-parallel-size 8 --pipeline-parallel-size 2" + vllm serve meta-llama/Llama-3.1-405B-Instruct --port 8080 --tensor-parallel-size 8 --pipeline-parallel-size 2 --distributed-executor-backend ray" resources: limits: nvidia.com/gpu: "8" @@ -383,24 +385,24 @@ kubectl get pod -owide -l modelserving.volcano.sh/name=llama-multinode Example output (from docs): ```text -NAMESPACE NAME READY STATUS RESTARTS AGE IP NODE ... -default llama-multinode-0-405b-0-0 1/1 Running 0 15m 10.244.0.56 192.168.5.12 ... -default llama-multinode-0-405b-0-1 1/1 Running 0 15m 10.244.0.58 192.168.5.43 ... -default llama-multinode-0-405b-1-0 1/1 Running 0 15m 10.244.0.57 192.168.5.58 ... -default llama-multinode-0-405b-1-1 1/1 Running 0 15m 10.244.0.53 192.168.5.36 ... +NAMESPACE NAME READY STATUS RESTARTS AGE IP NODE ... +default llama-multinode-0-llama-405b-0-0 1/1 Running 0 15m 10.244.0.56 192.168.5.12 ... +default llama-multinode-0-llama-405b-0-1 1/1 Running 0 15m 10.244.0.58 192.168.5.43 ... +default llama-multinode-0-llama-405b-1-0 1/1 Running 0 15m 10.244.0.57 192.168.5.58 ... +default llama-multinode-0-llama-405b-1-1 1/1 Running 0 15m 10.244.0.53 192.168.5.36 ... ``` Pod name pattern: - `llama-multinode----`. -The first number indicates `ServingGroup`. The second (`405b`) is the `Role`. The remaining indices identify the pod within the role. +The first index identifies the `ServingGroup`, followed by the role name (`llama-405b`), role replica index, and pod index. --- -## 6. Accessing the vLLM OpenAI-Compatible API +## 5. Accessing the vLLM OpenAI-Compatible API -Expose the entry via a Service: +Save the following Service as `service.yaml` to expose the entry pods: ```yaml apiVersion: v1 @@ -412,7 +414,6 @@ spec: selector: modelserving.volcano.sh/name: llama-multinode modelserving.volcano.sh/entry: "true" - # optionally further narrow to leader role if you label it ports: - name: http port: 80 @@ -420,9 +421,10 @@ spec: type: ClusterIP ``` -Port-forward from your local machine: +Apply the Service, then port-forward from your local machine: ```bash +kubectl apply -f service.yaml kubectl port-forward svc/llama-multinode-openai 30080:80 -n default ``` @@ -450,11 +452,12 @@ You should see an OpenAI-style response from vLLM. --- -## 7. Clean Up +## 6. Clean Up To remove the deployment and its resources: ```bash +kubectl delete -f service.yaml kubectl delete modelserving llama-multinode -n default ``` @@ -464,3 +467,5 @@ If you’re done with the entire stack: helm uninstall kthena -n kthena-system # or your Kthena release name helm uninstall volcano -n volcano-system ``` + +For model-aware routing and prefill-decode disaggregation, see the [Kthena documentation](https://kthena.volcano.sh/docs/user-guide/router-routing). diff --git a/docs/design/attention_backends.md b/docs/design/attention_backends.md index 3fdc23ff8422..d175add40b84 100644 --- a/docs/design/attention_backends.md +++ b/docs/design/attention_backends.md @@ -56,6 +56,55 @@ llm = LLM( ## Backend Selection Behavior +### Triton/FlashAttention Composite + +On Hopper, `TRITON_FLASH_ATTN` is preferred for compatible multimodal-prefix +configurations. It uses Triton when the current queries need bidirectional +image attention and FlashAttention for causal text prefills and decode, with a +shared KV cache. The causal child must resolve to FA4. This happens +automatically for FA4-only shapes such as head size 512 and models whose +version policy promotes all layers to FA4, including Gemma 4. A standalone +head-size-256 configuration whose version policy selects FA3 falls back to +Triton for the whole layer. + +No attention override is needed for supported multimodal models: + +```bash +vllm serve google/gemma-4-31B-it +``` + +### Triton/FlashInfer Composite + +On Blackwell, `TRITON_FLASHINFER` is preferred for compatible +multimodal-prefix configurations, including Gemma 4 with BF16 or FP8 KV cache. It uses +Triton when a batch's current queries require bidirectional image attention, +and FlashInfer for causal text prefills and decode. Historical image tokens +alone do not select Triton. + +This instantiates `create_composite_attention_backend` from +`vllm/v1/attention/backends/composite.py` with Triton, +FlashInfer, and `MMPrefixAttentionRouting`. The reusable factory owns child +implementations, metadata dispatch, compatible cache requirements, and workspace +sharing. The routing policy selects the child and defines graph-capture safety. +Other combinations can reuse the same machinery. + +The backend supports head dimensions 256/512, FP16/BF16 and FP8 KV cache, +and 64-token kernel pages with a head-major cache layout. TRTLLM handles causal +attention at both head dimensions; its hdim512 kernels do not support 128-token +pages. +For Gemma 4, full CUDA graphs cover single-token batches; multi-token batches +use the non-full-graph execution path. Models whose image masks extend beyond +the sliding window cannot use full attention graphs with this composite. +Context parallelism, R-SWA, attention sinks, +and adaptive verification are not supported by this composite. + +It can also be selected explicitly: + +```bash +vllm serve google/gemma-4-31B-it \ + --attention-backend TRITON_FLASHINFER +``` + ### Manual Selection When you explicitly set a backend via `--attention-backend` or `AttentionConfig`: diff --git a/docs/design/hisparse.md b/docs/design/hisparse.md new file mode 100644 index 000000000000..53a309371783 --- /dev/null +++ b/docs/design/hisparse.md @@ -0,0 +1,254 @@ +# HiSparse local KV offload architecture + +Status: experimental + +## The short version + +There are three different jobs: + +1. The normal KV cache system manages GPU block pools and tables. +2. `HiSparseCoordinator` manages logical host blocks, source-prefix identity, and + host/GPU residency transitions. +3. `HiSparseConnector` carries residency work between scheduler and worker; + `HiSparseWorker` coordinates transfers, while per-cache + `HiSparseRuntime` objects own host/hot views and GPU replacement state. + +Neither worker-side object allocates or frees logical blocks. HMA provides the +GPU allocation shared by the resident and hot groups; it does not manage CPU +memory or KV identity. + +```text +request ────► HiSparseCoordinator ── source blocks + residency policy + │ + ├──► KV cache manager ── resident/hot GPU leases (HMA) + │ + └──► HiSparseConnector ── host bytes + copies + GPU LRU +``` + +This is a local KV connector. When P/D or another offload connector is also +configured, `MultiConnector` composes it with `HiSparseConnector`. + +`host_pool_gib` is configured on `HiSparseConnector` and is the usable +host-cache capacity per data-parallel replica, not a node-wide memory budget. +Tensor-parallel ranks hold replicated views of that logical cache. Those views +may use private per-rank backing or one shared +physical allocation without changing the configured capacity. Physical host +memory consumption is therefore topology- and implementation-dependent. The +realized capacity may be slightly smaller because the budget is rounded down +to complete host blocks. + +## Ownership + +| Thing | Owner | What “owner” means | +| --- | --- | --- | +| HiSparse source and prefix identity | `HiSparseCoordinator` | maps tokens to logical host blocks | +| Resident GPU block leases | normal KV cache manager | allocates and frees HMA blocks | +| Resident block tables | normal KV cache manager | tells attention where resident pages are | +| Residency transitions | `HiSparseCoordinator` | plans spill-before-free transactions | +| Logical host block allocation | `HiSparseCoordinator` | owns the separate CPU block pool and its lifecycle | +| Pinned host-pool lifecycle | `HiSparseWorker` | worker-wide backing and teardown | +| Per-cache host view and hot contents | `HiSparseRuntime` | binds host/hot storage and fills cache-manager-provided hot leases | +| Hot row map and LRU | `HiSparseRuntime` | resolves hits and chooses victims on GPU | +| Resident-cache route | `HiSparseCacheHandle` | exposes resident or host/hot resolution to attention | +| Sparse attention | attention backend | consumes a device cache and physical row IDs | +| HMA | allocator | provides GPU capacity; owns no KV meaning | + +The key distinction is logical allocation versus contents. `HiSparseCoordinator` +owns host block IDs and their request/prefix associations. `HiSparseWorker` and +its per-cache runtimes own the corresponding bytes. The normal cache manager +sees only device pools. +The source group has `block_pool_id=None`; device-pool consumers must narrow it +before indexing, so host ownership cannot masquerade as a numeric GPU pool. + +For single-node MP tensor parallelism, every TP worker maps the same pinned host +pool and uses the same block and layer offsets. MLA source KV is replicated +across TP ranks, so this stores one physical copy instead of one copy per rank. +TP rank 0 writes the shared host pool; peers wait on its IPC events before +reading it. Other executor and parallel layouts retain private per-rank pools. + +The shared layout backs the per-replica logical capacity with one physical pool; +the private layout allocates one physical pool per rank. Physical pool size +includes block-stride alignment. + +## Code boundary + +```text +scheduler process worker process + +HiSparseConnector HiSparseConnector + └─ HiSparseCoordinator └─ HiSparseWorker + │ │ + │ connector metadata ├─ host bytes + │ - page transfers ├─ copy scheduling + │ - block-table replacements └─ per-layer hot state + └───────────────────────────────────────────────►│ + ◄──────── connector worker metadata ─────────────┘ + enqueued and completed transfer IDs +``` + +The command travels in `kv_connector_metadata`; transfer updates return in +`KVConnectorOutput.kv_connector_worker_meta`. The model runner does not +interpret page transfers. Enqueue acknowledgements let the scheduler release +source leases in stream order; completion acknowledgements publish the copied +host pages. + +## Resident device pages + +Resident pages are intentionally outside `HiSparseRuntime`. + +KV-cache initialization binds cache-manager allocations to the attention-facing +`HiSparseCacheHandle` before constructing `HiSparseWorker`. That same +handle's runtime retains the resident source index needed by a transfer plan. +There is no second resident object or registration wrapper. + +```text +KV cache setup + │ + ├─ bind resident allocation ──► HiSparseCacheHandle + │ cache + block table + slot mapping + │ + ├─ bind host/hot allocation ──► HiSparseRuntime + │ host + hot + GPU LRU + │ + └─ register cache handles ────► HiSparseWorker + step-level transfers + +HiSparseWorker registers the same HiSparseCacheHandle objects directly +``` + +Attention construction links each layer to the most recent layer that actually +owns an indexer. This releases a follower's duplicate LRU tensors before GPU +memory profiling. Cache binding only attaches storage; it does not infer +semantic groups from the physical packed-tensor order. The construction cursor +is discarded with the worker's pinned state. + +Every HiSparse decode batch uses the same fused resolver. It checks resident +pages first, then hot rows, then pinned host memory. A resident hit exits inside +the kernel before hot-LRU lookup or host copying; there is no framework-level +residency route or separate CUDA graph. No CPU decision is added to the decode +path. The resolver consumes the existing graph-stable request mapping from +attention metadata; neither the worker nor individual cache handles keep a +duplicate mapping. + +Speculative decoding resolves and consumes each verification step in order. +Each step receives distinct replayable plan rows while sharing the request's +hot-cache state, so a later step cannot reuse a hot row before an earlier step +has consumed it. + +## P/D import target + +The decoder chooses the landing target once per request from the normal cache +admission calculation. If the complete imported prefix fits the device pools, +NIXL transfers it directly into resident GPU pages. Otherwise, if the fixed +host-backed GPU footprint and host source blocks fit, the request imports into +the host tier. There is no context-length threshold or other heuristic, and a +request waiting for capacity retains its choice across admission retries. + +A host import reads through a bounded decoder-GPU staging pool before copying +into registered host memory. Pages needed immediately are mirrored into their +resident destinations during that copy. Both landing targets then use the same +fused decode resolver described above. + +## Indexer KV offloading + +HiSparse does not keep a private CPU copy of indexer KV. The indexer remains a +normal prefix-cacheable GPU cache group. If `OffloadingConnector` is configured +with HiSparse, it stores and restores that group through the generic KV +offloading path; HiSparse continues to own only the sparse MLA host tier. + +The two prefix sources can have different hit lengths. When the HiSparse host +prefix extends beyond the GPU-resident indexer prefix, the scheduler asks +`OffloadingConnector` to restore only the missing indexer suffix, capped at the +host prefix boundary. If that suffix is unavailable, all groups fall back to +the shorter prefix they share. NIXL P/D transfers continue to place indexer KV +directly in its GPU group. + +## Spill transaction + +A resident block cannot be reused until its contents have been handed to the +worker. + +```text +HiSparseCoordinator HiSparseWorker + │ │ + │ pin source and destination leases │ + │── SparseKVPageTransfer ─────────────►│ + │ │ enqueue GPU-to-host copy + │◄── enqueued transfer ID ────────────│ + │ replace resident table entry │ + │ release resident lease to HMA │ + │ │ copy reaches its event + │◄── completed transfer ID ───────────│ + │ mark host page valid │ + │ release destination host lease │ +``` + +“Enqueued” means the copy has entered the worker stream. Stream ordering makes +it safe to reuse the resident GPU block for later work, but the host page is +not yet published. “Completed” means the worker has observed the copy's event; +only then does the coordinator publish the host page for prefix reuse and +release its destination lease. A host-write event separately protects direct +CPU readers from writes already queued on the accelerator. + +The worker transfer contains only its transfer ID and physical copy +coordinates. Request identity and logical page state remain in the scheduler. + +## Hot lookup and LRU + +The NVIDIA CUDA path keeps replacement entirely on the accelerator: + +```text +top-K logical positions + │ + ▼ +resident page? ── yes ──► resident physical row + │ no + ▼ +hot row? ──────── yes ──► existing hot physical row + update GPU LRU + │ no + ▼ +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. + +## Main classes + +| Class | Inherits / implements | Responsibility | +| --- | --- | --- | +| `HiSparseCoordinator` | plain scheduler component | host allocation, source prefixes, resident leases, and spill state machine | +| `HiSparseConnector` | `KVConnectorBase_V1`, `SupportsHMA` | scheduler/worker metadata and lifecycle boundary | +| `HiSparseResidentManager` | `SingleTypeKVCacheManager` | normal block-pool bookkeeping with host-backed holes | +| `PagedCacheView` | immutable data object | shared resident/hot HMA tensor binding | +| `HiSparseWorker` | connector-owned worker component | worker-wide transfer scheduling and host-pool lifecycle | +| `HiSparseRuntime` | plain worker-owned component | per-cache host/hot tensors, GPU LRU, and fused resolution | +| `HiSparseCacheHandle` | plain attention component | resident view and fused cache resolution | +| `SparseKVOffloadCommand` | dataclass | opaque scheduler-to-worker work | + +## Performance invariants + +- Resident hits bypass hot-LRU lookup and host copies inside the fused resolver. +- Hot lookup, victim selection, and LRU updates stay on the GPU. +- A hot miss still copies directly from registered pinned host memory. +- Top-K resolution stays inside the attention invocation and remains graph + capturable. +- Compatible layers still share one miss plan. +- Index-sharing followers release their private LRU state before memory sizing. +- Indexer KV is untouched by HiSparse unless a generic KV offloader is configured. +- Resident and hot leases can still share one packed HMA allocation. +- No device scalar readback or CPU/device metadata round trip is added. +- The abstraction wraps the fused kernel; it does not add another kernel + launch. +- When HiSparse is disabled, the scheduler does not construct an offload + command or empty update table. + +## What remains platform-specific + +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. diff --git a/docs/design/metrics.md b/docs/design/metrics.md index c7bc85756e0d..3e070f588890 100644 --- a/docs/design/metrics.md +++ b/docs/design/metrics.md @@ -33,7 +33,8 @@ In v1, an extensive set of metrics are exposed via a Prometheus-compatible `/met - `vllm:request_prompt_tokens` (Histogram) - Histogram of input prompt token counts. - `vllm:request_generation_tokens` (Histogram) - Histogram of generation token counts. - `vllm:time_to_first_token_seconds` (Histogram) - Time to first token (TTFT). -- `vllm:inter_token_latency_seconds` (Histogram) - Inter-token latency. +- `vllm:inter_token_latency_seconds` (Histogram) - Inter-token latency (time between consecutive streamed outputs). +- `vllm:request_time_per_output_token_seconds` (Histogram) - Per-request Time Per Output Token (TPOT). - `vllm:e2e_request_latency_seconds` (Histogram) - End-to-end request latency. - `vllm:request_prefill_time_seconds` (Histogram) - Request prefill time. - `vllm:request_decode_time_seconds` (Histogram) - Request decode time. @@ -49,7 +50,8 @@ The subset of metrics exposed in the Grafana dashboard gives us an indication of - `vllm:e2e_request_latency_seconds_bucket` - End to end request latency measured in seconds. - `vllm:prompt_tokens` - Prompt tokens. - `vllm:generation_tokens` - Generation tokens. -- `vllm:inter_token_latency_seconds` - Inter-token latency (Time Per Output Token, TPOT) in seconds. +- `vllm:inter_token_latency_seconds` - Inter-token latency in seconds, measured between consecutive streamed outputs. It approximates TPOT when each output contains exactly one token, but differs from request-level TPOT when an output contains multiple tokens or when aggregation weights differ. +- `vllm:request_time_per_output_token_seconds` - Per-request Time Per Output Token (TPOT) in seconds, computed as `(end-to-end latency - TTFT) / (number of output tokens - 1)`. It is recorded as zero for requests that generate no more than one token. - `vllm:time_to_first_token_seconds` - Time to First Token (TTFT) latency in seconds. - `vllm:num_requests_running` (also, `_swapped` and `_waiting`) - Number of requests in the RUNNING, WAITING, and SWAPPED states. - `vllm:kv_cache_usage_perc` - Percentage of used cache blocks by vLLM. @@ -63,6 +65,20 @@ The subset of metrics exposed in the Grafana dashboard gives us an indication of See [the PR which added this Dashboard](https://github.com/vllm-project/vllm/pull/2316) for interesting and useful background on the choices made here. +`vllm:inter_token_latency_seconds` records one sample per streamed output event +(the wall-clock gap between successive outputs), whereas +`vllm:request_time_per_output_token_seconds` is recorded once per finished request +as `(end-to-end latency - TTFT) / (number of output tokens - 1)`. These two +metrics differ when an output bundles multiple tokens (e.g. speculative +decoding) or when mean ITL and mean per-request TPOT are aggregated over +differing weights. See [Understanding the latency metrics](../benchmarking/cli.md#understanding-the-latency-metrics) +for the benchmark definitions. Requests that generate no more than one token +are recorded with a TPOT of zero, while `vllm bench serve` excludes them from +its TPOT statistics. Therefore, the Prometheus histogram and benchmark TPOT +statistics can differ. Use `vllm:request_time_per_output_token_seconds` for +request-level TPOT and `vllm:inter_token_latency_seconds` when you specifically +want the inter-output latency distribution. + ### Prometheus Client Library Prometheus support was initially added [using the aioprometheus library](https://github.com/vllm-project/vllm/pull/1890), but a switch was made quickly to [prometheus_client](https://github.com/vllm-project/vllm/pull/2730). The rationale is discussed in both linked PRs. @@ -238,8 +254,7 @@ statistics relating to that iteration: iteration. - The prefill intervals for any requests that completed prefill in this iteration. -- The inter-token intervals (Time Per Output Token, TPOT), for all - requests included in this iteration. +- The inter-token intervals, for all requests included in this iteration. - The Time-To-First-Token (TTFT) for any requests that completed prefill in this iteration. However, we calculate this interval relative to when the request was first received by the frontend @@ -655,7 +670,7 @@ fall under the more general heading of "Observability". vLLM has support for OpenTelemetry tracing: - Added by and reinstated by -- Configured with `--oltp-traces-endpoint` and `--collect-detailed-traces` +- Configured with `--otlp-traces-endpoint` and `--collect-detailed-traces` - [OpenTelemetry blog post](https://opentelemetry.io/blog/2024/llm-observability/) - [User-facing docs](../../examples/observability/opentelemetry/README.md) - [Blog post](https://medium.com/@ronen.schaffer/follow-the-trail-supercharging-vllm-with-opentelemetry-distributed-tracing-aa655229b46f) diff --git a/docs/design/mm_processing.md b/docs/design/mm_processing.md index a2112a2922ed..202d8d04c166 100644 --- a/docs/design/mm_processing.md +++ b/docs/design/mm_processing.md @@ -8,9 +8,16 @@ In vLLM's rendering pipeline (see [BaseRenderer][vllm.renderers.base.BaseRendere Since Transformers 5.10, `ProcessorMixin` now allows multi-modal inputs to be passed by themselves. However, certain subclasses (such as `ChameleonProcessor`) and older out-of-tree implementations may still define their own `__call__` method that assumes the presence of text with corresponding placeholder tokens. This causes a problem as we don't have the original text anymore to pass to these processors. -To work around this, each model defines how to generate dummy text based on the number of multi-modal inputs, via [get_dummy_text][vllm.multimodal.processing.BaseDummyInputsBuilder.get_dummy_text], which its override of [_get_hf_processor_text][vllm.multimodal.processing.BaseMultiModalProcessor._get_hf_processor_text] returns so that [_apply_hf_processor_main][vllm.multimodal.processing.BaseMultiModalProcessor._apply_hf_processor_main] passes it to the HF processor together with the multi-modal inputs to obtain the processed multi-modal data. +To work around this, each model defines how to generate dummy text based on the number of multi-modal inputs, via [get_dummy_text][vllm.multimodal.processing.BaseDummyInputsBuilder.get_dummy_text], which its override of [_get_hf_mm_text][vllm.multimodal.processing.BaseMultiModalProcessor._get_hf_mm_text] returns. [_get_hf_mm_inputs][vllm.multimodal.processing.BaseMultiModalProcessor._get_hf_mm_inputs] adds this text and `truncation=False` to the normalized HF inputs. The truncation setting is attached only to calls that use dummy text, so unrelated processor calls do not receive a text-specific option. -Similarly, since the multi-modal data extracted by vLLM may not match what a specific HF processor expects, [_apply_hf_processor_main][vllm.multimodal.processing.BaseMultiModalProcessor._apply_hf_processor_main] allows each model to adapt the inputs via [_preprocess_hf_mm_data][vllm.multimodal.processing.BaseMultiModalProcessor._preprocess_hf_mm_data] (e.g. renaming keys like `audios` to `audio` or injecting extra keyword arguments such as `sampling_rate`) and the outputs via [_postprocess_hf_mm_data][vllm.multimodal.processing.BaseMultiModalProcessor._postprocess_hf_mm_data], without having to reimplement the entire method. +HF processing is split into four overridable stages: + +1. [_get_hf_mm_inputs][vllm.multimodal.processing.BaseMultiModalProcessor._get_hf_mm_inputs] extracts and normalizes processor data, kwargs, and passthrough data. It centrally renames vLLM's `audios` field to the standard HF `audio` keyword; processors that require another spelling can adapt it in an override. +2. [_call_hf_processor][vllm.multimodal.processing.BaseMultiModalProcessor._call_hf_processor] invokes the HF processor. Models can override it when they use a different callable or need separate construction and invocation kwargs. +3. [_finalize_hf_mm_data][vllm.multimodal.processing.BaseMultiModalProcessor._finalize_hf_mm_data] merges passthrough fields into the processed output. It lazily creates an empty output when there is no processor data. +4. [_postprocess_hf_mm_data][vllm.multimodal.processing.BaseMultiModalProcessor._postprocess_hf_mm_data] performs model-specific output transformations after passthrough fields have been merged. + +This division lets most models customize one stage without reimplementing [_apply_hf_processor_main][vllm.multimodal.processing.BaseMultiModalProcessor._apply_hf_processor_main]. Implementations that genuinely require multiple processor calls or manually construct their complete output may still override that method, but must use `_get_hf_mm_inputs` and `_finalize_hf_mm_data` to retain the common input and output behavior. ## Prompt Update Detection diff --git a/docs/design/nixl_kv_push_connector.md b/docs/design/nixl_kv_push_connector.md index 6f66be5eb9f5..a6debd7dba81 100644 --- a/docs/design/nixl_kv_push_connector.md +++ b/docs/design/nixl_kv_push_connector.md @@ -181,6 +181,50 @@ The completion notif sent from P to D after a WRITE is the existing is D's own request id, taken from the registration), so the D-side 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 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 (member) identity** 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 member routing derives its member-major layout 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 +member order. Descriptor offsets use each member's region capacity, so P and +D need not allocate the same number of blocks. Physical allocations are still +registered once, even when multiple members share them. + +Invariants enforced when the remote regions are aligned: + +* every locally owned member must be advertised exactly once by the + remote; a missing member fails the handshake rather than silently + leaving that layer's KV stale, and remote-only members (owned by other + PP stages) are ignored; +* a remote that omits member metadata while the local layout requires + member routing fails the handshake instead of falling back to + region-index routing; +* member 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. + +Decode-side PP is unsupported because completions are counted per +consumer rank. Mamba/SSM hybrids are unsupported under PP. + ## Scheduler-side responsibilities `NixlPushConnectorScheduler` extends the base scheduler with: diff --git a/docs/design/torch_compile.md b/docs/design/torch_compile.md index 4dc0da0c7d65..1ffc0ac53a40 100644 --- a/docs/design/torch_compile.md +++ b/docs/design/torch_compile.md @@ -27,6 +27,12 @@ With all these factors taken into consideration, usually we can guarantee that t A unique aspect of vLLM's `torch.compile` integration, is that we guarantee all the compilation finishes before we serve any requests. No requests will trigger new compilations. Otherwise, the engine would be blocked on that request, and the response time will have unexpected spikes. +An explicit `TRITON_CACHE_DIR` is preserved, allowing processes and model +configurations to reuse Triton artifacts. The directory can start empty: the +first compilation populates it and later matching specializations reuse it. +When copying compilation caches, also copy this directory if set. Cold-cache +tests should set it to a fresh directory. + By default, the cache saves compiled artifacts as binary files. If you would like to interact with the generated code for debugging purposes, set the field `compile_cache_save_format=unpacked` in the compilation config, or omit this and set the env variable `VLLM_COMPILE_CACHE_SAVE_FORMAT=unpacked`. ## Dynamic shapes and vllm guard dropping diff --git a/docs/features/README.md b/docs/features/README.md index 28362f401477..1df8960ee354 100644 --- a/docs/features/README.md +++ b/docs/features/README.md @@ -64,7 +64,7 @@ th:not(:first-child) { | [CP](../configuration/optimization.md#chunked-prefill) | [❌](https://github.com/vllm-project/vllm/issues/2729) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | [APC](automatic_prefix_caching.md) | [❌](https://github.com/vllm-project/vllm/issues/3687) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | [LoRA](lora.md) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | -| [SD](speculative_decoding/README.md) | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | +| [SD](speculative_decoding/README.md) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | CUDA graph | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | [❌](https://github.com/vllm-project/vllm/issues/26970) | | [pooling](../models/pooling_models/README.md) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | enc-dec | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | diff --git a/docs/features/automatic_prefix_caching.md b/docs/features/automatic_prefix_caching.md index 03cca3b38d6c..cfc8c19acb2b 100644 --- a/docs/features/automatic_prefix_caching.md +++ b/docs/features/automatic_prefix_caching.md @@ -15,7 +15,7 @@ Set `enable_prefix_caching=True` in vLLM engine to enable APC. Here is an exampl ## Hybrid Mamba models -Under `--mamba-cache-mode align`, Mamba state is stored only on the Mamba block grid, so a prefix-cache hit can resume only at a block boundary. `--enable-mamba-fine-grained-prefix-cache` also stores a checkpoint at the shared-prefix junction, the point where an earlier request with the same prefix stopped. Requests whose shared prefix ends inside a block can then reuse it. +Under `--mamba-cache-mode align`, Mamba state is stored only on the Mamba block grid, so a prefix-cache hit can resume only at a block boundary. `--enable-mamba-shared-prefix-checkpoint` also stores a checkpoint at the shared-prefix junction, the point where an earlier request with the same prefix stopped. Requests whose shared prefix ends inside a block can then reuse it. This helps when many requests share a long system prompt and then diverge. It is off by default, and takes effect only when all of the following hold: @@ -28,7 +28,7 @@ This helps when many requests share a long system prompt and then diverge. It is vllm serve \ --mamba-cache-mode align \ --prefix-match-unit 64 \ - --enable-mamba-fine-grained-prefix-cache + --enable-mamba-shared-prefix-checkpoint ``` `--prefix-match-unit` is required. It sets the granularity at which prefix-cache keys are computed. When unset it defaults to the greatest common divisor of the prefix-cacheable KV cache group block sizes. Under `align` that is the block size itself, so no sub-block boundary exists and the flag has no effect. diff --git a/docs/features/batch_invariance.md b/docs/features/batch_invariance.md index 789878f48a96..7353bd359462 100644 --- a/docs/features/batch_invariance.md +++ b/docs/features/batch_invariance.md @@ -145,6 +145,9 @@ When batch invariance is enabled, vLLM: 1. Uses deterministic kernel implementations for attention and other operations 2. Ensures consistent numerical behavior across different batch sizes 3. Disables certain optimizations that may introduce non-determinism (such as custom all-reduce operations in tensor parallel mode) +4. On CUDA devices with tuned matmul table entries for the model's bf16 unquantized forward linear layers (Ada, Hopper, Blackwell), + runs without `torch.compile` using breakable CUDA graphs so tile configs follow the runtime batch size; set `VLLM_USE_BREAKABLE_CUDAGRAPH=0` to opt out + (not applied when sequence parallelism / async TP are enabled, since those are `torch.compile` passes). !!! note Enabling batch invariance may impact performance compared to the default non-deterministic mode. This trade-off is intentional to guarantee reproducibility. diff --git a/docs/features/cross_encoder_cache.md b/docs/features/cross_encoder_cache.md new file mode 100644 index 000000000000..02fdb00f180b --- /dev/null +++ b/docs/features/cross_encoder_cache.md @@ -0,0 +1,109 @@ +# Cross-Encoder Output Reuse + +Cross-Encoder output reuse lets an Encoder load image embeddings computed by +another Encoder from a shared Mooncake Store. Local cache misses check the Store +before encoding; newly computed outputs are published asynchronously. Both paths +use the existing `ECMooncakeConnector` P2P delivery to Prefill. + +## Requirements + +- A working [disaggregated Encoder](disagg_encoder.md) deployment using + `ECMooncakeConnector`. See the [Mooncake integration example](../../tests/v1/ec_connector/integration/run_epd_mooncake_ec_full_pipeline.sh) + for the base E/PD configuration. +- Model Runner V2 (`VLLM_USE_V2_MODEL_RUNNER=1`), Encoder TP=1, and no dynamic LoRA. +- `--mm-processor-cache-gb` greater than zero to preserve content identifiers. +- Mooncake 0.3.12 or later with a running RAM Store. Disk offloading + (`enable_offload: true`) is not supported. + +Shared reuse supports images with contiguous 2D FP16, BF16 or FP32 encoder outputs. +Other modalities use normal encoding; non-contiguous outputs are not published. + +## Usage + +On each Encoder, add `cross_encoder_cache: true` to the existing producer +`--ec-transfer-config`, preserving its P2P settings: + +```json +{ + "ec_connector": "ECMooncakeConnector", + "ec_role": "ec_producer", + "ec_connector_extra_config": { + "cross_encoder_cache": true + } +} +``` + +Prefill keeps its existing `ECMooncakeConnector` consumer configuration and does +not need a Store client. + +Create a JSON file pointing to an existing Store. For an independently managed +RAM pool, use `standalone-store` with `global_segment_size` set to zero: + +```json +{ + "metadata_server": "http://STORE_HOST:2379/metadata", + "master_server_address": "STORE_HOST:50051", + "protocol": "tcp", + "device_name": "", + "mode": "standalone-store", + "global_segment_size": 0, + "local_buffer_size": "4GB" +} +``` + +Replace the addresses with your Store endpoints and set the path on each Encoder +before starting vLLM: + +```bash +export MOONCAKE_CONFIG_PATH=/path/to/mooncake_config.json +``` + +This connects to an existing Store; it does not start the storage services. +For Store setup and tenant configuration, see the +[Mooncake Store guide](mooncake_store_connector_usage.md#prerequisites). + +## Configuration + +These options belong in the producer's `ec_connector_extra_config`: + +| Option | Default | Description | +| --- | --- | --- | +| `cross_encoder_cache` | `false` | Enable shared output reuse. | +| `embedding_cache_prefix` | `""` | Namespace prefix for shared embeddings. | +| `embedding_model_identity` | Configured model path | Override the model field in Store keys. Matching multimodal identifiers are still required. | +| `store_max_pending_items` | `32` | Maximum pending publications per Encoder. Must be positive. | +| `store_max_pending_bytes` | `2147483648` (2 GiB) | Maximum retained tensor storage for pending publications per Encoder. Must be positive. | +| `store_read_buffer_bytes` | `134217728` (128 MiB) | Maximum reusable CPU staging buffer per Encoder; pinned for CUDA outputs. Must be positive. | + +Encoders sharing outputs must use the same immutable weights, compatible +preprocessing and matching multimodal identifiers. Use the same configured model +path for automatic identifiers: `embedding_model_identity` alone does not enable +reuse across different paths. Change `embedding_cache_prefix` when replacing +weights in place or changing output-affecting settings. Caller-provided UUIDs +must consistently identify the same input; see [cached inputs](multimodal_inputs.md#cached-inputs). + +## Limitations + +- Store reads are synchronous. Reuse skips Encoder computation, but not + preprocessing, P2P delivery or the scheduler's Encoder budget reservation. +- Hits are read in batches through one lazily allocated, registered CPU staging + buffer. Each chunk is copied to independent output tensors before the buffer + is reused. Reads are split by `store_read_buffer_bytes`; an individual object + larger than this capacity (including its 24-byte header) falls back to encoding. + Staging memory is additional to the Store's `local_buffer_size` and is released + on healthy shutdown. The first load includes allocation and registration costs. +- Publication is best-effort. A request can finish before its outputs reach the + Store, so concurrent cold requests may still encode the same image. +- Publication budgets count each retained backing storage once across pending + views. Its charge is released after the last view is safely reclaimed. + Exceeding a budget skips the write. Store client buffers and the P2P pool + consume additional memory. +- Cache misses and recoverable read errors fall back to encoding. Recoverable + publication errors skip the write. Unexpected native errors or unconfirmed + I/O completion or buffer release can fail the worker. +- Incompatible objects are rejected without replacement; they can trigger + repeated fallback until evicted. +- An independent Store can retain embeddings across Encoder restarts. Capacity, + eviction and resilience to Store failures depend on the Store deployment. +- The `protocol:v3` namespace isolates the compact embedding format from older + cache objects. Encoders using different format versions do not share hits. diff --git a/docs/features/custom_logitsprocs.md b/docs/features/custom_logitsprocs.md index 232f4363efee..8c1c25939928 100644 --- a/docs/features/custom_logitsprocs.md +++ b/docs/features/custom_logitsprocs.md @@ -8,84 +8,40 @@ A "custom" logits processor is written by a user of vLLM and is loaded into vLLM This document shows how to write, load and use a custom logits processor. +!!! note + This document covers the Model Runner V2 (MRV2) interface. The legacy V1 + model runner (`VLLM_USE_V2_MODEL_RUNNER=0`) uses a different interface, + `vllm.v1.sample.logits_processor.LogitsProcessor`, which is not covered + here. + ## Logits Processors Background A logits processor adjusts the next-token probability distribution, usually with the intention of steering the model towards a desired type of behavior. -In vLLM, logits processors operate at batch granularity. During a given engine step, the logits processor consumes a `(num_requests) x (vocab_size)` tensor of raw logits output by the model. For all requests which enable the logits processor, the logits processor applies a transformation to the corresponding row of the logits tensor, while leaving other rows unmodified. The transformed logits tensor is then passed to softmax. +In vLLM, logits processors operate at batch granularity. During a given engine step, the logits processor consumes a `(num_logits_rows) x (vocab_size)` tensor of raw logits output by the model. Note that a logits row is not a request: rows are reordered every step, and under speculative decoding a request owns one row per draft token. The logits processor applies a transformation to the rows of the logits tensor, while leaving other rows unmodified. The transformed logits tensor is then used for sampling. ## Creating a Custom Logits Processor -Custom logits processors must subclass `vllm.v1.sample.logits_processor.LogitsProcessor` and define (at minimum) the following methods: +Custom logits processors must subclass `vllm.v1.worker.gpu.sample.logits_processor.LogitsProcessor` and define (at minimum) the following methods: -* `validate_params(cls, sampling_params: SamplingParams)`: - * Raise `ValueError` if `SamplingParams` has invalid arguments (especially custom arguments) used by logits processor. - * When request is sent to entrypoint, `validate_params()` will validate `SamplingParams` and refuse request with invalid arguments. - * **Note:** it's important to implement `validate_params()` to prevent invalid parameters for custom logits processor. Otherwise requests with invalid parameters can cause unexpected behaviour in custom logits processor. - -* `__init__(self, vllm_config: VllmConfig, device: torch.device, is_pin_memory: bool)` +* `__init__(self, vllm_config: VllmConfig, req_states: LogitsProcRequestState)`: * `vllm_config`: engine configuration data structure - * `device`: hardware accelerator device info - * `is_pin_memory`: flag indicating whether pin memory is available to support logits processor implementation - -* `apply(self, logits: torch.Tensor) -> torch.Tensor`: - * Consume a `(num_requests) x (vocab_size)` logits tensor (`logits`) - * Apply logits processor transformation at batch granularity - * Return a transformed `(num_requests) x (vocab_size)` logits tensor - * You can modify the input logits processors in-place or out-of-place; in-place is more memory-efficient - -* `is_argmax_invariant(self) -> bool`: - * Return `True` if the logits processor is argmax invariant (never changes what is the highest-logit-value token ID for a given request), `False` if the logits processor may modify argmax - * `is_argmax_invariant()` is evaluated once at startup; if `True`, vLLM will skip applying this logits processor in a given step when all requests use greedy sampling - -* `update_state(self, batch_update: Optional["BatchUpdate"]) -> None`: - * Consume a `BatchUpdate` data structure representing persistent batch state changes at the beginning of the current engine step - * Use the `BatchUpdate` members to update logits processor internal state - * **Note:** batch update data structure may be `None`, signaling no change to the batch constituents. In this case, the LogitsProcessor might still want to update its state based on the updated `output_token_ids` lists that it could have retained when they were added. - -### How the vLLM engine builds the `BatchUpdate` data structure - -!!! important - Some logits processors design changes are still in progress. We expect - that in the future you will not need to account for batch state changes - when implementing a logits processor, and the information in this section - will become irrelevant. - -Logits processor `update_state()` implementations should assume the following model for how the model runner updates persistent batch state (expressed here in terms of the `BatchUpdate` abstraction): - -1. Identify indices of requests which finished in the current engine step - -2. Identify new requests introduced in the current step - -3. Use Add operations to replace as many finished requests with new requests, in order of increasing index of the replaced request starting with the lowest index - -4. Based on the relative number of new and finished requests: - - 1. If the numbers of new and finished requests are the same, proceed to next step + * `req_states`: a narrow, read-only view of the persistent batch, exposing the on-device token history (`all_token_ids`, `prompt_len`, `prefill_len`, `total_len`) plus `device`, `max_num_reqs` and `vocab_size` - 2. *If there are more new requests than finished requests:* apply Add operations to extend the batch with the remaining new requests which did not replace finished requests. Assign consecutive indices to these new requests, starting with `current_max_batch_index + 1` +* `add_request(self, req_idx, sampling_params) -> bool`: + * Initialize per-slot state for a request entering the batch. Slots are recycled through a free list, so per-slot state must be fully (re)initialized here; there is no removal hook, since freed slots are never read + * Return whether this processor modifies logits for the request. The sampler skips the logits-processing pipeline for batches in which no request needs it - 3. *If there are fewer new requests than finished requests:* +* `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 - * Apply Remove operations to finished requests which were not replaced with new requests. These removed request indices will necessarily be greater than the greatest index of the finished requests which were replaced in the previous step. The Removes may leave the batch in a non-contiguous state +* `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 - * **"Condense" the batch to be contiguous:** starting with the lowest-index empty slot (which was caused by a Remove), apply a Unidirectional Move from the current highest non-empty slot in the batch to fill the empty slot. Proceed with additional Unidirectional Move operations in order of increasing empty slot destination index and decreasing non-empty slot source index until the batch is contiguous - - * **Shrink the batch:** a side effect of condensing the batch is that empty slots resulting from Remove operations are grouped in a contiguous block at the end of the batch array. Thus, after condensing, update `BatchUpdate.batch_size` to reflect the number of non-empty slots - -5. Reorder the batch for improved efficiency. Depending on the attention backend implementation and the current characteristics of the batch, zero or more Swap Move operations may be applied to reorder the batch - -Notes: - -* A logits processor `update_state()` method must process batch update operations in the following order: removes, adds, moves - -* The index argument for Add operations refers to the index *at the time the Add occurred*, i.e. before any Move operations - * Example: if a request is Added at index 5 and then swapped with index 3, the Add operation in `BatchUpdate.added` will be associated with index 5 not 3 - * In other words Move operations can be assumed to be applied after Adds and Removes - -* Move operations can be assumed to be applied in the order in which they appear in `BatchUpdate.moved` - -* If there are no new/finished requests and there is no batch reordering, then the batch update for the logits processors will be `None` +* `apply(self, logits: torch.Tensor, ctx: LogitsContext) -> torch.Tensor`: + * Consume a `(num_logits_rows) x (vocab_size)` logits tensor and the step's batch layout (`ctx`: row-to-slot mappings, `input_ids`, positions) + * Apply logits processor transformation at batch granularity + * Return a transformed logits tensor. You can modify the input logits in-place or out-of-place; in-place is more memory-efficient ### Passing Custom Argument to a Custom Logits Processor @@ -93,7 +49,7 @@ Unlike built-in logits processors, custom logits processors may require configur ### Example Custom Logits Processor Implementation -The contrived example below implements a custom logits processor which consumes a `(num\_requests) \times (vocab\_size)` logits tensor and masks out all tokens except for one (`target_token`) with `float(-inf)`. The logits processor is disabled for any request that does not specify `target_token`. To determine whether the logits processor is enabled and which token to leave unmasked, the logits processor checks `SamplingParams.extra_args` for a `target_token` custom argument associated with each request: +The contrived example below implements a custom logits processor which masks out all tokens except for one (`target_token`) for the requests that enable it. The processor is disabled for any request that does not specify `target_token`; `add_request()` reports this via its return value, so the sampler can skip the logits-processing pipeline entirely when no request enables the processor. To determine whether the logits processor is enabled and which token to leave unmasked, the processor checks `SamplingParams.extra_args` for a `target_token` custom argument associated with each request: ??? code "Example custom logits processor definition" @@ -101,193 +57,55 @@ The contrived example below implements a custom logits processor which consumes import torch from vllm.config import VllmConfig from vllm.sampling_params import SamplingParams - from vllm.v1.sample.logits_processor import (BatchUpdate, - LogitsProcessor, - MoveDirectionality) + from vllm.v1.worker.gpu.sample.logits_processor import ( + LogitsProcRequestState, + LogitsContext, + LogitsProcessor, + ) - class DummyLogitsProcessor(LogitsProcessor): - """Fake logit processor to support unit testing and examples""" - @classmethod - def validate_params(cls, params: SamplingParams): - target_token: int | None = params.extra_args and params.extra_args.get( - "target_token" - ) - if target_token is not None and not isinstance(target_token, int): - raise ValueError(f"target_token value {target_token} is not int") - - def __init__(self, vllm_config: "VllmConfig", device: torch.device, - is_pin_memory: bool): - self.req_info: dict[int, int] = {} - - def is_argmax_invariant(self) -> bool: - """Never impacts greedy sampling""" - return False - - def update_state(self, batch_update: BatchUpdate | None): - if not batch_update: - return - - # Process added requests. - for index, params, _, _ in batch_update.added: - assert params is not None - self.validate_params(params) - if params.extra_args and (target_token := - params.extra_args.get("target_token")): - self.req_info[index] = target_token - else: - self.req_info.pop(index, None) - - if self.req_info: - # Process removed requests. - for index in batch_update.removed: - self.req_info.pop(index, None) - - # Process moved requests, unidirectional move (a->b) and swap - # (a<->b) - for adx, bdx, direct in batch_update.moved: - a_val = self.req_info.pop(adx, None) - b_val = self.req_info.pop(bdx, None) - if a_val is not None: - self.req_info[bdx] = a_val - if direct == MoveDirectionality.SWAP and b_val is not None: - self.req_info[adx] = b_val - - def apply(self, logits: torch.Tensor) -> torch.Tensor: - if not self.req_info: - return logits + class TargetTokenLogitsProcessor(LogitsProcessor): + """Masks out all tokens except `target_token` (a per-request custom + argument); requests without it are left alone.""" - # Save target values before modification - cols = torch.tensor( - list(self.req_info.values()), dtype=torch.long, device=logits.device + 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(). + self.target_token = torch.full( + (req_states.max_num_reqs,), -1, dtype=torch.int64 ) - rows = torch.tensor( - list(self.req_info.keys()), dtype=torch.long, device=logits.device + self.target_token_dev = torch.full( + (req_states.max_num_reqs,), -1, dtype=torch.int64, device=req_states.device ) - values_to_keep = logits[rows, cols].clone() - - # Mask all but target tokens - logits[rows] = float('-inf') - logits[rows, cols] = values_to_keep - - return logits - - ``` -In the rest of this document, we will use `DummyLogitsProcessor` as an example of a custom logits processor. + def add_request(self, req_idx: int, sampling_params: SamplingParams) -> bool: + target = (sampling_params.extra_args or {}).get("target_token") + self.target_token[req_idx] = target if target is not None else -1 + return target is not None -The `DummyLogitsProcessor.update_state()` implementation maintains a "sparse" representation of the batched requests in the `self.req_info` dictionary: only those requests which specify a `target_token` value have a key in the dictionary. `update_state()` adjusts the stored request indices and `target_token` values (keys and values respectively in `self.req_info`) in response to Add, Remove and Move operations against the persistent batch. + def apply_staged_writes(self) -> None: + self.target_token_dev.copy_(self.target_token, non_blocking=True) -### Wrapping an Existing Request-Level Logits Processor - -Although the vLLM engine applies logits processors at batch granularity, some users may want to use vLLM with a "request-level" logits processor implementation - an implementation which operates on individual requests. This will be especially true if your logits processor was developed for vLLM version 0, which required it to be a `Callable` (as described [here][vllm.logits_process]) conforming to the following type annotation: - -``` python -RequestLogitsProcessor = Union[ - - # (output token ids, logits tensor) -> logits tensor - Callable[[list[int], Tensor], Tensor], - - # (prompt token ids, output token ids, logits tensor) -> logits tensor - Callable[[list[int], list[int], Tensor], Tensor], -] -``` - -While request-level logits processors are explicitly *not* supported in the vLLM engine, vLLM *does* provide a convenient process to wrap an existing `Callable` request-level logits processor and create a batch-level logits processor that is compatible with vLLM. The `Callable` must conform to the type annotation above; if your request-level logits processor has a different interface, then in order to wrap it, you may need to modify it or implement an additional wrapper layer to comply with the interface specification above. - -You can wrap the request-level logits processor by subclassing `AdapterLogitsProcessor` as shown in the example below (in this example, `DummyPerReqLogitsProcessor` is a stand-in for your request-level logits processor which needs to be wrapped.): - -* Override `AdapterLogitsProcessor.validate_params(cls,params)` to validate request's sampling parameters. - -* Override `AdapterLogitsProcessor.is_argmax_invariant(self)` to accurately reflect whether your request-level logits processor may impact which token has the highest-value logit. - -* Override `AdapterLogitsProcessor.new_req_logits_processor(self,params)` to create a new request-level logits processor instance from a `SamplingParams` instance: - -??? code "Example of Wrapping a Request-Level Logits Processor" - - ``` python - ... - - from vllm.v1.sample.logits_processor import ( - AdapterLogitsProcessor, # Wrapper base-class - RequestLogitsProcessor, # Request-level logitsproc type annotation - ) - - ... - - # Stand-in for your request-level logits processor: - class DummyPerReqLogitsProcessor: - """The request-level logits processor masks out all logits except the - token id identified by `target_token`""" - - def __init__(self, target_token: int) -> None: - """Specify `target_token`""" - self.target_token = target_token - - def __call__( - self, - output_ids: list[int], - logits: torch.Tensor, - ) -> torch.Tensor: - val_to_keep = logits[self.target_token].item() - logits[:] = float("-inf") - logits[self.target_token] = val_to_keep + def apply(self, logits: torch.Tensor, ctx: LogitsContext) -> torch.Tensor: + # ctx.expanded_idx_mapping maps each logits row to its request slot. + cols = self.target_token_dev[ctx.expanded_idx_mapping.long()] + rows = torch.nonzero(cols >= 0).squeeze(1) + if rows.numel() == 0: + return logits + kept = logits[rows, cols[rows]].clone() + logits[rows] = float("-inf") + logits[rows, cols[rows]] = kept return logits - - ... - - # Example of wrapping the request-level logits processor: - class WrappedPerReqLogitsProcessor(AdapterLogitsProcessor): - """Example of wrapping a fake request-level logit processor to create a - batch-level logits processor""" - - @classmethod - def validate_params(cls, params: SamplingParams): - target_token: Any | None = params.extra_args and params.extra_args.get( - "target_token" - ) - if target_token is not None and not isinstance(target_token, int): - raise ValueError( - f"target_token value {target_token} is not int" - ) - - def is_argmax_invariant(self) -> bool: - return False - - def new_req_logits_processor( - self, - params: SamplingParams, - ) -> Optional[RequestLogitsProcessor]: - """This method returns a new request-level logits processor, customized - to the `target_token` value associated with a particular request. - - Returns None if the logits processor should not be applied to the - particular request. To use the logits processor the request must have - a "target_token" custom argument with an integer value. - - Args: - params: per-request sampling params - - Returns: - `Callable` request logits processor, or None - """ - target_token: Any | None = params.extra_args and params.extra_args.get( - "target_token" - ) - if target_token is None: - return None - return DummyPerReqLogitsProcessor(target_token) ``` -!!! note - Your `new_req_logits_processor()` override can return `None` to signal that the wrapped logits processor should not be applied to the request in question. - -Once you have created a custom subclass (like `WrappedPerReqLogitsProcessor`) which wraps your request level logits processor, you can pass the custom subclass to vLLM via any of the methods described in the following section. +Per-request state is keyed by the request slot index, and slots are recycled through a free list. The example keeps a per-slot state where `-1` means disabled; note that `add_request()` overwrites the slot's entry unconditionally, which is what keeps recycled slots from leaking state between requests. ## Ways to Load Your Custom Logits Processor in vLLM Logits processors are loaded at initialization. Critically, the set of loaded logits processors cannot be modified after the vLLM engine finishes loading, and new logits processors cannot be loaded on-demand for individual requests. +Loaded classes are validated against the Model Runner V2 interface; a class that does not subclass `vllm.v1.worker.gpu.sample.logits_processor.LogitsProcessor` is rejected with a clear error. + This section details different ways of making your logits processor visible to vLLM and triggering vLLM to load your logits processor. ### Method 1: Pass the Custom Logits Processor Fully-Qualified Class Name (FQCN) to vLLM at Initialization Time @@ -314,7 +132,7 @@ See examples below: # Pass in FQCN llm = LLM( model="facebook/opt-125m", - logits_processors=["your.module.path:DummyLogitsProcessor"], + logits_processors=["your.module.path:TargetTokenLogitsProcessor"], ) ``` @@ -323,14 +141,14 @@ See examples below: ``` python # Pass in FQCN engine_args = AsyncEngineArgs(model="facebook/opt-125m", - logits_processors=["your.module.path:DummyLogitsProcessor"]) + logits_processors=["your.module.path:TargetTokenLogitsProcessor"]) async_llm = AsyncLLM.from_engine_args(engine_args) ``` ??? code "Passing custom logits processor FQCN to vLLM server via CLI" ```bash - vllm serve facebook/opt-125m --logits_processors your.module.path:DummyLogitsProcessor + vllm serve facebook/opt-125m --logits_processors your.module.path:TargetTokenLogitsProcessor ``` ### Method 2: Automatically Detect Custom Logits Processors Installed in Your Python Environment As Entry Points @@ -345,7 +163,7 @@ Suppose that you have developed a Python package that holds your custom logits p ``` toml [project.entry-points."vllm.logits_processors"] - dummy_logits_processor = "your.module.path:DummyLogitsProcessor" + target_token_logits_processor = "your.module.path:TargetTokenLogitsProcessor" ``` Once your package is installed, your custom logits processor will be loaded automatically whenever vLLM is initialized. You do *not* need to pass the custom logits processor to the `LLM` or `AsyncLLM` constructors or to the vLLM server explicitly at initialization time if your logits processor is exposed as an entry point. @@ -361,26 +179,26 @@ You can pass one or more custom logits processor class objects to the `LLM` and ``` python # Import custom logits processor - from some.module import DummyLogitsProcessor + from some.module import TargetTokenLogitsProcessor # ...or... # Define custom logits processor locally - from vllm.v1.sample.logits_processor import LogitsProcessor + from vllm.v1.worker.gpu.sample.logits_processor import LogitsProcessor - class DummyLogitsProcessor(LogitsProcessor): - # See DummyLogitsProcessor implementation above + class TargetTokenLogitsProcessor(LogitsProcessor): + # See TargetTokenLogitsProcessor implementation above ... # Pass class object to LLM constructor llm = LLM( model="facebook/opt-125m", - logits_processors=[DummyLogitsProcessor], + logits_processors=[TargetTokenLogitsProcessor], ) # Pass class object to AsyncLLM constructor engine_args = AsyncEngineArgs(model="facebook/opt-125m", - logits_processors=[DummyLogitsProcessor]) + logits_processors=[TargetTokenLogitsProcessor]) async_llm = AsyncLLM.from_engine_args(engine_args) ``` @@ -388,7 +206,7 @@ You can pass one or more custom logits processor class objects to the `LLM` and The design of the custom logits processor determines whether the logits processor must be enabled/disabled for a given request, and what arguments must be provided to configure the logits processor. -The examples below show how a user would pass a custom argument (`target_token`) to `DummyLogitsProcessor` in order to (1) enable the logits processor for that particular request and (2) control the logits processor's behavior. +The examples below show how a user would pass a custom argument (`target_token`) to `TargetTokenLogitsProcessor` in order to (1) enable the logits processor for that particular request and (2) control the logits processor's behavior. ??? code "vLLM REST API: configure custom logits processor for a request" @@ -419,7 +237,7 @@ The examples below show how a user would pass a custom argument (`target_token`) ??? code "Offline: configure custom logits processor for an `LLM` request" ``` python - outputs_logitproc = llm.generate("your prompt", + outputs_logitproc = llm.generate("your prompt", SamplingParams(..., extra_args={"target_token": 67})) ``` @@ -438,31 +256,14 @@ The examples below show how a user would pass a custom argument (`target_token`) ## Best Practices for Writing Custom Logits Processors -Once vLLM loads a logits processor during initialization, then vLLM will invoke `update_state()` and `apply()` against that logits processor in every engine step. Both methods operate on all requests which currently reside in the vLLM persistent batch. Thus, it is important to implement these methods efficiently. - -* Write efficient `apply()` and `update_state()` implementations in light of the fact that logits processors operate at batch granularity - * For example, you may be able to use efficient vectorized operations to implement `apply()` or update internal state vectors in `update_state()` - * However, if you think that a logits processor may be used infrequently, it may be appropriate to use a "sparse" representation of request state i.e. the class can represent request configuration using a dictionary which only stores metadata about requests that enable the logits processor - * **Note:** wrapped request-level logits processors do not need to implement `apply()` and `update_state()`; the default `AdapterLogitsProcessor.update_state()` implementation maintains a sparse representation of request state, wherein requests for which `new_req_logits_processor()` returns `None` are not represented in the base-class state dictionary. The default implementation of `AdapterLogitsProcessor.apply()` applies the request-level logits processor to each row of input logits sequentially and assembles the output logits tensor. If the performance of this `AdapterLogitsProcessor` default implementation is insufficient, then avoid wrapping your request-level logits processor and instead re-implement it as a `LogitsProcessor` subclass with optimized `apply()` and `update_state()` implementations that operate at batch granularity +* Write an efficient `apply()` implementation in light of the fact that logits processors operate at batch granularity. For example, you may be able to use efficient vectorized operations to implement `apply()`, and to stage per-request state into contiguous tensors in `add_request()` instead of per-row Python loops * It is up to the logits processor author to determine: - 1. **The per-request attributes which configure the logits processor's behavior against that request.** Your custom logits processor's `update_state()` override determines how `SamplingParams` fields are mapped into logits processor state - - * **Note:** for wrapped request-level logits processors, `new_req_logits_processor()` determines how `SamplingParams` fields are used to initialize a request-level logits processor instance. - - 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`. Try to save compute and memory for requests which disable the logits processor - - * **Note:** for wrapped per-request logits processors, the default `AdapterLogitsProcessor.update_state()` implementation ensures that the request-level logits processor is disabled when `new_req_logits_processor()` returns `None` for that request - - 3. **The conditions under which the logits processor is short-circuited at the batch level.** Even if you have defined a way to disable the custom logits processor at the request level, it may be difficult to translate this into compute savings i.e. if your `update_state()` and `apply()` implementations use efficient vectorized implementations that operate on the whole persistent batch in a single command. For example, you cannot skip an entire vectorized operation in `apply()` just because one request disabled the logits processor. To save compute in the edge-case where no running requests utilize the custom logits processor, we recommend designing `apply()` to return the unmodified input tensor if all requests have the logits processor disabled. Similarly, consider whether steps can be skipped in `update_state()` if no requests enable the logits processor - - * Additionally, an easy way to save compute in `update_state()` is to exit early when the `batch_update` is `None` - - * **Note:** for wrapped per-request logits processors, the `AdapterLogitsProcessor` base-class implements the above optimizations by default + 1. **The per-request attributes which configure the logits processor's behavior against that request.** Your custom logits processor's `add_request()` override determines how `SamplingParams` fields are mapped into logits processor state -* Ensure that the logits processor `update_state` method discards information about finished requests (i.e. requests which are replaced by an Add or which are subject to a Remove) + 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 - * **Note:** for wrapped per-request logits processors, the `AdapterLogitsProcessor` base-class handles this by default +* 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 -* `is_argmax_invariant()` can be hard-coded to `True` or `False` if the logits processor has consistent behavior. However, the argmax invariance may also be determined programmatically (i.e. if your logits processor is user-customizable in some way that impacts whether the logits processor is argmax invariant). For this reason, `is_argmax_invariant()` is not a class method +* 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/disagg_encoder.md b/docs/features/disagg_encoder.md index c27d6b277284..ac304345fd1e 100644 --- a/docs/features/disagg_encoder.md +++ b/docs/features/disagg_encoder.md @@ -32,8 +32,9 @@ Design doc: