01 DeepSeek深度原理与架构

01 DeepSeek深度原理与架构
type
Post
status
Published
date
Mar 4, 2026
slug
deepseek-001
summary
tags
LLM
AI
category
DeepSeek
icon
password
😀
  • DeepSeek内容基础配置
‼️只支持linux、脚本无法在Win或Mac上跑通
‼️Mini模型暂定2B、可选择双卡4090、1T硬盘或4卡4090、2T硬盘
核心库版本 ↓

1 DeepSeekv3架构图解与基本参数配置

根据DeepSeekv3开源架构以及技术报告,我们绘制了DeepSeekv3基本架构图——
notion image
整体架构明显是一个类llama的decoder-only架构、具体架构板块如下——
  • 1. 输入嵌入层:模型以 ParallelEmbedding(并行嵌入层)作为起点,将输入的 token 映射到高维向量空间。
  • 2. 旋转位置编码 (Rotary Positional Encodings, RoPE):使用 RoPE 为输入嵌入添加位置信息,使模型能够有效地捕捉序列中 token 的顺序关系。
  • 3. RMSNorm 层归一化:每个注意力层和前馈层前均使用 RMSNorm(均方根归一化)进行规范化。
  • 4. 多头注意力机制 (Multi-Head Attention) 与 KV 缓存:这是带有KV缓存加速的多头注意力机制、提供两种缓存策略:
      1. Naive Cache(简单缓存):适合较短序列或常规场景。
      1. Absorb Cache(密集缓存):优化内存使用和推理效率,特别适用于长序列处理。
  • 5. 前馈网络 (FFN) 和混合专家模型 (MoE):在注意力模块之后,模型在以下两种模块之间进行 互斥选择
      1. FFN(前馈网络):使用 SiLU(Sigmoid Linear Unit) 作为激活函数,结合并行计算实现高效特征转换。
      1. MoE(专家混合模型):通过门控机制动态路由输入到部分专家网络。这种设计在不增加显著计算成本的前提下,提升了模型的参数效率和灵活性。
  • 6. 堆叠的 Transformer Blocks:模型由多个 Transformer Blocks 堆叠而成(图中标注为 *N)。每个 Transformer 块包含注意力模块、RMSNorm 层以及 FFN 或 MoE。
  • 7. 输出层:堆叠的 Transformer 块输出经过、使用RMSNorm 再次进行规范化、ColumnParallelLinear(列并行线性层)将特征映射到词汇表的概率分布、Softmax 层计算最终的概率分布。
在开源时,DeepSeekv3提供了三种不同规模的模型参数,但只提供了671B模型的权重——
参数名称
含义描述
16B
236B
671B
vocab_size
词汇表大小,支持的最大 token 数量
102400
102400
129280
dim
词向量维度,每个 token 的表示维度
2048
5120
7168
inter_dim
FFN 的中间层维度
10944
12288
18432
moe_inter_dim
MoE 模型中专家的中间层维度
1408
1536
2048
n_layers
Transformer 块的层数
27
60
61
n_dense_layers
使用 FFN 的 Transformer 块数量
1
1
3
n_heads
注意力头的数量
16
128
128
n_routed_experts
MoE 模型中所有专家的总数量
64
160
256
n_shared_experts
MoE 模型中共享专家的数量
2
2
1
n_activated_experts
每个输入激活的专家数量
6
6
8
n_expert_groups
专家分组的数量
-
8
8
n_limited_groups
路由到的分组数量限制
-
3
4
route_scale
专家路由得分的缩放因子
1.0
16.0
2.5
q_lora_rank
Q 投影的低秩分解维度
0
1536
1536
kv_lora_rank
KV 投影的低秩分解维度
512
512
512
qk_nope_head_dim
无位置嵌入的 Query/Key 头维度
128
128
128
qk_rope_head_dim
使用旋转位置编码的 Query/Key 头维度
64
64
64
v_head_dim
Value 投影的头维度
128
128
128
mscale
RoPE 编码的缩放因子,用于扩展序列长度
0.707
-
-
score_func
专家路由的打分函数类型(如 softmax/sigmoid)
-
-
sigmoid
dtype
数据类型(如 bf16、fp8)
-
-
fp8
在整个模型代码的最初进行参数配置可以使关键超参数显而易见,方便开发者快速了解模型的整体设置,而无需额外打开配置文件。这种方式对于调试或学习模型代码尤为重要。一些参数(如 world_sizerank 等分布式相关配置)需要与具体代码逻辑或硬件环境动态结合,因此直接在代码中定义更加直观,也能避免额外的动态加载过程。
‼️本次课程中我们讲解的是deepseekv3_model.py模型脚本、在原本开源的脚本基础上

