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

9 вопросов по теме «ML Engineering: Инженерия ML-кода» на собеседовании

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

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

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

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

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

01

Как тестировать ML-код, если «правильный ответ» стохастический?

Короткий ответ: Разделить детерминированное и стохастическое. Трансформы, лоссы и метрики покрываются обычными юнит-тестами на просчитываемых вручную кейсах; обучение проверяется смоук-тестами (оверфит крошечного батча, shape- и градиент-чеки) и поведенческими тестами с допуском.

Подробно:

Слой Что проверяет Пример
Юнит-тесты трансформы, лоссы, метрики на ручных кейсах loss(y, y) == 0
Shape/dtype/градиенты размерности, типы, градиент доходит до всех параметров после backward() ни один param.grad не None
Оверфит одного батча пайплайн способен выучить 10 примеров до ~100% не может → сломана «проводка», а не данные
Поведенческие инвариантность и направленность синоним не меняет класс; «ужасно» понижает sentiment
Golden-предикты эталонные предсказания с допуском, гоняются в CI ловят молчаливые регрессии после рефакторинга
  1. Тестируем код, а не модель — детерминированные куски (фичи, лоссы) обязаны иметь точные ассерты, стохастические — допуски и инварианты.
  2. Смоук в CI — короткий прогон на 2–3 шага: пайплайн собирается, лосс конечен и падает.

⚠️ Частая ошибка: «мы смотрим на test accuracy» — это оценка модели, а не тестирование кода. Баг в даталоадере может стоить пару пунктов метрики и никогда не всплыть.

02

Лосс на обучении не падает. Опишите порядок отладки.

Короткий ответ: Идти от данных к архитектуре, а не наоборот. Сначала посмотреть батч и метки глазами, затем решающий эксперимент — оверфит одного батча. Только потом LR, ошибки «проводки» тренировочного цикла и инициализация.

Подробно:

1. Данные глазами: сырой батч, метки, соответствие X ↔ y
2. Оверфит ОДНОГО батча до ~100% — решающий эксперимент:
   не может → сломана проводка; может → данные или LR
3. LR-свип: ×10 вверх и вниз — слишком большой LR
   тоже выглядит как «не падает»
4. Проводка: забытый optimizer.zero_grad(),
   неверная редукция лосса, метки не выровнены с логитами
   (сдвиг, порядок классов), замороженные параметры,
   забытый model.train()
5. Инициализация и нормализация входов
  1. Порядок важнее списка — интервьюер оценивает именно то, что данные вы проверяете раньше архитектуры: они ломаются на порядки чаще.
  2. Оверфит батча — одна проверка режет пространство гипотез пополам: неспособность выучить 10 примеров исключает «сложную задачу» как оправдание.

⚠️ Частая ошибка: первым делом менять архитектуру или добавлять слои — почти всегда виноваты данные или проводка цикла, а не ёмкость модели.

03

Что вы валидируете во входных данных до обучения — кодом, прямо в пайплайне?

Короткий ответ: Схему (колонки и dtype), границы значений, доли null, домены категорий, объём строк относительно прошлого запуска и распределение таргета. Проверки — это код в пайплайне, который громко падает ДО обучения, а не глаза аналитика после.

Подробно:

def validate(df, ref):
    assert set(df.columns) == set(ref.columns)         # схема
    assert df["age"].between(18, 100).all()            # границы значений
    assert df["income"].isna().mean() < 0.02           # доля null
    assert set(df["region"]) <= ref.region_domain      # домен категорий
    assert 0.5 < len(df) / ref.row_count < 2.0         # объём vs прошлый ран
    assert abs(df["y"].mean() - ref.y_rate) < 0.05     # распределение таргета
  1. Fail loudly vs карантин — нарушение схемы или таргета валит пайплайн целиком; единичные битые строки можно отправлять в карантин с алертом, но порог доли — тоже проверка.
  2. Референс из прошлого запуска — половина проверок относительные: сравнение с зафиксированной статистикой предыдущего успешного рана.
  3. Инструменты — Great Expectations, pandera; но интервьюеру важна концепция «валидация как гейт», а не название библиотеки.

⚠️ Частая ошибка: проверять только схему. Типы совпали, а колонка на 40% превратилась в null — обучение «успешно» пройдёт на мусоре.

