Развертывание мультимодальной рекомендательной системы на Amazon EKS с Bloom-фильтрами и Triton | AiManual
AiManual Logo Ai / Manual.
24 Май 2026 Гайд

Мультимодальные рекомендации на EKS: Bloom-фильтры, кэширование и Triton в продакшене

Пошаговый гайд по созданию production-ready мультимодальной рекомендательной системы на Amazon EKS: two-tower модель, Bloom-фильтры, кэширование признаков, Kube

Сладкая боль рекоммендеров

Вы когда-нибудь пытались скормить нейронке и текст описания товара, и картинку, и историю покупок за последний год, и ещё погоду на улице, а потом выкатить это в прод на тысячу RPS? Если да — вы знаете эту смесь восторга и желания поджечь кластер. Если нет — добро пожаловать в клуб, где один лишний эмбеддинг превращает ваш latency-бюджет в горстку пепла.

Обычные векторные базы данных (FAISS, pgvector) тут бессильны — они дают k кандидатов, но не понимают мультимодального контекста и не фильтруют уже показанное. А простой ANN-поиск без дополнительных этапов превращает рекомендации в кашу из повторяющихся товаров. Поэтому умные ребята уже давно собирают многостадийные пайплайны: генерация кандидатов -> лёгкая фильтрация -> тяжёлый реранкинг с контекстом.

Я покажу, как собрать такую систему на Amazon EKS, используя Kubeflow для обучения, Bloom-фильтры для дедупликации, Redis для кэширования признаков и Triton Inference Server для инференса. Без воды, только код и грабли.

Предупреждение: вся архитектура рассчитана на нагрузку от 10K уникальных пользователей в день. Если у вас стартап на трёх котах — не надо так. Начните с SageMaker и одного инстанса.

Архитектура: четыре этапа вместо одного

Типичная ошибка новичка — скормить все признаки в одну гигантскую модель и надеяться, что она сама разберётся. Работает, пока датасет маленький. Как только появляются сотни миллионов взаимодействий — модель начинает кодировать шум, а инференс превращается в пытку.

Вот как выглядит правильная архитектура на май 2026 года:

ЭтапЗадачаИнструмент
1. Candidate GenerationБыстрый поиск 500–1000 кандидатов из миллионаTwo-Tower ANN + FAISS
2. Bloom Filter & CacheУбрать повторы, подгрузить кэшированные эмбеддингиRedisBloom + Redis Cluster
3. Contextual RetrievalВытащить контекстные признаки (погода, время, устройство)Feature Store (Feast on EKS)
4. Heavy RankingDeep FM или трансформер с контекстом, топ-10Triton Inference Server
💡
Если хотите глубже понять, как не сгореть на этапе проектирования — посмотрите мой другой гайд: MLSD: Как не сгореть, проектируя рекомендательные системы. Там я разбираю типовые грабли, которые мы здесь пропустим.

1 Поднимаем EKS и Kubeflow

Без Kubernetes тут никуда — нам нужны горизонтальное масштабирование, управление GPU и бесшовный деплой пайплайнов. Используйте EKS 1.30 (на май 2026 это стабильная версия).

Установка Kubeflow 1.9 через официальный манифест:

# Создаём кластер с Karpenter для автоскейлинга GPU
eksctl create cluster --name recsys-cluster --region eu-west-1 \
  --nodegroup-name gpu --node-type g5.2xlarge --nodes 1 \
  --managed --spot

# Устанавливаем Kubeflow
wget https://github.com/kubeflow/manifests/archive/v1.9.0.tar.gz
tar -xzf v1.9.0.tar.gz
cd manifests-1.9.0
while IFS= read -r dir; do
  kubectl apply -k "$dir"
done < kustomize-cluster-stack.txt

Важный нюанс: не ставьте Kubeflow до настройки сетевых политик и IAM. Иначе через пару дней проснётесь с баном за подозрительный трафик от подов. Лучше сразу привяжите IRSA к сервис-аккаунтам.

2 Two-Tower модель: кандидаты без тормозов

Генерация кандидатов — это fast path. Берём две башни: одна для пользователя (все его признаки + контекст), другая для товара (текст, картинка, категория). Обучаем с контрастной потерей (Sampled Softmax) на парах (user, item).

