Jeśli kiedykolwiek obserwowałeś, jak LLM generuje długą odpowiedź i zastanawiałeś się, dlaczego po początkowym błysku promptu wydaje się pełzać, to właśnie na własne oczy widzisz wąskie gardło sprzętowe. Większość programistów obwinia swój kod w Pythonie, framework lub samą wielkość modelu. Profilują funkcje, zmieniają optymalizatory i oszczędzają milisekundy na preprocessingu. Żadne z tego nie rozwiązuje prawdziwego problemu. Limit prędkości nie leży w oprogramowaniu. Leży w krzemie.
Każde zadanie wnioskowania (inference) dużego modelu językowego zależy od dwóch cech fizycznych procesora GPU znajdującego się w Twoim serwerze: tego, jak szybko potrafi on przetwarzać liczby oraz jak szybko potrafi przemieszczać te liczby na pozycje gotowe do przetworzenia.
Matematyka jest tania. Przesyłanie danych – nie.
Marketing GPU uwielbia mówić o mocy obliczeniowej (compute). Biliony operacji zmiennoprzecinkowych na sekundę. Liczby te są oszałamiające. Ale moc obliczeniowa to tylko połowa sukcesu. Druga połowa to przepustowość pamięci – tempo, w jakim dane przemieszczają się z pamięci o wysokiej przepustowości do rdzeni obliczeniowych, gdzie odbywają się właściwe operacje arytmetyczne.
LLM nie może działać szybciej niż najsłabsze z tych dwóch ogniw. Wyobraź sobie profesjonalną kuchnię z dwudziestoma mistrzami kuchni. Piece są gorące, noże ostre, a każdy kucharz jest gotowy do pracy. Jednak dostawy produktów przyjeżdżają rowerem, koszyk po koszyku. Kuchnia staje w miejscu. Zatrudnienie większej liczby kucharzy tego nie naprawi. Zakup szybszych pieców również nie pomoże. Wąskim gardłem jest droga.
W nowoczesnych procesorach GPU stosowanych w centrach danych jednostki arytmetyczne są tak potężne, że często kończą obliczenia i pozostają bezczynne, marnując cykle zegara w oczekiwaniu, aż wagi i aktywacje przepłyną przez pamięć. Ta nierównowaga nie jest błędem w Twoim kodzie. To fizyczna rzeczywistość budowy układów scalonych. Przepustowość pamięci nie nadąża za surową mocą obliczeniową, a modele LLM są szczególnie bezlitosne dla tej dysproporcji, ponieważ ich przejścia w przód (forward passes) wymagają odwołania się do każdego pojedynczego parametru przy każdym wygenerowanym tokenie.
Dlaczego prompty wydają się szybkie, a generowanie powolne
Wnioskowanie LLM dzieli się na dwie wyraźne fazy, które obciążają sprzęt w zupełnie inny sposób.
Prefill następuje, gdy Twój prompt trafia do modelu. Wszystkie tokeny przychodzą jednocześnie. GPU może przetwarzać je równolegle, stosując duże mnożenia macierzy przez macierze. Tysiące jednostek arytmetycznych pracuje jednocześnie, a obciążenie pozostaje wysokie. Ta faza jest ograniczona mocą obliczeniową (compute-bound). Ten nagły skok prędkości, który widzisz na początku? To GPU robi dokładnie to, do czego zostało stworzone.
Decode to moment, w którym zaczynają się problemy. Gdy model generuje kolejny token, robi to jeden po drugim. Ten etap opiera się na operacjach macierzowo-wektorowych, które wykorzystują jedynie ułamek równoległej mocy obliczeniowej GPU. Co gorsza, każdy nowy token zmusza GPU do ponownego wczytania wszystkich wag modelu z pamięci. Jednostki arytmetyczne chcą pracować, ale zamiast tego czekają. Decode jest ograniczone przepustowością pamięci (memory-bound). GPU działa w efekcie jak kosztowny kontroler ruchu, przesyłając parametry tam i z powrotem przez szynę pamięci, podczas gdy silniki matematyczne „stygną”. To dlatego odpowiedź licząca sto słów może zająć dziesięć sekund, mimo że analiza początkowego promptu wydawała się natychmiastowa.
Bufor KV (KV cache) czyni tę sytuację jeszcze ciekawszą. Podczas dekodowania model przechowuje tensory kluczy (key) i wartości (value) dla każdego poprzedniego tokena, aby nie musieć ponownie obliczać mechanizmu uwagi (attention) od zera. Bufor ten rośnie wraz z długością sekwencji i znajduje się w pamięci. Oznacza to, że GPU nie tylko ponownie wczytuje wagi, ale przy każdym przejściu w przód (forward pass) odczytuje i zapisuje stale rozrastający się bufor. Rdzenie obliczeniowe prawie się nie pocą, podczas gdy szyna pamięci męczy się za nie obie.
Walka ze ścianą pamięci
Inżynierowie opracowali niewielki arsenał technik, aby zmniejszyć ilość przesyłanych danych lub przynajmniej rozłożyć koszt ich przesyłania.
Batching (grupowanie) jest najprostszy. Jeśli zapytanie jednego użytkownika wymusza pełne wczytanie wag z pamięci, to przetwarzanie ośmiu lub szesnastu zapytań jednocześnie pozwala GPU rozłożyć to obciążenie na wszystkie z nich. Wagi są odczytywane raz i wykorzystywane dla każdej sekwencji w partii (batch). W środowiskach produkcyjnych zaawansowane systemy harmonogramowania dynamicznie grupują zapytania, co nazywa się czasem continuous batching lub in-flight batching, dzięki czemu GPU rzadko przerywa pracę. To różnica między autobusem a szesnastoma osobnymi samochodami jadącymi tą samą trasą.
Kwantyzacja bezpośrednio uderza w problem przepustowości. Wagi modelu są zazwyczaj przechowywane w formatach zmiennoprzecinkowych szesnastobitowych. Poprzez ich kompresję do ośmiobitowych lub nawet czterobitowych liczb całkowitych, dosłownie zmniejszasz ilość danych przesyłanych przez szynę o połowę lub więcej. Model wciąż potrzebuje wystarczającej precyzji, aby generować spójne wyniki, ale nowoczesne metody kwantyzacji po treningu (post-training quantization) potrafią drastycznie zmniejszyć rozmiar modelu w pamięci bez utraty jakości. Mniejsza ilość przesyłanych danych oznacza mniej czasu spędzonego na oczekiwaniu w kontrolerze pamięci.
FlashAttention przebudowuje mechanizm uwagi, aby przechowywać wyniki pośrednie w szybkiej pamięci on-chip procesora GPU. Standardowa uwaga musiała zapisywać duże macierze uwagi w wolnej pamięci zewnętrznej, a następnie odczytywać je ponownie. FlashAttention dzieli obliczenia na mniejsze kafelki (tiles), które mieszczą się w pamięci SRAM, wykonuje kroki softmax i skalowania wewnątrz układu, a do pamięci o wysokiej przepustowości zapisuje jedynie końcowe wyniki. Wymienia nieco dodatkowej mocy obliczeniowej na znacznie mniej cykli odczytu i zapisu do pamięci głównej, co niemal zawsze jest korzystną strategią.
PagedAttention rozwiązuje inny rodzaj marnotrawstwa pamięci. Podczas dekodowania pamięć podręczna KV (KV cache) rośnie w sposób nieprzewidywalny. Tradycyjne systemy przydzielają stałe, ciągłe bloki pamięci dla każdej sekwencji, co pozostawia duże luki, gdy niektóre sekwencje kończą się wcześniej, a inne się rozszerzają. PagedAttention zapożycza koncepcję pamięci wirtualnej z systemów operacyjnych. Przechowuje wpisy KV cache w blokach o stałym rozmiarze, które mogą być przydzielane w sposób nieciągły i mapowane za pomocą tabeli pośredniej (indirection table). Zapobiega to marnowaniu pamięci w zarezerwowanych, ale połowicznie pustych buforach i pozwala na stosowanie większych rozmiarów partii (batch sizes), co z kolei zwiększa ogólną przepustowość, utrzymując szynę pamięci zajętą pożyteczną pracą zamiast narzutem związanym z fragmentacją.
Zmień pytanie
Gdy występują skoki opóźnień, zbyt wiele zespołów zastanawia się, czy powinny przejść na mniejszy model lub przepisać swój serwer wnioskowania (inference server). Te pytania są istotne, ale mają charakter drugorzędny. Pierwsze pytanie powinno dotyczyć samego sprzętu. Czy Twój procesor GPU faktycznie zajmuje się obliczeniami, czy może „głoduje” z braku danych?
Spójrz na swoje metryki utylizacji. Przeanalizuj nasycenie przepustowości pamięci wraz z obciążeniem obliczeniowym GPU (compute occupancy). Jeśli podczas dekodowania widzisz wysokie rywalizowanie o dostęp do pamięci (memory contention) i niską intensywność arytmetyczną (arithmetic intensity), nie masz problemu z architekturą modelu. Masz problem z fizyką. Rozwiązanie nie przyjdzie wraz z czystszym kodem w Pythonie. Przyjdzie wraz z bardziej agresywnym grupowaniem (batching), kwantyzacją wag, aby szybciej „przeciskały się przez rurę”, przebudową mechanizmu uwagi, aby pozostał wewnątrz układu, oraz zarządzaniem pamięcią KV cache, aby móc pomieścić większe partie bez braku miejsca.
Gdy zaczniesz postrzegać wnioskowanie (inference) przez ten pryzmat, optymalizacja stanie się procesem mechanicznym. Przestaniesz gonić mity o tym, że inteligencja modelu spowalnia procesy, a zaczniesz podejmować decyzje inżynieryjne oparte na tym, co sprzęt jest w stanie faktycznie dostarczyć. To jest ta zmiana, która odróżnia systemy produkcyjne zdolne do skalowania od tych, które jedynie działają.
