Strona główna / Artykuły

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.

Wydawca: REACTAPP.TOP5 min czytania1194 słów

Ź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:

  1. Krok postępu tokena. Jeden przebieg w kierunku przodu przez każdą aktywną sekwencję wytwarza dokładnie jeden nowy token.
  2. Nagłe usunięcie. Sekwencje, które wygenerowały <eos>, są zwracane i uwalniane z pamięci VRAM.
  3. 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.
  • Dekodowanie jest ograniczone przepustowością pamięci: kroki obejmujące jeden token trwające kilkadziesiąt milisekund decydują o jakości interakcji.
  • 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
    Statyczne grupowanie Niskie (często poniżej 40%) z powodu wypełniaczy Niskie do momentu zmiany grupowania Tylko ciągłe grupowanie Wysokie (często powyżej 85%) przy dynamicznych slotach Srednie Gwałtowne wzrosty przy przychodzeniu dużych danych wstępnych Ciągłe grupowanie + podzielone dane wstępne Optymalne (często powyżej 90%) w stanie nasycenia Gładkie i stabilne Minimalny, przewidywalny czas na token

    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.pipeline jest 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-prefill ograniczają liczbę tokenów na iterację, aby długie prompty systemowe lub konteksty RAG nie utrudniały pracy użytkownikom streamującym.
  • Obserwuj wartości p95/p99, a nie tylko średnie. Średnia liczba tokenów na sekundę może wydawać się prawidłowa, podczas gdy planowanie prefill nadal wpływa negatywnie na poszczególne sesje. Mierz czas do pierwszego tokena oddzielnie od opóźnienia między tokenami, aby czas oczekiwania w kolejce i stan dekodowania nie łączyły się w jeden mylący wskaźnik.
  • Planuj ilość pamięci KV z myślą o planerze. Ciągłe grupowanie danych jest przydatne tylko wtedy, gdy nieukończone sekwencje mogą być bezproblemowo usunięte, a nowe mogą zostać dodane; nadmierna ilość VRAM przekształca proces przyjmowania danych w chaos.
  • Łą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