Постійне групування та часткове попереднє заповнення у vLLM та SGLang
Статичні пакети LLM марнують ресурси GPU на додаткові дані. Розкладання операцій на рівні ітерацій та часткове попереднє заповнення забезпечують плавний потік декодування, навіть коли нові запити все ще обробляються.
Джерела: 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. Як працює безперервне пакетування (розклад на рівні ітерацій)
Замість того, щоб планувати весь процес обробки запиту від початку до кінця, двигун планує одну ітерацію токенів:
- Крок токену вперед. Одна проходження крізь кожну активну послідовність генерує рівно один новий токен.
- Миттєве видалення. Послідовності, які створили
<eos>, повертаються та звільняються у VRAM. - Миттєве прийняття. Звільнена пропускна здатність надається запиту у черзі без очікування на решту старої партії.
Робота на рівні ітерацій змушує ядра працювати над справжньою роботою, а не над заповненням нулями, що зазвичай призводить до значного підвищення продуктивності при порівнянній середній затримці. Одиниця роботи планувальника зменшується від «повного завершення» до «одного кроку для кожної активної послідовності», саме тому такі менеджери пам’яті, як paged attention pair, природно поєднуються з безперервним батчуванням: слоти з’являються та зникають без необхідності перетворення жорсткого статичного тензора.
3. Проблема попереднього заповнення та часткове попереднє заповнення
Безперервне декодування виявляє новий конфлікт між попереднім заповненням та декодуванням:
- Попереднє заповнення залежить від обчислень: модель обробляє повний запит, щоб створити кеш KV. Запит довжиною у кілька тисяч токенів може зайняти сотні мілісекунд на виконання складних матричних операцій.
Наївний планувальник, який призупиняє тридцять активних процесів декодування, щоб обробити один величезний запит, спричиняє перерви у потоці даних — класичну проблему передчасного заповнення. Часткове передчасне заповнення розділяє цей запит на частини (наприклад, по 512 токенів) та чергує ці частини з кроками декодування, щоб обробка запиту відбувалася протягом кількох ітерацій без різких підвищень затримок. Користувачі, які вже отримують дані, продовжують їх отримувати; новачки все одно роблять прогрес у обробці запиту. Та сама ідея застосовується, коли система RAG вставляє довгі контексти під час сеансу: без розділення на частини одна операція з отриманням даних може заблокувати десятки сусідніх процесів.
4. Порівняння продуктивності та пропускної здатності
| Архітектура обслуговування | Використання GPU | Затримка у кінцевій частині (p99) | Варіація між токенами |
|---|
Ці діапазони є орієнтовними; точні цифри залежать від розміру моделі, обладнання та комбінації запитів. Цей квалітативний ранг підтверджується у більшості публічних описів роботи систем: статичне групування поступається за ефективністю використання, тільки безперервне групування усуває марну обробку даних, але не вплив попереднього заповнення, а часткове попереднє заповнення забезпечує плавний потік даних при змішаному навантаженні.
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 не уповільнювали роботу користувачів.
У сукупності постійне групування даних та попереднє заповнення у частини є стандартним підходом для сучасних систем розподілу даних з відкритою структурою — а не додатковими налаштуваннями після невдачі статичного дизайну в умовах роботи.