DeepSeek-Coder 核心架构剖析

From LLM Guide

本文内容为中文原文;Article content is the original Chinese source, and this page does not provide a translation.

DeepSeek-Coder 核心架构剖析

🔙 返回 14.1-DeepSeek 家族总览

本文是对 DeepSeek-Coder 技术报告(arXiv:2401.14196)中架构与工程决策的深度解读,聚焦「为什么这样设计」以及「工程落地时的权衡」。


1. 总体定位: 从零训练的开源代码模型

DeepSeek-Coder 是 DeepSeek 家族的第一个重要开源模型,其核心定位非常清晰:用开源、可商用的代码模型打破闭源模型(Codex、GPT-3.5)在代码智能领域的垄断。

与后续 DeepSeek-Coder-V2 选择「继续预训练通用基座」不同,DeepSeek-Coder 选择了「从零训练」路线:

维度 DeepSeek-Coder DeepSeek-Coder-V2
基座 从零训练 DeepSeek-V2 中间Checkpoint
数据配比 87% 代码 + 13% 自然语言 60% 代码 + 10% 数学 + 30% 自然语言
编程语言 87 种 338 种
上下文长度 16K 128K
最大规模 33B 稠密 236B MoE(21B 激活)
架构 DeepSeek-LLM(标准 Transformer) DeepSeek-V2(MLA + DeepSeekMoE)

设计动机节点: 为什么第一版选择从零训练而非继续预训练? 2023 年底 DeepSeek 的通用基座模型(DeepSeek-LLM)刚刚发布,其通用能力虽然不错,但还没有达到可以作为「万能基座」的程度。在代码这个垂直领域,从零训练可以更精确地控制数据配比(87% 代码)、分词器(32K 词汇量专门面向代码)和架构细节(如 GQA 在 33B 上的应用)。此外,从零训练能够避免通用语料中的「非代码模式」对代码能力的潜在干扰。这个决策在工程上虽然成本更高,但为 DeepSeek 积累了从零构建代码模型的完整经验——这些经验后来被用于优化 Coder-V2 的数据工程流水线。


2. 数据工程: 仓库级语料库的构建

2.1 拓扑排序: 从文件级到仓库级的质变

DeepSeek-Coder 数据工程中最具创新性的决策是「仓库级预训练」。传统代码模型(如 StarCoder、CodeLlama)将代码文件视为独立的文本片段进行预训练,这导致模型虽然擅长单文件内的代码生成,但在处理跨文件引用时表现不佳。

DeepSeek-Coder 的解决方案分为三步:

  1. 依赖解析: 使用正则表达式提取文件间的 import/include/using 关系,构建仓库内的依赖图。
  2. 拓扑排序: 对依赖图进行拓扑排序,确保被依赖的文件排在依赖它的文件之前。对于循环依赖(如 A import B, B import A),采用「最小入度」策略选择下一个文件,而非要求入度为零。
  3. 序列拼接: 将排序后的文件按顺序拼接成训练样本,每个文件开头添加路径注释。

架构细节节点: 拓扑排序的「最小入度」变体是一个务实的工程选择。标准拓扑排序要求无环图(DAG),但真实代码库中循环依赖非常常见——Python 的相互 import、C 的头文件循环引用等。如果强制要求无环,要么需要人为打破循环(引入偏差),要么会丢失大量真实数据。最小入度策略的优雅之处在于:它不需要修改数据,而是通过「先处理依赖最少的节点」来近似拓扑序。当遇到 A<->B 的循环时,A 和 B 的入度都是 1,算法会任意选择一个(通常取决于遍历顺序),将其入度降为 0 后处理另一个。虽然这不是严格的最优解,但在统计意义上,它保留了绝大多数的「依赖在前、使用在后」的顺序信息。

2.2 仓库级去重: 保持结构完整性

去重是大语言模型数据预处理的标准步骤,但 DeepSeek-Coder 在「去重粒度」上做出了关键创新:

  • 文件级去重(StarCoder 等): 逐文件计算 minhash,重复文件被删除。风险:一个仓库中部分文件被删、部分保留,导致 import 了不存在模块的「断链」代码。
  • 仓库级去重(DeepSeek-Coder): 将整个仓库的连接代码视为单个样本进行 minhash 去重。要么整个仓库保留,要么整个删除。

工程落地视角: 这个决策的代价是「去重不够精细」——如果一个大仓库中只有 10% 的文件与另一个仓库重复,文件级去重可以只删除这 10%,而仓库级去重会删除整个仓库。但论文认为,保持仓库结构的完整性带来的收益超过了这种代价。实际上,GitHub 上相同功能的仓库克隆(如 fork)在去重时本就应该被整体删除,所以仓库级去重在这个场景下反而是更合理的。

2.3 质量筛选的三层防御

DeepSeek-Coder 的数据质量控制采用了三层过滤:

层级 方法 过滤目标 数据保留率
第一层: 规则过滤 行长度、字母比例、HTML 文本比、JSON/YAML 长度 minified 代码、数据文件、配置文件 32.8%
第二层: 编译器检查 用对应语言的编译器/解释器检查语法 语法错误代码 未公开
第三层: 质量模型 训练一个质量打分模型 可读性差、模块化低的代码 未公开

