Как я строил AI-инфраструктуру для игрового сервиса с 2M DAU
Реальный кейс: от постановки задачи до production-деплоя. Какие модели выбрали, почему отказались от облачных API и как добились задержки ответа менее 80мс на P99.
С чего всё началось
В начале 2024 года ко мне пришёл клиент — мобильный издатель с флагманской игрой, которая держит около 2M DAU. Задача казалась простой: «хотим добавить AI-ассистента, который помогает новым игрокам освоиться». На деле это оказалось задачей, которая коснулась всей backend-архитектуры.
В этой статье я разберу весь путь: от первоначального анализа до production-деплоя. Покажу, какие решения принимали и почему, включая те, от которых в итоге отказались.
Почему облачные API не подошли
Первым порывом было взять OpenAI API или Anthropic и быстро интегрировать. Мы провели нагрузочное тестирование и получили цифры, которые нас остановили:
- Средняя задержка — 340мс, P99 — 1.2с
- При пике 50k одновременных пользователей — rate limiting и очереди
- Стоимость при текущем объёме — ~$28k/месяц
- GDPR не позволял отправлять игровой контекст наружу
Последний пункт стал ключевым. Игровой контекст — прогресс игрока, история действий, инвентарь. Всё это PII в трактовке европейского регулятора.
Глоссарий
-
DAU
Daily Active Users — количество уникальных пользователей за сутки. Ключевая метрика нагрузки на backend.
-
P99 latency
99-й перцентиль задержки ответа: 99% запросов обрабатываются быстрее этого значения. Жёстче среднего, показывает реальный worst case.
-
RAG
Retrieval-Augmented Generation — подход, при котором LLM получает релевантный контекст из базы знаний перед генерацией ответа.
-
vLLM
Высокопроизводительный inference-движок для LLM с поддержкой continuous batching и PagedAttention. Де-факто стандарт для self-hosted inference.
-
Quantization
Сжатие весов модели с float32/float16 до int4/int8. Q4_K_M — формат llama.cpp: 4-битные веса, минимальная потеря качества.
-
Prefix caching
Кэширование KV-состояния общего префикса промптов (системный промпт, контекст игры). Резко снижает время до первого токена при повторяющихся запросах.
Выбор модели и inference-инфраструктура
Нам нужна была модель, которая умеет: отвечать на вопросы по контексту игры (RAG), генерировать подсказки на русском языке, работать в пределах 4k токенов контекста. При этом — помещаться на GPU, которые мы могли себе позволить.
После сравнения Mistral 7B, Llama-3 8B и Phi-3 mini остановились на Llama-3 8B в quantized версии (Q4_K_M через llama.cpp). На A10G она держала ~180 tok/s, чего нам хватало с запасом.
Inference-кластер
Развернули vLLM с continuous batching на 4×A10G. Перед ним — Ray Serve как маршрутизатор с балансировкой по queue depth. Кэш промптов через prefix caching (системный промпт с описанием игрового мира повторялся в 94% запросов).
from vllm import AsyncLLMEngine, AsyncEngineArgs
engine_args = AsyncEngineArgs(
model="meta-llama/Meta-Llama-3-8B-Instruct",
quantization="awq",
max_model_len=4096,
gpu_memory_utilization=0.90,
# Prefix caching — ключевое для нашего use-case
enable_prefix_caching=True,
# Continuous batching
max_num_seqs=256,
max_num_batched_tokens=8192,
)
engine = AsyncLLMEngine.from_engine_args(engine_args)
RAG-пайплайн: что работает в игровом контексте
Классический RAG с векторным поиском по всей базе знаний игры давал плохое качество — слишком много нерелевантных чанков попадало в контекст. Пришлось выстроить двухуровневую систему ретривала.
Уровень 1 — структурированный контекст: прогресс игрока, текущий квест, инвентарь. Это всегда присутствует в запросе и передаётся напрямую, без векторного поиска.
Уровень 2 — семантический поиск: только если вопрос выходит за рамки текущего контекста. Индекс в Qdrant (~180k чанков по игровой документации), re-ranking через cross-encoder.
Мониторинг качества ответов
Один из самых недооценённых аспектов RAG — evaluation в production. Мы настроили три уровня проверки:
- Online: thumbs up/down от игроков (13% response rate)
- Async: LLM-as-a-judge на 5% сэмпле запросов
- Offline: еженедельный прогон по golden set из 800 вопросов
Это позволило поймать регрессию при обновлении модели прежде, чем она попала бы в production.
Как устроен кластер изнутри
Четыре A10G-ноды за Ray Serve. Каждая нода — отдельный vLLM-процесс с prefix caching и continuous batching. Маршрутизатор смотрит на queue depth и шлёт запрос на наименее загруженную ноду.
При деградации одной ноды Ray Serve автоматически перераспределяет трафик на оставшиеся три. Среднее время восстановления — менее 2 секунд.
RAG pipeline: два уровня retrieval
Первый уровень — всегда в контексте: персонаж, локация, инвентарь. Передаётся напрямую без векторного поиска.
Второй уровень подключается только если вопрос выходит за рамки текущего игрового состояния. Qdrant + cross-encoder re-ranking дают precision@5 = 0.87 на нашем тестовом сете.
"Самый дорогой урок: начинать с evaluation-фреймворка, а не с самой модели. Иначе не знаешь, стало ли лучше.
— из ретроспективы проекта
Итоги и выводы
Через три месяца в production мы получили систему, которая отвечает 400k запросов в день, держит P99 latency в 80мс и стоит ~$7.2k/месяц вместо прогнозируемых $28k.
Что сработало лучше всего:
- Prefix caching дал −40% к нагрузке на inference одним конфиг-параметром
- Двухуровневый RAG поднял accuracy с 61% до 84% на golden set
- LLM-as-a-judge автоматизировал evaluation почти полностью
Что сделал бы иначе: начал бы с evaluation-фреймворка на день 1, а не строил его параллельно с системой. Это сэкономило бы 2 недели работы и один неловкий инцидент в production.
Что дальше
Смотрим на fine-tuning Llama-3 на игровых диалогах, чтобы убрать system-промпт полностью и освободить контекстное окно. Напишу отдельно, как пойдёт.
Частые вопросы
Три причины: стоимость при 2M DAU была бы в районе $80k/мес, latency облачных API на P99 не укладывалась в наш SLA 80мс, и мы не могли допустить передачу игровых данных пользователей на внешние серверы.
У нас 4×A10G (24GB VRAM каждая). Llama-3 8B в Q4_K_M занимает ~5GB, остаток идёт под KV-cache. При пиковой нагрузке ~1200 запросов/мин утилизация GPU держалась на уровне 65–75%.
LLM-as-a-judge на 5% трафика + golden set из 800 вопросов запускается еженедельно через CI. Если метрика падает — PR не проходит.
При 100k DAU скорее всего хватит одной A10G. Но при таком масштабе облачный API будет дешевле — self-hosted inference окупается примерно от 500k DAU при активном использовании AI-фич.
Источники
- vLLM: Easy, Fast, and Cheap LLM Serving GitHub
- Qdrant Vector Database Documentation qdrant.tech
- llama.cpp — LLM inference in C/C++ GitHub
- Ray Serve: Scalable Model Serving docs.ray.io
- Efficient Memory Management for Large Language Model Serving with PagedAttention arXiv 2309.06180