Bỏ qua để đến nội dung

Deploy LLM: nhiều GPU, phân tán

Sau bài này, bạn sẽ:

  • Ước lượng được bộ nhớ khi suy luận (inference): phần trọng số cố định, và phần KV cache lớn lên theo độ dài chuỗi và số người dùng.
  • Phân biệt được hai giai đoạn prefill và decode, cùng hai số đo độ trễ (latency) TTFT và TPOT.
  • Chọn được cách chia mô hình ra nhiều GPU khi phục vụ: song song tensor (tensor parallelism) trong một máy, song song pipeline (pipeline parallelism) giữa các máy.
  • Hiểu ý chính của continuous batching và PagedAttention, hai kỹ thuật giúp một GPU phục vụ nhiều người hơn.

Ở bài Deploy mô hình riêng, bạn chạy một LLM mở trên một máy, thử một mình thì trơn tru. Rồi công ty muốn mở cho 200 nhân viên cùng dùng, với mô hình 70 tỉ tham số. Bạn gặp hai bức tường.

Bức tường thứ nhất: mô hình không vừa. 70 tỉ tham số, mỗi tham số 2 byte, là khoảng 140 GB. Nhiều GPU máy chủ cỡ lớn có 80 GB. Mô hình phải được chia ra nhiều GPU.

Bức tường thứ hai: vừa rồi vẫn hết chỗ. Ngay cả mô hình nhỏ vừa gọn một GPU, khi vài chục người chat những cuộc dài cùng lúc, bộ nhớ vẫn đầy. Thủ phạm là KV cache.

Hãy hình dung một nhà hàng. Trọng số là gian bếp: rộng cố định, có một khách hay một trăm khách cũng vậy. KV cache là bàn ăn: mỗi nhóm khách cần một bàn, nối dài thêm khi họ gọi thêm món (thêm token). Bếp phải vừa nhà hàng đã, rồi phần sàn còn lại quyết định bạn tiếp được bao nhiêu nhóm.

Một câu trả lời đi qua hai giai đoạn. Đầu tiên mô hình đọc cả lời nhắc: mọi token đã có sẵn nên được xử lý cùng lúc, như đọc lướt đề bài. Sau đó nó viết từng token một, như viết bài bằng tay: chưa viết xong chữ này thì chưa biết chữ sau.

Mỗi bước viết chỉ sinh một token cho mỗi chuỗi, nhưng phải dùng tới toàn bộ trọng số. Một cách nghĩ thường gặp (bài báo PagedAttention, mục 2.2, cũng mô tả decode như vậy): lúc này GPU tốn thời gian đọc trọng số từ bộ nhớ nhiều hơn là tính. Khi đó, gom nhiều chuỗi vào cùng một bước rất lợi: đọc trọng số một lần, phục vụ nhiều người. Vì thế server LLM cố gom thật nhiều yêu cầu vào một lô (batch), và cần thật nhiều chỗ cho KV cache.

Bài toán của bài này: một mô hình cần bao nhiêu bộ nhớ khi phục vụ nhiều người, chia nó ra nhiều GPU thế nào khi không vừa, và làm sao xếp các yêu cầu để GPU luôn bận mà người dùng không phải chờ lâu?

Mô hình có Ψ\Psi tham số (cùng ký hiệu với bài Họ LLaMA), mỗi tham số lưu bằng ss byte. Phần trọng số chiếm:

Mw≈Ψ⋅sM_{\text{w}} \approx \Psi \cdot s

Với BF16, s=2s = 2; với int8, s=1s = 1; với int4, s=0.5s = 0.5. Dấu “xấp xỉ” vì lượng tử hóa (quantization) còn lưu thêm hệ số co giãn, như bài Đánh giá mô hình và PEFT đã nói. Phần này cố định: phục vụ một người hay sáu mươi người thì cũng chừng đó.

Đếm từ bảng kiến trúc trong bài báo Llama 3 (Grattafiori và cộng sự, 2024), bản 8B có 8.03 tỉ tham số, bản 70B có 70.55 tỉ (tự tính, ô code ở mục Ví dụ code đếm lại). Ở BF16, đó là 15.0 GiB và 131.4 GiB. Một GiB là 2302^{30} byte, khoảng 1.074 GB, nên 131.4 GiB là khoảng 141 GB: bản 70B không vừa một GPU 80 GB, kể cả khi chưa có người dùng nào.

