Фаза 00 · урок 12

Отладка и профилирование

Цель урока: AI-код ломается не так, как обычный код. Веб-приложение завершается сбоем и показывает трассировку стека. Неправильно настроенный цикл обучения работает 8 часов, тратит 200 долларов на время GPU и создаёт модель, которая для каждого…

Текущий релиз AlexBred.com: первые 100 уроков русскоязычной программы.

Курс
AI Engineering from Scratch
Фаза
Настройка и инструменты
Чтение
11 мин.
Проверено
Содержание урока
  1. Цели обучения
  2. Проблема
  3. Концепция
  4. Создание
  5. Часть 1. Отладка с помощью вывода (да, она работает)
  6. Часть 2. Отладчик Python (pdb и breakpoint)
  7. Часть 3. Журналирование в Python
  8. Часть 4. Измерение времени выполнения участков кода
  9. Часть 5. cProfile и line_profiler
  10. Часть 6. Профилирование памяти
  11. Часть 7. Распространённые ошибки в AI и способы их обнаружения
  12. Часть 8. Основы TensorBoard
  13. Часть 9. Отладчик VS Code
  14. Применение
  15. Запуск
  16. Упражнения

Худшие ошибки в 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, нехваткой памяти):

  1. Уменьшите размер пакета — это всегда нужно пробовать в первую очередь.
  2. Используйте torch.cuda.empty_cache(), чтобы освободить кэшированную память.
  3. Для крупных промежуточных результатов используйте del tensor, а затем torch.cuda.empty_cache().
  4. Используйте смешанную точность (torch.cuda.amp), чтобы вдвое сократить расход памяти.
  5. Для очень глубоких моделей используйте сохранение контрольных точек градиентов (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:

  1. Перед обучением: запустите check_shapes с примером пакета. Убедитесь, что размеры входов и выходов соответствуют ожиданиям.
  2. На первых 10 шагах: примените debug_print к функции потерь, выходам и градиентам. Убедитесь, что нигде нет NaN, а значения находятся в разумных диапазонах.
  3. Во время обучения: журналируйте функцию потерь, скорость обучения и нормы градиентов. Используйте TensorBoard для визуализации.
  4. Когда что-то ломается: добавьте breakpoint() в точке сбоя. Исследуйте тензоры в интерактивном режиме.
  5. Для оценки производительности: измерьте время загрузки данных, прямого и обратного проходов. Если вы близки к OOM, профилируйте память.

Запуск

Запустите скрипт набора инструментов для отладки:

python phases/00-setup-and-tooling/12-debugging-and-profiling/code/debug_tools.py

В файле outputs/prompt-debug-ai-code.md приведён промпт, который помогает диагностировать ошибки, характерные для AI.

Упражнения

  1. Запустите debug_tools.py и изучите вывод каждого раздела. Измените фиктивную модель так, чтобы она создавала NaN (подсказка: выполните деление на ноль в прямом проходе), и посмотрите, как детектор обнаружит это значение.
  2. Профилируйте цикл обучения с помощью cProfile и определите самую медленную функцию.
  3. Используйте tracemalloc, чтобы найти строку конвейера загрузки данных, которая выделяет больше всего памяти.
  4. Настройте TensorBoard для простого запуска обучения и определите, переобучается ли модель.
  5. Используйте breakpoint() внутри цикла обучения. Потренируйтесь проверять формы тензоров, устройства и значения градиентов в командной строке отладчика.

Источник: Debugging and Profiling — оригинал Навигация: назад: 00.11 — Linux для AI · Фаза 0 — Настройка и инструменты · Полный каталог · далее: 01.01 — Интуитивное понимание линейной алгебры.