DeepSeek-Coder 核心架构剖析
本文是对 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 的解决方案分为三步:
- 依赖解析: 使用正则表达式提取文件间的 import/include/using 关系,构建仓库内的依赖图。
- 拓扑排序: 对依赖图进行拓扑排序,确保被依赖的文件排在依赖它的文件之前。对于循环依赖(如 A import B, B import A),采用「最小入度」策略选择下一个文件,而非要求入度为零。
- 序列拼接: 将排序后的文件按顺序拼接成训练样本,每个文件开头添加路径注释。
架构细节节点: 拓扑排序的「最小入度」变体是一个务实的工程选择。标准拓扑排序要求无环图(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 提出的三阶段学习率策略:
这种「阶梯式衰减」而非「平滑余弦衰减」的设计有其独特的考量:
- 第一阶段(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 各有 个头,推理时需要存储 的 KV Cache。GQA 将 个 Query 头分成 组,每组共享一组 KV 头,KV Cache 降为 。DeepSeek-Coder 33B 的 ,组大小为 8,意味着 KV 头数为 ,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%。
这种「参数效率」来自三个因素的协同:
- 数据质量: 2T token 的高质量项目级语料,远优于 CodeLlama 的 500B。
- 数据规模: 2T vs 500B,4 倍的训练量让模型「见过更多模式」。
- 架构细节: 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 是一个容易被忽视但极具启示性的实验。它证明了:
- 代码预训练不是「单向增强」: 在 DeepSeek-LLM-7B 上继续预训练代码数据,不仅增强了代码能力,还显著提升了数学推理(GSM8K 从约 15% 提升到 62.4%)和自然语言理解(MMLU 从约 30% 提升到 49.1%)。
- 通用能力可以通过「代码 + 自然语言」混合预训练来增强: v1.5 的数据配比(70% 代码 + 30% 自然语言/数学)比原始 Coder(87% 代码 + 13% 自然语言)更加均衡,结果显示它在保持代码能力的同时大幅提升了通用能力。
- 为 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 上继续预训练代码数据——虽然起点更高(通用能力更强),但失去了「从零构建代码模型」过程中积累的数据工程经验。