Фаза 00 · урок 12
Отладка и профилирование
Цель урока: AI-код ломается не так, как обычный код. Веб-приложение завершается сбоем и показывает трассировку стека. Неправильно настроенный цикл обучения работает 8 часов, тратит 200 долларов на время GPU и создаёт модель, которая для каждого…
Текущий релиз AlexBred.com: первые 100 уроков русскоязычной программы.
Содержание урока
- Цели обучения
- Проблема
- Концепция
- Создание
- Часть 1. Отладка с помощью вывода (да, она работает)
- Часть 2. Отладчик Python (pdb и breakpoint)
- Часть 3. Журналирование в Python
- Часть 4. Измерение времени выполнения участков кода
- Часть 5. cProfile и line_profiler
- Часть 6. Профилирование памяти
- Часть 7. Распространённые ошибки в AI и способы их обнаружения
- Часть 8. Основы TensorBoard
- Часть 9. Отладчик VS Code
- Применение
- Запуск
- Упражнения
Худшие ошибки в AI не приводят к сбою. Модель молча обучается на мусоре и выдаёт красивую кривую функции потерь.
Тип: Практика Язык: Python Предварительные требования: Урок 1 (среда разработки), базовое знакомство с PyTorch Время: около 60 минут
Цели обучения
- Использовать условные
breakpoint()иdebug_print, чтобы во время обучения проверять формы тензоров, типы данных и значения NaN. - Профилировать циклы обучения с помощью
cProfile,line_profilerиtracemalloc, чтобы находить узкие места. - Обнаруживать распространённые ошибки в AI: несоответствия форм, потерю со значением NaN, утечку данных и тензоры на неверном устройстве.
- Настроить TensorBoard для визуализации кривых функции потерь, гистограмм весов и распределений градиентов.
Проблема
AI-код ломается не так, как обычный код. Веб-приложение завершается сбоем и показывает трассировку стека. Неправильно настроенный цикл обучения работает 8 часов, тратит 200 долларов на время GPU и создаёт модель, которая для каждого входа предсказывает среднее значение. Код ни разу не выдал ошибку. Причиной был тензор на неверном устройстве, забытый .detach() или утечка меток в признаки.
Вам нужны инструменты отладки, которые обнаружат такие незаметные сбои до того, как они впустую потратят ваше время и вычислительные ресурсы.
Концепция
Отладка AI выполняется на трёх уровнях:
Большинство сразу переходит к уровню 3 — разглядыванию TensorBoard. Но 80% ошибок в AI находятся на уровнях 1 и 2.
Создание
Часть 1. Отладка с помощью вывода (да, она работает)
Отладку с помощью вывода часто не воспринимают всерьёз. И зря. Для кода с тензорами точечный вызов печати полезнее пошагового выполнения в отладчике, потому что вам нужно одновременно видеть формы, типы данных и диапазоны значений.
def debug_print(name, tensor):
print(f"{name}: shape={tensor.shape}, dtype={tensor.dtype}, "
f"device={tensor.device}, "
f"min={tensor.min().item():.4f}, max={tensor.max().item():.4f}, "
f"mean={tensor.mean().item():.4f}, "
f"has_nan={tensor.isnan().any().item()}")
Вызывайте эту функцию после каждой подозрительной операции. Когда найдёте ошибку, удалите вызовы печати. Всё просто.
Часть 2. Отладчик Python (pdb и breakpoint)
Встроенный отладчик недооценивают при работе с AI. Добавьте breakpoint() в цикл обучения и исследуйте тензоры в интерактивном режиме.
def training_step(model, batch, criterion, optimizer):
inputs, labels = batch
outputs = model(inputs)
loss = criterion(outputs, labels)
if loss.item() > 100 or torch.isnan(loss):
breakpoint()
loss.backward()
optimizer.step()
Когда отладчик остановит выполнение, пригодятся следующие команды:
p outputs.shape— проверить формы.p loss.item()— посмотреть значение функции потерь.p torch.isnan(outputs).sum()— подсчитать значения NaN.p model.fc1.weight.grad— проверить градиенты.c— продолжить выполнение,q— выйти.
Это условная отладка. Вы останавливаетесь, только когда что-то выглядит неправильно. Для обучения на 10 000 шагов это важно.
Часть 3. Журналирование в Python
Замените вызовы печати журналированием, когда ваша отладка выходит за рамки быстрой проверки.
import logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
handlers=[
logging.FileHandler("training.log"),
logging.StreamHandler()
]
)
logger = logging.getLogger(__name__)
logger.info("Starting training: lr=%.4f, batch_size=%d", lr, batch_size)
logger.warning("Loss spike detected: %.4f at step %d", loss.item(), step)
logger.error("NaN loss at step %d, stopping", step)
Журналирование даёт временные метки, уровни серьёзности и вывод в файл. Когда запуск обучения завершается неудачей в 3 часа ночи, вам нужен файл журнала, а не исчезнувший за пределами экрана вывод терминала.
Часть 4. Измерение времени выполнения участков кода
Понять, на что уходит время, — первый шаг к оптимизации.
import time
class Timer:
def __init__(self, name=""):
self.name = name
def __enter__(self):
self.start = time.perf_counter()
return self
def __exit__(self, *args):
elapsed = time.perf_counter() - self.start
print(f"[{self.name}] {elapsed:.4f}s")
with Timer("data loading"):
batch = next(dataloader_iter)
with Timer("forward pass"):
outputs = model(batch)
with Timer("backward pass"):
loss.backward()
Распространённый результат: загрузка данных занимает 60% времени обучения. Решение — num_workers > 0 в вашем DataLoader, а не более быстрый GPU.
Часть 5. cProfile и line_profiler
Когда ручных таймеров недостаточно:
python -m cProfile -s cumtime train.py
Эта команда показывает каждый вызов функции, отсортированный по суммарному времени. Для построчного профилирования:
pip install line_profiler
@profile
def train_step(model, data, target):
output = model(data)
loss = F.cross_entropy(output, target)
loss.backward()
return loss
# Run with: kernprof -l -v train.py
Часть 6. Профилирование памяти
Память CPU с помощью tracemalloc
import tracemalloc
tracemalloc.start()
# your code here
model = build_model()
data = load_dataset()
snapshot = tracemalloc.take_snapshot()
top_stats = snapshot.statistics("lineno")
for stat in top_stats[:10]:
print(stat)
Память CPU с помощью memory_profiler
pip install memory_profiler
from memory_profiler import profile
@profile
def load_data():
raw = read_csv("data.csv") # watch memory jump here
processed = preprocess(raw) # and here
return processed
Запустите python -m memory_profiler your_script.py, чтобы увидеть построчное использование памяти.
Память GPU с помощью PyTorch
import torch
if torch.cuda.is_available():
print(torch.cuda.memory_summary())
print(f"Allocated: {torch.cuda.memory_allocated() / 1e9:.2f} GB")
print(f"Cached: {torch.cuda.memory_reserved() / 1e9:.2f} GB")
Когда вы сталкиваетесь с OOM (Out of Memory, нехваткой памяти):
- Уменьшите размер пакета — это всегда нужно пробовать в первую очередь.
- Используйте
torch.cuda.empty_cache(), чтобы освободить кэшированную память. - Для крупных промежуточных результатов используйте
del tensor, а затемtorch.cuda.empty_cache(). - Используйте смешанную точность (
torch.cuda.amp), чтобы вдвое сократить расход памяти. - Для очень глубоких моделей используйте сохранение контрольных точек градиентов (gradient checkpointing).
Часть 7. Распространённые ошибки в AI и способы их обнаружения
Несоответствие форм
Самая частая ошибка. Тензор имеет форму [batch, features], тогда как модель ожидает [batch, channels, height, width].
def check_shapes(model, sample_input):
print(f"Input: {sample_input.shape}")
hooks = []
def make_hook(name):
def hook(module, inp, out):
in_shape = inp[0].shape if isinstance(inp, tuple) else inp.shape
out_shape = out.shape if hasattr(out, "shape") else type(out)
print(f" {name}: {in_shape} -> {out_shape}")
return hook
for name, module in model.named_modules():
hooks.append(module.register_forward_hook(make_hook(name)))
with torch.no_grad():
model(sample_input)
for h in hooks:
h.remove()
Запустите эту функцию один раз с примером пакета. Она покажет каждое преобразование формы в вашей модели.
Функция потерь со значением NaN
Значение NaN у функции потерь означает, что что-то вышло из-под контроля. Распространённые причины:
- Слишком высокая скорость обучения.
- Деление на ноль в пользовательской функции потерь.
- Логарифм нуля или отрицательного числа.
- Взрывающиеся градиенты в RNN.
def detect_nan(model, loss, step):
if torch.isnan(loss):
print(f"NaN loss at step {step}")
for name, param in model.named_parameters():
if param.grad is not None:
if torch.isnan(param.grad).any():
print(f" NaN gradient in {name}")
if torch.isinf(param.grad).any():
print(f" Inf gradient in {name}")
return True
return False
Утечка данных
Ваша модель достигает точности 99% на тестовом наборе. Звучит отлично. Но это ошибка.
def check_data_leakage(train_set, test_set, id_column="id"):
train_ids = set(train_set[id_column].tolist())
test_ids = set(test_set[id_column].tolist())
overlap = train_ids & test_ids
if overlap:
print(f"DATA LEAKAGE: {len(overlap)} samples in both train and test")
return True
return False
Также проверяйте временную утечку: использование будущих данных для предсказания прошлого. Перед разделением отсортируйте данные по временной метке.
Неверное устройство
Тензоры на разных устройствах (CPU и GPU) вызывают ошибки выполнения. Но иногда тензор незаметно остаётся на CPU, когда всё остальное находится на GPU, и обучение просто идёт медленно.
def check_devices(model, *tensors):
model_device = next(model.parameters()).device
print(f"Model device: {model_device}")
for i, t in enumerate(tensors):
if t.device != model_device:
print(f" WARNING: tensor {i} on {t.device}, model on {model_device}")
Часть 8. Основы TensorBoard
TensorBoard показывает, что происходит внутри обучения с течением времени.
pip install tensorboard
from torch.utils.tensorboard import SummaryWriter
writer = SummaryWriter("runs/experiment_1")
for step in range(num_steps):
loss = train_step(model, batch)
writer.add_scalar("loss/train", loss.item(), step)
writer.add_scalar("lr", optimizer.param_groups[0]["lr"], step)
if step % 100 == 0:
for name, param in model.named_parameters():
writer.add_histogram(f"weights/{name}", param, step)
if param.grad is not None:
writer.add_histogram(f"grads/{name}", param.grad, step)
writer.close()
Запустите TensorBoard:
tensorboard --logdir=runs
На что обратить внимание:
- Функция потерь не уменьшается: скорость обучения слишком низкая или есть проблема с архитектурой модели.
- Функция потерь сильно колеблется: скорость обучения слишком высокая.
- Функция потерь становится NaN: численная нестабильность (см. раздел о NaN выше).
- Потери на обучающей выборке уменьшаются, а на валидационной растут: переобучение.
- Гистограммы весов схлопываются к нулю: исчезающие градиенты.
- Гистограммы градиентов взрываются: необходимо ограничение градиентов.
Часть 9. Отладчик VS Code
Для интерактивной отладки настройте VS Code с помощью файла launch.json:
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug Training",
"type": "debugpy",
"request": "launch",
"program": "${file}",
"console": "integratedTerminal",
"justMyCode": false
}
]
}
Устанавливайте точки останова, щёлкая по полю редактора. Используйте панель Variables для проверки свойств тензоров. Debug Console позволяет выполнять произвольные выражения Python в ходе работы программы.
Это удобно для пошагового прохождения по конвейерам предварительной обработки данных, когда нужно видеть каждое преобразование.
Применение
Вот рабочий процесс отладки, который позволяет обнаружить большинство ошибок в AI:
- Перед обучением: запустите
check_shapesс примером пакета. Убедитесь, что размеры входов и выходов соответствуют ожиданиям. - На первых 10 шагах: примените
debug_printк функции потерь, выходам и градиентам. Убедитесь, что нигде нет NaN, а значения находятся в разумных диапазонах. - Во время обучения: журналируйте функцию потерь, скорость обучения и нормы градиентов. Используйте TensorBoard для визуализации.
- Когда что-то ломается: добавьте
breakpoint()в точке сбоя. Исследуйте тензоры в интерактивном режиме. - Для оценки производительности: измерьте время загрузки данных, прямого и обратного проходов. Если вы близки к OOM, профилируйте память.
Запуск
Запустите скрипт набора инструментов для отладки:
python phases/00-setup-and-tooling/12-debugging-and-profiling/code/debug_tools.py
В файле outputs/prompt-debug-ai-code.md приведён промпт, который помогает диагностировать ошибки, характерные для AI.
Упражнения
- Запустите
debug_tools.pyи изучите вывод каждого раздела. Измените фиктивную модель так, чтобы она создавала NaN (подсказка: выполните деление на ноль в прямом проходе), и посмотрите, как детектор обнаружит это значение. - Профилируйте цикл обучения с помощью
cProfileи определите самую медленную функцию. - Используйте
tracemalloc, чтобы найти строку конвейера загрузки данных, которая выделяет больше всего памяти. - Настройте TensorBoard для простого запуска обучения и определите, переобучается ли модель.
- Используйте
breakpoint()внутри цикла обучения. Потренируйтесь проверять формы тензоров, устройства и значения градиентов в командной строке отладчика.
Источник: Debugging and Profiling — оригинал Навигация: назад: 00.11 — Linux для AI · Фаза 0 — Настройка и инструменты · Полный каталог · далее: 01.01 — Интуитивное понимание линейной алгебры.