Bỏ qua để đến nội dung

Transformers nâng cao

Sau bài này, bạn sẽ:

  • Đặt được BERT, GPT, T5 và BART vào ba họ Transformer: encoder-only (chỉ có encoder), decoder-only (chỉ có decoder) và encoder–decoder, và giải thích mỗi họ hợp việc gì nhờ mặt nạ và mục tiêu huấn luyện của nó.
  • So sánh được các cách mã hóa vị trí: sin/cos, embedding vị trí học được, vị trí tương đối, RoPE (rotary position embedding, mã hóa vị trí xoay) và ALiBi (attention with linear biases), nhất là khả năng ngoại suy (extrapolation) của mỗi cách: dùng cho chuỗi dài hơn lúc huấn luyện.
  • Giải thích được vì sao đặt chuẩn hóa lớp (layer normalization) trước khối con (Pre-LN) dễ huấn luyện mô hình sâu hơn đặt sau (Post-LN).
  • Tính được chi phí L2L^2 của attention, và nhận ra mỗi hướng giảm nó đánh đổi gì: attention cửa sổ trượt (sliding window attention), attention tuyến tính (linear attention), FlashAttention, KV cache với MQA (multi-query attention) và GQA (grouped-query attention, attention truy vấn theo nhóm).
  • BERT, thuộc phần Xử lý ngôn ngữ tự nhiên (Đã học)
  • GPT và huấn luyện phân tán, thuộc phần Xử lý ngôn ngữ tự nhiên (Đã học)

Transformer gốc (Vaswani và cộng sự, 2017) là một mô hình dịch máy: encoder đọc câu nguồn, decoder viết câu đích. Chỉ hai năm sau, cùng bộ linh kiện đó đã thành ba họ rất khác nhau:

  • BERT (2019) chỉ giữ encoder. Nó đọc cả câu một lần, mỗi từ nhìn cả hai phía, rồi trả về một vectơ cho từng từ. Hợp để hiểu: phân loại câu, tìm tên riêng.
  • GPT (2018 trở đi) chỉ giữ decoder. Nó đọc từ trái sang phải và đoán token kế tiếp. Hợp để viết tiếp.
  • T5 và BART (2020) giữ cả hai nửa. Hợp để biến một văn bản thành văn bản khác: dịch, tóm tắt.

Giống ba ngôi nhà xây từ cùng một loại gạch: khác nhau ở chỗ đặt tường (mặt nạ) và ở việc ngôi nhà được dùng để làm gì (mục tiêu huấn luyện).

BERT chỉ đọc được tối đa 512 token, khoảng một trang giấy. Llama 3 (bản 3.1, 2024) đọc được 128K token, cỡ một cuốn sách (xem bài Họ LLaMA). Hai chỗ phải thay đổi để đi được quãng đường đó:

  1. Vị trí. BERT học một vectơ riêng cho mỗi vị trí từ 0 tới 511. Đưa vào token thứ 600 thì không có vectơ nào cho nó. Cần một cách mã hóa vị trí dùng được cho cả những độ dài chưa gặp.
  2. Chi phí. Self-attention chấm điểm mọi cặp token. Gấp đôi độ dài thì số cặp gấp bốn. Với 128K token, chỉ riêng ma trận điểm của một đầu ở một lớp đã khoảng 34 GB nếu lưu đủ (ô code ở dưới sẽ tính).

Còn một chuyện nữa: mô hình càng sâu thì càng khó huấn luyện. Vị trí đặt một lớp chuẩn hóa hóa ra quan trọng hơn bạn tưởng.

Bài toán của bài này là: cùng một khối Transformer, vì sao lại có ba họ mô hình, và cần thay những linh kiện nào (mã hóa vị trí, chỗ đặt chuẩn hóa lớp, cách tính attention) để mô hình sâu hơn và đọc được văn bản dài hơn nhiều lần?

Ba họ khác nhau ở hai thứ: mặt nạ quyết định token nào nhìn được token nào, và mục tiêu huấn luyện quyết định mô hình học đoán cái gì.

Encoder-only. Mặt nạ toàn số 0: mỗi token nhìn mọi token. Không thể huấn luyện bằng cách đoán token kế tiếp, vì token kế tiếp đã nằm ngay trong tầm nhìn. BERT vì vậy che một số token rồi bắt mô hình đoán lại (mô hình ngôn ngữ có che, masked language modeling, MLM). Chi tiết ở bài BERT.

Decoder-only. Mặt nạ nhân quả. Mục tiêu là đoán token kế tiếp ở mọi vị trí. Mọi việc, kể cả dịch hay phân loại, được viết thành “đọc phần đầu, viết tiếp phần sau”. Chi tiết ở bài GPT và huấn luyện phân tán.