三层防御的累积效果是将原始 GitHub 数据压缩到约 798GB、6.03 亿个文件。虽然论文没有给出每层的具体保留率,但「规则过滤 32.8%」这个数字说明,仅第一层就过滤掉了约 2/3 的数据——GitHub 上的原始数据质量确实堪忧。


3. 训练策略的精细化调优

3.1 FIM 比率的消融实验

DeepSeek-Coder 对 FIM(Fill-in-the-Middle)训练策略进行了系统的消融实验,这是代码预训练领域最早的系统性分析之一:

配置 HumanEval-FIM HumanEval(生成) MBPP
0% FIM(纯 NTP) 较低 较高 较高
50% PSM 中等 中等 中等
50% MSP 中等 略低于 PSM 略低于 PSM
100% FIM 最高 最低 最低

实验结论:

  • FIM 和代码生成能力之间存在明确的权衡(trade-off)。
  • 50% PSM 率优于 MSP(Masked Span Prediction)策略。
  • 最终选择 50% PSM 率作为平衡方案。

设计动机节点: 为什么 PSM 优于 MSP? PSM(Prefix-Suffix-Middle)保持了「前缀在前、后缀随后」的直觉顺序,与程序员阅读代码时的认知流一致;而 MSP(Suffix-Prefix-Middle)将后缀放在前缀之前,是一种更「反直觉」的排列。虽然 MSP 在理论上可能增强模型处理任意顺序上下文的能力,但实验表明这种增益不足以弥补对自然顺序学习的干扰。这个发现也解释了为什么后续的代码模型(包括 StarCoder2、CodeLlama 和 DeepSeek-Coder-V2)都默认采用 PSM 模式。

3.2 三阶段学习率调度

DeepSeek-Coder 采用了 DeepSeek-LLM 提出的三阶段学习率策略:

