Перейти к содержанию
Данные и AI

11 вопросов по теме «ML Engineering: Распределённое обучение» на собеседовании

В этом материале — 11 вопросов из русской колоды RecallDeck по теме «ML Engineering: Распределённое обучение». Сначала сформулируйте короткий ответ сами, затем откройте подробный разбор и проверьте примеры, ограничения и отказные случаи.

10 мин чтения11 подробных ответовПроверено 24 августа 2026
Главная мысль

Сначала зафиксируйте grain данных, допущения, метрику и риск leakage, затем обсуждайте модель, инструмент или инфраструктуру.

Вопросы и ответы

11 подробных ответов

01

Data, tensor и pipeline parallelism: когда использовать каждый?

Короткий ответ: Data parallelism — когда модель влезает на один GPU: реплика на каждом устройстве, батч делится, градиенты сводятся all-reduce. Tensor parallelism режет отдельные matmul между GPU — нужны линки класса NVLink, внутри узла. Pipeline parallelism делит слои на стадии, микробатчи борются с bubble. Крупнейшие модели комбинируют все три — 3D-параллелизм.

Подробно:

Вид Что делим Когда применять Цена
Data (DDP) батч модель влезает на 1 GPU all-reduce градиентов каждый шаг
Tensor matmul внутри слоя слой/модель не влезает; быстрые линки внутри узла коммуникация в каждом слое, forward и backward
Pipeline слои → стадии модель не влезает, межузловые линки медленные pipeline bubble — простой стадий
3D всё сразу LLM-масштаб сложность оркестрации
  1. Порядок выбора — начинаем с data parallelism; не влезает слой → tensor внутри узла; не влезает модель целиком → pipeline между узлами.
  2. Bubble — стадии простаивают в начале и конце шага; лечится нарезкой батча на микробатчи.

⚠️ Частая ошибка: смешивать tensor и pipeline parallelism: TP режет матрицы внутри слоя (все GPU считают один слой вместе), PP режет модель по слоям на последовательные стадии.

02

Как работает PyTorch DDP под капотом и чем он лучше DataParallel?

Короткий ответ: DDP — один процесс на GPU, у каждого полная реплика модели. Хуки на backward собирают градиенты в бакеты и запускают асинхронный all-reduce, перекрытый с продолжающимся backprop; после синхронизации все реплики делают идентичный шаг оптимизатора. DataParallel — один процесс: GIL, scatter/gather через GPU-0, который становится бутылочным горлышком.

Подробно:

  1. Один процесс = один GPU — никакого GIL; запуск через torchrun.
  2. Бакеты градиентов — градиенты группируются (по умолчанию ~25 MB); как только бакет готов, all-reduce стартует, не дожидаясь конца backward.
  3. Перекрытие — коммуникация едет параллельно с вычислением градиентов ранних слоёв; шаг почти не платит за синхронизацию.
  4. Идентичный шаг — одинаковые усреднённые градиенты у всех → реплики не расходятся; broadcast весов нужен только на старте.
backward:  слой N ─► бакет заполнен ─► async all-reduce ─┐
           слой N−1, N−2 … градиенты считаются дальше    │ overlap
optimizer.step() ждёт только последний бакет ◄───────────┘

⚠️ Частая ошибка: думать, что градиенты отправляются на центральный сервер. All-reduce — peer-to-peer коллектив: каждый GPU в итоге держит одну и ту же сумму, выделенного агрегатора нет.

03

Что такое all-reduce и почему ring all-reduce оптимален по трафику?

Короткий ответ: All-reduce — коллективная операция: каждый воркер отдаёт свой тензор градиентов, и каждый получает поэлементную сумму по всем. Ring all-reduce гоняет данные по кольцу чанками: каждый GPU передаёт ≈2M байт независимо от числа воркеров — поэтому он оптимален по пропускной способности. Его реализует NCCL.

Подробно:

  1. Scatter-reduce — тензор режется на N чанков; за N−1 шагов по кольцу каждый GPU накапливает полную сумму одного чанка.
  2. All-gather — ещё N−1 шагов, и готовые чанки разъезжаются всем.
  3. Арифметика — 2(N−1) шагов по M/N байт: трафик на GPU = 2(N−1)/N × M ≈ 2M, от N не зависит.
  4. Против parameter server — у сервера входящий линк растёт как O(N); кольцо грузит все линки равномерно.
