Запуск DeepSeek-V4-Flash-0731 на кластере из двух DGX Spark упирается в жёсткий лимит unified memory. Модель занимает 87-89 GiB под веса и активации, ещё 11.2 GiB уходит на KV-кэш. Операционной системе остаётся 5-7 GB. Стандартные рычаги - gpu-memory-utilization, max-model-len, swap и CPU offload - не работают. Причина в архитектуре: когда GPU и CPU делят одну физическую память, выгрузка данных в «медленную» область не освобождает место, а лишь перемещает его.
Мы проверили комбинацию из трёх работающих оптимизаций: настройка NCCL-буферов, сокращение RSS vLLM-процессов и включение torch.compile с CUDA graphs. Результат - стабильный serving без деградации скорости: 82 tok/s на decode и 1400 tok/s на prefill. В этом разборе - карта памяти, причины отказов стандартных методов и пошаговое руководство по высвобождению гигабайт на unified memory системах.
Введение: проблема нехватки памяти на unified memory системах
DeepSeek-V4-Flash-0731 - это модель класса Mixture of Experts с 304 миллиардами параметров. На бумаге цифра внушительная, но реальный memory footprint определяют активные эксперты, буферы под тензорный параллелизм и KV-кэш. Кластер из двух DGX Spark с unified memory даёт общий пул, однако пропускная способность доступа к разным его областям отличается. GPU и CPU видят одни и те же страницы, но контроллер памяти приоритизирует запросы неравномерно.
Стек serving: vLLM с tensor parallelism = 2 и speculative decoding. Спекулятивное декодирование добавляет накладные расходы на драфт-модель, а TP=2 требует NCCL-буферов для синхронизации тензоров между двумя GPU. Всё это съедает память, которой и так впритык.
Первый запуск показал: 87-89 GiB уходит на веса и активации, 11.2 GiB - на KV-кэш. Остаток в 5-7 GB съедает DGX OS с драйверами и системными демонами. Попытки ужать кэш через gpu-memory-utilization или ограничить контекст через max-model-len дали нулевой эффект. Swap и CPU offload не помогли по фундаментальной причине: на unified memory данные физически не покидают чипы. Мы оказались в ситуации, где стандартные рецепты из документации vLLM бесполезны.
Дальше - разбор архитектуры, карта памяти и три оптимизации, которые вернули контроль над ресурсами.
Архитектура модели и serving-стека: почему памяти не хватает
MoE и memory footprint: почему 304B не значит 304B в памяти
Mixture of Experts не загружает все 304 миллиарда параметров одновременно. На каждый токен активируются только несколько экспертов - обычно 2 из 8 или аналогичная пропорция. Память под веса выделяется полностью: vLLM загружает все эксперты в VRAM при старте. Плюс активации, плюс буферы под промежуточные тензоры. Реальный расход - 87-89 GiB, что близко к сумме весов в FP8 и накладных расходов фреймворка.
Сравнение с запуском на двух RTX 4090 показательно: там 48 GB суммарной VRAM, и модель влезает только благодаря агрессивной квантизации и портированным Blackwell-ядрам. На DGX Spark unified memory больше, но модель в нативном FP8/FP4 требует почти 90 GiB - это физический минимум без квантизации.
KV-кэш добавляет 11.2 GiB при стандартном max-model-len. Каждый слой сохраняет ключи и значения для всех предыдущих токенов. При длинных контекстах этот буфер растёт линейно. Спекулятивное декодирование создаёт дополнительные записи в кэше под драфт-токены, увеличивая расход на 10-15%.
Unified memory в DGX Spark: особенности и подводные камни
DGX Spark использует единый пул физической памяти для GPU и CPU. Контроллер памяти динамически распределяет страницы между запросами от GPU-ядер и CPU-ядер. Пропускная способность при доступе GPU к «своей» области достигает 273 GB/s, а при обращении к страницам, которые контроллер отдал CPU, падает в разы.
Swap и CPU offload на такой архитектуре теряют смысл. Данные, выгруженные из GPU-области, остаются в той же физической памяти. Контроллер просто помечает страницы как CPU-доступные. Освобождения места не происходит. Более того, фрагментация при частых перемещениях страниц между зонами ухудшает latency доступа.
DGX OS резервирует часть unified memory под драйверы, системные демоны и буферы ввода-вывода. Размер резерва варьируется от 3 до 6 GB в зависимости от версии прошивки. Это объясняет, почему при суммарных 100+ GB памяти свободными остаются жалкие 5-7 GB.
Почему стандартные методы не сработали: разбор неудачных попыток
gpu-memory-utilization и max-model-len: почему урезание не помогает
Параметр gpu-memory-utilization в vLLM ограничивает долю VRAM, которую фреймворк может занять под KV-кэш и аллокатор. При значении 0.90 vLLM резервирует 90% памяти GPU. Снижение до 0.85 или 0.80 не дало эффекта: веса модели занимают 87-89 GiB фиксированно, и аллокатор просто получает меньше места под кэш, что приводит к отказам при попытке обработать запрос.
Уменьшение max-model-len сокращает KV-кэш, но его доля в общем потреблении - 11.2 GiB из 100+. Даже при урезании контекста вдвое экономия составит 5-6 GB. Этого недостаточно, когда ОС требует минимум 5-7 GB, а запас прочности нужен для пиковых нагрузок.
Swap и CPU offload: почему unified memory сводит их на нет
На дискретных GPU swap выгружает страницы VRAM в системную RAM через PCIe. Физически память освобождается. На DGX Spark swap-раздел расположен на том же чипе unified memory. Выгрузка туда - это перемещение данных внутри одного физического пула. Занятое место не освобождается.
CPU offload в vLLM переносит часть весов или KV-кэша в оперативную память CPU. На unified memory это означает смену атрибутов страниц с GPU-доступных на CPU-доступные. Контроллер памяти не возвращает страницы в общий пул. Более того, при следующем обращении GPU к этим данным страницы снова переключаются, создавая задержки и фрагментацию. В наших тестах CPU offload увеличил latency decode на 40% без высвобождения памяти.
Работающие оптимизации: пошаговое руководство
Настройка NCCL-буферов: уменьшаем оверхед коммуникаций
При TP=2 vLLM использует NCCL для синхронизации тензоров между двумя GPU. NCCL выделяет буферы под коллективные операции: all-reduce, all-gather, reduce-scatter. Размер по умолчанию - 512 MB на операцию. При активном спекулятивном декодировании количество одновременных коллективных вызовов растёт, и суммарный расход достигает 2-3 GB.
Решение - уменьшить NCCL-буферы через переменные окружения:
export NCCL_BUFFSIZE=16777216 # 16 MB вместо 512 MB
export NCCL_NTHREADS=4
export NCCL_P2P_DISABLE=0 # P2P через unified memory работает быстрее
Снижение буфера до 16 MB сократило потребление на 1.8 GB. NCCL_NTHREADS=4 ограничивает число потоков, уменьшая RSS каждого процесса. P2P через unified memory оставляем включённым: прямой доступ GPU к страницам другого GPU быстрее, чем проксирование через CPU.
Дополнительно можно выставить NCCL_MIN_NCHANNELS=4, если наблюдаются таймауты при инициализации. Это ограничивает число каналов связи и снижает memory footprint инициализации.
Сокращение RSS vLLM-процессов: боремся с фрагментацией
RSS (Resident Set Size) показывает, сколько физической памяти занимает процесс. У vLLM с Python-рантаймом RSS часто превышает реально используемую память из-за фрагментации кучи и аллокатора PyTorch. Разница может достигать 3-5 GB.
Мониторинг через smem:
smem -t -P vllm
Выявляем процессы с высоким USS (Unique Set Size) и сравниваем с PSS (Proportional Set Size). Большая разница указывает на фрагментацию.
Методы сокращения:
- PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True - аллокатор PyTorch использует расширяемые сегменты, уменьшая фрагментацию при частых аллокациях/деаллокациях.
- jemalloc - замена стандартного аллокатора glibc на jemalloc через LD_PRELOAD. Снижает фрагментацию кучи Python на 15-20%.
- torch.cuda.empty_cache() - периодический вызов (раз в 100 запросов) освобождает кэшированные, но неиспользуемые блоки памяти PyTorch.
Комбинация этих методов вернула 2.7 GB. Суммарно с NCCL-оптимизацией свободная память выросла до 9-10 GB - достаточно для стабильной работы ОС.
Torch.compile и CUDA graphs: снижаем накладные расходы на графы вычислений
Torch.compile оптимизирует вычислительный граф, сливая последовательные операции в одно ядро. Промежуточные тензоры не материализуются в памяти, а передаются через регистры. Для MoE-моделей эффект заметен на этапе routing: вычисление gating-функции и сбор выходов экспертов объединяются в один проход.
CUDA graphs захватывают повторяющиеся последовательности запусков ядер и воспроизводят их без накладных расходов на планирование CPU-GPU. В vLLM это ускоряет decode-фазу, где паттерн вычислений одинаков для каждого токена.
Включение в vLLM:
--compilation-config '{"mode": "max-autotune"}' \
--cudagraph_mode FULL_AND_PIECEWISE
Предупреждение: MoE с динамическим routing может конфликтовать с CUDA graphs. Если при запуске появляются ошибки вида «captured graph does not match», переключите cudagraph_mode на PIECEWISE или отключите для слоёв экспертов через --cudagraph_exclude_layers.
Экономия памяти от torch.compile - 0.8-1.2 GB за счёт устранения промежуточных аллокаций. CUDA graphs добавляют стабильности, исключая всплески потребления при запуске новых ядер.
Медленный рост host-RSS: диагностика и предотвращение
При обработке длинных контекстов (64K+ токенов) host-RSS vLLM-процессов растёт на 100-200 MB в час. За сутки непрерывного serving это превращается в 2.4-4.8 GB утечки. Симптом проявляется даже при стабильном GPU-потреблении.
Причины три:
- Фрагментация кучи Python: объекты KV-кэша в host-памяти создаются и удаляются, но аллокатор не возвращает страницы ОС.
- Неосвобождаемые буферы в vLLM: очередь запросов держит ссылки на завершённые последовательности до очистки сборщиком мусора.
- Особенность unified memory: страницы, к которым обращался GPU, получают приоритет и не возвращаются в общий пул.
Мониторинг через Grafana + Prometheus с метрикой process_resident_memory_bytes показывает тренд. Временное решение - перезапуск worker-ов раз в 12-24 часа с graceful draining очереди. Кардинальное - патч vLLM с принудительным вызовом malloc_trim(0) после каждого decode-батча.
Резервирование DGX OS: можно ли отжать память у системы?
DGX OS резервирует unified memory под драйверы GPU, демоны мониторинга и файловую систему. Размер резерва виден через:
cat /proc/meminfo | grep -E "MemTotal|MemFree|Buffers|Cached|SReclaimable"
Разница между MemTotal и суммой MemFree+Buffers+Cached+SReclaimable - это несжимаемый резерв ядра. На DGX Spark он составляет 3-4 GB.
Уменьшить резерв можно через kernel-параметры:
sysctl -w vm.min_free_kbytes=65536 # снижаем с 256 MB до 64 MB
sysctl -w vm.vfs_cache_pressure=200 # агрессивнее вытеснять inode/dentry кэш
Предупреждение: vm.min_free_kbytes ниже 65536 на системе с GPU-нагрузкой рискует вызвать OOM-killer при пиковых аллокациях. Рекомендуем этот шаг только после исчерпания всех остальных оптимизаций и с мониторингом доступной памяти в реальном времени.
Итоговый конфиг: запуск DeepSeek-V4-Flash-0731 на 2× DGX Spark
Сборка всех оптимизаций в одну команду запуска vLLM:
#!/bin/bash
export NCCL_BUFFSIZE=16777216
export NCCL_NTHREADS=4
export NCCL_P2P_DISABLE=0
export PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True
export LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so
vllm serve deepseek-ai/DeepSeek-V4-Flash-0731 \
--tensor-parallel-size 2 \
--max-model-len 65536 \
--gpu-memory-utilization 0.92 \
--compilation-config '{"mode": "max-autotune"}' \
--cudagraph_mode FULL_AND_PIECEWISE \
--speculative-config '{"method": "dspark", "num_speculative_tokens": 2}' \
--max-num-seqs 8 \
--enable-prefix-caching
Пояснение параметров:
- tensor-parallel-size 2 - распределение тензоров между двумя GPU DGX Spark.
- max-model-len 65536 - лимит контекста, достаточный для большинства задач и удерживающий KV-кэш в рамках 11.2 GiB.
- gpu-memory-utilization 0.92 - агрессивное значение, ставшее возможным после оптимизаций NCCL и RSS.
- compilation-config max-autotune - включает torch.compile с автотюнингом ядер.
- cudagraph_mode FULL_AND_PIECEWISE - захват графов для всех фаз, с fallback на piecewise для динамических операций MoE.
- speculative-config dspark - спекулятивное декодирование с 2 драфт-токенами, даёт прирост decode-скорости без значительного роста памяти.
- max-num-seqs 8 - ограничение конкурентных запросов, предотвращает раздувание очереди и рост host-RSS.
- enable-prefix-caching - переиспользование KV-кэша для общих префиксов, экономит память при пакетной обработке.
С этим конфигом кластер стабильно держит нагрузку: 82 tok/s на decode, 1400 tok/s на prefill. Свободной памяти - 9-10 GB, достаточно для пиковых аллокаций и системных нужд.
Для сравнения с альтернативными подходами к запуску DeepSeek-V4-Flash на ограниченном железе полезны материалы по запуску MoE-моделей через llama.cpp с regex offload и кастомным квантизациям на Apple Silicon. Там другие компромиссы, но принцип управления памятью схож.
Заключение: ключевые выводы и дальнейшие шаги
Unified memory в DGX Spark ломает привычные паттерны оптимизации. Swap и CPU offload не работают, потому что данные физически не покидают чип. gpu-memory-utilization и max-model-len дают мизерную экономию на фоне 87-89 GiB, занятых весами.
Рабочая стратегия - атака с трёх направлений. Настройка NCCL-буферов освобождает 1.8 GB за счёт уменьшения оверхеда коммуникаций при TP=2. Сокращение RSS через jemalloc и expandable_segments возвращает 2.7 GB. Torch.compile и CUDA graphs добавляют 0.8-1.2 GB, устраняя промежуточные аллокации. Суммарно получаем 9-10 GB свободной памяти вместо исходных 5-7 GB.
Рост host-RSS остаётся открытой проблемой. Мониторинг и периодический перезапуск worker-ов - временное решение. Нужен патч vLLM с агрессивным возвратом страниц ОС. Резервирование DGX OS можно ужать через kernel-параметры, но это крайняя мера с риском OOM.
Если вы экспериментировали с другими оптимизациями на unified memory или нашли способ стабилизировать host-RSS - поделитесь опытом. Практические цифры и конфиги полезнее общих советов.