d1-3B / hybrid.py
mlabonne's picture EdoardoMosca's picture Aurelien-Lac's picture iamleonie's picture
Initial commit
da1fe36
Raw History Blame Contribute Delete
14.4 kB
"""The pieces every LFM hybrid is made of, and the one shape that makes many
questions cost about one forward.
A model runs a batch of right-padded chains `(B, T)`, or a tree: a trunk of P
tokens (the state) and N branches (the questions) that each continue it, packed
back to back on one axis with no padding, `[trunk | branch 0 | branch 1 | ...]`.
Everything that works token by token (norms, projections, the MLP) runs once
over the packed axis. A convolution continues each branch from the trunk's last
inputs. Attention runs by parts: the trunk causally, every branch token over the
trunk in one flash call that reads the trunk's keys once for all questions
(under sliding attention, one fp32 product over the trunk's last window-1 keys),
each branch over its own tokens in one varlen flash call, the two merged by
their log-sum-exp. Nothing is cached, copied or padded.
"""
from __future__ import annotations
from typing import Callable
import torch
import torch.nn as nn
import torch.nn.functional as F
from transformers.modeling_layers import GradientCheckpointingLayer
from transformers.modeling_outputs import BaseModelOutputWithPast
Tensor = torch.Tensor
class Tree:
"""A trunk of `p` tokens and branches of `lengths` tokens, packed on one axis."""
def __init__(self, p: int, lengths: Tensor):
self.p, self.n = p, len(lengths)
self.cu = F.pad(lengths.cumsum(0), (1, 0)).int() # where each branch starts, past the trunk
self.longest = int(lengths.max())
self.branch = torch.repeat_interleave(torch.arange(self.n, device=lengths.device), lengths)
self.offset = torch.arange(len(self.branch), device=lengths.device) - self.cu[self.branch]
# a branch starts where the trunk ends
self.positions = torch.cat((torch.arange(p, device=lengths.device), p + self.offset))
self.leaves = p + self.cu[1:].long() - 1 # each branch's last token
self._conv = {}
def conv_order(self, keep: int) -> tuple[Tensor, Tensor]:
"""Gather and keep indices that run one causal conv over all branches,
each led by the trunk's last `keep` inputs. Indices are into the whole
input left-padded by `keep` zeros."""
if keep not in self._conv:
p, total, dev = self.p, len(self.branch), self.branch.device
slot = torch.arange(total, device=dev) + keep * (self.branch + 1)
order = torch.empty(total + self.n * keep, dtype=torch.long, device=dev)
order[slot] = keep + p + torch.arange(total, device=dev)
lead = (self.cu[:-1] + keep * torch.arange(self.n, device=dev))[:, None]
order[lead + torch.arange(keep, device=dev)] = p + torch.arange(keep, device=dev)
self._conv[keep] = order, slot - keep
return self._conv[keep]
# --------------------------------------------------------------------------- #
# norms and rotary
# --------------------------------------------------------------------------- #
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: Tensor) -> Tensor:
x32 = x.float()
return (x32 * torch.rsqrt(x32.pow(2).mean(-1, keepdim=True) + self.eps)).type_as(x) * self.weight
def cos_sin(angles: Tensor) -> tuple[Tensor, Tensor]:
angles = torch.cat((angles, angles), dim=-1)
return angles.cos(), angles.sin()
def rotate(x: Tensor, cos: Tensor, sin: Tensor) -> Tensor:
"""Half-split RoPE in fp32; `cos`/`sin` broadcast over the head axis."""
x32 = x.float()
a, b = x32.chunk(2, dim=-1)
return (x32 * cos + torch.cat((-b, a), dim=-1) * sin).type_as(x)
def rope(x: Tensor, pos: Tensor, theta: float, rotated: int) -> Tensor:
"""RoPE over `(B, T, H, D)` at positions `pos` `(T,)`, pairs past `rotated` left as they are."""
dim = x.shape[-1]
inv = 1.0 / theta ** (torch.arange(0, dim, 2, device=x.device).float() / dim)
inv[rotated:] = 0.0
cos, sin = cos_sin(pos.float()[:, None] * inv)
return rotate(x, cos[:, None], sin[:, None])
# --------------------------------------------------------------------------- #
# causal convolution
# --------------------------------------------------------------------------- #
def causal_conv(x: Tensor, weight: Tensor, tree: Tree | None) -> Tensor:
"""Depthwise causal conv over `(B, C, T)`; a tree's branches continue its trunk."""
keep = weight.shape[-1] - 1
padded = F.pad(x, (keep, 0))
if tree is None:
return F.conv1d(padded, weight, groups=x.shape[1])
order, kept = tree.conv_order(keep)
branches = F.conv1d(padded[..., order], weight, groups=x.shape[1])[..., kept]
if tree.p == 0:
return branches
return torch.cat((F.conv1d(padded[..., : keep + tree.p], weight, groups=x.shape[1]), branches), dim=-1)
# --------------------------------------------------------------------------- #
# attention, `(B, H, T, D)`
# --------------------------------------------------------------------------- #
# queries per explicit product off the flash kernels, which bounds its fp32 scores
CHUNK = 1024
def _fused(q: Tensor) -> bool:
"""Whether the flash kernels take these queries: a GPU in half precision, ROCm or CUDA from Ampere on."""
if not q.is_cuda or q.dtype not in (torch.float16, torch.bfloat16):
return False
return bool(torch.version.hip) or torch.cuda.get_device_capability(q.device)[0] >= 8
def _explicit(q: Tensor, k: Tensor, v: Tensor, allowed: Tensor | None, scale: float) -> tuple[Tensor, Tensor]:
"""Attention with its log-sum-exp, in fp32, for small key sets. GQA by broadcast."""
groups = q.shape[1] // k.shape[1]
k, v = k.repeat_interleave(groups, dim=1).float(), v.repeat_interleave(groups, dim=1).float()
scores = (q.float() @ k.transpose(-1, -2)) * scale
if allowed is not None:
scores = scores.masked_fill(~allowed, float("-inf"))
return torch.softmax(scores, dim=-1).nan_to_num() @ v, torch.logsumexp(scores, dim=-1)
def _cat(parts: list[tuple[Tensor, Tensor]]) -> tuple[Tensor, Tensor]:
return torch.cat([o for o, _ in parts], dim=2), torch.cat([lse for _, lse in parts], dim=2)
def _over_trunk(q: Tensor, k: Tensor, v: Tensor, scale: float) -> tuple[Tensor, Tensor]:
"""Every branch token over all of the trunk, with its log-sum-exp. Each KV
head's query group is folded into the query axis, so the trunk's keys are
read once and never repeated."""
batch, heads, length, dim = q.shape
kv = k.shape[1]
if not _fused(q):
return _cat([_explicit(part, k, v, None, scale) for part in q.split(CHUNK, dim=2)])
folded = q.reshape(batch, kv, heads // kv * length, dim)
out, lse = torch.ops.aten._scaled_dot_product_flash_attention(folded, k, v, 0.0, False, False, scale=scale)[:2]
return out.reshape(batch, heads, length, dim).float(), lse.reshape(batch, heads, length)
def _own(q: Tensor, k: Tensor, v: Tensor, tree: Tree, window: int | None, scale: float) -> tuple[Tensor, Tensor]:
"""Each branch over its own tokens, causally (within `window`), with its log-sum-exp."""
if not _fused(q) and len(tree.branch) <= CHUNK: # small enough for one masked product
same, off = tree.branch[:, None] == tree.branch[None], tree.offset
allowed = same & (off[None] <= off[:, None])
if window is not None:
allowed &= off[None] > off[:, None] - window
return _explicit(q, k, v, allowed, scale)
if not _fused(q): # branch by branch: memory grows with the longest question, not with the batch
parts = []
for a, b in zip(tree.cu[:-1].tolist(), tree.cu[1:].tolist()):
off = torch.arange(b - a, device=q.device)
allowed = off[None] <= off[:, None]
if window is not None:
allowed &= off[None] > off[:, None] - window
parts.append(_explicit(q[:, :, a:b], k[:, :, a:b], v[:, :, a:b], allowed, scale))
return _cat(parts)
local = {} if window is None else {"window_size_left": window - 1, "window_size_right": 0}
out, lse = torch.ops.aten._flash_attention_forward(
*(t[0].transpose(0, 1) for t in (q, k, v)), tree.cu, tree.cu, tree.longest, tree.longest,
0.0, True, False, scale=scale, **local)[:2]
return out.transpose(0, 1)[None].float(), lse[None]
def _merge(a: tuple[Tensor, Tensor], b: tuple[Tensor, Tensor]) -> Tensor:
(oa, la), (ob, lb) = a, b
m = torch.maximum(la, lb)
wa, wb = (la - m).exp(), (lb - m).exp()
return (oa * wa[..., None] + ob * wb[..., None]) / (wa + wb)[..., None]
def _windowed(q: Tensor, k: Tensor, v: Tensor, window: int, scale: float) -> Tensor:
"""Causal attention within `window`, as local blocks: each block of `window`
queries sees its own block and the one before it."""
batch, heads, length, dim = q.shape
blocks = -(-length // window)
def split(t: Tensor) -> Tensor: # (B, H, T, D) -> (B, blocks, H, window, D)
t = F.pad(t, (0, 0, 0, blocks * window - length))
return t.reshape(batch, t.shape[1], blocks, window, dim).transpose(1, 2)
def with_previous(t: Tensor) -> Tensor:
return torch.cat((F.pad(t, (0, 0, 0, 0, 0, 0, 1, 0))[:, :-1], t), dim=3)
qb, kb, vb = split(q), with_previous(split(k)), with_previous(split(v))
qpos = torch.arange(window, device=q.device)[:, None] + window
kpos = torch.arange(2 * window, device=q.device)[None, :]
allowed = (kpos <= qpos) & (kpos > qpos - window)
first = allowed & (kpos >= window) # block 0 has no block before it
mask = torch.stack([first] + [allowed] * (blocks - 1))[:, None].repeat(batch, 1, 1, 1)
out = F.scaled_dot_product_attention(qb.flatten(0, 1), kb.flatten(0, 1), vb.flatten(0, 1),
attn_mask=mask, scale=scale, enable_gqa=True)
out = out.reshape(batch, blocks, heads, window, dim).transpose(1, 2)
return out.reshape(batch, heads, blocks * window, dim)[:, :, :length]
def causal_attention(q: Tensor, k: Tensor, v: Tensor, window: int | None, scale: float) -> Tensor:
"""Right-padded chains: causal, within `window` if set."""
if window is None or q.shape[2] <= window:
return F.scaled_dot_product_attention(q, k, v, is_causal=True, scale=scale, enable_gqa=True)
return _windowed(q, k, v, window, scale)
def attention(q: Tensor, k: Tensor, v: Tensor, tree: Tree | None, window: int | None, scale: float) -> Tensor:
"""Chains, or a tree: the trunk causally, each branch over the trunk and over
its own tokens."""
if tree is None:
return causal_attention(q, k, v, window, scale)
p = tree.p
qb = q[:, :, p:]
mine = _own(qb, k[:, :, p:], v[:, :, p:], tree, window, scale)
if p == 0:
return mine[0].to(q.dtype)
trunk = causal_attention(q[:, :, :p], k[:, :, :p], v[:, :, :p], window, scale)
if window is None:
seen = _over_trunk(qb, k[:, :, :p], v[:, :, :p], scale)
else: # only the trunk's last window-1 keys are in reach of a branch
tail = min(p, window - 1)
allowed = torch.arange(p - tail, p, device=q.device)[None] > tree.positions[p:, None] - window
seen = _explicit(qb, k[:, :, p - tail:p], v[:, :, p - tail:p], allowed, scale)
return torch.cat((trunk, _merge(seen, mine).to(q.dtype)), dim=2)
# --------------------------------------------------------------------------- #
# the stack
# --------------------------------------------------------------------------- #
class DecoderLayer(GradientCheckpointingLayer):
"""Norm, operator (`conv` or `self_attn`), residual; norm, MLP, residual.
`sandwich` adds a norm on each branch's output."""
def __init__(self, kind: str, operator: nn.Module, mlp: nn.Module, dim: int, eps: float, sandwich: bool):
super().__init__()
self.operator_name = "conv" if kind == "conv" else "self_attn"
setattr(self, self.operator_name, operator)
self.feed_forward = mlp
self.operator_norm = RMSNorm(dim, eps)
self.ffn_norm = RMSNorm(dim, eps)
self.post_operator_norm = RMSNorm(dim, eps) if sandwich else nn.Identity()
self.post_ffn_norm = RMSNorm(dim, eps) if sandwich else nn.Identity()
def forward(self, x: Tensor, pos: Tensor, tree: Tree | None = None) -> Tensor:
h = x + self.post_operator_norm(getattr(self, self.operator_name)(self.operator_norm(x), pos, tree))
return h + self.post_ffn_norm(self.feed_forward(self.ffn_norm(h)))
class LanguageModel(nn.Module):
"""Embedding, the hybrid stack and the final norm, called like HF's text models.
Rows are right-padded, so the causal operators never let a pad reach a real
token and there is no attention mask to apply.
"""
def __init__(self, cfg, operator: Callable[[str], nn.Module], mlp: Callable[[], nn.Module],
eps: float, sandwich: bool = False):
super().__init__()
self.embed_tokens = nn.Embedding(cfg.vocab_size, cfg.hidden_size)
self.layers = nn.ModuleList(
DecoderLayer(kind, operator(kind), mlp(), cfg.hidden_size, eps, sandwich) for kind in cfg.layer_types)
self.embedding_norm = RMSNorm(cfg.hidden_size, eps) # the final norm, despite its name
def get_input_embeddings(self) -> nn.Embedding:
return self.embed_tokens
def run(self, h: Tensor, tree: Tree | None = None) -> Tensor:
pos = tree.positions if tree is not None else torch.arange(h.shape[1], device=h.device)
for layer in self.layers:
h = layer(h, pos, tree)
return self.embedding_norm(h)
def forward(self, input_ids=None, inputs_embeds=None, **_) -> BaseModelOutputWithPast:
h = self.embed_tokens(input_ids) if inputs_embeds is None else inputs_embeds
return BaseModelOutputWithPast(last_hidden_state=self.run(h))
def answer(self, trunk: Tensor, questions: Tensor, lengths: Tensor) -> Tensor:
"""Final hidden states at the last token of every question, in one pass.
`trunk` `(1, P, d)` embeddings; `questions` the ids of all questions back
to back, `(sum(lengths),)`."""
tree = Tree(trunk.shape[1], lengths)
h = self.run(torch.cat((trunk, self.embed_tokens(questions)[None]), dim=1), tree)
return h[0, tree.leaves]