Подробные статьи о технологиях, определяющих будущее.

Когда у GPU заканчивается VRAM: что делать

VRAM видеокарты — узкое место для ИИ и графики. Разбираемся, как на самом деле работают unified memory, выгрузка в VRAM и подкачка на NVMe.

Видеокарта, из которой через край льются светящиеся данные и стекают в накопитель внизу

Самая распространённая ошибка в машинном обучении — это не traceback в Python и не несовпадение размерностей. Это CUDA out of memory. Модель слишком большая, batch size слишком велик или промежуточные активации не помещаются в VRAM вашей видеокарты. В RTX 4090 её 24 ГБ. 70B-модель в половинной точности требует около 140 ГБ. Математика не сходится, а покупка более мощных GPU лишь откладывает проблему: H100 с 80 ГБ всё равно не вместит самые крупные модели в одну карту.

Но что если можно прозрачно расширить память GPU за счёт оперативной памяти или даже NVMe-накопителя? Инструменты вроде Greenboost от NVIDIA и похожие проекты делают именно это: используют иерархию памяти (VRAM → оперативная память → SSD), чтобы запускать нагрузки, которые на вашем железе формально не должны помещаться. Компромиссы по производительности реальные, но для многих сценариев на удивление терпимые. Чтобы понять, как это работает, нужно разобраться в самой иерархии памяти GPU.

Чем память GPU отличается от памяти CPU

Память CPU и GPU устроена под принципиально разные паттерны доступа. Нагрузки на CPU чувствительны к задержкам: один поток запрашивает кусок данных и блокируется, пока тот не придёт. Кэши CPU спроектированы так, чтобы минимизировать задержку при случайном доступе.

Нагрузки на GPU чувствительны к пропускной способности: тысячи потоков ждут свои данные, и GPU может переключаться между ними, скрывая задержки. Память GPU (HBM на серверных ускорителях, GDDR на потребительских картах) создана ради пропускной способности: она доставляет огромные объёмы данных в секунду, даже если отдельное обращение занимает больше времени, чем попадание в кэш CPU.

Memory bandwidth comparison (approximate):
RTX 4090 GDDR6X:     1,000 GB/s
H100 HBM3:           3,350 GB/s
DDR5 System RAM:       50 GB/s
PCIe 5.0 x16:          64 GB/s (theoretical max)
NVMe SSD:               7 GB/s
The bandwidth cliff between VRAM and system RAM is ~20x.
Between VRAM and NVMe it's ~140x.
This is why naive offloading to system RAM kills performance —
you're trying to feed a 1,000 GB/s appetite through a 50 GB/s straw.

Именно этот разрыв в пропускной способности делает наивную идею «виртуальной» памяти GPU — подкачки данных в оперативную память, как CPU делает с диском, — нерабочей. CPU переживает page fault с замедлением, быть может, в 10 раз. Когда же GPU обращается к оперативной памяти вместо VRAM, пропускная способность падает примерно в 20 раз, а для нагрузок, ограниченных пропускной способностью (а это большинство инференса ML), это означает и 20-кратное замедление.

Как на самом деле работает выгрузка в VRAM

Фокус не в том, чтобы считать оперативную память медленной VRAM. Суть в том, чтобы подгружать данные из оперативной памяти в VRAM заранее, до того как они понадобятся GPU, и прятать задержку за вычислениями. Это ключевая идея каждой практичной техники расширения VRAM.

Во время инференса нейросети вычисления идут последовательно по слоям. Пока GPU обрабатывает слой 5, он знает, что следующим будет слой 6. Умная система выгрузки может начать перенос весов слоя 6 из оперативной памяти в VRAM, пока слой 5 ещё считается. Если вычисления длятся дольше передачи (что часто бывает с большими слоями), перенос полностью скрывается, и GPU ни разу не простаивает.

# Conceptual overlap of compute and transfer
# (simplified pseudocode)
def inference_with_offloading(model, input_data):
# Only 2 layers fit in VRAM at a time
# Rest are in system RAM
for i, layer in enumerate(model.layers):
# Start async transfer of NEXT layer while computing current
if i + 1 < len(model.layers):
async_transfer_to_gpu(model.layers[i + 1])
# Compute on current layer (GPU is busy, transfer happens in parallel)
output = layer.forward(input_data)
# Evict current layer from VRAM (it's done)
transfer_to_ram(layer)
# Wait for next layer transfer to complete (usually already done)
sync_transfer()
input_data = output
return output

Такой конвейерный подход хорошо работает для инференса, потому что граф вычислений предсказуем: заранее известно, какие веса понадобятся дальше. Обучение сложнее, ведь backward pass требует активации из forward pass, и паттерны перемещения данных становятся запутаннее.

Подходы на практике

CUDA Unified Memory

CUDA Unified Memory от NVIDIA создаёт единое адресное пространство, охватывающее и VRAM, и оперативную память. Рантайм CUDA автоматически переносит страницы между ними в зависимости от паттернов доступа. Когда GPU обращается к странице в оперативной памяти, возникает page fault, и страница мигрирует в VRAM.

Плюс в том, что это прозрачно для приложения: CUDA-код не должен управлять размещением данных. Минус — page fault'ы дороги, а эвристики миграции в рантайме не всегда совпадают с паттерном доступа приложения. Для предсказуемых нагрузок вроде инференса нейросетей явное управление работает быстрее автоматической миграции.

