一個 19 格的鐘面,兩個數字落在同一格 左邊寫著 params: 6、10010 / 10010 和 758 + 467。右邊是一個 19 格的鐘面,指標指向第 10 格,那裡疊著兩塊強調色的數字牌 5 和 6;鐘面周圍散落著輸入序列裡的其他數字牌。 8 7 0 4 0 9 0 1 5 6 params: 6 10010 / 10010 758 + 467 = 1225

GitHub 上有一個叫 AdderBoard 的排行榜,比的是誰能做出最小的 Transformer,讓它把兩個十位數加對1。榜首那一份提交只有兩百來行 numpy 程式碼,我們把它下載下來,用排行榜自帶的驗證指令碼跑了一遍:

$ python verify.py submission_6p_handcoded.py
Parameters (unique): 6
Architecture: 1L Qwen-derived decoder, d=2, 1h, hd=2, ff=2 (fixed Q, folded norm)
...
Results: 10010/10010 correct (100.00%)
Time: 23.9s (419 additions/sec)
Status: QUALIFIED (threshold: 99%)

一萬零十道題,全對;而整個模型只有 6 個引數。作為對比,ChatGPT 這一類大模型的引數以千億計。

看到這個結果,很自然會冒出幾個疑問:Transformer 到底是個什麼結構,“引數”指的是什麼?6 個數字怎麼就能把十位數加對?榜上還分了“訓練出來的”和“手寫的”兩張表,訓練榜的第一名要 36 個引數,為什麼差這麼多?我們先把 Transformer 本身講清楚(原問題裡的 “transfer”,我們按 Transformer 理解),再順著排行榜一層層往下看。

一、Transformer 的基本結構

從“預測下一個字”說起

不管是聊天機器人還是這個排行榜上的小模型,Transformer 做的事情都只有一件:給它一串符號,它預測下一個符號。這些符號叫 token(詞元),可以是一個字、一個詞,在加法這件事上就是一個數字 0–9。

預測完一個,把它接到序列末尾,再預測下一個,一直重複到答案寫完。這種“邊寫邊把自己寫的東西當輸入”的方式叫自迴歸(autoregressive)。所以“讓 Transformer 做加法”的意思是:把兩個加數寫成一串數字餵給它,它一位一位地吐出和。十位數加十位數,和最多十一位,於是要預測 11 次。

那麼,每一次預測,模型內部發生了什麼?

一步預測裡的四個部件

一個最簡單的 Transformer 由四樣東西串起來。下面這張圖畫的是 758 + 467 這道題,模型已經寫出了個位 5、正在預測十位的那一步(數值取自後文那個 6 引數模型;輸入為什麼要倒著寫、中間為什麼有一串 0,第三、五節再講)。

Transformer 預測一個數字的一步 上方是 758 加 467 的輸入序列:起始 0、a 倒序 8 5 7、九個 0、b 倒序 7 6 4、結尾 0,以及已經寫出的個位 5。下方四張卡片依次是詞嵌入、注意力、MLP 和輸出頭。注意力從卡片連回序列裡的 a 的十位 5 和 b 的十位 6,各給 0.45 的權重。輸出頭給出最高分的數字 2,寫回序列末尾的空格,再進行下一步。 輸入序列(預測十位的這一步) 一步預測:四個部件 0 8 5 7 0 … 0 ×9 7 6 4 0 … 0 5 ? a = 758 倒序 b = 467 倒序 已寫出 ← 這一步要寫的 詞嵌入 "5" 查表 [999.98, −5] 注意力 5、6 各 0.45 8、7 各 0.05 MLP 滿十沒有? 滿了,減掉 98.6 輸出頭 0–9 各打一分 最高:2 四個部件的結果都加在同一個向量上(殘差流),不是一個替換一個。 寫出的 2 接到序列末尾,整條序列再算一遍,得到百位;11 步後答案寫完。
圖 1 · Transformer 每一步只做一件事:看完前面的序列,給下一個數字打分;寫下最高分的那個,再看一遍

詞嵌入(embedding)。神經網路只會算數,不認識符號,所以第一步是查表:每個 token 對應一個固定長度的向量,比如 “5” 對應 [999.98, −5]。向量的長度叫模型寬度 dd,大模型裡是幾千,這個榜上常見的是 2 到 7。

注意力(attention)。這是 Transformer 的標誌部件。每個位置都可以“往回看”序列裡所有更早的位置,決定從誰那裡取多少資訊。具體做法是:當前位置算一個查詢向量 Q,每個更早的位置各算一個鍵向量 K 和一個值向量 V;Q 和每個 K 做點積,得到一個“相關程度”的分數,經過 softmax 變成加起來等於 1 的權重,再用這組權重把各位置的 V 加權平均。圖裡,預測十位時,模型把 0.45 的權重分給第一個加數的十位 5,0.45 分給第二個加數的十位 6,各 0.05 分給兩個個位 8 和 7。對加法來說,注意力就是“把同一位上的兩個數字找出來”的那隻手。

前饋網路(MLP)。注意力只負責搬運和混合資訊,真正的非線性計算交給每個位置各自的一個小網路:先乘一個矩陣升維,過一個啟用函式(ReLU、SiLU 之類),再乘一個矩陣降回來。在加法裡,判斷“這一位滿沒滿十”就靠它。

輸出頭。最後把向量和詞表裡 10 個數字各自的向量做點積,得到 10 個分數,分數最高的就是預測結果。很多模型讓輸出頭和詞嵌入共用同一張表,這叫權重繫結(weight tying),後面會反覆出現。

還有兩個不顯眼但必需的零件。一是殘差連線:每個部件的輸出不是替換原來的向量,而是加到它上面,所以可以把整個過程看成一條“殘差流”,各部件往上面累加東西。二是歸一化(LayerNorm 或 RMSNorm),把向量的大小拉回一個固定範圍,防止數值越算越大。

位置從哪裡來

上面的注意力有一個漏洞:Q 和 K 的點積只看兩個向量的內容,不看它們在序列裡的位置。把序列打亂,每個位置得到的結果完全一樣。可加法偏偏最在乎位置,“5” 在個位還是在十位,意思差了十倍。

