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.


📚 Điều kiện tiên quyết
Nên đọc Bài 2 trước (dữ liệu phải sẵn sàng trước khi huấn luyện). Nếu bạn chưa quen gradient descent, xem qua Series 12 — Gradient Descent — bài này giả định bạn đã biết khái niệm gradient, chỉ tập trung vào góc độ hệ thống/hạ tầng.

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} \]

estimate-training-memory.js
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
🕳️ Cạm bẫy: Nhầm bộ nhớ inference với bộ nhớ training
Người mới thường ước lượng bộ nhớ theo kích thước file model (thường chỉ lưu weight ở FP16/INT8, dùng cho suy luận/inference) rồi bất ngờ khi training "hết bộ nhớ". Training cần thêm gradient + trạng thái optimizer + activation trung gian (để backprop) — tổng bộ nhớ training thường gấp 4–6 lần bộ nhớ cần cho riêng inference cùng model đó.

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ô
💡 Mẹo: Ba chiến lược không loại trừ nhau
Các hệ thống huấn luyện LLM quy mô lớn thật sự thường kết hợp CẢ BA cùng lúc (gọi là "3D parallelism") — ví dụ dùng pipeline parallelism giữa các nhóm GPU, model parallelism trong mỗi nhóm, và data parallelism giữa các bản sao pipeline. Bài này chỉ mô phỏng data parallelism (đơn giản nhất) để làm rõ cơ chế cốt lõi của việc đồng bộ.

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ố:

all-reduce-concept.js
// 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:

checkpoint-strategy.js
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}} \]

🔬 Đào sâu: Vì sao demo dưới đây cho thấy 8 worker CHẬM HƠN 4 worker
Với các con số giả định trong demo (200ms compute/step ở N=1, overhead đồng bộ 10ms cố định + 8ms cho mỗi worker thêm vào), tổng thời gian mỗi bước đạt cực tiểu quanh N=4, rồi TĂNG trở lại khi N=8, 16, 32. Đây không phải lỗi mô phỏng — đó chính xác là hiện tượng có thật khiến các đội hạ tầng ML phải đo đạc kỹ trước khi chọn số lượng GPU cho một job huấn luyện, thay vì mặc định "càng nhiều càng nhanh".

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:

⚙️ Distributed Training Simulator
Thời gian thật Lý tưởng (không overhead)
aisys-pipeline-sim.js (trích)
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 về aisys-pipeline-sim.js

📖 Tài liệu tham khảo

Bài viết liên quan trong series

Bài 2: Thu Thập & Làm Sạch Dữ Liệu Bài 4: RLHF & Alignment Quay lại Lộ trình Kỹ Thuật Hệ Thống AI

Bình luận