2 MLA潜在注意力机制与低秩KV缓存

潜在注意力机制(Multi-head Latent Attention)是在原始多头注意力机制的KV缓存上改进得到的全新注意力机制。KV缓存是在传统注意力机制的推理过程中用于推理加速的一种机制,它的核心哲学是用内存换效率,它会保存推理时需要的中间变量、从而为推理过程加速。

2.1 经典的KV缓存

在经典注意力机制中,我们所使用的预测方式是自回归预测——
seq = 蒹葭苍苍
t=0、将"蒹葭苍苍"输入解码器、输出新token0。
t=1、将"蒹葭苍苍token0"输入解码器、输出新的token1。
t=2、将"蒹葭苍苍token0token1"输入解码器、输出新的token2。
……
这是一个按顺序执行的循环流程、必须完成上一步之后才能执行下一步,因此是一个很缓慢的过程。大语言模型正式通过这样“一个字、一个字地输出”的流程为我们逐步生成一段话。
在每次将一个序列输入解码器时,这个序列都需要经过注意力机制的拆解、并最终输出全新的token。注意力机制是通过计算单词之间两两相关性来理解信息的计算方式,其中的公式是——
其中:
  • :查询矩阵,构成相关性计算中的主动发起乘法的词向量
  • :键矩阵,构成相关性计算中的被乘的词向量
  • :值矩阵,用于携带单词本身的语义信息
  • :词向量的维度
  • 是缩放因子,用于稳定梯度
如公式所示,每个序列进入注意力机制后,都会有自己的Q、K、V矩阵,其中Q与K矩阵一般呈现如下结构——
其中, 表示 Query 矩阵中第 行、第 列的元素。同理,一个(n,d)结构的K矩阵如下—
假设现在Q是4行3列、K.T是3行4列,二者相乘、我们会得到——
加上前瞻掩码、使用更简化的写法,你会发现脚标是这样构成的:
现在让我们回到自回归的流程中——
  • t=0、将"蒹葭苍苍"输入解码器、输出新token0。
    • 先将序列seq经过线性层转化为序列的Q、K、V,然后求解——
  • t=1、将"蒹葭苍苍token0"输入解码器、输出新的token1。
    • 先将序列seq经过线性层转化为序列的Q、K、V(相比上次带了一个新的token1),然后求解——
  • t=2、将"蒹葭苍苍token0token1"输入解码器、输出新的token2。
    • 先将序列seq经过线性层转化为序列的Q、K、V(相比上次带了一个新的token2),然后求解——
你发现了吗?事实上在每次推理时,QK.T矩阵只是比上一次多了一行,而这全新的一行是由新token 的Q和历史的K组成的!因此不难发现,在进行推理时、与其每次都使用线性层对完整的序列进行转化、不如直接保留历史的K和V供后续的计算。保存历史的K和V之后、在每个时间步中就只需要为K和V增加最新的Token即可、无需再重新进行完整的线性计算。
如果没有 KV 缓存,我们每次都要计算:
  1. Q, K, V
X 线性变换得到:
  1. 为QK加上位置编码
3. 计算完整的 QK.T

如果缓存了 K/V,我们不需要每次都计算 K/V 了,而是:
  1. 仅仅计算Q、以及新的token的K和V
    1. 其中仅包含当前新加入的 token(通常是 1 个 token),因此计算成本比全部的K和V矩阵的计算成本小得多。
  1. 为QK加上位置编码
  • 只需要对Knew做位置编码,之前缓存的K矩阵是带位置编码的。
  1. 拼接已有的 KV 缓存
  • 和  直接从缓存中取出,无需重新计算。
  1. 计算完整的 QK.T
因此、对传统的KV缓存来说,我们的代码有如下流程 ↓ (以llama为例子)
  • 进一步探讨:为什么只缓存KV不缓存Q?