Encoder–decoder. Encoder không có mặt nạ, decoder có mặt nạ nhân quả và nhìn sang encoder qua cross-attention, như ở bài Mô hình chuỗi sang chuỗi (Seq2Seq). Hai mô hình nổi tiếng nhất của họ này khác nhau ở cách làm hỏng văn bản khi pre-train, một kiểu khử nhiễu (denoising):

  • T5 (Raffel và cộng sự, 2020) dùng làm hỏng đoạn (span corruption). Chọn ngẫu nhiên 15% token, độ dài đoạn trung bình 3 (cấu hình cuối của bài báo). Mỗi đoạn liền nhau bị thay bằng một token đánh dấu (sentinel) riêng, ví dụ <X>, <Y>. Đầu ra chỉ gồm các đoạn bị bỏ, mỗi đoạn đứng sau token đánh dấu của nó, cộng một token đánh dấu cuối.
  • BART (Lewis và cộng sự, 2020) thử nhiều kiểu làm hỏng. Bản cuối dùng điền đoạn (text infilling): các đoạn có độ dài lấy theo phân phối Poisson với λ=3\lambda = 3 bị thay bằng một token [MASK], tổng cộng 30% token bị che; thêm vào đó các câu trong văn bản bị xáo thứ tự. Decoder phải viết lại toàn bộ văn bản gốc. Vì mỗi đoạn chỉ còn một [MASK], mô hình còn phải đoán đoạn bị mất dài bao nhiêu.

Một ví dụ tự đặt, tách theo âm tiết cho dễ đọc. Câu gốc: “Hà Nội là thủ đô của Việt Nam”.

Mục tiêuĐầu vào encoderĐích của decoder
T5, làm hỏng đoạnHà Nội <X> đô của <Y> Nam<X> là thủ <Y> Việt <Z>
BART, điền đoạnHà Nội [MASK] đô của [MASK] NamHà Nội là thủ đô của Việt Nam

Đích của T5 ngắn hơn nhiều, nên mỗi bước huấn luyện rẻ hơn. Phép so ba kiến trúc trong bài báo T5 có ở bài Seq2Seq.

Self-attention không biết thứ tự. Không có mặt nạ, đổi chỗ hai token trong đầu vào thì đầu ra chỉ đổi chỗ theo, các giá trị giữ nguyên: “chó cắn người” và “người cắn chó” là cùng một tập token. Phải đưa vị trí vào từ bên ngoài. Có hai chỗ để đưa vào: cộng vào embedding ở đầu vào, hoặc sửa điểm attention ở mọi lớp.

Sin/cos (Vaswani và cộng sự, 2017). Vị trí pp được mã hóa thành một vectơ DD chiều, với DD là số chiều của mô hình, rồi cộng vào embedding. Cặp chiều thứ kk là một sóng sin và cos:

PE(p, 2k)=sin⁡ ⁣(p10 0002k/D),PE(p, 2k+1)=cos⁡ ⁣(p10 0002k/D)\mathrm{PE}_{(p,\,2k)} = \sin\!\left(\frac{p}{10\,000^{2k/D}}\right), \qquad \mathrm{PE}_{(p,\,2k+1)} = \cos\!\left(\frac{p}{10\,000^{2k/D}}\right)

Mỗi cặp chiều quay một tốc độ: cặp đầu đổi nhanh theo pp, cặp cuối đổi rất chậm, như kim giây và kim giờ. Công thức tính được cho mọi pp, không có tham số học.

Embedding vị trí học được. Thay công thức bằng một bảng Lmax⁡×DL_{\max} \times D học cùng mô hình. Vaswani và cộng sự thử cả hai và thấy kết quả gần như như nhau; họ chọn sin/cos vì hy vọng nó dùng được cho chuỗi dài hơn lúc huấn luyện. BERT dùng bảng học được với Lmax⁡=512L_{\max} = 512, GPT-2 với Lmax⁡=1 024L_{\max} = 1\,024. Đây là gốc của giới hạn 512 token: vị trí 512 trở đi không có hàng nào trong bảng.

Vị trí tương đối. Hai cách trên cho mỗi token một nhãn tuyệt đối. Nhưng điều thường quan trọng là khoảng cách j−ij - i: “mèo” đứng ngay trước “đuổi” thì ý nghĩa như nhau dù câu nằm ở đầu hay giữa văn bản. Shaw, Uszkoreit và Vaswani (2018) thêm vào khóa, và cả vào giá trị, một vectơ học được theo j−ij - i, cắt ở ±k\pm k (họ dùng k=16k = 16): mọi khoảng cách xa hơn kk dùng chung một vectơ. T5 làm gọn hơn nữa: cộng một số bh(j−i)b_h(j - i) vào điểm attention. Mỗi đầu hh có bộ số riêng, dùng chung cho mọi lớp; khoảng cách được gom vào 32 nhóm, rộng dần theo thang logarit tới 128, xa hơn nữa thì chung một nhóm.

RoPE (Su và cộng sự, 2021) xoay q\mathbf{q} và k\mathbf{k} một góc tỉ lệ với vị trí, ở mọi lớp. Nhờ tính chất của phép xoay, điểm q⊤k\mathbf{q}^\top\mathbf{k} sau khi xoay chỉ phụ thuộc vị trí qua độ lệch. Phép dẫn và ô NumPy kiểm chứng có ở bài Họ LLaMA.

