Ciągłe grupowanie i przedwczesne wypełnianie w partiach w vLLM i SGLang
Statyczne grupy LLM marnują zasoby GPU na wypełnianie danych. Planowanie na poziomie iteracji oraz przedwczesne wypełnianie w blokach zapewniają płynny przepływ dekodowania, nawet gdy nowe polecenia są jeszcze przetwarzane.
Źródła: Continuous Batching & Chunked Prefill: How vLLM and SGLang Beat Static LLM Inference
Klasyczne grupowanie na poziomie zapytań powoduje, że duża część cykli GPU jest wykorzystywana na wypełnianie dodatkowymi danymi, gdy długość wyjść modeli językowych znacznie się różni. Ciągłe grupowanie na poziomie iteracji – upowszechnione w produkcji przez vLLM i SGLang po badaniach Orca – umożliwia dodawanie i usuwanie sekwencji na każdym kroku przetwarzania tokenów. Dzięki tym rozwiązaniom unika się zamarzania strumieni danych już w trakcie dekodowania podczas obsługi długich zapytań. Zespoły platformowe, które nadal używają statycznych pipeline’ów Hugging Face, napotykają oba te problemy, gdy ruch sieciowy łączy krótkie rozmowy z długimi procesami generowania tekstu.
1. Pułapka statycznego grupowania
Serwery do przetwarzania obrazów i tabel grupują ustalone tensorzy – szesnaście obrazów o wymiarach [16, 3, 224, 224], jedna próba przetwarzania, szesnaście odpowiedzi razem. Gdy ten model przenosi się na duże modele językowe, przepustowość spada z dwóch powodów strukturalnych:
- Nieprzewidywalna długość wyników. Jeden żądanie może zostać zakończone w kilkuset milisekundach; inne w tej samej grupie może przesyłać setki tokenów przez wiele sekund.
- Blokada z powodu najwolniejszego żądania. Statyczne grupowanie nie pozwala wcześnie zwrócić krótkiej odpowiedzi – jej miejsce pozostaje zajęte tokenami wypełniającymi, podczas gdy GPU czeka na najwolniejsze żądanie.
W statycznym grupowaniu najwolniejsze, najdłuższe żądania określają tempo dla wszystkich pozostałych. Ciągłe grupowanie traktuje grupę jak pas startowy: zakończone sekwencje odchodzą natychmiast, a te w oczekiwaniu zajmują wolne miejsca. Opłata za tokeny wypełniające to nie błąd drugorzędnej ważności – regularnie marnuje większość teoretycznych FLOPs na bezużyteczną pracę, gdy rozkłady długości są silnie prawoskrętne, co jest standardem dla asystentów do rozmów i programowania.
2. Jak działa ciągłe grupowanie (planowanie na poziomie iteracji)
Zamiast planować całe zapytanie od początku do końca, silnik planuje jedną iterację tokena:
- Krok postępu tokena. Jeden przebieg w kierunku przodu przez każdą aktywną sekwencję wytwarza dokładnie jeden nowy token.
- Nagłe usunięcie. Sekwencje, które wygenerowały
<eos>, są zwracane i uwalniane z pamięci VRAM. - Nagłe przyjęcie. Uwolniona przepustowość trafia do kolejki zapytań bez czekania na pozostałą część starej partii.
Działanie w stopniu iteracji sprawia, że rdzenie są zajmowane rzeczywistą pracą zamiast wypełnianiem zerami, co często przynosi wielokrotny wzrost wydajności przy porównywalnej medianowej latencji. Jednostka pracy planera zmienia się z „pełnego ukończenia” na „jeden krok dla każdej aktywnej sekwencji”, dlatego menedżery pamięci, takie jak paged attention, naturalnie łączą się z ciągłym grupowaniem: sloty pojawiają się i znikają bez konieczności przekształcania sztywnego, statycznego tensora.
3. Problem wypełniania początkowego i wypełnianie początkowe w partiach
Ciągłe dekodowanie ujawnia nowy konflikt pomiędzy wypełnianiem początkowym a dekodowaniem:
- Wypełnianie początkowe jest ograniczone zasobami obliczeniowymi: model przetwarza cały prompt, aby utworzyć bufor KV. Prompt składający się z tysięcy tokenów może zajmować setki milisekund intensywnej pracy z macierzami.
Naiwny planer, który wstrzymuje trzydzieści aktywnych procesów dekodowania, aby przetworzyć jeden duży prompt, powoduje przerwy w strumieniu – klasyczny problem związany z przedwczesnym uzupełnianiem danych. Przedwczesne uzupełnianie w fragmentach dzieli ten prompt na mniejsze części (na przykład po 512 tokenów) i łączy te fragmenty z krokami dekodowania, dzięki czemu praca nad promptem rozprzestrzenia się na kolejne iteracje bez gwałtownych skoków opóźnienia. Użytkownicy już streamujący nadal otrzymują tokeny; nowi użytkownicy mogą dalej przetwarzać swoje prompty. Ta sama zasada obowiązuje, gdy system RAG wtrąca długie konteksty w trakcie sesji: bez dzielenia na fragmenty jeden zbiór danych może sparaliżować dziesiątki innych procesów.
4. Porównanie wydajności i przepustowości
| Architektura obsługi |
|---|
Te zakresy są orientacyjne; dokładne wartości zależą od rozmiaru modelu, sprzętu oraz kombinacji promptów. Ranking jakościowy jest podobny we większości publicznych opisów funkcjonowania: statyczne grupowanie ustępuje pod względem wykorzystania zasobów, samo ciągłe grupowanie eliminuje marnotrawstwo podczas dekodowania, ale nie radzi sobie z zakłóceniami spowodowanymi danymi wstępnymi, natomiast podzielone dane wstępne przywracają płynny przepływ danych przy mieszanej obciążeniu.
5. Symulacja planeru na poziomie iteracji w Pythonie
Poniższy zarys pokazuje proces przyjmowania zapytań, częściowe wcześniejsze wypełnianie pamięci oraz usuwanie danych na każdym kroku. Jest to model edukacyjny, a nie środowisko produkcyjne, ale sprawia, że pętla sterująca jest wyraźna:
from dataclasses import dataclass
from typing import List
@dataclass
class LLMRequest:
request_id: str
remaining_prefill: int
tokens_to_generate: int
tokens_generated: int = 0
@property
def is_finished(self) -> bool:
return self.remaining_prefill == 0 and self.tokens_generated >= self.tokens_to_generate
class ContinuousBatchEngine:
"""
Simulates iteration-level scheduling with dynamic slot allocation
and chunked prefill budgeting.
"""
def __init__(self, max_batch_slots: int = 3, prefill_chunk_size: int = 256):
self.max_slots = max_batch_slots
self.chunk_size = prefill_chunk_size
self.waiting_queue: List[LLMRequest] = []
self.running_slots: List[LLMRequest] = []
def submit_request(self, request: LLMRequest):
self.waiting_queue.append(request)
def step_iteration(self, step_number: int):
print(f"\n--- [ITERATION STEP {step_number}] ---")
# 1. Admit waiting requests if open slots exist
while len(self.running_slots) < self.max_slots and self.waiting_queue:
new_req = self.waiting_queue.pop(0)
self.running_slots.append(new_req)
print(f" [ADMITTED] {new_req.request_id} entered running batch.")
# 2. Execute one token step across running batch
for req in self.running_slots:
if req.remaining_prefill > 0:
# Process a chunk of prefill
chunk = min(req.remaining_prefill, self.chunk_size)
req.remaining_prefill -= chunk
print(f" [PREFILL CHUNK] {req.request_id}: Processed {chunk} prompt tokens (Remaining: {req.remaining_prefill})")
else:
# Process single decode token
req.tokens_generated += 1
print(f" [DECODE TOKEN] {req.request_id}: Emitted token #{req.tokens_generated}/{req.tokens_to_generate}")
# 3. Evict finished requests immediately
finished = [r for r in self.running_slots if r.is_finished]
for req in finished:
self.running_slots.remove(req)
print(f" [COMPLETED & EVICTED] {req.request_id} returned to client.")
if __name__ == "__main__":
engine = ContinuousBatchEngine(max_batch_slots=2, prefill_chunk_size=200)
# Simulate 3 incoming requests of varying prompt & response sizes
engine.submit_request(LLMRequest("Req_A (Short)", remaining_prefill=0, tokens_to_generate=2))
engine.submit_request(LLMRequest("Req_B (Long)", remaining_prefill=0, tokens_to_generate=4))
engine.submit_request(LLMRequest("Req_C (Large Prompt)", remaining_prefill=400, tokens_to_generate=2))
# Run 5 continuous iterations
for step in range(1, 6):
engine.step_iteration(step)
6. Praktyczne wskazówki dla inżynierów platform AI
- Nie używaj prostych pipeline’ów Hugging Face jako serwerów produkcyjnych. Standardowe statyczne grupowanie zadań w
transformers.pipelinejest rozwiązaniem typowym dla laboratoriów. Modeli o otwartym kodzie należy umieszczać w środowiskach z ciągłym grupowaniem zadań, takich jak vLLM lub SGLang. - Włącz częściowe wcześniejsze wypełnianie pamięci przy mieszanych obciążeniach rozmowami. W vLLM flagi takie jak
--enable-chunked-prefillograniczają liczbę tokenów na iterację, aby długie prompty systemowe lub konteksty RAG nie utrudniały pracy użytkownikom streamującym.
Łącznie, ciągłe grupowanie danych i planowane prefill stanowią standardowe rozwiązania w nowoczesnych systemach dostarczania treści o otwartym profilu obciążenia – a nie opcjonalne ustawienia do dostosowywania po awarii statycznego projektu w produkcji.
Literatura pokrewna
- Optymalizacja inferencji LLM: prefill, dekodowanie i enterprise LLMOps — Wąskie gardła związane z prefill i dekodowaniem, ciągłe grupowanie danych, FlashAttention, kwantyzacja, PagedAttention, dekodowanie spekulatywne, prefill w formie fragmentów oraz serwisowanie rozproszone.
- Metryki inferencji vLLM: Od TTFT i TPOT do KV Cache i Goodput — Przewodnik praktyczny po metrykach vLLM w Prometheus – wskaźniki skuteczności, percentyle opóźnień, stosunek operacji wejścia/wyjścia tokenów, przerwy w działaniu, prefill w formie fragmentów, DPD oraz akceptacja dekodowania spekulatywnego.