Решения

Рекомендательные системы на GPU: DLRM и двухбашенные модели

Задача страницы. Практический гид по построению рекомендательных систем на GPU: DLRM (Dense+Sparse, большие эмбеддинги), двухбашенные модели (two‑tower) для быстрого кандидатообразования, негатив‑семплинг, оффлайн‑батчи и онлайн‑сервинг с низкой латентностью. Дадим пайплайны (retrieval→ranking→rerank/diversity), sizing по профилям 24/48/80 ГБ, конфиги (Docker/K8s/YAML), метрики (CTR/AUC/NDCG/latency), экономику и траблшутинг. Предполагается локальный NVMe‑кэш, режимы On‑Demand (онлайн‑рекомендации) и Interruptible (оффлайн‑тренинг/ингест), контейнеризация и наблюдаемость.

TL;DR

Сценарии (когда это нужно)

  • E‑commerce/маркетплейсы: персонифицированные ленты, блок «Похожие», up‑/cross‑sell.
  • Контент/медиа: рекомендации видео/музыки/статей; гибридные сигналы (контент+коллаборатив).
  • Финтех/реклама: next‑best‑offer, bid‑рекомендации, ограничение частоты.
  • B2B/SaaS: рекомендации функций/тем/документации, персональные дайджесты.
  • Поиск по каталогу: двухбашенный retrieval (ANN) для быстрых кандидатов, ранжирование DLRM.

Архитектуры и пайплайны

1) Онлайн‑сервинг (retrieval → ranking → rerank)

				
					Request(user_id, ctx) 
  └─> Feature Builder (real-time)
       ├─> User tower embed(u)              # two-tower
       ├─> ANN Search on Item Index (GPU)   # top-N candidates
       └─> Feature Join (candidates + ctx)
            └─> DLRM Ranker (dense + sparse + crosses)
                 └─> Rerank (diversity/business rules)
                      └─> Top-K + Response (JSON)
				
			

Особенности: кэш user‑эмбеддингов в HBM, шардированный ANN‑индекс на GPU, быстрый join фичей, p95 latency контролируется размером кандидатов/фичей/MLP.

2) Оффлайн‑тренинг (батчи, негатив‑семплинг)

				
					Logs/Events ─┬─> ETL → Feature Store (NVMe)
             ├─> Negative Sampling (in-batch/hard)
             └─> Trainer (FSDP/ZeRO, AMP)
                  ├─> Two-tower (user/item)
                  └─> DLRM (sparse tables + MLP)
                       └─> Checkpoint/Export
                            ├─> Item Tower → ANN Index Build (GPU)
                            └─> Ranker Weights → Serving
				
			

3) Обновление каталога/индекса (репликация без простоя)

				
					New Item Embeddings ─> Shadow ANN Index ─> Warmup/QA ─> Swap alias ─> Traffic shift
				
			

Профили GPU и ориентиры (онлайн‑сервинг)

Оценка по количеству одновременных онлайн‑рекомендаций при U≈0.7, ANN на GPU, K=200 кандидатов, ранкер DLRM среднего размера. Для оффлайна используйте формулы в «Экономике».

Профиль GPU

Память

Типовой стек

Real‑time RPS*

Комментарии

24 ГБ (Compact)

24 ГБ

Two‑tower retrieval + лёгкий DLRM

100–300

Малая глубина фичей, компактные эмбеддинги (32–64).

48 ГБ (Balanced)

48 ГБ

Two‑tower + средний DLRM

300–700

Баланс latency/качества, стабильный p95.

80 ГБ (HQ)

80 ГБ

Крупный рангер + расширенные фичи

700–1500

Глубокий MLP, много фичей, аггрегаты по сессии.

* Диапазон зависит от числа кандидатов, размера эмбеддингов/фичей, глубины MLP и I/O.

Вместимость on‑device кэша эмбеддингов (инференс, FP16, бюджет 75% HBM)

Профиль

dim=64

dim=128

24 ГБ

≈ 151 млн

≈ 75 млн

48 ГБ

≈ 302 млн

≈ 151 млн

80 ГБ

≈ 503 млн

≈ 252 млн

Для тренинга учитывайте умножители памяти (оптимизатор/градиенты/активации), см. «Экономика».

Конфиги и скелеты кода

Docker Compose (онлайн‑retrieval + DLRM‑ranker)

				
					version: "3.9"

