Qwen2.5-Coder 技術報告詳細解讀

知乎:Xode

鏈接:https://zhuanlan.zhihu.com/p/721189499

  • 0. 前文

  • 1. 模型架構

  • 2. Tokenizer

  • 3. 預訓練

    • 3.1 數據

    • 3.2 訓練

  • 4. Post-Training

    • 4.1 數據

    • 4.2 訓練

  • 5. 去除數據集汙染

  • 6. 評估

  • 7. 總結

[!tip]

  • 這不是技術報告的翻譯,全文人工撰寫

  • 這隻是個人的解讀,如果有問題歡迎探討

  • 筆者能力有限,全文可能難以深入到特別細節的理論研究,也不會有什麼公式推導

  • 全篇會儘量按照報告的行文順序來寫解讀,但中間可能會有些許變化,也不一定會提到報告中每個地方

0. 前文

這一篇 Coder 的技術報告風格不同於 Math,整體樣式都不太一樣:Math 的樣式模板和 Qwen Technical Report、Qwen2 Technical Report 保持一致、作者名也都是按照字典序排序的,可以說是更加「正統」;Coder 的樣式模板和 DeepSeek 的樣式非常類似,有區分一作,並且通訊也只有  Junyang Lin 大佬一個人(只有 2.5 的兩篇開始出現了通訊)。

此外,和 Math 模型類似,Code 模型的名稱也出現了變動。最開始的一代(只出現在技術報告中、沒開源)和 1.5 代的時候,代碼模型都叫做 Code-Qwen,但到了 2.5 代就改名成了 Qwen2.5-Coder,這樣做更加強調了 Math 和 Code 這類專有模型都同屬於 Qwen Series。不過不同於 Math-Qwen 改名為 Qwen-Math,Code-Qwen 變成了 “Coder”,據他們的說法是更加契合於 1.5 時提及的”結對編程”場景。

這次的模型開源了 1.5B、7B 和 32B(coming soon)三個尺寸,沒有 Qwen 正統和 Math 模型的最大號 72B。但我個人的感覺是,Code 這樣的模型在進化的過程中要麼逐漸變小、便於專有化部署和高頻使用;要麼逐漸把能力整合入主力模型,例如 GPT、Claude 和最近的 DeepSeek,所以我覺得專門的一個更大的代碼模型也確實沒必要。

1. 模型架構

既然都強化了 Qwen2.5 Series 的概念,Qwen2.5-Coder 自然在模型架構上不會有什麼分別。報告中只提了 1.5B 和 7B 的架構,但按理來說 32B 的模型架構應該也不會和 Qwen2.5-32B 有區別。不過 Qwen2.5-32B 和 Qwen1.5-32B 不完全一樣,中間 FFN 的升維維度從 256 * 107 變成了 256 * 108、也就是向 1024 對齊取整了,不太清楚這裡面具體的考量。

這裏值得一提的是,Qwen 從 2 代開始加深小模型的深度、削減寬度,並且增大 FFN 升維的維度,這大概也是現在的一個 LLM 主流認識:同樣參數下,窄而深的模型會比寬而淺的更好;增大 FFN 維度可以增強表徵能力。例如以往 FFN 維度都是 4 倍(對於 LLaMA 這樣的 GLU 來說,同參數規模要乘以 2/3,也就是 8/3 倍),但 Qwen 現在都是差不多 16/3 倍,拋開為了 128 或者 256 對齊,0.5B 的模型大概是 16/3 倍、1.5B 大概是 6 倍、7B 大概是 17/3 倍,就連 32B 也是 16/3 倍,只有最大號的 72B 大約是 11/3 倍。具體的數字應該是內部有不同的消融測試才定下來的,但整體趨勢是比最開始的 LLaMA2 FFN 升維更高。

2. Tokenizer

tokenizer 也和 2 代開始的保持一致,只是 Qwen2.5-Coder 開始為了 FIM(fill-in-the-middle,根據上下文補全代碼)任務引入了一些特殊 token,如下:

