Bài 1 trên 4 · Attention và ngữ cảnh dài
Tóm tắt
- Attention là cách mô hình giúp mỗi từ nhìn lại các từ trước nó và quyết định từ nào quan trọng với mình.
- Mỗi token phải so với mọi token trước nó, nên văn bản dài gấp đôi thì số phép so sánh gấp khoảng bốn lần. Đó là chi phí bậc hai.
- Khi mô hình viết tiếp, mỗi token mới lại phải đọc toàn bộ những gì đã lưu, nên càng về sau mỗi token càng tốn.
Bạn đọc tới chữ "nó" trong một câu chuyện và lướt mắt về trước để xem nó là ai. Việc này rất nhanh vì bạn chỉ nhìn vài chỗ. Mô hình ngôn ngữ làm điều tương tự bằng attention, nhưng ở dạng cơ bản, nó nhìn lại tất cả mọi thứ, mỗi lần. Bài này cho bạn thấy cả cơ chế lẫn cái giá đó.
Tự thử: một từ nhìn lại những từ nào?
Bấm vào một từ để xem nó chia sự chú ý thế nào cho các từ đứng trước.
Mỗi thanh là phần attention mà từ đang chọn dành cho một từ phía trước. Đổi cuối câu để thấy attention chuyển chỗ.
Thử theo thứ tự:
- Chọn đói. Khoảng 78% attention rơi vào "Con mèo", vì con đói là con đang đuổi.
- Đổi cuối câu thành …vì nó sợ. Khoảng 85% chuyển sang "con chuột". Không từ nào phía trước thay đổi, chỉ câu hỏi thay đổi.
- Chọn nó. Attention chia đều 45% và 45% cho hai con vật, vì chưa có gì phân biệt được.
Bản demo dùng vector do chúng tôi tự đặt để bạn thấy rõ cơ chế. Mô hình thật học các vector này từ dữ liệu.
Attention làm những bước nào?
Attention so khớp điều một token đang tìm với nhãn của mọi token trước đó, đổi điểm số thành tỷ lệ, rồi trộn nội dung theo tỷ lệ ấy. Hãy nghĩ tới một thủ thư: bạn nói điều cần tìm, thủ thư dò nhãn trên từng cuốn sách, rồi đưa bạn nội dung của những cuốn khớp nhất.
| Phần | Vai trò | Ví dụ thủ thư |
|---|---|---|
| Query | Token này đang tìm gì? | Câu bạn hỏi thủ thư |
| Key | Token kia nói về gì, để so khớp | Nhãn trên gáy sách |
| Value | Nội dung sẽ được lấy về | Ruột cuốn sách |
- QueryĐiều đang tìm
- KeySo với từng token
- SoftmaxĐiểm thành tỷ lệ
- ValueTrộn theo tỷ lệ
Phép so khớp là tích vô hướng của hai vector. Trong bài này chúng tôi gọi nó là một phép so sánh, và đó là đơn vị chi phí xuyên suốt loạt bài. Paper gốc về transformer gọi cách làm này là scaled dot-product attention.
Vì sao gấp đôi độ dài thì gấp bốn công việc?
Vì mỗi token phải so với mọi token trước nó. Token thứ 8 so 7 lần, token thứ 1.000 so 999 lần. Cộng lại, n token cần n(n-1)/2 phép so sánh, nên chi phí tăng theo bình phương độ dài. Người ta gọi đó là chi phí bậc hai.
Mỗi ô dưới đây là một phép so sánh. Bấm Gấp đôi và nhìn tam giác lớn lên.
Mỗi hàng là một token, mỗi ô là một phép so sánh với một token trước nó. Hàng màu cam là token mới nhất, phần nhạt là chỗ tam giác sẽ lớn tới ở 64 token.
Tam giác 64 token còn vẽ được. Bảng dưới là phần không vẽ được. Cột thời gian giả định máy làm được 1 tỷ phép so sánh mỗi giây, cho một lớp attention và một đầu attention. Đây là con số giả định để dễ hình dung, không phải tốc độ đo được trên phần cứng cụ thể.
| Độ dài ngữ cảnh | Số phép so sánh | Thời gian theo giả định |
|---|---|---|
| 1 nghìn token | khoảng 500 nghìn | dưới 1 mili giây |
| 64 nghìn token | khoảng 2 tỷ | khoảng 2 giây |
| 256 nghìn token | khoảng 33 tỷ | khoảng 33 giây |
| 1 triệu token | khoảng 500 tỷ | khoảng 8 phút |
| 10 triệu token | khoảng 50 nghìn tỷ | khoảng 14 giờ |
| 1 tỷ token | khoảng 500 triệu tỷ | khoảng 16 năm |
Đọc vào và viết ra khác nhau thế nào?
Hai giai đoạn này tốn theo hai cách khác nhau. Prefill là lúc mô hình đọc phần văn bản đưa vào: tổng chi phí tăng bậc hai. Decode là lúc mô hình viết tiếp từng token.
Để khỏi phải tính lại, mô hình lưu key và value của mọi token cũ vào KV cache. Nhưng mỗi token mới vẫn phải đọc toàn bộ cache đó. Vì vậy, paper về ZHA mô tả chi phí decode là băng thông bộ nhớ và dung lượng lưu trữ tăng tuyến tính theo độ dài chuỗi, cho mỗi token mới.
Nói cách khác, token thứ 1 triệu không chỉ tốn hơn token thứ 1 nghìn về phép tính. Nó còn phải kéo cả một triệu cặp key và value qua bộ nhớ.
Đã kiểm tra bằng cách chạy thật
Ngày 3/11/2026, chúng tôi chạy đoạn mã dưới đây trên Node 26.11.1 và Apple M4 Pro, không thư viện ngoài. Đây là vòng lặp thuần trên CPU, chỉ để thấy hình dạng tăng trưởng.
// attention.ts: node attention.ts
const DIM = 32;
function randomKeys(n: number): Float32Array {
const keys = new Float32Array(n * DIM);
for (let i = 0; i < keys.length; i++) keys[i] = Math.random() - 0.5;
return keys;
}
// Mỗi token so với mọi token trước nó.
function readAll(keys: Float32Array, n: number): number {
let comparisons = 0;
let sink = 0;
for (let token = 1; token < n; token++) {
for (let earlier = 0; earlier < token; earlier++) {
let score = 0;
for (let d = 0; d < DIM; d++) score += keys[token * DIM + d]! * keys[earlier * DIM + d]!;
sink += score;
comparisons++;
}
}
if (Number.isNaN(sink)) throw new Error("unreachable");
return comparisons;
}
let previous = 0;
for (const n of [2000, 4000, 8000, 16000]) {
const keys = randomKeys(n);
const start = performance.now();
const comparisons = readAll(keys, n);
const ms = performance.now() - start;
const growth = previous ? `${(ms / previous).toFixed(1)}x` : "";
console.log(`${n} tokens: ${comparisons} comparisons, ${ms.toFixed(0)} ms ${growth}`);
previous = ms;
}Kết quả, mỗi dòng gấp đôi số token:
2000 tokens: 1999000 comparisons, 53 ms
4000 tokens: 7998000 comparisons, 189 ms 3.6x
8000 tokens: 31996000 comparisons, 746 ms 4.0x
16000 tokens: 127992000 comparisons, 3000 ms 4.0xMáy của bạn sẽ cho số mili giây khác, nhưng tỷ lệ vẫn quanh 4 lần.
Điều này ảnh hưởng gì tới việc bạn dùng AI?
Mô hình đầy đủ càng đọc dài càng tốn cho nhà cung cấp, nên ngữ cảnh dài thường đi kèm giới hạn hoặc chi phí. Vài thói quen sau đều xuất phát từ cơ chế này.
| Tình huống | Nên làm | Vì sao |
|---|---|---|
| Đưa AI cả kho mã để sửa một lỗi | Chỉ đưa những tệp liên quan, hoặc để nó tự tìm | Mọi token đưa vào đều được đọc lại ở mỗi bước |
| Hội thoại rất dài đã đổi chủ đề | Mở phiên mới kèm một đoạn tóm tắt | Phần cũ không còn dùng vẫn nằm trong cache |
| Gọi API nhiều lần với cùng phần mở đầu dài | Dùng prompt caching nếu nhà cung cấp hỗ trợ | Không phải đọc lại phần mở đầu giống hệt |
| Câu hỏi ngắn, không cần tài liệu | Giữ ngữ cảnh ngắn | Đoạn ngắn rẻ hơn rất nhiều vì chi phí không tăng tuyến tính |
Những hiểu lầm hay gặp
- "Attention nghĩa là mô hình chỉ đọc phần quan trọng." Với attention đầy đủ thì ngược lại: nó tính điểm cho tất cả, rồi mới gán trọng số lớn cho phần quan trọng.
- "KV cache loại bỏ chi phí." Cache tránh tính lại key và value, nhưng token mới vẫn phải đọc cả cache.
- "Máy nhanh gấp đôi thì đọc được dài gấp đôi." Máy nhanh gấp đôi chỉ cho thêm khoảng 41% độ dài, vì chi phí tăng theo bình phương.
- "Một phép so sánh của mô hình thật giống trong demo." Mô hình thật có nhiều lớp và nhiều đầu attention, nên số thật lớn hơn. Hình dạng tăng trưởng mới là điều cần nhớ.
Bài tiếp theo sẽ xem các cách người ta đã thử để thoát khỏi chi phí này, và vì sao phần lớn chỉ dời nó sang chỗ khác.
Kiểm tra lại
Chọn một đáp án để xem vì sao đúng hay sai.
Một đoạn có 1.000 token cần khoảng 500 nghìn phép so sánh. Đoạn 2.000 token cần khoảng bao nhiêu?
Xem đáp án
2 triệu. Số phép so sánh là n(n-1)/2, tăng theo bình phương độ dài. Gấp đôi độ dài thì gấp bốn lần, nên từ 500 nghìn lên khoảng 2 triệu.
Trong attention, phần nào cho biết một token mang nội dung gì để token khác lấy về?
Xem đáp án
Value. Query là điều một token đang tìm. Key là nhãn để so khớp với query. Value là nội dung được lấy về, theo tỷ lệ mà phép so khớp quyết định.
Khi mô hình viết tiếp từng token, KV cache giúp được gì và không giúp được gì?
Xem đáp án
Giúp không phải tính lại key và value cũ, nhưng token mới vẫn phải đọc toàn bộ cache. Cache lưu key và value đã tính, nên không phải tính lại chúng. Nhưng với attention đầy đủ, mỗi token mới vẫn so với mọi key trong cache, nên mỗi bước đều dài thêm và cache cũng lớn dần.
Vì sao cùng một từ có thể nhìn về những chỗ khác nhau trong hai câu?
Xem đáp án
Vì query phụ thuộc vào từ và ngữ cảnh của nó, nên mỗi lần so khớp cho kết quả khác. Query được tính từ trạng thái hiện tại của token. Trong demo, đổi đói thành sợ làm phần lớn attention chuyển từ con mèo sang con chuột.