Vì sao LLM tự host chậm khi có 5 người dùng?
Một người dùng vẫn ổn nhưng 5 người thì chậm: xem batching, giới hạn KV cache, prefill và queue depth quyết định server LLM phục vụ đồng thời bao nhiêu người.
Vì sao LLM tự host bị chậm khi có thêm người dùng?
LLM tự host bị nghẽn khi có 5 người dùng đồng thời vì server vẫn chỉ tạo từng câu trả lời một, còn 4 yêu cầu 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ố yêu cầu song song tối đa mà mỗi model xử lý cùng lúc, mặc định là 1”. Không có thành phần nào bị hỏng. 4 trong 5 người dùng chỉ đang chờ đến lượt.
Cách khắc phục hiếm khi là nâng cấp server lớn hơn. Bạn cần serving engine có thể đưa nhiều yêu cầu qua model trong cùng một forward pass, đồng thời cần đủ bộ nhớ trống để lưu cuộc hội thoại của mọi người trong quá trình xử lý. Cả hai yếu tố đều quan trọng, nhưng yếu tố 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 trải qua
Prefill đọc toàn bộ prompt cùng lúc và xây dựng attention cache cho prompt đó. Tất cả token trong prompt được đưa qua model cùng nhau, nên prefill là một phép nhân ma trận lớn và bị giới hạn bởi throughput tính toán. Sau đó, decode ghi câu trả lời từng token một. Với mỗi token, hệ thống phải đọc lại toàn bộ weight của model từ memory, trong khi lượng tính toán thực hiện trên một token đó rất nhỏ. Decode bị giới hạn bởi băng thông memory.
Sự bất đối xứng này là toàn bộ lý do batching có hiệu quả. Khi decode cho một user, hệ thống đọc, chẳng hạn, 5 GB weight cho mỗi token và để phần lớn đơn vị tính toán ở trạng thái nhàn rỗi. Thêm request thứ hai, engine chỉ cần đọc cùng 5 GB một lần rồi tính toán 2 token từ đó. User thứ hai hầu như không làm tăng thời gian xử lý. Xử lý các request hoàn toàn lần lượt sẽ làm mất lợi thế này.
Hai chỉ số mô tả trải nghiệm của user. TTFT (time to first token) là thời gian chờ trong queue cộng với thời gian prefill. ITL (inter-token latency) là khoảng cách giữa các token được stream, và chỉ số này do decode quyết định. Một server chậm thường chậm ở một trong hai chỉ số này, và cách khắc phục cũng khác nhau.
Batch tĩnh khiến mọi người phải chờ phản hồi chậm nhất
Batch tĩnh là phiên bản đơn giản nhất. Bạn sẽ gặp kiểu này khi tự nhóm request trong code ứng dụng. Engine thu thập N request, chạy chúng cùng nhau và giữ mọi slot cho đến khi lượt sinh dài nhất trong nhóm hoàn tất.
Một user yêu cầu bản tóm tắt dài 1,200 token khiến bốn câu trả lời một dòng bị giữ trong batch, vì batch không giải phóng slot nào cho đến khi request chậm nhất hoàn tất.
Có 2 chi phí phát sinh. Các sequence đã hoàn tất vẫn chiếm slot nhưng không thực hiện phép tính hữu ích nào, nên throughput thực tế giảm khi độ dài output khác nhau; mà độ dài output của chat thường khác nhau rất nhiều. Một request đến sau thời điểm batch được tạo chỉ 1 step phải chờ toàn bộ batch xử lý xong rồi mới bắt đầu prefill. Vì vậy, TTFT của request đó 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à loại request sau mỗi 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 sinh 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 ở step 40 sẽ giải phóng slot ngay tại step 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)”, và 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 1. Vì vậy, nhiều người kết luận rằng phần cứng của họ không thể chạy đồng thời, trong khi chính cấu hình của họ mới là thứ không cho phép.
Các kết quả continuous batching được công bố thường đo trên những card datacenter có cả tài nguyên compute dự phòng và 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. Tuy nhiên, quy mô thì không. Phần về memory bên dưới giải thích lý do.
Prefill cạnh tranh tài nguyên compute với decode
Khi một request mới đến trong lúc 4 câu trả lời đang được stream, prompt của request đó phải được prefill trước, và prefill tốn nhiều compute. Nếu scheduler dành riêng một step cho prefill đó, 4 người dùng đang xem stream sẽ không nhận được token nào trong thời gian này. Với prompt dài, mọi cửa sổ đang mở đều bị tạm dừng thấy rõ. Đây là hiện tượng giật mà người dùng nói đến khi server bị khựng mỗi lần có người khác bấm gửi.
Chunked prefill chia prompt dài thành nhiều phần và trộn từng phần vào cùng step với các decode đang chạy. Tài liệu tuning của vLLM nêu rõ đánh đổi này: budget nhỏ hơn sẽ “đạt ITL tốt hơn vì có ít prefill làm chậm decode hơn”, còn giá trị cao hơn sẽ “đạt thời gian nhận token đầu tiên (TTFT) tốt hơn vì có thể xử lý nhiều token prefill hơn trong một batch”. Bạn phải chọn trải nghiệm nào cần ưu tiên: người đang chờ câu trả lời bắt đầu, hay những người đang theo dõi text được stream.
Độ dài prompt quyết định mức ảnh hưởng. Một prompt 6,000 token với câu trả lời 200 token tạo ra 6,000 token công việc prefill so với 200 decode step. Chat có retrieval-augmented generation và system prompt dài đều dễ rơi vào trường hợp này. Khi đó, prefill không còn là phần chi phí nhỏ có thể bỏ qua mà trở thành nguyên nhân khiến người dùng phải chờ. Prefix caching có ích khi phần prompt dài được lặp lại: vLLM cung cấp --enable-prefix-caching để tái sử dụng cache cho prefix chung của prompt thay vì tính toán lại prefix đó cho từng request.
Bộ nhớ hết 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). Nó cho phép quá trình decode không phải tính lại toàn bộ prompt cho mỗi token mới. Dung lượng KV cache trên mỗi token được cố định theo cấu trúc của model: 2 (một key, một value) nhân với số layer, nhân với số head key/value, nhân với kích thước head, nhân với số byte trên mỗi value. Đọc các thông số đó từ config.json của model.
Tính một lần thì giới hạn không còn khó hiểu. Một model 8B điển hình có 36 layer, 8 head key/value và kích thước head là 128. Nếu cache dùng 16-bit, mỗi token cần 2 36 8 128 2 byte. Con số này là 147,456 byte, khoảng 144 KiB. Một cuộc hội thoại dài 8,192 token vì thế 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ơ sở thực tế để xác định máy chứa được bao nhiêu user.
Concurrency làm tăng context theo cấp số nhân, và các công cụ thể hiện rõ điều đó. FAQ của Ollama ghi: "Việc 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 thành context 8K và cần 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, context bạn yêu cầu bằng -c được chia cho -np slot. Vì vậy, chỉ tăng số slot sẽ làm giảm lượng context mà mỗi request có thể giữ. Hãy đọc context trên mỗi slot từ startup log thay vì tự giả định.
vLLM thì preallocate bộ nhớ. --gpu-memory-utilization (mặc định là 0.92) là "tỷ lệ bộ nhớ GPU được dùng cho model executor". Phần còn lại sau khi cấp cho weights sẽ trở thành paged KV pool. Khi pool này hết chỗ, scheduler sẽ evict request thay vì làm request đó fail:
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 V1 engine của vLLM, chế độ preemption mặc định là RECOMPUTE. Vì vậy, request bị evict sẽ mất cache và phải prefill lại khi được readmit. Phần 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 end-to-end". Dòng log này là cách giải thích rõ nhất cho trường hợp một user không may phải chờ lâu hơn nhiều so với những user khác, trong khi latency trung bình vẫn ổn. Đặt disable_log_stats=False để ghi log tổng số lần xảy ra, hoặc đọc preemption counter từ các Prometheus metrics mà vLLM expose.
Điều gì thay đổi ở 2, 5 và 20 người dùng đồng thời
Hai người dùng. Trên GPU còn dư cache, mức ảnh hưởng gần như không đáng kể vì luồng decode thứ hai chạy cùng luồng thứ nhất với rất ít thời gian phát sinh thêm. Trên VPS chỉ dùng CPU với 4 đến 8 GB RAM, mức ảnh hưởng không phải bằng 0: cả hai luồng dùng chung một số ít vCPU và cùng băng thông RAM, nên mỗi người dùng thường chỉ thấy tốc độ khoảng một nửa số token mỗi giây. Nhu cầu cache cũng tăng gấp đôi trong khi ngân sách bộ nhớ nhỏ hơn nhiều.
Năm người dùng. Đây là lúc các giá trị mặc định không còn đủ, và vấn đề ban đầu là hàng đợi. Khi OLLAMA_NUM_PARALLEL ở mức 1, bốn người phải chờ người yêu cầu câu trả lời dài, nhưng tốc độ của mỗi người trở lại bình thường ngay khi đến lượt. Tăng số lượng xử lý song song sẽ làm vấn đề thay đổi: 5 slot, mỗi slot có context 8K, cần cache 40K token. Nếu không đủ chỗ trong VRAM, engine sẽ offload các layer sang RAM hệ thống. Nếu RAM cũng không đủ, máy sẽ dùng swap và tốc độ token mỗi giây sẽ giảm mạnh.
Hai mươi người dùng. 20 người trong giao diện chat thường không tạo ra 20 request đồng thời. Đây là điều quan trọng nhất cần hiểu trước khi mua phần cứng. Một người đọc câu trả lời rồi suy nghĩ từ 20 đến 60 giây trước lượt tiếp theo, nên phần lớn session của họ ở trạng thái idle. 20 agent hoặc 20 job tóm tắt tài liệu là 20 luồng thực sự, không có thời gian idle. Đó là một loại máy khác.
Người dùng của bạn có đồng thời không, 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 nội dung cho mỗi lượt, rồi chia cho số giây giữa các lượt.
- Trước tiên, đo tốc độ của một stream trên chính hệ thống của bạn, gồm cả prefill và decode. Khô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 rồi sử dụng kết quả đó.
- Ước tính duty cycle. 20 người dùng chat, mỗi lượt sinh nội dung trong 12 giây và mỗi 90 giây có một lượt, cho kết quả 20 * 12 / 90, tức khoảng 2.7 request đang xử lý.
- Đặt số slot cao hơn mức đó một chút, 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ố cache token thực tế bạn có.
- Giữ queue ngắn để các request vượt quá giới hạn fail nhanh và dễ nhận biết.
Số cache token khả dụng bằng bộ nhớ trống sau khi trừ phần weights, chia cho chi phí trên mỗi token nêu ở phần trước. Một GPU 24 GB chạy model 8B ở 16-bit dùng khoảng 16 GB cho weights và còn khoảng 6 GB cache khả dụng với mức utilisation mặc định, tương đương khoảng 5 cuộc hội thoại 8K. Để chứa được nhiều hơn, hãy rút ngắn context cho mỗi request hoặc lưu cache ở 8-bit (llama-server cần --cache-type-k q8_0). Cả hai cách đều tăng concurrency bằng cách đánh đổi một yếu tố khác. Bạn nên đọc kỹ sự đánh đổi này trước khi quyết định chi tiền mua hardware: khi nào GPU VPS hòa vốn so với token API.
Khi giá trị mặc định của Ollama không còn đủ
Tăng số lượng request chạy song song thông qua service unit, vì lệnh export trong shell không truyền được biến đến 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 phải in ra ba biến bạn vừa đặt. Nếu không, drop-in chưa được lưu, và mọi thao tác khác đều không có tác dụng. Sau đó, ollama ps liệt kê model đã nạp với kích thước lớn hơn riêng phần weights, vì 4 slot với 8,192 token sẽ dành 32,768 token cho cache bên cạnh model. Nếu cột PROCESSOR cho thấy một phần model nằm trên CPU trong khi bạn muốn toàn bộ model chạy trên GPU, nghĩa là bạn đã yêu cầu lượng cache lớn hơn phần bộ nhớ còn trống trên card. Hãy giảm một trong hai giá trị.
Giá trị mặc định của queue cần được xem xét thêm. Ollama xếp hàng tối đa OLLAMA_MAX_QUEUE request, và “giá trị mặc định là 512”. Sau đó, nó trả về “lỗi 503 cho biết server đang quá tải”. Queue sâu 512 trên một máy chỉ xử lý 4 request cùng lúc là một giới hạn không thực tế, vì client ở vị trí 300 sẽ timeout từ lâu trước khi đến lượt. Queue ngắn trả về lỗi để ứng dụng có thể retry hoặc báo lỗi. Cách này tốt hơn việc hiển thị spinner không bao giờ kết thúc.
Hãy kiểm tra trực tiếp. Gửi 2 request cùng lúc từ 2 terminal và theo dõi cả hai. Nếu request thứ hai không tạo ra kết quả nào cho đến khi request thứ nhất hoàn tất, thiết lập chạy song song chưa có hiệu lực.
Khi một serving engine thực sự mang lại hiệu quả
vLLM đáng để thiết lập thêm khi bạn có GPU còn dư tài nguyên và thực sự có hơn khoảng four request đang được xử lý đồng thời. Scheduler của nó hoạt động theo từng token, cache được chia page để tái sử dụng các vùng trống rời rạc, và biến VRAM còn dư thành khả năng xử lý đồng thời thay vì để không. Tính đến August 2026, cách cài đặt và khởi chạy được tài liệu hóa gồm two command:
uv pip install vllm --torch-backend=auto
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."}]
}'Một phản hồi chứa mảng choices cho biết server đã chạy và model đã được load. Khi có tải, two knob 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”. Knob đầu tiên giới hạn concurrency. Knob thứ hai là ngân sách chunked prefill được mô tả ở phần trước.
Khi có ít hơn khoảng four request đang được xử lý đồng thời, hoặc trên bất kỳ máy nào không có GPU được hỗ trợ, vLLM làm tăng độ phức tạp nhưng mang lại rất ít lợi ích. Nó yêu cầu card thuộc nhóm CUDA và chiếm phần lớn memory ngay khi khởi động, nên đâ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 với context ngắn hơn và một queue do bạn kiểm soát. cách Ollama và vLLM khác nhau 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 khác.
Sự đánh đổi thường bị bỏ qua
Continuous batching tăng throughput tổng thể và thường cũng cải thiện latency trung vị, vì request đang xếp hàng được bắt đầu sớm hơn. Tail latency lại tăng theo chiều ngược lại, nhưng phần này hiếm khi được đề cập.
Mỗi sequence thêm vào một step đều tạo thêm một ít công việc, vì vậy ITL tăng cho mọi request khi batch đầy dần. Prefill của request mới đến sẽ chiếm một phần step mà các request đang streaming lẽ ra được sử dụng. Khi cache chịu áp lực, scheduler sẽ preempt, khiến request mới sinh được một phần phải quay lại từ đầu của prefill.
Giao diện chat thể hiện tail latency, không phải giá trị trung bình. Một stream tạm dừng hai giây giữa câu sẽ tạo cảm giác bị hỏng, ngay cả khi tổng thời gian hoàn tất vẫn tốt. Hãy đo p95 TTFT và p95 ITL dưới mức tải dự kiến, đồng thời xem mean tokens per second là chỉ số capacity thay vì mô tả trải nghiệm thực tế.
Thiết lập thực tế bắt nguồn từ đó. Giới hạn concurrency thấp hơn một chút so với mức memory cho phép để engine không bao giờ phải preempt. Một queue ngắn và dễ dự đoán tốt hơn batch sâu nhưng liên tục thrash, vì người dùng chờ bốn giây rồi stream ổn định sẽ hài lòng hơn người dùng bắt đầu ngay nhưng bị stall hai lần.
Kiểm tra gì khi hệ thống chậm
Mỗi request vẫn bình thường nhưng thời gian chờ dài. Đây là vấn đề về hàng đợi, không phải tốc độ xử lý. Trước tiên hãy kiểm tra cấu hình xử lý song song. Model vẫn đang phục vụ đúng, nhưng mỗi lần chỉ xử lý một request.
Ollama trả về HTTP 503. Hàng đợi đã đầy. Máy có thể thực sự đã đạt giới hạn tải, hoặc OLLAMA_MAX_QUEUE được cố ý đặt thấp để giảm tải. Đây là đúng mục đích của thiết lập đó.
Số token mỗi giây giảm mạnh khi tải tăng trên máy dùng CPU. Hãy chạy vmstat 1 trong lúc hiện tượng này xảy ra. Các cột si và so có giá trị khác 0 nghĩa là máy đang dùng swap, nên weights bị đọc từ disk cho từng 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ố slot.
Cứ 10 người dùng thì có 1 người phải chờ lâu hơn nhiều so với những người còn lại. Hãy tìm preempted trong log của vLLM. Preemption và việc recompute do nó gây ra thường là nguyên nhân. Điều đó có nghĩa là cache đã bị cấp phát quá mức so với context length bạn cho phép.
TTFT kém ngay cả khi server đang idle. Đây là vấn đề ở bước prefill, không phải do concurrency. Prompt dài thực sự tốn thời gian trước khi token đầu tiên xuất hiện. Vì vậy, hãy kiểm tra kích thước prompt và prefix caching trước khi kiểm tra phần cứng.
FAQ
Vì sao LLM tự host của tôi chậm đi khi có người thứ hai sử dụng?
Thông thường, nó không thực sự chậm đi. Các request được xếp hàng. Ollama phát hành với OLLAMA_NUM_PARALLEL ở mức 1, nên request thứ hai phải đợi request thứ nhất sinh 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 tốc độ token mỗi giây trở lại bình thường ngay khi bắt đầu, thì bạn đang gặp tình trạng xếp hàng và tăng số lượng xử lý song song sẽ khắc phục được. Nếu cả hai stream đều chạy ở nửa tốc độ, thì 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 theo 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 và 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 dài 8,192 token cần khoảng 1.2 GB. Một card 24 GB chứa model đó ở 16-bit còn khoảng 6 GB cho cache, tương đương khoảng năm cuộc hội thoại với context đầy đủ, 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 giảm, vì request không còn phải chờ cả batch hoàn tất. Độ trễ đuôi tăng lên. Mỗi sequence bổ sung thêm công việc vào mọi bước decode, prefill của request mới đến chiếm một phần của một bước từ các user đang stream, và request bị preempt phải thực hiện prefill hai lần. Hãy đo độ trễ giữa các token ở p95, không chỉ đo giá trị trung bình, vì cửa sổ chat làm các khoảng dừng trở nên rõ ràng theo cách mà giá trị trung bình che khuất.
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 xử lý song song. Cách này miễn phí, chỉ cần một file drop-in và khắc phục được trường hợp phổ biến khi bốn 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 song song làm tăng số context bạn phải giữ, vì vậy hãy theo dõi xem layer có bị spill sang CPU không. Hãy chuyển sang vLLM khi bạn có GPU còn dư VRAM và thực sự có hơn khoảng bốn request đang chạy đồng thời, vì đó là lúc paged cache và việc lập lịch theo từng token mang lại lợi ích lớn hơn chi phí của chúng.
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 cảm nhận rõ 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 nhiều core hơn sẽ rút ngắn thời gian đến token đầu tiên đối với prompt dài. Trên VPS 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 context ngắn hơn, thay vì tăng vCPU.