LR(t)={LRmax⋅ttwarmupt<twarmupLRmaxtwarmup≤t<0.8TLRmax⋅1100.8T≤t<0.9TLRmax⋅1100.9T≤t≤T\text{LR}(t) = \begin{cases} \text{LR}_{max} \cdot \frac{t}{t_{warmup}} & t < t_{warmup} \\ \text{LR}_{max} & t_{warmup} \leq t < 0.8T \\ \text{LR}_{max} \cdot \sqrt{\frac{1}{10}} & 0.8T \leq t < 0.9T \\ \text{LR}_{max} \cdot \frac{1}{10} & 0.9T \leq t \leq T \end{cases}

这种「阶梯式衰减」而非「平滑余弦衰减」的设计有其独特的考量:

  • 第一阶段(warmup): 线性增长避免初始阶段的梯度爆炸。
  • 第二阶段(恒定期): 让模型在稳定的较大学习率下充分学习数据的主要模式。
  • 第三阶段(第一次衰减): 学习率降至约 31.6%,进入「精调」阶段,学习更细粒度的模式。
  • 第四阶段(第二次衰减): 学习率降至 10%,进行最终的微调和收敛。

译者注: 这种多阶段调度与常见的 cosine 衰减相比,在超大规模训练(2T token)中展现出更好的收敛稳定性。cosine 衰减在整个训练过程中持续降低学习率,可能导致模型在早期就陷入局部最优;而阶梯式衰减在大部分时间保持较高学习率,给予模型更多「逃离」局部最优的机会。

3.3 长上下文扩展的务实策略

DeepSeek-Coder 的长上下文扩展策略非常简洁:

  • 修改 RoPE 参数:缩放因子从 1 增加到 4,基频从 10000 改为 100000。
  • 继续训练 1000 步,批量大小 512,序列长度 16K。
  • 理论上可处理 64K,但经验上 16K 内最可靠。

这个策略的成本极低:仅 1000 步 × 512 × 16K ≈ 8.2B token,占 2T 总训练量的 0.4%。

局限与风险节点: 论文坦诚地承认「16K 内最可靠」,这意味着线性缩放 RoPE 虽然理论上支持 64K,但实际效果在超过 16K 后显著下降。这是所有位置编码外推方法的共同局限——无论 YARN、NTK-aware 还是线性插值,模型在训练时未见过的长度上都存在「注意力稀释」问题。后续的 DeepSeek-Coder-V2 通过 YARN 和两阶段渐进训练(32K → 128K)才将可靠上下文长度真正扩展到 128K。


4. 模型架构: 标准 Transformer 的实用调优

4.1 架构概览

DeepSeek-Coder 的架构基本沿用 DeepSeek-LLM 的设计,是一种标准的Encoder-Only Transformer,主要特点:

  • SwiGLU 激活函数: 替代传统 ReLU/GELU,提供更强的非线性表达能力。
  • RoPE 位置编码: 替代绝对位置编码,更好地处理相对位置关系。
  • GQA(33B 版本): Grouped-Query-Attention,组大小为 8,将 KV 头数从 56 降到 7,显著减少推理时的 KV Cache。
  • FlashAttention v2: 加速注意力计算,减少显存占用。
模型 1.3B 6.7B 33B
隐藏层维度 2048 4096 7168
层数 24 32 62
注意力头数 16 32 56
注意力类型 MHA MHA GQA(8 组)
中间层维度 5504 11008 19200
批量大小 1024 2304 3840

架构细节节点: GQA 在 33B 版本上的引入是一个关键的效率优化。标准 MHA 中,Query、Key、Value 各有 nhn_h 个头,推理时需要存储 2×nh×dhead×L2 \times n_h \times d_{head} \times L 的 KV Cache。GQA 将 nhn_h 个 Query 头分成 ngn_g 组,每组共享一组 KV 头,KV Cache 降为 2×ng×dhead×L2 \times n_g \times d_{head} \times L。DeepSeek-Coder 33B 的 nh=56n_h = 56,组大小为 8,意味着 KV 头数为 56/8=756/8 = 7,KV Cache 减少了 8 倍。这个优化在 33B 规模上尤为关键——如果没有 GQA,batch size 稍大就会导致显存溢出。

4.2 Scaling Law 的实践

DeepSeek-Coder 的批量大小和学习率遵循 DeepSeek-LLM 的 scaling law:

模型规模 批量大小 最大学习率 学习率比值
1.3B 1024 5.3e-4 -
6.7B 2304 4.2e-4 0.79x
33B 3840 3.5e-4 0.83x

随着模型规模增加,批量大小和学习率都增加,但学习率的增加速度低于批量大小。这符合大模型训练中的经验规律:更大的模型需要更大的 batch 来获得稳定的梯度估计,但学习率不能同比例增长,否则会导致优化不稳定。


5. 性能分析: 效率与效果的平衡

5.1 参数效率的突出表现

DeepSeek-Coder 最引人注目的结果是「小模型超越大模型」:

  • DeepSeek-Coder-Base 6.7B vs CodeLlama-Base 34B: 参数量只有 20%,但 HumanEval 平均 44.7% vs 41.0%,MBPP 60.6% vs 55.2%。
  • DeepSeek-Coder-Base 1.3B vs StarCoderBase 16B: 参数量只有 8%,但 HumanEval 平均 28.3% vs 28.0%,MBPP 46.2% vs 42.8%。

这种「参数效率」来自三个因素的协同:

  1. 数据质量: 2T token 的高质量项目级语料,远优于 CodeLlama 的 500B。
  2. 数据规模: 2T vs 500B,4 倍的训练量让模型「见过更多模式」。
  3. 架构细节: SwiGLU + GQA 的组合在同等参数量下提供了更强的表达能力。

5.2 跨文件能力的仓库级预训练验证

表 7 中的消融实验是论文中最有力的证据:

语言 仓库级预训练 EM 文件级预训练 EM 差值
Python 16.14% 16.02% +0.12%
Java 17.72% 16.64% +1.08%
TypeScript 14.03% 13.23% +0.80%
C# 16.23% 14.48% +1.75%

Java 和 C# 的收益最明显,这与这两种语言的「强类型 + 显式 import」特性有关——跨文件依赖在 Java/C# 中更为突出,因此仓库级预训练带来的结构信息更有价值。Python 的收益较小,可能是因为 Python 的动态类型和隐式导入使得跨文件依赖更难解析,也更难建模。


6. 从代码模型到通用模型: v1.5 的启示

DeepSeek-Coder-v1.5 7B 是一个容易被忽视但极具启示性的实验。它证明了:

  1. 代码预训练不是「单向增强」: 在 DeepSeek-LLM-7B 上继续预训练代码数据,不仅增强了代码能力,还显著提升了数学推理(GSM8K 从约 15% 提升到 62.4%)和自然语言理解(MMLU 从约 30% 提升到 49.1%)。
  2. 通用能力可以通过「代码 + 自然语言」混合预训练来增强: v1.5 的数据配比(70% 代码 + 30% 自然语言/数学)比原始 Coder(87% 代码 + 13% 自然语言)更加均衡,结果显示它在保持代码能力的同时大幅提升了通用能力。
  3. 为 DeepSeek-Math 铺平道路: v1.5 是 DeepSeek-Math 的直接基座。v1.5 实验验证了「从代码模型出发增强数学能力」的可行性,这是 DeepSeek-Math 选择 DeepSeek-Coder-Base-v1.5 作为初始化基座的核心依据。

谱系与影响节点: DeepSeek-Coder → DeepSeek-Coder-v1.5 → DeepSeek-Math 这条技术路线展示了 DeepSeek 的「渐进式增强」策略:先训练一个强大的代码模型,然后通过调整数据配比将其扩展为「代码 + 通用」模型,最后在此基础上专注于数学能力。这种策略的优势在于每一步都建立在前一步的验证之上,降低了整体研发风险。相比之下,同期其他团队(如 Meta 的 CodeLlama)选择直接在 Llama-2 上继续预训练代码数据——虽然起点更高(通用能力更强),但失去了「从零构建代码模型」过程中积累的数据工程经验。