Послойная выгрузка

Инструменты вроде Hugging Face Accelerate, DeepSpeed ZeRO-Inference и флаг --mmap в llama.cpp реализуют явную послойную выгрузку. Они держат в VRAM только активные слои, а остальное подгружают из оперативной памяти или с диска. Модели не нужно целиком помещаться в VRAM — достаточно, чтобы в ней одновременно умещались один-два слоя.

Именно так и работает запуск 70B-моделей на потребительском железе. 4-битная квантованная 70B-модель занимает около 40 ГБ целиком, но любой отдельный слой — лишь около 1–2 ГБ. Имея 24 ГБ VRAM и префетчинг, можно запускать модель с умеренной потерей скорости — возможно, на 30–50% медленнее, чем если бы она целиком помещалась в VRAM.

NVMe как расширенная память

Самый агрессивный подход использует NVMe-SSD как третий уровень памяти GPU. Пропускная способность по сравнению с VRAM ужасна (~7 ГБ/с против ~1000 ГБ/с), зато ёмкость практически безгранична. Накопитель NVMe на 4 ТБ стоит около $200 и может одновременно хранить десятки крупных моделей.

Проекты вроде Greenboost от NVIDIA реализуют это прозрачно: память GPU расширяется на NVMe, а интеллектуальный префетчинг минимизирует влияние ограниченной пропускной способности. Для инференса, где GPU большую часть времени занят вычислениями, а не перемещением данных, задержку NVMe можно полностью скрыть за счёт перекрытия с вычислениями.

Многое зависит от нагрузки. Вычислительно ёмкие операции (большие матричные умножения) хорошо прячут задержку передачи. Операции, ограниченные памятью (механизм внимания с большим контекстом), — нет. На практике выгрузка на NVMe лучше всего работает для пакетного инференса крупных моделей с небольшим batch size — как раз для сценария локального инференса LLM.

Преимущество unified memory у Apple

Архитектура unified memory в Apple Silicon подходит к задаче совсем иначе: она убирает различие между VRAM и оперативной памятью. CPU и GPU используют один и тот же физический пул памяти. Никакой «выгрузки» нет, потому что нет разделения: GPU обращается к той же памяти, что и CPU.

Это не решает проблему пропускной способности: у памяти M-серии (~400 ГБ/с у M4 Max) она ниже, чем у выделенной видеокарты, — но устраняется узкое место PCIe, которое делает выгрузку на дискретную GPU медленной. Mac с 128 ГБ unified memory может целиком разместить 70B-модель в памяти, доступной GPU, без каких-либо накладных расходов на выгрузку.

Цена вопроса — более низкий пиковый throughput по сравнению с дискретной GPU, когда нагрузка целиком помещается в её VRAM, но заметно лучшая производительность, когда не помещается. Для инференса больших моделей, которые не влезают в VRAM дискретной карты, Apple Silicon часто быстрее дискретной GPU с выгрузкой, несмотря на меньшую сырую вычислительную мощность.

Что это значит для разработчиков

Если вы пишете приложения на GPU — ML-инференс, графика, научные вычисления, — ограничение по VRAM влияет на архитектурные решения вполне конкретным образом.

  • Знайте размер рабочего набора. Профилируйте использование памяти GPU. Важен не пиковый объём выделений, а рабочий набор в каждый момент времени. Если пик составляет 48 ГБ, но ни одной операции не нужно больше 8 ГБ активных данных, выгрузка будет работать хорошо. Если одна операция действительно требует 48 ГБ одновременно, нужна более мощная GPU.
  • Выбирайте стратегию выгрузки по паттерну доступа. Последовательный доступ (послойный инференс) отлично работает с префетчингом. Случайный доступ (внимание по большому контексту) — нет. Понимайте, какому паттерну следует ваша нагрузка.
  • Квантизация обычно дешевле выгрузки. Перевод модели с FP16 на INT4 сокращает память в 4 раза при умеренной потере качества. Выгрузка в оперативную память добавляет задержку, не трогая качество, но экономит память ограниченно. Сначала квантизация, потом выгрузка.
  • Batch size — ваш главный регулятор. Большие батчи требуют больше памяти, зато лучше амортизируют накладные расходы. Маленькие батчи экономят память, но обрабатывают меньше элементов в секунду. Если вы близки к пределу VRAM, самое простое решение — уменьшить batch size.
  • Следите за фрагментацией памяти. Аллокатор CUDA со временем может фрагментировать VRAM, особенно при входах переменной длины. Свободно может быть 8 ГБ, но ни одного непрерывного блока в 2 ГБ нет. torch.cuda.memory_stats() в PyTorch показывает фрагментацию. torch.cuda.empty_cache() может помочь, но это не панацея.

Память GPU всегда будет узким местом крупных вычислительных задач: модели растут быстрее, чем VRAM. Но инструменты для управления этим узким местом — unified memory, интеллектуальная выгрузка, многоуровневое кэширование — становятся достаточно зрелыми, чтобы «не помещается в VRAM» перестало быть жёстким барьером. Это компромисс по производительности, и всё чаще — управляемый.