Проблематика масштабирования инференса
Инференс моделей машинного обучения в продакшене часто сталкивается с двумя противоречивыми требованиями: низкая латентность при обслуживании запросов в реальном времени и высокая пропускная способность при пакетной обработке больших объёмов данных. Платформа Databricks, построенная на Apache Spark, предоставляет гибкие возможности для управления ресурсами, однако эффективность инференса сильно зависит от выбранной архитектуры кластера и стратегии распределения нагрузки. Основные варианты — жидкие (liquid) кластеры с динамическим автоскейлингом и разделённые (partitioned) кластеры, где ресурсы фиксированы и распределяются по предопределённым партициям. Кроме того, в некоторых сценариях используется «соление» запросов (salted) — добавление случайного префикса к ключу партиционирования, что помогает избежать «скотча» (skew) данных.
Жидкие кластеры (Liquid) — динамическое масштабирование
Автоскейлинг и адаптивные автосессии
Жидкие кластеры поддерживают автоматическое масштабирование как в количестве исполнителей (executors), так и в их типе (CPU‑ vs GPU‑инстансы). При росте количества входящих запросов Spark Scheduler добавляет новые executor‑ы, а при падении нагрузки — освобождает их, минимизируя простои и затраты. В Databricks автоскейлинг реализуется через параметры min_workers, max_workers и autoscale в настройках кластера.
Плюсы для инференса
- Гибкость: мгновенное реагирование на всплески запросов без необходимости ручного вмешательства.
- Экономия: платёж только за реально использованные ресурсы, что особенно важно при переменной нагрузке.
- Поддержка GPU: при необходимости можно автоматически переключаться на GPU‑инстансы, ускоряя расчёт тяжёлых нейронных сетей.
Ограничения
- Задержка масштабирования: запуск новых executor‑ов занимает несколько секунд, что может увеличить 99‑й процентиль латентности.
- Непредсказуемость стоимости: при частом переключении между CPU и GPU стоимость может резко возрасти.
Разделённые кластеры (Partitioned) — статическое распределение
Фиксированная топология и партиционирование
В разделённых кластерах количество executor‑ов и их тип задаются один раз при создании кластера. Данные распределяются по партициям (например, по колонке user_id), а каждый executor обслуживает свою часть данных. Такой подход часто используется в сценариях батч‑инференса, когда необходимо обработать миллионы записей за один проход.
Плюсы для инференса
- Предсказуемая производительность: отсутствие автоскейлинга устраняет волатильность латентности.
- Оптимизированные планы выполнения: Spark может заранее построить план, учитывающий фиксированное распределение, что повышает эффективность кэша и broadcast‑переменных.
- Контроль затрат: фиксированная конфигурация упрощает бюджетирование и мониторинг расходов.
Ограничения
- Неэффективность при пиковых нагрузках: если запросов становится больше, чем рассчитано, система будет перегружена, а латентность возрастёт.
- Необходимость ручного балансирования: администратору требуется регулярно переоценивать количество партиций и размер кластера.
Соление запросов: зачем и когда
Проблема «скотча» (data skew)
При партиционировании по колонке с высокой кардинальностью (например, country = 'US' для большинства записей) часть executor‑ов получает диспропорционально большую нагрузку. Это приводит к узким местам в пайплайне инференса, когда одни задачи ждут завершения «тяжёлой» задачи.
Техника «соления»
Соление (salting) — добавление к ключу партиционирования случайного префикса, например hash(user_id) % 10. Таким образом, оригинальная «тяжёлая» партиция разбивается на несколько более мелких, распределённых равномерно между executor‑ами. В Spark это реализуется через withColumn("salted_key", concat(lit(rand()), col("user_id"))) и последующее repartition("salted_key").
Когда использовать
- Батч‑инференс с большими объёмами однородных запросов (например, предсказание рейтингов для всех пользователей).
- Онлайн‑инференс, где отдельные high‑frequency запросы могут приводить к скотчу в кэше модели.
Практические рекомендации по выбору стратегии
| Сценарий | Кластер | Партиционирование | Соление |
|---|---|---|---|
| Онлайн‑инференс с переменной нагрузкой | Жидкий, автоскейлинг, GPU при необходимости | Партиционирование по session_id (низкая кардинальность) | Не требуется, если запросы распределены равномерно |
| Периодический батч‑инференс (ежедневный) | Разделённый, фиксированный размер, CPU/GPU в зависимости от модели | Партиционирование по user_id или entity_id | Рекомендуется, если наблюдается скотч по популярным пользователям |
| Гибридный поток (микросервисы + nightly batch) | Гибрид: базовый жидкий кластер + отдельный разделённый для батчей | Комбинация: онлайн‑партиционирование по request_id, батч‑по date | Соление только для батч‑части, где наблюдается диспропорция |
Тюнинг Spark‑параметров
spark.sql.shuffle.partitions: для жидких кластеров ставьте значение, близкое к количеству executor‑ов; для разделённых — кратное количеству партиций.spark.executor.memoryиspark.driver.memory: учитывайте размер модели (например, 4 GB для BERT‑large) и размер батч‑входов.spark.sql.autoBroadcastJoinThreshold: если модель хранится в небольшом файле (меньше 10 MB), её можно broadcast‑ить, чтобы избежать shuffle при каждом запросе.
Оптимизация затрат и производительности
- Кэширование модели: загрузите модель один раз в драйвер и broadcast‑ите её всем executor‑ам (
spark.sparkContext.broadcast(model)). Это устраняет повторные загрузки с S3/ADLS. - Пакетная обработка запросов: даже в онлайн‑режиме собирайте запросы в небольшие батчи (например, 10‑20 записей) и выполняйте инференс единовременно, уменьшая количество вызовов к модели.
- Контроль уровня параллелизма: используйте
mapPartitionsвместоforeachдля снижения накладных расходов на сериализацию. - Мониторинг и алерты: настройте метрики
executorCpuTime,executorRunTimeиshuffleReadBytesв Databricks Jobs UI, чтобы быстро выявлять узкие места. - Автоматическое переключение: реализуйте скрипт, который при превышении порога нагрузки (например, > 300 req/s) автоматически пересоздаёт кластер в жидком режиме с GPU, а при падении нагрузки — возвращает в разделённый CPU‑класс.
Сочетание правильного типа кластера, грамотного партиционирования и, при необходимости, соления запросов позволяет достичь минимальной латентности и максимальной пропускной способности инференса в Databricks, одновременно контролируя затраты. Выбор между жидким и разделённым подходом должен базироваться на характере нагрузки, частоте обновления модели и требованиях к стоимости, а применение техники соления — на уровне дисбаланса данных в партициях. Эти практики формируют основу надёжного и масштабируемого ML‑инференса в современных аналитических платформах.