Mixed Precision Training
Huấn luyện hỗn hợp độ chính xác
Một mô hình 7B tham số train 4 ngày trên 8 GPU A100. Có cách nào giảm xuống ~2 ngày mà hầu như không mất chất lượng?
Hình minh họa
So sánh cách các định dạng lưu cùng một số thực
Nhập một giá trị bất kỳ và xem FP32, FP16, BF16, FP8 (E4M3/E5M2), INT8 lưu nó như thế nào. Sai số làm tròn là lý do Mixed Precision phải chọn lọc chỗ nào dùng precision nào.
Chi tiết FP16 (half precision)
IEEE 754 half — dải hẹp (±65504), dễ underflow gradient, cần loss scaling.
Min normal
6.104e-5
Max finite
6.550e+4
Epsilon (~ULP)
9.766e-4
Relative error
0.030801%
Exponent=1, mantissa làm tròn 584/1024.
Hình minh họa
Gradient underflow và cứu bởi loss scaling
Mỗi layer có một gradient mẫu. Khi lưu trong FP16 thuần, nhiều giá trị bị flush về 0 (underflow). Trượt thanh scale factor để xem loss scaling đưa gradient về dải biểu diễn được.
Layer có gradient
6
Underflow trong FP16 thuần
2
Được cứu bởi scale
2
| Layer | g gốc | g × S | FP16 (thuần) | FP16 (scaled) | BF16 | Tình trạng |
|---|---|---|---|---|---|---|
| embedding.weight | 3.20e-5 | 3.28e-2 | 3.20e-5 | 3.28e-2 | 3.19e-5 | OK |
| attn.qkv.weight | 8.10e-6 | 8.29e-3 | 8.10e-6 | 8.29e-3 | 8.11e-6 | OK |
| attn.out.weight | 4.00e-7 | 4.10e-4 | 4.00e-7 | 4.10e-4 | 4.00e-7 | OK |
| mlp.fc1.weight | 2.20e-8 | 2.25e-5 | 0 (lost) | 2.25e-5 | 2.20e-8 | cứu |
| mlp.fc2.weight | 6.30e-9 | 6.45e-6 | 0 (lost) | 6.45e-6 | 6.29e-9 | cứu |
| lm_head.weight | 1.10e-6 | 1.13e-3 | 1.10e-6 | 1.13e-3 | 1.10e-6 | OK |
Chú ý cột BF16 không bao giờ underflow — dải exponent của nó rộng như FP32 nên hầu như không cần loss scaling khi huấn luyện LLM.
Không phải mọi phép tính đều cần chính xác như nhau! Nhân ma trận (forward/backward) chịu được sai số nhỏ — nên ta dùng FP16/BF16 để nhanh 2-3x trên Tensor Cores. Nhưng cập nhật trọng số cần tích luỹ những thay đổi cực nhỏ qua hàng triệu bước — phải dùng FP32 để không đánh mất từng hạt gradient. Mixed Precision chính là chọn đúng precision cho đúng công việc.
Gradient có giá trị 1e-8. Ngưỡng normal nhỏ nhất của FP16 là ~6.1e-5. Điều gì xảy ra khi ta lưu gradient này ở FP16 không có loss scaling?
Mô hình đang train ổn định ở FP32. Bạn bật autocast FP16 (không đổi gì khác) và sau 100 step loss trở thành NaN. Bước đầu tiên bạn nên thử là gì?
Giải thích
Mixed Precision Training (Micikevicius et al., 2018) kết hợp nhiều định dạng số thực trong cùng một quá trình huấn luyện, nhằm tận dụng tốc độ của Tensor Cores mà vẫn giữ ổn định số học. Khác với quantization (thường cho inference), mixed precision được dùng khi đang huấn luyện và phụ thuộc chặt vào kiến trúc GPU (xem thêm tối ưu GPU).
1. Master Weights FP32. Ta giữ một bản sao FP32 của trọng số để tích luỹ các bước cập nhật nhỏ:
Đầu mỗi step ta cast sang FP16 để forward/backward; cuối step ta unscale gradient và cập nhật bản FP32.
2. Loss Scaling. Gradient thực tế thường nằm trong khoảng 1e-9 tới 1e-3. FP16 chỉ phủ ~6e-5 tới 65504. Ta nhân loss lên S trước backward để dịch phân phối gradient lên dải FP16:
3. FP16 / BF16 Compute. Forward và backward chạy ở 16 bit trên Tensor Cores. Accumulator (phép cộng) vẫn ở FP32 — tức là D = A·B + C với A, B ở FP16 và C, D ở FP32. Cách này giữ được độ chính xác khi tổng hàng nghìn phần tử.
import torch
from torch import nn
from torch.amp import autocast, GradScaler
from torch.utils.data import DataLoader
device = "cuda"
model = MyTransformer().to(device)
optimizer = torch.optim.AdamW(model.parameters(), lr=3e-4)
criterion = nn.CrossEntropyLoss()
# GradScaler duy nhất cho toàn bộ quá trình train
scaler = GradScaler()
for epoch in range(num_epochs):
for batch in DataLoader(train_ds, batch_size=32):
optimizer.zero_grad(set_to_none=True)
# 1) Forward + loss ở FP16 (hoặc BF16)
with autocast(device_type=device, dtype=torch.float16):
logits = model(batch.input_ids)
loss = criterion(logits, batch.labels)
# 2) Nhân loss lên S rồi backward (gradient đã được scale)
scaler.scale(loss).backward()
# 3) Unscale + clip gradient nếu cần
scaler.unscale_(optimizer)
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
# 4) Step: nếu overflow -> skip, sau đó update scale
scaler.step(optimizer)
scaler.update()Cái bẫy cần tránh. Một số op rất nhạy với FP16 và nên giữ ở FP32: softmax (có exp), layernorm (trừ trung bình), loss cross-entropy (log), một số op reduce-sum dài. PyTorch autocast xử lý phần lớn các trường hợp này, nhưng khi viết kernel custom cần chú ý.
torch.cumsum, torch.prod, phép toán số phức, và nhiều custom CUDA kernel không có policy autocast. Khi gặp NaN, hãy kiểm tra xem có op tự cast mà không thông báo không.- Mixed precision = FP16/BF16 cho forward+backward (nhanh 2-3x trên Tensor Cores) kết hợp FP32 cho master weights và update.
- FP16 có dải hẹp (6e-5 → 65504) nên cần loss scaling để gradient không underflow; BF16 có dải như FP32 nên gần như không cần.
- GradScaler động: nhân loss lên S trước backward, unscale trước update, tự điều chỉnh S khi phát hiện Inf/NaN.
- Tensor Cores chạy matmul 16-bit nhưng accumulate 32-bit — nhờ đó giữ được độ chính xác khi tổng hàng nghìn phần tử.
- FP8 trên Hopper dùng E4M3 cho activation và E5M2 cho gradient — tăng tốc thêm 2x so với BF16.
- Một số op nhạy (softmax, layernorm, loss CE) nên giữ FP32 để tránh NaN — autocast PyTorch xử lý phần lớn tự động.
Kiểm tra hiểu biết
Vì sao mixed precision giữ master weights ở FP32 thay vì dùng toàn bộ FP16?