所以 Transformer 還要加位置編碼。2017 年的原始論文用一組固定的正弦波,加到每個位置的詞嵌入上2;現在的大模型(包括這個榜上幾乎所有小模型)多用 RoPE(旋轉位置編碼):把 Q 和 K 按所在的位置轉一個角度,位置 pp 轉 pωp\omega。兩個向量都轉過以後再做點積,結果只取決於它們相隔多少個位置,而不是各自在哪裡3。記住“RoPE 是一個按位置轉動的鐘”,第五節會用到。

引數是什麼

圖裡每個部件都離不開幾張矩陣:詞嵌入是一張 10×dd 的表,Q、K、V 各是一個矩陣,MLP 有兩三個矩陣,歸一化還有一組縮放係數。這些矩陣裡的每一個數都是引數;訓練,就是用梯度下降一點點調這些數,讓預測越來越準。

以訓練榜上一個 122 引數的模型為例(寬度 3,注意力頭的維度 4,MLP 中間層 3),引數是這樣分的4:

122 引數模型的引數都花在哪(d=3,注意力頭維度 4,MLP 中間層 3)
部件形狀引數
詞嵌入(與輸出頭共用)10 × 330
注意力 Q、K、V3 × 4,各一個36
注意力輸出投影 O4 × 312
Q、K 各自的歸一化4 + 48
三處歸一化(注意力前、MLP 前、輸出前)3 + 3 + 39
MLP(SwiGLU:門、升維、降維)3 × 3,三個27
合計122

RoPE 是固定的旋轉角度,不算引數。

表 1 · 一個最小的單層 Transformer,引數主要花在詞嵌入和注意力的幾個投影矩陣上

引數量是衡量模型大小最直接的尺子。GPT-2 最小的版本有 1.24 億個引數,這個榜上的模型比它小一百萬倍。那麼,用這把尺子量,能做十位數加法的 Transformer 最小可以多小? 這就是 AdderBoard 要回答的問題。

二、AdderBoard 的規則

起點:兩個程式設計智慧體的比賽

排行榜的維護者是 Dimitris Papailiopoulos。2026 年 2 月,他把同一段提示詞分別交給 Claude Code 和 Codex:訓練一個儘可能小的 Transformer,十位數加法的準確率至少 99%,不許把答案編碼進輸入,不許呼叫計算器,輸出必須自迴歸地生成5。

Claude Code 從 79.5 萬個引數開始一路往下掃,發現寬度 12(4176 個引數)完全學不會,寬度 16(6080 個引數)就能 100% 正確,最後交出一個兩層、寬度 16 的模型。Codex 第一輪交出的是 36.6 萬個引數;維護者把“最小化引數”的要求加重再跑一次,Codex 想出了一個辦法:把同一位上的兩個數字合成一個 token(第 3 位上的 3 和 7 記作 P37),詞表從十幾個漲到 114 個,序列短了一半,最後只用 1644 個引數。維護者的評價是,這樣做沒有違規,但為了“引數少”這個寫明的目標,犧牲了“通用”這個沒寫明的目標,“審美上有點不舒服”5。

文章發出去以後,很多人開始接力往下壓,於是有了這個排行榜。

兩張榜

榜分成兩類1:

  • 訓練榜:引數由通用的訓練演算法從資料裡學出來,梯度下降、進化搜尋都可以,但演算法本身不能是為加法專門設計的。
  • 手寫榜:引數是人用紙筆推出來、直接填進去的。它不證明模型學得會,只證明這個結構表達得了加法,算是一種構造性證明。

規則在防什麼

規則裡寫得最細的,是“什麼才算 Transformer 在做加法”:

  1. 必須至少有一層自注意力。
  2. 必須自迴歸:一位一位地生成,每一位生成後接回輸入。進位必須從這個過程裡“長出來”,不能在 Python 裡用一個變數把進位傳給下一步。
  3. forward() 必須是標準的“張量進、分數出”,裡面不能有按數位迴圈、存進位這類專門針對加法的控制流。
  4. 生成迴圈必須是通用的:換一套權重、換一個任務,同樣的解碼程式碼照樣能用。

一句話:算加法的必須是模型,不能是程式碼。在此之外,輸入怎麼寫(倒序、加分隔符、補零)可以自選,只要格式固定、不洩露答案;位置編碼如果是固定的(正弦、RoPE),不算引數;繫結在一起的權重只算一次。

驗證方法也是固定的:verify.py 先跑 10 個邊界用例(全 9 加全 9、一串連續進位之類),再跑 10000 道隨機題,隨機種子固定為 2025,準確率不低於 99% 才算合格。

題目定好了,可是在動手之前,還有一個更基礎的問題:一個只會“看前面、猜下一個”的模型,做加法難在哪裡?

三、加法對 Transformer 難在哪

三件事

列豎式做加法,我們其實在做三件事1:

  1. 對齊:把兩個數同一位上的數字找出來。
  2. 逐位相加:算這一位的和,以及滿不滿十。
  3. 傳遞進位:把低一位的進位帶到高一位。

前兩件 Transformer 都有現成的部件:對齊靠注意力,逐位相加靠 MLP。麻煩在第三件。進位可能一路傳下去:9999999999 + 1,個位的進位要連著翻過十位,一直傳到最高位。而模型是從左往右一個一個寫答案的。

現象:寫答案的方向和進位的方向

按我們平常的習慣,和的最高位寫在最前面。看 758 + 467 = 1225:

758 + 467 的兩種書寫順序 左邊按習慣高位在前,答案 1225 從千位寫起,而進位從個位一路向左傳到千位,與寫字方向相反。右邊把數字倒過來寫,答案寫成 5221,個位先寫,進位從左向右傳,與寫字方向相同。 高位在前:先寫 1 a b 和 進位 寫的順序 千 0 0 1 ① 百 7 4 2 ② 十 5 6 2 ③ 個 8 7 5 ④ 寫第 ① 位之前,要先把三次進位都算完。 進位從右往左走,寫字從左往右走。 低位在前:先寫 5 a b 和 進位 寫的順序 個 8 7 5 ① 十 5 6 2 ② 百 7 4 2 ③ 千 0 0 1 ④ 進位和寫字同一個方向。 寫十位時,個位的 5 已經寫在序列裡了。
圖 2 · 把數字倒過來寫,進位就和模型寫字的方向一致:每一位只需要看上一位剛寫下的結果

高位在前時,模型寫下第一個數字 “1” 之前,要先知道百位有沒有進位,而百位有沒有進位取決於十位,十位又取決於個位。也就是說,第一步就得在腦子裡把整條進位鏈算完;十位數加法裡,這條鏈最長有十節,全都要壓在一次前向計算裡完成。

