Сколько миллисекунд ваша модель реально перемножает матрицы, а сколько — просто ждёт, пока инфраструктура пережуёт толстый JSON? Если вы смотрите только на бенчмарки самой нейросети в вакууме, ваш realtime-streaming-ml уже мёртв.
Когда очередная команда хвастается, что latency инференса нейросети у них составляет 15 миллисекунд на одну картинку, я прошу открыть графики балансировщика. Там внезапно всплывает 120 мс на p99. Разница между этими цифрами — суровая реальность продакшена. Сеть, сериализация, очереди, копирование тензоров. Всё это убивает реалтайм-аналитику задолго до того, как данные коснутся весов модели.
Анатомия задержки: где умирает latency инференса нейросети
Препарируем классический HTTP-запрос к микросервису компьютерного зрения. Берём кадр 1080p, который нужно прогнать через детектор объектов.
- Сетевой хоп. Клиент делает POST-запрос. Если соединение не держится открытым (нет keep-alive), вы оплачиваете TCP-хэндшейк и установку TLS-сессии. В хорошей сети это 10-15 мс потерянного времени.
- Сериализация. Перегонять изображение в base64, оборачивать в текстовый JSON и парсить всё это на сервере — преступление против high-load. Кодирование в base64 раздувает payload на 33%. Трёхмегабайтная картинка превращается в 4 МБ текста. Парсинг такого JSON в Python заставляет сборщик мусора сходить с ума, аллоцируя сотни мелких объектов, прежде чем собрать из них непрерывный numpy-массив. Это съедает 10-20 мс и блокирует поток выполнения.
- Копирование на GPU (Host-to-Device). Данные приехали в оперативную память сервера. Теперь их нужно перебросить в видеопамять через шину PCIe. Это ещё 2-5 мс, в зависимости от загрузки шины.
- Очередь и планировщик. Сервер ждёт, пока соберётся пачка запросов или освободится контекст исполнения. Искусственная задержка добавляет 10-15 мс.
- Форвард-пасс. Сама модель отрабатывает на GPU за смешные 8 мс.
Итог пайплайна: нейросеть трудилась 8 миллисекунд, а клиент прождал ответа почти 60 мс. КПД системы — 13%. Маркетинг вендоров всегда продаёт скорость голого форвард-пасса, но в проде вы платите за инфраструктурный клей.
Перцентили, которые не врут: бюджеты на реалтайм
В высоконагруженных системах нет понятия «в среднем». Средняя задержка (p50) — это температура по больнице, скрывающая предсмертные хрипы архитектуры под пиковой нагрузкой. Измерять инференс имеет смысл только по хвостам: p95 и p99.
Если вы строите транзакционный антифрод, ваш хард-лимит составляет 50 мс на весь цикл. Из них на ML-скоринг остаётся от силы 15 мс, остальное съедает маршрутизация и запросы к базе данных профилей. Не успели ответить — транзакция либо уходит без проверки, пропуская фрод, либо шлюз отбрасывает легитимного клиента по таймауту.
Аналитика видеопотока жёстче: 30 кадров в секунду требуют обработки за 33 мс на кадр. Не уложились в бюджет — кадры начинают дропаться, трекинг объектов теряет контекст, система забывает цель.
Голосовые боты и речевая аналитика живут в бюджете 100-150 мс на ответ. Выпали за этот предел — человек на другом конце провода начнёт перебивать робота, алгоритмы VAD сойдут с ума, перекрывая потоки, и диалог превратится в кашу.
Если ваш p50 равен 20 мс, а p99 улетает за 300 мс из-за пауз сборщика мусора (Stop-The-World) или сетевого джиттера, система неработоспособна. Бизнесу плевать, насколько быстро обрабатывается «средний» запрос, если 1% пользователей получает отказ в обслуживании.
Ошибки бенчмаркинга: как мы врём сами себе
Прежде чем срезать жир, нужно научиться его измерять. Самая частая ошибка при профилировании инференса — замер через обычный таймер вокруг вызова модели в коде. Вызовы CUDA-ядер асинхронны. Когда интерпретатор перешагивает строчку с вызовом модели, GPU ещё даже не начал считать тензоры. Вы измерили скорость отправки задачи в очередь драйвера, а не сам инференс. Пока вы не синхронизируете поток, ваши 2 мс — это иллюзия. Настоящий форвард-пасс покажет суровые 15 мс.
Срезаем жир: от gRPC до zero-copy
Первое правило реалтайм-инференса: никакого REST для ML-сервисов во внутреннем контуре. Заменяем текстовый JSON на бинарный Protobuf поверх gRPC. Выкидываем base64. Передача сырых байт-массивов напрямую в тензоры срезает до 30% накладных расходов сети и процессора. Обязательное использование keep-alive соединений убирает штрафы за рукопожатия протоколов.
Далее включаем динамический батчинг (dynamic batching). Отправлять запросы в GPU поштучно — значит жечь ресурсы вхолостую. Допустим, обработка одного запроса занимает 8 мс. Если 8 клиентов пришлют запросы одновременно без батчинга, они выстроятся в очередь. Последний клиент прождёт 64 мс только до начала вычислений.
Батчинг меняет математику. Сервер открывает окно в 2-3 мс, собирает микроочередь из независимых запросов и отправляет их в GPU одним куском. Форвард-пасс для батча из 8 элементов займёт 12 мс вместо 8. В итоге последний клиент получает ответ через 15 мс вместо 64 мс. Здесь кроется фундаментальный трейд-офф: мы осознанно немного ухудшаем p50 (быстрые запросы ждут соседей по окну), но радикально повышаем throughput и срезаем хвост p99 под пиковой нагрузкой.
Если препроцессинг (например, декодинг потока) и ML-инференс физически живут на одной ноде, пересылать пиксели даже через gRPC по локалхосту — безумие. Здесь включается zero-copy и разделяемая память. Мы кладём декодированный тензор в оперативную память один раз и передаём модели только указатель на эту область.
import tritonclient.grpc as grpcclient
import tritonclient.utils.shared_memory as shm
# Инициализируем клиента и выделяем регион разделяемой памяти
client = grpcclient.InferenceServerClient(url="localhost:8001")
shm_handle = shm.create_shared_memory_region("frame_data", byte_size, 0)
# Копируем тензор в память один раз (на стороне препроцессинга)
shm.set_shared_memory_region(shm_handle, [input_frame_array])
client.register_system_shared_memory("frame_input", "/frame_data", byte_size)
# Вызов инференса идёт по указателю: zero network payload
inputs[0].set_shared_memory("frame_input", byte_size)
result = client.infer(model_name="detector", inputs=inputs)В этом сценарии Host-to-Device копирование всё ещё происходит внутри инференс-сервера (из RAM в VRAM), но мы полностью исключили сетевой стек и повторную аллокацию памяти из уравнения. Задержка межпроцессного взаимодействия падает с миллисекунд до десятков микросекунд.
Edge-инференс: когда физику не обмануть
Иногда сетевой хоп до дата-центра физически невозможно ужать в требуемые 30 мс. Трансатлантический пинг или нестабильная сотовая вышка ломают любую облачную архитектуру. Выход один — тащить инференс к источнику данных. Это edge-вычисления.
Размещение инференса на промышленных контроллерах прямо на конвейере завода или в NVR-регистраторе решает главную проблему: данные больше не покидают периметр. Сетевая latency исчезает как класс. Облачный оверхед помножен на ноль. Но edge приносит свои свинцовые трейд-оффы.
Железо на местах всегда слабее серверных карт. Если в дата-центре у вас стоит мощный ускоритель, то на краю это будет энергоэффективный NPU. Чтобы уложиться в бюджет времени на слабом кремнии, тяжёлые модели придётся безжалостно квантовать до INT8 или компилировать под конкретную архитектуру через TensorRT. Сам инференс на edge-устройстве может занимать 15 мс вместо облачных 5 мс, но за счёт отсутствия сети общая задержка пайплайна падает с 80 мс до 16 мс. Это победа.
Но за скорость инженеры платят кровью. Обновление весов, мониторинг температуры (когда троттлинг от перегрева убивает тайминги) и сбор метрик на сотнях разрозненных железок превращаются в логистический ад. Edge-архитектура выбирается не ради хайпа, а от безысходности, когда законы физики становятся непреодолимым барьером.
Архитектура high-load систем — это всегда война за микросекунды за пределами самой модели. Исследователи могут бесконечно вылизывать архитектуру трансформера, но пока бэкенд ждёт сборщика мусора после парсинга JSON, а гигабайты тензоров гоняются по HTTP, система будет тормозить. Оптимизируйте транспорт. Настройте батчинг. Прекратите мерить скорость тепличными скриптами. Продакшен таких иллюзий не прощает.