印象中沒記錯的話,這些specialtokens和常用的FIM保持一致,沒有特別的自定義。

也順便提一下 FIM 大概是什麼任務,其實和字面義顯示的一樣,就是用於代碼補全,給定上下文代碼填中間的空:

<< 上文代碼 >>
{{ 中間需要補全的部分 }}
<< 下文代碼 >>

由於生成模型只能看到前文,沒有雙向的注意力,因此我們會把下文放到前面,類似於:

<< 上文代碼 >>
<< 下文代碼 >>
{{ 告知模型,開始生成中間需要補全的部分 }}

具體還會有別的設計,我們後文展開講。例如這裏的 <|repo_name|> 和 <|file_sep|> 就是為了 repo 級別的代碼補全而定義的。

3. 預訓練

3.1 數據

數據組成

  • 來源:主要是 Github,總共 92 種編程語言

  • 組成:

    • 代碼-文本關聯數據(Text-Code Grounding Data):用了 fastText(沒錯,還是它,報告提到更大的分類模型沒有帶來顯著收益)做了一個由粗到細的多級過濾,每級都會篩掉一批數據,從 582B tokens 篩選四級降到 118B tokens,但具體如何做的質量過濾並沒有細說。同時說明了更高質量但更少的數據也比更多、但質量更良莠不齊的數據效果更好。

    • 合成數據:只說了用 Code-Qwen1.5 進行合成,其餘細節沒有提及。可以看得出來,隨著優質自然語料逐漸消費殆盡、模型性能提升生成效果更好,合成數據從 Post-training 走向 Pre-training 的趨勢很明顯了。

    • 數學數據:這算是一個共識,數學和代碼數據可以相互促進提升能力,具體來說,數學的數據部分是直接拿的 Qwen2.5-Math 的過來用的

    • 文本數據:這裏用了 Qwen2.5 的文本數據,並且去除掉了含有代碼的部分

數據混合

數據混合算是預訓練中比較重要的一個問題,因為消融對比的成本非常高,所以如何調配得當、找到一個能力最優平衡點也是一門藝術。

Qwen2.5-Coder 也只做了三個實驗來對比文本:代碼:數學的混合比例,並且沒有提是多大的模型、多少數據量,可能也是成本太高吧。

具體來說,最後選用的代碼:文本:數學的數據比例是7:2:1,畢竟LLM還是一個文本模型,要是text比例不對的話,對通用能力甚至coding能力都會有損失。

最終這一部分的數據量達到了 5.2T tokens,但後面還有一個 repo 級別的預訓練數據,有 300B,因此預訓練數據總量在 5.5T Tokens。

3.2 訓練

可以看到,圖中的Stage1和Stage2就是預訓練的兩階段步驟,並且都是在Qwen2.5上直接訓練的。這裏除了常規的nexttokenprediction訓練以外,還有一個訓練任務就是前面提到的FIM任務,兩階段的FIM訓練設計相同,但細節上有所差異。

文件級別的預訓練

這個就是比較常規的預訓練階段,如前文所說,這裏用了 5.2T tokens 的數據,並且相較於 Qwen2.5-Math 的 4K 最大長度,這裏翻了一倍、用的是 8K 的最大長度,這和任務特性不同有關。

這裏的 FIM 訓練格式如下:

其實和我們前面看到的一樣,格式很簡單:

<|fim_prefix|>  # 特殊 token,用於告訴模型從這裏開始往後是「代碼上文」
{code_pre}    # 代碼上文具體內容
<|fim_suffix|>  # 特殊 token,用於告訴模型從這裏開始往後是「代碼下文」
{code_suf}    # 代碼上文具體內容
<|fim_middle|>  # 特殊 token,用於告訴模型從這裏開始往後是「代碼中間的部分」
{code_mid}    # 代碼中間具體內容
<|endoftext|>   # eot

