@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / f1d53d150c
Fork 0

A high-throughput and memory-efficient inference and serving engine for LLMs

Mirrored from https://github.com/tduncan-lum/Nvidia-Optimization-Stack---VLLM.git

Synced Nov 14, 05:30 UTC

[Multimodal][Speculative Decoding]Eagle Eagle3 mm support, enablement on qwen2.5vl (#22872)

Signed-off-by: Junhong <liujunhong11@huawei.com> Signed-off-by: Junhong Liu <98734602+LJH-LBJ@users.noreply.github.com> Co-authored-by: Junhong <liujunhong11@huawei.com> Co-authored-by: LJH-LBJ <98734602+LJH-LBJ@users.noreply.github.com>

WeiQing Chen committed Sep 27, 2025 at 11:35 UTC f1d53d150c5cd9c7d94db296793fc25f955ea8a9
8 files changed +210 -45
tests/models/registry.py
+3
@@ -651,6 +651,9 @@ _SPECULATIVE_DECODING_EXAMPLE_MODELS = {
651 "MiMoMTPModel": _HfExamplesInfo("XiaomiMiMo/MiMo-7B-RL",
652 trust_remote_code=True,
653 speculative_model="XiaomiMiMo/MiMo-7B-RL"),
654 + "Eagle3Qwen2_5vlForCausalLM": _HfExamplesInfo(
655 + "Qwen/Qwen2.5-VL-7B-Instruct",
656 + speculative_model="Rayzl/qwen2.5-vl-7b-eagle3-sgl"),
657 "Qwen3NextMTP": _HfExamplesInfo("Qwen/Qwen3-Next-80B-A3B-Instruct",
658 min_transformers_version="4.56.3"),
659 }
tests/v1/e2e/test_spec_decode.py
+7 -2
@@ -129,6 +129,11 @@ def test_ngram_correctness(
129 ["model_setup", "mm_enabled"],
130 [
131 (("eagle3", "Qwen/Qwen3-8B", "AngelSlim/Qwen3-8B_eagle3", 1), False),
132 + pytest.param(("eagle3", "Qwen/Qwen2.5-VL-7B-Instruct",
133 + "Rayzl/qwen2.5-vl-7b-eagle3-sgl", 1),
134 + False,
135 + marks=pytest.mark.skip(reason="Skipping due to its " \
136 + "head_dim not being a a multiple of 32")),
137 (("eagle", "meta-llama/Llama-3.1-8B-Instruct",
138 "yuhuili/EAGLE-LLaMA3.1-Instruct-8B", 1), False),
139 (("eagle3", "meta-llama/Llama-3.1-8B-Instruct",
@@ -145,8 +150,8 @@ def test_ngram_correctness(
150 "eagle618/eagle-deepseek-v3-random", 1), False),
151 ],
152 ids=[
148 - "qwen3_eagle3", "llama3_eagle", "llama3_eagle3", "llama4_eagle",
149 - "llama4_eagle_mm", "deepseek_eagle"
153 + "qwen3_eagle3", "qwen2_5_vl_eagle3", "llama3_eagle", "llama3_eagle3",
154 + "llama4_eagle", "llama4_eagle_mm", "deepseek_eagle"
155 ])
156 @pytest.mark.parametrize("attn_backend",
157 get_attn_backend_list_based_on_platform())
vllm/benchmarks/datasets.py
+80
@@ -1450,6 +1450,13 @@ def get_samples(args, tokenizer) -> list[SampleRequest]:
1450 ):
1451 dataset_class = MLPerfDataset
1452 args.hf_split = "train"
1453 + elif (
1454 + args.dataset_path in MMStarDataset.SUPPORTED_DATASET_PATHS
1455 + or args.hf_name in MMStarDataset.SUPPORTED_DATASET_PATHS
1456 + ):
1457 + dataset_class = MMStarDataset
1458 + args.hf_split = "val"
1459 + args.hf_subset = None
1460 else:
1461 supported_datasets = set([
1462 dataset_name for cls in HuggingFaceDataset.__subclasses__()
@@ -2721,3 +2728,76 @@ class PrefixRepetitionRandomDataset(BenchmarkDataset):
2728
2729 random.shuffle(requests)
2730 return requests
2731 +
2732 +
2733 +# -----------------------------------------------------------------------------
2734 +# MMStar Dataset Implementation
2735 +# -----------------------------------------------------------------------------
2736 +
2737 +
2738 +class MMStarDataset(HuggingFaceDataset):
2739 + """
2740 + Lin-Chen/MMStar: https://huggingface.co/datasets/Lin-Chen/MMStar
2741 + refer to: https://github.com/sgl-project/SpecForge/pull/106
2742 + """
2743 + DEFAULT_OUTPUT_LEN = 128
2744 + SUPPORTED_DATASET_PATHS = {"Lin-Chen/MMStar"}
2745 + IS_MULTIMODAL = True
2746 +
2747 + def sample(
2748 + self,
2749 + tokenizer: PreTrainedTokenizerBase,
2750 + num_requests: int,
2751 + output_len: Optional[int] = None,
2752 + enable_multimodal_chat: bool = False,
2753 + request_id_prefix: str = "",
2754 + no_oversample: bool = False,
2755 + **kwargs,
2756 + ) -> list[SampleRequest]:
2757 + # If --hf-output-len is not set, use the default output length.
2758 + output_len = (output_len
2759 + if output_len is not None else self.DEFAULT_OUTPUT_LEN)
2760 + sampled_requests: list[SampleRequest] = []
2761 +
2762 + for ind, item in enumerate(self.data):
2763 + if len(sampled_requests) >= num_requests:
2764 + break
2765 + # Split the question text from options
2766 + # (keep only the part before "Options:").
2767 + full_q: str = item.get("question", "")
2768 + question_text = full_q.split("Options:", 1)[0].strip()
2769 +
2770 + # Multimodal image content.
2771 + mm_content = process_image(item["image"])
2772 +
2773 + # Compute prompt token length (note: this is plain text length
2774 + # if enable_multimodal_chat is False).
2775 + prompt_len = len(tokenizer(question_text).input_ids)
2776 +
2777 + if enable_multimodal_chat:
2778 + # If multimodal content should be embedded in the chat message,
2779 + # convert to [{"role":"user","content":[...]}]
2780 + prompt = self.apply_multimodal_chat_transformation(
2781 + question_text, mm_content
2782 + )
2783 + mm_for_request = None # Already embedded in chat content.
2784 + else:
2785 + # Default: prompt is plain text,
2786 + # image is in mm_content for the bench to assemble.
2787 + prompt = question_text
2788 + mm_for_request = mm_content
2789 +
2790 + sampled_requests.append(
2791 + SampleRequest(
2792 + prompt=prompt,
2793 + prompt_len=prompt_len,
2794 + expected_output_len=output_len,
2795 + multi_modal_data=mm_for_request,
2796 + request_id=request_id_prefix + str(ind),
2797 + )
2798 + )
2799 +
2800 + self.maybe_oversample_requests(
2801 + sampled_requests, num_requests, request_id_prefix, no_oversample
2802 + )
2803 + return sampled_requests
vllm/model_executor/models/llama_eagle3.py
+19 -8
@@ -8,7 +8,6 @@ import torch
8 import torch.nn as nn
9 from transformers import LlamaConfig
10
11 -from vllm.compilation.decorators import support_torch_compile
11 from vllm.config import CacheConfig, VllmConfig, get_current_vllm_config
12 from vllm.logger import init_logger
13 from vllm.model_executor.layers.layernorm import RMSNorm
@@ -19,6 +18,7 @@ from vllm.model_executor.layers.quantization.base_config import (
18 from vllm.model_executor.layers.vocab_parallel_embedding import (
19 DEFAULT_VOCAB_PADDING_SIZE, ParallelLMHead, VocabParallelEmbedding)
20 from vllm.model_executor.model_loader.weight_utils import default_weight_loader
21 +from vllm.model_executor.models.interfaces import MultiModalEmbeddings
22 from vllm.model_executor.models.llama import (LlamaDecoderLayer,
23 LlamaForCausalLM)
24
@@ -102,7 +102,6 @@ class LlamaDecoderLayer(LlamaDecoderLayer):
102 return hidden_states, residual
103
104
105 -@support_torch_compile
105 class LlamaModel(nn.Module):
106
107 def __init__(
@@ -145,13 +144,21 @@ class LlamaModel(nn.Module):
144 eps=self.config.rms_norm_eps,
145 )
146
147 + def get_input_embeddings(
148 + self,
149 + input_ids: torch.Tensor,
150 + ) -> torch.Tensor:
151 + return self.embed_tokens(input_ids)
152 +
153 def forward(
154 self,
155 input_ids: torch.Tensor,
156 positions: torch.Tensor,
157 hidden_states: torch.Tensor,
158 + input_embeds: Optional[torch.Tensor] = None,
159 ) -> tuple[torch.Tensor, torch.Tensor]:
154 - input_embeds = self.embed_tokens(input_ids)
160 + if input_embeds is None:
161 + input_embeds = self.get_input_embeddings(input_ids)
162 assert hidden_states.shape[-1] == input_embeds.shape[-1]
163
164 residual = None
@@ -239,11 +246,7 @@ class Eagle3LlamaForCausalLM(LlamaForCausalLM):
246 hidden_states: torch.Tensor,
247 inputs_embeds: Optional[torch.Tensor] = None,
248 ) -> tuple[torch.Tensor, torch.Tensor]:
242 - if inputs_embeds is not None:
243 - raise NotImplementedError(
244 - f"{type(self).__name__} does not support multimodal inputs yet."
245 - )
246 - return self.model(input_ids, positions, hidden_states)
249 + return self.model(input_ids, positions, hidden_states, inputs_embeds)
250
251 def compute_logits(
252 self,
@@ -299,3 +302,11 @@ class Eagle3LlamaForCausalLM(LlamaForCausalLM):
302 skip_substrs=skip_substrs,
303 )
304 loader.load_weights(model_weights.items())
305 +
306 + def get_input_embeddings(
307 + self,
308 + input_ids: torch.Tensor,
309 + multimodal_embeddings: Optional[MultiModalEmbeddings] = None,
310 + ) -> torch.Tensor:
311 + inputs_embeds = self.model.get_input_embeddings(input_ids)
312 + return inputs_embeds
vllm/model_executor/models/qwen2_5_vl.py
+9 -2
@@ -68,7 +68,7 @@ from vllm.transformers_utils.config import uses_mrope
68 from vllm.utils import is_pin_memory_available
69 from vllm.utils.tensor_schema import TensorSchema, TensorShape
70
71 -from .interfaces import (MultiModalEmbeddings, SupportsLoRA,
71 +from .interfaces import (MultiModalEmbeddings, SupportsEagle3, SupportsLoRA,
72 SupportsMultiModal, SupportsMultiModalPruning,
73 SupportsPP, SupportsQuant)
74 from .qwen2_vl import Qwen2VLDummyInputsBuilder as Qwen2_5_VLDummyInputsBuilder
@@ -965,7 +965,7 @@ class Qwen2_5_VLMultiModalProcessor(Qwen2VLMultiModalProcessor):
965 dummy_inputs=Qwen2_5_VLDummyInputsBuilder)
966 class Qwen2_5_VLForConditionalGeneration(nn.Module, SupportsMultiModal,
967 SupportsLoRA, SupportsPP,
968 - SupportsQuant,
968 + SupportsQuant, SupportsEagle3,
969 SupportsMultiModalPruning):
970
971 packed_modules_mapping = {
@@ -1028,6 +1028,13 @@ class Qwen2_5_VLForConditionalGeneration(nn.Module, SupportsMultiModal,
1028 self.make_empty_intermediate_tensors = (
1029 self.language_model.make_empty_intermediate_tensors)
1030
1031 + def set_aux_hidden_state_layers(self, layers: tuple[int, ...]) -> None:
1032 + self.language_model.model.aux_hidden_state_layers = layers
1033 +
1034 + def get_eagle3_aux_hidden_state_layers(self) -> tuple[int, ...]:
1035 + num_layers = len(self.language_model.model.layers)
1036 + return (2, num_layers // 2, num_layers - 3)
1037 +
1038 def _validate_and_reshape_mm_tensor(self, mm_input: object,
1039 name: str) -> torch.Tensor:
1040 if not isinstance(mm_input, (torch.Tensor, list)):
vllm/model_executor/models/registry.py
+1
@@ -286,6 +286,7 @@ _SPECULATIVE_DECODING_MODELS = {
286 "EagleMiniCPMForCausalLM": ("minicpm_eagle", "EagleMiniCPMForCausalLM"),
287 "Eagle3LlamaForCausalLM": ("llama_eagle3", "Eagle3LlamaForCausalLM"),
288 "LlamaForCausalLMEagle3": ("llama_eagle3", "Eagle3LlamaForCausalLM"),
289 + "Eagle3Qwen2_5vlForCausalLM": ("llama_eagle3", "Eagle3LlamaForCausalLM"),
290 "EagleDeepSeekMTPModel": ("deepseek_eagle", "EagleDeepseekV3ForCausalLM"),
291 "DeepSeekMTPModel": ("deepseek_mtp", "DeepSeekMTP"),
292 "ErnieMTPModel": ("ernie_mtp", "ErnieMTP"),
vllm/v1/spec_decode/eagle.py
+79 -29
@@ -80,9 +80,17 @@ class EagleProposer:
80 self.input_ids = torch.zeros(self.max_num_tokens,
81 dtype=torch.int32,
82 device=device)
83 - self.positions = torch.zeros(self.max_num_tokens,
84 - dtype=torch.int64,
85 - device=device)
83 + self.uses_mrope = self.vllm_config.model_config.uses_mrope
84 + if self.uses_mrope:
85 + # M-RoPE need (3, max_num_tokens)
86 + self.mrope_positions = torch.zeros((3, self.max_num_tokens),
87 + dtype=torch.int64,
88 + device=device)
89 + else:
90 + # RoPE need (max_num_tokens,)
91 + self.positions = torch.zeros(self.max_num_tokens,
92 + dtype=torch.int64,
93 + device=device)
94 self.hidden_states = torch.zeros(
95 (self.max_num_tokens, self.hidden_size),
96 dtype=self.dtype,
@@ -143,11 +151,22 @@ class EagleProposer:
151 dtype=torch.int32,
152 ).repeat(max_batch_size, 1)
153
154 + def _get_positions(self, num_tokens: int):
155 + if self.uses_mrope:
156 + return self.mrope_positions[:, :num_tokens]
157 + return self.positions[:num_tokens]
158 +
159 + def _set_positions(self, num_tokens: int, positions: torch.Tensor):
160 + if self.uses_mrope:
161 + self.mrope_positions[:, :num_tokens] = positions
162 + else:
163 + self.positions[:num_tokens] = positions
164 +
165 def propose(
166 self,
167 # [num_tokens]
168 target_token_ids: torch.Tensor,
150 - # [num_tokens]
169 + # [num_tokens] or [3, num_tokens] when M-RoPE is enabled
170 target_positions: torch.Tensor,
171 # [num_tokens, hidden_size]
172 target_hidden_states: torch.Tensor,
@@ -198,7 +217,7 @@ class EagleProposer:
217 else:
218 num_input_tokens = num_tokens
219 # copy inputs to buffer for cudagraph
201 - self.positions[:num_tokens] = target_positions
220 + self._set_positions(num_tokens, target_positions)
221 self.hidden_states[:num_tokens] = target_hidden_states
222 if self.is_multimodal_model:
223 input_ids = self.input_ids[:num_tokens]
@@ -218,7 +237,7 @@ class EagleProposer:
237 num_tokens=num_input_tokens):
238 ret_hidden_states = self.model(
239 input_ids=input_ids,
221 - positions=self.positions[:num_input_tokens],
240 + positions=self._get_positions(num_input_tokens),
241 hidden_states=self.hidden_states[:num_input_tokens],
242 inputs_embeds=inputs_embeds,
243 )
@@ -235,7 +254,10 @@ class EagleProposer:
254 draft_token_ids = logits.argmax(dim=-1)
255 return draft_token_ids.view(-1, 1)
256
238 - positions = target_positions[last_token_indices]
257 + if self.uses_mrope:
258 + positions = target_positions[:, last_token_indices]
259 + else:
260 + positions = target_positions[last_token_indices]
261 if self.method in ("deepseek_mtp", "ernie_mtp", "longcat_flash_mtp"):
262 hidden_states = self.hidden_states[last_token_indices]
263 else:
@@ -282,25 +304,34 @@ class EagleProposer:
304 # cast to int32 is crucial when eagle model is compiled.
305 # tensor.argmax() returns int64 by default.
306 input_ids = draft_token_ids_list[-1].int()
285 - positions += 1
286 -
287 - # NOTE(woosuk): We should handle the case where the draft model
288 - # generates tokens beyond the max model length. Since it is complex
289 - # to remove such requests from the batch, we keep them in the batch
290 - # but adjust the position ids and slot mappings to avoid the
291 - # out-of-range access during the model execution. The draft tokens
292 - # generated with this adjustment should be ignored.
293 - exceeds_max_model_len = positions >= self.max_model_len
294 - # Mask out the position ids that exceed the max model length.
295 - # Otherwise, we may get out-of-range error in RoPE.
296 - clamped_positions = torch.where(exceeds_max_model_len, 0,
297 - positions)
307 + if self.uses_mrope:
308 + positions += 1
309 + # NOTE(woosuk): We should handle the case where the draft model
310 + # generates tokens beyond the max model length.
311 + # Since it is complex to remove such requests from the batch,
312 + # we keep them in the batch but adjust the position ids
313 + # and slot mappings to avoid the
314 + # out-of-range access during the model execution.
315 + # The draft tokens generated with this adjustment
316 + # should be ignored.
317 + exceeds_max_model_len = positions[0] >= self.max_model_len
318 + # Mask out the position ids that exceed the max model length.
319 + # Otherwise, we may get out-of-range error in RoPE.
320 + clamped_positions = torch.where\
321 + (exceeds_max_model_len.unsqueeze(0), \
322 + torch.zeros_like(positions), positions)
323 + else:
324 + positions += 1
325 + exceeds_max_model_len = positions >= self.max_model_len
326 + clamped_positions = torch.where(exceeds_max_model_len, 0,
327 + positions)
328
329 # Increment the sequence lengths.
330 common_attn_metadata.seq_lens += 1
331 common_attn_metadata.seq_lens_cpu += 1
332 # For the requests that exceed the max model length, we set the
333 # sequence length to 1 to minimize their overheads in attention.
334 +
335 common_attn_metadata.seq_lens.masked_fill_(exceeds_max_model_len,
336 1)
337
@@ -308,13 +339,22 @@ class EagleProposer:
339 common_attn_metadata.seq_lens_cpu - 1
340
341 # Compute the slot mapping.
311 - block_numbers = clamped_positions // self.block_size
342 + if self.uses_mrope:
343 + # all dimensions of positions are the same
344 + block_numbers = clamped_positions[0] // self.block_size
345 + else:
346 + block_numbers = clamped_positions // self.block_size
347 block_ids = common_attn_metadata.block_table_tensor.gather(
348 dim=1, index=block_numbers.view(-1, 1))
349 block_ids = block_ids.view(-1)
315 - common_attn_metadata.slot_mapping = (
316 - block_ids * self.block_size +
317 - clamped_positions % self.block_size)
350 + if self.uses_mrope:
351 + common_attn_metadata.slot_mapping = (
352 + block_ids * self.block_size +
353 + clamped_positions[0] % self.block_size)
354 + else:
355 + common_attn_metadata.slot_mapping = (
356 + block_ids * self.block_size +
357 + clamped_positions % self.block_size)
358 # Mask out the slot mappings that exceed the max model length.
359 # Otherwise, the KV cache will be inadvertently updated with the
360 # padding tokens.
@@ -330,7 +370,7 @@ class EagleProposer:
370
371 # copy inputs to buffer for cudagraph
372 self.input_ids[:batch_size] = input_ids
333 - self.positions[:batch_size] = clamped_positions
373 + self._set_positions(batch_size, clamped_positions)
374 self.hidden_states[:batch_size] = hidden_states
375 if self.is_multimodal_model:
376 inputs_embeds = self.model.get_input_embeddings(input_ids)
@@ -347,7 +387,7 @@ class EagleProposer:
387 num_tokens=input_batch_size):
388 ret_hidden_states = self.model(
389 input_ids=input_ids,
350 - positions=self.positions[:input_batch_size],
390 + positions=self._get_positions(input_batch_size),
391 hidden_states=self.hidden_states[:input_batch_size],
392 inputs_embeds=inputs_embeds,
393 )
@@ -787,6 +827,11 @@ class EagleProposer:
827
828 return spec_common_attn_metadata, token_indices
829
830 + def get_model_name(self, model: nn.Module) -> str:
831 + if hasattr(model, 'module'): # multi-GPU
832 + model = model.module
833 + return model.__class__.__name__
834 +
835 def load_model(self, target_model: nn.Module) -> None:
836 draft_model_config = \
837 self.vllm_config.speculative_config.draft_model_config
@@ -820,8 +865,13 @@ class EagleProposer:
865
866 if supports_multimodal(target_model):
867 # handle multimodality
823 - self.model.config.image_token_index = (
824 - target_model.config.image_token_index)
868 + if (self.get_model_name(target_model) ==
869 + "Qwen2_5_VLForConditionalGeneration"):
870 + self.model.config.image_token_index = (
871 + target_model.config.image_token_id)
872 + else:
873 + self.model.config.image_token_index = (
874 + target_model.config.image_token_index)
875 target_language_model = target_model.get_language_model()
876 else:
877 target_language_model = target_model
@@ -892,7 +942,7 @@ class EagleProposer:
942
943 self.model(
944 input_ids=input_ids,
895 - positions=self.positions[:num_tokens],
945 + positions=self._get_positions(num_tokens),
946 hidden_states=self.hidden_states[:num_tokens],
947 inputs_embeds=inputs_embeds,
948 )
vllm/v1/worker/gpu_model_runner.py
+12 -4
@@ -442,6 +442,16 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
442 device="cpu",
443 pin_memory=self.pin_memory)
444
445 + def _get_positions(self, num_tokens: Any):
446 + if isinstance(num_tokens, int):
447 + if self.uses_mrope:
448 + return self.mrope_positions.gpu[:, :num_tokens]
449 + return self.positions.gpu[:num_tokens]
450 + else:
451 + if self.uses_mrope:
452 + return self.mrope_positions.gpu[:, num_tokens]
453 + return self.positions.gpu[num_tokens]
454 +
455 def _make_buffer(self,
456 *size: Union[int, torch.SymInt],
457 dtype: torch.dtype,
@@ -2544,8 +2554,7 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
2554 token_indices_to_sample = None
2555 # input_ids can be None for multimodal models.
2556 target_token_ids = self.input_ids.gpu[:num_scheduled_tokens]
2547 - # TODO(woosuk): Support M-RoPE.
2548 - target_positions = self.positions.gpu[:num_scheduled_tokens]
2557 + target_positions = self._get_positions(num_scheduled_tokens)
2558 if self.use_aux_hidden_state_outputs:
2559 assert aux_hidden_states is not None
2560 target_hidden_states = torch.cat(
@@ -2570,8 +2579,7 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
2579 valid_sampled_tokens_count)
2580
2581 target_token_ids = self.input_ids.gpu[token_indices]
2573 - # TODO(woosuk): Support M-RoPE.
2574 - target_positions = self.positions.gpu[token_indices]
2582 + target_positions = self._get_positions(token_indices)
2583 if self.use_aux_hidden_state_outputs:
2584 assert aux_hidden_states is not None
2585 target_hidden_states = torch.cat(