ALiBi (Press, Smith và Lewis, 2022) bỏ hết vectơ vị trí. Nó chỉ cộng vào điểm attention một bias cố định, không học, tỉ lệ với khoảng cách:

sij=qi⊤kjd−mh (i−j),j≤is_{ij} = \frac{\mathbf{q}_i^\top \mathbf{k}_j}{\sqrt{d}} - m_h\,(i - j), \qquad j \le i
  • sijs_{ij} là điểm trước softmax của truy vấn ii với khóa jj. ALiBi được thiết kế cho mô hình nhân quả, nên chỉ xét j≤ij \le i; ô j>ij > i vẫn bị mặt nạ che.
  • i−j≥0i - j \ge 0 là khoảng cách. Token càng xa, điểm bị trừ càng nhiều. Khoảng cách 0 (chính nó) không bị trừ.
  • mh>0m_h > 0 là độ dốc (slope) của đầu hh. Ở đây mhm_h là tên của bài báo, không phải số mẫu mm như quy ước chung của site.

Độ dốc chọn thế nào? Với HH đầu, các độ dốc là cấp số nhân có số hạng đầu và công bội cùng bằng 2−8/H2^{-8/H} (cách tính này dành cho HH là lũy thừa của 2, như 8 hay 16). Với H=8H = 8, đó là 12,14,…,1256\tfrac{1}{2}, \tfrac{1}{4}, \dots, \tfrac{1}{256}. Đầu có mh=1/2m_h = 1/2 trừ 5 điểm cho token cách 10 vị trí, nên gần như chỉ nhìn vài token gần. Đầu có mh=1/256m_h = 1/256 chỉ trừ khoảng 0.04, nên vẫn nhìn xa được. Các tác giả thử cho độ dốc học được nhưng kết quả ngoại suy không tốt, nên giữ cố định.

Vì sao ALiBi ngoại suy được? Không có tham số nào gắn với một vị trí cụ thể. Ở độ dài mới, mọi thứ mô hình thấy vẫn là “điểm nội dung trừ một khoản theo khoảng cách”, đúng loại tín hiệu nó đã quen. Bài báo cho số đo: mô hình 1.3 tỉ tham số huấn luyện ở L=1 024L = 1\,024, dùng ở L=2 048L = 2\,048, đạt cùng perplexity với mô hình sin/cos huấn luyện thẳng ở 2 0482\,048, mà huấn luyện nhanh hơn 11% và tốn ít hơn 11% bộ nhớ. Trên WikiText-103, perplexity của ALiBi tốt nhất quanh 2L2L tới 3L3L, và vẫn giữ tốt khi đánh giá tới hơn 10 000 token, kể cả với mô hình huấn luyện ở L=512L = 512. Cũng trong bài báo đó, sin/cos ngoại suy rất kém dù về lý thuyết tính được cho mọi vị trí; RoPE khá hơn nhưng vẫn tụt; bias của T5 ngoại suy tốt nhưng chậm hơn hẳn.

CáchVị trí đưa vào đâuTham số họcDùng ởChuỗi dài hơn lúc huấn luyện
Sin/cosCộng vào embedding đầu vàoKhôngTransformer gốcTính được, nhưng chất lượng tụt nhanh
Học đượcCộng vào embedding đầu vàoBảng Lmax⁡×DL_{\max} \times DBERT, GPT-2Không có vectơ cho vị trí ≥Lmax⁡\ge L_{\max}
Tương đối (bias T5)Cộng vào điểm, mọi lớpMột số cho mỗi nhóm khoảng cách, mỗi đầuT5Khá tốt, nhưng chậm
RoPEXoay q\mathbf{q}, k\mathbf{k}, mọi lớpKhôngHọ LlamaTụt khi vượt xa; cần huấn luyện thêm để mở rộng
ALiBiCộng −mh(i−j)-m_h(i - j) vào điểm, mọi lớpKhôngMô hình của bài báo ALiBiTốt nhất quanh 2L2L, giữ tốt tới hơn 10 000 token

Mỗi khối Transformer có hai khối con, attention và mạng truyền thẳng (feed-forward network, FFN). Gọi chung một khối con là FF. Có hai chỗ đặt chuẩn hóa lớp LN\mathrm{LN}:

Post-LN:H←LN(H+F(H))Pre-LN:H←H+F(LN(H))\text{Post-LN:}\quad \mathbf{H} \leftarrow \mathrm{LN}\big(\mathbf{H} + F(\mathbf{H})\big) \qquad\qquad \text{Pre-LN:}\quad \mathbf{H} \leftarrow \mathbf{H} + F\big(\mathrm{LN}(\mathbf{H})\big)

Transformer gốc và BERT dùng Post-LN. GPT-2 dùng Pre-LN, cộng thêm một LN\mathrm{LN} ở cuối chồng khối. T5 và họ Llama cũng đặt chuẩn hóa trước (pre-norm), nhưng với biến thể chỉ co giãn: T5 bỏ vectơ dịch của LN, Llama dùng RMSNorm.

