AI 的混合精度:為什麼用 16-bit 也能訓練

artificial%20intelligence%20concept%2C%20digital%2...
發表時間:2026 年 10 月 28 日 | 更新日期:2026 年 10 月 28 日 | 編輯:雅寶社區編輯團隊
AI 的混合精度:為什麼用 16-bit 也能訓練 - 雅寶社區 · 頂客論壇

可表示範圍(約)

有效十進位位數

典型用途

FP32

32

1 / 8 / 23

±3.4×10³⁸

約 7 位

基準訓練、主權重

TF32

19(儲存 32)

1 / 8 / 10

同 FP32

約 3 位

A100 以上矩陣運算

FP16

16

1 / 5 / 10

約 6×10⁻⁸ ~ 65504

約 3.3 位

推論、混合精度訓練

BF16

16

1 / 8 / 7

同 FP32

約 2.4 位

大模型訓練主流

FP8(E4M3)

1 / 4 / 3

約 ±448

約 1 位

H100 以上、推論加速

看這張表就能理解,為什麼純粹把模型「全部轉成 FP16」會出事。FP16 的下界只有 6×10⁻⁸,而神經網路在反向傳播時,梯度值經常小於 10⁻⁸ 甚至 10⁻¹²——這些數字在 FP16 裡會直接被「洗成 0」,也就是所謂的下溢(Underflow)。一旦梯度變成 0,這個參數就再也不會更新了,訓練等於被悄悄凍結。

同時,FP16 的上界只有 65504。在訓練初期,如果權重初始化不當、學習率設太大,或者某個激活值突然爆衝,很容易就越過這個上限變成「inf」。一旦出現 inf,反向傳播的梯度就會變成 NaN,然後 NaNs 會像傳染病一樣污染整個網路——這就是溢位(Overflow)。

二、混合精度訓練到底「混合」了什麼

「混合精度」這個詞很容易被誤解成「一部分層用 FP16、一部分層用 FP32」這種靜態的混搭。實際上,現代混合精度訓練的核心概念是同一份權重同時以兩種格式存在,並且在不同的運算階段使用不同的格式。這個設計才是讓 16-bit 訓練能夠穩定收斂的真正關鍵。

主權重(Master Weights):32 位元的「本體」

混合精度訓練的第一個關鍵設計,是保留一份 FP32 的權重副本,業界通常稱為主權重(Master Weights)。這份 FP32 權重是「真正的」參數,它負責累積每一次的更新量;而 FP16 的權重只是每次前向傳播前,從 FP32 主權重「投影」出來的臨時副本。

為什麼要這樣做?因為參數更新量(也就是梯度乘以學習率)通常非常小。假設某個權重目前是 0.1,而這次的更新量是 10⁻⁵。在 FP16 的精度下,0.1 附近的解析度大約是 6×10⁻⁵,也就是說 10⁻⁵ 這個更新量根本無法被表達——它會被「吸」回原本的數值。這種現象稱為捨入吞掉更新(Swamping)。

如果這種情況反覆發生,模型就會陷入停滯:梯度算得出來、損失也有變化,但權重就是不動。改用 FP32 主權重來累加,10⁻⁵ 相對於 0.1 是完全可以表達的,更新就能被正確地累積下來。這就是為什麼幾乎所有混合精度實作,都一定要有一份 FP32 的主權重。

前向與反向的差別對待

