DeepSeek-Coder: When the Large Language Model Meets Programming - The Rise of Code Intelligence

DeepSeek-Coder:当大语言模型遇上编程——代码智能的崛起

作者/机构: Daya Guo*1, Qihao Zhu∗1,2, Dejian Yang1, Zhenda Xie1, Kai Dong1, Wentao Zhang1, Guanting Chen1, Xiao Bi 1, Y. Wu1, Y.K. Li1, Fuli Luo1, Yingfei Xiong2, Wenfeng Liang1
1 DeepSeek-AI
2 Key Lab of HCST (PKU), MOE; SCS, Peking University


A1 主要贡献

本文旨在解决开源代码模型与闭源模型之间的性能差距。尽管大型语言模型(LLMs)极大地改变了软件开发中的代码智能,但强大的闭源模型由于其专有性,限制了广泛的研究和开发。

为应对此挑战,本文提出了DeepSeek-Coder系列模型,这是一个包含从1.3B到33B参数规模的开源代码模型系列。其主要研究目标和贡献如下:


图1 | DeepSeek-Coder的性能


A3 背景知识与数据构建

DeepSeek-Coder的训练数据集由87%的源代码、10%的英文代码相关自然语言语料和3%的中文代码无关自然语言语料构成。英文语料来自GitHub的Markdown和StackExchange,用于增强模型对代码相关概念的理解及处理库使用、错误修复等任务的能力。中文语料则由高质量文章组成,旨在提升模型的中文理解能力。本节将概述代码训练数据的构建过程,该过程包括数据爬取、基于规则的过滤、依赖解析、项目级去重和质量筛选,如图2所示。


图2 | 数据集创建流程

2.1. GitHub数据爬取与过滤

数据收集与初步过滤。我们收集了2023年2月之前在GitHub上创建的公共代码仓库,并仅保留了87种编程语言(如表1所示)。为了减少待处理的数据量,我们应用了与StarCoder项目【索引25, Starcoder: may the source be with you!, 2023, arXiv preprint arXiv:2305.06161】类似的过滤规则,初步筛除低质量代码。通过这些规则,数据总量减少至原始大小的32.8%。具体过滤规则如下:
* 首先,我们过滤掉平均行长超过100个字符或最大行长超过1000个字符的文件。
* 其次,移除字母字符占比低于25%的文件。
* 除了XSLT编程语言外,我们还过滤掉在前100个字符中出现字符串 "<?xml version=" 的文件。
* 对于HTML文件,我们考虑可见文本与HTML代码的比例,保留可见文本占比至少为20%且不少于100个字符的文件。
* 对于通常包含更多数据的JSON和YAML文件,我们仅保留字符数在50到5000之间的文件,这有效移除了大多数数据密集型文件。

2.2. 依赖解析

