DSSD-dev / dssd /heads.py
FlorianVal's picture
DSSD demo from DSSD_demo 4a3ca5b (dssd 73b3f27)
74679d4 verified
Raw History Blame Contribute Delete
5.86 kB
"""Early-exit heads: full-vocabulary heads and pruned heads with a reject class.
A pruned head k predicts over its vocabulary V_k plus one extra output, the
reject class (⊥). Its training target is the full-model distribution collapsed
onto V_k ∪ {⊥}: p̄(v) = p(v) for v ∈ V_k and p̄(⊥) = p(V \\ V_k).
"""
from __future__ import annotations
import torch
import torch.nn as nn
import torch.nn.functional as F
class RMSNorm(nn.Module):
def __init__(self, dim: int, eps: float):
super().__init__()
self.weight = nn.Parameter(torch.ones(dim))
self.eps = eps
def forward(self, x: torch.Tensor) -> torch.Tensor:
dtype = x.dtype
x = x.float()
x = x * torch.rsqrt(x.pow(2).mean(-1, keepdim=True) + self.eps)
return self.weight * x.to(dtype)
class Adapter(nn.Module):
"""Residual low-rank MLP before an early head (Kangaroo/EAGLE-style, no attention,
hence no extra KV cache): h + up(silu(down(norm(h)))), identity at initialisation."""
def __init__(self, hidden_size: int, rank: int, eps: float):
super().__init__()
self.norm = RMSNorm(hidden_size, eps)
self.down = nn.Linear(hidden_size, rank, bias=False)
self.up = nn.Linear(rank, hidden_size, bias=False)
nn.init.zeros_(self.up.weight)
def forward(self, h: torch.Tensor) -> torch.Tensor:
return h + self.up(F.silu(self.down(self.norm(h))))
class ExitHead(nn.Module):
"""Norm + linear projection onto `vocab_ids` (+ optional reject class).
vocab_ids=None means the full vocabulary (output index == token id).
"""
def __init__(self, hidden_size: int, vocab_size: int, eps: float,
vocab_ids: torch.Tensor | None = None, reject: bool = False, adapter_rank: int = 0):
super().__init__()
self.adapter = Adapter(hidden_size, adapter_rank, eps) if adapter_rank > 0 else None
self.norm = RMSNorm(hidden_size, eps)
self.vocab_size = vocab_size
n_out = vocab_size if vocab_ids is None else int(vocab_ids.numel())
self.reject = reject
self.proj = nn.Linear(hidden_size, n_out + int(reject), bias=False)
# The ⊥ logit gets its own scalar bias (initialised to the log of the
# mean out-of-vocabulary mass when pruning).
self.reject_bias = nn.Parameter(torch.zeros(1)) if reject else None
if vocab_ids is None:
self.register_buffer("vocab_ids", None)
else:
self.register_buffer("vocab_ids", vocab_ids.long().clone())
@property
def n_vocab(self) -> int:
return self.proj.out_features - int(self.reject)
def forward(self, h: torch.Tensor) -> torch.Tensor:
"""Logits over V_k (∪ ⊥ as the last column), float32."""
if self.adapter is not None:
h = self.adapter(h)
logits = self.proj(self.norm(h)).float()
if self.reject:
logits = torch.cat([logits[..., :-1], logits[..., -1:] + self.reject_bias.float()], dim=-1)
return logits
def to_token_ids(self, idx: torch.Tensor) -> torch.Tensor:
# gather (not advanced indexing) so that it stays capturable in CUDA/HIP graphs
if self.vocab_ids is None:
return idx
return self.vocab_ids.gather(0, idx.reshape(-1)).reshape(idx.shape)
def scatter_full(self, probs_k: torch.Tensor) -> torch.Tensor:
"""Map probabilities over V_k (no ⊥ column) to the full vocabulary."""
if self.vocab_ids is None:
return probs_k
out = probs_k.new_zeros(*probs_k.shape[:-1], self.vocab_size)
out[..., self.vocab_ids] = probs_k
return out
@classmethod
def from_full(cls, full: "ExitHead", vocab_ids: torch.Tensor, reject: bool = True,
reject_bias: float = 0.0, adapter_rank: int = 0) -> "ExitHead":
"""Prune a full head to vocab_ids, appending a zero-weight ⊥ row (optional adapter)."""
assert full.vocab_ids is None
d = full.proj.in_features
head = cls(d, full.vocab_size, full.norm.eps, vocab_ids, reject, adapter_rank)
with torch.no_grad():
head.norm.weight.copy_(full.norm.weight)
head.proj.weight[: vocab_ids.numel()].copy_(full.proj.weight[vocab_ids])
if reject:
head.proj.weight[-1].zero_()
head.reject_bias.fill_(reject_bias)
return head
def collapse_target(p_full: torch.Tensor, vocab_ids: torch.Tensor) -> torch.Tensor:
"""p̄ over V_k ∪ {⊥} from full-vocabulary probabilities (…, V)."""
kept = p_full[..., vocab_ids]
rest = (1.0 - kept.sum(-1, keepdim=True)).clamp_min(0.0)
return torch.cat([kept, rest], dim=-1)
def head_draft_distribution(logits: torch.Tensor, reject: bool, temperature: float):
"""From head logits return (q over V_k renormalised, reject prob, confidence).
The confidence is the max probability over V_k ∪ {⊥} (⊥ included in the
softmax), so a head that puts its mass on ⊥ is not confident. At T=0 the
returned q is one-hot on the argmax over V_k.
"""
probs = F.softmax(logits, dim=-1)
if reject:
p_rej = probs[..., -1]
conf, arg = probs.max(-1)
is_rej = arg == probs.shape[-1] - 1
conf = torch.where(is_rej, torch.zeros_like(conf), conf)
body_logits = logits[..., :-1]
else:
p_rej = torch.zeros(probs.shape[:-1], device=probs.device)
conf = probs.max(-1).values
body_logits = logits
if temperature == 0.0:
q = F.one_hot(body_logits.argmax(-1), body_logits.shape[-1]).float()
else:
q = F.softmax(body_logits / temperature, dim=-1)
return q, p_rej, conf
def entropy(logits: torch.Tensor) -> torch.Tensor:
logp = F.log_softmax(logits, dim=-1)
return -(logp.exp() * logp).sum(-1)