Files
wehub-resource-sync 59a0a3844c
PR Test AMD / cancel-on-close (push) Has been skipped
PR Test NVIDIA ARM / scan (push) Has been skipped
PR Test NVIDIA / cancel-on-close (push) Has been skipped
PR Test AMD / scan (push) Has been skipped
PR Test NVIDIA ARM / cancel-on-close (push) Has been skipped
PR Test NVIDIA / scan (push) Has been skipped
Release Docker Images / build (cu129-torch-2.11.0) (push) Has been skipped
Release Docker Images / build (cu130-torch-2.11.0) (push) Has been skipped
Release PyPI / publish (push) Has been skipped
Scheduler Python Test / test (push) Successful in 27m19s
Docs / build (push) Successful in 28m8s
Scheduler C++ Test / test (push) Successful in 28m19s
Scheduler C++ Test / test-flat (push) Successful in 28m18s
Docs / deploy (push) Has been cancelled
PR Test AMD / finish (push) Has been cancelled
PR Test NVIDIA / finish (push) Has been cancelled
PR Test NVIDIA ARM / finish (push) Has been cancelled
PR Test NVIDIA ARM / ${{ matrix.name }} (${{ matrix.runner }}) (push) Has been cancelled
PR Test AMD / ${{ matrix.name }} (${{ matrix.runner }}) (push) Has been cancelled
PR Test NVIDIA / ${{ matrix.name }} (${{ matrix.runner }}) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:32:31 +08:00

661 lines
23 KiB
Python

