大语言模型中的BF16格式 | AI生成和翻译

Home 2026.05

问题: BF16是什么?它在大型语言模型(LLM)中是如何使用的?

回答:

BF16 从基本原理出发

位布局

浮点格式由三个字段定义:符号位、指数、尾数。

FP32:  [1位符号][8位指数][23位尾数] = 32位
FP16:  [1位符号][5位指数][10位尾数] = 16位
BF16:  [1位符号][8位指数][7位尾数]  = 16位  ← 与FP32指数位数相同

BF16 = “Brain Float 16”,由 Google Brain 开发。关键思路:保留FP32的指数范围,截断尾数

import struct

def fp32_bits(x):
    return struct.pack('f', x).hex()

def show_format(x):
    b = struct.unpack('I', struct.pack('f', x))[0]
    sign     = (b >> 31) & 0x1
    exponent = (b >> 23) & 0xFF
    mantissa = b & 0x7FFFFF
    print(f"FP32  {x}: sign={sign} exp={exponent:08b}({exponent-127:+d}) mantissa={mantissa:023b}")
    # BF16 就是 FP32 的高 16 位
    bf16_mantissa = mantissa >> 16  # 只保留高 7 位
    print(f"BF16  {x}: sign={sign} exp={exponent:08b}({exponent-127:+d}) mantissa={bf16_mantissa:07b}")

show_format(3.14)
show_format(0.0001)
show_format(65504.0)  # FP16 最大值
show_format(3.4e38)   # 接近 FP32 最大值 — BF16 能处理,FP16 会溢出

动态范围比较

格式 指数位数 最大值 最小正规数 精度(十进制位数)
FP32 8 ~3.4e38 ~1.2e-38 ~7.2
FP16 5 65504 ~6.1e-5 ~3.3
BF16 8 ~3.4e38 ~1.2e-38 ~2.3

BF16 与 FP32 具有相同的范围,但精度只有约2位十进制数。 FP16 精度更好,但范围小得可怜——梯度和权重很容易溢出为 inf 或下溢为0。


这对大型语言模型为何重要

1. 训练稳定性

FP16 的经典训练问题:

gradient = 1e-6   # 很小但有效
FP16 最小正规数 ≈ 6e-5
→ 梯度下溢为0 → 梯度消失 → 训练发散

BF16 可以处理这个问题,因为其指数范围等于 FP32:

BF16 最小正规数 ≈ 1.2e-38  ← 梯度 1e-6 完全没问题

这就是为什么 BF16 训练通常不需要损失缩放(而 FP16 需要 GradScaler)。

# FP16 训练 — 需要损失缩放
scaler = torch.cuda.amp.GradScaler()
with torch.autocast(device_type='cuda', dtype=torch.float16):
    loss = model(x)
scaler.scale(loss).backward()
scaler.step(optimizer)
scaler.update()

# BF16 训练 — 更简单,无需缩放器
with torch.autocast(device_type='cuda', dtype=torch.bfloat16):
    loss = model(x)
loss.backward()
optimizer.step()

2. 内存节省

FP32 7B 参数模型:7e9 * 4 字节 = 28 GB
BF16 7B 参数模型:7e9 * 2 字节 = 14 GB  ← 可放入 2 块 A100 40GB 而非 4 块

训练期间仍需优化器状态(Adam:2倍 FP32 参数副本 = 另外 28 GB),但混合精度在前向/反向传播中保持 BF16 权重,而 FP32 主副本用于优化器更新。

3. 硬件加速

现代 GPU 具有原生 BF16 Tensor Core:

GPU BF16 TFLOPS FP32 TFLOPS 加速比
A100 80GB 312 19.5 16x
H100 SXM 989 67 ~15x
RTX 4090 165 82.6 2x

A100/H100 具有巨大的 BF16 加速能力。这是 BF16 成为 LLM 训练标准的主要原因——与 FP32 相比,可免费获得 10-16 倍的吞吐量提升。


混合精度训练:全貌