低位在前時,情況反過來:先寫個位 5,再寫十位。寫十位的時候,個位已經寫在序列裡了,“個位有沒有進位”可以從剛寫下的 5 裡讀出來(8 + 7 = 15,寫出來的是 5,說明進了一位)。每一步只需要看當前這一對數字和上一位,進位隨著自迴歸一步步往前傳,正好借用了第 2 條規則要求的那個過程。

這件事早有人系統地研究過。維護者自己參與的論文《Teaching Arithmetic to Small Transformers》比較了多種資料寫法,結論是倒序輸出讓小模型學得又快又好6。Claude Code 在那次比賽裡第一次用不定長的 123+45= 格式時失敗了,換成補零到定長、結果倒序以後立刻成功5。

嘗試:用同一個模型對比兩種寫法

光看圖還不夠有說服力,我們自己訓了一下7。模型結構照搬訓練榜上那篇論文的程式碼(單層、RoPE、SwiGLU),寬度 8、688 個引數;資料格式除了數字順序,其餘完全相同:同樣的分隔 0,同樣補零到十位,同樣一次出 128 道隨機題。每種寫法用 3 個隨機種子各訓 1 萬步,在 CPU 上每次不到兩分鐘。

同一個模型,只改數字的書寫順序(訓練 1 萬步,每步 128 道題)
模型寫法整題全對每一位單獨看
688 引數
d=8,3 個種子
低位在前100%、100%、100%1000 步時就已全對
高位在前0%、0%、0%最高兩位約 90%,其餘多在 10%–40%
7336 引數
d=32
低位在前100%(1 個種子)1000 步時就已全對
高位在前27%、66%(2 個種子)每位 88%–100%,錯誤散在各位

模型結構取自 M10S-Transformer(單層,RoPE θ=3,SwiGLU,2 個查詢頭),學習率 0.01 餘弦退火;測試題是 verify.py 用種子 2025 生成的前 2000 道。

表 2 · 低位在前,688 個引數一千步就學會;高位在前,放大十倍也只對一部分

低位在前,三個種子都在第 1000 步(約 11 秒)時就把 2000 道測試題全部做對了。高位在前,三個種子訓完 1 萬步,一道整題也沒對:最高兩位能猜對九成左右,往下的數位大多隻比瞎猜(10%)好一點。把模型放大十倍到 7336 個引數,高位在前終於能學到每一位都有九成以上的正確率,可十一位要同時對,整題正確率只有 27% 和 66%;同樣大小的模型換成低位在前,一千步就全對。

高位在前的小模型卡在哪裡,我們沒有深究,從“只有最高兩位對”來看,它大概連每一位該看哪兩個數字都沒學會(這是推斷)。但結論很清楚:同樣的引數、同樣的資料量,只是把數字倒過來寫,就是學得會和學不會的區別。

榜上的每一個模型,無論訓練的還是手寫的,都用了低位在前的寫法。格式解決以後,剩下的問題就是:模型能壓到多小?

四、訓練榜:從 6080 到 36

現象:五週,兩條階梯

把兩張榜的榜首按時間連起來(日期取自倉庫的提交記錄),是兩條往下走的階梯:

AdderBoard 兩張榜第一名隨時間的變化 橫軸是 2026 年 2 月 19 日到 3 月 25 日,縱軸是引數個數的對數。訓練榜從 6080、1644 降到 311、234、89、62,3 月 25 日到 36;手寫榜從 2 月 24 日的 139 降到 36、12、10,3 月 25 日到 6。 兩張榜的第一名(引數個數,對數座標) 10 100 1000 10000 2月19日 2月24日 3月3日 3月25日 6080 · Claude Code 1644 · Codex(成對 token) 311 234 89 62 36 139 36 12 10 6* 訓練榜(強調色):變窄 → 低秩 → 繫結 → 圓弧嵌入 手寫榜(灰色):開榜兩天就壓到 36,之後只剩個位數可爭 Codex 第一輪交出 366,320 個引數,在圖的上方之外。日期為提交被收錄的日子;6* 的星號見第六節。
圖 3 · 五週裡,訓練榜從 6080 降到 36,手寫榜從 139 降到 6;後半程每省一個引數都更難

訓練榜從 Claude Code 的 6080 一路降到 36,手寫榜從開榜時的 139 降到 6。兩條線有一個共同點:越往後,每一步省下的引數越少,花的力氣越大。那麼,訓練出來的模型是靠什麼一步步變小的?

第一步:把模型變窄

最直接的辦法是減小寬度 dd。引數量大致隨 d2d^2 增長,寬度減半,引數少四分之三。但寬度不能無限減:Claude Code 發現寬度 12 一下子就完全學不會,社群裡也有好幾個人在 800 個引數附近看到準確率斷崖式下降1(斷崖的位置和具體結構有關,第三節那個用 RoPE 的 688 引數模型就沒有遇到)。早期幾支隊伍不約而同地停在了寬度 7。

再往下走,需要換一種思路:不是把矩陣變小,而是讓矩陣裡的數不再各自獨立。

第二步:讓引數共用

低秩分解。一個 7×7 的矩陣有 49 個數,如果把它寫成一個 7×3 矩陣乘一個 3×7 矩陣,只要 42 個;寬度越大省得越多。秩為 3 的分解是寬度 4 到 7 這一檔模型的主要手段,311 引數的模型靠它把寬度壓到了 41。

權重繫結。前面說過,詞嵌入和輸出頭可以共用一張表。同樣的思路可以推到注意力內部:讓 V 直接等於 K(tieKV),讓輸出投影等於 Q 的轉置(tieQO),讓幾處歸一化共用一組係數。122 引數的那個模型,就是這樣一步步減到 89、86、83 的4:

同一個 122 引數模型,靠共用引數逐步變小
改動省掉了什麼引數
基礎版只有詞嵌入和輸出頭共用122
MLP 中間層 3 → 2三個矩陣各少一列113
O = Qᵀ(tieQO)輸出投影不再獨立101
V = K(tieKV)值投影不再獨立89
兩處塊內歸一化共用少一組 3 個係數86
三處歸一化全部共用再少一組 3 個係數83