Nhìn đường đi của gradient. Gọi Hl\mathbf{H}_l là đầu ra sau khối con thứ ll, có NN khối con; mỗi lớp có hai khối con nên N=2NlayerN = 2N_{\text{layer}}, với NlayerN_{\text{layer}} là số lớp. Với Pre-LN, mỗi bước chỉ cộng thêm một phần hiệu chỉnh, nên trải hết các bước ta có:

HN=Hl+∑k=l+1NFk(LN(Hk−1))\mathbf{H}_N = \mathbf{H}_l + \sum_{k=l+1}^{N} F_k\big(\mathrm{LN}(\mathbf{H}_{k-1})\big)

Đạo hàm của HN\mathbf{H}_N theo Hl\mathbf{H}_l vì vậy là ma trận đơn vị cộng với đạo hàm của tổng. Phần đơn vị đến từ kết nối tắt (residual connection) và là một đường thẳng từ đầu ra về mọi lớp: gradient về tới lớp ll mà không phải đi qua một phép biến đổi nào. Với Post-LN, LN\mathrm{LN} nằm ngay trên đường chính, nên gradient từ đầu ra phải đi qua LN\mathrm{LN} của mọi khối phía trên.

Xiong và cộng sự (2020) phân tích chuyện này lúc khởi tạo. Với Post-LN, gradient của các tham số ở lớp cuối có độ lớn cỡ O(Dln⁡D)O(D\sqrt{\ln D}), với DD là số chiều của mô hình, không giảm khi thêm lớp, và lớn hơn hẳn gradient ở các lớp đầu. Với Pre-LN, nó cỡ O(Dln⁡D/Nlayer)O(D\sqrt{\ln D / N_{\text{layer}}}), nhỏ đi khi mô hình sâu thêm, và đều giữa các lớp. Gradient lớn ở lớp cuối cộng với tốc độ học lớn ngay bước đầu làm huấn luyện Post-LN mất ổn định, nên Post-LN cần khởi động tốc độ học (warmup: tăng dần tốc độ học trong các bước đầu, xem bài Debug mô hình ngôn ngữ). Với Pre-LN, các tác giả bỏ được warmup mà vẫn đạt kết quả tương đương, với ít thời gian huấn luyện hơn.

Ma trận điểm QK⊤\mathbf{Q}\mathbf{K}^\top có L×LL \times L ô, mỗi ô là một tích vô hướng dd chiều. Vậy mỗi đầu ở mỗi lớp tốn khoảng L2dL^2 d phép nhân cho điểm, thêm L2dL^2 d cho bước nhân với V\mathbf{V}, và L2L^2 số nếu lưu cả ma trận. Gấp đôi LL thì cả hai gấp bốn. Ở 512 token, đó là chuyện nhỏ; ở 128K token thì không. Có bốn hướng xử lý, mỗi hướng đánh đổi một thứ khác.

1. Attention thưa: chỉ tính một phần các cặp. Mỗi token chỉ nhìn ww token gần nhất, gọi là cửa sổ trượt. Với mặt nạ nhân quả, token ii nhìn các vị trí jj với i−w<j≤ii - w < j \le i. Số ô mỗi hàng không quá ww, nên chi phí khoảng L⋅wL \cdot w, tăng tuyến tính theo LL.

  • Một token không nhìn thẳng được token cách xa hơn w−1w - 1, nhưng các lớp chồng lên nhau mở rộng tầm nhìn: sau NlayerN_{\text{layer}} lớp, thông tin có thể đi xa tới khoảng Nlayer(w−1)N_{\text{layer}} (w - 1) vị trí, như các lớp tích chập chồng nhau. Mistral 7B (Jiang và cộng sự, 2023) dùng w=4 096w = 4\,096 với 32 lớp và tính tầm nhìn lý thuyết khoảng 131K token. Cửa sổ còn giới hạn KV cache: chỉ cần giữ khóa và giá trị của ww token gần nhất.
  • Longformer (Beltagy, Peters và Cohan, 2020) là một encoder: cửa sổ hai chiều, mỗi token nhìn w/2w/2 token mỗi bên (w=512w = 512), có thể giãn (dilated: chừa khoảng trống trong cửa sổ để nhìn xa hơn). Thêm vào đó vài token có attention toàn cục (global attention) do người dùng chọn theo tác vụ, ví dụ [CLS] hay các token của câu hỏi: chúng nhìn mọi token và mọi token nhìn chúng. Longformer đọc được 4 096 token, gấp 8 lần BERT.
  • BigBird (Zaheer và cộng sự, 2020) ghép ba kiểu: cửa sổ, vài token toàn cục, và vài cặp ngẫu nhiên. Các tác giả chứng minh mẫu thưa này vẫn giữ được sức biểu diễn của attention đầy đủ, và đọc được chuỗi dài gấp khoảng 8 lần trên cùng phần cứng.

2. Attention tuyến tính: đổi thứ tự phép nhân. Viết hàng ii của attention dưới dạng trung bình có trọng số, với hàm độ giống sim(q,k)=exp⁡(q⊤k/d)\mathrm{sim}(\mathbf{q}, \mathbf{k}) = \exp(\mathbf{q}^\top\mathbf{k}/\sqrt{d}):