04

Почему фиксация random seed не гарантирует воспроизводимость обучения?

Короткий ответ: Сид контролирует только генераторы случайных чисел. Остаются недетерминированные CUDA-ядра, автотюнинг cuDNN, планирование воркеров даталоадера, неассоциативность float-арифметики и версии библиотек. Полный ответ: сиды + детерминированные флаги + запиненное окружение + версии данных/кода/конфига.

Подробно:

Источник недетерминизма Митигация
CUDA-ядра с atomics в редукциях — порядок сложений плавает torch.use_deterministic_algorithms(True)
cuDNN autotune — выбирает алгоритм по замерам на лету cudnn.benchmark=False, cudnn.deterministic=True
Воркеры даталоадера — порядок и сиды процессов worker_init_fn с сидом от worker id, фиксированный generator
Float неассоциативен — другое число GPU → другой порядок сумм фиксировать конфигурацию железа и world size
Версии библиотек/драйверов lock-файл или Docker-образ + версии данных и конфига
  1. Цена детерминизма — детерминированные ядра заметно медленнее; в проде часто фиксируют всё, кроме них, и принимают шум в пределах допуска.
  2. Воспроизводимость ≠ бит-в-бит — практический стандарт: та же метрика в пределах заявленного разброса при полном восстановлении lineage.

⚠️ Частая ошибка: «поставил seed=42, значит воспроизводимо» — на другом GPU или другой версии cuDNN цифры разойдутся.

05

Что должен записывать трекер экспериментов, чтобы запуск был воспроизводимым и сравнимым?

Короткий ответ: Всё, из чего собирается результат: git-коммит кода, версию/хеш данных, полный конфиг с гиперпараметрами, окружение, метрики по шагам и артефакты (чекпоинты). Логировать одни метрики — ловушка: победителя потом не воспроизвести.

Подробно:

  1. Код — точный commit hash плюс флаг «грязного» рабочего дерева: незакоммиченный патч делает коммит бесполезным.
  2. Данные — версия или хеш датасета (DVC, снапшот): «та же таблица» через месяц — уже другие данные.
  3. Конфиг целиком — все гиперпараметры, сплиты, версии фичей; не только те, что «крутили» в этом эксперименте.
  4. Окружение — lock-файл зависимостей или образ, версия CUDA/драйвера.
  5. Метрики по шагам — кривые train/val, а не одна финальная цифра: видно расхождение и раннюю остановку.
  6. Артефакты — чекпоинт, токенизатор, препроцессор — привязанные к запуску.
run = код (commit) + данные (hash) + конфиг + env
      └─► метрики по шагам + артефакты (чекпоинт)

⚠️ Частая ошибка: сравнивать запуски по метрике, когда у них незаметно разные данные или сплит — сравнение бессмысленно без одинакового lineage.

06

GPU-утилизация 30% во время обучения. Как найдёте узкое место?

Короткий ответ: Это producer-consumer проблема: GPU почти всегда голодает из-за входного пайплайна. Сначала профилирование (torch.profiler, py-spy, «пила» в nvidia-smi), потом фиксы: num_workers, pin_memory, офлайн-препроцессинг, prefetch — и убрать синхронизации вроде .item() на каждом шаге.

Подробно:

1. Профилировать, не гадать: torch.profiler / py-spy;
   «пила» в nvidia-smi utilization = GPU ждёт данные
2. Даталоадер: num_workers > 0, pin_memory=True,
   .to(device, non_blocking=True)
3. Вынести тяжёлое из __getitem__: пре-токенизация,
   пре-ресайз офлайн, кэш на быстром диске
4. Prefetch следующего батча; батч крупнее,
   если позволяет память
5. Синхронизации в цикле: .item() / логирование
   каждый шаг, лишние .cpu() — скрытые sync-точки
6. Хранилище: сетевой диск, миллионы мелких файлов →
   шардированные форматы (webdataset, tfrecord)
  1. Фрейминг — конвейер «диск → CPU-декодирование → H2D-копия → GPU»: утилизация 30% значит, что самый медленный этап не GPU.
  2. Профайлер решает спор за минуты — timeline сразу показывает, кто держит шаг: DataLoader, копирование или вычисление.

