Прямой ответ
Batch-инференс — прогон модели по большому набору записей: классификация тикетов, разметка отзывов, извлечение полей из документов, генерация описаний. От интерактивного сценария он отличается требованиями: важны пропускная способность, устойчивость к сбоям и воспроизводимость, а не время ответа на один запрос.
Принципы, без которых пакетный прогон превращается в проблему:
- Идемпотентность и чекпойнты. Процесс обязан уметь прерваться и продолжить, не переделывая готовое. Каждая запись имеет стабильный идентификатор, результат сохраняется вместе с ним.
- Ограниченный параллелизм. Не «чем больше, тем лучше»: параллелизм ограничен возможностями сервера и лимитами API. Избыточный параллелизм даёт таймауты и ретраи, а не ускорение.
- Обработка ошибок по записи, а не по всей партии. Одна плохая запись не должна ронять весь прогон: ошибки собираются отдельно, прогон продолжается.
- Разделение «получить ответ» и «принять ответ». Ответ модели проходит валидацию формата и бизнес-проверки; невалидные результаты уходят на повтор с изменёнными параметрами или в ручную обработку.
Архитектура простого надёжного прогона:
- источник записей (таблица, файл, очередь);
- таблица результатов с уникальным ключом по записи и версией промпта;
- воркер с ограниченным параллелизмом и ретраями;
- отчёт о прогоне: всего, успешно, ошибки, повторные.
Практика: рабочий каркас
Схема хранения результатов:
CREATE TABLE llm_batch_results (
item_id TEXT NOT NULL,
prompt_ver TEXT NOT NULL,
model_id TEXT NOT NULL,
status TEXT NOT NULL, -- ok | invalid | error
payload JSONB,
error TEXT,
attempts INT NOT NULL DEFAULT 0,
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (item_id, prompt_ver, model_id)
);
Ключ из (item_id, prompt_ver, model_id) делает повторный запуск безопасным: при смене промпта или модели результаты не перезаписываются, а добавляются — можно сравнивать версии.
Воркер с ограниченным параллелизмом:
import asyncio, json
async def worker(sem, session, item, prompt_ver):
async with sem:
for attempt in range(3):
try:
raw = await call_llm(session, build_prompt(item, prompt_ver))
data = json.loads(raw)
if validate(data):
return save(item["id"], prompt_ver, "ok", data)
return save(item["id"], prompt_ver, "invalid", None, "schema")
except Exception as e:
if attempt == 2:
return save(item["id"], prompt_ver, "error", None, str(e)[:200])
await asyncio.sleep(2 ** attempt)
async def main(items, prompt_ver, concurrency=8):
sem = asyncio.Semaphore(concurrency)
async with aiohttp.ClientSession() as s:
await asyncio.gather(*(worker(sem, s, i, prompt_ver) for i in items))
Рекомендации:
- Пропускайте уже обработанное. Перед запуском выбирайте только те
item_id, для которых нет результата с текущимиprompt_verиmodel_id. Это превращает любой сбой в «продолжить с места остановки». - Ретраи с экспоненциальной задержкой. Механика и ограничения — в статье про retry и backoff.
- Считайте стоимость до прогона. Оцените среднее число токенов на запись и умножьте на объём; для локального сервера — время. Планирование ресурсов — в статье про capacity planning.
- Выбирайте движок под объём. Для десятков тысяч записей на GPU эффективнее движок с непрерывным батчингом — см. vLLM; для небольших объёмов достаточно локального сервера.
- Проверяйте качество на выборке. Автоматическая валидация формата не означает правильности содержания: вручную просмотрите 30–50 результатов и зафиксируйте долю приемлемых.
Как планировать длительность прогона. Оцените среднее время одного запроса при выбранном параллелизме и умножьте на объём, добавив 20–30 % на ретраи и деградацию под нагрузкой. Если расчётное время превышает приемлемое окно, увеличивайте параллелизм ступенями и измеряйте: рост конкуренции часто снижает среднюю скорость запроса, поэтому выигрыш оказывается меньше ожидаемого.
Практический сценарий: прогон 40 000 записей упал на 60 %
Контекст: классификация обращений; через три часа работы процесс завершился по таймауту соединения, часть результатов потеряна, повторный запуск начал всё сначала.
- Причины. Результаты буферизовались в памяти и писались в конце; параллелизм 64 вызывал массовые таймауты; ретраи отсутствовали; идемпотентности не было.
- Исправления. (а) Запись результата сразу после получения, в ту же транзакцию, что и отметка о попытке; (б) параллелизм снижен до 8 с постепенным ростом под контролем метрик сервера; (в) добавлены ретраи с экспоненциальной задержкой и отдельный учёт «безнадёжных» записей; (г) запуск выбирает только необработанные записи.
- Повторный прогон. Прерывание на 12 000 записей больше не приводит к потере: следующий запуск продолжает с 12 001-й. Общее время сократилось за счёт отсутствия ретраев-лавин.
- Контроль качества. На случайной выборке 50 записей доля корректных классификаций 94 %; ошибки разбираются отдельно и попадают в датасет для улучшения промпта — подход к оценкам описан в статье про eval.
- Профилактика. Прогон оформлен как задача оркестратора с метрикой прогресса и алертом на рост доли ошибок; гарантии доставки и повторной обработки — в статье про надёжность очередей.
Вывод: пакетный инференс — это инженерная задача про устойчивость и наблюдаемость. Качество модели вторично, если прогон нельзя безопасно перезапустить.
FAQ
Какой параллелизм считать правильным? Тот, при котором растёт пропускная способность и не растут ошибки. Практика: начинайте с малого, повышайте ступенями и следите за долей таймаутов и заполнением очереди на сервере. Оптимум обычно заметно ниже «максимально возможного».
Как обрабатывать записи, которые модель стабильно портит? Выделяйте их в отдельную категорию и не мучайте ретраями: после двух-трёх попыток запись уходит в ручную обработку или в поток с другой моделью. Бесконечные повторы тратят бюджет и не улучшают результат.
Нужно ли кэшировать ответы модели? Да, если одни и те же входы встречаются повторно. Ключ кэша — хеш от нормализованного входа, версии промпта и идентификатора модели с параметрами генерации; без параметров в ключе кэш отдаст несопоставимые результаты.
Можно ли использовать пакетный инференс для потоковых данных? Да, как микропакеты по расписанию. Для событий, требующих немедленной реакции, нужен отдельный потоковый контур; смешивать их в одном процессе не стоит из-за разных требований к задержке и устойчивости.
Источники: vLLM docs, llama.cpp, Python asyncio docs — ссылки в шапке.