oi=∑jsim(qi,kj) vj∑jsim(qi,kj)\mathbf{o}_i = \frac{\sum_{j} \mathrm{sim}(\mathbf{q}_i, \mathbf{k}_j)\, \mathbf{v}_j}{\sum_{j} \mathrm{sim}(\mathbf{q}_i, \mathbf{k}_j)}

Katharopoulos và cộng sự (2020) thay sim\mathrm{sim} bằng một tích vô hướng của hai vectơ đặc trưng, sim(q,k)=ϕ(q)⊤ϕ(k)\mathrm{sim}(\mathbf{q}, \mathbf{k}) = \phi(\mathbf{q})^\top\phi(\mathbf{k}), với ϕ(x)=elu(x)+1\phi(x) = \mathrm{elu}(x) + 1 áp từng thành phần, luôn dương. Thay vào, rồi đưa ϕ(qi)\phi(\mathbf{q}_i) ra ngoài tổng vì nó không phụ thuộc jj:

oi=∑jϕ(qi)⊤ϕ(kj) vj∑jϕ(qi)⊤ϕ(kj)=ϕ(qi)⊤(∑jϕ(kj) vj⊤)ϕ(qi)⊤(∑jϕ(kj))\mathbf{o}_i = \frac{\sum_{j} \phi(\mathbf{q}_i)^\top\phi(\mathbf{k}_j)\, \mathbf{v}_j}{\sum_{j} \phi(\mathbf{q}_i)^\top\phi(\mathbf{k}_j)} = \frac{\phi(\mathbf{q}_i)^\top \Big(\sum_{j} \phi(\mathbf{k}_j)\,\mathbf{v}_j^\top\Big)}{\phi(\mathbf{q}_i)^\top \Big(\sum_{j} \phi(\mathbf{k}_j)\Big)}

Hai tổng trong ngoặc không phụ thuộc ii. Tổng ở tử là một ma trận d×dd \times d, tổng ở mẫu là một vectơ dd chiều. Tính chúng một lần, mỗi hàng chỉ còn vài tích với ϕ(qi)\phi(\mathbf{q}_i): tổng chi phí cỡ Ld2L d^2 thay vì L2dL^2 d, và không bao giờ có ma trận L×LL \times L. Với mặt nạ nhân quả, hai tổng thành tổng cộng dồn tới ii, cập nhật mỗi bước như trạng thái ẩn của một mạng hồi tiếp; vì vậy bài báo có tên “Transformers are RNNs”. Cái giá: đây là một dạng attention khác, không cho cùng kết quả với softmax, và mô hình phải được huấn luyện với chính dạng này.

3. FlashAttention: tính đúng như cũ, đọc ghi bộ nhớ ít đi. Dao và cộng sự (2022) để ý rằng trên GPU, attention thường bị chặn bởi việc đọc ghi bộ nhớ hơn là số phép nhân. GPU có bộ nhớ chính lớn nhưng chậm (HBM, hàng chục GB) và bộ nhớ trên chip nhanh nhưng nhỏ (SRAM, khoảng 20 MB trên A100). Cách cài thường ghi ma trận điểm L×LL \times L ra HBM, đọc lại để làm softmax, ghi kết quả, rồi đọc lại lần nữa để nhân với V\mathbf{V}.

FlashAttention chia Q\mathbf{Q}, K\mathbf{K}, V\mathbf{V} thành từng khối vừa SRAM và làm hết các bước cho một khối trước khi ghi ra. Khó khăn là softmax cần cả hàng. Mẹo là giữ cho mỗi hàng hai số chạy: điểm lớn nhất μ\mu và tổng ℓ=∑jesij−μ\ell = \sum_j e^{s_{ij} - \mu} trên các khối đã đi qua. Khi gặp khối mới có điểm lớn nhất μ′>μ\mu' > \mu, tổng cũ chỉ cần nhân với eμ−μ′e^{\mu - \mu'} để đổi về mốc mới, vì es−μ⋅eμ−μ′=es−μ′e^{s - \mu} \cdot e^{\mu - \mu'} = e^{s - \mu'}; phần đầu ra đã cộng dồn cũng được nhân với cùng hệ số. Khi lan truyền ngược, FlashAttention tính lại các khối thay vì lưu ma trận.

Kết quả là attention chính xác: cùng đầu ra (chỉ khác sai số làm tròn), cùng bậc L2L^2 phép nhân, nhưng bộ nhớ chỉ tăng tuyến tính theo LL và chạy nhanh hơn. Bài báo đo được huấn luyện GPT-2 với L=1 024L = 1\,024 nhanh gấp 3 lần cách cài chuẩn, và BERT-large với L=512L = 512 nhanh hơn 15% so với kỷ lục MLPerf 1.1. Vì không đổi kết quả, nó dùng được ngay cho mô hình đã huấn luyện.

