File size: 12,614 Bytes
9e26fe9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
# 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