數字取自 M10S-Transformer 論文表 1,公式為 95 + 9×(MLP 中間層) − 12×tieKV − 12×tieQO − 3 或 6。

表 3 · 每繫結一處,模型就少一塊獨立的矩陣;代價是剩下的引數要身兼數職

把整張表換成一個公式。到 36 引數這一檔,詞嵌入那 30 個數也被拿掉了:10 個數字不再各有一個自由的向量,而是等間距地排在一段圓弧上(第三個座標恆為 0),整張表只由半徑、起始角、間隔角 3 個引數決定。K 也不再是獨立的矩陣,而是 Q 旋轉一個角度(1 個引數),MLP 的降維矩陣是升維矩陣轉置後再轉一個角度4。這些都是“結構上的先驗”:人知道數字之間是有順序的、等距的,就直接把這個形狀寫進模型,不讓它自己去學。

第三步:模型越小,越難訓練

引數省下來了,代價是訓練變得非常難。36 引數這一系列的作者把過程寫成了一篇論文,裡面有幾組數字值得細看4。

頓悟。這些小模型的訓練曲線很特別:訓練損失很早就降到接近 0,測試準確率卻在很長一段時間裡停在接近 0,然後某一刻突然跳到接近 100%。這叫 grokking(頓悟)8。122 個引數時,10 個隨機種子裡有 4 個能在 20 萬步內頓悟;減到 89 個引數,從頭訓練 15 次,一次也沒有。

多階段微調。解決辦法是接力:第一階段從頭訓練,拿到一個 50%–85% 準確率的模型;第二階段從它出發,換小一點的學習率、大一點的批次再訓;一直接到第四階段,89 引數的模型才到 100%。從一個只錯一題的模型出發再微調,24 次裡 24 次都能頓悟。

進位幽靈。四個獨立訓練出來、各只錯一題的 89 引數模型,錯的是同一道題,寫出的錯答案也一模一樣:6,028,846,396 + 90,949,567,正確答案 6,119,795,963,四個模型都寫成 5,119,795,963。這道題的進位是“有、有、無、有、無、有、無、有、無、無”交替出現的,模型在一個本該截斷進位的位置,讓一點殘餘的進位訊號漏了過去。把這道題加進訓練批次,50 步梯度更新就修好了。作者的結論是:這不是容量不夠,而是隨機出題時這種進位模式太少見,訓練資料沒覆蓋到。

定向微調。順著這個思路,作者乾脆在評測集上找出模型做錯的題,把它們混進訓練批次反覆訓,修一輪、再找、再修。83 引數的模型修了 4 輪、累計約 170 道題才清零;榜上這個 36 引數模型的提交說明裡寫的是“2 輪,累計 70 道錯題”4。這裡要注意,用來找錯題的評測集(種子 42)和官方驗證用的種子 2025 是分開的,所以官方驗證仍然是沒見過的題;但論文自己也承認,定向微調證明的是這個結構裝得下正確答案,不是它能自己學會。

我們用官方指令碼復跑了這個 36 引數模型:10010 道題全對,用時 47.7 秒7。

訓練榜到這裡已經很擠了,每少一個引數都要配一套複雜的訓練流程。可是手寫榜的榜首隻有 6 個引數,比 36 少了六分之五。不靠訓練、直接把數填進去,6 個引數是怎麼做加法的?

五、手寫榜:6 個引數怎麼做加法

我們把 6 引數模型的程式碼逐行讀了一遍,又在每一步列印了模型內部的數值9。它其實是 10 引數版本的“記賬改版”,計算過程一模一樣,下一節再說差在哪裡10。整個模型寬度只有 2,一層,一個注意力頭。還是用 758 + 467 這道題,看它怎麼一位位算出 1225。

輸入的寫法

輸入一共 31 個 token:

位置 0 1 … 10 11 … 19 20 … 29 30
[0] a 倒序(10 位) 9 个 0 b 倒序(10 位) [0]
758 + 467:
0 8 5 7 0 0 0 0 0 0 0 0 … 0 7 6 4 0 0 0 0 0 0 0 0

和訓練榜常用的“中間墊兩個 0”不同,這裡在兩個加數之間墊了 9 個 0。於是 a 的第 ii 位在位置 1+i1+i,b 的第 ii 位在位置 20+i20+i,同一位上的兩個數字恰好相隔 19 個位置。這個 19 是後面一切的關鍵。

詞嵌入:一個座標存數字

寬度為 2,每個 token 只有兩個座標。數字 dd 的嵌入是:

e(d)=[ 1000−0.001 d2,  −d ]e(d) = \big[\,1000 - 0.001\,d^2,\ \ -d\,\big]

第二個座標直接存著這個數字(取負號),第一個座標幾乎是常數 1000,只帶一點二次項,最後輸出時要用。整張表只用了 2 個引數:1000 和 0.001。

注意力:RoPE 的鐘面上,兩個加數重合

前面說過,RoPE 把每個位置轉一個角度。這個模型把轉速設成每 19 個位置轉一整圈。於是“相隔 19 個位置”在鐘面上就等於“沒隔”:

週期 19 的 RoPE 讓同一位的兩個數字重合 左邊是一個 19 格的鐘面,刻度表示往回隔了幾個位置除以 19 的餘數。查詢指向 10.3 格;10 格上是 a 的十位和 b 的十位,11 格上是兩個個位。右邊列出預測十位時被看的位置和權重:b₁=6 和 a₁=5 各 0.454,b₀=7 和 a₀=8 各 0.045,注意力輸出 125。 RoPE 的鐘面:19 個位置轉一圈 預測十位:位置 31 往回看 刻度 = 往回隔了幾個位置,滿 19 歸零 0 5 15 查詢 10.3 10:a₁ 與 b₁ 11:a₀ 與 b₀ 被看的位置權重 位置 21b₁ = 6隔 100.454 位置 2a₁ = 5隔 29 = 10 + 190.454 位置 20b₀ = 7隔 110.045 位置 1a₀ = 8隔 30 = 11 + 190.045 其餘 28 個< 0.002 注意力輸出10×(5+6) + (8+7) = 125 兩個加數之間墊 9 個 0,同一位的兩個數字 正好隔 19 個位置,在鐘面上落在同一格。 查詢指向 10.3 而不是 10,上一位的兩個數字 恰好分到十分之一的權重,捎上了進位的線索。
圖 4 · 隔 10 和隔 29 在週期 19 的鐘面上是同一格:注意力一次就把同一位的兩個數字都取到了