在进行推理时,由于我们只需要不断预测下一个token、因此我们事实上只需要QK.T矩阵的最后一行。
如果我们让注意力机制转变为让最新的一行输出、而忽略其他历史信息,因此实际上我们可以只计算——
  1. 新token的Q, K, V——
从 `X` 线性变换得到:
其中仅包含当前新加入的 token(通常是 1 个 token)。
  1. 为QK加上位置编码
  • 只需要对做位置编码,之前缓存的K矩阵是带位置编码的。
  1. 拼接已有的 KV 缓存
  • 和  直接从缓存中取出,无需重新计算。
  1. 计算单一token的QK.T
在我们的推理流程中,我们也会去实现类似的代码 ↓

2.2 低秩KV缓存与代码实现

KV缓存是典型的“牺牲内存加速运算”、对于不得不使用循环的推理流程来说确实是一个可靠的方案,然而它对内存的占用也是不可忽略的。DeepSeek所提出的“潜在注意力机制”就是尝试降低KV缓存所需的内存、同时也不损失太多精度的方案。
有哪些降低所需存储空间的基本方法呢?对于一个矩阵来说,要降低其所需的存储空间,要么就要缩小其尺寸压缩其信息、要么就降低其精度同时压缩其信息。对于潜在注意力机制而言,它选择的是“缩小尺寸、压缩信息”的方案,简单来说就是“先缩小再放大、中间损失忽略不计”的基本逻辑
首先我们来看DeepSeek对K矩阵和V矩阵执行的操作——
  • 1. 使用低秩投影(Low-rank Projection)让原始的X序列矩阵变成小尺寸矩阵C、将带有原始信息的小尺寸矩阵存储起来。这个方案不仅将原本需要保存的K、V两个矩阵转化为了一个携带原始信息的矩阵C,还降低了需要存储的矩阵的维度,因此大幅降低存储所需的内存。
  • 2. 在原始的大尺寸X矩阵上计算位置编码、并将该大尺寸位置编码存储起来,但由于位置编码只需要“薄薄的表单”就可以存储,因此几乎不占用内存。
  • 3. 在需要计算的时候、再从小尺寸矩阵C分别投影回大尺寸矩阵K和V,将大尺寸K结合位置编码后、基于大尺寸的K和V进行注意力计算,这样即可以保留足够的信息量、又可以存储较小的信息。
这个过程就是低秩KV缓存,具体来看 ↓
notion image
从数学公式来看,就是如下流程——
notion image
在这个过程中,低秩转换实际上就是线性投影,将原本(seq_len, 2048)结构的矩阵通过线性层投影为(seq_len, 512)维的矩阵C就可以存储起来。同时,用于保存位置编码的矩阵可能只有(seq_len, 64)尺寸大小,这样需要保存的矩阵就比原来需要的 2 x (seq_len, 2048)要小得多了,比原来节省了75%的存储成本。并且,由于C变小、因此GPU带宽压也会变得更小,在数据交换过程中变得更加容易。

对于Q矩阵,DeepSeek也支持完成低秩处理——
  • 1. 使用低秩投影(Low-rank Projection)让原始的X序列矩阵变成小尺寸矩阵C。不过并不保存任何的中间变量。
  • 2. 在原始的大尺寸X矩阵上计算位置编码,但同样的,不保存任何变量。
  • 3. 在需要计算的时候、再从小尺寸矩阵C投影回大尺寸的Q,将大尺寸的Q结合位置编码、再用大尺寸的Q进行注意力计算
不难发现,这个过程其实和K的处理过程一模一样,因此其数学过程也是 ↓
notion image
在训练过程中、如果我们对Q也执行低秩转换、可以帮助我们减少少许训练中所需的激活值内存。一般来说,激活值(Activation)是指向前传播中计算出的中间结果,这些指在反向传播时被用于梯度计算,这些激活值包括了——
  • 线性层的中间输出
  • 注意力计算时的 QKV 矩阵
  • softmax 计算出的注意力分数
  • 反向传播需要用到的梯度信息
