$ cat ./blog/gen-ai/llm-fundamentals/pretraining-va-scaling-laws.md
seriesCách hoạt động của LLMbài 10

Pretraining: next-token prediction và scaling laws

Bài thứ mười trong series Cách hoạt động của LLM: pretraining chỉ có một objective là cross-entropy trên token kế tiếp. Bài này đi qua loss, perplexity và bits-per-byte chạy trên model thật, scaling laws của Kaplan và Chinchilla cùng cuộc tranh luận sau đó, cách tính ngân sách compute ra GPU-giờ, mixed precision, bốn kiểu song song DP, FSDP, TP, PP, và các dấu hiệu cho thấy một lần train đang hỏng.

~/diagrams/pretraining-va-scaling-laws/pretraining-pipeline.htmltương tác
mở riêng ↗
HìnhHàng trên là một bước train: micro-batch cắt từ token stream đi qua forward bằng bf16, ra logits ở mọi vị trí; cross-entropy so logits với token kế tiếp; backward tính gradient rồi đồng bộ giữa các GPU; AdamW cập nhật bản master weights fp32; định kỳ ghi checkpoint và theo dõi loss, grad norm, MFU. Vòng lặp quay lại micro-batch kế tiếp với learning rate theo lịch. Hàng dưới là sáu hiểu lầm phổ biến, mỗi cái nối với bước mà nó hiểu sai. Mục 11 giải thích từng hiểu lầm.
bước tính trên GPU (forward, loss, backward)dữ liệu: token stream và micro-batchkhung đỏ: cập nhật trọng số; khung tím: checkpoint; khung xám: hiểu lầm

Bài thứ chín coi model như một hàm có sẵn: đưa context vào, nhận về logits tốt, phần còn lại là chuyện chọn token. Bài này quay lại câu hỏi đứng trước tất cả: model học ra những logits đó bằng cách nào?

Câu trả lời ngắn gọn đến mức dễ coi thường. Pretraining chỉ tối ưu đúng một thứ: cross-entropy của token kế tiếp, trên vài nghìn tỉ token. Không có nhãn do người gán, không có reward, không có mục tiêu phụ nào đáng kể. Cái khó không nằm ở objective mà nằm ở quy mô. Khi một lần train tốn hàng triệu GPU-giờ, ba câu hỏi kỹ thuật quyết định nó thành hay bại:

  • Chia ngân sách thế nào? Với một lượng compute cố định, nên dùng model to train trên ít token, hay model nhỏ train trên nhiều token? Đây là câu hỏi của scaling laws.
  • Nhét vào phần cứng thế nào? Một model 8B đã không vừa một GPU 80 GB khi train. Phải dùng mixed precision và chia model, chia dữ liệu lên hàng nghìn GPU.
  • Biết nó đang hỏng thế nào? Loss tăng vọt, gradient phát nổ, GPU chết giữa chừng. Ở quy mô lớn những chuyện này không phải tai nạn mà là chuyện hằng ngày.

Bài đi qua cả ba, theo thứ tự trong Hình 1 ở đầu trang.


01Từ logits đến loss

Bài 1, mục 5.2 đã nói ý quan trọng nhất: pretraining là self-supervised learning. Văn bản tự tạo ra nhãn cho chính nó, vì token đứng sau luôn là đáp án cho các token đứng trước. Bài này không nhắc lại phần đó.

Điều cần thêm vào là vai trò của logits. Ở bài 9, logits là đầu vào của decoding. Ở bài này, logits là đầu vào của loss. Cùng một forward pass, cùng một tensor [batch, seq_len, vocab_size], chỉ khác người dùng nó:

  • lúc inference, ta chỉ cần logits ở vị trí cuối để chọn token tiếp theo;
  • lúc train, ta dùng logits ở mọi vị trí, so từng vị trí với token thật đứng ngay sau nó, rồi lấy trung bình.

Phần còn lại của pretraining, từ scaling laws đến song song hoá, đều là kỹ thuật để làm phép so sánh này trên càng nhiều token càng tốt, với chi phí càng thấp càng tốt.


02Objective: next-token prediction

Công thức

Với một sequence x₁, x₂, …, x_T, model trả ra xác suất p(x_t | x_<t) cho mỗi vị trí. Loss của pretraining là negative log-likelihood trung bình:

L = −(1/T) · Σ_t log p(x_t | x₁, …, x_{t−1})

Đây chính là cross-entropy giữa phân phối model dự đoán và phân phối “one-hot” của token thật. Đơn vị là nats khi dùng logarit tự nhiên, như mọi thư viện deep learning.

Ba chi tiết làm objective này rẻ đến bất ngờ:

  1. Teacher forcing. Ở vị trí t, model luôn được nhìn token thật x_<t, không phải token do chính nó đoán ở bước trước. Nhờ đó mọi vị trí độc lập với nhau về mặt đầu vào.
  2. Causal mask. Mask tam giác trong attention bảo đảm vị trí t không nhìn được x_t trở đi. Kết hợp với teacher forcing, ta tính loss cho cả T vị trí trong một forward pass duy nhất, thay vì chạy model T lần. Một sequence 8.192 token cho ra 8.191 bài toán dự đoán chỉ với một lần chạy.
  3. Không cần nhãn. Chỉ cần dịch sequence đi một vị trí là có target.

Việc dịch labels một bước, vì sao labels = input_ids không có nghĩa là model học chép lại token hiện tại, và ba loại mask trong một sample đã được giải thích ở bài 5, mục 11. Bài này dùng lại kết quả đó.

Loss, perplexity và bits-per-byte

Cùng một đại lượng có ba cách đọc:

Đại lượng Công thức Đọc thế nào
Loss L (nats/token) con số optimizer thật sự tối thiểu hoá
Perplexity exp(L) model “phân vân” trung bình giữa bao nhiêu token
Bits-per-byte Σ nats / (ln 2 · số byte) số bit cần để nén mỗi byte văn bản

Perplexity 8 nghĩa là trung bình model không chắc chắn hơn việc bốc ngẫu nhiên đều giữa 8 ứng viên. Nghe trực quan, nhưng cả loss lẫn perplexity đều tính trên mỗi token, nên chúng phụ thuộc vào tokenizer. Một tokenizer cắt văn bản thành nhiều token nhỏ sẽ có nhiều vị trí dễ đoán (nửa sau của một từ thường rất dễ), kéo loss trung bình xuống mà model không giỏi hơn chút nào.

Bits-per-byte giải quyết chuyện đó bằng cách chia tổng lượng thông tin cho số byte của văn bản gốc, thứ không phụ thuộc tokenizer. Đây là lý do khi so sánh hai model khác tokenizer, như các bảng so sánh giữa Llama, Qwen và Gemma, người ta dùng bits-per-byte hoặc bits-per-character chứ không dùng perplexity. Bài 4 đã cho thấy cùng một câu tiếng Việt có thể ra số token rất khác nhau giữa các tokenizer, và đó chính là chỗ perplexity trở nên vô nghĩa khi đem so.

Tính trên một model thật

Đoạn code dưới đây chạy Qwen2.5-0.5B bản base (chưa qua post-training) trên CPU. Nó tính loss theo hai cách, cách của thư viện và cách tự viết từ logits, rồi quy ra perplexity và bits-per-byte cho một đoạn tiếng Anh và một đoạn tiếng Việt có nội dung tương đương.

# Python 3.12, torch 2.14.1 (CPU), transformers 5.18.0
import math
import torch
import torch.nn.functional as F
from transformers import AutoModelForCausalLM, AutoTokenizer

name = "Qwen/Qwen2.5-0.5B"                     # base model, chưa qua post-training
tok = AutoTokenizer.from_pretrained(name)
model = AutoModelForCausalLM.from_pretrained(name, dtype=torch.float32).eval()

