Bỏ qua điều hướng
chuanweb.com

Chương 8 · Tối ưu Inference

Kỹ thuật tối ưu inference: batching, quản lý bộ nhớ KV, offload, lượng tử và speculative decoding — hiệu quả khi nào và chi phí ra sao.

~85 phút đọc · Thuật ngữ

Tối ưu Inference

Cùng một model chạy trên cùng một accelerator có thể chỉ trả về vài chục token mỗi giây cho một người dùng, nhưng lại đạt tổng thông lượng hàng nghìn token mỗi giây trên toàn bộ người dùng. Điều này xảy ra vì nhiều request có thể được tính toán cùng lúc, chia sẻ một lần đọc trọng số (weights). Đồng thời, độ dài context, thời gian xếp hàng và thứ tự thực thi của mỗi request quyết định thời điểm người dùng thực sự nhận được câu trả lời. Chương này thảo luận cách tổ chức các request này sao cho trả về nhiều câu trả lời đúng hơn trong một ngân sách thời gian cho trước.

Một instance inference có thể chạy trên một card đơn, hoặc được thực thi đồng thời bởi nhiều card hoặc nhiều server. Chương 6 và 7 đã giải thích cách phép tính của model được tách nhỏ và cách các accelerator giao tiếp với nhau. Chương này lấy một tổ hợp accelerator cho trước làm điểm xuất phát và nghiên cứu batching (gom lô), lập lịch request, quản lý bộ nhớ, nén, offload và giải mã phỏng đoán (speculative decoding), so sánh hiệu quả thực thi của các cấu hình runtime khác nhau. Chương 9 sau đó thảo luận về nơi nên phân bổ tính toán và trạng thái, cách tổ chức các bản sao hoàn chỉnh, các nhóm dịch vụ phân tầng và phân công lao động theo toán tử, cùng cách xác định quy mô dịch vụ tương ứng.

Chương này bắt đầu từ một bài toán thiết kế cụ thể. Ví dụ instance Qwen3-8B chạy trên một GPU RTX PRO 6000 Blackwell Workstation Edition: 96 GB bộ nhớ GPU, băng thông 1792 GB/s, đỉnh dày đặc 503.8 TFLOP/s với đầu vào BF16 và tích lũy FP32, giới hạn công suất 600 W. Ngoại trừ Thí nghiệm 8-7 chạy trên Apple M2 Max, tất cả các thí nghiệm trong chương này đều chạy trên card này. Trọng số model dùng định dạng dấu phẩy động BF16 với 2 byte mỗi phần tử, chiếm khoảng 15.3 GiB. KV cache lưu khóa (K) và giá trị (V) của các token context, phục vụ cho các lần đọc sau này trong quá trình tính toán attention. Như trong Thí nghiệm 8-9, instance này được cấp 32 GiB bộ nhớ GPU, trong đó 12 GiB dành riêng cho KV cache và các buffer phụ trợ, phần còn lại khoảng 4.7 GiB dùng cho activations, các buffer cố định của tái phát đồ thị tính toán (computation graph replay) (Mục 5.5) và các không gian làm việc thực thi khác.

Instance xử lý hai loại request: hội thoại ngắn với đầu vào 2048 token, và các request context dài với đầu vào 8192 token; cả hai đều xuất ra 256 token. 6144 token đầu tiên của request dài là một system prompt và các định nghĩa công cụ (tool) giống hệt nhau. Mỗi request phải trả về một câu trả lời đầy đủ và đúng trong vòng 7 giây kể từ khi đến. Một request ngắn chạy một mình trên card này mất khoảng 6.95 giây (Mục 8.1.1), chỉ chừa lại khoảng 0.05 giây biên so với hạn chót này. Các điều kiện này sẽ được sử dụng lặp lại trong phần sau.

Điều kiện thiết kếHội thoại ngắnContext dài
Độ dài đầu vào2048 token8192 token
Độ dài đầu ra256 token256 token
Độ dài tiền tố chung06144 token
Hạn chót hoàn thành7 s7 s

Trước tiên chúng ta tính xem instance có thể đồng thời chứa KV cache tương ứng với bao nhiêu request, sau đó tính xem một batch lớn hơn tiết kiệm được bao nhiêu lượng đọc trọng số cho mỗi token đầu ra. Tiếp đó chúng ta giới thiệu paging, chia sẻ tiền tố, nén, offloading và giải mã phỏng đoán, phân tích từng kỹ thuật tiết kiệm được gì và thêm vào chi phí gì. Ở cuối chương, các phân tích này được áp dụng cho một câu hỏi duy nhất: nên chọn cấu hình nào khi 16 request dài đến đồng thời?

8.1 Quá trình thực thi và yêu cầu tài nguyên của các request inference

8.1.1 Tài nguyên thực thi và vòng đời của request

Một request là một đơn vị công việc inference có đầu vào, đầu ra và điều kiện hoàn thành riêng. Batch được thảo luận trong chương này đề cập đến nhóm các token đầu vào được tính toán cùng nhau trong một lần thực thi trên accelerator; một vòng lập lịch (scheduling iteration) là một chu kỳ trong đó bộ lập lịch (scheduler) chọn công việc của vòng hiện tại, đệ trình để thực thi và xử lý kết quả. Một request có thể trải qua nhiều vòng lặp, và các request khác được gom vào cùng batch với nó có thể thay đổi từ vòng này sang vòng khác.

Hình 8-1: A tiếp tục thực thi qua hai vòng lặp. B hoàn thành ở vòng đầu tiên, và ở vòng tiếp theo C thay thế vị trí thực thi của nó (mỗi vòng dành một hàng tính toán cho mỗi request đang hoạt động); trạng thái của request vẫn được lưu theo định danh riêng của nó.

Sau khi một request đến, nó trước tiên đi vào hàng đợi chờ. Chỉ sau khi bộ lập lịch thêm nó vào một batch và cấp không gian KV, model mới bắt đầu xử lý đầu vào — giai đoạn prefill. Vì các token đầu vào được cung cấp toàn bộ ngay từ đầu, model có thể tính toán nhiều token cùng một lúc và lưu khóa cùng giá trị của từng layer. Sau khi token cuối cùng của prompt được tính xong, model sinh ra token đầu ra đầu tiên.

Instance sau đó chuyển sang giai đoạn decode. Model lấy token vừa sinh ra làm đầu vào cho lượt lan truyền tiếp theo, đọc context KV và sinh token tiếp theo. Một request phải thực hiện các bước này một cách tuần tự, nhưng các bước hiện tại của các request khác nhau có thể được tính toán cùng nhau. Vì vậy dù một request chỉ có thể sinh token từng cái một, một instance vẫn có thể tăng tổng thông lượng bằng cách gom lô (batching).

Trên card này, Thí nghiệm 8-1 chạy đơn lẻ một request ngắn đầu vào 2K: prefill và trả về token đầu tiên mất khoảng 0.096 giây, sau đó trung bình mỗi 26.5 ms trả về một token. Giả sử request này chờ xếp hàng 0.1 giây đầu tiên, người dùng nhận được token đầu tiên sau 0.196 giây kể từ lúc đến. Với tổng cộng 256 token đầu ra, có 255 khoảng đầu ra, nên tổng thời gian của request là

0.1+0.096+255×0.0265=6.95 s.0.1+0.096+255\times0.0265=6.95\ \mathrm{s}.

Request này hoàn thành trong 7 giây. Nếu prefill của một request ngắn khác được chèn vào giữa quá trình sinh, một khoảng đầu ra phải chờ thêm khoảng 0.096 giây, kéo dài từ 26.5 ms lên khoảng 123 ms, và tổng thời gian tăng lên 7.05 giây. Phép tính cho mỗi token hoàn toàn không thay đổi — chỉ cần sắp xếp lại thứ tự thực thi cũng có thể đẩy một request vượt qua hạn chót của nó.

Hình 8-2 vẽ 6.95 giây này thành một trục thời gian. Khoảnh khắc token đầu tiên được trả về chia trục thời gian thành hai đoạn: mọi thứ trước nó — xếp hàng và xử lý đầu vào — quyết định khi nào người dùng thấy phản hồi, và 255 khoảng sau nó quyết định khi nào người dùng nhận được câu trả lời hoàn chỉnh. Mọi tối ưu hóa lập lịch được thảo luận sau này đều có thể được hiểu là làm thay đổi độ dài của một trong hai đoạn này.

Hình 8-2: Một request xuất ra 256 token. Xếp hàng mất 0.1 giây, prefill mất 0.096 giây, và mỗi khoảng đầu ra trong 255 khoảng tiếp theo là 26.5 ms (đo với batch size 1 trong Thí nghiệm 8-1). Thanh được vẽ theo tỉ lệ thời gian thực, các chấm đánh dấu token đầu tiên và cuối cùng, và đường đứt nét tại 7 giây đánh dấu hạn chót.

Khi một request hoàn thành, bộ lập lịch không còn lập lịch tính toán cho nó, và trình quản lý trạng thái quyết định xử lý KV để lại như thế nào. KV chỉ riêng của request đó được giải phóng khi request kết thúc, trong khi KV của một tiền tố chung được chia sẻ có thể được giữ lại để tái sử dụng cho các request sau, tiếp tục được quản lý theo chính sách cache. Mục 8.3 giải thích thêm khi nào cache này được chia sẻ và khi nào nó được giải phóng.

8.1.2 Yêu cầu dung lượng lưu trữ đối với trọng số, KV và các buffer runtime

Để đẩy nhiều request tiến triển theo trục thời gian riêng, một instance phải đồng thời giữ trọng số, trạng thái request và các kết quả tính toán trung gian. Ví dụ trong mục này dùng một card đơn, nên chỉ liệt kê footprint bộ nhớ trên chính card này. Một instance nhiều card phải liệt kê riêng các mảnh trọng số (weight shards), KV và buffer được giữ trên từng card: bộ nhớ trống trên một card chỉ có thể được dùng cho phép tính trên card khác nếu vị trí dữ liệu được thay đổi.

Gọi MwM_w là dung lượng của trọng số thường trú, MKVM_{\mathrm{KV}} là tổng dung lượng khối KV vật lý sau khi khử trùng lặp các tham chiếu được chia sẻ (KV được cấp theo các khối kích thước cố định, và các khối được nhiều request chia sẻ chỉ được đếm một lần), MaM_a là dung lượng của activations và không gian làm việc chung, và MuM_u là dung lượng của các buffer khác như tái phát đồ thị tính toán. Tổng các footprint bộ nhớ này phải nhỏ hơn hoặc bằng dung lượng khả dụng CC của accelerator:

Mw+MKV+Ma+Mu≤C.M_w+M_{\mathrm{KV}}+M_a+M_u\le C.

Trong số các footprint bộ nhớ này, trọng số được nạp vào bộ nhớ trước khi bất kỳ request nào đến, trong khi KV tiếp tục tăng khi các request đến và sinh đầu ra. Để xác định instance có thể xử lý đồng thời bao nhiêu request, trước tiên ta tính mỗi token cần lưu bao nhiêu KV. Qwen3-8B có 36 layer, 8 KV head mỗi layer, và chiều head là 128. Mỗi token phải lưu K và V tại mọi layer, và BF16 dùng 2 byte mỗi phần tử, nên dung lượng KV cần thiết mỗi token là

k=2×36×8×128×2=147 456 bytes=144 KiB.k=2\times36\times8\times128\times2 =147\,456\ \mathrm{bytes}=144\ \mathrm{KiB}.

Đầu vào 2048 token chiếm 288 MiB KV; đầu vào 8192 token chiếm 1152 MiB. Tất cả các request chia sẻ khoảng 15.3 GiB trọng số, nhưng mỗi request dài bổ sung với context độc lập thêm 1.125 GiB KV. Mười sáu request dài chỉ riêng để giữ KV đầu vào đã cần 18 GiB, vượt quá 12 GiB dành riêng.

KV tiếp tục tăng khi đầu ra được sinh ra. Prefill tạo ra token đầu ra đầu tiên, sau đó 255 token đầu ra trước đó được đưa ngược vào model từng cái một để sinh 255 token còn lại. Một request ngắn tính tổng cộng 2048+255=23032048+255=2303 token, và một request dài tính tổng cộng 8192+255=84478192+255=8447 token. Với 16 token mỗi khối, chúng cần không gian được cấp để chứa lần lượt 2304 và 8448 token.

Request độc lậpKV sau prefillKV được cấp sau 256 token đầu raSố request khớp trong 12 GiB đồng thời
Hội thoại ngắn288 MiB324 MiB37
Context dài1152 MiB1188 MiB10

Cột cuối cùng đến từ ⌊12288/324⌋\lfloor12288/324\rfloor và ⌊12288/1188⌋\lfloor12288/1188\rfloor tương ứng. Nếu chỉ tính dựa trên kích thước KV tại cuối prefill, 12 GiB có thể chứa 42 request ngắn; nhưng một khi tất cả các request này đã sinh đủ 256 token đầu ra, không gian cần thiết tăng lên 13,608 MiB. Việc dành trước không gian cần cho giai đoạn sinh tại thời điểm request được tiếp nhận giúp tránh phát hiện thiếu hụt bộ nhớ giữa chừng quá trình thực thi.

Chia sẻ một tiền tố chung có thể tiết kiệm thêm bộ nhớ. 6144 token đầu tiên của một request dài chiếm 864 MiB KV, và phần đầu vào còn lại chiếm 288 MiB. Nếu 16 request mỗi cái lưu bản sao tiền tố của riêng mình, 16 bản sao KV giống hệt nhau được lưu; nếu chúng thay vào đó tham chiếu cùng một KV, không gian tiết kiệm được có thể dùng cho phần hậu tố riêng của từng request. Mục 8.3 sẽ mô tả sơ đồ ánh xạ khối cụ thể và tính tổng dung lượng tại cuối giai đoạn sinh.

8.1.3 Từ thời gian của một request đến các mục tiêu dịch vụ

Bây giờ chúng ta chuyển hai đoạn của trục thời gian trong Mục 8.1.1 thành các chỉ số có thể dùng để đặt hạn chót. Gọi tat_a là thời điểm request đến, GG là tổng số token đầu ra, và t1t_1, tGt_G là các thời điểm token đầu tiên và cuối cùng được trả về cho người dùng. Khi đó

TTFT=t1−ta,\mathrm{TTFT}=t_1-t_a,

và khoảng đầu ra thứ jj là ITLj=tj+1−tj\mathrm{ITL}_j=t_{j+1}-t_j. Cộng thời gian trước và sau token đầu ra đầu tiên, tổng thời gian của request là

Trequest=TTFT+∑j=1G−1ITLj.T_{\mathrm{request}}=\mathrm{TTFT}+\sum_{j=1}^{G-1}\mathrm{ITL}_j.

Trong ví dụ ở Mục 8.1.1, TTFT là 0.196 giây, và 255 khoảng đầu ra tiếp theo tổng cộng 6.76 giây. TTFT ngắn hơn giúp người dùng thấy phản hồi sớm hơn, các khoảng đầu ra đều đặn giúp việc đọc liên tục dễ dàng hơn, và tổng thời gian quyết định khi nào câu trả lời hoàn chỉnh có sẵn. Một SLO biến các yêu cầu này thành các hạn chót. Ví dụ mở đầu của chương này yêu cầu tổng thời gian không vượt quá 7 giây; các ví dụ lập lịch dưới đây cũng sẽ so sánh khoảng đầu ra dài nhất.

Các chỉ số này áp dụng cho một request; đầu ra tổng thể của instance cũng phụ thuộc vào số request được xử lý cùng lúc — batch size (sau đây gọi là batch). Thông lượng là lượng công việc hoàn thành trong một đơn vị thời gian. Khi Thí nghiệm 8-1 xử lý đồng thời 16 request 2K, mỗi vòng decode mất khoảng 27.44 ms và tạo ra 16 token mỗi vòng, cho thông lượng sinh khoảng 16/0.02744≈58316/0.02744\approx583 token/s; khi một request đơn tạo ra một token mỗi 26.26 ms, giá trị này chỉ khoảng 38. Tăng số đầu ra mỗi vòng hoặc rút ngắn thời gian mỗi vòng đều làm tăng thông lượng; người dùng thực sự phải chờ bao lâu còn phụ thuộc vào thời gian xếp hàng trước khi request bắt đầu thực thi.

Đánh giá chất lượng dịch vụ cũng đòi hỏi kiểm tra xem câu trả lời có đúng không. Cố định độ dài đầu ra giúp so sánh thời gian thực thi dễ hơn trong cùng khối lượng công việc; để model dừng tự nhiên thay vào đó cho phép quan sát độ đúng của câu trả lời, độ dài phản hồi và số lần thử lại. Mục 8.6 tính số request hoàn thành đúng và đúng hạn trong một đơn vị thời gian, đánh giá chung cả tốc độ lẫn chất lượng.

8.2 Batching và lập lịch request

8.2.1 Việc tái sử dụng trọng số cải thiện hiệu quả batching như thế nào

Xét phép nhân ma trận Y=XWY=XW, với ma trận đầu vào XX có bb hàng. Cùng một phần tử trọng số tham gia vào phép tính cho mọi hàng, nên xử lý nhiều hàng cùng nhau cho phép hoàn thành nhiều phép nhân-cộng hơn cho mỗi lần đọc trọng số. Giả sử mỗi phần tử trọng số chỉ được đọc một lần mỗi batch, mỗi phép nhân-cộng tính là hai phép toán, và mỗi phần tử trọng số chiếm sws_w byte. Chỉ tính riêng lượng đọc trọng số, cường độ số học (arithmetic intensity) là

Iweight≈2bsw.I_{\mathrm{weight}}\approx\frac{2b}{s_w}.

Với BF16, sw=2s_w=2. Tăng số request cùng thực thi một bước decode từ 1 lên 16 — mỗi request cung cấp một token mới, và vector đặc trưng của mỗi token tạo thành một hàng của ma trận đầu vào — làm ma trận lớn lên từ một hàng thành 16 hàng, nên lượng công việc thực hiện trên mỗi lần đọc trọng số tăng lên 16 lần giá trị ban đầu. Các request khác nhau chia sẻ ma trận trọng số, nhưng phép tính attention vẫn phải đọc riêng context KV của từng request.

Điều kiện tiên quyết để chia sẻ là nhóm batch này dùng cùng một bộ trọng số. Low-rank adaptation (LoRA) nới lỏng điều kiện tiên quyết này: nó biểu diễn một hiệu chỉnh trọng số thành tích của hai ma trận nhỏ trong khi giữ nguyên trọng số nền, nên các tác vụ khác nhau có thể chia sẻ một model nền và mỗi tác vụ chỉ gắn thêm một adapter nhỏ (một module tham số huấn luyện được gắn vào model nền). Với projection Q và V của Qwen3-8B, dùng rank thấp chiều 16 và BF16, một adapter chiếm 14.6 MiB, và 100 adapter cùng nhau chiếm 1.43 GiB — nhỏ hơn nhiều so với 14.1 GiB trọng số ma trận.