混合精度的第二個關鍵,是不同運算階段使用不同精度。典型的流程是這樣:

  • 前向傳播:把 FP32 主權重轉成 FP16,用 FP16 做矩陣乘法與卷積,中間的激活值也保持 FP16。這一步是運算量最大的部分,也是最能從 16-bit 獲得加速的地方。
  • 反向傳播:同樣用 FP16 計算各層的梯度,因為反向的運算結構與前向類似,同樣能受益於 Tensor Core 的高吞吐量。
  • 梯度累加與權重更新:把 FP16 的梯度轉回 FP32,與 FP32 主權重做加法,這一步必須在 FP32 下進行,才能保住微小的更新量。
  • 這種架構有時被稱為「FP16 計算 + FP32 累加」。它的妙處在於:神經網路消耗算力的地方(矩陣乘法)全部走 16-bit,而需要精確累積的地方(權重更新)全部走 32-bit。兩個世界各取所需,互不干擾。

    損失縮放(Loss Scaling):把梯度「放大」再縮回來

    有了主權重,捨入問題解決了;但下溢問題還沒解決。梯度值本身可能小到 10⁻¹⁰,這個數字在轉成 FP16 的瞬間就會變成 0,根本來不及累加到主權重上。

    損失縮放的思路非常樸素:既然梯度太小,那就先把它放大再算。具體做法是在計算損失函數時,把 loss 乘以一個縮放係數 S(例如 1024 或 65536)。因為反向傳播是線性的,所有梯度都會等比例放大 S 倍。放大後的梯度就能安全地落在 FP16 的可表示範圍內,不會下溢。

    等到要把梯度應用到主權重時,再把它除以 S 還原回去。整個過程對模型的數學結果沒有任何影響,只是把「暫時的數值」搬進了一個更適合 16-bit 表達的區間。

    更進一步,還有動態損失縮放(Dynamic Loss Scaling)。靜態的縮放係數很難調:太小擋不住下溢,太大又容易溢位。動態版本會監控訓練過程,如果連續 N 個步驟都沒有出現 inf 或 NaN,就把 S 調大一點;一旦偵測到溢位,就跳過這一步更新、把 S 減半。這個機制讓使用者幾乎不需要手動調參,也是 PyTorch AMP 能夠開箱即用的原因。

    三、為什麼 16-bit 還能訓練得動?五個關鍵原因

    前面講的都是「補救措施」——主權重、損失縮放、精度分流。但真正值得深究的問題是:為什麼神經網路經得起這種精度損失?如果換成其他領域,例如氣象模擬或金融計算,把精度砍半幾乎必然導致結果崩壞。深度學習之所以能容忍,是因為它本身的數學性質與那些領域有本質上的不同。

    原因一:訓練的容錯度遠高於推論

    很多人會把訓練和推論的精度需求混在一起談,其實兩者完全不同。推論階段的模型是「成品」,輸入的每一個微小擾動都可能被放大成輸出結果的差異,尤其是在分類邊界附近的樣本;而訓練階段的模型是「半成品」,它本來就在不斷地被修改,每一次更新的方向都只是估計值,不是真理。

    換句話說,訓練時我們並不需要「精確地」算出梯度,我們只需要算出方向大致正確的梯度。這個要求寬鬆得多。既然梯度本身只是估計值,那麼用 16-bit 算出來的梯度與 32-bit 算出來的梯度有微小差異,對於最終的收斂結果影響極其有限。

    原因二:梯度是統計量,不是精確值

    反向傳播算出來的梯度,本質上是「整個資料集上損失函數梯度的估計」。我們每次只用了 mini-batch 的幾十或幾百個樣本,去近似全資料集的梯度。這個近似本身就帶有巨大的統計雜訊——batch size 越小,雜訊越大。

    把這個雜訊量級和 FP16 的捨入誤差放在一起比較,你會發現:FP16 帶來的相對誤差大約是 10⁻³ 量級,而 mini-batch 取樣帶來的梯度雜訊往往在 10⁻¹ 甚至更大的量級。換句話說,精度損失的信號完全被取樣雜訊給蓋過去了。這就像你在一個吵雜的工廠裡量體溫,體溫計誤差 0.1 度還是 0.5 度,其實根本聽不出來。

    原因三:誤差在大量迭代中被平均掉

    深度學習的訓練動輒數萬到數百萬步。每一步的捨入誤差雖然存在,但它的方向是隨機的——有時向上捨、有時向下捨。這些誤差在長期的累積下,大部分會互相抵消,而不是系統性地往同一個方向偏移。

    更關鍵的是,Adam、SGD with momentum 這類優化器本身就帶有平滑機制。它們不是直接採用單一步驟的梯度,而是對梯度做指數移動平均。這個平均過程會進一步壓制單一步驟的高頻誤差。所以從最佳化的角度看,混合精度引進的雜訊,反而被優化器自動過濾掉了一部分。

    原因四:損失縮放把下溢問題擋在門外

    前面提過,FP16 最大的風險是梯度下溢。但損失縮放從根本上解決了這個問題:只要你把梯度放大到 FP16 的「甜蜜區間」(大約 10⁻⁴ 到 10³ 之間),就不會下溢,也不會溢位。動態損失縮放更是會自動把梯度維持在這個區間內。

    換句話說,16-bit 的問題不是「無法表達」,而是「表達範圍太窄」。只要我們用一個可調整的縮放因子,把數值移動到那個窄窗口裡,問題就消失了。這是一個典型的工程思維:不改變系統的本質,只調整輸入的尺度。

    原因五:硬體與編譯器的協同設計

    最後一個原因常被忽略,但其實非常重要:現代的 GPU 和加速器,從硬體層面就是為了混合精度而設計的。NVIDIA 從 Volta 架構開始導入 Tensor Core,這是一種專門做 4×4 矩陣乘加的特殊單元,它在硬體內部會用 FP32 來累加 FP16 的乘法結果。

    也就是說,「用 16-bit 輸入、32-bit 累加」這件事情,是寫在晶片電路裡的,不是軟體模擬出來的。Google 的 TPU 也是如此,它內部的 bfloat16 乘法器同樣搭配 FP32 累加器。這種硬體設計讓混合精度不只是「省記憶體」,而是真正能帶來數倍吞吐量提升的關鍵。

    四、FP16 的三個致命陷阱與實戰解法

    理解了原理之後,還是要面對實務上的坑。即使有了 AMP(Automatic Mixed Precision)這樣的自動化工具,很多人在訓練時還是會遇到 loss 變成 NaN、模型完全不收斂、或是訓練速度反而變慢的情況。以下整理三個最常見的陷阱。

    陷阱一:梯度下溢與「靜默失敗」

    最陰險的問題是「靜默失敗」:訓練看起來正常在跑,loss 也緩慢下降,但某些層的參數其實早就停止更新了。這種情況通常發生在梯度極小的層,例如深層網路的底層、或是使用了很小的初始化尺度。

    解法:使用動態損失縮放,並定期監控各層梯度的 norm 值。如果發現某一層的梯度 norm 長期維持在極小值或直接變成 0,就要檢查是否有下溢。PyTorch 的 torch.cuda.amp.GradScaler 會提供 get_scale() 方法,讓你觀察目前的縮放係數變化趨勢。

    陷阱二:Softmax、LayerNorm 等歸約運算的精度問題

    不是所有運算都適合用 FP16。像 softmax、layer normalization、batch normalization 這些涉及「歸約(reduction)」的操作,需要對大量數值做加總或求平均。在 FP16 下累加幾千個數值,很容易因為尾數不足而產生明顯誤差,甚至因為中間結果超過 65504 而溢位。

    解法:把這些運算強制留在 FP32 執行。PyTorch 的 autocast 機制預設就會把 layer_norm、softmax、以及 loss 計算放進 FP32 白名單,這也是為什麼直接用 autocast 通常比自己手動 cast 安全得多。

    陷阱三:BN 統計量的累積誤差

    Batch Normalization 會維護 running mean 和 running variance,這些統計量是跨 step 累積的。如果在 FP16 下更新,會因為精度不足而導致統計量漂移,尤其是在 batch size 較小、資料分佈變化較大的情況下。

    解法:確保 BN 層的參數與 buffer 維持在 FP32。同時可以考慮改用 GroupNorm 或 LayerNorm,這些歸一化方式對精度較不敏感,在混合精度下更穩定。

    五、BF16、TF32、FP8:精度格式的演進路線

    FP16 是混合精度的第一站,但不是終點。隨著模型規模從幾億參數膨脹到上千億,工程界對精度格式的要求也越來越細緻。這段演進史本身就很值得一看,因為它清楚展示了「範圍」與「精度」之間的拉鋸。

    BF16:用精度換範圍的豪賭

    BF16 的設計哲學非常直接:與其讓工程師每天擔心梯度爆掉,不如直接把指數位留滿 8 位,讓它的動態範圍與 FP32 完全一致。代價是尾數只剩 7 位,有效數字大約 2.4 位。

    乍看之下,這個代價很誇張——2.4 位有效數字,連兩位整數都表達不完整。但實際訓練中,BF16 的表現往往不比 FP16 差,甚至更好。原因是:神經網路對尾數精度的容忍度,遠高於對數值範圍的容忍度。只要數字不會爆掉或歸零,粗一點的解析度並不會顯著影響收斂。

    而且 BF16 有一個 FP16 沒有的巨大優勢:不需要損失縮放。因為它的範圍和 FP32 一樣,梯度再小也不會下溢到 0,再大也不會超過上限。這讓訓練流程大幅簡化,也減少了因為損失縮放調不好而導致的各種奇怪問題。這正是為什麼在大型語言模型訓練中,BF16 幾乎成了事實標準。

    TF32:藏在硬體裡的折衷方案

    TF32 是 NVIDIA 在 Ampere 架構中引入的格式,它的定位很微妙。它的儲存格式其實還是 32 位元,但在送入 Tensor Core 做矩陣乘法時,硬體會自動把尾數截斷到 10 位,只保留 8 位指數。換句話說,TF32 是「計算時降精度、儲存時保精度」的硬體級混合精度。

    它的好處是:使用者幾乎不需要修改程式碼,只要把 matmul 的精度設定設為 "high",就能在 A100 上獲得比 FP32 快數倍的矩陣運算,同時因為指數範圍完整,完全不用擔心溢位問題。這種「無痛加速」讓 TF32 成為許多研究團隊的預設選擇。

    FP8:下一站的邊界在哪裡

    到了 H100 世代,FP8 開始進入實用階段。FP8 有兩種變體:E4M3(4 位指數、3 位尾數)與 E5M2(5 位指數、2 位尾數)。前者的精度稍好、範圍較窄,適合前向傳播;後者的範圍較大,適合反向傳播的梯度。

    FP8 的技術難度比 FP16 高得多。因為精度只剩 1 位有效數字左右,傳統的損失縮放已經不夠用,需要更精細的「per-tensor scaling」或「per-block scaling」——也就是為每個張量、甚至每個區塊單獨計算縮放因子。Transformer Engine 這類工具就是在處理這些細節。

    目前 FP8 主要用於大型語言模型的訓練與推論,而且通常搭配 BF16 作為主權重。它的極限在哪裡,還需要更多實證研究來確認。

    六、實戰:用 PyTorch 寫一段混合精度訓練

    講了這麼多理論,最後還是要回到可執行的程式碼。PyTorch 從 1.6 版開始提供 torch.cuda.amp,把混合精度的複雜度封裝成兩個物件:autocast 與 GradScaler。以下是典型的訓練迴圈寫法:

    import torch

    from torch.cuda.amp import autocast, GradScaler

    model = MyModel().cuda()

    optimizer = torch.optim.AdamW(model.parameters(), lr=3e-4)

    scaler = GradScaler()

    for epoch in range(num_epochs):

    for inputs, targets in dataloader:

    inputs = inputs.cuda(non_blocking=True)

    targets = targets.cuda(non_blocking=True)

    optimizer.zero_grad(set_to_none=True)

    前向傳播:自動把適合的運算轉成 FP16

    with autocast(dtype=torch.float16):

    outputs = model(inputs)

    loss = criterion(outputs, targets)

    反向傳播:先放大 loss,再計算梯度

    scaler.scale(loss).backward()

    更新前先把梯度縮回原尺度,並處理 inf/NaN

    scaler.step(optimizer)

    scaler.update()

    這段程式碼有幾個值得注意的細節。首先,autocast 只是一個上下文管理器,它會根據內建的白名單與黑名單,自動決定每個運算要用 FP16 還是 FP32。像矩陣乘法、卷積會走 FP16;而 softmax、layer norm、loss 計算則維持 FP32。你不需要手動 cast 任何張量。

    其次,scaler.scale(loss).backward() 這一行同時做了兩件事:把 loss 乘以當前的縮放係數,然後執行反向傳播。接著 scaler.step(optimizer) 會檢查梯度中是否有 inf 或 NaN:如果沒有,就把梯度除以縮放係數,然後執行 optimizer 的更新;如果有,就跳過這一步,並在 scaler.update() 時把縮放係數減半。

    如果你用的是 BF16,程式碼會更簡單,因為不需要 GradScaler:

    for inputs, targets in dataloader:

    optimizer.zero_grad(set_to_none=True)

    with autocast(dtype=torch.bfloat16):

    outputs = model(inputs)

    loss = criterion(outputs, targets)

    loss.backward()

    optimizer.step()

    這就是 BF16 的最大魅力:少了一個 GradScaler,就少了一整類潛在的 bug。對於剛開始嘗試混合精度的人來說,BF16 其實是更好的起點,前提是你的硬體支援(Ampere 架構以上)。

    硬體層面的加速:Tensor Core 到底在做什麼

    很多人以為混合精度的加速來自「位元數減半所以記憶體頻寬減半」,這只是其中一部分。真正的大頭來自 Tensor Core。

    Tensor Core 是 GPU 內部的特殊運算單元,它能在一個時鐘週期內完成 4×4 矩陣的乘加運算,而且內部的累加器是 FP32 精度。在 Volta 架構上,一顆 Tensor Core 每個週期可以做 64 次 FP16 FMA(fused multiply-add);到了 Ampere 與 Hopper,這個數字又翻了好幾倍。

    關鍵在於:Tensor Core 只接受 16-bit(或更低)的輸入,不支援 FP32。這意味著如果你堅持用純 FP32 訓練,你根本用不到 Tensor Core,只能走傳統的 CUDA Core 路徑。這就是為什麼混合精度往往能帶來 2 到 4 倍的實際訓練加速,而不是帳面上的 2 倍頻寬而已。

    七、什麼情況下不該用混合精度

    混合精度雖然好用,但它不是萬靈丹。以下幾種情況,你可能要特別小心,甚至應該暫時關掉它。

  • 需要高精度數值輸出的任務:例如某些科學計算、物理模擬、或是需要極高精度回歸的模型。如果你的模型輸出對微小誤差極度敏感,FP16 的捨入誤差可能會直接反映在最終指標上。
  • 訓練極小模型或極短訓練:混合精度帶來的收益主要來自大規模矩陣運算。如果你的模型很小、訓練步數很少,轉換與縮放帶來的額外開銷可能反而讓速度變慢。
  • 使用特殊運算層:某些自定義的 CUDA kernel 或第三方算子,可能沒有針對 FP16 做最佳化,甚至根本不支援 FP16。這種情況下強制開啟 autocast,輕則變慢,重則報錯。
  • GAN、強化學習等不穩定訓練:這類訓練本身對梯度雜訊就很敏感,混合精度可能放大不穩定性。通常需要更保守的學習率,或改用 BF16。
  • 除錯階段:當你在排查 loss 爆炸、梯度消失等問題時,先把混合精度關掉,回到純 FP32 環境,可以排除一個重要的變因。
  • 八、結語:16-bit 不是偷工減料,而是精算過的取捨

    回到最初的問題:為什麼用 16-bit 也能訓練?答案其實可以濃縮成一句話:因為我們把「需要精確」的部分與「可以容忍誤差」的部分,徹底分開了。

    權重更新需要精確,所以我們保留 FP32 主權重;梯度計算需要範圍,所以我們用損失縮放把數值搬進安全區間;矩陣乘法也就是絕大多數的計算量,只需要「大致正確」的方向,所以我們放心地交給 FP16 與 Tensor Core。這不是簡化,而是把不同精度用在最適合的地方——這才是「混合」兩個字真正的含義。

    從 FP32 到 FP16、BF16、TF32、再到 FP8,這條演進路線反映的其實是一個更根本的趨勢:隨著模型規模不斷膨脹,我們對「精度」的理解也越來越細緻。不再是「全部用最高精度」,而是「在每個環節用剛剛好的精度」。這種思維不只影響訓練效率,也正在重塑整個 AI 系統的設計哲學。

    下次當你看到訓練日誌裡那一行「AMP enabled」時,不妨想想背後這套精巧的機制:主權重的雙重身分、損失縮放的動態平衡、Tensor Core 的硬體級累加,以及無數工程師在精度與速度之間反覆拉扯出來的經驗。16-bit 能訓練,靠的從來不是運氣,而是對數值行為的深刻理解。

    ```

    🏠 返回首頁