texts = {
    "en": "The capital of France is Paris. It is known for the Eiffel Tower, "
          "the Louvre museum and its cafés along the river Seine.",
    "vi": "Thủ đô của Pháp là Paris. Thành phố nổi tiếng với tháp Eiffel, "
          "bảo tàng Louvre và những quán cà phê dọc sông Seine.",
}

for lang, text in texts.items():
    # Đặt <|endoftext|> ở đầu để token đầu tiên của văn bản cũng được dự đoán.
    ids = tok(tok.eos_token + text, return_tensors="pt").input_ids
    with torch.no_grad():
        out = model(ids, labels=ids)           # HF tự shift labels bên trong

    # Tự tính lại: logits ở vị trí t dự đoán token ở vị trí t+1.
    logits = out.logits[0, :-1]                # [T-1, vocab]
    target = ids[0, 1:]                        # [T-1]
    nll = F.cross_entropy(logits, target, reduction="none")   # nats, mỗi token

    n_tok, n_byte = target.numel(), len(text.encode("utf-8"))
    loss = nll.mean().item()
    print(f"[{lang}] logits {tuple(out.logits.shape)}  tokens={n_tok}  bytes={n_byte}")
    print(f"     loss HF     = {out.loss.item():.4f} nats/token")
    print(f"     loss tự tính = {loss:.4f} nats/token")
    print(f"     perplexity  = {math.exp(loss):.2f}")
    print(f"     bits/byte   = {nll.sum().item() / math.log(2) / n_byte:.4f}")
    worst = nll.topk(3)
    print("     khó đoán nhất:", [(tok.decode(target[i]), round(v.item(), 2))
                                   for v, i in zip(worst.values, worst.indices)])

Output khi chạy:

[en] logits (1, 32, 151936)  tokens=31  bytes=121
     loss HF     = 2.0763 nats/token
     loss tự tính = 2.0763 nats/token
     perplexity  = 7.97
     bits/byte   = 0.7674
     khó đoán nhất: [(' capital', 9.09), (' caf', 9.04), (' along', 6.94)]
[vi] logits (1, 36, 151936)  tokens=35  bytes=145
     loss HF     = 2.5614 nats/token
     loss tự tính = 2.5614 nats/token
     perplexity  = 12.95
     bits/byte   = 0.8920
     khó đoán nhất: [('Th', 10.22), ('ủ', 7.34), (' th', 6.64)]

Bốn điều đáng để ý:

  • Logits có ở mọi vị trí. Tensor (1, 32, 151936) nghĩa là một forward pass cho ra phân phối trên toàn bộ 151.936 token của vocabulary ở cả 32 vị trí. Ta dùng 31 vị trí đầu để dự đoán 31 token đứng sau, đúng như mục trước mô tả.
  • Loss của thư viện chính là cross-entropy trung bình. Hai dòng loss HF và loss tự tính trùng nhau đến chữ số thứ tư, vì model(ids, labels=ids) làm đúng phép shift và trung bình ở trên.
  • Token khó đoán nhất là token đầu thông tin. Chữ capital ở đầu câu tiếng Anh và Th ở đầu câu tiếng Việt có loss cao nhất, vì không có gì đứng trước để đoán. Sau khi biết “Thủ đô của Pháp là”, chữ “Paris” gần như không tốn gì.
  • So sánh giữa hai ngôn ngữ phải dùng bits-per-byte. Hai đoạn có số token và số byte khác nhau, nên perplexity 7,97 và 12,95 không cùng thước đo. Bits-per-byte (0,77 so với 0,89) mới cho biết model nén văn bản tiếng Việt kém hơn tiếng Anh một chút trên đơn vị byte, điều dễ hiểu với một model được train phần lớn trên tiếng Anh và tiếng Trung. Hai đoạn văn ngắn như vậy chỉ minh hoạ cách tính, không phải một phép đo đáng tin về năng lực tiếng Việt của model.

03Một bước train trông như thế nào

Phần này chỉ đủ để đọc các mục sau, không đi sâu vào optimizer. Một bước train, theo đúng hàng trên của Hình 1, gồm bốn việc:

  1. Forward: lấy một batch sequence, chạy qua model, ra logits ở mọi vị trí.
  2. Loss: cross-entropy giữa logits và token kế tiếp, như mục 02.
  3. Backward: tính gradient của loss theo từng tham số. Chi phí xấp xỉ gấp đôi forward.
  4. Optimizer step: dùng gradient để cập nhật tham số. LLM ngày nay gần như đều dùng AdamW, kèm gradient clipping theo global norm (PaLM dùng ngưỡng 1,0, PaLM paper mục 5) để chặn những bước cập nhật quá lớn.

Hai siêu tham số quyết định phần lớn hành vi của bước 4 là learning rate và batch size.

Lịch learning rate

Learning rate không cố định mà đi theo một lịch:

  • Warmup: tăng tuyến tính từ gần 0 lên giá trị đỉnh trong vài nghìn bước đầu. Ở đầu quá trình train, trọng số còn ngẫu nhiên và các ước lượng moment của Adam chưa ổn định; bước nhảy lớn lúc này dễ làm train phân kỳ.
  • Cosine decay: sau warmup, giảm dần theo nửa chu kỳ cosine về một giá trị nhỏ. Llama 3 405B dùng đỉnh 8 × 10⁻⁵, warmup tuyến tính 8.000 bước, rồi cosine giảm về 8 × 10⁻⁷ trong 1,2 triệu bước (Llama 3 paper, mục 3.4.1).
  • WSD (Warmup-Stable-Decay): warmup, giữ learning rate không đổi trong phần lớn quá trình, rồi giảm nhanh ở đoạn cuối. MiniCPM (Hu et al. 2024) đề xuất lịch này vì nó cho phép rẽ nhánh từ giai đoạn “stable” để thử nhiều độ dài train khác nhau mà không phải train lại từ đầu. Với cosine, độ dài lịch phải chốt từ trước, và chi tiết này sẽ quay lại ở mục 05.

Batch size tính bằng token

Ở quy mô LLM, batch size được đếm bằng token, không phải số sequence. Llama 3 405B bắt đầu với 4M token mỗi batch (sequence dài 4.096), tăng lên 8M token (sequence 8.192) sau 252M token đầu, rồi lên 16M token sau 2,87T token (mục 3.4.1). PaLM 540B cũng tăng dần từ 1M lên 2M rồi 4M token (PaLM paper, mục 5). Lý do được nêu trong cả hai paper: batch nhỏ ổn định hơn và hiệu quả hơn trên mỗi token ở đầu quá trình train, batch lớn thì tận dụng phần cứng tốt hơn về sau.

Một batch 16M token không nằm trên một GPU. Nó được chia thành các micro-batch cho từng GPU, và cách chia đó là chủ đề của mục 09.


04Scaling laws: Kaplan 2020

Câu hỏi của scaling laws rất thực dụng: nếu tôi có thêm 10 lần compute, tôi nên tăng kích thước model hay tăng số token, và loss sẽ giảm bao nhiêu?

Ba quy luật luỹ thừa

Kaplan et al. 2020 train hàng loạt Transformer với số tham số không tính embedding từ 768 đến 1,5 tỉ, trên tập dữ liệu từ 22 triệu đến 23 tỉ token (WebText2), và thấy loss giảm theo một power law khá đều với từng đại lượng khi đại lượng đó là nút thắt:

L(N) = (N_c / N)^α_N        α_N ≈ 0,076    N_c ≈ 8,8 × 10¹³ tham số (không tính embedding)
L(D) = (D_c / D)^α_D        α_D ≈ 0,095    D_c ≈ 5,4 × 10¹³ token
L(C_min) = (C_c / C_min)^α_C   α_C ≈ 0,050    C_c ≈ 3,1 × 10⁸ PF-ngày