在預測十位的這一步,序列最後一個 token 是剛寫下的個位 5,位置 31。它往回看:b 的十位在位置 21,相隔 10;a 的十位在位置 2,相隔 29,比 10 多一整圈。在鐘面上,a₁ 和 b₁ 落在同一個刻度上。查詢向量的方向被設在 10.3 這個刻度:離 10 最近,所以 a₁ 和 b₁ 各拿到 0.454 的權重;11 刻度上的 a₀、b₀(也就是上一位的兩個數字)離得稍遠,各拿到 0.045,恰好是 1/10。其餘位置加起來不到 0.002。

值向量取的是嵌入的第二個座標(數字本身)再乘一個係數,於是注意力的輸出是:

注意力輸出=10 (ai+bi)+(ai−1+bi−1)\text{注意力輸出} = 10\,(a_i + b_i) + (a_{i-1} + b_{i-1})

十位這一步就是 10×(5+6)+(8+7)=12510 \times (5+6) + (8+7) = 125。

這裡有兩個細節。一是那 9 個 0 的另一個作用:算最高位(第 11 位)時,a、b 都沒有這一位了,查詢指向的地方正好是補的 0,結果自然是 0。二是“注意力只看相對距離”這一點被用到了極致:每一步的查詢都指向“10.3 個位置之前”,同一套引數對 11 個數位通用,不需要為每一位單獨設計。

殘差:進位自己冒出來

注意力的輸出加回殘差流以後,要和當前 token 自己的嵌入相加。當前 token 是上一步寫下的那一位 si−1s_{i-1},嵌入第二個座標是 −si−1-s_{i-1}。於是:

h=10 (ai+bi)+(ai−1+bi−1)−si−1h = 10\,(a_i + b_i) + (a_{i-1} + b_{i-1}) - s_{i-1}

而上一位的豎式是 ai−1+bi−1+ci−1=si−1+10 cia_{i-1} + b_{i-1} + c_{i-1} = s_{i-1} + 10\,c_i,其中 cc 是進位。代進去:

h=10 (ai+bi+ci)−ci−1h = 10\,(a_i + b_i + c_i) - c_{i-1}

進位就這樣冒了出來:上一位兩個數字的和,減去上一位實際寫下的數字,差的那 10 就是進位。模型沒有任何地方專門存進位,它是從“自己剛寫下的那個數字”裡讀出來的,正是第三節說的低位在前的好處。十位這一步,h=125−5=120h = 125 - 5 = 120,正好是 10×(5+6+1)10 \times (5 + 6 + 1);百位那一步是 121−2=119=10×(7+4+1)−1121 - 2 = 119 = 10 \times (7 + 4 + 1) - 1。末尾那個 −ci−1-c_{i-1} 最多是 −1,對後面的取整沒有影響。

MLP:滿十就減掉

h/10h/10 現在是這一位的“豎式和”,範圍 0 到 19。要寫下的數字是它除以 10 的餘數,這一步交給 MLP。寬度 2 的 SwiGLU 裡有兩個門,閾值設得只差一點點,兩個門的輸出相減就成了一個陡峭的臺階:

6 引數模型裡 MLP 的臺階 橫軸是進入 MLP 前的 h,等於 10 乘這一位的豎式和,從 0 到 190;縱軸是 MLP 之後的值。h 小於 95 時輸出等於輸入,h 超過 95 時整條線向下跳約 99。圓點標出豎式和為 0 到 19 時的取值,豎式和不小於 10 的點用強調色。 MLP 前後:h 的第二個座標 0 30 60 90 MLP 後 0 50 95 150 190 MLP 前 h = 10 × 這一位的豎式和 十位:120 → 21.4 → 寫 2 h 過了 95(豎式和 ≥ 10),MLP 減掉約 99, 只留下個位;輸出頭再取離 h/10 最近的整數。
圖 5 · MLP 只做一件事:豎式和滿十,就把 h 減掉約 99(相當於豎式和減 10),剩下的就是這一位要寫的數字

hh 不到 95 時,MLP 輸出幾乎是 0;超過 95,它輸出大約 −99。十位那一步,120 減掉 98.6,剩 21.4。

輸出頭:離哪個數字最近

最後,輸出頭拿 hh 去和 10 個數字的嵌入做點積。還記得嵌入第一個座標裡那個 −0.001 d2-0.001\,d^2 嗎?它讓每個數字的分數成為 dd 的一個開口向下的拋物線,頂點落在 h/10h/10 附近,所以分數最高的就是離 h/10h/10 最近的那個數字。21.4 離 20 最近,輸出 2。

把三步放在一起:

758 + 467:6 引數模型前四步的內部數值
這一步注意力輸出減去上一步寫下的MLP 之後寫下
個位10×(8+7) + 0 = 150150 − 0 = 150150 − 97.8 = 52.25
十位10×(5+6) + 15 = 125125 − 5 = 120120 − 98.6 = 21.42
百位10×(7+4) + 11 = 121121 − 2 = 119119 − 98.6 = 20.42
千位10×(0+0) + 11 = 1111 − 2 = 99(不到 95,不減)1

後面七位全是 0。每一格都是 probe6.py 列印出的實際數值(四捨五入到 0.1)。

表 4 · 豎式裡的每一步都能在殘差流裡找到:同位相加、讀出進位、滿十減十、取整

破壞實驗:每個零件都不能少

為了確認上面的解讀不是看圖說話,我們動了幾處設定,各在 500 道驗證題上重新跑7:

改動 6 引數模型的一處設定,500 道驗證題的準確率
改動破壞了什麼準確率
原樣—100%
RoPE 週期 19 → 18a、b 不在同一格0%
RoPE 週期 19 → 20a、b 不在同一格0%
MLP 輸出係數設為 0不再滿十減十0.4%
換數值精度(2000 道題)
float32—100%
float16128000 超出上限 655040%

改週期時,查詢方向和注意力縮放按同樣的公式重新算過,只是週期不同。

表 5 · 任何一個零件失靈,整臺“加法電路”就停擺

週期改成 18 或 20,a 和 b 就不在同一個刻度上了,一道也做不對;把 MLP 的輸出係數設成 0,只剩“恰好一路都不用進位”的那 2 道題對。還有一個順帶的發現:把全部計算換成 float32 照樣全對,換成 float16 則全錯,因為門的係數 128000 超出了 float16 能表示的最大值 65504。6 個引數裡,每一個都是精心挑選的大數或小數。