项目级依赖解析与文件排序。以往的代码大模型【索引5, Evaluating large language models trained on code, 2021, arXiv preprint arXiv:2107.03374; 索引25, Starcoder: may the source be with you!, 2023, arXiv preprint arXiv:2305.06161; 索引30, Codegen: An open large language model for code with multi-turn program synthesis, 2022, arXiv preprint arXiv:2203.13474; 索引36, Code llama: Open foundation models for code, 2023, arXiv preprint arXiv:2308.12950】主要在文件级源代码上进行预训练,忽略了项目内不同文件间的依赖关系,导致模型在处理整个项目级代码场景时难以有效扩展。因此,我们在此步骤中利用同一代码仓库内文件间的依赖关系。具体做法是,首先解析文件间的依赖关系,然后按照确保每个文件所依赖的上下文都位于其前方的顺序排列这些文件。我们仅考虑文件间的调用关系,并使用正则表达式提取它们(例如Python中的"import"、C#中的"using"和C中的"include")。这种对齐方式使我们的数据集能更准确地反映真实的编码实践和结构,从而增强模型处理项目级代码场景的实用性。

拓扑排序算法。算法1描述了对同一项目内文件列表进行依赖分析的拓扑排序过程。该算法首先初始化一个邻接表graphs和一个记录入度的字典inDegree。然后遍历文件对以识别依赖关系,并相应更新graphsinDegree。接着,算法会识别整个依赖图中的不连通子图。对于每个子图,算法采用一种改进的拓扑排序方法:它选择入度最小的节点,而非标准方法中入度为零的节点,这使其能够处理图中的循环。选中的节点被添加到结果列表results中,并且其连接节点的入度会减少。这个过程持续进行,直到为每个子图生成一个拓扑排序序列。算法最终返回这些排序序列的列表,每个序列中的文件被拼接成一个训练样本。为了保留文件路径信息,在每个文件的开头会添加一个注释来指明其路径。

1: procedure T O P O L O G I C A L S O R T(files)
2:     graphs ← {} ⊲ Initialize an empty adjacency list
3:     inDegree ← {} ⊲ Initialize an empty dictionary for in-degrees
4:     for each file in files do
5:         graphs[file] ← []
6:         inDegree[file] ← 0
7:     end for
8:
9:     for each fileA in files do
10:        for each fileB in files do
11:            if H A S D E P E N D E N C Y(fileA, fileB) then ⊲ If fileA depends on fileB
12:                graphs[fileB].append(fileA) ⊲ Add edge from B to A
13:                inDegree[fileA] ← inDegree[fileA] + 1 ⊲ Increment in-degree of A
14:            end if
15:        end for
16:    end for
17:
18:    subgraphs ← getDisconnectedSubgraphs(graphs) ⊲ Identify disconnected subgraphs
19:    topologicalOrders ← []
20:    for each subgraph in subgraphs do
21:        result ← []
22:        while length(result) ≠ NumberOfNodes(subgraph) do
23:            node ← argmin({inDegree[node] | node ∈ subgraph and node ∉ result})
24:            for each neighbor in graphs[node] do
25:                inDegree[neighbor] ← inDegree[neighbor] − 1
26:            end for
27:            result.append(node)
28:        end while
29:        topologicalOrders.append(result)
30:    end for
31:
32:    return topologicalOrders
33: end procedure

2.3. 项目级去重

为保证项目结构完整性而采用项目级去重。近期的研究表明,对训练数据集进行去重可以显著提升大语言模型的性能。Lee等人【索引24, Deduplicating training data makes language models better, 2022, Proceedings of the 60th Annual Meeting of the Association for Computational Linguistics (Volume 1: Long Papers)】指出语言模型训练语料库中存在大量近似重复内容,移除长的重复子串可以提升LLM性能。Kocetkov等人【索引21, The stack: 3 tb of permissively licensed source code, 2022, Transactions on Machine Learning Research】对训练数据应用了近似去重方法,取得了显著的性能提升,并强调近似去重是实现代码基准任务竞争性性能的关键预处理步骤。我们的数据集中也采用了近似去重,但与先前工作的不同之处在于,我们在代码仓库(repository)级别进行去重,而非文件级别。因为后者可能会过滤掉仓库内的某些文件,从而破坏仓库的结构。具体来说,我们将仓库级别拼接的代码视为单个样本,并应用相同的近似去重算法,以确保仓库结构的完整性。

2.4. 质量筛选与数据净化

多重手段提升数据质量。除了应用2.1节中提到的过滤规则,我们还使用编译器和质量模型,结合启发式规则,进一步过滤掉低质量数据,包括存在语法错误、可读性差和模块化程度低的代码。表1提供了源代码的统计摘要,共包含87种语言,详细列出了每种语言的磁盘大小、文件数量和百分比。总数据量为798 GB,包含6.03亿个文件。

测试集信息污染的防范。为确保我们的代码训练数据不被测试集信息污染(这些信息可能存在于GitHub上),我们实施了一个n-gram过滤过程。该过程会移除任何匹配特定标准的码段。具体来说,我们过滤掉了包含来自HumanEval【索引5, Evaluating large language models trained on code, 2021, arXiv preprint arXiv:2107.03374】、MBPP【索引2, Program synthesis with large language models, 2021】、GSM8K【索引8, Training verifiers to solve math word problems, 2021, arXiv preprint arXiv:2110.14168】和MATH【索引18, Measuring mathematical problem solving with the math dataset, 2021, arXiv preprint arXiv:2103.03874】等来源的文档字符串、问题和解决方案的文件。过滤规则如下:如果一段代码包含与测试数据中任何10-gram字符串相同的字符串,则将其从训练数据中排除。如果测试数据包含短于10-gram但不少于3-gram的字符串,我们则使用精确匹配的方法进行过滤。


表1 | 所选编程语言的已清理训练数据摘要。


A2 方法细节

3.1. 训练策略

3.1.1. 下一词元预测(Next Token Prediction)

基本训练目标。我们模型的第一个训练目标是下一词元预测。在此过程中,将多个文件拼接成一个固定长度的条目。然后,使用这些条目来训练模型,使其能够根据给定的上下文预测下一个词元。

3.1.2. 中间填充(Fill-in-the-Middle)

引入FIM以增强代码补全能力。我们模型的第二个训练目标是中间填充。在代码预训练场景中,常常需要根据给定的上下文和后续文本生成相应的插入内容。由于编程语言中存在特定的依赖关系,仅依赖下一词元预测不足以学习这种中间填充能力。因此,一些方法【索引3, Efficient training of language models to fill in the middle, 2022, arXiv preprint arXiv:2207.14255; 索引25, Starcoder: may the source be with you!, 2023, arXiv preprint arXiv:2305.06161】提出了中间填充(Fill-in-the-Middle, FIM)的预训练方法。该方法将文本随机分为三部分,然后打乱这三部分的顺序并用特殊字符连接,旨在训练过程中融入填空预训练任务。在FIM方法中,采用了两种不同的模式:PSM (Prefix-Suffix-Middle) 和 SPM (Suffix-Prefix-Middle)。在PSM模式下,训练语料按前缀后缀中间的顺序组织,使中间部分位于前缀和后缀之间。相反,SPM模式将各部分排列为后缀前缀中间,呈现出不同的结构挑战。这些模式有助于增强模型处理代码中各种结构排列的能力,为高级代码预测任务提供了强大的训练框架。

FIM超参数消融实验。为了确定FIM方法中各种超参数的有效性,我们进行了一系列消融实验。
* 实验设置:实验采用DeepSeek-Coder-Base 1.3B作为模型架构,并专注于训练数据集中的Python子集以简化实验流程。主要目标是评估FIM技术的功效,使用了HumanEval-FIM基准【索引13, Incoder: A generative model for code infilling and synthesis, 2022, arXiv preprint arXiv:2204.05999】。该基准专门针对Python的单行FIM任务,其中HumanEval解决方案中的一行代码被随机遮蔽,测试模型预测缺失行的能力。我们假设PSM模式与传统的下一词元预测目标可能存在细微差异,因为PSM涉及重新排列原始文本的顺序,可能影响模型的学习动态。因此,我们在四种不同配置下实施PSM模式的FIM:0% FIM率、50% FIM率、100% FIM率,以及50% MSP率。MSP(Masked Span Prediction)策略,最初在T5【索引34, Exploring the limits of transfer learning with a unified text-to-text transformer, 2023】中引入,它会隐藏多个文本片段并训练模型重建这些片段。根据CodeGen2.5【索引31, Codegen2: Lessons for training llms on programming and natural languages, 2023】,MSP可能比PSM更能提升FIM性能,因此我们将其纳入比较分析。
* 结果:实验结果如图3所示。虽然模型在100% FIM率下于HumanEval-FIM上表现最佳,但此配置也导致了最弱的代码补全能力,表明FIM能力与代码补全能力之间存在权衡。此外,我们观察到在50% PSM率下,模型的表现优于MSP策略。为了在FIM效率和代码补全熟练度之间取得平衡,我们最终选择50% PSM率作为首选的训练策略。


图3 | 使用FIM目标的有效性。

FIM实现细节。在我们的实现中,我们为这项任务引入了三个哨兵词元。对于每个代码文件,我们首先将其内容分为三个部分,表示为prefix, middle, 和 suffix。使用PSM模式,我们按以下方式构建训练样本:


我们遵循Bavarian等人【索引3, Efficient training of language models to fill in the middle, 2022, arXiv preprint arXiv:2207.14255】的原始工作,在打包过程之前于文档级别实施FIM方法,FIM率为0.5,并采用PSM模式。

3.2. 分词器(Tokenizer)

BPE分词器。在分词过程中,我们使用HuggingFace Tokenizer库,在我们的训练语料库的一个子集上训练了字节对编码(BPE)分词器,如Sennrich等人【索引38, Neural machine translation of rare words with subword units, 2015, arXiv preprint arXiv:1508.07909】所述。最终,我们使用了一个词汇量大小为32,000的分词器。

3.3. 模型架构

多规模模型架构。我们开发了一系列具有不同参数量的模型,以满足多样化的应用需求,包括1.3B、6.7B和33B参数的模型。这些模型基于与DeepSeek大语言模型(LLM)【索引10, Deepseek llm: Scaling open-source language models with longtermism, 2024, arXiv preprint arXiv:2401.02954】相同的框架构建。每个模型都是一个仅解码器(decoder-only)的Transformer,并结合了Su等人【索引39, Roformer: Enhanced transformer with rotary position embedding, 2023】描述的旋转位置嵌入(Rotary Position Embedding, RoPE)。值得注意的是,DeepSeek 33B模型集成了分组查询注意力(Grouped-Query-Attention, GQA),组大小为8,以提升训练和推理效率。此外,我们采用FlashAttention v2【索引9, Flashattention-2: Faster attention with better parallelism and work partitioning, 2023】来加速注意力机制的计算。我们模型的架构细节总结在表2中。

3.4. 优化

优化器与学习率策略。我们遵循DeepSeek LLM【索引10, Deepseek llm: Scaling open-source language models with longtermism, 2024, arXiv preprint arXiv:2401.02954】的做法,使用AdamW【索引26, Decoupled weight decay regularization, 2019】作为优化器,其中$\beta_1$和$\beta_2$的值分别为0.9和0.95。我们根据DeepSeek LLM中建议的缩放定律调整批处理大小和学习率。对于学习率调度,我们实施了一个三阶段策略,包括2000步的预热,并将最终学习率设置为初始学习率的10%。值得注意的是,每个阶段的学习率都按前一阶段学习率的$\sqrt{\frac{1}{10}}$比例缩减,遵循DeepSeek LLM中建立的指导方针。

3.5. 训练环境

高效的训练框架与硬件。我们的实验是使用HAI-LLM【索引19, Hai-llm: An efficient and lightweight tool for training large models, 2023, URL https://www.high-flyer.cn/en/blog/hai-llm】框架进行的,该框架以其训练大型语言模型的高效和轻量化而闻名。此框架集成了多种并行策略以优化计算效率,包括张量并行【索引22, Reducing activation recomputation in large transformer models, 2023, Proceedings of Machine Learning and Systems, 5】、ZeRO数据并行【索引35, Zero: Memory optimizations toward training trillion parameter models, 2020, SC20: International Conference for High Performance Computing, Networking, Storage and Analysis】和PipeDream流水线并行【索引29, Pipedream: Generalized pipeline parallelism for dnn training, 2019, Proceedings of the 27th ACM Symposium on Operating Systems Principles】。我们的实验使用了配备NVIDIA A100和H800 GPU的集群。在A100集群中,每个节点配置8个GPU,通过NVLink桥接成对互连。H800集群也采用类似配置,每个节点包含8个GPU,通过NVLink和NVSwitch技术组合互连,确保节点内高效的数据传输。为了促进A100和H800集群中节点间的无缝通信,我们采用了以高吞吐量和低延迟著称的InfiniBand互连技术。这一设置为我们的计算实验提供了强大而高效的基础设施。

表2 | DeepSeek-Coder的超参数。

3.6. 长上下文(Long Context)

扩展上下文窗口。为了增强DeepSeek-Coder处理扩展上下文的能力,特别是在项目级代码处理等场景下,我们重新配置了RoPE【索引39, Roformer: Enhanced transformer with rotary position embedding, 2023】参数以扩展默认的上下文窗口。遵循先前的实践【索引6, Extending context window of large language models via positional interpolation, 2023, arXiv preprint arXiv:2306.15595; 索引20, Things i’m learning while training superhot, 2023, https://kaiokendev.github.io /til#extending-context-to-8k】,我们采用了线性缩放策略,将缩放因子从1增加到4,并将基频从10000更改为100000。模型额外进行了1000步的训练,批处理大小为512,序列长度为16K。学习率与预训练最后阶段保持一致。理论上,这些修改使我们的模型能够处理多达64K词元的上下文。然而,经验观察表明,模型在16K词元范围内提供最可靠的输出。未来的研究将继续完善和评估长上下文适应方法,旨在进一步提升DeepSeek-Coder在处理扩展上下文时的效率和用户友好性。

3.7. 指令微调(Instruction Tuning)

构建指令遵循模型。我们通过使用高质量数据对DeepSeek-Coder-Base进行基于指令的微调,开发了DeepSeek-Coder-Instruct。这些数据包含有帮助且公正的人类指令,采用Alpaca指令格式【索引41, Stanford alpaca: An instruction-following llama model, 2023, https://github.com/tatsu-lab /stanford_alpaca】构建。为了区分每个对话轮次,我们使用了一个特殊的分隔符词元<|EOT|>来表示每个片段的结束。在训练中,我们使用带有100步预热的余弦学习率调度,初始学习率为1e-5。我们还使用了400万词元的批处理大小,总共训练了20亿词元。

多轮对话能力示例。图4展示了一个使用DeepSeek-Coder-Instruct 33B的示例,该示例是一个构建贪吃蛇游戏的多轮对话场景。我们首先要求模型使用pygame编写一个贪吃蛇游戏,模型成功创建了一个可以无bug运行的基本游戏。为了改进游戏,我们进一步要求在左上角添加一个计分系统。模型随后引入了一个“score”变量和一个“display_score”函数,并解释了如何集成这些功能。这个例子说明了DeepSeek-Coder-Instruct在多轮对话设置中提供完整解决方案的能力。更多案例可见附录A。


图4 | DeepSeek-Coder-Instruct 33B在多轮对话中的响应示例。

5. 从通用大语言模型继续预训练

提升自然语言与数学能力。为了进一步增强DeepSeek-Coder模型的自然语言理解和数学推理能力,我们从通用语言模型DeepSeek-LLM-7B Base【索引10, Deepseek llm: Scaling open-source language models with longtermism, 2024, arXiv preprint arXiv:2401.02954】出发,在2万亿词元上进行了额外的预训练,得到了DeepSeek-Coder-v1.5 7B。此次预训练我们专门使用了表9中列出的数据源。与DeepSeek-Coder不同,DeepSeek-Coder-v1.5在预训练阶段仅采用下一词元预测目标,上下文长度为4K。

表9 | DeepSeek-Coder-v1.5 7B预训练的数据来源


A4 实验

实验环境

实验结果

4.1. 代码生成
4.2. 中间填充代码补全
4.3. 跨文件代码补全
4.4. 基于程序的数学推理
5. DeepSeek-Coder-v1.5 性能对比

A5 结论

本文介绍了DeepSeek-Coder系列模型,这是一组专为编码设计的LLM,包含1.3B、6.7B和33B三种参数规模。这些模型在精心策划的项目级代码语料库上进行训练,并利用“中间填充”预训练目标来增强代码插入能力。模型上下文窗口扩展至16,384个词元,显著提升了处理大型代码生成任务的效率。

评估结果显示,该系列中最先进的DeepSeek-Coder-Base 33B模型在多项标准测试中超越了现有的开源代码模型。DeepSeek-Coder-Base 6.7B模型尽管规模较小,其性能却与34B参数的CodeLlama相当,证明了预训练语料的高质量。通过高质量指令数据微调后,DeepSeek-Coder-Instruct 33B模型在一系列编码相关任务中超越了OpenAI的GPT-3.5 Turbo。

为了进一步提升模型的自然语言理解能力,团队基于DeepSeek-LLM 7B检查点进行了额外的预训练,创造了DeepSeek-Coder-v1.5。该模型在保持高水平编码性能的同时,展现了更强的自然语言理解能力。这支持了作者的观点:最有效的代码LLM应建立在强大的通用LLM之上,因为理解人类自然语言指令对于执行编码任务至关重要。未来,团队致力于开发并开源基于更大规模通用LLM的、更强大的代码模型。


A6 附录

A. 与DeepSeek-Coder-Instruct的聊天案例

案例展示。附录展示了两个与DeepSeek-Coder-Instruct交互的案例。一个涉及创建数据库和执行数据分析的多轮对话,另一个则围绕使用模型解决一个LeetCode示例问题。

案例一:数据库创建与分析。在图5的场景中,用户首先指示模型使用Python构建一个学生数据库并随机插入10条信息。接着,在第二轮对话中,用户要求模型分析学生的年龄分布。从图中可以看出,模型能够生成无bug的完整代码,并附有详细的解释。


图5 | 构建数据库和数据分析的示例。

案例二:解决域外LeetCode问题。在图6的场景中,通过一个域外的LeetCode竞赛问题进一步评估了模型的能力。该问题发布于2023年11月,晚于数据收集时间,因此不属于模型的训练数据。结果表明,该模型在解决超出其训练分布范围的问题方面表现出色。


图6 | 解决LeetCode问题的示例。

B. DeepSeek-Coder-Base训练过程中的基准曲线

训练性能监控。图7展示了DeepSeek-Coder-Base模型在训练阶段的基准曲线。为了进行验证,使用了一个精心挑选的训练语料库子集,包含8000个代码文件。该子集经过特意选择,以确保样本的多样性和代表性,这对于准确评估模型能力至关重要。这些模型的性能指标在图7的最后两个子图中详细展示,清晰地呈现了它们在整个训练过程中的效能。


图7 | DeepSeek-Coder-Base训练过程中的基准曲线。