
https://github.com/deepseek-ai/DeepSeek-V3
一、开源的分量天差地别:为什么DeepSeek V3源码格外珍贵
最近重新梳理了这一轮大模型的核心论文与技术实现,主线依然是绕不开的Transformer体系。在我看来,目前开源领域里,能把完整源码开放出来、且达到业界顶尖水准的高峰之作,当属DeepSeek V3。后续的V3.1、V3.2更多是在原有架构上的细节打磨与能力补全,属于“打补丁”式的迭代;即便到了V4,核心架构也没有跳出这个基础框架,更关键的是V4的完整源码并未公开。
这里需要先厘清一个概念:开源和开源之间的分量天差地别。现在很多所谓的“开源模型”,比如Kimi K3,开放的是模型权重——你可以下载下来做推理、做微调,但看不到内部的核心实现细节:注意力怎么写、MoE路由怎么设计、训练里有哪些关键trick,通通是黑盒。当然,能开放权重已经是诚意十足,我们没有理由苛求所有公司都开放完整源码,商业公司优先考虑商业利益与技术壁垒,本就是市场常态。但也正因为如此,DeepSeek V3完整开源的架构源码,对于想要真正吃透大模型底层逻辑的开发者和研究者来说,才显得格外珍贵——它不是一个“可用的权重包”,而是一份可拆解、可复现、可验证的技术标准答案。
二、Decoder-Only:为什么它成了通用大模型的主流选择
我们不妨拆解看看,DeepSeek V3到底在Transformer的基础上做了什么,能成为开源界的标杆。

