Что такое activation offloading и когда он нужен?
Краткий тезис
Activation offloading — это техника перемещения промежуточных активаций (выходов слоёв нейросети) из GPU VRAM в CPU RAM (или NVMe-накопитель) во время forward pass и обратная загрузка в GPU во время backward pass. Она необходима для обучения моделей с очень длинным контекстом (>50k токенов), когда активации не помещаются в доступную видеопамять. Основная цена — замедление обучения в 3–5 раз из-за передачи данных по шине PCIe.
1. Термин: Активации (Activations)
Активации — это выходные тензоры каждого слоя нейронной сети после применения функции активации (например, ReLU, GeLU). Во время forward pass они вычисляются и сохраняются, потому что нужны для вычисления градиентов на этапе backward pass.
Почему активации занимают много памяти:
- Размер активаций пропорционален: batch_size × seq_len × hidden_dim × num_layers
- Для LLM (например, Llama 2 7B с hidden_dim=4096, 32 слоя) при
seq_len=32kи batch_size=1 активации занимают ~32 × 4096 × 32 × 2 байта (fp16) ≈ 8.6 GB - При
seq_len=128k— уже ~34 GB, что превышает объём VRAM многих GPU (A100 80GB — предел)
Термин «VRAM» (Video Random Access Memory) — видеопамять GPU, используется для хранения параметров, градиентов, оптимизатора и активаций.
2. Проблема: Ограничение GPU памяти при длинном контексте
Современные LLM требуют огромных объёмов памяти при обучении на длинных последовательностях. Даже с gradient checkpointing (рекомпьютация активаций) может не хватать:
| Техника | Память (активации) | Время |
|---|---|---|
| Без оптимизации | ~O(L²) для attention, ~O(L) для FFN | 1x |
| Gradient checkpointing | ~O(√L) или ~O(log L) | ~1.2–1.3x |
| Activation offloading | ~O(1) на GPU (выгружено в CPU) | ~3–5x |
Gradient checkpointing — техника, при которой часть активаций не сохраняется, а пересчитывается заново во время backward. Это снижает память, но увеличивает время.
Однако при seq_len > 50k даже checkpointing может не спасти, потому что:
- Кэш KV (Key-Value) для attention растёт линейно с длиной контекста и занимает ~2 × batch_size × num_heads × seq_len × head_dim × num_layers байт
- Активации FFN (Feed-Forward Network) тоже велики
Тогда единственный выход — выгружать активации в CPU.
3. Что такое Activation Offloading
Activation offloading — это процесс:
- Во время forward pass: после вычисления активаций каждого слоя (или группы слоёв) они асинхронно копируются из GPU в CPU RAM (или NVMe).
- Во время backward pass: перед тем как слой понадобится для вычисления градиентов, активации prefetching'ом загружаются обратно в GPU.
Реализация обычно использует CUDA streams для перекрытия передачи данных и вычислений:
- Вычисления на одном stream, передача на другом
- Можно использовать pinned memory (закреплённая память CPU) для ускорения копирования
Термин «pinned memory» — страницы памяти CPU, которые не выгружаются на диск, что позволяет GPU обращаться к ним напрямую через DMA (Direct Memory Access).
4. Когда нужен Activation Offloading
Основные сценарии:
| Сценарий | Пример | Необходимость |
|---|---|---|
| Обучение с очень длинным контекстом | >50k токенов (целые книги, длинные документы, видео) | Критично: активации не помещаются в VRAM |
| Обучение на GPU с ограниченной памятью | RTX 3090 (24GB) для моделей 7B+ с seq_len=32k | Часто необходимо |
| Инференс с длинным контекстом | Генерация с context window >100k | Обычно не нужно (KV cache offloading — другая техника) |
| Fine-tuning с длинными последовательностями | LoRA на длинных документах | Может быть полезно, если batch_size мал |
Когда не нужен
- Короткие контексты (<8k) — активации обычно помещаются
- Наличие GPU с большим VRAM (A100 80GB, H100 80GB, B200)
- Инференс без backward (активации не хранятся)
5. Цена и компромиссы
Основная цена — скорость передачи по PCIe:
- PCIe 4.0 x16: пропускная способность ~32 GB/s (теоретически), на практике ~25 GB/s
- PCIe 5.0 x16: ~63 GB/s
- NVMe SSD: ~3–7 GB/s (если offload на диск)
Для модели 7B с активациями ~10 GB на один шаг:
- Время копирования туда-обратно: 2 × 10 GB / 25 GB/s ≈ 0.8 сек
- Время одного шага без offloading: ~0.5–1 сек
- Итоговое замедление: 2–3x (если перекрывать вычисления и передачу)
На практике замедление 3–5x из-за:
- Наложения передачи на вычисления не всегда идеально
- Дополнительных накладных расходов на управление памятью
- Возможной фрагментации памяти
Компромисс: память vs скорость — offloading позволяет обучать модели, которые иначе не влезли бы, но ценой времени.
6. Альтернативы и сравнение
| Техника | Память (активации) | Скорость | Сложность реализации |
|---|---|---|---|
| Gradient checkpointing | Умеренное снижение (2–4x) | ~1.2–1.3x | Низкая (встроено в PyTorch) |
| Activation offloading | Сильное снижение (10x+) | ~3–5x | Средняя (требует ручного управления) |
| ZeRO-Offload (DeepSpeed) | Оптимизатор + градиенты на CPU | ~1.5–2x | Низкая (конфигурация) |
| FSDP with CPU offload | Параметры + градиенты на CPU | ~2–3x | Средняя (требует настройки) |
| FlashAttention | Снижение памяти attention (O(L) вместо O(L²)) | ~1x (даже быстрее) | Низкая (замена модуля) |
Важно activation offloading часто комбинируют с gradient checkpointing и FlashAttention для максимальной экономии памяти.
7. Реализация на практике
Пример псевдокода activation offloading в PyTorch с использованием CUDA streams:
import torch
class OffloadedLayer(torch.nn.Module):
def __init__(self, layer):
super().__init__()
self.layer = layer
self.offload_stream = torch.cuda.Stream()
self.activations_cpu = [] # храним на CPU
def forward(self, x):
# Вычисляем активации
act = self.layer(x)
# Асинхронно копируем на CPU
with torch.cuda.stream(self.offload_stream):
act_cpu = act.to('cpu', non_blocking=True)
self.activations_cpu.append(act_cpu)
return act
def backward(self, grad_output):
# Загружаем активации обратно на GPU
with torch.cuda.stream(self.offload_stream):
act_gpu = self.activations_cpu.pop().to('cuda', non_blocking=True)
# Синхронизируем потоки
torch.cuda.current_stream().wait_stream(self.offload_stream)
# Вычисляем градиенты
return torch.autograd.Function.apply(self.layer, act_gpu, grad_output)
На практике используют готовые библиотеки:
- DeepSpeed — параметр
activation_offloadingв конфиге ZeRO-3 - FairScale —
OffloadModelдля offloading активаций - PyTorch FSDP —
cpu_offloadдля параметров, но не активаций
8. Связь с другими техниками оптимизации
Gradient accumulation — накопление градиентов за несколько шагов. Не снижает память активаций, но позволяет увеличить эффективный batch size.
Mixed precision training (fp16/bf16) — уменьшает размер активаций вдвое по сравнению с fp32. Обязательно использовать вместе с offloading.
FlashAttention — эффективная реализация attention, которая не хранит полную матрицу внимания (O(L²) → O(L)). Это снижает объём активаций attention, но не FFN.
Pipeline parallelism — распределение слоёв по разным GPU. Активации передаются между устройствами, что может быть альтернативой offloading.
9. Практические рекомендации
- Профилируйте память — используйте
torch.cuda.memory_summary()иnvidia-smiдля понимания, что именно занимает память. - Начинайте с gradient checkpointing — это проще и даёт хороший выигрыш.
- Добавляйте activation offloading только если checkpointing не хватает.
- Используйте pinned memory для CPU — ускоряет передачу.
- Оптимизируйте размер активаций — уменьшайте hidden_dim, используйте LoRA, снижайте batch_size.
- Комбинируйте с FlashAttention — это снижает нагрузку на attention.
- Тестируйте на маленьких данных — проверьте, что offloading работает корректно.
Пет-проект для закрепления
Задача Реализовать activation offloading для обучения небольшой модели (например, GPT-2) на последовательностях длиной 8192 токенов и сравнить с baseline (gradient checkpointing).
Инструменты PyTorch, CUDA streams, GPT-2 из Hugging Face, torch.cuda.memory_summary().
Шаги:
- Загрузите предобученный GPT-2 (124M параметров).
- Создайте датасет из длинных текстов (например, книги из Project Gutenberg).
- Обучите модель с gradient checkpointing (
model.gradient_checkpointing_enable()) — замерьте время шага и пиковую память. - Реализуйте activation offloading для нескольких последних слоёв (где активации самые большие) с помощью CUDA streams.
- Обучите с offloading — замерьте те же метрики.
- Сравните результаты в таблице.
Ожидаемый результат
- Пиковая память GPU снизится на 20–40%
- Время шага увеличится в 2–3 раза
- Потери в качестве (perplexity) не будет, если offloading реализован корректно
Связь с другими вопросами
| Вопрос | Тема |
|---|---|
| 478 | Gradient checkpointing (рекомпьютация активаций) |
| 480 | FlashAttention (эффективное внимание) |
| 477 | ZeRO-Offload (оффлоад оптимизатора и градиентов) |
| 481 | Mixed precision training (fp16/bf16) |
| 482 | Pipeline parallelism (распределение слоёв) |
| 476 | Distributed training (общие принципы) |
Навигация
- Предыдущий: 478
- Следующий: 480
- Индекс: 00. Индекс разборов