Cách đo tokens per second cho LLM chạy local
GPU thuê chỉ rẻ hơn tính phí theo token khi đạt ngưỡng throughput. Đo tokens per second bằng concurrency sweep rồi tính điểm hòa vốn trước khi quyết định.
Vì sao tokens per second quyết định GPU có đáng thuê hay không
Tokens per second là tốc độ server tạo ra văn bản đầu ra. Đây là con số quyết định việc thuê GPU có rẻ hơn trả phí API theo token hay không. GPU box được tính phí theo giờ, dù đang bận hay idle. Hosted API được tính phí theo token. Vì vậy, GPU chỉ có lợi nếu bạn duy trì output rate đủ cao trong phần lớn số giờ đã trả phí.
Điều đó có nghĩa là bạn cần số liệu đo thực tế, không phải một con số đọc được ở đâu đó. Phần này định nghĩa 4 số liệu đáng ghi lại, sau đó đưa ra các command tạo ra chúng và phép tính để chuyển các số liệu đó thành quyết định.
Vì sao con số tokens mỗi giây được công bố không phải là con số của bạn
DigitalOcean công bố các số liệu throughput vào tháng 7 năm 2026 cho một NVIDIA H200 chạy llama3.3-70b-instruct ở FP8 (floating point 8-bit) dưới vLLM. Các số liệu này hữu ích, nhưng không phải số liệu của bạn.
The data behind this chart
[
{
"config": "H200, one stream",
"tok_s": "47"
},
{
"config": "H100, saturated",
"tok_s": "236"
},
{
"config": "H200, saturated",
"tok_s": "2,036"
},
{
"config": "H200, saturated, in+out",
"tok_s": "4,071.6"
}
]Mọi hàng ở trên đều được trích từ trang đó. Hai hàng trong số đó là cận dưới của một khoảng mà trang đưa ra, nên hãy xem chúng là mức tối thiểu. Chúng tôi không tự đo bất kỳ con số nào trong biểu đồ này.
Hãy bắt đầu với 2 hàng cuối. Con số chính là 4,071.6 tok/s, còn tốc độ chỉ tính output là 2,036 tok/s. Con số chính tính cả input token và output token. Bài test dùng 1,024 input token và 1,024 output token, nên output gần như chiếm đúng một nửa con số chính. Việc tách 2 phần này quan trọng vì bạn bị tính phí theo output, và đây cũng là phần chậm hơn. Prefill (đọc prompt) xử lý toàn bộ input token trong một lượt. Decode (ghi câu trả lời) tạo từng token một. Con số throughput tổng hợp lấy trung bình giữa một phần có chi phí thấp và một phần có chi phí cao.
Bây giờ đến hàng đầu tiên. Cùng một H200 khi chỉ phục vụ từng request một đạt 47 tok/s, nên con số khi GPU bão hòa cao hơn 40 lần trên cùng phần cứng. Khoảng cách này xuất hiện vì trong phần lớn thời gian, mỗi bước decode khiến GPU phải chờ memory, còn các request chạy đồng thời sẽ tận dụng thời gian nhàn đó. Hàng thứ 2, 236 tok/s, là một H100 chạy cùng model và bị giới hạn bởi KV cache (key và value cache, tức vùng nhớ cho mỗi request mà một cuộc hội thoại đang được phục vụ giữ trên card). Một card 80 GB chứa được ít request đồng thời hơn khi chạy model 70B, nên đạt mức bão hòa thấp hơn.
Nếu đổi model hoặc thay đổi tỷ lệ input trên output, mọi con số ở trên đều thay đổi. Các số liệu được công bố giúp bạn đặt kỳ vọng, không giúp bạn lập ngân sách. Đây cũng là nguyên tắc áp dụng khi benchmark VPS một cách trung thực về disk và network.
Bốn chỉ số cần quan tâm
- Thời gian đến token đầu tiên, TTFT. Là độ trễ từ lúc gửi request đến lúc nhận được token đầu tiên. Chỉ số này gồm thời gian prefill và thời gian chờ trong queue. Người dùng cảm nhận trực tiếp độ trễ này.
- Số token output mỗi giây trên mỗi stream. Là tốc độ tạo một câu trả lời sau khi bắt đầu. Khi đạt khoảng 20 tok/s, tốc độ đã cao hơn tốc độ đọc của hầu hết mọi người, nên tăng thêm thường không đem lại nhiều lợi ích.
- Tổng throughput output khi bão hòa. Là tổng throughput của tất cả stream đồng thời khi server được tải đầy đủ. Đây là chỉ số về capacity và là chỉ số quyết định chi phí GPU.
- TTFT p50 và p99 khi có nhiều request đồng thời. p50 là giá trị của request ở giữa. p99 là giá trị mà 99 trên 100 request có độ trễ thấp hơn. Tình trạng chờ trong queue luôn xuất hiện ở p99 trước.
Hai chỉ số đầu tốt hơn khi server ít tải. Chỉ số thứ ba tốt hơn khi server đang bận. Chúng luôn đánh đổi lẫn nhau, nên không có một chỉ số duy nhất nào mô tả đầy đủ một server serving.
Cố định độ dài input và output trước khi đo
Throughput phụ thuộc vào hình dạng của traffic. Prompt 4,000 token với câu trả lời 50 token là tác vụ nặng về prefill. Prompt 200 token với câu trả lời 2,000 token là tác vụ nặng về decode. Cùng một server sẽ báo số token mỗi giây rất khác nhau cho hai trường hợp này. Vì vậy, hãy chọn một tỷ lệ, ghi tỷ lệ đó bên cạnh mọi số liệu bạn ghi lại và không bao giờ so sánh các số liệu có tỷ lệ khác nhau. Tỷ lệ 1,024 input và 1,024 output là mặc định hợp lý vì một số vendor công bố số liệu ở tỷ lệ này. Nếu bạn biết traffic thực tế của mình, hãy dùng chính traffic đó.
Hãy cố định cả độ dài output. Model dừng ở stop token sau 60 token sẽ tạo ra một lần chạy ngắn hơn và có vẻ nhanh hơn, vì TTFT chiếm tỷ trọng lớn hơn trong tổng thời gian. Flag --ignore-eos trong benchmark client của vLLM khiến mọi request tạo ra đúng số lượng token được yêu cầu, nhờ đó hai lần chạy vẫn có thể so sánh với nhau. Việc chọn model ảnh hưởng đến các số liệu này nhiều hơn bất kỳ flag nào: đưa model Qwen 3 lên một GPU VPS duy nhất đề cập đến khía cạnh bộ nhớ của lựa chọn đó.
Đo một stream trước
Bắt đầu với trường hợp đơn giản nhất. Đây là phép kiểm tra nhanh và cũng là mức trần. Ollama tự in thông tin timing.
ollama run llama3.1:8b --verbose "Write 200 words about disk scheduling."Dòng cần đọc là eval rate, tức số output token mỗi giây. prompt eval rate là tốc độ prefill, còn load duration là thời gian nạp model vào VRAM. Trong lần gọi đầu tiên sau khi cold start, load duration lớn nên total duration gây hiểu nhầm. Chạy lệnh 2 lần và đọc kết quả thứ 2. Theo mặc định, Ollama unload model khi không hoạt động trong 5 phút, vì vậy nếu chờ lâu giữa 2 lần chạy, bạn sẽ quay lại trường hợp cold start.
Các trường này cũng có trong API, thuận tiện hơn khi viết script.
curl -s http://127.0.0.1:11434/api/generate -d '{
"model": "llama3.1:8b",
"prompt": "Write 200 words about disk scheduling.",
"stream": false
}' | jq '{eval_count, eval_duration,
tok_s: (.eval_count / (.eval_duration / 1000000000))}'eval_duration tính bằng nanosecond, nên chia cho 1,000,000,000 để được số giây. Đây chính xác là phép chia mà tài liệu API của Ollama yêu cầu khi tính số token mỗi giây. Nếu server chưa chạy, tự host LLM bằng Ollama trên VPS có hướng dẫn cài đặt và tạo systemd unit.
TTFT cần một request streaming, và curl có thể đo thời gian này giúp bạn.
curl -s -o /dev/null -N \
-w 'ttfb=%{time_starttransfer}s total=%{time_total}s\n' \
http://127.0.0.1:8000/v1/chat/completions \
-H 'Content-Type: application/json' \
-d '{"model":"Qwen/Qwen3-8B",
"messages":[{"role":"user","content":"Write 200 words about disk scheduling."}],
"stream":true,"max_tokens":256}'time_starttransfer là thời điểm byte đầu tiên của response body đến nơi. Với một streaming chat completion, byte đó thuộc về server-sent event đầu tiên. Event này có thể chứa content token đầu tiên hoặc một delta chỉ có role được gửi ngay trước đó. Vì vậy, hãy xem giá trị này là TTFT với sai số khoảng 1 event. Giá trị đủ chính xác để so sánh 2 lần chạy trên cùng một server.
Các số đo khi chỉ có một stream làm cho máy trông tốt hơn ở 2 điểm. TTFT đạt mức tốt nhất có thể vì không có request nào xếp hàng trước bạn. Tốc độ trên mỗi stream cũng đạt mức tốt nhất vì toàn bộ card chỉ phục vụ một request. Cả 2 chỉ số đều không cho biết máy có thể xử lý tải thực tế đến đâu.
Chạy kiểm tra concurrency sweep như thế nào?
Một sweep chạy cùng một workload cố định với mức concurrency tăng dần và ghi lại kết quả ở từng bước. vLLM có sẵn client cho việc này. Client này dùng OpenAI API, nên cũng hoạt động với Ollama và mọi hệ thống tương thích với OpenAI.
vllm bench serve \
--backend openai-chat \
--base-url http://127.0.0.1:8000 \
--endpoint /v1/chat/completions \
--model Qwen/Qwen3-8B \
--dataset-name random \
--random-input-len 1024 \
--random-output-len 1024 \
--ignore-eos \
--num-prompts 160 \
--max-concurrency 16 \
--percentile-metrics ttft,tpot,itl,e2el \
--metric-percentiles 50,99--max-concurrency giới hạn số request đang được xử lý đồng thời. Đây là biến bạn sẽ sweep. --num-prompts là tổng số request gửi đi, nên đặt giá trị này gần gấp 10 lần concurrency để có giá trị trung bình ổn định. Bản tóm tắt in Output token throughput (tok/s): và Total token throughput (tok/s):, sau đó là Mean TTFT (ms):, Median TTFT (ms): và P99 TTFT (ms): dưới heading Time to First Token.
Kết quả không có tốc độ tính theo từng stream, nhưng có thể suy ra bằng một phép chia. Mean TPOT (ms): là thời gian trung bình cho mỗi output token tính từ token thứ hai trở đi. Vì vậy, 25 ms mỗi token tương đương 40 token mỗi giây trên mỗi stream. Chia throughput output cho concurrency cũng cho cùng kết quả.
Sau đó lặp lại quy trình và lưu từng lần chạy.
for C in 1 4 16 32 64 128; do
vllm bench serve \
--backend openai-chat \
--base-url http://127.0.0.1:8000 \
--endpoint /v1/chat/completions \
--model Qwen/Qwen3-8B \
--dataset-name random \
--random-input-len 1024 \
--random-output-len 1024 \
--ignore-eos \
--num-prompts $(( C * 10 )) \
--max-concurrency "$C" \
--percentile-metrics ttft,tpot,itl,e2el \
--metric-percentiles 50,99 \
--save-result --result-filename "sweep-c$C.json"
done:::detailsĐọc các file JSON đã lưu
Mỗi lần chạy ghi một file. Vì vậy, hãy lấy các field cần quan tâm từ tất cả file cùng lúc.
for f in sweep-c*.json; do
jq -r --arg f "$f" \
'[$f, .output_throughput, .total_token_throughput,
.median_ttft_ms, .p99_ttft_ms] | @tsv' "$f"
doneoutput_throughput là số output token mỗi giây. total_token_throughput cộng thêm input token, nên với tỷ lệ 1:1, giá trị này gần gấp đôi. p99_ttft_ms tồn tại chỉ vì --metric-percentiles đã bao gồm 99. Nếu yêu cầu một percentile không có trong dữ liệu, jq sẽ in null.
:::
Thực tế một concurrency sweep cho thấy gì?
The data behind this chart
[
{
"label": "1 stream",
"per_stream_tok_s": 92,
"total_tok_s": 92,
"ttft_p50_ms": 48,
"ttft_p99_ms": 61
},
{
"label": "4 streams",
"per_stream_tok_s": 88,
"total_tok_s": 352,
"ttft_p50_ms": 71,
"ttft_p99_ms": 96
},
{
"label": "16 streams",
"per_stream_tok_s": 71,
"total_tok_s": 1136,
"ttft_p50_ms": 152,
"ttft_p99_ms": 244
},
{
"label": "32 streams",
"per_stream_tok_s": 54,
"total_tok_s": 1728,
"ttft_p50_ms": 287,
"ttft_p99_ms": 498
},
{
"label": "64 streams",
"per_stream_tok_s": 34,
"total_tok_s": 2176,
"ttft_p50_ms": 611,
"ttft_p99_ms": 1240
},
{
"label": "128 streams",
"per_stream_tok_s": 18,
"total_tok_s": 2304,
"ttft_p50_ms": 1490,
"ttft_p99_ms": 3820
}
]6 dòng đó minh họa hình dạng mà một sweep tạo ra trên một GPU box nhỏ có thể thuê, với các bậc độ lớn hợp lý. Chúng không phải số đo trên server của bạn và cũng không phải số liệu do vendor cung cấp. Hãy chạy loop ở trên rồi thay chúng bằng số liệu của bạn.
Hãy đọc hình dạng của đường cong, vì chính hình dạng đó mới có tính khái quát. Với một stream, toàn bộ box tạo ra 92 token mỗi giây, với p99 TTFT là 61 ms. Ở 128 streams, tổng throughput đạt 2304 token mỗi giây, cao hơn 25 lần, trong khi tốc độ của từng stream giảm còn 18 token mỗi giây và p99 TTFT tăng lên 3820 ms. Tổng throughput tăng vì batching biến thời gian chờ memory khi GPU nhàn rỗi thành công việc hữu ích. Tốc độ của từng stream giảm vì cùng một năng lực compute nay phải được chia sẻ.
Lần tăng gấp đôi cuối cùng là dấu hiệu rõ nhất. Tăng từ 64 lên 128 stream chỉ bổ sung chưa đến 6 phần trăm vào tổng throughput, trong khi p99 TTFT tăng khoảng 3 lần. Điều đó có nghĩa KV cache đã đầy và request đang phải xếp hàng thay vì được xử lý. Điểm vận hành hữu ích nằm sớm hơn: ở 32 stream, box vẫn trả về 1728 token mỗi giây, bằng 75 phần trăm mức đỉnh, với tốc độ 54 token mỗi giây trên mỗi stream và p99 TTFT là 498 ms. Hãy báo cáo điểm đó làm capacity của bạn. Mức đỉnh của đường cong không phải là con số bạn có thể dùng để phục vụ người dùng.
Ollama và vLLM không đo cùng một thứ
Chạy phép quét đó trên một server Ollama mặc định thì tổng số sẽ hầu như không thay đổi. OLLAMA_NUM_PARALLEL mặc định là 1, nên một request được xử lý còn các request khác phải chờ. Hàng đợi khiến p99 TTFT tăng, trong khi tổng output vẫn giữ nguyên. Hãy tăng giá trị này trước khi đo bất kỳ thứ gì.
sudo systemctl edit ollama.service[Service]
Environment="OLLAMA_NUM_PARALLEL=8"Khởi động lại với sudo systemctl restart ollama, sau đó xác nhận model vẫn vừa với bộ nhớ. Mỗi slot song song được cấp một phần riêng trong context window, nên tài liệu Ollama lưu ý rằng context 2K với 4 request song song sẽ được cấp 8K. Tăng số slot quá cao sẽ khiến model tràn khỏi VRAM. Kiểm tra ollama ps: nếu cột PROCESSOR hiển thị giá trị như 48%/52% CPU/GPU, một phần model đang chạy trên CPU, và throughput sẽ giảm khi tăng concurrency thay vì tăng. Sau khi vượt quá số slot song song, các request sẽ được đưa vào hàng đợi tối đa là OLLAMA_MAX_QUEUE, mặc định 512. Sau đó server sẽ trả về 503.
vLLM dùng continuous batching, nên nó đưa request mới vào batch đang chạy khi có slot được giải phóng. Đường cong hiệu năng tiếp tục tăng cho đến khi KV cache hết chỗ. Ollama được tối ưu cho một model, một máy và chi phí thiết lập thấp. Vì vậy, hai engine sẽ cho các kết quả khác nhau với cùng một phép quét. Đây mới là chủ đề chính của So sánh Ollama và vLLM với vai trò engine serving. Hãy ghi lại engine và version đã tạo ra từng số liệu.
Năm cách đo sai thứ cần đo
- Client ở quá xa. Benchmark từ laptop qua Internet sẽ cộng round trip của bạn vào mọi TTFT, nên kết quả đo thực chất phản ánh kết nối tại nhà. Chạy client trong cùng region với server.
- Model đang ở trạng thái cold. Request đầu tiên phải tải weight và trên vLLM còn có thể phải thực hiện graph capture. Gửi một batch warmup rồi bỏ kết quả đó.
- Prefix caching đã trả lời thay bạn. vLLM mặc định bật automatic prefix caching, nên việc gửi đi gửi lại cùng một prompt sẽ đo cache thay vì prefill, còn TTFT giảm xuống chỉ còn một phần nhỏ so với giá trị thực.
--dataset-name randomtránh được vấn đề này vì mỗi prompt đều khác nhau. Để chắc chắn, khởi động server với--no-enable-prefix-caching. - Output quá ngắn. Với câu trả lời 32 token, TTFT chiếm phần lớn thời gian của mỗi request, nên tokens per second thực chất đang mô tả prefill. Dùng
--ignore-eosvới độ dài output thực tế. - Bạn báo cáo concurrency 1. Đây là con số dễ nhìn nhất trên bảng, nhưng không phản ánh chi phí.
Biến số đo được thành quyết định
Dùng throughput đầu ra bão hòa từ lần sweep, không dùng tốc độ của một stream duy nhất, rồi so sánh với mức giá theo token. Điểm hòa vốn được tính bằng một phép chia:
break_even_tok_s = (price_per_hour / price_per_million_output_tokens) * 1000000 / 3600Hãy tính với mức giá của DigitalOcean vào tháng 07/2026. Endpoint inference H200 dedicated của họ có giá $4.47 mỗi giờ, còn tùy chọn serverless tương đương có giá $0.65 cho mỗi triệu token. Vì vậy, 4.47 chia cho 0.65 bằng 6.88 triệu token mỗi giờ; chia tiếp cho 3,600 giây thì được khoảng 1,910 token đầu ra mỗi giây. Mức giá là của họ. Phép chia là của chúng ta.
Từ quyết định ở đây là sustained. Đạt 1,910 token mỗi giây khi bão hòa trong 2 giờ mỗi ngày không có nghĩa là đạt 1,910 token mỗi giây một cách sustained, vì bạn vẫn trả tiền cho 22 giờ còn lại. Mức crossover mà DigitalOcean tự tính cho GPU Droplet rẻ hơn, có giá $3.44 mỗi giờ, là 72.2 phần trăm mức sử dụng trung bình sustained. Thấp hơn mức đó, giá theo token có lợi hơn. GPU idle, không phải token xử lý chậm, thường là nguyên nhân khiến việc tự host không còn hiệu quả.
Vì vậy, quyết định của bạn có 2 đầu vào. Lần sweep cho biết giới hạn throughput. Mẫu traffic cho biết bạn thực sự sử dụng được bao nhiêu phần của giới hạn đó. Nhân 2 giá trị này, rồi đưa kết quả vào phép tính hòa vốn giữa GPU VPS và API tính phí theo token để xác định phương án phù hợp với lưu lượng của bạn.
FAQ
Tốc độ token mỗi giây bao nhiêu là tốt đối với LLM tự host?
Có 2 cách trả lời, vì chỉ số này phục vụ 2 mục đích. Nếu chỉ có 1 người đọc output, tốc độ trên khoảng 20 output token mỗi giây trên mỗi stream đã nhanh hơn tốc độ đọc, nên tăng thêm cũng không giúp ích. Nếu xét chi phí, con số cần quan tâm là tổng throughput output khi hệ thống đạt tải tối đa. Mức tốt là mức đủ vượt điểm hòa vốn của bạn. Với chi phí $0.65 trên mỗi triệu token và một máy có giá $4.47 mỗi giờ, ngưỡng này vào khoảng 1,910 output token mỗi giây khi duy trì liên tục, theo mức giá tháng 7 năm 2026. Một stream trên model lớn không bao giờ đạt được mức này. Đó là lý do batching tồn tại.
Vì sao throughput của Ollama không tăng khi tôi thêm các request đồng thời?
OLLAMA_NUM_PARALLEL mặc định là 1, nên server xử lý mỗi model 1 request tại một thời điểm và đưa các request còn lại vào queue, tối đa đến OLLAMA_MAX_QUEUE (mặc định là 512), trước khi trả về 503. Tổng output vẫn giữ nguyên trong khi p99 TTFT tăng. Đây là dấu hiệu của queue, không phải GPU đang bận xử lý. Đặt biến này trong systemd drop-in rồi restart. Sau đó kiểm tra ollama ps, vì mỗi parallel slot làm tăng context được cấp phát và có thể khiến một phần model chuyển sang CPU.
Tôi nên đo time to first token hay token mỗi giây?
Cả 2, vì chúng thay đổi theo hướng ngược nhau khi tải tăng. TTFT là độ trễ mà người dùng cảm nhận, còn throughput output khi hệ thống đạt tải tối đa là yếu tố ảnh hưởng đến hóa đơn. Ghi lại p50 và p99 TTFT ở mỗi mức concurrency. Sau đó chọn mức concurrency cao nhất mà p99 TTFT vẫn nằm trong ngưỡng bạn chấp nhận. Báo cáo throughput tại mức đó làm capacity của hệ thống, không dùng mức tối đa ở đỉnh của biểu đồ.
Tốc độ token mỗi giây cao hơn có luôn đồng nghĩa với chi phí trên mỗi token thấp hơn không?
Không. Chi phí trên mỗi token bằng giá theo giờ chia cho số token mà máy thực sự tạo ra trong giờ đó. Vì vậy, một server nhanh nhưng phần lớn thời gian idle vẫn có chi phí trên mỗi token cao. Utilisation mới là yếu tố quyết định, không phải tốc độ tối đa. Cũng cần kiểm tra đơn vị: tổng token throughput được báo cáo có thể tính cả input token, nên với tỷ lệ input trên output là 1:1, con số đó gần bằng 2 lần output rate mà bạn phải trả phí.