2.4 Chunked Prefill và lịch trình thống nhất theo token budget
Chunked Prefill chi tiết: cắt Prefill của prompt dài thành từng chunk để không còn làm Decode bị nghẽn; lịch trình thống nhất theo token budget của vLLM V1 xóa ranh giới Prefill/Decode thế nào
Phần Continuous Batching còn một đầu mối: khi Prefill của một prompt rất dài chen vào một bước tính, mọi request Decode trong cùng batch bị kéo chậm, và người dùng cảm thấy rõ một cú “khựng.” Chunked Prefill xử lý việc đó—nó cắt Prefill dài thành từng mảnh nhỏ và luồn vào khoảng trống giữa các Decode để không bên nào làm kẹt bên kia. vLLM V1 đi xa hơn: một lịch trình thống nhất theo token budget xóa hẳn đường phân Prefill/Decode. Phần này nói cả hai.
⏰ Ghi chú thời điểm: ý tưởng chia chunk theo bài SARATHI; logic lịch trình, tham số và gợi ý tinh chỉnh của vLLM dựa trên v0.32.0 (phát hành 2026-10-18).
📑 Mục lục
- 1. Prefill làm Decode bị nghẽn thế nào
- 2. Chunked Prefill: cắt Prefill dài thành từng chunk
- 3. Token Budget: một đơn vị tiền tệ cho lịch trình
- 4. Lịch trình thống nhất của vLLM V1
- 5. Đọc mã nguồn: Token Budget cắt Chunk thế nào
- 6. Tham số then chốt và tinh chỉnh
- 7. Đánh đổi và khi nào có ích
- Tóm tắt
- Danh sách tự kiểm
- Tài liệu tham khảo
1. Prefill làm Decode bị nghẽn thế nào
Trước hết là cơ chế nhiễu. Hai điểm lấy từ chương trước:
- Prefill là compute bound: một bước xử lý song song mọi token trong prompt; prompt càng dài, matmul của bước đó càng nặng và càng lâu.
- Decode là memory bound: mỗi bước chỉ xử lý 1 token nên bản thân nó nhanh; tốc độ token người dùng cảm nhận (TPOT) phụ thuộc vào các bước đó tiến đều.
Vấn đề: Continuous Batching dựng lại batch mỗi bước. Nếu một bước nhét Prefill của prompt 4000 token cùng một loạt request Decode, tường thời gian của bước đó bị Prefill khổng lồ kéo giãn—mọi request Decode trong batch phải chờ Prefill xong rồi token mới ra cùng lúc.
Ví von: các request Decode là gói nhỏ chạy đều trên băng chuyền, vài giây một gói; đột nhiên một container hàng khổng lồ (Prefill dài) cũng phải qua cùng cửa kiểm, cả dây chuyền kẹt, mọi gói nhỏ phía sau phải chờ. Về phía người dùng trông như: token chảy đều, khựng một cái, rồi lại chảy—một đỉnh TPOT (jitter P99).
📌 Điểm then chốt: đây không phải chuyện “có batch hay không”; mà là “một bước nhét quá nhiều việc compute bound.” Nếu Prefill dài là thủ phạm, cắt nó thành mảnh nhỏ rải qua nhiều bước sẽ không còn khóa cả dây chuyền trong một lần.
2. Chunked Prefill: cắt Prefill dài thành từng chunk
Chunked Prefill thẳng thắn: một prompt dài không bắt buộc Prefill xong trong một bước; cắt thành các Chunk và rải qua nhiều bước. Bài SARATHI cắt chunk độ dài bằng nhau; vLLM không cố định độ dài chunk trước—mỗi chunk lấy hết token budget còn lại trong bước đó (xem §5).
Ví dụ, prompt 4000 token cắt theo 512 token/chunk thì được 7 chunk đủ và một đuôi 416 token, tổng 8 chunk. Mỗi bước chỉ xử lý một chunk; phần tính còn lại để các request Decode trong cùng batch vẫn sinh token bình thường. Vậy:
- Tính toán mỗi bước nằm trong khoảng dự đoán được, không bị một Prefill khổng lồ thổi phồng đột ngột.
- Request Decode có thể tiếp tục tiến đều trong lúc Prefill dài vẫn đang chạy, và các đỉnh TPOT được làm phẳng.
Hình dưới đặt cảnh §1 và cảnh đã chia chunk lên cùng một trục thời gian. Mỗi ô là một bước: xanh là 8 request Decode mỗi cái phát 1 token; vàng là Prefill của prompt dài; khung đen đánh dấu các bước có Prefill; đường đứt cam là lúc token đầu của prompt dài xuất hiện. Độ rộng bước dùng mô hình rút gọn “overhead cố định + tỉ lệ với số token Prefill”, chỉ để thấy độ dài tương đối.
Quay lại băng chuyền: giờ container khổng lồ đó được bẻ thành các thùng chuẩn; mỗi lần kiểm chỉ cho một thùng, các gói nhỏ vẫn luồn qua giữa chừng. Container xong chậm hơn, nhưng dây chuyền vẫn chạy đều và không bị kẹt hẳn.
🔑 Ý chính: Chunked Prefill đổi “rải Prefill lớn qua nhiều bước” lấy “Decode đều”—nó làm phẳng một đỉnh tính thành vài đỉnh nhỏ kiểm soát được và ổn định độ trễ đuôi TPOT.
⚠️ Lưu ý: chia chunk làm TTFT của prompt dài này hơi lớn hơn (Prefill trải nhiều bước nên token đầu đến muộn hơn một chút). Đó là đánh đổi có chủ đích—nhường một ít TTFT từng request để đổi lấy TPOT Decode ổn định trên cả batch. Với dịch vụ quan tâm đến luồng token mượt, đánh đổi này thường đáng.
3. Token Budget: một đơn vị tiền tệ cho lịch trình
Để Chunked Prefill hạ cánh, scheduler cần một thước đo xem mỗi bước nhét bao nhiêu việc. Thước đó là Token Budget.
Nhìn ra: dù Prefill hay Decode, với GPU vẫn là “forward compute trên một số token nào đó.”
- Một request Decode đóng góp 1 token trong bước này (token tiếp theo cần sinh).
- Một request Prefill (hoặc một Chunk của nó) đóng góp N token trong bước này (độ dài chunk đó).
Vậy scheduler đặt trần token budget mỗi bước (ví dụ “tối đa 2048 token bước này”), rồi xếp request: trước hết giữ các request Decode đang sinh (mỗi cái tốn 1 token budget), rồi dành budget còn lại cho các Prefill chunk. Hết budget thì bước xuất phát.
💡 Mẹo: với token budget làm một thước, “Chunk nên lớn bao nhiêu,” “một bước nhét được bao nhiêu Decode,” và “Prefill dài chia sẻ bước với Decode thế nào” đều gộp thành cùng một bài xếp đồ—lấp đầy GPU trong ngân sách, đồng thời không để Decode bị nghẽn.
4. Lịch trình thống nhất của vLLM V1
Ở vLLM sớm (V0), Prefill và Decode là hai pha xử lý khác nhau; logic lịch trình phải xử lý từng bên, mã phức tạp và để hở khi chuyển pha.
Engine V1 của vLLM gọn lại sạch sẽ: bỏ hẳn phân tách Prefill/Decode. Với scheduler V1 không còn “request Prefill” đối “request Decode,” chỉ còn “request này xử lý bao nhiêu token trong bước này.” Một quyết định lịch trình là một dict đơn giản:
# Each V1 scheduler step is essentially this mapping
{
"req_A": 1, # Decoding: process 1 new token
"req_B": 1, # Decoding: process 1 new token
"req_C": 512, # One Prefill chunk of a long prompt
"req_D": 128, # Short prompt: Prefill finishes in one step
}
# Constraint: sum of all values ≤ token_budget
🔑 Ý chính: V1 dùng một biểu diễn {request_id: num_tokens} để phủ cùng lúc Chunked Prefill, Prefix Cache và Speculative Decoding. Tất cả chỉ là các giá trị khác nhau của “request này xử lý bao nhiêu token trong bước này”—một Prefill chunk là số lớn hơn, Decode là 1, kiểm chứng suy đoán là vài token ứng viên. Một khung lịch trình thống nhất mọi trường hợp.
Đó cũng là lý do V1 có thể biến Chunked Prefill thành hành vi mặc định: lịch trình vốn đã làm việc theo token và không phân pha, nên một prompt dài tự nhiên bị budget cắt thành chunk, không cần logic đặc biệt.
5. Đọc mã nguồn: Token Budget cắt Chunk thế nào
Giờ đem ý đó vào mã nguồn vLLM V1. Lịch trình nằm ở Scheduler.schedule() trong vllm/v1/core/sched/scheduler.py. Phần này không nhắc lại cấu trúc hai hàng đợi ở §2.2; chỉ tập trung vào “token budget biến Prefill dài thành Chunk thế nào” (mã vẫn đổi; lấy phiên bản bạn cài làm chuẩn).
Điểm then chốt: khi scheduler quyết “request này xử lý bao nhiêu token trong bước này,” nó cắt theo budget còn lại. Prompt dài không xong trong một bước chỉ lấy phần budget cho phép—chia chunk không phải một hàm “chunker” riêng; nó là kết quả tự nhiên của việc cắt theo budget.
# Pseudocode: reconstructs "budget truncation is chunking" inside schedule() (not literal source)
def schedule(self):
token_budget = self.max_num_batched_tokens
scheduled = {}
# Running requests: Decode needs 1 token each; may also be a Prefill continuation from last step
for req in self.running:
if token_budget == 0:
break
# How many tokens this request still needs (unfinished Prefill, or 1 for Decode)
need = req.num_tokens_with_spec - req.num_computed_tokens
# Key: truncated by remaining budget → a long Prefill is "cut" into a chunk here
num_tokens = min(need, token_budget)
blocks = self.kv_cache_manager.allocate_slots(req, num_tokens)
if blocks is None:
... # OOM: preempt and retry; details in §2.2.6.2
continue
scheduled[req.id] = num_tokens # {request_id: num_tokens}
token_budget -= num_tokens
# Budget left: pull new requests from waiting (also truncated into a first chunk)
# In source, if this step already preempted, skip this block and admit no new requests
while self.waiting and token_budget > 0 and self._can_admit():
req = self.waiting.peek_front()
# Check prefix cache first: hit tokens need no more compute (see §2.3)
_, num_cached = self.kv_cache_manager.get_computed_blocks(req)
need = req.num_tokens - num_cached
num_tokens = min(need, token_budget) # a long prompt's first chunk is truncated here too
if self.kv_cache_manager.allocate_slots(req, num_tokens) is None:
break # not enough memory: stop admitting new requests
self.waiting.pop_front()
self.running.append(req)
scheduled[req.id] = num_tokens
token_budget -= num_tokens
return SchedulerOutput(scheduled)
Hình dưới đi vòng lặp đó với số cụ thể. Budget 2048: 8 request Decode lấy mỗi cái 1 (xanh); request dài R vẫn trong running còn cần 1440 token Prefill và lấy hết (vàng); 600 còn lại dành cho request chờ W1 với prompt 900 token, bị cắt thành Chunk đầu (xanh lá); W2 hết budget và ở lại hàng đợi (xám nét đứt).
Hai điểm đáng nhớ:
min(need, token_budget)là bản chất của chia chunk: prompt 4000 token mà chỉ còn 512 budget thì Prefill 512 bước này và để 3488 cho sau—tự động thành một Chunk, không cần “chunker” riêng.- Phần tiếp được theo dõi bằng
num_computed_tokens: “request này đã tính tới đâu” được giữ lại; bước sau nối từ đó cho đến khinum_computed_tokensđuổi kịpnum_prompt_tokens(Prefill xong), rồi tự chuyển sang Decode +1 mỗi bước. Đó là “không phân pha Prefill/Decode” của §2.2 nhìn từ phía dữ liệu.
📌 Điểm then chốt: lịch trình V1 trong một câu—đẩy num_computed_tokens về phía num_tokens_with_spec; mỗi bước tiến min(nhu cầu còn lại, budget còn lại). Chunked Prefill, Decode và kiểm chứng suy đoán chỉ là biểu thức đó nhận các giá trị khác nhau trên các request khác nhau.
6. Tham số then chốt và tinh chỉnh
Trong vLLM, ba tham số quan trọng nhất với phần này. Khi điều kiện cho phép, V1 bật Chunked Prefill mặc định và ưu tiên Decode khi xếp batch, nên thường bạn không phải bật tắt tay:
| ⚙️ Tham số | Ý nghĩa | Hướng tinh chỉnh |
|---|---|---|
max_num_batched_tokens | Số token tối đa một bước xử lý được (Token Budget) | Lớn hơn → throughput↑, thời gian mỗi bước↑ (TPOT có thể↑); nhỏ hơn → TPOT ổn định hơn, throughput có thể↓ |
max_num_seqs | Số request tối đa trong một bước (trần batch) | Bị giới hạn bởi bộ nhớ / khối KV; cùng với tham số trên quyết định kích thước batch thực |
long_prefill_token_threshold | Prompt dài hơn ngưỡng này được coi là “Prefill dài”; số token xử lý trong một bước bị trần ở giá trị này | Mặc định 0 (không trần); đặt giá trị khi nhiều prompt dài chung batch để một request dài không nuốt hết budget bước. Không có hiệu lực khi nó là request duy nhất trong batch |
Tinh chỉnh xoay quanh đánh đổi của max_num_batched_tokens:
- Ưu tiên throughput: tăng budget, nhét nhiều token hơn mỗi bước, lấp đầy GPU hơn, tăng throughput hệ thống; nhưng Prefill chiếm tỉ lệ lớn hơn và độ ổn định TPOT của Decode giảm.
- Ưu tiên ổn định độ trễ: hạ budget, giữ tính toán mỗi bước kiểm soát được, Decode mượt hơn và đuôi TPOT thấp hơn; nhưng mỗi bước làm ít hơn và throughput đỉnh có thể chịu thiệt.
Tài liệu tinh chỉnh chính thức của vLLM đưa một thang tham chiếu: giá trị nhỏ hơn (ví dụ 2048) cho ITL tốt hơn (Inter-Token Latency, khoảng cách giữa hai token đầu ra liền kề, về cơ bản đồng nghĩa với TPOT) vì Prefill ít làm Decode bị nghẽn hơn; giá trị lớn hơn rút ngắn TTFT vì một bước Prefill được nhiều token hơn; với throughput, đặt 8192 trở lên, nhất là mô hình nhỏ trên GPU nhiều bộ nhớ.
💡 Mẹo: không có một giá trị tốt nhất. Việc đúng là load-test dưới tải mục tiêu của bạn (hỗn hợp độ dài prompt thực và concurrency), theo dõi TTFT, TPOT P95/P99 và throughput, rồi tìm throughput cao nhất vẫn đạt SLO. Tinh chỉnh cụ thể kèm ví dụ nằm ở chương 3, “Tinh chỉnh các thiết lập then chốt của vLLM.”
7. Đánh đổi và khi nào có ích
✅ Chỗ Chunked Prefill rõ ràng có ích:
- Độ dài prompt lệch nhiều, có prompt rất dài lẫn vào (QA tài liệu dài, RAG ngữ cảnh dài)
- Dịch vụ online quan tâm đến luồng token mượt và muốn đè đuôi TPOT (ví dụ chat trực tiếp)
- Tải online concurrent cao trộn Prefill và Decode
❌ Chỗ lợi ích hạn chế:
- Suy luận batch offline thuần chỉ quan tâm tổng throughput, không quan tâm jitter độ trễ từng request—overhead lịch trình từ chia chunk có thể không đáng
- Prompt đều ngắn, nên Prefill dài vốn không độc chiếm một bước
📌 Điểm then chốt: Chunked Prefill nhắm vào nhiễu khi Prefill và Decode chạy chung—đó là tối ưu “mượt,” không phải “throughput đỉnh.” Có dùng và tinh chỉnh thế nào phụ thuộc dịch vụ của bạn có quan tâm ổn định TPOT và tải có prompt dài hay không.
⚠️ Lưu ý: Chunked Prefill và Prefix Cache là cặp tự nhiên—hit tiền tố trước hết cắt bớt tính Prefill, rồi phần miss còn lại được chia chunk vào các bước Decode. Chồng lên nhau, tải prompt dài có thể cải thiện cả TTFT lẫn TPOT.
📝 Tóm tắt
- Prefill của prompt dài nhét vào một bước kéo giãn tường thời gian bước đó, nên các request Decode trong cùng batch thấy đỉnh TPOT.
- Chunked Prefill cắt Prefill dài thành chunk rải qua nhiều bước (trong vLLM, độ dài chunk do budget còn lại quyết), đổi một ít TTFT lấy Decode đều.
- Token Budget là đơn vị tiền tệ lịch trình chung: mỗi request Decode tốn 1 token budget, mỗi Prefill Chunk tốn đúng độ dài của nó, và một bước xếp trong ngân sách.
- Lịch trình thống nhất V1 của vLLM dùng
{request_id: num_tokens}để xóa ranh giới Prefill/Decode; một khung phủ Chunked Prefill, Prefix Cache và speculative decoding. - Trong mã nguồn, chia chunk là kết quả tự nhiên của
min(nhu cầu còn lại, budget còn lại), vớinum_computed_tokenstheo dõi phần tiếp—không có chunker riêng. - Núm chính là
max_num_batched_tokens(throughput ↔ ổn định TPOT): hướng dẫn chính thức khoảng 2048 cho ITL, 8192+ cho throughput, và bạn vẫn cần load-test trên tải của mình; khi nhiều prompt dài,long_prefill_token_thresholdcó thể trần Prefill của một request mỗi bước.
🎯 Danh sách tự kiểm
- Giải thích vì sao Prefill dài làm chậm TPOT của các request Decode trong cùng batch
- Mô tả Chunked Prefill làm phẳng một đỉnh tính thành vài đỉnh nhỏ thế nào
- Nói Chunked Prefill đổi cái gì (TTFT) lấy cái gì (TPOT ổn định)
- Giải thích vì sao Token Budget đo Prefill và Decode bằng một đơn vị
- Giải thích ích lợi của biểu diễn thống nhất
{request_id: num_tokens}của V1 - Từ mã nguồn, giải thích “cắt theo budget chính là chia chunk” và
num_computed_tokensnối chunk thế nào - Mô tả tăng / giảm
max_num_batched_tokensảnh hưởng throughput và TPOT ra sao - Đánh giá tải của bạn có hợp với Chunked Prefill không
📚 Tài liệu tham khảo
- SARATHI: Efficient LLM Inference by Piggybacking Decodes with Chunked Prefills
- vLLM V1 Alpha Release (unified token-budget scheduler)
- vLLM V1 source — vllm/v1/core/sched/scheduler.py (unified scheduling)
- vLLM — Optimization and Tuning (Chunked Prefill and max_num_batched_tokens)
- vLLM — Engine Arguments (long_prefill_token_threshold and other scheduling params)