Lượng tử hóa thu nhỏ phần này (70B ở int4 còn 32.9 GiB, tự tính), đổi lại chất lượng có thể giảm. Bài báo Llama 3 (mục 6.2) khi chạy bản 405B ở FP8 chỉ lượng tử hóa phần lớn tham số và kích hoạt (activation) của các lớp FFN (khoảng 50% thời gian tính khi suy luận), không lượng tử hóa tham số của attention. Họ thấy chất lượng nhạy với một số kiểu lượng tử hóa, nên thêm vài thay đổi, trong đó có để nguyên lớp Transformer đầu và cuối.

Ký hiệu như bài Họ LLaMA: NlayerN_{\text{layer}} lớp, HkvH_{\text{kv}} đầu K/V, dd chiều mỗi đầu, ss byte mỗi số; thêm độ dài chuỗi LL và số chuỗi đang phục vụ cùng lúc BB (batch).

Bước 1: một token, một lớp. Vectơ khóa của token này có Hkv⋅dH_{\text{kv}} \cdot d số (mỗi đầu K/V một vectơ dd chiều), vectơ giá trị cũng vậy. Lưu cả hai: 2⋅Hkv⋅d2 \cdot H_{\text{kv}} \cdot d số.

Bước 2: mọi lớp. Mỗi lớp attention có K, V riêng, nên nhân với NlayerN_{\text{layer}}. Đổi ra byte thì nhân với ss. Một token chiếm:

2⋅Nlayer⋅Hkv⋅d⋅s byte2 \cdot N_{\text{layer}} \cdot H_{\text{kv}} \cdot d \cdot s \ \text{byte}

Bước 3: cả chuỗi, rồi cả lô. Mỗi token đã xử lý (cả lời nhắc lẫn phần đã sinh) có một mục, nên một chuỗi dài LL token cần gấp LL lần. Mỗi chuỗi có cache riêng, không dùng chung với chuỗi khác, nên BB chuỗi cần gấp BB lần:

MKV=2⋅Nlayer⋅Hkv⋅d⋅s⋅L⋅BM_{\text{KV}} = 2 \cdot N_{\text{layer}} \cdot H_{\text{kv}} \cdot d \cdot s \cdot L \cdot B

Công thức là tích của các thừa số, nên nó tăng tuyến tính theo từng thừa số: LL gấp 4 thì cache gấp 4, BB gấp 16 thì cache gấp 16.

Áp vào số thật. Llama 3 8B có Nlayer=32N_{\text{layer}} = 32, Hkv=8H_{\text{kv}} = 8, d=4 096/32=128d = 4\,096/32 = 128. Ở BF16, mỗi token cần 2⋅32⋅8⋅128⋅2=131 0722 \cdot 32 \cdot 8 \cdot 128 \cdot 2 = 131\,072 byte (128 KiB), như bài Họ LLaMA đã tính. Từ đó (tự tính):

Llama 3 8B, KV cache ở BF16LLBBKV cacheSo với trọng số (15.0 GiB)
Một cuộc chat dài8 19211.0 GiBnhỏ hơn nhiều
16 cuộc chat dài8 1921616.0 GiBđã vượt
16 tài liệu rất dài32 7681664.0 GiBgấp hơn 4 lần

Số đầu truy vấn không có trong công thức, nên GQA có ích khi triển khai: nếu Llama 3 8B dùng 32 đầu K/V thay vì 8, một chuỗi 8 192 token cần 4.0 GiB thay vì 1.0 GiB, và cùng GPU đó phục vụ được ít người hơn 4 lần.

GPU có MGPUM_{\text{GPU}} byte. Trừ phần trọng số, rồi chia phần còn lại cho KV cache của một chuỗi (B=1B = 1), ta được số chuỗi tối đa:

Bmax⁡≈⌊MGPU−MwMKV(L,B=1)⌋B_{\max} \approx \left\lfloor \frac{M_{\text{GPU}} - M_{\text{w}}}{M_{\text{KV}}(L, B = 1)} \right\rfloor