Nếu một batch gồm các request từ các adapter khác nhau, thứ duy nhất còn được chia sẻ là trọng số nền. Phép tính rank thấp phải được nhóm theo adapter, và số hàng của mỗi nhóm là số request trong batch này thuộc về adapter đó; adapter càng phân tán thì càng ít hàng mỗi nhóm, và khả năng tái sử dụng trọng số cho phần tính toán này càng kém. Vậy nên có bao nhiêu adapter và đặt bao nhiêu request vào một batch là hai quyết định riêng biệt.

Dung lượng cũng phải được tính riêng. 100 adapter chiếm 1.43 GiB, trong khi 100 context 8K không chia sẻ lẫn nhau chiếm tổng cộng 112.5 GiB KV. Bao nhiêu adapter vừa đủ được quyết định bởi các ma trận nhỏ, trong khi bao nhiêu request có thể được phục vụ đồng thời vẫn được quyết định bởi KV; dư địa lớn ở trước không hàm ý dư địa lớn ở sau.

Gọi DwD_w là kích thước các trọng số ma trận cần đọc chính xác một lần mỗi vòng, mỗi request có LL token context không chia sẻ KV với nhau, và kk là dung lượng KV mỗi token. Tổng lượng đọc cho một vòng, và lượng đọc trung bình quy cho mỗi token đầu ra, lần lượt là

D(b,L)=Dw+bLk,d(b,L)=Dwb+Lk.D(b,L)=D_w+bLk,\qquad d(b,L)=\frac{D_w}{b}+Lk.

Trong lượng đọc mỗi token đầu ra, số hạng thứ nhất là lượng đọc trọng số được phân bổ, và số hạng thứ hai là lượng đọc context KV của request này. Qwen3-8B có DwD_w khoảng 14.1 GiB, và KV cho context 2K là 0.28125 GiB. Khi batch tăng từ 1 lên 16, lượng đọc trọng số phân bổ cho mỗi token đầu ra giảm xuống còn khoảng 0.88 GiB; khi batch tăng lên 64, nó giảm tiếp xuống khoảng 0.22 GiB. Tại thời điểm này, mỗi token đầu ra đọc nhiều KV hơn trọng số.

Hình 8-3 cho thấy sự thay đổi này. Các vạch liền kề trên trục hoành biểu thị việc nhân đôi batch size, nên số hạng trọng số giảm một nửa mỗi lần; số hạng KV không đổi, và tổng của hai số hạng dần tiến về đường ngang chỉ riêng KV. Với context 2K, khi batch tăng từ 1 lên 64, lượng đọc mỗi token đầu ra giảm từ khoảng 14.38 GiB xuống 0.50 GiB — khoảng 1/29 giá trị ban đầu. Khi độ dài context tăng lên 8K, dung lượng KV trở nên lớn gấp bốn lần, và ngay cả sau batching, mỗi token đầu ra vẫn phải đọc nhiều dữ liệu hơn.

Hình 8-3: Batching giảm lượng đọc cần thiết cho mỗi token sinh ra như thế nào. Trục hoành là số request được sinh đồng thời; trục tung là tổng số byte đọc trong một batch chia cho số token đầu ra của vòng đó. Các đường liền màu xanh lam và xanh lục tương ứng với các request có context hiện có là 2048 và 8192 token, bao gồm cả lượng đọc trọng số và KV; các đường chấm ngang cùng màu chỉ thể hiện lượng đọc KV tại độ dài đó. Đường đứt nét màu xám thể hiện lượng đọc trọng số dùng chung trong một batch chia cho số request. Model là Qwen3-8B dùng BF16; mỗi trọng số ma trận được đọc một lần mỗi vòng, và KV của mỗi request là độc lập. Cả hai trục đều dùng thang logarit.

Khi batch tăng, lợi ích từ việc phân bổ trọng số dần giảm đi. Điều này có thể được mô tả bằng batch tại đó lượng đọc trọng số bằng lượng đọc KV. Batch tối thiểu tại đó tổng lượng đọc KV đạt hoặc vượt lượng đọc trọng số dùng chung là

bKV=⌈DwLk⌉.b_{\mathrm{KV}}=\left\lceil\frac{D_w}{Lk}\right\rceil.

Tính với số byte trọng số chưa làm tròn, kết quả là 51 cho context 2K và 13 cho context 8K. Khi độ dài context tăng gấp bốn lần, lượng đọc KV vượt lượng đọc trọng số ở batch nhỏ hơn nhiều. Đồng thời, batch lớn hơn cũng cần nhiều bộ nhớ hơn: 64 request 2K, cùng với trọng số và KV sau một bước decode, chiếm tổng cộng khoảng 35.7 GB. Batching giảm lượng đọc cho mỗi token đầu ra, nhưng tăng lượng dữ liệu cư trú cùng lúc.

Bây giờ chúng ta ước tính thời gian cần cho các lần đọc này. Gọi FF là phép tính cho một request, PP là số phép toán accelerator hoàn thành mỗi giây, và β\beta là băng thông bộ nhớ. Tính riêng thời gian cần cho phép tính và cho việc đọc, rồi lấy giá trị lớn hơn trong hai giá trị để ước tính thời gian một vòng:

Tstep≈max⁡(bFP,Dw+bLkβ).T_{\mathrm{step}}\approx\max\left(\frac{bF}{P},\frac{D_w+bLk}{\beta}\right).

Đặt hai số hạng bằng nhau và biến đổi cho ta b(F/P−Lk/β)=Dw/βb(F/P-Lk/\beta)=D_w/\beta. Mỗi request bổ sung thêm F/PF/P vào thời gian tính toán và Lk/βLk/\beta vào thời gian đọc KV. Khi số hạng trước lớn hơn, thời gian tính toán tăng theo batch size cho đến khi bắt kịp thời gian đọc; ngược lại, việc đọc vẫn là số hạng tốn thời gian hơn trong suốt quá trình. Các giá trị 51 và 13 ở trên so sánh khối lượng của hai loại dữ liệu được đọc; phương trình này thay vào đó so sánh cách tính toán và băng thông bộ nhớ mỗi loại giới hạn thời gian thực thi như thế nào.

Cả hai số hạng đều được tính bằng đỉnh tính toán và đỉnh băng thông tương ứng, cho cận dưới vật lý của card này trong các điều kiện này. Tỉ số giữa cận dưới này và thời gian đo được của một vòng chính là MFU và MBU được định nghĩa ở Mục 1.2.2; Mục 8.6.3 sẽ dùng các bản ghi từng vòng để hiệu chỉnh hai tỉ số này, rồi áp dụng chúng cho các ví dụ còn lại của chương này.

Việc quy đổi điều này sang thời gian request cũng yêu cầu kiểm tra xem batch lớn hơn có làm trễ token đầu tiên không, và khoảng cách giữa các đầu ra liên tiếp có dài hơn không. Thí nghiệm quét batch ghi lại đồng thời cả hai thay đổi này. Với Qwen3-8B đầu vào 2K và đầu ra 256 token, khi batch tăng từ 1 lên 64, tổng thông lượng tăng từ khoảng 37 lên 1165 token/s; nhưng thời gian trả về token đầu tiên bị trễ từ khoảng 96 ms lên 3.29 giây, và khoảng đầu ra trung bình tăng từ khoảng 27 lên 40 ms. Dù mỗi vòng tạo ra nhiều đầu ra hơn, việc xử lý nhiều đầu vào hơn và chạy batch lớn hơn cũng tốn nhiều thời gian hơn. Trong thí nghiệm này, mỗi batch các request đến cùng lúc; một dịch vụ trực tuyến còn chờ để lấp đầy batch sẽ thêm thời gian chờ này vào độ trễ token đầu tiên.

Bài tập 8-1 · Tính toán: lượng đọc thay đổi thế nào khi batch tăng? Lấy Dw=15,136,811,008D_w=15,136,811,008 byte và k=144k=144 KiB, và tính lượng đọc mỗi token đầu ra cho context 2K và 8K tại các batch size 1, 4, 16 và 64. Với mỗi độ dài context, tìm batch nguyên nhỏ nhất tại đó lượng đọc KV lần đầu đạt hoặc vượt lượng đọc trọng số được phân bổ. Sau đó, dùng 12 GiB bộ nhớ KV khả dụng, 16 token mỗi khối và 256 token đầu ra, tìm số request độc lập tối đa có thể được chứa cho mỗi độ dài context. So sánh số request tối đa này với batch size tại đó hai lượng đọc trở nên bằng nhau, và xác định xem ràng buộc dung lượng có ngăn batch tăng lớn đến mức đó ngay từ đầu hay không.

Nếu thay vào đó trọng số được phục vụ từ một bộ lưu trữ độc lập đủ nhanh, thời gian đọc sẽ giảm, và lợi ích của batching cần được tính lại. Theo ví dụ 8K trong Chương 4, gọi WW là số byte trọng số được đọc trong toàn bộ batch, BB là số request trong batch, và KK là số byte KV được đọc cho mỗi request trong bước này. Trong sơ đồ thông thường, lượng đọc HBM được phân bổ cho mỗi token đầu ra là W/B+KW/B+K; khi trọng số được phục vụ từ một ROM độc lập đủ nhanh, HBM vẫn phải đọc KK cho mỗi token đầu ra. Trong Hình 8-4, khoảng cách giữa hai đường thu hẹp khi batch tăng, cho thấy lợi ích trước đây đạt được bằng cách dùng batch size để pha loãng lượng đọc trọng số đang biến mất.

Tuy nhiên, batch size vẫn ảnh hưởng đến mức độ phép tính ma trận tận dụng các đơn vị tính toán, cũng như lượng đọc KV trong batch. Thay lượng đọc mới vào mô hình thời gian ở trên cho phép ta tìm điểm giao nhau giữa phép tính và lượng đọc KV, rồi chọn batch size dựa trên độ trễ token đầu tiên và khoảng đầu ra.

Hình 8-4: Lượng đọc HBM được phân bổ cho mỗi token đầu ra. Với context 8K cố định, đường thông thường là W/B+K và đường ROM nhanh độc lập là K; trục tung được tính bằng công thức trên. W là số byte trọng số được đọc trong toàn bộ batch, B là số request trong batch, và K là số byte KV được đọc cho mỗi request trong bước này; W/B+K là lượng đọc được phân bổ cho mỗi token đầu ra.

8.2.2 Từ batch cố định đến continuous batching

Hai request bắt đầu sinh cùng lúc; một request cần tám token, request kia chỉ cần hai. Khi request ngắn hoàn thành, request dài vẫn còn sáu bước. Nếu ta khăng khăng chờ cả hai request hoàn thành trước khi bắt đầu nhóm tiếp theo, vị trí thực thi được giải phóng bởi request ngắn sẽ ngồi không suốt thời gian đó. Fixed batching tiếp nhận và hoàn thành cả một nhóm cùng nhau; continuous batching thay vào đó tổ chức lại các request đang hoạt động tại mỗi ranh giới vòng lặp, tiếp nhận các request mới ngay khi một vị trí được mở ra.

Phân tích batch trong Mục 8.2.1 giả định bb request tham gia tính toán mỗi vòng, nhưng trong thực tế số hàng thay đổi khi các request hoàn thành và các request mới được tiếp nhận. Vị trí thực thi trong Hình 8-1 tương ứng với một hàng tính toán ở đây, một tài nguyên khác với các vị trí request giao tiếp được ghi lại trong Chương 7. Việc các request mới có thể được tiếp nhận kịp thời hay không quyết định bao nhiêu hàng có thể tham gia tính toán trong vòng tiếp theo.

Tiếp nhận các request mới sớm hơn cũng có nghĩa là phép tính đầu vào của chúng chiếm accelerator sớm hơn. Nếu một request mới mang prompt dài, prefill của nó kéo dài khoảng chờ giữa hai bước decode của các request hiện có. Bộ lập lịch do đó phải phân chia thời gian accelerator giữa hai việc: xử lý đầu vào của các request mới, và để các request hiện có tiếp tục sinh.

Ví dụ: vì sao continuous batching rút ngắn tổng thời gian nhưng lại kéo dài các khoảng đầu ra? Có nhiều nhất hai request chạy cùng lúc. r0 và r1 đến tại thời điểm 0, mỗi cái có đầu vào 2048 token, lần lượt xuất ra 8 và 2 token; r2 đến tại 20 ms với đầu vào 8192 token và xuất ra 2 token; r3 đến tại 30 ms với đầu vào 2048 token và xuất ra 4 token. Thời gian mỗi vòng được khớp từ các phép đo batch-1 trong Thí nghiệm 8-1: 26.22 ms cố định, cộng 30.4 μs cho mỗi token mới, cộng 3 ns cho mỗi cặp query-key thỏa mãn quan hệ nhân quả. Các tham số này tái tạo 26.26 ms đo được của một vòng decode và 94.8 ms của prefill khi một request 2K chạy một mình. Thành phần cố định là phần của một vòng không phụ thuộc vào số token, bao gồm đọc toàn bộ bộ trọng số (khoảng 8.4 ms tại băng thông đỉnh 1792 GB/s) và chi phí đệ trình kernel từng cái một.

Fixed batching khiến r2 và r3 phải chờ toàn bộ nhóm r0/r1 hoàn thành, cho tổng thời gian khoảng 870 ms. Continuous batching tiếp nhận r2 ngay khi r1 hoàn thành, giảm tổng thời gian xuống khoảng 766 ms; nhưng đầu ra tiếp theo của r0 phải chờ prefill 8K của r2, và khoảng đầu ra lớn nhất tăng từ khoảng 26 ms lên 376 ms. Tổng thời gian giảm khoảng 12%, trong khi một lần dừng đầu ra tăng lên 14 lần độ dài ban đầu.

Hình 8-5: Fixed batching chờ toàn bộ nhóm hoàn thành trước khi tiếp nhận r2 và r3. Màu xanh lam là prefill, màu xanh lục là decode, và các hình tam giác đánh dấu thời điểm đến. Độ rộng của mỗi dải màu là thời lượng của toàn bộ vòng lập lịch, với các thời gian được tính từ các phép đo khớp trên RTX PRO 6000.

Hình 8-6: Continuous batching tiếp nhận r2 ngay khi r1 hoàn thành. Mỗi hàng là một request; màu xanh lam là xử lý đầu vào (prefill), màu xanh lục là sinh đầu ra (decode), các hình tam giác đánh dấu thời điểm request đến, và độ rộng của mỗi dải là thời lượng của vòng lập lịch mà nó thuộc về. r0 chờ xử lý đầu vào 8K của r2, và khoảng đầu ra dài nhất tăng lên khoảng 376 ms; tất cả các request hoàn thành trong khoảng 766 ms.

Tiến xa hơn, chia prefill của r2 thành các khối 2048 token mỗi khối, xử lý nhiều nhất 4096 token mỗi vòng (cùng cài đặt như trong Thí nghiệm 8-1), và để các request hiện có tiếp tục decode trước khi mỗi khối đầu vào mới được xử lý, khoảng đầu ra lớn nhất giảm xuống khoảng 133 ms, và tất cả các request hoàn thành với tổng thời gian khoảng 844 ms. Sau khi chia khối, bộ lập lịch có thể thường xuyên hơn để các request hiện có tiếp tục sinh, làm cho các khoảng đầu ra đều đặn hơn.

Hình 8-7: Chia một đầu vào dài thành các khối, để các request hiện có sinh trước mỗi vòng trước khi xử lý một khối đầu vào mới. Mỗi hàng là một request; màu xanh lam là xử lý đầu vào, màu xanh lục là sinh đầu ra, và các hình tam giác đánh dấu thời điểm đến. Việc chia khối giảm khoảng đầu ra dài nhất xuống khoảng 133 ms, với tất cả các request hoàn thành trong khoảng 844 ms. Trục hoành dùng cùng thang thời gian như Hình 8-5 và 8-6.

8.2.3 Sự can nhiễu của prefill dài với quá trình sinh, và thực thi chia khối

Khối prefill được xử lý cùng lúc càng lớn, số vòng xử lý đầu vào mà một request mới cần càng ít; khối càng nhỏ, các request đang chạy càng sớm có cơ hội thực thi tiếp theo. Các bộ lập lịch thường lập lịch trước các request đang decode, sau đó dùng phần còn lại của ngân sách token của vòng này để xử lý prefill. Các đầu vào dài do đó được trải rộng ra trên phép tính của nhiều khoảng đầu ra.

Ngay cả với độ dài khối cố định, khối lượng công việc attention vẫn tăng theo độ dài context. Giả sử khối hiện tại chứa cc token mới, với độ dài context hiện có là hh. Token mới đầu tiên attention đến h+1h+1 token, token thứ hai đến h+2h+2, và token cuối đến h+ch+c. Cộng từng cặp lại từng số hạng:

Npair=(h+1)+(h+2)+⋯+(h+c)=ch+c(c+1)2.N_{\mathrm{pair}}=(h+1)+(h+2)+\cdots+(h+c) =ch+\frac{c(c+1)}{2}.

Số hạng chch đến từ việc các token mới truy cập context cũ, và số hạng tam giác đến từ attention nhân quả trong khối. Lấy c=512c=512: 131,328 cặp của khối đầu tiên đến hoàn toàn từ bên trong khối; khối cuối cùng của một đầu vào 8K đã có 7680 token context, và số cặp của nó tăng lên 4,063,488 — khoảng 31 lần khối đầu tiên.

Hình 8-8 mô tả số cặp dưới dạng diện tích: mỗi token mới phải truy cập toàn bộ context cũ, tạo thành hình chữ nhật bên trái; trong khối, một token chỉ có thể attention đến các vị trí tại hoặc trước vị trí của chính nó, tạo thành hình tam giác bên phải. Với độ dài khối cố định, hình tam giác bên phải không đổi, trong khi hình chữ nhật bên trái mở rộng khi context tăng. Đây là lý do khối cuối cùng cần nhiều phép tính attention hơn.

Hình 8-8: Khi xử lý 4 token lần đầu tiên, không có context cũ — chỉ có các cặp attention nhân quả giữa các token mới, tạo thành 1 + 2 + 3 + 4 = 10 ô màu xanh lục. Các ô trắng biểu thị các token tương lai, không tham gia vào query hiện tại.

Hình 8-9: Với context hiện có là 8 token, 4 token mới tạo thành 4 × 8 = 32 ô màu xanh lam với context cũ, trong khi phần trong khối vẫn là 10 ô màu xanh lục. Với cùng độ dài khối, tổng số cặp tăng từ 10 lên 42.

Model cũng phải chạy các projection và FFN; phép biến đổi đặc trưng theo token này xử lý 512 token mới mỗi khối, nên khối lượng công việc của nó gần như không đổi. Trong các bản ghi từng khối đã thu thập, phép tính ma trận thân model (model backbone) của khối cuối cùng lớn hơn khoảng 32% so với khối đầu tiên, với thời gian thực thi trung vị tăng từ khoảng 25.3 ms lên 35.1 ms — tăng khoảng 39%. Thời gian thực thi tổng thể của khối do đó được quyết định chung bởi hai phần: một phần thay đổi chủ yếu theo số token mới, và một phần khác cũng tăng theo độ dài context hiện có.