4. Khi sinh văn bản: KV cache, MQA, GQA. Khi sinh, khóa và giá trị của mọi token đã có được lưu lại (KV cache), và cache lớn lên theo LL. MQA (Shazeer, 2019) cho mọi đầu truy vấn dùng chung một cặp khóa, giá trị; GQA (Ainslie và cộng sự, 2023) chia đầu truy vấn thành nhóm, mỗi nhóm chung một cặp. Chi tiết ở bài Họ LLaMA và Deploy LLM: nhiều GPU, phân tán.

Các hướng này cộng được với nhau: Mistral 7B dùng cùng lúc cửa sổ trượt, GQA và FlashAttention.

  • Ngoại suy có giới hạn. Perplexity của ALiBi tốt nhất quanh 2L2L; RoPE cần huấn luyện thêm để mở rộng ngữ cảnh. Hộp thứ hai ở mục Lỗi hay gặp nói rõ hơn.
  • Ngữ cảnh dài không có nghĩa là dùng tốt ngữ cảnh dài. Đọc được 128K token không bảo đảm mô hình tìm đúng chi tiết ở giữa văn bản; Liu và cộng sự (2023) thấy mô hình dùng thông tin ở đầu và cuối ngữ cảnh tốt hơn ở giữa (xem bài Chatbot RAG).
  • Bài này chỉ dừng ở ý tưởng. Phép dẫn RoPE, RMSNorm, SwiGLU và GQA chi tiết ở bài Họ LLaMA; mixture of experts và MLA ở bài Họ DeepSeek.

Bảng dưới đặt ba họ cạnh nhau. Hãy đọc theo từng cột: mặt nạ và mục tiêu huấn luyện ở hai hàng đầu quyết định đầu ra và việc hợp ở các hàng sau.

Tiêu chíEncoder-onlyDecoder-onlyEncoder–decoder
Mặt nạHai chiều: mọi token nhìn mọi tokenNhân quả: chỉ nhìn về trướcEncoder hai chiều; decoder nhân quả, cộng cross-attention sang encoder
Mục tiêu pre-trainChe token rồi đoán lại (MLM)Đoán token kế tiếpKhử nhiễu: làm hỏng đoạn (T5), điền đoạn và xáo câu (BART)
Đầu raMột vectơ cho mỗi tokenPhân phối của token kế tiếp, sinh từng tokenMột chuỗi mới, sinh từng token
Hợp vớiPhân loại, gán nhãn token, QA trích xuất, embedding cho truy xuấtSinh văn bản, chat; gần như mọi việc qua promptDịch, tóm tắt, biến văn bản này thành văn bản khác
Ví dụBERT (2019), RoBERTa, PhoBERTGPT (2018–2020), LlamaT5 (2020), BART (2020)

Bảng thứ hai tóm lại các hướng làm attention rẻ hơn. Cột cuối là thứ bạn phải trả.

HướngÝ chínhSố điểm phải tínhBộ nhớ cho điểmKết quả so với attention đầy đủĐánh đổi
Đầy đủMọi cặpL2L^2L2L^2Gốc
Cửa sổ trượt (Longformer, BigBird)Chỉ ww token gần, cộng vài token toàn cục (BigBird thêm cặp ngẫu nhiên)khoảng LwL wkhoảng LwL wKhácKhông nhìn thẳng được token xa
Attention tuyến tínhĐổi thứ tự nhân, bỏ softmaxKhông có ma trận điểm, chi phí Ld2L d^2d2d^2KhácLà mô hình khác, phải huấn luyện lại
FlashAttentionTính từng khối trong SRAML2L^2LLGiống (trừ làm tròn)Phải viết kernel GPU riêng
MQA, GQAĐầu truy vấn dùng chung khóa, giá trịNhư đầy đủNhư đầy đủ; KV cache nhỏ hơnKhácCó thể giảm chất lượng một chút

Code trong bài chạy thử lần cuối tháng 11/2026, với Python 3.14 và NumPy 2.4 (bản có trong Pyodide). Cả hai ô chạy ngay trên trang và chạy độc lập: ô nào cũng tự import và tự tạo dữ liệu.

Ô đầu tiên dựng ba ma trận cho một chuỗi L=6L = 6 token: mặt nạ nhân quả, mặt nạ cửa sổ trượt w=3w = 3, và bias ALiBi của đầu thứ nhất. Số 1 nghĩa là ô được nhìn. Rồi ô cộng bias vào điểm attention của Q\mathbf{Q}, K\mathbf{K} ngẫu nhiên (hạt giống 0) và kiểm tra softmax.

Mặt nạ nhân quả, cửa sổ trượt và bias ALiBi