对于大模型来说,激活值占用了训练过程中相当大的一部分 GPU 内存,特别是在注意力层(self-attention)里,QKV 矩阵和注意力分数矩阵(大小为 (batch, heads, seq_len, seq_len))都需要存储,导致内存消耗极大。
不过在推理过程中,Q做低秩缓存没有太大的意义,因此推理过程中一般没有Q的低秩缓存

  • 具体的代码实现
notion image
在这个过程中,位置编码链路和低秩矩阵压缩链路看起来是两条数据流,但实际代码在实现DeepSeek却只使用了一个线性层。我们是规定好低秩压缩的维度、以及用于承载旋转位置编码的矩阵所需要的维度后,使用一个线性层一次性将原始数据X(或中间值h)转换为所需的矩阵,再将两个矩阵分开做各自的运算。

3 DeepSeekMOE

门控是一种经典的深度学习机制。许多时候,我们使用线性层来“解读”原始信息,在原始信息上乘以一个常数、以常数的大小控制原始信息向下一层的流通,这被称为“门控机制”,控制常数的层被称之为“门”
当我们有多个线性层对信息进行不同的“解读”,同时又赋予每个信息对应的门时,这样的模型被称为混合专家模型——
当我们使用多个门同时控制信息的流通时,这些多个门构成的结构被称为是路由器。如图所示,混合专家模型 (MoE) 是一种动态路由策略,通过为不同的输入选择不同的子模型(专家模型)进行计算。相比于传统的全连接前馈网络(FFN),MoE 在每次前向传播时只激活部分专家模型,从而实现参数高效和计算高效。 MoE 已经广泛应用于大规模自然语言处理模型(如 GShard、Switch Transformer 等),并在计算机视觉等领域表现出色。其核心优势在于:模型具有超大参数量,但每次推理或训练时,只使用部分参数来进行计算。这是为什么针对DeepSeek这样的模型,我们会有全量参数和激活参数的区别。
描述文字
notion image

3.1 经典MOE模型速览

在混合专家模型的世界中、我们可以针对一个序列、一张表单或任意单独的一段信息设置一扇“门”(一个权重),我们也可以针对每一个token设置一个权重,大部分用于大模型的MOE使用的是token-level的路由设置。当我们将文字序列输入MOE时,常规的数据流如下所示 ↓
notion image
当上述流程转换为数学公式时,我们则可以有如下的表示 ↓
假设:
  • 假设输入向量为  是 token t 经过注意力机制后的隐藏状态(图中的数据X是完整的携带所有token的矩阵,则是一个token的向量)
  • 共有 N 个专家,每个专家都是一个带有门控的前馈网络(SwiGLU FFN),也就是至少会含有3个线性层。
  • 门控网络(路由器)是一个线性层,其结构是(dim, n_experts)。
  • 只有 K 个专家会被选择(Top-K 机制)。
根据上述假设,混合专家模型的一般流程为:
  • 1. 通过门控计算专家的权重分数 
    • 输入门控网络,通过门控网络(Gating Network)计算 token 对每个专家的偏好:
    • 其中  是门控网络的权重矩阵,i则代表特定的专家。
  • 2. 选出 Top-K 最高分的专家
    • 选择分数最高的 K 个专家,并将其他专家的权重通过掩码设为 0
    • 这样可以确保每个 token 只经过 K 个专家。
  • 3. 计算被选中的专家输出
    • 令  表示第 i 个专家的前馈网络:
    • 这里  是 被输入专家 i的输出结果。
    •  作为权重调整不同专家的贡献。
    •  作为当前层 MoE 的输出,将传递到 Transformer 的下一层。

3.2 DeepSeekMOE的基本流程