Sự tăng trưởng của context giải thích vì sao các khối cùng kích thước ngày càng chậm; thu nhỏ độ dài khối lại mang đến một loại chi phí khác. Chia một khối 256 token thành hai khối 128 token tăng gấp đôi số cơ hội lập lịch và giảm một nửa số hàng ma trận, nhưng mỗi khối nhỏ hơn vẫn phải dùng cùng một bộ trọng số. Lấy trọng số FFN 288 MiB của một layer làm ví dụ: nếu hai lần thực thi mỗi lần đọc riêng chúng từ bộ nhớ GPU, phần đọc này tăng từ 288 lên 576 MiB. Các khối nhỏ hơn cho các request đang chạy cơ hội thực thi sớm hơn, nhưng cũng tăng số lần thực thi; bộ lập lịch nên chọn khối lớn nhất vẫn thỏa mãn yêu cầu khoảng đầu ra, để giảm thiểu các lần khởi chạy và đọc thêm.

8.2.4 Ngân sách token, shape động và thời gian thực thi

Các bộ lập lịch thường đặt giới hạn trên cho số token được xử lý mỗi vòng, gọi là ngân sách token; 4096 token được xử lý tối đa mỗi vòng trong ví dụ chia khối ở Mục 8.2.2 là một trường hợp của điều này. Tuy nhiên, thực thi thực tế phải ghép các vector đặc trưng của các token này thành một ma trận và chọn một kernel hoặc một CUDA Graph đã được ghi trước (Mục 5.5.2). Ma trận thực sự được thực thi có thể lớn hơn thứ vòng này thực sự cần, nên xử lý bớt một token không nhất thiết có nghĩa là làm ít phép tính hơn.

Ví dụ, giả sử các graph được ghi trước cho 16 hàng và 32 hàng, và các đầu vào từ 17 đến 32 hàng đều được padding lên 32 hàng. Giảm khối lượng công việc từ 18 hàng xuống 17 hàng vẫn thực thi graph 32 hàng; chỉ khi giảm tiếp từ 17 hàng xuống 16 hàng, graph nhỏ hơn mới được dùng. Ghi một graph riêng cho 17 hàng sẽ loại bỏ 15 hàng padding, nhưng việc ghi graph đó cần thời gian chuẩn bị, và buffer dùng để tái phát cũng phải được giữ trong bộ nhớ lâu dài. Do đó, khi chọn các shape định sẵn cho CUDA Graphs, chi phí bộ nhớ runtime từ Mục 8.1 cũng phải được tính đến.

Shape thực thi quyết định một vòng mất bao lâu, trong khi giới hạn token quyết định vòng này giao bao nhiêu công việc cho các request mới. Nâng giới hạn giúp các đầu vào dài hoàn thành xử lý sớm hơn, nhưng các request đang chạy phải chờ đến khi vòng này kết thúc mới có thể tiếp tục sinh đầu ra. Trong một bản ghi tái phát của sáu request, các request đến mỗi 80 ms. Khi giới hạn token mỗi vòng tăng từ 512 lên 8192, thời gian xếp hàng trong engine cho các request đến sau giảm từ khoảng 149 ms xuống dưới 0.1 ms, trong khi khoảng đầu ra dài nhất thay vào đó tăng từ khoảng 32 ms lên 183 ms. Giới hạn càng cao, một vòng hoàn thành càng nhiều đầu vào, các request mới rời hàng đợi càng sớm — và các request đang chạy chờ đầu ra tiếp theo càng lâu.

Việc quyết định xử lý bao nhiêu token mỗi vòng do đó có thể được thực hiện theo hai bước. Đầu tiên xác định một vòng được phép chạy bao lâu dựa trên khoảng đầu ra cho phép, sau đó kết hợp điều này với độ dài context và shape thực thi định sẵn để quy đổi thành số token mới có thể xử lý. Trong cùng một khoảng thời gian, các request có context dài hơn có thể xử lý ít token mới hơn, trong khi các request có context ngắn hơn có thể dùng các khối lớn hơn. Khi lịch thực thi được quyết định, không gian KV vẫn phải được cấp cho các request này; mục tiếp theo thảo luận cách cấp phát và tái sử dụng bộ nhớ này.

Bài tập 8-2 · Cốt lõi · Phân tích: xác định kích thước khối prefill từ ràng buộc khoảng đầu ra. Dùng bốn request và mô hình thời gian thực thi từ Mục 8.2.2, tính bằng tay thời gian prefill ban đầu và bước decode tiếp theo của r0 và r1, sau đó tính thời lượng của vòng trong đó continuous batching tiếp nhận r2. Giải thích nguồn gốc của khoảng đầu ra dài nhất. Với độ dài khối mới cố định là c=512c=512, tính số cặp attention cho các độ dài context hiện có h=0h=0 và h=7680h=7680 tương ứng. Cuối cùng, phân tích bản ghi tái phát sáu request kèm theo, chọn giới hạn token mỗi vòng dưới mỗi một trong hai mục tiêu “khoảng dài nhất dưới 50 ms” và “tiếp nhận các request đến sau càng sớm càng tốt”, và xác định chi phí thời gian nào quyết định mỗi lựa chọn.

8.3 Cấp phát, tái sử dụng và giải phóng KV cache

8.3.1 Dành trước, phân mảnh và cấp phát phân trang (paged allocation)

Mục 8.1 dành trước không gian cho mỗi request dựa trên độ dài đầu ra tối đa, đảm bảo nó có thể tiếp tục sinh. Nếu một request kết thúc sớm, phần đuôi không gian dành trước của nó sẽ vĩnh viễn bỏ phí. Một phương án thay thế là mở rộng bộ nhớ dần khi quá trình sinh tiến hành, nhưng nếu không gian phải liên tục, các vùng liền kề bị các request khác chiếm giữ sẽ đòi hỏi phải di chuyển dữ liệu hoặc chờ đợi.

Loại cấp phát này có thể được ví như một quyển sổ lỏng lẻo: thứ tự đọc được cố định bởi số trang, nhưng các trang không cần nằm ở các vị trí vật lý liền kề — người ta chỉ cần nhớ mỗi trang nằm ở đâu. Khi một chuỗi tăng trưởng, một trang mới có thể được đặt vào bất kỳ vị trí trống nào với vị trí của nó được ghi lại, tránh việc phải dịch chuyển toàn bộ quyển sổ chỉ để giữ cho các trang liên tục.

Paged allocation chia một chuỗi thành các khối kích thước cố định. Một khối logic được đánh số theo vị trí trong chuỗi, tương tự như số trang; một khối vật lý là vùng bộ nhớ thực sự chứa KV; một bảng khối (block table) theo từng request ghi lại mỗi khối logic ánh xạ đến khối vật lý nào. Khi một chuỗi tăng trưởng đủ để cần một khối mới, engine cấp một khối vật lý từ nhóm tự do và thêm ánh xạ. Phép tính attention tra bảng khối để tìm KV tại vị trí cần thiết, nên các khối vật lý có thể nằm rải rác trong bộ nhớ. Điều được thảo luận ở đây là định địa chỉ KV theo khối trong bộ nhớ; việc swap-in và swap-out giữa các tầng của phân cấp bộ nhớ được để lại cho Mục 8.3.4 và 8.4.3.

Hình 8-10: Các khối logic 0, 1, 2 tạo thành chuỗi theo thứ tự; bảng khối trỏ chúng đến các khối vật lý 2, 0, 3 tương ứng. Khối vật lý 1 là tự do. Attention khôi phục thứ tự logic thông qua bảng khối.

Gọi mỗi khối chứa KV của pp token, và độ dài hiện tại là LL; khi đó cần ⌈L/p⌉\lceil L/p\rceil khối. Chỉ khối cuối cùng có các vị trí KV trống ở phần đuôi, lãng phí

p⌈Lp⌉−L,p\left\lceil\frac{L}{p}\right\rceil-L,

nhiều nhất KV tương đương p−1p-1 token. Khối càng nhỏ, cận trên này càng thấp, nhưng càng nhiều mục bảng khối và phép toán cấp phát. Năm 2023, các nhà nghiên cứu tại Berkeley và các tổ chức khác đề xuất PagedAttention trong vLLM, áp dụng ý tưởng quản lý bộ nhớ phân trang kiểu hệ điều hành vào KV cache, giảm ràng buộc mà không gian dành trước và phân mảnh bộ nhớ đặt lên concurrency phục vụ. PagedAttention đưa phép ánh xạ này vào chính việc thực thi attention; FlashAttention, ngược lại, tổ chức tính toán theo khối và các kết quả trung gian bên trong toán tử — hai cách này tác động lần lượt lên việc cấp phát trạng thái và lên việc truy cập bộ nhớ tính toán.

Ví dụ làm việc: KV paged allocation có thể tiết kiệm bao nhiêu không gian dành trước? Giả sử A, B, C, D hiện có độ dài 9, 13, 5, 15, mỗi cái được phép tối đa 16 token. Với dành trước trọn phạm vi, bốn request cùng nhau dành trước không gian KV cho 64 token; với phân trang p=4p=4, chúng được cấp lần lượt 12, 16, 8, 16, cho tổng không gian KV là 52 token. Trong số này, KV thực sự được lưu vẫn tương ứng với 42 token, nên dung lượng không dùng giảm từ 22 token xuống 10 token.

Hình 8-11: Bốn request chứa lần lượt 9, 13, 5 và 15 token; mỗi cái dành trước không gian KV cho 16 token, với tổng cấp phát không gian KV cho 64 token. Màu xanh lam là đã dùng; màu xám là dành trước nhưng không dùng.

Hình 8-12: Dưới việc cấp phát KV với 4 token mỗi khối, bốn request có được dung lượng cho lần lượt 12, 16, 8 và 16 token, tổng cộng 52. Màu xám biểu thị dung lượng không dùng ở phần đuôi khối, giảm từ 22 token xuống 10 token.

Nếu tám token đầu tiên của A và B giống hệt nhau, chúng có thể chia sẻ thêm hai khối đầy, giảm tổng cấp phát xuống 44 token. Phân trang giảm không gian dành trước nhưng không dùng; chia sẻ loại bỏ việc lưu trùng lặp của context — hai cách này tiết kiệm bộ nhớ ở những phần khác nhau.

Hình 8-13: Hai khối tiền tố dùng chung của A và B chỉ được lưu một lần, với mỗi bảng khối trỏ đến chúng. A và B giữ các khối đuôi riêng của mình; dung lượng KV thực sự được cấp trên bốn request giảm tiếp, xuống 44 token.

8.3.2 Chia sẻ nhánh, copy-on-write và giải phóng bộ nhớ

Trong hình chia sẻ tiền tố, B và A tham chiếu cùng hai khối vật lý. Nếu A hoàn thành trước, hai khối đó vẫn phải được giữ cho B; nếu B cần sửa nội dung của chúng, điều đó không được ảnh hưởng đến A. Việc triển khai chia sẻ do đó đòi hỏi xác định khi nào giải phóng các khối dùng chung, và cách ngăn các request khác nhau ghi đè dữ liệu của nhau. Mỗi mục trong một bảng khối trỏ đến một khối vật lý được gọi là một tham chiếu (reference); một khối dùng chung được tham chiếu bởi nhiều bảng khối. Một bộ đếm tham chiếu (reference count) ghi lại còn bao nhiêu người dùng cần nó. Mỗi lần một request nhận một tham chiếu, bộ đếm tăng thêm một; khi request kết thúc, nó giải phóng tham chiếu. Chỉ sau khi tham chiếu cuối cùng được giải phóng, khối mới có thể trở về nhóm tự do — và ngay cả khi đó, người ta vẫn phải xác nhận rằng các phép toán accelerator không còn truy cập nó. Bằng cách này, cùng một context dùng chung có thể phục vụ nhiều nhánh cùng lúc, và tiếp tục phục vụ các nhánh còn lại sau khi một nhánh kết thúc.

Hình 8-14: Sau khi A kết thúc, bộ đếm tham chiếu giảm từ 2 xuống 1, và B vẫn có thể dùng khối; chỉ sau khi tham chiếu cuối cùng được giải phóng và accelerator đã dùng xong khối, khối mới có thể trở về nhóm tự do.

Việc ghi vào một khối đuôi dùng chung đòi hỏi có được một bản sao độc lập. Ví dụ, giả sử một khối có thể chứa 4 token đã được ghi KV của ba token dùng chung, và hai nhánh sắp ghi các token khác nhau tiếp theo. Ghi trực tiếp vào khối gốc sẽ khiến hai nhánh tranh giành vị trí thứ tư của khối. Copy-on-write là cơ chế lấy một bản sao riêng trước khi sửa nội dung dùng chung. Ở đây ba token được sao chép trước, và hai nhánh mỗi nhánh thêm nối tiếp riêng; khối chỉ đọc đã đầy tiếp tục được chia sẻ. Khi các nhánh tăng trưởng, tiền tố chung vẫn là một bản sao duy nhất, trong khi mỗi nhánh chỉ lưu hậu tố mới thêm của riêng mình.

Hình 8-15: Một khối đuôi chứa 4 token mỗi khối đã chứa a, b, c dùng chung. Các nhánh thêm lần lượt x và y, đòi hỏi các khối đuôi vật lý khác nhau; khối đã đầy tiếp tục được chia sẻ.

Trong một thí nghiệm hiện có với bốn nhánh, tiền tố chung chiếm 95 khối, và mỗi nhánh có thêm 9 khối riêng. Bốn bảng khối cùng nhau giữ 4(95+9)=4164(95+9)=416 tham chiếu, nhưng số khối vật lý thực tế chỉ là

95+4×9=131.95+4\times9=131.

Việc lưu trữ độc lập sẽ cần 416 khối; chia sẻ cắt giảm khoảng 69%. Khoản tiết kiệm này đến từ việc khử trùng lặp tiền tố chung; 36 khối đuôi riêng vẫn tăng theo số nhánh.

Hệ thống phục vụ biết token nào thuộc về tiền tố chung và token nào sẽ được ghi độc lập sau khi phân nhánh, nên nó có thể chuyển trực tiếp các quan hệ tái sử dụng của model thành việc chia sẻ khối vật lý. Bảng khối xử lý ánh xạ địa chỉ, quan hệ tiền tố xác định phạm vi chia sẻ, và các bản sao riêng chỉ được tạo khi một nhánh ghi. Thứ trước đây được dành trước độc lập theo từng request nguyên vẹn do đó được cấp lại dựa trên trạng thái thực sự được tạo ra và tái sử dụng.

Dung lượng KV toàn batch sau khi chia sẻ tiền tố chung. 16 request dài chia sẻ 6144 token đầu vào đầu tiên, có KV chiếm tổng cộng 864 MiB. Phần riêng của mỗi request chứa 8192−6144+255=23038192-6144+255=2303 token. Cấp phát với 16 token mỗi khối cần không gian cho 2304 token, tức 324 MiB. Vậy không gian KV cần thiết khi nhóm request này sinh xong là

MKV=864+16×324=6048 MiB.M_{\mathrm{KV}}=864+16\times324=6048\ \mathrm{MiB}.

Nhóm request này ban đầu cần 16×1188=1900816\times1188=19008 MiB; sau khi chia sẻ, nó khoảng 5.91 GiB, vừa trong 12 GiB bộ nhớ dành riêng. Tổng quát, bb request dài tương tự chiếm 864+324b864+324b MiB, nên trần dung lượng tăng từ 10 request độc lập lên 35 request dùng chung.

Một khi một khối không còn được dùng nữa, nó vẫn phải được giải phóng đúng cách trước khi trao cho một request sau. Giải phóng bộ nhớ đòi hỏi thỏa mãn đồng thời hai điều kiện: không người dùng nào còn giữ tham chiếu, và các phép toán accelerator đã đệ trình không còn truy cập khối. Sau khi một người dùng hủy request, bộ lập lịch ngừng lập lịch tính toán mới cho nó, chờ các phép toán đã đệ trình hoàn thành, và chỉ sau đó giải phóng các khối riêng của nó. Trong một quan sát, lời gọi hủy trả về trong khoảng 1.6 ms, trong khi 104 khối tương ứng không được giải phóng cho đến khoảng 31 ms. Các request sau chỉ dùng các khối này sau khi giải phóng sẽ không ghi đè lên dữ liệu mà một request cũ vẫn đang đọc.

Hình 8-16: Giao diện hủy trả về có nghĩa là yêu cầu hủy đã được nhận. Chỉ sau khi các phép toán accelerator đã đệ trình hoàn thành, các khối riêng và tham chiếu của chúng mới được giải phóng; 1.6 ms và 31 ms quan sát được tương ứng với các sự kiện khác nhau.

Ngoài việc giải phóng bình thường khi một request kết thúc hoặc bị hủy, engine còn có thể tạm dừng (preempt) một request khi thiếu bộ nhớ: tạm dừng request và giải phóng KV của nó, sau đó về sau tính lại trạng thái từ đầu vào và đầu ra đã lưu. Trong cùng thí nghiệm, một lần tạm dừng xảy ra khi dùng nhóm KV 1 GiB, lập lịch thêm 1805 token so với thực thi bình thường; với nhóm 2 GiB, việc tính lại này không xảy ra. Tạm dừng tạm thời giải phóng bộ nhớ cho các request khác, đổi lại phép tính lặp lại khi thực thi tiếp tục.

Bài tập 8-3 · Tính toán: kích thước khối KV và chia sẻ tiền tố ảnh hưởng dung lượng thế nào. Phân trang bốn chuỗi có độ dài 9, 13, 5 và 15 thành các khối lần lượt 2, 4 và 8 token; tính số vị trí KV không dùng trong khối đuôi của mỗi chuỗi, và tìm tổng số vị trí KV không dùng và tổng số mục bảng khối trong mỗi trường hợp. Sau đó, dùng ví dụ mở đầu chương, tính dung lượng đầy đủ cho 8, 16 và 32 request dài trong cả điều kiện độc lập và dùng chung. Giữ tổng độ dài của mỗi request cố định, chuyển một khối đầy vốn riêng thành một phần của tiền tố chung, và tìm xem bb request cần cấp ít hơn bao nhiêu khối vật lý tổng cộng.

8.3.3 Tái sử dụng tiền tố qua các cuộc hội thoại và tác vụ agent

Mục 8.3.2 đề cập đến việc các request chạy đồng thời chia sẻ khối vật lý. Việc chia sẻ này cũng có thể kéo dài qua cả thời điểm kết thúc của một request: KV tiền tố để lại bởi một request cũ có thể giúp một request sau khỏi phải tính lại cùng một đoạn đầu vào. Nếu tiền tố của một request tiếp theo giống hệt từng token, và dùng cùng model, cùng quy tắc positional encoding, cùng adapter và cùng định dạng trạng thái, nó có thể dùng trực tiếp KV đã cache và tiếp tục prefill từ cuối tiền tố. Tiền tố chung 6144 token của ví dụ mở đầu chương có nghĩa là khi cache được trúng, mỗi request dài chỉ cần xử lý 2048 token đầu vào còn lại trước khi bắt đầu sinh.