首先要明确的是,它的底子依然是标准的Transformer,而且是Decoder-Only的纯解码器架构。自2017年《Attention Is All You Need》发表至今,Transformer家族演化出了Encoder-Only、Encoder-Decoder、Decoder-Only等多条路线,最终是Decoder-Only成为了通用大模型的绝对主流,背后有三个核心原因: 第一,自回归生成的天然适配性。Decoder-Only的因果注意力机制(每个token只能看到它之前的内容),和语言生成“逐词预测、依次输出”的逻辑完全自洽,训练目标与推理范式高度统一,生成的流畅性、连贯性和长文本把控能力天然更优。相比之下,Encoder-Only更擅长语义理解但生成能力薄弱,Encoder-Decoder结构更适合翻译、摘要等定向生成任务,在开放式对话、长文创作等通用场景下,对齐成本和效果上限都弱于Decoder-Only。 第二,大规模训练的工程简洁性。单一的解码器堆叠架构,训练目标统一为“下一词预测”,梯度传导路径清晰,超参调优和分布式并行的落地难度更低。无论是数据并行、张量并行还是流水线并行,在规整的Decoder堆叠结构上都更容易实现工程优化,也更符合大模型“越做越大”的Scaling Law迭代规律。 第三,全场景任务的泛化能力。Decoder-Only模型经过大规模无监督预训练后,只需要通过少量有监督微调、人类偏好对齐,就能统一适配文本理解、内容创作、逻辑推理、代码生成、多轮对话等几乎所有NLP任务,真正实现“一个模型打全场”。而其他架构的能力边界更明显,想要覆盖多场景往往需要针对性改造,泛化的效率和效果都有差距。
航母本质上还是靠烧开水驱动的
从2017年到今天,快十年过去,Transformer依然是大模型的绝对主流,生命力之强大令人惊叹。这总让我想起那句“航母本质上还是靠烧开水驱动的”——从瓦特的蒸汽机到现代舰用燃气轮机,底层的热力学原理从未改变,但工程实现的维度早已天差地别:材料耐受极限、动力输出密度、精密控制体系、全系统集成度、可靠性与续航能力、配套的工业供应链,每一个维度都是量级上的飞跃。Transformer也是同理:底层永远是“自注意力+残差连接+前馈网络”的三板斧,但走到今天,模型规模、注意力优化、长上下文支持、MoE架构、训练工程、推理加速、对齐技术,每一个细分方向都已经演化出极其复杂的技术体系,早已不是最初那篇论文里的简单模样。
三、核心升级之MLA:多头隐注意力的巧思与价值
DeepSeek V3的整体架构图看起来非常简洁:左侧就是标准的Transformer层级堆叠,一个Block接一个Block纵向堆叠;但核心的两个组件都做了颠覆性重构——注意力机制换成了MLA(Multi-Head Latent Attention,多头隐注意力),前馈网络换成了DeepSeekMoE混合专家结构。
先来说MLA。别看它实现起来各种低秩分解、支路拆分,效果也堪称惊艳,但底层逻辑依然没有跳出QKV注意力的本质,核心是用更精巧的设计,换来了更小的计算与显存开销。它的核心优势体现在三个方面: 一是KV Cache显存的极致压缩。传统MHA、GQA的KV缓存,大小和注意力头数、序列长度成正比,上下文越长,显存占用越夸张。MLA把每个token的Key和Value压缩成了一条全局共享的低秩隐向量,再搭配单份独立的位置编码向量,缓存大小直接和注意力头数解耦。在128k甚至更长的上下文场景下,KV缓存的显存占用能下降数倍,这也是DeepSeek系列长上下文能力的核心支撑。 二是训练与推理的效率优化。通过矩阵乘法结合律做运算重排,MLA把KV的升秩计算融合进了注意力打分环节,不需要显式生成完整的多头KV中间张量,既降低了训练时的激活显存,也减少了推理时的内存带宽压力。同时Query侧也支持低秩压缩,进一步压缩训练时的显存峰值。 三是位置与内容的解耦设计。它把语义内容和位置信息拆成两条独立支路,RoPE只作用在低维的位置分支上,既完整保留了旋转位置编码的效果,又降低了位置编码的计算开销,也更方便做长上下文外推的修正优化。
而这么一套精巧的设计,落到核心代码里也就几十行。如果没有开源,我们或许能从论文里猜到原理,却永远没法看到经过工业级训练验证的标准答案。当然,照着论文我们也能自己写出一版实现,但自己写的终究只是“原理验证”,你没法确定它能不能稳定跑通大规模训练、会不会有隐藏的数值坑、效果能不能复现论文指标。开源代码的价值,就在于它是被真金白银的算力验证过的“可行解”,而不是纸面的假设。
# 定义多头注意力层类,继承自PyTorch的nn.Module类
classMLA(Module):
"""
多头注意力层(MLA)。
属性:
dim (int): 输入特征的维度。
n_heads (int): 注意力头的数量。
n_local_heads (int): 分布式系统中本地注意力头的数量。
q_lora_rank (int): 查询(query)的低秩投影的秩。
kv_lora_rank (int): 键(key)和值(value)的低秩投影的秩。
qk_nope_head_dim (int): 非位置相关的查询/键投影的维度。
qk_rope_head_dim (int): 旋转位置编码的查询/键投影的维度。
qk_head_dim (int): 查询/键投影的总维度。
v_head_dim (int): 值投影的维度。
softmax_scale (float): 注意力计算中softmax函数的缩放因子。
"""
def__init__(self, args):
# 调用父类的初始化方法
super().__init__()
# 保存输入特征的维度
self.dim = args.dim
# 保存注意力头的数量
self.n_heads = args.n_heads
# 计算分布式系统中本地注意力头的数量
self.n_local_heads = args.n_heads // world_size
# 保存查询的低秩投影的秩
self.q_lora_rank = args.q_lora_rank
# 保存键和值的低秩投影的秩
self.kv_lora_rank = args.kv_lora_rank
# 保存非位置相关的查询/键投影的维度
self.qk_nope_head_dim = args.qk_nope_head_dim
# 保存旋转位置编码的查询/键投影的维度
self.qk_rope_head_dim = args.qk_rope_head_dim
# 计算查询/键投影的总维度
self.qk_head_dim = args.qk_nope_head_dim + args.qk_rope_head_dim
# 保存值投影的维度
self.v_head_dim = args.v_head_dim
# 如果查询的低秩投影的秩为0,直接使用列并行线性层进行查询投影
if self.q_lora_rank == 0:
self.wq = ColumnParallelLinear(self.dim, self.n_heads * self.qk_head_dim)
# 否则,使用低秩分解的方式进行查询投影
else:
self.wq_a = Linear(self.dim, self.q_lora_rank)
self.q_norm = RMSNorm(self.q_lora_rank)
self.wq_b = ColumnParallelLinear(self.q_lora_rank, self.n_heads * self.qk_head_dim)
# 对输入进行线性变换得到键和值的低秩表示
self.wkv_a = Linear(self.dim, self.kv_lora_rank + self.qk_rope_head_dim)
# 对键和值的低秩表示进行归一化
self.kv_norm = RMSNorm(self.kv_lora_rank)
# 对归一化后的键和值进行线性变换
self.wkv_b = ColumnParallelLinear(self.kv_lora_rank, self.n_heads * (self.qk_nope_head_dim + self.v_head_dim))
# 对多头注意力的输出进行行并行线性变换
self.wo = RowParallelLinear(self.n_heads * self.v_head_dim, self.dim)
# 计算softmax函数的缩放因子
self.softmax_scale = self.qk_head_dim ** -0.5
# 如果最大序列长度大于原始序列长度,对缩放因子进行调整
if args.max_seq_len > args.original_seq_len:
mscale = 0.1 * args.mscale * math.log(args.rope_factor) + 1.0
self.softmax_scale = self.softmax_scale * mscale * mscale
# 如果注意力实现方式为朴素方式
if attn_impl == "naive":
# 注册键缓存
self.register_buffer("k_cache", torch.zeros(args.max_batch_size, args.max_seq_len, self.n_local_heads, self.qk_head_dim), persistent=False)
# 注册值缓存
self.register_buffer("v_cache", torch.zeros(args.max_batch_size, args.max_seq_len, self.n_local_heads, self.v_head_dim), persistent=False)
# 否则
else:
# 注册键值缓存
self.register_buffer("kv_cache", torch.zeros(args.max_batch_size, args.max_seq_len, self.kv_lora_rank), persistent=False)
# 注册位置编码缓存
self.register_buffer("pe_cache", torch.zeros(args.max_batch_size, args.max_seq_len, self.qk_rope_head_dim), persistent=False)
defforward(self, x, start_pos, freqs_cis, mask=None):
"""
多头注意力层(MLA)的前向传播过程。
参数:
x (torch.Tensor): 输入张量,形状为 (batch_size, seq_len, dim)。
start_pos (int): 序列中用于缓存的起始位置。
freqs_cis (torch.Tensor): 预计算的复指数值,用于旋转位置编码。
mask (Optional[torch.Tensor]): 掩码张量,用于在注意力计算中排除某些位置。
返回:
torch.Tensor: 输出张量,形状与输入相同。
"""
# 获取输入张量的批次大小、序列长度
bsz, seqlen, _ = x.size()
# 计算序列的结束位置
end_pos = start_pos + seqlen
# 如果查询的低秩投影的秩为0,直接通过线性层得到查询
if self.q_lora_rank == 0:
q = self.wq(x)
# 否则,通过低秩分解的方式得到查询
else:
q = self.wq_b(self.q_norm(self.wq_a(x)))
# 调整查询的形状,将其划分为多个头
q = q.view(bsz, seqlen, self.n_local_heads, self.qk_head_dim)
# 将查询划分为非位置相关部分和位置相关部分
q_nope, q_pe = torch.split(q, [self.qk_nope_head_dim, self.qk_rope_head_dim], dim=-1)
# 对位置相关部分应用旋转位置编码
q_pe = apply_rotary_emb(q_pe, freqs_cis)
# 通过线性层得到键和值的低秩表示
kv = self.wkv_a(x)
# 将键和值的低秩表示划分为低秩部分和位置编码部分
kv, k_pe = torch.split(kv, [self.kv_lora_rank, self.qk_rope_head_dim], dim=-1)
# 对位置编码部分应用旋转位置编码
k_pe = apply_rotary_emb(k_pe.unsqueeze(2), freqs_cis)
# 如果注意力实现方式为朴素方式
if attn_impl == "naive":
# 将非位置相关部分和位置相关部分拼接得到完整的查询
q = torch.cat([q_nope, q_pe], dim=-1)
# 对键和值的低秩表示进行归一化和线性变换
kv = self.wkv_b(self.kv_norm(kv))
# 调整键和值的形状,将其划分为多个头
kv = kv.view(bsz, seqlen, self.n_local_heads, self.qk_nope_head_dim + self.v_head_dim)
# 将键和值划分为非位置相关部分和值部分
k_nope, v = torch.split(kv, [self.qk_nope_head_dim, self.v_head_dim], dim=-1)
# 将非位置相关部分和位置编码部分拼接得到完整的键
k = torch.cat([k_nope, k_pe.expand(-1, -1, self.n_local_heads, -1)], dim=-1)
# 将键存入缓存
self.k_cache[:bsz, start_pos:end_pos] = k
# 将值存入缓存
self.v_cache[:bsz, start_pos:end_pos] = v
# 计算注意力分数
scores = torch.einsum("bshd,bthd->bsht", q, self.k_cache[:bsz, :end_pos]) * self.softmax_scale
# 否则
else:
# 获取键和值的线性变换层的权重
wkv_b = self.wkv_b.weight if self.wkv_b.scale isNoneelse weight_dequant(self.wkv_b.weight, self.wkv_b.scale, block_size)
# 调整权重的形状
wkv_b = wkv_b.view(self.n_local_heads, -1, self.kv_lora_rank)
# 计算非位置相关部分的注意力分数
q_nope = torch.einsum("bshd,hdc->bshc", q_nope, wkv_b[:, :self.qk_nope_head_dim])
# 将键和值的低秩表示归一化后存入缓存
self.kv_cache[:bsz, start_pos:end_pos] = self.kv_norm(kv)
# 将位置编码部分存入缓存
self.pe_cache[:bsz, start_pos:end_pos] = k_pe.squeeze(2)
# 计算注意力分数
scores = (torch.einsum("bshc,btc->bsht", q_nope, self.kv_cache[:bsz, :end_pos]) +
torch.einsum("bshr,btr->bsht", q_pe, self.pe_cache[:bsz, :end_pos])) * self.softmax_scale
# 如果存在掩码,将掩码加到注意力分数上
if mask isnotNone:
scores += mask.unsqueeze(1)
# 对注意力分数应用softmax函数
scores = scores.softmax(dim=-1, dtype=torch.float32).type_as(x)
# 如果注意力实现方式为朴素方式
if attn_impl == "naive":
# 通过注意力分数和值缓存计算输出
x = torch.einsum("bsht,bthd->bshd", scores, self.v_cache[:bsz, :end_pos])
# 否则
else:
# 通过注意力分数和键值缓存计算中间结果
x = torch.einsum("bsht,btc->bshc", scores, self.kv_cache[:bsz, :end_pos])
# 通过中间结果和权重计算输出
x = torch.einsum("bshc,hdc->bshd", x, wkv_b[:, -self.v_head_dim:])
# 对输出进行线性变换
x = self.wo(x.flatten(2))
return x四、核心升级之DeepSeekMoE:混合专家的朴素本质
再来看DeepSeekMoE。很多人觉得MoE很神秘,听起来是很复杂的下一代架构,但拆开来看,核心逻辑其实非常朴素。MoE全称Mixture of Experts,也就是混合专家模型,核心思路是“分而治之”:传统Transformer的每个FFN层,所有参数都要参与每个token的计算;而MoE把前馈网络拆成了多个独立的“专家”子网络,每个token进来,只激活少数几个专家完成计算。这样一来,模型总参数量可以做得非常大,但每次推理实际激活的参数量却很少,用小模型的计算成本,就能拿到大模型的能力效果。 而单个专家的结构,其实和普通Transformer里的FFN没有本质区别——就是两层全连接层加激活函数的堆叠,没有任何花里胡哨的设计。DeepSeekMoE的巧思不在单个专家,而在整体的路由设计:它设置了一部分始终处于激活状态的共享专家,负责处理所有token通用的基础语义;剩下的独立专家,由路由网络根据token特征,选择top-k个(通常是top-2)参与本次前向传播。这种“共享专家+动态路由”的组合,既保障了模型基础能力的稳定性,又实现了参数的高效利用,同时也缓解了纯动态路由带来的负载不均衡问题。 而这套MoE的核心实现,同样只有短短几十行代码。
# 定义混合专家(Mixture-of-Experts, MoE)模块类,继承自PyTorch的nn.Module类
classMoE(nn.Module):
"""
混合专家(Mixture-of-Experts, MoE)模块。
属性:
dim (int): 输入特征的维度。
n_routed_experts (int): 模型中专家的总数。
n_local_experts (int): 在分布式系统中本地处理的专家数量。
n_activated_experts (int): 每个输入激活的专家数量。
gate (nn.Module): 门控机制,用于将输入路由到不同的专家。
experts (nn.ModuleList): 专家模块列表,包含多个专家网络。
shared_experts (nn.Module): 共享专家模块,应用于所有输入。
"""
def__init__(self, args):
"""
初始化MoE模块。
参数:
args: 模型参数对象,包含MoE模块的相关参数。
"""
# 调用父类的初始化方法
super().__init__()
# 保存输入特征的维度
self.dim = args.dim
# 确保专家总数能被分布式系统中的进程数整除
assert args.n_routed_experts % world_size == 0, f"专家数量必须能被进程数整除 (进程数={world_size})"
# 保存模型中专家的总数
self.n_routed_experts = args.n_routed_experts
# 计算本地处理的专家数量
self.n_local_experts = args.n_routed_experts // world_size
# 保存每个输入激活的专家数量
self.n_activated_experts = args.n_activated_experts
# 计算本地专家在所有专家中的起始索引
self.experts_start_idx = rank * self.n_local_experts
# 计算本地专家在所有专家中的结束索引
self.experts_end_idx = self.experts_start_idx + self.n_local_experts
# 初始化门控机制
self.gate = Gate(args)
# 初始化专家模块列表,本地负责的专家使用Expert模块,其他位置置为None
self.experts = nn.ModuleList([Expert(args.dim, args.moe_inter_dim) if self.experts_start_idx <= i < self.experts_end_idx elseNone
for i in range(self.n_routed_experts)])
# 初始化共享专家模块
self.shared_experts = MLP(args.dim, args.n_shared_experts * args.moe_inter_dim)
defforward(self, x):
"""
MoE模块的前向传播过程。
参数:
x (torch.Tensor): 输入张量。
返回:
torch.Tensor: 经过专家路由和计算后的输出张量。
"""
# 保存输入张量的原始形状
shape = x.size()
# 将输入张量展平为二维张量,方便后续处理
x = x.view(-1, self.dim)
# 通过门控机制得到每个输入分配到各个专家的权重和对应的专家索引
weights, indices = self.gate(x)
# 初始化输出张量,形状与输入相同,初始值全为0
y = torch.zeros_like(x)
# 统计每个专家被分配到的输入数量
counts = torch.bincount(indices.flatten(), minlength=self.n_routed_experts).tolist()
# 遍历本地负责的专家
for i in range(self.experts_start_idx, self.experts_end_idx):
# 如果该专家没有被分配到输入,则跳过
if counts[i] == 0:
continue
# 获取当前专家模块
expert = self.experts[i]
# 找出分配到当前专家的输入的索引
idx, top = torch.where(indices == i)
# 将这些输入通过当前专家模块进行计算,并乘以对应的权重,累加到输出张量中
y[idx] += expert(x[idx]) * weights[idx, top, None]
# 将输入通过共享专家模块进行计算
z = self.shared_experts(x)
# 如果使用分布式训练,对本地专家的输出进行全局归约操作
if world_size > 1:
dist.all_reduce(y)
# 将本地专家的输出和共享专家的输出相加,并恢复到原始形状
return (y + z).view(shape)五、几百行代码之外:从原理到落地的万水千山
你看,DeepSeek V3的核心原理,就是这么简洁。但这就像“航母靠烧开水驱动”一样——原理一句话就能说清,但把原理变成能跑、能用、能打的工业级产品,中间隔着万水千山。 几百行的模型核心代码,只能让我们“知其然”,却远不足以让我们“知其所以然”。从原理到可工作的大模型,中间的训练工程全是硬骨头: 首先是数据端的巨大门槛。预训练需要万亿token级别的高质量语料,数据的爬取、清洗、去重、过滤、质量分级、版权合规,是一套极其庞大的系统工程。低质量、高重复的数据会直接锁死模型的能力上限;而不同领域数据的配比、训练的先后顺序,又会深刻影响模型的能力偏向,没有大量实验和经验积累,根本摸不到最优解。 其次是大规模训练的工程难题。千亿级MoE模型的训练,需要数千甚至上万张GPU协同工作,张量并行、流水线并行、专家并行、数据并行的混合并行策略设计极其复杂,通信开销的优化直接决定了训练的吞吐效率。训练过程中还要时刻应对数值不稳定、loss突增、梯度爆炸等问题,需要精细的学习率调度、梯度裁剪、混合精度策略来保驾护航。更不用说集群的故障容错——单卡故障、网络波动是大规模训练的常态,没有完善的checkpoint机制和故障恢复能力,一次中断就可能损失数百万的算力成本。 再者是模型本身的训练痛点。单说MoE,专家负载均衡就是公认的老大难问题:如果路由总把token分给少数几个热门专家,就会导致部分专家过拟合、大部分专家训练不足,模型效果直接腰斩,必须设计专门的负载均衡损失和路由策略来约束。除此之外,长上下文训练的稳定性、对齐阶段的“能力遗忘”与“对齐税”平衡、RLHF/DPO的奖励模型设计,每一个都是需要反复试错的深坑。 最后是算力与成本的现实门槛。一次完整的预训练,算力成本动辄数千万甚至上亿元,每一次架构调整、超参改动,都要烧掉巨额的算力去验证。很多设计在纸面上成立,在小模型上验证有效,放大到大规模就完全不是那么回事,而这种放大验证的成本,是绝大多数团队根本承担不起的。
写在最后:感谢开源的力量
所以最后还是想说,感谢开源。它打破了技术壁垒的高墙,让我们这些普通的技术从业者,不必只靠论文里的只言片语去猜测,也不必困在商业黑盒之外徘徊,可以有幸一睹当代顶尖大模型的核心技术真容,触摸到最前沿架构设计的脉搏。这大概也是开源技术最动人的地方——站在巨人的肩膀上,我们才能看得更远。
夜雨聆风