Compare commits

...
Author SHA1 Message Date
yewentao256 cb68c1a314 Fix ci
Signed-off-by: yewentao256 <zhyanwentao@126.com>
2026-04-22 18:26:48 +00:00
Nicolò LucchesiandGitHub 33ef1941e2 [Bugfix][CI] Fix v1/kv_connector/unit/test_nixl_connector_hma.py::test_fewer_blocks_with_hma (#40597)
Signed-off-by: NickLucche <nlucches@redhat.com>
2026-04-22 14:21:02 +01:00
Hank_andGitHub a4905133f3 [xpu][rocm] Update current_platform.supports_fp8() for TritonExperts (#40132)
Signed-off-by: Hank <hcc.mayday@gmail.com>
2026-04-22 13:39:40 +02:00
ecbe42e991 [Doc] Clarify supported keys for --speculative-config (#40455)
Signed-off-by: Wangxiaoxiaoa <Wangxiaoxiaoa@users.noreply.github.com>
Co-authored-by: Wangxiaoxiaoa <Wangxiaoxiaoa@users.noreply.github.com>
2026-04-22 04:36:17 -07:00
ℍ𝕠𝕝𝕝𝕠𝕨 𝕄𝕒𝕟GitHubmergify[bot] <37929162+mergify[bot]@users.noreply.github.com>Jee Jee Li
a250f1bd5f [Bugfix] LoRA for DeepSeek V3.2 (#35077)
Signed-off-by: Hollow Man <hollowman@opensuse.org>
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
Co-authored-by: Jee Jee Li <pandaleefree@gmail.com>
2026-04-22 19:33:50 +08:00
lyd1992andGitHub 04eac6ba24 [Bugfix][CPU][RISC-V] Clamp exp() input to prevent NaN (#40428)
Signed-off-by: liuyudong <liuyudong@iscas.ac.cn>
2026-04-22 09:38:18 +00:00
9047288b68 support hotwords for FunASR model (#39674)
Signed-off-by: zixiao <shunli.dsl@alibaba-inc.com>
Co-authored-by: zixiao <shunli.dsl@alibaba-inc.com>
2026-04-22 02:25:06 -07:00
Johnny YangandGitHub ed6d30377d upgrade tpu-inference to v0.18.0 (#40395) 2026-04-22 01:33:45 -07:00
storyiconGitHubmergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
6aa057c9d7 [Multimodal] Support custom video metadata for pre-extracted frame sequences (#40133)
Signed-off-by: storyicon <storyicon@foxmail.com>
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
2026-04-22 15:50:04 +08:00
ChaunceyandGitHub a2bd09c960 [Bugfix] [Reasoning] Add reasoning_start_str/reasoning_end_str properties to reasoning parsers (#40566)
Signed-off-by: chaunceyjiang <chaunceyjiang@gmail.com>
2026-04-22 07:27:44 +00:00
philip-essentialandGitHub 123674879e [Model] Add block-local attention and YaRN for local layers to Gemma3 (#39823)
Signed-off-by: Philip Monk <169196560+philip-essential@users.noreply.github.com>
2026-04-21 23:34:50 -07:00
Carl YandGitHub 4254aeb56f [fix] flaky test_mla_attn_quant_fusion.py (#40530)
Signed-off-by: Carl You <4531192+carlyou@users.noreply.github.com>
2026-04-22 06:29:58 +00:00
Or OzeriGitHubmergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
aad88f8486 [kv_offload+HMA][8/N]: Support multi-group worker transfer (#38453)
Signed-off-by: Or Ozeri <oro@il.ibm.com>
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
2026-04-22 08:44:00 +03:00
Bugen ZhaoandGitHub 0210024ae7 [Bugfix] Pass effective chat template kwargs to reasoning parsers (#40460)
Signed-off-by: Bugen Zhao <i@bugenzhao.com>
2026-04-21 22:17:51 -07:00
Ekagra RanjanGitHubgemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>Cyrus Leung
4eafc72928 [Audio] Bundle get_generation_prompt() params into SpeechToTextParams (#36268)
Signed-off-by: Ekagra Ranjan <3116519+ekagra-ranjan@users.noreply.github.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Co-authored-by: Cyrus Leung <cyrus.tl.leung@gmail.com>
2026-04-22 12:24:18 +08:00
Micah WilliamsonandGitHub 6d09769700 [ROCm] Support non-causal attention in ROCM_ATTN (#40176)
Signed-off-by: Micah Williamson <micah.williamson@amd.com>
2026-04-22 12:57:12 +09:00
Carl YGitHubmergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
4506319a28 [compile] mla + group fp8 fusion (#38877)
Signed-off-by: Carl You <4531192+carlyou@users.noreply.github.com>
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
2026-04-21 23:16:58 -04:00
Rishapveer SinghGitHubmergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
9b60e2ffaa [Bugfix] Fix quantized model initialization failure with prefetch offloading (#40432)
Signed-off-by: Rishapveer Singh <singhrishapveer@gmail.com>
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
2026-04-21 20:15:58 -07:00
Martin HickeyandGitHub 3951d3eacd [MyPy] Enable mypy for vllm/model_executor/layers/ (#40159)
Signed-off-by: Martin Hickey <martin.hickey@ie.ibm.com>
2026-04-21 20:15:02 -07:00
6f2c71be8f [Multimodal] Add PyAV video backend for concurrent video decoding (#39986)
Signed-off-by: Jaseel Muhammad <jaseel.muhammad@mbzuai.ac.ae>
Signed-off-by: Isotr0py <2037008807@qq.com>
Co-authored-by: Isotr0py <2037008807@qq.com>
Co-authored-by: Isotr0py <mozf@mail2.sysu.edu.cn>
2026-04-21 20:14:57 -07:00
rasmithandGitHub 2463f00fb6 [AMD][CI][BugFix] Override normalize_e4m3fn_to_e4m3fnuz for fnuz machines in test_moe_layer_no_parallel (#40550)
Signed-off-by: Randall Smith <Randall.Smith@amd.com>
2026-04-22 02:21:02 +00:00
EdalatiAliGitHubgemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
f946659fff [Bugfix] Fix W4A8_FP8 MoE tp>1 correctness and view() TypeError (#40310)
Signed-off-by: EdalatiAli <aliedalati@cohere.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
2026-04-21 21:58:33 -04:00
Soila KavulyaandGitHub f90aa44662 [NIXL][XPU]Fix nixl import on XPU (#40430)
Signed-off-by: Soila Kavulya <soila.p.kavulya.intel.com>
2026-04-22 09:26:33 +08:00
rasmithandGitHub cefa5281a7 [ROCm][P/D][MORI][BugFix] Ensure correct api is used when making requests to prefill / decode nodes (#39835)
Signed-off-by: Randall Smith <Randall.Smith@amd.com>
2026-04-22 09:48:25 +09:00
Jhao-Ting ChenandGitHub 46794958f0 test: add nan/inf clamp regression test for fused_topk_bias (#40553)
Signed-off-by: Jhao-Ting Chen <jhaotingc@nvidia.com>
2026-04-22 00:46:53 +00:00
Khushali DesaiandGitHub 6ff8dea075 [Bugfix] avoid warmup if text only expectation in multi_modal run (#40409)
Signed-off-by: khushali9 <khushali.desai9@gmail.com>
2026-04-22 00:19:50 +00:00
TJianandGitHub 583e6f2226 [ROCm] [Wheel] [Bugfix] [Critical] Remove any packages installed from github from rocm.txt e.g fastsafetensors as it is incompatible with uv pip (#40461)
Signed-off-by: tjtanaa <tunjian.tan@embeddedllm.com>
2026-04-22 00:18:07 +00:00
96a85c5750 [Startup][UX] Enable CUDAGraph memory profiling by default (#38284)
Signed-off-by: Matthew Bonanni <mbonanni@redhat.com>
Co-authored-by: Tyler Michael Smith <tlrmchlsmth@gmail.com>
2026-04-21 18:16:59 -04:00
9db4650e5e [MoE Refactor] Add more MoE layer tests (#39349)
Signed-off-by: Bill Nell <bnell@redhat.com>
Signed-off-by: Robert Shaw <114415538+robertgshaw2-redhat@users.noreply.github.com>
Co-authored-by: Robert Shaw <114415538+robertgshaw2-redhat@users.noreply.github.com>
2026-04-21 18:12:36 -04:00
bnellnmandGitHub 5e584ce9ec [MoE Refactor] Remove SharedFusedMoE class (#35782)
Signed-off-by: Bill Nell <bnell@redhat.com>
2026-04-21 18:12:12 -04:00
Wentao YeandGitHub 1842447c09 [Refactor] Remove unused param (#39750)
Signed-off-by: yewentao256 <zhyanwentao@126.com>
2026-04-21 14:59:20 -07:00
Wentao YeandGitHub 16688b26a6 [Perf] Optimize batch invariant with fused rms norm, 2.1% E2E latency improvement (#40413)
Signed-off-by: yewentao256 <zhyanwentao@126.com>
2026-04-21 19:51:03 +00:00
Jakub ZakrzewskiandGitHub 6fbec8ed47 [Bugfix][Kernel] nvfp4 cutlass MoE: fix nvfp4 experts quant out-of-bounds read for expert counts not divisible by 4 or 16 (#40351)
Signed-off-by: Jakub Zakrzewski <jzakrzewski@nvidia.com>
2026-04-21 19:06:09 +00:00
166 changed files with 3804 additions and 933 deletions
+26 -12
View File
@@ -15,16 +15,12 @@
#include <torch/all.h>
namespace vec_op {
#ifdef RISCV_BF16_SUPPORT
#define VLLM_DISPATCH_CASE_FLOATING_TYPES(...) \
AT_DISPATCH_CASE(at::ScalarType::Float, __VA_ARGS__) \
AT_DISPATCH_CASE(at::ScalarType::Half, __VA_ARGS__) \
AT_DISPATCH_CASE(at::ScalarType::BFloat16, __VA_ARGS__)
#else
#define VLLM_DISPATCH_CASE_FLOATING_TYPES(...) \
AT_DISPATCH_CASE(at::ScalarType::Float, __VA_ARGS__) \
AT_DISPATCH_CASE(at::ScalarType::Half, __VA_ARGS__)
#endif
// BFloat16 is always supported on RISC-V: natively when RISCV_BF16_SUPPORT
// is defined, otherwise via the FP32-simulation fallback path.
#define VLLM_DISPATCH_CASE_FLOATING_TYPES(...) \
AT_DISPATCH_CASE(at::ScalarType::Float, __VA_ARGS__) \
AT_DISPATCH_CASE(at::ScalarType::Half, __VA_ARGS__) \
AT_DISPATCH_CASE(at::ScalarType::BFloat16, __VA_ARGS__)
#define VLLM_DISPATCH_FLOATING_TYPES(TYPE, NAME, ...) \
AT_DISPATCH_SWITCH(TYPE, NAME, VLLM_DISPATCH_CASE_FLOATING_TYPES(__VA_ARGS__))
@@ -486,9 +482,18 @@ struct FP32Vec8 : public Vec<FP32Vec8> {
}
FP32Vec8 exp() const {
// Clamp input to prevent NaN: exp(-inf) must return 0, not NaN.
// Without clamping, -inf * 0.0 = NaN in the final poly * scale step.
// Matches the clamping strategy used by x86 AVX-512 and ARM NEON.
constexpr float exp_lo = -87.3365447505f; // ln(FLT_MIN)
constexpr float exp_hi = 88.7228391117f; // ln(FLT_MAX)
fixed_fp32x8_t x = RVVI(__riscv_vfmin_vf_f32, LMUL_256)(
RVVI(__riscv_vfmax_vf_f32, LMUL_256)(reg, exp_lo, VEC_ELEM_NUM), exp_hi,
VEC_ELEM_NUM);
const float inv_ln2 = 1.44269504088896341f;
fixed_fp32x8_t x_scaled =
RVVI(__riscv_vfmul_vf_f32, LMUL_256)(reg, inv_ln2, VEC_ELEM_NUM);
RVVI(__riscv_vfmul_vf_f32, LMUL_256)(x, inv_ln2, VEC_ELEM_NUM);
fixed_i32x8_t n_int =
RVVI(__riscv_vfcvt_x_f_v_i32, LMUL_256)(x_scaled, VEC_ELEM_NUM);
fixed_fp32x8_t n_float =
@@ -706,9 +711,18 @@ struct FP32Vec16 : public Vec<FP32Vec16> {
}
FP32Vec16 exp() const {
// Clamp input to prevent NaN: exp(-inf) must return 0, not NaN.
// Without clamping, -inf * 0.0 = NaN in the final poly * scale step.
// Matches the clamping strategy used by x86 AVX-512 and ARM NEON.
constexpr float exp_lo = -87.3365447505f; // ln(FLT_MIN)
constexpr float exp_hi = 88.7228391117f; // ln(FLT_MAX)
fixed_fp32x16_t x = RVVI(__riscv_vfmin_vf_f32, LMUL_512)(
RVVI(__riscv_vfmax_vf_f32, LMUL_512)(reg, exp_lo, VEC_ELEM_NUM), exp_hi,
VEC_ELEM_NUM);
const float inv_ln2 = 1.44269504088896341f;
fixed_fp32x16_t x_scaled =
RVVI(__riscv_vfmul_vf_f32, LMUL_512)(reg, inv_ln2, VEC_ELEM_NUM);
RVVI(__riscv_vfmul_vf_f32, LMUL_512)(x, inv_ln2, VEC_ELEM_NUM);
fixed_i32x16_t n_int =
RVVI(__riscv_vfcvt_x_f_v_i32, LMUL_512)(x_scaled, VEC_ELEM_NUM);
fixed_fp32x16_t n_float =
@@ -277,7 +277,9 @@ void quant_impl(void* output, void* output_scale, void* input,
(totalWorkSize + block.x * grid.x - 1) / (block.x * grid.x);
if (blockRepeat > 1) {
size_t shared_mem_size = (n_experts + 1) * sizeof(uint32_t);
if (n_experts >= 4) {
// The shared-memory vectorized offset load only handles full 4-expert
// chunks. Use the scalar specialization for the remainder cases.
if (n_experts >= 4 && n_experts % 4 == 0) {
cvt_fp16_to_fp4<T, FUSE_SILU_MUL, false, false>
<<<grid, block, shared_mem_size, stream>>>(
m_topk, k, reinterpret_cast<T*>(input),
@@ -299,7 +301,9 @@ void quant_impl(void* output, void* output_scale, void* input,
n_experts);
}
} else {
if (n_experts >= 16) {
// The low-latency vectorized expert lookup only handles full 16-expert
// chunks. Fall back to the scalar lookup path for the remainder cases.
if (n_experts >= 16 && n_experts % 16 == 0) {
cvt_fp16_to_fp4<T, FUSE_SILU_MUL, false, false>
<<<grid, block, 0, stream>>>(
m_topk, k, reinterpret_cast<T*>(input),
+24
View File
@@ -307,6 +307,30 @@ RUN --mount=type=bind,source=.git,target=vllm/.git \
&& echo "Detected vLLM version: ${VLLM_VERSION}" \
&& echo "${VLLM_VERSION}" > /tmp/vllm_version.txt
# Fail if git-based package dependencies are found in requirements files
# (uv doesn't handle git+ URLs well, and packages should be distributed on PyPI)
# Extra notes: pip install is able to handle git+ URLs, but uv doesn't.
RUN echo "Checking for git-based packages in requirements files..." \
&& echo "Checking common.txt for git-based packages:" \
&& if grep -q 'git+' ${COMMON_WORKDIR}/vllm/requirements/common.txt; then \
echo "ERROR: Git-based packages found in common.txt:"; \
grep 'git+' ${COMMON_WORKDIR}/vllm/requirements/common.txt; \
echo "Please publish these packages to PyPI instead of using git dependencies."; \
exit 1; \
else \
echo " ✓ No git-based packages found in common.txt"; \
fi \
&& echo "Checking rocm.txt for git-based packages:" \
&& if grep -q 'git+' ${COMMON_WORKDIR}/vllm/requirements/rocm.txt; then \
echo "ERROR: Git-based packages found in rocm.txt:"; \
grep 'git+' ${COMMON_WORKDIR}/vllm/requirements/rocm.txt; \
echo "Please publish these packages to PyPI instead of using git dependencies."; \
exit 1; \
else \
echo " ✓ No git-based packages found in rocm.txt"; \
fi \
&& echo "All requirements files are clean - no git-based packages found"
# Pin vLLM dependencies to exact versions of custom ROCm wheels
# This ensures 'pip install vllm' automatically installs correct torch/triton/torchvision/amdsmi
COPY tools/vllm-rocm/pin_rocm_dependencies.py /tmp/pin_rocm_dependencies.py
+19 -20
View File
@@ -66,7 +66,7 @@ This is for controlling general behavior of the API when serving your model:
See [Audio preprocessing and chunking](#audio-preprocessing-and-chunking) for what each field controls.
Implement the prompt construction via [get_generation_prompt][vllm.model_executor.models.interfaces.SupportsTranscription.get_generation_prompt]. The server passes you the resampled waveform and task parameters; you return a valid [PromptType][vllm.inputs.llm.PromptType]. There are two common patterns:
Implement the prompt construction via [get_generation_prompt][vllm.model_executor.models.interfaces.SupportsTranscription.get_generation_prompt]. The server builds a [SpeechToTextParams][vllm.config.speech_to_text.SpeechToTextParams] object that bundles the resampled waveform, task parameters, and request-specific options. Your model receives this single object and returns a valid [PromptType][vllm.inputs.llm.PromptType]. There are two common patterns:
#### Multimodal LLM with audio embeddings (e.g., Voxtral, Gemma3n)
@@ -75,21 +75,20 @@ Return a dict containing `multi_modal_data` with the audio, and either a `prompt
??? code "get_generation_prompt()"
```python
from vllm.config.speech_to_text import SpeechToTextParams
class YourASRModel(nn.Module, SupportsTranscription):
...
@classmethod
def get_generation_prompt(
cls,
audio: np.ndarray,
stt_config: SpeechToTextConfig,
model_config: ModelConfig,
language: str | None,
task_type: Literal["transcribe", "translate"],
request_prompt: str,
to_language: str | None,
stt_params: SpeechToTextParams,
) -> PromptType:
# Example with a free-form instruction prompt
audio = stt_params.audio
stt_config = stt_params.stt_config
task_type = stt_params.task_type
task_word = "Transcribe" if task_type == "transcribe" else "Translate"
prompt = (
"<start_of_turn>user\n"
@@ -112,20 +111,22 @@ Return a dict with separate `encoder_prompt` and `decoder_prompt` entries:
??? code "get_generation_prompt()"
```python
from vllm.config.speech_to_text import SpeechToTextParams
class YourASRModel(nn.Module, SupportsTranscription):
...
@classmethod
def get_generation_prompt(
cls,
audio: np.ndarray,
stt_config: SpeechToTextConfig,
model_config: ModelConfig,
language: str | None,
task_type: Literal["transcribe", "translate"],
request_prompt: str,
to_language: str | None,
stt_params: SpeechToTextParams,
) -> PromptType:
audio = stt_params.audio
stt_config = stt_params.stt_config
language = stt_params.language
task_type = stt_params.task_type
request_prompt = stt_params.request_prompt
if language is None:
raise ValueError("Language must be specified")
@@ -213,15 +214,13 @@ Relevant server logic:
chunks = [y] if not do_split_audio else self._split_audio(y, int(sr))
prompts = []
for chunk in chunks:
prompt = self.model_cls.get_generation_prompt(
stt_params = request.build_stt_params(
audio=chunk,
stt_config=self.asr_config,
model_config=self.model_config,
language=language,
task_type=self.task_type,
request_prompt=request.prompt,
to_language=to_language,
)
prompt = self.model_cls.get_generation_prompt(stt_params)
prompts.append(prompt)
return prompts, duration
```
+2 -2
View File
@@ -44,7 +44,7 @@ The table below lists the quantization schemes supported by each fusion on each
| `fuse_allreduce_rms` | FP16/BF16, FP8 static, NVFP4 | FP16/BF16, FP8 static | — | — | — |
| `fuse_minimax_qk_norm`\* | FP16/BF16 | FP16/BF16 | FP16/BF16 | FP16/BF16 | — |
| `fuse_attn_quant`\* | FP8 static\*, NVFP4\* | FP8 static\* | FP8 static\* | — | FP8 static\* |
| `fuse_attn_quant` (MLA)\* | FP8 static\*, NVFP4\* | FP8 static\* | FP8 static\* | — | FP8 static(untested)\* |
| `fuse_attn_quant` (MLA)\* | FP8 static\*, FP8 per-group\*, NVFP4\* | FP8 static\*, FP8 per-group\* | FP8 static\*, FP8 per-group\* | — | FP8 static\* (untested) |
| `fuse_rope_kvcache` | — | — | — | — | FP16/BF16 |
| `enable_qk_norm_rope_fusion` | FP16/BF16 | FP16/BF16 | FP16/BF16† | FP16/BF16† | — |
| `enable_sp` | FP16/BF16, FP8 static† | FP16/BF16, FP8 static | FP16/BF16† | FP16/BF16† | — |
@@ -152,7 +152,7 @@ standard `Attention` and `MLAAttention` (used by DeepSeek-V2/V3/R1 models). Patt
- `FLASHINFER`: CUDA sm100+ with FlashInfer installed
`MLAAttention → FP8 static quant` / `MLAAttention → NVFP4 dynamic quant`:
`MLAAttention → FP8 static, FP8 per-group, NVFP4 dynamic quant`
The MLA fusion operates at the graph level on the `unified_mla_attention_with_output` op and works
with all MLA decode and prefill backend combinations. Unlike standard `Attention` backends (where
+64
View File
@@ -780,6 +780,70 @@ vllm serve Qwen/Qwen3-VL-30B-A3B-Instruct \
Works with common video formats like MP4 when using OpenCV backends.
#### Pre-extracted Frame Sequences with `media_io_kwargs`
When you extract video frames on the client side and send them as `video/jpeg` (base64-concatenated JPEG frames), you can preserve the original video metadata by using `media_io_kwargs` in your request. This enables more accurate video understanding by preserving temporal information that would otherwise be lost during client-side frame extraction.
**Supported Parameters:**
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `fps` | float | Frame rate of the original video |
| `frames_indices` | list[int] | Indices of the actually sampled frames |
| `total_num_frames` | int | Total frame count of the original video |
| `duration` | float | Duration of the original video in seconds |
| `do_sample_frames` | bool | Whether to perform frame sampling |
??? code
```python
from openai import OpenAI
client = OpenAI(base_url="http://localhost:8000/v1", api_key="EMPTY")
# Client-side frame extraction
frames = extract_frames(video_path, num_frames=32)
frames_b64 = ",".join([encode_image(f) for f in frames])
video_url = f"data:video/jpeg;base64,{frames_b64}"
# Pass video metadata via media_io_kwargs
response = client.chat.completions.create(
model="your-multimodal-model",
messages=[{
"role": "user",
"content": [
{"type": "video_url", "video_url": {"url": video_url}},
{"type": "text", "text": "Describe what happens in this video."}
]
}],
extra_body={
"media_io_kwargs": {
"video": {
"fps": 30.0,
"frames_indices": [0, 10, 20, 30, 40, 50, 60, 70, 80, 90,
100, 110, 120, 130, 140, 150, 160, 170,
180, 190, 200, 210, 220, 230, 240, 250,
260, 270, 280, 290, 300, 310],
"total_num_frames": 900,
"duration": 30.0,
}
}
},
)
print(response.choices[0].message.content)
```
**Why use `media_io_kwargs`?**
When extracting frames client-side, the server loses important context about the original video:
- **Temporal information**: Which frames were sampled and their positions in the original timeline
- **Video duration**: How long the original video was
- **Frame rate**: The original playback speed
By passing this metadata, the model can better understand the temporal distribution of the sampled frames and whether important moments might have been skipped.
#### Custom RGBA Background Color
To use a custom background color for RGBA images, pass the `rgba_background_color` parameter via `--media-io-kwargs`:
@@ -35,6 +35,99 @@ For reproducible measurements in your environment, use
[`examples/offline_inference/spec_decode.py`](../../../examples/offline_inference/spec_decode.py)
or the [benchmark CLI guide](../../benchmarking/cli.md).
## `--speculative-config` schema
Use `--speculative-config` to pass speculative decoding settings as a JSON
object on the CLI:
```bash
vllm serve <target-model> \
--speculative-config '{
"method": "draft_model",
"model": "<draft-model>",
"num_speculative_tokens": 5
}'
```
The same keys are accepted from Python via `LLM(..., speculative_config={...})`.
The tables below highlight common user-facing keys accepted in this JSON
object; they are not an exhaustive schema reference.
For more details, see the generated [engine arguments reference](../../configuration/engine_args.md)
and the API docs for [vllm.config.SpeculativeConfig][].
### Common keys
These keys are commonly used across speculative decoding setups, though some
only apply to model-based methods such as `draft_model`, `mtp`, `eagle3`, and
`dflash`.
| Key | Type | Default | Allowed values / meaning |
| --- | --- | --- | --- |
| `method` | `string` | `None` | Speculation method. Common values include `draft_model`, `ngram`, `suffix`, `mtp`, `eagle3`, and `dflash`. If omitted, vLLM infers the method from the provided configuration when possible. |
| `model` | `string` | `None` | Draft model, EAGLE head, or auxiliary model identifier. For `ngram`, `ngram_gpu`, `suffix`, and `mtp`, this can often be omitted. |
| `num_speculative_tokens` | `integer > 0` | `None` | Number of speculative tokens to propose per step. Required for methods that do not infer it from model metadata. |
| `draft_tensor_parallel_size` | `integer >= 1` | `None` | Tensor parallel size for the draft model. |
| `max_model_len` | `integer >= 1` | `None` | Maximum context length for the draft model. |
| `parallel_drafting` | `boolean` | `false` | Enable parallel draft token generation. Only compatible with EAGLE and draft-model methods. |
| `rejection_sample_method` | `string` | `strict` | `strict`, `probabilistic`, or `synthetic`. |
| `synthetic_acceptance_rate` | `float` | `None` | Average acceptance rate to target when `rejection_sample_method` is `synthetic`. Valid range is `[0, 1]`. |
### Method-specific keys
#### N-gram
| Key | Type | Default | Meaning |
| --- | --- | --- | --- |
| `prompt_lookup_max` | `integer >= 1` | `5` if both lookup bounds are omitted; otherwise mirrors `prompt_lookup_min` when omitted | Maximum n-gram window size. |
| `prompt_lookup_min` | `integer >= 1` | `5` if both lookup bounds are omitted; otherwise mirrors `prompt_lookup_max` when omitted | Minimum n-gram window size. |
Example:
```bash
vllm serve <target-model> \
--speculative-config '{
"method": "ngram",
"num_speculative_tokens": 4,
"prompt_lookup_min": 2,
"prompt_lookup_max": 5
}'
```
#### Suffix decoding
| Key | Type | Default | Meaning |
| --- | --- | --- | --- |
| `suffix_decoding_max_tree_depth` | `integer` | `24` | Maximum combined prefix-match and speculation tree depth. |
| `suffix_decoding_max_cached_requests` | `integer` | `10000` | Maximum number of requests cached in the global suffix tree. Set `0` to disable the global cache. |
| `suffix_decoding_max_spec_factor` | `float` | `1.0` | Caps speculative length as a multiple of prefix-match length. |
| `suffix_decoding_min_token_prob` | `float` | `0.1` | Minimum estimated token probability required to speculate a token. |
Example:
```bash
vllm serve <target-model> \
--speculative-config '{
"method": "suffix",
"num_speculative_tokens": 8,
"suffix_decoding_max_tree_depth": 24,
"suffix_decoding_max_cached_requests": 10000,
"suffix_decoding_max_spec_factor": 1.0,
"suffix_decoding_min_token_prob": 0.1
}'
```
### Notes
- `--speculative-config` expects a JSON object on the CLI. In YAML config
files, use a nested mapping instead of an escaped JSON string.
- `tensor_parallel_size` is not a valid key in `speculative_config`. Use
`draft_tensor_parallel_size` instead.
- Keys such as `temperature` and `top_p` are sampling parameters, not
`--speculative-config` fields.
- Internal fields such as `target_model_config`, `draft_model_config`,
`target_parallel_config`, `draft_parallel_config`, and `draft_load_config`
are populated by vLLM and are not intended to be set by users.
## Lossless guarantees of Speculative Decoding
In vLLM, speculative decoding aims to enhance inference efficiency while maintaining accuracy. This section addresses the lossless guarantees of
@@ -33,9 +33,9 @@ vllm serve Qwen/Qwen3-4B-Thinking-2507 \
--port 8000 \
--seed 42 \
-tp 1 \
--max_model_len 2048 \
--gpu_memory_utilization 0.8 \
--speculative_config '{"model": "Qwen/Qwen3-0.6B", "num_speculative_tokens": 5, "method": "draft_model"}'
--max-model-len 2048 \
--gpu-memory-utilization 0.8 \
--speculative-config '{"model": "Qwen/Qwen3-0.6B", "num_speculative_tokens": 5, "method": "draft_model"}'
```
The code used to request as completions as a client remains unchanged:
@@ -77,4 +77,8 @@ The code used to request as completions as a client remains unchanged:
```
!!! warning
Note: Please use `--speculative_config` to set all configurations related to speculative decoding. The previous method of specifying the model through `--speculative_model` and adding related parameters (e.g., `--num_speculative_tokens`) separately has been deprecated.
Note: Please use `--speculative-config` to set all configurations related
to speculative decoding. The previous method of specifying the model
through `--speculative-model` and adding related parameters such as
`--num-speculative-tokens` separately has been deprecated. For supported
keys and examples, see the [`--speculative-config` schema](README.md#--speculative-config-schema).
+1 -1
View File
@@ -38,7 +38,7 @@ for output in outputs:
```bash
vllm serve XiaomiMiMo/MiMo-7B-Base \
--tensor-parallel-size 1 \
--speculative_config '{"method":"mtp","num_speculative_tokens":1}'
--speculative-config '{"method":"mtp","num_speculative_tokens":1}'
```
## Notes
@@ -36,9 +36,9 @@ vllm serve Qwen/Qwen3-4B \
--port 8000 \
--seed 42 \
-tp 1 \
--max_model_len 2048 \
--gpu_memory_utilization 0.8 \
--speculative_config '{"model": "amd/PARD-Qwen3-0.6B", "num_speculative_tokens": 12, "method": "draft_model", "parallel_drafting": true}'
--max-model-len 2048 \
--gpu-memory-utilization 0.8 \
--speculative-config '{"model": "amd/PARD-Qwen3-0.6B", "num_speculative_tokens": 12, "method": "draft_model", "parallel_drafting": true}'
```
## Pre-trained PARD weights
+1
View File
@@ -472,6 +472,7 @@ th {
| `Qwen3MoeForCausalLM` | Qwen3MoE | `Qwen/Qwen3-30B-A3B`, etc. | ✅︎ | ✅︎ |
| `Qwen3NextForCausalLM` | Qwen3NextMoE | `Qwen/Qwen3-Next-80B-A3B-Instruct`, etc. | ✅︎ | ✅︎ |
| `RWForCausalLM` | Falcon RW | `tiiuae/falcon-40b`, etc. | | ✅︎ |
| `Rnj1ForCausalLM` | Rnj1 | `EssentialAI/rnj-1-instruct`, etc. | | |
| `SarvamMoEForCausalLM` | Sarvam 2 | `sarvamai/sarvam2-30b-a3b`, etc. | ✅︎ | ✅︎ |
| `SarvamMLAForCausalLM` | Sarvam 2 | `sarvamai/sarvam2-105b-a9b`, etc. | | ✅︎ |
| `SeedOssForCausalLM` | SeedOss | `ByteDance-Seed/Seed-OSS-36B-Instruct`, etc. | ✅︎ | ✅︎ |
@@ -12,7 +12,7 @@ import aiohttp
import msgpack
import regex as re
import zmq
from quart import Quart, make_response, request
from quart import Quart, Request, make_response, request
from vllm.distributed.kv_transfer.kv_connector.v1.moriio.moriio_common import (
MoRIIOConstants,
@@ -139,10 +139,13 @@ async def send_request_to_prefill(
return await response.json()
else:
raise RuntimeError(
"send_request_to_prefill response.status != 200response.status = ",
response.status,
error_message = (
f"send_request_to_prefill response ={response},"
f"reason={response.reason}, status={response.status},"
f"method={response.method}, url={response.url},"
f"real_url={response.real_url}"
)
raise RuntimeError(error_message)
async def start_decode_request(endpoint, req_data, request_id):
@@ -163,9 +166,13 @@ async def stream_decode_response(session, response, request_id):
async for chunk_bytes in response.content.iter_chunked(1024):
yield chunk_bytes
else:
raise RuntimeError(
f"decode response.status != 200, status = {response.status}"
error_message = (
f"stream_decode_response response ={response},"
f"reason={response.reason}, status={response.status},"
f"method={response.method}, url={response.url},"
f"real_url={response.real_url}"
)
raise RuntimeError(error_message)
finally:
await session.close()
@@ -175,8 +182,16 @@ def example_round_robin_dp_loader(request_number, dp_size):
@app.route("/v1/completions", methods=["POST"])
async def handle_completions_request():
return await handle_request("/completions", request)
@app.route("/v1/chat/completions", methods=["POST"])
async def handle_request():
async def handle_chat_completions_request():
return await handle_request("/chat/completions", request)
async def handle_request(api: str, request: Request):
try:
with _list_lock:
global request_nums
@@ -230,9 +245,10 @@ async def handle_request():
)
req_data_to_prefill["kv_transfer_params"]["transfer_id"] = transfer_id
prefill_request_url = prefill_instance_endpoint["request_address"] + api
send_prefill_task = asyncio.create_task(
send_request_to_prefill(
prefill_instance_endpoint["request_address"],
prefill_request_url,
req_data_to_prefill,
request_id,
decode_instance_endpoint,
@@ -241,7 +257,7 @@ async def handle_request():
selected_prefill_dp_rank,
)
)
ip, port = extract_ip_port_fast(prefill_instance_endpoint["request_address"])
ip, port = extract_ip_port_fast(prefill_request_url)
req_data["max_tokens"] -= 1
@@ -276,10 +292,9 @@ async def handle_request():
req_data["kv_transfer_params"]["remote_dp_rank"] = selected_prefill_dp_rank
req_data["kv_transfer_params"]["transfer_id"] = transfer_id
decode_request_url = decode_instance_endpoint["request_address"] + api
decode_request_task = asyncio.create_task(
start_decode_request(
decode_instance_endpoint["request_address"], req_data, request_id
)
start_decode_request(decode_request_url, req_data, request_id)
)
session, decode_response = await decode_request_task
@@ -27,7 +27,12 @@ from vllm.assets.audio import AudioAsset
def sync_openai(
audio_path: str, client: OpenAI, model: str, *, repetition_penalty: float = 1.3
audio_path: str,
client: OpenAI,
model: str,
*,
repetition_penalty: float = 1.3,
hotwords: str = None,
):
"""
Perform synchronous transcription using OpenAI-compatible API.
@@ -43,12 +48,15 @@ def sync_openai(
extra_body=dict(
seed=4419,
repetition_penalty=repetition_penalty,
hotwords=hotwords,
),
)
print("transcription result [sync]:", transcription.text)
async def stream_openai_response(audio_path: str, client: AsyncOpenAI, model: str):
async def stream_openai_response(
audio_path: str, client: AsyncOpenAI, model: str, hotwords: str = None
):
"""
Perform asynchronous transcription using OpenAI-compatible API.
"""
@@ -64,6 +72,7 @@ async def stream_openai_response(audio_path: str, client: AsyncOpenAI, model: st
extra_body=dict(
seed=420,
top_p=0.6,
hotwords=hotwords,
),
stream=True,
)
@@ -136,6 +145,7 @@ def main(args):
client=client,
model=model,
repetition_penalty=args.repetition_penalty,
hotwords=args.hotwords,
)
# Run the asynchronous function
@@ -146,7 +156,10 @@ def main(args):
)
asyncio.run(
stream_openai_response(
args.audio_path if args.audio_path else winning_call, client, model
args.audio_path if args.audio_path else winning_call,
client,
model,
hotwords=args.hotwords,
)
)
else:
@@ -174,5 +187,11 @@ if __name__ == "__main__":
default=1.3,
help="repetition penalty",
)
parser.add_argument(
"--hotwords",
type=str,
default=None,
help="hotwords",
)
args = parser.parse_args()
main(args)
+1 -3
View File
@@ -20,6 +20,4 @@ conch-triton-kernels==1.2.1
timm>=1.0.17
# amd-quark: required for Quark quantization on ROCm
# To be consistent with test_quark.py
amd-quark>=0.8.99
# Required for faster safetensors model loading
fastsafetensors @ git+https://github.com/foundation-model-stack/fastsafetensors.git@0.2.2
amd-quark>=0.8.99
+1 -3
View File
@@ -276,9 +276,7 @@ fastar==0.10.0
fastparquet==2026.3.0
# via genai-perf
fastsafetensors @ git+https://github.com/foundation-model-stack/fastsafetensors.git@65d80088fca7a8f567fba30415fbcc80f7d2259c
# via
# -c requirements/rocm.txt
# -r requirements/test/rocm.in
# via -r requirements/test/rocm.in
filelock==3.25.2
# via
# -c requirements/common.txt
+1 -1
View File
@@ -11,4 +11,4 @@ ray[default]
ray[data]
setuptools==78.1.0
nixl==0.3.0
tpu-inference==0.12.0
tpu-inference==0.18.0
+16
View File
@@ -116,6 +116,22 @@ def run_e2e_fusion_test(monkeypatch, caplog_mp_spawn):
model_kwargs["attention_config"] = {"backend": attn_backend.backend.name}
model_kwargs["tensor_parallel_size"] = tp_size
# Sparse MLA models (DSv3.2) hit an over-strict inductor assertion in
# decompose_auto_functionalized when +rotary_embedding is forced into
# the compile graph. Disable qk_norm+rope fusion (which auto-enables
# +rotary_embedding) for this combo to avoid the known torch bug.
# TODO: remove once upstream torch fix lands.
if requires_sparse:
if "pass_config" in compilation_config:
compilation_config["pass_config"].enable_qk_norm_rope_fusion = False
matches_check = [m for m in matches_check if m != "norm_rope_fusion"]
# DSv3.2 sparse indexer uses persistent_topk with k=config.index_topk
# (2048 for the default config). max_model_len must be >= index_topk
# or the topk kernel raises "k out of range" at runtime.
model_kwargs["max_model_len"] = max(
model_kwargs.get("max_model_len", 0), 2048
)
# Always compile the full graph instead of piecewise
if not compilation_config["use_inductor_graph_partition"]:
compilation_config["splitting_ops"] = []
+19 -5
View File
@@ -59,7 +59,10 @@ TRITON_MLA_ATTN = pytest.param(
)
FLASHMLA_SPARSE_ATTN = pytest.param(
AttentionBackendCase(backend=AttentionBackendEnum.FLASHMLA_SPARSE),
AttentionBackendCase(
backend=AttentionBackendEnum.FLASHMLA_SPARSE,
model_kwargs=dict(kv_cache_dtype="fp8_ds_mla"),
),
id="FLASHMLA_SPARSE",
marks=pytest.mark.skipif(
not is_blackwell(),
@@ -173,9 +176,8 @@ deepseek_v3_fp8 = ModelFusionInfo(
rms_quant_fusion=n_layers * 2 + min(3, n_layers), # add for 3 dense layers
# silu+block quant
act_quant_fusion=min(3, n_layers), # dense layers only
# MLA attn + per-group FP8 quant not supported yet:
# https://github.com/vllm-project/vllm/issues/35792
attn_quant_fusion=0,
# MLA attn + per-group FP8 quant
attn_quant_fusion=n_layers,
ar_rms_fusion=n_layers * 2 + 1,
# TODO
# sequence_parallel= n_layers * 2 + 1,
@@ -183,11 +185,23 @@ deepseek_v3_fp8 = ModelFusionInfo(
),
)
deepseek_r1_fp4 = ModelFusionInfo(
model_name="nvidia/DeepSeek-R1-0528-NVFP4-v2",
matches=lambda n_layers: Matches(
rms_quant_fusion=0,
act_quant_fusion=min(3, n_layers),
attn_quant_fusion=n_layers,
ar_rms_fusion=n_layers * 2 + 1,
),
)
deepseek_v32_fp4 = ModelFusionInfo(
model_name="nvidia/DeepSeek-V3.2-NVFP4",
matches=lambda n_layers: Matches(
rms_quant_fusion=0,
act_quant_fusion=0,
# silu+quant on dense layers only; MoE hides the act+quant site
act_quant_fusion=min(3, n_layers),
# MLA attn + NVFP4 output quant fuses on sparse MLA output path
attn_quant_fusion=n_layers,
ar_rms_fusion=n_layers * 2 + 1,
),
+3 -2
View File
@@ -24,6 +24,7 @@ from .models import (
TRITON_ATTN,
TRITON_MLA_ATTN,
deepseek_coder_v2_lite_fp8,
deepseek_r1_fp4,
deepseek_v3_fp8,
deepseek_v32_fp4,
llama3_8b_fp4,
@@ -148,11 +149,11 @@ def test_tp1_fp8_fusions(
@pytest.mark.parametrize(
"model_name, matches_fn, model_kwargs, hf_overrides",
[llama3_8b_fp4, llama4_scout_fp4, deepseek_v32_fp4],
[llama3_8b_fp4, llama4_scout_fp4, deepseek_r1_fp4, deepseek_v32_fp4],
)
@pytest.mark.parametrize(
"attn_backend",
[FLASHINFER_ATTN, FLASHMLA_SPARSE_ATTN],
[FLASHINFER_ATTN, FLASHINFER_MLA_ATTN, FLASHMLA_SPARSE_ATTN],
)
@pytest.mark.parametrize("n_layers", [6])
@pytest.mark.parametrize("custom_ops", custom_ops_combos("rms_norm"))
+3 -2
View File
@@ -21,6 +21,7 @@ from .models import (
FLASHMLA_SPARSE_ATTN,
TRITON_ATTN,
deepseek_coder_v2_lite_fp8,
deepseek_r1_fp4,
deepseek_v3_fp8,
deepseek_v32_fp4,
gpt_oss_20b,
@@ -113,11 +114,11 @@ def test_tp2_ar_rms_fp8_fusions(
@multi_gpu_test(num_gpus=2)
@pytest.mark.parametrize(
"model_name, matches_fn, model_kwargs, hf_overrides",
[llama3_8b_fp4, llama4_scout_fp4, deepseek_v32_fp4],
[llama3_8b_fp4, llama4_scout_fp4, deepseek_r1_fp4, deepseek_v32_fp4],
)
@pytest.mark.parametrize(
"attn_backend",
[FLASHINFER_ATTN, FLASHMLA_SPARSE_ATTN],
[FLASHINFER_ATTN, FLASHINFER_MLA_ATTN, FLASHMLA_SPARSE_ATTN],
)
@pytest.mark.parametrize("n_layers", [4])
@pytest.mark.parametrize("custom_ops", custom_ops_combos("rms_norm"))
@@ -29,12 +29,18 @@ from vllm.config import (
set_current_vllm_config,
)
from vllm.forward_context import get_forward_context, set_forward_context
from vllm.model_executor.kernels.linear.scaled_mm.cutlass import (
CutlassFp8BlockScaledMMKernel,
)
from vllm.model_executor.layers.attention import MLAAttention
from vllm.model_executor.layers.linear import ColumnParallelLinear
from vllm.model_executor.layers.quantization.fp8 import Fp8Config
from vllm.model_executor.layers.quantization.modelopt import ModelOptNvFp4Config
from vllm.model_executor.layers.quantization.utils.quant_utils import (
GroupShape,
QuantKey,
create_fp8_quant_key,
kFp8Dynamic128Sym,
kFp8StaticTensorSym,
kNvfp4Dynamic,
)
@@ -77,10 +83,6 @@ class MLAAttentionQuantPatternModel(torch.nn.Module):
self.vllm_config = vllm_config
self.dtype = vllm_config.model_config.dtype
# Create kv_b_proj (ColumnParallelLinear) on device.
# Reuse weights from prior model instance when available, because
# ColumnParallelLinear may get NaN from recycled CUDA memory after
# torch.compile runs in the same process.
kv_b_proj = ColumnParallelLinear(
input_size=kv_lora_rank,
output_size=num_heads * (qk_nope_head_dim + v_head_dim),
@@ -90,8 +92,7 @@ class MLAAttentionQuantPatternModel(torch.nn.Module):
kv_b_proj_weight = kwargs.get("kv_b_proj_weight")
if kv_b_proj_weight is not None:
kv_b_proj.weight.data.copy_(kv_b_proj_weight)
elif kv_b_proj.weight.data.isnan().any():
# Sanitize NaN from recycled CUDA memory
else:
kv_b_proj.weight.data.normal_()
# Create MLAAttention
@@ -279,6 +280,67 @@ class TestMLAAttentionNvfp4QuantPatternModel(MLAAttentionQuantPatternModel):
)
class TestMLAAttentionFp8GroupQuantPatternModel(MLAAttentionQuantPatternModel):
"""Test model for MLA Attention + per-group FP8 (block quant) fusion."""
quant_key = kFp8Dynamic128Sym
quant_config = Fp8Config(
is_checkpoint_fp8_serialized=True,
weight_block_size=[128, 128],
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
weight_quant_key = create_fp8_quant_key(
static=True, group_shape=GroupShape(128, 128)
)
device = kwargs.get("device", torch.device("cuda:0"))
# Subclass to set weight_block_size before process_weights_after_loading
class _BlockFP8Layer(TestFP8Layer):
def __init__(self, *a, **kw):
self.weight_block_size = [128, 128]
super().__init__(*a, **kw)
# Force CutlassFp8BlockScaledMMKernel to ensure the graph uses
# per_token_group_fp8_quant (not the deepgemm packed variant).
self.block_fp8_linear = _BlockFP8Layer(
weight_shape=(self.output_dim, self.output_dim),
activation_quant_key=self.quant_key,
weight_quant_key=weight_quant_key,
input_dtype=self.dtype,
device=device,
force_kernel=CutlassFp8BlockScaledMMKernel,
)
w = kwargs.get("w")
if w is not None:
self.block_fp8_linear.weight = w["weight"]
# Block-wise uses weight_scale_inv, not weight_scale
self.block_fp8_linear.weight_scale_inv = w["wscale"]
self.w = {
"weight": self.block_fp8_linear.weight,
"wscale": self.block_fp8_linear.weight_scale_inv,
}
def forward(
self,
q: torch.Tensor,
kv_c_normed: torch.Tensor,
k_pe: torch.Tensor,
):
"""Forward pass: MLA attention -> block FP8 linear (group quant)."""
attn_output = self.mla_attn(
q,
kv_c_normed,
k_pe,
output_shape=(q.shape[0], self.output_dim),
)
return self.block_fp8_linear(attn_output)
def is_nvfp4_supported():
return current_platform.has_device_capability(100)
@@ -286,6 +348,7 @@ def is_nvfp4_supported():
# MLA test configuration
MLA_DIMS: list[tuple[int, int, int, int, int]] = []
PATTERN_TEST_MODELS_MLA_FP8: list[tuple[str, type]] = []
PATTERN_TEST_MODELS_MLA_GROUP_FP8: list[tuple[str, type]] = []
PATTERN_TEST_MODELS_MLA_FP4: list[tuple[str, type]] = []
BACKENDS_MLA_FP8: list[AttentionBackendEnum] = []
BACKENDS_MLA_FP4: list[AttentionBackendEnum] = []
@@ -299,6 +362,12 @@ if current_platform.is_cuda():
TestMLAAttentionFp8StaticQuantPatternModel,
)
]
PATTERN_TEST_MODELS_MLA_GROUP_FP8 = [
(
"deepseek-ai/DeepSeek-V3",
TestMLAAttentionFp8GroupQuantPatternModel,
)
]
PATTERN_TEST_MODELS_MLA_FP4 = [
(
"deepseek-ai/DeepSeek-V2-Lite",
@@ -324,6 +393,13 @@ if current_platform.is_cuda():
["+quant_fp8", "-quant_fp8"],
)
)
+ list(
flat_product(
BACKENDS_MLA_FP8,
PATTERN_TEST_MODELS_MLA_GROUP_FP8,
["+quant_fp8"],
)
)
+ list(flat_product(BACKENDS_MLA_FP4, PATTERN_TEST_MODELS_MLA_FP4, [""])),
)
@pytest.mark.skipif(
@@ -470,12 +546,13 @@ def test_mla_attention_quant_pattern(
)
# Check quantization ops in the graph
is_per_group = quant_key.scale.group_shape.is_per_group()
quant_op = (
torch.ops.aten.reciprocal
if "-quant_fp8" in custom_ops_list
else QUANT_OPS[quant_key]
)
test_backend.check_before_ops([quant_op], fully_replaced=quant_key is kNvfp4Dynamic)
test_backend.check_before_ops([quant_op], fully_replaced=is_per_group)
assert attn_pass.pass_.matched_count == sum(attn_fusion_supported)
@@ -487,25 +564,24 @@ def test_mla_attention_quant_pattern(
assert len(attn_nodes_pre) == len(attn_nodes_post), (
"Should have same number of MLA attention nodes before and after fusion"
)
assert attn_nodes_pre[0].kwargs.get("output_scale") is None, (
"MLA attention should not have output_scale before fusion"
)
assert attn_nodes_post[0].kwargs.get("output_scale") is not None, (
"MLA attention should have output_scale after fusion"
)
assert attn_nodes_pre[0].kwargs.get("output_block_scale") is None, (
"MLA attention should not have output_block_scale before fusion"
)
# Before fusion: neither scale should be set
assert attn_nodes_pre[0].kwargs.get("output_scale") is None
assert attn_nodes_pre[0].kwargs.get("output_block_scale") is None
if quant_key.dtype == FP8_DTYPE:
assert attn_nodes_post[0].kwargs.get("output_block_scale") is None, (
"MLA attention should not have output_block_scale after FP8 fusion"
)
elif quant_key.dtype == FP4_DTYPE:
assert attn_nodes_post[0].kwargs.get("output_block_scale") is not None, (
"MLA attention should have output_block_scale after FP4 fusion"
)
# After fusion: derive expected scale presence from quant_key properties.
# - output_scale: present for static quant or non-FP8 (NVFP4 carries input_scale)
# - output_block_scale: present when quant uses per-group/block scaling
has_output_scale = attn_nodes_post[0].kwargs.get("output_scale") is not None
has_block_scale = attn_nodes_post[0].kwargs.get("output_block_scale") is not None
expects_output_scale = quant_key.scale.static or quant_key.dtype != FP8_DTYPE
assert has_output_scale == expects_output_scale, (
f"output_scale: expected present={expects_output_scale}, got {has_output_scale}"
)
assert has_block_scale == is_per_group, (
f"output_block_scale: expected present={is_per_group}, got {has_block_scale}"
)
# Check numerical correctness
torch.testing.assert_close(result_unfused, result_fused, atol=1e-2, rtol=1e-2)
+1 -1
View File
@@ -29,7 +29,7 @@ llm = LLM(
tensor_parallel_size=2,
pipeline_parallel_size=int(os.getenv("PP_SIZE", 1)),
distributed_executor_backend="external_launcher",
gpu_memory_utilization=random.uniform(0.7, 0.9),
gpu_memory_utilization=random.uniform(0.8, 0.92),
seed=0,
)
@@ -36,7 +36,7 @@ llm = LLM(
pipeline_parallel_size=int(os.getenv("PP_SIZE", "1")),
enable_expert_parallel=int(os.getenv("ENABLE_EP", "0")) == 1,
distributed_executor_backend="external_launcher",
gpu_memory_utilization=random.uniform(0.7, 0.9),
gpu_memory_utilization=random.uniform(0.8, 0.92),
seed=0,
max_model_len=1024,
max_num_seqs=16,
@@ -77,8 +77,8 @@ def _worker_parallel_launch(
*args: Any,
) -> None:
rank = node_rank * world_local_size + local_rank
torch.accelerator.set_device_index(local_rank)
device = torch.device("cuda", local_rank)
torch.accelerator.set_device_index(device)
torch.distributed.init_process_group(
backend="cpu:gloo,cuda:nccl",
init_method=init_method,
+69
View File
@@ -202,3 +202,72 @@ def test_fused_topk_nan_inf_clamp(
f"Row {row} has non-finite weights {topk_weights[row].tolist()} "
f"(bad_value={bad_value}, scoring_func={scoring_func})"
)
@pytest.mark.skipif(
not current_platform.is_cuda(), reason="This test is skipped on non-CUDA platform."
)
@pytest.mark.parametrize("num_experts", [6, 8, 16])
@pytest.mark.parametrize("topk", [3, 4])
@pytest.mark.parametrize("scoring_func", ["softmax", "sigmoid"])
@pytest.mark.parametrize("bad_value", [float("nan"), float("inf")])
@pytest.mark.parametrize("dtype", [torch.bfloat16, torch.half, torch.float32])
def test_fused_topk_bias_nan_inf_clamp(
num_experts: int,
topk: int,
scoring_func: str,
bad_value: float,
dtype: torch.dtype,
):
"""Regression test: NaN/Inf in gating logits must not produce duplicate
expert IDs or non-finite weights when e_score_correction_bias is present.
Same scenario as test_fused_topk_nan_inf_clamp but exercising the bias
path (fused_topk_bias) so the fix in topk_softmax_kernels.cu is covered
for that entry point as well.
"""
torch.manual_seed(0)
num_tokens = 4
hidden_size = 1024
hidden_states = torch.randn((num_tokens, hidden_size), dtype=dtype, device="cuda")
e_score_correction_bias = torch.randn(
(num_experts,), dtype=torch.float32, device="cuda"
)
gating_output = torch.randn((num_tokens, num_experts), dtype=dtype, device="cuda")
gating_output[1:, :] = bad_value
topk_weights, topk_ids = fused_topk_bias(
hidden_states=hidden_states,
gating_output=gating_output,
e_score_correction_bias=e_score_correction_bias,
topk=topk,
renormalize=False,
scoring_func=scoring_func,
)
# Normal row must still match the torch reference.
ref_weights, ref_ids = torch_topk(
gating_output=gating_output[:1],
topk=topk,
renormalize=False,
e_score_correction_bias=e_score_correction_bias,
scoring_func=scoring_func,
)
torch.testing.assert_close(
ref_weights.to(torch.float32), topk_weights[:1], atol=1e-2, rtol=1e-2
)
torch.testing.assert_close(ref_ids.to(torch.int32), topk_ids[:1], atol=0, rtol=0)
# Poisoned rows: IDs must be unique (no duplicates) and weights must be
# finite (no NaN/Inf propagation into downstream MoE kernels).
for row in range(1, num_tokens):
row_ids = topk_ids[row]
assert row_ids.unique().numel() == topk, (
f"Row {row} has duplicate expert IDs {row_ids.tolist()} "
f"(bad_value={bad_value}, scoring_func={scoring_func})"
)
assert torch.isfinite(topk_weights[row]).all(), (
f"Row {row} has non-finite weights {topk_weights[row].tolist()} "
f"(bad_value={bad_value}, scoring_func={scoring_func})"
)
+126 -48
View File
@@ -17,6 +17,7 @@ from typing import get_args
import pytest
import torch
import vllm.model_executor.layers.quantization.utils.w8a8_utils
from tests.kernels.moe.modular_kernel_tools.parallel_utils import (
ProcessGroupInfo,
_set_vllm_config,
@@ -37,7 +38,7 @@ from vllm.distributed.parallel_state import (
get_eplb_group,
)
from vllm.forward_context import set_forward_context
from vllm.model_executor.layers.fused_moe import FusedMoE, SharedFusedMoE, fused_experts
from vllm.model_executor.layers.fused_moe import FusedMoE, fused_experts
from vllm.model_executor.layers.fused_moe.activation import MoEActivation
from vllm.model_executor.layers.fused_moe.config import FusedMoEQuantConfig
from vllm.model_executor.layers.fused_moe.router.router_factory import (
@@ -65,8 +66,8 @@ fp8_dtype = torch.float8_e4m3fn # current_platform.fp8_dtype
SHAPE_COMBOS = [
(1, 128, 256),
(32, 1024, 512),
(222, 2048, 2048),
(32, 512, 512),
(222, 1024, 2048),
]
MAX_M = max([x[0] for x in SHAPE_COMBOS])
@@ -95,7 +96,7 @@ if has_flashinfer_nvlink_one_sided():
BACKENDS += ["flashinfer_nvlink_one_sided"]
if has_deep_ep():
BACKENDS += ["deepep_low_latency", "deepep_high_throughput"]
BACKENDS += ["deepep_high_throughput", "deepep_low_latency"]
if has_nixl_ep():
BACKENDS += ["nixl_ep"]
@@ -103,6 +104,7 @@ if has_nixl_ep():
QUANT_METHODS = [
None,
"fp8",
"fp8_blocked",
"modelopt_fp8",
"modelopt_fp4",
]
@@ -114,10 +116,21 @@ BACKEND_SUPPORTED_QUANTS: dict[str, set[str | None]] = {
"mori": {None, "fp8", "modelopt_fp8"},
"flashinfer_nvlink_two_sided": {None, "modelopt_fp8", "modelopt_fp4"},
"flashinfer_nvlink_one_sided": {None, "modelopt_fp8", "modelopt_fp4"},
"deepep_low_latency": {None, "modelopt_fp8", "modelopt_fp4"},
"deepep_high_throughput": {None, "fp8", "modelopt_fp8", "modelopt_fp4"},
"deepep_low_latency": {None, "fp8_blocked", "modelopt_fp4"},
"deepep_high_throughput": {None, "fp8_blocked", "modelopt_fp8", "modelopt_fp4"}, # noqa: E501
"nixl_ep": {None, "fp8", "modelopt_fp8"},
}
# Map from backend -> (DP/EP support, DP support, TP support)
BACKEND_EP_DP_TP_SUPPORT: dict[str, tuple[bool, bool, bool]] = {
"allgather_reducescatter": (True, True, True),
"mori": (True, False, False),
"flashinfer_nvlink_two_sided": (False, True, False),
"flashinfer_nvlink_one_sided": (False, True, False),
"deepep_low_latency": (True, False, False),
"deepep_high_throughput": (True, False, False),
"nixl_ep": (True, False, False),
}
# fmt: on
# Which quantization methods support EPLB.
@@ -132,6 +145,24 @@ EPLB_SUPPORTED_QUANTS: list[str | None] = [None, "fp8"]
EPLB_SUPPORTED_BACKENDS: list[str] = ["allgather_reducescatter"]
def mock_normalize_e4m3fn_to_e4m3fnuz(
weight: torch.Tensor,
weight_scale: torch.Tensor,
input_scale: torch.Tensor | None = None,
):
return weight, weight_scale, input_scale
# Needed since weights will already be in e4m3fnuz format on platforms that
# use the fnuz fp8 format and the normalize_e4m3fn_to_e4m3fnuz() function
# is not being tested here.
# NOTE: The weights are quantized by moe_quantize_weights_2d in
# _quantize_fp8_halves.
# NOTE: Not able to use monkeypatch because of the spawned parallel workers.
def override_normalize_e4m3fn_to_e4m3fnuz():
vllm.model_executor.layers.quantization.utils.w8a8_utils.normalize_e4m3fn_to_e4m3fnuz = mock_normalize_e4m3fn_to_e4m3fnuz # noqa: E501
def maybe_roundup_layer_hidden_size(
hidden_size: int,
act_dtype: torch.dtype,
@@ -424,27 +455,35 @@ def is_valid_config(config: MoETestConfig) -> tuple[bool, str | None]:
f"Skipping unsupported K {config.k} in {config.backend} w/o EP.",
)
if config.enable_eplb and config.ep_size == 1:
return False, "EPLB requires EP."
if config.backend is not None:
supports_ep_dp, supports_dp, supports_tp = BACKEND_EP_DP_TP_SUPPORT[
config.backend
]
if config.enable_eplb and config.quantization not in EPLB_SUPPORTED_QUANTS:
return False, f"EPLB not supported with {config.quantization} quantization."
if config.tp_size > 1 and not supports_tp:
return False, f"{config.backend} does not support TP."
if config.enable_eplb and config.backend not in EPLB_SUPPORTED_BACKENDS:
return False, f"EPLB not supported with {config.backend}."
if config.dp_size > 1 and config.ep_size == 1 and not supports_dp:
return False, f"{config.backend} does not support DP."
if (
config.backend is not None
and config.backend.startswith("flashinfer_nvlink")
and config.ep_size > 1
):
return False, "flashinfer_nvlink EP not yet supported."
if config.dp_size > 1 and config.ep_size > 1 and not supports_ep_dp:
return False, f"{config.backend} does not support EP/DP."
else:
if config.tp_size > 1 or config.ep_size > 1 or config.dp_size > 1:
return False, "An all2all backend is required for parallelism."
if config.enable_eplb and config.num_experts % config.dp_size != 0:
return False, "EPLB requires num_experts divisible by ep_size"
if config.enable_eplb:
if config.ep_size == 1:
return False, "EPLB requires EP."
if config.enable_eplb and config.ep_size == 1:
return False, "EPLB only works with EP+DP"
if config.quantization not in EPLB_SUPPORTED_QUANTS:
return False, f"EPLB not supported with {config.quantization} quantization."
if config.backend not in EPLB_SUPPORTED_BACKENDS:
return False, f"EPLB not supported with {config.backend}."
if config.num_experts % config.dp_size != 0:
return False, "EPLB requires num_experts divisible by ep_size"
# Disable fp4 tests until flashinfer is updated or the Dockerfile is
# modified to install cublasLt.h. See #39525.
@@ -507,27 +546,48 @@ class QuantizedWeights:
def _quantize_fp8_halves(
w1: torch.Tensor,
w2: torch.Tensor,
block_shape: list[int] | None = None,
) -> QuantizedWeights:
"""Quantize w13 gate/up halves separately to FP8, producing per-shard scales."""
half = w1.shape[1] // 2
w1q_a, w1s_a, _ = moe_quantize_weights(
w1[:, :half, :], None, fp8_dtype, False, None
w1[:, :half, :],
None,
fp8_dtype,
False,
block_shape,
)
w1q_b, w1s_b, _ = moe_quantize_weights(
w1[:, half:, :], None, fp8_dtype, False, None
w1[:, half:, :],
None,
fp8_dtype,
False,
block_shape,
)
assert w1s_a is not None and w1s_b is not None
w2q, w2s, _ = moe_quantize_weights(w2, None, fp8_dtype, False, None)
w2q, w2s, _ = moe_quantize_weights(w2, None, fp8_dtype, False, block_shape)
assert w2s is not None
if block_shape is not None:
# Blocked quantization: scales have shape (E, n_tiles, k_tiles)
# Concatenate gate and up scales along the n_tiles dimension (dim=1)
# to match the concatenation of gate and up weights
w13_weight_scale = torch.cat([w1s_a, w1s_b], dim=1)
# w2 scales keep their blocked shape (E, k_tiles, n_tiles)
w2_weight_scale = w2s
else:
# Non-blocked quantization: scales have shape (E, 1, 1)
# Each w1s_x is (E, 1, 1) -> reshape to (E, 1), cat to (E, 2)
w13_weight_scale = torch.cat([w1s_a.view(-1, 1), w1s_b.view(-1, 1)], dim=1)
# w2s is (E, 1, 1) -> reshape to (E,)
w2_weight_scale = w2s.view(-1)
return QuantizedWeights(
w13_weight=torch.cat([w1q_a, w1q_b], dim=1),
w2_weight=w2q,
# Each w1s_x is (E, 1, 1) -> reshape to (E, 1), cat to (E, 2)
w13_weight_scale=torch.cat([w1s_a.view(-1, 1), w1s_b.view(-1, 1)], dim=1),
# w2s is (E, 1, 1) -> reshape to (E,)
w2_weight_scale=w2s.view(-1),
w13_weight_scale=w13_weight_scale,
w2_weight_scale=w2_weight_scale,
)
@@ -536,7 +596,7 @@ def quantization_to_quant_dtype(
) -> torch.dtype | str | None:
if quantization is None:
return None
elif quantization in ["fp8", "modelopt_fp8"]:
elif quantization in ["fp8", "fp8_blocked", "modelopt_fp8"]:
return fp8_dtype
elif quantization in ["modelopt_fp4"]:
return "nvfp4"
@@ -558,6 +618,12 @@ def make_quant_config(
if quantization == "fp8":
return Fp8Config(True), _quantize_fp8_halves(w1, w2)
if quantization == "fp8_blocked":
block_shape = [128, 128]
return Fp8Config(True, weight_block_size=block_shape), _quantize_fp8_halves(
w1, w2, block_shape
)
if quantization == "modelopt_fp8":
qw = _quantize_fp8_halves(w1, w2)
# why?
@@ -858,11 +924,7 @@ def make_fused_moe_layer(
quant_config, qw = make_quant_config(quantization, w1, w2, global_num_experts)
kwargs = dict()
if shared_experts is None:
builder = FusedMoE
else:
builder = SharedFusedMoE
kwargs["shared_experts"] = shared_experts
kwargs["shared_experts"] = shared_experts
# Add gate and routed_input_transform if provided
if gate is not None:
@@ -872,7 +934,7 @@ def make_fused_moe_layer(
kwargs["routed_input_transform"] = routed_input_transform
kwargs["routed_output_transform"] = routed_output_transform
layer = builder(
layer = FusedMoE(
num_experts=global_num_experts,
top_k=top_k,
hidden_size=hidden_size,
@@ -900,11 +962,13 @@ def make_fused_moe_layer(
**kwargs,
)
weight_scale_name = getattr(layer.quant_method, "weight_scale_name", "weight_scale")
for name, value in [
("w13_weight", qw.w13_weight),
("w2_weight", qw.w2_weight),
("w13_weight_scale", qw.w13_weight_scale),
("w2_weight_scale", qw.w2_weight_scale),
(f"w13_{weight_scale_name}", qw.w13_weight_scale),
(f"w2_{weight_scale_name}", qw.w2_weight_scale),
("w13_weight_scale_2", qw.w13_weight_scale_2),
("w2_weight_scale_2", qw.w2_weight_scale_2),
("w13_input_scale", qw.w13_input_scale),
@@ -926,7 +990,7 @@ def make_fake_moe_layer(
top_k: int,
global_num_experts: int,
in_dtype: torch.dtype,
quant_dtype: torch.dtype | None,
quantization: str | None,
renormalize: bool = False,
shared_experts_config: SharedExpertsConfig | None = None,
use_grouped_topk: bool = False,
@@ -952,6 +1016,7 @@ def make_fake_moe_layer(
dp_size: int = 1,
ep_size: int = 1,
) -> Callable:
quant_dtype = None
activation = MoEActivation.from_str(activation)
router = create_fused_moe_router(
@@ -1143,7 +1208,6 @@ def _test_body_eplb(
routed_output_transform=routed_output_transform,
)
# Necessary?
if eplb_moe_layer._expert_map is not None:
eplb_moe_layer._expert_map = eplb_moe_layer._expert_map.to(device)
@@ -1271,6 +1335,7 @@ def _run_one_config(
gate = test_data.gate
routed_input_transform = test_data.routed_input_transform
routed_output_transform = test_data.routed_output_transform
activation = "silu"
baseline_layer = make_fake_moe_layer(
w1=w1,
@@ -1278,7 +1343,7 @@ def _run_one_config(
top_k=top_k,
global_num_experts=num_experts,
in_dtype=in_dtype,
quant_dtype=None, # quantization_to_quant_dtype(quantization),
quantization=quantization,
renormalize=False,
shared_experts_config=shared_experts_config,
gate=gate,
@@ -1288,6 +1353,7 @@ def _run_one_config(
tp_size=tp_size,
ep_size=ep_size,
dp_size=dp_size,
activation=activation,
)
baseline_output = baseline_layer(hidden_states, router_logits)
@@ -1332,9 +1398,9 @@ def _run_one_config(
gate=gate,
routed_input_transform=routed_input_transform,
routed_output_transform=routed_output_transform,
activation=activation,
)
# Necessary?
if moe_layer._expert_map is not None:
moe_layer._expert_map = moe_layer._expert_map.to(device)
@@ -1381,13 +1447,17 @@ def _run_one_config(
atol, rtol = 7.6e-2, 7.6e-2
else:
atol, rtol = 3.5e-2, 3.5e-2
elif quantization in ("fp8", "modelopt_fp8"):
if k >= 2048:
atol, rtol = 7.6e-2, 7.6e-2
else:
atol, rtol = 6e-2, 6e-2
elif quantization in ("fp8", "fp8_blocked", "modelopt_fp8"):
atol, rtol = 6e-2, 6e-2
elif quantization == "modelopt_fp4":
atol = rtol = 1e-1 + k * 5e-4
if k >= 2048:
atol = rtol = 1e-1 + (k * 1e-4)
else:
atol = rtol = 1e-1
if backend == "allgather_reducescatter" and tp_size > 1:
atol += 2e-1
rtol += 2e-1
else:
atol, rtol = 6e-2, 6e-2
@@ -1420,6 +1490,11 @@ def test_moe_layer_no_parallel(
if os.environ.get("VLLM_LOGGING_LEVEL") is None:
monkeypatch.setenv("VLLM_LOGGING_LEVEL", "ERROR")
# Needed since weights will already be in e4m3fnuz format and the
# normalize_e4m3fn_to_e4m3fnuz() function is not being tested here.
if current_platform.is_fp8_fnuz():
override_normalize_e4m3fn_to_e4m3fnuz()
test_config = MoETestConfig(
m,
n,
@@ -1495,6 +1570,9 @@ def _parallel_worker(
dp_rank = vllm_config.parallel_config.data_parallel_rank
if current_platform.is_fp8_fnuz():
override_normalize_e4m3fn_to_e4m3fnuz()
for test_config in test_configs:
cc = vllm_config.compilation_config
if "from_forward_context" in cc.static_forward_context:
@@ -1,9 +1,9 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""
Tests for SharedFusedMoE with routed_input_transform.
Tests for FusedMoE with routed_input_transform.
Verifies that applying routed_input_transform inside SharedFusedMoE
Verifies that applying routed_input_transform inside FusedMoE
produces the same results as applying the transform manually outside.
"""
@@ -13,7 +13,7 @@ import torch.nn as nn
from vllm.config import VllmConfig, set_current_vllm_config
from vllm.forward_context import set_forward_context
from vllm.model_executor.layers.fused_moe.shared_fused_moe import SharedFusedMoE
from vllm.model_executor.layers.fused_moe import FusedMoE
from vllm.platforms import current_platform
from vllm.utils.torch_utils import is_torch_equal_or_newer, set_random_seed
@@ -133,9 +133,9 @@ def test_routed_input_transform_inside_vs_outside(
workspace_init,
monkeypatch,
):
"""Compare SharedFusedMoE with transform inside vs manually applying outside.
Method A (inside): SharedFusedMoE with routed_input_transform
Method B (outside): Manually transform, then SharedFusedMoE without transform
"""Compare FusedMoE with transform inside vs manually applying outside.
Method A (inside): FusedMoE with routed_input_transform
Method B (outside): Manually transform, then FusedMoE without transform
"""
if current_platform.is_rocm():
monkeypatch.setenv("VLLM_ROCM_USE_AITER", "1" if use_rocm_aiter else "0")
@@ -157,8 +157,8 @@ def test_routed_input_transform_inside_vs_outside(
routed_transform = SimpleLinear(hidden_size, latent_size, dtype)
with set_current_vllm_config(vllm_config):
# Method A: SharedFusedMoE WITH routed_input_transform
moe_with_transform = SharedFusedMoE(
# Method A: FusedMoE WITH routed_input_transform
moe_with_transform = FusedMoE(
shared_experts=shared_experts,
routed_input_transform=routed_transform,
num_experts=num_experts,
@@ -173,9 +173,9 @@ def test_routed_input_transform_inside_vs_outside(
prefix="moe_with_transform",
)
# Method B: SharedFusedMoE WITHOUT routed_input_transform
# Method B: FusedMoE WITHOUT routed_input_transform
# Note: shared_experts=None because when transform is done outside,
moe_without_transform = SharedFusedMoE(
moe_without_transform = FusedMoE(
shared_experts=None,
routed_input_transform=None,
num_experts=num_experts,
+270 -2
View File
@@ -44,6 +44,7 @@ from vllm.model_executor.layers.vocab_parallel_embedding import (
VocabParallelEmbedding,
get_masked_input_and_mask,
)
from vllm.model_executor.models.deepseek_v2 import DeepSeekV2FusedQkvAProjLinear
from vllm.platforms import current_platform
from vllm.utils.torch_utils import set_random_seed
@@ -1422,7 +1423,107 @@ def test_variable_slice_lora_class_selection(default_vllm_config, dist_init):
f"for 2 packed modules, got {type(selected_layer_merged).__name__}"
)
# Case 5: Plain ColumnParallelLinear (not merged) - common in many models
fully_sharded_tp_lora_config = LoRAConfig(
max_loras=8,
max_lora_rank=16,
lora_dtype=torch.float16,
fully_sharded_loras=True,
)
fully_sharded_tp_layer = MergedColumnParallelLinear(
4096, [2048, 2048], bias=False, params_dtype=torch.float16
)
fully_sharded_tp_layer.tp_size = 2
assert not MergedColumnParallelLinearWithLoRA.can_replace_layer(
source_layer=fully_sharded_tp_layer,
lora_config=fully_sharded_tp_lora_config,
packed_modules_list=packed_modules_two,
), "Generic merged wrapper should reject fully sharded TP layers"
assert MergedColumnParallelLinearWithShardedLoRA.can_replace_layer(
source_layer=fully_sharded_tp_layer,
lora_config=fully_sharded_tp_lora_config,
packed_modules_list=packed_modules_two,
), "Sharded merged wrapper should remain eligible for fully sharded TP layers"
selected_fully_sharded_tp_layer = from_layer(
fully_sharded_tp_layer,
max_loras=8,
lora_config=fully_sharded_tp_lora_config,
packed_modules_list=packed_modules_two,
)
assert isinstance(
selected_fully_sharded_tp_layer,
MergedColumnParallelLinearWithShardedLoRA,
), (
"from_layer should select MergedColumnParallelLinearWithShardedLoRA "
"for fully sharded TP merged layers, got "
f"{type(selected_fully_sharded_tp_layer).__name__}"
)
# Case 5: DeepSeek's fused_qkv_a_proj should reuse the generic merged
# wrapper while preserving its custom base forward path.
deepseek_fused_layer = DeepSeekV2FusedQkvAProjLinear(
4096, [2048, 2048], prefix="model.layers.0.self_attn.fused_qkv_a_proj"
)
selected_deepseek_layer = from_layer(
deepseek_fused_layer,
max_loras=8,
lora_config=lora_config,
packed_modules_list=packed_modules_two,
)
assert isinstance(selected_deepseek_layer, MergedColumnParallelLinearWithLoRA), (
"from_layer should select MergedColumnParallelLinearWithLoRA "
f"for DeepSeek fused_qkv_a_proj, got {type(selected_deepseek_layer).__name__}"
)
fully_sharded_lora_config = LoRAConfig(
max_loras=8,
max_lora_rank=16,
lora_dtype=torch.float16,
fully_sharded_loras=True,
)
selected_fully_sharded_deepseek_layer = from_layer(
deepseek_fused_layer,
max_loras=8,
lora_config=fully_sharded_lora_config,
packed_modules_list=packed_modules_two,
)
assert isinstance(
selected_fully_sharded_deepseek_layer,
MergedColumnParallelLinearWithLoRA,
), (
"from_layer should keep using MergedColumnParallelLinearWithLoRA "
"for fused_qkv_a_proj when the base layer is effectively unsharded, got "
f"{type(selected_fully_sharded_deepseek_layer).__name__}"
)
# Case 6: Generic subclass of MergedColumnParallelLinear with 2 packed
# modules should still use the generic merged wrapper.
class CustomMergedColumnParallelLinear(MergedColumnParallelLinear):
pass
custom_merged_layer = CustomMergedColumnParallelLinear(
4096, [2048, 2048], bias=False, params_dtype=torch.float16
)
assert MergedColumnParallelLinearWithLoRA.can_replace_layer(
source_layer=custom_merged_layer,
lora_config=lora_config,
packed_modules_list=packed_modules_two,
), "MergedColumnParallelLinearWithLoRA should handle subclasses"
selected_custom_layer = from_layer(
custom_merged_layer,
max_loras=8,
lora_config=lora_config,
packed_modules_list=packed_modules_two,
)
assert isinstance(selected_custom_layer, MergedColumnParallelLinearWithLoRA), (
f"from_layer should select MergedColumnParallelLinearWithLoRA "
f"for subclassed merged layers, got {type(selected_custom_layer).__name__}"
)
# Case 7: Plain ColumnParallelLinear (not merged) - common in many models
# -> ColumnParallelLinearWithLoRA should be selected
plain_column_parallel = ColumnParallelLinear(
4096, 4096, bias=False, params_dtype=torch.float16
@@ -1455,7 +1556,7 @@ def test_variable_slice_lora_class_selection(default_vllm_config, dist_init):
f"for plain ColumnParallelLinear, got {type(selected_plain).__name__}"
)
# Case 6: MergedColumnParallelLinear with exactly 2 output sizes
# Case 8: MergedColumnParallelLinear with exactly 2 output sizes
# and empty packed_modules_list
# -> ColumnParallelLinearWithLoRA should NOT match (packed_modules_list != 1)
# -> MergedColumnParallelLinearVariableSliceWithLoRA should NOT match (< 3 slices)
@@ -1473,3 +1574,170 @@ def test_variable_slice_lora_class_selection(default_vllm_config, dist_init):
"MergedColumnParallelLinearVariableSliceWithLoRA "
"should NOT handle 2 slices even with empty packed_modules_list"
)
@pytest.mark.parametrize(
"wrapper_cls",
[ColumnParallelLinearWithLoRA, ColumnParallelLinearWithShardedLoRA],
)
def test_get_and_maybe_dequant_weights_accepts_lora_wrappers(dist_init, wrapper_cls):
from vllm.model_executor.layers.quantization.utils.quant_utils import (
get_and_maybe_dequant_weights,
)
linear = ColumnParallelLinear(4096, 4096, bias=False, params_dtype=torch.float16)
lora_linear = wrapper_cls(linear)
# Should work with LoRA wrappers and return [out, in] weights.
dequant_weight = get_and_maybe_dequant_weights(lora_linear, out_dtype=torch.float16)
assert dequant_weight.shape == linear.weight.shape
@torch.inference_mode()
@pytest.mark.parametrize("device", DEVICES)
@pytest.mark.parametrize("stage", STAGES)
@pytest.mark.parametrize("fully_sharded", [False, True])
def test_deepseek_fused_qkv_a_proj_lora_preserves_base_forward(
default_vllm_config, dist_init, device, stage, fully_sharded
):
if current_platform.is_cuda_alike():
torch.accelerator.set_device_index(device)
torch.set_default_device(device)
dtype = torch.float16 if current_platform.is_cuda_alike() else torch.float32
max_loras = 8
lora_config = LoRAConfig(
max_loras=max_loras,
max_lora_rank=8,
lora_dtype=dtype,
fully_sharded_loras=fully_sharded,
)
punica_wrapper = get_punica_wrapper(8192, 256, device, lora_config=lora_config)
assert check_punica_wrapper(punica_wrapper)
class OffsetDeepSeekFusedQkvAProjLinear(DeepSeekV2FusedQkvAProjLinear):
def forward(self, input_):
output, output_bias = super().forward(input_)
return output + 1, output_bias
layer = OffsetDeepSeekFusedQkvAProjLinear(
32, [16, 16], prefix="model.layers.0.self_attn.fused_qkv_a_proj"
)
layer.weight.data = torch.rand_like(layer.weight.data, dtype=dtype)
lora_layer = MergedColumnParallelLinearWithLoRA(layer)
lora_layer.create_lora_weights(max_loras, lora_config)
lora_layer.set_mapping(punica_wrapper)
id_to_index = get_random_id_to_index(1, max_loras, log=False)
active_slot = next(i for i, lora_id in enumerate(id_to_index) if lora_id == 1)
lora_a = [
torch.rand(8, 32, dtype=dtype, device=device),
torch.rand(8, 32, dtype=dtype, device=device),
]
lora_b = [
torch.rand(16, 8, dtype=dtype, device=device),
torch.rand(16, 8, dtype=dtype, device=device),
]
lora_layer.set_lora(active_slot, lora_a=lora_a, lora_b=lora_b)
inputs, index_mapping, prompt_mapping = create_random_inputs(
active_lora_ids=[1],
num_inputs=4,
input_size=(1, 32),
input_range=(0, 1),
input_type=dtype,
device=device,
)
lora_mapping = LoRAMapping(index_mapping, prompt_mapping, is_prefill=stage)
punica_wrapper.update_metadata(lora_mapping, id_to_index, max_loras, 512)
lora_result = lora_layer(torch.cat(inputs))[0]
expected_results = []
for input_ in inputs:
result = layer(input_)[0]
result[:, :16] += input_ @ lora_a[0].T @ lora_b[0].T
result[:, 16:] += input_ @ lora_a[1].T @ lora_b[1].T
expected_results.append(result)
rtol, atol = TOLERANCES[lora_result.dtype]
torch.testing.assert_close(
lora_result, torch.cat(expected_results), rtol=rtol, atol=atol
)
merged_layer = OffsetDeepSeekFusedQkvAProjLinear(
32, [16, 16], prefix="model.layers.0.self_attn.fused_qkv_a_proj"
)
merged_layer.weight.data = layer.weight.data.clone()
merged_layer.weight.data[:16].add_(lora_b[0] @ lora_a[0])
merged_layer.weight.data[16:].add_(lora_b[1] @ lora_a[1])
merged_result = merged_layer(torch.cat(inputs))[0]
torch.testing.assert_close(lora_result, merged_result, rtol=rtol, atol=atol)
@torch.inference_mode()
@pytest.mark.parametrize("device", DEVICES)
@pytest.mark.parametrize("stage", STAGES)
def test_replicated_lora_preserves_base_forward_for_subclasses(
default_vllm_config, dist_init, device, stage
):
if current_platform.is_cuda_alike():
torch.accelerator.set_device_index(device)
torch.set_default_device(device)
dtype = torch.float16 if current_platform.is_cuda_alike() else torch.float32
max_loras = 8
lora_config = LoRAConfig(max_loras=max_loras, max_lora_rank=8, lora_dtype=dtype)
punica_wrapper = get_punica_wrapper(8192, 256, device, lora_config=lora_config)
assert check_punica_wrapper(punica_wrapper)
class OffsetReplicatedLinear(ReplicatedLinear):
def forward(self, input_):
output, output_bias = super().forward(input_)
return output + 1, output_bias
layer = OffsetReplicatedLinear(32, 16, bias=False, params_dtype=dtype)
layer.weight.data = torch.rand_like(layer.weight.data, dtype=dtype)
lora_layer = ReplicatedLinearWithLoRA(layer)
lora_layer.create_lora_weights(max_loras, lora_config)
lora_layer.set_mapping(punica_wrapper)
id_to_index = get_random_id_to_index(1, max_loras, log=False)
active_slot = next(i for i, lora_id in enumerate(id_to_index) if lora_id == 1)
lora_a = torch.rand(8, 32, dtype=dtype, device=device)
lora_b = torch.rand(16, 8, dtype=dtype, device=device)
lora_layer.set_lora(active_slot, lora_a=lora_a, lora_b=lora_b)
inputs, index_mapping, prompt_mapping = create_random_inputs(
active_lora_ids=[1],
num_inputs=4,
input_size=(1, 32),
input_range=(0, 1),
input_type=dtype,
device=device,
)
lora_mapping = LoRAMapping(index_mapping, prompt_mapping, is_prefill=stage)
punica_wrapper.update_metadata(lora_mapping, id_to_index, max_loras, 512)
lora_result = lora_layer(torch.cat(inputs))[0]
expected_results = []
for input_ in inputs:
result = layer(input_)[0]
result += input_ @ lora_a.T @ lora_b.T
expected_results.append(result)
rtol, atol = TOLERANCES[lora_result.dtype]
torch.testing.assert_close(
lora_result, torch.cat(expected_results), rtol=rtol, atol=atol
)
merged_layer = OffsetReplicatedLinear(32, 16, bias=False, params_dtype=dtype)
merged_layer.weight.data = layer.weight.data.clone()
merged_layer.weight.data.add_(lora_b @ lora_a)
merged_result = merged_layer(torch.cat(inputs))[0]
torch.testing.assert_close(lora_result, merged_result, rtol=rtol, atol=atol)
+150
View File
@@ -13,6 +13,7 @@ from vllm.config.lora import LoRAConfig
from vllm.lora.layers import (
ColumnParallelLinearWithLoRA,
MergedColumnParallelLinearWithLoRA,
ReplicatedLinearWithLoRA,
RowParallelLinearWithLoRA,
)
from vllm.lora.lora_model import LoRAModel
@@ -26,6 +27,7 @@ from vllm.lora.model_manager import (
from vllm.lora.peft_helper import PEFTHelper
from vllm.lora.request import LoRARequest
from vllm.lora.worker_manager import LRUCacheWorkerLoRAManager, WorkerLoRAManager
from vllm.model_executor.layers.fused_moe import GateLinear
from vllm.platforms import current_platform
from .utils import create_peft_lora
@@ -132,6 +134,135 @@ def test_replace_submodules(default_vllm_config, dist_init, dummy_model):
assert isinstance(model.get_submodule("layer1.dense2"), RowParallelLinearWithLoRA)
def test_wrap_replicated_linear_subclasses(default_vllm_config, dist_init, dummy_model):
from vllm.model_executor.layers.linear import ReplicatedLinear
class CustomReplicatedLinear(ReplicatedLinear):
pass
model = dummy_model
model.add_module("custom_gate", CustomReplicatedLinear(10, 10, bias=False))
manager = LoRAModelManager(
model,
1,
1,
1,
LoRAConfig(
max_lora_rank=8, max_cpu_loras=8, max_loras=8, lora_dtype=DEFAULT_DTYPE
),
torch.device(DEVICES[0]),
)
assert isinstance(
manager.model.get_submodule("custom_gate"), ReplicatedLinearWithLoRA
)
def test_wrap_gate_linear(default_vllm_config, dist_init, dummy_model):
model = dummy_model
model.add_module("router_gate", GateLinear(10, 4, bias=False))
manager = LoRAModelManager(
model,
1,
1,
1,
LoRAConfig(
max_lora_rank=8, max_cpu_loras=8, max_loras=8, lora_dtype=DEFAULT_DTYPE
),
torch.device(DEVICES[0]),
)
assert isinstance(
manager.model.get_submodule("router_gate"), ReplicatedLinearWithLoRA
)
def test_skip_unsupported_matched_modules(default_vllm_config, dist_init, dummy_model):
class UnsupportedContainer(nn.Module):
def __init__(self):
super().__init__()
# This name matches a supported target suffix ("dense1"),
# but nn.Linear is not currently a LoRA-wrappable layer type.
self.dense1 = nn.Linear(10, 10, bias=False)
model = dummy_model
model.add_module("unsupported", UnsupportedContainer())
manager = LoRAModelManager(
model,
1,
1,
1,
LoRAConfig(
max_lora_rank=8, max_cpu_loras=8, max_loras=8, lora_dtype=DEFAULT_DTYPE
),
torch.device(DEVICES[0]),
)
# Should not crash and should keep unsupported matched modules unchanged.
assert isinstance(manager.model.get_submodule("unsupported.dense1"), nn.Linear)
assert "unsupported.dense1" not in manager.modules
def test_target_modules_fail_closed_on_unsupported_matched_modules(
default_vllm_config, dist_init, dummy_model
):
class UnsupportedContainer(nn.Module):
def __init__(self):
super().__init__()
self.dense1 = nn.Linear(10, 10, bias=False)
model = dummy_model
model.add_module("unsupported", UnsupportedContainer())
with pytest.raises(ValueError, match="unsupported.dense1"):
LoRAModelManager(
model,
1,
1,
1,
LoRAConfig(
max_lora_rank=8,
max_cpu_loras=8,
max_loras=8,
lora_dtype=DEFAULT_DTYPE,
target_modules=["dense1"],
),
torch.device(DEVICES[0]),
)
def test_get_dummy_lora_warmup_rank_for_fully_sharded_moe():
manager = LoRAModelManager.__new__(LoRAModelManager)
manager.lora_config = LoRAConfig(
max_lora_rank=64,
max_cpu_loras=1,
max_loras=1,
lora_dtype=DEFAULT_DTYPE,
fully_sharded_loras=True,
)
class DummyModule:
def __init__(self, tp_size: int, fully_sharded: bool):
self.tp_size = tp_size
self.fully_sharded = fully_sharded
manager.modules = {
"model.layers.0.self_attn.q_proj": DummyModule(
tp_size=32,
fully_sharded=True,
),
"model.layers.0.mlp.experts": DummyModule(
tp_size=32,
fully_sharded=True,
),
}
assert manager.get_dummy_lora_warmup_rank(8) == 32
@pytest.mark.parametrize("device", DEVICES)
def test_lora_model_manager(default_vllm_config, dist_init, dummy_model, device):
model = dummy_model
@@ -795,6 +926,25 @@ def test_target_modules_none_uses_all(
)
@pytest.mark.parametrize("device", DEVICES)
def test_target_modules_match_packed_runtime_modules(
default_vllm_config, dist_init, dummy_model_gate_up, device
):
"""Packed runtime modules should be selected by their adapter-visible names."""
_test_target_modules(
dummy_model_gate_up,
["gate_proj"],
device,
expected_lora=[("gate_up_proj", MergedColumnParallelLinearWithLoRA)],
expected_no_lora=[
("dense1", ColumnParallelLinearWithLoRA),
("dense2", RowParallelLinearWithLoRA),
("layer1.dense1", ColumnParallelLinearWithLoRA),
("layer1.dense2", RowParallelLinearWithLoRA),
],
)
@pytest.mark.parametrize("device", DEVICES)
def test_load_adapter_warns_on_unsupported_modules(
default_vllm_config, dist_init, dummy_model_gate_up, device, tmp_path
+21
View File
@@ -58,3 +58,24 @@ class TestIsInTargetModules:
def test_exact_name_no_match(self):
assert not is_in_target_modules("dense3", ["dense1", "dense2"])
def test_packed_parent_matches_child_target_modules(self):
assert is_in_target_modules(
"model.layers.0.mlp.gate_up_proj",
["gate_proj", "up_proj"],
{"gate_up_proj": ["gate_proj", "up_proj"]},
)
def test_packed_child_matches_parent_target_modules(self):
assert is_in_target_modules(
"model.layers.0.mlp.gate_proj",
["gate_up_proj"],
{"gate_up_proj": ["gate_proj", "up_proj"]},
)
def test_fused_parent_matches_child_target_modules(self):
assert is_in_target_modules(
"model.layers.0.self_attn.fused_qkv_a_proj",
["q_a_proj", "kv_a_proj_with_mqa"],
{"fused_qkv_a_proj": ["q_a_proj", "kv_a_proj_with_mqa"]},
)
@@ -6,7 +6,7 @@ import pytest
from vllm.assets.video import VideoAsset
from vllm.multimodal import MULTIMODAL_REGISTRY
from vllm.multimodal.inputs import batched_tensors_equal
from vllm.multimodal.video import OpenCVDynamicVideoBackend, OpenCVVideoBackend
from vllm.multimodal.video import DynamicVideoBackend, VideoBackend
from ...utils import build_model_context
@@ -70,9 +70,11 @@ def test_processor_override(
@pytest.mark.parametrize("model_id", ["zai-org/GLM-4.1V-9B-Thinking"])
@pytest.mark.parametrize("fps", [2])
@pytest.mark.parametrize("backend", ["opencv", "pyav"])
def test_video_loader_consistency(
model_id: str,
fps: int,
backend: str,
):
"""
Ensure dynamic video loader (pre-sampled by loader) and normal video
@@ -93,9 +95,11 @@ def test_video_loader_consistency(
with open(video_path, "rb") as f:
video_bytes = f.read()
static_video, static_metadata = OpenCVVideoBackend.load_bytes(video_bytes)
dynamic_video, dynamic_metadata = OpenCVDynamicVideoBackend.load_bytes(
video_bytes, fps=fps
static_video, static_metadata = VideoBackend.load_bytes(
video_bytes, backend=backend
)
dynamic_video, dynamic_metadata = DynamicVideoBackend.load_bytes(
video_bytes, fps=fps, backend=backend
)
# pre-sampled loader shouldn't read all frames
+4
View File
@@ -518,6 +518,10 @@ _TEXT_GENERATION_EXAMPLE_MODELS = {
extras={"tiny-random": "tiny-random/qwen3-next-moe"},
min_transformers_version="4.56.3",
),
"Rnj1ForCausalLM": _HfExamplesInfo(
"EssentialAI/rnj-1-instruct",
is_available_online=False,
),
"RWForCausalLM": _HfExamplesInfo("tiiuae/falcon-40b"),
"SarvamMoEForCausalLM": _HfExamplesInfo(
"sarvamai/sarvam-30b",
+104 -17
View File
@@ -71,7 +71,9 @@ def test_video_backend_handles_broken_frames(monkeypatch: pytest.MonkeyPatch):
video_data = f.read()
loader = VIDEO_LOADER_REGISTRY.load("opencv")
frames, metadata = loader.load_bytes(video_data, num_frames=-1)
frames, metadata = loader.load_bytes(
video_data, num_frames=-1, backend="opencv"
)
# Verify metadata consistency:
# frames_indices must match actual loaded frames
@@ -158,12 +160,12 @@ def test_video_recovery_simulated_failures(monkeypatch: pytest.MonkeyPatch):
# Test WITHOUT recovery - should have fewer frames due to failures
frames_no_recovery, meta_no = loader.load_bytes(
video_data, num_frames=8, frame_recovery=False
video_data, num_frames=8, frame_recovery=False, backend="opencv"
)
# Test WITH recovery - should recover using next valid frames
frames_with_recovery, meta_yes = loader.load_bytes(
video_data, num_frames=8, frame_recovery=True
video_data, num_frames=8, frame_recovery=True, backend="opencv"
)
# With recovery should have MORE frames than without
@@ -214,12 +216,12 @@ def test_video_recovery_with_corrupted_file(monkeypatch: pytest.MonkeyPatch):
# Test without recovery - frame 17 will be skipped
frames_no_recovery, meta_no_recovery = loader.load_bytes(
video_data, num_frames=8, frame_recovery=False
video_data, num_frames=8, frame_recovery=False, backend="opencv"
)
# Test with recovery - frame 18 should fill in for frame 17
frames_with_recovery, meta_with_recovery = loader.load_bytes(
video_data, num_frames=8, frame_recovery=True
video_data, num_frames=8, frame_recovery=True, backend="opencv"
)
# Verify metadata consistency for both modes
@@ -271,12 +273,16 @@ def test_video_recovery_dynamic_backend(monkeypatch: pytest.MonkeyPatch):
# Test without recovery
frames_no_recovery, meta_no = loader.load_bytes(
video_data, fps=2, max_duration=10, frame_recovery=False
video_data,
fps=2,
max_duration=10,
frame_recovery=False,
backend="opencv",
)
# Test with frame_recovery enabled
frames_with_recovery, meta_with = loader.load_bytes(
video_data, fps=2, max_duration=10, frame_recovery=True
video_data, fps=2, max_duration=10, frame_recovery=True, backend="opencv"
)
# Verify basic properties
@@ -310,27 +316,81 @@ def dummy_video_path(tmp_path):
return video_path
# ============================================================================
# PyAV Backend Tests
# ============================================================================
def test_pyav_backend_loads_frames(dummy_video_path, monkeypatch: pytest.MonkeyPatch):
"""Test that the pyav codec backend can load frames from a valid video."""
with monkeypatch.context() as m:
m.setenv("VLLM_VIDEO_LOADER_BACKEND", "opencv")
with open(dummy_video_path, "rb") as f:
video_data = f.read()
loader = VIDEO_LOADER_REGISTRY.load("opencv")
frames, metadata = loader.load_bytes(video_data, num_frames=8, backend="pyav")
assert frames.ndim == 4
assert frames.shape[3] == 3 # RGB
assert frames.shape[0] == 8
assert frames.shape[0] == len(metadata["frames_indices"])
assert metadata["video_backend"] == "pyav"
assert "total_num_frames" in metadata
assert "fps" in metadata
assert "duration" in metadata
def test_pyav_dynamic_backend_loads_frames(
dummy_video_path, monkeypatch: pytest.MonkeyPatch
):
"""Test that the pyav codec with dynamic sampling can load frames."""
with monkeypatch.context() as m:
m.setenv("VLLM_VIDEO_LOADER_BACKEND", "opencv_dynamic")
with open(dummy_video_path, "rb") as f:
video_data = f.read()
loader = VIDEO_LOADER_REGISTRY.load("opencv_dynamic")
frames, metadata = loader.load_bytes(
video_data, fps=2, max_duration=10, backend="pyav"
)
assert frames.ndim == 4
assert frames.shape[3] == 3 # RGB
assert frames.shape[0] > 0
assert frames.shape[0] == len(metadata["frames_indices"])
assert metadata["video_backend"] == "pyav_dynamic"
@pytest.mark.parametrize(
"backend, kwargs, expected_num_frames",
"loader_key, kwargs, expected_num_frames",
[
# opencv: num_frames directly controls count
pytest.param("opencv", {"num_frames": 32}, 32, id="opencv-num_frames"),
pytest.param("opencv", {"fps": 2}, 120, id="opencv-fps"),
# uniform sampling + opencv codec
pytest.param(
"opencv",
{"num_frames": 500, "fps": 2},
{"num_frames": 32, "backend": "opencv"},
32,
id="opencv-num_frames",
),
pytest.param("opencv", {"fps": 2, "backend": "opencv"}, 120, id="opencv-fps"),
pytest.param(
"opencv",
{"num_frames": 500, "fps": 2, "backend": "opencv"},
120,
id="opencv-num_frames_wins_fps",
),
# dynamic sampling + opencv codec
pytest.param(
"opencv_dynamic",
{"fps": 1, "max_duration": 60},
{"fps": 1, "max_duration": 60, "backend": "opencv"},
60,
id="opencv_dynamic-within_max_duration",
),
pytest.param(
"opencv_dynamic",
{"fps": 2, "max_duration": 30},
{"fps": 2, "max_duration": 30, "backend": "opencv"},
60,
id="opencv_dynamic-exceeds_max_duration",
),
@@ -349,18 +409,45 @@ def dummy_video_path(tmp_path):
119,
id="molmo2-fps",
),
# uniform sampling + pyav codec (same frame counts as opencv)
pytest.param(
"opencv",
{"num_frames": 32, "backend": "pyav"},
32,
id="pyav-num_frames",
),
pytest.param("opencv", {"fps": 2, "backend": "pyav"}, 120, id="pyav-fps"),
pytest.param(
"opencv",
{"num_frames": 500, "fps": 2, "backend": "pyav"},
120,
id="pyav-num_frames_wins_fps",
),
# dynamic sampling + pyav codec
pytest.param(
"opencv_dynamic",
{"fps": 1, "max_duration": 60, "backend": "pyav"},
60,
id="pyav_dynamic-within_max_duration",
),
pytest.param(
"opencv_dynamic",
{"fps": 2, "max_duration": 30, "backend": "pyav"},
60,
id="pyav_dynamic-exceeds_max_duration",
),
],
)
def test_video_loader_frames_sampling(
dummy_video_path,
monkeypatch: pytest.MonkeyPatch,
backend: str,
loader_key: str,
kwargs: dict,
expected_num_frames: int,
):
"""Test video loader frames sampling functionality."""
monkeypatch.setenv("VLLM_VIDEO_LOADER_BACKEND", backend)
loader = VIDEO_LOADER_REGISTRY.load(backend)
monkeypatch.setenv("VLLM_VIDEO_LOADER_BACKEND", loader_key)
loader = VIDEO_LOADER_REGISTRY.load(loader_key)
with open(dummy_video_path, "rb") as f:
long_video_bytes = f.read()
+111
View File
@@ -0,0 +1,111 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Unit tests for BaseRenderer.warmup MM-warmup behavior.
These tests exercise:
- Zero-limit modalities are filtered from mm_counts passed to
get_dummy_processor_inputs (e.g. --limit-mm-per-prompt image=0 ...)
- MM warmup is skipped entirely when mm_processor is None
No model weights are required: warmup() is called directly on a MagicMock
that acts as the renderer instance.
"""
from unittest.mock import MagicMock, patch
from vllm.renderers.base import BaseRenderer
from vllm.renderers.params import ChatParams
def _make_renderer_mock(mm_limits: dict[str, int]) -> MagicMock:
"""Return a MagicMock that quacks like a BaseRenderer instance.
render_chat is mocked to raise ChatTemplateResolutionError so the chat
warmup block is skipped cleanly, keeping the test focused on MM warmup.
"""
from vllm.entrypoints.chat_utils import ChatTemplateResolutionError
renderer = MagicMock()
# chat warmup: make render_chat raise so we skip past it cleanly
renderer.render_chat.side_effect = ChatTemplateResolutionError("no template")
# MM processor with configurable limits
mm_processor = MagicMock()
mm_processor.info.allowed_mm_limits = mm_limits
renderer.mm_processor = mm_processor
return renderer
class TestMmWarmupZeroLimitFiltering:
"""Zero-limit modalities must be excluded from mm_counts."""
def test_zero_limit_modality_excluded_from_mm_counts(self):
"""A modality with limit=0 must not appear in mm_counts."""
renderer = _make_renderer_mock({"image": 1, "video": 0})
with patch("vllm.multimodal.processing.TimingContext", autospec=True):
BaseRenderer.warmup(renderer, ChatParams())
get_inputs = renderer.mm_processor.dummy_inputs.get_dummy_processor_inputs
get_inputs.assert_called_once()
_, kwargs = get_inputs.call_args
assert "video" not in kwargs["mm_counts"]
assert kwargs["mm_counts"]["image"] == 1
def test_all_zero_limits_passes_empty_mm_counts(self):
"""When all limits are 0, mm_counts must be empty."""
renderer = _make_renderer_mock({"image": 0, "video": 0})
with patch("vllm.multimodal.processing.TimingContext", autospec=True):
BaseRenderer.warmup(renderer, ChatParams())
get_inputs = renderer.mm_processor.dummy_inputs.get_dummy_processor_inputs
get_inputs.assert_called_once()
_, kwargs = get_inputs.call_args
assert kwargs["mm_counts"] == {}
def test_positive_limits_all_included_in_mm_counts(self):
"""All modalities with limit > 0 must be present in mm_counts."""
renderer = _make_renderer_mock({"image": 2, "video": 1})
with patch("vllm.multimodal.processing.TimingContext", autospec=True):
BaseRenderer.warmup(renderer, ChatParams())
get_inputs = renderer.mm_processor.dummy_inputs.get_dummy_processor_inputs
get_inputs.assert_called_once()
_, kwargs = get_inputs.call_args
assert kwargs["mm_counts"] == {"image": 1, "video": 1}
class TestMmWarmupRunsNormally:
"""MM warmup must run when mm_processor is set and limits > 0."""
def test_processor_apply_called(self):
renderer = _make_renderer_mock({"image": 1})
with patch("vllm.multimodal.processing.TimingContext", autospec=True):
BaseRenderer.warmup(renderer, ChatParams())
renderer.mm_processor.apply.assert_called_once()
def test_mm_cache_cleared_after_warmup(self):
renderer = _make_renderer_mock({"image": 1})
with patch("vllm.multimodal.processing.TimingContext", autospec=True):
BaseRenderer.warmup(renderer, ChatParams())
renderer.clear_mm_cache.assert_called_once()
class TestMmWarmupSkippedWhenNoProcessor:
"""MM warmup must be skipped when mm_processor is None (text-only model)."""
def test_no_warmup_without_processor(self):
renderer = _make_renderer_mock({})
renderer.mm_processor = None # override to None
BaseRenderer.warmup(renderer, ChatParams())
renderer.model_config.get_multimodal_config.assert_not_called()
@@ -65,7 +65,7 @@ def test_v1_generation_is_deterministic_across_batch_sizes_with_needle(
assert max_batch_size >= 2, "Batch size should be >= 2 to mix needle."
# Keep GPU memory usage low to avoid startup allocation failures.
gpu_mem_util = float(os.getenv("VLLM_GPU_MEMORY_UTILIZATION", "0.4"))
gpu_mem_util = float(os.getenv("VLLM_GPU_MEMORY_UTILIZATION", "0.5"))
max_model_len = int(os.getenv("VLLM_MAX_MODEL_LEN", "5120"))
# Sampling parameters: longer outputs with a more random-sounding
@@ -12,7 +12,7 @@ import torch
from utils import skip_unsupported
from vllm.model_executor.layers.batch_invariant import rms_norm as triton_rms_norm
from vllm.model_executor.layers.layernorm import RMSNorm
from vllm.model_executor.layers.layernorm import RMSNorm, fused_add_rms_norm
from vllm.platforms import current_platform
DEVICE_TYPE = current_platform.device_type
@@ -71,6 +71,93 @@ def test_rms_norm_batch_invariant_vs_standard(
)
@skip_unsupported
@pytest.mark.parametrize("hidden_size", [512, 4096])
@pytest.mark.parametrize("dtype", [torch.float16, torch.bfloat16])
@pytest.mark.parametrize("eps", [1e-6])
def test_fused_add_rms_norm_batch_invariant_residual_path(
hidden_size: int,
dtype: torch.dtype,
eps: float,
):
"""
Test the batch-invariant fused residual-add + RMSNorm helper directly.
"""
device = torch.device(DEVICE_TYPE)
torch.manual_seed(42)
x_single = torch.randn(1, hidden_size, dtype=dtype, device=device)
residual_single = torch.randn(1, hidden_size, dtype=dtype, device=device)
weight = torch.randn(hidden_size, dtype=dtype, device=device)
x_batch = torch.cat(
[
x_single,
torch.randn(3, hidden_size, dtype=dtype, device=device),
],
dim=0,
)
residual_batch = torch.cat(
[
residual_single,
torch.randn(3, hidden_size, dtype=dtype, device=device),
],
dim=0,
)
out_single, residual_out_single = fused_add_rms_norm(
x_single.clone(),
residual_single.clone(),
weight,
eps,
)
out_batch, residual_out_batch = fused_add_rms_norm(
x_batch.clone(),
residual_batch.clone(),
weight,
eps,
)
merged_single = x_single + residual_single
ref_out = triton_rms_norm(merged_single, weight, eps=eps)
torch.testing.assert_close(
residual_out_single,
merged_single,
rtol=0.0,
atol=0.0,
msg="Residual output should equal x + residual exactly",
)
torch.testing.assert_close(
residual_out_batch[:1],
merged_single,
rtol=0.0,
atol=0.0,
msg="Residual output should be batch invariant",
)
torch.testing.assert_close(
out_single,
out_batch[:1],
rtol=0.0,
atol=0.0,
msg="Fused add RMSNorm output should be batch invariant",
)
if dtype == torch.bfloat16:
rtol, atol = 1e-1, 1e-1
else:
rtol, atol = 1e-2, 1e-2
torch.testing.assert_close(
out_single,
ref_out,
rtol=rtol,
atol=atol,
msg="Fused add RMSNorm output should stay numerically close to the "
"batch-invariant RMSNorm reference",
)
@skip_unsupported
@pytest.mark.parametrize("batch_size", [1, 16, 128])
@pytest.mark.parametrize("seq_len", [1, 32, 512])
+2 -2
View File
@@ -321,7 +321,7 @@ def test_speculators_model_integration(
test_prompts = get_test_prompts(mm_enabled=False)
# First run: Direct speculator model (simplified integration)
spec_llm = LLM(model=model_path, max_model_len=4096)
spec_llm = LLM(model=model_path, max_model_len=4096, gpu_memory_utilization=0.92)
evaluate_llm_for_gsm8k(
spec_llm, expected_accuracy_threshold=expected_accuracy_threshold
)
@@ -351,7 +351,7 @@ def test_speculators_model_integration(
cleanup_dist_env_and_memory()
# Second run: Reference without speculative decoding
ref_llm = LLM(model=verifier_model, max_model_len=4096)
ref_llm = LLM(model=verifier_model, max_model_len=4096, gpu_memory_utilization=0.92)
ref_outputs = ref_llm.chat(test_prompts, sampling_config)
del ref_llm
torch.accelerator.empty_cache()
@@ -2,9 +2,11 @@
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Unit tests for NixlConnectorScheduler with HMA and Mamba N-1 prefill."""
import gc
from unittest.mock import patch
import pytest
import torch
from vllm import LLM, SamplingParams
from vllm.config import KVTransferConfig
@@ -196,12 +198,13 @@ def test_fewer_blocks_with_hma(monkeypatch, model_name, sw_size):
llm_kwargs = {
"model": model_name,
"enforce_eager": True,
"gpu_memory_utilization": 0.47,
"gpu_memory_utilization": 0.3,
"kv_transfer_config": kv_transfer_config,
"max_model_len": 2048,
"max_num_seqs": 1,
# NOTE: Make sure HMA is enabled
"disable_hybrid_kv_cache_manager": False,
"max_num_batched_tokens": 1024,
"max_num_batched_tokens": 2048,
"enable_prefix_caching": False,
"block_size": block_size,
}
@@ -248,6 +251,8 @@ def test_fewer_blocks_with_hma(monkeypatch, model_name, sw_size):
assert len(group_block_ids) == expected_num_remote_blocks
def run_test_and_cleanup():
gc.collect()
torch.accelerator.empty_cache()
llm = LLM(**llm_kwargs)
try:
run_hma_test(llm)
+233 -13
View File
@@ -27,6 +27,7 @@ SEEDS = [0]
DEVICE_TYPE = current_platform.device_type
DEVICES = [f"{DEVICE_TYPE}:0"]
NUM_MAPPINGS = [3]
NUM_MAPPINGS_PER_GROUP = [2]
@pytest.mark.parametrize("gpu_to_cpu", [True, False])
@@ -58,9 +59,7 @@ def test_transfer(
# build CanonicalKVCacheTensor list: one per tensor
kv_cache_tensors: list[CanonicalKVCacheTensor] = []
for i in range(num_tensors):
gpu_tensor = torch.randint(
-128,
127,
gpu_tensor = torch.zeros(
(num_gpu_blocks, gpu_page_size_bytes),
dtype=torch.int8,
device=device,
@@ -119,26 +118,36 @@ def test_transfer(
for j in range(block_size_factor)
]
# maybe skip some GPU blocks to test reading from the middle of a CPU block
if not gpu_to_cpu:
blocks_to_skip = block_size_factor - 1
# maybe skip some GPU blocks to test reading/writing from the middle of a CPU block
blocks_to_skip = block_size_factor - 1
if blocks_to_skip > 0:
gpu_blocks = gpu_blocks[blocks_to_skip:]
cpu_blocks_expanded = cpu_blocks_expanded[blocks_to_skip:]
# set transfer direction
if gpu_to_cpu:
handler = handlers.gpu_to_cpu_handler
src_spec = GPULoadStoreSpec(gpu_blocks, group_sizes=(len(gpu_blocks),))
src_spec = GPULoadStoreSpec(
gpu_blocks, group_sizes=(len(gpu_blocks),), block_indices=(blocks_to_skip,)
)
dst_spec = CPULoadStoreSpec(cpu_blocks)
dst_to_src = dict(zip(cpu_blocks_expanded, gpu_blocks))
num_dst_sub_blocks = num_cpu_blocks * block_size_factor
num_dst_sub_blocks = num_gpu_blocks
else:
handler = handlers.cpu_to_gpu_handler
src_spec = CPULoadStoreSpec(cpu_blocks)
dst_spec = GPULoadStoreSpec(gpu_blocks, group_sizes=(len(gpu_blocks),))
dst_spec = GPULoadStoreSpec(
gpu_blocks, group_sizes=(len(gpu_blocks),), block_indices=(blocks_to_skip,)
)
dst_to_src = dict(zip(gpu_blocks, cpu_blocks_expanded))
num_dst_sub_blocks = num_gpu_blocks
# randomize src and dst tensors before transfer
for tensor in handler.src_tensors:
tensor.random_()
for tensor in handler.dst_tensors:
tensor.random_()
# clone src and dst tensors before transfer
orig_src_tensors = [x.clone() for x in handler.src_tensors]
orig_dst_tensors = [x.clone() for x in handler.dst_tensors]
@@ -146,7 +155,7 @@ def test_transfer(
# call transfer function
start_time = time.time()
assert handler.transfer_async(1, (src_spec, dst_spec))
assert set({x.job_id for x in handler._transfers}) == {1}
assert {x.job_id for x in handler._transfers} == {1}
# wait for transfer to complete
end_time = time.time() + 10
@@ -155,11 +164,14 @@ def test_transfer(
if finished:
assert finished[0].job_id == 1
assert finished[0].success
assert finished[0].transfer_type == (
("GPU", "CPU") if gpu_to_cpu else ("CPU", "GPU")
assert (
finished[0].transfer_type == ("GPU", "CPU")
if gpu_to_cpu
else ("CPU", "GPU")
)
assert finished[0].transfer_size == (
len(gpu_blocks) * handler.group_block_size_in_bytes[0]
len(gpu_blocks)
* sum([x.page_size_bytes for x in handler.kv_cache_groups_data_refs[0]])
)
assert finished[0].transfer_time > 0
assert finished[0].transfer_time < (time.time() - start_time)
@@ -196,3 +208,211 @@ def test_transfer(
handlers.gpu_to_cpu_handler.shutdown()
if mmap_region:
mmap_region.cleanup()
@pytest.mark.parametrize("gpu_to_cpu", [True, False])
@pytest.mark.parametrize("num_mappings_per_group", NUM_MAPPINGS_PER_GROUP)
@pytest.mark.parametrize("gpu_page_size_bytes", GPU_PAGE_SIZES)
@pytest.mark.parametrize("block_size_factor", BLOCK_SIZE_FACTORS)
@pytest.mark.parametrize("num_gpu_blocks", NUM_GPU_BLOCKS)
@pytest.mark.parametrize("num_cpu_blocks", NUM_CPU_BLOCKS)
@pytest.mark.parametrize("seed", SEEDS)
@pytest.mark.parametrize("device", DEVICES)
@torch.inference_mode()
def test_transfer_multi_group(
default_vllm_config,
gpu_to_cpu: bool,
num_mappings_per_group: int,
gpu_page_size_bytes: int,
block_size_factor: int,
num_gpu_blocks: int,
num_cpu_blocks: int,
seed: int,
device: str,
) -> None:
"""Test transfers with three KV cache groups:
- Group 0: aligned transfer with num_mappings_per_group blocks
- Group 1: zero blocks (empty group)
- Group 2: unaligned CPU->GPU transfer (logical_offset=block_size_factor-1,
causing the implementation to skip source sub-blocks) with
num_mappings_per_group blocks
"""
set_random_seed(seed)
# 3 groups, each with 2 tensors
num_groups = 3
tensors_per_group = 2
num_tensors = num_groups * tensors_per_group
kv_cache_tensors: list[CanonicalKVCacheTensor] = []
for _ in range(num_tensors):
gpu_tensor = torch.zeros(
(num_gpu_blocks, gpu_page_size_bytes),
dtype=torch.int8,
device=device,
)
kv_cache_tensors.append(
CanonicalKVCacheTensor(
tensor=gpu_tensor,
page_size_bytes=gpu_page_size_bytes,
)
)
kv_cache_groups_data_refs: list[list[CanonicalKVCacheRef]] = [
[
CanonicalKVCacheRef(
tensor_idx=g * tensors_per_group + i,
page_size_bytes=gpu_page_size_bytes,
)
for i in range(tensors_per_group)
]
for g in range(num_groups)
]
canonical_kv_caches = CanonicalKVCaches(
tensors=kv_cache_tensors, group_data_refs=kv_cache_groups_data_refs
)
handlers = CpuGpuOffloadingHandlers(
kv_caches=canonical_kv_caches,
block_size_factor=block_size_factor,
num_cpu_blocks=num_cpu_blocks,
)
# group 0: aligned, group 1: empty, group 2: unaligned on CPU->GPU
group_sizes_in_cpu_blocks = [num_mappings_per_group, 0, num_mappings_per_group]
total_cpu_blocks = sum(group_sizes_in_cpu_blocks)
total_gpu_blocks_needed = total_cpu_blocks * block_size_factor
gpu_blocks_all = random.sample(range(num_gpu_blocks), total_gpu_blocks_needed)
cpu_blocks_all = random.sample(range(num_cpu_blocks), total_cpu_blocks)
# split gpu/cpu blocks per group
gpu_blocks_per_group: list[list[int]] = []
cpu_blocks_per_group: list[list[int]] = []
gpu_offset = 0
cpu_offset = 0
for size in group_sizes_in_cpu_blocks:
gpu_count = size * block_size_factor
gpu_blocks_per_group.append(gpu_blocks_all[gpu_offset : gpu_offset + gpu_count])
cpu_blocks_per_group.append(cpu_blocks_all[cpu_offset : cpu_offset + size])
gpu_offset += gpu_count
cpu_offset += size
# expand cpu blocks to gpu-page granularity
cpu_blocks_expanded_per_group = [
[
cpu_block * block_size_factor + j
for cpu_block in cpu_blocks
for j in range(block_size_factor)
]
for cpu_blocks in cpu_blocks_per_group
]
# skip sub-blocks from group 2 to test unaligned transfers.
sub_blocks_to_skip = block_size_factor - 1 # e.g. 2 when block_size_factor=3
if sub_blocks_to_skip > 0:
gpu_blocks_per_group[2] = gpu_blocks_per_group[2][
sub_blocks_to_skip:-sub_blocks_to_skip
]
cpu_blocks_expanded_per_group[2] = cpu_blocks_expanded_per_group[2][
sub_blocks_to_skip:-sub_blocks_to_skip
]
# build flat gpu_blocks list and group_sizes in GPU blocks
gpu_blocks: list[int] = []
group_sizes: list[int] = []
for gpu_blks in gpu_blocks_per_group:
gpu_blocks.extend(gpu_blks)
group_sizes.append(len(gpu_blks))
# build flat cpu_blocks list
cpu_blocks = []
for cpu_blks in cpu_blocks_per_group:
cpu_blocks.extend(cpu_blks)
# block_indices: only relevant for unaligned transfers
block_indices: list[int] = [0, 0, sub_blocks_to_skip]
if gpu_to_cpu:
handler = handlers.gpu_to_cpu_handler
src_spec = GPULoadStoreSpec(
gpu_blocks, group_sizes=group_sizes, block_indices=block_indices
)
dst_spec = CPULoadStoreSpec(cpu_blocks)
# per-group mapping: cpu sub-block -> gpu sub-block
dst_to_src_per_group = [
dict(zip(expanded, gpu_blks))
for expanded, gpu_blks in zip(
cpu_blocks_expanded_per_group, gpu_blocks_per_group
)
]
num_dst_sub_blocks = num_cpu_blocks * block_size_factor
else:
handler = handlers.cpu_to_gpu_handler
src_spec = CPULoadStoreSpec(cpu_blocks)
dst_spec = GPULoadStoreSpec(
gpu_blocks, group_sizes=group_sizes, block_indices=block_indices
)
# per-group mapping: gpu sub-block -> cpu sub-block
dst_to_src_per_group = [
dict(zip(gpu_blks, expanded))
for gpu_blks, expanded in zip(
gpu_blocks_per_group, cpu_blocks_expanded_per_group
)
]
num_dst_sub_blocks = num_gpu_blocks
# randomize src and dst tensors before transfer
for tensor in handler.src_tensors:
tensor.random_()
for tensor in handler.dst_tensors:
tensor.random_()
orig_src_tensors = [x.clone() for x in handler.src_tensors]
orig_dst_tensors = [x.clone() for x in handler.dst_tensors]
assert handler.transfer_async(1, (src_spec, dst_spec))
assert {x.job_id for x in handler._transfers} == {1}
end_time = time.time() + 10
while time.time() < end_time:
finished = handler.get_finished()
if finished:
assert finished[0].job_id == 1
assert finished[0].success
expected_bytes = sum(
group_size * sum([x.page_size_bytes for x in data_refs])
for group_size, data_refs in zip(
group_sizes, handler.kv_cache_groups_data_refs
)
)
assert finished[0].transfer_size == expected_bytes
break
time.sleep(0.1)
# verify src tensors did not change
for orig_tensor, tensor in zip(orig_src_tensors, handler.src_tensors):
assert torch.equal(orig_tensor, tensor)
# verify dst tensors at gpu-page granularity
for group_idx, dst_to_src in enumerate(dst_to_src_per_group):
group_tensor_offset = group_idx * tensors_per_group
for tensor_idx in range(tensors_per_group):
src_tensor = handler.src_tensors[group_tensor_offset + tensor_idx]
dst_tensor = handler.dst_tensors[group_tensor_offset + tensor_idx]
orig_dst_tensor = orig_dst_tensors[group_tensor_offset + tensor_idx]
src_view = src_tensor.view(-1, gpu_page_size_bytes)
dst_view = dst_tensor.view(-1, gpu_page_size_bytes)
orig_dst_view = orig_dst_tensor.view(-1, gpu_page_size_bytes)
for dst_sub_block in range(num_dst_sub_blocks):
src_sub_block = dst_to_src.get(dst_sub_block)
if src_sub_block is not None:
expected = src_view[src_sub_block]
else:
expected = orig_dst_view[dst_sub_block]
torch.testing.assert_close(
dst_view[dst_sub_block].cpu(), expected.cpu()
)
handlers.cpu_to_gpu_handler.shutdown()
handlers.gpu_to_cpu_handler.shutdown()
-1
View File
@@ -29,7 +29,6 @@ SEPARATE_GROUPS = [
"tests",
# v0 related
"vllm/lora",
"vllm/model_executor/layers",
]
# TODO(woosuk): Include the code from Megatron and HuggingFace.
+1
View File
@@ -420,6 +420,7 @@ def rms_norm(
def fused_add_rms_norm(
input: torch.Tensor, residual: torch.Tensor, weight: torch.Tensor, epsilon: float
) -> None:
# Note: this func is batch invariant
torch.ops._C.fused_add_rms_norm(input, residual, weight, epsilon)
@@ -6,15 +6,18 @@ from collections.abc import Callable
import torch
from torch._higher_order_ops.auto_functionalize import auto_functionalized
from vllm._custom_ops import create_fp4_output_tensors
from vllm.config import VllmConfig, get_layers_from_vllm_config
from vllm.logger import init_logger
from vllm.model_executor.layers.attention.mla_attention import MLAAttention
from vllm.model_executor.layers.quantization.utils.quant_utils import (
QuantKey,
kFp8Dynamic64Sym,
kFp8Dynamic128Sym,
kFp8StaticTensorSym,
kNvfp4Dynamic,
)
from vllm.platforms import current_platform
from vllm.utils.math_utils import round_up
from vllm.utils.torch_utils import _USE_LAYERNAME, _encode_layer_name
from ..vllm_inductor_pass import VllmFusionPatternMatcherPass, VllmPatternReplacement
@@ -203,6 +206,8 @@ class MLAAttnNvfp4QuantPattern(
kv_c_normed,
k_pe,
output_attn,
output_quant,
output_scale,
input_scale,
kv_cache_dummy_dep,
layer_name,
@@ -218,9 +223,6 @@ class MLAAttnNvfp4QuantPattern(
output_block_scale=None,
kv_cache_dummy_dep=kv_cache_dummy_dep,
)
output_quant, output_scale = create_fp4_output_tensors(
at1[1].shape[0], at1[1].shape[1], at1[1].device, True
)
at2 = auto_functionalized(
self._QUANT_OP,
input=at1[1],
@@ -235,7 +237,14 @@ class MLAAttnNvfp4QuantPattern(
return _pattern_with_ln
def _pattern(
q, kv_c_normed, k_pe, output_attn, input_scale, kv_cache_dummy_dep
q,
kv_c_normed,
k_pe,
output_attn,
output_quant,
output_scale,
input_scale,
kv_cache_dummy_dep,
):
at1 = auto_functionalized(
MLA_ATTN_OP,
@@ -248,11 +257,6 @@ class MLAAttnNvfp4QuantPattern(
output_block_scale=None,
kv_cache_dummy_dep=kv_cache_dummy_dep,
)
# Replicate what scaled_fp4_quant() does: allocate output
# tensors inline then call the .out variant.
output_quant, output_scale = create_fp4_output_tensors(
at1[1].shape[0], at1[1].shape[1], at1[1].device, True
)
at2 = auto_functionalized(
self._QUANT_OP,
input=at1[1],
@@ -279,6 +283,8 @@ class MLAAttnNvfp4QuantPattern(
kv_c_normed,
k_pe,
output_attn,
_output_quant,
output_scale,
input_scale,
kv_cache_dummy_dep,
layer_name,
@@ -289,9 +295,6 @@ class MLAAttnNvfp4QuantPattern(
dtype=FP4_DTYPE,
device=q.device,
)
output_scale = create_fp4_output_tensors(
q.shape[0], self._output_dim, q.device, True
)[1]
output_scale_view = torch.ops.aten.view.dtype(output_scale, FP8_DTYPE)
at2 = auto_functionalized(
MLA_ATTN_OP,
@@ -309,7 +312,14 @@ class MLAAttnNvfp4QuantPattern(
return _replacement_with_ln
def _replacement(
q, kv_c_normed, k_pe, output_attn, input_scale, kv_cache_dummy_dep
q,
kv_c_normed,
k_pe,
output_attn,
_output_quant,
output_scale,
input_scale,
kv_cache_dummy_dep,
):
# MLA output in quant_dtype (FP4 packed as uint8)
output_attn = torch.empty(
@@ -317,10 +327,6 @@ class MLAAttnNvfp4QuantPattern(
dtype=FP4_DTYPE,
device=q.device,
)
# attention output block scale
output_scale = create_fp4_output_tensors(
q.shape[0], self._output_dim, q.device, True
)[1]
output_scale_view = torch.ops.aten.view.dtype(output_scale, FP8_DTYPE)
at2 = auto_functionalized(
MLA_ATTN_OP,
@@ -343,6 +349,8 @@ class MLAAttnNvfp4QuantPattern(
self.empty(5, self._kv_lora_rank, dtype=self._dtype),
self.empty(5, 1, self._qk_rope_head_dim, dtype=self._dtype),
self.empty(5, self._output_dim, dtype=self._dtype),
self.empty(5, self._output_dim // 2, dtype=FP4_DTYPE),
self.empty_i32(128, round_up(self._output_dim // 16, 4)),
self.empty_fp32(1, 1),
self.empty(0, dtype=self._dtype),
]
@@ -351,6 +359,218 @@ class MLAAttnNvfp4QuantPattern(
return inputs
class MLAAttnFp8GroupQuantPattern(
VllmPatternReplacement[..., tuple[torch.Tensor, torch.Tensor]]
):
"""
Fusion for MLA Attention+Fp8GroupQuant (per-group dynamic FP8).
Matches the pattern: MLA attention -> per_token_group_fp8_quant, and
replaces it with MLA attention(output_block_scale=group_scale_buffer).
Used by models with block FP8 quantization (e.g. DeepSeek V3).
"""
def __init__(
self,
layer: MLAAttention,
dtype: torch.dtype,
quant_key: QuantKey,
has_col_major_scales: bool,
is_e8m0: bool,
is_tma_aligned: bool,
) -> None:
self._layer_name = layer.layer_name
self._num_heads = layer.num_heads
self._v_head_dim = layer.v_head_dim
self._kv_lora_rank = layer.kv_lora_rank
self._qk_rope_head_dim = layer.qk_rope_head_dim
self._qk_head_dim = layer.qk_nope_head_dim + layer.qk_rope_head_dim
self._output_dim = layer.num_heads * layer.v_head_dim
self._dtype = dtype
self._layer = layer
self._group_size = quant_key.scale.group_shape[1]
self._has_col_major_scales = has_col_major_scales
self._is_e8m0 = is_e8m0
self._is_tma_aligned = is_tma_aligned
self._quant_matcher = MatcherQuantFP8(
quant_key,
has_col_major_scales=has_col_major_scales,
is_e8m0=is_e8m0,
is_tma_aligned=is_tma_aligned,
)
@property
def pattern(
self,
) -> Callable[..., tuple[torch.Tensor, torch.Tensor]]:
_ln = _encode_layer_name(self._layer_name)
if _USE_LAYERNAME:
def _pattern_with_ln( # type: ignore[misc]
q,
kv_c_normed,
k_pe,
output_attn,
kv_cache_dummy_dep,
scale,
layer_name,
):
at1 = auto_functionalized(
MLA_ATTN_OP,
q=q,
kv_c_normed=kv_c_normed,
k_pe=k_pe,
output=output_attn,
layer_name=layer_name,
output_scale=None,
output_block_scale=None,
kv_cache_dummy_dep=kv_cache_dummy_dep,
)
attn_out = at1[1]
result = torch.empty(
attn_out.shape, device=attn_out.device, dtype=FP8_DTYPE
)
finfo = torch.finfo(FP8_DTYPE)
_, result, scale = auto_functionalized(
self._quant_matcher.QUANT_OP,
input=attn_out,
output_q=result,
output_s=scale,
group_size=self._group_size,
eps=1e-10,
fp8_min=finfo.min,
fp8_max=finfo.max,
scale_ue8m0=self._is_e8m0,
dummy_is_scale_transposed=self._has_col_major_scales,
dummy_is_tma_aligned=self._is_tma_aligned,
)
return result, scale
return _pattern_with_ln
def _pattern(
q: torch.Tensor,
kv_c_normed: torch.Tensor,
k_pe: torch.Tensor,
output_attn: torch.Tensor,
kv_cache_dummy_dep: torch.Tensor,
scale: torch.Tensor,
) -> tuple[torch.Tensor, torch.Tensor]:
at1 = auto_functionalized(
MLA_ATTN_OP,
q=q,
kv_c_normed=kv_c_normed,
k_pe=k_pe,
output=output_attn,
layer_name=_ln,
output_scale=None,
output_block_scale=None,
kv_cache_dummy_dep=kv_cache_dummy_dep,
)
attn_out = at1[1]
result = torch.empty(
attn_out.shape, device=attn_out.device, dtype=FP8_DTYPE
)
finfo = torch.finfo(FP8_DTYPE)
_, result, scale = auto_functionalized(
self._quant_matcher.QUANT_OP,
input=attn_out,
output_q=result,
output_s=scale,
group_size=self._group_size,
eps=1e-10,
fp8_min=finfo.min,
fp8_max=finfo.max,
scale_ue8m0=self._is_e8m0,
dummy_is_scale_transposed=self._has_col_major_scales,
dummy_is_tma_aligned=self._is_tma_aligned,
)
return result, scale
return _pattern
@property
def replacement(
self,
) -> Callable[..., tuple[torch.Tensor, torch.Tensor]]:
_ln = _encode_layer_name(self._layer_name)
if _USE_LAYERNAME:
def _replacement_with_ln( # type: ignore[misc]
q,
kv_c_normed,
k_pe,
output_attn,
kv_cache_dummy_dep,
scale,
layer_name,
):
output_attn = torch.empty(
[q.shape[0], self._output_dim],
dtype=FP8_DTYPE,
device=q.device,
)
at1 = auto_functionalized(
MLA_ATTN_OP,
q=q,
kv_c_normed=kv_c_normed,
k_pe=k_pe,
output=output_attn,
layer_name=layer_name,
output_scale=None,
output_block_scale=scale,
kv_cache_dummy_dep=kv_cache_dummy_dep,
quant_group_size=self._group_size,
quant_scale_ue8m0=self._is_e8m0,
quant_col_major=self._has_col_major_scales,
quant_tma_aligned=self._is_tma_aligned,
)
return at1[1], at1[2]
return _replacement_with_ln
def _replacement(q, kv_c_normed, k_pe, output_attn, kv_cache_dummy_dep, scale):
output_attn = torch.empty(
[q.shape[0], self._output_dim],
dtype=FP8_DTYPE,
device=q.device,
)
at1 = auto_functionalized(
MLA_ATTN_OP,
q=q,
kv_c_normed=kv_c_normed,
k_pe=k_pe,
output=output_attn,
layer_name=_ln,
output_scale=None,
output_block_scale=scale,
kv_cache_dummy_dep=kv_cache_dummy_dep,
quant_group_size=self._group_size,
quant_scale_ue8m0=self._is_e8m0,
quant_col_major=self._has_col_major_scales,
quant_tma_aligned=self._is_tma_aligned,
)
return at1[1], at1[2]
return _replacement
def get_inputs(self) -> list[torch.Tensor]:
inputs: list = [
self.empty(5, self._num_heads, self._qk_head_dim, dtype=self._dtype),
self.empty(5, self._kv_lora_rank, dtype=self._dtype),
self.empty(5, 1, self._qk_rope_head_dim, dtype=self._dtype),
self.empty(5, self._output_dim, dtype=self._dtype),
self.empty(0, dtype=self._dtype),
self._quant_matcher.empty_f32(1, 1),
]
if _USE_LAYERNAME:
inputs.append(_encode_layer_name(self._layer_name))
return inputs
class MLAAttnQuantFusionPass(VllmFusionPatternMatcherPass):
"""
This pass fuses post-attention quantization onto MLA attention if supported.
@@ -389,4 +609,25 @@ class MLAAttnQuantFusionPass(VllmFusionPatternMatcherPass):
if _USE_LAYERNAME:
break
# Per-group FP8 (block quant) — register all flag combinations.
if current_platform.is_cuda():
for quant_key in [kFp8Dynamic128Sym, kFp8Dynamic64Sym]:
for col_major in [True, False]:
for is_e8m0 in [True, False]:
for tma_aligned in [False, True]:
for layer in layers:
if layer.impl.fused_output_quant_supported(quant_key):
self.register(
MLAAttnFp8GroupQuantPattern(
layer,
dtype,
quant_key,
col_major,
is_e8m0,
tma_aligned,
)
)
if _USE_LAYERNAME:
break
self.dump_patterns(config, self.pm_pass)
+2 -1
View File
@@ -37,7 +37,7 @@ from vllm.config.profiler import ProfilerConfig
from vllm.config.reasoning import ReasoningConfig
from vllm.config.scheduler import SchedulerConfig
from vllm.config.speculative import SpeculativeConfig
from vllm.config.speech_to_text import SpeechToTextConfig
from vllm.config.speech_to_text import SpeechToTextConfig, SpeechToTextParams
from vllm.config.structured_outputs import StructuredOutputsConfig
from vllm.config.utils import (
ConfigType,
@@ -113,6 +113,7 @@ __all__ = [
"SpeculativeConfig",
# From vllm.config.speech_to_text
"SpeechToTextConfig",
"SpeechToTextParams",
# From vllm.config.structured_outputs
"StructuredOutputsConfig",
# From vllm.config.profiler
+2 -2
View File
@@ -51,10 +51,10 @@ class CacheConfig:
"""Whether block_size was explicitly provided. Derived automatically."""
user_specified_mamba_block_size: bool = field(default=False, init=False)
"""Whether mamba_block_size was explicitly provided. Derived automatically."""
gpu_memory_utilization: float = Field(default=0.9, gt=0, le=1)
gpu_memory_utilization: float = Field(default=0.92, gt=0, le=1)
"""The fraction of GPU memory to be used for the model executor, which can
range from 0 to 1. For example, a value of 0.5 would imply 50% GPU memory
utilization. If unspecified, will use the default value of 0.9. This is a
utilization. If unspecified, will use the default value of 0.92. This is a
per-instance limit, and only applies to the current vLLM instance. It does
not matter if you have another vLLM instance running on the same GPU. For
example, if you have two vLLM instances running on the same GPU, you can
+46
View File
@@ -1,9 +1,55 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
from __future__ import annotations
from dataclasses import dataclass
from typing import TYPE_CHECKING
from vllm.config.utils import config
if TYPE_CHECKING:
import numpy as np
from vllm.config.model import ModelConfig
@dataclass
class SpeechToTextParams:
"""All parameters consumed by ``get_generation_prompt()``.
``TranscriptionRequest.build_stt_params()`` constructs this object,
mapping API-level fields into typed attributes. Models only receive
this object, so new parameters can be added here without changing the
``get_generation_prompt`` signature.
"""
audio: np.ndarray
"""Resampled audio waveform for a single chunk."""
stt_config: SpeechToTextConfig
"""Server-level speech-to-text configuration."""
model_config: ModelConfig
"""Model configuration."""
language: str | None = None
"""ISO 639-1 language code (validated / auto-detected)."""
hotwords: str | None = None
"""
hotwords refers to a list of important words or phrases that the model
should pay extra attention to during transcription.
"""
task_type: str = "transcribe"
"""``"transcribe"`` or ``"translate"``."""
request_prompt: str = ""
"""Optional text prompt to guide the model."""
to_language: str | None = None
"""Target language for translation (model-dependent)."""
@config
class SpeechToTextConfig:
@@ -7,6 +7,8 @@ import torch
import torch.distributed as dist
from torch.distributed import ProcessGroup
from vllm.utils import is_moe_layer
class Cache:
def __init__(self):
@@ -317,16 +319,7 @@ class DeviceCommunicatorBase:
if not self.is_ep_communicator:
return
moe_modules = [
module
for module in model.modules()
# TODO(bnell): Should use isinstance but can't. Maybe search for
# presence of quant_method.maybe_init_modular_kernel?
if (
module.__class__.__name__ == "FusedMoE"
or module.__class__.__name__ == "SharedFusedMoE"
)
]
moe_modules = [module for module in model.modules() if is_moe_layer(module)]
for module in moe_modules:
module.maybe_init_modular_kernel()
@@ -38,6 +38,7 @@ from vllm.distributed.parallel_state import (
from vllm.distributed.stateless_coordinator import StatelessGroupCoordinator
from vllm.logger import init_logger
from vllm.model_executor.layers.fused_moe.layer import FusedMoEParallelConfig
from vllm.utils import is_moe_layer
from vllm.v1.engine import ReconfigureDistributedRequest, ReconfigureRankType
from vllm.v1.worker.gpu_ubatch_wrapper import UBatchWrapper
from vllm.v1.worker.workspace import lock_workspace, unlock_workspace
@@ -319,10 +320,7 @@ class ElasticEPScalingExecutor:
moe_modules = [
module
for module in self.worker.model_runner.model.modules()
if (
module.__class__.__name__ == "FusedMoE"
or module.__class__.__name__ == "SharedFusedMoE"
)
if is_moe_layer(module)
]
num_local_experts = moe_modules[0].moe_config.num_local_experts
assert all(
@@ -846,7 +846,7 @@ class MoRIIOConnectorWorker:
]
def _ping(self, zmq_context):
http_request_address = f"http://{self.request_address}/v1/completions"
http_request_address = f"http://{self.request_address}/v1"
role = "P" if self.is_producer else "D"
retry_count = 0
@@ -381,7 +381,9 @@ class OffloadingConnectorScheduler:
for i in range(self.config.block_size_factor):
src_block_ids.append(block_ids[gpu_block_idx + i])
src_spec = GPULoadStoreSpec(
src_block_ids, group_sizes=(len(src_block_ids),)
src_block_ids,
group_sizes=(len(src_block_ids),),
block_indices=(0,),
)
reqs_to_store[req_id] = (src_spec, dst_spec)
+3 -3
View File
@@ -24,7 +24,7 @@ if "UCX_RCACHE_MAX_UNRELEASED" not in os.environ:
os.environ["UCX_RCACHE_MAX_UNRELEASED"] = "1024"
try:
if current_platform.is_cuda():
if not current_platform.is_rocm():
from nixl._api import nixl_agent as NixlWrapper
else:
from rixl._api import nixl_agent as NixlWrapper
@@ -35,7 +35,7 @@ except ImportError:
NixlWrapper = None # type: ignore[assignment, misc]
try:
if current_platform.is_cuda():
if not current_platform.is_rocm():
from nixl._api import nixl_agent_config
else:
from rixl._api import nixl_agent_config
@@ -44,7 +44,7 @@ except ImportError:
logger.warning_once("NIXL agent config is not available")
try:
if current_platform.is_cuda():
if not current_platform.is_rocm():
from nixl._bindings import nixlXferTelemetry
else:
from rixl._bindings import nixlXferTelemetry
+1 -1
View File
@@ -228,7 +228,7 @@ class LLM:
tokenizer_revision: str | None = None,
chat_template: Path | str | None = None,
seed: int = 0,
gpu_memory_utilization: float = 0.9,
gpu_memory_utilization: float = 0.92,
cpu_offload_gb: float = 0,
offload_group_size: int = 0,
offload_num_in_group: int = 1,
@@ -114,12 +114,15 @@ class OpenAIServingChatBatch(OpenAIServingChat):
"""
tokenizer = self.renderer.tokenizer
assert tokenizer is not None
single_requests = [
request.to_chat_completion_request(messages)
for messages in request.messages
]
reasoning_parser: ReasoningParser | None = None
if self.reasoning_parser_cls:
chat_template_kwargs = self._prepare_extra_chat_template_kwargs(
request.chat_template_kwargs,
self.default_chat_template_kwargs,
chat_template_kwargs = self._effective_chat_template_kwargs(
single_requests[0]
)
reasoning_parser = self.reasoning_parser_cls(
tokenizer,
@@ -155,7 +158,7 @@ class OpenAIServingChatBatch(OpenAIServingChat):
self.default_sampling_params,
self.override_max_tokens,
)
single_request = request.to_chat_completion_request(request.messages[i])
single_request = single_requests[i]
sampling_params = single_request.to_sampling_params(
max_tokens, self.default_sampling_params
)
@@ -189,6 +189,18 @@ class OpenAIServingChat(OpenAIServing):
)
)
def _effective_chat_template_kwargs(
self, request: ChatCompletionRequest
) -> dict[str, Any]:
return (
request.build_chat_params(
self.chat_template,
self.chat_template_content_format,
)
.with_defaults(self.default_chat_template_kwargs)
.chat_template_kwargs
)
async def render_chat_request(
self,
request: ChatCompletionRequest,
@@ -231,10 +243,7 @@ class OpenAIServingChat(OpenAIServing):
# Streaming response
tokenizer = self.renderer.tokenizer
assert tokenizer is not None
chat_template_kwargs = self._prepare_extra_chat_template_kwargs(
request.chat_template_kwargs,
self.default_chat_template_kwargs,
)
chat_template_kwargs = self._effective_chat_template_kwargs(request)
reasoning_parser: ReasoningParser | None = None
if self.reasoning_parser_cls:
reasoning_parser = self.reasoning_parser_cls(
@@ -1,7 +1,7 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
import logging
from collections.abc import Callable
from typing import Any
from openai.types.responses import ResponseFunctionToolCall, ResponseOutputItem
from openai.types.responses.response_function_tool_call_output_item import (
@@ -15,6 +15,7 @@ from openai.types.responses.response_reasoning_item import (
ResponseReasoningItem,
)
from vllm.entrypoints.chat_utils import ChatTemplateContentFormatOption
from vllm.entrypoints.constants import MCP_PREFIX
from vllm.entrypoints.openai.responses.protocol import (
ResponseInputOutputItem,
@@ -36,10 +37,12 @@ class ResponsesParser:
self,
*,
tokenizer: TokenizerLike,
reasoning_parser_cls: Callable[[TokenizerLike], ReasoningParser],
reasoning_parser_cls: type[ReasoningParser],
response_messages: list[ResponseInputOutputItem],
request: ResponsesRequest,
tool_parser_cls: type[ToolParser] | None,
chat_template: str | None,
chat_template_content_format: ChatTemplateContentFormatOption,
):
self.response_messages: list[ResponseInputOutputItem] = (
# TODO: initial messages may not be properly typed
@@ -49,7 +52,14 @@ class ResponsesParser:
self.tokenizer = tokenizer
self.request = request
self.reasoning_parser_instance = reasoning_parser_cls(tokenizer)
self.reasoning_parser_instance = reasoning_parser_cls(
tokenizer,
chat_template_kwargs=_effective_chat_template_kwargs(
request,
chat_template=chat_template,
chat_template_content_format=chat_template_content_format,
),
)
self.tool_parser_instance = None
if tool_parser_cls is not None:
self.tool_parser_instance = tool_parser_cls(tokenizer, request.tools)
@@ -159,10 +169,12 @@ class ResponsesParser:
def get_responses_parser_for_simple_context(
*,
tokenizer: TokenizerLike,
reasoning_parser_cls: Callable[[TokenizerLike], ReasoningParser],
reasoning_parser_cls: type[ReasoningParser],
response_messages: list[ResponseInputOutputItem],
request: ResponsesRequest,
tool_parser_cls,
chat_template: str | None,
chat_template_content_format: ChatTemplateContentFormatOption,
) -> ResponsesParser:
"""Factory function to create a ResponsesParser with
optional reasoning parser.
@@ -176,4 +188,17 @@ def get_responses_parser_for_simple_context(
response_messages=response_messages,
request=request,
tool_parser_cls=tool_parser_cls,
chat_template=chat_template,
chat_template_content_format=chat_template_content_format,
)
def _effective_chat_template_kwargs(
request: ResponsesRequest,
chat_template: str | None,
chat_template_content_format: ChatTemplateContentFormatOption,
) -> dict[str, Any]:
return request.build_chat_params(
default_template=chat_template,
default_template_content_format=chat_template_content_format,
).chat_template_kwargs
+3 -2
View File
@@ -6,7 +6,6 @@ import copy
import json
import logging
from abc import ABC, abstractmethod
from collections.abc import Callable
from contextlib import AsyncExitStack
from dataclasses import replace
from typing import TYPE_CHECKING, Any, Final, Union
@@ -273,7 +272,7 @@ class ParsableContext(ConversationContext):
*,
response_messages: list[ResponseInputOutputItem],
tokenizer: TokenizerLike,
reasoning_parser_cls: Callable[[TokenizerLike], ReasoningParser] | None,
reasoning_parser_cls: type[ReasoningParser] | None,
request: ResponsesRequest,
available_tools: list[str] | None,
tool_parser_cls: type[ToolParser] | None,
@@ -296,6 +295,8 @@ class ParsableContext(ConversationContext):
response_messages=response_messages,
request=request,
tool_parser_cls=tool_parser_cls,
chat_template=chat_template,
chat_template_content_format=chat_template_content_format,
)
self.tool_parser_cls = tool_parser_cls
self.request = request
+16 -2
View File
@@ -267,6 +267,14 @@ class OpenAIServingResponses(OpenAIServing):
self.tool_server = tool_server
def _effective_chat_template_kwargs(
self, request: ResponsesRequest
) -> dict[str, Any]:
return request.build_chat_params(
self.chat_template,
self.chat_template_content_format,
).chat_template_kwargs
def _validate_generator_input(
self,
engine_input: EngineInput,
@@ -464,7 +472,10 @@ class OpenAIServingResponses(OpenAIServing):
context = SimpleContext()
if self.parser and self.parser.reasoning_parser_cls is not None:
reasoning_parser = self.parser.reasoning_parser_cls(tokenizer)
reasoning_parser = self.parser.reasoning_parser_cls(
tokenizer,
chat_template_kwargs=self._effective_chat_template_kwargs(request),
)
if (
isinstance(
struct_out := sampling_params.structured_outputs,
@@ -835,7 +846,10 @@ class OpenAIServingResponses(OpenAIServing):
and self.parser.reasoning_parser_cls is not None
and isinstance(context, (SimpleContext, ParsableContext))
):
reasoning_parser = self.parser.reasoning_parser_cls(tokenizer)
reasoning_parser = self.parser.reasoning_parser_cls(
tokenizer,
chat_template_kwargs=self._effective_chat_template_kwargs(request),
)
accumulated = getattr(context, "_accumulated_token_ids", []) or []
num_reasoning_tokens = reasoning_parser.count_reasoning_tokens(accumulated)
@@ -1,9 +1,10 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
import json
import time
from http import HTTPStatus
from typing import Literal, TypeAlias
from typing import TYPE_CHECKING, Literal, TypeAlias
import torch
from fastapi import HTTPException, UploadFile
@@ -12,6 +13,7 @@ from pydantic import (
model_validator,
)
from vllm.config.speech_to_text import SpeechToTextParams
from vllm.entrypoints.openai.engine.protocol import (
DeltaMessage,
OpenAIBaseModel,
@@ -26,6 +28,11 @@ from vllm.sampling_params import (
)
from vllm.utils import random_uuid
if TYPE_CHECKING:
import numpy as np
from vllm.config import ModelConfig, SpeechToTextConfig
logger = init_logger(__name__)
_LONG_INFO = torch.iinfo(torch.long)
@@ -71,6 +78,12 @@ class TranscriptionRequest(OpenAIBaseModel):
will improve accuracy and latency.
"""
hotwords: str | None = None
"""
hotwords refers to a list of important words or phrases that the model
should pay extra attention to during transcription.
"""
prompt: str = Field(default="")
"""An optional text to guide the model's style or continue a previous audio
segment.
@@ -183,6 +196,24 @@ class TranscriptionRequest(OpenAIBaseModel):
"min_p": 0.0,
}
def build_stt_params(
self,
audio: "np.ndarray",
stt_config: "SpeechToTextConfig",
model_config: "ModelConfig",
task_type: str,
) -> SpeechToTextParams:
return SpeechToTextParams(
audio=audio,
stt_config=stt_config,
model_config=model_config,
language=self.language,
task_type=task_type,
request_prompt=self.prompt,
to_language=self.to_language,
hotwords=self.hotwords,
)
def to_beam_search_params(
self,
default_max_tokens: int,
@@ -277,6 +308,17 @@ class TranscriptionRequest(OpenAIBaseModel):
parameter=invalid_param,
)
# Parse vllm_xargs from JSON string (form data sends it as a string)
xargs = data.get("vllm_xargs")
if isinstance(xargs, str):
try:
data["vllm_xargs"] = json.loads(xargs)
except json.JSONDecodeError as e:
raise VLLMValidationError(
f"Failed to parse vllm_xargs. Must be valid JSON: {e}",
parameter="vllm_xargs",
) from e
return data
@@ -446,6 +488,12 @@ class TranslationRequest(OpenAIBaseModel):
will improve accuracy.
"""
hotwords: str | None = None
"""
hotwords refers to a list of important words or phrases that the model
should pay extra attention to during transcription.
"""
to_language: str | None = None
"""The language of the input audio we translate to.
@@ -472,6 +520,24 @@ class TranslationRequest(OpenAIBaseModel):
"temperature": 0,
}
def build_stt_params(
self,
audio: "np.ndarray",
stt_config: "SpeechToTextConfig",
model_config: "ModelConfig",
task_type: str,
) -> SpeechToTextParams:
return SpeechToTextParams(
audio=audio,
stt_config=stt_config,
model_config=model_config,
language=self.language,
task_type=task_type,
request_prompt=self.prompt,
to_language=self.to_language,
hotwords=self.hotwords,
)
def to_beam_search_params(
self,
default_max_tokens: int,
@@ -184,9 +184,8 @@ class OpenAISpeechToText(OpenAIServing):
request_id: str,
) -> tuple[list[EngineInput], float]:
# Validate request
language = self.model_cls.validate_language(request.language)
# Skip to_language validation to avoid extra logging for Whisper.
to_language = (
request.language = self.model_cls.validate_language(request.language)
request.to_language = (
self.model_cls.validate_language(request.to_language)
if request.to_language
else None
@@ -229,28 +228,23 @@ class OpenAISpeechToText(OpenAIServing):
min_energy_window_size=self.asr_config.min_energy_split_window_size,
)
if language is None and getattr(
if request.language is None and getattr(
self.model_cls, "supports_explicit_language_detection", False
):
# Auto-detect language from the first chunk.
language = await self._detect_language(
request.language = await self._detect_language(
chunks[0], f"{request_id}-lang_detect"
)
request.language = language
parsed_prompts: list[DictPrompt] = []
for chunk in chunks:
# The model has control over the construction, as long as it
# returns a valid PromptType.
prompt = self.model_cls.get_generation_prompt(
stt_params = request.build_stt_params(
audio=chunk,
stt_config=self.asr_config,
model_config=self.model_config,
language=language,
task_type=self.task_type,
request_prompt=request.prompt,
to_language=to_language,
)
prompt = self.model_cls.get_generation_prompt(stt_params)
parsed_prompt: DictPrompt
if request.response_format == "verbose_json":
+3 -1
View File
@@ -566,7 +566,9 @@ class OpenAIServingRender:
if reasoning_parser is not None:
tokenizer = renderer.get_tokenizer()
request = reasoning_parser(
tokenizer, model_config=self.model_config
tokenizer,
model_config=self.model_config,
chat_template_kwargs=chat_params.chat_template_kwargs,
).adjust_request(request=request)
# tool parsing is done only if a tool_parser has been set and if
+7 -6
View File
@@ -253,7 +253,7 @@ if TYPE_CHECKING:
VLLM_CUDA_COMPATIBILITY_PATH: str | None = None
VLLM_ELASTIC_EP_SCALE_UP_LAUNCH: bool = False
VLLM_ELASTIC_EP_DRAIN_REQUESTS: bool = False
VLLM_MEMORY_PROFILER_ESTIMATE_CUDAGRAPHS: bool = False
VLLM_MEMORY_PROFILER_ESTIMATE_CUDAGRAPHS: bool = True
VLLM_NIXL_EP_MAX_NUM_RANKS: int = 32
VLLM_XPU_ENABLE_XPU_GRAPH: bool = False
VLLM_LORA_ENABLE_DUAL_STREAM: bool = False
@@ -829,9 +829,10 @@ environment_variables: dict[str, Callable[[], Any]] = {
"VLLM_MAX_AUDIO_CLIP_FILESIZE_MB": lambda: int(
os.getenv("VLLM_MAX_AUDIO_CLIP_FILESIZE_MB", "25")
),
# Backend for Video IO
# - "opencv": Default backend that uses OpenCV stream buffered backend.
# - "identity": Returns raw video bytes for model processor to handle.
# Backend for Video IO — selects the frame-sampling algorithm.
# - "opencv": uniform sampling.
# - "opencv_dynamic": duration-aware dynamic sampling.
# - "identity": returns raw video bytes for model processor to handle.
#
# Custom backend implementations can be registered
# via `@VIDEO_LOADER_REGISTRY.register("my_custom_video_loader")` and
@@ -1687,9 +1688,9 @@ environment_variables: dict[str, Callable[[], Any]] = {
),
# If set to 1, enable CUDA graph memory estimation during memory profiling.
# This profiles CUDA graph memory usage to provide more accurate KV cache
# memory allocation. Disabled by default to preserve existing behavior.
# memory allocation. Enabled by default as of v0.21.0
"VLLM_MEMORY_PROFILER_ESTIMATE_CUDAGRAPHS": lambda: bool(
int(os.getenv("VLLM_MEMORY_PROFILER_ESTIMATE_CUDAGRAPHS", "0"))
int(os.getenv("VLLM_MEMORY_PROFILER_ESTIMATE_CUDAGRAPHS", "1"))
),
# NIXL EP environment variables
"VLLM_NIXL_EP_MAX_NUM_RANKS": lambda: int(
+9
View File
@@ -203,7 +203,16 @@ class BaseLinearLayerWithLoRA(BaseLayerWithLoRA):
self, x: torch.Tensor, bias: torch.Tensor | None = None
) -> torch.Tensor:
output = self.base_layer.quant_method.apply(self.base_layer, x, bias)
return self._apply_lora_to_output(x, output)
def _apply_base_forward(self, x: torch.Tensor) -> torch.Tensor:
base_output = self.base_layer(x)
output = base_output[0] if isinstance(base_output, tuple) else base_output
return self._apply_lora_to_output(x, output)
def _apply_lora_to_output(
self, x: torch.Tensor, output: torch.Tensor
) -> torch.Tensor:
original_shape = output.shape if output.ndim == 3 else None
# In transformers backend, x and output have extra batch dimension like
+42 -10
View File
@@ -40,11 +40,19 @@ def _mcp_apply(x, bias, layer: "ColumnParallelLinearWithLoRA"):
# Since communication is needed, the buffer is directly initialized as a
# tensor rather than a tuple of tensor.
buffers = torch.zeros(
(layer.n_slices, x.shape[0], layer.lora_a_stacked[0].shape[2]),
local_lora_rank = layer.lora_a_stacked[0].shape[2]
buffer_shape = (layer.n_slices, x.shape[0], local_lora_rank)
# Under torch.compile, the local-rank-1 fully-sharded path can otherwise
# get lowered to a reinterpret view with a non-canonical layout. The
# Triton shrink op mutates this buffer in place and expects the standard
# contiguous [slice, token, rank] stride contract.
buffers = torch.empty_strided(
buffer_shape,
(x.shape[0] * local_lora_rank, local_lora_rank, 1),
dtype=torch.float32,
device=x.device,
)
buffers.zero_()
shrunk_buffers: torch.Tensor | None = layer.punica_wrapper.add_shrink(
buffers, x, layer.lora_a_stacked, 1.0
@@ -86,7 +94,7 @@ class ColumnParallelLinearWithLoRA(BaseLinearLayerWithLoRA):
# The base_layer type is ColumnParallelLinear or
# MergedColumnParallelLinear, their weight sharding logic is
# inconsistent when TP is greater than 1.
self.is_merged_col_linear = type(base_layer) is MergedColumnParallelLinear
self.is_merged_col_linear = isinstance(base_layer, MergedColumnParallelLinear)
self.output_size = self.base_layer.output_size_per_partition
# There is only one LoRA layer
self.n_slices = 1
@@ -158,7 +166,7 @@ class ColumnParallelLinearWithLoRA(BaseLinearLayerWithLoRA):
) -> bool:
if type(source_layer) is maybe_get_oot_by_class(ColumnParallelLinear):
return True
if type(source_layer) is maybe_get_oot_by_class(MergedColumnParallelLinear):
if isinstance(source_layer, maybe_get_oot_by_class(MergedColumnParallelLinear)):
if len(packed_modules_list) != 1:
return False
# Exclude layers with 3+ output sizes - those are handled by
@@ -275,19 +283,41 @@ class MergedColumnParallelLinearWithLoRA(ColumnParallelLinearWithLoRA):
index, 0, : lora_b_i.shape[0], : lora_b_i.shape[1]
].copy_(lora_b_i, non_blocking=True)
def apply(self, x: torch.Tensor, bias: torch.Tensor | None = None) -> torch.Tensor:
merged_cls = maybe_get_oot_by_class(MergedColumnParallelLinear)
# Effectively unsharded subclasses can safely reuse their custom
# forward() implementation before applying the LoRA delta.
if (
self.tp_size == 1
and type(self.base_layer) is not merged_cls
and type(self.base_layer).forward is not merged_cls.forward
):
return self._apply_base_forward(x)
return _mcp_apply(x, bias, self)
@classmethod
@_not_fully_sharded_can_replace
def can_replace_layer(
cls,
source_layer: nn.Module,
lora_config: LoRAConfig,
packed_modules_list: list,
model_config: PretrainedConfig | None = None,
decorate: bool = True,
) -> bool:
return (
type(source_layer) is MergedColumnParallelLinear
and len(packed_modules_list) == 2
)
merged_cls = maybe_get_oot_by_class(MergedColumnParallelLinear)
if not isinstance(source_layer, merged_cls) or len(packed_modules_list) != 2:
return False
tp_size = getattr(source_layer, "tp_size", 1)
if type(source_layer) is merged_cls:
if not decorate:
return True
return not lora_config.fully_sharded_loras or tp_size == 1
# Only support effectively unsharded subclasses here. Sharded
# subclasses may have custom communication semantics that the generic
# merged-column LoRA path does not know how to preserve.
return tp_size == 1
class QKVParallelLinearWithLoRA(ColumnParallelLinearWithLoRA):
@@ -607,7 +637,9 @@ class MergedColumnParallelLinearVariableSliceWithLoRA(
) -> bool:
# Support MergedColumnParallelLinear with 3 or more slices
# (2 slices are handled by MergedColumnParallelLinearWithLoRA)
if type(source_layer) is not maybe_get_oot_by_class(MergedColumnParallelLinear):
if not isinstance(
source_layer, maybe_get_oot_by_class(MergedColumnParallelLinear)
):
return False
# If packed_modules_list has 3+ items, use this class
+2 -2
View File
@@ -610,7 +610,7 @@ class FusedMoEWithLoRA(BaseLayerWithLoRA):
) -> bool:
"""Returns True if the layer can be replaced by this LoRA layer."""
# source_layer is FusedMoE or SharedFusedMoE
# source_layer is FusedMoE
return isinstance(source_layer, FusedMoE) and len(packed_modules_list) == 2
@@ -772,5 +772,5 @@ class FusedMoE3DWithLoRA(FusedMoEWithLoRA):
model_config: PretrainedConfig | None = None,
) -> bool:
"""Returns True if the layer can be replaced by this LoRA layer."""
# source_layer is FusedMoE or SharedFusedMoE
# source_layer is FusedMoE
return isinstance(source_layer, FusedMoE) and len(packed_modules_list) == 1
+7 -1
View File
@@ -46,6 +46,12 @@ class ReplicatedLinearWithLoRA(BaseLinearLayerWithLoRA):
return output, output_bias
def apply(self, x: torch.Tensor, bias: torch.Tensor | None = None) -> torch.Tensor:
# ReplicatedLinear subclasses such as GateLinear override forward() to
# dispatch custom kernels and/or adjust the output dtype. Apply LoRA on
# top of the actual base-layer output instead of bypassing that path.
return self._apply_base_forward(x)
# ReplicatedLinear should always be replaced, regardless of the fully
# sharded LoRAs setting, because it is, by definition, copied per GPU.
@classmethod
@@ -56,7 +62,7 @@ class ReplicatedLinearWithLoRA(BaseLinearLayerWithLoRA):
packed_modules_list: list,
model_config: PretrainedConfig | None = None,
) -> bool:
return type(source_layer) is maybe_get_oot_by_class(ReplicatedLinear)
return isinstance(source_layer, maybe_get_oot_by_class(ReplicatedLinear))
def slice_lora_a(
self, lora_a: torch.Tensor | list[torch.Tensor | None]
+52 -7
View File
@@ -437,12 +437,21 @@ class LoRAModelManager:
),
)
# In some models, especially multimodal ones, layers with the same
# name may have different types, such as nn.Linear and
# ReplicatedLinear. The nn.Linear layers cannot be replaced with
# LoRA layers, leading to assertion error. The following check
# aims to prevent this error
if self.supports_mm and not isinstance(new_module, BaseLayerWithLoRA):
# Some matched modules can be unsupported by LoRA wrappers
# (e.g. subclasses with specialized forward behavior).
if not isinstance(new_module, BaseLayerWithLoRA):
error_msg = (
"LoRA target module "
f"{module_name} ({type(module).__name__}) matched the "
"deployment configuration but could not be wrapped by any "
"LoRA layer implementation."
)
if self.lora_config.target_modules is not None:
raise ValueError(
f"{error_msg} target_modules="
f"{sorted(self.lora_config.target_modules)}"
)
logger.warning_once("%s It will be ignored.", error_msg)
continue
self.register_module(module_name, new_module)
@@ -578,6 +587,38 @@ class LoRAModelManager:
model.loras[module_name] = lora
return model
def get_dummy_lora_warmup_rank(self, default_rank: int) -> int:
"""Return a dummy LoRA rank compatible with wrapped modules.
Dummy LoRAs keep warmup memory low by using a small rank. Fully
sharded MoE wrappers additionally require the dummy rank to be divisible
by tensor parallel size because they shard W13 along the rank axis.
"""
if not self.lora_config.fully_sharded_loras:
return default_rank
required_multiple = 1
for module in self.modules.values():
if not getattr(module, "fully_sharded", False):
continue
required_multiple = math.lcm(required_multiple, module.tp_size)
if required_multiple == 1 or default_rank % required_multiple == 0:
return default_rank
adjusted_rank = (
(default_rank + required_multiple - 1) // required_multiple
) * required_multiple
if adjusted_rank > self.lora_config.max_lora_rank:
raise ValueError(
"Unable to choose a dummy LoRA warmup rank compatible with "
"fully sharded MoE modules: "
f"default_rank={default_rank}, "
f"required_multiple={required_multiple}, "
f"max_lora_rank={self.lora_config.max_lora_rank}"
)
return adjusted_rank
def _match_target_modules(self, module_name: str) -> bool:
"""Check if a module should have LoRA applied.
@@ -594,7 +635,11 @@ class LoRAModelManager:
"""
if not is_supported_lora_module(module_name, self.supported_lora_modules):
return False
return is_in_target_modules(module_name, self.lora_config.target_modules)
return is_in_target_modules(
module_name,
self.lora_config.target_modules,
self.packed_modules_mapping,
)
def _get_punica_wrapper(self, module_name: str) -> PunicaWrapperBase | None:
"""
+25 -3
View File
@@ -73,7 +73,9 @@ def get_lora_id():
return _GLOBAL_LORA_ID
_all_lora_classes: set[type[BaseLayerWithLoRA]] = {
# Order matters here: more specific wrappers must be checked before generic
# merged/column-parallel wrappers in from_layer().
_all_lora_classes: tuple[type[BaseLayerWithLoRA], ...] = (
VocabParallelEmbeddingWithLoRA,
ColumnParallelLinearWithLoRA,
MergedColumnParallelLinearWithLoRA,
@@ -90,7 +92,7 @@ _all_lora_classes: set[type[BaseLayerWithLoRA]] = {
RowParallelLinearWithShardedLoRA,
FusedMoEWithLoRA,
FusedMoE3DWithLoRA,
}
)
def is_moe_model(model: nn.Module) -> bool:
@@ -258,6 +260,7 @@ def is_supported_lora_module(
def is_in_target_modules(
module_name: str,
target_modules: list[str] | None,
packed_modules_mapping: dict[str, list[str]] | None = None,
) -> bool:
"""Check if a module passes the deployment-time target_modules filter.
@@ -268,14 +271,33 @@ def is_in_target_modules(
module_name: Full dot-separated module name.
target_modules: Optional deployment-time restriction list from
LoRAConfig.target_modules.
packed_modules_mapping: Optional model-defined mapping from packed
runtime module names to their adapter-visible submodule names
(e.g. ``{"gate_up_proj": ["gate_proj", "up_proj"]}``).
Returns:
True if the module passes the filter, False otherwise.
"""
if target_modules is None:
return True
target_module_set = set(target_modules)
module_suffix = module_name.split(".")[-1]
return module_suffix in set(target_modules)
if module_suffix in target_module_set or module_name in target_module_set:
return True
if not packed_modules_mapping:
return False
# Runtime packed parent matched by deployment-time child targets.
packed_children = packed_modules_mapping.get(module_suffix)
if packed_children and any(child in target_module_set for child in packed_children):
return True
# Adapter-visible packed child matched by deployment-time parent target.
return any(
module_suffix in children and packed_parent in target_module_set
for packed_parent, children in packed_modules_mapping.items()
)
def get_adapter_absolute_path(lora_path: str) -> str:
+8 -1
View File
@@ -160,7 +160,11 @@ class WorkerLoRAManager:
lora_request.lora_path,
", ".join(sorted(expected_lora_modules_lst)),
)
elif not is_in_target_modules(module_name, target_modules):
elif not is_in_target_modules(
module_name,
target_modules,
packed_modules_mapping,
):
logger.warning_once(
"LoRA module '%s' in adapter '%s' is not in the "
"deployment-time target_modules restriction [%s]."
@@ -197,6 +201,9 @@ class WorkerLoRAManager:
self._cached_dummy_lora = dummy_lora
return self._adapter_manager.add_adapter(dummy_lora)
def get_dummy_lora_warmup_rank(self, default_rank: int) -> int:
return self._adapter_manager.get_dummy_lora_warmup_rank(default_rank)
def pin_adapter(self, adapter_id: int) -> bool:
return self._adapter_manager.pin_adapter(adapter_id)
+15 -12
View File
@@ -666,16 +666,7 @@ _ACTIVATION_REGISTRY = LazyDict(
"gelu": lambda: GELU(),
"gelu_fast": lambda: FastGELU(),
"gelu_new": lambda: NewGELU(),
"gelu_pytorch_tanh": lambda: (
# TODO:[ROCm] PyTorch native GELU with tanh is unstable with torch.compile
logger.warning_once(
"[ROCm] PyTorch's native GELU with tanh approximation is unstable. "
"Falling back to GELU(approximate='none')."
),
nn.GELU(approximate="none"),
)[1]
if current_platform.is_rocm()
else nn.GELU(approximate="tanh"),
"gelu_pytorch_tanh": lambda: _get_gelu_pytorch_tanh(),
"relu": lambda: nn.ReLU(),
"relu2": lambda: ReLUSquaredActivation(),
"silu": lambda: nn.SiLU(),
@@ -687,6 +678,18 @@ _ACTIVATION_REGISTRY = LazyDict(
)
def _get_gelu_pytorch_tanh() -> nn.Module:
"""Get PyTorch GELU with tanh approximation, with ROCm fallback."""
if current_platform.is_rocm():
# TODO:[ROCm] PyTorch native GELU with tanh is unstable with torch.compile
logger.warning_once(
"[ROCm] PyTorch's native GELU with tanh approximation is unstable. "
"Falling back to GELU(approximate='none')."
)
return nn.GELU(approximate="none")
return nn.GELU(approximate="tanh")
def get_act_fn(act_fn_name: str) -> nn.Module:
"""Get an activation function by name."""
act_fn_name = act_fn_name.lower()
@@ -703,12 +706,12 @@ def get_act_fn(act_fn_name: str) -> nn.Module:
return _ACTIVATION_REGISTRY[act_fn_name]
_ACTIVATION_AND_MUL_REGISTRY = LazyDict(
_ACTIVATION_AND_MUL_REGISTRY: LazyDict[nn.Module] = LazyDict(
{
"gelu": lambda: GeluAndMul(),
"silu": lambda: SiluAndMul(),
"geglu": lambda: GeluAndMul(),
"swigluoai": lambda *args, **kwargs: SwigluOAIAndMul(*args, **kwargs),
"swigluoai": lambda: SwigluOAIAndMul(),
}
)
@@ -33,6 +33,7 @@ from vllm.utils.torch_utils import (
)
from vllm.v1.attention.backend import (
AttentionBackend,
AttentionMetadata,
AttentionType,
)
from vllm.v1.attention.backends.registry import AttentionBackendEnum
@@ -209,6 +210,7 @@ class Attention(nn.Module, AttentionLayerBase):
`self.kv_cache`.
"""
super().__init__()
sliding_window: int | None
if per_layer_sliding_window is not None:
# per-layer sliding window
sliding_window = per_layer_sliding_window
@@ -334,8 +336,14 @@ class Attention(nn.Module, AttentionLayerBase):
)
cache_config.enable_prefix_caching = False
if extra_impl_args.get("chunk_lookback", -1) > -1:
assert self.attn_backend.get_name() == "TRITON_ATTN", (
f"Chunked attention with lookback requires the Triton backend, "
f"but got {self.attn_backend.get_name()}."
)
impl_cls = self.attn_backend.get_impl_cls()
self.impl = impl_cls(
self.impl = impl_cls( # type: ignore[assignment] # impl_cls always returns an AttentionImpl subclass
num_heads,
head_size,
scale,
@@ -576,7 +584,7 @@ class Attention(nn.Module, AttentionLayerBase):
def get_attn_backend(self) -> type[AttentionBackend]:
return self.attn_backend
def get_kv_cache_spec(self, vllm_config: VllmConfig) -> KVCacheSpec:
def get_kv_cache_spec(self, vllm_config: VllmConfig) -> KVCacheSpec | None:
# Block size may get updated after model loading, refresh it
block_size = vllm_config.cache_config.block_size
# Should not be called for enc-dec or encoder-only attention.
@@ -680,9 +688,16 @@ def get_attention_context(
extracted from the forward context.
"""
forward_context: ForwardContext = get_forward_context()
attn_metadata = forward_context.attn_metadata
if isinstance(attn_metadata, dict):
attn_metadata = attn_metadata[layer_name]
attn_metadata_raw = forward_context.attn_metadata
attn_metadata: AttentionMetadata
if isinstance(attn_metadata_raw, dict):
attn_metadata = attn_metadata_raw[layer_name]
elif isinstance(attn_metadata_raw, list):
# list[dict[str, AttentionMetadata]]: used in speculative decoding
# where [0] is the base-model (non-speculative) metadata dict.
attn_metadata = attn_metadata_raw[0][layer_name]
else:
attn_metadata = attn_metadata_raw
attn_layer: Attention | MLAAttention = forward_context.no_compile_layers[layer_name]
kv_cache = attn_layer.kv_cache
slot_mapping = forward_context.slot_mapping
@@ -708,7 +723,7 @@ def unified_kv_cache_update(
assert hasattr(attn_layer.impl, "do_kv_cache_update"), (
f"{attn_layer.impl.__class__.__name__} does not support kv cache update"
)
attn_layer.impl.do_kv_cache_update(
attn_layer.impl.do_kv_cache_update( # type: ignore[attr-defined]
attn_layer,
key,
value,
@@ -29,7 +29,7 @@ from vllm.v1.kv_cache_interface import (
@functools.lru_cache
def create_chunked_local_attention_backend(
underlying_attn_backend: AttentionBackend,
underlying_attn_backend: type[AttentionBackend],
attention_chunk_size: int,
) -> type[AttentionBackend]:
prefix = f"ChunkedLocalAttention_{attention_chunk_size}_"
@@ -72,7 +72,7 @@ def _get_cross_slot_mapping(
@functools.lru_cache
def create_cross_attention_backend(
underlying_attn_backend: AttentionBackend,
underlying_attn_backend: type[AttentionBackend],
) -> type[AttentionBackend]:
prefix = "CrossAttention_"
underlying_builder = underlying_attn_backend.get_builder_cls()
@@ -87,6 +87,7 @@ def create_cross_attention_backend(
) -> AttentionMetadata:
new_metadata = copy(common_attn_metadata)
new_metadata.causal = False
assert new_metadata.encoder_seq_lens_cpu is not None
max_encoder_len = int(new_metadata.encoder_seq_lens_cpu.max())
new_metadata.max_seq_len = max_encoder_len
# Any computed tokens indicated decode step>1 (no chunked prefill)
@@ -118,7 +119,7 @@ def create_cross_attention_backend(
self.device,
)
attn_metadata = super().build(common_prefix_len, new_metadata, fast_build)
attn_metadata.slot_mapping = slot_mapping
attn_metadata.slot_mapping = slot_mapping # type: ignore[attr-defined]
return attn_metadata
# NOTE(Lucas): we need a custom impl so we can use the slot-mapping computed by
@@ -144,8 +145,12 @@ def create_cross_attention_backend(
and key is not None
and value is not None
):
self.do_kv_cache_update(
layer, key, value, kv_cache, attn_metadata.slot_mapping
self.do_kv_cache_update( # type: ignore[attr-defined]
layer,
key,
value,
kv_cache,
attn_metadata.slot_mapping, # type: ignore[attr-defined]
)
return super().forward(
@@ -21,7 +21,7 @@ from vllm.v1.kv_cache_interface import KVCacheSpec
@functools.lru_cache
def create_encoder_only_attention_backend(
underlying_attn_backend: AttentionBackend,
underlying_attn_backend: type[AttentionBackend],
) -> type[AttentionBackend]:
prefix = "EncoderOnlyAttention_"
underlying_builder = underlying_attn_backend.get_builder_cls()
@@ -93,6 +93,6 @@ class EncoderOnlyAttention(Attention):
**kwargs,
)
def get_kv_cache_spec(self, vllm_config: VllmConfig) -> KVCacheSpec:
def get_kv_cache_spec(self, vllm_config: VllmConfig) -> KVCacheSpec | None:
# Does not need KV cache
return None
@@ -234,7 +234,12 @@ from vllm.model_executor.layers.quantization import QuantizationConfig
from vllm.model_executor.layers.quantization.input_quant_fp8 import QuantFP8
from vllm.model_executor.layers.quantization.utils.quant_utils import (
GroupShape,
QuantKey,
get_and_maybe_dequant_weights,
kFp8Dynamic64Sym,
kFp8Dynamic128Sym,
kFp8StaticTensorSym,
kNvfp4Dynamic,
)
from vllm.platforms import current_platform
from vllm.utils.flashinfer import has_flashinfer, has_nvidia_artifactory
@@ -276,6 +281,44 @@ from vllm.v1.kv_cache_interface import (
logger = init_logger(__name__)
_FP8_DTYPE = current_platform.fp8_dtype()
def _detect_output_quant_key(
output: torch.Tensor,
output_scale: torch.Tensor | None,
output_block_scale: torch.Tensor | None,
output_dim: int,
) -> QuantKey | None:
"""Detect the output quantization key from fusion pass parameters.
Returns the appropriate QuantKey, or None if no quantization is needed.
Detection is based on output dtype and which scale tensors are present.
"""
if output_scale is None and output_block_scale is None:
return None
if output_block_scale is not None:
if output.dtype == _FP8_DTYPE:
# Per-group FP8 uses block scales only, not a separate output_scale
assert output_scale is None
# Infer group size from scale shape
num_groups = output_block_scale.shape[-1]
group_size = output_dim // num_groups
if group_size == 128:
return kFp8Dynamic128Sym
elif group_size == 64:
return kFp8Dynamic64Sym
else:
raise ValueError(
f"Unsupported group FP8 group_size={group_size} "
f"(output_dim={output_dim}, num_groups={num_groups}). "
f"Only group_size 128 and 64 are supported."
)
# output_scale None implies MXFP4, not supported
assert output_scale is not None
return kNvfp4Dynamic
return kFp8StaticTensorSym
class MLAAttention(nn.Module, AttentionLayerBase):
"""Multi-Head Latent Attention layer.
@@ -389,7 +432,7 @@ class MLAAttention(nn.Module, AttentionLayerBase):
cache_config.enable_prefix_caching = False
impl_cls = cast(type[MLAAttentionImpl], self.attn_backend.get_impl_cls())
self.impl = impl_cls(
self.impl = impl_cls( # type: ignore[assignment] # impl_cls always returns an MLAAttentionImpl subclass
num_heads=self.num_heads,
head_size=self.head_size,
scale=self.scale,
@@ -485,16 +528,23 @@ class MLAAttention(nn.Module, AttentionLayerBase):
if self.use_direct_call:
forward_context: ForwardContext = get_forward_context()
attn_metadata = forward_context.attn_metadata
if isinstance(attn_metadata, dict):
attn_metadata = attn_metadata[self.layer_name]
attn_metadata_raw = forward_context.attn_metadata
attn_metadata: MLACommonMetadata
if isinstance(attn_metadata_raw, dict):
attn_metadata = attn_metadata_raw[self.layer_name] # type: ignore[assignment]
elif isinstance(attn_metadata_raw, list):
# list[dict[str, AttentionMetadata]]: used in speculative decoding
# where [0] is the base-model (non-speculative) metadata dict.
attn_metadata = attn_metadata_raw[0][self.layer_name] # type: ignore[assignment]
else:
attn_metadata = attn_metadata_raw
self_kv_cache = self.kv_cache
slot_mapping = forward_context.slot_mapping
assert isinstance(slot_mapping, dict), (
f"Expected slot_mapping to be a dict, got {type(slot_mapping)}. "
)
self.impl.do_kv_cache_update(
self.impl.do_kv_cache_update( # type: ignore[attr-defined]
kv_c_normed,
k_pe,
self_kv_cache,
@@ -542,9 +592,17 @@ class MLAAttention(nn.Module, AttentionLayerBase):
output: torch.Tensor,
output_scale: torch.Tensor | None = None,
output_block_scale: torch.Tensor | None = None,
quant_group_size: int | None = None,
quant_scale_ue8m0: bool | None = None,
quant_col_major: bool | None = None,
quant_tma_aligned: bool | None = None,
) -> torch.Tensor:
use_quant = output_scale is not None or output_block_scale is not None
if use_quant:
assert output is not None, "Output tensor must be provided."
quant_key = _detect_output_quant_key(
output, output_scale, output_block_scale, self.num_heads * self.v_head_dim
)
if quant_key is not None:
# The fusion pass has allocated output with quantized dtype
# (FP8 or uint8 for FP4). We can't write into it directly,
# so we swap in a temp buffer for computation, then quantize
@@ -575,7 +633,7 @@ class MLAAttention(nn.Module, AttentionLayerBase):
# The zero fill is required when used with DP + EP
# to ensure all ranks within a DP group compute the
# same expert outputs.
if use_quant:
if quant_key is not None:
return quant_output.fill_(0)
return output.fill_(0)
@@ -612,7 +670,7 @@ class MLAAttention(nn.Module, AttentionLayerBase):
num_mha_tokens = q.size(0) - num_mqa_tokens
if num_mha_tokens > 0:
self.impl.forward_mha(
self.impl.forward_mha( # type: ignore[attr-defined]
q[num_mqa_tokens:],
k_c_normed[num_mqa_tokens:],
k_pe[num_mqa_tokens:],
@@ -695,7 +753,7 @@ class MLAAttention(nn.Module, AttentionLayerBase):
# call decode attn
if not is_sparse_impl:
assert attn_metadata.decode is not None
attn_out, lse = self.impl.forward_mqa(mqa_q, kv_cache, attn_metadata, self)
attn_out, lse = self.impl.forward_mqa(mqa_q, kv_cache, attn_metadata, self) # type: ignore[attr-defined]
# correct dcp attn_out with lse.
if self.impl.dcp_world_size > 1:
@@ -717,18 +775,41 @@ class MLAAttention(nn.Module, AttentionLayerBase):
# v_up projection
self._v_up_proj(attn_out, out=mqa_output_slice)
if use_quant:
if quant_key is not None:
# Quantize the BF16 computation result into the quantized output
actual = output[:num_actual_toks]
if output_block_scale is not None:
if quant_key == kNvfp4Dynamic:
# NVFP4: two FP4 values packed into one uint8
assert output_block_scale is not None
fp4_data, fp4_scales = ops.scaled_fp4_quant(actual, output_scale)
quant_output[:num_actual_toks].copy_(fp4_data)
output_block_scale.copy_(fp4_scales)
else:
output_block_scale[: fp4_scales.shape[0]].copy_(fp4_scales)
elif quant_key in (kFp8Dynamic128Sym, kFp8Dynamic64Sym):
# Per-group FP8
assert output_block_scale is not None
assert quant_group_size is not None, (
"Group FP8 output quant requested but "
"quant_group_size not passed through custom op"
)
finfo = torch.finfo(_FP8_DTYPE)
torch.ops._C.per_token_group_fp8_quant(
actual,
quant_output[:num_actual_toks],
output_block_scale[:num_actual_toks],
quant_group_size,
1e-10, # eps
finfo.min,
finfo.max,
quant_scale_ue8m0,
quant_col_major,
quant_tma_aligned,
)
elif quant_key == kFp8StaticTensorSym:
# Static FP8 quantization
fp8_data, _ = self._quant_fp8_op(actual, output_scale)
quant_output[:num_actual_toks].copy_(fp8_data)
else:
raise ValueError(f"Unsupported quant_key: {quant_key}")
return quant_output
return output_padded
@@ -973,6 +1054,10 @@ def unified_mla_attention_with_output(
output_scale: torch.Tensor | None = None,
output_block_scale: torch.Tensor | None = None,
kv_cache_dummy_dep: torch.Tensor | None = None,
quant_group_size: int | None = None,
quant_scale_ue8m0: bool | None = None,
quant_col_major: bool | None = None,
quant_tma_aligned: bool | None = None,
) -> None:
# kv_cache_dummy_dep is not used but accepting it creates a data dependency
# that ensures torch.compile preserves ordering between KV cache update and
@@ -989,6 +1074,10 @@ def unified_mla_attention_with_output(
output=output,
output_scale=output_scale,
output_block_scale=output_block_scale,
quant_group_size=quant_group_size,
quant_scale_ue8m0=quant_scale_ue8m0,
quant_col_major=quant_col_major,
quant_tma_aligned=quant_tma_aligned,
)
@@ -1001,6 +1090,10 @@ def unified_mla_attention_with_output_fake(
output_scale: torch.Tensor | None = None,
output_block_scale: torch.Tensor | None = None,
kv_cache_dummy_dep: torch.Tensor | None = None,
quant_group_size: int | None = None,
quant_scale_ue8m0: bool | None = None,
quant_col_major: bool | None = None,
quant_tma_aligned: bool | None = None,
) -> None:
return
@@ -1053,9 +1146,9 @@ except ImportError:
"AITER_MLA backends use aiter kernels instead."
)
elif current_platform.is_xpu():
from vllm._xpu_ops import xpu_ops as ops
from vllm._xpu_ops import xpu_ops
flash_attn_varlen_func = ops.flash_attn_varlen_func # type: ignore[no-redef]
flash_attn_varlen_func = xpu_ops.flash_attn_varlen_func # type: ignore[no-redef,attr-defined,assignment]
def dynamic_per_batched_tensor_quant(
@@ -1988,7 +2081,7 @@ class MLACommonMetadataBuilder(AttentionMetadataBuilder[M]):
assert isinstance(attn_metadata.prefill, FlashInferPrefillMetadata)
self._build_fi_prefill_wrappers(attn_metadata.prefill)
return attn_metadata
return attn_metadata # type: ignore[return-value]
def reorg_kvcache(
@@ -2071,13 +2164,13 @@ class MLACommonImpl(MLAAttentionImpl[M], Generic[M]):
"""
def fused_output_quant_supported(self, quant_key):
from vllm.model_executor.layers.quantization.utils.quant_utils import (
return quant_key in (
kFp8StaticTensorSym,
kNvfp4Dynamic,
kFp8Dynamic128Sym,
kFp8Dynamic64Sym,
)
return quant_key in (kFp8StaticTensorSym, kNvfp4Dynamic)
def __init__(
self,
num_heads: int,
@@ -29,7 +29,6 @@ from vllm.model_executor.layers.fused_moe.router.fused_moe_router import (
FusedMoERouter,
)
from vllm.model_executor.layers.fused_moe.router.gate_linear import GateLinear
from vllm.model_executor.layers.fused_moe.shared_fused_moe import SharedFusedMoE
from vllm.model_executor.layers.fused_moe.unquantized_fused_moe_method import (
UnquantizedFusedMoEMethod,
)
@@ -64,7 +63,6 @@ __all__ = [
"FusedMoEPrepareAndFinalizeModular",
"GateLinear",
"RoutingMethodType",
"SharedFusedMoE",
"activation_without_mul",
"apply_moe_activation",
"override_config",
@@ -117,17 +117,20 @@ def maybe_make_prepare_finalize(
"Detected DP deployment with no --enable-expert-parallel. "
"Falling back to AllGather+ReduceScatter dispatch/combine."
)
device_communicator = get_ep_group().device_communicator
assert device_communicator is not None
assert device_communicator.all2all_manager is not None
return make_moe_prepare_and_finalize_naive_dp_ep(
is_sequence_parallel=moe.moe_parallel_config.is_sequence_parallel,
num_dispatchers=(
get_ep_group().device_communicator.all2all_manager.world_size
),
num_dispatchers=(device_communicator.all2all_manager.world_size),
use_monolithic=use_monolithic,
)
else:
return make_moe_prepare_and_finalize_no_dp_ep(use_monolithic)
all2all_manager = get_ep_group().device_communicator.all2all_manager
device_communicator = get_ep_group().device_communicator
assert device_communicator is not None
all2all_manager = device_communicator.all2all_manager
assert all2all_manager is not None
prepare_finalize: FusedMoEPrepareAndFinalize | None = None
@@ -7,6 +7,7 @@ from typing import Union
import torch
from vllm.config import ParallelConfig, SchedulerConfig
from vllm.config.kernel import MoEBackend
from vllm.distributed import get_dp_group, get_pcp_group, get_tensor_model_parallel_rank
from vllm.logger import init_logger
from vllm.model_executor.layers.fused_moe.activation import MoEActivation
@@ -990,8 +991,7 @@ class FusedMoEParallelConfig:
@property
def use_batched_activation_format(self):
# TODO(bnell): nixl also uses batched format
return self.use_deepep_ll_kernels
return self.use_deepep_ll_kernels or self.use_nixl_ep_kernels
@property
def use_ag_rs_all2all_kernels(self):
@@ -1193,7 +1193,7 @@ class FusedMoEConfig:
# Defaults to intermediate_size_per_partition if not specified.
intermediate_size_per_partition_unpadded: int | None = None
moe_backend: str = "auto"
moe_backend: MoEBackend = "auto"
max_num_tokens: int = SchedulerConfig.DEFAULT_MAX_NUM_BATCHED_TOKENS_FOR_BATCHED_DP
has_bias: bool = False
is_act_and_mul: bool = True
@@ -210,9 +210,9 @@ def persistent_masked_m_silu_mul_quant(
DeepGemmQuantScaleFMT.UE8M0,
]
cuda_arch = current_platform.get_device_capability(
device_id=y.device.index
).to_int()
device_capability = current_platform.get_device_capability(device_id=y.device.index)
assert device_capability is not None
cuda_arch = device_capability.to_int()
if current_platform.is_cuda() and cuda_arch >= 80:
torch.ops._C.persistent_masked_m_silu_mul_quant(
@@ -1952,24 +1952,7 @@ class TritonExperts(mk.FusedMoEExpertsModular):
weight_key: QuantKey | None,
activation_key: QuantKey | None,
) -> bool:
p = current_platform
if p.is_rocm():
from vllm.platforms.rocm import on_gfx9, on_gfx12x
is_rocm_on_gfx9 = on_gfx9()
is_rocm_on_gfx12x = on_gfx12x()
else:
is_rocm_on_gfx9 = False
is_rocm_on_gfx12x = False
device_supports_fp8 = (
is_rocm_on_gfx9
or is_rocm_on_gfx12x
or (p.is_cuda() and p.has_device_capability((8, 9)))
or p.is_xpu()
)
if not device_supports_fp8:
if not current_platform.supports_fp8():
return (weight_key, activation_key) == (None, None)
SUPPORTED_W_A = [
@@ -7,6 +7,7 @@ import torch
import vllm.model_executor.layers.fused_moe.modular_kernel as mk
from vllm import envs
from vllm.config.kernel import MoEBackend
from vllm.logger import init_logger
from vllm.model_executor.layers.fused_moe import (
FusedMoEConfig,
@@ -146,7 +147,7 @@ def backend_to_kernel_cls(
raise ValueError(f"Unknown MXFP4 MoE backend: {backend.value}")
def map_mxfp4_backend(runner_backend: str) -> Mxfp4MoeBackend:
def map_mxfp4_backend(runner_backend: MoEBackend) -> Mxfp4MoeBackend:
"""Map user's moe_backend string to Mxfp4MoeBackend."""
mapping: dict[str, Mxfp4MoeBackend] = {
"flashinfer_trtllm": Mxfp4MoeBackend.FLASHINFER_TRTLLM_MXFP4_BF16,
@@ -201,10 +202,12 @@ def select_gpt_oss_mxfp4_moe_backend(
Select the primary MXFP4 MoE backend.
Note: Shape-specific fallbacks may still occur at runtime.
"""
triton_kernels_supported = has_triton_kernels() and (
9,
0,
) <= current_platform.get_device_capability() < (11, 0)
device_capability = current_platform.get_device_capability()
triton_kernels_supported = (
has_triton_kernels()
and device_capability is not None
and (9, 0) <= device_capability < (11, 0)
)
# LoRA: separate experts backend path
if config.is_lora_enabled:
@@ -214,6 +214,19 @@ def select_unquantized_moe_backend(
return backend, k_cls
raise ValueError(_make_log_unsupported(backend, reason))
# LoRA needs Triton's unfused activation/reduction hooks. Selecting the
# backend here ensures weights stay in a LoRA-compatible layout instead of
# being permuted for a backend like FlashInfer or AITER during load.
if moe_config.is_lora_enabled:
backend = UnquantizedMoeBackend.TRITON
if activation_format == mk.FusedMoEActivationFormat.BatchedExperts:
backend = UnquantizedMoeBackend.BATCHED_TRITON
return _return_or_raise(
backend,
moe_config,
activation_format,
)
runner_backend = moe_config.moe_backend
if runner_backend != "auto":
requested_backend = map_unquantized_backend(runner_backend)
@@ -4,6 +4,9 @@ import torch
import vllm.model_executor.layers.fused_moe.modular_kernel as mk
from vllm.distributed import get_ep_group
from vllm.distributed.device_communicators.base_device_communicator import (
All2AllManagerBase,
)
from vllm.forward_context import get_forward_context
from vllm.model_executor.layers.fused_moe.config import FusedMoEQuantConfig
from vllm.model_executor.layers.fused_moe.utils import moe_kernel_quantize_input
@@ -11,12 +14,16 @@ from vllm.utils.flashinfer import nvfp4_block_scale_interleave
def get_local_sizes():
return get_forward_context().dp_metadata.get_chunk_sizes_across_dp_rank()
dp_metadata = get_forward_context().dp_metadata
assert dp_metadata is not None
return dp_metadata.get_chunk_sizes_across_dp_rank()
class FlashInferNVLinkOneSidedPrepareAndFinalize(mk.FusedMoEPrepareAndFinalizeModular):
"""FlashInfer implementation using the Moe AlltoAll kernel."""
all2all_manager: All2AllManagerBase
def __init__(
self,
max_num_tokens: int,
@@ -32,8 +39,12 @@ class FlashInferNVLinkOneSidedPrepareAndFinalize(mk.FusedMoEPrepareAndFinalizeMo
self.hidden_size = hidden_size
self.num_dispatchers_ = num_dispatchers
self.all2all_manager = get_ep_group().device_communicator.all2all_manager
self.all2all_manager.initialize(
device_communicator = get_ep_group().device_communicator
assert device_communicator is not None
all2all_manager = device_communicator.all2all_manager
assert all2all_manager is not None
self.all2all_manager = all2all_manager
self.all2all_manager.initialize( # type: ignore[attr-defined]
max_num_tokens=self.max_num_tokens,
top_k=self.top_k,
num_experts=self.num_experts,
@@ -97,7 +108,8 @@ class FlashInferNVLinkOneSidedPrepareAndFinalize(mk.FusedMoEPrepareAndFinalizeMo
payloads.append(topk_ids)
payloads.append(topk_weights)
recv_payloads = self.all2all_manager.moe_alltoall.dispatch(
assert self.all2all_manager.moe_alltoall is not None # type: ignore[attr-defined]
recv_payloads = self.all2all_manager.moe_alltoall.dispatch( # type: ignore[attr-defined]
token_selected_experts=topk_ids,
input_payloads=payloads,
runtime_max_tokens_per_rank=self.runtime_max_tokens_per_rank,
@@ -131,7 +143,7 @@ class FlashInferNVLinkOneSidedPrepareAndFinalize(mk.FusedMoEPrepareAndFinalizeMo
apply_router_weight_on_input: bool,
weight_and_reduce_impl: mk.TopKWeightAndReduce,
) -> None:
assert self.all2all_manager.moe_alltoall is not None
assert self.all2all_manager.moe_alltoall is not None # type: ignore[attr-defined]
ep_size = self.all2all_manager.world_size
hidden_size = fused_expert_output.shape[-1]
@@ -139,7 +151,7 @@ class FlashInferNVLinkOneSidedPrepareAndFinalize(mk.FusedMoEPrepareAndFinalizeMo
ep_size, self.runtime_max_tokens_per_rank, hidden_size
)
combined_output = self.all2all_manager.moe_alltoall.combine(
combined_output = self.all2all_manager.moe_alltoall.combine( # type: ignore[attr-defined]
payload=fused_expert_output,
runtime_max_tokens_per_rank=self.runtime_max_tokens_per_rank,
)
@@ -15,19 +15,26 @@ from vllm.utils.flashinfer import nvfp4_block_scale_interleave
def get_local_sizes():
return get_forward_context().dp_metadata.get_chunk_sizes_across_dp_rank()
dp_metadata = get_forward_context().dp_metadata
assert dp_metadata is not None
return dp_metadata.get_chunk_sizes_across_dp_rank()
class FlashInferNVLinkTwoSidedPrepareAndFinalize(mk.FusedMoEPrepareAndFinalizeModular):
"""Base class for FlashInfer MoE prepare and finalize operations."""
all2all_manager: All2AllManagerBase
def __init__(
self,
num_dispatchers: int = 1,
):
super().__init__()
self.num_dispatchers_ = num_dispatchers
self.all2all_manager = get_ep_group().device_communicator.all2all_manager
device_communicator = get_ep_group().device_communicator
assert device_communicator is not None
assert device_communicator.all2all_manager is not None
self.all2all_manager = device_communicator.all2all_manager
@property
def activation_format(self) -> mk.FusedMoEActivationFormat:
@@ -129,7 +136,7 @@ def flashinfer_alltoall_dispatch(
):
from flashinfer.comm.trtllm_alltoall import MnnvlMoe
assert all2all_manager.ensure_alltoall_workspace_initialized(), (
assert all2all_manager.ensure_alltoall_workspace_initialized(), ( # type: ignore[attr-defined]
"FlashInfer AllToAll workspace not available"
)
@@ -144,7 +151,7 @@ def flashinfer_alltoall_dispatch(
topk_ids,
topk_weights,
None,
all2all_manager.prepare_workspace_tensor,
all2all_manager.prepare_workspace_tensor, # type: ignore[attr-defined]
max_num_token,
ep_rank,
ep_size,
@@ -172,7 +179,7 @@ def flashinfer_alltoall_dispatch(
x = MnnvlMoe.mnnvl_moe_alltoallv(
x,
alltoall_info,
all2all_manager.workspace_tensor,
all2all_manager.workspace_tensor, # type: ignore[attr-defined]
ep_rank,
ep_size,
)
@@ -180,7 +187,7 @@ def flashinfer_alltoall_dispatch(
x_sf = MnnvlMoe.mnnvl_moe_alltoallv(
x_sf,
alltoall_info,
all2all_manager.workspace_tensor,
all2all_manager.workspace_tensor, # type: ignore[attr-defined]
ep_rank,
ep_size,
)
@@ -196,7 +203,7 @@ def flashinfer_alltoall_dispatch(
x = MnnvlMoe.mnnvl_moe_alltoallv(
x,
alltoall_info,
all2all_manager.workspace_tensor,
all2all_manager.workspace_tensor, # type: ignore[attr-defined]
ep_rank,
ep_size,
)
@@ -212,13 +219,13 @@ def flashinfer_alltoall_combine(
):
from flashinfer.comm.trtllm_alltoall import MnnvlMoe
assert all2all_manager.ensure_alltoall_workspace_initialized(), (
assert all2all_manager.ensure_alltoall_workspace_initialized(), ( # type: ignore[attr-defined]
"FlashInfer AllToAll workspace not available"
)
return MnnvlMoe.mnnvl_moe_alltoallv_combine(
output,
alltoall_info,
all2all_manager.workspace_tensor,
all2all_manager.workspace_tensor, # type: ignore[attr-defined]
ep_rank=all2all_manager.rank,
ep_size=all2all_manager.world_size,
top_k=top_k,
@@ -132,9 +132,11 @@ class MoEPrepareAndFinalizeNaiveDPEPModular(mk.FusedMoEPrepareAndFinalizeModular
)
if scales is None:
assert len(res) == 3
a1q, topk_weights, topk_ids = res
a1q_scale = None
else:
assert len(res) == 4
a1q, topk_weights, topk_ids, scales = res
a1q_scale = _unwrap_scale_and_prepare_for_moe(scales, quant_config)
@@ -217,9 +219,11 @@ class MoEPrepareAndFinalizeNaiveDPEPMonolithic(mk.FusedMoEPrepareAndFinalizeMono
)
if scales is None:
assert len(res) == 2
a1q, router_logits = res
a1q_scale = None
else:
assert len(res) == 3
a1q, router_logits, scales = res
a1q_scale = _unwrap_scale_and_prepare_for_moe(scales, quant_config)
@@ -54,11 +54,13 @@ class DefaultMoERunner(MoERunnerBase):
# NOTE: this will be removed once all kernels are migrated into the
# MoEKernel framework.
if self.do_naive_dispatch_combine:
hidden_states, router_logits = get_ep_group().dispatch_router_logits(
res = get_ep_group().dispatch_router_logits(
hidden_states,
router_logits,
self.moe_config.is_sequence_parallel,
)
assert len(res) == 2
hidden_states, router_logits = res
# NOTE: Similar with DP, PCP also needs dispatch and combine. For
# simplicity, AgRsAll2All was added separately for PCP here. Maybe
@@ -1,25 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
import torch
from vllm.model_executor.layers.fused_moe.layer import FusedMoE
# TODO(bnell): Remove this entirely
class SharedFusedMoE(FusedMoE):
"""
A FusedMoE operation that also computes the results of shared experts.
If an all2all communicator is being used the shared expert computation
can be interleaved with the fused all2all dispatch communication step.
"""
def forward(
self,
hidden_states: torch.Tensor,
router_logits: torch.Tensor,
) -> torch.Tensor:
return super().forward(
hidden_states=hidden_states,
router_logits=router_logits,
)
+25 -18
View File
@@ -16,7 +16,6 @@ from vllm.logger import init_logger
from vllm.model_executor.model_loader.weight_utils import sharded_weight_loader
from vllm.model_executor.utils import set_weight_attrs
from vllm.utils.torch_utils import direct_register_custom_op
from vllm.v1.attention.backend import AttentionMetadata
from vllm.v1.attention.backends.gdn_attn import GDNAttentionMetadata
from .fla.ops.kda import (
@@ -123,7 +122,7 @@ class KimiDeltaAttention(nn.Module, MambaBase):
self.cache_config = cache_config
if model_config is None:
raise ValueError("model_config must be provided")
kda_config = model_config.linear_attn_config
kda_config = model_config.linear_attn_config # type: ignore[attr-defined]
self.head_dim = kda_config["head_dim"]
self.num_heads = kda_config["num_heads"]
self.layer_idx = layer_idx
@@ -297,19 +296,21 @@ class KimiDeltaAttention(nn.Module, MambaBase):
core_attn_out: torch.Tensor,
) -> None:
forward_context = get_forward_context()
attn_metadata: AttentionMetadata = forward_context.attn_metadata
attn_metadata_raw = forward_context.attn_metadata
if attn_metadata is None:
if attn_metadata_raw is None:
# # V1 profile run
return
assert isinstance(attn_metadata, dict)
attn_metadata = attn_metadata[self.prefix]
assert isinstance(attn_metadata, GDNAttentionMetadata)
has_initial_state = attn_metadata.has_initial_state
non_spec_query_start_loc = attn_metadata.non_spec_query_start_loc
non_spec_state_indices_tensor = attn_metadata.non_spec_state_indices_tensor # noqa: E501
num_actual_tokens = attn_metadata.num_actual_tokens
assert isinstance(attn_metadata_raw, dict)
attn_metadata_narrowed = attn_metadata_raw[self.prefix]
assert isinstance(attn_metadata_narrowed, GDNAttentionMetadata)
has_initial_state = attn_metadata_narrowed.has_initial_state
non_spec_query_start_loc = attn_metadata_narrowed.non_spec_query_start_loc
non_spec_state_indices_tensor = (
attn_metadata_narrowed.non_spec_state_indices_tensor
) # noqa: E501
num_actual_tokens = attn_metadata_narrowed.num_actual_tokens
constant_caches = self.kv_cache
q_proj_states = q_proj_states[:num_actual_tokens]
@@ -335,7 +336,7 @@ class KimiDeltaAttention(nn.Module, MambaBase):
v_conv_weights = self.v_conv1d.weight.view(
self.v_conv1d.weight.size(0), self.v_conv1d.weight.size(2)
)
if attn_metadata.num_prefills > 0:
if attn_metadata_narrowed.num_prefills > 0:
q_proj_states = q_proj_states.transpose(0, 1)
k_proj_states = k_proj_states.transpose(0, 1)
v_proj_states = v_proj_states.transpose(0, 1)
@@ -348,7 +349,7 @@ class KimiDeltaAttention(nn.Module, MambaBase):
has_initial_state=has_initial_state,
cache_indices=non_spec_state_indices_tensor,
query_start_loc=non_spec_query_start_loc,
metadata=attn_metadata,
metadata=attn_metadata_narrowed,
).transpose(0, 1)
k = causal_conv1d_fn(
k_proj_states,
@@ -359,7 +360,7 @@ class KimiDeltaAttention(nn.Module, MambaBase):
has_initial_state=has_initial_state,
cache_indices=non_spec_state_indices_tensor,
query_start_loc=non_spec_query_start_loc,
metadata=attn_metadata,
metadata=attn_metadata_narrowed,
).transpose(0, 1)
v = causal_conv1d_fn(
v_proj_states,
@@ -370,11 +371,12 @@ class KimiDeltaAttention(nn.Module, MambaBase):
has_initial_state=has_initial_state,
cache_indices=non_spec_state_indices_tensor,
query_start_loc=non_spec_query_start_loc,
metadata=attn_metadata,
metadata=attn_metadata_narrowed,
).transpose(0, 1)
else:
assert non_spec_state_indices_tensor is not None
decode_conv_indices = non_spec_state_indices_tensor[
: attn_metadata.num_actual_tokens
: attn_metadata_narrowed.num_actual_tokens
]
q = causal_conv1d_update(
q_proj_states,
@@ -408,7 +410,9 @@ class KimiDeltaAttention(nn.Module, MambaBase):
lambda x: rearrange(x, "n (h d) -> 1 n h d", d=self.head_dim), (q, k, v)
)
if attn_metadata.num_prefills > 0:
if attn_metadata_narrowed.num_prefills > 0:
assert non_spec_state_indices_tensor is not None
assert has_initial_state is not None
zero_idx = non_spec_state_indices_tensor[~has_initial_state]
recurrent_state[zero_idx] = 0
initial_state = recurrent_state[non_spec_state_indices_tensor].contiguous()
@@ -429,6 +433,7 @@ class KimiDeltaAttention(nn.Module, MambaBase):
# Init cache
recurrent_state[non_spec_state_indices_tensor] = last_recurrent_state
else:
assert non_spec_query_start_loc is not None
(
core_attn_out_non_spec,
last_recurrent_state,
@@ -440,7 +445,9 @@ class KimiDeltaAttention(nn.Module, MambaBase):
beta=beta,
initial_state=recurrent_state,
use_qk_l2norm_in_kernel=True,
cu_seqlens=non_spec_query_start_loc[: attn_metadata.num_decodes + 1],
cu_seqlens=non_spec_query_start_loc[
: attn_metadata_narrowed.num_decodes + 1
],
ssm_state_indices=non_spec_state_indices_tensor,
)
core_attn_out[0, :num_actual_tokens] = core_attn_out_non_spec[
+1 -5
View File
@@ -61,10 +61,6 @@ def fused_add_rms_norm(
) -> tuple[torch.Tensor, torch.Tensor]:
from vllm import _custom_ops as ops
if envs.VLLM_BATCH_INVARIANT:
return rms_norm_batch_invariant(
x + residual, weight, variance_epsilon
), x + residual
ops.fused_add_rms_norm(
x,
residual,
@@ -80,7 +76,7 @@ def poly_norm(
from vllm import _custom_ops as ops
out = torch.empty_like(x)
ops.poly_norm(
ops.poly_norm( # type: ignore[attr-defined]
out,
x,
weight,
+2 -1
View File
@@ -42,9 +42,10 @@ class MambaBase(AttentionLayerBase):
def get_kv_cache_spec(self, vllm_config: VllmConfig) -> KVCacheSpec | None:
mamba_block_size = vllm_config.cache_config.mamba_block_size
assert mamba_block_size is not None
page_size_padded = vllm_config.cache_config.mamba_page_size_padded
return MambaSpec(
shapes=self.get_state_shape(),
shapes=tuple(self.get_state_shape()),
dtypes=self.get_state_dtype(),
block_size=mamba_block_size,
page_size_padded=page_size_padded,
@@ -62,7 +62,6 @@ from vllm.utils.torch_utils import (
_resolve_layer_name,
direct_register_custom_op,
)
from vllm.v1.attention.backend import AttentionMetadata
from vllm.v1.attention.backends.gdn_attn import GDNAttentionMetadata
logger = init_logger(__name__)
@@ -121,9 +120,9 @@ def fi_chunk_gated_delta_rule(
class ChunkGatedDeltaRule(CustomOp):
def __init__(self) -> None:
super().__init__()
backend_cfg = get_current_vllm_config().additional_config.get(
"gdn_prefill_backend", "auto"
)
additional_config = get_current_vllm_config().additional_config
assert isinstance(additional_config, dict)
backend_cfg = additional_config.get("gdn_prefill_backend", "auto")
backend = str(backend_cfg).strip().lower()
supports_flashinfer = (
@@ -621,18 +620,19 @@ class GatedDeltaNetAttention(PluggableLayer, MambaBase):
# Part 2: Core Attention
# ============================================================
forward_context = get_forward_context()
attn_metadata: AttentionMetadata = forward_context.attn_metadata
attn_metadata_raw = forward_context.attn_metadata
core_attn_out = torch.zeros(
(num_tokens, self.num_v_heads // self.tp_size, self.head_v_dim),
dtype=hidden_states.dtype,
device=hidden_states.device,
)
z = torch.empty_like(core_attn_out)
if attn_metadata is not None:
attn_metadata = attn_metadata[self.prefix]
if attn_metadata_raw is not None:
assert isinstance(attn_metadata_raw, dict)
attn_metadata = attn_metadata_raw[self.prefix]
# TODO: xpu does not support this param yet
spec_sequence_masks = attn_metadata.spec_sequence_masks
spec_sequence_masks = attn_metadata.spec_sequence_masks # type: ignore[attr-defined]
assert spec_sequence_masks is None
conv_weights = self.conv1d.weight.view(
@@ -658,12 +658,12 @@ class GatedDeltaNetAttention(PluggableLayer, MambaBase):
activation=self.activation,
A_log=self.A_log,
dt_bias=self.dt_bias,
num_prefills=attn_metadata.num_prefills,
num_decodes=attn_metadata.num_decodes,
has_initial_state=attn_metadata.has_initial_state,
non_spec_query_start_loc=attn_metadata.non_spec_query_start_loc,
non_spec_state_indices_tensor=attn_metadata.non_spec_state_indices_tensor,
num_actual_tokens=attn_metadata.num_actual_tokens,
num_prefills=attn_metadata.num_prefills, # type: ignore[attr-defined]
num_decodes=attn_metadata.num_decodes, # type: ignore[attr-defined]
has_initial_state=attn_metadata.has_initial_state, # type: ignore[attr-defined]
non_spec_query_start_loc=attn_metadata.non_spec_query_start_loc, # type: ignore[attr-defined]
non_spec_state_indices_tensor=attn_metadata.non_spec_state_indices_tensor, # type: ignore[attr-defined]
num_actual_tokens=attn_metadata.num_actual_tokens, # type: ignore[attr-defined]
tp_size=self.tp_size,
reorder_input=not self.gqa_interleaved_layout,
)
@@ -792,16 +792,16 @@ class GatedDeltaNetAttention(PluggableLayer, MambaBase):
core_attn_out: torch.Tensor,
):
forward_context = get_forward_context()
attn_metadata: AttentionMetadata = forward_context.attn_metadata
attn_metadata_raw = forward_context.attn_metadata
if attn_metadata is None:
if attn_metadata_raw is None:
# V1 profile run — warm up prefill kernels so that
# autotuning completes before KV cache allocation.
self._warmup_prefill_kernels(mixed_qkv)
return
assert isinstance(attn_metadata, dict)
attn_metadata = attn_metadata[self.prefix]
assert isinstance(attn_metadata_raw, dict)
attn_metadata = attn_metadata_raw[self.prefix] # type: ignore[index]
assert isinstance(attn_metadata, GDNAttentionMetadata)
if (
@@ -860,14 +860,16 @@ class GatedDeltaNetAttention(PluggableLayer, MambaBase):
# 1.1: Process the multi-query part
if spec_sequence_masks is not None:
# spec_state_indices_tensor is always set when spec_sequence_masks is set
assert spec_state_indices_tensor is not None
mixed_qkv_spec = causal_conv1d_update(
mixed_qkv_spec,
conv_state,
conv_weights,
self.conv1d.bias,
self.activation,
conv_state_indices=spec_state_indices_tensor[:, 0][
: attn_metadata.num_spec_decodes
conv_state_indices=spec_state_indices_tensor[:, 0][ # type: ignore[index]
: attn_metadata.num_spec_decodes # type: ignore[attr-defined]
],
num_accepted_tokens=num_accepted_tokens,
query_start_loc=spec_query_start_loc,
@@ -900,8 +902,8 @@ class GatedDeltaNetAttention(PluggableLayer, MambaBase):
conv_weights,
self.conv1d.bias,
self.activation,
conv_state_indices=non_spec_state_indices_tensor[
: attn_metadata.num_actual_tokens
conv_state_indices=non_spec_state_indices_tensor[ # type: ignore[index]
: attn_metadata.num_actual_tokens # type: ignore[attr-defined]
],
validate_data=True,
)
@@ -965,8 +967,9 @@ class GatedDeltaNetAttention(PluggableLayer, MambaBase):
v=value_spec,
initial_state=ssm_state,
inplace_final_state=True,
cu_seqlens=spec_query_start_loc[
: attn_metadata.num_spec_decodes + 1
cu_seqlens=spec_query_start_loc[ # type: ignore[index]
: attn_metadata.num_spec_decodes
+ 1 # type: ignore[attr-defined]
],
ssm_state_indices=spec_state_indices_tensor,
num_accepted_tokens=num_accepted_tokens,
@@ -978,8 +981,10 @@ class GatedDeltaNetAttention(PluggableLayer, MambaBase):
# 2.2: Process the remaining part
if attn_metadata.num_prefills > 0:
initial_state = ssm_state[non_spec_state_indices_tensor].contiguous()
initial_state[~has_initial_state, ...] = 0
assert non_spec_state_indices_tensor is not None
initial_state = ssm_state[non_spec_state_indices_tensor].contiguous() # type: ignore[index]
assert has_initial_state is not None
initial_state[~has_initial_state, ...] = 0 # type: ignore[operator]
(
core_attn_out_non_spec,
last_recurrent_state,
@@ -1012,8 +1017,9 @@ class GatedDeltaNetAttention(PluggableLayer, MambaBase):
v=value_non_spec,
initial_state=ssm_state,
inplace_final_state=True,
cu_seqlens=non_spec_query_start_loc[
: attn_metadata.num_decodes + 1
cu_seqlens=non_spec_query_start_loc[ # type: ignore[index]
: attn_metadata.num_decodes
+ 1 # type: ignore[attr-defined]
],
ssm_state_indices=non_spec_state_indices_tensor,
use_qk_l2norm_in_kernel=True,
@@ -1073,7 +1079,7 @@ class GatedDeltaNetAttention(PluggableLayer, MambaBase):
conv_weights,
self.conv1d.bias,
self.activation,
conv_state_indices=non_spec_state_indices_tensor[:num_actual_tokens],
conv_state_indices=non_spec_state_indices_tensor[:num_actual_tokens], # type: ignore[index]
validate_data=False,
)
out_buf = core_attn_out[:num_actual_tokens].unsqueeze(1)
@@ -1086,7 +1092,7 @@ class GatedDeltaNetAttention(PluggableLayer, MambaBase):
scale=self.head_k_dim**-0.5,
initial_state=ssm_state,
out=out_buf,
ssm_state_indices=non_spec_state_indices_tensor[:num_actual_tokens],
ssm_state_indices=non_spec_state_indices_tensor[:num_actual_tokens], # type: ignore[index]
use_qk_l2norm_in_kernel=True,
)
return
@@ -396,10 +396,11 @@ class MiniMaxText01LinearAttention(nn.Module, MambaBase):
self, hidden_states: torch.Tensor, output: torch.Tensor, positions: torch.Tensor
) -> None:
forward_context = get_forward_context()
attn_metadata: AttentionMetadata = forward_context.attn_metadata
if attn_metadata is not None:
assert isinstance(attn_metadata, dict)
attn_metadata = attn_metadata[self.prefix]
attn_metadata_raw = forward_context.attn_metadata
attn_metadata: AttentionMetadata | None = None
if attn_metadata_raw is not None:
assert isinstance(attn_metadata_raw, dict)
attn_metadata = attn_metadata_raw[self.prefix]
assert isinstance(attn_metadata, LinearAttentionMetadata)
num_actual_tokens = (
attn_metadata.num_prefill_tokens + attn_metadata.num_decode_tokens
@@ -40,6 +40,7 @@ from vllm.utils.torch_utils import (
_resolve_layer_name,
direct_register_custom_op,
)
from vllm.v1.attention.backend import AttentionMetadata
from vllm.v1.attention.backends.mamba1_attn import Mamba1AttentionMetadata
@@ -258,15 +259,16 @@ class MambaMixer(MambaBase, PluggableLayer):
"""
forward_context: ForwardContext = get_forward_context()
attn_metadata = forward_context.attn_metadata
attn_metadata_raw = forward_context.attn_metadata
assert self.cache_config is not None
mamba_block_size = self.cache_config.mamba_block_size
is_mamba_cache_all = self.cache_config.mamba_cache_mode == "all"
if attn_metadata is not None:
assert isinstance(attn_metadata, dict)
attn_metadata = attn_metadata[self.prefix]
attn_metadata: AttentionMetadata | None = None
if attn_metadata_raw is not None:
assert isinstance(attn_metadata_raw, dict)
attn_metadata = attn_metadata_raw[self.prefix]
assert isinstance(attn_metadata, Mamba1AttentionMetadata)
query_start_loc_p = attn_metadata.query_start_loc_p
state_indices_tensor_p = attn_metadata.state_indices_tensor_p
@@ -391,6 +393,9 @@ class MambaMixer(MambaBase, PluggableLayer):
ssm_outputs.append(scan_out_p)
if has_decode:
# state_indices_tensor_d is assigned when attn_metadata is not None,
# and has_decode is only True when attn_metadata is not None
assert state_indices_tensor_d is not None
if is_mamba_cache_all:
state_indices_tensor_d_input = state_indices_tensor_d.gather(
1, block_idx_last_computed_token_d.unsqueeze(1)
@@ -572,14 +572,16 @@ class MambaMixer2(MambaBase, PluggableLayer):
# kernels to operate in continuous batching and in chunked prefill
# modes; they are computed at top-level model forward since they
# stay the same and reused for all mamba layers in the same iteration
attn_metadata: AttentionMetadata = forward_context.attn_metadata
attn_metadata_raw = forward_context.attn_metadata
assert self.cache_config is not None
mamba_block_size = self.cache_config.mamba_block_size
is_mamba_cache_all = self.cache_config.mamba_cache_mode == "all"
if attn_metadata is not None:
assert isinstance(attn_metadata, dict)
attn_metadata = attn_metadata[self.prefix]
attn_metadata: AttentionMetadata | None = None
if attn_metadata_raw is not None:
assert isinstance(attn_metadata_raw, dict)
attn_metadata = attn_metadata_raw[self.prefix]
assert isinstance(attn_metadata, Mamba2AttentionMetadata)
# conv_state must be (..., dim, width-1) for the conv kernels.
# DS layout stores it that way directly; SD layout needs a
@@ -708,6 +710,7 @@ class MambaMixer2(MambaBase, PluggableLayer):
# 3. State Space Model sequence transformation
initial_states = None
if has_initial_states_p is not None and prep_initial_states:
assert state_indices_tensor_p is not None
kernel_ssm_indices = state_indices_tensor_p
if is_mamba_cache_all:
kernel_ssm_indices = state_indices_tensor_p.gather(
@@ -746,6 +749,13 @@ class MambaMixer2(MambaBase, PluggableLayer):
)
if is_mamba_cache_all:
assert mamba_block_size is not None
assert state_indices_tensor_p is not None
assert block_idx_first_scheduled_token_p is not None
assert block_idx_last_scheduled_token_p is not None
assert last_chunk_indices_p is not None
assert num_computed_tokens_p is not None
# The chunk_stride is the number of chunks per mamba block
# e.g., if mamba_block_size = 512 and chunk_size = 256,
# then chunk_stride = 2
@@ -810,6 +820,7 @@ class MambaMixer2(MambaBase, PluggableLayer):
ssm_state[cache_blocks_to_fill] = from_where
# For all seqs, store the last state (note: might be partial):
assert state_indices_tensor_p is not None
ssm_state[
state_indices_tensor_p.gather(
1, block_idx_last_scheduled_token_p.unsqueeze(1)
@@ -820,10 +831,12 @@ class MambaMixer2(MambaBase, PluggableLayer):
# update ssm states
# - varlen state is a (num_prefills, nheads, headdim, dstate)
# tensor
assert state_indices_tensor_p is not None
ssm_state[state_indices_tensor_p] = varlen_states
# Process decode requests
if has_decode:
assert state_indices_tensor_d is not None
if is_mamba_cache_all:
state_indices_tensor_d_input = state_indices_tensor_d.gather(
1, block_idx_last_computed_token_d.unsqueeze(1)
@@ -113,10 +113,11 @@ class ShortConv(MambaBase, CustomOp):
# chunked prefill modes; they are computed at top-level model forward
# since they stay the same and reused for all mamba layers in the same
# iteration.
attn_metadata: AttentionMetadata = forward_context.attn_metadata
if attn_metadata is not None:
assert isinstance(attn_metadata, dict)
attn_metadata = attn_metadata[self.prefix]
attn_metadata_raw = forward_context.attn_metadata
attn_metadata: AttentionMetadata | None = None
if attn_metadata_raw is not None:
assert isinstance(attn_metadata_raw, dict)
attn_metadata = attn_metadata_raw[self.prefix]
assert isinstance(attn_metadata, ShortConvAttentionMetadata)
conv_state = (
self.kv_cache[0]
@@ -115,6 +115,7 @@ def pooler_for_classify(
vllm_config = get_current_vllm_config()
model_config = vllm_config.model_config
assert model_config.pooler_config is not None
head = ClassifierPoolerHead(
head_dtype=model_config.head_dtype,
classifier=classifier,
@@ -124,6 +124,7 @@ def pooler_for_token_classify(
vllm_config = get_current_vllm_config()
model_config = vllm_config.model_config
assert model_config.pooler_config is not None
head = TokenClassifierPoolerHead(
head_dtype=model_config.head_dtype,
classifier=classifier,
@@ -198,11 +198,15 @@ class CompressedTensorsW4A8Fp8MoEMethod(CompressedTensorsMoEMethod):
# encode and reorder weight tensors, and get the layout to pass to
# the grouped gemm kernel. `b_strides1/2` specifies the entire layout
convert_packed_uint4b8_to_signed_int4_inplace(layer.w13_weight_packed)
# mirror the sync in CutlassW4A8LinearKernel; required for tp>1 correctness
torch.accelerator.synchronize()
w13_weight_shuffled, self.b_strides1 = (
ops.cutlass_encode_and_reorder_int4b_grouped(layer.w13_weight_packed)
)
replace_parameter(layer, "w13_weight_packed", w13_weight_shuffled)
convert_packed_uint4b8_to_signed_int4_inplace(layer.w2_weight_packed)
# mirror the sync in CutlassW4A8LinearKernel; required for tp>1 correctness
torch.accelerator.synchronize()
w2_weight_shuffled, self.b_strides2 = (
ops.cutlass_encode_and_reorder_int4b_grouped(layer.w2_weight_packed)
)
@@ -3,7 +3,7 @@
# Supports FP-Quant compression, see https://arxiv.org/abs/2509.23202
from typing import Any
from typing import Any, Literal, cast
import torch
from torch.nn.parameter import Parameter
@@ -251,7 +251,11 @@ class FPQuantLinearMethod(LinearMethodBase):
def fused_quantize_mx(
x_flat: torch.Tensor, hadamard_matrix: torch.Tensor, forward_method: str
) -> tuple[torch.Tensor, torch.Tensor]:
return fusedQuantizeMx(x_flat, hadamard_matrix, method=forward_method)
return fusedQuantizeMx(
x_flat,
hadamard_matrix,
method=cast(Literal["quest", "abs_max"], forward_method),
)
def fused_quantize_mx_fake(x_flat, hadamard_matrix, forward_method):
@@ -114,7 +114,7 @@ class QuarkConfig(QuantizationConfig):
:param hf_to_vllm_mapper: maps from hf model structure (the assumed
structure of the qconfig) to vllm model structure
"""
quant_config_with_hf_to_vllm_mapper = {}
quant_config_with_hf_to_vllm_mapper: dict[str, Any] = {}
for k, v in self.quant_config.items():
if isinstance(v, list):
@@ -356,6 +356,12 @@ def get_and_maybe_dequant_weights(
from vllm.model_executor.layers.linear import UnquantizedLinearMethod
from vllm.model_executor.layers.quantization.fp8 import Fp8LinearMethod
# LoRA linear wrappers store quantization metadata on `base_layer`.
# Unwrap here so callers can pass either a raw linear layer or its LoRA
# wrapper without special-casing.
while hasattr(layer, "base_layer") and hasattr(layer.base_layer, "quant_method"):
layer = layer.base_layer
weight = get_attribute_fallback(layer, ["weight", "qweight", "weight_packed"])
# Unquantized layer: just return base weights
@@ -818,7 +824,7 @@ def convert_bf16_scales_to_fp8(
# restore original shape
fp8_scales = fp8_scales.view(orig_shape)
chan_scales = chan_scales.view(orig_shape[:-1], -1)
chan_scales = chan_scales.view(*orig_shape[:-1], -1)
return fp8_scales, chan_scales
@@ -26,7 +26,7 @@ from vllm.v1.worker.workspace import current_workspace_manager
if current_platform.is_cuda_alike():
from vllm import _custom_ops as ops
elif current_platform.is_xpu():
from vllm._xpu_ops import xpu_ops as ops
from vllm._xpu_ops import xpu_ops
logger = init_logger(__name__)
@@ -84,12 +84,12 @@ def sparse_attn_indexer(
total_seq_lens,
topk_indices_buffer,
)
attn_metadata = attn_metadata[k_cache_prefix]
assert isinstance(attn_metadata, DeepseekV32IndexerMetadata)
slot_mapping = attn_metadata.slot_mapping
has_decode = attn_metadata.num_decodes > 0
has_prefill = attn_metadata.num_prefills > 0
num_decode_tokens = attn_metadata.num_decode_tokens
attn_metadata_narrowed = attn_metadata[k_cache_prefix]
assert isinstance(attn_metadata_narrowed, DeepseekV32IndexerMetadata)
slot_mapping = attn_metadata_narrowed.slot_mapping
has_decode = attn_metadata_narrowed.num_decodes > 0
has_prefill = attn_metadata_narrowed.num_prefills > 0
num_decode_tokens = attn_metadata_narrowed.num_decode_tokens
# During speculative decoding, k may be padded to the CUDA graph batch
# size while slot_mapping only covers actual tokens. Truncate k to avoid
@@ -97,6 +97,8 @@ def sparse_attn_indexer(
num_tokens = slot_mapping.shape[0]
k = k[:num_tokens]
# scale_fmt can be None, but the function expects str
assert scale_fmt is not None
ops.indexer_k_quant_and_cache(
k,
kv_cache,
@@ -107,7 +109,7 @@ def sparse_attn_indexer(
topk_indices_buffer[: hidden_states.shape[0]] = -1
if has_prefill:
prefill_metadata = attn_metadata.prefill
prefill_metadata = attn_metadata_narrowed.prefill
assert prefill_metadata is not None
# Get the full shared workspace buffers once (will allocate on first use)
@@ -144,7 +146,7 @@ def sparse_attn_indexer(
]
if current_platform.is_xpu():
ops.top_k_per_row_prefill(
xpu_ops.top_k_per_row_prefill( # type: ignore[attr-defined]
logits,
chunk.cu_seqlen_ks,
chunk.cu_seqlen_ke,
@@ -167,7 +169,7 @@ def sparse_attn_indexer(
)
if has_decode:
decode_metadata = attn_metadata.decode
decode_metadata = attn_metadata_narrowed.decode
assert decode_metadata is not None
# kv_cache shape [
# kv_cache size requirement [num_block, block_size, n_head, head_dim],
@@ -217,11 +219,11 @@ def sparse_attn_indexer(
topk_indices,
topk_workspace,
topk_tokens,
attn_metadata.max_seq_len,
attn_metadata_narrowed.max_seq_len,
)
else:
if current_platform.is_xpu():
ops.top_k_per_row_decode(
xpu_ops.top_k_per_row_decode( # type: ignore[attr-defined]
logits,
next_n,
seq_lens,

Some files were not shown because too many files have changed in this diff Show More