(Kaplan et al. 2020, phương trình 1.1–1.3 và Bảng 5. Dòng thứ ba là L(C_min) ở phương trình 1.3: C_min là compute tối thiểu để đạt một mức loss, quy đổi về trường hợp batch size nhỏ hơn nhiều so với critical batch size, chứ không phải compute thô của từng run. Một PF-ngày là 10¹⁵ FLOP/s chạy trong một ngày, tức khoảng 8,64 × 10¹⁹ FLOPs.)

Số mũ nhỏ nghĩa là cải thiện rất chậm nhưng rất đều. Với α_N = 0,076, tăng N lên gấp đôi chỉ nhân loss với 2^(−0,076) ≈ 0,95, tức giảm khoảng 5%. Muốn loss giảm một nửa chỉ nhờ tăng N, cần tăng N lên khoảng 2^(1/0,076) ≈ 9.000 lần. Đây là lý do mỗi thế hệ model cần nhiều compute hơn thế hệ trước cả một bậc độ lớn.

Hằng số N_c, D_c không có ý nghĩa vật lý; paper nói rõ chúng phụ thuộc tokenizer và dữ liệu. Thứ chuyển được từ setup này sang setup khác là dạng của quy luật và độ lớn tương đối của các số mũ.

Kết luận “tăng N trước”

Từ các quy luật trên, Kaplan suy ra cách chia một ngân sách compute C sao cho loss thấp nhất (Bảng 6):

N_opt ∝ C^0,73        D_opt ∝ C^0,27

Tăng compute lên 10 lần thì nên tăng model lên 10^0,73 ≈ 5,4 lần nhưng số token chỉ 10^0,27 ≈ 1,9 lần. Kết luận đi kèm: model lớn hiệu quả về mẫu hơn, nên tối ưu là train model rất lớn và dừng sớm, xa trước khi hội tụ.

Kết luận này định hình cả một thế hệ model. GPT-3 175B, Jurassic 178B, Gopher 280B đều được train trên khoảng 300 tỉ token (Chinchilla paper, Bảng 1), tức chưa đến 2 token cho mỗi tham số.

Các giả định cần nhớ

Ba chi tiết phương pháp của Kaplan sẽ trở thành trung tâm của cuộc tranh luận ở mục sau:

  • N đếm không tính embedding, và compute cũng bỏ phần embedding.
  • Mọi run dùng cùng một lịch learning rate có độ dài cố định, bất kể run đó định train bao nhiêu token. Loss ở các điểm trung gian được dùng như thể đó là loss cuối của một run ngắn hơn.
  • Phần lớn model nhỏ, nhiều model dưới 100M tham số.

05Chinchilla và cuộc tranh luận sau đó

Ba cách fit, một kết luận

Hoffmann et al. 2022 (DeepMind) đặt lại đúng câu hỏi của Kaplan nhưng với hơn 400 model từ 70M đến hơn 16B tham số, train trên 5B đến hơn 400B token (mục 1; abstract làm tròn thành 5 đến 500 tỉ token). Họ dùng ba phương pháp độc lập:

  1. Cố định kích thước model, đổi số token (mục 3.1): với mỗi lượng FLOPs, lấy điểm loss thấp nhất trên tất cả đường cong train.
  2. IsoFLOP profiles (mục 3.2): cố định FLOPs, quét kích thước model, tìm đáy của đường loss hình chữ U.
  3. Fit một hàm loss tham số (mục 3.3):
L(N, D) = E + A / N^α + B / D^β

Ba số hạng có nghĩa riêng: E là loss không thể giảm (entropy của chính ngôn ngữ), A / N^α là phần thiếu do model quá nhỏ, B / D^β là phần thiếu do train quá ít dữ liệu. Giá trị fit được báo cáo là E = 1,69, A = 406,4, B = 410,7, α = 0,34, β = 0,28 (Phụ lục D.2, phương trình 10).

Kết quả của cả ba cách (Bảng 2):

Phương pháp a trong N_opt ∝ C^a b trong D_opt ∝ C^b
1. Cực tiểu trên các đường train 0,50 0,50
2. IsoFLOP profiles 0,49 0,51
3. Fit hàm loss tham số 0,46 0,54
Kaplan et al. 2020 0,73 0,27

Cả ba đều nói N và D nên tăng ngang nhau. Bảng 3 của paper quy ra con số mà ai cũng nhớ: một model 1B cần khoảng 20,2B token, model 10B cần 205,1B token, tức khoảng 20 token cho mỗi tham số. Theo tỉ lệ đó, GPT-3 và Gopher bị undertrained nặng.

Ví dụ Chinchilla 70B train trên 1,4T token đánh bại Gopher 280B với compute tương đương đã có ở bài 5, mục 8.2, nên bài này không kể lại.

Vì sao Kaplan và Chinchilla khác nhau

Hai nhóm cùng dùng Transformer, cùng đo cross-entropy, sao lại ra 0,73 và 0,50? Câu trả lời được làm rõ dần qua ba paper.

Chinchilla tự đưa ra giả thuyết đầu tiên (mục 2): Kaplan dùng một lịch cosine cố định cho mọi run. Loss ở giữa một lịch dài cao hơn loss cuối của một run có lịch vừa khít với số token đó. Dùng các điểm trung gian ấy làm ước lượng sẽ đánh giá thấp lợi ích của việc train model nhỏ trên ít token, đẩy kết luận về phía “tăng N nhanh hơn D”. Phần phụ lục của chính paper (Hình A1) cho thấy khi độ dài chu kỳ cosine vượt số bước train thật hơn 25%, loss cuối xấu đi rõ rệt.

Porian et al. 2024 tái hiện Kaplan rồi sửa từng yếu tố một và tìm ra ba nguyên nhân: Kaplan không tính chi phí của layer cuối (lm_head), dùng warmup dài cố định, quá dài với model nhỏ, và không chỉnh optimizer theo từng quy mô. Sửa cả ba thì kết quả khớp với Chinchilla. Điều bất ngờ: họ thấy chỉnh lịch learning rate theo số token, đúng giả thuyết của Chinchilla, lại không phải yếu tố quyết định.

Pearce và Song 2024 đi từ hướng ngược lại: bắt đầu từ dữ liệu kiểu Chinchilla, đếm N theo cách của Kaplan (bỏ embedding) và giới hạn ở quy mô nhỏ như Kaplan. Phần lớn chênh lệch tự xuất hiện. Ở model nhỏ, embedding chiếm tỉ lệ tham số lớn, nên bỏ nó đi làm méo đường cong đúng ở vùng Kaplan đo nhiều nhất.

Hai paper đi từ hai phía và đều xác nhận hệ số của Chinchilla. Xu hướng Kaplan đo được vẫn tái hiện được trong setup của Kaplan, nhưng số mũ 0,73 là một ước lượng bị lệch do cách đếm tham số và cách thiết kế thí nghiệm. Sửa các yếu tố đó thì kết quả khớp với ~0,5 của Chinchilla. Pearce và Song viết rằng paper của họ “reaffirms Chinchilla’s scaling coefficients” và giải thích “much of” chênh lệch, tức phần lớn chứ không phải toàn bộ. Hai paper cũng chỉ ra những nguyên nhân khác nhau: Pearce và Song nêu embedding và quy mô nhỏ; Porian nêu layer cuối, warmup và cách chỉnh optimizer, và ghi rõ Pearce và Song không phát hiện vấn đề warmup. Một chi tiết dễ nhầm: 0,73 là số mũ theo C_min đã điều chỉnh; số mũ thô theo compute thực tế của Kaplan là 0,88.

Bản fit gốc của Chinchilla cũng bị chỉnh lại

Besiroglu et al. 2024 trích lại dữ liệu từ hình của Chinchilla, fit lại phương pháp 3 và thấy ba vấn đề. Hệ số đã làm tròn trong paper fit dữ liệu kém. Khoảng tin cậy hẹp đến mức không hợp lý với số điểm dữ liệu. Quan trọng nhất, hệ số của phương pháp 3 suy ra khoảng 70 token mỗi tham số, mâu thuẫn với chính tỉ lệ 20 mà DeepMind dùng để train Chinchilla. Bản fit lại của họ (Bảng 1):

