AI Core cho lập trình viên/Từ transformer đến LLM: causal mask và train song song
33/54
Bài 33 / 54~13 phútTransformer & attentionMiễn phí lượt xem

Từ transformer đến LLM: causal mask và train song song

Causal mask ép token chỉ nhìn quá khứ để train song song mọi vị trí, còn inference vẫn sinh tuần tự từng token, chậm dần theo số token sinh ra.

TL;DR: LLM hiện đại là decoder-only transformer: nhiều transformer block xếp chồng, dùng self-attention kèm causal mask — mỗi token chỉ được nhìn token đứng trước nó, không bao giờ thấy token đứng sau. Ràng buộc tưởng hạn chế này lại cho phép train song song: nhãn (token kế tiếp) đã có sẵn trong dữ liệu, model tính loss cho toàn bộ vị trí trong một lần forward, không cần đợi tuần tự như model đọc kiểu cũ. Nhưng inference thì ngược lại — token kế tiếp chưa tồn tại, phải sinh xong token này mới biết token sau, nên vòng lặp next-token quay lại tuần tự, độ trễ tỉ lệ thuận với số token sinh ra.

Bạn dùng API của một LLM để tóm tắt một tài liệu dài — trả lời gần như ngay lập tức. Cùng ngày, bạn yêu cầu chatbot viết một bài luận dài — chữ chạy ra chậm rãi, nối tiếp nhau, càng dài càng phải đợi lâu. Cùng một model, cùng kiến trúc, nhưng một tác vụ nhanh gần như tức thì, tác vụ kia chậm dần theo đúng số từ sinh ra.

Nghịch lý lộ rõ hơn ở phía train: model học từ toàn bộ một đoạn văn dài trong một lần forward duy nhất — không xử lý tuần tự từng token như model đọc kiểu cũ (Nỗi đau 2, bài 01 module này). Vậy vì sao xử lý một chuỗi có sẵn thì song song được, còn sinh một chuỗi mới lại tuần tự? Bài này trace đúng cơ chế tạo ra bất đối xứng đó: causal mask.

1. Analogy — chấm bài văn mẫu có sẵn vs tự viết bài trong giờ thi

Hình dung một bài kiểm tra tự luận: học sinh phải viết một đoạn văn hoàn chỉnh, viết tới đâu mới biết câu tiếp theo tới đó — không thể biết trước câu thứ mười sẽ ra sao khi mới viết xong câu thứ ba. Đây là lúc làm bài — quá trình bắt buộc tuần tự.

Nhưng giáo viên chấm bài thì khác hẳn. Cô có sẵn một bài văn mẫu hoàn chỉnh và chấm cùng lúc mọi câu: câu ba đúng hay sai, câu bảy đúng hay sai — tất cả kiểm tra song song, vì đáp án của mọi câu đã có sẵn trên giấy, không câu nào phải đợi câu khác chấm xong trước. Điều giáo viên phải tự nhắc mình: khi chấm câu ba, không được liếc trước câu bảy để "đoán" học sinh viết đúng — chỉ chấm dựa trên những gì đã viết trước câu ba.

Đời thườngTransformer
Học sinh viết bài — chưa biết câu sau sẽ viết gìInference — token kế tiếp chưa tồn tại, phải sinh xong token này mới tới token sau
Giáo viên chấm bài mẫu có sẵn — kiểm tra mọi câu cùng lúcTrain — token kế tiếp đã có sẵn trong dữ liệu, tính loss cho mọi vị trí trong một lần forward
Giáo viên phải tự cấm mình liếc câu sau khi chấm câu trướcCausal mask — ép model chỉ tính điểm dựa trên token đứng trước, dù đáp án tương lai đang nằm ngay đó trong dữ liệu
💡 Cách nhớ

Chấm bài (đáp án có sẵn) chấm được song song. Làm bài (chưa có gì viết ra) thì buộc phải tuần tự. Train giống chấm bài, inference giống làm bài — cùng một model, khác hẳn ràng buộc.

2. Causal mask — mỗi token chỉ được nhìn về quá khứ

Bài trước (bài 02–03 module này) đã dựng self-attention: mỗi token tính điểm tương quan với mọi token khác trong câu, kể cả token đứng sau nó. Đó là self-attention "đầy đủ", không giới hạn hướng nhìn.

Decoder-only LLM — kiến trúc hầu hết LLM hiện đại chạy — thêm đúng một ràng buộc lên self-attention đó, gọi là causal mask (mask nhân quả). Vaswani và cộng sự, "Attention Is All You Need" (arXiv:1706.03762), gọi tên cơ chế này là "masked multi-head attention": trước bước softmax, mọi điểm attention trỏ từ token ở vị trí i tới token đứng sau nó bị gán về gần như âm vô cực, khiến trọng số sau softmax gần như bằng không. Token thứ i chỉ được nhìn token từ vị trí một tới i, không bao giờ nhìn token đứng sau i.