換句話說,FIM 這種格式可以理解為 <|fim_middle|> 及之前都是傳統 SFT 的 query 部分,往後就是 response 部分,不過預訓練的時候應該和普通 next token prediction 一樣,計算的是全部 token 的損失、沒有 mask,但無論如何,eot 始終是最重要的,FIM 的一些具體訓練細節可以見 OpenAI 的這一篇 paper。

https://arxiv.org/abs/2207.14255

Repo 級別的預訓練

Repo 級別的預訓練,對於代碼模型來說是比較關鍵的,還是拿代碼補全這個下遊場景舉例,我們的項目往往是跨文件的,這個 class 定義在某個文件、那個 func 又在另一個文件之類的。因此,我們希望模型在補全的時候,能夠感知到整個倉庫的信息——說白了就是把整個 repo 都喂給模型。此時長上下文能力就顯得尤為重要了,因此 Qwen2.5-Coder 做了如下兩件事:

  • 加入了新的長代碼數據,大約有 300B,上下文長度從 8K 提升到了 32K

  • 調整了 RoPE 基頻(base frequency),從 1e4 擴大到了 1e6,並且延續 Qwen2 的做法,應用 YaRN 可以外推到 132K 上下文

這裏簡單講一下 RoPE 基頻(base)是幹嘛的,base 越大、模型長距離衰減越緩,通俗地說,模型能關注到更遠的文本,所以長文本能力就更好;

那麼為什麼不一味地放大這個 base 呢?具體細節可以看 RoPE,簡單來說就是 base 越大,相鄰 token 越難以區分,就需要更充分的訓練。

至於 YaRN 是一種應用廣泛的外推手段,這裏就不細講了,感興趣也有很多相關資料可以閱讀。

這裏也有 FIM 訓練任務,格式如下:

相比於文件級別的FIM,Repo級別的FIM主要是多了前面的部分:

<|repo_name|>{repo_name}  # <|repo_name|> 用於告訴模型這裏開始是倉庫名
              # {repo_name} 是倉庫名本身
<|file_sep|>{file_path1}  # <|file_sep|> 用於告訴模型這裏開始是新的文件
              # {file_path1} 是第一個文件路徑
{file_content1}       # {file_content1} 是第一個文件的具體內容
...             # 循環 sep、path、content
<|fim_prefix|>{code_pre}... # 和文件級別的 FIM 一致

4. Post-Training

4.1 數據

數據仍然是訓練的最重要部分,相比於預訓練,報告詳細說明了 Post-Training 中的數據處理部分。

數據構成

這一部分其實在原報告中是放在了 training policy 部分的,我們提到前面來說。在後面的訓練中會講到,模型分了兩階段訓練,即”由粗到細”的訓練方式。

  • 第一階段:數千萬個低質量但多樣化的指令數據,沒提具體數目

  • 第二階段:數百萬個高質量指令數據,沒提具體數目

多編程語言數據識別

這裏原文說的是 “nearly 100 programming languages”,應該對應的是前文的 92 種編程語言,不太可能有新增的語種。這裏提到兩點值得注意:

  • 語種分類:分類器用的是微調後 CodeBERT,微軟 2020 的一篇工作,可以簡單理解為支持代碼的 Bert。原本的 Bert 用的預訓練語言只有 6 種語言(Go、Java、JS、PHP、Python、Ruby),Qwen2.5-Coder 這裡應該是自己構造了數據做微調分類。並且用到了一個”垃圾桶”分類,把無關的(這裏是代碼含量過少的文本)全部往這個”垃圾桶”分類里塞,一方面符合下遊需求,另一方面也能提升模型在常規分類上的表現。

  • 長尾丟棄:長尾數據——或者說小眾編程語言數據——會被隨機地丟棄一部分,但主流語言數據不會動。不清楚這樣做的具體緣由,猜測可能是長尾數據總量過多過雜的話,也相當於主流編程語言的噪聲?

合成數據