Показываю код PyTorch с мультимодальным энкодером товара:

import torch
import torch.nn as nn
from transformers import AutoModel, ViTModel

class ItemTower(nn.Module):
    def __init__(self, text_model='bert-base-uncased', img_model='google/vit-base-patch16-384'):
        super().__init__()
        self.text_encoder = AutoModel.from_pretrained(text_model)
        self.image_encoder = ViTModel.from_pretrained(img_model)
        self.fusion = nn.Linear(768 + 768, 256)  # проекция в единое пространство

    def forward(self, text_ids, text_mask, image_pixels):
        text_emb = self.text_encoder(text_ids, attention_mask=text_mask).pooler_output
        img_emb = self.image_encoder(image_pixels).pooler_output
        combined = torch.cat([text_emb, img_emb], dim=-1)
        return self.fusion(combined)

class UserTower(nn.Module):
    def __init__(self, num_features=64):
        super().__init__()
        self.mlp = nn.Sequential(
            nn.Linear(num_features, 128),
            nn.ReLU(),
            nn.Linear(128, 256)
        )

    def forward(self, x):
        return self.mlp(x)

После обучения сохраняем эмбеддинги товаров в FAISS (индекс IVF_PQ для скорости) и деплоим башни в Triton. Зачем Triton? Он умеет батчить запросы и разворачивать несколько версий модели — идеально для A/B тестов.

Кстати, если вы думаете, что контрастное обучение — это серебряная пуля, почитайте фреймворк оценки AI-агентов от Amazon. Там показано, как правильно мерить качество рекомендаций без сдвига.

3 Bloom-фильтры: как не показывать одно и то же дважды

Классический подход — хранить в Redis список ID просмотренных товаров для каждого пользователя. Но когда пользователь посмотрел 10 000 товаров, доставать и проверять список — O(N) и убивает latency. Тут на сцену выходят Bloom-фильтры.

Bloom-фильтр — это вероятностная структура данных, которая с вероятностью ~1% скажет «возможно, был» (ложноположительное срабатывание) и никогда не ошибается в обратную сторону. Выделяете для каждого пользователя фильтр размером, скажем, 10 Кбит — он занимает 10 КБ и проверяется за O(k) хешей.

Устанавливаем RedisBloom модуль на кластер Redis:

helm repo add redislabs https://redislabs.github.io/redisbloom
helm install redis-bloom redislabs/redisbloom --set cluster.enabled=true

Теперь при каждом просмотре товара добавляем ID в Bloom-фильтр пользователя. На этапе кандидатов проверяем: если фильтр говорит «нет» — товар точно новый. Если «да» — с вероятностью 99% показывали, пропускаем. Ошибочное исключение 1% кандидатов дешевле, чем повторная демонстрация.

Ошибка, которую я видел раз сто: используют один Bloom-фильтр на все товары. Не делайте так. Фильтр должен быть per user и динамически расширяться через Scaling Bloom Filters, иначе через месяц ложно-положительные срабатывания съедят половину кандидатов.

4 Feature Caching: куда сохранять эмбеддинги и контекст

Вычисление эмбеддинга товара на лету при каждом запросе — убить latency. Правильно: гоняем батч-джоб на Kubeflow, который раз в 6 часов пересчитывает эмбеддинги для всех активных товаров и складывает их в Redis. Пользовательские эмбеддинги считать сложнее — они зависят от последних действий. Тут помогает Feast с online store (Redis).

Пример конфигурации feature view для товара:

feature_view:
  - name: item_embeddings
    ttl: 3600  # час жизни
    entities: [item_id]
    features:
      - name: embedding
        dtype: FLOAT_LIST
    batch_source:
      path: s3://recsys/embeddings/items/
      timestamp_field: created_at

При запросе рекомендаций мы сначала дёргаем Redis за кэшем пользователя и товаров. Если кэш пуст (холодный старт) — используем fallback на предвычисленные средние. Проблема холодного старта очень подробно разобрана в статье как Ozon отказался от ANN в пользу Query Prediction. Там показано, что иногда лучше вообще не использовать эмбеддинги для новых пользователей, а предсказывать запрос.

