File size: 10,054 Bytes
5e6d9f5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""模型主体:Transformer Block、MTP 多 token 预测头、完整模型。"""

import math
from typing import Optional, Tuple, List

import torch
import torch.nn as nn
import torch.nn.functional as F

from .layers import RMSNorm, build_rope_cache
from .mla import MLA
from .moe import MoE, DenseFFN


def _shift_left(x: torch.Tensor, k: int, fill: int):
    """y[:, i] = x[:, i+k],越界位置填 fill。"""
    if k == 0:
        return x.clamp_min(0) if fill == 0 else x
    y = torch.full_like(x, fill)
    if k < x.size(1):
        y[:, :-k] = x[:, k:]
    if fill == 0:
        y = y.clamp_min(0)
    return y


class Block(nn.Module):
    """Pre-Norm 残差块:x + MLA(norm(x)) → x + FFN(norm(x))。"""

    def __init__(self, cfg, layer_id: int):
        super().__init__()
        self.attn_norm = RMSNorm(cfg.dim, cfg.norm_eps)
        self.attn = MLA(cfg)
        self.ffn_norm = RMSNorm(cfg.dim, cfg.norm_eps)
        # V3 的做法:靠前的层用稠密 FFN,后面的层才换成 MoE,训练早期更稳
        self.is_moe = layer_id >= cfg.n_dense_layers
        self.ffn = MoE(cfg) if self.is_moe else DenseFFN(cfg)

    def forward(self, x, cos, sin, start_pos=0, mask=None):
        x = x + self.attn(self.attn_norm(x), cos, sin, start_pos, mask)
        h, aux = self.ffn(self.ffn_norm(x))
        return x + h, aux


class MTPModule(nn.Module):
    """Multi-Token Prediction 模块(DeepSeek-V3)。

    普通语言模型每个位置只预测下一个 token,训练信号比较稀疏。
    MTP 再挂一个轻量模块,让位置 i 同时预测 i+2:

        h'_i = Block( W · [ RMSNorm(h_i) ; RMSNorm(Emb(t_{i+1})) ] )
        p(t_{i+2}) = Head(RMSNorm(h'_i))

    嵌入层和输出头与主干共享,所以额外开销只有一层 Block。
    好处有二:训练时数据效率更高(模型被迫规划更远);
    推理时这个头天然就是个 draft model,可以做自投机解码,实测约 1.8x 加速。
    """

    def __init__(self, cfg, layer_id: int):
        super().__init__()
        self.h_norm = RMSNorm(cfg.dim, cfg.norm_eps)
        self.e_norm = RMSNorm(cfg.dim, cfg.norm_eps)
        self.proj = nn.Linear(2 * cfg.dim, cfg.dim, bias=False)
        self.block = Block(cfg, layer_id)

    def forward(self, h, emb, cos, sin, start_pos=0, mask=None):
        z = self.proj(torch.cat([self.h_norm(h), self.e_norm(emb)], dim=-1))
        return self.block(z, cos, sin, start_pos, mask)


class DeepSeekFlashMini(nn.Module):
    def __init__(self, cfg):
        super().__init__()
        self.cfg = cfg
        self.embed = nn.Embedding(cfg.vocab_size, cfg.dim)
        self.layers = nn.ModuleList([Block(cfg, i) for i in range(cfg.n_layers)])
        self.norm = RMSNorm(cfg.dim, cfg.norm_eps)
        self.head = nn.Linear(cfg.dim, cfg.vocab_size, bias=False)
        if cfg.tie_embeddings:
            self.head.weight = self.embed.weight

        self.mtp = nn.ModuleList([MTPModule(cfg, cfg.n_layers + i) for i in range(cfg.n_mtp)])
        self.mtp_norm = RMSNorm(cfg.dim, cfg.norm_eps) if cfg.n_mtp > 0 else None

        cos, sin = build_rope_cache(cfg.qk_rope_head_dim, cfg.max_seq_len,
                                    cfg.rope_theta, cfg.rope_scaling)
        self.register_buffer("rope_cos", cos, persistent=False)
        self.register_buffer("rope_sin", sin, persistent=False)

        self.apply(self._init_weights)
        # 残差分支的输出投影按深度缩放初始化,防止深层激活方差爆掉
        for name, p in self.named_parameters():
            if name.endswith("wo.weight") or name.endswith("w_down.weight"):
                nn.init.normal_(p, mean=0.0, std=0.02 / math.sqrt(2 * cfg.n_layers))

        self._cache_ready = False

    @staticmethod
    def _init_weights(m):
        if isinstance(m, nn.Linear):
            nn.init.normal_(m.weight, mean=0.0, std=0.02)
            if m.bias is not None:
                nn.init.zeros_(m.bias)
        elif isinstance(m, nn.Embedding):
            nn.init.normal_(m.weight, mean=0.0, std=0.02)

    # ================= 前向 =================
    def _rope(self, start_pos: int, T: int):
        if start_pos + T > self.cfg.max_seq_len:
            raise ValueError(
                f"位置 {start_pos + T} 超出上下文上限 max_seq_len={self.cfg.max_seq_len}。"
                f"请加大配置里的 max_seq_len(并考虑设置 rope_scaling 做外推)。")
        return self.rope_cos[start_pos:start_pos + T], self.rope_sin[start_pos:start_pos + T]

    def _mask(self, T: int, start_pos: int, device):
        """带 cache 的增量解码时,为多 token 输入构造对齐的因果 mask。"""
        if T == 1:
            return None
        if not self._cache_ready:
            return None            # 训练路径:交给 SDPA 的 is_causal
        S = start_pos + T
        return torch.ones(T, S, dtype=torch.bool, device=device).tril(S - T)

    def forward_trunk(self, tokens: torch.Tensor, start_pos: int = 0):
        """主干前向,返回 (最后一层隐状态 h, logits, 路由辅助损失)。"""
        B, T = tokens.shape
        cos, sin = self._rope(start_pos, T)
        mask = self._mask(T, start_pos, tokens.device)

        h = self.embed(tokens)
        aux_total = h.new_zeros(())
        for layer in self.layers:
            h, aux = layer(h, cos, sin, start_pos, mask)
            aux_total = aux_total + aux
        logits = self.head(self.norm(h))
        return h, logits, aux_total

    def mtp_forward(self, h: torch.Tensor, next_tokens: torch.Tensor,
                    start_pos: int = 0, depth: int = 0):
        """MTP 第 depth 层:给定主干隐状态和「下一个 token」,预测再下一个。"""
        B, T = next_tokens.shape
        cos, sin = self._rope(start_pos, T)
        mask = self._mask(T, start_pos, next_tokens.device)
        emb = self.embed(next_tokens)
        h2, aux = self.mtp[depth](h, emb, cos, sin, start_pos, mask)
        logits = self.head(self.mtp_norm(h2))
        return h2, logits, aux

    def forward(self, tokens: torch.Tensor, targets: Optional[torch.Tensor] = None,
                start_pos: int = 0):
        """训练入口。targets[i] 应为 tokens[i+1](由 dataset 准备好)。"""
        h, logits, aux = self.forward_trunk(tokens, start_pos)
        if targets is None:
            return {"logits": logits}

        loss_main = F.cross_entropy(logits.reshape(-1, logits.size(-1)),
                                    targets.reshape(-1), ignore_index=-100)
        out = {"logits": logits, "loss_main": loss_main, "aux_loss": aux}

        # ---- MTP:第 d 个模块用 t_{i+1+d} 当输入,监督目标是 t_{i+2+d} ----
        loss_mtp = logits.new_zeros(())
        if self.cfg.n_mtp > 0:
            cur_h = h
            for d in range(self.cfg.n_mtp):
                inp = _shift_left(targets, d, fill=0)          # t_{i+1+d}
                tgt = _shift_left(targets, d + 1, fill=-100)   # t_{i+2+d}
                cur_h, mtp_logits, aux_d = self.mtp_forward(cur_h, inp, start_pos, d)
                aux = aux + aux_d
                loss_mtp = loss_mtp + F.cross_entropy(
                    mtp_logits.reshape(-1, mtp_logits.size(-1)),
                    tgt.reshape(-1), ignore_index=-100)
            loss_mtp = loss_mtp / self.cfg.n_mtp

        out["loss_mtp"] = loss_mtp
        out["aux_loss"] = aux
        out["loss"] = loss_main + self.cfg.mtp_loss_weight * loss_mtp + aux
        return out

    # ================= KV cache 管理 =================
    def setup_cache(self, max_batch: int, max_seq_len: int, device=None, dtype=None):
        device = device or self.embed.weight.device
        dtype = dtype or self.embed.weight.dtype
        for m in self.modules():
            if isinstance(m, MLA):
                m.setup_cache(max_batch, max_seq_len, device, dtype)
        self._cache_ready = True

    def clear_cache(self):
        for m in self.modules():
            if isinstance(m, MLA):
                m.clear_cache()
        self._cache_ready = False

    def set_attn_impl(self, impl: str):
        for m in self.modules():
            if isinstance(m, MLA):
                m.attn_impl = impl

    # ================= 统计 =================
    def num_params(self) -> dict:
        cfg = self.cfg
        total = sum(p.numel() for p in self.parameters())
        if cfg.tie_embeddings:
            total_unique = total
        else:
            total_unique = total

        # 激活参数量:每个 token 实际参与计算的那部分
        inactive = 0
        for layer in self.layers:
            if layer.is_moe:
                per_expert = sum(p.numel() for p in layer.ffn.experts[0].parameters())
                skipped = cfg.n_routed_experts - cfg.n_activated_experts
                inactive += per_expert * skipped
        mtp_params = sum(p.numel() for p in self.mtp.parameters())

        return {
            "total": total_unique,
            "activated": total_unique - inactive - mtp_params,   # 推理时 MTP 可不用
            "mtp": mtp_params,
            "embedding": self.embed.weight.numel(),
        }

    def kv_cache_bytes_per_token(self, dtype_size: int = 2) -> dict:
        """对比 MLA 与等规模 MHA 的 KV cache 开销。"""
        cfg = self.cfg
        mla = (cfg.kv_lora_rank + cfg.qk_rope_head_dim) * cfg.n_layers * dtype_size
        mha = 2 * cfg.n_heads * cfg.v_head_dim * cfg.n_layers * dtype_size
        return {"mla": mla, "mha_equivalent": mha, "ratio": mha / mla}

    def expert_load_stats(self) -> List[float]:
        return [layer.ffn.gate.load_stats()["imbalance"]
                for layer in self.layers if layer.is_moe]

    @torch.no_grad()
    def update_expert_bias(self):
        """训练循环每个 optimizer step 后调用一次。"""
        speed = self.cfg.bias_update_speed
        if speed <= 0:
            return
        for m in self.modules():
            if isinstance(m, MoE):
                m.gate.update_bias(speed)