Evo1-1-7B-131K / attention.py
Taykhoom's picture
Initial release
9e26fe9
Raw
History Blame Contribute Delete
12.6 kB
# Copyright (c) Together / Apache 2.0.
#
# Minimal multi-head attention block for the Evo1 HF port.
#
# Replaces flash_attn.modules.mha.MHA with a small, dependency-light
# implementation that:
# - keeps the same parameter names (Wqkv, out_proj, rotary_emb.inv_freq)
# so existing checkpoints load directly,
# - supports attn_implementation in {"eager", "sdpa", "flash_attention_2"},
# - returns attention weights when output_attentions=True (eager path),
# - falls back to eager when output_attentions=True for sdpa/flash backends
# (per the standard HuggingFace dispatch convention),
# - keeps a one-method KV cache compatible with the existing
# InferenceParams dataclass for autoregressive generation.
#
# Math is causal, single-stream (no cross-attention), no ALiBi, no sliding
# window. Evo1 only ever exercised the qkv-packed self-attention path.
from __future__ import annotations
import math
import torch
import torch.nn as nn
import torch.nn.functional as F
from .rotary import RotaryEmbedding
def _flash_attn_required():
try:
from flash_attn import flash_attn_func, flash_attn_varlen_func # noqa: F401
from flash_attn.bert_padding import pad_input, unpad_input # noqa: F401
except ImportError as exc: # pragma: no cover - optional dep
raise ImportError(
"attn_implementation='flash_attention_2' requires the flash-attn "
"package. Install with `pip install flash-attn --no-build-isolation`."
) from exc
def _update_kv_cache(kv: torch.Tensor, inference_params, layer_idx: int) -> torch.Tensor:
"""Append `kv` to inference_params.key_value_memory_dict[layer_idx].
kv: (B, S, 2, H_kv, D) where S is the new-token chunk length (may be 1).
Returns the cumulative kv up to the current sequence position.
"""
num_heads, head_dim = kv.shape[-2:]
if layer_idx not in inference_params.key_value_memory_dict:
kv_cache = torch.empty(
inference_params.max_batch_size,
inference_params.max_seqlen,
2,
num_heads,
head_dim,
dtype=kv.dtype,
device=kv.device,
)
inference_params.key_value_memory_dict[layer_idx] = kv_cache
else:
kv_cache = inference_params.key_value_memory_dict[layer_idx]
batch_start = inference_params.batch_size_offset
batch_end = batch_start + kv.shape[0]
sequence_start = inference_params.seqlen_offset
sequence_end = sequence_start + kv.shape[1]
kv_cache[batch_start:batch_end, sequence_start:sequence_end, ...] = kv
return kv_cache[batch_start:batch_end, :sequence_end, ...]
class MHA(nn.Module):
"""Multi-head self-attention with backend-dispatch.
Constructor signature is a strict subset of flash_attn.modules.mha.MHA so
that the existing AttentionBlock instantiation site is left untouched.
Unsupported kwargs (cross_attn, dwconv, alibi, window_size, ...) are
accepted and ignored or hard-asserted: Evo1 never exercises them.
"""
def __init__(
self,
embed_dim: int,
num_heads: int,
num_heads_kv: int | None = None,
cross_attn: bool = False,
qkv_proj_bias: bool = True,
out_proj_bias: bool = True,
dropout: float = 0.0,
softmax_scale: float | None = None,
causal: bool = False,
layer_idx: int | None = None,
rotary_emb_dim: int = 0,
rotary_emb_base: float = 10000.0,
rotary_emb_scale_base: float | None = None,
rotary_emb_interleaved: bool = False,
use_flash_attn: bool = False, # legacy kwarg, kept for ctor compatibility
attn_implementation: str = "eager",
device=None,
dtype=None,
) -> None:
super().__init__()
if cross_attn:
raise NotImplementedError("Cross-attention is not supported in this minimal MHA.")
factory_kwargs = {"device": device, "dtype": dtype}
self.embed_dim = embed_dim
self.num_heads = num_heads
self.num_heads_kv = num_heads_kv if num_heads_kv is not None else num_heads
if self.embed_dim % num_heads != 0:
raise ValueError("embed_dim must be divisible by num_heads")
if self.num_heads % self.num_heads_kv != 0:
raise ValueError("num_heads must be divisible by num_heads_kv")
self.head_dim = self.embed_dim // num_heads
self.causal = causal
self.softmax_scale = softmax_scale
self.layer_idx = layer_idx
self.rotary_emb_dim = rotary_emb_dim
self.attn_implementation = attn_implementation
self.dropout_p = dropout
if self.rotary_emb_dim > 0:
self.rotary_emb = RotaryEmbedding(
self.rotary_emb_dim,
base=rotary_emb_base,
interleaved=rotary_emb_interleaved,
scale_base=rotary_emb_scale_base,
device=device,
)
qkv_dim = self.head_dim * (self.num_heads + 2 * self.num_heads_kv)
self.Wqkv = nn.Linear(embed_dim, qkv_dim, bias=qkv_proj_bias, **factory_kwargs)
self.out_proj = nn.Linear(embed_dim, embed_dim, bias=out_proj_bias, **factory_kwargs)
def allocate_inference_cache(self, batch_size, max_seqlen, dtype=None):
dtype = self.out_proj.weight.dtype if dtype is None else dtype
device = self.out_proj.weight.device
return torch.empty(
batch_size, max_seqlen, 2, self.num_heads_kv, self.head_dim,
dtype=dtype, device=device,
)
def _project_qkv(self, x: torch.Tensor) -> torch.Tensor:
"""Compute Wqkv(x) and reshape to (B, T, 3, H, D) when MHA, or
return (q, kv) tuple-like layout when GQA. Returns the packed qkv
tensor in either case (kv heads broadcast for SDPA/flash later).
For Evo1 we have num_heads_kv == num_heads (proj_groups=1), so the
common-case packed layout is fine; we keep a GQA branch for future
flexibility but assert MHA at construction time.
"""
qkv = self.Wqkv(x)
if self.num_heads_kv == self.num_heads:
return qkv.view(*qkv.shape[:-1], 3, self.num_heads, self.head_dim)
# GQA path (unused by Evo1):
q = qkv[..., : self.num_heads * self.head_dim]
kv = qkv[..., self.num_heads * self.head_dim:]
q = q.view(*q.shape[:-1], self.num_heads, self.head_dim)
kv = kv.view(*kv.shape[:-1], 2, self.num_heads_kv, self.head_dim)
return q, kv # type: ignore[return-value]
# ------------------------------------------------------------------ eager
def _forward_eager(
self,
qkv: torch.Tensor,
output_attentions: bool,
) -> tuple[torch.Tensor, torch.Tensor | None]:
# qkv: (B, T, 3, H, D). Match flash_attn / sdpa numerical behaviour by
# doing the attention math in fp32 internally (q*scale, QK^T matmul,
# softmax, attn @ V). Without this, the bf16 matmuls accumulate
# ~1e-2 absolute error per attention block and diverge meaningfully
# from flash_attn (which always accumulates in fp32 inside its CUDA
# kernel). Output is cast back to the original dtype for the residual
# add.
orig_dtype = qkv.dtype
q, k, v = qkv.unbind(dim=2)
q = q.permute(0, 2, 1, 3).float() # (B, H, T, D), fp32
k = k.permute(0, 2, 1, 3).float()
v = v.permute(0, 2, 1, 3).float()
scale = self.softmax_scale if self.softmax_scale is not None else 1.0 / math.sqrt(self.head_dim)
scores = torch.matmul(q, k.transpose(-2, -1)) * scale
if self.causal:
T = q.shape[-2]
mask = torch.triu(
torch.ones(T, T, device=scores.device, dtype=torch.bool), diagonal=1
)
scores = scores.masked_fill(mask, float("-inf"))
attn = F.softmax(scores, dim=-1)
if self.training and self.dropout_p > 0:
attn = F.dropout(attn, p=self.dropout_p)
out = torch.matmul(attn, v).permute(0, 2, 1, 3) # (B, T, H, D), fp32
out = out.to(orig_dtype)
return out, (attn.to(orig_dtype) if output_attentions else None)
# -------------------------------------------------------------------- sdpa
def _forward_sdpa(self, qkv: torch.Tensor) -> torch.Tensor:
q, k, v = qkv.unbind(dim=2)
q = q.permute(0, 2, 1, 3) # (B, H, T, D)
k = k.permute(0, 2, 1, 3)
v = v.permute(0, 2, 1, 3)
scale = self.softmax_scale if self.softmax_scale is not None else None
out = F.scaled_dot_product_attention(
q, k, v,
attn_mask=None,
dropout_p=self.dropout_p if self.training else 0.0,
is_causal=self.causal,
scale=scale,
)
return out.permute(0, 2, 1, 3) # (B, T, H, D)
# -------------------------------------------------------- flash_attention_2
def _forward_flash(self, qkv: torch.Tensor) -> torch.Tensor:
_flash_attn_required()
from flash_attn import flash_attn_qkvpacked_func
# flash_attn expects (B, T, 3, H, D) in fp16/bf16 already; Evo1 attn
# blocks already cast to bf16 in __init__.
out = flash_attn_qkvpacked_func(
qkv,
dropout_p=self.dropout_p if self.training else 0.0,
softmax_scale=self.softmax_scale,
causal=self.causal,
)
return out # (B, T, H, D)
# ----------------------------------------------------------- KV-cache path
def _forward_with_cache(
self,
qkv: torch.Tensor,
inference_params,
) -> torch.Tensor:
# qkv: (B, T, 3, H, D). Apply rotary at the current offset, append kv
# to cache, attend over the cumulative kv. For correctness we use SDPA
# which has stable behaviour at all sequence lengths.
if self.rotary_emb_dim > 0:
qkv = self.rotary_emb(
qkv,
seqlen_offset=inference_params.seqlen_offset,
max_seqlen=inference_params.max_seqlen,
)
q, k, v = qkv.unbind(dim=2)
kv = torch.stack((k, v), dim=2) # (B, T, 2, H, D)
kv = _update_kv_cache(kv, inference_params, self.layer_idx)
k_full, v_full = kv.unbind(dim=2) # (B, S_total, H, D)
q = q.permute(0, 2, 1, 3)
k_full = k_full.permute(0, 2, 1, 3)
v_full = v_full.permute(0, 2, 1, 3)
scale = self.softmax_scale if self.softmax_scale is not None else None
is_causal = self.causal and q.shape[-2] == k_full.shape[-2]
out = F.scaled_dot_product_attention(
q, k_full, v_full, is_causal=is_causal, scale=scale,
)
return out.permute(0, 2, 1, 3) # (B, T, H, D)
# ---------------------------------------------------------------- forward
def forward(
self,
x: torch.Tensor,
inference_params=None,
output_attentions: bool = False,
**_unused,
) -> tuple[torch.Tensor, torch.Tensor | None]:
"""Returns (out, attn_weights_or_None) where out is (B, T, embed_dim)."""
if self.num_heads_kv != self.num_heads:
raise NotImplementedError("GQA is not exercised by Evo1; please file an issue if needed.")
qkv = self._project_qkv(x) # (B, T, 3, H, D)
if inference_params is not None:
out_btd = self._forward_with_cache(qkv, inference_params)
attn_weights = None
else:
if self.rotary_emb_dim > 0:
qkv = self.rotary_emb(qkv, seqlen_offset=0, max_seqlen=qkv.shape[1])
backend = self.attn_implementation
if output_attentions and backend != "eager":
# Standard HF behaviour: silently fall back to eager so we can
# actually compute and return the attention matrix.
backend = "eager"
if backend == "eager":
out_btd, attn_weights = self._forward_eager(qkv, output_attentions=output_attentions)
elif backend == "sdpa":
out_btd = self._forward_sdpa(qkv)
attn_weights = None
elif backend == "flash_attention_2":
out_btd = self._forward_flash(qkv)
attn_weights = None
else:
raise ValueError(f"Unknown attn_implementation: {backend!r}")
# (B, T, H, D) -> (B, T, embed_dim)
B, T, H, D = out_btd.shape
out_flat = out_btd.reshape(B, T, H * D)
return self.out_proj(out_flat), attn_weights