⚠️ Частая ошибка: сразу просить GPU помощнее — ускорит только consumer, который и так простаивает 70% времени.

07

Что на самом деле делают num_workers и pin_memory в PyTorch DataLoader?

Короткий ответ: num_workers поднимает отдельные процессы (не потоки — из-за GIL), которые параллельно читают и декодируют батчи с префетчем. pin_memory кладёт готовый батч в page-locked память, что позволяет асинхронно копировать его на GPU через non_blocking=True.

Подробно:

loader = DataLoader(ds, batch_size=64,
    num_workers=8,     # 8 процессов декодируют и префетчат батчи
    pin_memory=True)   # батчи в page-locked (pinned) памяти

x = batch.to("cuda", non_blocking=True)  # асинхронный H2D:
# копия перекрывается с вычислениями — только из pinned памяти
  1. Процессы, а не потоки — Python GIL не даёт декодировать параллельно в потоках; воркеры форкаются и общаются через очередь.
  2. Pinned память — ОС не может выгрузить страницу, поэтому DMA-копирование на GPU идёт без промежуточного буфера и асинхронно.
  3. Цена воркеров — каждый держит свою копию датасета в памяти: RAM ×N; ленивая инициализация тяжёлых объектов обязательна.

⚠️ Частая ошибка: «поставим num_workers=32, будет быстрее» — воркеров больше, чем ядер CPU, значит контекст-свитчи и деградация; подбирается замером, стартуя с числа ядер.

08

Спроектируйте тренировочный пайплайн как DAG: какими свойствами должен обладать каждый шаг?

Короткий ответ: Каждый шаг — идемпотентный, обменивается с соседями только явными версионированными артефактами, параметризован датой запуска (ради бэкфиллов) и переживает ретраи. Плюс два гейта: валидация данных ДО обучения и eval ДО промоушена в registry.

Подробно:

ingest ──► validate ──► features ──► train ──► eval ──► promote
              │ гейт: fail            │          │ гейт: метрики
              ▼ до трейна             ▼          ▼ ≥ порога
            стоп               checkpoint     registry

каждый шаг: идемпотентен · читает/пишет версионированные
артефакты · параметризован датой · с ретраями
  1. Идемпотентность — повторный запуск шага за ту же дату даёт тот же результат и не дублирует данные: пишем в путь с версией/датой, а не append.
  2. Явные артефакты — шаги общаются через хранилище (parquet, чекпоинт), а не через память процесса: любой шаг можно перезапустить отдельно.
  3. Параметризация датойrun(date) вместо «сегодня»: бэкфилл за месяц — это просто 30 запусков.
  4. Гейты — битые данные не доходят до обучения, слабая модель — до продовой registry; промоушен всегда через сравнение с текущим чемпионом.

⚠️ Частая ошибка: сыпать названиями операторов Airflow вместо свойств шагов — интервьюер спрашивает про инварианты, оркестратор вторичен.

09

Модель отлично работает офлайн и плохо в проде. Назовите инженерные (не статистические) причины.

Короткий ответ: Прежде чем говорить «дрифт», исключить расхождения кода и данных между трейном и сервингом: два кодовых пути фичей, point-in-time нарушения в трейн-джоине, разъехавшиеся версии препроцессинга, устаревшие онлайн-фичи, потери при сериализации.

Подробно:

Причина Как проверить
Training-serving skew — фича реализована дважды (SQL офлайн, сервисный код онлайн) сверить офлайн- и онлайн-значения фичей на одних объектах
Point-in-time нарушение — трейн-джоин взял агрегаты «на сейчас», а не на момент события пересобрать джоин as-of даты события; офлайн-метрика упадёт до продовой
Версия препроцессинга — токенизатор/нормализация обновились, модель — нет хеш артефакта препроцессинга в трейне == в сервинге
Устаревшие онлайн-фичи — фича-стор отдаёт вчерашние значения мониторинг freshness; доля дефолтных значений в ответах
Сериализация/точность — экспорт в ONNX/fp16, другой рантайм сравнить логиты до/после экспорта на одном батче
  1. Дельта с ответом дата-сайентиста — «дрифт» — статистическая гипотеза; сеньорный инженерный ответ сначала называет кодовые пути, где трейн и прод видят разные данные.

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

Источники

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

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

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

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

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

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

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

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

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

RSS