這裏合成數據用的是打分過濾的方式,報告提到了三種組成部分:

  • GitHub:這裏主要從 GitHub 採集了大量無監督代碼語料,然後針對 1K tokens 以內的語料,用 LLM 生成 query(沒提具體什麼模型),再用代碼 LLM 生成 response(也沒提具體什麼模型),最後用 LLM 打分過濾(還是沒提具體什麼模型)

  • 第二種:這裏原文寫得我看得有些迷糊,我把原文貼在這裏,給出一個我自己的理解:與第一種構造方式相反,這裏是先給定代碼片段,然後讓模型生成 response,再根據 response 生成 query。至於 response 的內容,我猜可能是代碼功能描述、代碼擴展續寫之類的任務

    Given the code snippets of different programming languages, we construct an instruction dataset from the code snippets. To increase the diversity of the instruction dataset. Conversely, we first generate the answers from the code. Then we use the LLM scorer to filter the low-quality to obtain the final triplet.

  • 開源數據集:就是把一些公開數據集拿過來用

多編程語言指令數據

不得不說,這部分我個人感覺有點抽像,可能是 Agent System 本身都是這樣的吧(

這部分主要是說用 Agent 構建指令數據,我讀得也不算特別明白,先講一下報告中的流程:

  1. Language-Specific Intelligent Agents,構建語言專精的 Agent。這裏沒提具體 Agent 是怎麼構建的,只籠統地說是通過對應語言數據訓練而來的

  2. Collaborative Discussion Protocol,協作討論協議。這一步非常有 Agent 的感覺了,講的主要是類似於 Multi-Agent 式的交互,也就是說這些 Agents 會交互、協作並生成新的 code 指令

  3. Adaptive Memory System,自適應記憶系統。說實話,這個部分我不太理解 “adaptive” 的定語含義,看上去說的就是一個動態的、可更新的 Agent Memory,可能對應”動態”?這一部分的作用主要是存儲生成過的樣本、避免重覆

  4. Cross-Lingual Discussion,跨語言討論。更加沒理解了,原文提到這是一種新的知識蒸餾技術(a novel knowledge distillation technique),但大概就是不同語言的 Agent 之間可以相互交流自己語言的特性知識,可能這也算一種”傳授”?

  5. Synergy Evaluation Metric,協同評估指標。這個部分比較有意思,對應上文,這裏開發了一個新的指標來量化模型內不同編程語言之間的知識共享和協同程度,但不清楚具體是怎麼做的

  6. Adaptive Instruction Generation,自適應指令生成。這裏的 “adaptive” 應該也是對應的 “動態”,意思就是這裏會根據不同語種的 Agents 之間的跨語言能力差異來動態生成新指令

由於報告沒有給出一個 Agent 的具體例子,接下來我大概按照自己的理解描繪一個流程示例:

  1. 首先我們假設這個 Agent System 有一個強而有力的中央 Controller,可以操控、調整 Agents

  2. 假設我們假設構建了三個 Agents,分別是精通 Python 的 、精通 Java 的  和精通 C++ 的

  3. 協作:三個 Agents 在 Controller 的指引下,開始提出問題,假設  提出了一個問題說要計算一組數字的平均數, 提出可以直接用 sum(list) / len(list) 提出可以使用泛型  提出可以說用指針 *lst 直接操作內存,總之就是各自基於語言特性提出問題和方案

  4. 更新記憶:每個 Agents 都記住曾經提出過一個計算平均數的問題,於是後面避免和它重覆

  5. 知識蒸餾:這一步確實沒太理解,可能是特性分享?例如  可能會分享動態類型, 會分享 GC 機制, 會分享模板元編程。但還是不清楚這裏的”分享”和”傳授”是怎麼操作的

  6. 評估+生成:利用開發的跨語言指標進行評估,例如判斷出當前模型(不是 Agent)在 C++ 上偏弱,例如模型只會用 Python 和 Java 進行快速排序,但是還不會用 C++,那麼接下來就會補充更多 C++ 快排的指令數據。這點很好理解,缺什麼補什麼,主要是不清楚這個跨語言評估是如何做的

總的來說,這一部分還是缺乏比較多的細節,期待後面有可能能有更深入的資料和探討。

Checklist 模式的指令數據打分

這部分就比較常規了,什麼是 Checklist 模式呢?其實就類似於英語考試那樣的評分表,比如語言表述是否流暢(0-3分)、是否有語法錯誤(0-3分)、用詞高級程度(0-3分)……這裏 Qwen2.5-Coder 主要從 9 個維度打分:

  • 問答一致性:檢查指令的基本正確性,例如前後要求是否一致

  • 問答相關性:是否和計算機領域相關

  • 問答難度

  • 是否含有代碼

  • 代碼正確性:檢查指令答案的代碼是否正確

  • 碼風是否良好

  • 代碼的清晰度

  • 代碼註釋

  • 指令的教育價值

最後再給每個部分賦上權重,權重是預先定義好的,例如一致性佔 20%、其餘佔 10% 等等,具體權重報告里沒有提。

這種打分方式直接且有效,很多工作都這麼做,但其實也有一些問題:

  • 打分的維度如何確定?一般這種打分維度都是人為預先定義的,也就是天然存在 bias,當然也可以說這種 bias 本身就是一種 preference,所以可能問題不太大

  • 不同維度的權重如何確定?同上,權重也都是人為定義的

  • 誰來打分?如果用人工打分倒是沒太大問題,就類似於向人類偏好對齊了,只是實際操作中由於成本,肯定都是由另外的 LLM 來進行打分,最多人工輔助給示例或審查。報告里也沒提是否引入人工、如果用 LLM 用的是什麼模型(一般是更大更強的模型,例如 GPT-4o),所以我們不得而知。

用於代碼驗證的多語言沙箱

沙箱(Sand Box),簡單來說就是一個隔離環境,可以執行代碼,很多 LLM 執行代碼都是通過沙箱來完成的。為什麼要用隔離環境呢?一方面是怕 LLM 自身犯傻,要是直接寫出一個死循環代碼跑在本機上那全部都崩了;另一方面也是防禦攻擊注入,比如通過某些高超的越獄手段讓 LLM 寫出並執行了一些侵入性或者破壞性代碼,沙箱就能把這些攻擊和本機隔離開。

這裏沙箱沒有特別多要提的,驗證上也都是一些主流手段,就不贅述了。不過這裏提到是有引入人工定期更新代碼示例庫的,推測上面打分也是類似。

4.2 訓練

相較於數據部分的充實,這裏訓練只是提了兩個部分。

由粗到細的兩階段訓練

數據量前文提過了,具體訓練細節上也沒講特別多,主要有:

  • 粗階段:沒提什麼,大概是最普通的 SFT,沒特別多 trick

  • 細階段:提到了拒絕采樣,拒絕采樣算是老朋友了,可以看我前一篇 [Qwen2.5-Math 技術報告詳細解讀](Qwen2.5-Math 技術報告詳細解讀.md) 中有一些看法,這裏也稍微提一下,大概就是一種過濾低分指令、選擇高分指令的模式

混合訓練

這裏提到,SFT 數據往往都偏短,於是引入 FIM 數據來構造長上下文的指令數據。為什麼 FIM 更長?讓我們回憶一下前文,假設我們有一條 FIM 數據,長這樣:

<< 代碼上文 - 3K tokens >>
<< 代碼下文 - 4K tokens >>
{{ 期望填充的部分 }}

那麼我們轉為 SFT 數據,例如最簡單的加上一句”請根據上下文代碼填充中間內容”:

query: << 代碼上文 - 3K tokens >>\n<< 代碼下文 - 4K tokens >>\n\n請根據上下文代碼填充中間內容。
response: {{ 期望填充的部分 }}

這麼一來,就非常容易利用到 FIM 數據的特性,構建出長上下文的 SFT 數據了。當然報告中沒提構建的具體方法和例子,我這裏只是隨便打個比方。

不過我們該如何構建 FIM 數據呢?能不能直接隨機的 mask 掉代碼里的一部分來構建 FIM 數據呢?其實這是不太合適的,因為在實際的下遊任務和場景中,更多的是例如語句級別、函數級別這樣相對完整的一個小塊來補全的,如果訓練的時候是殘缺的一部分就有 gap 了。

例如我們希望:

...

# 註釋代表 mask 掉
# def add(foo, bar):
#     return foo + bar

# result = add_foo_bar(5, 10)

...

這裏的 add 函數要是能被完整 mask、作為待填充目標就很合適;但我們不希望:

...

# 星號代表 mask 掉
**************ar):
    return foo + bar

