vLLM, czyli model lokalny dla więcej niż jednej osoby
Narzędzia do uruchamiania modeli na własnym komputerze świetnie sprawdzają się przy pracy jednoosobowej. Problem pojawia się, gdy z tego samego modelu ma korzystać stu użytkowników naraz: przepustowość spada, żądania czekają w kolejce, a karta graficzna stoi bezczynnie między jednym a drugim.
vLLM rozwiązuje dokładnie ten problem. Jest serwerem wnioskowania zbudowanym pod obsługę wielu żądań równolegle, a różnica wobec rozwiązań jednoosobowych to nie procenty, tylko wielokrotności.
Dwa pomysły, na których to stoi
Warto zrozumieć, skąd bierze się ta różnica, bo tłumaczy to też ograniczenia.
Pierwszy pomysł to grupowanie ciągłe. Klasyczne podejście zbiera żądania w partię, przetwarza je razem i czeka, aż wszystkie skończą, zanim przyjmie kolejne. Ponieważ odpowiedzi mają różną długość, karta czeka na najdłuższą, a reszta miejsca stoi pusta.
Grupowanie ciągłe dokłada nowe żądanie w chwili, gdy któreś się skończy, bez czekania na resztę. Karta pracuje wtedy stale, a czas oczekiwania na wynik spada, bo nikt nie stoi w kolejce za żądaniem, które akurat generuje długą odpowiedź.
Drugi pomysł to stronicowana uwaga. Pamięć podręczna kontekstu, rosnąca z każdym wygenerowanym tokenem, jest przydzielana blokami zamiast ciągłym obszarem, dokładnie tak jak pamięć wirtualna w systemie operacyjnym. Klasyczne podejście rezerwuje miejsce na maksymalną długość odpowiedzi z góry, więc marnuje większość, a to ograniczenie decyduje o tym, ile żądań mieści się naraz.
Efekt obu jest zbieżny: więcej równoległych rozmów na tej samej karcie i mniej czasu spędzonego na czekaniu.
Uruchomienie
pip install vllmvllm serve mistralai/Mistral-Small-Instruct-2501 \
--max-model-len 16384 \
--gpu-memory-utilization 0.90Serwer wystawia interfejs zgodny z popularnym formatem, więc kod pisany pod dostawcę chmurowego działa po zmianie adresu.
from openai import OpenAI
klient = OpenAI(base_url="http://localhost:8000/v1", api_key="lokalnie")
odpowiedz = klient.chat.completions.create(
model="mistralai/Mistral-Small-Instruct-2501",
messages=[{"role": "user", "content": "Sklasyfikuj to zgloszenie: ..."}],
)Ta zgodność ma znaczenie praktyczne przy migracji. Aplikację działającą na modelu chmurowym przenosisz na własny sprzęt bez zmian w logice, co pozwala porównać oba warianty na tych samych danych.
Dwa parametry z polecenia decydują o zachowaniu i warto je rozumieć od początku. Pierwszy ogranicza długość kontekstu, a jego zmniejszenie zwalnia pamięć na większą liczbę równoległych żądań. Drugi określa, jaką część pamięci karty serwer może zająć.
Dobór pamięci
To najczęstsza przyczyna nieudanego pierwszego uruchomienia i warto policzyć to przed uruchomieniem, a nie po komunikacie o braku pamięci.
Pamięć karty dzieli się na trzy części. Pierwszą zajmują wagi modelu, w rozmiarze zależnym od liczby parametrów i precyzji. Drugą pamięć podręczna kontekstu, rosnąca z liczbą równoległych żądań i długością kontekstu. Trzecią narzut samego serwera.
| Kwantyzacja | Pamięć na miliard parametrów |
|---|---|
| Pełna precyzja | około 2 GB |
| Ośmiobitowa | około 1 GB |
| Czterobitowa | około 0,5 GB |
Model o siedmiu miliardach parametrów w kwantyzacji ośmiobitowej zajmuje więc około siedmiu gigabajtów, a reszta pamięci karty idzie na kontekst. Przy karcie dwudziestoczterogigabajtowej zostaje kilkanaście gigabajtów, co przekłada się na kilkadziesiąt równoległych rozmów przy umiarkowanej długości kontekstu.
Praktyczna zasada brzmi: zmniejszenie maksymalnej długości kontekstu jest najtańszym sposobem na zwiększenie liczby równoległych żądań. Ustawienie stu dwudziestu ośmiu tysięcy tokenów, gdy Twoje zapytania mają cztery tysiące, marnuje pamięć, której nikt nie użyje.
Kwantyzacja i kompromisy
Zmniejszenie precyzji wag to najprostszy sposób zmieszczenia większego modelu na tej samej karcie, a serwer obsługuje kilka formatów.
Wariant ośmiobitowy jest bezpiecznym punktem wyjścia. Strata jakości jest praktycznie niemierzalna w typowych zadaniach, a zysk pamięciowy dwukrotny wobec pełnej precyzji.
Wariant czterobitowy daje kolejne dwukrotne oszczędności i zauważalną, choć zwykle akceptowalną stratę. Warto ją zmierzyć na własnych przypadkach, bo różnice zależą od zadania: przy klasyfikacji są niewielkie, przy zadaniach wymagających precyzyjnego rozumowania większe.
Trzecim mechanizmem jest kwantyzacja samej pamięci podręcznej kontekstu. Przy długich rozmowach to ona zajmuje więcej niż wagi, więc zmniejszenie jej precyzji podnosi liczbę równoległych żądań bardziej niż zmiana formatu wag.
Praktyczna kolejność wygląda tak. Zacznij od wariantu ośmiobitowego i zmierz przepustowość. Jeśli nie wystarcza, zmniejsz maksymalną długość kontekstu, bo to nic nie kosztuje jakościowo. Dopiero potem sięgaj po niższą precyzję i sprawdzaj wynik na swoim zestawie przypadków.
Wdrożenie produkcyjne
Serwer uruchomiony poleceniem w terminalu nadaje się do testów. Wdrożenie obsługujące ruch wymaga kilku rzeczy więcej.
Pierwszą jest kontener. Obraz z serwerem i modelem pobranym z góry usuwa najdłuższy element uruchomienia, czyli ściąganie wag przy każdym starcie. Przy modelu ważącym kilkanaście gigabajtów to różnica między minutą a kwadransem.
Drugą jest uwierzytelnianie. Domyślnie serwer nie wymaga niczego, ale ma wbudowaną opcję --api-key oraz odpowiadającą jej zmienną środowiskową VLLM_API_KEY. Po ich podaniu przyjmuje wyłącznie żądania z jednym z tych kluczy w nagłówku. To mechanizm prosty: bez ról, bez limitów per klucz i bez rozliczania zużycia, więc przy wdrożeniu z wieloma odbiorcami punkt końcowy i tak warto postawić za odwrotnym pośrednikiem albo trzymać w sieci prywatnej niedostępnej z zewnątrz.
Trzecią jest kolejka przed serwerem. Przy nagłym skoku ruchu lepiej, żeby żądania czekały w kolejce, niż żeby serwer je odrzucał, bo odrzucone żądanie oznacza błąd widoczny dla użytkownika.
Czwartą jest plan na awarię karty. Jedna instancja to jeden punkt awarii, więc przy wdrożeniu, od którego coś zależy, warto mieć drugą albo przepięcie na interfejs dostawcy jako rozwiązanie zapasowe. Warstwa kierująca ruchem, choćby prosta trasa w Hono, pozwala przełączyć to bez zmian w aplikacji.
Co mierzyć
Trzy liczby wystarczą, żeby wiedzieć, czy wdrożenie działa poprawnie, i warto zbierać je od pierwszego dnia.
Czas do pierwszego tokenu mówi o odczuciu użytkownika. Przy strumieniowaniu to on decyduje, czy interfejs wydaje się szybki, niezależnie od tego, jak długa jest cała odpowiedź.
Liczba tokenów na sekundę w skali całego serwera mówi o przepustowości. Ta wartość rośnie wraz z liczbą równoległych żądań, aż do wysycenia karty, i to właśnie ten wzrost jest przewagą tego rozwiązania.
Zajętość pamięci karty mówi, jak blisko jesteś ściany. Serwer, który zaczyna odrzucać żądania, robi to zwykle bez ostrzeżenia, więc próg alarmowy warto ustawić wcześniej.
Warto testować przy obciążeniu odpowiadającym rzeczywistości. Pomiar przy jednym żądaniu naraz pokazuje najgorszy wynik tego rozwiązania, bo cała jego przewaga leży w obsłudze wielu naraz.
Buforowanie prefiksu
Mechanizm, który przy niektórych zastosowaniach daje więcej niż wszystkie pozostałe optymalizacje razem, a bywa pomijany, bo działa tylko w konkretnym układzie.
Jeśli wiele żądań zaczyna się od tego samego fragmentu, na przykład od długiej instrukcji systemowej albo od tej samej dokumentacji, serwer potrafi policzyć ten fragment raz i użyć wyniku dla kolejnych żądań.
Zysk jest proporcjonalny do udziału wspólnej części. Instrukcja systemowa na dwa tysiące tokenów przy zapytaniach na dwieście tokenów oznacza, że dziewięćdziesiąt procent pracy przy każdym żądaniu jest powtórzeniem, a buforowanie usuwa to powtórzenie.
Warunek jest jeden i taki sam jak przy buforowaniu u dostawców chmurowych: wspólna część musi być na początku i identyczna co do bajta. Data wstawiona w instrukcję systemową albo identyfikator sesji na początku unieważniają całość, a nic o tym nie informuje.
W obecnych wydaniach nie trzeba tego włączać, bo buforowanie prefiksu jest domyślnie aktywne. Cała praca leży więc po stronie układu zapytania, a nie konfiguracji serwera, i to jest dobra wiadomość, bo zmiana kolejności fragmentów promptu kosztuje mniej niż jakakolwiek zmiana w konfiguracji.
Przy aplikacji odpowiadającej na pytania o tę samą dokumentację warto ułożyć prompt tak, żeby dokumentacja szła przed pytaniem. To zmiana kolejności dwóch fragmentów, a przepustowość potrafi wzrosnąć kilkukrotnie.
Dobór modelu do zadania
Największa oszczędność przy własnym wdrożeniu nie leży w konfiguracji serwera, tylko w wyborze modelu odpowiedniego do zadania.
Klasyfikacja zgłoszeń, tagowanie i wydobywanie danych ze struktury to zadania, w których model trzymiliardowy wypada niewiele gorzej od dwudziestomiliardowego, a mieści się na karcie kilkukrotnie mniejszej i obsługuje wielokrotnie więcej żądań naraz.
Odwrotnie przy zadaniach wymagających rozumowania albo pracy z kodem, gdzie różnica między rozmiarami jest odczuwalna i mniejszy model po prostu nie wystarcza.
Praktyczne podejście polega na rozdzieleniu ruchu. Jedna instancja z modelem mniejszym obsługuje zadania masowe, druga z większym te trudne, a warstwa kierująca decyduje, dokąd idzie żądanie. Przy dużym wolumenie ten podział daje więcej niż jakakolwiek optymalizacja pojedynczej instancji.
Warto to zmierzyć na własnych przypadkach, zanim kupisz sprzęt. Trzydzieści prawdziwych zapytań przepuszczonych przez dwa modele mówi, czy mniejszy wystarczy, a to przekłada się wprost na wymagany sprzęt i rachunek.
vLLM kontra alternatywy
| Rozwiązanie | Mocna strona | Słabość | Kiedy wybrać |
|---|---|---|---|
| vLLM | Przepustowość przy wielu żądaniach, zgodny interfejs | Wymaga karty i konfiguracji | Wdrożenie produkcyjne z modelem własnym |
| Ollama | Prostota, działa na laptopie | Słaba przy wielu żądaniach naraz | Praca jednoosobowa, prototyp |
| API dostawcy | Zero utrzymania, najlepsze modele | Koszt za token, dane wychodzą na zewnątrz | Niski wolumen, brak wymogów lokalności |
| Wynajem modelu z wagami | Bez utrzymania sprzętu, niezależność | Koszt za token, dane u dostawcy | Średni wolumen, potrzeba modelu otwartego |
Wybór między pierwszymi dwoma wierszami sprowadza się do liczby jednoczesnych użytkowników. Przy jednej osobie prostsze narzędzie wystarcza i oszczędza konfiguracji. Przy dziesięciu osobach naraz różnica w przepustowości staje się odczuwalna, a przy stu jest rozstrzygająca.
Trzeci wiersz warto liczyć uczciwie. Karta graficzna kosztuje tyle samo w dzień bez ruchu, co w szczycie, więc przy kilkuset wywołaniach dziennie interfejs dostawcy zwykle wychodzi taniej. Próg opłacalności własnego sprzętu leży zwykle w okolicach kilku tysięcy wywołań dziennie, przy czym zależy to od długości zapytań i od tego, czy sprzęt już masz.
Kiedy to nie jest właściwy wybór
Przy pracy jednoosobowej narzut konfiguracyjny nie zwraca się. Model na laptopie uruchomiony prostszym narzędziem daje ten sam wynik przy zerowej pracy wdrożeniowej.
Przy niskim i nierównomiernym wolumenie własny sprzęt stoi bezczynnie przez większość doby, a rachunek jest ten sam. Interfejs dostawcy rozliczany za tokeny dopasowuje się wtedy do ruchu.
Przy wymaganiu najwyższej jakości odpowiedzi modele otwarte, które uruchomisz na jednej karcie, nadal ustępują czołówce zamkniętej przy trudnym rozumowaniu. To argument za układem mieszanym: zadania masowe lokalnie, trudne przez interfejs dostawcy.
Typowe błędy
Pierwszy to pozostawienie domyślnej długości kontekstu. Model deklarujący sto dwadzieścia osiem tysięcy tokenów zarezerwuje pod to pamięć, przez co liczba równoległych żądań spadnie kilkukrotnie.
Drugi to planowanie pamięci według liczby parametrów aktywnych przy architekturze mieszanki ekspertów. Do pamięci wchodzi całość, więc szybkość generowania nie zmniejsza wymagań.
Trzeci to pomiar wydajności przy jednym żądaniu. To najgorszy scenariusz dla tego rozwiązania i wynik nie mówi nic o zachowaniu przy prawdziwym ruchu.
Czwarty to brak limitu na długość odpowiedzi. Jedno żądanie generujące bez końca zajmuje miejsce w partii i obniża przepustowość dla wszystkich.
Piąty to wystawienie serwera bez uwierzytelniania. Punkt końcowy dostępny z sieci bez klucza to karta graficzna pracująca dla kogoś innego na Twój rachunek za prąd.
Szósty to brak monitorowania zajętości pamięci. Serwer odrzucający żądania robi to bez ostrzeżenia, a przyczyna bywa szukana w aplikacji zamiast w wysyceniu karty.
FAQ
Czym vLLM różni się od Ollamy?
Ollama jest zbudowana pod pracę jednoosobową i wygodę uruchomienia, vLLM pod obsługę wielu żądań naraz. Przy jednym użytkowniku różnica jest niewielka, przy kilkudziesięciu naraz przepustowość vLLM jest wielokrotnie wyższa.
Jaki sprzęt jest potrzebny?
Karta z pamięcią mieszczącą wagi modelu plus zapas na kontekst. Model siedmiomiliardowy w kwantyzacji ośmiobitowej mieści się na karcie dwudziestoczterogigabajtowej z miejscem na kilkadziesiąt równoległych rozmów. Modele większe wymagają karty serwerowej albo kilku kart naraz.
Czy interfejs jest zgodny z popularnym formatem?
Tak, dzięki czemu aplikację napisaną pod dostawcę chmurowego przenosisz zmieniając adres i nazwę modelu. To pozwala porównać oba warianty na tych samych danych, bez przepisywania logiki.
Kiedy własny sprzęt wychodzi taniej?
Przy wysokim i stałym wolumenie. Karta kosztuje tyle samo niezależnie od ruchu, więc próg opłacalności leży zwykle w okolicach kilku tysięcy wywołań dziennie. Przy mniejszym ruchu rozliczenie za tokeny wygrywa.
Czy da się obsłużyć kilka modeli naraz?
Na jednej karcie zwykle nie, bo pamięć jest wspólna, a wagi zajmują ją na stałe. Praktyczne rozwiązania to osobne instancje na osobnych kartach albo jeden model ogólny obsługujący wszystkie zadania, przy czym drugie bywa tańsze i wystarczające.
Dokumentacja stoi na stronie projektu, a omówienie mechanizmów wydajnościowych znajdziesz w przewodniku po przepustowości.