E = 1,8172    A = 482,01    B = 2085,43    α = 0,3478    β = 0,3658

Với hệ số này, phương pháp 3 lại khớp với phương pháp 1 và 2, quanh mức 20 token mỗi tham số.

Máy tính scaling law

Đoạn code dưới đây dùng hai bộ hệ số trên để tính N_opt và D_opt cho một ngân sách C. Công thức đóng là phương trình 4 trong Chinchilla: cực tiểu L(N, D) với ràng buộc C = 6ND. Hệ số 6 là 6N FLOPs mỗi token khi train mà bài 8, mục 6 đã đếm.

# Máy tính scaling law: Python 3.12, chỉ dùng thư viện chuẩn.
FITS = {
    # L(N, D) = E + A / N**alpha + B / D**beta
    "Hoffmann 2022 (Approach 3)": dict(E=1.69, A=406.4, B=410.7, alpha=0.34, beta=0.28),
    "Besiroglu 2024 (refit)":     dict(E=1.8172, A=482.01, B=2085.43, alpha=0.3478, beta=0.3658),
}

def loss(f, N, D):
    return f["E"] + f["A"] / N ** f["alpha"] + f["B"] / D ** f["beta"]

def optimal(f, C):
    """Cực tiểu L(N, D) với ràng buộc C = 6ND (Hoffmann et al., phương trình 4)."""
    a, b = f["alpha"], f["beta"]
    G = (a * f["A"] / (b * f["B"])) ** (1 / (a + b))
    N = G * (C / 6) ** (b / (a + b))
    return N, C / (6 * N)

for C in (5.76e23, 7.2e23, 3.8e25):
    print(f"C = {C:.2e} FLOPs")
    N20 = (C / 120) ** 0.5                     # quy tắc 20 token/tham số: C = 6 * N * 20N
    print(f"  {'20 token/tham số':<28} N = {N20/1e9:6.1f}B  D = {20*N20/1e12:5.2f}T  D/N = 20")
    for name, f in FITS.items():
        N, D = optimal(f, C)
        print(f"  {name:<28} N = {N/1e9:6.1f}B  D = {D/1e12:5.2f}T  D/N = {D/N:4.0f}")

f = FITS["Besiroglu 2024 (refit)"]
C = 6 * 8e9 * 15e12
N, D = optimal(f, C)
print(f"\nCùng C = {C:.2e} FLOPs, theo bản fit Besiroglu:")
print(f"  compute-optimal  {N/1e9:5.1f}B x {D/1e12:5.2f}T  loss = {loss(f, N, D):.4f}")
print(f"  kiểu Llama 3 8B    8.0B x 15.00T  loss = {loss(f, 8e9, 15e12):.4f}")

Output:

C = 5.76e+23 FLOPs
  20 token/tham số             N =   69.3B  D =  1.39T  D/N = 20
  Hoffmann 2022 (Approach 3)   N =   32.2B  D =  2.98T  D/N =   93
  Besiroglu 2024 (refit)       N =   72.2B  D =  1.33T  D/N =   18
C = 7.20e+23 FLOPs
  20 token/tham số             N =   77.5B  D =  1.55T  D/N = 20
  Hoffmann 2022 (Approach 3)   N =   35.6B  D =  3.37T  D/N =   95
  Besiroglu 2024 (refit)       N =   81.0B  D =  1.48T  D/N =   18
C = 3.80e+25 FLOPs
  20 token/tham số             N =  562.7B  D = 11.25T  D/N = 20
  Hoffmann 2022 (Approach 3)   N =  213.5B  D = 29.67T  D/N =  139
  Besiroglu 2024 (refit)       N =  618.7B  D = 10.24T  D/N =   17

Cùng C = 7.20e+23 FLOPs, theo bản fit Besiroglu:
  compute-optimal   81.0B x  1.48T  loss = 1.9683
  kiểu Llama 3 8B    8.0B x 15.00T  loss = 2.0221

Đọc output:

  • Ngân sách đầu tiên là của Gopher (5,76 × 10²³ FLOPs, Chinchilla Bảng 3). Quy tắc 20 token/tham số cho khoảng 69B trên 1,4T token, gần đúng cấu hình của Chinchilla 70B. Bản fit Besiroglu ra 72B, cũng gần đó.
  • Hệ số đã làm tròn của Hoffmann cho kết quả lệch hẳn: chỉ 32B nhưng 93 token mỗi tham số. Chính paper Chinchilla, dùng hệ số chưa làm tròn, báo cáo 40B cho ngân sách này (chú thích Hình 4). Đây là minh hoạ trực tiếp cho nhận xét của Besiroglu: phương pháp 3 với hệ số công bố không khớp với hai phương pháp còn lại.
  • Tỉ lệ token/tham số tối ưu không cố định. Với hệ số Besiroglu, α và β gần bằng nhau nên tỉ lệ gần như không đổi theo C (18, 18, 17). Với hệ số Hoffmann, tỉ lệ tăng dần theo C (93, 95, 139). “20” là một điểm trên đường cong của một bộ dữ liệu cụ thể, không phải hằng số tự nhiên.
  • Đổi bộ dữ liệu thì đổi luôn kết quả. Meta fit scaling law trên dữ liệu của chính họ và với ngân sách 3,8 × 10²⁵ FLOPs ra 402B tham số trên 16,55T token, khoảng 41 token mỗi tham số (Llama 3 paper, mục 3.2.1). Máy tính trên với hệ số Besiroglu ra 619B trên 10,2T. Hệ số scaling law phụ thuộc dữ liệu, tokenizer và kiến trúc; muốn dùng nghiêm túc thì phải tự fit trên setup của mình.
  • Hai dòng cuối là cầu nối sang mục sau. Cùng compute với Llama 3 8B, model compute-optimal theo bản fit này là 81B trên 1,5T token, và loss dự đoán thấp hơn khoảng 0,054 nats. Meta biết rõ điều đó nhưng vẫn chọn 8B. Lý do nằm ở chi phí sau khi train xong.

06Vượt quá Chinchilla-optimal

Compute-optimal chỉ tối ưu chi phí train

“Compute-optimal” trong Chinchilla có nghĩa rất hẹp: loss thấp nhất cho một lượng FLOPs train cố định. Nó không tính đến việc model sẽ được chạy bao nhiêu lần sau đó.

Sardana và Frankle 2023 thêm chi phí inference vào bài toán. Một model được gọi hàng tỉ lần mỗi ngày, và chi phí mỗi token khi inference tỉ lệ với N (2N FLOPs, cũng theo bài 8). Khi tổng số token inference đủ lớn, phép tính đổi chiều: bỏ thêm compute để train một model nhỏ hơn trên nhiều token hơn sẽ rẻ hơn tổng thể, vì mỗi lần gọi rẻ hơn. Theo phân tích của họ, nếu nhu cầu inference vào khoảng một tỉ request thì nên train nhỏ hơn và lâu hơn mức Chinchilla-optimal. Họ cũng train 47 model từ 150M đến 6B tham số, với tỉ lệ từ 10 đến 10.000 token mỗi tham số, và không thấy điểm bão hoà nào: chất lượng vẫn tiếp tục tăng khi tỉ lệ tăng.

Các model mở ngày nay train rất xa điểm Chinchilla

Đây là lý do các model mở phổ biến cố tình “overtrain”:

Model Tham số Token pretrain Token / tham số Nguồn
Chinchilla 70B 1,4T 20 Chinchilla, Bảng 1
Llama 3 8B 8,03B 15T+ ≈ 1.870 model card Llama 3
Llama 3.1 405B 405B 15,6T ≈ 39 Llama 3 paper, mục 1
Qwen3-0.6B 0,6B hơn 30T (giai đoạn đầu) > 50.000 Qwen3 report, mục 3.2

