forked from Karylab-cklius/vllm
Compare commits
33
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cb68c1a314 | ||
|
|
33ef1941e2 | ||
|
|
a4905133f3 | ||
|
|
ecbe42e991 | ||
|
|
a250f1bd5f | ||
|
|
04eac6ba24 | ||
|
|
9047288b68 | ||
|
|
ed6d30377d | ||
|
|
6aa057c9d7 | ||
|
|
a2bd09c960 | ||
|
|
123674879e | ||
|
|
4254aeb56f | ||
|
|
aad88f8486 | ||
|
|
0210024ae7 | ||
|
|
4eafc72928 | ||
|
|
6d09769700 | ||
|
|
4506319a28 | ||
|
|
9b60e2ffaa | ||
|
|
3951d3eacd | ||
|
|
6f2c71be8f | ||
|
|
2463f00fb6 | ||
|
|
f946659fff | ||
|
|
f90aa44662 | ||
|
|
cefa5281a7 | ||
|
|
46794958f0 | ||
|
|
6ff8dea075 | ||
|
|
583e6f2226 | ||
|
|
96a85c5750 | ||
|
|
9db4650e5e | ||
|
|
5e584ce9ec | ||
|
|
1842447c09 | ||
|
|
16688b26a6 | ||
|
|
6fbec8ed47 |
@@ -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),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"] = []
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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})"
|
||||
)
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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()
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -29,7 +29,6 @@ SEPARATE_GROUPS = [
|
||||
"tests",
|
||||
# v0 related
|
||||
"vllm/lora",
|
||||
"vllm/model_executor/layers",
|
||||
]
|
||||
|
||||
# TODO(woosuk): Include the code from Megatron and HuggingFace.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
+18
-6
@@ -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,
|
||||
)
|
||||
|
||||
+16
-9
@@ -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,
|
||||
)
|
||||
@@ -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[
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
+4
@@ -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
Reference in New Issue
Block a user