Ký hiệu ⌊⋅⌋\lfloor \cdot \rfloor là làm tròn xuống, vì không có “nửa người dùng”. Với Llama 3 8B ở BF16 trên GPU 80 GB (giả sử dùng được đúng 80⋅10980 \cdot 10^9 byte), chuỗi 8 192 token: trọng số chiếm 8.03⋅109⋅2≈16.06⋅1098.03 \cdot 10^9 \cdot 2 \approx 16.06 \cdot 10^9 byte (chính là 15.0 GiB), phần trống là 80−16.06≈63.980 - 16.06 \approx 63.9 GB, chia cho 1 GiB (khoảng 1.074 GB) mỗi chuỗi được khoảng 59.5, tức 59 chuỗi (tự tính).

Đây là ước lượng cận trên: nó bỏ qua kích hoạt tạm thời, phần server tự giữ lại và nhiều thứ khác, nên thực tế ít hơn. Nhưng nó cho thấy điều quan trọng: trọng số quyết định mô hình có vừa không, KV cache quyết định phục vụ được bao nhiêu người. vLLM in ra phần thứ hai này khi khởi động: dòng “GPU KV cache size” (tổng số token KV cache chứa được) và “Maximum concurrency” (ước lượng số yêu cầu chạy cùng lúc), theo trang Parallelism and Scaling của vLLM.

Prefill (giai đoạn nạp lời nhắc) xử lý mọi token của lời nhắc cùng lúc, điền KV cache cho chúng ở mọi lớp, rồi cho ra token đầu tiên. Decode (giai đoạn sinh) lặp lại: lấy token vừa sinh, tính K, V của nó, thêm một mục vào KV cache ở mỗi lớp, rồi sinh token kế tiếp. Đây chính là vòng lặp sinh của bài Xây dựng LLM đầu tiên, có thêm cache.

Hai giai đoạn được đo riêng. Trang benchmark của vLLM 0.32 định nghĩa:

  • TTFT (time to first token, thời gian tới token đầu): từ lúc gửi yêu cầu tới lúc nhận phần đầu tiên của câu trả lời, tức là gồm thời gian chờ trong hàng đợi và prefill. Vì thế lời nhắc dài làm TTFT dài.
  • TPOT (time per output token, thời gian mỗi token sinh ra): thời gian trung bình giữa các token sau token đầu, tức nhịp của decode. Câu trả lời có NN token, mất tổng cộng ttotalt_{\text{total}} giây, thì:
TPOT=ttotal−TTFTN−1\text{TPOT} = \frac{t_{\text{total}} - \text{TTFT}}{N - 1}

Mẫu số là N−1N - 1 vì token đầu đã tính vào TTFT. Viết ngược lại, ttotal=TTFT+(N−1)⋅TPOTt_{\text{total}} = \text{TTFT} + (N - 1) \cdot \text{TPOT}. Ví dụ, TTFT 0.5 giây, TPOT 40 ms, câu trả lời 201 token: tổng là 0.5+200⋅0.04=8.50.5 + 200 \cdot 0.04 = 8.5 giây.

Tên gọi chưa thống nhất: bài báo Llama 3 gọi độ trễ của decode là “time-to-incremental-token”, và trang của vLLM nhắc rằng mỗi công cụ benchmark đặt tên một kiểu. Khi so số, hãy xem số đo bắt đầu và kết thúc ở đâu.

Thông lượng và độ trễ kéo nhau. Thông lượng (throughput) ở đây là tổng số token mỗi giây của cả server. Lô lớn hơn thì thông lượng tăng, nhưng mỗi bước tính cho nhiều chuỗi hơn nên lâu hơn, và mỗi người thấy chữ chạy chậm lại. Hình 24 của bài báo Llama 3 vẽ đường đánh đổi này theo cỡ lô, khi so có và không micro-batching. Chọn điểm nào tùy mục tiêu: phục vụ thật nhiều người, hay trả lời thật nhanh. Như ở bài Deploy mô hình riêng, hãy đo TTFT và TPOT bằng p50, p95.

Khi suy luận không có gradient, không có trạng thái bộ tối ưu, chỉ còn trọng số và KV cache phải chia.