result = add_foo_bar(5, 10)

...

因此,我們需要”成塊”、相對完整地摘取代碼中的某一部分。如果學過編譯的朋友應該就能猜到接下來的做法了,沒錯,就是利用 AST(抽像語法樹)。通俗來說,這個 AST 的確就是一棵樹,一層一層地節點就像代碼中一層一層地包裹、深入,那麼我們可以找到某個節點、整個地摘除作為填充預測目標,這樣就保持完整性了。

這裏用到了一個好用的工具——tree-sitter,印象里 2020 年微軟的 GraphCodeBERT 工作就有用到。這個簡單理解就是一個解析器,支持非常多的編程語言,可以很容易的得到一份代碼的 AST,並且魯棒性也很好,一些有錯誤的代碼也能解析。

最後報告提到,混合的數據裡面 FIM 只佔一小部分。

5. 去除數據集汙染

和 Qwen2.5-Math 類似,Qwen2.5-Coder 當然也要去除數據集汙染(Decontamination)。簡單來說,就是排除掉訓練集中可能被汙染、和測試集相同或高度相似的部分,避免模型在測試集或者榜單上分數虛高。

Qwen2.5-Coder 用的是 10-gram 去重。

6. 評估

又到了評估部分,這一部分重要、但沒太多可說的,大概就是 Qwen2.5-Coder 在各大榜單上取得了優異成績。

