中文翻译暂不可用,显示俄语原文。
Как работают CUDA graphs и когда их использовать?
Краткий тезис
CUDA graphs — это механизм записи последовательности операций на GPU (запуск ядер, копирование памяти) в единый исполняемый граф. Основная цель — устранить накладные расходы на каждый отдельный kernel launch (overhead в микросекундах). Для LLM inference с короткими запросами и фиксированной формой данных CUDA graphs дают ускорение 10–30%. Однако они плохо подходят для динамических размеров входов (разная длина последовательностей) и рекуррентных вычислений, так как требуют перестроения графа при каждом изменении формы.
1. Проблема: Overhead запуска CUDA-ядер
Каждый вызов CUDA kernel (например, операция умножения матриц или attention) требует:
- Переключения контекста на стороне CPU
- Подготовки параметров launch (grid, block)
- Отправки команд в GPU через драйвер
Хотя latency одного launch составляет единицы микросекунд (1–5 мкс), в современном LLM inference число kernel-запусков на один forward pass может достигать тысяч (например, для модели на 7B параметров с FlashAttention — около 1000–3000 запусков). Суммарный overhead становится значимым, особенно при batch=1, когда время вычислений ядер близко к overhead.
Пример:
При 2000 kernel launches overhead = 2000 × 3 мкс = 6 мс. Если чистый compute time forward pass 50 мс, то overhead добавляет 12% времени. При batch=1 это может быть до 30% overhead для коротких промптов (накладные расходы фиксированы, а compute мал).
2. Что такое CUDA Graph
CUDA graph — объект, который фиксирует последовательность операций (запуски ядер, копирования, события) и позволяет воспроизвести их одним вызовом cudaGraphLaunch. Запись происходит в два этапа:
- Capture (захват) — запускаем интересующий фрагмент кода (например, forward pass модели) в специальном режиме. Драйвер записывает все операции.
- Instantiation (создание экземпляра) — полученный граф оптимизируется, создается готовый к многократному запуску объект CUDAGraph.
Ключевое: CPU overhead переносится из фазы выполнения в фазу захвата. После захвата запуск графа требует минимальных действий CPU.
Пример использования в PyTorch
import torch
# Гипотетическая модель
model = torch.nn.Linear(1024, 1024).cuda()
input_tensor = torch.randn(1, 1024, device='cuda')
# Захват графа
graph = torch.cuda.CUDAGraph()
with torch.cuda.graph(graph):
output = model(input_tensor)
# Первый запуск: CPU пересылает граф в GPU
graph.replay()
# Последующие запуски: overhead ~0
for _ in range(10):
graph.replay()
Важно: между replay нужно обновлять входные буферы, если они меняются. Для этого используют отдельные буферы-заполнители (static input/output tensors).
3. Архитектура: Захват и воспроизведение
3.1. Механизм захвата
После вызова cudaStreamBeginCapture(capture_stream) все последующие операции на данном стриме записываются. Запись включает:
- Имена ядер, параметры launch (grid, block, аргументы)
- Вызовы
cudaMemcpy,cudaMemset - Обращения к выделенной памяти (адреса, размеры)
Ограничения
- Нельзя использовать динамическое выделение памяти внутри захвата (memset, cudaMalloc). Память должна быть фиксирована до захвата.
- Ветвления на GPU не допускаются — операции выполняются строго в порядке записи.
3.2. Воспроизведение
При cudaGraphLaunch(graph, stream) GPU получает предварительно распарсенный список команд. CPU не тратит время на подготовку каждого launch. Механизм называется single-kernel launch.
3.3. Граф и memory pool
Все тензоры, участвующие в графе, должны быть выделены до захвата. Для работы с переиспользованием памяти (например, torch.utils._cuda_pool) требуется аккуратное управление, чтобы адреса не менялись между replay.
4. Когда использовать CUDA Graphs
| Сценарий | Пример | Ускорение |
|---|---|---|
| Короткие запросы (batch=1, small model) | Инференс BERT/tinyLLaMA | 10–30% |
| Фиксированная форма входов (same sequence length) | Batch-задачи с pad до константной длины | 10–20% |
| Конвейерная обработка (stream batching) | vLLM, TensorRT-LLM с static batching | 15–30% |
| Agentic loop (множество вызовов LLM) | Каждый вызов короткий (tool call, summarization) | 10–25% |
Ключевой признак overhead kernel launch составляет заметную долю от total inference time. Профилирование через ncu --set full покажет high "kernel launch overhead".
5. Когда не использовать
| Сценарий | Причина |
|---|---|
| Динамическая длина последовательности | Каждый новый forward pass требует перезахвата графа, что занимает время (десятки миллисекунд). |
| Очень длинные запросы (batch=1, длительность >500 мс) | Overhead (единицы мс) незначителен по сравнению с compute. |
| Рекуррентные модели (RNN/LSTM) | Граф не может зацикливаться; каждый шаг требует отдельного графа. |
| Операции с динамическим выделением | Например, torch.where с неизвестным числом ненулевых элементов. |
| Захват всего пайплайна с изменяемыми формами | Лучше захватывать только статические части (например, FFN, а attention с разными length выносить). |
Пример плохого случая LLM генерация с autoregressive decoding — длина растет с каждым шагом. При каждом новом токене длина последовательности меняется => граф надо перестроить. Для такого лучше использовать graph caching: предзаписать графы для разных длин (например, 128, 256, 512) и переключать между ними.
6. Ускорение в LLM инференсе
Практические фреймворки (vLLM, TensorRT-LLM) активно используют CUDA graphs для ускорения. Типичная схема:
- При запуске модели создается набор графов для разных длин последовательностей (block granularity).
- Во время инференса выбирается ближайший граф по длине (либо pad).
- Для коротких промптов (32–128 токенов) overhead практически исчезает.
Эксперименты
- LLaMA-7B, batch=1, prompt length=128: overhead ~50% от времени => после CUDA Graph acceleration ~20% относительно обычного запуска, общее ускорение 30–40% (за счет ускорения каждого kernel).
- GPT-175B с batch=1: overhead менее значим (compute доминирует), ускорение 5–10%.
7. Профилирование и отладка
Чтобы понять, нужны ли CUDA graphs, профилируйте:
sudo nsys profile -o profile_launch -t cuda,nvtx python inference.py
В отчёте ищите kernel launch overhead (раздел CUDA API). Если он >10% от общего времени — стоит внедрять.
Инструменты
ncu(Nsight Compute): детальный анализ ядерnsys(Nsight Systems): визуализация временной шкалы- PyTorch profiler:
torch.profiler.profileсprofile_memory=True
8. Связь с Agentic RAG
В архитектуре Agentic RAG агент может делать десятки последовательных вызовов LLM (tool calling, reasoning, retrieval summarization). Каждый вызов — короткий промпт (1024–2048 токенов). Если каждый вызов оптимизировать с помощью CUDA graphs, совокупная экономия latency может достигать 30–50%. Особенно это важно для real-time agent loops, где задержка в 100 мс критична.
Рекомендации для Agentic RAG
- Захватить граф для статической части модели (embedding, transformer layers) с фиксированной длиной.
- Для генерации (autoregressive) использовать caching графов по длинам.
- Если размер контекста меняется радикально (вызов разных инструментов), лучше захватить несколько графов под каждую типичную длину.
Пет-проект для закрепления
Задача Реализовать инференс небольшой LLM (GPT-2 124M) с CUDA Graph и без, замерить ускорение на коротких промптах.
Инструменты PyTorch 2.0+, CUDA 11+, Hugging Face Transformers, nsys.
Шаги:
- Загрузить модель
distilgpt2на GPU. - Подготовить вход фиксированной длины (128 токенов) с
torch.int64. - Написать функцию
forward_without_graph(), запустить 100 раз, замерить среднее время. - Реализовать capture graph:
with torch.no_grad(): # Статический буфер input_ids = torch.zeros(1, 128, dtype=torch.long, device='cuda') graph = torch.cuda.CUDAGraph() with torch.cuda.graph(graph): output = model(input_ids).logits - В цикле (100 раз) копировать новые input_ids в статический буфер (
buffer.copy_(new_inputs)), затемgraph.replay(). - Использовать
torch.cuda.synchronize()перед замером. - Профилировать через
nsysдля наглядности overhead. - Повторить для разных длин (64, 256, 512) и сравнить.
Ожидаемый результат
Ускорение 15–30% для длины 64–128 токенов; для длины 1024+ ускорение падает до 5–10%. График зависимости ускорения от длины промпта.
Связь с другими вопросами
| Вопрос | Тема |
|---|---|
| 844 | Как оптимизировать latency LLM инференса? (CUDA graphs как частный случай) |
| 846 | Что такое Flash Attention и как он работает? (часто комбинируется с CUDA graphs) |
| 847 | Как работает PagedAttention в vLLM? (использует CUDA graphs для статических частей) |
| 848 | Continuous batching: как уменьшить overhead? (схожая мотивация, другой подход) |
| 849 | Что такое Speculative Decoding? (может сочетаться с CUDA graphs для малых моделей) |
| 850 | Как вы профилируете и дебажите инференс LLM? (инструменты nsys/ncu) |
Навигация
- Предыдущий: 844
- Следующий: 846
- Индекс: 00. Индекс разборов