Với Llama 3 8B: 15 × 10¹² / 8,03 × 10⁹ ≈ 1.870, gần 1.900 token cho mỗi tham số, gấp khoảng 90 lần tỉ lệ Chinchilla. Dòng Qwen còn đi xa hơn: Qwen2.5 nâng dữ liệu pretraining lên 18T token, còn Qwen3 ghi rõ mọi model trong dòng, kể cả bản 0,6B, đều qua giai đoạn pretrain đầu trên hơn 30T token. Với bản nhỏ nhất, đó là hơn 50.000 token cho mỗi tham số.

Đổi lại, ta chấp nhận loss cao hơn một chút so với model compute-optimal cùng ngân sách, như 0,054 nats ở ví dụ cuối mục 05, để được một model rẻ hơn mười lần khi chạy.

Khi dữ liệu cạn: lặp epoch

Overtrain cần rất nhiều token, và dữ liệu chất lượng cao có hạn (bài 5 đã nói về chuyện lọc và dedup làm co dữ liệu thế nào). Khi hết dữ liệu mới, lựa chọn tự nhiên là đọc lại dữ liệu cũ.

Muennighoff et al. 2023 đo chuyện này với các run tới 900B token và model tới 9B tham số. Kết quả: với ngân sách compute cố định, lặp lại dữ liệu tới 4 epoch cho loss gần như không khác dùng dữ liệu mới hoàn toàn. Sau đó giá trị của token lặp giảm nhanh. Theo bản fit của họ, “nửa đời” của dữ liệu lặp vào khoảng 15 lần lặp (16 epoch); quá mức đó, thêm compute gần như không cải thiện gì nữa.

Loss thấp hơn chưa chắc đã giỏi hơn

Scaling laws dự đoán loss trên tập validation, và đó là thứ duy nhất chúng dự đoán. Thứ người dùng quan tâm là điểm trên các task cụ thể, và quan hệ giữa hai thứ này không đơn giản:

  • Hai model khác tokenizer thì loss mỗi token không so được với nhau (mục 02).
  • Hai model train trên hai data mix khác nhau thì loss trên cùng một tập validation phản ánh cả độ “giống” giữa data mix và tập đó, không chỉ năng lực.
  • Ngay cả trong cùng một họ model, quan hệ giữa loss và độ chính xác trên benchmark cũng phải fit riêng. Meta làm hai bước: fit negative log-likelihood trên các task downstream theo FLOPs, rồi fit quan hệ giữa negative log-likelihood đó và độ chính xác (Llama 3 paper, mục 3.2.1). Họ nói rõ lý do: scaling laws thông thường chỉ dự đoán loss của next-token prediction, không dự đoán được điểm benchmark.

07Ngân sách compute

Đây là phần có thể tính tay hoàn toàn. Công thức C ≈ 6ND đã có ở bài 8, mục 6; ở đây ta chỉ ghép thêm phần cứng vào.

Ba con số cần có

  1. C: tổng FLOPs train, xấp xỉ 6 · N · D.
  2. FLOPs đỉnh của một GPU ở precision dùng để train. Trang sản phẩm H100 của NVIDIA ghi H100 SXM đạt 1.979 teraFLOPS BF16 có sparsity. Train LLM dùng ma trận dày, không dùng sparsity, nên con số dùng được là một nửa: khoảng 989 TFLOP/s.
  3. MFU (Model FLOPs Utilization): tỉ lệ giữa FLOPs “có ích” mà model thật sự cần (tính theo 6N mỗi token cộng phần attention) và FLOPs đỉnh của phần cứng. Khái niệm này do PaLM đề xuất (Phụ lục B). PaLM 540B đạt 46,2%. Llama 3 405B đạt 38–43% BF16 MFU tuỳ cấu hình (Llama 3 paper, Bảng 4). Phần còn lại mất vào giao tiếp giữa GPU, chờ dữ liệu, kernel không tối ưu, tính lại activation.

Từ đó:

GPU-giây = C / (FLOPs đỉnh × MFU)

Bài toán mẫu: Llama 3 8B trên 15T token

Bước 1, tính C. Lấy N = 8,03 × 10⁹ (phép đếm ở bài 8, mục 6) và D = 15 × 10¹² (model card ghi “15T+”):

C = 6 × 8,03 × 10⁹ × 15 × 10¹² = 7,227 × 10²³ FLOPs

Bước 2, throughput thực tế mỗi GPU. Giả định MFU 40%, nằm trong khoảng 38–43% mà Meta báo cáo cho 405B:

989 × 10¹² × 0,40 = 3,958 × 10¹⁴ FLOP/s

Bước 3, đổi ra GPU-giờ.

7,227 × 10²³ / 3,958 × 10¹⁴ = 1,826 × 10⁹ GPU-giây
1,826 × 10⁹ / 3.600         ≈ 507.000 GPU-giờ ≈ 0,51M GPU-giờ

Bước 4, đối chiếu. Model card của Llama 3 ghi Llama 3 8B dùng 1,3M GPU-giờ trên H100-80GB. Con số thật lớn hơn ước lượng khoảng 2,6 lần. Nếu tính ngược, 1,3M GPU-giờ tương ứng MFU hiệu dụng chỉ khoảng 15,6%.

Meta không công bố MFU riêng cho model 8B, nên không thể chỉ ra chính xác phần chênh nằm ở đâu. Những ứng viên hợp lý: model nhỏ thường đạt MFU thấp hơn model lớn vì ma trận nhỏ hơn và tỉ lệ thời gian giao tiếp cao hơn; số token thật có thể nhiều hơn 15T (“15T+”); 6ND bỏ qua chi phí attention theo context; con số GPU-giờ trong model card có thể đã gồm thời gian mất vì gián đoạn và các giai đoạn annealing, long-context (vẫn thuộc pretraining, vì model card ghi rõ đây là GPU-giờ của pretraining). Riêng attention thì tính được và không phải nguyên nhân chính: với 32 layer, d_model = 4.096, context 8K, phần 6 × 32 × 8.192 × 4.096 ≈ 6,4 × 10⁹ FLOPs mỗi token chỉ cộng khoảng 13% so với 6N ≈ 4,8 × 10¹⁰, và khoảng 7% khi chia đôi vì causal mask. Cũng đừng nhầm với model card của Llama 3.1, nơi bản 3.1 8B ghi 1,46M GPU-giờ; con số 1,3M ở trên là của Llama 3 8B. Bài học thực tế: 6ND cho bạn cận dưới, không phải hoá đơn.

Kiểm tra lại với 405B

Model lớn khớp hơn nhiều, vì Meta công bố đủ dữ liệu:

C        = 6 × 405 × 10⁹ × 15,6 × 10¹²            = 3,79 × 10²⁵ FLOPs   (paper ghi 3,8 × 10²⁵)
MFU 43%  : 3,79 × 10²⁵ / (989 × 10¹² × 0,43) / 3.600 ≈ 24,7M GPU-giờ
MFU 38%  : 3,79 × 10²⁵ / (989 × 10¹² × 0,38) / 3.600 ≈ 28,0M GPU-giờ

Model card của Llama 3.1 ghi 405B dùng 30,84M GPU-giờ, cao hơn ước lượng ở MFU 38% khoảng 10% và ở MFU 43% khoảng 25%. MFU thật của cả lần train nằm giữa hai mức 43% và 38% (Bảng 4), nên phần vượt lên trên ước lượng phải đến từ chỗ khác: thời gian train hiệu dụng chỉ “trên 90%” (mục 3.3.4), tức mất tới khoảng 10% vì gián đoạn, cộng chi phí attention mà 6ND bỏ qua. Nếu chạy liên tục trên 16.384 GPU, 30,84M GPU-giờ tương đương khoảng 78 ngày.