DeepSeekMoE(DeepSeek Mixture of Experts)是一种改进的 MoE(Mixture of Experts)架构,它从GShared等已经优化过的MoE架构上改造而来,又做出了许多细致的优化。从今天的眼光来看,DeepSeekMoE流程与常规MoE的区别包括以下的3点——
1. 引入了共享专家(Shared Experts)机制:为模型增加了固定必须要通过的专家、以确保MoE模型的学习能力。
2. 进行了更细粒度的专家划分(Fine-Grained Experts Segmentation):具体地来说——
• 将每个专家的FFN隐藏层维度减少为原始大小的1/m倍,实现更小、更细粒度的专家 • 专家变得更小后,每个专家的计算能力会大幅下降,因此为了补偿专家能力的下降、提出了“分组”技术,以前由TopK机制选择K个专家,现在则是选择K组专家,允许每组中存在m个小专家,一组专家被赋予一个权重 • 这样就大幅增加了专家数量、但涉及的总参数量和计算量不变,相当于将大的专家拆分成更多的小专家,同时也允许更多的小专家被激活,这样保证了计算成本一致,同时也增强了模型在处理多样化知识时的能力。 • 出自论文《DeepSeekMOE:Towards Utimate Expert Specialization in MoE language Models》[https://arxiv.org/abs/2401.06066]
notion image
3. 将原始的softmax函数修改为Sigmoid函数:让各个专家之间的计算变得独立、更便于进行并行计算,同时让专家之间的概率不再相互影响之后,softmax带来的概率可能是更接近“1高多0”的情况,而sigmoid带来的概率会更加显著(有效的专家与无效的专家之间的差异更加明显),因此更有可能进一步让更多的专家同时被激活,避免了 Softmax 归一化导致的极端情况(即过度偏向某几个专家)。
notion image
当这些改变加在数学流程上时,DeepSeekMoE流程可以呈现为——
假设:
  • 输入向量  为 token t 经过注意力机制后的隐藏状态。
  • 每个专家都是一个带有门控的前馈网络(SwiGLU FFN),也就是至少会含有3个线性层。
  • 门控网络(路由器)是一个线性层,其结构是(dim, n_experts)。
  • 只有 K 个专家会被选择(Top-K 机制)。
=========
1. 计算  经过共享专家(Shared Experts)的输出
  • 普通 MoE 只有动态选择的专家routed_experts,但 DeepSeekMoE 额外增加  个共享专家)。
  • 所有 token 都必须经过这  个共享专家,确保基本特征转换。
在任意token输入所有共享专家后、无论有多少个共享专家,都对所有共享专家的结果进行加和,形成当前token在共享专家上的输出结果 ↓
  • 这里  表示第 i 个共享专家,它们是固定启用的,不依赖 token 选择。
=========
2. 通过门控计算专家与输入向量的亲合度分数 
  • 输入门控网络,通过门控网络(Gating Network)计算 token 对每个专家的亲合度分数。不同于普通 MoE 采用 Softmax 计算专家分配权重,DeepSeekMoE 采用 Sigmoid 计算专家匹配度——
  • 其中  是 专家 i 的中心向量(centroid vector),用于衡量  与专家i的匹配度。不难发现,事实上的功能等同于门控网络的权重向量,实际我们在实现这一步骤计算时,也是使用线性层进行转换。
=========
3. 选出  个路由专家
  • 选择分数最高的  个专家,其余专家的得分设为 0
  • 这确保每个 token 最多只经过  个专家,减少计算量。
=========
4. 归一化选中的专家权重
选中的  个专家需要 进行归一化,以确保不同 token 选择的专家权重总和为 1:
  • 归一化后的  作为最终的专家权重。
=========
5. 计算  经过路由专家(Routed Experts)的输出 只计算 被选中的  个专家:FFN(r)i(ut)
  • 这里  是 路由专家 i,只有  选出的专家会执行前馈计算。
=========
6. 路由专家(Routed Experts)结果与路由专家权重进行加权求和 只计算 被选中的  个专家
  •  作为该专家的权重,与专家输出的结果进行加权求和。
=========
7. 计算最终输出  综合共享专家和路由专家的计算结果:
  • 第一项 :架构中的残差链接。
  • 第二项 :所有 token 共享专家的计算结果。
  • 第三项 :根据  选择的路由专家计算结果。
不记得残差连接?如下所示 ↓
image-20250107200502389
所以你不难发现,DeepSeekv3中的MoE实际的数学路程与论文中所呈现的顺序相反——
notion image

3.3 免负载均衡的DeepSeekMOE

