Tại sao LLM tự host bị chậm khi có nhiều người dùng?
LLM của bạn khựng lại khi có 5 người dùng vì giới hạn mặc định của num_parallel. Bài viết giải thích cách tối ưu batching, KV cache và prefill để tăng khả năng xử lý đồng thời.
Tại sao LLM tự host lại chậm đi khi có thêm người dùng?
Khi LLM tự host bị nghẽn ở 5 người dùng đồng thời, nguyên nhân thường là server được cấu hình chỉ tạo một câu trả lời mỗi lần, còn 4 người dùng kia phải xếp hàng. Tài liệu của Ollama nêu rõ giá trị mặc định: OLLAMA_NUM_PARALLEL là “số request song song tối đa mà mỗi model xử lý cùng lúc, mặc định là 1”. Không có gì bị hỏng, và phần mềm serving có thể xử lý nhiều request cùng lúc. 4 trong 5 người phải chờ đến lượt vì một setting quy định như vậy.
Cách khắc phục hiếm khi là nâng cấp máy lớn hơn. Cần một serving engine có thể đưa nhiều request qua model trong cùng một forward pass, cùng với đủ memory dự phòng để giữ conversation của mọi người trong lúc xử lý. Cả hai phần đều quan trọng, nhưng chính phần thứ hai mới thực sự quyết định giới hạn của bạn.
Hai giai đoạn mà mọi request đều phải trải qua
Prefill đọc toàn bộ prompt cùng lúc và xây dựng attention cache cho nó. Mọi token trong prompt đều đi qua model cùng nhau, vì vậy prefill là một phép nhân ma trận lớn và bị giới hạn bởi thông lượng tính toán (arithmetic throughput). Sau đó, decode ghi câu trả lời từng token một. Mỗi token yêu cầu đọc toàn bộ trọng số (weights) của model từ bộ nhớ một lần nữa, trong khi khối lượng tính toán cho một token đó là rất nhỏ. Decode bị giới hạn bởi băng thông bộ nhớ (memory bandwidth).
Sự bất đối xứng đó chính là lý do tại sao batching lại hiệu quả. Việc decode cho một người dùng đọc khoảng 5 GB trọng số mỗi token và để hầu hết các đơn vị tính toán ở trạng thái nhàn rỗi. Thêm một request thứ hai, engine vẫn đọc cùng 5 GB đó một lần, sau đó tính toán hai token từ đó. Người dùng thứ hai gần như không tốn thêm thời gian. Việc phục vụ các request nối tiếp nhau một cách nghiêm ngặt sẽ làm lãng phí lợi thế này.
Hai con số mô tả trải nghiệm của người dùng. TTFT (thời gian đến token đầu tiên) là thời gian chờ trong hàng đợi cộng với thời gian prefill. ITL (độ trễ giữa các token) là khoảng cách giữa các token được stream, và nó được quyết định bởi decode. Một server chậm thường bị chậm ở một trong hai giai đoạn này, và cách khắc phục không giống nhau. Bạn cần xác định mình đang gặp vấn đề ở giai đoạn nào trước khi thay đổi bất kỳ thiết lập nào, và đo thời gian prefill và decode riêng biệt là cách để bạn tìm ra điều đó.
Static batching khiến mọi người phải chờ phản hồi chậm nhất
Static batching là phiên bản sơ khai, đây là kết quả khi bạn tự nhóm các request trong code ứng dụng. Engine thu thập N request, chạy chúng cùng lúc và giữ mọi slot cho đến khi quá trình tạo văn bản dài nhất trong nhóm hoàn tất.
Một người dùng yêu cầu tóm tắt 1.200 token sẽ giữ bốn câu trả lời ngắn bị kẹt trong batch, vì batch không giải phóng slot nào cho đến khi thành viên chậm nhất hoàn thành.
Hai chi phí phát sinh sau đó. Các chuỗi đã hoàn thành vẫn chiếm dụng slot mà không thực hiện tính toán hữu ích nào, do đó throughput thực tế giảm khi độ dài output thay đổi, và độ dài output của chat thì thay đổi rất nhiều. Một request đến sau khi batch đã hình thành một bước sẽ phải đợi toàn bộ batch xử lý xong trước khi bắt đầu prefill, nghĩa là TTFT của nó bị quyết định bởi bài viết dài của người khác.
Continuous batching tiếp nhận và giải phóng request theo từng token
Continuous batching lập lịch ở cấp độ từng bước decoding. Sau mỗi bước, scheduler loại các sequence vừa phát ra stop token, rồi tiếp nhận các request đang chờ vào những slot còn trống. Một reply kết thúc ở bước 40 sẽ giải phóng slot ngay tại bước 40, không phải khi cả batch kết thúc.
Đây không phải tính năng đặc biệt. llama-server mô tả -cb, --cont-batching là “có bật continuous batching hay không (còn gọi là dynamic batching) (mặc định: bật)”, liệt kê “Parallel decoding with multi-user support” trong danh sách tính năng, và từ một thay đổi được merge vào tháng 11 năm 2025, tính năng này mặc định khởi động với 4 slot (-np mặc định là -1, tức tự động). vLLM được xây dựng dựa trên ý tưởng này. Ollama cũng phục vụ các request song song. Cấu hình mặc định chỉ giới hạn số lượng ở mức một, nên nhiều người kết luận rằng phần cứng của họ không thể xử lý concurrency, trong khi chính cấu hình của họ đã tắt tính năng này.
Các kết quả continuous batching được công bố thường đo trên những card datacenter vừa còn dư năng lực tính toán, vừa có hàng chục gigabyte dành cho cache. Đặc điểm của các kết quả đó vẫn áp dụng cho máy của bạn. Quy mô thì không, và phần bộ nhớ bên dưới giải thích lý do.
Prefill cạnh tranh tài nguyên tính toán với decode
Khi một request mới gửi đến trong lúc bốn phản hồi khác đang được stream, prompt của nó phải được prefill trước, và prefill là tác vụ tốn nhiều tài nguyên tính toán. Nếu bộ lập lịch (scheduler) dành riêng một bước cho quá trình prefill này, bốn người dùng đang nhận stream sẽ không nhận được token nào trong khoảng thời gian đó. Với các prompt dài, đây là độ trễ có thể nhận thấy rõ trên mọi cửa sổ đang mở. Đây chính là hiện tượng giật (stutter) mà người dùng thường nhắc đến khi nói rằng server bị khựng mỗi khi có người khác nhấn gửi yêu cầu.
Chunked prefill chia nhỏ một prompt dài thành các phần và trộn từng phần vào cùng một bước với các tiến trình decode đang chạy. Hướng dẫn tinh chỉnh của vLLM nêu rõ sự đánh đổi này: ngân sách chunk nhỏ hơn "đạt được ITL tốt hơn vì có ít tiến trình prefill làm chậm quá trình decode hơn", trong khi các giá trị cao hơn "đạt được thời gian phản hồi token đầu tiên (TTFT) tốt hơn vì bạn có thể xử lý nhiều token prefill hơn trong một batch". Bạn đang phải chọn ưu tiên trải nghiệm của ai: người đang chờ phản hồi bắt đầu, hay những người đang xem văn bản hiển thị.
Độ dài prompt quyết định mức độ ảnh hưởng của vấn đề này. Một prompt 6,000 token với câu trả lời 200 token tương đương với 6,000 token công việc prefill so với 200 bước decode. Chat có hỗ trợ truy xuất (RAG) và các system prompt dài đều đẩy bạn vào tình trạng này, khiến prefill không còn là sai số nhỏ mà trở thành tác vụ chính khiến người dùng phải chờ đợi. Prefix caching giúp ích khi phần dài của prompt bị lặp lại: vLLM cung cấp --enable-prefix-caching, tính năng này tái sử dụng cache cho một prefix prompt dùng chung thay vì phải tính toán lại cho từng request.
Bộ nhớ cạn trước là KV cache
Mỗi token trong mọi cuộc hội thoại đang hoạt động để lại một vector key và một vector value ở mỗi layer của model. Đó là KV cache (key/value cache). KV cache giúp bước decode không phải tính lại toàn bộ prompt cho mỗi token mới. Số value được cache cho mỗi token là cố định theo cấu trúc của model: 2 (một key, một value) nhân với số layer, số head key/value và head dimension. Đọc các giá trị đó từ config.json của model. Nhân với số byte trên mỗi value để tính kích thước. Thừa số cuối là thiết lập runtime, không thuộc model: llama-server lưu cache ở f16 (2 byte) theo mặc định và hỗ trợ các kiểu nhỏ hơn như q8_0 thông qua -ctk và -ctv.
Tính một lần thì giới hạn sẽ không còn khó hiểu. Một model 8B điển hình có 36 layer, 8 head key/value và head dimension bằng 128, lưu cache ở 16-bit, sẽ tốn 2 36 8 128 2 byte cho mỗi token. Con số này là 147,456 byte, khoảng 144 KiB. Một cuộc hội thoại có 8,192 token cần khoảng 1.2 GB cache. Năm cuộc hội thoại cần khoảng 6 GB, chưa tính weights. Đó mới là câu trả lời thực tế cho việc hệ thống hỗ trợ được bao nhiêu người dùng.
Concurrency nhân số context, và các công cụ thể hiện rõ điều đó. FAQ của Ollama ghi: "Xử lý request song song cho một model sẽ làm tăng kích thước context theo số request song song. Ví dụ, context 2K với 4 request song song sẽ tạo context 8K và cấp phát thêm bộ nhớ." RAM cần thiết tăng theo OLLAMA_NUM_PARALLEL nhân với OLLAMA_CONTEXT_LENGTH. Trong llama-server, số slot tự động mặc định chạy mọi slot trên một KV buffer hợp nhất. Vì vậy, toàn bộ context -c là một pool duy nhất để các slot dùng chung. Tự đặt -np sẽ tắt buffer hợp nhất, trừ khi bạn đồng thời truyền --kv-unified. Khi đó, context bạn yêu cầu bằng -c được chia cho -np slot. Chỉ tăng số slot sẽ làm giảm lượng context mỗi request có thể giữ. Hãy đọc context trên mỗi slot từ startup log thay vì tự giả định.
vLLM cấp phát trước. --gpu-memory-utilization (mặc định 0.92) là "tỷ lệ bộ nhớ GPU được dùng cho model executor". Nếu không đặt --kv-cache-memory-bytes, vLLM sẽ suy ra kích thước KV cache từ tỷ lệ này. Phần ngân sách còn lại sau khi trừ weights và working memory đã được engine profile sẽ trở thành paged KV pool. Khi pool này cạn, scheduler sẽ evict một request thay vì làm request thất bại:
WARNING 05-09 00:49:33 scheduler.py:1057] Sequence group 0 is preempted by PreemptionMode.RECOMPUTE mode because there is not enough KV cache space.Trong engine V1 của vLLM, mode preemption mặc định là RECOMPUTE. Vì vậy, request bị evict sẽ mất cache và thực hiện prefill lại khi được admit lại. Công việc đó được thực hiện hai lần. Tài liệu cảnh báo rằng "preemption và recomputation có thể ảnh hưởng xấu đến latency đầu-cuối". Dòng log này là lời giải thích rõ nhất cho việc một người dùng không may phải chờ lâu hơn nhiều so với những người khác trong khi latency trung bình vẫn bình thường. Đặt disable_log_stats=False để ghi log số lần tích lũy, hoặc đọc preemption counter từ các Prometheus metrics mà vLLM cung cấp.
Những thay đổi ở mức 2, 5 và 20 người dùng đồng thời
Hai người dùng. Gần như không đáng kể trên GPU có dư cache, vì luồng giải mã thứ hai chạy cùng luồng thứ nhất với rất ít thời gian tăng thêm. Trên VPS chỉ dùng CPU với 4 đến 8 GB RAM, điều này không miễn phí: cả hai luồng chia sẻ cùng một số lượng vCPU và băng thông RAM, nên mỗi người dùng chỉ nhận được khoảng một nửa số token mỗi giây, và nhu cầu cache tăng gấp đôi trong khi ngân sách tài nguyên lại nhỏ hơn nhiều.
Năm người dùng. Đây là lúc các thiết lập mặc định không còn đủ và vấn đề bắt đầu xuất hiện ở hàng đợi. Với OLLAMA_NUM_PARALLEL ở mức 1, bốn người phải đợi người đang yêu cầu câu trả lời dài, và mỗi người trong số họ sẽ thấy tốc độ bình thường ngay khi đến lượt. Tăng số lượng xử lý song song sẽ làm thay đổi bản chất vấn đề: năm slot với mỗi slot 8K context tạo ra 40K token cache cần xử lý. Nếu không đủ VRAM, engine sẽ đẩy các layer sang RAM hệ thống, và nếu không đủ RAM, máy chủ sẽ thực hiện swap và tốc độ token mỗi giây sẽ sụp đổ.
Hai mươi người dùng. Hai mươi người trong giao diện chat thường không phải là hai mươi yêu cầu đồng thời, và đây là điều hữu ích nhất cần hiểu trước khi mua phần cứng. Một người đọc phản hồi và suy nghĩ từ 20 đến 60 giây giữa các lượt, nên phần lớn phiên làm việc của họ là ở trạng thái nhàn rỗi. Hai mươi agent, hoặc hai mươi tác vụ tóm tắt tài liệu, là hai mươi luồng thực sự không có thời gian nhàn rỗi. Đó là một bài toán phần cứng khác. Một lập trình viên đã trỏ một coding agent vào server Ollama của riêng họ sẽ gần với trường hợp thứ hai hơn trường hợp đầu, vì agent liên tục gửi yêu cầu chừng nào tác vụ còn chạy và không có khoảng nghỉ đọc như con người.
Người dùng của bạn có thực sự gửi request đồng thời, hay chỉ đăng nhập?
Hãy tính số request đang xử lý trước khi chọn cấu hình. Phép tính rất đơn giản: số request đang xử lý bằng số người dùng nhân với số giây cần để sinh output cho mỗi lượt, rồi chia cho số giây giữa các lượt.
- Trước tiên, hãy đo tốc độ của một luồng duy nhất trên máy của bạn, bao gồm cả prefill và decode. Đừng lấy số liệu từ GPU của người khác: đo số token mỗi giây trên chính máy của bạn và sử dụng kết quả đó.
- Ước tính duty cycle. 20 người dùng chat, mỗi lượt sinh trong 12 giây và mỗi lượt cách nhau 90 giây sẽ cho 20 * 12 / 90, tức khoảng 2.7 request đang xử lý.
- Đặt số slot cao hơn một chút so với giá trị đó, rồi đối chiếu với bộ nhớ: số slot nhân với context cho mỗi request phải nằm trong số token cache thực tế mà bạn có.
- Giữ queue ngắn để request vượt quá giới hạn fail nhanh và hiển thị rõ.
Số token cache khả dụng bằng bộ nhớ trống sau khi trừ weights, chia cho chi phí trên mỗi token được nêu ở phần trước. GPU 24 GB chạy model 8B ở 16-bit dùng khoảng 16 GB cho weights và có nhiều nhất khoảng 6 GB cache khả dụng ở mức utilisation mặc định. Sau khi dành riêng một phần cho working memory, con số này sẽ thấp hơn một chút, tương đương khoảng 5 cuộc hội thoại 8K. Để chứa nhiều request hơn, hãy rút ngắn context cho mỗi request hoặc lưu cache ở 8-bit (llama-server tốn --cache-type-k q8_0). Cả hai cách đều tăng concurrency bằng cách đánh đổi một phần khác. Bạn nên đọc kỹ sự đánh đổi này trước khi bỏ tiền mua hardware: điểm hòa vốn của GPU VPS so với token API.
Khi các thiết lập mặc định của Ollama không còn đủ
Hãy tăng số lượng xử lý song song thông qua unit của service, vì lệnh export trong shell sẽ không có tác dụng với một daemon do systemd quản lý.
sudo systemctl edit ollama.service[Service]
Environment="OLLAMA_NUM_PARALLEL=4"
Environment="OLLAMA_CONTEXT_LENGTH=8192"
Environment="OLLAMA_MAX_QUEUE=64"sudo systemctl daemon-reload
sudo systemctl restart ollama
systemctl show ollama --property=Environment
ollama pssystemctl show sẽ in ra ba biến bạn vừa thiết lập. Nếu không thấy, nghĩa là file drop-in chưa được lưu và mọi thao tác khác của bạn đều vô nghĩa. ollama ps sau đó liệt kê model đã load với kích thước lớn hơn trọng số đơn thuần, vì bốn slot với 8,192 token sẽ chiếm dụng 32,768 token bộ nhớ cache đi kèm. Cột PROCESSOR hiển thị một phần model trên CPU trong khi bạn muốn toàn bộ nằm trên GPU nghĩa là bạn đã yêu cầu lượng cache vượt quá dung lượng còn trống của card. Hãy giảm một trong hai con số này. Giảm context thường an toàn hơn, nhưng một cửa sổ quá nhỏ sẽ âm thầm cắt bớt các prompt dài thay vì báo lỗi, vì vậy hãy cân nhắc kỹ lưỡng num_ctx thay vì cứ giảm dần cho đến khi model vừa vặn.
Mặc định của hàng đợi cần được xem xét lại. Ollama xếp hàng tối đa OLLAMA_MAX_QUEUE yêu cầu, và "mặc định là 512". Vượt quá con số đó, nó sẽ phản hồi "với lỗi 503 cho biết server đang quá tải". Một hàng đợi sâu 512 trên một máy chủ chỉ xử lý bốn yêu cầu cùng lúc là một lời hứa bạn không thể thực hiện, vì client ở vị trí 300 sẽ timeout từ lâu trước khi đến lượt. Một hàng đợi ngắn sẽ trả về lỗi để ứng dụng của bạn có thể thử lại hoặc báo cáo, điều này tốt hơn là một vòng xoay chờ đợi không bao giờ kết thúc.
Hãy kiểm tra thực tế. Gửi hai yêu cầu cùng lúc từ hai terminal và theo dõi cả hai. Nếu yêu cầu thứ hai không tạo ra kết quả gì cho đến khi yêu cầu thứ nhất hoàn tất, nghĩa là thiết lập song song chưa có hiệu lực.
Khi một serving engine thực sự bắt đầu phát huy hiệu quả
vLLM đáng để cài đặt thêm khi bạn có GPU còn dư tài nguyên và thực sự có hơn khoảng bốn request đang xử lý đồng thời. Scheduler của nó hoạt động theo từng token, cache được chia trang để tái sử dụng các phân mảnh còn trống, và VRAM dư được chuyển thành khả năng xử lý đồng thời thay vì để không. Tính đến tháng 10 năm 2026, quy trình cài đặt và khởi chạy GPU được ghi nhận gồm ba command. vllm download-kernels tải các kernel đã biên dịch sẵn mà nếu không thì vLLM sẽ phải tải xuống và biên dịch khi khởi động:
uv pip install vllm --torch-backend=auto
vllm download-kernels
vllm serve Qwen/Qwen2.5-1.5B-Instructcurl http://localhost:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "Qwen/Qwen2.5-1.5B-Instruct",
"messages": [{"role": "user", "content": "Say hello."}]
}'Phản hồi chứa một mảng choices có nghĩa là server đã chạy và model đã được load. Khi có tải, hai tham số quan trọng là --max-num-seqs, "số sequence tối đa được xử lý trong một iteration", và --max-num-batched-tokens, "số token tối đa có thể được xử lý trong một iteration". Tham số thứ nhất giới hạn concurrency. Tham số thứ hai là ngân sách chunked prefill được mô tả ở trên.
Khi có dưới khoảng bốn request đang xử lý đồng thời, vLLM làm tăng độ phức tạp nhưng đem lại ít lợi ích. Không còn bắt buộc phải dùng GPU: tính đến tháng 10 năm 2026, hướng dẫn cài đặt CPU ghi nhận khả năng "inferencing và serving model cơ bản" trên CPU x86 và Arm, với wheel x86 dựng sẵn từ version 0.17.0 và một CPU Docker image. Tuy nhiên, cách sử dụng memory của vLLM vẫn giữ nguyên. Trên CPU, nó áp dụng cùng tỷ lệ sử dụng 0.92 cho system RAM và cấp phần memory còn lại sau khi nạp weights cho KV cache. Vì vậy, theo mặc định, nó chiếm phần lớn memory của máy ngay khi khởi động, trừ khi bạn giới hạn cache bằng --kv-cache-memory-bytes. Đây là lựa chọn không phù hợp trên VPS 4 đến 8 GB. Trong trường hợp đó, nên dùng model nhỏ hơn, context ngắn hơn và một queue do bạn kiểm soát. vLLM và Ollama khác nhau thế nào với vai trò serving engine trình bày đầy đủ cách lựa chọn, còn chạy Qwen 3 8B trên VPS cho thấy một model cỡ trung cần những gì trước khi bạn thêm dù chỉ một user.
Sự đánh đổi mà các quan niệm phổ biến thường bỏ qua
Continuous batching làm tăng tổng throughput và thường cải thiện cả độ trễ trung bình (median latency), vì các request trong hàng đợi được xử lý sớm hơn. Tuy nhiên, độ trễ đuôi (tail latency) lại diễn biến theo hướng ngược lại, và khía cạnh này hiếm khi được nhắc đến.
Mỗi sequence bổ sung trong một bước đều làm tăng khối lượng công việc, vì vậy ITL tăng lên đối với tất cả người dùng khi batch được lấp đầy. Quá trình prefill của một request mới đến sẽ chiếm một phần tài nguyên của bước mà lẽ ra người dùng đang streaming sẽ được hưởng. Khi chịu áp lực về cache, scheduler sẽ thực hiện preemption, đẩy một request đang tạo dở dang quay trở lại trạng thái bắt đầu prefill.
Giao diện chat thể hiện độ trễ đuôi, không phải giá trị trung bình. Một stream bị khựng lại hai giây giữa chừng sẽ bị coi là lỗi, ngay cả khi tổng thời gian hoàn thành vẫn tốt. Hãy đo lường p95 TTFT và p95 ITL dưới mức tải dự kiến, và coi số lượng token trung bình mỗi giây là con số về công suất thay vì là mô tả trải nghiệm người dùng.
Thiết lập thực tế dựa trên nguyên tắc đó. Hãy giới hạn concurrency thấp hơn một chút so với mức bộ nhớ cho phép để engine không bao giờ phải thực hiện preemption. Một hàng đợi ngắn và có thể dự đoán được sẽ tốt hơn một batch lớn gây ra tình trạng thrashing, bởi vì người dùng chờ bốn giây rồi stream mượt mà sẽ hài lòng hơn người dùng bắt đầu ngay lập tức nhưng bị khựng lại hai lần.
Những điểm cần kiểm tra khi hệ thống chậm
Mỗi người dùng đều bình thường, nhưng thời gian chờ đợi lâu. Đây là vấn đề hàng đợi, không phải vấn đề tốc độ. Hãy kiểm tra cài đặt song song trước. Model đang phục vụ chính xác, mỗi lần một request.
Lỗi HTTP 503 từ Ollama. Hàng đợi đã đầy. Hoặc là máy chủ thực sự đã đạt giới hạn công suất, hoặc OLLAMA_MAX_QUEUE được đặt thấp một cách có chủ đích để giảm tải, đây chính là điều bạn muốn hệ thống thực hiện.
Số lượng token mỗi giây giảm mạnh khi chịu tải trên máy chạy CPU. Hãy chạy vmstat 1 trong khi tình trạng này xảy ra. Các cột si và so khác không cho thấy máy đang bị swap, nghĩa là trọng số (weights) đang bị đọc từ ổ cứng trên mỗi token. Không có thay đổi cấu hình nào khắc phục được việc này. Hãy giảm kích thước model hoặc số lượng slot.
Một trong mười người dùng phải chờ lâu hơn hẳn những người khác. Tìm kiếm trong log của vLLM từ khóa preempted. Preemption và quá trình tính toán lại (recompute) thường là nguyên nhân, điều này có nghĩa là cache đang bị quá tải so với độ dài context mà bạn cho phép.
TTFT (Time To First Token) kém ngay cả khi máy chủ đang rảnh. Đây là vấn đề prefill, không phải concurrency. Các prompt dài tốn thời gian thực trước khi token đầu tiên xuất hiện, vì vậy hãy xem xét kích thước prompt và prefix caching trước khi kiểm tra phần cứng. Nếu thời gian chờ lâu chỉ xảy ra với người đầu tiên sau một khoảng lặng và những người sau đó đều ổn, thì đó không phải là prefill mà là do Ollama giải phóng model và đọc lại trọng số từ ổ cứng, bạn nên loại trừ khả năng này bằng cách giữ model luôn thường trú giữa các request.
FAQ
Vì sao LLM tự host của tôi chậm lại khi có người thứ hai sử dụng?
Thông thường, nó không thực sự chậm lại. Request được đưa vào hàng đợi. Ollama phát hành với OLLAMA_NUM_PARALLEL ở mức 1, nên request thứ hai phải chờ request thứ nhất phát ra token cuối cùng. Để phân biệt hai trường hợp, hãy đo thời gian stream của một người dùng trong khi người dùng khác đang chờ. Nếu số token mỗi giây vẫn bình thường ngay khi bắt đầu, vấn đề là hàng đợi và tăng số lượng chạy song song sẽ giải quyết được. Nếu cả hai stream đều chạy ở một nửa tốc độ, bạn thực sự đang chia sẻ memory bandwidth và đó là giới hạn phần cứng.
Một GPU nhỏ có thể phục vụ bao nhiêu người dùng đồng thời?
Hãy tính theo memory, không phải số người dùng. Trước tiên là weights, sau đó là KV cache. KV cache tiêu tốn 2 lần số layer nhân với số key/value head, nhân với head dimension, nhân với số byte, cho mỗi token của mỗi cuộc hội thoại đang hoạt động. Một model 8B điển hình có 36 layer, 8 key/value head và head dimension là 128 sẽ tiêu tốn khoảng 144 KiB cho mỗi token ở 16-bit. Vì vậy, một cuộc hội thoại 8,192 token cần khoảng 1.2 GB. Card 24 GB chứa model đó ở 16-bit chỉ còn tối đa khoảng 6 GB cho cache. Mức này tương đương khoảng 5 cuộc hội thoại ở full context, hoặc nhiều hơn nếu bạn rút ngắn context.
Continuous batching có làm phản hồi của từng người dùng chậm hơn không?
Độ trễ trung vị thường được cải thiện vì request không còn phải chờ cả batch hoàn tất. Độ trễ ở đuôi phân phối sẽ tăng. Mỗi sequence bổ sung thêm công việc vào mọi bước decoding. Một request mới đến sẽ chiếm một phần của một bước từ các user đang stream. Request bị preempt cũng phải prefill 2 lần. Hãy đo p95 inter-token latency, không chỉ đo giá trị trung bình. Cửa sổ chat làm các khoảng dừng dễ nhận thấy, trong khi giá trị trung bình có thể che khuất chúng.
Tôi nên tăng OLLAMA_NUM_PARALLEL hay chuyển sang vLLM?
Trước tiên, hãy tăng số lượng chạy song song. Cách này miễn phí, chỉ cần một file drop-in, và giải quyết trường hợp phổ biến khi 4 người phải xếp hàng sau một câu trả lời dài. Memory là giới hạn. Các request chạy song song sẽ nhân số context mà bạn phải giữ, vì vậy hãy theo dõi tình trạng layer bị chuyển sang CPU. Hãy chuyển sang vLLM khi GPU còn dư VRAM và thực sự có hơn khoảng 4 request đang chạy đồng thời. Đây là ngưỡng mà paged cache và per-token scheduling mang lại lợi ích lớn hơn chi phí.
Thêm CPU core có khắc phục được server LLM chậm không?
Không khắc phục được phần mà người dùng nhận thấy nhiều nhất. Decode đọc toàn bộ model từ memory cho mỗi token, nên bị giới hạn bởi RAM bandwidth. Các core bổ sung sẽ không còn giúp ích khi bandwidth đã bão hòa. Prefill có thể scale theo số core, vì vậy thêm core sẽ rút ngắn thời gian đến token đầu tiên với prompt dài. Trên VPS có 4 đến 8 GB, giới hạn chính thường là dung lượng memory. Cách khắc phục hiệu quả thường là dùng model nhỏ hơn hoặc rút ngắn context, thay vì thêm vCPU.