GPU0 ─► GPU1 ─► GPU2 ─► GPU3 ─► GPU0    (чанки по M/N)
scatter-reduce: N−1 шагов → у каждого готова сумма 1 чанка
all-gather:     N−1 шагов → полная сумма у всех
трафик/GPU: 2·(N−1)/N · M ≈ 2M — не растёт с числом GPU

⚠️ Частая ошибка: «всё собирается на мастере и раздаётся обратно» — это как раз parameter server, чей линк и есть бутылочное горлышко; кольцо придумали, чтобы его не было.

04

Куда уходит память GPU при обучении? Модель с весами на 1 GB — сколько нужно памяти?

Короткий ответ: Веса + градиенты (столько же) + состояния Adam (m и v — ещё два комплекта) + активации + буферы. В fp32 с Adam это 16 байт на параметр — вчетверо больше самих весов: модели на 1 GB нужно 3–5 GB ещё до активаций.

Подробно:

fp32 + Adam, на один параметр:
  веса        4 B
  градиенты   4 B
  Adam m      4 B
  Adam v      4 B
  ─────────────────
  итого      16 B/параметр (×4 от весов)

Модель 1 GB весов (~250M параметров) → ~4 GB (3–5 GB) до активаций

Mixed precision: 2 (fp16 веса) + 2 (градиенты) + 12 (fp32 master + m + v) ≈ 16 B
7B параметров → ≈112 GB — полный fine-tuning не влезает на один GPU
  1. Активации — сверху; растут с батчем и длиной последовательности, именно их режет gradient checkpointing.
  2. Буферы — временные тензоры CUDA, фрагментация аллокатора, буферы NCCL.
  3. Mixed precision не спасает оптимизатор — fp32 master-веса и моменты остаются, поэтому на параметр всё те же ≈16 B; выигрыш — в активациях и скорости.

⚠️ Частая ошибка: посчитать только веса. Интервьюер ждёт множитель ×4 для fp32+Adam и вывод: у 7B-модели полный fine-tuning требует нескольких GPU.

05

Mixed precision: fp16 против bf16 — откуда ускорение и почему все перешли на bf16?

Короткий ответ: Ускорение дают tensor cores плюс вдвое меньше памяти и трафика на веса и активации. У fp16 всего 5 бит экспоненты — крошечный диапазон, мелкие градиенты уходят в underflow: нужны dynamic loss scaling и fp32 master-веса. У bf16 экспонента как у fp32 (8 бит) — тот же диапазон, loss scaling не нужен; мантисса короче, но обучение это терпит.

Подробно:

Формат Знак Экспонента Мантисса Диапазон
fp32 1 8 23 ~10³⁸
fp16 1 5 10 ~65504
bf16 1 8 7 ~10³⁸ (как fp32)
  1. Откуда скорость — tensor cores перемножают half-матрицы в разы быстрее; вдвое меньше байт → меньше давление на память и интерконнект.
  2. Боль fp16 — мелкие градиенты обнуляются (underflow); лечится loss scaling: умножили лосс, поделили градиенты — лишняя механика и источник NaN.
  3. Почему победил bf16 — диапазон fp32 без масштабирования; потерю мантиссы компенсирует стохастичность SGD. Стандарт на A100/H100 и TPU.

⚠️ Частая ошибка: «bf16 точнее fp16». Наоборот: мантисса у bf16 короче, он грубее по точности — выигрывает он диапазоном, а для стабильности обучения важен именно диапазон.

06

Gradient accumulation против gradient checkpointing: что экономит каждый?

Короткий ответ: Accumulation имитирует большой батч: K микробатчей копят градиенты, шаг оптимизатора один — экономит память активаций за счёт лишних шагов, но память состояний оптимизатора не уменьшает. Checkpointing выбрасывает активации на forward и пересчитывает их на backward — платим ~30% compute за большую экономию памяти активаций.

Подробно:

