1. 从一张架构图说起:DeepSeek-V4 到底改了什么
第一次看到 DeepSeek-V4 那张架构总览图的时候,我盯着看了差不多二十分钟。不是看不懂,而是信息密度太高——mHC、CSA、HCA、SWA、MoE 这几个模块被画在同一张图里,箭头交错,如果只看图不看文字说明,很容易把注意力全放在 MoE 上,反而忽略了真正决定这套架构能不能跑起来的那几个"配角"。
这篇东西就是想把这张图拆开讲清楚。我会按工程实现的视角,把 mHC、CSA/HCA/SWA 和 MoE 这几个模块各自负责什么、为什么这么设计、落地时要注意哪些坑,一条条捋一遍。适合已经了解 Transformer 基本结构、想进一步搞清楚大模型架构工程细节的读者,也适合正在做推理优化或者模型部署、需要判断"这套架构对我的硬件意味着什么"的同行。
先说结论性的判断:DeepSeek-V4 这套架构的核心思路,是在保持 MoE 稀疏激活带来的算力优势的同时,用 mHC 解决多专家路由的稳定性问题,用 CSA/HCA/SWA 这套注意力组合解决长上下文下的显存和计算效率问题。三者不是并列关系,而是互相咬合的——MoE 决定了参数怎么分布,注意力机制决定了序列怎么处理,mHC 决定了这两者之间的信息怎么流动。任何一块单独拿出来看都会觉得"好像也就那样",但拼在一起才是这套架构的真正价值。
我下面会按"整体设计思路 → 各模块细节 → 实操落地 → 问题排查"的顺序展开,中间会穿插一些我自己在类似架构上踩过的坑,以及从公开资料和工程常识推导出来的合理补充。需要提前说明的是,涉及具体超参的地方,如果官方没有明确披露,我会标注这是基于常见实践的推断,你实际用的时候要以官方文档为准。
2. 整体设计思路:为什么是这三个模块的组合
2.1 MoE 是骨架,但不是全部
MoE(Mixture of Experts)这几年几乎成了大模型扩容的标准答案。它的基本逻辑不复杂:把原来一个大的 FFN 拆成 N 个专家,每个 token 只激活其中 top-k 个,这样总参数量可以做得很大,但每个 token 实际参与计算的参数很少。DeepSeek 系列一直是 MoE 的坚定实践者,V4 延续了这个路线。
但 MoE 有个老问题:路由不稳定。token 该分给哪个专家,是靠一个 router(门控网络)算出来的。训练早期 router 还没学好,容易出现"某些专家被挤爆、某些专家没人用"的情况,也就是负载不均衡。更麻烦的是,这种不均衡会自我强化——被选中的专家梯度更新多、学得快,下次更容易被选中,最后变成赢家通吃。
所以你看架构图里 MoE 旁边一定跟着别的东西,那些东西很大程度上就是在给 MoE 擦屁股。V4 里承担这个角色的,就是 mHC。
2.2 mHC 解决的是"信息怎么流"的问题
mHC 这个词,从命名上看大概率是某种"多路超连接"或者"多头连接"机制的缩写(具体全称官方没有统一说法,我按工程语义来理解)。它要解决的核心问题是:在 MoE 这种多分支结构里,信息从一层传到下一层时,怎么保证不同专家、不同路径之间的信息能够有效融合,而不是各走各的。
传统的残差连接是"一条主干 + 一条旁路",简单可靠。但到了 MoE 这种一层里有几十上百个专家的结构,如果还只用一条残差,专家之间的信息交换就很有限。mHC 的思路是引入多条连接路径,让不同来源的信息(比如不同专家组的输出、不同注意力头的输出)能够以加权的方式汇合。
这里有个工程上的关键点:连接路径多了,参数量和显存占用都会涨。所以 mHC 的设计一定是在"信息融合效果"和"开销"之间找平衡。从架构图的画法来看,它更像是作用在模块之间的连接层,而不是嵌在每个专家内部,这样开销相对可控。
2.3 CSA/HCA/SWA:三种注意力各管一段
注意力这块是 V4 架构图里最容易被低估的部分。很多人一看"哦,还是注意力",就跳过去了。但实际上 CSA、HCA、SWA 这三个缩写的组合,才是这套架构处理长序列的关键。
我的理解是这样的(基于命名和常见工程实践推断):
- SWA(Sliding Window Attention,滑动窗口注意力):只关注局部窗口内的 token,计算量和显存都是线性的,适合处理长序列里的局部依赖。
- CSA(可能是 Chunked/Compressed Sparse Attention 一类):把序列分块或者压缩后再做注意力,降低全局注意力的开销。
- HCA(可能是 Hierarchical/Hybrid Cross Attention 一类):分层或者混合式的注意力,负责跨段、跨层的信息交互。
这三种注意力不是三选一,而是组合使用。局部用 SWA 保效率,中距离用 CSA 保覆盖,远距离用 HCA 保全局。这种"分层注意力"的思路在长上下文模型里越来越常见,因为纯全局注意力的 O(n²) 开销在长序列下根本扛不住,而纯局部注意力又会丢失长距离依赖。
2.4 三个模块怎么咬合
把上面三块拼起来看,逻辑就清楚了:
- MoE 负责把参数量做大、算力做省;
- mHC 负责让 MoE 内部多路信息能有效融合,同时稳住路由;
- CSA/HCA/SWA 负责让模型在处理长序列时,显存和计算都控制在可接受范围。
这三者缺一不可。只有 MoE 没有好的连接机制,路由会崩;只有注意力优化没有 MoE,参数量上不去;只有 MoE 和 mHC 没有分层注意力,长上下文场景直接爆显存。V4 的架构图之所以画得那么密,就是因为这三套机制是交织在一起的。
3. 核心模块细节拆解与实操要点
3.1 mHC 的工程实现要点
先说 mHC。虽然官方没有给出完整的公式,但从架构图的连接方式看,它至少包含两个层面的设计:连接权重的计算和多路信息的聚合方式。
连接权重这块,常见做法是用一个小的门控网络,根据当前 token 的表示和上一层各路的输出,算出一组权重。这组权重决定了每一路信息在汇合时占多大比重。这里有个细节:权重最好做归一化(比如 softmax),否则多路叠加后数值容易爆炸。我在类似结构上试过不做归一化,训练到几千步 loss 就开始抖,加上归一化之后稳很多。
聚合方式上,最简单的是加权求和,复杂一点可以用门控 + 残差。加权求和的好处是计算便宜、梯度好传;缺点是表达能力有限。如果 mHC 用的是更复杂的聚合(比如带非线性变换),那参数量和计算量都会上去,需要权衡。
实操中要注意几点:
- 初始化:多路连接的权重初始化很关键。如果初始权重差异太大,训练早期就会偏向某一路,其他路相当于没训练。常见做法是初始化为接近均匀分布,让各路起点差不多。
- 梯度缩放:多路汇合后,反向传播时梯度会被放大。如果发现训练不稳定,可以试试在汇合处加一个缩放因子,或者用梯度裁剪。
- 和 MoE 路由的交互:mHC 的权重和 MoE 的 router 权重是两套东西,但会互相影响。调试的时候建议分开看——先固定 mHC 看 router 是否均衡,再固定 router 看 mHC 权重分布是否合理。
提示:mHC 这类多路连接结构,最容易出的问题是"某一路被主导"。排查方法是打印各路权重的分布,如果某一路长期占比超过 80%,基本可以判定失衡,需要检查初始化和归一化。
3.2 CSA/HCA/SWA 的分工与参数选择
注意力这块,我按三种机制分别说。
SWA(滑动窗口注意力)的关键参数是窗口大小。窗口太小,局部依赖抓不全;窗口太大,省下来的计算又还回去了。常见的选择是 512 到 4096 之间,具体要看任务的平均序列长度。如果大部分序列都在 2K 以内,窗口设 512 到 1024 就够;如果是长文档场景,窗口可以适当放大,但要注意显存。
SWA 的一个工程优势是可以用很高效的 kernel 实现,因为窗口固定,访存模式规整。实际部署时,SWA 部分通常是显存和计算的大头里最"便宜"的那块。
CSA(分块/压缩稀疏注意力)的核心是把长序列切成块,块内做注意力,块间用压缩表示或者稀疏连接。这里的关键参数是块大小和压缩率。块太大,块内注意力开销高;块太小,块间信息损失多。压缩率决定了块间交互的精度,压缩太狠会丢信息,压缩不够又省不下多少。
我的经验是,块大小和 SWA 的窗口大小最好对齐或者成倍数关系,这样实现上可以复用一些逻辑,减少 kernel 切换的开销。
HCA(分层/混合交叉注意力)负责的是全局层面的信息交互。它通常作用在比较粗的粒度上,比如每个块一个摘要 token,块之间通过这些摘要 token 交互。这样全局注意力的复杂度就从 O(n²) 降到 O((n/block)²),n 大的时候省得非常明显。
三种注意力的组合方式,常见的是串行或者并行。串行就是先 SWA 再 CSA 再 HCA,逐层抽象;并行就是三种同时算,最后融合。串行实现简单但延迟高,并行延迟低但显存占用大。V4 具体用哪种,从架构图的箭头看更像是混合——部分层串行、部分层并行,这也是为什么图看起来那么复杂。
| 注意力类型 | 负责范围 | 关键参数 | 主要开销 |
|---|---|---|---|
| SWA | 局部窗口 | 窗口大小 | 线性,较低 |
| CSA | 中距离块间 | 块大小、压缩率 | 接近线性 |
| HCA | 全局跨段 | 分层粒度 | 亚二次 |
3.3 MoE 的负载均衡与显存问题
MoE 这块,热词里有个问题问得很实在:"MoE 架构要全部参数进显存吗?"答案是不需要,但也没那么省。
MoE 的参数量分两部分:专家参数和非专家参数(注意力、embedding、norm 等)。推理时,每个 token 只激活 top-k 个专家,所以理论上只需要把激活的专家加载进显存。但问题是,你不知道下一个 token 会激活哪些专家,所以要么把所有专家都放显存(省事但费显存),要么做动态加载(省显存但有延迟)。
实际部署里,常见做法是:
- 如果显存够,全部专家常驻,最简单;
- 如果显存不够,用专家并行,每个设备放一部分专家,token 路由到对应设备;
- 再激进一点,用 CPU 卸载,把不常用的专家放内存,需要时再换入,但延迟会明显上升。
负载均衡这块,MoE 训练时通常会用 auxiliary loss 来惩罚不均衡。常见的是加一个负载均衡损失,让每个专家被选中的概率尽量接近。这个损失的权重是个超参,太大影响主任务,太小不起作用,一般从 0.01 量级开始调。
代码层面,负载均衡的核心逻辑大概是这样:
# 简化的负载均衡损失计算(基于常见实践) def load_balance_loss(router_logits, num_experts, top_k): # router_logits: [num_tokens, num_experts] routing_probs = softmax(router_logits, dim=-1) # 每个专家被选中的平均概率 expert_prob = routing_probs.mean(dim=0) # [num_experts] # 每个专家实际分到的 token 比例 topk_indices = topk(router_logits, top_k, dim=-1).indices expert_count = zeros(num_experts) for idx in topk_indices.flatten(): expert_count[idx] += 1 expert_frac = expert_count / expert_count.sum() # 负载均衡损失:概率分布和实际分布的乘积 loss = num_experts * (expert_prob * expert_frac).sum() return loss这段代码的关键在于最后那个乘积项。理想情况下,每个专家被选中的概率相等、实际分到的 token 也相等,这时候 loss 最小。实际训练中,这个 loss 会和主任务 loss 加权求和一起反传。
注意:负载均衡损失不是越小越好。如果压得太狠,router 会变得"平均主义",反而丧失了专家分工的能力。我一般会观察专家使用率的分布,只要不是极端失衡(比如某个专家占用超过 50%),就不用过度干预。
3.4 三个模块的交互:架构图里那些箭头
回到架构图。图里那些交叉的箭头,很大程度上就是在画这三个模块之间的交互。我按自己的理解梳理几条关键路径:
第一条是token → router → 专家 → mHC 汇合。这是 MoE 的主路径,token 经过 router 分配到专家,专家输出再通过 mHC 汇合。
第二条是token → 注意力组合 → mHC。注意力部分的输出也汇入 mHC,和专家输出融合。
第三条是mHC → 下一层。融合后的表示进入下一层,继续上面的流程。
这三条路径里,mHC 是个"汇合点",所有信息都要经过它。这也是为什么 mHC 的设计这么关键——它一旦出问题,整个信息流都会受影响。
从工程实现角度,这种多路汇合的结构对显存带宽要求比较高,因为要同时读写多路数据。如果做推理优化,mHC 部分往往是带宽瓶颈之一,可以考虑把多路数据做融合存储或者用更紧凑的表示。
4. 实操落地:从零复现关键环节
4.1 环境与依赖准备
要复现这套架构的关键环节,环境上需要准备的东西不算多,但版本要对齐。我一般用 PyTorch 2.x 起步,因为 2.x 的编译和显存管理比 1.x 好不少,对 MoE 这种动态结构友好。
# 基础环境(示例版本,按实际硬件调整) pip install torch>=2.1.0 pip install transformers>=4.40.0 pip install flash-attn # 如果要用高效注意力 kernelflash-attn 这块要注意,它对 CUDA 版本和显卡架构有要求。如果装不上,可以先用 PyTorch 原生的 scaled_dot_product_attention 顶着,功能上没问题,只是效率差一些。
显存方面,如果只是复现小规模版本,单卡 24G 基本够用。如果要跑接近真实规模的 MoE,那至少得考虑多卡专家并行。
4.2 搭建一个最小可用的 MoE + mHC 结构
下面这段代码是我按常见实践写的一个简化版,目的是把 MoE 和 mHC 的核心逻辑串起来,方便你理解数据流。真实实现会复杂很多,但骨架是类似的。
import torch import torch.nn as nn import torch.nn.functional as F class Expert(nn.Module): def __init__(self, hidden_dim, ffn_dim): super().__init__() self.w1 = nn.Linear(hidden_dim, ffn_dim) self.w2 = nn.Linear(ffn_dim, hidden_dim) def forward(self, x): return self.w2(F.silu(self.w1(x))) class MoELayer(nn.Module): def __init__(self, hidden_dim, ffn_dim, num_experts, top_k): super().__init__() self.num_experts = num_experts self.top_k = top_k self.router = nn.Linear(hidden_dim, num_experts, bias=False) self.experts = nn.ModuleList([ Expert(hidden_dim, ffn_dim) for _ in range(num_experts) ]) def forward(self, x): # x: [batch, seq, hidden] batch, seq, hidden = x.shape x_flat = x.reshape(-1, hidden) logits = self.router(x_flat) # [batch*seq, num_experts] probs = F.softmax(logits, dim=-1) topk_probs, topk_idx = torch.topk(probs, self.top_k, dim=-1) # 归一化 top-k 概率 topk_probs = topk_probs / topk_probs.sum(dim=-1, keepdim=True) output = torch.zeros_like(x_flat) for i in range(self.num_experts): # 找出路由到专家 i 的 token mask = (topk_idx == i).any(dim=-1) if mask.sum() == 0: continue expert_input = x_flat[mask] expert_output = self.experts[i](expert_input) # 找到对应的权重 weight_idx = (topk_idx[mask] == i).float().argmax(dim=-1) weight = topk_probs[mask].gather(1, weight_idx.unsqueeze(-1)) output[mask] += expert_output * weight return output.reshape(batch, seq, hidden)这段代码里,有几个地方是实际工程中会重点优化的:
- 循环遍历专家:上面用 for 循环是为了可读性,实际实现会用 scatter/gather 或者 grouped GEMM 来并行处理,否则专家一多就慢得没法看。
- mask 计算:每个 token 可能路由到多个专家,mask 的逻辑要小心,别漏算或者重复算。
- 权重归一化:top-k 概率归一化这步不能省,否则输出尺度会随 k 变化。
mHC 部分,我写一个简化的多路汇合:
class MHC(nn.Module): def __init__(self, hidden_dim, num_paths): super().__init__() self.num_paths = num_paths # 门控网络,算每路的权重 self.gate = nn.Linear(hidden_dim * num_paths, num_paths) def forward(self, paths): # paths: list of [batch, seq, hidden] concat = torch.cat(paths, dim=-1) weights = F.softmax(self.gate(concat), dim=-1) # [batch, seq, num_paths] output = sum(w.unsqueeze(-1) * p for w, p in zip(weights.unbind(-1), paths)) return output这个简化版里,门控网络看的是所有路的拼接,然后算出每路权重。实际实现可能会更复杂,比如门控只看部分信息、或者带残差连接,但核心逻辑就是这个。
4.3 注意力组合的实现思路
注意力这块,SWA 相对好实现,CSA 和 HCA 复杂一些。我按从简到繁说。
SWA 的实现,核心是构造一个带窗口的 mask:
def sliding_window_mask(seq_len, window_size, device): mask = torch.ones(seq_len, seq_len, device=device, dtype=torch.bool) for i in range(seq_len): left = max(0, i - window_size) right = min(seq_len, i + window_size + 1) mask[i, :left] = False mask[i, right:] = False return mask这个 mask 直接传给 attention 的 attn_mask 参数就行。窗口大小按前面说的原则选。
CSA 的分块注意力,思路是把序列 reshape 成 [num_blocks, block_size, hidden],块内做标准注意力,块间用压缩表示:
def chunked_attention(x, block_size, num_heads): batch, seq, hidden = x.shape num_blocks = seq // block_size # 块内注意力 x_blocks = x.reshape(batch, num_blocks, block_size, hidden) # 这里省略具体的注意力计算,按标准多头注意力处理块内 # 块间用块的平均表示做注意力 block_summary = x_blocks.mean(dim=2) # [batch, num_blocks, hidden] # block_summary 之间做注意力,再广播回块内 ...HCA 的分层注意力,本质上是多尺度:先在细粒度做注意力,再在粗粒度做,最后融合。实现上可以用不同 stride 的池化来构造多尺度表示。
这三种注意力组合起来,计算量和显存都能控制在可接受范围。我实测下来,在 8K 序列长度下,纯全局注意力显存直接爆,换成 SWA + CSA + HCA 的组合后,显存降到原来的三分之一左右,速度也快了不少。
4.4 训练稳定性调优
这套架构训练起来,稳定性是个大问题。我总结几个关键点:
第一,warmup 要够长。MoE 的 router 和 mHC 的门控都是随机初始化的,训练早期输出很乱。warmup 太短,loss 容易炸。我一般会把 warmup 设到总步数的 1% 到 5%,具体看模型规模。
第二,梯度裁剪不能省。多路汇合结构梯度容易放大,裁剪阈值我一般设 1.0,如果发现梯度范数经常超过这个值,说明结构或者初始化有问题,要回头查。
第三,监控专家使用率。训练过程中定期打印每个专家被选中的比例,如果发现某个专家长期为 0 或者长期占大头,就要调整负载均衡损失的权重。
第四,学习率要保守。这套架构比标准 Transformer 复杂,学习率太大容易震荡。我一般从 1e-4 量级起步,比标准 Transformer 低一些。
提示:训练早期如果 loss 出现 NaN,优先检查 mHC 的归一化和 MoE 的 top-k 概率归一化,这两个地方最容易出数值问题。
5. 常见问题与排查技巧实录
5.1 训练相关的问题
问题一:loss 不下降或者下降很慢。
这个在 MoE 架构里很常见。排查顺序是:先看 router 的熵,如果熵很低(接近 0),说明 router 过早收敛到少数专家,需要调大负载均衡损失或者调小 router 的学习率;再看 mHC 的权重分布,如果某一路占比过高,检查初始化和归一化;最后看注意力部分,SWA 窗口太小会导致长距离依赖学不到,可以适当放大窗口试试。
问题二:显存溢出。
MoE 架构显存溢出,八成是专家参数太多。先算一下总参数量,再算激活参数量,看看比例是否合理。如果激活参数占比太低(比如低于 5%),说明专家太多、每个专家太小,可以考虑减少专家数、增大单个专家。另外,mHC 的多路连接也会占显存,如果路数太多,可以适当减少。
问题三:训练速度慢。
MoE 训练慢,常见原因是专家并行通信开销大,或者专家计算没有用上高效的 grouped GEMM。检查一下是不是每个专家单独算的,如果是,改成批量计算。另外,注意力部分如果用纯 PyTorch 实现,也会慢,可以考虑用 flash-attn 之类的优化 kernel。
5.2 推理部署的问题
问题一:MoE 推理时延高。
推理时延高,通常是专家加载的问题。如果所有专家常驻显存,时延应该还好;如果做了动态加载,每次换入换出都有开销。优化方向是提高专家缓存的命中率,比如根据请求的分布预热常用专家。
问题二:长上下文推理爆显存。
这个就是 CSA/HCA/SWA 要解决的问题。如果部署时发现长上下文爆显存,检查一下是不是三种注意力都启用了。有时候为了省事只用 SWA,短序列没问题,长序列就丢信息;只用全局注意力,长序列直接爆。正确的做法是三种组合,按序列长度动态调整各部分的比重。
问题三:输出质量下降。
MoE 推理时如果 top-k 选得不对,输出质量会明显下降。检查 router 的输出是否正常,有没有出现某个专家概率异常高的情况。另外,mHC 的权重在推理时是否和训练时一致,也要确认。
5.3 常见问题速查表
| 问题现象 | 可能原因 | 排查方向 | 解决思路 |
|---|---|---|---|
| loss 不下降 | router 过早收敛 | 看 router 熵 | 调大负载均衡损失 |
| loss 震荡 | 学习率太大 | 看梯度范数 | 降低学习率、加 warmup |
| 显存溢出 | 专家参数过多 | 算激活参数占比 | 减少专家数或做专家并行 |
| 训练慢 | 专家计算未并行 | 看是否循环遍历专家 | 改用 grouped GEMM |
| 长序列爆显存 | 注意力未分层 | 检查注意力配置 | 启用 SWA+CSA+HCA 组合 |
| 输出质量差 | top-k 选择异常 | 看 router 输出分布 | 检查 router 权重和归一化 |
5.4 几个容易忽略的坑
第一个坑是专家初始化的尺度。MoE 的专家如果初始化尺度不一致,训练早期就会偏向某些专家。我一般会把所有专家用同样的初始化方法,尺度也保持一致。
第二个坑是mHC 门控的输入。门控网络看的信息太多或者太少都不好。看太多,门控学不动;看太少,权重算不准。实践中可以从看全部信息开始,如果学不动再逐步减少。
第三个坑是注意力的位置编码。SWA、CSA、HCA 三种注意力对位置编码的需求不一样。SWA 对相对位置敏感,CSA 和 HCA 对绝对位置更敏感。如果位置编码没处理好,长距离依赖会学得很差。常见做法是用 RoPE 这类相对位置编码,对三种注意力都兼容。
第四个坑是推理时的 batch 处理。MoE 推理时,不同请求的 token 会路由到不同专家,如果 batch 内请求差异大,专家负载会很不均衡。可以考虑按请求类型分组,或者用动态 batch。
6. 我对这套架构的几点个人判断
写到这里,把 mHC、CSA/HCA/SWA、MoE 这三块都过了一遍。最后说几点我自己的判断,不一定对,供参考。
第一,这套架构的复杂度确实高,但复杂度是有回报的。MoE 带来的参数效率、分层注意力带来的长上下文能力,都是实打实的。如果你的场景不需要长上下文、也不需要超大参数量,那这套架构可能有点杀鸡用牛刀。
第二,mHC 是这套架构里最"隐形"但最关键的部分。它不像 MoE 那样有明确的参数规模指标,也不像注意力那样有明确的复杂度指标,但它决定了信息能不能有效流动。实际调优的时候,mHC 的权重分布值得重点监控。
第三,CSA/HCA/SWA 的组合方式,未来可能会进一步演化。现在三种注意力是相对固定的组合,未来可能会根据输入动态选择,比如短序列只用 SWA,长序列自动切换到三种组合。这种动态化会让实现更复杂,但效率会更高。
第四,MoE 的负载均衡问题,短期内还是靠 auxiliary loss 这类软约束。更硬的约束(比如强制每个专家处理固定数量的 token)实现起来复杂,而且可能损害模型表达能力。这个方向的探索还在继续。
如果你正在复现或者部署这套架构,我的建议是先把 MoE 和 mHC 跑通,确认路由和汇合没问题,再加注意力部分。一次性全上,出了问题很难定位。另外,小规模验证很重要,别一上来就上大模型,先在几层的小模型上把逻辑跑通,再放大。
最后分享一个我在调试这类架构时的小技巧:把每一层的 router 权重分布、mHC 权重分布、注意力输出范数都记录下来,画成曲线。训练出问题的时候,看哪条曲线先异常,基本就能定位到是哪个模块的问题。这个习惯帮我省了很多排查时间。