# Mặt nạ nhân quả, mặt nạ cửa sổ trượt và bias ALiBi trên một chuỗi ngắn
import numpy as np
np.set_printoptions(precision=3, suppress=True)
L, w = 6, 3 # độ dài chuỗi, cỡ cửa sổ
i = np.arange(L)[:, None] # hàng: vị trí truy vấn
j = np.arange(L)[None, :] # cột: vị trí khóa
causal = j <= i # chỉ nhìn về trước, kể cả chính nó
window = causal & (i - j < w) # và cách không quá w - 1 vị trí
print("causal mask:")
print(causal.astype(int))
print(f"sliding window, w = {w}:")
print(window.astype(int))
# Độ dốc ALiBi cho n đầu: cấp số nhân, số hạng đầu và công bội 2^(-8/n)
n_heads = 8
slopes = 2.0 ** (-8 / n_heads * np.arange(1, n_heads + 1))
print("slopes:", slopes)
# Bias của đầu 1: -m * (i - j) ở các ô được nhìn, -inf ở các ô bị che
m = slopes[0]
bias = np.where(causal, m * (j - i), -np.inf)
print("ALiBi bias, head 1:")
print(bias)
print("last row, head 8:", slopes[-1] * (j[0] - (L - 1)))
# Cộng bias vào điểm attention rồi softmax theo từng hàng
rng = np.random.default_rng(0)
d = 4
Q, K = rng.normal(size=(L, d)), rng.normal(size=(L, d))
scores = Q @ K.T / np.sqrt(d) + bias
P = np.exp(scores - scores.max(axis=1, keepdims=True))
P /= P.sum(axis=1, keepdims=True)
print("weights, last row:", P[-1])
print("row sums:", P.sum(axis=1))

Lần chạy đầu cần tải Python, khoảng 10 MB.

Đọc kết quả:

  • Mặt nạ nhân quả là tam giác dưới: hàng ii có i+1i + 1 số 1 (đếm hàng từ 0).
  • Cửa sổ trượt giống hệt ở ba hàng đầu. Từ hàng 3, mỗi hàng chỉ còn đúng 3 số 1, và dải số 1 trượt sang phải theo hàng. Hàng cuối nhìn vị trí 3, 4, 5, không còn nhìn vị trí 0.
  • Độ dốc của 8 đầu là 0.5 0.25 0.125 ... 0.004 (máy in làm tròn tới 3 chữ số; số cuối là 1/256≈0.00391/256 \approx 0.0039).
  • Bias của đầu 1 bằng 0 trên đường chéo, giảm 0.5 mỗi bước sang trái, và là -inf ở phía trên đường chéo (ô bị mặt nạ che). Hàng cuối là −2.5,−2,…,0-2.5, -2, \dots, 0. Cùng hàng đó ở đầu 8 chỉ là −0.02,−0.016,…,0-0.02, -0.016, \dots, 0: gần như không phạt.
  • Trọng số hàng cuối là 0.03 0.072 0.107 0.116 0.448 0.228, và mọi hàng cộng lại bằng 1. Bias chỉ dịch điểm trước softmax, nên mỗi hàng vẫn là một phân phối xác suất. Ở đây Q\mathbf{Q}, K\mathbf{K} ngẫu nhiên nên trọng số không giảm đều theo khoảng cách, nhưng token xa nhất (vị trí 0) bị trừ nhiều nhất và có trọng số nhỏ nhất.

Ô thứ hai đếm số ô điểm phải tính trong attention nhân quả của một đầu ở một lớp, theo hai cách: đầy đủ (L(L+1)/2L(L+1)/2 ô) và cửa sổ trượt w=512w = 512. Cột GB là dung lượng nếu lưu cả ma trận L×LL \times L ở 2 byte mỗi số, như cách cài thường. Phần sau làm phép đổi thứ tự nhân của attention tuyến tính với L=1 000L = 1\,000, d=16d = 16, rồi so hai cách tính.

Chi phí L² và mẹo đổi thứ tự nhân

# Chi phí L^2 của attention, cửa sổ trượt, và mẹo đổi thứ tự nhân
import numpy as np
# Đếm cặp (i, j) phải tính điểm trong attention nhân quả, một đầu, một lớp
w = 512 # cỡ cửa sổ trượt
print(" L full pairs window pairs ratio GB")
for L in [512, 2048, 8192, 32768, 131072]:
full = L * (L + 1) // 2 # mọi j <= i
k = min(L, w) # hàng thứ k trở đi đủ w ô
win = k * (k + 1) // 2 + (L - k) * w
gb = L * L * 2 / 1e9 # cả ma trận L x L, 2 byte mỗi số
print(f"{L:6d} {full:13d} {win:13d} {full / win:6.1f} {gb:7.3f}")
def phi(x):
# elu(x) + 1: luôn dương, như Katharopoulos và cộng sự (2020)
return np.where(x > 0, x + 1, np.exp(x))
# Attention tuyến tính (không có mặt nạ): softmax thay bằng phi(q).phi(k)
rng = np.random.default_rng(0)
L, d = 1000, 16
Q, K, V = (rng.normal(size=(L, d)) for _ in range(3))
A = phi(Q) @ phi(K).T # ma trận L x L
out1 = (A @ V) / A.sum(axis=1, keepdims=True)
KV = phi(K).T @ V # chỉ d x d, không phụ thuộc L
z = phi(K).sum(axis=0) # vectơ d chiều
out2 = (phi(Q) @ KV) / (phi(Q) @ z)[:, None]
print("same output:", np.allclose(out1, out2))
print(f"multiplications: {2 * L * L * d:,} vs {2 * L * d * d:,}")

Lần chạy đầu cần tải Python, khoảng 10 MB.