Accumulation Checkpointing
Экономит активации (маленький физический батч) активации (пересчёт в backward)
Не экономит веса, градиенты, состояния оптимизатора веса, градиенты, состояния оптимизатора
Цена больше шагов на эпоху ~30% лишнего compute
Зачем эффективный батч больше, чем влезает впихнуть модель с длинными активациями
  1. Делить лосс на K — обязательно, иначе эффективный learning rate умножается на K.
  2. Совместимы — на практике включают оба сразу, плюс mixed precision.
  3. Классический follow-up (СНГ) — accumulation ломает BatchNorm: статистики считаются по маленькому физическому батчу, а не по эффективному → SyncBatchNorm или LayerNorm.

⚠️ Частая ошибка: заявить, что accumulation экономит память оптимизатора. Моменты Adam живут по одному на параметр независимо от батча — их режет только ZeRO/FSDP.

07

Зачем нужен SyncBatchNorm при распределённом обучении?

Короткий ответ: Обычный BatchNorm в DDP считает mean/var только по своему шарду батча на каждом GPU. При маленьком батче на GPU статистики шумные и разные у реплик — качество проседает. SyncBatchNorm делает all-reduce среднего и дисперсии по всем GPU: статистики как у полного глобального батча, ценой лишней синхронизации на каждый BN-слой.

Подробно:

  1. Что происходит без него — DDP синхронизирует градиенты, но не батчевые статистики: у каждой реплики свои mean/var и свои running-статистики.
  2. Когда болит — детекция/сегментация с батчем 1–4 на GPU: нормализация по паре примеров — почти шум.
  3. Когда не нужен — при батче 32+ на GPU локальных статистик хватает; в трансформерах LayerNorm от батча не зависит вовсе.
  4. Включение — nn.SyncBatchNorm.convert_sync_batchnorm(model) до обёртки в DDP.
BatchNorm в DDP SyncBatchNorm
Статистики по шарду одного GPU по всему глобальному батчу
Маленький батч/GPU шумные, реплики расходятся стабильные
Цена +1 синхронизация на каждый BN-слой

⚠️ Частая ошибка: считать, что DDP «синхронизирует всё». Он сводит только градиенты — статистики BN остаются локальными и разными у реплик, и именно это спрашивают на собеседовании.

08

Что именно шардирует ZeRO/FSDP на каждой стадии?

Короткий ответ: ZeRO раскладывает по data-parallel воркерам не батч, а состояние обучения. Stage 1 шардирует состояния оптимизатора, stage 2 — плюс градиенты, stage 3 (FSDP full shard) — плюс сами параметры: слой собирается all-gather'ом прямо перед вычислением и освобождается после. Память падает с ~16P до ~16P/N байт ценой дополнительной коммуникации.

Подробно:

Стадия Шардируется Память/GPU Доп. коммуникация
ZeRO-1 состояния Adam 4P + 4P + 8P/N почти нет
ZeRO-2 + градиенты 4P + 12P/N reduce-scatter вместо all-reduce
ZeRO-3 / FSDP + параметры ≈16P/N all-gather параметров на каждый слой, forward и backward
  1. Just-in-time сборка — в stage 3 полный слой существует только на время своего вычисления; после — шард освобождается.
  2. Это по-прежнему data parallelism — каждый GPU прогоняет свой срез батча через (собранную) полную модель; делится хранение, а не вычисление.
  3. Цена — чем выше стадия, тем больше трафика: stage 3 гоняет параметры каждый шаг, поэтому хочет быстрый интерконнект.

⚠️ Частая ошибка: называть FSDP «model parallelism». В tensor/pipeline parallelism GPU считают разные части модели; в FSDP шардируется только хранение — вычисление остаётся data-parallel.

09

Ваш DDP-джоб на 8 GPU быстрее одного GPU только в 5 раз. Как диагностировать?

Короткий ответ: Идти по списку, а не гадать: голодающий dataloader → соотношение коммуникации и вычислений → стрэгглеры → слишком маленький батч на GPU. И первым делом взять профилировщик (torch.profiler, nsys) — trace сразу покажет, где дыры.

Подробно:

Проверять по порядку:
1. Dataloader: GPU util «пилой» в nvidia-smi?
   → num_workers, pin_memory, prefetch, формат/кэш данных
2. Коммуникация vs compute: большие градиенты, нет NVLink,
   мелкие бакеты → bucket_cap_mb, реже синхронизация
3. Стрэгглеры: один медленный GPU или узел держит весь all-reduce
   → сравнить время шага по ранкам