到這裡,6 引數模型是怎麼算加法的已經拆完了。可是計算機本來就會做加法,CPU 裡的加法器是怎麼算的、怎麼進位的?我們平時列豎式又是怎麼算的?把這幾種做法擺在一起看,Transformer 的演算法有什麼不一樣?

六、與普通加法的對比

十進位制豎式:查表,加上進位

先看最熟悉的。列豎式時,我們從個位開始,每一位做兩件事:從背熟的加法表裡查出兩個數字的和,再加上右邊進過來的 1;結果滿十,就只寫個位,把 1 記到左邊。758 + 467:8 + 7 = 15,寫 5 進 1;5 + 6 + 1 = 12,寫 2 進 1;7 + 4 + 1 = 12,寫 2 進 1;最後寫 1。一共進位 3 次。

這裡的進位只有 0 和 1 兩種可能,方向固定從右往左,圖 2 裡畫的就是這個過程。

二進位制:全加器串成一條鏈

CPU 裡的數是二進位制存的,每一位只有 0 和 1。十位數最大是 9,999,999,999,要 34 個二進位制位才放得下。758 寫成二進位制是 1011110110,467 是 111010011。

二進位制的一位相加只有 8 種情況(a、b、進位各取 0 或 1),所以根本不用查表,用幾個邏輯閘就能算出來。這個小電路叫全加器:這一位的和是 a、b、進位三者的異或,進位出是“a 和 b 都是 1,或者進位是 1 且 a、b 恰好有一個是 1”11:

si=ai⊕bi⊕ci,ci+1=(ai∧bi)∨(ci∧(ai⊕bi))s_i = a_i \oplus b_i \oplus c_i, \qquad c_{i+1} = (a_i \wedge b_i) \vee \big(c_i \wedge (a_i \oplus b_i)\big)

一個全加器是 5 個門。把 34 個全加器排成一排,每個的進位出接到左邊那個的進位入,就是最簡單的加法器,叫行波進位加法器(ripple-carry adder):

758 加 467 的二進位制行波進位 從左到右是第 10 位到第 0 位。a 是 758 的二進位制 1011110110,b 是 467 的二進位制 0111010011,和是 1225 的二進位制 10011001001。每一位一個全加器,下方的箭頭把進位從右邊的位傳到左邊的位;第 1、2、4 到 9 位產生進位,用強調色表示。 758 + 467 在 CPU 裡:11 個全加器串成一條鏈 位 a b 和 進位 0 1 1 0 1 1 1 1 1 1 0 0 1 1 1 1 1 0 2 1 0 0 3 0 0 1 4 1 1 0 5 1 0 0 6 1 1 1 7 1 1 1 8 0 1 0 9 1 0 0 10 0 0 1 每個全加器只接三根線:a、b 和右邊傳來的進位。和 = a⊕b⊕進位,進位出 = (a∧b)∨(進位∧(a⊕b)),共 5 個門。 同一道題,十進位制只進位 3 次;二進位制進位 8 次,第 4 到第 9 位連成一串(虛線表示進位為 0)。
圖 6 · CPU 裡的加法:每一位一個固定的小電路,進位順著導線從低位流到高位,不用“找”也不用“寫下來再讀”

和十進位制比,二進位制的進位要頻繁得多:同一道題,十進位制進位 3 次,二進位制進位 8 次,而且第 4 到第 9 位的進位連成一串。我們在 verify.py 的 10000 道題上數了一下,十進位制裡最長的連續進位平均 3.0 位,二進位制裡平均 7.5 位,最長一次有 31 位連著進位7。

進位鏈太長就慢。行波進位加法器裡,最高位要等進位一個一個傳過來,34 位就要傳 34 級。所以真正的 CPU 用的是超前進位(carry-lookahead):每一位先各自算出兩個訊號,“這一位自己會不會產生進位”(ai∧bia_i \wedge b_i)和“右邊來的進位會不會穿過這一位”(ai⊕bia_i \oplus b_i),再用一棵樹形電路把所有位的進位同時算出來。以 64 位加法為例,進位要經過的級數從 64 級降到 log₂64 = 6 級左右11。這就是為什麼 CPU 做一次 64 位加法通常只要一個時鐘週期。

十進位制硬體:BCD 的“加 6”

有些場合(老式計算器、金融系統裡的十進位制運算)要求按十進位制算,常見的做法叫 BCD(二進位制編碼的十進位制):每個十進位制位單獨用 4 個二進位制位存12。4 個二進位制位能數到 15,可十進位制只用到 9,於是相加時多了一步修正:

758 + 467 用 BCD(每個十進位制位佔 4 個二進位制位)相加
位4 位二進位制相加(含進位)大於 9?加 6 修正寫下 / 進位
個位1000 + 0111 = 1111(15)是1111 + 0110 = 1 01015 / 1
十位0101 + 0110 + 1 = 1100(12)是1100 + 0110 = 1 00102 / 1
百位0111 + 0100 + 1 = 1100(12)是1100 + 0110 = 1 00102 / 1
千位0000 + 0000 + 1 = 0001(1)否不修正1 / 0

4 位二進位制能數到 15,十進位制只用到 9;加 6 正好跳過 10–15 這 6 個用不上的編碼,在 4 位裡等於減 10。

表 6 · BCD 的“加 6”和 6 引數模型 MLP 的“減 99”是同一件事:滿十,就從這一位拿走十

8 + 7 用 4 位二進位制相加得 1111,也就是 15,不是一個合法的十進位制位;規則是大於 9 就再加 6。加 6 等於跳過 10 到 15 這 6 個用不上的編碼,在 4 位裡就相當於減 10,溢位來的那一位正好就是進位。

這個“滿十就減十”,和 6 引數模型裡 MLP 做的事一模一樣(圖 5):h 是豎式和的 10 倍,過了 95 就減掉約 99。也就是說,Transformer 做的其實是十進位制加法,它的 MLP 扮演的就是 BCD 裡那個“加 6”修正電路。

三種演算法放在一起