08Mixed precision

Bốn định dạng số

Định dạng Bit số mũ Bit mantissa Ghi chú
fp32 8 23 chuẩn, chậm và tốn bộ nhớ
fp16 5 10 giá trị lớn nhất 65.504; gradient nhỏ dễ thành 0
bf16 8 7 cùng dải số mũ với fp32, độ chính xác thấp hơn fp16
fp8 (E4M3 / E5M2) 4 / 5 3 / 2 dùng được cho phép nhân ma trận, cần scaling cẩn thận

Train bằng số 16 bit nhanh hơn nhiều vì Tensor Core chạy nhanh gấp nhiều lần ở precision thấp, và tốn nửa bộ nhớ. Vấn đề là train hoàn toàn ở 16 bit thì hỏng.

fp16 cần loss scaling, bf16 thì không

Micikevicius et al. 2017 chỉ ra hai vấn đề của fp16. Thứ nhất, nhiều gradient nhỏ hơn 2⁻²⁴, ngưỡng nhỏ nhất fp16 biểu diễn được, nên bị làm tròn về 0. Thứ hai, cập nhật trọng số w ← w − lr · g thường nhỏ hơn rất nhiều so với w, và ở độ chính xác của fp16 phép cộng đó bị nuốt mất. Hai cách sửa trở thành chuẩn:

  • Loss scaling: nhân loss với một hệ số lớn trước khi backward để đẩy gradient vào vùng fp16 biểu diễn được, rồi chia lại trước khi cập nhật. Hệ số này thường được chỉnh động: giảm khi gặp overflow, tăng dần khi ổn định.
  • Master weights fp32: forward và backward dùng bản fp16, nhưng phép cập nhật chạy trên một bản sao fp32 của trọng số.

bf16 giải quyết vấn đề thứ nhất từ gốc. Nó giữ nguyên 8 bit số mũ của fp32, nên dải giá trị biểu diễn được giống fp32 và gradient nhỏ không bị làm tròn về 0. Kalamkar et al. 2019 cho thấy bf16 train được nhiều loại mạng mà không cần loss scaling. Cái giá là chỉ còn 7 bit mantissa, kém chính xác hơn fp16, nên master weights và optimizer state vẫn giữ ở fp32. Đây là cấu hình mặc định của hầu hết các lần pretrain LLM hiện nay, gồm cả Llama 3 (MFU được báo cáo theo BF16).

Một ví dụ ngược lại cho thấy fp16 khó dùng thế nào: OPT-175B giữ trọng số ở fp16 với dynamic loss scaling, và nhóm tác giả ghi nhận mối liên hệ giữa các lần loss phân kỳ với việc hệ số loss scaling sụt về 0 (OPT paper: fp16 và loss scaling ở mục 2.4, các lần phân kỳ ở mục 2.5).

FP8 bắt đầu được dùng để train

DeepSeek-V3 là một trong những model lớn đầu tiên được train với FP8 mixed precision ở quy mô lớn: 14,8T token, tổng cộng 2,788M GPU-giờ H800 cho toàn bộ quá trình train. Các phép nhân ma trận chính chạy ở FP8, với quantization chia nhỏ theo tile và block để giới hạn sai số. Master weights, gradient của trọng số và optimizer state vẫn ở precision cao hơn (DeepSeek-V3 report, mục 3.3). Họ báo cáo sai số loss tương đối so với BF16 luôn dưới 0,25%.

16 byte cho mỗi tham số

Đây là phép đếm quyết định mục 09. Với AdamW và mixed precision, mỗi tham số cần (ZeRO paper, mục 3.1):

Thành phần Byte / tham số
Trọng số 16 bit (dùng cho forward, backward) 2
Gradient 16 bit 2
Master weights fp32 4
Adam momentum fp32 4
Adam variance fp32 4
Tổng model states 16

Một model 8,03B tham số cần 8,03 × 10⁹ × 16 ≈ 128 GB chỉ cho model states, chưa tính activation, trong khi một H100 có 80 GB. ZeRO paper đưa ví dụ tương tự với GPT-2 1,5B: 3 GB cho trọng số fp16, nhưng ít nhất 24 GB khi train. Activation cộng thêm một khoản lớn nữa, tăng theo batch và độ dài sequence.

Vậy ngay cả một model “nhỏ” theo chuẩn hiện nay cũng không train được trên một GPU. Không phải vì thiếu FLOPs mà vì thiếu bộ nhớ.


09Song song hoá: DP, FSDP, TP, PP

Có hai lý do để dùng nhiều GPU: cho vừa bộ nhớ và cho xong trong thời gian chấp nhận được. Mỗi kiểu song song cắt bài toán theo một chiều khác nhau, và trả giá bằng một kiểu giao tiếp khác nhau.

~/diagrams/pretraining-va-scaling-laws/parallelism.htmltương tác
mở riêng ↗
HìnhCùng một model và một batch, chia lên hai GPU theo bốn cách. DP: mỗi GPU giữ cả model, xử lý nửa batch, cuối bước all-reduce gradient. FSDP: vẫn chia batch như DP, nhưng mỗi GPU chỉ giữ một nửa weights, gradient và optimizer state, gom đủ weights bằng all-gather khi cần và chia gradient bằng reduce-scatter. TP: cả hai GPU cùng xử lý cả batch, mỗi GPU giữ một nửa mỗi ma trận, phải all-reduce nhiều lần trong mỗi layer. PP: GPU 0 giữ nửa đầu số layer, GPU 1 giữ nửa sau, activation đi từ GPU này sang GPU kia. Các thẻ dưới sơ đồ ghi bộ nhớ model states mỗi GPU, nơi giao tiếp diễn ra, và cấu hình Llama 3 405B.
GPU giữ bản đầy đủ của modelkhung tím: GPU chỉ giữ một phần model; khung cam đứt: một kiểu song song

Data parallelism (DP): chia batch

Mỗi GPU giữ một bản sao đầy đủ của model và optimizer state, xử lý một phần khác nhau của batch. Sau backward, các GPU all-reduce gradient để mọi bản sao có cùng gradient trung bình, rồi cập nhật giống hệt nhau.

DP dễ cài và giao tiếp chỉ một lần mỗi bước, nhưng nó không tiết kiệm bộ nhớ chút nào: mỗi GPU vẫn cần đủ 16 byte mỗi tham số. Model không vừa một GPU thì DP thuần không giúp được.

FSDP / ZeRO: vẫn là data parallel, nhưng shard luôn state

Rajbhandari et al. 2019 (ZeRO) nhận ra rằng trong DP, N GPU đang giữ N bản sao giống hệt nhau của optimizer state, gradient và trọng số. Họ chia các thứ đó ra, mỗi GPU giữ 1/N, theo ba mức:

  • chia optimizer state: giảm bộ nhớ khoảng 4 lần, giao tiếp như DP;
  • chia thêm gradient: giảm khoảng 8 lần, giao tiếp vẫn như DP;
  • chia cả trọng số: bộ nhớ giảm tuyến tính theo N, giao tiếp tăng khoảng 50%.

PyTorch FSDP (Zhao et al. 2023) là bản cài đặt trong PyTorch của mức cao nhất. Trước khi tính một layer, các GPU all-gather để tạm có đủ trọng số của layer đó, tính xong thì bỏ đi. Sau backward, gradient được reduce-scatter: cộng lại và chia luôn, mỗi GPU chỉ nhận phần gradient ứng với shard của nó.

Điểm cần nắm: FSDP vẫn là data parallelism. Mỗi GPU vẫn xử lý một phần batch khác nhau và vẫn chạy toàn bộ phép tính của mọi layer. Nó chỉ không lưu cả model thường trực. Llama 3 dùng FSDP làm chiều DP của mình, chia optimizer state và gradient nhưng không chia lại trọng số sau forward để khỏi phải all-gather thêm một lần trong backward (Llama 3 paper, mục 3.3.2).

