DeepSeek-V4-Flash 架构:
一个 token 的旅程
近处看细节,远处读压缩记忆;每层只叫醒少量专家。
← → 逐步讲解 · 拖动进度条回看 · F 全屏
文字先变向量,再一层层改写
每个算子先对齐四个维度
B 是请求数,S 是本次处理的 token 数,d 是隐藏宽度。进入一个 Block 的残差多一条大小为 4 的轴。
| 位置 | 完整形状 | 单 token 的元素数 |
|---|---|---|
| 残差输入 | [B, S, 4, 4096] | 16,384 |
| mHC 汇合后 | [B, S, 4096] | 4,096 |
| Q / shared KV | [B,S,64,512] / [B,S,512] | 32,768 / 512 |
| 专家中间态 | [被路由的 token 数, 2048] | 2,048 |
decode 时通常 S=1;prefill 的 S 可以很大。B×S 决定 GEMM 行数。Mi 表示 2²⁰:1 MiB=2²⁰ 字节,1 Mi 个参数=2²⁰ 个参数。
低秩 Q:两次矩阵乘法接起来
qr = rms_norm(X @ Wq_a.T)
q = (qr @ Wq_b.T).reshape(B, S, H, 512)
q = q / sqrt(mean(q*q, axis=-1) + eps)
q[..., -64:] = RoPE(q[..., -64:], position)两块权重共有 4096×1024 + 1024×32768 = 36 Mi 个参数。直接投影需要 128 Mi;低秩节省权重,但多出一次中间读写与归一化。
每层看多远,由层号决定
四条信息通路,共用一个子层
mHC 真正混合的是什么
| 张量 | 形状 | 如何得到 |
|---|---|---|
| x / z | [B,S,4,4096] / [B,S,4096] | 4 路残差加权成一个子层输入 |
| pre / post / B | 4 / 4 / 4×4 | 24 个动态系数,经约束后参与混合 |
| 生成系数的权重 | [24, 16384] | Attention 与 MoE 各有一套 |
B 做 20 轮 Sinkhorn 行列归一化,使行列和接近 1;pre 与 post 有自己的门控约束,不需要加和为 1。B 的两个轴分别对应旧流和新流。
四路残差,算出一个具体输入
加权输入是 6。取 F(z)=2z=12、B=I、post=[.25,.5,.75,1],新残差为 [5,10,15,20]。这些教学系数已给定;真实系数由当前残差动态生成。
很多个问题,共用一本笔记
局部和压缩记忆,共用一次归一化
局部条目和选中的压缩条目组成同一个候选集合。每个头还有 learned sink:它增加分母,没有对应的 value 输出。
一维教学例:两个条目的分数为 log2、0,value 为 4、10,sink 分数为 0。
如果删掉 sink,输出变为 6;如果先各自 softmax 再相加,权重也会改变。对齐参考实现时,这两处都要检查。
输出投影也分组,并且降低中间维度
o = inverse_RoPE(o)
o = o.reshape(B, S, groups, 4096)
r = einsum("bsgd,grd->bsgr", o, Wo_a)
y = r.flatten(-2) @ Wo_b.T每组包含 8 个头。第一段共 32 Mi 参数,第二段 32 Mi,共 64 Mi。把所有头直接映射回 d 需要 128 Mi 参数;这里两段合计 64 Mi。
把连续信息,整理成更少的条目
压缩是逐通道的 learned pooling
softmax 沿 token 轴做;每个通道可以重视不同 token。pooling 后做 RMSNorm,再对末尾 64 维施加 RoPE。
CSA 步长为 4。每个 token 投影成 2×512 维:上一组用前半,当前组用后半,合成最多 8 个 token 的重叠窗口。
HCA 每 128 个 token 一组,无重叠。压缩条目的 RoPE 位置取当前组起点:0、128、256……。
一个通道,逐项算出压缩结果
结果为 7.4。这里展示四个有效候选;第一组没有前一组,缺失位置的 gate 为 −∞、权重为 0。这只是归一化前的一个通道。
压缩条目必须等整组完成后才可见
| 刚处理的位置 t(从 0 开始) | CSA 已完成组数 | 可见的新条目 |
|---|---|---|
| 2 | ⌊3/4⌋ = 0 | 无 |
| 3 | ⌊4/4⌋ = 1 | 第 0 组:token 0…3 |
| 6 | ⌊7/4⌋ = 1 | 仍只有第 0 组 |
| 7 | ⌊8/4⌋ = 2 | 第 1 组:前组+当前组重叠 |
visible = (t + 1) // ratio
valid = compressed_index < visible
if (t + 1) % ratio == 0:
cache[t // ratio] = pool(completed_group)prefill 可以并行算出整段的压缩条目,但每个 query 仍要加自己的因果 mask;否则前面的 token 会看到组内未来信息。
先用小索引器挑,再做大注意力
少读 KV 之前,Indexer 仍要给历史打分
Indexer 使用 64 个 128 维头,共享一份 128 维压缩索引缓存。w = weights_proj(x) / √128 / √64,可正可负;它不是 softmax 概率。TP 时先 all-reduce 分数,再选条目。
| 上下文长度 | 压缩候选 N/4 | Flash 主注意力压缩读数 |
|---|---|---|
| 32,768 | 8,192 | 512 |
| 131,072 | 32,768 | 512 |
| 1,048,576 | 262,144 | 512 |
top-k 固定了主注意力读数,Indexer 的候选扫描仍随上下文增长。prefill 参考路径还会构建 query×候选分数,必须把这项计入临时显存。
把远处概括得更短,就能全部读
长上下文时,先数真正访问的条目
| 32,768 token 的单层 | 每头访问条目 | Attention 核心 FLOP / query |
|---|---|---|
| 普通全注意力 | 32,768 | 4×64×32768×512 ≈ 4.29G |
| Flash CSA | 128+512=640 | 4×64×640×512 ≈ 83.9M |
| Flash HCA | 128+256=384 | 4×64×384×512 ≈ 50.3M |
两个乘加阶段分别是 QK 和加权 value,合计约 4HCd。读数少了,但 Indexer 与压缩器仍需要计算;小 batch 也可能被 kernel 启动和不连续访问拖慢。
N=65,536 时 HCA 的压缩读数达到 512,与 CSA 的 top-k 相同;再往后 HCA 继续增长。
256 个候选专家,每次选择 6 个
专家就是各自拥有权重的一小段前馈网络。图中只画出部分候选。
选谁和给多少权重,是两次计算
scores = sqrt(softplus(x @ Wgate.T))
if layer_id < 3:
ids = tid2eid[token_id] # 固定的 6 个专家 ID
else:
ids = topk(scores + bias, k=6)
w = scores[ids] / scores[ids].sum()
w *= route_scale
y = sum(w[j] * expert[ids[j]](x) for j in range(6))
y += shared_expert(x)专家选择可受 bias 影响,归一化权重取未加 bias 的 scores。Flash 的 route_scale=1.5,因此六个路由权重之和是 1.5,共享专家另外相加。
一个专家,三块矩阵
| 项目 | 计算 | 参数量 |
|---|---|---|
| 一个专家 | 3×4096×2048 | 24 Mi |
| 43 层全部路由专家 | 43×256×3dm | 277.025 B |
| 每 token 经过的专家参数 | 43×(6+1)×3dm | 7.575 B |
实际先把 up 限制在 [−10,10]、gate 上限设为 10,再做 SiLU 与逐元素乘法;路由权重融合在 W₂ 之前。表中未计 Attention、embedding、路由器、mHC 和 MTP。所有候选专家权重仍需存储。
把理解变成下一步的选择
短输入一致,长输入开始偏:检查边界
教学故障:参考输出与自写实现,在 3 token 时一致;4 token 时偏离;第 8 token 偏差再次增大。
| 检查 | 应当满足 | 最有价值的证据 |
|---|---|---|
| 压缩触发 | 处理 t=3、7 时追加条目 | 逐步打印 visible 和写入索引 |
| 重叠组 | 第二组使用前组前半+当前组后半 | 对比 pooling 前的 8×512 输入 |
| prefill / decode | 同一前缀最后位置输出接近 | 固定权重、禁用采样,比较中间张量 |
从第一个不一致的张量往前找。若 pooled KV 已不同,调采样温度和路由器都不会修复;若 KV 一致,再检查因果索引与 joint softmax。
同一套思路,不同的计算预算
| 配置 | V4-Flash | V4-Pro |
|---|---|---|
| 主干层数 / 宽度 | 43 / 4096 | 61 / 7168 |
| 总参数 / 每 token 激活 | 284B / 13B | 1.6T / 49B |
| 开头两层 | 纯 SWA | HCA + 局部窗口 |
| Query 头 / 低秩维度 | 64 / 1024 | 128 / 1536 |
| CSA top-k | 512 | 1024 |
| 路由专家 / 中间维度 | 256 / 2048 | 384 / 3072 |
| 输出投影分组 | 8 | 16 |
共同配置:4 路残差、512 维 shared KV、128 token 局部窗口、4 / 128 压缩比、6 个路由专家 + 1 共享专家、MTP 深度 1。总参数决定常驻权重规模,激活参数描述每 token 使用的权重规模。
为一个真实输入写出形状和字节数
展开参考解答
残差:2×16×4×4096=524,288 元素,乘 2 B = 1024 KiB。Q:2×16×64×512=1,048,576 元素,占 2048 KiB。
shared KV:2×16×512=16,384 元素,占 32 KiB。头数改变 Q 的大小;shared KV 无须再乘 Q 头数,也没有额外一份 V。
先写出张量形状与单位,再代入数字。
给选择加 bias,别给权重加 bias
展开参考解答
选择分数为 [5,2,3,4],所以选 E1 和 E4。归一化仍使用原始分数 [1,4],除以 5,再乘 1.5,得到 [0.3, 1.2]。
若误用 [5,4] 归一化,会得到 [0.8333, 0.6667],改变模型。共享专家没有参加这个 top-k,也不参加这次归一化。
先写出张量形状与单位,再代入数字。
定位一个只在整组边界出现的错误
展开参考解答
t=6 时错误规则放行 0、1,但只有第 0 组已完成。t=7 时放行 0、1、2,但只有前两组已完成。正确规则是严格小于完成组数:index < ⌊(t+1)/4⌋。
decode 中可能读到尚未写入的缓存;prefill 中则可能读到已经并行算出的未来条目。两条路径都错,却不一定以同一种症状出现。
先写出张量形状与单位,再代入数字。
把推导变成可以检查的程序
用 NumPy 对比整段与逐 token 的重叠 pooling,检查因果边界、路由 bias 与 mHC 数值。
python3 -m pip install numpy
python3 lab.pyprefill == streaming: True
pooled scalar: 7.400000
routing weights: [0.3, 1.2]
all checks passed修改参数前先预测结果,再运行检查。实验使用小张量和解析预算,在 CPU 上执行。
继续沿着实现走
配置核对:2026-10-20。动画中的 token、专家选择、显存块大小是教学示例。