Loại tái sử dụng này diễn ra liên tục dọc theo tiền tố. Giả sử hai prompt chia sẻ 100 token đầu tiên nhưng khác nhau ở token thứ 101 — ngay cả khi văn bản sau đó khớp lại, context mà mỗi cái phụ thuộc vào từ token thứ 101 trở đi đã khác nhau. Đặt một timestamp động ngay tại đầu prompt khiến các định nghĩa công cụ dùng chung dài hơn phía sau mất cơ hội tái sử dụng; đặt system prompt và định nghĩa công cụ ổn định trước, rồi thêm biến thể của vòng này sau đó, sẽ giữ được một tiền tố chung dài hơn.

Một cây tiền tố (prefix tree) biểu diễn trực tiếp cấu trúc này: đường đi từ gốc đến điểm phân nhánh là context dùng chung, và đường đi từ điểm phân nhánh đến một lá là hậu tố riêng. Trong Hình 8-17, bốn vòng đầu vào đầu tiên chia sẻ 206 token, và ba vòng tiếp theo tiếp tục mở rộng dọc theo cùng một đường đi. Các cạnh trong cây được gán nhãn bằng số token mới, và các lá cho biết tổng độ dài đầu vào.

Hình 8-17: Cây tiền tố nén của bốn vòng đầu vào đầu tiên từ một code agent. Gốc đã có 206 token chung; các cạnh cho biết số token được thêm; các lá là vòng đầu vào. Một nhánh chỉ ra rằng nội dung tiếp theo khác nhau.

Hình 8-18: Màu xanh lam là tiền tố khớp từng token với vòng trước; màu cam là phần còn lại của đầu vào. Hình này mô tả nội dung đầu vào có thể tái sử dụng đến mức nào; việc trúng cache thực tế vẫn phụ thuộc vào việc trạng thái có được giữ lại hay không.

Việc thêm nội dung vào cuối mỗi vòng giữ nguyên tiền tố chung hiện có, nhưng context càng dài thì footprint KV và lượng đọc tiếp theo càng lớn.

Mở rộng: một model attention lai xác định vị trí tiền tố khôi phục được như thế nào. Full attention giữ lại KV theo từng token, trong khi các model hồi quy (recurrent) thường chỉ giữ lại trạng thái hiện tại. Giả sử văn bản khớp đến token thứ 10,752, và các snapshot hồi quy được lưu tại token thứ 4096 và 8192 tương ứng; hệ thống khôi phục từ 8192 rồi tính tiếp đến 10,752, tính lại 2560 token. Thêm nhiều snapshot hơn giảm số token cần tính lại, nhưng đòi hỏi lưu nhiều trạng thái hơn. Lấy KDA trong Chương 2 làm ví dụ, một triển khai chia tensor trong layer này ra tám card (TP=8); trong trường hợp này, mỗi snapshot khoảng 53.6 MiB mỗi card, nên giữ 2 snapshot tốn khoảng 107 MiB, và giữ 32 snapshot tốn khoảng 1714 MiB. Snapshot càng dày, khi khôi phục càng ít token cần tính lại, nhưng càng nhiều trạng thái phải được giữ lại.

Hình 8-19: Văn bản khớp đến 10752, với snapshot trạng thái gần nhất tại 8192. Sau khi khôi phục, vẫn phải tính lại 2560 token để đạt trạng thái hồi quy tại điểm cuối khớp. Trong hình, 10752 và 8192 là số token tích lũy từ đầu chuỗi.

Tổ chức context cũng có thể chủ động thay đổi cơ hội tái sử dụng. Giả sử đã có một context 8K token, và vòng tiếp theo thêm một kết quả công cụ 1K; tối đa tiền tố 8K ban đầu có thể được tái sử dụng, chỉ cần xử lý phần mới. Nếu thay vào đó các chỉ dẫn mở đầu được viết lại, nội dung phía sau — dù giống hệt về mặt văn bản — không còn có thể tái sử dụng. Một cách tiếp cận khác là tóm tắt lịch sử xuống 2K rồi thêm 1K. Việc thay lịch sử gốc bằng một bản tóm tắt có nghĩa là phép tính tiếp theo dùng context ngắn hơn, nhưng phải trả thêm chi phí của một lượt tóm tắt và một lượt xây dựng lại. Tổ chức context do đó yêu cầu cân nhắc cùng lúc footprint bộ nhớ, thời gian tính lại và lượng đọc tiếp theo.

Hình 8-20: Ba cách cập nhật cùng một lịch sử. Màu xanh lam biểu thị tiền tố có thể tái sử dụng, màu cam biểu thị đầu vào phải được xử lý lại; sơ đồ tóm tắt đầu tiên tạo một bản tóm tắt, sau đó xây dựng lại cache. Độ dài được minh họa theo đơn vị K token.

Việc tái sử dụng tiền tố ở phía model cũng liên quan đến những phân biệt tinh tế hơn trong trạng thái. DeepSeek V4.1 xử lý riêng hai loại trạng thái cục bộ khi tái sử dụng tiền tố. Cấu trúc CED được giới thiệu trong Chương 2 bao gồm một encoder nhân quả và một decoder, mỗi phần có trạng thái cục bộ SWA riêng: encoder SWA có thể được giữ ngắn hạn trong một nhóm DRAM của host, để tiếp tục xử lý đầu vào mới dọc theo tiền tố hiện có trong vòng tiếp theo; decoder SWA chỉ được dùng cho việc sinh của chính vòng này, và bản triển khai được mô tả trong bài báo không lưu nó như một phần của prefix cache. KV toàn cục (global KV), ngược lại, đi vào một cache có thể được tái sử dụng trong thời gian dài hơn. Do đó, 40 layer SWA cư trú trong quá trình sinh và prefix cache được giữ lại giữa các vòng cần được hạch toán dung lượng riêng biệt.

Việc hai loại trạng thái này có được trúng hay không quyết định encoder cần khôi phục bao nhiêu tiền tố khi tiếp tục tính toán trong vòng tiếp theo. Giả sử khi công cụ trả về, các token hoặc biểu diễn đầu vào cần cho việc khôi phục vẫn có sẵn, và phiên bản model, chỉ số vị trí token và đầu vào vẫn nhất quán. Khi cả global KV và encoder SWA đều được trúng, encoder xử lý trực tiếp đầu vào được thêm; khi chỉ trúng global KV, encoder tái phát đến tối đa 128 token gần nhất của tiền tố đã cache và thực thi chúng cùng với hậu tố chưa cache. Đoạn tái phát xây dựng lại encoder SWA và tiếp tục dùng global KV hiện có; hậu tố mới được thêm tạo ra global KV và SWA mới. Khi global KV cũng không được trúng, phần tiền tố thiếu được tính lại từ đầu.

Sau đó, mọi prefill chia sẻ một bước chung: lấy đầu ra encoder của tối đa 128 token cuối cùng của prompt hoàn chỉnh, đưa qua 20 layer decoder để tái dựng gần đúng decoder SWA, chuẩn bị trạng thái cục bộ cho bước decode đầu tiên. Vậy khi cache encoder được trúng hoàn toàn, việc khôi phục tiền tố encoder có thể được bỏ qua, nhưng decoder vẫn cần thực hiện tái phát cửa sổ.

Trạng thái thu được từ tái phát cửa sổ ngắn là một trạng thái khôi phục gần đúng, với sai số phát sinh từ việc các phụ thuộc cục bộ sớm hơn bị cắt bớt. Dù mỗi layer chỉ truy cập 128 token gần nhất, việc xếp chồng nhiều layer vẫn khiến trạng thái cục bộ phụ thuộc gián tiếp vào đầu vào sớm hơn thông qua layer trước. Việc thực thi lại từ ranh giới cửa sổ thay đổi phần thông tin lịch sử này. Do đó Mục 3.2.2 của báo cáo kỹ thuật định nghĩa trạng thái được encoder xây dựng lại là một trạng thái gần đúng; trạng thái này lại đưa vào phép tính tiếp theo, khiến global KV và SWA của hậu tố mới thêm phụ thuộc vào điểm khởi đầu khôi phục.

Sự khác biệt giữa hai loại trạng thái cuối cùng thể hiện ở dung lượng cache. Dưới khối lượng công việc và chính sách cache được áp dụng trong báo cáo kỹ thuật DeepSeek V4, SWA chiếm khoảng một nửa cache dài hạn. V4.1 chuyển phần này ra khỏi cache bền vững, và nén tiếp global KV xuống còn khoảng một phần tư, nên khối lượng lưu trữ dài hạn trở thành khoảng 1/2×1/4=1/81/2\times1/4=1/8 so với ban đầu. Encoder SWA được giữ ngắn hạn nằm trong DRAM, trong khi decoder SWA chỉ cư trú trên accelerator trong quá trình sinh của vòng đó. Việc cache dữ liệu theo tầng thời gian sử dụng làm giảm thêm yêu cầu dung lượng dài hạn.

Hình 8-21: Ba đường khôi phục tiền tố của encoder và tái phát decoder dùng chung. "SWA" trong các trạng thái cache-hit đề cập cụ thể đến encoder; mọi đường vẫn xây dựng decoder SWA trong prefill trước khi sinh bắt đầu. Các mũi tên chỉ thứ tự thực thi; kích thước ô không biểu thị thời lượng.

8.3.4 Tiếp nhận cache, trục xuất, swap-out và tính toán lại

Việc giữ lại một tiền tố sau khi request kết thúc tồn tại để tiết kiệm việc tính toán lại lần sử dụng tiếp theo. Gọi php_h là xác suất tiền tố được tái sử dụng, TrT_r là thời gian tính toán lại, TfT_f là thời gian lấy trạng thái về từ cache và chuẩn bị nó, và TmT_m là thời gian cần để duy trì cache. Mỗi lần trúng tiết kiệm Tr−TfT_r-T_f; nhân với xác suất trúng và trừ đi thời gian duy trì cho giá trị tiết kiệm ròng kỳ vọng:

Vh=ph(Tr−Tf)−Tm.V_h=p_h(T_r-T_f)-T_m.

Lấy tiền tố chung 6144 token của request dài ở đầu chương làm ví dụ: KV của nó chiếm 864 MiB (0.906 GB). Tính lại tiền tố này trên một RTX PRO 6000 cần 96.5 TFLOP phép toán ma trận; tại 62% đỉnh BF16 mà prefill batch-1 trong Thí nghiệm 8-1 đạt được (Mục 8.6.3), việc này mất khoảng 307.9 ms. Sau khi swap tiền tố này ra bộ nhớ host, việc lấy nó về qua liên kết PCIe Gen5 x16 của card này (64 GB/s mỗi chiều) mất khoảng 14.2 ms; tính chi phí duy trì là một lượt truyền ghi lại về host trong lúc swap-out cho cùng 14.2 ms. Tại xác suất trúng 50%, khoản tiết kiệm trung bình là 132.7 ms. Để việc giữ lại tiền tố tiết kiệm thời gian, ta cần

ph>TmTr−Tf=14.2293.7=4.8%.p_h>\frac{T_m}{T_r-T_f}=\frac{14.2}{293.7}=4.8\%.

Nếu liên kết host đổi sang PCIe Gen4 x16 (32 GB/s mỗi chiều), việc lấy về và swap-out mỗi thao tác tăng lên 28.3 ms, và mỗi lần trúng chỉ tiết kiệm 279.6 ms, nâng xác suất trúng tối thiểu cần thiết lên 10.1%. Swap-out giải phóng không gian accelerator, nhưng chỉ khi tiền tố được dùng đủ thường xuyên thì nó mới bù được thời gian cần cho việc lấy về và duy trì.

Việc một tiền tố đáng được giữ lại không có nghĩa là nó nên được giữ với ưu tiên. Khi không gian chật hẹp, một tiền tố lớn chiếm chỗ mà lẽ ra có thể chứa nhiều tiền tố nhỏ hơn. Gọi A là tiền tố 6144 token ở trên; mỗi tiền tố loại B chứa 2048 token và chiếm 288 MiB. Cũng được tính với việc lấy về và swap-out PCIe Gen5, việc tính toán lại mất 94.7 ms, lấy về và duy trì mỗi thao tác mất 4.7 ms, và với cùng xác suất trúng 50%, giá trị tiết kiệm kỳ vọng là 40.3 ms. Hình 8-22 đặt cả hai vào một cache cùng kích thước: giữ một A tiết kiệm 132.7 ms, trong khi giữ ba B tiết kiệm tổng cộng 120.9 ms. Thời gian tính toán lại tăng siêu tuyến tính với độ dài tiền tố — prefill cho 6144 token gấp 3.25 lần so với 2048 token — nên cùng một khối bộ nhớ mang lại lợi ích cao hơn khi dùng để giữ một tiền tố dài.

Phép so sánh này cũng có thể được quy đổi thành khoản tiết kiệm trên mỗi đơn vị dung lượng: A tiết kiệm khoảng 0.154 ms mỗi MiB, B khoảng 0.140 ms. Sắp xếp theo chỉ số này cho phép ta so sánh các tiền tố có kích thước khác nhau.

Hình 8-22: Dung lượng cache là 864 MiB trong cả hai trường hợp. A chiếm 864 MiB với khoản tiết kiệm ròng kỳ vọng 132.7 ms; ba tiền tố loại B mỗi cái chiếm 288 MiB và tiết kiệm 40.3 ms, tổng cộng 120.9 ms. Độ rộng trong hình biểu thị dung lượng; các thời gian được tính từ thời gian tính toán lại trên RTX PRO 6000 và thời gian lấy về PCIe Gen5, với xác suất trúng 50% trong cả hai trường hợp.

Lựa chọn trên giả định một điều kiện tiên quyết: tiền tố phải tồn tại cho đến khi được dùng lại. Khi dung lượng cache không đủ, ngay cả khi đầu vào của vòng tiếp theo giữ nguyên tiền tố, nó vẫn có thể phải được tính lại vì trạng thái đã bị trục xuất. Một lần tái phát 12 vòng đầu vào agent chứa 19,556 token đầu vào. Dưới cùng các request can nhiễu, một nhóm cache 6 GiB trúng 16,304 token, chỉ cần xử lý lại 3,252; một nhóm 1 GiB không trúng lần nào, đòi hỏi toàn bộ đầu vào phải được xử lý lại. Tăng thời gian chờ giữa các vòng liền kề thêm 0.2 giây không làm thay đổi số lần trúng. Trong phép so sánh này, việc tăng dung lượng giữ lại được tiền tố có thể tái sử dụng, trong khi chỉ đơn giản trì hoãn request của vòng tiếp theo không tạo ra hiệu quả tương đương.

Tiếp nhận cache (cache admission) quyết định liệu tiền tố của một request có được giữ lại sau khi request kết thúc hay không; chính sách trục xuất quyết định xóa tiền tố nào trước khi không gian cạn; chính sách swap-out quyết định chuyển trạng thái sang tầng lưu trữ nào; chính sách tính toán lại quyết định thực thi lại bao nhiêu token đầu vào. Với 16 request dài ở đầu chương, giữ một tiền tố dùng chung duy nhất chỉ cần 864 MiB, mà vừa tránh việc lưu trùng lặp vừa tiết kiệm phép tính prefill cho các request sau.

Bài tập 8-4 · Cốt lõi · Phân tích: một cache giới hạn nên giữ những tiền tố nào để tiết kiệm thời gian nhất? Không gian cache khả dụng là 864 MiB. Tiền tố A chứa 6144 token và chiếm 864 MiB; ba tiền tố loại B độc lập lẫn nhau mỗi cái chứa 2048 token và chiếm 288 MiB. Trên RTX PRO 6000, tính thời gian tính toán lại từ số phép toán ma trận tại 62% đỉnh BF16 503.8 TFLOP/s, tính thời gian lấy về tại 64 GB/s mỗi chiều qua PCIe Gen5 x16, và lấy thời gian duy trì là một lượt swap-out, bằng thời gian lấy về; xác suất trúng là 50% trong cả hai trường hợp. So sánh khoản tiết kiệm ròng kỳ vọng từ việc giữ một tiền tố A so với ba tiền tố loại B. Giữ xác suất trúng của loại B cố định, tìm ngưỡng xác suất trúng của A, dưới ngưỡng đó nên giữ ba B thay vào đó. Sau đó đổi liên kết sang PCIe Gen4 x16 (32 GB/s mỗi chiều) và làm lại phép so sánh. Với phần phân tích dữ liệu, đọc đầu vào 12 vòng và tái phát cache kèm theo, tính riêng tỉ lệ tiền tố chung và tỉ lệ trúng thực tế, và giải thích vì sao việc các request khác chiếm không gian cache ảnh hưởng đến tỉ lệ trúng thực tế.

8.4 Nén và Offloading

8.4.1 Footprint bộ nhớ sau khi lượng tử hóa trọng số

Chia sẻ tiền tố tiết kiệm không gian bằng cách loại bỏ KV trùng lặp. Ngay cả khi các request không chia sẻ tiền tố chung, lượng tử hóa vẫn có thể giảm dung lượng lưu trữ cần thiết cho trọng số và KV. Lượng tử hóa thường mã hóa các giá trị theo nhóm, lưu một giá trị mã hóa có độ rộng bit thấp hơn cho mỗi nhóm cùng với siêu dữ liệu như một scale; tại thời điểm tính toán, các giá trị này được dùng để khôi phục một giá trị gần đúng.

Q2_K là một định dạng lượng tử hóa theo nhóm được dùng trong các triển khai của họ thư viện tính toán tensor GGML; mã hóa chính của nó dùng 2 bit, với thông tin bổ sung được lưu cho scale trong mỗi nhóm. Trong định dạng này, mỗi nhóm có 256 giá trị; mã hóa 2 bit chiếm 256×2/8=64256\times2/8=64 byte, và siêu dữ liệu như scale và giá trị tối thiểu chiếm thêm 20 byte. Vậy mỗi nhóm cần tổng cộng 84 byte, trung bình 84×8/256=2.62584\times8/256=2.625 bit mỗi giá trị. Khi độ rộng bit giảm, tỉ trọng của siêu dữ liệu trong tổng dung lượng tăng tương ứng — trong định dạng này, lên khoảng 24%.

Ví dụ làm việc mở rộng: sau khi nạp trọng số lượng tử hóa vào bộ nhớ, vẫn có thể chứa được context dài bao nhiêu? Một biến thể tệp Q2_K của Qwen3-235B-A22B dùng lượng tử hóa theo nhóm chủ yếu bằng mã hóa 2 bit, trộn với vài độ rộng bit; các mã lượng tử hóa cộng với dữ liệu dấu phẩy động chưa lượng tử hóa tổng cộng khoảng 64.43 GiB, siêu dữ liệu lượng tử hóa khoảng 15.37 GiB, và với tiêu đề tệp cùng padding, tổng cộng khoảng 79.81 GiB. Một biến thể khác, UD-Q2_K_XL, khoảng 81.97 GiB. Lấy Apple M2 Max dùng trong Thí nghiệm 8-7 làm ví dụ, bộ nhớ hợp nhất (unified memory) của nó danh nghĩa là 96 GB (thập phân); dành 8 GiB cho hệ thống và không gian làm việc chỉ còn khoảng 81.41 GiB.

