Сначала зафиксируйте grain данных, допущения, метрику и риск leakage, затем обсуждайте модель, инструмент или инфраструктуру.
Вопросы и ответы
11 подробных ответов
01Data, tensor и pipeline parallelism: когда использовать каждый?
concept
Короткий ответ: 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-масштаб | сложность оркестрации |
- Порядок выбора — начинаем с data parallelism; не влезает слой → tensor внутри узла; не влезает модель целиком → pipeline между узлами.
- Bubble — стадии простаивают в начале и конце шага; лечится нарезкой батча на микробатчи.
⚠️ Частая ошибка: смешивать tensor и pipeline parallelism: TP режет матрицы внутри слоя (все GPU считают один слой вместе), PP режет модель по слоям на последовательные стадии.
02Как работает PyTorch DDP под капотом и чем он лучше DataParallel?
middle
Короткий ответ: DDP — один процесс на GPU, у каждого полная реплика модели. Хуки на backward собирают градиенты в бакеты и запускают асинхронный all-reduce, перекрытый с продолжающимся backprop; после синхронизации все реплики делают идентичный шаг оптимизатора. DataParallel — один процесс: GIL, scatter/gather через GPU-0, который становится бутылочным горлышком.
Подробно:
- Один процесс = один GPU — никакого GIL; запуск через torchrun.
- Бакеты градиентов — градиенты группируются (по умолчанию ~25 MB); как только бакет готов, all-reduce стартует, не дожидаясь конца backward.
- Перекрытие — коммуникация едет параллельно с вычислением градиентов ранних слоёв; шаг почти не платит за синхронизацию.
- Идентичный шаг — одинаковые усреднённые градиенты у всех → реплики не расходятся; 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 оптимален по трафику?
middle
Короткий ответ: All-reduce — коллективная операция: каждый воркер отдаёт свой тензор градиентов, и каждый получает поэлементную сумму по всем. Ring all-reduce гоняет данные по кольцу чанками: каждый GPU передаёт ≈2M байт независимо от числа воркеров — поэтому он оптимален по пропускной способности. Его реализует NCCL.
Подробно:
- Scatter-reduce — тензор режется на N чанков; за N−1 шагов по кольцу каждый GPU накапливает полную сумму одного чанка.
- All-gather — ещё N−1 шагов, и готовые чанки разъезжаются всем.
- Арифметика — 2(N−1) шагов по M/N байт: трафик на GPU = 2(N−1)/N × M ≈ 2M, от N не зависит.
- Против 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 — сколько нужно памяти?
senior
Короткий ответ: Веса + градиенты (столько же) + состояния 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
- Активации — сверху; растут с батчем и длиной последовательности, именно их режет gradient checkpointing.
- Буферы — временные тензоры CUDA, фрагментация аллокатора, буферы NCCL.
- Mixed precision не спасает оптимизатор — fp32 master-веса и моменты остаются, поэтому на параметр всё те же ≈16 B; выигрыш — в активациях и скорости.
⚠️ Частая ошибка: посчитать только веса. Интервьюер ждёт множитель ×4 для fp32+Adam и вывод: у 7B-модели полный fine-tuning требует нескольких GPU.
05Mixed precision: fp16 против bf16 — откуда ускорение и почему все перешли на bf16?
middle
Короткий ответ: Ускорение дают 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) |
- Откуда скорость — tensor cores перемножают half-матрицы в разы быстрее; вдвое меньше байт → меньше давление на память и интерконнект.
- Боль fp16 — мелкие градиенты обнуляются (underflow); лечится loss scaling: умножили лосс, поделили градиенты — лишняя механика и источник NaN.
- Почему победил bf16 — диапазон fp32 без масштабирования; потерю мантиссы компенсирует стохастичность SGD. Стандарт на A100/H100 и TPU.
⚠️ Частая ошибка: «bf16 точнее fp16». Наоборот: мантисса у bf16 короче, он грубее по точности — выигрывает он диапазоном, а для стабильности обучения важен именно диапазон.
06Gradient accumulation против gradient checkpointing: что экономит каждый?
middle
Короткий ответ: Accumulation имитирует большой батч: K микробатчей копят градиенты, шаг оптимизатора один — экономит память активаций за счёт лишних шагов, но память состояний оптимизатора не уменьшает. Checkpointing выбрасывает активации на forward и пересчитывает их на backward — платим ~30% compute за большую экономию памяти активаций.
Подробно:
| Accumulation | Checkpointing | |
|---|---|---|
| Экономит | активации (маленький физический батч) | активации (пересчёт в backward) |
| Не экономит | веса, градиенты, состояния оптимизатора | веса, градиенты, состояния оптимизатора |
| Цена | больше шагов на эпоху | ~30% лишнего compute |
| Зачем | эффективный батч больше, чем влезает | впихнуть модель с длинными активациями |
- Делить лосс на K — обязательно, иначе эффективный learning rate умножается на K.
- Совместимы — на практике включают оба сразу, плюс mixed precision.
- Классический follow-up (СНГ) — accumulation ломает BatchNorm: статистики считаются по маленькому физическому батчу, а не по эффективному → SyncBatchNorm или LayerNorm.
⚠️ Частая ошибка: заявить, что accumulation экономит память оптимизатора. Моменты Adam живут по одному на параметр независимо от батча — их режет только ZeRO/FSDP.
07Зачем нужен SyncBatchNorm при распределённом обучении?
middle
Короткий ответ: Обычный BatchNorm в DDP считает mean/var только по своему шарду батча на каждом GPU. При маленьком батче на GPU статистики шумные и разные у реплик — качество проседает. SyncBatchNorm делает all-reduce среднего и дисперсии по всем GPU: статистики как у полного глобального батча, ценой лишней синхронизации на каждый BN-слой.
Подробно:
- Что происходит без него — DDP синхронизирует градиенты, но не батчевые статистики: у каждой реплики свои mean/var и свои running-статистики.
- Когда болит — детекция/сегментация с батчем 1–4 на GPU: нормализация по паре примеров — почти шум.
- Когда не нужен — при батче 32+ на GPU локальных статистик хватает; в трансформерах LayerNorm от батча не зависит вовсе.
- Включение — nn.SyncBatchNorm.convert_sync_batchnorm(model) до обёртки в DDP.
| BatchNorm в DDP | SyncBatchNorm | |
|---|---|---|
| Статистики | по шарду одного GPU | по всему глобальному батчу |
| Маленький батч/GPU | шумные, реплики расходятся | стабильные |
| Цена | — | +1 синхронизация на каждый BN-слой |
⚠️ Частая ошибка: считать, что DDP «синхронизирует всё». Он сводит только градиенты — статистики BN остаются локальными и разными у реплик, и именно это спрашивают на собеседовании.
08Что именно шардирует ZeRO/FSDP на каждой стадии?
senior
Короткий ответ: 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 |
- Just-in-time сборка — в stage 3 полный слой существует только на время своего вычисления; после — шард освобождается.
- Это по-прежнему data parallelism — каждый GPU прогоняет свой срез батча через (собранную) полную модель; делится хранение, а не вычисление.
- Цена — чем выше стадия, тем больше трафика: stage 3 гоняет параметры каждый шаг, поэтому хочет быстрый интерконнект.
⚠️ Частая ошибка: называть FSDP «model parallelism». В tensor/pipeline parallelism GPU считают разные части модели; в FSDP шардируется только хранение — вычисление остаётся data-parallel.
09Ваш DDP-джоб на 8 GPU быстрее одного GPU только в 5 раз. Как диагностировать?
middle
Короткий ответ: Идти по списку, а не гадать: голодающий 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
- Сначала измерить — trace профилировщика показывает, чего ждёт GPU: данных, сети или соседа; ответ «наверное, сеть» без trace — минус на собеседовании.
- All-reduce умеет прятаться — при хорошем overlap коммуникация скрыта за backward; если она торчит в trace — вот и потерянные 3×.
⚠️ Частая ошибка: сразу винить сеть. На практике чаще всего голодает dataloader — провалы GPU-утилизации между шагами видны даже в nvidia-smi.
10Трёхдневное обучение упало на второй день. Что должно было быть предусмотрено?
middle
Короткий ответ: Периодические чекпоинты с полным состоянием: веса + состояние оптимизатора + 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") # атомарная запись
- Атомарность — писать во временный файл и переименовывать: падение посреди torch.save не должно портить последний рабочий чекпоинт.
- Keep-last-k — не один файл (может побиться), и не все (диск не резиновый).
- Частота — компромисс между потерянными часами и стоимостью записи; для больших моделей запись делают асинхронной.
⚠️ Частая ошибка: чекпоинтить только веса. Без моментов Adam оптимизатор на резюме стартует с нуля — накопленная статистика потеряна, и лосс делает спайк.
11Можно ли зафайнтюнить 7B-модель на одном GPU с 24 GB? Как уместить?
senior
Короткий ответ: Полный 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 → влезает с запасом
Меню, по порядку:
- LoRA — убирает главный множитель: 12 B/параметр оптимизаторных затрат остаются только у крошечных адаптеров.
- Квантованная база (QLoRA) — замороженные веса в nf4: ×4 меньше, чем bf16.
- Gradient checkpointing — режет активации за ~30% лишнего compute.
- Батч 1 + gradient accumulation — минимум активаций, эффективный батч сохраняем.
- bf16 — для всего, что не квантовано.
⚠️ Частая ошибка: сыпать техниками без арифметики. Сильный ответ сначала показывает 112 GB против 24, а потом объясняет, какое слагаемое убирает каждый приём.
Источники
Источники и редакционная политика
Материалы RecallDeck сопоставлены с официальной документацией и открытыми публикациями компаний, когда первичный источник доступен. Мы не связаны с упомянутыми работодателями, не публикуем конфиденциальные задания и не продаём места в подборках. Формат найма может меняться — уточняйте его у рекрутера.
От чтения к воспроизведению
Отрепетируйте полный цикл интервью.
RecallDeck возвращает сложные темы по расписанию и помогает удерживать в памяти язык, SQL, архитектуру и поведенческие истории.