x-env: &env
  MODELS_DIR: /models
  CACHE_DIR: /var/cache/recsys
  PRECISION: "fp16"        # bf16|fp16
  TOPK: "200"
  RERANK_K: "50"
  ANN_INDEX_PATH: "/index/items"
  USER_CACHE_HOT: "true"

services:
  recsys-serving:
    image: cloudcompute/recsys-serving:latest
    environment: *env
    deploy:
      resources:
        reservations:
          devices: [{ capabilities: ["gpu"] }]
    ports: ["9000:9000"]
    volumes:
      - /nvme/models:/models
      - /nvme/recsys-cache:/var/cache/recsys
      - /nvme/index:/index
    command: ["python", "serve.py", "--host=0.0.0.0", "--port=9000"]


				
			

Docker Compose (оффлайн‑тренинг two‑tower + DLRM)

				
					version: "3.9"

services:
  recsys-train:
    image: cloudcompute/recsys-train:latest
    environment:
      DATA_DIR: /data
      CHECKPOINT_DIR: /checkpoints
      PRECISION: fp16
      NEGATIVE_SAMPLING: "in_batch"  # in_batch|hard|mixed
      BATCH_SIZE: "65536"
      FSDP: "true"
      LR: "3e-4"
    deploy:
      resources:
        reservations:
          devices: [{ capabilities: ["gpu"] }]
    volumes:
      - /mnt/logs:/data
      - /nvme/ckpt:/checkpoints
    command: ["python", "train.py"]


				
			

K8s (онлайн‑сервинг, 1 GPU/под)

				
					apiVersion: apps/v1
kind: Deployment
metadata:
  name: recsys-serving
spec:
  replicas: 3
  selector: { matchLabels: { app: recsys-serving } }
  template:
    metadata: { labels: { app: recsys-serving } }
    spec:
      containers:
        - name: serving
          image: cloudcompute/recsys-serving:latest
          ports: [{ containerPort: 9000 }]
          env:
            - { name: PRECISION, value: "fp16" }
            - { name: TOPK, value: "200" }
            - { name: RERANK_K, value: "50" }
          volumeMounts:
            - { name: models, mountPath: /models }
            - { name: cache,  mountPath: /var/cache/recsys }
            - { name: index,  mountPath: /index }
          resources:
            limits: { nvidia.com/gpu: 1, memory: "24Gi", cpu: "4" }
      volumes:
        - name: models  ; hostPath: { path: /nvme/models }
        - name: cache   ; hostPath: { path: /nvme/recsys-cache }
        - name: index   ; hostPath: { path: /nvme/index }


				
			

Конфиг пайплайна (YAML)

				
					schema:
  dense:  ["price", "discount", "session_time", "age_norm"]
  sparse: ["user_id", "item_id", "brand", "category", "geo", "device"]
  crosses:
    - ["brand","category"]
    - ["geo","device"]

two_tower:
  user_tower:
    emb_dims: { user_id: 64, geo: 16, device: 8 }
    mlp: [256, 128, 64]
  item_tower:
    emb_dims: { item_id: 64, brand: 16, category: 16 }
    mlp: [256, 128, 64]
  loss: "bpr"              # bpr|softmax|sampled_softmax
  neg_sampling: "in_batch" # in_batch|hard|mixed

dlrm_ranker:
  embedding_dims: { user_id: 64, item_id: 64, brand: 16, category: 16, geo: 16, device: 8 }
  bottom_mlp: [256, 128]
  top_mlp: [256, 128, 1]
  interactions: "dot"      # dot|concat|cross
  loss: "bce"

serving:
  ann:
    type: "gpu_hnsw"       # gpu_flat|gpu_ivf|gpu_hnsw
    topk: 200
  rerank:
    k: 50
    diversity_lambda: 0.2
  thresholds:
    min_score: 0.05

				
			

Python (скелет): двухбашенный тренинг с in‑batch негативами

				
					import torch, torch.nn as nn, torch.nn.functional as F