Sau khi nạp tệp đầu tiên vào bộ nhớ, còn lại 1.60 GiB; tệp thứ hai đã vượt quá không gian khả dụng khoảng 0.56 GiB. KV BF16 của model này cho context 8K chiếm khoảng 1.47 GiB, nên sơ đồ đầu tiên vẫn có thể xử lý một request, với khoảng 0.13 GiB dư. Nếu độ dài context tăng lên 32K, KV sẽ cần khoảng 5.88 GiB, đòi hỏi hoặc thêm bộ nhớ hoặc một cách lưu trữ trọng số khác. Hai tệp chỉ chênh nhau 2.16 GiB, nhưng chừng đó đã đủ để quyết định một request có thể được chứa hay không.

Trong một hệ thống bộ nhớ hợp nhất, CPU và GPU chia sẻ bộ nhớ vật lý. Sau khi một tệp trọng số được ánh xạ vào không gian địa chỉ, các trang của nó chỉ cần cư trú trong bộ nhớ vật lý khi dữ liệu tương ứng được truy cập. Nếu tổng khối lượng dữ liệu được truy cập trong quá trình thực thi vượt dung lượng bộ nhớ, các trang bị swap vào lặp đi lặp lại. Vậy để giải quyết tình trạng dung lượng không đủ, ta có thể hoặc giảm số byte mà bản thân dữ liệu chiếm giữ, hoặc chỉ giữ lại dữ liệu hiện đang cần và đưa phần còn lại vào ngay trước khi dùng. Mục tiếp theo trước tiên tính lợi ích của việc nén KV, sau đó phân tích thời gian cần để đưa trọng số vào.

8.4.2 Nén KV và chi phí đọc

Áp dụng cùng phương pháp nhóm cho KV cho phép ta tính xem không gian tiết kiệm được có thể chứa thêm đầu vào của bao nhiêu request. Trọng số được chia sẻ giữa nhiều request, nhưng KV tiếp tục tăng liên tục theo đầu vào và đầu ra. Với model của chương này, KV BF16 cho một context 8K là 1152 MiB. Lượng tử hóa thay đổi độ rộng bit của mỗi giá trị, trong khi biểu diễn context thay đổi số giá trị phải lưu mỗi token: Mục 9.2.2 đặt cạnh nhau GQA và trạng thái MLA gọn nhẹ cho cùng context 8K, và trạng thái sau chỉ 549 MiB. Mỗi 32 giá trị BF16 chiếm 64 byte. q8_0 là một định dạng lượng tử hóa 8 bit chia sẻ một scale cho mỗi 32 giá trị. Chuyển sang q8_0 lưu 32 byte mã và 2 byte scale, trở thành 34/6434/64 so với ban đầu; q4_0 dùng cùng cách nhóm nhưng hạ độ rộng mã xuống 4 bit, lưu 16 byte mã và 2 byte scale, trở thành 18/6418/64.

Vậy dung lượng cần cho cùng một context là

Mq8=1152×3464=612 MiB,Mq4=1152×1864=324 MiB.M_{q8}=1152\times\frac{34}{64}=612\ \mathrm{MiB}, \qquad M_{q4}=1152\times\frac{18}{64}=324\ \mathrm{MiB}.

So với BF16, q8_0 tiết kiệm 540 MiB, q4_0 tiết kiệm 828 MiB. Bao gồm cả scale, hai định dạng này trung bình lần lượt 8.5 bit và 4.5 bit mỗi giá trị. Vì mỗi nhóm phải lưu một scale, siêu dữ liệu cũng tăng theo độ dài KV.

Hình 8-23 khôi phục bước này về một bố cục cấp byte. Mỗi hàng lưu cùng 32 giá trị: phần mã hóa ngắn lại, nhưng scale vẫn được giữ. Vậy tổng độ dài của q8_0 và q4_0 lần lượt là 34 và 18 byte, không phải 32 và 16 byte như độ rộng bit mã hóa đơn thuần gợi ý.

Hình 8-23: Không gian cần để lưu cùng một nhóm 32 giá trị của BF16, q8_0 và q4_0. Các thanh được vẽ tỉ lệ với số byte; màu cam biểu thị scale 2 byte của mỗi nhóm. Nhân tổng độ dài mỗi nhóm với số nhóm cho dung lượng KV của cả một context.

Sau khi request dài trong ví dụ làm việc mở đầu sinh xong, KV BF16 cấp theo khối chiếm tổng cộng 1188 MiB. Chuyển sang q8_0 với cùng bố cục cần 1188×34/64=631.1251188\times34/64=631.125 MiB; với 16 request dài độc lập, tổng cộng khoảng 9.86 GiB, vừa trong 12 GiB bộ nhớ dành riêng. Điều này cho thấy chia sẻ và nén tiết kiệm không gian bằng những cách khác nhau: chia sẻ loại bỏ các tiền tố trùng lặp, trong khi nén giảm số byte mà mỗi giá trị chiếm giữ.

Khi tính attention với KV được nén, lượng đọc giảm, nhưng việc chuyển đổi định dạng tốn thêm thời gian. Gọi ΔD\Delta D là số byte đọc ít hơn trong mỗi lần tính, β\beta là băng thông hiệu dụng, và TcT_c là thời gian thực thi tuần tự thêm do chuyển đổi; khoản tiết kiệm thời gian ròng khi đó là

ΔT=ΔDβ−Tc.\Delta T=\frac{\Delta D}{\beta}-T_c.

Ví dụ, chuyển một context 8K trên RTX PRO 6000 từ BF16 sang q8_0 đọc ít hơn 540 MiB. Tại băng thông hiệu dụng khoảng 1.16 TB/s trong điều kiện giới hạn đọc (Mục 8.6.3, 65% đỉnh 1792 GB/s), việc này tiết kiệm khoảng 0.487 ms. Nếu chuyển đổi thêm 0.2 ms, khoản tiết kiệm ròng khoảng 0.287 ms; nếu chuyển đổi thêm 0.8 ms, nó thay vào đó thêm khoảng 0.313 ms. Liệu cùng một định dạng nén có tăng tốc thực thi hay không phụ thuộc vào việc thời gian chuyển đổi có nhỏ hơn thời gian đọc tiết kiệm được hay không.

Context càng dài, thời gian tiết kiệm được từ việc đọc ít đi càng nhiều. Một đánh giá ban đầu chạy Llama-3.1-8B trên H100 khớp các khoảng đầu ra cho BF16 và FP8 lần lượt là 6.44+4.37×10−5L6.44+4.37\times10^{-5}L và 6.58+2.37×10−5L6.58+2.37\times10^{-5}L ms. Số hạng cố định của FP8 lớn hơn 0.14 ms, trong khi số hạng tăng theo mỗi token context nhỏ hơn 2×10−52\times10^{-5} ms. Chia chênh lệch chi phí cố định cho chênh lệch chi phí mỗi token context cho điểm giao nhau khoảng 7000 token; tại 2K, BF16 khoảng 6.53 ms và FP8 khoảng 6.63 ms, trong khi tại 16K chúng khoảng 7.16 và 6.97 ms tương ứng. Tại context dài hơn, thời gian tiết kiệm được từ việc đọc ít đi vượt quá chi phí cố định thêm vào.

Phần mở đầu của mục này lưu ý rằng biểu diễn context quyết định số giá trị phải lưu mỗi token; các hạng mục trạng thái cục bộ và toàn cục trong các model attention lai cũng có thể được so sánh theo dung lượng theo cách này. Dưới đường cơ sở context 8K, cộng dung lượng của lịch sử toàn cục và SWA hiệu dụng. Hai con số của DeepSeek V4.1 là 6.953 MiB và 2.578 MiB, tổng cộng 9.531 MiB; hai hạng mục tương ứng của V4 tổng cộng 30.521 MiB, khoảng 3.20 lần tổng của V4.1. Cửa sổ cục bộ chiếm không gian cố định, nên khoảng cách dung lượng gộp tại context ngắn nhỏ hơn khoảng cách 3.95 lần chỉ riêng của lịch sử toàn cục.

8.4.3 Offload trọng số, prefetch và di chuyển theo từng bước

Nén giảm kích thước của chính dữ liệu. Một cách khác để mở rộng dung lượng là để dữ liệu cư trú trên GPU theo hình thức luân phiên: đưa vào chỉ khi cần cho tính toán, và tái sử dụng không gian đó sau khi xong. Weight offloading chuyển một số trọng số vốn sẽ cư trú trên GPU sang bộ nhớ host, và sao chép chúng về GPU ngay trước khi tính toán layer tương ứng. Bộ nhớ accelerator được giải phóng có thể dùng cho KV, nhưng mỗi lần lan truyền tới phải sao chép lại các trọng số này. Khi một request sinh đầu ra từng bước, việc di chuyển này lặp lại với mọi vòng decode.

Ba ma trận BF16 của một layer FFN trong Qwen3-8B tổng cộng 288 MiB. Offload một trong mỗi bốn layer trên 36 layer sẽ offload chín bộ trọng số FFN, giải phóng 2592 MiB. GPU cũng phải dành trước không gian buffer cho prefetch (sao chép trọng số của một layer lên GPU trước khi tính layer đó): một bộ buffer chiếm 288 MiB, cho khoản tiết kiệm ròng 2304 MiB; hai bộ chiếm 576 MiB, cho khoản tiết kiệm ròng 2016 MiB. Lấy KV 1152 MiB của một đầu vào 8K làm đơn vị, cách trước có thể chứa thêm hai request như vậy, cách sau chỉ một. Nếu thay vào đó ta dành trước 1188 MiB cần thiết sau khi sinh xong, cả hai sơ đồ chỉ có thể chứa thêm một request hoàn chỉnh.

Hình 8-24: Offload chín bộ FFN giải phóng tổng cộng 2592 MiB. Màu cam là buffer prefetch được giữ trên accelerator; màu xanh lục là không gian ròng có thể được cấp lại. Một bộ buffer cho ròng 2304 MiB; hai bộ cho ròng 2016 MiB.

Prefetch phải tuân theo hai quan hệ thứ tự: phép tính chỉ có thể đọc trọng số sau khi bản sao hoàn thành, và buffer chỉ có thể được ghi bộ tiếp theo khi phép tính không còn cần bộ hiện tại. Với một bộ buffer, cùng một buffer lần lượt được dùng cho việc sao chép, tính toán và bản sao tiếp theo; với hai bộ, phép tính của một layer có thể tiến hành trong khi trọng số của layer tiếp theo được sao chép vào buffer kia. Điều này chồng lấp việc sao chép với tính toán, nhưng bản thân các buffer cũng dùng nhiều bộ nhớ hơn.

Trước tiên, tính chi phí liên kết của việc di chuyển này. Card này kết nối với host qua PCIe Gen5 x16, danh nghĩa 64 GB/s mỗi chiều. Mỗi vòng phải đưa vào 2592 MiB, tức 2.72 GB, cần

Tcopy≥2.72 GB64 GB/s≈42.5 ms.T_{\mathrm{copy}}\ge\frac{2.72\ \mathrm{GB}}{64\ \mathrm{GB/s}}\approx42.5\ \mathrm{ms}.

Con số này đã vượt quá 26.26 ms của một vòng decode batch-1. Ngay cả khi mọi phép tính khác có thể chồng lấp hoàn toàn với việc sao chép, mỗi vòng vẫn cần ít nhất khoảng 42.5 ms. Nếu mỗi vòng sinh một token cho mỗi một trong bốn request, tổng thông lượng đầu ra tối đa khoảng 4/0.0425≈944/0.0425\approx94 token/s, và khoảng đầu ra của mỗi request cũng ít nhất khoảng 42.5 ms. Sinh 256 token đầu ra cần 255 lần lan truyền tới tiếp theo, và riêng việc di chuyển trọng số đã mất khoảng 10.8 giây. Vậy offload các trọng số này qua PCIe không thể đáp ứng hạn chót hoàn thành 7 giây.

Cũng có những liên kết nhanh hơn nhiều giữa CPU và GPU. GH200 kết nối CPU Grace và GPU Hopper bằng NVLink-C2C, ở 450 GB/s mỗi chiều. Cùng 2.72 GB bây giờ mất ít nhất khoảng 6.0 ms mỗi vòng, tổng cộng khoảng 1.54 giây trong 255 vòng. Khi bản sao của mỗi vòng ngắn hơn một vòng tính toán decode, prefetch có thể che lấp hoàn toàn việc sao chép sau tính toán.

Hình 8-25: Khối lượng sao chép mỗi vòng vẫn là 2592 MiB (2.72 GB). Qua PCIe Gen5 x16 (64 GB/s mỗi chiều), việc này mất ít nhất khoảng 42.5 ms; qua NVLink-C2C của GH200 (450 GB/s mỗi chiều), mất ít nhất khoảng 6.0 ms.

Điều quan trọng ở đây là tỉ số giữa khối lượng dữ liệu di chuyển mỗi vòng và băng thông hiệu dụng. Prefetch cho phép di chuyển và tính toán xảy ra đồng thời, nhưng số byte cần truyền mỗi vòng vẫn không đổi.

Nếu trọng số được nén trước khi di chuyển, thời gian giải nén cũng phải được cộng vào. Gọi SS byte là dữ liệu gốc, rSrS byte là kích thước nén, BlinkB_{\mathrm{link}} là băng thông liên kết, và RdecR_{\mathrm{dec}} là thông lượng giải nén (đo bằng byte đầu ra gốc). Truyền trọn rồi giải nén mất rS/Blink+S/RdecrS/B_{\mathrm{link}}+S/R_{\mathrm{dec}}, trong khi truyền trực tiếp mất S/BlinkS/B_{\mathrm{link}}; vậy nén trước khi di chuyển chỉ nhanh hơn khi

Rdec>Blink1−rR_{\mathrm{dec}}>\frac{B_{\mathrm{link}}}{1-r}

Nén 96 MiB trọng số xuống một phần tư kích thước, truyền trực tiếp qua PCIe Gen5 x16 mất khoảng 1.57 ms, trong khi truyền dữ liệu nén chỉ mất 0.39 ms; để giữ được khoản tiết kiệm 1.18 ms này, bộ giải nén phải xuất ra hơn 85.3 GB dữ liệu gốc mỗi giây. Liên kết càng nhanh, thông lượng giải nén cần thiết càng cao.

Trong offloading được thảo luận trong mục này, phép tính model vẫn luôn được thực thi bởi GPU, và bộ nhớ host chịu trách nhiệm giữ các trọng số không đang được dùng. Mục 9.3 sẽ dịch chuyển thêm chính vị trí của phép tính: một khi trọng số expert đã nằm trong bộ nhớ host, CPU có thể tính trực tiếp, chỉ gửi các activation nhỏ hơn về GPU — tại thời điểm đó ta cần so sánh số hàng đầu vào của mỗi expert, thời gian tính toán CPU và thời gian truyền trọng số.

Bài tập 8-5 · Tính toán: dung lượng tiết kiệm bằng offload trọng số so với việc di chuyển phát sinh. Giả sử chín bộ trọng số FFN được offload, mỗi bộ chiếm 288 MiB. Giả sử mỗi bộ buffer prefetch có thể chứa một bộ trọng số FFN; tính bộ nhớ accelerator ròng tiết kiệm được khi dùng một so với hai bộ buffer, và tìm xem không gian này có thể chứa thêm bao nhiêu trạng thái đầu vào 1152 MiB, hoặc bao nhiêu trạng thái request hoàn chỉnh 1188 MiB. Sau đó, lấy 64 GB/s mỗi chiều của PCIe Gen5 x16 và 450 GB/s mỗi chiều của GH200 NVLink-C2C tương ứng, tìm cận dưới của thời gian sao chép mỗi bước và của tổng thời gian cho 255 bước sao chép. Nếu tổng ngân sách thời gian cho toàn bộ việc sao chép là 1 giây, tìm băng thông một chiều tối thiểu cần thiết.

8.4.4 Chọn dung lượng và tốc độ dưới một ràng buộc chất lượng

Mục 8.4.2 đã tính không gian tiết kiệm được bằng nén và thời gian chuyển đổi thêm vào, nhưng chưa trả lời liệu câu trả lời có còn như cũ sau khi thay đổi định dạng số hay không. Ở đây, backend thực thi (execution backend) đề cập đến triển khai kernel thực sự thực hiện phép tính attention. Nén KV thay đổi các giá trị được lưu; việc đổi backend thực thi cũng có thể đồng thời thay đổi định dạng tính toán của Q, quá trình chuyển đổi và độ chính xác số của các phép toán khử. Việc so sánh từng hạng mục định dạng lưu trữ và độ chính xác tính toán của các tensor cho phép ta xác định chính xác nguồn gốc của sự khác biệt đầu ra.

Phần sau dùng tám tác vụ cố định cho phép so sánh từng hạng mục này; ba hình sắp xếp các tác vụ này theo cùng một thứ tự. Tám tác vụ đến từ các tài liệu có hai độ dài: mỗi tài liệu chứa 128 hoặc 512 bản ghi sáu chữ số, với bốn tài liệu độc lập ở mỗi độ dài, được ghi nhãn trong nhật ký thí nghiệm là n128-r0 đến n128-r3 và n512-r0 đến n512-r3, và được đánh số là tác vụ 1 đến 8 trong các hình theo thứ tự đó. Mỗi tác vụ chạy hai lần tại mỗi trong hai mức concurrency, so sánh ảnh hưởng của các định dạng số khác nhau bằng cùng một tác vụ.

Một bộ thí nghiệm Qwen3-8B giữ trọng số BF16 cố định và so sánh ba phương pháp tính attention. Trong 32 lượt chạy sinh tự nhiên, dùng KV BF16 trả lời đúng 28; chuyển sang triển khai FP8 gốc trả lời đúng 26; giữ KV FP8 nhưng khôi phục Q về BF16 lại trả lời đúng 28. Cấu hình cuối cùng này chỉ thay đổi độ chính xác của Q, và kết quả vẫn thay đổi — cho thấy định dạng lưu trữ của KV và độ chính xác tính toán của Q phải được xem xét riêng biệt.

Hình 8-26: Đường cơ sở KV BF16. Mỗi hàng là một tác vụ cố định; bốn cột là hai lượt chạy sinh tự nhiên tại mỗi mức concurrency 1 và 4. Các vòng tròn màu xanh lục đánh dấu câu trả lời đúng, các chữ thập màu cam đánh dấu lỗi, tổng cộng 28/32 đúng.

Hình 8-27: Cùng các tác vụ và thứ tự chạy; triển khai FP8 gốc đạt 26/32 đúng tổng thể. Phép so sánh này bao gồm ảnh hưởng của lựa chọn triển khai đến độ chính xác của Q. Các vòng tròn đánh dấu câu trả lời đúng, các chữ thập đánh dấu lỗi; mỗi hàng tương ứng với cùng một câu hỏi, và các cột biểu thị mức concurrency và chỉ số lần chạy lặp.

Hình 8-28 còn khôi phục chỉ riêng query Q về độ chính xác BF16, giữ KV ở FP8. So sánh cùng một cột qua ba hình tiết lộ những câu hỏi cụ thể nào có kết quả thay đổi, chứ không chỉ tổng số câu trả lời đúng.

