Prefill vs decode: quá trình sinh văn bản diễn ra thế nào
Nhấn Enter và quá trình sinh văn bản chạy trong hai giai đoạn có vật lý hoàn toàn khác nhau. Prefill xử lý toàn bộ prompt của bạn trong một lượt song song — nặng về tính toán, nhanh trên mỗi token, nhưng bạn phải chờ hết tất cả trước khi từ đầu tiên xuất hiện. Decode sau đó nhả câu trả lời từng token một — mỗi bước đơn lẻ đọc lại gần như toàn bộ trọng số mô hình từ bộ nhớ chỉ để tạo một token. "Token đầu chậm, sau đó mới streaming" không phải đặc quirk của máy bạn; đó chính là hai giai đoạn này.
Giai đoạn 1: prefill — prompt
- Toàn bộ token của prompt đi qua mô hình đồng thời: những phép nhân ma trận song song khổng lồ, đúng thứ GPU được chế tạo để làm. Mức sử dụng cao; token mỗi giây cao.
- Bạn cảm nhận nó dưới dạng thời gian đến token đầu tiên (TTFT): khoảng lặng sau khi bạn nhấn Enter.
- Lượt đi này cũng lấp đầy KV cache bằng vector khóa/giá trị của mọi token prompt — tấm nháp mà decode sẽ đọc.
- Prompt dài hơn, TTFT dài lên tuyến tính: system prompt khổng lồ, tài liệu RAG và lịch sử chat đều phải prefill mỗi lần trừ khi engine của bạn cache tiền tố.
Giai đoạn 2: decode — câu trả lời
- Một token mỗi lượt forward. Lượt đó cần mọi trọng số; tạo một token duy nhất nghĩa là streaming toàn bộ mô hình ra khỏi VRAM.
- Trần giới hạn là băng thông bộ nhớ, không phải sức tính: đại khái decode tok/s ≈ băng thông hiệu dụng ÷ số byte trọng số. 8 GB trọng số trên card có ~400 GB/s hiệu dụng ≈ trần 50 tok/s.
- Đây là lý do lượng hóa giúp decode nhanh hơn: ít bit hơn mỗi trọng số nghĩa là ít byte phải stream hơn mỗi bước (xem tốc độ).
Vì sao bất đối xứng: prefill xử lý hàng nghìn token cho mỗi lượt đọc trọng số (ma trận khổng lồ, cường độ số học cao — bị giới hạn bởi tính toán), decode xử lý đúng một token (mọi byte trọng số được lấy về chỉ vì một token — bị giới hạn bởi băng thông). Cùng trọng số, hai nút thắt đối nghịch.
total_time ≈ prompt_tokens / prefill_tok_s + output_tokens / decode_tok_s
Ví dụ minh họa
prompt: 1,000 tokens prefill 2,000 tok/s → 0.5 s (TTFT)
output: 300 tokens decode 40 tok/s → 7.5 s
total ≈ 8.0 s — 94% thời gian đồng hồ là decode
- Nửa giây đầu tiên xử lý 1,000 token; 7.5 giây tiếp theo tạo 300 token. Cùng mô hình, cùng GPU, chênh 50 lần — khác giai đoạn, không phải khác mức nỗ lực.
- 40 tok/s tương đương khoảng 160 ký tự tiếng Anh mỗi giây (≈4 ký tự/token): nhanh hơn bất kỳ ai đánh máy, nên đọc dạng streaming rất thoải mái (token).
- Cùng 40 tok/s đó với tiếng Hàn chỉ khoảng 40–60 ký tự/giây: tỉ lệ token y hệt, ít văn bản hơn mỗi token.
- Độ dài đầu ra là chiếc đồng hồ phía decode: max_tokens chặn việc sinh kéo dài bao lâu, stop sequence kết thúc sớm. Cả hai thay đổi thời gian đồng hồ tuyến tính (tham số engine).
- Streaming thay đổi cảm nhận, không thay đổi vật lý: bạn xem decode diễn ra thay vì chờ đợi nó, với cùng 40 tok/s.
Ví dụ minh họa 2: trường hợp RAG
prompt: 12,000 tokens (system + 20 đoạn truy xuất) prefill 1,500 tok/s → 8.0 s
output: 500 tokens decode 40 tok/s → 12.5 s
total ≈ 20.5 s — riêng TTFT đã chiếm 39% thời gian chờ
Đây là hình dáng của các thiết lập cục bản nặng về truy xuất: prompt béo nghĩa là mô hình "suy nghĩ" mất vài giây trước khi gõ. Thuốc chữa, theo thứ tự hiệu quả:
- Truy xuất ít hơn — top 3–5 đoạn thường thắng top 20 (RAG).
- Giữ system prompt ổn định để engine tái dùng được tiền tố đã cache.
- Cắt gọn lịch sử: các lượt cũ có thể được tóm tắt thay vì phát lại nguyên văn.
KV cache: tấm nháp ngày càng lớn
Không có cache, việc sinh token thứ N sẽ phải tính lại attention trên cả N−1 token trước từ đầu — mỗi bước làm lại công việc y hệt, tổng chi phí theo bình phương độ dài. Thay vào đó:
- Prefill tính khóa và giá trị cho mọi token prompt và lưu một lần.
- Mỗi bước decode chỉ tính query/key/value của token mới và nối K/V của nó vào cache.
- Attention cho token mới là query của nó nhân vô hướng với mọi khóa đã lưu.
- Một token được decode, theo thứ tự: embed nó, chạy các tầng đọc cache, chiếu ra từ vựng, lấy mẫu — nối K/V của nó, lặp lại.
Giờ mỗi bước decode tuyến tính theo độ dài ngữ cảnh (đọc cache) thay vì bình phương (tính lại tất cả). Cái giá là chính cái cache, thứ lớn dần theo mỗi token:
KV bytes ≈ 2 × layers × kv_heads × head_dim × bytes_per_value × context
mô hình hạng 8B (32 tầng, 8 đầu KV, đầu dim 128, FP16):
2 × 32 × 8 × 128 × 2 B ≈ 128 KiB mỗi token
→ ~2.6 GB ở ngữ cảnh 20k, ~17 GB ở ngữ cảnh 128k
Cache ấy cạnh tranh với trọng số giành VRAM — hội thoại ngữ-cảnh-dài làm sập những máy chạy ổn cùng mô hình đó với prompt ngắn (KV cache, toán VRAM).
Xử lý theo lô thay đổi bài toán kinh tế
Đọc 8 GB trọng số để nhả một token là lãng phí; cùng một lượt đọc đó có thể phục vụ nhiều chuỗi cùng lúc. Gộp lô 8 request và một luồng đọc trọng số tạo 8 token — thông lượng leo gần tuyến tính cho đến khi sức tính bão hòa. Đó là toàn bộ mẹo đứng sau việc phục vụ (API phục vụ, đồng thời). Một mình trên laptop bạn trả đủ giá mỗi token; các server phân bổ giá đó qua nhiều người dùng đồng thời.
Đọc các đồng hồ
| Đồng hồ | Giai đoạn | Yếu tố chính |
|---|---|---|
| TTFT | prefill | độ dài prompt, sức tính, số lần trúng cache tiền tố |
| Prompt eval tok/s | prefill | sức tính, kích thước lô |
| Generation tok/s | decode | băng thông bộ nhớ, số byte trọng số, lô |
| Throughput (req/s) | cả hai | kích thước lô, tái dùng cache tiền tố, khe song song |
| Bộ nhớ ngữ cảnh | cả hai | byte KV mỗi token, lượng hóa cache |
Đo trước khi tinh chỉnh: mọi engine nghiêm túc đều ghi log tốc độ đánh-giá-prompt và sinh-văn-bản theo từng request. Một phút đọc log cho biết giai đoạn nào ăn mất độ trễ của bạn; đoán mò thì không.
Tinh chỉnh bám theo giai đoạn: prompt ngắn hơn và cache tiền tố chữa TTFT; lượng hóa và trọng số nhỏ hơn chữa decode (tham số engine, khắc phục sự cố).