同一道加法,三種演算法
十進位制豎式(人)二進位制加法器(CPU)6 引數 Transformer
數怎麼表示0–9 十個符號0/1 兩種電平,十位數要 34 位一個 token 一個數字,嵌入成 [1000−0.001d², −d]
同一位怎麼對齊豎式上下對齊導線固定:第 i 位接第 i 位注意力去“找”:週期 19 的鐘面讓兩位重合
一位怎麼相加背加法表異或門:a⊕b⊕進位注意力加權求和:10(aᵢ+bᵢ)
怎麼知道要進位和 ≥ 10與門、或門:(a∧b)∨(進位∧(a⊕b))上一位兩數之和,減去上一步寫下的數字,差 10 就是進位
滿了怎麼辦只寫個位二進位制每位本來只有 0/1,進位直接出去MLP 臺階:h ≥ 95 時減約 99
進位怎麼傳記在腦子裡或寫個小 1沿導線傳給左邊的全加器;快的 CPU 用超前進位並行算出寫進序列,下一步從剛寫下的數字裡重新讀出來
代價(我們的測量)—34 位行波加法器 170 個門;Python 裡一次 a+b 約 29 納秒numpy 實現每道題約 2.4 毫秒,慢約 8 萬倍

門數按每個全加器 5 個門估算;Python 的 29 納秒大部分是直譯器開銷,CPU 的加法指令本身通常一個時鐘週期。

表 7 · 三種演算法做的是同一件事;不同的是“對齊”和“進位”分別靠什麼完成

把這張表橫著讀,三種演算法的“一位怎麼相加”“滿了怎麼辦”都很像,真正的差別在兩行。

對齊。豎式靠紙上的位置,CPU 靠導線,第 i 位和第 i 位從一開始就接在一起。Transformer 沒有導線,序列裡所有位置都只是排成一行的 token,它得用注意力在每一步重新“找”到同一位的兩個數字,6 引數模型為此專門設計了 9 個 0 和週期 19 的鐘面。

進位。CPU 的進位是一根導線上的電平,從低位流到高位,從來不離開電路。Transformer 裡沒有任何地方能存一個進位:每一步算完,模型只留下一個寫出來的數字。下一步要知道進位,只能把上一位的兩個數字再看一遍,和自己剛寫下的數字比一比,差了 10 就說明進過位。進位不是傳過去的,而是每一步重新推出來的。這也可以解釋第三節裡高位在前的寫法為什麼學不會:那樣寫時,進位還沒“寫下來”,模型就沒有地方去讀它(這是我們的理解)。

代價。一個 34 位的行波加法器只有 170 個門;而 6 引數模型用 numpy 跑,每道題要 2.4 毫秒,比 Python 裡一次普通的 a + b(約 29 納秒,大部分還是直譯器開銷)慢約 8 萬倍7。這本來就不是用來比速度的:AdderBoard 問的是“Transformer 這種結構能不能表示加法、最少要幾個引數”,不是“用 Transformer 做加法劃不划算”。

看完和普通加法的對比,再回頭數 6 引數模型裡的數:鐘面的週期 19、查詢指向的 10.3、歸一化的係數……這些難道不也是模型裡的數嗎?為什麼只算 6 個?

七、引數該怎麼數

6 還是 10

這正是 6 引數提交引起的爭論13。它的父版本有 10 個引數,6 引數版本做了兩處改動:

  • 查詢投影的 2 個數固定下來。作者認為這兩個數決定的是“指向哪個刻度”,和 RoPE 的週期一樣屬於位置編碼的一部分,按規則固定位置編碼不計引數。
  • 最後一層歸一化的 2 個係數並進輸出頭。歸一化後再乘係數、再和嵌入表點積,等價於先把係數乘進嵌入表;而嵌入表本來就是由 2 個引數算出來的,係數並進去以後不再單獨佔引數。

維護者認為第二處在數學上站得住,第一處“更有爭議”,因為父版本明明把這兩個數當引數算了。最後的處理是收錄,但在 6 後面打一個星號,註明“按嚴格演算法是 10 個”。提交者也說,如果爭議太大可以撤回,這兩個技巧只是他的智慧體在研究別的問題時順手發現的。

這場爭論說明,在這個尺度上,“引數個數”已經不只是模型的屬性,也取決於記賬規則:哪些數算結構常數,哪些數算引數。從 10 到 6,模型算的東西一點沒變。

能表示,不等於學得會

另一個容易混淆的地方是兩張榜之間的差距。手寫的 6(或 10)和訓練的 36,並不意味著訓練方法落後了五六倍。手寫模型裡的數是人推出來的:1000、0.001、128000,跨了八個數量級,臺階陡到幾乎是一個開關。梯度下降很難從隨機起點走到這樣一個尖銳的解上,訓練榜上的模型都要用平滑得多的方式實現同樣的功能。

所以兩張榜回答的是兩個問題:手寫榜問“這個結構能不能表示加法”,訓練榜問“通用的訓練方法能不能找到它”。手寫榜說明,一層、寬度 2 的 Transformer 就足以表示十位數加法;訓練榜說明,要讓梯度下降自己找到解,目前至少要 36 個引數,外加一整套多階段、定向的訓練流程。

八、回到開頭

回到開頭那段輸出:6 個引數,10010 道題全對。現在可以說清楚它是怎麼回事了。

它不是一個“學會了加法”的模型,而是一個用 Transformer 零件搭出來的加法電路:RoPE 的鐘面負責把同一位的兩個數字對齊,注意力把它們加起來,順便捎上上一位;殘差流裡減掉上一步寫下的數字,進位自然冒出來;MLP 的臺階負責滿十減十;拋物線形的輸出頭負責取整。每個零件都是標準 Transformer 裡本來就有的,只是引數被設成了特定的值。

這個排行榜還有幾個值得帶走的觀察。

格式比容量重要。從第三節的實驗可以看到,同一個模型,只是把數字倒過來寫,就從學不會變成很快學會。

智慧體參與了大部分提交。榜上 52 條記錄裡,有 27 條在“Built with”一欄填了 Claude Code、Codex、Gemini 之類的工具,排行榜本身也起源於讓兩個智慧體比賽。它們會不會鑽規則的空子(成對 token、引數記賬)也是這個榜的一部分故事:規則寫不到的地方,“最優”會往意想不到的方向走。

這裡的“小”有邊界。這些模型只會十位數加法,寬度、週期、補零的個數都是為這道題量身定做的;換成十一位數,6 引數模型多半要重新設計補零個數和鐘面週期(這是我們的推斷,沒有實測)。它們回答的是“表示加法最少需要什麼”,不是“怎麼做一個更好的語言模型”。