Hình 8-28: Giữ query Q ở BF16 trong khi KV vẫn ở FP8, tổng cộng 28/32 đúng; các câu hỏi trả lời sai khác với đường cơ sở BF16. Cả ba hình dùng cùng các tác vụ và thứ tự cột, với trọng số model trong suốt quá trình là BF16. Các vòng tròn đánh dấu câu trả lời đúng, các chữ thập đánh dấu lỗi; mỗi hàng tương ứng với cùng một câu hỏi, và các cột biểu thị mức concurrency và chỉ số lần chạy lặp.

Trong hình, tác vụ 5 (n512-r0 trong nhật ký thí nghiệm, tài liệu dài đầu tiên với 512 bản ghi) sai cả bốn lần dưới KV BF16, nhưng đúng cả bốn lần dưới “KV FP8 + Q BF16”; tác vụ 6 (n512-r1) cho thấy chính xác mô hình ngược lại. Cả hai cấu hình đều trả lời đúng 28 câu tổng thể, nhưng các tác vụ trả lời sai là khác nhau. Xếp thẳng hàng kết quả của cùng một tác vụ cho phép ta thấy chính xác nơi khác biệt xảy ra, và liệu chúng có nhất quán qua các lần chạy lặp hay không.

Các lỗi cũng làm tăng thời gian cần để hoàn thành một tác vụ. Sau khi một câu trả lời đầu tiên thất bại và được thử lại, tổng thời gian để đạt một câu trả lời đúng phải bao gồm lần sinh đầu tiên, kiểm tra câu trả lời và sinh lại. Trong một thí nghiệm được ghi lại, hai lần thử lại đã sửa lỗi nhưng thêm khoảng 2.0 giây thời gian sinh và 92 token. Nếu câu trả lời đầu tiên nhanh hơn nhưng cũng dễ sai hơn, việc hoàn thành toàn bộ tác vụ có thể thực sự lâu hơn.

So sánh hai sơ đồ dung lượng KV đã thảo luận trước đó: 16 request dài độc lập chiếm khoảng 9.86 GiB với q8_0, so với khoảng 5.91 GiB với tiền tố BF16 dùng chung. Để đánh giá sơ đồ nào phù hợp hơn, ta cũng phải tính thời gian chuyển đổi, sinh và thử lại vào tổng thời lượng của mỗi request. Ví dụ làm việc toàn diện trong Mục 8.6 sẽ hoàn thành bước này.

Bài tập 8-6 · Phân tích dữ liệu: nén KV ảnh hưởng dung lượng, độ đúng của câu trả lời và chi phí thử lại như thế nào. Giả sử mỗi nhóm chứa 32 giá trị với thêm 2 byte cho scale, tính lại dung lượng KV cho một đầu vào 8K và một request dài hoàn chỉnh dưới các định dạng q8_0 và q4_0. Sau đó đọc từng hạng mục các kết quả kèm theo của ba cấu hình độ chính xác KV/Q, và liệt kê những câu hỏi nào đổi độ đúng giữa “KV BF16” và “KV FP8 + Q BF16”. Cuối cùng, đọc các bản ghi thử lại, cộng thời gian câu trả lời đầu tiên và các lần thử lại của mỗi tác vụ thành công, và so sánh với kết quả thu được khi chỉ đếm thời gian của lần sinh thành công cuối cùng.

8.5 Giải mã phỏng đoán (Speculative Decoding)

8.5.1 Bản nháp (draft), xác minh, rollback và tính đúng đắn

Hai mục trước chủ yếu thay đổi cách dữ liệu được lưu và đọc, nhưng mỗi request vẫn sinh từng vòng một. Nếu một phép tính model mục tiêu có thể xác định nhiều đầu ra cùng lúc, sinh 256 token sẽ không còn cần cùng số vòng. Trong decode thông thường, model mục tiêu tạo ra một token mỗi lượt lan truyền tới. Mục 8.2 gộp các request khác nhau vào một phép tính để một lần đọc trọng số phục vụ nhiều hàng hơn. Giải mã phỏng đoán đi theo một hướng khác: trước tiên tạo ra một chuỗi bản nháp (draft sequence) cho cùng request, sau đó để model mục tiêu xác minh đồng thời vài token của chuỗi đó. Nếu model mục tiêu chấp nhận nhiều token bản nháp, một phép tính duy nhất xác định nhiều đầu ra.

Bắt đầu với sinh tham lam (greedy generation): tại mỗi bước, chọn token mà model mục tiêu gán xác suất cao nhất. Giả sử chuỗi bản nháp chứa bốn token, và model mục tiêu tính toán, tại mỗi vị trí token, đầu ra lẽ ra phải là gì. Nếu hai token đầu khớp với bản nháp nhưng token thứ ba không khớp, kết quả cuối cùng giữ lại hai token bản nháp đầu và xuất ra token của model mục tiêu được chọn tại vị trí thứ ba. Token bản nháp thứ tư, được sinh sau token thứ ba sai, bị loại bỏ cùng với nó. Vòng tiếp theo tiếp tục sinh từ chuỗi đã sửa. Nếu cả bốn token bản nháp khớp, model mục tiêu thường có thể sinh thêm một token sau chúng.

Hình 8-29 cho thấy thứ tự xử lý khi token thứ ba phân kỳ. Dù model mục tiêu đã tính kết quả xác minh cho các token theo sau, token thứ tư phụ thuộc vào token bản nháp thứ ba sai và không thể được giữ lại. Một lượt xác minh cuối cùng giữ lại hai token bản nháp khớp và một token đã sửa, tổng cộng ba đầu ra.

Hình 8-29: Quá trình xác minh tham lam. a, b, c, d, x biểu thị các token; model mục tiêu chọn x tại vị trí thứ ba, khác với token bản nháp c. Token thứ tư và các kết quả xác minh tiếp theo của nó bị loại bỏ; vòng tiếp theo tiếp tục sinh từ a, b, x. Các ô biểu thị vị trí token trong chuỗi, không phải thời gian thực thi.

Với việc lấy mẫu ngẫu nhiên, đầu ra cuối cùng vẫn phải tuân theo phân bố xác suất của model mục tiêu. Gọi phân bố mục tiêu là p(x)p(x) và phân bố bản nháp là q(x)q(x). Trước tiên lấy mẫu một token bản nháp xx từ qq, sau đó chấp nhận nó với xác suất sau:

α(x)=min⁡(1,p(x)q(x)).\alpha(x)=\min\left(1,\frac{p(x)}{q(x)}\right).

Xác suất lấy mẫu xx và chấp nhận trực tiếp là q(x)α(x)=min⁡(p(x),q(x))q(x)\alpha(x)=\min(p(x),q(x)). Để đạt xác suất mục tiêu p(x)p(x), vẫn còn một thiếu hụt [p(x)−q(x)]+[p(x)-q(x)]_+. Ở đây [z]+=max⁡(z,0)[z]_+=\max(z,0) giữ lại chỉ phần chênh lệch dương. Vậy sau khi từ chối token bản nháp, phần dư dương này được chuẩn hóa và lấy mẫu lại từ phân bố kết quả. Kết hợp chấp nhận trực tiếp với từ chối-và-lấy-mẫu-lại, xác suất cuối cùng xuất ra xx chính xác là p(x)p(x).

Minh họa điều này với một ví dụ đơn giản chỉ dùng hai ký hiệu, A và B. Phân bố mục tiêu là p(A)=1/4p(A)=1/4, p(B)=3/4p(B)=3/4. Nếu bản nháp luôn sinh A, chấp nhận A với xác suất 1/41/4; trong 3/43/4 thời gian còn lại, từ chối A và xuất ra B thay vào đó. Nếu bản nháp luôn sinh B, chấp nhận B với xác suất 3/43/4; ngược lại xuất ra A thay vào đó. Cả hai phương pháp cuối cùng đều cho phân bố mục tiêu, nhưng xác suất chấp nhận của bản nháp khác nhau, và hiệu quả thực thi thu được cũng khác nhau.

Hình 8-30: Xác suất chấp nhận A là 1/4; xác suất xuất ra B sau khi từ chối là 3/4. Hai đường cùng nhau cho phân bố mục tiêu.

Hình 8-31: Xác suất chấp nhận B là 3/4; xác suất xuất ra A sau khi từ chối là 1/4. Phân bố mục tiêu giống nhau, nhưng xác suất chấp nhận của bản nháp cao hơn.

Sau khi xác minh, độ dài KV hiệu dụng được điều chỉnh về vị trí đầu ra đã xác nhận, và KV tương ứng với các token bản nháp bị loại bỏ không còn tham gia vào phép tính tiếp theo. Trình phân trang cập nhật các tham chiếu khối theo độ dài đầu ra đã xác nhận, và bộ lấy mẫu cùng bộ kiểm tra ngữ pháp (thành phần giới hạn các token hợp lệ theo một định dạng đầu ra chỉ định) cũng được khôi phục về trạng thái tương ứng. Vòng tiếp theo sau đó tiếp tục tính toán từ chuỗi đã sửa.

8.5.2 Độ trễ mỗi vòng và số lượng đầu ra

Giải mã phỏng đoán thay đổi cả độ trễ mỗi vòng lẫn số token cuối cùng xuất ra mỗi vòng. Gọi vòng rr mất thời gian TrT_r và xuất ra NrN_r token; khi đó, qua nhiều vòng liên tiếp, độ trễ trung bình mỗi token đầu ra là

tˉspec=∑rTr∑rNr.\bar t_{\mathrm{spec}}=\frac{\sum_r T_r}{\sum_r N_r}.

Ví dụ, nếu hai vòng mỗi vòng mất 1.5 ms và xuất ra lần lượt 1 và 5 token, tổng cộng là 3 ms cho 6 token, cho trung bình 0.5 ms mỗi token. Nếu thay vào đó người ta trước tiên tính trung bình mỗi vòng rồi lấy trung bình không trọng số của 1.5 và 0.3 ms/token, kết quả là 0.9 ms. Điều này tương đương với việc cho vòng chỉ xuất ra một token cùng trọng số với vòng xuất ra năm token. Chỉ chia tổng thời gian cho tổng số token mới cho độ trễ trung bình đúng trên mỗi token.

Ví dụ làm việc: tỉ lệ chấp nhận bản nháp quyết định số đầu ra trung bình mỗi vòng như thế nào? Tiếp tục với phân bố mục tiêu hai ký hiệu, giả định tính độc lập giữa các vị trí token. Mỗi vòng bản nháp sinh bốn bản sao của cùng một ký hiệu; đầu ra cuối cùng cũng bao gồm token đã sửa khi từ chối, hoặc một token thừa nếu tất cả đều được chấp nhận. Gọi aa là xác suất chấp nhận tại vị trí hiện tại, với điều kiện mọi token bản nháp trước đó đã được chấp nhận. Mỗi vòng xuất ra ít nhất một token; xuất thêm token thứ hai đòi hỏi chấp nhận token bản nháp đầu tiên, với xác suất aa; xuất thêm token thứ ba đòi hỏi chấp nhận hai token đầu, với xác suất a2a^2. Cộng các số hạng này lại, số đầu ra trung bình mỗi vòng là

E[N]=1+a+a2+a3+a4.\mathbb E[N]=1+a+a^2+a^3+a^4.

Mỗi số hạng trên biểu thị xác suất “cuối cùng xuất ra ít nhất chừng này token”. Khi bản nháp là AAAA, a=1/4a=1/4, và số đầu ra trung bình mỗi vòng khoảng 1.33 token; khi bản nháp là BBBB, a=3/4a=3/4, và số đầu ra trung bình khoảng 3.05. Trên RTX PRO 6000, tại batch 1 với context 2K, decode thông thường mất 26.26 ms mỗi vòng (Thí nghiệm 8-1). Khi xác minh 5 vị trí, lượng đọc trọng số và KV giống như decode thông thường; phép tính ma trận thêm cho 4 hàng bổ sung lên tới khoảng 65 GFLOP, tại 62% đỉnh BF16 chỉ mất khoảng 0.2 ms, nên một vòng xác minh vẫn được tính là 26.26 ms. Thí nghiệm 8-5 quan sát thấy cùng hiện tượng: DFlash (một mạng bản nháp tạo ra toàn bộ một đoạn bản nháp song song trong một lượt lan truyền tới) sinh 7 token bản nháp mỗi vòng và xác minh 8 vị trí, với độ trễ trung vị 15.2 ms — tương đương với 16.9 ms của một bước decode thông thường trong cùng cấu hình. Cộng thêm 0.1 ms để truy vấn bản nháp, một vòng tổng cộng 26.36 ms, và hai cấu hình bản nháp tương ứng với độ trễ trung bình mỗi token khoảng 19.8 và 8.64 ms — cả hai đều nhanh hơn 26.26 ms/token của decode thông thường. Việc xác minh hầu như không làm tăng thời lượng của vòng, và ngay cả khi hầu hết token bản nháp bị từ chối, mỗi vòng vẫn cho ít nhất một token, nên ngay cả AAAA có tỉ lệ chấp nhận thấp vẫn đạt được gia tốc.

Thời gian tiết kiệm được bằng cách chấp nhận nhiều token bản nháp hơn cũng có thể bị triệt tiêu bởi chi phí truy vấn. Để vượt qua decode thông thường, tổng thời gian của một vòng phải nhỏ hơn số đầu ra trung bình nhân 26.26 ms. Sau khi trừ 26.26 ms cần cho việc xác minh, AAAA chừa khoảng 8.7 ms cho việc truy vấn bản nháp, trong khi BBBB chừa khoảng 53.9 ms. Hình 8-32 vẽ hai điểm giao nhau này lên trục chi phí truy vấn.

Hình 8-32: Phân bố mục tiêu hai ký hiệu, bốn token bản nháp được sinh mỗi vòng. Việc xác minh mỗi vòng mất 26.26 ms, với thời gian truy vấn thay đổi dọc theo trục hoành; độ trễ mỗi token đầu ra là tổng thời gian của vòng chia cho số đầu ra trung bình. Decode thông thường là 26.26 ms/token (RTX PRO 6000, batch 1, context 2K). Không có việc dừng sớm, và các token thừa được tính vào tổng đầu ra. Các điểm được đánh dấu cho thấy trường hợp độ trễ truy vấn là 0.1 ms; AAAA và BBBB cắt đường thực thi thông thường tại các thời gian truy vấn khoảng 8.7 và 53.9 ms tương ứng.

Công việc chuẩn bị trước khi dùng một bản nháp cũng phải được tính vào tổng thời gian. Nếu việc xây dựng một chỉ mục context mất 2 giây, và bản nháp BBBB tiết kiệm khoảng 26.26−8.64≈17.626.26-8.64\approx17.6 ms cho mỗi token đầu ra, thì sinh khoảng 114 token sẽ bù được 2 giây chi phí này. Một request xuất ra 256 token bằng decode thông thường mất khoảng 6.72 giây; xây dựng chỉ mục trước trong 2 giây rồi dùng bản nháp BBBB dự kiến mất khoảng 4.22 giây. Nếu chỉ mục có thể được xây dựng trước và chia sẻ giữa nhiều request, chi phí này có thể được phân bổ cho nhiều đầu ra hơn nữa.

8.5.3 Nguồn bản nháp và các phương pháp sinh tuần tự so với song song

Bốn token bản nháp tiết kiệm được bao nhiêu thời gian phụ thuộc cả vào số token vượt qua xác minh lẫn thời gian cần để tạo ra đoạn bản nháp đó. Các hệ thống thực tế có thể tra cứu bản nháp từ context, hoặc gọi một mạng bổ sung để tạo bản nháp; hai lựa chọn này mang lại các thời gian chuẩn bị và footprint bộ nhớ khác nhau.

Tra cứu context tìm các đoạn lặp lại từ đầu vào hoặc các phản hồi hiện có, với chi phí chính là việc khớp và xây dựng chỉ mục. Một model nhỏ độc lập sinh bản nháp từng bước đòi hỏi bộ nhớ bổ sung cho trọng số và KV của chính nó. Các phương pháp kiểu EAGLE đưa các biểu diễn trung gian của model mục tiêu vào một mạng dự đoán nhỏ hơn; các phương pháp kiểu DFlash sinh nhiều token bản nháp song song. Một đầu MTP được tích hợp trong model dùng khả năng dự đoán được huấn luyện chung để tạo bản nháp.

Chia mỗi vòng thành bốn giai đoạn — sinh bản nháp, chuẩn bị xác minh, xác minh mục tiêu và xác định đầu ra — cho phép so sánh các phương pháp này từng số hạng. Lấy mạng bản nháp DFlash trong Thí nghiệm 8-5 làm ví dụ; trọng số của nó khoảng 1.95 GiB (2.10 GB). Tại băng thông hiệu dụng của batch 1 (26.26 ms để đọc 15.44 GB, khoảng 588 GB/s), một lượt lan truyền tới của mạng này mất khoảng 3.57 ms. Sinh bốn token bản nháp từng cái một sẽ cần bốn lượt lan truyền tới, tổng cộng 14.28 ms; sinh toàn bộ đoạn bản nháp song song, như DFlash làm, chỉ cần một lượt lan truyền tới, khoảng 3.57 ms. Xác minh mục tiêu cần 26.26 ms trong cả hai trường hợp. Nếu mỗi vòng cuối cùng xuất ra trung bình 3 token, cách tuần tự tốn khoảng (14.28+26.26)/3≈13.5(14.28+26.26)/3\approx13.5 ms mỗi token, trong khi cách song song tốn khoảng (3.57+26.26)/3≈9.9(3.57+26.26)/3\approx9.9 ms mỗi token. Ở đây, gia tốc đến từ việc rút ngắn thời gian sinh bản nháp.

Bây giờ giả sử tỉ lệ chấp nhận của bản nháp song song thấp hơn, và số đầu ra trung bình giảm xuống 2. Độ trễ trung bình mỗi token đầu ra tăng lên (3.57+26.26)/2≈14.9(3.57+26.26)/2\approx14.9 ms — chậm hơn cách tuần tự. Việc sinh bản nháp nhanh hơn tiết kiệm 10.7 ms, nhưng với ít đầu ra hơn mỗi vòng, thời gian được phân bổ cho mỗi token đầu ra tăng lên, và request tổng thể trở nên chậm hơn.

8.5.4 Chọn động độ dài bản nháp và tranh chấp concurrency

Một khi nguồn bản nháp được cố định, người ta cũng phải quyết định bản nháp của mỗi vòng nên dài bao nhiêu. Bản nháp dài hơn nâng cận trên của số token một vòng có thể xuất ra. Nhưng chỉ khi các token trước đó đều được chấp nhận thì các token sau mới có cơ hội lọt vào đầu ra cuối cùng. Gọi aja_j là xác suất chấp nhận có điều kiện của token thứ jj với điều kiện mọi token bản nháp trước đó đã được chấp nhận; khi đó số đầu ra trung bình cho một bản nháp độ dài mm là

E[Nm]=1+∑j=1m∏i=1jai.\mathbb E[N_m]=1+\sum_{j=1}^{m}\prod_{i=1}^{j}a_i.

