Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
67c74e5980 | ||
|
|
455839ec66 | ||
|
|
513b5f82dd | ||
|
|
135722d58d | ||
|
|
7758daca7c | ||
|
|
5f79634b3b | ||
|
|
05adde9955 | ||
|
|
9cdcda5fc2 | ||
|
|
a5d4a264dd | ||
|
|
a5e0ea09ea | ||
|
|
28a1a85d89 | ||
|
|
82ccd9f3b2 | ||
|
|
d9f3961abb | ||
|
|
4fee1390f4 | ||
|
|
bf590b6a24 | ||
|
|
66df2c03f5 | ||
|
|
17896d817b | ||
|
|
1a131a066c | ||
|
|
6fffb0b8c8 | ||
|
|
a076bd1e67 | ||
|
|
c244b6fc83 | ||
|
|
15cc87d2fe | ||
|
|
c68fa39ced | ||
|
|
05ebca5250 | ||
|
|
88d34c6409 | ||
|
|
b8160878f0 | ||
|
|
84c276d7ea | ||
|
|
5eb3657578 |
+82
-25
@@ -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 {
|
||||||
|
|||||||
@@ -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
@@ -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");
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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",
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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."""
|
||||||
...
|
...
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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.")
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
+22
-9
@@ -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,)
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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":
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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,
|
||||||
|
)
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user