# Copyright (c) 2026 LightSeek Foundation
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
"""LLaMA Eagle3 draft model for speculative decoding.
Extends base classes. Preserves the low-latency fused allreduce+norm
path from the original implementation.
"""
from __future__ import annotations
from collections.abc import Iterable
import torch
from torch import nn
from transformers import LlamaConfig
from tokenspeed.runtime.distributed.mapping import Mapping
from tokenspeed.runtime.execution.context import (
ForwardContext,
report_collective_sizing,
)
from tokenspeed.runtime.execution.forward_batch_info import ForwardMode
from tokenspeed.runtime.layers.activation import SiluAndMul
from tokenspeed.runtime.layers.common import concat
from tokenspeed.runtime.layers.layernorm import RMSNorm
from tokenspeed.runtime.layers.linear import (
ColumnParallelLinear,
MergedColumnParallelLinear,
RowParallelLinear,
)
from tokenspeed.runtime.layers.logits_processor import LogitsProcessor
from tokenspeed.runtime.layers.quantization.base_config import QuantizationConfig
from tokenspeed.runtime.layers.vocab_parallel_embedding import ParallelLMHead
from tokenspeed.runtime.model_loader.weight_utils import default_weight_loader
from tokenspeed.runtime.models.base import (
BaseCausalLM,
BaseDecoderLayer,
BaseTransformerModel,
)
from tokenspeed.runtime.models.llama import LlamaAttention as BaseLlamaAttention
from tokenspeed.runtime.utils import add_prefix, get_colorful_logger
logger = get_colorful_logger(__name__)
# ---------------------------------------------------------------------------
# Attention
# ---------------------------------------------------------------------------
class LlamaAttention(BaseLlamaAttention):
"""Eagle3 draft head attention.
Inherits ``__init__`` (with ``qkv_input_size=2*hidden_size`` for the
[embed || hidden] concat) and ``forward`` (= qkv_proj + o_proj scaffolding)
from base. Overrides ``_attn`` so the draft's first step skips dead
catch-up rows: on backends that support fused KV pre-write, q is sliced
to one live row per request and dispatched as DECODE; otherwise the
fallback runs the full N-row attn and post-slices the output. Inactive
draft steps delegate to base.
"""
def _attn(
self,
positions: torch.Tensor,
q: torch.Tensor,
k: torch.Tensor,
v: torch.Tensor,
ctx: ForwardContext,
out_cache_loc: torch.Tensor,
) -> torch.Tensor:
# Active draft first step (drafter set up gather_ids + accept_lengths).
# Covers both decode catch-up and prefill catch-up; multi-step decode
# delegates to base.
if ctx.accept_lengths is None:
return super()._attn(positions, q, k, v, ctx, out_cache_loc)
if ctx.attn_backend.support_kv_cache_prewrite(ctx.forward_mode):
fused_kv_arg = self._build_fused_kv_arg(v, ctx, out_cache_loc)
if fused_kv_arg is not None:
# Trim only on the sliced single-token decode path; the
# post-slice fallback below still runs full N-row attn and
# needs the original seq_lens.
self._apply_correction(ctx)
q_rope = self._fused_rope_kv_write(
positions, q, k, fused_kv_arg
).index_select(0, ctx.gather_ids)
# record_kv_cache (keyed off the real mode) forces the backend's
# PD layerwise cache-step record that the DECODE dispatch would
# otherwise skip on an EXTEND/MIXED catch-up.
return ctx.attn_backend.forward(
q_rope,
None,
None,
self.attn,
out_cache_loc,
ctx.token_to_kv_pool,
ForwardMode.DECODE,
ctx.bs,
save_kv_cache=False,
record_kv_cache=not ctx.forward_mode.is_decode_or_idle(),
)
q, k = self.rotary_emb(positions, q, k)
return self.attn(q, k, v, ctx=ctx, out_cache_loc=out_cache_loc).index_select(
0, ctx.gather_ids
)
def _apply_correction(self, ctx: ForwardContext) -> None:
"""Trim decode rows' cache_seqlens by ``spec_num_tokens - accept_lengths``."""
seq_lens_buf = ctx.draft_seq_lens_buf
if seq_lens_buf is None or ctx.accept_lengths is None:
return
num_extends = ctx.num_extends
if num_extends >= ctx.bs:
return
correction = (
ctx.attn_backend.spec_num_tokens - ctx.accept_lengths[num_extends:]
).to(seq_lens_buf.dtype)
seq_lens_buf[num_extends : ctx.bs].sub_(correction).clamp_(min=1)
# ---------------------------------------------------------------------------
# MLP
# ---------------------------------------------------------------------------
class LlamaMLP(nn.Module):
def __init__(
self,
hidden_size: int,
intermediate_size: int,
hidden_act: str,
mapping: Mapping,
quant_config: QuantizationConfig | None = None,
prefix: str = "",
) -> None:
super().__init__()
tp_rank = mapping.dense.tp_rank
tp_size = mapping.dense.tp_size
tp_group = mapping.dense.tp_group
self.gate_up_proj = MergedColumnParallelLinear(
hidden_size,
[intermediate_size] * 2,
bias=False,
quant_config=quant_config,
tp_size=tp_size,
tp_rank=tp_rank,
tp_group=tp_group,
prefix=add_prefix("gate_up_proj", prefix),
)
self.down_proj = RowParallelLinear(
intermediate_size,
hidden_size,
bias=False,
quant_config=quant_config,
reduce_results=False,
tp_rank=tp_rank,
tp_size=tp_size,
tp_group=tp_group,
prefix=add_prefix("down_proj", prefix),
)
self.act_fn = SiluAndMul()
self.gateup_unquanted = quant_config is None
def forward(self, x, block_scale=None):
if x.shape[0] == 0:
return x
gate_up, _ = self.gate_up_proj(x)
x = self.act_fn(gate_up)
x, _ = self.down_proj(x)
return x
# ---------------------------------------------------------------------------
# Decoder layer
# ---------------------------------------------------------------------------
class Eagle3DecoderLayer(BaseDecoderLayer):
"""Eagle3 decoder layer with low-latency fused allreduce+norm path.
Inherits norm/attn/mlp/comm_manager init from BaseDecoderLayer.
Overrides forward with eagle3-specific embed+hidden concat logic.
"""
def __init__(
self,
config: LlamaConfig,
layer_id: int,
mapping: Mapping,
quant_config: QuantizationConfig | None = None,
prefix: str = "",
) -> None:
self._eagle3_config = config
self._eagle3_mapping = mapping
self._eagle3_quant_config = quant_config
self._eagle3_prefix = prefix
super().__init__(
config=config,
layer_id=layer_id,
mapping=mapping,
quant_config=quant_config,
prefix=prefix,
)
self.hidden_norm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
def resolve_attn(self, prefix: str) -> nn.Module:
config = self._eagle3_config
return LlamaAttention(
config,
self._eagle3_mapping,
hidden_size=config.hidden_size,
num_heads=config.num_attention_heads,
num_kv_heads=config.num_key_value_heads,
layer_id=self.layer_id,
quant_config=self._eagle3_quant_config,
prefix=add_prefix("self_attn", prefix),
qkv_input_size=2 * config.hidden_size,
)
def resolve_mlp(self, prefix: str) -> nn.Module:
config = self._eagle3_config
inter_size = (
config.intermediate_size_mlp
if config.model_type == "llama4_text"
else config.intermediate_size
)
return LlamaMLP(
config.hidden_size,
inter_size,
config.hidden_act,
self._eagle3_mapping,
self._eagle3_quant_config,
prefix=f"{prefix}.mlp",
)
def _maybe_narrow_residual(
self,
residual: torch.Tensor,
ctx: ForwardContext,
) -> torch.Tensor:
"""Align residual with attn output narrowed to [bs, H]."""
if ctx.accept_lengths is not None and not ctx.forward_mode.is_idle():
return residual.index_select(0, ctx.gather_ids)
return residual
def forward_low_latency(
self,
positions: torch.Tensor,
embeds: torch.Tensor,
hidden_states: torch.Tensor,
ctx: ForwardContext,
out_cache_loc: torch.Tensor,
residual: torch.Tensor | None,
final_norm: RMSNorm = None,
fuse_embed_reduce: bool = False,
) -> tuple[torch.Tensor, torch.Tensor]:
residual = hidden_states
if fuse_embed_reduce:
# Fuse embedding allreduce with input_layernorm.
embeds, _, *_ = self.input_layernorm.forward_with_allreduce_fusion(
self.mapping.attn.tp_rank,
self.mapping.attn.tp_group,
embeds,
torch.zeros_like(embeds),
)
else:
embeds = self.input_layernorm(embeds)
hidden_states = self.hidden_norm(hidden_states)
hidden_states = concat(embeds, hidden_states)
# Attention
hidden_states = self.comm_manager.pre_attn_comm(hidden_states, ctx)
hidden_states = self.self_attn(
positions=positions,
hidden_states=hidden_states,
ctx=ctx,
out_cache_loc=out_cache_loc,
)
residual = self._maybe_narrow_residual(residual, ctx)
# Fused post-attn allreduce + norm (uses attn tp group)
block_scale = None
hidden_states, residual, block_scale, *_ = (
self.post_attention_layernorm.forward_with_allreduce_fusion(
self.mapping.attn.tp_rank,
self.mapping.attn.tp_group,
hidden_states,
residual,
fuse_block_quant_fp8=not self.mlp.gateup_unquanted,
)
)
hidden_states = self.mlp(hidden_states, block_scale)
# Fused final allreduce + norm (uses dense tp group)
hidden_states, residual, *_ = final_norm.forward_with_allreduce_fusion(
self.mapping.dense.tp_rank,
self.mapping.dense.tp_group,
hidden_states,
residual,
fuse_block_quant_fp8=False,
)
return hidden_states, residual
def forward(
self,
positions: torch.Tensor,
embeds: torch.Tensor,
hidden_states: torch.Tensor,
ctx: ForwardContext,
out_cache_loc: torch.Tensor,
residual: torch.Tensor | None,
final_norm: RMSNorm = None,
fuse_embed_reduce: bool = False,
) -> tuple[torch.Tensor, torch.Tensor]:
if self.comm_manager.should_fuse(hidden_states.shape[0]):
return self.forward_low_latency(
positions,
embeds,
hidden_states,
ctx,
out_cache_loc,
residual,
final_norm,
fuse_embed_reduce=fuse_embed_reduce,
)
# Non-fused path: fuse_embed_reduce is always False here because
# the model only sets it when should_fuse() is True.
residual = hidden_states
embeds = self.input_layernorm(embeds)
hidden_states = self.hidden_norm(hidden_states)
hidden_states = torch.cat([embeds, hidden_states], dim=-1)
# Attention
hidden_states = self.comm_manager.pre_attn_comm(hidden_states, ctx)
hidden_states = self.self_attn(
positions=positions,
hidden_states=hidden_states,
ctx=ctx,
out_cache_loc=out_cache_loc,
)
residual = self._maybe_narrow_residual(residual, ctx)
hidden_states, residual = self.comm_manager.post_attn_comm(
hidden_states, residual, ctx
)
hidden_states, residual = self.post_attention_layernorm(hidden_states, residual)
# MLP
hidden_states = self.comm_manager.pre_mlp_comm(hidden_states, ctx)
hidden_states = self.mlp(hidden_states)
hidden_states, residual = self.comm_manager.post_mlp_comm(
hidden_states, residual, ctx
)
return hidden_states, residual
# ---------------------------------------------------------------------------
# Model and CausalLM
# ---------------------------------------------------------------------------
class Eagle3LlamaModel(BaseTransformerModel):
layer_cls = Eagle3DecoderLayer
def __init__(
self,
config: LlamaConfig,
mapping: Mapping,
quant_config: QuantizationConfig | None = None,
prefix: str = "",
) -> None:
super().__init__(
config=config,
mapping=mapping,
quant_config=quant_config,
prefix=prefix,
)
# Eagle3 uses "midlayer" (not "layers.0") in checkpoint weights.
# Re-register the single layer under the correct name.
self.midlayer = self.layers[0]
del self.layers
self.num_fc_input_dim = (
len(config.eagle_aux_hidden_state_layer_ids)
if hasattr(config, "eagle_aux_hidden_state_layer_ids")
else 3
)
self.fc = ColumnParallelLinear(
config.hidden_size * self.num_fc_input_dim,
config.hidden_size,
bias=False,
gather_output=True,
quant_config=quant_config,
prefix=add_prefix("fc", prefix),
tp_rank=self.mapping.attn.tp_rank,
tp_size=self.mapping.attn.tp_size,
tp_group=self.mapping.attn.tp_group,
)
# norm_before_fc: RMSNorm over the concatenated aux states before fc (replicated)
self.input_norm = (
RMSNorm(
config.hidden_size * self.num_fc_input_dim,
eps=config.rms_norm_eps,
)
if getattr(config, "norm_before_fc", False)
else None
)
def forward(
self,
input_ids: torch.Tensor,
positions: torch.Tensor,
ctx: ForwardContext,
out_cache_loc: torch.Tensor,
input_embeds: torch.Tensor = None,
hidden_states: torch.Tensor = None,
) -> torch.Tensor:
if input_embeds is None:
# When TP > 1 and fused allreduce+norm is available, skip the
# NCCL allreduce in the embedding and let the midlayer fuse it
# with the input_layernorm.
midlayer = self.midlayer
num_tokens = input_ids.shape[0]
fuse_embed_reduce = (
self.mapping.attn.tp_size > 1
and midlayer.comm_manager.should_fuse(num_tokens)
)
embeds = self.embed_tokens(input_ids, reduce_results=not fuse_embed_reduce)
else:
embeds = input_embeds
fuse_embed_reduce = False
if hidden_states is None:
raise ValueError("Eagle3 forward requires hidden_states")
if hidden_states.size(-1) != embeds.size(-1):
if self.input_norm is not None:
hidden_states = self.input_norm(hidden_states)
hidden_states, _ = self.fc(hidden_states)
residual = None
midlayer = self.midlayer
hidden_states, residual = midlayer(
positions,
embeds,
hidden_states,
ctx,
out_cache_loc,
residual,
self.norm,
fuse_embed_reduce=fuse_embed_reduce,
)
# Decide on pre-slice token count so this matches the path midlayer
# actually took; under draft reduce, hidden_states.shape[0] shrinks.
if midlayer.comm_manager.should_fuse(input_ids.shape[0]):
hidden_states_to_logits, hidden_states_to_aux = hidden_states, residual
else:
hidden_states_to_logits, hidden_states_to_aux = self.norm(
hidden_states, residual
)
hidden_states_to_logits, _ = midlayer.comm_manager.post_final_norm_comm(
hidden_states_to_logits, None, ctx
)
hidden_states_to_aux, _ = midlayer.comm_manager.post_final_norm_comm(
hidden_states_to_aux, None, ctx
)
return hidden_states_to_logits, [hidden_states_to_aux]
class LlamaForCausalLMEagle3(BaseCausalLM):
model_cls = Eagle3LlamaModel
def __init__(
self,
config: LlamaConfig,
mapping: Mapping,
quant_config: QuantizationConfig | None = None,
prefix: str = "",
) -> None:
nn.Module.__init__(self)
self.config = config
self.mapping = mapping
self.quant_config = quant_config
if self.config.num_hidden_layers != 1:
raise ValueError("EAGLE3 currently only supports 1 layer")
self.model = self.resolve_model(config, mapping, quant_config, prefix)
self.load_lm_head_from_target = False
if self.config.tie_word_embeddings:
self.lm_head = self.model.embed_tokens
else:
if getattr(config, "draft_vocab_size", None) is None:
self.load_lm_head_from_target = True
self.lm_head = ParallelLMHead(
getattr(config, "draft_vocab_size", None) or config.vocab_size,
config.hidden_size,
quant_config=quant_config,
tp_rank=mapping.attn.tp_rank,
tp_size=mapping.attn.tp_size,
tp_group=mapping.attn.tp_group,
prefix=add_prefix("lm_head", prefix),
)
self.logits_processor = LogitsProcessor(
config,
skip_all_gather=self.mapping.attn.has_dp,
do_argmax=True,
tp_rank=self.mapping.attn.tp_rank,
tp_size=self.mapping.attn.tp_size,
tp_group=self.mapping.attn.tp_group,
)
self.capture_aux_hidden_states = True
self.hot_token_id = None
def forward(
self,
ctx: ForwardContext,
input_ids: torch.Tensor,
positions: torch.Tensor,
out_cache_loc: torch.Tensor,
**kwargs,
) -> torch.Tensor:
with report_collective_sizing(ctx, ctx.bs, ctx.global_bs):
return super().forward(ctx, input_ids, positions, out_cache_loc, **kwargs)
def prepare_model_kwargs(
self, ctx: ForwardContext, input_ids: torch.Tensor, kwargs: dict
) -> dict:
model_kwargs = super().prepare_model_kwargs(ctx, input_ids, kwargs)
captured_hidden_states = kwargs.get("captured_hidden_states")
if captured_hidden_states is not None:
model_kwargs["hidden_states"] = captured_hidden_states
else:
# During CUDA graph capture warmup, provide dummy hidden states.
num_tokens = input_ids.shape[0]
hidden_size = self.config.hidden_size
num_fc = self.model.num_fc_input_dim
model_kwargs["hidden_states"] = torch.zeros(
num_tokens,
hidden_size * num_fc,
dtype=torch.bfloat16,
device=input_ids.device,
)
return model_kwargs
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> None:
params_dict = dict(self.named_parameters())
stacked_params_mapping = [
(".qkv_proj", ".q_proj", "q"),
(".qkv_proj", ".k_proj", "k"),
(".qkv_proj", ".v_proj", "v"),
(".gate_up_proj", ".gate_proj", 0),
(".gate_up_proj", ".up_proj", 1),
]
for name, loaded_weight in weights:
# some Eagle3 checkpoints name the block "layers.0" not "midlayer"
if name.startswith("layers.0."):
name = "midlayer." + name[len("layers.0.") :]
if "d2t" in name:
self.hot_token_id = loaded_weight + torch.arange(loaded_weight.shape[0])
continue
if "t2d" in name:
continue
for param_name, weight_name, shard_id in stacked_params_mapping:
if weight_name not in name:
continue
name = name.replace(weight_name, param_name)
param_name = f"model.{name}" if name not in params_dict else name
if param_name in params_dict:
param = params_dict[param_name]
weight_loader = getattr(
param, "weight_loader", default_weight_loader
)
weight_loader(param, loaded_weight, shard_id)
break
else:
param_name = name if name in params_dict else f"model.{name}"
if param_name in params_dict:
param = params_dict[param_name]
weight_loader = getattr(
param, "weight_loader", default_weight_loader
)
weight_loader(param, loaded_weight)
def get_hot_token_id(self):
return self.hot_token_id
def get_embed(self):
return self.model.embed_tokens.weight
def set_embed_and_head(self, embed, head):
# If draft hidden size != target hidden size, embed cannot be shared
if (
hasattr(self.config, "target_hidden_size")
and self.config.target_hidden_size != self.config.hidden_size
):
return
del self.model.embed_tokens.weight
self.model.embed_tokens.weight = embed
if head is not None and self.load_lm_head_from_target:
del self.lm_head.weight
self.lm_head.weight = head
torch.cuda.empty_cache()
torch.cuda.synchronize()
EntryClass = [LlamaForCausalLMEagle3]