Generative AI Design Patterns #7: prefix cache skrócił TTFT 40 razy, cache semantyczny pomylił sobotę z niedzielą
Rozdział 8 „Generative AI Design Patterns" dotyczy ograniczeń: kosztu, latencji, sprzętu i pamięci rozmowy. Większość liczb z tego rozdziału się zestarzeje, ale dwa wzorce są trwałe: test degradacji i pamięć długoterminowa. Sprawdziłem cztery tezy na Gemmie 4 26B na stacji z RTX 5090. Prefix cache skrócił czas do pierwszego tokenu przy prompcie 35 tys. tokenów z 2,4 s do 59 ms. Cache semantyczny na bge-m3 nie odróżnił „sklep w sobotę" od „sklep w niedzielę": żaden próg nie oddzielił parafraz od pytań różniących się jednym słowem. Przy ruchu o 20% większym, niż serwer przerabia, p95 czasu do pierwszego tokenu wyniósł 13 s. Test, który czeka na odpowiedź przed wysłaniem kolejnego zapytania, pokazał przy dwukrotnym przeciążeniu 3,5 s. Profil faktów wyciągany z rozmowy odpowiadał tak dobrze jak pełna historia (21 i 20 na 21) przy prompcie ponad sześć razy krótszym.
To dziewiąty wpis serii o książce Valliappy Lakshmanana i Hannesa Hapkego. Recenzja i mapa serii są we wpisie 0, poprzedni wpis dotyczył agentów i narzędzi. Cytuję po rozdziale i numerze wzorca, bo PDF nie ma stron druku.
Teza
Rozdział 8 jest najbardziej inżynierski w książce i najszybciej się zestarzeje. Ceny instancji, progi cache, nazwy modeli i lista technik inferencji opisują stan z połowy 2025 r. Autorzy datują część z nich sami, na przykład ceny instancji na czerwiec 2025 r. Pod tymi liczbami są jednak dwie myśli, które nie zależą od wersji modelu.
Pierwsza pochodzi z Pattern 27: test obciążeniowy znajduje moment, w którym serwer pada, a w aplikacji z modelem trzeba wiedzieć, kiedy usługa zaczyna się psuć. Autorzy piszą, że trzeba umieć wskazać, „how service quality will start to degrade, not just the point at which it fails" (rozdz. 8, Pattern 27). Druga pochodzi z Pattern 28: każde wywołanie modelu jest bezstanowe, a doklejanie całej historii do każdego promptu jest za drogie, bo koszt rośnie kwadratowo z długością sekwencji (rozdz. 8, Pattern 28).
Moja teza: w tym rozdziale trwałe jest to, co mówi, jak mierzyć, a datowane to, co mówi, ile wychodzi. Pomiar na stacji z RTX 5090 potwierdza autorów w każdym z czterech miejsc, które sprawdziłem. W dwóch pokazuje przy tym coś, czego autorzy nie mówią wprost. Cache semantyczny myli się najbardziej tam, gdzie pomyłka najbardziej kosztuje. Sposób prowadzenia testu degradacji zmienia wynik bardziej niż sam serwer.
Pattern 24: Small Language Model
Problem. Duży model jest drogi, wymaga GPU, których brakuje, i odpowiada wolno. Autorzy zaczynają od liczb: Llama 4 Scout potrzebuje czterech kart H100 80 GB, czyli ponad 10 $ za godzinę instancji, a model 70 mld parametrów w FP32 to ok. 280 GB samych wag (rozdz. 8, Pattern 24). Zmierzyli też na dwóch A100-40 GB Gemmę 3 27B (3,26 tok/s) i Gemmę 3 1B (8,82 tok/s). Mniejszy model przy dokumentowaniu kodu zwracał jednak opis prozą zamiast docstringów. Prosta podmiana modelu na mniejszy nie działa.
Rozwiązanie. Trzy techniki. Destylacja: mniejszy model uczy się naśladować rozkład większego, a temperatura w stracie zachowuje wiedzę nauczyciela o alternatywnych tokenach, a nie tylko o zwycięzcy. Kwantyzacja: niższa precyzja wag przy zachowaniu prawie całej wiedzy, od QAT przez GPTQ i AWQ po 4-bitowe ładowanie przez BitsAndBytesConfig. Speculative decoding: mały model szkicuje, duży sprawdza, co skraca latencję bez utraty dokładności, za to dokłada złożoności. Pruning autorzy odrzucają wprost, bo „significantly impacts the LLM's performance" (rozdz. 8, Pattern 24).
Najrozsądniejsza rada z tego wzorca dotyczy ścieżki wdrożenia: najpierw prototyp na modelu z czołówki, potem logowanie promptów, które faktycznie przychodzą, i dopiero na koniec destylacja pod dokładnie te typy zapytań.
Kiedy nie używać. Gdy zadanie jest szerokie. Model destylowany traci ogólne zdolności nauczyciela i w zastosowaniu ogólnym wypada gorzej (rozdz. 8, Pattern 24).
Tego wzorca nie mierzyłem, bo wymagałoby to podmiany modelu na współdzielonej stacji. Bliskie pomiary są w dwóch wcześniejszych wpisach. Qwen3.8-27B w INT4 był szybszy od tego samego modelu w NVFP4, choć karta obsługuje FP4 sprzętowo. W porównaniu czterech modeli w RAG-u najszybszy kandydat nie zaliczył wszystkich tur, a mniejszy model gęsty wypadł gorzej niż MoE z 4 mld aktywnych parametrów.
Pattern 25: Prompt Caching
Problem. Część zapytań się powtarza. Autorzy podają statystyki z call center, na przykład 40% dzwoniących do sklepów pyta o godziny otwarcia (rozdz. 8, Pattern 25). Nie podają jednak źródła, więc traktuję je jako ilustrację.
Rozwiązanie. Pod jedną nazwą kryją się dwie różne rzeczy. Cache po stronie klienta trzyma gotową odpowiedź pod kluczem z promptu. Daje największy zysk, bo nie ma wywołania, ale odpowiedź jest powtórzona dosłownie. Cache po stronie serwera (prefix caching) trzyma stan wewnętrzny modelu dla wspólnego początku promptu. Odpowiedź nadal jest generowana, więc cache „nie wpływa na kreatywność", a czas do pierwszego tokenu wyraźnie spada. Według autorów prompt musi zwykle przekroczyć 1024 tokeny, zależnie od dostawcy. Według autorów vLLM był pierwszym silnikiem z automatycznym prefix cachingiem. Wariantem cache klienta jest cache semantyczny: klucz kanoniczny albo wyszukiwanie po embeddingach. Autorzy nazywają jego dwie wady. Trzeba dobrać próg podobieństwa, który waży fałszywe trafienia i chybienia. Użytkownicy dostają też tę samą odpowiedź na różne pytania, przez co system „might seem less »smart«" (rozdz. 8, Pattern 25).
Kiedy nie używać. Cache semantycznego nie używaj tam, gdzie niuanse pytania zmieniają odpowiedź.
Pomiar: prefix cache i układ promptu
Wziąłem syntetyczny regulamin fikcyjnego sklepu w czterech długościach: ok. 0,5, 2,2, 8,7 i 35 tys. tokenów. Do każdego zadawałem krótkie pytanie, a model generował 16 tokenów. W wariancie zimnym prompt zaczynał się od unikalnego identyfikatora sesji, więc żaden prefiks nie mógł trafić w cache. W wariancie ciepłym regulamin był identyczny, a zmieniało się tylko pytanie. Każdy wariant powtarzałem 10 razy, sekwencyjnie. Trafienia liczyłem z liczników prefix_cache_queries_total i prefix_cache_hits_total w /metrics vLLM przed każdym zapytaniem i po nim. Przyrost zapytań równał się za każdym razem liczbie tokenów promptu, więc w trakcie pomiaru nikt inny nie korzystał z serwera.
| Prompt | TTFT zimny (mediana) | TTFT ciepły (mediana) | Trafienia w cache (ciepły) |
|---|---|---|---|
| ok. 0,5 tys. tokenów | 33 ms | 13 ms | 94% |
| ok. 2,2 tys. | 69 ms | 27 ms | 98% |
| ok. 8,7 tys. | 315 ms | 26 ms | 100% |
| ok. 35 tys. | 2376 ms | 59 ms | 100% |
Przy 35 tys. tokenów prefix cache skrócił czas do pierwszego tokenu 40 razy. Zimny TTFT rośnie szybciej niż długość promptu: cztery razy dłuższy prompt (8,7 → 35 tys.) dał 7,5 raza dłuższe czekanie. To zgadza się z tym, co autorzy piszą o kwadratowym koszcie długich sekwencji przy Pattern 28. Próg 1024 tokenów z książki dotyczy API dostawców. W tym vLLM blok cache ma 16 tokenów, więc trafiał już prompt 500-tokenowy.
Drugi test sprawdzał radę z Pattern 27: tekst przewidywalny na początku promptu, dynamiczny (np. wyniki RAG) na końcu. Do regulaminu 8,7 tys. tokenów dokładałem unikalny blok ok. 1,8 tys. tokenów z historią zamówień klienta, raz za regulaminem, raz przed nim.
| Układ | TTFT (mediana, 10 prób) | Trafienia w cache |
|---|---|---|
| regulamin, potem dane klienta | 107 ms | 82% |
| dane klienta, potem regulamin | 404 ms | 0% |
Jedna zmiana kolejności, ta sama treść i cztery razy dłuższe czekanie. Prefix cache trafia tylko w identyczny początek, więc jeden zmienny token na początku promptu unieważnia wszystko, co jest za nim. Najczęstsze źródło takiego tokenu to data albo identyfikator wstawiony do promptu systemowego „dla kontekstu".
Pomiar: próg cache semantycznego
Przed uruchomieniem spisałem zbiór: 24 pytania w cache (sklep i bank), do każdego dwie parafrazy z tą samą odpowiedzią i dwa prawie-duplikaty, które wymagają innej odpowiedzi, na przykład „w sobotę" i „w niedzielę", „kartę debetową" i „kartę kredytową", „przed wysyłką" i „po wysyłce". Do tego 10 pytań spoza bazy. Każde zapytanie embeddowałem bge-m3 i szukałem najbliższego pytania w cache po cosinusie.
| Grupa | Podobieństwo do najbliższego (min–mediana–max) |
|---|---|
| parafrazy (48) | 0,764 – 0,892 – 0,974 |
| prawie-duplikaty (48) | 0,673 – 0,861 – 0,976 |
| spoza bazy (10) | 0,360 – 0,467 – 0,669 |
| Próg | Parafrazy obsłużone z cache | Prawie-duplikaty z cudzą odpowiedzią |
|---|---|---|
| 0,76 | 48/48 | 43/48 |
| 0,84 | 38/48 | 26/48 |
| 0,90 | 21/48 | 14/48 |
| 0,94 | 3/48 | 6/48 |
| 0,98 | 0/48 | 0/48 |
Każda parafraza trafiła we właściwe pytanie, a pytania spoza bazy dało się odciąć progiem: najwyższe miało 0,669, najniższa parafraza 0,764. Prawie-duplikatów nie odcina żaden próg. Przy 0,94 cache obsługiwał już tylko 3 parafrazy na 48, a cudzą odpowiedź nadal dostawało 6 prawie-duplikatów. Najwyżej ocenione były „Czy oddział w Krakowie przy Rynku jest czynny w niedzielę?" (0,976 do pytania o sobotę) i „Jak zablokować zgubioną kartę kredytową?" (0,970 do pytania o debetową). W 15 z 24 pozycji któryś prawie-duplikat był bliżej oryginału niż któraś z jego parafraz.
Zbiór jest stronniczy w jedną stronę: prawie-duplikaty pisałem celowo jako minimalne zmiany jednego słowa. Zakładam jednak, że właśnie takie pytania zadają klienci. Autorzy ostrzegają, że system wyda się „mniej mądry". Pomiar pokazuje coś gorszego: przy każdym progu, który obsługuje parafrazy, klient pytający o niedzielę dostaje godziny z soboty. Embedding mierzy podobieństwo pytania, a nie tożsamość odpowiedzi. Dlatego za bezpieczniejszy z wariantów autorów uważam klucz kanoniczny, czego jednak nie mierzyłem: model mapuje pytanie na jedną z zamkniętej listy intencji z parametrami („godziny otwarcia", dzień = niedziela), a cache trzyma odpowiedź pod tą parą.
Pattern 26: Inference Optimization
Problem. Wzorzec dotyczy wyłącznie self-hostingu. Motywacja autorów to dane zdrowotne, finansowe i prawne, które nie mogą opuścić firmy. Użytkownicy modeli 70B+ oczekują jednak czasów odpowiedzi jak od ChatGPT, a kart graficznych brakuje (rozdz. 8, Pattern 26).
Rozwiązanie. Trzy techniki, które się nie wykluczają. Continuous batching: żądania wchodzą do wsadu i wychodzą z niego po każdym kroku generowania, więc krótkie nie czekają na najdłuższe. vLLM i SGLang robią to domyślnie. Speculative decoding w architekturze draft-target: model szkicujący nie musi być uczniem dużego, oba mogą być trenowane osobno. Kompresja promptu: twarda, czyli usuwanie redundancji, i miękka, czyli zamiana akapitów na wektory, która działa tylko z modelem, pod który ją zrobiono. W przykładzie autorów continuous batching na 100 próbkach skrócił czas ze 106,11 s do 4,60 s. Skrypt wypisał „Speedup factor: 23.07x", a w tekście jest „20x". To jeden przebieg na ich sprzęcie. Speculative decoding przyspieszył „by roughly 14.2%". Ramka w książce ostrzega, że zbyt długie num_speculative_tokens może dać przepustowość gorszą niż sam duży model.
Na stacji nie włączałem speculative decoding, bo wymagałoby to restartu współdzielonego serwera. Continuous batching widać jednak w teście poniżej. Przejście z 1 do 16 równoległych zapytań zwiększyło przepustowość z 227 do 1388 tokenów na sekundę (6,1×), a pełny czas pojedynczej odpowiedzi wydłużył się z 1,1 do 2,9 s. Z liczbą 20× z książki tego nie porównuję: inny model, inna karta, inne długości.
Pattern 27: Degradation Testing
Problem. Klasyczny test obciążeniowy szuka punktu, w którym serwer zaczyna zwracać błędy 400 i 500. W aplikacji z modelem usługa psuje się dużo wcześniej: użytkownik czeka coraz dłużej na pierwsze słowo, choć żaden błąd nie pada.
Rozwiązanie. Cztery metryki. TTFT to czas do pierwszego tokenu, czyli to, co użytkownik odbiera jako bezczynność. EERL (end-to-end request latency) to pełny czas odpowiedzi z kolejką i siecią, raportowany jako P50, P95 i P99. TPS to przepustowość całego systemu, która według autorów po punkcie nasycenia zaczyna spadać. RPS to liczba ukończonych żądań na sekundę. Do tego cztery rodzaje testów z różnymi celami: skalowalność (gdzie jest punkt przegięcia), stress (gdzie jest awaria i jak system z niej wraca), load (czy spodziewany szczyt przechodzi) i pomiar bazowy (rozdz. 8, Pattern 27). W przykładzie złej konfiguracji autorów większość żądań padła, a średni TTFT wyniósł 52 s.
Pomiar: ten sam serwer, dwa sposoby testu
Każde zapytanie miało unikalny prompt ok. 2,2 tys. tokenów, bez pomocy prefix cache, i pytanie ze sprawdzalną odpowiedzią: „ile dni dotyczy paragraf N" plus wyjaśnienie dla klienta (mediana 257 tokenów wyjścia). Serwer ma ustawione --max-num-seqs 16, czyli obsługuje naraz najwyżej 16 sekwencji. Na tę część miałem zgodę autora na krótkie obciążenie współdzielonej stacji. Cały test trwał kilka minut.
Najpierw test zamknięty, czyli tak, jak działa większość prostych skryptów: c wątków, każdy wysyła następne zapytanie dopiero po odebraniu poprzedniej odpowiedzi.
| Wątki | TTFT p50 / p95 | EERL p50 / p95 | TPS | RPS |
|---|---|---|---|---|
| 1 | 0,09 / 0,09 s | 1,12 / 1,18 s | 227 | 0,89 |
| 4 | 0,09 / 0,20 s | 1,68 / 1,88 s | 594 | 2,31 |
| 8 | 0,12 / 0,39 s | 2,07 / 2,39 s | 980 | 3,77 |
| 16 | 0,18 / 0,72 s | 2,94 / 3,53 s | 1388 | 5,39 |
| 32 | 3,03 / 3,51 s | 5,81 / 6,42 s | 1402 | 5,41 |
Punkt nasycenia jest wyraźny. Między 16 a 32 wątkami przepustowość stoi w miejscu (1388 → 1402 tok/s), a nie spada, jak u autorów, a pełny czas odpowiedzi rośnie dwukrotnie, bo połowa zapytań czeka w kolejce. Monitor /metrics pokazał 16 zapytań w toku i do 26 w kolejce. Na tym kończy się większość testów i taki wynik wygląda znośnie: p95 TTFT 3,5 s przy dwukrotnym przeciążeniu.
Potem test otwarty. Zapytania przychodziły według zegara (proces Poissona) z intensywnością równą części przepustowości zmierzonej przy 16 wątkach, czyli 5,39 zapytania na sekundę. Każdy poziom trwał 60 s, a latencję liczyłem od zaplanowanego momentu wysłania, nie od faktycznego.
| Ruch względem przepustowości | TTFT p50 / p95 | EERL p50 / p95 | Najdłuższa kolejka |
|---|---|---|---|
| 50% | 0,09 / 0,14 s | 2,00 / 2,57 s | 0 |
| 80% | 0,10 / 1,14 s | 2,84 / 3,90 s | 9 |
| 100% | 0,37 / 1,65 s | 3,17 / 4,50 s | 12 |
| 120% | 5,11 / 12,99 s | 8,02 / 15,81 s | 72 |
Przy ruchu o 20% większym od przepustowości p95 TTFT wyniósł 13 s, prawie cztery razy więcej niż w teście zamkniętym przy dwukrotnym przeciążeniu. Kolejka urosła do 72 zapytań i rosłaby dalej, bo przychodziło więcej, niż serwer przerabiał. Wartość 13 s zależy więc od długości testu: przy dwóch minutach byłaby wyższa. Test zamknięty tego nie pokazał, bo jego klient zwalnia razem z serwerem. Gdy odpowiedzi przychodzą wolniej, wysyła mniej zapytań i sam ogranicza kolejkę.
Druga rzecz, której nie widać w teście zamkniętym: przy 80% przepustowości p95 TTFT było już ośmiokrotnie wyższe niż przy 50% (1,14 s wobec 0,14 s), a w kolejce czekało do 9 zapytań. Losowe przyjścia tworzą chwilowe zatory na długo przed nasyceniem. Moim zdaniem to jest punkt, w którym usługa „zaczyna się psuć", o którym piszą autorzy. Konkretnego progu autorzy nie podają.
Sprawdziłem też, czy przy obciążeniu spada jakość. Surowy wynik sugerował spadek: przy 1 wątku poprawna liczba dni padła w 19 na 20 odpowiedzi, a przy 32 w 64 na 96. To był jednak artefakt mojego projektu testu. Paragraf losowałem osobno na każdym poziomie, a trafność zależała od paragrafu: dla jednego 1 na 47, dla innego 35 na 35. Po warstwowaniu po paragrafie trafność wyniosła 0,753 przy niskim obciążeniu (do 8 wątków i 50% ruchu, 252 zapytania) i 0,759 przy wysokim (32 wątki i 120% ruchu, 493 zapytania). Jakość się nie zmieniła. W tym serwerze degradacja dotyczy czasu, nie treści. Lekcja metodyczna: na każdym poziomie obciążenia trzeba zadawać ten sam zestaw pytań.
Pattern 28: Long-Term Memory
Problem. Każde wywołanie modelu jest bezstanowe. Chatbot udaje stan, doklejając poprzednie tury do promptu, ale przy długiej historii robi się to za drogie (rozdz. 8, Pattern 28).
Rozwiązanie. Cztery rodzaje pamięci. Working to bieżąca sesja, przycinana do limitu tokenów, a nie do liczby wiadomości, z zachowaniem promptu systemowego i bez urwanych par. Episodic to wiadomości z wcześniejszych sesji wyszukiwane po trafności i metadanych, czyli RAG po własnej historii. Procedural to instrukcje i profil użytkownika, także fakty wydobyte z rozmów, na przykład „jestem uczulony na orzechy". Semantic to fakty wyszukiwane po treści, a nie po świeżości. Implementacją referencyjną według autorów jest Mem0, który obsługuje wszystkie cztery rodzaje.
Pomiar: pięć strategii pamięci na jednej rozmowie
Napisałem rozmowę o planowaniu majówki w Pradze. Użytkownik w pierwszych sześciu turach podaje fakty: mieszka w Toruniu, ma alergię na orzechy, nie lata samolotem, ma 4000 zł budżetu, pies nazywa się Fuks i waży 15 kg, żona ma urodziny 3 maja. Potem przez 18 tur pyta o zwiedzanie i koszty, a w 25. turze zmienia budżet zdaniem, w którym nie pada słowo „budżet": „Wiesz co, jednak zejdźmy z tego do 3000 zł". Po kolejnych 14 turach ostatnia dotyczy kolacji w kuchni włoskiej. Odpowiedzi asystenta generował model na pełnej historii, więc mają naturalną długość (mediana 314 tokenów). Cała rozmowa ma 39 tur i 13,2 tys. tokenów. Po każdej wiadomości użytkownika osobne wywołanie aktualizowało profil faktów (pamięć proceduralna). Dostawało obecny profil, ostatnią odpowiedź asystenta i nową wiadomość.
Następnie zadałem 7 pytań kontrolnych w pięciu konfiguracjach, każde 3 razy. Odpowiedzi oceniał ten sam model jako sędzia, z jednym kryterium na pytanie i wyjściem ograniczonym do „0" lub „1". Dla pięciu pytań sprawdziłem sędziego heurystyką słów kluczowych (3000 zł, Fuks i 15 kg, 3 maja, Toruń, kuchnia włoska): na 75 odpowiedzi nie było żadnej rozbieżności.
| Pytanie | Pełna historia | Ostatnie 8 wiadomości | Przycięcie do 2 tys. tokenów | 2 tys. + profil | 2 tys. + pamięć epizodyczna |
|---|---|---|---|---|---|
| ile mamy pieniędzy (3000 zł) | 3/3 | 0/3 | 0/3 | 3/3 | 3/3 |
| przekąski na drogę (bez orzechów) | 3/3 | 3/3 | 3/3 | 3/3 | 3/3 |
| środek transportu (bez samolotu) | 3/3 | 2/3 | 0/3 | 3/3 | 2/3 |
| ile km z domu do Pragi (z Torunia) | 3/3 | 0/3 | 0/3 | 3/3 | 0/3 |
| imię i waga psa | 3/3 | 0/3 | 0/3 | 3/3 | 3/3 |
| dzień niespodzianki (3 maja) | 2/3 | 0/3 | 0/3 | 3/3 | 3/3 |
| wybrana kuchnia (kontrola, ostatnia tura) | 3/3 | 3/3 | 3/3 | 3/3 | 3/3 |
| razem | 20/21 | 8/21 | 6/21 | 21/21 | 17/21 |
| średnio tokenów promptu | 13 373 | 1486 | 1809 | 2134 | 1943 |
Profil dorównał pełnej historii przy prompcie ponad sześć razy krótszym: 21 na 21 wobec 20 na 21. W pierwszym przebiegu oba miały po 20 na 21, więc różnicy jednej odpowiedzi nie traktuję jako przewagi. Zmianę budżetu profil zapisał poprawnie, choć wiadomość nie zawierała słowa „budżet”. Prawdopodobnie pomogło to, że ekstrakcja widziała poprzednią odpowiedź asystenta, ale wariantu bez niej nie sprawdzałem. Profil ma swój koszt: 39 wywołań ekstrakcji zużyło 26,5 tys. tokenów promptu i 8,0 tys. tokenów wyjścia. Profil puchnie też ze wszystkim. Na koniec zawierał „zainteresowania" w rodzaju browaru i kawiarni na Vinohradach, czyli wszystko, o co użytkownik zapytał, a nie tylko to, co o sobie powiedział.
Pamięć epizodyczna całkowicie poległa na jednym pytaniu (0 na 3) i raz zawiodła przy transporcie. Ta pierwsza porażka wynikała z wyszukiwania, nie z modelu. Pytanie „ile km z domu do Pragi" nie wyciągnęło wiadomości „Mieszkamy w Toruniu" do czterech najbliższych. Słowo „dom" nie jest podobne do nazwy miasta. Zmianę budżetu bez słowa „budżet" znalazła natomiast poprawnie.
Dwie rzeczy psują ten pomiar i obie są pouczające. Pierwsza: asystent powtarza fakty użytkownika we własnych odpowiedziach. W 34., 35., 37. i 39. turze przypominał o alergii na orzechy, więc pytanie o przekąski zaliczyły wszystkie strategie, także te, w których oknie użytkownik o alergii nie pisał. Druga wyszła w pierwszym przebiegu. Rozmowa kończyła się wtedy turą „Dzięki, to chyba wszystko", a asystent odpowiedział podsumowaniem z budżetem, alergią i imieniem psa. Oba okna pamięci roboczej uzyskały wtedy po 12 na 21, a nie 8 i 6. Wyniki w tabeli pochodzą z drugiego przebiegu, na tej samej historii bez tej ostatniej tury. Pamięć robocza „pamięta" więc tyle, ile asystent przypadkiem powtórzył, i trudno na tym polegać.
Rada autorów, żeby przycinać do limitu tokenów, a nie do liczby wiadomości, w tym pomiarze niczego nie zmieniła: oba okna objęły podobny kawałek rozmowy. Różnica pojawia się, gdy wiadomości mają bardzo różną długość.
Czego te pomiary nie mówią
To jeden model (Gemma 4 26B-A4B w NVFP4) na jednym serwerze vLLM 0.30.0 z jedną konfiguracją. Liczby latencji opisują tę kartę i ten limit 16 sekwencji, nie inne wdrożenie. Test degradacji trwał po 60 s na poziom. Przy 120% ruchu wynik zależy od długości testu, a dłuższego obciążenia współdzielonej stacji nie robiłem. Cache semantyczny sprawdziłem na jednym modelu embeddingów i 24 pytaniach, które sam napisałem. Rozmowa do testu pamięci jest jedna, syntetyczna i ma 39 tur. Przy temperaturze 0 serwer nie jest powtarzalny, dlatego każde pytanie w teście pamięci zadałem 3 razy. Sędzią był ten sam model co generator, z kryterium podanym wprost. Destylacji, kwantyzacji i speculative decoding nie mierzyłem.
To samo w innych książkach
Pekka Enberg w Latency opisuje mechanizm, który rozjechał oba testy degradacji. To coordinated omission: narzędzie, które czeka na odpowiedź, zanim wyśle kolejne żądanie, „sends fewer requests to the system" właśnie wtedy, gdy system zwalnia. Naprawa polega na wysyłaniu w stałych odstępach i asynchronicznym odbieraniu odpowiedzi (s. 71–73). Enberg przypisuje ten opis Gilowi Tene. U mnie taki test przy dwukrotnym przeciążeniu pokazał p95 TTFT prawie cztery razy niższy niż ruch według zegara przy przeciążeniu o 20%. Ten sam autor przytacza prawo Little'a: współbieżność to iloczyn przepustowości i latencji, a gdy praca przychodzi szybciej, niż system ją przetwarza, kolejka rośnie bez ograniczeń (s. 47–49). Tabela testu zamkniętego to pokazuje: przepustowość stoi, liczba zapytań w systemie podwaja się, a pełny czas odpowiedzi rośnie z 2,94 do 5,81 s.
Przy cache Enberg dodaje rzecz, o której autorzy GADP nie piszą. Przy 80% trafień rozkład latencji za cache'em ma „heavy tail" (s. 207–208). Wniosek dla prefix cache jest mój: średnia poprawia się z każdym trafieniem, ale ogon wyznaczają chybienia. W moim pomiarze chybienie przy 35 tys. tokenów kosztowało 2,4 s wobec 59 ms trafienia. Każdy zmienny token na początku promptu przesuwa zapytanie do tej drugiej grupy.
Ian Gorton w Foundations of Scalable Systems zawęża tezę, że bezstanowość modelu to wyłącznie kłopot. Stan trzymany w instancji zużywa pamięć proporcjonalnie do liczby klientów, a „any scalable service will need stateless APIs" (s. 101–102). Dla Pattern 28 oznacza to, że pamięć ma siedzieć w zewnętrznym magazynie. Profil z mojego pomiaru to kilkaset znaków tekstu na użytkownika, który da się trzymać w zwykłej bazie.
Nitin Borwankar w Vector Databases zaczyna rozdział o wyszukiwaniu w rozmowach od zdania, że wiadomość „That approach works well but consider the performance implications" jest „meaningless without the surrounding context" (s. 225). Pamięć epizodyczna z Pattern 28 to właśnie wyszukiwanie po pojedynczych wiadomościach. Mój pomiar trafił w ten problem z drugiej strony: pytanie użyło słowa „dom", a odpowiedź siedziała w wiadomości ze słowem „Toruń".
Kiedy nie używać — zestawienie
| Wzorzec | Nie używaj, gdy | Zamiast tego |
|---|---|---|
| Small Language Model | zadanie jest szerokie | model ogólny; destylacja dopiero pod zalogowane typy zapytań |
| Prompt Caching (serwer) | zmienna treść stoi na początku promptu | stały prefiks, dane dynamiczne na końcu |
| Prompt Caching (semantyczny) | jedno słowo zmienia odpowiedź (dzień, rodzaj karty, przed/po) | klucz kanoniczny: intencja i parametry z zamkniętej listy |
| Inference Optimization | nie hostujesz modelu sam | to zadanie dostawcy API |
| Degradation Testing | test czeka na odpowiedź przed kolejnym żądaniem | ruch według zegara, latencja od zaplanowanego wysłania |
| Long-Term Memory (working) | fakty z początku rozmowy są potrzebne do końca | profil faktów wyciągany na bieżąco |
| Long-Term Memory (episodic) | pytanie i fakt nie dzielą słownictwa | profil; wyszukiwanie z kontekstem wiadomości |
Ocena trwałości
Moja ocena, nie autorów. Small Language Model jest datowany: modele, ceny i sprzęt zmieniają się co kwartał, choć sama ścieżka „prototyp, logi, destylacja" przetrwa. Prompt Caching jest trwały jako mechanizm, a progi są datowane. Przy prefix cache w moim vLLM wynosiły 16 tokenów, a nie 1024. Pułapka cache semantycznego jest trwała, bo wynika z tego, co mierzy embedding. Inference Optimization jest datowany: continuous batching jest już domyślny, a lista technik się zmieni. Degradation Testing jest trwały i według mnie to najważniejszy wzorzec rozdziału. Teoria kolejek i coordinated omission są starsze od LLM-ów. Long-Term Memory jest trwały, bo bezstanowość wywołań i koszt długiej sekwencji wynikają z architektury, a nie z wersji API.
Starszy krewny
Prefix caching to memoizacja: ten sam początek, ten sam wynik pośredni. Cache semantyczny przypomina cache HTTP z kluczem bez nagłówka Vary, czyli jedną odpowiedź dla zapytań, które tylko wyglądają podobnie. Degradation Testing to teoria kolejek i prawo Little'a, a coordinated omission to znany błąd narzędzi do testów obciążeniowych. Pamięć długoterminowa to sesja HTTP i profil użytkownika: bezstanowy protokół, stan w zewnętrznym magazynie, a w żądaniu tylko klucz albo wyciąg.
Dalej w serii
Następny wpis (8) dotyczy rozdziału 9, czyli zabezpieczeń: Template Generation, Assembled Reformat, Self-Check i Guardrails. Mapa całej serii jest we wpisie 0.