forked from Karylab-cklius/vllm
Compare commits
36
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a02155c787 | ||
|
|
97a98006b0 | ||
|
|
0a684ab0c0 | ||
|
|
0d9210a502 | ||
|
|
1d874867ea | ||
|
|
2e2e626b40 | ||
|
|
af91f4b3e4 | ||
|
|
2396a61108 | ||
|
|
97a668152b | ||
|
|
58b2012aa2 | ||
|
|
b7c20d0cfa | ||
|
|
a2b1f9fc3b | ||
|
|
642076d26c | ||
|
|
5feb3950e5 | ||
|
|
4ec199b66a | ||
|
|
7ca017778f | ||
|
|
fbfe58133d | ||
|
|
9dd62d80ab | ||
|
|
f878367898 | ||
|
|
bd091079cb | ||
|
|
b23bd73f54 | ||
|
|
e2d7adeb64 | ||
|
|
15cb8e140d | ||
|
|
f007cceb42 | ||
|
|
0a5069e4e3 | ||
|
|
8ce53a616e | ||
|
|
319db65b68 | ||
|
|
83a7669827 | ||
|
|
401bed48ad | ||
|
|
d492d1e697 | ||
|
|
f37e113590 | ||
|
|
a10e369f06 | ||
|
|
aa3f2efe42 | ||
|
|
a32d1bff95 | ||
|
|
ee47a21fcd | ||
|
|
0f471a3088 |
@@ -813,8 +813,8 @@ steps:
|
||||
|
||||
# Download artifacts from current build
|
||||
echo "Downloading artifacts from current build"
|
||||
# buildkite-agent artifact download "artifacts/rocm-base-wheels/*.whl" .
|
||||
# buildkite-agent artifact download "artifacts/rocm-vllm-wheel/*.whl" .
|
||||
buildkite-agent artifact download "artifacts/rocm-base-wheels/*.whl" .
|
||||
buildkite-agent artifact download "artifacts/rocm-vllm-wheel/*.whl" .
|
||||
|
||||
# # Run upload script
|
||||
bash .buildkite/scripts/upload-rocm-wheels.sh
|
||||
|
||||
@@ -10,7 +10,9 @@ steps:
|
||||
- vllm/engine/arg_utils.py
|
||||
- vllm/config/model.py
|
||||
- vllm/model_executor
|
||||
- vllm/model_executor/warmup
|
||||
- tests/model_executor
|
||||
- tests/model_executor/test_jit_warmup.py
|
||||
- tests/entrypoints/openai/completion/test_tensorizer_entrypoint.py
|
||||
commands:
|
||||
- apt-get update && apt-get install -y curl libsodium23
|
||||
@@ -34,7 +36,9 @@ steps:
|
||||
- vllm/engine/arg_utils.py
|
||||
- vllm/config/model.py
|
||||
- vllm/model_executor
|
||||
- vllm/model_executor/warmup
|
||||
- tests/model_executor
|
||||
- tests/model_executor/test_jit_warmup.py
|
||||
- tests/entrypoints/openai/completion/test_tensorizer_entrypoint.py
|
||||
- vllm/_aiter_ops.py
|
||||
- vllm/platforms/rocm.py
|
||||
|
||||
+86
-42
@@ -227,6 +227,22 @@ if(VLLM_GPU_LANG STREQUAL "CUDA")
|
||||
cuda_archs_loose_intersection(CUDA_ARCHS
|
||||
"${CUDA_SUPPORTED_ARCHS}" "${CUDA_ARCHS}")
|
||||
message(STATUS "CUDA supported target architectures: ${CUDA_ARCHS}")
|
||||
|
||||
set(VLLM_COMPILED_CUDA_ARCHS)
|
||||
foreach(_ARCH ${CUDA_ARCHS})
|
||||
set(_COMPILED_ARCH "${_ARCH}")
|
||||
if(CMAKE_CUDA_COMPILER_VERSION VERSION_GREATER_EQUAL 13.0)
|
||||
if(_ARCH MATCHES "^(10|11|12)\\.0$")
|
||||
set(_COMPILED_ARCH "${_ARCH}f")
|
||||
endif()
|
||||
elseif(CMAKE_CUDA_COMPILER_VERSION VERSION_GREATER_EQUAL 12.8)
|
||||
if(_ARCH MATCHES "^(10\\.(0|1|3)|12\\.(0|1))$")
|
||||
set(_COMPILED_ARCH "${_ARCH}a")
|
||||
endif()
|
||||
endif()
|
||||
list(APPEND VLLM_COMPILED_CUDA_ARCHS "${_COMPILED_ARCH}")
|
||||
endforeach()
|
||||
list(JOIN VLLM_COMPILED_CUDA_ARCHS "," VLLM_COMPILED_CUDA_ARCHS_STR)
|
||||
else()
|
||||
#
|
||||
# For other GPU targets override the GPU architectures detected by cmake/torch
|
||||
@@ -385,8 +401,20 @@ if(VLLM_GPU_LANG STREQUAL "CUDA" OR VLLM_GPU_LANG STREQUAL "HIP")
|
||||
#
|
||||
# _C_stable_libtorch extension (ops registered via STABLE_TORCH_LIBRARY)
|
||||
#
|
||||
# Shared entry sources are part of the base extension source list, but some
|
||||
# optional kernel families below append source-local feature macros to them.
|
||||
set(VLLM_STABLE_TORCH_BINDINGS_SRC
|
||||
"csrc/libtorch_stable/torch_bindings.cpp")
|
||||
set(CACHE_KERNELS_SRC "csrc/libtorch_stable/cache_kernels.cu")
|
||||
set(SCALED_MM_ENTRY_SRC
|
||||
"csrc/libtorch_stable/quantization/w8a8/cutlass/scaled_mm_entry.cu")
|
||||
set(NVFP4_QUANT_ENTRY_SRC
|
||||
"csrc/libtorch_stable/quantization/fp4/nvfp4_quant_entry.cu")
|
||||
set(NVFP4_SCALED_MM_ENTRY_SRC
|
||||
"csrc/libtorch_stable/quantization/fp4/nvfp4_scaled_mm_entry.cu")
|
||||
|
||||
set(VLLM_STABLE_EXT_SRC
|
||||
"csrc/libtorch_stable/torch_bindings.cpp"
|
||||
"${VLLM_STABLE_TORCH_BINDINGS_SRC}"
|
||||
"csrc/libtorch_stable/cuda_view.cu"
|
||||
"csrc/libtorch_stable/cuda_utils_kernels.cu"
|
||||
"csrc/libtorch_stable/activation_kernels.cu"
|
||||
@@ -409,7 +437,7 @@ if(VLLM_GPU_LANG STREQUAL "CUDA" OR VLLM_GPU_LANG STREQUAL "HIP")
|
||||
"csrc/libtorch_stable/sampler.cu"
|
||||
"csrc/libtorch_stable/topk.cu"
|
||||
"csrc/libtorch_stable/mamba/selective_scan_fwd.cu"
|
||||
"csrc/libtorch_stable/cache_kernels.cu"
|
||||
"${CACHE_KERNELS_SRC}"
|
||||
"csrc/libtorch_stable/cache_kernels_fused.cu"
|
||||
"csrc/libtorch_stable/custom_all_reduce.cu"
|
||||
"csrc/libtorch_stable/fused_deepseek_v4_qnorm_rope_kv_insert_kernel.cu")
|
||||
@@ -425,11 +453,6 @@ if(VLLM_GPU_LANG STREQUAL "CUDA" OR VLLM_GPU_LANG STREQUAL "HIP")
|
||||
cuda_archs_loose_intersection(COOPERATIVE_TOPK_ARCHS
|
||||
"9.0a;10.0a;10.1a;10.3a;12.0a;12.1a" "${CUDA_ARCHS}")
|
||||
endif()
|
||||
|
||||
if(COOPERATIVE_TOPK_ARCHS)
|
||||
list(APPEND VLLM_GPU_FLAGS "-DVLLM_ENABLE_COOPERATIVE_TOPK=1")
|
||||
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(VLLM_GPU_LANG STREQUAL "CUDA")
|
||||
@@ -467,11 +490,14 @@ if(VLLM_GPU_LANG STREQUAL "CUDA" OR VLLM_GPU_LANG STREQUAL "HIP")
|
||||
|
||||
list(APPEND VLLM_STABLE_EXT_SRC
|
||||
"csrc/libtorch_stable/cutlass_extensions/common.cpp"
|
||||
"csrc/libtorch_stable/quantization/w8a8/cutlass/scaled_mm_entry.cu"
|
||||
"csrc/libtorch_stable/quantization/fp4/nvfp4_quant_entry.cu"
|
||||
"csrc/libtorch_stable/quantization/fp4/nvfp4_scaled_mm_entry.cu"
|
||||
"${SCALED_MM_ENTRY_SRC}"
|
||||
"${NVFP4_QUANT_ENTRY_SRC}"
|
||||
"${NVFP4_SCALED_MM_ENTRY_SRC}"
|
||||
"csrc/libtorch_stable/quantization/awq/gemm_kernels.cu"
|
||||
"csrc/libtorch_stable/minimax_reduce_rms_kernel.cu")
|
||||
set_compile_definitions_for_srcs(
|
||||
SRCS "${VLLM_STABLE_TORCH_BINDINGS_SRC}"
|
||||
DEFINITIONS VLLM_COMPILED_CUDA_ARCHS=\"${VLLM_COMPILED_CUDA_ARCHS_STR}\")
|
||||
|
||||
#
|
||||
# Machete kernels
|
||||
@@ -547,11 +573,15 @@ if(VLLM_GPU_LANG STREQUAL "CUDA" OR VLLM_GPU_LANG STREQUAL "HIP")
|
||||
CUDA_ARCHS "${CUDA_ARCHS}")
|
||||
|
||||
if(COOPERATIVE_TOPK_ARCHS)
|
||||
set(COOPERATIVE_TOPK_SRC "csrc/libtorch_stable/cooperative_topk.cu")
|
||||
list(APPEND VLLM_STABLE_EXT_SRC
|
||||
"csrc/libtorch_stable/cooperative_topk.cu")
|
||||
"${COOPERATIVE_TOPK_SRC}")
|
||||
set_gencode_flags_for_srcs(
|
||||
SRCS "csrc/libtorch_stable/cooperative_topk.cu"
|
||||
SRCS "${COOPERATIVE_TOPK_SRC}"
|
||||
CUDA_ARCHS "${COOPERATIVE_TOPK_ARCHS}")
|
||||
set_compile_definitions_for_srcs(
|
||||
SRCS "${VLLM_STABLE_TORCH_BINDINGS_SRC};${COOPERATIVE_TOPK_SRC}"
|
||||
DEFINITIONS VLLM_ENABLE_COOPERATIVE_TOPK=1)
|
||||
endif()
|
||||
|
||||
# Only build Marlin kernels if we are building for at least some compatible archs.
|
||||
@@ -760,8 +790,10 @@ if(VLLM_GPU_LANG STREQUAL "CUDA" OR VLLM_GPU_LANG STREQUAL "HIP")
|
||||
set_gencode_flags_for_srcs(
|
||||
SRCS "${SCALED_MM_SM90_SRCS}"
|
||||
CUDA_ARCHS "${SCALED_MM_ARCHS}")
|
||||
set_compile_definitions_for_srcs(
|
||||
SRCS "${SCALED_MM_ENTRY_SRC};${SCALED_MM_SM90_SRCS}"
|
||||
DEFINITIONS ENABLE_SCALED_MM_SM90=1)
|
||||
list(APPEND VLLM_STABLE_EXT_SRC "${SCALED_MM_SM90_SRCS}")
|
||||
list(APPEND VLLM_GPU_FLAGS "-DENABLE_SCALED_MM_SM90=1")
|
||||
# Let scaled_mm_c2x know it doesn't need to build these arches
|
||||
list(APPEND SCALED_MM_3X_ARCHS "${SCALED_MM_ARCHS}")
|
||||
message(STATUS "Building scaled_mm_c3x_sm90 for archs: ${SCALED_MM_ARCHS}")
|
||||
@@ -794,8 +826,10 @@ if(VLLM_GPU_LANG STREQUAL "CUDA" OR VLLM_GPU_LANG STREQUAL "HIP")
|
||||
set_gencode_flags_for_srcs(
|
||||
SRCS "${SCALED_MM_SM120_SRCS}"
|
||||
CUDA_ARCHS "${SCALED_MM_ARCHS}")
|
||||
set_compile_definitions_for_srcs(
|
||||
SRCS "${SCALED_MM_ENTRY_SRC};${SCALED_MM_SM120_SRCS}"
|
||||
DEFINITIONS ENABLE_SCALED_MM_SM120=1)
|
||||
list(APPEND VLLM_STABLE_EXT_SRC "${SCALED_MM_SM120_SRCS}")
|
||||
list(APPEND VLLM_GPU_FLAGS "-DENABLE_SCALED_MM_SM120=1")
|
||||
# Let scaled_mm_c2x know it doesn't need to build these arches
|
||||
list(APPEND SCALED_MM_3X_ARCHS "${SCALED_MM_ARCHS}")
|
||||
message(STATUS "Building scaled_mm_c3x_sm120 for archs: ${SCALED_MM_ARCHS}")
|
||||
@@ -828,8 +862,10 @@ if(VLLM_GPU_LANG STREQUAL "CUDA" OR VLLM_GPU_LANG STREQUAL "HIP")
|
||||
set_gencode_flags_for_srcs(
|
||||
SRCS "${SCALED_MM_SM100_SRCS}"
|
||||
CUDA_ARCHS "${SCALED_MM_ARCHS}")
|
||||
set_compile_definitions_for_srcs(
|
||||
SRCS "${SCALED_MM_ENTRY_SRC};${SCALED_MM_SM100_SRCS}"
|
||||
DEFINITIONS ENABLE_SCALED_MM_SM100=1)
|
||||
list(APPEND VLLM_STABLE_EXT_SRC "${SCALED_MM_SM100_SRCS}")
|
||||
list(APPEND VLLM_GPU_FLAGS "-DENABLE_SCALED_MM_SM100=1")
|
||||
# Let scaled_mm_c2x know it doesn't need to build these arches
|
||||
list(APPEND SCALED_MM_3X_ARCHS "${SCALED_MM_ARCHS}")
|
||||
message(STATUS "Building scaled_mm_c3x_sm100 for archs: ${SCALED_MM_ARCHS}")
|
||||
@@ -858,8 +894,10 @@ if(VLLM_GPU_LANG STREQUAL "CUDA" OR VLLM_GPU_LANG STREQUAL "HIP")
|
||||
set_gencode_flags_for_srcs(
|
||||
SRCS "${SCALED_MM_C2X_SRCS}"
|
||||
CUDA_ARCHS "${SCALED_MM_2X_ARCHS}")
|
||||
set_compile_definitions_for_srcs(
|
||||
SRCS "${SCALED_MM_ENTRY_SRC};${SCALED_MM_C2X_SRCS}"
|
||||
DEFINITIONS ENABLE_SCALED_MM_C2X=1)
|
||||
list(APPEND VLLM_STABLE_EXT_SRC "${SCALED_MM_C2X_SRCS}")
|
||||
list(APPEND VLLM_GPU_FLAGS "-DENABLE_SCALED_MM_C2X=1")
|
||||
message(STATUS "Building scaled_mm_c2x for archs: ${SCALED_MM_2X_ARCHS}")
|
||||
else()
|
||||
if (SCALED_MM_3X_ARCHS)
|
||||
@@ -884,8 +922,10 @@ if(VLLM_GPU_LANG STREQUAL "CUDA" OR VLLM_GPU_LANG STREQUAL "HIP")
|
||||
set_gencode_flags_for_srcs(
|
||||
SRCS "${CUTLASS_MOE_SM90_SRCS}"
|
||||
CUDA_ARCHS "${SCALED_MM_ARCHS}")
|
||||
set_compile_definitions_for_srcs(
|
||||
SRCS "${SCALED_MM_ENTRY_SRC};${CUTLASS_MOE_SM90_SRCS}"
|
||||
DEFINITIONS ENABLE_CUTLASS_MOE_SM90=1)
|
||||
list(APPEND VLLM_STABLE_EXT_SRC "${CUTLASS_MOE_SM90_SRCS}")
|
||||
list(APPEND VLLM_GPU_FLAGS "-DENABLE_CUTLASS_MOE_SM90=1")
|
||||
message(STATUS "Building grouped_mm_c3x for archs: ${SCALED_MM_ARCHS}")
|
||||
else()
|
||||
if (NOT ${CMAKE_CUDA_COMPILER_VERSION} VERSION_GREATER_EQUAL 12.3 AND SCALED_MM_ARCHS)
|
||||
@@ -908,8 +948,13 @@ if(VLLM_GPU_LANG STREQUAL "CUDA" OR VLLM_GPU_LANG STREQUAL "HIP")
|
||||
set_gencode_flags_for_srcs(
|
||||
SRCS "${CUTLASS_MOE_SM100_SRCS}"
|
||||
CUDA_ARCHS "${SCALED_MM_ARCHS}")
|
||||
set_compile_definitions_for_srcs(
|
||||
SRCS "${SCALED_MM_ENTRY_SRC};${CUTLASS_MOE_SM100_SRCS}"
|
||||
DEFINITIONS ENABLE_CUTLASS_MOE_SM10X_OR_SM11X=1)
|
||||
list(APPEND VLLM_STABLE_EXT_SRC "${CUTLASS_MOE_SM100_SRCS}")
|
||||
list(APPEND VLLM_GPU_FLAGS "-DENABLE_CUTLASS_MOE_SM100=1")
|
||||
# The implementation is named sm100 historically, but it is built for the
|
||||
# SM10x/SM11x family: CUDA 12 Thor reports SM101, CUDA 13 Thor reports
|
||||
# SM110. Keep the compile-time macro aligned with runtime dispatch.
|
||||
message(STATUS "Building grouped_mm_c3x for archs: ${SCALED_MM_ARCHS}")
|
||||
else()
|
||||
if (NOT ${CMAKE_CUDA_COMPILER_VERSION} VERSION_GREATER_EQUAL 12.8 AND SCALED_MM_ARCHS)
|
||||
@@ -950,10 +995,16 @@ if(VLLM_GPU_LANG STREQUAL "CUDA" OR VLLM_GPU_LANG STREQUAL "HIP")
|
||||
# FP4/NVFP4 kernels (moved from _C to _C_stable_libtorch)
|
||||
#
|
||||
|
||||
# SM12x FP4 kernels. These share some generic NVFP4 quantization entry
|
||||
# sources with the SM10x/11x block below; set_gencode_flags_for_srcs appends
|
||||
# per-source flags, so shared files accumulate both SM12x and SM10x/11x
|
||||
# gencodes when both families are requested.
|
||||
# Shared FP4 implementation sources live next to the FP4 arch logic because
|
||||
# both SM12x and SM10x/11x append family-specific gencodes and feature macros
|
||||
# to them.
|
||||
set(FP4_SHARED_SRCS
|
||||
"csrc/libtorch_stable/quantization/fp4/nvfp4_quant_kernels.cu"
|
||||
"csrc/libtorch_stable/quantization/fp4/activation_nvfp4_quant_fusion_kernels.cu"
|
||||
"csrc/libtorch_stable/quantization/fp4/nvfp4_experts_quant.cu"
|
||||
"csrc/libtorch_stable/quantization/fp4/nvfp4_blockwise_moe_kernel.cu"
|
||||
"csrc/libtorch_stable/nvfp4_kv_cache_kernels.cu")
|
||||
|
||||
if(${CMAKE_CUDA_COMPILER_VERSION} VERSION_GREATER_EQUAL 13.0)
|
||||
cuda_archs_loose_intersection(FP4_SM120_ARCHS "12.0f" "${CUDA_ARCHS}")
|
||||
else()
|
||||
@@ -961,18 +1012,19 @@ if(VLLM_GPU_LANG STREQUAL "CUDA" OR VLLM_GPU_LANG STREQUAL "HIP")
|
||||
endif()
|
||||
if(${CMAKE_CUDA_COMPILER_VERSION} VERSION_GREATER_EQUAL 12.8 AND FP4_SM120_ARCHS)
|
||||
set(FP4_SM120_SRCS
|
||||
"csrc/libtorch_stable/quantization/fp4/nvfp4_quant_kernels.cu"
|
||||
"csrc/libtorch_stable/quantization/fp4/activation_nvfp4_quant_fusion_kernels.cu"
|
||||
"csrc/libtorch_stable/quantization/fp4/nvfp4_experts_quant.cu"
|
||||
${FP4_SHARED_SRCS}
|
||||
"csrc/libtorch_stable/quantization/fp4/nvfp4_scaled_mm_sm120_kernels.cu"
|
||||
"csrc/libtorch_stable/quantization/fp4/nvfp4_blockwise_moe_kernel.cu"
|
||||
"csrc/libtorch_stable/nvfp4_kv_cache_kernels.cu")
|
||||
)
|
||||
set_gencode_flags_for_srcs(
|
||||
SRCS "${FP4_SM120_SRCS}"
|
||||
CUDA_ARCHS "${FP4_SM120_ARCHS}")
|
||||
set_compile_definitions_for_srcs(
|
||||
SRCS "${NVFP4_QUANT_ENTRY_SRC};${NVFP4_SCALED_MM_ENTRY_SRC};${CACHE_KERNELS_SRC};${FP4_SM120_SRCS}"
|
||||
DEFINITIONS ENABLE_NVFP4_SM120=1)
|
||||
set_compile_definitions_for_srcs(
|
||||
SRCS "${SCALED_MM_ENTRY_SRC}"
|
||||
DEFINITIONS ENABLE_CUTLASS_MOE_SM120=1)
|
||||
list(APPEND VLLM_STABLE_EXT_SRC "${FP4_SM120_SRCS}")
|
||||
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}")
|
||||
else()
|
||||
message(STATUS "Not building SM12x NVFP4 as no compatible archs were found.")
|
||||
@@ -987,14 +1039,10 @@ if(VLLM_GPU_LANG STREQUAL "CUDA" OR VLLM_GPU_LANG STREQUAL "HIP")
|
||||
endif()
|
||||
if(${CMAKE_CUDA_COMPILER_VERSION} VERSION_GREATER_EQUAL 12.8 AND FP4_SM100_ARCHS)
|
||||
set(FP4_SM100_SRCS
|
||||
"csrc/libtorch_stable/quantization/fp4/nvfp4_quant_kernels.cu"
|
||||
"csrc/libtorch_stable/quantization/fp4/activation_nvfp4_quant_fusion_kernels.cu"
|
||||
"csrc/libtorch_stable/quantization/fp4/nvfp4_experts_quant.cu"
|
||||
${FP4_SHARED_SRCS}
|
||||
"csrc/libtorch_stable/quantization/fp4/nvfp4_scaled_mm_kernels.cu"
|
||||
"csrc/libtorch_stable/quantization/fp4/nvfp4_blockwise_moe_kernel.cu"
|
||||
"csrc/libtorch_stable/quantization/fp4/mxfp4_experts_quant.cu"
|
||||
"csrc/libtorch_stable/quantization/fp4/mxfp4_blockwise_moe_kernel.cu"
|
||||
"csrc/libtorch_stable/nvfp4_kv_cache_kernels.cu")
|
||||
"csrc/libtorch_stable/quantization/fp4/mxfp4_blockwise_moe_kernel.cu")
|
||||
if(NOT ${CMAKE_CUDA_COMPILER_VERSION} VERSION_GREATER_EQUAL 12.9)
|
||||
message(STATUS
|
||||
"Building mxfp4_experts_quant unsupported stubs because CUDA compiler version is not >= 12.9 (found ${CMAKE_CUDA_COMPILER_VERSION}).")
|
||||
@@ -1002,9 +1050,10 @@ if(VLLM_GPU_LANG STREQUAL "CUDA" OR VLLM_GPU_LANG STREQUAL "HIP")
|
||||
set_gencode_flags_for_srcs(
|
||||
SRCS "${FP4_SM100_SRCS}"
|
||||
CUDA_ARCHS "${FP4_SM100_ARCHS}")
|
||||
set_compile_definitions_for_srcs(
|
||||
SRCS "${NVFP4_QUANT_ENTRY_SRC};${NVFP4_SCALED_MM_ENTRY_SRC};${CACHE_KERNELS_SRC};${FP4_SM100_SRCS}"
|
||||
DEFINITIONS ENABLE_NVFP4_SM100=1)
|
||||
list(APPEND VLLM_STABLE_EXT_SRC "${FP4_SM100_SRCS}")
|
||||
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}")
|
||||
else()
|
||||
message(STATUS "Not building SM10x/11x NVFP4/MXFP4 as no compatible archs were found.")
|
||||
@@ -1058,7 +1107,6 @@ if(VLLM_GPU_LANG STREQUAL "CUDA" OR VLLM_GPU_LANG STREQUAL "HIP")
|
||||
SRCS "${CUTLASS_MLA_SRCS}"
|
||||
CUDA_ARCHS "${MLA_ARCHS}")
|
||||
list(APPEND VLLM_STABLE_EXT_SRC "${CUTLASS_MLA_SRCS}")
|
||||
list(APPEND VLLM_GPU_FLAGS "-DENABLE_CUTLASS_MLA=1")
|
||||
# Add MLA-specific include directories only to MLA source files
|
||||
set_source_files_properties(${CUTLASS_MLA_SRCS}
|
||||
PROPERTIES INCLUDE_DIRECTORIES "${CUTLASS_DIR}/examples/77_blackwell_fmha;${CUTLASS_DIR}/examples/common")
|
||||
@@ -1106,10 +1154,6 @@ if(VLLM_GPU_LANG STREQUAL "CUDA" OR VLLM_GPU_LANG STREQUAL "HIP")
|
||||
# Needed to use cuda/hip APIs from C-shim
|
||||
if(VLLM_GPU_LANG STREQUAL "CUDA")
|
||||
target_compile_definitions(_C_stable_libtorch PRIVATE USE_CUDA)
|
||||
if(COOPERATIVE_TOPK_ARCHS)
|
||||
target_compile_definitions(_C_stable_libtorch PRIVATE
|
||||
VLLM_ENABLE_COOPERATIVE_TOPK=1)
|
||||
endif()
|
||||
# Needed by CUTLASS kernels
|
||||
target_compile_definitions(_C_stable_libtorch PRIVATE
|
||||
CUTLASS_ENABLE_DIRECT_CUDA_DRIVER_CALL=1)
|
||||
|
||||
@@ -22,7 +22,7 @@ if(QUTLASS_SRC_DIR)
|
||||
set(qutlass_BINARY_DIR "${CMAKE_BINARY_DIR}/qutlass-binary-dir-unused")
|
||||
else()
|
||||
set(_QUTLASS_UPSTREAM_REPO "https://github.com/IST-DASLab/qutlass.git")
|
||||
set(_QUTLASS_UPSTREAM_TAG "830d2c4537c7396e14a02a46fbddd18b5d107c65")
|
||||
set(_QUTLASS_UPSTREAM_TAG "e74319e3405ce6d71965732880f5dc1f52371f64")
|
||||
|
||||
set(_qutlass_fc_root "${FETCHCONTENT_BASE_DIR}")
|
||||
if(NOT _qutlass_fc_root)
|
||||
@@ -125,8 +125,6 @@ if(${CMAKE_CUDA_COMPILER_VERSION} VERSION_GREATER_EQUAL 12.8 AND QUTLASS_ARCHS)
|
||||
CUDA_ARCHS "${QUTLASS_ARCHS}"
|
||||
)
|
||||
|
||||
# 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
|
||||
@@ -139,9 +137,11 @@ if(${CMAKE_CUDA_COMPILER_VERSION} VERSION_GREATER_EQUAL 12.8 AND QUTLASS_ARCHS)
|
||||
WITH_SOABI)
|
||||
|
||||
target_compile_definitions(_qutlass_C PRIVATE
|
||||
QUTLASS_DISABLE_PYBIND=1
|
||||
QUTLASS_MINIMAL_BUILD=1
|
||||
TARGET_CUDA_ARCH=${QUTLASS_TARGET_CC}
|
||||
CUTLASS_ENABLE_DIRECT_CUDA_DRIVER_CALL=1)
|
||||
CUTLASS_ENABLE_DIRECT_CUDA_DRIVER_CALL=1
|
||||
TORCH_TARGET_VERSION=0x020B000000000000ULL
|
||||
USE_CUDA)
|
||||
|
||||
set_property(SOURCE ${QUTLASS_SOURCES} APPEND PROPERTY COMPILE_OPTIONS
|
||||
$<$<COMPILE_LANGUAGE:CUDA>:--expt-relaxed-constexpr --use_fast_math -O3>
|
||||
|
||||
@@ -39,7 +39,7 @@ else()
|
||||
FetchContent_Declare(
|
||||
vllm-flash-attn
|
||||
GIT_REPOSITORY https://github.com/vllm-project/flash-attention.git
|
||||
GIT_TAG caaa4eb59845388a20b1f435ecaafb4bd9517ad8
|
||||
GIT_TAG 168920233059c48de6199e2cda74003b2ce3d199
|
||||
GIT_PROGRESS TRUE
|
||||
# Don't share the vllm-flash-attn build between build types
|
||||
BINARY_DIR ${CMAKE_BINARY_DIR}/vllm-flash-attn
|
||||
|
||||
@@ -344,6 +344,28 @@ macro(set_gencode_flags_for_srcs)
|
||||
endif()
|
||||
endmacro()
|
||||
|
||||
#
|
||||
# For a list of source files append preprocessor definitions to file-specific
|
||||
# compile options. Use this for optional kernel feature macros so toggling one
|
||||
# kernel family does not perturb the compile command for every source in the
|
||||
# extension target.
|
||||
#
|
||||
macro(set_compile_definitions_for_srcs)
|
||||
set(options)
|
||||
set(oneValueArgs)
|
||||
set(multiValueArgs SRCS DEFINITIONS)
|
||||
cmake_parse_arguments(arg "${options}" "${oneValueArgs}"
|
||||
"${multiValueArgs}" ${ARGN})
|
||||
|
||||
foreach(_DEF ${arg_DEFINITIONS})
|
||||
set_property(
|
||||
SOURCE ${arg_SRCS}
|
||||
APPEND PROPERTY
|
||||
COMPILE_DEFINITIONS "${_DEF}"
|
||||
)
|
||||
endforeach()
|
||||
endmacro()
|
||||
|
||||
#
|
||||
# For the given `SRC_CUDA_ARCHS` list of gencode versions in the form
|
||||
# `<major>.<minor>[letter]` compute the "loose intersection" with the
|
||||
|
||||
@@ -47,6 +47,8 @@ torch::stable::Tensor permute_cols(torch::stable::Tensor const& A,
|
||||
torch::stable::Tensor const& perm);
|
||||
|
||||
#ifndef USE_ROCM
|
||||
std::string get_compiled_cuda_archs();
|
||||
|
||||
bool cutlass_scaled_mm_supports_fp8(int64_t cuda_device_capability);
|
||||
bool cutlass_scaled_mm_supports_block_fp8(int64_t cuda_device_capability);
|
||||
bool cutlass_group_gemm_supported(int64_t cuda_device_capability);
|
||||
|
||||
@@ -39,11 +39,15 @@ __global__ void marlin_int4_fp8_preprocess_kernel_awq(
|
||||
// AWQ zeros: (size_k // group_size, size_n // 8)
|
||||
const int32_t* __restrict__ qzeros, int32_t size_n, int32_t size_k,
|
||||
int32_t group_size) {
|
||||
int32_t val =
|
||||
qweight[(blockIdx.x * 32 + threadIdx.x) * size_n / 8 + blockIdx.y];
|
||||
int32_t zero =
|
||||
qzeros[(blockIdx.x * 32 + threadIdx.x) / group_size * size_n / 8 +
|
||||
blockIdx.y];
|
||||
// Thread mapping: threadIdx.x -> column dim (coalesced read within a row),
|
||||
// blockIdx.x -> row dim. Adjacent threads read consecutive int32 in the
|
||||
// same row (stride 1) instead of striding across rows (stride size_n/8).
|
||||
int col = blockIdx.y * 32 + threadIdx.x;
|
||||
if (col >= size_n / 8) return;
|
||||
(void)size_k;
|
||||
|
||||
int32_t val = qweight[blockIdx.x * (size_n / 8) + col];
|
||||
int32_t zero = qzeros[blockIdx.x / group_size * (size_n / 8) + col];
|
||||
int32_t new_val = 0;
|
||||
|
||||
#pragma unroll
|
||||
@@ -58,7 +62,7 @@ __global__ void marlin_int4_fp8_preprocess_kernel_awq(
|
||||
zero >>= 4;
|
||||
}
|
||||
|
||||
output[(blockIdx.x * 32 + threadIdx.x) * size_n / 8 + blockIdx.y] = new_val;
|
||||
output[blockIdx.x * (size_n / 8) + col] = new_val;
|
||||
}
|
||||
|
||||
torch::stable::Tensor marlin_int4_fp8_preprocess(
|
||||
@@ -102,7 +106,7 @@ torch::stable::Tensor marlin_int4_fp8_preprocess(
|
||||
"qweight.size(0) % qzeros.size(0) != 0");
|
||||
STD_TORCH_CHECK(group_size % 8 == 0, "group_size % 8 != 0");
|
||||
|
||||
dim3 blocks(size_k / 32, size_n / 8);
|
||||
dim3 blocks(size_k, (size_n / 8 + 31) / 32);
|
||||
marlin_int4_fp8_preprocess_kernel_awq<<<blocks, 32, 0, stream>>>(
|
||||
reinterpret_cast<const int32_t*>(qweight.const_data_ptr()),
|
||||
reinterpret_cast<int32_t*>(output.mutable_data_ptr()),
|
||||
|
||||
@@ -51,7 +51,8 @@ void cutlass_moe_mm_sm90(torch::stable::Tensor& out_tensors,
|
||||
|
||||
#endif
|
||||
|
||||
#if defined ENABLE_CUTLASS_MOE_SM100 && ENABLE_CUTLASS_MOE_SM100
|
||||
#if defined ENABLE_CUTLASS_MOE_SM10X_OR_SM11X && \
|
||||
ENABLE_CUTLASS_MOE_SM10X_OR_SM11X
|
||||
void cutlass_moe_mm_sm100(torch::stable::Tensor& out_tensors,
|
||||
torch::stable::Tensor const& a_tensors,
|
||||
torch::stable::Tensor const& b_tensors,
|
||||
@@ -83,8 +84,9 @@ void cutlass_scaled_mm_sm100(torch::stable::Tensor& c,
|
||||
std::optional<torch::stable::Tensor> const& bias);
|
||||
#endif
|
||||
|
||||
#if (defined(ENABLE_CUTLASS_MOE_SM90) && ENABLE_CUTLASS_MOE_SM90) || \
|
||||
(defined(ENABLE_CUTLASS_MOE_SM100) && ENABLE_CUTLASS_MOE_SM100) || \
|
||||
#if (defined(ENABLE_CUTLASS_MOE_SM90) && ENABLE_CUTLASS_MOE_SM90) || \
|
||||
(defined(ENABLE_CUTLASS_MOE_SM10X_OR_SM11X) && \
|
||||
ENABLE_CUTLASS_MOE_SM10X_OR_SM11X) || \
|
||||
(defined(ENABLE_CUTLASS_MOE_SM120) && ENABLE_CUTLASS_MOE_SM120)
|
||||
void get_cutlass_moe_mm_data_caller(
|
||||
const torch::stable::Tensor& topk_ids,
|
||||
@@ -175,11 +177,14 @@ bool cutlass_scaled_mm_supports_block_fp8(int64_t cuda_device_capability) {
|
||||
|
||||
bool cutlass_group_gemm_supported(int64_t cuda_device_capability) {
|
||||
// CUTLASS grouped FP8 kernels need at least CUDA 12.3 and SM90 (Hopper)
|
||||
// or CUDA 12.8 and SM100 (Blackwell). Only report archs that have an
|
||||
// actual cutlass_moe_mm dispatch compiled into this file.
|
||||
// or CUDA 12.8 and SM10x/SM11x (Blackwell / Thor). CUDA 12 reports Thor as
|
||||
// SM101 while CUDA 13 reports it as SM110, but both use this sm100-named
|
||||
// implementation. Only report archs that have an actual cutlass_moe_mm
|
||||
// dispatch compiled into this file.
|
||||
|
||||
#if defined CUDA_VERSION
|
||||
#if defined ENABLE_CUTLASS_MOE_SM100 && ENABLE_CUTLASS_MOE_SM100
|
||||
#if defined ENABLE_CUTLASS_MOE_SM10X_OR_SM11X && \
|
||||
ENABLE_CUTLASS_MOE_SM10X_OR_SM11X
|
||||
if (cuda_device_capability >= 100 && cuda_device_capability < 120) {
|
||||
return CUDA_VERSION >= 12080;
|
||||
}
|
||||
@@ -281,8 +286,11 @@ void cutlass_moe_mm(torch::stable::Tensor& out_tensors,
|
||||
torch::stable::Tensor const& c_strides, bool per_act_token,
|
||||
bool per_out_ch) {
|
||||
int32_t version_num = get_sm_version_num();
|
||||
#if defined ENABLE_CUTLASS_MOE_SM100 && ENABLE_CUTLASS_MOE_SM100
|
||||
if (version_num >= 100 && version_num < 110) {
|
||||
#if defined ENABLE_CUTLASS_MOE_SM10X_OR_SM11X && \
|
||||
ENABLE_CUTLASS_MOE_SM10X_OR_SM11X
|
||||
// Keep runtime dispatch aligned with the CMake arch list and support query:
|
||||
// CUDA 12 Thor is SM101 and CUDA 13 Thor is SM110.
|
||||
if (version_num >= 100 && version_num < 120) {
|
||||
cutlass_moe_mm_sm100(out_tensors, a_tensors, b_tensors, a_scales, b_scales,
|
||||
expert_offsets, problem_sizes, a_strides, b_strides,
|
||||
c_strides, per_act_token, per_out_ch);
|
||||
@@ -316,8 +324,9 @@ void get_cutlass_moe_mm_data(
|
||||
// This function currently gets compiled only if we have a valid cutlass moe
|
||||
// mm to run it for.
|
||||
int32_t version_num = get_sm_version_num();
|
||||
#if (defined ENABLE_CUTLASS_MOE_SM90 && ENABLE_CUTLASS_MOE_SM90) || \
|
||||
(defined ENABLE_CUTLASS_MOE_SM100 && ENABLE_CUTLASS_MOE_SM100) || \
|
||||
#if (defined ENABLE_CUTLASS_MOE_SM90 && ENABLE_CUTLASS_MOE_SM90) || \
|
||||
(defined ENABLE_CUTLASS_MOE_SM10X_OR_SM11X && \
|
||||
ENABLE_CUTLASS_MOE_SM10X_OR_SM11X) || \
|
||||
(defined ENABLE_CUTLASS_MOE_SM120 && ENABLE_CUTLASS_MOE_SM120)
|
||||
get_cutlass_moe_mm_data_caller(topk_ids, expert_offsets, problem_sizes1,
|
||||
problem_sizes2, input_permutation,
|
||||
@@ -338,8 +347,9 @@ void get_cutlass_moe_mm_problem_sizes_from_expert_offsets(
|
||||
torch::stable::Tensor& problem_sizes2, const int64_t n, const int64_t k,
|
||||
const bool swap_ab) {
|
||||
int32_t version_num = get_sm_version_num();
|
||||
#if (defined ENABLE_CUTLASS_MOE_SM90 && ENABLE_CUTLASS_MOE_SM90) || \
|
||||
(defined ENABLE_CUTLASS_MOE_SM100 && ENABLE_CUTLASS_MOE_SM100) || \
|
||||
#if (defined ENABLE_CUTLASS_MOE_SM90 && ENABLE_CUTLASS_MOE_SM90) || \
|
||||
(defined ENABLE_CUTLASS_MOE_SM10X_OR_SM11X && \
|
||||
ENABLE_CUTLASS_MOE_SM10X_OR_SM11X) || \
|
||||
(defined ENABLE_CUTLASS_MOE_SM120 && ENABLE_CUTLASS_MOE_SM120)
|
||||
get_cutlass_moe_mm_problem_sizes_from_expert_offsets_caller(
|
||||
expert_first_token_offset, problem_sizes1, problem_sizes2, n, k, swap_ab);
|
||||
@@ -362,8 +372,9 @@ void get_cutlass_batched_moe_mm_data(
|
||||
// This function currently gets compiled only if we have a valid cutlass moe
|
||||
// mm to run it for.
|
||||
int32_t version_num = get_sm_version_num();
|
||||
#if (defined ENABLE_CUTLASS_MOE_SM90 && ENABLE_CUTLASS_MOE_SM90) || \
|
||||
(defined ENABLE_CUTLASS_MOE_SM100 && ENABLE_CUTLASS_MOE_SM100) || \
|
||||
#if (defined ENABLE_CUTLASS_MOE_SM90 && ENABLE_CUTLASS_MOE_SM90) || \
|
||||
(defined ENABLE_CUTLASS_MOE_SM10X_OR_SM11X && \
|
||||
ENABLE_CUTLASS_MOE_SM10X_OR_SM11X) || \
|
||||
(defined ENABLE_CUTLASS_MOE_SM120 && ENABLE_CUTLASS_MOE_SM120)
|
||||
get_cutlass_batched_moe_mm_data_caller(expert_offsets, problem_sizes1,
|
||||
problem_sizes2, expert_num_tokens,
|
||||
|
||||
@@ -4,6 +4,10 @@
|
||||
|
||||
#include <torch/csrc/stable/library.h>
|
||||
|
||||
#ifndef USE_ROCM
|
||||
std::string get_compiled_cuda_archs() { return VLLM_COMPILED_CUDA_ARCHS; }
|
||||
#endif
|
||||
|
||||
// Register ops with STABLE_TORCH_LIBRARY for libtorch stable ABI compatibility.
|
||||
// Note: We register under namespace "_C" so ops are accessible as
|
||||
// torch.ops._C.<op_name> for compatibility with existing code.
|
||||
@@ -30,6 +34,7 @@ STABLE_TORCH_LIBRARY_FRAGMENT(_C, ops) {
|
||||
ops.def("permute_cols(Tensor A, Tensor perm) -> Tensor");
|
||||
|
||||
ops.def("get_cuda_view_from_cpu_tensor(Tensor cpu_tensor) -> Tensor");
|
||||
ops.def("get_compiled_cuda_archs() -> str");
|
||||
|
||||
#ifndef USE_ROCM
|
||||
|
||||
@@ -763,6 +768,7 @@ STABLE_TORCH_LIBRARY_IMPL(_C_cuda_utils, CompositeExplicitAutograd,
|
||||
// ops.impl("op_name", &func) without a dispatch key in the non-stable API.
|
||||
STABLE_TORCH_LIBRARY_IMPL(_C, CompositeExplicitAutograd, ops) {
|
||||
#ifndef USE_ROCM
|
||||
ops.impl("get_compiled_cuda_archs", TORCH_BOX(&get_compiled_cuda_archs));
|
||||
ops.impl("cutlass_scaled_mm_supports_fp8",
|
||||
TORCH_BOX(&cutlass_scaled_mm_supports_fp8));
|
||||
ops.impl("cutlass_group_gemm_supported",
|
||||
|
||||
@@ -164,8 +164,8 @@ Priority is **1 = highest** (tried first).
|
||||
| `FLASHINFER` | XQA† | fp16, bf16 | `auto`, `float16`, `bfloat16`, `fp8`, `fp8_e4m3`, `fp8_e5m2` | 16, 32, 64, 128, 256, 512, 1024 | 64, 128, 256, 512 | ❌ | ❌ | ❌ | ✅ | Decoder | 9.0 |
|
||||
| `FLASHINFER` | trtllm-gen† | fp16, bf16 | `auto`, `float16`, `bfloat16`, `fp8`, `fp8_e4m3`, `fp8_e5m2`, `nvfp4` | 16, 32, 64, 128, 256, 512, 1024 | 64, 128, 256, 512 | ✅ | ✅ | ❌ | ✅ | Decoder | 10.x |
|
||||
| `FLASH_ATTN` | FA2* | fp16, bf16 | `auto`, `float16`, `bfloat16` | %16 | Any | ❌ | ✅ | ❌ | ✅ | All | ≥8.0 |
|
||||
| `FLASH_ATTN` | FA3* | fp16, bf16 | `auto`, `float16`, `bfloat16`, `fp8`, `fp8_e4m3`, `fp8_e5m2` | %16 | Any | ✅ | ✅ | ❌ | ✅ | All | 9.x |
|
||||
| `FLASH_ATTN` | FA4* | fp16, bf16 | `auto`, `float16`, `bfloat16` | %16 | Any | ✅ | ✅ | ❌ | ✅ | All | ≥10.0 |
|
||||
| `FLASH_ATTN` | FA3* | fp16, bf16 | `auto`, `float16`, `bfloat16`, `fp8`, `fp8_e4m3` | %16 | Any | ✅ | ✅ | ❌ | ✅ | All | 9.x |
|
||||
| `FLASH_ATTN` | FA4* | fp16, bf16 | `auto`, `float16`, `bfloat16`, `fp8`, `fp8_e4m3` | %16 | Any | ✅ | ✅ | ❌ | ✅ | All | ≥10.0 |
|
||||
| `FLASH_ATTN_DIFFKV` | | fp16, bf16 | `auto` | Any | Any | ❌ | ❌ | ❌ | ✅ | Decoder | Any |
|
||||
| `FLEX_ATTENTION` | | fp16, bf16, fp32 | `auto`, `float16`, `bfloat16` | %16 | Any | ❌ | ✅ | ✅ | ❌ | Decoder, Encoder Only | Any |
|
||||
| `HPC_ATTN` | | fp16, bf16 | `auto`, `bfloat16`, `fp8_e4m3` | 64 | 128 | ❌ | ❌ | ❌ | ❌ | Decoder | ≥9.0 |
|
||||
|
||||
@@ -75,7 +75,7 @@ vllm serve <model> \
|
||||
| `max_tracker_size` | no | `64000` | single-tier | Max entries in the lookup tracker. |
|
||||
| `secondary_tiers` | no | `[]` | multi-tier | List of secondary tier configs (see below). |
|
||||
| `offload_prompt_only` | no | `true` | both | If `true`, only prompt (prefill) blocks are offloaded; decode blocks are skipped. |
|
||||
| `self_describing_kv_events` | no | `false` | single-tier | Opt-in. When `true` *and* KV cache events are enabled (`--kv-events-config` with `enable_kv_cache_events`), the connector emits self-describing block-granular `BlockStored`/`BlockRemoved` payloads (constituent block hashes, whole-chunk `token_ids`, per-block `block_size`, parent hash, LoRA + group/cache-spec metadata) instead of the placeholder fallback, so external KV-event consumers can index offloaded blocks. Inert unless events are enabled. Currently rejected by `TieringOffloadingSpec`. Full-attention groups only; sliding-window/SSM groups keep the placeholder fallback. In chunk mode (`block_size` > GPU block size, or `blocks_per_chunk` > 1), overlapping chunks re-announce shared per-block hashes, so consumers must reference-count (deduplicate) repeated store/remove announcements. |
|
||||
| `self_describing_kv_events` | no | `false` | both | Opt-in. When `true` *and* KV cache events are enabled (`--kv-events-config` with `enable_kv_cache_events`), the connector emits self-describing block-granular `BlockStored`/`BlockRemoved` payloads (constituent block hashes, whole-chunk `token_ids`, per-block `block_size`, parent hash, LoRA + group/cache-spec metadata) instead of the placeholder fallback, so external KV-event consumers can index offloaded blocks. Inert unless events are enabled. With `TieringOffloadingSpec`, a CPU promotion is self-describing when a local request observes its primary-tier `HIT` before event translation; otherwise its stored event may retain the placeholder, while a later `HIT` can backfill metadata for removal. Pending-removal/re-promotion races and externally initiated promotions may also produce placeholders, and consumers must ignore removals for unknown hashes. Full-attention groups only; sliding-window/SSM groups keep the placeholder fallback. In chunk mode (`block_size` > GPU block size, or `blocks_per_chunk` > 1), overlapping chunks re-announce shared per-block hashes, so consumers must reference-count (deduplicate) repeated store/remove announcements. |
|
||||
| `spec_module_path` | no | — | both | Python import path for a custom `OffloadingSpec` not in the built-in registry. Required only when `spec_name` is not built-in (advanced). |
|
||||
|
||||
## Secondary Tiers
|
||||
|
||||
@@ -46,6 +46,26 @@ export CPU_ARCH=$(uname -m) # x86_64 or aarch64
|
||||
uv pip install https://github.com/vllm-project/vllm/releases/download/v${VLLM_VERSION}/vllm-${VLLM_VERSION}+cu${CUDA_VERSION}-cp38-abi3-manylinux_2_28_${CPU_ARCH}.whl --extra-index-url https://download.pytorch.org/whl/cu${CUDA_VERSION}
|
||||
```
|
||||
|
||||
!!! warning "CUDA architecture coverage"
|
||||
|
||||
Pre-built CUDA wheels are compiled for the CUDA architectures selected by
|
||||
vLLM's release and build pipelines. This list is intentionally smaller than
|
||||
every architecture CMake can build, because each additional architecture
|
||||
increases wheel size.
|
||||
|
||||
In particular, CUDA 12.9 wheels do not use CUDA 13 family-specific targets.
|
||||
To keep wheel size bounded, published CUDA 12.9 wheels may omit some newer
|
||||
architecture-specific Blackwell/Thor targets, such as `sm_103` or
|
||||
`sm_121`, even though vLLM can build them from source. CUDA 13 wheels use
|
||||
family-specific targets such as `sm_100f`, `sm_110f`, and `sm_120f`, which
|
||||
cover the corresponding major-version GPU family.
|
||||
|
||||
If vLLM logs a warning that your visible CUDA device is not covered by the
|
||||
wheel's compiled CUDA architectures, or if you see a CUDA error such as
|
||||
`no kernel image is available for execution on the device`, install a CUDA
|
||||
13 wheel when possible, or build from source with a `TORCH_CUDA_ARCH_LIST`
|
||||
that includes your GPU.
|
||||
|
||||
#### Install the latest code
|
||||
|
||||
LLM inference is a fast-evolving field, and the latest code may contain bug fixes, performance improvements, and new features that are not released yet. To allow users to try the latest code without waiting for the next release, vLLM provides wheels for every commit since `v0.5.3` on <https://wheels.vllm.ai/nightly>. There are multiple indices that could be used:
|
||||
|
||||
@@ -67,7 +67,7 @@ The Transcriptions API supports uploading audio files in various formats includi
|
||||
- `response_format`: Format of the response ("json", "text") (optional)
|
||||
- `temperature`: Sampling temperature between 0 and 1 (optional)
|
||||
|
||||
For the complete list of supported parameters including sampling parameters and vLLM extensions, see the [protocol definitions](https://github.com/vllm-project/vllm/blob/main/vllm/entrypoints/openai/protocol.py#L2182).
|
||||
For the complete list of supported parameters including sampling parameters and vLLM extensions, see the [protocol definitions](https://github.com/vllm-project/vllm/blob/main/vllm/entrypoints/speech_to_text/transcription/protocol.py).
|
||||
|
||||
**Response Format:**
|
||||
|
||||
|
||||
@@ -174,6 +174,33 @@ If the script runs successfully, you should see the message `sanity check is suc
|
||||
|
||||
If the test script hangs or crashes, usually it means the hardware/drivers are broken in some sense. You should try to contact your system administrator or hardware vendor for further assistance. As a common workaround, you can try to tune some NCCL environment variables, such as `export NCCL_P2P_DISABLE=1` to see if it helps. Please check [their documentation](https://docs.nvidia.com/deeplearning/nccl/user-guide/docs/env.html) for more information. Please only use these environment variables as a temporary workaround, as they might affect the performance of the system. The best solution is still to fix the hardware/drivers so that the test script can run successfully.
|
||||
|
||||
## CUDA architecture not covered by the wheel
|
||||
|
||||
If vLLM logs a warning that the current wheel was built for a set of CUDA
|
||||
architectures but one of your visible CUDA devices is not covered, the installed
|
||||
wheel may not contain native CUDA kernels for that GPU. The same issue may also
|
||||
surface later as a CUDA runtime error such as `no kernel image is available for
|
||||
execution on the device`.
|
||||
|
||||
The architecture list in a pre-built wheel is determined by the vLLM release and
|
||||
build pipelines, then filtered by CMake before individual kernels choose their
|
||||
own per-kernel architectures. It is not the same as the full set of
|
||||
architectures that vLLM can build from source.
|
||||
|
||||
CUDA 12.9 wheels use architecture-specific targets for newer NVIDIA GPUs. To
|
||||
keep wheel size bounded, published CUDA 12.9 wheels may omit some newer
|
||||
Blackwell/Thor targets such as `sm_103` or `sm_121`, even though vLLM can build
|
||||
them from source. CUDA 13 wheels can use family-specific targets such as
|
||||
`sm_100f`, `sm_110f`, and `sm_120f`, which cover the corresponding
|
||||
major-version GPU family.
|
||||
|
||||
If you see this warning or error, install a CUDA 13 wheel when possible.
|
||||
Otherwise, build vLLM from source and set `TORCH_CUDA_ARCH_LIST` to include
|
||||
your GPU's compute capability. See the CUDA installation guide's
|
||||
[pre-built wheels](../getting_started/installation/gpu.md#pre-built-wheels) and
|
||||
[build wheel from source](../getting_started/installation/gpu.md#build-wheel-from-source)
|
||||
sections for details.
|
||||
|
||||
## Python multiprocessing
|
||||
|
||||
### `RuntimeError` Exception
|
||||
|
||||
Generated
+2
-2
@@ -6321,9 +6321,9 @@ checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9"
|
||||
|
||||
[[package]]
|
||||
name = "xgrammar-structural-tag"
|
||||
version = "0.1.0+xgrammar.0.2.2.4d145cc"
|
||||
version = "0.2.0+xgrammar.0.2.4.dd729e7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2436dea2393d55a3b188588aa300c5a8afe8f45a77da52c611fb4498a6c876e6"
|
||||
checksum = "d4d24c842efc3c24e9756aa426d530cbdac0980e49af223cb384e276e981ca0a"
|
||||
dependencies = [
|
||||
"auto_impl",
|
||||
"serde",
|
||||
|
||||
+1
-1
@@ -143,7 +143,7 @@ vllm-server = { path = "src/server" }
|
||||
vllm-text = { path = "src/text" }
|
||||
vllm-tokenizer = { path = "src/tokenizer" }
|
||||
winnow = { version = "1.0.2", features = ["simd"] }
|
||||
xgrammar-structural-tag = "0.1.0"
|
||||
xgrammar-structural-tag = "0.2.0"
|
||||
zeromq = { version = "0.6.0", default-features = false, features = [
|
||||
"tokio-runtime",
|
||||
"all-transport",
|
||||
|
||||
@@ -74,7 +74,7 @@ impl DefaultChatOutputProcessor {
|
||||
Box::new(CombinedParser::new(reasoning_parser, tool_parser)) as Box<dyn UnifiedParser>
|
||||
};
|
||||
|
||||
apply_structural_tag_constraint(request, parser.structural_tag_model())?;
|
||||
apply_structural_tag_constraint(request, parser.structural_tag_builder())?;
|
||||
|
||||
if parser.preserve_special_tokens() {
|
||||
request.decode_options.skip_special_tokens = false;
|
||||
|
||||
@@ -7,7 +7,8 @@ use thiserror_ext::AsReport;
|
||||
use vllm_engine_core_client::protocol::structured_outputs::{
|
||||
StructuredOutputBackend, StructuredOutputsParams,
|
||||
};
|
||||
use vllm_parser::tool::StructuralTagModel;
|
||||
use vllm_parser::tool::StructuralTagBuilder;
|
||||
use xgrammar_structural_tag::builders::StructuralTagOptions;
|
||||
use xgrammar_structural_tag::{
|
||||
FunctionDefinition, FunctionToolParam, ToolChoice as StructuralTagToolChoice, ToolParam,
|
||||
build_structural_tag,
|
||||
@@ -20,9 +21,9 @@ use crate::{Error, Result as ChatResult};
|
||||
/// support and the request's tool choice.
|
||||
pub(super) fn apply_structural_tag_constraint(
|
||||
request: &mut ChatRequest,
|
||||
model: Option<StructuralTagModel>,
|
||||
builder: Option<&dyn StructuralTagBuilder>,
|
||||
) -> ChatResult<()> {
|
||||
let Some(model) = model else {
|
||||
let Some(builder) = builder else {
|
||||
return Ok(());
|
||||
};
|
||||
let Some(tool_choice) = structural_tag_tool_choice(request) else {
|
||||
@@ -42,11 +43,16 @@ pub(super) fn apply_structural_tag_constraint(
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let structural_tag = build_structural_tag(model, &tools, tool_choice, false)
|
||||
.and_then(|tag| tag.to_json_string())
|
||||
.map_err(|error| Error::StructuralTag {
|
||||
message: error.to_report_string(),
|
||||
})?;
|
||||
let structural_tag = build_structural_tag(
|
||||
builder,
|
||||
&tools,
|
||||
tool_choice,
|
||||
StructuralTagOptions::default().with_reasoning(false),
|
||||
)
|
||||
.and_then(|tag| tag.to_json_string())
|
||||
.map_err(|error| Error::StructuralTag {
|
||||
message: error.to_report_string(),
|
||||
})?;
|
||||
|
||||
// Overwrite any existing structured output settings with the structural tag constraint.
|
||||
request.sampling_params.structured_outputs = Some(StructuredOutputsParams {
|
||||
@@ -141,7 +147,7 @@ mod tests {
|
||||
let mut request = request(ChatToolChoice::Auto, vec![chat_tool("search", Some(true))]);
|
||||
let parser = qwen3_coder_parser(&request.tools);
|
||||
|
||||
apply_structural_tag_constraint(&mut request, parser.structural_tag_model())
|
||||
apply_structural_tag_constraint(&mut request, parser.structural_tag_builder())
|
||||
.expect("structural tag should build");
|
||||
|
||||
let tag = structural_tag_value(&request);
|
||||
@@ -154,7 +160,7 @@ mod tests {
|
||||
let mut request = request(ChatToolChoice::Auto, vec![chat_tool("search", None)]);
|
||||
let parser = qwen3_coder_parser(&request.tools);
|
||||
|
||||
apply_structural_tag_constraint(&mut request, parser.structural_tag_model())
|
||||
apply_structural_tag_constraint(&mut request, parser.structural_tag_builder())
|
||||
.expect("structural tag decision should succeed");
|
||||
|
||||
assert!(request.sampling_params.structured_outputs.is_none());
|
||||
@@ -169,7 +175,7 @@ mod tests {
|
||||
});
|
||||
let parser = qwen3_coder_parser(&request.tools);
|
||||
|
||||
apply_structural_tag_constraint(&mut request, parser.structural_tag_model())
|
||||
apply_structural_tag_constraint(&mut request, parser.structural_tag_builder())
|
||||
.expect("structural tag should build");
|
||||
|
||||
let params = structured_outputs(&request);
|
||||
@@ -184,7 +190,7 @@ mod tests {
|
||||
let mut request = request(ChatToolChoice::Required, vec![chat_tool("search", None)]);
|
||||
let parser = qwen3_coder_parser(&request.tools);
|
||||
|
||||
apply_structural_tag_constraint(&mut request, parser.structural_tag_model())
|
||||
apply_structural_tag_constraint(&mut request, parser.structural_tag_builder())
|
||||
.expect("structural tag should build");
|
||||
|
||||
let tag = structural_tag_value(&request);
|
||||
@@ -201,7 +207,7 @@ mod tests {
|
||||
});
|
||||
let parser = qwen3_coder_parser(&request.tools);
|
||||
|
||||
apply_structural_tag_constraint(&mut request, parser.structural_tag_model())
|
||||
apply_structural_tag_constraint(&mut request, parser.structural_tag_builder())
|
||||
.expect("structural tag should build");
|
||||
|
||||
let params = structured_outputs(&request);
|
||||
@@ -221,7 +227,7 @@ mod tests {
|
||||
);
|
||||
let parser = qwen3_coder_parser(&request.tools);
|
||||
|
||||
apply_structural_tag_constraint(&mut request, parser.structural_tag_model())
|
||||
apply_structural_tag_constraint(&mut request, parser.structural_tag_builder())
|
||||
.expect("structural tag should build");
|
||||
|
||||
let tag = structural_tag_value(&request).to_string();
|
||||
@@ -234,7 +240,7 @@ mod tests {
|
||||
let mut request = request(ChatToolChoice::None, vec![chat_tool("search", Some(true))]);
|
||||
let parser = qwen3_coder_parser(&request.tools);
|
||||
|
||||
apply_structural_tag_constraint(&mut request, parser.structural_tag_model())
|
||||
apply_structural_tag_constraint(&mut request, parser.structural_tag_builder())
|
||||
.expect("structural tag decision should succeed");
|
||||
|
||||
assert!(request.sampling_params.structured_outputs.is_none());
|
||||
@@ -249,7 +255,7 @@ mod tests {
|
||||
});
|
||||
let parser = qwen3_coder_parser(&request.tools);
|
||||
|
||||
apply_structural_tag_constraint(&mut request, parser.structural_tag_model())
|
||||
apply_structural_tag_constraint(&mut request, parser.structural_tag_builder())
|
||||
.expect("structural tag decision should succeed");
|
||||
|
||||
let params = structured_outputs(&request);
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use vllm_parser::tool::{
|
||||
Result, StructuralTagModel, Tool, ToolParser, ToolParserError, ToolParserOutput,
|
||||
Result, StructuralTagBuilder, Tool, ToolParser, ToolParserError, ToolParserOutput,
|
||||
};
|
||||
use vllm_parser::unified::{
|
||||
UnifiedParser, UnifiedParserError, UnifiedParserEvent, UnifiedParserOutput,
|
||||
@@ -85,8 +85,8 @@ impl<T: UnifiedParser> ToolParser for UnifiedToolParserAdapter<T> {
|
||||
self.inner.preserve_special_tokens()
|
||||
}
|
||||
|
||||
fn structural_tag_model(&self) -> Option<StructuralTagModel> {
|
||||
self.inner.structural_tag_model()
|
||||
fn structural_tag_builder(&self) -> Option<&dyn StructuralTagBuilder> {
|
||||
self.inner.structural_tag_builder()
|
||||
}
|
||||
|
||||
fn tool_call_id(&self, tool_index: usize) -> Option<&str> {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
use super::{DeepSeekDsmlToolParser, DsmlTokens};
|
||||
use crate::tool::{Result, StructuralTagModel, Tool, ToolParser, ToolParserOutput};
|
||||
use crate::tool::{Result, StructuralTagBuilder, Tool, ToolParser, ToolParserOutput};
|
||||
|
||||
/// Tool parser for DeepSeek V3.2 models.
|
||||
///
|
||||
@@ -47,8 +47,8 @@ impl ToolParser for DeepSeekV32ToolParser {
|
||||
true
|
||||
}
|
||||
|
||||
fn structural_tag_model(&self) -> Option<StructuralTagModel> {
|
||||
Some(StructuralTagModel::DeepSeekV32)
|
||||
fn structural_tag_builder(&self) -> Option<&dyn StructuralTagBuilder> {
|
||||
Some(xgrammar_structural_tag::Model::DeepSeekV32.builder())
|
||||
}
|
||||
|
||||
fn parse_into(&mut self, chunk: &str, output: &mut ToolParserOutput) -> Result<()> {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
use super::{DeepSeekDsmlToolParser, DsmlTokens};
|
||||
use crate::tool::{Result, StructuralTagModel, Tool, ToolParser, ToolParserOutput};
|
||||
use crate::tool::{Result, StructuralTagBuilder, Tool, ToolParser, ToolParserOutput};
|
||||
|
||||
/// Tool parser for DeepSeek V4 models.
|
||||
///
|
||||
@@ -50,8 +50,8 @@ impl ToolParser for DeepSeekV4ToolParser {
|
||||
true
|
||||
}
|
||||
|
||||
fn structural_tag_model(&self) -> Option<StructuralTagModel> {
|
||||
Some(StructuralTagModel::DeepSeekV4)
|
||||
fn structural_tag_builder(&self) -> Option<&dyn StructuralTagBuilder> {
|
||||
Some(xgrammar_structural_tag::Model::DeepSeekV4.builder())
|
||||
}
|
||||
|
||||
fn parse_into(&mut self, chunk: &str, output: &mut ToolParserOutput) -> Result<()> {
|
||||
@@ -73,7 +73,7 @@ mod tests {
|
||||
|
||||
use super::DeepSeekV4ToolParser;
|
||||
use crate::tool::test_utils::{collect_stream, test_tools};
|
||||
use crate::tool::{StructuralTagModel, ToolParser, ToolParserTestExt as _};
|
||||
use crate::tool::{ToolParser, ToolParserTestExt as _};
|
||||
|
||||
fn build_tool_call(function_name: &str, params: &[(&str, &str)]) -> String {
|
||||
let params = params
|
||||
@@ -91,13 +91,10 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deepseek_v4_exposes_structural_tag_model() {
|
||||
fn deepseek_v4_exposes_structural_tag_builder() {
|
||||
let parser = DeepSeekV4ToolParser::new(&test_tools());
|
||||
|
||||
assert_eq!(
|
||||
parser.structural_tag_model(),
|
||||
Some(StructuralTagModel::DeepSeekV4)
|
||||
);
|
||||
assert!(parser.structural_tag_builder().is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
use super::{DeepSeekJsonFormat, DeepSeekJsonToolParser};
|
||||
use crate::tool::{Result, StructuralTagModel, Tool, ToolParser, ToolParserOutput};
|
||||
use crate::tool::{Result, StructuralTagBuilder, Tool, ToolParser, ToolParserOutput};
|
||||
|
||||
/// Tool parser for DeepSeek V3 JSON-fenced tool calls.
|
||||
///
|
||||
@@ -35,8 +35,8 @@ impl ToolParser for DeepSeekV3ToolParser {
|
||||
Ok(Box::new(Self::new(tools)))
|
||||
}
|
||||
|
||||
fn structural_tag_model(&self) -> Option<StructuralTagModel> {
|
||||
Some(StructuralTagModel::DeepSeekR1)
|
||||
fn structural_tag_builder(&self) -> Option<&dyn StructuralTagBuilder> {
|
||||
Some(xgrammar_structural_tag::Model::DeepSeekR1.builder())
|
||||
}
|
||||
|
||||
fn parse_into(&mut self, chunk: &str, output: &mut ToolParserOutput) -> Result<()> {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
use super::{DeepSeekJsonFormat, DeepSeekJsonToolParser};
|
||||
use crate::tool::{Result, StructuralTagModel, Tool, ToolParser, ToolParserOutput};
|
||||
use crate::tool::{Result, StructuralTagBuilder, Tool, ToolParser, ToolParserOutput};
|
||||
|
||||
/// Tool parser for DeepSeek V3.1 raw JSON tool calls.
|
||||
///
|
||||
@@ -31,8 +31,8 @@ impl ToolParser for DeepSeekV31ToolParser {
|
||||
Ok(Box::new(Self::new(tools)))
|
||||
}
|
||||
|
||||
fn structural_tag_model(&self) -> Option<StructuralTagModel> {
|
||||
Some(StructuralTagModel::DeepSeekV31)
|
||||
fn structural_tag_builder(&self) -> Option<&dyn StructuralTagBuilder> {
|
||||
Some(xgrammar_structural_tag::Model::DeepSeekV31.builder())
|
||||
}
|
||||
|
||||
fn parse_into(&mut self, chunk: &str, output: &mut ToolParserOutput) -> Result<()> {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
use super::{GlmXmlToolParser, Separator};
|
||||
use crate::tool::{Result, StructuralTagModel, Tool, ToolParser, ToolParserOutput};
|
||||
use crate::tool::{Result, StructuralTagBuilder, Tool, ToolParser, ToolParserOutput};
|
||||
|
||||
/// Tool parser for GLM-4.7 MoE XML-style tool calls.
|
||||
///
|
||||
@@ -25,8 +25,8 @@ impl ToolParser for Glm47MoeToolParser {
|
||||
Ok(Box::new(Self::new(tools)))
|
||||
}
|
||||
|
||||
fn structural_tag_model(&self) -> Option<StructuralTagModel> {
|
||||
Some(StructuralTagModel::Glm47)
|
||||
fn structural_tag_builder(&self) -> Option<&dyn StructuralTagBuilder> {
|
||||
Some(xgrammar_structural_tag::Model::Glm47.builder())
|
||||
}
|
||||
|
||||
fn parse_into(&mut self, chunk: &str, output: &mut ToolParserOutput) -> Result<()> {
|
||||
|
||||
@@ -10,7 +10,7 @@ use winnow::token::{literal, rest, take_until};
|
||||
use super::parameters::ToolSchemas;
|
||||
use super::utils::{MarkerScanState, parse_buffered_event, safe_text_len, take_until_marker};
|
||||
use super::{Result, ToolCallDelta, ToolParser, ToolParserOutput};
|
||||
use crate::tool::{StructuralTagModel, Tool};
|
||||
use crate::tool::{StructuralTagBuilder, Tool};
|
||||
|
||||
const TOOL_CALLS_START: &str = "<tool_calls>";
|
||||
const TOOL_CALLS_END: &str = "</tool_calls>";
|
||||
@@ -116,8 +116,8 @@ impl ToolParser for HyV3ToolParser {
|
||||
Ok(Box::new(Self::new(tools)))
|
||||
}
|
||||
|
||||
fn structural_tag_model(&self) -> Option<StructuralTagModel> {
|
||||
Some(StructuralTagModel::HyV3)
|
||||
fn structural_tag_builder(&self) -> Option<&dyn StructuralTagBuilder> {
|
||||
Some(xgrammar_structural_tag::Model::HyV3.builder())
|
||||
}
|
||||
|
||||
fn parse_into(&mut self, chunk: &str, output: &mut ToolParserOutput) -> Result<()> {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
use super::{JsonToolCallConfig, JsonToolCallParser, JsonToolCallWhitespace};
|
||||
use crate::tool::{Result, StructuralTagModel, Tool, ToolParser, ToolParserOutput};
|
||||
use crate::tool::{Result, StructuralTagBuilder, Tool, ToolParser, ToolParserOutput};
|
||||
|
||||
const HERMES_CONFIG: JsonToolCallConfig = JsonToolCallConfig {
|
||||
parser_name: "Hermes",
|
||||
@@ -48,8 +48,8 @@ impl ToolParser for HermesToolParser {
|
||||
Ok(Box::new(Self::new(tools)))
|
||||
}
|
||||
|
||||
fn structural_tag_model(&self) -> Option<StructuralTagModel> {
|
||||
Some(StructuralTagModel::Hermes)
|
||||
fn structural_tag_builder(&self) -> Option<&dyn StructuralTagBuilder> {
|
||||
Some(xgrammar_structural_tag::Model::Hermes.builder())
|
||||
}
|
||||
|
||||
fn parse_into(&mut self, chunk: &str, output: &mut ToolParserOutput) -> Result<()> {
|
||||
|
||||
@@ -12,7 +12,9 @@ use super::{
|
||||
argument_delta_event, tool_call_header_event,
|
||||
};
|
||||
use crate::tool::utils::{JsonObjectScanState, parse_buffered_event};
|
||||
use crate::tool::{Result, StructuralTagModel, Tool, ToolCallDelta, ToolParser, ToolParserOutput};
|
||||
use crate::tool::{
|
||||
Result, StructuralTagBuilder, Tool, ToolCallDelta, ToolParser, ToolParserOutput,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
enum LlamaJsonMode {
|
||||
@@ -136,8 +138,8 @@ impl ToolParser for Llama3JsonToolParser {
|
||||
Ok(Box::new(Self::new(tools)))
|
||||
}
|
||||
|
||||
fn structural_tag_model(&self) -> Option<StructuralTagModel> {
|
||||
Some(StructuralTagModel::Llama)
|
||||
fn structural_tag_builder(&self) -> Option<&dyn StructuralTagBuilder> {
|
||||
Some(xgrammar_structural_tag::Model::Llama.builder())
|
||||
}
|
||||
|
||||
fn parse_into(&mut self, chunk: &str, output: &mut ToolParserOutput) -> Result<()> {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
use super::{JsonToolCallConfig, JsonToolCallParser, JsonToolCallWhitespace};
|
||||
use crate::tool::{Result, StructuralTagModel, Tool, ToolParser, ToolParserOutput};
|
||||
use crate::tool::{Result, StructuralTagBuilder, Tool, ToolParser, ToolParserOutput};
|
||||
|
||||
const QWEN_XML_CONFIG: JsonToolCallConfig = JsonToolCallConfig {
|
||||
parser_name: "Qwen XML",
|
||||
@@ -50,8 +50,8 @@ impl ToolParser for Qwen3XmlToolParser {
|
||||
Ok(Box::new(Self::new(tools)))
|
||||
}
|
||||
|
||||
fn structural_tag_model(&self) -> Option<StructuralTagModel> {
|
||||
Some(StructuralTagModel::Qwen3)
|
||||
fn structural_tag_builder(&self) -> Option<&dyn StructuralTagBuilder> {
|
||||
Some(xgrammar_structural_tag::Model::Qwen3.builder())
|
||||
}
|
||||
|
||||
fn parse_into(&mut self, chunk: &str, output: &mut ToolParserOutput) -> Result<()> {
|
||||
|
||||
@@ -11,7 +11,7 @@ use winnow::token::{literal, rest, take_until, take_while};
|
||||
|
||||
use super::utils::{JsonObjectScanState, parse_buffered_event, safe_text_len, take_json_object};
|
||||
use super::{Result, ToolCallDelta, ToolParser, ToolParserOutput};
|
||||
use crate::tool::{StructuralTagModel, Tool};
|
||||
use crate::tool::{StructuralTagBuilder, Tool};
|
||||
|
||||
const TOOL_CALLS_START: &str = "<|tool_calls_section_begin|>";
|
||||
const TOOL_CALLS_END: &str = "<|tool_calls_section_end|>";
|
||||
@@ -150,8 +150,8 @@ impl ToolParser for KimiK2ToolParser {
|
||||
true
|
||||
}
|
||||
|
||||
fn structural_tag_model(&self) -> Option<StructuralTagModel> {
|
||||
Some(StructuralTagModel::Kimi)
|
||||
fn structural_tag_builder(&self) -> Option<&dyn StructuralTagBuilder> {
|
||||
Some(xgrammar_structural_tag::Model::Kimi.builder())
|
||||
}
|
||||
|
||||
fn tool_call_id(&self, tool_index: usize) -> Option<&str> {
|
||||
|
||||
@@ -10,7 +10,7 @@ use winnow::token::{literal, rest, take_until};
|
||||
use super::parameters::ToolSchemas;
|
||||
use super::utils::{MarkerScanState, parse_buffered_event, safe_text_len, take_until_marker};
|
||||
use super::{Result, ToolCallDelta, ToolParser, ToolParserOutput};
|
||||
use crate::tool::{StructuralTagModel, Tool};
|
||||
use crate::tool::{StructuralTagBuilder, Tool};
|
||||
|
||||
const TOOL_CALL_START: &str = "<minimax:tool_call>";
|
||||
const TOOL_CALL_END: &str = "</minimax:tool_call>";
|
||||
@@ -115,8 +115,8 @@ impl ToolParser for MinimaxM2ToolParser {
|
||||
Ok(Box::new(Self::new(tools)))
|
||||
}
|
||||
|
||||
fn structural_tag_model(&self) -> Option<StructuralTagModel> {
|
||||
Some(StructuralTagModel::Minimax)
|
||||
fn structural_tag_builder(&self) -> Option<&dyn StructuralTagBuilder> {
|
||||
Some(xgrammar_structural_tag::Model::Minimax.builder())
|
||||
}
|
||||
|
||||
fn parse_into(&mut self, chunk: &str, output: &mut ToolParserOutput) -> Result<()> {
|
||||
|
||||
@@ -36,7 +36,7 @@ pub use qwen_coder::Qwen3CoderToolParser;
|
||||
pub use seed_oss::SeedOssToolParser;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
pub use xgrammar_structural_tag::Model as StructuralTagModel;
|
||||
pub use xgrammar_structural_tag::builders::StructuralTagBuilder;
|
||||
|
||||
use crate::utils;
|
||||
|
||||
@@ -187,8 +187,8 @@ pub trait ToolParser: Send {
|
||||
false
|
||||
}
|
||||
|
||||
/// Return the xgrammar structural-tag model used for strict tool calling.
|
||||
fn structural_tag_model(&self) -> Option<StructuralTagModel> {
|
||||
/// Return the xgrammar structural-tag builder used for strict tool calling.
|
||||
fn structural_tag_builder(&self) -> Option<&dyn StructuralTagBuilder> {
|
||||
None
|
||||
}
|
||||
|
||||
|
||||
@@ -9,8 +9,8 @@ use winnow::token::{literal, take_until};
|
||||
|
||||
use super::parameters::ToolSchemas;
|
||||
use super::utils::{MarkerScanState, parse_buffered_event, safe_text_len, take_until_marker};
|
||||
use super::{Result, StructuralTagModel, ToolCallDelta, ToolParser, ToolParserOutput};
|
||||
use crate::tool::Tool;
|
||||
use super::{Result, ToolCallDelta, ToolParser, ToolParserOutput};
|
||||
use crate::tool::{StructuralTagBuilder, Tool};
|
||||
|
||||
const TOOL_CALL_START: &str = "<tool_call>";
|
||||
const TOOL_CALL_END: &str = "</tool_call>";
|
||||
@@ -146,8 +146,8 @@ impl ToolParser for Qwen3CoderToolParser {
|
||||
Ok(Box::new(Self::new(tools)))
|
||||
}
|
||||
|
||||
fn structural_tag_model(&self) -> Option<StructuralTagModel> {
|
||||
Some(StructuralTagModel::Qwen3Coder)
|
||||
fn structural_tag_builder(&self) -> Option<&dyn StructuralTagBuilder> {
|
||||
Some(xgrammar_structural_tag::Model::Qwen3Coder.builder())
|
||||
}
|
||||
|
||||
fn parse_into(&mut self, chunk: &str, output: &mut ToolParserOutput) -> Result<()> {
|
||||
@@ -294,7 +294,7 @@ mod tests {
|
||||
use serde_json::{Value, json};
|
||||
use thiserror_ext::AsReport;
|
||||
|
||||
use super::{Qwen3CoderToolParser, StructuralTagModel, ToolParser};
|
||||
use super::{Qwen3CoderToolParser, ToolParser};
|
||||
use crate::tool::test_utils::{collect_stream, split_by_chars, test_tools};
|
||||
use crate::tool::{ToolParserOutput, ToolParserTestExt as _};
|
||||
|
||||
@@ -308,13 +308,10 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn qwen_coder_exposes_structural_tag_model() {
|
||||
fn qwen_coder_exposes_structural_tag_builder() {
|
||||
let parser = Qwen3CoderToolParser::new(&test_tools());
|
||||
|
||||
assert_eq!(
|
||||
parser.structural_tag_model(),
|
||||
Some(StructuralTagModel::Qwen3Coder)
|
||||
);
|
||||
assert!(parser.structural_tag_builder().is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -7,7 +7,7 @@ use vllm_tokenizer::DynTokenizer;
|
||||
|
||||
use super::{Result, UnifiedParser, UnifiedParserError, UnifiedParserOutput};
|
||||
use crate::reasoning::ReasoningParser;
|
||||
use crate::tool::{StructuralTagModel, Tool, ToolParser, ToolParserOutput};
|
||||
use crate::tool::{StructuralTagBuilder, Tool, ToolParser, ToolParserOutput};
|
||||
|
||||
/// Unified parser that composes existing reasoning and tool parsers.
|
||||
pub struct CombinedParser {
|
||||
@@ -79,8 +79,8 @@ impl UnifiedParser for CombinedParser {
|
||||
|| self.tool.as_ref().is_some_and(|parser| parser.preserve_special_tokens())
|
||||
}
|
||||
|
||||
fn structural_tag_model(&self) -> Option<StructuralTagModel> {
|
||||
self.tool.as_ref().and_then(|parser| parser.structural_tag_model())
|
||||
fn structural_tag_builder(&self) -> Option<&dyn StructuralTagBuilder> {
|
||||
self.tool.as_ref().and_then(|parser| parser.structural_tag_builder())
|
||||
}
|
||||
|
||||
fn tool_call_id(&self, tool_index: usize) -> Option<&str> {
|
||||
@@ -269,10 +269,7 @@ mod tests {
|
||||
fn combined_parser_emits_tool_calls_from_visible_content() {
|
||||
let tool = Qwen3XmlToolParser::create(&test_tools()).unwrap();
|
||||
let mut parser = CombinedParser::new(None, Some(tool));
|
||||
assert!(matches!(
|
||||
parser.structural_tag_model(),
|
||||
Some(crate::tool::StructuralTagModel::Qwen3)
|
||||
));
|
||||
assert!(parser.structural_tag_builder().is_some());
|
||||
|
||||
let output = collect(
|
||||
&mut parser,
|
||||
|
||||
@@ -16,7 +16,7 @@ use vllm_tokenizer::DynTokenizer;
|
||||
|
||||
use crate::reasoning::ReasoningError;
|
||||
use crate::tool::{
|
||||
StructuralTagModel, Tool, ToolCallDelta, ToolParserError, ToolParserEvent, ToolParserOutput,
|
||||
StructuralTagBuilder, Tool, ToolCallDelta, ToolParserError, ToolParserEvent, ToolParserOutput,
|
||||
};
|
||||
|
||||
/// Result alias for unified parser operations.
|
||||
@@ -171,8 +171,8 @@ pub trait UnifiedParser: Send {
|
||||
false
|
||||
}
|
||||
|
||||
/// Return the xgrammar structural-tag model used for strict tool calling.
|
||||
fn structural_tag_model(&self) -> Option<StructuralTagModel> {
|
||||
/// Return the xgrammar structural-tag builder used for strict tool calling.
|
||||
fn structural_tag_builder(&self) -> Option<&dyn StructuralTagBuilder> {
|
||||
None
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
import sys
|
||||
from types import ModuleType, SimpleNamespace
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from vllm.platforms.interface import DeviceCapability
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def cuda_platform_base(monkeypatch: pytest.MonkeyPatch) -> Any:
|
||||
stable_libtorch_module = ModuleType("vllm._C_stable_libtorch")
|
||||
monkeypatch.setitem(
|
||||
sys.modules,
|
||||
"vllm._C_stable_libtorch",
|
||||
stable_libtorch_module,
|
||||
)
|
||||
from vllm.platforms.cuda import CudaPlatformBase
|
||||
|
||||
return CudaPlatformBase
|
||||
|
||||
|
||||
def test_compiled_arch_covers_device(cuda_platform_base: Any) -> None:
|
||||
assert cuda_platform_base._compiled_arch_covers_device(
|
||||
"12.1a", DeviceCapability(12, 1)
|
||||
)
|
||||
assert not cuda_platform_base._compiled_arch_covers_device(
|
||||
"12.0a", DeviceCapability(12, 1)
|
||||
)
|
||||
|
||||
assert cuda_platform_base._compiled_arch_covers_device(
|
||||
"12.0f", DeviceCapability(12, 1)
|
||||
)
|
||||
assert not cuda_platform_base._compiled_arch_covers_device(
|
||||
"10.0f", DeviceCapability(12, 1)
|
||||
)
|
||||
|
||||
|
||||
def test_warn_if_device_arch_not_compiled(
|
||||
monkeypatch: pytest.MonkeyPatch, cuda_platform_base: Any
|
||||
) -> None:
|
||||
def device_count(cls: type[Any]) -> int:
|
||||
return 2
|
||||
|
||||
def get_device_capability(
|
||||
cls: type[Any], device_id: int = 0
|
||||
) -> DeviceCapability | None:
|
||||
capabilities = {
|
||||
0: DeviceCapability(12, 1),
|
||||
1: DeviceCapability(10, 3),
|
||||
}
|
||||
return capabilities[device_id]
|
||||
|
||||
def get_device_name(cls: type[Any], device_id: int = 0) -> str:
|
||||
return f"GPU {device_id}"
|
||||
|
||||
warnings: list[tuple[str, str, str]] = []
|
||||
|
||||
def warning_once(message: str, compiled_archs: str, devices: str) -> None:
|
||||
warnings.append((message, compiled_archs, devices))
|
||||
|
||||
monkeypatch.setattr(cuda_platform_base, "device_count", classmethod(device_count))
|
||||
monkeypatch.setattr(
|
||||
cuda_platform_base,
|
||||
"get_device_capability",
|
||||
classmethod(get_device_capability),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
cuda_platform_base, "get_device_name", classmethod(get_device_name)
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"vllm.platforms.cuda.torch.ops",
|
||||
SimpleNamespace(
|
||||
_C=SimpleNamespace(get_compiled_cuda_archs=lambda: "12.0f,10.0a")
|
||||
),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"vllm.platforms.cuda.logger",
|
||||
SimpleNamespace(warning_once=warning_once),
|
||||
)
|
||||
|
||||
cuda_platform_base._warn_if_device_arch_not_compiled()
|
||||
|
||||
assert len(warnings) == 1
|
||||
assert warnings[0][1] == "12.0f, 10.0a"
|
||||
assert "1: GPU 1 (compute capability 10.3)" in warnings[0][2]
|
||||
|
||||
|
||||
def test_warn_if_device_arch_not_compiled_no_warning(
|
||||
monkeypatch: pytest.MonkeyPatch, cuda_platform_base: Any
|
||||
) -> None:
|
||||
def device_count(cls: type[Any]) -> int:
|
||||
return 1
|
||||
|
||||
def get_device_capability(
|
||||
cls: type[Any], device_id: int = 0
|
||||
) -> DeviceCapability | None:
|
||||
return DeviceCapability(10, 3)
|
||||
|
||||
def get_device_name(cls: type[Any], device_id: int = 0) -> str:
|
||||
raise AssertionError("get_device_name should not be called for covered devices")
|
||||
|
||||
warnings: list[tuple[str, str, str]] = []
|
||||
|
||||
def warning_once(message: str, compiled_archs: str, devices: str) -> None:
|
||||
warnings.append((message, compiled_archs, devices))
|
||||
|
||||
monkeypatch.setattr(cuda_platform_base, "device_count", classmethod(device_count))
|
||||
monkeypatch.setattr(
|
||||
cuda_platform_base,
|
||||
"get_device_capability",
|
||||
classmethod(get_device_capability),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
cuda_platform_base, "get_device_name", classmethod(get_device_name)
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"vllm.platforms.cuda.torch.ops",
|
||||
SimpleNamespace(_C=SimpleNamespace(get_compiled_cuda_archs=lambda: "10.0f")),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"vllm.platforms.cuda.logger",
|
||||
SimpleNamespace(warning_once=warning_once),
|
||||
)
|
||||
|
||||
cuda_platform_base._warn_if_device_arch_not_compiled()
|
||||
|
||||
assert warnings == []
|
||||
@@ -60,6 +60,8 @@ def _make_quick_allreduce(
|
||||
qar.use_fp16_kernels = use_fp16_kernels
|
||||
qar.qr_quant_level = QuickReduceRegime[quant_level]
|
||||
qar.qr_max_size = qr_max_size
|
||||
qar.qr_min_size = None
|
||||
qar.qr_quantization_min_size = None
|
||||
return qar
|
||||
|
||||
|
||||
@@ -511,13 +513,21 @@ def test_quick_reduce_regime_values():
|
||||
assert QuickReduceRegime.INT8.value == 1
|
||||
assert QuickReduceRegime.INT6.value == 2
|
||||
assert QuickReduceRegime.INT4.value == 3
|
||||
assert QuickReduceRegime.NONE.value == 4
|
||||
assert QuickReduceRegime.INT3.value == 4
|
||||
assert QuickReduceRegime.NONE.value == 5
|
||||
|
||||
|
||||
def test_quick_reduce_regime_names():
|
||||
from vllm.distributed.device_communicators.quick_all_reduce import QuickReduceRegime
|
||||
|
||||
assert set(QuickReduceRegime.__members__) == {"FP", "INT8", "INT6", "INT4", "NONE"}
|
||||
assert set(QuickReduceRegime.__members__) == {
|
||||
"FP",
|
||||
"INT8",
|
||||
"INT6",
|
||||
"INT4",
|
||||
"INT3",
|
||||
"NONE",
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("quant_level", QUANT_LEVELS + ["NONE"])
|
||||
@@ -693,7 +703,7 @@ def test_quick_allreduce_min_size_table():
|
||||
for dtype in [torch.float16, torch.bfloat16]:
|
||||
for world_size in QuickAllReduce._SUPPORTED_WORLD_SIZES:
|
||||
min_sizes = QuickAllReduce._QR_MIN_SIZE[(dtype, world_size)]
|
||||
assert len(min_sizes) == 4
|
||||
assert len(min_sizes) == 5
|
||||
assert all(size > 0 for size in min_sizes)
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,880 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""End-to-end tests for routed-expert capture on the monolithic MoE path.
|
||||
|
||||
These tests exercise the wiring that lets ``RoutedExpertsCapturer`` see the
|
||||
expert IDs picked by FlashInfer's fused router-and-experts kernels (the
|
||||
"monolithic" path). When ``set_capture_fn`` is installed on
|
||||
a ``FusedMoEExpertsMonolithic`` subclass that supports it, the kernel call
|
||||
should:
|
||||
|
||||
* allocate an int16 ``(num_tokens, top_k)`` buffer,
|
||||
* pass it to FlashInfer as ``routing_replay_out``,
|
||||
* invoke the callback after the kernel returns.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from vllm.model_executor.layers.fused_moe.activation import MoEActivation
|
||||
from vllm.model_executor.layers.fused_moe.config import (
|
||||
FusedMoEConfig,
|
||||
FusedMoEParallelConfig,
|
||||
FusedMoEQuantConfig,
|
||||
RoutingMethodType,
|
||||
fp8_w8a8_moe_quant_config,
|
||||
)
|
||||
from vllm.model_executor.layers.fused_moe.experts.trtllm_bf16_moe import (
|
||||
TrtLlmBf16ExpertsMonolithic,
|
||||
)
|
||||
from vllm.model_executor.layers.fused_moe.experts.trtllm_fp8_moe import (
|
||||
TrtLlmFp8ExpertsMonolithic,
|
||||
)
|
||||
from vllm.model_executor.layers.fused_moe.experts.trtllm_nvfp4_moe import (
|
||||
TrtLlmNvFp4ExpertsMonolithic,
|
||||
)
|
||||
from vllm.platforms import current_platform
|
||||
|
||||
try:
|
||||
from vllm.utils.flashinfer import has_flashinfer_trtllm_fused_moe
|
||||
except ImportError:
|
||||
pytest.skip("flashinfer not available", allow_module_level=True)
|
||||
|
||||
if not has_flashinfer_trtllm_fused_moe() or not current_platform.is_cuda():
|
||||
pytest.skip(
|
||||
"Requires FlashInfer TRT-LLM fused MoE on CUDA",
|
||||
allow_module_level=True,
|
||||
)
|
||||
|
||||
if not current_platform.has_device_capability(100):
|
||||
pytest.skip(
|
||||
"TRT-LLM fused MoE kernels require SM100+",
|
||||
allow_module_level=True,
|
||||
)
|
||||
|
||||
|
||||
def _shuffle_bf16_weights_block_major_k(
|
||||
w: torch.Tensor, epilogue_tile_m: int = 64, block_k: int = 128
|
||||
) -> torch.Tensor:
|
||||
"""Reshape ``w`` (E, M, K) into the ``BlockMajorK`` layout expected by
|
||||
``trtllm_bf16_moe``: ``(E, K/block_k, M, block_k)`` after a per-expert
|
||||
row shuffle.
|
||||
"""
|
||||
from flashinfer import shuffle_matrix_a
|
||||
from flashinfer.fused_moe import convert_to_block_layout
|
||||
|
||||
num_experts = w.shape[0]
|
||||
shuffled = []
|
||||
for i in range(num_experts):
|
||||
t = shuffle_matrix_a(w[i].view(torch.uint8), epilogue_tile_m)
|
||||
shuffled.append(convert_to_block_layout(t, block_k))
|
||||
return torch.stack(shuffled).view(torch.bfloat16)
|
||||
|
||||
|
||||
def _make_bf16_monolithic_experts(
|
||||
num_experts: int,
|
||||
top_k: int,
|
||||
hidden_size: int,
|
||||
intermediate_size: int,
|
||||
routing_method: RoutingMethodType,
|
||||
device: torch.device,
|
||||
) -> tuple[TrtLlmBf16ExpertsMonolithic, torch.Tensor, torch.Tensor]:
|
||||
"""Construct the monolithic BF16 experts plus the BlockMajorK weights
|
||||
expected by ``trtllm_bf16_moe``.
|
||||
"""
|
||||
parallel_cfg = FusedMoEParallelConfig.make_no_parallel()
|
||||
moe_config = FusedMoEConfig(
|
||||
num_experts=num_experts,
|
||||
experts_per_token=top_k,
|
||||
hidden_dim=hidden_size,
|
||||
intermediate_size=intermediate_size,
|
||||
num_local_experts=num_experts,
|
||||
num_logical_experts=num_experts,
|
||||
moe_parallel_config=parallel_cfg,
|
||||
in_dtype=torch.bfloat16,
|
||||
activation=MoEActivation.SILU,
|
||||
device=device,
|
||||
routing_method=routing_method,
|
||||
max_num_tokens=max(8, 1),
|
||||
)
|
||||
quant_config = FusedMoEQuantConfig.make(
|
||||
quant_dtype=None,
|
||||
per_act_token_quant=False,
|
||||
per_out_ch_quant=False,
|
||||
block_shape=None,
|
||||
)
|
||||
|
||||
experts = TrtLlmBf16ExpertsMonolithic(
|
||||
moe_config=moe_config, quant_config=quant_config
|
||||
)
|
||||
|
||||
gemm1 = (
|
||||
torch.randn(
|
||||
num_experts,
|
||||
2 * intermediate_size,
|
||||
hidden_size,
|
||||
device=device,
|
||||
dtype=torch.bfloat16,
|
||||
)
|
||||
* 0.1
|
||||
)
|
||||
gemm2 = (
|
||||
torch.randn(
|
||||
num_experts,
|
||||
hidden_size,
|
||||
intermediate_size,
|
||||
device=device,
|
||||
dtype=torch.bfloat16,
|
||||
)
|
||||
* 0.1
|
||||
)
|
||||
w13 = _shuffle_bf16_weights_block_major_k(gemm1)
|
||||
w2 = _shuffle_bf16_weights_block_major_k(gemm2)
|
||||
return experts, w13, w2
|
||||
|
||||
|
||||
def _run_bf16_monolithic(
|
||||
experts: TrtLlmBf16ExpertsMonolithic,
|
||||
hidden_states: torch.Tensor,
|
||||
w13: torch.Tensor,
|
||||
w2: torch.Tensor,
|
||||
router_logits: torch.Tensor,
|
||||
num_experts: int,
|
||||
*,
|
||||
n_group: int | None = None,
|
||||
topk_group: int | None = None,
|
||||
routed_scaling_factor: float | None = None,
|
||||
e_score_correction_bias: torch.Tensor | None = None,
|
||||
) -> torch.Tensor:
|
||||
return experts.apply(
|
||||
hidden_states=hidden_states,
|
||||
w1=w13,
|
||||
w2=w2,
|
||||
router_logits=router_logits,
|
||||
activation=MoEActivation.SILU,
|
||||
global_num_experts=num_experts,
|
||||
expert_map=None,
|
||||
a1q_scale=None,
|
||||
apply_router_weight_on_input=False,
|
||||
num_expert_group=n_group,
|
||||
topk_group=topk_group,
|
||||
e_score_correction_bias=e_score_correction_bias,
|
||||
routed_scaling_factor=routed_scaling_factor,
|
||||
)
|
||||
|
||||
|
||||
_DSV3_NUM_EXPERTS = 32
|
||||
_DSV3_N_GROUP = 4
|
||||
_DSV3_TOPK_GROUP = 2
|
||||
|
||||
|
||||
def _make_dsv3_routing_bias(num_experts: int, device: torch.device) -> torch.Tensor:
|
||||
return torch.randn(num_experts, device=device, dtype=torch.bfloat16)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("num_tokens", [2, 7, 16])
|
||||
@pytest.mark.parametrize("top_k", [2, 4])
|
||||
def test_trtllm_bf16_monolithic_routing_replay_records_valid_experts(
|
||||
num_tokens: int,
|
||||
top_k: int,
|
||||
) -> None:
|
||||
"""The capture callback should receive the int16 routed-expert IDs the
|
||||
kernel actually used, the values should be valid expert indices, and
|
||||
each token should pick ``top_k`` distinct experts."""
|
||||
if top_k > _DSV3_N_GROUP * _DSV3_TOPK_GROUP:
|
||||
pytest.skip(
|
||||
f"DSV3 requires top_k <= n_group * topk_group "
|
||||
f"({_DSV3_N_GROUP * _DSV3_TOPK_GROUP})"
|
||||
)
|
||||
torch.manual_seed(0)
|
||||
device = torch.device("cuda:0")
|
||||
|
||||
num_experts = _DSV3_NUM_EXPERTS
|
||||
hidden_size = 1024
|
||||
intermediate_size = 1024
|
||||
|
||||
experts, w13, w2 = _make_bf16_monolithic_experts(
|
||||
num_experts=num_experts,
|
||||
top_k=top_k,
|
||||
hidden_size=hidden_size,
|
||||
intermediate_size=intermediate_size,
|
||||
routing_method=RoutingMethodType.DeepSeekV3,
|
||||
device=device,
|
||||
)
|
||||
|
||||
captured: list[torch.Tensor] = []
|
||||
|
||||
def capture_fn(replay_out: torch.Tensor) -> None:
|
||||
captured.append(replay_out.clone())
|
||||
|
||||
assert experts.supports_routing_replay_capture()
|
||||
experts.set_capture_fn(capture_fn)
|
||||
|
||||
hidden_states = (
|
||||
torch.randn(num_tokens, hidden_size, device=device, dtype=torch.bfloat16) * 0.1
|
||||
)
|
||||
router_logits = torch.rand(
|
||||
num_tokens, num_experts, device=device, dtype=torch.float32
|
||||
)
|
||||
routing_bias = _make_dsv3_routing_bias(num_experts, device)
|
||||
|
||||
_ = _run_bf16_monolithic(
|
||||
experts,
|
||||
hidden_states=hidden_states,
|
||||
w13=w13,
|
||||
w2=w2,
|
||||
router_logits=router_logits,
|
||||
num_experts=num_experts,
|
||||
n_group=_DSV3_N_GROUP,
|
||||
topk_group=_DSV3_TOPK_GROUP,
|
||||
routed_scaling_factor=1.0,
|
||||
e_score_correction_bias=routing_bias,
|
||||
)
|
||||
|
||||
assert len(captured) == 1
|
||||
replay = captured[0]
|
||||
assert replay.dtype == torch.int16
|
||||
assert replay.shape == (num_tokens, top_k)
|
||||
assert (replay >= 0).all(), f"got out-of-range values: {replay}"
|
||||
assert (replay < num_experts).all(), f"got out-of-range values: {replay}"
|
||||
|
||||
for t in range(num_tokens):
|
||||
unique = replay[t].unique()
|
||||
assert unique.numel() == top_k, (
|
||||
f"token {t}: expected {top_k} distinct experts, "
|
||||
f"got {unique.numel()} ({replay[t].tolist()})"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("num_tokens", [2, 7, 16])
|
||||
@pytest.mark.parametrize(
|
||||
"routing_method",
|
||||
[
|
||||
RoutingMethodType.Renormalize,
|
||||
RoutingMethodType.RenormalizeNaive,
|
||||
],
|
||||
)
|
||||
def test_trtllm_bf16_monolithic_routing_replay_non_dsv3(
|
||||
num_tokens: int,
|
||||
routing_method: RoutingMethodType,
|
||||
) -> None:
|
||||
"""Routing replay works for non-DeepSeekV3 routing methods too.
|
||||
FlashInfer's ``routing_replay_out`` is routing-method-agnostic."""
|
||||
torch.manual_seed(0)
|
||||
device = torch.device("cuda:0")
|
||||
|
||||
num_experts = 8
|
||||
top_k = 2
|
||||
hidden_size = 1024
|
||||
intermediate_size = 1024
|
||||
|
||||
experts, w13, w2 = _make_bf16_monolithic_experts(
|
||||
num_experts=num_experts,
|
||||
top_k=top_k,
|
||||
hidden_size=hidden_size,
|
||||
intermediate_size=intermediate_size,
|
||||
routing_method=routing_method,
|
||||
device=device,
|
||||
)
|
||||
|
||||
captured: list[torch.Tensor] = []
|
||||
experts.set_capture_fn(lambda r: captured.append(r.clone()))
|
||||
|
||||
hidden_states = (
|
||||
torch.randn(num_tokens, hidden_size, device=device, dtype=torch.bfloat16) * 0.1
|
||||
)
|
||||
router_logits = torch.rand(
|
||||
num_tokens, num_experts, device=device, dtype=torch.float32
|
||||
)
|
||||
|
||||
_ = _run_bf16_monolithic(
|
||||
experts,
|
||||
hidden_states=hidden_states,
|
||||
w13=w13,
|
||||
w2=w2,
|
||||
router_logits=router_logits,
|
||||
num_experts=num_experts,
|
||||
)
|
||||
|
||||
assert len(captured) == 1
|
||||
replay = captured[0]
|
||||
assert replay.dtype == torch.int16
|
||||
assert replay.shape == (num_tokens, top_k)
|
||||
assert (replay >= 0).all(), f"got out-of-range values: {replay}"
|
||||
assert (replay < num_experts).all(), f"got out-of-range values: {replay}"
|
||||
for t in range(num_tokens):
|
||||
unique = replay[t].unique()
|
||||
assert unique.numel() == top_k, (
|
||||
f"token {t}: expected {top_k} distinct experts, "
|
||||
f"got {unique.numel()} ({replay[t].tolist()})"
|
||||
)
|
||||
|
||||
|
||||
def test_trtllm_bf16_monolithic_capture_disabled_skips_buffer_alloc() -> None:
|
||||
"""With no callback installed the kernel should not see a
|
||||
``routing_replay_out`` tensor — verify the helper short-circuits."""
|
||||
torch.manual_seed(0)
|
||||
device = torch.device("cuda:0")
|
||||
experts, _, _ = _make_bf16_monolithic_experts(
|
||||
num_experts=_DSV3_NUM_EXPERTS,
|
||||
top_k=2,
|
||||
hidden_size=1024,
|
||||
intermediate_size=1024,
|
||||
routing_method=RoutingMethodType.DeepSeekV3,
|
||||
device=device,
|
||||
)
|
||||
# No callback installed.
|
||||
buf = experts._maybe_make_routing_replay_buffer(num_tokens=4, device=device)
|
||||
assert buf is None
|
||||
|
||||
# Dispatch is also a no-op.
|
||||
experts._maybe_dispatch_routing_replay(buf, num_tokens=4)
|
||||
|
||||
|
||||
def test_trtllm_bf16_monolithic_supports_capture_for_all_routing() -> None:
|
||||
"""FlashInfer's ``routing_replay_out`` is supported by all routing
|
||||
methods, so ``supports_routing_replay_capture`` should be True
|
||||
regardless of routing method."""
|
||||
device = torch.device("cuda:0")
|
||||
for routing_method in (
|
||||
RoutingMethodType.DeepSeekV3,
|
||||
RoutingMethodType.Renormalize,
|
||||
RoutingMethodType.RenormalizeNaive,
|
||||
):
|
||||
experts, _, _ = _make_bf16_monolithic_experts(
|
||||
num_experts=_DSV3_NUM_EXPERTS,
|
||||
top_k=2,
|
||||
hidden_size=1024,
|
||||
intermediate_size=1024,
|
||||
routing_method=routing_method,
|
||||
device=device,
|
||||
)
|
||||
assert experts.supports_routing_replay_capture() is True, (
|
||||
f"{routing_method!r} should support routing replay capture"
|
||||
)
|
||||
|
||||
|
||||
def test_trtllm_bf16_monolithic_capture_buffer_shape_and_dtype() -> None:
|
||||
"""When capture is installed, the allocated buffer is int16 and shaped
|
||||
``(num_tokens, experts_per_token)``."""
|
||||
device = torch.device("cuda:0")
|
||||
experts, _, _ = _make_bf16_monolithic_experts(
|
||||
num_experts=_DSV3_NUM_EXPERTS,
|
||||
top_k=4,
|
||||
hidden_size=1024,
|
||||
intermediate_size=1024,
|
||||
routing_method=RoutingMethodType.DeepSeekV3,
|
||||
device=device,
|
||||
)
|
||||
experts.set_capture_fn(lambda r: None)
|
||||
buf = experts._maybe_make_routing_replay_buffer(num_tokens=11, device=device)
|
||||
assert buf is not None
|
||||
assert buf.dtype == torch.int16
|
||||
assert buf.shape[0] >= 11
|
||||
assert buf.shape[1] == 4
|
||||
assert buf.device.type == "cuda"
|
||||
|
||||
|
||||
def test_routed_experts_capturer_e2e_via_monolithic_experts() -> None:
|
||||
"""End-to-end: bind ``RoutedExpertsCapturer.capture`` as the callback
|
||||
on the monolithic experts and verify the captured rows land in the
|
||||
capturer's device buffer at the correct layer slot.
|
||||
|
||||
Mirrors the wiring done in ``GPUModelRunner._bind_routed_experts_capturer``
|
||||
for the monolithic path: a single closure is installed on the monolithic
|
||||
``fused_experts`` (in addition to ``router.set_capture_fn`` on the
|
||||
non-monolithic path) and the capturer routes per-layer based on the
|
||||
closed-over ``layer_id``.
|
||||
"""
|
||||
from vllm.model_executor.layers.fused_moe.routed_experts_capturer import (
|
||||
RoutedExpertsCapturer,
|
||||
)
|
||||
|
||||
torch.manual_seed(7)
|
||||
device = torch.device("cuda:0")
|
||||
num_tokens = 4
|
||||
top_k = 2
|
||||
num_experts = _DSV3_NUM_EXPERTS
|
||||
hidden_size = 1024
|
||||
intermediate_size = 1024
|
||||
|
||||
experts, w13, w2 = _make_bf16_monolithic_experts(
|
||||
num_experts=num_experts,
|
||||
top_k=top_k,
|
||||
hidden_size=hidden_size,
|
||||
intermediate_size=intermediate_size,
|
||||
routing_method=RoutingMethodType.DeepSeekV3,
|
||||
device=device,
|
||||
)
|
||||
|
||||
num_layers = 3
|
||||
layer_id = 1
|
||||
capturer = RoutedExpertsCapturer.__new__(RoutedExpertsCapturer)
|
||||
capturer.dp_rank = 0
|
||||
capturer.tp_size = 1
|
||||
capturer.device_buffer = torch.full(
|
||||
(num_tokens + 4, num_layers, top_k),
|
||||
-1,
|
||||
dtype=torch.int32,
|
||||
device=device,
|
||||
)
|
||||
|
||||
def capture_fn(replay_out: torch.Tensor) -> None:
|
||||
capturer.capture(layer_id, replay_out)
|
||||
|
||||
experts.set_capture_fn(capture_fn)
|
||||
|
||||
hidden_states = (
|
||||
torch.randn(num_tokens, hidden_size, device=device, dtype=torch.bfloat16) * 0.1
|
||||
)
|
||||
router_logits = torch.rand(
|
||||
num_tokens, num_experts, device=device, dtype=torch.float32
|
||||
)
|
||||
routing_bias = _make_dsv3_routing_bias(num_experts, device)
|
||||
|
||||
# Patch get_forward_context to return a dp_metadata=None context so the
|
||||
# capturer takes the single-DP branch.
|
||||
import vllm.model_executor.layers.fused_moe.routed_experts_capturer as rec
|
||||
|
||||
with patch.object(
|
||||
rec,
|
||||
"get_forward_context",
|
||||
return_value=SimpleNamespace(dp_metadata=None),
|
||||
):
|
||||
_ = _run_bf16_monolithic(
|
||||
experts,
|
||||
hidden_states=hidden_states,
|
||||
w13=w13,
|
||||
w2=w2,
|
||||
router_logits=router_logits,
|
||||
num_experts=num_experts,
|
||||
n_group=_DSV3_N_GROUP,
|
||||
topk_group=_DSV3_TOPK_GROUP,
|
||||
routed_scaling_factor=1.0,
|
||||
e_score_correction_bias=routing_bias,
|
||||
)
|
||||
|
||||
captured = capturer.device_buffer[:num_tokens, layer_id, :].cpu()
|
||||
# Valid expert IDs at this layer.
|
||||
assert (captured >= 0).all()
|
||||
assert (captured < num_experts).all()
|
||||
for t in range(num_tokens):
|
||||
unique = captured[t].unique()
|
||||
assert unique.numel() == top_k, (
|
||||
f"token {t}: expected {top_k} distinct experts at layer "
|
||||
f"{layer_id}, got {unique.numel()}"
|
||||
)
|
||||
|
||||
# Other layers / trailing token rows untouched.
|
||||
for other_layer in range(num_layers):
|
||||
if other_layer == layer_id:
|
||||
continue
|
||||
assert (capturer.device_buffer[:, other_layer, :].cpu() == -1).all(), (
|
||||
f"layer {other_layer} should be untouched, got writes"
|
||||
)
|
||||
assert (capturer.device_buffer[num_tokens:, layer_id, :].cpu() == -1).all(), (
|
||||
"tail rows beyond num_tokens should remain sentinel"
|
||||
)
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# FP8 block-scale (DeepSeekFp8) — vLLM's ``TrtLlmFp8ExpertsMonolithic``
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _make_fp8_block_scale_monolithic_experts(
|
||||
num_experts: int,
|
||||
top_k: int,
|
||||
hidden_size: int,
|
||||
intermediate_size: int,
|
||||
device: torch.device,
|
||||
) -> tuple[TrtLlmFp8ExpertsMonolithic, torch.Tensor, torch.Tensor]:
|
||||
"""Set up ``TrtLlmFp8ExpertsMonolithic`` for the DeepSeekFp8 block-scale
|
||||
code path with DSV3 routing.
|
||||
|
||||
Weights are shuffled into the BlockMajorK layout the kernel expects
|
||||
(same helper the vLLM weight loader uses for DeepSeek-FP8 models).
|
||||
"""
|
||||
from vllm.model_executor.layers.quantization.utils.flashinfer_utils import (
|
||||
_shuffle_deepseek_fp8_moe_weights,
|
||||
)
|
||||
|
||||
block_k = 128
|
||||
parallel_cfg = FusedMoEParallelConfig.make_no_parallel()
|
||||
moe_config = FusedMoEConfig(
|
||||
num_experts=num_experts,
|
||||
experts_per_token=top_k,
|
||||
hidden_dim=hidden_size,
|
||||
intermediate_size=intermediate_size,
|
||||
num_local_experts=num_experts,
|
||||
num_logical_experts=num_experts,
|
||||
moe_parallel_config=parallel_cfg,
|
||||
in_dtype=torch.bfloat16,
|
||||
activation=MoEActivation.SILU,
|
||||
device=device,
|
||||
routing_method=RoutingMethodType.DeepSeekV3,
|
||||
max_num_tokens=max(8, 1),
|
||||
)
|
||||
|
||||
# Random fp8 weights + ones-block scales (the kernel decoder only cares
|
||||
# that the per-block scales are present and finite for routing/replay).
|
||||
gemm1 = torch.randn(
|
||||
num_experts, 2 * intermediate_size, hidden_size, device=device
|
||||
).to(torch.float8_e4m3fn)
|
||||
gemm2 = torch.randn(num_experts, hidden_size, intermediate_size, device=device).to(
|
||||
torch.float8_e4m3fn
|
||||
)
|
||||
w13_shuffled, w2_shuffled = _shuffle_deepseek_fp8_moe_weights(gemm1, gemm2)
|
||||
|
||||
w1_scale = torch.ones(
|
||||
num_experts,
|
||||
2 * intermediate_size // block_k,
|
||||
hidden_size // block_k,
|
||||
device=device,
|
||||
dtype=torch.float32,
|
||||
)
|
||||
w2_scale = torch.ones(
|
||||
num_experts,
|
||||
hidden_size // block_k,
|
||||
intermediate_size // block_k,
|
||||
device=device,
|
||||
dtype=torch.float32,
|
||||
)
|
||||
quant_config = fp8_w8a8_moe_quant_config(
|
||||
w1_scale=w1_scale,
|
||||
w2_scale=w2_scale,
|
||||
block_shape=[block_k, block_k],
|
||||
per_act_token_quant=False,
|
||||
)
|
||||
|
||||
experts = TrtLlmFp8ExpertsMonolithic(
|
||||
moe_config=moe_config, quant_config=quant_config
|
||||
)
|
||||
return experts, w13_shuffled, w2_shuffled
|
||||
|
||||
|
||||
def _run_fp8_block_scale_monolithic(
|
||||
experts: TrtLlmFp8ExpertsMonolithic,
|
||||
hidden_states_fp8: torch.Tensor,
|
||||
hidden_states_scale: torch.Tensor,
|
||||
w13: torch.Tensor,
|
||||
w2: torch.Tensor,
|
||||
router_logits: torch.Tensor,
|
||||
num_experts: int,
|
||||
routing_bias: torch.Tensor,
|
||||
) -> torch.Tensor:
|
||||
return experts.apply(
|
||||
hidden_states=hidden_states_fp8,
|
||||
w1=w13,
|
||||
w2=w2,
|
||||
router_logits=router_logits,
|
||||
activation=MoEActivation.SILU,
|
||||
global_num_experts=num_experts,
|
||||
expert_map=None,
|
||||
# The block-scale apply path reads ``a1q_scale`` and transposes it
|
||||
# to ``(hidden_size/128, num_tokens)`` for the kernel call.
|
||||
a1q_scale=hidden_states_scale,
|
||||
apply_router_weight_on_input=False,
|
||||
num_expert_group=_DSV3_N_GROUP,
|
||||
topk_group=_DSV3_TOPK_GROUP,
|
||||
e_score_correction_bias=routing_bias,
|
||||
routed_scaling_factor=1.0,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("num_tokens", [2, 7, 16])
|
||||
@pytest.mark.parametrize("top_k", [2, 4])
|
||||
def test_trtllm_fp8_block_scale_monolithic_routing_replay_records_valid_experts(
|
||||
num_tokens: int,
|
||||
top_k: int,
|
||||
) -> None:
|
||||
"""End-to-end: ``TrtLlmFp8ExpertsMonolithic`` (DeepSeekFp8 block-scale
|
||||
path, DSV3 routing) captures valid expert IDs."""
|
||||
if top_k > _DSV3_N_GROUP * _DSV3_TOPK_GROUP:
|
||||
pytest.skip(
|
||||
f"DSV3 requires top_k <= n_group * topk_group "
|
||||
f"({_DSV3_N_GROUP * _DSV3_TOPK_GROUP})"
|
||||
)
|
||||
torch.manual_seed(0)
|
||||
device = torch.device("cuda:0")
|
||||
|
||||
num_experts = _DSV3_NUM_EXPERTS
|
||||
hidden_size = 1024
|
||||
intermediate_size = 1024
|
||||
|
||||
experts, w13, w2 = _make_fp8_block_scale_monolithic_experts(
|
||||
num_experts=num_experts,
|
||||
top_k=top_k,
|
||||
hidden_size=hidden_size,
|
||||
intermediate_size=intermediate_size,
|
||||
device=device,
|
||||
)
|
||||
assert experts.supports_routing_replay_capture()
|
||||
|
||||
captured: list[torch.Tensor] = []
|
||||
experts.set_capture_fn(lambda r: captured.append(r.clone()))
|
||||
|
||||
# Per-token / per-block hidden scales (ones is fine for the routing
|
||||
# path; the GEMM output isn't being asserted on).
|
||||
hidden_states = (
|
||||
torch.randn(num_tokens, hidden_size, device=device, dtype=torch.bfloat16) * 0.1
|
||||
).to(torch.float8_e4m3fn)
|
||||
hidden_states_scale = torch.ones(
|
||||
num_tokens, hidden_size // 128, device=device, dtype=torch.float32
|
||||
)
|
||||
router_logits = torch.rand(
|
||||
num_tokens, num_experts, device=device, dtype=torch.float32
|
||||
)
|
||||
routing_bias = _make_dsv3_routing_bias(num_experts, device)
|
||||
|
||||
_ = _run_fp8_block_scale_monolithic(
|
||||
experts,
|
||||
hidden_states_fp8=hidden_states,
|
||||
hidden_states_scale=hidden_states_scale,
|
||||
w13=w13,
|
||||
w2=w2,
|
||||
router_logits=router_logits,
|
||||
num_experts=num_experts,
|
||||
routing_bias=routing_bias,
|
||||
)
|
||||
|
||||
assert len(captured) == 1
|
||||
replay = captured[0]
|
||||
assert replay.dtype == torch.int16
|
||||
assert replay.shape == (num_tokens, top_k)
|
||||
assert (replay >= 0).all(), f"got out-of-range values: {replay}"
|
||||
assert (replay < num_experts).all(), f"got out-of-range values: {replay}"
|
||||
for t in range(num_tokens):
|
||||
unique = replay[t].unique()
|
||||
assert unique.numel() == top_k, (
|
||||
f"token {t}: expected {top_k} distinct experts, "
|
||||
f"got {unique.numel()} ({replay[t].tolist()})"
|
||||
)
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# NVFP4 — vLLM's ``TrtLlmNvFp4ExpertsMonolithic``
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _make_nvfp4_monolithic_experts(
|
||||
num_experts: int,
|
||||
top_k: int,
|
||||
hidden_size: int,
|
||||
intermediate_size: int,
|
||||
device: torch.device,
|
||||
) -> tuple[
|
||||
TrtLlmNvFp4ExpertsMonolithic,
|
||||
torch.Tensor, # w13 (packed nvfp4 uint8)
|
||||
torch.Tensor, # w13 block-scale (fp8)
|
||||
torch.Tensor, # w2 (packed nvfp4 uint8)
|
||||
torch.Tensor, # w2 block-scale (fp8)
|
||||
torch.Tensor, # input global scale (per-tensor float32)
|
||||
]:
|
||||
"""Set up ``TrtLlmNvFp4ExpertsMonolithic`` with NVFP4-quantized weights
|
||||
and DSV3 routing.
|
||||
|
||||
NVFP4 = per-block-of-16 fp4 with an fp8 scale, plus a per-tensor
|
||||
"global" scale. We follow the layout in ``test_ocp_mx_moe.py`` /
|
||||
``flashinfer/tests/moe/test_trtllm_gen_routed_fused_moe.py``:
|
||||
* weights: uint8 (packed fp4) ``(E, M, K//2)``
|
||||
* weight scales: fp8 ``(E, M, K//16)``
|
||||
* hidden states: uint8 (packed fp4) ``(N, K//2)``
|
||||
* hidden state scales: fp8 ``(N, K//16)``
|
||||
"""
|
||||
from flashinfer import fp4_quantize
|
||||
|
||||
block_size = 16
|
||||
parallel_cfg = FusedMoEParallelConfig.make_no_parallel()
|
||||
moe_config = FusedMoEConfig(
|
||||
num_experts=num_experts,
|
||||
experts_per_token=top_k,
|
||||
hidden_dim=hidden_size,
|
||||
intermediate_size=intermediate_size,
|
||||
num_local_experts=num_experts,
|
||||
num_logical_experts=num_experts,
|
||||
moe_parallel_config=parallel_cfg,
|
||||
in_dtype=torch.bfloat16,
|
||||
activation=MoEActivation.SILU,
|
||||
device=device,
|
||||
routing_method=RoutingMethodType.DeepSeekV3,
|
||||
max_num_tokens=max(8, 1),
|
||||
)
|
||||
|
||||
gemm1 = torch.randn(
|
||||
num_experts,
|
||||
2 * intermediate_size,
|
||||
hidden_size,
|
||||
device=device,
|
||||
dtype=torch.bfloat16,
|
||||
)
|
||||
gemm2 = torch.randn(
|
||||
num_experts,
|
||||
hidden_size,
|
||||
intermediate_size,
|
||||
device=device,
|
||||
dtype=torch.bfloat16,
|
||||
)
|
||||
# Per-tensor weight scaling factor (used to build ``g1_alphas`` /
|
||||
# ``g2_alphas`` below).
|
||||
w_global_scale = torch.tensor(1.0, device=device)
|
||||
# Per-tensor input scaling factor.
|
||||
a_global_scale = torch.tensor(1.0, device=device)
|
||||
|
||||
w13_q, w13_scale = fp4_quantize(
|
||||
gemm1,
|
||||
w_global_scale,
|
||||
block_size,
|
||||
sf_use_ue8m0=False,
|
||||
is_sf_swizzled_layout=False,
|
||||
)
|
||||
w13_scale = w13_scale.view(torch.float8_e4m3fn).reshape(
|
||||
num_experts, 2 * intermediate_size, hidden_size // block_size
|
||||
)
|
||||
w2_q, w2_scale = fp4_quantize(
|
||||
gemm2,
|
||||
w_global_scale,
|
||||
block_size,
|
||||
sf_use_ue8m0=False,
|
||||
is_sf_swizzled_layout=False,
|
||||
)
|
||||
w2_scale = w2_scale.view(torch.float8_e4m3fn).reshape(
|
||||
num_experts, hidden_size, intermediate_size // block_size
|
||||
)
|
||||
|
||||
# NVFP4 dq scale chain: g1_alphas = w1_scale_2 * a1_scale_2,
|
||||
# g2_alphas = w2_scale_2 * a2_scale_2. The kernel multiplies by these.
|
||||
g_alphas = torch.full((num_experts,), 1.0, device=device, dtype=torch.float32)
|
||||
a2_gscale = torch.full((num_experts,), 1.0, device=device, dtype=torch.float32)
|
||||
|
||||
quant_config = FusedMoEQuantConfig.make(
|
||||
quant_dtype="nvfp4",
|
||||
per_act_token_quant=False,
|
||||
per_out_ch_quant=False,
|
||||
block_shape=None,
|
||||
w1_scale=w13_scale,
|
||||
w2_scale=w2_scale,
|
||||
g1_alphas=g_alphas,
|
||||
g2_alphas=g_alphas,
|
||||
a1_gscale=a_global_scale,
|
||||
a2_gscale=a2_gscale,
|
||||
)
|
||||
|
||||
experts = TrtLlmNvFp4ExpertsMonolithic(
|
||||
moe_config=moe_config, quant_config=quant_config
|
||||
)
|
||||
return experts, w13_q, w13_scale, w2_q, w2_scale, a_global_scale
|
||||
|
||||
|
||||
def _run_nvfp4_monolithic(
|
||||
experts: TrtLlmNvFp4ExpertsMonolithic,
|
||||
hidden_states_q: torch.Tensor,
|
||||
hidden_states_scale: torch.Tensor,
|
||||
router_logits: torch.Tensor,
|
||||
num_experts: int,
|
||||
routing_bias: torch.Tensor,
|
||||
) -> torch.Tensor:
|
||||
"""The monolithic NVFP4 apply expects packed fp4 hidden states + the
|
||||
matching fp8 per-block scale stored in the ``a1q_scale`` slot."""
|
||||
# Stash the weight tensors on the experts in the locations the apply()
|
||||
# implementation reads from (it pulls them from quant_config / scales
|
||||
# already; w1/w2 come in as args).
|
||||
return experts.apply(
|
||||
hidden_states=hidden_states_q,
|
||||
w1=experts._w13_packed,
|
||||
w2=experts._w2_packed,
|
||||
router_logits=router_logits,
|
||||
activation=MoEActivation.SILU,
|
||||
global_num_experts=num_experts,
|
||||
expert_map=None,
|
||||
a1q_scale=hidden_states_scale,
|
||||
apply_router_weight_on_input=False,
|
||||
num_expert_group=_DSV3_N_GROUP,
|
||||
topk_group=_DSV3_TOPK_GROUP,
|
||||
e_score_correction_bias=routing_bias,
|
||||
routed_scaling_factor=1.0,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("num_tokens", [2, 7, 16])
|
||||
@pytest.mark.parametrize("top_k", [2, 4])
|
||||
def test_trtllm_nvfp4_monolithic_routing_replay_records_valid_experts(
|
||||
num_tokens: int,
|
||||
top_k: int,
|
||||
) -> None:
|
||||
"""End-to-end: ``TrtLlmNvFp4ExpertsMonolithic`` captures valid expert IDs
|
||||
on the DSV3 routing path."""
|
||||
if top_k > _DSV3_N_GROUP * _DSV3_TOPK_GROUP:
|
||||
pytest.skip(
|
||||
f"DSV3 requires top_k <= n_group * topk_group "
|
||||
f"({_DSV3_N_GROUP * _DSV3_TOPK_GROUP})"
|
||||
)
|
||||
from flashinfer import fp4_quantize
|
||||
|
||||
torch.manual_seed(0)
|
||||
device = torch.device("cuda:0")
|
||||
|
||||
num_experts = _DSV3_NUM_EXPERTS
|
||||
hidden_size = 1024
|
||||
intermediate_size = 1024
|
||||
block_size = 16
|
||||
|
||||
experts, w13_q, _w13_s, w2_q, _w2_s, a_gs = _make_nvfp4_monolithic_experts(
|
||||
num_experts=num_experts,
|
||||
top_k=top_k,
|
||||
hidden_size=hidden_size,
|
||||
intermediate_size=intermediate_size,
|
||||
device=device,
|
||||
)
|
||||
# The apply() reads w1/w2 from its args, but we keep them on the experts
|
||||
# for convenience of the helper.
|
||||
experts._w13_packed = w13_q
|
||||
experts._w2_packed = w2_q
|
||||
|
||||
assert experts.supports_routing_replay_capture()
|
||||
captured: list[torch.Tensor] = []
|
||||
experts.set_capture_fn(lambda r: captured.append(r.clone()))
|
||||
|
||||
hidden_states = (
|
||||
torch.randn(num_tokens, hidden_size, device=device, dtype=torch.bfloat16) * 0.1
|
||||
)
|
||||
hidden_states_q, hidden_states_scale = fp4_quantize(
|
||||
hidden_states,
|
||||
a_gs,
|
||||
block_size,
|
||||
sf_use_ue8m0=False,
|
||||
is_sf_swizzled_layout=False,
|
||||
)
|
||||
# The vLLM apply() does the .view(fp8_e4m3fn).reshape itself, so leave
|
||||
# ``hidden_states_scale`` in its native (uint8 packed) form.
|
||||
router_logits = torch.rand(
|
||||
num_tokens, num_experts, device=device, dtype=torch.float32
|
||||
)
|
||||
routing_bias = _make_dsv3_routing_bias(num_experts, device)
|
||||
|
||||
_ = _run_nvfp4_monolithic(
|
||||
experts,
|
||||
hidden_states_q=hidden_states_q,
|
||||
hidden_states_scale=hidden_states_scale,
|
||||
router_logits=router_logits,
|
||||
num_experts=num_experts,
|
||||
routing_bias=routing_bias,
|
||||
)
|
||||
|
||||
assert len(captured) == 1
|
||||
replay = captured[0]
|
||||
assert replay.dtype == torch.int16
|
||||
assert replay.shape == (num_tokens, top_k)
|
||||
assert (replay >= 0).all(), f"got out-of-range values: {replay}"
|
||||
assert (replay < num_experts).all(), f"got out-of-range values: {replay}"
|
||||
for t in range(num_tokens):
|
||||
unique = replay[t].unique()
|
||||
assert unique.numel() == top_k, (
|
||||
f"token {t}: expected {top_k} distinct experts, "
|
||||
f"got {unique.numel()} ({replay[t].tolist()})"
|
||||
)
|
||||
@@ -0,0 +1,31 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
from types import SimpleNamespace
|
||||
|
||||
import torch
|
||||
|
||||
from vllm import _custom_ops as ops
|
||||
|
||||
|
||||
def test_cutlass_group_gemm_python_guard_allows_thor(monkeypatch):
|
||||
seen_capabilities: list[int] = []
|
||||
|
||||
def fake_cutlass_group_gemm_supported(capability: int) -> bool:
|
||||
seen_capabilities.append(capability)
|
||||
return True
|
||||
|
||||
monkeypatch.setattr(
|
||||
torch.ops,
|
||||
"_C",
|
||||
SimpleNamespace(cutlass_group_gemm_supported=fake_cutlass_group_gemm_supported),
|
||||
)
|
||||
|
||||
# CUDA 12 reports Thor as SM101 and CUDA 13 reports it as SM110. Both
|
||||
# should reach the C++ query for the SM10x/SM11x CUTLASS MoE kernel.
|
||||
assert ops.cutlass_group_gemm_supported(101)
|
||||
assert ops.cutlass_group_gemm_supported(110)
|
||||
|
||||
# SM120 uses separate kernels and must not be advertised by this path.
|
||||
assert not ops.cutlass_group_gemm_supported(120)
|
||||
assert seen_capabilities == [101, 110]
|
||||
@@ -0,0 +1,306 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
import ast
|
||||
from dataclasses import dataclass
|
||||
from types import SimpleNamespace
|
||||
from typing import Any, cast
|
||||
|
||||
import pytest
|
||||
|
||||
from vllm.model_executor.warmup.jit_warmup import (
|
||||
VllmJitKernel,
|
||||
WarmupIntRange,
|
||||
get_ast_full_name,
|
||||
zip_inputs,
|
||||
)
|
||||
|
||||
|
||||
def _next_power_of_2(value: int) -> int:
|
||||
return 1 << max(0, value - 1).bit_length()
|
||||
|
||||
|
||||
def _round_up(value: int, *, multiple: int) -> int:
|
||||
return ((value + multiple - 1) // multiple) * multiple
|
||||
|
||||
|
||||
def _config(
|
||||
*,
|
||||
bias: int = 0,
|
||||
disabled: bool = False,
|
||||
name: str = "base",
|
||||
vectorized: bool = False,
|
||||
) -> SimpleNamespace:
|
||||
return SimpleNamespace(
|
||||
bias=bias,
|
||||
disabled=disabled,
|
||||
name=name,
|
||||
vectorized=vectorized,
|
||||
)
|
||||
|
||||
|
||||
class ToyKernel(VllmJitKernel["ToyKernel.CompileKey"]):
|
||||
@dataclass(frozen=True)
|
||||
class CompileKey:
|
||||
block_size: int
|
||||
work: int
|
||||
vector_width: int
|
||||
descriptor: tuple[object, ...]
|
||||
enabled: bool
|
||||
|
||||
def dispatch( # type: ignore[override]
|
||||
self,
|
||||
*,
|
||||
tokens: int,
|
||||
cfg: Any,
|
||||
lanes: int = 1,
|
||||
mode: str = "default",
|
||||
debug: int = 0,
|
||||
) -> CompileKey:
|
||||
block_size = _next_power_of_2(tokens)
|
||||
work: int = block_size * lanes + cfg.bias
|
||||
return self.CompileKey(
|
||||
block_size=block_size,
|
||||
work=work,
|
||||
vector_width=4 if cfg.vectorized and block_size >= 4 else 1,
|
||||
descriptor=(
|
||||
cfg.name,
|
||||
mode,
|
||||
-block_size,
|
||||
block_size % 3,
|
||||
block_size**2,
|
||||
),
|
||||
enabled=not cfg.disabled,
|
||||
)
|
||||
|
||||
def get_warmup_keys(self, max_tokens: int, cfg: Any) -> list[CompileKey]:
|
||||
return self._trace_dispatch(self.dispatch)(
|
||||
tokens=WarmupIntRange(1, max_tokens + 1),
|
||||
cfg=cfg,
|
||||
# This argument is intentionally unused by dispatch expressions.
|
||||
debug=WarmupIntRange(0, 100),
|
||||
)
|
||||
|
||||
def compile(self, compile_key: CompileKey) -> None:
|
||||
pass
|
||||
|
||||
|
||||
class RecordingToyKernel(ToyKernel):
|
||||
def __init__(self) -> None:
|
||||
self.compiled: list[ToyKernel.CompileKey] = []
|
||||
super().__init__()
|
||||
|
||||
def compile(self, compile_key: ToyKernel.CompileKey) -> None:
|
||||
self.compiled.append(compile_key)
|
||||
|
||||
|
||||
def test_trace_dispatch_expands_ranges_dedupes_and_ignores_unused_inputs() -> None:
|
||||
cfg = _config()
|
||||
|
||||
assert ToyKernel().get_warmup_keys(5, cfg) == [
|
||||
ToyKernel.CompileKey(1, 1, 1, ("base", "default", -1, 1, 1), True),
|
||||
ToyKernel.CompileKey(2, 2, 1, ("base", "default", -2, 2, 4), True),
|
||||
ToyKernel.CompileKey(4, 4, 1, ("base", "default", -4, 1, 16), True),
|
||||
ToyKernel.CompileKey(8, 8, 1, ("base", "default", -8, 2, 64), True),
|
||||
]
|
||||
|
||||
|
||||
def test_compile_key_uses_defaults_locals_attributes_and_expressions() -> None:
|
||||
cfg = _config(bias=3, disabled=True, name="cfg", vectorized=True)
|
||||
|
||||
assert ToyKernel().compile_key(
|
||||
{
|
||||
"tokens": 4,
|
||||
"cfg": cfg,
|
||||
"lanes": 2,
|
||||
}
|
||||
) == ToyKernel.CompileKey(
|
||||
block_size=4,
|
||||
work=11,
|
||||
vector_width=4,
|
||||
descriptor=("cfg", "default", -4, 1, 16),
|
||||
enabled=False,
|
||||
)
|
||||
|
||||
|
||||
def test_trace_dispatch_combines_zipped_rows_with_independent_values() -> None:
|
||||
cfg = _config(vectorized=True)
|
||||
|
||||
keys = ToyKernel()._trace_dispatch(ToyKernel().dispatch)(
|
||||
zip_inputs(
|
||||
dict(tokens=1, mode="small"),
|
||||
dict(tokens=4, mode="wide"),
|
||||
),
|
||||
cfg=cfg,
|
||||
lanes=(1, 2),
|
||||
)
|
||||
|
||||
assert keys == [
|
||||
ToyKernel.CompileKey(1, 1, 1, ("base", "small", -1, 1, 1), True),
|
||||
ToyKernel.CompileKey(1, 2, 1, ("base", "small", -1, 1, 1), True),
|
||||
ToyKernel.CompileKey(4, 4, 4, ("base", "wide", -4, 1, 16), True),
|
||||
ToyKernel.CompileKey(4, 8, 4, ("base", "wide", -4, 1, 16), True),
|
||||
]
|
||||
|
||||
|
||||
def test_zip_inputs_validates_input_rows() -> None:
|
||||
with pytest.raises(ValueError, match="requires at least one"):
|
||||
zip_inputs()
|
||||
with pytest.raises(ValueError, match="rows must be mappings"):
|
||||
zip_inputs(cast(Any, ("tokens", 1)))
|
||||
with pytest.raises(ValueError, match="at least one dispatch input name"):
|
||||
zip_inputs({})
|
||||
with pytest.raises(ValueError, match="dispatch input names must be strings"):
|
||||
zip_inputs(cast(Any, {1: 2}))
|
||||
with pytest.raises(ValueError, match="same dispatch input names"):
|
||||
zip_inputs({"tokens": 1}, {"mode": "small"})
|
||||
|
||||
|
||||
def test_trace_dispatch_rejects_bad_positional_groups_and_duplicates() -> None:
|
||||
kernel = ToyKernel()
|
||||
|
||||
with pytest.raises(TypeError, match="zip_inputs"):
|
||||
kernel._trace_dispatch(kernel.dispatch)(
|
||||
cast(Any, {"tokens": 1}),
|
||||
cfg=_config(),
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="specified more than once"):
|
||||
kernel._trace_dispatch(kernel.dispatch)(
|
||||
zip_inputs(dict(tokens=1, mode="small")),
|
||||
tokens=2,
|
||||
cfg=_config(),
|
||||
)
|
||||
|
||||
|
||||
def test_helper_calls_support_keywords_and_reject_star_kwargs() -> None:
|
||||
class HelperKernel(VllmJitKernel["HelperKernel.CompileKey"]):
|
||||
@dataclass(frozen=True)
|
||||
class CompileKey:
|
||||
value: int
|
||||
|
||||
def dispatch( # type: ignore[override]
|
||||
self,
|
||||
*,
|
||||
tokens: int,
|
||||
block_size: int,
|
||||
) -> CompileKey:
|
||||
return self.CompileKey(value=_round_up(tokens, multiple=block_size))
|
||||
|
||||
def get_warmup_keys(self) -> list[CompileKey]:
|
||||
return []
|
||||
|
||||
def compile(self, compile_key: CompileKey) -> None:
|
||||
pass
|
||||
|
||||
class StarKwargsKernel(VllmJitKernel["StarKwargsKernel.CompileKey"]):
|
||||
@dataclass(frozen=True)
|
||||
class CompileKey:
|
||||
value: int
|
||||
|
||||
def dispatch( # type: ignore[override]
|
||||
self,
|
||||
*,
|
||||
tokens: int,
|
||||
block_size: int,
|
||||
) -> CompileKey:
|
||||
return self.CompileKey(value=_round_up(tokens, **{"multiple": block_size}))
|
||||
|
||||
def get_warmup_keys(self) -> list[CompileKey]:
|
||||
return []
|
||||
|
||||
def compile(self, compile_key: CompileKey) -> None:
|
||||
pass
|
||||
|
||||
assert HelperKernel().compile_key(
|
||||
{
|
||||
"tokens": 5,
|
||||
"block_size": 4,
|
||||
}
|
||||
) == HelperKernel.CompileKey(value=8)
|
||||
with pytest.raises(ValueError, match=r"cannot use \*\*kwargs"):
|
||||
StarKwargsKernel().compile_key({"tokens": 5, "block_size": 4})
|
||||
|
||||
|
||||
def test_dispatch_body_must_be_local_assignments_then_compile_key_return() -> None:
|
||||
class BranchKernel(VllmJitKernel["BranchKernel.CompileKey"]):
|
||||
@dataclass(frozen=True)
|
||||
class CompileKey:
|
||||
value: int
|
||||
|
||||
def dispatch(self, *, value: int) -> CompileKey: # type: ignore[override]
|
||||
if value > 0:
|
||||
value = 1
|
||||
return self.CompileKey(value=value)
|
||||
|
||||
def get_warmup_keys(self) -> list[CompileKey]:
|
||||
return []
|
||||
|
||||
def compile(self, compile_key: CompileKey) -> None:
|
||||
pass
|
||||
|
||||
class KwargsReturnKernel(VllmJitKernel["KwargsReturnKernel.CompileKey"]):
|
||||
@dataclass(frozen=True)
|
||||
class CompileKey:
|
||||
value: int
|
||||
|
||||
def dispatch(self, *, value: int) -> CompileKey: # type: ignore[override]
|
||||
return self.CompileKey(**{"value": value})
|
||||
|
||||
def get_warmup_keys(self) -> list[CompileKey]:
|
||||
return []
|
||||
|
||||
def compile(self, compile_key: CompileKey) -> None:
|
||||
pass
|
||||
|
||||
with pytest.raises(ValueError, match="local assignments"):
|
||||
BranchKernel()
|
||||
with pytest.raises(ValueError, match=r"cannot use \*\*kwargs in CompileKey"):
|
||||
KwargsReturnKernel()
|
||||
|
||||
|
||||
def test_dispatch_reports_unsupported_expression_with_context() -> None:
|
||||
class UnsupportedKernel(VllmJitKernel["UnsupportedKernel.CompileKey"]):
|
||||
@dataclass(frozen=True)
|
||||
class CompileKey:
|
||||
value: object
|
||||
|
||||
def dispatch(self, *, value: int) -> CompileKey: # type: ignore[override]
|
||||
return self.CompileKey(value={value})
|
||||
|
||||
def get_warmup_keys(self) -> list[CompileKey]:
|
||||
return []
|
||||
|
||||
def compile(self, compile_key: CompileKey) -> None:
|
||||
pass
|
||||
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
UnsupportedKernel().compile_key({"value": 1})
|
||||
|
||||
message = str(exc_info.value)
|
||||
assert "Unsupported dispatch expression" in message
|
||||
assert "{value}" in message
|
||||
assert "Supported dispatch expressions" in message
|
||||
|
||||
|
||||
def test_warmup_compiles_all_returned_keys_in_order() -> None:
|
||||
kernel = RecordingToyKernel()
|
||||
cfg = _config()
|
||||
|
||||
kernel.warmup(3, cfg)
|
||||
|
||||
assert kernel.compiled == [
|
||||
ToyKernel.CompileKey(1, 1, 1, ("base", "default", -1, 1, 1), True),
|
||||
ToyKernel.CompileKey(2, 2, 1, ("base", "default", -2, 2, 4), True),
|
||||
ToyKernel.CompileKey(4, 4, 1, ("base", "default", -4, 1, 16), True),
|
||||
]
|
||||
|
||||
|
||||
def test_get_ast_full_name_handles_names_attributes_and_other_nodes() -> None:
|
||||
dotted_expr = ast.parse("foo.bar.baz").body[0]
|
||||
call_expr = ast.parse("foo()").body[0]
|
||||
assert isinstance(dotted_expr, ast.Expr)
|
||||
assert isinstance(call_expr, ast.Expr)
|
||||
|
||||
assert get_ast_full_name(dotted_expr.value) == "foo.bar.baz"
|
||||
assert get_ast_full_name(call_expr.value) is None
|
||||
@@ -66,6 +66,12 @@ def _make_router(eplb_state: EplbLayerState | None = None) -> DummyRouter:
|
||||
)
|
||||
|
||||
|
||||
def _make_modular_routed_experts():
|
||||
return types.SimpleNamespace(
|
||||
quant_method=types.SimpleNamespace(is_monolithic=False),
|
||||
)
|
||||
|
||||
|
||||
def test_base_router_capture_pre_eplb_mapping():
|
||||
router = _make_router()
|
||||
captured = []
|
||||
@@ -122,6 +128,8 @@ def test_gpu_model_runner_binds_router_capture(monkeypatch):
|
||||
def __init__(self):
|
||||
self.layer_id = 7
|
||||
self.router = _make_router()
|
||||
self.routed_experts = _make_modular_routed_experts()
|
||||
self._quant_method = self.routed_experts.quant_method
|
||||
|
||||
class DummyCapturer:
|
||||
def __init__(self):
|
||||
@@ -160,6 +168,8 @@ def test_gpu_model_runner_binding_stage(monkeypatch):
|
||||
def __init__(self):
|
||||
self.layer_id = 11
|
||||
self.router = _make_router()
|
||||
self.routed_experts = _make_modular_routed_experts()
|
||||
self._quant_method = self.routed_experts.quant_method
|
||||
|
||||
class DummyCapturer:
|
||||
def __init__(self):
|
||||
@@ -197,6 +207,8 @@ def test_gpu_model_runner_does_not_bind_draft_router_capture(monkeypatch):
|
||||
def __init__(self, layer_id):
|
||||
self.layer_id = layer_id
|
||||
self.router = _make_router()
|
||||
self.routed_experts = _make_modular_routed_experts()
|
||||
self._quant_method = self.routed_experts.quant_method
|
||||
|
||||
target_module = DummyFusedMoE(layer_id=7)
|
||||
draft_module = DummyFusedMoE(layer_id=0)
|
||||
@@ -222,6 +234,49 @@ def test_gpu_model_runner_does_not_bind_draft_router_capture(monkeypatch):
|
||||
assert draft_module.router.capture_fn is None
|
||||
|
||||
|
||||
def test_gpu_model_runner_rejects_monolithic_without_replay_support(monkeypatch):
|
||||
from vllm.v1.worker import gpu_model_runner as gmr
|
||||
|
||||
class DummyFusedMoE:
|
||||
def __init__(self):
|
||||
self.layer_id = 3
|
||||
self.router = _make_router()
|
||||
# Use a concrete monolithic expert and override its capability
|
||||
# instead of instantiating the abstract base class directly.
|
||||
from vllm.model_executor.layers.fused_moe.experts.cpu_moe import (
|
||||
CPUExpertsFp8,
|
||||
)
|
||||
|
||||
fused_experts = CPUExpertsFp8.__new__(CPUExpertsFp8)
|
||||
self.routed_experts = types.SimpleNamespace(
|
||||
quant_method=types.SimpleNamespace(
|
||||
is_monolithic=True,
|
||||
moe_kernel=types.SimpleNamespace(
|
||||
impl=types.SimpleNamespace(fused_experts=fused_experts)
|
||||
),
|
||||
)
|
||||
)
|
||||
self._quant_method = self.routed_experts.quant_method
|
||||
self._quant_method.moe_kernel.impl.fused_experts = fused_experts
|
||||
fused_experts.supports_routing_replay_capture = lambda: False
|
||||
|
||||
class DummyCapturer:
|
||||
def capture(self, layer_id, topk_ids):
|
||||
pass
|
||||
|
||||
dummy_module = DummyFusedMoE()
|
||||
import vllm.model_executor.layers.fused_moe.layer as fused_moe_layer
|
||||
|
||||
monkeypatch.setattr(fused_moe_layer, "MoERunner", DummyFusedMoE)
|
||||
|
||||
dummy_self = types.SimpleNamespace(
|
||||
model=types.SimpleNamespace(modules=lambda: [dummy_module])
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="monolithic MoE kernel"):
|
||||
gmr.GPUModelRunner._bind_routed_experts_capturer(dummy_self, DummyCapturer())
|
||||
|
||||
|
||||
def test_routed_experts_capturer_single_dp_no_metadata():
|
||||
"""dp_metadata is None: capture writes the full topk_ids rows."""
|
||||
capturer = _capturer_with_buffer(dp_rank=0)
|
||||
|
||||
@@ -9,6 +9,7 @@ import torch.nn.functional as F
|
||||
from transformers import AutoModel
|
||||
|
||||
from vllm.platforms import current_platform
|
||||
from vllm.utils.mem_constants import MiB_bytes
|
||||
|
||||
from ....conftest import HfRunner
|
||||
from ....utils import VLLM_PATH
|
||||
@@ -106,7 +107,12 @@ def test_prm_models(
|
||||
if current_platform.is_cpu():
|
||||
pytest.skip("CPU only supports V1")
|
||||
|
||||
with vllm_runner(model, max_model_len=1024, dtype=dtype) as vllm_model:
|
||||
with vllm_runner(
|
||||
model,
|
||||
max_model_len=1024,
|
||||
dtype=dtype,
|
||||
kv_cache_memory_bytes=64 * MiB_bytes,
|
||||
) as vllm_model:
|
||||
vllm_outputs = vllm_model.token_classify(math_step_prompts)
|
||||
|
||||
with hf_runner(model, dtype=dtype, auto_cls=AutoModel) as hf_model:
|
||||
@@ -145,7 +151,12 @@ def test_prm_models_with_golden_outputs(
|
||||
if not FIXTURE_REWARD_RESULT.get(model):
|
||||
pytest.skip(f"No available golden outputs for {model}.")
|
||||
|
||||
with vllm_runner(model, max_model_len=1024, dtype=dtype) as vllm_model:
|
||||
with vllm_runner(
|
||||
model,
|
||||
max_model_len=1024,
|
||||
dtype=dtype,
|
||||
kv_cache_memory_bytes=64 * MiB_bytes,
|
||||
) as vllm_model:
|
||||
vllm_outputs = vllm_model.token_classify(math_step_prompts)
|
||||
|
||||
golden_outputs = load_reward_outputs(FIXTURE_REWARD_RESULT[model])
|
||||
|
||||
@@ -74,6 +74,44 @@ def test_cosmos3_new_checkpoint_weights_mapper():
|
||||
)
|
||||
|
||||
|
||||
def test_cosmos3_modelopt_quantizer_weights_mapper():
|
||||
"""ModelOpt/Diffusers FP8 checkpoints ship native fake-quant buffers
|
||||
(``*_quantizer._amax`` / ``._scale``) alongside the vLLM-consumable
|
||||
``weight_scale`` / ``input_scale`` sidecars. vLLM must drop the former
|
||||
(it has no parameter for them) while keeping the latter."""
|
||||
from vllm.model_executor.models.cosmos3 import Cosmos3ForConditionalGeneration
|
||||
|
||||
mapper = Cosmos3ForConditionalGeneration.hf_to_vllm_mapper
|
||||
|
||||
# Native ModelOpt quantizer buffers are dropped.
|
||||
assert (
|
||||
mapper.apply_list(
|
||||
[
|
||||
"layers.0.self_attn.to_q.input_quantizer._amax",
|
||||
"layers.0.self_attn.to_q.weight_quantizer._amax",
|
||||
"layers.0.self_attn.to_q.weight_quantizer._scale",
|
||||
"layers.0.mlp.down_proj.output_quantizer._amax",
|
||||
]
|
||||
)
|
||||
== []
|
||||
)
|
||||
|
||||
# The FP8 scale sidecars vLLM actually consumes are kept and remapped.
|
||||
assert mapper.apply_list(
|
||||
[
|
||||
"layers.0.self_attn.to_q.weight",
|
||||
"layers.0.self_attn.to_q.weight_scale",
|
||||
"layers.0.self_attn.to_q.input_scale",
|
||||
"layers.0.mlp.down_proj.input_scale",
|
||||
]
|
||||
) == [
|
||||
"language_model.model.layers.0.self_attn.q_proj.weight",
|
||||
"language_model.model.layers.0.self_attn.q_proj.weight_scale",
|
||||
"language_model.model.layers.0.self_attn.q_proj.input_scale",
|
||||
"language_model.model.layers.0.mlp.down_proj.input_scale",
|
||||
]
|
||||
|
||||
|
||||
def test_cosmos3_edge_checkpoint_weights_mapper():
|
||||
from vllm.model_executor.models.cosmos3_edge import (
|
||||
Cosmos3EdgeForConditionalGeneration,
|
||||
|
||||
@@ -9,8 +9,8 @@ Note: these tests will only pass on L4 GPU.
|
||||
import pytest
|
||||
|
||||
from tests.quantization.utils import is_quant_method_supported
|
||||
from vllm.v1.attention.backends.fa_utils import flash_attn_supports_kv_cache_dtype
|
||||
from vllm.platforms import current_platform
|
||||
from vllm.v1.attention.backends.fa_utils import get_flash_attn_version
|
||||
from ..utils import check_logprobs_close
|
||||
|
||||
|
||||
@@ -70,13 +70,7 @@ def test_models(
|
||||
if kv_cache_dtype == "fp8_e5m2" and current_platform.is_cuda():
|
||||
pytest.skip(f"{kv_cache_dtype} is not supported by FLASH_ATTN on CUDA.")
|
||||
|
||||
if not (
|
||||
current_platform.is_xpu()
|
||||
or (
|
||||
get_flash_attn_version() == 3
|
||||
and current_platform.is_device_capability_family(90)
|
||||
)
|
||||
):
|
||||
if not flash_attn_supports_kv_cache_dtype(kv_cache_dtype):
|
||||
pytest.skip(
|
||||
f"{kv_cache_dtype} is not supported on this GPU type with {backend} attention."
|
||||
)
|
||||
|
||||
@@ -6,6 +6,7 @@ import mimetypes
|
||||
import os
|
||||
import shutil
|
||||
import time
|
||||
from io import BytesIO
|
||||
from tempfile import NamedTemporaryFile, TemporaryDirectory
|
||||
|
||||
import aiohttp
|
||||
@@ -111,6 +112,34 @@ async def test_fetch_image_base64(
|
||||
assert _image_equals(data_image_sync, data_image_async)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_fetch_image_keep_original_mode():
|
||||
"""media_io_kwargs can disable the default RGB conversion."""
|
||||
# RGBA image: opaque black pixel on a fully transparent background
|
||||
rgba_image = Image.new("RGBA", (4, 4), (0, 0, 0, 0))
|
||||
rgba_image.putpixel((2, 2), (0, 0, 0, 255))
|
||||
buffer = BytesIO()
|
||||
rgba_image.save(buffer, "PNG")
|
||||
data_url = (
|
||||
f"data:image/png;base64,{base64.b64encode(buffer.getvalue()).decode('utf-8')}"
|
||||
)
|
||||
|
||||
# Default behavior: RGBA is composited onto a white background
|
||||
default_image = MediaConnector().fetch_image(data_url)
|
||||
assert default_image.mode == "RGB"
|
||||
assert default_image.getpixel((0, 0)) == (255, 255, 255)
|
||||
assert default_image.getpixel((2, 2)) == (0, 0, 0)
|
||||
|
||||
# image_mode=None via media_io_kwargs: original mode is preserved
|
||||
connector = MediaConnector(media_io_kwargs={"image": {"image_mode": None}})
|
||||
image_sync = connector.fetch_image(data_url)
|
||||
image_async = await connector.fetch_image_async(data_url)
|
||||
for image in (image_sync, image_async):
|
||||
assert image.mode == "RGBA"
|
||||
assert image.getpixel((0, 0)) == (0, 0, 0, 0)
|
||||
assert image.getpixel((2, 2)) == (0, 0, 0, 255)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("image_url", TEST_IMAGE_ASSETS, indirect=True)
|
||||
async def test_fetch_image_local_files(image_url: str):
|
||||
|
||||
@@ -80,6 +80,29 @@ def test_image_media_io_rgba_custom_background(tmp_path):
|
||||
assert green_numpy[0][0][2] == 0 # B
|
||||
|
||||
|
||||
def test_image_media_io_no_mode_conversion(tmp_path):
|
||||
"""image_mode=None skips conversion and preserves the original mode."""
|
||||
# RGBA image: opaque black pixel on a fully transparent background
|
||||
rgba_image = Image.new("RGBA", (10, 10), (0, 0, 0, 0))
|
||||
rgba_image.putpixel((5, 5), (0, 0, 0, 255))
|
||||
test_image_path = tmp_path / "test_rgba.png"
|
||||
rgba_image.save(test_image_path)
|
||||
|
||||
# Default behavior: RGBA is composited onto a white background
|
||||
image_io_default = ImageMediaIO()
|
||||
converted_default = image_io_default.load_file(test_image_path)
|
||||
assert converted_default.media.mode == "RGB"
|
||||
assert converted_default.media.getpixel((0, 0)) == (255, 255, 255)
|
||||
assert converted_default.media.getpixel((5, 5)) == (0, 0, 0)
|
||||
|
||||
# image_mode=None: original mode and alpha channel are preserved
|
||||
image_io_keep = ImageMediaIO(image_mode=None)
|
||||
converted_keep = image_io_keep.load_file(test_image_path)
|
||||
assert converted_keep.media.mode == "RGBA"
|
||||
assert converted_keep.media.getpixel((0, 0)) == (0, 0, 0, 0)
|
||||
assert converted_keep.media.getpixel((5, 5)) == (0, 0, 0, 255)
|
||||
|
||||
|
||||
def test_image_media_io_rgba_background_color_validation():
|
||||
"""Test that invalid rgba_background_color values are properly rejected."""
|
||||
|
||||
|
||||
@@ -165,6 +165,38 @@ def test_modelopt_mixed_precision_does_not_quantize_unlisted_fused_sibling():
|
||||
assert config._resolve_quant_algo("model.layers.0.linear_attn.in_proj_ba") is None
|
||||
|
||||
|
||||
def test_modelopt_mixed_precision_composes_gemma4_mappers():
|
||||
from vllm.model_executor.models.gemma4 import Gemma4ForCausalLM
|
||||
from vllm.model_executor.models.gemma4_mm import (
|
||||
Gemma4ForConditionalGeneration,
|
||||
)
|
||||
|
||||
config = _mixed_precision_config(
|
||||
{
|
||||
"model.language_model.layers.0.experts": {
|
||||
"quant_algo": "NVFP4",
|
||||
"group_size": 16,
|
||||
},
|
||||
"model.language_model.layers.1.moe.experts.gate_up_proj": {
|
||||
"quant_algo": "NVFP4",
|
||||
"group_size": 16,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
config.apply_vllm_mapper(
|
||||
Gemma4ForConditionalGeneration.hf_to_vllm_mapper.get_unstacked_mapper()
|
||||
)
|
||||
config.apply_vllm_mapper(Gemma4ForCausalLM.hf_to_vllm_mapper.get_unstacked_mapper())
|
||||
|
||||
expected_prefix = "language_model.model.layers.0.moe.experts"
|
||||
assert set(config.quantized_layers) == {
|
||||
expected_prefix,
|
||||
"language_model.model.layers.1.moe.gate_up_proj",
|
||||
}
|
||||
assert config._resolve_quant_algo(expected_prefix) == "NVFP4"
|
||||
|
||||
|
||||
def test_modelopt_mixed_precision_infers_fused_gate_up_projection():
|
||||
from vllm.model_executor.layers.linear import LinearBase
|
||||
|
||||
|
||||
@@ -1,141 +0,0 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from vllm.utils.extensible_tensor import ExtensibleTensor
|
||||
|
||||
pytestmark = pytest.mark.skipif(not torch.cuda.is_available(), reason="requires CUDA")
|
||||
|
||||
|
||||
def test_extensible_tensor_grows_without_moving() -> None:
|
||||
buffer = ExtensibleTensor(4096, device="cuda")
|
||||
try:
|
||||
base_ptr = buffer.base_ptr
|
||||
first_view = buffer.resize_(1024)
|
||||
assert first_view.data_ptr() == base_ptr
|
||||
first_view.fill_(7)
|
||||
|
||||
second_view = buffer.resize_(2048)
|
||||
assert second_view.data_ptr() == base_ptr
|
||||
assert torch.equal(second_view[:1024], torch.full_like(second_view[:1024], 7))
|
||||
|
||||
second_view[1024:].fill_(3)
|
||||
assert torch.equal(buffer.tensor, second_view)
|
||||
|
||||
full_view = buffer.full_view()
|
||||
assert full_view.data_ptr() == base_ptr
|
||||
assert full_view.numel() == 4096
|
||||
finally:
|
||||
buffer.free()
|
||||
|
||||
|
||||
def test_extensible_tensor_rejects_shrink_and_overflow() -> None:
|
||||
buffer = ExtensibleTensor(1024, device="cuda")
|
||||
try:
|
||||
buffer.resize_(512)
|
||||
with pytest.raises(ValueError, match="grow-only"):
|
||||
buffer.resize_(256)
|
||||
with pytest.raises(ValueError, match="exceeds the segment capacity"):
|
||||
buffer.resize_(1025)
|
||||
finally:
|
||||
buffer.free()
|
||||
|
||||
|
||||
def test_segments_grow_in_lockstep_and_zero_new() -> None:
|
||||
"""Each segment's committed prefix grows in lockstep.
|
||||
|
||||
Data written to a segment's committed prefix survives a grow; the newly
|
||||
committed range of each segment is zeroed with `zero_new=True` while old
|
||||
bytes are preserved.
|
||||
"""
|
||||
et = ExtensibleTensor(max_num_bytes=8192, device="cuda", num_segments=2)
|
||||
try:
|
||||
assert et.num_segments == 2
|
||||
assert et.segment_capacity_bytes == 4096
|
||||
|
||||
et.resize_per_segment_(256, zero_new=True)
|
||||
assert et.bytes_per_segment == 256
|
||||
assert et.num_bytes == 512
|
||||
fv = et.full_view()
|
||||
assert fv.shape == (8192,)
|
||||
# Committed prefixes start zeroed.
|
||||
assert torch.count_nonzero(fv[:256]) == 0
|
||||
assert torch.count_nonzero(fv[4096 : 4096 + 256]) == 0
|
||||
|
||||
pattern_a = torch.arange(256, device="cuda", dtype=torch.uint8)
|
||||
pattern_b = 255 - pattern_a
|
||||
fv[:256].copy_(pattern_a)
|
||||
fv[4096 : 4096 + 256].copy_(pattern_b)
|
||||
|
||||
et.resize_per_segment_(1024, zero_new=True)
|
||||
fv2 = et.full_view()
|
||||
assert fv2.data_ptr() == fv.data_ptr()
|
||||
# Old bytes of both segments preserved; freshly committed ranges zeroed.
|
||||
assert torch.equal(fv2[:256], pattern_a)
|
||||
assert torch.equal(fv2[4096 : 4096 + 256], pattern_b)
|
||||
assert torch.count_nonzero(fv2[256:1024]) == 0
|
||||
assert torch.count_nonzero(fv2[4096 + 256 : 4096 + 1024]) == 0
|
||||
finally:
|
||||
et.free()
|
||||
|
||||
|
||||
def test_segments_at_granularity_scale() -> None:
|
||||
"""Segments spanning multiple mapping granules commit correctly.
|
||||
|
||||
Uses a segment capacity that is not a multiple of the allocation
|
||||
granularity, so a granule straddles the segment boundary and is shared by
|
||||
the first commit of one segment and a later commit of the other -- it must
|
||||
be mapped exactly once.
|
||||
"""
|
||||
probe = ExtensibleTensor(max_num_bytes=1, device="cuda")
|
||||
granularity = probe.capacity_bytes
|
||||
probe.free()
|
||||
# Two segments of 1.5 granules each; the middle granule straddles the
|
||||
# boundary.
|
||||
max_num_bytes = 3 * granularity
|
||||
et = ExtensibleTensor(max_num_bytes=max_num_bytes, device="cuda", num_segments=2)
|
||||
try:
|
||||
seg = et.segment_capacity_bytes
|
||||
assert seg == max_num_bytes // 2
|
||||
|
||||
step = granularity // 2
|
||||
et.resize_per_segment_(step, zero_new=True)
|
||||
fv = et.full_view()
|
||||
fv[:step].fill_(1)
|
||||
fv[seg : seg + step].fill_(2)
|
||||
|
||||
# Grow to the full segment capacity: previously mapped granules
|
||||
# (including the boundary-straddling one) are reused, new ones are
|
||||
# committed and zeroed.
|
||||
et.resize_per_segment_(seg, zero_new=True)
|
||||
fv2 = et.full_view()
|
||||
assert torch.all(fv2[:step] == 1)
|
||||
assert torch.all(fv2[seg : seg + step] == 2)
|
||||
assert torch.count_nonzero(fv2[step:seg]) == 0
|
||||
assert torch.count_nonzero(fv2[seg + step :]) == 0
|
||||
finally:
|
||||
et.free()
|
||||
|
||||
|
||||
def test_multi_segment_invalid_usage_raises() -> None:
|
||||
"""Prefix-view APIs and invalid segment configs raise for multi-segment
|
||||
buffers."""
|
||||
with pytest.raises(ValueError):
|
||||
ExtensibleTensor(max_num_bytes=100, device="cuda", num_segments=3)
|
||||
|
||||
et = ExtensibleTensor(max_num_bytes=8192, device="cuda", num_segments=2)
|
||||
try:
|
||||
with pytest.raises(ValueError):
|
||||
_ = et.tensor
|
||||
with pytest.raises(ValueError):
|
||||
et.resize_(256)
|
||||
|
||||
et.resize_per_segment_(256)
|
||||
with pytest.raises(ValueError):
|
||||
et.resize_per_segment_(128) # shrink
|
||||
with pytest.raises(ValueError):
|
||||
et.resize_per_segment_(et.segment_capacity_bytes + 1) # over capacity
|
||||
finally:
|
||||
et.free()
|
||||
@@ -67,6 +67,23 @@ def test_memory_profiling():
|
||||
non_torch_ratio = result.non_torch_increase / (256 * 1024 * 1024) # noqa
|
||||
assert abs(non_torch_ratio - 1) <= 0.05
|
||||
assert result.torch_peak_increase == 1024 * 1024 * 1024
|
||||
|
||||
expected_total_consumed = (256 + 512) * 1024 * 1024
|
||||
total_consumed_ratio = result.total_consumed / expected_total_consumed
|
||||
assert abs(total_consumed_ratio - 1) <= 0.05, (
|
||||
f"total_consumed={result.total_consumed}, "
|
||||
f"expected={expected_total_consumed}, "
|
||||
f"ratio={total_consumed_ratio}"
|
||||
)
|
||||
|
||||
expected_non_kv = expected_total_consumed + 1024 * 1024 * 1024
|
||||
non_kv_ratio = result.non_kv_cache_memory / expected_non_kv
|
||||
assert abs(non_kv_ratio - 1) <= 0.05, (
|
||||
f"non_kv_cache_memory={result.non_kv_cache_memory}, "
|
||||
f"expected={expected_non_kv}, "
|
||||
f"ratio={non_kv_ratio}"
|
||||
)
|
||||
|
||||
del weights
|
||||
lib.cudaFree(handle1)
|
||||
lib.cudaFree(handle2)
|
||||
|
||||
@@ -21,6 +21,7 @@ from vllm.platforms import current_platform
|
||||
from vllm.utils.math_utils import cdiv
|
||||
from vllm.utils.torch_utils import (
|
||||
STR_DTYPE_TO_TORCH_DTYPE,
|
||||
is_quantized_kv_cache,
|
||||
is_torch_equal_or_newer,
|
||||
set_random_seed,
|
||||
)
|
||||
@@ -45,6 +46,11 @@ BACKENDS_TO_TEST = [
|
||||
|
||||
DEVICE_TYPE = current_platform.device_type
|
||||
|
||||
FP8_KV_CACHE_DTYPES = {
|
||||
"fp8": torch.float8_e4m3fn,
|
||||
"fp8_e4m3": torch.float8_e4m3fn,
|
||||
}
|
||||
|
||||
# Remove flashinfer from the list if it's not available
|
||||
try:
|
||||
import flashinfer # noqa: F401
|
||||
@@ -110,6 +116,7 @@ def create_and_prepopulate_kv_cache(
|
||||
num_blocks: int,
|
||||
common_attn_metadata: CommonAttentionMetadata,
|
||||
randomize_blocks: bool = True,
|
||||
kv_cache_dtype: str = "auto",
|
||||
) -> torch.Tensor:
|
||||
"""Create and prepopulate a KV cache with context data.
|
||||
|
||||
@@ -140,8 +147,18 @@ def create_and_prepopulate_kv_cache(
|
||||
block_table = common_attn_metadata.block_table_tensor
|
||||
slot_mapping = common_attn_metadata.slot_mapping
|
||||
|
||||
# For an fp8 kv cache, store the cache in the fp8 dtype so that assigning
|
||||
# the higher-precision context tensors quantizes them, mirroring runtime.
|
||||
fp8_kv_cache = is_quantized_kv_cache(kv_cache_dtype)
|
||||
storage_dtype = FP8_KV_CACHE_DTYPES[kv_cache_dtype] if fp8_kv_cache else dtype
|
||||
|
||||
kv_cache = torch.zeros(
|
||||
num_blocks, block_size, num_kv_heads, 2 * head_size, dtype=dtype, device=device
|
||||
num_blocks,
|
||||
block_size,
|
||||
num_kv_heads,
|
||||
2 * head_size,
|
||||
dtype=storage_dtype,
|
||||
device=device,
|
||||
)
|
||||
kv_cache_flat = kv_cache.view(-1, num_kv_heads, 2 * head_size)
|
||||
|
||||
@@ -195,7 +212,12 @@ def create_and_prepopulate_kv_cache(
|
||||
] * block_size + token_inter_block_offsets.to(device)
|
||||
|
||||
# Transpose to logical (num_blocks, num_kv_heads, block_size, 2*hs)
|
||||
return kv_cache.transpose(1, 2).contiguous()
|
||||
kv_cache = kv_cache.transpose(1, 2).contiguous()
|
||||
|
||||
if fp8_kv_cache:
|
||||
kv_cache = kv_cache.view(torch.uint8)
|
||||
|
||||
return kv_cache
|
||||
|
||||
|
||||
class MockAttentionLayer:
|
||||
@@ -224,6 +246,7 @@ def run_attention_backend(
|
||||
kv_cache: torch.Tensor,
|
||||
attn_type: AttentionType = AttentionType.DECODER,
|
||||
sliding_window: int | None = None,
|
||||
kv_cache_dtype: str = "auto",
|
||||
) -> torch.Tensor:
|
||||
"""Run attention computation using the specified backend's AttentionImpl."""
|
||||
|
||||
@@ -291,13 +314,16 @@ def run_attention_backend(
|
||||
alibi_slopes=None,
|
||||
sliding_window=sliding_window,
|
||||
attn_type=attn_type,
|
||||
kv_cache_dtype="auto",
|
||||
kv_cache_dtype=kv_cache_dtype,
|
||||
)
|
||||
|
||||
# Create mock layer and output buffer
|
||||
mock_layer = MockAttentionLayer(device)
|
||||
output = torch.empty_like(query)
|
||||
|
||||
if is_quantized_kv_cache(kv_cache_dtype) and impl.supports_quant_query_input:
|
||||
query = query.to(current_platform.fp8_dtype())
|
||||
|
||||
# Run forward pass
|
||||
# NOTE: The query, key, and value are already shaped correctly
|
||||
# in the calling test function.
|
||||
@@ -324,6 +350,7 @@ def _test_backend_correctness(
|
||||
atol: float = 1e-2,
|
||||
rtol: float = 1e-2,
|
||||
tensor_parallel_size: int = 1,
|
||||
kv_cache_dtype: str = "auto",
|
||||
):
|
||||
"""
|
||||
Test that all backends produce similar outputs to a reference implementation
|
||||
@@ -372,6 +399,7 @@ def _test_backend_correctness(
|
||||
num_gpu_blocks=8192,
|
||||
hf_config_override=hf_config_override,
|
||||
)
|
||||
vllm_config.cache_config.cache_dtype = kv_cache_dtype
|
||||
device = torch.device(f"{DEVICE_TYPE}:0")
|
||||
|
||||
kv_cache_spec = create_standard_kv_cache_spec(vllm_config, attn_type)
|
||||
@@ -392,6 +420,13 @@ def _test_backend_correctness(
|
||||
block_size = vllm_config.cache_config.block_size
|
||||
scale = 1.0 / (head_size**0.5)
|
||||
|
||||
fp8_kv_cache = is_quantized_kv_cache(kv_cache_dtype)
|
||||
if fp8_kv_cache:
|
||||
query_fp8_dtype = current_platform.fp8_dtype()
|
||||
kv_fp8_dtype = FP8_KV_CACHE_DTYPES[kv_cache_dtype]
|
||||
atol = max(atol, 6e-2)
|
||||
rtol = max(rtol, 1e-1)
|
||||
|
||||
# 2. Generate data and compute SDPA reference output
|
||||
all_q_vllm, all_k_vllm, all_v_vllm = [], [], []
|
||||
all_sdpa_outputs = []
|
||||
@@ -407,10 +442,17 @@ def _test_backend_correctness(
|
||||
k_full = torch.randn(s_len, num_kv_heads, head_size, dtype=dtype, device=device)
|
||||
v_full = torch.randn(s_len, num_kv_heads, head_size, dtype=dtype, device=device)
|
||||
|
||||
if fp8_kv_cache:
|
||||
q_ref = q.to(query_fp8_dtype).to(dtype)
|
||||
k_ref = k_full.to(kv_fp8_dtype).to(dtype)
|
||||
v_ref = v_full.to(kv_fp8_dtype).to(dtype)
|
||||
else:
|
||||
q_ref, k_ref, v_ref = q, k_full, v_full
|
||||
|
||||
# SDPA expects (N, H, L, D), so unsqueeze batch and permute
|
||||
q_sdpa_in = q.unsqueeze(0).transpose(1, 2)
|
||||
k_sdpa_in = k_full.unsqueeze(0).transpose(1, 2)
|
||||
v_sdpa_in = v_full.unsqueeze(0).transpose(1, 2)
|
||||
q_sdpa_in = q_ref.unsqueeze(0).transpose(1, 2)
|
||||
k_sdpa_in = k_ref.unsqueeze(0).transpose(1, 2)
|
||||
v_sdpa_in = v_ref.unsqueeze(0).transpose(1, 2)
|
||||
|
||||
if num_q_heads != num_kv_heads:
|
||||
assert num_q_heads % num_kv_heads == 0, (
|
||||
@@ -471,6 +513,7 @@ def _test_backend_correctness(
|
||||
num_blocks=vllm_config.cache_config.num_gpu_blocks or 1000,
|
||||
common_attn_metadata=common_attn_metadata,
|
||||
randomize_blocks=True,
|
||||
kv_cache_dtype=kv_cache_dtype,
|
||||
)
|
||||
|
||||
# 4. Run vLLM backends and compare
|
||||
@@ -488,6 +531,12 @@ def _test_backend_correctness(
|
||||
else:
|
||||
backend_cls = None
|
||||
|
||||
if is_quantized_kv_cache(kv_cache_dtype) and (
|
||||
backend_cls is None
|
||||
or not backend_cls.supports_kv_cache_dtype(kv_cache_dtype)
|
||||
):
|
||||
continue
|
||||
|
||||
if backend_name == AttentionBackendEnum.FLASHINFER:
|
||||
set_kv_cache_layout("HND")
|
||||
reset_kv_cache_layout = True
|
||||
@@ -521,6 +570,7 @@ def _test_backend_correctness(
|
||||
kv_cache_for_backend,
|
||||
sliding_window=sliding_window,
|
||||
attn_type=attn_type,
|
||||
kv_cache_dtype=kv_cache_dtype,
|
||||
)
|
||||
finally:
|
||||
if reset_kv_cache_layout:
|
||||
@@ -570,8 +620,13 @@ def _test_backend_correctness(
|
||||
)
|
||||
@pytest.mark.parametrize("model", ["meta-llama/Meta-Llama-3-8B"])
|
||||
@pytest.mark.parametrize("tensor_parallel_size", [1, 2, 4])
|
||||
@pytest.mark.parametrize("kv_cache_dtype", ["auto", "fp8", "fp8_e4m3"])
|
||||
def test_causal_backend_correctness(
|
||||
default_vllm_config, batch_spec_name: str, model: str, tensor_parallel_size: int
|
||||
default_vllm_config,
|
||||
batch_spec_name: str,
|
||||
model: str,
|
||||
tensor_parallel_size: int,
|
||||
kv_cache_dtype: str,
|
||||
):
|
||||
"""Test backend's correctness with causal attention."""
|
||||
|
||||
@@ -612,6 +667,7 @@ def test_causal_backend_correctness(
|
||||
SMALL_BLOCK_BACKENDS,
|
||||
causal_mask_mod,
|
||||
tensor_parallel_size=tensor_parallel_size,
|
||||
kv_cache_dtype=kv_cache_dtype,
|
||||
)
|
||||
|
||||
# Fast FlexAttention needs to run with block_size=128
|
||||
@@ -623,6 +679,7 @@ def test_causal_backend_correctness(
|
||||
causal_mask_mod,
|
||||
block_size=128,
|
||||
tensor_parallel_size=tensor_parallel_size,
|
||||
kv_cache_dtype=kv_cache_dtype,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -119,6 +119,7 @@ def test_mla_post_load_preserves_runtime_weight_addresses(monkeypatch):
|
||||
layer.kv_b_proj.quant_method = None
|
||||
layer.is_aiter_triton_fp4_bmm_enabled = False
|
||||
layer.is_aiter_triton_fp8_bmm_enabled = False
|
||||
layer.dcp_q_replicate = False
|
||||
layer.quant_config = None
|
||||
layer.layer_name = "test"
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ from vllm.v1.kv_cache_interface import (
|
||||
EncoderOnlyAttentionSpec,
|
||||
FullAttentionSpec,
|
||||
MambaSpec,
|
||||
get_kv_quant_mode,
|
||||
)
|
||||
|
||||
|
||||
@@ -178,6 +179,7 @@ def create_standard_kv_cache_spec(
|
||||
head_size=vllm_config.model_config.get_head_size(),
|
||||
dtype=vllm_config.model_config.dtype,
|
||||
sliding_window=vllm_config.model_config.get_sliding_window(),
|
||||
kv_quant_mode=get_kv_quant_mode(vllm_config.cache_config.cache_dtype),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -149,30 +149,6 @@ def test_has_cache_restores_from_freeable():
|
||||
assert manager.num_freeable_slots == 6
|
||||
|
||||
|
||||
def test_make_profiling_reservation():
|
||||
assert (
|
||||
EncoderCacheManager.make_profiling_reservation(
|
||||
cache_size=0,
|
||||
embed_size=8,
|
||||
dtype=torch.float16,
|
||||
device="cpu",
|
||||
)
|
||||
is None
|
||||
)
|
||||
|
||||
reservation = EncoderCacheManager.make_profiling_reservation(
|
||||
cache_size=7,
|
||||
embed_size=8,
|
||||
dtype=torch.float16,
|
||||
device="cpu",
|
||||
)
|
||||
|
||||
assert reservation is not None
|
||||
assert reservation.shape == (7, 8)
|
||||
assert reservation.dtype == torch.float16
|
||||
assert reservation.device.type == "cpu"
|
||||
|
||||
|
||||
def test_get_freed_mm_hashes_clears_freed_list():
|
||||
manager = EncoderCacheManager(cache_size=10)
|
||||
req1 = MockRequest("reqA", ["a"], [5])
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
import copy
|
||||
import hashlib
|
||||
import importlib
|
||||
from collections.abc import Callable
|
||||
@@ -1477,6 +1478,128 @@ def test_get_max_concurrency_for_kv_cache_config():
|
||||
assert num_tokens == max_concurrency_hybrid_model * max_model_len
|
||||
assert max_concurrency == max_concurrency_hybrid_model
|
||||
|
||||
# Unequal group sizes in the standard layout: each group's pages cost
|
||||
# whole pool blocks, so a request needs 1024 + 129 = 1153 blocks — the
|
||||
# same as the equal-hybrid case above, regardless of the second group
|
||||
# holding only 2 layers.
|
||||
kv_cache_config_unequal_groups = KVCacheConfig(
|
||||
num_blocks=1153 * 3,
|
||||
kv_cache_tensors=[],
|
||||
kv_cache_groups=[
|
||||
KVCacheGroupSpec([f"layer_{i}" for i in range(32)], full_attention_spec),
|
||||
KVCacheGroupSpec(["layer_32", "layer_33"], sliding_window_spec),
|
||||
],
|
||||
)
|
||||
assert (
|
||||
get_max_concurrency_for_kv_cache_config(
|
||||
vllm_config, kv_cache_config_unequal_groups
|
||||
)
|
||||
== 3
|
||||
)
|
||||
|
||||
# UniformTypeKVCacheSpecs group (worker config shape): the aggregated
|
||||
# spec's memory/page ratio equals a single layer's page count, so the
|
||||
# group needs 1024 blocks and the request 1153 in total. The previous
|
||||
# formula normalized both groups' memory by the first group's page size,
|
||||
# reporting 3459/1057 = 3.27 here instead of 3 — and a different value
|
||||
# again for the scheduler-config shape below.
|
||||
uniform_full_spec = UniformTypeKVCacheSpecs(
|
||||
block_size=full_attention_spec.block_size,
|
||||
kv_cache_specs={f"layer_{i}": full_attention_spec for i in range(4)},
|
||||
)
|
||||
kv_cache_config_uniform_group = KVCacheConfig(
|
||||
num_blocks=1153 * 3,
|
||||
kv_cache_tensors=[],
|
||||
kv_cache_groups=[
|
||||
KVCacheGroupSpec([f"layer_{i}" for i in range(4)], uniform_full_spec),
|
||||
KVCacheGroupSpec(["layer_4", "layer_5"], sliding_window_spec),
|
||||
],
|
||||
)
|
||||
assert (
|
||||
get_max_concurrency_for_kv_cache_config(
|
||||
vllm_config, kv_cache_config_uniform_group
|
||||
)
|
||||
== 3
|
||||
)
|
||||
|
||||
# Scheduler-config shape: generate_scheduler_kv_cache_config replaces the
|
||||
# uniform-type group's spec with a representative per-layer spec.
|
||||
# Capacity must not change between the two shapes (the engine computes
|
||||
# on the scheduler config, the worker loop on the worker config).
|
||||
kv_cache_config_scheduler_shape = generate_scheduler_kv_cache_config(
|
||||
[copy.deepcopy(kv_cache_config_uniform_group)]
|
||||
)
|
||||
assert get_max_concurrency_for_kv_cache_config(
|
||||
vllm_config, kv_cache_config_scheduler_shape
|
||||
) == get_max_concurrency_for_kv_cache_config(
|
||||
vllm_config, kv_cache_config_uniform_group
|
||||
)
|
||||
|
||||
|
||||
def test_get_max_concurrency_packed_kv_cache_config():
|
||||
from vllm.v1.core.kv_cache_utils import (
|
||||
_get_kv_cache_config_packed,
|
||||
_use_packed_kv_cache_config,
|
||||
)
|
||||
|
||||
model_config = ModelConfig(
|
||||
"Qwen/Qwen1.5-7B",
|
||||
runner="generate",
|
||||
dtype="float16",
|
||||
max_model_len=16384,
|
||||
)
|
||||
scheduler_config = SchedulerConfig(
|
||||
max_num_batched_tokens=1024,
|
||||
enable_chunked_prefill=True,
|
||||
max_model_len=model_config.max_model_len,
|
||||
is_encoder_decoder=model_config.is_encoder_decoder,
|
||||
async_scheduling=False,
|
||||
)
|
||||
vllm_config = VllmConfig(
|
||||
model_config=model_config,
|
||||
scheduler_config=scheduler_config,
|
||||
)
|
||||
|
||||
# All-UniformTypeKVCacheSpecs groups select the packed layout.
|
||||
mla_specs = {f"layer_{i}": new_mla_spec() for i in range(4)}
|
||||
swa_specs = {
|
||||
f"layer_{i}": SlidingWindowMLASpec(
|
||||
block_size=16,
|
||||
num_kv_heads=1,
|
||||
head_size=576,
|
||||
dtype=torch.float32,
|
||||
sliding_window=128,
|
||||
)
|
||||
for i in range(4, 6)
|
||||
}
|
||||
kv_cache_groups = [
|
||||
KVCacheGroupSpec(
|
||||
list(mla_specs),
|
||||
UniformTypeKVCacheSpecs(block_size=16, kv_cache_specs=mla_specs),
|
||||
),
|
||||
KVCacheGroupSpec(
|
||||
list(swa_specs),
|
||||
UniformTypeKVCacheSpecs(block_size=16, kv_cache_specs=swa_specs),
|
||||
),
|
||||
]
|
||||
assert _use_packed_kv_cache_config(vllm_config, kv_cache_groups)
|
||||
num_blocks, kv_cache_tensors = _get_kv_cache_config_packed(
|
||||
vllm_config, kv_cache_groups, 2 * GiB_bytes
|
||||
)
|
||||
assert num_blocks > 0
|
||||
kv_cache_config_packed = KVCacheConfig(
|
||||
num_blocks=num_blocks,
|
||||
kv_cache_tensors=kv_cache_tensors,
|
||||
kv_cache_groups=kv_cache_groups,
|
||||
)
|
||||
# Per-request blocks: the MLA group needs cdiv(16384, 16) = 1024 pages;
|
||||
# the SWA group cdiv(min(128 - 1 + 1024, 16384), 16) + 1 = 73. The
|
||||
# previous formula normalized by the first group's page size and gave
|
||||
# 1061 blocks per request instead of 1097.
|
||||
assert get_max_concurrency_for_kv_cache_config(
|
||||
vllm_config, kv_cache_config_packed
|
||||
) == num_blocks / (1024 + 73)
|
||||
|
||||
|
||||
def test_allocate_with_lookahead():
|
||||
"""Verify that lookahead tokens correctly affect block allocation"""
|
||||
|
||||
@@ -49,18 +49,6 @@ def test_prefix_caching_from_cli():
|
||||
args = parser.parse_args(["--prefix-caching-hash-algo", "invalid"])
|
||||
|
||||
|
||||
def test_extensible_kv_cache_from_cli():
|
||||
parser = EngineArgs.add_cli_args(FlexibleArgumentParser())
|
||||
|
||||
args = parser.parse_args([])
|
||||
engine_args = EngineArgs.from_cli_args(args=args)
|
||||
assert not engine_args.enable_extensible_kv_cache
|
||||
|
||||
args = parser.parse_args(["--enable-extensible-kv-cache"])
|
||||
engine_args = EngineArgs.from_cli_args(args=args)
|
||||
assert engine_args.enable_extensible_kv_cache
|
||||
|
||||
|
||||
@pytest.mark.skipif(_xxhash is None, reason="xxhash not installed")
|
||||
def test_prefix_caching_xxhash_from_cli():
|
||||
parser = EngineArgs.add_cli_args(FlexibleArgumentParser())
|
||||
|
||||
@@ -7,7 +7,13 @@ import torch
|
||||
|
||||
from tests.v1.kv_connector.unit.utils import create_vllm_config
|
||||
from vllm.config import KVEventsConfig, KVTransferConfig
|
||||
from vllm.distributed.kv_events import MEDIUM_CPU, MEDIUM_FS, BlockRemoved, BlockStored
|
||||
from vllm.distributed.kv_events import (
|
||||
MEDIUM_CPU,
|
||||
MEDIUM_FS,
|
||||
MEDIUM_OBJ,
|
||||
BlockRemoved,
|
||||
BlockStored,
|
||||
)
|
||||
from vllm.distributed.kv_transfer.kv_connector.v1.offloading.config import (
|
||||
build_offloading_config,
|
||||
)
|
||||
@@ -62,8 +68,9 @@ def _wire_hash(block_hash: BlockHash):
|
||||
return maybe_convert_block_hash(block_hash)
|
||||
|
||||
|
||||
def _request(*, block_hashes: list[BlockHash], token_count: int):
|
||||
def _request(*, block_hashes: list[BlockHash], token_count: int, req_id: str = "req"):
|
||||
req = MagicMock()
|
||||
req.request_id = req_id
|
||||
req.block_hashes = block_hashes
|
||||
req.all_token_ids = list(range(1, token_count + 1))
|
||||
req.lora_request = None
|
||||
@@ -104,10 +111,32 @@ def _record_chunks(
|
||||
return keys
|
||||
|
||||
|
||||
def _record_lookup_chunks(
|
||||
tracker: OffloadingEventsTracker,
|
||||
req,
|
||||
group_config: GroupOffloadConfig,
|
||||
num_chunks: int,
|
||||
) -> list[OffloadKey]:
|
||||
keys: list[OffloadKey] = []
|
||||
hbf = group_config.hashes_per_chunk
|
||||
for chunk_idx in range(num_chunks):
|
||||
tail_hash = req.block_hashes[(chunk_idx + 1) * hbf - 1]
|
||||
assert tail_hash is not None
|
||||
key = make_offload_key(tail_hash, group_config.group_idx)
|
||||
tracker.record_lookup(
|
||||
req,
|
||||
group_config,
|
||||
chunk_idx,
|
||||
key,
|
||||
)
|
||||
keys.append(key)
|
||||
return keys
|
||||
|
||||
|
||||
def _stored_event(
|
||||
keys: list[OffloadKey],
|
||||
locality: Locality | None = None,
|
||||
medium: str = _CPU_MEDIUM,
|
||||
locality: Locality | None = None,
|
||||
) -> OffloadingEvent:
|
||||
return OffloadingEvent(
|
||||
keys=keys,
|
||||
@@ -119,8 +148,8 @@ def _stored_event(
|
||||
|
||||
def _removed_event(
|
||||
keys: list[OffloadKey],
|
||||
locality: Locality | None = None,
|
||||
medium: str = _CPU_MEDIUM,
|
||||
locality: Locality | None = None,
|
||||
) -> OffloadingEvent:
|
||||
return OffloadingEvent(
|
||||
keys=keys,
|
||||
@@ -130,6 +159,21 @@ def _removed_event(
|
||||
)
|
||||
|
||||
|
||||
def _lookup_chunk() -> tuple[
|
||||
OffloadingEventsTracker, MagicMock, GroupOffloadConfig, OffloadKey
|
||||
]:
|
||||
tracker = _tracker()
|
||||
req = _request(block_hashes=[_hash(0)], token_count=4)
|
||||
group_config = _group_config()
|
||||
key = _record_lookup_chunks(
|
||||
tracker,
|
||||
req,
|
||||
group_config,
|
||||
num_chunks=1,
|
||||
)[0]
|
||||
return tracker, req, group_config, key
|
||||
|
||||
|
||||
def test_take_events_forwards_locality_to_rich_store():
|
||||
tracker = _tracker()
|
||||
req = _request(block_hashes=[_hash(0)], token_count=4)
|
||||
@@ -220,18 +264,37 @@ def test_take_events_publishes_routable_block_stored():
|
||||
assert len(tracker._pending_event_metadata) == 6
|
||||
|
||||
|
||||
def test_take_events_factor_gt_1_chunk_store_and_remove():
|
||||
def test_promotion_emits_full_cpu_stored_event():
|
||||
tracker, _, _, key = _lookup_chunk()
|
||||
|
||||
[event] = tracker.take_events([_stored_event([key])])
|
||||
|
||||
assert isinstance(event, BlockStored)
|
||||
assert event.medium == MEDIUM_CPU
|
||||
assert event.block_hashes == [_wire_hash(_hash(0))]
|
||||
assert event.parent_block_hash is None
|
||||
assert event.token_ids == [1, 2, 3, 4]
|
||||
assert event.block_size == 4
|
||||
assert event.lora_id is None
|
||||
assert event.lora_name is None
|
||||
assert event.extra_keys is None
|
||||
assert event.group_idx == 0
|
||||
assert event.kv_cache_spec_kind == KVCacheSpecKind.FULL_ATTENTION.value
|
||||
assert event.kv_cache_spec_sliding_window is None
|
||||
|
||||
|
||||
def test_lookup_promotion_factor_gt_1_store_and_remove():
|
||||
block_size = 4
|
||||
blocks_per_chunk = 3
|
||||
blocks_per_chunk = 2
|
||||
tracker = _tracker()
|
||||
group_config = _group_config(
|
||||
block_size=block_size, blocks_per_chunk=blocks_per_chunk
|
||||
)
|
||||
req = _request(
|
||||
block_hashes=[_hash(i) for i in range(6)],
|
||||
block_hashes=[_hash(i) for i in range(4)],
|
||||
token_count=block_size * blocks_per_chunk * 2,
|
||||
)
|
||||
keys = _record_chunks(tracker, req, group_config, num_chunks=2)
|
||||
keys = _record_lookup_chunks(tracker, req, group_config, num_chunks=2)
|
||||
|
||||
stored = list(tracker.take_events([_stored_event(keys)]))
|
||||
assert len(stored) == 2
|
||||
@@ -293,6 +356,7 @@ def test_take_events_opt_out_keeps_placeholders():
|
||||
group_config = _group_config()
|
||||
req = _request(block_hashes=[_hash(i) for i in range(3)], token_count=12)
|
||||
keys = _record_chunks(tracker, req, group_config, num_chunks=3)
|
||||
_record_lookup_chunks(tracker, req, group_config, num_chunks=3)
|
||||
|
||||
assert not tracker.self_describing_enabled
|
||||
assert not tracker._pending_event_metadata
|
||||
@@ -315,11 +379,21 @@ def test_take_events_opt_out_keeps_placeholders():
|
||||
assert len(events[3].block_hashes) == 3
|
||||
|
||||
|
||||
def test_record_store_skips_sliding_window_group():
|
||||
@pytest.mark.parametrize(
|
||||
"sliding_window_size_in_chunks",
|
||||
[1, 2],
|
||||
ids=["ssm", "sliding-window"],
|
||||
)
|
||||
def test_event_metadata_skips_non_full_attention_group(
|
||||
sliding_window_size_in_chunks: int,
|
||||
):
|
||||
tracker = _tracker()
|
||||
group_config = _group_config(sliding_window_size_in_chunks=2)
|
||||
group_config = _group_config(
|
||||
sliding_window_size_in_chunks=sliding_window_size_in_chunks
|
||||
)
|
||||
req = _request(block_hashes=[_hash(i) for i in range(3)], token_count=12)
|
||||
keys = _record_chunks(tracker, req, group_config, num_chunks=3)
|
||||
_record_lookup_chunks(tracker, req, group_config, num_chunks=3)
|
||||
|
||||
assert not tracker._pending_event_metadata
|
||||
|
||||
@@ -329,6 +403,57 @@ def test_record_store_skips_sliding_window_group():
|
||||
assert events[0].block_size == 0
|
||||
|
||||
|
||||
def test_pending_cpu_removal_consumes_hit_backfill_until_next_hit():
|
||||
tracker = _tracker()
|
||||
block_hashes = [_hash(0), _hash(1)]
|
||||
req = _request(block_hashes=block_hashes, token_count=8)
|
||||
group_config = _group_config(blocks_per_chunk=2)
|
||||
key = _record_chunks(tracker, req, group_config, num_chunks=1)[0]
|
||||
confirmed_meta = tracker._pending_event_metadata[key]
|
||||
lookup_req = _request(
|
||||
block_hashes=block_hashes,
|
||||
token_count=8,
|
||||
req_id="new-request",
|
||||
)
|
||||
|
||||
tracker.record_lookup(
|
||||
lookup_req,
|
||||
group_config,
|
||||
0,
|
||||
key,
|
||||
)
|
||||
assert tracker._pending_event_metadata[key] is confirmed_meta
|
||||
|
||||
removed = list(tracker.take_events([_removed_event([key])]))
|
||||
assert len(removed) == 1
|
||||
assert removed[0].block_hashes == [
|
||||
_wire_hash(_hash(0)),
|
||||
_wire_hash(_hash(1)),
|
||||
]
|
||||
|
||||
stored = list(tracker.take_events([_stored_event([key])]))
|
||||
assert len(stored) == 1
|
||||
assert stored[0].block_size == 0
|
||||
assert stored[0].token_ids == []
|
||||
|
||||
tracker.record_lookup(lookup_req, group_config, 0, key)
|
||||
removed = list(tracker.take_events([_removed_event([key])]))
|
||||
assert removed[0].block_hashes == [
|
||||
_wire_hash(_hash(0)),
|
||||
_wire_hash(_hash(1)),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("medium", [MEDIUM_FS, MEDIUM_OBJ])
|
||||
def test_secondary_stored_event_does_not_mutate_cpu_metadata(medium: str):
|
||||
tracker, _, _, key = _lookup_chunk()
|
||||
expected_metadata = dict(tracker._pending_event_metadata)
|
||||
|
||||
stored = list(tracker.take_events([_stored_event([key], medium)]))
|
||||
assert stored[0].token_ids == [1, 2, 3, 4]
|
||||
assert tracker._pending_event_metadata == expected_metadata
|
||||
|
||||
|
||||
def test_take_events_groups_removed_hashes_by_kv_group():
|
||||
tracker = _tracker()
|
||||
group0_config = _group_config(group_idx=0, blocks_per_chunk=2)
|
||||
@@ -378,7 +503,7 @@ def test_reset_cache_clears_side_table():
|
||||
tracker = _tracker()
|
||||
group_config = _group_config()
|
||||
req = _request(block_hashes=[_hash(i) for i in range(3)], token_count=12)
|
||||
_record_chunks(tracker, req, group_config, num_chunks=3)
|
||||
_record_lookup_chunks(tracker, req, group_config, num_chunks=3)
|
||||
|
||||
assert tracker._pending_event_metadata
|
||||
|
||||
@@ -387,7 +512,7 @@ def test_reset_cache_clears_side_table():
|
||||
assert not tracker._pending_event_metadata
|
||||
|
||||
|
||||
def test_tiering_rejects_self_describing_kv_events():
|
||||
def test_tiering_accepts_self_describing_kv_events():
|
||||
vllm_config = create_vllm_config(
|
||||
block_size=4,
|
||||
max_num_batched_tokens=16,
|
||||
@@ -423,5 +548,9 @@ def test_tiering_rejects_self_describing_kv_events():
|
||||
],
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="TieringOffloadingSpec"):
|
||||
TieringOffloadingSpec(build_offloading_config(vllm_config, kv_cache_config))
|
||||
spec = TieringOffloadingSpec(build_offloading_config(vllm_config, kv_cache_config))
|
||||
tracker = OffloadingEventsTracker(spec.kv_events_config)
|
||||
|
||||
assert spec.kv_events_config.enable_kv_cache_events
|
||||
assert spec.kv_events_config.self_describing_kv_events
|
||||
assert tracker.self_describing_enabled
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import MagicMock
|
||||
from unittest.mock import MagicMock, call
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
@@ -11,6 +11,7 @@ from tests.v1.kv_connector.unit.offloading_connector.utils import (
|
||||
to_keys,
|
||||
)
|
||||
from tests.v1.kv_connector.unit.utils import EOS_TOKEN_ID
|
||||
from vllm.distributed.kv_events import MEDIUM_CPU, BlockRemoved, BlockStored
|
||||
from vllm.distributed.kv_transfer.kv_connector.v1.offloading.metrics import (
|
||||
OffloadingConnectorStats,
|
||||
_ConnectorMetricName,
|
||||
@@ -18,7 +19,9 @@ from vllm.distributed.kv_transfer.kv_connector.v1.offloading.metrics import (
|
||||
from vllm.distributed.kv_transfer.kv_connector.v1.offloading.scheduler import (
|
||||
OffloadingConnectorScheduler,
|
||||
RequestOffloadState,
|
||||
is_store_reachable_swa_chunk,
|
||||
)
|
||||
from vllm.v1.core.kv_cache_utils import BlockHash
|
||||
from vllm.v1.kv_cache_interface import (
|
||||
FullAttentionSpec,
|
||||
KVCacheGroupSpec,
|
||||
@@ -26,6 +29,7 @@ from vllm.v1.kv_cache_interface import (
|
||||
)
|
||||
from vllm.v1.kv_offload.base import (
|
||||
LookupResult,
|
||||
OffloadingEvent,
|
||||
OffloadingManager,
|
||||
OffloadPolicy,
|
||||
ReqContext,
|
||||
@@ -123,6 +127,55 @@ def test_scheduler_reports_lookup_sync_delay(request_runner):
|
||||
assert reduced[f"{_ConnectorMetricName.LOOKUP_SYNC_DELAY}_sum"] > 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
(
|
||||
"absolute_chunk_index",
|
||||
"storable_chunk_count",
|
||||
"alignment_chunk_count",
|
||||
"sliding_window_chunks",
|
||||
"is_eagle_group",
|
||||
"expected",
|
||||
),
|
||||
[
|
||||
# Full 64-chunk segment: ordinary SWA keeps 62-63; EAGLE also keeps 61.
|
||||
(61, 64, 64, 2, False, False),
|
||||
(62, 64, 64, 2, False, True),
|
||||
(60, 64, 64, 2, True, False),
|
||||
(61, 64, 64, 2, True, True),
|
||||
# Partial 48-of-64 segment: the reachable tail ends at chunk 47.
|
||||
(45, 48, 64, 2, False, False),
|
||||
(46, 48, 64, 2, False, True),
|
||||
(44, 48, 64, 2, True, False),
|
||||
(45, 48, 64, 2, True, True),
|
||||
# A later partial segment uses its own actual end (chunks 64-79).
|
||||
(76, 80, 64, 3, False, False),
|
||||
(77, 80, 64, 3, False, True),
|
||||
# No alignment means no store-pruning optimization.
|
||||
(0, 1, None, None, False, True),
|
||||
# A tail at least as large as the segment keeps every chunk.
|
||||
(0, 2, 64, 2, False, True),
|
||||
],
|
||||
)
|
||||
def test_is_store_reachable_swa_chunk(
|
||||
absolute_chunk_index: int,
|
||||
storable_chunk_count: int,
|
||||
alignment_chunk_count: int | None,
|
||||
sliding_window_chunks: int | None,
|
||||
is_eagle_group: bool,
|
||||
expected: bool,
|
||||
):
|
||||
assert (
|
||||
is_store_reachable_swa_chunk(
|
||||
absolute_chunk_index,
|
||||
storable_chunk_count,
|
||||
alignment_chunk_count,
|
||||
sliding_window_chunks,
|
||||
is_eagle_group,
|
||||
)
|
||||
is expected
|
||||
)
|
||||
|
||||
|
||||
def test_scheduler_reports_lookup_async_delay_on_resolve(request_runner):
|
||||
"""A deferred lookup reports its async delay once it resolves."""
|
||||
runner = request_runner(
|
||||
@@ -143,6 +196,159 @@ def test_scheduler_reports_lookup_async_delay_on_resolve(request_runner):
|
||||
assert reduced[f"{_ConnectorMetricName.LOOKUP_ASYNC_DELAY}_sum"] > 0
|
||||
|
||||
|
||||
def test_max_offload_tokens_zero_does_not_record_pending_lookups(request_runner):
|
||||
runner = request_runner(
|
||||
block_size=4,
|
||||
num_gpu_blocks=10,
|
||||
async_scheduling=False,
|
||||
)
|
||||
runner.manager.lookup.return_value = LookupResult.RETRY
|
||||
runner.manager.take_events.return_value = []
|
||||
runner.manager.prepare_store.side_effect = lambda keys, req_context: (
|
||||
generate_store_output(keys)
|
||||
)
|
||||
|
||||
runner.new_request(
|
||||
token_ids=[1] * 12,
|
||||
kv_transfer_params={"max_offload_tokens": 0},
|
||||
)
|
||||
runner.run(decoded_tokens=[])
|
||||
|
||||
tracker = runner.connector_scheduler._events_tracker
|
||||
assert runner.manager.lookup.call_count == 3
|
||||
assert not tracker._pending_event_metadata
|
||||
assert list(runner.connector_scheduler.take_events()) == []
|
||||
|
||||
runner.manager.lookup.return_value = LookupResult.MISS
|
||||
runner.run(decoded_tokens=[EOS_TOKEN_ID])
|
||||
|
||||
assert not tracker._pending_event_metadata
|
||||
assert list(runner.connector_scheduler.take_events()) == []
|
||||
|
||||
|
||||
def test_abort_before_hit_uses_placeholder_then_later_hit_heals_removal(
|
||||
request_runner,
|
||||
):
|
||||
runner = request_runner(
|
||||
block_size=4,
|
||||
num_gpu_blocks=10,
|
||||
async_scheduling=False,
|
||||
blocks_per_chunk=2,
|
||||
)
|
||||
raw_events: list[OffloadingEvent] = []
|
||||
|
||||
def take_raw_events():
|
||||
yield from raw_events
|
||||
raw_events.clear()
|
||||
|
||||
runner.manager.lookup.return_value = LookupResult.RETRY
|
||||
runner.manager.take_events.side_effect = take_raw_events
|
||||
runner.manager.prepare_store.side_effect = lambda keys, req_context: (
|
||||
generate_store_output([])
|
||||
)
|
||||
|
||||
runner.new_request(token_ids=[1] * 8)
|
||||
runner.run(decoded_tokens=[])
|
||||
|
||||
tracker = runner.connector_scheduler._events_tracker
|
||||
assert not tracker._pending_event_metadata
|
||||
key = runner.manager.lookup.call_args.args[0]
|
||||
req_id = str(runner.req_id)
|
||||
req_status = runner.connector_scheduler._req_status[req_id]
|
||||
|
||||
runner.scheduler.finish_requests((req_id,), RequestStatus.FINISHED_ABORTED)
|
||||
|
||||
assert not tracker._pending_event_metadata
|
||||
|
||||
raw_events.append(OffloadingEvent(keys=[key], medium=MEDIUM_CPU, removed=False))
|
||||
events = list(runner.connector_scheduler.take_events())
|
||||
assert len(events) == 1
|
||||
assert isinstance(events[0], BlockStored)
|
||||
assert events[0].block_size == 0
|
||||
assert events[0].token_ids == []
|
||||
|
||||
runner.manager.lookup.return_value = LookupResult.HIT
|
||||
group_config = runner.connector_scheduler.config.kv_group_configs[0]
|
||||
assert (
|
||||
runner.connector_scheduler._maximal_prefix_lookup(
|
||||
[key],
|
||||
req_status.req_context,
|
||||
req_status.req,
|
||||
group_config,
|
||||
0,
|
||||
)
|
||||
== 1
|
||||
)
|
||||
assert key in tracker._pending_event_metadata
|
||||
|
||||
raw_events.append(OffloadingEvent(keys=[key], medium=MEDIUM_CPU, removed=True))
|
||||
[event] = runner.connector_scheduler.take_events()
|
||||
assert isinstance(event, BlockRemoved)
|
||||
assert event.medium == MEDIUM_CPU
|
||||
assert len(event.block_hashes) == 2
|
||||
assert key not in tracker._pending_event_metadata
|
||||
|
||||
|
||||
@pytest.mark.parametrize("blocks_per_chunk", [1, 2])
|
||||
def test_promotion_hit_precedes_stored_event_translation(
|
||||
request_runner,
|
||||
blocks_per_chunk: int,
|
||||
):
|
||||
runner = request_runner(
|
||||
block_size=4,
|
||||
num_gpu_blocks=10,
|
||||
async_scheduling=False,
|
||||
blocks_per_chunk=blocks_per_chunk,
|
||||
)
|
||||
token_ids = [1] * 4 * blocks_per_chunk
|
||||
|
||||
runner.manager.prepare_store.side_effect = lambda keys, req_context: (
|
||||
generate_store_output(keys)
|
||||
)
|
||||
runner.new_request(token_ids=token_ids)
|
||||
runner.run(
|
||||
decoded_tokens=[EOS_TOKEN_ID],
|
||||
expected_stored=tuple(range(blocks_per_chunk)),
|
||||
)
|
||||
runner.scheduler.reset_prefix_cache()
|
||||
runner.connector_scheduler._events_tracker.reset()
|
||||
|
||||
raw_events: list[OffloadingEvent] = []
|
||||
|
||||
def lookup(key, req_context):
|
||||
raw_events.append(OffloadingEvent(keys=[key], medium=MEDIUM_CPU, removed=False))
|
||||
return LookupResult.HIT
|
||||
|
||||
def take_raw_events():
|
||||
yield from raw_events
|
||||
raw_events.clear()
|
||||
|
||||
runner.manager.lookup.side_effect = lookup
|
||||
runner.manager.take_events.side_effect = take_raw_events
|
||||
runner.manager.prepare_store.side_effect = lambda keys, req_context: (
|
||||
generate_store_output([])
|
||||
)
|
||||
publisher = MagicMock()
|
||||
runner.scheduler.kv_event_publisher = publisher
|
||||
|
||||
runner.new_request(token_ids=token_ids)
|
||||
runner.run(
|
||||
decoded_tokens=[],
|
||||
expected_loaded=tuple(range(blocks_per_chunk)),
|
||||
)
|
||||
|
||||
events = [
|
||||
event
|
||||
for publish_call in publisher.publish.call_args_list
|
||||
for event in publish_call.args[0].events
|
||||
if isinstance(event, BlockStored) and event.medium == MEDIUM_CPU
|
||||
]
|
||||
assert len(events) == 1
|
||||
assert len(events[0].block_hashes) == blocks_per_chunk
|
||||
assert events[0].block_size == 4
|
||||
assert events[0].token_ids == token_ids
|
||||
|
||||
|
||||
@pytest.mark.parametrize("async_scheduling", [True, False])
|
||||
def test_offloading_connector(request_runner, async_scheduling: bool):
|
||||
block_size = 4
|
||||
@@ -241,7 +447,7 @@ def test_offloading_connector(request_runner, async_scheduling: bool):
|
||||
runner.manager.prepare_store.side_effect = lambda keys, req_context: (
|
||||
generate_store_output([])
|
||||
)
|
||||
runner.connector_scheduler._maximal_prefix_lookup = lambda key, req_context: 1
|
||||
runner.connector_scheduler._maximal_prefix_lookup = lambda keys, ctx, *_: 1
|
||||
runner.run(decoded_tokens=[EOS_TOKEN_ID], expected_loaded=(0, 1, 2))
|
||||
|
||||
# single block lookup with a hit in a middle block
|
||||
@@ -249,7 +455,7 @@ def test_offloading_connector(request_runner, async_scheduling: bool):
|
||||
runner.manager.prepare_store.side_effect = lambda keys, req_context: (
|
||||
generate_store_output([])
|
||||
)
|
||||
runner.connector_scheduler._maximal_prefix_lookup = lambda key, req_context: 1
|
||||
runner.connector_scheduler._maximal_prefix_lookup = lambda keys, ctx, *_: 1
|
||||
runner.run(decoded_tokens=[EOS_TOKEN_ID], expected_loaded=(3, 4, 5))
|
||||
|
||||
|
||||
@@ -307,7 +513,7 @@ def test_request_preemption(request_runner, async_scheduling: bool):
|
||||
|
||||
# request should now return from preemption
|
||||
# re-load [0, ..., 8] from the CPU and store [9, 10, 11]
|
||||
runner.connector_scheduler._maximal_prefix_lookup = lambda key, req_context: 3
|
||||
runner.connector_scheduler._maximal_prefix_lookup = lambda keys, ctx, *_: 3
|
||||
runner.manager.prepare_store.side_effect = lambda keys, req_context: (
|
||||
generate_store_output(keys)
|
||||
)
|
||||
@@ -427,7 +633,7 @@ def test_concurrent_lookups_of_the_same_prefix(request_runner, async_scheduling:
|
||||
# start a request to load the first block, but don't complete
|
||||
runner.scheduler.reset_prefix_cache()
|
||||
runner.new_request(token_ids=[0] * tokens_per_chunk)
|
||||
runner.connector_scheduler._maximal_prefix_lookup = lambda key, req_context: 1
|
||||
runner.connector_scheduler._maximal_prefix_lookup = lambda keys, ctx, *_: 1
|
||||
runner.run(
|
||||
decoded_tokens=[],
|
||||
complete_transfers=False,
|
||||
@@ -439,7 +645,7 @@ def test_concurrent_lookups_of_the_same_prefix(request_runner, async_scheduling:
|
||||
|
||||
# start a new request to load the same first block
|
||||
runner.new_request(token_ids=[0] * tokens_per_chunk)
|
||||
runner.connector_scheduler._maximal_prefix_lookup = lambda key, req_context: 1
|
||||
runner.connector_scheduler._maximal_prefix_lookup = lambda keys, ctx, *_: 1
|
||||
runner.run(
|
||||
decoded_tokens=[],
|
||||
complete_transfers=False,
|
||||
@@ -491,7 +697,7 @@ def test_abort_loading_requests(request_runner, async_scheduling: bool):
|
||||
# start a request to load the first block, but don't complete
|
||||
runner.scheduler.reset_prefix_cache()
|
||||
runner.new_request(token_ids=[0] * tokens_per_chunk)
|
||||
runner.connector_scheduler._maximal_prefix_lookup = lambda key, req_context: 1
|
||||
runner.connector_scheduler._maximal_prefix_lookup = lambda keys, ctx, *_: 1
|
||||
runner.run(
|
||||
decoded_tokens=[],
|
||||
complete_transfers=False,
|
||||
@@ -793,73 +999,144 @@ def _make_scheduler_with_lookup(
|
||||
|
||||
scheduler = object.__new__(OffloadingConnectorScheduler)
|
||||
scheduler.manager = manager
|
||||
scheduler._events_tracker = MagicMock()
|
||||
return scheduler
|
||||
|
||||
|
||||
_EMPTY_REQ_CTX = ReqContext(req_id="")
|
||||
_LOOKUP_REQ = MagicMock()
|
||||
_LOOKUP_REQ.request_id = "req"
|
||||
_LOOKUP_GROUP_CONFIG = MagicMock()
|
||||
|
||||
|
||||
def _maximal_lookup(sched, keys, start_chunk_idx: int = 0):
|
||||
return sched._maximal_prefix_lookup(
|
||||
keys,
|
||||
_EMPTY_REQ_CTX,
|
||||
_LOOKUP_REQ,
|
||||
_LOOKUP_GROUP_CONFIG,
|
||||
start_chunk_idx,
|
||||
)
|
||||
|
||||
|
||||
class TestMaximalPrefixLookup:
|
||||
def test_all_hit(self):
|
||||
sched = _make_scheduler_with_lookup({1: LookupResult.HIT, 2: LookupResult.HIT})
|
||||
assert sched._maximal_prefix_lookup(to_keys([1, 2]), _EMPTY_REQ_CTX) == 2
|
||||
assert _maximal_lookup(sched, to_keys([1, 2])) == 2
|
||||
|
||||
def test_records_absolute_chunk_indices(self):
|
||||
keys = to_keys([1, 2])
|
||||
sched = _make_scheduler_with_lookup({1: LookupResult.HIT, 2: LookupResult.HIT})
|
||||
|
||||
assert _maximal_lookup(sched, keys, start_chunk_idx=3) == 2
|
||||
assert sched._events_tracker.record_lookup.call_args_list == [
|
||||
call(
|
||||
_LOOKUP_REQ,
|
||||
_LOOKUP_GROUP_CONFIG,
|
||||
3,
|
||||
keys[0],
|
||||
),
|
||||
call(
|
||||
_LOOKUP_REQ,
|
||||
_LOOKUP_GROUP_CONFIG,
|
||||
4,
|
||||
keys[1],
|
||||
),
|
||||
]
|
||||
|
||||
def test_all_miss(self):
|
||||
sched = _make_scheduler_with_lookup({})
|
||||
assert sched._maximal_prefix_lookup(to_keys([1, 2]), _EMPTY_REQ_CTX) == 0
|
||||
assert _maximal_lookup(sched, to_keys([1, 2])) == 0
|
||||
sched._events_tracker.record_lookup.assert_not_called()
|
||||
|
||||
def test_partial_prefix(self):
|
||||
sched = _make_scheduler_with_lookup({1: LookupResult.HIT, 2: LookupResult.HIT})
|
||||
assert sched._maximal_prefix_lookup(to_keys([1, 2, 3]), _EMPTY_REQ_CTX) == 2
|
||||
assert _maximal_lookup(sched, to_keys([1, 2, 3])) == 2
|
||||
|
||||
def test_miss_then_hit(self):
|
||||
sched = _make_scheduler_with_lookup({2: LookupResult.HIT})
|
||||
assert sched._maximal_prefix_lookup(to_keys([1, 2]), _EMPTY_REQ_CTX) == 0
|
||||
assert _maximal_lookup(sched, to_keys([1, 2])) == 0
|
||||
|
||||
def test_single_hit(self):
|
||||
sched = _make_scheduler_with_lookup({1: LookupResult.HIT})
|
||||
assert sched._maximal_prefix_lookup(to_keys([1]), _EMPTY_REQ_CTX) == 1
|
||||
assert _maximal_lookup(sched, to_keys([1])) == 1
|
||||
|
||||
def test_empty(self):
|
||||
sched = _make_scheduler_with_lookup({})
|
||||
assert sched._maximal_prefix_lookup([], _EMPTY_REQ_CTX) == 0
|
||||
assert _maximal_lookup(sched, []) == 0
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"pending_result",
|
||||
[LookupResult.RETRY, LookupResult.HIT_PENDING],
|
||||
)
|
||||
def test_pending_result_is_not_recorded(
|
||||
self,
|
||||
pending_result: LookupResult,
|
||||
):
|
||||
sched = _make_scheduler_with_lookup({1: pending_result})
|
||||
|
||||
assert _maximal_lookup(sched, to_keys([1])) is None
|
||||
sched._events_tracker.record_lookup.assert_not_called()
|
||||
|
||||
def test_retry_defers(self):
|
||||
keys = to_keys([1, 2])
|
||||
sched = _make_scheduler_with_lookup(
|
||||
{1: LookupResult.RETRY, 2: LookupResult.HIT}
|
||||
)
|
||||
assert sched._maximal_prefix_lookup(to_keys([1, 2]), _EMPTY_REQ_CTX) is None
|
||||
assert _maximal_lookup(sched, keys) is None
|
||||
assert sched.manager.lookup.call_count == 2
|
||||
sched._events_tracker.record_lookup.assert_called_once_with(
|
||||
_LOOKUP_REQ,
|
||||
_LOOKUP_GROUP_CONFIG,
|
||||
1,
|
||||
keys[1],
|
||||
)
|
||||
|
||||
def test_retry_after_hit_defers(self):
|
||||
keys = to_keys([1, 2])
|
||||
sched = _make_scheduler_with_lookup(
|
||||
{1: LookupResult.HIT, 2: LookupResult.RETRY}
|
||||
)
|
||||
assert sched._maximal_prefix_lookup(to_keys([1, 2]), _EMPTY_REQ_CTX) is None
|
||||
assert _maximal_lookup(sched, keys) is None
|
||||
sched._events_tracker.record_lookup.assert_called_once_with(
|
||||
_LOOKUP_REQ,
|
||||
_LOOKUP_GROUP_CONFIG,
|
||||
0,
|
||||
keys[0],
|
||||
)
|
||||
|
||||
def test_hit_pending_defers(self):
|
||||
keys = to_keys([1, 2])
|
||||
sched = _make_scheduler_with_lookup(
|
||||
{1: LookupResult.HIT_PENDING, 2: LookupResult.HIT}
|
||||
)
|
||||
assert sched._maximal_prefix_lookup(to_keys([1, 2]), _EMPTY_REQ_CTX) is None
|
||||
assert _maximal_lookup(sched, keys) is None
|
||||
assert sched.manager.lookup.call_count == 2
|
||||
sched._events_tracker.record_lookup.assert_called_once_with(
|
||||
_LOOKUP_REQ,
|
||||
_LOOKUP_GROUP_CONFIG,
|
||||
1,
|
||||
keys[1],
|
||||
)
|
||||
|
||||
def test_hit_pending_does_not_stop_scan(self):
|
||||
"""HIT_PENDING defers but does not break — scan continues until miss."""
|
||||
sched = _make_scheduler_with_lookup(
|
||||
{1: LookupResult.HIT_PENDING, 2: LookupResult.MISS, 3: LookupResult.HIT}
|
||||
)
|
||||
assert sched._maximal_prefix_lookup(to_keys([1, 2, 3]), _EMPTY_REQ_CTX) is None
|
||||
assert _maximal_lookup(sched, to_keys([1, 2, 3])) is None
|
||||
assert sched.manager.lookup.call_count == 2
|
||||
sched._events_tracker.record_lookup.assert_not_called()
|
||||
|
||||
def test_retry_stops_at_miss(self):
|
||||
"""RETRY is treated as hit for iteration, but miss stops the scan."""
|
||||
sched = _make_scheduler_with_lookup(
|
||||
{1: LookupResult.RETRY, 2: LookupResult.MISS, 3: LookupResult.HIT}
|
||||
)
|
||||
assert sched._maximal_prefix_lookup(to_keys([1, 2, 3]), _EMPTY_REQ_CTX) is None
|
||||
assert _maximal_lookup(sched, to_keys([1, 2, 3])) is None
|
||||
# lookup should have been called for blocks 1 and 2 (stops at miss)
|
||||
assert sched.manager.lookup.call_count == 2
|
||||
sched._events_tracker.record_lookup.assert_not_called()
|
||||
|
||||
|
||||
class TestSlidingWindowLookup:
|
||||
@@ -1011,7 +1288,7 @@ def test_request_level_policy_stores_all_blocks(request_runner, async_scheduling
|
||||
|
||||
# New request with 2 offloaded chunks; first matches what's in CPU.
|
||||
runner.new_request(token_ids=[0] * tokens_per_chunk * 2)
|
||||
runner.connector_scheduler._maximal_prefix_lookup = lambda key, req_context: 1
|
||||
runner.connector_scheduler._maximal_prefix_lookup = lambda keys, ctx, *_: 1
|
||||
runner.manager.prepare_store.side_effect = lambda keys, req_context: (
|
||||
generate_store_output(keys)
|
||||
)
|
||||
@@ -1042,7 +1319,7 @@ def test_loads_do_not_populate_fence_index(request_runner):
|
||||
async_scheduling=False,
|
||||
)
|
||||
runner.new_request(token_ids=[0] * 12)
|
||||
runner.connector_scheduler._maximal_prefix_lookup = lambda key, req_context: 1
|
||||
runner.connector_scheduler._maximal_prefix_lookup = lambda keys, ctx, *_: 1
|
||||
runner.run(decoded_tokens=[], complete_transfers=False)
|
||||
assert runner.connector_scheduler._block_id_to_pending_jobs == {}
|
||||
|
||||
@@ -1088,7 +1365,7 @@ def test_fence_at_update_state_after_alloc(request_runner):
|
||||
|
||||
runner.scheduler.reset_prefix_cache()
|
||||
runner.new_request(token_ids=[0] * 4)
|
||||
runner.connector_scheduler._maximal_prefix_lookup = lambda key, req_context: 1
|
||||
runner.connector_scheduler._maximal_prefix_lookup = lambda keys, ctx, *_: 1
|
||||
runner.manager.prepare_store.side_effect = lambda keys, req_context: (
|
||||
generate_store_output([])
|
||||
)
|
||||
@@ -1139,7 +1416,7 @@ def test_fence_at_build_store_jobs(request_runner):
|
||||
|
||||
runner.scheduler.reset_prefix_cache()
|
||||
runner.new_request(token_ids=[1] * 4)
|
||||
runner.connector_scheduler._maximal_prefix_lookup = lambda key, req_context: 0
|
||||
runner.connector_scheduler._maximal_prefix_lookup = lambda keys, ctx, *_: 0
|
||||
runner.manager.prepare_store.side_effect = lambda keys, req_context: (
|
||||
generate_store_output([])
|
||||
)
|
||||
@@ -1365,7 +1642,7 @@ def test_reset_cache(request_runner, async_scheduling: bool):
|
||||
# Leave the load in-flight so that reset_cache must flush it.
|
||||
runner.scheduler.reset_prefix_cache()
|
||||
runner.new_request(token_ids=[0] * tokens_per_chunk)
|
||||
runner.connector_scheduler._maximal_prefix_lookup = lambda key, req_context: 1
|
||||
runner.connector_scheduler._maximal_prefix_lookup = lambda keys, ctx, *_: 1
|
||||
runner.manager.prepare_store.side_effect = lambda keys, req_context: (
|
||||
generate_store_output([])
|
||||
)
|
||||
@@ -1553,9 +1830,7 @@ def test_async_preempt_readmit_before_transfer_output_is_deferred(request_runner
|
||||
# preemption batch's ModelRunnerOutput is consumed by update_from_output().
|
||||
free_block_queue.num_free_blocks = num_free_blocks_empty
|
||||
assert runner.scheduler.reset_prefix_cache()
|
||||
runner.connector_scheduler._maximal_prefix_lookup = lambda key, req_context: len(
|
||||
key
|
||||
)
|
||||
runner.connector_scheduler._maximal_prefix_lookup = lambda keys, ctx, *_: len(keys)
|
||||
|
||||
readmit_output = runner.scheduler.schedule()
|
||||
|
||||
@@ -1669,7 +1944,7 @@ def test_swa_alignment_skip(request_runner, async_scheduling: bool):
|
||||
runner.scheduler.reset_prefix_cache()
|
||||
runner.new_request(token_ids=[0] * num_tokens + [1])
|
||||
runner.manager.lookup.return_value = LookupResult.HIT
|
||||
runner.connector_scheduler._maximal_prefix_lookup = lambda key, req_context: 2
|
||||
runner.connector_scheduler._maximal_prefix_lookup = lambda keys, ctx, *_: 2
|
||||
runner.run(
|
||||
decoded_tokens=[EOS_TOKEN_ID],
|
||||
# Group 0: full prefix lookup hits 2 offloaded chunks
|
||||
@@ -1840,6 +2115,13 @@ class TestEagle:
|
||||
req.request_id = "test-req"
|
||||
req.num_tokens = num_tokens
|
||||
req.kv_transfer_params = None
|
||||
num_hash_blocks = max(
|
||||
len(hashes) * scheduler.config.kv_group_configs[idx].hashes_per_chunk
|
||||
for idx, hashes in enumerate(offload_keys_per_group)
|
||||
)
|
||||
req.block_hashes = [BlockHash(str(i).encode()) for i in range(num_hash_blocks)]
|
||||
req.all_token_ids = list(range(num_tokens))
|
||||
req.lora_request = None
|
||||
|
||||
state = RequestOffloadState(
|
||||
config=scheduler.config,
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""Unit tests for the EAGLE speculator's draft attention metadata builder.
|
||||
|
||||
These tests guard the regression where ``_build_draft_attn_metadata`` did
|
||||
not populate ``seq_lens_cpu_upper_bound`` on the per-step
|
||||
``CommonAttentionMetadata``. Several downstream attention backends and
|
||||
helpers (``split_decodes_prefills_and_extends``, the MLA indexer,
|
||||
flex-attention, cross-attention) assert this field is non-None, so
|
||||
omitting it caused crashes at the start of draft decode for certain
|
||||
backends (e.g. ``ROCM_AITER_FA`` with eagle/eagle3 spec decode):
|
||||
|
||||
AssertionError: assert common_attn_metadata.seq_lens_cpu_upper_bound is not None
|
||||
"""
|
||||
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
|
||||
import torch
|
||||
|
||||
from vllm.v1.worker.gpu.spec_decode import speculator as base_speculator
|
||||
from vllm.v1.worker.gpu.spec_decode.eagle.speculator import EagleSpeculator
|
||||
|
||||
|
||||
def _make_fake_speculator(
|
||||
*,
|
||||
max_num_reqs: int = 8,
|
||||
max_num_tokens: int = 16,
|
||||
max_model_len: int = 1024,
|
||||
draft_max_seq_len: int = 1024,
|
||||
) -> SimpleNamespace:
|
||||
"""Build a fake EagleSpeculator with just the attributes used by
|
||||
``_build_draft_attn_metadata``. We deliberately avoid constructing a
|
||||
real ``EagleSpeculator`` because that requires a full ``VllmConfig``
|
||||
and a draft model.
|
||||
"""
|
||||
fake_input_buffers = SimpleNamespace(
|
||||
query_start_loc=torch.zeros(max_num_reqs + 1, dtype=torch.int32),
|
||||
seq_lens=torch.zeros(max_num_reqs, dtype=torch.int32),
|
||||
)
|
||||
fake_block_tables = SimpleNamespace(
|
||||
input_block_tables=[torch.zeros(max_num_reqs, 4, dtype=torch.int32)],
|
||||
slot_mappings=torch.zeros(1, max_num_tokens, dtype=torch.int64),
|
||||
)
|
||||
return SimpleNamespace(
|
||||
arange=torch.arange(max_num_reqs + 1, dtype=torch.int32, device="cpu"),
|
||||
block_tables=fake_block_tables,
|
||||
input_buffers=fake_input_buffers,
|
||||
attn_groups=[],
|
||||
kv_cache_config=SimpleNamespace(kv_cache_groups=[]),
|
||||
max_model_len=max_model_len,
|
||||
draft_max_seq_len=draft_max_seq_len,
|
||||
)
|
||||
|
||||
|
||||
def _run_build(fake, *, num_reqs, num_reqs_padded, num_tokens_padded, base, step):
|
||||
captured: dict[str, object] = {}
|
||||
|
||||
def fake_build_attn_metadata(**kwargs):
|
||||
captured.update(kwargs)
|
||||
return {}
|
||||
|
||||
with patch.object(base_speculator, "build_attn_metadata", fake_build_attn_metadata):
|
||||
EagleSpeculator._build_draft_attn_metadata(
|
||||
fake, # type: ignore[arg-type]
|
||||
num_reqs=num_reqs,
|
||||
num_reqs_padded=num_reqs_padded,
|
||||
num_tokens_padded=num_tokens_padded,
|
||||
seq_lens_cpu_upper_bound=base,
|
||||
step=step,
|
||||
)
|
||||
return captured
|
||||
|
||||
|
||||
def test_build_draft_attn_metadata_sets_seq_lens_cpu_upper_bound():
|
||||
"""The fix: every per-step ``CommonAttentionMetadata`` carries a non-None
|
||||
``seq_lens_cpu_upper_bound`` derived from the target-side upper bound plus
|
||||
the current draft-step offset. Padded entries are zeroed (matching the
|
||||
main model runner's convention)."""
|
||||
fake = _make_fake_speculator()
|
||||
base = torch.tensor([100, 200, 300, 0], dtype=torch.int32)
|
||||
|
||||
captured = _run_build(
|
||||
fake, num_reqs=3, num_reqs_padded=4, num_tokens_padded=4, base=base, step=2
|
||||
)
|
||||
|
||||
bound = captured["seq_lens_cpu_upper_bound"]
|
||||
assert isinstance(bound, torch.Tensor), (
|
||||
"seq_lens_cpu_upper_bound must be a tensor, not None"
|
||||
)
|
||||
assert bound.shape == (4,), (
|
||||
f"expected shape (num_reqs_padded=4,), got {bound.shape}"
|
||||
)
|
||||
assert bound.device.type == "cpu"
|
||||
assert bound.dtype == torch.int32
|
||||
# base[:num_reqs] + step, padded tail zeroed.
|
||||
assert torch.equal(bound, torch.tensor([102, 202, 302, 0], dtype=torch.int32))
|
||||
|
||||
|
||||
def test_build_draft_attn_metadata_handles_zero_unpadded_reqs():
|
||||
"""Edge case: when ``num_reqs == 0`` the upper-bound tensor must
|
||||
still be a valid all-zero tensor of length ``num_reqs_padded``."""
|
||||
fake = _make_fake_speculator()
|
||||
base = torch.zeros(2, dtype=torch.int32)
|
||||
|
||||
captured = _run_build(
|
||||
fake, num_reqs=0, num_reqs_padded=2, num_tokens_padded=2, base=base, step=1
|
||||
)
|
||||
|
||||
bound = captured["seq_lens_cpu_upper_bound"]
|
||||
assert isinstance(bound, torch.Tensor)
|
||||
assert bound.shape == (2,)
|
||||
assert torch.equal(bound, torch.zeros(2, dtype=torch.int32))
|
||||
|
||||
|
||||
def test_build_draft_attn_metadata_clamps_to_max_model_len():
|
||||
"""The per-request upper bound (target bound + step) is clamped to the
|
||||
model length so it never exceeds the allocated KV range."""
|
||||
fake = _make_fake_speculator(max_model_len=1024)
|
||||
base = torch.tensor([1023, 500], dtype=torch.int32)
|
||||
|
||||
captured = _run_build(
|
||||
fake, num_reqs=2, num_reqs_padded=2, num_tokens_padded=2, base=base, step=3
|
||||
)
|
||||
|
||||
bound = captured["seq_lens_cpu_upper_bound"]
|
||||
# 1023 + 3 = 1026 -> clamped to 1024; 500 + 3 = 503 unaffected.
|
||||
assert torch.equal(bound, torch.tensor([1024, 503], dtype=torch.int32))
|
||||
@@ -1,715 +0,0 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""GPU integration tests for the extensible KV cache allocation paths.
|
||||
|
||||
Drives `GPUModelRunner._allocate_kv_cache_tensors` / `_reshape_kv_cache_tensors`
|
||||
/ `extend_kv_cache` directly with fake attention backends, covering the buffer
|
||||
layouts the extensible flow supports: block-major (one committed prefix),
|
||||
K/V-split (one prefix per half), Mamba (block-major per layer), and hybrid
|
||||
attention + Mamba (attention re-strided to block-major). Buffer sizes exceed
|
||||
the CUDA VMM allocation granularity so touching a block that the commit logic
|
||||
missed would fault instead of silently passing.
|
||||
"""
|
||||
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from vllm.v1.attention.backend import AttentionBackend
|
||||
from vllm.v1.kv_cache_interface import (
|
||||
FullAttentionSpec,
|
||||
KVCacheConfig,
|
||||
KVCacheGroupSpec,
|
||||
KVCacheTensor,
|
||||
MambaSpec,
|
||||
)
|
||||
from vllm.v1.worker.gpu.attn_utils import (
|
||||
_allocate_extensible_kv_cache,
|
||||
_kv_cache_num_segments_by_layer,
|
||||
_reshape_kv_cache,
|
||||
narrow_kv_caches_to_num_blocks,
|
||||
)
|
||||
from vllm.v1.worker.gpu_model_runner import GPUModelRunner
|
||||
from vllm.v1.worker.gpu_worker import Worker
|
||||
from vllm.v1.worker.utils import AttentionGroup
|
||||
|
||||
pytestmark = pytest.mark.skipif(not torch.cuda.is_available(), reason="requires CUDA")
|
||||
|
||||
BLOCK_SIZE = 16
|
||||
NUM_BLOCKS = 256
|
||||
|
||||
|
||||
class _SplitKVBackend(AttentionBackend):
|
||||
"""Fake backend with a K/V-split layout, like FlashAttention."""
|
||||
|
||||
@staticmethod
|
||||
def get_kv_cache_shape(
|
||||
num_blocks: int,
|
||||
block_size: int,
|
||||
num_kv_heads: int,
|
||||
head_size: int,
|
||||
cache_dtype_str: str = "auto",
|
||||
) -> tuple[int, ...]:
|
||||
return (2, num_blocks, block_size, num_kv_heads, head_size)
|
||||
|
||||
|
||||
class _BlockMajorBackend(AttentionBackend):
|
||||
"""Fake backend with a num-blocks-first layout, like FlashInfer."""
|
||||
|
||||
@staticmethod
|
||||
def get_kv_cache_shape(
|
||||
num_blocks: int,
|
||||
block_size: int,
|
||||
num_kv_heads: int,
|
||||
head_size: int,
|
||||
cache_dtype_str: str = "auto",
|
||||
) -> tuple[int, ...]:
|
||||
return (num_blocks, 2, block_size, num_kv_heads, head_size)
|
||||
|
||||
|
||||
class _StrideOrderBackend(AttentionBackend):
|
||||
"""Fake backend whose stride order makes a kv-first shape block-major."""
|
||||
|
||||
@staticmethod
|
||||
def get_kv_cache_shape(
|
||||
num_blocks: int,
|
||||
block_size: int,
|
||||
num_kv_heads: int,
|
||||
head_size: int,
|
||||
cache_dtype_str: str = "auto",
|
||||
) -> tuple[int, ...]:
|
||||
return (2, num_blocks, block_size, num_kv_heads, head_size)
|
||||
|
||||
@staticmethod
|
||||
def get_kv_cache_stride_order(
|
||||
include_num_layers_dimension: bool = False,
|
||||
) -> tuple[int, ...]:
|
||||
assert not include_num_layers_dimension
|
||||
return (1, 0, 2, 3, 4)
|
||||
|
||||
|
||||
def _full_attention_spec() -> FullAttentionSpec:
|
||||
# page_size_bytes = 2 (K+V) * 16 * 8 * 128 * 2 bytes = 64 KiB; 256 blocks
|
||||
# = 16 MiB, several VMM granules per buffer.
|
||||
return FullAttentionSpec(
|
||||
block_size=BLOCK_SIZE,
|
||||
num_kv_heads=8,
|
||||
head_size=128,
|
||||
dtype=torch.bfloat16,
|
||||
)
|
||||
|
||||
|
||||
def _mamba_spec() -> MambaSpec:
|
||||
# page_size_bytes = (8*128 + 16*64) * 4 bytes = 8 KiB per block per layer.
|
||||
return MambaSpec(
|
||||
block_size=BLOCK_SIZE,
|
||||
shapes=((8, 128), (16, 64)),
|
||||
dtypes=(torch.float32, torch.float32),
|
||||
)
|
||||
|
||||
|
||||
def _make_runner(kv_cache_config: KVCacheConfig, attn_groups) -> GPUModelRunner:
|
||||
runner = object.__new__(GPUModelRunner)
|
||||
runner.device = torch.device("cuda:0")
|
||||
runner.kv_cache_config = kv_cache_config
|
||||
runner.attn_groups = attn_groups
|
||||
runner.runner_only_attn_layers = set()
|
||||
runner.cache_config = SimpleNamespace(cache_dtype="auto")
|
||||
return runner
|
||||
|
||||
|
||||
def _attention_config(spec: FullAttentionSpec, backend) -> tuple[KVCacheConfig, list]:
|
||||
kv_cache_config = KVCacheConfig(
|
||||
num_blocks=NUM_BLOCKS,
|
||||
kv_cache_tensors=[
|
||||
KVCacheTensor(size=NUM_BLOCKS * spec.page_size_bytes, shared_by=["layer.0"])
|
||||
],
|
||||
kv_cache_groups=[KVCacheGroupSpec(layer_names=["layer.0"], kv_cache_spec=spec)],
|
||||
)
|
||||
attn_groups = [
|
||||
[
|
||||
AttentionGroup(
|
||||
backend=backend,
|
||||
layer_names=["layer.0"],
|
||||
kv_cache_spec=spec,
|
||||
kv_cache_group_id=0,
|
||||
)
|
||||
]
|
||||
]
|
||||
return kv_cache_config, attn_groups
|
||||
|
||||
|
||||
def _free_buffers(runner: GPUModelRunner) -> None:
|
||||
buffers = getattr(runner, "extensible_kv_buffers", None)
|
||||
if buffers is not None:
|
||||
buffers.free()
|
||||
|
||||
|
||||
def test_kv_cache_num_segments_by_layer() -> None:
|
||||
"""Segment counts follow the physical layout of each layer's backend."""
|
||||
spec = _full_attention_spec()
|
||||
for backend, expected in (
|
||||
(_SplitKVBackend, 2),
|
||||
(_BlockMajorBackend, 1),
|
||||
# kv-first logical shape but block-major physical order -> 1 segment.
|
||||
(_StrideOrderBackend, 1),
|
||||
):
|
||||
kv_cache_config, attn_groups = _attention_config(spec, backend)
|
||||
runner = _make_runner(kv_cache_config, attn_groups)
|
||||
assert runner._kv_cache_num_segments_by_layer() == {"layer.0": expected}
|
||||
|
||||
|
||||
def test_extensible_split_layout_grows_both_halves() -> None:
|
||||
"""A K/V-split layer keeps its natural layout and both halves grow in
|
||||
lockstep."""
|
||||
spec = _full_attention_spec()
|
||||
kv_cache_config, attn_groups = _attention_config(spec, _SplitKVBackend)
|
||||
runner = _make_runner(kv_cache_config, attn_groups)
|
||||
try:
|
||||
raw_tensors = runner._allocate_kv_cache_tensors(
|
||||
kv_cache_config, extensible=True
|
||||
)
|
||||
kv_caches = runner._reshape_kv_cache_tensors(raw_tensors, [BLOCK_SIZE])
|
||||
kv_cache = kv_caches["layer.0"]
|
||||
assert kv_cache.shape == (2, NUM_BLOCKS, BLOCK_SIZE, 8, 128)
|
||||
[(buffer, bytes_per_block_per_segment)] = runner.extensible_kv_buffers.buffers
|
||||
assert buffer.num_segments == 2
|
||||
assert bytes_per_block_per_segment == spec.page_size_bytes // 2
|
||||
|
||||
# Only block 0 is committed -- in each half.
|
||||
kv_cache[0, 0].fill_(1) # K, block 0
|
||||
kv_cache[1, 0].fill_(2) # V, block 0
|
||||
torch.accelerator.synchronize()
|
||||
|
||||
runner.extend_kv_cache(NUM_BLOCKS)
|
||||
# Old data survives the grow; new blocks are usable in both halves and
|
||||
# zeroed.
|
||||
assert torch.all(kv_cache[0, 0] == 1)
|
||||
assert torch.all(kv_cache[1, 0] == 2)
|
||||
kv_cache[0, NUM_BLOCKS - 1].fill_(3)
|
||||
kv_cache[1, NUM_BLOCKS - 1].fill_(4)
|
||||
torch.accelerator.synchronize()
|
||||
assert torch.all(kv_cache[0, NUM_BLOCKS - 1] == 3)
|
||||
assert torch.all(kv_cache[1, NUM_BLOCKS - 1] == 4)
|
||||
assert torch.count_nonzero(kv_cache[:, 1 : NUM_BLOCKS - 1]) == 0
|
||||
finally:
|
||||
_free_buffers(runner)
|
||||
|
||||
|
||||
def test_extensible_block_major_layout() -> None:
|
||||
"""A layer whose physical layout is block-major uses a single segment."""
|
||||
spec = _full_attention_spec()
|
||||
kv_cache_config, attn_groups = _attention_config(spec, _BlockMajorBackend)
|
||||
runner = _make_runner(kv_cache_config, attn_groups)
|
||||
try:
|
||||
raw_tensors = runner._allocate_kv_cache_tensors(
|
||||
kv_cache_config, extensible=True
|
||||
)
|
||||
kv_caches = runner._reshape_kv_cache_tensors(raw_tensors, [BLOCK_SIZE])
|
||||
kv_cache = kv_caches["layer.0"]
|
||||
assert kv_cache.shape == (NUM_BLOCKS, 2, BLOCK_SIZE, 8, 128)
|
||||
[(buffer, bytes_per_block_per_segment)] = runner.extensible_kv_buffers.buffers
|
||||
assert buffer.num_segments == 1
|
||||
assert bytes_per_block_per_segment == spec.page_size_bytes
|
||||
|
||||
kv_cache[0].fill_(1)
|
||||
runner.extend_kv_cache(NUM_BLOCKS)
|
||||
kv_cache[NUM_BLOCKS - 1].fill_(2)
|
||||
torch.accelerator.synchronize()
|
||||
assert torch.all(kv_cache[0] == 1)
|
||||
assert torch.all(kv_cache[NUM_BLOCKS - 1] == 2)
|
||||
assert torch.count_nonzero(kv_cache[1 : NUM_BLOCKS - 1]) == 0
|
||||
finally:
|
||||
_free_buffers(runner)
|
||||
|
||||
|
||||
def test_legacy_split_layout_commits_everything() -> None:
|
||||
"""Without `extensible`, the full buffer is committed up front."""
|
||||
spec = _full_attention_spec()
|
||||
kv_cache_config, attn_groups = _attention_config(spec, _SplitKVBackend)
|
||||
runner = _make_runner(kv_cache_config, attn_groups)
|
||||
raw_tensors = runner._allocate_kv_cache_tensors(kv_cache_config, extensible=False)
|
||||
kv_caches = runner._reshape_kv_cache_tensors(raw_tensors, [BLOCK_SIZE])
|
||||
kv_cache = kv_caches["layer.0"]
|
||||
kv_cache[0, NUM_BLOCKS - 1].fill_(1)
|
||||
kv_cache[1, NUM_BLOCKS - 1].fill_(2)
|
||||
torch.accelerator.synchronize()
|
||||
assert torch.all(kv_cache[0, NUM_BLOCKS - 1] == 1)
|
||||
assert torch.all(kv_cache[1, NUM_BLOCKS - 1] == 2)
|
||||
with pytest.raises(RuntimeError, match="extensible"):
|
||||
runner.extend_kv_cache(NUM_BLOCKS)
|
||||
|
||||
|
||||
def test_extensible_mamba_grows_per_layer() -> None:
|
||||
"""Mamba per-layer buffers are block-major and grow with the KV cache."""
|
||||
spec = _mamba_spec()
|
||||
num_blocks = 512
|
||||
layer_names = ["mamba.0", "mamba.1"]
|
||||
kv_cache_config = KVCacheConfig(
|
||||
num_blocks=num_blocks,
|
||||
kv_cache_tensors=[
|
||||
KVCacheTensor(size=num_blocks * spec.page_size_bytes, shared_by=[name])
|
||||
for name in layer_names
|
||||
],
|
||||
kv_cache_groups=[KVCacheGroupSpec(layer_names=layer_names, kv_cache_spec=spec)],
|
||||
)
|
||||
attn_groups = [
|
||||
[
|
||||
AttentionGroup(
|
||||
backend=_BlockMajorBackend,
|
||||
layer_names=layer_names,
|
||||
kv_cache_spec=spec,
|
||||
kv_cache_group_id=0,
|
||||
)
|
||||
]
|
||||
]
|
||||
runner = _make_runner(kv_cache_config, attn_groups)
|
||||
try:
|
||||
raw_tensors = runner._allocate_kv_cache_tensors(
|
||||
kv_cache_config, extensible=True
|
||||
)
|
||||
kv_caches = runner._reshape_kv_cache_tensors(raw_tensors, [BLOCK_SIZE])
|
||||
assert set(kv_caches) == set(layer_names)
|
||||
assert len(runner.extensible_kv_buffers.buffers) == len(layer_names)
|
||||
for buffer, bytes_per_block_per_segment in runner.extensible_kv_buffers.buffers:
|
||||
assert buffer.num_segments == 1
|
||||
assert bytes_per_block_per_segment == spec.page_size_bytes
|
||||
|
||||
# Write block 0 of every state of every layer (the committed
|
||||
# prefixes), then grow.
|
||||
for name in layer_names:
|
||||
for state_tensor in kv_caches[name]:
|
||||
state_tensor[0].fill_(1)
|
||||
torch.accelerator.synchronize()
|
||||
runner.extend_kv_cache(num_blocks)
|
||||
for name in layer_names:
|
||||
for state_tensor in kv_caches[name]:
|
||||
state_tensor[num_blocks - 1].fill_(2)
|
||||
torch.accelerator.synchronize()
|
||||
for name in layer_names:
|
||||
for state_tensor in kv_caches[name]:
|
||||
assert torch.all(state_tensor[0] == 1)
|
||||
assert torch.all(state_tensor[num_blocks - 1] == 2)
|
||||
assert torch.count_nonzero(state_tensor[1 : num_blocks - 1]) == 0
|
||||
finally:
|
||||
_free_buffers(runner)
|
||||
|
||||
|
||||
def test_extensible_hybrid_attention_mamba() -> None:
|
||||
"""In hybrid models the attention cache is re-strided to block-major, so
|
||||
its buffer must use a single segment."""
|
||||
attn_spec = _full_attention_spec()
|
||||
mamba_spec = _mamba_spec()
|
||||
kv_cache_config = KVCacheConfig(
|
||||
num_blocks=NUM_BLOCKS,
|
||||
kv_cache_tensors=[
|
||||
KVCacheTensor(
|
||||
size=NUM_BLOCKS * attn_spec.page_size_bytes, shared_by=["attn.0"]
|
||||
),
|
||||
KVCacheTensor(
|
||||
size=NUM_BLOCKS * mamba_spec.page_size_bytes, shared_by=["mamba.0"]
|
||||
),
|
||||
],
|
||||
kv_cache_groups=[
|
||||
KVCacheGroupSpec(layer_names=["attn.0"], kv_cache_spec=attn_spec),
|
||||
KVCacheGroupSpec(layer_names=["mamba.0"], kv_cache_spec=mamba_spec),
|
||||
],
|
||||
)
|
||||
attn_groups = [
|
||||
[
|
||||
AttentionGroup(
|
||||
backend=_SplitKVBackend,
|
||||
layer_names=["attn.0"],
|
||||
kv_cache_spec=attn_spec,
|
||||
kv_cache_group_id=0,
|
||||
)
|
||||
],
|
||||
[
|
||||
AttentionGroup(
|
||||
backend=_BlockMajorBackend,
|
||||
layer_names=["mamba.0"],
|
||||
kv_cache_spec=mamba_spec,
|
||||
kv_cache_group_id=1,
|
||||
)
|
||||
],
|
||||
]
|
||||
runner = _make_runner(kv_cache_config, attn_groups)
|
||||
try:
|
||||
# The K/V-split attention layer is forced to one segment by the hybrid
|
||||
# block-major re-stride.
|
||||
assert runner._kv_cache_num_segments_by_layer() == {"attn.0": 1, "mamba.0": 1}
|
||||
|
||||
raw_tensors = runner._allocate_kv_cache_tensors(
|
||||
kv_cache_config, extensible=True
|
||||
)
|
||||
kv_caches = runner._reshape_kv_cache_tensors(
|
||||
raw_tensors, [BLOCK_SIZE, BLOCK_SIZE]
|
||||
)
|
||||
attn_cache = kv_caches["attn.0"]
|
||||
# `_update_hybrid_attention_mamba_layout` re-strides to interleave K/V
|
||||
# per block: block b spans one contiguous page.
|
||||
hidden_size = attn_cache.shape[2:].numel()
|
||||
assert attn_cache.stride()[:2] == (hidden_size, 2 * hidden_size)
|
||||
|
||||
attn_cache[0, 0].fill_(1) # K, block 0
|
||||
attn_cache[1, 0].fill_(2) # V, block 0
|
||||
for state_tensor in kv_caches["mamba.0"]:
|
||||
state_tensor[0].fill_(3)
|
||||
torch.accelerator.synchronize()
|
||||
|
||||
runner.extend_kv_cache(NUM_BLOCKS)
|
||||
attn_cache[0, NUM_BLOCKS - 1].fill_(4)
|
||||
attn_cache[1, NUM_BLOCKS - 1].fill_(5)
|
||||
for state_tensor in kv_caches["mamba.0"]:
|
||||
state_tensor[NUM_BLOCKS - 1].fill_(6)
|
||||
torch.accelerator.synchronize()
|
||||
assert torch.all(attn_cache[0, 0] == 1)
|
||||
assert torch.all(attn_cache[1, 0] == 2)
|
||||
assert torch.all(attn_cache[0, NUM_BLOCKS - 1] == 4)
|
||||
assert torch.all(attn_cache[1, NUM_BLOCKS - 1] == 5)
|
||||
assert torch.count_nonzero(attn_cache[:, 1 : NUM_BLOCKS - 1]) == 0
|
||||
for state_tensor in kv_caches["mamba.0"]:
|
||||
assert torch.all(state_tensor[0] == 3)
|
||||
assert torch.all(state_tensor[NUM_BLOCKS - 1] == 6)
|
||||
assert torch.count_nonzero(state_tensor[1 : NUM_BLOCKS - 1]) == 0
|
||||
finally:
|
||||
_free_buffers(runner)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# V2 model runner (vllm.v1.worker.gpu) extensible allocation
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _v2_allocate(kv_cache_config, attn_groups, kernel_block_sizes):
|
||||
flat_groups = [g for groups in attn_groups for g in groups]
|
||||
raw_tensors, buffers = _allocate_extensible_kv_cache(
|
||||
kv_cache_config,
|
||||
{},
|
||||
torch.device("cuda:0"),
|
||||
flat_groups,
|
||||
kernel_block_sizes,
|
||||
"auto",
|
||||
)
|
||||
kv_caches = _reshape_kv_cache(
|
||||
attn_groups=flat_groups,
|
||||
kv_cache_raw_tensors=raw_tensors,
|
||||
cache_dtype="auto",
|
||||
kernel_block_sizes=kernel_block_sizes,
|
||||
shared_kv_cache_layers={},
|
||||
kv_cache_config=kv_cache_config,
|
||||
)
|
||||
return kv_caches, buffers
|
||||
|
||||
|
||||
def test_v2_num_segments_by_layer() -> None:
|
||||
"""V2 segment counts follow the layer's physical layout, and hybrid
|
||||
models force block-major (one segment)."""
|
||||
spec = _full_attention_spec()
|
||||
for backend, expected in (
|
||||
(_SplitKVBackend, 2),
|
||||
(_BlockMajorBackend, 1),
|
||||
(_StrideOrderBackend, 1),
|
||||
):
|
||||
_, attn_groups = _attention_config(spec, backend)
|
||||
flat_groups = [g for groups in attn_groups for g in groups]
|
||||
assert _kv_cache_num_segments_by_layer(
|
||||
flat_groups, [BLOCK_SIZE], "auto", has_mamba=False
|
||||
) == {"layer.0": expected}
|
||||
assert _kv_cache_num_segments_by_layer(
|
||||
flat_groups, [BLOCK_SIZE], "auto", has_mamba=True
|
||||
) == {"layer.0": 1}
|
||||
|
||||
|
||||
def test_v2_extensible_split_layout_grows_incrementally() -> None:
|
||||
"""A K/V-split layer grows both halves in lockstep through the staged
|
||||
commits the V2 flow performs (init -> warmup prefix -> final size)."""
|
||||
spec = _full_attention_spec()
|
||||
kv_cache_config, attn_groups = _attention_config(spec, _SplitKVBackend)
|
||||
kv_caches, buffers = _v2_allocate(kv_cache_config, attn_groups, [BLOCK_SIZE])
|
||||
try:
|
||||
kv_cache = kv_caches["layer.0"]
|
||||
assert kv_cache.shape == (2, NUM_BLOCKS, BLOCK_SIZE, 8, 128)
|
||||
assert buffers.num_blocks_committed == 1
|
||||
|
||||
kv_cache[0, 0].fill_(1) # K, block 0
|
||||
kv_cache[1, 0].fill_(2) # V, block 0
|
||||
torch.accelerator.synchronize()
|
||||
|
||||
# Warmup-style prefix commit, then the final post-warmup commit.
|
||||
buffers.commit(8)
|
||||
kv_cache[0, 7].fill_(3)
|
||||
torch.accelerator.synchronize()
|
||||
buffers.commit(NUM_BLOCKS)
|
||||
# Shrink requests are ignored.
|
||||
buffers.commit(1)
|
||||
assert buffers.num_blocks_committed == NUM_BLOCKS
|
||||
|
||||
kv_cache[1, NUM_BLOCKS - 1].fill_(4)
|
||||
torch.accelerator.synchronize()
|
||||
assert torch.all(kv_cache[0, 0] == 1)
|
||||
assert torch.all(kv_cache[1, 0] == 2)
|
||||
assert torch.all(kv_cache[0, 7] == 3)
|
||||
assert torch.all(kv_cache[1, NUM_BLOCKS - 1] == 4)
|
||||
assert torch.count_nonzero(kv_cache[:, 1:7]) == 0
|
||||
assert torch.count_nonzero(kv_cache[:, 8 : NUM_BLOCKS - 1]) == 0
|
||||
assert buffers.physical_bytes >= NUM_BLOCKS * spec.page_size_bytes
|
||||
finally:
|
||||
buffers.free()
|
||||
|
||||
|
||||
def test_v2_extensible_hybrid_attention_mamba() -> None:
|
||||
"""V2 hybrid models re-stride attention to block-major; both the
|
||||
attention and Mamba buffers grow as single-segment prefixes."""
|
||||
attn_spec = _full_attention_spec()
|
||||
mamba_spec = _mamba_spec()
|
||||
kv_cache_config = KVCacheConfig(
|
||||
num_blocks=NUM_BLOCKS,
|
||||
kv_cache_tensors=[
|
||||
KVCacheTensor(
|
||||
size=NUM_BLOCKS * attn_spec.page_size_bytes, shared_by=["attn.0"]
|
||||
),
|
||||
KVCacheTensor(
|
||||
size=NUM_BLOCKS * mamba_spec.page_size_bytes, shared_by=["mamba.0"]
|
||||
),
|
||||
],
|
||||
kv_cache_groups=[
|
||||
KVCacheGroupSpec(layer_names=["attn.0"], kv_cache_spec=attn_spec),
|
||||
KVCacheGroupSpec(layer_names=["mamba.0"], kv_cache_spec=mamba_spec),
|
||||
],
|
||||
)
|
||||
attn_groups = [
|
||||
[
|
||||
AttentionGroup(
|
||||
backend=_SplitKVBackend,
|
||||
layer_names=["attn.0"],
|
||||
kv_cache_spec=attn_spec,
|
||||
kv_cache_group_id=0,
|
||||
)
|
||||
],
|
||||
[
|
||||
AttentionGroup(
|
||||
backend=_BlockMajorBackend,
|
||||
layer_names=["mamba.0"],
|
||||
kv_cache_spec=mamba_spec,
|
||||
kv_cache_group_id=1,
|
||||
)
|
||||
],
|
||||
]
|
||||
kv_caches, buffers = _v2_allocate(
|
||||
kv_cache_config, attn_groups, [BLOCK_SIZE, BLOCK_SIZE]
|
||||
)
|
||||
try:
|
||||
attn_cache = kv_caches["attn.0"]
|
||||
# Re-strided to interleave K/V per block: block b spans one page.
|
||||
hidden_size = attn_cache.shape[2:].numel()
|
||||
assert attn_cache.stride()[:2] == (hidden_size, 2 * hidden_size)
|
||||
|
||||
attn_cache[0, 0].fill_(1)
|
||||
attn_cache[1, 0].fill_(2)
|
||||
for state_tensor in kv_caches["mamba.0"]:
|
||||
state_tensor[0].fill_(3)
|
||||
torch.accelerator.synchronize()
|
||||
|
||||
buffers.commit(NUM_BLOCKS)
|
||||
attn_cache[0, NUM_BLOCKS - 1].fill_(4)
|
||||
attn_cache[1, NUM_BLOCKS - 1].fill_(5)
|
||||
for state_tensor in kv_caches["mamba.0"]:
|
||||
state_tensor[NUM_BLOCKS - 1].fill_(6)
|
||||
torch.accelerator.synchronize()
|
||||
assert torch.all(attn_cache[0, 0] == 1)
|
||||
assert torch.all(attn_cache[1, 0] == 2)
|
||||
assert torch.all(attn_cache[0, NUM_BLOCKS - 1] == 4)
|
||||
assert torch.all(attn_cache[1, NUM_BLOCKS - 1] == 5)
|
||||
assert torch.count_nonzero(attn_cache[:, 1 : NUM_BLOCKS - 1]) == 0
|
||||
for state_tensor in kv_caches["mamba.0"]:
|
||||
assert torch.all(state_tensor[0] == 3)
|
||||
assert torch.all(state_tensor[NUM_BLOCKS - 1] == 6)
|
||||
assert torch.count_nonzero(state_tensor[1 : NUM_BLOCKS - 1]) == 0
|
||||
finally:
|
||||
buffers.free()
|
||||
|
||||
|
||||
def test_v2_extensible_packed_layout() -> None:
|
||||
"""A packed (block_stride) layout uses one shared block-major buffer;
|
||||
per-layer pages within a block stay isolated across commits."""
|
||||
spec = _full_attention_spec()
|
||||
page_bytes = spec.page_size_bytes
|
||||
block_stride = 2 * page_bytes # two layers packed per block
|
||||
layer_names = ["packed.0", "packed.1"]
|
||||
kv_cache_config = KVCacheConfig(
|
||||
num_blocks=NUM_BLOCKS,
|
||||
kv_cache_tensors=[
|
||||
KVCacheTensor(
|
||||
size=NUM_BLOCKS * block_stride,
|
||||
shared_by=[name],
|
||||
offset=i * page_bytes,
|
||||
block_stride=block_stride,
|
||||
)
|
||||
for i, name in enumerate(layer_names)
|
||||
],
|
||||
kv_cache_groups=[KVCacheGroupSpec(layer_names=layer_names, kv_cache_spec=spec)],
|
||||
)
|
||||
attn_groups = [
|
||||
[
|
||||
AttentionGroup(
|
||||
backend=_BlockMajorBackend,
|
||||
layer_names=layer_names,
|
||||
kv_cache_spec=spec,
|
||||
kv_cache_group_id=0,
|
||||
)
|
||||
]
|
||||
]
|
||||
kv_caches, buffers = _v2_allocate(kv_cache_config, attn_groups, [BLOCK_SIZE])
|
||||
try:
|
||||
assert len(buffers.buffers) == 1
|
||||
[(buffer, bytes_per_block)] = buffers.buffers
|
||||
assert buffer.num_segments == 1
|
||||
assert bytes_per_block == block_stride
|
||||
|
||||
cache0, cache1 = kv_caches["packed.0"], kv_caches["packed.1"]
|
||||
assert cache0.shape == (NUM_BLOCKS, 2, BLOCK_SIZE, 8, 128)
|
||||
|
||||
cache0[0].fill_(1)
|
||||
cache1[0].fill_(2)
|
||||
torch.accelerator.synchronize()
|
||||
buffers.commit(NUM_BLOCKS)
|
||||
cache0[NUM_BLOCKS - 1].fill_(3)
|
||||
torch.accelerator.synchronize()
|
||||
assert torch.all(cache0[0] == 1)
|
||||
assert torch.all(cache1[0] == 2)
|
||||
assert torch.all(cache0[NUM_BLOCKS - 1] == 3)
|
||||
# The other layer's page of the same block is untouched, and all
|
||||
# middle blocks were zeroed on commit.
|
||||
assert torch.count_nonzero(cache1[1:]) == 0
|
||||
assert torch.count_nonzero(cache0[1 : NUM_BLOCKS - 1]) == 0
|
||||
|
||||
committed = NUM_BLOCKS // 2
|
||||
narrowed = narrow_kv_caches_to_num_blocks(
|
||||
kv_caches,
|
||||
[g for groups in attn_groups for g in groups],
|
||||
[BLOCK_SIZE],
|
||||
"auto",
|
||||
committed,
|
||||
kv_cache_config,
|
||||
)
|
||||
narrowed0 = narrowed["packed.0"]
|
||||
narrowed1 = narrowed["packed.1"]
|
||||
assert narrowed0.untyped_storage().data_ptr() == buffer.base_ptr
|
||||
assert (
|
||||
narrowed0.untyped_storage().data_ptr()
|
||||
== narrowed1.untyped_storage().data_ptr()
|
||||
)
|
||||
assert narrowed0.untyped_storage().nbytes() == committed * block_stride
|
||||
assert narrowed0.stride() == cache0.stride()
|
||||
assert narrowed1.stride() == cache1.stride()
|
||||
finally:
|
||||
buffers.free()
|
||||
|
||||
|
||||
def test_v2_extensible_release_and_recommit() -> None:
|
||||
"""Sleep/wake cycle: release_physical discards data but keeps VA and
|
||||
views valid; recommit restores the committed size with zeroed pages."""
|
||||
spec = _full_attention_spec()
|
||||
kv_cache_config, attn_groups = _attention_config(spec, _SplitKVBackend)
|
||||
kv_caches, buffers = _v2_allocate(kv_cache_config, attn_groups, [BLOCK_SIZE])
|
||||
try:
|
||||
kv_cache = kv_caches["layer.0"]
|
||||
base_ptr = buffers.buffers[0][0].base_ptr
|
||||
buffers.commit(NUM_BLOCKS)
|
||||
kv_cache.fill_(7)
|
||||
torch.accelerator.synchronize()
|
||||
assert buffers.physical_bytes > 0
|
||||
|
||||
buffers.release_physical()
|
||||
assert buffers.physical_bytes == 0
|
||||
assert buffers.num_blocks_committed == 0
|
||||
|
||||
buffers.recommit()
|
||||
assert buffers.num_blocks_committed == NUM_BLOCKS
|
||||
assert buffers.buffers[0][0].base_ptr == base_ptr
|
||||
torch.accelerator.synchronize()
|
||||
# Data was discarded; fresh pages are zeroed and writable through
|
||||
# the original views.
|
||||
assert torch.count_nonzero(kv_cache) == 0
|
||||
kv_cache[1, NUM_BLOCKS - 1].fill_(9)
|
||||
torch.accelerator.synchronize()
|
||||
assert torch.all(kv_cache[1, NUM_BLOCKS - 1] == 9)
|
||||
finally:
|
||||
buffers.free()
|
||||
|
||||
|
||||
def test_v2_extensible_connector_sleep_fails_before_remapping() -> None:
|
||||
"""Connector registrations must not survive physical-page replacement."""
|
||||
worker = object.__new__(Worker)
|
||||
worker.model_runner = SimpleNamespace(extensible_kv_buffers=object())
|
||||
worker.vllm_config = SimpleNamespace(kv_transfer_config=object())
|
||||
|
||||
with pytest.raises(RuntimeError, match="invalidates.*memory registration"):
|
||||
worker.sleep()
|
||||
|
||||
|
||||
def test_v2_narrow_kv_caches_to_num_blocks() -> None:
|
||||
"""Connector-registration views are trimmed to the committed block count
|
||||
along each layout's block dim, keeping base pointers and strides (so the
|
||||
K and V segment prefixes are addressed exactly)."""
|
||||
spec = _full_attention_spec()
|
||||
kv_cache_config, attn_groups = _attention_config(spec, _SplitKVBackend)
|
||||
kv_caches, buffers = _v2_allocate(kv_cache_config, attn_groups, [BLOCK_SIZE])
|
||||
try:
|
||||
committed = 16
|
||||
buffers.commit(committed)
|
||||
narrowed = narrow_kv_caches_to_num_blocks(
|
||||
kv_caches,
|
||||
[g for groups in attn_groups for g in groups],
|
||||
[BLOCK_SIZE],
|
||||
"auto",
|
||||
committed,
|
||||
kv_cache_config,
|
||||
)
|
||||
full = kv_caches["layer.0"]
|
||||
trimmed = narrowed["layer.0"]
|
||||
assert trimmed.shape == (2, committed, BLOCK_SIZE, 8, 128)
|
||||
assert trimmed.stride() == full.stride()
|
||||
# K prefix starts at the buffer base; V prefix at the segment offset.
|
||||
assert trimmed[0].data_ptr() == full[0].data_ptr()
|
||||
assert trimmed[1].data_ptr() == full[1].data_ptr()
|
||||
# The narrowed views cover only committed memory.
|
||||
trimmed[0, committed - 1].fill_(1)
|
||||
trimmed[1, committed - 1].fill_(2)
|
||||
torch.accelerator.synchronize()
|
||||
assert torch.all(full[0, committed - 1] == 1)
|
||||
assert torch.all(full[1, committed - 1] == 2)
|
||||
finally:
|
||||
buffers.free()
|
||||
|
||||
|
||||
def test_v2_extensible_defragment_on_commit() -> None:
|
||||
"""commit(defragment=True) re-maps each segment prefix as ONE physical
|
||||
chunk (required for KV-transfer registration), discarding prior data."""
|
||||
spec = _full_attention_spec()
|
||||
kv_cache_config, attn_groups = _attention_config(spec, _SplitKVBackend)
|
||||
kv_caches, buffers = _v2_allocate(kv_cache_config, attn_groups, [BLOCK_SIZE])
|
||||
try:
|
||||
kv_cache = kv_caches["layer.0"]
|
||||
# Staged commits spanning multiple VMM granules -> multiple physical
|
||||
# chunks per segment.
|
||||
buffers.commit(8)
|
||||
buffers.commit(NUM_BLOCKS // 2)
|
||||
kv_cache[0, 0].fill_(1)
|
||||
torch.accelerator.synchronize()
|
||||
[(buffer, _)] = buffers.buffers
|
||||
assert len(buffer._buffer._handles) > 2
|
||||
|
||||
buffers.commit(NUM_BLOCKS, defragment=True)
|
||||
# One chunk per segment; data discarded (zeroed); views still work.
|
||||
assert len(buffer._buffer._handles) == 2
|
||||
assert buffers.num_blocks_committed == NUM_BLOCKS
|
||||
torch.accelerator.synchronize()
|
||||
assert torch.count_nonzero(kv_cache) == 0
|
||||
kv_cache[1, NUM_BLOCKS - 1].fill_(3)
|
||||
torch.accelerator.synchronize()
|
||||
assert torch.all(kv_cache[1, NUM_BLOCKS - 1] == 3)
|
||||
finally:
|
||||
buffers.free()
|
||||
@@ -1083,7 +1083,8 @@ def parse_flash_attn_features() -> dict[str, dict[str, Any]]:
|
||||
return {}
|
||||
|
||||
# Analyze the functions to determine FA3/FA4-specific features
|
||||
fa3_supports_fp8 = True
|
||||
fa3_supports_fp8 = False
|
||||
fa4_supports_fp8 = False
|
||||
fa3_supports_sinks = False
|
||||
fa4_supports_sinks = False
|
||||
fa3_compute_cap: str | None = None
|
||||
@@ -1093,6 +1094,44 @@ def parse_flash_attn_features() -> dict[str, dict[str, Any]]:
|
||||
if not isinstance(node, ast.FunctionDef):
|
||||
continue
|
||||
|
||||
# Check flash_attn_supports_kv_cache_dtype for fp8 support per FA version.
|
||||
# Accept both equality checks and membership checks such as `in (3, 4)`.
|
||||
if node.name == "flash_attn_supports_kv_cache_dtype":
|
||||
for n in ast.walk(node):
|
||||
if not (
|
||||
isinstance(n, ast.Compare)
|
||||
and len(n.ops) == 1
|
||||
and len(n.comparators) == 1
|
||||
):
|
||||
continue
|
||||
is_version_compare = (
|
||||
isinstance(n.left, ast.Name) and n.left.id == "fa_version"
|
||||
) or (
|
||||
isinstance(n.left, ast.Call)
|
||||
and isinstance(n.left.func, ast.Name)
|
||||
and n.left.func.id == "get_flash_attn_version"
|
||||
)
|
||||
if not is_version_compare:
|
||||
continue
|
||||
|
||||
versions: list[Any] = []
|
||||
comparator = n.comparators[0]
|
||||
if isinstance(n.ops[0], ast.Eq) and isinstance(
|
||||
comparator, ast.Constant
|
||||
):
|
||||
versions = [comparator.value]
|
||||
elif isinstance(n.ops[0], ast.In) and isinstance(
|
||||
comparator, (ast.Tuple, ast.List, ast.Set)
|
||||
):
|
||||
versions = [
|
||||
elt.value
|
||||
for elt in comparator.elts
|
||||
if isinstance(elt, ast.Constant)
|
||||
]
|
||||
|
||||
fa3_supports_fp8 |= 3 in versions
|
||||
fa4_supports_fp8 |= 4 in versions
|
||||
|
||||
# Check flash_attn_supports_sinks - looks for `fa_version == 3/4`
|
||||
# or `get_flash_attn_version() == 3/4` (also accepts `in (3, 4)`)
|
||||
if node.name == "flash_attn_supports_sinks":
|
||||
@@ -1199,7 +1238,7 @@ def parse_flash_attn_features() -> dict[str, dict[str, Any]]:
|
||||
},
|
||||
"fa4": {
|
||||
"compute_capability": fa4_compute_cap,
|
||||
"supports_fp8": False,
|
||||
"supports_fp8": fa4_supports_fp8,
|
||||
"supports_sink": fa4_supports_sinks,
|
||||
},
|
||||
}
|
||||
@@ -1255,6 +1294,22 @@ def parse_flashinfer_trtllm_features() -> dict[str, dict[str, Any]]:
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _apply_fp8_support(kv_cache_dtypes: str, supports_fp8: bool) -> str:
|
||||
"""Add or remove fp8 dtypes from a comma-separated kv_cache_dtypes string.
|
||||
|
||||
The base FLASH_ATTN backend lists fp8 dtypes in ``supported_kv_cache_dtypes``,
|
||||
but actual support varies by FA version (e.g. FA2 has no fp8 support), so each
|
||||
variant's row is normalized to match its own capability.
|
||||
"""
|
||||
fp8_dtypes = {"fp8", "fp8_e4m3", "fp8_e5m2"}
|
||||
dtypes = kv_cache_dtypes.split(", ")
|
||||
base_dtypes = [dtype for dtype in dtypes if dtype not in fp8_dtypes]
|
||||
if supports_fp8:
|
||||
advertised_fp8_dtypes = [dtype for dtype in dtypes if dtype in fp8_dtypes]
|
||||
return ", ".join(base_dtypes + advertised_fp8_dtypes)
|
||||
return ", ".join(base_dtypes)
|
||||
|
||||
|
||||
def _expand_flash_attn_variants(
|
||||
all_backends: list[dict[str, Any]],
|
||||
fa_features: dict[str, dict[str, Any]],
|
||||
@@ -1275,6 +1330,9 @@ def _expand_flash_attn_variants(
|
||||
fa2["_sort_key"] = "FLASH_ATTN"
|
||||
fa2["_sort_order"] = 0
|
||||
fa2["supports_sink"] = fa_features["fa2"]["supports_sink"]
|
||||
fa2["kv_cache_dtypes"] = _apply_fp8_support(
|
||||
backend["kv_cache_dtypes"], fa_features["fa2"]["supports_fp8"]
|
||||
)
|
||||
|
||||
# Create FA3 entry (uses parsed compute_capability from fa_utils)
|
||||
fa3 = backend.copy()
|
||||
@@ -1284,11 +1342,9 @@ def _expand_flash_attn_variants(
|
||||
if fa_features["fa3"]["compute_capability"]:
|
||||
fa3["compute_capability"] = fa_features["fa3"]["compute_capability"]
|
||||
fa3["supports_sink"] = fa_features["fa3"]["supports_sink"]
|
||||
if fa_features["fa3"]["supports_fp8"]:
|
||||
base_dtypes = backend["kv_cache_dtypes"].split(", ")
|
||||
fp8_dtypes = ["fp8", "fp8_e4m3", "fp8_e5m2"]
|
||||
new_dtypes = [d for d in fp8_dtypes if d not in base_dtypes]
|
||||
fa3["kv_cache_dtypes"] = ", ".join(base_dtypes + new_dtypes)
|
||||
fa3["kv_cache_dtypes"] = _apply_fp8_support(
|
||||
backend["kv_cache_dtypes"], fa_features["fa3"]["supports_fp8"]
|
||||
)
|
||||
|
||||
expanded.append(fa2)
|
||||
expanded.append(fa3)
|
||||
@@ -1302,6 +1358,9 @@ def _expand_flash_attn_variants(
|
||||
if fa_features["fa4"].get("compute_capability"):
|
||||
fa4["compute_capability"] = fa_features["fa4"]["compute_capability"]
|
||||
fa4["supports_sink"] = fa_features["fa4"]["supports_sink"]
|
||||
fa4["kv_cache_dtypes"] = _apply_fp8_support(
|
||||
backend["kv_cache_dtypes"], fa_features["fa4"]["supports_fp8"]
|
||||
)
|
||||
expanded.append(fa4)
|
||||
|
||||
return expanded
|
||||
|
||||
+13
-7
@@ -835,12 +835,16 @@ def cutlass_scaled_mm_azp(
|
||||
|
||||
|
||||
def cutlass_group_gemm_supported(cuda_device_capability: int) -> bool:
|
||||
if cuda_device_capability < 90 or cuda_device_capability >= 110:
|
||||
# CUTLASS grouped FP8 MoE uses the same sm100-named implementation for
|
||||
# SM10x and SM11x. Thor reports SM101 with CUDA 12 and SM110 with CUDA 13,
|
||||
# so keep this Python guard aligned with the C++ support query/dispatch.
|
||||
if cuda_device_capability < 90 or cuda_device_capability >= 120:
|
||||
return False
|
||||
try:
|
||||
return torch.ops._C.cutlass_group_gemm_supported(cuda_device_capability)
|
||||
except AttributeError:
|
||||
# Return False on non-CUDA platforms where it is not available
|
||||
except (AttributeError, RuntimeError):
|
||||
# Return False on non-CUDA platforms where it is not available or not
|
||||
# implemented for the current build.
|
||||
return False
|
||||
|
||||
|
||||
@@ -3863,10 +3867,10 @@ def fusedQuantizeMx(
|
||||
raise ValueError(f"invalid method {method!r}, must be 'quest' or 'abs_max'")
|
||||
|
||||
|
||||
if hasattr(torch.ops._qutlass_C, "fusedQuantizeNv"):
|
||||
if hasattr(torch.ops._qutlass_C, "fusedQuantizeNvAbsMax"):
|
||||
|
||||
@register_fake("_qutlass_C::fusedQuantizeNv")
|
||||
def _fake_fused_quantize_nv(
|
||||
@register_fake("_qutlass_C::fusedQuantizeNvAbsMax")
|
||||
def _fake_fused_quantize_nv_absmax(
|
||||
a: torch.Tensor,
|
||||
b: torch.Tensor,
|
||||
xh_e2m1: torch.Tensor,
|
||||
@@ -3892,7 +3896,9 @@ def fusedQuantizeNv(
|
||||
padded_rows, padded_cols, dtype=torch.float8_e4m3fn, device=a.device
|
||||
)
|
||||
|
||||
return torch.ops._qutlass_C.fusedQuantizeNv(a, b, xh_e2m1, xh_e4m3, global_scale)
|
||||
return torch.ops._qutlass_C.fusedQuantizeNvAbsMax(
|
||||
a, b, xh_e2m1, xh_e4m3, global_scale
|
||||
)
|
||||
|
||||
|
||||
def hadacore_transform(x: torch.Tensor, inplace: bool = True) -> torch.Tensor:
|
||||
|
||||
@@ -177,18 +177,6 @@ class CacheConfig:
|
||||
gpu_memory_utilization. Note that kv_cache_memory_bytes
|
||||
(when not-None) ignores gpu_memory_utilization"""
|
||||
|
||||
enable_extensible_kv_cache: bool = False
|
||||
"""Use driver virtual memory to reserve the KV cache address range up
|
||||
front, run warmup and CUDA graph capture with only a small block prefix
|
||||
physically committed, and commit the final size afterwards.
|
||||
|
||||
This makes automatic KV sizing account for the memory that warmup and
|
||||
CUDA graph capture actually consume (including worst-case activation
|
||||
working sets, e.g. with speculative decoding), and avoids warmup-time
|
||||
OOMs. Requires driver VMM support (CUDA or ROCm; falls back to standard
|
||||
allocation with a warning where unavailable, e.g. WSL2).
|
||||
"""
|
||||
|
||||
kv_offloading_size: float | None = None
|
||||
"""Size of the KV cache offloading buffer in GiB. When TP > 1, this is
|
||||
the total buffer size summed across all TP ranks. By default, this is set
|
||||
@@ -234,8 +222,6 @@ class CacheConfig:
|
||||
"kv_cache_max_concurrency",
|
||||
# WIP feature toggle not impacting compiled graph shape
|
||||
"kv_sharing_fast_prefill",
|
||||
# Runtime memory allocation strategy, not graph structure.
|
||||
"enable_extensible_kv_cache",
|
||||
}
|
||||
|
||||
from vllm.config.utils import get_hash_factors, hash_factors
|
||||
|
||||
@@ -175,9 +175,15 @@ class KernelConfig:
|
||||
enable_flashinfer_autotune: bool = None # type: ignore[assignment]
|
||||
"""If True, run FlashInfer autotuning during kernel warmup."""
|
||||
|
||||
# TODO(roberto): Remove after registered CuTeDSL warmups are migrated
|
||||
# to the shared JIT warmup infrastructure.
|
||||
# https://github.com/vllm-project/vllm/pull/47451
|
||||
enable_cutedsl_warmup: bool = True
|
||||
"""If True, run CuTeDSL compile warmup during kernel warmup."""
|
||||
|
||||
enable_jit_warmup: bool = True
|
||||
"""If True, run JIT compile warmup during kernel warmup."""
|
||||
|
||||
enable_bf16x3_router_gemm: bool = False
|
||||
"""If True, use the experimental SM100 BF16x3 CuteDSL router GEMM."""
|
||||
|
||||
@@ -250,6 +256,7 @@ class KernelConfig:
|
||||
"""
|
||||
ignored_factors = {
|
||||
"enable_cutedsl_warmup",
|
||||
"enable_jit_warmup",
|
||||
"enable_flashinfer_autotune",
|
||||
"ir_op_priority", # handled separately below
|
||||
}
|
||||
@@ -260,6 +267,7 @@ class KernelConfig:
|
||||
@field_validator(
|
||||
"enable_flashinfer_autotune",
|
||||
"enable_cutedsl_warmup",
|
||||
"enable_jit_warmup",
|
||||
mode="wrap",
|
||||
)
|
||||
@classmethod
|
||||
|
||||
@@ -255,14 +255,6 @@ class KVConnectorBase_V1(ABC):
|
||||
|
||||
Args:
|
||||
kv_caches: dictionary of layer names, kv cache
|
||||
|
||||
Note:
|
||||
The views' shapes/strides/numel are the authoritative source of
|
||||
the KV cache geometry; do not derive block sizes or extents from
|
||||
`untyped_storage().nbytes()`. With the extensible KV cache, the
|
||||
underlying storage spans the reserved virtual-address capacity,
|
||||
of which only each view's per-segment block prefix is physically
|
||||
committed (and safe to access or register).
|
||||
"""
|
||||
return
|
||||
|
||||
|
||||
@@ -61,9 +61,9 @@ def get_offloading_event_group_spec(
|
||||
|
||||
@dataclass(slots=True)
|
||||
class _OffloadEventMetadata:
|
||||
"""BlockStored payload snapshot for one OffloadKey, captured at store
|
||||
time and kept until the matching eviction event. ``medium`` is forwarded
|
||||
from the OffloadingEvent."""
|
||||
"""BlockStored payload snapshot for one OffloadKey, captured while the
|
||||
Request is available and kept until the matching eviction event. ``medium``
|
||||
is forwarded from the OffloadingEvent."""
|
||||
|
||||
# The chunk's constituent block hashes; the last one is the OffloadKey.
|
||||
block_hashes: tuple[BlockHash, ...]
|
||||
@@ -81,10 +81,11 @@ class _OffloadEventMetadata:
|
||||
class OffloadingEventsTracker:
|
||||
"""Tracks offloaded chunks' KV event payloads from store to eviction.
|
||||
|
||||
The scheduler calls :meth:`record_store` from ``_build_store_jobs``
|
||||
while the ``Request`` is available, and routes the manager's raw
|
||||
:class:`OffloadingEvent` stream through :meth:`take_events`. All state
|
||||
is bounded by the CPU pool capacity and cleared by :meth:`reset`.
|
||||
The scheduler calls :meth:`record_store` from ``_build_store_jobs`` and
|
||||
:meth:`record_lookup` for ready primary-tier hits while the ``Request`` is
|
||||
available. Deferred and missing lookups add no state. Under the connector's
|
||||
supported success-only transfer model, entries follow primary allocations
|
||||
until CPU removal translation or :meth:`reset`.
|
||||
"""
|
||||
|
||||
def __init__(self, config: OffloadingKVEventsConfig):
|
||||
@@ -93,8 +94,7 @@ class OffloadingEventsTracker:
|
||||
config.enable_kv_cache_events and config.self_describing_kv_events
|
||||
)
|
||||
|
||||
# OffloadKey -> payload snapshot, kept until the eviction event so
|
||||
# BlockRemoved can fan out. Bounded: one entry per offloaded chunk.
|
||||
# OffloadKey -> payload snapshot, kept until CPU removal or reset.
|
||||
self._pending_event_metadata: dict[OffloadKey, _OffloadEventMetadata] = {}
|
||||
|
||||
def record_store(
|
||||
@@ -116,6 +116,23 @@ class OffloadingEventsTracker:
|
||||
meta = self._build_event_metadata(req, group_config, chunk_idx)
|
||||
self._pending_event_metadata[offload_key] = meta
|
||||
|
||||
def record_lookup(
|
||||
self,
|
||||
req: Request,
|
||||
group_config: "GroupOffloadConfig",
|
||||
chunk_idx: int,
|
||||
offload_key: OffloadKey,
|
||||
) -> None:
|
||||
"""Snapshot metadata for a ready primary-tier lookup hit."""
|
||||
if not self.self_describing_enabled:
|
||||
return
|
||||
if group_config.sliding_window_size_in_chunks is not None:
|
||||
return
|
||||
if offload_key not in self._pending_event_metadata:
|
||||
self._pending_event_metadata[offload_key] = self._build_event_metadata(
|
||||
req, group_config, chunk_idx
|
||||
)
|
||||
|
||||
def take_events(self, events: Iterable[OffloadingEvent]) -> Iterable[KVCacheEvent]:
|
||||
"""Translate raw OffloadingEvents into self-describing KV events.
|
||||
|
||||
@@ -165,7 +182,7 @@ class OffloadingEventsTracker:
|
||||
assert len(chunk_hashes) == hbf
|
||||
|
||||
if group_config.sliding_window_size_in_chunks is not None:
|
||||
# record_store filters these out before calling this helper.
|
||||
# The recording methods filter these out before calling this helper.
|
||||
raise AssertionError("self-describing events only support full attention")
|
||||
|
||||
parent_block_hash: BlockHash | None
|
||||
@@ -232,7 +249,8 @@ class OffloadingEventsTracker:
|
||||
"OffloadingEventsTracker: no event metadata for "
|
||||
"offload key during BlockStored emission; emitting a "
|
||||
"placeholder payload. Expected for non-full-attention "
|
||||
"groups; otherwise indicates a missing populate path."
|
||||
"groups and promotions not observed as a primary-tier "
|
||||
"hit before translation."
|
||||
)
|
||||
yield self._placeholder_stored(key, event.medium, locality)
|
||||
continue
|
||||
|
||||
@@ -111,6 +111,26 @@ def get_sliding_window_size_in_chunks(
|
||||
return None
|
||||
|
||||
|
||||
def is_store_reachable_swa_chunk(
|
||||
absolute_chunk_index: int,
|
||||
storable_chunk_count: int,
|
||||
alignment_chunk_count: int | None,
|
||||
sliding_window_chunks: int | None,
|
||||
is_eagle_group: bool,
|
||||
) -> bool:
|
||||
"""Return whether an SWA chunk can participate in an external-cache hit."""
|
||||
if alignment_chunk_count is None:
|
||||
return True
|
||||
assert sliding_window_chunks is not None
|
||||
position_in_segment = absolute_chunk_index % alignment_chunk_count
|
||||
segment_start = absolute_chunk_index - position_in_segment
|
||||
actual_segment_length = min(
|
||||
alignment_chunk_count, storable_chunk_count - segment_start
|
||||
)
|
||||
reachable_tail = sliding_window_chunks + int(is_eagle_group)
|
||||
return position_in_segment >= actual_segment_length - reachable_tail
|
||||
|
||||
|
||||
def resolve_mamba_align_size(
|
||||
spec: "OffloadingSpec", kv_cache_config: KVCacheConfig
|
||||
) -> int | None:
|
||||
@@ -463,15 +483,27 @@ class OffloadingConnectorScheduler:
|
||||
del self._req_status[req_id]
|
||||
|
||||
def _maximal_prefix_lookup(
|
||||
self, keys: Iterable[OffloadKey], req_context: ReqContext
|
||||
self,
|
||||
keys: Iterable[OffloadKey],
|
||||
req_context: ReqContext,
|
||||
req: Request,
|
||||
group_config: GroupOffloadConfig,
|
||||
start_chunk_idx: int,
|
||||
) -> int | None:
|
||||
"""Return the number of consecutive offloaded chunks from the start,
|
||||
or None if the backend deferred a lookup."""
|
||||
hit_count = 0
|
||||
defer_lookup = False
|
||||
for key in keys:
|
||||
match self.manager.lookup(key, req_context):
|
||||
for local_idx, key in enumerate(keys):
|
||||
result = self.manager.lookup(key, req_context)
|
||||
match result:
|
||||
case LookupResult.HIT:
|
||||
self._events_tracker.record_lookup(
|
||||
req,
|
||||
group_config,
|
||||
start_chunk_idx + local_idx,
|
||||
key,
|
||||
)
|
||||
hit_count += 1
|
||||
case LookupResult.HIT_PENDING:
|
||||
defer_lookup = True
|
||||
@@ -616,7 +648,11 @@ class OffloadingConnectorScheduler:
|
||||
num_hit_chunks: int | None
|
||||
if sliding_window_size_in_chunks is None:
|
||||
num_hit_chunks = self._maximal_prefix_lookup(
|
||||
offload_keys, req_status.req_context
|
||||
offload_keys,
|
||||
req_status.req_context,
|
||||
req_status.req,
|
||||
group_config,
|
||||
start_chunk_idx,
|
||||
)
|
||||
else:
|
||||
required_window = sliding_window_size_in_chunks
|
||||
@@ -974,9 +1010,6 @@ class OffloadingConnectorScheduler:
|
||||
]
|
||||
assert len(offload_keys) == len(offload_block_ids)
|
||||
|
||||
alignment_chunk_count = group_config.alignment_chunk_count
|
||||
tail = group_config.sliding_window_size_in_chunks
|
||||
|
||||
for key_idx, (offload_key, block_id) in enumerate(
|
||||
zip(offload_keys, offload_block_ids)
|
||||
):
|
||||
@@ -984,15 +1017,18 @@ class OffloadingConnectorScheduler:
|
||||
continue
|
||||
# Skip SWA chunks that can never serve a load hit:
|
||||
# within each full-attention alignment segment, only the
|
||||
# trailing `tail` chunks are reachable by
|
||||
# _sliding_window_lookup. For DeepSeek V4 with 100K
|
||||
# tokens this reduces SWA stores by ~78%.
|
||||
if alignment_chunk_count is not None:
|
||||
assert tail is not None
|
||||
abs_chunk_idx = start_chunk_idx + key_idx
|
||||
pos_in_segment = abs_chunk_idx % alignment_chunk_count
|
||||
if pos_in_segment < alignment_chunk_count - tail:
|
||||
continue
|
||||
# trailing chunks queried by _sliding_window_lookup are
|
||||
# reachable. EAGLE/MTP requires one additional chunk that
|
||||
# lookup later drops as its volatile draft tail.
|
||||
abs_chunk_idx = start_chunk_idx + key_idx
|
||||
if not is_store_reachable_swa_chunk(
|
||||
abs_chunk_idx,
|
||||
num_chunks,
|
||||
group_config.alignment_chunk_count,
|
||||
group_config.sliding_window_size_in_chunks,
|
||||
group_config.is_eagle_group,
|
||||
):
|
||||
continue
|
||||
new_offload_keys.append(offload_key)
|
||||
|
||||
if not new_offload_keys:
|
||||
|
||||
@@ -525,7 +525,6 @@ class EngineArgs:
|
||||
offload_params: set[str] = get_field(PrefetchOffloadConfig, "offload_params")
|
||||
gpu_memory_utilization: float = CacheConfig.gpu_memory_utilization
|
||||
kv_cache_memory_bytes: int | None = CacheConfig.kv_cache_memory_bytes
|
||||
enable_extensible_kv_cache: bool = CacheConfig.enable_extensible_kv_cache
|
||||
max_num_batched_tokens: int | None = None
|
||||
max_num_scheduled_tokens: int | None = None
|
||||
max_num_partial_prefills: int = SchedulerConfig.max_num_partial_prefills
|
||||
@@ -1166,10 +1165,6 @@ class EngineArgs:
|
||||
cache_group.add_argument(
|
||||
"--kv-cache-memory-bytes", **cache_kwargs["kv_cache_memory_bytes"]
|
||||
)
|
||||
cache_group.add_argument(
|
||||
"--enable-extensible-kv-cache",
|
||||
**cache_kwargs["enable_extensible_kv_cache"],
|
||||
)
|
||||
cache_group.add_argument("--kv-cache-dtype", **cache_kwargs["cache_dtype"])
|
||||
cache_group.add_argument(
|
||||
"--num-gpu-blocks-override", **cache_kwargs["num_gpu_blocks_override"]
|
||||
@@ -1910,7 +1905,6 @@ class EngineArgs:
|
||||
block_size=self.block_size, # type: ignore[arg-type]
|
||||
gpu_memory_utilization=self.gpu_memory_utilization,
|
||||
kv_cache_memory_bytes=self.kv_cache_memory_bytes,
|
||||
enable_extensible_kv_cache=self.enable_extensible_kv_cache,
|
||||
cache_dtype=resolved_cache_dtype, # type: ignore[arg-type]
|
||||
is_attention_free=model_config.is_attention_free,
|
||||
num_gpu_blocks_override=self.num_gpu_blocks_override,
|
||||
|
||||
@@ -119,11 +119,6 @@ class LLM(BeamSearchOfflineMixin, PoolingOfflineMixin, OfflineInferenceMixin):
|
||||
compared with using gpu_memory_utilization. Note that
|
||||
kv_cache_memory_bytes (when not-None) ignores
|
||||
gpu_memory_utilization
|
||||
enable_extensible_kv_cache: Use CUDA virtual memory to reserve the KV
|
||||
cache address range before CUDA graph capture and commit the final
|
||||
cache size after capture. Supported by V1 CUDA workers for all
|
||||
attention backends (block-major and K/V-split KV cache layouts)
|
||||
and for Mamba / linear-attention models.
|
||||
cpu_offload_gb: The size (GiB) of CPU memory to use for offloading
|
||||
the model weights. This virtually increases the GPU memory space
|
||||
you can use to hold the model weights, at the cost of CPU-GPU data
|
||||
@@ -216,7 +211,6 @@ class LLM(BeamSearchOfflineMixin, PoolingOfflineMixin, OfflineInferenceMixin):
|
||||
profiler_config: dict[str, Any] | ProfilerConfig | None = None,
|
||||
attention_config: dict[str, Any] | AttentionConfig | None = None,
|
||||
kv_cache_memory_bytes: int | None = None,
|
||||
enable_extensible_kv_cache: bool = False,
|
||||
compilation_config: int | dict[str, Any] | CompilationConfig | None = None,
|
||||
quantization_config: dict[str, Any] | QuantizationConfigArgs | None = None,
|
||||
logits_processors: list[str | type[LogitsProcessor]] | None = None,
|
||||
@@ -315,7 +309,6 @@ class LLM(BeamSearchOfflineMixin, PoolingOfflineMixin, OfflineInferenceMixin):
|
||||
seed=seed,
|
||||
gpu_memory_utilization=gpu_memory_utilization,
|
||||
kv_cache_memory_bytes=kv_cache_memory_bytes,
|
||||
enable_extensible_kv_cache=enable_extensible_kv_cache,
|
||||
cpu_offload_gb=cpu_offload_gb,
|
||||
offload_group_size=offload_group_size,
|
||||
offload_num_in_group=offload_num_in_group,
|
||||
|
||||
@@ -325,13 +325,12 @@ def log_version_and_model(lgr: Logger, version: str, model_name: str) -> None:
|
||||
message = "vLLM server version %s, serving model %s"
|
||||
else:
|
||||
logo_template = Template(
|
||||
"\n ${w}█ █ █▄ ▄█${r}\n"
|
||||
" ${o}▄▄${r} ${b}▄█${r} ${w}█ █ █ ▀▄▀ █${r} version ${w}%s${r}\n"
|
||||
" ${o}█${r}${b}▄█▀${r} ${w}█ █ █ █${r} model ${w}%s${r}\n"
|
||||
" ${b}▀▀${r} ${w}▀▀▀▀▀ ▀▀▀▀▀ ▀ ▀${r}\n"
|
||||
"\n ${b}█ █ █▄ ▄█${r}\n"
|
||||
" ${o}▄▄${r} ${b}▄█${r} ${b}█ █ █ ▀▄▀ █${r} version ${b}%s${r}\n"
|
||||
" ${o}█${r}${b}▄█▀${r} ${b}█ █ █ █${r} model ${b}%s${r}\n"
|
||||
" ${b}▀▀${r} ${b}▀▀▀▀▀ ▀▀▀▀▀ ▀ ▀${r}\n"
|
||||
)
|
||||
colors = {
|
||||
"w": "\033[97;1m", # white
|
||||
"o": "\033[93m", # orange
|
||||
"b": "\033[94m", # blue
|
||||
"r": "\033[0m", # reset
|
||||
|
||||
@@ -186,6 +186,7 @@ if TYPE_CHECKING:
|
||||
VLLM_MOE_USE_DEEP_GEMM: bool = True
|
||||
VLLM_USE_DEEP_GEMM_E8M0: bool = True
|
||||
VLLM_USE_DEEP_GEMM_TMA_ALIGNED_SCALES: bool = True
|
||||
VLLM_DCP_Q_REPLICATE: bool = False
|
||||
VLLM_DEEP_GEMM_WARMUP: Literal[
|
||||
"skip",
|
||||
"full",
|
||||
@@ -1490,6 +1491,8 @@ environment_variables: dict[str, Callable[[], Any]] = {
|
||||
"VLLM_USE_DEEP_GEMM_TMA_ALIGNED_SCALES": lambda: bool(
|
||||
int(os.getenv("VLLM_USE_DEEP_GEMM_TMA_ALIGNED_SCALES", "1"))
|
||||
),
|
||||
# Opt-in MLA DCP query replication: skip the decode query all-gather.
|
||||
"VLLM_DCP_Q_REPLICATE": lambda: bool(int(os.getenv("VLLM_DCP_Q_REPLICATE", "0"))),
|
||||
# DeepGemm JITs the kernels on-demand. The warmup attempts to make DeepGemm
|
||||
# JIT all the required kernels before model execution so there is no
|
||||
# JIT'ing in the hot-path. However, this warmup increases the engine
|
||||
|
||||
@@ -366,6 +366,7 @@ class MLAAttention(nn.Module, AttentionLayerBase):
|
||||
q_lora_rank: int | None,
|
||||
kv_lora_rank: int,
|
||||
kv_b_proj: ColumnParallelLinear,
|
||||
dcp_q_replicate: bool = False,
|
||||
cache_config: CacheConfig | None = None,
|
||||
quant_config: QuantizationConfig | None = None,
|
||||
prefix: str = "",
|
||||
@@ -384,10 +385,11 @@ class MLAAttention(nn.Module, AttentionLayerBase):
|
||||
self.q_lora_rank = q_lora_rank
|
||||
self.kv_lora_rank = kv_lora_rank
|
||||
self.kv_b_proj = kv_b_proj
|
||||
self.dcp_q_replicate = dcp_q_replicate
|
||||
self.W_UK_T_dcp_qrep: torch.Tensor | None = None
|
||||
self.head_size = kv_lora_rank + qk_rope_head_dim
|
||||
self.layer_name = prefix
|
||||
self.indexer = indexer
|
||||
|
||||
self.num_kv_heads = 1
|
||||
self.qk_head_dim = self.qk_nope_head_dim + self.qk_rope_head_dim
|
||||
|
||||
@@ -591,6 +593,7 @@ class MLAAttention(nn.Module, AttentionLayerBase):
|
||||
kv_c_normed: torch.Tensor,
|
||||
k_pe: torch.Tensor,
|
||||
output_shape: torch.Size | None = None,
|
||||
q_dcp_replicated: torch.Tensor | None = None,
|
||||
) -> torch.Tensor:
|
||||
if self.calculate_kv_scales:
|
||||
torch.ops.vllm.maybe_calc_kv_scales(
|
||||
@@ -646,6 +649,7 @@ class MLAAttention(nn.Module, AttentionLayerBase):
|
||||
self_kv_cache,
|
||||
attn_metadata,
|
||||
output=output,
|
||||
q_dcp_replicated=q_dcp_replicated,
|
||||
)
|
||||
return output
|
||||
else:
|
||||
@@ -665,6 +669,7 @@ class MLAAttention(nn.Module, AttentionLayerBase):
|
||||
output,
|
||||
encoded,
|
||||
kv_cache_dummy_dep=kv_cache_dummy_dep,
|
||||
q_dcp_replicated=q_dcp_replicated,
|
||||
)
|
||||
return output
|
||||
|
||||
@@ -682,6 +687,7 @@ class MLAAttention(nn.Module, AttentionLayerBase):
|
||||
quant_scale_ue8m0: bool | None = None,
|
||||
quant_col_major: bool | None = None,
|
||||
quant_tma_aligned: bool | None = None,
|
||||
q_dcp_replicated: torch.Tensor | None = None,
|
||||
) -> torch.Tensor:
|
||||
assert output is not None, "Output tensor must be provided."
|
||||
|
||||
@@ -738,6 +744,8 @@ class MLAAttention(nn.Module, AttentionLayerBase):
|
||||
output_padded = output
|
||||
output = output[:num_actual_toks, ...]
|
||||
q = q[:num_actual_toks, ...]
|
||||
if q_dcp_replicated is not None:
|
||||
q_dcp_replicated = q_dcp_replicated[:num_actual_toks, ...]
|
||||
k_c_normed = k_c_normed[:num_actual_toks, ...]
|
||||
k_pe = k_pe[:num_actual_toks, ...]
|
||||
|
||||
@@ -793,7 +801,12 @@ class MLAAttention(nn.Module, AttentionLayerBase):
|
||||
)
|
||||
|
||||
if num_mqa_tokens > 0:
|
||||
mqa_q = q[:num_mqa_tokens]
|
||||
if q_dcp_replicated is not None:
|
||||
mqa_q = q_dcp_replicated[:num_mqa_tokens]
|
||||
qrep_decode = True
|
||||
else:
|
||||
mqa_q = q[:num_mqa_tokens]
|
||||
qrep_decode = False
|
||||
mqa_output_slice = output[:num_mqa_tokens]
|
||||
|
||||
mqa_q_nope, mqa_q_pe = mqa_q.split(
|
||||
@@ -833,7 +846,9 @@ class MLAAttention(nn.Module, AttentionLayerBase):
|
||||
else:
|
||||
# Pads the head_dim if necessary (for the underlying kernel)
|
||||
N, B, P = mqa_q_nope.shape
|
||||
_, _, L = self.W_UK_T.shape
|
||||
W_UK_T = self.W_UK_T_dcp_qrep if qrep_decode else self.W_UK_T
|
||||
assert W_UK_T is not None
|
||||
_, _, L = W_UK_T.shape
|
||||
|
||||
if self.q_pad_num_heads is not None:
|
||||
mqa_ql_nope = mqa_q_nope.new_empty((self.q_pad_num_heads, B, L))
|
||||
@@ -842,7 +857,7 @@ class MLAAttention(nn.Module, AttentionLayerBase):
|
||||
mqa_ql_nope = mqa_q_nope.new_empty((N, B, L))
|
||||
|
||||
# Multiply (N, B, P) x (N, P, L) -> (N, B, L)
|
||||
torch.bmm(mqa_q_nope, self.W_UK_T, out=mqa_ql_nope)
|
||||
torch.bmm(mqa_q_nope, W_UK_T, out=mqa_ql_nope)
|
||||
|
||||
# Convert from (N, B, L) to (B, N, L)
|
||||
mqa_ql_nope = mqa_ql_nope.transpose(0, 1)
|
||||
@@ -855,6 +870,7 @@ class MLAAttention(nn.Module, AttentionLayerBase):
|
||||
)
|
||||
else:
|
||||
mqa_q = (mqa_ql_nope, mqa_q_pe)
|
||||
# concatenate nope + pe -> (B, N, L + P) (fp8 op above may have fused)
|
||||
if self.impl.dcp_world_size > 1:
|
||||
if self.use_pcp:
|
||||
if self.impl.dcp_world_size > self.impl.pcp_world_size:
|
||||
@@ -863,8 +879,11 @@ class MLAAttention(nn.Module, AttentionLayerBase):
|
||||
mqa_q = get_tp_group().all_gather(mqa_q, dim=1)
|
||||
else:
|
||||
if isinstance(mqa_q, tuple):
|
||||
# concatenate mqa_ql_nope and mqa_q_pe -> (B, N, L + P)
|
||||
mqa_q = torch.cat(mqa_q, dim=-1)
|
||||
mqa_q = get_dcp_group().all_gather(mqa_q, dim=1)
|
||||
if not qrep_decode:
|
||||
# mqa_q do allgather in head dim.
|
||||
mqa_q = get_dcp_group().all_gather(mqa_q, dim=1)
|
||||
|
||||
# call decode attn
|
||||
if not self.impl.is_sparse:
|
||||
@@ -952,6 +971,21 @@ class MLAAttention(nn.Module, AttentionLayerBase):
|
||||
self.kv_b_proj, out_dtype=act_dtype
|
||||
).T
|
||||
|
||||
if self.dcp_q_replicate:
|
||||
# qrep wired here: validate unsupported decode backends once.
|
||||
assert self.q_pad_num_heads in (None, self.num_heads), (
|
||||
"DCP query replication is unsupported on head-padding MLA "
|
||||
"backends (q_pad_num_heads)."
|
||||
)
|
||||
if (
|
||||
self.is_aiter_triton_fp4_bmm_enabled
|
||||
or self.is_aiter_triton_fp8_bmm_enabled
|
||||
):
|
||||
raise NotImplementedError(
|
||||
"DCP query replication is not implemented for the aiter "
|
||||
"FP4/FP8 MLA BMM paths."
|
||||
)
|
||||
|
||||
assert kv_b_proj_weight.shape == (
|
||||
self.kv_lora_rank,
|
||||
self.num_heads * (self.qk_nope_head_dim + self.v_head_dim),
|
||||
@@ -1032,6 +1066,10 @@ class MLAAttention(nn.Module, AttentionLayerBase):
|
||||
replace_parameter(self, "W_UV", W_UV.transpose(0, 1), prefer_copy=True)
|
||||
# Convert from (L, N, P) to (N, P, L)
|
||||
replace_parameter(self, "W_UK_T", W_UK.permute(1, 2, 0), prefer_copy=True)
|
||||
if self.dcp_q_replicate:
|
||||
self.W_UK_T_dcp_qrep = get_dcp_group().all_gather(
|
||||
self.W_UK_T.contiguous(), dim=0
|
||||
)
|
||||
|
||||
# If we should not load quant weights, we initialize the scales to 1.0
|
||||
# as the default value. See [Note: Register q/k/v/prob scales in state dict]
|
||||
@@ -1176,6 +1214,7 @@ def unified_mla_attention_with_output(
|
||||
quant_scale_ue8m0: bool | None = None,
|
||||
quant_col_major: bool | None = None,
|
||||
quant_tma_aligned: bool | None = None,
|
||||
q_dcp_replicated: torch.Tensor | None = None,
|
||||
) -> None:
|
||||
# kv_cache_dummy_dep is not used but accepting it creates a data dependency
|
||||
# that ensures torch.compile preserves ordering between KV cache update and
|
||||
@@ -1196,6 +1235,7 @@ def unified_mla_attention_with_output(
|
||||
quant_scale_ue8m0=quant_scale_ue8m0,
|
||||
quant_col_major=quant_col_major,
|
||||
quant_tma_aligned=quant_tma_aligned,
|
||||
q_dcp_replicated=q_dcp_replicated,
|
||||
)
|
||||
|
||||
|
||||
@@ -1212,6 +1252,7 @@ def unified_mla_attention_with_output_fake(
|
||||
quant_scale_ue8m0: bool | None = None,
|
||||
quant_col_major: bool | None = None,
|
||||
quant_tma_aligned: bool | None = None,
|
||||
q_dcp_replicated: torch.Tensor | None = None,
|
||||
) -> None:
|
||||
return
|
||||
|
||||
|
||||
@@ -34,6 +34,9 @@ class TrtLlmBf16ExpertsBase:
|
||||
monolithic interfaces.
|
||||
"""
|
||||
|
||||
def supports_routing_replay_capture(self) -> bool:
|
||||
return True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
moe_config: FusedMoEConfig,
|
||||
@@ -246,7 +249,11 @@ class TrtLlmBf16ExpertsMonolithic(TrtLlmBf16ExpertsBase, mk.FusedMoEExpertsMonol
|
||||
|
||||
assert activation in [MoEActivation.SILU, MoEActivation.RELU2_NO_MUL]
|
||||
|
||||
return flashinfer.fused_moe.trtllm_bf16_moe(
|
||||
routing_replay_out = self._maybe_make_routing_replay_buffer(
|
||||
num_tokens=hidden_states.shape[0],
|
||||
device=hidden_states.device,
|
||||
)
|
||||
out = flashinfer.fused_moe.trtllm_bf16_moe(
|
||||
routing_logits=router_logits,
|
||||
routing_bias=e_score_correction_bias,
|
||||
hidden_states=hidden_states,
|
||||
@@ -263,4 +270,9 @@ class TrtLlmBf16ExpertsMonolithic(TrtLlmBf16ExpertsBase, mk.FusedMoEExpertsMonol
|
||||
routing_method_type=self.routing_method_type,
|
||||
activation_type=activation_to_flashinfer_int(activation),
|
||||
tune_max_num_tokens=fi_moe_largest_bucket(self.moe_config),
|
||||
routing_replay_out=routing_replay_out,
|
||||
)
|
||||
self._maybe_dispatch_routing_replay(
|
||||
routing_replay_out, num_tokens=hidden_states.shape[0]
|
||||
)
|
||||
return out
|
||||
|
||||
@@ -265,6 +265,9 @@ class TrtLlmFp8ExpertsMonolithic(TrtLlmFp8ExpertsBase, mk.FusedMoEExpertsMonolit
|
||||
Fp8 TRTLLM-Gen MoE kernels. Supports monolithic interface.
|
||||
"""
|
||||
|
||||
def supports_routing_replay_capture(self) -> bool:
|
||||
return True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
moe_config: FusedMoEConfig,
|
||||
@@ -403,6 +406,11 @@ class TrtLlmFp8ExpertsMonolithic(TrtLlmFp8ExpertsBase, mk.FusedMoEExpertsMonolit
|
||||
n_group = num_expert_group or 0
|
||||
selected_topk_group = topk_group or 0
|
||||
|
||||
routing_replay_out = self._maybe_make_routing_replay_buffer(
|
||||
num_tokens=hidden_states.shape[0],
|
||||
device=hidden_states.device,
|
||||
)
|
||||
|
||||
kwargs = dict(
|
||||
routing_logits=router_logits,
|
||||
routing_bias=e_score_correction_bias,
|
||||
@@ -427,11 +435,16 @@ class TrtLlmFp8ExpertsMonolithic(TrtLlmFp8ExpertsBase, mk.FusedMoEExpertsMonolit
|
||||
use_shuffled_weight=use_shuffled_weight,
|
||||
weight_layout=weight_layout,
|
||||
fp8_quantization_type=fp8_quant_type,
|
||||
routing_replay_out=routing_replay_out,
|
||||
tune_max_num_tokens=fi_moe_largest_bucket(self.moe_config),
|
||||
)
|
||||
if is_mxfp8 or activation == MoEActivation.RELU2_NO_MUL:
|
||||
kwargs["activation_type"] = activation_type
|
||||
return flashinfer.fused_moe.trtllm_fp8_block_scale_moe(**kwargs)
|
||||
result = flashinfer.fused_moe.trtllm_fp8_block_scale_moe(**kwargs)
|
||||
self._maybe_dispatch_routing_replay(
|
||||
routing_replay_out, num_tokens=hidden_states.shape[0]
|
||||
)
|
||||
return result
|
||||
|
||||
def _apply_per_tensor(
|
||||
self,
|
||||
@@ -464,6 +477,11 @@ class TrtLlmFp8ExpertsMonolithic(TrtLlmFp8ExpertsBase, mk.FusedMoEExpertsMonolit
|
||||
else:
|
||||
assert not apply_router_weight_on_input
|
||||
|
||||
routing_replay_out = self._maybe_make_routing_replay_buffer(
|
||||
num_tokens=hidden_states.shape[0],
|
||||
device=hidden_states.device,
|
||||
)
|
||||
|
||||
out = flashinfer.fused_moe.trtllm_fp8_per_tensor_scale_moe(
|
||||
routing_logits=router_logits,
|
||||
routing_bias=e_score_correction_bias,
|
||||
@@ -485,6 +503,10 @@ class TrtLlmFp8ExpertsMonolithic(TrtLlmFp8ExpertsBase, mk.FusedMoEExpertsMonolit
|
||||
routing_method_type=self.routing_method_type,
|
||||
activation_type=activation_type,
|
||||
tune_max_num_tokens=fi_moe_largest_bucket(self.moe_config),
|
||||
routing_replay_out=routing_replay_out,
|
||||
)
|
||||
self._maybe_dispatch_routing_replay(
|
||||
routing_replay_out, num_tokens=hidden_states.shape[0]
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
@@ -122,6 +122,9 @@ class TrtLlmMxfp4ExpertsMonolithic(
|
||||
Wraps flashinfer.trtllm_fp4_block_scale_moe().
|
||||
"""
|
||||
|
||||
def supports_routing_replay_capture(self) -> bool:
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def _supports_parallel_config(
|
||||
moe_parallel_config: FusedMoEParallelConfig,
|
||||
@@ -184,6 +187,10 @@ class TrtLlmMxfp4ExpertsMonolithic(
|
||||
device=hidden_states.device,
|
||||
)
|
||||
|
||||
routing_replay_out = self._maybe_make_routing_replay_buffer(
|
||||
num_tokens=hidden_states.shape[0],
|
||||
device=hidden_states.device,
|
||||
)
|
||||
trtllm_fp4_block_scale_moe(
|
||||
routing_logits=router_logits.to(torch.bfloat16),
|
||||
routing_bias=None,
|
||||
@@ -213,8 +220,11 @@ class TrtLlmMxfp4ExpertsMonolithic(
|
||||
do_finalize=True,
|
||||
tune_max_num_tokens=max(self.max_capture_size, 1),
|
||||
output=output,
|
||||
routing_replay_out=routing_replay_out,
|
||||
)
|
||||
self._maybe_dispatch_routing_replay(
|
||||
routing_replay_out, num_tokens=hidden_states.shape[0]
|
||||
)
|
||||
|
||||
return output
|
||||
|
||||
|
||||
|
||||
@@ -122,6 +122,9 @@ class TrtLlmMxint4ExpertsMonolithic(mk.FusedMoEExpertsMonolithic):
|
||||
# The kernel handles quantization internally.
|
||||
return True
|
||||
|
||||
def supports_routing_replay_capture(self) -> bool:
|
||||
return True
|
||||
|
||||
def apply(
|
||||
self,
|
||||
hidden_states: torch.Tensor,
|
||||
@@ -144,7 +147,12 @@ class TrtLlmMxint4ExpertsMonolithic(mk.FusedMoEExpertsMonolithic):
|
||||
|
||||
assert self.w1_scale is not None
|
||||
assert self.w2_scale is not None
|
||||
return flashinfer_trtllm_mxint4_moe(
|
||||
|
||||
routing_replay_out = self._maybe_make_routing_replay_buffer(
|
||||
num_tokens=hidden_states.shape[0],
|
||||
device=hidden_states.device,
|
||||
)
|
||||
result = flashinfer_trtllm_mxint4_moe(
|
||||
x=hidden_states,
|
||||
router_logits=router_logits,
|
||||
w13_weight_packed=w1,
|
||||
@@ -160,4 +168,9 @@ class TrtLlmMxint4ExpertsMonolithic(mk.FusedMoEExpertsMonolithic):
|
||||
topk_group=topk_group,
|
||||
e_score_correction_bias=e_score_correction_bias,
|
||||
routing_method_type=self.routing_method,
|
||||
routing_replay_out=routing_replay_out,
|
||||
)
|
||||
self._maybe_dispatch_routing_replay(
|
||||
routing_replay_out, num_tokens=hidden_states.shape[0]
|
||||
)
|
||||
return result
|
||||
|
||||
@@ -435,6 +435,9 @@ class TrtLlmNvFp4ExpertsMonolithic(
|
||||
Monolithic version of the kernel (router + experts).
|
||||
"""
|
||||
|
||||
def supports_routing_replay_capture(self) -> bool:
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def _supports_parallel_config(moe_parallel_config: FusedMoEParallelConfig) -> bool:
|
||||
"""The modular implementation should be used for the Dp/Ep or EPLB case."""
|
||||
@@ -509,10 +512,14 @@ class TrtLlmNvFp4ExpertsMonolithic(
|
||||
|
||||
output1_scale_gate_scalar = self.quant_config.g1_alphas
|
||||
|
||||
routing_replay_out = self._maybe_make_routing_replay_buffer(
|
||||
num_tokens=hidden_states.shape[0],
|
||||
device=hidden_states.device,
|
||||
)
|
||||
# Invoke kernel.
|
||||
# NOTE: Activation padding and output
|
||||
# truncation are handled by the MoE runner's
|
||||
return flashinfer.fused_moe.trtllm_fp4_block_scale_moe(
|
||||
result = flashinfer.fused_moe.trtllm_fp4_block_scale_moe(
|
||||
routing_logits=router_logits,
|
||||
routing_bias=e_score_correction_bias,
|
||||
hidden_states=hidden_states,
|
||||
@@ -544,4 +551,9 @@ class TrtLlmNvFp4ExpertsMonolithic(
|
||||
activation_type=activation_to_flashinfer_int(activation),
|
||||
per_token_scale=per_token_scale,
|
||||
tune_max_num_tokens=fi_moe_largest_bucket(self.moe_config),
|
||||
routing_replay_out=routing_replay_out,
|
||||
)[0]
|
||||
self._maybe_dispatch_routing_replay(
|
||||
routing_replay_out, num_tokens=hidden_states.shape[0]
|
||||
)
|
||||
return result
|
||||
|
||||
@@ -1000,6 +1000,57 @@ class FusedMoEExpertsMonolithic(FusedMoEExperts):
|
||||
def is_monolithic() -> bool:
|
||||
return True
|
||||
|
||||
routing_replay_capture_fn: Callable[[torch.Tensor], None] | None = None
|
||||
_routing_replay_buffer: torch.Tensor | None = None
|
||||
|
||||
def supports_routing_replay_capture(self) -> bool:
|
||||
"""Whether this expert supports routing replay capture.
|
||||
|
||||
Subclasses backed by a kernel that exposes routed expert IDs
|
||||
(e.g. FlashInfer's ``routing_replay_out``) should override.
|
||||
"""
|
||||
return False
|
||||
|
||||
def set_capture_fn(
|
||||
self,
|
||||
capture_fn: Callable[[torch.Tensor], None] | None,
|
||||
) -> None:
|
||||
self.routing_replay_capture_fn = capture_fn
|
||||
if capture_fn is None:
|
||||
self._routing_replay_buffer = None
|
||||
return
|
||||
self._routing_replay_buffer = torch.empty(
|
||||
(self.moe_config.max_num_tokens, self.moe_config.experts_per_token),
|
||||
dtype=torch.int16,
|
||||
device=self.moe_config.device,
|
||||
)
|
||||
|
||||
def _maybe_make_routing_replay_buffer(
|
||||
self,
|
||||
num_tokens: int,
|
||||
device: torch.device,
|
||||
) -> torch.Tensor | None:
|
||||
if self.routing_replay_capture_fn is None:
|
||||
return None
|
||||
buf = self._routing_replay_buffer
|
||||
assert buf is not None
|
||||
if buf.shape[0] < num_tokens or buf.device != device:
|
||||
raise ValueError(
|
||||
"Routing replay buffer was initialized for "
|
||||
f"{buf.shape[0]} tokens on {buf.device}, but the kernel "
|
||||
f"received {num_tokens} tokens on {device}."
|
||||
)
|
||||
return buf
|
||||
|
||||
def _maybe_dispatch_routing_replay(
|
||||
self,
|
||||
routing_replay_out: torch.Tensor | None,
|
||||
num_tokens: int,
|
||||
) -> None:
|
||||
if routing_replay_out is None or self.routing_replay_capture_fn is None:
|
||||
return
|
||||
self.routing_replay_capture_fn(routing_replay_out[:num_tokens])
|
||||
|
||||
def apply(
|
||||
self,
|
||||
hidden_states: torch.Tensor,
|
||||
|
||||
@@ -90,10 +90,20 @@ class RoutedExpertsCapturer:
|
||||
) -> None:
|
||||
hf_config = vllm_config.model_config.hf_text_config
|
||||
num_experts_per_tok = _get_num_experts_per_tok(hf_config)
|
||||
num_layers = hf_config.num_hidden_layers
|
||||
logger.info(
|
||||
"RoutedExpertsCapturer: allocating buffer with "
|
||||
"max_tokens=%d, num_layers=%d, num_experts_per_tok=%d "
|
||||
"(hf_config.model_type=%s)",
|
||||
max_num_batched_tokens,
|
||||
num_layers,
|
||||
num_experts_per_tok,
|
||||
getattr(hf_config, "model_type", "unknown"),
|
||||
)
|
||||
self.device_buffer = torch.zeros(
|
||||
(
|
||||
max_num_batched_tokens,
|
||||
hf_config.num_hidden_layers,
|
||||
num_layers,
|
||||
num_experts_per_tok,
|
||||
),
|
||||
# Use int32 for the device / host transit buffers: it
|
||||
|
||||
@@ -11,6 +11,7 @@ from torch.nn.parameter import Parameter
|
||||
from typing_extensions import TypeIs
|
||||
|
||||
import vllm.envs as envs
|
||||
from vllm.config import get_current_vllm_config
|
||||
from vllm.distributed import (
|
||||
divide,
|
||||
get_tensor_model_parallel_rank,
|
||||
@@ -254,6 +255,8 @@ class LinearBase(PluggableLayer):
|
||||
*,
|
||||
return_bias: bool = True,
|
||||
disable_tp: bool = False,
|
||||
tp_rank: int | None = None,
|
||||
tp_size: int | None = None,
|
||||
):
|
||||
super().__init__()
|
||||
|
||||
@@ -277,8 +280,17 @@ class LinearBase(PluggableLayer):
|
||||
raise ValueError("All linear layers should support quant method.")
|
||||
self.return_bias = return_bias
|
||||
self.disable_tp = disable_tp
|
||||
self.tp_rank = get_tensor_model_parallel_rank() if not disable_tp else 0
|
||||
self.tp_size = get_tensor_model_parallel_world_size() if not disable_tp else 1
|
||||
if disable_tp:
|
||||
self.tp_rank, self.tp_size = 0, 1
|
||||
else:
|
||||
self.tp_rank = (
|
||||
tp_rank if tp_rank is not None else get_tensor_model_parallel_rank()
|
||||
)
|
||||
self.tp_size = (
|
||||
tp_size
|
||||
if tp_size is not None
|
||||
else get_tensor_model_parallel_world_size()
|
||||
)
|
||||
|
||||
def update_param_tp_status(self):
|
||||
# Single source of truth for a parameter's TP state. BasevLLMParameter
|
||||
@@ -425,6 +437,11 @@ class ColumnParallelLinear(LinearBase):
|
||||
(e.g. model.layers.0.qkv_proj)
|
||||
return_bias: If true, return bias together with outputs in forward pass.
|
||||
disable_tp: If true, weights matrix won't be sharded through tp rank.
|
||||
tp_rank: Override the tensor-parallel rank used for sharding. Defaults to
|
||||
the global TP rank. Used to shard at a coarser granularity than one
|
||||
shard per rank (see ``DCPGroupColumnParallelLinear``).
|
||||
tp_size: Override the tensor-parallel world size used for sharding.
|
||||
Defaults to the global TP world size.
|
||||
"""
|
||||
|
||||
# --8<-- [end:column_parallel_linear]
|
||||
@@ -442,10 +459,21 @@ class ColumnParallelLinear(LinearBase):
|
||||
*,
|
||||
return_bias: bool = True,
|
||||
disable_tp: bool = False,
|
||||
tp_rank: int | None = None,
|
||||
tp_size: int | None = None,
|
||||
):
|
||||
# Divide the weight matrix along the last dimension.
|
||||
self.tp_rank = get_tensor_model_parallel_rank() if not disable_tp else 0
|
||||
self.tp_size = get_tensor_model_parallel_world_size() if not disable_tp else 1
|
||||
if disable_tp:
|
||||
self.tp_rank, self.tp_size = 0, 1
|
||||
else:
|
||||
self.tp_rank = (
|
||||
tp_rank if tp_rank is not None else get_tensor_model_parallel_rank()
|
||||
)
|
||||
self.tp_size = (
|
||||
tp_size
|
||||
if tp_size is not None
|
||||
else get_tensor_model_parallel_world_size()
|
||||
)
|
||||
self.input_size_per_partition = input_size
|
||||
self.output_size_per_partition = divide(output_size, self.tp_size)
|
||||
self.output_partition_sizes = [self.output_size_per_partition]
|
||||
@@ -465,6 +493,8 @@ class ColumnParallelLinear(LinearBase):
|
||||
prefix,
|
||||
return_bias=return_bias,
|
||||
disable_tp=disable_tp,
|
||||
tp_rank=self.tp_rank,
|
||||
tp_size=self.tp_size,
|
||||
)
|
||||
|
||||
self._maybe_allow_fp8_block_shape_mismatch()
|
||||
@@ -586,6 +616,47 @@ class ColumnParallelLinear(LinearBase):
|
||||
return s
|
||||
|
||||
|
||||
class DCPGroupColumnParallelLinear(ColumnParallelLinear):
|
||||
"""Column-parallel linear whose weight is sharded across DCP groups.
|
||||
|
||||
With Decode Context Parallelism (DCP) the KV cache is sharded across a DCP
|
||||
group, so MLA decode must attend the group's full head set. This layer shards
|
||||
its output across DCP *groups* (effective tp size ``tp_size //
|
||||
dcp_world_size``) rather than across every rank, so each rank in a group
|
||||
holds the whole group's heads, letting decode skip the query all-gather.
|
||||
|
||||
:meth:`forward` returns the group's full head set. :meth:`_local_view`
|
||||
extracts this rank's TP shard for prefill.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
dcp_world_size = (
|
||||
get_current_vllm_config().parallel_config.decode_context_parallel_size
|
||||
)
|
||||
rank = get_tensor_model_parallel_rank()
|
||||
world_size = get_tensor_model_parallel_world_size()
|
||||
self.group_size = max(dcp_world_size, 1)
|
||||
self.qrep_active = self.group_size > 1
|
||||
self.rank_in_group = rank % self.group_size
|
||||
super().__init__(
|
||||
*args,
|
||||
**kwargs,
|
||||
tp_rank=rank // self.group_size,
|
||||
tp_size=world_size // self.group_size,
|
||||
)
|
||||
|
||||
def _local_view(self, out: torch.Tensor) -> torch.Tensor:
|
||||
"""Slice this rank's tp head shard from a group-heads output.
|
||||
|
||||
``out`` is head-shaped, i.e. ``(..., group_heads, head_dim)``.
|
||||
"""
|
||||
if self.group_size == 1:
|
||||
return out
|
||||
n = out.shape[-2] // self.group_size
|
||||
start = self.rank_in_group * n
|
||||
return out[..., start : start + n, :].contiguous()
|
||||
|
||||
|
||||
class MergedColumnParallelLinear(ColumnParallelLinear):
|
||||
"""Packed linear layers with column parallelism.
|
||||
|
||||
|
||||
@@ -175,7 +175,7 @@ class MHCPreOp(CustomOp):
|
||||
norm_weight: torch.Tensor | None = None,
|
||||
norm_eps: float = 0.0,
|
||||
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
||||
return self.forward_native(
|
||||
return torch.ops._xpu_C.mhc_pre(
|
||||
residual,
|
||||
fn,
|
||||
hc_scale,
|
||||
@@ -185,9 +185,6 @@ class MHCPreOp(CustomOp):
|
||||
hc_sinkhorn_eps,
|
||||
hc_post_mult_value,
|
||||
sinkhorn_repeat,
|
||||
n_splits,
|
||||
norm_weight,
|
||||
norm_eps,
|
||||
)
|
||||
|
||||
|
||||
@@ -260,7 +257,7 @@ class MHCPostOp(CustomOp):
|
||||
post_layer_mix: torch.Tensor,
|
||||
comb_res_mix: torch.Tensor,
|
||||
) -> torch.Tensor:
|
||||
return self.forward_native(
|
||||
return torch.ops._xpu_C.mhc_post(
|
||||
x,
|
||||
residual,
|
||||
post_layer_mix,
|
||||
@@ -369,16 +366,8 @@ class HCHeadOp(CustomOp):
|
||||
out = torch.empty(
|
||||
num_tokens, hidden_size, dtype=torch.bfloat16, device=hidden_states.device
|
||||
)
|
||||
torch.ops.vllm.hc_head_triton(
|
||||
hs_flat,
|
||||
hc_fn,
|
||||
hc_scale,
|
||||
hc_base,
|
||||
out,
|
||||
hidden_size,
|
||||
rms_norm_eps,
|
||||
hc_eps,
|
||||
hc_mult,
|
||||
torch.ops._xpu_C.hc_head_fused(
|
||||
hs_flat, hc_fn, hc_scale, hc_base, out, rms_norm_eps, hc_eps
|
||||
)
|
||||
return out.view(*outer_shape, hidden_size)
|
||||
|
||||
@@ -548,7 +537,7 @@ class MHCFusedPostPreOp(CustomOp):
|
||||
norm_weight: torch.Tensor | None = None,
|
||||
norm_eps: float = 0.0,
|
||||
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
|
||||
return self.forward_native(
|
||||
return torch.ops._xpu_C.mhc_fused_post_pre(
|
||||
x,
|
||||
residual,
|
||||
post_layer_mix,
|
||||
@@ -561,8 +550,4 @@ class MHCFusedPostPreOp(CustomOp):
|
||||
hc_sinkhorn_eps,
|
||||
hc_post_mult_value,
|
||||
sinkhorn_repeat,
|
||||
n_splits,
|
||||
tile_n,
|
||||
norm_weight,
|
||||
norm_eps,
|
||||
)
|
||||
|
||||
@@ -93,6 +93,10 @@ class MultiHeadLatentAttentionWrapper(PluggableLayer):
|
||||
# the topk_tokens buffer written by a previous layer in the same pass.
|
||||
# Refer: https://arxiv.org/abs/2603.12201 for more details.
|
||||
self.skip_topk = skip_topk
|
||||
# qrep is active when the query projection is a DCP-group-sharded layer
|
||||
# that materializes the full group head set locally.
|
||||
q_proj_layer = self.q_b_proj if self.q_lora_rank is not None else self.q_proj
|
||||
self.dcp_q_replicate = getattr(q_proj_layer, "qrep_active", False)
|
||||
if self.indexer is not None:
|
||||
assert hasattr(self.indexer, "topk_tokens")
|
||||
self.topk_tokens = self.indexer.topk_tokens
|
||||
@@ -110,6 +114,7 @@ class MultiHeadLatentAttentionWrapper(PluggableLayer):
|
||||
quant_config=quant_config,
|
||||
prefix=f"{prefix}.attn",
|
||||
kv_b_proj=self.kv_b_proj,
|
||||
dcp_q_replicate=self.dcp_q_replicate,
|
||||
use_sparse=self.is_sparse,
|
||||
indexer=self.indexer,
|
||||
topk_indices_buffer=mla_modules.topk_indices_buffer,
|
||||
@@ -143,7 +148,8 @@ class MultiHeadLatentAttentionWrapper(PluggableLayer):
|
||||
dim=-1,
|
||||
)
|
||||
q_c = self.q_a_layernorm(q_c)
|
||||
q = self.q_b_proj(q_c)[0]
|
||||
q_proj_layer = self.q_b_proj
|
||||
q_proj_input = q_c
|
||||
else:
|
||||
assert self.kv_a_proj_with_mqa is not None, (
|
||||
"kv_a_proj_with_mqa is required when q_lora_rank is None"
|
||||
@@ -152,15 +158,20 @@ class MultiHeadLatentAttentionWrapper(PluggableLayer):
|
||||
"q_proj is required when q_lora_rank is None"
|
||||
)
|
||||
kv_lora = self.kv_a_proj_with_mqa(hidden_states)[0]
|
||||
q = self.q_proj(hidden_states)[0]
|
||||
q_proj_layer = self.q_proj
|
||||
q_proj_input = hidden_states
|
||||
|
||||
kv_c, k_pe = kv_lora.split([self.kv_lora_rank, self.qk_rope_head_dim], dim=-1)
|
||||
kv_c_normed = self.kv_a_layernorm(kv_c)
|
||||
|
||||
q = q.view(-1, self.num_heads, self.qk_head_dim)
|
||||
# Add head dim of 1 to k_pe
|
||||
k_pe = k_pe.unsqueeze(1)
|
||||
|
||||
q = q_proj_layer(q_proj_input)[0]
|
||||
heads = self.num_heads
|
||||
if self.dcp_q_replicate:
|
||||
heads *= q_proj_layer.group_size
|
||||
q = q.view(-1, heads, self.qk_head_dim)
|
||||
|
||||
if self.rotary_emb is not None:
|
||||
q[..., self.qk_nope_head_dim :], k_pe = self.rotary_emb(
|
||||
positions, q[..., self.qk_nope_head_dim :], k_pe
|
||||
@@ -172,11 +183,16 @@ class MultiHeadLatentAttentionWrapper(PluggableLayer):
|
||||
if llama_4_scaling is not None:
|
||||
q *= llama_4_scaling
|
||||
|
||||
q_dcp_replicated = None
|
||||
if self.dcp_q_replicate:
|
||||
q_dcp_replicated, q = q, q_proj_layer._local_view(q)
|
||||
|
||||
attn_out = self.mla_attn(
|
||||
q,
|
||||
kv_c_normed,
|
||||
k_pe,
|
||||
output_shape=(hidden_states.shape[0], self.num_heads * self.v_head_dim),
|
||||
q_dcp_replicated=q_dcp_replicated,
|
||||
)
|
||||
|
||||
return self.o_proj(attn_out)[0]
|
||||
|
||||
@@ -190,6 +190,7 @@ def flashinfer_trtllm_mxint4_moe(
|
||||
topk_group: int | None = None,
|
||||
e_score_correction_bias: torch.Tensor | None = None,
|
||||
routing_method_type: int | None = None,
|
||||
routing_replay_out: torch.Tensor | None = None,
|
||||
) -> torch.Tensor:
|
||||
"""
|
||||
Apply FlashInfer TensorRT-LLM MxInt4 MoE kernel.
|
||||
@@ -262,6 +263,7 @@ def flashinfer_trtllm_mxint4_moe(
|
||||
do_finalize=True,
|
||||
output=None,
|
||||
tune_max_num_tokens=8192,
|
||||
routing_replay_out=routing_replay_out,
|
||||
)
|
||||
if isinstance(out, (tuple, list)):
|
||||
out = out[0]
|
||||
|
||||
@@ -37,6 +37,11 @@ class Cosmos3ForConditionalGeneration(Qwen3VLForConditionalGeneration):
|
||||
".to_out.": ".o_proj.",
|
||||
".norm_q.": ".q_norm.",
|
||||
".norm_k.": ".k_norm.",
|
||||
# ModelOpt-native dialect (diffusers/transformers read these; vLLM reads
|
||||
# weight_scale/input_scale instead), drop so AutoWeightsLoader passes
|
||||
".input_quantizer.": None,
|
||||
".weight_quantizer.": None,
|
||||
".output_quantizer.": None,
|
||||
},
|
||||
orig_to_new_prefix={
|
||||
"proj_in.": None,
|
||||
|
||||
@@ -33,6 +33,7 @@ from torch import nn
|
||||
from transformers import DeepseekV2Config, DeepseekV3Config
|
||||
|
||||
import vllm._custom_ops as ops
|
||||
import vllm.envs as envs
|
||||
from vllm._aiter_ops import rocm_aiter_ops
|
||||
from vllm.compilation.decorators import support_torch_compile
|
||||
from vllm.config import CacheConfig, ParallelConfig, VllmConfig, get_current_vllm_config
|
||||
@@ -56,6 +57,7 @@ from vllm.model_executor.layers.fused_moe import (
|
||||
from vllm.model_executor.layers.layernorm import LayerNorm, RMSNorm
|
||||
from vllm.model_executor.layers.linear import (
|
||||
ColumnParallelLinear,
|
||||
DCPGroupColumnParallelLinear,
|
||||
MergedColumnParallelLinear,
|
||||
QKVParallelLinear,
|
||||
ReplicatedLinear,
|
||||
@@ -1015,9 +1017,17 @@ class DeepseekV2MLAAttention(nn.Module):
|
||||
prefix=f"{prefix}.kv_a_proj_with_mqa",
|
||||
)
|
||||
|
||||
qrep_enabled = (
|
||||
envs.VLLM_DCP_Q_REPLICATE
|
||||
and vllm_config.parallel_config.decode_context_parallel_size > 1
|
||||
and vllm_config.parallel_config.prefill_context_parallel_size <= 1
|
||||
)
|
||||
q_proj_cls = (
|
||||
DCPGroupColumnParallelLinear if qrep_enabled else ColumnParallelLinear
|
||||
)
|
||||
if self.q_lora_rank is not None:
|
||||
self.q_a_layernorm = RMSNorm(self.q_lora_rank, eps=config.rms_norm_eps)
|
||||
self.q_b_proj = ColumnParallelLinear(
|
||||
self.q_b_proj = q_proj_cls(
|
||||
self.q_lora_rank,
|
||||
self.num_heads * self.qk_head_dim,
|
||||
bias=False,
|
||||
@@ -1025,7 +1035,7 @@ class DeepseekV2MLAAttention(nn.Module):
|
||||
prefix=f"{prefix}.q_b_proj",
|
||||
)
|
||||
else:
|
||||
self.q_proj = ColumnParallelLinear(
|
||||
self.q_proj = q_proj_cls(
|
||||
proj_input_size,
|
||||
self.num_heads * self.qk_head_dim,
|
||||
bias=False,
|
||||
|
||||
@@ -84,6 +84,12 @@ from .utils import (
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
_GEMMA4_EXPERT_PARENT_MAPPER = WeightsMapper(
|
||||
orig_to_new_regex={
|
||||
re.compile(r"(?<!\.moe)\.experts$"): ".moe.experts",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _remap_gemma4_expert_weight_name(name: str) -> str:
|
||||
return re.sub(r"(?<!\.moe)\.experts\.(\d+)\.", r".moe.experts.\1.", name)
|
||||
@@ -1508,7 +1514,7 @@ class Gemma4Model(nn.Module, EagleModelMixin):
|
||||
class Gemma4ForCausalLM(
|
||||
nn.Module, SupportsLoRA, SupportsPP, MixtureOfExperts, SupportsEagle3
|
||||
):
|
||||
hf_to_vllm_mapper = WeightsMapper(
|
||||
hf_to_vllm_mapper = _GEMMA4_EXPERT_PARENT_MAPPER | WeightsMapper(
|
||||
orig_to_new_prefix={
|
||||
# Gemma4ForConditionalGeneration already loads the text stack
|
||||
# from `model.language_model.*`. We reuse that same checkpoint
|
||||
|
||||
@@ -40,7 +40,10 @@ from vllm.inputs import MultiModalDataDict
|
||||
from vllm.logger import init_logger
|
||||
from vllm.model_executor.layers.layernorm import RMSNorm
|
||||
from vllm.model_executor.layers.linear import ReplicatedLinear
|
||||
from vllm.model_executor.models.gemma4 import Gemma4ForCausalLM
|
||||
from vllm.model_executor.models.gemma4 import (
|
||||
_GEMMA4_EXPERT_PARENT_MAPPER,
|
||||
Gemma4ForCausalLM,
|
||||
)
|
||||
from vllm.model_executor.models.module_mapping import MultiModelKeys
|
||||
from vllm.model_executor.models.transformers.utils import recursive_replace_linear
|
||||
from vllm.multimodal import MULTIMODAL_REGISTRY
|
||||
@@ -998,7 +1001,7 @@ class Gemma4ForConditionalGeneration(
|
||||
}
|
||||
|
||||
# Maps checkpoint prefixes to vLLM module paths.
|
||||
hf_to_vllm_mapper = WeightsMapper(
|
||||
hf_to_vllm_mapper = _GEMMA4_EXPERT_PARENT_MAPPER | WeightsMapper(
|
||||
orig_to_new_prefix={
|
||||
# vision tower
|
||||
"model.vision_tower": "vision_tower",
|
||||
@@ -1010,7 +1013,7 @@ class Gemma4ForConditionalGeneration(
|
||||
"model.language_model.": "language_model.model.",
|
||||
"lm_head.": "language_model.lm_head.",
|
||||
"model": "language_model.model",
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
|
||||
|
||||
@@ -651,9 +651,9 @@ class MiniCPMV4_6ProcessingInfo(MiniCPMVProcessingInfo):
|
||||
class MiniCPMV4_6ViTWindowAttentionSelfAttn(nn.Module):
|
||||
hf_to_vllm_mapper = WeightsMapper(
|
||||
orig_to_new_stacked={
|
||||
"q_proj": ("qkv_proj", "q"),
|
||||
"k_proj": ("qkv_proj", "k"),
|
||||
"v_proj": ("qkv_proj", "v"),
|
||||
".q_proj": (".qkv_proj", "q"),
|
||||
".k_proj": (".qkv_proj", "k"),
|
||||
".v_proj": (".qkv_proj", "v"),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""Run registered CuTeDSL warmup compile units."""
|
||||
|
||||
# TODO(roberto): Remove this compatibility registry after registered CuTeDSL
|
||||
# warmups are migrated to the shared JIT warmup infrastructure.
|
||||
# https://github.com/vllm-project/vllm/pull/47451
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
|
||||
@@ -1,204 +0,0 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""FA4 MLA prefill CuTeDSL compile warmup config."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Hashable, Iterator
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, Literal
|
||||
|
||||
import torch
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from vllm.v1.attention.backends.fa_utils import (
|
||||
FlashAttentionCuTeDSLCompileSpec,
|
||||
)
|
||||
|
||||
FA4ArchitectureFamily = Literal["sm90", "sm100f", "sm120"]
|
||||
|
||||
FA4_STANDARD_DTYPES = (torch.bfloat16, torch.float16)
|
||||
|
||||
# Current vLLM MLA prefill expands K/V to num_heads before FA4, so this plan
|
||||
# covers qhead_per_kvhead=1.
|
||||
# Batch is not a current FA4 MLA-prefill key field. Use b1 for compile-only
|
||||
# specs because it is the conservative case for Split-KV shape heuristics.
|
||||
# TODO(roberto): FA4 also has direct-GQA and qv/top-k absorbed-MLA paths, but vLLM
|
||||
# does not use them in this backend yet; they need a separate
|
||||
# num_kv_heads/qv/top-k-aware warmup plan if wired in later.
|
||||
FA4_MLA_PREFILL_COMPILE_BATCH_SIZE = 1
|
||||
FA4_MLA_PREFILL_Q_TILE = 128
|
||||
FA4_MLA_PREFILL_K_TILE = 128
|
||||
FA4_MLA_PREFILL_LONG_K_BLOCKS = 32
|
||||
FA4_MLA_PREFILL_VERY_LONG_K_BLOCKS = 64
|
||||
FA4_MLA_PREFILL_CAUSAL_OPTIONS = (False, True)
|
||||
FA4_MLA_PREFILL_LSE_OPTIONS = (False, True)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class FA4MLAPrefillCompileContext:
|
||||
dtype: torch.dtype
|
||||
num_heads: int
|
||||
qk_head_dim: int
|
||||
v_head_dim: int
|
||||
kv_nope_head_dim: int
|
||||
requires_v_padding: bool
|
||||
scale: float
|
||||
num_splits: int
|
||||
fa_version: int
|
||||
|
||||
# Return the V head dim FA4 sees.
|
||||
@property
|
||||
def effective_v_head_dim(self) -> int:
|
||||
if self.requires_v_padding:
|
||||
return self.qk_head_dim
|
||||
return self.v_head_dim
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class FA4MLAPrefillCompileRequest:
|
||||
"""One compile-only FA4 MLA prefill request."""
|
||||
|
||||
key: Hashable
|
||||
compile_spec: FlashAttentionCuTeDSLCompileSpec
|
||||
|
||||
# Compile this request.
|
||||
def compile(self) -> None:
|
||||
self.compile_spec.compile()
|
||||
|
||||
|
||||
# Yield deduped compile requests.
|
||||
def iter_fa4_mla_prefill_compile_requests(
|
||||
ctx: FA4MLAPrefillCompileContext,
|
||||
) -> Iterator[FA4MLAPrefillCompileRequest]:
|
||||
"""Yield compile requests for this fixed MLA backend.
|
||||
|
||||
FA4 dedupes duplicate atomic kernel selections in its own JIT cache.
|
||||
"""
|
||||
seen: set[Hashable] = set()
|
||||
for compile_spec in iter_fa4_mla_prefill_compile_specs(ctx):
|
||||
key = compile_spec.request_key()
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
yield FA4MLAPrefillCompileRequest(
|
||||
key=key,
|
||||
compile_spec=compile_spec,
|
||||
)
|
||||
|
||||
|
||||
# Build compile specs for this setup.
|
||||
def iter_fa4_mla_prefill_compile_specs(
|
||||
ctx: FA4MLAPrefillCompileContext,
|
||||
) -> Iterator[FlashAttentionCuTeDSLCompileSpec]:
|
||||
"""Yield compile-only FA4 MLA prefill requests for this fixed setup."""
|
||||
|
||||
arch_family = _fa4_architecture_family_from_compute_capability(
|
||||
*torch.cuda.get_device_capability()
|
||||
)
|
||||
if not _supports_fa4_mla_prefill(ctx, arch_family):
|
||||
return
|
||||
|
||||
from vllm.v1.attention.backends.fa_utils import (
|
||||
FlashAttentionCuTeDSLCompileSpec,
|
||||
)
|
||||
|
||||
batch_size = FA4_MLA_PREFILL_COMPILE_BATCH_SIZE
|
||||
v_stride = None
|
||||
if not ctx.requires_v_padding:
|
||||
v_stride = (
|
||||
ctx.num_heads * ctx.kv_nope_head_dim,
|
||||
ctx.kv_nope_head_dim,
|
||||
1,
|
||||
)
|
||||
|
||||
for _, max_seqlen_q, max_seqlen_k in _shape_probes_for_context(ctx, arch_family):
|
||||
total_q_tokens = batch_size * max_seqlen_q
|
||||
total_kv_tokens = batch_size * max_seqlen_k
|
||||
for causal in FA4_MLA_PREFILL_CAUSAL_OPTIONS:
|
||||
for return_lse in FA4_MLA_PREFILL_LSE_OPTIONS:
|
||||
yield FlashAttentionCuTeDSLCompileSpec(
|
||||
q_shape=(total_q_tokens, ctx.num_heads, ctx.qk_head_dim),
|
||||
k_shape=(total_kv_tokens, ctx.num_heads, ctx.qk_head_dim),
|
||||
v_shape=(
|
||||
total_kv_tokens,
|
||||
ctx.num_heads,
|
||||
ctx.effective_v_head_dim,
|
||||
),
|
||||
v_stride=v_stride,
|
||||
q_dtype=ctx.dtype,
|
||||
cu_seqlens_q_shape=(batch_size + 1,),
|
||||
cu_seqlens_k_shape=(batch_size + 1,),
|
||||
max_seqlen_q=max_seqlen_q,
|
||||
max_seqlen_k=max_seqlen_k,
|
||||
softmax_scale=ctx.scale,
|
||||
causal=causal,
|
||||
return_softmax_lse=return_lse,
|
||||
num_splits=ctx.num_splits,
|
||||
fa_version=ctx.fa_version,
|
||||
)
|
||||
|
||||
|
||||
# Pick one q/k point per current FA4 MLA-prefill shape regime.
|
||||
def _shape_probes_for_context(
|
||||
ctx: FA4MLAPrefillCompileContext,
|
||||
arch_family: FA4ArchitectureFamily,
|
||||
) -> tuple[tuple[str, int, int], ...]:
|
||||
q_stage1_q = 1
|
||||
q_stage2_q = FA4_MLA_PREFILL_Q_TILE + 1
|
||||
# FA4 never auto-splits when ceil(max_seqlen_k / tile_n) <= 4.
|
||||
no_split_k = 4 * FA4_MLA_PREFILL_K_TILE
|
||||
long_k = FA4_MLA_PREFILL_LONG_K_BLOCKS * FA4_MLA_PREFILL_K_TILE
|
||||
# Diff-head-dim Blackwell Split-KV switches tile_n at 64 K blocks.
|
||||
very_long_k = FA4_MLA_PREFILL_VERY_LONG_K_BLOCKS * FA4_MLA_PREFILL_K_TILE
|
||||
|
||||
base_probes = (
|
||||
("q_stage1", q_stage1_q, FA4_MLA_PREFILL_K_TILE),
|
||||
("q_stage2", q_stage2_q, no_split_k),
|
||||
)
|
||||
# SM120 currently rejects Split-KV in FA4; num_splits=1 also has no split
|
||||
# shape regimes on any architecture.
|
||||
if ctx.num_splits == 1 or arch_family == "sm120":
|
||||
return base_probes
|
||||
|
||||
long_k_probes = (
|
||||
("q_stage1_long_k", q_stage1_q, long_k),
|
||||
("q_stage2_long_k", q_stage2_q, long_k),
|
||||
)
|
||||
|
||||
# SM90 does not have the SM100 q_stage or diff-head-dim tile_n=64 branch.
|
||||
# Same-dim SM100-family MLA also does not need the very-long-K probe.
|
||||
if arch_family == "sm90" or ctx.qk_head_dim == ctx.effective_v_head_dim:
|
||||
return (*base_probes, *long_k_probes)
|
||||
|
||||
very_long_k_probes = (
|
||||
("q_stage1_very_long_k", q_stage1_q, very_long_k),
|
||||
("q_stage2_very_long_k", q_stage2_q, very_long_k),
|
||||
)
|
||||
return (*base_probes, *long_k_probes, *very_long_k_probes)
|
||||
|
||||
|
||||
# Check whether this setup can use FA4 MLA prefill.
|
||||
def _supports_fa4_mla_prefill(
|
||||
ctx: FA4MLAPrefillCompileContext,
|
||||
arch_family: FA4ArchitectureFamily,
|
||||
) -> bool:
|
||||
return (
|
||||
ctx.dtype in FA4_STANDARD_DTYPES
|
||||
and ctx.num_heads > 0
|
||||
and (arch_family != "sm120" or ctx.num_splits == 1)
|
||||
)
|
||||
|
||||
|
||||
# Map CUDA capability to the FA4 arch family used by warmup checks.
|
||||
def _fa4_architecture_family_from_compute_capability(
|
||||
major: int,
|
||||
minor: int,
|
||||
) -> FA4ArchitectureFamily:
|
||||
if (major, minor) == (9, 0):
|
||||
return "sm90"
|
||||
if major == 10:
|
||||
return "sm100f"
|
||||
if (major, minor) == (12, 0):
|
||||
return "sm120"
|
||||
raise ValueError(f"FA4 warmup does not know CUDA capability {major}.{minor}")
|
||||
@@ -0,0 +1,30 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""Warm up FA4 CuTeDSL MLA prefill compile keys."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from vllm.v1.attention.backends.mla.prefill import get_mla_prefill_backend
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from vllm.v1.worker.gpu_worker import Worker
|
||||
|
||||
|
||||
def fa4_cutedsl_warmup(worker: Worker) -> None:
|
||||
runner = worker.model_runner
|
||||
if runner.is_pooling_model:
|
||||
return
|
||||
|
||||
vllm_config = runner.vllm_config
|
||||
if not vllm_config.model_config.use_mla:
|
||||
return
|
||||
|
||||
backend_cls = get_mla_prefill_backend(vllm_config)
|
||||
if backend_cls.get_name() != "FLASH_ATTN":
|
||||
return
|
||||
|
||||
from vllm.v1.attention.backends.mla.prefill import flash_attn
|
||||
|
||||
flash_attn.FA4_MLA_PREFILL_KERNEL.warmup(vllm_config)
|
||||
@@ -0,0 +1,474 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""Shared interfaces and tracing helpers for explicit JIT warmup keys."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import ast
|
||||
import inspect
|
||||
import itertools
|
||||
import operator
|
||||
import textwrap
|
||||
from abc import ABC, abstractmethod
|
||||
from collections.abc import Callable, Iterable, Mapping
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Generic, TypeVar
|
||||
|
||||
__all__ = [
|
||||
"VllmJitKernel",
|
||||
"WarmupIntRange",
|
||||
"get_ast_full_name",
|
||||
"get_function_source_node",
|
||||
"zip_inputs",
|
||||
]
|
||||
|
||||
|
||||
CompileKeyT = TypeVar("CompileKeyT")
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class WarmupIntRange:
|
||||
start: int
|
||||
stop: int
|
||||
step: int = 1
|
||||
|
||||
|
||||
WarmupValues = Any
|
||||
CompileKeyDispatchFn = Callable[..., CompileKeyT]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class _WarmupInputRows:
|
||||
"""Warmup dispatch inputs expanded in lockstep."""
|
||||
|
||||
rows: tuple[Mapping[str, WarmupValues], ...]
|
||||
|
||||
|
||||
def _expand_warmup_values(values: WarmupValues) -> tuple[Any, ...]:
|
||||
if isinstance(values, WarmupIntRange):
|
||||
return tuple(range(values.start, values.stop, values.step))
|
||||
if isinstance(values, (list, tuple)):
|
||||
return tuple(values)
|
||||
return (values,)
|
||||
|
||||
|
||||
def zip_inputs(*rows: Mapping[str, WarmupValues]) -> _WarmupInputRows:
|
||||
"""Group row-wise dispatch inputs that should be expanded in lockstep."""
|
||||
if not rows:
|
||||
raise ValueError("zip_inputs requires at least one dispatch input row")
|
||||
if not all(isinstance(row, Mapping) for row in rows):
|
||||
raise ValueError("zip_inputs rows must be mappings")
|
||||
|
||||
first_names = frozenset(rows[0])
|
||||
if not first_names:
|
||||
raise ValueError("zip_inputs rows require at least one dispatch input name")
|
||||
if not all(isinstance(name, str) for name in first_names):
|
||||
raise ValueError("zip_inputs dispatch input names must be strings")
|
||||
|
||||
input_rows: list[Mapping[str, WarmupValues]] = []
|
||||
for row in rows:
|
||||
names = frozenset(row)
|
||||
if names != first_names:
|
||||
raise ValueError("zip_inputs rows must use the same dispatch input names")
|
||||
input_rows.append(dict(row))
|
||||
|
||||
return _WarmupInputRows(rows=tuple(input_rows))
|
||||
|
||||
|
||||
def _expand_warmup_value_grid(
|
||||
values: Mapping[str, WarmupValues],
|
||||
input_names: frozenset[str],
|
||||
) -> tuple[dict[str, Any], ...]:
|
||||
names = tuple(name for name in values if name in input_names)
|
||||
if not names:
|
||||
return ({},)
|
||||
|
||||
expanded_values = tuple(_expand_warmup_values(values[name]) for name in names)
|
||||
return tuple(
|
||||
dict(zip(names, value_set)) for value_set in itertools.product(*expanded_values)
|
||||
)
|
||||
|
||||
|
||||
def _expand_warmup_input_rows(
|
||||
rows: tuple[Mapping[str, WarmupValues], ...],
|
||||
input_names: frozenset[str],
|
||||
) -> tuple[dict[str, Any], ...]:
|
||||
active_names = frozenset(name for name in rows[0] if name in input_names)
|
||||
if not active_names:
|
||||
return ({},)
|
||||
|
||||
return tuple(
|
||||
{name: value for name, value in row.items() if name in active_names}
|
||||
for row in rows
|
||||
)
|
||||
|
||||
|
||||
def _merge_warmup_kwargs(parts: Iterable[Mapping[str, Any]]) -> dict[str, Any]:
|
||||
merged: dict[str, Any] = {}
|
||||
for part in parts:
|
||||
for name, value in part.items():
|
||||
if name in merged:
|
||||
raise ValueError(
|
||||
f"Warmup dispatch input '{name}' is specified more than once"
|
||||
)
|
||||
merged[name] = value
|
||||
return merged
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class _CompileKeyDispatchTrace:
|
||||
local_exprs: tuple[tuple[str, ast.AST], ...]
|
||||
field_exprs: tuple[tuple[str, ast.AST], ...]
|
||||
globals: Mapping[str, Any]
|
||||
input_names: frozenset[str]
|
||||
defaults: Mapping[str, Any]
|
||||
|
||||
def compile_key(
|
||||
self,
|
||||
compile_key_type: type[CompileKeyT],
|
||||
kwargs: Mapping[str, Any],
|
||||
) -> CompileKeyT:
|
||||
dispatch_values = {**self.defaults, **kwargs}
|
||||
for name, expr in self.local_exprs:
|
||||
dispatch_values[name] = _eval_dispatch_expr(
|
||||
expr, dispatch_values, self.globals
|
||||
)
|
||||
return compile_key_type(
|
||||
**{
|
||||
field: _eval_dispatch_expr(expr, dispatch_values, self.globals)
|
||||
for field, expr in self.field_exprs
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
_BIN_OPS: dict[type[ast.operator], Callable[[Any, Any], Any]] = {
|
||||
ast.Add: operator.add,
|
||||
ast.Sub: operator.sub,
|
||||
ast.Mult: operator.mul,
|
||||
ast.FloorDiv: operator.floordiv,
|
||||
ast.Mod: operator.mod,
|
||||
ast.Pow: operator.pow,
|
||||
}
|
||||
_CMP_OPS: dict[type[ast.cmpop], Callable[[Any, Any], bool]] = {
|
||||
ast.Eq: operator.eq,
|
||||
ast.NotEq: operator.ne,
|
||||
ast.Lt: operator.lt,
|
||||
ast.LtE: operator.le,
|
||||
ast.Gt: operator.gt,
|
||||
ast.GtE: operator.ge,
|
||||
}
|
||||
|
||||
|
||||
def _dispatch_expr_source(node: ast.AST) -> str:
|
||||
try:
|
||||
return ast.unparse(node)
|
||||
except Exception:
|
||||
return ast.dump(node)
|
||||
|
||||
|
||||
def _dispatch_expr_error(node: ast.AST, reason: str) -> ValueError:
|
||||
return ValueError(
|
||||
f"{reason}: {_dispatch_expr_source(node)}. "
|
||||
"Supported dispatch expressions are names, constants, attributes, "
|
||||
"tuple/list literals, conditional expressions, comparisons, boolean "
|
||||
"operators, unary not/minus, arithmetic, and calls without **kwargs."
|
||||
)
|
||||
|
||||
|
||||
class _DispatchExprEvaluator(ast.NodeVisitor):
|
||||
def __init__(
|
||||
self,
|
||||
values: Mapping[str, Any],
|
||||
globals_: Mapping[str, Any],
|
||||
) -> None:
|
||||
self.values = values
|
||||
self.globals = globals_
|
||||
|
||||
def eval(self, node: ast.AST) -> Any:
|
||||
return self.visit(node)
|
||||
|
||||
def generic_visit(self, node: ast.AST) -> Any:
|
||||
raise _dispatch_expr_error(node, "Unsupported dispatch expression")
|
||||
|
||||
def visit_Name(self, node: ast.Name) -> Any:
|
||||
if node.id in self.values:
|
||||
return self.values[node.id]
|
||||
if node.id in self.globals:
|
||||
return self.globals[node.id]
|
||||
raise _dispatch_expr_error(node, f"Unknown dispatch name '{node.id}'")
|
||||
|
||||
def visit_Constant(self, node: ast.Constant) -> Any:
|
||||
return node.value
|
||||
|
||||
def visit_IfExp(self, node: ast.IfExp) -> Any:
|
||||
return self.visit(node.body if self.visit(node.test) else node.orelse)
|
||||
|
||||
def visit_Tuple(self, node: ast.Tuple) -> tuple[Any, ...]:
|
||||
return tuple(self.visit(elt) for elt in node.elts)
|
||||
|
||||
def visit_List(self, node: ast.List) -> list[Any]:
|
||||
return [self.visit(elt) for elt in node.elts]
|
||||
|
||||
def visit_BoolOp(self, node: ast.BoolOp) -> Any:
|
||||
if isinstance(node.op, ast.And):
|
||||
result = None
|
||||
for value in node.values:
|
||||
result = self.visit(value)
|
||||
if not result:
|
||||
return result
|
||||
return result
|
||||
if isinstance(node.op, ast.Or):
|
||||
result = None
|
||||
for value in node.values:
|
||||
result = self.visit(value)
|
||||
if result:
|
||||
return result
|
||||
return result
|
||||
raise _dispatch_expr_error(node, "Unsupported dispatch boolean operator")
|
||||
|
||||
def visit_Compare(self, node: ast.Compare) -> bool:
|
||||
left = self.visit(node.left)
|
||||
for op_node, comparator in zip(node.ops, node.comparators):
|
||||
right = self.visit(comparator)
|
||||
op = _CMP_OPS.get(type(op_node))
|
||||
if op is None:
|
||||
raise _dispatch_expr_error(
|
||||
node, "Unsupported dispatch comparison operator"
|
||||
)
|
||||
if not op(left, right):
|
||||
return False
|
||||
left = right
|
||||
return True
|
||||
|
||||
def visit_UnaryOp(self, node: ast.UnaryOp) -> Any:
|
||||
operand = self.visit(node.operand)
|
||||
if isinstance(node.op, ast.Not):
|
||||
return not operand
|
||||
if isinstance(node.op, ast.USub):
|
||||
return -operand
|
||||
raise _dispatch_expr_error(node, "Unsupported dispatch unary operator")
|
||||
|
||||
def visit_BinOp(self, node: ast.BinOp) -> Any:
|
||||
op = _BIN_OPS.get(type(node.op))
|
||||
if op is None:
|
||||
raise _dispatch_expr_error(node, "Unsupported dispatch binary operator")
|
||||
return op(self.visit(node.left), self.visit(node.right))
|
||||
|
||||
def visit_Call(self, node: ast.Call) -> Any:
|
||||
args = [self.visit(arg) for arg in node.args]
|
||||
fn = self.visit(node.func)
|
||||
call_kwargs: dict[str, Any] = {}
|
||||
for keyword in node.keywords:
|
||||
if keyword.arg is None:
|
||||
raise _dispatch_expr_error(
|
||||
node, "Dispatch helper calls cannot use **kwargs"
|
||||
)
|
||||
call_kwargs[keyword.arg] = self.visit(keyword.value)
|
||||
return fn(*args, **call_kwargs)
|
||||
|
||||
def visit_Attribute(self, node: ast.Attribute) -> Any:
|
||||
return getattr(self.visit(node.value), node.attr)
|
||||
|
||||
|
||||
def get_ast_full_name(node: ast.AST) -> str | None:
|
||||
if isinstance(node, ast.Name):
|
||||
return node.id
|
||||
if isinstance(node, ast.Attribute):
|
||||
parent = get_ast_full_name(node.value)
|
||||
if parent is not None:
|
||||
return f"{parent}.{node.attr}"
|
||||
return None
|
||||
|
||||
|
||||
def get_function_source_node(fn: Callable[..., Any]) -> ast.FunctionDef:
|
||||
source_fn = getattr(fn, "fn", fn)
|
||||
source = textwrap.dedent(inspect.getsource(source_fn))
|
||||
tree = ast.parse(source)
|
||||
function_defs = [node for node in tree.body if isinstance(node, ast.FunctionDef)]
|
||||
if len(function_defs) != 1:
|
||||
name = getattr(source_fn, "__name__", type(source_fn).__name__)
|
||||
raise ValueError(f"Expected one function in {name}, found {len(function_defs)}")
|
||||
return function_defs[0]
|
||||
|
||||
|
||||
def _eval_dispatch_expr(
|
||||
node: ast.AST,
|
||||
kwargs: Mapping[str, Any],
|
||||
globals_: Mapping[str, Any],
|
||||
) -> Any:
|
||||
return _DispatchExprEvaluator(kwargs, globals_).eval(node)
|
||||
|
||||
|
||||
def _collect_input_names(
|
||||
node: ast.AST,
|
||||
candidate_names: set[str],
|
||||
local_names: set[str] | None = None,
|
||||
) -> set[str]:
|
||||
if local_names is None:
|
||||
local_names = set()
|
||||
return {
|
||||
child.id
|
||||
for child in ast.walk(node)
|
||||
if (
|
||||
isinstance(child, ast.Name)
|
||||
and child.id in candidate_names
|
||||
and child.id not in local_names
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
def _collect_dispatch_body(
|
||||
fn: CompileKeyDispatchFn[Any],
|
||||
function_def: ast.FunctionDef,
|
||||
) -> tuple[list[tuple[str, ast.AST]], ast.Call]:
|
||||
local_exprs: list[tuple[str, ast.AST]] = []
|
||||
for statement in function_def.body:
|
||||
if (
|
||||
isinstance(statement, ast.Expr)
|
||||
and isinstance(statement.value, ast.Constant)
|
||||
and isinstance(statement.value.value, str)
|
||||
):
|
||||
continue
|
||||
|
||||
if isinstance(statement, ast.Assign):
|
||||
if len(statement.targets) != 1 or not isinstance(
|
||||
statement.targets[0], ast.Name
|
||||
):
|
||||
raise _dispatch_expr_error(
|
||||
statement, "Dispatch assignments must target one local name"
|
||||
)
|
||||
local_exprs.append((statement.targets[0].id, statement.value))
|
||||
continue
|
||||
|
||||
if isinstance(statement, ast.AnnAssign):
|
||||
if statement.value is None:
|
||||
raise _dispatch_expr_error(
|
||||
statement, "Dispatch annotations must assign a value"
|
||||
)
|
||||
if not isinstance(statement.target, ast.Name):
|
||||
raise _dispatch_expr_error(
|
||||
statement, "Dispatch assignments must target one local name"
|
||||
)
|
||||
local_exprs.append((statement.target.id, statement.value))
|
||||
continue
|
||||
|
||||
if isinstance(statement, ast.Return) and isinstance(statement.value, ast.Call):
|
||||
return local_exprs, statement.value
|
||||
|
||||
if isinstance(statement, ast.Return):
|
||||
raise _dispatch_expr_error(
|
||||
statement, "Dispatch must return one CompileKey(...) call"
|
||||
)
|
||||
|
||||
raise _dispatch_expr_error(
|
||||
statement,
|
||||
"Dispatch may only contain local assignments before CompileKey return",
|
||||
)
|
||||
|
||||
raise ValueError(f"Expected {fn.__name__} to return one CompileKey(...) call")
|
||||
|
||||
|
||||
def _trace_compile_key_dispatch(
|
||||
fn: CompileKeyDispatchFn[Any],
|
||||
) -> _CompileKeyDispatchTrace:
|
||||
source_fn = getattr(fn, "__func__", fn)
|
||||
globals_ = source_fn.__globals__
|
||||
function_def = get_function_source_node(fn)
|
||||
|
||||
local_exprs, return_call = _collect_dispatch_body(fn, function_def)
|
||||
|
||||
field_exprs: list[tuple[str, ast.AST]] = []
|
||||
signature = inspect.signature(fn)
|
||||
defaults = {
|
||||
name: parameter.default
|
||||
for name, parameter in signature.parameters.items()
|
||||
if parameter.default is not inspect.Parameter.empty
|
||||
}
|
||||
candidate_names = set(signature.parameters)
|
||||
input_names: set[str] = set()
|
||||
local_names = {name for name, _ in local_exprs}
|
||||
for _, expr in local_exprs:
|
||||
input_names.update(_collect_input_names(expr, candidate_names))
|
||||
for keyword in return_call.keywords:
|
||||
if keyword.arg is None:
|
||||
raise ValueError(f"{fn.__name__} cannot use **kwargs in CompileKey")
|
||||
field_exprs.append((keyword.arg, keyword.value))
|
||||
input_names.update(
|
||||
_collect_input_names(keyword.value, candidate_names, local_names)
|
||||
)
|
||||
|
||||
return _CompileKeyDispatchTrace(
|
||||
tuple(local_exprs),
|
||||
tuple(field_exprs),
|
||||
globals_,
|
||||
frozenset(input_names),
|
||||
defaults,
|
||||
)
|
||||
|
||||
|
||||
class VllmJitKernel(Generic[CompileKeyT], ABC):
|
||||
"""Kernel wrapper that owns dispatch, warmup keys, and compilation."""
|
||||
|
||||
CompileKey: type[CompileKeyT]
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.compile_key_dispatch_trace = _trace_compile_key_dispatch(self.dispatch)
|
||||
|
||||
def compile_key(self, kwargs: Mapping[str, Any]) -> CompileKeyT:
|
||||
return self.compile_key_dispatch_trace.compile_key(self.CompileKey, kwargs)
|
||||
|
||||
def _trace_dispatch(
|
||||
self, dispatch: CompileKeyDispatchFn[CompileKeyT]
|
||||
) -> Callable[..., list[CompileKeyT]]:
|
||||
compile_key_dispatch_trace = _trace_compile_key_dispatch(dispatch)
|
||||
|
||||
def traced(
|
||||
*input_groups: _WarmupInputRows,
|
||||
**kwargs: WarmupValues,
|
||||
) -> list[CompileKeyT]:
|
||||
for group in input_groups:
|
||||
if not isinstance(group, _WarmupInputRows):
|
||||
raise TypeError(
|
||||
"_trace_dispatch positional arguments must be "
|
||||
"zip_inputs(...) groups"
|
||||
)
|
||||
expanded_input_groups = tuple(
|
||||
_expand_warmup_input_rows(
|
||||
group.rows, compile_key_dispatch_trace.input_names
|
||||
)
|
||||
for group in input_groups
|
||||
)
|
||||
expanded_kwargs = _expand_warmup_value_grid(
|
||||
kwargs, compile_key_dispatch_trace.input_names
|
||||
)
|
||||
dispatch_value_groups = (*expanded_input_groups, expanded_kwargs)
|
||||
return list(
|
||||
dict.fromkeys(
|
||||
compile_key_dispatch_trace.compile_key(
|
||||
self.CompileKey, _merge_warmup_kwargs(dispatch_values)
|
||||
)
|
||||
for dispatch_values in itertools.product(*dispatch_value_groups)
|
||||
)
|
||||
)
|
||||
|
||||
return traced
|
||||
|
||||
@abstractmethod
|
||||
def dispatch(self, **kwargs: Any) -> CompileKeyT:
|
||||
"""Build one compile key from one concrete dispatch point."""
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def get_warmup_keys(self, *args: Any, **kwargs: Any) -> list[CompileKeyT]:
|
||||
"""Return compile keys that should be warmed for this kernel."""
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def compile(self, compile_key: CompileKeyT) -> None:
|
||||
"""Compile one warmup key."""
|
||||
raise NotImplementedError
|
||||
|
||||
def warmup(self, *args: Any, **kwargs: Any) -> None:
|
||||
"""Compile this kernel's warmup keys."""
|
||||
for compile_key in self.get_warmup_keys(*args, **kwargs):
|
||||
self.compile(compile_key)
|
||||
@@ -0,0 +1,183 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
import ast
|
||||
import inspect
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from vllm.model_executor.warmup.jit_warmup import (
|
||||
get_ast_full_name,
|
||||
get_function_source_node,
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TritonWarmupTensor:
|
||||
# Compile-only tensor descriptor for Triton pointer specialization.
|
||||
dtype: Any
|
||||
aligned: bool = True
|
||||
shape: tuple[int, ...] = (1,)
|
||||
|
||||
def data_ptr(self) -> int:
|
||||
return 0 if self.aligned else 1
|
||||
|
||||
def ptr_range(self) -> int:
|
||||
return 0
|
||||
|
||||
def stride(self) -> tuple[int, ...]:
|
||||
strides: list[int] = []
|
||||
stride = 1
|
||||
for size in reversed(self.shape):
|
||||
strides.append(stride)
|
||||
stride *= size
|
||||
return tuple(reversed(strides))
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TritonPointerInputVariant:
|
||||
# Named pointer-alignment variant for compile-only Triton warmup.
|
||||
alignments: tuple[tuple[str, bool], ...]
|
||||
|
||||
@classmethod
|
||||
def from_alignment(cls, **aligned: bool) -> "TritonPointerInputVariant":
|
||||
return cls(tuple(aligned.items()))
|
||||
|
||||
def is_aligned(self, name: str) -> bool:
|
||||
for alignment_name, aligned in self.alignments:
|
||||
if alignment_name == name:
|
||||
return aligned
|
||||
raise KeyError(f"Unknown Triton pointer input variant: {name}")
|
||||
|
||||
def pointer(
|
||||
self,
|
||||
name: str,
|
||||
dtype: Any,
|
||||
shape: tuple[int, ...] = (1,),
|
||||
) -> TritonWarmupTensor:
|
||||
return TritonWarmupTensor(dtype, aligned=self.is_aligned(name), shape=shape)
|
||||
|
||||
|
||||
def _literal_str_refs(node: ast.AST) -> tuple[str | int, ...]:
|
||||
if isinstance(node, ast.Constant) and isinstance(node.value, str | int):
|
||||
return (node.value,)
|
||||
if isinstance(node, ast.List | ast.Tuple):
|
||||
refs: list[str | int] = []
|
||||
for elt in node.elts:
|
||||
if isinstance(elt, ast.Constant) and isinstance(elt.value, str | int):
|
||||
refs.append(elt.value)
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Unsupported Triton specialization ref: {ast.dump(elt)}"
|
||||
)
|
||||
return tuple(refs)
|
||||
raise ValueError(f"Unsupported Triton specialization refs: {ast.dump(node)}")
|
||||
|
||||
|
||||
def _normalize_arg_refs(
|
||||
refs: tuple[str | int, ...],
|
||||
arg_names: tuple[str, ...],
|
||||
) -> frozenset[str]:
|
||||
names: set[str] = set()
|
||||
for ref in refs:
|
||||
if isinstance(ref, int):
|
||||
names.add(arg_names[ref])
|
||||
else:
|
||||
names.add(ref)
|
||||
return frozenset(names)
|
||||
|
||||
|
||||
def _decorator_keyword_refs(
|
||||
function_def: ast.FunctionDef,
|
||||
keyword_name: str,
|
||||
) -> tuple[str | int, ...]:
|
||||
for decorator in function_def.decorator_list:
|
||||
if not isinstance(decorator, ast.Call):
|
||||
continue
|
||||
decorator_name = get_ast_full_name(decorator.func)
|
||||
if decorator_name not in ("triton.jit", "jit"):
|
||||
continue
|
||||
for keyword in decorator.keywords:
|
||||
if keyword.arg == keyword_name:
|
||||
return _literal_str_refs(keyword.value)
|
||||
return ()
|
||||
|
||||
|
||||
def _triton_do_not_specialize_args(
|
||||
kernel: Callable[..., Any],
|
||||
function_def: ast.FunctionDef,
|
||||
arg_names: tuple[str, ...],
|
||||
) -> frozenset[str]:
|
||||
refs = getattr(kernel, "do_not_specialize", None)
|
||||
if refs is not None:
|
||||
return _normalize_arg_refs(tuple(refs), arg_names)
|
||||
return _normalize_arg_refs(
|
||||
_decorator_keyword_refs(function_def, "do_not_specialize"),
|
||||
arg_names,
|
||||
)
|
||||
|
||||
|
||||
def _triton_constexpr_arg_names(
|
||||
kernel: Callable[..., Any],
|
||||
function_def: ast.FunctionDef,
|
||||
arg_names: tuple[str, ...],
|
||||
) -> frozenset[str]:
|
||||
constexprs = getattr(kernel, "constexprs", None)
|
||||
if constexprs is not None:
|
||||
return frozenset(arg_names[index] for index in constexprs)
|
||||
|
||||
names: set[str] = set()
|
||||
for arg in function_def.args.args + function_def.args.kwonlyargs:
|
||||
if arg.annotation is None:
|
||||
continue
|
||||
annotation = get_ast_full_name(arg.annotation)
|
||||
if annotation in ("tl.constexpr", "triton.language.constexpr", "constexpr"):
|
||||
names.add(arg.arg)
|
||||
return frozenset(names)
|
||||
|
||||
|
||||
def _leftmost_name(node: ast.AST) -> str | None:
|
||||
if isinstance(node, ast.Name):
|
||||
return node.id
|
||||
if isinstance(node, ast.BinOp):
|
||||
return _leftmost_name(node.left)
|
||||
return None
|
||||
|
||||
|
||||
def _pointer_arg_names(
|
||||
function_def: ast.FunctionDef,
|
||||
arg_names: tuple[str, ...],
|
||||
) -> frozenset[str]:
|
||||
candidate_names = set(arg_names)
|
||||
pointer_names = {name for name in arg_names if name.endswith("_ptr")}
|
||||
for node in ast.walk(function_def):
|
||||
if not isinstance(node, ast.Call):
|
||||
continue
|
||||
if get_ast_full_name(node.func) not in ("tl.load", "tl.store"):
|
||||
continue
|
||||
if not node.args:
|
||||
continue
|
||||
name = _leftmost_name(node.args[0])
|
||||
if name in candidate_names:
|
||||
pointer_names.add(name)
|
||||
return frozenset(pointer_names)
|
||||
|
||||
|
||||
def trace_triton_kernel_specialization_args(
|
||||
kernel: Callable[..., Any],
|
||||
) -> tuple[str, ...]:
|
||||
function_def = get_function_source_node(kernel)
|
||||
source_fn = getattr(kernel, "fn", kernel)
|
||||
arg_names = tuple(inspect.signature(source_fn).parameters)
|
||||
constexpr_args = _triton_constexpr_arg_names(kernel, function_def, arg_names)
|
||||
do_not_specialize_args = _triton_do_not_specialize_args(
|
||||
kernel, function_def, arg_names
|
||||
)
|
||||
pointer_args = _pointer_arg_names(function_def, arg_names)
|
||||
|
||||
return tuple(
|
||||
name
|
||||
for name in arg_names
|
||||
if name in constexpr_args
|
||||
or (name not in pointer_args and name not in do_not_specialize_args)
|
||||
)
|
||||
@@ -17,6 +17,9 @@ from vllm.model_executor.warmup.deep_gemm_warmup import deep_gemm_warmup
|
||||
from vllm.model_executor.warmup.deepseek_v4_mhc_warmup import (
|
||||
deepseek_v4_mhc_warmup,
|
||||
)
|
||||
from vllm.model_executor.warmup.fa4_cutedsl_warmup import (
|
||||
fa4_cutedsl_warmup,
|
||||
)
|
||||
from vllm.model_executor.warmup.flashinfer_autotune_cache import (
|
||||
resolve_flashinfer_autotune_file,
|
||||
write_flashinfer_autotune_cache,
|
||||
@@ -27,7 +30,7 @@ from vllm.model_executor.warmup.flashinfer_sparse_mla_warmup import (
|
||||
)
|
||||
from vllm.model_executor.warmup.qwen_triton_warmup import qwen_triton_warmup
|
||||
from vllm.model_executor.warmup.sparse_mla_triton_warmup import (
|
||||
sparse_mla_triton_warmup_if_needed,
|
||||
sparse_mla_triton_warmup,
|
||||
)
|
||||
from vllm.model_executor.warmup.v1_block_table_warmup import (
|
||||
warm_v1_block_table_kernels,
|
||||
@@ -94,7 +97,6 @@ def kernel_warmup(worker: "Worker"):
|
||||
)
|
||||
|
||||
# Run next so input-prep kernels JIT against pristine runner state.
|
||||
sparse_mla_triton_warmup_if_needed(worker)
|
||||
flashinfer_sparse_mla_decode_autotune_warmup(worker)
|
||||
deepseek_v4_sparse_mla_attention_warmup(worker)
|
||||
|
||||
@@ -156,8 +158,15 @@ def kernel_warmup(worker: "Worker"):
|
||||
)
|
||||
|
||||
if worker.vllm_config.kernel_config.enable_cutedsl_warmup:
|
||||
# TODO(roberto): Remove after registered CuTeDSL warmups are migrated
|
||||
# to the shared JIT warmup infrastructure.
|
||||
# https://github.com/vllm-project/vllm/pull/47451
|
||||
cutedsl_warmup()
|
||||
|
||||
if worker.vllm_config.kernel_config.enable_jit_warmup:
|
||||
fa4_cutedsl_warmup(worker)
|
||||
sparse_mla_triton_warmup(worker)
|
||||
|
||||
|
||||
def _flashinfer_autotune_skip_ops(runner: "GPUModelRunner") -> set[str] | None:
|
||||
if envs.VLLM_FLASHINFER_AUTOTUNE_SKIP_OPS is not None:
|
||||
|
||||
@@ -174,12 +174,6 @@ def _warm_zero_kv_blocks_with_runner_zeroer(runner: object) -> bool:
|
||||
if not callable(zero_block_ids):
|
||||
return False
|
||||
|
||||
# With the extensible KV cache (V2), only a prefix of the blocks is
|
||||
# physically committed; make sure the blocks zeroed below are backed.
|
||||
ensure_kv_cache_blocks = getattr(runner, "ensure_kv_cache_blocks", None)
|
||||
if callable(ensure_kv_cache_blocks):
|
||||
ensure_kv_cache_blocks(max(_ZERO_KV_N_BLOCKS))
|
||||
|
||||
for n_blocks in _ZERO_KV_N_BLOCKS:
|
||||
zero_block_ids(list(range(n_blocks)))
|
||||
return True
|
||||
|
||||
@@ -4,11 +4,10 @@
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import torch
|
||||
|
||||
from vllm.logger import init_logger
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from vllm.config import VllmConfig
|
||||
from vllm.v1.worker.gpu_model_runner import GPUModelRunner
|
||||
from vllm.v1.worker.gpu_worker import Worker
|
||||
|
||||
@@ -31,47 +30,7 @@ _GENERIC_SPARSE_MLA_BACKENDS = frozenset(
|
||||
)
|
||||
_INDEXER_PREFILL_CHUNK_METADATA_BACKENDS = frozenset({"DEEPSEEK_V32_INDEXER"})
|
||||
|
||||
_SPARSE_PREFILL_METADATA_NUM_PREFILLS = (1, 2, 4, 8)
|
||||
_SPARSE_PREFILL_METADATA_NUM_DECODES = (0, 1, 2)
|
||||
_DSV4_PREFILL_CHUNK_METADATA_COMPRESS_RATIOS = (4, 128)
|
||||
_PREFILL_CHUNK_METADATA_SEQ_LEN_MULTIPLIERS = (2, 3)
|
||||
_PREFILL_CHUNK_METADATA_QUERY_SLICE_OFFSETS = (
|
||||
# query_slice_start offset, query_slice_stop offset
|
||||
(0, 0),
|
||||
(0, -1),
|
||||
(1, 0),
|
||||
(1, -1),
|
||||
)
|
||||
_COMBINE_TOPK_SWA_INPUT_VARIANTS = (
|
||||
# offset_topk, offset_query_and_seq, offset_gather
|
||||
(False, False, False),
|
||||
(False, True, False),
|
||||
(True, True, True),
|
||||
)
|
||||
_DSV4_COMBINE_TOPK_SWA_WARMUP_CASES = (
|
||||
# compress_ratio, topk, topk_width, N
|
||||
(1, 0, 512, 512),
|
||||
(4, 512, 512, 512 * 4),
|
||||
# DSv4-Pro C4A traffic uses top-k 1024 with N=1024.
|
||||
(4, 1024, 1024, 1024),
|
||||
(128, 8192, 8192, 8192 * 128),
|
||||
# Real C128A traffic also specializes N=1 in one call path.
|
||||
(128, 8192, 8192, 1),
|
||||
)
|
||||
|
||||
|
||||
def _clamp_warmup_tokens(num_tokens: int, max_tokens: int) -> int:
|
||||
return max(0, min(num_tokens, max_tokens))
|
||||
|
||||
|
||||
def _next_power_of_2(x: int) -> int:
|
||||
return 1 << (x - 1).bit_length()
|
||||
|
||||
|
||||
def _hf_config_int(runner: "GPUModelRunner", name: str, default: int) -> int:
|
||||
model_config = getattr(runner.vllm_config, "model_config", None)
|
||||
hf_config = getattr(model_config, "hf_config", None)
|
||||
return int(getattr(hf_config, name, default) or default)
|
||||
_INDEXER_PREFILL_CHUNK_METADATA_BACKENDS = frozenset({"DEEPSEEK_V32_INDEXER"})
|
||||
|
||||
|
||||
def _attention_backend_name(backend: object) -> str | None:
|
||||
@@ -96,242 +55,57 @@ def _has_attention_backend(
|
||||
return False
|
||||
|
||||
|
||||
def _warm_sparse_swa_prefill_metadata_kernel(
|
||||
device: torch.device,
|
||||
window_size: int,
|
||||
prefill_tokens: int,
|
||||
def _compile_sparse_swa_prefill_metadata_kernel(
|
||||
vllm_config: "VllmConfig",
|
||||
) -> None:
|
||||
from vllm.v1.attention.backends.mla.sparse_swa import (
|
||||
_compute_prefill_metadata_kernel,
|
||||
_COMPUTE_PREFILL_METADATA_KERNEL,
|
||||
)
|
||||
|
||||
for num_prefills in _SPARSE_PREFILL_METADATA_NUM_PREFILLS:
|
||||
for num_decodes in _SPARSE_PREFILL_METADATA_NUM_DECODES:
|
||||
query_lens = [1] * num_decodes
|
||||
query_lens += [prefill_tokens] * num_prefills
|
||||
query_start_locs = [0]
|
||||
for query_len in query_lens:
|
||||
query_start_locs.append(query_start_locs[-1] + query_len)
|
||||
query_start_loc = torch.tensor(
|
||||
query_start_locs,
|
||||
dtype=torch.int32,
|
||||
device=device,
|
||||
)
|
||||
seq_lens = torch.tensor(
|
||||
[1] * num_decodes + [window_size + q for q in query_lens[num_decodes:]],
|
||||
dtype=torch.int32,
|
||||
device=device,
|
||||
)
|
||||
prefill_gather_lens = torch.empty(
|
||||
num_prefills, dtype=torch.int32, device=device
|
||||
)
|
||||
_compute_prefill_metadata_kernel[(1,)](
|
||||
prefill_gather_lens,
|
||||
seq_lens,
|
||||
query_start_loc,
|
||||
num_prefills,
|
||||
num_decodes,
|
||||
window_size,
|
||||
BLOCK_SIZE=_next_power_of_2(num_prefills),
|
||||
)
|
||||
_COMPUTE_PREFILL_METADATA_KERNEL.warmup(vllm_config)
|
||||
|
||||
|
||||
def _warm_prefill_chunk_metadata_kernel(
|
||||
device: torch.device,
|
||||
compress_ratio: int,
|
||||
query_len: int,
|
||||
def _compile_prefill_chunk_metadata_kernel(
|
||||
vllm_config: "VllmConfig",
|
||||
) -> None:
|
||||
from vllm.v1.attention.backends.mla.indexer import build_prefill_chunk_metadata
|
||||
|
||||
num_reqs = 2
|
||||
query_start_loc_cpu = torch.arange(
|
||||
0, (num_reqs + 1) * query_len, query_len, dtype=torch.int32
|
||||
)
|
||||
query_start_loc = query_start_loc_cpu.to(device=device)
|
||||
|
||||
uncompressed_seq_lens_cpu = torch.tensor(
|
||||
[
|
||||
compress_ratio * multiplier + query_len
|
||||
for multiplier in _PREFILL_CHUNK_METADATA_SEQ_LEN_MULTIPLIERS
|
||||
],
|
||||
dtype=torch.int32,
|
||||
)
|
||||
compressed_seq_lens_cpu = uncompressed_seq_lens_cpu // compress_ratio
|
||||
uncompressed_seq_lens = uncompressed_seq_lens_cpu.to(device=device)
|
||||
compressed_seq_lens = compressed_seq_lens_cpu.to(device=device)
|
||||
block_table = torch.zeros(
|
||||
(num_reqs, int(compressed_seq_lens_cpu.max().item())),
|
||||
dtype=torch.int32,
|
||||
device=device,
|
||||
from vllm.v1.attention.backends.mla.indexer import (
|
||||
_BUILD_PREFILL_CHUNK_METADATA_KERNEL,
|
||||
)
|
||||
|
||||
offset_uncompressed_seq_lens = torch.empty(
|
||||
num_reqs + 1, dtype=torch.int32, device=device
|
||||
)[1:]
|
||||
offset_uncompressed_seq_lens.copy_(uncompressed_seq_lens)
|
||||
query_slices = tuple(
|
||||
slice(start, num_reqs * query_len + stop)
|
||||
for start, stop in _PREFILL_CHUNK_METADATA_QUERY_SLICE_OFFSETS
|
||||
)
|
||||
for warmup_uncompressed_seq_lens in (
|
||||
uncompressed_seq_lens,
|
||||
offset_uncompressed_seq_lens,
|
||||
):
|
||||
for query_slice in query_slices:
|
||||
build_prefill_chunk_metadata(
|
||||
0,
|
||||
num_reqs,
|
||||
query_start_loc,
|
||||
query_start_loc_cpu,
|
||||
warmup_uncompressed_seq_lens,
|
||||
compressed_seq_lens,
|
||||
compressed_seq_lens_cpu,
|
||||
block_table,
|
||||
compress_ratio,
|
||||
query_slice=query_slice,
|
||||
)
|
||||
_BUILD_PREFILL_CHUNK_METADATA_KERNEL.warmup(vllm_config)
|
||||
|
||||
|
||||
def _warm_combine_topk_swa_indices_kernel(
|
||||
device: torch.device,
|
||||
num_tokens: int,
|
||||
window_size: int,
|
||||
compress_ratio: int,
|
||||
topk: int,
|
||||
topk_width: int,
|
||||
n: int,
|
||||
def _compile_combine_topk_swa_indices_kernel(
|
||||
vllm_config: "VllmConfig",
|
||||
) -> None:
|
||||
from vllm.models.deepseek_v4.common.ops.cache_utils import combine_topk_swa_indices
|
||||
|
||||
if num_tokens <= 0:
|
||||
return
|
||||
|
||||
def _make_topk_indices(*, offset: bool) -> torch.Tensor:
|
||||
if offset:
|
||||
topk_storage = torch.full(
|
||||
(num_tokens * topk_width + 1,),
|
||||
-1,
|
||||
dtype=torch.int32,
|
||||
device=device,
|
||||
)
|
||||
topk_indices = topk_storage[1:].reshape(num_tokens, topk_width)
|
||||
else:
|
||||
topk_indices = torch.full(
|
||||
(num_tokens, topk_width), -1, dtype=torch.int32, device=device
|
||||
)
|
||||
if topk > 0:
|
||||
topk_indices.copy_(
|
||||
torch.arange(num_tokens * topk_width, dtype=torch.int32, device=device)
|
||||
.reshape(num_tokens, topk_width)
|
||||
.remainder(topk_width)
|
||||
)
|
||||
return topk_indices
|
||||
|
||||
query_start_loc = torch.tensor([0, num_tokens], dtype=torch.int32, device=device)
|
||||
seq_lens = torch.tensor(
|
||||
[window_size + num_tokens], dtype=torch.int32, device=device
|
||||
)
|
||||
gather_lens = torch.tensor(
|
||||
[min(window_size + num_tokens, window_size + num_tokens - 1)],
|
||||
dtype=torch.int32,
|
||||
device=device,
|
||||
)
|
||||
offset_query_start_loc = torch.empty(3, dtype=torch.int32, device=device)[1:]
|
||||
offset_query_start_loc.copy_(query_start_loc)
|
||||
offset_seq_lens = torch.empty(2, dtype=torch.int32, device=device)[1:]
|
||||
offset_seq_lens.copy_(seq_lens)
|
||||
offset_gather_lens = torch.empty(2, dtype=torch.int32, device=device)[1:]
|
||||
offset_gather_lens.copy_(gather_lens)
|
||||
|
||||
for (
|
||||
offset_topk,
|
||||
offset_query_and_seq,
|
||||
offset_gather,
|
||||
) in _COMBINE_TOPK_SWA_INPUT_VARIANTS:
|
||||
warmup_topk_indices = _make_topk_indices(offset=offset_topk)
|
||||
warmup_query_start_loc = (
|
||||
offset_query_start_loc if offset_query_and_seq else query_start_loc
|
||||
)
|
||||
warmup_seq_lens = offset_seq_lens if offset_query_and_seq else seq_lens
|
||||
warmup_gather_lens = offset_gather_lens if offset_gather else gather_lens
|
||||
n_values = (n,) if n == 1 else (n, n + 1)
|
||||
for m in (window_size + num_tokens, topk_width):
|
||||
for n_value in n_values:
|
||||
combine_topk_swa_indices(
|
||||
warmup_topk_indices,
|
||||
warmup_query_start_loc,
|
||||
warmup_seq_lens,
|
||||
warmup_gather_lens,
|
||||
window_size,
|
||||
compress_ratio,
|
||||
topk,
|
||||
M=m,
|
||||
N=n_value,
|
||||
)
|
||||
|
||||
|
||||
@torch.inference_mode()
|
||||
def sparse_mla_triton_warmup(
|
||||
runner: "GPUModelRunner",
|
||||
num_tokens: int,
|
||||
*,
|
||||
compress_ratios: tuple[int, ...],
|
||||
combine_topk_swa_cases: tuple[tuple[int, int, int, int], ...] = (),
|
||||
) -> None:
|
||||
device = getattr(runner, "device", torch.device("cuda"))
|
||||
window_size = _hf_config_int(runner, "sliding_window", 128)
|
||||
|
||||
_warm_sparse_swa_prefill_metadata_kernel(device, window_size, num_tokens)
|
||||
for compress_ratio in compress_ratios:
|
||||
_warm_prefill_chunk_metadata_kernel(device, compress_ratio, num_tokens)
|
||||
for compress_ratio, topk, topk_width, n in combine_topk_swa_cases:
|
||||
_warm_combine_topk_swa_indices_kernel(
|
||||
device,
|
||||
num_tokens,
|
||||
window_size,
|
||||
compress_ratio,
|
||||
topk,
|
||||
topk_width,
|
||||
n,
|
||||
)
|
||||
|
||||
|
||||
def deepseek_v4_sparse_triton_warmup(
|
||||
runner: "GPUModelRunner",
|
||||
num_tokens: int,
|
||||
) -> None:
|
||||
sparse_mla_triton_warmup(
|
||||
runner,
|
||||
num_tokens,
|
||||
compress_ratios=_DSV4_PREFILL_CHUNK_METADATA_COMPRESS_RATIOS,
|
||||
combine_topk_swa_cases=_DSV4_COMBINE_TOPK_SWA_WARMUP_CASES,
|
||||
from vllm.models.deepseek_v4.common.ops.cache_utils import (
|
||||
_COMBINE_TOPK_SWA_INDICES_KERNEL,
|
||||
)
|
||||
|
||||
_COMBINE_TOPK_SWA_INDICES_KERNEL.warmup(vllm_config)
|
||||
|
||||
def sparse_mla_triton_warmup_if_needed(worker: "Worker") -> None:
|
||||
|
||||
def sparse_mla_triton_warmup(worker: "Worker") -> None:
|
||||
runner = worker.model_runner
|
||||
if runner.is_pooling_model:
|
||||
return
|
||||
|
||||
max_tokens = worker.scheduler_config.max_num_batched_tokens
|
||||
num_tokens = _clamp_warmup_tokens(8, max_tokens)
|
||||
if num_tokens <= 0:
|
||||
max_num_prefills = min(worker.scheduler_config.max_num_seqs, max_tokens)
|
||||
if max_tokens <= 0 or max_num_prefills <= 0:
|
||||
return
|
||||
|
||||
vllm_config = runner.vllm_config
|
||||
try:
|
||||
if _has_attention_backend(runner, _DEEPSEEK_V4_SPARSE_MLA_BACKENDS):
|
||||
deepseek_v4_sparse_triton_warmup(runner, num_tokens)
|
||||
_compile_sparse_swa_prefill_metadata_kernel(vllm_config)
|
||||
_compile_prefill_chunk_metadata_kernel(vllm_config)
|
||||
_compile_combine_topk_swa_indices_kernel(vllm_config)
|
||||
elif _has_attention_backend(runner, _GENERIC_SPARSE_MLA_BACKENDS):
|
||||
sparse_mla_triton_warmup(
|
||||
runner,
|
||||
num_tokens,
|
||||
compress_ratios=(1,),
|
||||
)
|
||||
_compile_sparse_swa_prefill_metadata_kernel(vllm_config)
|
||||
_compile_prefill_chunk_metadata_kernel(vllm_config)
|
||||
elif _has_attention_backend(runner, _INDEXER_PREFILL_CHUNK_METADATA_BACKENDS):
|
||||
_warm_prefill_chunk_metadata_kernel(
|
||||
getattr(runner, "device", torch.device("cuda")),
|
||||
compress_ratio=1,
|
||||
query_len=num_tokens,
|
||||
)
|
||||
_compile_prefill_chunk_metadata_kernel(vllm_config)
|
||||
|
||||
except Exception:
|
||||
logger.warning("Skipping sparse MLA Triton warmup.", exc_info=True)
|
||||
|
||||
@@ -14,14 +14,23 @@ preparation.
|
||||
window indices for sparse prefill.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
import torch
|
||||
|
||||
from vllm.model_executor.layers.quantization.utils.quant_utils import (
|
||||
get_fp8_min_max,
|
||||
)
|
||||
from vllm.model_executor.warmup.jit_warmup import VllmJitKernel, zip_inputs
|
||||
from vllm.model_executor.warmup.jit_warmup_triton_helper import (
|
||||
TritonPointerInputVariant,
|
||||
TritonWarmupTensor,
|
||||
)
|
||||
from vllm.platforms import current_platform
|
||||
from vllm.triton_utils import tl, triton
|
||||
from vllm.utils.import_utils import has_cutedsl
|
||||
from vllm.utils.math_utils import next_power_of_2
|
||||
|
||||
|
||||
@triton.jit
|
||||
@@ -528,7 +537,6 @@ def combine_topk_swa_indices(
|
||||
N: int,
|
||||
) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
num_tokens = topk_indices.shape[0]
|
||||
num_reqs = seq_lens.shape[0]
|
||||
combined_topk = (
|
||||
(topk + window_size + _SPARSE_PREFILL_TOPK_ALIGNMENT - 1)
|
||||
// _SPARSE_PREFILL_TOPK_ALIGNMENT
|
||||
@@ -544,13 +552,10 @@ def combine_topk_swa_indices(
|
||||
num_tokens, dtype=torch.int32, device=topk_indices.device
|
||||
)
|
||||
|
||||
NUM_WORKERS = 128
|
||||
_combine_topk_swa_indices_kernel[(num_reqs, NUM_WORKERS)](
|
||||
_COMBINE_TOPK_SWA_INDICES_KERNEL(
|
||||
combined_indices,
|
||||
combined_indices.stride(0),
|
||||
combined_lens,
|
||||
topk_indices,
|
||||
topk_indices.stride(0),
|
||||
query_start_loc,
|
||||
seq_lens,
|
||||
gather_lens,
|
||||
@@ -559,82 +564,245 @@ def combine_topk_swa_indices(
|
||||
TOP_K=topk,
|
||||
COMPRESS_RATIO=compress_ratio,
|
||||
WINDOW_SIZE=window_size,
|
||||
PADDED_TOP_K=triton.next_power_of_2(topk_indices.shape[-1]),
|
||||
)
|
||||
return combined_indices, combined_lens
|
||||
|
||||
|
||||
@triton.jit
|
||||
def _combine_topk_swa_indices_kernel(
|
||||
combined_indices_ptr,
|
||||
combined_indices_stride,
|
||||
combined_lens_ptr,
|
||||
topk_indices_ptr,
|
||||
topk_indices_stride,
|
||||
query_start_loc_ptr,
|
||||
seq_lens_ptr,
|
||||
gather_lens_ptr,
|
||||
M,
|
||||
N,
|
||||
TOP_K: tl.constexpr,
|
||||
COMPRESS_RATIO: tl.constexpr,
|
||||
WINDOW_SIZE: tl.constexpr,
|
||||
PADDED_TOP_K: tl.constexpr,
|
||||
_COMBINE_TOPK_SWA_NUM_WORKERS = 128
|
||||
|
||||
|
||||
# Representative pointer alignment variants for Triton pointer specialization.
|
||||
_COMBINE_TOPK_SWA_POINTER_INPUTS = zip_inputs(
|
||||
dict(
|
||||
topk_indices=True,
|
||||
query_start_loc=True,
|
||||
seq_lens=True,
|
||||
gather_lens=True,
|
||||
),
|
||||
dict(
|
||||
topk_indices=True,
|
||||
query_start_loc=False,
|
||||
seq_lens=False,
|
||||
gather_lens=True,
|
||||
),
|
||||
dict(
|
||||
topk_indices=False,
|
||||
query_start_loc=False,
|
||||
seq_lens=False,
|
||||
gather_lens=False,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
_DSV4_COMBINE_TOPK_SWA_WARMUP_INPUTS = zip_inputs(
|
||||
# DSv4-Flash / SWA-only and C4A.
|
||||
dict(compress_ratio=1, topk=0, topk_width=512),
|
||||
dict(compress_ratio=4, topk=512, topk_width=512),
|
||||
# DSv4-Pro C4A.
|
||||
dict(compress_ratio=4, topk=1024, topk_width=1024),
|
||||
# DSv4-Pro C128A.
|
||||
dict(compress_ratio=128, topk=8192, topk_width=8192),
|
||||
)
|
||||
|
||||
|
||||
def _hf_config_int(vllm_config: Any, name: str, default: int) -> int:
|
||||
model_config = getattr(vllm_config, "model_config", None)
|
||||
hf_config = getattr(model_config, "hf_config", None)
|
||||
return int(getattr(hf_config, name, default) or default)
|
||||
|
||||
|
||||
def _scheduler_config_int(vllm_config: Any, name: str, default: int) -> int:
|
||||
scheduler_config = getattr(vllm_config, "scheduler_config", None)
|
||||
return int(getattr(scheduler_config, name, default) or default)
|
||||
|
||||
|
||||
class CombineTopkSwaIndicesKernel(
|
||||
VllmJitKernel["CombineTopkSwaIndicesKernel.CompileKey"]
|
||||
):
|
||||
batch_idx = tl.program_id(0)
|
||||
worker_id = tl.program_id(1)
|
||||
num_workers = tl.num_programs(1)
|
||||
@dataclass(frozen=True)
|
||||
class CompileKey:
|
||||
TOP_K: int
|
||||
COMPRESS_RATIO: int
|
||||
WINDOW_SIZE: int
|
||||
PADDED_TOP_K: int
|
||||
input_variant: TritonPointerInputVariant
|
||||
|
||||
# query_start_loc is a global tensor; rebase to chunk-local offsets
|
||||
# by subtracting the chunk's starting value.
|
||||
base = tl.load(query_start_loc_ptr)
|
||||
query_start = tl.load(query_start_loc_ptr + batch_idx) - base
|
||||
query_end = tl.load(query_start_loc_ptr + batch_idx + 1) - base
|
||||
query_len = query_end - query_start
|
||||
seq_len = tl.load(seq_lens_ptr + batch_idx)
|
||||
gather_len = tl.load(gather_lens_ptr + batch_idx)
|
||||
start_pos = seq_len - query_len
|
||||
# The SWA portion of the gathered buffer starts from position
|
||||
# (seq_len - gather_len), not position 0. We need this offset
|
||||
# to correctly index into the gathered buffer.
|
||||
gather_start = seq_len - gather_len
|
||||
@staticmethod
|
||||
@triton.jit(
|
||||
do_not_specialize=[
|
||||
"combined_indices_stride",
|
||||
"topk_indices_stride",
|
||||
"M",
|
||||
"N",
|
||||
]
|
||||
)
|
||||
def kernel(
|
||||
combined_indices_ptr,
|
||||
combined_indices_stride,
|
||||
combined_lens_ptr,
|
||||
topk_indices_ptr,
|
||||
topk_indices_stride,
|
||||
query_start_loc_ptr,
|
||||
seq_lens_ptr,
|
||||
gather_lens_ptr,
|
||||
M,
|
||||
N,
|
||||
TOP_K: tl.constexpr,
|
||||
COMPRESS_RATIO: tl.constexpr,
|
||||
WINDOW_SIZE: tl.constexpr,
|
||||
PADDED_TOP_K: tl.constexpr,
|
||||
):
|
||||
batch_idx = tl.program_id(0)
|
||||
worker_id = tl.program_id(1)
|
||||
num_workers = tl.num_programs(1)
|
||||
|
||||
for token_idx in range(query_start + worker_id, query_end, num_workers):
|
||||
# topk_len is fully determined by the query token's absolute position:
|
||||
# both the C4A indexer and the C128A metadata builder emit
|
||||
# min((pos + 1) // compress_ratio, topk_tokens) valid entries.
|
||||
# Caller passes TOP_K=0 for SWA-only layers to zero this out.
|
||||
token_idx_in_query = token_idx - query_start
|
||||
pos = start_pos + token_idx_in_query
|
||||
topk_len = tl.minimum((pos + 1) // COMPRESS_RATIO, TOP_K)
|
||||
swa_len = tl.minimum(pos + 1, WINDOW_SIZE)
|
||||
# query_start_loc is a global tensor; rebase to chunk-local offsets
|
||||
# by subtracting the chunk's starting value.
|
||||
base = tl.load(query_start_loc_ptr)
|
||||
query_start = tl.load(query_start_loc_ptr + batch_idx) - base
|
||||
query_end = tl.load(query_start_loc_ptr + batch_idx + 1) - base
|
||||
query_len = query_end - query_start
|
||||
seq_len = tl.load(seq_lens_ptr + batch_idx)
|
||||
gather_len = tl.load(gather_lens_ptr + batch_idx)
|
||||
start_pos = seq_len - query_len
|
||||
# The SWA portion of the gathered buffer starts from position
|
||||
# (seq_len - gather_len), not position 0. We need this offset
|
||||
# to correctly index into the gathered buffer.
|
||||
gather_start = seq_len - gather_len
|
||||
|
||||
offset = tl.arange(0, PADDED_TOP_K)
|
||||
mask = offset < topk_len
|
||||
topk_indices = tl.load(
|
||||
topk_indices_ptr + token_idx * topk_indices_stride + offset,
|
||||
mask=mask,
|
||||
for token_idx in range(query_start + worker_id, query_end, num_workers):
|
||||
# topk_len is fully determined by the query token's absolute position:
|
||||
# both the C4A indexer and the C128A metadata builder emit
|
||||
# min((pos + 1) // compress_ratio, topk_tokens) valid entries.
|
||||
# Caller passes TOP_K=0 for SWA-only layers to zero this out.
|
||||
token_idx_in_query = token_idx - query_start
|
||||
pos = start_pos + token_idx_in_query
|
||||
topk_len = tl.minimum((pos + 1) // COMPRESS_RATIO, TOP_K)
|
||||
swa_len = tl.minimum(pos + 1, WINDOW_SIZE)
|
||||
|
||||
offset = tl.arange(0, PADDED_TOP_K)
|
||||
mask = offset < topk_len
|
||||
topk_indices = tl.load(
|
||||
topk_indices_ptr + token_idx * topk_indices_stride + offset,
|
||||
mask=mask,
|
||||
)
|
||||
tl.store(
|
||||
combined_indices_ptr + token_idx * combined_indices_stride + offset,
|
||||
topk_indices + M * batch_idx,
|
||||
mask=mask,
|
||||
)
|
||||
offset = tl.arange(0, WINDOW_SIZE)
|
||||
# Index into gathered buffer: N + (position - gather_start)
|
||||
# For positions [pos - swa_len + 1, pos], the buffer indices are:
|
||||
# [N + pos - swa_len + 1 - gather_start, N + pos - gather_start]
|
||||
tl.store(
|
||||
combined_indices_ptr
|
||||
+ token_idx * combined_indices_stride
|
||||
+ topk_len
|
||||
+ offset,
|
||||
M * batch_idx + N + offset + pos - swa_len + 1 - gather_start,
|
||||
mask=offset < swa_len,
|
||||
)
|
||||
|
||||
combined_len = topk_len + swa_len
|
||||
tl.store(combined_lens_ptr + token_idx, combined_len)
|
||||
|
||||
def dispatch( # type: ignore[override]
|
||||
self,
|
||||
*,
|
||||
topk_width: int,
|
||||
topk_indices: bool,
|
||||
query_start_loc: bool,
|
||||
seq_lens: bool,
|
||||
gather_lens: bool,
|
||||
topk: int,
|
||||
compress_ratio: int,
|
||||
WINDOW_SIZE: int,
|
||||
) -> CompileKey:
|
||||
padded_topk = next_power_of_2(topk_width)
|
||||
input_variant = TritonPointerInputVariant.from_alignment(
|
||||
topk_indices=topk_indices,
|
||||
query_start_loc=query_start_loc,
|
||||
seq_lens=seq_lens,
|
||||
gather_lens=gather_lens,
|
||||
)
|
||||
tl.store(
|
||||
combined_indices_ptr + token_idx * combined_indices_stride + offset,
|
||||
topk_indices + M * batch_idx,
|
||||
mask=mask,
|
||||
)
|
||||
offset = tl.arange(0, WINDOW_SIZE)
|
||||
# Index into gathered buffer: N + (position - gather_start)
|
||||
# For positions [pos - swa_len + 1, pos], the buffer indices are:
|
||||
# [N + pos - swa_len + 1 - gather_start, N + pos - gather_start]
|
||||
tl.store(
|
||||
combined_indices_ptr
|
||||
+ token_idx * combined_indices_stride
|
||||
+ topk_len
|
||||
+ offset,
|
||||
M * batch_idx + N + offset + pos - swa_len + 1 - gather_start,
|
||||
mask=offset < swa_len,
|
||||
return self.CompileKey(
|
||||
TOP_K=topk,
|
||||
COMPRESS_RATIO=compress_ratio,
|
||||
WINDOW_SIZE=WINDOW_SIZE,
|
||||
PADDED_TOP_K=padded_topk,
|
||||
input_variant=input_variant,
|
||||
)
|
||||
|
||||
combined_len = topk_len + swa_len
|
||||
tl.store(combined_lens_ptr + token_idx, combined_len)
|
||||
def get_warmup_keys(self, vllm_config: Any) -> list[CompileKey]:
|
||||
if _scheduler_config_int(vllm_config, "max_num_batched_tokens", 0) <= 0:
|
||||
return []
|
||||
|
||||
window_size = _hf_config_int(vllm_config, "sliding_window", 128)
|
||||
return self._trace_dispatch(self.dispatch)(
|
||||
_DSV4_COMBINE_TOPK_SWA_WARMUP_INPUTS,
|
||||
_COMBINE_TOPK_SWA_POINTER_INPUTS,
|
||||
WINDOW_SIZE=window_size,
|
||||
)
|
||||
|
||||
def compile(self, compile_key: CompileKey) -> None:
|
||||
warmup = getattr(self.kernel, "warmup", None)
|
||||
assert warmup is not None
|
||||
int32_ptr = TritonWarmupTensor(torch.int32)
|
||||
input_variant = compile_key.input_variant
|
||||
warmup(
|
||||
int32_ptr,
|
||||
1, # do not specialize combined_indices_stride
|
||||
int32_ptr,
|
||||
input_variant.pointer("topk_indices", torch.int32),
|
||||
1, # do not specialize topk_indices_stride
|
||||
input_variant.pointer("query_start_loc", torch.int32),
|
||||
input_variant.pointer("seq_lens", torch.int32),
|
||||
input_variant.pointer("gather_lens", torch.int32),
|
||||
1, # do not specialize M
|
||||
1, # do not specialize N
|
||||
TOP_K=compile_key.TOP_K,
|
||||
COMPRESS_RATIO=compile_key.COMPRESS_RATIO,
|
||||
WINDOW_SIZE=compile_key.WINDOW_SIZE,
|
||||
PADDED_TOP_K=compile_key.PADDED_TOP_K,
|
||||
grid=(1, _COMBINE_TOPK_SWA_NUM_WORKERS),
|
||||
)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
combined_indices: torch.Tensor,
|
||||
combined_lens: torch.Tensor,
|
||||
topk_indices: torch.Tensor,
|
||||
query_start_loc: torch.Tensor,
|
||||
seq_lens: torch.Tensor,
|
||||
gather_lens: torch.Tensor,
|
||||
M: int,
|
||||
N: int,
|
||||
*,
|
||||
TOP_K: int,
|
||||
COMPRESS_RATIO: int,
|
||||
WINDOW_SIZE: int,
|
||||
) -> None:
|
||||
num_reqs = seq_lens.shape[0]
|
||||
self.kernel[(num_reqs, _COMBINE_TOPK_SWA_NUM_WORKERS)](
|
||||
combined_indices,
|
||||
combined_indices.stride(0),
|
||||
combined_lens,
|
||||
topk_indices,
|
||||
topk_indices.stride(0),
|
||||
query_start_loc,
|
||||
seq_lens,
|
||||
gather_lens,
|
||||
M,
|
||||
N,
|
||||
TOP_K=TOP_K,
|
||||
COMPRESS_RATIO=COMPRESS_RATIO,
|
||||
WINDOW_SIZE=WINDOW_SIZE,
|
||||
PADDED_TOP_K=next_power_of_2(topk_indices.shape[-1]),
|
||||
)
|
||||
|
||||
|
||||
_COMBINE_TOPK_SWA_INDICES_KERNEL = CombineTopkSwaIndicesKernel()
|
||||
|
||||
|
||||
def build_flashinfer_mixed_sparse_indices(
|
||||
|
||||
@@ -476,15 +476,17 @@ class MediaConnector:
|
||||
self,
|
||||
image_url: str,
|
||||
*,
|
||||
image_mode: str = "RGB",
|
||||
image_mode: str | None = "RGB",
|
||||
) -> Image.Image:
|
||||
"""
|
||||
Load a PIL image from an HTTP or base64 data URL.
|
||||
|
||||
By default, the image is converted into RGB format.
|
||||
By default, the image is converted into RGB format. Set
|
||||
`media_io_kwargs={"image": {"image_mode": None}}` to keep the
|
||||
original image mode (e.g. preserving the alpha channel).
|
||||
"""
|
||||
image_io = ImageMediaIO(
|
||||
image_mode=image_mode, **self.media_io_kwargs.get("image", {})
|
||||
**({"image_mode": image_mode} | self.media_io_kwargs.get("image", {}))
|
||||
)
|
||||
|
||||
try:
|
||||
@@ -501,15 +503,17 @@ class MediaConnector:
|
||||
self,
|
||||
image_url: str,
|
||||
*,
|
||||
image_mode: str = "RGB",
|
||||
image_mode: str | None = "RGB",
|
||||
) -> Image.Image:
|
||||
"""
|
||||
Asynchronously load a PIL image from an HTTP or base64 data URL.
|
||||
|
||||
By default, the image is converted into RGB format.
|
||||
By default, the image is converted into RGB format. Set
|
||||
`media_io_kwargs={"image": {"image_mode": None}}` to keep the
|
||||
original image mode (e.g. preserving the alpha channel).
|
||||
"""
|
||||
image_io = ImageMediaIO(
|
||||
image_mode=image_mode, **self.media_io_kwargs.get("image", {})
|
||||
**({"image_mode": image_mode} | self.media_io_kwargs.get("image", {}))
|
||||
)
|
||||
|
||||
try:
|
||||
@@ -526,14 +530,14 @@ class MediaConnector:
|
||||
self,
|
||||
video_url: str,
|
||||
*,
|
||||
image_mode: str = "RGB",
|
||||
image_mode: str | None = "RGB",
|
||||
video_processor: str | None = None,
|
||||
) -> tuple[npt.NDArray, dict[str, Any]]:
|
||||
"""
|
||||
Load video from an HTTP or base64 data URL.
|
||||
"""
|
||||
image_io = ImageMediaIO(
|
||||
image_mode=image_mode, **self.media_io_kwargs.get("image", {})
|
||||
**({"image_mode": image_mode} | self.media_io_kwargs.get("image", {}))
|
||||
)
|
||||
video_io_kwargs = dict(self.media_io_kwargs.get("video", {}))
|
||||
if "video_backend" not in video_io_kwargs and (
|
||||
@@ -552,16 +556,18 @@ class MediaConnector:
|
||||
self,
|
||||
video_url: str,
|
||||
*,
|
||||
image_mode: str = "RGB",
|
||||
image_mode: str | None = "RGB",
|
||||
video_processor: str | None = None,
|
||||
) -> tuple[npt.NDArray, dict[str, Any]]:
|
||||
"""
|
||||
Asynchronously load video from an HTTP or base64 data URL.
|
||||
|
||||
By default, the image is converted into RGB format.
|
||||
By default, the image is converted into RGB format. Set
|
||||
`media_io_kwargs={"image": {"image_mode": None}}` to keep the
|
||||
original image mode (e.g. preserving the alpha channel).
|
||||
"""
|
||||
image_io = ImageMediaIO(
|
||||
image_mode=image_mode, **self.media_io_kwargs.get("image", {})
|
||||
**({"image_mode": image_mode} | self.media_io_kwargs.get("image", {}))
|
||||
)
|
||||
video_io_kwargs = dict(self.media_io_kwargs.get("video", {}))
|
||||
if "video_backend" not in video_io_kwargs and (
|
||||
|
||||
@@ -25,9 +25,11 @@ class ImageMediaIO(MediaIO[Image.Image]):
|
||||
error handling.
|
||||
"""
|
||||
|
||||
def __init__(self, image_mode: str = "RGB", **kwargs) -> None:
|
||||
def __init__(self, image_mode: str | None = "RGB", **kwargs) -> None:
|
||||
super().__init__()
|
||||
|
||||
# Target mode for loaded images; `None` keeps the original mode
|
||||
# (i.e. no conversion, alpha channel is preserved as-is).
|
||||
self.image_mode = image_mode
|
||||
# `kwargs` contains custom arguments from
|
||||
# --media-io-kwargs for this modality, merged with
|
||||
@@ -62,7 +64,7 @@ class ImageMediaIO(MediaIO[Image.Image]):
|
||||
"""Convert image mode with custom background color."""
|
||||
if isinstance(image, MediaWithBytes):
|
||||
image = image.media
|
||||
if image.mode == self.image_mode:
|
||||
if self.image_mode is None or image.mode == self.image_mode:
|
||||
return image
|
||||
elif image.mode == "RGBA" and self.image_mode == "RGB":
|
||||
return rgba_to_rgb(image, self.rgba_background_color)
|
||||
|
||||
@@ -58,13 +58,14 @@ def encode_audio_url(
|
||||
def encode_image_base64(
|
||||
image: Image.Image,
|
||||
*,
|
||||
image_mode: str = "RGB",
|
||||
image_mode: str | None = "RGB",
|
||||
format: str = "PNG",
|
||||
) -> str:
|
||||
"""
|
||||
Encode a pillow image to base64 format.
|
||||
|
||||
By default, the image is converted into RGB format before being encoded.
|
||||
Pass `image_mode=None` to keep the original image mode.
|
||||
"""
|
||||
image_io = ImageMediaIO(image_mode=image_mode)
|
||||
return image_io.encode_base64(image, image_format=format)
|
||||
@@ -73,13 +74,14 @@ def encode_image_base64(
|
||||
def encode_image_url(
|
||||
image: Image.Image,
|
||||
*,
|
||||
image_mode: str = "RGB",
|
||||
image_mode: str | None = "RGB",
|
||||
format: str = "PNG",
|
||||
) -> str:
|
||||
"""
|
||||
Encode a pillow image as a data URL.
|
||||
|
||||
By default, the image is converted into RGB format before being encoded.
|
||||
Pass `image_mode=None` to keep the original image mode.
|
||||
"""
|
||||
image_b64 = encode_image_base64(image, image_mode=image_mode, format=format)
|
||||
mimetype = mimetypes.types_map.get("." + format.lower(), "image")
|
||||
|
||||
+62
-1
@@ -14,6 +14,7 @@ from datetime import timedelta
|
||||
from functools import cache, lru_cache, wraps
|
||||
from typing import TYPE_CHECKING, NamedTuple, TypeVar
|
||||
|
||||
import regex as re
|
||||
import torch
|
||||
from torch.distributed import PrefixStore, ProcessGroup
|
||||
from torch.distributed.distributed_c10d import is_nccl_available
|
||||
@@ -48,6 +49,8 @@ _R = TypeVar("_R")
|
||||
|
||||
pynvml = import_pynvml()
|
||||
|
||||
_COMPILED_CUDA_ARCH_RE = re.compile(r"^(?P<major>\d+)\.(?P<minor>\d+)(?P<kind>[af])?$")
|
||||
|
||||
# pytorch 2.5 uses cudnn sdpa by default, which will cause crash on some models
|
||||
# see https://github.com/huggingface/diffusers/issues/9704 for details
|
||||
torch.backends.cuda.enable_cudnn_sdp(False)
|
||||
@@ -282,6 +285,62 @@ class CudaPlatformBase(Platform):
|
||||
def log_warnings(cls):
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def _compiled_arch_covers_device(arch: str, capability: DeviceCapability) -> bool:
|
||||
match = _COMPILED_CUDA_ARCH_RE.match(arch)
|
||||
if match is None:
|
||||
return False
|
||||
|
||||
major = int(match.group("major"))
|
||||
minor = int(match.group("minor"))
|
||||
kind = match.group("kind")
|
||||
|
||||
if kind == "f":
|
||||
return major == capability.major
|
||||
return major == capability.major and minor == capability.minor
|
||||
|
||||
@classmethod
|
||||
def _warn_if_device_arch_not_compiled(cls) -> None:
|
||||
try:
|
||||
compiled_archs_str = torch.ops._C.get_compiled_cuda_archs()
|
||||
except (AttributeError, RuntimeError):
|
||||
return
|
||||
|
||||
compiled_archs = [
|
||||
arch.strip() for arch in compiled_archs_str.split(",") if arch.strip()
|
||||
]
|
||||
if not compiled_archs:
|
||||
return
|
||||
|
||||
unsupported_devices: list[str] = []
|
||||
for device_id in range(cls.device_count()):
|
||||
capability = cls.get_device_capability(device_id)
|
||||
if capability is None:
|
||||
continue
|
||||
if any(
|
||||
cls._compiled_arch_covers_device(arch, capability)
|
||||
for arch in compiled_archs
|
||||
):
|
||||
continue
|
||||
device_name = cls.get_device_name(device_id)
|
||||
unsupported_devices.append(
|
||||
f"{device_id}: {device_name} (compute capability "
|
||||
f"{capability.as_version_str()})"
|
||||
)
|
||||
|
||||
if unsupported_devices:
|
||||
logger.warning_once(
|
||||
"The current vLLM wheel was built for CUDA architectures %s, "
|
||||
"but the following visible CUDA device(s) are not covered: %s. "
|
||||
"This may cause CUDA kernel launch failures or force slower "
|
||||
"fallback paths. Note that CUDA 13 family-specific targets "
|
||||
"such as 10.0f and 12.0f cover the corresponding major-version "
|
||||
"GPU family, while architecture-specific targets such as 12.1a "
|
||||
"cover only that exact compute capability.",
|
||||
", ".join(compiled_archs),
|
||||
"; ".join(unsupported_devices),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def is_pin_memory_available(cls) -> bool:
|
||||
if in_wsl():
|
||||
@@ -290,7 +349,8 @@ class CudaPlatformBase(Platform):
|
||||
# kernel with limited pinned memory support for CUDA.
|
||||
version = _get_wsl_kernel_version()
|
||||
if version is None or version < (4, 19, 121):
|
||||
logger.warning_once(
|
||||
# warning_once() causes a circular import on WSL, see #48397.
|
||||
logger.warning(
|
||||
"Using 'pin_memory=False' as WSL is detected and the "
|
||||
"WSL2 kernel version is below 4.19.121. This may slow "
|
||||
"down performance. Please run `wsl --update`."
|
||||
@@ -948,6 +1008,7 @@ class NvmlCudaPlatform(CudaPlatformBase):
|
||||
@classmethod
|
||||
@with_nvml_context
|
||||
def log_warnings(cls):
|
||||
cls._warn_if_device_arch_not_compiled()
|
||||
device_ids: int = pynvml.nvmlDeviceGetCount()
|
||||
if device_ids > 1:
|
||||
device_names = [cls._get_physical_device_name(i) for i in range(device_ids)]
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user