Firma wdraża lokalny model 13B, uruchamia go dla dziesięciu wewnętrznych użytkowników. Przez pierwszy tydzień działa bez problemów. Potem dołącza drugi zespół, pojawia się kolejny przypadek użycia, liczba równoległych requestów skacze do 20–30, a czas odpowiedzi rośnie z trzech sekund do czterdziestu. Ktoś proponuje kupno mocniejszego GPU. Kupują. Czas odpowiedzi spada do dwudziestu pięciu sekund. Problem nie leży w sprzęcie — problem tkwi w tym, co dzieje się z KV-cache i batchingiem pod maską.
Throughput i latencja inferencji LLM to nie tylko kwestia sprzętu. To kwestia architektury. To, jak framework serwujący zarządza pamięcią, jak grupuje requesty w batche, jak współdzieli wyniki pośrednie — decyduje o tym, czy z jednego GPU uzyskasz wydajność odpowiadającą jego cenie, czy płacisz za moc, która nigdy nie jest wykorzystywana. Ten artykuł wyjaśni mechanizmy i pokaże, gdzie leżą dźwignie do poprawy bez konieczności dokupowania sprzętu.
Gdzie ginie wydajność: anatomia inferencji LLM
Żeby zrozumieć, gdzie można optymalizować, trzeba najpierw zrozumieć, co się dzieje podczas generowania tekstu. Inferencja LLM przebiega w dwóch fazach.
Faza prefill przetwarza cały wejściowy prompt naraz — to operacja podatna na zrównoleglenie, która obciąża GPU jak mnożenie macierzy. Wynikiem są wektory kluczy i wartości (KV) dla każdego tokenu wejścia, które zostają zapisane w pamięci. Czas potrzebny na tę fazę nazywa się TTFT — Time to First Token. To właśnie TTFT decyduje, kiedy użytkownik zobaczy pierwszy znak odpowiedzi.
Faza decode generuje tokeny jeden po drugim, przy czym każdy nowy token zależy od wszystkich poprzednich. To operacja sekwencyjna — nie da się jej zrównoleglić w ramach jednego requestu. Prędkość generowania mierzy się w tokenach na sekundę (tokens/sec).
Kluczowy wniosek: prefill jest compute-bound (obciąża rdzenie GPU), decode jest memory-bound (obciąża przepustowość pamięci). Te dwie fazy mają różne bottlenecki i wymagają różnych optymalizacji. Większość naiwnych implementacji poświęca throughput w imię prostoty.
Statyczny batching: dlaczego nie działa przy mieszanych obciążeniach
Klasyczne podejście do datkowania jest proste: czekamy, aż zapełni się paczka requestów, wysyłamy je wszystkie naraz, model przetwarza je równolegle, wyniki wracają. Potem czeka się na kolejną paczkę.
Problem pojawia się, gdy requesty mają różną długość — a to codzienność. Jeden request chce jednoliniowej odpowiedzi, inny generuje 500 tokenów. Statyczny batching czeka, aż najdłuższy request w paczce się zakończy. Pozostałe rdzenie GPU, które mogłyby już obsługiwać nowy request, stoją bezczynnie. Efektywne wykorzystanie GPU spada do 20–40% przy typowym mieszanym obciążeniu.
W praktyce oznacza to, że GPU, za które płacisz, przez większość czasu czeka. Nie na dane, nie na sieć — czeka na jeden wolny request.
Continuous batching: dynamiczne uzupełnianie paczki
Continuous batching rozwiązuje ten problem inaczej. Zamiast czekać na koniec paczki, każdy wygenerowany token staje się okazją: serwer zagląda do kolejki oczekujących requestów i natychmiast dodaje nowe do bieżącej paczki. Request, który właśnie się zakończył, zwalnia miejsce dla nowego — bez przerwy.
Efekt jest dramatyczny: na tym samym sprzęcie continuous batching osiąga typowo 2–3× wyższy throughput w porównaniu ze statycznym batchingiem przy mieszanych obciążeniach. Nie dlatego, że GPU jest szybsze, lecz dlatego, że niewykorzystane cykle są wypełniane nowymi requestami.
vLLM, SGLang i TGI implementują continuous batching natywnie. Ollama nie implementuje go w pełni — jest zoptymalizowany pod single-user desktop, nie pod produkcyjne obciążenia wieloużytkownikowe. Dla zespołu z dziesiątką równoległych użytkowników różnica jest odczuwalna: przy ośmiu równoległych requestach vLLM jest wyraźnie wydajniejszy niż Ollama na tym samym sprzęcie. Szczegółowe porównanie frameworków serwujących znajdziesz w artykule vLLM vs SGLang vs Ollama.
KV-cache: gdzie „ginie" VRAM przy długich kontekstach
Każdy token przetwarzany przez model — zarówno w fazie prefill, jak i decode — produkuje wektor klucza i wartości dla każdej warstwy attention. Te wektory muszą być przechowywane, żeby przy generowaniu kolejnego tokenu nie trzeba było ich przeliczać od nowa. To właśnie magazyn nosi nazwę KV-cache.
Rozmiar KV-cache rośnie liniowo z długością kontekstu. Dla orientacji: model 70B przy kontekstowym oknie 128K potrzebuje na sam KV-cache rzędu kilkudziesięciu gigabajtów — ponad i powyżej VRAM zajmowanej przez sam model. Dla czterech równoległych requestów przy tej samej długości kontekstu rozmiar rośnie czterokrotnie.
Nowoczesne modele najczęściej implementują Grouped Query Attention (GQA), które drastycznie zmniejsza rozmiar KV-cache w porównaniu z klasycznym multi-head attention dzięki temu, że grupy głów „query" współdzielą wspólne wektory „key" i „value". Większość aktualnych rodzin modeli (Llama, Qwen, Mistral) ma GQA — to dziś standard, nie wyjątek.
Kolejna dźwignia to kwantyzacja KV-cache: obniżenie precyzji wektorów KV do INT8 lub FP8 może zmniejszyć ich rozmiar o połowę przy minimalnej utracie jakości wyjść. Produkcyjne frameworki serwujące obsługują to jako opcjonalną optymalizację.
Praktyczny wniosek: jeśli brakuje Ci VRAM i rozważasz długi kontekst, KV-cache to pierwsze miejsce, gdzie należy szukać przyczyny. Zanim dokupisz GPU, warto zmierzyć, ile VRAM KV-cache faktycznie zajmuje przy Twoim typowym obciążeniu.
PagedAttention: wirtualne stronicowanie dla KV-cache
Naiwne zarządzanie KV-cache tworzy kolejny problem: rezerwuje pamięć dla maksymalnej możliwej długości kontekstu z góry, nawet gdy większość requestów jest znacznie krótsza. Wyobraź sobie, że alokujesz 128K slotów, bo model to technicznie obsługuje, ale przeciętny request ma 2000 tokenów. Większość zaalokowanej pamięci leży pusta — a z powodu fragmentacji nie da się jej efektywnie odzyskać.
PagedAttention, kluczowa innowacja vLLM, rozwiązuje ten problem inspiracją z systemów operacyjnych. Tak jak OS stronicuje RAM na bloki fizyczne i mapuje je wirtualnie, PagedAttention dzieli KV-cache na bloki stałego rozmiaru (strony), które są przydzielane dynamicznie w zależności od tego, ile tokenów request faktycznie wygenerował. Pamięć jest alokowana on-demand, nie z wyprzedzeniem.
Efekt: fragmentacja KV-cache spada z typowych 60–80% marnotrawstwa do poniżej 4%. Oznacza to, że ta sama VRAM obsługuje wielokrotnie więcej równoległych requestów albo tę samą liczbę requestów z dłuższym kontekstem.
W produkcyjnym wdrożeniu to różnica, która bezpośrednio wpływa na to, ile GPU potrzebujesz — a więc i na koszty serwowania.
Latencja a throughput: to nie jest to samo
Jeden z najczęstszych pojęciowych nieporozumień: optymalizacja throughputu i optymalizacja latencji to częściowo sprzeczne cele.
Throughput mierzy, ile tokenów (lub requestów) serwer generuje na sekundę w agregacie — interesuje Cię przy workloadach wsadowych, przetwarzaniu dokumentów offline, API z wysokim wolumenem.
Latencja mierzy, jak szybko konkretny użytkownik otrzymuje swoją odpowiedź — TTFT i całkowity czas do końca generowania. Interesuje Cię przy aplikacjach interaktywnych, chatbotach, copilotach.
Problem pojawia się, gdy continuous batching jest stosowany agresywnie: serwer może celowo opóźniać wysłanie odpowiedzi, żeby zdążyć zapełnić większą paczkę — wyższy throughput, gorsza latencja dla jednostki. Większość produkcyjnych frameworków ma parametry (--max-num-seqs, scheduler-delay-factor), którymi ten trade-off reguluje się według priorytetów.
Dla aplikacji interaktywnych zazwyczaj właściwą strategią jest priorytetyzacja TTFT — użytkownik, który widzi pierwsze tokeny w ciągu dwóch sekund, postrzega system jako „szybki", nawet jeśli całkowite generowanie trwa dłużej. Streaming odpowiedzi (SSE lub WebSocket) jeszcze bardziej poprawia to odczucie bez zmiany rzeczywistego throughputu.
Przy porównywaniu rozwiązań serwujących: SGLang osiąga na workloadach intensywnych prefiksowo rzędem około jednej piątej niższy TTFT niż vLLM na tym samym sprzęcie, co przy aplikacjach interaktywnych jest odczuwalną różnicą. Dla batch serwowania różnica jest mniejsza.
Prefix caching i współdzielenie KV-cache
Gdy wiele requestów zaczyna się od tego samego promptu systemowego — co przy produkcyjnych aplikacjach jest normą — każdy request przelicza ten sam prefiks od nowa. To marnotrawstwo: prefill tych samych 500 tokenów promptu systemowego 1000 razy dziennie.
Prefix caching (albo prompt caching) rozwiązuje ten problem: serwer zapisuje wyniki KV prefiksu i przy kolejnym requeście z identycznym początkiem po prostu odczytuje je z cache zamiast przeliczać. Efekt jest podwójny — zmniejsza latencję (TTFT dla cachowanego prefiksu jest bliskie zeru) i koszty obliczeniowe.
SGLang implementuje to przez RadixAttention — LRU cache wartości KV zorganizowane w drzewo radixowe, gdzie automatycznie wyszukiwany i współdzielony jest najdłuższy wspólny prefiks między requestami. Na workloadach z długimi, powtarzającymi się prefiksami (RAG z tym samym kontekstem, wieloturowy chatbot z tym samym promptem systemowym) poprawa throughputu jest mierzalna.
Po stronie cloud API dostawcy implementują analogiczną funkcję: automatyczne prompt caching zmniejsza koszty tokenów wejściowych przy powtarzających się prefiksach o rzędu 50–90%. Jest to istotne również dla architektur hybrydowych, gdzie część requestów idzie do chmury — właściwe strukturyzowanie promptu (stały prompt systemowy na początku, dynamiczna treść na końcu) może znacznie obniżyć rachunek. Szczegółowo omawia to artykuł Prompt caching a koszt.
Jak wyciągnąć więcej z jednego GPU bez dokupowania sprzętu
Podsumowanie praktycznych dźwigni, które obserwujemy w wdrożeniach — w kolejności według typowego wpływu:
- 1.Przejdź na produkcyjny framework serwujący — jeśli działasz na
Ollamaz kilkoma równoległymi użytkownikami, przejście navLLMlubSGLangto najistotniejsza zmiana, jaką możesz wprowadzić. Continuous batching i PagedAttention są wbudowane.
- 1.Ustaw właściwą kwantyzację — Q4_K_M lub AWQ 4-bit wyraźnie zmniejsza pamięciowy footprint modelu w porównaniu z FP16 (typowo do jednej trzeciej, jednej czwartej), co zwalnia VRAM dla większych partii przy utracie jakości poniżej 5–8%. Więcej o formatach w artykule Kwantyzacja LLM (GGUF, AWQ, GPTQ).
- 1.Włącz kwantyzację KV-cache — FP8 lub INT8 KV-cache zmniejsza pamięciowy footprint o połowę przy minimalnym wpływie na wyjścia. W
vLLMto parametr--kv-cache-dtype.
- 1.Optymalizuj pod swój kontekst — jeśli Twoje requesty wykorzystują średnio tylko 20% ustawionej maksymalnej długości kontekstu, obniżenie
--max-model-lenzwolni VRAM dla większej liczby równoległych requestów.
- 1.Wykorzystaj prefix caching — jeśli prompt systemowy lub kontekst RAG jest taki sam w kolejnych requestach, skieruj je do frameworku z RadixAttention (SGLang) lub włącz prefix caching w
vLLM.
- 1.Monitoruj rzeczywiste wykorzystanie GPU —
nvidia-smi dmoni metryki frameworku serwującego pokażą, gdzie leży bottleneck. Niskie wykorzystanie mocy obliczeniowej GPU przy wysokiej latencji typowo sygnalizuje problem z zarządzaniem KV-cache lub nieoptymalne batchowanie.
Kiedy wiele GPU ma sens
Horyzontalne skalowanie (tensor parallelizm na kilku GPU) jest uzasadnione w dwóch sytuacjach: model fizycznie nie mieści się na jednym GPU nawet po kwantyzacji, albo wyczerpałeś wszystkie optymalizacje na jednym GPU i throughput nadal nie wystarczy.
Tensor parallelizm (--tensor-parallel-size 2 w vLLM) dzieli macierze modelu między GPU — każde GPU przetwarza część obliczeń, wyniki są synchronizowane. Skaluje się niemal liniowo dla fazy prefill, mniej efektywnie dla decode (z powodu narzutu synchronizacji).
Dla większości firmowych wdrożeń z jednym modelem i dziesiątkami równoległych użytkowników, jedno dobrze skonfigurowane GPU z właściwym frameworkiem serwującym jest wydajniejsze i ekonomicznie bardziej uzasadnione niż dwa GPU z Ollama. Dokupowanie sprzętu przed optymalizacją softwaru to częsty, ale zupełnie zbędny koszt.
W kwestii doboru konkretnego GPU i wymaganej VRAM dla danego modelu i obciążenia odsyłamy do praktycznego przewodnika w artykule Jakie GPU do inferencji LLM.
Najczęstsze pytania
Czym jest TTFT i dlaczego ma znaczenie?
TTFT (Time to First Token) to czas od wysłania requestu do momentu, gdy serwer zaczyna strumieniować pierwsze tokeny odpowiedzi. W aplikacjach interaktywnych (chatboty, copiloty) TTFT jest kluczowy dla percepcji szybkości — użytkownik, który dostaje pierwszy token w ciągu 1–2 sekund, postrzega system jako responsywny, nawet jeśli cała odpowiedź trwa 10 sekund. Długi kontekst i pełny KV-cache wydłużają TTFT, bo faza prefill trwa dłużej.
Jaka jest różnica między PagedAttention a continuous batching?
To dwie ortogonalne optymalizacje rozwiązujące różne problemy. PagedAttention dotyczy zarządzania pamięcią — zmniejsza fragmentację KV-cache dzięki dynamicznej alokacji w stronach zamiast rezerwacji z góry. Continuous batching dotyczy harmonogramowania requestów — zamiast czekać na koniec paczki, dodaje nowe requesty do bieżącego przebiegu na bieżąco. Oba działają razem i są zaimplementowane w produkcyjnych frameworkach takich jak vLLM.
Czy prefix caching opłaca się nawet przy mniejszych wdrożeniach?
Tak, jeśli masz spójny prompt systemowy lub kontekst RAG powtarzający się w kolejnych requestach. Nawet przy kilkunastu równoległych użytkownikach prefix caching może zmniejszyć TTFT o 40–70% dla requestów z długim cachowanym prefiksem. Po stronie cloud API bezpośrednio wpływa to na koszty — właściwie ustrukturyzowany prompt ze stałym prefiksem może zaoszczędzić kilkadziesiąt procent na tokenach wejściowych.
Kiedy warto przejść z Ollama na vLLM?
Gdy masz więcej niż jednego równoległego użytkownika w środowisku produkcyjnym. Ollama doskonale sprawdza się na desktopie dewelopera, do prototypowania i single-user dostępu lokalnego. Przy wielu równoległych requestach continuous batching i PagedAttention w vLLM zapewnią wyraźnie wyższy throughput za tę samą cenę sprzętu. Migracja jest stosunkowo prosta — vLLM udostępnia API kompatybilne z OpenAI, więc zmiana URL i klucza zazwyczaj wystarczy.
Jak długość kontekstu wpływa na throughput?
Liniowo i znacząco. Dłuższy kontekst oznacza większy KV-cache na request, co bezpośrednio zmniejsza liczbę requestów mieszczących się w dostępnej VRAM jednocześnie. Model z kontekstowym oknem 1M potrzebuje na jeden długi request rzędu stu gigabajtów KV-cache — co dla typowych produkcyjnych GPU jest nieosiągalne. W przypadku większości realnych przypadków użycia RAG z krótszym kontekstem jest ekonomicznie wyraźnie korzystniejszy niż długie okno kontekstowe wypełnione całym dokumentem. Więcej o decydowaniu między RAG a długim kontekstem w artykule Kontekstowe okno — kiedy 1M tokenów pomoże.
*MP Industrial Solutions pomaga firmom zaprojektować architekturę serwowania LLM dopasowaną do obciążenia i budżetu — od wyboru frameworka serwującego po konfigurację KV-cache i kwantyzacji. Jeśli rozwiązujesz problem spowolnienia istniejącego wdrożenia lub planujesz produkcyjny deployment, chętnie przyjrzymy się konkretnym liczbom i zaproponujemy optymalizację.*
