forked from Karylab-cklius/vllm
Compare commits
36
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8b00f41237 | ||
|
|
0586a20184 | ||
|
|
7a672233eb | ||
|
|
ea890c8066 | ||
|
|
8717eccaf4 | ||
|
|
9ac6a8ed61 | ||
|
|
ef762c26e5 | ||
|
|
985d00a751 | ||
|
|
70ae0bb9e5 | ||
|
|
18d9bd9399 | ||
|
|
28ee7f57df | ||
|
|
c12ed89131 | ||
|
|
703e492aab | ||
|
|
5f0bafab12 | ||
|
|
545393a3ab | ||
|
|
d71445af45 | ||
|
|
eb8e264edd | ||
|
|
8f5070c447 | ||
|
|
934fa2b599 | ||
|
|
713fb6cdb7 | ||
|
|
ebfbcfe46a | ||
|
|
e9de72fe6c | ||
|
|
d272418f45 | ||
|
|
7ff7f5c8eb | ||
|
|
dced290769 | ||
|
|
93bad11912 | ||
|
|
0fbf42af84 | ||
|
|
e6cd8913dd | ||
|
|
859e4d436b | ||
|
|
4a083cc858 | ||
|
|
ca7e1f2c43 | ||
|
|
dec860fb19 | ||
|
|
0a49fb2b13 | ||
|
|
4a8abf37c7 | ||
|
|
01192139bf | ||
|
|
b9a7cd464c |
@@ -647,7 +647,7 @@ steps:
|
||||
- pytest -v -s v1/cudagraph/test_cudagraph_mode.py
|
||||
|
||||
- label: e2e Core (1 GPU) # TBD
|
||||
timeout_in_minutes: 180
|
||||
timeout_in_minutes: 35
|
||||
mirror_hardwares: [amdexperimental, amdproduction, amdgfx90anightly, amdmi250]
|
||||
agent_pool: mi250_1
|
||||
optional: true
|
||||
@@ -2075,19 +2075,6 @@ steps:
|
||||
- export VLLM_ALLOW_INSECURE_SERIALIZATION=1
|
||||
- pytest -v -s v1/spec_decode/test_acceptance_length.py -m slow_test
|
||||
|
||||
- label: e2e Core (1 GPU) # TBD
|
||||
timeout_in_minutes: 180
|
||||
mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300]
|
||||
agent_pool: mi300_1
|
||||
optional: true
|
||||
working_dir: "/vllm-workspace/tests"
|
||||
source_file_dependencies:
|
||||
- vllm/v1/
|
||||
- tests/v1/e2e/
|
||||
- vllm/platforms/rocm.py
|
||||
commands:
|
||||
- pytest -v -s v1/e2e/general --ignore v1/e2e/general/test_async_scheduling.py
|
||||
|
||||
- label: e2e Scheduling (1 GPU) # TBD
|
||||
timeout_in_minutes: 180
|
||||
mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi300]
|
||||
|
||||
@@ -74,6 +74,16 @@ steps:
|
||||
- tests/v1/e2e/general/
|
||||
commands:
|
||||
- pytest -v -s v1/e2e/general --ignore v1/e2e/general/test_async_scheduling.py
|
||||
mirror:
|
||||
amd:
|
||||
device: mi250_1
|
||||
timeout_in_minutes: 35
|
||||
depends_on:
|
||||
- image-build-amd
|
||||
source_file_dependencies:
|
||||
- vllm/v1/
|
||||
- tests/v1/e2e/general/
|
||||
- vllm/platforms/rocm.py
|
||||
|
||||
- label: V1 e2e (2 GPUs)
|
||||
key: v1-e2e-2-gpus
|
||||
|
||||
@@ -104,6 +104,7 @@ steps:
|
||||
source_file_dependencies:
|
||||
- csrc/quantization/
|
||||
- vllm/model_executor/layers/quantization
|
||||
- vllm/config/
|
||||
- tests/kernels/quantization
|
||||
- tests/kernels/quantization/test_rocm_skinny_gemms.py
|
||||
- vllm/_aiter_ops.py
|
||||
|
||||
@@ -101,6 +101,15 @@ steps:
|
||||
num_devices: 8
|
||||
commands:
|
||||
- pytest -s -v evals/gsm8k/test_gsm8k_correctness.py --config-list-file=configs/models-h200.txt
|
||||
mirror:
|
||||
amd:
|
||||
device: mi300_8
|
||||
timeout_in_minutes: 180
|
||||
depends_on:
|
||||
- image-build-amd
|
||||
commands:
|
||||
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
|
||||
- pytest -s -v evals/gsm8k/test_gsm8k_correctness.py --config-list-file=configs/models-mi3xx.txt
|
||||
|
||||
- label: MoE Refactor Integration Test (H100 - TEMPORARY)
|
||||
key: moe-refactor-integration-test-h100-temporary
|
||||
|
||||
@@ -68,7 +68,6 @@ steps:
|
||||
- cd .. && VLLM_WORKER_MULTIPROC_METHOD=spawn pytest -v -s tests/models/multimodal/generation/test_whisper.py -m core_model # Otherwise, mp_method="spawn" doesn't work
|
||||
mirror:
|
||||
amd:
|
||||
soft_fail: true
|
||||
device: mi325_1
|
||||
depends_on:
|
||||
- image-build-amd
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
---
|
||||
name: ci-fails-buildkite
|
||||
description: Fetch and diagnose vLLM Buildkite CI failure logs. Use when investigating failing CI jobs on a PR or build, when the user pastes a buildkite.com URL, or asks to fetch/diagnose CI logs.
|
||||
---
|
||||
|
||||
# Diagnosing vLLM Buildkite CI Failures
|
||||
|
||||
Buildkite logs are public; no login needed.
|
||||
|
||||
`.buildkite/scripts/ci-fetch-log.sh` saves each log as `ci-<build>-<job-name>.log`, stripped of timestamps and ANSI codes. Existing files are kept; set `CI_FETCH_LOG_FORCE=1` to refetch.
|
||||
|
||||
## Fetching logs
|
||||
|
||||
```bash
|
||||
# All failed jobs in a PR's latest build (current branch's PR if omitted):
|
||||
.buildkite/scripts/ci-fetch-log.sh --pr <PR>
|
||||
|
||||
# All failed jobs in a build (--soft also includes soft-failed jobs;
|
||||
# --all fetches every finished job):
|
||||
.buildkite/scripts/ci-fetch-log.sh "https://buildkite.com/vllm/ci/builds/<N>"
|
||||
|
||||
# One job — `gh pr checks` URLs (#<job_uuid>) and web UI URLs (?sid=) both
|
||||
# work; pass "-" as a second argument to stream to stdout:
|
||||
.buildkite/scripts/ci-fetch-log.sh "https://buildkite.com/vllm/ci/builds/<N>#<job_uuid>"
|
||||
```
|
||||
|
||||
To clean an already-downloaded log with `.buildkite/scripts/ci-clean-log.sh`:
|
||||
|
||||
```bash
|
||||
./ci-clean-log.sh ci.log
|
||||
```
|
||||
|
||||
## Reference
|
||||
|
||||
See [docs/contributing/ci/failures.md](../../../docs/contributing/ci/failures.md) for the full guide: filing CI failure issues, investigating/bisecting, reproducing flaky tests, and daily triage.
|
||||
+2
-3
@@ -2,15 +2,14 @@
|
||||
# for more info about CODEOWNERS file
|
||||
|
||||
# This lists cover the "core" components of vLLM that require careful review
|
||||
/vllm/compilation @zou3519 @youkaichao @ProExpertProg @BoyuanFeng @vadiklyutiy
|
||||
/vllm/compilation @zou3519 @youkaichao @ProExpertProg @BoyuanFeng
|
||||
/vllm/distributed/kv_transfer @NickLucche @ApostaC @orozery @xuechendi
|
||||
/vllm/lora @jeejeelee
|
||||
/vllm/model_executor/layers/attention @LucasWilkinson @MatthewBonanni
|
||||
/vllm/model_executor/layers/fused_moe @mgoin @pavanimajety @zyongye
|
||||
/vllm/model_executor/layers/quantization @mgoin @robertgshaw2-redhat @tlrmchlsmth @yewentao256 @pavanimajety @zyongye
|
||||
/vllm/model_executor/layers/mamba @tdoublep @tomeras91
|
||||
/vllm/model_executor/layers/mamba/gdn_linear_attn.py @tdoublep @ZJY0516 @vadiklyutiy
|
||||
/vllm/model_executor/layers/rotary_embedding.py @vadiklyutiy
|
||||
/vllm/model_executor/layers/mamba/gdn/qwen_gdn_linear_attn.py @tdoublep @ZJY0516 @vadiklyutiy
|
||||
/vllm/model_executor/model_loader @22quinn
|
||||
/vllm/model_executor/layers/batch_invariant.py @yewentao256
|
||||
/vllm/ir @ProExpertProg
|
||||
|
||||
+3
-1
@@ -199,7 +199,9 @@ cython_debug/
|
||||
.vscode/
|
||||
|
||||
# Claude
|
||||
.claude/
|
||||
.claude/*
|
||||
!.claude/skills/
|
||||
!.claude/skills/**
|
||||
|
||||
# Codex
|
||||
.codex/
|
||||
|
||||
@@ -114,17 +114,6 @@ Follow these rules for all code changes in this repository:
|
||||
- Keep comments and docstrings minimal and concise.
|
||||
- Assume the reader is familiar with vLLM.
|
||||
|
||||
### Diagnosing CI failures
|
||||
|
||||
Buildkite logs are public; no login needed. Details: [docs/contributing/ci/failures.md](docs/contributing/ci/failures.md).
|
||||
|
||||
```bash
|
||||
# All failed-job logs for a PR's latest build (current branch's PR if omitted):
|
||||
.buildkite/scripts/ci-fetch-log.sh --pr <PR>
|
||||
# Any Buildkite build or job URL also works:
|
||||
.buildkite/scripts/ci-fetch-log.sh "<buildkite_url>"
|
||||
```
|
||||
|
||||
### Commit messages
|
||||
|
||||
Add attribution using commit trailers such as `Co-authored-by:` (other projects use `Assisted-by:` or `Generated-by:`). For example:
|
||||
|
||||
+56
-72
@@ -319,82 +319,35 @@ if(VLLM_GPU_LANG STREQUAL "CUDA" OR VLLM_GPU_LANG STREQUAL "HIP")
|
||||
endif()
|
||||
|
||||
#
|
||||
# _C extension
|
||||
# Legacy _C extension (ROCm only — CUDA ops migrated to _C_stable_libtorch)
|
||||
#
|
||||
|
||||
set(VLLM_EXT_SRC
|
||||
"csrc/quantization/activation_kernels.cu"
|
||||
"csrc/torch_bindings.cpp")
|
||||
|
||||
if(VLLM_GPU_LANG STREQUAL "CUDA")
|
||||
SET(CUTLASS_ENABLE_HEADERS_ONLY ON CACHE BOOL "Enable only the header library")
|
||||
|
||||
# Set CUTLASS_REVISION. Used for FetchContent. Also fixes some bogus messages when building.
|
||||
set(CUTLASS_REVISION "v4.4.2")
|
||||
|
||||
# Use the specified CUTLASS source directory for compilation if VLLM_CUTLASS_SRC_DIR is provided
|
||||
if (DEFINED ENV{VLLM_CUTLASS_SRC_DIR})
|
||||
set(VLLM_CUTLASS_SRC_DIR $ENV{VLLM_CUTLASS_SRC_DIR})
|
||||
endif()
|
||||
|
||||
if(VLLM_CUTLASS_SRC_DIR)
|
||||
if(NOT IS_ABSOLUTE VLLM_CUTLASS_SRC_DIR)
|
||||
get_filename_component(VLLM_CUTLASS_SRC_DIR "${VLLM_CUTLASS_SRC_DIR}" ABSOLUTE)
|
||||
endif()
|
||||
message(STATUS "The VLLM_CUTLASS_SRC_DIR is set, using ${VLLM_CUTLASS_SRC_DIR} for compilation")
|
||||
FetchContent_Declare(cutlass SOURCE_DIR ${VLLM_CUTLASS_SRC_DIR})
|
||||
else()
|
||||
FetchContent_Declare(
|
||||
cutlass
|
||||
GIT_REPOSITORY https://github.com/nvidia/cutlass.git
|
||||
# Please keep this in sync with CUTLASS_REVISION line above.
|
||||
GIT_TAG ${CUTLASS_REVISION}
|
||||
GIT_PROGRESS TRUE
|
||||
|
||||
# Speed up CUTLASS download by retrieving only the specified GIT_TAG instead of the history.
|
||||
# Important: If GIT_SHALLOW is enabled then GIT_TAG works only with branch names and tags.
|
||||
# So if the GIT_TAG above is updated to a commit hash, GIT_SHALLOW must be set to FALSE
|
||||
GIT_SHALLOW TRUE
|
||||
)
|
||||
endif()
|
||||
FetchContent_MakeAvailable(cutlass)
|
||||
|
||||
set_gencode_flags_for_srcs(
|
||||
SRCS "${VLLM_EXT_SRC}"
|
||||
CUDA_ARCHS "${CUDA_ARCHS}")
|
||||
|
||||
# if CUDA endif
|
||||
endif()
|
||||
|
||||
if (VLLM_GPU_LANG STREQUAL "HIP")
|
||||
# Add QuickReduce kernels (ROCm-only; not part of stable ABI migration).
|
||||
# TODO: Remove the cuda_view when ROCm upgrade to torch 2.11.
|
||||
list(APPEND VLLM_EXT_SRC
|
||||
if(VLLM_GPU_LANG STREQUAL "HIP")
|
||||
set(VLLM_EXT_SRC
|
||||
"csrc/torch_bindings.cpp"
|
||||
"csrc/custom_quickreduce.cu"
|
||||
"csrc/cuda_view.cu"
|
||||
"csrc/libtorch_stable/cuda_utils_kernels.cu"
|
||||
)
|
||||
# if ROCM endif
|
||||
endif()
|
||||
"csrc/libtorch_stable/cuda_utils_kernels.cu")
|
||||
|
||||
message(STATUS "Enabling C extension.")
|
||||
define_extension_target(
|
||||
_C
|
||||
DESTINATION vllm
|
||||
LANGUAGE ${VLLM_GPU_LANG}
|
||||
SOURCES ${VLLM_EXT_SRC}
|
||||
COMPILE_FLAGS ${VLLM_GPU_FLAGS}
|
||||
ARCHITECTURES ${VLLM_GPU_ARCHES}
|
||||
INCLUDE_DIRECTORIES ${CUTLASS_INCLUDE_DIR}
|
||||
INCLUDE_DIRECTORIES ${CUTLASS_TOOLS_UTIL_INCLUDE_DIR}
|
||||
USE_SABI 3
|
||||
WITH_SOABI)
|
||||
message(STATUS "Enabling C extension.")
|
||||
define_extension_target(
|
||||
_C
|
||||
DESTINATION vllm
|
||||
LANGUAGE ${VLLM_GPU_LANG}
|
||||
SOURCES ${VLLM_EXT_SRC}
|
||||
COMPILE_FLAGS ${VLLM_GPU_FLAGS}
|
||||
ARCHITECTURES ${VLLM_GPU_ARCHES}
|
||||
INCLUDE_DIRECTORIES ${CUTLASS_INCLUDE_DIR}
|
||||
INCLUDE_DIRECTORIES ${CUTLASS_TOOLS_UTIL_INCLUDE_DIR}
|
||||
USE_SABI 3
|
||||
WITH_SOABI)
|
||||
|
||||
# If CUTLASS is compiled on NVCC >= 12.5, it by default uses
|
||||
# cudaGetDriverEntryPointByVersion as a wrapper to avoid directly calling the
|
||||
# driver API. This causes problems when linking with earlier versions of CUDA.
|
||||
# Setting this variable sidesteps the issue by calling the driver directly.
|
||||
target_compile_definitions(_C PRIVATE CUTLASS_ENABLE_DIRECT_CUDA_DRIVER_CALL=1)
|
||||
# If CUTLASS is compiled on NVCC >= 12.5, it by default uses
|
||||
# cudaGetDriverEntryPointByVersion as a wrapper to avoid directly calling the
|
||||
# driver API. This causes problems when linking with earlier versions of CUDA.
|
||||
# Setting this variable sidesteps the issue by calling the driver directly.
|
||||
target_compile_definitions(_C PRIVATE CUTLASS_ENABLE_DIRECT_CUDA_DRIVER_CALL=1)
|
||||
endif() # _C HIP endif
|
||||
|
||||
if(VLLM_GPU_LANG STREQUAL "CUDA" OR VLLM_GPU_LANG STREQUAL "HIP")
|
||||
#
|
||||
@@ -403,6 +356,7 @@ if(VLLM_GPU_LANG STREQUAL "CUDA" OR VLLM_GPU_LANG STREQUAL "HIP")
|
||||
set(VLLM_STABLE_EXT_SRC
|
||||
"csrc/libtorch_stable/torch_bindings.cpp"
|
||||
"csrc/libtorch_stable/activation_kernels.cu"
|
||||
"csrc/libtorch_stable/quantization/activation_kernels.cu"
|
||||
"csrc/libtorch_stable/quantization/w8a8/int8/scaled_quant.cu"
|
||||
"csrc/libtorch_stable/quantization/w8a8/fp8/common.cu"
|
||||
"csrc/libtorch_stable/quantization/w8a8/fp8/per_token_group_quant.cu"
|
||||
@@ -429,6 +383,38 @@ if(VLLM_GPU_LANG STREQUAL "CUDA" OR VLLM_GPU_LANG STREQUAL "HIP")
|
||||
"csrc/libtorch_stable/fused_deepseek_v4_qnorm_rope_kv_insert_kernel.cu")
|
||||
|
||||
if(VLLM_GPU_LANG STREQUAL "CUDA")
|
||||
SET(CUTLASS_ENABLE_HEADERS_ONLY ON CACHE BOOL "Enable only the header library")
|
||||
|
||||
# Set CUTLASS_REVISION. Used for FetchContent. Also fixes some bogus messages when building.
|
||||
set(CUTLASS_REVISION "v4.4.2")
|
||||
|
||||
# Use the specified CUTLASS source directory for compilation if VLLM_CUTLASS_SRC_DIR is provided
|
||||
if (DEFINED ENV{VLLM_CUTLASS_SRC_DIR})
|
||||
set(VLLM_CUTLASS_SRC_DIR $ENV{VLLM_CUTLASS_SRC_DIR})
|
||||
endif()
|
||||
|
||||
if(VLLM_CUTLASS_SRC_DIR)
|
||||
if(NOT IS_ABSOLUTE VLLM_CUTLASS_SRC_DIR)
|
||||
get_filename_component(VLLM_CUTLASS_SRC_DIR "${VLLM_CUTLASS_SRC_DIR}" ABSOLUTE)
|
||||
endif()
|
||||
message(STATUS "The VLLM_CUTLASS_SRC_DIR is set, using ${VLLM_CUTLASS_SRC_DIR} for compilation")
|
||||
FetchContent_Declare(cutlass SOURCE_DIR ${VLLM_CUTLASS_SRC_DIR})
|
||||
else()
|
||||
FetchContent_Declare(
|
||||
cutlass
|
||||
GIT_REPOSITORY https://github.com/nvidia/cutlass.git
|
||||
# Please keep this in sync with CUTLASS_REVISION line above.
|
||||
GIT_TAG ${CUTLASS_REVISION}
|
||||
GIT_PROGRESS TRUE
|
||||
|
||||
# Speed up CUTLASS download by retrieving only the specified GIT_TAG instead of the history.
|
||||
# Important: If GIT_SHALLOW is enabled then GIT_TAG works only with branch names and tags.
|
||||
# So if the GIT_TAG above is updated to a commit hash, GIT_SHALLOW must be set to FALSE
|
||||
GIT_SHALLOW TRUE
|
||||
)
|
||||
endif()
|
||||
FetchContent_MakeAvailable(cutlass)
|
||||
|
||||
list(APPEND VLLM_STABLE_EXT_SRC
|
||||
"csrc/libtorch_stable/cuda_view.cu"
|
||||
"csrc/libtorch_stable/cuda_utils_kernels.cu"
|
||||
@@ -929,7 +915,6 @@ if(VLLM_GPU_LANG STREQUAL "CUDA" OR VLLM_GPU_LANG STREQUAL "HIP")
|
||||
SRCS "${FP4_SM120_SRCS}"
|
||||
CUDA_ARCHS "${FP4_SM120_ARCHS}")
|
||||
list(APPEND VLLM_STABLE_EXT_SRC "${FP4_SM120_SRCS}")
|
||||
target_compile_definitions(_C PRIVATE ENABLE_NVFP4_SM120=1)
|
||||
list(APPEND VLLM_GPU_FLAGS "-DENABLE_NVFP4_SM120=1")
|
||||
list(APPEND VLLM_GPU_FLAGS "-DENABLE_CUTLASS_MOE_SM120=1")
|
||||
message(STATUS "Building SM12x NVFP4 for archs: ${FP4_SM120_ARCHS}")
|
||||
@@ -962,7 +947,6 @@ if(VLLM_GPU_LANG STREQUAL "CUDA" OR VLLM_GPU_LANG STREQUAL "HIP")
|
||||
SRCS "${FP4_SM100_SRCS}"
|
||||
CUDA_ARCHS "${FP4_SM100_ARCHS}")
|
||||
list(APPEND VLLM_STABLE_EXT_SRC "${FP4_SM100_SRCS}")
|
||||
target_compile_definitions(_C PRIVATE ENABLE_NVFP4_SM100=1)
|
||||
list(APPEND VLLM_GPU_FLAGS "-DENABLE_NVFP4_SM100=1")
|
||||
list(APPEND VLLM_GPU_FLAGS "-DENABLE_CUTLASS_MOE_SM100=1")
|
||||
message(STATUS "Building SM10x/11x NVFP4/MXFP4 for archs: ${FP4_SM100_ARCHS}")
|
||||
|
||||
@@ -17,7 +17,7 @@ else()
|
||||
FetchContent_Declare(
|
||||
fmha_sm100
|
||||
GIT_REPOSITORY https://github.com/vllm-project/MSA.git
|
||||
GIT_TAG 544eee5e09ae2dfa774d5b06739013f9b7402c57
|
||||
GIT_TAG fee783153f3efe57e3e933c5cb7e267a7cebcfb5
|
||||
GIT_PROGRESS TRUE
|
||||
CONFIGURE_COMMAND ""
|
||||
BUILD_COMMAND ""
|
||||
@@ -36,13 +36,38 @@ set(FMHA_SM100_PY_ROOT "${fmha_sm100_SOURCE_DIR}/python/fmha_sm100")
|
||||
|
||||
install(FILES
|
||||
"${FMHA_SM100_PY_ROOT}/__init__.py"
|
||||
"${FMHA_SM100_PY_ROOT}/api.py"
|
||||
"${FMHA_SM100_PY_ROOT}/bench_utils.py"
|
||||
"${FMHA_SM100_PY_ROOT}/jit.py"
|
||||
"${FMHA_SM100_PY_ROOT}/sparse.py"
|
||||
"${FMHA_SM100_PY_ROOT}/sparse_fmha_adapter.py"
|
||||
DESTINATION vllm/third_party/fmha_sm100
|
||||
COMPONENT fmha_sm100)
|
||||
|
||||
install(DIRECTORY "${FMHA_SM100_PY_ROOT}/csrc/"
|
||||
DESTINATION vllm/third_party/fmha_sm100/csrc
|
||||
COMPONENT fmha_sm100
|
||||
PATTERN "__pycache__" EXCLUDE
|
||||
PATTERN "*.pyc" EXCLUDE
|
||||
PATTERN ".git*" EXCLUDE)
|
||||
|
||||
install(DIRECTORY "${FMHA_SM100_PY_ROOT}/cute/"
|
||||
DESTINATION vllm/third_party/fmha_sm100/cute
|
||||
COMPONENT fmha_sm100
|
||||
PATTERN "__pycache__" EXCLUDE
|
||||
PATTERN "*.pyc" EXCLUDE
|
||||
PATTERN ".git*" EXCLUDE)
|
||||
|
||||
install(DIRECTORY "${FMHA_SM100_PY_ROOT}/cutlass/include/"
|
||||
DESTINATION vllm/third_party/fmha_sm100/cutlass/include
|
||||
COMPONENT fmha_sm100
|
||||
PATTERN "__pycache__" EXCLUDE
|
||||
PATTERN "*.pyc" EXCLUDE
|
||||
PATTERN ".git*" EXCLUDE)
|
||||
|
||||
install(DIRECTORY "${FMHA_SM100_PY_ROOT}/cutlass/tools/util/include/"
|
||||
DESTINATION vllm/third_party/fmha_sm100/cutlass/tools/util/include
|
||||
COMPONENT fmha_sm100
|
||||
PATTERN "__pycache__" EXCLUDE
|
||||
PATTERN "*.pyc" EXCLUDE
|
||||
PATTERN ".git*" EXCLUDE)
|
||||
|
||||
@@ -60,6 +60,7 @@ endif()
|
||||
|
||||
if(${CMAKE_CUDA_COMPILER_VERSION} VERSION_GREATER_EQUAL 12.8 AND QUTLASS_ARCHS)
|
||||
set(QUTLASS_SOURCES
|
||||
csrc/qutlass_registration.cpp
|
||||
${qutlass_SOURCE_DIR}/qutlass/csrc/bindings.cpp
|
||||
${qutlass_SOURCE_DIR}/qutlass/csrc/gemm.cu
|
||||
${qutlass_SOURCE_DIR}/qutlass/csrc/gemm_ada.cu
|
||||
@@ -78,8 +79,19 @@ if(${CMAKE_CUDA_COMPILER_VERSION} VERSION_GREATER_EQUAL 12.8 AND QUTLASS_ARCHS)
|
||||
|
||||
if(CUTLASS_INCLUDE_DIR AND EXISTS "${CUTLASS_INCLUDE_DIR}/cutlass/cutlass.h")
|
||||
list(APPEND QUTLASS_INCLUDES "${CUTLASS_INCLUDE_DIR}")
|
||||
if(CUTLASS_TOOLS_UTIL_INCLUDE_DIR AND
|
||||
EXISTS "${CUTLASS_TOOLS_UTIL_INCLUDE_DIR}/cutlass/util/packed_stride.hpp")
|
||||
list(APPEND QUTLASS_INCLUDES "${CUTLASS_TOOLS_UTIL_INCLUDE_DIR}")
|
||||
else()
|
||||
get_filename_component(_qutlass_cutlass_root "${CUTLASS_INCLUDE_DIR}" DIRECTORY)
|
||||
if(EXISTS "${_qutlass_cutlass_root}/tools/util/include/cutlass/util/packed_stride.hpp")
|
||||
list(APPEND QUTLASS_INCLUDES "${_qutlass_cutlass_root}/tools/util/include")
|
||||
endif()
|
||||
endif()
|
||||
elseif(EXISTS "${qutlass_SOURCE_DIR}/qutlass/third_party/cutlass/include/cutlass/cutlass.h")
|
||||
list(APPEND QUTLASS_INCLUDES "${qutlass_SOURCE_DIR}/qutlass/third_party/cutlass/include")
|
||||
list(APPEND QUTLASS_INCLUDES
|
||||
"${qutlass_SOURCE_DIR}/qutlass/third_party/cutlass/include"
|
||||
"${qutlass_SOURCE_DIR}/qutlass/third_party/cutlass/tools/util/include")
|
||||
message(STATUS "[QUTLASS] Using QuTLASS vendored CUTLASS headers (no vLLM CUTLASS detected).")
|
||||
else()
|
||||
message(FATAL_ERROR "[QUTLASS] CUTLASS headers not found. "
|
||||
@@ -91,12 +103,23 @@ if(${CMAKE_CUDA_COMPILER_VERSION} VERSION_GREATER_EQUAL 12.8 AND QUTLASS_ARCHS)
|
||||
CUDA_ARCHS "${QUTLASS_ARCHS}"
|
||||
)
|
||||
|
||||
target_sources(_C PRIVATE ${QUTLASS_SOURCES})
|
||||
target_include_directories(_C PRIVATE ${QUTLASS_INCLUDES})
|
||||
target_compile_definitions(_C PRIVATE
|
||||
# QuTLASS uses legacy ATen headers and cannot be built with TORCH_TARGET_VERSION.
|
||||
# Keep it as its own extension (registers torch.ops._qutlass_C).
|
||||
define_extension_target(
|
||||
_qutlass_C
|
||||
DESTINATION vllm
|
||||
LANGUAGE ${VLLM_GPU_LANG}
|
||||
SOURCES ${QUTLASS_SOURCES}
|
||||
COMPILE_FLAGS ${VLLM_GPU_FLAGS}
|
||||
ARCHITECTURES ${VLLM_GPU_ARCHES}
|
||||
INCLUDE_DIRECTORIES ${QUTLASS_INCLUDES}
|
||||
USE_SABI 3
|
||||
WITH_SOABI)
|
||||
|
||||
target_compile_definitions(_qutlass_C PRIVATE
|
||||
QUTLASS_DISABLE_PYBIND=1
|
||||
TARGET_CUDA_ARCH=${QUTLASS_TARGET_CC}
|
||||
)
|
||||
CUTLASS_ENABLE_DIRECT_CUDA_DRIVER_CALL=1)
|
||||
|
||||
set_property(SOURCE ${QUTLASS_SOURCES} APPEND PROPERTY COMPILE_OPTIONS
|
||||
$<$<COMPILE_LANGUAGE:CUDA>:--expt-relaxed-constexpr --use_fast_math -O3>
|
||||
@@ -111,4 +134,5 @@ else()
|
||||
"[QUTLASS] Skipping build: no supported arch (12.0f / 10.0f) found in "
|
||||
"CUDA_ARCHS='${CUDA_ARCHS}'.")
|
||||
endif()
|
||||
add_custom_target(_qutlass_C)
|
||||
endif()
|
||||
|
||||
@@ -268,9 +268,14 @@ int64_t sm100_cutlass_mla_get_workspace_size(int64_t max_seq_len, int64_t num_ba
|
||||
using TileShapeD = typename MlaSm100Type::TileShapeD;
|
||||
arguments.problem_shape =
|
||||
cute::make_tuple(TileShapeH{}, static_cast<int>(max_seq_len), TileShapeD{}, static_cast<int>(num_batches));
|
||||
// Assumes device 0 when getting sm_count.
|
||||
arguments.hw_info.sm_count =
|
||||
sm_count <= 0 ? cutlass::KernelHardwareInfo::query_device_multiprocessor_count(/*device_id=*/0) : sm_count;
|
||||
if (sm_count <= 0) {
|
||||
int current_device = 0;
|
||||
cudaGetDevice(¤t_device);
|
||||
arguments.hw_info.sm_count =
|
||||
cutlass::KernelHardwareInfo::query_device_multiprocessor_count(current_device);
|
||||
} else {
|
||||
arguments.hw_info.sm_count = sm_count;
|
||||
}
|
||||
arguments.split_kv = static_cast<int>(num_kv_splits);
|
||||
MlaSm100Type::Fmha::set_split_kv(arguments);
|
||||
|
||||
|
||||
@@ -67,6 +67,13 @@
|
||||
#include "../quantization/w8a8/fp8/nvidia/quant_utils.cuh"
|
||||
#endif
|
||||
|
||||
// Direct float -> E4M3 FP8 conversion for the indexer Q / index-K outputs.
|
||||
#ifndef USE_ROCM
|
||||
#include <cuda_fp8.h>
|
||||
#else
|
||||
#include <hip/hip_fp8.h>
|
||||
#endif
|
||||
|
||||
#ifndef FINAL_MASK
|
||||
#ifdef USE_ROCM
|
||||
#define FINAL_MASK 0xffffffffffffffffULL
|
||||
@@ -75,6 +82,19 @@
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifdef USE_ROCM
|
||||
// ROCm-compatible direct float -> E4M3 FP8 conversion (mirrors the DeepSeek V4
|
||||
// fused kernel).
|
||||
__device__ __forceinline__ uint8_t rocm_cvt_float_to_fp8_e4m3(float val) {
|
||||
#if defined(HIP_FP8_TYPE_OCP)
|
||||
__hip_fp8_e4m3 fp8_val(val);
|
||||
#else
|
||||
__hip_fp8_e4m3_fnuz fp8_val(val);
|
||||
#endif
|
||||
return reinterpret_cast<uint8_t&>(fp8_val);
|
||||
}
|
||||
#endif
|
||||
|
||||
namespace vllm {
|
||||
namespace minimax_m3_fused_ops {
|
||||
|
||||
@@ -193,6 +213,8 @@ __device__ __forceinline__ void storeElems(
|
||||
*reinterpret_cast<uint2*>(dst) = v;
|
||||
}
|
||||
|
||||
// Main K/V cache store. kAuto = unquantized (cache_t == scalar_t); fp8 cache
|
||||
// dtypes use the scaled-convert path with identity scale.
|
||||
template <typename scalar_t, typename cache_t, Fp8KVCacheDataType kv_dt>
|
||||
__device__ __forceinline__ void storeCacheElems(
|
||||
cache_t* __restrict__ dst, float const (&elems)[kElemsPerLane]) {
|
||||
@@ -208,6 +230,32 @@ __device__ __forceinline__ void storeCacheElems(
|
||||
}
|
||||
}
|
||||
|
||||
// Store 4 fp32 registers -> 4 contiguous E4M3 FP8 bytes (direct cast,
|
||||
// saturating to ±448). Used for the fp8 indexer-Q / index-K outputs; no scale
|
||||
// (RMSNorm outputs are O(1) and the score path only needs relative block
|
||||
// ordering).
|
||||
__device__ __forceinline__ void storeElemsFp8(
|
||||
uint8_t* __restrict__ dst, float const (&elems)[kElemsPerLane]) {
|
||||
constexpr float kFp8Max = 448.0f;
|
||||
#ifndef USE_ROCM
|
||||
__nv_fp8x2_storage_t out2[kElemsPerLane / 2];
|
||||
#pragma unroll
|
||||
for (int i = 0; i < kElemsPerLane / 2; i++) {
|
||||
float2 vv = make_float2(elems[2 * i], elems[2 * i + 1]);
|
||||
vv.x = fminf(fmaxf(vv.x, -kFp8Max), kFp8Max);
|
||||
vv.y = fminf(fmaxf(vv.y, -kFp8Max), kFp8Max);
|
||||
out2[i] = __nv_cvt_float2_to_fp8x2(vv, __NV_SATFINITE, __NV_E4M3);
|
||||
}
|
||||
*reinterpret_cast<uint32_t*>(dst) = *reinterpret_cast<uint32_t const*>(out2);
|
||||
#else
|
||||
#pragma unroll
|
||||
for (int i = 0; i < kElemsPerLane; i++) {
|
||||
float vv = fminf(fmaxf(elems[i], -kFp8Max), kFp8Max);
|
||||
dst[i] = rocm_cvt_float_to_fp8_e4m3(vv);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// Kernel
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
@@ -224,12 +272,14 @@ __device__ __forceinline__ void storeCacheElems(
|
||||
// V : nkv only if kInsertKV (V-cache insert; no warps in dense)
|
||||
// IQ: niq only if kIsSparse (norm+RoPE)
|
||||
// IK: 1 only if kIsSparse (norm+RoPE; +index-cache insert)
|
||||
// cache_t/kv_dt: main attention KV-cache dtype (auto/fp8). out_idx_t/kFp8Idx:
|
||||
// indexer index-K cache + index-Q output dtype (scalar_t or e4m3 byte).
|
||||
template <typename scalar_t, typename cache_t, Fp8KVCacheDataType kv_dt,
|
||||
bool kIsSparse, bool kInsertKV>
|
||||
typename out_idx_t, bool kIsSparse, bool kInsertKV, bool kFp8Idx>
|
||||
__global__ void fusedMiniMaxM3QNormRopeKVInsertKernel(
|
||||
scalar_t* __restrict__ qkv, // [N, qkv_row] in/out (packs index if sparse)
|
||||
scalar_t* __restrict__ q_out, // [N, nq*128] contiguous, or nullptr
|
||||
scalar_t* __restrict__ index_q_out, // [N, niq*128] contiguous, or nullptr
|
||||
scalar_t* __restrict__ q_out, // [N, nq*128] contiguous, or nullptr
|
||||
out_idx_t* __restrict__ index_q_out, // [N, niq*128]; scalar_t or e4m3 byte
|
||||
scalar_t const* __restrict__ q_norm_w,
|
||||
scalar_t const* __restrict__ k_norm_w,
|
||||
scalar_t const* __restrict__ iq_norm_w,
|
||||
@@ -238,8 +288,8 @@ __global__ void fusedMiniMaxM3QNormRopeKVInsertKernel(
|
||||
int64_t const* __restrict__ positions, // [N] i64
|
||||
int64_t const* __restrict__ slot_mapping, // main K/V slots or nullptr
|
||||
int64_t const* __restrict__ index_slot_mapping, // index K slots/nullptr
|
||||
cache_t* __restrict__ kv_cache, // [nb,2,bs,nkv,128] or nullptr
|
||||
scalar_t* __restrict__ index_cache, // [nb*bs, 128] or nullptr
|
||||
cache_t* __restrict__ kv_cache, // [nb,2,bs,nkv,128] or nullptr
|
||||
out_idx_t* __restrict__ index_cache, // [nb*bs, 128]; scalar_t or e4m3 byte
|
||||
float const eps, int const rotary_dim, int const num_tokens, int const nq,
|
||||
int const nkv, int const niq, int const block_size,
|
||||
// kv_cache strides (in elements) for logical shape [nb, 2, bs, nkv, 128].
|
||||
@@ -334,9 +384,12 @@ __global__ void fusedMiniMaxM3QNormRopeKVInsertKernel(
|
||||
store_ptr = q_out + static_cast<int64_t>(tokenIdx) * nq * kHeadDim +
|
||||
slot * kHeadDim;
|
||||
} else if (isIQ && index_q_out != nullptr) {
|
||||
store_ptr = index_q_out +
|
||||
static_cast<int64_t>(tokenIdx) * niq * kHeadDim +
|
||||
(slot - iq_begin) * kHeadDim;
|
||||
// bf16 index_q_out: gather here. fp8: written by the explicit fp8 store.
|
||||
if constexpr (!kFp8Idx) {
|
||||
store_ptr = index_q_out +
|
||||
static_cast<int64_t>(tokenIdx) * niq * kHeadDim +
|
||||
(slot - iq_begin) * kHeadDim;
|
||||
}
|
||||
}
|
||||
|
||||
// PDL: wait for the predecessor kernel (the qkv-projection GEMM that
|
||||
@@ -356,7 +409,19 @@ __global__ void fusedMiniMaxM3QNormRopeKVInsertKernel(
|
||||
scalar_t const* cos_ptr = cos_sin_cache + pos * rotary_dim;
|
||||
normAndRope<scalar_t>(elems, laneId, eps, norm_w, do_rope, rotary_dim,
|
||||
cos_ptr, /*apply_norm=*/norm_w != nullptr);
|
||||
storeElems<scalar_t>(store_ptr + dim_base, elems);
|
||||
if constexpr (kFp8Idx) {
|
||||
// index_q is e4m3 bytes; Q/K (and in-place index_k) stay scalar_t.
|
||||
if (isIQ && index_q_out != nullptr) {
|
||||
storeElemsFp8(index_q_out +
|
||||
static_cast<int64_t>(tokenIdx) * niq * kHeadDim +
|
||||
(slot - iq_begin) * kHeadDim + dim_base,
|
||||
elems);
|
||||
} else {
|
||||
storeElems<scalar_t>(store_ptr + dim_base, elems);
|
||||
}
|
||||
} else {
|
||||
storeElems<scalar_t>(store_ptr + dim_base, elems);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Cache inserts (sparse serving only). ───────────────────────────────
|
||||
@@ -367,8 +432,11 @@ __global__ void fusedMiniMaxM3QNormRopeKVInsertKernel(
|
||||
: (isIK ? index_slot_mapping[tokenIdx] : -1);
|
||||
if (sm >= 0) { // skip padded / unscheduled tokens
|
||||
if (isIK) {
|
||||
scalar_t* dst = index_cache + sm * kHeadDim + dim_base;
|
||||
storeElems<scalar_t>(dst, elems);
|
||||
if constexpr (kFp8Idx) {
|
||||
storeElemsFp8(index_cache + sm * kHeadDim + dim_base, elems);
|
||||
} else {
|
||||
storeElems<scalar_t>(index_cache + sm * kHeadDim + dim_base, elems);
|
||||
}
|
||||
} else if (isK || isV) {
|
||||
// kv_cache logical shape [num_blocks, 2, block_size, nkv, head_dim].
|
||||
// Paging is logical (block = sm/block_size, token = sm%block_size);
|
||||
@@ -398,19 +466,19 @@ __global__ void fusedMiniMaxM3QNormRopeKVInsertKernel(
|
||||
// Launch wrapper
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
template <typename scalar_t, typename cache_t, Fp8KVCacheDataType kv_dt>
|
||||
void launchFusedMiniMaxM3(scalar_t* qkv, scalar_t* q_out, scalar_t* index_q_out,
|
||||
scalar_t const* q_norm_w, scalar_t const* k_norm_w,
|
||||
scalar_t const* iq_norm_w, scalar_t const* ik_norm_w,
|
||||
scalar_t const* cos_sin_cache,
|
||||
int64_t const* positions, int64_t const* slot_mapping,
|
||||
int64_t const* index_slot_mapping, cache_t* kv_cache,
|
||||
scalar_t* index_cache, float const eps,
|
||||
int const rotary_dim, int const num_tokens,
|
||||
int const nq, int const nkv, int const niq,
|
||||
int const block_size, int64_t const kv_s_block,
|
||||
int64_t const kv_s_kv, int64_t const kv_s_token,
|
||||
int64_t const kv_s_head, bool const has_index,
|
||||
bool const insert_kv, cudaStream_t stream) {
|
||||
void launchFusedMiniMaxM3(
|
||||
scalar_t* qkv, scalar_t* q_out, void* index_q_out, scalar_t const* q_norm_w,
|
||||
scalar_t const* k_norm_w, scalar_t const* iq_norm_w,
|
||||
scalar_t const* ik_norm_w, scalar_t const* cos_sin_cache,
|
||||
int64_t const* positions, int64_t const* slot_mapping,
|
||||
int64_t const* index_slot_mapping, cache_t* kv_cache, void* index_cache,
|
||||
float const eps, int const rotary_dim, int const num_tokens, int const nq,
|
||||
int const nkv, int const niq, int const block_size,
|
||||
int64_t const kv_s_block, int64_t const kv_s_kv, int64_t const kv_s_token,
|
||||
int64_t const kv_s_head, bool const has_index, bool const insert_kv,
|
||||
bool const fp8_idx, cudaStream_t stream) {
|
||||
// Index outputs are scalar_t (bf16) or e4m3 bytes (uint8_t); reinterpret the
|
||||
// void* pointers per instantiation in the LAUNCH macro.
|
||||
// Slot count must match the kernel's compile-time gating.
|
||||
int const v_slots = insert_kv ? nkv : 0;
|
||||
int const idx_slots = has_index ? niq + 1 : 0;
|
||||
@@ -440,25 +508,27 @@ void launchFusedMiniMaxM3(scalar_t* qkv, scalar_t* q_out, scalar_t* index_q_out,
|
||||
config.attrs = attrs;
|
||||
config.numAttrs = (sm_version >= 90) ? 1 : 0;
|
||||
|
||||
#define LAUNCH(IS_SPARSE, INSERT) \
|
||||
cudaLaunchKernelEx( \
|
||||
&config, \
|
||||
fusedMiniMaxM3QNormRopeKVInsertKernel<scalar_t, cache_t, kv_dt, \
|
||||
IS_SPARSE, INSERT>, \
|
||||
qkv, q_out, index_q_out, q_norm_w, k_norm_w, iq_norm_w, ik_norm_w, \
|
||||
cos_sin_cache, positions, slot_mapping, index_slot_mapping, kv_cache, \
|
||||
index_cache, eps, rotary_dim, num_tokens, nq, nkv, niq, block_size, \
|
||||
kv_s_block, kv_s_kv, kv_s_token, kv_s_head)
|
||||
#define LAUNCH(IS_SPARSE, INSERT, FP8, OUT_T) \
|
||||
cudaLaunchKernelEx( \
|
||||
&config, \
|
||||
fusedMiniMaxM3QNormRopeKVInsertKernel<scalar_t, cache_t, kv_dt, OUT_T, \
|
||||
IS_SPARSE, INSERT, FP8>, \
|
||||
qkv, q_out, reinterpret_cast<OUT_T*>(index_q_out), q_norm_w, k_norm_w, \
|
||||
iq_norm_w, ik_norm_w, cos_sin_cache, positions, slot_mapping, \
|
||||
index_slot_mapping, kv_cache, reinterpret_cast<OUT_T*>(index_cache), \
|
||||
eps, rotary_dim, num_tokens, nq, nkv, niq, block_size, kv_s_block, \
|
||||
kv_s_kv, kv_s_token, kv_s_head)
|
||||
#else
|
||||
// ROCm: standard kernel launch syntax (no PDL/stream serialization).
|
||||
// clang-format off
|
||||
#define LAUNCH(IS_SPARSE, INSERT) \
|
||||
fusedMiniMaxM3QNormRopeKVInsertKernel<scalar_t, cache_t, kv_dt, \
|
||||
IS_SPARSE, INSERT> \
|
||||
#define LAUNCH(IS_SPARSE, INSERT, FP8, OUT_T) \
|
||||
fusedMiniMaxM3QNormRopeKVInsertKernel<scalar_t, cache_t, kv_dt, OUT_T, \
|
||||
IS_SPARSE, INSERT, FP8> \
|
||||
<<<grid, kBlockSize, 0, stream>>>( \
|
||||
qkv, q_out, index_q_out, q_norm_w, k_norm_w, iq_norm_w, \
|
||||
ik_norm_w, cos_sin_cache, positions, slot_mapping, \
|
||||
index_slot_mapping, kv_cache, index_cache, eps, rotary_dim, \
|
||||
qkv, q_out, reinterpret_cast<OUT_T*>(index_q_out), q_norm_w, \
|
||||
k_norm_w, iq_norm_w, ik_norm_w, cos_sin_cache, positions, \
|
||||
slot_mapping, index_slot_mapping, kv_cache, \
|
||||
reinterpret_cast<OUT_T*>(index_cache), eps, rotary_dim, \
|
||||
num_tokens, nq, nkv, niq, block_size, kv_s_block, kv_s_kv, \
|
||||
kv_s_token, kv_s_head)
|
||||
// clang-format on
|
||||
@@ -466,14 +536,22 @@ void launchFusedMiniMaxM3(scalar_t* qkv, scalar_t* q_out, scalar_t* index_q_out,
|
||||
|
||||
if (has_index) {
|
||||
if (insert_kv) {
|
||||
LAUNCH(true, true); // sparse serving
|
||||
if (fp8_idx) {
|
||||
LAUNCH(true, true, true, uint8_t); // sparse serving, fp8 index outputs
|
||||
} else {
|
||||
LAUNCH(true, true, false, scalar_t); // sparse serving, bf16
|
||||
}
|
||||
} else {
|
||||
LAUNCH(true, false); // sparse profiling
|
||||
if (fp8_idx) {
|
||||
LAUNCH(true, false, true, uint8_t); // sparse profiling, fp8 index_q
|
||||
} else {
|
||||
LAUNCH(true, false, false, scalar_t); // sparse profiling, bf16
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Dense layer: never has an index branch and never inserts here (the
|
||||
// generic Attention layer owns the KV insert).
|
||||
LAUNCH(false, false);
|
||||
LAUNCH(false, false, false, scalar_t);
|
||||
}
|
||||
#undef LAUNCH
|
||||
}
|
||||
@@ -485,8 +563,9 @@ void launchFusedMiniMaxM3(scalar_t* qkv, scalar_t* q_out, scalar_t* index_q_out,
|
||||
vllm::minimax_m3_fused_ops::launchFusedMiniMaxM3<st, CACHE_T, KV_DTYPE>( \
|
||||
reinterpret_cast<st*>(qkv.data_ptr()), \
|
||||
q_out.has_value() ? reinterpret_cast<st*>(q_out->data_ptr()) : nullptr, \
|
||||
index_q_out.has_value() ? reinterpret_cast<st*>(index_q_out->data_ptr()) \
|
||||
: nullptr, \
|
||||
index_q_out.has_value() \
|
||||
? reinterpret_cast<void*>(index_q_out->data_ptr()) \
|
||||
: nullptr, \
|
||||
reinterpret_cast<st const*>(q_norm_weight.data_ptr()), \
|
||||
reinterpret_cast<st const*>(k_norm_weight.data_ptr()), \
|
||||
has_index ? reinterpret_cast<st const*>(index_q_norm_weight->data_ptr()) \
|
||||
@@ -502,11 +581,11 @@ void launchFusedMiniMaxM3(scalar_t* qkv, scalar_t* q_out, scalar_t* index_q_out,
|
||||
: nullptr, \
|
||||
insert_kv ? reinterpret_cast<CACHE_T*>(kv_cache->data_ptr()) : nullptr, \
|
||||
(insert_kv && has_index) \
|
||||
? reinterpret_cast<st*>(index_cache->data_ptr()) \
|
||||
? reinterpret_cast<void*>(index_cache->data_ptr()) \
|
||||
: nullptr, \
|
||||
static_cast<float>(eps), static_cast<int>(rotary_dim), num_tokens, nq, \
|
||||
nkv, niq, static_cast<int>(block_size), kv_s_block, kv_s_kv, kv_s_token, \
|
||||
kv_s_head, has_index, insert_kv, stream)
|
||||
kv_s_head, has_index, insert_kv, fp8_idx, stream)
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// Torch op wrapper
|
||||
@@ -612,6 +691,7 @@ void fused_minimax_m3_qknorm_rope_kv_insert(
|
||||
torch::headeronly::ScalarType::Long &&
|
||||
index_slot_mapping->numel() == slot_mapping->numel()),
|
||||
"index_slot_mapping must be int64 CUDA with slot_mapping length");
|
||||
// Main attention KV cache: auto matches qkv, fp8 uses uint8 storage.
|
||||
if (kv_dt == vllm::Fp8KVCacheDataType::kAuto) {
|
||||
STD_TORCH_CHECK(kv_cache->scalar_type() == qkv.scalar_type(),
|
||||
"auto kv_cache dtype must match qkv");
|
||||
@@ -620,9 +700,13 @@ void fused_minimax_m3_qknorm_rope_kv_insert(
|
||||
kv_cache->scalar_type() == torch::headeronly::ScalarType::Byte,
|
||||
"fp8 kv_cache must use uint8 storage");
|
||||
}
|
||||
STD_TORCH_CHECK(index_cache.has_value() &&
|
||||
index_cache->scalar_type() == qkv.scalar_type(),
|
||||
"insert mode requires matching index_cache");
|
||||
// Indexer index-K cache: independent dtype -- qkv dtype or fp8 e4m3.
|
||||
STD_TORCH_CHECK(
|
||||
index_cache.has_value() &&
|
||||
(index_cache->scalar_type() == qkv.scalar_type() ||
|
||||
index_cache->scalar_type() ==
|
||||
torch::headeronly::ScalarType::Float8_e4m3fn),
|
||||
"insert mode requires index_cache matching qkv dtype or fp8 e4m3");
|
||||
STD_TORCH_CHECK(kv_cache->dim() == 5 && kv_cache->stride(4) == 1,
|
||||
"kv_cache must be [nb,2,bs,nkv,head_dim] with contiguous "
|
||||
"head_dim (stride(4)==1)");
|
||||
@@ -652,14 +736,31 @@ void fused_minimax_m3_qknorm_rope_kv_insert(
|
||||
"index_q_out requires the index branch (num_index_heads > 0)");
|
||||
STD_TORCH_CHECK(
|
||||
index_q_out->is_cuda() && index_q_out->is_contiguous() &&
|
||||
index_q_out->scalar_type() == qkv.scalar_type(),
|
||||
"index_q_out must be a contiguous CUDA tensor matching qkv dtype");
|
||||
(index_q_out->scalar_type() == qkv.scalar_type() ||
|
||||
index_q_out->scalar_type() ==
|
||||
torch::headeronly::ScalarType::Float8_e4m3fn),
|
||||
"index_q_out must be contiguous CUDA, qkv dtype or fp8 e4m3");
|
||||
STD_TORCH_CHECK(index_q_out->numel() ==
|
||||
static_cast<int64_t>(num_tokens) * niq * kHeadDim,
|
||||
"index_q_out must have num_tokens * num_index_heads * 128 "
|
||||
"elements");
|
||||
}
|
||||
|
||||
// fp8 index path: the index-K cache and index-Q outputs are e4m3 bytes while
|
||||
// q/k/v + q_out stay qkv dtype. Both index outputs must agree.
|
||||
auto const kFp8 = torch::headeronly::ScalarType::Float8_e4m3fn;
|
||||
bool const fp8_idx =
|
||||
(index_cache.has_value() && index_cache->scalar_type() == kFp8) ||
|
||||
(index_q_out.has_value() && index_q_out->scalar_type() == kFp8);
|
||||
if (fp8_idx) {
|
||||
STD_TORCH_CHECK(
|
||||
!index_cache.has_value() || index_cache->scalar_type() == kFp8,
|
||||
"fp8 index path: index_cache must be fp8 e4m3");
|
||||
STD_TORCH_CHECK(
|
||||
!index_q_out.has_value() || index_q_out->scalar_type() == kFp8,
|
||||
"fp8 index path: index_q_out must be fp8 e4m3");
|
||||
}
|
||||
|
||||
const torch::stable::accelerator::DeviceGuard device_guard(
|
||||
qkv.get_device_index());
|
||||
auto stream = get_current_cuda_stream(qkv.get_device_index());
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
#include <torch/headeronly/core/ScalarType.h>
|
||||
|
||||
#include "../../cuda_compat.h"
|
||||
#include "core/math.hpp"
|
||||
#include "libtorch_stable/core/math.hpp"
|
||||
#include "libtorch_stable/dispatch_utils.h"
|
||||
#include "libtorch_stable/torch_utils.h"
|
||||
|
||||
|
||||
@@ -2,9 +2,25 @@
|
||||
|
||||
#include <torch/csrc/stable/library.h>
|
||||
#include <torch/csrc/stable/tensor.h>
|
||||
#include <torch/headeronly/util/Exception.h>
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <torch/csrc/stable/ops.h>
|
||||
|
||||
inline torch::stable::Tensor weak_ref_tensor(torch::stable::Tensor& tensor) {
|
||||
// Ensure tensor is on CUDA
|
||||
STD_TORCH_CHECK(tensor.device().is_cuda(), "Tensor must be on CUDA device");
|
||||
|
||||
// Get the raw data pointer
|
||||
void* data_ptr = tensor.mutable_data_ptr();
|
||||
|
||||
/// Create a new tensor from the raw data pointer
|
||||
return torch::stable::from_blob(data_ptr, tensor.sizes(), tensor.strides(),
|
||||
tensor.device(), tensor.scalar_type());
|
||||
}
|
||||
|
||||
void per_token_group_quant_fp8(const torch::stable::Tensor& input,
|
||||
torch::stable::Tensor& output_q,
|
||||
@@ -371,6 +387,18 @@ void silu_and_mul(torch::stable::Tensor& out, torch::stable::Tensor& input);
|
||||
void silu_and_mul_clamp(torch::stable::Tensor& out,
|
||||
torch::stable::Tensor& input, double limit,
|
||||
double alpha = 1.0, double beta = 0.0);
|
||||
|
||||
void silu_and_mul_quant(torch::stable::Tensor& out,
|
||||
torch::stable::Tensor& input,
|
||||
torch::stable::Tensor& scale);
|
||||
|
||||
void persistent_masked_m_silu_mul_quant(
|
||||
const torch::stable::Tensor& input, // (E, T, 2*H)
|
||||
const torch::stable::Tensor& tokens_per_expert, // (E)
|
||||
torch::stable::Tensor& y_q, // (E, T, H) [OUT]
|
||||
torch::stable::Tensor& y_s, // (E, T, H//group_size) [OUT]
|
||||
bool use_ue8m0);
|
||||
|
||||
void mul_and_silu(torch::stable::Tensor& out, torch::stable::Tensor& input);
|
||||
void gelu_and_mul(torch::stable::Tensor& out, torch::stable::Tensor& input);
|
||||
void gelu_tanh_and_mul(torch::stable::Tensor& out,
|
||||
|
||||
+63
-55
@@ -1,16 +1,12 @@
|
||||
#include <ATen/cuda/CUDAContext.h>
|
||||
#include <torch/all.h>
|
||||
#include <c10/cuda/CUDAGuard.h>
|
||||
#include "libtorch_stable/torch_utils.h"
|
||||
|
||||
#include <cmath>
|
||||
#include "core/math.hpp"
|
||||
#include "../cuda_compat.h"
|
||||
#include "dispatch_utils.h"
|
||||
|
||||
#include "libtorch_stable/core/math.hpp"
|
||||
#include "cuda_compat.h"
|
||||
#include "libtorch_stable/dispatch_utils.h"
|
||||
#include "quantization/w8a8/fp8/common.cuh"
|
||||
|
||||
#include <c10/util/Float8_e4m3fn.h>
|
||||
|
||||
#ifndef USE_ROCM
|
||||
#include <cuda_bf16.h>
|
||||
#include <cuda_fp16.h>
|
||||
@@ -33,7 +29,6 @@ typedef __hip_fp8x4_e4m3_fnuz __nv_fp8x4_e4m3;
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#include "core/registration.h"
|
||||
namespace vllm {
|
||||
|
||||
template <typename T>
|
||||
@@ -564,41 +559,47 @@ __global__ void silu_mul_fp8_quant_deep_gemm_kernel(
|
||||
} // namespace vllm
|
||||
|
||||
// Launch activation, gating, and quantize kernel.
|
||||
#define LAUNCH_ACTIVATION_GATE_KERNEL(KERNEL) \
|
||||
int d = input.size(-1) / 2; \
|
||||
int64_t num_tokens = input.numel() / input.size(-1); \
|
||||
dim3 grid(num_tokens, num_tokens > 16 ? num_tokens > 32 ? 1 : 2 : 4); \
|
||||
dim3 block(std::min(d, 512)); \
|
||||
const at::cuda::OptionalCUDAGuard device_guard(device_of(input)); \
|
||||
const cudaStream_t stream = at::cuda::getCurrentCUDAStream(); \
|
||||
VLLM_DISPATCH_FLOATING_TYPES( \
|
||||
input.scalar_type(), "act_and_mul_kernel", [&] { \
|
||||
VLLM_DISPATCH_FP8_TYPES( \
|
||||
out.scalar_type(), "fused_add_rms_norm_kernel_fp8_type", [&] { \
|
||||
vllm::act_and_mul_quant_kernel<scalar_t, KERNEL<scalar_t>, \
|
||||
fp8_t> \
|
||||
<<<grid, block, 0, stream>>>(out.data_ptr<fp8_t>(), \
|
||||
input.data_ptr<scalar_t>(), \
|
||||
scale.data_ptr<float>(), d); \
|
||||
}); \
|
||||
#define LAUNCH_ACTIVATION_GATE_KERNEL(KERNEL) \
|
||||
int d = input.size(-1) / 2; \
|
||||
int64_t num_tokens = input.numel() / input.size(-1); \
|
||||
dim3 grid(num_tokens, num_tokens > 16 ? num_tokens > 32 ? 1 : 2 : 4); \
|
||||
dim3 block(std::min(d, 512)); \
|
||||
const torch::stable::accelerator::DeviceGuard device_guard( \
|
||||
input.get_device_index()); \
|
||||
const cudaStream_t stream = \
|
||||
get_current_cuda_stream(input.get_device_index()); \
|
||||
VLLM_STABLE_DISPATCH_FLOATING_TYPES( \
|
||||
input.scalar_type(), "act_and_mul_kernel", [&] { \
|
||||
VLLM_STABLE_DISPATCH_FP8_TYPES( \
|
||||
out.scalar_type(), "act_and_mul_quant_kernel_fp8_type", [&] { \
|
||||
vllm::act_and_mul_quant_kernel<scalar_t, KERNEL<scalar_t>, \
|
||||
fp8_t> \
|
||||
<<<grid, block, 0, stream>>>( \
|
||||
out.mutable_data_ptr<fp8_t>(), \
|
||||
input.const_data_ptr<scalar_t>(), \
|
||||
scale.const_data_ptr<float>(), d); \
|
||||
}); \
|
||||
});
|
||||
|
||||
void silu_and_mul_quant(torch::Tensor& out, // [..., d]
|
||||
torch::Tensor& input, // [..., 2 * d]
|
||||
torch::Tensor& scale) {
|
||||
TORCH_CHECK(out.dtype() == torch::kFloat8_e4m3fn ||
|
||||
out.dtype() == torch::kFloat8_e4m3fnuz);
|
||||
TORCH_CHECK(input.dtype() == torch::kFloat16 ||
|
||||
input.dtype() == torch::kBFloat16);
|
||||
TORCH_CHECK(input.size(-1) % 2 == 0);
|
||||
void silu_and_mul_quant(torch::stable::Tensor& out, // [..., d]
|
||||
torch::stable::Tensor& input, // [..., 2 * d]
|
||||
torch::stable::Tensor& scale) {
|
||||
STD_TORCH_CHECK(
|
||||
out.scalar_type() == torch::headeronly::ScalarType::Float8_e4m3fn ||
|
||||
out.scalar_type() == torch::headeronly::ScalarType::Float8_e4m3fnuz);
|
||||
STD_TORCH_CHECK(
|
||||
input.scalar_type() == torch::headeronly::ScalarType::Half ||
|
||||
input.scalar_type() == torch::headeronly::ScalarType::BFloat16,
|
||||
"Input must be FP16 or BF16");
|
||||
STD_TORCH_CHECK(input.size(-1) % 2 == 0);
|
||||
LAUNCH_ACTIVATION_GATE_KERNEL(vllm::silu_kernel);
|
||||
}
|
||||
|
||||
void persistent_masked_m_silu_mul_quant(
|
||||
const at::Tensor& input, // (E, T, 2*H)
|
||||
const at::Tensor& tokens_per_expert, // (E)
|
||||
at::Tensor& y_q, // (E, T, H) [OUT]
|
||||
at::Tensor& y_s, // (E, T, H//group_size) [OUT]
|
||||
const torch::stable::Tensor& input, // (E, T, 2*H)
|
||||
const torch::stable::Tensor& tokens_per_expert, // (E)
|
||||
torch::stable::Tensor& y_q, // (E, T, H) [OUT]
|
||||
torch::stable::Tensor& y_s, // (E, T, H//group_size) [OUT]
|
||||
bool cast_scale_ue8m0) {
|
||||
#ifndef USE_ROCM
|
||||
|
||||
@@ -606,14 +607,18 @@ void persistent_masked_m_silu_mul_quant(
|
||||
// fixed GROUP_SIZE of 128.
|
||||
static constexpr int GROUP_SIZE = 128;
|
||||
|
||||
TORCH_CHECK(input.dtype() == torch::kBFloat16);
|
||||
TORCH_CHECK(y_q.dtype() == torch::kFloat8_e4m3fn ||
|
||||
y_q.dtype() == torch::kFloat8_e4m3fnuz);
|
||||
TORCH_CHECK(input.size(-1) % (GROUP_SIZE * 2) == 0);
|
||||
STD_TORCH_CHECK(input.scalar_type() ==
|
||||
torch::headeronly::ScalarType::BFloat16);
|
||||
STD_TORCH_CHECK(
|
||||
y_q.scalar_type() == torch::headeronly::ScalarType::Float8_e4m3fn ||
|
||||
y_q.scalar_type() == torch::headeronly::ScalarType::Float8_e4m3fnuz);
|
||||
STD_TORCH_CHECK(input.size(-1) % (GROUP_SIZE * 2) == 0);
|
||||
|
||||
bool const is_packed_ue8m0 =
|
||||
(y_s.dtype() == torch::kInt32 && cast_scale_ue8m0);
|
||||
TORCH_CHECK(y_s.dtype() == torch::kFloat32 || is_packed_ue8m0);
|
||||
(y_s.scalar_type() == torch::headeronly::ScalarType::Int &&
|
||||
cast_scale_ue8m0);
|
||||
STD_TORCH_CHECK(y_s.scalar_type() == torch::headeronly::ScalarType::Float ||
|
||||
is_packed_ue8m0);
|
||||
|
||||
using Idx_t = int64_t;
|
||||
|
||||
@@ -631,7 +636,7 @@ void persistent_masked_m_silu_mul_quant(
|
||||
|
||||
int const NUM_GROUPS = H / GROUP_SIZE;
|
||||
|
||||
const cudaStream_t stream = at::cuda::getCurrentCUDAStream();
|
||||
const cudaStream_t stream = get_current_cuda_stream(input.get_device_index());
|
||||
|
||||
// TODO: Get this from cuda_arch ?
|
||||
static constexpr int SILU_V2_BLOCK_COUNT = 132 * 32;
|
||||
@@ -643,18 +648,21 @@ void persistent_masked_m_silu_mul_quant(
|
||||
static constexpr int max_shared_mem_bytes = \
|
||||
GROUP_SIZE * 2 * STAGES * NUM_WARPS * 2; \
|
||||
dim3 grid(sms), block(THREAD_COUNT); \
|
||||
const at::cuda::OptionalCUDAGuard device_guard(device_of(input)); \
|
||||
VLLM_DISPATCH_FP8_TYPES( \
|
||||
const torch::stable::accelerator::DeviceGuard device_guard( \
|
||||
input.get_device_index()); \
|
||||
VLLM_STABLE_DISPATCH_FP8_TYPES( \
|
||||
y_q.scalar_type(), "silu_mul_fp8_quant_deep_gemm_kernel", [&] { \
|
||||
vllm::silu_mul_fp8_quant_deep_gemm_kernel< \
|
||||
BLOCK_COUNT, max_shared_mem_bytes, fp8_t, scale_t, THREAD_COUNT, \
|
||||
Idx_t, CEIL_UE8M0, GROUP_SIZE, STAGES> \
|
||||
<<<grid, block, max_shared_mem_bytes + (E + 1) * 16, stream>>>( \
|
||||
reinterpret_cast<__nv_bfloat16*>(input.data_ptr()), \
|
||||
(fp8_t*)y_q.data_ptr(), \
|
||||
reinterpret_cast<scale_t*>(y_s.data_ptr()), \
|
||||
reinterpret_cast<int32_t*>(tokens_per_expert.data_ptr()), E, \
|
||||
T, H, stride_i_e, stride_i_t, stride_i_h, stride_yq_e, \
|
||||
reinterpret_cast<const __nv_bfloat16*>( \
|
||||
input.const_data_ptr()), \
|
||||
y_q.mutable_data_ptr<fp8_t>(), \
|
||||
reinterpret_cast<scale_t*>(y_s.mutable_data_ptr()), \
|
||||
reinterpret_cast<const int32_t*>( \
|
||||
tokens_per_expert.const_data_ptr()), \
|
||||
E, T, H, stride_i_e, stride_i_t, stride_i_h, stride_yq_e, \
|
||||
stride_yq_t, stride_yq_h, STRIDE_YS_E, STRIDE_YS_T, \
|
||||
STRIDE_YS_G, STRIDE_YS_P, stride_counts_e); \
|
||||
});
|
||||
@@ -679,7 +687,7 @@ void persistent_masked_m_silu_mul_quant(
|
||||
Idx_t stride_ys_g = y_s.stride(2);
|
||||
Idx_t stride_ys_p = 0;
|
||||
if (!cast_scale_ue8m0) {
|
||||
TORCH_CHECK(!is_packed_ue8m0);
|
||||
STD_TORCH_CHECK(!is_packed_ue8m0);
|
||||
LAUNCH_ON_H(float, stride_ys_e, stride_ys_t, stride_ys_g, stride_ys_p,
|
||||
false);
|
||||
return;
|
||||
@@ -692,8 +700,8 @@ void persistent_masked_m_silu_mul_quant(
|
||||
return;
|
||||
}
|
||||
|
||||
TORCH_CHECK(cast_scale_ue8m0 && is_packed_ue8m0);
|
||||
TORCH_CHECK(y_s.dtype() == torch::kInt32);
|
||||
STD_TORCH_CHECK(cast_scale_ue8m0 && is_packed_ue8m0);
|
||||
STD_TORCH_CHECK(y_s.scalar_type() == torch::headeronly::ScalarType::Int);
|
||||
|
||||
// Int32 packed ue8m0 scales tensor.
|
||||
// Let E, T, G be the number to experts, number of tokens and number of groups
|
||||
@@ -31,7 +31,7 @@
|
||||
|
||||
#include "cutlass/util/packed_stride.hpp"
|
||||
|
||||
#include "core/math.hpp"
|
||||
#include "libtorch_stable/core/math.hpp"
|
||||
#include "core/batch_invariant.hpp"
|
||||
|
||||
using namespace cute;
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
|
||||
#include "cutlass/util/packed_stride.hpp"
|
||||
|
||||
#include "core/math.hpp"
|
||||
#include "libtorch_stable/core/math.hpp"
|
||||
#include "core/batch_invariant.hpp"
|
||||
|
||||
using namespace cute;
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
#include "cutlass/gemm/collective/collective_builder.hpp"
|
||||
#include "cutlass/util/packed_stride.hpp"
|
||||
|
||||
#include "core/math.hpp"
|
||||
#include "libtorch_stable/core/math.hpp"
|
||||
#include "libtorch_stable/cutlass_extensions/common.hpp"
|
||||
// clang-format on
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
#include "cutlass/epilogue/collective/collective_builder.hpp"
|
||||
#include "cutlass/gemm/collective/collective_builder.hpp"
|
||||
|
||||
#include "core/math.hpp"
|
||||
#include "libtorch_stable/core/math.hpp"
|
||||
#include "libtorch_stable/cutlass_extensions/common.hpp"
|
||||
// clang-format on
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
#include "cutlass/epilogue/threadblock/fusion/visitors.hpp"
|
||||
#include "cutlass/gemm/kernel/default_gemm_universal_with_visitor.h"
|
||||
|
||||
#include "core/math.hpp"
|
||||
#include "libtorch_stable/core/math.hpp"
|
||||
#include "libtorch_stable/cutlass_extensions/common.hpp"
|
||||
// clang-format on
|
||||
|
||||
|
||||
@@ -301,8 +301,9 @@ __global__ void per_token_group_quant_8bit_packed_register_kernel(
|
||||
|
||||
const int sf_k_local = local_group_id % kGroupsPerBlockX;
|
||||
const int row_local = local_group_id / kGroupsPerBlockX;
|
||||
const int sf_k_idx = blockIdx.x * kGroupsPerBlockX + sf_k_local;
|
||||
const int mn_idx = blockIdx.y * kRowsPerBlock + row_local;
|
||||
// Rows on grid.x: mn scales with tokens and can exceed the 65535 grid.y cap.
|
||||
const int sf_k_idx = blockIdx.y * kGroupsPerBlockX + sf_k_local;
|
||||
const int mn_idx = blockIdx.x * kRowsPerBlock + row_local;
|
||||
|
||||
#if (defined(__CUDA_ARCH__) && (__CUDA_ARCH__ >= 900))
|
||||
asm volatile("griddepcontrol.wait;");
|
||||
@@ -496,14 +497,15 @@ void per_token_group_quant_8bit_packed(const torch::stable::Tensor& input,
|
||||
" is not a multiple of 4.");
|
||||
const int kx = GetGroupsPerBlockX(padded_groups_per_row);
|
||||
const int ry = 16 / kx;
|
||||
const int64_t blocks_x = padded_groups_per_row / kx;
|
||||
const int64_t blocks_y = (tma_aligned_mn + ry - 1) / ry;
|
||||
const int64_t row_blocks = (tma_aligned_mn + ry - 1) / ry;
|
||||
const int64_t sf_k_blocks = padded_groups_per_row / kx;
|
||||
const int num_threads = (kx * ry) * THREADS_PER_GROUP;
|
||||
// CUDA caps grid.x and grid.y at 2^31 - 1; guard against pathological inputs.
|
||||
STD_TORCH_CHECK(blocks_x <= static_cast<int64_t>(INT32_MAX) &&
|
||||
blocks_y <= static_cast<int64_t>(INT32_MAX),
|
||||
// CUDA caps grid.x at 2^31 - 1 and grid.y at 2^16 - 1 (65535).
|
||||
constexpr int64_t kMaxGridDimYZ = 65535;
|
||||
STD_TORCH_CHECK(row_blocks <= static_cast<int64_t>(INT32_MAX) &&
|
||||
sf_k_blocks <= kMaxGridDimYZ,
|
||||
"per_token_group_quant_8bit_packed grid too large: (",
|
||||
blocks_x, ", ", blocks_y, ").");
|
||||
row_blocks, ", ", sf_k_blocks, ").");
|
||||
|
||||
auto dst_type = output_q.scalar_type();
|
||||
|
||||
@@ -513,8 +515,8 @@ void per_token_group_quant_8bit_packed(const torch::stable::Tensor& input,
|
||||
#define LAUNCH_REG_KERNEL_INST(T, DST_DTYPE, KX, RY) \
|
||||
do { \
|
||||
cudaLaunchConfig_t config = {}; \
|
||||
config.gridDim = dim3(static_cast<unsigned int>(blocks_x), \
|
||||
static_cast<unsigned int>(blocks_y)); \
|
||||
config.gridDim = dim3(static_cast<unsigned int>(row_blocks), \
|
||||
static_cast<unsigned int>(sf_k_blocks)); \
|
||||
config.blockDim = dim3(num_threads); \
|
||||
config.dynamicSmemBytes = 0; \
|
||||
config.stream = stream; \
|
||||
@@ -539,8 +541,8 @@ void per_token_group_quant_8bit_packed(const torch::stable::Tensor& input,
|
||||
#else
|
||||
#define LAUNCH_REG_KERNEL_INST(T, DST_DTYPE, KX, RY) \
|
||||
do { \
|
||||
dim3 grid(static_cast<unsigned int>(blocks_x), \
|
||||
static_cast<unsigned int>(blocks_y)); \
|
||||
dim3 grid(static_cast<unsigned int>(row_blocks), \
|
||||
static_cast<unsigned int>(sf_k_blocks)); \
|
||||
dim3 block(num_threads); \
|
||||
per_token_group_quant_8bit_packed_register_kernel<T, DST_DTYPE, 128, KX, \
|
||||
RY> \
|
||||
|
||||
@@ -34,6 +34,20 @@ STABLE_TORCH_LIBRARY_FRAGMENT(_C, ops) {
|
||||
// TODO: Remove this once ROCm upgrade to torch 2.11.
|
||||
ops.def("get_cuda_view_from_cpu_tensor(Tensor cpu_tensor) -> Tensor");
|
||||
|
||||
// Note about marlin kernel 'workspace' arguments:
|
||||
// Technically these should be mutable since they are modified by the kernel.
|
||||
// But since they are set back to zero once the kernel is finished we can
|
||||
// hand wave and say that they have no net effect.
|
||||
//
|
||||
// The reason to mark 'workspace' as immutable is so that they don't interfere
|
||||
// with using ScalarType arguments in the ops. If they are marked as mutable,
|
||||
// pytorch throws an assert in
|
||||
// 'torch._higher_order_ops._register_effectful_op' that prevents these
|
||||
// kernels from being torch.compile'd.
|
||||
// See the following document for more info on custom types and ops that use
|
||||
// custom types:
|
||||
// https://docs.google.com/document/d/18fBMPuOJ0fY5ZQ6YyrHUppw9FA332CpNtgB6SOIgyuA
|
||||
|
||||
// Machete (Dense) Optimized Mixed Precision GEMM for Hopper.
|
||||
ops.def(
|
||||
"machete_supported_schedules("
|
||||
@@ -480,6 +494,11 @@ STABLE_TORCH_LIBRARY_FRAGMENT(_C, ops) {
|
||||
"Tensor workspace, int k, int max_seq_len) -> ()");
|
||||
|
||||
// Activation ops
|
||||
ops.def(
|
||||
"persistent_masked_m_silu_mul_quant(Tensor input, Tensor counts, Tensor! "
|
||||
"y_q, Tensor! y_s, bool use_ue8m0) -> ()");
|
||||
ops.def("weak_ref_tensor(Tensor input) -> Tensor");
|
||||
|
||||
// Activation function used in SwiGLU.
|
||||
ops.def("silu_and_mul(Tensor! result, Tensor input) -> ()");
|
||||
|
||||
@@ -492,6 +511,10 @@ STABLE_TORCH_LIBRARY_FRAGMENT(_C, ops) {
|
||||
"silu_and_mul_with_clamp(Tensor! result, Tensor input, float limit, "
|
||||
"float alpha=1.0, float beta=0.0) -> ()");
|
||||
|
||||
// SwiGLU activation with FP8 quantization.
|
||||
ops.def(
|
||||
"silu_and_mul_quant(Tensor! result, Tensor input, Tensor scale) -> ()");
|
||||
|
||||
// Activation function used in GeGLU with `none` approximation.
|
||||
ops.def("gelu_and_mul(Tensor! out, Tensor input) -> ()");
|
||||
|
||||
@@ -690,6 +713,10 @@ STABLE_TORCH_LIBRARY_IMPL(_C, CUDA, ops) {
|
||||
ops.impl("persistent_topk", TORCH_BOX(&persistent_topk));
|
||||
|
||||
// Activation kernels (shared CUDA/ROCm)
|
||||
ops.impl("persistent_masked_m_silu_mul_quant",
|
||||
TORCH_BOX(&persistent_masked_m_silu_mul_quant));
|
||||
ops.impl("weak_ref_tensor", TORCH_BOX(&weak_ref_tensor));
|
||||
ops.impl("silu_and_mul_quant", TORCH_BOX(&silu_and_mul_quant));
|
||||
ops.impl("silu_and_mul", TORCH_BOX(&silu_and_mul));
|
||||
ops.impl("mul_and_silu", TORCH_BOX(&mul_and_silu));
|
||||
ops.impl("gelu_and_mul", TORCH_BOX(&gelu_and_mul));
|
||||
|
||||
-32
@@ -9,28 +9,6 @@
|
||||
|
||||
#include <vector>
|
||||
|
||||
torch::Tensor weak_ref_tensor(torch::Tensor& tensor) {
|
||||
// Ensure tensor is on CUDA
|
||||
if (!tensor.is_cuda()) {
|
||||
throw std::runtime_error("Tensor must be on CUDA device");
|
||||
}
|
||||
|
||||
// Get the raw data pointer
|
||||
void* data_ptr = tensor.data_ptr();
|
||||
|
||||
// Get tensor sizes and strides
|
||||
std::vector<int64_t> sizes = tensor.sizes().vec();
|
||||
std::vector<int64_t> strides = tensor.strides().vec();
|
||||
|
||||
// Get tensor options (dtype, device)
|
||||
auto options = tensor.options();
|
||||
|
||||
// Create a new tensor from the raw data pointer
|
||||
auto new_tensor = torch::from_blob(data_ptr, sizes, strides, options);
|
||||
|
||||
return new_tensor;
|
||||
}
|
||||
|
||||
// rms_norm and fused_add_rms_norm declarations also exist in
|
||||
// csrc/libtorch_stable/ops.h (torch::stable ABI for CUDA). They remain here
|
||||
// because the CPU build still uses these torch::Tensor declarations.
|
||||
@@ -53,16 +31,6 @@ void silu_and_mul(torch::Tensor& out, torch::Tensor& input);
|
||||
void silu_and_mul_clamp(torch::Tensor& out, torch::Tensor& input, double limit,
|
||||
double alpha = 1.0, double beta = 0.0);
|
||||
|
||||
void silu_and_mul_quant(torch::Tensor& out, torch::Tensor& input,
|
||||
torch::Tensor& scale);
|
||||
|
||||
void persistent_masked_m_silu_mul_quant(
|
||||
const at::Tensor& input, // (E, T, 2*H)
|
||||
const at::Tensor& counts, // (E)
|
||||
at::Tensor& y_q, // (E, T, H) [OUT]
|
||||
at::Tensor& y_s, // (E, T, H//group_size) [OUT]
|
||||
bool use_ue8m0);
|
||||
|
||||
void gelu_and_mul(torch::Tensor& out, torch::Tensor& input);
|
||||
|
||||
void gelu_tanh_and_mul(torch::Tensor& out, torch::Tensor& input);
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
#include "core/registration.h"
|
||||
|
||||
// QuTLASS registers torch.ops._qutlass_C via TORCH_LIBRARY in bindings.cpp.
|
||||
// This stub lets Python import vllm._qutlass_C to trigger op registration.
|
||||
REGISTER_EXTENSION(_qutlass_C)
|
||||
@@ -20,17 +20,6 @@
|
||||
|
||||
TORCH_LIBRARY_EXPAND(TORCH_EXTENSION_NAME, ops) {
|
||||
// vLLM custom ops
|
||||
//
|
||||
|
||||
ops.def(
|
||||
"persistent_masked_m_silu_mul_quant(Tensor input, Tensor counts, Tensor! "
|
||||
"y_q, Tensor! y_s,"
|
||||
"bool use_ue8m0) -> ()");
|
||||
ops.impl("persistent_masked_m_silu_mul_quant", torch::kCUDA,
|
||||
&persistent_masked_m_silu_mul_quant);
|
||||
|
||||
ops.def("weak_ref_tensor(Tensor input) -> Tensor");
|
||||
ops.impl("weak_ref_tensor", torch::kCUDA, &weak_ref_tensor);
|
||||
|
||||
#ifdef USE_ROCM
|
||||
// TODO: Remove this once we upgrade to torch 2.11.
|
||||
@@ -39,35 +28,6 @@ TORCH_LIBRARY_EXPAND(TORCH_EXTENSION_NAME, ops) {
|
||||
ops.def("get_cuda_view_from_cpu_tensor(Tensor cpu_tensor) -> Tensor");
|
||||
ops.impl("get_cuda_view_from_cpu_tensor", torch::kCPU,
|
||||
&get_cuda_view_from_cpu_tensor);
|
||||
#endif
|
||||
|
||||
// Activation ops (quantized only — basic ops moved to _C_stable_libtorch)
|
||||
ops.def(
|
||||
"silu_and_mul_quant(Tensor! result, Tensor input, Tensor scale) -> ()");
|
||||
ops.impl("silu_and_mul_quant", torch::kCUDA, &silu_and_mul_quant);
|
||||
|
||||
// Horizontally-fused DeepseekV4-MLA: per-head RMSNorm + GPT-J RoPE for Q, and
|
||||
// GPT-J RoPE + UE8M0 FP8 quant + paged cache insert for KV, all in one
|
||||
// kernel launch. Registered in _C_stable_libtorch (incl. the FlashInfer V4
|
||||
// full-cache bf16/fp8 variants).
|
||||
|
||||
// Quantization ops
|
||||
#ifndef USE_ROCM
|
||||
|
||||
// Note about marlin kernel 'workspace' arguments:
|
||||
// Technically these should be mutable since they are modified by the kernel.
|
||||
// But since they are set back to zero once the kernel is finished we can
|
||||
// hand wave and say that they have no net effect.
|
||||
//
|
||||
// The reason to mark 'workspace' as immutable is so that they don't interfere
|
||||
// with using ScalarType arguments in the ops. If they are marked as mutable,
|
||||
// pytorch throws an assert in
|
||||
// 'torch._higher_order_ops._register_effectful_op' that prevents these
|
||||
// kernels from being torch.compile'd.
|
||||
// See the following document for more info on custom types and ops that use
|
||||
// custom types:
|
||||
// https://docs.google.com/document/d/18fBMPuOJ0fY5ZQ6YyrHUppw9FA332CpNtgB6SOIgyuA
|
||||
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -792,7 +792,7 @@ RUN --mount=type=cache,target=/opt/uv/cache \
|
||||
# Install FlashInfer JIT cache (requires CUDA-version-specific index URL)
|
||||
# https://docs.flashinfer.ai/installation.html
|
||||
# From versions.json: .flashinfer.version
|
||||
ARG FLASHINFER_VERSION=0.6.12
|
||||
ARG FLASHINFER_VERSION=0.6.13rc2
|
||||
RUN --mount=type=cache,target=/opt/uv/cache \
|
||||
uv pip install --system flashinfer-jit-cache==${FLASHINFER_VERSION} \
|
||||
--index-url https://flashinfer.ai/whl/cu$(echo $CUDA_VERSION | cut -d. -f1,2 | tr -d '.')
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
"default": "true"
|
||||
},
|
||||
"FLASHINFER_VERSION": {
|
||||
"default": "0.6.12"
|
||||
"default": "0.6.13rc2"
|
||||
},
|
||||
"GDRCOPY_CUDA_VERSION": {
|
||||
"default": "12.8"
|
||||
|
||||
@@ -133,7 +133,7 @@ The model should inherit protocol `IsAttentionFree` and also implement class met
|
||||
For the mamba layers themselves, please use the [`MambaMixer`](../../../vllm/model_executor/layers/mamba/mamba_mixer.py) (for Mamba-1) or [`MambaMixer2`](../../../vllm/model_executor/layers/mamba/mamba_mixer2.py) (for Mamba-2) classes.
|
||||
The model should also be added to the `MODELS_CONFIG_MAP` dictionary in [vllm/model_executor/models/config.py](../../../vllm/model_executor/models/config.py) to ensure that the runtime defaults are optimized.
|
||||
|
||||
For case (2), we recommend using as a reference the implementation of [`JambaForCausalLM`](../../../vllm/model_executor/models/jamba.py) (for an example of a model that uses Mamba-1 and attention together) or [`BambaForCausalLM`](../../../vllm/model_executor/models/bamba.py) (for an example of a model that uses Mamba-2 and attention together).
|
||||
For case (2), we recommend using as a reference the implementation of [`JambaForCausalLM`](../../../vllm/model_executor/models/jamba.py) (for an example of a model that uses Mamba-1 and attention together) or [`NemotronHForCausalLM`](../../../vllm/model_executor/models/nemotron_h.py) (for an example of a model that uses Mamba-2 and attention together).
|
||||
These models should follow the same instructions as case (1), but they should inherit protocol `IsHybrid` (instead of `IsAttentionFree`) and it is *not* necessary to add them to the `MODELS_CONFIG_MAP` (their runtime defaults will be inferred from the protocol).
|
||||
|
||||
For case (3), we recommend looking at the implementation of [`MiniMaxText01ForCausalLM`](../../../vllm/model_executor/models/minimax_text_01.py) or [`Lfm2ForCausalLM`](../../../vllm/model_executor/models/lfm2.py) as a reference, which use custom "mamba-like" layers `MiniMaxText01LinearAttention` and `ShortConv` respectively.
|
||||
|
||||
@@ -9,8 +9,8 @@ torchaudio==2.11.0
|
||||
# These must be updated alongside torch
|
||||
torchvision==0.26.0 # Required for phi3v processor. See https://github.com/pytorch/vision?tab=readme-ov-file#installation for corresponding version
|
||||
# FlashInfer should be updated together with the Dockerfile
|
||||
flashinfer-python==0.6.12
|
||||
flashinfer-cubin==0.6.12
|
||||
flashinfer-python==0.6.13rc2
|
||||
flashinfer-cubin==0.6.13rc2
|
||||
apache-tvm-ffi==0.1.9
|
||||
tilelang==0.1.9
|
||||
nvidia-cudnn-frontend>=1.19.1
|
||||
|
||||
@@ -40,7 +40,7 @@ lm-eval[api]>=0.4.12 # required for model evaluation test
|
||||
mteb[bm25s]>=2, <3 # required for mteb test
|
||||
transformers==5.5.3
|
||||
tokenizers==0.22.2
|
||||
schemathesis>=3.39.15 # Required for openai schema test.
|
||||
schemathesis>=4.0.0 # Required for openai schema test.
|
||||
# quantization
|
||||
bitsandbytes==0.49.2
|
||||
buildkite-test-collector==0.1.9
|
||||
|
||||
@@ -31,7 +31,7 @@ lm-eval[api]>=0.4.12 # required for model evaluation test
|
||||
mteb[bm25s]>=2, <3 # required for mteb test
|
||||
transformers==5.5.3
|
||||
tokenizers==0.22.2
|
||||
schemathesis>=3.39.15 # Required for openai schema test.
|
||||
schemathesis>=4.0.0 # Required for openai schema test.
|
||||
# quantization
|
||||
bitsandbytes>=0.49.2
|
||||
buildkite-test-collector==0.1.9
|
||||
|
||||
@@ -39,7 +39,7 @@ lm-eval[api]>=0.4.12 # required for model evaluation test
|
||||
mteb[bm25s]>=2, <3 # required for mteb test
|
||||
transformers==5.5.3
|
||||
tokenizers==0.22.2
|
||||
schemathesis>=3.39.15 # Required for openai schema test
|
||||
schemathesis>=4.0.0 # Required for openai schema test
|
||||
# quantization
|
||||
bitsandbytes==0.49.2
|
||||
buildkite-test-collector==0.1.9
|
||||
|
||||
@@ -769,6 +769,7 @@ class precompiled_wheel_utils:
|
||||
"vllm/_C.abi3.so",
|
||||
"vllm/_C_stable_libtorch.abi3.so",
|
||||
"vllm/_moe_C_stable_libtorch.abi3.so",
|
||||
"vllm/_qutlass_C.abi3.so",
|
||||
"vllm/_flashmla_C.abi3.so",
|
||||
"vllm/_flashmla_extension_C.abi3.so",
|
||||
"vllm/_sparse_flashmla_C.abi3.so",
|
||||
@@ -1135,6 +1136,7 @@ if _is_cuda():
|
||||
# DeepGEMM requires CUDA 12.3+ (SM90/SM100)
|
||||
# Optional since it won't build on unsupported architectures
|
||||
ext_modules.append(CMakeExtension(name="vllm._deep_gemm_C", optional=True))
|
||||
ext_modules.append(CMakeExtension(name="vllm._qutlass_C", optional=True))
|
||||
# fmha_sm100 is a Python/CuTe-DSL package installed into vllm.third_party.
|
||||
ext_modules.append(CMakeExtension(name="vllm.fmha_sm100", optional=True))
|
||||
|
||||
@@ -1149,7 +1151,8 @@ if _is_cpu():
|
||||
ext_modules.append(CMakeExtension(name="vllm._C"))
|
||||
|
||||
if _build_custom_ops():
|
||||
ext_modules.append(CMakeExtension(name="vllm._C"))
|
||||
if _is_hip():
|
||||
ext_modules.append(CMakeExtension(name="vllm._C"))
|
||||
if _is_cuda() or _is_hip():
|
||||
ext_modules.append(CMakeExtension(name="vllm._C_stable_libtorch"))
|
||||
ext_modules.append(CMakeExtension(name="vllm._moe_C_stable_libtorch"))
|
||||
@@ -1168,7 +1171,15 @@ package_data = {
|
||||
"third_party/deep_gemm/include/**/*.h",
|
||||
"third_party/deep_gemm/include/**/*.hpp",
|
||||
# fmha_sm100 sparse CuTe-DSL helper kernels (vendored via cmake)
|
||||
"third_party/fmha_sm100/csrc/**/*.cu",
|
||||
"third_party/fmha_sm100/csrc/**/*.h",
|
||||
"third_party/fmha_sm100/csrc/**/*.jinja",
|
||||
"third_party/fmha_sm100/csrc/**/*.cu.jinja",
|
||||
"third_party/fmha_sm100/cute/**/*.cu",
|
||||
"third_party/fmha_sm100/cutlass/include/**/*.h",
|
||||
"third_party/fmha_sm100/cutlass/include/**/*.hpp",
|
||||
"third_party/fmha_sm100/cutlass/tools/util/include/**/*.h",
|
||||
"third_party/fmha_sm100/cutlass/tools/util/include/**/*.hpp",
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@@ -649,3 +649,196 @@ def test_cloud_storage_tokenizer_skips_get_model_path(monkeypatch):
|
||||
args = EngineArgs(model="s3://bucket/model", tokenizer="s3://bucket/tokenizer")
|
||||
assert args.model == "s3://bucket/model"
|
||||
assert args.tokenizer == "s3://bucket/tokenizer"
|
||||
|
||||
|
||||
class TestDeviceIds:
|
||||
def test_device_ids_with_cvd_out_of_range(self, monkeypatch):
|
||||
"""--device-ids index beyond the CVD set raises ValueError."""
|
||||
from vllm.platforms import current_platform
|
||||
|
||||
key = current_platform.device_control_env_var
|
||||
monkeypatch.setenv(key, "4,5")
|
||||
args = EngineArgs(model="m", device_ids=[0, 2])
|
||||
with pytest.raises(ValueError, match="out of range"):
|
||||
args._resolve_device_ids()
|
||||
|
||||
def test_device_ids_with_cvd_resolve_to_physical_ids(self, monkeypatch):
|
||||
"""--device-ids are CVD-local indices resolved to physical ids."""
|
||||
from vllm.platforms import current_platform
|
||||
|
||||
key = current_platform.device_control_env_var
|
||||
monkeypatch.setenv(key, "4,5")
|
||||
args = EngineArgs(model="m", device_ids=[0, 1])
|
||||
assert args._resolve_device_ids() == [4, 5]
|
||||
|
||||
def test_device_ids_with_uuid_cvd_resolve_to_physical_ids(self, monkeypatch):
|
||||
"""--device-ids support UUID CVD values resolved by the platform."""
|
||||
from vllm.platforms import current_platform
|
||||
|
||||
key = current_platform.device_control_env_var
|
||||
monkeypatch.setenv(key, "GPU-abcd1234,GPU-ef567890")
|
||||
monkeypatch.setattr(
|
||||
type(current_platform),
|
||||
"device_control_id_to_physical_device_id",
|
||||
classmethod(
|
||||
lambda cls, device_id: {"GPU-abcd1234": 4, "GPU-ef567890": 5}[device_id]
|
||||
),
|
||||
)
|
||||
|
||||
args = EngineArgs(model="m", device_ids=[0, 1])
|
||||
assert args._resolve_device_ids() == [4, 5]
|
||||
|
||||
def test_device_ids_with_uuid_args_resolve_to_physical_ids(self, monkeypatch):
|
||||
"""UUID --device-ids are resolved to physical IDs immediately."""
|
||||
from vllm.platforms import current_platform
|
||||
|
||||
monkeypatch.setattr(
|
||||
type(current_platform),
|
||||
"device_control_id_to_physical_device_id",
|
||||
classmethod(lambda cls, device_id: {"GPU-abcd1234": 4}[device_id]),
|
||||
)
|
||||
|
||||
args = EngineArgs(model="m", device_ids=["GPU-abcd1234"])
|
||||
assert args._resolve_device_ids() == [4]
|
||||
|
||||
def test_device_ids_reject_mixed_integer_and_uuid_args(self):
|
||||
"""--device-ids must not mix CVD indices and UUIDs."""
|
||||
args = EngineArgs(model="m", device_ids=[0, "GPU-abcd1234"])
|
||||
with pytest.raises(ValueError, match="must not mix"):
|
||||
args._resolve_device_ids()
|
||||
|
||||
def test_no_device_ids(self):
|
||||
"""No --device-ids returns None."""
|
||||
args = EngineArgs(model="m")
|
||||
assert args._resolve_device_ids() is None
|
||||
|
||||
def test_cli_parsing(self):
|
||||
"""--device-ids parses comma-separated string from CLI."""
|
||||
parser = FlexibleArgumentParser()
|
||||
EngineArgs.add_cli_args(parser)
|
||||
parsed = parser.parse_args(["--model", "m", "--device-ids", "0,2,4"])
|
||||
assert parsed.device_ids == [0, 2, 4]
|
||||
|
||||
def test_cli_parsing_uuid(self):
|
||||
"""--device-ids parses comma-separated UUID strings from CLI."""
|
||||
parser = FlexibleArgumentParser()
|
||||
EngineArgs.add_cli_args(parser)
|
||||
parsed = parser.parse_args(
|
||||
["--model", "m", "--device-ids", "GPU-abcd1234,GPU-ef567890"]
|
||||
)
|
||||
assert parsed.device_ids == ["GPU-abcd1234", "GPU-ef567890"]
|
||||
|
||||
def test_assigned_physical_gpu_ids_are_physical_with_cvd(self, monkeypatch):
|
||||
"""assigned_physical_gpu_ids are already physical and not composed with CVD."""
|
||||
import vllm.platforms.interface as platform_interface
|
||||
from vllm.platforms import current_platform
|
||||
|
||||
monkeypatch.setattr(platform_interface, "_assigned_physical_gpu_ids", [4, 5])
|
||||
monkeypatch.setenv(current_platform.device_control_env_var, "4,5")
|
||||
|
||||
assert current_platform.device_id_to_physical_device_id(0) == 4
|
||||
assert current_platform.device_id_to_physical_device_id(1) == 5
|
||||
assert current_platform.logical_device_id_to_visible_device_id(0) == 0
|
||||
assert current_platform.logical_device_id_to_visible_device_id(1) == 1
|
||||
|
||||
def test_assigned_physical_gpu_ids_map_to_visible_uuid_cvd(self, monkeypatch):
|
||||
"""Physical IDs map back to visible ordinals when CVD uses UUIDs."""
|
||||
import vllm.platforms.interface as platform_interface
|
||||
from vllm.platforms import current_platform
|
||||
|
||||
monkeypatch.setattr(platform_interface, "_assigned_physical_gpu_ids", [5])
|
||||
monkeypatch.setenv(
|
||||
current_platform.device_control_env_var,
|
||||
"GPU-abcd1234,GPU-ef567890",
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
type(current_platform),
|
||||
"device_control_id_to_physical_device_id",
|
||||
classmethod(
|
||||
lambda cls, device_id: {"GPU-abcd1234": 4, "GPU-ef567890": 5}[device_id]
|
||||
),
|
||||
)
|
||||
|
||||
assert current_platform.logical_device_id_to_visible_device_id(0) == 1
|
||||
|
||||
def test_device_ids_reject_duplicates(self):
|
||||
"""--device-ids must not contain duplicate entries."""
|
||||
args = EngineArgs(model="m", device_ids=[2, 2])
|
||||
with pytest.raises(ValueError, match="duplicates"):
|
||||
args._resolve_device_ids()
|
||||
|
||||
def test_cli_parsing_strips_whitespace(self):
|
||||
"""--device-ids tolerates whitespace around commas."""
|
||||
parser = FlexibleArgumentParser()
|
||||
EngineArgs.add_cli_args(parser)
|
||||
parsed = parser.parse_args(["--model", "m", "--device-ids", "0, 2, 4"])
|
||||
assert parsed.device_ids == [0, 2, 4]
|
||||
|
||||
def test_visible_ordinal_to_physical_ignores_assigned_ids(self, monkeypatch):
|
||||
"""visible_device_id_to_physical_device_id maps torch device ordinals,
|
||||
independent of the logical-to-physical mapping.
|
||||
|
||||
Regression test: CustomAllreduce passes device.index (a visible
|
||||
ordinal) and must not index into assigned_physical_gpu_ids, which
|
||||
raised IndexError for non-identity --device-ids like [2, 3].
|
||||
"""
|
||||
import vllm.platforms.interface as platform_interface
|
||||
from vllm.platforms import current_platform
|
||||
|
||||
monkeypatch.setattr(platform_interface, "_assigned_physical_gpu_ids", [2, 3])
|
||||
monkeypatch.delenv(current_platform.device_control_env_var, raising=False)
|
||||
|
||||
# CVD unset: visible ordinal == physical ID, even beyond the
|
||||
# assigned list's length.
|
||||
assert current_platform.visible_device_id_to_physical_device_id(2) == 2
|
||||
assert current_platform.visible_device_id_to_physical_device_id(3) == 3
|
||||
|
||||
monkeypatch.setenv(current_platform.device_control_env_var, "4,5")
|
||||
assert current_platform.visible_device_id_to_physical_device_id(1) == 5
|
||||
with pytest.raises(IndexError, match="out of range"):
|
||||
current_platform.visible_device_id_to_physical_device_id(2)
|
||||
|
||||
|
||||
class TestDpDeviceIdSharding:
|
||||
def test_dp_supervisor_device_ids_stay_env_relative(self):
|
||||
"""Regression test: the DP supervisor must pass env-relative indices,
|
||||
not physical IDs, because each child re-resolves --device-ids
|
||||
against its inherited device-control env var."""
|
||||
import argparse
|
||||
|
||||
from vllm.entrypoints.openai.dp_supervisor import _build_device_ids
|
||||
|
||||
args = argparse.Namespace(
|
||||
tensor_parallel_size=2, pipeline_parallel_size=1, device_ids=None
|
||||
)
|
||||
assert _build_device_ids(args, local_rank=0) == [0, 1]
|
||||
assert _build_device_ids(args, local_rank=1) == [2, 3]
|
||||
|
||||
def test_dp_supervisor_shards_user_device_ids(self):
|
||||
"""User-provided --device-ids are sharded across DP children."""
|
||||
import argparse
|
||||
|
||||
from vllm.entrypoints.openai.dp_supervisor import _build_device_ids
|
||||
|
||||
args = argparse.Namespace(
|
||||
tensor_parallel_size=2, pipeline_parallel_size=1, device_ids=[4, 5, 6, 7]
|
||||
)
|
||||
assert _build_device_ids(args, local_rank=0) == [4, 5]
|
||||
assert _build_device_ids(args, local_rank=1) == [6, 7]
|
||||
with pytest.raises(ValueError, match="needs devices"):
|
||||
_build_device_ids(args, local_rank=2)
|
||||
|
||||
def test_dp_rank_shards_user_assigned_gpu_ids(self):
|
||||
"""get_physical_gpu_ids_for_local_dp_rank slices the user-provided
|
||||
--device-ids list instead of recomputing from the env var."""
|
||||
from vllm.platforms import current_platform
|
||||
from vllm.v1.engine.utils import get_physical_gpu_ids_for_local_dp_rank
|
||||
|
||||
evar = current_platform.device_control_env_var
|
||||
assert get_physical_gpu_ids_for_local_dp_rank(
|
||||
evar, local_dp_rank=1, world_size=2, user_assigned_gpu_ids=[4, 5, 6, 7]
|
||||
) == [6, 7]
|
||||
with pytest.raises(ValueError, match="needs devices"):
|
||||
get_physical_gpu_ids_for_local_dp_rank(
|
||||
evar, local_dp_rank=2, world_size=2, user_assigned_gpu_ids=[4, 5, 6, 7]
|
||||
)
|
||||
|
||||
@@ -364,7 +364,7 @@ class MockVLLMServer:
|
||||
await self._serve_task
|
||||
|
||||
|
||||
def launch_mock_vllm(child_args: argparse.Namespace, env_updates: dict[str, str]):
|
||||
def launch_mock_vllm(child_args: argparse.Namespace):
|
||||
logger.info("Launching mock vLLM on port %s", child_args.port)
|
||||
mock_vllm = MockVLLMServer(
|
||||
port=child_args.port,
|
||||
@@ -375,7 +375,7 @@ def launch_mock_vllm(child_args: argparse.Namespace, env_updates: dict[str, str]
|
||||
|
||||
|
||||
def launch_mock_vllm_with_drain(
|
||||
child_args: argparse.Namespace, env_updates: dict[str, str]
|
||||
child_args: argparse.Namespace,
|
||||
):
|
||||
logger.info("Launching mock vLLM with 15s drain on port %s", child_args.port)
|
||||
mock_vllm = MockVLLMServer(
|
||||
|
||||
@@ -6,15 +6,22 @@ from typing import Final
|
||||
import pytest
|
||||
import schemathesis
|
||||
from hypothesis import HealthCheck, settings
|
||||
from schemathesis import GenerationConfig
|
||||
from schemathesis.models import Case
|
||||
from schemathesis import GenerationMode
|
||||
from schemathesis.config import (
|
||||
ChecksConfig,
|
||||
CoveragePhaseConfig,
|
||||
GenerationConfig,
|
||||
PhasesConfig,
|
||||
PositiveDataAcceptanceConfig,
|
||||
ProjectConfig,
|
||||
ProjectsConfig,
|
||||
SchemathesisConfig,
|
||||
)
|
||||
|
||||
from vllm.platforms import current_platform
|
||||
|
||||
from ...utils import RemoteOpenAIServer
|
||||
|
||||
schemathesis.experimental.OPEN_API_3_1.enable()
|
||||
|
||||
MODEL_NAME = "HuggingFaceTB/SmolVLM-256M-Instruct"
|
||||
MAXIMUM_IMAGES = 2
|
||||
_ROCM_TIMEOUT_MULTIPLIER = 3 if current_platform.is_rocm() else 1
|
||||
@@ -44,21 +51,38 @@ def server():
|
||||
@pytest.fixture(scope="module")
|
||||
def get_schema(server):
|
||||
# avoid generating null (\x00) bytes in strings during test case generation
|
||||
return schemathesis.openapi.from_uri(
|
||||
return schemathesis.openapi.from_url(
|
||||
f"{server.url_root}/openapi.json",
|
||||
generation_config=GenerationConfig(allow_x00=False),
|
||||
config=SchemathesisConfig(
|
||||
projects=ProjectsConfig(
|
||||
default=ProjectConfig(
|
||||
generation=GenerationConfig(
|
||||
allow_x00=False,
|
||||
modes=[GenerationMode.POSITIVE],
|
||||
),
|
||||
checks=ChecksConfig(
|
||||
positive_data_acceptance=PositiveDataAcceptanceConfig(
|
||||
enabled=False,
|
||||
),
|
||||
),
|
||||
phases=PhasesConfig(
|
||||
coverage=CoveragePhaseConfig(enabled=False),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
schema = schemathesis.from_pytest_fixture("get_schema")
|
||||
schema = schemathesis.pytest.from_fixture("get_schema")
|
||||
|
||||
|
||||
@schemathesis.hook
|
||||
def before_generate_case(context: schemathesis.hooks.HookContext, strategy):
|
||||
def before_generate_case(context: schemathesis.HookContext, strategy):
|
||||
op = context.operation
|
||||
assert op is not None
|
||||
|
||||
def no_invalid_types(case: schemathesis.models.Case):
|
||||
def no_invalid_types(case: schemathesis.Case):
|
||||
"""
|
||||
Skips tool_calls with `"type": "custom"` which schemathesis incorrectly
|
||||
generates instead of the valid `"type": "function"`.
|
||||
@@ -68,39 +92,25 @@ def before_generate_case(context: schemathesis.hooks.HookContext, strategy):
|
||||
-d '{"messages": [{"role": "assistant", "tool_calls": [{"custom": {"input": "", "name": ""}, "id": "", "type": "custom"}]}]}' \
|
||||
http://localhost:8000/v1/chat/completions
|
||||
""" # noqa: E501
|
||||
if hasattr(case, "body") and isinstance(case.body, dict):
|
||||
if (
|
||||
"messages" in case.body
|
||||
and isinstance(case.body["messages"], list)
|
||||
and len(case.body["messages"]) > 0
|
||||
):
|
||||
for message in case.body["messages"]:
|
||||
if not isinstance(message, dict):
|
||||
continue
|
||||
if (
|
||||
hasattr(case, "body")
|
||||
and isinstance(case.body, dict)
|
||||
and "messages" in case.body
|
||||
and isinstance(case.body["messages"], list)
|
||||
and len(case.body["messages"]) > 0
|
||||
):
|
||||
for message in case.body["messages"]:
|
||||
if not isinstance(message, dict):
|
||||
continue
|
||||
|
||||
tool_calls = message.get("tool_calls", [])
|
||||
if isinstance(tool_calls, list):
|
||||
for tool_call in tool_calls:
|
||||
if isinstance(tool_call, dict):
|
||||
if tool_call.get("type") != "function":
|
||||
return False
|
||||
if "custom" in tool_call:
|
||||
return False
|
||||
|
||||
# Sometimes structured_outputs.grammar is generated to be empty
|
||||
# Causing a server error in EBNF grammar parsing
|
||||
# https://github.com/vllm-project/vllm/pull/22587#issuecomment-3195253421
|
||||
structured_outputs = case.body.get("structured_outputs", {})
|
||||
grammar = (
|
||||
structured_outputs.get("grammar")
|
||||
if isinstance(structured_outputs, dict)
|
||||
else None
|
||||
)
|
||||
|
||||
if grammar == "":
|
||||
# Allow None (will be handled as no grammar)
|
||||
# But skip empty strings
|
||||
return False
|
||||
tool_calls = message.get("tool_calls", [])
|
||||
if isinstance(tool_calls, list):
|
||||
for tool_call in tool_calls:
|
||||
if isinstance(tool_call, dict):
|
||||
if tool_call.get("type") != "function":
|
||||
return False
|
||||
if "custom" in tool_call:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@@ -108,7 +118,6 @@ def before_generate_case(context: schemathesis.hooks.HookContext, strategy):
|
||||
|
||||
|
||||
@schema.parametrize()
|
||||
@schema.override(headers={"Content-Type": "application/json"})
|
||||
@settings(
|
||||
deadline=LONG_TIMEOUT_SECONDS * 1000,
|
||||
max_examples=50,
|
||||
@@ -122,7 +131,7 @@ def before_generate_case(context: schemathesis.hooks.HookContext, strategy):
|
||||
# generating large-but-valid request bodies before vLLM is called.
|
||||
suppress_health_check=[HealthCheck.filter_too_much, HealthCheck.data_too_large],
|
||||
)
|
||||
def test_openapi_stateless(case: Case):
|
||||
def test_openapi_stateless(case: schemathesis.Case):
|
||||
key = (
|
||||
case.operation.method.upper(),
|
||||
case.operation.path,
|
||||
@@ -151,4 +160,8 @@ def test_openapi_stateless(case: Case):
|
||||
}.get(key, DEFAULT_TIMEOUT_SECONDS)
|
||||
|
||||
# No need to verify SSL certificate for localhost
|
||||
case.call_and_validate(verify=False, timeout=timeout)
|
||||
case.call_and_validate(
|
||||
verify=False,
|
||||
timeout=timeout,
|
||||
headers={"Content-Type": "application/json"},
|
||||
)
|
||||
|
||||
@@ -78,7 +78,16 @@ def test_gsm8k_correctness(config_filename):
|
||||
"Skipping DeepSeek-V3.2 and DeepSeek-R1 on ROCm platforms "
|
||||
"due to agent pool disk space issues and pod evictions."
|
||||
)
|
||||
if current_platform.is_rocm() and (
|
||||
"Qwen3.5-35B-A3B-MXFP4-AITER-TP2" in config_filename.name
|
||||
):
|
||||
from vllm.platforms.rocm import on_gfx950
|
||||
|
||||
if not on_gfx950():
|
||||
pytest.skip(
|
||||
"Skipping Qwen3.5-35B-A3B-MXFP4-AITER-TP2 on non-GFX950 platforms. "
|
||||
"The quantization scheme is not supported on non-GFX950 platforms."
|
||||
)
|
||||
# Parse server arguments from config (use shlex to handle quoted strings)
|
||||
server_args_str = eval_config.get("server_args", "")
|
||||
server_args = shlex.split(server_args_str) if server_args_str else []
|
||||
|
||||
@@ -134,6 +134,7 @@ def _reference_index_topk(
|
||||
topk: int,
|
||||
init_blocks: int,
|
||||
local_blocks: int,
|
||||
sm_scale: float = 1.0,
|
||||
) -> torch.Tensor:
|
||||
total_q, num_idx_heads, _ = idx_q.shape
|
||||
out = torch.full(
|
||||
@@ -149,7 +150,7 @@ def _reference_index_topk(
|
||||
num_blocks = (seq_len + BLOCK_SIZE - 1) // BLOCK_SIZE
|
||||
pages = block_table[req_id, :num_blocks]
|
||||
k = index_kv_cache[pages].reshape(num_blocks * BLOCK_SIZE, -1)
|
||||
score = torch.einsum("qhd,kd->hqk", q.float(), k.float())
|
||||
score = torch.einsum("qhd,kd->hqk", q.float(), k.float()) * sm_scale
|
||||
|
||||
q_pos = prefix_len + torch.arange(q_len, device=idx_q.device)
|
||||
k_pos = torch.arange(k.shape[0], device=idx_q.device)
|
||||
@@ -244,6 +245,270 @@ def test_prefill_index_topk_correctness():
|
||||
_assert_topk_indices_equal_unordered(actual, expected)
|
||||
|
||||
|
||||
# MSA indexer (SM100): fmha_sm100 OnlyScore for the per-block scores, then the
|
||||
# Triton minimax_m3_index_topk for selection (no sparse_topk_select). Uses a
|
||||
# deterministic construction (idx_q == 1, distinct e4m3-exact per-block values)
|
||||
# so scores are strictly monotonic in the block id -> exact top-k agreement.
|
||||
def _fmha_indexer_topk(
|
||||
idx_q: torch.Tensor, # [total_q, H, 128] bf16/e4m3
|
||||
index_cache: torch.Tensor, # [num_pages, 128, 128] bf16/e4m3
|
||||
block_table: torch.Tensor,
|
||||
q_lens: torch.Tensor,
|
||||
seq_lens: torch.Tensor,
|
||||
prefix_lens: torch.Tensor,
|
||||
sm_scale: float,
|
||||
topk: int,
|
||||
) -> torch.Tensor:
|
||||
"""Replicate MiniMaxM3IndexerMSAImpl's score path (single decode/prefill side)."""
|
||||
from vllm.third_party.fmha_sm100.api import _fmha_sm100, _fmha_sm100_plan
|
||||
|
||||
num_idx_heads, head_dim = idx_q.shape[1], idx_q.shape[2]
|
||||
nvp = [(s + 127) // 128 for s in seq_lens.tolist()]
|
||||
kv_indices = torch.cat([block_table[r, : nvp[r]] for r in range(len(nvp))]).to(
|
||||
torch.int32
|
||||
)
|
||||
|
||||
qo = q_lens.cpu().to(torch.int32)
|
||||
kv = seq_lens.cpu().to(torch.int32)
|
||||
plan = _fmha_sm100_plan(
|
||||
qo,
|
||||
kv,
|
||||
num_idx_heads,
|
||||
num_kv_heads=1,
|
||||
qo_offset=kv - qo,
|
||||
page_size=128,
|
||||
output_maxscore=True,
|
||||
causal=True,
|
||||
num_kv_splits=1,
|
||||
)
|
||||
k_pages = index_cache.view(index_cache.shape[0], 1, 128, head_dim)
|
||||
_, max_score = _fmha_sm100(
|
||||
idx_q,
|
||||
k_pages,
|
||||
k_pages,
|
||||
plan,
|
||||
kv_indices=kv_indices,
|
||||
output_o=False,
|
||||
output_maxscore=True,
|
||||
sm_scale=sm_scale,
|
||||
)
|
||||
|
||||
batch = q_lens.numel()
|
||||
cu = torch.zeros(batch + 1, dtype=torch.int32, device=idx_q.device)
|
||||
cu[1:] = q_lens.to(torch.int32).cumsum(0)
|
||||
# max_score [H, k_tiles, total_q] -> transpose to [H, total_q, k_tiles].
|
||||
return minimax_m3_index_topk(
|
||||
max_score.transpose(1, 2),
|
||||
cu,
|
||||
prefix_lens.to(torch.int32),
|
||||
int(q_lens.max()),
|
||||
topk,
|
||||
0, # init_blocks
|
||||
0, # local_blocks
|
||||
)
|
||||
|
||||
|
||||
# e4m3-exact, strictly-increasing per-block values: with idx_q == 1 (also exact)
|
||||
# the per-block scores are exact and distinct in BOTH bf16 and e4m3, so the fp8
|
||||
# score path selects the same top-k as the reference (no quantization ties).
|
||||
_E4M3_EXACT_VALUES = [
|
||||
*range(1, 17), # 1..16 (step 1)
|
||||
*range(18, 33, 2), # 18..32 (step 2)
|
||||
*range(36, 65, 4), # 36..64 (step 4)
|
||||
*range(72, 129, 8), # 72..128 (step 8)
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not current_platform.is_device_capability_family(100),
|
||||
reason="fmha_sm100 indexer requires SM100 (Blackwell).",
|
||||
)
|
||||
@pytest.mark.parametrize("index_dtype", [torch.bfloat16, torch.float8_e4m3fn])
|
||||
@pytest.mark.parametrize(
|
||||
("q_lens", "prefix_lens"),
|
||||
[
|
||||
((4, 3), (2048, 2560)), # prefill: every token sees >= 16 causal blocks
|
||||
((1, 1, 1), (2048, 3000, 4096)), # decode: one query token per request
|
||||
],
|
||||
)
|
||||
def test_fmha_sm100_indexer_matches_reference(q_lens, prefix_lens, index_dtype):
|
||||
torch.manual_seed(0)
|
||||
num_idx_heads, head_dim = 4, HEAD_DIM
|
||||
device = "cuda"
|
||||
|
||||
q_lens_t = torch.tensor(q_lens, device=device, dtype=torch.int32)
|
||||
prefix_lens_t = torch.tensor(prefix_lens, device=device, dtype=torch.int32)
|
||||
seq_lens = prefix_lens_t + q_lens_t
|
||||
batch = len(q_lens)
|
||||
max_blocks = (int(seq_lens.max()) + BLOCK_SIZE - 1) // BLOCK_SIZE
|
||||
assert max_blocks <= len(_E4M3_EXACT_VALUES)
|
||||
num_pages = batch * max_blocks
|
||||
block_table = torch.randperm(num_pages, device=device, dtype=torch.int32).reshape(
|
||||
batch, max_blocks
|
||||
)
|
||||
|
||||
idx_q = torch.ones(
|
||||
int(q_lens_t.sum()), num_idx_heads, head_dim, device=device, dtype=index_dtype
|
||||
)
|
||||
index_cache = torch.empty(
|
||||
num_pages, BLOCK_SIZE, head_dim, device=device, dtype=index_dtype
|
||||
)
|
||||
for r in range(batch):
|
||||
for b in range(max_blocks):
|
||||
index_cache[block_table[r, b]] = float(_E4M3_EXACT_VALUES[b])
|
||||
|
||||
sm_scale = head_dim**-0.5
|
||||
actual = _fmha_indexer_topk(
|
||||
idx_q,
|
||||
index_cache,
|
||||
block_table,
|
||||
q_lens_t,
|
||||
seq_lens,
|
||||
prefix_lens_t,
|
||||
sm_scale,
|
||||
TOPK,
|
||||
)
|
||||
expected = _reference_index_topk(
|
||||
idx_q,
|
||||
index_cache,
|
||||
block_table,
|
||||
q_lens_t,
|
||||
seq_lens,
|
||||
prefix_lens_t,
|
||||
TOPK,
|
||||
init_blocks=0,
|
||||
local_blocks=0,
|
||||
sm_scale=sm_scale,
|
||||
)
|
||||
_assert_topk_indices_equal_unordered(actual, expected)
|
||||
|
||||
|
||||
# Full impl-level parity: drive both MiniMaxM3IndexerMSAImpl (fmha_sm100 score +
|
||||
# Triton top-k) and MiniMaxM3IndexerTritonImpl through their real metadata
|
||||
# builders on the SAME CommonAttentionMetadata + index cache, and assert the
|
||||
# selected blocks agree. This exercises all the metadata the impl/kernels consume
|
||||
# (decode/prefill split, cu_seqlens_q rebasing, prefix_lens, kv_indices gather,
|
||||
# decode_pages split) -- a metadata bug on either side shifts the causal window
|
||||
# or the block->page mapping and breaks the comparison.
|
||||
@pytest.mark.skipif(
|
||||
not current_platform.is_device_capability_family(100),
|
||||
reason="fmha_sm100 indexer requires SM100 (Blackwell).",
|
||||
)
|
||||
@pytest.mark.parametrize("topk", [8, 16])
|
||||
def test_msa_indexer_impl_matches_triton(topk, monkeypatch):
|
||||
import vllm.models.minimax_m3.common.indexer as indexer_mod
|
||||
from tests.v1.attention.utils import (
|
||||
BatchSpec,
|
||||
create_common_attn_metadata,
|
||||
create_vllm_config,
|
||||
)
|
||||
from vllm.config import set_current_vllm_config
|
||||
from vllm.forward_context import set_forward_context
|
||||
from vllm.models.minimax_m3.common.indexer import (
|
||||
MiniMaxM3IndexerTritonImpl,
|
||||
MiniMaxM3IndexerTritonMetadataBuilder,
|
||||
)
|
||||
from vllm.models.minimax_m3.nvidia.indexer_msa import (
|
||||
MiniMaxM3IndexerMSAImpl,
|
||||
MiniMaxM3IndexerMSAMetadataBuilder,
|
||||
)
|
||||
|
||||
torch.manual_seed(0)
|
||||
device = torch.device("cuda")
|
||||
num_idx_heads, head_dim = 4, HEAD_DIM
|
||||
# TP=1: avoid requiring an initialized distributed group in a unit test.
|
||||
monkeypatch.setattr(indexer_mod, "get_tensor_model_parallel_world_size", lambda: 1)
|
||||
|
||||
vllm_config = create_vllm_config(
|
||||
block_size=BLOCK_SIZE, max_model_len=8192, max_num_batched_tokens=8192
|
||||
)
|
||||
vllm_config.model_config.hf_config.sparse_attention_config = {
|
||||
"sparse_num_index_heads": num_idx_heads
|
||||
}
|
||||
|
||||
# Decode-first mixed batch: 2 decode reqs (q_len 1) then 2 prefill reqs. Long
|
||||
# prefixes so every token sees > TOPK causal blocks (non-trivial selection).
|
||||
batch = BatchSpec(seq_lens=[2305, 2561, 2624, 2720], query_lens=[1, 1, 64, 96])
|
||||
common = create_common_attn_metadata(
|
||||
batch, BLOCK_SIZE, device, arange_block_indices=True
|
||||
)
|
||||
num_tokens = batch.compute_num_tokens()
|
||||
|
||||
# Deterministic index cache: distinct, monotonic per-logical-block values so
|
||||
# the top-k is unambiguous (both kernels pick the same blocks, no fp ties).
|
||||
block_table = common.block_table_tensor
|
||||
num_pages = int(block_table.max().item()) + 1
|
||||
index_cache = torch.zeros(
|
||||
num_pages, BLOCK_SIZE, head_dim, device=device, dtype=DTYPE
|
||||
)
|
||||
for r, seq_len in enumerate(batch.seq_lens):
|
||||
for b in range((seq_len + BLOCK_SIZE - 1) // BLOCK_SIZE):
|
||||
index_cache[block_table[r, b]] = float(b + 1)
|
||||
index_q = torch.ones(
|
||||
num_tokens, num_idx_heads * head_dim, device=device, dtype=DTYPE
|
||||
)
|
||||
|
||||
spec = MLAAttentionSpec(
|
||||
block_size=BLOCK_SIZE, num_kv_heads=1, head_size=head_dim, dtype=DTYPE
|
||||
)
|
||||
impl_kwargs = dict(
|
||||
num_kv_heads=num_idx_heads,
|
||||
scale=head_dim**-0.5,
|
||||
topk_blocks=topk,
|
||||
sparse_block_size=BLOCK_SIZE,
|
||||
num_index_heads=num_idx_heads,
|
||||
index_head_dim=head_dim,
|
||||
init_blocks=0,
|
||||
local_blocks=0,
|
||||
)
|
||||
|
||||
with set_current_vllm_config(vllm_config):
|
||||
msa_impl = MiniMaxM3IndexerMSAImpl(prefix="idx_msa", **impl_kwargs)
|
||||
triton_impl = MiniMaxM3IndexerTritonImpl(prefix="idx_triton", **impl_kwargs)
|
||||
msa_builder = MiniMaxM3IndexerMSAMetadataBuilder(
|
||||
spec, [msa_impl.index_cache.prefix], vllm_config, device
|
||||
)
|
||||
triton_builder = MiniMaxM3IndexerTritonMetadataBuilder(
|
||||
spec, [triton_impl.index_cache.prefix], vllm_config, device
|
||||
)
|
||||
|
||||
# Both impls score against the same index keys.
|
||||
msa_impl.index_cache.kv_cache = index_cache
|
||||
triton_impl.index_cache.kv_cache = index_cache
|
||||
|
||||
# Exercise the shared persistent top-k buffer for BOTH impls: each must write
|
||||
# decode ([:, :nd]) and prefill ([:, nd:]) into its buffer and return views.
|
||||
# Separate buffers so the two forwards don't clobber each other.
|
||||
nd = sum(q for q in batch.query_lens if q <= 1)
|
||||
msa_impl.topk_indices_buffer = torch.full(
|
||||
(num_idx_heads, num_tokens, topk), -2, dtype=torch.int32, device=device
|
||||
)
|
||||
triton_impl.topk_indices_buffer = torch.full(
|
||||
(num_idx_heads, num_tokens, topk), -2, dtype=torch.int32, device=device
|
||||
)
|
||||
|
||||
attn_metadata = {
|
||||
msa_impl.index_cache.prefix: msa_builder.build(0, common),
|
||||
triton_impl.index_cache.prefix: triton_builder.build(0, common),
|
||||
}
|
||||
with set_forward_context(attn_metadata, vllm_config):
|
||||
msa_decode, msa_prefill = msa_impl(index_q)
|
||||
tri_decode, tri_prefill = triton_impl(index_q)
|
||||
|
||||
assert msa_decode is not None and tri_decode is not None
|
||||
assert msa_prefill is not None and tri_prefill is not None
|
||||
_assert_topk_indices_equal_unordered(msa_decode, tri_decode)
|
||||
_assert_topk_indices_equal_unordered(msa_prefill, tri_prefill)
|
||||
# decode/prefill outputs are views into each impl's persistent buffer.
|
||||
for impl, dec, pre in (
|
||||
(msa_impl, msa_decode, msa_prefill),
|
||||
(triton_impl, tri_decode, tri_prefill),
|
||||
):
|
||||
buf = impl.topk_indices_buffer
|
||||
assert dec.data_ptr() == buf[:, :nd, :].data_ptr()
|
||||
assert pre.data_ptr() == buf[:, nd:, :].data_ptr()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("decode_query_len", "max_decode_query_len"),
|
||||
[
|
||||
@@ -317,6 +582,65 @@ def test_decode_index_topk_correctness(
|
||||
_assert_topk_indices_equal_unordered(actual, expected)
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not current_platform.is_device_capability_family(100),
|
||||
reason="fp8 e4m3 indexer cache is the SM100 (MSA) path.",
|
||||
)
|
||||
@pytest.mark.parametrize("num_idx_heads", [1, 4])
|
||||
def test_decode_index_topk_fp8(num_idx_heads: int):
|
||||
"""The fp8 (e4m3) indexer cache feeds the Triton decode kernel on the MSA
|
||||
path. The kernel must score in fp32 (no scaling) so its top-k matches a
|
||||
reference computed from the dequantized fp8 values."""
|
||||
torch.manual_seed(0)
|
||||
topk, init_blocks, local_blocks, head_dim = 8, 0, 1, 128
|
||||
decode_query_len = 1
|
||||
active_seq_lens = torch.tensor((129, 1025, 4097), device="cuda", dtype=torch.int32)
|
||||
q_lens = torch.full_like(active_seq_lens, decode_query_len)
|
||||
prefix_lens = active_seq_lens - decode_query_len
|
||||
batch = active_seq_lens.numel()
|
||||
max_seq_len = int(active_seq_lens.max())
|
||||
max_blocks = (max_seq_len + BLOCK_SIZE - 1) // BLOCK_SIZE
|
||||
num_pages = batch * max_blocks
|
||||
block_table = torch.randperm(num_pages, device="cuda", dtype=torch.int32).reshape(
|
||||
batch, max_blocks
|
||||
)
|
||||
idx_q = torch.randn(
|
||||
batch * decode_query_len, num_idx_heads, head_dim, device="cuda"
|
||||
).to(torch.float8_e4m3fn)
|
||||
index_kv_cache = torch.randn(num_pages, BLOCK_SIZE, head_dim, device="cuda").to(
|
||||
torch.float8_e4m3fn
|
||||
)
|
||||
|
||||
actual = minimax_m3_index_decode(
|
||||
idx_q,
|
||||
index_kv_cache,
|
||||
block_table,
|
||||
active_seq_lens,
|
||||
max_seq_len=max_seq_len,
|
||||
topk=topk,
|
||||
init_blocks=init_blocks,
|
||||
local_blocks=local_blocks,
|
||||
num_kv_heads=num_idx_heads,
|
||||
sm_scale=head_dim**-0.5,
|
||||
decode_query_len=decode_query_len,
|
||||
)
|
||||
# Reference from the DEQUANTIZED fp8 values (the kernel computes the fp8 QK
|
||||
# in fp32, so it must match an fp32 matmul of the same e4m3 values).
|
||||
expected = _reference_index_topk(
|
||||
idx_q.float(),
|
||||
index_kv_cache.float(),
|
||||
block_table,
|
||||
q_lens,
|
||||
active_seq_lens,
|
||||
prefix_lens,
|
||||
topk,
|
||||
init_blocks,
|
||||
local_blocks,
|
||||
head_dim**-0.5,
|
||||
)
|
||||
_assert_topk_indices_equal_unordered(actual, expected)
|
||||
|
||||
|
||||
# Sparse attention kernels.
|
||||
def _reference_sparse_attn(
|
||||
q: torch.Tensor,
|
||||
|
||||
@@ -5,8 +5,9 @@ Tests for the FlashInfer TRTLLM NvFP4 MoE backend
|
||||
(`TrtLlmNvFp4ExpertsModular`).
|
||||
|
||||
Covers the activations the wrapper claims to support — SiLU, RELU^2 (non-gated),
|
||||
and GELU — including a Gemma4-shaped case (128 experts, top-k 8,
|
||||
intermediate_size 704) that exercises the non-256-aligned padding path.
|
||||
GELU, and clamped SwiGLU-OAI (MiniMax-M3) — including a Gemma4-shaped case
|
||||
(128 experts, top-k 8, intermediate_size 704) that exercises the non-256-aligned
|
||||
padding path.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
@@ -80,6 +81,29 @@ if _CLAMP_OP_NAME not in op_registry:
|
||||
|
||||
SILU_WITH_CLAMP = op_registry[_CLAMP_OP_NAME]
|
||||
|
||||
# Clamped SwiGLU-OAI (MiniMax-M3): non-default alpha/beta so the kernel must
|
||||
# honor gemm1_alpha (raw) and gemm1_beta (folded by g1_alphas), not just clamp.
|
||||
_SWIGLU_ALPHA = 1.702
|
||||
_SWIGLU_BETA = 1.0
|
||||
_OAI_OP_NAME = "test_swigluoai_with_clamp"
|
||||
|
||||
if _OAI_OP_NAME not in op_registry:
|
||||
|
||||
@CustomOp.register(_OAI_OP_NAME)
|
||||
class _SwigluOAIWithClampTest(SiluAndMulWithClamp):
|
||||
custom_op_name = _OAI_OP_NAME
|
||||
|
||||
def __init__(self, *, compile_native: bool = True) -> None:
|
||||
super().__init__(
|
||||
_SWIGLU_LIMIT,
|
||||
alpha=_SWIGLU_ALPHA,
|
||||
beta=_SWIGLU_BETA,
|
||||
compile_native=compile_native,
|
||||
)
|
||||
|
||||
|
||||
SWIGLUOAI_REF = op_registry[_OAI_OP_NAME]
|
||||
|
||||
|
||||
ACTIVATION_CASES = [
|
||||
pytest.param(MoEActivation.SILU, MoEActivation.SILU, None, id="silu"),
|
||||
@@ -91,6 +115,12 @@ ACTIVATION_CASES = [
|
||||
id="relu2_no_mul",
|
||||
),
|
||||
pytest.param(MoEActivation.GELU, MoEActivation.GELU, None, id="gelu"),
|
||||
pytest.param(
|
||||
MoEActivation.SWIGLUOAI_UNINTERLEAVE,
|
||||
SWIGLUOAI_REF,
|
||||
_SWIGLU_LIMIT,
|
||||
id="swigluoai_uninterleave",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
@@ -148,6 +178,10 @@ def test_trtllm_fp4_moe_no_graph(
|
||||
is_scale_swizzled=False,
|
||||
)
|
||||
quant_config.gemm1_clamp_limit = swiglu_limit
|
||||
is_oai = activation == MoEActivation.SWIGLUOAI_UNINTERLEAVE
|
||||
if is_oai:
|
||||
quant_config.gemm1_alpha = _SWIGLU_ALPHA
|
||||
quant_config.gemm1_beta = _SWIGLU_BETA
|
||||
if swiglu_limit is not None:
|
||||
assert quant_config.g1_alphas is not None
|
||||
assert quant_config.a2_gscale is not None
|
||||
@@ -192,6 +226,27 @@ def test_trtllm_fp4_moe_no_graph(
|
||||
fake_layer.w2_input_scale = torch.ones_like(quant_config.g2_alphas)
|
||||
trtllm_inner.process_weights_after_loading(fake_layer)
|
||||
|
||||
if is_oai:
|
||||
# alpha stays raw; beta and clamp are folded by g1_alphas
|
||||
# (== _LARGE_OUTPUT1_SCALE here), so the fold is load-bearing.
|
||||
assert torch.allclose(
|
||||
trtllm_inner.gemm1_alpha,
|
||||
torch.full_like(trtllm_inner.gemm1_alpha, _SWIGLU_ALPHA),
|
||||
)
|
||||
assert torch.allclose(
|
||||
trtllm_inner.gemm1_beta,
|
||||
torch.full_like(
|
||||
trtllm_inner.gemm1_beta, _SWIGLU_BETA / _LARGE_OUTPUT1_SCALE
|
||||
),
|
||||
)
|
||||
assert torch.allclose(
|
||||
trtllm_inner.gemm1_clamp_limit,
|
||||
torch.full_like(
|
||||
trtllm_inner.gemm1_clamp_limit,
|
||||
_SWIGLU_LIMIT / _LARGE_OUTPUT1_SCALE,
|
||||
),
|
||||
)
|
||||
|
||||
trtllm_experts = mk.FusedMoEKernel(
|
||||
maybe_make_prepare_finalize(
|
||||
moe=moe_config,
|
||||
|
||||
@@ -345,6 +345,63 @@ def test_per_token_group_quant_fp8_packed_zero_fills_padded_output_q(
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not current_platform.is_cuda_alike(),
|
||||
reason="packed FP8 per-token-group quant kernel requires a CUDA-alike GPU",
|
||||
)
|
||||
def test_per_token_group_quant_fp8_packed_large_mn():
|
||||
"""Regression test for https://github.com/vllm-project/vllm/issues/45099.
|
||||
|
||||
Some background: gridDim.x and gridDim.y have different limits of 2^31 - 1 and
|
||||
2^16 - 1, respectively.
|
||||
Prior code introduced a bug where it incorrectly assumed grid.x and y both have
|
||||
2^31 - 1 limits and mixed them up, which doesn't surface until the kernel is
|
||||
launched with a large mn that exceeds grid.y limit (2^16 - 1).
|
||||
|
||||
This issue doesn't surface often because each forward pass only processes a
|
||||
bounded token batch, not the full context.
|
||||
Quantizing tensors with more rows than that will fail at launch with
|
||||
"CUDA error: invalid argument".
|
||||
This is a differential test that compares fp8 output against Triton output
|
||||
reference when token size sits just above the gridDim.y 2^16 - 1 limit.
|
||||
"""
|
||||
|
||||
device = "cuda"
|
||||
group_size = 128
|
||||
# hidden 2048 -> 2048/128 = 16 groups per row -> kx=16, ry=1: one grid row per mn
|
||||
# row, so any mn > 65535 overflowed grid.y before the fix.
|
||||
num_tokens, hidden_dim = 65537, 2048
|
||||
torch.manual_seed(42)
|
||||
x = torch.randn((num_tokens, hidden_dim), device=device, dtype=torch.bfloat16) * 8
|
||||
|
||||
out_q, out_s_packed = fp8_utils.per_token_group_quant_fp8_packed_for_deepgemm(
|
||||
x,
|
||||
group_size=group_size,
|
||||
use_ue8m0=True,
|
||||
)
|
||||
|
||||
with patch("vllm.platforms.current_platform.is_cuda_alike", return_value=False):
|
||||
ref_q, ref_s = fp8_utils.per_token_group_quant_fp8(
|
||||
x, group_size, use_ue8m0=True
|
||||
)
|
||||
|
||||
assert torch.equal(out_q, ref_q), "Quantized output mismatch"
|
||||
|
||||
# Vectorized packed-scale check; the per-element loop used by the smaller
|
||||
# tests is too slow at this size. groups_per_row is a multiple of 4 here,
|
||||
# so there is no K padding and the packed view lines up.
|
||||
mn = num_tokens
|
||||
groups_per_row = hidden_dim // group_size
|
||||
k_num_packed = (groups_per_row + 3) // 4
|
||||
assert groups_per_row % 4 == 0
|
||||
ref_exponents = (ref_s.reshape(mn, groups_per_row).view(torch.int32) >> 23) & 0xFF
|
||||
exp = ref_exponents.view(mn, k_num_packed, 4)
|
||||
expected = (
|
||||
exp[..., 0] | (exp[..., 1] << 8) | (exp[..., 2] << 16) | (exp[..., 3] << 24)
|
||||
)
|
||||
assert torch.equal(out_s_packed.cpu(), expected.cpu()), "Packed scale mismatch"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("shape", [(32, 128), (64, 256), (16, 512)])
|
||||
@pytest.mark.parametrize("group_size", [64, 128])
|
||||
@pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA not available")
|
||||
|
||||
@@ -60,8 +60,10 @@ def test_rocm_compressed_tensors_w8a8(
|
||||
vllm_runner, example_prompts, model_path, max_tokens, num_logprobs
|
||||
):
|
||||
dtype = "bfloat16"
|
||||
|
||||
with vllm_runner(model_path, dtype=dtype) as vllm_model:
|
||||
# Pin to TRITON_ATTN, see https://github.com/vllm-project/vllm/issues/46179
|
||||
with vllm_runner(
|
||||
model_path, dtype=dtype, attention_backend="TRITON_ATTN"
|
||||
) as vllm_model:
|
||||
vllm_model.generate_greedy_logprobs(example_prompts, max_tokens, num_logprobs)
|
||||
|
||||
|
||||
|
||||
@@ -278,3 +278,99 @@ def test_sparse_full(num_tokens, block_size, kv_cache_dtype):
|
||||
torch.testing.assert_close(
|
||||
index_cache.view(-1, HEAD_DIM), expected_index_cache, rtol=0, atol=0
|
||||
)
|
||||
|
||||
|
||||
# ── Test 3: fp8 (e4m3) index outputs ─────────────────────────────────────────
|
||||
# The fp8 score path stores index_q and the index-K cache as e4m3 while q/k/v +
|
||||
# q_out stay bf16. Asserts: (1) q/k/v/q_out are bit-identical to the bf16 run
|
||||
# (the index dtype must not perturb the main branch), and (2) the e4m3 index
|
||||
# outputs dequantize close to the bf16 reference.
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not torch.cuda.is_available() or torch.cuda.get_device_capability() < (8, 9),
|
||||
reason="e4m3 conversion requires CUDA SM89+.",
|
||||
)
|
||||
@pytest.mark.parametrize("num_tokens", [1, 7, 64, 513])
|
||||
@pytest.mark.parametrize("block_size", [16, 64])
|
||||
def test_sparse_full_fp8_index(num_tokens, block_size):
|
||||
torch.manual_seed(1)
|
||||
device, dtype, eps = "cuda", torch.bfloat16, 1e-6
|
||||
base, max_pos = 5_000_000.0, 4096
|
||||
num_heads, num_kv_heads, num_idx_heads = 16, 4, 4
|
||||
|
||||
q_w = torch.randn(HEAD_DIM, dtype=dtype, device=device) * 0.1
|
||||
k_w = torch.randn(HEAD_DIM, dtype=dtype, device=device) * 0.1
|
||||
iq_w = torch.randn(HEAD_DIM, dtype=dtype, device=device) * 0.1
|
||||
ik_w = torch.randn(HEAD_DIM, dtype=dtype, device=device) * 0.1
|
||||
cos_sin = make_cos_sin_cache(max_pos, ROTARY_DIM, base, dtype, device)
|
||||
positions = torch.randint(
|
||||
0, max_pos, (num_tokens,), dtype=torch.int64, device=device
|
||||
)
|
||||
|
||||
qsz, kvsz = num_heads * HEAD_DIM, num_kv_heads * HEAD_DIM
|
||||
iqsz, iksz = num_idx_heads * HEAD_DIM, HEAD_DIM
|
||||
qkv0 = torch.randn(
|
||||
num_tokens, qsz + 2 * kvsz + iqsz + iksz, dtype=dtype, device=device
|
||||
)
|
||||
|
||||
num_blocks = (num_tokens + block_size - 1) // block_size + 1
|
||||
slot_mapping = torch.randperm(
|
||||
num_blocks * block_size, dtype=torch.int64, device=device
|
||||
)[:num_tokens]
|
||||
index_slot_mapping = torch.roll(slot_mapping, shifts=1)
|
||||
|
||||
def run(index_dtype):
|
||||
qkv = qkv0.clone()
|
||||
kv_cache = torch.zeros(
|
||||
num_blocks,
|
||||
2,
|
||||
block_size,
|
||||
num_kv_heads,
|
||||
HEAD_DIM,
|
||||
dtype=dtype,
|
||||
device=device,
|
||||
)
|
||||
index_cache = torch.zeros(
|
||||
num_blocks, block_size, HEAD_DIM, dtype=index_dtype, device=device
|
||||
)
|
||||
q_out = torch.empty(num_tokens, qsz, dtype=dtype, device=device)
|
||||
index_q = torch.empty(num_tokens, iqsz, dtype=index_dtype, device=device)
|
||||
ops.fused_minimax_m3_qknorm_rope_kv_insert(
|
||||
qkv,
|
||||
q_w,
|
||||
k_w,
|
||||
cos_sin,
|
||||
positions,
|
||||
num_heads,
|
||||
num_kv_heads,
|
||||
ROTARY_DIM,
|
||||
eps,
|
||||
iq_w,
|
||||
ik_w,
|
||||
num_idx_heads,
|
||||
slot_mapping,
|
||||
index_slot_mapping,
|
||||
kv_cache,
|
||||
index_cache,
|
||||
block_size,
|
||||
q_out,
|
||||
index_q,
|
||||
)
|
||||
return qkv, kv_cache, index_cache, q_out, index_q
|
||||
|
||||
qkv_bf, kvc_bf, idxc_bf, qo_bf, iq_bf = run(torch.bfloat16)
|
||||
qkv_fp, kvc_fp, idxc_fp, qo_fp, iq_fp = run(torch.float8_e4m3fn)
|
||||
|
||||
assert iq_fp.dtype == torch.float8_e4m3fn
|
||||
assert idxc_fp.dtype == torch.float8_e4m3fn
|
||||
|
||||
# (1) The main branch (q/k/v in qkv, q_out, kv cache) must be bit-identical:
|
||||
# the index output dtype must not perturb anything else.
|
||||
torch.testing.assert_close(qo_fp, qo_bf, rtol=0, atol=0)
|
||||
torch.testing.assert_close(qkv_fp, qkv_bf, rtol=0, atol=0)
|
||||
torch.testing.assert_close(kvc_fp, kvc_bf, rtol=0, atol=0)
|
||||
|
||||
# (2) Dequantized e4m3 index outputs match the bf16 reference within fp8 ulp.
|
||||
torch.testing.assert_close(iq_fp.float(), iq_bf.float(), rtol=0.13, atol=0.05)
|
||||
torch.testing.assert_close(idxc_fp.float(), idxc_bf.float(), rtol=0.13, atol=0.05)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
import transformers.utils
|
||||
from PIL import Image
|
||||
@@ -52,6 +53,7 @@ def _get_cherry_blossom_image() -> Image.Image:
|
||||
)
|
||||
|
||||
|
||||
@torch.inference_mode()
|
||||
def _run_test(
|
||||
hf_runner: type[HfRunner],
|
||||
vllm_runner: type[VllmRunner],
|
||||
|
||||
@@ -92,3 +92,49 @@ def test_processor_num_frames_timestamp(
|
||||
assert len(video_phs) == 1, (
|
||||
f"Expected exactly 1 video placeholder, got {len(video_phs)}"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("model_id", [MODEL_ID])
|
||||
@pytest.mark.parametrize("num_videos", [2, 4])
|
||||
def test_processor_multi_video(
|
||||
model_id: str,
|
||||
num_videos: int,
|
||||
) -> None:
|
||||
"""Verify that multi-video processing produces correct placeholders.
|
||||
|
||||
This exercises the token-level replacement path in
|
||||
``_call_hf_processor`` which avoids the quadratic text-level
|
||||
prompt expansion.
|
||||
"""
|
||||
ctx = build_model_context(
|
||||
model_id,
|
||||
limit_mm_per_prompt={"image": 0, "video": num_videos},
|
||||
)
|
||||
processor = MULTIMODAL_REGISTRY.create_processor(ctx.model_config)
|
||||
|
||||
prompt = "<|vision_start|><|video_pad|><|vision_end|>" * num_videos
|
||||
mm_data = {"video": [_build_video_mm_data(num_frames=8)["video"][0]] * num_videos}
|
||||
|
||||
processed = processor(
|
||||
prompt,
|
||||
mm_items=processor.info.parse_mm_data(mm_data),
|
||||
hf_processor_mm_kwargs={"num_frames": 8},
|
||||
)
|
||||
|
||||
token_ids = processed["prompt_token_ids"]
|
||||
assert len(token_ids) > 0
|
||||
|
||||
video_phs = processed["mm_placeholders"].get("video", [])
|
||||
assert len(video_phs) == num_videos, (
|
||||
f"Expected {num_videos} video placeholders, got {len(video_phs)}"
|
||||
)
|
||||
|
||||
# All placeholders should have the same length (same video params)
|
||||
# and must not overlap.
|
||||
lengths = {ph.length for ph in video_phs}
|
||||
assert len(lengths) == 1, f"Placeholder lengths differ: {lengths}"
|
||||
for i in range(1, len(video_phs)):
|
||||
prev_end = video_phs[i - 1].offset + video_phs[i - 1].length
|
||||
assert video_phs[i].offset >= prev_end, (
|
||||
f"Placeholder {i} overlaps with placeholder {i - 1}"
|
||||
)
|
||||
|
||||
@@ -96,6 +96,9 @@ class MockTokenizer:
|
||||
return "".join(parts)
|
||||
|
||||
|
||||
CHUNK_SIZES = [1, 2, 3, 5, 11, 23, None]
|
||||
|
||||
|
||||
def make_mock_tokenizer(sample: Sample) -> MockTokenizer:
|
||||
"""Build a mock tokenizer from a sample's vocab and token data."""
|
||||
return MockTokenizer(
|
||||
|
||||
@@ -19,6 +19,7 @@ import pytest
|
||||
from pydantic import TypeAdapter
|
||||
|
||||
from tests.parser.engine.replay_harness import (
|
||||
CHUNK_SIZES,
|
||||
MockTokenizer,
|
||||
assert_parse_output,
|
||||
collect_output,
|
||||
@@ -113,8 +114,6 @@ _PAIRINGS = _discover_pairings()
|
||||
|
||||
_ALL_SAMPLES = [(p.parser_cls, s) for p in _PAIRINGS for s in p.samples]
|
||||
|
||||
CHUNK_SIZES = [1, 2, 3, 5, 11, 23, None]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("chunk_size", CHUNK_SIZES, ids=lambda c: f"chunk={c}")
|
||||
@pytest.mark.parametrize(
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""Regression test for U+FFFD leak at reasoning→content transition.
|
||||
|
||||
When byte-fallback tokens span the reasoning/content boundary,
|
||||
decoding isolated content-side token IDs via tokenizer.decode()
|
||||
produces U+FFFD (Unicode replacement character). The fix flushes
|
||||
the reasoning parser's engine lexer instead.
|
||||
|
||||
Reproduces the bug at various chunk sizes and validates that the
|
||||
fix prevents U+FFFD from leaking into streamed content.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from tests.parser.engine.replay_harness import (
|
||||
CHUNK_SIZES,
|
||||
MockTokenizer,
|
||||
collect_output,
|
||||
replay_streaming,
|
||||
)
|
||||
from vllm.parser.abstract_parser import DelegatingParser
|
||||
from vllm.parser.engine.registered_adapters import (
|
||||
Glm47MoeParserReasoningAdapter,
|
||||
Glm47MoeParserToolAdapter,
|
||||
Qwen3ParserReasoningAdapter,
|
||||
Qwen3ParserToolAdapter,
|
||||
)
|
||||
|
||||
|
||||
class ByteFallbackMockTokenizer(MockTokenizer):
|
||||
"""MockTokenizer that returns U+FFFD for specified token IDs.
|
||||
|
||||
Simulates byte-fallback tokenizer behavior where isolated
|
||||
partial-byte tokens decode to the Unicode replacement character.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
vocab: dict[str, int],
|
||||
tokens: list[tuple[int, str]],
|
||||
ufffd_token_ids: set[int],
|
||||
) -> None:
|
||||
super().__init__(vocab, tokens)
|
||||
self._ufffd_token_ids = frozenset(ufffd_token_ids)
|
||||
|
||||
def decode(self, ids: list[int], skip_special_tokens: bool = False) -> str:
|
||||
parts: list[str] = []
|
||||
for tid in ids:
|
||||
if skip_special_tokens and tid in self._special_ids:
|
||||
continue
|
||||
if tid in self._ufffd_token_ids:
|
||||
parts.append("�")
|
||||
else:
|
||||
text = self._token_decode_map.get(tid, f"?{tid}?")
|
||||
parts.append(text)
|
||||
return "".join(parts)
|
||||
|
||||
|
||||
# ── Model-specific DelegatingParser subclasses ───────────────────────
|
||||
|
||||
|
||||
class _Glm47Delegating(DelegatingParser):
|
||||
reasoning_parser_cls = Glm47MoeParserReasoningAdapter
|
||||
tool_parser_cls = Glm47MoeParserToolAdapter
|
||||
|
||||
|
||||
class _Qwen3Delegating(DelegatingParser):
|
||||
reasoning_parser_cls = Qwen3ParserReasoningAdapter
|
||||
tool_parser_cls = Qwen3ParserToolAdapter
|
||||
|
||||
|
||||
# ── Shared test data ─────────────────────────────────────────────────
|
||||
|
||||
_SHARED_TOKENS: list[tuple[int, str]] = [
|
||||
(100, "Let me"),
|
||||
(101, " think"),
|
||||
(102, " about"),
|
||||
(103, " Samsung."),
|
||||
(51, "</think>"),
|
||||
(200, "삼성"),
|
||||
(201, "전자의"),
|
||||
(202, " 주가를"),
|
||||
(203, " 분석합니다."),
|
||||
]
|
||||
|
||||
_SHARED_UFFFD_IDS: set[int] = {200}
|
||||
|
||||
EXPECTED_REASONING = "Let me think about Samsung."
|
||||
EXPECTED_CONTENT = "삼성전자의 주가를 분석합니다."
|
||||
|
||||
_MODEL_CONFIGS = [
|
||||
pytest.param(
|
||||
{
|
||||
"<think>": 50,
|
||||
"</think>": 51,
|
||||
"<tool_call>": 60,
|
||||
"</tool_call>": 61,
|
||||
"<arg_key>": 62,
|
||||
"</arg_key>": 63,
|
||||
"<arg_value>": 64,
|
||||
"</arg_value>": 65,
|
||||
},
|
||||
_Glm47Delegating,
|
||||
id="glm47",
|
||||
),
|
||||
pytest.param(
|
||||
{
|
||||
"<think>": 50,
|
||||
"</think>": 51,
|
||||
"<tool_call>": 60,
|
||||
"</tool_call>": 61,
|
||||
},
|
||||
_Qwen3Delegating,
|
||||
id="qwen3",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
# ── Tests ────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestUfffdReasoningTransition:
|
||||
"""U+FFFD must not appear at the reasoning→content transition."""
|
||||
|
||||
@pytest.mark.parametrize("vocab,delegating_cls", _MODEL_CONFIGS)
|
||||
@pytest.mark.parametrize("chunk_size", CHUNK_SIZES, ids=lambda c: f"chunk={c}")
|
||||
def test_no_ufffd(self, chunk_size, vocab, delegating_cls):
|
||||
tokenizer = ByteFallbackMockTokenizer(vocab, _SHARED_TOKENS, _SHARED_UFFFD_IDS)
|
||||
parser = delegating_cls(tokenizer)
|
||||
deltas = replay_streaming(
|
||||
parser,
|
||||
_SHARED_TOKENS,
|
||||
chunk_size=chunk_size,
|
||||
finished_on_last=True,
|
||||
)
|
||||
output = collect_output(deltas)
|
||||
|
||||
assert "�" not in output.content, (
|
||||
f"U+FFFD leaked into content: {output.content!r}"
|
||||
)
|
||||
assert output.content == EXPECTED_CONTENT
|
||||
assert output.reasoning == EXPECTED_REASONING
|
||||
|
||||
def test_byte_fallback_tokenizer_produces_ufffd(self):
|
||||
"""Validate the fixture: decode() returns U+FFFD for isolated
|
||||
byte-fallback token IDs, proving the old code path would leak."""
|
||||
vocab = dict(_MODEL_CONFIGS[0].values[0])
|
||||
tokenizer = ByteFallbackMockTokenizer(vocab, _SHARED_TOKENS, _SHARED_UFFFD_IDS)
|
||||
assert tokenizer.decode([200]) == "�"
|
||||
|
||||
@pytest.mark.parametrize("chunk_size", CHUNK_SIZES, ids=lambda c: f"chunk={c}")
|
||||
def test_multiple_ufffd_tokens_at_boundary(self, chunk_size):
|
||||
"""Multiple consecutive byte-fallback tokens at the boundary."""
|
||||
tokens: list[tuple[int, str]] = [
|
||||
(100, "Reasoning."),
|
||||
(51, "</think>"),
|
||||
(200, "삼"),
|
||||
(201, "성"),
|
||||
(202, "전자"),
|
||||
]
|
||||
ufffd_ids: set[int] = {200, 201}
|
||||
vocab = dict(_MODEL_CONFIGS[0].values[0])
|
||||
|
||||
tokenizer = ByteFallbackMockTokenizer(vocab, tokens, ufffd_ids)
|
||||
parser = _Glm47Delegating(tokenizer)
|
||||
deltas = replay_streaming(
|
||||
parser,
|
||||
tokens,
|
||||
chunk_size=chunk_size,
|
||||
finished_on_last=True,
|
||||
)
|
||||
output = collect_output(deltas)
|
||||
|
||||
assert "�" not in output.content, (
|
||||
f"U+FFFD leaked into content: {output.content!r}"
|
||||
)
|
||||
assert output.content == "삼성전자"
|
||||
assert output.reasoning == "Reasoning."
|
||||
@@ -18,6 +18,7 @@ from vllm.model_executor.kernels.linear.scaled_mm.ScaledMMLinearKernel import (
|
||||
FP8ScaledMMLinearLayerConfig,
|
||||
)
|
||||
from vllm.model_executor.layers.quantization.utils.quant_utils import (
|
||||
get_fp8_min_max,
|
||||
kFp8DynamicTokenSym,
|
||||
kFp8StaticChannelSym,
|
||||
kFp8StaticTensorSym,
|
||||
@@ -309,7 +310,7 @@ def test_hipb_mm_kernel_forward_accuracy(enable_hipb_mm_kernel):
|
||||
_check_bpreshuffle_runtime_support(weight_shape, num_tokens=num_tokens)
|
||||
|
||||
fp8_dtype = current_platform.fp8_dtype()
|
||||
fp8_max = torch.finfo(fp8_dtype).max
|
||||
fp8_max = get_fp8_min_max()[1]
|
||||
device = torch.device("cuda")
|
||||
|
||||
# Build a bf16 weight and quantize per output channel (one scale per row).
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""Tests for contiguous KV cache packing in _get_kv_cache_config_deepseek_v4."""
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from vllm.v1.core.kv_cache_utils import _get_kv_cache_config_deepseek_v4
|
||||
from vllm.v1.kv_cache_interface import (
|
||||
KVCacheGroupSpec,
|
||||
MLAAttentionSpec,
|
||||
UniformTypeKVCacheSpecs,
|
||||
)
|
||||
|
||||
|
||||
def _make_mla_spec(page_size: int, block_size: int = 256) -> MLAAttentionSpec:
|
||||
return MLAAttentionSpec(
|
||||
block_size=block_size,
|
||||
num_kv_heads=1,
|
||||
head_size=512,
|
||||
dtype=torch.uint8,
|
||||
page_size_padded=page_size,
|
||||
cache_dtype_str="fp8_ds_mla",
|
||||
model_version="deepseek_v4",
|
||||
alignment=576,
|
||||
)
|
||||
|
||||
|
||||
def _make_groups(n_c4, n_c128, n_swa):
|
||||
PS_C4_MLA = 37440
|
||||
PS_C4_IDX = 8640
|
||||
PS_C128 = 1728
|
||||
PS_SWA = 37440
|
||||
|
||||
mla_specs = {}
|
||||
for i in range(n_c4):
|
||||
mla_specs[f"c4_mla.{i}"] = _make_mla_spec(PS_C4_MLA)
|
||||
mla_specs[f"c4_idx.{i}"] = _make_mla_spec(PS_C4_IDX)
|
||||
for i in range(n_c128):
|
||||
mla_specs[f"c128_mla.{i}"] = _make_mla_spec(PS_C128)
|
||||
|
||||
mla_group = KVCacheGroupSpec(
|
||||
layer_names=list(mla_specs.keys()),
|
||||
kv_cache_spec=UniformTypeKVCacheSpecs(block_size=256, kv_cache_specs=mla_specs),
|
||||
)
|
||||
|
||||
swa_specs = {}
|
||||
for i in range(n_swa):
|
||||
swa_specs[f"swa.{i}"] = _make_mla_spec(PS_SWA)
|
||||
|
||||
swa_group = KVCacheGroupSpec(
|
||||
layer_names=list(swa_specs.keys()),
|
||||
kv_cache_spec=UniformTypeKVCacheSpecs(block_size=256, kv_cache_specs=swa_specs),
|
||||
)
|
||||
|
||||
return [mla_group, swa_group]
|
||||
|
||||
|
||||
def _mock_vllm_config():
|
||||
config = MagicMock()
|
||||
config.cache_config.num_gpu_blocks_override = None
|
||||
return config
|
||||
|
||||
|
||||
def _run(n_c4=3, n_c128=2, n_swa=5, mem=100 * 1024 * 1024):
|
||||
groups = _make_groups(n_c4, n_c128, n_swa)
|
||||
return _get_kv_cache_config_deepseek_v4(_mock_vllm_config(), groups, mem)
|
||||
|
||||
|
||||
def _page_sizes_by_layer(
|
||||
groups: list[KVCacheGroupSpec],
|
||||
) -> dict[str, int]:
|
||||
page_sizes = {}
|
||||
for group in groups:
|
||||
specs = group.kv_cache_spec.kv_cache_specs
|
||||
for layer_name in group.layer_names:
|
||||
page_sizes[layer_name] = specs[layer_name].page_size_bytes
|
||||
return page_sizes
|
||||
|
||||
|
||||
class TestInterleavedPacking:
|
||||
def test_all_tensors_have_block_stride(self):
|
||||
_, tensors = _run()
|
||||
for t in tensors:
|
||||
assert t.block_stride > 0
|
||||
|
||||
def test_all_tensors_share_same_size(self):
|
||||
_, tensors = _run()
|
||||
sizes = set(t.size for t in tensors)
|
||||
assert len(sizes) == 1
|
||||
assert sizes.pop() > 0
|
||||
|
||||
def test_offsets_within_one_block(self):
|
||||
_, tensors = _run()
|
||||
for t in tensors:
|
||||
assert t.offset < t.block_stride
|
||||
|
||||
def test_all_layers_accounted_for(self):
|
||||
n_c4, n_c128, n_swa = 5, 4, 7
|
||||
_, tensors = _run(n_c4=n_c4, n_c128=n_c128, n_swa=n_swa)
|
||||
all_names = set()
|
||||
for t in tensors:
|
||||
all_names.update(t.shared_by)
|
||||
expected = n_c4 * 2 + n_c128 + n_swa
|
||||
assert len(all_names) == expected
|
||||
|
||||
def test_strided_views_are_independent(self):
|
||||
groups = _make_groups(n_c4=3, n_c128=2, n_swa=5)
|
||||
page_sizes = _page_sizes_by_layer(groups)
|
||||
num_blocks, tensors = _get_kv_cache_config_deepseek_v4(
|
||||
_mock_vllm_config(), groups, 100 * 1024 * 1024
|
||||
)
|
||||
backing = torch.zeros(tensors[0].size, dtype=torch.uint8)
|
||||
views = []
|
||||
for t in tensors:
|
||||
page_size = page_sizes[t.shared_by[0]]
|
||||
v = torch.as_strided(
|
||||
backing,
|
||||
size=(num_blocks, page_size),
|
||||
stride=(t.block_stride, 1),
|
||||
storage_offset=t.offset,
|
||||
)
|
||||
views.append(v)
|
||||
|
||||
for i, v in enumerate(views):
|
||||
v.fill_(i + 1)
|
||||
|
||||
for i, v in enumerate(views):
|
||||
assert (v == i + 1).all(), f"View {i} was corrupted"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-v"])
|
||||
@@ -4,9 +4,16 @@
|
||||
import pytest
|
||||
|
||||
from vllm import LLM, SamplingParams
|
||||
from vllm.platforms import current_platform
|
||||
|
||||
from ....utils import create_new_process_for_each_test
|
||||
|
||||
if current_platform.is_rocm():
|
||||
pytest.skip(
|
||||
"Cascade attention backends FLASH_ATTN and FLASHINFER are notsupported on ROCm",
|
||||
allow_module_level=True,
|
||||
)
|
||||
|
||||
|
||||
@create_new_process_for_each_test()
|
||||
@pytest.mark.parametrize("attn_backend", ["FLASH_ATTN", "FLASHINFER"])
|
||||
|
||||
@@ -991,7 +991,7 @@ class VllmBackend:
|
||||
},
|
||||
payload_fn=lambda: json.dumps(
|
||||
{
|
||||
"model": self.vllm_config.model_config.model,
|
||||
"model": getattr(self.vllm_config.model_config, "model", "unknown"),
|
||||
"prefix": self.prefix,
|
||||
"mode": str(cc.mode),
|
||||
"backend": cc.backend,
|
||||
|
||||
@@ -302,6 +302,14 @@ class ParallelConfig:
|
||||
Each entry must use `numactl --physcpubind` CPU-list syntax, for example
|
||||
`"0-3"` or `"0,2,4-7"`.
|
||||
"""
|
||||
assigned_physical_gpu_ids: list[int] | None = None
|
||||
"""Mapping from vLLM-local logical GPU IDs to physical GPU IDs.
|
||||
|
||||
For example, ``[2, 3]`` means logical GPU 0 maps to physical GPU 2,
|
||||
and logical GPU 1 maps to physical GPU 3. Physical IDs are used only
|
||||
at platform/topology boundaries such as NVML, NIC affinity, P2P
|
||||
checks, and final CUDA device selection when needed. When None,
|
||||
logical IDs map to visible device IDs in order."""
|
||||
|
||||
distributed_timeout_seconds: int | None = None
|
||||
"""Timeout in seconds for distributed operations (e.g., init_process_group).
|
||||
@@ -772,6 +780,7 @@ class ParallelConfig:
|
||||
"numa_bind",
|
||||
"numa_bind_nodes",
|
||||
"numa_bind_cpus",
|
||||
"assigned_physical_gpu_ids",
|
||||
}
|
||||
|
||||
from vllm.config.utils import get_hash_factors, hash_factors
|
||||
|
||||
@@ -704,7 +704,14 @@ class FlashInferNVLinkOneSidedManager(All2AllManagerBase):
|
||||
self.num_experts = num_experts
|
||||
|
||||
self.cleanup()
|
||||
gpus_per_node = torch.accelerator.device_count()
|
||||
from vllm.platforms.interface import get_assigned_physical_gpu_ids
|
||||
|
||||
assigned_physical_gpu_ids = get_assigned_physical_gpu_ids()
|
||||
gpus_per_node = (
|
||||
len(assigned_physical_gpu_ids)
|
||||
if assigned_physical_gpu_ids is not None
|
||||
else torch.accelerator.device_count()
|
||||
)
|
||||
logger.debug(
|
||||
"Making One-sided NVLink mapping: rank=%d, world size=%d",
|
||||
self.rank,
|
||||
|
||||
@@ -320,13 +320,21 @@ def gpu_p2p_access_check(src: int, tgt: int) -> bool:
|
||||
|
||||
is_distributed = dist.is_initialized()
|
||||
|
||||
num_dev = current_platform.device_count()
|
||||
cuda_visible_devices = envs.CUDA_VISIBLE_DEVICES
|
||||
if cuda_visible_devices is None:
|
||||
cuda_visible_devices = ",".join(str(i) for i in range(num_dev))
|
||||
from vllm.platforms.interface import get_assigned_physical_gpu_ids
|
||||
|
||||
assigned_physical_gpu_ids = get_assigned_physical_gpu_ids()
|
||||
if assigned_physical_gpu_ids is not None:
|
||||
# Key by the ordered list: the cache stores directed local-index
|
||||
# pairs, so permutations of the same set are distinct mappings.
|
||||
cache_key = ",".join(str(i) for i in assigned_physical_gpu_ids)
|
||||
num_dev = len(assigned_physical_gpu_ids)
|
||||
else:
|
||||
num_dev = current_platform.device_count()
|
||||
cuda_visible_devices = envs.CUDA_VISIBLE_DEVICES
|
||||
cache_key = cuda_visible_devices or ",".join(str(i) for i in range(num_dev))
|
||||
|
||||
path = os.path.join(
|
||||
envs.VLLM_CACHE_ROOT, f"gpu_p2p_access_cache_for_{cuda_visible_devices}.json"
|
||||
envs.VLLM_CACHE_ROOT, f"gpu_p2p_access_cache_for_{cache_key}.json"
|
||||
)
|
||||
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||
from vllm.distributed.parallel_state import get_world_group
|
||||
@@ -338,7 +346,15 @@ def gpu_p2p_access_check(src: int, tgt: int) -> bool:
|
||||
# enter this block to calculate the cache
|
||||
logger.info("generating GPU P2P access cache in %s", path)
|
||||
cache: dict[str, bool] = {}
|
||||
ids = list(range(num_dev))
|
||||
# The probe subprocesses inherit this process's device-control env
|
||||
# var, so they must be given visible ordinals, not physical IDs.
|
||||
if assigned_physical_gpu_ids is not None:
|
||||
ids = [
|
||||
current_platform.logical_device_id_to_visible_device_id(local)
|
||||
for local in range(num_dev)
|
||||
]
|
||||
else:
|
||||
ids = list(range(num_dev))
|
||||
# batch of all pairs of GPUs
|
||||
batch_src, batch_tgt = zip(*list(product(ids, ids)))
|
||||
# NOTE: we use `subprocess` rather than `multiprocessing` here
|
||||
@@ -368,8 +384,11 @@ def gpu_p2p_access_check(src: int, tgt: int) -> bool:
|
||||
) from e
|
||||
with open(output_file.name, "rb") as f:
|
||||
result = pickle.load(f)
|
||||
# Cache entries must be keyed by local indices (0..N-1) because
|
||||
# gpu_p2p_access_check() is called with local ranks.
|
||||
id_to_local = {device_id: local for local, device_id in enumerate(ids)}
|
||||
for _i, _j, r in zip(batch_src, batch_tgt, result):
|
||||
cache[f"{_i}->{_j}"] = r
|
||||
cache[f"{id_to_local[_i]}->{id_to_local[_j]}"] = r
|
||||
with open(path, "w") as f:
|
||||
json.dump(cache, f, indent=4)
|
||||
if is_distributed:
|
||||
|
||||
@@ -34,7 +34,12 @@ def _can_p2p(rank: int, world_size: int) -> bool:
|
||||
continue
|
||||
if envs.VLLM_SKIP_P2P_CHECK:
|
||||
logger.debug("Skipping P2P check and trusting the driver's P2P report.")
|
||||
return torch.cuda.can_device_access_peer(rank, i)
|
||||
# can_device_access_peer takes visible device ordinals, while
|
||||
# rank and i are logical local IDs.
|
||||
return torch.cuda.can_device_access_peer(
|
||||
current_platform.logical_device_id_to_visible_device_id(rank),
|
||||
current_platform.logical_device_id_to_visible_device_id(i),
|
||||
)
|
||||
if not gpu_p2p_access_check(rank, i):
|
||||
return False
|
||||
return True
|
||||
@@ -126,13 +131,10 @@ class CustomAllreduce:
|
||||
CUSTOM_ALL_REDUCE_MAX_SIZES[device_capability_str][world_size],
|
||||
max_size,
|
||||
)
|
||||
cuda_visible_devices = envs.CUDA_VISIBLE_DEVICES
|
||||
if cuda_visible_devices:
|
||||
device_ids = list(map(int, cuda_visible_devices.split(",")))
|
||||
else:
|
||||
device_ids = list(range(current_platform.device_count()))
|
||||
|
||||
physical_device_id = device_ids[device.index]
|
||||
# device.index is a visible ordinal, not a logical local ID.
|
||||
physical_device_id = current_platform.visible_device_id_to_physical_device_id(
|
||||
device.index
|
||||
)
|
||||
tensor = torch.tensor([physical_device_id], dtype=torch.int, device="cpu")
|
||||
gather_list = [
|
||||
torch.tensor([0], dtype=torch.int, device="cpu") for _ in range(world_size)
|
||||
|
||||
@@ -129,12 +129,10 @@ class QuickAllReduce:
|
||||
assert isinstance(device, torch.device)
|
||||
self.device = device
|
||||
|
||||
cuda_visible_devices = envs.CUDA_VISIBLE_DEVICES
|
||||
if cuda_visible_devices:
|
||||
device_ids = list(map(int, cuda_visible_devices.split(",")))
|
||||
else:
|
||||
device_ids = list(range(current_platform.device_count()))
|
||||
physical_device_id = device_ids[device.index]
|
||||
# device.index is a visible ordinal, not a logical local ID.
|
||||
physical_device_id = current_platform.visible_device_id_to_physical_device_id(
|
||||
device.index
|
||||
)
|
||||
tensor = torch.tensor([physical_device_id], dtype=torch.int, device="cpu")
|
||||
gather_list = [
|
||||
torch.tensor([0], dtype=torch.int, device="cpu")
|
||||
|
||||
@@ -840,7 +840,13 @@ class MessageQueue:
|
||||
The MessageQueue instance for the calling process,
|
||||
and a list of handles (only non-empty for the reader process).
|
||||
"""
|
||||
local_size = current_platform.device_count()
|
||||
from vllm.platforms.interface import get_assigned_physical_gpu_ids
|
||||
|
||||
assigned_physical_gpu_ids = get_assigned_physical_gpu_ids()
|
||||
if assigned_physical_gpu_ids is not None:
|
||||
local_size = len(assigned_physical_gpu_ids)
|
||||
else:
|
||||
local_size = current_platform.device_count()
|
||||
rank = dist.get_rank()
|
||||
same_node = rank // local_size == reader_rank // local_size
|
||||
buffer_io = MessageQueue(
|
||||
|
||||
@@ -482,10 +482,11 @@ def _init_lmcache_engine(
|
||||
)
|
||||
|
||||
# Change current device.
|
||||
num_gpus = torch.accelerator.device_count()
|
||||
local_rank = parallel_config.rank % num_gpus
|
||||
torch.accelerator.set_device_index(local_rank)
|
||||
device = torch.device(f"cuda:{local_rank}")
|
||||
from vllm.distributed.parallel_state import get_world_group
|
||||
|
||||
device_index = get_world_group().device_index
|
||||
torch.accelerator.set_device_index(device_index)
|
||||
device = torch.device(f"cuda:{device_index}")
|
||||
metadata = LMCacheEngineMetadata(
|
||||
model_config.model,
|
||||
parallel_config.world_size,
|
||||
|
||||
@@ -841,8 +841,106 @@ class NixlBaseConnectorWorker:
|
||||
# Forwarding a real layer name rather than a synthetic key
|
||||
self.register_kv_caches({first_layer: kv_cache})
|
||||
|
||||
def _register_packed_kv_cache(
|
||||
self,
|
||||
storage: torch.UntypedStorage,
|
||||
) -> None:
|
||||
"""Register a packed KV cache as a single NIXL region.
|
||||
|
||||
The packed allocation interleaves all layers per block, so each
|
||||
block_stride-byte chunk is one logical block. We register 1
|
||||
NIXL region and create 1 descriptor per block.
|
||||
"""
|
||||
self.transfer_topo = TransferTopology(
|
||||
tp_rank=self.tp_rank,
|
||||
tp_size=self.world_size,
|
||||
block_size=self.block_size,
|
||||
engine_id=self.engine_id,
|
||||
is_mla=self.use_mla,
|
||||
total_num_kv_heads=self.model_config.get_total_num_kv_heads(),
|
||||
attn_backends=self.attn_backends,
|
||||
tensor_shape=None,
|
||||
is_mamba=self._has_mamba,
|
||||
)
|
||||
self.compat_hash = compute_nixl_compatibility_hash(
|
||||
self.vllm_config,
|
||||
self.backend_name,
|
||||
self.transfer_topo.cross_layers_blocks,
|
||||
)
|
||||
|
||||
total_size = storage.nbytes()
|
||||
block_stride = total_size // self.num_blocks
|
||||
base_addr = storage.data_ptr()
|
||||
device_id = storage.device.index
|
||||
assert device_id is not None
|
||||
|
||||
logger.info(
|
||||
"Registering packed KV cache: total_size=%s, block_stride=%s, "
|
||||
"num_blocks=%s, num_regions=1",
|
||||
total_size,
|
||||
block_stride,
|
||||
self.num_blocks,
|
||||
)
|
||||
|
||||
self.device_id = device_id
|
||||
caches_data = [(base_addr, total_size, self.device_id, "")]
|
||||
|
||||
self.block_len_per_layer = [block_stride]
|
||||
self.num_regions = 1
|
||||
self.num_descs = self.num_blocks
|
||||
self.kv_caches_base_addr[self.engine_id][self.tp_rank] = [base_addr]
|
||||
|
||||
descs = self.nixl_wrapper.get_reg_descs(caches_data, self.nixl_memory_type)
|
||||
self.nixl_wrapper.register_memory(descs, backends=self.nixl_backends)
|
||||
self._registered_descs.append(descs)
|
||||
|
||||
self.dst_num_blocks[self.engine_id] = self.num_blocks
|
||||
|
||||
self.src_xfer_handles_by_block_size[self.block_size], (self.src_blocks_data) = (
|
||||
self.register_local_xfer_handler(self.block_size)
|
||||
)
|
||||
|
||||
agent_metadata = NixlAgentMetadata(
|
||||
engine_id=self.engine_id,
|
||||
agent_metadata=self.nixl_wrapper.get_agent_metadata(),
|
||||
device_id=self.device_id,
|
||||
kv_caches_base_addr=(
|
||||
self.kv_caches_base_addr[self.engine_id][self.tp_rank]
|
||||
),
|
||||
num_blocks=self.num_blocks,
|
||||
block_lens=self.block_len_per_layer,
|
||||
kv_cache_layout=self.kv_cache_layout,
|
||||
block_size=self.block_size,
|
||||
ssm_sizes=self._mamba_ssm_size,
|
||||
attn_backend_name=self.backend_name,
|
||||
physical_blocks_per_logical_kv_block=(
|
||||
self._physical_blocks_per_logical_kv_block
|
||||
),
|
||||
)
|
||||
assert self.compat_hash is not None
|
||||
encoder = msgspec.msgpack.Encoder()
|
||||
self.xfer_handshake_metadata = NixlHandshakePayload(
|
||||
compatibility_hash=self.compat_hash,
|
||||
agent_metadata_bytes=encoder.encode(agent_metadata),
|
||||
)
|
||||
|
||||
def register_kv_caches(self, kv_caches: dict[str, torch.Tensor]):
|
||||
"""Register the KV Cache data in nixl."""
|
||||
|
||||
# Detect packed allocation: all tensors are strided views into the
|
||||
# same backing storage (different data_ptr but same storage).
|
||||
# This happens with DSv4-style contiguous per-block packing.
|
||||
if len(kv_caches) > 1 and not self._has_mamba:
|
||||
storage = next(iter(kv_caches.values())).untyped_storage()
|
||||
storage_ptrs = {
|
||||
cache.untyped_storage().data_ptr() for cache in kv_caches.values()
|
||||
}
|
||||
data_ptrs = {cache.data_ptr() for cache in kv_caches.values()}
|
||||
if len(storage_ptrs) == 1 and len(data_ptrs) > 1:
|
||||
self._register_packed_kv_cache(storage)
|
||||
self.device_kv_caches = kv_caches
|
||||
return
|
||||
|
||||
self.transfer_topo = TransferTopology(
|
||||
tp_rank=self.tp_rank,
|
||||
tp_size=self.world_size,
|
||||
|
||||
@@ -72,18 +72,22 @@ class OffloadingConnectorWorker:
|
||||
if isinstance(layer_kv_cache_spec, AttentionSpec):
|
||||
layer_kv_cache = kv_caches[layer_name]
|
||||
assert isinstance(layer_kv_cache, torch.Tensor)
|
||||
assert layer_kv_cache.storage_offset() == 0
|
||||
|
||||
storage = layer_kv_cache.untyped_storage()
|
||||
page = layer_kv_cache_spec.page_size_bytes
|
||||
elem_size = layer_kv_cache.element_size()
|
||||
byte_offset = layer_kv_cache.storage_offset() * elem_size
|
||||
block_stride_bytes = layer_kv_cache.stride(0) * elem_size
|
||||
tensors_per_block[layer_name] = (
|
||||
torch.tensor(
|
||||
[],
|
||||
dtype=torch.int8,
|
||||
device=layer_kv_cache.device,
|
||||
)
|
||||
.set_(storage)
|
||||
.view(num_blocks, page),
|
||||
).set_(
|
||||
layer_kv_cache.untyped_storage(),
|
||||
byte_offset,
|
||||
(num_blocks, page),
|
||||
(block_stride_bytes, 1),
|
||||
),
|
||||
)
|
||||
page_size_bytes[layer_name] = layer_kv_cache_spec.page_size_bytes
|
||||
unpadded_page_size_bytes[layer_name] = (
|
||||
|
||||
@@ -392,6 +392,14 @@ class GroupCoordinator:
|
||||
|
||||
self.rank = torch.distributed.get_rank()
|
||||
self.local_rank = local_rank
|
||||
self.device_index: int
|
||||
if _WORLD is not None:
|
||||
self.device_index = _WORLD.device_index
|
||||
else:
|
||||
assert local_rank >= 0, (
|
||||
"local_rank must be provided when creating the world group"
|
||||
)
|
||||
self.device_index = local_rank
|
||||
|
||||
self_device_group = None
|
||||
self_cpu_group = None
|
||||
@@ -442,11 +450,18 @@ class GroupCoordinator:
|
||||
from vllm.platforms import current_platform
|
||||
|
||||
if current_platform.is_cuda_alike():
|
||||
self.device = torch.device(f"cuda:{local_rank}")
|
||||
visible_device_index = (
|
||||
current_platform.logical_device_id_to_visible_device_id(
|
||||
self.device_index
|
||||
)
|
||||
)
|
||||
self.device = torch.device(f"cuda:{visible_device_index}")
|
||||
elif current_platform.is_xpu():
|
||||
self.device = torch.device(f"xpu:{local_rank}")
|
||||
self.device = torch.device(f"xpu:{self.device_index}")
|
||||
elif current_platform.is_out_of_tree():
|
||||
self.device = torch.device(f"{current_platform.device_name}:{local_rank}")
|
||||
self.device = torch.device(
|
||||
f"{current_platform.device_name}:{self.device_index}"
|
||||
)
|
||||
else:
|
||||
self.device = torch.device("cpu")
|
||||
|
||||
@@ -1438,7 +1453,12 @@ def _init_process_group_for_split_group(
|
||||
"""
|
||||
if torch.accelerator.is_available() and backend != "gloo":
|
||||
init_backend = "cpu:gloo,cuda:nccl"
|
||||
device_id: torch.device | None = torch.device(f"cuda:{local_rank}")
|
||||
from vllm.platforms import current_platform
|
||||
|
||||
visible_device_index = current_platform.logical_device_id_to_visible_device_id(
|
||||
local_rank
|
||||
)
|
||||
device_id: torch.device | None = torch.device(f"cuda:{visible_device_index}")
|
||||
else:
|
||||
init_backend = "gloo"
|
||||
device_id = None
|
||||
|
||||
@@ -86,6 +86,15 @@ class StatelessGroupCoordinator(GroupCoordinator):
|
||||
|
||||
self.rank = global_rank
|
||||
self.local_rank = local_rank
|
||||
from vllm.distributed.parallel_state import _WORLD
|
||||
|
||||
if _WORLD is not None:
|
||||
self.device_index = _WORLD.device_index
|
||||
else:
|
||||
assert local_rank >= 0, (
|
||||
"local_rank must be provided when creating the world group"
|
||||
)
|
||||
self.device_index = local_rank
|
||||
|
||||
self_device_group = None
|
||||
self_cpu_group = None
|
||||
@@ -152,11 +161,18 @@ class StatelessGroupCoordinator(GroupCoordinator):
|
||||
self.tcp_store_group = self_tcp_store_group
|
||||
|
||||
if current_platform.is_cuda_alike():
|
||||
self.device = torch.device(f"cuda:{local_rank}")
|
||||
visible_device_index = (
|
||||
current_platform.logical_device_id_to_visible_device_id(
|
||||
self.device_index
|
||||
)
|
||||
)
|
||||
self.device = torch.device(f"cuda:{visible_device_index}")
|
||||
elif current_platform.is_xpu():
|
||||
self.device = torch.device(f"xpu:{local_rank}")
|
||||
self.device = torch.device(f"xpu:{self.device_index}")
|
||||
elif current_platform.is_out_of_tree():
|
||||
self.device = torch.device(f"{current_platform.device_name}:{local_rank}")
|
||||
self.device = torch.device(
|
||||
f"{current_platform.device_name}:{self.device_index}"
|
||||
)
|
||||
else:
|
||||
self.device = torch.device("cpu")
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import copy
|
||||
import dataclasses
|
||||
import functools
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from collections.abc import Callable
|
||||
from dataclasses import MISSING, asdict, dataclass, fields, is_dataclass
|
||||
@@ -465,6 +466,7 @@ class EngineArgs:
|
||||
numa_bind: bool = ParallelConfig.numa_bind
|
||||
numa_bind_nodes: list[int] | None = ParallelConfig.numa_bind_nodes
|
||||
numa_bind_cpus: list[str] | None = ParallelConfig.numa_bind_cpus
|
||||
device_ids: list[int | str] | None = None
|
||||
tensor_parallel_size: int = ParallelConfig.tensor_parallel_size
|
||||
prefill_context_parallel_size: int = ParallelConfig.prefill_context_parallel_size
|
||||
decode_context_parallel_size: int = ParallelConfig.decode_context_parallel_size
|
||||
@@ -979,6 +981,20 @@ class EngineArgs:
|
||||
parallel_group.add_argument(
|
||||
"--numa-bind-cpus", **parallel_kwargs["numa_bind_cpus"]
|
||||
)
|
||||
parallel_group.add_argument(
|
||||
"--device-ids",
|
||||
type=lambda s: [
|
||||
int(device_id) if device_id.isdigit() else device_id
|
||||
for device_id in (part.strip() for part in s.split(","))
|
||||
],
|
||||
default=None,
|
||||
help="Comma-separated physical GPU device IDs or UUIDs to use "
|
||||
'(e.g. --device-ids "2,3,5,7"). Avoids setting '
|
||||
"CUDA_VISIBLE_DEVICES, preserving full GPU topology "
|
||||
"visibility for GPU-NIC affinity and DeepGEMM. "
|
||||
"Note: has no effect with Ray executors; use Ray "
|
||||
"placement groups for GPU selection instead.",
|
||||
)
|
||||
parallel_group.add_argument(
|
||||
"--tensor-parallel-size", "-tp", **parallel_kwargs["tensor_parallel_size"]
|
||||
)
|
||||
@@ -1716,6 +1732,47 @@ class EngineArgs:
|
||||
)
|
||||
return SpeculativeConfig(**self.speculative_config)
|
||||
|
||||
def _resolve_device_ids(self) -> list[int] | None:
|
||||
if not self.device_ids:
|
||||
return None
|
||||
if self.distributed_executor_backend == "ray":
|
||||
logger.warning(
|
||||
"--device-ids has no effect when using the Ray executor. "
|
||||
"Use Ray placement groups for GPU selection instead."
|
||||
)
|
||||
ids = self.device_ids
|
||||
if len(set(ids)) != len(ids):
|
||||
raise ValueError(f"--device-ids must not contain duplicates: {ids}")
|
||||
if all(isinstance(i, str) for i in ids):
|
||||
return [
|
||||
current_platform.device_control_id_to_physical_device_id(i)
|
||||
for i in cast(list[str], ids)
|
||||
]
|
||||
if any(isinstance(i, str) for i in ids):
|
||||
raise ValueError("--device-ids must not mix integer IDs and UUIDs")
|
||||
int_ids = cast(list[int], ids)
|
||||
# Compose with CUDA_VISIBLE_DEVICES: if CVD is set, treat
|
||||
# --device-ids values as indices into the CVD-visible set.
|
||||
cvd = getattr(
|
||||
envs,
|
||||
current_platform.device_control_env_var,
|
||||
os.environ.get(current_platform.device_control_env_var),
|
||||
)
|
||||
if cvd:
|
||||
cvd_ids = [
|
||||
current_platform.device_control_id_to_physical_device_id(x)
|
||||
for x in cvd.split(",")
|
||||
]
|
||||
for i in int_ids:
|
||||
if i >= len(cvd_ids):
|
||||
raise ValueError(
|
||||
f"--device-ids index {i} is out of range for "
|
||||
f"{current_platform.device_control_env_var}"
|
||||
f"={cvd} ({len(cvd_ids)} devices visible)"
|
||||
)
|
||||
return [cvd_ids[i] for i in int_ids]
|
||||
return int_ids
|
||||
|
||||
def create_diffusion_config(self) -> DiffusionConfig | None:
|
||||
if self.diffusion_config is None:
|
||||
return None
|
||||
@@ -2029,6 +2086,7 @@ class EngineArgs:
|
||||
cp_kv_cache_interleave_size=self.cp_kv_cache_interleave_size,
|
||||
_api_process_count=self._api_process_count,
|
||||
_api_process_rank=self._api_process_rank,
|
||||
assigned_physical_gpu_ids=self._resolve_device_ids(),
|
||||
numa_bind=self.numa_bind,
|
||||
numa_bind_nodes=self.numa_bind_nodes,
|
||||
numa_bind_cpus=self.numa_bind_cpus,
|
||||
|
||||
@@ -898,12 +898,6 @@ class LLM(BeamSearchOfflineMixin, PoolingOfflineMixin, OfflineInferenceMixin):
|
||||
def finish_weight_update(self) -> None:
|
||||
"""Finish the current weight update."""
|
||||
self.llm_engine.collective_rpc("finish_weight_update")
|
||||
# Invalidate cached state computed with the old weights so it isn't
|
||||
# reused for subsequent requests:
|
||||
# - prefix cache: KV blocks computed with the old weights
|
||||
# - encoder cache: multimodal embeddings keyed only by mm_hash
|
||||
self.llm_engine.reset_prefix_cache()
|
||||
self.llm_engine.reset_encoder_cache()
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""Return a transformers-style hierarchical view of the model."""
|
||||
|
||||
@@ -23,12 +23,10 @@ import uvloop
|
||||
from fastapi import FastAPI, Response
|
||||
|
||||
from vllm.logger import init_logger
|
||||
from vllm.platforms import current_platform
|
||||
from vllm.utils.system_utils import (
|
||||
decorate_logs,
|
||||
kill_process_tree,
|
||||
set_process_title,
|
||||
update_environment_variables,
|
||||
)
|
||||
|
||||
logger = init_logger(__name__)
|
||||
@@ -127,22 +125,29 @@ def _build_vllm_dp_server_args(
|
||||
child_args.data_parallel_multi_port_external_lb = False
|
||||
child_args.data_parallel_supervisor_port = None
|
||||
child_args.api_server_count = 1
|
||||
child_args.device_ids = _build_device_ids(args, local_rank)
|
||||
return child_args
|
||||
|
||||
|
||||
def _build_vllm_dp_server_env(
|
||||
args: argparse.Namespace, local_rank: int
|
||||
) -> dict[str, str]:
|
||||
# set visible devices for the child process
|
||||
def _build_device_ids(args: argparse.Namespace, local_rank: int) -> list[int | str]:
|
||||
"""Build the --device-ids value for a DP child process.
|
||||
|
||||
The child resolves these against its own inherited device-control env
|
||||
var (e.g. CUDA_VISIBLE_DEVICES), so integer IDs must stay env-relative
|
||||
here rather than being translated to physical IDs.
|
||||
"""
|
||||
devices_per_rank = args.tensor_parallel_size * args.pipeline_parallel_size
|
||||
start = local_rank * devices_per_rank
|
||||
stop = start + devices_per_rank
|
||||
device_env = current_platform.device_control_env_var
|
||||
visible_devices = ",".join(
|
||||
str(current_platform.device_id_to_physical_device_id(idx))
|
||||
for idx in range(start, stop)
|
||||
)
|
||||
return {device_env: visible_devices}
|
||||
device_ids = getattr(args, "device_ids", None)
|
||||
if device_ids is not None:
|
||||
if stop > len(device_ids):
|
||||
raise ValueError(
|
||||
f"--device-ids has {len(device_ids)} entries, but DP rank "
|
||||
f"{local_rank} needs devices [{start}, {stop})"
|
||||
)
|
||||
return device_ids[start:stop]
|
||||
return list(range(start, stop))
|
||||
|
||||
|
||||
def _child_base_url(args: argparse.Namespace, port: int) -> str:
|
||||
@@ -228,9 +233,7 @@ def _build_dp_supervisor_app(supervisor: DPSupervisor) -> FastAPI:
|
||||
return app
|
||||
|
||||
|
||||
def _run_vllm_dp_server(
|
||||
child_args: argparse.Namespace, env_updates: dict[str, str]
|
||||
) -> None:
|
||||
def _run_vllm_dp_server(child_args: argparse.Namespace) -> None:
|
||||
"""
|
||||
Entrypoint function for the vLLM DP Server.
|
||||
"""
|
||||
@@ -241,7 +244,6 @@ def _run_vllm_dp_server(
|
||||
os.setpgrp()
|
||||
|
||||
name = f"APIServer_DP{child_args.data_parallel_rank}"
|
||||
update_environment_variables(env_updates)
|
||||
set_process_title(name)
|
||||
decorate_logs(name)
|
||||
uvloop.run(run_server(child_args))
|
||||
@@ -345,11 +347,10 @@ class DPSupervisor:
|
||||
context = multiprocessing.get_context("spawn")
|
||||
for local_rank in range(self.args.data_parallel_size_local):
|
||||
child_args = _build_vllm_dp_server_args(self.args, local_rank)
|
||||
child_env = _build_vllm_dp_server_env(self.args, local_rank)
|
||||
process = context.Process(
|
||||
target=_run_vllm_dp_server,
|
||||
name=f"APIServer_DPRank_{child_args.data_parallel_rank}",
|
||||
args=(child_args, child_env),
|
||||
args=(child_args,),
|
||||
)
|
||||
process.start()
|
||||
self._processes.append(process)
|
||||
|
||||
+3
-4
@@ -1579,10 +1579,9 @@ environment_variables: dict[str, Callable[[], Any]] = {
|
||||
os.getenv("VLLM_TOOL_PARSE_REGEX_TIMEOUT_SECONDS", "1")
|
||||
),
|
||||
# Enforce function parameter schemas in structural-tag based tool calling.
|
||||
"VLLM_ENFORCE_STRICT_TOOL_CALLING": lambda: os.getenv(
|
||||
"VLLM_ENFORCE_STRICT_TOOL_CALLING", "True"
|
||||
).lower()
|
||||
in ("true", "1"),
|
||||
"VLLM_ENFORCE_STRICT_TOOL_CALLING": lambda: (
|
||||
os.getenv("VLLM_ENFORCE_STRICT_TOOL_CALLING", "True").lower() in ("true", "1")
|
||||
),
|
||||
# Control the max chunk bytes (in MB) for the rpc message queue.
|
||||
# Object larger than this threshold will be broadcast to worker
|
||||
# processes via zmq.
|
||||
|
||||
@@ -56,6 +56,35 @@ class TrtLlmFp8ExpertsBase:
|
||||
self.moe_config = moe_config
|
||||
self.quant_config = quant_config
|
||||
|
||||
# Per-expert SwiGLU parameters from quant_config (MXFP8 + Swiglu only).
|
||||
device = torch.accelerator.current_device_index()
|
||||
if quant_config.gemm1_alpha is not None:
|
||||
self.gemm1_alpha = torch.tensor(
|
||||
[quant_config.gemm1_alpha] * self.local_num_experts,
|
||||
dtype=torch.float32,
|
||||
device=device,
|
||||
)
|
||||
else:
|
||||
self.gemm1_alpha = None
|
||||
|
||||
if quant_config.gemm1_beta is not None:
|
||||
self.gemm1_beta = torch.tensor(
|
||||
[quant_config.gemm1_beta] * self.local_num_experts,
|
||||
dtype=torch.float32,
|
||||
device=device,
|
||||
)
|
||||
else:
|
||||
self.gemm1_beta = None
|
||||
|
||||
if quant_config.gemm1_clamp_limit is not None:
|
||||
self.gemm1_clamp_limit = torch.tensor(
|
||||
[quant_config.gemm1_clamp_limit] * self.local_num_experts,
|
||||
dtype=torch.float32,
|
||||
device=device,
|
||||
)
|
||||
else:
|
||||
self.gemm1_clamp_limit = None
|
||||
|
||||
@staticmethod
|
||||
def activation_format() -> mk.FusedMoEActivationFormat:
|
||||
return mk.FusedMoEActivationFormat.Standard
|
||||
@@ -77,8 +106,12 @@ class TrtLlmFp8ExpertsBase:
|
||||
|
||||
@staticmethod
|
||||
def _supports_activation(activation: MoEActivation) -> bool:
|
||||
"""Supports only SiLU and RELU^2 non-gated activation."""
|
||||
return activation in [MoEActivation.SILU, MoEActivation.RELU2_NO_MUL]
|
||||
"""Supports SiLU, SwiGLU-OAI (uninterleaved), and RELU^2 non-gated."""
|
||||
return activation in [
|
||||
MoEActivation.SILU,
|
||||
MoEActivation.SWIGLUOAI_UNINTERLEAVE,
|
||||
MoEActivation.RELU2_NO_MUL,
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def _supports_parallel_config(moe_parallel_config: FusedMoEParallelConfig) -> bool:
|
||||
@@ -198,6 +231,9 @@ class TrtLlmFp8ExpertsModular(TrtLlmFp8ExpertsBase, mk.FusedMoEExpertsModular):
|
||||
hidden_states_scale=hidden_states_scale,
|
||||
gemm1_weights=w1,
|
||||
gemm1_weights_scale=self.quant_config.w1_scale,
|
||||
gemm1_alpha=self.gemm1_alpha,
|
||||
gemm1_beta=self.gemm1_beta,
|
||||
gemm1_clamp_limit=self.gemm1_clamp_limit,
|
||||
gemm2_weights=w2,
|
||||
gemm2_weights_scale=self.quant_config.w2_scale,
|
||||
num_experts=global_num_experts,
|
||||
@@ -327,7 +363,11 @@ class TrtLlmFp8ExpertsMonolithic(TrtLlmFp8ExpertsBase, mk.FusedMoEExpertsMonolit
|
||||
from flashinfer.fused_moe import Fp8QuantizationType, WeightLayout
|
||||
|
||||
assert not apply_router_weight_on_input
|
||||
assert activation in [MoEActivation.SILU, MoEActivation.RELU2_NO_MUL]
|
||||
assert activation in [
|
||||
MoEActivation.SILU,
|
||||
MoEActivation.SWIGLUOAI_UNINTERLEAVE,
|
||||
MoEActivation.RELU2_NO_MUL,
|
||||
]
|
||||
activation_type = activation_to_flashinfer_int(activation)
|
||||
assert self.topk <= global_num_experts
|
||||
assert global_num_experts % 4 == 0
|
||||
@@ -362,6 +402,9 @@ class TrtLlmFp8ExpertsMonolithic(TrtLlmFp8ExpertsBase, mk.FusedMoEExpertsMonolit
|
||||
hidden_states_scale=hidden_states_scale,
|
||||
gemm1_weights=w1,
|
||||
gemm1_weights_scale=self.quant_config.w1_scale,
|
||||
gemm1_alpha=self.gemm1_alpha,
|
||||
gemm1_beta=self.gemm1_beta,
|
||||
gemm1_clamp_limit=self.gemm1_clamp_limit,
|
||||
gemm2_weights=w2,
|
||||
gemm2_weights_scale=self.quant_config.w2_scale,
|
||||
num_experts=global_num_experts,
|
||||
|
||||
@@ -66,16 +66,47 @@ class TrtLlmNvFp4ExpertsBase:
|
||||
else:
|
||||
self.g1_scale_c = self.quant_config.a2_gscale.clone()
|
||||
|
||||
if moe_config.is_act_and_mul and quant_config.gemm1_clamp_limit is not None:
|
||||
device = torch.accelerator.current_device_index()
|
||||
self.gemm1_clamp_limit = torch.full(
|
||||
# Fall back to moe_config.swiglu_* when quant_config doesn't carry them
|
||||
# (ModelOpt NVFP4 checkpoints store these on moe_config, not quant_config).
|
||||
device = torch.accelerator.current_device_index()
|
||||
|
||||
def _per_expert(val: float | None) -> torch.Tensor | None:
|
||||
if val is None:
|
||||
return None
|
||||
return torch.full(
|
||||
(self.local_num_experts,),
|
||||
quant_config.gemm1_clamp_limit,
|
||||
float(val),
|
||||
dtype=torch.float32,
|
||||
device=device,
|
||||
)
|
||||
|
||||
clamp = quant_config.gemm1_clamp_limit
|
||||
if clamp is None:
|
||||
clamp = getattr(moe_config, "swiglu_limit", None)
|
||||
alpha = quant_config.gemm1_alpha
|
||||
if alpha is None:
|
||||
alpha = getattr(moe_config, "swiglu_alpha", None)
|
||||
beta = quant_config.gemm1_beta
|
||||
if beta is None:
|
||||
beta = getattr(moe_config, "swiglu_beta", None)
|
||||
|
||||
|
||||
if moe_config.is_act_and_mul:
|
||||
self.gemm1_clamp_limit = _per_expert(clamp)
|
||||
self.gemm1_alpha = _per_expert(alpha)
|
||||
self.gemm1_beta = _per_expert(beta)
|
||||
else:
|
||||
self.gemm1_clamp_limit = None
|
||||
self.gemm1_alpha = None
|
||||
self.gemm1_beta = None
|
||||
|
||||
logger.info_once(
|
||||
"activation=%s, gemm1_alpha=%s, gemm1_beta=%s, gemm1_clamp_limit=%s",
|
||||
moe_config.activation,
|
||||
alpha,
|
||||
beta,
|
||||
clamp,
|
||||
)
|
||||
|
||||
def process_weights_after_loading(self, layer: torch.nn.Module) -> None:
|
||||
layer.w13_weight_scale_2.data.mul_(layer.w13_input_scale)
|
||||
@@ -109,6 +140,25 @@ class TrtLlmNvFp4ExpertsBase:
|
||||
)
|
||||
self.gemm1_clamp_limit = layer.gemm1_clamp_limit
|
||||
|
||||
# beta shifts the raw GEMM1 accumulator, so fold by g1_alphas like the
|
||||
# clamp limit. alpha is applied to the dequantized gate, so it stays
|
||||
# raw. Register both on the layer so EPLB rearranges them with the
|
||||
# other per-expert tensors.
|
||||
if self.gemm1_beta is not None:
|
||||
gemm1_beta = self.gemm1_beta / self.quant_config.g1_alphas
|
||||
layer.register_parameter(
|
||||
"gemm1_beta",
|
||||
torch.nn.Parameter(gemm1_beta, requires_grad=False),
|
||||
)
|
||||
self.gemm1_beta = layer.gemm1_beta
|
||||
|
||||
if self.gemm1_alpha is not None:
|
||||
layer.register_parameter(
|
||||
"gemm1_alpha",
|
||||
torch.nn.Parameter(self.gemm1_alpha, requires_grad=False),
|
||||
)
|
||||
self.gemm1_alpha = layer.gemm1_alpha
|
||||
|
||||
@staticmethod
|
||||
def _supports_current_device() -> bool:
|
||||
"""Supports only Blackwell-family GPUs."""
|
||||
@@ -137,12 +187,14 @@ class TrtLlmNvFp4ExpertsBase:
|
||||
|
||||
@staticmethod
|
||||
def _supports_activation(activation: MoEActivation) -> bool:
|
||||
"""Supports only SiLU, RELU^2 non-gated and GELU activation."""
|
||||
"""Supports SiLU, RELU^2 non-gated, GELU, and clamped SwiGLU-OAI."""
|
||||
return activation in [
|
||||
MoEActivation.SILU,
|
||||
MoEActivation.RELU2_NO_MUL,
|
||||
MoEActivation.GELU,
|
||||
MoEActivation.GELU_TANH,
|
||||
MoEActivation.SWIGLUOAI,
|
||||
MoEActivation.SWIGLUOAI_UNINTERLEAVE,
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
@@ -248,8 +300,8 @@ class TrtLlmNvFp4ExpertsModular(TrtLlmNvFp4ExpertsBase, mk.FusedMoEExpertsModula
|
||||
gemm1_weights=w1,
|
||||
gemm1_weights_scale=self.quant_config.w1_scale.view(torch.float8_e4m3fn),
|
||||
gemm1_bias=None,
|
||||
gemm1_alpha=None,
|
||||
gemm1_beta=None,
|
||||
gemm1_alpha=self.gemm1_alpha,
|
||||
gemm1_beta=self.gemm1_beta,
|
||||
gemm1_clamp_limit=self.gemm1_clamp_limit,
|
||||
gemm2_weights=w2,
|
||||
gemm2_weights_scale=self.quant_config.w2_scale.view(torch.float8_e4m3fn),
|
||||
@@ -409,8 +461,8 @@ class TrtLlmNvFp4ExpertsMonolithic(
|
||||
gemm1_weights=w1,
|
||||
gemm1_weights_scale=self.quant_config.w1_scale.view(torch.float8_e4m3fn),
|
||||
gemm1_bias=None,
|
||||
gemm1_alpha=None,
|
||||
gemm1_beta=None,
|
||||
gemm1_alpha=self.gemm1_alpha,
|
||||
gemm1_beta=self.gemm1_beta,
|
||||
gemm1_clamp_limit=self.gemm1_clamp_limit,
|
||||
gemm2_weights=w2,
|
||||
gemm2_weights_scale=self.quant_config.w2_scale.view(torch.float8_e4m3fn),
|
||||
|
||||
@@ -140,6 +140,7 @@ def FusedMoE(
|
||||
apply_routed_scale_to_output: bool = False,
|
||||
zero_expert_type: str | None = None,
|
||||
hash_indices_table: torch.Tensor | None = None,
|
||||
reduce_results: bool = True,
|
||||
runner_cls: type[MoERunner] | None = None,
|
||||
runner_args: dict[str, Any] | None = None,
|
||||
routed_experts_cls: type[RoutedExperts] | None = None,
|
||||
@@ -198,6 +199,9 @@ def FusedMoE(
|
||||
output instead of topk_weights
|
||||
zero_expert_type: Type of zero expert handling
|
||||
hash_indices_table: Hash table for expert indices
|
||||
reduce_results: Whether to all-reduce the final output across TP/EP
|
||||
ranks. Set to False to defer the all-reduce (e.g. to fuse it into
|
||||
a subsequent GemmaRMSNorm).
|
||||
runner_cls: Custom MoERunner class (None = use default MoERunner)
|
||||
runner_args: Additional arguments for runner constructor
|
||||
routed_experts_cls: Custom RoutedExperts class (None = use default)
|
||||
@@ -385,6 +389,7 @@ def FusedMoE(
|
||||
routed_scaling_factor=routed_scaling_factor
|
||||
if apply_routed_scale_to_output
|
||||
else 1.0,
|
||||
reduce_results=reduce_results,
|
||||
**runner_args if runner_args is not None else {},
|
||||
)
|
||||
|
||||
|
||||
@@ -254,6 +254,7 @@ class MoERunner(MoERunnerInterface):
|
||||
routed_input_transform: torch.nn.Module | None = None,
|
||||
routed_output_transform: torch.nn.Module | None = None,
|
||||
routed_scaling_factor: float = 1.0,
|
||||
reduce_results: bool = True,
|
||||
):
|
||||
super().__init__()
|
||||
self.moe_config = moe_config
|
||||
@@ -265,6 +266,7 @@ class MoERunner(MoERunnerInterface):
|
||||
self.shared_expert_gate = shared_expert_gate
|
||||
self.routed_experts = routed_experts
|
||||
self.enable_dbo = enable_dbo
|
||||
self.reduce_results = reduce_results
|
||||
|
||||
# When both gates are present and FSE is enabled, fuse their
|
||||
# weight matrices into [num_experts + num_shared, hidden] so one
|
||||
@@ -420,6 +422,15 @@ class MoERunner(MoERunnerInterface):
|
||||
* If we have SP (TP=N, DP=M, EP), there is a separate AG step handled
|
||||
in the model.
|
||||
"""
|
||||
# A combine kernel that already reduces the fused output is
|
||||
# incompatible with deferring the all-reduce (reduce_results=False,
|
||||
# e.g. fusing it into a subsequent GemmaRMSNorm): the deferred
|
||||
# all-reduce would double-reduce the fused output.
|
||||
assert not (self._fused_output_is_reduced and not self.reduce_results), (
|
||||
"reduce_results=False is incompatible with a combine kernel that "
|
||||
"already reduces the fused output (e.g. DeepEP/Mori/NIXL/"
|
||||
"FlashInfer-NVLink all2all backends)."
|
||||
)
|
||||
if (
|
||||
shared_output is not None
|
||||
and not self.moe_config.is_sequence_parallel
|
||||
@@ -447,6 +458,7 @@ class MoERunner(MoERunnerInterface):
|
||||
not self.moe_config.is_sequence_parallel
|
||||
and (self.moe_config.tp_size > 1 or self.moe_config.ep_size > 1)
|
||||
and not self._fused_output_is_reduced
|
||||
and self.reduce_results
|
||||
):
|
||||
states = tensor_model_parallel_all_reduce(states)
|
||||
|
||||
|
||||
+2
@@ -153,6 +153,8 @@ class CompressedTensorsW8A8Mxfp8MoEMethod(CompressedTensorsMoEMethod):
|
||||
a2_scale=layer.w2_input_scale,
|
||||
block_shape=self.weight_block_size,
|
||||
swiglu_limit=getattr(layer, "swiglu_limit", None),
|
||||
gemm1_alpha=getattr(layer, "swiglu_alpha", None),
|
||||
gemm1_beta=getattr(layer, "swiglu_beta", None),
|
||||
)
|
||||
|
||||
def maybe_make_prepare_finalize(
|
||||
|
||||
@@ -2283,6 +2283,7 @@ class ModelOptMixedPrecisionConfig(ModelOptQuantConfigBase):
|
||||
fp8_config: ModelOptFp8Config,
|
||||
nvfp4_config: ModelOptNvFp4Config,
|
||||
w4a16_nvfp4_config: ModelOptNvFp4Config,
|
||||
mxfp8_config: ModelOptMxFp8Config,
|
||||
) -> None:
|
||||
super().__init__(exclude_modules)
|
||||
self.kv_cache_quant_method = kv_cache_quant_method
|
||||
@@ -2290,6 +2291,7 @@ class ModelOptMixedPrecisionConfig(ModelOptQuantConfigBase):
|
||||
self.fp8_config = fp8_config
|
||||
self.nvfp4_config = nvfp4_config
|
||||
self.w4a16_nvfp4_config = w4a16_nvfp4_config
|
||||
self.mxfp8_config = mxfp8_config
|
||||
|
||||
def get_name(self) -> QuantizationMethods:
|
||||
return "modelopt_mixed"
|
||||
@@ -2379,6 +2381,12 @@ class ModelOptMixedPrecisionConfig(ModelOptQuantConfigBase):
|
||||
group_size=group_size,
|
||||
)
|
||||
|
||||
mxfp8_config = ModelOptMxFp8Config(
|
||||
is_checkpoint_mxfp8_serialized=True,
|
||||
kv_cache_quant_algo=kv_cache_quant_method,
|
||||
exclude_modules=[],
|
||||
)
|
||||
|
||||
return cls(
|
||||
kv_cache_quant_method=kv_cache_quant_method,
|
||||
exclude_modules=exclude_modules,
|
||||
@@ -2386,6 +2394,7 @@ class ModelOptMixedPrecisionConfig(ModelOptQuantConfigBase):
|
||||
fp8_config=fp8_config,
|
||||
nvfp4_config=nvfp4_config,
|
||||
w4a16_nvfp4_config=w4a16_nvfp4_config,
|
||||
mxfp8_config=mxfp8_config,
|
||||
)
|
||||
|
||||
def _resolve_quant_algo(self, prefix: str) -> str | None:
|
||||
@@ -2440,6 +2449,17 @@ class ModelOptMixedPrecisionConfig(ModelOptQuantConfigBase):
|
||||
if key.startswith(parent_dot):
|
||||
return info["quant_algo"].upper()
|
||||
|
||||
# 4. Parent-prefix fallback for fused projections (qkv_proj, gate_up_proj).
|
||||
for candidate in self._quantized_layer_prefix_candidates(prefix):
|
||||
parent_dot = candidate.rsplit(".", 1)[0] + "."
|
||||
algos = {
|
||||
info["quant_algo"].upper()
|
||||
for key, info in self.quantized_layers.items()
|
||||
if key.startswith(parent_dot) and "." not in key[len(parent_dot):]
|
||||
}
|
||||
if len(algos) == 1:
|
||||
return algos.pop()
|
||||
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
@@ -2485,6 +2505,8 @@ class ModelOptMixedPrecisionConfig(ModelOptQuantConfigBase):
|
||||
return ModelOptNvFp4LinearMethod(self.nvfp4_config)
|
||||
if quant_algo == "W4A16_NVFP4":
|
||||
return ModelOptNvFp4W4A16LinearMethod(self.w4a16_nvfp4_config)
|
||||
if quant_algo == "MXFP8":
|
||||
return ModelOptMxFp8LinearMethod(self.mxfp8_config)
|
||||
# Layer not in quantized_layers — leave unquantized
|
||||
return UnquantizedLinearMethod()
|
||||
|
||||
@@ -2504,6 +2526,11 @@ class ModelOptMixedPrecisionConfig(ModelOptQuantConfigBase):
|
||||
quant_config=self.w4a16_nvfp4_config,
|
||||
moe_config=layer.moe_config,
|
||||
)
|
||||
if quant_algo == "MXFP8":
|
||||
return ModelOptMxFp8FusedMoE(
|
||||
quant_config=self.mxfp8_config,
|
||||
moe_config=layer.moe_config,
|
||||
)
|
||||
return None
|
||||
|
||||
return None
|
||||
|
||||
@@ -224,6 +224,8 @@ class Mxfp8OnlineMoEMethod(OnlineMoEMethodBase):
|
||||
w2_bias=getattr(layer, "w2_bias", None),
|
||||
block_shape=self.weight_block_size,
|
||||
swiglu_limit=getattr(layer, "swiglu_limit", None),
|
||||
gemm1_alpha=getattr(layer, "swiglu_alpha", None),
|
||||
gemm1_beta=getattr(layer, "swiglu_beta", None),
|
||||
)
|
||||
|
||||
def process_weights_after_loading(self, layer: Module) -> None:
|
||||
|
||||
@@ -36,6 +36,13 @@ def activation_to_flashinfer_type(activation: MoEActivation) -> "ActivationType"
|
||||
MoEActivation.GELU: ActivationType.Geglu,
|
||||
MoEActivation.GELU_TANH: ActivationType.Geglu,
|
||||
MoEActivation.RELU2_NO_MUL: ActivationType.Relu2,
|
||||
# Both OAI variants map to Swiglu: FlashInfer has no SwigluOAI enum;
|
||||
# the clamped/biased behavior is driven by the per-expert gemm1_alpha/
|
||||
# gemm1_beta/gemm1_clamp_limit tensors (see trtllm_nvfp4_moe.py).
|
||||
# The interleaved-vs-contiguous row layout difference between the two
|
||||
# is resolved in process_weights_after_loading, not here.
|
||||
MoEActivation.SWIGLUOAI: ActivationType.Swiglu,
|
||||
MoEActivation.SWIGLUOAI_UNINTERLEAVE: ActivationType.Swiglu,
|
||||
}
|
||||
return ACTIVATION_TO_FI_ACTIVATION[activation]
|
||||
|
||||
|
||||
@@ -1202,6 +1202,49 @@ class Qwen3VLDummyInputsBuilder(BaseDummyInputsBuilder[Qwen3VLProcessingInfo]):
|
||||
return video_items
|
||||
|
||||
|
||||
def _replace_video_token_placeholders(
|
||||
prompt_ids: list[int],
|
||||
target: list[int],
|
||||
replacements: list[list[int]],
|
||||
) -> list[int]:
|
||||
"""Replace each 3-token video placeholder with its expanded sequence.
|
||||
|
||||
Args:
|
||||
prompt_ids: Token IDs of the original (unexpanded) prompt.
|
||||
target: 3-element list ``[vision_start_id, video_pad_id,
|
||||
vision_end_id]`` to search for.
|
||||
replacements: Per-video expanded token sequences, in prompt order.
|
||||
|
||||
Returns:
|
||||
Token IDs with every placeholder triplet replaced.
|
||||
"""
|
||||
result: list[int] = []
|
||||
repl_idx = 0
|
||||
i = 0
|
||||
n = len(prompt_ids)
|
||||
t0, t1, t2 = target
|
||||
num_repl = len(replacements)
|
||||
|
||||
while i < n:
|
||||
if (
|
||||
i + 2 < n
|
||||
and prompt_ids[i] == t0
|
||||
and prompt_ids[i + 1] == t1
|
||||
and prompt_ids[i + 2] == t2
|
||||
):
|
||||
result.extend(replacements[repl_idx])
|
||||
repl_idx += 1
|
||||
i += 3
|
||||
else:
|
||||
result.append(prompt_ids[i])
|
||||
i += 1
|
||||
|
||||
assert repl_idx == num_repl, (
|
||||
f"Found {repl_idx} video placeholders but expected {num_repl}"
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
class Qwen3VLMultiModalProcessor(BaseMultiModalProcessor[Qwen3VLProcessingInfo]):
|
||||
def _call_hf_processor(
|
||||
self,
|
||||
@@ -1211,15 +1254,23 @@ class Qwen3VLMultiModalProcessor(BaseMultiModalProcessor[Qwen3VLProcessingInfo])
|
||||
tok_kwargs: Mapping[str, object],
|
||||
) -> BatchFeature:
|
||||
mm_data = dict(mm_data)
|
||||
processor = self.info.get_hf_processor(**mm_kwargs)
|
||||
|
||||
# Separate video processing from image processing. Because the videos
|
||||
# are processed into several image patches
|
||||
video_input_ids_lst: list[list[int]] = []
|
||||
if videos := mm_data.pop("videos", []):
|
||||
video_grid_thw_lst = []
|
||||
pixel_values_videos_lst = []
|
||||
timestamps_per_video = []
|
||||
|
||||
hf_config = self.info.get_hf_config()
|
||||
tokenizer = self.info.get_tokenizer()
|
||||
merge_size = hf_config.vision_config.spatial_merge_size
|
||||
video_pruning_rate = self.info.ctx.get_mm_config().video_pruning_rate
|
||||
vision_start_token_id = hf_config.vision_start_token_id
|
||||
vision_end_token_id = hf_config.vision_end_token_id
|
||||
video_token_id = hf_config.video_token_id
|
||||
|
||||
for item in videos:
|
||||
video_array, metadata = item
|
||||
|
||||
@@ -1269,55 +1320,38 @@ class Qwen3VLMultiModalProcessor(BaseMultiModalProcessor[Qwen3VLProcessingInfo])
|
||||
tok_kwargs=tok_kwargs,
|
||||
)
|
||||
|
||||
merge_size = processor.video_processor.merge_size
|
||||
# Get video grid info for EVS calculation.
|
||||
# Discard HF output input_ids — we use get_video_repl below
|
||||
# to generate the correct (EVS-adjusted) token sequence.
|
||||
video_outputs.pop("input_ids", None)
|
||||
|
||||
video_grid_thw = video_outputs["video_grid_thw"]
|
||||
num_frames = int(video_grid_thw[0, 0])
|
||||
tokens_per_frame_base = int(video_grid_thw[0, 1:].prod()) // (
|
||||
merge_size**2
|
||||
)
|
||||
|
||||
# Apply EVS if enabled.
|
||||
video_pruning_rate = self.info.ctx.get_mm_config().video_pruning_rate
|
||||
if video_pruning_rate is not None and video_pruning_rate > 0.0:
|
||||
num_tokens = compute_retained_tokens_count(
|
||||
tokens_per_frame=tokens_per_frame_base,
|
||||
num_frames=num_frames,
|
||||
q=video_pruning_rate,
|
||||
)
|
||||
# Here we just need placeholders that won't actually be replaced -
|
||||
# we just need to make sure the total number of tokens is correct
|
||||
# assign all tokens to the first frame.
|
||||
tokens_per_frame = [num_tokens] + [0] * (num_frames - 1)
|
||||
select_token_id = False
|
||||
else:
|
||||
tokens_per_frame = [tokens_per_frame_base] * num_frames
|
||||
select_token_id = True
|
||||
|
||||
# Generate the video replacement with EVS-adjusted token counts
|
||||
tokenizer = self.info.get_tokenizer()
|
||||
hf_config = self.info.get_hf_config()
|
||||
video_repl = Qwen3VLMultiModalProcessor.get_video_repl(
|
||||
tokens_per_frame=tokens_per_frame,
|
||||
timestamps=timestamps,
|
||||
tokenizer=tokenizer,
|
||||
vision_start_token_id=hf_config.vision_start_token_id,
|
||||
vision_end_token_id=hf_config.vision_end_token_id,
|
||||
video_token_id=hf_config.video_token_id,
|
||||
vision_start_token_id=vision_start_token_id,
|
||||
vision_end_token_id=vision_end_token_id,
|
||||
video_token_id=video_token_id,
|
||||
select_token_id=select_token_id,
|
||||
)
|
||||
|
||||
# Convert token IDs to text for the HF processor flow
|
||||
video_placeholder = tokenizer.decode(
|
||||
video_repl.full, skip_special_tokens=False
|
||||
)
|
||||
input_ids = video_outputs.pop("input_ids")
|
||||
video_placeholder = processor.tokenizer.batch_decode(input_ids)[0]
|
||||
prompt = prompt.replace(
|
||||
"<|vision_start|><|video_pad|><|vision_end|>",
|
||||
video_placeholder,
|
||||
1,
|
||||
)
|
||||
video_input_ids_lst.append(list(video_repl.full))
|
||||
|
||||
video_grid_thw_lst.append(video_outputs["video_grid_thw"])
|
||||
pixel_values_videos_lst.append(video_outputs["pixel_values_videos"])
|
||||
@@ -1335,6 +1369,24 @@ class Qwen3VLMultiModalProcessor(BaseMultiModalProcessor[Qwen3VLProcessingInfo])
|
||||
mm_kwargs=mm_kwargs,
|
||||
tok_kwargs=tok_kwargs,
|
||||
)
|
||||
|
||||
# Replace each placeholder triplet with pre-computed video tokens.
|
||||
if video_input_ids_lst:
|
||||
hf_config = self.info.get_hf_config()
|
||||
video_target = [
|
||||
hf_config.vision_start_token_id,
|
||||
hf_config.video_token_id,
|
||||
hf_config.vision_end_token_id,
|
||||
]
|
||||
input_ids = processed_outputs.pop("input_ids")
|
||||
if not isinstance(input_ids, list):
|
||||
input_ids = input_ids.tolist()
|
||||
(prompt_ids,) = input_ids
|
||||
expanded_ids = _replace_video_token_placeholders(
|
||||
prompt_ids, video_target, video_input_ids_lst
|
||||
)
|
||||
processed_outputs["input_ids"] = [expanded_ids]
|
||||
|
||||
combined_outputs = dict(
|
||||
processed_outputs,
|
||||
**video_outputs,
|
||||
|
||||
@@ -457,6 +457,7 @@ class MiniMaxM3SparseAttention(nn.Module, AttentionLayerBase):
|
||||
quant_config: QuantizationConfig | None = None,
|
||||
prefix: str = "",
|
||||
cache_config: CacheConfig | None = None,
|
||||
topk_indices_buffer: torch.Tensor | None = None,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.hidden_size = config.hidden_size
|
||||
@@ -535,6 +536,9 @@ class MiniMaxM3SparseAttention(nn.Module, AttentionLayerBase):
|
||||
self.kv_cache_dtype, vllm_config.model_config
|
||||
)
|
||||
|
||||
# Shared top-k buffer: the indexer writes the selected blocks into it and
|
||||
# the attend impl reads them back (no Python value crosses the break).
|
||||
self.topk_indices_buffer = topk_indices_buffer
|
||||
self.attn_backend = MiniMaxM3SparseBackend
|
||||
# Indexer and main attention are separate impls. On ROCm the SM100 gate
|
||||
# is always False, so both pick Triton and the index cache stays bf16.
|
||||
@@ -565,6 +569,7 @@ class MiniMaxM3SparseAttention(nn.Module, AttentionLayerBase):
|
||||
local_blocks=sparse_cfg.get("sparse_local_block", 0),
|
||||
score_type=sparse_cfg.get("sparse_score_type", "max"),
|
||||
cache_config=cache_config,
|
||||
topk_indices_buffer=topk_indices_buffer,
|
||||
)
|
||||
|
||||
# Register the main K/V cache so the KV-cache manager allocates it.
|
||||
@@ -657,9 +662,10 @@ class MiniMaxM3SparseAttention(nn.Module, AttentionLayerBase):
|
||||
output: torch.Tensor,
|
||||
) -> torch.Tensor:
|
||||
# Single eager break around both: their split-K kernels read per-request
|
||||
# metadata and can't be captured into a cudagraph.
|
||||
topk_idx = self.indexer(index_query)
|
||||
return self.impl.forward(self, query, self.kv_cache, topk_idx, output)
|
||||
# metadata and can't be captured into a cudagraph. The indexer writes its
|
||||
# top-k into the shared ``topk_indices_buffer``; the attend reads it back.
|
||||
self.indexer(index_query)
|
||||
return self.impl.forward(self, query, self.kv_cache, output)
|
||||
|
||||
|
||||
class MiniMaxM3DecoderLayer(nn.Module):
|
||||
@@ -671,6 +677,7 @@ class MiniMaxM3DecoderLayer(nn.Module):
|
||||
quant_config: QuantizationConfig | None = None,
|
||||
force_sparse_attn: bool = False,
|
||||
force_moe: bool = False,
|
||||
topk_indices_buffer: torch.Tensor | None = None,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.hidden_size = config.hidden_size
|
||||
@@ -690,6 +697,7 @@ class MiniMaxM3DecoderLayer(nn.Module):
|
||||
quant_config=quant_config,
|
||||
prefix=f"{prefix}.self_attn",
|
||||
cache_config=cache_config,
|
||||
topk_indices_buffer=topk_indices_buffer,
|
||||
)
|
||||
else:
|
||||
self.self_attn = MiniMaxM3Attention(
|
||||
@@ -771,6 +779,22 @@ class MiniMaxM3Model(nn.Module, EagleModelMixin):
|
||||
prefix=f"{prefix}.embed_tokens",
|
||||
)
|
||||
|
||||
# Reserved top-k indices buffer shared by all sparse-attention indexer
|
||||
# layers (mirrors DeepseekV4); the indexer writes its per-head decode/
|
||||
# prefill block selection into it, the attend reads it back.
|
||||
sparse_cfg = getattr(config, "sparse_attention_config", None)
|
||||
if sparse_cfg is not None:
|
||||
tp_size = get_tensor_model_parallel_world_size()
|
||||
num_index_heads = max(1, sparse_cfg["sparse_num_index_heads"] // tp_size)
|
||||
self.topk_indices_buffer = torch.empty(
|
||||
num_index_heads,
|
||||
vllm_config.scheduler_config.max_num_batched_tokens,
|
||||
sparse_cfg["sparse_topk_blocks"],
|
||||
dtype=torch.int32,
|
||||
)
|
||||
else:
|
||||
self.topk_indices_buffer = None
|
||||
|
||||
self.start_layer, self.end_layer, self.layers = make_layers(
|
||||
config.num_hidden_layers,
|
||||
lambda prefix: MiniMaxM3DecoderLayer(
|
||||
@@ -778,6 +802,7 @@ class MiniMaxM3Model(nn.Module, EagleModelMixin):
|
||||
prefix,
|
||||
cache_config=cache_config,
|
||||
quant_config=quant_config,
|
||||
topk_indices_buffer=self.topk_indices_buffer,
|
||||
),
|
||||
prefix=f"{prefix}.layers",
|
||||
)
|
||||
|
||||
@@ -25,12 +25,14 @@ from vllm.config.attention import IndexerKVDType
|
||||
from vllm.config.cache import CacheDType
|
||||
from vllm.distributed import get_tensor_model_parallel_world_size
|
||||
from vllm.forward_context import get_forward_context
|
||||
from vllm.logger import init_logger
|
||||
from vllm.model_executor.layers.attention_layer_base import AttentionLayerBase
|
||||
from vllm.models.minimax_m3.common.ops.index_topk import (
|
||||
minimax_m3_index_decode,
|
||||
minimax_m3_index_score,
|
||||
minimax_m3_index_topk,
|
||||
)
|
||||
from vllm.platforms import current_platform
|
||||
from vllm.v1.attention.backend import (
|
||||
AttentionBackend,
|
||||
AttentionCGSupport,
|
||||
@@ -46,6 +48,8 @@ from vllm.v1.kv_cache_interface import (
|
||||
MLAAttentionSpec,
|
||||
)
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
|
||||
class MiniMaxM3IndexerBackend(AttentionBackend):
|
||||
"""Indexer side-cache backend (key-only)."""
|
||||
@@ -120,16 +124,20 @@ class MiniMaxM3IndexerCache(nn.Module, AttentionLayerBase):
|
||||
backend_cls: type[AttentionBackend] = MiniMaxM3IndexerBackend,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
if indexer_kv_dtype != "bf16":
|
||||
if indexer_kv_dtype in ("fp8", "fp8_e4m3"):
|
||||
cache_dtype = torch.float8_e4m3fn
|
||||
elif indexer_kv_dtype == "bf16":
|
||||
cache_dtype = torch.bfloat16
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
f"indexer_kv_dtype={indexer_kv_dtype!r} is not supported yet "
|
||||
"for the MiniMax M3 indexer cache (only 'bf16')."
|
||||
f"indexer_kv_dtype={indexer_kv_dtype!r} is not supported by the "
|
||||
"MiniMax M3 indexer cache (only 'bf16' or 'fp8'/'fp8_e4m3')."
|
||||
)
|
||||
self.kv_cache = torch.tensor([])
|
||||
self.head_dim = head_dim
|
||||
self.indexer_kv_dtype = indexer_kv_dtype
|
||||
# Storage dtype for the side cache (bf16 today; quantized layouts later).
|
||||
self.dtype = torch.bfloat16
|
||||
# Side-cache storage dtype: bf16, or e4m3 for the fp8 score path.
|
||||
self.dtype = cache_dtype
|
||||
self.prefix = prefix
|
||||
self.cache_config = cache_config
|
||||
# Impl-chosen backend -> each impl gets its own builder (get_attn_backend).
|
||||
@@ -344,6 +352,7 @@ class MiniMaxM3IndexerImpl(nn.Module):
|
||||
score_type: str = "max",
|
||||
cache_config: CacheConfig | None = None,
|
||||
indexer_kv_dtype: IndexerKVDType = "bf16",
|
||||
topk_indices_buffer: torch.Tensor | None = None,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.num_kv_heads = num_kv_heads
|
||||
@@ -356,6 +365,9 @@ class MiniMaxM3IndexerImpl(nn.Module):
|
||||
self.num_index_heads = num_index_heads
|
||||
self.index_head_dim = index_head_dim
|
||||
self.indexer_kv_dtype = indexer_kv_dtype
|
||||
# Shared, stable-address top-k output buffer (set by the model for the
|
||||
# cudagraph-safe MSA impl); None -> impl allocates fresh (eager).
|
||||
self.topk_indices_buffer = topk_indices_buffer
|
||||
# Owns the side cache (registers itself in the static forward context).
|
||||
self.index_cache = MiniMaxM3IndexerCache(
|
||||
head_dim=index_head_dim,
|
||||
@@ -392,6 +404,10 @@ class MiniMaxM3IndexerTritonImpl(MiniMaxM3IndexerImpl):
|
||||
)
|
||||
kv = self.index_cache.kv_cache
|
||||
|
||||
# Both sides write into the single shared persistent topk_indices_buffer
|
||||
# (decode at [:, :nd], prefill at [:, nd:]) and return views into it; the
|
||||
# kernels' out= writes out[:, :total_q]. None -> allocate fresh.
|
||||
buf = self.topk_indices_buffer
|
||||
decode_topk: torch.Tensor | None = None
|
||||
prefill_topk: torch.Tensor | None = None
|
||||
if index_md.num_decodes > 0:
|
||||
@@ -409,6 +425,7 @@ class MiniMaxM3IndexerTritonImpl(MiniMaxM3IndexerImpl):
|
||||
self.num_kv_heads,
|
||||
d.decode_query_len,
|
||||
d.max_decode_query_len,
|
||||
out=buf,
|
||||
)
|
||||
if index_md.num_prefills > 0:
|
||||
p = index_md.prefill
|
||||
@@ -432,29 +449,61 @@ class MiniMaxM3IndexerTritonImpl(MiniMaxM3IndexerImpl):
|
||||
self.topk_blocks,
|
||||
self.init_blocks,
|
||||
self.local_blocks,
|
||||
out=buf[:, nd:, :] if buf is not None else None,
|
||||
)
|
||||
return decode_topk, prefill_topk
|
||||
|
||||
|
||||
def select_indexer_impl_cls(
|
||||
*,
|
||||
topk_blocks: int,
|
||||
indexer_kv_dtype: IndexerKVDType = "bf16",
|
||||
) -> type[MiniMaxM3IndexerImpl]:
|
||||
"""Pick the indexer impl off the index-cache dtype.
|
||||
"""Pick the indexer impl off the platform, top-k count, and cache dtype.
|
||||
|
||||
The SM100 MSA indexer score path is disabled for now; use the local Triton
|
||||
indexer. If re-enabled, add a NVIDIA-specific ``MiniMaxM3IndexerImpl`` here.
|
||||
On Blackwell (SM100) with ``topk_blocks`` in ``(4, 8, 16, 32)`` (matching the
|
||||
main MSA attend), the fmha_sm100 score path + Triton top-k is used for both
|
||||
bf16 and fp8 index caches. Everything else falls back to the Triton indexer
|
||||
(bf16 only).
|
||||
"""
|
||||
if indexer_kv_dtype in ("mxfp4", "nvfp4"):
|
||||
raise NotImplementedError(
|
||||
f"indexer_kv_dtype={indexer_kv_dtype!r} needs the (not-yet-added) "
|
||||
"CuteDSL indexer impl."
|
||||
)
|
||||
is_sm100 = (
|
||||
current_platform.is_cuda() and current_platform.is_device_capability_family(100)
|
||||
)
|
||||
use_msa = (
|
||||
is_sm100
|
||||
and topk_blocks in (4, 8, 16, 32)
|
||||
and indexer_kv_dtype in ("bf16", "fp8", "fp8_e4m3")
|
||||
)
|
||||
if use_msa:
|
||||
# Lazy import so AMD / non-SM100 never import fmha_sm100.
|
||||
from vllm.models.minimax_m3.nvidia.indexer_msa import (
|
||||
MiniMaxM3IndexerMSAImpl,
|
||||
)
|
||||
|
||||
logger.info_once(
|
||||
"MiniMax M3 indexer: selected MSA (fmha_sm100 score + Triton top-k) "
|
||||
"[topk_blocks=%d, indexer_kv_dtype=%s]",
|
||||
topk_blocks,
|
||||
indexer_kv_dtype,
|
||||
)
|
||||
return MiniMaxM3IndexerMSAImpl
|
||||
if indexer_kv_dtype != "bf16":
|
||||
raise NotImplementedError(
|
||||
f"indexer_kv_dtype={indexer_kv_dtype!r} is not supported by the "
|
||||
"Triton indexer impl."
|
||||
)
|
||||
logger.info_once(
|
||||
"MiniMax M3 indexer: selected Triton (no fmha_sm100) "
|
||||
"[topk_blocks=%d, indexer_kv_dtype=%s, sm100=%s]",
|
||||
topk_blocks,
|
||||
indexer_kv_dtype,
|
||||
is_sm100,
|
||||
)
|
||||
return MiniMaxM3IndexerTritonImpl
|
||||
|
||||
|
||||
@@ -480,9 +529,11 @@ class MiniMaxM3Indexer(nn.Module):
|
||||
score_type: str = "max",
|
||||
cache_config: CacheConfig | None = None,
|
||||
indexer_kv_dtype: IndexerKVDType = "bf16",
|
||||
topk_indices_buffer: torch.Tensor | None = None,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
impl_cls = select_indexer_impl_cls(
|
||||
topk_blocks=topk_blocks,
|
||||
indexer_kv_dtype=indexer_kv_dtype,
|
||||
)
|
||||
self.impl = impl_cls(
|
||||
@@ -498,6 +549,7 @@ class MiniMaxM3Indexer(nn.Module):
|
||||
score_type=score_type,
|
||||
cache_config=cache_config,
|
||||
indexer_kv_dtype=indexer_kv_dtype,
|
||||
topk_indices_buffer=topk_indices_buffer,
|
||||
)
|
||||
|
||||
@property
|
||||
|
||||
@@ -373,7 +373,10 @@ def _decode_index_score_kernel(
|
||||
+ off_k[:, None] * stride_ik_pos
|
||||
+ off_d * stride_ik_d,
|
||||
) # [N,D]
|
||||
kq = tl.dot(k, q) # [N,HQ]
|
||||
# fp32 accumulation is required for the fp8 (e4m3) index cache: q/k are
|
||||
# loaded in their stored dtype (bf16 or e4m3) and the MMA accumulates in
|
||||
# fp32 so the per-block max score is exact for the fp8 indexer too.
|
||||
kq = tl.dot(k, q, out_dtype=tl.float32) # [N,HQ]
|
||||
kq = tl.where(pos_mask & q_mask[None, :], kq, float("-inf"))
|
||||
score = tl.max(kq, axis=0) # [HQ]
|
||||
is_visible_block = blk < num_blocks_q
|
||||
@@ -709,16 +712,25 @@ def minimax_m3_index_topk(
|
||||
topk: int,
|
||||
init_blocks: int,
|
||||
local_blocks: int,
|
||||
out: torch.Tensor | None = None,
|
||||
) -> torch.Tensor:
|
||||
"""Select index top-k from a precomputed score tensor."""
|
||||
"""Select index top-k from a precomputed score tensor.
|
||||
|
||||
When ``out`` is provided (a ``[num_idx_heads, >=total_q, topk]`` buffer), the
|
||||
result is written into ``out[:, :total_q, :]`` instead of a fresh tensor --
|
||||
used to keep the top-k output at a stable address for cudagraph capture.
|
||||
"""
|
||||
num_idx_heads = score.shape[0]
|
||||
batch = cu_seqlens_q.shape[0] - 1
|
||||
total_q = score.shape[1]
|
||||
topk_idx = torch.empty(
|
||||
(num_idx_heads, total_q, topk),
|
||||
dtype=torch.int32,
|
||||
device=score.device,
|
||||
)
|
||||
if out is not None:
|
||||
topk_idx = out[:, :total_q, :]
|
||||
else:
|
||||
topk_idx = torch.empty(
|
||||
(num_idx_heads, total_q, topk),
|
||||
dtype=torch.int32,
|
||||
device=score.device,
|
||||
)
|
||||
# block_size_q == 1 -> query blocks coincide with query tokens.
|
||||
grid_topk = (max_query_len, batch, num_idx_heads)
|
||||
_topk_index_kernel[grid_topk](
|
||||
@@ -757,10 +769,13 @@ def minimax_m3_index_decode(
|
||||
num_kv_heads: int,
|
||||
decode_query_len: int,
|
||||
max_decode_query_len: int,
|
||||
out: torch.Tensor | None = None,
|
||||
) -> torch.Tensor:
|
||||
"""Decode index block-score + top-k, both split-K (cudagraph-safe).
|
||||
|
||||
Returns topk_idx [num_kv_heads, total_q, topk] (0-indexed block ids, -1 pad).
|
||||
When ``out`` ([num_kv_heads, >=total_q, topk]) is given, writes into
|
||||
``out[:, :total_q, :]`` (stable address for cudagraph) instead of allocating.
|
||||
"""
|
||||
total_q, num_idx_heads, head_dim = idx_q.shape
|
||||
assert num_idx_heads == num_kv_heads, (
|
||||
@@ -794,7 +809,7 @@ def minimax_m3_index_decode(
|
||||
)
|
||||
# split-K over seq blocks; chunk count depends only on shape constants so
|
||||
# the grid is fixed within a cuda graph.
|
||||
TARGET_GRID = 512
|
||||
TARGET_GRID = 4096
|
||||
MAX_NUM_KV_CHUNKS = 256
|
||||
# Use the configured max decode length to avoid Triton recompiles when
|
||||
# switching between qlen=1 and spec-decode verification batches.
|
||||
@@ -834,14 +849,17 @@ def minimax_m3_index_decode(
|
||||
**score_kwargs,
|
||||
)
|
||||
|
||||
topk_idx = torch.empty(
|
||||
(num_idx_heads, total_q, topk),
|
||||
dtype=torch.int32,
|
||||
device=idx_q.device,
|
||||
)
|
||||
if out is not None:
|
||||
topk_idx = out[:, :total_q, :]
|
||||
else:
|
||||
topk_idx = torch.empty(
|
||||
(num_idx_heads, total_q, topk),
|
||||
dtype=torch.int32,
|
||||
device=idx_q.device,
|
||||
)
|
||||
# Chunk count is shape-constant (cudagraph-safe), capped so the merge sorts
|
||||
# pow2(num_topk_chunks * pow2(topk)) candidates.
|
||||
TOPK_TARGET_GRID = 64
|
||||
TOPK_TARGET_GRID = 512
|
||||
MAX_NUM_TOPK_CHUNKS = 16
|
||||
topk_target = max(
|
||||
1, min(MAX_NUM_TOPK_CHUNKS, TOPK_TARGET_GRID // max(1, batch * num_idx_heads))
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""Main block-sparse GQA attention for MiniMax M3 sparse layers.
|
||||
|
||||
The lightning indexer (``indexer.py``) selects the top-k KV blocks; this module
|
||||
holds the main attention that attends only to those blocks: the paged K/V cache
|
||||
backend, its metadata + builder, and the impl that consumes the indexer's
|
||||
``topk_idx``. The Triton attend kernel lives here; the SM100 (MSA)
|
||||
The lightning indexer (``indexer.py``) selects the top-k KV blocks (written into
|
||||
the shared ``layer.topk_indices_buffer``); this module holds the main attention
|
||||
that attends only to those blocks: the paged K/V cache backend, its metadata +
|
||||
builder, and the impl that reads the indexer's top-k from that buffer. The Triton
|
||||
attend kernel lives here; the SM100 (MSA)
|
||||
``build_k2q_csr`` + ``sparse_atten_func`` attend lives in
|
||||
``nvidia/sparse_attention_msa.py``.
|
||||
|
||||
@@ -272,9 +273,10 @@ class MiniMaxM3SparseImpl(AttentionImplBase[MiniMaxM3SparseMetadata]):
|
||||
"""Abstract base for block-sparse GQA over the indexer-selected blocks.
|
||||
|
||||
Inherits ``AttentionImplBase`` for a custom forward signature (the layer
|
||||
pre-inserts K/V and runs the indexer, so forward takes the queries +
|
||||
``topk_idx``). The Triton and MSA subclasses each own a full ``forward`` --
|
||||
no shared forward code.
|
||||
pre-inserts K/V and runs the indexer, which writes the selected blocks into
|
||||
the shared ``layer.topk_indices_buffer``; the attend reads them back from
|
||||
there). The Triton and MSA subclasses each own a full ``forward`` -- no
|
||||
shared forward code.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
@@ -311,10 +313,14 @@ class MiniMaxM3SparseImpl(AttentionImplBase[MiniMaxM3SparseMetadata]):
|
||||
layer: AttentionLayer,
|
||||
query: torch.Tensor,
|
||||
kv_cache: torch.Tensor,
|
||||
topk_idx: tuple[torch.Tensor | None, torch.Tensor | None],
|
||||
output: torch.Tensor,
|
||||
) -> torch.Tensor:
|
||||
"""Attend the queries to the indexer-selected blocks. Per kernel."""
|
||||
"""Attend the queries to the indexer-selected blocks. Per kernel.
|
||||
|
||||
The indexer has already written the top-k block ids into
|
||||
``layer.topk_indices_buffer`` (decode at ``[:, :nd]``, prefill at
|
||||
``[:, nd:num_tokens]``); the attend reads them from there.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
@@ -326,7 +332,6 @@ class MiniMaxM3SparseTritonImpl(MiniMaxM3SparseImpl):
|
||||
layer: AttentionLayer,
|
||||
query: torch.Tensor,
|
||||
kv_cache: torch.Tensor,
|
||||
topk_idx: tuple[torch.Tensor | None, torch.Tensor | None],
|
||||
output: torch.Tensor,
|
||||
) -> torch.Tensor:
|
||||
attn_metadata = get_forward_context().attn_metadata
|
||||
@@ -334,10 +339,12 @@ class MiniMaxM3SparseTritonImpl(MiniMaxM3SparseImpl):
|
||||
return output # profiling run; caches unbound
|
||||
main_md = attn_metadata[layer.layer_name] # type: ignore[attr-defined]
|
||||
assert isinstance(main_md, MiniMaxM3SparseMetadata)
|
||||
decode_topk, prefill_topk = topk_idx
|
||||
|
||||
nd = main_md.num_decode_tokens
|
||||
num_tokens = main_md.num_actual_tokens
|
||||
# Indexer top-k from the shared buffer: decode [:, :nd], prefill [:, nd:].
|
||||
topk = layer.topk_indices_buffer # type: ignore[attr-defined]
|
||||
assert topk is not None
|
||||
hd = self.head_size
|
||||
q = query[:num_tokens].view(-1, self.num_heads, hd)
|
||||
out = output[:num_tokens].view(-1, self.num_heads, hd)
|
||||
@@ -348,11 +355,11 @@ class MiniMaxM3SparseTritonImpl(MiniMaxM3SparseImpl):
|
||||
# Decode [:nd]: split-K over the selected blocks (request-major chunks).
|
||||
if main_md.num_decodes > 0:
|
||||
d = main_md.decode
|
||||
assert d is not None and decode_topk is not None
|
||||
assert d is not None
|
||||
minimax_m3_sparse_attn_decode(
|
||||
q[:nd],
|
||||
kv_cache,
|
||||
decode_topk,
|
||||
topk[:, :nd, :],
|
||||
d.block_table,
|
||||
d.seq_lens,
|
||||
self.num_kv_heads,
|
||||
@@ -364,11 +371,11 @@ class MiniMaxM3SparseTritonImpl(MiniMaxM3SparseImpl):
|
||||
# Prefill [nd:]: cu_seqlens_q already rebased to 0.
|
||||
if main_md.num_prefills > 0:
|
||||
p = main_md.prefill
|
||||
assert p is not None and prefill_topk is not None
|
||||
assert p is not None
|
||||
minimax_m3_sparse_attn(
|
||||
q[nd:],
|
||||
kv_cache,
|
||||
prefill_topk,
|
||||
topk[:, nd:num_tokens, :],
|
||||
p.block_table,
|
||||
p.cu_seqlens_q,
|
||||
p.seq_lens,
|
||||
|
||||
@@ -0,0 +1,251 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""MSA (SM100/Blackwell) indexer impl for MiniMax M3.
|
||||
|
||||
Prefill scores with ``fmha_sm100``'s score-only (``OnlyScore``) path then selects
|
||||
top-k blocks with the Triton ``minimax_m3_index_topk`` kernel -- fmha is much
|
||||
faster than Triton for the wide prefill score (benchmarked ~3-5x).
|
||||
|
||||
Decode uses the Triton fused ``minimax_m3_index_decode`` (the same kernel the
|
||||
Triton indexer impl uses): for q_len==1 it is a purpose-built vector x matrix
|
||||
score (no wasted tensor-core tiles) with a 256-way split-K and a fused split-K
|
||||
top-k, which beats fmha's OnlyScore (wasted MMA on a single query, 64-split cap)
|
||||
by ~1.1-3.7x. It is cudagraph-safe by construction (shape-constant split grids)
|
||||
and writes the shared ``topk_indices_buffer`` via ``out=``.
|
||||
|
||||
``fmha_sm100`` imports are function-local so this module is import-safe on
|
||||
AMD / non-SM100.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import ClassVar
|
||||
|
||||
import torch
|
||||
|
||||
from vllm.forward_context import get_forward_context
|
||||
from vllm.models.minimax_m3.common.indexer import (
|
||||
MiniMaxM3IndexerBackend,
|
||||
MiniMaxM3IndexerDecodeMetadata,
|
||||
MiniMaxM3IndexerImpl,
|
||||
MiniMaxM3IndexerMetadata,
|
||||
MiniMaxM3IndexerMetadataBuilder,
|
||||
)
|
||||
from vllm.models.minimax_m3.common.ops.index_topk import (
|
||||
minimax_m3_index_decode,
|
||||
minimax_m3_index_topk,
|
||||
)
|
||||
from vllm.v1.attention.backend import (
|
||||
AttentionBackend,
|
||||
AttentionCGSupport,
|
||||
CommonAttentionMetadata,
|
||||
)
|
||||
from vllm.v1.attention.backends.utils import split_decodes_and_prefills
|
||||
|
||||
# Page size == sparse block size == index-K block; fmha tile id == M3 block id.
|
||||
PAGE_SIZE = 128
|
||||
|
||||
|
||||
class MiniMaxM3IndexerMSABackend(MiniMaxM3IndexerBackend):
|
||||
"""Indexer side-cache backend selecting the MSA builder."""
|
||||
|
||||
@staticmethod
|
||||
def get_builder_cls() -> type["MiniMaxM3IndexerMSAMetadataBuilder"]:
|
||||
return MiniMaxM3IndexerMSAMetadataBuilder
|
||||
|
||||
|
||||
@dataclass
|
||||
class MiniMaxM3IndexerMSAPrefillMetadata:
|
||||
"""fmha score plan + Triton top-k inputs for the prefill side (eager)."""
|
||||
|
||||
plan: dict # fmha_sm100 PlanInfo
|
||||
cu_seqlens_q: torch.Tensor # [num_prefills + 1] int32, rebased to 0
|
||||
prefix_lens: torch.Tensor # [num_prefills] int32, context tokens
|
||||
max_query_len: int
|
||||
page_table: torch.Tensor # flat physical page indices for the prefill side
|
||||
|
||||
|
||||
@dataclass
|
||||
class MiniMaxM3IndexerMSAMetadata(MiniMaxM3IndexerMetadata):
|
||||
"""Decode reuses the inherited base ``decode`` field (the Triton decode
|
||||
metadata); ``prefill_msa`` carries the fmha score plan for the prefill side
|
||||
(the base ``prefill`` field is unused on this path)."""
|
||||
|
||||
prefill_msa: MiniMaxM3IndexerMSAPrefillMetadata | None = None
|
||||
|
||||
|
||||
class MiniMaxM3IndexerMSAMetadataBuilder(MiniMaxM3IndexerMetadataBuilder):
|
||||
"""Decode metadata is the cudagraph-safe Triton decode metadata; the prefill
|
||||
fmha plan is built eagerly (prefill batches are not captured)."""
|
||||
|
||||
_cudagraph_support: ClassVar[AttentionCGSupport] = AttentionCGSupport.UNIFORM_BATCH
|
||||
|
||||
def build(
|
||||
self,
|
||||
common_prefix_len: int,
|
||||
common_attn_metadata: CommonAttentionMetadata,
|
||||
fast_build: bool = False,
|
||||
) -> MiniMaxM3IndexerMSAMetadata:
|
||||
num_reqs = common_attn_metadata.num_reqs
|
||||
num_tokens = common_attn_metadata.num_actual_tokens
|
||||
seq_lens = common_attn_metadata.seq_lens
|
||||
block_table = common_attn_metadata.block_table_tensor
|
||||
query_start_loc = common_attn_metadata.query_start_loc
|
||||
|
||||
num_decodes, num_prefills, num_decode_tokens, num_prefill_tokens = (
|
||||
split_decodes_and_prefills(
|
||||
common_attn_metadata,
|
||||
decode_threshold=self.reorder_batch_threshold,
|
||||
require_uniform=True,
|
||||
)
|
||||
)
|
||||
assert num_decodes + num_prefills == num_reqs
|
||||
assert num_decode_tokens + num_prefill_tokens == num_tokens
|
||||
|
||||
# Context (prefix) lengths into the stable cudagraph buffer.
|
||||
context_lens = self.context_len_buffer[:num_reqs]
|
||||
context_lens.copy_(
|
||||
common_attn_metadata.compute_num_computed_tokens(), non_blocking=True
|
||||
)
|
||||
|
||||
decode: MiniMaxM3IndexerDecodeMetadata | None = None
|
||||
if num_decodes > 0:
|
||||
qsl_cpu = common_attn_metadata.query_start_loc_cpu
|
||||
query_lens_cpu = qsl_cpu[1 : num_decodes + 1] - qsl_cpu[:num_decodes]
|
||||
decode_query_len = int(query_lens_cpu[0].item())
|
||||
assert decode_query_len > 0
|
||||
assert torch.all(
|
||||
(query_lens_cpu == decode_query_len) | (query_lens_cpu == 0)
|
||||
)
|
||||
decode = MiniMaxM3IndexerDecodeMetadata(
|
||||
seq_lens=seq_lens[:num_decodes],
|
||||
block_table=block_table[:num_decodes],
|
||||
max_seq_len=common_attn_metadata.max_seq_len,
|
||||
decode_query_len=decode_query_len,
|
||||
max_decode_query_len=self.max_decode_query_len,
|
||||
)
|
||||
|
||||
prefill: MiniMaxM3IndexerMSAPrefillMetadata | None = None
|
||||
if num_prefills > 0:
|
||||
# Prefill is eager (not captured); the host lengths it needs (and the
|
||||
# _fmha_sm100_plan .tolist() inside) make the D->H sync acceptable.
|
||||
from vllm.third_party.fmha_sm100.api import _fmha_sm100_plan
|
||||
|
||||
lo, hi = num_decodes, num_reqs
|
||||
qsl_cpu = common_attn_metadata.query_start_loc_cpu[: num_reqs + 1]
|
||||
qo_lens_cpu = (qsl_cpu[1:] - qsl_cpu[:-1]).to(torch.int32)
|
||||
kv_lens_cpu = seq_lens[:num_reqs].cpu().to(torch.int32)
|
||||
nvp = (kv_lens_cpu + PAGE_SIZE - 1) // PAGE_SIZE
|
||||
side_qo = qo_lens_cpu[lo:hi]
|
||||
side_kv = kv_lens_cpu[lo:hi]
|
||||
plan = _fmha_sm100_plan(
|
||||
side_qo,
|
||||
side_kv,
|
||||
self.num_index_heads,
|
||||
num_kv_heads=1,
|
||||
qo_offset=side_kv - side_qo, # bottom-right causal
|
||||
page_size=PAGE_SIZE,
|
||||
output_maxscore=True,
|
||||
causal=True,
|
||||
num_kv_splits=1,
|
||||
)
|
||||
cols = torch.arange(block_table.shape[1], device=block_table.device)
|
||||
valid = cols[None, :] < nvp[lo:hi].to(block_table.device)[:, None]
|
||||
prefill = MiniMaxM3IndexerMSAPrefillMetadata(
|
||||
plan=plan,
|
||||
cu_seqlens_q=(query_start_loc[lo : hi + 1] - query_start_loc[lo]).to(
|
||||
torch.int32
|
||||
),
|
||||
prefix_lens=context_lens[lo:hi],
|
||||
max_query_len=int(side_qo.max()),
|
||||
page_table=block_table[lo:hi][valid].to(torch.int32),
|
||||
)
|
||||
|
||||
return MiniMaxM3IndexerMSAMetadata(
|
||||
seq_lens=seq_lens,
|
||||
max_seq_len=common_attn_metadata.max_seq_len,
|
||||
slot_mapping=common_attn_metadata.slot_mapping,
|
||||
num_actual_tokens=num_tokens,
|
||||
num_decodes=num_decodes,
|
||||
num_decode_tokens=num_decode_tokens,
|
||||
num_prefills=num_prefills,
|
||||
num_prefill_tokens=num_prefill_tokens,
|
||||
decode=decode,
|
||||
prefill_msa=prefill,
|
||||
)
|
||||
|
||||
|
||||
class MiniMaxM3IndexerMSAImpl(MiniMaxM3IndexerImpl):
|
||||
"""Decode: Triton fused score+top-k. Prefill: fmha_sm100 OnlyScore + top-k."""
|
||||
|
||||
indexer_backend_cls: ClassVar[type[AttentionBackend]] = MiniMaxM3IndexerMSABackend
|
||||
|
||||
def forward(
|
||||
self,
|
||||
index_query: torch.Tensor,
|
||||
) -> tuple[torch.Tensor | None, torch.Tensor | None]:
|
||||
attn_metadata = get_forward_context().attn_metadata
|
||||
if not isinstance(attn_metadata, dict):
|
||||
return None, None # profiling run; caches unbound
|
||||
md = attn_metadata[self.index_cache.prefix]
|
||||
assert isinstance(md, MiniMaxM3IndexerMSAMetadata)
|
||||
|
||||
num_tokens = md.num_actual_tokens
|
||||
nd = md.num_decode_tokens
|
||||
index_q = index_query[:num_tokens].view(
|
||||
-1, self.num_index_heads, self.index_head_dim
|
||||
)
|
||||
kv = self.index_cache.kv_cache
|
||||
# Both sides write into the single shared persistent topk_indices_buffer:
|
||||
# decode at [:, :nd], prefill at [:, nd:] (each kernel writes [:, :total_q]).
|
||||
buf = self.topk_indices_buffer
|
||||
|
||||
decode_topk: torch.Tensor | None = None
|
||||
if md.decode is not None:
|
||||
d = md.decode
|
||||
decode_topk = minimax_m3_index_decode(
|
||||
index_q[:nd],
|
||||
kv,
|
||||
d.block_table,
|
||||
d.seq_lens,
|
||||
d.max_seq_len,
|
||||
self.topk_blocks,
|
||||
self.init_blocks,
|
||||
self.local_blocks,
|
||||
self.num_kv_heads,
|
||||
d.decode_query_len,
|
||||
d.max_decode_query_len,
|
||||
out=buf,
|
||||
)
|
||||
|
||||
prefill_topk: torch.Tensor | None = None
|
||||
if md.prefill_msa is not None:
|
||||
from vllm.third_party.fmha_sm100.api import _fmha_sm100
|
||||
|
||||
p = md.prefill_msa
|
||||
# Index-K cache (num_blocks, 128, D) -> paged MQA (num_blocks,1,128,D).
|
||||
k_pages = kv.view(kv.shape[0], 1, PAGE_SIZE, self.index_head_dim)
|
||||
_, max_score = _fmha_sm100(
|
||||
index_q[nd:],
|
||||
k_pages,
|
||||
k_pages, # V placeholder; not read in OnlyScore
|
||||
p.plan,
|
||||
kv_indices=p.page_table,
|
||||
output_o=False,
|
||||
output_maxscore=True,
|
||||
sm_scale=self.scale,
|
||||
)
|
||||
# Triton top-k wants [num_index_heads, num_tokens, max_block]; the
|
||||
# transpose is a strided view (the kernel reads via strides).
|
||||
out = buf[:, nd:, :] if buf is not None else None
|
||||
prefill_topk = minimax_m3_index_topk(
|
||||
max_score.transpose(1, 2),
|
||||
p.cu_seqlens_q,
|
||||
p.prefix_lens,
|
||||
p.max_query_len,
|
||||
self.topk_blocks,
|
||||
self.init_blocks,
|
||||
self.local_blocks,
|
||||
out=out,
|
||||
)
|
||||
|
||||
return decode_topk, prefill_topk
|
||||
@@ -193,6 +193,7 @@ class MiniMaxM3MoE(nn.Module):
|
||||
layer_id: int,
|
||||
quant_config: QuantizationConfig | None = None,
|
||||
prefix: str = "",
|
||||
reduce_results: bool = True,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.tp_size = get_tensor_model_parallel_world_size()
|
||||
@@ -259,6 +260,7 @@ class MiniMaxM3MoE(nn.Module):
|
||||
shared_experts=self.shared_experts,
|
||||
quant_config=quant_config,
|
||||
prefix=f"{prefix}.experts",
|
||||
reduce_results=reduce_results,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
@@ -402,6 +404,7 @@ class MiniMaxM3SparseAttention(nn.Module, AttentionLayerBase):
|
||||
quant_config: QuantizationConfig | None = None,
|
||||
prefix: str = "",
|
||||
cache_config: CacheConfig | None = None,
|
||||
topk_indices_buffer: torch.Tensor | None = None,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.hidden_size = config.hidden_size
|
||||
@@ -489,6 +492,10 @@ class MiniMaxM3SparseAttention(nn.Module, AttentionLayerBase):
|
||||
# cache (--attention-config '{"indexer_kv_dtype": ...}').
|
||||
self.indexer_kv_dtype = vllm_config.attention_config.indexer_kv_dtype
|
||||
|
||||
# Shared top-k buffer: the indexer writes the selected blocks into it and
|
||||
# the attend impl reads them back (so nothing crosses the eager break as a
|
||||
# Python value, which would freeze at capture).
|
||||
self.topk_indices_buffer = topk_indices_buffer
|
||||
self.attn_backend = MiniMaxM3SparseBackend
|
||||
# Indexer (top-k selection) and main attention are separate impls, each
|
||||
# picking Triton vs MSA off its cache dtype. impl is AttentionImplBase
|
||||
@@ -519,6 +526,7 @@ class MiniMaxM3SparseAttention(nn.Module, AttentionLayerBase):
|
||||
score_type=sparse_cfg.get("sparse_score_type", "max"),
|
||||
cache_config=cache_config,
|
||||
indexer_kv_dtype=self.indexer_kv_dtype,
|
||||
topk_indices_buffer=topk_indices_buffer,
|
||||
)
|
||||
|
||||
# Register the main K/V cache so the KV-cache manager allocates it.
|
||||
@@ -576,7 +584,12 @@ class MiniMaxM3SparseAttention(nn.Module, AttentionLayerBase):
|
||||
main_slot_mapping = fwd_slot_mapping[self.layer_name]
|
||||
index_slot_mapping = fwd_slot_mapping[self.indexer.index_cache.prefix]
|
||||
q = qkv.new_empty((num_tokens, self.q_size))
|
||||
index_q = qkv.new_empty((num_tokens, self.index_q_size))
|
||||
# index_q matches the index-K cache dtype (e4m3 for the fp8 score path);
|
||||
# the fused kernel emits fp8 directly when this buffer is e4m3.
|
||||
index_q = qkv.new_empty(
|
||||
(num_tokens, self.index_q_size),
|
||||
dtype=self.indexer.index_cache.dtype,
|
||||
)
|
||||
ops.fused_minimax_m3_qknorm_rope_kv_insert(
|
||||
qkv,
|
||||
self.q_norm.weight,
|
||||
@@ -613,9 +626,10 @@ class MiniMaxM3SparseAttention(nn.Module, AttentionLayerBase):
|
||||
output: torch.Tensor,
|
||||
) -> torch.Tensor:
|
||||
# Single eager break around both: their split-K kernels read per-request
|
||||
# metadata and can't be captured into a cudagraph.
|
||||
topk_idx = self.indexer(index_query)
|
||||
return self.impl.forward(self, query, self.kv_cache, topk_idx, output)
|
||||
# metadata and can't be captured into a cudagraph. The indexer writes its
|
||||
# top-k into the shared ``topk_indices_buffer``; the attend reads it back.
|
||||
self.indexer(index_query)
|
||||
return self.impl.forward(self, query, self.kv_cache, output)
|
||||
|
||||
|
||||
class MiniMaxM3DecoderLayer(nn.Module):
|
||||
@@ -627,6 +641,7 @@ class MiniMaxM3DecoderLayer(nn.Module):
|
||||
force_sparse_attn: bool = False,
|
||||
force_moe: bool = False,
|
||||
is_mtp_block: bool = False,
|
||||
topk_indices_buffer: torch.Tensor | None = None,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
if is_mtp_block:
|
||||
@@ -642,13 +657,12 @@ class MiniMaxM3DecoderLayer(nn.Module):
|
||||
layer_id = int(prefix.split(sep=".")[-1])
|
||||
self.layer_id = layer_id
|
||||
|
||||
# Complete the preceding dense MLP's deferred all-reduce
|
||||
# (reduce_results=False), fused into this layer's input_layernorm.
|
||||
# Disable this fusion when PP is set
|
||||
# Complete the preceding FFN's deferred all-reduce (its down_proj / MoE
|
||||
# combine ran with reduce_results=False), fused into this layer's
|
||||
# input_layernorm. Both dense and MoE FFNs defer under PP==1, so every
|
||||
# non-first layer fuses; disable when PP>1 (FFNs reduce themselves).
|
||||
self.fuse_input_allreduce = (
|
||||
layer_id > 0
|
||||
and not _is_moe_layer(config, layer_id - 1)
|
||||
and vllm_config.parallel_config.pipeline_parallel_size == 1
|
||||
layer_id > 0 and vllm_config.parallel_config.pipeline_parallel_size == 1
|
||||
)
|
||||
|
||||
is_sparse_attention_layer = (
|
||||
@@ -662,6 +676,7 @@ class MiniMaxM3DecoderLayer(nn.Module):
|
||||
quant_config=quant_config,
|
||||
prefix=f"{prefix}.self_attn",
|
||||
cache_config=cache_config,
|
||||
topk_indices_buffer=topk_indices_buffer,
|
||||
)
|
||||
else:
|
||||
self.self_attn = MiniMaxM3Attention(
|
||||
@@ -681,6 +696,12 @@ class MiniMaxM3DecoderLayer(nn.Module):
|
||||
layer_id=layer_id,
|
||||
quant_config=quant_config,
|
||||
prefix=f"{prefix}.block_sparse_moe",
|
||||
# Defer the MoE all-reduce only when it can be fused into a
|
||||
# following GemmaRMSNorm
|
||||
reduce_results=(
|
||||
vllm_config.parallel_config.pipeline_parallel_size > 1
|
||||
or is_mtp_block
|
||||
),
|
||||
)
|
||||
else:
|
||||
self.mlp = MiniMaxM3MLP(
|
||||
@@ -747,17 +768,43 @@ class MiniMaxM3Model(nn.Module, EagleModelMixin):
|
||||
prefix=f"{prefix}.embed_tokens",
|
||||
)
|
||||
|
||||
# Reserved top-k indices buffer shared by all sparse-attention indexer
|
||||
# layers (mirrors DeepseekV4); kept at a stable address so the indexer's
|
||||
# top-k output survives cudagraph capture/replay. Shape matches the
|
||||
# per-head index top-k output [num_index_heads, total_q, topk].
|
||||
sparse_cfg = getattr(config, "sparse_attention_config", None)
|
||||
if sparse_cfg is not None:
|
||||
tp_size = get_tensor_model_parallel_world_size()
|
||||
num_index_heads = max(1, sparse_cfg["sparse_num_index_heads"] // tp_size)
|
||||
max_num_batched_tokens = vllm_config.scheduler_config.max_num_batched_tokens
|
||||
padded_num_tokens = (max_num_batched_tokens + 3) // 4 * 4
|
||||
self.topk_indices_buffer = torch.empty(
|
||||
num_index_heads,
|
||||
padded_num_tokens,
|
||||
sparse_cfg["sparse_topk_blocks"],
|
||||
dtype=torch.int32,
|
||||
)
|
||||
else:
|
||||
self.topk_indices_buffer = None
|
||||
|
||||
self.start_layer, self.end_layer, self.layers = make_layers(
|
||||
config.num_hidden_layers,
|
||||
lambda prefix: MiniMaxM3DecoderLayer(
|
||||
vllm_config=vllm_config,
|
||||
prefix=prefix,
|
||||
topk_indices_buffer=self.topk_indices_buffer,
|
||||
),
|
||||
prefix=f"{prefix}.layers",
|
||||
)
|
||||
|
||||
self.norm = MiniMAXGemmaRMSNorm(config.hidden_size, eps=config.rms_norm_eps)
|
||||
|
||||
# The final decoder layer has no next layer, so its deferred all-reduce is
|
||||
# completed here in the model norm.
|
||||
self.fuse_final_allreduce = (
|
||||
vllm_config.parallel_config.pipeline_parallel_size == 1
|
||||
)
|
||||
|
||||
def embed_input_ids(self, input_ids: torch.Tensor) -> torch.Tensor:
|
||||
return self.embed_tokens(input_ids)
|
||||
|
||||
@@ -781,7 +828,12 @@ class MiniMaxM3Model(nn.Module, EagleModelMixin):
|
||||
aux_hidden_states, idx + 1, hidden_states, residual
|
||||
)
|
||||
|
||||
hidden_states, _ = self.norm(hidden_states, residual)
|
||||
if self.fuse_final_allreduce and residual is not None:
|
||||
hidden_states, _ = fused_allreduce_gemma_rms_norm(
|
||||
hidden_states, residual, self.norm
|
||||
)
|
||||
else:
|
||||
hidden_states, _ = self.norm(hidden_states, residual)
|
||||
|
||||
if len(aux_hidden_states) > 0:
|
||||
return hidden_states, aux_hidden_states
|
||||
|
||||
@@ -29,7 +29,6 @@ class MiniMaxM3SparseMSAImpl(MiniMaxM3SparseImpl):
|
||||
layer: AttentionLayer,
|
||||
query: torch.Tensor,
|
||||
kv_cache: torch.Tensor,
|
||||
topk_idx: tuple[torch.Tensor | None, torch.Tensor | None],
|
||||
output: torch.Tensor,
|
||||
) -> torch.Tensor:
|
||||
attn_metadata = get_forward_context().attn_metadata
|
||||
@@ -37,10 +36,12 @@ class MiniMaxM3SparseMSAImpl(MiniMaxM3SparseImpl):
|
||||
return output # profiling run; caches unbound
|
||||
main_md = attn_metadata[layer.layer_name] # type: ignore[attr-defined]
|
||||
assert isinstance(main_md, MiniMaxM3SparseMetadata)
|
||||
decode_topk, prefill_topk = topk_idx
|
||||
|
||||
nd = main_md.num_decode_tokens
|
||||
num_tokens = main_md.num_actual_tokens
|
||||
# Indexer top-k from the shared buffer: decode [:, :nd], prefill [:, nd:].
|
||||
topk = layer.topk_indices_buffer # type: ignore[attr-defined]
|
||||
assert topk is not None
|
||||
hd = self.head_size
|
||||
q = query[:num_tokens].view(-1, self.num_heads, hd)
|
||||
out = output[:num_tokens].view(-1, self.num_heads, hd)
|
||||
@@ -51,11 +52,11 @@ class MiniMaxM3SparseMSAImpl(MiniMaxM3SparseImpl):
|
||||
# Decode [:nd]: Triton split-K placeholder (no MSA decode yet).
|
||||
if main_md.num_decodes > 0:
|
||||
d = main_md.decode
|
||||
assert d is not None and decode_topk is not None
|
||||
assert d is not None
|
||||
minimax_m3_sparse_attn_decode(
|
||||
q[:nd],
|
||||
kv_cache,
|
||||
decode_topk,
|
||||
topk[:, :nd, :],
|
||||
d.block_table,
|
||||
d.seq_lens,
|
||||
self.num_kv_heads,
|
||||
@@ -72,7 +73,9 @@ class MiniMaxM3SparseMSAImpl(MiniMaxM3SparseImpl):
|
||||
)
|
||||
|
||||
p = main_md.prefill
|
||||
assert p is not None and prefill_topk is not None
|
||||
assert p is not None
|
||||
# build_k2q_csr() doesn't support strided topk buffer
|
||||
prefill_topk = topk[:, nd:num_tokens, :]
|
||||
qp = q[nd:]
|
||||
k_cache = kv_cache[:, 0].transpose(1, 2)
|
||||
v_cache = kv_cache[:, 1].transpose(1, 2)
|
||||
|
||||
@@ -794,11 +794,10 @@ class DelegatingParser(Parser):
|
||||
reasoning_transitioned = True
|
||||
current_token_ids = self.extract_content_ids(delta_token_ids)
|
||||
if self._engine_based:
|
||||
flush_delta = reasoning_parser.finish_streaming() # type: ignore[union-attr, attr-defined]
|
||||
current_text = (
|
||||
self.model_tokenizer.decode(current_token_ids)
|
||||
if current_token_ids
|
||||
else ""
|
||||
)
|
||||
(delta_message.content if delta_message else None) or ""
|
||||
) + ((flush_delta.content if flush_delta else None) or "")
|
||||
if delta_message and self._tool_parser is not None:
|
||||
delta_message.content = None
|
||||
else:
|
||||
|
||||
@@ -172,7 +172,9 @@ class ParserEngine(Parser):
|
||||
|
||||
def finish_streaming(self) -> DeltaMessage | None:
|
||||
events = self._engine.finish()
|
||||
return self._events_to_delta(events) if events else None
|
||||
if events or self._deferred_content:
|
||||
return self._events_to_delta(events, finished=True)
|
||||
return None
|
||||
|
||||
def _reset(self, initial_state: ParserState | None = None) -> None:
|
||||
self._engine.reset(initial_state=initial_state)
|
||||
|
||||
+30
-1
@@ -19,7 +19,6 @@ from torch.distributed.distributed_c10d import is_nccl_available
|
||||
from typing_extensions import ParamSpec
|
||||
|
||||
# import custom ops, trigger op registration
|
||||
import vllm._C # noqa
|
||||
import vllm._C_stable_libtorch # noqa
|
||||
import vllm.envs as envs
|
||||
from vllm.logger import init_logger
|
||||
@@ -40,6 +39,11 @@ else:
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
try:
|
||||
import vllm._qutlass_C # noqa: F401
|
||||
except ImportError as e:
|
||||
logger.warning("Failed to import from vllm._qutlass_C: %r", e)
|
||||
|
||||
_P = ParamSpec("_P")
|
||||
_R = TypeVar("_R")
|
||||
|
||||
@@ -187,6 +191,22 @@ class CudaPlatformBase(Platform):
|
||||
"RAY_EXPERIMENTAL_NOSET_CUDA_VISIBLE_DEVICES",
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def import_kernels(cls) -> None:
|
||||
"""Import CUDA kernel extensions (_C_stable_libtorch, optional _qutlass_C)."""
|
||||
try:
|
||||
import vllm._C_stable_libtorch # noqa: F401
|
||||
except ImportError as e:
|
||||
logger.warning("Failed to import from vllm._C_stable_libtorch: %r", e)
|
||||
try:
|
||||
import vllm._moe_C_stable_libtorch # noqa: F401
|
||||
except ImportError as e:
|
||||
logger.warning("Failed to import from vllm._moe_C_stable_libtorch: %r", e)
|
||||
try:
|
||||
import vllm._qutlass_C # noqa: F401
|
||||
except ImportError as e:
|
||||
logger.warning("Failed to import from vllm._qutlass_C: %r", e)
|
||||
|
||||
@property
|
||||
def supported_dtypes(self) -> list[torch.dtype]:
|
||||
if self.has_device_capability(80):
|
||||
@@ -665,6 +685,15 @@ class CudaPlatformBase(Platform):
|
||||
# all the related functions work on real physical device ids.
|
||||
# the major benefit of using NVML is that it will not initialize CUDA
|
||||
class NvmlCudaPlatform(CudaPlatformBase):
|
||||
@classmethod
|
||||
@with_nvml_context
|
||||
def device_control_id_to_physical_device_id(cls, device_id: str) -> int:
|
||||
try:
|
||||
return int(device_id)
|
||||
except ValueError:
|
||||
handle = pynvml.nvmlDeviceGetHandleByUUID(device_id)
|
||||
return pynvml.nvmlDeviceGetIndex(handle)
|
||||
|
||||
@classmethod
|
||||
@cache
|
||||
@with_nvml_context
|
||||
|
||||
+102
-1
@@ -30,6 +30,33 @@ else:
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
_assigned_physical_gpu_ids: list[int] | None = None
|
||||
|
||||
|
||||
def set_assigned_physical_gpu_ids(ids: list[int]) -> None:
|
||||
"""Set the physical GPU IDs assigned to this worker process.
|
||||
Called during worker init so that device_id_to_physical_device_id()
|
||||
can map local_rank to the correct physical device without relying
|
||||
on CUDA_VISIBLE_DEVICES.
|
||||
|
||||
Idempotent: a second call with the same value is a no-op.
|
||||
Raises RuntimeError if called again with a different value.
|
||||
|
||||
This is expected to run during single-threaded worker initialization."""
|
||||
global _assigned_physical_gpu_ids
|
||||
if _assigned_physical_gpu_ids is not None:
|
||||
if _assigned_physical_gpu_ids != ids:
|
||||
raise RuntimeError(
|
||||
f"set_assigned_physical_gpu_ids called with conflicting values: "
|
||||
f"existing={_assigned_physical_gpu_ids}, new={ids}"
|
||||
)
|
||||
return
|
||||
_assigned_physical_gpu_ids = ids
|
||||
|
||||
|
||||
def get_assigned_physical_gpu_ids() -> list[int] | None:
|
||||
return _assigned_physical_gpu_ids
|
||||
|
||||
|
||||
@functools.cache
|
||||
def in_wsl() -> bool:
|
||||
@@ -233,8 +260,34 @@ class Platform:
|
||||
"""
|
||||
import vllm.kernels # noqa: F401
|
||||
|
||||
@classmethod
|
||||
def device_control_id_to_physical_device_id(cls, device_id: str) -> int:
|
||||
"""Map one device-control env entry to an integer physical device ID."""
|
||||
try:
|
||||
return int(device_id)
|
||||
except ValueError as e:
|
||||
raise ValueError(
|
||||
f"Non-integer device ID {device_id!r} is not supported by "
|
||||
f"{cls.device_name}."
|
||||
) from e
|
||||
|
||||
@classmethod
|
||||
def device_id_to_physical_device_id(cls, device_id: int):
|
||||
"""Map a vLLM-local logical device ID to a physical device ID.
|
||||
|
||||
The input is a logical local ID (e.g. a local rank), NOT a visible
|
||||
device ordinal; for the latter use
|
||||
visible_device_id_to_physical_device_id(). The two coincide only
|
||||
when no logical-to-physical mapping is in effect.
|
||||
"""
|
||||
if _assigned_physical_gpu_ids is not None:
|
||||
if device_id >= len(_assigned_physical_gpu_ids):
|
||||
raise IndexError(
|
||||
f"device_id {device_id} is out of range for "
|
||||
f"assigned_physical_gpu_ids {_assigned_physical_gpu_ids} "
|
||||
f"({len(_assigned_physical_gpu_ids)} devices assigned)"
|
||||
)
|
||||
return _assigned_physical_gpu_ids[device_id]
|
||||
# Treat empty device control env var as unset. This is a valid
|
||||
# configuration in Ray setups where the engine is launched in
|
||||
# a CPU-only placement group located on a GPU node.
|
||||
@@ -244,10 +297,58 @@ class Platform:
|
||||
):
|
||||
device_ids = os.environ[cls.device_control_env_var].split(",")
|
||||
physical_device_id = device_ids[device_id]
|
||||
return int(physical_device_id)
|
||||
return cls.device_control_id_to_physical_device_id(physical_device_id)
|
||||
else:
|
||||
return device_id
|
||||
|
||||
@classmethod
|
||||
def logical_device_id_to_visible_device_id(cls, device_id: int) -> int:
|
||||
"""Map a vLLM-local logical device ID to the current process's
|
||||
visible accelerator ordinal.
|
||||
|
||||
vLLM internals use logical local IDs. Physical IDs are used only
|
||||
at platform/topology boundaries. This helper performs the final
|
||||
translation needed by APIs such as ``torch.device("cuda:N")``.
|
||||
"""
|
||||
physical_device_id = cls.device_id_to_physical_device_id(device_id)
|
||||
device_control_env = os.environ.get(cls.device_control_env_var, "")
|
||||
if not device_control_env:
|
||||
return physical_device_id
|
||||
|
||||
visible_physical_device_ids = [
|
||||
cls.device_control_id_to_physical_device_id(physical_id)
|
||||
for physical_id in device_control_env.split(",")
|
||||
]
|
||||
if physical_device_id not in visible_physical_device_ids:
|
||||
raise RuntimeError(
|
||||
f"Physical device {physical_device_id} for logical device "
|
||||
f"{device_id} is not visible in {cls.device_control_env_var}="
|
||||
f"{device_control_env}"
|
||||
)
|
||||
return visible_physical_device_ids.index(physical_device_id)
|
||||
|
||||
@classmethod
|
||||
def visible_device_id_to_physical_device_id(cls, device_id: int) -> int:
|
||||
"""Map a visible accelerator ordinal (e.g. ``torch.device.index``)
|
||||
to a physical device ID.
|
||||
|
||||
This is the inverse of the env-var translation performed by
|
||||
logical_device_id_to_visible_device_id() and is independent of any
|
||||
logical-to-physical mapping set via set_assigned_physical_gpu_ids().
|
||||
"""
|
||||
device_control_env = os.environ.get(cls.device_control_env_var, "")
|
||||
if not device_control_env:
|
||||
return device_id
|
||||
visible_device_ids = device_control_env.split(",")
|
||||
if device_id >= len(visible_device_ids):
|
||||
raise IndexError(
|
||||
f"visible device ordinal {device_id} is out of range for "
|
||||
f"{cls.device_control_env_var}={device_control_env}"
|
||||
)
|
||||
return cls.device_control_id_to_physical_device_id(
|
||||
visible_device_ids[device_id]
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def import_kernels(cls) -> None:
|
||||
"""Import any platform-specific C kernels."""
|
||||
|
||||
@@ -1236,10 +1236,21 @@ def _get_kv_cache_config_deepseek_v4(
|
||||
num_blocks = available_memory // total_num_bytes_per_block
|
||||
num_blocks = may_override_num_blocks(vllm_config, num_blocks)
|
||||
|
||||
total_size = total_num_bytes_per_block * num_blocks
|
||||
|
||||
kv_cache_tensors: list[KVCacheTensor] = []
|
||||
byte_offset = 0
|
||||
for ps, slots in buckets.items():
|
||||
for slot in slots:
|
||||
kv_cache_tensors.append(KVCacheTensor(size=ps * num_blocks, shared_by=slot))
|
||||
kv_cache_tensors.append(
|
||||
KVCacheTensor(
|
||||
size=total_size,
|
||||
shared_by=slot,
|
||||
offset=byte_offset,
|
||||
block_stride=total_num_bytes_per_block,
|
||||
)
|
||||
)
|
||||
byte_offset += ps
|
||||
|
||||
return num_blocks, kv_cache_tensors
|
||||
|
||||
|
||||
@@ -1109,9 +1109,3 @@ class AsyncLLM(EngineClient):
|
||||
async def finish_weight_update(self) -> None:
|
||||
"""Finish the current weight update."""
|
||||
await self.collective_rpc("finish_weight_update")
|
||||
# Invalidate cached state computed with the old weights so it isn't
|
||||
# reused for subsequent requests:
|
||||
# - prefix cache: KV blocks computed with the old weights
|
||||
# - encoder cache: multimodal embeddings keyed only by mm_hash
|
||||
await self.reset_prefix_cache()
|
||||
await self.reset_encoder_cache()
|
||||
|
||||
+16
-9
@@ -74,7 +74,7 @@ from vllm.v1.engine.utils import (
|
||||
EngineHandshakeMetadata,
|
||||
EngineZmqAddresses,
|
||||
SignalCallback,
|
||||
get_device_indices,
|
||||
get_physical_gpu_ids_for_local_dp_rank,
|
||||
)
|
||||
from vllm.v1.executor import Executor
|
||||
from vllm.v1.kv_cache_interface import KVCacheConfig, get_kv_cache_spec_kind
|
||||
@@ -2175,23 +2175,30 @@ class EngineCoreActorMixin:
|
||||
pass
|
||||
else:
|
||||
device_control_env_var = current_platform.device_control_env_var
|
||||
self._set_cuda_visible_devices(
|
||||
self._set_assigned_physical_gpu_ids(
|
||||
vllm_config, local_dp_rank, device_control_env_var
|
||||
)
|
||||
|
||||
def _set_cuda_visible_devices(
|
||||
self, vllm_config: VllmConfig, local_dp_rank: int, device_control_env_var: str
|
||||
def _set_assigned_physical_gpu_ids(
|
||||
self,
|
||||
vllm_config: VllmConfig,
|
||||
local_dp_rank: int,
|
||||
device_control_env_var: str,
|
||||
):
|
||||
world_size = vllm_config.parallel_config.world_size
|
||||
# Set CUDA_VISIBLE_DEVICES or equivalent.
|
||||
try:
|
||||
value = get_device_indices(
|
||||
device_control_env_var, local_dp_rank, world_size
|
||||
physical_gpu_ids = get_physical_gpu_ids_for_local_dp_rank(
|
||||
device_control_env_var,
|
||||
local_dp_rank,
|
||||
world_size,
|
||||
user_assigned_gpu_ids=(
|
||||
vllm_config.parallel_config.assigned_physical_gpu_ids
|
||||
),
|
||||
)
|
||||
os.environ[device_control_env_var] = value
|
||||
vllm_config.parallel_config.assigned_physical_gpu_ids = physical_gpu_ids
|
||||
except IndexError as e:
|
||||
raise Exception(
|
||||
f"Error setting {device_control_env_var}: "
|
||||
f"Error computing assigned_physical_gpu_ids: "
|
||||
f"local range: [{local_dp_rank * world_size}, "
|
||||
f"{(local_dp_rank + 1) * world_size}) "
|
||||
f'base value: "{os.getenv(device_control_env_var)}"'
|
||||
|
||||
+66
-43
@@ -12,7 +12,6 @@ from multiprocessing import Process, connection
|
||||
from multiprocessing.process import BaseProcess
|
||||
from multiprocessing.queues import Queue
|
||||
from typing import TYPE_CHECKING, cast
|
||||
from unittest.mock import patch
|
||||
|
||||
import msgspec
|
||||
import zmq
|
||||
@@ -175,38 +174,38 @@ class CoreEngineProcManager:
|
||||
self.manager_stopped = threading.Event()
|
||||
self.failed_proc_name: str | None = None
|
||||
|
||||
# All ranks share this config object: capture the user-provided
|
||||
# --device-ids list before the per-rank shard overwrites it. Mutating
|
||||
# the config before each proc.start() works because the spawn method
|
||||
# pickles process args at start() time, sequentially per rank.
|
||||
user_assigned_gpu_ids = vllm_config.parallel_config.assigned_physical_gpu_ids
|
||||
try:
|
||||
for proc, local_dp_rank in zip(self.processes, local_dp_ranks):
|
||||
# Adjust device control in DP for platforms that cannot rely
|
||||
# on torch.accelerator.set_device_index(), and for Ray launchers.
|
||||
device_control_context: contextlib.AbstractContextManager[None] = (
|
||||
contextlib.nullcontext()
|
||||
)
|
||||
# Populate the logical-to-physical GPU mapping in DP for
|
||||
# platforms that cannot rely on
|
||||
# torch.accelerator.set_device_index(), and for Ray.
|
||||
needs_device_env_isolation = not (
|
||||
current_platform.is_cuda_alike() or current_platform.is_xpu()
|
||||
)
|
||||
if is_dp and (
|
||||
needs_device_env_isolation or vllm_config.parallel_config.use_ray
|
||||
):
|
||||
device_control_context = set_device_control_env_var(
|
||||
vllm_config, local_dp_rank
|
||||
set_assigned_physical_gpu_ids_for_dp_rank(
|
||||
vllm_config, local_dp_rank, user_assigned_gpu_ids
|
||||
)
|
||||
|
||||
with (
|
||||
device_control_context,
|
||||
numa_utils.configure_subprocess(
|
||||
# EngineCore itself does not have a TP/PP-local rank.
|
||||
# When DP is enabled, set_device_control_env_var()
|
||||
# narrows visible devices to this DP shard first, so
|
||||
# local_rank=0 means "the first local GPU in this
|
||||
# shard". The actual TP/PP worker processes spawned by
|
||||
# the executor are bound separately with their own
|
||||
# local_rank values.
|
||||
vllm_config,
|
||||
local_rank=0,
|
||||
dp_local_rank=local_dp_rank,
|
||||
process_kind="EngineCore",
|
||||
),
|
||||
with numa_utils.configure_subprocess(
|
||||
# EngineCore itself does not have a TP/PP-local rank.
|
||||
# When DP is enabled, set_assigned_physical_gpu_ids_for_dp_rank()
|
||||
# populates the logical-to-physical mapping for this DP
|
||||
# shard, so local_rank=0 means "the first local GPU in
|
||||
# this shard". The actual TP/PP worker processes spawned
|
||||
# by the executor are bound separately with their own
|
||||
# local_rank values.
|
||||
vllm_config,
|
||||
local_rank=0,
|
||||
dp_local_rank=local_dp_rank,
|
||||
process_kind="EngineCore",
|
||||
):
|
||||
proc.start()
|
||||
finally:
|
||||
@@ -281,55 +280,79 @@ class SignalCallback:
|
||||
self._event.set()
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def set_device_control_env_var(
|
||||
vllm_config: VllmConfig, local_dp_rank: int
|
||||
) -> Iterator[None]:
|
||||
def set_assigned_physical_gpu_ids_for_dp_rank(
|
||||
vllm_config: VllmConfig,
|
||||
local_dp_rank: int,
|
||||
user_assigned_gpu_ids: list[int] | None = None,
|
||||
) -> None:
|
||||
"""
|
||||
Temporarily set CUDA_VISIBLE_DEVICES or equivalent
|
||||
for engine subprocess.
|
||||
Populate assigned_physical_gpu_ids on the config for the given DP rank.
|
||||
|
||||
user_assigned_gpu_ids is the full (un-sharded) --device-ids list, if the
|
||||
user provided one; this DP rank's shard is sliced from it. It is passed
|
||||
explicitly rather than read from the config because callers may reuse
|
||||
one config object across DP ranks, overwriting the field each time.
|
||||
"""
|
||||
world_size = vllm_config.parallel_config.world_size
|
||||
local_world_size = vllm_config.parallel_config.local_world_size
|
||||
evar = current_platform.device_control_env_var
|
||||
|
||||
value = get_device_indices(evar, local_dp_rank, world_size, local_world_size)
|
||||
with patch.dict(os.environ, values=((evar, value),)):
|
||||
yield
|
||||
physical_gpu_ids = get_physical_gpu_ids_for_local_dp_rank(
|
||||
evar,
|
||||
local_dp_rank,
|
||||
world_size,
|
||||
local_world_size,
|
||||
user_assigned_gpu_ids=user_assigned_gpu_ids,
|
||||
)
|
||||
vllm_config.parallel_config.assigned_physical_gpu_ids = physical_gpu_ids
|
||||
|
||||
|
||||
def get_device_indices(
|
||||
def get_physical_gpu_ids_for_local_dp_rank(
|
||||
device_control_env_var: str,
|
||||
local_dp_rank: int,
|
||||
world_size: int,
|
||||
local_world_size: int | None = None,
|
||||
):
|
||||
user_assigned_gpu_ids: list[int] | None = None,
|
||||
) -> list[int]:
|
||||
"""
|
||||
Returns a comma-separated string of device indices for the specified
|
||||
Returns list of physical GPU IDs for the specified
|
||||
data parallel rank.
|
||||
|
||||
For example, if world_size=2 and local_dp_rank=1, and there are 4 devices,
|
||||
this will select devices 2 and 3 for local_dp_rank=1.
|
||||
this will return [2, 3] for local_dp_rank=1.
|
||||
|
||||
If user_assigned_gpu_ids is provided (e.g. from --device-ids), this DP
|
||||
rank's shard is sliced from it instead of being derived from the
|
||||
device-control env var.
|
||||
"""
|
||||
if local_world_size is None:
|
||||
local_world_size = world_size
|
||||
if user_assigned_gpu_ids is not None:
|
||||
start = local_dp_rank * world_size
|
||||
stop = start + local_world_size
|
||||
if stop > len(user_assigned_gpu_ids):
|
||||
raise ValueError(
|
||||
f"--device-ids provides {len(user_assigned_gpu_ids)} devices, "
|
||||
f"but DP rank {local_dp_rank} needs devices [{start}, {stop})"
|
||||
)
|
||||
return user_assigned_gpu_ids[start:stop]
|
||||
try:
|
||||
value = ",".join(
|
||||
str(current_platform.device_id_to_physical_device_id(i))
|
||||
return [
|
||||
current_platform.device_id_to_physical_device_id(i)
|
||||
for i in range(
|
||||
local_dp_rank * world_size,
|
||||
local_dp_rank * world_size + local_world_size,
|
||||
)
|
||||
)
|
||||
]
|
||||
except IndexError as e:
|
||||
raise Exception(
|
||||
f"Error setting {device_control_env_var}: "
|
||||
f"Error computing device indices for "
|
||||
f"{device_control_env_var}: "
|
||||
f"local range: [{local_dp_rank * world_size}, "
|
||||
f"{(local_dp_rank + 1) * world_size}) "
|
||||
"base value: "
|
||||
f'"{os.getenv(device_control_env_var)}"'
|
||||
) from e
|
||||
return value
|
||||
|
||||
|
||||
def _apply_dp_identity_suffix(dp_vllm_config, dp_rank: int) -> None:
|
||||
@@ -453,11 +476,11 @@ class CoreEngineActorManager:
|
||||
# https://github.com/ray-project/ray/blob/master/python/ray/_private/accelerators/intel_gpu.py#L56 # noqa: E501
|
||||
if current_platform.is_xpu():
|
||||
device_evar = current_platform.device_control_env_var
|
||||
device_indices = get_device_indices(
|
||||
physical_gpu_ids = get_physical_gpu_ids_for_local_dp_rank(
|
||||
device_evar, local_index, world_size
|
||||
)
|
||||
actor_env_vars = self.env_vars_dict.copy()
|
||||
actor_env_vars[device_evar] = device_indices
|
||||
actor_env_vars[device_evar] = ",".join(str(d) for d in physical_gpu_ids)
|
||||
runtime_env = RuntimeEnv(env_vars=actor_env_vars)
|
||||
|
||||
actor = (
|
||||
|
||||
@@ -826,6 +826,16 @@ class WorkerProc:
|
||||
signal.signal(signal.SIGTERM, signal_handler)
|
||||
signal.signal(signal.SIGINT, signal_handler)
|
||||
|
||||
# Publish the logical-to-physical mapping early so topology helpers
|
||||
# work before init_device (needed by set_worker_net_device below).
|
||||
assigned_physical_gpu_ids = kwargs[
|
||||
"vllm_config"
|
||||
].parallel_config.assigned_physical_gpu_ids
|
||||
if assigned_physical_gpu_ids is not None:
|
||||
from vllm.platforms.interface import set_assigned_physical_gpu_ids
|
||||
|
||||
set_assigned_physical_gpu_ids(assigned_physical_gpu_ids)
|
||||
|
||||
# Set net device env vars for the worker if VLLM_GPU_NIC_PCIE_MAPPING is set
|
||||
set_worker_net_device(kwargs.get("local_rank", 0), kwargs["vllm_config"])
|
||||
|
||||
|
||||
@@ -258,30 +258,35 @@ class RayDistributedExecutor(Executor):
|
||||
}
|
||||
self.collective_rpc("adjust_rank", args=(rerank_mapping,))
|
||||
|
||||
# Get the set of GPU IDs used on each node.
|
||||
worker_node_and_gpu_ids = []
|
||||
# Get the set of physical GPU IDs used on each node.
|
||||
worker_node_and_physical_gpu_ids = []
|
||||
for worker in [self.driver_dummy_worker] + self.workers:
|
||||
if worker is None:
|
||||
# driver_dummy_worker can be None when using ray spmd worker.
|
||||
continue
|
||||
worker_node_and_gpu_ids.append(
|
||||
ray.get(worker.get_node_and_gpu_ids.remote()) # type: ignore[attr-defined]
|
||||
worker_node_and_physical_gpu_ids.append(
|
||||
ray.get(worker.get_node_and_physical_gpu_ids.remote()) # type: ignore[attr-defined]
|
||||
)
|
||||
|
||||
node_workers = defaultdict(list) # node id -> list of worker ranks
|
||||
node_gpus = defaultdict(list) # node id -> list of gpu ids
|
||||
node_physical_gpu_ids = defaultdict(list) # node id -> physical GPU IDs
|
||||
|
||||
for i, (node_id, gpu_ids) in enumerate(worker_node_and_gpu_ids):
|
||||
for i, (node_id, physical_gpu_ids) in enumerate(
|
||||
worker_node_and_physical_gpu_ids
|
||||
):
|
||||
node_workers[node_id].append(i)
|
||||
# `gpu_ids` can be a list of strings or integers.
|
||||
# `physical_gpu_ids` can be a list of strings or integers.
|
||||
# convert them to integers for consistency.
|
||||
# NOTE: gpu_ids can be larger than 9 (e.g. 16 GPUs),
|
||||
# NOTE: physical GPU IDs can be larger than 9 (e.g. 16 GPUs),
|
||||
# string sorting is not sufficient.
|
||||
# see https://github.com/vllm-project/vllm/issues/5590
|
||||
gpu_ids = [int(x) for x in gpu_ids]
|
||||
node_gpus[node_id].extend(gpu_ids)
|
||||
for node_id, gpu_ids in node_gpus.items():
|
||||
node_gpus[node_id] = sorted(gpu_ids)
|
||||
physical_gpu_ids = [
|
||||
current_platform.device_control_id_to_physical_device_id(str(x))
|
||||
for x in physical_gpu_ids
|
||||
]
|
||||
node_physical_gpu_ids[node_id].extend(physical_gpu_ids)
|
||||
for node_id, physical_gpu_ids in node_physical_gpu_ids.items():
|
||||
node_physical_gpu_ids[node_id] = sorted(physical_gpu_ids)
|
||||
|
||||
all_ips = set(worker_ips + [driver_ip])
|
||||
n_ips = len(all_ips)
|
||||
@@ -297,23 +302,8 @@ class RayDistributedExecutor(Executor):
|
||||
" each node."
|
||||
)
|
||||
|
||||
# Set environment variables for the driver and workers.
|
||||
# We set CUDA_VISIBLE_DEVICES to ALL GPUs on the node for each worker.
|
||||
# This is needed because:
|
||||
# 1. Ray's compiled DAG needs to find the allocated GPU in
|
||||
# CUDA_VISIBLE_DEVICES.
|
||||
# 2. vLLM's communication layer (NCCL, CustomAllreduce) needs to see
|
||||
# all GPUs for P2P checks and communication setup. Though if it was
|
||||
# just this reason, we could have also just kept the visible devices
|
||||
# unset.
|
||||
# Each worker will use local_rank to index into the visible devices.
|
||||
all_args_to_update_environment_variables = [
|
||||
{
|
||||
current_platform.device_control_env_var: ",".join(
|
||||
map(str, node_gpus[node_id])
|
||||
),
|
||||
}
|
||||
for (node_id, _) in worker_node_and_gpu_ids
|
||||
all_args_to_update_environment_variables: list[dict[str, str]] = [
|
||||
{} for _ in worker_node_and_physical_gpu_ids
|
||||
]
|
||||
|
||||
# Environment variables to copy from driver to workers
|
||||
@@ -336,7 +326,7 @@ class RayDistributedExecutor(Executor):
|
||||
"update_environment_variables", args=(self._get_env_vars_to_be_updated(),)
|
||||
)
|
||||
|
||||
if len(node_gpus) == 1:
|
||||
if len(node_physical_gpu_ids) == 1:
|
||||
# in single node case, we don't need to get the IP address.
|
||||
# the loopback address is sufficient
|
||||
# NOTE: a node may have several IP addresses, one for each
|
||||
@@ -352,10 +342,11 @@ class RayDistributedExecutor(Executor):
|
||||
|
||||
# Initialize the actual workers inside worker wrapper.
|
||||
all_kwargs = []
|
||||
for rank, (node_id, _) in enumerate(worker_node_and_gpu_ids):
|
||||
for rank, (node_id, _) in enumerate(worker_node_and_physical_gpu_ids):
|
||||
local_rank = node_workers[node_id].index(rank)
|
||||
kwargs = dict(
|
||||
vllm_config=self.vllm_config,
|
||||
assigned_physical_gpu_ids=sorted(node_physical_gpu_ids[node_id]),
|
||||
local_rank=local_rank,
|
||||
rank=rank,
|
||||
distributed_init_method=distributed_init_method,
|
||||
|
||||
@@ -79,24 +79,25 @@ class RayWorkerProc(WorkerProc):
|
||||
1. __init__: lightweight setup, stores init args (no device/model init)
|
||||
2. initialize_worker: called after GPU IDs are discovered, completes
|
||||
the full WorkerProc initialization with the correct local_rank and
|
||||
CUDA_VISIBLE_DEVICES.
|
||||
logical-to-physical GPU mapping.
|
||||
|
||||
CUDA_VISIBLE_DEVICES setup flow:
|
||||
GPU assignment flow:
|
||||
|
||||
1. RayExecutorV2 enables RAY_EXPERIMENTAL_NOSET_CUDA_VISIBLE_DEVICES so Ray does
|
||||
not set CUDA_VISIBLE_DEVICES on RayWorkerProc actors at creation time.
|
||||
2. Each actor is scheduled with a placement group and bundle index; Ray resolves
|
||||
the physical GPU ID for that bundle at placement time.
|
||||
3. After placement, the worker discovers that GPU ID and sets
|
||||
CUDA_VISIBLE_DEVICES before finishing WorkerProc initialization.
|
||||
3. After placement, the executor discovers each worker's GPU ID and passes the
|
||||
node's logical-to-physical mapping (assigned_physical_gpu_ids) to
|
||||
initialize_worker(); CUDA_VISIBLE_DEVICES is never modified.
|
||||
|
||||
There is no workaround for this unset-and-reset sequence when the placement group
|
||||
is externally managed: scheduling must complete before CUDA_VISIBLE_DEVICES can
|
||||
match the GPU tied to the worker's bundle.
|
||||
Scheduling must complete before the mapping is known when the placement
|
||||
group is externally managed: only then is the GPU tied to the worker's
|
||||
bundle resolved.
|
||||
|
||||
This sequence allows multiple vLLM instances to coexist on the same node:
|
||||
each instance is unaware which physical devices others hold, and the
|
||||
externally managed placement group avoids CUDA_VISIBLE_DEVICES conflicts
|
||||
externally managed placement group avoids device assignment conflicts
|
||||
by binding workers to specific placement group bundles.
|
||||
"""
|
||||
|
||||
@@ -120,28 +121,33 @@ class RayWorkerProc(WorkerProc):
|
||||
is_driver_worker=is_driver_worker,
|
||||
)
|
||||
|
||||
def get_node_and_gpu_ids(self) -> tuple[str, list[int]]:
|
||||
"""Return (node_id, gpu_ids) assigned to this actor by Ray."""
|
||||
def get_node_and_physical_gpu_ids(self) -> tuple[str, list[int]]:
|
||||
"""Return (node_id, physical_gpu_ids) assigned to this actor by Ray."""
|
||||
node_id = ray.get_runtime_context().get_node_id()
|
||||
device_key = current_platform.ray_device_key
|
||||
if not device_key:
|
||||
raise RuntimeError(
|
||||
f"current platform {current_platform.device_name} does not support ray."
|
||||
)
|
||||
gpu_ids = ray.get_runtime_context().get_accelerator_ids()[device_key]
|
||||
return node_id, [int(x) for x in gpu_ids]
|
||||
physical_gpu_ids = ray.get_runtime_context().get_accelerator_ids()[device_key]
|
||||
return node_id, [
|
||||
current_platform.device_control_id_to_physical_device_id(str(x))
|
||||
for x in physical_gpu_ids
|
||||
]
|
||||
|
||||
def initialize_worker(
|
||||
self,
|
||||
local_rank: int,
|
||||
env_vars: dict[str, str],
|
||||
driver_env_vars: dict[str, str] | None = None,
|
||||
assigned_physical_gpu_ids: list[int] | None = None,
|
||||
) -> None:
|
||||
"""Complete initialization after GPU assignment is known.
|
||||
|
||||
*driver_env_vars* are applied with ``setdefault`` — they fill
|
||||
in missing vars but never overwrite node-local values.
|
||||
*env_vars* (e.g. CUDA_VISIBLE_DEVICES) always overwrite.
|
||||
*env_vars* always overwrite.
|
||||
*assigned_physical_gpu_ids* maps local_rank to physical CUDA device ID.
|
||||
"""
|
||||
if driver_env_vars:
|
||||
for key, value in driver_env_vars.items():
|
||||
@@ -149,6 +155,13 @@ class RayWorkerProc(WorkerProc):
|
||||
for key, value in env_vars.items():
|
||||
os.environ[key] = value
|
||||
|
||||
if assigned_physical_gpu_ids is not None:
|
||||
vllm_config = self._init_kwargs["vllm_config"]
|
||||
assert isinstance(vllm_config, VllmConfig)
|
||||
vllm_config.parallel_config.assigned_physical_gpu_ids = (
|
||||
assigned_physical_gpu_ids
|
||||
)
|
||||
|
||||
self.local_rank = local_rank
|
||||
super().__init__(
|
||||
local_rank=local_rank,
|
||||
@@ -365,36 +378,48 @@ class RayExecutorV2(MultiprocExecutor):
|
||||
)
|
||||
self.ray_worker_handles.append(handle)
|
||||
|
||||
# Step 6: Discover GPU IDs assigned to each worker via Ray runtime context.
|
||||
worker_node_and_gpu_ids = ray.get(
|
||||
[h.actor.get_node_and_gpu_ids.remote() for h in self.ray_worker_handles]
|
||||
# Step 6: Discover physical GPU IDs assigned to each worker via Ray
|
||||
# runtime context.
|
||||
worker_node_and_physical_gpu_ids = ray.get(
|
||||
[
|
||||
h.actor.get_node_and_physical_gpu_ids.remote()
|
||||
for h in self.ray_worker_handles
|
||||
]
|
||||
)
|
||||
|
||||
node_workers: dict[str, list[int]] = defaultdict(list)
|
||||
node_gpus: dict[str, list[int]] = defaultdict(list)
|
||||
for i, (node_id, gpu_ids) in enumerate(worker_node_and_gpu_ids):
|
||||
node_physical_gpu_ids: dict[str, list[int]] = defaultdict(list)
|
||||
for i, (node_id, physical_gpu_ids) in enumerate(
|
||||
worker_node_and_physical_gpu_ids
|
||||
):
|
||||
node_workers[node_id].append(i)
|
||||
node_gpus[node_id].extend(gpu_ids)
|
||||
for node_id, gpu_ids in node_gpus.items():
|
||||
node_gpus[node_id] = sorted(gpu_ids)
|
||||
node_physical_gpu_ids[node_id].extend(physical_gpu_ids)
|
||||
for node_id, physical_gpu_ids in node_physical_gpu_ids.items():
|
||||
node_physical_gpu_ids[node_id] = sorted(physical_gpu_ids)
|
||||
|
||||
# Step 7: Initialize workers with correct local_rank and
|
||||
# CUDA_VISIBLE_DEVICES. Each worker sees all GPUs assigned to
|
||||
# this executor on its node; local_rank indexes into that set.
|
||||
# Step 7: Initialize workers with local logical ranks and the
|
||||
# logical-to-physical GPU mapping discovered from Ray placement.
|
||||
init_worker_refs = []
|
||||
for i, (node_id, _) in enumerate(worker_node_and_gpu_ids):
|
||||
for i, (node_id, _) in enumerate(worker_node_and_physical_gpu_ids):
|
||||
local_rank = node_workers[node_id].index(i)
|
||||
worker_env_vars = {
|
||||
current_platform.device_control_env_var: ",".join(
|
||||
map(str, node_gpus[node_id])
|
||||
),
|
||||
}
|
||||
assigned_physical_gpu_ids = sorted(node_physical_gpu_ids[node_id])
|
||||
worker_env_vars: dict[str, str] = {}
|
||||
self.ray_worker_handles[i].local_rank = local_rank
|
||||
init_worker_refs.append(
|
||||
self.ray_worker_handles[i].actor.initialize_worker.remote(
|
||||
local_rank, worker_env_vars, self.driver_env_vars
|
||||
local_rank,
|
||||
worker_env_vars,
|
||||
self.driver_env_vars,
|
||||
assigned_physical_gpu_ids=assigned_physical_gpu_ids,
|
||||
)
|
||||
)
|
||||
# Also set on the executor-side config for consistency. The mapping
|
||||
# is per-node, so only do this when all workers share one node.
|
||||
if len(node_physical_gpu_ids) == 1:
|
||||
node_id_0 = worker_node_and_physical_gpu_ids[0][0]
|
||||
self.vllm_config.parallel_config.assigned_physical_gpu_ids = sorted(
|
||||
node_physical_gpu_ids[node_id_0]
|
||||
)
|
||||
ray.get(init_worker_refs)
|
||||
|
||||
# Step 8: Collect response MQ handles
|
||||
|
||||
@@ -93,7 +93,7 @@ try:
|
||||
def get_node_ip(self) -> str:
|
||||
return get_ip()
|
||||
|
||||
def get_node_and_gpu_ids(self) -> tuple[str, list[int]]:
|
||||
def get_node_and_physical_gpu_ids(self) -> tuple[str, list[int]]:
|
||||
node_id = ray.get_runtime_context().get_node_id()
|
||||
device_key = vllm.platforms.current_platform.ray_device_key
|
||||
if not device_key:
|
||||
@@ -101,8 +101,10 @@ try:
|
||||
"current platform %s does not support ray.",
|
||||
vllm.platforms.current_platform.device_name,
|
||||
)
|
||||
gpu_ids = ray.get_runtime_context().get_accelerator_ids()[device_key]
|
||||
return node_id, gpu_ids
|
||||
physical_gpu_ids = ray.get_runtime_context().get_accelerator_ids()[
|
||||
device_key
|
||||
]
|
||||
return node_id, physical_gpu_ids
|
||||
|
||||
def setup_device_if_necessary(self):
|
||||
# TODO(swang): This is needed right now because Ray CG executes
|
||||
|
||||
@@ -847,6 +847,8 @@ class KVCacheTensor:
|
||||
|
||||
size: int # size of the KV cache tensor in bytes
|
||||
shared_by: list[str] # layer names that share the same KV cache tensor
|
||||
offset: int = 0 # byte offset of this layer within a contiguous block
|
||||
block_stride: int = 0 # total bytes per block in a packed layout (0 = not packed)
|
||||
|
||||
|
||||
@dataclass
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
from collections.abc import Iterable, Sequence
|
||||
from dataclasses import dataclass
|
||||
from math import prod
|
||||
from typing import Any, cast
|
||||
|
||||
import torch
|
||||
@@ -155,8 +156,17 @@ def _allocate_kv_cache(
|
||||
kv_cache_config: KVCacheConfig, shared_layers: dict[str, str], device: torch.device
|
||||
):
|
||||
kv_cache_raw_tensors: dict[str, torch.Tensor] = {}
|
||||
packed_backing: torch.Tensor | None = None
|
||||
for kv_cache_tensor in kv_cache_config.kv_cache_tensors:
|
||||
tensor = torch.zeros(kv_cache_tensor.size, dtype=torch.int8, device=device)
|
||||
if kv_cache_tensor.block_stride > 0:
|
||||
# Allocate once; all packed tensors alias the same backing.
|
||||
if packed_backing is None:
|
||||
packed_backing = torch.zeros(
|
||||
kv_cache_tensor.size, dtype=torch.int8, device=device
|
||||
)
|
||||
tensor = packed_backing
|
||||
else:
|
||||
tensor = torch.zeros(kv_cache_tensor.size, dtype=torch.int8, device=device)
|
||||
for layer_name in kv_cache_tensor.shared_by:
|
||||
kv_cache_raw_tensors[layer_name] = tensor
|
||||
|
||||
@@ -176,10 +186,18 @@ def _reshape_kv_cache(
|
||||
cache_dtype: str,
|
||||
kernel_block_sizes: list[int],
|
||||
shared_kv_cache_layers: dict[str, str],
|
||||
kv_cache_config: "KVCacheConfig | None" = None,
|
||||
) -> dict[str, Any]:
|
||||
kv_caches: dict[str, Any] = {}
|
||||
has_attn, has_mamba = False, False
|
||||
|
||||
layer_packing: dict[str, tuple[int, int]] = {}
|
||||
if kv_cache_config is not None:
|
||||
for kv_tensor in kv_cache_config.kv_cache_tensors:
|
||||
if kv_tensor.block_stride > 0:
|
||||
for ln in kv_tensor.shared_by:
|
||||
layer_packing[ln] = (kv_tensor.offset, kv_tensor.block_stride)
|
||||
|
||||
for group in attn_groups:
|
||||
if group.kv_cache_group_id >= len(kernel_block_sizes):
|
||||
continue
|
||||
@@ -198,8 +216,13 @@ def _reshape_kv_cache(
|
||||
continue
|
||||
|
||||
kv_raw_tensor = kv_cache_raw_tensors[layer_name]
|
||||
assert kv_raw_tensor.numel() % kv_cache_spec.page_size_bytes == 0
|
||||
num_blocks = kv_raw_tensor.numel() // kv_cache_spec.page_size_bytes
|
||||
packing = layer_packing.get(layer_name)
|
||||
if packing is not None:
|
||||
_, blk_stride = packing
|
||||
num_blocks = kv_raw_tensor.numel() // blk_stride
|
||||
else:
|
||||
assert kv_raw_tensor.numel() % kv_cache_spec.page_size_bytes == 0
|
||||
num_blocks = kv_raw_tensor.numel() // kv_cache_spec.page_size_bytes
|
||||
|
||||
if isinstance(kv_cache_spec, AttentionSpec):
|
||||
has_attn = True
|
||||
@@ -232,8 +255,18 @@ def _reshape_kv_cache(
|
||||
]
|
||||
|
||||
dtype = kv_cache_spec.dtype
|
||||
kv_tensor = kv_raw_tensor.view(dtype)
|
||||
if kv_cache_spec.page_size_padded is not None:
|
||||
if packing is not None:
|
||||
offset, block_stride = packing
|
||||
assert inv_order[0] == 0
|
||||
page_bytes = prod(kv_cache_shape[1:]) * get_dtype_size(dtype)
|
||||
kv_cache = (
|
||||
kv_raw_tensor.view(-1, block_stride)[
|
||||
:, offset : offset + page_bytes
|
||||
]
|
||||
.view(dtype)
|
||||
.view(kv_cache_shape)
|
||||
)
|
||||
elif kv_cache_spec.page_size_padded is not None:
|
||||
# Use strided view to handle page_size_bytes that
|
||||
# include padding. This follows the same pattern as
|
||||
# MambaSpec handling in gpu_model_runner.py.
|
||||
@@ -246,13 +279,13 @@ def _reshape_kv_cache(
|
||||
strides = list(torch.empty(kv_cache_shape).stride())
|
||||
strides[inv_order[0]] = page_stride
|
||||
kv_cache = torch.as_strided(
|
||||
kv_tensor,
|
||||
kv_raw_tensor.view(dtype),
|
||||
size=kv_cache_shape,
|
||||
stride=tuple(strides),
|
||||
)
|
||||
else:
|
||||
# No padding — safe to use a contiguous view.
|
||||
kv_cache = kv_tensor.view(kv_cache_shape)
|
||||
kv_cache = kv_raw_tensor.view(dtype).view(kv_cache_shape)
|
||||
kv_caches[layer_name] = kv_cache.permute(*inv_order)
|
||||
|
||||
elif isinstance(kv_cache_spec, MambaSpec):
|
||||
@@ -365,6 +398,7 @@ def init_kv_cache(
|
||||
kernel_block_sizes=kernel_block_sizes,
|
||||
cache_dtype=cache_dtype,
|
||||
shared_kv_cache_layers=shared_kv_cache_layers,
|
||||
kv_cache_config=kv_cache_config,
|
||||
)
|
||||
bind_kv_cache(kv_caches, forward_context, runner_kv_caches)
|
||||
return kv_caches
|
||||
|
||||
@@ -12,6 +12,7 @@ from contextlib import contextmanager
|
||||
from copy import copy, deepcopy
|
||||
from dataclasses import dataclass, replace
|
||||
from functools import reduce
|
||||
from math import prod
|
||||
from typing import TYPE_CHECKING, Any, NamedTuple, TypeAlias, cast
|
||||
|
||||
import numpy as np
|
||||
@@ -7029,10 +7030,21 @@ class GPUModelRunner(
|
||||
corresponding memory buffer for KV cache.
|
||||
"""
|
||||
kv_cache_raw_tensors: dict[str, torch.Tensor] = {}
|
||||
packed_backing: torch.Tensor | None = None
|
||||
for kv_cache_tensor in kv_cache_config.kv_cache_tensors:
|
||||
tensor = torch.zeros(
|
||||
kv_cache_tensor.size, dtype=torch.int8, device=self.device
|
||||
)
|
||||
if kv_cache_tensor.block_stride > 0:
|
||||
# Allocate once; all packed tensors alias the same backing.
|
||||
if packed_backing is None:
|
||||
packed_backing = torch.zeros(
|
||||
kv_cache_tensor.size,
|
||||
dtype=torch.int8,
|
||||
device=self.device,
|
||||
)
|
||||
tensor = packed_backing
|
||||
else:
|
||||
tensor = torch.zeros(
|
||||
kv_cache_tensor.size, dtype=torch.int8, device=self.device
|
||||
)
|
||||
for layer_name in kv_cache_tensor.shared_by:
|
||||
kv_cache_raw_tensors[layer_name] = tensor
|
||||
|
||||
@@ -7074,6 +7086,14 @@ class GPUModelRunner(
|
||||
"""
|
||||
kv_caches: dict[str, torch.Tensor] = {}
|
||||
has_attn, has_mamba = False, False
|
||||
|
||||
# Map layer names to (offset, block_stride) within the packed
|
||||
# backing tensor so we can create strided views per layer.
|
||||
layer_packing: dict[str, tuple[int, int]] = {}
|
||||
for kv_tensor in self.kv_cache_config.kv_cache_tensors:
|
||||
if kv_tensor.block_stride > 0:
|
||||
for ln in kv_tensor.shared_by:
|
||||
layer_packing[ln] = (kv_tensor.offset, kv_tensor.block_stride)
|
||||
for group in self._kv_cache_spec_attn_group_iterator():
|
||||
kv_cache_spec = group.kv_cache_spec
|
||||
attn_backend = group.backend
|
||||
@@ -7085,8 +7105,13 @@ class GPUModelRunner(
|
||||
if layer_name in self.runner_only_attn_layers:
|
||||
continue
|
||||
raw_tensor = kv_cache_raw_tensors[layer_name]
|
||||
assert raw_tensor.numel() % kv_cache_spec.page_size_bytes == 0
|
||||
num_blocks = raw_tensor.numel() // kv_cache_spec.page_size_bytes
|
||||
packing = layer_packing.get(layer_name)
|
||||
if packing is not None:
|
||||
_, blk_stride = packing
|
||||
num_blocks = raw_tensor.numel() // blk_stride
|
||||
else:
|
||||
assert raw_tensor.numel() % kv_cache_spec.page_size_bytes == 0
|
||||
num_blocks = raw_tensor.numel() // kv_cache_spec.page_size_bytes
|
||||
if isinstance(kv_cache_spec, AttentionSpec):
|
||||
has_attn = True
|
||||
num_blocks_per_kv_block = (
|
||||
@@ -7127,8 +7152,17 @@ class GPUModelRunner(
|
||||
for i in range(len(kv_cache_stride_order))
|
||||
]
|
||||
|
||||
raw_tensor = kv_cache_raw_tensors[layer_name].view(dtype)
|
||||
if kv_cache_spec.page_size_padded is not None:
|
||||
if packing is not None:
|
||||
offset, block_stride = packing
|
||||
assert inv_order[0] == 0
|
||||
page_bytes = prod(kv_cache_shape[1:]) * get_dtype_size(dtype)
|
||||
kv_cache = (
|
||||
kv_cache_raw_tensors[layer_name]
|
||||
.view(-1, block_stride)[:, offset : offset + page_bytes]
|
||||
.view(dtype)
|
||||
.view(kv_cache_shape)
|
||||
)
|
||||
elif kv_cache_spec.page_size_padded is not None:
|
||||
# Use strided view to handle page_size_bytes that
|
||||
# include padding. This follows
|
||||
# the same pattern as MambaSpec handling below.
|
||||
@@ -7142,13 +7176,17 @@ class GPUModelRunner(
|
||||
strides = list(torch.empty(kv_cache_shape).stride())
|
||||
strides[inv_order[0]] = page_stride
|
||||
kv_cache = torch.as_strided(
|
||||
raw_tensor,
|
||||
kv_cache_raw_tensors[layer_name].view(dtype),
|
||||
size=kv_cache_shape,
|
||||
stride=tuple(strides),
|
||||
)
|
||||
else:
|
||||
# No padding — safe to use a contiguous view.
|
||||
kv_cache = raw_tensor.view(kv_cache_shape)
|
||||
kv_cache = (
|
||||
kv_cache_raw_tensors[layer_name]
|
||||
.view(dtype)
|
||||
.view(kv_cache_shape)
|
||||
)
|
||||
kv_caches[layer_name] = kv_cache.permute(*inv_order)
|
||||
|
||||
elif isinstance(kv_cache_spec, MambaSpec):
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user