class Tower(nn.Module):
    def __init__(self, emb_dims, mlp_layers):
        super().__init__()
        self.embs = nn.ModuleDict({k: nn.Embedding(num_embeddings=card, embedding_dim=dim)
                                   for k, (card, dim) in emb_dims.items()})
        dims = [sum(dim for _, dim in emb_dims.values())] + mlp_layers
        self.mlp = nn.Sequential(*[nn.Linear(dims[i], dims[i+1]) for i in range(len(dims)-1)])

    def forward(self, x_sparse):
        e = [self.embs[k](x_sparse[k]) for k in self.embs]  # [B, dim]
        h = torch.cat(e, dim=-1)
        return F.normalize(self.mlp(h), dim=-1)

class TwoTower(nn.Module):
    def __init__(self, user_cfg, item_cfg):
        super().__init__()
        self.user = Tower(user_cfg["emb_dims"], user_cfg["mlp"])
        self.item = Tower(item_cfg["emb_dims"], item_cfg["mlp"])

    def forward(self, user_sparse, item_sparse):
        u = self.user(user_sparse)    # [B, D]
        v = self.item(item_sparse)    # [B, D]
        return u, v

def bpr_loss(u, v):
    # in-batch negatives: scores = u @ v.T
    s = u @ v.T                      # [B, B]
    pos = s.diag()
    neg = s - torch.diag_embed(pos)
    loss = -torch.log(torch.sigmoid(pos.unsqueeze(1) - neg)).mean()
    return loss

# Тренинг-цикл (AMP + FSDP/ZeRO по необходимости)


				
			

Python (скелет): онлайн‑сервинг retrieval→ranking

				
					def retrieve_candidates(user_vec, ann_index, topk=200):
    # ann_index.search(user_vec) -> ids, distances
    ids, d = ann_index.search(user_vec, topk)
    return ids

def rank_candidates(features_batch, dlrm_model):
    # features_batch: объединённые dense+sparse фичи для user×items
    with torch.cuda.amp.autocast():
        scores = dlrm_model(features_batch)  # [B, 1]
    return scores

def recommend(user_features):
    user_vec = user_tower(user_features["sparse"])
    cand_ids = retrieve_candidates(user_vec, ann_index, topk=cfg.topk)
    feats = feature_join(user_features, cand_ids)  # dense+sparse join
    scores = rank_candidates(feats, dlrm_model)
    reranked = diversify(cand_ids, scores, lambda_=0.2)
    return topk(reranked, k=cfg.rerank_k)


				
			

Наблюдаемость/метрики/алерты

Offline (качество):

  • recsys_auc, recsys_ndcg@k, recsys_map@k, recsys_mrr@k, recsys_ece (калибровка).
  • recsys_hardneg_share — доля «жёстких» негативов; recsys_label_leak_check.

Online (прод):

  • Latency: recsys_latency_seconds{stage=feature|ann|rank|rerank} (p50/p95).
  • Throughput: recsys_qps, recsys_batch_size_effective.
  • CTR/CVR/Revenue‑per‑Mille: агрегаты по трафику/сегментам.
  • Drift: recsys_feature_drift_{mean,std}, recsys_score_drift.
  • GPU: recsys_gpu_utilization, recsys_gpu_mem_peak_bytes, recsys_nvme_{read,write}_mb_s.

Алерты (примеры):

  • recsys_latency_p95 > SLA — уменьшить TOP‑K, упростить MLP, включить FP16/BF16/фьюзинг, добавить GPU.
  • recsys_auc_drop > threshold — регресс качества; откат модели/индекса.
  • gpu_mem_peak/HBM > 0.9 — уменьшить размеры эмбеддингов/батч, разделить сервисы (ANN/ранкер) по пулам.
  • recsys_feature_drift > threshold — пересобрать нормировку/калибровку, обновить фичи.

Подробнее: https://cloudcompute.ru/solutions/monitoring-logging/https://cloudcompute.ru/solutions/llm-inference/observability/

Экономика и формулы

Обозначения: c_gpu — цена GPU/час, U — целевая загрузка, t_infer — среднее время инференса одной рекомендации (сек).

  • Стоимость 1 млн рекомендаций (онлайн):
    Cost_per_1M_recs ≈ (c_gpu × 1e6 × t_infer) / (3600 × U).
  • Сколько GPU (онлайн):
    GPU_count = ceil( (QPS × t_infer) / U ).
  • Время тренинга:
    T_epoch ≈ N_samples / (throughput_samples_per_sec × GPU_count × U).
  • Память эмбеддингов (тренинг):
    Mem_params = Σ_i |V_i| × d_i × bytes
    Mem_opt ≈ multipliers × Mem_params (Adam: ~2× для m,v; +градиенты).
    Итог: Mem_total ≈ (1 + multipliers + градиенты + активации) × Mem_params.
    Для инференса можно держать on‑device кэш hot‑ID, cold‑ID — на CPU/NVMe (UVA/pinned), см. https://cloudcompute.ru/solutions/performance-tuning/
  • Задержка retrival→rank:
    Latency_total ≈ t_ann(topk) + t_join + t_rank(k) + t_rules. Балансируйте topk/rerank_k.

