Prefill a decode w LLM: dlaczego pierwszy token trwa
Faza prefill jest ograniczona mocą obliczeniową i wpływa na czas pierwszego tokena. Faza decode zależy od przepustowości pamięci. Zmierz te parametry osobno dla swojego GPU.
Prefill a decode – różnice w jednym akapicie
Rozróżnienie między fazami prefill i decode wyjaśnia większość problemów z opóźnieniami w przypadku samodzielnie hostowanych modeli LLM (large language model). Faza prefill odczytuje cały prompt w jednym przebiegu i jest ograniczona mocą obliczeniową. Faza decode generuje odpowiedź token po tokenie i jest ograniczona przepustowością pamięci. Czas do uzyskania pierwszego tokena (time to first token) zależy od fazy prefill. Liczba tokenów na sekundę (tokens per second) zależy od fazy decode.
Obie fazy działają na tym samym GPU (graphics processing unit), wykorzystują te same wagi i ten sam proces, dlatego naturalne jest traktowanie ich jako jednego obciążenia. W rzeczywistości zachowują się one jak dwa różne programy współdzielące jedno urządzenie. Rozdzielenie tych pojęć sprawia, że lista niejasnych wyników staje się zrozumiała.
Dlaczego faza prefill jest ograniczona mocą obliczeniową?
Faza prefill przetwarza cały prompt przez każdą warstwę modelu w jednym przebiegu. Prompt o długości 2000 tokenów oznacza, że każda operacja mnożenia macierzy musi wykonać pracę dla 2000 wierszy, co sprawia, że GPU wykonuje dużą liczbę operacji arytmetycznych w przeliczeniu na każdy bajt wczytanych wag. Ten stosunek, czyli liczba operacji arytmetycznych na bajt przesłanych danych, nazywany jest intensywnością obliczeniową, a faza prefill charakteryzuje się jej wysoką wartością. Urządzenie pracuje blisko swojego limitu obliczeniowego, podczas gdy magistrala pamięci dysponuje zapasem przepustowości.
Faza prefill generuje dwa elementy: cache KV (tensory kluczy i wartości) dla każdego tokena promptu oraz pierwszy token wyjściowy. Żadne dane nie trafiają do odbiorcy, dopóki ten przebieg się nie zakończy, dlatego czas trwania fazy prefill oraz czas do uzyskania pierwszego tokena (TTFT) są niemal tożsame.
Koszt fazy prefill rośnie wraz z długością promptu. Część liniowa wynika z operacji na macierzach w każdej warstwie. Część kwadratowa wynika z mechanizmu attention, w którym każdy token odnosi się do wszystkich poprzednich, co staje się istotne przy długim kontekście. Z tego powodu podwojenie długości promptu powoduje co najmniej podwojenie wartości TTFT.
Można to zaobserwować w ciągu minuty. Wyślij na serwer prompt o długości 200 tokenów, a następnie prompt o długości 2000 tokenów, żądając w obu przypadkach takiej samej liczby tokenów wyjściowych. Wartość TTFT wzrośnie gwałtownie. Szybkość strumieniowania po wygenerowaniu pierwszego tokena pozostanie niemal bez zmian.
Dlaczego proces dekodowania jest ograniczony przepustowością pamięci?
Dekodowanie generuje jeden token w każdym kroku. Aby wygenerować ten pojedynczy token, GPU musi odczytać z pamięci wszystkie wagi modelu, użyć każdej wagi do kilku operacji, a następnie ją odrzucić. Intensywność obliczeniowa jest bliska 1, więc jednostki obliczeniowe większość czasu spędzają w stanie oczekiwania.
Dekodowanie jest powolne, ponieważ każdy token wymaga odczytania całego modelu z pamięci, więc to magistrala pamięci wyznacza tempo, a jednostki obliczeniowe pozostają bezczynne.
Oznacza to, że górny limit prędkości dekodowania pojedynczego strumienia można obliczyć na papierze. Należy podzielić przepustowość pamięci przez liczbę bajtów zajmowanych przez wagi.
The data behind this chart
[
{
"device": "CPU, dual channel DDR5-5600",
"mem_bandwidth_gb_s": 90,
"decode_ceiling_tok_s": 6
},
{
"device": "NVIDIA A10G",
"mem_bandwidth_gb_s": 600,
"decode_ceiling_tok_s": 38
},
{
"device": "NVIDIA L40S",
"mem_bandwidth_gb_s": 864,
"decode_ceiling_tok_s": 54
},
{
"device": "NVIDIA RTX 4090",
"mem_bandwidth_gb_s": 1008,
"decode_ceiling_tok_s": 63
},
{
"device": "NVIDIA A100 80GB SXM",
"mem_bandwidth_gb_s": 2039,
"decode_ceiling_tok_s": 127
},
{
"device": "NVIDIA H100 SXM",
"mem_bandwidth_gb_s": 3350,
"decode_ceiling_tok_s": 209
}
]Kolumna przepustowości zawiera oficjalne dane techniczne opublikowane przez producenta. Kolumna limitu to ta wartość podzielona przez 16 GB, czyli rozmiar modelu o 8 miliardach parametrów zapisanego z precyzją 16 bitów. Jest to wynik obliczeń, a nie testu wydajności. Zmierzona prędkość będzie niższa, a określenie skali tej różnicy jest użyteczne, ponieważ pozwala ustalić, czy należy zoptymalizować stos serwujący, czy wymienić sprzęt.
Po przeanalizowaniu 6 wierszy wzorzec staje się jasny. Procesor korzystający z dwukanałowej pamięci DDR5 osiąga przepustowość około 90 GB/s, co ogranicza dekodowanie dla tego modelu do około 6 tokenów na sekundę. Karta L40S osiąga wynik w okolicach 54. Układ H100 SXM, z opublikowaną przepustowością na poziomie 3350 GB/s, osiąga około 209.
Dlatego też kwantyzacja jest najskuteczniejszym pojedynczym narzędziem wpływającym na prędkość dekodowania. Przechowywanie tego samego modelu w 8 bitach zamiast 16 zmniejsza o połowę liczbę bajtów odczytywanych na token, co sprawia, że teoretyczny limit wzrasta mniej więcej dwukrotnie. Nie dodano żadnych mocy obliczeniowych. Zmniejszono jedynie ilość przesyłanych danych w pamięci.
Jak zmierzyć poszczególne fazy na własnym serwerze?
Ollama zwraca podział w treści odpowiedzi. Należy wysłać zapytanie o kompletne wygenerowanie tekstu bez strumieniowania i odczytać liczniki.
curl -s http://localhost:11434/api/generate -d '{
"model": "llama3.2",
"prompt": "Explain memory bandwidth in two sentences.",
"stream": false
}' | jq '{prompt_eval_count, prompt_eval_duration, eval_count, eval_duration}'Należy użyć tagu modelu, który został faktycznie pobrany, co wyświetli polecenie ollama list. prompt_eval_count oraz prompt_eval_duration to wartości prefill: liczba tokenów w prompcie oraz czas poświęcony na ich przetworzenie. eval_count oraz eval_duration dotyczą fazy decode. Czas trwania podawany jest w nanosekundach, zatem szybkość decode to eval_count / eval_duration * 1e9, a szybkość prefill to prompt_eval_count / prompt_eval_duration * 1e9. Należy oczekiwać, że szybkość prefill będzie znacznie wyższa niż szybkość decode dla tego samego zapytania. Ta różnica jest przyczyną wszystkich zjawisk opisanych w tym dokumencie.
W przypadku serwera zgodnego z OpenAI, takiego jak vLLM, polecenie curl może zmierzyć czas do otrzymania pierwszego bajtu.
curl -N -s -o /dev/null \
-w 'pretransfer %{time_pretransfer}s first_byte %{time_starttransfer}s\n' \
http://localhost:8000/v1/completions \
-H 'Content-Type: application/json' \
-d '{"model": "meta-llama/Llama-3.1-8B-Instruct", "prompt": "Explain memory bandwidth.", "max_tokens": 128, "stream": true}'time_starttransfer to moment otrzymania pierwszego bajtu treści, więc wraz z "stream": true stanowi to TTFT powiększony o czas nawiązania połączenia. Należy odjąć time_pretransfer, aby wyeliminować koszt zestawienia połączenia. Test warto wykonać dwukrotnie i zachować drugi wynik, ponieważ pierwsze wywołanie może uwzględniać czas ładowania modelu do pamięci.
vLLM udostępnia również podział w postaci metryk Prometheus pod adresem /metrics. Po uruchomieniu curl -s http://localhost:8000/metrics | grep -E 'time_to_first_token|inter_token_latency' otrzymuje się histogramy vllm:time_to_first_token_seconds oraz vllm:inter_token_latency_seconds. Należy dodać vllm:num_requests_running oraz vllm:num_requests_waiting dla głębokości kolejki, a także vllm:kv_cache_usage_perc dla obciążenia pamięci podręcznej. Te pięć nazw stanowi kompletny zestaw danych dla panelu monitorowania.
Pod obciążeniem narzędzie vllm bench serve --model <name> --num-prompts 200 --request-rate 4 steruje działającym serwerem i raportuje czas do pierwszego tokena oraz opóźnienie na token wyjściowy z uwzględnieniem percentyli, co jest jedynym sposobem na zaobserwowanie wzajemnego wpływu obu faz. Przed przystąpieniem do jakiejkolwiek optymalizacji należy ustalić rzetelną linię bazową: metoda opisana w mierzenie liczby tokenów na sekundę w lokalnym LLM pozwala uzyskać wynik, który pozostaje aktualny po restarcie systemu.
Dlaczego długi prompt systemowy opóźnia wygenerowanie pierwszego tokena, ale nie wpływa na szybkość strumieniowania?
Ponieważ prompt systemowy stanowi wyłącznie etap przetwarzania wstępnego (prefill). Jest on przetwarzany jednorazowo, w tym samym przebiegu co reszta promptu, zanim pojawi się pierwszy token. Po zakończeniu tego etapu prompt istnieje już tylko w postaci wpisów w pamięci podręcznej KV (KV cache), a proces dekodowania odczytuje je wraz z pozostałymi danymi. Zatem prompt systemowy o długości 3000 tokenów zwiększa czas TTFT w każdym żądaniu, pozostawiając liczbę tokenów na sekundę niemal bez zmian.
Niemal, ponieważ nie jest to zmiana zerowa. Dodatkowe wpisy KV są odczytywane ponownie na każdym kroku dekodowania, więc bardzo długi prompt nieznacznie spowalnia ten proces. Kolejna sekcja omawia to zagadnienie.
Rozwiązaniem jest zaprzestanie ponownego obliczania tego samego prefiksu. Serwer z funkcją prefix caching przechowuje pamięć podręczną KV współdzielonego prefiksu i używa jej ponownie, dzięki czemu drugie żądanie zawierające ten sam prompt systemowy całkowicie pomija tę część etapu prefill. vLLM nazywa to automatycznym buforowaniem prefiksów (automatic prefix caching); sprawdź vllm serve --help w swojej wersji, ponieważ domyślne ustawienia zmieniały się w kolejnych wydaniach. Pamięć podręczna KV wewnątrz GPU to coś innego niż prompt cache, za który nalicza opłaty dostawca API, dlatego warto zapoznać się z treścią różnica między pamięcią podręczną KV a pamięcią podręczną promptu przed przystąpieniem do optymalizacji któregokolwiek z tych mechanizmów.
Dlaczego dekodowanie zwalnia w miarę zapełniania kontekstu?
Istnieją dwa powody, oba związane z pamięcią podręczną KV (KV cache).
Pierwszym jest przepustowość. Na każdym etapie dekodowania mechanizm uwagi (attention) odczytuje klucze i wartości każdego poprzedniego tokena. Wagi stanowią stały koszt na token. Pamięć podręczna KV rośnie. Jej rozmiar można obliczyć na podstawie config.json modelu: liczba bajtów na token równa się 2 pomnożone przez num_hidden_layers, przez num_key_value_heads, przez wymiar głowy (hidden_size podzielone przez num_attention_heads) oraz przez liczbę bajtów na element. Mnożnik 2 uwzględnia jeden klucz i jedną wartość.
Dla typowego modelu o 8 miliardach parametrów, 32 warstwach, 8 głowach kluczy i wartości w ramach GQA (grouped query attention), wymiarze głowy 128 i precyzji 16 bitów, daje to 2 x 32 x 8 x 128 x 2 = 131 072 bajty, czyli około 128 KiB na token. Konwersacja o długości 8000 tokenów zajmuje zatem około 1 GB pamięci podręcznej KV na każde żądanie.
Drugim powodem jest pojemność. Ten 1 GB to pamięć, która nie może przechowywać wag ani kontekstu innego użytkownika. Serwer ustala rozmiar puli KV jednorazowo podczas uruchamiania, w vLLM za pomocą --gpu-memory-utilization, a gdy pula jest pełna, nowe żądania oczekują. Wzrost vllm:num_requests_waiting przy jednoczesnym utrzymywaniu się vllm:kv_cache_usage_perc blisko wartości 1 jest dokładnym sygnałem tego stanu. Niektóre stosy oprogramowania przerywają działanie żądania i przeliczają jego pamięć podręczną później zamiast ustawiać je w kolejce, co użytkownik odczuwa jako zawieszenie w trakcie generowania strumienia.
Długi kontekst generuje podwójny koszt: więcej pracy podczas fazy prefill na początku oraz większy odczyt pamięci na każdy token podczas generowania reszty odpowiedzi.
Dlaczego przetwarzanie wsadowe zwiększa przepustowość, ale pogarsza opóźnienia w ogonie rozkładu?
Ponieważ proces dekodowania jest ograniczony przepustowością pamięci, obsługa dodatkowych żądań jest niemal bezkosztowa pod względem obliczeniowym. Jednokrotny odczyt wag pozwala wygenerować token dla każdej sekwencji wewnątrz wsadu, dzięki czemu całkowita przepustowość rośnie niemal liniowo wraz z rozmiarem wsadu, dopóki nie wyczerpie się pula KV lub wsad nie stanie się ponownie ograniczony mocą obliczeniową. Mechanizm Continuous batching przebudowuje wsad na każdym kroku, więc zakończone żądanie jest usuwane, a oczekujące dołączane bez konieczności czekania na pozostałe elementy.
Koszty tego rozwiązania uwidaczniają się w percentylach. Kolejny token każdego użytkownika musi teraz czekać na najwolniejszą część wspólnego kroku, przez co p50 (mediana) pozostaje na akceptowalnym poziomie, podczas gdy p99 (najwolniejsze 1 na 100 żądań) ulega wydłużeniu. To właśnie p99 jest najbardziej odczuwalne dla użytkowników, ponieważ objawia się jako przerwa w środku generowanego zdania.
Faza prefill dodatkowo zaostrza ten problem. Duży prompt, który pojawia się w trakcie strumieniowania, zajmuje urządzenie na jeden długi krok, co powoduje zauważalne opóźnienie dla wszystkich pozostałych użytkowników. Technika chunked prefill eliminuje większość tych przestojów poprzez dzielenie długich promptów na mniejsze fragmenty i mieszanie ich z partiami dekodowania. Od sierpnia 2026 silnik vLLM V1 stosuje to podejście domyślnie i udostępnia regulację balansu za pomocą --max-num-batched-tokens. Dokumentacja strojenia vLLM jasno określa ten kompromis: mniejsze wartości, w okolicach 2048, zapewniają lepsze opóźnienie między tokenami (ITL), ponieważ mniej operacji prefill przerywa dekodowanie, natomiast większe wartości poprawiają TTFT, gdyż więcej tokenów prefill mieści się w jednym wsadzie. Ta pojedyncza flaga stanowi przełącznik między prefill a dekodowaniem, wyrażony jako wartość liczbowa. Moment, w którym p99 przestaje być akceptowalny, jest kwestią wydajności systemu, a artykuł ilu jednoczesnych użytkowników może obsłużyć samodzielnie hostowany LLM szczegółowo omawia to zagadnienie przy użyciu tych samych metryk.
Dlaczego mocniejszy GPU czasami nie przynosi zmian?
Ponieważ większa moc zazwyczaj oznacza większą wydajność obliczeniową, a proces dekodowania nie jest od niej zależny.
Porównaj dwa wiersze z powyższej tabeli. Karta A100 80GB posiada przepustowość wynoszącą 2039 GB/s, podczas gdy L40S oferuje 864 GB/s. Limit dekodowania podąża za tymi wartościami: odpowiednio 127 tokenów na sekundę wobec 54. RTX 4090 jest pod wieloma względami bardzo szybką kartą, a jej przepustowość 1008 GB/s wyznacza limit na poziomie 63. Niezależnie od innych różnic między kartami, szybkość dekodowania pojedynczego strumienia jest ściśle powiązana z przepustowością pamięci podaną w specyfikacji.
Istnieją zatem dwa sposoby na przyspieszenie dekodowania: odczytywanie mniejszej liczby bajtów na token (poprzez kwantyzację wag lub uruchomienie mniejszego modelu) albo zwiększenie przepustowości. W przypadku fazy prefill sytuacja jest odwrotna. Wymaga ona mocy obliczeniowej, więc szybsza karta faktycznie skraca czas TTFT przy długich promptach. Jeśli problemem jest długi czas oczekiwania na pierwszy token, lepszy sprzęt może go rozwiązać. Jeśli jednak tekst generowany jest zbyt wolno, wymiana sprzętu prawdopodobnie nie przyniesie poprawy.
Czy należy uruchamiać prefill i decode na oddzielnych procesach roboczych?
Duże stosy serwujące wykonują dokładnie to zadanie, a technika ta nazywana jest dezagregacją prefill i decode. Jedna pula procesów roboczych obsługuje wyłącznie prefill, a druga wyłącznie decode, przy czym pamięć podręczna KV utworzona przez pierwszą pulę jest przesyłana do drugiej za pośrednictwem szybkiego połączenia. Rozwiązanie to sprawdza się, ponieważ obie fazy wymagają odmiennego sprzętu i innego planowania zadań. Prefill wymaga dużej mocy obliczeniowej i przetwarzania dużych partii tokenów. Decode wymaga wysokiej przepustowości i obsługi wielu jednoczesnych sekwencji. Rozdzielenie tych faz pozwala na niezależne skalowanie każdej puli i zapobiega sytuacji, w której jeden ogromny prompt blokuje wszystkie aktywne strumienie.
Na pojedynczym VPS (virtual private server) z jedną jednostką GPU takie rozwiązanie prawie nigdy nie jest opłacalne. Prowadziłoby to do rywalizacji jednego urządzenia z samym sobą i zamiany wskaźnika na transfer sieciowy gigabajtów pamięci podręcznej. Technika ta przynosi korzyści dopiero wtedy, gdy dostępna jest wystarczająca liczba akceleratorów, aby dedykować całe maszyny do każdej z faz, oraz gdy występuje stały ruch pozwalający na obciążenie obu pul. Poniżej tego progu, chunked prefill zapewnia większość tych samych korzyści w zakresie izolacji przy użyciu jednej flagi.
Co zmienić, gdy wartości są nieprawidłowe
Gdy TTFT jest zbyt wysokie:
- Skróć prompt. Koszt prefill obejmuje tokeny promptu, a prompt systemowy jest opłacany przy każdym żądaniu.
- Włącz prefix caching, aby powtarzający się prefiks był obliczany raz, a nie za każdym razem.
- Zwiększ
--max-num-batched-tokens, aby w każdym kroku wykonywano więcej pracy związanej z prefill. - Sprawdź kolejkę, zanim obarczysz winą model. Wartość
vllm:num_requests_waitingpowyżej zera oznacza, że żądanie jeszcze się nie rozpoczęło, co wskazuje na problem z wydajnością (pojemnością).
Gdy liczba tokenów na sekundę jest zbyt niska:
- Zastosuj kwantyzację wag. Mniejsza liczba bajtów na wagę oznacza mniej bajtów odczytywanych na token.
- Porównaj opublikowaną przepustowość pamięci karty z powyższym wykresem i sprawdź, jak blisko limitu się znajdujesz.
- Zmniejsz
--max-num-batched-tokens, aby operacje prefill rzadziej przerywały dekodowanie. - Sprawdź długość kontekstu. Konwersacja rozrośnięta do tysięcy tokenów powoduje odczyt znacznie większej pamięci podręcznej KV w każdym kroku.
Środowisko uruchomieniowe również ma tutaj znaczenie, ponieważ Ollama i vLLM planują prefill oraz dekodowanie w różny sposób, a ustawienie, które pomaga w jednym przypadku, może nie mieć wpływu na drugi. Najpierw dokonaj pomiarów w obu fazach, a następnie zmieniaj tylko jeden parametr.
FAQ
Dlaczego wygenerowanie pierwszego tokena trwa sekundy, a reszta przesyłana jest szybko?
Czas oczekiwania wynika z fazy prefill, a przesyłanie z fazy decode. Prefill przetwarza cały prompt w jednym cyklu obliczeniowym przed wygenerowaniem jakiegokolwiek wyniku, dlatego koszt tej operacji rośnie wraz z długością promptu. Faza decode emituje następnie jeden token na krok z prędkością ograniczoną przepustowością pamięci, co jest niemal niezależne od długości promptu. Częstą przyczyną jest długi system prompt dołączany do każdego żądania. Mechanizm prefix caching eliminuje powtarzalną część tego kosztu.
Czy dłuższy prompt spowalnia liczbę tokenów na sekundę?
Nieznacznie, z powodów innych niż w przypadku TTFT. Każdy krok dekodowania odczytuje klucze i wartości wszystkich poprzednich tokenów, więc większa pamięć podręczna KV oznacza więcej bajtów odczytywanych na token. Dla typowego modelu o 8 miliardach parametrów pamięć podręczna zajmuje około 128 KiB na token, więc kontekst 8,000 tokenów to około 1 GB danych przetwarzanych w każdym kroku. Główny wpływ długiego promptu nadal dotyczy TTFT, a nie prędkości strumieniowania.
Która specyfikacja GPU najlepiej prognozuje prędkość dekodowania?
Przepustowość pamięci. Podziel opublikowaną przepustowość przez rozmiar wag w pamięci, aby uzyskać teoretyczny limit dla jednego strumienia. Karta o wyższej mocy obliczeniowej, ale tej samej przepustowości, nie będzie generować tokenów szybciej. Dlatego kwantyzacja do 8 bitów mniej więcej podwaja prędkość dekodowania: zmniejsza o połowę liczbę bajtów odczytywanych na token bez wpływu na operacje obliczeniowe.
Dlaczego przepustowość rośnie przy dodawaniu użytkowników, podczas gdy każdy z nich odczuwa spowolnienie?
Jeden odczyt wag obsługuje token dla każdej sekwencji w partii, więc całkowita liczba tokenów na sekundę rośnie wraz z rozmiarem partii. Każdy pojedynczy token musi teraz czekać na wspólny krok, więc opóźnienie dla użytkownika jednocześnie rośnie. Monitoruj opóźnienie p99 między tokenami, a nie zagregowaną liczbę przepustowości, i sprawdź vllm:num_requests_waiting, aby zweryfikować, czy żądania są kolejkowane, zamiast być przetwarzane na bieżąco.