Tăng độ dài bản nháp từ m−1m-1 lên mm làm tăng đầu ra trung bình thêm ΔN=∏i=1mai\Delta N=\prod_{i=1}^{m}a_i token. Giả sử việc này thêm ΔT\Delta T thời gian; ban đầu vòng mất thời gian TT và xuất ra trung bình NN token, cho độ trễ trung bình mỗi token đầu ra là T/NT/N. Để độ dài bản nháp tăng lên cho độ trễ trung bình mỗi token đầu ra ngắn hơn, ta cần (T+ΔT)/(N+ΔN)<T/N(T+\Delta T)/(N+\Delta N)<T/N, biến đổi thành

ΔTΔN<TN.\frac{\Delta T}{\Delta N}<\frac{T}{N}.

Nói cách khác, thời gian trung bình phải trả cho đầu ra mới được thêm phải thấp hơn độ trễ trung bình ban đầu mỗi token. Vì các xác suất chấp nhận được nhân liên tiếp, các token bản nháp sau có xác suất sống sót giảm dần, khiến chúng càng khó mang lại lợi ích thời gian đủ lớn.

Shape thực thi định sẵn cũng ảnh hưởng đến thời gian thêm ΔT\Delta T. Nếu xác minh bốn hoặc năm token đều pad vector đặc trưng của mỗi token lên ma trận tám hàng và dùng một đồ thị tính toán tám hàng chuẩn bị sẵn, thêm một token nữa đòi hỏi tương đối ít phép tính thêm; nhưng nếu số token cần xác minh tăng từ tám lên chín, đòi hỏi pad lên ma trận mười sáu hàng và chuyển sang đồ thị tính toán tương ứng, phép tính nhảy vọt đột ngột. Số request đồng thời cũng ảnh hưởng đến lợi ích của giải mã phỏng đoán: dưới tải thấp, xác minh nhiều hàng có thể dùng phần tính toán nhàn rỗi; dưới tải cao, batching thông thường đã chia sẻ trọng số hiệu quả, và việc sinh và xác minh bản nháp còn phải tranh chấp thêm với các request khác về tài nguyên accelerator và bộ nhớ.

Một bộ phép đo so sánh các độ dài bản nháp khác nhau. K7 và K15 ký hiệu các cấu hình sinh lần lượt 7 và 15 token bản nháp mỗi vòng. Với tác vụ trích xuất ngắn của Qwen3-8B/DFlash tại concurrency 1, độ trễ toàn phần trung vị của decode thông thường, K7 và K15 lần lượt khoảng 90.0, 30.6 và 29.4 ms. Chuyển từ thực thi thông thường sang K7 tiết kiệm khoảng 59 ms, trong khi đi từ K7 sang K15 chỉ tiết kiệm thêm khoảng 1.2 ms. Các bản ghi từng vòng cũng cho thấy các vòng trong đó cả hai độ dài khối chỉ chấp nhận một token bản nháp duy nhất: K7 loại bỏ sáu token bản nháp, K15 loại bỏ mười bốn, và độ dài đầu ra cuối cùng giống nhau. Khối xác minh lớn hơn làm tăng phép tính mà không tạo ra nhiều đầu ra hơn trong vòng đó.

Do đó, khi điều chỉnh động độ dài bản nháp, có thể áp dụng điều kiện trên: sinh bản nháp dài hơn cho các request có xác suất chấp nhận cao và thời gian thêm thấp cho mỗi vị trí xác minh, và rút ngắn độ dài khối cho các request thường thất bại xác minh ở vài vị trí đầu, hoặc những request cần chuyển sang một shape thực thi lớn hơn. Khi xác định độ dài bản nháp, người ta cũng phải chừa không gian cho KV của cả model mục tiêu lẫn model bản nháp, rồi làm theo phương pháp trong Mục 8.3 để quyết định tiếp nhận bao nhiêu request.

Bài tập 8-7 · Suy diễn: độ dài bản nháp nào tối thiểu hóa độ trễ trung bình mỗi token đầu ra? Cố định xác suất chấp nhận bản nháp ở a=3/4a=3/4, và tính số token đầu ra trung bình mỗi vòng cho các độ dài khối bản nháp 1, 2, 4 và 8. Dùng dữ liệu RTX PRO 6000 từ Mục 8.5.3, làm tròn đến một chữ số thập phân: một vòng xác minh mất 26.3 ms, và mạng bản nháp sinh token từng cái một ở 3.6 ms mỗi token bản nháp, tức Tm=26.3+3.6mT_m=26.3+3.6m ms; tìm độ trễ trung bình mỗi token đầu ra cho mỗi độ dài khối. Sau đó giả sử bản nháp thực thi theo nhóm 4 vị trí, với các nhóm không đầy vẫn được tính thời gian như các nhóm đầy, tức Tm=26.3+3.6×4⌈m/4⌉T_m=26.3+3.6\times4\lceil m/4\rceil ms; so sánh độ dài khối 4 và 5, và dùng điều kiện biên để giải thích lựa chọn.

8.6 Hiệu năng và các lựa chọn cấu hình dưới tải request

8.6.1 Tốc độ đến, xếp hàng, tiếp nhận và hủy bỏ

Các mục 8.2 đến 8.5 thảo luận cách thay đổi footprint bộ nhớ và phương pháp thực thi của một instance. Việc phục vụ trực tuyến cũng phải xử lý các request đến liên tục: trước khi một request hoàn thành, request tiếp theo đã vào hàng đợi; trong khi một request chờ công cụ trả về, phép tính model tạm dừng, nhưng KV của nó vẫn phải được giữ lại. Bộ lập lịch do đó vừa phải sắp xếp phép tính hiện tại vừa giữ trạng thái cho các request đang sinh hoặc chờ để tiếp tục.

Cần bao nhiêu không gian để giữ trạng thái của các request chưa hoàn thành này phụ thuộc vào số request trung bình có mặt trong hệ thống. Trong dịch vụ trạng thái ổn định, số request trung bình trong hệ thống tuân theo định luật Little (Little’s Law):

nˉ=λTˉ,\bar n=\lambda\bar T,

trong đó λ\lambda là tốc độ đến trung bình và Tˉ\bar T là thời gian trung bình một request ở trong hệ thống. Cộng thời gian cư trú của mọi request quan sát được và chia cho thời lượng quan sát cho số request trung bình trong hệ thống. Ví dụ, tại tốc độ đến 4 request mỗi giây với thời gian cư trú trung bình 0.5 giây, hệ thống giữ trung bình 2 request; nếu thời gian cư trú trung bình tăng lên 2 giây, con số này trở thành 8.

Một số request trong số này đang xếp hàng, một số đang tính toán, và một số chỉ giữ trạng thái. Tăng batch size có thể nâng thông lượng đầu ra, nhưng lượng đọc KV, phép tính và các chi phí khác dần giới hạn sự tăng trưởng này. Khi tốc độ xử lý không còn theo kịp tốc độ đến tăng lên, các request mới tích tụ trong hàng đợi, và thời gian chờ tăng tương ứng.

Điều khiển tiếp nhận (admission control) quyết định có bắt đầu xử lý một request mới hay không. Trước khi chấp nhận một request, hãy kiểm tra không gian KV khả dụng và còn bao nhiêu thời gian trước hạn chót. Với request ngắn trong Mục 8.1, 0.196 giây đến đầu ra đầu tiên cộng 6.76 giây sinh đã dùng 6.95 giây; xếp hàng thêm 0.1 giây sẽ vượt quá thời gian chờ. Việc xác định trước các request như vậy cho phép hệ thống từ chối chúng, chuyển chúng sang một instance khác, hoặc dùng một cấu hình thực thi nhanh hơn. Hủy bỏ (cancellation), ngược lại, ngừng lập lịch các vòng tiếp theo khi một request đã mất tính hữu ích, và giải phóng không gian của nó một khi các phép toán accelerator đang bay hoàn thành.

Các tối ưu hóa này tương tác với nhau. Nén KV cho phép một instance tiếp nhận nhiều request đồng thời hơn; batch lớn hơn, đến lượt nó, cho phép decode thông thường chia sẻ trọng số hiệu quả hơn, thay đổi lợi ích tương đối của giải mã phỏng đoán. Phần sau áp dụng các thay đổi này cho cùng một tập request, tính thời gian thực thi và footprint bộ nhớ gộp.

8.6.2 Thông lượng hiệu dụng dưới các ràng buộc chất lượng và SLO

Chỉ nhìn vào độ dài hàng đợi và tốc độ hoàn thành không thể biết người dùng có thực sự nhận được một câu trả lời dùng được hay không. Các thí nghiệm chất lượng trong Mục 8.4.4 tạo ra các câu trả lời sai, trong khi xếp hàng tạo ra các phản hồi đúng-nhưng-muộn. Chỉ khi đếm chung cả hai loại tổn thất mới có thể đánh giá hiệu quả dịch vụ. Gọi cửa sổ quan sát có độ dài TobsT_{\mathrm{obs}}, để Qi=1Q_i=1 khi request ii đúng, và Di=1D_i=1 khi nó hoàn thành đúng hạn. Đếm theo các request hoàn thành, thông lượng hiệu dụng là

Rgood=∑iQiDiTobs.R_{\mathrm{good}}=\frac{\sum_i Q_iD_i}{T_{\mathrm{obs}}}.

Tử số chỉ đếm các request vừa đúng vừa hoàn thành đúng hạn. Nếu thay vào đó tử số đếm mọi request hoàn thành, ta có thông lượng hoàn thành. Khoảng cách giữa hai giá trị cho thấy bao nhiêu công suất xử lý đã đi vào các câu trả lời sai hoặc muộn.

Một bộ phép đo cho thấy kích thước của hai loại tổn thất này. Thí nghiệm truy hồi Qwen3-8B dùng cùng tám câu hỏi, yêu cầu các request hoàn thành đúng trong vòng 3 giây kể từ khi đến, và so sánh ba chế độ: tiếp nhận từng cái một (individual admission), continuous batching, và đệ trình toàn bộ các request cùng lúc. Mỗi thí nghiệm đệ trình 16 request; qua tất cả các thí nghiệm, tổng cộng 336 phản hồi được sinh ra, trong đó 294 câu trả lời đúng, nhưng chỉ 155 cũng đáp ứng được hạn chót. 139 phản hồi đúng còn lại trả về quá muộn để đáp ứng yêu cầu dịch vụ.

Hình 8-33: Mỗi điều kiện tiếp nhận-và-đến dùng ba cửa sổ gồm 16 request. Các thanh cho thấy thông lượng trung vị của tất cả các request hoàn thành; các chấm cho thấy kết quả của từng cửa sổ. Thời gian được đo từ lúc đến theo lịch trình.

Hình 8-34: Tử số chỉ đếm các request đúng và hoàn thành trong 3 giây. Cả hai hình dùng cùng thứ tự trục hoành và cùng phạm vi trục tung; khoảng cách đến từ các phản hồi sai hoặc muộn.

Tại tốc độ đến 1/s, cả hai chiến lược trực tuyến đạt thông lượng hiệu dụng khoảng 0.90 req/s. Với ít công việc hơn mỗi batch, tiếp nhận từng cái một cũng có thể xử lý kịp phần lớn các request đúng. Khi tốc độ đến tăng lên 4/s, thông lượng hoàn thành của tiếp nhận từng cái một khoảng 1.22 req/s, nhưng thông lượng hiệu dụng của nó giảm xuống 0.31 req/s; hàng đợi dài ra, và nhiều đầu ra đúng bỏ lỡ hạn chót. Continuous batching nâng thông lượng hoàn thành lên khoảng 2.33 req/s, với thông lượng hiệu dụng đạt khoảng 1.02 req/s — nhiều request hơn hoàn thành trong hạn chót.

Đẩy tiếp tốc độ đến của continuous batching lên 16/s, thông lượng hoàn thành chỉ tăng lên khoảng 2.38 req/s, trong khi thông lượng hiệu dụng thay vào đó giảm xuống khoảng 0.60 req/s, giảm khoảng 41%. Thông lượng hoàn thành về cơ bản đã ngừng tăng; nhiều request đến chủ yếu thêm vào thời gian xếp hàng. Với tập các cửa sổ này, 4/s trả về nhiều câu trả lời đúng đúng hạn hơn 16/s. Khi chọn một cấu hình dịch vụ, trước tiên tìm tốc độ đến tại đó thông lượng hiệu dụng bắt đầu suy giảm, rồi giới hạn số request mới được tiếp nhận để tránh thời gian xếp hàng quá mức.

Với việc tiếp nhận từng cái một, các request khác chưa vào engine vẫn chờ trong hàng đợi ứng dụng. Việc tính thời gian do đó nên bắt đầu từ thời điểm đến theo lịch trình ban đầu của mỗi request, bao gồm cả khoản chờ bên ngoài engine này. Kết hợp các phép đo trong mục này, người ta phải tính riêng accelerator xuất ra bao nhiêu token mỗi giây, một request đơn chờ bao lâu, và mỗi giây có bao nhiêu request đúng và đúng hạn.

8.6.3 Cấu hình, accelerator và chi phí toàn tác vụ

Trước khi so sánh các cấu hình, hãy quy đổi các phép đo thành tốc độ xử lý. Một khi batching, độ chính xác và cài đặt cache được cố định, chia công việc hoàn thành cho thời gian chiếm dụng accelerator cho tốc độ xử lý thực tế của từng giai đoạn: tốc độ prefill rPr_P, đo bằng số token đầu vào mới được xử lý mỗi giây, và tốc độ decode rDr_D, đo bằng số lần gọi decode hoàn thành mỗi giây. Khi nhiều request trong một batch chia sẻ một lượt thực thi, mỗi request được tính là một lần gọi hoàn thành, và thời gian chiếm dụng chỉ được đếm một lần. Với một request có LL token đầu vào mới và GG token đầu ra, thời gian chiếm dụng accelerator (tính bằng GPU-giây) là

tGPU=LrP+G−1rD.t_{\mathrm{GPU}}=\frac{L}{r_P}+\frac{G-1}{r_D}.

Nếu không có phép đo khớp, người ta có thể ước tính bằng cách nhân các giá trị thông số kỹ thuật với hiệu suất: thời gian prefill được ước tính là phép tính ma trận chia cho tích của đỉnh tính toán và hiệu suất tính toán; thời gian một vòng decode được ước tính là số byte trọng số và KV được đọc chia cho tích của băng thông đỉnh và hiệu suất băng thông. Bảng dưới đây cho hiệu suất đo được trên RTX PRO 6000 trong Thí nghiệm 8-1. Một vòng decode được lấy từ các vòng trong nhật ký từng vòng của engine không bao gồm prefill; khoảng thời gian mà client thấy còn bao gồm thời gian giao hàng và dài hơn một chút, ví dụ 26.5 ms trong Mục 8.1.1.

Context 2K, không có tiền tố dùng chungbatch 1batch 4batch 16batch 64
Trọng số và KV đọc mỗi vòng15.44 GB16.34 GB19.97 GB34.46 GB
Thời gian cần tại 1792 GB/s8.62 ms9.12 ms11.14 ms19.23 ms
Vòng decode đo được26.26 ms26.62 ms27.44 ms29.63 ms
Hiệu suất băng thông decode33%34%41%65%
Hiệu suất tính toán prefill62%61%61%58%

Hiệu suất decode tăng theo batch size không phải vì việc đọc trở nên nhanh hơn. Thí nghiệm này tắt CUDA Graph, nên mỗi vòng phải đệ trình các kernel từng cái một: lượng đọc tăng từ 15.44 GB lên 34.46 GB, trong khi vòng chỉ tăng từ 26.26 ms lên 29.63 ms — batch càng lớn, chi phí gần như cố định này được phân bổ cho càng nhiều byte. Hiệu suất prefill, trong khi đó, giữ ổn định ở 58%–62% đỉnh BF16; với đầu vào 8K có 6K đầu tiên đã được cache, con số là 52%–58%.

Hiệu suất băng thông trong bảng là MBU từ Mục 1.2.2, và hiệu suất tính toán là MFU. Tại batch 1, một vòng decode vượt cận dưới đọc 8.62 ms khoảng 17.6 ms, và khoảng cách này hầu như không đổi theo lượng đọc. Theo tiêu chí trong Mục 1.3.4, đây không phải là chi phí của model mà là chi phí đệ trình các kernel từng cái một cùng với việc lập lịch và đồng bộ phía host — loại chi phí mà CUDA Graph và lập lịch tốt hơn có thể loại bỏ; trường hợp vLLM v0.6.0 trong Mục 5.5.1 cho thấy chi phí phía host có thể được giảm đến đâu.

Ví dụ toàn diện: chọn các cấu hình chia sẻ tiền tố, nén và phỏng đoán dưới các ràng buộc bộ nhớ và hạn chót. Thông lượng hiệu dụng cung cấp tiêu chuẩn để đánh giá dịch vụ, trong khi phân tích dung lượng và thời gian có thể loại trừ các cấu hình không phù hợp trước khi chạy bất cứ thứ gì. Lấy instance inference được định nghĩa ở đầu chương: RTX PRO 6000 cấp 32 GiB cho instance này; sau khi trừ trọng số và không gian làm việc cơ bản, còn 12 GiB cho KV và các buffer phụ trợ. Mười sáu request dài đến đồng thời, mỗi cái có 8192 token đầu vào và 256 token đầu ra, trong đó 6144 token đầu vào đầu tiên giống hệt nhau; hạn chót là 7 giây. Bốn sơ đồ dưới đây được so sánh, giả định cả bốn đều cho câu trả lời đúng. Sơ đồ chia sẻ đã cache tiền tố chung trước khi các request đến. Sơ đồ phỏng đoán dùng model bản nháp DFlash từ Thí nghiệm 8-5. Bật nó làm tăng bộ nhớ GPU đỉnh của tiến trình khoảng 3.1 GiB (từ 17,706 MiB lên 20,894 MiB), trong đó khoảng 1.95 GiB là trọng số của model bản nháp. Model bản nháp cũng phải xử lý đầu vào; trong Thí nghiệm 8-5, với đầu vào khoảng 2300 token, thời gian đến token đầu tiên tăng từ 129.4 ms lên 141.6 ms, tăng 9.4%. Giả sử tập request này trung bình 3 token đầu ra mỗi vòng.

Sơ đồ chia sẻ cho tập request này chính xác là điều kiện được đo trong Thí nghiệm 8-1: đầu vào 8K, 6K đầu được cache, batch 16. Giai đoạn prefill xử lý 32,768 token mới trong 2.04 giây, cho rP≈16,059r_P\approx16{,}059 token mới/s, đạt 58% đỉnh tính toán; sau đó mỗi vòng decode mất 27.35 ms, cho rD=16/0.02735≈585r_D=16/0.02735\approx585 lần gọi/s. Mỗi request chiếm tGPU=2048/16059+255/585≈0.563t_{\mathrm{GPU}}=2048/16059+255/585\approx0.563 GPU-giây, tổng cộng khoảng 9.01 GPU-giây cho 16 request. Các sơ đồ còn lại được suy ra từ bảng này và các phép đo trong cùng điều kiện; bảng dưới đây cho thời gian từng giai đoạn cho toàn bộ batch 16 request.