Song song tensor: trong một máy. Mỗi GPU giữ một phần ma trận của mọi lớp; với attention, mỗi GPU lo một số đầu (chia theo đầu như Megatron-LM; nếu số GPU nhiều hơn HkvH_{\text{kv}}, ví dụ mô hình có 8 đầu K/V như Llama 3 mà chia tensor ra 16 GPU, thì đầu K/V thường phải nhân bản, như bài báo Llama 2 nhắc với MQA), nên cũng chỉ giữ KV cache của các đầu đó. Đổi lại, sau mỗi lớp các GPU phải cộng gộp kết quả, ở mọi bước sinh token. Trao đổi dày đặc như vậy cần đường truyền rất nhanh, như NVLink trong một máy.

Song song pipeline: giữa các máy. Mỗi GPU (hay mỗi máy) giữ một đoạn các lớp, cùng KV cache của đoạn đó. Giữa hai đoạn kề nhau chỉ chuyền kích hoạt, nên chịu được đường truyền chậm hơn. Bong bóng (pipeline bubble), vấn đề lớn khi huấn luyện, theo bài báo Llama 3 (mục 6.1) không còn là vấn đề khi suy luận, vì không có lượt lan truyền ngược buộc phải xả đường ống. Họ chia lô thành micro-batch để các giai đoạn cùng bận: thông lượng tăng, đổi lại độ trễ tăng vì thêm điểm đồng bộ.

Ví dụ thật: Llama 3 405B. Theo cùng mục 6.1, bản 405B ở BF16 không vừa một máy 8 GPU H100: 405⋅109⋅2=810405 \cdot 10^9 \cdot 2 = 810 GB, trong khi 8 GPU 80 GB có 640 GB (tự tính). Họ chạy trên 16 GPU của hai máy: song song tensor trong mỗi máy nhờ NVLink, song song pipeline giữa hai máy.

Còn song song dữ liệu (data parallelism)? Nó không giúp mô hình vừa bộ nhớ, vì GPU nào cũng giữ cả mô hình. Nhưng ở mức dịch vụ, nó là cách mở rộng quen thuộc: chạy nhiều bản mô hình (mỗi bản có thể đã chia bằng tensor, pipeline) sau một bộ cân bằng tải (load balancer).

Trang Parallelism and Scaling của vLLM gợi ý chọn theo thứ tự:

Tình huống (cho một bản mô hình)Cách chia
Vừa một GPUKhông cần chia
Không vừa một GPU, vừa một máySong song tensor, số phần bằng số GPU của máy
Không vừa một máySong song tensor bằng số GPU mỗi máy, song song pipeline bằng số máy
GPU không có NVLinkƯu tiên song song pipeline
Số GPU không chia đều được mô hìnhSong song pipeline, vì chia theo lớp thì không cần chia đều

Vừa rồi thì xem số người dùng đồng thời trong log. Chưa đủ thì thêm GPU hoặc thêm bản.

Continuous batching: lập lịch theo từng bước

Phần tiêu đề “Continuous batching: lập lịch theo từng bước”

Batching tĩnh giống chiếc xe buýt chỉ cho khách lên ở bến đầu và chỉ mở cửa ở bến cuối: lô chạy tới khi chuỗi dài nhất xong. Chuỗi ngắn xong từ lâu vẫn chiếm ghế, còn yêu cầu mới phải chờ chuyến sau.

Orca (Yu và cộng sự, OSDI 2022) đề xuất lập lịch theo từng vòng lặp (iteration-level scheduling), ngày nay thường gọi là continuous batching. Bộ lập lịch chỉ cho mô hình chạy một bước sinh token trên lô, rồi quyết định lại. Chuỗi nào xong thì trả kết quả ngay, chỗ trống được yêu cầu mới lấp ở bước kế tiếp. Xe buýt giờ mở cửa ở mọi trạm. Vì các chuỗi trong lô dài ngắn khác nhau, Orca còn đề xuất selective batching: chỉ gộp lô cho một số phép tính. Paper báo cáo thông lượng gấp 36.9 lần NVIDIA FasterTransformer ở cùng mức độ trễ, trên GPT-3 175B, với phần cứng và tải của họ.

Lô lớn cần chỗ cho KV cache. Kwon và cộng sự (2023) chỉ ra chỗ phí: KV cache của mỗi yêu cầu lớn dần khi sinh, không biết trước sẽ dài bao nhiêu, nên các hệ thống trước đó giữ trước một vùng nhớ liền nhau đủ cho độ dài tối đa. Câu trả lời thường ngắn hơn nhiều, nên phần giữ trước bỏ không; các vùng to nhỏ khác nhau còn để lại khe hở. Trong đo đạc của paper, các hệ thống đó chỉ dùng 20.4% tới 38.2% bộ nhớ KV cache cho trạng thái của token thật.

