Головна / Статті

Постійне групування та часткове попереднє заповнення у vLLM та SGLang

Статичні пакети LLM марнують ресурси GPU на додаткові дані. Розкладання операцій на рівні ітерацій та часткове попереднє заповнення забезпечують плавний потік декодування, навіть коли нові запити все ще обробляються.

Видавець: REACTAPP.TOP5 хв читання1194 слів

Джерела: Continuous Batching & Chunked Prefill: How vLLM and SGLang Beat Static LLM Inference

Класичне батчування на рівні запитів залишає значну кількість циклів GPU для заповнювачів даних, коли довжина результатів роботи мовних моделей сильно варіюється. Постійне батчування на рівні ітерацій — яке стало популярним у продакшені завдяки vLLM та SGLang після досліджень Orca — дозволяє включати та вилучати послідовності на кожному кроці обробки токенів. Метод часткового попереднього заповнення запобігає тому, щоб великі запити зупиняли процес декодування, який вже триває. Команди платформ, які все ще використовують статичні пайплайни Hugging Face, стикаються з обома проблемами, як тільки трафік поєднує короткі розмови з довгими результатами генерації тексту.

1. Пастка статичного батчування

Сервери для обробки зображень та таблиць батчують фіксовані тензори — шістнадцять зображень формату [16, 3, 224, 224], один прохід моделі та шістнадцять відповідей разом. Якщо застосувати цю схему до великих мовних моделей, пропускна здатність значно знижується через дві структурні причини:

  • Непередбачувана довжина результатів. Одна заява може бути оброблена за кілька сотень мілісекунд; інша в тому ж пакеті може передавати сотні токенів протягом багатьох секунд.
  • Блокування через найповільнішу заяву. У статичних пакетах коротка відповідь не може бути надана раніше — її місце зайняте токенами-доповненнями, поки GPU чекає на найповільнішу заяву.

У статичному пакетуванні найповільніша, найдовша заява визначає темп для всіх інших. У безперервному пакетуванні пакет розглядається як злітно-посадкова смуга: оброблені послідовності відразу залишають його, а ті, що чекають, займають вільні місця. Використання токенів-доповнень — це не незначна неефективність: при сильно витягнутих розподілах довжин, що є стандартом для чат- та кодувальних асистентів, це регулярно марнує більшу частину теоретичних FLOPs на безпотрібну роботу.

2. Як працює безперервне пакетування (розклад на рівні ітерацій)

Замість того, щоб планувати весь процес обробки запиту від початку до кінця, двигун планує одну ітерацію токенів:

  1. Крок токену вперед. Одна проходження крізь кожну активну послідовність генерує рівно один новий токен.
  2. Миттєве видалення. Послідовності, які створили <eos>, повертаються та звільняються у VRAM.
  3. Миттєве прийняття. Звільнена пропускна здатність надається запиту у черзі без очікування на решту старої партії.

Робота на рівні ітерацій змушує ядра працювати над справжньою роботою, а не над заповненням нулями, що зазвичай призводить до значного підвищення продуктивності при порівнянній середній затримці. Одиниця роботи планувальника зменшується від «повного завершення» до «одного кроку для кожної активної послідовності», саме тому такі менеджери пам’яті, як paged attention pair, природно поєднуються з безперервним батчуванням: слоти з’являються та зникають без необхідності перетворення жорсткого статичного тензора.

3. Проблема попереднього заповнення та часткове попереднє заповнення

Безперервне декодування виявляє новий конфлікт між попереднім заповненням та декодуванням:

  • Попереднє заповнення залежить від обчислень: модель обробляє повний запит, щоб створити кеш KV. Запит довжиною у кілька тисяч токенів може зайняти сотні мілісекунд на виконання складних матричних операцій.
  • Декодування обмежене пропускною здатністю пам’яті: кроки по одному токену, які тривають десятки мілісекунд, визначають рівень інтерактивності.
  • Наївний планувальник, який призупиняє тридцять активних процесів декодування, щоб обробити один величезний запит, спричиняє перерви у потоці даних — класичну проблему передчасного заповнення. Часткове передчасне заповнення розділяє цей запит на частини (наприклад, по 512 токенів) та чергує ці частини з кроками декодування, щоб обробка запиту відбувалася протягом кількох ітерацій без різких підвищень затримок. Користувачі, які вже отримують дані, продовжують їх отримувати; новачки все одно роблять прогрес у обробці запиту. Та сама ідея застосовується, коли система RAG вставляє довгі контексти під час сеансу: без розділення на частини одна операція з отриманням даних може заблокувати десятки сусідніх процесів.

    4. Порівняння продуктивності та пропускної здатності

    Архітектура обслуговування Використання GPU Затримка у кінцевій частині (p99) Варіація між токенами
    Статичне групування Низьке (часто менше 40%) через додатковий простір Високе — блокується довгими послідовностями Низьке до заміни груп Тільки безперервне групування Високе (часто понад 85%) з динамічними слотами Середнє Підвищення при надходженні великих попередніх даних Безперервне групування + часткове попереднє заповнення Оптимальне (часто понад 90%) насичене Плавне та стабільне Мінімальний, прогнозований час на токен

    Ці діапазони є орієнтовними; точні цифри залежать від розміру моделі, обладнання та комбінації запитів. Цей квалітативний ранг підтверджується у більшості публічних описів роботи систем: статичне групування поступається за ефективністю використання, тільки безперервне групування усуває марну обробку даних, але не вплив попереднього заповнення, а часткове попереднє заповнення забезпечує плавний потік даних при змішаному навантаженні.

    5. Симуляція планувальника на рівні ітерацій у Python

    Наведений нижче скетч демонструє процес прийому даних, часткове попереднє заповнення та видалення елементів на кожному кроці. Це навчальна модель, а не середовище для роботи в продакшені, проте вона чітко ілюструє логіку контрольного циклу:

    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. Практичні рекомендації для інженерів платформ AI

    • Не використовуйте наївні пайплайни Hugging Face як сервери для роботи в продакшені. Стандартне статичне групування даних у transformers.pipeline є патерном для лабораторних умов. Обгорніть моделі з відкритим кодом у середовища з безперервним групуванням даних, такі як vLLM чи SGLang.
    • Увімкніть часткове попереднє заповнення для змішаних навантажень у чаті. У vLLM флаги на кшталт --enable-chunked-prefill обмежують кількість токенів за ітерацією, щоб довгі системні запити чи контексти RAG не уповільнювали роботу користувачів.
  • Слідкуйте за показниками p95/p99, а не лише за середніми значеннями. Середня кількість токенів на секунду може здаватися нормальною, тоді як механізм попереднього заповнення все ще спричиняє нестабільність окремих сесій. Вимірюйте час до отримання першого токену окремо від затримки між токенами, щоб час очікування в черзі та стан декодування не зливалися в одне оманливе число.
  • Розраховуйте обсяг пам’яті KV з урахуванням механізму планування. Постійне групування даних корисне лише тоді, коли незавершені послідовності можуть бути легко видалені, а нові — додані; надмірне використання VRAM перетворює процес обробки на хаотичний.
  • У сукупності постійне групування даних та попереднє заповнення у частини є стандартним підходом для сучасних систем розподілу даних з відкритою структурою — а не додатковими налаштуваннями після невдачі статичного дизайну в умовах роботи.