Lab tổng hợp · 13 giai đoạn
Lab tổng hợp LLM Training · huấn luyện một LLM thu nhỏ từ dữ liệu đến checkpoint và suy luận bằng NumPy
Chạy bằng NumPy một LLM thu nhỏ đi hết vòng huấn luyện: dữ liệu, forward, loss, backward, AdamW, ZeRO, SFT, LoRA, distillation, DPO, rồi nạp checkpoint để suy luận.
1Định nghĩa
Bài Lab tổng hợp này cho bạn chạy bằng NumPy (thư viện tính toán số của Python) ngay trong trình duyệt toàn bộ vòng đời huấn luyện của một mô hình ngôn ngữ lớn (LLM, large language model) thu nhỏ: chuẩn bị dữ liệu, tính điểm số bằng forward, đo sai lầm bằng loss, tính hướng sửa bằng backward, cập nhật trọng số, rồi đưa kết quả sang suy luận. Mỗi giai đoạn trong output mang đúng tên các ô trên sơ đồ kiến trúc ở Tab 01, nên bạn nhìn thấy số thật chảy qua từng ô thay vì chỉ đọc mô tả. Đây là cách ôn lại vòng lặp huấn luyện bằng giá trị cụ thể.
Cơ chế chạy như sau: một mô hình 2 lớp với 5856 tham số học cách đoán token kế tiếp qua forward, cross-entropy loss, backward và AdamW; sau đó cùng khung vòng lặp ấy được dùng lại cho bốn phương pháp huấn luyện khác nhau là SFT, LoRA, distillation và DPO, còn bộ nhớ của cả vòng được tính bằng ZeRO và tính lại activation. Cuối cùng checkpoint được nạp lại để tiếp nhận một yêu cầu suy luận. Mọi loss, đếm số, đếm byte và sai số là kết quả tính thật trên mô hình đồ chơi, còn lab không đo thời gian hay tốc độ GPU thật.
Ví dụ đời thường: Lab giống một xưởng dạy nghề thu nhỏ có đủ mọi công đoạn. Người học đọc đề (token ID), làm bài (forward), chấm điểm (loss), tìm chỗ sai (backward), sửa cách làm theo từng mức độ khác nhau (AdamW), thỉnh thoảng làm bài thi thử chưa gặp (kiểm định) và cất vở vào tủ (checkpoint). Sau đó xưởng thử bốn cách dạy tiếp: học theo mẫu hỏi đáp, chỉ chỉnh một vài thao tác, bắt chước thầy giỏi và chọn giữa hai cách trả lời. Phép so sánh dừng lại ở quy mô: xưởng này chỉ có tám bài tập nên học thuộc chứ chưa phải thành thạo.
2Luồng
- 1 · Dữ liệu → token IDs
11 dòng thô thành 8 tài liệu sạch, từ vựng 23 token; tài liệu đầu thành 11 ID; một batch 4 tài liệu có 63 vị trí.
- 2 · Next-token targets
Tài liệu 11 token cho 10 cặp (input, nhãn dịch một vị trí); mặt nạ cắt theo ranh giới tài liệu.
- 3 · Forward: embedding, attention, MLP, norm → logits
Attention vị trí 3 chia trọng số cho đúng 4 vị trí 0..3; đổi token cuối không làm đổi logits phía trước (0.0); logits có shape (10, 23).
- 4 · Cross-entropy loss
Loss ban đầu 3.2481 gần ln 23 = 3.1355 vì mô hình chưa học; p(đáp án) từ 0.0226 đến 0.1285 ở bốn vị trí đầu.
- 5 · Backward & gradient từng lớp
Gradient theo logits là p trừ one-hot, tổng mỗi hàng bằng 0; gradient giải tích khớp sai phân hữu hạn (sai lệch cỡ 5e-10) ở 16 tensor.
- 6 · AdamW
Bước đầu mỗi tham số đổi gần đúng lr = 0.01 theo dấu của gradient (khoảng 0.0098 đến 0.0101); loss một bước 3.2746 xuống 2.8802.
- 7 · Vòng lặp, eval, checkpoint
300 bước: loss huấn luyện 0.1014, loss kiểm định thấp nhất 0.8048 ở bước 50 rồi lên 1.0469; checkpoint tiếp tục được gấp 2.96 lần bản weights-only.
- 8 · Bộ nhớ: ZeRO & recompute
5856 tham số cần 16 byte mỗi tham số; ZeRO-3 với 2 worker còn 46848 byte mỗi worker; cập nhật chia theo ZeRO-1 và gradient tính lại lệch 0.0.
- 9 · SFT: chat template + response mask
Mặt nạ chỉ chấm 4 nhãn cuối của cặp đầu; từ 0 trên 8 lên 8 trên 8 câu đúng, loss 4.1347 xuống 0.0036.
- 10 · LoRA
Chỉ 1088 tham số (18.6%) được học; trọng số gốc lệch 0.0; mô hình gộp và chưa gộp cho cùng logits; 8 trên 8 câu đúng.
- 11 · Distillation
Student 1008 tham số (17% teacher): học từ soft target có KL 0.0068 và 8 trên 8 câu đúng, học nhãn cứng có KL 0.1955 và 5 trên 8.
- 12 · Preference (DPO)
Khi policy bằng tham chiếu loss đúng ln 2 = 0.6931; sau 60 bước biên 8.317, xác suất câu chosen của cặp đầu lại giảm từ 0.982 xuống 0.520.
- 13 · Từ checkpoint sang suy luận
Prompt 13 token, prefill một lượt rồi decode với KV cache cho đúng văn bản sua. như tính lại; 17 vị trí so với 75.
Mã Mermaid
flowchart TD
step1["1 · Dữ liệu → token IDs"]
step2["2 · Next-token targets"]
step3["3 · Forward: embedding, attention, MLP, norm → logits"]
step4["4 · Cross-entropy loss"]
step5["5 · Backward #amp; gradient từng lớp"]
step6["6 · AdamW"]
step7["7 · Vòng lặp, eval, checkpoint"]
step8["8 · Bộ nhớ: ZeRO #amp; recompute"]
step9["9 · SFT: chat template + response mask"]
step10["10 · LoRA"]
step11["11 · Distillation"]
step12["12 · Preference (DPO)"]
step13["13 · Từ checkpoint sang suy luận"]
step1 --> step2
step2 --> step3
step3 --> step4
step4 --> step5
step5 --> step6
step6 --> step7
step7 --> step8
step8 --> step9
step9 --> step10
step10 --> step11
step11 --> step12
step12 --> step13 3Giải pháp
Mỗi bài trong khóa này giải thích một ô của sơ đồ, nhưng đọc riêng từng bài rất khó thấy các ô nối nhau thế nào: dữ liệu ra khỏi ô nào, vào ô nào, và con số nào ở một ô thay đổi khi ô khác thay đổi. Lab gom 13 giai đoạn vào một file để bạn theo dõi cùng một mô hình nhỏ từ lúc chưa biết gì (loss ban đầu 3.2481, gần mức đoán đều ln 23 = 3.1355) đến lúc trả lời được câu hỏi và được nạp lại cho suy luận.
Đầu vào gồm dữ liệu tự viết và một cấu hình nhỏ. Kho nền có 11 dòng thô, sau khi bỏ dòng trống, hai dòng trùng và chuẩn hóa chữ thường còn 8 tài liệu; tách riêng 2 câu kiểm định. Dữ liệu SFT là 8 cặp hỏi đáp, dữ liệu ưu tiên là 6 bộ (câu hỏi, câu được chọn, câu bị loại). Tokenizer tách theo ký tự nên từ vựng chỉ có 23 token. Mô hình là bộ giải mã Transformer 2 lớp, chiều ẩn 16, một head attention, khởi tạo ngẫu nhiên với seed 7; chạy lại cho đúng các số cũ.
Giai đoạn 1 (ô 1A) biến văn bản thành số: tài liệu đầu "meo an ca." thành 11 token ID, cuối là eos (token báo hết tài liệu); một batch (nhóm tài liệu xử lý cùng lúc) gồm 4 tài liệu có 63 vị trí, nên một epoch (một lượt qua toàn bộ dữ liệu) là 2 bước. Giai đoạn 2 (ô 1B) dịch nhãn đi một vị trí: tài liệu 11 token cho 10 cặp (input, token đứng ngay sau), và mặt nạ nhân quả (che các vị trí phía sau) được cắt theo ranh giới tài liệu, nên khi đóng gói hai tài liệu, tài liệu sau không nhìn thấy tài liệu trước.
Giai đoạn 3 (ô 2A đến 2D) đi qua forward: bảng embedding (bảng tra ID ra vector) cho x shape (10, 16), tức 10 vị trí với 16 số mỗi vị trí; attention ở vị trí 3 chia trọng số tổng bằng 1 cho đúng 4 vị trí 0 đến 3 (các vị trí sau có trọng số 0), và đó là trọng số trên vị trí chứ chưa phải xác suất của token. Đổi token cuối rồi chạy lại, logits (điểm số của mọi token trong từ vựng) ở các vị trí trước đó đổi 0.0. Norm đưa độ lớn trung bình của vector từ 0.136 về 1.000; nếu đặt ma trận ra của attention và MLP bằng 0 thì đầu ra khối bằng đầu vào, minh họa đường tắt residual. Giai đoạn 4 (ô 3A) chấm điểm: loss là trung bình của trừ ln p(token đúng), ví dụ vị trí 3 có p = 0.1285 cho 2.052 nat (nat là đơn vị của loss khi dùng logarit tự nhiên), trung bình 3.2481.
Giai đoạn 5 (ô 3B, 3C) tính backward. Gradient theo logits là p trừ one-hot nên tổng mỗi hàng bằng 0; từ đó gradient đi ngược qua unembedding, từng lớp (lớp 0 có chuẩn gradient 0.9951, lớp 1 có 0.6791) và embedding. Để chắc công thức đúng, lab dịch phần tử có gradient lớn nhất của mỗi trong 16 tensor đi ±1e-5, đo loss thay đổi bao nhiêu (sai phân hữu hạn) và thấy khớp, sai lệch lớn nhất cỡ 5e-10. Giai đoạn 6 (ô 3D) theo một tham số qua AdamW: gradient -0.017946, m = (1 - β1) nhân g = -0.001795, v = (1 - β2) nhân g² = 3.221e-07; sau hiệu chỉnh ở bước đầu m̂ trên căn v̂ bằng đúng dấu của g, nên bước cập nhật gần đúng bằng lr = 0.01. Vì thế ở bước đầu AdamW đổi mỗi tham số một lượng 0.0098 đến 0.0101, còn SGD cùng lr đổi từ 6e-09 đến 2.6e-03; một bước giảm loss 3.2746 xuống 2.8802.
Giai đoạn 7 (ô 3E và 4C) chạy 300 bước. Loss huấn luyện đo trên cả tập đi 0.2657 (bước 50), 0.1259, 0.1128, 0.1070, 0.1031 rồi 0.1014 (bước 300); loss kiểm định thấp nhất 0.8048 ở bước 50 rồi lên 1.0469. Hai đường tách nhau: đó là dấu hiệu bắt đầu học thuộc chỉ 8 tài liệu, chưa phải kết luận chắc chắn vì kiểm định chỉ có 2 câu. Checkpoint đầy đủ ở bước 100 nặng khoảng 153,6 nghìn byte, gấp 2.96 lần bản weights-only 51960 byte vì mang thêm m và v. Chạy tiếp 10 bước từ bản đầy đủ lệch 0.0 so với chạy liền; chỉ nạp trọng số (m, v về 0 và bộ đếm bước về 0) lệch 9.0e-02.
Giai đoạn 8 (ô 4A, 4B) tính bộ nhớ mà không đổi toán học. Mỗi tham số cần 2 byte trọng số, 2 byte gradient và 12 byte trạng thái bộ tối ưu (bản chính 32 bit cộng m và v), tổng 16 byte; với 5856 tham số và 2 worker, mỗi worker cần 93696 byte (data parallel), 58560 (ZeRO-1), 52704 (ZeRO-2) hoặc 46848 (ZeRO-3). Cùng công thức cho mô hình 7 tỉ tham số trên 8 GPU cho 112, 38.5, 26.25 và 14 GB, tính tay chưa gồm activation. Lab còn chia dữ liệu cho 2 worker (gradient cộng lại lệch cỡ 1e-17), chia m và v thành hai đoạn 2928 phần tử (cập nhật chia lệch 0.0) và tính lại activation từng lớp khi backward (gradient lệch 0.0, số lưu giảm từ 90240 còn 48960).
Giai đoạn 9 đến 12 là bốn phương pháp khác nhau, mỗi phương pháp đổi một phần của vòng lặp. SFT (ô 1C, 4D) ghép mỗi cặp thành chuỗi "user câu hỏi assistant câu trả lời eos" và chỉ chấm các nhãn của câu trả lời: từ 0 trên 8 lên 8 trên 8 câu đúng, loss 4.1347 xuống 0.0036; loss trên token prompt (không được chấm) đi từ 3.8980 lên 5.0391, và câu hỏi chưa gặp "meo bay o dau" vẫn được trả lời bằng "tren ghe." nên không có bằng chứng về khả năng tổng quát. LoRA (ô 4E) đóng băng 5856 trọng số gốc và chỉ học cặp ma trận hạng thấp A, B (1088 tham số, 18.6%): B bắt đầu bằng 0 nên mô hình y hệt gốc ở bước 0, trọng số gốc lệch 0.0 sau huấn luyện và mô hình gộp bằng mô hình chưa gộp.
Distillation (ô 4F): student 1008 tham số (17% teacher) học khớp phân phối xác suất của teacher bằng KL (đo hai phân phối khác nhau bao nhiêu): KL 0.0068 và 8 trên 8 câu đúng, so với 0.1955 và 5 trên 8 khi cùng student chỉ học nhãn cứng. DPO (ô 4G) so câu được chọn và câu bị loại với một mô hình tham chiếu: khi policy còn bằng tham chiếu loss đúng bằng ln 2 = 0.6931; sau 60 bước biên z là 8.317, nhưng xác suất cả câu được chọn của cặp đầu lại giảm từ 0.982 xuống 0.520 còn câu bị loại giảm từ 1.02e-03 xuống 2.70e-10. Biên đo hiệu của hai thay đổi nên có thể tăng dù câu được chọn không tăng.
Giai đoạn 13 (ô 5A đến 5C) nạp checkpoint SFT (51960 byte, không có m, v) và chạy suy luận. Prompt "meo uong gi" qua mẫu hội thoại thành 13 token ID; prefill (xử lý cả prompt một lượt) ghi KV cache của lớp 0 shape (13, 16) và chọn token đầu "s"; mỗi bước decode (sinh từng token) chỉ đưa một token mới, đọc K, V cũ từ cache rồi thêm K, V mới, cache dài 13 lên 17. Logits có cache khớp logits tính lại cả chuỗi (sai lệch cỡ 1e-15) và văn bản sinh ra là "sua.". Số vị trí phải xử lý là 17 với cache so với 75 nếu mỗi bước tính lại cả chuỗi; đây là số vị trí, chưa phải thời gian. Nạp nhầm một checkpoint khác cùng cấu hình không báo lỗi nhưng cho đầu ra khác hẳn.
Giới hạn của kết luận: mô hình và dữ liệu cực nhỏ, nên các số thuộc về lab này chứ không đại diện cho mô hình thật; số câu đúng đo trên chính các cặp đã học; ZeRO và recompute chỉ là đếm byte và so gradient; LoRA, distillation và DPO mỗi phương pháp chỉ chạy một seed và vài chục ví dụ. Dòng PASS chỉ xác nhận các phép kiểm tra cơ chế đã đạt, không xác nhận mô hình học tốt.
4Profiling
- Kiểm tra tính đúng trước khi đọc các con số học được
Trước khi so loss hay số câu đúng, hãy xem các phép kiểm tra cơ chế: gradient giải tích khớp sai phân hữu hạn (sai lệch cỡ 5e-10) ở 16 tensor; đổi token cuối không đổi logits phía trước; gradient cộng từ các worker bằng gradient cả batch; cập nhật chia theo ZeRO-1 và gradient tính lại cho sai lệch 0.0; LoRA gộp bằng chưa gộp; cache bằng tính lại. Ba nhóm này chứng minh cơ chế đúng, không chứng minh mô hình học tốt hay chạy nhanh.
- Đọc loss huấn luyện và loss kiểm định cùng nhau
Workload là 8 tài liệu huấn luyện và 2 câu kiểm định, batch 4 tài liệu, lr 0.01, seed 7; baseline là loss đoán đều ln 23 = 3.1355. Mỗi 50 bước hãy ghi loss batch, loss cả tập huấn luyện và loss kiểm định. Nếu hai đường loss cùng giảm thì mô hình đang học điều chuyển được sang dữ liệu chưa thấy; nếu loss huấn luyện giảm còn loss kiểm định tăng (như từ bước 50 ở lab) thì nghi học thuộc, nhưng cần thêm bằng chứng như kiểm định lớn hơn. Đừng viết loss giảm thành mô hình tốt.
- Đếm byte bộ nhớ, không đo
Công thức 16 byte cho mỗi tham số, 4P + 12P/N, 2P + 14P/N và 16P/N chỉ tính trạng thái mô hình (trọng số, gradient, bộ tối ưu); chưa gồm activation, bộ đệm giao tiếp và phân mảnh bộ nhớ. Con số 14 GB của ZeRO-3 cho mô hình 7 tỉ tham số trên 8 GPU là phép tính tay. Khi làm trên phần cứng thật, hãy đo bộ nhớ đỉnh và thời gian mỗi bước cùng workload, vì ZeRO-3 thêm giao tiếp và recompute thêm một lượt forward (khoảng 33% theo mô hình đơn giản, thời gian thật phụ thuộc kernel và kích thước lớp).
- Phân biệt khớp số học, loss thấp và chất lượng trả lời
Khớp số học (cache bằng tính lại, merge bằng không merge) cho biết cơ chế đúng. Loss thấp trên dữ liệu huấn luyện cho biết mô hình thuộc dữ liệu. Chất lượng trả lời cần bộ đánh giá riêng: ở lab, câu hỏi chưa gặp được trả lời bằng câu của một cặp khác, và DPO có thể làm xác suất câu được chọn giảm trong khi biên tăng. Hãy báo cả ba loại bằng chứng với đúng nhãn của chúng.
Mẹo đo lường: ghim số liệu trên workload thật, ghi cả p50/p95/p99 thay vì chỉ trung bình, và luôn ghi lại điều kiện chạy (batch, cache, đồng thời, phần cứng) trước và sau khi tối ưu. Số đo thiếu điều kiện chạy thì không so sánh được.
5Ví dụ
Vì sao bước AdamW đầu tiên gần bằng lr bất kể gradient lớn hay nhỏ?
Hãy dự đoán trước: tham số Q0 ở vị trí (0, 7) có gradient -0.017946 còn tham số khác có thể có gradient nhỏ hơn hàng nghìn lần, vậy bước đầu của chúng khác nhau bao nhiêu? Mở khối 6. Ở bước 1, m = 0.1 nhân g và v = 0.001 nhân g², sau hiệu chỉnh m̂ = g và v̂ = g², nên cập nhật bằng lr nhân g chia (căn của g² cộng ε) gần đúng bằng lr nhân dấu của g, tức -0.010000 ở tham số này. Toàn mô hình, mọi tham số có gradient khác 0 đổi 0.0098 đến 0.0101, còn SGD với cùng lr đổi từ 6e-09 đến 2.6e-03. Tự kiểm tra: AdamW luôn đổi mỗi tham số đúng lr ở mọi bước không? Đối chiếu: không, chỉ ở bước đầu m̂ và v̂ cùng đến từ một gradient; sau đó m là trung bình của nhiều gradient nên bước nhỏ lại khi gradient đổi dấu liên tục.
Prompt không được chấm thì loss của nó đi đâu?
Mở khối 9. Dự đoán: sau SFT chỉ chấm câu trả lời, loss trên token prompt giảm, đứng yên hay tăng? Kết quả đã chạy: từ 3.8980 lên 5.0391, trong khi loss trên token câu trả lời đi từ 4.1347 xuống 0.0036. Prompt không sinh gradient (hàng gradient ở vị trí bị che có tổng bằng 0), nên mô hình không có lý do giữ khả năng đoán các token của câu hỏi, và các thay đổi để trả lời đúng có thể làm nó tệ đi. Đó là điều mong muốn nếu mục tiêu là học cách đáp, nhưng cũng là lý do dữ liệu SFT cần đúng mẫu. Tự kiểm tra: đặt MASK_RESPONSE_ONLY = False thì loss trên token prompt sau SFT là bao nhiêu? Đối chiếu: 0.1851, vì lúc đó prompt cũng được chấm (xem bài tập 4).
Biên DPO tăng nhưng câu được chọn lại kém chắc chắn hơn
Mở khối 12 cho cặp đầu (hỏi "meo an gi", chosen "ca.", rejected "sua."). Dự đoán: sau DPO xác suất của cả câu chosen tăng hay giảm? Ban đầu policy bằng tham chiếu nên biên z = 0 và loss đúng bằng ln 2 = 0.6931. Sau 60 bước z trung bình là 8.317 và loss 0.0005, nhưng xác suất cả câu chosen đi từ 0.982 xuống 0.520, còn xác suất câu rejected đi từ 1.02e-03 xuống 2.70e-10. Biên chỉ đo hiệu giữa hai thay đổi, nên đẩy rejected xuống rất mạnh là đủ để biên tăng mạnh dù chosen không tăng. Tự kiểm tra: biên z dương lớn có đủ để kết luận câu trả lời của policy tốt hơn không? Đối chiếu: không; cần đánh giá câu trả lời trực tiếp và dữ liệu ưu tiên thật, còn ở đây chỉ có 6 cặp tự viết.
6Thuật ngữ
- Giai đoạn của lab
- Một khối [n · Tên] trong output, khớp một hoặc vài ô trên sơ đồ kiến trúc ở Tab 01 (ví dụ giai đoạn 3 khớp các ô 2A đến 2D).
- Mặt nạ loss (loss mask)
- Danh sách đúng/sai cho từng vị trí, cho biết vị trí nào được tính vào loss; ở SFT chỉ các token của câu trả lời được chấm.
- Sai phân hữu hạn (finite difference)
- Cách ước lượng gradient bằng cách dịch một tham số đi một lượng rất nhỏ rồi đo loss đổi bao nhiêu; lab dùng nó để kiểm tra gradient tính bằng công thức.
- Checkpoint weights-only và checkpoint tiếp tục được
- Bản weights-only chỉ có trọng số, cấu hình và bảng từ vựng, đủ để suy luận; bản tiếp tục được có thêm m, v của AdamW và số bước nên nặng gần gấp ba.
- Biên DPO (margin)
- Hiệu giữa mức tăng log-xác suất của câu được chọn và mức tăng của câu bị loại, so với mô hình tham chiếu, nhân β; biên dương nghĩa là policy đã nghiêng về câu được chọn.
- Đồng hồ và tốc độ
- Lab này không đo thời gian hay tốc độ GPU; mọi số là loss, đếm số, đếm byte hoặc sai số tính thật trên mô hình đồ chơi.
Glossary (7)
- Token ID
- Số thứ tự của một token trong bảng từ vựng; mô hình dùng số này để tra vector nhúng, bản thân số chưa mang ý nghĩa.
- Logits
- Điểm số chưa chuẩn hóa của mọi token trong từ vựng ở một vị trí; softmax đổi chúng thành xác suất tổng bằng 1.
- Nat
- Đơn vị của loss khi dùng logarit tự nhiên; đoán đều trên 23 token cho loss ln 23 = 3.1355 nat.
- Epoch
- Một lượt đi hết toàn bộ dữ liệu huấn luyện; với 8 tài liệu và batch 4, một epoch là 2 bước.
- Học thuộc (overfitting)
- Loss trên dữ liệu huấn luyện tiếp tục giảm trong khi loss trên dữ liệu chưa thấy đi lên; cần thêm bằng chứng khác trước khi kết luận chắc chắn.
- KV cache
- Bộ nhớ giữ key và value của mọi vị trí đã xử lý để mỗi bước decode chỉ tính vị trí mới.
- Mô hình tham chiếu
- Bản sao đóng băng của mô hình trước khi huấn luyện ưu tiên, dùng làm mốc đo xem policy đã dịch chuyển bao xa.
7Bài liên quan
8Nguồn
9Bài lab
Huấn luyện một LLM thu nhỏ qua 13 giai đoạn của sơ đồ bằng NumPy
Thực hành NumPy · huấn luyện LLM thu nhỏ (13 giai đoạn)Đọc phần hướng dẫn rồi bấm Chạy. Mỗi khối [n · Tên] khớp một hoặc vài ô trên sơ đồ kiến trúc ở Tab 01; hãy đọc dòng Quan sát, đối chiếu các con số, rồi mới đọc Kết luận và Không chứng minh.
Bạn sẽ học được gì?
Sau bài lab, bạn giải thích được bằng số cụ thể mười điều: (1) văn bản trở thành token ID và batch ra sao; (2) vì sao mỗi chuỗi tự sinh ra các bài toán dự đoán token kế tiếp; (3) forward đi qua embedding, attention, MLP và norm tới logits như thế nào; (4) cross-entropy loss nén bảng điểm thành một số ra sao; (5) backward cho gradient từng lớp và vì sao có thể kiểm chứng bằng sai phân hữu hạn; (6) AdamW đổi gradient thành bước cập nhật thế nào; (7) vòng lặp, đánh giá và checkpoint làm gì; (8) ZeRO và recompute tiết kiệm bộ nhớ mà không đổi toán học; (9) SFT, LoRA, distillation và DPO khác nhau ở phần nào của vòng huấn luyện; (10) checkpoint trở thành suy luận prefill rồi decode với KV cache ra sao.
Bạn không cần biết trước về học máy. Mỗi thuật ngữ được giải thích ở chỗ nó xuất hiện đầu tiên; bạn chỉ cần đọc được Python và biết một mảng NumPy có shape, ví dụ (10, 16) là 10 hàng, mỗi hàng 16 số.
Đầu vào và cấu hình thu nhỏ của bài lab
Dữ liệu do tác giả tự viết, không tải từ đâu. Kho huấn luyện nền gồm 11 dòng thô như "Meo an ca.", một dòng trống và hai dòng trùng; sau khi làm sạch còn 8 tài liệu. Tập kiểm định (validation: dữ liệu chỉ dùng để đo, không dùng để cập nhật) có 2 câu chưa gặp: "cho an ca." và "meo uong nuoc.". Dữ liệu SFT là 8 cặp hỏi đáp như "meo an gi" và "ca."; dữ liệu ưu tiên là 6 bộ (câu hỏi, câu được chọn, câu bị loại) như "meo an gi", "ca.", "sua.".
Mô hình là một bộ giải mã Transformer với 2 lớp, 1 head attention (head: một nhóm tính attention độc lập; ở đây chỉ có một nên head dùng toàn bộ chiều ẩn), chiều ẩn d = 16, mạng truyền thẳng SwiGLU (mạng mở rộng chiều rồi lọc bằng một cổng) rộng 32, RMSNorm (chuẩn hóa biên độ vector) và mã hóa vị trí cố định. Tokenizer tách theo ký tự nên từ vựng chỉ có 23 token: 19 ký tự cộng 4 token đặc biệt là pad (đệm), eos (kết thúc), user và assistant (đánh dấu lượt nói). Tổng cộng 5856 tham số, khởi tạo ngẫu nhiên với seed 7 nên chạy lại cho đúng các số cũ.
Các hằng số ở đầu file (STEPS, LR, ZERO_WORKERS, MASK_RESPONSE_ONLY, LORA_RANK, DISTILL_T, DPO_BETA và các hằng khác) là những núm bạn sẽ xoay ở phần bài tập. Chạy một lần mất vài giây trong trình duyệt.
Bảng ánh xạ giữa sơ đồ Tab 01 và 13 giai đoạn của lab
Giai đoạn 1 Dữ liệu → token IDs (ô 1A) = clean_corpus, encode_text và pack_lm; giai đoạn 2 Next-token targets (ô 1B) = cách pack_lm dịch nhãn và dựng mặt nạ; giai đoạn 3 Forward (ô 2A đến 2D) = block_forward và forward; giai đoạn 4 Cross-entropy loss (ô 3A) = cross_entropy; giai đoạn 5 Backward & gradient từng lớp (ô 3B, 3C) = block_backward, backward và check_gradients; giai đoạn 6 AdamW (ô 3D) = adamw_update; giai đoạn 7 Vòng lặp, eval, checkpoint (ô 3E, cùng ô 4C pretraining) = train_lm, save_checkpoint và load_checkpoint.
Giai đoạn 8 Bộ nhớ: ZeRO & recompute (ô 4A, 4B) = zero_bytes cùng hai phép mô phỏng chia dữ liệu và chia trạng thái AdamW, và nhánh backward tính lại từng lớp; giai đoạn 9 SFT (ô 1C, 4D) = chat_ids và pack_sft; giai đoạn 10 LoRA (ô 4E) = các cặp adapter A, B và hàm merged; giai đoạn 11 Distillation (ô 4F) = distill_loss_grad; giai đoạn 12 Preference (DPO) (ô 4G) = seq_logp_and_dlogits và dpo_grads; giai đoạn 13 Từ checkpoint sang suy luận (ô 5A đến 5C) = step_forward trên checkpoint nạp lại. Tên giai đoạn trong output khớp tên ô trên sơ đồ.
Cách thực hiện và đọc từng khối output
1. Trước khi bấm Chạy, hãy dự đoán: loss lúc chưa huấn luyện là bao nhiêu nếu mô hình đoán đều trên 23 token (gợi ý: ln 23)? Sau 300 bước, loss kiểm định sẽ thấp hơn hay cao hơn loss huấn luyện, và vì sao? Viết dự đoán ra giấy.
2. Bấm Chạy. Ở khối 1 và 2, đọc bảng từ vựng, các ID của tài liệu đầu và cặp input, nhãn: nhãn luôn là token đứng ngay sau input. Ở khối 3, tìm hàng attention chỉ có 4 phần tử khác 0, phép thử đổi token cuối cho sai lệch 0.0 và so shape (10, 16) của x với (10, 23) của logits. Ở khối 4, nhìn từng giá trị trừ ln p ở bốn vị trí đầu rồi so trung bình với ln 23.
3. Ở khối 5, đọc sai lệch giữa gradient giải tích và sai phân hữu hạn; ở khối 6, theo dõi một tham số cụ thể từ gradient g qua m, v tới bước cập nhật và so với SGD. Ở khối 7, đọc bảng loss theo bước: hai cột loss huấn luyện và loss kiểm định có đi cùng nhau không; rồi so lần chạy tiếp từ checkpoint đầy đủ (lệch 0.0) với lần chỉ nạp trọng số.
4. Ở khối 8, tự tính trước byte mỗi worker rồi đối chiếu ba dòng ZeRO và các sai lệch bằng 0. Ở khối 9, nhìn mặt nạ loss của cặp đầu và hai con số loss trên token câu trả lời với token prompt. Ở khối 10 đến 12, so số tham số học được, số câu đúng và các sai lệch kiểm tra; ở khối 12, đọc cả xác suất của câu chosen lẫn câu rejected chứ không chỉ biên.
5. Ở khối 13, đối chiếu ID của prompt, shape của KV cache, số vị trí có cache so với tính lại và dòng nạp nhầm checkpoint. Cuối cùng, đối chiếu dự đoán ban đầu với kết quả rồi làm các bài tập: sửa một hằng số ở đầu file, dự đoán, chạy lại và giải thích.
Kết quả này chứng tỏ điều gì và giới hạn ở đâu?
Các phép kiểm tra trong lab xác nhận, cho đúng mô hình và dữ liệu này: gradient giải tích khớp sai phân hữu hạn trong 1e-6 ở mọi tensor, cả với loss KL của distillation và loss DPO; gradient cả batch bằng tổng gradient từng worker; cập nhật AdamW chia theo ZeRO-1 giống hệt cập nhật không chia; gradient tính lại từng lớp giống hệt gradient lưu đầy đủ; mô hình LoRA gộp và chưa gộp cho cùng logits; trọng số gốc không đổi khi huấn luyện LoRA; logits có KV cache khớp logits tính lại cả chuỗi (sai lệch cỡ 1e-15, chặn trên 1e-9).
Lab không chứng minh: chất lượng ngôn ngữ (dữ liệu chỉ vài chục ký tự, câu trả lời được học thuộc); khả năng trả lời câu hỏi chưa gặp (xem câu meo bay o dau ở khối 9); tốc độ hay bộ nhớ đỉnh trên GPU thật (ZeRO chỉ là phép chia mảng và đếm byte trong một tiến trình, recompute chỉ so gradient); LoRA hay distillation tốt hơn tinh chỉnh toàn bộ ở mô hình thật (kết quả chỉ trên 8 cặp và một seed); và RLHF với dữ liệu ưu tiên của con người (DPO ở đây chỉ có 6 cặp tự viết).
Kết quả mong đợiOutput gồm phần mở đầu, 13 khối đánh số 1 đến 13 và dòng TỔNG KẾT. Mốc kiểm tra nhanh: từ vựng 23 token và 5856 tham số; loss ban đầu 3.2481 so với ln 23 = 3.1355; sau 300 bước loss huấn luyện 0.1014 và loss kiểm định 1.0469 (thấp nhất 0.8048 ở bước 50); bản tiếp tục được gấp 2.96 lần bản weights-only; ZeRO-3 với 2 worker còn 46848 byte mỗi worker; SFT đưa số câu đúng từ 0 lên 8 trên 8 còn LoRA 8 trên 8 với 18.6% tham số học được; distillation cho KL 0.0068 so với 0.1955; DPO nâng biên lên 8.317; cache cho 17 vị trí so với 75 và đầu ra sua.; cuối cùng là dòng PASS.
Bài tập 1 · Tăng ZERO_WORKERS từ 2 lên 4
Dựa vào khối 8: với 2 worker, mỗi worker cần 93696 byte (data parallel), 58560 (ZeRO-1), 52704 (ZeRO-2) và 46848 (ZeRO-3). Hãy dự đoán bốn con số này khi ZERO_WORKERS = 4 (gợi ý: công thức là 16P, 4P + 12P/N, 2P + 14P/N và 16P/N với P = 5856). Sau đó sửa dòng ZERO_WORKERS = 4, chạy lại và cho biết cột nào không đổi, cột nào không thể nhỏ hơn một mức nào đó và vì sao.
Đối chiếu sau khi tự trả lời
Với 4 worker: data parallel 93696 (không đổi vì mỗi worker vẫn giữ đủ mọi thứ), ZeRO-1 40992, ZeRO-2 32208 và ZeRO-3 23424. ZeRO-3 giảm đúng một nửa vì chia cả ba thành phần cho N. ZeRO-1 không thể nhỏ hơn 4P = 23424 và ZeRO-2 không thể nhỏ hơn 2P = 11712 vì phần trọng số và (với ZeRO-1) gradient vẫn được giữ đủ trên mỗi worker, dù N lớn bao nhiêu. Gradient của các worker cộng lại vẫn khớp gradient cả batch (lệch cỡ 1e-17) và cập nhật chia theo ZeRO-1 vẫn lệch 0.0: chia nhỏ trạng thái không đổi toán học. Đây là phép đếm byte, chưa phải đo bộ nhớ hay thời gian giao tiếp.
Bài tập 2 · Giảm STEPS từ 300 xuống 50
Dự đoán: nếu chỉ huấn luyện 50 bước thay vì 300, loss huấn luyện cuối cùng cao hơn hay thấp hơn 0.1014, và loss kiểm định cuối cùng cao hơn hay thấp hơn 1.0469? Sửa STEPS = 50, chạy lại và giải thích hiện tượng bằng bảng loss ở khối 7.
Đối chiếu sau khi tự trả lời
Loss huấn luyện cao hơn (0.2657, vì mô hình mới học ít bước) nhưng loss kiểm định thấp hơn (0.8048 so với 1.0469). Bảng ở lần chạy mặc định đã cho thấy điều đó: loss kiểm định thấp nhất ở bước 50 rồi tăng dần trong khi loss huấn luyện vẫn giảm, dấu hiệu mô hình bắt đầu học thuộc 8 tài liệu. Kết luận chỉ mang tính minh họa vì tập kiểm định chỉ có 2 câu; trên dữ liệu thật người ta dùng tập kiểm định đủ lớn để chọn bước dừng hoặc checkpoint.
Bài tập 3 · Tăng LR từ 0.01 lên 0.3
Dự đoán: learning rate lớn gấp 30 lần có làm huấn luyện nhanh hơn không? Sửa LR = 0.3 (hằng số này dùng cho cả huấn luyện nền và SFT), chạy lại và so loss huấn luyện sau 300 bước, câu mô hình sinh ra từ prompt meo an và số câu đúng sau SFT ở khối 9.
Đối chiếu sau khi tự trả lời
Không. Loss huấn luyện sau 300 bước chỉ xuống 2.0025 (so với 0.1014), prompt meo an cho ra chuỗi lặp " n n n n n n" thay vì " ca.", và SFT cho 0 trên 8 câu đúng (loss SFT chỉ xuống từ 2.5787 còn 1.7642). Bước quá dài làm mỗi lần cập nhật nhảy qua vùng loss thấp thay vì đi vào nó; AdamW chuẩn hóa độ dài bước theo lr nên lr là núm quyết định. Lưu ý các phép kiểm tra cơ chế (gradient khớp sai phân hữu hạn, ZeRO, merge, cache) vẫn đạt vì chúng không phụ thuộc vào việc mô hình học tốt hay không; dòng PASS không nói mô hình đã học tốt.
Bài tập 4 · Tắt MASK_RESPONSE_ONLY
Ở khối 9, loss trên token prompt đi từ 3.8980 lên 5.0391 sau SFT (token prompt không được chấm). Dự đoán: nếu đặt MASK_RESPONSE_ONLY = False (chấm cả prompt) thì loss trên token prompt sau SFT sẽ thế nào, loss trên token câu trả lời có tốt hơn không, và số câu đúng có đổi không? Chạy lại để kiểm tra.
Đối chiếu sau khi tự trả lời
Loss trên token prompt giảm mạnh xuống 0.1851 vì mô hình học luôn việc đoán các token của câu hỏi. Loss trên token câu trả lời không tốt hơn: 0.0055 so với 0.0036. Số câu đúng vẫn là 8 trên 8, nên trên dữ liệu nhỏ này khác biệt chủ yếu nằm ở nơi công sức học đi vào. Mặt nạ response-only dùng toàn bộ tín hiệu học cho việc trả lời; chấm cả prompt dạy mô hình sinh ra câu hỏi, điều thường không mong muốn khi huấn luyện một trợ lý. Kết quả này không chứng minh mặt nạ nào cho chất lượng hội thoại tốt hơn ở mô hình thật.
Bài tập 5 · Tăng LORA_RANK từ 2 lên 8
Dự đoán: số tham số học được và tỉ lệ so với 5856 sẽ là bao nhiêu khi hạng r tăng 4 lần (gợi ý: mỗi ma trận d_vào × d_ra thêm r × (d_vào + d_ra) tham số, tổng cho 14 ma trận)? Số câu đúng và loss sau LoRA có tốt hơn nhiều không? Sửa LORA_RANK = 8 rồi chạy lại.
Đối chiếu sau khi tự trả lời
Số tham số học được tăng gấp 4 lần, từ 1088 (18.6%) lên 4352 (74.3% số tham số gốc). Số câu đúng vẫn là 8 trên 8 và loss sau LoRA chỉ nhích từ 0.0120 xuống 0.0096. Với chiều ẩn nhỏ như d = 16, hạng 8 đã là một nửa chiều nên hạng thấp không còn thấp: chi phí tăng gấp 4 mà lợi ích gần như không đổi. Lợi thế của LoRA nằm ở chỗ hạng r nhỏ hơn rất nhiều so với d, điều mô hình thật (d hàng nghìn) đạt được còn mô hình đồ chơi này thì không; đừng suy ra cách chọn r cho mô hình thật từ kết quả trên.
Bài tập 6 · Giảm DPO_BETA từ 0.5 xuống 0.1
β nhân vào biên z nên β nhỏ làm loss giảm chậm hơn. Dự đoán: sau 60 bước biên z trung bình sẽ lớn hơn hay nhỏ hơn 8.317, và policy sẽ dịch xa hơn hay gần hơn mô hình tham chiếu (nhìn xác suất của câu chosen và rejected của cặp đầu)? Sửa DPO_BETA = 0.1, chạy lại rồi giải thích.
Đối chiếu sau khi tự trả lời
Biên z trung bình nhỏ hơn (4.608) và loss còn cao hơn (0.0130 so với 0.0005), nhưng policy dịch xa hơn: xác suất cả câu rejected của cặp đầu xuống tới 2.13e-18 (so với 2.70e-10) và xác suất câu chosen là 0.653 (so với 0.520). Vì z = β nhân với phần chênh log-xác suất, β nhỏ cần chênh lệch lớn hơn mới đạt cùng z, tức ràng buộc với mô hình tham chiếu lỏng hơn; β lớn giữ policy gần tham chiếu hơn. Ở cả hai lần, xác suất câu chosen vẫn giảm so với tham chiếu (0.982): biên dương không có nghĩa câu được chọn trở nên chắc chắn hơn.
Bài tập 7 · Giảm DISTILL_T từ 2.0 xuống 1.0
Ở khối 11, nhiệt độ T = 2 làm phân phối của teacher phẳng hơn. Dự đoán: nếu T = 1 (không làm phẳng) thì student học từ soft target có KL tới teacher thấp hơn hay cao hơn 0.0068 sau cùng số bước, và có còn trả lời đúng 8 trên 8 không? Sửa DISTILL_T = 1.0 rồi chạy lại.
Đối chiếu sau khi tự trả lời
KL cao hơn: 0.0166 so với 0.0068, còn số câu đúng vẫn là 8 trên 8, và student chỉ học nhãn cứng vẫn là 5 trên 8 với KL 0.1955. Ở T = 1 gần như toàn bộ xác suất của teacher nằm ở một token (0.996) nên soft target gần giống nhãn cứng; T = 2 làm lộ thứ tự ưu tiên giữa các token còn lại nên student bắt chước cả phân phối trong cùng số bước. Chênh lệch đo trên một seed và 8 cặp, nên đừng coi T = 2 là giá trị tốt nhất nói chung.