小結

  1. Transformer 是什麼? 一個預測下一個 token 的模型:詞嵌入把符號變成向量,注意力往回看並取資訊,MLP 逐位置計算,輸出頭給每個候選打分,殘差流把各部件的結果累加起來;位置編碼(如 RoPE)告訴它誰在前誰在後。答案一位一位地自迴歸生成。
  2. AdderBoard 比什麼? 比誰的 Transformer 用最少的引數把十位數加法做到 99% 以上,分訓練和手寫兩張榜,規則要求加法必須由模型算出來,不能由解碼程式碼代勞。
  3. 加法難在哪? 難在進位。高位在前時,第一位就要算完整條進位鏈;低位在前時,進位可以從上一步寫下的數字裡讀出來。所有上榜模型都用低位在前的寫法。
  4. 訓練模型怎麼變小? 先變窄,再靠低秩分解、權重繫結、用公式代替整張表讓引數共用;代價是訓練越來越難,要靠頓悟、多階段微調和定向微調,目前最少 36 個引數。
  5. 6 個引數怎麼做加法? 週期 19 的 RoPE 讓同一位的兩個數字在鐘面上重合;注意力算出 10(ai+bi)10(a_i+b_i) 加上一位的和;減去上一步寫下的數字得到進位;MLP 滿十減十;拋物線輸出頭取整。
  6. 和普通加法有什麼不同? 一位怎麼加、滿十怎麼辦都差不多,MLP 的“減十”就是 BCD 的“加 6”;差別在對齊和進位:CPU 靠導線對齊、靠導線傳進位,Transformer 要用注意力去找同一位,進位則從自己上一步寫下的數字裡重新推出來。
  7. 6 個還是 10 個? 取決於把哪些數當作結構常數。手寫榜證明“能表示”,訓練榜證明“學得會”,兩者回答的是不同的問題。

如果想自己看一遍,最省事的是下載 6 引數那份程式碼9,它只依賴 numpy,配合排行榜的 verify.py 半分鐘就能跑完;在 _forward 裡列印注意力權重,就能看到上面那張鐘面。

參考資料

參考資料

  1. anadim/AdderBoardDimitris Papailiopoulos · GitHubgithub.com ↗排行榜本體:README 裡的兩張榜、規則和 verify.py。本文依據 2026-03-26 的最新提交 07061de,此後倉庫沒有更新。https://github.com/anadim/AdderBoard ↩ ↩2 ↩3 ↩4 ↩5

  2. Attention Is All You NeedAshish Vaswani 等 · NeurIPS 2017 · 2017-06-12arxiv.org ↗Transformer 的原始論文。排行榜“固定位置編碼不計引數”的規矩就來自這裡的正弦位置編碼。https://arxiv.org/abs/1706.03762 ↩

  3. RoFormer: Enhanced Transformer with Rotary Position EmbeddingJianlin Su 等 · arXiv · 2021-04-20arxiv.org ↗RoPE(旋轉位置編碼),榜上小模型幾乎都用它。https://arxiv.org/abs/2104.09864 ↩

  4. tbukic/M10S-TransformerTom Bukic · GitHubgithub.com ↗訓練榜前列的 36、41、44… 引數模型都出自這裡,paper/main.tex 是附帶的論文,講引數構成、多階段微調、“進位幽靈”和定向微調。https://github.com/tbukic/M10S-Transformer ↩ ↩2 ↩3 ↩4 ↩5

  5. Addition Under PressureDimitris Papailiopoulos · Substack · 2026-02-19dimitrisp.substack.com ↗排行榜的起點:同一段提示詞交給 Claude Code 和 Codex,分別得到 6080 和 1644 個引數的模型。https://dimitrisp.substack.com/p/addition-under-pressure ↩ ↩2 ↩3

  6. Teaching Arithmetic to Small TransformersNayoung Lee, Kartik Sreenivasan, Jason D. Lee, Kangwook Lee, Dimitris Papailiopoulos · ICLR 2024 · 2023-07-07arxiv.org ↗系統比較了加法資料的寫法,倒序輸出讓小模型學得快得多。維護者在部落格裡說 Claude Code 選倒序正是受這篇影響。https://arxiv.org/abs/2307.03381 ↩

  7. 本文的實驗程式碼與調研筆記/mnt/project-files/adderboard-research/:probe6.py 逐步列印 6 引數模型的內部數值,period.py 做破壞實驗,train2.py 是自己訓練的對照實驗,binary.py 算二進位制和 BCD 的進位並測速,輸出在 logs/。PyTorch 2.14.1 CPU 版,4 核。 ↩ ↩2 ↩3 ↩4 ↩5

  8. Grokking: Generalization Beyond Overfitting on Small Algorithmic DatasetsAlethea Power 等 · arXiv · 2022-01-06arxiv.org ↗“頓悟”現象:訓練損失早就降下來,測試準確率在很久以後才突然上升。憑領域知識整理,發文前需要核對。https://arxiv.org/abs/2201.02177 ↩

  9. zcbtrak:6 引數手寫加法器GitHub Gistgist.github.com ↗純 numpy 實現,本文的機制分析都在這份程式碼上跑。https://gist.github.com/zcbtrak/b9af065d6395a3ecd72e3b8d2e867ae9 ↩ ↩2

  10. Lokimorty:10 引數手寫加法器GitHub Gistgist.github.com ↗6 引數版本的父版本,用 Apple MLX 寫成,Linux 上跑不了;數值上和 6 引數版本完全相同。https://gist.github.com/Lokimorty/d54e5c61997e00fb922b6692739a0f6c ↩

  11. Adder (electronics)Wikipediaen.wikipedia.org ↗全加器、行波進位加法器和超前進位加法器的標準介紹。https://en.wikipedia.org/wiki/Adder_(electronics) ↩ ↩2

  12. Binary-coded decimalWikipediaen.wikipedia.org ↗BCD 加法裡“大於 9 就加 6”的修正規則。https://en.wikipedia.org/wiki/Binary-coded_decimal ↩

  13. Submission #75: 6-parameter hand-coded adderGitHubgithub.com ↗6 個引數該不該算 6 個的討論,維護者 2026-03-25 以“6*”收錄。https://github.com/anadim/AdderBoard/issues/75 ↩