Viết thành ma trận cho một câu bốn token, hàng là token đang tính, cột là token được phép nhìn:

Cau 4 token: [Toi] [thich] [hoc] [may]

           Toi  thich  hoc  may
  Toi       1     0     0    0     -- token 1 chi thay chinh no
  thich     1     1     0    0     -- token 2 thay token 1, 2
  hoc       1     1     1    0     -- token 3 thay token 1, 2, 3
  may       1     1     1    1     -- token 4 thay ca 4 token

-- 1 = duoc nhin, 0 = bi che ve am vo cuc truoc softmax
Thử đoán

Nếu bỏ hẳn causal mask, để mọi token tự do nhìn thấy cả token đứng sau nó, rồi vẫn train model dự đoán token kế tiếp như bình thường — điều gì sẽ xảy ra với quá trình học? Viết ra dự đoán của bạn trước khi đọc tiếp.

Không che tương lai, token ở vị trí i có thể tính attention trỏ thẳng vào token kế tiếp — chính là đáp án model đang phải dự đoán. Model chỉ cần học "sao chép điểm số cao vào đúng vị trí đáp án đang nằm sẵn trong input", loss lập tức gần bằng không, nhưng vô dụng lúc sinh văn bản thật vì token kế tiếp chưa hề tồn tại để mà nhìn trộm. Hiện tượng này gọi là rò rỉ nhãn (label leakage) — causal mask tồn tại chính để chặn lỗ hổng đó.

3. Vì sao causal mask không phá mất tính song song vừa đạt được?

Bài 01 module này gọi tên nỗi đau của model đọc tuần tự: h_t được tính từ h_(t-1), bước t phải đợi bước t-1 xong — chuỗi phụ thuộc chạy dọc thời gian, không song song hoá được. Attention (bài 02–03) giải quyết đúng nỗi đau đó: mỗi token nhìn thẳng mọi token khác trong một phép tính ma trận duy nhất.

Causal mask ép mỗi token chỉ nhìn về quá khứ — ràng buộc này có tái tạo lại chuỗi phụ thuộc mà bài 01 vừa thoát khỏi không? Không, vì causal mask là một ma trận cố định, biết trước ngay khi biết độ dài câu, không phụ thuộc kết quả tính toán của bất kỳ vị trí nào khác. Áp ma trận cố định đó lên attention của toàn bộ chuỗi vẫn là một phép nhân ma trận duy nhất, không phải vòng lặp phải đợi bước trước.

Cộng thêm một điều kiện chỉ đúng lúc train: dữ liệu huấn luyện có sẵn toàn bộ câu trước khi train bắt đầu — nhãn của mọi vị trí đã nằm sẵn, không cần tính xong vị trí trước để có nhãn cho vị trí sau:

function train_step(cau X gom N token, causal_mask):
    H <- transformer_block(X, causal_mask)   -- 1 lan goi, tinh CA N vi tri cung luc
    loss <- 0
    for i tu 1 den N-1:                       -- vong lap nay chi CONG DON loss
        phan_phoi_i <- softmax(H[i])
        loss <- loss + cross_entropy(phan_phoi_i, X[i+1])   -- nhan X[i+1] DA CO SAN
    return loss
-- Time: 1 forward pass cho ca N vi tri, khong phai N buoc tuan tu

Khác với model đọc tuần tự, transformer_block(X, causal_mask) là một lệnh — bên trong nó, phép nhân ma trận attention xử lý mọi vị trí song song trên GPU. Vòng for phía sau chỉ cộng dồn loss cho từng vị trí, hoàn toàn không quyết định thứ tự tính toán của attention.

Khung train: câu có sẵn đi qua một lần forward kèm causal mask, toả ra loss cho mọi vị trí cùng lúc. Khung inference: prompt chạy qua transformer block, chọn một token, nối vào input rồi quay lại chạy từ đầu

4. Inference vẫn tuần tự — vòng lặp next-token quay lại

Bài đó đã trace vòng lặp autoregressive: model dự đoán một phân phối xác suất cho token kế tiếp, chọn một token từ đó, nối vào chuỗi input, rồi lặp lại. Bài này vừa giải thích: đúng vòng lặp đó là lý do inference của decoder-only transformer không bao giờ song song được như train.

Ở mục 3, train song song được vì nhãn của mọi vị trí đã có sẵn trong dữ liệu — model chỉ cần bị cấm nhìn trộm. Lúc sinh văn bản mới, điều kiện đó biến mất: token kế tiếp chưa tồn tại, nó chính là thứ model đang phải tạo ra. Causal mask lúc này gần như "tự động" — không có token tương lai nào để che, vì tương lai còn chưa được viết ra.