DeepSeekMoE是DeepSeek公司一项重要的创新、但DeepSeekv3架构所使用的是DeepSeekMoE基础上改进的架构,即免负载均衡的DeepSeekMoE
负载不均衡是传统的混合专家(MoE)模型在处理专家负载时面临着一个重要问题,由于 MoE 模型根据输入的特征选择不同的专家进行计算,通常会出现某些专家频繁被激活,而其他专家几乎不参与计算的情况。这种不均衡的负载会导致部分专家的计算资源被过度消耗,而其他专家则未被充分利用,进而影响整体计算效率和模型性能。为了缓解这一问题,许多传统方法采用辅助损失(auxiliary loss)来强制平衡各个专家的负载,通过增加额外的损失项来惩罚负载不均衡。然而,过大的辅助损失往往会干扰模型的学习过程,导致模型性能下降。尽管这些方法可以在一定程度上减轻负载不均衡,但它们仍然面临着调整损失权重的难题,且可能影响模型的效果。
为了在负载平衡和模型性能之间取得更好的折中,DeepSeekv3开创了一种无辅助损失的负载平衡策略。具体来说,他们为每个专家引入了一个偏置项 ,并将其加到相应的亲和度得分(也就是我们之前总提到的每个专家的权重) 上来帮助确定专家是否被激活。
经过上述计算后,如果亲合度得分在 top-K 所规定的范围内,那该专家被激活,而且具体的激活值为,否则激活值为 0。
notion image
除此之外,在论文中还特别提到,DeepSeekv3会在训练过程中动态调整每个专家的偏置项 bi,以保证负载平衡:
  • 如果某个专家的负载过重(即该专家被激活过多),偏置项会减小,这样该专家的亲和度得分会降低,减少其被激活的概率。
  • 如果某个专家的负载过轻,偏置项会增加,增加该专家被激活的概率。
  • 这个动态调整过程通过一个超参数 γ 来控制偏置项的更新速度。
但事实上,在后来的架构实现中、DeepSeekv3采取的策略是:如果用户加载的是671B的DeepSeekv3模型、则直接让bias作为模型超参数、跟着损失函数一起更新、如果用户加载的是比671B更小的模型、则直接不设置bias选项。但无论如何,在开源代码中都没有设置用于更新bias的参数γ。在后续的开源的config中、也没有对γ进行设置。
【头脑风暴】DeepSeek为什么不对这部分进行开源?如果我们要自定义gamma相关的代码、最难的点在哪里?
以下是头脑风暴的伪代码——

3.4 补充辅助损失

尽管 DeepSeek-V3 主要依赖于无辅助损失的负载平衡策略,但为了防止在单个序列中出现极端的负载不平衡,模型还是引入了序列级别的负载平衡损失。这意味着,对于每一个输入序列,模型会对该序列中的每个专家的负载进行平衡、并且该流程会影响DeepSeekv3模型的预训练过程。
任何负载均衡方向的辅助损失都会从两个不同的角度衡量专家i的负载情况,分别是——
  • (论文中的第 20 个公式): 这是专家 i 在整个序列中的负载平衡指标,它是所有时间步 t 上归一化激活值的平均值。该指标用于衡量每个专家的负载是否平衡。
其中,(第 19 式) 是对专家负载的归一化处理,将每个专家在特定时间步 t 的激活值  除以该时间步所有专家的总激活值。这是为了避免某个专家的过高激活值影响到负载的判断。
  •   (论文中的第 18 个公式): 对每个专家 i,计算其负载平衡指标。具体来说,首先计算专家 i 在每个时间步中的激活情况(即0,1表示,使用 Top-k 路由选择,表示每个时间步选择激活最强的 k 个专家)。然后,根据这些激活情况,计算专家 i 在序列中的激活比例。
最终的辅助损失为——
  • (第 17 式): 这是负载平衡的总损失,定义为所有专家负载平衡的加权和。这里的加权系数由 平衡因子 α 控制,该因子通常是一个非常小的值,确保负载平衡不会主导训练过程。
其中,平衡因子 α 是一个超参数,在 DeepSeek-V3 中将赋予一个非常小的值;1(⋅) 表示指示函数;而 T 表示序列中的标记数量。序列级别的负载平衡损失鼓励每个序列中的专家负载保持平衡。当负载不均衡时,这个辅助损失将变得更大。
【深度】为什么这里让两个因子相乘就能够代表负载的均衡?

3.5 DeepSeekMoE的专家并行