Sơ đồTổ chức trạng tháiTrước khi token đầu tiên trả về (prefill toàn batch)Thực thi sau khi token đầu tiên trả về
ABF16, context độc lập7.34 s255 vòng, 29.63 ms mỗi vòng
BBF16, tiền tố dùng chung2.04 s255 vòng, 27.35 ms mỗi vòng
Cq8_0, context độc lập7.34 s255 vòng, khoảng 28.26 ms mỗi vòng
DBF16, tiền tố dùng chung với phỏng đoán2.23 s85 vòng, 3 token đầu ra mỗi vòng, 27.35 ms mỗi vòng

A và C không chia sẻ tiền tố, nên mỗi request phải xử lý trọn vẹn 8192 token; phép tính ma trận của toàn bộ batch là 2137.5 TFLOP, tại cùng hiệu suất tính toán 58% mất khoảng 7.34 giây. A đọc 34.46 GB mỗi vòng, giống như 64 request tại context 2K, cho 29.63 ms từ bảng; KV của C được nén 34/6434/64, đọc 25.40 GB mỗi vòng, nội suy giữa các cột 19.97 GB và 34.46 GB trong bảng thành khoảng 28.26 ms — chưa tính thời gian chuyển đổi định dạng. Prefill của D nhiều hơn B 9.4%, khoảng 2.23 giây. Trong D, mỗi vòng xác minh 8 vị trí cho mỗi một trong 16 request, cần khoảng 2.56 TFLOP phép tính ma trận, tại hiệu suất 58% mất khoảng 8.8 ms — ngắn hơn 27.35 ms mỗi vòng của B; theo Mục 8.2.1, lấy giá trị lớn hơn trong hai giá trị, mỗi vòng vẫn là 27.35 ms. Lượt lan truyền tới của mạng bản nháp cũng hoàn thành trong cùng vòng; trong Thí nghiệm 8-5, độ trễ trung vị mỗi vòng của DFlash không dài hơn một bước decode thông thường (Mục 8.5.2), nên không có thêm thời gian nào được cộng vào đây.

Trước tiên kiểm tra bộ nhớ có đủ không. A cần 16×1188=1900816\times1188=19008 MiB, vượt 12 GiB, và không thể tiếp nhận tập các request này đồng thời. B chỉ cần một bản sao tiền tố cộng 16 trạng thái riêng, tổng cộng 6048 MiB, khoảng 5.91 GiB. Trạng thái của C, nén 34/6434/64, tổng cộng khoảng 9.86 GiB. D thêm 3188 MiB lên trên B, tổng cộng 9236 MiB, khoảng 9.02 GiB. B, C và D đều vừa trong bộ nhớ, nên so sánh thời gian mỗi sơ đồ cần để hoàn thành các request.

Tổng thời gian của B là 2.04+255×0.02735=9.012.04+255\times0.02735=9.01 giây, nhất quán với độ trễ request trung vị 9.02 giây đo được trong Thí nghiệm 8-1, nhưng vượt hạn chót 7 giây. Trên card này, chỉ 255 vòng decode đã mất 6.97 giây; sinh 256 token đầu ra từng cái một cho cả 16 request cùng nhau không thể kịp giờ. C mất việc tái sử dụng tiền tố, với prefill tăng từ 2.04 giây lên 7.34 giây, cho tổng thời gian 7.34+255×0.02826=14.557.34+255\times0.02826=14.55 giây. D xuất ra 3 token mỗi vòng, sinh 255 token còn lại trong 85 vòng, cho tổng thời gian

TD=2.23+85×0.02735=4.56 s.T_D=2.23+85\times0.02735=4.56\ \mathrm{s}.

Với A bị loại do thiếu bộ nhớ và B, C bị loại do quá thời gian, chỉ D trả về cả 16 câu trả lời đúng đúng hạn. Phép so sánh này dùng, theo thứ tự, dung lượng sau chia sẻ tiền tố, kích thước dưới định dạng nén, thời gian mỗi vòng và số đầu ra mỗi vòng. Trên card này, thời gian một vòng decode hầu như không đổi theo batch size hay độ dài context; chỉ giảm số vòng mới có thể rút ngắn đáng kể giai đoạn sinh.

Hình 8-35 vẽ hai phép kiểm tra này trên cùng một mặt phẳng. Vượt qua đường đứt nét theo chiều ngang nghĩa là KV và các buffer phụ trợ vượt 12 GiB; vượt qua nó theo chiều dọc nghĩa là thời gian hoàn thành vượt 7 giây. Chỉ D nằm trong cả hai giới hạn.

Hình 8-35: So sánh các cấu hình cho 16 request dài trên RTX PRO 6000. Trục hoành là mức sử dụng KV và buffer phụ trợ, sau khi trừ trọng số dùng chung và không gian làm việc cơ bản; trục tung là tổng thời gian cho toàn bộ batch request, suy ra từ các phép đo trong Thí nghiệm 8-1 và 8-5. A thiếu bộ nhớ và không thể chạy trên instance này; hình vẽ cho thấy thời gian nó sẽ cần. Vùng được tô bóng thỏa mãn cả hai giới hạn 12 GiB và 7 giây. A là BF16 context độc lập, B là BF16 tiền tố dùng chung, C là q8_0 context độc lập, và D là BF16 tiền tố dùng chung cộng giải mã phỏng đoán.

Cuối cùng, so sánh chi phí. Thời gian card này chi cho mỗi kết quả đạt chuẩn bằng tổng thời gian từ lúc đến đến khi hoàn thành đầy đủ của batch, chia cho số kết quả đạt chuẩn. Với D, con số này là 4.56/16≈0.2854.56/16\approx0.285 GPU-giây; tại giới hạn công suất 600 W, năng lượng cho mỗi kết quả đạt chuẩn không quá khoảng 171 J. B chiếm card này 9.01 giây, nhưng dưới hạn chót 7 giây tạo ra không kết quả đạt chuẩn nào — toàn bộ thời gian và năng lượng đó là lãng phí.

Lựa chọn thay đổi thế nào nếu bộ nhớ thu nhỏ hoặc đầu ra ngắn lại? Nếu bộ nhớ khả dụng cho KV và các buffer phụ trợ giảm từ 12 GiB xuống 6 GiB, yêu cầu khoảng 9.02 GiB của D không còn vừa; yêu cầu khoảng 5.91 GiB của B vừa nhưng sẽ quá hạn. Tập request này khi đó không có sơ đồ khả thi nào, chỉ còn lại các lựa chọn như nới hạn chót, rút ngắn đầu ra, hoặc chuyển sang một nguồn bản nháp không tiêu thụ bộ nhớ GPU (xem Bài tập 8-9). Nếu bộ nhớ giữ nguyên nhưng đầu ra được rút ngắn xuống 16 token, B cần 2.04+15×0.02735=2.452.04+15\times0.02735=2.45 giây, và D cần 2.23+5×0.02735=2.372.23+5\times0.02735=2.37 giây. Cả hai đều hoàn thành đúng hạn, và D chỉ nhanh hơn 0.08 giây: 0.19 giây prefill thêm mà model bản nháp thực hiện gần như triệt tiêu thời gian tiết kiệm được bằng cách chạy ít hơn 10 vòng decode.

Phân tích trên chỉ bao trùm thời gian thực thi trong instance. Tính thời lượng của một tác vụ agent hoàn chỉnh cũng đòi hỏi thêm thời gian thực thi công cụ và chờ đợi bên ngoài. Giả sử một tác vụ mất tổng cộng 100 giây, trong đó decode chiếm 50 giây; tăng tốc decode gấp bốn lần thay đổi tổng thời gian thành 50+50/4=62.550+50/4=62.5 giây, gia tốc tổng thể 1.6 lần. Nếu decode ban đầu chỉ chiếm 20 giây, tổng thời gian thay vào đó trở thành 80+20/4=8580+20/4=85 giây, gia tốc tổng thể chỉ khoảng 1.18 lần. Tổng quát, gọi ff là tỉ phần của tổng thời gian ban đầu bị decode chiếm giữ, và giai đoạn đó được tăng tốc lên ss lần, với thời gian của các giai đoạn khác không đổi; định luật Amdahl khi đó cho

Stask=1(1−f)+f/s.S_{\mathrm{task}}=\frac{1}{(1-f)+f/s}.

Hình 8-36: Các đường gia tốc Amdahl với thời gian của các giai đoạn khác cố định và không có công việc chuẩn bị thêm. Ba đường lấy decode lần lượt là 20%, 50% và 80% thời gian đường cơ sở. Tỉ phần thời gian ban đầu mà giai đoạn này chiếm giữ càng cao, lợi ích từ một gia tốc cục bộ càng lớn.

Trong Hình 8-36, tỉ phần decode càng thấp, đường cong xẹp xuống càng sớm: ngay cả khi giai đoạn sinh tiếp tục tăng tốc, các lời gọi công cụ và thời gian chờ khác vẫn chiếm giữ tỉ phần thời gian của chúng. Việc so sánh mức chiếm dụng accelerator và tiêu thụ năng lượng cũng phải bao trùm các giai đoạn này, và nên được tính dựa trên số cuối cùng của các request đạt chuẩn. Gọi TGPUT_{\mathrm{GPU}} là tổng thời gian accelerator bị chiếm dụng trong cửa sổ quan sát, EobsE_{\mathrm{obs}} là tổng năng lượng tiêu thụ, và NgN_g là số kết quả đạt chuẩn; khi đó mỗi kết quả đạt chuẩn trung bình chiếm TGPU/NgT_{\mathrm{GPU}}/N_g thời gian GPU và tiêu thụ Eobs/NgE_{\mathrm{obs}}/N_g năng lượng. Thí nghiệm 8-9 đọc các bộ đếm năng lượng của cả GPU lẫn gói CPU (toàn bộ gói xử lý) trên cùng một card: dưới continuous batching tại các tốc độ đến 1, 4 và 16 req/s, mỗi kết quả đạt chuẩn tiêu thụ lần lượt 615.0, 652.0 và 1083.7 J; tiếp nhận từng cái một tại 4 req/s tiêu thụ 1982.3 J. Tại 16/s, thông lượng hoàn thành của continuous batching gần với giá trị tại 4/s, nhưng nhiều phản hồi hơn đến muộn, và năng lượng mỗi kết quả đạt chuẩn cao hơn khoảng 66%. Ngay cả khi một câu trả lời sai hoặc trả về quá muộn, năng lượng đã tiêu thụ vẫn phải được tính vào tổng chi phí.

Bài tập 8-8 · Tính toán: gia tốc giai đoạn và chuẩn bị bản nháp thay đổi tổng thời gian tác vụ như thế nào. Một tác vụ 100 giây gồm 20 giây prefill, 50 giây decode và 30 giây sử dụng công cụ và chờ đợi. Giảm một nửa thời gian của từng phần trong ba phần, và tìm tổng thời gian tác vụ cho mỗi trường hợp. Riêng biệt, bắt đầu từ tác vụ gốc, giảm thời gian decode xuống một phần tư giá trị ban đầu và thêm 8 giây thời gian chuẩn bị bản nháp; tìm gia tốc ròng, và tính tại thời gian chuẩn bị nào sự thay đổi này không còn tiết kiệm thời gian.

Bài tập 8-9 · Cốt lõi · Thiết kế toàn diện: giảm thời gian GPU chiếm giữ mỗi kết quả đạt chuẩn trong các ràng buộc dung lượng và hạn chót. Dùng dung lượng và thời gian của ví dụ toàn diện trên RTX PRO 6000, xây Sơ đồ E trên nền B: chia sẻ tiền tố BF16, và chuyển bản nháp sang tra cứu từ context (Mục 8.5.2), với chỉ mục được giữ trong bộ nhớ host, không tiêu thụ bộ nhớ GPU và không thêm prefill; mỗi request xuất ra trung bình 2 token mỗi vòng, tiếp theo là 128 vòng sau đó, với toàn bộ batch mất 27.35 ms mỗi vòng cộng 0.1 ms tra cứu. Tính footprint bộ nhớ, tổng thời gian và số GPU-giây chiếm giữ mỗi kết quả đạt chuẩn của E, và so sánh với B và D; sau đó riêng biệt thay đổi bộ nhớ khả dụng cho KV và các buffer phụ trợ xuống 6 GiB, và thay đổi độ dài đầu ra xuống 16 token, rồi lựa chọn lại. Trong phần thí nghiệm mở, chạy cả hai cấu hình trên cùng một tập bài toán và cùng một dấu vết (trace) đến, ghi lại thời điểm đến, đầu ra đầu tiên, hoàn thành, độ đúng và trạng thái đỉnh của tất cả các request, và tính thông lượng hiệu dụng bằng các công thức của mục này.

Lịch sử và đọc thêm

Việc tiếp nhận request mới theo từng vòng lặp, paged attention (attention phân trang) và cache theo cây tiền tố (prefix-tree caching) lần lượt thúc đẩy lập lịch request, cấp phát trạng thái và tái sử dụng phép tính. Các thiết kế liên quan của Orca, PagedAttention và SGLang có thể được đọc theo các hướng này; Sarathi-Serve và NanoFlow tiếp tục nghiên cứu chunking (chia khối), pipelining (đường ống) và sự chồng lấp giữa các tài nguyên khác nhau.

Việc phục vụ đa adapter trong Mục 8.2.1 có thể được tiếp nối qua Punica và S-LoRA, các hệ thống lần lượt quản lý adapter và trạng thái request, thực thi phép tính hạng thấp của từng adapter đồng thời chia sẻ một model nền chung.

Attention lai (hybrid attention) và các model hồi quy (recurrent) mở rộng việc lập chỉ mục cache thành một bài toán khôi phục trạng thái; Marconi và các công trình liên quan thảo luận về cơ hội khôi phục và giá trị của cache. Offload trọng số, mã hóa tensor và thực thi bộ nhớ hợp nhất (unified memory) liên hệ dung lượng accelerator với chi phí liên kết; các bản ghi M2 Max đi kèm cho thấy mức cấp phát KV thực tế và quá trình xử lý đầu vào của các model nhỏ.

Các quy tắc chấp nhận và hiệu chỉnh của speculative sampling (lấy mẫu phỏng đoán) đặt nền tảng cho việc bảo toàn phân bố mục tiêu; các phương pháp sau đó nháp từ tra cứu context, dự đoán bản nháp từ các đặc trưng trung gian và sinh bản nháp song song chủ yếu thay đổi chi phí mỗi vòng và độ dài bản nháp được chấp nhận. Các tác vụ agent hoàn chỉnh cũng bao gồm các giai đoạn công cụ và giai đoạn bên ngoài; các bản ghi tốc độ và tác vụ đã công bố có thể được đọc cùng với phân rã giai đoạn ở Mục 8.6.

Chú thích nguồn

Tóm tắt chương

Chương này bắt đầu từ quá trình thực thi của một request đơn lẻ và phân tích dần dần mối quan hệ giữa dung lượng bộ nhớ, cách bố trí phép tính và kết quả dịch vụ. Các request khác nhau chia sẻ trọng số, trong khi KV tăng theo context của riêng từng request. Batching giảm lượng đọc trọng số được phân bổ cho mỗi token đầu ra, nhưng mỗi request vẫn phải đọc context của chính nó. Lợi ích này phụ thuộc vào hiệu năng của các tài nguyên bên dưới: một khi trọng số thay vào đó được phục vụ từ bộ lưu trữ nhanh hơn, độc lập hơn, batching không còn phân bổ việc đọc trọng số, và các batch size cùng các sơ đồ giải mã phỏng đoán khác nhau phải được so sánh lại từ đầu. Continuous batching tận dụng ngay các slot thực thi được giải phóng bởi các request ngắn, trong khi chunked prefill giảm sự can thiệp của các đầu vào dài lên quá trình sinh của các request hiện có.

Cách KV được quản lý quyết định một instance có thể xử lý bao nhiêu request đồng thời. Phân trang cấp phát không gian theo nhu cầu, chia sẻ chỉ giữ một bản sao của tiền tố chung, và cache tiền tố tiếp tục loại bỏ phép tính dư thừa. Việc nối thêm, viết lại và tóm tắt context thay đổi mức tái sử dụng cache cũng như chi phí lưu trữ và đọc trạng thái sau đó; khi so sánh các cách tổ chức context này với các tối ưu hóa phục vụ khác, cần áp dụng cùng các yêu cầu chất lượng tác vụ. Nén tiếp tục giảm dung lượng lưu trữ; offload chuyển một phần trọng số ra khỏi accelerator, với cái giá là việc di chuyển lặp lại. Giải mã phỏng đoán thêm chi phí sinh và xác minh bản nháp, nhưng cho phép một phép tính model mục tiêu duy nhất quyết định nhiều token đầu ra. Tất cả các thay đổi này có thể được so sánh qua footprint bộ nhớ, độ trễ mỗi vòng và số đầu ra mỗi vòng.

Ví dụ toàn diện ở cuối chương trước tiên kiểm tra bộ nhớ có đủ hay không, sau đó xác định hạn chót có thể được đáp ứng hay không, và cuối cùng so sánh thời gian GPU chiếm giữ cho mỗi kết quả đạt chuẩn. Với chia sẻ tiền tố, 16 request dài trước đây không thể vừa giờ có thể thực thi đồng thời; nhưng trên RTX PRO 6000, sinh từng token một cho 256 token đầu ra vẫn sẽ lỡ hạn chót, và chính giải mã phỏng đoán, bằng cách giảm số vòng decode, giúp chúng hoàn thành đúng hạn. Khi bộ nhớ khả dụng giảm, tập request này không tìm được sơ đồ khả thi nào; khi đầu ra được rút ngắn, lợi thế của giải mã phỏng đoán gần như biến mất. Lý do đằng sau mỗi sự thay đổi lựa chọn đều có thể truy về các khối lượng dữ liệu, thời gian thực thi và số đầu ra đã được suy ra trong chương này.

Sau khi hoàn thành các so sánh cấu hình trong chương này, ba loại kết quả nên được ghi lại: trạng thái của bao nhiêu request có thể được giữ đồng thời dưới một accelerator, độ chính xác và độ dài request nhất định; prefill và decode mỗi quá trình xử lý công việc nhanh đến mức nào; và độ trễ request thay đổi thế nào theo batch size và tốc độ đến. Các kết quả này đã bao gồm ảnh hưởng của việc lập lịch, cache và hiệu quả kernel.

Chương tiếp theo xây dựng trên các kết quả này để quyết định mỗi loại accelerator đảm nhận công việc gì, cần bao nhiêu đơn vị thực thi, và KV được truyền và chia sẻ như thế nào. Nhiều accelerator có thể cùng thực thi một instance duy nhất, hoặc tạo thành nhiều bản sao hoàn chỉnh, hoặc các nhóm giai đoạn được lập lịch riêng. Các quyết định batching xác định tài nguyên hiện có xử lý request như thế nào, trong khi các quyết định triển khai xác định các tài nguyên này phân chia công việc ra sao; cùng nhau chúng định hình hiệu năng của dịch vụ hoàn chỉnh.