Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion vllm/model_executor/models/llama_eagle.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from vllm.model_executor.model_loader.weight_utils import default_weight_loader
from vllm.model_executor.models.llama import (LlamaDecoderLayer,
LlamaForCausalLM)
from vllm.v1.spec_decode.eagle_mixin import EagleMixin

from .utils import AutoWeightsLoader, maybe_prefix

Expand Down Expand Up @@ -133,7 +134,7 @@ def load_weights(self, weights: Iterable[tuple[str,
return loaded_params


class EagleLlamaForCausalLM(LlamaForCausalLM):
class EagleLlamaForCausalLM(EagleMixin, LlamaForCausalLM):

def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
nn.Module.__init__(self)
Expand Down
23 changes: 2 additions & 21 deletions vllm/model_executor/models/llama_eagle3.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from vllm.model_executor.model_loader.weight_utils import default_weight_loader
from vllm.model_executor.models.llama import (LlamaDecoderLayer,
LlamaForCausalLM)
from vllm.v1.spec_decode.eagle_mixin import Eagle3Mixin

from .utils import AutoWeightsLoader, maybe_prefix

Expand Down Expand Up @@ -207,7 +208,7 @@ def load_weights(self, weights: Iterable[tuple[str,
return loaded_params


class Eagle3LlamaForCausalLM(LlamaForCausalLM):
class Eagle3LlamaForCausalLM(Eagle3Mixin, LlamaForCausalLM):

def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
nn.Module.__init__(self)
Expand Down Expand Up @@ -254,26 +255,6 @@ def forward(
) -> tuple[torch.Tensor, torch.Tensor]:
return self.model(input_ids, positions, hidden_states, inputs_embeds)

def compute_logits(
self,
hidden_states: torch.Tensor,
) -> Optional[torch.Tensor]:
logits = self.logits_processor(self.lm_head, hidden_states)
if self.draft_id_to_target_id is None:
assert logits.shape[1] == self.config.vocab_size, \
"Expected logits to have shape " \
f"(*, {self.config.vocab_size}), but got {logits.shape}"
return logits

base = torch.arange(self.config.draft_vocab_size, device=logits.device)
targets = base + self.draft_id_to_target_id
logits_new = logits.new_full((
logits.shape[0],
self.config.vocab_size,
), float('-inf'))
logits_new[:, targets] = logits
return logits_new

def combine_hidden_states(
self,
hidden_states: torch.Tensor,
Expand Down
10 changes: 5 additions & 5 deletions vllm/v1/spec_decode/eagle.py
Original file line number Diff line number Diff line change
Expand Up @@ -255,11 +255,10 @@ def propose(
else:
last_hidden_states, hidden_states = ret_hidden_states
sample_hidden_states = last_hidden_states[last_token_indices]
logits = self.model.compute_logits(sample_hidden_states)

# Early exit if there is only one draft token to be generated.
if self.num_speculative_tokens == 1:
draft_token_ids = logits.argmax(dim=-1)
draft_token_ids = self.model.propose_chain(sample_hidden_states)
return draft_token_ids.view(-1, 1)

if self.uses_mrope:
Expand All @@ -273,6 +272,7 @@ def propose(

if isinstance(attn_metadata, TreeAttentionMetadata):
# Draft using tree attention.
logits = self.model.compute_logits(sample_hidden_states, None)
Comment thread
ekagra-ranjan marked this conversation as resolved.
Resolved
draft_token_ids_list = self.propose_tree(
batch_size=batch_size,
logits=logits,
Expand All @@ -283,7 +283,7 @@ def propose(
# [batch_size, num_tree_tokens]
return torch.cat(draft_token_ids_list, dim=1)

draft_token_ids = logits.argmax(dim=-1)
draft_token_ids = self.model.propose_chain(sample_hidden_states)

if self.allowed_attn_types is not None and \
not isinstance(attn_metadata, self.allowed_attn_types):
Expand Down Expand Up @@ -406,8 +406,8 @@ def propose(
else:
last_hidden_states, hidden_states = ret_hidden_states
hidden_states = hidden_states[:batch_size]
logits = self.model.compute_logits(last_hidden_states[:batch_size])
draft_token_ids = logits.argmax(dim=-1)
draft_token_ids = self.model.propose_chain(
last_hidden_states[:batch_size])
draft_token_ids_list.append(draft_token_ids)

# [batch_size, num_speculative_tokens]
Expand Down
45 changes: 45 additions & 0 deletions vllm/v1/spec_decode/eagle_mixin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import torch
from typing import Optional

class EagleMixin:
def propose_chain(self, hidden_states: torch.Tensor) -> torch.Tensor:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

propose_chain feels like a poor name for this function, as it conflicts with EagleProposer.propose(). How about something closer to "get_draft_logits" or similar?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or "greedy_sample_draft_tokens" might be more appropriate since I now see that this is a shortcut for compute_logits + greedy sampling

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a method called propose_tree so propose_chain is analogious to it. The propose_chain and propose_tree are called from within EagleProposer.propose(). The purpose of this is to get the chain draft so I am not sure get_draft_logits is a better name

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In what way is propose_chain here analogous to propose_tree? The latter seems like a much more comprehensive propose()-style function, whereas propose_chain here seems like a much simpler combination of compute_logits and greedy sampling?

@ekagra-ranjan ekagra-ranjan Sep 30, 2025 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In what way is propose_chain here analogous to propose_tree?

propose_tree() is called inside propose() and returns draft token ids at the current step
propose_chain() is called inside propose() and returns draft token ids at the current step

The functionality/role of propose_chain() is analogous to propose_tree(), i.e. its called by propose() and returns the draft tokens ids sampled at the current step.

For E1, its compute logits and argmax whereas in E3 it has an extra step of offset computation. This different behavior of E1 and E3 is abstracted away from propose() function which will call propose_chain().

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see what you mean. I think I would still prefer something like sample_chain or propose_sample_greedy or similar, but I am okay to go with propose_chain if you think it is the best choice.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alright, I have renamed it to sample_chain following your suggestion.

logits = self.compute_logits(hidden_states)
draft_token_ids = logits.argmax(dim=-1)
return draft_token_ids

class Eagle3Mixin:
def compute_draft_logits(
self,
hidden_states: torch.Tensor,
) -> Optional[torch.Tensor]:
# Computes draft logits from hidden states.
logits = self.logits_processor(self.lm_head, hidden_states)
return logits

def compute_logits(
self,
hidden_states: torch.Tensor,
) -> Optional[torch.Tensor]:
# Maps draft logits to target logits.
logits = self.compute_draft_logits(hidden_states)
if self.draft_id_to_target_id is None:
assert logits.shape[1] == self.config.vocab_size, \
"Expected logits to have shape " \
f"(*, {self.config.vocab_size}), but got {logits.shape}"
return logits

base = torch.arange(self.config.draft_vocab_size, device=logits.device)
targets = base + self.draft_id_to_target_id
logits_new = logits.new_full((
logits.shape[0],
self.config.vocab_size,
), float('-inf'))
logits_new[:, targets] = logits
return logits_new

def propose_chain(self, hidden_states: torch.Tensor) -> torch.Tensor:
logits = self.compute_draft_logits(hidden_states)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is supposed to call compute_logits? Otherwise I don't see where Eagle3Mixin.compute_logits would ever get called here.

Suggested change
logits = self.compute_draft_logits(hidden_states)
logits = self.compute_logits(hidden_states)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

compute_draft_logits() is the optimized version of compute_logits() and is used in chain sampling. The original compute_logits() is still called by the Tree sampling here in eagle.py

draft_token_ids = logits.argmax(dim=-1)
offset = self.draft_id_to_target_id[draft_token_ids]
Comment thread
ekagra-ranjan marked this conversation as resolved.
Resolved
draft_token_ids += offset
return draft_token_ids
Comment thread
ekagra-ranjan marked this conversation as resolved.
Resolved