[New model][Multimodal] Add DeepSeek-V4-Flash-Vision-Exp support (#54566)
Signed-off-by: Isotr0py <Isotr0py@outlook.com>
This commit is contained in:
@@ -28,7 +28,9 @@ void topk_softplus_sqrt(
|
||||
const std::optional<torch::stable::Tensor>& correction_bias,
|
||||
const std::optional<torch::stable::Tensor>& input_ids,
|
||||
const std::optional<torch::stable::Tensor>& tid2eid,
|
||||
const std::optional<torch::stable::Tensor>& is_padding);
|
||||
const std::optional<torch::stable::Tensor>& is_padding,
|
||||
const std::optional<torch::stable::Tensor>& bias_vl,
|
||||
int64_t image_sentinel_lo);
|
||||
|
||||
void moe_sum(torch::stable::Tensor& input, torch::stable::Tensor& output,
|
||||
std::optional<torch::stable::Tensor> topk_ids,
|
||||
|
||||
@@ -81,19 +81,92 @@ __launch_bounds__(128) __global__
|
||||
int num_experts, float routed_scaling_factor,
|
||||
const HashIndType* input_ids,
|
||||
const HashIndType* tid2eid,
|
||||
const bool* is_padding) {
|
||||
const bool* is_padding, const float* bias_vl,
|
||||
int64_t image_sentinel_lo) {
|
||||
const int warp = (blockIdx.x * blockDim.x + threadIdx.x) / 32;
|
||||
const int lane = threadIdx.x % 32;
|
||||
if (warp >= num_rows) return;
|
||||
const int64_t token_id = load_index_as_int64(input_ids, warp);
|
||||
const bool is_pad_row = is_padding != nullptr && is_padding[warp];
|
||||
// Image tokens carry five consecutive in-vocab sentinel ids starting at
|
||||
// image_sentinel_lo (0 = disabled); they skip the tid2eid lookup and
|
||||
// select experts by score + bias_vl instead. Ids above the sentinel block
|
||||
// are regular special tokens and must not match.
|
||||
const bool is_image = bias_vl != nullptr && image_sentinel_lo > 0 &&
|
||||
token_id >= image_sentinel_lo &&
|
||||
token_id < image_sentinel_lo + 5;
|
||||
|
||||
#if defined(__CUDA_ARCH__) && (__CUDA_ARCH__ >= 900)
|
||||
cudaGridDependencySynchronize();
|
||||
#endif
|
||||
int expert = 0;
|
||||
float weight = 0.f;
|
||||
if (lane < 6 && !is_pad_row) {
|
||||
if (is_image && !is_pad_row) {
|
||||
// Top-6 by (sqrtsoftplus(x) + bias_vl) over the full row; the routing
|
||||
// weights are the raw sqrtsoftplus scores of the selected experts.
|
||||
constexpr int MAX_PER_LANE = 12; // supports num_experts <= 384
|
||||
const int per_lane = num_experts / 32;
|
||||
float vals[MAX_PER_LANE];
|
||||
float sels[MAX_PER_LANE];
|
||||
#pragma unroll
|
||||
for (int i = 0; i < MAX_PER_LANE; ++i) {
|
||||
if (i < per_lane) {
|
||||
const int e = lane + 32 * i;
|
||||
const float x = input[warp * num_experts + e];
|
||||
float v = sqrtf(fmaxf(x, 0.f) + __logf(1.f + __expf(-fabsf(x))));
|
||||
if (isnan(v)) {
|
||||
v = 0.f;
|
||||
}
|
||||
vals[i] = v;
|
||||
sels[i] = v + bias_vl[e];
|
||||
} else {
|
||||
vals[i] = 0.f;
|
||||
sels[i] = -INFINITY;
|
||||
}
|
||||
}
|
||||
int slot_expert[6];
|
||||
float slot_weight[6];
|
||||
#pragma unroll
|
||||
for (int j = 0; j < 6; ++j) {
|
||||
float best = -INFINITY;
|
||||
int best_e = num_experts;
|
||||
#pragma unroll
|
||||
for (int i = 0; i < MAX_PER_LANE; ++i) {
|
||||
if (sels[i] > best) {
|
||||
best = sels[i];
|
||||
best_e = lane + 32 * i;
|
||||
}
|
||||
}
|
||||
// Warp argmax reduce; ties go to the lower expert id.
|
||||
#pragma unroll
|
||||
for (int mask = 16; mask > 0; mask >>= 1) {
|
||||
const float other_best = VLLM_SHFL_XOR_SYNC(best, mask);
|
||||
const int other_e = VLLM_SHFL_XOR_SYNC(best_e, mask);
|
||||
if (other_best > best || (other_best == best && other_e < best_e)) {
|
||||
best = other_best;
|
||||
best_e = other_e;
|
||||
}
|
||||
}
|
||||
float w = 0.f;
|
||||
#pragma unroll
|
||||
for (int i = 0; i < MAX_PER_LANE; ++i) {
|
||||
if (lane + 32 * i == best_e) {
|
||||
w = vals[i];
|
||||
sels[i] = -INFINITY;
|
||||
}
|
||||
}
|
||||
#pragma unroll
|
||||
for (int mask = 16; mask > 0; mask >>= 1) {
|
||||
w += VLLM_SHFL_XOR_SYNC(w, mask);
|
||||
}
|
||||
slot_expert[j] = best_e;
|
||||
slot_weight[j] = w;
|
||||
}
|
||||
if (lane < 6) {
|
||||
expert = slot_expert[lane];
|
||||
weight = slot_weight[lane];
|
||||
}
|
||||
} else if (lane < 6 && !is_pad_row) {
|
||||
// only load and calculate for 6 experts
|
||||
expert = static_cast<int>(tid2eid[token_id * 6 + lane]);
|
||||
const float x = input[warp * num_experts + expert];
|
||||
@@ -127,7 +200,8 @@ void launchDsv4HashTopk(const float* input, float* output, OutIndType* indices,
|
||||
double routed_scaling_factor,
|
||||
const HashIndType* input_ids,
|
||||
const HashIndType* tid2eid, cudaStream_t stream,
|
||||
const bool* is_padding) {
|
||||
const bool* is_padding, const float* bias_vl,
|
||||
int64_t image_sentinel_lo) {
|
||||
if (num_rows == 0) return;
|
||||
auto* kernel = &dsv4HashTopkSoftplusSqrt<OutIndType, HashIndType>;
|
||||
cudaLaunchConfig_t config = {};
|
||||
@@ -141,7 +215,8 @@ void launchDsv4HashTopk(const float* input, float* output, OutIndType* indices,
|
||||
config.numAttrs = 1;
|
||||
const float scale = static_cast<float>(routed_scaling_factor);
|
||||
cudaLaunchKernelEx(&config, kernel, input, output, indices, num_rows,
|
||||
num_experts, scale, input_ids, tid2eid, is_padding);
|
||||
num_experts, scale, input_ids, tid2eid, is_padding,
|
||||
bias_vl, image_sentinel_lo);
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -174,7 +249,8 @@ __launch_bounds__(WARPS_PER_CTA* WARP_SIZE_PARAM) __global__
|
||||
const int start_expert, const int end_expert, const bool renormalize,
|
||||
double routed_scaling_factor, const float* correction_bias,
|
||||
const HashIndType* input_ids, const HashIndType* tid2eid,
|
||||
const bool* is_padding) {
|
||||
const bool* is_padding, const float* bias_vl,
|
||||
int64_t image_sentinel_lo) {
|
||||
static_assert(std::is_same_v<InputType, float> ||
|
||||
std::is_same_v<InputType, __nv_bfloat16> ||
|
||||
std::is_same_v<InputType, __half>,
|
||||
@@ -241,6 +317,19 @@ __launch_bounds__(WARPS_PER_CTA* WARP_SIZE_PARAM) __global__
|
||||
const bool row_is_active = finished ? !finished[thread_row] : true;
|
||||
const bool is_pad_row = is_padding != nullptr && is_padding[thread_row];
|
||||
|
||||
// Image tokens carry five consecutive in-vocab sentinel ids starting at
|
||||
// image_sentinel_lo (0 = disabled); they select experts with bias_vl
|
||||
// instead of correction_bias / the tid2eid hash table. Ids above the
|
||||
// sentinel block are regular special tokens and must not match.
|
||||
int64_t token_id = 0;
|
||||
if (input_ids != nullptr) {
|
||||
token_id = load_index_as_int64(input_ids, thread_row);
|
||||
}
|
||||
const bool use_vl_bias = bias_vl != nullptr && image_sentinel_lo > 0 &&
|
||||
token_id >= image_sentinel_lo &&
|
||||
token_id < image_sentinel_lo + 5;
|
||||
const float* row_bias = use_vl_bias ? bias_vl : correction_bias;
|
||||
|
||||
// We finally start setting up the read pointers for each thread. First, each
|
||||
// thread jumps to the start of the row it will read.
|
||||
const InputType* thread_row_ptr = input + thread_row * ELTS_PER_ROW;
|
||||
@@ -323,9 +412,10 @@ __launch_bounds__(WARPS_PER_CTA* WARP_SIZE_PARAM) __global__
|
||||
constexpr float threshold = 20.0f;
|
||||
constexpr float beta = 1.0f;
|
||||
|
||||
// Hash MoE path: indices are predetermined from lookup table
|
||||
if constexpr (USE_HASH) {
|
||||
const int64_t token_id = load_index_as_int64(input_ids, thread_row);
|
||||
// Hash MoE path: indices are predetermined from lookup table. Image rows
|
||||
// (sentinel ids) fall through to the score-based top-k path with bias_vl.
|
||||
const bool hash_row = USE_HASH && !use_vl_bias;
|
||||
if (hash_row) {
|
||||
const int64_t token_expert_offset = token_id * static_cast<int64_t>(k);
|
||||
if (!is_pad_row) {
|
||||
#pragma unroll
|
||||
@@ -417,13 +507,13 @@ __launch_bounds__(WARPS_PER_CTA* WARP_SIZE_PARAM) __global__
|
||||
if (isnan(val)) {
|
||||
val = 0.f;
|
||||
}
|
||||
if (correction_bias) {
|
||||
if (row_bias) {
|
||||
const int group_id = ii / ELTS_PER_LDG;
|
||||
const int local_id = ii % ELTS_PER_LDG;
|
||||
const int expert_idx = first_elt_read_by_thread +
|
||||
group_id * THREADS_PER_ROW * ELTS_PER_LDG +
|
||||
local_id;
|
||||
val = val + correction_bias[expert_idx];
|
||||
val = val + row_bias[expert_idx];
|
||||
}
|
||||
row_chunk[ii] = val;
|
||||
}
|
||||
@@ -488,8 +578,8 @@ __launch_bounds__(WARPS_PER_CTA* WARP_SIZE_PARAM) __global__
|
||||
// to global memory. (This will be a single) thread per row of the
|
||||
// input/output matrices.
|
||||
const int idx = k * thread_row + k_idx;
|
||||
if (correction_bias != nullptr && should_process_row) {
|
||||
max_val -= correction_bias[expert];
|
||||
if (row_bias != nullptr && should_process_row) {
|
||||
max_val -= row_bias[expert];
|
||||
}
|
||||
output[idx] = max_val;
|
||||
indices[idx] =
|
||||
@@ -576,7 +666,8 @@ void topkGatingSoftplusSqrtLauncherHelper(
|
||||
const int start_expert, const int end_expert, const bool renormalize,
|
||||
double routed_scaling_factor, const float* correction_bias,
|
||||
const bool use_hash, const HashIndType* input_ids,
|
||||
const HashIndType* tid2eid, cudaStream_t stream, const bool* is_padding) {
|
||||
const HashIndType* tid2eid, cudaStream_t stream, const bool* is_padding,
|
||||
const float* bias_vl, int64_t image_sentinel_lo) {
|
||||
static constexpr int BYTES_PER_LDG =
|
||||
MIN(MAX_BYTES_PER_LDG, sizeof(InputType) * EXPERTS);
|
||||
using Constants =
|
||||
@@ -605,12 +696,13 @@ void topkGatingSoftplusSqrtLauncherHelper(
|
||||
cudaLaunchKernelEx(&config, kernel, input, finished, output, num_rows,
|
||||
indices, source_row, k, start_expert, end_expert,
|
||||
renormalize, routed_scaling_factor, correction_bias,
|
||||
input_ids, tid2eid, is_padding);
|
||||
input_ids, tid2eid, is_padding, bias_vl,
|
||||
image_sentinel_lo);
|
||||
#else
|
||||
kernel<<<num_blocks, block_dim, 0, stream>>>(
|
||||
input, finished, output, num_rows, indices, source_row, k, start_expert,
|
||||
end_expert, renormalize, routed_scaling_factor, correction_bias,
|
||||
input_ids, tid2eid, is_padding);
|
||||
input_ids, tid2eid, is_padding, bias_vl, image_sentinel_lo);
|
||||
#endif
|
||||
})
|
||||
}
|
||||
@@ -624,7 +716,7 @@ void topkGatingSoftplusSqrtLauncherHelper(
|
||||
gating_output, nullptr, topk_weights, topk_indices, \
|
||||
token_expert_indices, num_tokens, topk, 0, num_experts, renormalize, \
|
||||
routed_scaling_factor, correction_bias, use_hash, input_ids, tid2eid, \
|
||||
stream, is_padding);
|
||||
stream, is_padding, bias_vl, image_sentinel_lo);
|
||||
#else
|
||||
#define LAUNCH_SOFTPLUS_SQRT(NUM_EXPERTS, WARPS_PER_TB, MAX_BYTES) \
|
||||
if (WARP_SIZE == 64) { \
|
||||
@@ -633,14 +725,14 @@ void topkGatingSoftplusSqrtLauncherHelper(
|
||||
gating_output, nullptr, topk_weights, topk_indices, \
|
||||
token_expert_indices, num_tokens, topk, 0, num_experts, renormalize, \
|
||||
routed_scaling_factor, correction_bias, use_hash, input_ids, \
|
||||
tid2eid, stream, is_padding); \
|
||||
tid2eid, stream, is_padding, bias_vl, image_sentinel_lo); \
|
||||
} else if (WARP_SIZE == 32) { \
|
||||
topkGatingSoftplusSqrtLauncherHelper<NUM_EXPERTS, WARPS_PER_TB, 32, \
|
||||
MAX_BYTES>( \
|
||||
gating_output, nullptr, topk_weights, topk_indices, \
|
||||
token_expert_indices, num_tokens, topk, 0, num_experts, renormalize, \
|
||||
routed_scaling_factor, correction_bias, use_hash, input_ids, \
|
||||
tid2eid, stream, is_padding); \
|
||||
tid2eid, stream, is_padding, bias_vl, image_sentinel_lo); \
|
||||
} else { \
|
||||
assert(false && \
|
||||
"Unsupported warp size. Only 32 and 64 are supported for ROCm"); \
|
||||
@@ -654,14 +746,16 @@ void topkGatingSoftplusSqrtKernelLauncher(
|
||||
const int topk, const bool renormalize, double routed_scaling_factor,
|
||||
const float* correction_bias, const bool use_hash,
|
||||
const HashIndType* input_ids, const HashIndType* tid2eid,
|
||||
cudaStream_t stream, const bool* is_padding) {
|
||||
cudaStream_t stream, const bool* is_padding, const float* bias_vl,
|
||||
int64_t image_sentinel_lo) {
|
||||
#ifndef USE_ROCM
|
||||
if constexpr (std::is_same_v<InputType, float>) {
|
||||
if (use_hash && topk == 6 && renormalize &&
|
||||
(num_experts == 256 || num_experts == 384)) {
|
||||
launchDsv4HashTopk<IndType, HashIndType>(
|
||||
gating_output, topk_weights, topk_indices, num_tokens, num_experts,
|
||||
routed_scaling_factor, input_ids, tid2eid, stream, is_padding);
|
||||
routed_scaling_factor, input_ids, tid2eid, stream, is_padding,
|
||||
bias_vl, image_sentinel_lo);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -761,12 +855,30 @@ void dispatch_topk_softplus_sqrt_launch(
|
||||
const std::optional<torch::stable::Tensor>& correction_bias,
|
||||
const std::optional<torch::stable::Tensor>& input_ids,
|
||||
const std::optional<torch::stable::Tensor>& tid2eid, cudaStream_t stream,
|
||||
const std::optional<torch::stable::Tensor>& is_padding) {
|
||||
const std::optional<torch::stable::Tensor>& is_padding,
|
||||
const std::optional<torch::stable::Tensor>& bias_vl,
|
||||
int64_t image_sentinel_lo) {
|
||||
const float* bias_ptr = nullptr;
|
||||
if (correction_bias.has_value()) {
|
||||
bias_ptr = correction_bias.value().const_data_ptr<float>();
|
||||
}
|
||||
|
||||
const float* bias_vl_ptr = nullptr;
|
||||
if (bias_vl.has_value()) {
|
||||
const torch::stable::Tensor& bias_vl_tensor = bias_vl.value();
|
||||
STD_TORCH_CHECK(input_ids.has_value(),
|
||||
"input_ids is required when bias_vl is set");
|
||||
STD_TORCH_CHECK(
|
||||
bias_vl_tensor.scalar_type() == torch::headeronly::ScalarType::Float,
|
||||
"bias_vl tensor must be float32");
|
||||
STD_TORCH_CHECK(bias_vl_tensor.dim() == 1, "bias_vl tensor must be 1D");
|
||||
STD_TORCH_CHECK(bias_vl_tensor.size(0) == num_experts,
|
||||
"bias_vl size mismatch, expected: ", num_experts);
|
||||
STD_TORCH_CHECK(bias_vl_tensor.is_contiguous(),
|
||||
"bias_vl tensor must be contiguous");
|
||||
bias_vl_ptr = bias_vl_tensor.const_data_ptr<float>();
|
||||
}
|
||||
|
||||
auto launch = [&](auto* topk_indices_ptr) {
|
||||
using OutIndType =
|
||||
typename std::remove_pointer<decltype(topk_indices_ptr)>::type;
|
||||
@@ -800,7 +912,8 @@ void dispatch_topk_softplus_sqrt_launch(
|
||||
topk_indices_ptr, token_expert_indices.mutable_data_ptr<int>(),
|
||||
num_tokens, num_experts, topk, renormalize, routed_scaling_factor,
|
||||
bias_ptr, true, input_ids.value().const_data_ptr<int64_t>(),
|
||||
tid2eid.value().const_data_ptr<int64_t>(), stream, is_padding_ptr);
|
||||
tid2eid.value().const_data_ptr<int64_t>(), stream, is_padding_ptr,
|
||||
bias_vl_ptr, image_sentinel_lo);
|
||||
} else {
|
||||
STD_TORCH_CHECK(tid2eid.value().scalar_type() ==
|
||||
torch::headeronly::ScalarType::Int);
|
||||
@@ -810,7 +923,35 @@ void dispatch_topk_softplus_sqrt_launch(
|
||||
topk_indices_ptr, token_expert_indices.mutable_data_ptr<int>(),
|
||||
num_tokens, num_experts, topk, renormalize, routed_scaling_factor,
|
||||
bias_ptr, true, input_ids.value().const_data_ptr<int>(),
|
||||
tid2eid.value().const_data_ptr<int>(), stream, is_padding_ptr);
|
||||
tid2eid.value().const_data_ptr<int>(), stream, is_padding_ptr,
|
||||
bias_vl_ptr, image_sentinel_lo);
|
||||
}
|
||||
} else if (bias_vl_ptr != nullptr) {
|
||||
// Non-hash rows with bias_vl: image tokens (five sentinel ids starting
|
||||
// at image_sentinel_lo) select experts by score + bias_vl. input_ids
|
||||
// drives the per-row branch, so
|
||||
// dispatch the hash-index type on its dtype.
|
||||
if (input_ids.value().scalar_type() ==
|
||||
torch::headeronly::ScalarType::Long) {
|
||||
vllm::moe::topkGatingSoftplusSqrtKernelLauncher<OutIndType, ComputeType,
|
||||
int64_t>(
|
||||
gating_output, topk_weights.mutable_data_ptr<float>(),
|
||||
topk_indices_ptr, token_expert_indices.mutable_data_ptr<int>(),
|
||||
num_tokens, num_experts, topk, renormalize, routed_scaling_factor,
|
||||
bias_ptr, false, input_ids.value().const_data_ptr<int64_t>(),
|
||||
static_cast<const int64_t*>(nullptr), stream, is_padding_ptr,
|
||||
bias_vl_ptr, image_sentinel_lo);
|
||||
} else {
|
||||
STD_TORCH_CHECK(input_ids.value().scalar_type() ==
|
||||
torch::headeronly::ScalarType::Int);
|
||||
vllm::moe::topkGatingSoftplusSqrtKernelLauncher<OutIndType, ComputeType,
|
||||
int>(
|
||||
gating_output, topk_weights.mutable_data_ptr<float>(),
|
||||
topk_indices_ptr, token_expert_indices.mutable_data_ptr<int>(),
|
||||
num_tokens, num_experts, topk, renormalize, routed_scaling_factor,
|
||||
bias_ptr, false, input_ids.value().const_data_ptr<int>(),
|
||||
static_cast<const int*>(nullptr), stream, is_padding_ptr,
|
||||
bias_vl_ptr, image_sentinel_lo);
|
||||
}
|
||||
} else {
|
||||
vllm::moe::topkGatingSoftplusSqrtKernelLauncher<OutIndType, ComputeType>(
|
||||
@@ -818,7 +959,8 @@ void dispatch_topk_softplus_sqrt_launch(
|
||||
topk_indices_ptr, token_expert_indices.mutable_data_ptr<int>(),
|
||||
num_tokens, num_experts, topk, renormalize, routed_scaling_factor,
|
||||
bias_ptr, false, static_cast<const OutIndType*>(nullptr),
|
||||
static_cast<const OutIndType*>(nullptr), stream, is_padding_ptr);
|
||||
static_cast<const OutIndType*>(nullptr), stream, is_padding_ptr,
|
||||
nullptr, 0);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -843,7 +985,9 @@ void topk_softplus_sqrt(
|
||||
const std::optional<torch::stable::Tensor>& correction_bias,
|
||||
const std::optional<torch::stable::Tensor>& input_ids,
|
||||
const std::optional<torch::stable::Tensor>& tid2eid,
|
||||
const std::optional<torch::stable::Tensor>& is_padding) {
|
||||
const std::optional<torch::stable::Tensor>& is_padding,
|
||||
const std::optional<torch::stable::Tensor>& bias_vl,
|
||||
int64_t image_sentinel_lo) {
|
||||
const int num_experts = gating_output.size(-1);
|
||||
const auto num_tokens = gating_output.numel() / num_experts;
|
||||
const int topk = topk_weights.size(-1);
|
||||
@@ -857,21 +1001,21 @@ void topk_softplus_sqrt(
|
||||
gating_output.const_data_ptr<float>(), topk_weights, topk_indices,
|
||||
token_expert_indices, num_tokens, num_experts, topk, renormalize,
|
||||
routed_scaling_factor, correction_bias, input_ids, tid2eid, stream,
|
||||
is_padding);
|
||||
is_padding, bias_vl, image_sentinel_lo);
|
||||
} else if (gating_output.scalar_type() ==
|
||||
torch::headeronly::ScalarType::Half) {
|
||||
dispatch_topk_softplus_sqrt_launch<__half>(
|
||||
reinterpret_cast<const __half*>(gating_output.const_data_ptr()),
|
||||
topk_weights, topk_indices, token_expert_indices, num_tokens,
|
||||
num_experts, topk, renormalize, routed_scaling_factor, correction_bias,
|
||||
input_ids, tid2eid, stream, is_padding);
|
||||
input_ids, tid2eid, stream, is_padding, bias_vl, image_sentinel_lo);
|
||||
} else if (gating_output.scalar_type() ==
|
||||
torch::headeronly::ScalarType::BFloat16) {
|
||||
dispatch_topk_softplus_sqrt_launch<__nv_bfloat16>(
|
||||
reinterpret_cast<const __nv_bfloat16*>(gating_output.const_data_ptr()),
|
||||
topk_weights, topk_indices, token_expert_indices, num_tokens,
|
||||
num_experts, topk, renormalize, routed_scaling_factor, correction_bias,
|
||||
input_ids, tid2eid, stream, is_padding);
|
||||
input_ids, tid2eid, stream, is_padding, bias_vl, image_sentinel_lo);
|
||||
} else {
|
||||
STD_TORCH_CHECK(false, "Unsupported gating_output data type: ",
|
||||
gating_output.scalar_type());
|
||||
|
||||
@@ -20,7 +20,8 @@ STABLE_TORCH_LIBRARY_FRAGMENT(_moe_C, m) {
|
||||
"topk_softplus_sqrt(Tensor! topk_weights, Tensor! topk_indices, Tensor! "
|
||||
"token_expert_indices, Tensor gating_output, bool renormalize, float "
|
||||
"routed_scaling_factor, Tensor? "
|
||||
"bias, Tensor? input_ids, Tensor? tid2eid, Tensor? is_padding) -> ()");
|
||||
"bias, Tensor? input_ids, Tensor? tid2eid, Tensor? is_padding, "
|
||||
"Tensor? bias_vl=None, int image_sentinel_lo=0) -> ()");
|
||||
|
||||
// Calculate the result of moe by summing up the partial results
|
||||
// from all selected experts. topk_ids/expert_map are optional and, when
|
||||
|
||||
@@ -528,6 +528,7 @@ These models primarily accept the [`LLM.generate`](./generative_models.md#llmgen
|
||||
| `DeepseekVLV2ForCausalLM` | DeepSeek-VL2 | T + I<sup>+</sup> | `deepseek-ai/deepseek-vl2-tiny`, `deepseek-ai/deepseek-vl2-small`, `deepseek-ai/deepseek-vl2`, etc. | | ✅︎ |
|
||||
| `DeepseekOCRForCausalLM` | DeepSeek-OCR | T + I<sup>+</sup> | `deepseek-ai/DeepSeek-OCR`, etc. | ✅︎ | ✅︎ |
|
||||
| `DeepseekOCR2ForCausalLM` | DeepSeek-OCR-2 | T + I<sup>+</sup> | `deepseek-ai/DeepSeek-OCR-2`, etc. | ✅︎ | ✅︎ |
|
||||
| `DeepseekV4ForConditionalGeneration` | DeepSeek-V4 (vision) | T + I<sup>+</sup> | `deepseek-ai/DeepSeek-V4-Flash-Vision-Exp` | | ✅︎ |
|
||||
| `Eagle2_5_VLForConditionalGeneration` | Eagle2.5-VL | T + I<sup>E+</sup> | `nvidia/Eagle2.5-8B`, etc. | ✅︎ | ✅︎ |
|
||||
| `Ernie4_5_VLMoeForConditionalGeneration` | Ernie4.5-VL | T + I<sup>+</sup>/ V<sup>+</sup> | `baidu/ERNIE-4.5-VL-28B-A3B-PT`, `baidu/ERNIE-4.5-VL-424B-A47B-PT` | | ✅︎ |
|
||||
| `Exaone4_5_ForConditionalGeneration` | EXAONE-4.5 | T + I<sup>E+</sup> | `LGAI-EXAONE/EXAONE-4.5-33B`, etc. | ✅︎ | ✅︎ |
|
||||
|
||||
@@ -465,3 +465,34 @@ def test_draft_model_arch_config(
|
||||
_assert_model_config_methods(
|
||||
model_config, expected, check_head_size=check_head_size
|
||||
)
|
||||
|
||||
|
||||
def test_deepseek_v4_convertor_splits_vision_architecture():
|
||||
"""The vision checkpoint shares model_type/architectures with the
|
||||
text-only DeepSeek-V4; the convertor routes it to the VL wrapper class
|
||||
by rewriting hf_config.architectures (model-class resolution reads the
|
||||
raw attribute)."""
|
||||
from vllm.transformers_utils.configs.deepseek_v4 import DeepseekV4Config
|
||||
from vllm.transformers_utils.model_arch_config_convertor import (
|
||||
DeepseekV4ModelArchConfigConvertor,
|
||||
)
|
||||
|
||||
text_cfg = DeepseekV4Config(architectures=["DeepseekV4ForCausalLM"])
|
||||
conv = DeepseekV4ModelArchConfigConvertor(text_cfg, text_cfg)
|
||||
assert conv.get_architectures() == ["DeepseekV4ForCausalLM"]
|
||||
assert text_cfg.architectures == ["DeepseekV4ForCausalLM"]
|
||||
|
||||
vl_cfg = DeepseekV4Config(
|
||||
architectures=["DeepseekV4ForCausalLM"], vision_n_layers=32
|
||||
)
|
||||
conv = DeepseekV4ModelArchConfigConvertor(vl_cfg, vl_cfg)
|
||||
assert conv.get_architectures() == ["DeepseekV4ForConditionalGeneration"]
|
||||
assert vl_cfg.architectures == ["DeepseekV4ForConditionalGeneration"]
|
||||
|
||||
# Speculative-draft configs (set by SpeculativeConfig.hf_config_override)
|
||||
# keep their own architecture even with a vision tower in the config.
|
||||
for draft_arch in ("DSparkDraftModel", "DeepSeekV4MTPModel"):
|
||||
draft_cfg = DeepseekV4Config(architectures=[draft_arch], vision_n_layers=32)
|
||||
conv = DeepseekV4ModelArchConfigConvertor(draft_cfg, draft_cfg)
|
||||
assert conv.get_architectures() == [draft_arch]
|
||||
assert draft_cfg.architectures == [draft_arch]
|
||||
|
||||
@@ -26,18 +26,46 @@ def _torch_topk_softplus_sqrt(
|
||||
e_score_correction_bias: torch.Tensor | None = None,
|
||||
input_ids: torch.Tensor | None = None,
|
||||
hash_indices_table: torch.Tensor | None = None,
|
||||
bias_vl: torch.Tensor | None = None,
|
||||
image_sentinel_lo: int = 0,
|
||||
):
|
||||
scores = F.softplus(gating_output.float()).sqrt()
|
||||
original_scores = scores
|
||||
if e_score_correction_bias is not None:
|
||||
scores_for_choice = scores + e_score_correction_bias.unsqueeze(0)
|
||||
else:
|
||||
scores_for_choice = scores
|
||||
|
||||
if hash_indices_table is not None:
|
||||
assert input_ids is not None
|
||||
topk_ids = hash_indices_table[input_ids.long()]
|
||||
if bias_vl is None or image_sentinel_lo == 0:
|
||||
topk_ids = hash_indices_table[input_ids.long()]
|
||||
else:
|
||||
image_mask = (input_ids.long() >= image_sentinel_lo) & (
|
||||
input_ids.long() < image_sentinel_lo + 5
|
||||
)
|
||||
topk_ids = hash_indices_table[input_ids.long()]
|
||||
vl_ids = (scores + bias_vl).topk(topk, dim=-1).indices
|
||||
topk_ids = torch.where(
|
||||
image_mask.unsqueeze(-1), vl_ids.to(topk_ids.dtype), topk_ids
|
||||
)
|
||||
else:
|
||||
if bias_vl is not None and image_sentinel_lo > 0:
|
||||
# Image tokens carry five consecutive in-vocab sentinel ids
|
||||
# starting at image_sentinel_lo and select experts with bias_vl
|
||||
# instead of the regular bias.
|
||||
assert input_ids is not None
|
||||
assert e_score_correction_bias is not None
|
||||
image_mask = (
|
||||
(input_ids.long() >= image_sentinel_lo)
|
||||
& (input_ids.long() < image_sentinel_lo + 5)
|
||||
).unsqueeze(-1)
|
||||
row_bias = torch.where(
|
||||
image_mask,
|
||||
bias_vl.unsqueeze(0),
|
||||
e_score_correction_bias.unsqueeze(0),
|
||||
)
|
||||
scores_for_choice = scores + row_bias
|
||||
elif e_score_correction_bias is not None:
|
||||
scores_for_choice = scores + e_score_correction_bias.unsqueeze(0)
|
||||
else:
|
||||
scores_for_choice = scores
|
||||
# Match the fused kernel's deterministic tie-break: lower expert ids
|
||||
# win when scores are equal. torch.topk does not guarantee which tied
|
||||
# index it selects at the k-th boundary.
|
||||
@@ -368,3 +396,227 @@ def test_fused_topk_softplus_sqrt_padding(
|
||||
sorted_w_ref = topk_weights_ref[rows_to_compare].gather(1, idx_ref)
|
||||
sorted_w = topk_weights[rows_to_compare].gather(1, idx_ops)
|
||||
torch.testing.assert_close(sorted_w_ref, sorted_w, atol=2e-2, rtol=1e-2)
|
||||
|
||||
|
||||
def _make_mixed_input_ids(
|
||||
num_tokens: int, image_sentinel_lo: int, dtype: torch.dtype = torch.long
|
||||
) -> torch.Tensor:
|
||||
"""Mix of text, image-sentinel, and above-sentinel ids.
|
||||
|
||||
Every third token is an image sentinel id (cycling through the five slots
|
||||
[image_sentinel_lo, image_sentinel_lo + 5)); every sixth token carries an
|
||||
id just above the sentinel block, simulating the named special tokens
|
||||
that follow it — those must route as regular text tokens.
|
||||
"""
|
||||
input_ids = torch.randint(
|
||||
0, image_sentinel_lo, (num_tokens,), dtype=torch.long, device="cuda"
|
||||
)
|
||||
pos = torch.arange(num_tokens, device="cuda")
|
||||
image_rows = pos % 3 == 0
|
||||
input_ids[image_rows] = image_sentinel_lo + (pos[image_rows] // 3) % 5
|
||||
above_rows = pos % 6 == 3
|
||||
input_ids[above_rows] = image_sentinel_lo + 5
|
||||
return input_ids.to(dtype)
|
||||
|
||||
|
||||
def _assert_topk_matches(
|
||||
topk_weights: torch.Tensor,
|
||||
topk_ids: torch.Tensor,
|
||||
topk_weights_ref: torch.Tensor,
|
||||
topk_ids_ref: torch.Tensor,
|
||||
) -> None:
|
||||
# Different kernels may return the topk experts in different orders when
|
||||
# scores tie; sort by expert id before comparing.
|
||||
sorted_ref_ids, idx_ref = topk_ids_ref.sort(dim=-1)
|
||||
sorted_ids, idx_ops = topk_ids.sort(dim=-1)
|
||||
torch.testing.assert_close(
|
||||
sorted_ref_ids, sorted_ids.to(sorted_ref_ids.dtype), atol=0, rtol=0
|
||||
)
|
||||
|
||||
sorted_w_ref = topk_weights_ref.gather(1, idx_ref)
|
||||
sorted_w = topk_weights.gather(1, idx_ops.to(torch.long))
|
||||
torch.testing.assert_close(sorted_w_ref, sorted_w, atol=2e-2, rtol=1e-2)
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not current_platform.is_cuda_alike(),
|
||||
reason="This test is skipped on non-CUDA platform.",
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("num_experts", "topk", "renormalize", "dtype"),
|
||||
[
|
||||
(256, 6, True, torch.float32), # Triton dsv4 fast path
|
||||
(256, 8, True, torch.bfloat16), # CUDA topk kernel
|
||||
(384, 6, False, torch.half),
|
||||
(512, 16, True, torch.float32),
|
||||
],
|
||||
)
|
||||
def test_fused_topk_softplus_sqrt_bias_vl(
|
||||
num_experts: int,
|
||||
topk: int,
|
||||
renormalize: bool,
|
||||
dtype: torch.dtype,
|
||||
):
|
||||
"""Image sentinel tokens must select experts with bias_vl.
|
||||
|
||||
Rows with ids just above the sentinel block (image_sentinel_lo + 5)
|
||||
simulate the named special tokens that follow it and must route through
|
||||
the regular bias path; the reference comparison covers them.
|
||||
"""
|
||||
torch.manual_seed(0)
|
||||
num_tokens = 64
|
||||
hidden_size = 1024
|
||||
vocab_size = 128
|
||||
image_sentinel_lo = vocab_size - 8
|
||||
hidden_states = torch.randn((num_tokens, hidden_size), dtype=dtype, device="cuda")
|
||||
gating_output = torch.randn((num_tokens, num_experts), dtype=dtype, device="cuda")
|
||||
e_score_correction_bias = torch.randn(
|
||||
(num_experts,), dtype=torch.float32, device="cuda"
|
||||
)
|
||||
bias_vl = torch.randn((num_experts,), dtype=torch.float32, device="cuda")
|
||||
input_ids = _make_mixed_input_ids(num_tokens, image_sentinel_lo)
|
||||
assert (input_ids == image_sentinel_lo + 5).any()
|
||||
|
||||
topk_weights_ref, topk_ids_ref = _torch_topk_softplus_sqrt(
|
||||
gating_output=gating_output,
|
||||
topk=topk,
|
||||
renormalize=renormalize,
|
||||
routed_scaling_factor=1.5,
|
||||
e_score_correction_bias=e_score_correction_bias,
|
||||
input_ids=input_ids,
|
||||
bias_vl=bias_vl,
|
||||
image_sentinel_lo=image_sentinel_lo,
|
||||
)
|
||||
|
||||
topk_weights, topk_ids = fused_topk_bias(
|
||||
hidden_states=hidden_states,
|
||||
gating_output=gating_output,
|
||||
scoring_func="sqrtsoftplus",
|
||||
e_score_correction_bias=e_score_correction_bias,
|
||||
topk=topk,
|
||||
renormalize=renormalize,
|
||||
routed_scaling_factor=1.5,
|
||||
input_tokens=input_ids,
|
||||
bias_vl=bias_vl,
|
||||
image_sentinel_lo=image_sentinel_lo,
|
||||
)
|
||||
|
||||
_assert_topk_matches(topk_weights, topk_ids, topk_weights_ref, topk_ids_ref)
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not current_platform.is_cuda_alike(),
|
||||
reason="This test is skipped on non-CUDA platform.",
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("num_experts", "topk", "renormalize", "dtype", "indices_dtype"),
|
||||
[
|
||||
(256, 6, True, torch.float32, torch.long), # specialized hash kernel
|
||||
(384, 6, True, torch.float32, torch.int32), # specialized hash kernel
|
||||
(384, 8, False, torch.bfloat16, torch.long), # generic hash kernel
|
||||
(512, 6, True, torch.float32, torch.long), # generic hash kernel
|
||||
],
|
||||
)
|
||||
def test_fused_topk_softplus_sqrt_hash_bias_vl(
|
||||
num_experts: int,
|
||||
topk: int,
|
||||
renormalize: bool,
|
||||
dtype: torch.dtype,
|
||||
indices_dtype: torch.dtype,
|
||||
):
|
||||
"""Hash MoE: text rows use tid2eid, image rows use topk(score + bias_vl).
|
||||
|
||||
Sentinel ids are in-vocab; rows with ids above the sentinel block
|
||||
(image_sentinel_lo + 5) must still come straight from the hash table.
|
||||
"""
|
||||
torch.manual_seed(0)
|
||||
num_tokens = 64
|
||||
hidden_size = 1024
|
||||
vocab_size = 64
|
||||
image_sentinel_lo = vocab_size - 8
|
||||
hidden_states = torch.randn((num_tokens, hidden_size), dtype=dtype, device="cuda")
|
||||
gating_output = torch.randn((num_tokens, num_experts), dtype=dtype, device="cuda")
|
||||
hash_indices_table = torch.stack(
|
||||
[torch.randperm(num_experts)[:topk] for _ in range(vocab_size)]
|
||||
).to(device="cuda", dtype=indices_dtype)
|
||||
bias_vl = torch.randn((num_experts,), dtype=torch.float32, device="cuda")
|
||||
input_ids = _make_mixed_input_ids(num_tokens, image_sentinel_lo)
|
||||
image_rows = (input_ids >= image_sentinel_lo) & (input_ids < image_sentinel_lo + 5)
|
||||
assert image_rows.any() and (~image_rows).any()
|
||||
assert (input_ids == image_sentinel_lo + 5).any()
|
||||
|
||||
topk_weights_ref, topk_ids_ref = _torch_topk_softplus_sqrt(
|
||||
gating_output=gating_output,
|
||||
topk=topk,
|
||||
renormalize=renormalize,
|
||||
routed_scaling_factor=2.5,
|
||||
input_ids=input_ids,
|
||||
hash_indices_table=hash_indices_table,
|
||||
bias_vl=bias_vl,
|
||||
image_sentinel_lo=image_sentinel_lo,
|
||||
)
|
||||
|
||||
topk_weights, topk_ids = fused_topk_bias(
|
||||
hidden_states=hidden_states,
|
||||
gating_output=gating_output,
|
||||
scoring_func="sqrtsoftplus",
|
||||
e_score_correction_bias=None,
|
||||
topk=topk,
|
||||
renormalize=renormalize,
|
||||
input_tokens=input_ids,
|
||||
hash_indices_table=hash_indices_table,
|
||||
routed_scaling_factor=2.5,
|
||||
bias_vl=bias_vl,
|
||||
image_sentinel_lo=image_sentinel_lo,
|
||||
)
|
||||
|
||||
_assert_topk_matches(topk_weights, topk_ids, topk_weights_ref, topk_ids_ref)
|
||||
|
||||
# Text rows must come straight from the hash table, image rows from
|
||||
# topk(score + bias_vl).
|
||||
text_ids = hash_indices_table[input_ids[~image_rows].long()]
|
||||
torch.testing.assert_close(
|
||||
topk_ids[~image_rows].to(text_ids.dtype), text_ids, atol=0, rtol=0
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not current_platform.is_cuda(),
|
||||
reason="The DeepSeek V4 fast path is CUDA-only.",
|
||||
)
|
||||
def test_dsv4_fast_topk_bias_vl():
|
||||
torch.manual_seed(0)
|
||||
num_tokens = 33
|
||||
num_experts = 256
|
||||
vocab_size = 128
|
||||
image_sentinel_lo = vocab_size - 8
|
||||
gating_output = torch.randn(
|
||||
(num_tokens, num_experts), dtype=torch.float32, device="cuda"
|
||||
)
|
||||
correction_bias = torch.randn(num_experts, dtype=torch.float32, device="cuda")
|
||||
bias_vl = torch.randn(num_experts, dtype=torch.float32, device="cuda")
|
||||
input_ids = _make_mixed_input_ids(num_tokens, image_sentinel_lo)
|
||||
|
||||
topk_weights_ref, topk_ids_ref = _torch_topk_softplus_sqrt(
|
||||
gating_output=gating_output,
|
||||
topk=6,
|
||||
renormalize=True,
|
||||
routed_scaling_factor=1.5,
|
||||
e_score_correction_bias=correction_bias,
|
||||
input_ids=input_ids,
|
||||
bias_vl=bias_vl,
|
||||
image_sentinel_lo=image_sentinel_lo,
|
||||
)
|
||||
topk_weights, topk_ids = dsv4_topk(
|
||||
gating_output,
|
||||
correction_bias,
|
||||
torch.int64,
|
||||
1.5,
|
||||
input_ids=input_ids,
|
||||
bias_vl=bias_vl,
|
||||
image_sentinel_lo=image_sentinel_lo,
|
||||
)
|
||||
|
||||
assert topk_ids.dtype == torch.int64
|
||||
torch.testing.assert_close(topk_ids_ref.to(torch.int64), topk_ids, atol=0, rtol=0)
|
||||
torch.testing.assert_close(topk_weights_ref, topk_weights, atol=2e-5, rtol=2e-5)
|
||||
|
||||
@@ -164,6 +164,11 @@ def test_model_tensor_schema(model_id: str):
|
||||
"Kimi-K2.5's offline inference has issues about vision chunks. Fix later."
|
||||
)
|
||||
|
||||
if model_id == "deepseek-ai/DeepSeek-V4-Flash-Vision-Exp" and not (
|
||||
current_platform.is_cuda()
|
||||
):
|
||||
pytest.skip("Deepseek V4 is only supported on CUDA")
|
||||
|
||||
model_info = HF_EXAMPLE_MODELS.find_hf_info(model_id)
|
||||
model_info.check_available_online(on_fail="skip")
|
||||
model_info.check_transformers_version(
|
||||
|
||||
@@ -833,6 +833,9 @@ _MULTIMODAL_EXAMPLE_MODELS = {
|
||||
"DeepseekOCR2ForCausalLM": _HfExamplesInfo(
|
||||
"deepseek-ai/DeepSeek-OCR-2",
|
||||
),
|
||||
"DeepseekV4ForConditionalGeneration": _HfExamplesInfo(
|
||||
"deepseek-ai/DeepSeek-V4-Flash-Vision-Exp",
|
||||
),
|
||||
"Dots3NoteForCausalLM": _HfExamplesInfo(
|
||||
"dots-studio/dots3-note-prev",
|
||||
is_available_online=False,
|
||||
|
||||
@@ -132,6 +132,12 @@ def can_initialize(
|
||||
f"capability {capability.major}.{capability.minor}"
|
||||
)
|
||||
|
||||
if model_arch == "DeepseekV4ForConditionalGeneration":
|
||||
from vllm.platforms import current_platform
|
||||
|
||||
if not current_platform.is_cuda():
|
||||
pytest.skip("Deepseek V4 is only supported on CUDA")
|
||||
|
||||
with (
|
||||
patch.object(V1EngineCore, "_initialize_kv_caches", _initialize_kv_caches_v1),
|
||||
monkeypatch.context() as m,
|
||||
|
||||
@@ -64,6 +64,11 @@ def test_registry_imports(model_arch):
|
||||
):
|
||||
pytest.skip("HY V4 is only supported on CUDA")
|
||||
|
||||
if model_arch == "DeepseekV4ForConditionalGeneration" and (
|
||||
current_platform.is_cuda()
|
||||
):
|
||||
pytest.skip("Deepseek V4 is only supported on CUDA")
|
||||
|
||||
# Ensure all model classes can be imported successfully
|
||||
model_cls = ModelRegistry._try_load_model_cls(model_arch)
|
||||
assert model_cls is not None
|
||||
|
||||
@@ -6,6 +6,7 @@ from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from vllm.entrypoints.chat_utils import parse_chat_messages
|
||||
from vllm.renderers.registry import RENDERER_REGISTRY
|
||||
@@ -427,3 +428,60 @@ def test_deepseek_v4_encode_messages_rejects_invalid_arguments(kwargs):
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
encode_messages([{"role": "user", "content": "Hello"}], **kwargs)
|
||||
|
||||
|
||||
def test_deepseek_v4_image_blocks_become_placeholders():
|
||||
prompt = _tokenizer().apply_chat_template(
|
||||
[
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"type": "text", "text": "first:"},
|
||||
{"type": "image_url", "image_url": {"url": "file:///a.png"}},
|
||||
{"type": "text", "text": "second:"},
|
||||
{"type": "image", "source": {"data": "AAAA"}},
|
||||
],
|
||||
}
|
||||
],
|
||||
tokenize=False,
|
||||
thinking=False,
|
||||
)
|
||||
|
||||
assert "<|User|>first:<|deepseek_image|>second:<|deepseek_image|>" in prompt
|
||||
|
||||
|
||||
def test_deepseek_v4_image_sentinel_ids_match_tokenizer():
|
||||
"""The borrowed sentinel ids must line up with the reserved
|
||||
``<|place_holder_mm_span_XXXX|>`` tokens in the tokenizer."""
|
||||
from vllm.models.deepseek_v4.common.mm_preprocess import (
|
||||
IMAGE_SENTINEL_BASE_ID,
|
||||
IMAGE_SENTINEL_TOKEN_NAMES,
|
||||
image_sentinel_mask,
|
||||
validate_image_sentinel_ids,
|
||||
)
|
||||
|
||||
class FakeTokenizer:
|
||||
def __init__(self, offset: int = 0) -> None:
|
||||
self.offset = offset
|
||||
|
||||
def convert_tokens_to_ids(self, token: str) -> int:
|
||||
return (
|
||||
IMAGE_SENTINEL_BASE_ID
|
||||
+ self.offset
|
||||
+ (IMAGE_SENTINEL_TOKEN_NAMES.index(token))
|
||||
)
|
||||
|
||||
validate_image_sentinel_ids(FakeTokenizer()) # no raise
|
||||
with pytest.raises(ValueError, match="sentinel"):
|
||||
validate_image_sentinel_ids(FakeTokenizer(offset=1))
|
||||
|
||||
ids = torch.tensor(
|
||||
[
|
||||
1,
|
||||
IMAGE_SENTINEL_BASE_ID,
|
||||
IMAGE_SENTINEL_BASE_ID + 4,
|
||||
IMAGE_SENTINEL_BASE_ID + 5,
|
||||
129264,
|
||||
]
|
||||
)
|
||||
assert image_sentinel_mask(ids).tolist() == [False, True, True, False, False]
|
||||
|
||||
@@ -0,0 +1,580 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""Tests for DeepSeek-V4 in-image bidirectional SWA visibility (vision variant).
|
||||
|
||||
The reference semantics are a torch port of the official
|
||||
``get_image_visible`` / ``get_window_topk_idxs_visible``: inside an image span
|
||||
[span_start, span_end] (inclusive), a token at ``pos`` sees
|
||||
``min(pos - span_start, max_image_tokens - 1)`` extra tokens to the left and
|
||||
``min(span_end - pos, max_image_tokens)`` to the right; the window then starts
|
||||
at ``max(pos - (window - 1) - max(left - (window - 1), 0), 0)`` and ends at
|
||||
``pos + right`` (inclusive).
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
from tests.v1.attention.utils import create_vllm_config
|
||||
from vllm.models.deepseek_v4.common.ops.cache_utils import (
|
||||
build_flashinfer_mixed_sparse_indices,
|
||||
combine_topk_swa_indices,
|
||||
)
|
||||
from vllm.v1.attention.backend import CommonAttentionMetadata
|
||||
from vllm.v1.attention.backends.mla.sparse_swa import (
|
||||
DeepseekSparseSWAMetadataBuilder,
|
||||
_compute_image_visibility_kernel,
|
||||
_compute_swa_indices_and_lens_kernel,
|
||||
)
|
||||
from vllm.v1.kv_cache_interface import SlidingWindowMLASpec
|
||||
|
||||
WINDOW = 8
|
||||
MAX_IMG = 6
|
||||
WIDTH = WINDOW + MAX_IMG
|
||||
BLOCK_SIZE = 64
|
||||
|
||||
|
||||
def ref_left_right(
|
||||
seq_lens: list[int],
|
||||
query_lens: list[int],
|
||||
spans_per_req: list[list[tuple[int, int]]],
|
||||
max_image_tokens: int,
|
||||
) -> tuple[list[int], list[int]]:
|
||||
"""Per-token (left, right) for the flattened decode-first token stream."""
|
||||
lefts: list[int] = []
|
||||
rights: list[int] = []
|
||||
for seq_len, query_len, spans in zip(seq_lens, query_lens, spans_per_req):
|
||||
prefix_len = seq_len - query_len
|
||||
for i in range(query_len):
|
||||
pos = prefix_len + i
|
||||
left = right = 0
|
||||
for span_start, span_end in spans:
|
||||
if span_start <= pos <= span_end:
|
||||
left = min(pos - span_start, max_image_tokens - 1)
|
||||
right = min(span_end - pos, max_image_tokens)
|
||||
lefts.append(left)
|
||||
rights.append(right)
|
||||
return lefts, rights
|
||||
|
||||
|
||||
def ref_swa_bounds(pos: int, window: int, left: int, right: int) -> tuple[int, int]:
|
||||
"""Reference [start, end) window bounds for one query token."""
|
||||
left_add = max(left - (window - 1), 0)
|
||||
start = max(pos - (window - 1) - left_add, 0)
|
||||
return start, pos + right + 1
|
||||
|
||||
|
||||
def make_batch(
|
||||
seq_lens: list[int],
|
||||
query_lens: list[int],
|
||||
device: torch.device,
|
||||
):
|
||||
"""Build the kernel inputs for a decode-first batch."""
|
||||
num_reqs = len(seq_lens)
|
||||
query_start_loc = torch.zeros(num_reqs + 1, dtype=torch.int32, device=device)
|
||||
query_start_loc[1:] = torch.tensor(
|
||||
query_lens, dtype=torch.int32, device=device
|
||||
).cumsum(0)
|
||||
num_tokens = int(query_start_loc[-1])
|
||||
seq_lens_t = torch.tensor(seq_lens, dtype=torch.int32, device=device)
|
||||
token_to_req = torch.repeat_interleave(
|
||||
torch.arange(num_reqs, dtype=torch.int32, device=device),
|
||||
torch.tensor(query_lens, dtype=torch.int32, device=device),
|
||||
)
|
||||
max_blocks = (max(seq_lens) + BLOCK_SIZE - 1) // BLOCK_SIZE
|
||||
block_table = torch.arange(
|
||||
num_reqs * max_blocks, dtype=torch.int32, device=device
|
||||
).view(num_reqs, max_blocks)
|
||||
slot_mapping = torch.arange(num_tokens, dtype=torch.int64, device=device)
|
||||
return query_start_loc, seq_lens_t, token_to_req, slot_mapping, block_table
|
||||
|
||||
|
||||
def ref_swa_slot_rows(
|
||||
seq_lens: list[int],
|
||||
query_lens: list[int],
|
||||
spans_per_req: list[list[tuple[int, int]]],
|
||||
block_table: torch.Tensor,
|
||||
window: int,
|
||||
max_image_tokens: int,
|
||||
width: int,
|
||||
) -> tuple[list[list[int]], list[int]]:
|
||||
"""Reference paged slot-id rows and lens for every token in the batch."""
|
||||
block_table_cpu = block_table.cpu()
|
||||
lefts, rights = ref_left_right(
|
||||
seq_lens, query_lens, spans_per_req, max_image_tokens
|
||||
)
|
||||
rows: list[list[int]] = []
|
||||
lens: list[int] = []
|
||||
token = 0
|
||||
for req, (seq_len, query_len) in enumerate(zip(seq_lens, query_lens)):
|
||||
prefix_len = seq_len - query_len
|
||||
for i in range(query_len):
|
||||
pos = prefix_len + i
|
||||
start, end = ref_swa_bounds(pos, window, lefts[token], rights[token])
|
||||
row = []
|
||||
for p in range(start, end):
|
||||
blk = int(block_table_cpu[req, p // BLOCK_SIZE])
|
||||
row.append(blk * BLOCK_SIZE + p % BLOCK_SIZE)
|
||||
lens.append(len(row))
|
||||
row.extend([-1] * (width - len(row)))
|
||||
rows.append(row)
|
||||
token += 1
|
||||
return rows, lens
|
||||
|
||||
|
||||
def run_swa_kernel(
|
||||
seq_lens: list[int],
|
||||
query_lens: list[int],
|
||||
spans_per_req: list[list[tuple[int, int]]],
|
||||
window: int = WINDOW,
|
||||
max_image_tokens: int = MAX_IMG,
|
||||
with_image: bool = True,
|
||||
) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
device = torch.device("cuda")
|
||||
query_start_loc, seq_lens_t, token_to_req, slot_mapping, block_table = make_batch(
|
||||
seq_lens, query_lens, device
|
||||
)
|
||||
num_tokens = int(query_start_loc[-1])
|
||||
width = window + (max_image_tokens if with_image else 0)
|
||||
swa_indices = torch.zeros(num_tokens, 1, width, dtype=torch.int32, device=device)
|
||||
swa_lens = torch.zeros(num_tokens, dtype=torch.int32, device=device)
|
||||
is_valid = slot_mapping >= 0
|
||||
|
||||
if with_image:
|
||||
lefts, rights = ref_left_right(
|
||||
seq_lens, query_lens, spans_per_req, max_image_tokens
|
||||
)
|
||||
left_t = torch.tensor(lefts, dtype=torch.int32, device=device)
|
||||
right_t = torch.tensor(rights, dtype=torch.int32, device=device)
|
||||
else:
|
||||
left_t = right_t = swa_lens # unused dummies
|
||||
|
||||
_compute_swa_indices_and_lens_kernel[(num_tokens,)](
|
||||
swa_indices,
|
||||
swa_indices.stride(0),
|
||||
swa_lens,
|
||||
window,
|
||||
width,
|
||||
left_t,
|
||||
right_t,
|
||||
query_start_loc,
|
||||
seq_lens_t,
|
||||
token_to_req,
|
||||
is_valid,
|
||||
block_table,
|
||||
block_table.stride(0),
|
||||
BLOCK_SIZE,
|
||||
token_offset=0,
|
||||
HAS_IMAGE=with_image,
|
||||
TRITON_BLOCK_SIZE=1024,
|
||||
)
|
||||
return swa_indices[:, 0], swa_lens
|
||||
|
||||
|
||||
# seq_lens, query_lens, spans per request (positions are prompt-absolute,
|
||||
# inclusive on both ends, matching the reference's sentinel-bracketed spans).
|
||||
class _Case(TypedDict):
|
||||
seq_lens: list[int]
|
||||
query_lens: list[int]
|
||||
spans: list[list[tuple[int, int]]]
|
||||
|
||||
|
||||
CASES: list[_Case] = [
|
||||
# two image spans in one request, one pure-text request
|
||||
{
|
||||
"seq_lens": [30, 12],
|
||||
"query_lens": [30, 12],
|
||||
"spans": [[(4, 12), (20, 24)], []],
|
||||
},
|
||||
# span larger than max_image_tokens, straddling the window boundary
|
||||
{
|
||||
"seq_lens": [26],
|
||||
"query_lens": [26],
|
||||
"spans": [[(2, 17)]],
|
||||
},
|
||||
# chunked prefill: span fully inside the second chunk's query
|
||||
{
|
||||
"seq_lens": [40, 9],
|
||||
"query_lens": [16, 9],
|
||||
"spans": [[(30, 38)], []],
|
||||
},
|
||||
# span ending exactly at the prompt end; tiny request
|
||||
{
|
||||
"seq_lens": [7, 15],
|
||||
"query_lens": [7, 15],
|
||||
"spans": [[(0, 6)], [(10, 14)]],
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.skipif(not torch.cuda.is_available(), reason="requires CUDA")
|
||||
@pytest.mark.parametrize("case", CASES)
|
||||
def test_swa_indices_kernel_with_image_spans(case):
|
||||
rows, lens = ref_swa_slot_rows(
|
||||
case["seq_lens"],
|
||||
case["query_lens"],
|
||||
case["spans"],
|
||||
make_batch(case["seq_lens"], case["query_lens"], torch.device("cuda"))[4],
|
||||
WINDOW,
|
||||
MAX_IMG,
|
||||
WIDTH,
|
||||
)
|
||||
indices, actual_lens = run_swa_kernel(
|
||||
case["seq_lens"], case["query_lens"], case["spans"], with_image=True
|
||||
)
|
||||
assert actual_lens.cpu().tolist() == lens
|
||||
assert indices.cpu().tolist() == rows
|
||||
|
||||
|
||||
@pytest.mark.skipif(not torch.cuda.is_available(), reason="requires CUDA")
|
||||
@pytest.mark.parametrize("case", CASES)
|
||||
def test_swa_indices_kernel_without_image_unchanged(case):
|
||||
"""HAS_IMAGE=False must reproduce the plain causal sliding window."""
|
||||
rows, lens = ref_swa_slot_rows(
|
||||
case["seq_lens"],
|
||||
case["query_lens"],
|
||||
[[] for _ in case["seq_lens"]],
|
||||
make_batch(case["seq_lens"], case["query_lens"], torch.device("cuda"))[4],
|
||||
WINDOW,
|
||||
MAX_IMG,
|
||||
WINDOW,
|
||||
)
|
||||
indices, actual_lens = run_swa_kernel(
|
||||
case["seq_lens"], case["query_lens"], case["spans"], with_image=False
|
||||
)
|
||||
assert indices.shape[-1] == WINDOW
|
||||
assert actual_lens.cpu().tolist() == lens
|
||||
assert indices.cpu().tolist() == rows
|
||||
|
||||
|
||||
@pytest.mark.skipif(not torch.cuda.is_available(), reason="requires CUDA")
|
||||
def test_image_visibility_kernel():
|
||||
"""The builder's visibility kernel must match get_image_visible."""
|
||||
device = torch.device("cuda")
|
||||
seq_lens = [30, 12]
|
||||
query_lens = [30, 12]
|
||||
spans = [[(4, 12), (20, 24)], []]
|
||||
query_start_loc, seq_lens_t, token_to_req, _, _ = make_batch(
|
||||
seq_lens, query_lens, device
|
||||
)
|
||||
num_tokens = int(query_start_loc[-1])
|
||||
|
||||
# CSR span layout: request -> contiguous [start, end) span list.
|
||||
indptr = [0, 2, 2]
|
||||
starts = [4, 20]
|
||||
ends = [12, 24]
|
||||
left = torch.zeros(num_tokens, dtype=torch.int32, device=device)
|
||||
right = torch.zeros(num_tokens, dtype=torch.int32, device=device)
|
||||
_compute_image_visibility_kernel[(num_tokens,)](
|
||||
left,
|
||||
right,
|
||||
torch.tensor(indptr, dtype=torch.int32, device=device),
|
||||
torch.tensor(starts, dtype=torch.int32, device=device),
|
||||
torch.tensor(ends, dtype=torch.int32, device=device),
|
||||
query_start_loc,
|
||||
seq_lens_t,
|
||||
token_to_req,
|
||||
MAX_IMG,
|
||||
token_offset=0,
|
||||
)
|
||||
ref_left, ref_right = ref_left_right(seq_lens, query_lens, spans, MAX_IMG)
|
||||
assert left.cpu().tolist() == ref_left
|
||||
assert right.cpu().tolist() == ref_right
|
||||
|
||||
|
||||
def combine_case(
|
||||
compress_ratio: int,
|
||||
topk: int,
|
||||
seq_lens: list[int],
|
||||
query_lens: list[int],
|
||||
spans: list[list[tuple[int, int]]],
|
||||
with_image: bool,
|
||||
):
|
||||
"""Run combine_topk_swa_indices and return (indices, lens, expected)."""
|
||||
device = torch.device("cuda")
|
||||
num_reqs = len(seq_lens)
|
||||
query_start_loc = torch.zeros(num_reqs + 1, dtype=torch.int32, device=device)
|
||||
query_start_loc[1:] = torch.tensor(
|
||||
query_lens, dtype=torch.int32, device=device
|
||||
).cumsum(0)
|
||||
num_tokens = int(query_start_loc[-1])
|
||||
seq_lens_t = torch.tensor(seq_lens, dtype=torch.int32, device=device)
|
||||
gather_lens = torch.tensor(
|
||||
[q + min(s - q, WINDOW - 1) for s, q in zip(seq_lens, query_lens)],
|
||||
dtype=torch.int32,
|
||||
device=device,
|
||||
)
|
||||
N = (max(seq_lens) + compress_ratio - 1) // compress_ratio
|
||||
M = N + int(gather_lens.max()) + 8
|
||||
gen = torch.Generator(device="cpu").manual_seed(0)
|
||||
topk_indices = torch.randint(
|
||||
0, 4096, (num_tokens, max(topk, 1)), generator=gen, dtype=torch.int32
|
||||
).to(device)
|
||||
topk_indices = topk_indices[:, : max(topk, 1)]
|
||||
|
||||
if with_image:
|
||||
lefts, rights = ref_left_right(seq_lens, query_lens, spans, MAX_IMG)
|
||||
left_t = torch.tensor(lefts, dtype=torch.int32, device=device)
|
||||
right_t = torch.tensor(rights, dtype=torch.int32, device=device)
|
||||
else:
|
||||
left_t = right_t = None
|
||||
|
||||
combined_indices, combined_lens = combine_topk_swa_indices(
|
||||
topk_indices,
|
||||
query_start_loc,
|
||||
seq_lens_t,
|
||||
gather_lens,
|
||||
WINDOW,
|
||||
compress_ratio,
|
||||
topk,
|
||||
M,
|
||||
N,
|
||||
left_visible=left_t,
|
||||
right_visible=right_t,
|
||||
max_image_tokens=MAX_IMG,
|
||||
)
|
||||
|
||||
# Reference rows.
|
||||
lefts, rights = ref_left_right(
|
||||
seq_lens,
|
||||
query_lens,
|
||||
spans if with_image else [[] for _ in seq_lens],
|
||||
MAX_IMG,
|
||||
)
|
||||
topk_cpu = topk_indices.cpu()
|
||||
width = WINDOW + MAX_IMG
|
||||
combined_topk = (topk + width + 127) // 128 * 128
|
||||
rows = []
|
||||
lens = []
|
||||
token = 0
|
||||
for b, (seq_len, query_len) in enumerate(zip(seq_lens, query_lens)):
|
||||
prefix_len = seq_len - query_len
|
||||
gather_start = seq_len - int(gather_lens[b])
|
||||
for i in range(query_len):
|
||||
pos = prefix_len + i
|
||||
topk_len = min((pos + 1) // compress_ratio, topk)
|
||||
start, end = ref_swa_bounds(pos, WINDOW, lefts[token], rights[token])
|
||||
swa_len = end - start
|
||||
row = [-1] * combined_topk
|
||||
for j in range(topk_len):
|
||||
row[j] = int(topk_cpu[token, j]) + M * b
|
||||
for j in range(swa_len):
|
||||
row[topk_len + j] = M * b + N + start + j - gather_start
|
||||
rows.append(row)
|
||||
lens.append(topk_len + swa_len)
|
||||
token += 1
|
||||
return combined_indices, combined_lens, rows, lens
|
||||
|
||||
|
||||
COMBINE_CASES = [
|
||||
dict(compress_ratio=1, topk=0), # SWA-only layer
|
||||
dict(compress_ratio=4, topk=16), # C4A layer
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.skipif(not torch.cuda.is_available(), reason="requires CUDA")
|
||||
@pytest.mark.parametrize("cfg", COMBINE_CASES)
|
||||
def test_combine_topk_swa_with_image_spans(cfg):
|
||||
case = CASES[0]
|
||||
indices, lens, rows, exp_lens = combine_case(
|
||||
cfg["compress_ratio"],
|
||||
cfg["topk"],
|
||||
case["seq_lens"],
|
||||
case["query_lens"],
|
||||
case["spans"],
|
||||
with_image=True,
|
||||
)
|
||||
assert lens.cpu().tolist() == exp_lens
|
||||
assert indices.cpu().tolist() == rows
|
||||
|
||||
|
||||
@pytest.mark.skipif(not torch.cuda.is_available(), reason="requires CUDA")
|
||||
@pytest.mark.parametrize("cfg", COMBINE_CASES)
|
||||
def test_combine_topk_swa_without_image_unchanged(cfg):
|
||||
"""left_visible=None must reproduce the plain causal combined indices."""
|
||||
case = CASES[0]
|
||||
indices, lens, rows, exp_lens = combine_case(
|
||||
cfg["compress_ratio"],
|
||||
cfg["topk"],
|
||||
case["seq_lens"],
|
||||
case["query_lens"],
|
||||
case["spans"],
|
||||
with_image=False,
|
||||
)
|
||||
assert lens.cpu().tolist() == exp_lens
|
||||
assert indices.cpu().tolist() == rows
|
||||
|
||||
|
||||
@pytest.mark.skipif(not torch.cuda.is_available(), reason="requires CUDA")
|
||||
def test_flashinfer_mixed_sparse_indices_with_image_spans():
|
||||
device = torch.device("cuda")
|
||||
# 1 decode token (req 0) + two prefill requests (reqs 1, 2).
|
||||
seq_lens = [20, 30, 9]
|
||||
query_lens = [1, 30, 9]
|
||||
spans = [[], [(4, 12)], [(0, 6)]]
|
||||
query_start_loc = torch.tensor([0, 1, 31, 40], dtype=torch.int32, device=device)
|
||||
seq_lens_t = torch.tensor(seq_lens, dtype=torch.int32, device=device)
|
||||
token_to_req = torch.tensor(
|
||||
[0] + [1] * 30 + [2] * 9, dtype=torch.int32, device=device
|
||||
)
|
||||
block_table = torch.arange(3, dtype=torch.int32, device=device).view(3, 1)
|
||||
decode_swa = torch.tensor(
|
||||
[[100, 101, 102, 103, 104, 105, 106, 107]], dtype=torch.int32, device=device
|
||||
)
|
||||
prefill_topk = torch.zeros(39, 0, dtype=torch.int32, device=device)
|
||||
|
||||
lefts, rights = ref_left_right(seq_lens, query_lens, spans, MAX_IMG)
|
||||
left_t = torch.tensor(lefts, dtype=torch.int32, device=device)
|
||||
right_t = torch.tensor(rights, dtype=torch.int32, device=device)
|
||||
|
||||
sparse_indices, sparse_lens = build_flashinfer_mixed_sparse_indices(
|
||||
decode_swa_indices=decode_swa,
|
||||
decode_compressed_indices=None,
|
||||
decode_compressed_topk_lens=None,
|
||||
prefill_topk_indices=prefill_topk,
|
||||
query_start_loc=query_start_loc,
|
||||
seq_lens=seq_lens_t,
|
||||
token_to_req_indices=token_to_req,
|
||||
swa_block_table=block_table,
|
||||
swa_block_size=BLOCK_SIZE,
|
||||
compressed_block_table=None,
|
||||
compressed_block_size=BLOCK_SIZE,
|
||||
window_size=WINDOW,
|
||||
compress_ratio=1,
|
||||
topk=0,
|
||||
prefill_left_visible=left_t,
|
||||
prefill_right_visible=right_t,
|
||||
max_image_tokens=MAX_IMG,
|
||||
)
|
||||
|
||||
swa_total = WINDOW + MAX_IMG
|
||||
assert sparse_indices.shape == (40, swa_total)
|
||||
assert sparse_lens.cpu().tolist() == [swa_total] * 40
|
||||
# Decode row: slots copied, image-extension columns padded with -1.
|
||||
decode_row = sparse_indices[0].cpu().tolist()
|
||||
assert decode_row[:WINDOW] == list(range(100, 108))
|
||||
assert decode_row[WINDOW:] == [-1] * MAX_IMG
|
||||
# Prefill rows: paged slot ids over the widened window.
|
||||
exp_rows, _ = ref_swa_slot_rows(
|
||||
seq_lens, query_lens, spans, block_table, WINDOW, MAX_IMG, swa_total
|
||||
)
|
||||
actual = sparse_indices[1:].cpu().tolist()
|
||||
assert actual == exp_rows[1:]
|
||||
|
||||
|
||||
def make_builder(vision: bool) -> DeepseekSparseSWAMetadataBuilder:
|
||||
overrides: dict = {"sliding_window": WINDOW}
|
||||
if vision:
|
||||
overrides.update(vision_n_layers=2, vision_max_n_token=MAX_IMG)
|
||||
vllm_config = create_vllm_config(
|
||||
max_model_len=4096,
|
||||
max_num_batched_tokens=64,
|
||||
max_num_seqs=8,
|
||||
hf_config_override=overrides,
|
||||
)
|
||||
spec = SlidingWindowMLASpec(
|
||||
block_size=BLOCK_SIZE,
|
||||
num_kv_heads=1,
|
||||
head_size=512,
|
||||
dtype=torch.bfloat16,
|
||||
sliding_window=WINDOW,
|
||||
cache_dtype_str="auto",
|
||||
model_version="deepseek_v4",
|
||||
)
|
||||
return DeepseekSparseSWAMetadataBuilder(
|
||||
kv_cache_spec=spec,
|
||||
layer_names=["layer0"],
|
||||
vllm_config=vllm_config,
|
||||
device=torch.device("cuda"),
|
||||
)
|
||||
|
||||
|
||||
def build_metadata(
|
||||
builder: DeepseekSparseSWAMetadataBuilder,
|
||||
seq_lens: list[int],
|
||||
query_lens: list[int],
|
||||
mm_req_doc_ranges: dict[int, list[tuple[int, int]]] | None,
|
||||
):
|
||||
device = torch.device("cuda")
|
||||
query_start_loc, seq_lens_t, _, slot_mapping, block_table = make_batch(
|
||||
seq_lens, query_lens, device
|
||||
)
|
||||
return builder.build(
|
||||
0,
|
||||
CommonAttentionMetadata(
|
||||
query_start_loc=query_start_loc,
|
||||
query_start_loc_cpu=query_start_loc.cpu(),
|
||||
seq_lens=seq_lens_t,
|
||||
seq_lens_cpu_upper_bound=seq_lens_t.cpu(),
|
||||
num_reqs=len(seq_lens),
|
||||
num_actual_tokens=int(query_start_loc[-1]),
|
||||
max_query_len=max(query_lens),
|
||||
max_seq_len=max(seq_lens),
|
||||
block_table_tensor=block_table,
|
||||
slot_mapping=slot_mapping,
|
||||
causal=True,
|
||||
mm_req_doc_ranges=mm_req_doc_ranges,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skipif(not torch.cuda.is_available(), reason="requires CUDA")
|
||||
def test_builder_in_image_visibility():
|
||||
seq_lens = [30, 12]
|
||||
query_lens = [30, 12]
|
||||
spans = [[(4, 12), (20, 24)], []]
|
||||
builder = make_builder(vision=True)
|
||||
md = build_metadata(builder, seq_lens, query_lens, {0: spans[0], 1: spans[1]})
|
||||
assert md.num_prefills == 2
|
||||
assert md.num_decode_tokens == 0
|
||||
assert md.prefill_swa_indices.shape[-1] == WIDTH
|
||||
assert md.prefill_left_visible is not None
|
||||
assert md.prefill_right_visible is not None
|
||||
|
||||
ref_left, ref_right = ref_left_right(seq_lens, query_lens, spans, MAX_IMG)
|
||||
assert md.prefill_left_visible.cpu().tolist() == ref_left
|
||||
assert md.prefill_right_visible.cpu().tolist() == ref_right
|
||||
|
||||
_, _, _, _, block_table = make_batch(seq_lens, query_lens, torch.device("cuda"))
|
||||
rows, lens = ref_swa_slot_rows(
|
||||
seq_lens, query_lens, spans, block_table, WINDOW, MAX_IMG, WIDTH
|
||||
)
|
||||
assert md.prefill_swa_lens.cpu().tolist() == lens
|
||||
assert md.prefill_swa_indices[:, 0].cpu().tolist() == rows
|
||||
|
||||
|
||||
@pytest.mark.skipif(not torch.cuda.is_available(), reason="requires CUDA")
|
||||
def test_builder_no_image_spans_fast_path():
|
||||
"""Vision model, image-free batch: no visibility tensors, plain window."""
|
||||
seq_lens = [30, 12]
|
||||
query_lens = [30, 12]
|
||||
builder = make_builder(vision=True)
|
||||
md = build_metadata(builder, seq_lens, query_lens, {0: [], 1: []})
|
||||
assert md.prefill_left_visible is None
|
||||
assert md.prefill_right_visible is None
|
||||
|
||||
_, _, _, _, block_table = make_batch(seq_lens, query_lens, torch.device("cuda"))
|
||||
rows, lens = ref_swa_slot_rows(
|
||||
seq_lens, query_lens, [[], []], block_table, WINDOW, MAX_IMG, WIDTH
|
||||
)
|
||||
assert md.prefill_swa_lens.cpu().tolist() == lens
|
||||
assert md.prefill_swa_indices[:, 0].cpu().tolist() == rows
|
||||
|
||||
|
||||
@pytest.mark.skipif(not torch.cuda.is_available(), reason="requires CUDA")
|
||||
def test_builder_text_model_unchanged():
|
||||
"""Text-only model: buffers stay window-sized and spans are ignored."""
|
||||
seq_lens = [30, 12]
|
||||
query_lens = [30, 12]
|
||||
builder = make_builder(vision=False)
|
||||
assert builder.max_image_tokens == 0
|
||||
md = build_metadata(builder, seq_lens, query_lens, None)
|
||||
assert md.prefill_swa_indices.shape[-1] == WINDOW
|
||||
assert md.prefill_left_visible is None
|
||||
|
||||
_, _, _, _, block_table = make_batch(seq_lens, query_lens, torch.device("cuda"))
|
||||
rows, lens = ref_swa_slot_rows(
|
||||
seq_lens, query_lens, [[], []], block_table, WINDOW, MAX_IMG, WINDOW
|
||||
)
|
||||
assert md.prefill_swa_lens.cpu().tolist() == lens
|
||||
assert md.prefill_swa_indices[:, 0].cpu().tolist() == rows
|
||||
@@ -2501,6 +2501,8 @@ def topk_hash_softplus_sqrt(
|
||||
input_tokens: torch.Tensor | None = None,
|
||||
hash_indices_table: torch.Tensor | None = None,
|
||||
is_padding: torch.Tensor | None = None,
|
||||
bias_vl: torch.Tensor | None = None,
|
||||
image_sentinel_lo: int = 0,
|
||||
) -> None:
|
||||
torch.ops._moe_C.topk_softplus_sqrt(
|
||||
topk_weights,
|
||||
@@ -2513,6 +2515,8 @@ def topk_hash_softplus_sqrt(
|
||||
input_tokens,
|
||||
hash_indices_table,
|
||||
is_padding,
|
||||
bias_vl,
|
||||
image_sentinel_lo,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -697,7 +697,10 @@ class ModelConfig:
|
||||
self.tokenizer_mode = "kimi_k3"
|
||||
elif arch == "DeepseekV32ForCausalLM":
|
||||
self.tokenizer_mode = "deepseek_v32"
|
||||
elif arch == "DeepseekV4ForCausalLM":
|
||||
elif arch in (
|
||||
"DeepseekV4ForCausalLM",
|
||||
"DeepseekV4ForConditionalGeneration",
|
||||
):
|
||||
self.tokenizer_mode = "deepseek_v4"
|
||||
elif arch in ("InklingForCausalLM", "InklingForConditionalGeneration"):
|
||||
self.tokenizer_mode = "inkling"
|
||||
|
||||
@@ -77,6 +77,7 @@ DEFAULT_BREAKABLE_CUDAGRAPH_ARCHITECTURES = frozenset(
|
||||
"DeepseekV32MTPModel",
|
||||
"DeepseekV32ForCausalLM",
|
||||
"DeepseekV4ForCausalLM",
|
||||
"DeepseekV4ForConditionalGeneration",
|
||||
"DeepSeekV4MTPModel",
|
||||
"Dots3NoteForCausalLM",
|
||||
"Dots3NoteMTPModel",
|
||||
|
||||
@@ -131,6 +131,8 @@ def FusedMoEFactory(
|
||||
apply_routed_scale_to_output: bool = False,
|
||||
zero_expert_type: str | None = None,
|
||||
hash_indices_table: torch.Tensor | None = None,
|
||||
bias_vl: torch.Tensor | None = None,
|
||||
image_sentinel_lo: int = 0,
|
||||
runner_cls: type[MoERunner] | None = None,
|
||||
runner_args: dict[str, Any] | None = None,
|
||||
routed_experts_cls: type[RoutedExperts] | None = None,
|
||||
@@ -199,6 +201,9 @@ def FusedMoEFactory(
|
||||
output instead of topk_weights
|
||||
zero_expert_type: Type of zero expert handling
|
||||
hash_indices_table: Hash table for expert indices
|
||||
bias_vl: Vision routing bias for image tokens (Deepseek V4)
|
||||
image_sentinel_lo: First of five consecutive in-vocab image sentinel
|
||||
ids (0 = vision routing disabled)
|
||||
runner_cls: Custom MoERunner class (None = use default MoERunner)
|
||||
runner_args: Additional arguments for runner constructor
|
||||
routed_experts_cls: Custom RoutedExperts class (None = use default)
|
||||
@@ -313,6 +318,8 @@ def FusedMoEFactory(
|
||||
zero_expert_type=zero_expert_type,
|
||||
num_logical_experts=logical_num_experts,
|
||||
hash_indices_table=hash_indices_table,
|
||||
bias_vl=bias_vl,
|
||||
image_sentinel_lo=image_sentinel_lo,
|
||||
)
|
||||
|
||||
if params_dtype is None:
|
||||
|
||||
@@ -44,8 +44,12 @@ if current_platform.is_cuda():
|
||||
topk_weights_ptr,
|
||||
topk_ids_ptr,
|
||||
routed_scaling_factor,
|
||||
input_ids_ptr,
|
||||
bias_vl_ptr,
|
||||
image_sentinel_lo,
|
||||
NUM_EXPERTS: tl.constexpr,
|
||||
BLOCK_N: tl.constexpr,
|
||||
HAS_VL: tl.constexpr,
|
||||
launch_pdl: tl.constexpr,
|
||||
):
|
||||
row = tl.program_id(0)
|
||||
@@ -54,6 +58,19 @@ if current_platform.is_cuda():
|
||||
bias = tl.load(
|
||||
correction_bias_ptr + expert_offsets, mask=expert_mask, other=0.0
|
||||
).to(tl.float32)
|
||||
if HAS_VL:
|
||||
# Image tokens carry five consecutive in-vocab sentinel ids
|
||||
# starting at image_sentinel_lo and use bias_vl for expert
|
||||
# selection instead of the regular correction bias. Ids above the
|
||||
# sentinel block are regular special tokens and must not match.
|
||||
token_id = tl.load(input_ids_ptr + row).to(tl.int64)
|
||||
bias_vl = tl.load(
|
||||
bias_vl_ptr + expert_offsets, mask=expert_mask, other=0.0
|
||||
).to(tl.float32)
|
||||
is_image = (token_id >= image_sentinel_lo) & (
|
||||
token_id < image_sentinel_lo + 5
|
||||
)
|
||||
bias = tl.where(is_image, bias_vl, bias)
|
||||
|
||||
if launch_pdl:
|
||||
tl.extra.cuda.gdc_wait()
|
||||
@@ -101,8 +118,17 @@ def dsv4_topk(
|
||||
correction_bias: torch.Tensor,
|
||||
indices_dtype: torch.dtype,
|
||||
routed_scaling_factor: float,
|
||||
input_ids: torch.Tensor | None = None,
|
||||
bias_vl: torch.Tensor | None = None,
|
||||
image_sentinel_lo: int = 0,
|
||||
) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
num_tokens, num_experts = gating_output.shape
|
||||
has_vl = bias_vl is not None and image_sentinel_lo > 0
|
||||
if bias_vl is not None:
|
||||
assert input_ids is not None, "bias_vl routing requires input_ids"
|
||||
assert bias_vl.dtype == torch.float32 and bias_vl.is_contiguous()
|
||||
assert bias_vl.shape == (num_experts,)
|
||||
assert input_ids.is_contiguous()
|
||||
shape = (num_tokens, _TOPK)
|
||||
topk_weights = gating_output.new_empty(shape, dtype=torch.float32)
|
||||
topk_ids = gating_output.new_empty(shape, dtype=indices_dtype)
|
||||
@@ -113,8 +139,12 @@ def dsv4_topk(
|
||||
topk_weights,
|
||||
topk_ids,
|
||||
routed_scaling_factor,
|
||||
input_ids,
|
||||
bias_vl,
|
||||
image_sentinel_lo,
|
||||
NUM_EXPERTS=num_experts,
|
||||
BLOCK_N=triton.next_power_of_2(num_experts),
|
||||
HAS_VL=has_vl,
|
||||
num_warps=1,
|
||||
launch_pdl=current_platform.is_arch_support_pdl(),
|
||||
)
|
||||
|
||||
@@ -81,6 +81,8 @@ def vllm_topk_softplus_sqrt(
|
||||
input_tokens: torch.Tensor | None = None,
|
||||
hash_indices_table: torch.Tensor | None = None,
|
||||
routed_scaling_factor: float = 1.0,
|
||||
bias_vl: torch.Tensor | None = None,
|
||||
image_sentinel_lo: int = 0,
|
||||
) -> tuple[torch.Tensor, ...]:
|
||||
ops.topk_hash_softplus_sqrt(
|
||||
topk_weights,
|
||||
@@ -93,6 +95,8 @@ def vllm_topk_softplus_sqrt(
|
||||
input_tokens,
|
||||
hash_indices_table,
|
||||
is_padding=_get_padding_mask(topk_indices.shape[0]),
|
||||
bias_vl=bias_vl,
|
||||
image_sentinel_lo=image_sentinel_lo,
|
||||
)
|
||||
|
||||
return topk_weights, topk_indices
|
||||
@@ -122,6 +126,8 @@ def fused_topk_bias(
|
||||
input_tokens: torch.Tensor | None = None,
|
||||
hash_indices_table: torch.Tensor | None = None,
|
||||
routed_scaling_factor: float = 1.0,
|
||||
bias_vl: torch.Tensor | None = None,
|
||||
image_sentinel_lo: int = 0,
|
||||
):
|
||||
if (
|
||||
input_tokens is not None
|
||||
@@ -130,18 +136,28 @@ def fused_topk_bias(
|
||||
):
|
||||
input_tokens = input_tokens.to(dtype=hash_indices_table.dtype)
|
||||
|
||||
if bias_vl is not None:
|
||||
# Image tokens carry five consecutive in-vocab sentinel ids starting
|
||||
# at image_sentinel_lo and select experts with bias_vl instead of the
|
||||
# regular routing path. image_sentinel_lo == 0 disables this.
|
||||
assert input_tokens is not None, "bias_vl routing requires input_tokens"
|
||||
|
||||
if not rocm_aiter_ops.is_fused_moe_enabled():
|
||||
assert hidden_states.size(0) == gating_output.size(0), (
|
||||
"Number of tokens mismatch"
|
||||
)
|
||||
|
||||
output_indices_dtype = torch.int32 if indices_type is None else indices_type
|
||||
if scoring_func == "sqrtsoftplus" and can_use_dsv4_topk(
|
||||
gating_output,
|
||||
e_score_correction_bias,
|
||||
topk,
|
||||
renormalize,
|
||||
output_indices_dtype,
|
||||
if (
|
||||
scoring_func == "sqrtsoftplus"
|
||||
and hash_indices_table is None
|
||||
and can_use_dsv4_topk(
|
||||
gating_output,
|
||||
e_score_correction_bias,
|
||||
topk,
|
||||
renormalize,
|
||||
output_indices_dtype,
|
||||
)
|
||||
):
|
||||
assert e_score_correction_bias is not None
|
||||
return dsv4_topk(
|
||||
@@ -149,6 +165,9 @@ def fused_topk_bias(
|
||||
e_score_correction_bias,
|
||||
output_indices_dtype,
|
||||
routed_scaling_factor,
|
||||
input_ids=input_tokens,
|
||||
bias_vl=bias_vl,
|
||||
image_sentinel_lo=image_sentinel_lo,
|
||||
)
|
||||
|
||||
M, _ = hidden_states.size()
|
||||
@@ -200,6 +219,8 @@ def fused_topk_bias(
|
||||
input_tokens,
|
||||
hash_indices_table,
|
||||
routed_scaling_factor,
|
||||
bias_vl=bias_vl,
|
||||
image_sentinel_lo=image_sentinel_lo,
|
||||
)
|
||||
else:
|
||||
raise ValueError(f"Unsupported scoring function: {scoring_func}")
|
||||
@@ -255,6 +276,8 @@ def fused_topk_bias(
|
||||
input_tokens,
|
||||
hash_indices_table,
|
||||
routed_scaling_factor,
|
||||
bias_vl=bias_vl,
|
||||
image_sentinel_lo=image_sentinel_lo,
|
||||
)
|
||||
|
||||
n_routed_experts = gating_output.shape[-1]
|
||||
@@ -270,10 +293,39 @@ def fused_topk_bias(
|
||||
) + e_score_correction_bias.unsqueeze(0)
|
||||
else:
|
||||
scores_for_choice = scores.view(-1, n_routed_experts)
|
||||
image_mask = None
|
||||
if bias_vl is not None and image_sentinel_lo > 0:
|
||||
# Image tokens (five consecutive sentinel ids starting at
|
||||
# image_sentinel_lo) select experts with bias_vl instead of
|
||||
# e_score_correction_bias / the hash table. Ids above the sentinel
|
||||
# block are regular special tokens and must not match.
|
||||
assert input_tokens is not None, "bias_vl routing requires input_tokens"
|
||||
image_mask = (
|
||||
(input_tokens >= image_sentinel_lo) & (input_tokens < image_sentinel_lo + 5)
|
||||
).unsqueeze(-1)
|
||||
text_bias = (
|
||||
e_score_correction_bias
|
||||
if e_score_correction_bias is not None
|
||||
else torch.zeros_like(bias_vl)
|
||||
)
|
||||
row_bias = torch.where(image_mask, bias_vl, text_bias)
|
||||
scores_for_choice = scores.view(-1, n_routed_experts) + row_bias
|
||||
# For batch invariance, use sorted=True to ensure deterministic expert selection
|
||||
if hash_indices_table is not None:
|
||||
assert input_tokens is not None
|
||||
topk_indices = hash_indices_table[input_tokens]
|
||||
if image_mask is None:
|
||||
topk_indices = hash_indices_table[input_tokens]
|
||||
else:
|
||||
# Clamp sentinel rows (overwritten below) to keep the table
|
||||
# lookup well-defined.
|
||||
safe_ids = torch.where(image_mask.squeeze(-1), 0, input_tokens).to(
|
||||
hash_indices_table.dtype
|
||||
)
|
||||
topk_indices = hash_indices_table[safe_ids]
|
||||
vl_indices = torch.topk(scores_for_choice, k=topk, dim=-1)[1]
|
||||
topk_indices = torch.where(
|
||||
image_mask, vl_indices.to(topk_indices.dtype), topk_indices
|
||||
)
|
||||
else:
|
||||
use_sorted = envs.VLLM_BATCH_INVARIANT
|
||||
topk_indices = torch.topk(scores_for_choice, k=topk, dim=-1, sorted=use_sorted)[
|
||||
@@ -306,6 +358,8 @@ class FusedTopKBiasRouter(BaseRouter):
|
||||
hash_indices_table: torch.Tensor | None = None,
|
||||
num_fused_shared_experts: int = 0,
|
||||
shared_expert_weight: float = 1.0,
|
||||
bias_vl: torch.Tensor | None = None,
|
||||
image_sentinel_lo: int = 0,
|
||||
):
|
||||
super().__init__(
|
||||
top_k=top_k,
|
||||
@@ -318,6 +372,11 @@ class FusedTopKBiasRouter(BaseRouter):
|
||||
self.routed_scaling_factor = routed_scaling_factor
|
||||
self.scoring_func = scoring_func
|
||||
self._hash_indices_table = hash_indices_table
|
||||
# Vision bias: image sentinel tokens (five consecutive in-vocab ids
|
||||
# starting at image_sentinel_lo) select experts with bias_vl instead
|
||||
# of e_score_correction_bias / the hash table.
|
||||
self.bias_vl = bias_vl
|
||||
self.image_sentinel_lo = image_sentinel_lo
|
||||
# Fused shared experts: append constant slots (ids immediately after
|
||||
# the routed experts, [global, global+n)) routed to by every token at
|
||||
# ``shared_expert_weight``, AFTER the routed top-k is renormalized.
|
||||
@@ -357,6 +416,8 @@ class FusedTopKBiasRouter(BaseRouter):
|
||||
input_tokens=input_ids,
|
||||
hash_indices_table=self._hash_indices_table,
|
||||
routed_scaling_factor=self.routed_scaling_factor,
|
||||
bias_vl=self.bias_vl.data if self.bias_vl is not None else None,
|
||||
image_sentinel_lo=self.image_sentinel_lo,
|
||||
)
|
||||
|
||||
if self.num_fused_shared_experts > 0:
|
||||
|
||||
@@ -60,6 +60,9 @@ def create_fused_moe_router(
|
||||
zero_expert_type: str | None = None,
|
||||
num_logical_experts: int | None = None,
|
||||
hash_indices_table: torch.Tensor | None = None,
|
||||
# Deepseek V4 vision routing bias parameters
|
||||
bias_vl: torch.Tensor | None = None,
|
||||
image_sentinel_lo: int = 0,
|
||||
) -> FusedMoERouter:
|
||||
"""
|
||||
Factory function to create the appropriate FusedMoERouter subclass based on
|
||||
@@ -211,6 +214,8 @@ def create_fused_moe_router(
|
||||
hash_indices_table=hash_indices_table,
|
||||
num_fused_shared_experts=num_fused_shared_experts,
|
||||
shared_expert_weight=shared_expert_weight,
|
||||
bias_vl=bias_vl,
|
||||
image_sentinel_lo=image_sentinel_lo,
|
||||
)
|
||||
|
||||
if (
|
||||
|
||||
@@ -1002,6 +1002,7 @@ MODELS_CONFIG_MAP: dict[str, type[VerifyAndUpdateConfig]] = {
|
||||
"ColBERTJinaRobertaModel": JinaRobertaModelConfig,
|
||||
"ColQwen3_5": ColQwen3_5Config,
|
||||
"DeepseekV4ForCausalLM": DeepseekV4ForCausalLMConfig,
|
||||
"DeepseekV4ForConditionalGeneration": DeepseekV4ForCausalLMConfig,
|
||||
"DeepseekV32ForCausalLM": DeepseekV32ForCausalLM,
|
||||
"DiffusionGemmaForBlockDiffusion": DiffusionGemmaModelForBlockDiffusionConfig, # noqa: E501
|
||||
"Ernie4_5_VLMoeForConditionalGeneration": Ernie4_5_VLMoeForConditionalGenerationConfig, # noqa: E501
|
||||
|
||||
@@ -362,6 +362,10 @@ _MULTIMODAL_MODELS = {
|
||||
"DeepseekVLV2ForCausalLM": ("deepseek_vl2", "DeepseekVLV2ForCausalLM"),
|
||||
"DeepseekOCRForCausalLM": ("deepseek_ocr", "DeepseekOCRForCausalLM"),
|
||||
"DeepseekOCR2ForCausalLM": ("deepseek_ocr2", "DeepseekOCR2ForCausalLM"),
|
||||
"DeepseekV4ForConditionalGeneration": (
|
||||
"vllm.models.deepseek_v4",
|
||||
"DeepseekV4ForConditionalGeneration",
|
||||
),
|
||||
"Dots3NoteForCausalLM": (
|
||||
"vllm.models.dots3_note",
|
||||
"Dots3NoteForCausalLM",
|
||||
|
||||
@@ -20,7 +20,13 @@ if current_platform.is_rocm():
|
||||
)
|
||||
from .amd.model import DeepseekV4ForCausalLM
|
||||
from .amd.mtp import DeepSeekV4MTP
|
||||
from .vl_stub import ( # type: ignore[assignment]
|
||||
DeepseekV4ForConditionalGeneration,
|
||||
)
|
||||
elif current_platform.is_xpu():
|
||||
from .vl_stub import ( # type: ignore[assignment]
|
||||
DeepseekV4ForConditionalGeneration,
|
||||
)
|
||||
from .xpu.dspark import DSparkDeepseekV4ForCausalLM # type: ignore[assignment]
|
||||
from .xpu.model import DeepseekV4ForCausalLM # type: ignore[assignment]
|
||||
from .xpu.mtp import DeepSeekV4MTP # type: ignore[assignment]
|
||||
@@ -30,10 +36,14 @@ else:
|
||||
)
|
||||
from .nvidia.model import DeepseekV4ForCausalLM # type: ignore[assignment]
|
||||
from .nvidia.mtp import DeepSeekV4MTP # type: ignore[assignment]
|
||||
from .nvidia.vl_model import ( # type: ignore[assignment]
|
||||
DeepseekV4ForConditionalGeneration,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"DSparkDeepseekV4ForCausalLM",
|
||||
"DeepSeekV4MTP",
|
||||
"DeepseekV4FP8Config",
|
||||
"DeepseekV4ForCausalLM",
|
||||
"DeepseekV4ForConditionalGeneration",
|
||||
]
|
||||
|
||||
@@ -205,6 +205,13 @@ class DeepseekV4Attention(nn.Module, AttentionLayerBase, ABC):
|
||||
self.n_groups = config.o_groups
|
||||
self.n_local_groups = self.n_groups // tp_size
|
||||
self.window_size = config.sliding_window
|
||||
# Vision variant: image spans are visible bidirectionally, widening
|
||||
# prefill SWA index rows by up to max_image_tokens columns.
|
||||
self.max_image_tokens = (
|
||||
getattr(config, "vision_max_n_token", 0)
|
||||
if getattr(config, "vision_n_layers", 0) > 0
|
||||
else 0
|
||||
)
|
||||
# NOTE(zyongye) Compress ratio can't be 0
|
||||
# we do this for because MTP layer is not included
|
||||
# in the compress ratio list
|
||||
@@ -363,6 +370,7 @@ class DeepseekV4Attention(nn.Module, AttentionLayerBase, ABC):
|
||||
_COMPUTE_SWA_INDICES_AND_LENS_KERNEL.register_warmup(
|
||||
window_size=self.window_size,
|
||||
block_size=self.swa_cache_layer.block_size,
|
||||
max_image_tokens=self.max_image_tokens,
|
||||
)
|
||||
|
||||
if self.compress_ratio > 1:
|
||||
|
||||
@@ -0,0 +1,512 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""Multimodal preprocessing for the DeepSeek-V4 vision variants
|
||||
(DeepSeek-V4-Flash-Vision-Exp).
|
||||
|
||||
The image transform and sentinel-block construction are ported from the
|
||||
official repository's ``image_processor.py`` so that token counts bit-match
|
||||
the reference. Each ``<|deepseek_image|>`` placeholder in the prompt expands
|
||||
to a variable-length block of sentinel tokens; only positions with
|
||||
``type == IMAGE`` receive vision embeddings, the other sentinels are looked
|
||||
up from learned embedding vectors in the model.
|
||||
|
||||
Unlike the reference (which uses out-of-vocab ids ``vocab_size + type``),
|
||||
the sentinel block borrows five consecutive reserved tokenizer tokens
|
||||
(``<|place_holder_mm_span_0431|>`` .. ``_0435|>``): they are special tokens
|
||||
the tokenizer never emits from plain text, so the ids stay in-vocabulary and
|
||||
work with stock token validation, logprobs and detokenization. The ids are
|
||||
pure markers — every sentinel position's embedding is overwritten with
|
||||
vision/sentinel vectors before the decoder layers see it, exactly like the
|
||||
reference's out-of-vocab scheme.
|
||||
"""
|
||||
|
||||
import math
|
||||
from collections.abc import Mapping, Sequence
|
||||
from typing import Any, cast
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
from PIL import Image, ImageOps
|
||||
from transformers import BatchFeature
|
||||
from typing_extensions import assert_never
|
||||
|
||||
from vllm.config.multimodal import BaseDummyOptions, ImageDummyOptions
|
||||
from vllm.inputs import MultiModalDataDict
|
||||
from vllm.multimodal.inputs import MultiModalFieldConfig, MultiModalKwargsItems
|
||||
from vllm.multimodal.parse import ImageSize, MultiModalDataItems
|
||||
from vllm.multimodal.processing import (
|
||||
BaseDummyInputsBuilder,
|
||||
BaseMultiModalProcessor,
|
||||
BaseProcessingInfo,
|
||||
PromptReplacement,
|
||||
PromptUpdate,
|
||||
PromptUpdateDetails,
|
||||
)
|
||||
from vllm.multimodal.processing.processor import (
|
||||
MultiModalPromptUpdates,
|
||||
MultiModalPromptUpdatesApplyResult,
|
||||
PlaceholderFeaturesInfo,
|
||||
UpdateMode,
|
||||
_plan_prompt_updates,
|
||||
)
|
||||
from vllm.transformers_utils.configs.deepseek_v4 import DeepseekV4Config
|
||||
|
||||
IMAGE_START, IMAGE_PAD, IMAGE, IMAGE_NEW_LINE, IMAGE_END = range(5)
|
||||
COMPRESS_PAD_TO = 4
|
||||
|
||||
IMAGE_PLACEHOLDER = "<|deepseek_image|>"
|
||||
|
||||
# Sentinel roles borrow five consecutive ``<|place_holder_mm_span_XXXX|>``
|
||||
# tokens (reserved special tokens, never emitted from plain text). The order
|
||||
# must match IMAGE_START..IMAGE_END above so that
|
||||
# ``id == IMAGE_SENTINEL_BASE_ID + type``.
|
||||
IMAGE_SENTINEL_BASE_ID = 129257
|
||||
IMAGE_SENTINEL_TOKEN_NAMES = (
|
||||
"<|place_holder_mm_span_0431|>", # IMAGE_START
|
||||
"<|place_holder_mm_span_0432|>", # IMAGE_PAD
|
||||
"<|place_holder_mm_span_0433|>", # IMAGE
|
||||
"<|place_holder_mm_span_0434|>", # IMAGE_NEW_LINE
|
||||
"<|place_holder_mm_span_0435|>", # IMAGE_END
|
||||
)
|
||||
|
||||
|
||||
def image_sentinel_mask(token_ids: torch.Tensor) -> torch.Tensor:
|
||||
"""Boolean mask for image-block sentinel positions (in-vocab ids)."""
|
||||
return (token_ids >= IMAGE_SENTINEL_BASE_ID) & (
|
||||
token_ids < IMAGE_SENTINEL_BASE_ID + len(IMAGE_SENTINEL_TOKEN_NAMES)
|
||||
)
|
||||
|
||||
|
||||
def validate_image_sentinel_ids(tokenizer) -> None:
|
||||
"""Check the borrowed sentinel ids against the tokenizer."""
|
||||
for i, name in enumerate(IMAGE_SENTINEL_TOKEN_NAMES):
|
||||
token_id = tokenizer.convert_tokens_to_ids(name)
|
||||
if token_id != IMAGE_SENTINEL_BASE_ID + i:
|
||||
raise ValueError(
|
||||
f"Image sentinel token {name!r} has id {token_id}, expected "
|
||||
f"{IMAGE_SENTINEL_BASE_ID + i}; the DeepSeek-V4 vision "
|
||||
"sentinel block requires these consecutive reserved ids."
|
||||
)
|
||||
|
||||
|
||||
def grid_tokens(best_height, best_width, patch_size, downsample_ratio):
|
||||
"""Number of LLM tokens the aligner grid occupies (N-layout, including
|
||||
row/align padding)."""
|
||||
n_llm_h = math.ceil((best_height // patch_size) / downsample_ratio)
|
||||
n_llm_w = math.ceil((best_width // patch_size) / downsample_ratio)
|
||||
num_tokens = n_llm_h * (n_llm_w + 1) + 2
|
||||
if n_llm_h % 2 == 1:
|
||||
num_tokens += n_llm_w + 1
|
||||
num_tokens += (n_llm_h + 1) // 2 * (n_llm_w + 1) % 2 * 2
|
||||
return n_llm_h, n_llm_w, num_tokens
|
||||
|
||||
|
||||
def solve_resize_ratio(height, width, patch_size, downsample_ratio, max_n_token):
|
||||
r = height / width
|
||||
max_w_float = math.sqrt((max_n_token - 2) / r + 0.25) - 0.5
|
||||
max_h_float = max_w_float * r
|
||||
if max_w_float < 1.0:
|
||||
max_w = 1
|
||||
max_h = (max_n_token - 2) // (max_w + 1)
|
||||
if max_h % 2 == 1:
|
||||
max_h -= 1
|
||||
best_width = max_w * patch_size * downsample_ratio
|
||||
best_height = max_h * patch_size * downsample_ratio
|
||||
elif max_h_float < 2.0:
|
||||
max_h = 2
|
||||
max_w = ((max_n_token - 2) // max_h) - 1
|
||||
assert max_w > 1
|
||||
best_width = max_w * patch_size * downsample_ratio
|
||||
best_height = max_h * patch_size * downsample_ratio
|
||||
else:
|
||||
max_w = math.floor(max_w_float)
|
||||
max_h = math.floor(max_h_float)
|
||||
if max_h % 2 == 1:
|
||||
max_h -= 1
|
||||
beta = min(
|
||||
max_w * patch_size * downsample_ratio / width,
|
||||
max_h * patch_size * downsample_ratio / height,
|
||||
)
|
||||
best_width = math.floor(width * beta / patch_size) * patch_size
|
||||
best_height = math.floor(height * beta / patch_size) * patch_size
|
||||
n_llm_h, n_llm_w, num_tokens = grid_tokens(
|
||||
best_height, best_width, patch_size, downsample_ratio
|
||||
)
|
||||
return n_llm_h, n_llm_w, best_height, best_width, num_tokens
|
||||
|
||||
|
||||
def safe_resize(
|
||||
height, width, best_height, best_width, patch_size, downsample_ratio, max_n_token
|
||||
):
|
||||
max_n_token -= COMPRESS_PAD_TO - 1
|
||||
n_llm_h, n_llm_w, num_tokens = grid_tokens(
|
||||
best_height, best_width, patch_size, downsample_ratio
|
||||
)
|
||||
budget = max_n_token
|
||||
while num_tokens > max_n_token:
|
||||
n_llm_h, n_llm_w, best_height, best_width, num_tokens = solve_resize_ratio(
|
||||
height, width, patch_size, downsample_ratio, budget
|
||||
)
|
||||
budget -= 1
|
||||
return n_llm_h, n_llm_w, best_height, best_width
|
||||
|
||||
|
||||
def load_image(
|
||||
image: Image.Image,
|
||||
*,
|
||||
patch_size: int,
|
||||
downsample_ratio: int,
|
||||
max_n_token: int,
|
||||
min_pixels: int,
|
||||
max_wh_ratio: float | None,
|
||||
):
|
||||
"""Transform one PIL image into ViT patches.
|
||||
|
||||
Same math as the reference ``load_image``, except the image is already
|
||||
decoded (vLLM supplies PIL images instead of a record dict).
|
||||
"""
|
||||
p = patch_size
|
||||
image = image.convert("RGB")
|
||||
width, height = image.size
|
||||
if max_wh_ratio is not None and width > height * max_wh_ratio:
|
||||
width = height * max_wh_ratio
|
||||
if 0 < width * height < min_pixels:
|
||||
ratio = (min_pixels / (width * height)) ** 0.5
|
||||
width = int(width * ratio)
|
||||
height = int(height * ratio)
|
||||
best_width = math.ceil(width / p) * p
|
||||
best_height = math.ceil(height / p) * p
|
||||
n_llm_h, n_llm_w, best_height, best_width = safe_resize(
|
||||
height, width, best_height, best_width, p, downsample_ratio, max_n_token
|
||||
)
|
||||
n_vit_h, n_vit_w = best_height // p, best_width // p
|
||||
if max_wh_ratio is not None and image.width >= max_wh_ratio * image.height:
|
||||
image = image.resize((best_width, best_height))
|
||||
else:
|
||||
image = ImageOps.pad(image, (best_width, best_height), color=(127, 127, 127))
|
||||
x = torch.from_numpy(np.asarray(image, dtype=np.float32)).permute(2, 0, 1) / 255
|
||||
x = ((x - 0.5) / 0.5).to(torch.bfloat16)
|
||||
patches = (
|
||||
x.reshape(3, n_vit_h, p, n_vit_w, p)
|
||||
.permute(1, 3, 0, 2, 4)
|
||||
.reshape(n_vit_h * n_vit_w, 3, p, p)
|
||||
)
|
||||
return patches, n_vit_h, n_vit_w, n_llm_h, n_llm_w
|
||||
|
||||
|
||||
def build_image_block(n_llm_h: int, n_llm_w: int, start_pos: int):
|
||||
"""Builds the N-layout token types (final order) and the aligner-row order
|
||||
for IMAGE slots."""
|
||||
compress_pad = COMPRESS_PAD_TO - 1 - start_pos % COMPRESS_PAD_TO
|
||||
pad_h = n_llm_h % 2
|
||||
rows = n_llm_h + pad_h
|
||||
row_len = n_llm_w + 1
|
||||
pad_last = rows // 2 * row_len % 2 * 2
|
||||
types = torch.tensor(
|
||||
([IMAGE] * n_llm_w + [IMAGE_NEW_LINE]) * n_llm_h
|
||||
+ [IMAGE_PAD] * (row_len * pad_h),
|
||||
dtype=torch.int64,
|
||||
)
|
||||
order = torch.arange(rows * row_len).view(rows // 2, 2, row_len)
|
||||
order = order.transpose(1, 2).reshape(-1)
|
||||
image_idx = torch.full((rows * row_len,), -1, dtype=torch.int64)
|
||||
image_idx.view(rows, row_len)[:n_llm_h, :n_llm_w] = torch.arange(
|
||||
n_llm_h * n_llm_w
|
||||
).view(n_llm_h, n_llm_w)
|
||||
perm = image_idx[order]
|
||||
perm = perm[perm >= 0]
|
||||
types = torch.cat(
|
||||
[
|
||||
torch.full((compress_pad,), IMAGE_PAD, dtype=torch.int64),
|
||||
torch.tensor([IMAGE_START]),
|
||||
types[order],
|
||||
torch.full((pad_last,), IMAGE_PAD, dtype=torch.int64),
|
||||
torch.tensor([IMAGE_END]),
|
||||
]
|
||||
)
|
||||
return types, perm
|
||||
|
||||
|
||||
def build_image_block_pad_free(n_llm_h: int, n_llm_w: int):
|
||||
"""``build_image_block`` without the position-dependent compressor pad.
|
||||
|
||||
``start_pos`` is chosen so that ``compress_pad == 0``; the pad is instead
|
||||
prepended when the block is spliced into the final prompt, where its
|
||||
position is known.
|
||||
"""
|
||||
return build_image_block(n_llm_h, n_llm_w, COMPRESS_PAD_TO - 1)
|
||||
|
||||
|
||||
class DeepseekV4VLImageProcessor:
|
||||
"""Per-image transform (the PIL-input equivalent of the reference
|
||||
``load_image``)."""
|
||||
|
||||
def __init__(self, config: DeepseekV4Config) -> None:
|
||||
super().__init__()
|
||||
self.patch_size = config.vision_patch_size
|
||||
self.downsample_ratio = config.vision_downsample_ratio
|
||||
self.max_n_token = config.vision_max_n_token
|
||||
self.min_pixels = config.vision_min_pixels
|
||||
self.max_wh_ratio = config.vision_max_wh_ratio
|
||||
|
||||
def __call__(self, image: Image.Image):
|
||||
return load_image(
|
||||
image,
|
||||
patch_size=self.patch_size,
|
||||
downsample_ratio=self.downsample_ratio,
|
||||
max_n_token=self.max_n_token,
|
||||
min_pixels=self.min_pixels,
|
||||
max_wh_ratio=self.max_wh_ratio,
|
||||
)
|
||||
|
||||
|
||||
class DeepseekV4VLProcessor:
|
||||
"""Minimal stand-in for the HF processor of DeepSeek-V4 vision models.
|
||||
|
||||
The official repository ships image preprocessing as plain functions in
|
||||
``image_processor.py`` (no ``auto_map`` processor), so this class wraps
|
||||
their ports directly and the model loads without ``--trust-remote-code``.
|
||||
|
||||
``__call__`` returns a ``BatchFeature`` with one entry per image
|
||||
(flattened across images):
|
||||
|
||||
- ``patches``: ``(sum(n_vit_h * n_vit_w), 3, p, p)`` bf16 ViT patches.
|
||||
- ``vit_grid``: ``(num_images, 2)`` int64 ``[n_vit_h, n_vit_w]``.
|
||||
- ``llm_grid``: ``(num_images, 2)`` int64 ``[n_llm_h, n_llm_w]``.
|
||||
- ``perm``: concatenated per-image ``(n_llm_h * n_llm_w,)`` int64 index
|
||||
selecting aligner outputs into the final N-layout order.
|
||||
- ``types``: concatenated per-image pad-free sentinel block types;
|
||||
``block ids = IMAGE_SENTINEL_BASE_ID + types``.
|
||||
"""
|
||||
|
||||
def __init__(self, config: DeepseekV4Config) -> None:
|
||||
super().__init__()
|
||||
self.config = config
|
||||
self.image_processor = DeepseekV4VLImageProcessor(config)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
text: str | None = None,
|
||||
images: Sequence[Image.Image] | None = None,
|
||||
return_tensors: str | None = None,
|
||||
**kwargs: Any,
|
||||
) -> BatchFeature:
|
||||
patches_list = []
|
||||
vit_grid = []
|
||||
llm_grid = []
|
||||
perm_list = []
|
||||
types_list = []
|
||||
for image in images or []:
|
||||
patches, n_vit_h, n_vit_w, n_llm_h, n_llm_w = self.image_processor(image)
|
||||
types, perm = build_image_block_pad_free(n_llm_h, n_llm_w)
|
||||
patches_list.append(patches)
|
||||
vit_grid.append((n_vit_h, n_vit_w))
|
||||
llm_grid.append((n_llm_h, n_llm_w))
|
||||
perm_list.append(perm)
|
||||
types_list.append(types)
|
||||
|
||||
if not patches_list:
|
||||
return BatchFeature({})
|
||||
|
||||
return BatchFeature(
|
||||
{
|
||||
"patches": torch.cat(patches_list),
|
||||
"vit_grid": torch.tensor(vit_grid, dtype=torch.int64),
|
||||
"llm_grid": torch.tensor(llm_grid, dtype=torch.int64),
|
||||
"perm": torch.cat(perm_list),
|
||||
"types": torch.cat(types_list),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class DeepseekV4VLProcessingInfo(BaseProcessingInfo):
|
||||
def get_hf_config(self) -> DeepseekV4Config:
|
||||
return self.ctx.get_hf_config(DeepseekV4Config)
|
||||
|
||||
def get_hf_processor(self, **kwargs: object) -> DeepseekV4VLProcessor:
|
||||
if kwargs:
|
||||
raise ValueError(f"Unexpected processor kwargs: {sorted(kwargs)}")
|
||||
return DeepseekV4VLProcessor(self.get_hf_config())
|
||||
|
||||
def get_supported_mm_limits(self) -> Mapping[str, int | None]:
|
||||
return {"image": None}
|
||||
|
||||
def get_mm_max_tokens_per_item(
|
||||
self,
|
||||
seq_len: int,
|
||||
mm_counts: Mapping[str, int],
|
||||
) -> Mapping[str, int]:
|
||||
# ``safe_resize`` reserves COMPRESS_PAD_TO - 1 tokens of
|
||||
# vision_max_n_token for the compressor-alignment pad, so the full
|
||||
# sentinel block is bounded by vision_max_n_token; the margin is kept
|
||||
# in case that reservation changes.
|
||||
return {
|
||||
"image": self.get_hf_config().vision_max_n_token + COMPRESS_PAD_TO - 1,
|
||||
}
|
||||
|
||||
def get_image_placeholder_token_id(self) -> int:
|
||||
token_id = self.get_tokenizer().convert_tokens_to_ids(IMAGE_PLACEHOLDER)
|
||||
if token_id is None:
|
||||
raise ValueError(f"Token not found in tokenizer: {IMAGE_PLACEHOLDER}")
|
||||
return token_id
|
||||
|
||||
def get_image_size_with_most_features(self) -> ImageSize:
|
||||
hf_config = self.get_hf_config()
|
||||
patch_size = hf_config.vision_patch_size
|
||||
downsample_ratio = hf_config.vision_downsample_ratio
|
||||
# A square maximizes the ViT patch count (area) within the token
|
||||
# budget; solve the budget-derived size directly to keep the dummy
|
||||
# image small.
|
||||
budget = hf_config.vision_max_n_token - (COMPRESS_PAD_TO - 1)
|
||||
side = budget * patch_size * downsample_ratio
|
||||
_, _, best_h, best_w, _ = solve_resize_ratio(
|
||||
side, side, patch_size, downsample_ratio, budget
|
||||
)
|
||||
return ImageSize(width=best_w, height=best_h)
|
||||
|
||||
|
||||
class DeepseekV4VLDummyInputsBuilder(
|
||||
BaseDummyInputsBuilder[DeepseekV4VLProcessingInfo]
|
||||
):
|
||||
def get_dummy_text(self, mm_counts: Mapping[str, int]) -> str:
|
||||
return IMAGE_PLACEHOLDER * mm_counts.get("image", 0)
|
||||
|
||||
def get_dummy_mm_data(
|
||||
self,
|
||||
seq_len: int,
|
||||
mm_counts: Mapping[str, int],
|
||||
mm_options: Mapping[str, BaseDummyOptions],
|
||||
) -> MultiModalDataDict:
|
||||
size = self.info.get_image_size_with_most_features()
|
||||
return {
|
||||
"image": self._get_dummy_images(
|
||||
width=size.width,
|
||||
height=size.height,
|
||||
num_images=mm_counts.get("image", 0),
|
||||
overrides=cast(ImageDummyOptions | None, mm_options.get("image")),
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
class DeepseekV4VLMultiModalProcessor(
|
||||
BaseMultiModalProcessor[DeepseekV4VLProcessingInfo]
|
||||
):
|
||||
def _get_mm_fields_config(
|
||||
self,
|
||||
hf_inputs: BatchFeature,
|
||||
hf_processor_mm_kwargs: Mapping[str, object],
|
||||
) -> Mapping[str, MultiModalFieldConfig]:
|
||||
vit_grid = hf_inputs.get("vit_grid")
|
||||
llm_grid = hf_inputs.get("llm_grid")
|
||||
|
||||
if vit_grid is None or llm_grid is None:
|
||||
empty = torch.empty(0, dtype=torch.long)
|
||||
patch_sizes = perm_sizes = types_sizes = empty
|
||||
else:
|
||||
patch_sizes = vit_grid.prod(-1)
|
||||
perm_sizes = llm_grid.prod(-1)
|
||||
n_llm_h, n_llm_w = llm_grid[:, 0], llm_grid[:, 1]
|
||||
# Pad-free block length; same formula as ``grid_tokens`` given
|
||||
# the LLM grid.
|
||||
types_sizes = (
|
||||
n_llm_h * (n_llm_w + 1)
|
||||
+ 2
|
||||
+ (n_llm_h % 2) * (n_llm_w + 1)
|
||||
+ (n_llm_h + 1) // 2 * (n_llm_w + 1) % 2 * 2
|
||||
)
|
||||
|
||||
return {
|
||||
"patches": MultiModalFieldConfig.flat_from_sizes("image", patch_sizes),
|
||||
"vit_grid": MultiModalFieldConfig.batched("image", keep_on_cpu=True),
|
||||
"llm_grid": MultiModalFieldConfig.batched("image", keep_on_cpu=True),
|
||||
"perm": MultiModalFieldConfig.flat_from_sizes("image", perm_sizes),
|
||||
"types": MultiModalFieldConfig.flat_from_sizes(
|
||||
"image", types_sizes, keep_on_cpu=True
|
||||
),
|
||||
}
|
||||
|
||||
def _get_prompt_updates(
|
||||
self,
|
||||
mm_items: MultiModalDataItems,
|
||||
hf_processor_mm_kwargs: Mapping[str, object],
|
||||
out_mm_kwargs: MultiModalKwargsItems,
|
||||
) -> Sequence[PromptUpdate]:
|
||||
image_token_id = self.info.get_image_placeholder_token_id()
|
||||
validate_image_sentinel_ids(self.info.get_tokenizer())
|
||||
image_embed_id = IMAGE_SENTINEL_BASE_ID + IMAGE
|
||||
|
||||
def get_image_replacement(item_idx: int) -> PromptUpdateDetails:
|
||||
types: torch.Tensor = out_mm_kwargs["image"][item_idx]["types"].data
|
||||
full = (IMAGE_SENTINEL_BASE_ID + types).tolist()
|
||||
return PromptUpdateDetails.select_token_id(full, image_embed_id)
|
||||
|
||||
return [
|
||||
PromptReplacement(
|
||||
modality="image",
|
||||
target=[image_token_id],
|
||||
replacement=get_image_replacement,
|
||||
),
|
||||
]
|
||||
|
||||
def _apply_token_matches_with_placeholders(
|
||||
self,
|
||||
token_ids: list[int],
|
||||
mm_prompt_updates: MultiModalPromptUpdates,
|
||||
) -> tuple[
|
||||
list[int],
|
||||
MultiModalPromptUpdatesApplyResult,
|
||||
Mapping[str, list[PlaceholderFeaturesInfo]],
|
||||
]:
|
||||
# Same as the base implementation, except that each image block gets
|
||||
# its compressor-alignment pad (``3 - start_pos % 4`` IMAGE_PAD
|
||||
# sentinels) prepended while splicing: the pad depends on the block's
|
||||
# final position in the prompt, which is unknown when the (cacheable)
|
||||
# prompt updates are built.
|
||||
matched_updates, result = _plan_prompt_updates(token_ids, mm_prompt_updates)
|
||||
placeholders: dict[str, list[PlaceholderFeaturesInfo]] = {
|
||||
modality: [] for modality in mm_prompt_updates
|
||||
}
|
||||
|
||||
pad_id = IMAGE_SENTINEL_BASE_ID + IMAGE_PAD
|
||||
|
||||
new_token_ids = list[int]()
|
||||
prev_end_idx = 0
|
||||
for matched_update in matched_updates:
|
||||
update = matched_update.update
|
||||
match = matched_update.match
|
||||
|
||||
if update.mode == UpdateMode.INSERT:
|
||||
end_idx_to_insert = match.end_idx
|
||||
elif update.mode == UpdateMode.REPLACE:
|
||||
end_idx_to_insert = match.start_idx
|
||||
else:
|
||||
assert_never(update.mode)
|
||||
|
||||
new_token_ids.extend(token_ids[prev_end_idx:end_idx_to_insert])
|
||||
start_idx = len(new_token_ids)
|
||||
|
||||
tokens = list(update.content.full)
|
||||
if tokens and update.modality == "image":
|
||||
compress_pad = COMPRESS_PAD_TO - 1 - start_idx % COMPRESS_PAD_TO
|
||||
tokens = [pad_id] * compress_pad + tokens
|
||||
|
||||
if tokens:
|
||||
content_is_embed = update.content.is_embed
|
||||
is_embed = (
|
||||
content_is_embed(tokens) if content_is_embed is not None else None
|
||||
)
|
||||
placeholders[update.modality].append(
|
||||
PlaceholderFeaturesInfo(
|
||||
modality=update.modality,
|
||||
item_idx=update.item_idx,
|
||||
start_idx=start_idx,
|
||||
tokens=tokens,
|
||||
is_embed=is_embed,
|
||||
)
|
||||
)
|
||||
new_token_ids.extend(tokens)
|
||||
|
||||
prev_end_idx = match.end_idx
|
||||
|
||||
new_token_ids.extend(token_ids[prev_end_idx:])
|
||||
return new_token_ids, result, placeholders
|
||||
@@ -536,10 +536,16 @@ def combine_topk_swa_indices(
|
||||
M: int,
|
||||
N: int,
|
||||
out: tuple[torch.Tensor, torch.Tensor] | None = None,
|
||||
left_visible: torch.Tensor | None = None,
|
||||
right_visible: torch.Tensor | None = None,
|
||||
max_image_tokens: int = 0,
|
||||
) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
num_tokens = topk_indices.shape[0]
|
||||
# max_image_tokens widens the SWA column region for in-image
|
||||
# bidirectional visibility; the width is fixed per model so the
|
||||
# caller-provided workspace stays valid on image-free batches.
|
||||
combined_topk = (
|
||||
(topk + window_size + _SPARSE_PREFILL_TOPK_ALIGNMENT - 1)
|
||||
(topk + window_size + max_image_tokens + _SPARSE_PREFILL_TOPK_ALIGNMENT - 1)
|
||||
// _SPARSE_PREFILL_TOPK_ALIGNMENT
|
||||
* _SPARSE_PREFILL_TOPK_ALIGNMENT
|
||||
)
|
||||
@@ -568,6 +574,9 @@ def combine_topk_swa_indices(
|
||||
TOP_K=topk,
|
||||
COMPRESS_RATIO=compress_ratio,
|
||||
WINDOW_SIZE=window_size,
|
||||
left_visible=left_visible,
|
||||
right_visible=right_visible,
|
||||
max_image_tokens=max_image_tokens,
|
||||
)
|
||||
return combined_indices, combined_lens
|
||||
|
||||
@@ -628,6 +637,7 @@ class CombineTopkSwaIndicesKernel(
|
||||
TOP_K: int
|
||||
COMPRESS_RATIO: int
|
||||
WINDOW_SIZE: int
|
||||
IMAGE_WIDTH: int
|
||||
PADDED_TOP_K: int
|
||||
input_variant: TritonPointerInputVariant
|
||||
|
||||
@@ -649,11 +659,14 @@ class CombineTopkSwaIndicesKernel(
|
||||
query_start_loc_ptr,
|
||||
seq_lens_ptr,
|
||||
gather_lens_ptr,
|
||||
left_visible_ptr,
|
||||
right_visible_ptr,
|
||||
M,
|
||||
N,
|
||||
TOP_K: tl.constexpr,
|
||||
COMPRESS_RATIO: tl.constexpr,
|
||||
WINDOW_SIZE: tl.constexpr,
|
||||
IMAGE_WIDTH: tl.constexpr,
|
||||
PADDED_TOP_K: tl.constexpr,
|
||||
):
|
||||
batch_idx = tl.program_id(0)
|
||||
@@ -682,7 +695,19 @@ class CombineTopkSwaIndicesKernel(
|
||||
token_idx_in_query = token_idx - query_start
|
||||
pos = start_pos + token_idx_in_query
|
||||
topk_len = tl.minimum((pos + 1) // COMPRESS_RATIO, TOP_K)
|
||||
swa_len = tl.minimum(pos + 1, WINDOW_SIZE)
|
||||
if IMAGE_WIDTH > 0:
|
||||
# In-image bidirectional visibility: the window starts up to
|
||||
# max(left - (window - 1), 0) positions earlier and extends
|
||||
# `right` positions past the query token (both 0 outside
|
||||
# image spans, reducing to the plain causal window).
|
||||
left = tl.load(left_visible_ptr + token_idx)
|
||||
right = tl.load(right_visible_ptr + token_idx)
|
||||
else:
|
||||
left = 0
|
||||
right = 0
|
||||
left_add = tl.maximum(left - (WINDOW_SIZE - 1), 0)
|
||||
swa_start = tl.maximum(pos - (WINDOW_SIZE - 1) - left_add, 0)
|
||||
swa_len = pos + right - swa_start + 1
|
||||
|
||||
offset = tl.arange(0, PADDED_TOP_K)
|
||||
mask = offset < topk_len
|
||||
@@ -695,18 +720,19 @@ class CombineTopkSwaIndicesKernel(
|
||||
topk_indices + M * batch_idx,
|
||||
mask=mask,
|
||||
)
|
||||
offset = tl.arange(0, WINDOW_SIZE)
|
||||
# Index into gathered buffer: N + (position - gather_start)
|
||||
# For positions [pos - swa_len + 1, pos], the buffer indices are:
|
||||
# [N + pos - swa_len + 1 - gather_start, N + pos - gather_start]
|
||||
tl.store(
|
||||
combined_indices_ptr
|
||||
+ token_idx * combined_indices_stride
|
||||
+ topk_len
|
||||
+ offset,
|
||||
M * batch_idx + N + offset + pos - swa_len + 1 - gather_start,
|
||||
mask=offset < swa_len,
|
||||
)
|
||||
# For positions [swa_start, pos + right], the buffer indices are:
|
||||
# [N + swa_start - gather_start, N + pos + right - gather_start]
|
||||
for i in range(0, WINDOW_SIZE + IMAGE_WIDTH, WINDOW_SIZE):
|
||||
swa_offset = i + tl.arange(0, WINDOW_SIZE)
|
||||
tl.store(
|
||||
combined_indices_ptr
|
||||
+ token_idx * combined_indices_stride
|
||||
+ topk_len
|
||||
+ swa_offset,
|
||||
M * batch_idx + N + swa_start + swa_offset - gather_start,
|
||||
mask=swa_offset < swa_len,
|
||||
)
|
||||
|
||||
combined_len = topk_len + swa_len
|
||||
tl.store(combined_lens_ptr + token_idx, combined_len)
|
||||
@@ -722,6 +748,7 @@ class CombineTopkSwaIndicesKernel(
|
||||
topk: int,
|
||||
compress_ratio: int,
|
||||
WINDOW_SIZE: int,
|
||||
image_width: int = 0,
|
||||
) -> CompileKey:
|
||||
padded_topk = next_power_of_2(topk_width)
|
||||
input_variant = TritonPointerInputVariant.from_alignment(
|
||||
@@ -734,6 +761,7 @@ class CombineTopkSwaIndicesKernel(
|
||||
TOP_K=topk,
|
||||
COMPRESS_RATIO=compress_ratio,
|
||||
WINDOW_SIZE=WINDOW_SIZE,
|
||||
IMAGE_WIDTH=image_width,
|
||||
PADDED_TOP_K=padded_topk,
|
||||
input_variant=input_variant,
|
||||
)
|
||||
@@ -743,10 +771,19 @@ class CombineTopkSwaIndicesKernel(
|
||||
return []
|
||||
|
||||
window_size = _hf_config_int(vllm_config, "sliding_window", 128)
|
||||
image_width = (
|
||||
_hf_config_int(vllm_config, "vision_max_n_token", 0)
|
||||
if _hf_config_int(vllm_config, "vision_n_layers", 0) > 0
|
||||
else 0
|
||||
)
|
||||
# Warm both the plain-window variant (batches without image spans)
|
||||
# and the in-image bidirectional variant.
|
||||
image_widths = [0, image_width] if image_width > 0 else [0]
|
||||
return self._trace_dispatch(self.dispatch)(
|
||||
_DSV4_COMBINE_TOPK_SWA_WARMUP_INPUTS,
|
||||
_COMBINE_TOPK_SWA_POINTER_INPUTS,
|
||||
WINDOW_SIZE=window_size,
|
||||
image_width=image_widths,
|
||||
)
|
||||
|
||||
def compile(self, compile_key: CompileKey) -> None:
|
||||
@@ -763,11 +800,14 @@ class CombineTopkSwaIndicesKernel(
|
||||
input_variant.pointer("query_start_loc", torch.int32),
|
||||
input_variant.pointer("seq_lens", torch.int32),
|
||||
input_variant.pointer("gather_lens", torch.int32),
|
||||
int32_ptr,
|
||||
int32_ptr,
|
||||
1, # do not specialize M
|
||||
1, # do not specialize N
|
||||
TOP_K=compile_key.TOP_K,
|
||||
COMPRESS_RATIO=compile_key.COMPRESS_RATIO,
|
||||
WINDOW_SIZE=compile_key.WINDOW_SIZE,
|
||||
IMAGE_WIDTH=compile_key.IMAGE_WIDTH,
|
||||
PADDED_TOP_K=compile_key.PADDED_TOP_K,
|
||||
grid=(1, _COMBINE_TOPK_SWA_NUM_WORKERS),
|
||||
)
|
||||
@@ -786,8 +826,13 @@ class CombineTopkSwaIndicesKernel(
|
||||
TOP_K: int,
|
||||
COMPRESS_RATIO: int,
|
||||
WINDOW_SIZE: int,
|
||||
left_visible: torch.Tensor | None = None,
|
||||
right_visible: torch.Tensor | None = None,
|
||||
max_image_tokens: int = 0,
|
||||
) -> None:
|
||||
num_reqs = seq_lens.shape[0]
|
||||
has_image = left_visible is not None
|
||||
image_width = max_image_tokens if has_image else 0
|
||||
self.kernel[(num_reqs, _COMBINE_TOPK_SWA_NUM_WORKERS)](
|
||||
combined_indices,
|
||||
combined_indices.stride(0),
|
||||
@@ -797,11 +842,14 @@ class CombineTopkSwaIndicesKernel(
|
||||
query_start_loc,
|
||||
seq_lens,
|
||||
gather_lens,
|
||||
left_visible if has_image else topk_indices,
|
||||
right_visible if has_image else topk_indices,
|
||||
M,
|
||||
N,
|
||||
TOP_K=TOP_K,
|
||||
COMPRESS_RATIO=COMPRESS_RATIO,
|
||||
WINDOW_SIZE=WINDOW_SIZE,
|
||||
IMAGE_WIDTH=image_width,
|
||||
PADDED_TOP_K=next_power_of_2(topk_indices.shape[-1]),
|
||||
)
|
||||
|
||||
@@ -828,20 +876,33 @@ def build_flashinfer_mixed_sparse_indices(
|
||||
decode_is_valid_token: torch.Tensor | None = None,
|
||||
swa_block_span: int | None = None,
|
||||
compressed_block_span: int | None = None,
|
||||
prefill_left_visible: torch.Tensor | None = None,
|
||||
prefill_right_visible: torch.Tensor | None = None,
|
||||
max_image_tokens: int = 0,
|
||||
) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
"""Build the FlashInfer DSV4 sparse-index matrix for decode-first batches.
|
||||
|
||||
Produces ``sparse_indices`` of shape ``[num_tokens, swa_index_width +
|
||||
padded_topk]`` (the first ``swa_index_width`` columns are SWA slot ids, the
|
||||
Produces ``sparse_indices`` of shape ``[num_tokens, swa_total_width +
|
||||
padded_topk]`` (the first ``swa_total_width`` columns are SWA slot ids, the
|
||||
rest are compressed/top-k slot ids) and ``sparse_topk_lens`` (active length
|
||||
per token). Decode tokens read precomputed SWA/compressed indices; prefill
|
||||
tokens derive their SWA window from the position and translate local
|
||||
compressed indices to global slots via the block tables.
|
||||
|
||||
When ``prefill_left_visible``/``prefill_right_visible`` are given (vision
|
||||
variant), the SWA column region widens by ``max_image_tokens`` and prefill
|
||||
tokens inside an image span get a bidirectionally widened window; decode
|
||||
rows are padded with -1 across the extra columns.
|
||||
"""
|
||||
assert decode_swa_indices.dtype == torch.int32
|
||||
assert decode_swa_indices.dim() == 2
|
||||
swa_index_width = decode_swa_indices.shape[-1]
|
||||
assert swa_index_width >= window_size
|
||||
has_image = prefill_left_visible is not None
|
||||
image_width = max_image_tokens if has_image else 0
|
||||
swa_total_width = swa_index_width + image_width
|
||||
if has_image:
|
||||
assert prefill_right_visible is not None
|
||||
if decode_compressed_topk_lens is not None:
|
||||
assert decode_compressed_topk_lens.dtype == torch.int32
|
||||
assert prefill_topk_indices.dtype == torch.int32
|
||||
@@ -890,7 +951,7 @@ def build_flashinfer_mixed_sparse_indices(
|
||||
padded_topk = max(topk, decode_compressed_topk)
|
||||
padded_topk = (padded_topk + 3) // 4 * 4
|
||||
sparse_indices = torch.empty(
|
||||
(num_tokens, swa_index_width + padded_topk),
|
||||
(num_tokens, swa_total_width + padded_topk),
|
||||
dtype=torch.int32,
|
||||
device=decode_swa_indices.device,
|
||||
)
|
||||
@@ -924,6 +985,8 @@ def build_flashinfer_mixed_sparse_indices(
|
||||
decode_is_valid_token,
|
||||
prefill_topk_indices,
|
||||
prefill_topk_indices.stride(0),
|
||||
prefill_left_visible if has_image else token_to_req_indices,
|
||||
prefill_right_visible if has_image else token_to_req_indices,
|
||||
query_start_loc,
|
||||
seq_lens,
|
||||
token_to_req_indices,
|
||||
@@ -938,6 +1001,7 @@ def build_flashinfer_mixed_sparse_indices(
|
||||
NUM_DECODE_TOKENS=num_decode_tokens,
|
||||
WINDOW_SIZE=window_size,
|
||||
SWA_INDEX_WIDTH=swa_index_width,
|
||||
IMAGE_WIDTH=image_width,
|
||||
COMPRESS_RATIO=compress_ratio,
|
||||
TOP_K=topk,
|
||||
PADDED_TOP_K=padded_topk,
|
||||
@@ -992,6 +1056,8 @@ def _build_flashinfer_mixed_sparse_indices_kernel(
|
||||
decode_is_valid_token_ptr,
|
||||
prefill_topk_indices_ptr,
|
||||
prefill_topk_stride,
|
||||
left_visible_ptr,
|
||||
right_visible_ptr,
|
||||
query_start_loc_ptr,
|
||||
seq_lens_ptr,
|
||||
token_to_req_indices_ptr,
|
||||
@@ -1006,6 +1072,7 @@ def _build_flashinfer_mixed_sparse_indices_kernel(
|
||||
NUM_DECODE_TOKENS,
|
||||
WINDOW_SIZE: tl.constexpr,
|
||||
SWA_INDEX_WIDTH: tl.constexpr,
|
||||
IMAGE_WIDTH: tl.constexpr,
|
||||
COMPRESS_RATIO: tl.constexpr,
|
||||
TOP_K: tl.constexpr,
|
||||
PADDED_TOP_K: tl.constexpr,
|
||||
@@ -1017,14 +1084,18 @@ def _build_flashinfer_mixed_sparse_indices_kernel(
|
||||
TOPK_BLOCK_SIZE: tl.constexpr,
|
||||
):
|
||||
token_idx = tl.program_id(0)
|
||||
# Total SWA column count; > SWA_INDEX_WIDTH only for in-image
|
||||
# bidirectional visibility (vision variant), where the extra columns are
|
||||
# -1-padded for decode rows.
|
||||
SWA_TOTAL_WIDTH: tl.constexpr = SWA_INDEX_WIDTH + IMAGE_WIDTH
|
||||
|
||||
if token_idx < NUM_DECODE_TOKENS:
|
||||
for i in range(0, SWA_INDEX_WIDTH, WINDOW_BLOCK_SIZE):
|
||||
for i in range(0, SWA_TOTAL_WIDTH, WINDOW_BLOCK_SIZE):
|
||||
offset = i + tl.arange(0, WINDOW_BLOCK_SIZE)
|
||||
mask = offset < SWA_INDEX_WIDTH
|
||||
mask = offset < SWA_TOTAL_WIDTH
|
||||
values = tl.load(
|
||||
decode_swa_indices_ptr + token_idx * decode_swa_stride + offset,
|
||||
mask=mask,
|
||||
mask=offset < SWA_INDEX_WIDTH,
|
||||
other=-1,
|
||||
)
|
||||
values = _remap_flashinfer_index(values, swa_block_size, swa_block_span)
|
||||
@@ -1067,7 +1138,7 @@ def _build_flashinfer_mixed_sparse_indices_kernel(
|
||||
tl.store(
|
||||
sparse_indices_ptr
|
||||
+ token_idx * sparse_indices_stride
|
||||
+ SWA_INDEX_WIDTH
|
||||
+ SWA_TOTAL_WIDTH
|
||||
+ offset,
|
||||
values,
|
||||
mask=mask,
|
||||
@@ -1081,7 +1152,7 @@ def _build_flashinfer_mixed_sparse_indices_kernel(
|
||||
else:
|
||||
compressed_len = tl.full((), DECODE_COMPRESSED_TOPK, dtype=tl.int32)
|
||||
|
||||
tl.store(sparse_topk_lens_ptr + token_idx, SWA_INDEX_WIDTH + compressed_len)
|
||||
tl.store(sparse_topk_lens_ptr + token_idx, SWA_TOTAL_WIDTH + compressed_len)
|
||||
return
|
||||
|
||||
prefill_idx = token_idx - NUM_DECODE_TOKENS
|
||||
@@ -1093,13 +1164,22 @@ def _build_flashinfer_mixed_sparse_indices_kernel(
|
||||
start_pos = seq_len - query_len
|
||||
token_idx_in_query = token_idx - query_start
|
||||
pos = start_pos + token_idx_in_query
|
||||
swa_len = tl.minimum(pos + 1, WINDOW_SIZE)
|
||||
swa_start_pos = pos - swa_len + 1
|
||||
if IMAGE_WIDTH > 0:
|
||||
# In-image bidirectional visibility: window starts up to
|
||||
# max(left - (window - 1), 0) earlier and extends `right` past pos.
|
||||
left = tl.load(left_visible_ptr + token_idx)
|
||||
right = tl.load(right_visible_ptr + token_idx)
|
||||
else:
|
||||
left = 0
|
||||
right = 0
|
||||
left_add = tl.maximum(left - (WINDOW_SIZE - 1), 0)
|
||||
swa_start_pos = tl.maximum(pos - (WINDOW_SIZE - 1) - left_add, 0)
|
||||
swa_len = pos + right - swa_start_pos + 1
|
||||
topk_len = tl.minimum((pos + 1) // COMPRESS_RATIO, TOP_K)
|
||||
|
||||
for i in range(0, SWA_INDEX_WIDTH, WINDOW_BLOCK_SIZE):
|
||||
for i in range(0, SWA_TOTAL_WIDTH, WINDOW_BLOCK_SIZE):
|
||||
offset = i + tl.arange(0, WINDOW_BLOCK_SIZE)
|
||||
mask = offset < SWA_INDEX_WIDTH
|
||||
mask = offset < SWA_TOTAL_WIDTH
|
||||
pos_offset = swa_start_pos + offset
|
||||
block_indices = pos_offset // swa_block_size
|
||||
block_numbers = tl.load(
|
||||
@@ -1143,10 +1223,10 @@ def _build_flashinfer_mixed_sparse_indices_kernel(
|
||||
tl.store(
|
||||
sparse_indices_ptr
|
||||
+ token_idx * sparse_indices_stride
|
||||
+ SWA_INDEX_WIDTH
|
||||
+ SWA_TOTAL_WIDTH
|
||||
+ offset,
|
||||
slot_ids,
|
||||
mask=mask,
|
||||
)
|
||||
|
||||
tl.store(sparse_topk_lens_ptr + token_idx, SWA_INDEX_WIDTH + topk_len)
|
||||
tl.store(sparse_topk_lens_ptr + token_idx, SWA_TOTAL_WIDTH + topk_len)
|
||||
|
||||
@@ -0,0 +1,232 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""DeepSeek-V4 vision tower (ViT + aligner) with TP-sharded linears.
|
||||
|
||||
Ported from the official reference implementation
|
||||
(deepseek-ai/DeepSeek-V4-Flash-Vision-Exp). Weight names match the HF
|
||||
checkpoint so no renaming is needed at load time. Attention and MLP weights
|
||||
are tensor-parallel sharded (replicated when the vision head count is not
|
||||
divisible by TP size, or under ``--mm-encoder-tp-mode data``); the patch
|
||||
embed and norms are replicated, so the residual stream is full-width on
|
||||
every rank.
|
||||
"""
|
||||
|
||||
from functools import lru_cache
|
||||
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
from torch import nn
|
||||
|
||||
from vllm.distributed import get_tensor_model_parallel_world_size
|
||||
from vllm.model_executor.layers.activation import SiluAndMul
|
||||
from vllm.model_executor.layers.attention.mm_encoder_attention import (
|
||||
MMEncoderAttention,
|
||||
)
|
||||
from vllm.model_executor.layers.linear import (
|
||||
ColumnParallelLinear,
|
||||
MergedColumnParallelLinear,
|
||||
QKVParallelLinear,
|
||||
ReplicatedLinear,
|
||||
RowParallelLinear,
|
||||
)
|
||||
from vllm.model_executor.models.vision import is_vit_use_data_parallel
|
||||
|
||||
|
||||
@lru_cache(8)
|
||||
def get_vision_cos_sin(
|
||||
n_h: int, n_w: int, dim: int, theta: float
|
||||
) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
inv_freq = 1.0 / (theta ** (torch.arange(0, dim, 2, dtype=torch.float32) / dim))
|
||||
hpos = torch.arange(n_h).unsqueeze(1).expand(n_h, n_w)
|
||||
wpos = torch.arange(n_w).unsqueeze(0).expand(n_h, n_w)
|
||||
freqs = torch.stack([hpos, wpos], dim=-1).reshape(-1, 2, 1).float()
|
||||
freqs = (freqs * inv_freq).flatten(1)
|
||||
return freqs.cos().unsqueeze(1), freqs.sin().unsqueeze(1)
|
||||
|
||||
|
||||
def apply_rotary(x: torch.Tensor, cos: torch.Tensor, sin: torch.Tensor) -> torch.Tensor:
|
||||
dtype = x.dtype
|
||||
x1, x2 = x.float().chunk(2, dim=-1)
|
||||
return torch.cat([x1 * cos - x2 * sin, x2 * cos + x1 * sin], dim=-1).to(dtype)
|
||||
|
||||
|
||||
class DeepseekV4RMSNorm(nn.Module):
|
||||
def __init__(self, dim: int, eps: float = 1e-6):
|
||||
super().__init__()
|
||||
self.eps = eps
|
||||
self.weight = nn.Parameter(torch.ones(dim, dtype=torch.float32))
|
||||
|
||||
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
||||
dtype = x.dtype
|
||||
x = x.float()
|
||||
x = x * torch.rsqrt(x.square().mean(-1, keepdim=True) + self.eps)
|
||||
return (self.weight * x).to(dtype)
|
||||
|
||||
|
||||
class DeepseekV4PatchEmbed(nn.Module):
|
||||
def __init__(self, config):
|
||||
super().__init__()
|
||||
# Replicated: the residual stream is full-width on every rank.
|
||||
self.proj = ReplicatedLinear(
|
||||
3 * config.vision_patch_size**2,
|
||||
config.vision_dim,
|
||||
bias=True,
|
||||
quant_config=None,
|
||||
)
|
||||
|
||||
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
||||
out, _ = self.proj(x.flatten(1))
|
||||
return out
|
||||
|
||||
|
||||
class DeepseekV4VisionAttention(nn.Module):
|
||||
def __init__(self, config, prefix: str = ""):
|
||||
super().__init__()
|
||||
# Convention from minimax_m3/siglip: compute the TP/DP choice locally
|
||||
# instead of threading a flag through every constructor.
|
||||
use_data_parallel = is_vit_use_data_parallel(config.vision_n_heads)
|
||||
self.tp_size = (
|
||||
1 if use_data_parallel else get_tensor_model_parallel_world_size()
|
||||
)
|
||||
self.n_heads = config.vision_n_heads // self.tp_size
|
||||
self.head_dim = config.vision_dim // config.vision_n_heads
|
||||
self.hidden_size = config.vision_dim
|
||||
self.wqkv = QKVParallelLinear(
|
||||
config.vision_dim,
|
||||
self.head_dim,
|
||||
config.vision_n_heads,
|
||||
bias=True,
|
||||
quant_config=None,
|
||||
prefix=f"{prefix}.wqkv",
|
||||
disable_tp=use_data_parallel,
|
||||
)
|
||||
self.wo = RowParallelLinear(
|
||||
config.vision_dim,
|
||||
config.vision_dim,
|
||||
bias=True,
|
||||
quant_config=None,
|
||||
prefix=f"{prefix}.wo",
|
||||
disable_tp=use_data_parallel,
|
||||
)
|
||||
self.attn = MMEncoderAttention(
|
||||
num_heads=self.n_heads,
|
||||
head_size=self.head_dim,
|
||||
prefix=f"{prefix}.attn",
|
||||
)
|
||||
|
||||
def forward(
|
||||
self, x: torch.Tensor, cos: torch.Tensor, sin: torch.Tensor
|
||||
) -> torch.Tensor:
|
||||
n = x.size(0)
|
||||
qkv, _ = self.wqkv(x)
|
||||
q, k, v = (t.view(n, self.n_heads, self.head_dim) for t in qkv.chunk(3, -1))
|
||||
q = apply_rotary(q, cos, sin).unsqueeze(0) # (b=1, n, h, d)
|
||||
k = apply_rotary(k, cos, sin).unsqueeze(0)
|
||||
# One image per call: a dense batch, no varlen packing metadata.
|
||||
o = self.attn(q, k, v.unsqueeze(0))
|
||||
out, _ = self.wo(o.reshape(n, -1))
|
||||
return out
|
||||
|
||||
|
||||
class DeepseekV4VisionMLP(nn.Module):
|
||||
def __init__(self, config, prefix: str = ""):
|
||||
super().__init__()
|
||||
use_data_parallel = is_vit_use_data_parallel(config.vision_n_heads)
|
||||
self.w1 = MergedColumnParallelLinear(
|
||||
config.vision_dim,
|
||||
[config.vision_inter_dim] * 2,
|
||||
bias=False,
|
||||
quant_config=None,
|
||||
prefix=f"{prefix}.w1",
|
||||
disable_tp=use_data_parallel,
|
||||
)
|
||||
self.w2 = RowParallelLinear(
|
||||
config.vision_inter_dim,
|
||||
config.vision_dim,
|
||||
bias=False,
|
||||
quant_config=None,
|
||||
prefix=f"{prefix}.w2",
|
||||
disable_tp=use_data_parallel,
|
||||
)
|
||||
self.act_fn = SiluAndMul()
|
||||
|
||||
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
||||
gate_up, _ = self.w1(x)
|
||||
out, _ = self.w2(self.act_fn(gate_up))
|
||||
return out
|
||||
|
||||
|
||||
class DeepseekV4VisionBlock(nn.Module):
|
||||
def __init__(self, config, prefix: str = ""):
|
||||
super().__init__()
|
||||
self.norm1 = DeepseekV4RMSNorm(config.vision_dim)
|
||||
self.attn = DeepseekV4VisionAttention(config, prefix=f"{prefix}.attn")
|
||||
self.norm2 = DeepseekV4RMSNorm(config.vision_dim)
|
||||
self.mlp = DeepseekV4VisionMLP(config, prefix=f"{prefix}.mlp")
|
||||
|
||||
def forward(
|
||||
self, x: torch.Tensor, cos: torch.Tensor, sin: torch.Tensor
|
||||
) -> torch.Tensor:
|
||||
x = x + self.attn(self.norm1(x), cos, sin)
|
||||
return x + self.mlp(self.norm2(x))
|
||||
|
||||
|
||||
class DeepseekV4ViT(nn.Module):
|
||||
"""DeepSeek-V4 ViT: full bidirectional attention per image, 2D RoPE."""
|
||||
|
||||
def __init__(self, config):
|
||||
super().__init__()
|
||||
self.rope_dim = config.vision_dim // config.vision_n_heads // 2
|
||||
self.rope_theta = config.vision_rope_theta
|
||||
self.patch_embed = DeepseekV4PatchEmbed(config)
|
||||
self.blocks = nn.ModuleList(
|
||||
[
|
||||
DeepseekV4VisionBlock(config, prefix=f"blocks.{i}")
|
||||
for i in range(config.vision_n_layers)
|
||||
]
|
||||
)
|
||||
self.norm = DeepseekV4RMSNorm(config.vision_dim)
|
||||
|
||||
def forward(
|
||||
self, patches: torch.Tensor, n_vit_h: int, n_vit_w: int
|
||||
) -> torch.Tensor:
|
||||
x = self.patch_embed(patches)
|
||||
cos, sin = get_vision_cos_sin(n_vit_h, n_vit_w, self.rope_dim, self.rope_theta)
|
||||
cos = cos.to(device=x.device)
|
||||
sin = sin.to(device=x.device)
|
||||
for block in self.blocks:
|
||||
x = block(x, cos, sin)
|
||||
return self.norm(x)
|
||||
|
||||
|
||||
class DeepseekV4Aligner(nn.Module):
|
||||
"""Spatial merge (downsample_ratio x downsample_ratio) + MLP projector."""
|
||||
|
||||
def __init__(self, config):
|
||||
super().__init__()
|
||||
use_data_parallel = is_vit_use_data_parallel(config.vision_n_heads)
|
||||
self.downsample_ratio = config.vision_downsample_ratio
|
||||
in_dim = config.vision_dim * self.downsample_ratio**2
|
||||
self.w1 = ColumnParallelLinear(
|
||||
in_dim,
|
||||
config.hidden_size,
|
||||
bias=True,
|
||||
quant_config=None,
|
||||
disable_tp=use_data_parallel,
|
||||
)
|
||||
self.w2 = RowParallelLinear(
|
||||
config.hidden_size,
|
||||
config.hidden_size,
|
||||
bias=True,
|
||||
quant_config=None,
|
||||
disable_tp=use_data_parallel,
|
||||
)
|
||||
|
||||
def forward(self, x: torch.Tensor, n_vit_h: int, n_vit_w: int) -> torch.Tensor:
|
||||
r = self.downsample_ratio
|
||||
x = x.view(n_vit_h, n_vit_w, -1).permute(2, 0, 1)
|
||||
x = F.pad(x, (0, -n_vit_w % r, 0, -n_vit_h % r))
|
||||
x = F.unfold(x.unsqueeze(0), r, stride=r).squeeze(0).transpose(0, 1)
|
||||
hidden, _ = self.w1(x)
|
||||
out, _ = self.w2(F.gelu(hidden))
|
||||
return out
|
||||
@@ -435,6 +435,10 @@ class DeepseekV4FlashInferMLAAttention(DeepseekV4Attention):
|
||||
decode_is_valid_token=decode_is_valid_token,
|
||||
swa_block_span=swa_block_span,
|
||||
compressed_block_span=compressed_block_span,
|
||||
prefill_left_visible=swa_metadata.prefill_left_visible,
|
||||
prefill_right_visible=swa_metadata.prefill_right_visible,
|
||||
# getattr for tests that bypass __init__ via object.__new__.
|
||||
max_image_tokens=getattr(self, "max_image_tokens", 0),
|
||||
)
|
||||
if cache_key != "c4a":
|
||||
swa_metadata.flashinfer_sparse_index_cache[cache_key] = (
|
||||
|
||||
@@ -119,7 +119,9 @@ class DeepseekV4FlashMLAAttention(DeepseekV4Attention):
|
||||
else:
|
||||
assert self.topk_indices_buffer is not None
|
||||
top_k = self.topk_indices_buffer.shape[-1]
|
||||
combined_topk = round_up(top_k + self.window_size, 128)
|
||||
combined_topk = round_up(
|
||||
top_k + self.window_size + self.max_image_tokens, 128
|
||||
)
|
||||
current_workspace_manager().get_simultaneous(
|
||||
((self.PREFILL_CHUNK_SIZE, M, q.shape[-1]), torch.bfloat16),
|
||||
((self.max_num_batched_tokens, combined_topk), torch.int32),
|
||||
@@ -311,7 +313,7 @@ class DeepseekV4FlashMLAAttention(DeepseekV4Attention):
|
||||
)
|
||||
assert chunk_plan, "prefill chunk plan must be non-empty when num_prefills > 0"
|
||||
workspace_manager = current_workspace_manager()
|
||||
combined_topk = round_up(top_k + self.window_size, 128)
|
||||
combined_topk = round_up(top_k + self.window_size + self.max_image_tokens, 128)
|
||||
for chunk_start, chunk_end, chunk_N, chunk_M in chunk_plan:
|
||||
chunk_size = chunk_end - chunk_start
|
||||
workspace = workspace_manager.get_simultaneous(
|
||||
@@ -369,6 +371,21 @@ class DeepseekV4FlashMLAAttention(DeepseekV4Attention):
|
||||
chunk_M,
|
||||
chunk_N,
|
||||
out=(combined_indices_out, combined_lens_out),
|
||||
left_visible=(
|
||||
swa_metadata.prefill_left_visible[
|
||||
num_decode_tokens + query_start : num_decode_tokens + query_end
|
||||
]
|
||||
if swa_metadata.prefill_left_visible is not None
|
||||
else None
|
||||
),
|
||||
right_visible=(
|
||||
swa_metadata.prefill_right_visible[
|
||||
num_decode_tokens + query_start : num_decode_tokens + query_end
|
||||
]
|
||||
if swa_metadata.prefill_right_visible is not None
|
||||
else None
|
||||
),
|
||||
max_image_tokens=self.max_image_tokens,
|
||||
)
|
||||
flash_mla_sparse_fwd(
|
||||
q=q[query_start:query_end],
|
||||
|
||||
@@ -92,6 +92,8 @@ from vllm.utils.math_utils import cdiv
|
||||
from vllm.v1.attention.backends.registry import AttentionBackendEnum
|
||||
from vllm.v1.worker.ubatching import dbo_current_ubatch_id
|
||||
|
||||
from ..common.mm_preprocess import IMAGE_SENTINEL_BASE_ID
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
|
||||
@@ -798,6 +800,12 @@ class DeepseekV4MoE(nn.Module):
|
||||
|
||||
self.gate.e_score_correction_bias = None
|
||||
self.gate.tid2eid = None
|
||||
self.gate.bias_vl = None
|
||||
# Image tokens borrow five consecutive reserved in-vocab ids starting
|
||||
# at IMAGE_SENTINEL_BASE_ID; 0 disables vision routing (text model).
|
||||
self.image_sentinel_lo = (
|
||||
IMAGE_SENTINEL_BASE_ID if getattr(config, "vision_n_layers", 0) > 0 else 0
|
||||
)
|
||||
is_hash_moe = extract_layer_index(prefix) < config.num_hash_layers
|
||||
self.hash_indices_dtype = torch.int64 if self.use_mega_moe else torch.int32
|
||||
if is_hash_moe:
|
||||
@@ -813,12 +821,25 @@ class DeepseekV4MoE(nn.Module):
|
||||
),
|
||||
requires_grad=False,
|
||||
)
|
||||
elif getattr(config, "topk_method", None) == "noaux_tc":
|
||||
if getattr(config, "topk_method", None) == "noaux_tc" and (
|
||||
not is_hash_moe or getattr(config, "vision_n_layers", 0) > 0
|
||||
):
|
||||
# Vision checkpoints ship a gate bias on hash layers too (it is
|
||||
# unused for routing there; image tokens use bias_vl instead).
|
||||
self.gate.e_score_correction_bias = nn.Parameter(
|
||||
torch.empty(config.n_routed_experts, dtype=torch.float32),
|
||||
requires_grad=False,
|
||||
)
|
||||
|
||||
if getattr(config, "vision_n_layers", 0) > 0:
|
||||
# Vision checkpoints route image sentinel tokens with bias_vl
|
||||
# instead of e_score_correction_bias / the hash table. Created on
|
||||
# every MoE layer, hash layers included.
|
||||
self.gate.bias_vl = nn.Parameter(
|
||||
torch.empty(config.n_routed_experts, dtype=torch.float32),
|
||||
requires_grad=False,
|
||||
)
|
||||
|
||||
if config.n_shared_experts is None:
|
||||
self.shared_experts = None
|
||||
else:
|
||||
@@ -950,6 +971,8 @@ class DeepseekV4MoE(nn.Module):
|
||||
routed_scaling_factor=self.routed_scaling_factor,
|
||||
e_score_correction_bias=self.gate.e_score_correction_bias,
|
||||
hash_indices_table=self.gate.tid2eid,
|
||||
bias_vl=getattr(self.gate, "bias_vl", None),
|
||||
image_sentinel_lo=self.image_sentinel_lo,
|
||||
swiglu_limit=self.swiglu_limit,
|
||||
router_logits_dtype=torch.float32,
|
||||
enable_eplb=parallel_config.enable_eplb,
|
||||
@@ -962,6 +985,10 @@ class DeepseekV4MoE(nn.Module):
|
||||
) -> torch.Tensor:
|
||||
if self.gate.tid2eid is not None and input_ids is None:
|
||||
raise ValueError("DeepSeek V4 hash MoE routing requires input_ids.")
|
||||
# getattr for test doubles that fake the gate module.
|
||||
bias_vl = getattr(self.gate, "bias_vl", None)
|
||||
if bias_vl is not None and input_ids is None:
|
||||
raise ValueError("DeepSeek V4 vision MoE routing requires input_ids.")
|
||||
|
||||
if not self.use_mega_moe:
|
||||
return self._forward_fused_moe(hidden_states, input_ids)
|
||||
@@ -981,6 +1008,8 @@ class DeepseekV4MoE(nn.Module):
|
||||
input_tokens=input_ids,
|
||||
hash_indices_table=self.gate.tid2eid,
|
||||
routed_scaling_factor=self.routed_scaling_factor,
|
||||
bias_vl=bias_vl.data if bias_vl is not None else None,
|
||||
image_sentinel_lo=self.image_sentinel_lo if bias_vl is not None else 0,
|
||||
)
|
||||
activation_clamp = (
|
||||
float(self.swiglu_limit) if self.swiglu_limit is not None else None
|
||||
|
||||
@@ -178,7 +178,7 @@ class DeepSeekV4MultiTokenPredictorLayer(nn.Module):
|
||||
inputs_embeds
|
||||
).unsqueeze(-2)
|
||||
hidden_states, residual, post_mix, res_mix = self.mtp_block(
|
||||
positions=positions, x=hidden_states, input_ids=None
|
||||
positions=positions, x=hidden_states, input_ids=input_ids
|
||||
)
|
||||
hidden_states = mhc_post_tilelang(hidden_states, residual, post_mix, res_mix)
|
||||
if self.mtp_block.use_sequence_parallel:
|
||||
|
||||
@@ -0,0 +1,333 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""DeepSeek-V4 vision variant (e.g. DeepSeek-V4-Flash-Vision-Exp).
|
||||
|
||||
Thin multimodal wrapper around the text-only ``DeepseekV4ForCausalLM``:
|
||||
|
||||
- ``vision`` ViT + ``aligner`` produce per-image embeddings for the IMAGE
|
||||
sentinel positions; four learned vectors (``image_start`` / ``image_pad`` /
|
||||
``image_newline`` / ``image_end``) fill the remaining sentinel positions.
|
||||
- Image placeholders (``<|deepseek_image|>``) are expanded by the processor
|
||||
in ``common/mm_preprocess.py`` into sentinel blocks borrowing reserved
|
||||
in-vocab tokens ``<|place_holder_mm_span_0431|>``..``_0435|>``
|
||||
(see ``common/vision.py`` for the tower itself).
|
||||
- Merged embeddings enter the text model via ``inputs_embeds``, i.e. before
|
||||
its hyper-connection stream expansion. Raw ``input_ids`` still flow into
|
||||
the model so the MoE router can apply ``bias_vl`` to image tokens
|
||||
(``requires_raw_input_tokens``).
|
||||
"""
|
||||
|
||||
from collections.abc import Iterable
|
||||
|
||||
import torch
|
||||
from torch import nn
|
||||
|
||||
from vllm.model_executor.models.interfaces import (
|
||||
MultiModalEmbeddings,
|
||||
SupportsEagle3,
|
||||
SupportsMultiModal,
|
||||
SupportsPP,
|
||||
)
|
||||
from vllm.model_executor.models.utils import (
|
||||
AutoWeightsLoader,
|
||||
WeightsMapper,
|
||||
init_vllm_registered_model,
|
||||
maybe_prefix,
|
||||
)
|
||||
from vllm.multimodal import MULTIMODAL_REGISTRY
|
||||
|
||||
from ..common.mm_preprocess import (
|
||||
IMAGE_PLACEHOLDER,
|
||||
IMAGE_SENTINEL_BASE_ID,
|
||||
DeepseekV4VLDummyInputsBuilder,
|
||||
DeepseekV4VLMultiModalProcessor,
|
||||
DeepseekV4VLProcessingInfo,
|
||||
image_sentinel_mask,
|
||||
)
|
||||
from ..common.vision import DeepseekV4Aligner, DeepseekV4ViT
|
||||
from .model import _make_deepseek_v4_weights_mapper
|
||||
|
||||
|
||||
def _make_deepseek_v4_vl_weights_mapper(
|
||||
expert_dtype: str, image_enabled: bool
|
||||
) -> WeightsMapper:
|
||||
"""Text-checkpoint mapping rules re-rooted under ``language_model.``."""
|
||||
base = _make_deepseek_v4_weights_mapper(expert_dtype)
|
||||
orig_to_new_prefix: dict[str, str | None] = {
|
||||
"layers.": "language_model.model.layers.",
|
||||
"embed.": "language_model.model.embed.",
|
||||
"norm.": "language_model.model.norm.",
|
||||
"hc_head": "language_model.model.hc_head",
|
||||
"mtp.": "language_model.model.mtp.",
|
||||
}
|
||||
if not image_enabled:
|
||||
orig_to_new_prefix.update({"vision.": None, "aligner.": None, "image_": None})
|
||||
return WeightsMapper(
|
||||
orig_to_new_prefix=orig_to_new_prefix,
|
||||
orig_to_new_regex=base.orig_to_new_regex,
|
||||
orig_to_new_suffix={
|
||||
"head.weight": "language_model.lm_head.weight",
|
||||
"embed.weight": "embed_tokens.weight",
|
||||
".ffn.gate.bias": ".ffn.gate.e_score_correction_bias",
|
||||
},
|
||||
orig_to_new_substr={
|
||||
".shared_experts.w2": ".shared_experts.down_proj",
|
||||
# The MTP/DSpark draft heads are not supported for the vision
|
||||
# variant; drop their weights.
|
||||
"mtp.": None,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@MULTIMODAL_REGISTRY.register_processor(
|
||||
DeepseekV4VLMultiModalProcessor,
|
||||
info=DeepseekV4VLProcessingInfo,
|
||||
dummy_inputs=DeepseekV4VLDummyInputsBuilder,
|
||||
)
|
||||
class DeepseekV4ForConditionalGeneration(
|
||||
nn.Module, SupportsMultiModal, SupportsPP, SupportsEagle3
|
||||
):
|
||||
"""Multimodal entry point for DeepSeek-V4 checkpoints with a vision tower.
|
||||
|
||||
``SupportsEagle3`` (aux hidden-state plumbing for MTP/DSpark drafters)
|
||||
delegates through ``language_model`` via the protocol defaults.
|
||||
"""
|
||||
|
||||
# The MoE router needs raw token ids to detect image sentinel tokens
|
||||
# (borrowed reserved ids, see common/mm_preprocess.py) and apply bias_vl.
|
||||
requires_raw_input_tokens = True
|
||||
|
||||
@classmethod
|
||||
def get_placeholder_str(cls, modality: str, i: int) -> str | None:
|
||||
if modality == "image":
|
||||
return IMAGE_PLACEHOLDER
|
||||
raise ValueError(f"Unsupported modality: {modality!r}")
|
||||
|
||||
def __init__(self, *, vllm_config, prefix: str = "") -> None:
|
||||
super().__init__()
|
||||
model_config = vllm_config.model_config
|
||||
config = model_config.hf_config
|
||||
self.config = config
|
||||
self.multimodal_config = model_config.multimodal_config
|
||||
assert self.multimodal_config is not None
|
||||
|
||||
image_enabled = (
|
||||
config.vision_n_layers > 0
|
||||
and self.multimodal_config.get_limit_per_prompt("image") > 0
|
||||
)
|
||||
with self._mark_tower_model(vllm_config, {"image"}):
|
||||
self.vision: DeepseekV4ViT | None = None
|
||||
self.aligner: DeepseekV4Aligner | None = None
|
||||
self.image_start: nn.Parameter | None = None
|
||||
self.image_end: nn.Parameter | None = None
|
||||
self.image_newline: nn.Parameter | None = None
|
||||
self.image_pad: nn.Parameter | None = None
|
||||
if image_enabled:
|
||||
self.vision = DeepseekV4ViT(config)
|
||||
self.aligner = DeepseekV4Aligner(config)
|
||||
for name in (
|
||||
"image_start",
|
||||
"image_end",
|
||||
"image_newline",
|
||||
"image_pad",
|
||||
):
|
||||
setattr(
|
||||
self,
|
||||
name,
|
||||
nn.Parameter(
|
||||
torch.empty(config.hidden_size, dtype=torch.float32)
|
||||
),
|
||||
)
|
||||
self.vision.to(dtype=model_config.dtype)
|
||||
self.aligner.to(dtype=model_config.dtype)
|
||||
|
||||
with self._mark_language_model(vllm_config):
|
||||
# The arch convertor routes any config with a vision tower to
|
||||
# this wrapper class; mark the copy handed to the text backbone
|
||||
# so it resolves to DeepseekV4ForCausalLM instead of recursing
|
||||
# (with_hf_config deepcopies the config, the marker survives).
|
||||
config._dsv4_vl_inner = True # type: ignore[attr-defined]
|
||||
try:
|
||||
self.language_model = init_vllm_registered_model(
|
||||
vllm_config=vllm_config,
|
||||
hf_config=config,
|
||||
prefix=maybe_prefix(prefix, "language_model"),
|
||||
architectures=["DeepseekV4ForCausalLM"],
|
||||
)
|
||||
finally:
|
||||
del config._dsv4_vl_inner # type: ignore[attr-defined]
|
||||
# The outer mapper (see load_weights) fully resolves HF names into
|
||||
# this wrapper's namespace before AutoWeightsLoader strips the
|
||||
# "language_model." prefix and delegates to the child's load_weights,
|
||||
# so the child's own mapper must be a no-op. Its suffix rules are not
|
||||
# idempotent (e.g. "lm_head.weight".endswith("head.weight") would
|
||||
# re-fire "head.weight" -> "lm_head.weight").
|
||||
self.language_model.hf_to_vllm_mapper = WeightsMapper()
|
||||
self.make_empty_intermediate_tensors = ( # type: ignore[method-assign]
|
||||
self.language_model.make_empty_intermediate_tensors
|
||||
)
|
||||
|
||||
expert_dtype = getattr(config, "expert_dtype", "fp4")
|
||||
self.hf_to_vllm_mapper = _make_deepseek_v4_vl_weights_mapper(
|
||||
expert_dtype, image_enabled
|
||||
)
|
||||
|
||||
def _parse_and_validate_image_input(self, **kwargs: object) -> dict | None:
|
||||
patches = kwargs.pop("patches", None)
|
||||
if patches is None:
|
||||
return None
|
||||
vit_grid = kwargs.pop("vit_grid", None)
|
||||
llm_grid = kwargs.pop("llm_grid", None)
|
||||
perm = kwargs.pop("perm", None)
|
||||
assert vit_grid is not None and llm_grid is not None and perm is not None
|
||||
return {
|
||||
"patches": patches,
|
||||
"vit_grid": vit_grid,
|
||||
"llm_grid": llm_grid,
|
||||
"perm": perm,
|
||||
}
|
||||
|
||||
def _encode_image(
|
||||
self,
|
||||
patches: torch.Tensor,
|
||||
n_vit_h: int,
|
||||
n_vit_w: int,
|
||||
perm: torch.Tensor,
|
||||
) -> torch.Tensor:
|
||||
assert self.vision is not None and self.aligner is not None
|
||||
image_embeds = self.aligner(
|
||||
self.vision(patches, n_vit_h, n_vit_w), n_vit_h, n_vit_w
|
||||
)
|
||||
# Reorder into the N-layout block order used in the prompt.
|
||||
return image_embeds[perm.to(image_embeds.device)]
|
||||
|
||||
def _process_image_input(
|
||||
self,
|
||||
patches: torch.Tensor,
|
||||
vit_grid: torch.Tensor,
|
||||
llm_grid: torch.Tensor,
|
||||
perm: torch.Tensor,
|
||||
) -> tuple[torch.Tensor, ...]:
|
||||
assert self.vision is not None and self.aligner is not None
|
||||
patches = patches.to(self.aligner.w1.weight.dtype)
|
||||
|
||||
embeds: list[torch.Tensor] = []
|
||||
vit_offset = 0
|
||||
llm_offset = 0
|
||||
for (n_vit_h, n_vit_w), (n_llm_h, n_llm_w) in zip(
|
||||
vit_grid.tolist(), llm_grid.tolist(), strict=True
|
||||
):
|
||||
n_vit = n_vit_h * n_vit_w
|
||||
n_llm = n_llm_h * n_llm_w
|
||||
embeds.append(
|
||||
self._encode_image(
|
||||
patches[vit_offset : vit_offset + n_vit],
|
||||
n_vit_h,
|
||||
n_vit_w,
|
||||
perm[llm_offset : llm_offset + n_llm],
|
||||
)
|
||||
)
|
||||
vit_offset += n_vit
|
||||
llm_offset += n_llm
|
||||
return tuple(embeds)
|
||||
|
||||
def embed_multimodal(self, **kwargs: object) -> MultiModalEmbeddings:
|
||||
image_input = self._parse_and_validate_image_input(**kwargs)
|
||||
if image_input is None or self.vision is None:
|
||||
return []
|
||||
return self._process_image_input(
|
||||
image_input["patches"],
|
||||
image_input["vit_grid"],
|
||||
image_input["llm_grid"],
|
||||
image_input["perm"],
|
||||
)
|
||||
|
||||
def embed_input_ids(
|
||||
self,
|
||||
input_ids: torch.Tensor,
|
||||
multimodal_embeddings: MultiModalEmbeddings | None = None,
|
||||
*,
|
||||
is_multimodal: torch.Tensor | None = None,
|
||||
) -> torch.Tensor:
|
||||
from vllm.model_executor.models.utils import _merge_multimodal_embeddings
|
||||
|
||||
# All ids are in-vocab here: image-block sentinels are borrowed
|
||||
# reserved tokens (their embedding rows are always overwritten below).
|
||||
inputs_embeds = self.language_model.embed_input_ids(input_ids)
|
||||
|
||||
if self.image_start is not None:
|
||||
# Branch-free sentinel overwrite: safe inside compiled/captured
|
||||
# regions (no data-dependent control flow).
|
||||
sentinel_mask = image_sentinel_mask(input_ids)
|
||||
if is_multimodal is not None:
|
||||
# IMAGE positions get vision embeddings via the merge below.
|
||||
sentinel_mask = sentinel_mask & ~is_multimodal.to(input_ids.device)
|
||||
table = torch.stack(
|
||||
[
|
||||
self.image_start,
|
||||
self.image_pad,
|
||||
self.image_pad,
|
||||
self.image_newline,
|
||||
self.image_end,
|
||||
]
|
||||
).to(inputs_embeds.dtype)
|
||||
idx = (input_ids - IMAGE_SENTINEL_BASE_ID).clamp(0, 4)
|
||||
inputs_embeds = torch.where(
|
||||
sentinel_mask.unsqueeze(-1), table[idx], inputs_embeds
|
||||
)
|
||||
|
||||
if multimodal_embeddings is None or len(multimodal_embeddings) == 0:
|
||||
return inputs_embeds
|
||||
|
||||
assert is_multimodal is not None
|
||||
return _merge_multimodal_embeddings(
|
||||
inputs_embeds=inputs_embeds,
|
||||
multimodal_embeddings=multimodal_embeddings,
|
||||
is_multimodal=is_multimodal,
|
||||
)
|
||||
|
||||
def forward(
|
||||
self,
|
||||
input_ids: torch.Tensor,
|
||||
positions: torch.Tensor,
|
||||
intermediate_tensors=None,
|
||||
inputs_embeds: torch.Tensor | None = None,
|
||||
**kwargs,
|
||||
) -> torch.Tensor:
|
||||
return self.language_model(
|
||||
input_ids, positions, intermediate_tensors, inputs_embeds
|
||||
)
|
||||
|
||||
def compute_logits(self, hidden_states: torch.Tensor) -> torch.Tensor | None:
|
||||
return self.language_model.compute_logits(hidden_states)
|
||||
|
||||
def compute_logits_local(self, hidden_states: torch.Tensor) -> torch.Tensor:
|
||||
return self.language_model.compute_logits_local(hidden_states)
|
||||
|
||||
def get_expert_mapping(self) -> list[tuple[str, str, int, str]]:
|
||||
return self.language_model.get_expert_mapping()
|
||||
|
||||
def get_mtp_target_hidden_states(self) -> torch.Tensor | None:
|
||||
"""Pre-hc_head residual stream buffer for the MTP/DSpark draft model."""
|
||||
return self.language_model.get_mtp_target_hidden_states()
|
||||
|
||||
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
|
||||
# Map HF names into this wrapper's namespace up front and sort, so
|
||||
# the "language_model." group reaches the child loader as one
|
||||
# contiguous block (AutoWeightsLoader delegates per contiguous group,
|
||||
# and the child's load_weights finalizes fused expert weights, which
|
||||
# must not run on a partially loaded model).
|
||||
mapped = sorted(self.hf_to_vllm_mapper.apply(weights), key=lambda x: x[0])
|
||||
loader = AutoWeightsLoader(self)
|
||||
loaded_params = loader.load_weights(mapped)
|
||||
# The child's load_weights already ran its post-load finalization.
|
||||
self._weights_finalized = True
|
||||
return loaded_params
|
||||
|
||||
def process_weights_after_loading(self) -> None:
|
||||
# Model-level post-load hook (called by the loader after any load
|
||||
# format). Under DummyModelLoader the child's load_weights — and
|
||||
# hence its finalize step — is bypassed, so run it here instead.
|
||||
if getattr(self, "_weights_finalized", False):
|
||||
return
|
||||
self.language_model.process_weights_after_loading()
|
||||
@@ -0,0 +1,14 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""Stub for platforms where the DeepSeek-V4 vision variant is unsupported."""
|
||||
|
||||
from torch import nn
|
||||
|
||||
|
||||
class DeepseekV4ForConditionalGeneration(nn.Module):
|
||||
def __init__(self, *, vllm_config, prefix: str = ""):
|
||||
super().__init__()
|
||||
raise NotImplementedError(
|
||||
"DeepSeek-V4 vision (DeepseekV4ForConditionalGeneration) is only "
|
||||
"supported on NVIDIA GPUs for now."
|
||||
)
|
||||
@@ -28,6 +28,10 @@ USER_SP_TOKEN = "<|User|>"
|
||||
ASSISTANT_SP_TOKEN = "<|Assistant|>"
|
||||
LATEST_REMINDER_SP_TOKEN = "<|latest_reminder|>"
|
||||
|
||||
# Placeholder text inlined at each image's position; the multimodal processor
|
||||
# later expands it into the sentinel token block.
|
||||
IMAGE_PLACEHOLDER = "<|deepseek_image|>"
|
||||
|
||||
# Task special tokens for internal classification tasks
|
||||
DS_TASK_SP_TOKENS = {
|
||||
"action": "<|action|>",
|
||||
@@ -199,6 +203,30 @@ def find_last_user_index(messages: List[Dict[str, Any]]) -> int:
|
||||
# Message Rendering
|
||||
# ============================================================
|
||||
|
||||
def flatten_content_blocks(content: Any) -> Any:
|
||||
"""Flatten OpenAI-style content blocks to plain text.
|
||||
|
||||
Image blocks are inlined as IMAGE_PLACEHOLDER at their position. The image
|
||||
data itself travels out of band (multi_modal_data) and is matched to
|
||||
placeholders by order. Plain-string content passes through unchanged.
|
||||
"""
|
||||
if not isinstance(content, list):
|
||||
return content
|
||||
parts: List[str] = []
|
||||
for block in content:
|
||||
if not isinstance(block, dict):
|
||||
parts.append(str(block))
|
||||
elif block.get("type") in ("image", "image_url"):
|
||||
parts.append(IMAGE_PLACEHOLDER)
|
||||
elif block.get("type") == "text":
|
||||
parts.append(block.get("text", ""))
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Unsupported content block type: {block.get('type')!r}"
|
||||
)
|
||||
return "".join(parts)
|
||||
|
||||
|
||||
def render_message(
|
||||
index: int,
|
||||
messages: List[Dict[str, Any]],
|
||||
@@ -292,6 +320,8 @@ def render_message(
|
||||
block_type = block.get("type")
|
||||
if block_type == "text":
|
||||
parts.append(block.get("text", ""))
|
||||
elif block_type in ("image", "image_url"):
|
||||
parts.append(IMAGE_PLACEHOLDER)
|
||||
elif block_type == "tool_result":
|
||||
tool_content = block.get("content", "")
|
||||
if isinstance(tool_content, list):
|
||||
@@ -307,7 +337,7 @@ def render_message(
|
||||
parts.append(f"[Unsupported {block_type}]")
|
||||
prompt += "\n\n".join(parts)
|
||||
else:
|
||||
prompt += content or ""
|
||||
prompt += flatten_content_blocks(content) or ""
|
||||
|
||||
elif role == "latest_reminder":
|
||||
prompt += LATEST_REMINDER_SP_TOKEN + latest_reminder_msg_template.format(content=content)
|
||||
@@ -447,13 +477,15 @@ def merge_tool_messages(messages: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
||||
"content_blocks": [tool_block],
|
||||
})
|
||||
elif role == "user":
|
||||
text_block = {"type": "text", "text": msg.get("content", "")}
|
||||
# Flatten OpenAI-style content blocks (images become placeholders)
|
||||
text_content = flatten_content_blocks(msg.get("content", ""))
|
||||
text_block = {"type": "text", "text": text_content}
|
||||
if merged and merged[-1].get("role") == "user" and "content_blocks" in merged[-1] and merged[-1].get("task") is None:
|
||||
merged[-1]["content_blocks"].append(text_block)
|
||||
else:
|
||||
new_msg = {
|
||||
"role": "user",
|
||||
"content": msg.get("content", ""),
|
||||
"content": text_content,
|
||||
"content_blocks": [text_block],
|
||||
}
|
||||
# Preserve extra fields (task, wo_eos, mask, etc.)
|
||||
|
||||
@@ -14,10 +14,39 @@ class DeepseekV4Config(PretrainedConfig):
|
||||
rope_scaling: dict[str, Any] | None = None,
|
||||
rope_parameters: dict[str, Any] | None = None,
|
||||
rope_theta: float = 10000.0,
|
||||
vision_n_layers: int = 0,
|
||||
vision_dim: int = 1024,
|
||||
vision_n_heads: int = 16,
|
||||
vision_inter_dim: int = 2816,
|
||||
vision_patch_size: int = 14,
|
||||
vision_rope_theta: float = 10000.0,
|
||||
vision_downsample_ratio: int = 3,
|
||||
vision_max_n_token: int = 384,
|
||||
vision_min_pixels: int = 147456,
|
||||
vision_max_wh_ratio: float = 8,
|
||||
**kwargs,
|
||||
):
|
||||
self.max_position_embeddings = max_position_embeddings
|
||||
self.rope_scaling = rope_scaling
|
||||
self.rope_theta = rope_theta
|
||||
self.rope_parameters = rope_scaling or rope_parameters
|
||||
# Vision tower config; vision_n_layers == 0 means text-only.
|
||||
self.vision_n_layers = vision_n_layers
|
||||
self.vision_dim = vision_dim
|
||||
self.vision_n_heads = vision_n_heads
|
||||
self.vision_inter_dim = vision_inter_dim
|
||||
self.vision_patch_size = vision_patch_size
|
||||
self.vision_rope_theta = vision_rope_theta
|
||||
self.vision_downsample_ratio = vision_downsample_ratio
|
||||
self.vision_max_n_token = vision_max_n_token
|
||||
self.vision_min_pixels = vision_min_pixels
|
||||
self.vision_max_wh_ratio = vision_max_wh_ratio
|
||||
# The sparse-SWA index kernels widen the window within image spans
|
||||
# in-kernel, so mm-prefix ranges longer than sliding_window must be
|
||||
# kept (they are only consumed by those kernels).
|
||||
self.mm_prefix_clamp_sliding_window = vision_n_layers > 0
|
||||
# The visibility span covers the sentinel block [IMAGE_START,
|
||||
# IMAGE_END]; the mm placeholder additionally carries a leading
|
||||
# compressor-alignment pad (see mm_preprocess.COMPRESS_PAD_TO).
|
||||
self.mm_prefix_span_leading_pad_modulus = 4 if vision_n_layers > 0 else 0
|
||||
super().__init__(**kwargs)
|
||||
|
||||
@@ -579,6 +579,40 @@ class DeepSeekMTPModelArchConfigConvertor(ModelArchConfigConvertorBase):
|
||||
return getattr(self.hf_text_config, "num_nextn_predict_layers", 0)
|
||||
|
||||
|
||||
class DeepseekV4ModelArchConfigConvertor(ModelArchConfigConvertorBase):
|
||||
def __init__(
|
||||
self,
|
||||
hf_config: PretrainedConfig,
|
||||
hf_text_config: PretrainedConfig,
|
||||
revision: str | None = None,
|
||||
):
|
||||
# DeepSeek-V4-Flash-Vision-Exp ships the same architectures/model_type
|
||||
# as the text-only DeepSeek-V4-Flash; route to the VL wrapper class
|
||||
# when the config carries a vision tower. Mutate (not just override
|
||||
# get_architectures) because model-class resolution reads the raw
|
||||
# hf_config.architectures (get_model_architecture).
|
||||
# Only rewrite the stock text architecture: speculative-draft configs
|
||||
# (DSparkDraftModel / DeepSeekV4MTPModel) keep their own classes, and
|
||||
# the VL wrapper marks the config copy it hands to the inner text
|
||||
# backbone with _dsv4_vl_inner so this rewrite does not recurse.
|
||||
if (
|
||||
getattr(hf_config, "vision_n_layers", 0) > 0
|
||||
and getattr(hf_config, "architectures", None) == ["DeepseekV4ForCausalLM"]
|
||||
and not getattr(hf_config, "_dsv4_vl_inner", False)
|
||||
):
|
||||
hf_config.architectures = ["DeepseekV4ForConditionalGeneration"]
|
||||
super().__init__(hf_config, hf_text_config, revision)
|
||||
|
||||
def is_mm_prefix_lm(self, supports_multimodal: bool = True) -> bool:
|
||||
# The vision variant needs the mm-prefix plumbing: it makes the
|
||||
# scheduler prefill image spans atomically (disable_chunked_mm_input)
|
||||
# and routes per-request image ranges to the sparse-SWA metadata
|
||||
# builder, which widens the sliding window bidirectionally in-kernel.
|
||||
if not supports_multimodal:
|
||||
return False
|
||||
return getattr(self.hf_config, "vision_n_layers", 0) > 0
|
||||
|
||||
|
||||
class MimoMTPModelArchConfigConvertor(ModelArchConfigConvertorBase):
|
||||
def get_num_hidden_layers(self) -> int:
|
||||
return getattr(self.hf_text_config, "num_nextn_predict_layers", 0)
|
||||
@@ -775,6 +809,7 @@ MODEL_ARCH_CONFIG_CONVERTORS = {
|
||||
"cohere_asr": CohereAsrModelArchConfigConvertor,
|
||||
"dbrx": DbrxModelArchConfigConvertor,
|
||||
"deepseek_mtp": DeepSeekMTPModelArchConfigConvertor,
|
||||
"deepseek_v4": DeepseekV4ModelArchConfigConvertor,
|
||||
"diffusion_gemma_text": Gemma4ModelArchConfigConvertor,
|
||||
"ernie_mtp": ErnieMTPModelArchConfigConvertor,
|
||||
"falcon": FalconModelArchConfigConvertor,
|
||||
|
||||
@@ -9,6 +9,7 @@ from vllm.config import CacheConfig, VllmConfig, get_current_vllm_config
|
||||
from vllm.model_executor.layers.attention_layer_base import AttentionLayerBase
|
||||
from vllm.model_executor.warmup.jit_warmup import (
|
||||
WarmupIntRange,
|
||||
zip_inputs,
|
||||
)
|
||||
from vllm.model_executor.warmup.jit_warmup_triton_helper import (
|
||||
LaunchSpec,
|
||||
@@ -167,9 +168,15 @@ class DeepseekSparseSWAMetadata:
|
||||
decode_swa_width: int = 0
|
||||
# Paged-coordinate prefill SWA indices/lens (FP8 paged-direct prefill).
|
||||
prefill_swa_indices: torch.Tensor | None = (
|
||||
None # [num_prefill_tokens, 1, window_size]
|
||||
None # [num_prefill_tokens, 1, prefill_index_width]
|
||||
)
|
||||
prefill_swa_lens: torch.Tensor | None = None # [num_prefill_tokens]
|
||||
# In-image bidirectional visibility (vision variant): per-token counts of
|
||||
# extra visible tokens to the left/right inside an image span. Indexed by
|
||||
# absolute (decode-first) token position; only prefill rows are written.
|
||||
# None when the model is text-only or the batch has no image spans.
|
||||
prefill_left_visible: torch.Tensor | None = None
|
||||
prefill_right_visible: torch.Tensor | None = None
|
||||
|
||||
# Number of decode/prefill requests/tokens (batch is reordered: decodes first)
|
||||
num_decodes: int = 0
|
||||
@@ -416,6 +423,17 @@ class DeepseekSparseSWAMetadataBuilder(AttentionMetadataBuilder):
|
||||
assert hasattr(hf_config, "sliding_window")
|
||||
self.window_size = hf_config.sliding_window
|
||||
|
||||
# Vision variant: image spans (up to vision_max_n_token tokens) are
|
||||
# visible bidirectionally, so prefill index rows widen from
|
||||
# window_size to window_size + max_image_tokens. Text-only models keep
|
||||
# max_image_tokens == 0 and take the original code paths everywhere.
|
||||
self.max_image_tokens = (
|
||||
getattr(hf_config, "vision_max_n_token", 0)
|
||||
if getattr(hf_config, "vision_n_layers", 0) > 0
|
||||
else 0
|
||||
)
|
||||
self.prefill_index_width = self.window_size + self.max_image_tokens
|
||||
|
||||
# Detect which DeepseekV4 layer types this model uses so we only build a
|
||||
# FlashMLA tile-scheduler plan for types that will actually be called.
|
||||
# Models without compress_ratios (pure SWA) fall back to swaonly.
|
||||
@@ -447,7 +465,7 @@ class DeepseekSparseSWAMetadataBuilder(AttentionMetadataBuilder):
|
||||
self.prefill_swa_indices = torch.zeros(
|
||||
max_tokens,
|
||||
1,
|
||||
self.window_size,
|
||||
self.prefill_index_width,
|
||||
dtype=torch.int32,
|
||||
device=self.device,
|
||||
)
|
||||
@@ -456,6 +474,27 @@ class DeepseekSparseSWAMetadataBuilder(AttentionMetadataBuilder):
|
||||
dtype=torch.int32,
|
||||
device=self.device,
|
||||
)
|
||||
# In-image visibility side buffers (vision variant only). The span
|
||||
# CSR is uploaded per step from mm_req_doc_ranges; span capacity is
|
||||
# bounded by max_tokens since each span holds at least one token.
|
||||
if self.max_image_tokens > 0:
|
||||
self.left_visible = torch.zeros(
|
||||
max_tokens, dtype=torch.int32, device=self.device
|
||||
)
|
||||
self.right_visible = torch.zeros(
|
||||
max_tokens, dtype=torch.int32, device=self.device
|
||||
)
|
||||
self.span_indptr = torch.zeros(
|
||||
self.vllm_config.scheduler_config.max_num_seqs + 1,
|
||||
dtype=torch.int32,
|
||||
device=self.device,
|
||||
)
|
||||
self.span_starts = torch.zeros(
|
||||
max_tokens, dtype=torch.int32, device=self.device
|
||||
)
|
||||
self.span_ends = torch.zeros(
|
||||
max_tokens, dtype=torch.int32, device=self.device
|
||||
)
|
||||
self.is_valid_token = torch.zeros(
|
||||
max_tokens,
|
||||
dtype=torch.bool,
|
||||
@@ -556,6 +595,9 @@ class DeepseekSparseSWAMetadataBuilder(AttentionMetadataBuilder):
|
||||
decode_swa_indices,
|
||||
self.decode_swa_lens,
|
||||
self.window_size,
|
||||
decode_swa_indices.shape[-1],
|
||||
self.decode_swa_lens, # unused (HAS_IMAGE=False)
|
||||
self.decode_swa_lens, # unused (HAS_IMAGE=False)
|
||||
query_start_loc,
|
||||
seq_lens,
|
||||
token_to_req_indices,
|
||||
@@ -566,16 +608,42 @@ class DeepseekSparseSWAMetadataBuilder(AttentionMetadataBuilder):
|
||||
token_offset=0,
|
||||
)
|
||||
|
||||
# Vision variant: per-token in-image visibility for prefill tokens.
|
||||
# Decode tokens are always past the image spans (spans are prefilled
|
||||
# atomically), so the decode path above never needs them.
|
||||
prefill_left_visible: torch.Tensor | None = None
|
||||
prefill_right_visible: torch.Tensor | None = None
|
||||
mm_ranges = common_attn_metadata.mm_req_doc_ranges
|
||||
if (
|
||||
self.max_image_tokens > 0
|
||||
and num_prefill_tokens > 0
|
||||
and mm_ranges
|
||||
and any(mm_ranges.values())
|
||||
):
|
||||
prefill_left_visible, prefill_right_visible = self._build_image_visibility(
|
||||
common_attn_metadata.num_reqs,
|
||||
mm_ranges,
|
||||
num_decode_tokens,
|
||||
num_prefill_tokens,
|
||||
seq_lens,
|
||||
query_start_loc,
|
||||
token_to_req_indices,
|
||||
)
|
||||
|
||||
# Prefill SWA indices live in paged coordinates. `token_offset` lets
|
||||
# the kernel read is_valid_token / token_to_req_indices at absolute
|
||||
# prefill positions while writing output starting at index 0.
|
||||
if num_prefill_tokens > 0:
|
||||
has_image = prefill_left_visible is not None
|
||||
prefill_swa_indices = self.prefill_swa_indices[:num_prefill_tokens]
|
||||
prefill_swa_lens = self.prefill_swa_lens[:num_prefill_tokens]
|
||||
_COMPUTE_SWA_INDICES_AND_LENS_KERNEL(
|
||||
prefill_swa_indices,
|
||||
prefill_swa_lens,
|
||||
self.window_size,
|
||||
self.prefill_index_width,
|
||||
prefill_left_visible if has_image else prefill_swa_lens,
|
||||
prefill_right_visible if has_image else prefill_swa_lens,
|
||||
query_start_loc,
|
||||
seq_lens,
|
||||
token_to_req_indices,
|
||||
@@ -584,6 +652,7 @@ class DeepseekSparseSWAMetadataBuilder(AttentionMetadataBuilder):
|
||||
self.block_size,
|
||||
num_tokens=num_prefill_tokens,
|
||||
token_offset=num_decode_tokens,
|
||||
has_image=has_image,
|
||||
)
|
||||
|
||||
# Pre-compute DeepseekV4 prefill metadata shared across all attention layers.
|
||||
@@ -623,6 +692,8 @@ class DeepseekSparseSWAMetadataBuilder(AttentionMetadataBuilder):
|
||||
if num_prefill_tokens > 0
|
||||
else None
|
||||
),
|
||||
prefill_left_visible=prefill_left_visible,
|
||||
prefill_right_visible=prefill_right_visible,
|
||||
block_size=self.block_size,
|
||||
num_decodes=num_decodes,
|
||||
num_prefills=num_prefills,
|
||||
@@ -641,6 +712,50 @@ class DeepseekSparseSWAMetadataBuilder(AttentionMetadataBuilder):
|
||||
**deepseek_v4_fields, # type: ignore[arg-type]
|
||||
)
|
||||
|
||||
def _build_image_visibility(
|
||||
self,
|
||||
num_reqs: int,
|
||||
mm_ranges: dict[int, list[tuple[int, int]]],
|
||||
num_decode_tokens: int,
|
||||
num_prefill_tokens: int,
|
||||
seq_lens: torch.Tensor,
|
||||
query_start_loc: torch.Tensor,
|
||||
token_to_req_indices: torch.Tensor,
|
||||
) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
"""Upload image spans and compute per-token in-image visibility.
|
||||
|
||||
Returns (left, right) int32 tensors indexed by absolute (decode-first)
|
||||
token position; only prefill rows are meaningful.
|
||||
"""
|
||||
indptr = [0] * (num_reqs + 1)
|
||||
starts: list[int] = []
|
||||
ends: list[int] = []
|
||||
for req_idx in range(num_reqs):
|
||||
for span_start, span_end in mm_ranges.get(req_idx, ()):
|
||||
starts.append(span_start)
|
||||
ends.append(span_end)
|
||||
indptr[req_idx + 1] = len(starts)
|
||||
num_spans = len(starts)
|
||||
assert num_spans <= self.span_starts.shape[0]
|
||||
self.span_indptr[: num_reqs + 1].copy_(torch.tensor(indptr, dtype=torch.int32))
|
||||
if num_spans > 0:
|
||||
self.span_starts[:num_spans].copy_(torch.tensor(starts, dtype=torch.int32))
|
||||
self.span_ends[:num_spans].copy_(torch.tensor(ends, dtype=torch.int32))
|
||||
_compute_image_visibility_kernel[(num_prefill_tokens,)](
|
||||
self.left_visible,
|
||||
self.right_visible,
|
||||
self.span_indptr,
|
||||
self.span_starts,
|
||||
self.span_ends,
|
||||
query_start_loc,
|
||||
seq_lens,
|
||||
token_to_req_indices,
|
||||
self.max_image_tokens,
|
||||
token_offset=num_decode_tokens,
|
||||
)
|
||||
end = num_decode_tokens + num_prefill_tokens
|
||||
return self.left_visible[:end], self.right_visible[:end]
|
||||
|
||||
def update_draft_decode_metadata(
|
||||
self,
|
||||
metadata: DeepseekSparseSWAMetadata,
|
||||
@@ -658,6 +773,9 @@ class DeepseekSparseSWAMetadataBuilder(AttentionMetadataBuilder):
|
||||
metadata.decode_swa_indices,
|
||||
metadata.decode_swa_lens,
|
||||
metadata.decode_swa_indices.shape[-1],
|
||||
metadata.decode_swa_indices.shape[-1],
|
||||
metadata.decode_swa_lens, # unused (HAS_IMAGE=False)
|
||||
metadata.decode_swa_lens, # unused (HAS_IMAGE=False)
|
||||
metadata.query_start_loc,
|
||||
metadata.seq_lens,
|
||||
metadata.token_to_req_indices,
|
||||
@@ -755,90 +873,153 @@ class DeepseekSparseSWAMetadataBuilder(AttentionMetadataBuilder):
|
||||
return result
|
||||
|
||||
|
||||
@triton.jit(do_not_specialize=["token_offset"])
|
||||
def _compute_image_visibility_kernel(
|
||||
left_visible_ptr,
|
||||
right_visible_ptr,
|
||||
span_indptr_ptr,
|
||||
span_starts_ptr,
|
||||
span_ends_ptr,
|
||||
query_start_loc_ptr,
|
||||
seq_lens_ptr,
|
||||
token_to_req_indices_ptr,
|
||||
max_image_tokens,
|
||||
token_offset,
|
||||
):
|
||||
"""Per-token in-image visible counts (port of `get_image_visible`).
|
||||
|
||||
One program per prefill token. A token at position pos inside a span
|
||||
[span_start, span_end] sees min(pos - span_start, max_image_tokens - 1)
|
||||
extra tokens to its left and min(span_end - pos, max_image_tokens) to its
|
||||
right; tokens outside every span get 0/0 (plain causal window).
|
||||
"""
|
||||
pid = tl.program_id(0)
|
||||
token_idx = pid + token_offset
|
||||
req_idx = tl.load(token_to_req_indices_ptr + token_idx)
|
||||
|
||||
query_start = tl.load(query_start_loc_ptr + req_idx)
|
||||
query_end = tl.load(query_start_loc_ptr + req_idx + 1)
|
||||
seq_len = tl.load(seq_lens_ptr + req_idx)
|
||||
pos = seq_len - (query_end - query_start) + token_idx - query_start
|
||||
|
||||
span_lo = tl.load(span_indptr_ptr + req_idx)
|
||||
span_hi = tl.load(span_indptr_ptr + req_idx + 1)
|
||||
left = tl.zeros((), dtype=tl.int32)
|
||||
right = tl.zeros((), dtype=tl.int32)
|
||||
for i in range(span_lo, span_hi):
|
||||
span_start = tl.load(span_starts_ptr + i)
|
||||
span_end = tl.load(span_ends_ptr + i)
|
||||
in_span = (pos >= span_start) & (pos <= span_end)
|
||||
left = tl.where(
|
||||
in_span, tl.minimum(pos - span_start, max_image_tokens - 1), left
|
||||
)
|
||||
right = tl.where(in_span, tl.minimum(span_end - pos, max_image_tokens), right)
|
||||
tl.store(left_visible_ptr + token_idx, left)
|
||||
tl.store(right_visible_ptr + token_idx, right)
|
||||
|
||||
|
||||
# TODO(ben): unify this kernel to reduce duplication
|
||||
|
||||
|
||||
@triton.jit(
|
||||
do_not_specialize=[
|
||||
"swa_indices_stride",
|
||||
"block_table_stride",
|
||||
"token_offset",
|
||||
]
|
||||
)
|
||||
def _compute_swa_indices_and_lens_kernel(
|
||||
swa_indices_ptr,
|
||||
swa_indices_stride,
|
||||
swa_lens_ptr,
|
||||
window_size,
|
||||
index_width,
|
||||
left_visible_ptr,
|
||||
right_visible_ptr,
|
||||
query_start_loc_ptr,
|
||||
seq_lens_ptr,
|
||||
token_to_req_indices_ptr,
|
||||
is_valid_token_ptr,
|
||||
block_table_ptr,
|
||||
block_table_stride,
|
||||
block_size,
|
||||
token_offset,
|
||||
HAS_IMAGE: tl.constexpr,
|
||||
TRITON_BLOCK_SIZE: tl.constexpr,
|
||||
):
|
||||
pid = tl.program_id(0)
|
||||
token_idx = pid + token_offset
|
||||
is_valid = tl.load(is_valid_token_ptr + token_idx)
|
||||
if not is_valid:
|
||||
tl.store(swa_lens_ptr + pid, 0)
|
||||
# Clear the row so a padded token cannot gather through stale indices.
|
||||
for i in range(0, index_width, TRITON_BLOCK_SIZE):
|
||||
offset = i + tl.arange(0, TRITON_BLOCK_SIZE)
|
||||
tl.store(
|
||||
swa_indices_ptr + pid * swa_indices_stride + offset,
|
||||
-1,
|
||||
mask=offset < index_width,
|
||||
)
|
||||
return
|
||||
|
||||
req_idx = tl.load(token_to_req_indices_ptr + token_idx)
|
||||
|
||||
query_start = tl.load(query_start_loc_ptr + req_idx)
|
||||
query_end = tl.load(query_start_loc_ptr + req_idx + 1)
|
||||
query_len = query_end - query_start
|
||||
|
||||
seq_len = tl.load(seq_lens_ptr + req_idx)
|
||||
prefix_len = seq_len - query_len
|
||||
|
||||
pos = prefix_len + token_idx - query_start
|
||||
if HAS_IMAGE:
|
||||
# In-image bidirectional visibility widens the window: the window
|
||||
# starts up to max(left - (window - 1), 0) positions earlier and
|
||||
# extends `right` positions past the query token.
|
||||
left = tl.load(left_visible_ptr + token_idx)
|
||||
right = tl.load(right_visible_ptr + token_idx)
|
||||
else:
|
||||
left = 0
|
||||
right = 0
|
||||
left_add = tl.maximum(left - (window_size - 1), 0)
|
||||
start_pos = tl.maximum(pos - (window_size - 1) - left_add, 0)
|
||||
end_pos = pos + right + 1
|
||||
|
||||
swa_len = end_pos - start_pos
|
||||
tl.store(swa_lens_ptr + pid, swa_len)
|
||||
|
||||
for i in range(0, index_width, TRITON_BLOCK_SIZE):
|
||||
offset = i + tl.arange(0, TRITON_BLOCK_SIZE)
|
||||
|
||||
pos_offset = start_pos + offset
|
||||
block_indices = pos_offset // block_size
|
||||
block_numbers = tl.load(
|
||||
block_table_ptr + req_idx * block_table_stride + block_indices,
|
||||
mask=pos_offset < end_pos,
|
||||
)
|
||||
block_offsets = pos_offset % block_size
|
||||
slot_ids = block_numbers * block_size + block_offsets
|
||||
|
||||
slot_ids = tl.where(offset < swa_len, slot_ids, -1)
|
||||
tl.store(
|
||||
swa_indices_ptr + pid * swa_indices_stride + offset,
|
||||
slot_ids,
|
||||
mask=offset < index_width,
|
||||
)
|
||||
|
||||
|
||||
class ComputeSWAIndicesAndLensKernel(
|
||||
VllmTritonJitKernel["ComputeSWAIndicesAndLensKernel.CompileKey"]
|
||||
):
|
||||
@dataclass(frozen=True)
|
||||
class CompileKey:
|
||||
window_size: int
|
||||
index_width: int
|
||||
has_image: int
|
||||
block_size: int
|
||||
triton_block_size: int
|
||||
|
||||
@staticmethod
|
||||
@triton.jit(
|
||||
do_not_specialize=[
|
||||
"swa_indices_stride",
|
||||
"block_table_stride",
|
||||
"token_offset",
|
||||
]
|
||||
)
|
||||
def kernel(
|
||||
swa_indices_ptr,
|
||||
swa_indices_stride,
|
||||
swa_lens_ptr,
|
||||
window_size,
|
||||
query_start_loc_ptr,
|
||||
seq_lens_ptr,
|
||||
token_to_req_indices_ptr,
|
||||
is_valid_token_ptr,
|
||||
block_table_ptr,
|
||||
block_table_stride,
|
||||
block_size,
|
||||
token_offset,
|
||||
TRITON_BLOCK_SIZE: tl.constexpr,
|
||||
):
|
||||
pid = tl.program_id(0)
|
||||
token_idx = pid + token_offset
|
||||
is_valid = tl.load(is_valid_token_ptr + token_idx)
|
||||
if not is_valid:
|
||||
tl.store(swa_lens_ptr + pid, 0)
|
||||
# Clear the row so a padded token cannot gather through stale indices.
|
||||
for i in range(0, window_size, TRITON_BLOCK_SIZE):
|
||||
offset = i + tl.arange(0, TRITON_BLOCK_SIZE)
|
||||
tl.store(
|
||||
swa_indices_ptr + pid * swa_indices_stride + offset,
|
||||
-1,
|
||||
mask=offset < window_size,
|
||||
)
|
||||
return
|
||||
|
||||
req_idx = tl.load(token_to_req_indices_ptr + token_idx)
|
||||
|
||||
query_start = tl.load(query_start_loc_ptr + req_idx)
|
||||
query_end = tl.load(query_start_loc_ptr + req_idx + 1)
|
||||
query_len = query_end - query_start
|
||||
|
||||
seq_len = tl.load(seq_lens_ptr + req_idx)
|
||||
prefix_len = seq_len - query_len
|
||||
|
||||
pos = prefix_len + token_idx - query_start
|
||||
start_pos = tl.maximum(pos - window_size + 1, 0)
|
||||
end_pos = pos + 1
|
||||
|
||||
swa_len = end_pos - start_pos
|
||||
tl.store(swa_lens_ptr + pid, swa_len)
|
||||
|
||||
for i in range(0, window_size, TRITON_BLOCK_SIZE):
|
||||
offset = i + tl.arange(0, TRITON_BLOCK_SIZE)
|
||||
|
||||
pos_offset = start_pos + offset
|
||||
block_indices = pos_offset // block_size
|
||||
block_numbers = tl.load(
|
||||
block_table_ptr + req_idx * block_table_stride + block_indices,
|
||||
mask=pos_offset < end_pos,
|
||||
)
|
||||
block_offsets = pos_offset % block_size
|
||||
slot_ids = block_numbers * block_size + block_offsets
|
||||
|
||||
slot_ids = tl.where(offset < swa_len, slot_ids, -1)
|
||||
tl.store(
|
||||
swa_indices_ptr + pid * swa_indices_stride + offset,
|
||||
slot_ids,
|
||||
mask=offset < window_size,
|
||||
)
|
||||
kernel = staticmethod(_compute_swa_indices_and_lens_kernel)
|
||||
|
||||
def dispatch( # type: ignore[override]
|
||||
self,
|
||||
@@ -854,8 +1035,24 @@ class ComputeSWAIndicesAndLensKernel(
|
||||
*,
|
||||
window_size: int,
|
||||
block_size: int,
|
||||
max_image_tokens: int = 0,
|
||||
) -> list[CompileKey]:
|
||||
# Decode rows always use the plain causal window. Vision models widen
|
||||
# prefill rows by max_image_tokens and additionally need the in-image
|
||||
# bidirectional variant for batches that contain image spans.
|
||||
width_has_image = [(window_size, 0)]
|
||||
if max_image_tokens > 0:
|
||||
width_has_image += [
|
||||
(window_size + max_image_tokens, 0),
|
||||
(window_size + max_image_tokens, 1),
|
||||
]
|
||||
return self._trace_dispatch(self.dispatch)(
|
||||
zip_inputs(
|
||||
*(
|
||||
dict(index_width=index_width, has_image=has_image)
|
||||
for index_width, has_image in width_has_image
|
||||
)
|
||||
),
|
||||
window_size=window_size,
|
||||
block_size=block_size,
|
||||
)
|
||||
@@ -866,6 +1063,9 @@ class ComputeSWAIndicesAndLensKernel(
|
||||
swa_indices=TritonWarmupTensor(torch.int32, shape=(1, 1), strides=(1, 1)),
|
||||
swa_lens=int32_ptr,
|
||||
window_size=compile_key.window_size,
|
||||
index_width=compile_key.index_width,
|
||||
left_visible=int32_ptr,
|
||||
right_visible=int32_ptr,
|
||||
query_start_loc=int32_ptr,
|
||||
seq_lens=int32_ptr,
|
||||
token_to_req_indices=int32_ptr,
|
||||
@@ -874,6 +1074,7 @@ class ComputeSWAIndicesAndLensKernel(
|
||||
block_size=compile_key.block_size,
|
||||
num_tokens=1,
|
||||
token_offset=0,
|
||||
has_image=bool(compile_key.has_image),
|
||||
)
|
||||
|
||||
@kernel_launcher
|
||||
@@ -882,6 +1083,9 @@ class ComputeSWAIndicesAndLensKernel(
|
||||
swa_indices: torch.Tensor,
|
||||
swa_lens: torch.Tensor,
|
||||
window_size: int,
|
||||
index_width: int,
|
||||
left_visible: torch.Tensor,
|
||||
right_visible: torch.Tensor,
|
||||
query_start_loc: torch.Tensor,
|
||||
seq_lens: torch.Tensor,
|
||||
token_to_req_indices: torch.Tensor,
|
||||
@@ -891,10 +1095,12 @@ class ComputeSWAIndicesAndLensKernel(
|
||||
*,
|
||||
num_tokens: int,
|
||||
token_offset: int,
|
||||
has_image: bool = False,
|
||||
) -> LaunchSpec:
|
||||
return (num_tokens,), dict(
|
||||
swa_indices_stride=swa_indices.stride(0),
|
||||
block_table_stride=block_table.stride(0),
|
||||
HAS_IMAGE=has_image,
|
||||
TRITON_BLOCK_SIZE=1024,
|
||||
)
|
||||
|
||||
|
||||
@@ -2429,6 +2429,16 @@ class GPUModelRunner(
|
||||
_bidi_sw = getattr(hf_text_config, "sliding_window", None)
|
||||
_clamps_in_kernel = getattr(
|
||||
self.model, "mm_prefix_clamp_sliding_window", False
|
||||
) or getattr(hf_text_config, "mm_prefix_clamp_sliding_window", False)
|
||||
# Some models (DeepSeek-V4 vision) define the bidirectional span
|
||||
# over the whole sentinel block ([IMAGE_START, IMAGE_END]) rather
|
||||
# than the embed tokens, and prepend a position-dependent
|
||||
# alignment pad before the first sentinel. For those, derive the
|
||||
# span from the full placeholder range and strip the pad.
|
||||
# TODO(Isotr0py): Refactor mm_prefix_lm implementation
|
||||
# for better readability and maintainability.
|
||||
_span_pad_modulus = getattr(
|
||||
hf_text_config, "mm_prefix_span_leading_pad_modulus", 0
|
||||
)
|
||||
for req_id in self.input_batch.req_ids:
|
||||
image_doc_ranges = []
|
||||
@@ -2437,7 +2447,18 @@ class GPUModelRunner(
|
||||
if mm_feature.modality == "audio":
|
||||
continue
|
||||
pos_info = mm_feature.mm_position
|
||||
img_doc_range = pos_info.extract_embeds_range()
|
||||
if _span_pad_modulus:
|
||||
pad = (
|
||||
_span_pad_modulus - 1 - pos_info.offset % _span_pad_modulus
|
||||
)
|
||||
img_doc_range = [
|
||||
(
|
||||
pos_info.offset + pad,
|
||||
pos_info.offset + pos_info.length - 1,
|
||||
)
|
||||
]
|
||||
else:
|
||||
img_doc_range = pos_info.extract_embeds_range()
|
||||
for r in img_doc_range:
|
||||
if (
|
||||
not _clamps_in_kernel
|
||||
|
||||
Reference in New Issue
Block a user