先說一些常見的榜單,例如 HumanEval 和 MBPP,這些暫時沒有像 GSM8K 那樣飽和,但主要問題是語種單一、只有 Python 問題。當然後面 Qwen2.5-Coder 也測了 MultiPL-E,一個涵蓋八種主流語言的 benchmark,但還是遠少於模型訓練的 92 種語言,所以我們也不知道中間提到的跨語言能力對齊的 Agent 部分具體效用如何,希望有一個更加完善的 benchmark。

這裏 Qwen2.5-Coder 還測了數學能力,也的確佐證了代碼和數學能相互促進。那麼具體怎麼樣呢?因為分了很多個表格,大概就是:

  • Base 模型:同規模比 Qwen2 數學略強,弱於 Qwen2.5-Math 和 Qwen2-Math(Qwen2.5 主系列沒放基座結果)

  • SFT 模型:這個由於沒有經過 RL,因此比 Math 系列弱了不少;但是卻比經過 RL 的 Qwen2-Instruct 強(Qwen2.5 主系列沒放 7B 以下的性能)

還測了一下 Text2SQL 的性能,也很不錯。

此外,相較於前兩天出的 Yi-Coder 如何呢?對比了一下兩家官方自己報告的數據,在相信兩家自己報告的性能的情況下(應該也沒什麼問題,不至於),Qwen2.5-Coder 還是更強的。我猜測沒對比的原因是因為 LLM 在榜單上的性能本身就有浮動,各家報告性能真實性應該是可以保證的,但是沒複現他人的模型情況下,一般還是較為謹慎報告的。

儘管我在評估部分提的比較簡略,但是報告原文是寫得很詳實的,感興趣的可以去原文詳細閱讀。

7. 總結

相比於 Qwen2.5-Math 的技術報告,這一篇 Coder 的技術報告更加工程化,但很可惜還是缺少一些關鍵細節,希望後面能有更多的資料補充。

以上均是我通讀後的個人解讀,可能有不少疏忽或錯誤,歡迎指出和探討。