Hệ quả: muốn có token kế tiếp, model bắt buộc phải chạy xong transformer_block với chuỗi hiện có, lấy phân phối tại đúng vị trí cuối cùng, chọn một token, nối vào, rồi chạy lại toàn bộ transformer_block một lần nữa cho chuỗi vừa dài thêm một token:

input <- token cua prompt
while chua gap token ket thuc:
    H <- transformer_block(input, causal_mask)   -- chay lai TU DAU, input dai them 1 token
    phan_phoi_cuoi <- softmax(H[vi tri cuoi])     -- CHI can phan phoi tai vi tri cuoi cung
    token_moi <- chon_tu(phan_phoi_cuoi)
    input <- input + [token_moi]
-- Time: N lan goi transformer_block de sinh N token, khong phai 1 lan nhu train

Sinh một câu trả lời dài nghĩa là chạy transformer_block nhiều lần liên tiếp — mỗi lần đợi lần trước xong mới bắt đầu. Đây chính là độ trễ "chữ chạy từng dòng" ở đầu bài: độ trễ tỉ lệ thuận với số token sinh ra, không phải với độ dài prompt đầu vào — prompt được xử lý song song ngay trong lần forward đầu tiên, đúng như mục 3 vừa mổ.

Bài đó đã tách hai pha theo chi phí: train trả một lần rất nặng, inference trả theo từng lượt gọi. Bài này thêm một bất đối xứng khác nằm ngay trong cùng kiến trúc: bản thân train là một lần forward song song, còn inference là nhiều lần forward tuần tự — nên chi phí inference không chỉ tính theo lượt gọi, mà còn tăng theo đúng số token model phải sinh ra trong lượt gọi đó.

Biết là có — KV cache

Chạy lại toàn bộ transformer_block từ đầu cho mỗi token mới nghe có vẻ lãng phí — phần token cũ đã được tính hệt như lần trước. Thực tế các hệ thống production cache lại kết quả đó (KV cache) để đỡ tính lại phần quá khứ; bài này chỉ cần bạn biết cơ chế đó tồn tại, không đào sâu ở đây.

5. Pitfall tổng hợp

Nhầm 1 — nghĩ causal mask chỉ cần thiết lúc inference, để model "không gian lận" khi sinh:

✅ Causal mask áp dụng cả lúc train — đó chính là cơ chế mục 2 vừa giải thích: lúc train, nhãn tương lai đã nằm sẵn trong dữ liệu, nên phải chủ động che thì model mới học thật (không rò rỉ nhãn). Lúc inference, tương lai còn chưa tồn tại nên causal mask gần như không cần "làm gì" — không có gì để che.

Nhầm 2 — nghĩ vì train "song song mọi vị trí" nên có thể tăng tốc inference bằng cách sinh nhiều token cùng lúc theo cách tương tự:

✅ Train song song được vì nhãn của mọi vị trí đã có sẵn trước khi chạy (mục 3). Lúc sinh, token kế tiếp chưa tồn tại tới khi token hiện tại được chọn xong — không có "nhãn có sẵn" nào để tính song song. (Có kỹ thuật sinh nhanh hơn như speculative decoding, nhưng đó là chủ đề khác, ngoài phạm vi bài này.)

Nhầm 3 — nghĩ causal mask áp dụng cho toàn bộ transformer block, kể cả tầng feed-forward:

✅ Causal mask chỉ có ý nghĩa trong self-attention — nơi một token thực sự "nhìn" sang token khác. Tầng feed-forward xử lý từng vị trí hoàn toàn độc lập, không có bước nào để nhìn trộm token khác, nên không cần mask gì cả.

6. 📚 Deep Dive

📚 Nguồn gốc — đọc khi muốn xuống tầng sâu hơn

Paper chính chủ:

Ghi chú: bài này thuần cơ chế kiến trúc, không trích số liệu benchmark cụ thể từ hai paper trên — ví dụ đầu bài (tóm tắt nhanh, bài luận chậm) là minh hoạ định tính cho độ trễ, không phải kết quả đo từ một hệ thống cụ thể.

7. Liên hệ các bài khác

