FinCodeCow · Обзор AI-инфраструктуры

Профилирование, тюнинг и масштабирование PyTorch

Обзор тринадцатой главы книги Chris Fregly «AI Systems Performance Engineering» (O'Reilly, 2026) — от NVTX и Nsight до torch.compile, CUDA-графов, FSDP и HTA
Об источнике

Настоящий материал представляет собой обзор тринадцатой главы книги Chris Fregly, «AI Systems Performance Engineering: Optimizing Model Training and Inference Workloads with GPUs, CUDA, and PyTorch», O'Reilly Media, 2026 (Copyright © 2026 Flux Capacitor, LLC).

Прямые цитаты из книги выделены отдельными блоками с пометкой «Цитата из книги». Остальной текст — систематизированное изложение содержания главы, сопоставление с актуальными данными, проверка прогнозов, а также технические детали, выходящие за пределы исходного текста и основанные на официальной документации PyTorch и открытых источниках. Цитаты приводятся в ограниченном объёме в целях обзора и критического анализа.

§Контекст главы

Тринадцатая глава книги Fregly посвящена практическому профилированию, тюнингу и масштабированию рабочих нагрузок PyTorch на GPU. Глава охватывает полный цикл: от инструментов профилирования (PyTorch Profiler, Nsight Systems, Nsight Compute, Linux perf) и методологии выявления узких мест до конкретных техник оптимизации (torch.compile, CUDA-графы, тюнинг аллокатора памяти, activation checkpointing, оптимизация конвейера данных) и масштабирования на несколько GPU и узлов (DDP, FSDP, tensor parallel, pipeline parallel). Завершается глава обсуждением непрерывного бенчмаркинга и защиты производительности от регрессий.

Цитата из книги · Глава 13

«Конвейеры обучения и инференса AI-моделей могут страдать от узких мест на каждом уровне стека: от накладных расходов интерпретатора Python и простоев при загрузке данных на стороне CPU до неэффективного использования CUDA-ядер и конфликтов за память GPU. Чтобы оптимизировать систему результативно, необходимо профилировать её на нескольких уровнях, применяя инструменты, охватывающие весь стек целиком».

Изложение содержания

Глава построена как практическое руководство. Сначала рассматриваются инструменты профилирования и методология выявления узких мест на примере MoE-модели. Затем — конкретные техники оптимизации, каждая из которых сопровождается примерами кода и результатами замеров. Далее — масштабирование на несколько GPU и узлов. Завершается глава вопросами непрерывного бенчмаркинга и защиты производительности от регрессий. Ключевая идея: производительность PyTorch-нагрузок — это не одна «серебряная пуля», а систематическая работа по профилированию и устранению узких мест на каждом уровне стека.

• • •

INVTX-маркеры и инструменты профилирования

Цитата из книги · Глава 13

«NVIDIA Tools Extension (NVTX) — это лёгкий API для аннотирования диапазонов кода. NVTX-маркеры обозначают временные диапазоны на временной шкале профайлера и позволяют разным инструментам сопоставлять события одних и тех же фаз. Например, NVTX-диапазон "forward" появится и в трассировках PyTorch Profiler, и на временной шкале Nsight Systems. Это делает кросс-инструментальный анализ гораздо проще на разных уровнях стека».

NVTX-маркеры вставляются в код через CUDA C++, PyTorch или любую C++/Python-библиотеку, поддерживающую GPU NVIDIA. Многие библиотеки уже вставляют NVTX-маркеры за разработчика для критических участков кода, таких как train_step, forward, backward, optimizer_step. Но их можно добавлять и самостоятельно с помощью torch.profiler.record_function() и torch.cuda.nvtx.range_push().

ИнструментОбластьКлючевые возможностиТипичное применение
PyTorch Profiler (Kineto)Уровень операций (CPU/GPU)Поддержка NVTX, запись форм тензоров, статистика памяти, экспорт трассировок, выявление graph breaksДетальный разбор кода модели; медленные операции, накладные расходы запуска ядер
Nsight Systems (nsys)Системная временная шкала (CPU, GPU, ОС, I/O)Единая временная шкала потоков CPU и потоков GPU, интеграция с NVTX, поддержка многопроцессных запусковСквозной обзор конвейера; простои загрузчика, перекрытие CPU-GPU, синхронизация GPU
Nsight Compute (ncu)Анализ отдельных GPU-ядерАппаратные метрики по ядрам, корреляция с исходным кодом, Roofline-анализ, отчёты по occupancy и пропускной способностиГлубокий анализ ядер после выявления горячих точек; определение memory-bound или compute-bound
PyTorch Memory ProfilerИспользование памяти GPU по операциямВременная шкала снимков памяти, пиковая память по операциям, интеграция с torch.cuda.memory_stats()Диагностика фрагментации, неожиданно высокого потребления памяти
Linux perfПрофилирование CPU и системных событийСэмплирование циклов CPU, кэш-промахов, branch-misses, flame graphs, off-CPU анализНакладные расходы Python, простои при загрузке данных, проблемы планирования ОС
Holistic Trace Analysis (HTA)Визуализация трассировок распределённого обученияБраузерный анализатор Kineto-трассировок, агрегация трассировок нескольких воркеров, интеграция с PerfettoАнализ multi-GPU/multinode выполнения; поиск дисбаланса, простоев, проверка перекрытия коммуникаций с вычислениями
Chrome trace / PerfettoОфлайн-просмотр трассировокВеб-интерфейсы для стандартных форматов трассировок, расширенная фильтрация (Perfetto SQL), обмен трассировкамиПросмотр трассировок без специализированного ПО; дашборды и удалённая совместная работа
TorchEvalМетрики модели и логирование производительностиСтандартизированный API метрик (throughput, accuracy), простая интеграцияЛогирование пропускной способности и латентности рядом с точностью во время обучения и оценки
ExecuTorchМобильные и edge-устройстваЭкспорт модели, профилирование, отладка, анализ памяти, визуализацияЗапуск PyTorch-моделей и сбор метрик на ограниченных платформах
Стратегия выбора инструмента

Начните с PyTorch Profiler для быстрого обзора на уровне операций. Если видны пробелы на временной шкале или простои GPU — перейдите к Nsight Systems. Если конкретное ядро доминирует — углубитесь в Nsight Compute. Для CPU-узких мест — Linux perf. Для много-GPU — HTA. Для обмена результатами — Perfetto.

Итоги раздела I
Ключевое положение главы

NVTX-маркеры — единый язык аннотирования, который понимают все инструменты профилирования. Это делает кросс-инструментальный анализ сквозным.

Практическое следствие

Комбинация PyTorch Profiler (уровень операций) + Nsight Systems (системная временная шкала) + Nsight Compute (анализ ядер) покрывает все уровни стека.

Вывод для инженеров

Добавляйте NVTX-маркеры на верхнем уровне — для фаз итерации. Избыточные маркеры загрязняют временную шкалу и усложняют анализ.

• • •

IIПрофилирование PyTorch: пример MoE

В главе профилирование демонстрируется на примере модели трансформера со смесью экспертов (MoE). MoE — это LLM с несколькими экспертными слоями, где каждый эксперт представляет собой сеть прямого распространения. Маршрутизация токенов к экспертам управляется системой гейтинга. Архитектура богата на оптимизационные возможности: множество matmul среднего размера, кастомные ядра маршрутизации (dispatch/combine) и динамическое распределение токенов.

Использование PyTorch Profiler

Сначала настраивается модель и входные данные. Используется Hugging Face Transformers для загрузки модели и токенизатора, модель перемещается на GPU, подготавливается небольшой батч:

import torch
from transformers import AutoModelForCausalLM, AutoTokenizer

model_name = "..."
tokenizer = AutoTokenizer.from_pretrained(model_name)
device = torch.device("cuda")
model = AutoModelForCausalLM.from_pretrained(model_name).to(device)
optimizer = torch.optim.AdamW(model.parameters(), lr=1e-4, fused=True)

batch_size = 4
input_texts = ["MoEs are great."] * batch_size
enc = tokenizer(input_texts, return_tensors="pt", padding=True, truncation=True)
input_ids = enc.input_ids.to(device)
attention_mask = enc.attention_mask.to(device)
labels = input_ids.clone()

Чтобы не захватывать разовые затраты на настройку, модель прогревается несколькими итерациями перед профилированием. Это подготовит модель к анализу: скомпилирует JIT-ядра, заполнит кэши.

for _ in range(5):
    with torch.autocast(device_type="cuda", dtype=torch.bfloat16):
        outputs = model(input_ids, attention_mask=attention_mask, labels=labels)
        loss = outputs.loss
        loss.backward()
        optimizer.step()
        optimizer.zero_grad(set_to_none=True)

Теперь профилируется одна итерация обучения, высокоуровневые регионы помечаются через record_function и NVTX-диапазоны:

from torch import profiler

with profiler.profile(
    activities=[profiler.ProfilerActivity.CPU, profiler.ProfilerActivity.CUDA],
    record_shapes=True,
    profile_memory=True,
    with_stack=True,
    with_flops=True
) as prof:
    with profiler.record_function("train_step"):
        torch.cuda.nvtx.range_push("forward")
        with torch.autocast(device_type="cuda", dtype=torch.bfloat16):
            outputs = model(input_ids, attention_mask=attention_mask, labels=labels)
            loss = outputs.loss
        torch.cuda.nvtx.range_pop()

        torch.cuda.nvtx.range_push("backward")
        loss.backward()
        torch.cuda.nvtx.range_push("optimizer_step")
        optimizer.step()
        torch.cuda.nvtx.range_pop()
        optimizer.zero_grad()
        torch.cuda.nvtx.range_pop()

После выполнения можно изучить результаты на уровне операций, вызвав prof.key_averages().table(). Запрашивается топ-10 операций, отсортированных по self_cuda_time_total:

print(
    prof.key_averages()
    .table(
        sort_by="self_cuda_time_total",
        row_limit=10,
        fields=["self_cuda_time_total", "calls"]
    )
)
ОперацияSelf CUDA totalВызовов
aten::matmul43.00 мс128
aten::linear35.50 мс64
dispatch18.70 мс2
combine12.10 мс2
aten::layer_norm10.20 мс16
aten::softmax5.70 мс4
aten::scatter4.10 мс16
aten::gather3.60 мс16
aten::to2.90 мс8
aten::add_2.20 мс64
Ключевой вывод

Главное узкое место — matmul в экспертных FFN, за ним следуют ядра dispatch и combine. Вместе они определяют время итерации обучения. Это означает, что для ускорения необходимо оптимизировать именно эти операции — либо напрямую, либо уменьшая количество их вызовов.

Системное профилирование с Nsight Systems и NVTX

Вставленные NVTX-маркеры делают анализ временной шкалы в Nsight Systems простым. Чтобы агрегировать метрики по фазам, код профилируется через nsys со сводкой на основе NVTX:

nsys profile \
    --output=profile \
    --stats=true \
    -t cuda,nvtx \
    python train.py

nsys stats --report=nvtx_gpu_proj_sum profile.nsys-rep
NVTX-диапазонGPU-время (мс)Self GPU-время (мс)Экземпляров
train_step138.00.01
forward60.560.5130
backward58.358.3130
optimizer_step19.219.21

Механизм проекции NVTX — это не точная привязка ядра к диапазону, а статистическая оценка. CUPTI фиксирует, когда CPU вызвал push/pop NVTX-маркера, и какие GPU-ядра были запущены в этом временном окне на хосте.

Итоги раздела II
Ключевое положение главы

PyTorch Profiler выявляет matmul в экспертных FFN как главное узкое место MoE-модели, за ним следуют dispatch и combine.

Практическое следствие

Nsight Systems с NVTX-разметкой даёт сводку GPU-времени по фазам итерации. Механизм проекции NVTX — статистическая оценка, а не точная привязка.

Вывод для инженеров

Начинайте с PyTorch Profiler для выявления горячих операций. Затем используйте Nsight Systems для анализа временной шкалы. Только после этого переходите к Nsight Compute для глубокого анализа конкретных ядер.

• • •

IIIRoofline-анализ GEMM-ядер

Для глубокого анализа экспертного matmul вызывается CLI-профайлер Nsight Compute (ncu), нацеленный на конкретные GEMM-ядра по имени:

ncu \
    --target-processes all \
    --kernel-name-regex "matmul" \
    --metrics \
        gpu__time_duration.avg, \
        gpu__dram_throughput.avg.pct_of_peak_sustained_elapsed, \
        lts__throughput.avg.pct_of_peak_sustained_elapsed, \
        sm__sass_thread_inst_executed_op_fp32_pred_on.sum, \
        sm__warps_active.avg.pct_of_peak_sustained_active \
    --csv full \
    -o matmul_roofline_report \
    python train.py
Конфигурация ядра% пиковых FLOPS% пиковой пропускной способности памятиДостигнутая SM occupancyХарактеристика
Базовая50%70%60%Memory bound (простои на передаче данных)
Оптимизированная85%40%80%Compute bound (близко к аппаратному потолку)

В базовом запуске GEMM-ядро достигает лишь 50% пиковых FLOPS, 70% пиковой пропускной способности памяти и умеренной 60% SM occupancy. Это недостаточно для полного скрытия задержек памяти. Метрики указывают на memory-bound ядро. После оптимизации (слияние ядер, рост арифметической интенсивности, снижение перемещения памяти) пиковые FLOPS растут до 85%, пропускная способность памяти падает до 40%, а occupancy растёт до 80%. Ядро сдвигается из memory-bound в compute-bound.

Универсальной целевой occupancy не существует

Многие высокопроизводительные ядра достигают полного скрытия задержек при 25–50% достигнутой occupancy. Используйте метрики occupancy Nsight Compute вместе с разбивкой причин простоев и числом готовых варпов на активный цикл, чтобы судить, действительно ли большее occupancy снизит простои.

Итоги раздела III
Ключевое положение главы

Roofline-анализ Nsight Compute показывает, что GEMM-ядро MoE-модели изначально memory bound (50% FLOPS, 70% памяти). После оптимизаций оно становится compute bound (85% FLOPS, 40% памяти).

Практическое следствие

Нет универсальной целевой occupancy. Для каждого ядра существует свой оптимум, определяемый балансом latency hiding и ресурсов.

Вывод для инженеров

Используйте Roofline для диагностики: если ядро ниже наклонной линии — оптимизируйте доступ к памяти; если ниже горизонтальной — оптимизируйте вычисления.

• • •

IVCPU-профилирование через Linux perf

Для более целостной картины того, где тратится время — на хосте и на устройстве — применяется Linux perf для анализа циклов CPU, кэш-промахов, branch-misses и т. д. Затем эти данные используются для серии оптимизаций.

perf stat -e \
    cycles,instructions,cache-misses,branch-misses \
    python train.py
perf record -F 2000 -g --call-graph dwarf -o perf.data \
    python train.py

perf report --stdio -n -g -i perf.data

На основе профиля выявляются следующие узкие места и меры их устранения:

  • Избыточные накладные расходы Python (45% в py::forward) — использовать torch.compile для устранения накладных расходов интерпретатора и слияния Python-операций в оптимизированный CUDA-код.
  • Крупное горячее место matmul (20.5%) — PyTorch Compiler или кастомное CUDA C++-ядро для обхода Python и прямого использования оптимизированного CUDA-кода.
  • Простои загрузки данных (10.2%) — увеличить num_workers DataLoader, включить persistent_workers=True.
  • Накладные расходы синхронизации градиентов (8.7%) — увеличить размер бакета градиентов в DDP с 25 МБ до 50 МБ.
  • Узкое место I/O (5.3% в read) — pinned memory, non-blocking копии, контейнерные форматы (Arrow, WebDataset, TFRecord).
Итоги раздела IV
Ключевое положение главы

Linux perf выявляет 45% накладных расходов Python и значительные затраты на синхронизацию градиентов (8.7%) и загрузку данных (10.2%).

Практическое следствие

Для CPU-узких мест используйте Linux perf. Для GPU-узких мест — Nsight. Комбинация даёт полную картину.

Вывод для инженеров

Применяйте оптимизации по одной, измеряя эффект каждой. Это позволяет изолировать влияние изменений и избежать неожиданных взаимодействий.

• • •

VPyTorch Compiler (torch.compile)

Цитата из книги · Глава 13

«Одна из самых быстрых побед в PyTorch — использовать компилятор через torch.compile(). Стек компилятора включает TorchDynamo, AOT Autograd и TorchInductor, которые захватывают графы, сливают операции и генерируют высокопроизводительный код для целевого бэкенда».

Использовать компилятор с настройками по умолчанию просто: model = torch.compile(model). Под капотом TorchDynamo трассирует Python-код, AOT Autograd захватывает backward-проход, а TorchInductor, опираясь на OpenAI Triton для генерации GPU-ядер, автоматически создаёт эффективные fused-ядра.

compiled_model = torch.compile(model, mode="reduce-overhead")

В этом случае eager-режим занимает примерно 248 мс на итерацию, а скомпилированный — около 173 мс. Ускорение примерно на 30% в основном от объединения множества мелких GPU-ядер.

РежимОписаниеВремя компиляцииДоп. памятьКлючевые особенности
defaultСбалансированные оптимизацииНизкое–среднееНетБазовое слияние, простой autotuning
reduce-overheadСнижает накладные расходы на итерациюСреднееДа (кэш workspace)Использует CUDA Graphs для устранения накладных расходов запуска
max-autotuneМаксимизирует runtime-производительностьВысокоеВозможноАгрессивный autotuning Triton, включает CUDA Graphs
max-autotune-no-cudagraphsТо же, но без CUDA-графовВысокоеНетДля динамических форм или отладки

Схема 1. Compiled vs Noncompiled Regions

Профиль PyTorch: скомпилированные и eager-участки графа

compiled
compiled
eager
compiled
eager
compiled
Compiled Function Eager fallback (graph break)
Циановые сегменты — скомпилированные, янтарные — eager-откаты
torch.compile. Диапазоны Compiled Function на временной шкале показывают, какие участки графа захвачены TorchDynamo и ускорены TorchInductor, а где произошёл graph break и модель вернулась к eager-исполнению.

Профилирование и отладка компилятора

Когда torch.compile не даёт ожидаемого ускорения, используются инструменты диагностики. torch._dynamo.explain(model) выводит отчёт о graph breaks, причинах их возникновения и о том, какие части модели не были захвачены TorchDynamo. Переменная TORCH_LOGS="+dynamo,+inductor" включает подробное логирование компилятора.

import torch._dynamo as dynamo

def toy_example(a, b):
    x = a / (torch.abs(a) + 1)
    print("woo")
    if b.sum() < 0:
        b = -b
    return x * b

explanation = dynamo.explain(toy_example)(torch.randn(10), torch.randn(10))
print(explanation)

Отчёт покажет количество графов, количество graph breaks и причины их возникновения. Для минимизации graph breaks используется torch.where() вместо Python-условий, а также torch.compiler.set_stance("fail_on_recompile") для принудительного выброса ошибки при обнаружении graph break.

Итоги раздела V
Ключевое положение главы

torch.compile даёт ~30% ускорения за счёт слияния мелких GPU-ядер. Режим reduce-overhead дополнительно использует CUDA Graphs.

Практическое следствие

Для диагностики проблем компилятора используются torch._dynamo.explain() и TORCH_LOGS. Graph breaks выявляются и устраняются заменой Python-условий на тензорные операции.

Вывод для инженеров

Не все graph breaks вредны: для DDP и FSDP они ожидаемы и необходимы для перекрытия коммуникаций с вычислениями.

• • •

VIОптимизированные механизмы внимания

Трансформерные модели значительную часть времени проводят в механизмах внимания. PyTorch предоставляет несколько уровней оптимизации:

  • Scaled Dot-Product Attention (SDPA) — torch.nn.functional.scaled_dot_product_attention автоматически использует самое быстрое доступное ядро (FlashAttention, memory-efficient или cuDNN) в зависимости от форм тензоров и типов данных.
  • FlexAttention — компиляторный подход для кастомных паттернов разреженности (block-sparse, sliding-window). Позволяет описывать маску как функцию, а компилятор генерирует оптимизированные ядра.
  • FlexDecoding — оптимизация фазы декодирования для LLM. Компилирует специализированное ядро для декодирования (Q_len=1) с учётом растущего KV-кэша.
  • Context Parallel — шардирование внимания по длине последовательности для обработки сверхдлинных контекстов.

Схема 2. FlexAttention: пользовательские варианты attention

JIT-компиляция масок (block-sparse, sliding-window, causal) без ручных CUDA-ядер

Causal, sliding-window ±2, block-sparse 2×2
FlexAttention. Вместо ручных CUDA-ядер паттерн разреженности описывается функцией-маской, а компилятор генерирует оптимизированные ядра под конкретный шаблон.
Итоги раздела VI
Ключевое положение главы

SDPA автоматически выбирает лучший бэкенд. FlexAttention позволяет описывать кастомные паттерны разреженности без ручных CUDA-ядер.

Практическое следствие

FlexAttention с nested jagged tensors (NJT) устраняет паддинг для последовательностей переменной длины.

Вывод для инженеров

Проверяйте, какой бэкенд SDPA фактически используется, и при необходимости указывайте его явно через torch.nn.attention.sdpa_kernel.

• • •

VIIКонкурентность с CUDA-потоками

CUDA-потоки обеспечивают конкурентность и перекрытие операций на GPU. По умолчанию PyTorch планирует все операции на стандартном потоке устройства (stream 0) последовательно. Однако многие задачи независимы, и GPU может выполнять их параллельно с несколькими потоками.

device = 'cuda'
transfer_stream = torch.cuda.Stream(device=device)
compute_stream = torch.cuda.default_stream(device=device)

dataloader_iter = iter(dataloader)

first_batch = next(dataloader_iter, None)
if first_batch:
    with torch.cuda.stream(transfer_stream):
        next_inputs, next_labels = (
            first_batch[0].to(device, non_blocking=True),
            first_batch[1].to(device, non_blocking=True),
        )

for _ in range(len(dataloader)):
    compute_stream.wait_stream(transfer_stream)
    inputs, labels = next_inputs, next_labels

    batch = next(dataloader_iter, None)
    if batch:
        with torch.cuda.stream(transfer_stream):
            next_inputs, next_labels = (
                batch[0].to(device, non_blocking=True),
                batch[1].to(device, non_blocking=True),
            )

    with torch.cuda.stream(compute_stream):
        outputs = model(inputs)
        loss = loss_fn(outputs, labels)
        loss.backward()
        optimizer.step()
        optimizer.zero_grad(set_to_none=True)

Схема 3. Перекрытие compute и передачи данных

Отдельные CUDA-потоки: один готовит следующую партию, другой считает текущую

compute_stream
batch N
batch N+1
batch N+2
batch N+3
transfer_stream
H2D N+1
H2D N+2
H2D N+3
H2D N+4
Янтарный — вычисления, циановый — cudaMemcpyAsync в pinned memory
Перекрытие потоков. Пока compute_stream обрабатывает текущий батч, transfer_stream уже готовит следующий. Это скрывает задержку H2D-копий за вычислениями.
Частая ошибка

Вызов print() на CUDA-тензоре вызывает неявную синхронизацию (torch.cuda.synchronize()), которая останавливает все потоки. В production-коде следует избегать любых операций, читающих значения GPU-тензоров на CPU, без явной необходимости.

Синхронизация потоков через события

event = torch.cuda.Event(enable_timing=False)

with torch.cuda.stream(stream1):
    kernel_launch(...)
    event.record()

stream2.wait_event(event)

with torch.cuda.stream(stream2):
    other_kernel_launch(...)
Итоги раздела VII
Ключевое положение главы

CUDA-потоки позволяют перекрывать передачу данных и вычисления. Отдельный transfer_stream готовит следующий батч, пока compute_stream обрабатывает текущий.

Практическое следствие

События CUDA обеспечивают точечную синхронизацию без глобальной блокировки. torch.cuda.Event(enable_timing=False) снижает накладные расходы.

Вывод для инженеров

Для профилирования многопоточных конвейеров используйте Nsight Systems. Проверяйте, что non_blocking=True и pinned memory используются корректно.

• • •

VIIICUDA-графы для снижения накладных расходов

CUDA-графы устраняют накладные расходы на запуск на итерацию, снижают накладные расходы CPU и ликвидируют крошечные простои между ядрами. PyTorch предоставляет API torch.cuda.CUDAGraph для захвата и воспроизведения CUDA-графов.

g = torch.cuda.CUDAGraph()
capture_stream = torch.cuda.Stream()

static_input = torch.randn(batch_shape, device='cuda')
static_output = torch.empty(output_shape, device='cuda')

with torch.cuda.stream(capture_stream):
    tmp = model(static_input)
    static_output.copy_(tmp)

capture_stream.synchronize()

with torch.cuda.graph(g, stream=capture_stream):
    tmp = model(static_input)
    static_output.copy_(tmp)

capture_stream.synchronize()
Правила захвата CUDA-графа

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

МетрикаДо CUDA GraphsПосле CUDA Graphs
CPU-вызовов на 100 итераций300 отдельных запусков ядер100 воспроизведений графа
Синхронизаций хоста300 cudaDeviceSynchronize0
Простой GPU между ядрами~3 мкс на итерацию0 мкс (непрерывное выполнение)
Сквозная задержка итерации~1.00 мс~0.75 мс (25% быстрее)

CUDA Graph Trees используются torch.compile в режиме reduce-overhead для компиляции и кэширования отдельных статических графов под каждую форму входа. Рекомендуется поддерживать формы входов максимально согласованными между итерациями.

Итоги раздела VIII
Ключевое положение главы

CUDA-графы устраняют накладные расходы на запуск ядер, снижая задержку итерации на 25%. Графы перезаписывают выходной буфер, поэтому для сохранения результатов требуется клонирование.

Практическое следствие

CUDA Graph Trees в torch.compile(mode="reduce-overhead") кэшируют статические графы под каждую форму входа. Чем меньше различных форм — тем эффективнее.

Вывод для инженеров

Для стабильных форм и длительных нагрузок CUDA-графы дают значительный выигрыш. Для динамических форм используйте max-autotune-no-cudagraphs.

• • •

IXТюнинг CUDA Memory Allocator

PyTorch использует кэширующий аллокатор памяти для CUDA. По умолчанию он адаптируется к паттернам аллокации, разбивая и переиспользуя блоки GPU-памяти. Ключевые параметры настройки через переменные окружения:

  • PYTORCH_CUDA_ALLOC_CONF=max_split_size_mb:N — максимальный размер блока, который аллокатор может разделять. Указывает аллокатору сохранять большие свободные блоки (до 256 МБ) вместо постоянного дробления на мелкие фрагменты.
  • PYTORCH_CUDA_ALLOC_CONF=roundup_power2_divisions:[N:M,...] — группировка запросов в фиксированные бакеты. Например, в диапазоне 512–1024 МБ с 512:2 запрос в 600 МБ округляется до 768 МБ. Это снижает фрагментацию и увеличивает переиспользование.
  • PYTORCH_CUDA_ALLOC_CONF=backend:cudaMallocAsync — асинхронный аллокатор CUDA. Устраняет глобальные синхронизации при аллокации и освобождении памяти.

Схема 4. PyTorch Memory Profiler: 3 итерации forward + backward

Пиковые и кумулятивные аллокации GPU-памяти по операциям

time → memory iter 1 iter 2 iter 3 peak allocated reserved (cache) peak
Циан — allocated, янтарь — reserved (cache), пунктир — пик
Memory profiler. Если mem_get_info() показывает мало свободной памяти, а memory_stats() — что сумма аллоцированных тензоров значительно меньше общей памяти, фрагментация нарастает. В этом случае roundup_power2_divisions — ключевой инструмент.
export PYTORCH_CUDA_ALLOC_CONF=\
max_split_size_mb:256,\
roundup_power2_divisions:[256:1,512:2,1024:4,:8],\
backend:cudaMallocAsync
Итоги раздела IX
Ключевое положение главы

Тюнинг аллокатора через max_split_size_mb и roundup_power2_divisions снижает фрагментацию и увеличивает переиспользование памяти.

Практическое следствие

Бэкенд cudaMallocAsync устраняет глобальные синхронизации при аллокации. Мониторинг фрагментации через memory_reserved() и memory_allocated().

Вывод для инженеров

Для длительных нагрузок с переменными размерами тензоров настройка аллокатора даёт стабильное потребление памяти без OOM.

• • •

XActivation Checkpointing для экономии памяти

Для очень больших моделей activation checkpointing (gradient checkpointing) необходим для управления памятью. Вместо сохранения всех активаций forward-прохода сохраняются только входы каждого блока. При backward-проходе активации пересчитываются заново.

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

import torch
from torch.utils.checkpoint import checkpoint

class TransformerBlock(torch.nn.Module):
    def forward(self, x):
        return checkpoint(self._forward, x, use_reentrant=False)

    def _forward(self, x):
        return self.attention(self.mlp(x))
Селективный checkpointing

Селективный activation checkpointing позволяет выбирать, какие именно слои пересчитывать. Например, можно чекпоинтить каждый второй слой трансформера. Это даёт более гибкий компромисс между памятью и вычислениями.

Итоги раздела X
Ключевое положение главы

Activation checkpointing обменивает вычисления на память. Селективный подход позволяет выбрать оптимальный компромисс.

Практическое следствие

Для MoE-моделей с большим числом экспертов checkpointing блоков трансформера даёт значительную экономию памяти.

Вывод для инженеров

Checkpointing обязателен для моделей, которые иначе не помещаются в память. Он позволяет использовать больший batch size.

• • •

XIОптимизация входного конвейера данных

Частая причина неэффективности — GPU простаивают в ожидании данных. DataLoader PyTorch поддерживает запуск нескольких воркеров для параллельной загрузки и предобработки данных, пока обучается модель.

  • pin_memory=True — передача через page-locked память. Ускоряет копирование CPU→GPU.
  • non_blocking=True и persistent_workers=True — для скрытия копирования и избежания пересоздания воркеров между эпохами.
  • prefetch_factor — сколько батчей каждый воркер предзагружает заранее. По умолчанию 2.
loader = DataLoader(
    dataset,
    batch_size=B,
    num_workers=8,
    pin_memory=True,
    persistent_workers=True,
    prefetch_factor=4,
)
Эвристика для num_workers

Для CPU-связанных конвейеров данных часто 2–8 воркеров на GPU достаточно. Не следуйте правилу «один воркер на CPU» вслепую — это часто даёт слишком много воркеров, конкурирующих за GIL.

Итоги раздела XI
Ключевое положение главы

pin_memory=True, persistent_workers=True и prefetch_factor — три ключевых параметра для предотвращения простоев GPU в ожидании данных.

Практическое следствие

Для CPU-связанных пайплайнов 2–8 воркеров на GPU достаточно. Избыток воркеров конкурирует за GIL.

Вывод для инженеров

Профилируйте DataLoader изолированно: замеряйте время на 100 батчей без GPU-работы. Сравнивайте с целевым временем итерации.

• • •

XIIFSDP: автоматический checkpointing и offloading

FSDP в PyTorch — стратегия распределённого параллелизма, шардирующая параметры модели, градиенты и активации между GPU. Технически FSDP реализует стратегию ZeRO Stage-3 для шардирования состояний модели между GPU.

import torch
import torch.nn as nn
import torch.distributed as dist
from torch.distributed.fsdp import (
    FullyShardedDataParallel as FSDP,
    CPUOffload,
    ShardingStrategy,
    MixedPrecision
)
from torch.distributed.fsdp.wrap import transformer_auto_wrap_policy

dist.init_process_group("nccl")
torch.cuda.set_device(dist.get_rank() % torch.cuda.device_count())

class MyModel(nn.Module):
    def __init__(self):
        super().__init__()
        self.layers = nn.Sequential(
            nn.Linear(4096, 4096),
            nn.ReLU(),
            nn.Linear(4096, 4096),
        )

    def forward(self, x):
        return self.layers(x)

model = MyModel().cuda()

auto_wrap_policy = transformer_auto_wrap_policy(
    model,
    min_num_params=1e8,
)

fsdp_model = FSDP(
    model,
    auto_wrap_policy=auto_wrap_policy,
    sharding_strategy=ShardingStrategy.FULL_SHARD,
    use_orig_params=True,
    cpu_offload=CPUOffload(offload_params=True, pin_memory=True),
)

Когда сеть медленная — или доступно всего несколько GPU — можно использовать ShardingStrategy.SHARD_GRAD_OP (ZeRO Stage-2) для шардирования только градиентов и состояния оптимизатора. На суперчиповых системах Grace-Blackwell доступны SuperOffload и GraceAdam.

Схема 5. FSDP (ZeRO Stage-3): шардирование модели по GPU

Параметры, градиенты и состояния оптимизатора разбиваются между 4 GPU

Full Model Parameters (replicated) Gradients (replicated) Optimizer state (replicated) FSDP shard GPU 0 params 1/4 grads 1/4 optim 1/4 GPU 1 GPU 2 GPU 3 params 1/4 grads 1/4 optim 1/4 each GPU 1/N
Full Model → 4 шарда: params, grads и optim разбиваются между GPU
FSDP = ZeRO Stage-3. Параметры, градиенты и состояния оптимизатора шардируются между GPU. Forward требует all-gather, backward — reduce-scatter. Шардирование на уровне блока трансформера даёт лучшую производительность и память.
Итоги раздела XII
Ключевое положение главы

FSDP реализует ZeRO Stage-3: параметры, градиенты и состояния оптимизатора шардируются между GPU. Шардирование на уровне блока трансформера даёт максимальную эффективность.

Практическое следствие

ShardingStrategy.FULL_SHARD (ZeRO-3) для минимальной памяти; SHARD_GRAD_OP (ZeRO-2) когда сеть медленная; HYBRID_SHARD для баланса памяти и коммуникаций.

Вывод для инженеров

Автоматический checkpointing и offloading через FSDP снижают потребление памяти без boilerplate-кода.

• • •

XIIIМасштабирование с PyTorch Distributed

DDP с torch.compile

При использовании DistributedDataParallel, синхронизирующего градиенты между GPU через all-reduce, PyTorch автоматически создаёт graph breaks в точках синхронизации. Дизайн PyTorch — компилировать backward-вычисления каждого бакета как отдельный граф, чтобы между этими графами выполнять all-reduce.

FSDP с torch.compile

Рекомендуется оборачивать каждый блок трансформера — а не просто отдельный низкоуровневый слой — в собственный FSDP-модуль с use_orig_params=True. Так TorchDynamo вставит graph break на каждой границе шарда, где нужна коммуникация.

Tensor и pipeline parallel с torch.compile

Tensor parallel и pipeline parallel ортогональны torch.compile. Компилятор PyTorch в основном фокусируется на оптимизации вычислений внутри каждого сегмента. Рекомендуется использовать torch.compile для оптимизации вычислений внутри каждого слоя или бакета.

TorchTitan, AsyncTP, AutoParallel и SimpleFSDP

TorchTitan — набор референсных реализаций для крупномасштабного обучения. SimpleFSDP перереализует FSDP дружественным к torch.compile способом через DTensor. В экспериментах TorchTitan SimpleFSDP снизил потребление памяти до ~28%, а пропускную способность улучшил до ~69% по сравнению с традиционным FSDP2 eager-путём.

Итоги раздела XIII
Ключевое положение главы

Graph breaks в DDP и FSDP — ожидаемы и необходимы для перекрытия коммуникаций с вычислениями. Компиляция каждого бакета отдельно — это дизайн, а не баг.

Практическое следствие

SimpleFSDP снижает память до ~28% и увеличивает пропускную способность до ~69% относительно FSDP2 eager (максимум на 2D-параллелизме; средний выигрыш — ~29% для 8B и ~28% для 70B).

Вывод для инженеров

Используйте torch.compile для оптимизации вычислений внутри каждого сегмента, но доверяйте distributed-стратегии управление коммуникациями.

• • •

XIVМного-GPU профилирование с HTA

При масштабировании на множество GPU управление трассировками может стать громоздким. HTA от Meta помогает объединять и визуализировать многорабочие трассировки. HTA поглощает JSON-трассировки от torch.profiler с каждой GPU/ранга и представляет единый временной график.

С HTA можно увидеть трассировки всех 8 GPU, выровненные по времени. NVTX-маркеры с каждого ранга выравниваются и визуализируются. Так можно заметить, что ранг 0 входит в backward-проход в момент T, а ранг 1 — только в T+1, возможно, потому что ранг 1 ждал ранг 0 в all-reduce.

from torch.profiler import profile, schedule, tensorboard_trace_handler

with profile(
    activities=[ProfilerActivity.CPU, ProfilerActivity.CUDA],
    schedule=schedule(wait=1, warmup=1, active=3, repeat=1),
    on_trace_ready=tensorboard_trace_handler('./log'),
    record_shapes=True,
    profile_memory=True,
    with_stack=True
) as prof:
    for step, batch in enumerate(dataloader):
        train_step(batch)
        prof.step()
Итоги раздела XIV
Ключевое положение главы

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

Практическое следствие

Используйте torch.profiler.schedule для записи трассировок на нескольких итерациях без перегрузки.

Вывод для инженеров

Для multinode-профилирования HTA — стандартный инструмент. TensorBoard trace viewer для этой задачи устарел.

• • •

XVCI и бенчмаркинг

После применения всех оптимизаций критически важно поддерживать их и непрерывно проверять на регрессии производительности. Следует настроить простую CI регрессии производительности с TorchBench и GitHub Actions для автоматического отлова замедлений.

- name: Run MoE benchmark
  run: |
    torchbench run --model moe --iters 10 --batch-size 4 --json results.json

- name: Compare throughput
  run: |
    python scripts/compare_perf.py baseline.json results.json

Схема 6. PyTorch HUD Performance Dashboard

Публичный дашборд nightly-бенчмарков: throughput, память, регрессии по коммитам

Benchmarks → vLLM / Llama-3.1-8B

last 7 days NVIDIA Blackwell

Throughput (tokens/sec)

Per-model FLOPS utilization

Compile time
12.4 s
−18% за 30 дней
Peak memory
14.2 GB
+2% регрессия
Tokens/sec
2 340
+9%
Regression
1
vLLM #a1f2c9
ALERT Commit a1f2c9 — throughput drop −5.2% on Llama-3.1-8B (sustained 3 runs). Recommended: revert or bisect.
PyTorch HUD: nightly-бенчмарки, регрессии, KPI
Continuous benchmarking. Ночные бенчмарки и публичный HUD дашборд позволяют автоматически ловить регрессии производительности. В CI сравнивайте пропускную способность с базовым числом и проваливайте сборку при устойчивом падении ≥5%.
Итоги раздела XV
Ключевое положение главы

CI с TorchBench и GitHub Actions автоматически выявляет регрессии производительности. Публичный PyTorch HUD — источник внешних сигналов.

Практическое следствие

Устойчивое падение пропускной способности ≥5% на трёх прогонах — сигнал для расследования.

Вывод для инженеров

Performance regression testing должен быть частью CI/CD. Без него оптимизации деградируют со временем.

• • •

XVIПроверка прогнозов

Подтверждено (распространение) Прогноз 1: torch.compile становится стандартом

«torch.compile даёт ~30% ускорения за счёт слияния мелких GPU-ядер».

Подтверждено в части распространения; цифра ~30% — книжная оценка. К 2026 году torch.compile стал стандартной практикой для production-нагрузок. PyTorch 2.x активно развивает компилятор, а TorchInductor генерирует высокооптимизированные ядра для NVIDIA GPU. Режимы reduce-overhead и max-autotune используются в production-кластерах. Реальный выигрыш от fusion сильно зависит от модели и режима компиляции: в публикациях Meta он обычно составляет 10–30%.

Подтверждено Прогноз 2: FSDP вытесняет DDP для больших моделей

«FSDP реализует ZeRO Stage-3 для шардирования состояний модели между GPU».

Подтверждено. FSDP стал стандартом для обучения моделей, не помещающихся в память одного GPU. SimpleFSDP и TorchTitan демонстрируют дальнейшее развитие подхода. HYBRID_SHARD и SHARD_GRAD_OP используются для баланса памяти и коммуникаций.

Подтверждено (распространение) Прогноз 3: CUDA-графы снижают накладные расходы

«CUDA-графы устраняют накладные расходы на запуск ядер, снижая задержку итерации на 25%».

Подтверждено в части распространения; цифра 25% — книжная оценка. CUDA Graphs интегрированы в torch.compile(mode="reduce-overhead") и активно используются в inference-движках (vLLM, TensorRT-LLM). Ограничение — статические формы — остаётся, но CUDA Graph Trees решают эту проблему для дискретных наборов форм. Реальный выигрыш зависит от доли launch overhead в общем времени итерации и обычно составляет 10–30%.

Подтверждено Прогноз 4: HTA становится стандартом multi-GPU профилирования

«HTA агрегирует трассировки всех рангов в единый временной график».

Подтверждено. HTA активно используется в Meta и других организациях для анализа распределённого обучения. TensorBoard trace viewer устарел, и HTA с Perfetto стали стандартом. CUPTI Counter Analysis позволяет получать GPU performance counters.

Частично подтверждено Прогноз 5: Селективный activation checkpointing становится нормой

«Не обязательно чекпоинтить всё. Можно сфокусироваться на самых больших слоях».

Частично подтверждено. Селективный checkpointing поддерживается в TorchTitan и torchtune. PyTorch предоставляет API для выбора конкретных слоёв. Однако универсальной стратегии нет — выбор зависит от архитектуры модели и доступной памяти.

• • •

XVIIДополнительные замечания

Дополнительные замечания

Memory profiler и Mosaic. В главе рассматривается стандартный PyTorch memory profiler. Дополнительно Meta разработала Mosaic — инструмент для анализа снимков памяти, который позволяет выявлять утечки и фрагментацию на уровне отдельных аллокаций. Mosaic интегрируется с torch.cuda.memory._dump_snapshot().

CUDA Graph Trees и динамические формы. Хотя глава упоминает CUDA Graph Trees, механизм их работы с динамическими формами заслуживает отдельного внимания. Trees классифицируют каждое хранилище, живое на момент захвата, как приватную аллокацию пула или известный статический вход. Это позволяет переиспользовать графы для нескольких форм без полного перезахвата.

PyTorch HUD и регрессии. Публичный PyTorch HUD (hud.pytorch.org) отслеживает производительность torch.compile относительно eager-режима. Однако недавние проблемы с миграцией на AWS привели к периодам ненадёжности дашборда. Для критичных workloads рекомендуется собственный CI с бенчмарками.

NVIDIA Blackwell и профилирование. Для новых архитектур Blackwell (B200/B300) требуются обновлённые версии Nsight Systems и Nsight Compute. PyTorch 2.x поддерживает профилирование на Blackwell, но некоторые метрики (например, TMEM utilization) могут быть недоступны в старых версиях инструментов.

MLPerf Logging. Для стандартизированного логирования производительности в распределённых нагрузках используется библиотека mlperf_logging. Она позволяет записывать ключевые события (начало обучения, конец эпохи, финальная точность) в формате, совместимом с MLPerf. Это упрощает сравнение производительности между конфигурациями.

• • •

XVIIIКлючевые положения

  1. NVTX-маркеры — единый язык профилирования. PyTorch Profiler, Nsight Systems и Nsight Compute понимают NVTX. Это делает кросс-инструментальный анализ сквозным.
  2. Профилирование выявляет узкие места. В MoE-модели главное узкое место — matmul в экспертных FFN, за ним — dispatch и combine.
  3. Roofline-анализ показывает memory-bound vs compute-bound. Базовая GEMM-ядро MoE — memory bound (50% FLOPS, 70% памяти). После оптимизаций — compute bound (85% FLOPS, 40% памяти).
  4. Linux perf дополняет GPU-профилирование. 45% накладных расходов Python, 8.7% — на синхронизацию градиентов, 10.2% — на загрузку данных.
  5. torch.compile даёт ~30% ускорения. Слияние мелких GPU-ядер и устранение накладных расходов Python. Режим reduce-overhead использует CUDA Graphs.
  6. CUDA-графы снижают задержку итерации на 25%. Устраняют накладные расходы запуска ядер. CUDA Graph Trees кэшируют графы под каждую форму входа.
  7. Аллокатор памяти тюнится через max_split_size_mb, roundup_power2_divisions и backend:cudaMallocAsync.
  8. Activation checkpointing обменивает вычисления на память. Селективный подход позволяет выбрать оптимальный компромисс.
  9. DataLoader: три ключевых параметра — pin_memory=True, persistent_workers=True, prefetch_factor.
  10. FSDP реализует ZeRO Stage-3. Шардирование на уровне блока трансформера даёт максимальную эффективность. SimpleFSDP снижает память до ~28% и увеличивает пропускную способность до ~69%.
  11. Graph breaks в DDP и FSDP ожидаемы. Компиляция каждого бакета отдельно — дизайн, а не баг.
  12. HTA агрегирует трассировки всех рангов. Позволяет выявлять дисбаланс и синхронизационные задержки в multi-GPU обучении.
  13. CI с TorchBench выявляет регрессии. Устойчивое падение пропускной способности ≥5% на трёх прогонах — сигнал для расследования.
  14. PyTorch HUD — публичный дашборд. Ночные бенчмарки и отслеживание регрессий по коммитам.
Общий итог обзора

Тринадцатая глава книги Fregly — систематическое руководство по профилированию, тюнингу и масштабированию PyTorch. Ключевая идея: производительность — это не одна «серебряная пуля», а непрерывный цикл профилирования и устранения узких мест на каждом уровне стека. От NVTX-маркеров и Nsight до torch.compile, CUDA-графов, FSDP и HTA — каждый инструмент решает свою задачу. Для инженера это означает, что понимание методологии профилирования и принципов работы каждого инструмента важнее, чем знание конкретных цифр текущего поколения.

Проверка прогнозов показывает: книга оказалась точной в части распространения технологий. torch.compile стал стандартом; FSDP вытеснил DDP для больших моделей; CUDA-графы снижают накладные расходы; HTA заменил TensorBoard trace viewer. Конкретные проценты ускорения (30%, 25%) — книжные оценки, реальный выигрыш зависит от модели и режима. Главный вывод остаётся неизменным: инвестиции в методологию профилирования обеспечивают адаптацию к любому стеку.

Источники: Chris Fregly, «AI Systems Performance Engineering» (O'Reilly, 2026), Глава 13 — цитаты приводятся в ограниченном объёме в целях обзора и критического анализа. Официальная документация PyTorch (torch.profiler, torch.compile, FSDP, CUDA Graphs); NVIDIA Nsight Systems/Compute documentation; Holistic Trace Analysis (Meta); MLPerf Training/Inference v5.0; TorchBench; PyTorch HUD.

FinCodeCow · AI Infrastructure Review

Profiling, Tuning and Scaling PyTorch

A review of Chapter 13 of Chris Fregly's «AI Systems Performance Engineering» (O'Reilly, 2026) — from NVTX and Nsight to torch.compile, CUDA graphs, FSDP and HTA
About the source

This material is a review of Chapter 13 of Chris Fregly, «AI Systems Performance Engineering: Optimizing Model Training and Inference Workloads with GPUs, CUDA, and PyTorch», O'Reilly Media, 2026 (Copyright © 2026 Flux Capacitor, LLC).

Direct quotes from the book are marked with separate blocks labeled «Quote from the book». The rest of the text is a systematized exposition of the chapter's content, a comparison with current data, forecast verification, and technical details beyond the original text based on official PyTorch documentation and open sources. Quotes are provided in limited volume for review and critical analysis purposes.

§Chapter Context

Chapter 13 of Fregly's book is dedicated to practical profiling, tuning, and scaling of PyTorch workloads on GPUs. The chapter covers the full cycle: from profiling tools (PyTorch Profiler, Nsight Systems, Nsight Compute, Linux perf) and bottleneck identification methodology to specific optimization techniques (torch.compile, CUDA graphs, memory allocator tuning, activation checkpointing, data pipeline optimization) and scaling to multiple GPUs and nodes (DDP, FSDP, tensor parallel, pipeline parallel). The chapter concludes with a discussion of continuous benchmarking and protecting performance from regressions.

Quote from the book · Chapter 13

«AI training and inference pipelines can suffer from performance bottlenecks at every layer, including Python interpreter overhead, CPU host-side data-loading stalls, CUDA kernel underutilization, and GPU device-memory contention. To optimize effectively, you need to profile at multiple levels of the stack using multiple tools that cover the entire system».

Exposition of content

The chapter is structured as a practical guide. First, profiling tools and methodology are examined using a MoE model as an example. Then, specific optimization techniques are presented, each accompanied by code examples and measurement results. Next, scaling to multiple GPUs and nodes is discussed. The chapter concludes with continuous benchmarking and protecting performance from regressions. The key idea: PyTorch performance is not a single «silver bullet» but systematic work on profiling and eliminating bottlenecks at every level of the stack.

• • •

INVTX Markers and Profiling Tools

Quote from the book · Chapter 13

«NVIDIA Tools Extension (NVTX) markers designate time ranges on the profiler timeline and allow different tools to correlate events of the same phases. For example, an NVTX range for "forward" will appear in both PyTorch profiler traces and Nsight Systems' timelines. This makes cross-tool analysis much easier at different layers of the stack».

NVTX markers are inserted into code via CUDA C++, PyTorch, or any C++/Python library that supports NVIDIA GPUs. Many libraries already insert NVTX markers on your behalf for critical code sections such as train_step, forward, backward, optimizer_step. But you can also add them yourself using torch.profiler.record_function() and torch.cuda.nvtx.range_push().

ToolScopeKey FeaturesTypical Use Case
PyTorch Profiler (Kineto)Operation level (CPU/GPU)NVTX support, shape recording, memory stats, trace export, graph break identificationDetailed model code breakdown; slow ops, kernel launch overhead
Nsight Systems (nsys)System-wide timeline (CPU, GPU, OS, I/O)Unified CPU thread and GPU stream timeline, NVTX integration, multiprocess supportEnd-to-end pipeline overview; loader stalls, CPU-GPU overlap, GPU synchronization
Nsight Compute (ncu)Per-kernel GPU analysisPer-kernel hardware metrics, source correlation, Roofline analysis, occupancy and throughput reportsDeep kernel analysis after hotspots identified; memory-bound vs compute-bound determination
PyTorch Memory ProfilerGPU memory usage by operationMemory snapshot timeline, per-op peak memory, integration with torch.cuda.memory_stats()Fragmentation diagnosis, unexpectedly high memory usage
Linux perfCPU and system eventsCPU cycle sampling, cache misses, branch misses, flame graphs, off-CPU analysisPython overhead, data loading stalls, OS scheduling issues
Holistic Trace Analysis (HTA)Distributed training trace visualizationBrowser-based Kineto trace analyzer, multi-worker trace aggregation, Perfetto integrationMulti-GPU/multinode execution analysis; finding imbalance, idle time, communication overlap verification
Chrome trace / PerfettoOffline trace viewingWeb UIs for standard trace formats, advanced filtering (Perfetto SQL), easy trace sharingInspect traces without specialized software; dashboards and remote collaboration
TorchEvalModel metrics and performance loggingStandardized metrics API (throughput, accuracy), easy integrationLogging throughput and latency alongside accuracy during training and evaluation
ExecuTorchMobile, embedded, and edge devicesModel exporting, profiling, debugging, memory analysis, visualizationRunning PyTorch models and collecting metrics on constrained platforms
Tool selection strategy

Start with PyTorch Profiler for a quick operation-level overview. If you see gaps in the timeline or GPU idle time — move to Nsight Systems. If a specific kernel dominates — drill into Nsight Compute. For CPU bottlenecks — Linux perf. For multi-GPU — HTA. For sharing results — Perfetto.

Section I Summary
Key point of the chapter

NVTX markers are a unified annotation language understood by all profiling tools. This makes cross-tool analysis seamless.

Practical consequence

The combination PyTorch Profiler (operation level) + Nsight Systems (system timeline) + Nsight Compute (kernel analysis) covers all stack layers.

Takeaway for engineers

Add NVTX markers at the top level — for iteration phases. Excessive markers pollute the timeline and complicate analysis.

• • •

IIProfiling PyTorch: MoE Example

In the chapter, profiling is demonstrated using a mixture-of-experts (MoE) transformer model. MoE is an LLM with multiple expert layers, where each expert is a feed-forward network. Token routing to experts is controlled by a gating system. The architecture is rich in optimization opportunities: many medium-sized matmuls, custom routing kernels (dispatch/combine), and dynamic token distribution.

Using PyTorch Profiler

First, the model and inputs are set up. Hugging Face Transformers is used to load the model and tokenizer, the model is moved to GPU, and a small batch is prepared:

import torch
from transformers import AutoModelForCausalLM, AutoTokenizer

model_name = "..."
tokenizer = AutoTokenizer.from_pretrained(model_name)
device = torch.device("cuda")
model = AutoModelForCausalLM.from_pretrained(model_name).to(device)
optimizer = torch.optim.AdamW(model.parameters(), lr=1e-4, fused=True)

batch_size = 4
input_texts = ["MoEs are great."] * batch_size
enc = tokenizer(input_texts, return_tensors="pt", padding=True, truncation=True)
input_ids = enc.input_ids.to(device)
attention_mask = enc.attention_mask.to(device)
labels = input_ids.clone()

To avoid capturing one-time setup costs, the model is warmed up with several iterations before profiling. This prepares the model for analysis: compiles JIT kernels and fills caches.

for _ in range(5):
    with torch.autocast(device_type="cuda", dtype=torch.bfloat16):
        outputs = model(input_ids, attention_mask=attention_mask, labels=labels)
        loss = outputs.loss
        loss.backward()
        optimizer.step()
        optimizer.zero_grad(set_to_none=True)

Now one training iteration is profiled, with high-level regions marked via record_function and NVTX ranges:

from torch import profiler

with profiler.profile(
    activities=[profiler.ProfilerActivity.CPU, profiler.ProfilerActivity.CUDA],
    record_shapes=True,
    profile_memory=True,
    with_stack=True,
    with_flops=True
) as prof:
    with profiler.record_function("train_step"):
        torch.cuda.nvtx.range_push("forward")
        with torch.autocast(device_type="cuda", dtype=torch.bfloat16):
            outputs = model(input_ids, attention_mask=attention_mask, labels=labels)
            loss = outputs.loss
        torch.cuda.nvtx.range_pop()

        torch.cuda.nvtx.range_push("backward")
        loss.backward()
        torch.cuda.nvtx.range_push("optimizer_step")
        optimizer.step()
        torch.cuda.nvtx.range_pop()
        optimizer.zero_grad()
        torch.cuda.nvtx.range_pop()

After execution, operation-level results can be examined by calling prof.key_averages().table(). The top 10 operations sorted by self_cuda_time_total are requested:

print(
    prof.key_averages()
    .table(
        sort_by="self_cuda_time_total",
        row_limit=10,
        fields=["self_cuda_time_total", "calls"]
    )
)
OperationSelf CUDA totalCalls
aten::matmul43.00 ms128
aten::linear35.50 ms64
dispatch18.70 ms2
combine12.10 ms2
aten::layer_norm10.20 ms16
aten::softmax5.70 ms4
aten::scatter4.10 ms16
aten::gather3.60 ms16
aten::to2.90 ms8
aten::add_2.20 ms64
Key takeaway

The main bottleneck is matmul in the expert FFN, followed by dispatch and combine kernels. Together they determine the training iteration time. This means that to accelerate, these operations must be optimized — either directly or by reducing their number of calls.

System profiling with Nsight Systems and NVTX

The NVTX markers make timeline analysis in Nsight Systems straightforward. To aggregate metrics by phase, the code is profiled with nsys using an NVTX-based summary:

nsys profile \
    --output=profile \
    --stats=true \
    -t cuda,nvtx \
    python train.py

nsys stats --report=nvtx_gpu_proj_sum profile.nsys-rep
NVTX rangeGPU time (ms)Self GPU time (ms)Instances
train_step138.00.01
forward60.560.5130
backward58.358.3130
optimizer_step19.219.21

The NVTX projection mechanism is not exact kernel-to-range binding but a statistical estimate. CUPTI records when the CPU issued push/pop NVTX markers and which GPU kernels were launched in that time window on the host.

Section II Summary
Key point of the chapter

PyTorch Profiler identifies matmul in expert FFN as the main bottleneck of the MoE model, followed by dispatch and combine.

Practical consequence

Nsight Systems with NVTX annotation provides a GPU-time summary by iteration phase. The NVTX projection mechanism is a statistical estimate, not exact binding.

Takeaway for engineers

Start with PyTorch Profiler to identify hot operations. Then use Nsight Systems for timeline analysis. Only then move to Nsight Compute for deep analysis of specific kernels.

• • •

IIIRoofline Analysis for GEMM Kernels

For deep analysis of the expert matmul, the Nsight Compute CLI profiler (ncu) is invoked, targeting specific GEMM kernels by name:

ncu \
    --target-processes all \
    --kernel-name-regex "matmul" \
    --metrics \
        gpu__time_duration.avg, \
        gpu__dram_throughput.avg.pct_of_peak_sustained_elapsed, \
        lts__throughput.avg.pct_of_peak_sustained_elapsed, \
        sm__sass_thread_inst_executed_op_fp32_pred_on.sum, \
        sm__warps_active.avg.pct_of_peak_sustained_active \
    --csv full \
    -o matmul_roofline_report \
    python train.py
Kernel configuration% peak FLOPS% peak memory BWAchieved SM occupancyCharacteristic
Baseline50%70%60%Memory bound (stalling on memory transfers)
Optimized85%40%80%Compute bound (near hardware roofline)

In the baseline run, the GEMM kernel achieves only 50% of peak FLOPS, 70% of peak memory bandwidth, and a moderate 60% SM occupancy. This is not enough to fully hide memory latency. The metrics indicate a memory-bound kernel. After optimization (kernel fusion, increased arithmetic intensity, reduced memory movement), peak FLOPS rises to 85%, memory bandwidth drops to 40%, and occupancy grows to 80%. The kernel shifts from memory-bound to compute-bound.

No universal target occupancy exists

Many high-performance kernels achieve full latency hiding at 25–50% achieved occupancy. Use Nsight Compute occupancy metrics along with stall reason breakdowns and eligible warps per active cycle to judge whether more occupancy will actually reduce stalls.

Section III Summary
Key point of the chapter

Roofline analysis in Nsight Compute shows that the MoE GEMM kernel is initially memory bound (50% FLOPS, 70% memory). After optimizations it becomes compute bound (85% FLOPS, 40% memory).

Practical consequence

There is no universal target occupancy. Each kernel has its own optimum determined by the latency hiding vs resource balance.

Takeaway for engineers

Use Roofline for diagnosis: if the kernel is below the slanted line — optimize memory access; if below the horizontal line — optimize computation.

• • •

IVCPU Profiling with Linux perf

For a more holistic view of where time is spent — on the host and on the device — Linux perf is used to analyze CPU cycles, cache misses, branch misses, etc. These insights then drive a series of optimizations.

perf stat -e \
    cycles,instructions,cache-misses,branch-misses \
    python train.py
perf record -F 2000 -g --call-graph dwarf -o perf.data \
    python train.py

perf report --stdio -n -g -i perf.data

Based on the profile, the following bottlenecks and mitigation measures are identified:

  • Excess Python overhead (45% in py::forward) — use torch.compile to eliminate interpreter overhead and fuse Python-side operations into optimized CUDA code.
  • Large matmul hotspot (20.5%) — PyTorch Compiler or custom CUDA C++ kernel to bypass Python and use optimized CUDA code directly.
  • Data loading stalls (10.2%) — increase DataLoader num_workers, enable persistent_workers=True.
  • Gradient sync overhead (8.7%) — increase DDP gradient bucket size from 25 MB to 50 MB.
  • Host I/O bottleneck (5.3% in read) — pinned memory, non-blocking copies, container formats (Arrow, WebDataset, TFRecord).
Section IV Summary
Key point of the chapter

Linux perf identifies 45% Python overhead and significant costs for gradient synchronization (8.7%) and data loading (10.2%).

Practical consequence

For CPU bottlenecks use Linux perf. For GPU bottlenecks — Nsight. The combination provides the full picture.

Takeaway for engineers

Apply optimizations one at a time, measuring the effect of each. This isolates the impact of changes and avoids unexpected interactions.

• • •

VPyTorch Compiler (torch.compile)

Quote from the book · Chapter 13

«One of the fastest wins in PyTorch is using the compiler via torch.compile(). The compiler stack includes TorchDynamo, AOT Autograd, and TorchInductor, which capture graphs, fuse operations, and generate high-performance code for the target backend».

Using the compiler with default settings is simple: model = torch.compile(model). Under the hood, TorchDynamo traces the Python code, AOT Autograd captures the backward pass, and TorchInductor, relying on OpenAI Triton for GPU kernel generation, automatically builds efficient fused kernels.

compiled_model = torch.compile(model, mode="reduce-overhead")

In this case, eager mode takes about 248 ms per iteration, while compiled mode runs around 173 ms. The ~30% speedup comes mainly from merging many small GPU kernels.

ModeDescriptionCompile timeExtra memoryKey features
defaultBalanced optimizationsLow–mediumNoGeneral fusion, basic autotuning
reduce-overheadReduces per-iteration overheadMediumYes (workspace caching)Uses CUDA Graphs to eliminate launch overhead
max-autotuneMaximizes runtime performanceHighPossibleAggressive Triton autotuning, enables CUDA Graphs
max-autotune-no-cudagraphsSame, without CUDA graphsHighNoFor dynamic shapes or debugging

Figure 1. Compiled vs Noncompiled Regions

PyTorch profile: compiled and eager regions of the graph

compiled
compiled
eager
compiled
eager
compiled
Compiled Function Eager fallback (graph break)
Cyan segments — compiled, amber — eager fallback
torch.compile. Compiled Function ranges on the timeline show which graph regions are captured by TorchDynamo and accelerated by TorchInductor, and where a graph break occurred and the model fell back to eager execution.

Profiling and debugging the compiler

When torch.compile does not deliver the expected speedup, diagnostic tools are used. torch._dynamo.explain(model) prints a report of graph breaks, their causes, and which model parts were not captured by TorchDynamo. The TORCH_LOGS="+dynamo,+inductor" variable enables verbose compiler logging.

import torch._dynamo as dynamo

def toy_example(a, b):
    x = a / (torch.abs(a) + 1)
    print("woo")
    if b.sum() < 0:
        b = -b
    return x * b

explanation = dynamo.explain(toy_example)(torch.randn(10), torch.randn(10))
print(explanation)

The report will show the number of graphs, the number of graph breaks, and their causes. To minimize graph breaks, torch.where() is used instead of Python conditionals, and torch.compiler.set_stance("fail_on_recompile") forces an error when a graph break is encountered.

Section V Summary
Key point of the chapter

torch.compile delivers ~30% speedup through small GPU kernel fusion. The reduce-overhead mode additionally uses CUDA Graphs.

Practical consequence

For diagnosing compiler issues, torch._dynamo.explain() and TORCH_LOGS are used. Graph breaks are identified and eliminated by replacing Python conditionals with tensor operations.

Takeaway for engineers

Not all graph breaks are harmful: for DDP and FSDP they are expected and necessary for overlapping communications with computations.

• • •

VIOptimized Attention Mechanisms

Transformer models spend a significant portion of time in attention mechanisms. PyTorch provides several levels of optimization:

  • Scaled Dot-Product Attention (SDPA) — torch.nn.functional.scaled_dot_product_attention automatically uses the fastest available kernel (FlashAttention, memory-efficient, or cuDNN) depending on tensor shapes and data types.
  • FlexAttention — a compiler-based approach for custom sparsity patterns (block-sparse, sliding-window). Allows describing the mask as a function, and the compiler generates optimized kernels.
  • FlexDecoding — optimization of the decoding phase for LLMs. Compiles a specialized kernel for decoding (Q_len=1) with a growing KV cache.
  • Context Parallel — attention sharding along the sequence length dimension for handling very long contexts.

Figure 2. FlexAttention: custom attention variants

JIT compilation of masks (block-sparse, sliding-window, causal) without manual CUDA kernels

Causal, sliding-window ±2, block-sparse 2×2
FlexAttention. Instead of manual CUDA kernels, the sparsity pattern is described by a mask function, and the compiler generates optimized kernels for that specific pattern.
Section VI Summary
Key point of the chapter

SDPA automatically selects the best backend. FlexAttention allows describing custom sparsity patterns without manual CUDA kernels.

Practical consequence

FlexAttention with nested jagged tensors (NJT) eliminates padding for variable-length sequences.

Takeaway for engineers

Verify which SDPA backend is actually used, and if necessary, specify it explicitly via torch.nn.attention.sdpa_kernel.

• • •

VIIConcurrency with CUDA Streams

CUDA streams provide concurrency and overlap of GPU operations. By default, PyTorch schedules all operations on the default device stream (stream 0) sequentially. However, many tasks are independent, and the GPU can execute them in parallel with multiple streams.

device = 'cuda'
transfer_stream = torch.cuda.Stream(device=device)
compute_stream = torch.cuda.default_stream(device=device)

dataloader_iter = iter(dataloader)

first_batch = next(dataloader_iter, None)
if first_batch:
    with torch.cuda.stream(transfer_stream):
        next_inputs, next_labels = (
            first_batch[0].to(device, non_blocking=True),
            first_batch[1].to(device, non_blocking=True),
        )

for _ in range(len(dataloader)):
    compute_stream.wait_stream(transfer_stream)
    inputs, labels = next_inputs, next_labels

    batch = next(dataloader_iter, None)
    if batch:
        with torch.cuda.stream(transfer_stream):
            next_inputs, next_labels = (
                batch[0].to(device, non_blocking=True),
                batch[1].to(device, non_blocking=True),
            )

    with torch.cuda.stream(compute_stream):
        outputs = model(inputs)
        loss = loss_fn(outputs, labels)
        loss.backward()
        optimizer.step()
        optimizer.zero_grad(set_to_none=True)

Figure 3. Overlapping compute and data transfer

Separate CUDA streams: one prepares the next batch, the other computes the current one

compute_stream
batch N
batch N+1
batch N+2
batch N+3
transfer_stream
H2D N+1
H2D N+2
H2D N+3
H2D N+4
Amber — compute, cyan — cudaMemcpyAsync into pinned memory
Stream overlap. While compute_stream processes the current batch, transfer_stream prepares the next one. This hides H2D copy latency behind computations.
Frequent mistake

Calling print() on a CUDA tensor triggers an implicit synchronization (torch.cuda.synchronize()) that stops all streams. In production code, avoid any operations that read GPU tensor values on the CPU without explicit need.

Stream synchronization with events

event = torch.cuda.Event(enable_timing=False)

with torch.cuda.stream(stream1):
    kernel_launch(...)
    event.record()

stream2.wait_event(event)

with torch.cuda.stream(stream2):
    other_kernel_launch(...)
Section VII Summary
Key point of the chapter

CUDA streams allow overlapping data transfer and computation. A separate transfer_stream prepares the next batch while compute_stream processes the current one.

Practical consequence

CUDA events provide fine-grained synchronization without global blocking. torch.cuda.Event(enable_timing=False) reduces overhead.

Takeaway for engineers

For profiling multithreaded pipelines use Nsight Systems. Verify that non_blocking=True and pinned memory are used correctly.

• • •

VIIICUDA Graphs for Reducing Overhead

CUDA graphs eliminate per-iteration launch overhead, reduce CPU overhead, and remove tiny inter-kernel gaps. PyTorch provides the torch.cuda.CUDAGraph API for capturing and replaying CUDA graphs.

g = torch.cuda.CUDAGraph()
capture_stream = torch.cuda.Stream()

static_input = torch.randn(batch_shape, device='cuda')
static_output = torch.empty(output_shape, device='cuda')

with torch.cuda.stream(capture_stream):
    tmp = model(static_input)
    static_output.copy_(tmp)

capture_stream.synchronize()

with torch.cuda.graph(g, stream=capture_stream):
    tmp = model(static_input)
    static_output.copy_(tmp)

capture_stream.synchronize()
CUDA graph capture rules

Avoid memory allocation during capture; use a dedicated stream; synchronize before and after capture; remember the graph overwrites the output buffer on each iteration — clone if you need to keep it.

MetricBefore CUDA GraphsAfter CUDA Graphs
CPU launch calls per 100 iterations300 separate kernel launches100 graph replays
Host synchronization calls300 cudaDeviceSynchronize0
GPU idle between kernels~3 µs gaps per iteration0 µs (continuous execution)
End-to-end iteration latency~1.00 ms~0.75 ms (25% faster)

CUDA Graph Trees are used by torch.compile in reduce-overhead mode to compile and cache separate static graphs for each input shape. Keeping input shapes as consistent as possible across iterations is recommended.

Section VIII Summary
Key point of the chapter

CUDA graphs eliminate kernel launch overhead, reducing iteration latency by 25%. Graphs overwrite the output buffer, so cloning is required to preserve results.

Practical consequence

CUDA Graph Trees in torch.compile(mode="reduce-overhead") cache static graphs for each input shape. Fewer distinct shapes — more efficient.

Takeaway for engineers

For stable shapes and long-running workloads, CUDA graphs provide significant gains. For dynamic shapes, use max-autotune-no-cudagraphs.

• • •

IXTuning the CUDA Memory Allocator

PyTorch uses a caching memory allocator for CUDA. By default, it adapts to allocation patterns, splitting and reusing GPU memory blocks. Key tuning options via environment variables:

  • PYTORCH_CUDA_ALLOC_CONF=max_split_size_mb:N — the maximum block size the allocator may split. Tells the allocator to keep large free blocks (up to 256 MB) instead of continually breaking them into small fragments.
  • PYTORCH_CUDA_ALLOC_CONF=roundup_power2_divisions:[N:M,...] — groups requests into fixed buckets. For example, in the 512–1024 MB range with 512:2, a 600 MB request rounds up to 768 MB. This reduces fragmentation and increases reuse.
  • PYTORCH_CUDA_ALLOC_CONF=backend:cudaMallocAsync — asynchronous CUDA allocator. Eliminates global synchronizations on allocation and deallocation.

Figure 4. PyTorch Memory Profiler: 3 iterations of forward + backward

Peak and cumulative GPU memory allocations by operation

time → memory iter 1 iter 2 iter 3 peak allocated reserved (cache) peak
Cyan — allocated, amber — reserved (cache), dashed — peak
Memory profiler. If mem_get_info() shows little free memory while memory_stats() shows the sum of allocated tensors is significantly less than total memory, fragmentation is growing. In this case, roundup_power2_divisions is the key tool.
export PYTORCH_CUDA_ALLOC_CONF=\
max_split_size_mb:256,\
roundup_power2_divisions:[256:1,512:2,1024:4,:8],\
backend:cudaMallocAsync
Section IX Summary
Key point of the chapter

Allocator tuning via max_split_size_mb and roundup_power2_divisions reduces fragmentation and increases memory reuse.

Practical consequence

The cudaMallocAsync backend eliminates global synchronizations on allocation. Fragmentation monitoring via memory_reserved() and memory_allocated().

Takeaway for engineers

For long-running workloads with variable tensor sizes, allocator tuning provides stable memory usage without OOM.

• • •

XActivation Checkpointing for Memory Savings

For very large models, activation checkpointing (gradient checkpointing) is essential for memory management. Instead of storing all forward activations, only the inputs of each block are saved. During the backward pass, activations are recomputed.

You don't have to checkpoint everything. Focus on the largest layers — usually transformer blocks, which hold the bulk of activations, but not smaller layers like layer norm and embeddings.

import torch
from torch.utils.checkpoint import checkpoint

class TransformerBlock(torch.nn.Module):
    def forward(self, x):
        return checkpoint(self._forward, x, use_reentrant=False)

    def _forward(self, x):
        return self.attention(self.mlp(x))
Selective checkpointing

Selective activation checkpointing allows choosing which layers to recompute. For example, you can checkpoint every other transformer layer. This provides a more flexible trade-off between memory and computation.

Section X Summary
Key point of the chapter

Activation checkpointing trades computation for memory. The selective approach allows choosing the optimal trade-off.

Practical consequence

For MoE models with many experts, checkpointing transformer blocks provides significant memory savings.

Takeaway for engineers

Checkpointing is mandatory for models that otherwise do not fit in memory. It allows using a larger batch size.

• • •

XIOptimizing the Input Data Pipeline

A frequent cause of inefficiency is GPUs idling while waiting for data. PyTorch DataLoader supports multiple workers for parallel data loading and preprocessing while the model trains.

  • pin_memory=True — transfer through page-locked memory. Accelerates CPU→GPU copies.
  • non_blocking=True and persistent_workers=True — to hide copies and avoid re-spawning workers between epochs.
  • prefetch_factor — how many batches each worker prefetches. Default is 2.
loader = DataLoader(
    dataset,
    batch_size=B,
    num_workers=8,
    pin_memory=True,
    persistent_workers=True,
    prefetch_factor=4,
)
Heuristic for num_workers

For CPU-bound data pipelines, 2–8 workers per GPU is often enough. Do not follow the "one worker per CPU" rule blindly — it often results in too many workers competing for the GIL.

Section XI Summary
Key point of the chapter

pin_memory=True, persistent_workers=True, and prefetch_factor are the three key parameters for preventing GPU idle time waiting for data.

Practical consequence

For CPU-bound pipelines, 2–8 workers per GPU is sufficient. Excess workers compete for the GIL.

Takeaway for engineers

Profile the DataLoader in isolation: time 100 batches without GPU work. Compare with the target iteration time.

• • •

XIIFSDP: Automatic Checkpointing and Offloading

FSDP in PyTorch is a distributed parallelism strategy that shards model parameters, gradients, and activations across GPUs. Technically, FSDP implements the ZeRO Stage-3 strategy for sharding model states across GPUs.

import torch
import torch.nn as nn
import torch.distributed as dist
from torch.distributed.fsdp import (
    FullyShardedDataParallel as FSDP,
    CPUOffload,
    ShardingStrategy,
    MixedPrecision
)
from torch.distributed.fsdp.wrap import transformer_auto_wrap_policy

dist.init_process_group("nccl")
torch.cuda.set_device(dist.get_rank() % torch.cuda.device_count())

class MyModel(nn.Module):
    def __init__(self):
        super().__init__()
        self.layers = nn.Sequential(
            nn.Linear(4096, 4096),
            nn.ReLU(),
            nn.Linear(4096, 4096),
        )

    def forward(self, x):
        return self.layers(x)

model = MyModel().cuda()

auto_wrap_policy = transformer_auto_wrap_policy(
    model,
    min_num_params=1e8,
)

fsdp_model = FSDP(
    model,
    auto_wrap_policy=auto_wrap_policy,
    sharding_strategy=ShardingStrategy.FULL_SHARD,
    use_orig_params=True,
    cpu_offload=CPUOffload(offload_params=True, pin_memory=True),
)

When the network is slow — or only a few GPUs are available — ShardingStrategy.SHARD_GRAD_OP (ZeRO Stage-2) can be used to shard only gradients and optimizer state. On Grace-Blackwell superchip systems, SuperOffload and GraceAdam are available.

Figure 5. FSDP (ZeRO Stage-3): sharding the model across GPUs

Parameters, gradients and optimizer states are split across 4 GPUs

Full Model Parameters (replicated) Gradients (replicated) Optimizer state (replicated) FSDP shard GPU 0 params 1/4 grads 1/4 optim 1/4 GPU 1 GPU 2 GPU 3 params 1/4 grads 1/4 optim 1/4 each GPU 1/N
Full Model → 4 shards: params, grads and optim split across GPUs
FSDP = ZeRO Stage-3. Parameters, gradients, and optimizer states are sharded across GPUs. Forward requires all-gather, backward requires reduce-scatter. Sharding at the transformer block level provides the best performance and memory characteristics.
Section XII Summary
Key point of the chapter

FSDP implements ZeRO Stage-3: parameters, gradients and optimizer states are sharded across GPUs. Sharding at the transformer block level provides maximum efficiency.

Practical consequence

ShardingStrategy.FULL_SHARD (ZeRO-3) for minimal memory; SHARD_GRAD_OP (ZeRO-2) when the network is slow; HYBRID_SHARD for a balance of memory and communication.

Takeaway for engineers

Automatic checkpointing and offloading via FSDP reduce memory usage without boilerplate code.

• • •

XIIIScaling with PyTorch Distributed

DDP with torch.compile

When using DistributedDataParallel, which synchronizes gradients across GPUs via all-reduce, PyTorch automatically creates graph breaks at synchronization points. PyTorch's design compiles the backward computation of each bucket as a separate graph, so all-reduce can run between those graphs.

FSDP with torch.compile

Wrap each transformer block — not just an individual low-level layer — in its own FSDP module with use_orig_params=True. This way TorchDynamo inserts a graph break at each shard boundary where communication is needed.

Tensor and pipeline parallel with torch.compile

Tensor parallel and pipeline parallel are orthogonal to torch.compile. The PyTorch compiler mainly focuses on optimizing computation within each segment. Use torch.compile to optimize computation within each layer or bucket.

TorchTitan, AsyncTP, AutoParallel and SimpleFSDP

TorchTitan is a set of reference implementations for large-scale training. SimpleFSDP reimplements FSDP in a torch.compile-friendly way via DTensor. In TorchTitan experiments, SimpleFSDP reduced memory consumption to ~28% and improved training throughput up to ~69% compared to the traditional FSDP2 eager path.

Section XIII Summary
Key point of the chapter

Graph breaks in DDP and FSDP are expected and necessary for overlapping communications with computations. Compiling each bucket separately is by design, not a bug.

Practical consequence

SimpleFSDP reduces memory to ~28% and increases throughput up to ~69% relative to FSDP2 eager (maximum on 2D parallelism; average gain is ~29% for 8B and ~28% for 70B).

Takeaway for engineers

Use torch.compile to optimize computation within each segment, but trust the distributed strategy to manage communications.

• • •

XIVMulti-GPU Profiling with HTA

When scaling to many GPUs, managing traces can become cumbersome. HTA from Meta helps aggregate and visualize multi-worker traces. HTA ingests JSON traces from torch.profiler for each GPU/rank and presents a unified timeline.

With HTA you can see traces from all 8 GPUs aligned in time. NVTX markers from each rank are aligned and visualized. You might notice that rank 0 enters the backward pass at time T, while rank 1 only at T+1, possibly because rank 1 was waiting for rank 0 in all-reduce.

from torch.profiler import profile, schedule, tensorboard_trace_handler

with profile(
    activities=[ProfilerActivity.CPU, ProfilerActivity.CUDA],
    schedule=schedule(wait=1, warmup=1, active=3, repeat=1),
    on_trace_ready=tensorboard_trace_handler('./log'),
    record_shapes=True,
    profile_memory=True,
    with_stack=True
) as prof:
    for step, batch in enumerate(dataloader):
        train_step(batch)
        prof.step()
Section XIV Summary
Key point of the chapter

HTA aggregates traces from all ranks into a unified timeline, allowing identification of imbalance and synchronization delays.

Practical consequence

Use torch.profiler.schedule to record traces over multiple iterations without overload.

Takeaway for engineers

For multinode profiling, HTA is the standard tool. TensorBoard trace viewer is deprecated for this purpose.

• • •

XVCI and Benchmarking

After applying all optimizations, it is critical to maintain them and continuously check for performance regressions. Set up a simple performance regression CI with TorchBench and GitHub Actions to automatically catch slowdowns.

- name: Run MoE benchmark
  run: |
    torchbench run --model moe --iters 10 --batch-size 4 --json results.json

- name: Compare throughput
  run: |
    python scripts/compare_perf.py baseline.json results.json

Figure 6. PyTorch HUD Performance Dashboard

Public nightly benchmark dashboard: throughput, memory, regressions per commit

Benchmarks → vLLM / Llama-3.1-8B

last 7 days NVIDIA Blackwell

Throughput (tokens/sec)

Per-model FLOPS utilization

Compile time
12.4 s
−18% over 30 days
Peak memory
14.2 GB
+2% regression
Tokens/sec
2 340
+9%
Regression
1
vLLM #a1f2c9
ALERT Commit a1f2c9 — throughput drop −5.2% on Llama-3.1-8B (sustained 3 runs). Recommended: revert or bisect.
PyTorch HUD: nightly benchmarks, regressions, KPIs
Continuous benchmarking. Nightly benchmarks and the public HUD dashboard automatically catch performance regressions. In CI, compare throughput against a baseline and fail the build on a sustained ≥5% drop.
Section XV Summary
Key point of the chapter

CI with TorchBench and GitHub Actions automatically detects performance regressions. The public PyTorch HUD is a source of external signals.

Practical consequence

A sustained ≥5% throughput drop over three runs is a signal for investigation.

Takeaway for engineers

Performance regression testing must be part of CI/CD. Without it, optimizations degrade over time.

• • •

XVIForecast Verification

Confirmed (adoption) Forecast 1: torch.compile becomes standard

«torch.compile delivers ~30% speedup through small GPU kernel fusion».

Confirmed in terms of adoption; the ~30% figure is a book estimate. By 2026, torch.compile became standard practice for production workloads. PyTorch 2.x actively develops the compiler, and TorchInductor generates highly optimized kernels for NVIDIA GPUs. The reduce-overhead and max-autotune modes are used in production clusters. The actual fusion gain strongly depends on the model and compilation mode: in Meta publications it is typically 10–30%.

Confirmed Forecast 2: FSDP displaces DDP for large models

«FSDP implements ZeRO Stage-3 for sharding model states across GPUs».

Confirmed. FSDP became the standard for training models that don't fit in one GPU's memory. SimpleFSDP and TorchTitan demonstrate further development of the approach. HYBRID_SHARD and SHARD_GRAD_OP are used to balance memory and communication.

Confirmed (adoption) Forecast 3: CUDA graphs reduce overhead

«CUDA graphs eliminate kernel launch overhead, reducing iteration latency by 25%».

Confirmed in terms of adoption; the 25% figure is a book estimate. CUDA Graphs are integrated into torch.compile(mode="reduce-overhead") and actively used in inference engines (vLLM, TensorRT-LLM). The static shapes limitation remains, but CUDA Graph Trees solve this for discrete shape sets. The actual gain depends on the share of launch overhead in total iteration time and is typically 10–30%.

Confirmed Forecast 4: HTA becomes standard for multi-GPU profiling

«HTA aggregates traces from all ranks into a unified timeline».

Confirmed. HTA is actively used at Meta and other organizations for distributed training analysis. TensorBoard trace viewer is deprecated, and HTA with Perfetto have become standard. CUPTI Counter Analysis allows obtaining GPU performance counters.

Partially confirmed Forecast 5: Selective activation checkpointing becomes the norm

«You don't have to checkpoint everything. Focus on the largest layers».

Partially confirmed. Selective checkpointing is supported in TorchTitan and torchtune. PyTorch provides APIs for selecting specific layers. However, there is no universal strategy — the choice depends on the model architecture and available memory.

• • •

XVIIAdditional Observations

Additional observations

Memory profiler and Mosaic. The chapter examines the standard PyTorch memory profiler. Additionally, Meta developed Mosaic — a tool for analyzing memory snapshots that identifies leaks and fragmentation at the level of individual allocations. Mosaic integrates with torch.cuda.memory._dump_snapshot().

CUDA Graph Trees and dynamic shapes. Although the chapter mentions CUDA Graph Trees, their mechanism with dynamic shapes deserves separate attention. Trees classify each storage that is live at the end of capture as either a private-pool allocation or a known static input. This allows reusing graphs for multiple shapes without full recapture.

PyTorch HUD and regressions. The public PyTorch HUD (hud.pytorch.org) tracks torch.compile performance relative to eager mode. However, recent issues with AWS migration led to periods of dashboard unreliability. For critical workloads, a custom CI with benchmarks is recommended.

NVIDIA Blackwell and profiling. New Blackwell architectures (B200/B300) require updated versions of Nsight Systems and Nsight Compute. PyTorch 2.x supports profiling on Blackwell, but some metrics (e.g., TMEM utilization) may be unavailable in older tool versions.

MLPerf Logging. For standardized performance logging in distributed workloads, the mlperf_logging library is used. It allows recording key events (training start, epoch end, final accuracy) in a format compatible with MLPerf. This simplifies performance comparison between configurations.

• • •

XVIIIKey Points

  1. NVTX markers are the unified profiling language. PyTorch Profiler, Nsight Systems, and Nsight Compute all understand NVTX. This makes cross-tool analysis seamless.
  2. Profiling reveals bottlenecks. In the MoE model, the main bottleneck is matmul in expert FFN, followed by dispatch and combine.
  3. Roofline analysis shows memory-bound vs compute-bound. The baseline MoE GEMM kernel is memory bound (50% FLOPS, 70% memory). After optimizations — compute bound (85% FLOPS, 40% memory).
  4. Linux perf complements GPU profiling. 45% Python overhead, 8.7% gradient synchronization, 10.2% data loading.
  5. torch.compile delivers ~30% speedup. Small GPU kernel fusion and elimination of Python overhead. reduce-overhead mode uses CUDA Graphs.
  6. CUDA graphs reduce iteration latency by 25%. Eliminate kernel launch overhead. CUDA Graph Trees cache graphs per input shape.
  7. Memory allocator tuning via max_split_size_mb, roundup_power2_divisions, and backend:cudaMallocAsync.
  8. Activation checkpointing trades computation for memory. Selective checkpointing allows choosing the optimal trade-off.
  9. DataLoader: three key parameters — pin_memory=True, persistent_workers=True, prefetch_factor.
  10. FSDP implements ZeRO Stage-3. Sharding at the transformer block level provides maximum efficiency. SimpleFSDP reduces memory to ~28% and increases throughput up to ~69%.
  11. Graph breaks in DDP and FSDP are expected. Compiling each bucket separately is by design, not a bug.
  12. HTA aggregates traces from all ranks. Allows identifying imbalance and synchronization delays in multi-GPU training.
  13. CI with TorchBench detects regressions. A sustained ≥5% throughput drop over three runs is a signal for investigation.
  14. PyTorch HUD is a public dashboard. Nightly benchmarks and regression tracking per commit.
Overall Review Summary

Chapter 13 of Fregly's book is a systematic guide to profiling, tuning, and scaling PyTorch. The key idea: performance is not a single «silver bullet» but a continuous cycle of profiling and eliminating bottlenecks at every level of the stack. From NVTX markers and Nsight to torch.compile, CUDA graphs, FSDP, and HTA — each tool solves its own problem. For an engineer, this means that understanding profiling methodology and the principles behind each tool is more important than knowing specific numbers of the current generation.

Forecast verification shows: the book proved accurate in terms of technology adoption. torch.compile became standard; FSDP displaced DDP for large models; CUDA graphs reduce overhead; HTA replaced TensorBoard trace viewer. The specific speedup percentages (30%, 25%) are book estimates; actual gains depend on the model and mode. The main takeaway remains unchanged: investments in profiling methodology ensure adaptation to any stack.

Sources: Chris Fregly, «AI Systems Performance Engineering» (O'Reilly, 2026), Chapter 13 — quotes provided in limited volume for review and critical analysis purposes. Official PyTorch documentation (torch.profiler, torch.compile, FSDP, CUDA Graphs); NVIDIA Nsight Systems/Compute documentation; Holistic Trace Analysis (Meta); MLPerf Training/Inference v5.0; TorchBench; PyTorch HUD.