Jak zmierzyć liczbę tokenów na sekundę w lokalnym LLM
Dowiedz się, jak poprawnie zmierzyć przepustowość LLM przy użyciu testów współbieżności. Oblicz, czy wynajęcie GPU jest tańsze niż rozliczanie API na podstawie liczby tokenów.
Dlaczego liczba tokenów na sekundę decyduje o opłacalności GPU
Liczba tokenów na sekundę to szybkość, z jaką serwer generuje tekst wyjściowy. Jest to wskaźnik decydujący o tym, czy wynajęcie GPU jest tańsze niż opłacanie API za każdy token. Za serwer GPU płaci się godzinowo, niezależnie od tego, czy jest on obciążony, czy bezczynny. Usługi API rozliczane są za token. Zatem GPU staje się opłacalne tylko wtedy, gdy utrzymuje się wystarczająco wysokie tempo generowania przez większość opłaconych godzin.
Wymaga to przeprowadzenia własnych pomiarów, a nie opierania się na danych teoretycznych. Niniejsza strona definiuje cztery wartości, które warto rejestrować, a następnie przedstawia polecenia służące do ich uzyskania oraz obliczenia pozwalające podjąć decyzję.
Dlaczego opublikowana liczba tokenów na sekundę nie jest Twoim wynikiem
W lipcu 2026 roku firma DigitalOcean opublikowała dane dotyczące przepustowości dla pojedynczego układu NVIDIA H200 uruchomionego z llama3.3-70b-instruct w formacie FP8 (8-bitowa liczba zmiennoprzecinkowa) w środowisku vLLM. Liczby te są użyteczne, ale nie dotyczą Twojego środowiska.
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"
}
]Każdy wiersz powyżej pochodzi z przywołanej strony, a dwa z nich stanowią dolną granicę podanego zakresu, więc należy je traktować jako wartość minimalną. Żaden z wyników w tej tabeli nie był mierzony przez nas.
Zacznijmy od dwóch ostatnich wierszy. Wartość nagłówkowa wynosi 4,071.6 tok/s, podczas gdy szybkość generowania wyłącznie wyjścia to 2,036 tok/s. Wartość nagłówkowa sumuje tokeny wejściowe i wyjściowe. W tym teście użyto 1024 tokenów wejściowych oraz 1024 tokenów wyjściowych, więc niemal dokładnie połowa wartości nagłówkowej przypada na wyjście. Podział ten ma znaczenie, ponieważ to za wyjście ponosisz opłaty i jest to wolniejsza część procesu. Prefill (odczyt promptu) przetwarza wszystkie tokeny wejściowe w jednym przebiegu. Decode (generowanie odpowiedzi) tworzy jeden token na raz. Całkowita przepustowość uśrednia tanią operację z kosztowną.
Teraz pierwszy wiersz. Ten sam układ H200 obsługujący jedno żądanie naraz generuje 47 tok/s, więc wartość przy pełnym obciążeniu jest ponad czterdziestokrotnie wyższa na identycznym sprzęcie. Ta różnica wynika z faktu, że pojedynczy krok dekodowania sprawia, iż GPU przez większość czasu oczekuje na dane z pamięci, a równoległe żądania wypełniają ten czas bezczynności. Drugi wiersz, 236 tok/s, dotyczy pojedynczego układu H100 na tym samym modelu, ograniczanego przez KV cache (pamięć podręczną kluczy i wartości, czyli pamięć per-request utrzymywaną przez konwersację na karcie). Karta 80 GB mieści mniej równoległych żądań dla modelu 70B, więc osiąga nasycenie przy niższej wartości.
Zmień model lub proporcję wejścia do wyjścia, a każda z powyższych liczb ulegnie zmianie. Opublikowane dane kształtują Twoje oczekiwania, a nie budżet. Jest to ta sama zasada, która obowiązuje przy rzetelnym benchmarkowaniu VPS pod kątem dysku i sieci.
Cztery kluczowe parametry
- Time to first token, TTFT. Opóźnienie między wysłaniem żądania a otrzymaniem pierwszego tokena wyjściowego. Jest to suma czasu przetwarzania wstępnego (prefill) oraz czasu oczekiwania w kolejce. Użytkownik odczuwa ten parametr bezpośrednio.
- Output tokens per second, per stream. Szybkość generowania odpowiedzi po rozpoczęciu procesu. Powyżej około 20 tok/s prędkość ta przewyższa tempo czytania większości ludzi, więc dalsze zwiększanie wydajności w tym obszarze przynosi niewielkie korzyści.
- Saturated total output throughput. Sumaryczna przepustowość wyjściowa dla wszystkich jednoczesnych strumieni przy pełnym obciążeniu serwera. Jest to wskaźnik wydajności, który determinuje opłacalność wykorzystania GPU.
- p50 and p99 TTFT under concurrency. p50 to wartość dla środkowego żądania. p99 to wartość, poniżej której mieści się 99 na 100 żądań. Kolejkowanie zawsze najpierw uwidacznia się w statystyce p99.
Pierwsze dwa parametry poprawiają się, gdy serwer jest mało obciążony. Trzeci parametr poprawia się, gdy serwer pracuje pod dużym obciążeniem. Wartości te są ze sobą sprzeczne, dlatego pojedyncza liczba nie jest w stanie w pełni opisać wydajności serwera.
Dostosowanie długości danych wejściowych i wyjściowych przed pomiarem
Przepustowość zależy od charakterystyki ruchu. Prompt o długości 4,000 tokenów z odpowiedzią 50 tokenów to obciążenie typu prefill-heavy. Prompt o długości 200 tokenów z odpowiedzią 2,000 tokenów to obciążenie typu decode-heavy. Ten sam serwer wykazuje bardzo różne wartości tokenów na sekundę dla tych dwóch przypadków, dlatego należy wybrać jeden stosunek, zapisać go obok każdej rejestrowanej liczby i nigdy nie porównywać wyników między różnymi stosunkami. Wartość 1,024 tokenów wejściowych na 1,024 wyjściowych jest rozsądnym ustawieniem domyślnym, ponieważ wielu dostawców publikuje wyniki przy takim właśnie stosunku. Jeśli znana jest charakterystyka rzeczywistego ruchu, należy użyć własnych danych.
Należy również wymusić długość wyjściową. Model, który napotyka token stopu po 60 tokenach, generuje krótszy przebieg, który wydaje się szybszy, ponieważ TTFT stanowi wtedy większy udział w całkowitym czasie. Flaga --ignore-eos w kliencie benchmarkowym vLLM sprawia, że każde żądanie generuje dokładnie określoną liczbę tokenów, dzięki czemu dwa przebiegi pozostają porównywalne. Wybór modelu wpływa na te liczby bardziej niż jakakolwiek flaga: dopasowanie modelu Qwen 3 do pojedynczego GPU na VPS omawia kwestie pamięciowe związane z tym wyborem.
Pomiar pojedynczego strumienia
Zacznij od najprostszego przypadku. Jest to test poprawności działania oraz wyznaczenie górnego limitu wydajności. Ollama wyświetla własne pomiary czasu.
ollama run llama3.1:8b --verbose "Write 200 words about disk scheduling."Wartość do odczytania to eval rate, czyli liczba tokenów wyjściowych na sekundę. prompt eval rate to szybkość prefillu, a load duration to czas ładowania modelu do VRAM. Przy pierwszym wywołaniu po zimnym starcie load duration jest duża, więc total duration jest myląca. Uruchom polecenie dwukrotnie i odczytaj drugi wynik. Ollama domyślnie zwalnia nieaktywny model po pięciu minutach, więc długa przerwa między uruchomieniami przywraca stan zimnego startu.
Te same pola są dostępne przez API, co ułatwia automatyzację skryptami.
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 jest wyrażone w nanosekundach, więc dzielenie przez 1 000 000 000 daje wynik w sekundach. To dzielenie jest dokładnie tym, co zaleca dokumentacja API Ollama dla obliczania tokenów na sekundę. Jeśli serwer nie jest jeszcze uruchomiony, self-hosting an LLM with Ollama on a VPS zawiera instrukcje instalacji oraz konfiguracji jednostki systemd.
TTFT wymaga żądania strumieniowego, a curl może zmierzyć ten czas.
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 to moment, w którym dociera pierwszy bajt treści odpowiedzi. W strumieniowym czacie ten bajt należy do pierwszego zdarzenia typu server-sent event, którym jest albo pierwszy token treści, albo delta zawierająca tylko rolę, wysłana tuż przed nim. Należy więc traktować tę wartość jako TTFT z marginesem błędu jednego zdarzenia. Jest to wystarczająco dokładne, aby porównać dwa uruchomienia na tym samym serwerze.
Wyniki dla pojedynczego strumienia są optymistyczne z dwóch powodów. Wartość TTFT jest najlepszą możliwą, ponieważ w kolejce nie ma żadnych innych zadań. Szybkość na strumień jest również maksymalna, ponieważ cała karta graficzna obsługuje tylko jedno żądanie. Żadna z tych wartości nie określa rzeczywistej przepustowości serwera.
Jak przeprowadzić test obciążenia przy zmiennej współbieżności?
Test obciążenia polega na uruchomieniu stałego zestawu zadań przy rosnącej liczbie współbieżnych połączeń i rejestrowaniu wyników na każdym etapie. vLLM zawiera klienta do tego celu, który obsługuje API OpenAI, dzięki czemu działa również z Ollama oraz każdym innym rozwiązaniem zgodnym z 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,99Parametr --max-concurrency ogranicza liczbę żądań w locie i jest zmienną podlegającą testowaniu. --num-prompts to całkowita liczba wysłanych żądań; należy utrzymać ją na poziomie około dziesięciokrotności współbieżności, aby uzyskać stabilną średnią. Podsumowanie wyświetla Output token throughput (tok/s): oraz Total token throughput (tok/s):, a następnie Mean TTFT (ms):, Median TTFT (ms): i P99 TTFT (ms): w sekcji Time to First Token.
W danych wyjściowych brakuje wartości szybkości dla pojedynczego strumienia, ale można ją łatwo obliczyć przez dzielenie. Mean TPOT (ms): to średni czas przypadający na jeden token wyjściowy po pierwszym tokenie, więc 25 ms na token oznacza 40 tokenów na sekundę dla strumienia. Podzielenie całkowitej przepustowości wyjściowej przez współbieżność daje ten sam wynik.
Następnie należy wykonać pętlę, zapisując wyniki każdego uruchomienia.
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"
doneOdczytywanie zapisanych plików JSON
Każde uruchomienie tworzy jeden plik, dlatego należy wyodrębnić interesujące pola ze wszystkich plików jednocześnie.
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 oznacza liczbę tokenów wyjściowych na sekundę. total_token_throughput uwzględnia również tokeny wejściowe, więc przy stosunku 1:1 wartość ta jest bliska podwojonej liczbie tokenów wyjściowych. p99_ttft_ms istnieje tylko dlatego, że --metric-percentiles zawierało 99; w przypadku żądania percentyla, który nie został uwzględniony, narzędzie jq zwraca null.
Co faktycznie pokazuje test współbieżności?
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
}
]Wiersze 6 stanowią ilustrację kształtu, jaki przyjmuje test na małym serwerze GPU przeznaczonym na wynajem, przy zachowaniu wiarygodnych rzędów wielkości. Nie są to pomiary Twojego serwera ani dane od dostawcy. Uruchom powyższą pętlę i zastąp je własnymi wynikami.
Analizuj kształt wykresu, ponieważ to on ma charakter uniwersalny. Przy jednym strumieniu cały serwer generuje 92 tokenów na sekundę przy p99 TTFT wynoszącym 61 ms. Przy 128 streams całkowita przepustowość osiąga 2304 tokenów na sekundę, czyli dwudziestopięciokrotnie więcej, podczas gdy każdy pojedynczy strumień spada do 18 tokenów na sekundę, a p99 TTFT osiąga 3820 ms. Całkowita przepustowość rośnie, ponieważ przetwarzanie wsadowe zamienia przestoje pamięci w użyteczną pracę. Szybkość na strumień spada, ponieważ te same zasoby obliczeniowe są teraz współdzielone.
Ostatnie podwojenie jest kluczowe. Przejście z 64 na 128 strumieni zwiększa całkowitą wydajność o mniej niż sześć procent, podczas gdy p99 TTFT wzrasta mniej więcej trzykrotnie. Oznacza to, że pamięć podręczna KV jest pełna, a żądania trafiają do kolejki zamiast być przetwarzane. Użyteczny punkt operacyjny znajduje się wcześniej: przy 32 strumieniach serwer nadal zwraca 1728 tokenów na sekundę, co stanowi 75 procent wartości szczytowej, przy 54 tokenach na sekundę na strumień i p99 TTFT na poziomie 498 ms. Przyjmij ten punkt jako miarę swojej wydajności. Szczyt krzywej to wartość, przy której nie da się obsłużyć użytkowników.
Ollama i vLLM nie mierzą tych samych parametrów
Uruchomienie testu wydajnościowego na domyślnym serwerze Ollama spowoduje, że całkowity wynik niemal się nie zmieni. Wartość OLLAMA_NUM_PARALLEL jest domyślnie ustawiona na 1, co oznacza, że przetwarzane jest jedno żądanie, podczas gdy pozostałe oczekują w kolejce. To właśnie kolejka powoduje wzrost p99 TTFT, podczas gdy całkowita przepustowość pozostaje stała. Zwiększ tę wartość przed przystąpieniem do jakichkolwiek pomiarów.
sudo systemctl edit ollama.service[Service]
Environment="OLLAMA_NUM_PARALLEL=8"Zrestartuj usługę z parametrem sudo systemctl restart ollama, a następnie upewnij się, że model nadal mieści się w pamięci. Każdy równoległy slot zajmuje część okna kontekstowego, dlatego dokumentacja Ollama wskazuje, że kontekst 2K przy 4 równoległych żądaniach wymaga alokacji 8K. Zbyt wysoka liczba slotów spowoduje, że model nie zmieści się w VRAM. Sprawdź ollama ps: kolumna PROCESSOR wskazująca wartość typu 48%/52% CPU/GPU oznacza, że część modelu znajduje się w pamięci CPU, co spowoduje spadek przepustowości wraz ze wzrostem współbieżności zamiast jej wzrostu. Po wyczerpaniu dostępnych slotów równoległych żądania trafiają do kolejki OLLAMA_MAX_QUEUE, której domyślny rozmiar wynosi 512; po jej przekroczeniu serwer zwraca błąd 503.
vLLM wykorzystuje mechanizm continuous batching, dzięki czemu włącza nowe żądania do aktualnie przetwarzanej partii w miarę zwalniania slotów, a krzywa wydajności rośnie aż do wyczerpania pamięci KV cache. Ollama optymalizuje działanie pod kątem jednego modelu, jednej maszyny i niskich kosztów wdrożenia. Z tego powodu oba silniki generują różne wyniki dla tego samego testu, co jest głównym tematem porównania Ollama i vLLM jako silników serwujących. Rejestruj, który silnik oraz która wersja wygenerowały poszczególne wyniki.
Pięć sposobów na błędny pomiar
- Klient znajduje się zbyt daleko. Testowanie wydajności z laptopa przez Internet dodaje czas opóźnienia (RTT) do każdego TTFT, przez co mierzona jest jakość łącza domowego. Uruchom klienta w tym samym regionie, w którym znajduje się serwer.
- Model był w stanie "zimnym". Pierwsze żądanie uwzględnia ładowanie wag, a w przypadku vLLM także przechwytywanie grafu. Wyślij partię rozgrzewkową i odrzuć jej wynik.
- Cache prefiksu udzielił odpowiedzi za Ciebie. vLLM domyślnie włącza automatyczne buforowanie prefiksów, więc wysyłanie tego samego promptu wielokrotnie mierzy wydajność cache, a nie prefill, co sprawia, że TTFT spada do ułamka rzeczywistej wartości.
--dataset-name randomzapobiega temu zjawisku, ponieważ każdy prompt jest inny. Aby mieć pewność, uruchom serwer z flagą--no-enable-prefix-caching. - Dane wyjściowe były zbyt krótkie. Przy odpowiedziach o długości 32 tokenów, TTFT dominuje w każdym żądaniu, a wskaźnik tokenów na sekundę opisuje w rzeczywistości fazę prefill. Użyj
--ignore-eosz realistyczną długością odpowiedzi. - Raportowano współbieżność na poziomie 1. Jest to najbardziej optymistyczna wartość w zestawieniu, która nie ma żadnego odniesienia do kosztów.
Przekształcenie zmierzonej wartości w decyzję
Należy przyjąć nasyconą przepustowość wyjściową z testu, a nie szybkość pojedynczego strumienia, i porównać ją z cennikiem za token. Punkt rentowności wyznacza proste dzielenie:
break_even_tok_s = (price_per_hour / price_per_million_output_tokens) * 1000000 / 3600Przeanalizujmy to na przykładzie cen DigitalOcean z lipca 2026 roku. Dedykowany punkt końcowy wnioskowania H200 kosztował 4.47 USD za godzinę, a odpowiednik bezserwerowy 0.65 USD za milion tokenów. Dzieląc 4.47 przez 0.65 otrzymujemy 6.88 miliona tokenów na godzinę, a po podzieleniu przez 3,600 sekund uzyskujemy około 1,910 tokenów wyjściowych na sekundę. Ceny są ich, dzielenie nasze.
Słowem kluczowym jest tutaj utrzymanie. Osiągnięcie 1,910 tokenów na sekundę przy nasyceniu przez dwie godziny dziennie nie oznacza 1,910 tokenów na sekundę w sposób ciągły, ponieważ płaci się również za pozostałe dwadzieścia dwie godziny. W przypadku tańszego GPU Droplet od DigitalOcean za 3.44 USD za godzinę, punkt przecięcia opłacalności przypada na 72.2 procent średniego stałego wykorzystania; poniżej tej wartości korzystniejszy jest model płatności za token. To bezczynne godziny pracy GPU, a nie wolne tokeny, zazwyczaj decydują o nieopłacalności hostingu własnego.
Decyzja opiera się zatem na dwóch danych wejściowych. Test wyznacza górny limit. Wzorzec ruchu określa ułamek tego limitu, który faktycznie zostanie wykorzystany. Należy je pomnożyć, a następnie przejść do porównania GPU VPS z progiem rentowności API za token i odczytać wynik dla danego wolumenu.
FAQ
Jaka wartość tokenów na sekundę jest odpowiednia dla samodzielnie hostowanego modelu LLM?
Istnieją dwie odpowiedzi, ponieważ metryka ta pełni dwie funkcje. Dla jednej osoby czytającej wynik, każda wartość powyżej około 20 tokenów wyjściowych na sekundę na strumień jest szybsza niż tempo czytania, więc wyższa wartość nie przynosi korzyści. W kontekście kosztów, istotną liczbą jest nasycona całkowita przepustowość wyjściowa, a „dobra” wartość to taka, która pozwala osiągnąć próg rentowności. Przy koszcie 0,65 USD za milion tokenów na serwerze kosztującym 4,47 USD za godzinę, próg ten wynosi około 1910 tokenów wyjściowych na sekundę przy cenach z lipca 2026 roku. Jeden strumień w dużym modelu nigdy go nie osiąga, dlatego stosuje się przetwarzanie wsadowe (batching).
Dlaczego przepustowość Ollama pozostaje stała po dodaniu równoległych żądań?
OLLAMA_NUM_PARALLEL domyślnie wynosi 1, więc serwer przetwarza jedno żądanie na raz dla danego modelu i kolejkuje pozostałe, do limitu OLLAMA_MAX_QUEUE (domyślnie 512), zanim zwróci błąd 503. Całkowita przepustowość wyjściowa pozostaje stała, podczas gdy p99 TTFT rośnie, co jest oznaką kolejkowania, a nie pełnego obciążenia GPU. Ustaw zmienną w pliku typu drop-in dla systemd i zrestartuj usługę, a następnie sprawdź ollama ps, ponieważ każdy równoległy slot zwiększa przydzielony kontekst i może wymusić przeniesienie części modelu do CPU.
Czy powinienem mierzyć czas do pierwszego tokena (TTFT), czy liczbę tokenów na sekundę?
Oba parametry, ponieważ zmieniają się w przeciwnych kierunkach wraz ze wzrostem obciążenia. TTFT to odczucie użytkownika, a nasycona przepustowość wyjściowa to wartość odzwierciedlona na fakturze. Rejestruj p50 oraz p99 TTFT na każdym poziomie współbieżności, a następnie wybierz najwyższą współbieżność, przy której p99 TTFT jest dla Ciebie akceptowalne. Podawaj przepustowość w tym punkcie jako swoją wydajność, a nie wartość maksymalną z wierzchołka krzywej.
Czy wyższa liczba tokenów na sekundę zawsze oznacza niższy koszt za token?
Nie. Koszt za token to cena godzinowa podzielona przez liczbę tokenów, które serwer faktycznie wygenerował w ciągu tej godziny, więc szybki serwer, który przez większość dnia pozostaje bezczynny, nadal ma wysoki koszt za token. O wszystkim decyduje utylizacja, a nie prędkość szczytowa. Zwracaj również uwagę na jednostki: podawana całkowita przepustowość tokenów zlicza tokeny wejściowe, więc przy stosunku wejścia do wyjścia 1:1 jest to wartość bliska dwukrotności tempa wyjściowego, za które jesteś rozliczany.