Dlaczego dwie karty RTX 3090 osiągają prędkość 55 tok/s zamiast 220 przy użyciu SGLang i DFlash2.
Praktyczny przegląd uruchamiania Qwen3.8-27B na dwóch karcach 3090 przy użyciu SGLang oraz wyjaśnienie, jak łącza PCIe x4, brak funkcji P2P, mała pamięć cache KV oraz nagłe skoki poboru mocy obniżyły deklarowaną prędkość.
Krótkie wideo rozeszło się wśród entuzjastów samodzielnego hostowania. Pokazuje ono Qwen3.8-27B piszące kod na dwóch kartach RTX 3090, przy prędkości generowania sięgającej 220 tokenów na sekundę. W tym układzie użyto SGLang w połączeniu z DFlash2, który jest narzędziem do spekulatywnego projektowania. Ten mały model proponuje jednocześnie siedem przyszłych tokenów, a duży model przyjmuje je lub odrzuca za jednym razem, dzięki czemu prawidłowe przewidywania nie kosztują nic.
Atrakcja ta wykracza poza zwykłą demonstrację. Przy takiej prędkości agent mógłby korzystać z lokalnie hostowanego modelu, uruchamiając kilka agentów programistycznych równocześnie na jednym urządzeniu, bez potrzeby kluczy API, bez opłat za zużycie i bez wychodzenia danych poza budynek. Serwer domowy z dwiema dodatkowymi kartami GPU klasy 3090 wydaje się idealny: dwa razy większa przepustowość pamięci zapewnia z pewnością 220 tokenów na sekundę.
Rzeczywistość nie wygląda w ten sposób. Powody są bardziej pouczające niż sam klip.
Instalacja SGLang
Każdy, kto jest przyzwyczajony do llama.cpp, gdzie jeden plik binarny i jeden plik modelu umożliwiają rozmowę w dziesięć minut, uzna SGLang za trudny do opanowania. Oto sekwencja działań na serwerze Ubuntu.
Krok 1: środowisko Pythona. SGLang ma dużą drzewo zależności, dlatego po sklonowaniu repozytorium tworzy własne wirtualne środowisko na Pythonie 3.12.
git clone https://github.com/sgl-project/sglang.git
cd sglang
python3.12 -m venv .venv
source .venv/bin/activate
Krok 2: uporczywa instalacja. Pierwsza prawdziwa próba instalacji trwała około 39 minut, wypełniając cache pakietów większością plików z PyPI, po czym została przerwana z powodu błędu.
RuntimeError: cargo is required to discover the Rust extension modules
Kompilator Rust do serwera inferencji GPU. Nowsze wersje SGLang zawierają opcjonalne rozszerzenia Rust, a narzędzia budowania odmawiają dalszej pracy bez Cargo, chociaż silnik w ogóle ich nie używa. Ustawienie jednej zmiennej środowiskowej pozwala je pominąć.
SGLANG_BUILD_RUST_EXTS=none pip install -e "./python[all]"
To drugie uruchomienie zajęło jeszcze około 30 minut, po czym zakończyło się bez problemów. Wynikiem było sglang 0.5.20, torch zbudowany dla CUDA 13, a triton automatycznie ustawiony na wersji 3.7.1, co akurat jest konieczne dla kart graficznych Ampere, aby uniknąć znanego błędu jądra.
Krok 3: modele. Dwa pliki do pobrania zostały przygotowane zgodnie z zaleceniami społeczności:
cyankiwi/Qwen3.8-27B-AWQ-INT4to główny model. Kwantyzacja INT4 zmniejsza jego rozmiar do około 20 GB w 5 fragmentach, co jest wystarczająco małe dla dwóch kart graficznych o pojemności 24 GB.z-lab/Qwen3.8-27B-DFlash2to wersja robocza, o rozmiarze około 3,9 GB.
Krok 4: uruchomienie. Wystarczy jeden polecenie, przy czym TP=2 oznacza paralelizm tensorów na obu kartach 3090.
CUDA_VISIBLE_DEVICES=1,2 python -m sglang.launch_server \
--model-path /mnt/models/Qwen3.8-27B-AWQ-INT4 \
--tp 2 \
--speculative-algorithm DFLASH \
--speculative-draft-model-path /mnt/models/Qwen3.8-27B-DFlash2 \
--speculative-num-draft-tokens 8 \
--mamba-radix-cache-strategy extra_buffer \
--disable-prefill-cuda-graph \
--cuda-graph-max-bs-decode 16 \
--mem-fraction-static 0.85 \
--context-length 65536 \
--reasoning-parser qwen3 \
--host 0.0.0.0 --port 30000
Należy uważać na dwa pułapki. Flaga o nazwie --cuda-graph-max-bs uniemożliwia uruchomienie, ponieważ w najnowszych wersjach została przemianowana na odrębne warianty do dekodowania i wypełniania wcześniej. Ponadto pominięcie --reasoning-parser qwen3 sprawia, że API zwraca surowe wyniki rozumowania modelu, zawierające nawiasy think, wewnątrz normalnego pola z treścią. Dodanie tego parsera dzieli wynik na uporządkowane pola reasoning_content i content, co jest tym samym układem używanym przez inne serwery kompatybilne z OpenAI i co będzie docenione przez każdego, kto pisze klienta.
Krok 5: czekanie. Uruchomienie po pierwszym starcie trwało około sześciu minut, aż pojawiła się wiadomość o gotowości. Nic się nie zepsuło: SGLang kompiluje jądra GPU w odpowiednim czasie za pośrednictwem Triton i rejestruje grafy CUDA na każdej karcie. Późniejsze uruchomienia wykorzystują pamięć cache i trwają około 35 sekund. Pasek postępu, który wydaje się zatrzymany podczas tego pierwszego uruchomienia, to po prostu trwająca kompilacja.
Co pokazały pomiary
Server działał poprawnie. Odpowiadał na polecenia, streamował reasoning_content i przyjmował połączenia agentów. Oto liczby zmierzone bezpośrednio na maszynie:
| Metric | Min | Max | Average |
|-------------------------|----------|-----------|-----------|
| Decode speed | 47 t/s | ~100 t/s | ~55 t/s |
| Prompt prefill | 342 t/s | 466 t/s | ~428 t/s |
| Accept length (DFlash) | 3.2 | 6.8 | ~4 |
A oto obrazek pokazujący pojemność:
| Capability | Advertised limit | Reality |
|-------------------------|---------------------------|-----------------------|
| Max context per request | 65,536 (I set it) | fits, one at a time |
| Total KV pool | ~131K tokens max | 103K at default flags |
| Parallel slots | 48 concurrent requests | 3–6 with real prompts |
To oznacza średnio około 55 tokenów na sekundę, a nie 220. Problem polega na tym, że pojedyncza karta RTX 3090 Ti z oprogramowaniem llama.cpp osiąga około 75 tokenów na sekundę przy tej samej rodzinie modeli. Dwie karty wykorzystujące paralelizm tensorowy oraz zaawansowany mechanizm generowania plików przegrywają z jedną kartą używającą starszego podejścia.
Wskaźniki akceptacji tłumaczeń wyjaśniają część tej różnicy. Gdy mechanizm DFlash trafnie przewiduje wyniki, przy akceptacji 6,8 z 8 tokenów, prędkość generowania plików tymczasowo osiąga około 100 t/s. Gdy przewidywania są błędne, prędkość spada do 47. Nawet najlepsze grupy tłumaczeń znacznie ustępują wartości 220, więc mechanizm generowania plików nie był główną przyczyną problemu. Coś innego obniżało prędkość.
Sposób na znalezienie rozwiązania w dzienniku
Podpowiedź cały czas znajdowała się w dzienniku uruchamiania:
Setup Custom allreduce failed with CUDART error: peer access is not
supported between these two devices.
W ramach paralelizmu tensorowego każda macierz wag jest dzielona pomiędzy dwa karty GPU, a częściowe wyniki muszą być łączone po każdym bloku uwagi oraz po każdym bloku MLP. W modelu o wielkości 27B oznacza to około 100 przejść tam i z powrotem na każdy wygenerowany token. Łączenie wyników w takim tempie wymaga szybkiego połączenia pomiędzy kartami, a te dwa modele nie miały takiego połączenia. Dwie oddzielne usterki wzajemnie się nasilały.
Brak połączenia typu peer-to-peer. Karty GeForce nie obsługują już oficjalnie funkcji GPU-direct P2P, więc każda transmisja musi przebiegać przez pamięć RAM systemu: z karty GPU 1 do pamięci, następnie na kartę GPU 2 i z powrotem. To podwaja liczbę kopiowania danych i potrójnia opóźnienie.
Ograniczone połączenie – prawdziwy problem. Sprawdzenie stanu fizycznego połączenia wyjaśnia sytuację:
/sys/bus/pci/devices/05:00.0/current_link_width → 4 (max 16)
/sys/bus/pci/devices/09:00.0/current_link_width → 4 (max 16)
Oba karty graficzne korzystały z interfejsu PCIe x4, a nie x16. Po cztery pasma każda, przekazywane przez mostek hosta, umożliwiają prędkość przesyłania danych wynoszącą zaledwie 1 do 2 GB/s pomiędzy kartami, jednak obciążenie przekracza tę prędkość około 100 razy na każdy token. Karty graficzne stały w bezczynności, czekając na kopiowanie danych. To tłumaczy powolną prędkość prefill na poziomie 428 t/s w porównaniu z oczekiwanymi 1,200 lub więcej, oraz spowolnienie procesu dekodowania na około 55.
Karty graficzne są szybkie. Przewód łączący je jest wolny. W modelu paralelizmu tensorowego przewód ten faktycznie staje się modelem.
Jak ktoś osiąga 220
Nikt nie pokazuje swojej płyty głównej podczas demonstracji. Dobrze połączona para kabeli x16 z działającym P2P oferuje około dziesięciokrotnie większą przepustowość między kartami niż w tym układzie. Podane liczby najprawdopodobniej wynikają z krótkiego wykonywania przewidywalnego kodu przy ciepłych buforach pamięci i idealnych warunkach testowych, a nie z ciągłej prędkości działania. Aspekt programowy tego rozwiązania jest solidny, ale w tle zakłada obecność sprzętu, którego mało które domowe komputery posiadają.
Dwa kolejne wnioski z codziennych testów
Pokój kontekstowy jest mniejszy, niż się spodziewano. Dwie karty o pojemności 24 GB wydają się wystarczające. Należy jednak odjąć wagi INT4 (około 10 GB na każdej karcie), narzędzie do tworzenia grafów, struktury CUDA oraz, ponieważ Qwen3.8 wykorzystuje hybrydową uwagę, pamięć stanu rekurencyjnego. To, co pozostało na cache KV, to łącznie około 103 tysięcy tokenów, a przy bardziej ryzykownych ustawieniach nawet 131 tysięcy tokenów. Jedna prośba o przetworzenie 65 tysięcy tokenów mieści się bez problemu. Ogłaszane 48 jednoczesnych sesji nie jest możliwe, ponieważ każda aktywna rozmowa również zajmuje część puli stanu modelu Mamba, a w praktyce mieści się tylko od trzech do sześciu sesji. W przeciwieństwie do tego, llama.cpp przeznacza niemal całą VRAM na jeden model i bez trudności radzi sobie z dużymi kontekstami.
Przepięcie związane z mocą. Gdy obie karty 3090 pracują jednocześnie, wykonywając zadania prefill i drafting przy pełnym obciążeniu tensorowym, pobór mocy gwałtownie rośnie z poziomu spoczynkowego do około 700 watów w ciągu zaledwie kilku milisekund. Zasilacze słabo radzą sobie z takimi sytuacjami. Jeden z takich skoków spowodował aktywację ochrony i wyłączenie serwera w trakcie pracy. Urządzenie uruchomiło się ponownie samo, ale lekcja jest jasna: dwie karty przy takim obciążeniu wymagają wysokiej jakości zasilacza posiadającego wystarczającą rezerwę mocy, a wartość watażu podana na pudełku nie jest wystarczająca.
Czy warto to próbować w domu?
SGLang plus DFlash2 to imponująca inżynieria, o ile spełniają się jej założenia, a te założenia stanowią listę kontrolną sprzętu:
- Oba karty graficzne w prawdziwych slotach x16. Płyty do gier dla użytkowników domowych zazwyczaj mają jedną kartę, a druga może być podłączona przez chipset w trybie x4. Przeczytaj instrukcję przed zakupem, ponieważ to decyduje o różnicy między 220 a 55.
- Funkcjonalna komunikacja między kartami graficznymi. Oznacza to wsparcie dla Resizable BAR oraz dostosowań ACS w BIOS-ie, a także odmiany sterowników stworzone przez społeczność dla GeForce lub NVLink poza środowiskiem konsumenckim. W praktyce jest to domena płyt głównych serwerowych.
- Wysokiej jakości zasilacz z zapasem mocy. Nagłe wzrosty obciążenia dwiema kartami o mocy 350 W w końcu doprowadzą do awarii słabego zasilacza.
- Cztery karty RTX 3090 do obsługi Qwen3.8-27B w pełnym kontekście. Taki budżet sprawia, że pamięć KV przestaje być ograniczeniem, w odróżnieniu od skróconej ilości pamięci 103K.
Oprogramowanie tego typu nie wybacza taniego sprzętu. Działa wolniej w miejscach, o których nikt nie pomyślał.
Mimo braku obu elementów – oprogramowania oraz sprzętu, który zakłada – llama.cpp wciąż pozostaje rozsądnym wyborem. Oznacza to jedną kartę, jeden plik binarny, prędkość około 75 tokenów na sekundę przy standardowym interfejsie PCIe, brak narzędzi typu Rust oraz brak nieplanowanych restartów. Para kart GPU może ponownie działać niezależnie z prędkością x4.
Cyfra 220 była prawdziwa. Po prostu nie odnosi się do tej konfiguracji i prawdopodobnie nie do wielu innych, dopóki nie zostanie sprawdzone połączenie między kartami.