Compare commits

...
Author SHA1 Message Date
yewentao256 dd54d6bc9d Merge branch 'main' into wentao-fix-v2-mrope
Signed-off-by: yewentao256 <zhyanwentao@126.com>
2026-06-22 22:26:56 +00:00
e2fe837572 [CI] Fix CPU-Multi-Modal Model Tests timeout by adding a 4th shard (#46388)
Signed-off-by: Tyler Michael Smith <tlrmchlsmth@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-22 22:08:00 +00:00
Aarushi JainandGitHub fbf9ff7cf4 [CI][ROCm] Restrict MLA cross-layer KV cache test to supported backends on ROCm (#46401)
Signed-off-by: aarushjain29 <Aarushi.Jain2@amd.com>
2026-06-22 17:05:26 -05:00
6cc2c9ba3a [CI] Add DGX Spark GPQA smoke test (#39541)
Signed-off-by: mgoin <mgoin64@gmail.com>
Signed-off-by: Michael Goin <mgoin64@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 14:52:38 -07:00
c0b2d8f471 [Bugfix] FusedMoE: coerce shape-(1,) per-tensor scales to 0-D scalar … (#43362)
Signed-off-by: Varshith <kvarshithgowda@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Michael Goin <mgoin64@gmail.com>
2026-06-22 13:26:53 -07:00
Mohammad Miadh AngkadandGitHub d1a38c2762 [Kernel][Performance] Add FlashInfer cutedsl NVFP4 GEMM backend (#42235)
Signed-off-by: Mohammad Miadh Angkad <176301910+mmangkad@users.noreply.github.com>
2026-06-22 16:17:18 -04:00
2b4a7491ec [ROCm][CI] Query total device memory via amdsmi to avoid HIP init (#46141)
Signed-off-by: stefankoncarevic <stefan.koncarevic@amd.com>
Co-authored-by: Andreas Karatzas <akaratza@amd.com>
2026-06-22 15:12:24 -05:00
SaddssandGitHub 82ede09a5a [Bugfix][KVConnector] Fix SimpleCPUOffloadConnector GPU->CPU store race (#46278) 2026-06-22 13:08:47 -07:00
Nick HillandGitHub fbf520cf3a [MRV2] Generalize use of WhisperModelState (#46096)
Signed-off-by: Nick Hill <nickhill123@gmail.com>
2026-06-22 12:40:02 -07:00
44d95069e9 Enable DeepSeek V4 and GLM-5.1 on SM120 (#43477)
Signed-off-by: Zihua Wu <13583761+lucifer1004@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Yongye Zhu <zyy1102000@gmail.com>
2026-06-22 11:54:14 -07:00
3ce15fd574 [v1][kvconnector] DecodeBenchConnector: fill list/tuple (Mamba/KDA) KV caches (#45080)
Signed-off-by: Dao Le <Dao007forever@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 11:54:00 -07:00
Jinzhen LinGitHubmergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
e4b3da3feb [Quantization][CI] add humming lm-eval test (#43752)
Signed-off-by: Jinzhen Lin <jinzhen.ljz@antgroup.com>
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
2026-06-22 11:23:55 -07:00
3e6529cc0e [Bugfix][Spec Decode] Fix EAGLE drafter multimodal encoder cache misses (#46315)
Signed-off-by: Nick Hill <nickhill123@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Roger Wang <hey@rogerw.io>
2026-06-22 18:14:02 +00:00
ac614587f5 [EPLB] Enable nixl eplb communicator for elastic ep (#45013)
Signed-off-by: Markov Ilya <markovilya197@gmail.com>
Signed-off-by: Markov Ilya <markovilya19@gmail.com>
Co-authored-by: Markov Ilya <markovilya19@gmail.com>
2026-06-22 10:54:08 -07:00
Wentao YeandGitHub e1c35296c3 Merge branch 'main' into wentao-fix-v2-mrope 2026-06-22 11:39:51 -04:00
yewentao256 faea2977ee fix v2 mrope
Signed-off-by: yewentao256 <zhyanwentao@126.com>
2026-06-18 18:49:07 +00:00
83 changed files with 3735 additions and 694 deletions
+1 -1
View File
@@ -136,7 +136,7 @@ steps:
- |
bash .buildkite/scripts/hardware_ci/run-cpu-test.sh 45m "
pytest -x -v -s tests/models/multimodal/generation --ignore=tests/models/multimodal/generation/test_pixtral.py -m cpu_model --num-shards=$$BUILDKITE_PARALLEL_JOB_COUNT --shard-id=$$BUILDKITE_PARALLEL_JOB"
parallelism: 3
parallelism: 4
- label: "Arm CPU Test"
depends_on: []
+3 -2
View File
@@ -21,12 +21,13 @@ else
exit 0
fi
# build (Grace/GH200 is the arm64 GPU target; sm_90)
# build for arm64 GPU targets: Grace/GH200 (sm_90) and DGX Spark/GB10
# (sm_121, family-covered by 12.0 under CUDA 13)
docker build --file docker/Dockerfile \
--platform linux/arm64 \
--build-arg max_jobs=16 \
--build-arg nvcc_threads=4 \
--build-arg torch_cuda_arch_list="9.0" \
--build-arg torch_cuda_arch_list="9.0 12.0" \
--build-arg USE_SCCACHE=1 \
--build-arg buildkite_commit="$BUILDKITE_COMMIT" \
--tag "$REGISTRY"/"$REPO":"$BUILDKITE_COMMIT"-arm64 \
@@ -1,5 +1,5 @@
group: Expert Parallelism
depends_on:
depends_on:
- image-build-xpu
steps:
- label: EPLB Algorithm
@@ -1,5 +1,5 @@
group: Models - Multimodal
depends_on:
depends_on:
- image-build-xpu
steps:
- label: "Multi-Modal Models (Standard) 1: qwen2"
+57
View File
@@ -136,6 +136,49 @@ 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: LM Eval Humming (A100 - TEMPORARY)
key: lm-eval-humming-a100
timeout_in_minutes: 30
device: a100
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/fused_moe/experts/fused_humming_moe.py
- vllm/model_executor/layers/fused_moe/oracle/mxfp4.py
commands:
- pytest -s -v evals/gsm8k/test_gsm8k_correctness.py --config-list-file=evals/gsm8k/configs/humming/config.txt
- label: LM Eval Humming (H100 - TEMPORARY)
key: lm-eval-humming-h100
timeout_in_minutes: 30
device: h100
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/fused_moe/experts/fused_humming_moe.py
- vllm/model_executor/layers/fused_moe/oracle/mxfp4.py
commands:
- pytest -s -v evals/gsm8k/test_gsm8k_correctness.py --config-list-file=evals/gsm8k/configs/humming/config.txt
- pytest -s -v evals/gsm8k/test_gsm8k_correctness.py --config-list-file=evals/gsm8k/configs/humming/config-act-fp8.txt
- label: LM Eval Humming (B200 - TEMPORARY)
key: lm-eval-humming-b200
timeout_in_minutes: 30
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/fused_moe/experts/fused_humming_moe.py
- vllm/model_executor/layers/fused_moe/oracle/mxfp4.py
commands:
- pytest -s -v evals/gsm8k/test_gsm8k_correctness.py --config-list-file=evals/gsm8k/configs/humming/config.txt
- pytest -s -v evals/gsm8k/test_gsm8k_correctness.py --config-list-file=evals/gsm8k/configs/humming/config-act-fp8.txt
- label: LM Eval TurboQuant KV Cache
key: lm-eval-turboquant-kv-cache
@@ -177,6 +220,20 @@ 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-b200.txt
- label: GPQA Eval (GPT-OSS) (DGX Spark)
key: gpqa-eval-gpt-oss-spark
timeout_in_minutes: 120
device: dgx-spark
optional: true
num_devices: 1
source_file_dependencies:
- csrc/
- vllm/model_executor/layers/quantization
- tests/evals/gpt_oss/
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-spark.txt
- label: MRCR Eval Small Models
device: h200_35gb
timeout_in_minutes: 30
+59 -29
View File
@@ -8,43 +8,73 @@ if (DEFINED ENV{DEEPGEMM_SRC_DIR})
set(DEEPGEMM_SRC_DIR $ENV{DEEPGEMM_SRC_DIR})
endif()
# Local tree: set deepgemm_SOURCE_DIR directly (no FetchContent download).
# Upstream git: use FetchContent_Populate with explicit options (CMP0169 NEW
# disallows one-argument Populate(dep) after Declare; MakeAvailable would run
# DeepGEMM's top-level CMakeLists.txt, which vLLM must not load).
if(DEEPGEMM_SRC_DIR)
FetchContent_Declare(
deepgemm
SOURCE_DIR ${DEEPGEMM_SRC_DIR}
CONFIGURE_COMMAND ""
BUILD_COMMAND ""
)
# cmake_path(ABSOLUTE_PATH <var> ...) reads the path from <var>; NORMALIZE is a
# flag (no trailing path argument). Resolve relative paths against vLLM root.
set(_deepgemm_user_src "${DEEPGEMM_SRC_DIR}")
cmake_path(ABSOLUTE_PATH _deepgemm_user_src
BASE_DIRECTORY "${CMAKE_SOURCE_DIR}"
NORMALIZE)
set(DEEPGEMM_SRC_DIR "${_deepgemm_user_src}")
if(NOT IS_DIRECTORY "${DEEPGEMM_SRC_DIR}")
message(FATAL_ERROR
"DEEPGEMM_SRC_DIR is not an existing directory: '${DEEPGEMM_SRC_DIR}'")
endif()
set(deepgemm_SOURCE_DIR "${DEEPGEMM_SRC_DIR}")
message(STATUS "DeepGEMM using local DEEPGEMM_SRC_DIR: ${deepgemm_SOURCE_DIR}")
else()
# This ref should be kept in sync with tools/install_deepgemm.sh
FetchContent_Declare(
deepgemm
GIT_REPOSITORY https://github.com/deepseek-ai/DeepGEMM.git
GIT_TAG 891d57b4db1071624b5c8fa0d1e51cb317fa709f
GIT_SUBMODULES "third-party/cutlass" "third-party/fmt"
GIT_PROGRESS TRUE
CONFIGURE_COMMAND ""
BUILD_COMMAND ""
)
# Keep in sync with tools/install_deepgemm.sh
set(_DEEPGEMM_UPSTREAM_REPO "https://github.com/deepseek-ai/DeepGEMM.git")
set(_DEEPGEMM_UPSTREAM_TAG "891d57b4db1071624b5c8fa0d1e51cb317fa709f")
set(_deepgemm_fc_root "${FETCHCONTENT_BASE_DIR}")
if(NOT _deepgemm_fc_root)
set(_deepgemm_fc_root "${CMAKE_BINARY_DIR}/_deps")
endif()
set(_deepgemm_src "${_deepgemm_fc_root}/deepgemm-src")
set(_deepgemm_bin "${_deepgemm_fc_root}/deepgemm-build")
set(_deepgemm_sub "${_deepgemm_fc_root}/deepgemm-subbuild")
if(EXISTS "${_deepgemm_src}/csrc/python_api.cpp")
set(deepgemm_SOURCE_DIR "${_deepgemm_src}")
set(deepgemm_BINARY_DIR "${_deepgemm_bin}")
else()
FetchContent_Populate(
deepgemm
SUBBUILD_DIR "${_deepgemm_sub}"
SOURCE_DIR "${_deepgemm_src}"
BINARY_DIR "${_deepgemm_bin}"
GIT_REPOSITORY "${_DEEPGEMM_UPSTREAM_REPO}"
GIT_TAG "${_DEEPGEMM_UPSTREAM_TAG}"
GIT_SUBMODULES "third-party/cutlass" "third-party/fmt"
GIT_PROGRESS TRUE
)
endif()
message(STATUS "DeepGEMM is available at ${deepgemm_SOURCE_DIR}")
endif()
# Use FetchContent_Populate (not MakeAvailable) to avoid processing
# DeepGEMM's own CMakeLists.txt which has incompatible find_package calls.
FetchContent_GetProperties(deepgemm)
if(NOT deepgemm_POPULATED)
FetchContent_Populate(deepgemm)
endif()
message(STATUS "DeepGEMM is available at ${deepgemm_SOURCE_DIR}")
# DeepGEMM requires CUDA 12.3+ for SM90, 12.9+ for SM100
# DeepGEMM requires CUDA 12.3+ for SM90, 12.9+ for SM100 (official upstream),
# and 12.8+ for SM120 / SM12x. CUDA 13+ can use the family-specific SM12x
# arch; CUDA 12.x builds the arch-specific SM120/SM121 variants.
set(DEEPGEMM_SUPPORT_ARCHS)
if(${CMAKE_CUDA_COMPILER_VERSION} VERSION_GREATER_EQUAL 12.3)
list(APPEND DEEPGEMM_SUPPORT_ARCHS "9.0a")
endif()
if(${CMAKE_CUDA_COMPILER_VERSION} VERSION_GREATER_EQUAL 12.9)
list(APPEND DEEPGEMM_SUPPORT_ARCHS "10.0f")
elseif(${CMAKE_CUDA_COMPILER_VERSION} VERSION_GREATER_EQUAL 12.8)
list(APPEND DEEPGEMM_SUPPORT_ARCHS "10.0a")
if(${CMAKE_CUDA_COMPILER_VERSION} VERSION_GREATER_EQUAL 12.8)
if(${CMAKE_CUDA_COMPILER_VERSION} VERSION_GREATER_EQUAL 12.9)
list(APPEND DEEPGEMM_SUPPORT_ARCHS "10.0f")
else()
list(APPEND DEEPGEMM_SUPPORT_ARCHS "10.0a")
endif()
if(${CMAKE_CUDA_COMPILER_VERSION} VERSION_GREATER_EQUAL 13.0)
list(APPEND DEEPGEMM_SUPPORT_ARCHS "12.0f")
else()
list(APPEND DEEPGEMM_SUPPORT_ARCHS "12.0a" "12.1a")
endif()
endif()
cuda_archs_loose_intersection(DEEPGEMM_ARCHS
+38 -16
View File
@@ -6,25 +6,47 @@ if(DEFINED ENV{QUTLASS_SRC_DIR})
set(QUTLASS_SRC_DIR $ENV{QUTLASS_SRC_DIR})
endif()
# CMP0169 NEW: one-argument FetchContent_Populate(name) after Declare is invalid.
# Use explicit Populate(...) for git, or set SOURCE_DIR for local trees.
if(QUTLASS_SRC_DIR)
FetchContent_Declare(
qutlass
SOURCE_DIR ${QUTLASS_SRC_DIR}
CONFIGURE_COMMAND ""
BUILD_COMMAND ""
)
set(_qutlass_user_src "${QUTLASS_SRC_DIR}")
cmake_path(ABSOLUTE_PATH _qutlass_user_src
BASE_DIRECTORY "${CMAKE_SOURCE_DIR}"
NORMALIZE)
set(QUTLASS_SRC_DIR "${_qutlass_user_src}")
if(NOT IS_DIRECTORY "${QUTLASS_SRC_DIR}")
message(FATAL_ERROR
"[QUTLASS] QUTLASS_SRC_DIR is not an existing directory: '${QUTLASS_SRC_DIR}'")
endif()
set(qutlass_SOURCE_DIR "${QUTLASS_SRC_DIR}")
set(qutlass_BINARY_DIR "${CMAKE_BINARY_DIR}/qutlass-binary-dir-unused")
else()
FetchContent_Declare(
qutlass
GIT_REPOSITORY https://github.com/IST-DASLab/qutlass.git
GIT_TAG 830d2c4537c7396e14a02a46fbddd18b5d107c65
GIT_PROGRESS TRUE
CONFIGURE_COMMAND ""
BUILD_COMMAND ""
)
endif()
set(_QUTLASS_UPSTREAM_REPO "https://github.com/IST-DASLab/qutlass.git")
set(_QUTLASS_UPSTREAM_TAG "830d2c4537c7396e14a02a46fbddd18b5d107c65")
FetchContent_Populate(qutlass)
set(_qutlass_fc_root "${FETCHCONTENT_BASE_DIR}")
if(NOT _qutlass_fc_root)
set(_qutlass_fc_root "${CMAKE_BINARY_DIR}/_deps")
endif()
set(_qutlass_src "${_qutlass_fc_root}/qutlass-src")
set(_qutlass_bin "${_qutlass_fc_root}/qutlass-build")
set(_qutlass_sub "${_qutlass_fc_root}/qutlass-subbuild")
if(EXISTS "${_qutlass_src}/qutlass/csrc/bindings.cpp")
set(qutlass_SOURCE_DIR "${_qutlass_src}")
set(qutlass_BINARY_DIR "${_qutlass_bin}")
else()
FetchContent_Populate(
qutlass
SUBBUILD_DIR "${_qutlass_sub}"
SOURCE_DIR "${_qutlass_src}"
BINARY_DIR "${_qutlass_bin}"
GIT_REPOSITORY "${_QUTLASS_UPSTREAM_REPO}"
GIT_TAG "${_QUTLASS_UPSTREAM_TAG}"
GIT_PROGRESS TRUE
)
endif()
endif()
if(NOT qutlass_SOURCE_DIR)
message(FATAL_ERROR "[QUTLASS] source directory could not be resolved.")
+6 -14
View File
@@ -133,16 +133,6 @@ Priority is **1 = highest** (tried first).
| 7 | `FLASHINFER_MLA_SPARSE`**\*** |
| 8 | `FLASHMLA_SPARSE` |
**Ampere/Hopper (SM 8.x-9.x):**
| Priority | Backend |
| -------- | ------- |
| 1 | `FLASH_ATTN_MLA` |
| 2 | `FLASHMLA` |
| 3 | `FLASHINFER_MLA` |
| 4 | `TRITON_MLA` |
| 5 | `FLASHMLA_SPARSE` |
> **\*** For sparse MLA, FP8 KV cache always prefers `FLASHINFER_MLA_SPARSE`. With BF16 KV cache, `FLASHINFER_MLA_SPARSE` is preferred for low query-head counts (<= 16), while `FLASHMLA_SPARSE` is preferred otherwise.
>
> **Note:** ROCm and CPU platforms have their own selection logic. See the platform-specific documentation for details.
@@ -231,7 +221,8 @@ MLA decode backends are selected using the standard
| ------- | ------ | --------- | ----------- | ---------- | ---- | ---------- | ------ | --------- | --- | --------------- | ------------ |
| `CUTLASS_MLA` | fp16, bf16 | `auto`, `float16`, `bfloat16`, `fp8`, `fp8_e4m3` | 128 | Any | ❌ | ❌ | ❌ | ❌ | ✅ | Decoder | 10.x |
| `FLASHINFER_MLA` | fp16, bf16 | `auto`, `float16`, `bfloat16`, `fp8`, `fp8_e4m3` | 32, 64 | Any | ❌ | ❌ | ❌ | ❌ | ❌ | Decoder | 10.x |
| `FLASHINFER_MLA_SPARSE` | fp16, bf16 | `auto`, `float16`, `bfloat16`, `fp8`, `fp8_e4m3` | 32, 64 | 576 | ❌ | ❌ | | ❌ | ❌ | Decoder | 10.x |
| `FLASHINFER_MLA_SPARSE` | fp16, bf16 | `auto`, `float16`, `bfloat16`, `fp8`, `fp8_e4m3` | 32, 64 | Any | ❌ | ❌ | | ❌ | ❌ | Decoder | 10.x |
| `FLASHINFER_MLA_SPARSE_SM120` | bf16 | `auto`, `fp8`, `fp8_e4m3`, `fp8_ds_mla` | 64, 256 | Any | ❌ | ❌ | ❌ | ❌ | ❌ | Decoder | 12.x |
| `FLASHMLA` | fp16, bf16 | `auto`, `float16`, `bfloat16`, `fp8`, `fp8_e4m3` | 64 | Any | ❌ | ❌ | ❌ | ❌ | ✅ | Decoder | 9.x-10.x |
| `FLASHMLA_SPARSE` | bf16 | `auto`, `bfloat16`, `fp8_ds_mla` | 64 | 576 | ❌ | ❌ | ✅ | ❌ | ❌ | Decoder | 9.x-10.x |
| `FLASH_ATTN_MLA` | fp16, bf16 | `auto`, `float16`, `bfloat16` | %16 | Any | ❌ | ❌ | ❌ | ❌ | ✅ | Decoder | 9.x |
@@ -248,10 +239,11 @@ DeepSeek V4 sparse MLA uses its own decode backends, selected via
`--attention-backend=<BACKEND>` (e.g., `FLASHMLA_SPARSE_DSV4`,
`FLASHINFER_MLA_SPARSE_DSV4`). They share the V4 sparse-index
pipeline (compressor + SWA + indexer, 256-token blocks, head 512);
default on NVIDIA is `FLASHMLA_SPARSE_DSV4`.
default on NVIDIA is `FLASHINFER_MLA_SPARSE_DSV4` on SM12x and
`FLASHMLA_SPARSE_DSV4` on other supported CUDA architectures.
| Backend | Dtypes | KV Dtypes | Block Sizes | Head Sizes | Sink | Non-Causal | Sparse | MM Prefix | DCP | Attention Types | Compute Cap. |
| ------- | ------ | --------- | ----------- | ---------- | ---- | ---------- | ------ | --------- | --- | --------------- | ------------ |
| `FLASHINFER_MLA_SPARSE_DSV4` | bf16 | `auto`, `bfloat16`, `fp8` | Any | Any | | ❌ | | ❌ | ❌ | Decoder | Any |
| `FLASHMLA_SPARSE_DSV4` | bf16 | `auto`, `fp8_ds_mla`, `fp8` | 256 | 512 | | ❌ | ✅ | ❌ | ❌ | Decoder | 9.x-10.x |
| `FLASHINFER_MLA_SPARSE_DSV4` | bf16 | `auto`, `bfloat16`, `fp8`, `fp8_e4m3`, `fp8_ds_mla` | 256 | 512 | | ❌ | | ❌ | ❌ | Decoder | 10.x, 12.x |
| `FLASHMLA_SPARSE_DSV4` | bf16 | `auto`, `fp8_ds_mla`, `fp8` | 256 | 512 | | ❌ | ✅ | ❌ | ❌ | Decoder | 9.x-10.x |
| `ROCM_FLASHMLA_SPARSE_DSV4` | fp16, bf16 | `auto` | Any | Any | ❌ | ❌ | ❌ | ❌ | ❌ | Decoder | N/A |
+1 -1
View File
@@ -26,4 +26,4 @@ quack-kernels>=0.3.3
tokenspeed-mla==0.1.2
# Humming kernels for quantization gemm
humming-kernels[cu13]==0.1.4
humming-kernels[cu13]==0.1.6
+34 -35
View File
@@ -59,9 +59,8 @@ def _run_gsm8k_eval(server: RemoteOpenAIServer, stage: str) -> float:
return accuracy
@multi_gpu_test(num_gpus=4)
def test_elastic_ep_scaling():
vllm_serve_args = [
def _base_serve_args(use_async_eplb: bool = False) -> list[str]:
args = [
"--trust-remote-code",
"--tensor-parallel-size",
"1",
@@ -79,7 +78,11 @@ def test_elastic_ep_scaling():
"--eplb-config.num_redundant_experts",
"0",
"--eplb-config.use_async",
"false",
"true" if use_async_eplb else "false",
"--eplb-config.step_interval",
"10",
"--eplb-config.window_size",
"5",
"--data-parallel-backend",
"ray",
"--data-parallel-size",
@@ -90,7 +93,23 @@ def test_elastic_ep_scaling():
leader_address = os.environ.get("LEADER_ADDRESS")
if leader_address:
vllm_serve_args.extend(["--data-parallel-address", leader_address])
args.extend(["--data-parallel-address", leader_address])
return args
@pytest.mark.parametrize(
"use_async_eplb", [False, True], ids=["sync_eplb", "async_eplb"]
)
@multi_gpu_test(num_gpus=4)
def test_elastic_ep_scaling(use_async_eplb: bool):
if use_async_eplb:
from vllm.distributed.eplb.eplb_communicator import has_nixl
if not has_nixl():
pytest.skip("Async EPLB with elastic EP requires NIXL (not installed)")
vllm_serve_args = _base_serve_args(use_async_eplb)
with RemoteOpenAIServer(
MODEL_NAME, vllm_serve_args, env_dict={}, max_wait_seconds=1200
@@ -128,44 +147,24 @@ def test_elastic_ep_scaling():
print(f" Tolerance: {ACCURACY_TOL:.3f}")
@pytest.mark.parametrize(
"use_async_eplb", [False, True], ids=["sync_eplb", "async_eplb"]
)
@multi_gpu_test(num_gpus=4)
def test_elastic_ep_scaling_uneven():
def test_elastic_ep_scaling_uneven(use_async_eplb: bool):
"""Test scale up with uneven worker distribution.
This tests the case where num_new_workers % old_dp_size != 0,
specifically 2 -> 3 where remainder = 1 % 2 = 1.
This exercises the remainder handling in sender-receiver pairing.
"""
vllm_serve_args = [
"--trust-remote-code",
"--tensor-parallel-size",
"1",
"--gpu-memory-utilization",
"0.8",
"--max-model-len",
"4096",
"--max-num-seqs",
str(MAX_NUM_SEQS),
"--enable-expert-parallel",
"--all2all-backend",
"allgather_reducescatter",
"--enable-elastic-ep",
"--enable-eplb",
"--eplb-config.num_redundant_experts",
"0",
"--eplb-config.use_async",
"false",
"--data-parallel-backend",
"ray",
"--data-parallel-size",
"2",
"--api-server-count",
"1",
]
if use_async_eplb:
from vllm.distributed.eplb.eplb_communicator import has_nixl
leader_address = os.environ.get("LEADER_ADDRESS")
if leader_address:
vllm_serve_args.extend(["--data-parallel-address", leader_address])
if not has_nixl():
pytest.skip("Async EPLB with elastic EP requires NIXL (not installed)")
vllm_serve_args = _base_serve_args(use_async_eplb)
with RemoteOpenAIServer(
MODEL_NAME, vllm_serve_args, env_dict={}, max_wait_seconds=1200
+122
View File
@@ -782,3 +782,125 @@ def test_rearrange_expert_weights_profile_mode(world_size):
_test_rearrange_expert_weights_profile_mode,
world_size,
)
def _test_nixl_deferred_init_worker(
env,
world_size: int,
num_layers: int,
num_local_experts: int,
num_logical_experts: int,
) -> None:
"""Exercise NixlEplbCommunicator with defer_remote_setup=True (elastic EP path)."""
from vllm.distributed.eplb.eplb_communicator import NixlEplbCommunicator
set_env_vars_and_device(env)
vllm_config = VllmConfig()
vllm_config.parallel_config.tensor_parallel_size = world_size
with set_current_vllm_config(vllm_config):
ensure_model_parallel_initialized(
tensor_model_parallel_size=world_size, pipeline_model_parallel_size=1
)
ep_group_coordinator = get_tp_group()
ep_group = ep_group_coordinator.cpu_group
ep_rank = torch.distributed.get_rank()
device = torch.device(f"cuda:{ep_rank}")
total_physical_experts = world_size * num_local_experts
hidden_sizes = [32, 64]
redundancy_config = create_redundancy_config(
num_logical_experts, total_physical_experts
)
old_indices = create_expert_indices_with_redundancy(
num_layers,
num_logical_experts,
total_physical_experts,
redundancy_config,
)
new_redundancy_config = create_redundancy_config(
num_logical_experts, total_physical_experts
)
new_indices = create_expert_indices_with_redundancy(
num_layers,
num_logical_experts,
total_physical_experts,
new_redundancy_config,
)
expert_weights = create_expert_weights(
num_layers, num_local_experts, hidden_sizes, ep_rank, device, old_indices
)
expert_buffer = [torch.empty_like(w) for w in expert_weights[0]]
communicator = NixlEplbCommunicator(
cpu_group=ep_group_coordinator.cpu_group,
all_expert_weights=expert_weights,
expert_buffer=expert_buffer,
defer_remote_setup=True,
)
assert not communicator._remote_state_initialized
rearrange_expert_weights_inplace(
old_indices,
new_indices,
expert_weights,
expert_buffer,
ep_group,
communicator,
)
assert communicator._remote_state_initialized
local_ok = verify_expert_weights_after_shuffle(
expert_weights,
new_indices,
hidden_sizes,
ep_rank,
num_local_experts,
)
local_ok = (
verify_redundant_experts_have_same_weights(
expert_weights,
new_indices,
hidden_sizes,
ep_rank,
world_size,
num_local_experts,
)
and local_ok
)
assert_verification_synced(
local_ok,
"Deferred NIXL init verification failed on at least one rank.",
)
@pytest.mark.skipif(not has_nixl(), reason="NIXL is not available")
@pytest.mark.parametrize(
"world_size,num_layers,num_local_experts,num_logical_experts",
[(2, 2, 3, 4)],
)
def test_nixl_deferred_init(
world_size,
num_layers,
num_local_experts,
num_logical_experts,
):
"""Test NixlEplbCommunicator with defer_remote_setup=True (elastic EP path)."""
if torch.accelerator.device_count() < world_size:
pytest.skip(f"Need at least {world_size} GPUs to run the test")
distributed_run(
_test_nixl_deferred_init_worker,
world_size,
num_layers,
num_local_experts,
num_logical_experts,
)
@@ -0,0 +1,5 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
model_name: "openai/gpt-oss-20b"
metric_threshold: 0.568
reasoning_effort: "low"
@@ -0,0 +1,2 @@
# DGX Spark model configurations for GPQA evaluation
gpt-oss-20b-sm120.yaml
@@ -0,0 +1,12 @@
model_name: "nm-testing/Qwen3-30B-A3B-MXFP4A16"
accuracy_threshold: 0.86
num_questions: 1319
num_fewshot: 5
server_args: >-
--enforce-eager
--max-model-len 8192
--tensor-parallel-size 1
--quantization humming
--kernel-config.enable_flashinfer_autotune=False
env:
VLLM_HUMMING_INPUT_QUANT_CONFIG: '{"dtype":"float8e4m3"}'
@@ -0,0 +1,10 @@
model_name: "nm-testing/Qwen3-30B-A3B-MXFP4A16"
accuracy_threshold: 0.86
num_questions: 1319
num_fewshot: 5
server_args: >-
--enforce-eager
--max-model-len 8192
--tensor-parallel-size 1
--quantization humming
--kernel-config.enable_flashinfer_autotune=False
@@ -0,0 +1,2 @@
gpt-oss-20b-humming-act-fp8.yaml
Qwen3-30B-A3B-MXFP4A16-humming-act-fp8.yaml
@@ -0,0 +1,2 @@
gpt-oss-20b-humming.yaml
Qwen3-30B-A3B-MXFP4A16-humming.yaml
@@ -0,0 +1,11 @@
model_name: "openai/gpt-oss-20b"
accuracy_threshold: 0.30
num_questions: 1319
num_fewshot: 5
server_args: >-
--enforce-eager
--max-model-len 8192
--tensor-parallel-size 1
--moe-backend humming
env:
VLLM_HUMMING_INPUT_QUANT_CONFIG: '{"dtype":"float8e4m3"}'
@@ -0,0 +1,9 @@
model_name: "openai/gpt-oss-20b"
accuracy_threshold: 0.30
num_questions: 1319
num_fewshot: 5
server_args: >-
--enforce-eager
--max-model-len 8192
--tensor-parallel-size 1
--moe-backend humming
@@ -325,3 +325,24 @@ class TestWeightLoadingWithPaddedHiddenSize:
shard_id="w2",
expert_id=0,
)
class TestPerTensorScaleCoercion:
"""Regression test for shape-(1,) per-tensor scales (issue #43297).
llm-compressor NVFP4 emits per-tensor weight and input scales as
shape-(1,) tensors. `_to_scalar` collapses them to a 0-D scalar so the
scalar-slot assignments in the weight loader neither broadcast nor raise.
"""
def test_collapses_to_scalar(self):
# shape-(1,) and 0-D both reduce to a 0-D scalar.
for loaded_weight in (torch.tensor([0.5]), torch.tensor(0.5)):
scalar = RoutedExperts._to_scalar(loaded_weight)
assert scalar.shape == ()
assert scalar.item() == pytest.approx(0.5)
def test_rejects_non_scalar(self):
# numel > 1 must fail loudly instead of silently picking an element.
with pytest.raises(RuntimeError):
RoutedExperts._to_scalar(torch.tensor([0.1, 0.2]))
@@ -75,7 +75,7 @@ def get_ref_results(
@pytest.mark.parametrize("shape", SHAPES)
@pytest.mark.parametrize("seed", SEEDS)
@pytest.mark.parametrize("device", CUDA_DEVICES)
@pytest.mark.parametrize("backend", ["cutlass", "cudnn", "trtllm", "b12x"])
@pytest.mark.parametrize("backend", ["cute-dsl", "cutlass", "cudnn", "trtllm", "b12x"])
@pytest.mark.parametrize("autotune", [False, True])
@torch.inference_mode()
def test_flashinfer_nvfp4_gemm(
@@ -88,6 +88,8 @@ def test_flashinfer_nvfp4_gemm(
) -> None:
if "trtllm" in backend and dtype == torch.float16:
pytest.skip("Only torch.bfloat16 is supported for TRTLLM FP4 GEMM operations")
if backend == "cute-dsl" and not current_platform.is_device_capability_family(100):
pytest.skip("FlashInfer cutedsl backend is only supported on SM10x")
if backend == "b12x" and not current_platform.has_device_capability(120):
pytest.skip("b12x FP4 GEMM requires SM120+ (CC 12.0+)")
if backend == "b12x" and not has_flashinfer_b12x_gemm():
@@ -1,60 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
import sys
from hashlib import sha256
from pathlib import Path
from types import SimpleNamespace
from vllm.model_executor.warmup import kernel_warmup
def test_resolve_flashinfer_autotune_file_default_layout(
monkeypatch, tmp_path: Path
) -> None:
fake_jit = SimpleNamespace(
env=SimpleNamespace(
FLASHINFER_WORKSPACE_DIR=Path("/flashinfer-cache/0.6.11.post2/103a")
)
)
fake_flashinfer = SimpleNamespace(jit=fake_jit)
monkeypatch.setitem(sys.modules, "flashinfer", fake_flashinfer)
monkeypatch.setitem(sys.modules, "flashinfer.jit", fake_jit)
monkeypatch.setattr(
kernel_warmup, "aot_compile_hash_factors", lambda _: ["env-hash", "config-hash"]
)
monkeypatch.setattr(kernel_warmup.envs, "VLLM_CACHE_ROOT", str(tmp_path))
monkeypatch.setattr(kernel_warmup.envs, "VLLM_FLASHINFER_AUTOTUNE_CACHE_DIR", None)
runner = SimpleNamespace(vllm_config=SimpleNamespace())
cache_hash = sha256(str(["env-hash", "config-hash"]).encode()).hexdigest()
path = kernel_warmup._resolve_flashinfer_autotune_file(runner)
assert path == (
tmp_path
/ "flashinfer_autotune_cache"
/ "0.6.11.post2"
/ "103a"
/ cache_hash
/ "autotune_configs.json"
)
assert path.parent.is_dir()
def test_resolve_flashinfer_autotune_file_uses_override_dir(
monkeypatch, tmp_path: Path
) -> None:
monkeypatch.setattr(
kernel_warmup.envs, "VLLM_FLASHINFER_AUTOTUNE_CACHE_DIR", str(tmp_path)
)
monkeypatch.setattr(
kernel_warmup, "aot_compile_hash_factors", lambda _: ["env-hash", "config-hash"]
)
runner = SimpleNamespace(vllm_config=SimpleNamespace())
cache_hash = sha256(str(["env-hash", "config-hash"]).encode()).hexdigest()
path = kernel_warmup._resolve_flashinfer_autotune_file(runner)
assert path == tmp_path / cache_hash / "autotune_configs.json"
+7
View File
@@ -90,6 +90,7 @@ def test_models(example_prompts, model_name) -> None:
EAGER = [True, False]
SM_100_NVFP4_BACKENDS = [
"flashinfer_cutedsl",
"flashinfer_cudnn",
"flashinfer_trtllm",
"flashinfer_cutlass",
@@ -102,12 +103,18 @@ SM_100_NVFP4_BACKENDS = [
"backend",
[
"emulation",
"flashinfer_cutedsl",
"flashinfer_cudnn",
"flashinfer_trtllm", # the small seq_len ensures trtllm_8x4_layout backend is used
"flashinfer_cutlass",
],
)
def test_nvfp4(vllm_runner, model, eager, backend):
if backend == "flashinfer_cutedsl" and not (
current_platform.is_device_capability_family(100)
):
pytest.skip("The flashinfer_cutedsl backend is only supported on SM10x")
if (
not current_platform.has_device_capability(100)
and backend in SM_100_NVFP4_BACKENDS
@@ -0,0 +1,54 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Behavior checks for FlashInfer SM120 sparse MLA backend selection."""
from types import SimpleNamespace
import torch
from vllm.config import set_current_vllm_config
from vllm.platforms.interface import DeviceCapability
from vllm.utils import flashinfer as fi_utils
from vllm.v1.attention.backends.mla.flashinfer_mla_sparse import (
FlashInferMLASparseSM120Backend,
)
from vllm.v1.attention.backends.registry import AttentionBackendEnum
def _fake_vllm_config(model_type: str) -> SimpleNamespace:
return SimpleNamespace(
model_config=SimpleNamespace(
hf_text_config=SimpleNamespace(model_type=model_type, index_topk=2048),
),
)
def test_sm120_backend_uses_dedicated_backend_name() -> None:
assert FlashInferMLASparseSM120Backend.get_name() == "FLASHINFER_MLA_SPARSE_SM120"
assert (
AttentionBackendEnum.FLASHINFER_MLA_SPARSE_SM120.get_class()
is FlashInferMLASparseSM120Backend
)
def test_v32_glm_sm120_backend_accepts_glm_block_size(
monkeypatch,
) -> None:
monkeypatch.setattr(fi_utils, "has_flashinfer_sparse_mla_sm120", lambda: True)
with set_current_vllm_config(_fake_vllm_config("glm4_moe")):
invalid_reasons = FlashInferMLASparseSM120Backend.validate_configuration(
head_size=576,
dtype=torch.bfloat16,
kv_cache_dtype="fp8",
block_size=256,
use_mla=True,
has_sink=False,
use_sparse=True,
use_mm_prefix=False,
use_per_head_quant_scales=False,
device_capability=DeviceCapability(12, 0),
attn_type="decoder",
)
assert invalid_reasons == []
@@ -36,7 +36,7 @@ if not current_platform.is_cuda():
from vllm.utils.math_utils import cdiv
from vllm.v1.attention.backends.mla.flashinfer_mla_sparse import (
FlashInferMLASparseBackend,
FlashInferMLASparseTRTLLMBackend,
)
from vllm.v1.attention.backends.mla.flashmla_sparse import (
FlashMLASparseBackend,
@@ -174,8 +174,8 @@ def _quantize_dequantize_fp8_ds_mla(
@pytest.mark.parametrize(
"backend_cls",
[FlashMLASparseBackend, FlashInferMLASparseBackend],
ids=["FlashMLA", "FlashInfer"],
[FlashMLASparseBackend, FlashInferMLASparseTRTLLMBackend],
ids=["FlashMLA", "FlashInferTRTLLM"],
)
@pytest.mark.parametrize("batch_name", list(SPARSE_BACKEND_BATCH_SPECS.keys()))
@pytest.mark.parametrize("kv_cache_dtype", ["auto", "fp8", "fp8_ds_mla"])
@@ -217,9 +217,12 @@ def test_sparse_backend_decode_correctness(
ok, reason = flashmla.is_flashmla_sparse_supported()
if not ok:
pytest.skip(reason)
elif backend_cls == FlashInferMLASparseBackend:
if not current_platform.has_device_capability(100):
pytest.skip("FlashInferMLASparseBackend requires SM 10.0 or higher")
elif backend_cls == FlashInferMLASparseTRTLLMBackend:
device_capability = current_platform.get_device_capability()
if device_capability is None or not backend_cls.supports_compute_capability(
device_capability
):
pytest.skip("FlashInferMLASparseTRTLLMBackend requires SM 10.x capability")
batch_spec = SPARSE_BACKEND_BATCH_SPECS[batch_name]
use_fp8_ds_mla_quantization = kv_cache_dtype == "fp8_ds_mla"
+32
View File
@@ -4708,6 +4708,38 @@ def test_free_encoder_inputs_respects_unconfirmed_placeholders():
assert manager.get_cached_input_ids(request) == set()
def test_free_encoder_inputs_defers_for_eagle_lookahead():
"""With EAGLE speculative decoding, the encoder input is retained one extra
position so the drafter's +1 look-ahead mm-embedding gather (which reads one
position past the target's computed range) still finds it cached. This is
the primary mechanism that prevents the drafter "Encoder cache miss"; the
worker-side token-embedding fallback is only a backstop."""
scheduler = create_scheduler(model="llava-hf/llava-1.5-7b-hf")
# create_scheduler only builds ngram spec configs; force the eagle path that
# _free_encoder_inputs keys off (self.use_eagle).
scheduler.use_eagle = True
mm_positions = [[PlaceholderRange(offset=50, length=100)]]
request = create_requests(
num_requests=1,
num_tokens=250,
mm_positions=mm_positions,
)[0]
manager = scheduler.encoder_cache_manager
manager.allocate(request, 0)
mm_end = 150 # offset + length
# Confirmed progress reaches the range end: without spec decode this frees
# (see test below), but the drafter's +1 look-ahead still needs it.
request.num_computed_tokens = mm_end
scheduler._free_encoder_inputs(request)
assert manager.get_cached_input_ids(request) == {0}
# One position past the range end: the +1 look-ahead has now passed it.
request.num_computed_tokens = mm_end + 1
scheduler._free_encoder_inputs(request)
assert manager.get_cached_input_ids(request) == set()
def test_free_encoder_inputs_unchanged_without_spec_decode():
"""Without speculative decoding, encoder inputs are freed as soon as
num_computed_tokens passes the placeholder range, as before."""
@@ -3,6 +3,8 @@
import pytest
from vllm.platforms import current_platform
def test_mla_common_backend_rejects_cross_layer_kv_cache():
"""MLACommonBackend defaults to the identity permutation (layers dim
@@ -24,8 +26,13 @@ def test_mla_common_backend_rejects_cross_layer_kv_cache():
@pytest.mark.parametrize(
"backend_path",
# See: https://github.com/vllm-project/vllm/issues/46411
[
"vllm.v1.attention.backends.mla.triton_mla.TritonMLABackend",
]
if current_platform.is_rocm()
else [
"vllm.v1.attention.backends.mla.triton_mla.TritonMLABackend",
"vllm.v1.attention.backends.mla.cutlass_mla.CutlassMLABackend",
"vllm.v1.attention.backends.mla.flashattn_mla.FlashAttnMLABackend",
"vllm.v1.attention.backends.mla.flashmla.FlashMLABackend",
+183
View File
@@ -0,0 +1,183 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Worker-side unit tests for SimpleCPUOffloadConnector.
Covers the GPU->CPU store cross-stream synchronization: the store copy must be
ordered after the compute stream that writes the KV blocks, otherwise it can
read partially written / stale blocks and silently corrupt the CPU cache.
"""
from __future__ import annotations
import time
import pytest
import torch
from vllm.platforms import current_platform
if not current_platform.is_cuda_alike():
pytest.skip("Requires CUDA or ROCm", allow_module_level=True)
from vllm.v1.simple_kv_offload.copy_backend import DmaCopyBackend
from vllm.v1.simple_kv_offload.cuda_mem_ops import (
CU_MEMCPY_SRC_ACCESS_ORDER_ANY,
CU_MEMCPY_SRC_ACCESS_ORDER_STREAM,
build_params,
pin_tensor,
)
from vllm.v1.simple_kv_offload.metadata import SimpleCPUOffloadMetadata
from vllm.v1.simple_kv_offload.worker import SimpleCPUOffloadWorker
NUM_BLOCKS = 64
BLOCK_BYTES = 4096
ITERS = 30
# Keep the compute stream busy so the KV write lands late; this makes the
# store-vs-compute race deterministic instead of timing-dependent.
SLEEP_CYCLES = 50_000_000
def _make_backend() -> tuple[DmaCopyBackend, torch.Tensor, torch.Tensor]:
gpu = {"k": torch.zeros((NUM_BLOCKS, BLOCK_BYTES), dtype=torch.int8, device="cuda")}
cpu = {"k": torch.zeros((NUM_BLOCKS, BLOCK_BYTES), dtype=torch.int8, device="cpu")}
pin_tensor(cpu["k"])
low_pri, _ = torch.cuda.Stream.priority_range()
backend = DmaCopyBackend()
backend.init(
gpu,
cpu,
gpu["k"].device,
torch.cuda.Stream(priority=low_pri),
torch.cuda.Stream(priority=low_pri),
)
return backend, gpu["k"], cpu["k"]
def _drive_store(
backend: DmaCopyBackend,
gpu: torch.Tensor,
cpu: torch.Tensor,
*,
with_barrier: bool,
) -> int:
"""Run ITERS store cycles; return how many landed corrupted in the CPU pool.
Each cycle writes a unique value on a compute stream (after a deliberate
delay) and then issues the GPU->CPU store. The store is issued *after* the
write in host program order, mirroring the connector's deferred-store
assumption. Only the compute-done event creates a real device-side
happens-before edge.
"""
block_ids = list(range(gpu.shape[0]))
compute_stream = torch.cuda.Stream()
corrupt = 0
for it in range(ITERS):
val = (it % 126) + 1 # 1..126; distinct from the zero-initialized pool
with torch.cuda.stream(compute_stream):
torch.cuda._sleep(SLEEP_CYCLES)
gpu.fill_(val)
wait_event = None
if with_barrier:
wait_event = torch.Event()
wait_event.record(compute_stream)
store_events: list[tuple[int, torch.Event]] = []
backend.launch_copy(
block_ids,
block_ids,
is_store=True,
event_idx=it,
events_list=store_events,
wait_event=wait_event,
)
deadline = time.time() + 10.0
while not store_events and time.time() < deadline:
time.sleep(0.0005)
assert store_events, "background copy was never enqueued"
store_events[0][1].synchronize()
if int((cpu[:, 0].to(torch.int32) != val).sum().item()):
corrupt += 1
return corrupt
def test_store_orders_after_compute_write():
"""The store must wait for the compute event; without it, it races.
Asserts both directions so the test is self-validating: the no-barrier
control must actually corrupt (proving the race window is exercised), and
the fixed path with the compute-done event must be clean.
"""
backend, gpu, cpu = _make_backend()
try:
control = _drive_store(backend, gpu, cpu, with_barrier=False)
fixed = _drive_store(backend, gpu, cpu, with_barrier=True)
finally:
backend.shutdown()
assert control > 0, (
"no-barrier store did not race the compute write; the test no longer "
"exercises the hazard it is meant to guard"
)
assert fixed == 0, f"store raced compute even with the barrier: {fixed} corrupt"
class _RecordingBackend:
"""Captures launch_copy calls without touching the GPU."""
def __init__(self) -> None:
self.calls: list[dict] = []
def launch_copy(
self,
src_blocks,
dst_blocks,
is_store,
event_idx,
events_list,
wait_event=None,
) -> None:
self.calls.append({"is_store": is_store, "wait_event": wait_event})
def test_get_finished_passes_wait_event_for_store_only():
"""get_finished gates stores on a compute-done event but not loads."""
worker = SimpleCPUOffloadWorker(
vllm_config=None, kv_cache_config=None, cpu_capacity_bytes=0
)
recording = _RecordingBackend()
worker._backend = recording
worker._connector_metadata = SimpleCPUOffloadMetadata(
load_event=0,
load_gpu_blocks=[0],
load_cpu_blocks=[0],
store_event=1,
store_gpu_blocks=[1],
store_cpu_blocks=[1],
)
worker.get_finished(set())
store_calls = [c for c in recording.calls if c["is_store"]]
load_calls = [c for c in recording.calls if not c["is_store"]]
assert len(store_calls) == 1
assert len(load_calls) == 1
assert isinstance(store_calls[0]["wait_event"], torch.Event)
assert load_calls[0]["wait_event"] is None
def test_build_params_src_access_order():
"""build_params defaults to ANY and honors an explicit STREAM override."""
gpu = {"k": torch.zeros((4, 64), dtype=torch.int8, device="cuda")}
cpu = {"k": torch.zeros((4, 64), dtype=torch.int8, device="cpu")}
stream = torch.cuda.Stream()
default = build_params(gpu, cpu, stream)
assert default.attrs.srcAccessOrder == CU_MEMCPY_SRC_ACCESS_ORDER_ANY
ordered = build_params(
gpu, cpu, stream, src_access_order=CU_MEMCPY_SRC_ACCESS_ORDER_STREAM
)
assert ordered.attrs.srcAccessOrder == CU_MEMCPY_SRC_ACCESS_ORDER_STREAM
@@ -132,9 +132,9 @@ def get_available_attention_backends() -> list[str]:
)
return [
backend.name
for backend, _ in valid_backends
if backend not in EXCLUDED_BACKENDS
candidate.backend.name
for candidate in valid_backends
if candidate.backend not in EXCLUDED_BACKENDS
]
+157
View File
@@ -0,0 +1,157 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Tests for EncoderRunner.gather_mm_embeddings (model runner V2).
Covers the speculative-drafter encoder-cache handling: the drafter reads one
position ahead of the target model (``draft_lookahead``). The +1 look-ahead
feature past the processed boundary is used when its encoder output is present
and tolerated (token-embedding fallback) when it is not, while a miss within
the processed range still fails loudly.
"""
import numpy as np
import pytest
import torch
from vllm.multimodal.inputs import MultiModalFeatureSpec, PlaceholderRange
from vllm.v1.worker.gpu.mm.encoder_cache import EncoderCache
from vllm.v1.worker.gpu.mm.encoder_runner import EncoderRunner
pytestmark = pytest.mark.cpu_test
HIDDEN = 4
def _feature(identifier: str, offset: int, length: int) -> MultiModalFeatureSpec:
return MultiModalFeatureSpec(
data=None,
modality="image",
identifier=identifier,
mm_position=PlaceholderRange(offset=offset, length=length),
)
def _make_runner(
features: list[MultiModalFeatureSpec],
cached: list[MultiModalFeatureSpec],
) -> EncoderRunner:
cache = EncoderCache()
cache.mm_features["req0"] = features
for f in cached:
length = f.mm_position.length
cache.encoder_outputs[f.identifier] = torch.arange(
length * HIDDEN, dtype=torch.float32
).reshape(length, HIDDEN)
return EncoderRunner(
model=None, # unused by gather_mm_embeddings
max_num_tokens=64,
hidden_size=HIDDEN,
encoder_cache=cache,
dtype=torch.float32,
device=torch.device("cpu"),
)
def _gather(runner: EncoderRunner, *, num_scheduled: int, draft_lookahead: int):
# Single prefilling request, computed_prefill=0, prefill_len large.
return runner.gather_mm_embeddings(
req_ids=["req0"],
total_num_scheduled_tokens=num_scheduled,
num_scheduled_tokens=np.array([num_scheduled]),
query_start_loc=np.array([0]),
prefill_lens=np.array([1000]),
computed_prefill_lens=np.array([0]),
draft_lookahead=draft_lookahead,
)
def test_draft_lookahead_uses_boundary_feature_when_cached():
"""The drafter's +1 look-ahead can reach the feature at offset ==
processed_end (the next chunk). When its encoder output is already cached
(the scheduler encoded it ahead), it is used for the look-ahead position
rather than ignored."""
f0 = _feature("h0", offset=0, length=8)
f1 = _feature("h1", offset=8, length=8) # starts exactly at processed_end
runner = _make_runner([f0, f1], cached=[f0, f1])
mm_embeds, is_mm_embed = _gather(runner, num_scheduled=8, draft_lookahead=1)
# f0 covers positions 0..6 (+1 skew); f1's first embed covers position 7.
assert len(mm_embeds) == 2
assert bool(is_mm_embed[7])
assert int(is_mm_embed.sum()) == 8
def test_draft_lookahead_tolerates_missing_boundary_feature():
"""When the +1 look-ahead feature past the processed boundary is not yet
encoded, fall back to the token embedding (the draft token is verified by
the target) instead of raising."""
f0 = _feature("h0", offset=0, length=8)
f1 = _feature("h1", offset=8, length=8) # boundary feature, not cached
runner = _make_runner([f0, f1], cached=[f0])
mm_embeds, is_mm_embed = _gather(runner, num_scheduled=8, draft_lookahead=1)
# Only f0 is gathered; f1's boundary position falls back silently.
assert len(mm_embeds) == 1
assert not bool(is_mm_embed[7])
assert int(is_mm_embed.sum()) == 7
def test_draft_lookahead_raises_on_interior_miss():
"""A miss for a feature within the processed range (not the look-ahead
boundary) is a real invariant violation and must fail loudly, even on the
drafter path."""
f0 = _feature("h0", offset=0, length=8) # interior, within processed range
runner = _make_runner([f0], cached=[])
with pytest.raises(RuntimeError, match="Encoder cache miss"):
_gather(runner, num_scheduled=8, draft_lookahead=1)
def test_target_path_raises_on_encoder_cache_miss():
"""On the target path (no look-ahead) a miss is a real invariant
violation and must fail loudly."""
f0 = _feature("h0", offset=0, length=8)
runner = _make_runner([f0], cached=[])
with pytest.raises(RuntimeError, match="Encoder cache miss"):
_gather(runner, num_scheduled=8, draft_lookahead=0)
@pytest.mark.parametrize("draft_lookahead", [0, 1])
def test_multi_request_batch_gathers_per_request(draft_lookahead):
"""Two prefilling requests in one batch: per-request query bounds must be
indexed by request, not applied as whole arrays."""
a0 = _feature("a0", offset=0, length=8)
b0 = _feature("b0", offset=0, length=8)
cache = EncoderCache()
cache.mm_features["req0"] = [a0]
cache.mm_features["req1"] = [b0]
for f in (a0, b0):
cache.encoder_outputs[f.identifier] = torch.arange(
f.mm_position.length * HIDDEN, dtype=torch.float32
).reshape(f.mm_position.length, HIDDEN)
runner = EncoderRunner(
model=None,
max_num_tokens=64,
hidden_size=HIDDEN,
encoder_cache=cache,
dtype=torch.float32,
device=torch.device("cpu"),
)
mm_embeds, is_mm_embed = runner.gather_mm_embeddings(
req_ids=["req0", "req1"],
total_num_scheduled_tokens=16,
num_scheduled_tokens=np.array([8, 8]),
query_start_loc=np.array([0, 8]),
prefill_lens=np.array([1000, 1000]),
computed_prefill_lens=np.array([0, 0]),
draft_lookahead=draft_lookahead,
)
# Both requests contribute a feature; with the +1 skew each marks 7 of its
# 8 positions (the skew drops one), otherwise all 8.
assert len(mm_embeds) == 2
assert int(is_mm_embed.sum()) == (14 if draft_lookahead else 16)
@@ -0,0 +1,99 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Tests for GPUModelRunner._gather_mm_embeddings (model runner V1).
Mirrors tests/v1/worker/test_encoder_runner.py (the V2 runner): the EAGLE/MTP
drafter reads one position ahead of the target (shift_computed_tokens=1). The
+1 look-ahead feature past the processed boundary is used when its encoder
output is present and tolerated (token-embedding fallback) when it is not,
while a miss within the processed range still fails loudly.
`_gather_mm_embeddings` only uses CPU-side state, so it is exercised against a
lightweight stub for `self` instead of a full (CUDA-only) runner.
"""
from types import SimpleNamespace
import pytest
import torch
from vllm.multimodal.inputs import MultiModalFeatureSpec, PlaceholderRange
from vllm.v1.worker.gpu_model_runner import GPUModelRunner
pytestmark = pytest.mark.cpu_test
HIDDEN = 4
def _feature(identifier: str, offset: int, length: int) -> MultiModalFeatureSpec:
return MultiModalFeatureSpec(
data=None,
modality="image",
identifier=identifier,
mm_position=PlaceholderRange(offset=offset, length=length),
)
def _gather(features, cached, *, num_scheduled, shift, num_computed=0):
encoder_cache = {
f.identifier: torch.arange(
f.mm_position.length * HIDDEN, dtype=torch.float32
).reshape(f.mm_position.length, HIDDEN)
for f in cached
}
req_state = SimpleNamespace(num_computed_tokens=num_computed, mm_features=features)
runner = SimpleNamespace(
input_batch=SimpleNamespace(req_ids=["req0"]),
requests={"req0": req_state},
encoder_cache=encoder_cache,
is_multimodal_pruning_enabled=False,
uses_mrope=False,
)
scheduler_output = SimpleNamespace(
total_num_scheduled_tokens=num_scheduled,
num_scheduled_tokens={"req0": num_scheduled},
)
return GPUModelRunner._gather_mm_embeddings(
runner, scheduler_output, shift_computed_tokens=shift
)
def test_draft_shift_uses_boundary_feature_when_cached():
"""The drafter's +1 look-ahead reaches the feature at offset ==
processed_end; when it is already cached it is used for the look-ahead
position rather than ignored."""
f0 = _feature("h0", offset=0, length=8)
f1 = _feature("h1", offset=8, length=8) # starts exactly at processed_end
mm_embeds, is_mm_embed = _gather([f0, f1], [f0, f1], num_scheduled=8, shift=1)
# f0 covers positions 0..6 (+1 skew); f1's first embed covers position 7.
assert len(mm_embeds) == 2
assert bool(is_mm_embed[7])
assert int(is_mm_embed.sum()) == 8
def test_draft_shift_tolerates_missing_boundary_feature():
"""When the +1 look-ahead feature past the processed boundary is not yet
encoded, fall back to the token embedding instead of raising."""
f0 = _feature("h0", offset=0, length=8)
f1 = _feature("h1", offset=8, length=8) # boundary feature, not cached
mm_embeds, is_mm_embed = _gather([f0, f1], [f0], num_scheduled=8, shift=1)
assert len(mm_embeds) == 1 # only f0; f1's boundary position falls back
assert not bool(is_mm_embed[7])
assert int(is_mm_embed.sum()) == 7
def test_draft_shift_raises_on_interior_miss():
"""A miss for a feature within the processed range (not the look-ahead
boundary) is a real invariant violation, even on the drafter path."""
f0 = _feature("h0", offset=0, length=8) # interior, within processed range
with pytest.raises(RuntimeError, match="Encoder cache miss"):
_gather([f0], [], num_scheduled=8, shift=1)
def test_target_path_raises_on_encoder_cache_miss():
"""On the target path (no shift) a miss is a real invariant violation."""
f0 = _feature("h0", offset=0, length=8)
with pytest.raises(RuntimeError, match="Encoder cache miss"):
_gather([f0], [], num_scheduled=8, shift=0)
@@ -690,7 +690,9 @@ def parse_compute_capability(node: ast.ClassDef) -> str:
major_list.sort()
if len(major_list) == 1:
return f"{major_list[0]}.x"
return f"{major_list[0]}.x-{major_list[-1]}.x"
if major_list == list(range(major_list[0], major_list[-1] + 1)):
return f"{major_list[0]}.x-{major_list[-1]}.x"
return ", ".join(f"{major}.x" for major in major_list)
if min_cap:
if max_cap:
@@ -1668,7 +1670,8 @@ def generate_mla_section(
"`--attention-backend=<BACKEND>` (e.g., `FLASHMLA_SPARSE_DSV4`,",
"`FLASHINFER_MLA_SPARSE_DSV4`). They share the V4 sparse-index",
"pipeline (compressor + SWA + indexer, 256-token blocks, head 512);",
"default on NVIDIA is `FLASHMLA_SPARSE_DSV4`.",
"default on NVIDIA is `FLASHINFER_MLA_SPARSE_DSV4` on SM12x and",
"`FLASHMLA_SPARSE_DSV4` on other supported CUDA architectures.",
"",
]
)
@@ -956,6 +956,15 @@ class AsyncTPPass(VllmFusionPatternMatcherPass):
a_scale_view=a_scale_view,
)
)
self.register(
FlashInferAllGatherFP4Pattern(
self.model_dtype,
self.device,
"cute-dsl",
use_8x4_sf_layout=False,
a_scale_view="float8",
)
)
# NVFP4 reduce-scatter does not need scale communication: FP4
# scales are consumed by the local GEMM and only BF16 partial
# outputs are reduced. Keep this PR scoped to the all-gather
+2
View File
@@ -141,6 +141,7 @@ LinearBackend = Literal[
"auto",
"cutlass",
"flashinfer_cutlass",
"flashinfer_cutedsl",
"flashinfer_trtllm",
"flashinfer_cudnn",
"flashinfer_b12x",
@@ -198,6 +199,7 @@ class KernelConfig:
- "auto": Automatically select the best backend based on model and hardware
- "cutlass": Use CUTLASS-based kernels
- "flashinfer_cutlass": Use FlashInfer with CUTLASS kernels
- "flashinfer_cutedsl": Use FlashInfer with CuteDSL kernels
- "flashinfer_trtllm": Use FlashInfer with TensorRT-LLM kernels
- "flashinfer_cudnn": Use FlashInfer with cuDNN kernels
- "flashinfer_b12x": Use FlashInfer b12x CuteDSL NVFP4 GEMM (SM120+)
+22 -22
View File
@@ -803,13 +803,6 @@ class ParallelConfig:
if self.enable_elastic_ep:
if not self.enable_eplb:
raise ValueError("Elastic EP is only supported with enable_eplb=True.")
if self.eplb_config.use_async:
raise ValueError(
"Elastic EP requires the pynccl communicator, which is "
"incompatible with async EPLB due to NCCL multi-stream "
"conflicts. Disable async EPLB (eplb_config.use_async=False) "
"to use elastic EP."
)
if self.pipeline_parallel_size > 1:
raise ValueError(
"Elastic EP is not supported with pipeline parallelism "
@@ -821,6 +814,15 @@ class ParallelConfig:
"or data_parallel_hybrid_lb. Elastic EP relies on a single API "
"server and core client to coordinate scale up/down."
)
if self.eplb_config.use_async:
from vllm.distributed.nixl_utils import is_nixl_available
if not is_nixl_available():
raise ValueError(
"Elastic EP with async EPLB requires the NIXL "
"package. Either install NIXL or set "
"--eplb-config.use_async=false."
)
if self.data_parallel_size > 1 or self.data_parallel_size_local == 0:
# Data parallel was specified in the engine args.
@@ -929,23 +931,21 @@ class ParallelConfig:
)
if self.enable_eplb and self.eplb_config.communicator is None:
if self.enable_elastic_ep:
# Elastic EP requires stateless mode
# (torch.distributed.batch_isend_irecv doesn't
# support stateless mode), so we use PyNCCL backend
# Prefer NIXL when available: zero-copy RDMA reads, compatible
# with both async EPLB and elastic EP (deferred remote setup).
# Fallbacks: pynccl for elastic EP (stateless groups need it),
# torch_gloo for static EP. torch_nccl is avoided because NCCL
# is incompatible with async EPLB (multi-stream conflicts) and
# batched isend/irecv hangs under high load.
# See https://github.com/pytorch/pytorch/issues/174288
from vllm.distributed.nixl_utils import is_nixl_available
if is_nixl_available():
self.eplb_config.communicator = "nixl"
elif self.enable_elastic_ep:
self.eplb_config.communicator = "pynccl"
else:
# Avoid torch_nccl: NCCL is fundamentally incompatible
# with async EPLB due to multi-stream conflicts, and
# batched isend/irecv hangs under high load.
# See https://github.com/pytorch/pytorch/issues/174288
# Prefer nixl when available; fall back to torch_gloo.
from vllm.distributed.nixl_utils import is_nixl_available
if is_nixl_available():
self.eplb_config.communicator = "nixl"
else:
self.eplb_config.communicator = "torch_gloo"
self.eplb_config.communicator = "torch_gloo"
@property
def use_ray(self) -> bool:
@@ -207,6 +207,9 @@ class ElasticEPScalingExecutor:
)
if new_dp_size > old_dp_size:
self._set_eplb_suppressed(True)
eplb_state = self.worker.model_runner.eplb_state
if eplb_state is not None:
eplb_state.drain_async()
elif new_dp_size < old_dp_size:
self._stage_standby_moe_quant_methods()
@@ -540,6 +543,11 @@ class ElasticEPScalingExecutor:
eplb_model_state.physical_to_logical_map.shape[1]
)
eplb_state.is_async = is_async_enabled
# Start the async worker thread if it doesn't exist yet (idempotent).
# This is needed for new workers after scale-up: they create EplbState
# in setup_eplb_from_mapping() but don't start the thread there because
# groups aren't ready yet.
eplb_state.start_async_loop()
if get_ep_group().rank == 0:
logger.info("[Elastic EP] Expert resharding completed")
@@ -549,6 +557,9 @@ class ElasticEPScalingExecutor:
def perform_scale_down_eplb_reshuffle(self, new_dp_size: int) -> None:
self._set_eplb_suppressed(True)
eplb_state = self.worker.model_runner.eplb_state
if eplb_state is not None:
eplb_state.drain_async()
parallel_config = self.worker.vllm_config.parallel_config
tp_size = parallel_config.tensor_parallel_size
old_ep_size = parallel_config.data_parallel_size * tp_size
+27 -13
View File
@@ -8,7 +8,6 @@ import threading
from typing import TYPE_CHECKING
import torch
from torch.distributed import ProcessGroup
from vllm.distributed.parallel_state import get_eplb_group
from vllm.logger import init_logger
@@ -26,8 +25,7 @@ def start_async_worker(
state: "EplbState",
is_profile: bool = False,
) -> threading.Thread:
eplb_group = get_eplb_group().device_group
rank = eplb_group.rank()
rank = get_eplb_group().device_group.rank()
device_index = state.cuda_device_index
assert state.is_async
@@ -38,7 +36,6 @@ def start_async_worker(
try:
transfer_run_periodically(
state=state,
eplb_group=eplb_group,
cuda_stream=cuda_stream,
is_profile=is_profile,
)
@@ -78,13 +75,15 @@ def run_rebalance_experts(
def transfer_run_periodically(
state: "EplbState",
eplb_group: ProcessGroup,
cuda_stream: torch.cuda.Stream,
is_profile: bool = False,
) -> None:
while True:
state.rearrange_event.wait(stream=cuda_stream)
logger.info("async worker woke up for EPLB transfer")
eplb_group = get_eplb_group().device_group
eplb_cpu_group = get_eplb_group().cpu_group
ep_rank = eplb_group.rank()
assert state.is_async
for model_state in state.model_states.values():
@@ -101,16 +100,32 @@ def transfer_run_periodically(
new_physical_to_logical_map = run_rebalance_experts(
model_state, state, physical_to_logical_map_cpu, cuda_stream
)
logger.info(
"Async worker computed new indices for model %s",
model_state.model_name,
)
# Execute one EPLB layer transfer per model forward pass. Each iteration
# of this loop will copy the new set of expert weights into
# model_state.expert_buffer, which will be consumed by the main thread in
# move_to_workspace
while model_state.rebalanced and layer_idx < num_layers:
# move_to_workspace.
# We sync the rebalanced flag across ranks before each iteration so
# all ranks make a coordinated decision to continue or stop.
while layer_idx < num_layers:
flag = torch.tensor(
[int(model_state.rebalanced)],
dtype=torch.int32,
device="cpu",
)
torch.distributed.all_reduce(flag, group=eplb_cpu_group)
if int(flag.item()) != eplb_cpu_group.size():
logger.warning(
"async worker (rank=%d): layer %d coordinated stop "
"(flag_sum=%d, group_size=%d)",
ep_rank,
layer_idx,
int(flag.item()),
eplb_cpu_group.size(),
)
model_state.rebalanced = False
break
transfer_metadata = transfer_layer(
old_layer_indices=physical_to_logical_map_cpu[layer_idx],
new_layer_indices=new_physical_to_logical_map[layer_idx],
@@ -143,6 +158,5 @@ def transfer_run_periodically(
# finish copying model_state.expert_buffer into
# model_state.model.expert_weights[layer_idx]
consumed_event.wait(stream=cuda_stream)
logger.debug("Layer %d transfer complete", layer_idx)
assert model_state.pending_result is None
layer_idx += 1
+71 -25
View File
@@ -246,7 +246,20 @@ class NixlEplbCommunicator(EplbCommunicator):
cpu_group: ProcessGroup,
all_expert_weights: Sequence[Sequence[torch.Tensor]],
expert_buffer: Sequence[torch.Tensor],
defer_remote_setup: bool = False,
) -> None:
"""Create a NIXL-backed EPLB communicator.
Args:
cpu_group: CPU process group for metadata exchange.
all_expert_weights: Expert weight tensors for all MoE layers.
expert_buffer: Pre-allocated receive buffer tensors.
defer_remote_setup: If True, postpone the collective
all-gather of NIXL agent metadata until the first
``set_transfer_context`` call. Required for elastic EP
where ranks join asynchronously and cannot participate
in collectives at construction time.
"""
assert all_expert_weights, (
"NixlEplbCommunicator requires non-empty all_expert_weights."
)
@@ -302,10 +315,29 @@ class NixlEplbCommunicator(EplbCommunicator):
] = {}
self._cuda_device_id = int(self._device.index or 0)
self._remote_state_initialized = False
self._init_step("buffers", self._init_registered_buffers)
if defer_remote_setup:
logger.info_once("NIXL EPLB: deferring remote agent setup (elastic EP).")
else:
self._init_remote_state()
self._log_initialized()
def _init_remote_state(self) -> None:
"""Exchange NIXL agent metadata and RDMA pointer info with all peers.
This is a collective operation (uses ``all_gather_object`` twice).
Under elastic EP the call is deferred to the first
``set_transfer_context`` invocation, where all ranks are
guaranteed to be synchronized.
"""
self._init_step("agents", self._init_remote_agents)
self._init_step("send meta", self._exchange_remote_send_meta)
self._log_initialized()
self._remote_state_initialized = True
def _ensure_remote_state(self) -> None:
if not self._remote_state_initialized:
self._init_remote_state()
@property
def needs_profile_buffer_reservation(self) -> bool:
@@ -339,8 +371,7 @@ class NixlEplbCommunicator(EplbCommunicator):
pass
def set_transfer_context(self, old_indices: np.ndarray, layer_idx: int) -> None:
# Pre-compute expert_id -> src_row mapping for every rank so that
# add_recv can immediately issue NIXL READs.
self._ensure_remote_state()
assert not self._xfer_entries, (
f"set_transfer_context() called with {len(self._xfer_entries)} "
f"pending transfers from layer {self._layer_idx}; "
@@ -523,6 +554,21 @@ class NixlEplbCommunicator(EplbCommunicator):
)
return (local_handle, remote_handle, xfer_handle)
def _post_read_barrier(self) -> None:
"""Correctness fence: prevents overwrite-while-remote-read race.
We avoid ``torch.distributed.monitored_barrier`` because it
calls ``get_backend(group)`` which fails for stateless groups
(elastic EP). An async ``all_reduce`` + ``wait(timeout)``
works with both regular and stateless groups and provides
equivalent timeout detection.
"""
_dummy = torch.zeros(1, dtype=torch.int32)
work = torch.distributed.all_reduce(
_dummy, group=self._cpu_group, async_op=True
)
work.wait(timeout=timedelta(minutes=5))
def execute(self) -> None:
assert self._layer_idx is not None or not self._xfer_entries, (
"set_transfer_context() must be called before execute() "
@@ -531,13 +577,7 @@ class NixlEplbCommunicator(EplbCommunicator):
try:
self._wait_for_all_transfers([x[2] for x in self._xfer_entries])
# Post-READ barrier.
# Correctness fence for zero-copy: prevents overwrite-while-
# remote-read race.
torch.distributed.monitored_barrier(
group=self._cpu_group,
timeout=timedelta(minutes=5),
)
self._post_read_barrier()
finally:
for local_h, remote_h, xfer_h in self._xfer_entries:
with contextlib.suppress(Exception):
@@ -628,11 +668,13 @@ def create_eplb_communicator(
device and CPU communication groups.
backend: Communicator backend name (``"torch_nccl"``,
``"torch_gloo"``, ``"pynccl"``, or ``"nixl"``).
Stateless (elastic EP) groups only support ``"torch_nccl"``
and ``"pynccl"``; ``"torch_nccl"`` is silently promoted to
``"pynccl"`` in that case. When tensors reside on CPU,
``"torch_gloo"`` or ``"torch_nccl"`` are used via the CPU
process group.
Falls back to ``"torch_nccl"`` when *None*.
Stateless (elastic EP) groups support ``"torch_nccl"``,
``"pynccl"``, and ``"nixl"``; ``"torch_nccl"`` is silently
promoted to ``"pynccl"``. ``"nixl"`` uses deferred remote
agent setup to avoid collective deadlocks during elastic
scaling. When tensors reside on CPU, ``"torch_gloo"`` or
``"torch_nccl"`` are used via the CPU process group.
expert_weights: Expert weight tensors for *all* MoE layers.
Shape ``(num_layers)(num_tensors_per_layer)``.
NixlEplbCommunicator registers all layers with NIXL for
@@ -686,18 +728,21 @@ def create_eplb_communicator(
is_stateless = isinstance(group_coordinator, StatelessGroupCoordinator)
if is_stateless:
if backend not in ("torch_nccl", "pynccl"):
if backend == "nixl":
pass # handled below with defer_remote_setup=True
elif backend not in ("torch_nccl", "pynccl"):
raise ValueError(
f"Elastic EP requires 'torch_nccl' or 'pynccl' EPLB communicator "
f"(got '{backend}')."
f"Elastic EP requires 'torch_nccl', 'pynccl', or 'nixl' "
f"EPLB communicator (got '{backend}')."
)
if backend == "torch_nccl":
logger.warning(
"Stateless elastic EP requires PyNCCL backend. "
"Forcing EPLB communicator to 'pynccl'."
)
backend = "pynccl"
return _create_pynccl()
else:
if backend == "torch_nccl":
logger.warning(
"Stateless elastic EP requires PyNCCL backend. "
"Forcing EPLB communicator to 'pynccl'."
)
backend = "pynccl"
return _create_pynccl()
if backend == "nixl":
if not has_nixl():
@@ -714,6 +759,7 @@ def create_eplb_communicator(
cpu_group=group_coordinator.cpu_group,
all_expert_weights=expert_weights,
expert_buffer=expert_buffer,
defer_remote_setup=is_stateless,
)
except Exception as exc:
raise RuntimeError(
+42 -2
View File
@@ -27,6 +27,7 @@ physical experts.
"""
import threading
import time
from collections.abc import Sequence
from dataclasses import dataclass
@@ -825,6 +826,45 @@ class EplbState:
is_profile=is_profile,
)
def drain_async(self) -> None:
"""Drain in-flight async EPLB by consuming all remaining layer results.
Each pending result is acknowledged (consumed_event recorded) so the
async worker can proceed, but the transferred weights are intentionally
NOT applied a full synchronous rearrange is expected to follow.
Ranks are kept in lockstep via _all_ranks_result_ready (all_reduce
on the EP CPU group). The async worker's coordinated-stop collectives
use the separate EPLB group, so the two sets of collectives do not
interfere.
No-op when no async cycle is in progress (rebalanced=False).
"""
if not self.is_async:
return
for model_key, ms in self.model_states.items():
needs_drain = ms.rebalanced
if needs_drain:
logger.info(
"Draining async EPLB worker for model %s",
model_key,
)
while ms.rebalanced:
if self._all_ranks_result_ready(ms):
result = ms.pending_result
assert result is not None
if result.layer_idx == ms.model.num_moe_layers - 1:
ms.rebalanced = False
ms.pending_result = None
result.consumed_event.record()
else:
time.sleep(0.001)
if needs_drain:
logger.info(
"Async EPLB worker drained for model %s",
model_key,
)
def _all_ranks_result_ready(self, model_state: EplbModelState) -> bool:
parallel_state = get_ep_group()
has_result = int(model_state.pending_result is not None)
@@ -850,8 +890,9 @@ class EplbState:
"""
All-reduce a list of tensors.
"""
ep_group = get_ep_group().device_group
if len(tensor_list) == 1:
all_reduce(tensor_list[0], group=get_ep_group().device_group)
all_reduce(tensor_list[0], group=ep_group)
return tensor_list
assert all(t.dim() == 2 for t in tensor_list), "All tensors must be 2D."
assert all(t.shape[1] == tensor_list[0].shape[1] for t in tensor_list), (
@@ -863,7 +904,6 @@ class EplbState:
shapes = [t.shape for t in tensor_list]
concat_tensor = torch.cat(tensor_list, dim=0)
ep_group = get_ep_group().device_group
all_reduce(concat_tensor, group=ep_group)
all_reduce_list = []
@@ -387,40 +387,28 @@ class DecodeBenchConnectorWorker:
kv_cache = self.kv_caches[layer_name]
# Convert block_ids to tensor on device
block_ids_tensor = torch.tensor(
block_ids, dtype=torch.long, device=kv_cache.device
)
# Filter invalid block IDs
valid_mask = block_ids_tensor < kv_cache.shape[0]
valid_block_ids = block_ids_tensor[valid_mask]
if len(valid_block_ids) == 0:
continue
# Create fill values - either constant or random
block_shape = kv_cache.shape[1:]
if self.fill_std > 0:
# Random normal sampling
fill_values = torch.normal(
mean=self.fill_mean,
std=self.fill_std,
size=(len(valid_block_ids),) + block_shape,
dtype=kv_cache.dtype,
device=kv_cache.device,
)
# Attention layers store KV as a single block-indexed tensor whose
# first dim is num_blocks; fill the requested block rows. Hybrid /
# linear-attention layers (e.g. Mamba, Kimi Delta Attention) store
# their state as a list/tuple of tensors that are NOT block-indexed
# — each tensor is a single state buffer with no num_blocks
# dimension — so fill each tensor in its entirety with the same
# dummy values.
if isinstance(kv_cache, torch.Tensor):
self._fill_block_tensor(kv_cache, block_ids)
elif isinstance(kv_cache, (list, tuple)) and all(
isinstance(t, torch.Tensor) for t in kv_cache
):
for state_tensor in kv_cache:
self._fill_state_tensor(state_tensor)
else:
# Constant fill value
fill_values = torch.full(
(len(valid_block_ids),) + block_shape,
self.fill_mean,
dtype=kv_cache.dtype,
device=kv_cache.device,
logger.warning_once(
"DecodeBenchConnector: skipping fill for layer %s whose KV "
"cache is %s, not a tensor or a list/tuple of tensors.",
layer_name,
type(kv_cache).__name__,
)
# Batch fill operation
kv_cache[valid_block_ids] = fill_values
continue
logger.debug(
"DecodeBenchConnector: Filled %d blocks in group %d with %s values "
@@ -431,3 +419,62 @@ class DecodeBenchConnectorWorker:
self.fill_mean,
self.fill_std,
)
def _fill_block_tensor(self, kv_cache: torch.Tensor, block_ids: list[int]):
"""Fill the requested block rows of a block-indexed KV cache tensor.
Args:
kv_cache: A KV cache tensor whose first dim is num_blocks.
block_ids: Block IDs to fill. IDs that are out of range for this
tensor's first dim are ignored.
"""
# Convert block_ids to tensor on device
block_ids_tensor = torch.tensor(
block_ids, dtype=torch.long, device=kv_cache.device
)
# Filter invalid block IDs
valid_mask = block_ids_tensor < kv_cache.shape[0]
valid_block_ids = block_ids_tensor[valid_mask]
if len(valid_block_ids) == 0:
return
# Create fill values - either constant or random
block_shape = kv_cache.shape[1:]
if self.fill_std > 0:
# Random normal sampling
fill_values = torch.normal(
mean=self.fill_mean,
std=self.fill_std,
size=(len(valid_block_ids),) + block_shape,
dtype=kv_cache.dtype,
device=kv_cache.device,
)
else:
# Constant fill value
fill_values = torch.full(
(len(valid_block_ids),) + block_shape,
self.fill_mean,
dtype=kv_cache.dtype,
device=kv_cache.device,
)
# Batch fill operation
kv_cache[valid_block_ids] = fill_values
def _fill_state_tensor(self, kv_cache: torch.Tensor):
"""Fill an entire non-block-indexed state tensor with dummy values.
Hybrid / linear-attention layers (e.g. Mamba, Kimi Delta Attention)
store their per-layer state as tensors with no num_blocks dimension,
so the whole tensor is filled with the same constant or random values
used for block fills, rather than selected block rows.
Args:
kv_cache: A state tensor to fill in its entirety.
"""
if self.fill_std > 0:
kv_cache.normal_(mean=self.fill_mean, std=self.fill_std)
else:
kv_cache.fill_(self.fill_mean)
@@ -115,6 +115,7 @@ from vllm.model_executor.kernels.linear.nvfp4.fbgemm import (
from vllm.model_executor.kernels.linear.nvfp4.flashinfer import (
FlashInferB12xNvFp4LinearKernel,
FlashInferCudnnNvFp4LinearKernel,
FlashInferCuteDslNvFp4LinearKernel,
FlashInferCutlassNvFp4LinearKernel,
FlashInferTrtllmNvFp4LinearKernel,
)
@@ -209,6 +210,9 @@ _LINEAR_BACKEND_KERNEL_MAP: dict[str, set[type]] = {
FlashInferCutlassNvFp4LinearKernel,
FlashInferMxFp4LinearKernel,
},
"flashinfer_cutedsl": {
FlashInferCuteDslNvFp4LinearKernel,
},
"flashinfer_trtllm": {
FlashInferTrtllmNvFp4LinearKernel,
},
@@ -399,6 +403,7 @@ _POSSIBLE_MXFP8_KERNELS: dict[PlatformEnum, list[type[Mxfp8LinearKernel]]] = {
_POSSIBLE_NVFP4_KERNELS: dict[PlatformEnum, list[type[NvFp4LinearKernel]]] = {
PlatformEnum.CUDA: [
FlashInferCuteDslNvFp4LinearKernel,
# FlashInferB12xNvFp4LinearKernel excluded from auto-selection until
# upstream CUTLASS SM121 MMA op guard is resolved; use
# --linear-backend flashinfer_b12x to opt in explicitly.
@@ -1038,6 +1043,7 @@ __all__ = [
"CutlassNvFp4LinearKernel",
"EmulationNvFp4LinearKernel",
"FbgemmNvFp4LinearKernel",
"FlashInferCuteDslNvFp4LinearKernel",
"FlashInferB12xNvFp4LinearKernel",
"FlashInferCutlassNvFp4LinearKernel",
"FlashInferTrtllmNvFp4LinearKernel",
@@ -28,6 +28,72 @@ from vllm.utils.flashinfer import (
from .base import NvFp4LinearKernel, NvFp4LinearLayerConfig
class FlashInferCuteDslNvFp4LinearKernel(NvFp4LinearKernel):
"""NVFP4 GEMM via FlashInfer's cutedsl backend."""
@classmethod
def is_supported(
cls, compute_capability: int | None = None
) -> tuple[bool, str | None]:
if not current_platform.is_device_capability_family(100):
return False, "FlashInfer cutedsl requires sm_10x"
if not has_flashinfer():
return False, "FlashInfer required"
return True, None
@classmethod
def can_implement(cls, config: NvFp4LinearLayerConfig) -> tuple[bool, str | None]:
return True, None
def process_weights_after_loading(self, layer: torch.nn.Module) -> None:
# cutedsl uses the same swizzled + padded layout as cutlass.
layer.weight_scale = torch.nn.Parameter(
swizzle_blockscale(layer.weight_scale.data), requires_grad=False
)
padded_weight, weights_padding_cols = pad_nvfp4_weight_for_cutlass(
layer.weight.data
)
layer.weight = torch.nn.Parameter(padded_weight, requires_grad=False)
layer.weights_padding_cols = weights_padding_cols
def apply_weights(
self,
layer: torch.nn.Module,
x: torch.Tensor,
bias: torch.Tensor | None = None,
) -> torch.Tensor:
output_size = layer.output_size_per_partition
output_dtype = x.dtype
output_shape = [*x.shape[:-1], output_size]
x_fp4, x_blockscale = scaled_fp4_quant(
x,
layer.input_global_scale_inv,
is_sf_swizzled_layout=True,
backend="flashinfer-cutedsl",
)
x_fp4 = pad_nvfp4_activation_for_cutlass(
x_fp4, getattr(layer, "weights_padding_cols", 0)
)
out = flashinfer_scaled_fp4_mm(
x_fp4,
layer.weight,
x_blockscale,
layer.weight_scale,
layer.alpha,
output_dtype,
backend="cute-dsl",
)
out = slice_nvfp4_output(out, output_size)
if bias is not None:
out = out + bias
return out.view(*output_shape)
class FlashInferCutlassNvFp4LinearKernel(NvFp4LinearKernel):
"""NVFP4 GEMM via FlashInfer's CUTLASS wrapper."""
@@ -208,6 +208,7 @@ from vllm.config import (
get_current_vllm_config,
get_current_vllm_config_or_none,
)
from vllm.config.cache import CacheDType
from vllm.distributed.parallel_state import (
get_dcp_group,
is_global_first_rank,
@@ -319,6 +320,22 @@ def _detect_output_quant_key(
return kFp8StaticTensorSym
def _canonicalize_sparse_mla_kv_cache_dtype(
attn_backend: type[AttentionBackend],
kv_cache_dtype: CacheDType,
) -> CacheDType:
backend_name = attn_backend.get_name()
if backend_name == "FLASHMLA_SPARSE" and is_quantized_kv_cache(kv_cache_dtype):
return "fp8_ds_mla"
if backend_name == "FLASHINFER_MLA_SPARSE_SM120" and kv_cache_dtype in (
"auto",
"fp8",
"fp8_e4m3",
):
return "fp8_ds_mla"
return kv_cache_dtype
class MLAAttention(nn.Module, AttentionLayerBase):
"""Multi-Head Latent Attention layer.
@@ -369,7 +386,7 @@ class MLAAttention(nn.Module, AttentionLayerBase):
self.qk_head_dim = self.qk_nope_head_dim + self.qk_rope_head_dim
if cache_config is not None:
kv_cache_dtype = cache_config.cache_dtype
kv_cache_dtype: CacheDType = cache_config.cache_dtype
calculate_kv_scales = cache_config.calculate_kv_scales
else:
kv_cache_dtype = "auto"
@@ -393,24 +410,22 @@ class MLAAttention(nn.Module, AttentionLayerBase):
num_heads=self.num_heads,
)
# FlashMLA Sparse Attention fp8 backend uses "fp8_ds_mla" kv-cache format
# Automatically convert fp8 kv-cache format to "fp8_ds_mla"
if (
self.attn_backend.get_name() == "FLASHMLA_SPARSE"
and is_quantized_kv_cache(kv_cache_dtype)
and kv_cache_dtype != "fp8_ds_mla"
):
assert cache_config is not None
cache_config.cache_dtype = "fp8_ds_mla"
kv_cache_dtype = "fp8_ds_mla"
normalized_kv_cache_dtype = _canonicalize_sparse_mla_kv_cache_dtype(
self.attn_backend, kv_cache_dtype
)
if normalized_kv_cache_dtype != kv_cache_dtype:
if cache_config is not None:
cache_config.cache_dtype = normalized_kv_cache_dtype
kv_cache_dtype = normalized_kv_cache_dtype
logger.info_once(
"Using DeepSeek's fp8_ds_mla KV cache format. To use standard "
"fp8 kv-cache format, please set `--attention-backend "
"FLASHINFER_MLA_SPARSE`"
"Using %s KV cache format for %s backend.",
kv_cache_dtype,
self.attn_backend.get_name(),
)
if (
self.attn_backend.get_name() == "FLASHINFER_MLA_SPARSE"
and kv_cache_dtype != "fp8_ds_mla"
and is_quantized_kv_cache(kv_cache_dtype)
):
logger.info_once(
@@ -23,24 +23,82 @@ def expert_num_tokens_round_up_and_sum(
return torch.sum(ent).item()
def compute_aligned_M_and_alignment(
M: int,
num_topk: int,
local_num_experts: int,
alignment: int,
expert_tokens_meta: mk.ExpertTokensMetadata | None,
) -> tuple[int, int]:
"""Return (M_sum, alignment_used).
`alignment_used` may be smaller than the caller-supplied `alignment` on
SM100/SM120 when DeepGEMM can JIT a smaller BLOCK_M for the per-call
expected_m. Callers that index by block size (e.g. ``M_sum // block_m``)
or assert workspace alignment must use the returned `alignment_used`,
not their original `alignment` argument.
Prefer this over the int-returning :func:`compute_aligned_M` when the
GEMM call site needs to wrap itself in ``mk_alignment_scope`` or
otherwise reason about the actual per-expert padding.
"""
if (expert_tokens_meta is not None) and (
expert_tokens_meta.expert_num_tokens_cpu is not None
):
return (
expert_num_tokens_round_up_and_sum(
expert_tokens_meta.expert_num_tokens_cpu, alignment=alignment
),
alignment,
)
# expert_num_tokens not on cpu. Cap padding by min(M*num_topk,
# local_num_experts) — at batch=1 decode only `num_topk` experts can be
# active, so the worst-case `local_num_experts*(align-1)` is too loose.
# Also shrink `alignment` to DeepGEMM's per-call theoretical BLOCK_M on
# SM100/SM120 when smaller.
expected_m = M * num_topk
try:
from vllm.utils.deep_gemm import (
get_theoretical_mk_alignment_for_contiguous_layout,
)
# num_groups=local_num_experts so the helper recovers per-expert em;
# omitting it over-picks BLOCK_M on SM120 (heuristic assumes em is
# already per-expert).
per_call_align = get_theoretical_mk_alignment_for_contiguous_layout(
expected_m=expected_m,
num_groups=local_num_experts,
)
if per_call_align and per_call_align <= alignment:
alignment = per_call_align
except Exception:
pass
max_active_experts = min(M * num_topk, local_num_experts)
M_sum = (M * num_topk) + max_active_experts * (alignment - 1)
M_sum = round_up(M_sum, alignment)
return M_sum, alignment
def compute_aligned_M(
M: int,
num_topk: int,
local_num_experts: int,
alignment: int,
expert_tokens_meta: mk.ExpertTokensMetadata | None,
):
if (expert_tokens_meta is not None) and (
expert_tokens_meta.expert_num_tokens_cpu is not None
):
return expert_num_tokens_round_up_and_sum(
expert_tokens_meta.expert_num_tokens_cpu, alignment=alignment
)
) -> int:
"""Return ``M_sum`` only (backward-compat wrapper).
# expert_num_tokens information is not available on the cpu.
# compute the max required size.
M_sum = (M * num_topk) + local_num_experts * (alignment - 1)
M_sum = round_up(M_sum, alignment)
Equivalent to :func:`compute_aligned_M_and_alignment`'s first return
value. Existing downstream callers and the warmup path that only size
a workspace use this. Call sites that need the actual per-expert
alignment (to wrap GEMMs in ``mk_alignment_scope``) should use
:func:`compute_aligned_M_and_alignment` instead.
"""
M_sum, _ = compute_aligned_M_and_alignment(
M, num_topk, local_num_experts, alignment, expert_tokens_meta
)
return M_sum
@@ -51,12 +109,6 @@ def apply_expert_map(expert_id, expert_map):
return expert_id
@triton.jit
def round_up_128(x: int) -> int:
y = 128
return ((x + y - 1) // y) * y
@triton.jit
def _fwd_kernel_ep_scatter_1(
num_recv_tokens_per_expert,
@@ -65,6 +117,7 @@ def _fwd_kernel_ep_scatter_1(
num_experts: tl.constexpr,
BLOCK_E: tl.constexpr,
BLOCK_EXPERT_NUM: tl.constexpr,
ALIGN_M: tl.constexpr,
):
cur_expert = tl.program_id(0)
@@ -74,7 +127,8 @@ def _fwd_kernel_ep_scatter_1(
mask=offset_cumsum < num_experts,
other=0,
)
tokens_per_expert = round_up_128(tokens_per_expert)
# Round up to ALIGN_M so cumsum matches the workspace's per-expert slices.
tokens_per_expert = ((tokens_per_expert + ALIGN_M - 1) // ALIGN_M) * ALIGN_M
cumsum = tl.cumsum(tokens_per_expert) - tokens_per_expert
# Extract this block's offset from the register vector (warp shuffle,
@@ -227,10 +281,12 @@ def ep_scatter(
output_tensor_scale: torch.Tensor,
m_indices: torch.Tensor,
output_index: torch.Tensor,
align_m: int = 128,
block_size: int = 128,
pack_ue8m0: bool = False,
):
BLOCK_E = 128 # token num of per expert is aligned to 128
# BLOCK_E is the m_indices fill-loop tile (masked), independent of align_m.
BLOCK_E = 128
BLOCK_D = block_size # block size of activation-scale quantization
num_warps = 8
num_experts = num_recv_tokens_per_expert.shape[0]
@@ -238,7 +294,7 @@ def ep_scatter(
# grid = (triton.cdiv(hidden_size, BLOCK_D), num_experts)
grid = num_experts
assert m_indices.shape[0] % BLOCK_E == 0
assert m_indices.shape[0] % align_m == 0
assert expert_start_loc.shape[0] == num_experts
# pack_ue8m0: scatter packs 4 UE8M0 bytes per int32; else copies scales as-is.
@@ -253,6 +309,7 @@ def ep_scatter(
num_warps=num_warps,
BLOCK_E=BLOCK_E,
BLOCK_EXPERT_NUM=triton.next_power_of_2(num_experts),
ALIGN_M=align_m,
)
grid = min(recv_topk.shape[0], 1024 * 8)
@@ -418,7 +475,7 @@ def deepgemm_moe_permute(
if block_size is not None:
block_k = block_size
M_sum = compute_aligned_M(
M_sum, align_used = compute_aligned_M_and_alignment(
M=topk_ids.size(0),
num_topk=topk_ids.size(1),
local_num_experts=local_num_experts,
@@ -482,11 +539,12 @@ def deepgemm_moe_permute(
output_tensor_scale=aq_scale_out,
m_indices=expert_ids,
output_index=inv_perm,
align_m=align_used,
block_size=block_k,
pack_ue8m0=pack_ue8m0,
)
return aq_out, aq_scale_out, expert_ids, inv_perm
return aq_out, aq_scale_out, expert_ids, inv_perm, align_used
def deepgemm_unpermute_and_reduce(
@@ -318,11 +318,12 @@ class BatchedDeepGemmExperts(mk.FusedMoEExpertsModular):
def supports_packed_ue8m0_act_scales(self) -> bool:
"""
DeepGemm supports packed ue8m0 activation scales format in devices == sm100
DeepGemm supports packed ue8m0 activation scales on Blackwell-family
GPUs (SM100 datacenter and SM120 consumer).
"""
return (
is_deep_gemm_e8m0_used()
and current_platform.is_device_capability_family(100)
return is_deep_gemm_e8m0_used() and (
current_platform.is_device_capability_family(100)
or current_platform.is_device_capability_family(120)
)
def finalize_weight_and_reduce_impl(self) -> mk.TopKWeightAndReduce:
@@ -12,7 +12,7 @@ from vllm.model_executor.layers.fused_moe.config import (
FusedMoEQuantConfig,
)
from vllm.model_executor.layers.fused_moe.deep_gemm_utils import (
compute_aligned_M,
compute_aligned_M_and_alignment,
deepgemm_moe_permute,
deepgemm_unpermute_and_reduce,
)
@@ -43,6 +43,7 @@ from vllm.utils.deep_gemm import (
is_deep_gemm_supported,
m_grouped_fp8_fp4_gemm_nt_contiguous,
m_grouped_fp8_gemm_nt_contiguous,
mk_alignment_scope,
)
from vllm.utils.import_utils import has_deep_gemm
@@ -210,10 +211,10 @@ class DeepGemmExperts(mk.FusedMoEExpertsModular):
# Use the contiguous-layout M alignment (matches apply()); block_shape[0]
# is the quant block (1 for MXFP8) and would under-size the workspace.
block_m = get_mk_alignment_for_contiguous_layout()[0]
M_sum = compute_aligned_M(
M_sum, align_used = compute_aligned_M_and_alignment(
M, topk, local_num_experts, block_m, expert_tokens_meta
)
assert M_sum % block_m == 0
assert M_sum % align_used == 0
activation_out_dim = self.adjust_N_for_activation(N, activation)
workspace1 = (M_sum, max(activation_out_dim, K))
@@ -316,7 +317,7 @@ class DeepGemmExperts(mk.FusedMoEExpertsModular):
assert w2.size(1) == K
M_sum = compute_aligned_M(
M_sum, _ = compute_aligned_M_and_alignment(
M=topk_ids.size(0),
num_topk=topk_ids.size(1),
local_num_experts=local_num_experts,
@@ -327,7 +328,7 @@ class DeepGemmExperts(mk.FusedMoEExpertsModular):
a1q_perm = _resize_cache(
workspace13.view(dtype=torch.float8_e4m3fn), (M_sum, K)
)
a1q, a1q_scale, expert_ids, inv_perm = deepgemm_moe_permute(
a1q, a1q_scale, expert_ids, inv_perm, align_used = deepgemm_moe_permute(
aq=a1q,
aq_scale=a1q_scale,
topk_ids=topk_ids,
@@ -349,23 +350,35 @@ class DeepGemmExperts(mk.FusedMoEExpertsModular):
else {}
)
mm1_out = _resize_cache(workspace2, (M_sum, N))
m_grouped_fp8_gemm_nt_contiguous(
(a1q, a1q_scale), (w1, self.w1_scale), mm1_out, expert_ids, **gemm_kwargs
)
# Cap DG's BLOCK_M heuristic at the workspace's per-expert alignment;
# otherwise the scheduler can pick the wrong expert id from m_indices
# under cudagraph replay.
with mk_alignment_scope(align_used):
mm1_out = _resize_cache(workspace2, (M_sum, N))
m_grouped_fp8_gemm_nt_contiguous(
(a1q, a1q_scale),
(w1, self.w1_scale),
mm1_out,
expert_ids,
**gemm_kwargs,
)
activation_out_dim = self.adjust_N_for_activation(N, activation)
quant_out = _resize_cache(
workspace13.view(dtype=torch.float8_e4m3fn), (M_sum, activation_out_dim)
)
a2q, a2q_scale = self._act_mul_quant(
input=mm1_out.view(-1, N), output=quant_out, activation=activation
)
activation_out_dim = self.adjust_N_for_activation(N, activation)
quant_out = _resize_cache(
workspace13.view(dtype=torch.float8_e4m3fn), (M_sum, activation_out_dim)
)
a2q, a2q_scale = self._act_mul_quant(
input=mm1_out.view(-1, N), output=quant_out, activation=activation
)
mm2_out = _resize_cache(workspace2, (M_sum, K))
m_grouped_fp8_gemm_nt_contiguous(
(a2q, a2q_scale), (w2, self.w2_scale), mm2_out, expert_ids, **gemm_kwargs
)
mm2_out = _resize_cache(workspace2, (M_sum, K))
m_grouped_fp8_gemm_nt_contiguous(
(a2q, a2q_scale),
(w2, self.w2_scale),
mm2_out,
expert_ids,
**gemm_kwargs,
)
if apply_router_weight_on_input:
topk_weights = torch.ones_like(topk_weights)
@@ -384,7 +397,8 @@ class DeepGemmFP4Experts(mk.FusedMoEExpertsModular):
"""DeepGemm-based fused MoE expert implementation for FP4 weights.
Uses m_grouped_fp8_fp4_gemm_nt_contiguous with FP8 activations and
MXFP4 (FP4 E2M1 packed as uint8) weights. Requires SM100+ (Blackwell).
MXFP4 (FP4 E2M1 packed as uint8) weights. Requires Blackwell-family
GPUs (SM100 datacenter or SM120 consumer).
"""
# FP8 activation block size (hardcoded since mxfp4_w4a8 quant config
@@ -409,9 +423,9 @@ class DeepGemmFP4Experts(mk.FusedMoEExpertsModular):
def _supports_current_device() -> bool:
from vllm.platforms import current_platform
return (
is_deep_gemm_supported()
and current_platform.is_device_capability_family(100)
return is_deep_gemm_supported() and (
current_platform.is_device_capability_family(100)
or current_platform.is_device_capability_family(120)
)
@staticmethod
@@ -454,10 +468,10 @@ class DeepGemmFP4Experts(mk.FusedMoEExpertsModular):
activation: MoEActivation,
) -> tuple[tuple[int, ...], tuple[int, ...], tuple[int, ...]]:
block_m = get_mk_alignment_for_contiguous_layout()[0]
M_sum = compute_aligned_M(
M_sum, align_used = compute_aligned_M_and_alignment(
M, topk, local_num_experts, block_m, expert_tokens_meta
)
assert M_sum % block_m == 0
assert M_sum % align_used == 0
activation_out_dim = self.adjust_N_for_activation(N, activation)
workspace1 = (M_sum, max(activation_out_dim, K))
@@ -533,7 +547,7 @@ class DeepGemmFP4Experts(mk.FusedMoEExpertsModular):
if global_num_experts == -1:
global_num_experts = local_num_experts
M_sum = compute_aligned_M(
M_sum, _ = compute_aligned_M_and_alignment(
M=topk_ids.size(0),
num_topk=topk_ids.size(1),
local_num_experts=local_num_experts,
@@ -544,7 +558,7 @@ class DeepGemmFP4Experts(mk.FusedMoEExpertsModular):
a1q_perm = _resize_cache(
workspace13.view(dtype=torch.float8_e4m3fn), (M_sum, K)
)
a1q, a1q_scale, expert_ids, inv_perm = deepgemm_moe_permute(
a1q, a1q_scale, expert_ids, inv_perm, align_used = deepgemm_moe_permute(
aq=a1q,
aq_scale=a1q_scale,
topk_ids=topk_ids,
@@ -555,37 +569,40 @@ class DeepGemmFP4Experts(mk.FusedMoEExpertsModular):
)
assert a1q.size(0) == M_sum
# FC1: FP8 activations x FP4 weights
# DeepGEMM 2.4.2 requires FP4-packed weights as int8 (kPackedFP4).
mm1_out = _resize_cache(workspace2, (M_sum, N))
m_grouped_fp8_fp4_gemm_nt_contiguous(
(a1q, a1q_scale),
(w1.view(torch.int8), self.w1_scale),
mm1_out,
expert_ids,
recipe_a=(1, self._ACT_BLOCK_K),
recipe_b=(1, self._WEIGHT_BLOCK_K),
)
# Cap DG's BLOCK_M heuristic at the workspace's per-expert alignment;
# see DeepGemmExperts.apply for rationale.
with mk_alignment_scope(align_used):
# FC1: FP8 activations x FP4 weights
# DeepGEMM 2.4.2 requires FP4-packed weights as int8 (kPackedFP4).
mm1_out = _resize_cache(workspace2, (M_sum, N))
m_grouped_fp8_fp4_gemm_nt_contiguous(
(a1q, a1q_scale),
(w1.view(torch.int8), self.w1_scale),
mm1_out,
expert_ids,
recipe_a=(1, self._ACT_BLOCK_K),
recipe_b=(1, self._WEIGHT_BLOCK_K),
)
# SwiGLU activation + FP8 requant
activation_out_dim = self.adjust_N_for_activation(N, activation)
quant_out = _resize_cache(
workspace13.view(dtype=torch.float8_e4m3fn), (M_sum, activation_out_dim)
)
a2q, a2q_scale = self._act_mul_quant(
input=mm1_out.view(-1, N), output=quant_out, activation=activation
)
# SwiGLU activation + FP8 requant
activation_out_dim = self.adjust_N_for_activation(N, activation)
quant_out = _resize_cache(
workspace13.view(dtype=torch.float8_e4m3fn), (M_sum, activation_out_dim)
)
a2q, a2q_scale = self._act_mul_quant(
input=mm1_out.view(-1, N), output=quant_out, activation=activation
)
# FC2: FP8 activations x FP4 weights
mm2_out = _resize_cache(workspace2, (M_sum, K))
m_grouped_fp8_fp4_gemm_nt_contiguous(
(a2q, a2q_scale),
(w2.view(torch.int8), self.w2_scale),
mm2_out,
expert_ids,
recipe_a=(1, self._ACT_BLOCK_K),
recipe_b=(1, self._WEIGHT_BLOCK_K),
)
# FC2: FP8 activations x FP4 weights
mm2_out = _resize_cache(workspace2, (M_sum, K))
m_grouped_fp8_fp4_gemm_nt_contiguous(
(a2q, a2q_scale),
(w2.view(torch.int8), self.w2_scale),
mm2_out,
expert_ids,
recipe_a=(1, self._ACT_BLOCK_K),
recipe_b=(1, self._WEIGHT_BLOCK_K),
)
if apply_router_weight_on_input:
topk_weights = torch.ones_like(topk_weights)
@@ -57,6 +57,40 @@ if has_triton_kernels():
)
def _pack_deepgemm_mxfp4_scales(
w13_weight: torch.Tensor,
w2_weight: torch.Tensor,
w13_weight_scale: torch.Tensor,
w2_weight_scale: torch.Tensor,
) -> tuple[torch.Tensor, torch.Tensor]:
from vllm.model_executor.layers.quantization.utils.fp8_utils import (
deepgemm_post_process_weight_scale_block,
)
num_experts = w13_weight.shape[0]
intermediate_size_2 = w13_weight.shape[1] # = intermediate*2
hidden_size = w13_weight.shape[2] * 2 # weight is FP4-packed
intermediate_size = w2_weight.shape[2] * 2 # weight is FP4-packed
block_shape = (1, 32) # MXFP4 block (per-row, K=32)
return (
deepgemm_post_process_weight_scale_block(
ws=w13_weight_scale.data,
mn=intermediate_size_2,
k=hidden_size,
quant_block_shape=block_shape,
num_groups=num_experts,
),
deepgemm_post_process_weight_scale_block(
ws=w2_weight_scale.data,
mn=hidden_size,
k=intermediate_size,
quant_block_shape=block_shape,
num_groups=num_experts,
),
)
class Mxfp4MoeBackend(Enum):
NONE = "None"
# DeepGEMM FP8xFP4 backend (SM100+)
@@ -652,15 +686,18 @@ def convert_gpt_oss_weight_to_mxfp4_moe_kernel_format(
"""Convert loaded weights into backend-specific kernel format."""
if mxfp4_backend == Mxfp4MoeBackend.DEEPGEMM_MXFP4:
from vllm.model_executor.layers.quantization.utils.fp8_utils import (
_upcast_e8m0_to_fp32,
w13_weight_scale, w2_weight_scale = _pack_deepgemm_mxfp4_scales(
w13_weight,
w2_weight,
w13_weight_scale,
w2_weight_scale,
)
return (
w13_weight.data,
w2_weight.data,
_upcast_e8m0_to_fp32(w13_weight_scale.data),
_upcast_e8m0_to_fp32(w2_weight_scale.data),
w13_weight_scale,
w2_weight_scale,
w13_bias,
w2_bias,
)
@@ -1195,17 +1232,18 @@ def convert_weight_to_mxfp4_moe_kernel_format(
"""
if mxfp4_backend == Mxfp4MoeBackend.DEEPGEMM_MXFP4:
from vllm.model_executor.layers.quantization.utils.fp8_utils import (
_upcast_e8m0_to_fp32,
w13_weight_scale, w2_weight_scale = _pack_deepgemm_mxfp4_scales(
w13_weight,
w2_weight,
w13_weight_scale,
w2_weight_scale,
)
# Weights stay as uint8 packed FP4 — no layout change needed.
# Convert E8M0 uint8 scales to float32.
return (
w13_weight.data,
w2_weight.data,
_upcast_e8m0_to_fp32(w13_weight_scale.data),
_upcast_e8m0_to_fp32(w2_weight_scale.data),
w13_weight_scale,
w2_weight_scale,
w13_bias,
w2_bias,
)
@@ -275,6 +275,12 @@ class RoutedExperts(PluggableLayer):
# Weight Loading Methods
#
@staticmethod
def _to_scalar(loaded_weight: torch.Tensor) -> torch.Tensor:
# Per-tensor scales arrive 0-D or as shape-(1,) (llm-compressor NVFP4);
# reduce to a 0-D scalar. numel > 1 raises instead of broadcasting.
return loaded_weight.reshape(())
def _load_per_tensor_weight_scale(
self,
shard_id: str,
@@ -288,10 +294,10 @@ class RoutedExperts(PluggableLayer):
# We have to keep the weight scales of w1 and w3 because
# we need to re-quantize w1/w3 weights after weight loading.
idx = 0 if shard_id == "w1" else 1
param_data[expert_id][idx] = loaded_weight
param_data[expert_id][idx] = self._to_scalar(loaded_weight)
# If we are in the row parallel case (down_proj)
elif shard_id == "w2":
param_data[expert_id] = loaded_weight
param_data[expert_id] = self._to_scalar(loaded_weight)
def _load_combined_w13_weight_scale(
self,
@@ -525,7 +531,7 @@ class RoutedExperts(PluggableLayer):
param_data = param.data
# Input scales can be loaded directly and should be equal.
param_data[expert_id] = loaded_weight
param_data[expert_id] = self._to_scalar(loaded_weight)
def _load_g_idx(
self,
@@ -692,7 +698,9 @@ class RoutedExperts(PluggableLayer):
):
scale_expert_id = global_expert_id if use_global_sf else expert_id
scale_shard_id = 0 if shard_id == "w1" else 1
param.data[scale_expert_id][scale_shard_id] = loaded_weight.reshape(())
param.data[scale_expert_id][scale_shard_id] = self._to_scalar(
loaded_weight
)
return True if return_success else None
if (
@@ -852,7 +852,7 @@ class HummingMoEMethod(FusedMoEMethodBase):
# use moe modular
experts: HummingIndexedExperts | HummingGroupedExperts
layer.ensure_moe_quant_config_init()
layer._ensure_moe_quant_config_init()
assert self.moe_quant_config is not None
if get_humming_moe_gemm_type() == "indexed":
experts = HummingIndexedExperts(layer, self.moe, self.moe_quant_config)
@@ -1058,6 +1058,34 @@ def _upcast_e8m0_to_fp32(scale: torch.Tensor) -> torch.Tensor:
return fp32_bits.view(torch.float32)
def deepgemm_post_process_weight_scale_block(
ws: torch.Tensor,
mn: int,
k: int,
quant_block_shape: tuple[int, ...],
num_groups: int,
is_sfa: bool = False,
) -> torch.Tensor:
if ws.dtype in (torch.float8_e8m0fnu, torch.uint8):
# Scales already in E8M0 from checkpoint; upcast to fp32 and let
# DeepGEMM pack the layout expected by the target architecture.
ws = _upcast_e8m0_to_fp32(ws)
else:
assert ws.dtype == torch.float32, (
f"Expected tensor scales dtype to be torch.float32 or "
f"torch.float8_e8m0fnu or torch.uint8, got {ws.dtype} instead"
)
return transform_sf_into_required_layout(
sf=ws,
mn=mn,
k=k,
recipe=(1, quant_block_shape[0], quant_block_shape[1]),
num_groups=num_groups,
is_sfa=is_sfa,
)
def deepgemm_post_process_fp8_weight_block(
wq: torch.Tensor,
ws: torch.Tensor,
@@ -1073,13 +1101,13 @@ def deepgemm_post_process_fp8_weight_block(
if ws.dtype in (torch.float8_e8m0fnu, torch.uint8):
# Scales already in E8M0 from checkpoint (float8_e8m0fnu, or raw E8M0
# bits as uint8 for MXFP8) upcast to fp32 and skip requantization
# bits as uint8 for MXFP8) - upcast to fp32 and skip requantization
# (weights already have power-of-two scales).
ws = _upcast_e8m0_to_fp32(ws)
else:
assert ws.dtype == torch.float32, (
f"Expected tensor scales dtype to be torch.float32 or "
f"torch.float8_e8m0fnu, got {ws.dtype} instead"
f"torch.float8_e8m0fnu or torch.uint8, got {ws.dtype} instead"
)
if use_e8m0:
requant_weight_ue8m0_inplace(wq, ws, block_size=quant_block_shape)
@@ -1094,16 +1122,12 @@ def deepgemm_post_process_fp8_weight_block(
r = wq.size(0) // g
wq = wq.view(g, r, d)
ws = ws.view(g, r // quant_block_shape[0], d // quant_block_shape[1])
# Pre-transform scale with recipe=(1, 128, 128) to broadcast + pack
# into TMA-aligned UE8M0 (INT32) layout. At runtime fp8_einsum uses
# recipe=(1, 1, 128) which sees INT dtype and skips re-transform.
dg_ws = transform_sf_into_required_layout(
sf=ws,
dg_ws = deepgemm_post_process_weight_scale_block(
ws=ws,
mn=r,
k=d,
recipe=(1, quant_block_shape[0], quant_block_shape[1]),
quant_block_shape=quant_block_shape,
num_groups=g,
is_sfa=False,
)
return wq, dg_ws
@@ -1113,22 +1137,12 @@ def deepgemm_post_process_fp8_weight_block(
wq = wq.unsqueeze(0)
ws = ws.unsqueeze(0)
# From https://github.com/deepseek-ai/DeepGEMM/blob/c9f8b34dcdacc20aa746b786f983492c51072870/csrc/utils/layout.hpp#L46
# (1, block_n, block_k): (1, 128, 128) for FP8 block, (1, 1, 32) for MXFP8.
recipe = (1, quant_block_shape[0], quant_block_shape[1])
# Ref : https://github.com/deepseek-ai/DeepGEMM/blob/c9f8b34dcdacc20aa746b786f983492c51072870/csrc/apis/gemm.hpp
# DeepGemm uses the `transform_sf_into_required_layout` function to
# represent scales in the correct format.
dg_ws = transform_sf_into_required_layout(
sf=ws,
dg_ws = deepgemm_post_process_weight_scale_block(
ws=ws,
mn=wq.size(1),
k=wq.size(2),
recipe=recipe,
quant_block_shape=quant_block_shape,
num_groups=wq.size(0),
# is the scale factors for A in (Refers to the argument A in A @ B).
# Weights are B.
is_sfa=False,
)
if original_ndim == 2:
+58 -25
View File
@@ -12,7 +12,9 @@ from tqdm import tqdm
import vllm.envs as envs
from vllm.distributed.parallel_state import get_dp_group, is_global_first_rank
from vllm.model_executor.layers.fused_moe import MoERunner
from vllm.model_executor.layers.fused_moe.deep_gemm_utils import compute_aligned_M
from vllm.model_executor.layers.fused_moe.deep_gemm_utils import (
compute_aligned_M_and_alignment,
)
from vllm.model_executor.layers.fused_moe.experts.deep_gemm_moe import DeepGemmExperts
from vllm.model_executor.layers.fused_moe.experts.triton_deep_gemm_moe import (
TritonOrDeepGemmExperts,
@@ -25,6 +27,7 @@ from vllm.utils.deep_gemm import (
fp8_gemm_nt,
get_mk_alignment_for_contiguous_layout,
m_grouped_fp8_gemm_nt_contiguous,
mk_alignment_scope,
)
from vllm.utils.math_utils import cdiv
from vllm.utils.platform_utils import num_compute_units
@@ -238,7 +241,7 @@ def _get_grouped_gemm_params(
w2: torch.Tensor,
num_topk: int,
max_tokens: int,
) -> tuple[int, int, torch.Tensor]:
) -> tuple[int, int, list[tuple[int, int, torch.Tensor]]]:
assert w1.size(0) == w2.size(0), "w1 and w2 must have the same number of experts"
block_m = get_mk_alignment_for_contiguous_layout()[0]
@@ -248,19 +251,46 @@ def _get_grouped_gemm_params(
# Assumes all ranks have the same max_num_batched_tokens
max_tokens = get_dp_group().world_size * max_tokens
# This is the maximum GroupedGemm M size that we expect to run
# the grouped_gemm with.
MAX_M = compute_aligned_M(
max_tokens, num_topk, num_experts, block_m, expert_tokens_meta=None
request_m_values = _generate_optimal_warmup_m_values(
max_tokens,
max(w1.size(1), w2.size(1)),
device,
)
# Distribute expert-ids evenly.
MAX_BLOCKS = MAX_M // block_m
expert_ids_block = torch.randint(
low=0, high=num_experts, size=(MAX_BLOCKS,), device=device, dtype=torch.int32
)
expert_ids = torch.repeat_interleave(expert_ids_block, block_m, dim=0)
request_m_values = sorted({m for m in (*request_m_values, max_tokens) if m > 0})
if not request_m_values:
return 0, block_m, []
return MAX_M, block_m, expert_ids
cases_by_shape: dict[tuple[int, int], torch.Tensor] = {}
for request_m in request_m_values:
M_sum, align_used = compute_aligned_M_and_alignment(
M=request_m,
num_topk=num_topk,
local_num_experts=num_experts,
alignment=block_m,
expert_tokens_meta=None,
)
if (M_sum, align_used) in cases_by_shape:
continue
num_blocks = M_sum // align_used
expert_ids_block = torch.randint(
low=0,
high=num_experts,
size=(num_blocks,),
device=device,
dtype=torch.int32,
)
cases_by_shape[(M_sum, align_used)] = torch.repeat_interleave(
expert_ids_block, align_used, dim=0
)
max_m = max(M_sum for M_sum, _ in cases_by_shape)
warmup_cases = [
(M_sum, align_used, expert_ids)
for (M_sum, align_used), expert_ids in sorted(cases_by_shape.items())
]
return max_m, block_m, warmup_cases
def _deepgemm_grouped_fp8_gemm_nt_contiguous_warmup(
@@ -278,7 +308,11 @@ def _deepgemm_grouped_fp8_gemm_nt_contiguous_warmup(
):
return
MAX_M, block_m, expert_ids = _get_grouped_gemm_params(w1, w2, num_topk, max_tokens)
MAX_M, block_m, warmup_cases = _get_grouped_gemm_params(
w1, w2, num_topk, max_tokens
)
if not warmup_cases:
return
device = w1.device
def _warmup(w: torch.Tensor, w_scale: torch.Tensor):
@@ -289,15 +323,14 @@ def _deepgemm_grouped_fp8_gemm_nt_contiguous_warmup(
)
out = torch.empty((MAX_M, n), device=device, dtype=torch.bfloat16)
m_values = list(range(block_m, MAX_M + 1, block_m))
for num_tokens in m_values:
m_grouped_fp8_gemm_nt_contiguous(
(a1q[:num_tokens], a1q_scales[:num_tokens]),
(w, w_scale),
out[:num_tokens],
expert_ids[:num_tokens],
)
for num_tokens, align_used, expert_ids in warmup_cases:
with mk_alignment_scope(align_used):
m_grouped_fp8_gemm_nt_contiguous(
(a1q[:num_tokens], a1q_scales[:num_tokens]),
(w, w_scale),
out[:num_tokens],
expert_ids,
)
if pbar is not None:
pbar.update(1)
@@ -350,8 +383,8 @@ def _count_warmup_iterations(model: torch.nn.Module, max_tokens: int) -> int:
w13, _, w2, _, num_topk = _extract_data_from_fused_moe_module(m)
if w13.size() in seen_grouped_sizes and w2.size() in seen_grouped_sizes:
continue
MAX_M, block_m, _ = _get_grouped_gemm_params(w13, w2, num_topk, max_tokens)
n_values = (MAX_M - block_m) // block_m + 1
_, _, warmup_cases = _get_grouped_gemm_params(w13, w2, num_topk, max_tokens)
n_values = len(warmup_cases)
if w13.size() not in seen_grouped_sizes:
total += n_values
seen_grouped_sizes.add(w13.size())
@@ -0,0 +1,226 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Warm up DeepSeek V4 mHC TileLang kernels before serving requests.
Ported from lucifer1004/vllm-jasl with the two env-var knobs removed
(`VLLM_ENABLE_DEEPSEEK_V4_MHC_WARMUP`, `VLLM_DEEPSEEK_V4_MHC_WARMUP_TOKEN_SIZES`).
Gating is intrinsic: non-DSv4 models and layers without hc_* attributes
return early, so the warmup is a no-op except where it's needed.
"""
import time
from collections.abc import Iterable
import torch
from vllm.logger import init_logger
from vllm.tracing import instrument
from vllm.utils.math_utils import cdiv
logger = init_logger(__name__)
_AUTO_WARMUP_MAX_TOKENS = 16_384
_DEFAULT_TOKEN_SIZE_CANDIDATES = (
1,
2,
4,
8,
16,
32,
64,
128,
256,
512,
1024,
2048,
4096,
8192,
16_384,
)
def _compute_mhc_pre_num_split(
*,
num_tokens: int,
hidden_size: int,
hc_mult: int,
num_sms: int,
) -> int:
block_k = 64
block_m = 64
k = hc_mult * hidden_size
grid_size = cdiv(num_tokens, block_m)
split_k = num_sms // grid_size
num_block_k = cdiv(k, block_k)
split_k = min(split_k, num_block_k // 4)
return max(split_k, 1)
def _normalize_token_sizes(
token_sizes: Iterable[int],
*,
max_tokens: int,
) -> list[int]:
return sorted({size for size in token_sizes if 1 <= size <= max_tokens})
def _select_mhc_warmup_token_sizes(
*,
max_tokens: int,
cudagraph_capture_sizes: list[int],
) -> list[int]:
if max_tokens <= 0:
return []
max_auto_tokens = min(max_tokens, _AUTO_WARMUP_MAX_TOKENS)
candidates = list(_DEFAULT_TOKEN_SIZE_CANDIDATES)
candidates.extend(cudagraph_capture_sizes)
candidates.append(max_auto_tokens)
return _normalize_token_sizes(candidates, max_tokens=max_auto_tokens)
def _find_first_mhc_layer(model: torch.nn.Module) -> torch.nn.Module | None:
for module in model.modules():
if module.__class__.__name__ != "DeepseekV4DecoderLayer":
continue
if all(
hasattr(module, attr)
for attr in (
"hc_pre",
"hc_post",
"hc_attn_fn",
"hc_attn_scale",
"hc_attn_base",
"hc_ffn_fn",
"hc_ffn_scale",
"hc_ffn_base",
)
):
return module
return None
def _find_deepseek_v4_model(model: torch.nn.Module) -> torch.nn.Module | None:
for module in model.modules():
if module.__class__.__name__ != "DeepseekV4Model":
continue
if all(
hasattr(module, attr)
for attr in ("hc_head_fn", "hc_head_scale", "hc_head_base")
):
return module
return None
def _warmup_layer_mhc(
layer: torch.nn.Module,
token_sizes: list[int],
) -> None:
max_tokens = max(token_sizes)
hidden_size = int(layer.hidden_size)
hc_mult = int(layer.hc_mult)
device = layer.hc_attn_fn.device
residual = torch.zeros(
max_tokens,
hc_mult,
hidden_size,
dtype=torch.bfloat16,
device=device,
)
for size in token_sizes:
residual_slice = residual[:size]
for fn, scale, base in (
(layer.hc_attn_fn, layer.hc_attn_scale, layer.hc_attn_base),
(layer.hc_ffn_fn, layer.hc_ffn_scale, layer.hc_ffn_base),
):
layer_input, post_mix, comb_mix = layer.hc_pre(
residual_slice,
fn,
scale,
base,
)
layer.hc_post(layer_input, residual_slice, post_mix, comb_mix)
def _warmup_hc_head(
model: torch.nn.Module,
token_sizes: list[int],
) -> None:
# Upstream a8887c208 ("[DSV4] aiter mhc support (ROCm)") refactored
# ``hc_head`` from a free function into the ``HCHeadOp`` CustomOp
# instance attached to the model as ``hc_head_op``. We call through
# that instance so the warmup exercises the same dispatched
# implementation as the inference path.
hc_head_op = getattr(model, "hc_head_op", None)
if hc_head_op is None:
return
max_tokens = max(token_sizes)
hidden_size = int(model.config.hidden_size)
hc_mult = int(model.hc_mult)
device = model.hc_head_fn.device
hidden_states = torch.zeros(
max_tokens,
hc_mult,
hidden_size,
dtype=torch.bfloat16,
device=device,
)
for size in token_sizes:
hc_head_op(
hidden_states[:size],
model.hc_head_fn,
model.hc_head_scale,
model.hc_head_base,
model.rms_norm_eps,
model.hc_eps,
)
@instrument(span_name="DeepSeek V4 mHC warmup")
def deepseek_v4_mhc_warmup(
model: torch.nn.Module,
*,
max_tokens: int,
cudagraph_capture_sizes: list[int] | None = None,
) -> None:
# Cheap model-type gate before walking ``model.modules()``. The class
# walk below is O(num_layers) and shows up in startup time on very
# large checkpoints; bail out for any model that is not DeepSeek V4.
config = getattr(model, "config", None)
model_type = getattr(config, "model_type", None) if config is not None else None
if model_type is not None and model_type != "deepseek_v4":
return
layer = _find_first_mhc_layer(model)
if layer is None:
return
device = layer.hc_attn_fn.device
if device.type != "cuda":
return
deepseek_model = _find_deepseek_v4_model(model)
token_sizes = _select_mhc_warmup_token_sizes(
max_tokens=max_tokens,
cudagraph_capture_sizes=cudagraph_capture_sizes or [],
)
if not token_sizes:
return
started = time.perf_counter()
logger.info(
"Warming up DeepSeek V4 mHC TileLang kernels for token sizes: %s",
token_sizes,
)
with torch.inference_mode():
_warmup_layer_mhc(layer, token_sizes)
if deepseek_model is not None:
_warmup_hc_head(deepseek_model, token_sizes)
torch.accelerator.synchronize()
logger.info(
"DeepSeek V4 mHC TileLang warmup finished in %.2f seconds.",
time.perf_counter() - started,
)
@@ -0,0 +1,56 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""FlashInfer autotune cache helpers."""
import hashlib
import os
import tempfile
from contextlib import suppress
from pathlib import Path
from typing import TYPE_CHECKING
import vllm.envs as envs
from vllm.compilation.caching import aot_compile_hash_factors
if TYPE_CHECKING:
from vllm.v1.worker.gpu_model_runner import GPUModelRunner
def flashinfer_autotune_cache_hash(runner: "GPUModelRunner") -> str:
factors = aot_compile_hash_factors(runner.vllm_config)
return hashlib.sha256(str(factors).encode()).hexdigest()
def resolve_flashinfer_autotune_file(runner: "GPUModelRunner") -> Path:
override_dir = envs.VLLM_FLASHINFER_AUTOTUNE_CACHE_DIR
if override_dir:
root = Path(override_dir).expanduser()
else:
from flashinfer.jit import env as flashinfer_jit_env
flashinfer_workspace = flashinfer_jit_env.FLASHINFER_WORKSPACE_DIR
root = (
Path(envs.VLLM_CACHE_ROOT)
/ "flashinfer_autotune_cache"
/ flashinfer_workspace.parent.name
/ flashinfer_workspace.name
)
output_dir = root / flashinfer_autotune_cache_hash(runner)
output_dir.mkdir(parents=True, exist_ok=True)
return output_dir / "autotune_configs.json"
def write_flashinfer_autotune_cache(cache_path: Path, contents: bytes) -> None:
cache_path.parent.mkdir(parents=True, exist_ok=True)
fd, tmp_path = tempfile.mkstemp(
dir=cache_path.parent, suffix=".tmp", prefix=f".{cache_path.name}."
)
try:
with os.fdopen(fd, "wb") as f:
f.write(contents)
os.replace(tmp_path, cache_path)
except BaseException:
with suppress(OSError):
os.unlink(tmp_path)
raise
@@ -0,0 +1,255 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Warmup and autotune helpers for FlashInfer sparse MLA backends."""
from typing import TYPE_CHECKING, cast
import torch
from vllm.logger import init_logger
from vllm.model_executor.warmup.flashinfer_autotune_cache import (
resolve_flashinfer_autotune_file,
write_flashinfer_autotune_cache,
)
from vllm.platforms import current_platform
from vllm.utils.flashinfer import autotune as flashinfer_autotune
from vllm.utils.flashinfer import has_flashinfer
from vllm.v1.worker.gpu.warmup import run_mixed_prefill_decode_warmup
if TYPE_CHECKING:
from vllm.v1.worker.gpu.model_runner import GPUModelRunner as V2GPUModelRunner
from vllm.v1.worker.gpu_model_runner import GPUModelRunner
from vllm.v1.worker.gpu_worker import Worker
logger = init_logger(__name__)
_DEEPSEEK_V4_SPARSE_MLA_BACKENDS = frozenset(
{
"FLASHMLA_SPARSE_DSV4",
"FLASHINFER_MLA_SPARSE_DSV4",
"ROCM_FLASHMLA_SPARSE_DSV4",
"DEEPSEEK_SPARSE_SWA",
}
)
_FLASHINFER_MLA_SPARSE_BACKENDS = frozenset({"FLASHINFER_MLA_SPARSE_SM120"})
_DEEPSEEK_V4_FLASHINFER_MLA_SPARSE_BACKENDS = frozenset({"FLASHINFER_MLA_SPARSE_DSV4"})
_FLASHINFER_SM120_SPARSE_MLA_DECODE_LABELS = {
"FLASHINFER_MLA_SPARSE_SM120": "DSv3.2",
"FLASHINFER_MLA_SPARSE_DSV4": "DSv4",
}
_SPARSE_MLA_MIXED_WARMUP_TOKENS = 16
def _attention_backend_name(backend: object) -> str | None:
get_name = getattr(backend, "get_name", None)
if get_name is None:
return None
try:
return get_name()
except NotImplementedError:
return None
def _has_deepseek_v4_sparse_mla_backend(runner: "GPUModelRunner") -> bool:
for groups in getattr(runner, "attn_groups", []) or ():
for group in groups:
name = _attention_backend_name(getattr(group, "backend", None))
if name in _DEEPSEEK_V4_SPARSE_MLA_BACKENDS:
return True
return False
def _flashinfer_sparse_mla_decode_label(
runner: "GPUModelRunner",
allowed_backends: frozenset[str],
) -> str | None:
for groups in getattr(runner, "attn_groups", []) or ():
for group in groups:
name = _attention_backend_name(getattr(group, "backend", None))
if name in allowed_backends:
return _FLASHINFER_SM120_SPARSE_MLA_DECODE_LABELS.get(name)
return None
def _clamp_warmup_tokens(num_tokens: int, max_tokens: int) -> int:
return max(0, min(num_tokens, max_tokens))
def _uses_v2_model_runner(runner: "GPUModelRunner") -> bool:
vllm_config = getattr(runner, "vllm_config", None)
return bool(getattr(vllm_config, "use_v2_model_runner", False))
def _run_flashinfer_sparse_mla_decode_autotune(
worker: "Worker",
num_tokens: int,
allowed_backends: frozenset[str],
) -> bool:
"""Autotune FlashInfer's SM120 sparse-MLA decode path."""
runner = worker.model_runner
log_label = _flashinfer_sparse_mla_decode_label(runner, allowed_backends)
if log_label is None:
return False
if worker.vllm_config.kernel_config.enable_flashinfer_autotune is not True:
return False
if not has_flashinfer() or not current_platform.is_device_capability_family(120):
return False
try:
from flashinfer.autotuner import AutoTuner
except ImportError:
logger.warning(
"Skipping FlashInfer SM120 sparse MLA decode autotune because "
"FlashInfer autotuner is unavailable."
)
return False
from vllm.distributed.parallel_state import get_world_group
world = get_world_group()
is_leader = world.rank_in_group == 0
cache_path = resolve_flashinfer_autotune_file(runner)
dummy_run_kwargs = dict(
num_tokens=num_tokens,
skip_eplb=True,
is_profile=True,
force_attention=True,
create_mixed_batch=True,
)
if is_leader:
logger.info(
"Autotuning FlashInfer SM120 sparse MLA %s decode with cache: %s",
log_label,
cache_path,
)
with torch.inference_mode():
warmup_executed = True
if is_leader:
if _uses_v2_model_runner(runner):
v2_runner = cast("V2GPUModelRunner", runner)
warmup_executed = run_mixed_prefill_decode_warmup(
v2_runner,
worker.execute_model,
worker.sample_tokens,
num_tokens,
mixed_step_context=flashinfer_autotune(True, cache=str(cache_path)),
req_id_prefix="_sparse_mla_v2_warmup",
)
else:
with flashinfer_autotune(True, cache=str(cache_path)):
runner._dummy_run(**dummy_run_kwargs)
else:
if _uses_v2_model_runner(runner):
v2_runner = cast("V2GPUModelRunner", runner)
warmup_executed = run_mixed_prefill_decode_warmup(
v2_runner,
worker.execute_model,
worker.sample_tokens,
num_tokens,
req_id_prefix="_sparse_mla_v2_warmup",
)
else:
runner._dummy_run(**dummy_run_kwargs)
if not warmup_executed:
return False
tune_results: bytes | None = None
if is_leader and cache_path.exists():
with open(cache_path, "rb") as f:
tune_results = f.read()
tune_results = world.broadcast_object(tune_results, src=0)
if tune_results is None:
logger.warning(
"No FlashInfer SM120 sparse MLA %s decode autotune cache entries found. "
"Falling back to FlashInfer's default tactic heuristic.",
log_label,
)
world.barrier()
return True
write_flashinfer_autotune_cache(cache_path, tune_results)
world.barrier()
AutoTuner.get().load_configs(str(cache_path))
logger.info(
"FlashInfer SM120 sparse MLA %s decode autotune cache loaded on rank %d "
"from %s.",
log_label,
world.rank_in_group,
cache_path,
)
return True
def _flashinfer_sparse_mla_decode_autotune(
worker: "Worker",
num_tokens: int,
) -> bool:
return _run_flashinfer_sparse_mla_decode_autotune(
worker, num_tokens, _FLASHINFER_MLA_SPARSE_BACKENDS
)
def _deepseek_v4_sparse_mla_decode_autotune(
worker: "Worker",
num_tokens: int,
) -> bool:
return _run_flashinfer_sparse_mla_decode_autotune(
worker, num_tokens, _DEEPSEEK_V4_FLASHINFER_MLA_SPARSE_BACKENDS
)
def flashinfer_sparse_mla_decode_autotune_warmup(worker: "Worker") -> None:
"""Autotune generic FlashInfer sparse MLA decode when selected."""
runner = worker.model_runner
if runner.is_pooling_model:
return
max_tokens = worker.scheduler_config.max_num_batched_tokens
mixed_tokens = _clamp_warmup_tokens(_SPARSE_MLA_MIXED_WARMUP_TOKENS, max_tokens)
if mixed_tokens <= 0:
return
_flashinfer_sparse_mla_decode_autotune(worker, mixed_tokens)
def deepseek_v4_sparse_mla_attention_warmup(worker: "Worker") -> None:
"""Warm DSv4 sparse-MLA mixed prefill+decode attention."""
runner = worker.model_runner
if runner.is_pooling_model or not _has_deepseek_v4_sparse_mla_backend(runner):
return
max_tokens = worker.scheduler_config.max_num_batched_tokens
mixed_tokens = _clamp_warmup_tokens(_SPARSE_MLA_MIXED_WARMUP_TOKENS, max_tokens)
if mixed_tokens <= 0:
return
logger.info(
"Warming up DeepSeek V4 sparse MLA attention for mixed tokens=%s.",
mixed_tokens,
)
mixed_warmup_done = _deepseek_v4_sparse_mla_decode_autotune(worker, mixed_tokens)
if not mixed_warmup_done:
if _uses_v2_model_runner(runner):
v2_runner = cast("V2GPUModelRunner", runner)
run_mixed_prefill_decode_warmup(
v2_runner,
worker.execute_model,
worker.sample_tokens,
mixed_tokens,
req_id_prefix="_sparse_mla_v2_warmup",
)
else:
runner._dummy_run(
num_tokens=mixed_tokens,
skip_eplb=True,
is_profile=True,
force_attention=True,
create_mixed_batch=True,
)
+28 -32
View File
@@ -6,16 +6,24 @@ This is useful specifically for JIT'ed kernels as we don't want JIT'ing to
happen during model execution.
"""
import hashlib
from pathlib import Path
from typing import TYPE_CHECKING
import torch
import vllm.envs as envs
from vllm.compilation.caching import aot_compile_hash_factors
from vllm.logger import init_logger
from vllm.model_executor.warmup.deep_gemm_warmup import deep_gemm_warmup
from vllm.model_executor.warmup.deepseek_v4_mhc_warmup import (
deepseek_v4_mhc_warmup,
)
from vllm.model_executor.warmup.flashinfer_autotune_cache import (
resolve_flashinfer_autotune_file,
write_flashinfer_autotune_cache,
)
from vllm.model_executor.warmup.flashinfer_sparse_mla_warmup import (
deepseek_v4_sparse_mla_attention_warmup,
flashinfer_sparse_mla_decode_autotune_warmup,
)
from vllm.platforms import current_platform
from vllm.utils.deep_gemm import is_deep_gemm_supported
from vllm.utils.flashinfer import has_flashinfer
@@ -27,36 +35,26 @@ if TYPE_CHECKING:
logger = init_logger(__name__)
def _flashinfer_autotune_cache_hash(runner: "GPUModelRunner") -> str:
factors = aot_compile_hash_factors(runner.vllm_config)
return hashlib.sha256(str(factors).encode()).hexdigest()
def _resolve_flashinfer_autotune_file(runner: "GPUModelRunner") -> Path:
override_dir = envs.VLLM_FLASHINFER_AUTOTUNE_CACHE_DIR
if override_dir:
root = Path(override_dir).expanduser()
else:
from flashinfer.jit import env as flashinfer_jit_env
flashinfer_workspace = flashinfer_jit_env.FLASHINFER_WORKSPACE_DIR
root = (
Path(envs.VLLM_CACHE_ROOT)
/ "flashinfer_autotune_cache"
/ flashinfer_workspace.parent.name
/ flashinfer_workspace.name
)
output_dir = root / _flashinfer_autotune_cache_hash(runner)
output_dir.mkdir(parents=True, exist_ok=True)
return output_dir / "autotune_configs.json"
def kernel_warmup(worker: "Worker"):
from vllm.model_executor.warmup.minimax_m3_msa_warmup import (
minimax_m3_msa_warmup,
)
# DSv4 mHC TileLang kernels (hc_pre/hc_post/hc_head_op) run every decoder
# layer per token; warm them across token sizes first so the first real
# request doesn't pay JIT cost. No-op for non-DSv4 models (gated inside).
deepseek_v4_mhc_warmup(
worker.get_model(),
max_tokens=worker.scheduler_config.max_num_batched_tokens,
cudagraph_capture_sizes=(
worker.vllm_config.compilation_config.cudagraph_capture_sizes or []
),
)
# Run next so input-prep kernels JIT against pristine runner state.
flashinfer_sparse_mla_decode_autotune_warmup(worker)
deepseek_v4_sparse_mla_attention_warmup(worker)
# Deep GEMM warmup
do_deep_gemm_warmup = (
envs.VLLM_USE_DEEP_GEMM
@@ -147,7 +145,7 @@ def flashinfer_autotune(runner: "GPUModelRunner") -> None:
world = get_world_group()
is_leader = world.rank_in_group == 0
cache_path = _resolve_flashinfer_autotune_file(runner)
cache_path = resolve_flashinfer_autotune_file(runner)
if is_leader:
logger.info("Using FlashInfer autotune cache file: %s", cache_path)
@@ -183,9 +181,7 @@ def flashinfer_autotune(runner: "GPUModelRunner") -> None:
"Falling back to default tactics."
)
else:
if not is_leader and world.local_rank == 0:
with open(cache_path, "wb") as f:
f.write(tune_results)
write_flashinfer_autotune_cache(cache_path, tune_results)
world.barrier()
from flashinfer.autotuner import AutoTuner
+34 -32
View File
@@ -62,23 +62,22 @@ logger = init_logger(__name__)
def _resolve_dsv4_kv_cache_dtype(
use_flashmla_fp8_layout: bool,
use_fp8_ds_mla_layout: bool,
kv_cache_dtype: str,
cache_config: CacheConfig | None,
) -> tuple[str, torch.dtype]:
"""Map ``(layout, --kv-cache-dtype)`` to ``(cache_dtype_str, torch_dtype)``.
Both layouts are paged; they differ in the per-token block format. The
FlashMLA fp8 layout (FlashMLA / ROCm Aiter) is the ``fp8_ds_mla`` format:
UE8M0 block-scaled fp8 packed as ``uint8`` (the canonical ``fp8_ds_mla``
string is written back onto ``cache_config`` so the page-size specs pick
the 576B per-token slot). Otherwise (FlashInfer) each token's KV row is
stored in its plain element dtype bf16 or per-tensor FP8 E4M3.
``fp8_ds_mla`` format is UE8M0 block-scaled fp8 packed as ``uint8`` (the
canonical ``fp8_ds_mla`` string is written back onto ``cache_config`` so the
page-size specs pick the 576B per-token slot). Plain-row backends store each
token's KV row in its element dtype: bf16 or per-tensor FP8 E4M3.
"""
if use_flashmla_fp8_layout:
if use_fp8_ds_mla_layout:
# fp8_ds_mla block format: UE8M0 block-scaled fp8 packed as uint8.
assert kv_cache_dtype.startswith("fp8"), (
f"DeepseekV4 FlashMLA fp8 layout only supports fp8 kv-cache, "
f"DeepseekV4 fp8_ds_mla layout only supports fp8 kv-cache, "
f"got {kv_cache_dtype}"
)
if kv_cache_dtype != "fp8_ds_mla":
@@ -100,18 +99,20 @@ class DeepseekV4Attention(nn.Module, AttentionLayerBase, ABC):
The platform-specific sparse-MLA forward (``forward_mqa`` /
``get_padded_num_q_heads`` / ``_o_proj`` / ``backend_cls``) is provided by a
subclass ``DeepseekV4FlashMLAAttention`` / ``DeepseekV4FlashInferMLAAttention``
(CUDA) or ``DeepseekV4ROCMAiterMLAAttention`` (ROCm) selected by the
platform-specific deepseek_v4 model module. The base is never instantiated
directly.
subclass ``DeepseekV4FlashMLAAttention`` /
``DeepseekV4FlashInferSM120Attention`` /
``DeepseekV4FlashInferMLAAttention`` (CUDA) or
``DeepseekV4ROCMAiterMLAAttention`` (ROCm) selected by the platform-specific
deepseek_v4 model module. The base is never instantiated directly.
"""
# Provided by the platform subclass.
backend_cls: ClassVar[type[AttentionBackend]]
# KV-cache per-token block format (both layouts are paged). True (default)
# = FlashMLA / ROCm fp8_ds_mla (UE8M0 block-scaled fp8 packed as uint8);
# False = FlashInfer plain bf16 / per-tensor fp8 KV row.
use_flashmla_fp8_layout: ClassVar[bool] = True
# = fp8_ds_mla (UE8M0 block-scaled fp8 packed as uint8); False = plain
# bf16 / per-tensor fp8 KV row. Backends can override the instance hook when
# a single attention class dispatches across arch-specific layouts.
use_fp8_ds_mla_layout: ClassVar[bool] = True
# Prefill is processed in fixed-size chunks; this bounds the bf16 kv-gather
# workspace allocated in _forward_prefill and is also read by the dummy-run
# path to pre-reserve that workspace.
@@ -145,6 +146,10 @@ class DeepseekV4Attention(nn.Module, AttentionLayerBase, ABC):
"""Inverse-RoPE + wo_a + wo_b output projection (platform-specific)."""
raise NotImplementedError
def _uses_fp8_ds_mla_layout(self) -> bool:
"""Return whether this instance stores fp8 KV in fp8_ds_mla layout."""
return self.use_fp8_ds_mla_layout
def __init__(
self,
vllm_config: VllmConfig,
@@ -276,13 +281,10 @@ class DeepseekV4Attention(nn.Module, AttentionLayerBase, ABC):
)
self.max_model_len = vllm_config.model_config.max_model_len
# Resolve the kv-cache dtype from this backend's block format (a
# ClassVar set by the subclass): fp8_ds_mla (UE8M0 block-scaled fp8 as
# uint8) for FlashMLA / ROCm, vs a plain bf16 / per-tensor fp8 row for
# FlashInfer. The same resolution drives the SWA cache tensor dtype
# below.
# Resolve the kv-cache dtype from this backend's block format. The same
# resolution drives the SWA cache tensor dtype below.
self.kv_cache_dtype, self.kv_cache_torch_dtype = _resolve_dsv4_kv_cache_dtype(
self.use_flashmla_fp8_layout, cache_config.cache_dtype, cache_config
self._uses_fp8_ds_mla_layout(), cache_config.cache_dtype, cache_config
)
self.swa_cache_layer = DeepseekV4SWACache(
@@ -539,7 +541,7 @@ class DeepseekV4Attention(nn.Module, AttentionLayerBase, ABC):
# kv is unchanged; attention reads kv solely via swa_kv_cache.
if cache_dtype == torch.uint8:
# Legacy FlashMLA UE8M0 paged path. Horizontally fused:
# fp8_ds_mla UE8M0 paged path. Horizontally fused:
# Q side: per-head RMSNorm (no weight) + GPT-J RoPE, zero-filling
# the padding head slots; the kernel allocates and returns
# the padded q tensor.
@@ -557,10 +559,10 @@ class DeepseekV4Attention(nn.Module, AttentionLayerBase, ABC):
swa_metadata.block_size,
)
# FlashInfer full-cache path: the [num_blocks, block_size, 512] cache
# stores the KV row in its plain dtype (no Q padding). bf16 rewrites q
# in place; per-tensor fp8 writes a separately-allocated fp8 q and
# quantizes the KV row.
# Plain-row path: the [num_blocks, block_size, 512] cache stores the KV
# row in its element dtype (no Q padding). bf16 rewrites q in place;
# per-tensor fp8 writes a separately-allocated fp8 q and quantizes the
# KV row.
block_size = swa_metadata.block_size
swa_kv_cache_3d = swa_kv_cache.view(-1, block_size, self.head_dim)
if cache_dtype == torch.bfloat16:
@@ -601,18 +603,18 @@ class DeepseekV4Attention(nn.Module, AttentionLayerBase, ABC):
self.compress_ratio <= 1
): # SWA part. Allocated separately as DeepseekV4SWACache.
return None
# FlashMLA uses the fp8_ds_mla block format (UE8M0 block-scaled fp8 as
# uint8, 576B aligned); FlashInfer stores a plain bf16 / per-tensor fp8
# row with no extra alignment.
is_flashmla = self.kv_cache_dtype == "fp8_ds_mla"
# fp8_ds_mla is a UE8M0 block-scaled uint8 layout and needs 576B
# alignment; plain bf16 / per-tensor fp8 rows use natural element-size
# pages.
uses_fp8_ds_mla_layout = self.kv_cache_dtype == "fp8_ds_mla"
return MLAAttentionSpec(
block_size=vllm_config.cache_config.block_size,
num_kv_heads=1,
head_size=self.head_dim,
dtype=torch.uint8 if is_flashmla else self.kv_cache_torch_dtype,
dtype=torch.uint8 if uses_fp8_ds_mla_layout else self.kv_cache_torch_dtype,
compress_ratio=self.compress_ratio,
cache_dtype_str=self.kv_cache_dtype,
alignment=576 if is_flashmla else None, # FlashMLA needs 576B
alignment=576 if uses_fp8_ds_mla_layout else None,
model_version="deepseek_v4",
)
@@ -40,9 +40,18 @@ def _fused_inv_rope_fp8_quant_per_head(
USE_GDC: tl.constexpr,
launch_pdl: tl.constexpr, # triton metadata
):
# int64: stride multiply overflows int32 past num_tokens=32768 (IMA).
# Cast every stride to int64 — without this, Python-int strides are
# inferred as int32 and `pid_token(int64) × stride(int32)` can lower to
# int32 arithmetic, wrapping past 2³¹ for large prefill batches → IMA.
pid_token = tl.program_id(0).to(tl.int64)
pid_gh = tl.program_id(1).to(tl.int64)
o_stride_token = o_stride_token.to(tl.int64)
o_stride_head = o_stride_head.to(tl.int64)
cache_stride_pos = cache_stride_pos.to(tl.int64)
fp8_stride_group = fp8_stride_group.to(tl.int64)
fp8_stride_token = fp8_stride_token.to(tl.int64)
scale_stride_group = scale_stride_group.to(tl.int64)
scale_stride_k = scale_stride_k.to(tl.int64)
g = pid_gh // heads_per_group
head_in_group = pid_gh % heads_per_group
+9 -9
View File
@@ -155,17 +155,17 @@ class CompressorStateCache(torch.nn.Module, AttentionLayerBase):
raise ValueError(f"Invalid compress ratio: {compress_ratio}")
def get_kv_cache_spec(self, vllm_config: VllmConfig) -> KVCacheSpec:
# FlashMLA's UE8M0 paged layout needs 576B alignment; the FlashInfer
# full-cache path shares state pages with contiguous KV pages, so
# padding would break page matching.
is_flashmla = vllm_config.cache_config.cache_dtype == "fp8_ds_mla"
# fp8_ds_mla is the UE8M0 paged layout and needs 576B alignment. Plain
# full-cache rows share state pages with contiguous KV pages, so padding
# would break page matching.
uses_fp8_ds_mla_layout = vllm_config.cache_config.cache_dtype == "fp8_ds_mla"
return SlidingWindowMLASpec( # only has one vector instead of K + V
block_size=self.block_size,
num_kv_heads=1,
head_size=self.state_dim,
dtype=self.dtype,
sliding_window=self.sliding_window,
alignment=576 if is_flashmla else None,
alignment=576 if uses_fp8_ds_mla_layout else None,
)
def forward(self): ...
@@ -340,8 +340,8 @@ class DeepseekCompressor(nn.Module):
k_cache_layer = self._static_forward_context[self.k_cache_prefix]
kv_cache = k_cache_layer.kv_cache
# FlashInfer V4 reads a contiguous bf16 / per-tensor fp8 cache row; the
# legacy FlashMLA path uses the UE8M0 paged uint8 layout.
# Plain-row V4 reads a contiguous bf16 / per-tensor fp8 cache row; the
# fp8_ds_mla path uses the UE8M0 paged uint8 layout.
store_full_kv = self.head_dim == 512 and kv_cache.dtype != torch.uint8
store_full_fp8 = kv_cache.dtype == torch.float8_e4m3fn
fp8_scale = (
@@ -358,8 +358,8 @@ class DeepseekCompressor(nn.Module):
compress_norm_rope_store_cutedsl,
)
# head=512 on CUDA always uses cutedsl, for both the legacy UE8M0
# layout and the FlashInfer full-cache layout. The full-cache flags
# head=512 on CUDA always uses cutedsl, for both the fp8_ds_mla
# layout and the plain full-cache layout. The full-cache flags
# are consumed only here.
compress_norm_rope_store_fn = compress_norm_rope_store_cutedsl
extra_kwargs: dict[str, Any] = dict(
@@ -1,13 +1,6 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""DeepSeek V4 FlashInfer TRTLLM-gen sparse MLA backend.
Uses FlashInfer's public ``trtllm_batch_decode_sparse_mla_dsv4`` launcher with a
plain bf16 / per-tensor FP8 KV row (vs FlashMLA's packed ``fp8_ds_mla`` block
format). Shares the V4 sparse-index pipeline (SWA cache + compressor + indexer,
256-token blocks, head_size 512) with the FlashMLA V4 backend; only the
attention forward differs.
"""
"""DeepSeek V4 FlashInfer sparse MLA backend."""
from typing import TYPE_CHECKING, ClassVar, cast
@@ -18,6 +11,7 @@ from vllm.forward_context import get_forward_context
from vllm.models.deepseek_v4.attention import DeepseekV4Attention
from vllm.models.deepseek_v4.common.ops import (
build_flashinfer_mixed_sparse_indices,
compute_global_topk_indices_and_lens,
)
from vllm.models.deepseek_v4.nvidia.ops.o_proj import (
compute_fp8_einsum_recipe,
@@ -27,13 +21,14 @@ from vllm.models.deepseek_v4.sparse_mla import (
DeepseekV4FlashMLABackend,
DeepseekV4FlashMLAMetadata,
)
from vllm.platforms import current_platform
from vllm.platforms.interface import DeviceCapability
from vllm.utils.flashinfer import flashinfer_trtllm_batch_decode_sparse_mla_dsv4
from vllm.v1.attention.backend import MultipleOf
if TYPE_CHECKING:
from vllm.v1.attention.backends.mla.sparse_swa import DeepseekSparseSWAMetadata
# 128 MB TRTLLM-gen workspace, allocated once per device and zero-initialized
# (required for first use). Reused across all FlashInfer V4 layers.
_FLASHINFER_DSV4_WORKSPACE_BUFFER_SIZE = 128 * 1024 * 1024
_flashinfer_dsv4_workspace_by_device: dict[torch.device, torch.Tensor] = {}
@@ -51,34 +46,113 @@ def _get_flashinfer_dsv4_workspace(device: torch.device) -> torch.Tensor:
class DeepseekV4FlashInferMLASparseBackend(DeepseekV4FlashMLABackend):
"""Shares the FlashMLA V4 metadata/cache pipeline; swaps the attention impl.
"""FlashInfer backend using the DSv4 sparse metadata/cache layout.
Inheriting from the FlashMLA V4 backend reuses its ``DeepseekV4FlashMLAMetadata``
builder.
Inheriting from the FlashMLA V4 backend reuses its
``DeepseekV4FlashMLAMetadata`` builder.
"""
supported_dtypes: ClassVar[list[torch.dtype]] = [torch.bfloat16]
supported_kv_cache_dtypes: ClassVar[list[CacheDType]] = ["auto", "bfloat16", "fp8"]
supported_kv_cache_dtypes: ClassVar[list[CacheDType]] = [
"auto",
"bfloat16",
"fp8",
"fp8_e4m3",
"fp8_ds_mla",
]
@staticmethod
def get_supported_kernel_block_sizes() -> list[int | MultipleOf]:
return [256]
@staticmethod
def get_name() -> str:
return "FLASHINFER_MLA_SPARSE_DSV4"
@classmethod
def get_supported_head_sizes(cls) -> list[int]:
return [512]
@classmethod
def supports_sink(cls) -> bool:
return True
@classmethod
def is_sparse(cls) -> bool:
return True
@classmethod
def supports_compute_capability(cls, capability: DeviceCapability) -> bool:
return capability.major in [10, 12]
@classmethod
def supports_combination(
cls,
head_size: int,
dtype: torch.dtype,
kv_cache_dtype: CacheDType | None,
block_size: int | None,
use_mla: bool,
has_sink: bool,
use_sparse: bool,
use_mm_prefix: bool,
device_capability: DeviceCapability,
) -> str | None:
if device_capability.major == 10:
if kv_cache_dtype == "fp8_ds_mla":
return (
"FLASHINFER_MLA_SPARSE_DSV4 SM10x uses the plain "
"per-tensor FP8 KV layout, not fp8_ds_mla"
)
if kv_cache_dtype not in (None, "auto", "bfloat16", "fp8", "fp8_e4m3"):
return "kv_cache_dtype not supported"
return None
if device_capability.major == 12:
if kv_cache_dtype not in ("fp8", "fp8_e4m3", "fp8_ds_mla"):
return "kv_cache_dtype not supported"
from vllm.utils.flashinfer import has_flashinfer_sparse_mla_sm120
if not has_flashinfer_sparse_mla_sm120():
return (
"FLASHINFER_MLA_SPARSE_DSV4 SM120 requires FlashInfer's "
"sparse MLA decode API"
)
return None
return "FLASHINFER_MLA_SPARSE_DSV4 requires SM10x or SM12x"
@staticmethod
def get_kv_cache_shape(
num_blocks: int,
block_size: int,
num_kv_heads: int,
head_size: int,
cache_dtype_str: str = "auto",
) -> tuple[int, ...]:
device_capability = current_platform.get_device_capability()
if device_capability is not None and device_capability.major == 12:
return DeepseekV4FlashMLABackend.get_kv_cache_shape(
num_blocks,
block_size,
num_kv_heads,
head_size,
cache_dtype_str,
)
assert num_kv_heads == 1
return (num_blocks, block_size, head_size)
class DeepseekV4FlashInferMLAAttention(DeepseekV4Attention):
"""FlashInfer TRTLLM-gen sparse MLA attention layer for DeepSeek V4."""
"""FlashInfer TRTLLM-gen sparse MLA attention layer for SM100 DeepSeek V4."""
backend_cls = DeepseekV4FlashInferMLASparseBackend
# FlashInfer stores a plain bf16 / per-tensor fp8 KV row, not the FlashMLA
# packed fp8_ds_mla block format (UE8M0 block-scaled fp8 as uint8).
use_flashmla_fp8_layout: ClassVar[bool] = False
use_fp8_ds_mla_layout: ClassVar[bool] = False
@classmethod
def get_padded_num_q_heads(cls, num_heads: int) -> int:
# FP8 decode kernel only supports h_q = 64 or 128.
if num_heads > 128:
raise ValueError(
f"DeepseekV4 Flashinfer MLA Sparse does not support {num_heads} heads "
f"DeepseekV4 FlashInfer MLA Sparse does not support {num_heads} heads "
"(FP8 decode kernel requires h_q in {64, 128})."
)
return 64 if num_heads <= 64 else 128
@@ -106,8 +180,6 @@ class DeepseekV4FlashInferMLAAttention(DeepseekV4Attention):
# per-tensor FP8 cache path consumes these; bf16 reads ``self.scale``.
if self.kv_cache_torch_dtype != torch.float8_e4m3fn:
return
# TODO: load real per-tensor Q/KV scales from the checkpoint; unit
# scales until the scale tensor names are wired.
fp8_q_scale = 1.0
fp8_kv_scale = 1.0
self.register_buffer(
@@ -125,9 +197,8 @@ class DeepseekV4FlashInferMLAAttention(DeepseekV4Attention):
torch.tensor([fp8_kv_scale], dtype=torch.float32),
persistent=False,
)
# TRTLLM-gen takes scalar scale args on a distinct (correct) C++ path
# vs 1-elem tensors, so these are Python floats. bmm1 folds the softmax
# scale and the Q/KV per-tensor scales; bmm2 is the KV scale.
# TRTLLM-gen takes scalar scale args on a distinct C++ path vs
# one-element tensors, so these are Python floats.
self._flashinfer_fp8_bmm1_scale = self.scale * fp8_q_scale * fp8_kv_scale
self._flashinfer_fp8_bmm2_scale = fp8_kv_scale
@@ -387,9 +458,8 @@ class DeepseekV4FlashInferMLAAttention(DeepseekV4Attention):
query_start_loc_cpu = swa_metadata.query_start_loc_cpu
assert query_start_loc is not None and query_start_loc_cpu is not None
# Keep Perkz's two-call decode/prefill split: the TRTLLM-gen launcher is
# tuned for uniform-q batches, and collapsing the mixed batch into a
# single call is the suspected source of the prior IMA.
# Keep the TRTLLM-gen decode/prefill split: the launcher is tuned for
# uniform-q batches, and this avoids flattening mixed batches into one call.
if num_decode_tokens > 0:
decode_cu = query_start_loc[: num_decodes + 1]
decode_cu_cpu = query_start_loc_cpu[: num_decodes + 1]
@@ -434,3 +504,379 @@ class DeepseekV4FlashInferMLAAttention(DeepseekV4Attention):
cum_seq_lens_q=prefill_cu,
max_q_len=int(prefill_lens_cpu.max().item()),
)
class DeepseekV4FlashInferSM120Attention(DeepseekV4Attention):
"""DeepSeek V4 sparse MLA attention through FlashInfer's SM120 kernels."""
backend_cls = DeepseekV4FlashInferMLASparseBackend
use_fp8_ds_mla_layout: ClassVar[bool] = True
@staticmethod
def _get_workspace(device: torch.device) -> torch.Tensor:
return _get_flashinfer_dsv4_workspace(device)
@staticmethod
def _as_sparse_cache(kv_cache: torch.Tensor) -> torch.Tensor:
if kv_cache.dtype == torch.float8_e4m3fn:
kv_cache = kv_cache.view(torch.uint8)
if kv_cache.dim() == 4:
return kv_cache
return kv_cache.unsqueeze(-2)
@classmethod
def get_padded_num_q_heads(cls, num_heads: int) -> int:
if num_heads <= 16:
return 16
if num_heads <= 32:
return 32
if num_heads <= 64:
return 64
if num_heads <= 128:
return 128
raise ValueError(
f"DeepseekV4 FlashInfer MLA Sparse does not support {num_heads} heads "
"(SM120 kernel requires h_q in {16, 32, 64, 128})."
)
def _o_proj(self, o: torch.Tensor, positions: torch.Tensor) -> torch.Tensor:
return deep_gemm_fp8_o_proj(
o,
positions,
self.rotary_emb.cos_sin_cache,
self.wo_a,
self.wo_b,
n_groups=self.n_local_groups,
heads_per_group=self.n_local_heads // self.n_local_groups,
nope_dim=self.nope_head_dim,
rope_dim=self.rope_head_dim,
o_lora_rank=self.o_lora_rank,
einsum_recipe=self._einsum_recipe,
tma_aligned_scales=self._tma_aligned_scales,
)
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
from vllm.utils.flashinfer import has_flashinfer_sparse_mla_sm120
if not has_flashinfer_sparse_mla_sm120():
raise RuntimeError(
"FLASHINFER_MLA_SPARSE_DSV4 on SM120 requires FlashInfer's "
"sparse MLA decode API."
)
self._einsum_recipe, self._tma_aligned_scales = compute_fp8_einsum_recipe()
# Per-tensor FP8 cache path scales.
if self.kv_cache_torch_dtype != torch.float8_e4m3fn:
return
fp8_q_scale = 1.0
fp8_kv_scale = 1.0
self.register_buffer(
"_flashinfer_fp8_q_scale",
torch.tensor([fp8_q_scale], dtype=torch.float32),
persistent=False,
)
self.register_buffer(
"_flashinfer_fp8_q_scale_inv",
torch.tensor([1.0 / fp8_q_scale], dtype=torch.float32),
persistent=False,
)
self.register_buffer(
"_flashinfer_fp8_kv_scale",
torch.tensor([fp8_kv_scale], dtype=torch.float32),
persistent=False,
)
# FlashInfer expects scalar scale arguments for this path.
self._flashinfer_fp8_bmm1_scale = self.scale * fp8_q_scale * fp8_kv_scale
self._flashinfer_fp8_bmm2_scale = fp8_kv_scale
def _reserve_empty_forward_workspace(self) -> None:
self._get_workspace(
torch.device("cuda", torch.accelerator.current_device_index())
)
def _forward_sparse_impl(
self,
q: torch.Tensor,
output: torch.Tensor,
flashmla_metadata: DeepseekV4FlashMLAMetadata | None,
swa_metadata: "DeepseekSparseSWAMetadata",
self_kv_cache: torch.Tensor | None,
swa_kv_cache: torch.Tensor,
swa_only: bool,
) -> None:
num_decode_tokens = swa_metadata.num_decode_tokens
if swa_metadata.num_prefills > 0:
self._forward_prefill(
q=q[num_decode_tokens:],
compressed_k_cache=self_kv_cache,
swa_k_cache=swa_kv_cache,
output=output[num_decode_tokens:],
attn_metadata=flashmla_metadata,
swa_metadata=swa_metadata,
)
if swa_metadata.num_decodes > 0:
self._forward_decode(
q=q[:num_decode_tokens],
kv_cache=self_kv_cache,
swa_metadata=swa_metadata,
attn_metadata=flashmla_metadata,
swa_only=swa_only,
output=output[:num_decode_tokens],
)
def forward_mqa(
self,
q: torch.Tensor,
kv: torch.Tensor,
positions: torch.Tensor,
output: torch.Tensor,
) -> None:
# Output may be padded to backend-supported head counts.
assert output.shape[0] == q.shape[0] and output.shape[-1] == q.shape[-1], (
f"output buffer shape {output.shape} incompatible with q shape {q.shape}"
)
assert output.shape[1] >= q.shape[1], (
f"output heads {output.shape[1]} must be >= q heads {q.shape[1]}"
)
# Per-tensor FP8 q produces a bf16 attention output.
expected_output_dtype = (
torch.bfloat16 if q.dtype == torch.float8_e4m3fn else q.dtype
)
assert output.dtype == expected_output_dtype, (
f"output dtype {output.dtype} must match expected {expected_output_dtype} "
f"for q dtype {q.dtype}"
)
forward_context = get_forward_context()
attn_metadata = forward_context.attn_metadata
if attn_metadata is None:
self._reserve_empty_forward_workspace()
output.zero_()
return
assert isinstance(attn_metadata, dict)
flashmla_metadata = cast(
DeepseekV4FlashMLAMetadata | None, attn_metadata.get(self.prefix)
)
swa_metadata = cast(
"DeepseekSparseSWAMetadata | None",
attn_metadata.get(self.swa_cache_layer.prefix),
)
assert swa_metadata is not None
swa_only = self.compress_ratio <= 1
# SWA-only layers don't allocate their own compressed KV cache.
self_kv_cache = self.kv_cache if not swa_only else None
swa_kv_cache = self.swa_cache_layer.kv_cache
self._forward_sparse_impl(
q=q,
output=output,
flashmla_metadata=flashmla_metadata,
swa_metadata=swa_metadata,
self_kv_cache=self_kv_cache,
swa_kv_cache=swa_kv_cache,
swa_only=swa_only,
)
def _prepare_query(self, q: torch.Tensor, output: torch.Tensor) -> torch.Tensor:
if self.kv_cache_torch_dtype == torch.float8_e4m3fn:
assert q.dtype == torch.float8_e4m3fn
q = q.to(torch.bfloat16)
else:
assert q.dtype == torch.bfloat16
padded_heads = output.shape[1]
if q.shape[1] < padded_heads:
padded_query = q.new_zeros((q.shape[0], padded_heads, q.shape[2]))
padded_query[:, : q.shape[1], :] = q
q = padded_query
return q.contiguous()
def _forward_decode(
self,
q: torch.Tensor,
kv_cache: torch.Tensor | None,
swa_metadata: "DeepseekSparseSWAMetadata",
attn_metadata: DeepseekV4FlashMLAMetadata | None,
swa_only: bool,
output: torch.Tensor,
) -> None:
num_decodes = swa_metadata.num_decodes
num_decode_tokens = swa_metadata.num_decode_tokens
extra_sparse_indices = None
extra_sparse_lengths = None
if not swa_only:
if attn_metadata is None:
raise RuntimeError(
"Sparse MLA metadata is required for compressed layers."
)
if swa_metadata.is_valid_token is None:
raise RuntimeError(
"SWA validity metadata is required for compressed layers."
)
is_valid = swa_metadata.is_valid_token[:num_decode_tokens]
if self.compress_ratio == 4:
if self.topk_indices_buffer is None:
raise RuntimeError(
"C4A decode requires top-k indices from the indexer."
)
block_size = attn_metadata.block_size // self.compress_ratio
global_indices, extra_sparse_lengths = (
compute_global_topk_indices_and_lens(
self.topk_indices_buffer[:num_decode_tokens],
swa_metadata.token_to_req_indices,
attn_metadata.block_table[:num_decodes],
block_size,
is_valid,
)
)
extra_sparse_indices = global_indices.view(num_decode_tokens, 1, -1)
else:
extra_sparse_indices = attn_metadata.c128a_global_decode_topk_indices
extra_sparse_lengths = attn_metadata.c128a_decode_topk_lens
swa_indices = swa_metadata.decode_swa_indices
swa_lens = swa_metadata.decode_swa_lens
assert swa_indices is not None
assert swa_lens is not None
q = self._prepare_query(q, output)
swa_cache = self._as_sparse_cache(self.swa_cache_layer.kv_cache)
extra_cache = self._as_sparse_cache(kv_cache) if kv_cache is not None else None
if extra_cache is not None and extra_sparse_indices is None:
raise RuntimeError(
"Compressed sparse MLA decode requires compressed sparse indices."
)
flashinfer_trtllm_batch_decode_sparse_mla_dsv4(
query=q,
swa_kv_cache=swa_cache,
workspace_buffer=self._get_workspace(q.device),
sparse_indices=swa_indices,
compressed_kv_cache=extra_cache,
out=output,
bmm1_scale=self.scale,
sinks=self.attn_sink,
kv_layout="NHD",
swa_topk_lens=swa_lens,
extra_sparse_indices=extra_sparse_indices,
extra_sparse_topk_lens=extra_sparse_lengths,
)
def _forward_prefill(
self,
q: torch.Tensor,
compressed_k_cache: torch.Tensor | None,
swa_k_cache: torch.Tensor,
output: torch.Tensor,
attn_metadata: DeepseekV4FlashMLAMetadata | None,
swa_metadata: "DeepseekSparseSWAMetadata",
) -> None:
swa_only = self.compress_ratio <= 1
num_prefills = swa_metadata.num_prefills
num_decodes = swa_metadata.num_decodes
num_decode_tokens = swa_metadata.num_decode_tokens
num_prefill_tokens = swa_metadata.num_prefill_tokens
query_start_loc_cpu = swa_metadata.query_start_loc_cpu
assert query_start_loc_cpu is not None
prefill_token_base = query_start_loc_cpu[num_decodes]
local_topk_indices: torch.Tensor | None
if swa_only:
local_topk_indices = None
elif self.compress_ratio == 4:
if self.topk_indices_buffer is None:
raise RuntimeError(
"C4A prefill requires top-k indices from the indexer."
)
local_topk_indices = self.topk_indices_buffer[
num_decode_tokens : num_decode_tokens + num_prefill_tokens
]
else:
if attn_metadata is None:
raise RuntimeError("C128A prefill metadata is missing.")
local_topk_indices = attn_metadata.c128a_prefill_topk_indices
extra_sparse_indices: torch.Tensor | None = None
extra_sparse_lengths: torch.Tensor | None = None
if local_topk_indices is not None:
if attn_metadata is None:
raise RuntimeError("C4A prefill metadata is missing.")
if swa_metadata.token_to_req_indices is None:
raise RuntimeError("C4A prefill request mapping is missing.")
if swa_metadata.is_valid_token is None:
raise RuntimeError("C4A prefill validity metadata is missing.")
prefill_token_slice = slice(
num_decode_tokens, num_decode_tokens + num_prefill_tokens
)
block_size = attn_metadata.block_size // self.compress_ratio
extra_sparse_indices, extra_sparse_lengths = (
compute_global_topk_indices_and_lens(
local_topk_indices,
swa_metadata.token_to_req_indices[prefill_token_slice],
attn_metadata.block_table,
block_size,
swa_metadata.is_valid_token[prefill_token_slice],
)
)
assert swa_metadata.prefill_swa_indices is not None
assert swa_metadata.prefill_swa_lens is not None
q = self._prepare_query(q, output)
swa_kv_paged = self._as_sparse_cache(swa_k_cache)
if swa_only:
extra_kv_paged = None
else:
if compressed_k_cache is None:
raise RuntimeError(
"Compressed sparse MLA layers require their compressed KV cache."
)
extra_kv_paged = self._as_sparse_cache(compressed_k_cache)
num_chunks = (
num_prefills + self.PREFILL_CHUNK_SIZE - 1
) // self.PREFILL_CHUNK_SIZE
for chunk_idx in range(num_chunks):
chunk_start = chunk_idx * self.PREFILL_CHUNK_SIZE
chunk_end = min(chunk_start + self.PREFILL_CHUNK_SIZE, num_prefills)
query_start = (
query_start_loc_cpu[num_decodes + chunk_start] - prefill_token_base
)
query_end = (
query_start_loc_cpu[num_decodes + chunk_end] - prefill_token_base
)
extra_sparse_indices_chunk = (
extra_sparse_indices[query_start:query_end]
if extra_sparse_indices is not None
else None
)
extra_sparse_lengths_chunk = (
extra_sparse_lengths[query_start:query_end]
if extra_sparse_lengths is not None
else None
)
q_chunk = q[query_start:query_end]
swa_indices_chunk = swa_metadata.prefill_swa_indices[query_start:query_end]
swa_lens_chunk = swa_metadata.prefill_swa_lens[query_start:query_end]
if extra_kv_paged is not None and extra_sparse_indices_chunk is None:
raise RuntimeError(
"Compressed sparse MLA prefill requires compressed sparse indices."
)
flashinfer_trtllm_batch_decode_sparse_mla_dsv4(
query=q_chunk,
swa_kv_cache=swa_kv_paged,
workspace_buffer=self._get_workspace(q.device),
sparse_indices=swa_indices_chunk,
compressed_kv_cache=extra_kv_paged,
out=output[query_start:query_end],
bmm1_scale=self.scale,
sinks=self.attn_sink,
kv_layout="NHD",
swa_topk_lens=swa_lens_chunk,
extra_sparse_indices=extra_sparse_indices_chunk,
extra_sparse_topk_lens=extra_sparse_lengths_chunk,
)
+27 -5
View File
@@ -60,9 +60,11 @@ from vllm.model_executor.utils import set_weight_attrs
from vllm.models.deepseek_v4.attention import DeepseekV4Attention
from vllm.models.deepseek_v4.nvidia.flashinfer_sparse import (
DeepseekV4FlashInferMLAAttention,
DeepseekV4FlashInferSM120Attention,
)
from vllm.models.deepseek_v4.nvidia.flashmla import DeepseekV4FlashMLAAttention
from vllm.models.deepseek_v4.nvidia.ops.prepare_megamoe import prepare_megamoe_inputs
from vllm.platforms import current_platform
from vllm.sequence import IntermediateTensors
from vllm.utils.math_utils import cdiv
from vllm.v1.attention.backends.registry import AttentionBackendEnum
@@ -736,14 +738,34 @@ class DeepseekV4MoE(nn.Module):
def _select_dsv4_attn_cls(vllm_config: VllmConfig) -> type[DeepseekV4Attention]:
"""Pick the CUDA sparse-MLA attention class for the configured backend.
An explicit ``--attention-backend FLASHINFER_MLA_SPARSE_DSV4`` selects the
FlashInfer TRTLLM-gen path; otherwise the FlashMLA path is used.
The generic CUDA backend selector does not instantiate DSv4 layers directly,
so map generic sparse-MLA choices to the DSv4-specialized attention class.
Without an explicit backend, SM12 defaults to FlashInfer while the other
CUDA arches keep the FlashMLA path.
"""
if (
vllm_config.attention_config.backend
== AttentionBackendEnum.FLASHINFER_MLA_SPARSE_DSV4
backend = vllm_config.attention_config.backend
device_capability = current_platform.get_device_capability()
if backend in (
AttentionBackendEnum.FLASHINFER_MLA_SPARSE,
AttentionBackendEnum.FLASHINFER_MLA_SPARSE_SM120,
):
raise ValueError(
f"{backend.name} is not a DeepSeek V4 attention backend. "
"Use FLASHINFER_MLA_SPARSE_DSV4 for DeepSeek V4 FlashInfer "
"sparse MLA."
)
if backend == AttentionBackendEnum.FLASHINFER_MLA_SPARSE_DSV4:
if device_capability is not None and device_capability.major == 12:
return DeepseekV4FlashInferSM120Attention
return DeepseekV4FlashInferMLAAttention
if backend in (
AttentionBackendEnum.FLASHMLA_SPARSE,
AttentionBackendEnum.FLASHMLA_SPARSE_DSV4,
):
return DeepseekV4FlashMLAAttention
if device_capability is not None and device_capability.major == 12:
return DeepseekV4FlashInferSM120Attention
return DeepseekV4FlashMLAAttention
+4
View File
@@ -86,6 +86,10 @@ class DeepseekV4FlashMLABackend(AttentionBackend):
def is_sparse(cls) -> bool:
return True
@classmethod
def supports_sink(cls) -> bool:
return True
@classmethod
def supports_compute_capability(cls, capability: DeviceCapability) -> bool:
return capability.major in [9, 10]
+46 -15
View File
@@ -11,7 +11,7 @@ import platform
from collections.abc import Callable
from datetime import timedelta
from functools import cache, lru_cache, wraps
from typing import TYPE_CHECKING, TypeVar
from typing import TYPE_CHECKING, NamedTuple, TypeVar
import torch
from torch.distributed import PrefixStore, ProcessGroup
@@ -31,6 +31,7 @@ if TYPE_CHECKING:
from vllm.config import VllmConfig
from vllm.config.cache import CacheDType
from vllm.config.kernel import IrOpPriorityConfig
from vllm.v1.attention.backend import AttentionBackend
from vllm.v1.attention.selector import AttentionSelectorConfig
else:
VllmConfig = None
@@ -126,6 +127,11 @@ def _get_backend_priorities(
AttentionBackendEnum.TRITON_MLA,
*sparse_backends,
]
elif device_capability.major == 12:
return [
AttentionBackendEnum.TRITON_MLA,
AttentionBackendEnum.FLASHINFER_MLA_SPARSE_SM120,
]
else:
return [
AttentionBackendEnum.FLASH_ATTN_MLA,
@@ -153,6 +159,21 @@ def _get_backend_priorities(
]
def _backend_cls_path(backend_cls: type[AttentionBackend]) -> str:
module, qualname = backend_cls.full_cls_name()
return f"{module}.{qualname}"
def _get_attn_backend_class(backend: AttentionBackendEnum) -> type[AttentionBackend]:
return backend.get_class()
class _BackendCandidate(NamedTuple):
backend_class: type[AttentionBackend]
backend: AttentionBackendEnum
priority: int
def with_nvml_context(fn: Callable[_P, _R]) -> Callable[_P, _R]:
@wraps(fn)
def wrapper(*args: _P.args, **kwargs: _P.kwargs) -> _R:
@@ -340,7 +361,7 @@ class CudaPlatformBase(Platform):
attn_selector_config: AttentionSelectorConfig,
num_heads: int | None = None,
) -> tuple[
list[tuple[AttentionBackendEnum, int]],
list[_BackendCandidate],
dict[AttentionBackendEnum, tuple[int, list[str]]],
]:
valid_backends_priorities = []
@@ -354,7 +375,7 @@ class CudaPlatformBase(Platform):
)
for priority, backend in enumerate(backend_priorities):
try:
backend_class = backend.get_class()
backend_class = _get_attn_backend_class(backend)
invalid_reasons_i = backend_class.validate_configuration(
device_capability=device_capability,
**attn_selector_config._asdict(),
@@ -364,7 +385,9 @@ class CudaPlatformBase(Platform):
if invalid_reasons_i:
invalid_reasons[backend] = (priority, invalid_reasons_i)
else:
valid_backends_priorities.append((backend, priority))
valid_backends_priorities.append(
_BackendCandidate(backend_class, backend, priority)
)
return valid_backends_priorities, invalid_reasons
@@ -381,7 +404,7 @@ class CudaPlatformBase(Platform):
# First try checking just the selected backend, if there is one.
if selected_backend is not None:
try:
backend_class = selected_backend.get_class()
backend_class = _get_attn_backend_class(selected_backend)
invalid_reasons = backend_class.validate_configuration(
device_capability=device_capability,
**attn_selector_config._asdict(),
@@ -395,7 +418,7 @@ class CudaPlatformBase(Platform):
)
else:
logger.info("Using %s backend.", selected_backend)
return selected_backend.get_path()
return _backend_cls_path(backend_class)
# No selected backend or the selected backend is invalid,
# so we try finding a valid backend.
@@ -425,13 +448,13 @@ class CudaPlatformBase(Platform):
# We have found some valid backends. Select the one with the
# highest priority.
sorted_indices = sorted(
range(len(valid_backends_priorities)),
key=lambda i: valid_backends_priorities[i][1],
selected_candidate = min(
valid_backends_priorities,
key=lambda candidate: candidate.priority,
)
selected_index = sorted_indices[0]
selected_backend = valid_backends_priorities[selected_index][0]
selected_priority = valid_backends_priorities[selected_index][1]
selected_backend_class = selected_candidate.backend_class
selected_backend = selected_candidate.backend
selected_priority = selected_candidate.priority
# If the user specified --block-size (but not --attention-backend),
# check whether that constraint precluded any higher-priority backends.
@@ -457,10 +480,14 @@ class CudaPlatformBase(Platform):
logger.info_once(
"Using %s attention backend out of potential backends: %s.",
selected_backend.name,
"[" + ", ".join(f"'{b[0].name}'" for b in valid_backends_priorities) + "]",
"["
+ ", ".join(
f"'{candidate.backend.name}'" for candidate in valid_backends_priorities
)
+ "]",
)
return selected_backend.get_path()
return _backend_cls_path(selected_backend_class)
@classmethod
def get_supported_vit_attn_backends(cls) -> list[AttentionBackendEnum]:
@@ -635,7 +662,11 @@ class CudaPlatformBase(Platform):
@classmethod
def support_deep_gemm(cls) -> bool:
"""Currently, only Hopper and Blackwell GPUs are supported."""
return cls.is_device_capability(90) or cls.is_device_capability_family(100)
return (
cls.is_device_capability(90)
or cls.is_device_capability_family(100)
or cls.is_device_capability_family(120)
)
@classmethod
def is_integrated_gpu(cls, device_id: int = 0) -> bool:
+26 -2
View File
@@ -27,8 +27,10 @@ logger = init_logger(__name__)
try:
from amdsmi import (
AmdSmiException,
AmdSmiMemoryType,
amdsmi_get_gpu_asic_info,
amdsmi_get_gpu_device_uuid,
amdsmi_get_gpu_memory_total,
amdsmi_get_processor_handles,
amdsmi_init,
amdsmi_shut_down,
@@ -167,6 +169,14 @@ def _query_gcn_arch_from_amdsmi() -> str:
raise RuntimeError("amdsmi did not return valid GCN arch")
@with_amdsmi_context
def _query_total_memory_from_amdsmi(physical_device_id: int) -> int:
"""Query total VRAM (bytes) from amdsmi. Raises if not available."""
handles = amdsmi_get_processor_handles()
handle = handles[physical_device_id]
return amdsmi_get_gpu_memory_total(handle, AmdSmiMemoryType.VRAM)
def _get_gcn_arch() -> str:
"""
Get GCN arch via amdsmi (no CUDA init), fallback to torch.cuda.
@@ -726,8 +736,22 @@ class RocmPlatform(Platform):
@classmethod
def get_device_total_memory(cls, device_id: int = 0) -> int:
device_props = torch.cuda.get_device_properties(device_id)
return device_props.total_memory
# Query total VRAM via amdsmi so we don't initialize a HIP context in
# the calling process. torch.cuda.get_device_properties() creates a
# HIP context, which makes vLLM fall back from `fork` to `spawn` for
# worker processes. Keeping this query context-free preserves `fork`
# where it is otherwise valid (e.g. out-of-tree models registered in
# the parent process).
try:
physical_device_id = cls.device_id_to_physical_device_id(device_id)
return _query_total_memory_from_amdsmi(physical_device_id)
except Exception as e:
logger.debug("Failed to get total memory via amdsmi: %s", e)
logger.warning_once(
"Failed to get total memory via amdsmi, falling back to "
"torch.cuda. This will initialize CUDA."
)
return torch.cuda.get_device_properties(device_id).total_memory
@classmethod
def apply_config_platform_defaults(cls, vllm_config: "VllmConfig") -> None:
+144 -3
View File
@@ -5,6 +5,7 @@
Users of vLLM should always import **only** these wrappers.
"""
import contextlib
import functools
import importlib
import os
@@ -37,7 +38,10 @@ def should_auto_disable_deep_gemm(model_type: str | None) -> bool:
"""
if model_type is None:
return False
if not current_platform.is_device_capability_family(100):
if not (
current_platform.is_device_capability_family(100)
or current_platform.is_device_capability_family(120)
):
return False
return model_type in _DEEPGEMM_BLACKWELL_EXCLUDED_MODEL_TYPES
@@ -71,7 +75,10 @@ class DeepGemmQuantScaleFMT(Enum):
cls._oracle_cache = ( # type: ignore
cls.UE8M0
if current_platform.is_device_capability_family(100)
if (
current_platform.is_device_capability_family(100)
or current_platform.is_device_capability_family(120)
)
else cls.FLOAT32_CEIL_UE8M0
)
@@ -138,7 +145,15 @@ _get_paged_mqa_logits_metadata_impl: Callable[..., Any] | None = None
_tf32_hc_prenorm_gemm_impl: Callable[..., Any] | None = None
_get_mn_major_tma_aligned_tensor_impl: Callable[..., Any] | None = None
_get_mk_alignment_for_contiguous_layout_impl: Callable[..., Any] | None = None
_get_theoretical_mk_alignment_for_contiguous_layout_impl: Callable[..., Any] | None = (
None
)
_transform_sf_into_required_layout_impl: Callable[..., Any] | None = None
_pack_ue8m0_to_int_impl: Callable[..., Any] | None = None
_get_mn_major_tma_aligned_packed_ue8m0_tensor_impl: Callable[..., Any] | None = None
_get_k_grouped_mn_major_tma_aligned_packed_ue8m0_tensor_impl: (
Callable[..., Any] | None
) = None
@functools.cache
@@ -203,7 +218,11 @@ def _lazy_init() -> None:
global _tf32_hc_prenorm_gemm_impl
global _get_mn_major_tma_aligned_tensor_impl
global _get_mk_alignment_for_contiguous_layout_impl
global _get_theoretical_mk_alignment_for_contiguous_layout_impl
global _transform_sf_into_required_layout_impl
global _pack_ue8m0_to_int_impl
global _get_mn_major_tma_aligned_packed_ue8m0_tensor_impl
global _get_k_grouped_mn_major_tma_aligned_packed_ue8m0_tensor_impl
# fast path
if (
_cublaslt_gemm_nt_impl is not None
@@ -218,6 +237,9 @@ def _lazy_init() -> None:
or _tf32_hc_prenorm_gemm_impl is not None
or _get_mk_alignment_for_contiguous_layout_impl is not None
or _transform_sf_into_required_layout_impl is not None
or _pack_ue8m0_to_int_impl is not None
or _get_mn_major_tma_aligned_packed_ue8m0_tensor_impl is not None
or _get_k_grouped_mn_major_tma_aligned_packed_ue8m0_tensor_impl is not None
):
return
@@ -258,9 +280,19 @@ def _lazy_init() -> None:
_get_mk_alignment_for_contiguous_layout_impl = getattr(
_dg, "get_mk_alignment_for_contiguous_layout", None
)
_get_theoretical_mk_alignment_for_contiguous_layout_impl = getattr(
_dg, "get_theoretical_mk_alignment_for_contiguous_layout", None
)
_transform_sf_into_required_layout_impl = getattr(
_dg, "transform_sf_into_required_layout", None
)
_pack_ue8m0_to_int_impl = getattr(_dg, "pack_ue8m0_to_int", None)
_get_mn_major_tma_aligned_packed_ue8m0_tensor_impl = getattr(
_dg, "get_mn_major_tma_aligned_packed_ue8m0_tensor", None
)
_get_k_grouped_mn_major_tma_aligned_packed_ue8m0_tensor_impl = getattr(
_dg, "get_k_grouped_mn_major_tma_aligned_packed_ue8m0_tensor", None
)
DeepGemmQuantScaleFMT.init_oracle_cache()
@@ -280,7 +312,6 @@ def set_num_sms(num_sms: int) -> None:
dg.set_num_sms(num_sms)
@functools.cache
def get_mk_alignment_for_contiguous_layout() -> list[int]:
_lazy_init()
if _get_mk_alignment_for_contiguous_layout_impl is None:
@@ -289,6 +320,70 @@ def get_mk_alignment_for_contiguous_layout() -> list[int]:
return [mk_align_size, mk_align_size]
def get_theoretical_mk_alignment_for_contiguous_layout(
expected_m: int | None = None,
num_groups: int | None = None,
) -> int:
"""Per-call optimal M alignment for grouped contiguous GEMMs.
`expected_m` is the TOTAL routed tokens (sum across experts, typically
M × num_topk). `num_groups` is the number of experts on this rank.
The helper divides to recover per-expert em and picks an alignment based
on data-driven thresholds (see deep_gemm runtime.hpp comments).
Older callers that omit `num_groups` are interpreted as passing already
per-expert em (legacy behaviour preserved for backward compat).
"""
_lazy_init()
if _get_theoretical_mk_alignment_for_contiguous_layout_impl is None:
return _missing()
if num_groups is None:
return _get_theoretical_mk_alignment_for_contiguous_layout_impl(expected_m)
if num_groups <= 0:
raise ValueError(f"num_groups must be positive, got {num_groups}")
try:
return _get_theoretical_mk_alignment_for_contiguous_layout_impl(
expected_m, num_groups
)
except TypeError:
per_group_m = None if expected_m is None else cdiv(expected_m, num_groups)
return _get_theoretical_mk_alignment_for_contiguous_layout_impl(per_group_m)
def set_mk_alignment_for_contiguous_layout(value: int) -> None:
"""Set DeepGEMM's BLOCK_M cap for grouped contiguous GEMMs.
The DG heuristic constrains BLOCK_M this value when picking a kernel
layout. Use this in concert with `compute_aligned_M_and_alignment`'s
per-call alignment so the workspace's per-expert padding matches the
kernel's BLOCK_M; a mismatch leads to the scheduler reading the wrong
expert_id from `m_indices` at `m_block_idx * BLOCK_M` stride and
OOB-indexing the B-weights tensor (manifests as IMA under CUDA-graph
replay).
"""
_lazy_init()
dg = _import_deep_gemm()
if dg is None:
raise RuntimeError("DeepGEMM is not available")
dg.set_mk_alignment_for_contiguous_layout(value)
@contextlib.contextmanager
def mk_alignment_scope(value: int):
"""Temporarily set DeepGEMM's BLOCK_M cap, restoring on exit.
Use around a sequence of grouped-contiguous GEMM calls whose workspace
is padded to `value` (typically the per_call_align returned by
`compute_aligned_M_and_alignment`).
"""
prev = get_mk_alignment_for_contiguous_layout()[0]
set_mk_alignment_for_contiguous_layout(value)
try:
yield
finally:
set_mk_alignment_for_contiguous_layout(prev)
def get_col_major_tma_aligned_tensor(x: torch.Tensor) -> torch.Tensor:
"""Wrapper for DeepGEMM's get_mn_major_tma_aligned_tensor"""
_lazy_init()
@@ -297,6 +392,48 @@ def get_col_major_tma_aligned_tensor(x: torch.Tensor) -> torch.Tensor:
return _get_mn_major_tma_aligned_tensor_impl(x)
def pack_ue8m0_to_int(x: torch.Tensor) -> torch.Tensor:
"""Pack 4 UE8M0 (uint8) scales into one int32.
DeepGEMM's SM100/SM120 FP8/FP4 kernels accept either ``float32`` scales
(legacy format, 4 B/scale) or ``int32`` packed UE8M0 scales (1 B/scale
after 4:1 packing 4× smaller than the legacy fp32 representation).
"""
_lazy_init()
if _pack_ue8m0_to_int_impl is None:
return _missing()
return _pack_ue8m0_to_int_impl(x)
def get_mn_major_tma_aligned_packed_ue8m0_tensor(x: torch.Tensor) -> torch.Tensor:
"""Pack UE8M0 (uint8) → int32 with the MN-major TMA-aligned layout the
DeepGEMM kernels consume directly. 16× smaller than the fp32 legacy SF
format. Use for non-grouped 2D scale tensors.
"""
_lazy_init()
if _get_mn_major_tma_aligned_packed_ue8m0_tensor_impl is None:
return _missing()
return _get_mn_major_tma_aligned_packed_ue8m0_tensor_impl(x)
def get_k_grouped_mn_major_tma_aligned_packed_ue8m0_tensor(
sf: torch.Tensor,
ks_tensor: torch.Tensor,
ks: list[int],
gran_k: int,
) -> torch.Tensor:
"""Grouped (3D, expert-batched) variant of
``get_mn_major_tma_aligned_packed_ue8m0_tensor``. Use for MoE weight
scale tensors of shape ``(num_experts, mn, k_scale)``.
"""
_lazy_init()
if _get_k_grouped_mn_major_tma_aligned_packed_ue8m0_tensor_impl is None:
return _missing()
return _get_k_grouped_mn_major_tma_aligned_packed_ue8m0_tensor_impl(
sf, ks_tensor, ks, gran_k
)
def cublaslt_gemm_nt(*args, **kwargs):
_lazy_init()
if _cublaslt_gemm_nt_impl is None:
@@ -601,4 +738,8 @@ __all__ = [
"should_use_deepgemm_for_fp8_linear",
"get_col_major_tma_aligned_tensor",
"get_mk_alignment_for_contiguous_layout",
"get_theoretical_mk_alignment_for_contiguous_layout",
"pack_ue8m0_to_int",
"get_mn_major_tma_aligned_packed_ue8m0_tensor",
"get_k_grouped_mn_major_tma_aligned_packed_ue8m0_tensor",
]
+35 -11
View File
@@ -72,11 +72,10 @@ def _missing(*_: Any, **__: Any) -> NoReturn:
)
def _missing_dsv4_sparse_mla(*_: Any, **__: Any) -> NoReturn:
def _missing_sparse_mla(*_: Any, **__: Any) -> NoReturn:
raise RuntimeError(
"flashinfer.mla.trtllm_batch_decode_sparse_mla_dsv4 is not available. "
"Install a FlashInfer build that includes DeepSeek V4 sparse MLA "
"TRTLLM-GEN support."
"FlashInfer sparse MLA decode APIs are not available. "
"Install a FlashInfer build that includes sparse MLA decode support."
)
@@ -149,14 +148,18 @@ flashinfer_b12x_fused_moe = _lazy_import_wrapper(
trtllm_fp4_block_scale_moe = _lazy_import_wrapper(
"flashinfer", "trtllm_fp4_block_scale_moe"
)
# DeepSeek V4 sparse MLA TRTLLM-GEN decode launcher (public wrapper). Handles
# the SWA + compressed KV pools, the concatenated sparse-index matrix, and
# per-tensor FP8 / BF16 inputs with BF16 output.
flashinfer_trtllm_batch_decode_sparse_mla_dsv4 = _lazy_import_wrapper(
"flashinfer.mla",
"trtllm_batch_decode_sparse_mla_dsv4",
fallback_fn=_missing_dsv4_sparse_mla,
flashinfer_trtllm_batch_decode_with_kv_cache_mla = _lazy_import_wrapper(
"flashinfer.decode",
"trtllm_batch_decode_with_kv_cache_mla",
fallback_fn=_missing_sparse_mla,
)
flashinfer_trtllm_batch_decode_sparse_mla_dsv4 = _lazy_import_wrapper(
"flashinfer.decode",
"trtllm_batch_decode_sparse_mla_dsv4",
fallback_fn=_missing_sparse_mla,
)
# Special case for autotune since it returns a context manager
autotune = _lazy_import_wrapper(
"flashinfer.autotuner",
@@ -209,6 +212,26 @@ def has_flashinfer_moe() -> bool:
)
@functools.cache
def has_flashinfer_sparse_mla_sm120() -> bool:
"""Return ``True`` if FlashInfer sparse MLA decode support is available."""
if not has_flashinfer():
return False
try:
from flashinfer.autotuner import autotune
from flashinfer.decode import (
trtllm_batch_decode_sparse_mla_dsv4,
trtllm_batch_decode_with_kv_cache_mla,
)
except ImportError:
return False
return (
callable(trtllm_batch_decode_sparse_mla_dsv4)
and callable(trtllm_batch_decode_with_kv_cache_mla)
and callable(autotune)
)
@functools.cache
def has_flashinfer_cutedsl() -> bool:
"""Return ``True`` if FlashInfer cutedsl module is available."""
@@ -988,6 +1011,7 @@ __all__ = [
"flashinfer_b12x_fused_moe",
"flashinfer_convert_sf_to_mma_layout",
"trtllm_fp4_block_scale_moe",
"flashinfer_trtllm_batch_decode_with_kv_cache_mla",
"flashinfer_trtllm_batch_decode_sparse_mla_dsv4",
"autotune",
"has_flashinfer_moe",
+1 -1
View File
@@ -436,7 +436,7 @@ class CommonAttentionMetadata:
positions: torch.Tensor | None = None
"""(num_actual_tokens,) token positions. Optional; set when the caller
has positions available so that builders can pre-compute position-dependent
metadata (e.g. C128A topk indices for DeepSeek V4)."""
sparse metadata for DeepSeek V4 C128A layers."""
is_prefilling: torch.Tensor | None = None
"""(batch_size,) bool tensor: True if request is still in prefill phase
@@ -1,23 +1,12 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""FlashInfer MLA Sparse Attention Backend.
This backend uses the FlashInfer TRT-LLM MLA kernel with sparse_mla_top_k
for models like DeepSeek-V3.2 that use index-based sparse attention.
For sparse MLA:
- block_tables shape changes from [batch_size, max_num_blocks] (dense)
to [batch_size, q_len_per_request, sparse_mla_top_k] (sparse)
- The sparse indices represent physical cache slot positions to attend to
- sparse_mla_top_k parameter must be set to the topk value
"""
"""FlashInfer sparse MLA attention backend."""
from dataclasses import dataclass
from typing import TYPE_CHECKING, ClassVar
import numpy as np
import torch
from flashinfer.decode import trtllm_batch_decode_with_kv_cache_mla
from vllm.config import VllmConfig
from vllm.config.cache import CacheDType
@@ -52,34 +41,13 @@ logger = init_logger(__name__)
FLASHINFER_MLA_SPARSE_WORKSPACE_BUFFER_SIZE = 128 * 1024 * 1024
class FlashInferMLASparseBackend(AttentionBackend):
"""FlashInfer MLA backend with sparse attention support.
This backend uses the FlashInfer TRT-LLM MLA kernel with sparse_mla_top_k
for models like DeepSeek-V3.2 that use index-based sparse attention.
"""
supported_dtypes: ClassVar[list[torch.dtype]] = [torch.float16, torch.bfloat16]
supported_kv_cache_dtypes: ClassVar[list[CacheDType]] = [
"auto",
"float16",
"bfloat16",
"fp8",
"fp8_e4m3",
]
@staticmethod
def get_supported_kernel_block_sizes() -> list[int | MultipleOf]:
return [32, 64]
class _FlashInferMLASparseBackendBase(AttentionBackend):
"""Common metadata for concrete FlashInfer sparse MLA backends."""
@staticmethod
def get_name() -> str:
return "FLASHINFER_MLA_SPARSE"
@staticmethod
def get_impl_cls() -> type["FlashInferMLASparseImpl"]:
return FlashInferMLASparseImpl
@staticmethod
def get_builder_cls() -> type["FlashInferMLASparseMetadataBuilder"]:
return FlashInferMLASparseMetadataBuilder
@@ -96,9 +64,29 @@ class FlashInferMLASparseBackend(AttentionBackend):
def is_sparse(cls) -> bool:
return True
class FlashInferMLASparseTRTLLMBackend(_FlashInferMLASparseBackendBase):
"""FlashInfer sparse MLA backend using the TRTLLM-gen launcher."""
supported_dtypes: ClassVar[list[torch.dtype]] = [torch.float16, torch.bfloat16]
supported_kv_cache_dtypes: ClassVar[list[CacheDType]] = [
"auto",
"float16",
"bfloat16",
"fp8",
"fp8_e4m3",
]
@staticmethod
def get_supported_kernel_block_sizes() -> list[int | MultipleOf]:
return [32, 64]
@staticmethod
def get_impl_cls() -> type[SparseMLAAttentionImpl]:
return FlashInferMLASparseImpl
@classmethod
def supports_compute_capability(cls, capability: DeviceCapability) -> bool:
# FlashInfer sparse MLA targets Blackwell (SM 10.x)
return capability.major == 10
@classmethod
@@ -114,10 +102,15 @@ class FlashInferMLASparseBackend(AttentionBackend):
use_mm_prefix: bool,
device_capability: DeviceCapability,
) -> str | None:
# FlashInfer MLA sparse kernel requires qk_nope_head_dim in [128, 192]
from vllm.config import get_current_vllm_config
vllm_config = get_current_vllm_config()
if kv_cache_dtype == "fp8_ds_mla":
return (
"FLASHINFER_MLA_SPARSE SM10 does not support fp8_ds_mla kv-cache dtype"
)
# FlashInfer MLA sparse SM10 kernel requires qk_nope_head_dim in [128, 192].
if vllm_config.model_config is not None:
hf_text_config = vllm_config.model_config.hf_text_config
qk_nope_head_dim = getattr(hf_text_config, "qk_nope_head_dim", 1)
@@ -146,6 +139,102 @@ class FlashInferMLASparseBackend(AttentionBackend):
return "HND"
class FlashInferMLASparseSM120Backend(_FlashInferMLASparseBackendBase):
"""FlashInfer sparse MLA backend for SM120."""
supported_dtypes: ClassVar[list[torch.dtype]] = [torch.bfloat16]
supported_kv_cache_dtypes: ClassVar[list[CacheDType]] = [
"auto",
"fp8",
"fp8_e4m3",
"fp8_ds_mla",
]
@staticmethod
def get_name() -> str:
return "FLASHINFER_MLA_SPARSE_SM120"
@staticmethod
def get_supported_kernel_block_sizes() -> list[int | MultipleOf]:
return [64, 256]
@staticmethod
def get_impl_cls() -> type[SparseMLAAttentionImpl]:
from vllm.v1.attention.backends.mla.flashinfer_mla_sparse_sm120 import (
FlashInferMLASparseSM120Impl,
)
return FlashInferMLASparseSM120Impl
@classmethod
def supports_compute_capability(cls, capability: DeviceCapability) -> bool:
return capability.major == 12
@classmethod
def supports_combination(
cls,
head_size: int,
dtype: torch.dtype,
kv_cache_dtype: CacheDType | None,
block_size: int | None,
use_mla: bool,
has_sink: bool,
use_sparse: bool,
use_mm_prefix: bool,
device_capability: DeviceCapability,
) -> str | None:
from vllm.config import get_current_vllm_config
from vllm.utils.flashinfer import has_flashinfer_sparse_mla_sm120
if not has_flashinfer_sparse_mla_sm120():
return (
"FLASHINFER_MLA_SPARSE_SM120 requires FlashInfer's "
"sparse MLA decode API"
)
if dtype != torch.bfloat16:
return "dtype not supported"
if kv_cache_dtype not in (
None,
"auto",
"fp8",
"fp8_e4m3",
"fp8_ds_mla",
):
return "kv_cache_dtype not supported"
vllm_config = get_current_vllm_config()
if vllm_config.model_config is not None:
hf_text_config = vllm_config.model_config.hf_text_config
index_topk = getattr(hf_text_config, "index_topk", None)
if index_topk is None:
return (
"FLASHINFER_MLA_SPARSE_SM120 requires a model with "
"index_topk config"
)
if int(index_topk) != 2048:
return (
"FLASHINFER_MLA_SPARSE_SM120 requires index_topk=2048; "
f"got {index_topk}"
)
return None
@staticmethod
def get_kv_cache_shape(
num_blocks: int,
block_size: int,
num_kv_heads: int, # assumed to be 1 for MLA
head_size: int,
cache_dtype_str: str = "auto",
) -> tuple[int, ...]:
if cache_dtype_str in ("auto", "fp8", "fp8_e4m3", "fp8_ds_mla"):
# fp8_ds_mla packed layout: 512 NoPE + 16 scales + 128 RoPE.
return (num_blocks, block_size, 656)
return (num_blocks, block_size, head_size)
@classmethod
def get_required_kv_cache_layout(cls) -> "KVCacheLayoutType | None":
return None
@dataclass
class FlashInferMLASparseMetadata(AttentionMetadata):
"""Attention metadata for FlashInfer MLA Sparse backend."""
@@ -353,6 +442,8 @@ class FlashInferMLASparseImpl(SparseMLAAttentionImpl[FlashInferMLASparseMetadata
if is_quantized_kv_cache(self.kv_cache_dtype):
self.bmm2_scale *= layer._k_scale_float
from flashinfer.decode import trtllm_batch_decode_with_kv_cache_mla
o = trtllm_batch_decode_with_kv_cache_mla(
query=q.unsqueeze(1),
kv_cache=kv_c_and_k_pe_cache.unsqueeze(1),
@@ -0,0 +1,155 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""SM120 implementation variant for ``FLASHINFER_MLA_SPARSE_SM120``."""
from typing import TYPE_CHECKING, cast
import torch
from vllm.v1.attention.backend import (
AttentionLayer,
AttentionType,
SparseMLAAttentionImpl,
)
from vllm.v1.attention.backends.mla.flashinfer_mla_sparse import (
FlashInferMLASparseMetadata,
_get_workspace_buffer,
)
from vllm.v1.attention.backends.mla.sparse_utils import (
triton_convert_req_index_to_global_index,
)
if TYPE_CHECKING:
from vllm.model_executor.models.deepseek_v2 import Indexer
def _kv_scale_format_for_model(model_type: str | None) -> str:
if model_type is not None and model_type.startswith("glm"):
return "arbitrary_fp32"
return "pow2_fp32"
class FlashInferMLASparseSM120Impl(SparseMLAAttentionImpl[FlashInferMLASparseMetadata]):
"""SM120 FlashInfer sparse-MLA implementation."""
def __init__(
self,
num_heads: int,
head_size: int,
scale: float,
num_kv_heads: int,
alibi_slopes: list[float] | None,
sliding_window: int | None,
kv_cache_dtype: str,
logits_soft_cap: float | None,
attn_type: str,
kv_sharing_target_layer_name: str | None,
indexer: "Indexer | None" = None,
**mla_args,
) -> None:
if any([alibi_slopes, sliding_window, logits_soft_cap]):
raise NotImplementedError(
"FLASHINFER_MLA_SPARSE_SM120 does not support alibi_slopes / "
"sliding_window / logits_soft_cap"
)
if attn_type != AttentionType.DECODER:
raise NotImplementedError(
"FLASHINFER_MLA_SPARSE_SM120 only supports decoder self-attention"
)
self.num_heads = num_heads
self.head_size = head_size
self.scale = float(scale)
self.num_kv_heads = num_kv_heads
self.kv_cache_dtype = kv_cache_dtype
if self.kv_cache_dtype != "fp8_ds_mla":
raise NotImplementedError(
"FLASHINFER_MLA_SPARSE_SM120 requires the packed fp8_ds_mla "
f"KV cache layout; got kv_cache_dtype={kv_cache_dtype!r}."
)
self.kv_lora_rank: int = mla_args["kv_lora_rank"]
self.qk_nope_head_dim: int = mla_args["qk_nope_head_dim"]
self.qk_rope_head_dim: int = mla_args["qk_rope_head_dim"]
from vllm.config import get_current_vllm_config
vllm_config = get_current_vllm_config()
model_type = None
if vllm_config.model_config is not None:
model_type = getattr(
vllm_config.model_config.hf_text_config, "model_type", None
)
self.kv_scale_format = _kv_scale_format_for_model(model_type)
assert indexer is not None, (
"FLASHINFER_MLA_SPARSE_SM120 requires a sparse-MLA indexer "
"(model with index_topk in its config)."
)
self.topk_indices_buffer: torch.Tensor | None = indexer.topk_indices_buffer
from vllm.utils.flashinfer import has_flashinfer_sparse_mla_sm120
if not has_flashinfer_sparse_mla_sm120():
raise RuntimeError(
"FLASHINFER_MLA_SPARSE_SM120 requires FlashInfer's "
"sparse MLA decode API."
)
assert self.topk_indices_buffer is not None
self.supports_quant_query_input = False
self._workspace_buffer: torch.Tensor | None = None
def forward_mqa(
self,
q: torch.Tensor | tuple[torch.Tensor, torch.Tensor],
kv_c_and_k_pe_cache: torch.Tensor,
attn_metadata: FlashInferMLASparseMetadata,
layer: AttentionLayer,
) -> tuple[torch.Tensor, torch.Tensor | None]:
if isinstance(q, tuple):
q = torch.cat(q, dim=-1)
num_actual_toks = q.shape[0]
assert self.topk_indices_buffer is not None
topk_indices = self.topk_indices_buffer[:num_actual_toks]
topk_indices_physical = cast(
torch.Tensor,
triton_convert_req_index_to_global_index(
attn_metadata.req_id_per_token[:num_actual_toks],
attn_metadata.block_table,
topk_indices,
BLOCK_SIZE=attn_metadata.block_size,
NUM_TOPK_TOKENS=topk_indices.shape[1],
),
)
output = q.new_empty(
(num_actual_toks, self.num_heads, self.kv_lora_rank),
dtype=q.dtype,
)
if self._workspace_buffer is None:
self._workspace_buffer = _get_workspace_buffer(q.device)
from vllm.utils.flashinfer import (
flashinfer_trtllm_batch_decode_with_kv_cache_mla,
)
out = flashinfer_trtllm_batch_decode_with_kv_cache_mla(
query=q.unsqueeze(1),
kv_cache=kv_c_and_k_pe_cache.view(torch.uint8).unsqueeze(1),
workspace_buffer=self._workspace_buffer,
qk_nope_head_dim=self.qk_nope_head_dim,
kv_lora_rank=self.kv_lora_rank,
qk_rope_head_dim=self.qk_rope_head_dim,
block_tables=topk_indices_physical.unsqueeze(1),
seq_lens=None,
max_seq_len=attn_metadata.topk_tokens,
out=output.unsqueeze(1),
bmm1_scale=self.scale,
bmm2_scale=1.0,
sparse_mla_top_k=attn_metadata.topk_tokens,
kv_scale_format=self.kv_scale_format,
)
return out.squeeze(1), None
+74 -15
View File
@@ -74,14 +74,14 @@ class DeepseekV4SWACache(torch.nn.Module, AttentionLayerBase):
# determines the SWA block size of 64 tokens per block.
# TODO(yifan): make SWA block size automatically determined and configurable.
self.block_size = 64
# uint8: legacy FlashMLA UE8M0 paged layout. bfloat16 / float8_e4m3fn:
# FlashInfer contiguous full-cache layout.
# uint8: fp8_ds_mla UE8M0 paged layout. bfloat16 / float8_e4m3fn:
# contiguous full-cache layout.
assert self.dtype in (torch.uint8, torch.bfloat16, torch.float8_e4m3fn)
def get_kv_cache_spec(self, vllm_config: VllmConfig) -> KVCacheSpec:
# FlashMLA's UE8M0 paged layout needs 576B alignment; FlashInfer's
# contiguous bf16/fp8 cache uses the natural element-size page.
is_flashmla = self.cache_config.cache_dtype == "fp8_ds_mla"
# fp8_ds_mla's UE8M0 paged layout needs 576B alignment; contiguous
# bf16/fp8 cache uses the natural element-size page.
uses_fp8_ds_mla_layout = self.cache_config.cache_dtype == "fp8_ds_mla"
return SlidingWindowMLASpec(
block_size=self.block_size,
num_kv_heads=1,
@@ -89,7 +89,7 @@ class DeepseekV4SWACache(torch.nn.Module, AttentionLayerBase):
dtype=self.dtype,
sliding_window=self.window_size,
cache_dtype_str=self.cache_config.cache_dtype,
alignment=576 if is_flashmla else None,
alignment=576 if uses_fp8_ds_mla_layout else None,
model_version="deepseek_v4",
)
@@ -164,6 +164,11 @@ class DeepseekSparseSWAMetadata:
token_to_req_indices: torch.Tensor | None = None # [num_tokens]
decode_swa_indices: torch.Tensor | None = None # [num_decode_tokens, window_size]
decode_swa_lens: torch.Tensor | None = None # [num_decode_tokens]
# Paged-coordinate prefill SWA indices/lens (FP8 paged-direct prefill).
prefill_swa_indices: torch.Tensor | None = (
None # [num_prefill_tokens, 1, window_size]
)
prefill_swa_lens: torch.Tensor | None = None # [num_prefill_tokens]
# Number of decode/prefill requests/tokens (batch is reordered: decodes first)
num_decodes: int = 0
@@ -343,6 +348,20 @@ class DeepseekSparseSWAMetadataBuilder(AttentionMetadataBuilder):
dtype=torch.int32,
device=self.device,
)
# Allocated unconditionally — consumer picks paged-direct vs dequant
# at call time.
self.prefill_swa_indices = torch.zeros(
max_tokens,
1,
self.window_size,
dtype=torch.int32,
device=self.device,
)
self.prefill_swa_lens = torch.zeros(
max_tokens,
dtype=torch.int32,
device=self.device,
)
self.is_valid_token = torch.zeros(
max_tokens,
dtype=torch.bool,
@@ -402,6 +421,29 @@ class DeepseekSparseSWAMetadataBuilder(AttentionMetadataBuilder):
block_table,
block_table.stride(0),
self.block_size,
token_offset=0,
TRITON_BLOCK_SIZE=1024,
)
# Prefill SWA indices live in paged coordinates. `token_offset` lets
# the kernel read is_valid_token / token_to_req_indices at absolute
# prefill positions while writing output starting at index 0.
if num_prefill_tokens > 0:
prefill_swa_indices = self.prefill_swa_indices[:num_prefill_tokens]
prefill_swa_lens = self.prefill_swa_lens[:num_prefill_tokens]
_compute_swa_indices_and_lens_kernel[(num_prefill_tokens,)](
prefill_swa_indices,
prefill_swa_indices.stride(0),
prefill_swa_lens,
self.window_size,
query_start_loc,
seq_lens,
token_to_req_indices,
is_valid_token,
block_table,
block_table.stride(0),
self.block_size,
token_offset=num_decode_tokens,
TRITON_BLOCK_SIZE=1024,
)
@@ -431,6 +473,16 @@ class DeepseekSparseSWAMetadataBuilder(AttentionMetadataBuilder):
token_to_req_indices=token_to_req_indices,
decode_swa_indices=self.decode_swa_indices[:num_decode_tokens],
decode_swa_lens=self.decode_swa_lens[:num_decode_tokens],
prefill_swa_indices=(
self.prefill_swa_indices[:num_prefill_tokens]
if num_prefill_tokens > 0
else None
),
prefill_swa_lens=(
self.prefill_swa_lens[:num_prefill_tokens]
if num_prefill_tokens > 0
else None
),
block_size=self.block_size,
num_decodes=num_decodes,
num_prefills=num_prefills,
@@ -465,6 +517,7 @@ class DeepseekSparseSWAMetadataBuilder(AttentionMetadataBuilder):
num_decode_tokens == 0
or current_platform.is_rocm()
or current_platform.is_xpu()
or current_platform.is_device_capability_family(120)
):
return out
for layer_type in self._layer_types:
@@ -489,7 +542,7 @@ class DeepseekSparseSWAMetadataBuilder(AttentionMetadataBuilder):
Returns a dict of keyword arguments to pass to the
DeepseekSparseSWAMetadata constructor.
Note: C128A topk indices are computed by the FlashMLASparse builder
Note: C128A sparse metadata is computed by the FlashMLASparse builder
(which owns the C128A block_table), not here.
"""
result: dict[str, torch.Tensor | int | None] = {}
@@ -539,10 +592,14 @@ def _compute_prefill_metadata_kernel(
"""Compute prefill gather_lens in a single pass."""
offset = tl.arange(0, BLOCK_SIZE)
mask = offset < num_prefills
# SM12x + Triton 3.6 raises IMA on out-of-bounds address arithmetic for
# masked-off lanes even though the load mask gates the actual read, so
# clamp the offset. Caller guarantees num_prefills > 0.
safe_offset = tl.minimum(offset, num_prefills - 1)
seq_len = tl.load(seq_lens_ptr + num_decodes + offset, mask=mask)
qsl_start = tl.load(query_start_loc_ptr + num_decodes + offset, mask=mask)
qsl_end = tl.load(query_start_loc_ptr + num_decodes + offset + 1, mask=mask)
seq_len = tl.load(seq_lens_ptr + num_decodes + safe_offset, mask=mask)
qsl_start = tl.load(query_start_loc_ptr + num_decodes + safe_offset, mask=mask)
qsl_end = tl.load(query_start_loc_ptr + num_decodes + safe_offset + 1, mask=mask)
query_len = qsl_end - qsl_start
prefix_len = seq_len - query_len
@@ -551,7 +608,7 @@ def _compute_prefill_metadata_kernel(
tl.store(prefill_gather_lens_ptr + offset, gather_len, mask=mask)
@triton.jit
@triton.jit(do_not_specialize=["token_offset"])
def _compute_swa_indices_and_lens_kernel(
swa_indices_ptr,
swa_indices_stride,
@@ -564,12 +621,14 @@ def _compute_swa_indices_and_lens_kernel(
block_table_ptr,
block_table_stride,
block_size,
token_offset,
TRITON_BLOCK_SIZE: tl.constexpr,
):
token_idx = tl.program_id(0)
pid = tl.program_id(0)
token_idx = pid + token_offset
is_valid = tl.load(is_valid_token_ptr + token_idx)
if not is_valid:
tl.store(swa_lens_ptr + token_idx, 0)
tl.store(swa_lens_ptr + pid, 0)
return
req_idx = tl.load(token_to_req_indices_ptr + token_idx)
@@ -586,7 +645,7 @@ def _compute_swa_indices_and_lens_kernel(
end_pos = pos + 1
swa_len = end_pos - start_pos
tl.store(swa_lens_ptr + token_idx, swa_len)
tl.store(swa_lens_ptr + pid, swa_len)
for i in range(0, window_size, TRITON_BLOCK_SIZE):
offset = i + tl.arange(0, TRITON_BLOCK_SIZE)
@@ -602,7 +661,7 @@ def _compute_swa_indices_and_lens_kernel(
slot_ids = tl.where(offset < swa_len, slot_ids, -1)
tl.store(
swa_indices_ptr + token_idx * swa_indices_stride + offset,
swa_indices_ptr + pid * swa_indices_stride + offset,
slot_ids,
mask=offset < window_size,
)
+5 -1
View File
@@ -71,7 +71,11 @@ class AttentionBackendEnum(Enum, metaclass=_AttentionBackendEnumMeta):
)
FLASHINFER_MLA_SPARSE = (
"vllm.v1.attention.backends.mla.flashinfer_mla_sparse."
"FlashInferMLASparseBackend"
"FlashInferMLASparseTRTLLMBackend"
)
FLASHINFER_MLA_SPARSE_SM120 = (
"vllm.v1.attention.backends.mla.flashinfer_mla_sparse."
"FlashInferMLASparseSM120Backend"
)
TRITON_MLA = "vllm.v1.attention.backends.mla.triton_mla.TritonMLABackend"
CUTLASS_MLA = "vllm.v1.attention.backends.mla.cutlass_mla.CutlassMLABackend"
+1 -1
View File
@@ -73,7 +73,7 @@ def is_flashmla_sparse_supported() -> tuple[bool, str | None]:
):
return (
False,
"FlashMLA Sparse is only supported on Hopper and Blackwell devices.",
"FlashMLA Sparse is only supported on Hopper and Blackwell DC devices.",
)
return True, None
+9 -5
View File
@@ -1872,6 +1872,11 @@ class Scheduler(SchedulerInterface):
if not cached_encoder_input_ids:
return
# Defer the free by the drafter's look-ahead so an entry stays
# referenced until the drafter's +1 read has also passed it, mirroring
# the shift the encoder scheduling path applies.
spec_lookahead = 1 if self.use_eagle else 0
# Here, we use list(set) to avoid modifying the set while iterating
# over it.
for input_id in list(cached_encoder_input_ids):
@@ -1884,13 +1889,12 @@ class Scheduler(SchedulerInterface):
# KVs have been calculated and cached already.
self.encoder_cache_manager.free_encoder_input(request, input_id)
elif (
start_pos + num_tokens
start_pos + num_tokens + spec_lookahead
<= request.num_computed_tokens - request.num_output_placeholders
):
# The encoder output is already processed and stored in the
# decoder's KV cache, and progress is far enough past the
# placeholder range that no pending draft-token rejection can
# roll num_computed_tokens back into it.
# Processed, stored in the decoder KV cache, and far enough past
# the placeholder range (plus the drafter's look-ahead) that no
# rejection or drafter gather can reference it.
self.encoder_cache_manager.free_encoder_input(request, input_id)
def update_draft_token_ids(self, draft_token_ids: DraftTokenIds) -> None:
+38 -5
View File
@@ -12,6 +12,8 @@ import torch
from vllm.logger import init_logger
from vllm.platforms import current_platform
from vllm.v1.simple_kv_offload.cuda_mem_ops import (
CU_MEMCPY_SRC_ACCESS_ORDER_ANY,
CU_MEMCPY_SRC_ACCESS_ORDER_STREAM,
BatchMemcpyParams,
build_params,
copy_blocks,
@@ -43,8 +45,20 @@ class DmaCopyBackend:
self._load_stream = load_stream
self._store_stream = store_stream
self._store_params = build_params(gpu_caches, cpu_caches, store_stream)
self._load_params = build_params(cpu_caches, gpu_caches, load_stream)
# Stores read the live KV cache -> STREAM (paired with the compute-done
# wait in get_finished); loads read stable pinned host memory -> ANY.
self._store_params = build_params(
gpu_caches,
cpu_caches,
store_stream,
src_access_order=CU_MEMCPY_SRC_ACCESS_ORDER_STREAM,
)
self._load_params = build_params(
cpu_caches,
gpu_caches,
load_stream,
src_access_order=CU_MEMCPY_SRC_ACCESS_ORDER_ANY,
)
self._queue = queue.SimpleQueue()
self._thread = threading.Thread(
@@ -61,11 +75,20 @@ class DmaCopyBackend:
is_store: bool,
event_idx: int,
events_list: list[tuple[int, torch.Event]],
wait_event: torch.Event | None = None,
) -> None:
params = self._store_params if is_store else self._load_params
assert params is not None and self._queue is not None
self._queue.put(
(src_blocks, dst_blocks, params, is_store, event_idx, events_list)
(
src_blocks,
dst_blocks,
params,
is_store,
event_idx,
events_list,
wait_event,
)
)
def shutdown(self) -> None:
@@ -89,9 +112,19 @@ class DmaCopyBackend:
item = q.get()
if item is None:
return
src_blocks, dst_blocks, params, is_store, event_idx, events_list = item
copy_blocks(src_blocks, dst_blocks, params)
(
src_blocks,
dst_blocks,
params,
is_store,
event_idx,
events_list,
wait_event,
) = item
stream = store_stream if is_store else load_stream
if wait_event is not None:
stream.wait_event(wait_event)
copy_blocks(src_blocks, dst_blocks, params)
event = torch.Event()
event.record(stream)
events_list.append((event_idx, event))
+10 -6
View File
@@ -13,6 +13,12 @@ from vllm.platforms import current_platform
logger = init_logger(__name__)
# CUmemcpySrcAccessOrder values (CUDA driver API). STREAM(1): source read in
# stream order, safe when the source may still be written. ANY(3): source may
# be read early, only safe for a stable source (e.g. pinned host memory).
CU_MEMCPY_SRC_ACCESS_ORDER_STREAM = 1
CU_MEMCPY_SRC_ACCESS_ORDER_ANY = 3
def pin_tensor(tensor: torch.Tensor) -> None:
"""Pin a CPU tensor via cudaHostRegister.
@@ -106,8 +112,8 @@ class BatchMemcpyParams(NamedTuple):
dst_bases: np.ndarray # [num_layers] uint64
bpb: np.ndarray # [num_layers] uint64 — bytes per block
num_layers: int
# CUDA only: one attributes entry with srcAccessOrder=ANY. Unused on
# ROCm (7.2.1 or 7.2.2) because the current runtime rejects numAttrs > 0.
# CUDA only: one attributes entry carrying srcAccessOrder. Unused on ROCm
# (7.2.1 or 7.2.2) because the current runtime rejects numAttrs > 0.
attrs: _CUmemcpyAttributes
attrs_idx: ctypes.c_size_t
# NOTE: cuMemcpyBatchAsync_v2() removed fail_idx field, but we use
@@ -120,6 +126,7 @@ def build_params(
src_caches: dict[str, torch.Tensor],
dst_caches: dict[str, torch.Tensor],
stream: torch.cuda.Stream,
src_access_order: int = CU_MEMCPY_SRC_ACCESS_ORDER_ANY,
) -> BatchMemcpyParams:
global _batch_memcpy_fn
if _batch_memcpy_fn is None:
@@ -137,10 +144,7 @@ def build_params(
dst_bases.append(d.data_ptr())
bpb.append(s_bpb)
# ``srcAccessOrder=3`` == CU_MEMCPY_SRC_ACCESS_ORDER_ANY /
# hipMemcpySrcAccessOrderAny. See
# https://docs.nvidia.com/cuda/cuda-driver-api/group__CUDA__MEM.html#group__CUDA__MEM_1g6f1ff58e3065df3eb4b573dba77ad31f # noqa: E501
attrs = _CUmemcpyAttributes(srcAccessOrder=3)
attrs = _CUmemcpyAttributes(srcAccessOrder=src_access_order)
return BatchMemcpyParams(
src_bases=np.array(src_bases, dtype=np.uint64),
+13 -5
View File
@@ -57,6 +57,10 @@ class SimpleCPUOffloadWorker:
# Metadata for the current step
self._connector_metadata: SimpleCPUOffloadMetadata | None = None
# Compute-done event recorded before each store; reused across steps
# (get_finished runs once per step, copy queue is FIFO).
self._store_compute_done: torch.Event | None = None
# Pending event index sets, populated in bind_connector_metadata
self._pending_load_event_indices: set[int] = set()
self._pending_store_event_indices: set[int] = set()
@@ -206,9 +210,11 @@ class SimpleCPUOffloadWorker:
) -> tuple[set[str] | None, set[str] | None]:
"""Submit transfers and report completed events to the scheduler.
Called after model execution. The manager only schedules stores for
blocks whose KV data is confirmed computed, so we launch both loads
and stores immediately no deferral or cross-stream sync needed.
Stores (GPU->CPU) read the live KV cache, which the compute stream may
still be writing under v1 overlapped execution, so they are ordered
after a compute-done event recorded on the current stream. Loads
(CPU->GPU) read stable pinned host memory and launch immediately. See
#45704 for the bug and #39306 for the srcAccessOrder rationale.
Returns:
tuple of (finished_sending, finished_recving).
@@ -218,7 +224,6 @@ class SimpleCPUOffloadWorker:
# (1) Submit transfers
metadata = self._connector_metadata
if metadata is not None:
# Launch loads (CPU->GPU).
if metadata.load_cpu_blocks:
self._backend.launch_copy(
metadata.load_cpu_blocks,
@@ -227,14 +232,17 @@ class SimpleCPUOffloadWorker:
event_idx=metadata.load_event,
events_list=self._load_events,
)
# Launch stores (GPU->CPU).
if metadata.store_gpu_blocks:
if self._store_compute_done is None:
self._store_compute_done = torch.Event()
self._store_compute_done.record(torch.cuda.current_stream())
self._backend.launch_copy(
metadata.store_gpu_blocks,
metadata.store_cpu_blocks,
is_store=True,
event_idx=metadata.store_event,
events_list=self._store_events,
wait_event=self._store_compute_done,
)
# (2) Track completed transfer events
+21 -10
View File
@@ -68,15 +68,19 @@ class EncoderRunner:
query_start_loc: np.ndarray,
prefill_lens: np.ndarray,
computed_prefill_lens: np.ndarray,
draft_lookahead: int = 0,
) -> tuple[list[torch.Tensor], torch.Tensor]:
is_prefilling = (computed_prefill_lens < prefill_lens).tolist()
all_decode = not any(is_prefilling)
if all_decode:
if draft_lookahead:
computed_prefill_lens = computed_prefill_lens + draft_lookahead
is_prefilling_np = computed_prefill_lens < prefill_lens
if not is_prefilling_np.any():
# All decode requests, so no need to gather any embeddings.
return [], torch.zeros(
total_num_scheduled_tokens, dtype=torch.bool, device=self.device
)
is_prefilling = is_prefilling_np.tolist()
query_start = computed_prefill_lens.tolist()
query_end = (computed_prefill_lens + num_scheduled_tokens).tolist()
@@ -89,11 +93,12 @@ class EncoderRunner:
# OPTIMIZATION: Skip decode requests.
continue
cur_query_start = query_start[i]
cur_query_end = query_end[i]
mm_features = self.encoder_cache.mm_features[req_id]
lo, hi = get_mm_features_in_window(
mm_features,
start=query_start[i],
end=query_end[i],
mm_features, start=cur_query_start, end=cur_query_end
)
for idx in range(lo, hi):
mm_feature = mm_features[idx]
@@ -101,8 +106,8 @@ class EncoderRunner:
start_pos = pos_info.offset
num_encoder_tokens = pos_info.length
start_idx = max(query_start[i] - start_pos, 0)
end_idx = min(query_end[i] - start_pos, num_encoder_tokens)
start_idx = max(cur_query_start - start_pos, 0)
end_idx = min(cur_query_end - start_pos, num_encoder_tokens)
assert start_idx < end_idx
curr_embeds_start, curr_embeds_end = (
pos_info.get_embeds_indices_in_range(start_idx, end_idx)
@@ -114,7 +119,13 @@ class EncoderRunner:
mm_hash = mm_feature.identifier
encoder_output = self.encoder_cache.encoder_outputs.get(mm_hash, None)
assert encoder_output is not None, f"Encoder cache miss for {mm_hash}."
if encoder_output is None:
# A feature starting at/after the processed boundary is only
# reached via the drafter's +1 look-ahead and might not be
# encoded yet; fall back to the token embedding for drafting.
if start_pos + draft_lookahead >= cur_query_end:
continue
raise RuntimeError(f"Encoder cache miss for {mm_hash}.")
if (is_embed := pos_info.is_embed) is not None:
is_embed = is_embed[start_idx:end_idx]
@@ -122,7 +133,7 @@ class EncoderRunner:
else:
mm_embeds_item = encoder_output[start_idx:end_idx]
req_start_pos = query_start_loc[i] + start_pos - query_start[i]
req_start_pos = query_start_loc[i] + start_pos - cur_query_start
is_mm_embed[req_start_pos + start_idx : req_start_pos + end_idx] |= (
True if is_embed is None else is_embed
)
+4 -2
View File
@@ -314,6 +314,7 @@ class GPUModelRunner(LoRAModelRunnerMixin):
self.model_state = init_model_state(
self.vllm_config, self.model, self.encoder_cache, self.device
)
self.model_state.req_states = self.req_states
self.decode_query_len = (
self.num_speculative_steps
@@ -1409,8 +1410,9 @@ class GPUModelRunner(LoRAModelRunnerMixin):
input_batch.num_scheduled_tokens,
input_batch.query_start_loc_np,
input_batch.prefill_len_np,
# +1 to consider the skew in eagle
input_batch.num_computed_prefill_tokens_np + 1,
input_batch.num_computed_prefill_tokens_np,
# The EAGLE/MTP drafter reads one position ahead of the target.
draft_lookahead=1,
)
# Postprocess results and update request states.
+7 -6
View File
@@ -4,6 +4,7 @@ import torch
import torch.nn as nn
from vllm.config import VllmConfig
from vllm.model_executor.layers.attention import CrossAttention
from vllm.v1.worker.gpu.mm.encoder_cache import EncoderCache
@@ -18,13 +19,13 @@ def init_model_state(
cls = model.get_model_state_cls()
return cls(vllm_config, model, encoder_cache, device)
if (
"WhisperForConditionalGeneration" in vllm_config.model_config.architectures
or "CohereAsrForConditionalGeneration" in vllm_config.model_config.architectures
):
from vllm.v1.worker.gpu.model_states.whisper import WhisperModelState
# Cross-attention encoder-decoder models (Whisper, CohereASR, NemotronParse, ...)
if any(isinstance(m, CrossAttention) for m in model.modules()):
from vllm.v1.worker.gpu.model_states.encoder_decoder import (
EncoderDecoderModelState,
)
return WhisperModelState(vllm_config, model, encoder_cache, device)
return EncoderDecoderModelState(vllm_config, model, encoder_cache, device)
if vllm_config.model_config.is_hybrid:
from vllm.v1.worker.gpu.model_states.mamba_hybrid import MambaHybridModelState
+91 -23
View File
@@ -7,7 +7,8 @@ import torch.nn as nn
from vllm.config import VllmConfig
from vllm.config.compilation import CUDAGraphMode
from vllm.tasks import GenerationTask
from vllm.model_executor.models.interfaces import supports_multimodal_pruning
from vllm.multimodal.utils import get_mm_features_in_window
from vllm.v1.core.sched.output import NewRequestData
from vllm.v1.kv_cache_interface import KVCacheConfig
from vllm.v1.worker.gpu.attn_utils import build_attn_metadata
@@ -33,6 +34,7 @@ class DefaultModelState(ModelState):
self.scheduler_config = vllm_config.scheduler_config
self.model = model
self.device = device
self.req_states: RequestState | None = None
self.supports_mm_inputs = encoder_cache is not None
self.max_model_len = self.model_config.max_model_len
@@ -40,6 +42,11 @@ class DefaultModelState(ModelState):
self.max_num_tokens = self.scheduler_config.max_num_batched_tokens
self.inputs_embeds_size = self.model_config.get_inputs_embeds_size()
self.dtype = self.model_config.dtype
self.is_multimodal_pruning_enabled = (
supports_multimodal_pruning(model)
and self.model_config.multimodal_config is not None
and self.model_config.multimodal_config.is_multimodal_pruning_enabled()
)
if self.supports_mm_inputs:
assert encoder_cache is not None
@@ -62,28 +69,6 @@ class DefaultModelState(ModelState):
device=self.device,
)
def get_supported_generation_tasks(self) -> tuple[GenerationTask, ...]:
from vllm.model_executor.models.interfaces import (
supports_realtime,
supports_transcription,
)
from vllm.model_executor.models.interfaces_base import is_text_generation_model
supported_tasks = list[GenerationTask]()
if is_text_generation_model(self.model):
supported_tasks.append("generate")
if supports_transcription(self.model):
if self.model.supports_transcription_only:
return ("transcription",)
supported_tasks.append("transcription")
if supports_realtime(self.model):
supported_tasks.append("realtime")
return tuple(supported_tasks)
def add_request(self, req_index: int, new_req_data: NewRequestData) -> None:
if self.rope_state is not None:
assert new_req_data.prefill_token_ids is not None
@@ -98,6 +83,82 @@ class DefaultModelState(ModelState):
if self.rope_state is not None:
self.rope_state.apply_staged_writes()
def _recompute_mrope_positions(
self,
mm_embeds: list[torch.Tensor],
input_batch: InputBatch,
) -> list[torch.Tensor]:
assert self.rope_state is not None
assert self.req_states is not None
req_states = self.req_states
mm_embeds_out: list[torch.Tensor] = []
mm_embed_idx = 0
for batch_idx, req_id in enumerate(input_batch.req_ids):
req_idx = req_states.req_id_to_index[req_id]
num_computed_tokens = int(req_states.num_computed_tokens_np[req_idx])
mm_features = self.encoder_cache.mm_features[req_id]
query_start = num_computed_tokens
query_end = query_start + int(input_batch.num_scheduled_tokens[batch_idx])
num_req_mm_embeds = 0
lo, hi = get_mm_features_in_window(
mm_features,
start=query_start,
end=query_end,
)
# iterate and get the mm_embeds num in current window
for mm_feature in mm_features[lo:hi]:
start_pos = mm_feature.mm_position.offset
num_encoder_tokens = mm_feature.mm_position.length
start_idx = max(query_start - start_pos, 0)
end_idx = min(query_end - start_pos, num_encoder_tokens)
curr_embeds_start, curr_embeds_end = (
mm_feature.mm_position.get_embeds_indices_in_range(
start_idx, end_idx
)
)
if curr_embeds_start != curr_embeds_end:
num_req_mm_embeds += 1
if num_req_mm_embeds == 0:
continue
req_mm_embeds = mm_embeds[mm_embed_idx : mm_embed_idx + num_req_mm_embeds]
mm_embed_idx += num_req_mm_embeds
# get prompttoken ids
prompt_len = int(req_states.prompt_len.np[req_idx])
prompt_token_ids = req_states.all_token_ids._uva_buf.np[
req_idx, :prompt_len
].tolist()
# get mrope positions
start = req_idx * self.rope_state.num_dims
end = start + self.rope_state.num_dims
mrope_positions = torch.tensor(
self.rope_state.prefill_positions._uva_buf.np[start:end, :prompt_len],
dtype=torch.long,
)
req_mm_embeds, new_positions, new_delta = (
self.model.recompute_mrope_positions(
input_ids=prompt_token_ids,
multimodal_embeddings=tuple(req_mm_embeds),
mrope_positions=mrope_positions,
num_computed_tokens=num_computed_tokens,
)
)
new_positions_cpu = new_positions.to(device="cpu", dtype=torch.int32)
self.rope_state.prefill_positions._uva_buf.cpu[
start:end, : new_positions_cpu.shape[1]
].copy_(new_positions_cpu)
self.rope_state.prefill_delta.np[req_idx] = new_delta
self.rope_state.prefill_delta.copy_to_uva()
mm_embeds_out.extend(req_mm_embeds)
assert mm_embed_idx == len(mm_embeds)
return mm_embeds_out
def get_mm_embeddings(
self,
scheduled_encoder_inputs: dict[str, list[int]],
@@ -120,6 +181,13 @@ class DefaultModelState(ModelState):
input_batch.prefill_len_np,
input_batch.num_computed_prefill_tokens_np,
)
if (
mm_embeds
and self.is_multimodal_pruning_enabled
and self.rope_state is not None
and self.rope_state.has_delta
):
mm_embeds = self._recompute_mrope_positions(mm_embeds, input_batch)
# Use unpadded input_ids to match is_mm_embed size (num_tokens).
# input_batch.input_ids may be padded for CUDA graphs.
input_ids_unpadded = input_batch.input_ids[: input_batch.num_tokens]
@@ -23,7 +23,7 @@ from vllm.v1.worker.utils import AttentionGroup
@dataclass
class WhisperAttnMetadata(ModelSpecificAttnMetadata):
class EncoderDecoderAttnMetadata(ModelSpecificAttnMetadata):
encoder_seq_lens: dict[int, tuple[torch.Tensor, np.ndarray]]
def get_extra_common_attn_kwargs(
@@ -41,7 +41,11 @@ class WhisperAttnMetadata(ModelSpecificAttnMetadata):
}
class WhisperModelState(ModelState):
class EncoderDecoderModelState(ModelState):
"""ModelState for cross-attention encoder-decoder models
(Whisper, CohereASR, NemotronParse, FireRedLID, ...)
"""
def __init__(
self,
vllm_config: VllmConfig,
@@ -80,9 +84,6 @@ class WhisperModelState(ModelState):
self.encoder_outputs: list[torch.Tensor] = []
def get_supported_generation_tasks(self):
return ("transcription",)
def get_mm_embeddings(
self, scheduled_encoder_inputs: dict[str, list[int]], input_batch: InputBatch
) -> None:
@@ -94,11 +95,11 @@ class WhisperModelState(ModelState):
encoder_inputs[req_id] = req_encoder_inputs
_, mm_kwargs = self.encoder_runner.prepare_mm_inputs(encoder_inputs)
if mm_kwargs:
# Whisper consumes encoder outputs through `encoder_outputs`, not
# `inputs_embeds`. Single modality (audio) so execute_mm_encoder
# preserves request order; use its return value directly.
# No need to store in encoder_cache: cross-attention K/V are written
# to the KV cache on the first step; decode steps use the cache.
# Encoder-decoder models consume encoder outputs through the
# `encoder_outputs` forward kwarg, not `inputs_embeds`. Single modality
# so execute_mm_encoder preserves request order; use its return value
# directly. No need to store in encoder_cache: cross-attention K/V are
# written to the KV cache on the first step; decode steps use the cache.
self.encoder_outputs = self.encoder_runner.execute_mm_encoder(mm_kwargs)
else:
# Decode steps: encoder K/V are in cross-attention KV cache.
@@ -131,7 +132,7 @@ class WhisperModelState(ModelState):
else:
num_reqs = input_batch.num_reqs
num_tokens = input_batch.num_tokens
whisper_attn_metadata = WhisperAttnMetadata(
enc_dec_attn_metadata = EncoderDecoderAttnMetadata(
self._get_encoder_seq_lens(
input_batch.req_ids, attn_groups, for_capture, num_reqs
)
@@ -158,7 +159,7 @@ class WhisperModelState(ModelState):
kv_cache_config=kv_cache_config,
seq_lens_cpu_upper_bound=seq_lens_cpu_upper_bound,
dcp_local_seq_lens=input_batch.dcp_local_seq_lens,
model_specific_attn_metadata=whisper_attn_metadata,
model_specific_attn_metadata=enc_dec_attn_metadata,
for_cudagraph_capture=for_capture,
)
return attn_metadata
+18 -2
View File
@@ -46,9 +46,25 @@ class ModelState(ABC):
) -> None:
raise NotImplementedError
@abstractmethod
model: nn.Module
def get_supported_generation_tasks(self) -> tuple[GenerationTask, ...]:
raise NotImplementedError
from vllm.model_executor.models.interfaces import (
supports_realtime,
supports_transcription,
)
from vllm.model_executor.models.interfaces_base import is_text_generation_model
supported_tasks = list[GenerationTask]()
if is_text_generation_model(self.model):
supported_tasks.append("generate")
if supports_transcription(self.model):
if self.model.supports_transcription_only:
return ("transcription",)
supported_tasks.append("transcription")
if supports_realtime(self.model):
supported_tasks.append("realtime")
return tuple(supported_tasks)
def add_request(self, req_index: int, new_req_data: NewRequestData) -> None:
return None
+130
View File
@@ -2,12 +2,14 @@
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
from collections.abc import Callable
from contextlib import AbstractContextManager, nullcontext
from typing import Any
import numpy as np
import torch
from vllm import PoolingParams, SamplingParams
from vllm.logger import init_logger
from vllm.utils.math_utils import cdiv
from vllm.v1.core.sched.output import (
CachedRequestData,
@@ -18,6 +20,134 @@ from vllm.v1.core.sched.output import (
from vllm.v1.request import Request
from vllm.v1.worker.gpu.model_runner import GPUModelRunner
logger = init_logger(__name__)
def run_mixed_prefill_decode_warmup(
model_runner: GPUModelRunner,
worker_execute_model: Callable[[SchedulerOutput], Any],
worker_sample_tokens: Callable[[GrammarOutput | None], Any],
num_tokens: int,
*,
mixed_step_context: AbstractContextManager[object] | None = None,
req_id_prefix: str = "_v2_mixed_warmup",
) -> bool:
"""Run a V2 mixed prefill+decode step through normal scheduler inputs."""
if model_runner.is_pooling_model or num_tokens < 3:
return False
decode_req_id = f"{req_id_prefix}_decode_"
prefill_req_id = f"{req_id_prefix}_prefill_"
decode_prompt_len = 2
decode_scheduled_tokens = 1
prefill_len = num_tokens - decode_scheduled_tokens
decode_token_ids = list(range(decode_prompt_len))
prefill_token_ids = list(range(prefill_len))
kv_cache_groups = model_runner.kv_cache_config.kv_cache_groups
num_kv_cache_groups = len(kv_cache_groups)
group_block_sizes = [g.kv_cache_spec.block_size for g in kv_cache_groups]
decode_prefill_block_counts = [
cdiv(decode_prompt_len, block_size) for block_size in group_block_sizes
]
decode_block_counts = [
cdiv(decode_prompt_len + decode_scheduled_tokens, block_size)
for block_size in group_block_sizes
]
decode_block_deltas = [
decode - prefill
for decode, prefill in zip(decode_block_counts, decode_prefill_block_counts)
]
prefill_block_counts = [
cdiv(prefill_len, block_size) for block_size in group_block_sizes
]
required_blocks = sum(decode_block_counts) + sum(prefill_block_counts)
if model_runner.kv_cache_config.num_blocks <= required_blocks:
logger.warning(
"Skipping V2 mixed prefill+decode warmup because only %d KV blocks "
"are available for %d required warmup blocks.",
model_runner.kv_cache_config.num_blocks,
required_blocks,
)
return False
next_block_id = 1
def _alloc_blocks(num_blocks: int) -> list[int]:
nonlocal next_block_id
block_ids = list(range(next_block_id, next_block_id + num_blocks))
next_block_id += num_blocks
return block_ids
sampling_params = SamplingParams(max_tokens=2, temperature=0.0)
decode_prefill_output = SchedulerOutput.make_empty()
decode_prefill_output.scheduled_new_reqs = [
NewRequestData(
req_id=decode_req_id,
prompt_token_ids=decode_token_ids,
mm_features=[],
sampling_params=sampling_params,
pooling_params=None,
block_ids=tuple(_alloc_blocks(n) for n in decode_prefill_block_counts),
num_computed_tokens=0,
lora_request=None,
prefill_token_ids=decode_token_ids,
),
]
decode_prefill_output.num_scheduled_tokens = {
decode_req_id: decode_prompt_len,
}
decode_prefill_output.total_num_scheduled_tokens = decode_prompt_len
decode_prefill_output.num_common_prefix_blocks = [0] * num_kv_cache_groups
decode_new_blocks = tuple(_alloc_blocks(n) for n in decode_block_deltas)
cached_decode_req = CachedRequestData.make_empty()
cached_decode_req.req_ids = [decode_req_id]
cached_decode_req.num_computed_tokens = [decode_prompt_len]
cached_decode_req.num_output_tokens = [1]
cached_decode_req.new_block_ids = [
decode_new_blocks if any(decode_block_deltas) else None
]
mixed_output = SchedulerOutput.make_empty()
mixed_output.scheduled_cached_reqs = cached_decode_req
mixed_output.scheduled_new_reqs = [
NewRequestData(
req_id=prefill_req_id,
prompt_token_ids=prefill_token_ids,
mm_features=[],
sampling_params=sampling_params,
pooling_params=None,
block_ids=tuple(_alloc_blocks(n) for n in prefill_block_counts),
num_computed_tokens=0,
lora_request=None,
prefill_token_ids=prefill_token_ids,
),
]
mixed_output.num_scheduled_tokens = {
decode_req_id: decode_scheduled_tokens,
prefill_req_id: prefill_len,
}
mixed_output.total_num_scheduled_tokens = num_tokens
mixed_output.num_common_prefix_blocks = [0] * num_kv_cache_groups
cleanup_output = SchedulerOutput.make_empty()
cleanup_output.finished_req_ids = {decode_req_id, prefill_req_id}
context = mixed_step_context or nullcontext()
model_runner.kv_connector.set_disabled(True)
try:
worker_execute_model(decode_prefill_output)
worker_sample_tokens(None)
with context:
worker_execute_model(mixed_output)
worker_sample_tokens(None)
worker_execute_model(cleanup_output)
finally:
model_runner.kv_connector.set_disabled(False)
return True
@torch.inference_mode()
def warmup_kernels(
+10 -1
View File
@@ -3152,7 +3152,16 @@ class GPUModelRunner(
mm_hash = mm_feature.identifier
encoder_output = self.encoder_cache.get(mm_hash, None)
assert encoder_output is not None, f"Encoder cache miss for {mm_hash}."
if encoder_output is None:
# A feature starting at/after the processed boundary is only
# reached via the drafter's +1 look-ahead and might not be
# encoded yet; fall back to the token embedding for drafting.
if (
start_pos
>= req_state.num_computed_tokens + num_scheduled_tokens
):
continue
raise RuntimeError(f"Encoder cache miss for {mm_hash}.")
if (is_embed := pos_info.is_embed) is not None:
is_embed = is_embed[start_idx:end_idx]