2.2 Continuous Batching
Nguyên lý Continuous Batching: từ điểm yếu của Static Batching, hiểu Iteration-level Scheduling giúp request vào/ra mỗi bước, và đối chiếu với mã nguồn Scheduler của vLLM V1
Nếu PagedAttention trả lời “GPU memory chứa được bao nhiêu request”, Continuous Batching trả lời “những request đã vào có thực sự nuôi no GPU không”. Đó là nửa còn lại của cú nhảy throughput ở các inference engine hiện đại. Mục này bắt đầu từ điểm yếu chí mạng của batching truyền thống, chỉ rõ slot trống trong batch đến từ đâu và bị loại thế nào. Cốt lõi gói trong một câu: đổi lịch từ “theo batch” sang “theo bước iteration”.
⏰ Độ mới của thông tin: ý tưởng lịch cấp iteration dựa trên paper Orca; hàng đợi Scheduler, preemption và đọc mã nguồn dựa trên vLLM v0.32.0 (phát hành 2026-10-18).
📑 Mục lục
- 1. Vì sao cần batch
- 2. Điểm yếu chí mạng của Static Batching
- 3. Continuous Batching: lịch cấp iteration
- 4. Một vòng lịch diễn ra những gì
- 5. Phối hợp với PagedAttention
- 6. Đọc mã nguồn: Scheduler của vLLM V1
- 7. Các đánh đổi then chốt khi lịch
- Tóm tắt
- Checklist tự kiểm
- Tài liệu tham khảo
1. Vì sao cần batch
Quay lại nguyên lý chương trước: Decode là Memory Bound, nút thắt là chuyển toàn bộ trọng số mô hình từ GPU memory vào đơn vị tính. Ở đây có một cơ hội lớn — chuyển trọng số một lần mà chỉ phục vụ 1 Token của 1 request thì quá phí; nếu cùng lần chuyển đó phục vụ Token của nhiều request, arithmetic intensity tăng gấp bội và sức tính nhàn rỗi được dùng.
Đó là giá trị của Batching: gộp tính của nhiều request thành một phép nhân ma trận lớn, san sẻ chi phí chuyển trọng số.
Ví dụ: xe buýt đi làm. Chạy xe vì một người (chuyển trọng số một lần chỉ đưa một Token) rất lãng phí nhiên liệu; đầy xe rồi mới chạy (một Batch phục vụ nhiều request) thì chi phí mỗi người giảm mạnh. Batch càng lớn, chi phí mỗi Token càng thấp, throughput hệ thống càng cao.
📌 Điểm then chốt: Batching nâng throughput, gần như không đánh đổi độ trễ từng request — vì nút thắt là chuyển trọng số, thêm vài Token tính trên Decode Memory Bound gần như “miễn phí”. Câu hỏi chỉ còn: tổ chức Batch đó thế nào?
2. Điểm yếu chí mạng của Static Batching
Cách đơn giản nhất gọi là Static Batching: gom đủ một batch request, đưa cùng vào mô hình, đợi cả batch sinh xong hết, rồi mới bắt đầu batch tiếp.
Nghe hợp lý, nhưng có điểm yếu chí mạng — độ dài sinh lệch rất xa. Trong cùng batch, request A có thể xong sau 20 Token trong khi request B cần 500. Với Static Batching:
- Request A xong ở bước 20, nhưng phải ngồi chờ trong batch đến khi request B chậm nhất chạy hết 500 bước, cả batch mới được giải phóng.
- Trong 480 bước sau khi A đã xong, “slot” A chiếm trong Batch chỉ quay không — GPU làm việc vô ích cho một vị trí không còn ý nghĩa.
Ví dụ: đi chung xe mà không cho ai xuống cho đến khi mọi hành khách tới đích. Người tới sớm lẽ ra đã xuống, lại bị khóa trong xe đi vòng; ghế trống phí, người đang chờ vẫn không lên được.
Hình dưới vẽ ba request cùng batch trên trục thời gian 500 bước: xanh là đang sinh; ô xanh gạch chéo là slot buộc quay không sau khi request đã xong.
⚠️ Lưu ý: Với Static Batching, nhiều sức tính phí vào slot trống của request đã xong, đồng thời request mới phải đợi cả batch kết thúc mới vào được. Tỉ lệ lãng phí phụ thuộc độ lệch độ dài trong batch: 8 request, dài nhất chạy 500 bước, 7 cái còn lại trung bình chỉ 50, thì trong slot-step chỉ có làm việc thật — khoảng 79% quay không. Độ lệch độ dài càng lớn, lãng phí càng nặng.
3. Continuous Batching: lịch cấp iteration
Continuous Batching (còn gọi In-flight Batching) phá thế bằng cách tinh độ hạt lịch từ “cả một batch” xuống “một bước iteration”.
🔑 Khái niệm cốt lõi: Iteration-level Scheduling — không còn lấy “vòng đời đầy đủ của một batch” làm đơn vị lịch; mỗi bước sinh (một Token) thì lịch lại một lần. Sau mỗi bước engine kiểm ngay:
- Request nào đã ra EOS hoặc chạm trần độ dài? → Đưa ra khỏi batch ngay và giải phóng GPU memory; không đợi người khác.
- Memory và dung lượng batch có chỗ trống? → Kéo request mới từ hàng đợi waiting ngay vào tính của bước tiếp.
Vậy thành phần batch chảy động: request đến là ghép, xong là ra. Gần như mỗi bước GPU làm việc đầy tải cho những request đang thật sự còn sinh; slot trống không còn quay không.
Hình dưới đặt cả hai lịch trên cùng 4 slot. Xanh là A–D đến cùng lúc; vàng là E, F, G đến sau; ô xanh gạch chéo là slot quay không do request đã xong; ô trắng là trống. Phía trên: Static Batching đợi B xong ở bước 10 mới cho E, F, G vào. Phía dưới: Continuous Batching bổ sung request mới ngay bước mà A, C, D thoát.
Quay lại ví dụ đi chung: giờ là xe buýt vẫy tay là dừng, tới trạm là xuống — hành khách xuống đúng điểm, ghế trống ngay cho người đang chờ trên vỉa hè, xe luôn đầy chỗ mà vẫn chạy.
📌 Điểm then chốt: Lợi ích Continuous Batching đến từ hai chỗ: request xong thoát ngay (không quay không), và request mới vào ngay (không đợi cả batch), nên độ trễ xếp hàng cũng giảm mạnh. Paper Orca lần đầu hệ thống hóa lịch cấp iteration báo cáo, trên GPT-3 175B với cùng độ trễ, throughput cao hơn FasterTransformer lúc đó 36.9 lần (bội số đó còn gồm tối ưu khác của Orca và phụ thuộc tải).
4. Một vòng lịch diễn ra những gì
Zoom vào từng step của vLLM. Có thể hình dung một vòng không dừng:
📥 Nhận request mới → 🗂️ Lịch → ⚙️ Một bước forward → 📤 Xử lý đầu ra → 🔁 Quay lại đầu
- Nhận request mới: Đưa request vừa đến vào hàng Waiting.
- Quyết định lịch: Scheduler quyết định bước này ai lên xe. Nó phải phục vụ cùng lúc hai nhóm — request trong Running đang sinh (Decode), và request trong Waiting có thể nhét vào (Prefill). Còn chỗ hay không phụ thuộc còn bao nhiêu KV Block trống trong GPU memory (đúng thông tin PagedAttention cung cấp).
- Một bước forward: Chạy một lần forward mô hình trên bộ request đã chọn; mỗi request Decode sinh 1 Token mới, Prefill sinh Token đầu.
- Xử lý đầu ra: Nối Token mới vào KV Cache của từng request; kiểm request nào chạm EOS hoặc trần độ dài, đánh dấu xong, và trả KV Block về pool trống.
- Lặp: Quay lại bước 1.
💡 Gợi ý: Khi memory thiếu và hàng Running quá đầy, scheduler còn làm Preemption: tạm dừng một số request, giải phóng KV Block của chúng, nhường memory để request khác tiến. Trong vLLM V1, request bị preempt luôn bỏ KV và recompute khi phục hồi; mục 6.3 có mã tương ứng.
5. Phối hợp với PagedAttention
Continuous Batching và PagedAttention là cặp phải đi cùng mới phát huy hết. Điểm này quan trọng:
- Continuous Batching muốn “thoát và bổ sung bất cứ lúc nào” thì phải cấp phát và thu hồi GPU memory ở độ hạt mịn, bất kỳ lúc nào. Với khối liên tục lớn kiểu cũ, khoảng trống sau khi một request thoát chưa chắc vừa request mới, bổ sung động không thành.
- Khối kích thước cố định + cấp theo nhu cầu + thu hồi tức thì của PagedAttention đúng là độ đàn hồi memory hạt mịn đó: request xong thì khối về pool trống; request mới lấy khối từ pool theo nhu cầu.
| Kỹ thuật | Vấn đề giải quyết | Năng lực cung cấp |
|---|---|---|
| PagedAttention | Phân mảnh, tận dụng thấp | Cấp phát memory hạt mịn, thu hồi tức thì |
| Continuous Batching | GPU quay không, độ trễ xếp hàng | Lịch cấp iteration, thành phần batch động |
🔑 Khái niệm cốt lõi: PagedAttention lo “memory xếp thế nào”; Continuous Batching lo “request lịch thế nào”; tích hai cái mới có throughput cao của vLLM. Một cái thôi không đủ — như hệ thống xe buýt vừa cần quản lý ghế linh hoạt vừa cần lịch vẫy tay là dừng.
6. Đọc mã nguồn: Scheduler của vLLM V1
Đưa vòng lịch ở trên xuống code. Engine vLLM V1 gom logic lịch cấp iteration vào class Scheduler trong vllm/v1/core/sched/scheduler.py. Phần dưới theo khung thiết kế trên nhánh main (code vẫn đổi; chữ ký method lấy theo phiên bản bạn dùng).
⚠️ Lưu ý: Thay đổi thiết kế lớn nhất của V1 so với V0 là scheduler không còn thấy “giai đoạn Prefill” và “giai đoạn Decode”. Comment mã nguồn viết rõ “no ‘decoding phase’ nor ‘prefill phase’”: mỗi request chỉ đẩy liên tục num_computed_tokens (số Token đã tính) tới num_tokens_with_spec (tổng mục tiêu gồm cả Token speculative). Góc nhìn thống nhất đó chính là tiền đề để Chunked Prefill ở mục 2.4 trộn Prefill và Decode trong một bước.
6.1 Hai hàng đợi và một vòng
Scheduler giữ hai trạng thái cốt lõi:
Trong hình, ô trắng là request đang xếp trong waiting, ô xanh là request đang tính trong running, mũi tên cam là chuyển giữa hai nhóm.
self.running: Một list chứa request đang tính.self.waiting: Một hàng ưu tiên chứa request đang chờ. Chính sách lịch cóFCFS(đến trước phục vụ trước) vàPRIORITY. v0.32.0 còn vài hàng phụ nhưkv_holding_waiting,deferred_waiting, vàstep_skipped_waitingtạm mỗi bước, dùng để giữ request bước này chưa lịch được vì phụ thuộc chưa sẵn (ví dụ truyền KV bất đồng bộ chưa xong). Đọc đường chính có thể tạm bỏ qua chúng.
Hai method quan trọng nhất map đúng vòng “lịch → forward → xử lý đầu ra” ở mục 4:
schedule(): Cốt lõi một bước lịch. Nó nhận một ngân sách token (token_budget), trước hết duyệt hàngrunningđể cấp Token và khối KV cho request đang sinh, chỉ khi còn ngân sách và không xảy ra preempt mới kéo request mới từwaiting. Cuối cùng đóng góiSchedulerOutputgiao mô hình chạy.update_from_output(): Sau khi có đầu ra bước của mô hình, nối Token đã sample vào từng request, kiểm điều kiện dừng (EOS / trần độ dài), giải phóng tài nguyên request đã xong, và lắp kết quả trả client.
6.2 Thứ tự quyết định trong một bước schedule
Thứ tự trong schedule() cho thấy rõ “vì sao request đang chạy ưu tiên hơn request mới”:
# Pseudocode: khôi phục đường chính của V1 Scheduler.schedule() (không phải mã nguồn từng chữ)
def schedule(self):
token_budget = self.max_num_batched_tokens
scheduled = []
preempted = []
# Ưu tiên 1: nuôi các request đang chạy (chủ yếu Decode)
req_index = 0
while req_index < len(self.running) and token_budget > 0:
req = self.running[req_index]
num_new = compute_new_tokens(req, token_budget) # giới hạn bởi ngân sách / max_model_len
while True: # cấp không được thì preempt rồi thử lại
blocks = self.kv_cache_manager.allocate_slots(req, num_new)
if blocks is not None:
break
victim = self.running.pop() # FCFS đá cái cuối (vào muộn nhất)
self._preempt_request(victim) # giải phóng khối, đẩy lại đầu waiting
preempted.append(victim)
if victim is req: # đá trúng chính mình; request này bỏ bước
break
if blocks is None:
break
scheduled.append(req)
token_budget -= num_new
req_index += 1
# Ưu tiên 2: chỉ khi còn ngân sách và bước này không preempt, mới kéo Prefill mới
if not preempted and token_budget > 0:
while self.waiting and token_budget > 0:
req = self.waiting.pop_front()
if not self._can_admit(req): # kiểm trần running / LoRA / v.v.
break
self.running.append(req)
scheduled.append(req)
return SchedulerOutput(scheduled)
Hai thiết kế then chốt nằm trong logic này:
- Request đang bay ưu tiên hơn request mới: Giữ request đã chạy tiếp tục tiến trước; ngân sách còn lại mới nhận request mới. Tránh nạn “request mới chen liên tục, request cũ mãi không xong”.
- Preempt khi memory cạn: Khi
allocate_slotstrảNone(không xin được khối KV), scheduler chọn một “nạn nhân”, giải phóng khối rồi thử cấp lại, đến khi thành công hoặc chính request hiện tại bị đá. Dưới FCFS đá phần tử cuối củarunning(vào muộn nhất); dưới PRIORITY đá(priority, arrival_time)lớn nhất, tức ưu tiên thấp nhất, cùng ưu tiên thì đến muộn nhất.
6.3 Hành vi preempt trong code
Request bị preempt đi qua dọn dẹp trong _preempt_request(): giải phóng khối KV và encoder cache → đặt trạng thái PREEMPTED → đưa num_computed_tokens về 0 → đẩy lại đầu hàng waiting để khi có tài nguyên thì phục hồi trước.
📌 Điểm then chốt: Đưa num_computed_tokens về 0 nghĩa là chiến lược phục hồi preempt của V1 là Recompute: lần sau request bị preempt được lịch, Prefill lại Prompt và Token đã sinh từ đầu, không swap KV từ CPU (ứng với hai cách phục hồi ở mục 2.1). Tài liệu tinh chỉnh chính thức nói V1 mặc định recompute chứ không swap vì dưới kiến trúc V1 recompute rẻ hơn; bật Prefix Cache thì khối đầy đã giải phóng nếu chưa bị đuổi vẫn có thể hit trực tiếp khi phục hồi.
💡 Gợi ý: Đọc mã scheduler hãy nắm đường chính — hai hàng waiting/running + ngân sách token_budget + giá trị trả về của allocate_slots điều khiển preempt. Vòng “lịch” trừu tượng ở mục 4 chính là ba thứ đó tương tác. Chi tiết lịch sâu hơn (Chunked Prefill cắt thế nào) ở mục 2.4; chương 3 sẽ đi lại cùng luồng với Scheduler tinh gọn của nano-vllm.
7. Các đánh đổi then chốt khi lịch
Continuous Batching mạnh, nhưng scheduler đối mặt các mục tiêu xung đột. Kỹ thuật cần rõ các đánh đổi:
- Throughput vs độ trễ: Một bước nhét càng nhiều request, throughput hệ thống càng cao, nhưng mỗi bước forward cũng lâu hơn, TPOT từng request hơi xấu đi. Cỡ batch phải cân hai phía.
- Tranh chấp Prefill vs Decode: Prefill của request mới là việc nặng Compute-Bound. Nhét Prefill Prompt siêu dài vào một bước sẽ kéo chậm rõ TPOT Decode cùng batch (TPOT rung). Đó đúng là việc Chunked Prefill ở mục sau giải — cắt Prefill dài thành khối để không độc chiếm cả step.
- Công bằng vs hiệu suất: Request đến trước có ưu tiên không? Request dài có bị request ngắn chen liên tục đến đói không? Chính sách lịch phải cân hiệu suất tổng thể với công bằng giữa các request.
⚠️ Lưu ý: Continuous Batching khiến chỉ số độ trễ nhạy với tải. Tải thấp batch nhỏ, độ trễ thấp; tải cao batch lớn, hàng dài ra, đuôi P95/P99 tăng rõ. Khi lập kế hoạch dung lượng, luôn đo độ trễ ở mức đồng thời mục tiêu, không nhìn số liệu hệ thống trống.
📝 Tóm tắt
- Bản chất batching là san sẻ chi phí chuyển trọng số, dùng sức tính nhàn của Decode Memory Bound, nâng throughput gần như không tăng độ trễ.
- Static Batching chí mạng vì “trong batch kéo nhau”: request ngắn phải đợi cái dài nhất; độ lệch độ dài càng lớn, càng nhiều slot-step quay không.
- Continuous Batching tinh độ hạt lịch xuống bước iteration (Iteration-level Scheduling): xong là ra, đến là vào; slot quay không gần như biến mất.
- Nó phối sâu với PagedAttention: cái sau cung cấp memory hạt mịn thu hồi được nên cái trước mới giữ được batch động.
- Trên mã nguồn,
SchedulerV1 dùng hai hàngwaiting/running+ ngân sáchtoken_budgetđiều khiển lịch, ưu tiên request đang bay, preempt khi memory cạn, và không còn tách giai Prefill/Decode. - Lịch có các đánh đổi throughput/độ trễ, tranh chấp Prefill/Decode, công bằng/hiệu suất; Prefill nhiễu Decode được Chunked Prefill giảm thêm.
🎯 Checklist tự kiểm
- Giải thích từ góc Memory Bound vì sao batching nâng throughput
- Nói rõ cơ chế request ngắn bị request dài kéo dưới Static Batching
- Giải thích khác biệt cốt lõi giữa Iteration-level Scheduling và lịch cấp batch
- Mô tả luồng “thoát request xong, bổ sung request mới” trong một bước lịch vLLM
- Giải thích vì sao Continuous Batching phải dựa trên quản lý memory hạt mịn của PagedAttention
- Nói rõ hai hàng
waiting/runningvà thứ tự quyết định củaschedule()trong Scheduler V1 - Giải thích vì sao scheduler V1 “không tách Prefill/Decode”, và mặc định preempt dùng recompute
- Phân tích cỡ batch đẩy throughput và TPOT theo hướng ngược nhau thế nào
- Giải thích vì sao Prefill Prompt dài nhiễu Decode cùng batch, và kỹ thuật nào giảm
📚 Tài liệu tham khảo
- Blog vLLM: Easy, Fast, and Cheap LLM Serving with PagedAttention
- Orca: A Distributed Serving System for Transformer-Based Generative Models (người đề xuất lịch cấp iteration)
- vLLM V1 Alpha Release (thiết kế scheduler thống nhất)
- Mã nguồn vLLM V1 — vllm/v1/core/sched/scheduler.py (hiện thực Scheduler)
- vLLM — Optimization and Tuning (preempt và recompute)