Mở đầu: Tại sao không thể "thêm GPU cho nhanh" mãi mãi
Trực giác phổ biến: model AI càng lớn, cứ thêm GPU vào là train nhanh hơn tỷ lệ thuận. Bài học này sẽ phá vỡ trực giác đó bằng cả lý thuyết lẫn con số cụ thể — có một điểm mà thêm worker vào KHÔNG những không giúp gì mà còn làm chậm đi, vì chi phí giao tiếp giữa các worker bắt đầu áp đảo lợi ích tính toán song song. Đây là bài toán kỹ thuật hệ thống thực sự đứng sau con số "huấn luyện trên 10.000 GPU" mà bạn hay đọc thấy.
Bài học đi từ giới hạn bộ nhớ của 1 GPU, qua 3 chiến lược song song hoá (data/model/pipeline parallelism), cơ chế đồng bộ gradient bằng all-reduce, tới bài toán đánh đổi compute-vs-communication được minh hoạ trực tiếp bằng demo tương tác dùng chính công thức trong bài.
1. Vì sao một GPU không đủ
Một model ngôn ngữ lớn không chỉ cần lưu trọng số (weights) — quá trình huấn luyện còn cần lưu gradient và trạng thái optimizer (ví dụ Adam lưu 2 giá trị phụ cho mỗi tham số). Với optimizer Adam ở độ chính xác 32-bit, tổng bộ nhớ cần cho 1 tham số xấp xỉ:
\[ \text{Bộ nhớ} \approx N_{\text{tham số}} \times (4 \text{ byte (weight)} + 4 \text{ byte (gradient)} + 8 \text{ byte (2 trạng thái Adam)}) = N_{\text{tham số}} \times 16 \text{ byte} \]
function estimateTrainingMemoryGB(paramCountBillions) {
const bytesPerParam = 16; // weight(4) + gradient(4) + 2 trạng thái Adam(8), FP32
const totalBytes = paramCountBillions * 1e9 * bytesPerParam;
return totalBytes / 1024 ** 3;
}
// Model 7 tỷ tham số (7B) cần ~104 GB chỉ riêng cho training state —
// vượt xa bộ nhớ của một GPU tiêu dùng phổ biến (24 GB), thậm chí vượt cả
// nhiều GPU trung tâm dữ liệu đơn lẻ (80 GB) => BẮT BUỘC phải chia ra nhiều GPU.
console.log(estimateTrainingMemoryGB(7)); // ≈ 104.3 GB
2. Data, Model & Pipeline Parallelism
Khi 1 GPU không đủ, có 3 chiến lược chia việc cho nhiều GPU — mỗi chiến lược giải quyết một dạng giới hạn khác nhau:
| Chiến lược | Chia cái gì | Khi nào dùng |
|---|---|---|
| Data Parallelism | Chia batch dữ liệu — mỗi GPU giữ TOÀN BỘ model, xử lý 1 phần batch | Model vừa bộ nhớ 1 GPU, muốn tăng thông lượng (throughput) |
| Model Parallelism | Chia CHÍNH model (từng layer/tensor) ra nhiều GPU | Model quá lớn, không vừa bộ nhớ dù chỉ 1 bản sao |
| Pipeline Parallelism | Chia model theo layer thành các "trạm", chạy như dây chuyền lắp ráp | Model rất sâu (nhiều layer), muốn giảm giao tiếp so với model parallelism thô |
3. Đồng bộ Gradient (All-Reduce) & Checkpoint
Với data parallelism, mỗi worker tính gradient trên phần dữ liệu riêng của mình — nhưng để cập nhật model nhất quán, TẤT CẢ worker phải có cùng một gradient trung bình trước khi bước tiếp theo. Đây là việc của thuật toán all-reduce: mỗi worker gửi gradient của mình, nhận về gradient trung bình của toàn bộ nhóm, rồi mới cùng cập nhật trọng số:
// Mô phỏng khái niệm (không phải triển khai mạng thật) của all-reduce
// kiểu "ring" — mỗi worker chỉ nói chuyện với 2 hàng xóm, tổng dữ liệu
// truyền đi KHÔNG phụ thuộc vào số worker (khác all-to-all ngây thơ).
function ringAllReduce(workerGradients) {
const n = workerGradients.length;
const paramCount = workerGradients[0].length;
const averaged = new Array(paramCount).fill(0);
for (let p = 0; p < paramCount; p++) {
let sum = 0;
for (let w = 0; w < n; w++) sum += workerGradients[w][p];
averaged[p] = sum / n; // mọi worker cuối cùng đều có cùng giá trị này
}
return averaged;
}
Vì huấn luyện có thể chạy hàng tuần trên hàng nghìn GPU, xác suất một GPU gặp sự cố phần cứng giữa chừng là không nhỏ. Checkpoint — lưu toàn bộ trạng thái model + optimizer định kỳ — cho phép phục hồi từ điểm gần nhất thay vì train lại từ đầu:
function shouldCheckpoint(step, checkpointEveryNSteps) {
return step > 0 && step % checkpointEveryNSteps === 0;
}
// Đánh đổi: checkpoint dày (vd mỗi 100 bước) tốn thời gian ghi đĩa/mạng lưu trữ
// nhưng phục hồi nhanh sau sự cố; checkpoint thưa (vd mỗi 5000 bước) tốn ít
// overhead nhưng mất nhiều tiến độ hơn nếu sự cố xảy ra ngay trước điểm lưu.
4. Sharding & Overhead Giao Tiếp
Đây là nơi lý thuyết gặp thực tế: thêm worker giúp compute per step giảm (chia việc), nhưng sync overhead lại tăng (nhiều worker hơn = nhiều dữ liệu gradient phải trao đổi/điều phối hơn). Tổng thời gian mỗi bước là tổng của cả hai — và tại một điểm nào đó, overhead thắng thế:
\[ T_{\text{step}}(N) = \underbrace{\frac{T_{\text{compute}}}{N}}_{\text{giảm theo N}} + \underbrace{T_{\text{sync}}(N)}_{\text{tăng theo N}} \]
5. Thực hành tương tác: Distributed Training Simulator
Chọn số lượng worker và quan sát: thời gian compute/step giảm, nhưng overhead đồng bộ tăng — cột màu xanh đặc là thời gian THẬT (có overhead), phần vân chéo là thời gian LÝ TƯỞNG (song song hoàn hảo, không overhead) để bạn thấy khoảng cách giữa lý thuyết và thực tế ngày càng doãng ra khi N lớn:
export function simulateTraining(workerCount) {
const computePerStep = BASE_COMPUTE_MS_PER_STEP / workerCount;
const syncOverhead = workerCount > 1
? FIXED_SYNC_LATENCY_MS + SYNC_COST_PER_EXTRA_WORKER_MS * (workerCount - 1)
: 0;
const stepTime = computePerStep + syncOverhead;
// ... nhân với tổng số bước để ra tổng thời gian huấn luyện
}
Nếu bạn muốn xem cách song song hoá thật sự được thực thi ở mức phần cứng (GPU thread/warp), xem thêm Series 2 — WebGPU Compute Shader: Nhân Ma Trận — cùng ý tưởng song song hoá nhưng ở tầng GPU compute thay vì tầng điều phối cụm máy.
Tải file code thực hành minh họa bài học
File JavaScript aisys-pipeline-sim.js — mô hình chi phí compute/sync theo số worker và hàm
render demo tương tác dùng trong bài:
📖 Tài liệu tham khảo
- Kỹ thuật Megatron-LM (model parallelism): Megatron-LM: Training Multi-Billion Parameter Language Models (Shoeybi et al., 2019) — nền tảng cho kỹ thuật chia tensor/model parallelism hiện đại.
- ZeRO — tối ưu bộ nhớ optimizer: ZeRO: Memory Optimizations Toward Training Trillion Parameter Models (Rajbhandari et al., 2019) — kỹ thuật sharding trạng thái optimizer nhắc ở mục 1 và 4.
- Ring All-Reduce: Horovod / Ring All-Reduce — bài trình bày kỹ thuật — thuật toán đồng bộ gradient dùng thực tế trong công nghiệp.
Bình luận