Tensor parallelism (TP): chia trong một layer

Shoeybi et al. 2019 (Megatron-LM) chia từng ma trận trong một layer lên nhiều GPU. Với FFN, ma trận đầu tiên chia theo cột, ma trận thứ hai chia theo hàng, nên mỗi GPU tính được phần của mình mà không cần giao tiếp ở giữa. Chỉ cần một all-reduce ở cuối khối trong forward và một trong backward. Attention chia theo head theo cách tương tự.

Ưu điểm: giảm bộ nhớ của cả trọng số lẫn activation, và mọi GPU cùng xử lý một batch. Nhược điểm: giao tiếp xảy ra nhiều lần trong mỗi layer, trên đường tính toán chính. TP chỉ hiệu quả khi các GPU nối với nhau bằng NVLink trong cùng một server. Narayanan et al. 2021 rút ra quy tắc: dùng TP tối đa bằng số GPU trong một server (thường là 8), vượt ra ngoài server thì chuyển sang PP (Takeaway #1).

Pipeline parallelism (PP): chia theo layer

PP chia các layer thành stage, mỗi GPU một stage. Activation đi từ stage này sang stage kế tiếp trong forward, gradient đi ngược lại trong backward. Giao tiếp chỉ là gửi activation ở ranh giới stage, nhỏ và điểm-tới-điểm, nên chịu được mạng giữa các server.

Cái giá là bubble. Nếu chỉ đưa một batch vào, stage 2 phải chờ stage 1 xong, stage 1 lại ngồi chờ trong lúc stage 2 tính. GPipe (Huang et al. 2018) chia batch thành m micro-batch chạy nối đuôi nhau để lấp khoảng trống. Với p stage, tỉ lệ thời gian nhàn rỗi là:

bubble = (p − 1) / m          (Narayanan et al. 2021, mục 2.2.1)

Với 16 stage như Llama 3 405B, muốn bubble dưới 10% cần hơn 150 micro-batch mỗi bước. GPipe ghi nhận bubble gần như không đáng kể khi m ≥ 4p. Nhiều micro-batch hơn thì phải giữ activation của nhiều micro-batch hơn, nên lịch chạy PP luôn là bài toán đánh đổi giữa bubble và bộ nhớ.

So sánh

Chia gì Model states mỗi GPU (Ψ tham số, N GPU) Giao tiếp Dùng khi
DP batch 16Ψ (không giảm) all-reduce gradient, mỗi bước một lần model vừa một GPU
FSDP / ZeRO-3 batch, cộng shard state ≈ 16Ψ / N all-gather trọng số, reduce-scatter gradient model không vừa, muốn giữ lập trình kiểu DP
TP ma trận trong layer ≈ 16Ψ / N all-reduce nhiều lần mỗi layer trong một server có NVLink
PP các layer ≈ 16Ψ / N gửi activation giữa stage giữa các server, model rất sâu

Ghép lại thành song song nhiều chiều

Model lớn dùng cả bốn cùng lúc, mỗi kiểu đặt vào đúng tầng mạng hợp với nó. Narayanan et al. 2021 ghép TP, PP và DP để train một model 1 nghìn tỉ tham số đạt 502 petaFLOP/s trên 3.072 GPU, tức 52% throughput đỉnh mỗi GPU.

Llama 3 405B dùng song song 4D theo thứ tự [TP, CP, PP, DP], trong đó DP là FSDP (mục 3.3.2). Cấu hình đầu tiên trong Bảng 4: TP 8 · CP 1 · PP 16 · DP 64 trên 8.192 GPU, 430 TFLOPs mỗi GPU, MFU 43%. Cách xếp này đi đúng theo băng thông: TP 8 nằm gọn trong một server 8 GPU nối NVLink; PP và DP trải qua mạng giữa các server.

Hai chiều còn lại chỉ cần biết tên. Context parallelism (CP) chia theo chiều dài sequence, cần cho giai đoạn long-context: Llama 3 bật CP 16 khi train với sequence 131.072 token, MFU giảm còn 38% (Bảng 4). Expert parallelism chia các expert của model Mixture-of-Experts lên nhiều GPU, sẽ có ở bài 14.


10Khi một lần train đang hỏng

Theo dõi gì

Loss đơn thuần là tín hiệu quá chậm. Các nhóm train model lớn theo dõi ít nhất:

  • Loss, cả tổng lẫn loss theo từng domain (web, code, toán, đa ngôn ngữ). Một domain tách nhánh khỏi phần còn lại thường là dấu hiệu sớm của vấn đề trong data mix hoặc pipeline dữ liệu.
  • Grad norm toàn cục. Grad norm tăng vọt thường đi trước loss spike vài bước.
  • Độ lớn của logits, cả attention logits lẫn output logits. Wortsman et al. 2023 chỉ ra đây là hai nguồn mất ổn định chính, mục sau sẽ nói kỹ.
  • Throughput và MFU. MFU tụt mà loss vẫn bình thường thường là chuyện phần cứng: một GPU chậm, một đường mạng lỗi.

Loss spike và phân kỳ

Loss spike là khi loss đang giảm đều bỗng tăng vọt, có khi tự hồi lại, có khi kéo cả lần train phân kỳ. Ba ví dụ có tài liệu:

  • PaLM 540B (mục 5.1): loss spike khoảng 20 lần trong quá trình train dù đã bật gradient clipping, ở những thời điểm rất thất thường, kể cả gần cuối. Model nhỏ hơn không gặp. Cách xử lý: quay về checkpoint khoảng 100 bước trước spike, bỏ qua khoảng 200–500 batch dữ liệu quanh đó. Chi tiết quan trọng: họ thử lấy chính các batch đó train từ một checkpoint khác, sớm hơn, và không thấy spike. Kết luận của họ: spike không do “dữ liệu xấu” tự thân, mà do tổ hợp giữa một số batch cụ thể và một trạng thái tham số cụ thể.
  • OPT-175B (mục 2.5): khi loss phân kỳ, nhóm tác giả giảm learning rate rồi khởi động lại từ checkpoint sớm hơn. Họ cũng giảm gradient clipping từ 1,0 xuống 0,3 ở giai đoạn đầu. Toàn bộ quá trình được ghi lại trong một logbook công khai, đọc như nhật ký của một ca trực.
  • Llama 3 405B (mục 3.4.1): với lịch batch size tăng dần, Meta báo cáo lần train “rất ổn định”: ít loss spike và không phải can thiệp để sửa phân kỳ.

Nguyên nhân và các bản sửa phổ biến

Wortsman et al. 2023 tái hiện các kiểu mất ổn định của model lớn trên model nhỏ bằng cách dùng learning rate cao, nhờ đó nghiên cứu được với chi phí thấp. Họ tách ra hai nguồn chính:

  • Attention logits phình to. Tích q · k lớn dần đến mức softmax của attention gần như one-hot, gradient trở nên bất thường. Dehghani et al. 2023 gặp đúng hiện tượng này khi scale ViT lên 22B và sửa bằng QK-norm: áp LayerNorm lên query và key trước tích vô hướng. Wortsman xác nhận QK-norm cũng giúp LLM.
  • Output logits phân kỳ. Logit ở lm_head trôi xa khỏi 0, softmax cuối mất ổn định. Bản sửa là z-loss: cộng thêm một hạng phạt nhỏ đẩy log Z (log của mẫu số softmax) về gần 0. PaLM dùng 10⁻⁴ · log² Z (mục 5).

Họ cũng thấy warmup dài hơn làm model bớt nhạy với learning rate, và chỉnh weight decay độc lập với learning rate, theo khuyến nghị của Loshchilov và Hutter, cũng có tác dụng tương tự. Bảng tóm tắt:

Triệu chứng Nguyên nhân thường gặp Xử lý
Spike rồi tự hồi một số batch gặp trạng thái tham số bất lợi quay checkpoint, bỏ qua batch quanh đó
Grad norm tăng dần, loss phân kỳ learning rate quá cao so với quy mô giảm LR, tăng warmup, khởi động lại
Attention gần như one-hot attention logits phình to QK-norm
Output logits trôi xa mẫu số softmax mất kiểm soát z-loss
Loss một domain tách nhánh lỗi data mix hoặc pipeline dữ liệu kiểm tra lại shard và thứ tự dữ liệu

Lỗi phần cứng là chuyện hằng ngày

Một lần train đồng bộ trên hàng nghìn GPU có tính chất của chuỗi đèn mắc nối tiếp: một GPU hỏng có thể làm dừng cả job. Số liệu Meta công bố cho Llama 3 405B (mục 3.3.4 và Bảng 5):

  • trong một đợt 54 ngày, job bị gián đoạn 466 lần;
  • 47 lần có kế hoạch (nâng firmware, đổi cấu hình, cập nhật dữ liệu), 419 lần ngoài ý muốn;
  • khoảng 78% số lần ngoài ý muốn do lỗi phần cứng đã xác nhận hoặc nghi ngờ; riêng GPU chiếm 58,7%, đứng đầu là GPU hỏng (148 lần, 30,1%) và bộ nhớ HBM3 (72 lần, 17,2%);
  • chỉ 3 lần cần can thiệp thủ công đáng kể, phần còn lại do tự động hoá xử lý;
  • thời gian train hiệu dụng vẫn trên 90%.

Có cả những lỗi khó thấy hơn: silent data corruption (6 lần trong Bảng 5), khi GPU tính sai mà không báo lỗi, và những GPU “chậm nhưng vẫn chạy” kéo cả nghìn GPU khác chậm theo. Meta còn ghi nhận throughput dao động 1–2% theo giờ trong ngày, do nhiệt độ giữa trưa ảnh hưởng tới xung nhịp GPU.

OPT-175B, chạy trên 992 GPU A100 80GB, cho thấy cùng bức tranh ở quy mô nhỏ hơn: ít nhất 35 lần khởi động lại thủ công và hơn 100 host bị thay ra trong khoảng 2 tháng; nhóm tác giả ước tính thêm hơn 70 lần khởi động lại tự động do lỗi phần cứng (OPT mục 2.5; cấu hình 992 GPU ở mục 2.4).

Hệ quả: checkpoint thường xuyên và khởi động lại nhanh không phải tính năng phụ. Chúng là một phần của thuật toán train, ngang hàng với optimizer. Và như bài 5 đã nhắc, data loader phải biết chính xác đã tiêu thụ bao nhiêu token thì khởi động lại mới đúng chỗ.


11Những chỗ dễ hiểu sai

Sáu dòng dưới đây khớp với hàng dưới của Hình 1.

Hiểu lầm Thực tế
“Mọi model nên train 20 token cho mỗi tham số” 20 là tỉ lệ cực tiểu hoá loss cho một ngân sách train cố định, trên dữ liệu của Chinchilla. Khi tính cả inference, model nên nhỏ hơn và train lâu hơn nhiều: Llama 3 8B dùng gần 1.900 token mỗi tham số
“bf16 chỉ là fp16 đổi tên” bf16 có 8 bit số mũ như fp32, nên không cần loss scaling; fp16 có 5 bit số mũ và cần loss scaling. Đổi lại bf16 kém chính xác hơn fp16
“Chinchilla chứng minh Kaplan sai” Kaplan không đo nhầm, nhưng số mũ 0,73 là ước lượng bị lệch. Chênh lệch đến từ cách đếm tham số (bỏ embedding), chi phí layer cuối, warmup và quy mô thí nghiệm. Sửa các yếu tố đó thì kết quả khớp ~0,5 của Chinchilla; hai paper kiểm lại đều xác nhận hệ số của Chinchilla
“FSDP là model parallelism” FSDP là data parallelism có shard state: mỗi GPU vẫn xử lý một phần batch riêng và chạy toàn bộ phép tính. Nó chỉ không lưu cả model thường trực
“Loss spike nào cũng do dữ liệu bẩn” PaLM cho chạy lại đúng các batch gây spike từ một checkpoint khác và không thấy spike. Spike đến từ tổ hợp giữa dữ liệu và trạng thái tham số, hoặc từ logits phình to
“Loss thấp hơn thì model giỏi hơn” chỉ đúng khi cùng tokenizer, cùng dữ liệu đánh giá. Loss mỗi token phụ thuộc tokenizer, và quan hệ giữa loss với điểm benchmark phải fit riêng

Tổng kết: một objective, ba bài toán kỹ thuật

Pretraining đơn giản ở mục tiêu và khó ở mọi thứ còn lại. Model chỉ học dự đoán token kế tiếp, nhưng để làm điều đó trên mười mấy nghìn tỉ token thì phải giải ba bài toán: chia ngân sách, nhét vào phần cứng, và giữ cho lần train sống sót.

Những gì nên mang theo:

  • objective là cross-entropy của token kế tiếp, tính song song ở mọi vị trí nhờ teacher forcing và causal mask; loss, perplexity và bits-per-byte là cùng một đại lượng, và chỉ bits-per-byte so được giữa các tokenizer;
  • scaling laws là quy luật thực nghiệm: loss giảm theo luỹ thừa của N, D và C ≈ 6ND; Kaplan khuyên tăng N nhanh hơn D, Chinchilla cho thấy tăng ngang nhau, khoảng 20 token mỗi tham số; phần lớn chênh lệch đến từ cách đếm và cách thiết kế thí nghiệm, và chính bản fit của Chinchilla cũng đã được sửa;
  • compute-optimal chỉ tối ưu chi phí train; khi tính inference, model nhỏ train rất lâu có lợi hơn, nên Llama 3 8B dùng gần 1.900 token mỗi tham số; dữ liệu lặp tới khoảng 4 epoch gần như không mất gì;
  • ngân sách compute tính tay được: 6ND / (FLOPs đỉnh × MFU) ra GPU-giây; với Llama 3 405B, phép tính thấp hơn số công bố khoảng 10–25% tuỳ MFU giả định, với 8B thì thực tế tốn gấp khoảng 2,6 lần, nên coi 6ND là cận dưới;
  • bộ nhớ là lý do phải song song hoá: AdamW cùng mixed precision tốn 16 byte mỗi tham số, model 8B đã cần 128 GB; DP chia batch, FSDP chia batch và state, TP chia ma trận trong node, PP chia layer giữa các node, và model lớn ghép cả bốn theo băng thông mạng;
  • bf16 giữ dải số mũ của fp32 nên không cần loss scaling, master weights vẫn ở fp32, và FP8 đã bắt đầu dùng được để train;
  • một lần train đang hỏng lộ ra qua grad norm, độ lớn logits, loss theo domain và MFU; spike được xử lý bằng quay checkpoint và bỏ batch, mất ổn định do logits được chặn bằng QK-norm và z-loss; ở quy mô 16.000 GPU, lỗi phần cứng xảy ra nhiều lần mỗi ngày.

Model vừa pretrain xong là một base model. Nó dự đoán token kế tiếp rất giỏi, nhưng như bài 1 đã nhắc, việc của nó chỉ là viết tiếp văn bản: hỏi nó một câu, nó có thể viết tiếp thành năm câu hỏi khác. Bài 11 sẽ đi qua post-training, chặng biến base model thành một assistant biết làm theo chỉ dẫn.


Tài liệu tham khảo

Paper

Tài liệu và số liệu phần cứng

Trương Đức Dũng

AI Engineer - Hà Nội

Tập trung vào LLM, RAG và các hệ thống AI cần chạy ổn định trong môi trường vận hành thật.