GitHub 上有一個叫 AdderBoard 的排行榜,比的是誰能做出最小的 Transformer,讓它把兩個十位數加對1。榜首那一份提交只有兩百來行 numpy 程式碼,我們把它下載下來,用排行榜自帶的驗證指令碼跑了一遍:
$ python verify.py submission_6p_handcoded.pyParameters (unique): 6Architecture: 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,第三、五節再講)。
詞嵌入(embedding)。神經網路只會算數,不認識符號,所以第一步是查表:每個 token 對應一個固定長度的向量,比如 “5” 對應 [999.98, −5]。向量的長度叫模型寬度 ,大模型裡是幾千,這個榜上常見的是 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 按所在的位置轉一個角度,位置 轉 。兩個向量都轉過以後再做點積,結果只取決於它們相隔多少個位置,而不是各自在哪裡3。記住“RoPE 是一個按位置轉動的鐘”,第五節會用到。
引數是什麼
圖裡每個部件都離不開幾張矩陣:詞嵌入是一張 10× 的表,Q、K、V 各是一個矩陣,MLP 有兩三個矩陣,歸一化還有一組縮放係數。這些矩陣裡的每一個數都是引數;訓練,就是用梯度下降一點點調這些數,讓預測越來越準。
以訓練榜上一個 122 引數的模型為例(寬度 3,注意力頭的維度 4,MLP 中間層 3),引數是這樣分的4:
| 部件 | 形狀 | 引數 |
|---|---|---|
| 詞嵌入(與輸出頭共用) | 10 × 3 | 30 |
| 注意力 Q、K、V | 3 × 4,各一個 | 36 |
| 注意力輸出投影 O | 4 × 3 | 12 |
| Q、K 各自的歸一化 | 4 + 4 | 8 |
| 三處歸一化(注意力前、MLP 前、輸出前) | 3 + 3 + 3 | 9 |
| MLP(SwiGLU:門、升維、降維) | 3 × 3,三個 | 27 |
| 合計 | 122 |
RoPE 是固定的旋轉角度,不算引數。
引數量是衡量模型大小最直接的尺子。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 在做加法”:
- 必須至少有一層自注意力。
- 必須自迴歸:一位一位地生成,每一位生成後接回輸入。進位必須從這個過程裡“長出來”,不能在 Python 裡用一個變數把進位傳給下一步。
forward()必須是標準的“張量進、分數出”,裡面不能有按數位迴圈、存進位這類專門針對加法的控制流。- 生成迴圈必須是通用的:換一套權重、換一個任務,同樣的解碼程式碼照樣能用。
一句話:算加法的必須是模型,不能是程式碼。在此之外,輸入怎麼寫(倒序、加分隔符、補零)可以自選,只要格式固定、不洩露答案;位置編碼如果是固定的(正弦、RoPE),不算引數;繫結在一起的權重只算一次。
驗證方法也是固定的:verify.py 先跑 10 個邊界用例(全 9 加全 9、一串連續進位之類),再跑 10000 道隨機題,隨機種子固定為 2025,準確率不低於 99% 才算合格。
題目定好了,可是在動手之前,還有一個更基礎的問題:一個只會“看前面、猜下一個”的模型,做加法難在哪裡?
三、加法對 Transformer 難在哪
三件事
列豎式做加法,我們其實在做三件事1:
- 對齊:把兩個數同一位上的數字找出來。
- 逐位相加:算這一位的和,以及滿不滿十。
- 傳遞進位:把低一位的進位帶到高一位。
前兩件 Transformer 都有現成的部件:對齊靠注意力,逐位相加靠 MLP。麻煩在第三件。進位可能一路傳下去:9999999999 + 1,個位的進位要連著翻過十位,一直傳到最高位。而模型是從左往右一個一個寫答案的。
現象:寫答案的方向和進位的方向
按我們平常的習慣,和的最高位寫在最前面。看 758 + 467 = 1225:
高位在前時,模型寫下第一個數字 “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 上每次不到兩分鐘。
| 模型 | 寫法 | 整題全對 | 每一位單獨看 |
|---|---|---|---|
| 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 道。
低位在前,三個種子都在第 1000 步(約 11 秒)時就把 2000 道測試題全部做對了。高位在前,三個種子訓完 1 萬步,一道整題也沒對:最高兩位能猜對九成左右,往下的數位大多隻比瞎猜(10%)好一點。把模型放大十倍到 7336 個引數,高位在前終於能學到每一位都有九成以上的正確率,可十一位要同時對,整題正確率只有 27% 和 66%;同樣大小的模型換成低位在前,一千步就全對。
高位在前的小模型卡在哪裡,我們沒有深究,從“只有最高兩位對”來看,它大概連每一位該看哪兩個數字都沒學會(這是推斷)。但結論很清楚:同樣的引數、同樣的資料量,只是把數字倒過來寫,就是學得會和學不會的區別。
榜上的每一個模型,無論訓練的還是手寫的,都用了低位在前的寫法。格式解決以後,剩下的問題就是:模型能壓到多小?
四、訓練榜:從 6080 到 36
現象:五週,兩條階梯
把兩張榜的榜首按時間連起來(日期取自倉庫的提交記錄),是兩條往下走的階梯:
訓練榜從 Claude Code 的 6080 一路降到 36,手寫榜從開榜時的 139 降到 6。兩條線有一個共同點:越往後,每一步省下的引數越少,花的力氣越大。那麼,訓練出來的模型是靠什麼一步步變小的?
第一步:把模型變窄
最直接的辦法是減小寬度 。引數量大致隨 增長,寬度減半,引數少四分之三。但寬度不能無限減: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 |
| 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。
把整張表換成一個公式。到 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 的第 位在位置 ,b 的第 位在位置 ,同一位上的兩個數字恰好相隔 19 個位置。這個 19 是後面一切的關鍵。
詞嵌入:一個座標存數字
寬度為 2,每個 token 只有兩個座標。數字 的嵌入是:
第二個座標直接存著這個數字(取負號),第一個座標幾乎是常數 1000,只帶一點二次項,最後輸出時要用。整張表只用了 2 個引數:1000 和 0.001。
注意力:RoPE 的鐘面上,兩個加數重合
前面說過,RoPE 把每個位置轉一個角度。這個模型把轉速設成每 19 個位置轉一整圈。於是“相隔 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。
值向量取的是嵌入的第二個座標(數字本身)再乘一個係數,於是注意力的輸出是:
十位這一步就是 。
這裡有兩個細節。一是那 9 個 0 的另一個作用:算最高位(第 11 位)時,a、b 都沒有這一位了,查詢指向的地方正好是補的 0,結果自然是 0。二是“注意力只看相對距離”這一點被用到了極致:每一步的查詢都指向“10.3 個位置之前”,同一套引數對 11 個數位通用,不需要為每一位單獨設計。
殘差:進位自己冒出來
注意力的輸出加回殘差流以後,要和當前 token 自己的嵌入相加。當前 token 是上一步寫下的那一位 ,嵌入第二個座標是 。於是:
而上一位的豎式是 ,其中 是進位。代進去:
進位就這樣冒了出來:上一位兩個數字的和,減去上一位實際寫下的數字,差的那 10 就是進位。模型沒有任何地方專門存進位,它是從“自己剛寫下的那個數字”裡讀出來的,正是第三節說的低位在前的好處。十位這一步,,正好是 ;百位那一步是 。末尾那個 最多是 −1,對後面的取整沒有影響。
MLP:滿十就減掉
現在是這一位的“豎式和”,範圍 0 到 19。要寫下的數字是它除以 10 的餘數,這一步交給 MLP。寬度 2 的 SwiGLU 裡有兩個門,閾值設得只差一點點,兩個門的輸出相減就成了一個陡峭的臺階:
不到 95 時,MLP 輸出幾乎是 0;超過 95,它輸出大約 −99。十位那一步,120 減掉 98.6,剩 21.4。
輸出頭:離哪個數字最近
最後,輸出頭拿 去和 10 個數字的嵌入做點積。還記得嵌入第一個座標裡那個 嗎?它讓每個數字的分數成為 的一個開口向下的拋物線,頂點落在 附近,所以分數最高的就是離 最近的那個數字。21.4 離 20 最近,輸出 2。
把三步放在一起:
| 這一步 | 注意力輸出 | 減去上一步寫下的 | MLP 之後 | 寫下 |
|---|---|---|---|---|
| 個位 | 10×(8+7) + 0 = 150 | 150 − 0 = 150 | 150 − 97.8 = 52.2 | 5 |
| 十位 | 10×(5+6) + 15 = 125 | 125 − 5 = 120 | 120 − 98.6 = 21.4 | 2 |
| 百位 | 10×(7+4) + 11 = 121 | 121 − 2 = 119 | 119 − 98.6 = 20.4 | 2 |
| 千位 | 10×(0+0) + 11 = 11 | 11 − 2 = 9 | 9(不到 95,不減) | 1 |
後面七位全是 0。每一格都是 probe6.py 列印出的實際數值(四捨五入到 0.1)。
破壞實驗:每個零件都不能少
為了確認上面的解讀不是看圖說話,我們動了幾處設定,各在 500 道驗證題上重新跑7:
| 改動 | 破壞了什麼 | 準確率 |
|---|---|---|
| 原樣 | — | 100% |
| RoPE 週期 19 → 18 | a、b 不在同一格 | 0% |
| RoPE 週期 19 → 20 | a、b 不在同一格 | 0% |
| MLP 輸出係數設為 0 | 不再滿十減十 | 0.4% |
| 換數值精度(2000 道題) | ||
| float32 | — | 100% |
| float16 | 128000 超出上限 65504 | 0% |
改週期時,查詢方向和注意力縮放按同樣的公式重新算過,只是週期不同。
週期改成 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:
一個全加器是 5 個門。把 34 個全加器排成一排,每個的進位出接到左邊那個的進位入,就是最簡單的加法器,叫行波進位加法器(ripple-carry adder):
和十進位制比,二進位制的進位要頻繁得多:同一道題,十進位制進位 3 次,二進位制進位 8 次,而且第 4 到第 9 位的進位連成一串。我們在 verify.py 的 10000 道題上數了一下,十進位制裡最長的連續進位平均 3.0 位,二進位制裡平均 7.5 位,最長一次有 31 位連著進位7。
進位鏈太長就慢。行波進位加法器裡,最高位要等進位一個一個傳過來,34 位就要傳 34 級。所以真正的 CPU 用的是超前進位(carry-lookahead):每一位先各自算出兩個訊號,“這一位自己會不會產生進位”()和“右邊來的進位會不會穿過這一位”(),再用一棵樹形電路把所有位的進位同時算出來。以 64 位加法為例,進位要經過的級數從 64 級降到 log₂64 = 6 級左右11。這就是為什麼 CPU 做一次 64 位加法通常只要一個時鐘週期。
十進位制硬體:BCD 的“加 6”
有些場合(老式計算器、金融系統裡的十進位制運算)要求按十進位制算,常見的做法叫 BCD(二進位制編碼的十進位制):每個十進位制位單獨用 4 個二進位制位存12。4 個二進位制位能數到 15,可十進位制只用到 9,於是相加時多了一步修正:
| 位 | 4 位二進位制相加(含進位) | 大於 9? | 加 6 修正 | 寫下 / 進位 |
|---|---|---|---|---|
| 個位 | 1000 + 0111 = 1111(15) | 是 | 1111 + 0110 = 1 0101 | 5 / 1 |
| 十位 | 0101 + 0110 + 1 = 1100(12) | 是 | 1100 + 0110 = 1 0010 | 2 / 1 |
| 百位 | 0111 + 0100 + 1 = 1100(12) | 是 | 1100 + 0110 = 1 0010 | 2 / 1 |
| 千位 | 0000 + 0000 + 1 = 0001(1) | 否 | 不修正 | 1 / 0 |
4 位二進位制能數到 15,十進位制只用到 9;加 6 正好跳過 10–15 這 6 個用不上的編碼,在 4 位裡等於減 10。
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 的加法指令本身通常一個時鐘週期。
把這張表橫著讀,三種演算法的“一位怎麼相加”“滿了怎麼辦”都很像,真正的差別在兩行。
對齊。豎式靠紙上的位置,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 引數模型多半要重新設計補零個數和鐘面週期(這是我們的推斷,沒有實測)。它們回答的是“表示加法最少需要什麼”,不是“怎麼做一個更好的語言模型”。
小結
- Transformer 是什麼? 一個預測下一個 token 的模型:詞嵌入把符號變成向量,注意力往回看並取資訊,MLP 逐位置計算,輸出頭給每個候選打分,殘差流把各部件的結果累加起來;位置編碼(如 RoPE)告訴它誰在前誰在後。答案一位一位地自迴歸生成。
- AdderBoard 比什麼? 比誰的 Transformer 用最少的引數把十位數加法做到 99% 以上,分訓練和手寫兩張榜,規則要求加法必須由模型算出來,不能由解碼程式碼代勞。
- 加法難在哪? 難在進位。高位在前時,第一位就要算完整條進位鏈;低位在前時,進位可以從上一步寫下的數字裡讀出來。所有上榜模型都用低位在前的寫法。
- 訓練模型怎麼變小? 先變窄,再靠低秩分解、權重繫結、用公式代替整張表讓引數共用;代價是訓練越來越難,要靠頓悟、多階段微調和定向微調,目前最少 36 個引數。
- 6 個引數怎麼做加法? 週期 19 的 RoPE 讓同一位的兩個數字在鐘面上重合;注意力算出 加上一位的和;減去上一步寫下的數字得到進位;MLP 滿十減十;拋物線輸出頭取整。
- 和普通加法有什麼不同? 一位怎麼加、滿十怎麼辦都差不多,MLP 的“減十”就是 BCD 的“加 6”;差別在對齊和進位:CPU 靠導線對齊、靠導線傳進位,Transformer 要用注意力去找同一位,進位則從自己上一步寫下的數字裡重新推出來。
- 6 個還是 10 個? 取決於把哪些數當作結構常數。手寫榜證明“能表示”,訓練榜證明“學得會”,兩者回答的是不同的問題。
如果想自己看一遍,最省事的是下載 6 引數那份程式碼9,它只依賴 numpy,配合排行榜的 verify.py 半分鐘就能跑完;在 _forward 裡列印注意力權重,就能看到上面那張鐘面。
參考資料
參考資料
-
anadim/AdderBoardgithub.com ↗排行榜本體:README 裡的兩張榜、規則和
verify.py。本文依據 2026-03-26 的最新提交07061de,此後倉庫沒有更新。https://github.com/anadim/AdderBoard ↩ ↩2 ↩3 ↩4 ↩5 -
Attention Is All You Needarxiv.org ↗Transformer 的原始論文。排行榜“固定位置編碼不計引數”的規矩就來自這裡的正弦位置編碼。https://arxiv.org/abs/1706.03762 ↩
-
RoFormer: Enhanced Transformer with Rotary Position Embeddingarxiv.org ↗RoPE(旋轉位置編碼),榜上小模型幾乎都用它。https://arxiv.org/abs/2104.09864 ↩
-
tbukic/M10S-Transformergithub.com ↗訓練榜前列的 36、41、44… 引數模型都出自這裡,
paper/main.tex是附帶的論文,講引數構成、多階段微調、“進位幽靈”和定向微調。https://github.com/tbukic/M10S-Transformer ↩ ↩2 ↩3 ↩4 ↩5 -
Addition Under Pressuredimitrisp.substack.com ↗排行榜的起點:同一段提示詞交給 Claude Code 和 Codex,分別得到 6080 和 1644 個引數的模型。https://dimitrisp.substack.com/p/addition-under-pressure ↩ ↩2 ↩3
-
Teaching Arithmetic to Small Transformersarxiv.org ↗系統比較了加法資料的寫法,倒序輸出讓小模型學得快得多。維護者在部落格裡說 Claude Code 選倒序正是受這篇影響。https://arxiv.org/abs/2307.03381 ↩
-
本文的實驗程式碼與調研筆記
/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 -
Grokking: Generalization Beyond Overfitting on Small Algorithmic Datasetsarxiv.org ↗“頓悟”現象:訓練損失早就降下來,測試準確率在很久以後才突然上升。憑領域知識整理,發文前需要核對。https://arxiv.org/abs/2201.02177 ↩
-
zcbtrak:6 引數手寫加法器gist.github.com ↗純 numpy 實現,本文的機制分析都在這份程式碼上跑。https://gist.github.com/zcbtrak/b9af065d6395a3ecd72e3b8d2e867ae9 ↩ ↩2
-
Lokimorty:10 引數手寫加法器gist.github.com ↗6 引數版本的父版本,用 Apple MLX 寫成,Linux 上跑不了;數值上和 6 引數版本完全相同。https://gist.github.com/Lokimorty/d54e5c61997e00fb922b6692739a0f6c ↩
-
Adder (electronics)en.wikipedia.org ↗全加器、行波進位加法器和超前進位加法器的標準介紹。https://en.wikipedia.org/wiki/Adder_(electronics) ↩ ↩2
-
Binary-coded decimalen.wikipedia.org ↗BCD 加法裡“大於 9 就加 6”的修正規則。https://en.wikipedia.org/wiki/Binary-coded_decimal ↩
-
Submission #75: 6-parameter hand-coded addergithub.com ↗6 個引數該不該算 6 個的討論,維護者 2026-03-25 以“6*”收錄。https://github.com/anadim/AdderBoard/issues/75 ↩