4. Микробатч слишком мал: kernel-launch overhead съедает выигрыш
   → больше батч на GPU, torch.compile / CUDA graphs
  1. Сначала измерить — trace профилировщика показывает, чего ждёт GPU: данных, сети или соседа; ответ «наверное, сеть» без trace — минус на собеседовании.
  2. All-reduce умеет прятаться — при хорошем overlap коммуникация скрыта за backward; если она торчит в trace — вот и потерянные 3×.

⚠️ Частая ошибка: сразу винить сеть. На практике чаще всего голодает dataloader — провалы GPU-утилизации между шагами видны даже в nvidia-smi.

10

Трёхдневное обучение упало на второй день. Что должно было быть предусмотрено?

Короткий ответ: Периодические чекпоинты с полным состоянием: веса + состояние оптимизатора + LR scheduler + номер шага + RNG-состояния + позиция даталоадера; плюс цикл, умеющий с них подниматься, атомарная запись и хранение последних k чекпоинтов.

Подробно:

ckpt = {
    "model": model.state_dict(),
    "optimizer": optimizer.state_dict(),   # моменты Adam!
    "scheduler": scheduler.state_dict(),
    "step": step, "epoch": epoch,
    "rng": {"torch": ..., "cuda": ..., "numpy": ...},
    "sampler": sampler.state_dict(),       # позиция в данных
}
torch.save(ckpt, "ckpt.tmp")
os.replace("ckpt.tmp", "ckpt.pt")          # атомарная запись
  1. Атомарность — писать во временный файл и переименовывать: падение посреди torch.save не должно портить последний рабочий чекпоинт.
  2. Keep-last-k — не один файл (может побиться), и не все (диск не резиновый).
  3. Частота — компромисс между потерянными часами и стоимостью записи; для больших моделей запись делают асинхронной.

⚠️ Частая ошибка: чекпоинтить только веса. Без моментов Adam оптимизатор на резюме стартует с нуля — накопленная статистика потеряна, и лосс делает спайк.

11

Можно ли зафайнтюнить 7B-модель на одном GPU с 24 GB? Как уместить?

Короткий ответ: Полный fine-tuning — нет: ≈16 байт на параметр × 7B ≈ 112 GB. Уместить можно, срезая слагаемые по очереди: LoRA (состояния оптимизатора только для ~0.1–1% параметров), квантованная база (QLoRA: 4-битные веса + LoRA), gradient checkpointing, батч 1 с накоплением, bf16.

Подробно:

Полный FT: 7B × 16 B ≈ 112 GB  ≫ 24 GB → не влезает
Срезаем слагаемые:
  LoRA:  градиенты и Adam только для адаптеров (~0.1–1%)
         база заморожена: 7B × 2 B (bf16) = 14 GB + адаптеры
  QLoRA: база в 4 бит: 7B × 0.5 B ≈ 3.5 GB
         + LoRA + checkpointing → влезает с запасом

Меню, по порядку:

  1. LoRA — убирает главный множитель: 12 B/параметр оптимизаторных затрат остаются только у крошечных адаптеров.
  2. Квантованная база (QLoRA) — замороженные веса в nf4: ×4 меньше, чем bf16.
  3. Gradient checkpointing — режет активации за ~30% лишнего compute.
  4. Батч 1 + gradient accumulation — минимум активаций, эффективный батч сохраняем.
  5. bf16 — для всего, что не квантовано.

⚠️ Частая ошибка: сыпать техниками без арифметики. Сильный ответ сначала показывает 112 GB против 24, а потом объясняет, какое слагаемое убирает каждый приём.

Источники

Источники и редакционная политика

Материалы RecallDeck сопоставлены с официальной документацией и открытыми публикациями компаний, когда первичный источник доступен. Мы не связаны с упомянутыми работодателями, не публикуем конфиденциальные задания и не продаём места в подборках. Формат найма может меняться — уточняйте его у рекрутера.

От чтения к воспроизведению

Отрепетируйте полный цикл интервью.

RecallDeck возвращает сложные темы по расписанию и помогает удерживать в памяти язык, SQL, архитектуру и поведенческие истории.

Начать подготовку

Продолжить подготовку

Библиотека собеседований RecallDeck

Подробные русские ответы, разборы этапов найма и планы подготовки для российского IT-рынка.

RSS