Compare commits

...
Author SHA1 Message Date
khluu 67c74e5980 p
Signed-off-by: khluu <khluu000@gmail.com>
2026-05-01 14:47:49 -07:00
khluu 455839ec66 p
Signed-off-by: khluu <khluu000@gmail.com>
2026-05-01 14:28:31 -07:00
Yongye Zhuandkhluu 513b5f82dd [DSV4] Add knob to enable pre-attn gemm (#41443)
Signed-off-by: Yongye Zhu <zyy1102000@gmail.com>
(cherry picked from commit bcf5cac9fb)
2026-05-01 12:24:17 -07:00
khluu 135722d58d Revert "."
This reverts commit a5e0ea09ea.
2026-05-01 12:23:33 -07:00
Rohan Potdarandkhluu 7758daca7c [ROCm][Bugfix][GPTOSS]: fix input_ids and expert_map args for quark w4a8 gptoss (#41165)
Signed-off-by: Rohan138 <rohanpotdar138@gmail.com>
(cherry picked from commit 3795d7acf4)
2026-05-01 12:17:45 -07:00
Isotr0pyandkhluu 5f79634b3b [Perf] Intergrate Tile Kernels head_compute_mix_kernel for Deepseek-V4 (#41255)
Signed-off-by: Isotr0py <Isotr0py@outlook.com>
Co-authored-by: Roger Wang <hey@rogerw.io>
(cherry picked from commit a9484dac7b)
2026-05-01 12:17:28 -07:00
Bugen Zhaoandkhluu 05adde9955 [Bugfix] Pass reasoning parser kwargs to structured output (#41199)
Signed-off-by: Bugen Zhao <i@bugenzhao.com>
Co-authored-by: wang.yuqi <yuqi.wang@daocloud.io>
(cherry picked from commit a07642667d)
2026-05-01 12:14:44 -07:00
Jiangyun Zhuandkhluu 9cdcda5fc2 [Bugfix] BailingMoeV2.5: rotate full qk_rope_head_dim in MLA RoPE (#41185)
Signed-off-by: zjy0516 <riverclouds.zhu@qq.com>
(cherry picked from commit 6d7d4da99e)
2026-05-01 12:13:50 -07:00
JiangWeixiangandkhluu a5d4a264dd [Bugfix ] fix bailing_moe_linear (#40859)
Signed-off-by: ghphotoframe <854746559@qq.com>
(cherry picked from commit ed57f77192)
2026-05-01 12:13:41 -07:00
Yongye Zhuandkhluu a5e0ea09ea .
Signed-off-by: Yongye Zhu <zyy1102000@gmail.com>

(cherry picked from commit db0c9d52d4)

Signed-off-by: khluu <khluu000@gmail.com>
2026-04-30 23:16:02 -07:00
Yongye Zhuandkhluu 28a1a85d89 [DSV4] Add BF16 and MXFP8 A2A support for flashinfer a2a one sided (#40960)
Signed-off-by: Yongye Zhu <zyy1102000@gmail.com>
Signed-off-by: Zijing Liu <liuzijing2014@gmail.com>
Co-authored-by: Zijing Liu <liuzijing2014@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
(cherry picked from commit b4806c8ee1)
2026-04-30 21:39:14 -07:00
Yongye Zhuandkhluu 82ccd9f3b2 [Bugfix] Fix persistent_topk cooperative deadlock at TopK=1024 (#41189)
Signed-off-by: Yongye Zhu <zyy1102000@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
(cherry picked from commit a749a33d8d)
2026-04-30 21:39:14 -07:00
Thien Tranandkhluu d9f3961abb [DSv4] Use cvt PTX for FP32->FP4 conversion (#41015)
Signed-off-by: Thien Tran <gau.nernst@yahoo.com.sg>
(cherry picked from commit 296741d025)
2026-04-30 21:39:14 -07:00
Jee Jee Liandkhluu 4fee1390f4 [Bugfix] Fix repeated DSv4 RoPE cache initialization (#41148)
Signed-off-by: Jee Jee Li <pandaleefree@gmail.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
(cherry picked from commit 9d8ad5b408)
2026-04-30 21:39:14 -07:00
Chaunceyandkhluu bf590b6a24 [Bugfix] DSV32/V4 add missing type conversion for non-streaming tool calls (#41198)
Signed-off-by: chaunceyjiang <chaunceyjiang@gmail.com>
(cherry picked from commit 762022cafb)
2026-04-30 21:39:13 -07:00
Wei Zhaoandkhluu 66df2c03f5 [Bugfix] Fix max_num_batched_token not captured in cuda graph (#40734)
Signed-off-by: wzhao18 <wzhao18.sz@gmail.com>
Signed-off-by: Wei Zhao <51183510+wzhao18@users.noreply.github.com>
Co-authored-by: Wei Zhao (Engrg-Hardware 1) <weizha@login-bia02.bia.clusters.nvidia.com>
(cherry picked from commit 8b49cf3a37)
2026-04-30 21:39:13 -07:00
Jiangyun Zhuandkhluu 17896d817b [Bugfix] fix inductor error for dpsk v4 (#41135)
Signed-off-by: zjy0516 <riverclouds.zhu@qq.com>
(cherry picked from commit 2ae73c758c)
2026-04-30 21:39:13 -07:00
Wei Zhaoandkhluu 1a131a066c [Bugfix] Fix Deepseek V4 import error due to AOT compile cache loading (#41090)
Signed-off-by: wzhao18 <wzhao18.sz@gmail.com>
Signed-off-by: Wei Zhao <51183510+wzhao18@users.noreply.github.com>
(cherry picked from commit 803b9d7881)
2026-04-30 21:39:13 -07:00
qizixiandkhluu 6fffb0b8c8 [DSV4] Align aux stream API with DeepseekV4DecoderLayer (#41171)
Signed-off-by: zixi-qi <zixi@inferact.ai>
(cherry picked from commit 6fb3f7b46b)
2026-04-30 21:39:13 -07:00
Nick Hillandkhluu a076bd1e67 [Core] Account for num_gpu_blocks_override in max_model_len checks (#41069)
Signed-off-by: Nick Hill <nickhill123@gmail.com>
(cherry picked from commit e68fa1b90a)
2026-04-30 21:39:13 -07:00
Yongye Zhuandkhluu c244b6fc83 [DSV4] Enable Multi-stream for Pre-Attn GEMM (#41061)
Signed-off-by: Yongye Zhu <zyy1102000@gmail.com>
(cherry picked from commit 5aa371dc8e)
2026-04-30 21:39:13 -07:00
Jee Jee Liandkhluu 15cc87d2fe [Model][DSV4] Support base model (#41006)
Signed-off-by: Jee Jee Li <pandaleefree@gmail.com>
(cherry picked from commit 2c8b76c5cb)
2026-04-30 21:39:13 -07:00
youkaichaoandkhluu c68fa39ced Auto-disable expandable_segments around cumem memory pool (#40812)
Signed-off-by: youkaichao <youkaichao@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
(cherry picked from commit 2ce95a761b)
2026-04-30 21:39:13 -07:00
Yongye ZhuandGitHub 05ebca5250 Temporary disable persistent topk (#41442)
Signed-off-by: Yongye Zhu <zyy1102000@gmail.com>
2026-04-30 20:37:19 -07:00
Zhewen Liandkhluu 88d34c6409 [Docker] Install numactl CLI in CUDA runtime image (#41032)
Signed-off-by: Zhewen Li <zhewenli@inferact.ai>
Co-authored-by: Zhewen Li <zhewenli@inferact.ai>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
(cherry picked from commit 6232fb4b66)
2026-04-27 12:04:32 -07:00
Yongye Zhuandkhluu b8160878f0 [DSV4] Add silu clamp limit to shared expert (#40950)
Signed-off-by: Yongye Zhu <zyy1102000@gmail.com>
(cherry picked from commit 706a04d34b)
2026-04-27 00:40:46 -07:00
Dao007foreverandkhluu 84c276d7ea [Bugfix] Cap SWA/chunked-local runtime admission to startup pool-sizing bound (#40946)
Signed-off-by: Dao Le <Dao007forever@gmail.com>
Signed-off-by: Nick Hill <nickhill123@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Nick Hill <nickhill123@gmail.com>
(cherry picked from commit 7b1bc0a3eb)
2026-04-26 21:43:00 -07:00
khluu 5eb3657578 Revert "[Frontend] Remove frontend pooling multi task support. (#37861)"
This reverts commit d2e2e856ad.
2026-04-26 21:42:15 -07:00
90 changed files with 2257 additions and 792 deletions
+82 -25
View File
@@ -11,29 +11,74 @@
namespace vllm { namespace vllm {
template <typename scalar_t, scalar_t (*ACT_FN)(const scalar_t&), template <typename scalar_t, scalar_t (*ACT_FN)(const scalar_t&),
bool act_first> bool act_first, bool HAS_CLAMP>
__device__ __forceinline__ scalar_t compute(const scalar_t& x, __device__ __forceinline__ scalar_t compute(const scalar_t& x,
const scalar_t& y) { const scalar_t& y,
return act_first ? ACT_FN(x) * y : x * ACT_FN(y); const float limit) {
if constexpr (act_first) {
scalar_t gate = x;
scalar_t up = y;
if constexpr (HAS_CLAMP) {
gate = (scalar_t)fminf((float)gate, limit);
up = (scalar_t)fmaxf(fminf((float)up, limit), -limit);
}
return ACT_FN(gate) * up;
} else {
scalar_t gate = x;
scalar_t up = y;
if constexpr (HAS_CLAMP) {
gate = (scalar_t)fmaxf(fminf((float)gate, limit), -limit);
up = (scalar_t)fminf((float)up, limit);
}
return gate * ACT_FN(up);
}
} }
template <typename packed_t, packed_t (*PACKED_ACT_FN)(const packed_t&), template <typename packed_t, packed_t (*PACKED_ACT_FN)(const packed_t&),
bool act_first> bool act_first, bool HAS_CLAMP>
__device__ __forceinline__ packed_t packed_compute(const packed_t& x, __device__ __forceinline__ packed_t packed_compute(const packed_t& x,
const packed_t& y) { const packed_t& y,
return act_first ? packed_mul(PACKED_ACT_FN(x), y) const float limit) {
: packed_mul(x, PACKED_ACT_FN(y)); if constexpr (act_first) {
packed_t gate = x;
packed_t up = y;
if constexpr (HAS_CLAMP) {
float2 g = cast_to_float2(gate);
float2 u = cast_to_float2(up);
g.x = fminf(g.x, limit);
g.y = fminf(g.y, limit);
u.x = fmaxf(fminf(u.x, limit), -limit);
u.y = fmaxf(fminf(u.y, limit), -limit);
gate = cast_to_packed<packed_t>(g);
up = cast_to_packed<packed_t>(u);
}
return packed_mul(PACKED_ACT_FN(gate), up);
} else {
packed_t gate = x;
packed_t up = y;
if constexpr (HAS_CLAMP) {
float2 g = cast_to_float2(gate);
float2 u = cast_to_float2(up);
g.x = fmaxf(fminf(g.x, limit), -limit);
g.y = fmaxf(fminf(g.y, limit), -limit);
u.x = fminf(u.x, limit);
u.y = fminf(u.y, limit);
gate = cast_to_packed<packed_t>(g);
up = cast_to_packed<packed_t>(u);
}
return packed_mul(gate, PACKED_ACT_FN(up));
}
} }
// Activation and gating kernel template. // Activation and gating kernel template.
template <typename scalar_t, typename packed_t, template <typename scalar_t, typename packed_t,
scalar_t (*ACT_FN)(const scalar_t&), scalar_t (*ACT_FN)(const scalar_t&),
packed_t (*PACKED_ACT_FN)(const packed_t&), bool act_first, packed_t (*PACKED_ACT_FN)(const packed_t&), bool act_first,
bool use_vec, bool use_256b = false> bool use_vec, bool HAS_CLAMP, bool use_256b = false>
__global__ void act_and_mul_kernel( __global__ void act_and_mul_kernel(
scalar_t* __restrict__ out, // [..., d] scalar_t* __restrict__ out, // [..., d]
const scalar_t* __restrict__ input, // [..., 2, d] const scalar_t* __restrict__ input, // [..., 2, d]
const int d) { const int d, const float limit) {
const scalar_t* x_ptr = input + blockIdx.x * 2 * d; const scalar_t* x_ptr = input + blockIdx.x * 2 * d;
const scalar_t* y_ptr = x_ptr + d; const scalar_t* y_ptr = x_ptr + d;
scalar_t* out_ptr = out + blockIdx.x * d; scalar_t* out_ptr = out + blockIdx.x * d;
@@ -58,8 +103,9 @@ __global__ void act_and_mul_kernel(
} }
#pragma unroll #pragma unroll
for (int j = 0; j < pvec_t::NUM_ELTS; j++) { for (int j = 0; j < pvec_t::NUM_ELTS; j++) {
x.elts[j] = packed_compute<packed_t, PACKED_ACT_FN, act_first>( x.elts[j] =
x.elts[j], y.elts[j]); packed_compute<packed_t, PACKED_ACT_FN, act_first, HAS_CLAMP>(
x.elts[j], y.elts[j], limit);
} }
if constexpr (use_256b) { if constexpr (use_256b) {
st256(x, &out_vec[i]); st256(x, &out_vec[i]);
@@ -72,7 +118,8 @@ __global__ void act_and_mul_kernel(
for (int64_t idx = threadIdx.x; idx < d; idx += blockDim.x) { for (int64_t idx = threadIdx.x; idx < d; idx += blockDim.x) {
const scalar_t x = VLLM_LDG(&x_ptr[idx]); const scalar_t x = VLLM_LDG(&x_ptr[idx]);
const scalar_t y = VLLM_LDG(&y_ptr[idx]); const scalar_t y = VLLM_LDG(&y_ptr[idx]);
out_ptr[idx] = compute<scalar_t, ACT_FN, act_first>(x, y); out_ptr[idx] =
compute<scalar_t, ACT_FN, act_first, HAS_CLAMP>(x, y, limit);
} }
} }
} }
@@ -151,8 +198,11 @@ packed_gelu_tanh_kernel(const packed_t& val) {
// Launch activation and gating kernel. // Launch activation and gating kernel.
// Use ACT_FIRST (bool) indicating whether to apply the activation function // Use ACT_FIRST (bool) indicating whether to apply the activation function
// first. // first. HAS_CLAMP (bool) enables pre-activation clamping: gate input is
#define LAUNCH_ACTIVATION_GATE_KERNEL(KERNEL, PACKED_KERNEL, ACT_FIRST) \ // clamped (max only) and up input is clamped (both sides) before the
// activation function is applied.
#define LAUNCH_ACTIVATION_GATE_KERNEL(KERNEL, PACKED_KERNEL, ACT_FIRST, \
HAS_CLAMP, LIMIT) \
auto dtype = input.scalar_type(); \ auto dtype = input.scalar_type(); \
int d = input.size(-1) / 2; \ int d = input.size(-1) / 2; \
int64_t num_tokens = input.numel() / input.size(-1); \ int64_t num_tokens = input.numel() / input.size(-1); \
@@ -177,8 +227,8 @@ packed_gelu_tanh_kernel(const packed_t& val) {
scalar_t, typename vllm::PackedTypeConverter<scalar_t>::Type, \ scalar_t, typename vllm::PackedTypeConverter<scalar_t>::Type, \
KERNEL<scalar_t>, \ KERNEL<scalar_t>, \
PACKED_KERNEL<typename vllm::PackedTypeConverter<scalar_t>::Type>, \ PACKED_KERNEL<typename vllm::PackedTypeConverter<scalar_t>::Type>, \
ACT_FIRST, true, true><<<grid, block, 0, stream>>>( \ ACT_FIRST, true, HAS_CLAMP, true><<<grid, block, 0, stream>>>( \
out.data_ptr<scalar_t>(), input.data_ptr<scalar_t>(), d); \ out.data_ptr<scalar_t>(), input.data_ptr<scalar_t>(), d, LIMIT); \
}); \ }); \
} else { \ } else { \
VLLM_DISPATCH_FLOATING_TYPES(dtype, "act_and_mul_kernel", [&] { \ VLLM_DISPATCH_FLOATING_TYPES(dtype, "act_and_mul_kernel", [&] { \
@@ -186,8 +236,8 @@ packed_gelu_tanh_kernel(const packed_t& val) {
scalar_t, typename vllm::PackedTypeConverter<scalar_t>::Type, \ scalar_t, typename vllm::PackedTypeConverter<scalar_t>::Type, \
KERNEL<scalar_t>, \ KERNEL<scalar_t>, \
PACKED_KERNEL<typename vllm::PackedTypeConverter<scalar_t>::Type>, \ PACKED_KERNEL<typename vllm::PackedTypeConverter<scalar_t>::Type>, \
ACT_FIRST, true, false><<<grid, block, 0, stream>>>( \ ACT_FIRST, true, HAS_CLAMP, false><<<grid, block, 0, stream>>>( \
out.data_ptr<scalar_t>(), input.data_ptr<scalar_t>(), d); \ out.data_ptr<scalar_t>(), input.data_ptr<scalar_t>(), d, LIMIT); \
}); \ }); \
} \ } \
} else { \ } else { \
@@ -197,8 +247,8 @@ packed_gelu_tanh_kernel(const packed_t& val) {
scalar_t, typename vllm::PackedTypeConverter<scalar_t>::Type, \ scalar_t, typename vllm::PackedTypeConverter<scalar_t>::Type, \
KERNEL<scalar_t>, \ KERNEL<scalar_t>, \
PACKED_KERNEL<typename vllm::PackedTypeConverter<scalar_t>::Type>, \ PACKED_KERNEL<typename vllm::PackedTypeConverter<scalar_t>::Type>, \
ACT_FIRST, false><<<grid, block, 0, stream>>>( \ ACT_FIRST, false, HAS_CLAMP><<<grid, block, 0, stream>>>( \
out.data_ptr<scalar_t>(), input.data_ptr<scalar_t>(), d); \ out.data_ptr<scalar_t>(), input.data_ptr<scalar_t>(), d, LIMIT); \
}); \ }); \
} }
@@ -206,7 +256,14 @@ void silu_and_mul(torch::Tensor& out, // [..., d]
torch::Tensor& input) // [..., 2 * d] torch::Tensor& input) // [..., 2 * d]
{ {
LAUNCH_ACTIVATION_GATE_KERNEL(vllm::silu_kernel, vllm::packed_silu_kernel, LAUNCH_ACTIVATION_GATE_KERNEL(vllm::silu_kernel, vllm::packed_silu_kernel,
true); true, false, 0.0f);
}
void silu_and_mul_clamp(torch::Tensor& out, // [..., d]
torch::Tensor& input, // [..., 2 * d]
double limit) {
LAUNCH_ACTIVATION_GATE_KERNEL(vllm::silu_kernel, vllm::packed_silu_kernel,
true, true, (float)limit);
} }
void mul_and_silu(torch::Tensor& out, // [..., d] void mul_and_silu(torch::Tensor& out, // [..., d]
@@ -215,21 +272,21 @@ void mul_and_silu(torch::Tensor& out, // [..., d]
// The difference between mul_and_silu and silu_and_mul is that mul_and_silu // The difference between mul_and_silu and silu_and_mul is that mul_and_silu
// applies the silu to the latter half of the input. // applies the silu to the latter half of the input.
LAUNCH_ACTIVATION_GATE_KERNEL(vllm::silu_kernel, vllm::packed_silu_kernel, LAUNCH_ACTIVATION_GATE_KERNEL(vllm::silu_kernel, vllm::packed_silu_kernel,
false); false, false, 0.0f);
} }
void gelu_and_mul(torch::Tensor& out, // [..., d] void gelu_and_mul(torch::Tensor& out, // [..., d]
torch::Tensor& input) // [..., 2 * d] torch::Tensor& input) // [..., 2 * d]
{ {
LAUNCH_ACTIVATION_GATE_KERNEL(vllm::gelu_kernel, vllm::packed_gelu_kernel, LAUNCH_ACTIVATION_GATE_KERNEL(vllm::gelu_kernel, vllm::packed_gelu_kernel,
true); true, false, 0.0f);
} }
void gelu_tanh_and_mul(torch::Tensor& out, // [..., d] void gelu_tanh_and_mul(torch::Tensor& out, // [..., d]
torch::Tensor& input) // [..., 2 * d] torch::Tensor& input) // [..., 2 * d]
{ {
LAUNCH_ACTIVATION_GATE_KERNEL(vllm::gelu_tanh_kernel, LAUNCH_ACTIVATION_GATE_KERNEL(
vllm::packed_gelu_tanh_kernel, true); vllm::gelu_tanh_kernel, vllm::packed_gelu_tanh_kernel, true, false, 0.0f);
} }
namespace vllm { namespace vllm {
+2
View File
@@ -163,6 +163,8 @@ void rotary_embedding(torch::Tensor& positions, torch::Tensor& query,
void silu_and_mul(torch::Tensor& out, torch::Tensor& input); void silu_and_mul(torch::Tensor& out, torch::Tensor& input);
void silu_and_mul_clamp(torch::Tensor& out, torch::Tensor& input, double limit);
void silu_and_mul_quant(torch::Tensor& out, torch::Tensor& input, void silu_and_mul_quant(torch::Tensor& out, torch::Tensor& input,
torch::Tensor& scale); torch::Tensor& scale);
+59 -4
View File
@@ -82,18 +82,73 @@ void launch_persistent_topk(const torch::Tensor& logits,
size_t smem_size = P::kFixedSmemLarge + chunk_size * sizeof(uint32_t); size_t smem_size = P::kFixedSmemLarge + chunk_size * sizeof(uint32_t);
if (smem_size < P::kSmemMedium) smem_size = P::kSmemMedium; if (smem_size < P::kSmemMedium) smem_size = P::kSmemMedium;
// Query occupancy for the instantiation that will actually launch;
// overestimating it deadlocks the cooperative barrier.
int occupancy = 1; int occupancy = 1;
cudaOccupancyMaxActiveBlocksPerMultiprocessor( cudaError_t occ_err = cudaSuccess;
&occupancy, P::persistent_topk_kernel<TopK, 4>, P::kThreadsPerBlock, if (vec_size == 4) {
smem_size); occ_err = cudaOccupancyMaxActiveBlocksPerMultiprocessor(
&occupancy, P::persistent_topk_kernel<TopK, 4>, P::kThreadsPerBlock,
smem_size);
} else if (vec_size == 2) {
occ_err = cudaOccupancyMaxActiveBlocksPerMultiprocessor(
&occupancy, P::persistent_topk_kernel<TopK, 2>, P::kThreadsPerBlock,
smem_size);
} else {
occ_err = cudaOccupancyMaxActiveBlocksPerMultiprocessor(
&occupancy, P::persistent_topk_kernel<TopK, 1>, P::kThreadsPerBlock,
smem_size);
}
TORCH_CHECK(occ_err == cudaSuccess,
"persistent_topk occupancy query failed: ",
cudaGetErrorString(occ_err));
if (occupancy < 1) occupancy = 1; if (occupancy < 1) occupancy = 1;
uint32_t max_resident_ctas = static_cast<uint32_t>(num_sms) * occupancy; // The cooperative spin-wait barrier only runs when at least one row hits
// the radix path (seq_len > RADIX_THRESHOLD). Below that, non-CTA-0 CTAs
// early-exit, so oversubscription can't deadlock and headroom is wasted.
const bool needs_cooperative =
static_cast<uint32_t>(max_seq_len) > P::RADIX_THRESHOLD;
const uint32_t hw_resident_cap =
static_cast<uint32_t>(num_sms) * static_cast<uint32_t>(occupancy);
uint32_t max_resident_ctas = hw_resident_cap;
if (needs_cooperative) {
// Reserve one CTA per SM when occupancy allows; fall back to a single
// CTA when occupancy == 1 (the most deadlock-prone case — any straggler
// kernel that takes the only slot on one SM hangs the barrier). Never
// drop below one full group's worth.
uint32_t headroom = (occupancy > 1) ? static_cast<uint32_t>(num_sms) : 1u;
if (max_resident_ctas >= headroom + ctas_per_group) {
max_resident_ctas -= headroom;
}
}
uint32_t num_groups = std::min(max_resident_ctas / ctas_per_group, uint32_t num_groups = std::min(max_resident_ctas / ctas_per_group,
static_cast<uint32_t>(num_rows)); static_cast<uint32_t>(num_rows));
if (num_groups == 0) num_groups = 1; if (num_groups == 0) num_groups = 1;
uint32_t total_ctas = num_groups * ctas_per_group; uint32_t total_ctas = num_groups * ctas_per_group;
// If the cooperative launch wouldn't fit, fall back to FilteredTopK
// instead of deadlocking. Only relevant when needs_cooperative.
if (needs_cooperative && total_ctas > hw_resident_cap) {
TORCH_CHECK(max_smem_per_block >= 128 * 1024,
"persistent_topk would oversubscribe and the FilteredTopK "
"fallback requires >=128KB smem per block (have ",
max_smem_per_block, "). total_ctas=", total_ctas,
" > num_sms*occupancy=", hw_resident_cap, " (TopK=", TopK,
", vec_size=", vec_size, ", ctas_per_group=", ctas_per_group,
", smem=", smem_size, ").");
cudaError_t status =
vllm::FilteredTopKRaggedTransform<float, int32_t, TopK>(
logits.data_ptr<float>(), output.data_ptr<int32_t>(),
lengths.data_ptr<int32_t>(), static_cast<uint32_t>(num_rows),
static_cast<uint32_t>(TopK), static_cast<uint32_t>(stride),
stream);
TORCH_CHECK(status == cudaSuccess,
"FilteredTopK fallback failed: ", cudaGetErrorString(status));
return;
}
size_t state_bytes = num_groups * sizeof(P::RadixRowState); size_t state_bytes = num_groups * sizeof(P::RadixRowState);
TORCH_CHECK(workspace.size(0) >= static_cast<int64_t>(state_bytes), TORCH_CHECK(workspace.size(0) >= static_cast<int64_t>(state_bytes),
"workspace too small, need ", state_bytes, " bytes"); "workspace too small, need ", state_bytes, " bytes");
+6
View File
@@ -106,6 +106,12 @@ TORCH_LIBRARY_EXPAND(TORCH_EXTENSION_NAME, ops) {
ops.def("silu_and_mul(Tensor! result, Tensor input) -> ()"); ops.def("silu_and_mul(Tensor! result, Tensor input) -> ()");
ops.impl("silu_and_mul", torch::kCUDA, &silu_and_mul); ops.impl("silu_and_mul", torch::kCUDA, &silu_and_mul);
// SwiGLU activation with input clamping.
ops.def(
"silu_and_mul_with_clamp(Tensor! result, Tensor input, float limit) "
"-> ()");
ops.impl("silu_and_mul_with_clamp", torch::kCUDA, &silu_and_mul_clamp);
ops.def( ops.def(
"silu_and_mul_quant(Tensor! result, Tensor input, Tensor scale) -> ()"); "silu_and_mul_quant(Tensor! result, Tensor input, Tensor scale) -> ()");
ops.impl("silu_and_mul_quant", torch::kCUDA, &silu_and_mul_quant); ops.impl("silu_and_mul_quant", torch::kCUDA, &silu_and_mul_quant);
+21 -33
View File
@@ -478,9 +478,6 @@ FROM ${FINAL_BASE_IMAGE} AS vllm-base
ARG CUDA_VERSION ARG CUDA_VERSION
ARG PYTHON_VERSION ARG PYTHON_VERSION
ARG DEADSNAKES_MIRROR_URL
ARG DEADSNAKES_GPGKEY_URL
ARG GET_PIP_URL
ENV DEBIAN_FRONTEND=noninteractive ENV DEBIAN_FRONTEND=noninteractive
WORKDIR /vllm-workspace WORKDIR /vllm-workspace
@@ -490,43 +487,35 @@ WORKDIR /vllm-workspace
RUN PYTHON_VERSION_STR=$(echo ${PYTHON_VERSION} | sed 's/\.//g') && \ RUN PYTHON_VERSION_STR=$(echo ${PYTHON_VERSION} | sed 's/\.//g') && \
echo "export PYTHON_VERSION_STR=${PYTHON_VERSION_STR}" >> /etc/environment echo "export PYTHON_VERSION_STR=${PYTHON_VERSION_STR}" >> /etc/environment
# Install Python and system dependencies # Install Python (via uv / python-build-standalone) and system dependencies.
# This replaces the deadsnakes PPA, removing the build-time dependency on
# Launchpad and matching how the build-stage (`base`) installs Python.
# python-build-standalone bundles dev headers, the venv module, and
# python3-config, so the python3.X-dev / python3.X-venv apt packages
# are not needed.
RUN apt-get update -y \ RUN apt-get update -y \
&& apt-get install -y --no-install-recommends \ && apt-get install -y --no-install-recommends \
software-properties-common \
curl \ curl \
sudo \ sudo \
ffmpeg \ ffmpeg \
libsm6 \ libsm6 \
libxext6 \ libxext6 \
libgl1 \ libgl1 \
&& if [ ! -z ${DEADSNAKES_MIRROR_URL} ] ; then \
if [ ! -z "${DEADSNAKES_GPGKEY_URL}" ] ; then \
mkdir -p -m 0755 /etc/apt/keyrings ; \
curl -L ${DEADSNAKES_GPGKEY_URL} | gpg --dearmor > /etc/apt/keyrings/deadsnakes.gpg ; \
sudo chmod 644 /etc/apt/keyrings/deadsnakes.gpg ; \
echo "deb [signed-by=/etc/apt/keyrings/deadsnakes.gpg] ${DEADSNAKES_MIRROR_URL} $(lsb_release -cs) main" > /etc/apt/sources.list.d/deadsnakes.list ; \
fi ; \
else \
for i in 1 2 3; do \
add-apt-repository -y ppa:deadsnakes/ppa && break || \
{ echo "Attempt $i failed, retrying in 5s..."; sleep 5; }; \
done ; \
fi \
&& apt-get update -y \
&& apt-get install -y --no-install-recommends \
python${PYTHON_VERSION} \
python${PYTHON_VERSION}-dev \
python${PYTHON_VERSION}-venv \
libibverbs-dev \ libibverbs-dev \
&& rm -rf /var/lib/apt/lists/* \ && rm -rf /var/lib/apt/lists/* \
&& update-alternatives --install /usr/bin/python3 python3 /usr/bin/python${PYTHON_VERSION} 1 \ && curl -LsSf https://astral.sh/uv/install.sh | sh \
&& update-alternatives --set python3 /usr/bin/python${PYTHON_VERSION} \ && $HOME/.local/bin/uv venv /opt/venv --python ${PYTHON_VERSION} \
&& ln -sf /usr/bin/python${PYTHON_VERSION}-config /usr/bin/python3-config \ && rm -f /usr/bin/python3 /usr/bin/python3-config /usr/bin/pip \
&& rm -f /usr/lib/python${PYTHON_VERSION}/EXTERNALLY-MANAGED \ && ln -s /opt/venv/bin/python3 /usr/bin/python3 \
&& curl -sS ${GET_PIP_URL} | python${PYTHON_VERSION} \ && ln -s /opt/venv/bin/python${PYTHON_VERSION} /usr/bin/python${PYTHON_VERSION} \
&& ln -s /opt/venv/bin/python3-config /usr/bin/python3-config \
&& ln -s /opt/venv/bin/pip /usr/bin/pip \
&& python3 --version && python3 -m pip --version && python3 --version && python3 -m pip --version
# Activate virtual environment and add uv to PATH
ENV PATH="/opt/venv/bin:/root/.local/bin:$PATH"
ENV VIRTUAL_ENV="/opt/venv"
# Install CUDA development tools for runtime JIT compilation # Install CUDA development tools for runtime JIT compilation
# (FlashInfer, DeepGEMM, EP kernels all require compilation at runtime) # (FlashInfer, DeepGEMM, EP kernels all require compilation at runtime)
RUN CUDA_VERSION_DASH=$(echo $CUDA_VERSION | cut -d. -f1,2 | tr '.' '-') && \ RUN CUDA_VERSION_DASH=$(echo $CUDA_VERSION | cut -d. -f1,2 | tr '.' '-') && \
@@ -540,7 +529,9 @@ RUN CUDA_VERSION_DASH=$(echo $CUDA_VERSION | cut -d. -f1,2 | tr '.' '-') && \
libcurand-dev-${CUDA_VERSION_DASH} \ libcurand-dev-${CUDA_VERSION_DASH} \
libcublas-${CUDA_VERSION_DASH} \ libcublas-${CUDA_VERSION_DASH} \
# Required by fastsafetensors (fixes #20384) # Required by fastsafetensors (fixes #20384)
libnuma-dev && \ libnuma-dev \
# numactl CLI for NUMA binding at runtime
numactl && \
# Fixes nccl_allocator requiring nccl.h at runtime # Fixes nccl_allocator requiring nccl.h at runtime
# https://github.com/vllm-project/vllm/blob/1336a1ea244fa8bfd7e72751cabbdb5b68a0c11a/vllm/distributed/device_communicators/pynccl_allocator.py#L22 # https://github.com/vllm-project/vllm/blob/1336a1ea244fa8bfd7e72751cabbdb5b68a0c11a/vllm/distributed/device_communicators/pynccl_allocator.py#L22
# NCCL packages don't use the cuda-MAJOR-MINOR naming convention, # NCCL packages don't use the cuda-MAJOR-MINOR naming convention,
@@ -549,9 +540,6 @@ RUN CUDA_VERSION_DASH=$(echo $CUDA_VERSION | cut -d. -f1,2 | tr '.' '-') && \
apt-get install -y --no-install-recommends --allow-change-held-packages libnccl-dev=${NCCL_VER} libnccl2=${NCCL_VER} && \ apt-get install -y --no-install-recommends --allow-change-held-packages libnccl-dev=${NCCL_VER} libnccl2=${NCCL_VER} && \
rm -rf /var/lib/apt/lists/* rm -rf /var/lib/apt/lists/*
# Install uv for faster pip installs
RUN python3 -m pip install uv
# Environment for uv # Environment for uv
ENV UV_HTTP_TIMEOUT=500 ENV UV_HTTP_TIMEOUT=500
ENV UV_INDEX_STRATEGY="unsafe-best-match" ENV UV_INDEX_STRATEGY="unsafe-best-match"
@@ -741,7 +729,7 @@ ENV HF_XET_HIGH_PERFORMANCE 1
ENV HF_HUB_DOWNLOAD_TIMEOUT 60 ENV HF_HUB_DOWNLOAD_TIMEOUT 60
# Copy in the v1 package for testing (it isn't distributed yet) # Copy in the v1 package for testing (it isn't distributed yet)
COPY vllm/v1 /usr/local/lib/python${PYTHON_VERSION}/dist-packages/vllm/v1 COPY vllm/v1 /opt/venv/lib/python${PYTHON_VERSION}/site-packages/vllm/v1
# Source code is used in the `python_only_compile.sh` test # Source code is used in the `python_only_compile.sh` test
# We hide it inside `src/` so that this source code # We hide it inside `src/` so that this source code
+1 -1
View File
@@ -36,7 +36,7 @@ th {
| deepep_high_throughput | standard | fp8 | G(128),A,T<sup>2</sup> | Y | Y | [`DeepEPHTPrepareAndFinalize`][vllm.model_executor.layers.fused_moe.prepare_finalize.deepep_ht.DeepEPHTPrepareAndFinalize] | | deepep_high_throughput | standard | fp8 | G(128),A,T<sup>2</sup> | Y | Y | [`DeepEPHTPrepareAndFinalize`][vllm.model_executor.layers.fused_moe.prepare_finalize.deepep_ht.DeepEPHTPrepareAndFinalize] |
| deepep_low_latency | batched | fp8 | G(128),A,T<sup>3</sup> | Y | Y | [`DeepEPLLPrepareAndFinalize`][vllm.model_executor.layers.fused_moe.prepare_finalize.deepep_ll.DeepEPLLPrepareAndFinalize] | | deepep_low_latency | batched | fp8 | G(128),A,T<sup>3</sup> | Y | Y | [`DeepEPLLPrepareAndFinalize`][vllm.model_executor.layers.fused_moe.prepare_finalize.deepep_ll.DeepEPLLPrepareAndFinalize] |
| flashinfer_nvlink_two_sided | standard | nvfp4,fp8 | G,A,T | N | N | [`FlashInferNVLinkTwoSidedPrepareAndFinalize`][vllm.model_executor.layers.fused_moe.prepare_finalize.flashinfer_nvlink_two_sided.FlashInferNVLinkTwoSidedPrepareAndFinalize] | | flashinfer_nvlink_two_sided | standard | nvfp4,fp8 | G,A,T | N | N | [`FlashInferNVLinkTwoSidedPrepareAndFinalize`][vllm.model_executor.layers.fused_moe.prepare_finalize.flashinfer_nvlink_two_sided.FlashInferNVLinkTwoSidedPrepareAndFinalize] |
| flashinfer_nvlink_one_sided | standard | nvfp4 | G,A,T | N | N | [`FlashInferNVLinkOneSidedPrepareAndFinalize`][vllm.model_executor.layers.fused_moe.prepare_finalize.flashinfer_nvlink_one_sided.FlashInferNVLinkOneSidedPrepareAndFinalize] | | flashinfer_nvlink_one_sided | standard | nvfp4,bf16,mxfp8 | G,A,T | N | N | [`FlashInferNVLinkOneSidedPrepareAndFinalize`][vllm.model_executor.layers.fused_moe.prepare_finalize.flashinfer_nvlink_one_sided.FlashInferNVLinkOneSidedPrepareAndFinalize] |
!!! info "Table key" !!! info "Table key"
1. All types: mxfp4, nvfp4, int4, int8, fp8 1. All types: mxfp4, nvfp4, int4, int8, fp8
+3 -3
View File
@@ -292,10 +292,10 @@ Pooling models now support token-wise task.
### Score task ### Score task
`score` task have has been removed in v0.21, use `classify` instead. Only when a classification model outputs num_labels `score` task is deprecated and will be removed in v0.20. Please use `classify` instead. Only when a
equal to 1 can it be used as a scoring model and have its scoring API enabled. classification model outputs num_labels equal to 1 can it be used as a scoring model and have its scoring API enabled.
### Pooling multitask support ### Pooling multitask support
Pooling multitask support has been removed in v0.21. When the default pooling task is not what you want, Pooling multitask support is deprecated and will be removed in v0.20. When the default pooling task is not what you want,
you need to manually specify it via `PoolerConfig(task=<task>)` offline or `--pooler-config.task <task>` online. you need to manually specify it via `PoolerConfig(task=<task>)` offline or `--pooler-config.task <task>` online.
@@ -4,74 +4,68 @@
import torch import torch
from vllm import LLM from vllm import LLM
from vllm.config import PoolerConfig
from vllm.inputs import TextPrompt from vllm.inputs import TextPrompt
from vllm.multimodal.utils import fetch_image from vllm.multimodal.utils import fetch_image
# Initialize model
model = LLM(
model="jinaai/jina-embeddings-v4-vllm-text-matching",
runner="pooling",
max_model_len=1024,
gpu_memory_utilization=0.8,
)
def main(): # Create text prompts
# Initialize model text1 = "Ein wunderschöner Sonnenuntergang am Strand"
model = LLM( text1_prompt = TextPrompt(prompt=f"Query: {text1}")
model="jinaai/jina-embeddings-v4-vllm-text-matching",
pooler_config=PoolerConfig(task="token_embed"),
runner="pooling",
max_model_len=1024,
gpu_memory_utilization=0.8,
)
# Create text prompts text2 = "浜辺に沈む美しい夕日"
text1 = "Ein wunderschöner Sonnenuntergang am Strand" text2_prompt = TextPrompt(prompt=f"Query: {text2}")
text1_prompt = TextPrompt(prompt=f"Query: {text1}")
text2 = "浜辺に沈む美しい夕日" # Create image prompt
text2_prompt = TextPrompt(prompt=f"Query: {text2}") image = fetch_image(
"https://vllm-public-assets.s3.us-west-2.amazonaws.com/multimodal_asset/eskimo.jpg" # noqa: E501
)
image_prompt = TextPrompt(
prompt="<|im_start|>user\n<|vision_start|><|image_pad|><|vision_end|>Describe the image.<|im_end|>\n", # noqa: E501
multi_modal_data={"image": image},
)
# Create image prompt # Encode all prompts
image = fetch_image( prompts = [text1_prompt, text2_prompt, image_prompt]
"https://vllm-public-assets.s3.us-west-2.amazonaws.com/multimodal_asset/eskimo.jpg" # noqa: E501 outputs = model.encode(prompts, pooling_task="token_embed")
)
image_prompt = TextPrompt(
prompt="<|im_start|>user\n<|vision_start|><|image_pad|><|vision_end|>Describe the image.<|im_end|>\n", # noqa: E501
multi_modal_data={"image": image},
)
# Encode all prompts
prompts = [text1_prompt, text2_prompt, image_prompt]
outputs = model.encode(prompts, pooling_task="token_embed")
def get_embeddings(outputs):
VISION_START_TOKEN_ID, VISION_END_TOKEN_ID = 151652, 151653
embeddings = []
for output in outputs:
if VISION_START_TOKEN_ID in output.prompt_token_ids:
# Gather only vision tokens
img_start_pos = torch.where(
torch.tensor(output.prompt_token_ids) == VISION_START_TOKEN_ID
)[0][0]
img_end_pos = torch.where(
torch.tensor(output.prompt_token_ids) == VISION_END_TOKEN_ID
)[0][0]
embeddings_tensor = output.outputs.data.detach().clone()[
img_start_pos : img_end_pos + 1
]
else:
# Use all tokens for text-only prompts
embeddings_tensor = output.outputs.data.detach().clone()
# Pool and normalize embeddings
pooled_output = (
embeddings_tensor.sum(dim=0, dtype=torch.float32)
/ embeddings_tensor.shape[0]
)
embeddings.append(torch.nn.functional.normalize(pooled_output, dim=-1))
return embeddings
embeddings = get_embeddings(outputs)
for embedding in embeddings:
print(embedding.shape)
if __name__ == "__main__": def get_embeddings(outputs):
main() VISION_START_TOKEN_ID, VISION_END_TOKEN_ID = 151652, 151653
embeddings = []
for output in outputs:
if VISION_START_TOKEN_ID in output.prompt_token_ids:
# Gather only vision tokens
img_start_pos = torch.where(
torch.tensor(output.prompt_token_ids) == VISION_START_TOKEN_ID
)[0][0]
img_end_pos = torch.where(
torch.tensor(output.prompt_token_ids) == VISION_END_TOKEN_ID
)[0][0]
embeddings_tensor = output.outputs.data.detach().clone()[
img_start_pos : img_end_pos + 1
]
else:
# Use all tokens for text-only prompts
embeddings_tensor = output.outputs.data.detach().clone()
# Pool and normalize embeddings
pooled_output = (
embeddings_tensor.sum(dim=0, dtype=torch.float32)
/ embeddings_tensor.shape[0]
)
embeddings.append(torch.nn.functional.normalize(pooled_output, dim=-1))
return embeddings
embeddings = get_embeddings(outputs)
for embedding in embeddings:
print(embedding.shape)
@@ -4,7 +4,6 @@
from argparse import Namespace from argparse import Namespace
from vllm import LLM, EngineArgs from vllm import LLM, EngineArgs
from vllm.config import PoolerConfig
from vllm.utils.argparse_utils import FlexibleArgumentParser from vllm.utils.argparse_utils import FlexibleArgumentParser
@@ -14,7 +13,6 @@ def parse_args():
# Set example specific arguments # Set example specific arguments
parser.set_defaults( parser.set_defaults(
model="BAAI/bge-m3", model="BAAI/bge-m3",
pooler_config=PoolerConfig(task="token_embed"),
runner="pooling", runner="pooling",
enforce_eager=True, enforce_eager=True,
) )
@@ -34,6 +32,15 @@ def main(args: Namespace):
# You should pass runner="pooling" for embedding models # You should pass runner="pooling" for embedding models
llm = LLM(**vars(args)) llm = LLM(**vars(args))
# Generate embedding. The output is a list of EmbeddingRequestOutputs.
outputs = llm.embed(prompts)
# Print the outputs.
print("\nGenerated Outputs:\n" + "-" * 60)
for prompt, output in zip(prompts, outputs):
embeds = output.outputs.embedding
print(len(embeds))
# Generate embedding for each token. The output is a list of PoolingRequestOutput. # Generate embedding for each token. The output is a list of PoolingRequestOutput.
outputs = llm.encode(prompts, pooling_task="token_embed") outputs = llm.encode(prompts, pooling_task="token_embed")
@@ -43,20 +50,6 @@ def main(args: Namespace):
multi_vector = output.outputs.data multi_vector = output.outputs.data
print(multi_vector.shape) print(multi_vector.shape)
query = "What is the capital of France?"
documents = [
"The capital of Brazil is Brasilia.",
"The capital of France is Paris.",
]
# Generate scores.
outputs = llm.score(query, documents)
# Print the outputs.
print("\nGenerated Outputs:\n" + "-" * 60)
for document, output in zip(documents, outputs):
score = output.outputs.score
print(f"Pair: {[query, document]!r} \nScore: {score}")
print("-" * 60)
if __name__ == "__main__": if __name__ == "__main__":
args = parse_args() args = parse_args()
@@ -7,11 +7,10 @@ Example online usage of Pooling API for multi vector retrieval.
Run `vllm serve <model> --runner pooling` Run `vllm serve <model> --runner pooling`
to start up the server in vLLM. e.g. to start up the server in vLLM. e.g.
vllm serve BAAI/bge-m3 --pooler-config.task token_embed vllm serve BAAI/bge-m3
""" """
import argparse import argparse
import pprint
import requests import requests
import torch import torch
@@ -33,8 +32,7 @@ def parse_args():
def main(args): def main(args):
pooling_url = f"http://{args.host}:{args.port}/pooling" api_url = f"http://{args.host}:{args.port}/pooling"
score_url = f"http://{args.host}:{args.port}/score"
model_name = args.model model_name = args.model
prompts = [ prompts = [
@@ -45,23 +43,11 @@ def main(args):
] ]
prompt = {"model": model_name, "input": prompts} prompt = {"model": model_name, "input": prompts}
pooling_response = post_http_request(prompt=prompt, api_url=pooling_url) pooling_response = post_http_request(prompt=prompt, api_url=api_url)
for output in pooling_response.json()["data"]: for output in pooling_response.json()["data"]:
multi_vector = torch.tensor(output["data"]) multi_vector = torch.tensor(output["data"])
print(multi_vector.shape) print(multi_vector.shape)
queries = "What is the capital of France?"
documents = [
"The capital of Brazil is Brasilia.",
"The capital of France is Paris.",
]
prompt = {"model": model_name, "queries": queries, "documents": documents}
score_response = post_http_request(prompt=prompt, api_url=score_url)
print("\nPrompt when queries is string and documents is a list:")
pprint.pprint(prompt)
print("\nScore Response:")
pprint.pprint(score_response.json())
if __name__ == "__main__": if __name__ == "__main__":
args = parse_args() args = parse_args()
+5 -2
View File
@@ -34,7 +34,10 @@ def _run_vllm(vllm_runner):
mode=CompilationMode.VLLM_COMPILE, mode=CompilationMode.VLLM_COMPILE,
cudagraph_mode=CUDAGraphMode.NONE, cudagraph_mode=CUDAGraphMode.NONE,
), ),
num_gpu_blocks_override=8, # Phi-tiny-MoE uses SWA, whose admission cap is `cdiv(L, block_size) + 1`
# at default block_size=16 — i.e. 17 blocks for max_model_len=256. Use
# 32 for headroom.
num_gpu_blocks_override=32,
): ):
pass pass
@@ -190,7 +193,7 @@ def _run_model(vllm_runner, spec: ModelStartupSpec):
cudagraph_mode=CUDAGraphMode.NONE, cudagraph_mode=CUDAGraphMode.NONE,
pass_config=PassConfig(fuse_allreduce_rms=False), pass_config=PassConfig(fuse_allreduce_rms=False),
), ),
num_gpu_blocks_override=8, num_gpu_blocks_override=16,
): ):
pass pass
+3
View File
@@ -405,6 +405,9 @@ def test_should_split():
(None, 0, 1, False, 2048, CUDAGraphMode.NONE, 0), (None, 0, 1, False, 2048, CUDAGraphMode.NONE, 0),
# truncated to nearest multiple of 8 or 16 # truncated to nearest multiple of 8 or 16
(None, 257, 1, False, 2048, CUDAGraphMode.FULL_AND_PIECEWISE, 256), (None, 257, 1, False, 2048, CUDAGraphMode.FULL_AND_PIECEWISE, 256),
# max_num_batched_tokens <= max_cudagraph_capture_size should always be
# captured even if not landing on a 16-stride step
(None, 2048, 1, False, 257, CUDAGraphMode.FULL_AND_PIECEWISE, 257),
# max from list # max from list
([1, 2, 4, 15], None, 1, False, 2048, CUDAGraphMode.FULL_AND_PIECEWISE, 15), ([1, 2, 4, 15], None, 1, False, 2048, CUDAGraphMode.FULL_AND_PIECEWISE, 15),
# SP forces full-graph compilation, sizes are filtered by TP # SP forces full-graph compilation, sizes are filtered by TP
@@ -1,12 +1,13 @@
# SPDX-License-Identifier: Apache-2.0 # SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
import logging
import weakref import weakref
import pytest import pytest
import torch import torch
from tests.models.utils import softmax from tests.models.utils import softmax
from vllm import LLM, ClassificationRequestOutput, PoolingParams from vllm import LLM, ClassificationRequestOutput, PoolingParams, PoolingRequestOutput
from vllm.distributed import cleanup_dist_env_and_memory from vllm.distributed import cleanup_dist_env_and_memory
from vllm.tasks import PoolingTask from vllm.tasks import PoolingTask
@@ -65,6 +66,18 @@ def test_list_prompts(llm: LLM):
assert len(outputs[i].outputs.probs) == num_labels assert len(outputs[i].outputs.probs) == num_labels
@pytest.mark.skip_global_cleanup
def test_token_classify(llm: LLM, caplog_vllm):
with caplog_vllm.at_level(level=logging.WARNING, logger="vllm"):
outputs = llm.encode(prompt, pooling_task="token_classify", use_tqdm=False)
assert "deprecated" in caplog_vllm.text
assert len(outputs) == 1
assert isinstance(outputs[0], PoolingRequestOutput)
assert outputs[0].prompt_token_ids == prompt_token_ids
assert outputs[0].outputs.data.shape == (len(prompt_token_ids), num_labels)
@pytest.mark.skip_global_cleanup @pytest.mark.skip_global_cleanup
def test_pooling_params(llm: LLM): def test_pooling_params(llm: LLM):
def get_outputs(use_activation): def get_outputs(use_activation):
@@ -97,12 +110,10 @@ def test_score_api(llm: LLM):
llm.score("ping", "pong", use_tqdm=False) llm.score("ping", "pong", use_tqdm=False)
@pytest.mark.parametrize("task", ["embed", "token_embed", "token_classify", "plugin"]) @pytest.mark.parametrize("task", ["embed", "token_embed", "plugin"])
def test_unsupported_tasks(llm: LLM, task: PoolingTask): def test_unsupported_tasks(llm: LLM, task: PoolingTask):
if task == "plugin": if task == "plugin":
err_msg = "No IOProcessor plugin installed." err_msg = "No IOProcessor plugin installed."
elif task == "token_classify":
err_msg = "Try switching the model's pooling_task via.+"
else: else:
err_msg = "Embedding API is not supported by this model.+" err_msg = "Embedding API is not supported by this model.+"
with pytest.raises(ValueError, match=err_msg): with pytest.raises(ValueError, match=err_msg):
@@ -436,7 +436,26 @@ async def test_pooling_classify(server: RemoteOpenAIServer, model_name: str):
@pytest.mark.asyncio @pytest.mark.asyncio
@pytest.mark.parametrize("model_name", [MODEL_NAME]) @pytest.mark.parametrize("model_name", [MODEL_NAME])
@pytest.mark.parametrize("task", ["embed", "token_embed", "token_classify", "plugin"]) async def test_pooling_token_classify(server: RemoteOpenAIServer, model_name: str):
task = "token_classify"
response = requests.post(
server.url_for("pooling"),
json={
"model": model_name,
"input": input_text,
"encoding_format": "float",
"task": task,
},
)
poolings = PoolingResponse.model_validate(response.json())
assert len(poolings.data) == 1
assert len(poolings.data[0].data) == 8
assert len(poolings.data[0].data[0]) == 2
@pytest.mark.asyncio
@pytest.mark.parametrize("model_name", [MODEL_NAME])
@pytest.mark.parametrize("task", ["embed", "token_embed", "plugin"])
async def test_pooling_not_supported( async def test_pooling_not_supported(
server: RemoteOpenAIServer, model_name: str, task: str server: RemoteOpenAIServer, model_name: str, task: str
): ):
@@ -450,11 +469,8 @@ async def test_pooling_not_supported(
}, },
) )
assert response.json()["error"]["type"] == "BadRequestError" assert response.json()["error"]["type"] == "BadRequestError"
if task == "plugin": if task == "plugin":
err_msg = "No IOProcessor plugin installed." err_msg = "No IOProcessor plugin installed."
elif task == "token_classify":
err_msg = "Try switching the model's pooling_task via"
else: else:
err_msg = f"Unsupported task: {task!r}" err_msg = f"Unsupported task: {task!r}"
assert response.json()["error"]["message"].startswith(err_msg) assert response.json()["error"]["message"].startswith(err_msg)
@@ -1,5 +1,6 @@
# SPDX-License-Identifier: Apache-2.0 # SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
import logging
import weakref import weakref
import pytest import pytest
@@ -37,11 +38,11 @@ def llm():
seed=0, seed=0,
attention_config=attention_config, attention_config=attention_config,
) )
assert embedding_size == llm.model_config.embedding_size
yield weakref.proxy(llm) yield weakref.proxy(llm)
del llm del llm
cleanup_dist_env_and_memory() cleanup_dist_env_and_memory()
@@ -73,6 +74,16 @@ def test_list_prompts(llm: LLM):
assert len(outputs[i].outputs.embedding) == embedding_size assert len(outputs[i].outputs.embedding) == embedding_size
@pytest.mark.skip_global_cleanup
def test_token_embed(llm: LLM, caplog_vllm):
with caplog_vllm.at_level(level=logging.WARNING, logger="vllm"):
outputs = llm.encode(prompt, pooling_task="token_embed", use_tqdm=False)
assert "deprecated" in caplog_vllm.text
multi_vector = outputs[0].outputs.data
assert multi_vector.shape == (11, 384)
@pytest.mark.skip_global_cleanup @pytest.mark.skip_global_cleanup
def test_pooling_params(llm: LLM): def test_pooling_params(llm: LLM):
def get_outputs(normalize): def get_outputs(normalize):
@@ -96,14 +107,10 @@ def test_pooling_params(llm: LLM):
) )
@pytest.mark.parametrize( @pytest.mark.parametrize("task", ["token_classify", "classify", "plugin"])
"task", ["token_classify", "classify", "token_embed", "plugin"]
)
def test_unsupported_tasks(llm: LLM, task: PoolingTask): def test_unsupported_tasks(llm: LLM, task: PoolingTask):
if task == "plugin": if task == "plugin":
err_msg = "No IOProcessor plugin installed." err_msg = "No IOProcessor plugin installed."
elif task == "token_embed":
err_msg = "Try switching the model's pooling_task via.+"
else: else:
err_msg = "Classification API is not supported by this model.+" err_msg = "Classification API is not supported by this model.+"
with pytest.raises(ValueError, match=err_msg): with pytest.raises(ValueError, match=err_msg):
+22 -5
View File
@@ -732,9 +732,28 @@ async def test_pooling_embed(server: RemoteOpenAIServer, model_name: str):
@pytest.mark.asyncio @pytest.mark.asyncio
@pytest.mark.parametrize("model_name", [MODEL_NAME]) @pytest.mark.parametrize("model_name", [MODEL_NAME])
@pytest.mark.parametrize( async def test_pooling_token_embed(server: RemoteOpenAIServer, model_name: str):
"task", ["classify", "token_classify", "token_embed", "plugin"] task = "token_embed"
) response = requests.post(
server.url_for("pooling"),
json={
"model": model_name,
"input": input_text,
"encoding_format": "float",
"task": task,
},
)
poolings = PoolingResponse.model_validate(response.json())
assert len(poolings.data) == 1
assert len(poolings.data[0].data) == len(input_tokens)
assert len(poolings.data[0].data[0]) == 384
@pytest.mark.asyncio
@pytest.mark.parametrize("model_name", [MODEL_NAME])
@pytest.mark.parametrize("task", ["classify", "token_classify", "plugin"])
async def test_pooling_not_supported( async def test_pooling_not_supported(
server: RemoteOpenAIServer, model_name: str, task: str server: RemoteOpenAIServer, model_name: str, task: str
): ):
@@ -750,8 +769,6 @@ async def test_pooling_not_supported(
assert response.json()["error"]["type"] == "BadRequestError" assert response.json()["error"]["type"] == "BadRequestError"
if task == "plugin": if task == "plugin":
err_msg = "No IOProcessor plugin installed." err_msg = "No IOProcessor plugin installed."
elif task == "token_embed":
err_msg = "Try switching the model's pooling_task via"
else: else:
err_msg = f"Unsupported task: {task!r}" err_msg = f"Unsupported task: {task!r}"
assert response.json()["error"]["message"].startswith(err_msg) assert response.json()["error"]["message"].startswith(err_msg)
@@ -452,6 +452,25 @@ async def test_pooling_classify(server: RemoteOpenAIServer):
assert len(poolings.data[0].data) == 1 assert len(poolings.data[0].data) == 1
@pytest.mark.asyncio
async def test_pooling_token_classify(server: RemoteOpenAIServer):
response = requests.post(
server.url_for("pooling"),
json={
"model": MODEL_NAME,
"task": "token_classify",
"input": input_text,
"encoding_format": "float",
},
)
poolings = PoolingResponse.model_validate(response.json())
assert len(poolings.data) == 1
assert len(poolings.data[0].data) == len(input_tokens)
assert len(poolings.data[0].data[0]) == 1
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_rerank_max_tokens_per_doc( async def test_rerank_max_tokens_per_doc(
server: RemoteOpenAIServer, server: RemoteOpenAIServer,
@@ -525,7 +544,7 @@ async def test_rerank_max_tokens_per_doc_validation(
@pytest.mark.asyncio @pytest.mark.asyncio
@pytest.mark.parametrize("task", ["embed", "token_embed", "token_classify", "plugin"]) @pytest.mark.parametrize("task", ["embed", "token_embed", "plugin"])
async def test_pooling_not_supported(server: RemoteOpenAIServer, task: str): async def test_pooling_not_supported(server: RemoteOpenAIServer, task: str):
response = requests.post( response = requests.post(
server.url_for("pooling"), server.url_for("pooling"),
@@ -539,8 +558,6 @@ async def test_pooling_not_supported(server: RemoteOpenAIServer, task: str):
assert response.json()["error"]["type"] == "BadRequestError" assert response.json()["error"]["type"] == "BadRequestError"
if task == "plugin": if task == "plugin":
err_msg = "No IOProcessor plugin installed." err_msg = "No IOProcessor plugin installed."
elif task == "token_classify":
err_msg = "Try switching the model's pooling_task via"
else: else:
err_msg = f"Unsupported task: {task!r}" err_msg = f"Unsupported task: {task!r}"
assert response.json()["error"]["message"].startswith(err_msg) assert response.json()["error"]["message"].startswith(err_msg)
@@ -1,5 +1,6 @@
# SPDX-License-Identifier: Apache-2.0 # SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
import logging
import weakref import weakref
import pytest import pytest
@@ -59,19 +60,22 @@ def test_token_ids_prompts(llm: LLM):
@pytest.mark.skip_global_cleanup @pytest.mark.skip_global_cleanup
def test_score_api(llm: LLM): def test_score_api(llm: LLM):
err_msg = "This model does not support the Scoring API." err_msg = "Scoring API is only enabled for num_labels == 1."
with pytest.raises(ValueError, match=err_msg): with pytest.raises(ValueError, match=err_msg):
llm.score("ping", "pong", use_tqdm=False) llm.score("ping", "pong", use_tqdm=False)
@pytest.mark.parametrize("task", ["classify", "embed", "token_embed", "plugin"]) @pytest.mark.parametrize("task", ["classify", "embed", "token_embed", "plugin"])
def test_unsupported_tasks(llm: LLM, task: PoolingTask, caplog_vllm): def test_unsupported_tasks(llm: LLM, task: PoolingTask, caplog_vllm):
if task == "plugin": if task == "classify":
err_msg = "No IOProcessor plugin installed." with caplog_vllm.at_level(level=logging.WARNING, logger="vllm"):
elif task == "classify": llm.encode(prompt, pooling_task=task, use_tqdm=False)
err_msg = "Try switching the model's pooling_task via.+" assert "deprecated" in caplog_vllm.text
else: else:
err_msg = "Embedding API is not supported by this model.+" if task == "plugin":
err_msg = "No IOProcessor plugin installed."
else:
err_msg = "Embedding API is not supported by this model.+"
with pytest.raises(ValueError, match=err_msg): with pytest.raises(ValueError, match=err_msg):
llm.encode(prompt, pooling_task=task, use_tqdm=False) llm.encode(prompt, pooling_task=task, use_tqdm=False)
@@ -50,7 +50,7 @@ async def test_pooling_token_classify(server: RemoteOpenAIServer, model_name: st
@pytest.mark.asyncio @pytest.mark.asyncio
@pytest.mark.parametrize("model_name", [MODEL_NAME]) @pytest.mark.parametrize("model_name", [MODEL_NAME])
@pytest.mark.parametrize("task", ["classify", "embed", "token_embed", "plugin"]) @pytest.mark.parametrize("task", ["embed", "token_embed", "plugin"])
async def test_pooling_not_supported( async def test_pooling_not_supported(
server: RemoteOpenAIServer, model_name: str, task: str server: RemoteOpenAIServer, model_name: str, task: str
): ):
@@ -63,12 +63,9 @@ async def test_pooling_not_supported(
"task": task, "task": task,
}, },
) )
assert response.json()["error"]["type"] == "BadRequestError"
if task == "plugin": if task == "plugin":
err_msg = "No IOProcessor plugin installed." err_msg = "No IOProcessor plugin installed."
elif task == "classify":
err_msg = "Try switching the model's pooling_task via"
else: else:
err_msg = f"Unsupported task: {task!r}" err_msg = f"Unsupported task: {task!r}"
assert response.json()["error"]["message"].startswith(err_msg) assert response.json()["error"]["message"].startswith(err_msg)
@@ -1,5 +1,6 @@
# SPDX-License-Identifier: Apache-2.0 # SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
import logging
import weakref import weakref
import pytest import pytest
@@ -63,12 +64,15 @@ def test_token_ids_prompts(llm: LLM):
@pytest.mark.parametrize("task", ["embed", "classify", "token_classify", "plugin"]) @pytest.mark.parametrize("task", ["embed", "classify", "token_classify", "plugin"])
def test_unsupported_tasks(llm: LLM, task: PoolingTask, caplog_vllm): def test_unsupported_tasks(llm: LLM, task: PoolingTask, caplog_vllm):
if task == "plugin": if task == "embed":
err_msg = "No IOProcessor plugin installed." with caplog_vllm.at_level(level=logging.WARNING, logger="vllm"):
elif task == "embed": llm.encode(prompt, pooling_task=task, use_tqdm=False)
err_msg = "Try switching the model's pooling_task via.+" assert "deprecated" in caplog_vllm.text
else: else:
err_msg = "Classification API is not supported by this model.+" if task == "plugin":
err_msg = "No IOProcessor plugin installed."
else:
err_msg = "Classification API is not supported by this model.+"
with pytest.raises(ValueError, match=err_msg): with pytest.raises(ValueError, match=err_msg):
llm.encode(prompt, pooling_task=task, use_tqdm=False) llm.encode(prompt, pooling_task=task, use_tqdm=False)
@@ -73,7 +73,7 @@ async def test_pooling_token_embed(server: RemoteOpenAIServer, model_name: str):
@pytest.mark.asyncio @pytest.mark.asyncio
@pytest.mark.parametrize("model_name", [MODEL_NAME]) @pytest.mark.parametrize("model_name", [MODEL_NAME])
@pytest.mark.parametrize("task", ["embed", "classify", "token_classify", "plugin"]) @pytest.mark.parametrize("task", ["classify", "token_classify", "plugin"])
async def test_pooling_not_supported( async def test_pooling_not_supported(
server: RemoteOpenAIServer, model_name: str, task: str server: RemoteOpenAIServer, model_name: str, task: str
): ):
@@ -86,12 +86,9 @@ async def test_pooling_not_supported(
"task": task, "task": task,
}, },
) )
assert response.json()["error"]["type"] == "BadRequestError"
if task == "plugin": if task == "plugin":
err_msg = "No IOProcessor plugin installed." err_msg = "No IOProcessor plugin installed."
elif task == "embed":
err_msg = "Try switching the model's pooling_task via"
else: else:
err_msg = f"Unsupported task: {task!r}" err_msg = f"Unsupported task: {task!r}"
assert response.json()["error"]["message"].startswith(err_msg) assert response.json()["error"]["message"].startswith(err_msg)
+80
View File
@@ -16,6 +16,7 @@ from vllm.model_executor.layers.activation import (
NewGELU, NewGELU,
QuickGELU, QuickGELU,
SiluAndMul, SiluAndMul,
SiluAndMulWithClamp,
SwigluOAIAndMul, SwigluOAIAndMul,
SwigluStepAndMul, SwigluStepAndMul,
swiglustep_and_mul_triton, swiglustep_and_mul_triton,
@@ -116,6 +117,85 @@ def test_act_and_mul(
opcheck(fn, (out, x)) opcheck(fn, (out, x))
SWIGLU_LIMITS = [3.0, 7.0, 15.0]
@pytest.mark.parametrize("swiglu_limit", SWIGLU_LIMITS)
@pytest.mark.parametrize("num_tokens", NUM_TOKENS)
@pytest.mark.parametrize("d", D)
@pytest.mark.parametrize("dtype", DTYPES)
@pytest.mark.parametrize("seed", SEEDS)
@pytest.mark.parametrize("device", CUDA_DEVICES)
@torch.inference_mode()
def test_silu_and_mul_with_clamp(
default_vllm_config,
swiglu_limit: float,
num_tokens: int,
d: int,
dtype: torch.dtype,
seed: int,
device: str,
) -> None:
"""SiluAndMulWithClamp: cuda kernel must match native reference."""
set_random_seed(seed)
torch.set_default_device(device)
# Use large values to ensure clamping is exercised.
x = torch.randn(num_tokens, 2 * d, dtype=dtype) * swiglu_limit * 2
layer = SiluAndMulWithClamp(swiglu_limit, compile_native=False)
out = layer(x)
ref_out = layer.forward_native(x)
rtol = {
torch.float16: 2e-3,
torch.bfloat16: 2e-2,
torch.float: 1.3e-6,
}
torch.testing.assert_close(
out, ref_out, atol=get_default_atol(out), rtol=rtol[out.dtype]
)
# Verify clamping is actually being applied: the clamped output should
# differ from the unclamped SiluAndMul output when inputs are large.
unclamped_out = SiluAndMul.forward_native(x)
assert not torch.equal(ref_out.float(), unclamped_out.float()), (
"Input was not large enough to exercise the clamp; increase scale"
)
# Verify gate clamping semantics with a controlled scalar case.
# gate=large_val is clamped to limit first, then silu(limit) * 1.0.
x_gate = torch.tensor(
[[swiglu_limit * 20.0, 1.0]], dtype=torch.float32, device=device
)
out_gate = SiluAndMulWithClamp(swiglu_limit, compile_native=False)(x_gate)
expected_gate = torch.nn.functional.silu(
torch.tensor(swiglu_limit, dtype=torch.float32)
).item()
torch.testing.assert_close(
out_gate,
torch.tensor([[expected_gate]], dtype=torch.float32, device=device),
atol=1e-3,
rtol=1e-3,
)
# Verify up clamping semantics: up >> limit gets clamped to limit.
x_up = torch.tensor(
[[1.0, swiglu_limit * 20.0]], dtype=torch.float32, device=device
)
out_up = SiluAndMulWithClamp(swiglu_limit, compile_native=False)(x_up)
silu_1 = torch.nn.functional.silu(torch.tensor(1.0)).item()
torch.testing.assert_close(
out_up,
torch.tensor([[silu_1 * swiglu_limit]], dtype=torch.float32, device=device),
atol=1e-3,
rtol=1e-3,
)
# opcheck
out_buf = torch.empty(x.shape[:-1] + (d,), dtype=dtype, device=device)
opcheck(torch.ops._C.silu_and_mul_with_clamp, (out_buf, x, swiglu_limit))
@pytest.mark.parametrize( @pytest.mark.parametrize(
"activation", "activation",
[ [
+228 -4
View File
@@ -3,12 +3,11 @@
""" """
Round-trip tests for compressor → FP8 quant + KV cache insert → gather + dequant. Round-trip tests for compressor → FP8 quant + KV cache insert → gather + dequant.
Two paths tested: Four test functions cover five paths:
A) DeepseekV4 Attention: head_dim=512 (448 FP8 nope + 64 bf16 rope), quant_block=64 A) DeepseekV4 Attention: head_dim=512 (448 FP8 nope + 64 bf16 rope), quant_block=64
B) Indexer: head_dim=128 (all FP8), quant_block=128 B) Indexer: head_dim=128 (all FP8), quant_block=128
C) DeepseekV4 Attention magnitude range: correctness across small/large values
These serve as golden references for validating the future fused D) Indexer fused Triton kernel: compress+norm+rope+quant+insert
compressor+quant+cache kernel.
""" """
import math import math
@@ -21,6 +20,12 @@ from vllm.v1.attention.ops.deepseek_v4_ops import (
dequantize_and_gather_k_cache, dequantize_and_gather_k_cache,
quantize_and_insert_k_cache, quantize_and_insert_k_cache,
) )
from vllm.v1.attention.ops.deepseek_v4_ops.fused_compress_quant_cache import (
_fused_kv_compress_norm_rope_insert_indexer_attn,
_fused_kv_compress_norm_rope_insert_indexer_mxfp4_attn,
)
from .test_fused_indexer_q_rope_quant import quantize_to_mxfp4
def _ue8m0_reference(x: torch.Tensor, block_size: int, fp8_max: float): def _ue8m0_reference(x: torch.Tensor, block_size: int, fp8_max: float):
@@ -309,3 +314,222 @@ def test_deepseek_v4_quant_magnitude_range():
f"Token {t}: rel_err={rel_err:.4f}, abs_diff={abs_diff:.6f}, " f"Token {t}: rel_err={rel_err:.4f}, abs_diff={abs_diff:.6f}, "
f"magnitude={magnitude:.4f}" f"magnitude={magnitude:.4f}"
) )
# ── Test D: Indexer fused K-cache insert (Triton kernels) ────────────────────
#
# Both kernels share the same Triton signature; use_fp4 selects between them.
# Full pipeline: state-cache gather → softmax-weighted compress → RMSNorm →
# GPT-J RoPE → quant (MXFP4 or FP8) → paged cache insert.
def _reference_kv_compress_norm_rope(
state_cache: torch.Tensor,
block_table: torch.Tensor,
positions: torch.Tensor,
rms_weight: torch.Tensor,
cos_sin_cache: torch.Tensor,
compress_ratio: int = 1,
overlap: int = 0,
use_fp4: bool = False,
rms_eps: float = 1e-6,
fp8_max: float = 448.0,
):
"""Compress → RMSNorm → GPT-J RoPE → quantize.
Gathers (1+overlap)*compress_ratio state entries per output token, applies
per-element softmax over the scores, and computes the weighted kv sum.
Returns (quantized_values, scale) matching the kernel's output layout.
"""
device = state_cache.device
head_dim = rms_weight.shape[0]
rope_dim = cos_sin_cache.shape[-1]
state_block_size = state_cache.shape[1]
state_width = state_cache.shape[-1] // 2
nope_dim = head_dim - rope_dim
total = (1 + overlap) * compress_ratio
results = []
for pos in positions.tolist():
src = torch.arange(pos - total + 1, pos + 1, dtype=torch.int64, device=device)
valid = src >= 0
idx = src.clamp(min=0)
pages = block_table[0, idx // state_block_size]
offsets = idx % state_block_size
raw = state_cache[pages, offsets].float() # [total, state_dim]
# Group 0 (tokens 0..cr-1): kv[:H], score[SW:SW+H]
# Group 1 (tokens cr..2cr-1): kv[H:2H], score[SW+H:SW+2H]
if overlap:
sw = state_width
g0_kv = raw[:compress_ratio, :head_dim]
g1_kv = raw[compress_ratio:, head_dim : 2 * head_dim]
g0_scores = raw[:compress_ratio, sw : sw + head_dim]
g1_scores = raw[compress_ratio:, sw + head_dim : sw + 2 * head_dim]
kv = torch.cat([g0_kv, g1_kv])
scores = torch.cat([g0_scores, g1_scores])
else:
kv = raw[:, :head_dim]
scores = raw[:, state_width : state_width + head_dim]
scores[~valid] = float("-inf")
kv[~valid] = 0.0
weights = torch.softmax(scores, dim=0)
compressed = (kv * weights).sum(dim=0) # [H]
var = (compressed * compressed).mean()
normed = compressed * torch.rsqrt(var + rms_eps) * rms_weight.float()
compressed_pos = (pos // compress_ratio) * compress_ratio
cos, sin = cos_sin_cache[compressed_pos].float().chunk(2)
nope, rope = normed.split([nope_dim, rope_dim])
rope = torch.stack(
[rope[0::2] * cos - rope[1::2] * sin, rope[1::2] * cos + rope[0::2] * sin],
dim=-1,
).reshape(rope_dim)
results.append(torch.cat([nope, rope]).to(state_cache.dtype))
result = torch.stack(results)
if use_fp4:
return quantize_to_mxfp4(result)
else:
pairs = [
_ue8m0_reference(result[t], head_dim, fp8_max) for t in range(len(result))
]
quants, scales = zip(*pairs)
return torch.stack(quants), torch.cat(scales)
@pytest.mark.parametrize("num_tokens", [1, 7, 32])
@pytest.mark.parametrize("kv_block_size", [16, 32])
@pytest.mark.parametrize("use_fp4", [False, True])
def test_fused_kv_insert_indexer(num_tokens: int, kv_block_size: int, use_fp4: bool):
"""Fused K compress+norm+rope+quant+insert for the indexer KV cache."""
HEAD_DIM = 128
ROPE_DIM = 64
BLOCK_SIZE = 16
RMS_EPS = 1e-6
FP8_MAX = 448.0
device = "cuda"
torch.manual_seed(42)
compress_ratio = 4
if use_fp4:
TOKEN_STRIDE = HEAD_DIM // 2 # packed nibbles: 64 bytes
SCALE_DIM = HEAD_DIM // 32 # ue8m0 bytes: 4
QUANT_BLOCK = 32
kernel = _fused_kv_compress_norm_rope_insert_indexer_mxfp4_attn
else:
TOKEN_STRIDE = HEAD_DIM # FP8 bytes: 128
SCALE_DIM = 4 # 1 float32: 4 bytes
QUANT_BLOCK = HEAD_DIM
kernel = _fused_kv_compress_norm_rope_insert_indexer_attn
# overlap=1 whenever compress_ratio==4, matching DeepseekCompressor logic.
overlap = 1 if compress_ratio == 4 else 0
coff = 1 + overlap # multiplier for state_dim per entry
num_pages = (compress_ratio * num_tokens - 1) // BLOCK_SIZE + 2
state_cache = torch.randn(
num_pages,
BLOCK_SIZE,
2 * coff * HEAD_DIM, # kv_state + score_state, each coff*HEAD_DIM wide
dtype=torch.bfloat16,
device=device,
)
block_table = torch.arange(num_pages, dtype=torch.int32, device=device).unsqueeze(0)
token_to_req = torch.zeros(num_tokens, dtype=torch.int32, device=device)
slot_mapping = torch.arange(num_tokens, dtype=torch.int64, device=device)
positions = torch.arange(
compress_ratio - 1,
compress_ratio * num_tokens,
compress_ratio,
dtype=torch.int64,
device=device,
)
rms_weight = torch.randn(HEAD_DIM, dtype=torch.bfloat16, device=device)
cos_sin_cache = torch.randn(compress_ratio * num_tokens, ROPE_DIM, device=device)
kv_n_blocks = (num_tokens + kv_block_size - 1) // kv_block_size + 1
kv_cache = torch.zeros(
kv_n_blocks,
kv_block_size * (TOKEN_STRIDE + SCALE_DIM),
dtype=torch.uint8,
device=device,
)
kernel[(num_tokens,)](
state_cache,
state_cache.stride(0),
state_cache.stride(1),
token_to_req,
positions,
slot_mapping,
block_table,
block_table.stride(0),
BLOCK_SIZE,
rms_weight,
RMS_EPS,
cos_sin_cache,
cos_sin_cache.stride(0),
kv_cache,
slot_mapping,
kv_block_size,
HEAD_SIZE=HEAD_DIM,
TRITON_BLOCK_SIZE=HEAD_DIM,
STATE_WIDTH=coff * HEAD_DIM,
COMPRESS_RATIO=compress_ratio,
OVERLAP=overlap,
ROPE_HEAD_DIM=ROPE_DIM,
FP8_MAX=FP8_MAX,
QUANT_BLOCK=QUANT_BLOCK,
TOKEN_STRIDE=TOKEN_STRIDE,
SCALE_DIM=SCALE_DIM,
KV_BLOCK_STRIDE=kv_cache.stride(0),
num_warps=1,
)
k_quant, scale = _reference_kv_compress_norm_rope(
state_cache,
block_table,
positions,
rms_weight,
cos_sin_cache,
compress_ratio,
overlap,
use_fp4,
rms_eps=RMS_EPS,
fp8_max=FP8_MAX,
)
if use_fp4:
for i in range(num_tokens):
blk, pos = i // kv_block_size, i % kv_block_size
val_off = pos * TOKEN_STRIDE
fp4_actual = kv_cache[blk, val_off : val_off + TOKEN_STRIDE]
assert torch.equal(k_quant[i], fp4_actual), (
f"token {i}: packed nibbles differ, "
f"{(k_quant[i] != fp4_actual).sum()} "
f"/ {TOKEN_STRIDE}"
)
scale_off = kv_block_size * TOKEN_STRIDE + pos * SCALE_DIM
scale_actual = kv_cache[blk, scale_off : scale_off + SCALE_DIM]
assert torch.equal(scale_actual, scale[i]), (
f"token {i}: ue8m0 {scale_actual.tolist()} != {scale[i].tolist()}"
)
else:
k_quant = k_quant.view(torch.uint8)
for i in range(num_tokens):
blk, pos = i // kv_block_size, i % kv_block_size
val_off = pos * TOKEN_STRIDE
assert torch.equal(
k_quant[i], kv_cache[blk, val_off : val_off + TOKEN_STRIDE]
), f"token {i}: FP8 bytes differ"
scale_off = kv_block_size * TOKEN_STRIDE + pos * SCALE_DIM
actual_scale = kv_cache[blk, scale_off : scale_off + SCALE_DIM].view(
torch.float32
)
assert torch.equal(actual_scale, scale[i : i + 1]), (
f"token {i}: scale {actual_scale.item()} != {scale[i].item()}"
)
@@ -30,6 +30,56 @@ N_HEAD = 64
MAX_POS = 4096 MAX_POS = 4096
def quantize_to_mxfp4(
x: torch.Tensor,
) -> tuple[torch.Tensor, torch.Tensor]:
"""Reference MXFP4 quantization.
Args:
x: [..., head_dim] where head_dim is divisible by 32
Returns:
packed: [..., head_dim//2] uint8 2 E2M1 nibbles/byte, low nibble = even index
scales: [..., head_dim//32] uint8 1 ue8m0 byte
"""
MXFP4_BLOCK_SIZE = 32
orig_shape = x.shape
head_dim = orig_shape[-1]
n_blocks = head_dim // MXFP4_BLOCK_SIZE
x_f32 = x.float().reshape(-1, n_blocks, MXFP4_BLOCK_SIZE)
# Per-block ue8m0 scale: 2^ceil(log2(amax / 6.0)), stored as byte = exp + 127
# 6 * 2^-126 is from https://huggingface.co/deepseek-ai/DeepSeek-V4-Pro/blob/main/inference/kernel.py#L163
amax = x_f32.abs().amax(dim=-1, keepdim=True).clamp(min=6 * (2**-126))
log2_ratio = (amax * (1.0 / 6.0)).log2().ceil().clamp(-127.0, 127.0)
scale = log2_ratio.exp2()
ue8m0 = (log2_ratio + 127.0).to(torch.uint8) # [*, n_blocks]
# E2M1 round-to-nearest-even: midpoints round to the even code.
# E2M1 values: [0.00, 0.50, 1.00, 1.50, 2.00, 3.00, 4.00, 6.00]
# boundaries: [ 0.25, 0.75, 1.25, 1.75, 2.50, 3.50, 5.00]
x_scaled = (x_f32 / scale).clamp(-6.0, 6.0)
abs_x = x_scaled.abs()
code = torch.zeros_like(abs_x, dtype=torch.int32)
code = torch.where(abs_x > 0.25, 1, code)
code = torch.where(abs_x >= 0.75, 2, code)
code = torch.where(abs_x > 1.25, 3, code)
code = torch.where(abs_x >= 1.75, 4, code)
code = torch.where(abs_x > 2.5, 5, code)
code = torch.where(abs_x >= 3.5, 6, code)
code = torch.where(abs_x > 5.0, 7, code)
sign = ((x_scaled.view(torch.int32) >> 31) & 1).to(torch.uint8)
nibble = code.to(torch.uint8) | (sign << 3)
# Pack: even-index element → low nibble, odd-index → high nibble
nibble_flat = nibble.reshape(-1, head_dim)
packed = (nibble_flat[:, 0::2] | (nibble_flat[:, 1::2] << 4)).contiguous()
packed = packed.reshape(*orig_shape[:-1], head_dim // 2)
scales = ue8m0.view(*orig_shape[:-1], n_blocks)
return packed, scales
def _reference( def _reference(
positions: torch.Tensor, positions: torch.Tensor,
q: torch.Tensor, q: torch.Tensor,
@@ -37,6 +87,7 @@ def _reference(
weights: torch.Tensor, weights: torch.Tensor,
softmax_scale: float, softmax_scale: float,
head_scale: float, head_scale: float,
use_fp4: bool = False,
) -> tuple[torch.Tensor, torch.Tensor]: ) -> tuple[torch.Tensor, torch.Tensor]:
q_rot = q.clone() q_rot = q.clone()
ops.rotary_embedding( ops.rotary_embedding(
@@ -49,22 +100,33 @@ def _reference(
HEAD_DIM - ROPE_DIM, # rope_dim_offset → rotate the tail HEAD_DIM - ROPE_DIM, # rope_dim_offset → rotate the tail
False, False,
) )
q_fp8, q_scale = per_token_group_quant_fp8(
q_rot.view(-1, HEAD_DIM).contiguous(),
HEAD_DIM,
use_ue8m0=True,
)
q_fp8 = q_fp8.view(-1, N_HEAD, HEAD_DIM)
q_scale = q_scale.view(-1, N_HEAD)
weights_out = weights.to(torch.float32) * q_scale * softmax_scale * head_scale if use_fp4:
return q_fp8, weights_out q_packed, ue8m0 = quantize_to_mxfp4(q_rot.view(-1, N_HEAD, HEAD_DIM))
# Pack 4 ue8m0 bytes into 1 int32
q_scale = ue8m0.view(torch.int32).squeeze(-1)
# FP4 path: q_scale stays separate (cannot be folded into a per-token scalar)
weights_out = weights.to(torch.float32) * softmax_scale * head_scale
return (q_packed, q_scale), weights_out
else:
q_fp8, q_scale = per_token_group_quant_fp8(
q_rot.view(-1, HEAD_DIM).contiguous(),
HEAD_DIM,
use_ue8m0=True,
)
q_fp8 = q_fp8.view(-1, N_HEAD, HEAD_DIM)
q_scale = q_scale.view(-1, N_HEAD)
weights_out = weights.to(torch.float32) * q_scale * softmax_scale * head_scale
return q_fp8, weights_out
@pytest.mark.parametrize("num_tokens", [1, 7, 32, 257]) @pytest.mark.parametrize("num_tokens", [1, 7, 32, 257])
@pytest.mark.parametrize("cache_dtype", [torch.float32, torch.bfloat16]) @pytest.mark.parametrize("cache_dtype", [torch.float32, torch.bfloat16])
@pytest.mark.parametrize("use_fp4", [False, True])
@torch.inference_mode() @torch.inference_mode()
def test_fused_indexer_q_rope_quant_matches_unfused(num_tokens, cache_dtype): def test_fused_indexer_q_rope_quant_matches_unfused(num_tokens, cache_dtype, use_fp4):
device = "cuda" device = "cuda"
torch.manual_seed(0) torch.manual_seed(0)
@@ -77,21 +139,32 @@ def test_fused_indexer_q_rope_quant_matches_unfused(num_tokens, cache_dtype):
softmax_scale = HEAD_DIM**-0.5 softmax_scale = HEAD_DIM**-0.5
head_scale = N_HEAD**-0.5 head_scale = N_HEAD**-0.5
q_fp8_ref, weights_ref = _reference( q_quant_ref, weights_ref = _reference(
positions, q, cos_sin_cache, weights, softmax_scale, head_scale positions, q, cos_sin_cache, weights, softmax_scale, head_scale, use_fp4
) )
q_fp8_fused, weights_fused = fused_indexer_q_rope_quant( q_quant_fused, weights_fused = fused_indexer_q_rope_quant(
positions, q.clone(), cos_sin_cache, weights, softmax_scale, head_scale positions, q.clone(), cos_sin_cache, weights, softmax_scale, head_scale, use_fp4
) )
if use_fp4:
q_quant_ref, q_scale_ref = q_quant_ref
q_quant_fused, q_scale_fused = q_quant_fused
assert torch.equal(q_scale_ref, q_scale_fused), (
f"q_scale mismatch: "
f"{(q_scale_ref != q_scale_fused).sum().item()} "
f"/ {q_scale_ref.numel()} bytes differ"
)
# fp8 tensors aren't directly comparable via torch.equal — reinterpret as int8. # fp8 tensors aren't directly comparable via torch.equal — reinterpret as int8.
ref_bits = q_fp8_ref.view(torch.int8) ref_bits = q_quant_ref.view(torch.int8)
fused_bits = q_fp8_fused.view(torch.int8) fused_bits = q_quant_fused.view(torch.int8)
assert torch.equal(ref_bits, fused_bits), ( assert torch.equal(ref_bits, fused_bits), (
f"q_fp8 mismatch: " f"q_quant_fused mismatch: "
f"{(ref_bits != fused_bits).sum().item()} / {ref_bits.numel()} bytes differ" f"{(ref_bits != fused_bits).sum().item()} / {ref_bits.numel()} bytes differ"
) )
assert weights_fused.dtype == torch.float32
assert torch.equal(weights_ref, weights_fused), ( assert torch.equal(weights_ref, weights_fused), (
f"weights mismatch: max abs diff " f"weights mismatch: max abs diff "
f"{(weights_ref - weights_fused).abs().max().item()}" f"{(weights_ref - weights_fused).abs().max().item()}"
@@ -6,7 +6,6 @@ from transformers import AutoModel
from tests.models.utils import check_embeddings_close from tests.models.utils import check_embeddings_close
from vllm import TokensPrompt from vllm import TokensPrompt
from vllm.config import PoolerConfig
@pytest.mark.parametrize( @pytest.mark.parametrize(
@@ -22,7 +21,6 @@ def test_embed_models(hf_runner, vllm_runner, model: str):
with vllm_runner( with vllm_runner(
model, model,
runner="pooling", runner="pooling",
pooler_config=PoolerConfig(task="token_embed"),
max_model_len=128, max_model_len=128,
max_num_batched_tokens=chunk_size, max_num_batched_tokens=chunk_size,
enforce_eager=True, enforce_eager=True,
+14 -46
View File
@@ -3,6 +3,7 @@
import httpx import httpx
import openai import openai
import pytest import pytest
import pytest_asyncio
import torch import torch
from ....utils import RemoteOpenAIServer from ....utils import RemoteOpenAIServer
@@ -24,42 +25,29 @@ sentences_2 = [
similarity_reference = [[0.6259, 0.3474], [0.3309, 0.6734]] similarity_reference = [[0.6259, 0.3474], [0.3309, 0.6734]]
lexical_score_reference = [0.19554901123046875, 0.0] lexical_score_reference = [0.19554901123046875, 0.0]
colbert_score_reference = [0.7797, 0.4620] colbert_score_reference = [0.7797, 0.4620]
SUPPORTED_TASKS = ["embed", "token_embed", "token_classify"]
@pytest.fixture(scope="module", params=SUPPORTED_TASKS)
def pooling_task(request):
yield request.param
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
def server(pooling_task): def server():
args = [ args = [
"--max-model-len", "--max-model-len",
str(MAX_MODEL_LEN), str(MAX_MODEL_LEN),
"--hf-overrides", "--hf-overrides",
'{"architectures": ["BgeM3EmbeddingModel"]}', '{"architectures": ["BgeM3EmbeddingModel"]}',
"--pooler-config.task",
pooling_task,
] ]
with RemoteOpenAIServer(MODEL_NAME, args) as remote_server: with RemoteOpenAIServer(MODEL_NAME, args) as remote_server:
yield remote_server yield remote_server
@pytest_asyncio.fixture
async def client(server):
async with server.get_async_client() as async_client:
yield async_client
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_bge_m3_api_server_embedding(server, pooling_task): async def test_bge_m3_api_server_embedding(client: openai.AsyncOpenAI):
client = server.get_async_client()
if pooling_task != "embed":
with pytest.raises(openai.InternalServerError):
await run_client_embeddings(
client,
MODEL_NAME,
sentences_1,
)
return
embeddings_list_1 = await run_client_embeddings( embeddings_list_1 = await run_client_embeddings(
client, client,
MODEL_NAME, MODEL_NAME,
@@ -129,14 +117,7 @@ def compute_lexical_matching_score(
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_bge_m3_api_server_sparse_embedding(server, pooling_task): async def test_bge_m3_api_server_sparse_embedding(client: openai.AsyncOpenAI):
client = server.get_async_client()
if pooling_task != "token_classify":
with pytest.raises(openai.BadRequestError):
await sparse_embeddings(client, sentences_1)
return
embeddings_1 = await sparse_embeddings(client, sentences_1) embeddings_1 = await sparse_embeddings(client, sentences_1)
embeddings_2 = await sparse_embeddings(client, sentences_2) embeddings_2 = await sparse_embeddings(client, sentences_2)
@@ -156,11 +137,9 @@ async def test_bge_m3_api_server_sparse_embedding(server, pooling_task):
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_bge_m3_api_server_sparse_embedding_corner_case(server, pooling_task): async def test_bge_m3_api_server_sparse_embedding_corner_case(
if pooling_task != "token_classify": client: openai.AsyncOpenAI,
return ):
client = server.get_async_client()
embeddings = await sparse_embeddings(client, ["Hi"]) embeddings = await sparse_embeddings(client, ["Hi"])
assert len(embeddings) == 1 assert len(embeddings) == 1
assert 2673 in embeddings[0] assert 2673 in embeddings[0]
@@ -176,18 +155,7 @@ def colbert_score(q_reps: torch.Tensor, p_reps: torch.Tensor) -> torch.Tensor:
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_bge_m3_api_server_multi_vector(server, pooling_task): async def test_bge_m3_api_server_multi_vector(client: openai.AsyncOpenAI):
client = server.get_async_client()
if pooling_task != "token_embed":
with pytest.raises(openai.BadRequestError):
await client.post(
"../pooling",
body={"model": MODEL_NAME, "input": sentences_1, "task": "token_embed"},
cast_to=httpx.Response,
)
return
result_1 = await client.post( result_1 = await client.post(
"../pooling", "../pooling",
body={"model": MODEL_NAME, "input": sentences_1, "task": "token_embed"}, body={"model": MODEL_NAME, "input": sentences_1, "task": "token_embed"},
@@ -4,7 +4,6 @@ import pytest
import torch import torch
from vllm import TokensPrompt from vllm import TokensPrompt
from vllm.config import PoolerConfig
@pytest.mark.parametrize( @pytest.mark.parametrize(
@@ -21,7 +20,6 @@ def test_extract_hidden_states(hf_runner, vllm_runner, model: str):
max_model_len=128, max_model_len=128,
enforce_eager=True, enforce_eager=True,
runner="pooling", runner="pooling",
pooler_config=PoolerConfig(task="token_embed"),
enable_prefix_caching=True, enable_prefix_caching=True,
) as vllm_model: ) as vllm_model:
pooling_outputs = vllm_model.llm.encode( pooling_outputs = vllm_model.llm.encode(
@@ -46,3 +44,14 @@ def test_extract_hidden_states(hf_runner, vllm_runner, model: str):
assert len(output.prompt_token_ids) == n assert len(output.prompt_token_ids) == n
assert len(output.outputs.data) == n assert len(output.outputs.data) == n
assert output.num_cached_tokens == 0 assert output.num_cached_tokens == 0
# skip_reading_prefix_cache can still write to cache
# to accelerate following requests
pooling_outputs = vllm_model.llm.encode(
[TokensPrompt(prompt_token_ids=t) for t in token_prompts],
pooling_task="embed",
)
for n, output in zip(n_prompt_tokens, pooling_outputs):
assert len(output.prompt_token_ids) == n
assert output.num_cached_tokens > 0
@@ -5,7 +5,6 @@ import torch
from transformers import AutoModel from transformers import AutoModel
from tests.models.utils import check_embeddings_close from tests.models.utils import check_embeddings_close
from vllm.config import PoolerConfig
@pytest.mark.parametrize( @pytest.mark.parametrize(
@@ -18,7 +17,6 @@ def test_embed_models(hf_runner, vllm_runner, example_prompts, model: str, dtype
with vllm_runner( with vllm_runner(
model, model,
runner="pooling", runner="pooling",
pooler_config=PoolerConfig(task="token_embed"),
max_model_len=None, max_model_len=None,
) as vllm_model: ) as vllm_model:
vllm_outputs = vllm_model.token_embed(example_prompts) vllm_outputs = vllm_model.token_embed(example_prompts)
@@ -146,7 +146,7 @@ def test_multi_vector_retrieval_models_using_normalize(
model, model,
max_model_len=512, max_model_len=512,
dtype=dtype, dtype=dtype,
pooler_config=PoolerConfig(use_activation=False, task="token_embed"), pooler_config=PoolerConfig(use_activation=False),
) as vllm_model: ) as vllm_model:
wo_normalize = vllm_model.token_embed(example_prompts) wo_normalize = vllm_model.token_embed(example_prompts)
@@ -154,7 +154,7 @@ def test_multi_vector_retrieval_models_using_normalize(
model, model,
max_model_len=512, max_model_len=512,
dtype=dtype, dtype=dtype,
pooler_config=PoolerConfig(use_activation=True, task="token_embed"), pooler_config=PoolerConfig(use_activation=True),
) as vllm_model: ) as vllm_model:
w_normalize = vllm_model.token_embed(example_prompts) w_normalize = vllm_model.token_embed(example_prompts)
@@ -188,6 +188,30 @@ class TestExtractToolCalls:
"location": "NYC" "location": "NYC"
} }
def test_type_conversion_in_non_streaming(self):
"""Non-streaming extraction must convert params using the tool schema."""
tool = ChatCompletionToolsParam(
function=FunctionDefinition(
name="toggle",
parameters={
"type": "object",
"properties": {
"enabled": {"type": "boolean"},
"count": {"type": "integer"},
},
},
),
)
parser = make_parser(tools=[tool])
model_output = build_tool_call("toggle", {"enabled": "true", "count": "42"})
result = parser.extract_tool_calls(model_output, None)
assert result.tools_called
assert len(result.tool_calls) == 1
args = json.loads(result.tool_calls[0].function.arguments)
assert args == {"enabled": True, "count": 42}
assert isinstance(args["enabled"], bool)
assert isinstance(args["count"], int)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Tests: extract_tool_calls_streaming # Tests: extract_tool_calls_streaming
+48
View File
@@ -2074,6 +2074,54 @@ def test_auto_fit_max_model_len_not_triggered():
assert vllm_config.model_config.max_model_len == 16 assert vllm_config.model_config.max_model_len == 16
def test_auto_fit_max_model_len_respects_num_gpu_blocks_override():
"""Auto-fit must size max_model_len against the override-clamped pool, not
the raw `available_memory`. Without this, auto-fit could pick a
max_model_len that no longer fits once `num_gpu_blocks_override` is applied.
"""
model_config = ModelConfig(max_model_len=16384)
model_config.original_max_model_len = -1 # request auto-fit
vllm_config = VllmConfig(model_config=model_config)
# Cap the cache to 32 blocks regardless of available memory.
vllm_config.cache_config.num_gpu_blocks_override = 32
mem_per_block_per_layer = 16 * 2 * 64 * 4 * 2
kv_cache_specs = {
"layer_1": new_kv_cache_spec(), # block_size=16
"layer_2": new_kv_cache_spec(),
}
# Plenty of raw memory (1024 blocks per layer would fit max_model_len=16384).
large_available_memory = mem_per_block_per_layer * 2 * 1024
get_kv_cache_configs(vllm_config, [kv_cache_specs], [large_available_memory])
# 32 blocks * block_size 16 = 512 token slots, so max_model_len must
# auto-fit at or below that.
assert 0 < vllm_config.model_config.max_model_len <= 32 * 16
def test_check_enough_kv_cache_memory_respects_num_gpu_blocks_override():
"""Admission check must use the override-clamped pool size, not raw
`available_memory`. Without this, startup could accept a max_model_len
that does not actually fit in `num_gpu_blocks_override` blocks.
"""
model_config = ModelConfig(max_model_len=16384)
vllm_config = VllmConfig(model_config=model_config)
# 32 blocks is far too small for max_model_len=16384 (would need 1024).
vllm_config.cache_config.num_gpu_blocks_override = 32
mem_per_block_per_layer = 16 * 2 * 64 * 4 * 2
kv_cache_specs = {
"layer_1": new_kv_cache_spec(),
"layer_2": new_kv_cache_spec(),
}
# Plenty of raw memory: a bytes-only check against this would pass.
large_available_memory = mem_per_block_per_layer * 2 * 1024
with pytest.raises(ValueError, match="max seq len"):
get_kv_cache_configs(vllm_config, [kv_cache_specs], [large_available_memory])
def test_unify_hybrid_kv_cache_specs(): def test_unify_hybrid_kv_cache_specs():
# 1. has_full_attention and has_sliding_window # 1. has_full_attention and has_sliding_window
before_spec_1 = new_kv_cache_spec() before_spec_1 = new_kv_cache_spec()
+108
View File
@@ -2512,3 +2512,111 @@ def test_block_lookup_cache_multi_blocks_per_key():
assert cache.pop(key1, 11) is block11 assert cache.pop(key1, 11) is block11
assert cache.get_one_block(key1) is None assert cache.get_one_block(key1) is None
assert cache.pop(key1, 12) is None assert cache.pop(key1, 12) is None
def test_can_fit_full_sequence_swa_cap_admits_long_prompt():
"""Hybrid full+SWA model with a pool sized at the startup minimum should
admit a prompt longer than the SWA cap, because SlidingWindowManager
recycles blocks during chunked prefill (issue #39734)."""
block_size = 16
sliding_window = 4 * block_size # 64 tokens
max_num_batched_tokens = 8 * block_size # 128 tokens
max_model_len = 64 * block_size # 1024 tokens — much larger than the SWA cap
# Startup pool sizing: full demands cdiv(max_model_len, bs) = 64 blocks,
# SWA demands cdiv(SW-1+max_batched, bs) + 1 = cdiv(191, 16) + 1 = 13.
# Pool minimum = 64 + 13 = 77; +1 for the null block.
num_blocks = 64 + 13 + 1
config = KVCacheConfig(
num_blocks=num_blocks,
kv_cache_tensors=[],
kv_cache_groups=[
KVCacheGroupSpec(
["layer_full"],
FullAttentionSpec(
block_size=block_size,
num_kv_heads=1,
head_size=1,
dtype=torch.float32,
),
),
KVCacheGroupSpec(
["layer_swa"],
SlidingWindowSpec(
block_size=block_size,
num_kv_heads=1,
head_size=1,
dtype=torch.float32,
sliding_window=sliding_window,
),
),
],
)
manager = KVCacheManager(
config,
max_model_len=max_model_len,
max_num_batched_tokens=max_num_batched_tokens,
enable_caching=True,
hash_block_size=block_size,
)
# A prompt that is shorter than max_model_len but longer than SW + chunk:
# cdiv(prompt_len, bs) = 32 blocks. Without the cap, admission would
# demand 32 (full) + 32 (SWA) = 64 blocks. With the cap, SWA contributes
# only 13, so total = 32 + 13 = 45 ≤ pool size.
prompt_len = 32 * block_size
req = make_request("long", list(range(prompt_len)), block_size, sha256)
assert manager.can_fit_full_sequence(req)
def test_can_fit_full_sequence_full_attention_still_gates_oversized():
"""The cap only loosens the SWA group; a prompt that exceeds the
full-attention pool capacity must still be rejected."""
block_size = 16
sliding_window = 4 * block_size
max_num_batched_tokens = 8 * block_size
max_model_len = 64 * block_size
# Provide a tiny pool — even a small prompt should be rejected.
num_blocks = 5
config = KVCacheConfig(
num_blocks=num_blocks,
kv_cache_tensors=[],
kv_cache_groups=[
KVCacheGroupSpec(
["layer_full"],
FullAttentionSpec(
block_size=block_size,
num_kv_heads=1,
head_size=1,
dtype=torch.float32,
),
),
KVCacheGroupSpec(
["layer_swa"],
SlidingWindowSpec(
block_size=block_size,
num_kv_heads=1,
head_size=1,
dtype=torch.float32,
sliding_window=sliding_window,
),
),
],
)
manager = KVCacheManager(
config,
max_model_len=max_model_len,
max_num_batched_tokens=max_num_batched_tokens,
enable_caching=True,
hash_block_size=block_size,
)
# 16 blocks of full attention demand alone exceeds the 5-block pool.
prompt_len = 16 * block_size
req = make_request("oversized", list(range(prompt_len)), block_size, sha256)
assert not manager.can_fit_full_sequence(req)
@@ -22,11 +22,13 @@ pytestmark = pytest.mark.cpu_test
def get_sliding_window_manager(sliding_window_spec, block_pool, enable_caching=True): def get_sliding_window_manager(sliding_window_spec, block_pool, enable_caching=True):
# Tests don't exercise admission gating; pass a large cap that is a no-op.
return SlidingWindowManager( return SlidingWindowManager(
sliding_window_spec, sliding_window_spec,
block_pool=block_pool, block_pool=block_pool,
enable_caching=enable_caching, enable_caching=enable_caching,
kv_cache_group_id=0, kv_cache_group_id=0,
max_admission_blocks_per_request=10**9,
) )
@@ -38,6 +40,7 @@ def get_chunked_local_attention_manager(
block_pool=block_pool, block_pool=block_pool,
enable_caching=enable_caching, enable_caching=enable_caching,
kv_cache_group_id=0, kv_cache_group_id=0,
max_admission_blocks_per_request=10**9,
) )
@@ -324,10 +324,13 @@ def run_test(
): ):
spec_decoding = spec_config is not None spec_decoding = spec_config is not None
cache_arg: dict[str, Any] = ( cache_arg: dict[str, Any] = (
# Force preemptions # Force preemptions: with 32 blocks the cache holds at most a single
dict(num_gpu_blocks_override=32) # max-length request, so the ~34 concurrent prompts contend and trigger
# preemption. (Prompts here are << max_model_len, so dropping
# max_model_len from 4096 to 512 doesn't change generation behavior.)
dict(num_gpu_blocks_override=32, max_model_len=512)
if test_preemption if test_preemption
else dict(gpu_memory_utilization=0.9) else dict(gpu_memory_utilization=0.9, max_model_len=4096)
) )
spec_mml = (spec_config or {}).get("max_model_len") spec_mml = (spec_config or {}).get("max_model_len")
spec_method = (spec_config or {}).get("method", "none") spec_method = (spec_config or {}).get("method", "none")
@@ -343,7 +346,6 @@ def run_test(
with VllmRunner( with VllmRunner(
model, model,
max_model_len=4096,
enable_chunked_prefill=test_prefill_chunking, enable_chunked_prefill=test_prefill_chunking,
# Force prefill chunking # Force prefill chunking
max_num_batched_tokens=48 if test_prefill_chunking else None, max_num_batched_tokens=48 if test_prefill_chunking else None,
@@ -8,11 +8,16 @@ from unittest.mock import Mock
import pytest import pytest
from vllm.config import ModelConfig, SchedulerConfig, VllmConfig from vllm.config import ModelConfig, SchedulerConfig, VllmConfig
from vllm.reasoning import ReasoningParser
from vllm.v1.request import Request from vllm.v1.request import Request
from vllm.v1.structured_output import StructuredOutputManager from vllm.v1.structured_output import StructuredOutputManager
class MockReasoner:
def __init__(self, tokenizer):
self.is_reasoning_end = Mock(return_value=False)
self.is_reasoning_end_streaming = Mock(return_value=False)
class TestReasoningStructuredOutput: class TestReasoningStructuredOutput:
"""Test reasoning-aware structured output functionality.""" """Test reasoning-aware structured output functionality."""
@@ -50,13 +55,6 @@ class TestReasoningStructuredOutput:
config.speculative_config = None config.speculative_config = None
return config return config
@pytest.fixture
def mock_reasoning_parser(self):
"""Create a mock ReasoningParser."""
parser = Mock(spec=ReasoningParser)
parser.is_reasoning_end = Mock(return_value=False)
return parser
@pytest.fixture @pytest.fixture
def mock_request_with_structured_output(self): def mock_request_with_structured_output(self):
"""Create a mock request with structured output.""" """Create a mock request with structured output."""
@@ -64,6 +62,8 @@ class TestReasoningStructuredOutput:
request.structured_output_request = Mock() request.structured_output_request = Mock()
request.structured_output_request.reasoning_ended = None request.structured_output_request.reasoning_ended = None
request.structured_output_request.grammar = Mock() request.structured_output_request.grammar = Mock()
request.structured_output_request.reasoning_parser_kwargs = None
request.structured_output_request.reasoner = None
request.structured_output_request.grammar.is_terminated = Mock( request.structured_output_request.grammar.is_terminated = Mock(
return_value=False return_value=False
) )
@@ -74,6 +74,13 @@ class TestReasoningStructuredOutput:
request.num_output_placeholders = 0 request.num_output_placeholders = 0
return request return request
@pytest.fixture
def manager_with_reasoner(self, mock_vllm_config):
manager = StructuredOutputManager(mock_vllm_config)
manager.reasoner_cls = MockReasoner
manager.tokenizer = Mock()
return manager
def test_should_fill_bitmask_with_enable_in_reasoning( def test_should_fill_bitmask_with_enable_in_reasoning(
self, mock_vllm_config, mock_request_with_structured_output self, mock_vllm_config, mock_request_with_structured_output
): ):
@@ -89,22 +96,17 @@ class TestReasoningStructuredOutput:
def test_should_fill_bitmask_without_enable_in_reasoning( def test_should_fill_bitmask_without_enable_in_reasoning(
self, self,
mock_vllm_config, manager_with_reasoner,
mock_request_with_structured_output, mock_request_with_structured_output,
mock_reasoning_parser,
): ):
"""Test should_fill_bitmask when enable_in_reasoning is False.""" """Test should_fill_bitmask when enable_in_reasoning is False."""
# Keep enable_in_reasoning as False (default) # Keep enable_in_reasoning as False (default)
config = mock_vllm_config.structured_outputs_config config = manager_with_reasoner.vllm_config.structured_outputs_config
assert config.enable_in_reasoning is False assert config.enable_in_reasoning is False
manager = StructuredOutputManager(mock_vllm_config) result = manager_with_reasoner.should_fill_bitmask(
manager.reasoner = mock_reasoning_parser mock_request_with_structured_output
)
# Mock reasoning not ended
mock_reasoning_parser.is_reasoning_end.return_value = False
result = manager.should_fill_bitmask(mock_request_with_structured_output)
# Should set reasoning_ended and return its value # Should set reasoning_ended and return its value
assert ( assert (
@@ -118,68 +120,92 @@ class TestReasoningStructuredOutput:
): ):
"""Test should_fill_bitmask when no reasoner is configured.""" """Test should_fill_bitmask when no reasoner is configured."""
manager = StructuredOutputManager(mock_vllm_config) manager = StructuredOutputManager(mock_vllm_config)
manager.reasoner = None
result = manager.should_fill_bitmask(mock_request_with_structured_output) result = manager.should_fill_bitmask(mock_request_with_structured_output)
# Should default to True when no reasoner # Should default to True when no reasoner
assert result is True assert result is True
def test_should_fill_bitmask_uses_request_reasoning_parser_kwargs(
self, mock_vllm_config, mock_request_with_structured_output
):
"""Test request-level parser kwargs override the default reasoner."""
class KwargReasoner:
def __init__(self, tokenizer, chat_template_kwargs=None):
self.chat_template_kwargs = chat_template_kwargs or {}
def is_reasoning_end(self, input_ids):
return not self.chat_template_kwargs.get("enable_thinking", False)
manager = StructuredOutputManager(mock_vllm_config)
manager.reasoner_cls = KwargReasoner
manager.tokenizer = Mock()
structured_req = mock_request_with_structured_output.structured_output_request
structured_req.reasoning_parser_kwargs = {
"chat_template_kwargs": {"enable_thinking": True}
}
result = manager.should_fill_bitmask(mock_request_with_structured_output)
assert result is False
assert (
mock_request_with_structured_output.structured_output_request.reasoner
is not None
)
def test_should_advance_with_enable_in_reasoning( def test_should_advance_with_enable_in_reasoning(
self, self,
mock_vllm_config, manager_with_reasoner,
mock_request_with_structured_output, mock_request_with_structured_output,
mock_reasoning_parser,
): ):
"""Test should_advance when enable_in_reasoning is True.""" """Test should_advance when enable_in_reasoning is True."""
# Enable enable_in_reasoning # Enable enable_in_reasoning
mock_vllm_config.structured_outputs_config.enable_in_reasoning = True manager_with_reasoner.enable_in_reasoning = True
manager = StructuredOutputManager(mock_vllm_config)
manager.reasoner = mock_reasoning_parser
# Should always return True when enable_in_reasoning is enabled # Should always return True when enable_in_reasoning is enabled
result = manager.should_advance(mock_request_with_structured_output) result = manager_with_reasoner.should_advance(
mock_request_with_structured_output
)
assert result is True assert result is True
def test_should_advance_reasoning_not_ended( def test_should_advance_reasoning_not_ended(
self, self,
mock_vllm_config, manager_with_reasoner,
mock_request_with_structured_output, mock_request_with_structured_output,
mock_reasoning_parser,
): ):
"""Test should_advance when reasoning has not ended.""" """Test should_advance when reasoning has not ended."""
manager = StructuredOutputManager(mock_vllm_config)
manager.reasoner = mock_reasoning_parser
# Set reasoning as not ended # Set reasoning as not ended
( (
mock_request_with_structured_output.structured_output_request mock_request_with_structured_output.structured_output_request
).reasoning_ended = False ).reasoning_ended = False
mock_reasoning_parser.is_reasoning_end.return_value = False
result = manager.should_advance(mock_request_with_structured_output) result = manager_with_reasoner.should_advance(
mock_request_with_structured_output
)
# Should return False since reasoning hasn't ended # Should return False since reasoning hasn't ended
assert result is False assert result is False
def test_should_advance_reasoning_just_ended( def test_should_advance_reasoning_just_ended(
self, self,
mock_vllm_config, manager_with_reasoner,
mock_request_with_structured_output, mock_request_with_structured_output,
mock_reasoning_parser,
): ):
"""Test should_advance when reasoning ends in current step.""" """Test should_advance when reasoning ends in current step."""
manager = StructuredOutputManager(mock_vllm_config)
manager.reasoner = mock_reasoning_parser
# Set reasoning as not ended initially, but ends in this step # Set reasoning as not ended initially, but ends in this step
( (
mock_request_with_structured_output.structured_output_request mock_request_with_structured_output.structured_output_request
).reasoning_ended = False ).reasoning_ended = False
mock_reasoning_parser.is_reasoning_end.return_value = True reasoner = MockReasoner(tokenizer=Mock())
reasoner.is_reasoning_end_streaming.return_value = True
structured_req = mock_request_with_structured_output.structured_output_request
structured_req.reasoner = reasoner
result = manager.should_advance(mock_request_with_structured_output) result = manager_with_reasoner.should_advance(
mock_request_with_structured_output
)
# Should set reasoning_ended to True but return False for this step # Should set reasoning_ended to True but return False for this step
assert ( assert (
@@ -190,20 +216,18 @@ class TestReasoningStructuredOutput:
def test_should_advance_reasoning_already_ended( def test_should_advance_reasoning_already_ended(
self, self,
mock_vllm_config, manager_with_reasoner,
mock_request_with_structured_output, mock_request_with_structured_output,
mock_reasoning_parser,
): ):
"""Test should_advance when reasoning has already ended.""" """Test should_advance when reasoning has already ended."""
manager = StructuredOutputManager(mock_vllm_config)
manager.reasoner = mock_reasoning_parser
# Set reasoning as already ended # Set reasoning as already ended
( (
mock_request_with_structured_output.structured_output_request mock_request_with_structured_output.structured_output_request
).reasoning_ended = True ).reasoning_ended = True
result = manager.should_advance(mock_request_with_structured_output) result = manager_with_reasoner.should_advance(
mock_request_with_structured_output
)
# Should return True since reasoning has ended # Should return True since reasoning has ended
assert result is True assert result is True
+10
View File
@@ -1432,6 +1432,10 @@ class VllmConfig:
cudagraph_capture_sizes = [1, 2, 4] + list(range(8, 256, 8)) + list( cudagraph_capture_sizes = [1, 2, 4] + list(range(8, 256, 8)) + list(
range(256, max_graph_size + 1, 16)) range(256, max_graph_size + 1, 16))
`max_num_batched_tokens` is also appended to the list if it fits
within `max_cudagraph_capture_size`, so the max batch size is captured
even when off-stride.
In the end, `vllm_config.compilation_config.cudagraph_capture_sizes` In the end, `vllm_config.compilation_config.cudagraph_capture_sizes`
will be the final sizes to capture cudagraph (in ascending order). will be the final sizes to capture cudagraph (in ascending order).
@@ -1520,6 +1524,12 @@ class VllmConfig:
cudagraph_capture_sizes += list( cudagraph_capture_sizes += list(
range(256, max_cudagraph_capture_size + 1, 16) range(256, max_cudagraph_capture_size + 1, 16)
) )
# ensure max_num_tokens is captured if within max capture size
if (
max_num_tokens <= max_cudagraph_capture_size
and max_num_tokens not in cudagraph_capture_sizes
):
cudagraph_capture_sizes.append(max_num_tokens)
# de-duplicate and sort the sizes # de-duplicate and sort the sizes
cudagraph_capture_sizes = sorted(set(cudagraph_capture_sizes)) cudagraph_capture_sizes = sorted(set(cudagraph_capture_sizes))
+40 -32
View File
@@ -128,13 +128,6 @@ class CuMemAllocator:
return CuMemAllocator.instance return CuMemAllocator.instance
def __init__(self): def __init__(self):
conf = os.environ.get("PYTORCH_CUDA_ALLOC_CONF", "")
assert "expandable_segments:True" not in conf, (
"Expandable segments are not compatible with memory pool. "
"Please track https://github.com/pytorch/pytorch/issues/147851 "
"for the latest updates."
)
self.pointer_to_data: dict[int, AllocationData] = {} self.pointer_to_data: dict[int, AllocationData] = {}
self.current_tag: str = CuMemAllocator.default_tag self.current_tag: str = CuMemAllocator.default_tag
self.allocator_and_pools: dict[str, Any] = {} self.allocator_and_pools: dict[str, Any] = {}
@@ -264,34 +257,49 @@ class CuMemAllocator:
assert isinstance(tag, str) assert isinstance(tag, str)
# Expandable segments are incompatible with the memory pool used for
# sleep mode (see https://github.com/pytorch/pytorch/issues/147851).
# If the user has enabled expandable segments via
# PYTORCH_CUDA_ALLOC_CONF, temporarily disable them for the duration
# of the memory pool context and restore on exit.
conf = os.environ.get("PYTORCH_CUDA_ALLOC_CONF", "")
expandable_was_enabled = "expandable_segments:True" in conf
if expandable_was_enabled:
torch.cuda.memory._set_allocator_settings("expandable_segments:False")
old_tag = self.current_tag old_tag = self.current_tag
self.current_tag = tag self.current_tag = tag
with use_memory_pool_with_allocator( try:
self.python_malloc_callback, self.python_free_callback with use_memory_pool_with_allocator(
) as data: self.python_malloc_callback, self.python_free_callback
# start to hit another PyTorch bug in PyTorch 2.6, ) as data:
# possibly because of gc-related issue w.r.t. the allocator and # start to hit another PyTorch bug in PyTorch 2.6,
# the memory pool. # possibly because of gc-related issue w.r.t. the allocator
# to avoid the issue, we keep a reference of the data. # and the memory pool.
# see https://github.com/pytorch/pytorch/issues/146431 . # to avoid the issue, we keep a reference of the data.
self.allocator_and_pools[tag] = data # see https://github.com/pytorch/pytorch/issues/146431 .
yield self.allocator_and_pools[tag] = data
# PyTorch's bug, calling torch.cuda.empty_cache() will error yield
# when using pluggable allocator, see # PyTorch's bug, calling torch.cuda.empty_cache() will error
# https://github.com/pytorch/pytorch/issues/145168 . # when using pluggable allocator, see
# if we have some memory allocated and then freed, # https://github.com/pytorch/pytorch/issues/145168 .
# the memory will not be released, e.g. in online quantization, # if we have some memory allocated and then freed,
# where the model is created in higher precision, and then # the memory will not be released, e.g. in online
# quantized in lower precision. # quantization, where the model is created in higher
# Find all unused allocations and manually release them. # precision, and then quantized in lower precision.
# TODO: we should expose `empty_cache` method in the memory pool. # Find all unused allocations and manually release them.
# TODO: ask for help from PyTorch team to expose this method. # TODO: we should expose `empty_cache` method in the memory
allocations = data[0].snapshot() # pool.
for allocation in allocations: # TODO: ask for help from PyTorch team to expose this method.
if allocation["allocated_size"] == 0: allocations = data[0].snapshot()
handle = self._python_free_callback(allocation["address"]) for allocation in allocations:
unmap_and_release(handle) if allocation["allocated_size"] == 0:
handle = self._python_free_callback(allocation["address"])
unmap_and_release(handle)
finally:
self.current_tag = old_tag self.current_tag = old_tag
if expandable_was_enabled:
torch.cuda.memory._set_allocator_settings("expandable_segments:True")
def get_current_usage(self) -> int: def get_current_usage(self) -> int:
""" """
@@ -584,6 +584,8 @@ class FlashInferNVLinkOneSidedManager(All2AllManagerBase):
top_k: int, top_k: int,
num_experts: int, num_experts: int,
hidden_size: int, hidden_size: int,
dispatch_dtype_bytes_per_elem: int = 0,
dispatch_scale_bytes_per_token: int = 0,
): ):
"""Initialize the MoeAlltoAll workspace.""" """Initialize the MoeAlltoAll workspace."""
if self.initialized: if self.initialized:
@@ -614,9 +616,13 @@ class FlashInferNVLinkOneSidedManager(All2AllManagerBase):
ep_config = MnnvlConfig( ep_config = MnnvlConfig(
comm_backend=CustomCommunicator(self.cpu_group), comm_backend=CustomCommunicator(self.cpu_group),
) )
if dispatch_dtype_bytes_per_elem == 0:
hidden_bytes = hidden_size // 2
else:
hidden_bytes = hidden_size * dispatch_dtype_bytes_per_elem
total_dispatch_payload_size_per_token = ( total_dispatch_payload_size_per_token = (
hidden_size // 2 # nvfp4 hidden states hidden_bytes
+ hidden_size // 16 # fp8 scaling factors + dispatch_scale_bytes_per_token
+ top_k * 4 # int32 topks ids + top_k * 4 # int32 topks ids
+ top_k * 4 # float32 topk weights + top_k * 4 # float32 topk weights
) )
+1
View File
@@ -78,6 +78,7 @@ class EngineClient(ABC):
priority: int = 0, priority: int = 0,
data_parallel_rank: int | None = None, data_parallel_rank: int | None = None,
reasoning_ended: bool | None = None, reasoning_ended: bool | None = None,
reasoning_parser_kwargs: dict[str, Any] | None = None,
) -> AsyncGenerator[RequestOutput, None]: ) -> AsyncGenerator[RequestOutput, None]:
"""Generate outputs for a request.""" """Generate outputs for a request."""
... ...
+8 -5
View File
@@ -79,7 +79,7 @@ from vllm.renderers.inputs.preprocess import (
prompt_to_seq, prompt_to_seq,
) )
from vllm.sampling_params import BeamSearchParams, RequestOutputKind, SamplingParams from vllm.sampling_params import BeamSearchParams, RequestOutputKind, SamplingParams
from vllm.tasks import SCORE_TYPE_MAP, PoolingTask from vllm.tasks import PoolingTask
from vllm.tokenizers import TokenizerLike from vllm.tokenizers import TokenizerLike
from vllm.usage.usage_lib import UsageContext from vllm.usage.usage_lib import UsageContext
from vllm.utils.counter import Counter from vllm.utils.counter import Counter
@@ -1204,9 +1204,12 @@ class LLM:
f"Supported tasks: {self.supported_tasks}" f"Supported tasks: {self.supported_tasks}"
) )
else: else:
raise ValueError( logger.warning_once(
f"Try switching the model's pooling_task " "Pooling multitask support is deprecated and will "
f'via `PoolerConfig(task="{pooling_task}")`' "be removed in v0.20. When the default pooling task is "
"not what you want, you need to manually specify it "
'via PoolerConfig(task="%s"). ',
pooling_task,
) )
if pooling_task == "plugin" and "plugin" not in self.pooling_io_processors: if pooling_task == "plugin" and "plugin" not in self.pooling_io_processors:
@@ -1409,7 +1412,7 @@ class LLM:
"pooling model." "pooling model."
) )
score_type: str | None = SCORE_TYPE_MAP.get(self.pooling_task, None) # type: ignore[arg-type] score_type = self.model_config.score_type
if ( if (
score_type == "cross-encoder" score_type == "cross-encoder"
and getattr(self.model_config.hf_config, "num_labels", 0) != 1 and getattr(self.model_config.hf_config, "num_labels", 0) != 1
@@ -347,6 +347,11 @@ class OpenAIServingChat(OpenAIServing):
priority=request.priority, priority=request.priority,
data_parallel_rank=data_parallel_rank, data_parallel_rank=data_parallel_rank,
reasoning_ended=reasoning_ended, reasoning_ended=reasoning_ended,
reasoning_parser_kwargs={
"chat_template_kwargs": chat_template_kwargs,
}
if reasoning_parser
else None,
) )
generators.append(generator) generators.append(generator)
+10 -1
View File
@@ -472,9 +472,13 @@ class OpenAIServingResponses(OpenAIServing):
context = SimpleContext() context = SimpleContext()
if self.parser and self.parser.reasoning_parser_cls is not None: if self.parser and self.parser.reasoning_parser_cls is not None:
chat_template_kwargs = self._effective_chat_template_kwargs(request)
reasoning_parser_kwargs = {
"chat_template_kwargs": chat_template_kwargs,
}
reasoning_parser = self.parser.reasoning_parser_cls( reasoning_parser = self.parser.reasoning_parser_cls(
tokenizer, tokenizer,
chat_template_kwargs=self._effective_chat_template_kwargs(request), chat_template_kwargs=chat_template_kwargs,
) )
if ( if (
isinstance( isinstance(
@@ -497,6 +501,9 @@ class OpenAIServingResponses(OpenAIServing):
lora_request=lora_request, lora_request=lora_request,
priority=request.priority, priority=request.priority,
trace_headers=trace_headers, trace_headers=trace_headers,
reasoning_parser_kwargs=reasoning_parser_kwargs
if self.parser and self.parser.reasoning_parser_cls is not None
else None,
) )
generators.append(generator) generators.append(generator)
@@ -643,6 +650,7 @@ class OpenAIServingResponses(OpenAIServing):
lora_request: LoRARequest | None = None, lora_request: LoRARequest | None = None,
priority: int = 0, priority: int = 0,
trace_headers: Mapping[str, str] | None = None, trace_headers: Mapping[str, str] | None = None,
reasoning_parser_kwargs: dict[str, Any] | None = None,
): ):
max_model_len = self.model_config.max_model_len max_model_len = self.model_config.max_model_len
@@ -666,6 +674,7 @@ class OpenAIServingResponses(OpenAIServing):
lora_request=lora_request, lora_request=lora_request,
trace_headers=trace_headers, trace_headers=trace_headers,
priority=priority, priority=priority,
reasoning_parser_kwargs=reasoning_parser_kwargs,
) )
async for res in generator: async for res in generator:
+12 -3
View File
@@ -15,7 +15,10 @@ from starlette.datastructures import Headers
from vllm import PoolingParams, PoolingRequestOutput, envs from vllm import PoolingParams, PoolingRequestOutput, envs
from vllm.config import VllmConfig from vllm.config import VllmConfig
from vllm.engine.protocol import EngineClient from vllm.engine.protocol import EngineClient
from vllm.entrypoints.chat_utils import ChatTemplateConfig from vllm.entrypoints.chat_utils import (
ChatTemplateConfig,
ChatTemplateContentFormatOption,
)
from vllm.entrypoints.logger import RequestLogger from vllm.entrypoints.logger import RequestLogger
from vllm.entrypoints.openai.engine.protocol import ErrorResponse from vllm.entrypoints.openai.engine.protocol import ErrorResponse
from vllm.entrypoints.openai.models.serving import OpenAIServingModels from vllm.entrypoints.openai.models.serving import OpenAIServingModels
@@ -45,7 +48,9 @@ class PoolingServingBase(ABC):
models: OpenAIServingModels, models: OpenAIServingModels,
*, *,
request_logger: RequestLogger | None, request_logger: RequestLogger | None,
chat_template_config: ChatTemplateConfig, chat_template: str | None = None,
chat_template_content_format: ChatTemplateContentFormatOption = "auto",
trust_request_chat_template: bool = False,
return_tokens_as_token_ids: bool = False, return_tokens_as_token_ids: bool = False,
log_error_stack: bool = False, log_error_stack: bool = False,
): ):
@@ -58,7 +63,11 @@ class PoolingServingBase(ABC):
self.request_logger = request_logger self.request_logger = request_logger
self.return_tokens_as_token_ids = return_tokens_as_token_ids self.return_tokens_as_token_ids = return_tokens_as_token_ids
self.log_error_stack = log_error_stack self.log_error_stack = log_error_stack
self.chat_template_config = chat_template_config self.chat_template_config = ChatTemplateConfig(
chat_template=chat_template,
chat_template_content_format=chat_template_content_format,
trust_request_chat_template=trust_request_chat_template,
)
# Shared thread pool executor for preprocessing and postprocessing. # Shared thread pool executor for preprocessing and postprocessing.
self._executor: Executor = models.renderer._executor self._executor: Executor = models.renderer._executor
+24 -33
View File
@@ -10,7 +10,7 @@ from vllm.entrypoints.chat_utils import ChatTemplateConfig
from vllm.logger import init_logger from vllm.logger import init_logger
from vllm.plugins.io_processors import has_io_processor from vllm.plugins.io_processors import has_io_processor
from vllm.renderers import BaseRenderer from vllm.renderers import BaseRenderer
from vllm.tasks import POOLING_TASKS, SCORE_TYPE_MAP, SupportedTask from vllm.tasks import POOLING_TASKS, SupportedTask
from .base.io_processor import PoolingIOProcessor from .base.io_processor import PoolingIOProcessor
from .utils import enable_scoring_api from .utils import enable_scoring_api
@@ -43,24 +43,23 @@ def init_pooling_io_processors(
) -> dict[str, PoolingIOProcessor]: ) -> dict[str, PoolingIOProcessor]:
model_config = vllm_config.model_config model_config = vllm_config.model_config
processors: dict[str, type[PoolingIOProcessor]] = {} processors: dict[str, type[PoolingIOProcessor]] = {}
pooling_task = model_config.get_pooling_task(supported_tasks)
if pooling_task == "classify": if "classify" in supported_tasks:
from .classify.io_processor import ClassifyIOProcessor from .classify.io_processor import ClassifyIOProcessor
processors["classify"] = ClassifyIOProcessor processors["classify"] = ClassifyIOProcessor
if pooling_task == "token_classify": if "token_classify" in supported_tasks:
from .classify.io_processor import TokenClassifyIOProcessor from .classify.io_processor import TokenClassifyIOProcessor
processors["token_classify"] = TokenClassifyIOProcessor processors["token_classify"] = TokenClassifyIOProcessor
if pooling_task == "embed": if "embed" in supported_tasks:
from .embed.io_processor import EmbedIOProcessor from .embed.io_processor import EmbedIOProcessor
processors["embed"] = EmbedIOProcessor processors["embed"] = EmbedIOProcessor
if pooling_task == "token_embed": if "token_embed" in supported_tasks:
from .embed.io_processor import TokenEmbedIOProcessor from .embed.io_processor import TokenEmbedIOProcessor
processors["token_embed"] = TokenEmbedIOProcessor processors["token_embed"] = TokenEmbedIOProcessor
@@ -72,15 +71,15 @@ def init_pooling_io_processors(
from .pooling.io_processor import PluginWithIOProcessorPlugins from .pooling.io_processor import PluginWithIOProcessorPlugins
processors["plugin"] = PluginWithIOProcessorPlugins processors["plugin"] = PluginWithIOProcessorPlugins
elif pooling_task == "plugin": elif "plugin" in supported_tasks:
from .pooling.io_processor import PluginWithoutIOProcessorPlugins from .pooling.io_processor import PluginWithoutIOProcessorPlugins
processors["plugin"] = PluginWithoutIOProcessorPlugins processors["plugin"] = PluginWithoutIOProcessorPlugins
if enable_scoring_api(supported_tasks, model_config): if enable_scoring_api(supported_tasks, model_config):
score_type = model_config.score_type
from .scoring.io_processor import ScoringIOProcessors from .scoring.io_processor import ScoringIOProcessors
score_type: str | None = SCORE_TYPE_MAP.get(pooling_task, None) # type: ignore[arg-type]
if score_type is not None and score_type in ScoringIOProcessors: if score_type is not None and score_type in ScoringIOProcessors:
processors[score_type] = ScoringIOProcessors[score_type] processors[score_type] = ScoringIOProcessors[score_type]
@@ -141,10 +140,6 @@ def init_pooling_state(
request_logger: RequestLogger | None, request_logger: RequestLogger | None,
supported_tasks: tuple["SupportedTask", ...], supported_tasks: tuple["SupportedTask", ...],
): ):
model_config = engine_client.model_config
if model_config is None:
return
from vllm.entrypoints.chat_utils import load_chat_template from vllm.entrypoints.chat_utils import load_chat_template
from vllm.tasks import POOLING_TASKS from vllm.tasks import POOLING_TASKS
@@ -153,14 +148,8 @@ def init_pooling_state(
from .pooling.serving import ServingPooling from .pooling.serving import ServingPooling
from .scoring.serving import ServingScores from .scoring.serving import ServingScores
model_config = engine_client.model_config
resolved_chat_template = load_chat_template(args.chat_template) resolved_chat_template = load_chat_template(args.chat_template)
pooling_task = model_config.get_pooling_task(supported_tasks)
chat_template_config = ChatTemplateConfig(
chat_template=resolved_chat_template,
chat_template_content_format=args.chat_template_content_format,
trust_request_chat_template=args.trust_request_chat_template,
)
state.serving_pooling = ( state.serving_pooling = (
( (
@@ -169,7 +158,9 @@ def init_pooling_state(
state.openai_serving_models, state.openai_serving_models,
supported_tasks=supported_tasks, supported_tasks=supported_tasks,
request_logger=request_logger, request_logger=request_logger,
chat_template_config=chat_template_config, chat_template=resolved_chat_template,
chat_template_content_format=args.chat_template_content_format,
trust_request_chat_template=args.trust_request_chat_template,
) )
) )
if any(t in supported_tasks for t in POOLING_TASKS) if any(t in supported_tasks for t in POOLING_TASKS)
@@ -180,9 +171,11 @@ def init_pooling_state(
engine_client, engine_client,
state.openai_serving_models, state.openai_serving_models,
request_logger=request_logger, request_logger=request_logger,
chat_template_config=chat_template_config, chat_template=resolved_chat_template,
chat_template_content_format=args.chat_template_content_format,
trust_request_chat_template=args.trust_request_chat_template,
) )
if pooling_task == "embed" if "embed" in supported_tasks
else None else None
) )
state.serving_classification = ( state.serving_classification = (
@@ -190,18 +183,21 @@ def init_pooling_state(
engine_client, engine_client,
state.openai_serving_models, state.openai_serving_models,
request_logger=request_logger, request_logger=request_logger,
chat_template_config=chat_template_config, chat_template=resolved_chat_template,
chat_template_content_format=args.chat_template_content_format,
trust_request_chat_template=args.trust_request_chat_template,
) )
if pooling_task == "classify" if "classify" in supported_tasks
else None else None
) )
state.serving_scores = ( state.serving_scores = (
ServingScores( ServingScores(
engine_client, engine_client,
state.openai_serving_models, state.openai_serving_models,
supported_tasks=supported_tasks,
request_logger=request_logger, request_logger=request_logger,
chat_template_config=chat_template_config, chat_template=resolved_chat_template,
chat_template_content_format=args.chat_template_content_format,
trust_request_chat_template=args.trust_request_chat_template,
enable_flash_late_interaction=getattr( enable_flash_late_interaction=getattr(
args, "enable_flash_late_interaction", True args, "enable_flash_late_interaction", True
), ),
@@ -218,12 +214,7 @@ def get_pooling_invocation_types(
# NOTE: Items defined earlier take higher priority # NOTE: Items defined earlier take higher priority
invocation_types: list[tuple[RequestType, tuple[GetHandlerFn, EndpointFn]]] = [] invocation_types: list[tuple[RequestType, tuple[GetHandlerFn, EndpointFn]]] = []
if model_config is None: if "embed" in supported_tasks:
return invocation_types
pooling_task = model_config.get_pooling_task(supported_tasks)
if pooling_task == "embed":
from .embed.api_router import create_embedding, embedding from .embed.api_router import create_embedding, embedding
from .embed.protocol import EmbeddingRequest from .embed.protocol import EmbeddingRequest
@@ -231,7 +222,7 @@ def get_pooling_invocation_types(
(EmbeddingRequest, (embedding, create_embedding)), (EmbeddingRequest, (embedding, create_embedding)),
] ]
if pooling_task == "classify": if "classify" in supported_tasks:
from .classify.api_router import classify, create_classify from .classify.api_router import classify, create_classify
from .classify.protocol import ClassificationRequest from .classify.protocol import ClassificationRequest
+6 -4
View File
@@ -78,15 +78,17 @@ class ServingPooling(PoolingServingBase):
# plugin task uses io_processor.parse_request to verify inputs # plugin task uses io_processor.parse_request to verify inputs
if pooling_task != "plugin" and pooling_task != self.pooling_task: if pooling_task != "plugin" and pooling_task != self.pooling_task:
if pooling_task not in self.supported_tasks: if pooling_task not in self.io_processors:
raise ValueError( raise ValueError(
f"Unsupported task: {pooling_task!r} " f"Unsupported task: {pooling_task!r} "
f"Supported tasks: {self.supported_tasks}" f"Supported tasks: {self.supported_tasks}"
) )
else: else:
raise ValueError( logger.warning_once(
"Try switching the model's pooling_task " "Pooling multitask support is deprecated and will be removed "
f"via --pooler-config.task {request.task}." "in v0.20. When the default pooling task is not what you want, you "
"need to manually specify it via --pooler-config.task %s. ",
pooling_task,
) )
if pooling_task == "plugin" and "plugin" not in self.io_processors: if pooling_task == "plugin" and "plugin" not in self.io_processors:
+1 -7
View File
@@ -8,7 +8,6 @@ from vllm.engine.protocol import EngineClient
from vllm.entrypoints.openai.engine.protocol import UsageInfo from vllm.entrypoints.openai.engine.protocol import UsageInfo
from vllm.logger import init_logger from vllm.logger import init_logger
from vllm.outputs import PoolingRequestOutput, ScoringRequestOutput from vllm.outputs import PoolingRequestOutput, ScoringRequestOutput
from vllm.tasks import SCORE_TYPE_MAP, SupportedTask
from vllm.v1.pool.late_interaction import ( from vllm.v1.pool.late_interaction import (
build_late_interaction_doc_params, build_late_interaction_doc_params,
build_late_interaction_query_params, build_late_interaction_query_params,
@@ -39,15 +38,10 @@ class ServingScores(PoolingServing):
self, self,
engine_client: EngineClient, engine_client: EngineClient,
*args, *args,
supported_tasks: tuple[SupportedTask, ...],
enable_flash_late_interaction: bool = True, enable_flash_late_interaction: bool = True,
**kwargs, **kwargs,
): ):
pooling_task = engine_client.model_config.get_pooling_task(supported_tasks) self.io_processor_name: str = engine_client.model_config.score_type
score_type = SCORE_TYPE_MAP.get(pooling_task, None) # type: ignore[arg-type]
assert score_type is not None
self.io_processor_name: str = score_type
self.enable_flash_late_interaction = ( self.enable_flash_late_interaction = (
self.io_processor_name == "late-interaction" self.io_processor_name == "late-interaction"
and enable_flash_late_interaction and enable_flash_late_interaction
+2 -6
View File
@@ -141,14 +141,10 @@ def enable_scoring_api(
supported_tasks: tuple["SupportedTask", ...], supported_tasks: tuple["SupportedTask", ...],
model_config: ModelConfig | None = None, model_config: ModelConfig | None = None,
) -> bool: ) -> bool:
if model_config is None: if any(t in supported_tasks for t in ("embed", "token_embed")):
return False
pooling_task = model_config.get_pooling_task(supported_tasks)
if pooling_task in ("embed", "token_embed"):
return True return True
if pooling_task == "classify": if model_config is not None and "classify" in supported_tasks:
num_labels = getattr(model_config.hf_config, "num_labels", 0) num_labels = getattr(model_config.hf_config, "num_labels", 0)
if num_labels != 1: if num_labels != 1:
logger.debug_once("Scoring API is only enabled for num_labels == 1.") logger.debug_once("Scoring API is only enabled for num_labels == 1.")
+12
View File
@@ -245,6 +245,7 @@ if TYPE_CHECKING:
VLLM_DEBUG_WORKSPACE: bool = False VLLM_DEBUG_WORKSPACE: bool = False
VLLM_DISABLE_SHARED_EXPERTS_STREAM: bool = False VLLM_DISABLE_SHARED_EXPERTS_STREAM: bool = False
VLLM_SHARED_EXPERTS_STREAM_TOKEN_THRESHOLD: int = 256 VLLM_SHARED_EXPERTS_STREAM_TOKEN_THRESHOLD: int = 256
VLLM_MULTI_STREAM_GEMM_TOKEN_THRESHOLD: int = 4096
VLLM_COMPILE_CACHE_SAVE_FORMAT: Literal["binary", "unpacked"] = "binary" VLLM_COMPILE_CACHE_SAVE_FORMAT: Literal["binary", "unpacked"] = "binary"
VLLM_USE_V2_MODEL_RUNNER: bool = False VLLM_USE_V2_MODEL_RUNNER: bool = False
VLLM_LOG_MODEL_INSPECTION: bool = False VLLM_LOG_MODEL_INSPECTION: bool = False
@@ -1662,6 +1663,17 @@ environment_variables: dict[str, Callable[[], Any]] = {
"VLLM_SHARED_EXPERTS_STREAM_TOKEN_THRESHOLD": lambda: int( "VLLM_SHARED_EXPERTS_STREAM_TOKEN_THRESHOLD": lambda: int(
int(os.getenv("VLLM_SHARED_EXPERTS_STREAM_TOKEN_THRESHOLD", 256)) int(os.getenv("VLLM_SHARED_EXPERTS_STREAM_TOKEN_THRESHOLD", 256))
), ),
# Token-count cutoff for multi-stream overlap of the attention input
# GEMM with auxiliary GEMMs (e.g. fused_wqa_wkv overlapped with indexer
# weights / kv-score projections in DeepSeek-V4). At or below this many
# tokens the FP8 main GEMM has idle SMs to share with the bf16 aux GEMMs
# and overlap is a 5-45% win; above it the FP8 GEMM saturates the device
# and the cross-stream sync becomes pure overhead. Set to 0 to disable
# the multi-stream path entirely. Empirical crossover on B300 (148 SMs)
# is ~4096; B200 (132 SMs) is expected ~3072.
"VLLM_MULTI_STREAM_GEMM_TOKEN_THRESHOLD": lambda: int(
os.getenv("VLLM_MULTI_STREAM_GEMM_TOKEN_THRESHOLD", "4096")
),
# Format for saving torch.compile cache artifacts # Format for saving torch.compile cache artifacts
# - "binary": saves as binary file # - "binary": saves as binary file
# Safe for multiple vllm serve processes accessing the same torch compile cache. # Safe for multiple vllm serve processes accessing the same torch compile cache.
+40
View File
@@ -151,6 +151,46 @@ class SiluAndMul(CustomOp):
return self.forward_cuda(x) return self.forward_cuda(x)
@CustomOp.register("silu_and_mul_with_clamp")
class SiluAndMulWithClamp(CustomOp):
"""SwiGLU activation with input clamping (used by some MoE shared experts).
Computes:
gate = clamp(x[..., :d], max=swiglu_limit)
up = clamp(x[..., d:], min=-swiglu_limit, max=swiglu_limit)
out = silu(gate) * up
where d = x.shape[-1] // 2.
Shapes:
x: (num_tokens, 2 * d) or (batch_size, seq_len, 2 * d)
return: (num_tokens, d) or (batch_size, seq_len, d)
"""
def __init__(self, swiglu_limit: float, *, compile_native: bool = True):
super().__init__(compile_native=compile_native)
self.swiglu_limit = float(swiglu_limit)
if current_platform.is_cuda_alike() or current_platform.is_xpu():
self.op = torch.ops._C.silu_and_mul_with_clamp
elif current_platform.is_cpu():
self._forward_method = self.forward_native
def forward_native(self, x: torch.Tensor) -> torch.Tensor:
d = x.shape[-1] // 2
gate = torch.clamp(x[..., :d], max=self.swiglu_limit)
up = torch.clamp(x[..., d:], min=-self.swiglu_limit, max=self.swiglu_limit)
return F.silu(gate) * up
def forward_cuda(self, x: torch.Tensor) -> torch.Tensor:
d = x.shape[-1] // 2
output_shape = x.shape[:-1] + (d,)
out = torch.empty(output_shape, dtype=x.dtype, device=x.device)
self.op(out, x, self.swiglu_limit)
return out
def forward_xpu(self, x: torch.Tensor) -> torch.Tensor:
return self.forward_cuda(x)
# --8<-- [start:mul_and_silu] # --8<-- [start:mul_and_silu]
@CustomOp.register("mul_and_silu") @CustomOp.register("mul_and_silu")
class MulAndSilu(CustomOp): class MulAndSilu(CustomOp):
@@ -14,7 +14,6 @@ from vllm.model_executor.layers.layernorm import RMSNorm
from vllm.model_executor.layers.linear import ( from vllm.model_executor.layers.linear import (
MergedColumnParallelLinear, MergedColumnParallelLinear,
) )
from vllm.model_executor.layers.utils import cublas_gemm_bf16_bf16_fp32
from vllm.platforms import current_platform from vllm.platforms import current_platform
from vllm.triton_utils import tl, triton from vllm.triton_utils import tl, triton
from vllm.v1.attention.backend import ( from vllm.v1.attention.backend import (
@@ -271,16 +270,12 @@ class DeepseekCompressor(nn.Module):
def forward( def forward(
self, self,
# [num_tokens, hidden_size] # [num_tokens, 2 * self.coff * self.head_dim]
x: torch.Tensor, kv_score: torch.Tensor,
# [num_tokens] # [num_tokens]
positions: torch.Tensor, positions: torch.Tensor,
rotary_emb, rotary_emb,
) -> None: ) -> None:
num_tokens, _ = x.shape
# bf16 weights/activations but fp32 output for numerical stability of
# the downstream compressor math.
kv_score = cublas_gemm_bf16_bf16_fp32(x, self.fused_wkv_wgate.weight)
# Each of shape [num_tokens, coff * self.head_dim] # Each of shape [num_tokens, coff * self.head_dim]
# input bf16, output are fp32 # input bf16, output are fp32
kv, score = kv_score.split( kv, score = kv_score.split(
@@ -4,18 +4,21 @@
DeepseekV4 MLA Attention Layer DeepseekV4 MLA Attention Layer
""" """
from collections.abc import Callable
from dataclasses import dataclass from dataclasses import dataclass
from typing import TYPE_CHECKING, cast from typing import TYPE_CHECKING, Any, cast
import torch import torch
import torch.nn as nn import torch.nn as nn
import torch.nn.functional as F import torch.nn.functional as F
from transformers import DeepseekV2Config, DeepseekV3Config from transformers import DeepseekV2Config, DeepseekV3Config
import vllm.envs as envs
from vllm.model_executor.layers.linear import ( from vllm.model_executor.layers.linear import (
ReplicatedLinear, ReplicatedLinear,
) )
from vllm.model_executor.layers.sparse_attn_indexer import SparseAttnIndexer from vllm.model_executor.layers.sparse_attn_indexer import SparseAttnIndexer
from vllm.model_executor.layers.utils import cublas_gemm_bf16_bf16_fp32
from vllm.utils.deep_gemm import fp8_einsum from vllm.utils.deep_gemm import fp8_einsum
from vllm.utils.torch_utils import direct_register_custom_op from vllm.utils.torch_utils import direct_register_custom_op
from vllm.v1.attention.ops.deepseek_v4_ops import ( from vllm.v1.attention.ops.deepseek_v4_ops import (
@@ -51,7 +54,10 @@ from vllm.model_executor.layers.quantization.input_quant_fp8 import (
from vllm.model_executor.layers.quantization.utils.quant_utils import ( from vllm.model_executor.layers.quantization.utils.quant_utils import (
GroupShape, GroupShape,
) )
from vllm.utils.multi_stream_utils import maybe_execute_in_parallel from vllm.utils.multi_stream_utils import (
execute_in_parallel,
maybe_execute_in_parallel,
)
from vllm.v1.attention.backend import AttentionBackend, AttentionMetadata from vllm.v1.attention.backend import AttentionBackend, AttentionMetadata
from vllm.v1.attention.backends.mla.flashmla_sparse import ( from vllm.v1.attention.backends.mla.flashmla_sparse import (
DeepseekV4FlashMLASparseBackend, DeepseekV4FlashMLASparseBackend,
@@ -94,7 +100,7 @@ class DeepseekV4MLAModules:
indexer: torch.nn.Module | None indexer: torch.nn.Module | None
indexer_rotary_emb: torch.nn.Module indexer_rotary_emb: torch.nn.Module
topk_indices_buffer: torch.Tensor | None topk_indices_buffer: torch.Tensor | None
aux_stream: torch.cuda.Stream | None = None aux_stream_list: list[torch.cuda.Stream] | None = None
# --8<-- [start:multi_head_latent_attention] # --8<-- [start:multi_head_latent_attention]
@@ -217,8 +223,11 @@ class DeepseekV4MultiHeadLatentAttentionWrapper(PluggableLayer):
+ 1 # 1B pad + 1 # 1B pad
) )
self.aux_stream = mla_modules.aux_stream self.aux_stream_list = mla_modules.aux_stream_list
self.ln_events = [torch.cuda.Event(), torch.cuda.Event()] # [0]: GEMM start / post-GEMM event0. [1..3]: GEMM done events;
# [1] doubles as post-GEMM event1. Reuse is safe: GEMM fully joins
# before post-GEMM starts.
self.ln_events = [torch.cuda.Event() for _ in range(4)]
assert cache_config is not None, "DeepseekV4 attention requires cache_config" assert cache_config is not None, "DeepseekV4 attention requires cache_config"
self.swa_cache_layer = DeepseekV4SWACache( self.swa_cache_layer = DeepseekV4SWACache(
@@ -277,9 +286,6 @@ class DeepseekV4MultiHeadLatentAttentionWrapper(PluggableLayer):
hidden_states: torch.Tensor, hidden_states: torch.Tensor,
llama_4_scaling: torch.Tensor | None = None, llama_4_scaling: torch.Tensor | None = None,
) -> torch.Tensor: ) -> torch.Tensor:
qr_kv, _ = self.fused_wqa_wkv(hidden_states)
qr, kv = qr_kv.split([self.q_lora_rank, self.head_dim], dim=-1)
# Pre-allocate attention output with FlashMLA-padded head count. # Pre-allocate attention output with FlashMLA-padded head count.
# The op writes into `o_padded`; we slice to n_local_heads after. # The op writes into `o_padded`; we slice to n_local_heads after.
num_tokens = hidden_states.shape[0] num_tokens = hidden_states.shape[0]
@@ -292,8 +298,6 @@ class DeepseekV4MultiHeadLatentAttentionWrapper(PluggableLayer):
# Attention (inside custom op for torch.compile boundary) # Attention (inside custom op for torch.compile boundary)
torch.ops.vllm.deepseek_v4_attention( torch.ops.vllm.deepseek_v4_attention(
hidden_states, hidden_states,
qr,
kv,
positions, positions,
o_padded, o_padded,
self.layer_name, self.layer_name,
@@ -332,17 +336,73 @@ class DeepseekV4MultiHeadLatentAttentionWrapper(PluggableLayer):
return self.wo_b(z.flatten(1)) return self.wo_b(z.flatten(1))
def attn_gemm_parallel_execute(self, hidden_states) -> tuple[Any, ...]:
assert self.aux_stream_list is not None
assert len(self.aux_stream_list) >= 3
# fused_wqa_wkv (heaviest) on default; the three lighter input GEMMs
# on aux streams 0..2 when their owning module exists. ln_events[0]
# is the fan-out start event; ln_events[1..3] are per-aux done events.
aux_fns: list[Callable[[], Any] | None] = [None, None, None]
if self.compressor is not None:
# Local ref so the closure keeps a non-None type for mypy.
compressor = self.compressor
def compressor_kv_score() -> torch.Tensor:
return cublas_gemm_bf16_bf16_fp32(
hidden_states, compressor.fused_wkv_wgate.weight
)
aux_fns[0] = compressor_kv_score
if self.indexer is not None:
indexer = self.indexer
def indexer_weights_proj() -> torch.Tensor:
# ReplicatedLinear returns (output, bias); bias is None.
weights, _ = indexer.weights_proj(hidden_states)
return weights
def indexer_compressor_kv_score() -> torch.Tensor:
return cublas_gemm_bf16_bf16_fp32(
hidden_states, indexer.compressor.fused_wkv_wgate.weight
)
aux_fns[1] = indexer_weights_proj
aux_fns[2] = indexer_compressor_kv_score
def fused_wqa_wkv() -> torch.Tensor:
# MergedColumnParallelLinear returns (output, bias); bias is None.
qr_kv, _ = self.fused_wqa_wkv(hidden_states)
return qr_kv
qr_kv, (kv_score, indexer_weights, indexer_kv_score) = execute_in_parallel(
fused_wqa_wkv,
aux_fns,
self.ln_events[0],
self.ln_events[1:4],
self.aux_stream_list[:3],
enable=hidden_states.shape[0]
<= envs.VLLM_MULTI_STREAM_GEMM_TOKEN_THRESHOLD,
)
return qr_kv, kv_score, indexer_kv_score, indexer_weights
def attention_impl( def attention_impl(
self, self,
hidden_states: torch.Tensor, hidden_states: torch.Tensor,
qr: torch.Tensor,
kv: torch.Tensor,
positions: torch.Tensor, positions: torch.Tensor,
out: torch.Tensor, # [num_tokens, padded_heads, head_dim], written in place out: torch.Tensor, # [num_tokens, padded_heads, head_dim], written in place
) -> None: ) -> None:
forward_context = get_forward_context() forward_context = get_forward_context()
attn_metadata = forward_context.attn_metadata attn_metadata = forward_context.attn_metadata
qr_kv, kv_score, indexer_kv_score, indexer_weights = (
self.attn_gemm_parallel_execute(hidden_states)
)
qr, kv = qr_kv.split([self.q_lora_rank, self.head_dim], dim=-1)
qr, kv = fused_q_kv_rmsnorm( qr, kv = fused_q_kv_rmsnorm(
qr, qr,
kv, kv,
@@ -350,42 +410,60 @@ class DeepseekV4MultiHeadLatentAttentionWrapper(PluggableLayer):
self.kv_norm.weight.data, self.kv_norm.weight.data,
self.eps, self.eps,
) )
q = self.wq_b(qr).view(-1, self.n_local_heads, self.head_dim)
# Overlap kv_insert with whichever of indexer/compressor is present. # wq_b + kv_insert (+ MLA compressor when an indexer is present) ride
# Indexer implies compressor; when both exist, compressor rides on the # on the default stream so q stays on its consumer stream (mla_attn
# aux stream alongside kv_insert so the heavy indexer owns default. # downstream reads q on default). Indexer/compressor go on aux for
# overlap with default's GEMM + cache write.
if self.indexer is not None: if self.indexer is not None:
assert self.aux_stream_list is not None
aux_stream = self.aux_stream_list[0]
indexer = self.indexer indexer = self.indexer
# Local ref so the closure keeps a non-None type for mypy. # Local ref so the closure keeps a non-None type for mypy.
assert self.compressor is not None assert self.compressor is not None
compressor = self.compressor compressor = self.compressor
def kv_insert_and_compress() -> None: def wq_b_kv_insert_and_compress() -> torch.Tensor:
q = self.wq_b(qr).view(-1, self.n_local_heads, self.head_dim)
self._fused_qnorm_rope_kv_insert(q, kv, positions, attn_metadata) self._fused_qnorm_rope_kv_insert(q, kv, positions, attn_metadata)
compressor(hidden_states, positions, self.rotary_emb) compressor(kv_score, positions, self.rotary_emb)
return q
maybe_execute_in_parallel( q, _ = maybe_execute_in_parallel(
lambda: indexer(hidden_states, qr, positions, self.indexer_rotary_emb), wq_b_kv_insert_and_compress,
kv_insert_and_compress, lambda: indexer(
self.ln_events[0], hidden_states,
self.ln_events[1], qr,
self.aux_stream, indexer_kv_score,
) indexer_weights,
elif self.compressor is not None: positions,
# Compressor on default, kv_insert on aux. self.indexer_rotary_emb,
compressor = self.compressor
maybe_execute_in_parallel(
lambda: compressor(hidden_states, positions, self.rotary_emb),
lambda: self._fused_qnorm_rope_kv_insert(
q, kv, positions, attn_metadata
), ),
self.ln_events[0], self.ln_events[0],
self.ln_events[1], self.ln_events[1],
self.aux_stream, aux_stream,
)
elif self.compressor is not None:
# wq_b + kv_insert on default, compressor on aux.
assert self.aux_stream_list is not None
aux_stream = self.aux_stream_list[0]
compressor = self.compressor
def wq_b_kv_insert() -> torch.Tensor:
q = self.wq_b(qr).view(-1, self.n_local_heads, self.head_dim)
self._fused_qnorm_rope_kv_insert(q, kv, positions, attn_metadata)
return q
q, _ = maybe_execute_in_parallel(
wq_b_kv_insert,
lambda: compressor(kv_score, positions, self.rotary_emb),
self.ln_events[0],
self.ln_events[1],
aux_stream,
) )
else: else:
# SWA-only layer: no compressor, no overlap. # SWA-only layer: no compressor, no overlap.
q = self.wq_b(qr).view(-1, self.n_local_heads, self.head_dim)
self._fused_qnorm_rope_kv_insert(q, kv, positions, attn_metadata) self._fused_qnorm_rope_kv_insert(q, kv, positions, attn_metadata)
# Handle dummy run (no metadata). # Handle dummy run (no metadata).
@@ -455,21 +533,17 @@ class DeepseekV4MultiHeadLatentAttentionWrapper(PluggableLayer):
def deepseek_v4_attention( def deepseek_v4_attention(
hidden_states: torch.Tensor, hidden_states: torch.Tensor,
qr: torch.Tensor,
kv: torch.Tensor,
positions: torch.Tensor, positions: torch.Tensor,
out: torch.Tensor, out: torch.Tensor,
layer_name: str, layer_name: str,
) -> None: ) -> None:
forward_context: ForwardContext = get_forward_context() forward_context: ForwardContext = get_forward_context()
self = forward_context.no_compile_layers[layer_name] self = forward_context.no_compile_layers[layer_name]
self.attention_impl(hidden_states, qr, kv, positions, out) self.attention_impl(hidden_states, positions, out)
def deepseek_v4_attention_fake( def deepseek_v4_attention_fake(
hidden_states: torch.Tensor, hidden_states: torch.Tensor,
qr: torch.Tensor,
kv: torch.Tensor,
positions: torch.Tensor, positions: torch.Tensor,
out: torch.Tensor, out: torch.Tensor,
layer_name: str, layer_name: str,
@@ -1057,18 +1131,20 @@ class DeepseekV4Indexer(nn.Module):
self, self,
hidden_states: torch.Tensor, hidden_states: torch.Tensor,
qr: torch.Tensor, qr: torch.Tensor,
compressed_kv_score: torch.Tensor,
indexer_weights: torch.Tensor,
positions: torch.Tensor, positions: torch.Tensor,
rotary_emb: nn.Module, rotary_emb: nn.Module,
) -> torch.Tensor: ) -> torch.Tensor:
# ReplicatedLinear returns (output, bias); bias is None.
q, _ = self.wq_b(qr) q, _ = self.wq_b(qr)
q = q.view(-1, self.n_head, self.head_dim) q = q.view(-1, self.n_head, self.head_dim)
k = self.compressor(hidden_states, positions, rotary_emb) k = self.compressor(compressed_kv_score, positions, rotary_emb)
weights, _ = self.weights_proj(hidden_states)
q_quant, weights = fused_indexer_q_rope_quant( q_quant, weights = fused_indexer_q_rope_quant(
positions, positions,
q, q,
rotary_emb.cos_sin_cache, rotary_emb.cos_sin_cache,
weights, indexer_weights,
self.softmax_scale, self.softmax_scale,
self.n_head**-0.5, self.n_head**-0.5,
use_fp4=self.use_fp4_kv, use_fp4=self.use_fp4_kv,
@@ -228,23 +228,37 @@ def maybe_make_prepare_finalize(
elif moe.use_fi_nvl_one_sided_kernels: elif moe.use_fi_nvl_one_sided_kernels:
assert quant_config is not None assert quant_config is not None
if quant_config.quant_dtype != "nvfp4":
raise ValueError(
"The 'flashinfer_nvlink_one_sided' all2all backend only "
"supports nvfp4 activation quantization, but got "
f"quant_dtype={quant_config.quant_dtype!r}. Use a different "
"all2all backend (e.g. 'flashinfer_nvlink_two_sided' or "
"'allgather_reducescatter') for non-nvfp4 models."
)
max_num_tokens = ( max_num_tokens = (
get_current_vllm_config().scheduler_config.max_num_batched_tokens get_current_vllm_config().scheduler_config.max_num_batched_tokens
) )
if quant_config.quant_dtype is None:
dispatch_dtype_bytes_per_elem = 2
dispatch_scale_bytes_per_token = 0
elif quant_config.quant_dtype == "nvfp4":
dispatch_dtype_bytes_per_elem = 0
dispatch_scale_bytes_per_token = moe.hidden_dim // 16
elif quant_config.quant_dtype == "mxfp8":
dispatch_dtype_bytes_per_elem = 1
align = quant_config.mx_alignment
if align > 0:
padded_k = ((moe.hidden_dim + align - 1) // align) * align
else:
padded_k = moe.hidden_dim
dispatch_scale_bytes_per_token = padded_k // 32
else:
raise NotImplementedError(
"flashinfer_nvlink_one_sided dispatch supports nvfp4, mxfp8, "
"and bf16 (quant_dtype=None) today; got "
f"quant_dtype={quant_config.quant_dtype!r}"
)
prepare_finalize = FlashInferNVLinkOneSidedPrepareAndFinalize( prepare_finalize = FlashInferNVLinkOneSidedPrepareAndFinalize(
max_num_tokens=max_num_tokens, max_num_tokens=max_num_tokens,
top_k=moe.experts_per_token, top_k=moe.experts_per_token,
num_experts=moe.num_experts, num_experts=moe.num_experts,
hidden_size=moe.hidden_dim, hidden_size=moe.hidden_dim,
num_dispatchers=all2all_manager.world_size, num_dispatchers=all2all_manager.world_size,
dispatch_dtype_bytes_per_elem=dispatch_dtype_bytes_per_elem,
dispatch_scale_bytes_per_token=dispatch_scale_bytes_per_token,
) )
elif moe.use_ag_rs_all2all_kernels and allow_new_interface: elif moe.use_ag_rs_all2all_kernels and allow_new_interface:
@@ -247,6 +247,8 @@ class FusedMoEQuantConfig:
gemm1_beta: float | None = None gemm1_beta: float | None = None
gemm1_clamp_limit: float | None = None gemm1_clamp_limit: float | None = None
mx_alignment: int = 0
def __post_init__(self): def __post_init__(self):
assert not self.per_act_token_quant or self.block_shape is None, ( assert not self.per_act_token_quant or self.block_shape is None, (
"illegal quantization" "illegal quantization"
@@ -705,6 +707,7 @@ def mxfp4_mxfp8_moe_quant_config(
gemm1_alpha: float | None = None, gemm1_alpha: float | None = None,
gemm1_beta: float | None = None, gemm1_beta: float | None = None,
gemm1_clamp_limit: float | None = None, gemm1_clamp_limit: float | None = None,
mx_alignment: int = 0,
) -> FusedMoEQuantConfig: ) -> FusedMoEQuantConfig:
""" """
Construct a quant config for mxfp4 activations and mxfp4 weights. Construct a quant config for mxfp4 activations and mxfp4 weights.
@@ -717,6 +720,7 @@ def mxfp4_mxfp8_moe_quant_config(
gemm1_alpha=gemm1_alpha, gemm1_alpha=gemm1_alpha,
gemm1_beta=gemm1_beta, gemm1_beta=gemm1_beta,
gemm1_clamp_limit=gemm1_clamp_limit, gemm1_clamp_limit=gemm1_clamp_limit,
mx_alignment=mx_alignment,
) )
@@ -45,7 +45,7 @@ def _gelu_and_mul(
# Uses static methods or standalone functions to avoid instantiating CustomOp # Uses static methods or standalone functions to avoid instantiating CustomOp
# classes, which would call get_current_vllm_config() before config is set. # classes, which would call get_current_vllm_config() before config is set.
_CPU_MOE_ACT_FN: dict[MoEActivation, Callable[[torch.Tensor], torch.Tensor]] = { _CPU_MOE_ACT_FN: dict[MoEActivation, Callable[[torch.Tensor], torch.Tensor]] = {
MoEActivation.SILU: SiluAndMul.forward_native, MoEActivation.SILU: lambda x: SiluAndMul(compile_native=False).forward_native(x),
MoEActivation.SWIGLUOAI: _swigluoai_forward_native, MoEActivation.SWIGLUOAI: _swigluoai_forward_native,
MoEActivation.GELU: _gelu_and_mul, MoEActivation.GELU: _gelu_and_mul,
} }
@@ -44,6 +44,9 @@ class TrtLlmMxfp4ExpertsBase:
moe_config.intermediate_size_per_partition moe_config.intermediate_size_per_partition
) )
self.hidden_dim = moe_config.hidden_dim self.hidden_dim = moe_config.hidden_dim
self.hidden_dim_unpadded = (
moe_config.hidden_dim_unpadded or moe_config.hidden_dim
)
self.local_num_experts = moe_config.num_local_experts self.local_num_experts = moe_config.num_local_experts
self.ep_rank = moe_config.moe_parallel_config.ep_rank self.ep_rank = moe_config.moe_parallel_config.ep_rank
@@ -82,9 +85,6 @@ class TrtLlmMxfp4ExpertsBase:
get_current_vllm_config().compilation_config.max_cudagraph_capture_size get_current_vllm_config().compilation_config.max_cudagraph_capture_size
) )
# P1-5 fix: use public quant_dtype property instead of private _a1
self.use_mxfp8_input = quant_config.quant_dtype == "mxfp8"
@staticmethod @staticmethod
def _supports_current_device() -> bool: def _supports_current_device() -> bool:
p = current_platform p = current_platform
@@ -121,8 +121,7 @@ class TrtLlmMxfp4ExpertsBase:
@property @property
def expects_unquantized_inputs(self) -> bool: def expects_unquantized_inputs(self) -> bool:
# Expert handles MXFP8 quantization internally if needed return False
return True
class TrtLlmMxfp4ExpertsMonolithic( class TrtLlmMxfp4ExpertsMonolithic(
@@ -181,24 +180,19 @@ class TrtLlmMxfp4ExpertsMonolithic(
) -> torch.Tensor: ) -> torch.Tensor:
from flashinfer import trtllm_fp4_block_scale_moe from flashinfer import trtllm_fp4_block_scale_moe
# Handle input quantization if a1q_scale is not None:
if self.use_mxfp8_input: x_quant = hidden_states
from flashinfer import mxfp8_quantize x_scale = a1q_scale.view(torch.float8_e4m3fn)
x_quant, x_scale = mxfp8_quantize(
hidden_states,
is_sf_swizzled_layout=False,
alignment=256,
)
x_scale = x_scale.view(torch.float8_e4m3fn).reshape(
*hidden_states.shape[:-1], -1
)
else: else:
assert hidden_states.dtype == torch.bfloat16 assert hidden_states.dtype == torch.bfloat16
x_quant = hidden_states x_quant = hidden_states
x_scale = None x_scale = None
output = torch.empty(
output = torch.empty_like(hidden_states) *hidden_states.shape[:-1],
self.hidden_dim_unpadded,
dtype=torch.bfloat16,
device=hidden_states.device,
)
from vllm.utils.flashinfer import _is_fi_autotuning, autotune from vllm.utils.flashinfer import _is_fi_autotuning, autotune
@@ -244,10 +238,6 @@ class TrtLlmMxfp4ExpertsModular(TrtLlmMxfp4ExpertsBase, mk.FusedMoEExpertsModula
Moved from trtllm_moe.py. Moved from trtllm_moe.py.
""" """
@property
def expects_unquantized_inputs(self) -> bool:
return True
@staticmethod @staticmethod
def _supports_parallel_config( def _supports_parallel_config(
moe_parallel_config: FusedMoEParallelConfig, moe_parallel_config: FusedMoEParallelConfig,
@@ -284,7 +274,7 @@ class TrtLlmMxfp4ExpertsModular(TrtLlmMxfp4ExpertsBase, mk.FusedMoEExpertsModula
# The workspaces for this implementation are managed by flashinfer. # The workspaces for this implementation are managed by flashinfer.
workspace1 = (0,) workspace1 = (0,)
workspace2 = (0,) workspace2 = (0,)
output = (M, K) output = (M, self.hidden_dim_unpadded)
return (workspace1, workspace2, output) return (workspace1, workspace2, output)
def apply( def apply(
@@ -310,18 +300,9 @@ class TrtLlmMxfp4ExpertsModular(TrtLlmMxfp4ExpertsBase, mk.FusedMoEExpertsModula
intermediate_size = self.intermediate_size_per_partition intermediate_size = self.intermediate_size_per_partition
local_expert_offset = self.moe_config.ep_rank * local_num_experts local_expert_offset = self.moe_config.ep_rank * local_num_experts
# Handle input quantization if a1q_scale is not None:
if self.use_mxfp8_input: x_quant = hidden_states
from flashinfer import mxfp8_quantize x_scale = a1q_scale.view(torch.float8_e4m3fn)
x_quant, x_scale = mxfp8_quantize(
hidden_states,
is_sf_swizzled_layout=False,
alignment=256,
)
x_scale = x_scale.view(torch.float8_e4m3fn).reshape(
*hidden_states.shape[:-1], -1
)
else: else:
assert hidden_states.dtype == torch.bfloat16 assert hidden_states.dtype == torch.bfloat16
x_quant = hidden_states x_quant = hidden_states
@@ -1195,10 +1195,18 @@ def make_mxfp4_moe_quant_config(
gemm1_beta=gemm1_beta, gemm1_beta=gemm1_beta,
gemm1_clamp_limit=swiglu_limit, gemm1_clamp_limit=swiglu_limit,
) )
elif mxfp4_backend in ( elif mxfp4_backend == Mxfp4MoeBackend.FLASHINFER_TRTLLM_MXFP4_MXFP8:
Mxfp4MoeBackend.FLASHINFER_TRTLLM_MXFP4_MXFP8, return mxfp4_mxfp8_moe_quant_config(
Mxfp4MoeBackend.FLASHINFER_CUTLASS_MXFP4_MXFP8, w1_bias=w1_bias,
): w2_bias=w2_bias,
w1_scale=w1_scale,
w2_scale=w2_scale,
gemm1_alpha=gemm1_alpha,
gemm1_beta=gemm1_beta,
gemm1_clamp_limit=swiglu_limit,
mx_alignment=256,
)
elif mxfp4_backend == Mxfp4MoeBackend.FLASHINFER_CUTLASS_MXFP4_MXFP8:
return mxfp4_mxfp8_moe_quant_config( return mxfp4_mxfp8_moe_quant_config(
w1_bias=w1_bias, w1_bias=w1_bias,
w2_bias=w2_bias, w2_bias=w2_bias,
@@ -1250,7 +1258,6 @@ def make_mxfp4_moe_kernel(
"""Create a FusedMoEKernel for the given MXFP4 backend.""" """Create a FusedMoEKernel for the given MXFP4 backend."""
is_monolithic = issubclass(experts_cls, mk.FusedMoEExpertsMonolithic) is_monolithic = issubclass(experts_cls, mk.FusedMoEExpertsMonolithic)
# Create Prepare/Finalize.
prepare_finalize = maybe_make_prepare_finalize( prepare_finalize = maybe_make_prepare_finalize(
moe=moe_config, moe=moe_config,
quant_config=moe_quant_config, quant_config=moe_quant_config,
@@ -31,6 +31,8 @@ class FlashInferNVLinkOneSidedPrepareAndFinalize(mk.FusedMoEPrepareAndFinalizeMo
num_experts: int, num_experts: int,
hidden_size: int, hidden_size: int,
num_dispatchers: int = 1, num_dispatchers: int = 1,
dispatch_dtype_bytes_per_elem: int = 0,
dispatch_scale_bytes_per_token: int = 0,
): ):
super().__init__() super().__init__()
self.max_num_tokens = max_num_tokens self.max_num_tokens = max_num_tokens
@@ -38,6 +40,7 @@ class FlashInferNVLinkOneSidedPrepareAndFinalize(mk.FusedMoEPrepareAndFinalizeMo
self.num_experts = num_experts self.num_experts = num_experts
self.hidden_size = hidden_size self.hidden_size = hidden_size
self.num_dispatchers_ = num_dispatchers self.num_dispatchers_ = num_dispatchers
self.scale_elems_per_token = dispatch_scale_bytes_per_token
device_communicator = get_ep_group().device_communicator device_communicator = get_ep_group().device_communicator
assert device_communicator is not None assert device_communicator is not None
@@ -49,6 +52,8 @@ class FlashInferNVLinkOneSidedPrepareAndFinalize(mk.FusedMoEPrepareAndFinalizeMo
top_k=self.top_k, top_k=self.top_k,
num_experts=self.num_experts, num_experts=self.num_experts,
hidden_size=self.hidden_size, hidden_size=self.hidden_size,
dispatch_dtype_bytes_per_elem=dispatch_dtype_bytes_per_elem,
dispatch_scale_bytes_per_token=dispatch_scale_bytes_per_token,
) )
@property @property
@@ -92,19 +97,24 @@ class FlashInferNVLinkOneSidedPrepareAndFinalize(mk.FusedMoEPrepareAndFinalizeMo
else a1.shape[0] else a1.shape[0]
) )
a1q, a1q_scale = moe_kernel_quantize_input( if defer_input_quant:
a1, a1q, a1q_scale = a1, None
quant_config.a1_gscale, else:
quant_config.quant_dtype, a1q, a1q_scale = moe_kernel_quantize_input(
quant_config.per_act_token_quant, a1,
quant_config.block_shape, quant_config.a1_gscale,
is_fp4_scale_swizzled=False, # delay swizzle to after comm quant_config.quant_dtype,
) quant_config.per_act_token_quant,
quant_config.block_shape,
is_fp4_scale_swizzled=False, # delay swizzle to after comm
mx_alignment=quant_config.mx_alignment,
)
payloads = [] payloads = []
payloads.append(a1q) payloads.append(a1q)
if a1q_scale is not None: if a1q_scale is not None:
payloads.append(a1q_scale) payloads.append(a1q_scale)
topk_ids_payload_index = len(payloads)
payloads.append(topk_ids) payloads.append(topk_ids)
payloads.append(topk_weights) payloads.append(topk_weights)
@@ -113,6 +123,8 @@ class FlashInferNVLinkOneSidedPrepareAndFinalize(mk.FusedMoEPrepareAndFinalizeMo
token_selected_experts=topk_ids, token_selected_experts=topk_ids,
input_payloads=payloads, input_payloads=payloads,
runtime_max_tokens_per_rank=self.runtime_max_tokens_per_rank, runtime_max_tokens_per_rank=self.runtime_max_tokens_per_rank,
invalid_token_expert_id=-1, # Follow TRTLLM Pattern
expert_id_payload_index=topk_ids_payload_index,
) )
if a1q_scale is not None: if a1q_scale is not None:
a1q_recv, a1q_scale_recv, topk_ids_recv, topk_weights_recv = recv_payloads a1q_recv, a1q_scale_recv, topk_ids_recv, topk_weights_recv = recv_payloads
@@ -124,7 +136,8 @@ class FlashInferNVLinkOneSidedPrepareAndFinalize(mk.FusedMoEPrepareAndFinalizeMo
a1q_scale_recv = a1q_scale_recv.view(-1, a1q_scale_recv.shape[-1]) a1q_scale_recv = a1q_scale_recv.view(-1, a1q_scale_recv.shape[-1])
a1q_scale_recv = a1q_scale_recv.view(torch.uint8) a1q_scale_recv = a1q_scale_recv.view(torch.uint8)
a1q_scale_recv = nvfp4_block_scale_interleave(a1q_scale_recv) a1q_scale_recv = nvfp4_block_scale_interleave(a1q_scale_recv)
a1q_scale_recv = a1q_scale_recv.view(-1, self.hidden_size // 16) assert self.scale_elems_per_token > 0
a1q_scale_recv = a1q_scale_recv.view(-1, self.scale_elems_per_token)
else: else:
a1q_recv, topk_ids_recv, topk_weights_recv = recv_payloads a1q_recv, topk_ids_recv, topk_weights_recv = recv_payloads
a1q_scale_recv = None a1q_scale_recv = None
@@ -174,6 +174,7 @@ def flashinfer_alltoall_dispatch(
# the hidden states, breaking the A2A kernel. So, we # the hidden states, breaking the A2A kernel. So, we
# delay the swizzling until after the A2A. # delay the swizzling until after the A2A.
is_fp4_scale_swizzled=False, is_fp4_scale_swizzled=False,
mx_alignment=quant_config.mx_alignment,
) )
x = MnnvlMoe.mnnvl_moe_alltoallv( x = MnnvlMoe.mnnvl_moe_alltoallv(
@@ -40,6 +40,7 @@ def _quantize_and_setup_dispatch(
per_act_token_quant=quant_config.per_act_token_quant, per_act_token_quant=quant_config.per_act_token_quant,
block_shape=quant_config.block_shape, block_shape=quant_config.block_shape,
is_fp4_scale_swizzled=False, is_fp4_scale_swizzled=False,
mx_alignment=quant_config.mx_alignment,
) )
# Skip gathering scales if we have static quantization # Skip gathering scales if we have static quantization
@@ -31,6 +31,7 @@ def _quantize_input(
per_act_token_quant=quant_config.per_act_token_quant, per_act_token_quant=quant_config.per_act_token_quant,
block_shape=quant_config.block_shape, block_shape=quant_config.block_shape,
is_fp4_scale_swizzled=quant_config.is_nvfp4_scale_swizzled, is_fp4_scale_swizzled=quant_config.is_nvfp4_scale_swizzled,
mx_alignment=quant_config.mx_alignment,
) )
return a1q, a1q_scale return a1q, a1q_scale
@@ -208,11 +208,12 @@ def _mxfp8_e4m3_quantize(
per_act_token_quant: bool, per_act_token_quant: bool,
block_shape: list[int] | None = None, block_shape: list[int] | None = None,
is_sf_swizzled_layout: bool = False, is_sf_swizzled_layout: bool = False,
mx_alignment: int = 0,
) -> tuple[torch.Tensor, torch.Tensor]: ) -> tuple[torch.Tensor, torch.Tensor]:
assert A_scale is None assert A_scale is None
assert not per_act_token_quant assert not per_act_token_quant
assert block_shape is None or block_shape == [1, 32] assert block_shape is None or block_shape == [1, 32]
return mxfp8_e4m3_quantize(A, is_sf_swizzled_layout) return mxfp8_e4m3_quantize(A, is_sf_swizzled_layout, mx_alignment)
def _mxfp6_e3m2_quantize( def _mxfp6_e3m2_quantize(
@@ -258,6 +259,7 @@ def moe_kernel_quantize_input(
is_fp4_scale_swizzled: bool = True, is_fp4_scale_swizzled: bool = True,
ocp_mx_scheme: str | None = None, ocp_mx_scheme: str | None = None,
quantization_emulation: bool = False, quantization_emulation: bool = False,
mx_alignment: int = 0,
) -> tuple[torch.Tensor, torch.Tensor | None]: ) -> tuple[torch.Tensor, torch.Tensor | None]:
# Handle OCP MX scheme that requires QDQ (quantize-dequantize) for emulation # Handle OCP MX scheme that requires QDQ (quantize-dequantize) for emulation
if ocp_mx_scheme is not None: if ocp_mx_scheme is not None:
@@ -319,7 +321,8 @@ def moe_kernel_quantize_input(
A_scale, A_scale,
per_act_token_quant, per_act_token_quant,
block_shape, block_shape,
is_sf_swizzled_layout=is_fp4_scale_swizzled, is_sf_swizzled_layout=False,
mx_alignment=mx_alignment,
) )
elif quant_dtype == "mxfp6_e3m2": elif quant_dtype == "mxfp6_e3m2":
if not quantization_emulation: if not quantization_emulation:
@@ -55,9 +55,6 @@ class MambaStateDtypeCalculator:
model_dtype: ModelDType | torch.dtype, model_dtype: ModelDType | torch.dtype,
mamba_cache_dtype: MambaDType, mamba_cache_dtype: MambaDType,
) -> tuple[torch.dtype, ...]: ) -> tuple[torch.dtype, ...]:
# TODO (tdoublep) requires testing
if mamba_cache_dtype == "float32":
raise ValueError("fp32 state for minimax is not yet supported")
state_dtype = get_kv_cache_torch_dtype(mamba_cache_dtype, model_dtype) state_dtype = get_kv_cache_torch_dtype(mamba_cache_dtype, model_dtype)
return (state_dtype,) return (state_dtype,)
+134
View File
@@ -448,3 +448,137 @@ direct_register_custom_op(
mutates_args=[], mutates_args=[],
fake_impl=_mhc_post_fake, fake_impl=_mhc_post_fake,
) )
@tilelang.jit(
pass_configs={
tilelang.PassConfigKey.TL_DISABLE_WARP_SPECIALIZED: True,
tilelang.PassConfigKey.TL_DISABLE_TMA_LOWER: True,
tilelang.PassConfigKey.TL_PTXAS_REGISTER_USAGE_LEVEL: 10,
},
)
def hc_head_fuse_tilelang(
residual,
fn,
hc_scale,
hc_base,
out,
hidden_size: int,
rms_eps: float,
hc_eps: float,
hc_mult: int = 4,
n_thr: int = 128,
h_blk: int = 1024,
):
"""Two-pass fused kernel for hc_head.
Pass 1: accumulate per-token squared sum and hc_mult dot-products
(projections onto fn rows) using cross-thread reducers.
Pass 2: apply sigmoid-gated weighted sum of residual channels to output.
Avoids materialising mixes / rsqrt / pre tensors to global memory.
"""
num_tokens = T.dynamic("num_tokens")
hc_dim = hc_mult * hidden_size
h_block = math.gcd(h_blk, hidden_size)
n_h = hidden_size // h_block
residual: T.Tensor[[num_tokens, hc_mult, hidden_size], T.bfloat16] # type: ignore[no-redef,valid-type]
fn: T.Tensor[[hc_mult, hc_dim], T.float32] # type: ignore[no-redef,valid-type]
hc_scale: T.Tensor[[1], T.float32] # type: ignore[no-redef,valid-type]
hc_base: T.Tensor[[hc_mult], T.float32] # type: ignore[no-redef,valid-type]
out: T.Tensor[[num_tokens, hidden_size], T.bfloat16] # type: ignore[no-redef,valid-type]
with T.Kernel(num_tokens, threads=n_thr) as i:
T.pdl_sync()
# ------------------------------------------------------------------
# Pass 1 for each residual channel m_c and h_block:
# • accumulate squared sum (for RMS norm denominator)
# • accumulate hc_mult dot-products with fn rows
# ------------------------------------------------------------------
sqrsum_r = T.alloc_reducer((1,), T.float32, replication="all")
mixes_r = T.alloc_reducer((hc_mult,), T.float32, replication="all")
T.fill(sqrsum_r, 0.0)
T.fill(mixes_r, 0.0)
for m_c in T.serial(hc_mult):
for i_h in T.serial(n_h):
x_local = T.alloc_fragment(h_block, T.float32)
T.copy(residual[i, m_c, i_h * h_block], x_local)
for k in T.Parallel(h_block):
sqrsum_r[0] += x_local[k] * x_local[k]
for m_m in T.unroll(hc_mult):
fn_local = T.alloc_fragment(h_block, T.float32)
T.copy(fn[m_m, m_c * hidden_size + i_h * h_block], fn_local)
for k in T.Parallel(h_block):
mixes_r[m_m] += x_local[k] * fn_local[k]
T.finalize_reducer(sqrsum_r)
T.finalize_reducer(mixes_r)
# ------------------------------------------------------------------
# Compute pre_mix = sigmoid(mix * rsqrt * scale + base) + eps
# ------------------------------------------------------------------
pre_mix_shared = T.alloc_shared(hc_mult, T.float32)
rsqrt_val = T.alloc_fragment(1, T.float32)
rsqrt_val[0] = T.rsqrt(sqrsum_r[0] / hc_dim + rms_eps)
for m in T.Parallel(hc_mult):
pre_mix_shared[m] = (
T.sigmoid(mixes_r[m] * rsqrt_val[0] * hc_scale[0] + hc_base[m]) + hc_eps
)
# ------------------------------------------------------------------
# Pass 2 apply_mix: pipelined weighted sum over residual channels
# ------------------------------------------------------------------
for i0_h in T.Pipelined(n_h, num_stages=2):
xs = T.alloc_shared((hc_mult, h_block), T.bfloat16)
xl = T.alloc_fragment((hc_mult, h_block), T.float32)
T.copy(residual[i, 0, i0_h * h_block], xs, disable_tma=True)
T.copy(xs, xl)
ol = T.alloc_fragment(h_block, T.float32)
T.clear(ol)
for i_hc in T.serial(hc_mult):
pre = pre_mix_shared[i_hc]
for i1_h in T.Parallel(h_block):
ol[i1_h] += pre * xl[i_hc, i1_h]
T.copy(ol, out[i, i0_h * h_block], disable_tma=True)
T.pdl_trigger()
def _hc_head_fused_kernel(
hs_flat: torch.Tensor,
fn: torch.Tensor,
hc_scale: torch.Tensor,
hc_base: torch.Tensor,
out: torch.Tensor,
hidden_size: int,
rms_eps: float,
hc_eps: float,
hc_mult: int,
) -> None:
"""Fill pre-allocated `out` (T, H) in-place with the hc_head result."""
if hs_flat.shape[0] > 0:
hc_head_fuse_tilelang(
hs_flat,
fn,
hc_scale,
hc_base,
out,
hidden_size,
rms_eps,
hc_eps,
hc_mult,
)
direct_register_custom_op(
op_name="hc_head_fused_kernel",
op_func=_hc_head_fused_kernel,
mutates_args=["out"],
)
@@ -1571,14 +1571,14 @@ class QuarkOCP_MX_MoEMethod_OSS(QuarkOCP_MX_MoEMethod):
def apply_monolithic( def apply_monolithic(
self, self,
layer: torch.nn.Module, layer: FusedMoE,
x: torch.Tensor, x: torch.Tensor,
router_logits: torch.Tensor, router_logits: torch.Tensor,
expert_map: torch.Tensor | None = None, input_ids: torch.Tensor | None = None,
) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]: ) -> torch.Tensor:
if layer.enable_eplb: if layer.enable_eplb:
raise NotImplementedError( raise NotImplementedError(
"EPLB not supported for `QuarkW4MXFp4MoEMethod_OSS` yet." f"EPLB not supported for {self.__class__.__name__} yet."
) )
from vllm.model_executor.layers.fused_moe.experts.gpt_oss_triton_kernels_moe import ( # noqa: E501 from vllm.model_executor.layers.fused_moe.experts.gpt_oss_triton_kernels_moe import ( # noqa: E501
@@ -1595,7 +1595,7 @@ class QuarkOCP_MX_MoEMethod_OSS(QuarkOCP_MX_MoEMethod):
topk=layer.top_k, topk=layer.top_k,
renormalize=layer.renormalize, renormalize=layer.renormalize,
global_num_experts=layer.global_num_experts, global_num_experts=layer.global_num_experts,
expert_map=expert_map, expert_map=layer.expert_map,
quant_config=self.moe_quant_config, quant_config=self.moe_quant_config,
apply_router_weight_on_input=layer.apply_router_weight_on_input, apply_router_weight_on_input=layer.apply_router_weight_on_input,
unpadded_N_w1=self.moe.intermediate_size_per_partition_unpadded * 2, unpadded_N_w1=self.moe.intermediate_size_per_partition_unpadded * 2,
@@ -85,7 +85,9 @@ def _mxfp8_e4m3_quantize_torch(
def _mxfp8_e4m3_quantize_impl( def _mxfp8_e4m3_quantize_impl(
x: torch.Tensor, is_sf_swizzled_layout: bool = False x: torch.Tensor,
is_sf_swizzled_layout: bool = False,
alignment: int = 0,
) -> tuple[torch.Tensor, torch.Tensor]: ) -> tuple[torch.Tensor, torch.Tensor]:
from vllm.platforms import current_platform from vllm.platforms import current_platform
@@ -93,7 +95,9 @@ def _mxfp8_e4m3_quantize_impl(
from flashinfer import mxfp8_quantize as flashinfer_mxfp8_quantize from flashinfer import mxfp8_quantize as flashinfer_mxfp8_quantize
x_q, x_scales = flashinfer_mxfp8_quantize( x_q, x_scales = flashinfer_mxfp8_quantize(
x, is_sf_swizzled_layout=is_sf_swizzled_layout x,
is_sf_swizzled_layout=is_sf_swizzled_layout,
alignment=alignment if alignment > 0 else 32,
) )
if x_scales.ndim == 1 and x.ndim == 2 and not is_sf_swizzled_layout: if x_scales.ndim == 1 and x.ndim == 2 and not is_sf_swizzled_layout:
x_scales = x_scales.view(x.size(0), -1) x_scales = x_scales.view(x.size(0), -1)
@@ -103,9 +107,11 @@ def _mxfp8_e4m3_quantize_impl(
def mxfp8_e4m3_quantize( def mxfp8_e4m3_quantize(
x: torch.Tensor, is_sf_swizzled_layout: bool = False x: torch.Tensor,
is_sf_swizzled_layout: bool = False,
alignment: int = 0,
) -> tuple[torch.Tensor, torch.Tensor]: ) -> tuple[torch.Tensor, torch.Tensor]:
return torch.ops.vllm.mxfp8_quantize(x, is_sf_swizzled_layout) return torch.ops.vllm.mxfp8_quantize(x, is_sf_swizzled_layout, alignment)
def dequant_mxfp8_to_bf16(x: torch.Tensor, scales: torch.Tensor) -> torch.Tensor: def dequant_mxfp8_to_bf16(x: torch.Tensor, scales: torch.Tensor) -> torch.Tensor:
@@ -125,7 +131,9 @@ def dequant_mxfp8_to_bf16(x: torch.Tensor, scales: torch.Tensor) -> torch.Tensor
def mxfp8_e4m3_quantize_fake( def mxfp8_e4m3_quantize_fake(
x: torch.Tensor, is_sf_swizzled_layout: bool = False x: torch.Tensor,
is_sf_swizzled_layout: bool = False,
alignment: int = 0,
) -> tuple[torch.Tensor, torch.Tensor]: ) -> tuple[torch.Tensor, torch.Tensor]:
"""Fake implementation for torch.compile tracing.""" """Fake implementation for torch.compile tracing."""
fp_data = torch.empty_like(x, dtype=MXFP8_VALUE_DTYPE) fp_data = torch.empty_like(x, dtype=MXFP8_VALUE_DTYPE)
@@ -45,6 +45,7 @@ class DeepseekScalingRotaryEmbedding(RotaryEmbeddingBase):
beta_slow: int = 1, beta_slow: int = 1,
mscale: float = 1, mscale: float = 1,
mscale_all_dim: float = 0, mscale_all_dim: float = 0,
init_cache: bool = True,
) -> None: ) -> None:
self.scaling_factor = scaling_factor self.scaling_factor = scaling_factor
self.extrapolation_factor = extrapolation_factor self.extrapolation_factor = extrapolation_factor
@@ -65,7 +66,13 @@ class DeepseekScalingRotaryEmbedding(RotaryEmbeddingBase):
and head_size in [64, 128, 256, 512] and head_size in [64, 128, 256, 512]
) )
super().__init__( super().__init__(
head_size, rotary_dim, max_position_embeddings, base, is_neox_style, dtype head_size,
rotary_dim,
max_position_embeddings,
base,
is_neox_style,
dtype,
init_cache=init_cache,
) )
def _compute_inv_freq(self, scaling_factor: float) -> torch.Tensor: def _compute_inv_freq(self, scaling_factor: float) -> torch.Tensor:
@@ -211,7 +218,9 @@ class DeepseekV4ScalingRotaryEmbedding(DeepseekScalingRotaryEmbedding):
""" """
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) # Avoid compute cache repeatedly
kwargs.pop("init_cache", None)
super().__init__(*args, **kwargs, init_cache=False)
cache_fp32 = self._compute_cos_sin_cache() cache_fp32 = self._compute_cos_sin_cache()
self.register_buffer("cos_sin_cache", cache_fp32, persistent=False) self.register_buffer("cos_sin_cache", cache_fp32, persistent=False)
@@ -320,7 +320,7 @@ def sparse_attn_indexer(
num_rows = logits.shape[0] num_rows = logits.shape[0]
topk_indices = topk_indices_buffer[:num_padded_tokens, :topk_tokens] topk_indices = topk_indices_buffer[:num_padded_tokens, :topk_tokens]
if current_platform.is_cuda() and topk_tokens in (512, 1024, 2048): if current_platform.is_cuda() and topk_tokens in (512, 2048):
workspace_manager = current_workspace_manager() workspace_manager = current_workspace_manager()
(topk_workspace,) = workspace_manager.get_simultaneous( (topk_workspace,) = workspace_manager.get_simultaneous(
((RADIX_TOPK_WORKSPACE_SIZE,), torch.uint8), ((RADIX_TOPK_WORKSPACE_SIZE,), torch.uint8),
@@ -17,6 +17,7 @@ from vllm.distributed import (
) )
from vllm.forward_context import get_forward_context from vllm.forward_context import get_forward_context
from vllm.logger import init_logger from vllm.logger import init_logger
from vllm.model_executor.custom_op import PluggableLayer
from vllm.model_executor.layers.fla.ops.layernorm_guard import ( from vllm.model_executor.layers.fla.ops.layernorm_guard import (
RMSNormGated, RMSNormGated,
layernorm_fn, layernorm_fn,
@@ -204,14 +205,19 @@ class BailingMoeV25MLAAttention(nn.Module):
self.q_a_layernorm = None self.q_a_layernorm = None
self.q_b_proj = None self.q_b_proj = None
rope_parameters = _build_rope_parameters(config) rope_parameters = _build_rope_parameters(config) or {}
# MLA rotates the full qk_rope_head_dim,
# partial_rotary_factor is for the linear-attn head only.
rope_parameters = {
k: v for k, v in rope_parameters.items() if k != "partial_rotary_factor"
}
rope_parameters["rope_dim"] = self.qk_rope_head_dim
max_position = getattr(config, "max_position_embeddings", 8192) max_position = getattr(config, "max_position_embeddings", 8192)
self.rotary_emb = get_rope( self.rotary_emb = get_rope(
head_size=self.qk_rope_head_dim, head_size=self.qk_rope_head_dim,
max_position=max_position, max_position=max_position,
is_neox_style=False, is_neox_style=False,
rope_parameters=rope_parameters or None, rope_parameters=rope_parameters,
dtype=torch.float32,
) )
# Build MLAModules for MultiHeadLatentAttentionWrapper # Build MLAModules for MultiHeadLatentAttentionWrapper
@@ -425,14 +431,18 @@ class BailingGroupRMSNormGate(RMSNormGated):
param.data.copy_(loaded_weight[shard].contiguous()) param.data.copy_(loaded_weight[shard].contiguous())
class BailingMoELinearAttention(nn.Module, MambaBase): # --8<-- [start:bailing_moe_linear_attention]
""" @PluggableLayer.register("bailing_moe_linear_attention")
Bailing MoE Linear Attention implementation using minimax backend. class BailingMoELinearAttention(PluggableLayer, MambaBase):
"""Pluggable Bailing MoE Linear Attention layer which allows OOT backends
to add custom implementations.
This implements the linear attention mechanism from sglang, adapted for vLLM's This implements the linear attention mechanism from sglang, adapted for
v1 engine with MambaBase interface support. vLLM's v1 engine with MambaBase interface support.
""" """
# --8<-- [end:bailing_moe_linear_attention]
@property @property
def mamba_type(self) -> str: def mamba_type(self) -> str:
return "linear_attention" return "linear_attention"
@@ -569,7 +579,6 @@ class BailingMoELinearAttention(nn.Module, MambaBase):
self.head_dim, self.head_dim,
max_position=self.max_position_embeddings, max_position=self.max_position_embeddings,
is_neox_style=True, is_neox_style=True,
dtype=torch.float32,
rope_parameters=rope_parameters or None, rope_parameters=rope_parameters or None,
) )
@@ -754,8 +763,6 @@ class BailingMoELinearAttention(nn.Module, MambaBase):
def _decode_infer(self, q, k, v, kv_cache, state_indices_tensor, attn_metadata): def _decode_infer(self, q, k, v, kv_cache, state_indices_tensor, attn_metadata):
"""Handle decode (single token per sequence).""" """Handle decode (single token per sequence)."""
num_prefill_tokens = attn_metadata.num_prefill_tokens
num_prefills = attn_metadata.num_prefills
hidden = linear_attention_decode( hidden = linear_attention_decode(
q, q,
k, k,
@@ -763,10 +770,10 @@ class BailingMoELinearAttention(nn.Module, MambaBase):
kv_cache, kv_cache,
self.tp_slope, self.tp_slope,
state_indices_tensor, state_indices_tensor,
q_start=num_prefill_tokens, q_start=0,
q_end=None, q_end=attn_metadata.num_decode_tokens,
slot_start=num_prefills, slot_start=0,
slot_end=None, slot_end=attn_metadata.num_decodes,
block_size=32, block_size=32,
) )
return hidden return hidden
@@ -1149,6 +1156,7 @@ class BailingMoeV25ForCausalLM(nn.Module, HasInnerState, IsHybrid, SupportsPP):
config.vocab_size, config.vocab_size,
config.hidden_size, config.hidden_size,
quant_config=quant_config, quant_config=quant_config,
prefix=maybe_prefix(prefix, "lm_head"),
) )
self.logits_processor = LogitsProcessor(config.vocab_size) self.logits_processor = LogitsProcessor(config.vocab_size)
else: else:
+184 -48
View File
@@ -7,16 +7,16 @@ from itertools import islice
import regex as re import regex as re
import torch import torch
import torch.nn as nn import torch.nn as nn
import torch.nn.functional as F
from vllm.compilation.decorators import support_torch_compile from vllm.compilation.decorators import support_torch_compile
from vllm.config import VllmConfig from vllm.config import VllmConfig, get_current_vllm_config
from vllm.distributed import ( from vllm.distributed import (
get_ep_group, get_ep_group,
get_tensor_model_parallel_rank, get_tensor_model_parallel_rank,
get_tensor_model_parallel_world_size, get_tensor_model_parallel_world_size,
) )
from vllm.forward_context import get_forward_context from vllm.forward_context import get_forward_context
from vllm.model_executor.layers.activation import SiluAndMul, SiluAndMulWithClamp
from vllm.model_executor.layers.deepseek_v4_attention import ( from vllm.model_executor.layers.deepseek_v4_attention import (
DeepseekV4Indexer, DeepseekV4Indexer,
DeepseekV4MLAModules, DeepseekV4MLAModules,
@@ -34,7 +34,10 @@ from vllm.model_executor.layers.linear import (
RowParallelLinear, RowParallelLinear,
) )
from vllm.model_executor.layers.logits_processor import LogitsProcessor from vllm.model_executor.layers.logits_processor import LogitsProcessor
from vllm.model_executor.layers.quantization import QuantizationMethods from vllm.model_executor.layers.quantization import (
QuantizationConfig,
QuantizationMethods,
)
from vllm.model_executor.layers.quantization.fp8 import Fp8Config from vllm.model_executor.layers.quantization.fp8 import Fp8Config
from vllm.model_executor.layers.quantization.mxfp4 import Mxfp4MoEMethod from vllm.model_executor.layers.quantization.mxfp4 import Mxfp4MoEMethod
from vllm.model_executor.layers.quantization.utils.quant_utils import ( from vllm.model_executor.layers.quantization.utils.quant_utils import (
@@ -46,12 +49,10 @@ from vllm.model_executor.layers.vocab_parallel_embedding import (
VocabParallelEmbedding, VocabParallelEmbedding,
) )
from vllm.model_executor.model_loader.weight_utils import default_weight_loader from vllm.model_executor.model_loader.weight_utils import default_weight_loader
from vllm.model_executor.models.deepseek_v2 import DeepseekV2MLP
from vllm.model_executor.utils import set_weight_attrs from vllm.model_executor.utils import set_weight_attrs
from vllm.platforms import current_platform from vllm.platforms import current_platform
from vllm.sequence import IntermediateTensors from vllm.sequence import IntermediateTensors
from vllm.triton_utils import tl, triton from vllm.triton_utils import tl, triton
from vllm.utils.multi_stream_utils import AuxStreamType
from vllm.utils.torch_utils import direct_register_custom_op from vllm.utils.torch_utils import direct_register_custom_op
from .utils import ( from .utils import (
@@ -62,18 +63,114 @@ from .utils import (
maybe_prefix, maybe_prefix,
) )
_DEEPSEEK_V4_EXPERT_DTYPES = ("fp4", "fp8")
class DeepseekV4MLP(nn.Module):
def __init__(
self,
hidden_size: int,
intermediate_size: int,
hidden_act: str,
swiglu_limit: float | None = None,
quant_config: QuantizationConfig | None = None,
reduce_results: bool = True,
is_sequence_parallel: bool = False,
prefix: str = "",
) -> None:
super().__init__()
# If is_sequence_parallel, the input and output tensors are sharded
# across the ranks within the tp_group. In this case the weights are
# replicated and no collective ops are needed.
# Otherwise we use standard TP with an allreduce at the end.
self.gate_up_proj = MergedColumnParallelLinear(
hidden_size,
[intermediate_size] * 2,
bias=False,
quant_config=quant_config,
disable_tp=is_sequence_parallel,
prefix=f"{prefix}.gate_up_proj",
)
self.down_proj = RowParallelLinear(
intermediate_size,
hidden_size,
bias=False,
quant_config=quant_config,
reduce_results=reduce_results,
disable_tp=is_sequence_parallel,
prefix=f"{prefix}.down_proj",
)
if hidden_act != "silu":
raise ValueError(
f"Unsupported activation: {hidden_act}. Only silu is supported for now."
)
if swiglu_limit is not None:
self.act_fn = SiluAndMulWithClamp(swiglu_limit)
else:
self.act_fn = SiluAndMul()
def forward(self, x):
gate_up, _ = self.gate_up_proj(x)
x = self.act_fn(gate_up)
x, _ = self.down_proj(x)
return x
class DeepseekV4FP8Config(Fp8Config): class DeepseekV4FP8Config(Fp8Config):
"""FP8 config that routes MoE layers to MXFP4 quantization. """FP8 config for DeepSeek V4 with expert-dtype-aware MoE dispatch.
DeepSeek V4 checkpoints use FP8 for linear/attention layers but DeepSeek V4 checkpoints always use FP8 block quantization for
MXFP4 for MoE expert weights. This config inherits standard FP8 linear/attention layers. The MoE expert weights vary by checkpoint:
behavior and overrides only the MoE dispatch. - ``expert_dtype="fp4"`` (e.g. DeepSeek-V4-Flash): MXFP4 experts
with ue8m0 (e8m0fnu) FP8 linear scales.
- ``expert_dtype="fp8"`` (e.g. DeepSeek-V4-Flash-Base): FP8 block
experts with float32 FP8 linear scales.
The dispatch and the linear scale dtype are both keyed off
``expert_dtype`` from the model's hf_config; missing values default
to ``"fp4"`` so existing FP4 checkpoints stay unchanged.
NOTE: ``expert_dtype`` is resolved lazily because this config is
constructed during VllmConfig setup, before ``set_current_vllm_config``
is active. Reading hf_config eagerly in ``__init__`` would always see
the default ``"fp4"`` and silently misroute Flash-Base checkpoints.
""" """
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.is_scale_e8m0: bool = True self._resolved_expert_dtype: str | None = None
# ``is_scale_e8m0`` is a property that resolves on first read,
# by which time the current vllm_config has been set.
@property
def expert_dtype(self) -> str:
if self._resolved_expert_dtype is None:
try:
hf_config = get_current_vllm_config().model_config.hf_config
except Exception:
# vllm_config not yet set; defer the decision until a
# later call lands inside set_current_vllm_config.
return "fp4"
expert_dtype = getattr(hf_config, "expert_dtype", "fp4")
if expert_dtype not in _DEEPSEEK_V4_EXPERT_DTYPES:
raise ValueError(
f"Unsupported DeepSeek V4 expert_dtype={expert_dtype!r}; "
f"expected one of {_DEEPSEEK_V4_EXPERT_DTYPES}."
)
self._resolved_expert_dtype = expert_dtype
from vllm.logger import init_logger
init_logger(__name__).info_once(
"DeepSeek V4 expert_dtype resolved to %r", expert_dtype
)
return self._resolved_expert_dtype
@property
def is_scale_e8m0(self) -> bool:
# FP4 checkpoints store FP8 linear scales as e8m0fnu; FP8 expert
# checkpoints (Flash-Base) store them as float32.
return self.expert_dtype == "fp4"
@classmethod @classmethod
def get_name(cls) -> QuantizationMethods: def get_name(cls) -> QuantizationMethods:
@@ -101,11 +198,14 @@ class DeepseekV4FP8Config(Fp8Config):
fused_mapping=self.packed_modules_mapping, fused_mapping=self.packed_modules_mapping,
): ):
return UnquantizedFusedMoEMethod(layer.moe_config) return UnquantizedFusedMoEMethod(layer.moe_config)
return Mxfp4MoEMethod(layer.moe_config) if self.expert_dtype == "fp4":
return Mxfp4MoEMethod(layer.moe_config)
# expert_dtype == "fp8": fall through to Fp8Config which
# returns Fp8MoEMethod with block-wise float32 scales.
return super().get_quant_method(layer, prefix) return super().get_quant_method(layer, prefix)
def is_mxfp4_quant(self, prefix, layer): def is_mxfp4_quant(self, prefix, layer):
return isinstance(layer, FusedMoE) return isinstance(layer, FusedMoE) and self.expert_dtype == "fp4"
@triton.jit @triton.jit
@@ -635,6 +735,12 @@ class DeepseekV4MoE(nn.Module):
raise NotImplementedError( raise NotImplementedError(
"DeepSeek V4 MegaMoE currently supports sqrtsoftplus routing only." "DeepSeek V4 MegaMoE currently supports sqrtsoftplus routing only."
) )
if self.use_mega_moe and getattr(config, "expert_dtype", "fp4") != "fp4":
raise NotImplementedError(
"DeepSeek V4 MegaMoE only supports fp4 experts; got expert_dtype="
f"{config.expert_dtype!r}. Drop --kernel-config moe_backend="
"deep_gemm_mega_moe for this checkpoint."
)
self.gate = GateLinear( self.gate = GateLinear(
config.hidden_size, config.hidden_size,
@@ -672,10 +778,11 @@ class DeepseekV4MoE(nn.Module):
else: else:
intermediate_size = config.moe_intermediate_size * config.n_shared_experts intermediate_size = config.moe_intermediate_size * config.n_shared_experts
self.shared_experts = DeepseekV2MLP( self.shared_experts = DeepseekV4MLP(
hidden_size=config.hidden_size, hidden_size=config.hidden_size,
intermediate_size=intermediate_size, intermediate_size=intermediate_size,
hidden_act=config.hidden_act, hidden_act=config.hidden_act,
swiglu_limit=self.swiglu_limit,
quant_config=quant_config, quant_config=quant_config,
reduce_results=self.use_mega_moe, reduce_results=self.use_mega_moe,
prefix=f"{prefix}.shared_experts", prefix=f"{prefix}.shared_experts",
@@ -817,7 +924,7 @@ class DeepseekV4Attention(nn.Module):
vllm_config: VllmConfig, vllm_config: VllmConfig,
prefix: str, prefix: str,
topk_indices_buffer: torch.Tensor | None = None, topk_indices_buffer: torch.Tensor | None = None,
aux_stream: torch.cuda.Stream | None = None, aux_stream_list: list[torch.cuda.Stream] | None = None,
): ):
super().__init__() super().__init__()
config = vllm_config.model_config.hf_config config = vllm_config.model_config.hf_config
@@ -919,7 +1026,6 @@ class DeepseekV4Attention(nn.Module):
max_position=self.max_position_embeddings, max_position=self.max_position_embeddings,
rope_parameters=rope_parameters, rope_parameters=rope_parameters,
is_neox_style=False, is_neox_style=False,
dtype=config.torch_dtype,
) )
self.indexer = None self.indexer = None
@@ -950,7 +1056,7 @@ class DeepseekV4Attention(nn.Module):
indexer=self.indexer, indexer=self.indexer,
indexer_rotary_emb=self.rotary_emb, indexer_rotary_emb=self.rotary_emb,
topk_indices_buffer=topk_indices_buffer, topk_indices_buffer=topk_indices_buffer,
aux_stream=aux_stream, aux_stream_list=aux_stream_list,
) )
self.mla_attn = DeepseekV4MultiHeadLatentAttentionWrapper( self.mla_attn = DeepseekV4MultiHeadLatentAttentionWrapper(
hidden_size=self.hidden_size, hidden_size=self.hidden_size,
@@ -986,9 +1092,14 @@ class DeepseekV4DecoderLayer(nn.Module):
vllm_config, vllm_config,
prefix, prefix,
topk_indices_buffer: torch.Tensor | None = None, topk_indices_buffer: torch.Tensor | None = None,
aux_stream_dict: dict[AuxStreamType, torch.cuda.Stream] | None = None, aux_stream_list: list[torch.cuda.Stream] | None = None,
): ):
super().__init__() super().__init__()
# Lazy import to avoid top-level tilelang dependency.
# Registers both torch.ops.vllm.mhc_pre and mhc_post
import vllm.model_executor.layers.mhc # noqa: F401
config = vllm_config.model_config.hf_config config = vllm_config.model_config.hf_config
self.hidden_size = config.hidden_size self.hidden_size = config.hidden_size
@@ -997,9 +1108,7 @@ class DeepseekV4DecoderLayer(nn.Module):
vllm_config, vllm_config,
prefix=f"{prefix}.attn", prefix=f"{prefix}.attn",
topk_indices_buffer=topk_indices_buffer, topk_indices_buffer=topk_indices_buffer,
aux_stream=aux_stream_dict.get(AuxStreamType.Attention) aux_stream_list=aux_stream_list,
if aux_stream_dict is not None
else None,
) )
self.ffn = DeepseekV4MoE(vllm_config, prefix=f"{prefix}.ffn") self.ffn = DeepseekV4MoE(vllm_config, prefix=f"{prefix}.ffn")
@@ -1061,11 +1170,6 @@ class DeepseekV4DecoderLayer(nn.Module):
hc_scale: torch.Tensor, hc_scale: torch.Tensor,
hc_base: torch.Tensor, hc_base: torch.Tensor,
): ):
# Lazy import to avoid top-level tilelang dependency.
# Registers both torch.ops.vllm.mhc_pre and mhc_post,
# so hc_post() doesn't need its own import.
import vllm.model_executor.layers.mhc # noqa: F401
post_mix, res_mix, layer_input = torch.ops.vllm.mhc_pre( post_mix, res_mix, layer_input = torch.ops.vllm.mhc_pre(
residual=x, residual=x,
fn=hc_fn, fn=hc_fn,
@@ -1127,10 +1231,11 @@ class DeepseekV4Model(nn.Module):
self.hc_dim = self.hc_mult * config.hidden_size self.hc_dim = self.hc_mult * config.hidden_size
self.rms_norm_eps = config.rms_norm_eps self.rms_norm_eps = config.rms_norm_eps
aux_stream_list = [torch.cuda.Stream() for _ in range(1)] # Three aux streams: one per non-default input GEMM in
self.aux_stream_dict = { # DeepseekV4MultiHeadLatentAttentionWrapper.attn_gemm_parallel_execute
AuxStreamType.Attention: aux_stream_list[0], # (compressor kv_score, indexer.weights_proj, indexer.compressor
} # kv_score). fused_wqa_wkv stays on the default stream.
aux_stream_list = [torch.cuda.Stream() for _ in range(3)]
self.device = current_platform.device_type self.device = current_platform.device_type
# Reserved topk indices buffer for all Indexer layers to reuse. # Reserved topk indices buffer for all Indexer layers to reuse.
@@ -1154,7 +1259,7 @@ class DeepseekV4Model(nn.Module):
vllm_config, vllm_config,
prefix=prefix, prefix=prefix,
topk_indices_buffer=self.topk_indices_buffer, topk_indices_buffer=self.topk_indices_buffer,
aux_stream_dict=self.aux_stream_dict, aux_stream_list=aux_stream_list,
), ),
prefix=f"{prefix}.layers", prefix=f"{prefix}.layers",
) )
@@ -1345,20 +1450,45 @@ def hc_head(
rms_norm_eps: float, rms_norm_eps: float,
hc_eps: float, hc_eps: float,
) -> torch.Tensor: ) -> torch.Tensor:
x = hidden_states hc_mult, hidden_size = hidden_states.shape[-2:]
shape, dtype = x.size(), x.dtype outer_shape = hidden_states.shape[:-2]
x = x.flatten(1).float() hs_flat = hidden_states.view(-1, hc_mult, hidden_size)
rsqrt = torch.rsqrt(x.square().mean(-1, keepdim=True) + rms_norm_eps) num_tokens = hs_flat.shape[0]
mixes = F.linear(x, hc_fn) * rsqrt out = torch.empty(
pre = torch.sigmoid(mixes * hc_scale + hc_base) + hc_eps num_tokens, hidden_size, dtype=torch.bfloat16, device=hidden_states.device
y = torch.sum(pre.unsqueeze(-1) * x.view(shape), dim=1) )
return y.to(dtype) torch.ops.vllm.hc_head_fused_kernel(
hs_flat,
hc_fn,
hc_scale,
hc_base,
out,
hidden_size,
rms_norm_eps,
hc_eps,
hc_mult,
)
return out.view(*outer_shape, hidden_size)
class DeepseekV4ForCausalLM(nn.Module): def _make_deepseek_v4_weights_mapper(expert_dtype: str) -> WeightsMapper:
model_cls = DeepseekV4Model if expert_dtype == "fp4":
# MXFP4 experts use Mxfp4MoEMethod, which registers scales as
hf_to_vllm_mapper = WeightsMapper( # ``w{1,2,3}_weight_scale`` (no _inv suffix). FP8 linear and
# shared experts use Fp8LinearMethod's block scales, which
# register as ``weight_scale_inv``.
scale_regex = {
re.compile(r"(\.experts\.\d+\.w[123])\.scale$"): r"\1.weight_scale",
re.compile(r"\.scale$"): ".weight_scale_inv",
}
else:
# FP8 experts use Fp8MoEMethod (block_quant=True), which registers
# scales as ``w{13,2}_weight_scale_inv``. Map all ``.scale`` keys
# there.
scale_regex = {
re.compile(r"\.scale$"): ".weight_scale_inv",
}
return WeightsMapper(
orig_to_new_prefix={ orig_to_new_prefix={
"layers.": "model.layers.", "layers.": "model.layers.",
"embed.": "model.embed.", "embed.": "model.embed.",
@@ -1366,12 +1496,7 @@ class DeepseekV4ForCausalLM(nn.Module):
"hc_head": "model.hc_head", "hc_head": "model.hc_head",
"mtp.": "model.mtp.", "mtp.": "model.mtp.",
}, },
orig_to_new_regex={ orig_to_new_regex=scale_regex,
# Routed MoE expert scales: experts.N.wX.scale -> .weight_scale
re.compile(r"(\.experts\.\d+\.w[123])\.scale$"): r"\1.weight_scale",
# Everything else (FP8 linear + shared experts): .scale -> .weight_scale_inv
re.compile(r"\.scale$"): ".weight_scale_inv",
},
orig_to_new_suffix={ orig_to_new_suffix={
"head.weight": "lm_head.weight", "head.weight": "lm_head.weight",
"embed.weight": "embed_tokens.weight", "embed.weight": "embed_tokens.weight",
@@ -1383,11 +1508,22 @@ class DeepseekV4ForCausalLM(nn.Module):
}, },
) )
class DeepseekV4ForCausalLM(nn.Module):
model_cls = DeepseekV4Model
# Default mapper assumes the original FP4-expert checkpoint layout.
# Overridden per-instance in __init__ when expert_dtype != "fp4".
hf_to_vllm_mapper = _make_deepseek_v4_weights_mapper("fp4")
def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""): def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
super().__init__() super().__init__()
config = vllm_config.model_config.hf_config config = vllm_config.model_config.hf_config
self.config = config self.config = config
expert_dtype = getattr(config, "expert_dtype", "fp4")
if expert_dtype != "fp4":
self.hf_to_vllm_mapper = _make_deepseek_v4_weights_mapper(expert_dtype)
self.model = self.model_cls( self.model = self.model_cls(
vllm_config=vllm_config, prefix=maybe_prefix(prefix, "model") vllm_config=vllm_config, prefix=maybe_prefix(prefix, "model")
+25 -9
View File
@@ -35,7 +35,6 @@ from vllm.model_executor.layers.vocab_parallel_embedding import (
from vllm.model_executor.model_loader.weight_utils import default_weight_loader from vllm.model_executor.model_loader.weight_utils import default_weight_loader
from vllm.platforms import current_platform from vllm.platforms import current_platform
from vllm.sequence import IntermediateTensors from vllm.sequence import IntermediateTensors
from vllm.utils.multi_stream_utils import AuxStreamType
from .deepseek_mtp import SharedHead from .deepseek_mtp import SharedHead
from .deepseek_v2 import get_spec_layer_idx_from_weight_name from .deepseek_v2 import get_spec_layer_idx_from_weight_name
@@ -48,9 +47,14 @@ from .utils import maybe_prefix
logger = init_logger(__name__) logger = init_logger(__name__)
# MoE expert scales are fused into per-layer w13/w2 tensors; other FP8 linear # MoE expert scales are fused into per-layer w13/w2 tensors. The exact
# scales use `.weight_scale_inv`. Mirrors the regex in # parameter suffix depends on which FusedMoE method handles the experts:
# DeepseekV4ForCausalLM.hf_to_vllm_mapper. # - fp4 experts (Mxfp4MoEMethod) register ``w{1,2,3}_weight_scale``;
# - fp8 experts (Fp8MoEMethod with block_quant=True) register
# ``w{1,2,3}_weight_scale_inv``.
# Other FP8 linear scales (including shared experts) always use
# ``.weight_scale_inv``. Mirrors the per-instance mapper built by
# ``_make_deepseek_v4_weights_mapper`` in deepseek_v4.py.
_EXPERT_SCALE_RE = re.compile(r"\.experts\.\d+\.w[123]\.scale$") _EXPERT_SCALE_RE = re.compile(r"\.experts\.\d+\.w[123]\.scale$")
@@ -60,6 +64,7 @@ class DeepSeekV4MultiTokenPredictorLayer(nn.Module):
vllm_config: VllmConfig, vllm_config: VllmConfig,
topk_indices_buffer: torch.Tensor, topk_indices_buffer: torch.Tensor,
prefix: str, prefix: str,
aux_stream_list: list[torch.cuda.Stream] | None = None,
) -> None: ) -> None:
super().__init__() super().__init__()
@@ -107,14 +112,11 @@ class DeepSeekV4MultiTokenPredictorLayer(nn.Module):
self.shared_head = SharedHead( self.shared_head = SharedHead(
config=config, prefix=prefix, quant_config=quant_config config=config, prefix=prefix, quant_config=quant_config
) )
self.aux_stream_dict = {
AuxStreamType.Attention: torch.cuda.Stream(),
}
self.mtp_block = DeepseekV4DecoderLayer( self.mtp_block = DeepseekV4DecoderLayer(
vllm_config, vllm_config,
prefix, prefix,
topk_indices_buffer=topk_indices_buffer, topk_indices_buffer=topk_indices_buffer,
aux_stream_dict=self.aux_stream_dict, aux_stream_list=aux_stream_list,
) )
def forward( def forward(
@@ -164,6 +166,10 @@ class DeepSeekV4MultiTokenPredictor(nn.Module):
device=self.device, device=self.device,
) )
# Three aux streams shared across all MTP layers, mirroring
# DeepseekV4Model.
aux_stream_list = [torch.cuda.Stream() for _ in range(3)]
# to map the exact layer index from weights # to map the exact layer index from weights
self.layers = torch.nn.ModuleDict( self.layers = torch.nn.ModuleDict(
{ {
@@ -171,6 +177,7 @@ class DeepSeekV4MultiTokenPredictor(nn.Module):
vllm_config, vllm_config,
self.topk_indices_buffer, self.topk_indices_buffer,
f"{prefix}.layers.{idx}", f"{prefix}.layers.{idx}",
aux_stream_list=aux_stream_list,
) )
for idx in range( for idx in range(
self.mtp_start_layer_idx, self.mtp_start_layer_idx,
@@ -326,6 +333,15 @@ class DeepSeekV4MTP(nn.Module):
num_experts=self.config.n_routed_experts, num_experts=self.config.n_routed_experts,
) )
# FP8 experts register ``..._weight_scale_inv`` (block_quant) while
# FP4/MXFP4 experts register ``..._weight_scale``. Choose the suffix
# for the rename below based on the model's expert dtype.
expert_scale_suffix = (
".weight_scale"
if getattr(self.config, "expert_dtype", "fp4") == "fp4"
else ".weight_scale_inv"
)
for name, loaded_weight in weights: for name, loaded_weight in weights:
mtp_layer_idx = _find_mtp_layer_idx(name) mtp_layer_idx = _find_mtp_layer_idx(name)
# V4 checkpoints store MTP weights as `mtp.{i}.*`; remap to # V4 checkpoints store MTP weights as `mtp.{i}.*`; remap to
@@ -347,7 +363,7 @@ class DeepSeekV4MTP(nn.Module):
continue continue
if name.endswith(".scale"): if name.endswith(".scale"):
suffix = ( suffix = (
".weight_scale" expert_scale_suffix
if _EXPERT_SCALE_RE.search(name) if _EXPERT_SCALE_RE.search(name)
else ".weight_scale_inv" else ".weight_scale_inv"
) )
+7
View File
@@ -87,6 +87,13 @@ class PoolingParams(
return deepcopy(self) return deepcopy(self)
def verify(self, model_config: ModelConfig) -> None: def verify(self, model_config: ModelConfig) -> None:
if self.task == "score":
logger.warning_once(
"`score` task is deprecated and will be removed in v0.20. "
"Please use `classify` instead."
)
self.task = "classify"
# plugin task uses io_processor.parse_request to verify inputs, # plugin task uses io_processor.parse_request to verify inputs,
# skipping PoolingParams verify # skipping PoolingParams verify
if self.task == "plugin": if self.task == "plugin":
-5
View File
@@ -16,11 +16,6 @@ PoolingTask = Literal[
POOLING_TASKS: tuple[PoolingTask, ...] = get_args(PoolingTask) POOLING_TASKS: tuple[PoolingTask, ...] = get_args(PoolingTask)
ScoreType = Literal["bi-encoder", "cross-encoder", "late-interaction"] ScoreType = Literal["bi-encoder", "cross-encoder", "late-interaction"]
SCORE_TYPE_MAP: dict[PoolingTask, ScoreType] = {
"embed": "bi-encoder",
"classify": "cross-encoder",
"token_embed": "late-interaction",
}
FrontendTask = Literal["render"] FrontendTask = Literal["render"]
FRONTEND_TASKS: tuple[FrontendTask, ...] = get_args(FrontendTask) FRONTEND_TASKS: tuple[FrontendTask, ...] = get_args(FrontendTask)
+2 -1
View File
@@ -191,12 +191,13 @@ class DeepSeekV32ToolParser(ToolParser):
tool_call_match tool_call_match
): ):
param_dict = self._parse_invoke_params(invoke_content) param_dict = self._parse_invoke_params(invoke_content)
params = self._convert_params_with_schema(invoke_name, param_dict)
tool_calls.append( tool_calls.append(
ToolCall( ToolCall(
type="function", type="function",
function=FunctionCall( function=FunctionCall(
name=invoke_name, name=invoke_name,
arguments=json.dumps(param_dict, ensure_ascii=False), arguments=json.dumps(params, ensure_ascii=False),
), ),
) )
) )
+70
View File
@@ -56,3 +56,73 @@ def maybe_execute_in_parallel(
result0 = fn0() result0 = fn0()
result1 = fn1() result1 = fn1()
return (result0, result1) return (result0, result1)
def execute_in_parallel(
default_fn: Callable[[], Any],
aux_fns: list[Callable[[], Any] | None],
start_event: torch.cuda.Event,
done_events: list[torch.cuda.Event],
aux_streams: list[torch.cuda.Stream] | None = None,
enable: bool = False,
) -> tuple[Any, list[Any]]:
"""Run default_fn on the current stream and aux_fns concurrently on
aux_streams.
Generalizes maybe_execute_in_parallel to N aux callables. Slots where
aux_fns[i] is None are skipped (no stream switch, no event record); their
corresponding entry in the returned aux_results list is None.
start_event fans out from the current stream to every launched aux stream;
done_events[i] is recorded after aux_fns[i] so the current stream joins
before returning. Falls back to sequential execution on the current stream
when aux_streams is None or enable is False; in that case default_fn runs
first, then aux_fns in order.
Args:
default_fn: Callable for the default (current) stream.
aux_fns: Per-aux callables; entries may be None to skip.
start_event: CUDA event recorded on the current stream before
default_fn so each launched aux stream can wait on it.
done_events: One CUDA event per aux slot, recorded after the
corresponding aux_fn. Length must match aux_fns.
aux_streams: Per-aux CUDA streams. Length must match aux_fns.
Multi-stream is disabled when None.
enable: Opt-in switch for the multi-stream path. Defaults to False,
so callers that pass aux_streams must also pass enable=True
(typically gated by an env var) to actually overlap. When False,
execution falls back to sequential on the current stream.
Returns:
Tuple of (default_result, aux_results) where aux_results[i] is the
result of aux_fns[i] (or None when skipped).
"""
aux_results: list[Any]
if aux_streams is None or not enable:
default_result = default_fn()
aux_results = [fn() if fn is not None else None for fn in aux_fns]
return default_result, aux_results
assert len(aux_fns) == len(aux_streams) == len(done_events), (
"aux_fns, aux_streams, and done_events must be the same length"
)
aux_results = [None] * len(aux_fns)
pending: list[torch.cuda.Event] = []
start_event.record()
for i, fn in enumerate(aux_fns):
if fn is None:
continue
with torch.cuda.stream(aux_streams[i]):
start_event.wait()
aux_results[i] = fn()
done_events[i].record()
pending.append(done_events[i])
default_result = default_fn()
for ev in pending:
ev.wait()
return default_result, aux_results
@@ -21,7 +21,7 @@ and N_QUANT_BLOCKS ue8m0 bytes.
from vllm.triton_utils import tl, triton from vllm.triton_utils import tl, triton
from .fused_indexer_q import _e2m1_nibble from .fused_indexer_q import _fp32x2_to_fp4x2
# ============================================================================= # =============================================================================
@@ -566,18 +566,18 @@ def _fused_kv_compress_norm_rope_insert_indexer_mxfp4_attn(
tl.max(tl.abs(even_2d), axis=1), tl.max(tl.abs(even_2d), axis=1),
tl.max(tl.abs(odd_2d), axis=1), tl.max(tl.abs(odd_2d), axis=1),
) )
amax = tl.maximum(amax, 1e-4) amax = tl.maximum(amax, 6.0 * (2**-126))
# ue8m0 block scale: 2^ceil(log2(amax / 6.0)), stored as (exp + 127) byte. # ue8m0 block scale: 2^ceil(log2(amax / 6.0)), stored as (exp + 127) byte.
log2_ratio = tl.ceil(tl.log2(amax / 6.0)) log2_ratio = tl.ceil(tl.log2(amax * (1.0 / 6.0)))
log2_ratio = tl.minimum(tl.maximum(log2_ratio, -127.0), 127.0) log2_ratio = tl.minimum(tl.maximum(log2_ratio, -127.0), 127.0)
inv_scale = tl.exp2(-log2_ratio) inv_scale = tl.exp2(-log2_ratio)
ue8m0 = (log2_ratio + 127.0).to(tl.uint8) # [N_QUANT_BLOCKS] ue8m0 = (log2_ratio + 127.0).to(tl.uint8) # [N_QUANT_BLOCKS]
inv_scale_col = tl.reshape(inv_scale, (N_QUANT_BLOCKS, 1)) inv_scale_col = tl.reshape(inv_scale, (N_QUANT_BLOCKS, 1))
lo_nib = _e2m1_nibble(even_2d * inv_scale_col) # (N_BLOCKS, HALF_BLOCK) uint8 packed = _fp32x2_to_fp4x2(
hi_nib = _e2m1_nibble(odd_2d * inv_scale_col) even_2d * inv_scale_col, odd_2d * inv_scale_col
packed = lo_nib | (hi_nib << 4) ) # (N_BLOCKS, HALF_BLOCK) uint8
packed_flat = tl.reshape(packed, (TOKEN_STRIDE,)) packed_flat = tl.reshape(packed, (TOKEN_STRIDE,))
tl.store(val_ptr + tl.arange(0, TOKEN_STRIDE), packed_flat) tl.store(val_ptr + tl.arange(0, TOKEN_STRIDE), packed_flat)
@@ -24,36 +24,22 @@ def _get_cos_sin(
@triton.jit @triton.jit
def _e2m1_nibble(x): def _fp32x2_to_fp4x2(x_lo, x_hi):
"""Quantize fp32 x (already scale-divided) to E2M1 4-bit nibble in uint8. # NOTE: $1 is high nibble, $2 is low nibble
Matches torch.bucketize with boundaries return tl.inline_asm_elementwise(
[0.25, 0.75, 1.25, 1.75, 2.5, 3.5, 5.0] and right=False (each boundary """
belongs to the lower bucket), plus sign bit.""" {
abs_x = tl.minimum(tl.abs(x), 6.0) .reg .b8 tmp;
code = tl.where( cvt.rn.satfinite.e2m1x2.f32 tmp, $1, $2;
abs_x <= 0.25, cvt.u32.u8 $0, tmp;
0.0, }
tl.where( """,
abs_x <= 0.75, constraints="=r,f,f",
1.0, args=[x_hi, x_lo],
tl.where( dtype=tl.uint32,
abs_x <= 1.25, is_pure=True,
2.0, pack=1,
tl.where( ).to(tl.uint8)
abs_x <= 1.75,
3.0,
tl.where(
abs_x <= 2.5,
4.0,
tl.where(abs_x <= 3.5, 5.0, tl.where(abs_x <= 5.0, 6.0, 7.0)),
),
),
),
),
)
code_u8 = code.to(tl.uint8)
sign = ((x < 0) & (code_u8 != 0)).to(tl.uint8)
return code_u8 | (sign << 3)
@triton.jit @triton.jit
@@ -65,17 +51,16 @@ def _quantize_mxfp4_pair(x_lo, x_hi):
- ue8m0 : scalar uint8 (block scale = 2^(ue8m0 - 127)) - ue8m0 : scalar uint8 (block scale = 2^(ue8m0 - 127))
""" """
amax = tl.maximum(tl.max(tl.abs(x_lo)), tl.max(tl.abs(x_hi))) amax = tl.maximum(tl.max(tl.abs(x_lo)), tl.max(tl.abs(x_hi)))
amax = tl.maximum(amax, 1e-4) # 6 * 2^-126 is from https://huggingface.co/deepseek-ai/DeepSeek-V4-Pro/blob/main/inference/kernel.py#L163
amax = tl.maximum(amax, 6.0 * (2**-126))
# ue8m0 block scale: 2^ceil(log2(amax/6.0)). # ue8m0 block scale: 2^ceil(log2(amax/6.0)).
log2_ratio = tl.math.ceil(tl.math.log2(amax / 6.0)) log2_ratio = tl.math.ceil(tl.math.log2(amax * (1.0 / 6.0)))
log2_ratio = tl.minimum(tl.maximum(log2_ratio, -127.0), 127.0) log2_ratio = tl.minimum(tl.maximum(log2_ratio, -127.0), 127.0)
scale = tl.math.exp2(log2_ratio) scale = tl.math.exp2(log2_ratio)
ue8m0 = (log2_ratio + 127.0).to(tl.uint8) ue8m0 = (log2_ratio + 127.0).to(tl.uint8)
inv_scale = 1.0 / scale inv_scale = 1.0 / scale
lo_nib = _e2m1_nibble(x_lo * inv_scale) packed = _fp32x2_to_fp4x2(x_lo * inv_scale, x_hi * inv_scale)
hi_nib = _e2m1_nibble(x_hi * inv_scale)
packed = lo_nib | (hi_nib << 4)
return packed, ue8m0 return packed, ue8m0
@@ -10,6 +10,7 @@ INT32-packed UE8M0 on SM100) so fp8_einsum skips transform_sf_into_required_layo
import torch import torch
from vllm.triton_utils import tl, triton from vllm.triton_utils import tl, triton
from vllm.utils.torch_utils import direct_register_custom_op
@triton.jit @triton.jit
@@ -180,34 +181,74 @@ def fused_inv_rope_fp8_quant(
fp8_dtype = torch.float8_e4m3fn fp8_dtype = torch.float8_e4m3fn
fp8_max = torch.finfo(fp8_dtype).max fp8_max = torch.finfo(fp8_dtype).max
fp8_buf = torch.empty(
(n_groups, num_tokens, d),
dtype=fp8_dtype,
device=o.device,
)
tma_aligned_T = get_tma_aligned_size(num_tokens, 4) tma_aligned_T = get_tma_aligned_size(num_tokens, 4)
if tma_aligned_scales: if tma_aligned_scales:
packed_sf_k = (num_scale_blocks + 3) // 4 packed_sf_k = (num_scale_blocks + 3) // 4
scale_buf = torch.empty( scale_inner = packed_sf_k
n_groups * packed_sf_k * tma_aligned_T,
dtype=torch.int32,
device=o.device,
).as_strided(
(n_groups, num_tokens, packed_sf_k),
(packed_sf_k * tma_aligned_T, 1, tma_aligned_T),
)
else: else:
scale_buf = torch.empty( scale_inner = num_scale_blocks
n_groups * num_scale_blocks * tma_aligned_T,
dtype=torch.float32,
device=o.device,
).as_strided(
(n_groups, num_tokens, num_scale_blocks),
(num_scale_blocks * tma_aligned_T, 1, tma_aligned_T),
)
common_args = dict( # Run kernel through a custom op so inductor sees an opaque boundary.
# It's a pytorch bug, see https://github.com/vllm-project/vllm/issues/41106
fp8_buf, scale_buf = torch.ops.vllm.fused_inv_rope_fp8_quant_kernel(
o,
positions,
cos_sin_cache,
heads_per_group,
quant_group_size,
chunks_per_head,
nope_dim % quant_group_size,
rope_dim // 2,
tma_aligned_scales,
fp8_max,
tma_aligned_T,
num_tokens,
n_groups,
d,
scale_inner,
)
return fp8_buf.transpose(0, 1), scale_buf.transpose(0, 1)
def _fused_inv_rope_fp8_quant_kernel_impl(
o: torch.Tensor,
positions: torch.Tensor,
cos_sin_cache: torch.Tensor,
heads_per_group: int,
quant_group_size: int,
chunks_per_head: int,
rope_start: int,
half_rope: int,
tma_aligned_scales: bool,
fp8_max: float,
tma_aligned_T: int,
num_tokens: int,
n_groups: int,
d: int,
scale_inner: int,
) -> tuple[torch.Tensor, torch.Tensor]:
fp8_buf = torch.empty(
(n_groups, num_tokens, d),
dtype=torch.float8_e4m3fn,
device=o.device,
)
scale_dtype = torch.int32 if tma_aligned_scales else torch.float32
scale_buf = torch.empty(
n_groups * scale_inner * tma_aligned_T,
dtype=scale_dtype,
device=o.device,
).as_strided(
(n_groups, num_tokens, scale_inner),
(scale_inner * tma_aligned_T, 1, tma_aligned_T),
)
grid = (tma_aligned_T, n_groups * heads_per_group)
_fused_inv_rope_fp8_quant_per_head[grid](
o,
positions,
cos_sin_cache,
fp8_buf,
scale_buf,
num_tokens,
heads_per_group=heads_per_group, heads_per_group=heads_per_group,
o_stride_token=o.stride(0), o_stride_token=o.stride(0),
o_stride_head=o.stride(1), o_stride_head=o.stride(1),
@@ -220,23 +261,52 @@ def fused_inv_rope_fp8_quant(
eps=1e-10, eps=1e-10,
QUANT_GROUP_SIZE=quant_group_size, QUANT_GROUP_SIZE=quant_group_size,
CHUNKS_PER_HEAD=chunks_per_head, CHUNKS_PER_HEAD=chunks_per_head,
ROPE_START=nope_dim % quant_group_size, ROPE_START=rope_start,
HALF_ROPE=rope_dim // 2, HALF_ROPE=half_rope,
TMA_ALIGNED_SCALES=tma_aligned_scales, TMA_ALIGNED_SCALES=tma_aligned_scales,
num_stages=1, num_stages=1,
launch_pdl=False, launch_pdl=False,
)
grid = (tma_aligned_T, n_groups * heads_per_group)
_fused_inv_rope_fp8_quant_per_head[grid](
o,
positions,
cos_sin_cache,
fp8_buf,
scale_buf,
num_tokens,
**common_args,
num_warps=1, num_warps=1,
) )
return fp8_buf, scale_buf
return fp8_buf.transpose(0, 1), scale_buf.transpose(0, 1)
def _fused_inv_rope_fp8_quant_kernel_fake(
o: torch.Tensor,
positions: torch.Tensor,
cos_sin_cache: torch.Tensor,
heads_per_group: int,
quant_group_size: int,
chunks_per_head: int,
rope_start: int,
half_rope: int,
tma_aligned_scales: bool,
fp8_max: float,
tma_aligned_T: int,
num_tokens: int,
n_groups: int,
d: int,
scale_inner: int,
) -> tuple[torch.Tensor, torch.Tensor]:
fp8_buf = torch.empty(
(n_groups, num_tokens, d),
dtype=torch.float8_e4m3fn,
device=o.device,
)
scale_dtype = torch.int32 if tma_aligned_scales else torch.float32
scale_buf = torch.empty(
n_groups * scale_inner * tma_aligned_T,
dtype=scale_dtype,
device=o.device,
).as_strided(
(n_groups, num_tokens, scale_inner),
(scale_inner * tma_aligned_T, 1, tma_aligned_T),
)
return fp8_buf, scale_buf
direct_register_custom_op(
op_name="fused_inv_rope_fp8_quant_kernel",
op_func=_fused_inv_rope_fp8_quant_kernel_impl,
fake_impl=_fused_inv_rope_fp8_quant_kernel_fake,
)
+13
View File
@@ -34,6 +34,7 @@ class KVCacheCoordinator(ABC):
self, self,
kv_cache_config: KVCacheConfig, kv_cache_config: KVCacheConfig,
max_model_len: int, max_model_len: int,
max_num_batched_tokens: int,
use_eagle: bool, use_eagle: bool,
enable_caching: bool, enable_caching: bool,
enable_kv_cache_events: bool, enable_kv_cache_events: bool,
@@ -65,6 +66,8 @@ class KVCacheCoordinator(ABC):
self.single_type_managers = tuple( self.single_type_managers = tuple(
get_manager_for_kv_cache_spec( get_manager_for_kv_cache_spec(
kv_cache_spec=kv_cache_group.kv_cache_spec, kv_cache_spec=kv_cache_group.kv_cache_spec,
max_num_batched_tokens=max_num_batched_tokens,
max_model_len=max_model_len,
block_pool=self.block_pool, block_pool=self.block_pool,
enable_caching=enable_caching, enable_caching=enable_caching,
kv_cache_group_id=i, kv_cache_group_id=i,
@@ -271,6 +274,7 @@ class KVCacheCoordinatorNoPrefixCache(KVCacheCoordinator):
self, self,
kv_cache_config: KVCacheConfig, kv_cache_config: KVCacheConfig,
max_model_len: int, max_model_len: int,
max_num_batched_tokens: int,
use_eagle: bool, use_eagle: bool,
enable_kv_cache_events: bool, enable_kv_cache_events: bool,
dcp_world_size: int, dcp_world_size: int,
@@ -281,6 +285,7 @@ class KVCacheCoordinatorNoPrefixCache(KVCacheCoordinator):
super().__init__( super().__init__(
kv_cache_config, kv_cache_config,
max_model_len, max_model_len,
max_num_batched_tokens,
use_eagle, use_eagle,
False, False,
enable_kv_cache_events, enable_kv_cache_events,
@@ -316,6 +321,7 @@ class UnitaryKVCacheCoordinator(KVCacheCoordinator):
self, self,
kv_cache_config: KVCacheConfig, kv_cache_config: KVCacheConfig,
max_model_len: int, max_model_len: int,
max_num_batched_tokens: int,
use_eagle: bool, use_eagle: bool,
enable_caching: bool, enable_caching: bool,
enable_kv_cache_events: bool, enable_kv_cache_events: bool,
@@ -327,6 +333,7 @@ class UnitaryKVCacheCoordinator(KVCacheCoordinator):
super().__init__( super().__init__(
kv_cache_config, kv_cache_config,
max_model_len, max_model_len,
max_num_batched_tokens,
use_eagle, use_eagle,
enable_caching, enable_caching,
enable_kv_cache_events, enable_kv_cache_events,
@@ -381,6 +388,7 @@ class HybridKVCacheCoordinator(KVCacheCoordinator):
self, self,
kv_cache_config: KVCacheConfig, kv_cache_config: KVCacheConfig,
max_model_len: int, max_model_len: int,
max_num_batched_tokens: int,
use_eagle: bool, use_eagle: bool,
enable_caching: bool, enable_caching: bool,
enable_kv_cache_events: bool, enable_kv_cache_events: bool,
@@ -392,6 +400,7 @@ class HybridKVCacheCoordinator(KVCacheCoordinator):
super().__init__( super().__init__(
kv_cache_config, kv_cache_config,
max_model_len, max_model_len,
max_num_batched_tokens,
use_eagle, use_eagle,
enable_caching, enable_caching,
enable_kv_cache_events, enable_kv_cache_events,
@@ -574,6 +583,7 @@ class HybridKVCacheCoordinator(KVCacheCoordinator):
def get_kv_cache_coordinator( def get_kv_cache_coordinator(
kv_cache_config: KVCacheConfig, kv_cache_config: KVCacheConfig,
max_model_len: int, max_model_len: int,
max_num_batched_tokens: int,
use_eagle: bool, use_eagle: bool,
enable_caching: bool, enable_caching: bool,
enable_kv_cache_events: bool, enable_kv_cache_events: bool,
@@ -586,6 +596,7 @@ def get_kv_cache_coordinator(
return KVCacheCoordinatorNoPrefixCache( return KVCacheCoordinatorNoPrefixCache(
kv_cache_config, kv_cache_config,
max_model_len, max_model_len,
max_num_batched_tokens,
use_eagle, use_eagle,
enable_kv_cache_events, enable_kv_cache_events,
dcp_world_size=dcp_world_size, dcp_world_size=dcp_world_size,
@@ -597,6 +608,7 @@ def get_kv_cache_coordinator(
return UnitaryKVCacheCoordinator( return UnitaryKVCacheCoordinator(
kv_cache_config, kv_cache_config,
max_model_len, max_model_len,
max_num_batched_tokens,
use_eagle, use_eagle,
enable_caching, enable_caching,
enable_kv_cache_events, enable_kv_cache_events,
@@ -608,6 +620,7 @@ def get_kv_cache_coordinator(
return HybridKVCacheCoordinator( return HybridKVCacheCoordinator(
kv_cache_config, kv_cache_config,
max_model_len, max_model_len,
max_num_batched_tokens,
use_eagle, use_eagle,
enable_caching, enable_caching,
enable_kv_cache_events, enable_kv_cache_events,
+7
View File
@@ -109,6 +109,7 @@ class KVCacheManager:
kv_cache_config: KVCacheConfig, kv_cache_config: KVCacheConfig,
max_model_len: int, max_model_len: int,
hash_block_size: int, hash_block_size: int,
max_num_batched_tokens: int | None = None,
enable_caching: bool = True, enable_caching: bool = True,
use_eagle: bool = False, use_eagle: bool = False,
log_stats: bool = False, log_stats: bool = False,
@@ -118,6 +119,11 @@ class KVCacheManager:
metrics_collector: KVCacheMetricsCollector | None = None, metrics_collector: KVCacheMetricsCollector | None = None,
) -> None: ) -> None:
self.max_model_len = max_model_len self.max_model_len = max_model_len
# When unset, fall back to `max_model_len` so the recycling-aware cap
# collapses to the prior (uncapped) admission behavior. The scheduler
# always supplies the real value at runtime.
if max_num_batched_tokens is None:
max_num_batched_tokens = max_model_len
self.enable_caching = enable_caching self.enable_caching = enable_caching
self.use_eagle = use_eagle self.use_eagle = use_eagle
@@ -131,6 +137,7 @@ class KVCacheManager:
self.coordinator = get_kv_cache_coordinator( self.coordinator = get_kv_cache_coordinator(
kv_cache_config=kv_cache_config, kv_cache_config=kv_cache_config,
max_model_len=self.max_model_len, max_model_len=self.max_model_len,
max_num_batched_tokens=max_num_batched_tokens,
use_eagle=self.use_eagle, use_eagle=self.use_eagle,
enable_caching=self.enable_caching, enable_caching=self.enable_caching,
enable_kv_cache_events=enable_kv_cache_events, enable_kv_cache_events=enable_kv_cache_events,
+68 -53
View File
@@ -890,31 +890,48 @@ def get_max_concurrency_for_kv_cache_config(
return max_concurrency return max_concurrency
def may_override_num_blocks( def may_override_num_blocks(vllm_config: VllmConfig, num_blocks: int) -> int:
vllm_config: VllmConfig, num_blocks: int, suppress_log: bool = False
) -> int:
""" """
Override the number of kv cache blocks if `num_gpu_blocks_override` is set. Override the number of kv cache blocks if `num_gpu_blocks_override` is set.
The override is logged once, at the call site in `get_kv_cache_configs`.
""" """
if vllm_config.cache_config.num_gpu_blocks_override is not None: if vllm_config.cache_config.num_gpu_blocks_override is not None:
num_gpu_blocks_override = vllm_config.cache_config.num_gpu_blocks_override num_blocks = vllm_config.cache_config.num_gpu_blocks_override
if not suppress_log:
logger.info(
"Overriding num_gpu_blocks=%d with num_gpu_blocks_override=%d",
num_blocks,
num_gpu_blocks_override,
)
num_blocks = num_gpu_blocks_override
return num_blocks return num_blocks
def _pool_bytes_per_block(kv_cache_groups: list[KVCacheGroupSpec]) -> int:
"""
Bytes consumed by one block in the worker's shared KV cache pool, mirroring
the divisor used by `get_kv_cache_config_from_groups` to convert
`available_memory` into `num_blocks`. Used to compute the effective KV cache
capacity once `num_gpu_blocks_override` is applied.
"""
if len(kv_cache_groups) == 1 and isinstance(
kv_cache_groups[0].kv_cache_spec, UniformTypeKVCacheSpecs
):
return kv_cache_groups[0].kv_cache_spec.page_size_bytes
if all(
isinstance(g.kv_cache_spec, UniformTypeKVCacheSpecs) for g in kv_cache_groups
):
# DeepseekV4: shared layout sized by the largest per-page-size bucket.
full_mla_spec = cast(UniformTypeKVCacheSpecs, kv_cache_groups[0].kv_cache_spec)
layer_tuple_page_bytes = sum(full_mla_spec.get_page_sizes())
num_layer_tuples = max(
cast(UniformTypeKVCacheSpecs, g.kv_cache_spec).get_num_layer_tuples()
for g in kv_cache_groups
)
return layer_tuple_page_bytes * num_layer_tuples
group_size = max(len(g.layer_names) for g in kv_cache_groups)
page_size = get_uniform_page_size([g.kv_cache_spec for g in kv_cache_groups])
return page_size * group_size
def get_num_blocks( def get_num_blocks(
vllm_config: VllmConfig, vllm_config: VllmConfig,
num_layers: int, num_layers: int,
available_memory: int, available_memory: int,
page_size: int, page_size: int,
suppress_log: bool = False,
) -> int: ) -> int:
""" """
Get the number of kv cache blocks. Get the number of kv cache blocks.
@@ -924,15 +941,10 @@ def get_num_blocks(
num_layers: The number of layers num_layers: The number of layers
available_memory: Memory available for KV cache in bytes. available_memory: Memory available for KV cache in bytes.
page_size: The page size of the KV cache. page_size: The page size of the KV cache.
suppress_log: Whether to suppress override log messages. Used when creating a
temporary/dummy KV cache config, e.g. during CG memory profiling
""" """
num_blocks = int(available_memory // page_size // num_layers) num_blocks = int(available_memory // page_size // num_layers)
num_blocks = max(num_blocks, 0) num_blocks = max(num_blocks, 0)
num_blocks = may_override_num_blocks( return may_override_num_blocks(vllm_config, num_blocks)
vllm_config, num_blocks, suppress_log=suppress_log
)
return num_blocks
def get_uniform_page_size(kv_cache_specs: Iterable[KVCacheSpec]) -> int: def get_uniform_page_size(kv_cache_specs: Iterable[KVCacheSpec]) -> int:
@@ -1220,7 +1232,6 @@ def get_kv_cache_config_from_groups(
vllm_config: VllmConfig, vllm_config: VllmConfig,
kv_cache_groups: list[KVCacheGroupSpec], kv_cache_groups: list[KVCacheGroupSpec],
available_memory: int, available_memory: int,
suppress_log: bool = False,
) -> KVCacheConfig: ) -> KVCacheConfig:
""" """
Generate the KV cache configuration from the KV cache groups and spec Generate the KV cache configuration from the KV cache groups and spec
@@ -1252,9 +1263,7 @@ def get_kv_cache_config_from_groups(
num_blocks = ( num_blocks = (
available_memory // kv_cache_groups[0].kv_cache_spec.page_size_bytes available_memory // kv_cache_groups[0].kv_cache_spec.page_size_bytes
) )
num_blocks = may_override_num_blocks( num_blocks = may_override_num_blocks(vllm_config, num_blocks)
vllm_config, num_blocks, suppress_log=suppress_log
)
per_layer_specs = kv_cache_groups[0].kv_cache_spec.kv_cache_specs per_layer_specs = kv_cache_groups[0].kv_cache_spec.kv_cache_specs
kv_cache_tensors = [ kv_cache_tensors = [
KVCacheTensor( KVCacheTensor(
@@ -1288,11 +1297,7 @@ def get_kv_cache_config_from_groups(
) )
assert group_size > 0, "group_size must be greater than 0" assert group_size > 0, "group_size must be greater than 0"
num_blocks = get_num_blocks( num_blocks = get_num_blocks(
vllm_config, vllm_config, group_size, available_memory, page_size
group_size,
available_memory,
page_size,
suppress_log=suppress_log,
) )
kv_cache_tensors = [] kv_cache_tensors = []
for i in range(group_size): for i in range(group_size):
@@ -1686,36 +1691,24 @@ def _report_kv_cache_config(
vllm_config: The global VllmConfig vllm_config: The global VllmConfig
kv_cache_config: The resolved KV cache configuration kv_cache_config: The resolved KV cache configuration
""" """
min_block_size = min( max_model_len = vllm_config.model_config.max_model_len
[group.kv_cache_spec.block_size for group in kv_cache_config.kv_cache_groups]
)
# Log the KV cache size and maximum concurrency.
num_tokens = (
kv_cache_config.num_blocks
// len(kv_cache_config.kv_cache_groups)
* min_block_size
)
dcp_size = vllm_config.parallel_config.decode_context_parallel_size
pcp_size = vllm_config.parallel_config.prefill_context_parallel_size
if pcp_size * dcp_size > 1:
num_tokens *= pcp_size * dcp_size
logger.info(
"Multiplying the GPU KV cache size by the cp_world_size %d "
"(pcp_world_size %d * dcp_world_size %d).",
pcp_size * dcp_size,
pcp_size,
dcp_size,
)
num_tokens_str = f"{num_tokens:,}"
logger.info_once("GPU KV cache size: %s tokens", num_tokens_str)
max_model_len_str = f"{vllm_config.model_config.max_model_len:,}"
max_concurrency = get_max_concurrency_for_kv_cache_config( max_concurrency = get_max_concurrency_for_kv_cache_config(
vllm_config, kv_cache_config vllm_config, kv_cache_config
) )
# GPU KV cache size in tokens = max_concurrency * max_model_len: the total
# tokens of context the pool can hold at peak utilization. Sourcing this
# from the concurrency calculation handles hybrid layouts correctly: SWA /
# chunked-local groups have a per-request block count that's capped by
# their window, so a naive `num_blocks // num_groups * block_size` formula
# underestimates capacity for these models. DCP/PCP sharding is already
# accounted for in each spec's `max_memory_usage_bytes`.
num_tokens = int(max_concurrency * max_model_len)
logger.info_once("GPU KV cache size: %s tokens", f"{num_tokens:,}")
logger.info_once( logger.info_once(
"Maximum concurrency for %s tokens per request: %.2fx", "Maximum concurrency for %s tokens per request: %.2fx",
max_model_len_str, f"{max_model_len:,}",
max_concurrency, max_concurrency,
) )
@@ -1986,6 +1979,28 @@ def get_kv_cache_configs(
for worker_spec in kv_cache_specs for worker_spec in kv_cache_specs
] ]
# If `num_gpu_blocks_override` is set, the cache size that will actually
# be allocated is decoupled from the profiled `available_memory`:
# `may_override_num_blocks` in `get_kv_cache_config_from_groups` clamps
# `num_blocks` to the override. Reflect that in `available_memory` here so
# auto-fit, the admission check, and the per-worker config builder all
# plan against the same effective capacity.
override = vllm_config.cache_config.num_gpu_blocks_override
if override is not None:
adjusted_memory: list[int] = []
for groups, avail_mem in zip(projected_groups_per_worker, available_memory):
if not groups:
adjusted_memory.append(avail_mem)
continue
bytes_per_block = _pool_bytes_per_block(groups)
logger.info(
"Overriding num_gpu_blocks=%d with num_gpu_blocks_override=%d",
avail_mem // bytes_per_block,
override,
)
adjusted_memory.append(override * bytes_per_block)
available_memory = adjusted_memory
if vllm_config.model_config.original_max_model_len == -1: if vllm_config.model_config.original_max_model_len == -1:
_auto_fit_max_model_len( _auto_fit_max_model_len(
vllm_config, projected_groups_per_worker, available_memory vllm_config, projected_groups_per_worker, available_memory
+1
View File
@@ -228,6 +228,7 @@ class Scheduler(SchedulerInterface):
self.kv_cache_manager = KVCacheManager( self.kv_cache_manager = KVCacheManager(
kv_cache_config=kv_cache_config, kv_cache_config=kv_cache_config,
max_model_len=self.max_model_len, max_model_len=self.max_model_len,
max_num_batched_tokens=self.scheduler_config.max_num_batched_tokens,
enable_caching=self.cache_config.enable_prefix_caching, enable_caching=self.cache_config.enable_prefix_caching,
use_eagle=self.use_eagle, use_eagle=self.use_eagle,
log_stats=self.log_stats, log_stats=self.log_stats,
+35 -1
View File
@@ -41,6 +41,7 @@ class SingleTypeKVCacheManager(ABC):
kv_cache_group_id: int, kv_cache_group_id: int,
dcp_world_size: int = 1, dcp_world_size: int = 1,
pcp_world_size: int = 1, pcp_world_size: int = 1,
max_admission_blocks_per_request: int | None = None,
) -> None: ) -> None:
""" """
Initializes the SingleTypeKVCacheManager. Initializes the SingleTypeKVCacheManager.
@@ -48,6 +49,12 @@ class SingleTypeKVCacheManager(ABC):
kv_cache_spec: The kv_cache_spec for this manager. kv_cache_spec: The kv_cache_spec for this manager.
block_pool: The block pool. block_pool: The block pool.
kv_cache_group_id: The id of the kv cache group of this manager. kv_cache_group_id: The id of the kv cache group of this manager.
max_admission_blocks_per_request: Recycling-aware per-request
block cap used by `get_num_blocks_to_allocate`. Only set for
spec types that recycle blocks across chunks (SWA,
chunked-local); `None` (the default) means no cap, which is
correct for full-attention-style specs that hold every
block until the request finishes.
""" """
self.block_size = kv_cache_spec.block_size self.block_size = kv_cache_spec.block_size
self.dcp_world_size = dcp_world_size self.dcp_world_size = dcp_world_size
@@ -57,6 +64,7 @@ class SingleTypeKVCacheManager(ABC):
self.kv_cache_spec = kv_cache_spec self.kv_cache_spec = kv_cache_spec
self.block_pool = block_pool self.block_pool = block_pool
self.enable_caching = enable_caching self.enable_caching = enable_caching
self._max_admission_blocks_per_request = max_admission_blocks_per_request
self.new_block_ids: list[int] = [] self.new_block_ids: list[int] = []
# Mapping from request ID to blocks to track the blocks allocated # Mapping from request ID to blocks to track the blocks allocated
@@ -105,6 +113,19 @@ class SingleTypeKVCacheManager(ABC):
""" """
num_required_blocks = cdiv(num_tokens, self.block_size) num_required_blocks = cdiv(num_tokens, self.block_size)
if self._max_admission_blocks_per_request is not None:
# Recycling-aware specs (SWA, chunked-local) cap the per-request
# reservation here so admission matches the startup pool sizer
# (`SlidingWindowSpec.max_admission_blocks_per_request` / its
# chunked-local counterpart). `remove_skipped_blocks` runs from
# `allocate_slots` before each chunk's `get_num_blocks_to_allocate`,
# so per-request peak real-held blocks <= this cap, which keeps
# `sum(reservations) <= pool` <=> `sum(peak_real_held) <= pool`.
# Drift between the two would re-introduce the deadlock from
# issue #39734 or, worse, mid-prefill OOM.
num_required_blocks = min(
num_required_blocks, self._max_admission_blocks_per_request
)
num_req_blocks = len(self.req_to_blocks.get(request_id, ())) num_req_blocks = len(self.req_to_blocks.get(request_id, ()))
if request_id in self.num_cached_block: if request_id in self.num_cached_block:
@@ -1126,8 +1147,21 @@ spec_manager_map: dict[type[KVCacheSpec], type[SingleTypeKVCacheManager]] = {
def get_manager_for_kv_cache_spec( def get_manager_for_kv_cache_spec(
kv_cache_spec: KVCacheSpec, **kwargs kv_cache_spec: KVCacheSpec,
max_num_batched_tokens: int,
max_model_len: int,
**kwargs,
) -> SingleTypeKVCacheManager: ) -> SingleTypeKVCacheManager:
manager_class = spec_manager_map[type(kv_cache_spec)] manager_class = spec_manager_map[type(kv_cache_spec)]
# SlidingWindow / ChunkedLocalAttention managers recycle blocks across
# chunks; the runtime admission cap must match the recycling-aware bound
# the startup pool sizer uses (single source of truth: the spec method).
if isinstance(kv_cache_spec, (SlidingWindowSpec, ChunkedLocalAttentionSpec)):
kwargs["max_admission_blocks_per_request"] = (
kv_cache_spec.max_admission_blocks_per_request(
max_num_batched_tokens=max_num_batched_tokens,
max_model_len=max_model_len,
)
)
manager = manager_class(kv_cache_spec, **kwargs) manager = manager_class(kv_cache_spec, **kwargs)
return manager return manager
+1
View File
@@ -114,6 +114,7 @@ class EngineCoreRequest(
external_req_id: str | None = None external_req_id: str | None = None
reasoning_ended: bool | None = None reasoning_ended: bool | None = None
reasoning_parser_kwargs: dict[str, Any] | None = None
@property @property
def params(self) -> SamplingParams | PoolingParams: def params(self) -> SamplingParams | PoolingParams:
+6 -1
View File
@@ -293,6 +293,7 @@ class AsyncLLM(EngineClient):
data_parallel_rank: int | None = None, data_parallel_rank: int | None = None,
prompt_text: str | None = None, prompt_text: str | None = None,
reasoning_ended: bool | None = None, reasoning_ended: bool | None = None,
reasoning_parser_kwargs: dict[str, Any] | None = None,
) -> RequestOutputCollector: ) -> RequestOutputCollector:
"""Add new request to the AsyncLLM.""" """Add new request to the AsyncLLM."""
@@ -313,7 +314,7 @@ class AsyncLLM(EngineClient):
) )
if isinstance(prompt, AsyncGenerator): if isinstance(prompt, AsyncGenerator):
if reasoning_ended is not None: if reasoning_ended is not None or reasoning_parser_kwargs is not None:
raise NotImplementedError raise NotImplementedError
# Streaming input case. # Streaming input case.
@@ -361,6 +362,8 @@ class AsyncLLM(EngineClient):
if reasoning_ended is not None: if reasoning_ended is not None:
request.reasoning_ended = reasoning_ended request.reasoning_ended = reasoning_ended
if reasoning_parser_kwargs is not None:
request.reasoning_parser_kwargs = reasoning_parser_kwargs
self.input_processor.assign_request_id(request) self.input_processor.assign_request_id(request)
@@ -534,6 +537,7 @@ class AsyncLLM(EngineClient):
priority: int = 0, priority: int = 0,
data_parallel_rank: int | None = None, data_parallel_rank: int | None = None,
reasoning_ended: bool | None = None, reasoning_ended: bool | None = None,
reasoning_parser_kwargs: dict[str, Any] | None = None,
) -> AsyncGenerator[RequestOutput, None]: ) -> AsyncGenerator[RequestOutput, None]:
""" """
Main function called by the API server to kick off a request Main function called by the API server to kick off a request
@@ -563,6 +567,7 @@ class AsyncLLM(EngineClient):
data_parallel_rank=data_parallel_rank, data_parallel_rank=data_parallel_rank,
prompt_text=prompt_text, prompt_text=prompt_text,
reasoning_ended=reasoning_ended, reasoning_ended=reasoning_ended,
reasoning_parser_kwargs=reasoning_parser_kwargs,
) )
# The output_handler task pushes items into the queue. # The output_handler task pushes items into the queue.
+42 -21
View File
@@ -376,19 +376,28 @@ class MLAAttentionSpec(FullAttentionSpec):
class ChunkedLocalAttentionSpec(AttentionSpec): class ChunkedLocalAttentionSpec(AttentionSpec):
attention_chunk_size: int attention_chunk_size: int
def max_memory_usage_bytes(self, vllm_config: VllmConfig) -> int: def max_admission_blocks_per_request(
max_model_len = vllm_config.model_config.max_model_len self, max_num_batched_tokens: int, max_model_len: int
max_num_batched_tokens = vllm_config.scheduler_config.max_num_batched_tokens ) -> int:
"""Per-request admission cap, in blocks.
# During chunked prefill, we allocate KV cache for at most Single source of truth for both startup pool sizing
# `self.attention_chunk_size` computed tokens plus the newly scheduled (`max_memory_usage_bytes`) and the runtime admission gate, so requests
# tokens. And we won't allocate KV cache for more than `max_model_len` admitted by startup can also be admitted at runtime.
# tokens. """
# During chunked prefill, we hold KV for at most one chunk window.
num_tokens = min( num_tokens = min(
self.attention_chunk_size + max_num_batched_tokens, max_model_len self.attention_chunk_size + max_num_batched_tokens, max_model_len
) )
return cdiv(num_tokens, self.block_size)
return cdiv(num_tokens, self.block_size) * self.page_size_bytes def max_memory_usage_bytes(self, vllm_config: VllmConfig) -> int:
max_model_len = vllm_config.model_config.max_model_len
max_num_batched_tokens = vllm_config.scheduler_config.max_num_batched_tokens
max_blocks = self.max_admission_blocks_per_request(
max_num_batched_tokens=max_num_batched_tokens, max_model_len=max_model_len
)
return max_blocks * self.page_size_bytes
@dataclass(frozen=True, kw_only=True) @dataclass(frozen=True, kw_only=True)
@@ -409,26 +418,38 @@ class SlidingWindowSpec(AttentionSpec):
* get_dtype_size(self.dtype) * get_dtype_size(self.dtype)
) )
def max_admission_blocks_per_request(
self, max_num_batched_tokens: int, max_model_len: int
) -> int:
"""Per-request admission cap, in blocks.
Single source of truth for both startup pool sizing
(`max_memory_usage_bytes`) and the runtime admission gate. Per-request
real-held blocks plateau at this bound because
`SlidingWindowManager.remove_skipped_blocks` runs from `allocate_slots`
before each chunk's `get_num_blocks_to_allocate`.
"""
# During chunked prefill, we hold KV for the last `sliding_window-1`
# computed tokens plus the newly scheduled tokens, and never more
# than `max_model_len`.
num_tokens = min(
self.sliding_window - 1 + max_num_batched_tokens, max_model_len
)
# +1 because the sliding window may not start from the beginning of
# the block. E.g. block size 4 and num_token 4 needs two blocks
# [XXCD][EF] to store the 6-token window [CDEF].
return cdiv(num_tokens, self.block_size) + 1
def max_memory_usage_bytes(self, vllm_config: VllmConfig) -> int: def max_memory_usage_bytes(self, vllm_config: VllmConfig) -> int:
assert vllm_config.parallel_config.decode_context_parallel_size == 1, ( assert vllm_config.parallel_config.decode_context_parallel_size == 1, (
"DCP not support sliding window." "DCP not support sliding window."
) )
max_model_len = vllm_config.model_config.max_model_len max_model_len = vllm_config.model_config.max_model_len
max_num_batched_tokens = vllm_config.scheduler_config.max_num_batched_tokens max_num_batched_tokens = vllm_config.scheduler_config.max_num_batched_tokens
max_blocks = self.max_admission_blocks_per_request(
# During chunked prefill, we allocate KV cache for the last max_num_batched_tokens=max_num_batched_tokens, max_model_len=max_model_len
# `self.sliding_window-1` computed tokens plus the newly scheduled
# tokens. And we won't allocate KV cache for more than `max_model_len`
# tokens.
num_tokens = min(
self.sliding_window - 1 + max_num_batched_tokens, max_model_len
) )
return max_blocks * self.page_size_bytes
# +1 here because the sliding window may not start from the beginning
# of the block. For example, if the block size is 4 and num_token
# is 4, we need two blocks [XXCD] [EF] to store the sliding
# window [CDEF] of 6 tokens.
return (cdiv(num_tokens, self.block_size) + 1) * self.page_size_bytes
@dataclass(frozen=True, kw_only=True) @dataclass(frozen=True, kw_only=True)
+5
View File
@@ -74,6 +74,7 @@ class Request:
block_hasher: Callable[["Request"], list["BlockHash"]] | None = None, block_hasher: Callable[["Request"], list["BlockHash"]] | None = None,
resumable: bool = False, resumable: bool = False,
reasoning_ended: bool | None = None, reasoning_ended: bool | None = None,
reasoning_parser_kwargs: dict[str, Any] | None = None,
) -> None: ) -> None:
self.request_id = request_id self.request_id = request_id
self.client_index = client_index self.client_index = client_index
@@ -86,6 +87,9 @@ class Request:
) )
if self.structured_output_request is not None: if self.structured_output_request is not None:
self.structured_output_request.reasoning_ended = reasoning_ended self.structured_output_request.reasoning_ended = reasoning_ended
self.structured_output_request.reasoning_parser_kwargs = (
reasoning_parser_kwargs
)
self.arrival_time = arrival_time if arrival_time is not None else time.time() self.arrival_time = arrival_time if arrival_time is not None else time.time()
self.status = RequestStatus.WAITING self.status = RequestStatus.WAITING
@@ -195,6 +199,7 @@ class Request:
block_hasher=block_hasher, block_hasher=block_hasher,
resumable=request.resumable, resumable=request.resumable,
reasoning_ended=request.reasoning_ended, reasoning_ended=request.reasoning_ended,
reasoning_parser_kwargs=request.reasoning_parser_kwargs,
) )
def append_output_token_ids( def append_output_token_ids(
+3
View File
@@ -110,6 +110,9 @@ class SimpleCPUOffloadScheduler:
self.cpu_coordinator: KVCacheCoordinator = get_kv_cache_coordinator( self.cpu_coordinator: KVCacheCoordinator = get_kv_cache_coordinator(
kv_cache_config=self.cpu_kv_cache_config, kv_cache_config=self.cpu_kv_cache_config,
max_model_len=vllm_config.model_config.max_model_len, max_model_len=vllm_config.model_config.max_model_len,
max_num_batched_tokens=(
vllm_config.scheduler_config.max_num_batched_tokens
),
use_eagle=False, use_eagle=False,
enable_caching=True, enable_caching=True,
enable_kv_cache_events=self.enable_kv_cache_events, enable_kv_cache_events=self.enable_kv_cache_events,
+26 -7
View File
@@ -37,7 +37,10 @@ class StructuredOutputManager:
def __init__(self, vllm_config: VllmConfig): def __init__(self, vllm_config: VllmConfig):
self.backend: StructuredOutputBackend | None = None self.backend: StructuredOutputBackend | None = None
self.reasoner: ReasoningParser | None = None # We only store the class of the reasoner in the manager.
# The parser instance is request-scoped because some reasoning parsers
# depend on per-request chat-template kwargs.
self.reasoner_cls: type[ReasoningParser] | None = None
self.vllm_config = vllm_config self.vllm_config = vllm_config
# When in external_launcher mode, async grammar compilation causes deadlocks # When in external_launcher mode, async grammar compilation causes deadlocks
@@ -85,15 +88,29 @@ class StructuredOutputManager:
self.vllm_config.structured_outputs_config.reasoning_parser self.vllm_config.structured_outputs_config.reasoning_parser
) )
if reasoning_parser: if reasoning_parser:
reasoner_cls = ReasoningParserManager.get_reasoning_parser( self.reasoner_cls = ReasoningParserManager.get_reasoning_parser(
reasoning_parser reasoning_parser
) )
self.reasoner = reasoner_cls(tokenizer=self.tokenizer)
self.enable_in_reasoning = ( self.enable_in_reasoning = (
self.vllm_config.structured_outputs_config.enable_in_reasoning self.vllm_config.structured_outputs_config.enable_in_reasoning
) )
def _get_reasoner(self, request: "Request") -> "ReasoningParser | None":
structured_req = request.structured_output_request
if structured_req is None or self.reasoner_cls is None:
return None
if structured_req.reasoner is None:
# Lazily build the request-local parser so the structured-output
# gate observes the same template kwargs used by the frontend.
parser_kwargs = structured_req.reasoning_parser_kwargs or {}
structured_req.reasoner = self.reasoner_cls(
tokenizer=self.tokenizer,
**parser_kwargs,
)
return structured_req.reasoner
def grammar_init(self, request: "Request") -> None: def grammar_init(self, request: "Request") -> None:
if request.structured_output_request is None: if request.structured_output_request is None:
return return
@@ -285,7 +302,8 @@ class StructuredOutputManager:
# NOTE (Hanchen) if enable_in_reasoning is True, it means that # NOTE (Hanchen) if enable_in_reasoning is True, it means that
# the model needs to be constrained in reasoning. So we should always # the model needs to be constrained in reasoning. So we should always
# enable the bitmask filling. # enable the bitmask filling.
if self.reasoner is not None: reasoner = self._get_reasoner(request)
if reasoner is not None:
if self.enable_in_reasoning: if self.enable_in_reasoning:
return True return True
assert request.structured_output_request is not None assert request.structured_output_request is not None
@@ -295,7 +313,7 @@ class StructuredOutputManager:
# After unifying the `openai_gptoss` and non-`openai_gptoss` styles, # After unifying the `openai_gptoss` and non-`openai_gptoss` styles,
# it can be removed. # it can be removed.
request.structured_output_request.reasoning_ended = ( request.structured_output_request.reasoning_ended = (
self.reasoner.is_reasoning_end(request.prompt_token_ids or []) reasoner.is_reasoning_end(request.prompt_token_ids or [])
) )
return request.structured_output_request.reasoning_ended return request.structured_output_request.reasoning_ended
return True return True
@@ -311,7 +329,8 @@ class StructuredOutputManager:
assert request.structured_output_request.grammar is not None assert request.structured_output_request.grammar is not None
# by default, we should always advance # by default, we should always advance
# for cases that don't use thinking mode. # for cases that don't use thinking mode.
if self.reasoner is None: reasoner = self._get_reasoner(request)
if reasoner is None:
return True return True
# if the model needs structured in reasoning, we should advance # if the model needs structured in reasoning, we should advance
@@ -328,7 +347,7 @@ class StructuredOutputManager:
start = ( start = (
delta_from if delta_from >= 0 else max(len(all_token_ids) + delta_from, 0) delta_from if delta_from >= 0 else max(len(all_token_ids) + delta_from, 0)
) )
if self.reasoner.is_reasoning_end_streaming( if reasoner.is_reasoning_end_streaming(
all_token_ids, itertools.islice(all_token_ids, start, None) all_token_ids, itertools.islice(all_token_ids, start, None)
): ):
# Reasoning just ended, so we shouldn't advance til # Reasoning just ended, so we shouldn't advance til
+8 -1
View File
@@ -5,7 +5,7 @@ import functools
import json import json
from concurrent.futures import Future from concurrent.futures import Future
from concurrent.futures._base import TimeoutError from concurrent.futures._base import TimeoutError
from typing import cast from typing import TYPE_CHECKING, Any, cast
from vllm.sampling_params import SamplingParams, StructuredOutputsParams from vllm.sampling_params import SamplingParams, StructuredOutputsParams
from vllm.v1.structured_output.backend_types import ( from vllm.v1.structured_output.backend_types import (
@@ -14,12 +14,19 @@ from vllm.v1.structured_output.backend_types import (
StructuredOutputOptions, StructuredOutputOptions,
) )
if TYPE_CHECKING:
from vllm.reasoning import ReasoningParser
@dataclasses.dataclass @dataclasses.dataclass
class StructuredOutputRequest: class StructuredOutputRequest:
params: StructuredOutputsParams params: StructuredOutputsParams
_grammar: Future[StructuredOutputGrammar] | StructuredOutputGrammar | None = None _grammar: Future[StructuredOutputGrammar] | StructuredOutputGrammar | None = None
reasoning_ended: bool | None = None reasoning_ended: bool | None = None
reasoning_parser_kwargs: dict[str, Any] | None = None
# Cached per request; do not share reasoning parsers across requests because
# their behavior can depend on reasoning_parser_kwargs.
reasoner: "ReasoningParser | None" = None
@staticmethod @staticmethod
def from_sampling_params( def from_sampling_params(
+1 -1
View File
@@ -5874,7 +5874,7 @@ class GPUModelRunner(
saved_override = self.cache_config.num_gpu_blocks_override saved_override = self.cache_config.num_gpu_blocks_override
self.cache_config.num_gpu_blocks_override = min_blocks self.cache_config.num_gpu_blocks_override = min_blocks
minimal_config = get_kv_cache_config_from_groups( minimal_config = get_kv_cache_config_from_groups(
self.vllm_config, kv_cache_groups, available_memory=0, suppress_log=True self.vllm_config, kv_cache_groups, available_memory=0
) )
self.cache_config.num_gpu_blocks_override = saved_override self.cache_config.num_gpu_blocks_override = saved_override