Ý tưởng của PagedAttention mượn từ cách hệ điều hành quản lý bộ nhớ ảo bằng phân trang (paging):

  • KV cache của mỗi chuỗi được chia thành các khối (block), mỗi khối chứa K, V của một số token cố định.
  • Các khối không cần nằm liền nhau. Mỗi yêu cầu có một bảng khối (block table) ghi khối thứ mấy của chuỗi nằm ở chỗ nào trong bộ nhớ.
  • Khối mới chỉ được cấp khi các khối trước đã đầy. Không giữ trước cho độ dài tối đa.

Như nhà hàng thôi giữ trọn bàn 16 ghế cho nhóm 2 người, mà kê thêm ghế khi có người tới. Nhờ bảng khối, các chuỗi còn dùng chung được khối, ví dụ khi sinh nhiều câu trả lời cho cùng một lời nhắc. Paper báo cáo gần như không phí bộ nhớ KV cache, và thông lượng gấp 2 tới 4 lần FasterTransformer và Orca ở cùng mức độ trễ. Hệ thống cài đặt ý này là vLLM; code của vLLM nay đã khác bản gốc, nên bài chỉ dừng ở ý của paper.

Speculative decoding (giải mã suy đoán; Leviathan và cộng sự, 2022) cho một mô hình nhỏ đoán trước vài token, rồi mô hình lớn kiểm tra chúng song song trong một lượt. Cách lấy mẫu của họ bảo đảm phân phối đầu ra không đổi so với chỉ dùng mô hình lớn. Paper báo cáo nhanh 2 tới 3 lần trên T5-XXL.

  • Số bộ nhớ là ước lượng, giả định KV cache lưu ở BF16. Số thật lấy từ log của server.
  • Kiến trúc khác thì phải đếm lại. Mixture of experts, hay các cách thu nhỏ KV cache khác GQA, làm đổi các con số; mixture of experts thuộc bài Họ DeepSeek.
  • Bài không nói tới nhiều kỹ thuật phục vụ khác (dùng lại cache cho tiền tố chung, tách prefill và decode ra máy riêng…); xem tài liệu của vLLM, SGLang ở mục Đọc thêm.

Hình dưới là hình ba kiểu song song của bài Họ LLaMA. Lần này hãy đọc nó cho suy luận: bỏ qua các mũi tên “gradient” và “cộng gradient”, rồi nhìn hàng tensor (mỗi lớp bị cắt đôi, trao đổi ở mọi lớp) và hàng pipeline (mỗi GPU một đoạn lớp, chỉ chuyền kích hoạt).

Song song dữ liệu: chia lôLớp 1Lớp 2Lớp 3Lớp 4GPU 1GPU 2Song song tensor: chia ma trận trong lớpLớp 1Lớp 2Lớp 3Lớp 4GPU 1GPU 2Song song pipeline: chia các lớpmicro-batchLớp 1Lớp 2Lớp 3Lớp 4GPU 1GPU 2Nửa lô ANửa lô Bcộng gradientCả lôcộng kết quả4321kích hoạtgradient
Cùng một mô hình 4 lớp trên 2 GPU; phần tô đậm là thứ bị chia. ZeRO/FSDP (chia thêm trạng thái của song song dữ liệu) và song song context không có trong hình.

Bảng dưới tóm tắt ba kiểu khi suy luận. Hãy nhìn cột “Trao đổi”: nó quyết định kiểu nào hợp với đường truyền nào.

Kiểu song songChia gìTrao đổiDùng khi suy luận
Song song dữ liệuYêu cầu (mỗi bản giữ cả mô hình)Không cầnNhân bản để phục vụ thêm người
Song song tensorMa trận trong mỗi lớpSau mỗi lớp, ở mọi bướcTrong một máy có NVLink
Song song pipelineCác lớpKích hoạt giữa hai đoạn kề nhauGiữa các máy, hoặc GPU không có NVLink