┌─────────────────────────────────────────────────────────┐
│  前向传播                                               │
│  权重 (BF16) → 激活值 (BF16)                            │
│                           ↓                            │
│  损失 (矩阵乘法输出中 FP32 累加)                        │
│                           ↓                            │
│  反向传播                                               │
│  梯度以 BF16 计算                                       │
│                           ↓                            │
│  优化器步骤                                             │
│  FP32 主权重 ← BF16 梯度转换为 FP32                     │
│  Adam m, v 以 FP32 存储                                 │
│  更新后的 FP32 权重 → 转换回 BF16                       │
└─────────────────────────────────────────────────────────┘

以 nanoGPT 风格实现:

import torch
import torch.nn as nn

model = GPT(config).cuda()

# 优化器自动处理 FP32 主权重
# 使用 AMP 时 — PyTorch 内部管理主副本
optimizer = torch.optim.AdamW(model.parameters(), lr=3e-4)

# torch.compile + BF16 = A100/H100 上的最大吞吐量
model = torch.compile(model)

ctx = torch.autocast(device_type='cuda', dtype=torch.bfloat16)

for x, y in dataloader:
    with ctx:
        logits, loss = model(x, y)
    loss.backward()
    torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
    optimizer.step()
    optimizer.zero_grad(set_to_none=True)

这正是 Karpathy 在 nanoGPT 中使用的内容——dtype=torch.bfloat16 + A100。


推理:超越 BF16 的量化

在推理时,可以更进一步:

训练:     BF16 权重 + BF16 激活值(混合 FP32 累加器)
推理:     INT8 / INT4 权重 + BF16 激活值(GPTQ, AWQ, bitsandbytes)
# bitsandbytes 4位推理
from transformers import AutoModelForCausalLM, BitsAndBytesConfig

bnb_config = BitsAndBytesConfig(
    load_in_4bit=True,
    bnb_4bit_compute_dtype=torch.bfloat16,  # 激活值为 BF16
    bnb_4bit_use_double_quant=True,
    bnb_4bit_quant_type="nf4"               # NormalFloat4
)

model = AutoModelForCausalLM.from_pretrained(
    "meta-llama/Llama-3-8B",
    quantization_config=bnb_config,
    device_map="auto"
)

权重以 INT4 存储(7B 模型约 3.5 GB),在矩阵乘法时动态反量化为 BF16。


BF16 vs FP16:何时使用哪种

场景 建议
在 A100/H100 上训练 BF16 — 始终使用
在较旧 GPU (V100) 上训练 FP16 + GradScaler(V100 没有 BF16 Tensor Core)
在消费级 GPU 上推理 如果支持则用 BF16,否则 FP16
Apple Silicon (MPS) M2+ 支持 BF16
TPU 原生 BF16(Google 为 TPU 设计)

检查支持情况:

print(torch.cuda.is_bf16_supported())  # A100, H100, RTX 30xx+ 上为 True

实际中的精度损失

BF16 只有 7 位尾数 → 相邻可表示值之间的分辨率约为 0.8%。这会带来问题吗?

实证表明:不会,对于 LLM 权重来说。神经网络权重对这种级别的噪声具有鲁棒性——它本质上是免费的正则化。超过约 10 亿参数后,BF16 与 FP32 训练的损失曲线几乎无法区分。

可能出现问题的地方:需要高数值精度的操作——softmax(大数的指数)、层归一化、损失计算。即使在 BF16 自动混合精度期间,这些操作也会在矩阵乘法累加器中以 FP32 内部保留。


总结

BF16 = FP32 的指数 + 7 位尾数
     = 相同范围,更低精度
     = 没有困扰 FP16 的溢出/下溢问题
     = 相比 FP32 节省 2 倍内存
     = A100/H100 Tensor Core 上实现 10-16 倍吞吐量
     = 现代 LLM 训练的标准

如果你在 H100(你正在使用)上训练,BF16 + torch.compile 是默认技术栈——无需 GradScaler,没有因溢出导致的损失尖峰,最大 FLOP 利用率。


Back Donate