专家并行机制是 Mixture-of-Experts (MoE) 模型在大规模分布式计算中的关键技术,旨在通过将模型的多个专家分布在不同的计算节点或设备上,来实现高效的计算和负载平衡。在大规模 MoE 模型中,模型的参数量非常庞大,且每个输入数据仅激活部分专家。如果每个设备都计算所有专家,计算量和内存消耗将急剧增加。专家并行机制的目标是将专家分配到多个设备上,只有特定的设备负责处理某些专家,从而达到:
  • 负载均衡:不同的设备处理不同的专家,避免某个设备负载过重。
  • 计算效率:通过并行计算,提高计算效率。
  • 内存管理:每个设备只需要加载和处理自己负责的专家,降低内存消耗。
核心问题1——如何分配专家到不同的设备呢
  • 本地专家:在分布式设置下,每张GPU只负责一部分专家。这些专家被称为 本地专家。例如,假设有 1000 个专家,如果系统有 10 个进程(即 world_size = 10),每个节点将负责 100 个专家。这种方式通过分配专家来确保每个设备的计算负载均衡。
  • 专家的起始和结束索引
    • 每个设备负责一部分专家,这部分专家通过两个索引进行标识:experts_start_idx 和 experts_end_idx
    • experts_start_idx 表示当前设备上负责的第一位专家的索引,experts_end_idx 表示最后一位专家的索引。
    • 例如,如果 n_routed_experts = 1000 且 world_size = 10,则每张GPU负责 100 个专家。假设GPU的 rank = 0,则该GPU负责的专家索引为 0 到 99(即从第 1 个专家到第 100 个专家)。
  • 局部专家的路由计算:每个设备仅会参与计算它所负责的专家的输出。路由机制会确保每个输入数据仅由选定的 top-k 专家 来处理。因此,专家并行机制 基于数据路由而不需要每个设备计算所有专家,从而有效减少了计算量。
从代码的角度来说,每张GPU只加载和计算它所负责的 本地专家。这通过以下代码实现:
self.experts_start_idx = rank * self.n_local_experts self.experts_end_idx = self.experts_start_idx + self.n_local_experts
  • rank 表示当前GPU的标识符(例如,在分布式训练中,rank 表示当前节点的编号)。
  • n_local_experts 是每张GPU上负责的专家数量。通过 rank 和 n_local_experts 计算当前节点负责的专家范围。

核心问题2——如何分配不同的token到不同的专家上呢
在前向传播过程中,每张GPU只负责计算它所负责的专家的输出,其中self.experts是专家的列表,x是输入的数据,weights是经过topk筛选出的权重。
  • counts[i] 记录了每个专家的激活次数,如果某个专家没有被激活,则跳过该专家。
  • 只计算当前GPU所负责的专家(由 self.experts_start_idx 和 self.experts_end_idx 确定的范围内)。
  • 我们会从所有的序列中索引出当前专家负责的token,然后将token输入到指定的专家中进行计算。
torch.where 用于根据选定的专家索引来选择输入数据的相应部分。具体的代码片段是:
  • indices 是一个张量,表示每个输入样本对应的 专家索引
  • i 是当前专家的索引。
  • indices == i 生成一个布尔条件张量,表示哪些样本被路由到了专家 i
  • torch.where(indices == i) 返回两个张量:
    • idx:表示满足条件 indices == i 的样本的索引。
    • top:这些满足条件的样本的位置索引。
在这段代码中,torch.where 用于 筛选出 路由到特定专家 i 的输入数据样本,并将它们的索引存储在 idx 和 top 变量中。
在分布式训练中,每个节点只计算它所负责的部分专家的输出。为了确保各个节点计算的结果一致,我们需要对结果进行 同步

3.6 DeepSeekMoE的全套实现代码

在我们借助代码实现DeepSeekMOE的过程中,我们同样采取大模型脚本编写中惯例的模块化代码编写方式、先编写MoE所需的各个模块、再写一个串联起所有元素的类用来实现DeepSeekMoE的全局。
  • 分布式SwiGLU FFN作为共享专家层
notion image
 
 
 
 
 
 
 

📎 参考文章

  • 一些引用
  • 引用文章
 
💡
欢迎您在底部评论区留言,一起交流~
上一篇
01 DeepAgents
下一篇
02 架构思维篇: Al时代的设计模式
Loading...