8. Tóm tắt

  • LLM hiện đại là decoder-only transformer: nhiều transformer block xếp chồng, tất cả dùng self-attention kèm causal mask.
  • Causal mask ép mỗi token chỉ được tính attention với token đứng trước nó, gán gần như âm vô cực cho mọi vị trí ở tương lai trước bước softmax.
  • Train song song được vì nhãn (token kế tiếp) đã có sẵn trong dữ liệu huấn luyện — model chỉ cần bị cấm nhìn trộm đáp án đó, không phải chờ tuần tự như model đọc kiểu cũ.
  • Causal mask là một ma trận cố định, không tạo ra chuỗi phụ thuộc theo thời gian như h_t phụ thuộc h_(t-1) — đây là lý do nó không phá mất tính song song mà attention vừa giành lại được.
  • Bỏ causal mask khi train sẽ gây rò rỉ nhãn: model học cách sao chép đáp án đang nằm sẵn trong input thay vì học pattern ngôn ngữ thật.
  • Inference vẫn tuần tự vì token kế tiếp chưa tồn tại — phải sinh xong token này mới tính được token sau, quay lại đúng vòng lặp autoregressive.
  • Bất đối xứng train song song / inference tuần tự khiến độ trễ sinh văn bản tỉ lệ thuận với số token sinh ra, không phải độ dài prompt.
  • KV cache tồn tại để đỡ tính lại phần quá khứ mỗi lần sinh token mới — chi tiết cơ chế nằm ngoài phạm vi bài này.

9. Tự kiểm tra

Tự kiểm tra
Q1
Giải thích cơ chế causal mask trong self-attention: nó thay đổi điều gì trước bước softmax, và vì sao token ở một vị trí không bao giờ 'nhìn thấy' token đứng ngay sau nó?
Causal mask gán gần như âm vô cực cho mọi điểm attention trỏ từ một vị trí tới vị trí đứng sau nó, ngay trước khi áp softmax — softmax biến giá trị âm vô cực thành trọng số gần bằng không, nên token đó coi như không nhận thông tin gì từ tương lai. Mask áp dụng đồng loạt cho toàn bộ ma trận attention, nên không vị trí nào lọt qua quy tắc đó.
Q2
Nếu bỏ causal mask lúc train, model vẫn có thể tính được loss rất thấp — nhưng đó không phải dấu hiệu tốt. Giải thích vì sao, và gọi tên hiện tượng này.
Không mask, một vị trí có thể tính attention trỏ thẳng vào chính token nó đang phải dự đoán — token đó đã nằm sẵn trong dữ liệu. Model chỉ cần học sao chép giá trị đó, loss giảm rất nhanh mà không phản ánh khả năng dự đoán thật. Hiện tượng này gọi là rò rỉ nhãn (label leakage): đo tốt lúc train nhưng vô dụng lúc sinh văn bản thật, khi token cần dự đoán chưa hề tồn tại để mà nhìn trộm.
Q3
Causal mask ép mỗi token chỉ nhìn về quá khứ. Vì sao ràng buộc này không tái tạo lại đúng chuỗi phụ thuộc tuần tự mà model đọc tuần tự kiểu cũ từng mắc phải?
Chuỗi phụ thuộc của model đọc tuần tự nằm ở chỗ trạng thái một bước cần giá trị của bước ngay trước làm đầu vào, nên phải tính xong bước trước mới tính được bước sau. Causal mask không có đặc điểm đó: nó là một ma trận cố định, biết trước ngay khi biết độ dài câu, không phụ thuộc kết quả tính toán của vị trí nào khác — nên áp được lên toàn chuỗi trong một phép nhân ma trận duy nhất, không cần vòng lặp chờ.
Q4
Một đồng nghiệp nói: 'Train song song hoá được thì chắc sinh văn bản cũng nhanh song song luôn.' Chỉ ra chỗ sai trong lập luận này.
Train song song được nhờ một điều kiện đặc thù: nhãn của mọi vị trí đã có sẵn trong dữ liệu trước khi chạy, model chỉ cần bị cấm nhìn trộm. Lúc sinh văn bản, điều kiện đó không tồn tại — token kế tiếp chưa được tạo ra, chính là thứ model đang phải quyết định. Không có nhãn có sẵn nào để tính song song, nên model phải sinh xong token này mới có input để sinh token sau.
Q5
Causal mask áp dụng ở đâu bên trong một transformer block: self-attention, feed-forward, cả hai, hay không tầng nào? Giải thích vì sao.
Chỉ trong self-attention — đây là tầng duy nhất một token thực sự 'nhìn' sang token khác để tính điểm tương quan, nên là nơi duy nhất có nguy cơ nhìn trộm tương lai. Tầng feed-forward xử lý từng vị trí hoàn toàn độc lập, không có bước nào đọc thông tin của vị trí khác, nên không cần mask gì cả.

Bài tiếp theo: Scaling laws — nhìn định tính

Bài này đáng gửi cho bạn học cùng?

Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.

Bài này có giúp bạn hiểu bản chất không?

Hỏi đáp về bài này

Chưa có câu hỏi

Đặt câu hỏi

Có gì chưa rõ trong bài? Đặt câu hỏi đầu tiên — câu trả lời từ cộng đồng giúp bạn (và người sau).

Đặt câu hỏi đầu tiên

Bài tiếp theo

Scaling laws — dự đoán loss khi tăng params, data, compute