2.1 PagedAttention
Đi sâu kỹ thuật lõi PagedAttention của vLLM: mượn phân trang bộ nhớ ảo, dùng Block Table xóa phân mảnh KV Cache, và hỗ trợ chia sẻ prefix cùng Copy-on-Write
Sổ cái bộ nhớ KV Cache ở chương trước để lại một vấn đề: cách cấp phát bộ nhớ GPU liên tục truyền thống lãng phí nhiều vì “dự trữ tức là phí,” tỷ lệ dùng thật có thể chỉ khoảng hai đến ba phần mười (bài vLLM đo được chỉ 20.4%–38.2% bộ nhớ KV Cache trong các hệ lúc đó thực sự chứa trạng thái Token). vLLM dùng PagedAttention để giải, bằng cách đưa phương pháp phân trang quản lý bộ nhớ của hệ điều hành lên bộ nhớ GPU. Mục này nói bốn việc: nó mượn ý tưởng gì, Block Table hoạt động thế nào, vì sao diệt được phân mảnh, và khả năng chia sẻ prefix mà phân trang mang theo.
⏰ Thời hạn thông tin: phân trang, bảng khối và CoW dựa trên bài PagedAttention; tham số mặc định và đọc mã V1 dựa trên vLLM v0.32.0 (phát hành 2026-10-18).
📑 Mục lục
- 1. Nhắc lại vấn đề: hai tội của cấp phát liên tục
- 2. Ý tưởng lõi: mượn phân trang bộ nhớ ảo của OS
- 3. Block Table: ánh xạ khối logic sang khối vật lý
- 4. Phân trang diệt phân mảnh thế nào
- 5. Nhìn từ kernel: đọc khối vật lý rải rác thế nào
- 6. Phần thưởng: chia sẻ bộ nhớ và Copy-on-Write
- 7. Preempt và phục hồi: hết bộ nhớ GPU thì sao
- 8. Thực hành: tinh chỉnh PagedAttention trong vLLM
- 9. Đọc mã nguồn: quản lý KV Cache trong vLLM V1
- 10. Chi phí và đánh đổi
- Tóm tắt
- Danh sách tự kiểm
- Tài liệu tham khảo
1. Nhắc lại vấn đề: hai tội của cấp phát liên tục
Trước khi sửa, đóng đinh vấn đề. Framework suy luận truyền thống (trước PagedAttention) cấp cho KV Cache mỗi request một tấm bộ nhớ GPU liên tục, dự trữ theo “độ dài dài nhất request này có thể sinh.” Điều đó tạo hai tội:
- Internal Fragmentation (phân mảnh nội): Nếu request có thể sinh tới 2048 Token, bạn dự trữ chỗ cho 2048 Token, nhưng nó có thể dừng ở EOS sau chỉ 100. 1948 slot Token còn lại bị request này “khóa” và không dùng; request khác không vào được.
- External Fragmentation (phân mảnh ngoại): Các tấm dự trữ lớn để lại khe lệch giữa chúng. Mảnh vụn cộng lại có thể nhiều, nhưng không khe nào vừa một request mới đầy đủ, nên đều phí.
Ví dụ: nhà hàng giữ bàn 10 người cho mọi nhóm “phòng khi đủ 10.” Hầu hết bàn chỉ ngồi hai ba người; nhiều ghế trống. Nhóm 4 người mới vẫn xếp hàng ở cửa vì không còn bàn trống nguyên—dù ghế trống cộng lại đủ chỗ dễ dàng.
📌 Điểm then chốt: Phân mảnh đến từ chồng liên tục + dự trữ. Bỏ “phải liên tục” và “phải dự trữ theo độ dài tối đa,” phân mảnh biến mất. PagedAttention bắt đầu đúng từ đó.
2. Ý tưởng lõi: mượn phân trang bộ nhớ ảo của OS
Hệ điều hành gặp cùng vấn đề từ vài thập kỷ trước: tiến trình muốn không gian địa chỉ liên tục, nhưng bộ nhớ vật lý bị mọi tiến trình cắt nát. Câu trả lời của OS là phân trang bộ nhớ ảo—tiến trình thấy “địa chỉ ảo” liên tục, OS ánh xạ qua Page Table lên các “trang vật lý” rải rác tùy ý. Tiến trình cảm giác sở hữu một tấm liên tục; vật lý thì ghép từ chỗ này chỗ kia.
PagedAttention chép cơ chế đó lên KV Cache:
- Cắt KV Cache thành KV Block kích thước cố định; mỗi khối giữ Key và Value của một số Token cố định (số đó là
block_size; trong vLLM giá trị thường gặp là 16). - KV Cache của một request về logic là chuỗi “khối logic” liên tục, nhưng vật lý có thể nằm bất kỳ đâu trong bộ nhớ GPU.
- Một Block Table ghi “khối logic nào của request này ánh xạ sang khối vật lý nào trong bộ nhớ GPU.”
🔑 Khái niệm lõi: PagedAttention = phân trang bộ nhớ ảo cho KV Cache. Chuỗi Token bị cắt thành khối, cấp theo nhu cầu, ánh xạ qua bảng khối. Từ đó bộ nhớ GPU không cần liên tục, và không còn dự trữ trước theo độ dài tối đa.
💡 Gợi ý: Tài liệu vLLM nhấn mạnh KV Block ở đây là đơn vị bộ nhớ GPU riêng của vLLM, hoàn toàn khác “thread block” của CUDA. Đừng lẫn.
3. Block Table: ánh xạ khối logic sang khối vật lý
Block Table là trung tâm của cơ chế. Đây là một ví dụ làm việc.
Giả sử block_size = 4 (mỗi khối giữ KV của 4 Token; tải thật thường dùng 16; ta dùng 4 cho dễ hình). Một request Prefill 9 Token, nên cần khối logic:
- Khối logic 0: Token 0~3
- Khối logic 1: Token 4~7
- Khối logic 2: Token 8 (chỉ dùng 1 slot, còn 3)
vLLM lấy 3 khối vật lý từ pool trống—ID vật lý không cần liên tục, ví dụ #7, #2, #5. Block Table trông như:
| Khối logic | Khối vật lý | Trạng thái |
|---|---|---|
| 0 | #7 | Đầy (4/4) |
| 1 | #2 | Đầy (4/4) |
| 2 | #5 | Điền một phần (1/4) |
Hình đặt bảng đó trở lại bộ nhớ GPU: trái là 3 khối logic liên tục của request, giữa là Block Table, phải là pool vật lý. Ô đỏ/vàng/xanh đặc là KV đã ghi cho khối logic 0, 1, 2; ô xanh gạch chéo là slot đã cấp nhưng trống trong #5; khối tím và hồng gạch chéo thuộc request khác; ô trắng là trống.
Trong Decode, mỗi Token mới điền slot trống tiếp theo trong khối logic hiện tại. Khối logic 2 còn 3 slot, nên nhận thêm được 3 Token; khi đầy (ở Token 12), vLLM theo nhu cầu xin một khối vật lý mới và gắn vào khối logic 3—dùng bao nhiêu cấp bấy nhiêu, không dự trữ trước.
Trong Attention, CUDA Kernel dùng Block Table lấy physical_block_number và physical_block_offset của từng vị trí logic, rồi đọc toàn bộ lịch sử KV từ các khối vật lý rải rác.
4. Phân trang diệt phân mảnh thế nào
Quay lại hai tội ở mục 1, và cách phân trang phá từng cái:
- Diệt phân mảnh nội: Không còn dự trữ theo độ dài tối đa; cấp từng khối một và chỉ xin tiếp khi đầy. Phí bị trần ở “phần chưa điền của khối cuối,” nhiều nhất dưới một khối (ví dụ nhiều nhất 15 slot Token khi
block_size=16). So với thói quen cũ phí hàng nghìn slot Token, đó là không đáng kể. - Diệt phân mảnh ngoại: Mọi khối vật lý cùng một kích thước, nên khối trống nào cũng phục vụ request nào. Không còn “đủ chỗ nhưng sai hình”; mọi khối trong pool trống đều tương đương và tái dùng được.
Hình so sánh cả hai bộ cấp phát trên cùng 64 slot Token. Ô đặc là Token đã ghi (đỏ/xanh dương/xanh lá cho request A/B/C); ô cùng màu gạch chéo là slot dự trữ nhưng chưa dùng (phân mảnh nội); ô xám đứt là lỗ rải giữa vùng dự trữ (phân mảnh ngoại); ô trắng là trống. Bên trái, slot trống cộng lại đủ cho request C mới, nhưng không đoạn liên tục nào đủ dài; bên phải C lấy hai khối trống không liền kề.
| 📊 Chiều | Liên tục truyền thống | PagedAttention |
|---|---|---|
| Layout bộ nhớ | Một tấm liên tục mỗi request | Khối kích thước cố định, vật lý rải |
| Khi nào cấp | Dự trữ trước theo độ dài tối đa | Cấp từng khối theo nhu cầu |
| Phân mảnh nội | Nặng (dự trữ >> dùng thật) | Rất nhỏ (< 1 khối) |
| Phân mảnh ngoại | Có (khe lệch) | Hết (khối đều) |
| Tỷ lệ dùng bộ nhớ | Thấp | Gần tối ưu |
📌 Điểm then chốt: Tỷ lệ dùng bộ nhớ cao hơn nghĩa là cùng một card chứa được nhiều request đồng thời hơn. Chương trước chỉ ra Decode là Memory Bound; nhét nhiều request hơn vào một Batch nâng thông lượng hệ thống rất nhiều, nên bộ nhớ PagedAttention tiết kiệm được biến thành thông lượng.
5. Nhìn từ kernel: đọc khối vật lý rải rác thế nào
Đến đây câu chuyện là ánh xạ logic. Khi KV đã rải khắp bộ nhớ GPU, Attention Kernel vẫn gom lại hiệu quả thế nào? Mục này nhìn vào trong Kernel. Hiểu được sẽ thấy PagedAttention không chỉ là mẹo bộ nhớ; đó là đồng thiết kế toán tử và layout bộ nhớ.
Attention truyền thống giả định K và V của một chuỗi nằm liên tục; Kernel đi bằng địa chỉ gốc cộng offset. PagedAttention phá tính liên tục, nên Kernel phải hỏi bảng khối trước và dịch “vị trí logic” thành “địa chỉ vật lý.” Hai đại lượng quan trọng:
physical_block_number: Đọc khối vật lý nào; nhân với stride khối để lấy base của khối.physical_block_offset: Slot nào trong khối đó giữ Token này.
Trong tài liệu Kernel chính thức của vLLM, phép tính địa chỉ con trỏ Key đại khái như sau (phác thảo giản lược, không chép nguyên văn):
// Từ số khối vật lý + offset head + offset trong khối, lấy địa chỉ bắt đầu Key của token này
const scalar_t* k_ptr = k_cache
+ physical_block_number * kv_block_stride // định vị khối vật lý
+ kv_head_idx * kv_head_stride // định vị KV head
+ physical_block_offset * x; // định vị token trong khối
Một Warp (32 thread) tiến từng khối ở vòng ngoài: mỗi vòng nhảy sang khối vật lý tiếp theo theo số khối, và k_ptr trỏ Key ở khối khác. Vậy lưu trữ vật lý rải được truy cập như một ngữ cảnh logic liên tục. Chi phí của lớp gián tiếp nằm ở việc tính lại địa chỉ mỗi vòng; mục 10 (“Chi phí và đánh đổi”) nói về overhead đó.
⚠️ Lưu ý: Bản đọc Kernel chính thức đó được đánh dấu là “tài liệu lịch sử dựa trên bài gốc; không còn phản ánh mã vLLM hiện tại.” Coi công thức con trỏ trên là phác thảo nguyên lý, không phải mã nguồn hiện hành. Để xem triển khai thật, nhìn các Attention backend của engine V1 (mục 2.5 nói FlashAttention/FlashInfer và các backend khác tiếp quản lớp này thế nào).
6. Phần thưởng: chia sẻ bộ nhớ và Copy-on-Write
Phân trang còn mở ra thứ cấp phát liên tục không làm được: nhiều request chia sẻ cùng một khối vật lý.
Vì KV được tham chiếu gián tiếp theo khối qua bảng khối, bảng của nhiều request có thể trỏ cùng một khối vật lý. Trường hợp điển hình:
- Chia sẻ prefix giữa các request: Một batch request chung cùng System Prompt, nên KV prefix giống nhau—không cần lưu N lần. Cho mọi Block Table trỏ cùng các khối vật lý. Chia sẻ này chỉ đọc: mỗi request chỉ đọc các khối prefix đó và ghi nội dung mới vào khối riêng. Đó là nền vật lý cho mục sau (2.3 Prefix Cache).
- Nhánh trong một request: Khi sample nhiều câu trả lời song song từ một Prompt (
n > 1) hoặc chạy Beam Search, các ứng viên chia sẻ KV Prompt nhưng bắt đầu lệch bên trong một khối—lúc đó cần CoW bên dưới.
Hai chuỗi đang chia sẻ một khối sau đó lệch thì sao? Bài PagedAttention (và vLLM V0) trả lời bằng một cơ chế OS cổ điển khác—Copy-on-Write (CoW):
- Khi chia sẻ chỉ đọc, mọi người trỏ cùng khối; một bộ đếm tham chiếu (
ref_cnt) theo dõi bao nhiêu request đang dùng. - Khi một request phải ghi vào khối đang chia sẻ, trước hết copy khối đó thành bản riêng, ghi trên bản copy, rồi đổi đích Block Table của nó. Request khác không bị đụng.
Hình bên trái là chia sẻ chỉ đọc, bên phải là CoW khi B ghi. Xanh là KV Prompt hai request chia sẻ; đỏ và vàng là Token A và B tự ghi; ô trắng là slot trống.
💡 Gợi ý: “Bộ đếm tham chiếu + Copy-on-Write” cùng ý với tiến trình cha/con chia sẻ trang sau fork() trên Linux. Nếu đã biết phân trang OS và CoW, gần như không có gì trong PagedAttention là mới—đó cũng là một phần vì sao thiết kế cảm giác sạch.
7. Preempt và phục hồi: hết bộ nhớ GPU thì sao
Phân trang sửa “cách lưu hiệu quả,” nhưng vẫn còn vấn đề thực tế: quá nhiều request đồng thời hút cạn pool khối vật lý, và một bước Decode mới không xin được khối. Không thể cứ crash request. Câu trả lời của vLLM là Preemption: giống OS lập lịch tiến trình, tạm “dừng” một số request, đưa tài nguyên của chúng cho người khác, và phục hồi khi có chỗ trở lại.
Cò kích hoạt thẳng: một bước Decode cần khối mới cho một số request, nhưng khối vật lý trống không đủ. Scheduler chọn một số request để “đá ra” và giải phóng khối vật lý của chúng. Với KV Cache đã tính của request bị đá có hai lựa chọn:
| 🔄 Phục hồi | Làm gì | ✅ Ưu | ❌ Giá |
|---|---|---|---|
| Swapping | Copy KV Cache từ bộ nhớ GPU sang bộ nhớ CPU; copy lại khi phục hồi | Không tính lại; tiết FLOPs | Dùng bộ nhớ CPU; chi phí đi-về PCIe |
| Recomputation | Bỏ KV; khi phục hồi coi các Token đã sinh như Prompt mới và Prefill lại | Không dùng bộ nhớ CPU; đơn giản | Chạy lại Prefill; phí FLOPs |
Ví dụ: swapping là bỏ giấy tờ lúc không cần vào ngăn kéo (bộ nhớ CPU) rồi lấy ra sau; recomputation là vứt bản nháp và suy lại từ trí nhớ (các Token đã sinh). Cái nào thắng phụ thuộc “ngăn kéo có đủ lớn / truyền có đủ nhanh” đối với “suy lại đắt đến mức nào.”
💡 Gợi ý: Bảng trên là câu chuyện bài báo và V0. Preempt của vLLM V1 chỉ tính lại: request bị preempt giải phóng mọi khối, đưa num_computed_tokens về 0, và quay lại đầu hàng waiting; phục hồi nghĩa là Prefill lại. Tài liệu tinh chỉnh chính thức nói dưới kiến trúc V1 recompute rẻ hơn. Khi bật Prefix Cache, khối đầy đã giải phóng nhưng chưa bị đuổi vẫn có thể trúng lúc phục hồi, nên ít việc phải tính lại hơn.
📌 Điểm then chốt: Preempt là lưới an toàn của vLLM dưới áp lực bộ nhớ để bạn không OOM và request không fail cứng. Nếu log liên tục cảnh báo preempt, đồng thời quá cao—hạ đồng thời, nâng gpu_memory_utilization để cho KV Cache thêm chỗ, hoặc thêm GPU. Đó là tín hiệu “hệ thống đang chống đỡ,” không phải trạng thái ổn định.
8. Thực hành: tinh chỉnh PagedAttention trong vLLM
Với người dùng, bạn có thể điều khiển PagedAttention bằng các tham số engine sau (mặc định từ vLLM v0.32.0; đổi phiên bản thì kiểm vllm serve --help).
from vllm import LLM
llm = LLM(
model="meta-llama/Llama-3.1-8B-Instruct",
# Tỷ lệ tối đa tổng bộ nhớ GPU instance vLLM này được dùng; sau trọng số
# và đỉnh activation, phần còn lại vào pool khối KV Cache. Mặc định 0.92 ở v0.32.0.
gpu_memory_utilization=0.92,
# Mỗi KV Block giữ KV của bao nhiêu token. Độ hạt phân trang cơ bản.
block_size=16,
# Bật prefix caching để request cùng prefix tái dùng khối vật lý (xem §2.3).
enable_prefix_caching=True,
# Số chuỗi tối đa xử lý cùng lúc trong một batch.
max_num_seqs=256,
)
Quan hệ của từng tham số với PagedAttention:
gpu_memory_utilization: Mặc định v0.32.0 là0.92(phiên bản sớm hơn dùng0.9), nên instance được dùng tới 92% bộ nhớ GPU; phần còn lại sau trọng số và activation trở thành pool khối KV Cache. Cao hơn nghĩa là đồng thời nhiều hơn và ít preempt hơn, nhưng ít dư cho activation và overhead khác; quá mạnh thì OOM.block_size: Độ hạt phân trang—Token mỗi khối, như trên. Tài liệu gọi là “kích thước khối cache liên tục tính bằng Token.” Đánh đổi “độ dài Block Table / chi phí quản lý” với “phân mảnh nội ở khối đuôi.”enable_prefix_caching: Khi bật, request chung prefix (ví dụ chung System Prompt) tái dùng khối vật lý đã cache và bỏ Prefill lặp. Đây là công tắc cấu hình cho khả năng chia sẻ bộ nhớ ở mục 6.max_num_seqs: Số chuỗi tối đa mỗi iteration; cùng Continuous Batching ở mục sau đặt trần đồng thời.
💡 Gợi ý: Hầu hết trường hợp bạn không cần đổi block_size tay—mặc định đã tinh trên hầu hết mô hình. Núm bạn thực sự vặn là gpu_memory_utilization (siết bộ nhớ) và enable_prefix_caching (bật khi prefix được chia sẻ).
9. Đọc mã nguồn: quản lý KV Cache trong vLLM V1
Mục này cho thấy vLLM biến “bảng khối + pool trống + bộ đếm tham chiếu” thành mã thế nào. Dựa trên engine vLLM V1 dưới vllm/v1/core/ (mã tiếp tục tiến hóa; coi chữ ký phương thức phụ thuộc phiên bản, và coi đây là khung thiết kế ổn định).
⚠️ Lưu ý: Tài liệu Kernel trích ở mục 5 là tài liệu lịch sử thời V0. Engine V1 viết lại lập lịch và quản lý KV Cache và tập trung logic vào hai lớp lõi, KVCacheManager và BlockPool. Phần dưới là triển khai V1.
9.1 Hai lớp lõi chia việc thế nào
V1 kẻ một đường sạch:
KVCacheManager(kv_cache_manager.py): Manager từ góc nhìn request. Mọi bước scheduler đi qua nó để cấp và giải phóng khối cho request. Các phương thức lõi ánh xạ một-một với khái niệm trên:get_computed_blocks: Tra prefix-cache; trả các khối đã tính mà request này trúng được, và bao nhiêu Token trúng. Cửa vào cho “prefix chia sẻ” ở mục 6.allocate_slots: Xin slot/khối cho Token mới của một request, giải phóng khối không còn cần, và kiểm khối trống có đủ không; nếu không, trảNone(scheduler rồi preempt). Đây là “cấp từng khối theo nhu cầu” từ mục 3.free: Khi request kết thúc, giải phóng khối theo thứ tự ngược—thu đuôi trước, vì khối đầu dễ bị prefix cache tái dùng hơn và nên sống lâu hơn.
BlockPool(block_pool.py): Kho khối vật lý toàn cục. Nó không quan tâm request nào sở hữu gì; chỉ theo dõi “khối nào trống, khối nào đang cache.”
9.2 Vì sao khối trống dùng danh sách liên kết kép
Một chi tiết dễ bỏ: BlockPool giữ mọi khối trong list thường self.blocks: list[KVCacheBlock] (chỉ số theo block id), nhưng khối trống được quản lý bởi FreeKVCacheBlockQueue riêng—bên trong là danh sách liên kết kép, không phải stack hay deque thường.
Khối trống cần hai thao tác cùng lúc:
- Khi cấp, lấy nhanh N từ đầu (
popleft_n); - Khi trúng prefix-cache, một khối “trống nhưng vẫn giữ nội dung tái dùng được” phải được kích hoạt lại—nhổ từ vị trí giữa tùy ý trong list (
remove).
Hàng đợi thường không xóa giữa trong ; danh sách liên kết kép làm được. Prefix caching cần “khối trống vẫn có thể mang cache tái dùng,” nên vLLM chọn danh sách liên kết kép.
9.3 Bộ đếm tham chiếu và thứ tự giải phóng
ref_cnt là xương sống của chia sẻ bộ nhớ. Hành vi trong mã khớp phần “chia sẻ chỉ đọc” ở mục 6:
# Pseudocode: khôi phục logic chính của BlockPool (không phải mã nguồn từng chữ)
def get_new_blocks(self, num_blocks):
if num_blocks > self.get_num_free_blocks():
raise ValueError("not enough free blocks") # kích hoạt preempt tầng trên
blocks = self.free_block_queue.popleft_n(num_blocks)
for blk in blocks:
assert blk.ref_cnt == 0 # khối mới cấp phải không còn ref
blk.ref_cnt += 1
return blocks
def free_blocks(self, ordered_blocks):
for blk in ordered_blocks:
blk.ref_cnt -= 1
to_free = [b for b in ordered_blocks if b.ref_cnt == 0] # chỉ thu khi không ai dùng
no_hash = [b for b in to_free if b.block_hash is None]
has_hash = [b for b in to_free if b.block_hash is not None]
self.free_block_queue.prepend_n(no_hash) # không có giá trị cache: đầu list, tái dùng trước
self.free_block_queue.append_n(has_hash) # đã hash: đuôi list, đuổi sau cùng
def touch(self, blocks): # gọi khi trúng prefix-cache
for blk in blocks:
if blk.ref_cnt == 0: # "cứu" nó khỏi free list
self.free_block_queue.remove(blk)
blk.ref_cnt += 1
Hai thiết kế đáng nhớ:
- Giải phóng không nghĩa là xóa sạch: Khối có
ref_cntvề 0 chỉ quay lại free list; nội dung vẫn còn. Nếu prefix hash của request sau trúng nó,touch()hồi sinh và tái dùng. Đó là cơ chế tầng thấp đằng sau Automatic Prefix Caching bỏ Prefill lặp. - Khối đã hash sống lâu hơn: Khi giải phóng, vLLM đặt “khối không có giá trị cache” ở đầu list (cấp lại và ghi đè trước; chú thích mã nói điều đó cũng để khối vừa giải phóng được tái dùng ngay cho locality GPU tốt hơn) và “khối đã hash, tái dùng được” ở đuôi list (đuổi muộn hơn). Cấp phát luôn lấy từ đầu, nên khối đã hash bị đuổi theo thứ tự giải phóng—thực chất LRU.
⚠️ Lưu ý: BlockPool của V1 không có CoW. Ba hàm trên chỉ tăng/giảm ref_cnt và nối free list; không có đường “copy một khối.” V1 bỏ CoW vì hai lý do thiết kế:
- Chỉ chia sẻ khối đầy: Prefix caching chỉ hash và chia sẻ khối đã đầy; khối đuôi chưa điền không bao giờ chia sẻ. Token mới sinh luôn nối vào khối mới riêng của request. Chia sẻ prefix giữa request vì vậy luôn chỉ đọc; không bao giờ có khoảnh khắc “ghi vào khối đang chia sẻ.”
n > 1thành các request con độc lập: V1 tách sampling song song thànhnrequest con mỗi cáin = 1(xemParentRequesttrongvllm/v1/engine/parallel_sampling.py). Các con chia sẻ khối đầy của Prompt qua prefix cache và không còn cần nhánh trong khối.
Vậy CoW ở mục 6 là cơ chế bài báo và V0 (cow_block_if_not_appendable của V0 đã đi cùng V0). Hiểu nó giúp nắm thiết kế; bạn sẽ không tìm thấy mã khớp trên đường KV Cache của V1.
📌 Điểm then chốt: Khi đọc mã nguồn, nắm đường chính này—KVCacheManager lo request, BlockPool lo khối vật lý, ref_cnt lo chia sẻ, FreeKVCacheBlockQueue lo đuổi. Cùng nhau chúng hiện thực phân trang, chia sẻ chỉ đọc và preempt từ mục 3–7.
10. Chi phí và đánh đổi
PagedAttention có cái giá; kỹ thuật cần thấy rõ đánh đổi:
- ✅ Lợi ích: Tỷ lệ dùng bộ nhớ gần tối ưu, chia sẻ prefix, và phục hồi qua preempt khi hết bộ nhớ (bài báo và V0 hỗ trợ swap và recompute; V1 chỉ recompute) thay vì fail thẳng.
- ❌ Chi phí: Attention Kernel cần thêm tra bảng khối và lớp gián tiếp (tính lại con trỏ ở mục 5); phức tạp hơn Attention liên tục ngây thơ. Bản thân Block Table cũng tốn một ít overhead quản lý.
Về block_size: quá nhỏ thì Block Table dài ra và chi phí quản lý tăng; quá lớn thì phân mảnh nội quay lại (phí nhiều hơn ở khối cuối). Mặc định thường gặp 16 của vLLM nằm giữa chi phí quản lý và phân mảnh.
⚠️ Lưu ý: “Phân trang + bảng khối + bộ đếm tham chiếu” nhấn suốt bài này là nền thiết kế ổn định của vLLM (CoW chỉ có trong bài báo và V0; xem §9.3). Cấu trúc dữ liệu cụ thể trong engine V1 (ví dụ KV Cache Manager) đã được viết lại vài lần. Học ý tưởng lõi không đổi; coi mã nguồn mới nhất là chuẩn cho chi tiết mã.
📝 Tóm tắt
- PagedAttention đưa phân trang bộ nhớ ảo của OS lên KV Cache: cắt thành khối, cấp theo nhu cầu, ánh xạ qua bảng khối.
- Block Table ánh xạ các khối logic liên tục mà request thấy lên các khối vật lý rải rác, nên bộ nhớ không cần liên tục và không cần dự trữ theo độ dài tối đa.
- Về phía Kernel,
physical_block_number+physical_block_offsettra địa chỉ và đọc khối vật lý rải như một ngữ cảnh logic liên tục—đồng thiết kế toán tử và layout bộ nhớ. - Nó diệt cả phân mảnh nội (không dự trữ thừa) lẫn phân mảnh ngoại (khối đều), đẩy tỷ lệ dùng gần tối ưu, và biến thành đồng thời cùng thông lượng cao hơn.
- Phân trang tự nhiên hỗ trợ chia sẻ bộ nhớ + Copy-on-Write, đặt nền cho tái dùng prefix (Prefix Cache).
- Khi bộ nhớ cạn, preempt là lưới an toàn (bài báo và V0 có thể swap sang CPU hoặc recompute; V1 mặc định và chỉ dùng recompute) để request fail mềm thay vì OOM.
- Trong thực tế núm bạn vặn là
gpu_memory_utilizationvàenable_prefix_caching;block_sizeđánh đổi chi phí quản lý với phân mảnh (vLLM thường dùng 16). - Ở mã nguồn, engine V1 đặt điều này bằng
KVCacheManager(request) +BlockPool(khối vật lý) +ref_cnt(chia sẻ) +FreeKVCacheBlockQueue(đuổi).
🎯 Danh sách tự kiểm
- Giải thích được nguyên nhân phân mảnh nội vs ngoại dưới cấp phát liên tục
- Giải thích Block Table qua ánh xạ “khối logic → khối vật lý”
- Vẽ được Block Table của một request sau Prefill và mô tả khối lớn dần theo nhu cầu trong Decode
- Giải thích Attention Kernel đọc KV rải qua số khối vật lý và offset trong khối thế nào
- Giải thích vì sao PagedAttention có thể xóa cả phân mảnh nội lẫn ngoại
- Giải thích chia sẻ chỉ đọc hỗ trợ tái dùng prefix thế nào, Copy-on-Write hỗ trợ sampling song song thế nào, và vì sao V1 không còn cần CoW
- Phân biệt hai chế độ phục hồi preempt (swap vs recompute) và cái giá của chúng
- Nói được
gpu_memory_utilization,block_sizevàenable_prefix_cachingmỗi cái ảnh hưởng gì - Nói được sự chia việc giữa
KVCacheManagervàBlockPooltrong vLLM V1 - Giải thích vì sao khối trống dùng danh sách liên kết kép thay vì hàng đợi thường
- Phân tích điều gì sai nếu
block_sizequá lớn hoặc quá nhỏ - Nối “tiết kiệm bộ nhớ” với “nâng thông lượng” qua kết luận Memory Bound
📚 Tài liệu tham khảo
- Efficient Memory Management for Large Language Model Serving with PagedAttention (bài vLLM)
- Blog vLLM: Easy, Fast, and Cheap LLM Serving with PagedAttention
- vLLM Design — PagedAttention Kernel
- vLLM Design — Automatic Prefix Caching
- vLLM — Engine Arguments
- vLLM — Optimization and Tuning (preempt và recompute của V1)
- Mã nguồn vLLM V1 — vllm/v1/core (KVCacheManager, BlockPool)