Code trong bài chạy thử lần cuối tháng 10/2026, với Python 3.14 (bản có trong Pyodide). Ô tính bộ nhớ chạy ngay trên trang. Các lệnh vllm serve ở cuối mục cần máy có GPU; bài chưa chạy thử chúng, chỉ đối chiếu với tài liệu chính thức của vLLM.

Ô dưới đây viết hai công thức của mục Lý thuyết thành hai hàm Python thuần. n_params đếm tham số từ kiến trúc, với DD là số chiều của mô hình (dim trong code): bốn ma trận của attention (WQ\mathbf{W}_Q, WO\mathbf{W}_O cỡ D×DD \times D; WK\mathbf{W}_K, WV\mathbf{W}_V cỡ D×HkvdD \times H_{\text{kv}} d), ba ma trận của SwiGLU, cộng bảng nhúng và lớp đầu ra (cỡ từ vựng nhân DD), bỏ qua các vectơ nhỏ của RMSNorm. kv_bytes là đúng công thức MKVM_{\text{KV}}. Ô in kết quả cho hai mô hình Llama 3, ba cặp (L,B)(L, B), và hai câu hỏi “nếu thì”.

Bộ nhớ trọng số và KV cache của Llama 3

# Bộ nhớ để phục vụ một LLM chỉ có decoder: trọng số + KV cache
GIB = 1024**3
# Số kiến trúc của Llama 3 (bảng 3 của paper), từ vựng V = 128 000
CFG = {
"8B": dict(layers=32, dim=4096, ffn=14336, heads=32, kv=8),
"70B": dict(layers=80, dim=8192, ffn=28672, heads=64, kv=8),
}
V = 128_000
def n_params(c):
dh = c["dim"] // c["heads"] # số chiều mỗi đầu
attn = 2 * c["dim"] ** 2 + 2 * c["dim"] * c["kv"] * dh # Wq, Wo, Wk, Wv
ffn = 3 * c["dim"] * c["ffn"] # SwiGLU: 3 ma trận
return c["layers"] * (attn + ffn) + 2 * V * c["dim"] # + nhúng, đầu ra
def kv_bytes(c, L, B, s=2):
dh = c["dim"] // c["heads"]
return 2 * c["layers"] * c["kv"] * dh * s * L * B # s byte mỗi số
for name, c in CFG.items():
p = n_params(c)
w = [p * s / GIB for s in (2, 1, 0.5)] # BF16, int8, int4
print(f"{name}: {p / 1e9:.2f}B params, weights "
f"{w[0]:.1f} / {w[1]:.1f} / {w[2]:.1f} GiB")
for L, B in [(8192, 1), (8192, 16), (32768, 16)]:
kv = kv_bytes(c, L, B) / GIB
print(f" KV cache L={L:>5}, B={B:>2}: {kv:6.1f} GiB")
c = CFG["8B"]
mha = dict(c, kv=32) # giả sử mỗi đầu truy vấn có K, V riêng
print(f"8B, 32 K/V heads, L=8192: {kv_bytes(mha, 8192, 1) / GIB:.1f} GiB")
free = 80e9 - n_params(c) * 2 # GPU 80 GB trừ trọng số BF16
print("8B on 80 GB, max B at L=8192:", int(free // kv_bytes(c, 8192, 1)))

Lần chạy đầu cần tải Python, khoảng 10 MB.

Đọc kết quả:

  • Số tham số: 8.03B và 70.55B, khớp tên gọi. Trọng số ở BF16, int8, int4: 15.0 / 7.5 / 3.7 GiB và 131.4 / 65.7 / 32.9 GiB.
  • KV cache của 8B: 1.0, 16.0 và 64.0 GiB: BB gấp 16 thì cache gấp 16, rồi LL gấp 4 thì cache gấp 4.
  • KV cache của 70B: 2.5, 40.0 và 160.0 GiB. Mỗi token cần 320 KiB, vì có 80 lớp thay vì 32. Riêng cache ở dòng ba đã lớn hơn một GPU 80 GB: dù trọng số ở int4, cấu hình này vẫn phải chia ra nhiều GPU.
  • Dòng “32 K/V heads”: 4.0 GiB thay vì 1.0 GiB. GQA làm cache nhỏ đi đúng bằng tỉ lệ 32/832/8.
  • Dòng cuối: 59 chuỗi, đúng ước lượng ở mục Lý thuyết.

Thử sửa code: đổi s=2 thành s=1 trong kv_bytes để xem KV cache lưu ở 8 bit, hoặc đổi 80e9 thành dung lượng GPU của bạn.

Biết cần chia thế nào rồi, bạn chỉ cần báo cho server. Hai lệnh dưới theo ví dụ trên trang Parallelism and Scaling của vLLM; <tên mô hình> là chỗ giữ chỗ.

Cửa sổ terminal
# Một máy 4 GPU: song song tensor 4 phần
vllm serve <tên mô hình> --tensor-parallel-size 4 --port 8001
# Hai máy, mỗi máy 8 GPU: tensor 8 trong máy, pipeline 2 giữa hai máy.
# Cần dựng cụm Ray trước, theo hướng dẫn trên trang của vLLM.
vllm serve <đường dẫn tới mô hình> \
--tensor-parallel-size 8 \
--pipeline-parallel-size 2 \
--distributed-executor-backend ray

Chia mô hình, continuous batching và quản lý khối KV cache đều nằm phía server. Phía client không đổi: vẫn là code của bài Sử dụng API văn bản, chỉ trỏ OPENAI_BASE_URL vào http://<máy chủ>:<cổng>/v1 và đặt LLM_MODEL đúng tên mô hình đã truyền cho vllm serve. vLLM (bản 0.32.0) và SGLang (bản 0.5.22) là hai server mã nguồn mở làm việc này, tại thời điểm viết (10/2026); bài chỉ nêu làm ví dụ, không so sánh hai công cụ.

Câu 1/4 Server chạy Llama 3 8B ở BF16: trọng số khoảng 15 GiB, KV cache khoảng 16 GiB khi phục vụ 16 chuỗi dài 8 192 token. Bạn lượng tử hóa trọng số xuống int4, còn KV cache vẫn lưu ở BF16. Với cùng 16 chuỗi đó, câu nào đúng?
Câu 2/4 Một chatbot RAG ghép khoảng 20 000 token tài liệu vào mỗi lời nhắc. Người dùng phàn nàn: gõ câu hỏi xong phải chờ lâu mới thấy chữ đầu tiên, nhưng sau đó chữ chạy ra đều và nhanh. Số đo nào cao, và vì sao?
Câu 3/4 Bạn phục vụ một mô hình 70B ở BF16 (khoảng 131 GiB trọng số). Có 2 máy, mỗi máy 4 GPU 80 GB nối với nhau bằng NVLink; giữa hai máy là mạng chậm hơn nhiều. Theo bài, cách nào hợp lý nhất?
Câu 4/4 Một lô có 4 yêu cầu: ba yêu cầu cần sinh 20 token, một yêu cầu cần 500 token. So batching tĩnh với continuous batching, câu nào đúng?

Ý chính:

  • Bộ nhớ khi suy luận có hai phần. Trọng số Mw≈Ψ⋅sM_{\text{w}} \approx \Psi \cdot s cố định, quyết định mô hình có vừa không. KV cache MKV=2⋅Nlayer⋅Hkv⋅d⋅s⋅L⋅BM_{\text{KV}} = 2 \cdot N_{\text{layer}} \cdot H_{\text{kv}} \cdot d \cdot s \cdot L \cdot B tăng theo độ dài và số chuỗi, quyết định phục vụ được bao nhiêu người.
  • Prefill đọc cả lời nhắc và quyết định TTFT; decode sinh từng token và quyết định TPOT, với ttotal=TTFT+(N−1)⋅TPOTt_{\text{total}} = \text{TTFT} + (N - 1) \cdot \text{TPOT}. Lô lớn tăng thông lượng nhưng tăng độ trễ mỗi người.
  • Không vừa một GPU: song song tensor trong một máy (cần NVLink), thêm song song pipeline giữa các máy. Cần phục vụ thêm người: nhân bản mô hình sau bộ cân bằng tải.
  • Continuous batching lập lịch theo từng bước sinh token, nên lô luôn đầy. PagedAttention chia KV cache thành khối cấp theo nhu cầu, nên ít phí bộ nhớ và lô lớn hơn.

Đọc thêm:

Nội dung theo giấy phép CC BY-NC-SA 4.0. Mã nguồn theo giấy phép MIT.Giới thiệu