5 Contextual Ranking на Triton

После того как мы отфильтровали кандидатов через Bloom и подгрузили фичи, остаётся 50–200 товаров. Теперь запускаем тяжёлую ranking-модель — DeepFM или небольшой трансформер. Эта модель получает на вход конкатенацию: user_embedding, item_embeddings, контекст (время суток, день недели, тип устройства, гео).

Деплоим модель в Triton с поддержкой динамического батчинга:

tritonserver --model-repository=/models \
  --backend-config=python,shm-default-byte-size=67108864

Чтобы обеспечить SLA под 50 мс, ставим Horizontal Pod Autoscaler по CPU и GPU памяти. Когда нагрузка скачет (например, чёрная пятница), Karpenter добавляет ноды с инстансами p4d за пару минут.

Однажды я забыл включить --cuda-memory-pool-size и Triton упал OOM на первом же батче из 1024 запросов. Не повторяйте.

Сводим всё в пайплайн Kubeflow

Всё перечисленное — не отдельные микросервисы, а этапы одного пайплайна. Kubeflow позволяет оркестровать обучение two-tower, обновление FAISS индекса, пересчёт эмбеддингов, проверку качества (AUC, MRR) и деплой новой версии в Triton. Пример DAG:

from kfp import dsl

def recsys_pipeline(image: str):
    train = dsl.ContainerOp(name='train-two-tower', image=image).set_gpu_limit(1)
    push_index = dsl.ContainerOp(name='build-faiss', image=image).after(train)
    deploy_triton = dsl.ContainerOp(name='deploy-to-triton', image=image).after(push_index)
    dsl.Condition(deploy_triton.success, 'production').after(deploy_triton)

Если хотите подробностей про CI/CD для ML на EKS — гляньте гайд по автоматизации сбора доказательств для аудита на Bedrock. Там похожие принципы, только для compliance.

Шесть граблей, на которые наступит каждый

  1. Размер Bloom-фильтра: 10 Кбит на пользователя — мало для активной аудитории. Формула: m = - (n ln p) / (ln 2)^2. Если хотите p=1% при n=10000, берите 95 Кбит.
  2. Кэш без TTL: Эмбеддинги устаревают — товары раскупают, пользователи меняют интересы. Ставьте TTL 1 час для user_embedding и 6 часов для item.
  3. Размер батча в Triton: Не делайте батч больше 128, если модели не Quark-квантованы. Latency скачет до 200 мс.
  4. Не учитывать положение пользователя: Если у вас гео-зависимые рекомендации (ресторан, доставка), обычный ANN не поможет. Нужен Multi-Stage Retrieval с геофильтром.
  5. Отсутствие мониторинга дрейфа: Контекст меняется — сегодня жара, завтра холод. Система должна пересчитывать эмбеддинги не по расписанию, а по триггерам (Feature Drift Detector).
  6. Забыть про cold start для товаров: Новый товар без эмбеддинга. Решение — Content-based фильтрация через CLIP или Sentence-BERT. Тема разобрана в гайде по виртуальной примерке на Nova Canvas и OpenSearch.

Хватит на сегодня — запускайте

Вся описанная архитектура работает на EKS уже у нескольких клиентов, с которыми я работал. Не верьте, что рекомендательные системы — это магия. Это инженерная задача: правильно разложить на стадии, прикрутить кэши и фильтры, не забыть про контекст. Bloom-фильтры, Redis, Triton — всё это проверено годами. Но главное — никогда не пытайтесь объединить всё в одну модель. Доверьтесь многостадийному пайплайну, и ваши 100 мс latency станут реальностью.

Кстати, скоро мы увидим, как рекомендательные системы превратятся в агентные системы — они сами будут собирать контекст с веб-страниц, API погоды и даже из почты пользователя. Amazon уже делает такие прототипы на Bedrock. Если интересно — почитайте архитектуру Agentic AI на Amazon Bedrock. Тот же принцип, только агенты вместо башен.

А теперь идите и настройте свой EKS. Код из статьи можно взять за основу — я специально показал самые важные куски, а не копипасту из документации. Если наступите на грабли, напишите в комментариях, я подскажу.

Подписаться на канал