Kontekst 262K na jednej RTX 4090: co dodałem do llama.cpp-turboq
Silnik już trzymał 4,125-bitowy cache KV wewnątrz flash attention. Żeby agenci kodujący mogli na nim pracować cały dzień, potrzebne były asynchroniczne checkpointy, mniej przechwyceń grafu CUDA, seria poprawek MTP i strojenie samplera warte +31%.
Moja lokalna maszyna do inferencji to jedna RTX 4090 z 24 GiB VRAM. Cały dzień serwuje moim harnessom agentowym model 27B z kontekstem 262 144 tokenów. Ta notatka jest o tym, ile pracy wymagało, żeby to stało się nudne.
Konfiguracja
- GPU: jedna RTX 4090, 24 GiB, wszystkie warstwy na karcie.
- Model: Qwopus3.6-27B-v2-MTP, dostrojony Qwen3.6 27B z głowicami predykcji wielu tokenów, Q4_K_M.
- Kontekst: 262 144 tokeny, z 4-bitowym cache KV w TBQ4.
Serwer produkcyjny działa z:
--temp 0.6 --top-p 0.95 --min-p 0.05 # koniec pętli wywołań narzędzi, dekodowanie 62 → 82 tok/s
--reasoning on --reasoning-budget 8192
--jinja
--spec-type mtp --spec-draft-n-max 3 # dekodowanie spekulatywne MTP
--spec-adapt # adaptacyjna głębokość szkicu
-ctk tbq4_0 -ctv tbq4_0 # KV 4,125 bpv, 4,1 GiB przy 262K
--flash-attn on
-c 262144 -ngl 99
Od czego zacząłem
Moje repozytorium to fork Indras-Mirror/llama.cpp-turboq-mtp, który sam jest forkiem llama.cpp. Najważniejsza funkcja to ich praca, nie moja: jądro flash attention, które czyta surowe bloki TBQ4 prosto z pamięci globalnej i dekwantyzuje je w wewnętrznej pętli. TBQ4 zapisuje 128 wartości w 66 bajtach: 2 bajty skali plus 64 bajty 4-bitowych indeksów, czyli 4,125 bita na wartość. Atencja działa w obróconej dziedzinie (Hadamarda), więc nie ma osobnego przebiegu dekwantyzacji ani bufora F16.
To dzięki temu jądru kontekst w ogóle się mieści. Przy 262K tokenów cache TBQ4 zajmuje około 4,1 GiB. Cache w FP16 zająłby 16 GiB, czyli dwie trzecie karty, zanim załadujesz choć jedną wagę.
Nie napisałem więc tego jądra. Moja praca zaczyna się tam, gdzie kończy się benchmark: kiedy agent kodujący godzinami obciąża serwer długimi promptami, wywołaniami narzędzi i kontekstem bliskim limitu.
1. Checkpointy blokowały prefill
Serwer zapisuje checkpointy promptu w trakcie prefillu, żeby kolejne zapytanie mogło użyć cache zamiast przetwarzać cały prompt od nowa. Przy prefillu 262K oznacza to 32 odczyty stanu po około 149 MiB. Każdy robił pełne ctx->synchronize(), a potem blokujące ggml_backend_tensor_get dla każdego tensora stanu. Strumień obliczeń stał, a host kopiował pamięć.
Przerobiłem to na zapis asynchroniczny:
create_checkpoint (pętla prefillu, bez blokowania)
└─ asynchroniczne D2H do przypiętego slotu (cudaStreamPerThread)
└─ cudaLaunchHostFunc: slot → zwykła pamięć, zwolnienie slotu
kolejne zapytanie (GPU bezczynne)
└─ jeśli kopia jeszcze trwa: cudaStreamSynchronize // tanio, GPU stoi
└─ przywrócenie checkpointu
Bufor ma dwa sloty, więc przypięta pamięć hosta nigdy nie przekracza około 300 MiB, niezależnie od liczby checkpointów. Przypięcie wszystkich 32 wymagałoby około 4,7 GB, a ten host nie ma tyle wolnej pamięci. Dane są synchronizowane dopiero wtedy, gdy checkpoint jest faktycznie używany, a wtedy GPU i tak stoi.
2. Mniej przechwyceń grafu CUDA
Upstream dopełnia cache KV do sztywnych 256. Dodałem konfigurowalne minimum, LLAMA_KV_PAD_MIN, domyślnie 4096. To 16× mniej ponownych przechwyceń grafu CUDA niż w upstreamie.
3. Agenci w pętli i +31% z samplera
Pod obciążeniem agentowym model wpadał w pętle wywołań narzędzi. Zatrzymały je trzy zmiany:
- Sampler:
--temp 0.6 --top-p 0.95 --min-p 0.05zatrzymało pętle i podniosło dekodowanie z 62 do 82 tok/s, czyli o 31%. - Stochastyczny sampler szkicu: sampler szkicu MTP nie jest już zachłanny.
- Adaptacyjny bezpiecznik: bezpiecznik jednej próbki łapie pętle, które mimo to się zaczną.
--spec-adapt ogranicza głębokość szkicu MTP, gdy akceptacja spada poniżej 35%, żeby trudny fragment tekstu nie palił obliczeń na szkice, które i tak zostaną odrzucone.
82 tok/s to szczytowe dekodowanie przy stabilnym generowaniu. Nie na takich ustawieniach pracowałbym cały dzień.
4. Poprawność w długich sesjach
Żadna z tych rzeczy nie wychodzi w krótkim benchmarku. Wychodzą w długich sesjach agentowych:
- Kontekst cienia MTP: poprawiona synchronizacja z głównym kontekstem i wyścig danych między nim a asynchronicznymi checkpointami.
- Dryf stanu rekurencyjnego: poprawiony dryf w Gated DeltaNet przy dużym kontekście, który kończył się martwą pętlą.
- Wyjście poza zakres: poprawiony crash tensora MTP, a pełny kontekst jest teraz obsługiwany łagodnie zamiast awarią.
- Wycofywanie przy M-RoPE: wycofywanie odrzuconych szkiców MTP działa dla modeli takich jak Qwen3.8-27B, ze wzmocnioną ścieżką 1D.
- Regresja pętli: oczekujący bufor MTP synchronizuje się ponownie przy każdej zmianie pamięci modelu docelowego.
- Szybkość prefillu: połączone jądro chunk pipeline dla prefillu DeltaNet.
5. Serwowanie dla harnessów
Harness po drugiej stronie oczekuje API zgodnego z OpenAI i nie obchodzi go, co jest pod spodem, więc serwer musi się zachowywać:
- Podmiana modelu: modele podmieniają się w tym samym procesie przez rejestr presetów, bez osobnego routera.
- Reasoning effort:
reasoning_effortprzyjmuje słowo albo surowy budżet tokenów, a zapytanie nadpisuje konfigurację serwera. - Przycinanie promptu: przycinanie uwzględnia wiadomości już na etapie parsowania i nigdy nie usuwa ostatniej.
6. Ten sam cache na Apple silicon
Przeniosłem obsługę cache KV TBQ3 i TBQ4 na Metal, poprawiając przy okazji błąd pakowania 3-bitowego. Dodałem też dekwantyzację w obróconej dziedzinie dla atencji, więc ścieżka Metal również obywa się bez osobnego przebiegu dekwantyzacji.
Liczby
| Co | Wartość |
|---|---|
| Kontekst | 262 144 tokeny na 24 GiB |
| Cache KV | TBQ4, 4,125 bpv, 4,1 GiB przy 262K |
| Dekodowanie, szczyt przy stabilnym generowaniu | 82 tok/s |
| Strojenie samplera | 62 → 82 tok/s (+31%) |
| Ponowne przechwycenia grafu CUDA | 16× mniej |
| Przypięta pamięć na checkpointy | ≤ ~300 MiB zamiast ~4,7 GB |
Co dalej
Akceptację MTP ogranicza teraz głowica szkicu, a nie silnik. Następna dźwignia jest po stronie modelu: ponowne wytrenowanie głowicy MTP z celem FastMTP. Opis jest w repozytorium.
Kod, commit po commicie: github.com/jtrefon/llama.cpp-turboq-mtp.