Подробно про компромисс throughput↔latency: https://cloudcompute.ru/solutions/throughput-vs-latency/ • Планирование бюджета: https://cloudcompute.ru/solutions/cost-planner/

Безопасность/политики

  • PII/персонализация: хэшируйте пользовательские идентификаторы; разделяйте пользовательские/товарные фичи по бакетам; шифрование «в канале/на диске».
  • Ретеншн: события/логи — TTL; обучающие выборки — версионирование/аудит; без хранения исходных PII в фичах.
  • Доступ: разграничение по тенантам/региону; отдельные пулы GPU; журналирование экспорта моделей/индексов.
  • Защита от утечки сигналов: фильтрация лейблов/фичей, анти‑label‑leakage проверки.

Подробнее: https://cloudcompute.ru/solutions/security/https://cloudcompute.ru/solutions/storage-data/

Траблшутинг

Симптом

Возможная причина

Решение

Latency p95 ↑

Большой TOP‑K/тяжёлый ранкер/медленный join

Снизить TOP‑K, упростить MLP, предварительный фильтр, ускорить фичестор (NVMe)

CTR не растёт при AUC↑

Модель некалибрована/не учтён бизнес‑контекст

Калибровка (Platt/Isotonic), бизнес‑правила/диверсификация, онлайн‑A/B

Память HBM OOM

Крупные эмбеддинги/батчи

FP16/BF16, уменьшить d, шардировать таблицы, вынос cold‑ID на CPU/NVMe

Обучение «взрывается»

Слишком агрессивные hard‑negatives/LR

Ослабить hard‑negatives, reduce LR, gradient clipping

Фичи «разъехались»

Скью между тренингом и продом

Единый фичестор, версионирование трансформов, тест «тренинг=прод»

Перенасыщение популярных ID

Частотные дисбалансы

Частотное семплирование, регуляризация/декорреляция фичей

Падение recall@K в retrieval

Недостаточный ANN/квантование

Поднять efSearch/nprobe, уменьшить PQ, увеличить размер индекса

Дрифт показателей

Смена сезона/каталога

Регулярные переобучения, онлайн‑апдейты эмбеддингов, детект дрифта

См. также: https://cloudcompute.ru/solutions/performance-tuning/https://cloudcompute.ru/solutions/interruptible-patterns/

Как запустить в cloudcompute.ru

  1. Откройте Шаблоны запусков: https://cloudcompute.ru/solutions/templates/ — выберите RecSys (Serving) и/или RecSys (Training + Index Build).
  2. Выберите профиль GPU: 24/48/80 ГБ — по глубине модели/объёму эмбеддингов и SLA.
  3. Смонтируйте диски: /nvme/models, /nvme/recsys-cache, /nvme/index, для тренинга — /mnt/logs и /nvme/ckpt.
  4. Заполните переменные окружения из docker-compose.yml (ANN, TOP‑K, precision, batching).
  5. Для продакшна: раздельные пулы On‑Demand/Interruptible, автоскейл по U/latency/QPS, дашборды и алерты, канареечный деплой.

Дополнительно:

Чек‑лист перед продом

  • Согласованы целевые метрики: AUC/NDCG@K/CTR uplift.
  • Стабильный p95 latency для feature→ANN→rank→rerank; запас по U ≥ 0.2.
  • Индекс кандидатов прогрет; стратегия shadow‑index → alias‑swap задеплоена.
  • Единый фичестор и идентичные трансформы в тренинге/проде.
  • Настроены алерты: latency/QPS/HBM/recall@K/дрифт.
  • Политики PII/ретеншна/доступов внедрены.
  • Чекпоинты/откат модели и индекса готовы.
  • Нагрузочный прогон ≥ 30 мин на целевом профиле/трафике.

Навигация