Đọc kết quả:

  • Ở L=512L = 512, hai cách bằng nhau (131 328 ô), vì cửa sổ đã bao cả chuỗi.
  • Mỗi lần LL gấp 4, số ô đầy đủ gấp khoảng 16, còn cửa sổ gấp khoảng 4 khi LL đã lớn hơn nhiều so với ww (từ 512 lên 2 048 thì vẫn gấp 7.0, vì ở 512 cửa sổ đã bao cả chuỗi). Ở L=131 072L = 131\,072 (128K), attention đầy đủ cần khoảng 8.6 tỉ ô, cửa sổ chỉ khoảng 67 triệu, ít hơn 128.3 lần. Tỉ lệ này xấp xỉ L/(2w)=131 072/1 024=128L / (2w) = 131\,072 / 1\,024 = 128, vì đầy đủ có khoảng L2/2L^2/2 ô còn cửa sổ có khoảng LwL w.
  • Cột GB đi từ 0.001 ở 512 token tới 34.360 ở 128K token, cho một đầu ở một lớp. Đây là lý do FlashAttention không bao giờ lưu cả ma trận.
  • same output: True: hai thứ tự nhân cho cùng kết quả, đúng như phép biến đổi ở mục Lý thuyết. Số phép nhân là 32 000 000 khi đi qua ma trận L×LL \times L, và 512 000 khi tính ∑jϕ(kj)vj⊤\sum_j \phi(\mathbf{k}_j)\mathbf{v}_j^\top trước, ít hơn L/d=62.5L/d = 62.5 lần.

Hãy thử đổi w = 512 thành w = 4096 trong ô thứ hai: ở 2 048 token hai cột bằng nhau, còn ở 128K token tỉ lệ chỉ còn 16.3 lần. Cửa sổ càng rộng, mô hình nhìn được càng xa trong một lớp, và phần tiết kiệm càng nhỏ.

Câu 1/5 Một mô hình dùng ALiBi với 8 đầu. Truy vấn ở vị trí 100 xét khóa ở vị trí 20. Bias cộng vào điểm của cặp này ở đầu 1 (m1=1/2m_1 = 1/2) và ở đầu 8 (m8=1/256m_8 = 1/256) là bao nhiêu, và điều đó nói gì?
Câu 2/5 Một decoder có 3 lớp, lớp nào cũng dùng attention cửa sổ trượt w=4w = 4: mỗi token nhìn chính nó và 3 token ngay trước. Ở đầu ra lớp thứ 3, token ở vị trí 20 có thể mang thông tin của token xa nhất ở vị trí nào?
Câu 3/5 So với cách cài attention thông thường, FlashAttention thay đổi điều gì?
Câu 4/5 Bạn cần gán nhãn từng token (tên người, địa danh) cho 50 000 câu tin tức, máy chỉ có CPU. Họ Transformer nào là điểm khởi đầu hợp nhất?
Câu 5/5 Một Transformer Post-LN 48 lớp có loss thành NaN sau vài trăm bước khi bạn bỏ warmup. Theo Xiong và cộng sự (2020), thay đổi kiến trúc nào nhắm thẳng vào nguyên nhân?

Ý chính:

  • Cùng khối Transformer, ba họ khác nhau ở mặt nạ và mục tiêu: encoder-only (hai chiều, MLM, như BERT) để hiểu; decoder-only (nhân quả, đoán token kế, như GPT) để sinh; encoder–decoder (khử nhiễu: làm hỏng đoạn của T5, điền đoạn của BART) để biến văn bản này thành văn bản khác.
  • Vị trí đưa vào embedding (sin/cos, bảng học được, giới hạn 512 của BERT) hoặc vào điểm attention ở mọi lớp (bias tương đối của T5, RoPE, ALiBi). ALiBi cộng −mh(i−j)-m_h(i - j) với độ dốc cố định 12,…,1256\tfrac{1}{2}, \dots, \tfrac{1}{256} cho 8 đầu, và huấn luyện ở LL vẫn giữ perplexity tốt ở chuỗi dài hơn nhiều, dù chưa chắc đã dùng được thông tin ở xa.
  • Pre-LN, H←H+F(LN(H))\mathbf{H} \leftarrow \mathbf{H} + F(\mathrm{LN}(\mathbf{H})), giữ kết nối tắt là phép cộng thuần, gradient lúc khởi tạo đều giữa các lớp, nên mô hình sâu học ổn định và không cần warmup như Post-LN.
  • Attention tốn L2L^2 ô cho mỗi đầu, mỗi lớp. Cửa sổ trượt giảm còn LwL w nhưng không nhìn thẳng ra xa; attention tuyến tính đổi thứ tự nhân, còn Ld2L d^2, nhưng là mô hình khác; FlashAttention giữ đúng kết quả, chỉ bớt đọc ghi bộ nhớ; MQA/GQA thu nhỏ KV cache khi sinh.

Đọc thêm:

Nội dung theo giấy phép CC BY-NC-SA 4.0. Mã nguồn theo giấy phép MIT.Giới thiệu