
Распределение веса модели в памяти двух видеокарт в stable-diffusion.cpp
Современные диффузионные модели генерации изображений класса DiT (Diffusion Transformers), например, FLUX, для комфортной работы требуют больших массивов видеопамяти (десятки ГБ VRAM). Поэтому их локальный запуск на видеокартах прошлых поколений подчастую является невозможным или трудно реализуемым. Попытка запустить мало-мальски приличную по размеру и возможностям модель на одной карте с объемом 6 ГБ приводят либо к критической ошибке нехватки памяти (Out of Memory, OOM), либо к катастрофическому падению скорости из-за постоянной откачки слоев в системную оперативную память (SysMEM fallback) по относительно медленной шине PCI-Express.
В статьях «stable-diffusion.cpp — отличная альтернатива ComfyUI» и «Генерация картинок в zimage-turbo на 6GB видеокарте» были рассмотрены некоторые возможности инференс-движка stable-diffusion.cpp по генерации изображений. При дальнейшем, более глубоком изучении возможностей этого вычислительного бэкенда выяснилось, что он позволяет распределять веса моделей в памяти разных устройств, причем не только в оперативную память, но и на другие видеокарты, установленные в компьютере, либо подключенные по сети (как у lama.cpp).
Вызывает интерес возможность запуска более серьезных моделей генерации и обработки изображений на связке из двух слабых видеокарт, каждая из которых имеет по 6GB VRAM, например, на Nvidia GeForce GTX 1660Ti плюс GTX 1660S 6GB.
Исходные условия, настройка железа для инференса
Для максимального задействования доступной видеопамяти на компьютере установлена операционная система Gentoo с с быстрой системой инициализации openrc и легковесным окружением xfce+lightdm:

Вывод изображения настроен через встроенную графику Intel HD630 через xorg.conf:
Section "ServerLayout"
Identifier "Layout0"
Screen 0 "Screen0"
Option "AllowNVIDIAGPUScreens"
EndSection
Section "ServerFlags"
Option "Xinerama" "false"
EndSection
# Intel HD 630 (основной дисплей)
Section "Device"
Identifier "IntelGraphics"
Driver "modesetting"
BusID "PCI:0:2:0"
Option "AccelMethod" "glamor"
Option "DRI" "3"
Option "TearFree" "true"
Option "XLIB_SKIP_ARGB_VISUALS" "true"
Option "XembedNoTransparency" "true"
EndSection
# Nvidia GTX 1660 Ti (Вычисления CUDA / PRIME)
Section "Device"
Identifier "Nvidia_GTX1660Ti"
Driver "nvidia"
BusID "PCI:1:0:0"
Option "AllowEmptyInitialConfiguration" "true"
Option "ConnectedMonitor" "None"
Option "IgnoreEDID" "true"
Option "Coolbits" "31" # Разгон и управление вентиляторами
EndSection
# Nvidia GTX 1660 Super (Вычисления CUDA)
Section "Device"
Identifier "Nvidia_GTX1660S"
Driver "nvidia"
BusID "PCI:2:0:0"
Option "AllowEmptyInitialConfiguration" "true"
Option "ConnectedMonitor" "None"
Option "IgnoreEDID" "true"
Option "Coolbits" "31"
EndSection
# Screen (работает через интегрированную видеокарту Intel)
Section "Screen"
Identifier "Screen0"
Device "IntelGraphics"
DefaultDepth 24
SubSection "Display"
Depth 24
Modes "1920x1080"
EndSubSection
EndSection
Благодаря этому сведено до минимума влияние графической оболочки (X11/Wayland), композитора и браузеров на VRAM целевых карт.
В такой конфигурации nvidia-smi на обеих шестигигабайтных видеокартах до запуска движка показывает по шесть МиБ памяти, занятой сервером X,:

Теория по распределению весов AI-моделей для работы с изображениями и мультимедиа в stable-diffusion.cpp
Теоретически каждая из видеокарт имеет по 6144 MiB GDDR6 видеопамяти, но для работы даже в linux доступно гораздо меньше. Это связано с влиянием следующих факторов:
1. Вычитание технического буфера (Compute-Buffer Headroom)
Движок инференса еще ло начала работы резервирует неприкосновенный запас памяти на каждом устройстве. Этот буфер необходим для размещения промежуточных графов вычислений, контекста выполнения CUDA, оперативного кэша активаций и временных тензоров.
Как только программа обращается к библиотеке libcuda.so, в памяти видеокарты создается CUDA Context (виртуальная среда управления потоками, распределения задач по ядрам и работы с памятью). В зависимости от версии драйвера Nvidia на это автоматически уходит 300-600 МиБ VRAM на каждую карту.
Буфер графа вычислений забирает себе еще один непрерывный кусок VRAM под промежуточные результаты вычислений (активации слоев). Его размер напрямую зависит от разрешения генерируемой картинки (например, 512×512 или 1024×1024) и размера батча. Чем больше тензор на входе, тем массивнее промежуточные матрицы. Движок закладывает ~1 ГБ, чтобы гарантировать, что граф не упрется в нехватку памяти посреди шага генерации.
Кэш контекста KV и планировщик GGML также требуют пространства в памяти под кэширование ключей и значений (KV Cache), синхронизацию потоков между CUDA-блоками.
Согласно документации sd.cpp, размер памяти, выделяемой на технические расходы фиксирован и составляет около 2 ГиБ (2048 МиБ) на каждый активный GPU. Таким образом, доступная память для весов равна:
- GPU 0 (CUDA0, Nvidia_GTX1660Ti): 6144 — 2048 = 4096 МиБ
- GPU 1 (CUDA1, Nvidia_GTX1660S): 6144 — 2048 = 4096 МиБ
За вычетом технического буфера, суммарный пул VRAM под веса AI-трансформера составляет 4096+4096 = 8192 МиБ (8ГБ).
2. Пропорциональное распределение блоков (Transformer Blocks Allocation)
AI-движок сканирует архитектуру модели и нарезает блоки трансформера на непрерывные диапазоны (contiguous ranges). Количество блоков, отправляемых на конкретную карту, пропорционально значению её индивидуальной памяти. Так как видеокарты идентичны и имеют равный объем доступной памяти (по 4096 МиБ), коэффициент распределения составляет ровно 50/50.
Если модель содержит, к примеру, 18 тяжелых блоков трансформера, планировщик по умолчанию отдаст:
- Блоки с 1 по 9 на CUDA0
- Блоки с 10 по 18 — на CUDA1
3. Асимметричная нагрузка на главное устройство (Main Device)
Первая карта в строке инициализации в sd.cpp (видеокарта CUDA0) назначается главным устройством модуля. На неё возлагается дополнительная нагрузка, из-за чего её реальный расход памяти всегда выше, чем у ведомой CUDA1. Помимо своей половины блоков трансформера, CUDA0 обязана удерживать в своей памяти:
- Не-блочные тензоры (Non-block tensors): Входные эмбеддинги, финальные слои нормализации (final norms) и проекторы.
- Вспомогательные под-модули (Small sub-runners): Например, легковесные базовые модели CLIP, которые не были вынесены на другие бэкенды.
- Входные и выходные тензоры всего вычислительного графа: Точки, куда подается шум и откуда забирается готовый латентный образ.
4. Синхронизация через Residual Stream (Шина PCIe 3.0 или канал связи локальной сети как узкое горлышко)
В процессе инференса каждая карта считает свои блоки локально на максимальной аппаратной скорости CUDA-ядер. Однако на стыке диапазонов блоков (например, при переходе от вычисления 9-го блока на CUDA0 к 10-му блоку на CUDA1) планировщик вынужден передавать промежуточные данные. Передается так называемый остаточный поток (residual stream) — вектор состояний скрытого пространства модели. При этом данные копируются из памяти CUDA0 в память CUDA1 через системную шину материнской платы.
Скорость обмена данными ограничивается полосой пропускания кнаала связи, в данном случае — интерфейсом PCIe GEN 3.0.
- Учитывая указанные ограничения, максимальный размер выбранной модели для двух параллельно работающих видеокарт с общим объемом видеопамяти 12GB не может превышать 8GB. Это все же лучше, чем при использовании только одного GPU и позволяет запускать не только квантованные до Q4-Q5 модели zimage-turb, но и менее зажатые Q8.0-версии, либо FLUX-Schnell и другие.
При распределении нагрузки нужно учитывать, что на главной карте при параллельной работе потребление памяти существенно выше (на 1–1.5 ГБ)из-за «якоря» в виде эмбеддингов и системных оффлоадов. Поэтому реально модель весом ровно 8 ГБ в памяти двух видекарт может и не заработать.
Для уменьшения размер оверхеда при сборке stable-diffusion.cpp можно:
- максимально задействовать флаги оптимизации в make/cmake:
git clone --recursive https://github.com/leejet/stable-diffusion.cpp
cd stable-diffusion.cpp
cmake -B build \
-DCMAKE_BUILD_TYPE=Release \
-DGGML_CUDA=ON \
-DGGML_RPC=ON \
-DGGML_NCCL=ON \
-DGGML_CCACHE=OFF \
-DGGML_CUDA_FORCE_MMQ=ON \
-DCMAKE_CUDA_ARCHITECTURES=75 \
-DCMAKE_C_FLAGS="-O3 -march=native -pipe -flto" \
-DCMAKE_CXX_FLAGS="-O3 -march=native -pipe -flto" \
-DCMAKE_CUDA_FLAGS="-O3" \
&& cmake --build build -j$(nproc)
- при запуске использовать параметр:
--batch-count 1
- уменьшить разрешение генерации до —width 512 —height 512.
Благодаря этому теоретически можно снизить выделение VRAM на технические расходы с 2 ГБ до ~1.2–1.4 ГБ. Это неизбежная плата за высокую скорость параллельных матричных вычислений без задержек на динамическую аллокацию памяти».

Запуск генерации изображения с разбиением веса модели на две видеокарты Nvidia
Для активации гибридного режима работы с параллельным использованием двух видеокарт в sd.cpp (алгоритм планировщика ggml_backend_sched) в скрипте запуска можно использовать следующие аргументы:
--backend "diffusion=CUDA1&CUDA0,te=CUDA1,vae=CUDA1" \
--params-backend "diffusion=CUDA1,te=CPU,vae=CUDA1" \
--split-mode layer
или
--split-mode row
--auto-fit
где
- backend «diffusion=CUDA1&CUDA0,te=CPU,vae=CUDA1» — конфигурация вычислительных бэкендов (runtime backends) для каждого изолированного ИИ-модуля. Диффузионная DiT-сеть параллельно рассчитывается на обеих видеокартах в режиме Layer Split (послойного параллелизма), где первая в списке карта CUDA1 (GTX 1660 SUPER) становится «главной» и принимает на себя служебный оверхед (LoRA-слои, входные/выходные матрицы графа). Текстовый энкодер te (Qwen3-4B) принудительно выносится на ядра центрального процессора (CPU) для выполнения расчетов в системной памяти, а легкий vae-декодер монопольно обрабатывается графическим чипом CUDA1.
- params-backend «diffusion=CUDA1,te=CPU,vae=CUDA1» — конфигурация бэкендов резидентного хранения весов моделей в физической памяти. Поскольку данный флаг не принимает списки устройств через амперсанд (&), для диффузии здесь явно указывается только главная карта диапазона (CUDA1), что позволяет планировщику ggml_backend_sched легально инициализировать размещение весов и автоматически распределить блоки трансформера на ведомую CUDA0 при построении графа. Веса текстового энкодера жестко запираются в оперативной памяти ПК (te=CPU), полностью освобождая около 4 ГБ физической видеопамяти на обеих картах перед фазой денойзинга.
- auto-fit — автоматическое распределение diffusion/te/vae по устройствам исходя из размера модели и памяти. Переопределяет backend и params-backend, может «размазать» модули между несколькими GPU. Не всегда это делает оптимально, распределяя веса на медленнодействующие компоненты (например, скидывая диффузную модель в RAM), что приводит к очень сильному торможению компьютера. Для решения проблемы и рационального использования GPU рекомендуется использовать ручное распределение слоев через флаг backend с принудительным Layer Split.
- split-mode layer — послойная нарезка модели (выбор по умолчанию). Видеокарты получают монолитные пакеты из целых блоков трансформера. Межкарточный обмен по PCIe 3.0 происходит дискретно и только на границах диапазонов слоев для передачи residual stream.
- split-mode row — построчный тензорный параллелизм (CUDA only). Каждая математическая матрица умножения (matmul) пилится пополам между GPU. Требует непрерывной аппаратной синхронизации карт на каждой микрооперации. Без высокоскоростного интерфейса уровня NVLink данный режим сильно использует пропускную способность PCIe.
Внимание!
Разделение слоев нельзя одновременно комбинировать с сегментацией по графу —max-vram или с функцией —stream-layers для разделяемого модуля; это механизмы для работы с одним устройством, и для него они отключаются.
При запуске в режиме split-mode layer, даже с отключенными опциями max-vram и stream-layers движок sd.cpp падает при генерации с ошибкой на втором шаге денойзинга. Планировщик некорректно оценивает свободную память видеокарт и обнуляет доступные лимиты (ошибка needs 1386.6 MB, but only 0.0 MB is available).
Логи генерации картинки в z-image-turbo-art-Q8_0.gguf с использованием сервера stable-diffusion.cpp с неверным синтаксисом:
...
[INFO ] model_loader.cpp:1272 - loading tensors completed, taking 146.99s (read: 143.48s, memcpy: 0.00s, convert: 0.86s, copy_to_backend: 1.31s)
|======> | 1/8 - 167.89s/it[ERROR] layer_split_partition.cpp:161 - z_image graph-cut layer split: segment 0 needs 1386.6 MB on CUDA0, but only 0.0 MB is available under current VRAM limits
[ERROR] stable-diffusion.cpp:2555 - diffusion model compute failed
[ERROR] stable-diffusion.cpp:2663 - Diffusion model sampling failed
[ERROR] stable-diffusion.cpp:5358 - sampling for image 1/1 failed after 167.99s
При падении сервера из-за ошибок, в памяти остаются зависшие процессы, их можно увидеть по команде
ps -o pid,rss,vz,comm -p $(pgrep sd-server)
PID RSS VSZ COMMAND
15458 326924 10107908 sd-server
20687 699332 49776092 sd-server
25963 326560 9911304 sd-server
Их и кеш нужно принудительно завершить/очистить командами:
killall sd-server
или (если процесс заблокировал шину PCIe):
killall -9 sd-server
echo 3 | sudo tee /proc/sys/vm/drop_caches
Для автоматизации этого процесса в скрипт запуска можно добавить специальный блок перед основной командой:
# =====================================================================
# БЛОК ОЧИСТКИ ПАМЯТИ И ЗАВЕРШЕНИЯ СТАРЫХ ПРОЦЕССОВ
# =====================================================================
echo "--- Зачистка окружения перед стартом ---"
# 1. Завершаем все висящие процессы сервера
if pgrep sd-server > /dev/null; then
echo "[INFO] Найдены активные процессы sd-server. Принудительно завершаем..."
sudo killall -9 sd-server 2>/dev/null
sleep 2 # Пауза, чтобы драйвер CUDA успел освободить фреймбуфер карт
fi
# 2. Сброс графических процессоров (очистка остаточного кэша CUDA)
# Опция -r (reset) принудительно очищает VRAM драйвера без перезагрузки ПК
echo "[INFO] Мгновенный сброс памяти GPU через nvidia-smi..."
sudo nvidia-smi -r -i 0,1 2>/dev/null || echo "[WARN] Сброс памяти занят, продолжаем..."
echo "----------------------------------------"
# =====================================================================
Пример скрипта запуска sd-server с выгрузкой весов диффузной модели z-image-turbo-art-Q8_0.gguf (6.7GB) и vae (taef1_decoder.safetensors, (9.4MB) на две видеокарты Nvidia, а текстового энкодера Qwen3-4B-Q8_0.gguf (4GB) — в оперативную память компьютера:
./sd-server \
--listen-ip 127.0.0.1 \
--listen-port 1234 \
--diffusion-model "/home/intel42/Рабочий стол/AI_Models/Z-Image-Turbo/z-image-turbo-art-Q8_0.gguf" \
--llm "/home/intel42/Рабочий стол/AI_Models/Qwen3-4B-Q8_0.gguf" \
--taesd "/home/intel42/Рабочий стол/AI_Models/taef1_decoder.safetensors" \
--lora-model-dir "/home/intel42/Рабочий стол/AI_Models/lora" \
--lora-apply-mode at_runtime \
--backend "diffusion=CUDA1&CUDA0,te=CPU,vae=CUDA1" \
--params-backend "diffusion=CUDA1,te=CPU,vae=CUDA1" \
--split-mode row \
--fa \
--color \
--threads 4 \
--mmap \
--steps 8 \
--cfg-scale 1 \
--sampling-method euler \
--scheduler sgm_uniform \
--width 768 \
--height 768 \
--vae-conv-direct
где
- fa — активация механизма Flash Attention. Оптимизирует расчет матриц внимания (Attention), радикально снижая количество операций чтения/записи во фреймбуфер GPU. Это уменьшает пиковое потребление памяти вычислительным графом и ускоряет инференс на архитектуре Turing.
- color — включает цветную маркировку логов в консоли (ANSI-раскраску). Позволяет визуально разделять информационные сообщения ([INFO]), предупреждения ([WARN]) и ошибки ([ERROR]), существенно упрощая отладку планировщика и мониторинг шагов генерации.
- threads 4 — количество вычислительных потоков CPU, выделяемых для работы планировщика и обработки текстового энкодера. Поскольку модуль te вынесен в RAM, это значение критично: настройка под 4 физических ядра i7-7700 исключает троттлинг процессора при подготовке промпта.
- mmap — включение механизма Memory Mapping (проектирования файлов в память). Позволяет ОС Linux маппить веса моделей напрямую с накопителя в виртуальное адресное пространство. Это ускоряет инициализацию и снижает накладные расходы на физическое копирование данных в ОЗУ.
- steps 8 — базовое количество шагов денойзинга (итераций) для генерации одного изображения. Выставление минимального значения (8 шагов) оптимизировано под zimage-turbo, Turbo/Schnell архитектуры, позволяя быстро замерить чистую скорость работы GPU-кластера.
- cfg-scale 1 — коэффициент соответствия текстовому запросу (Classifier-Free Guidance). Значение 1.0 отключает двойной просчет негативного промпта. Это экономит половину вычислительной мощности карт на каждом шаге и снижает нагрузку на VRAM, что необходимо для Turbo-моделей.
- sampling-method euler — выбор классического математического решателя дифференциальных уравнений (семплера Эйлера). Самый быстрый и наименее требовательный к памяти метод генерации, хорошо подходящий для проведения бенчмарков.
- scheduler sgm_uniform — планировщик шагов денойзинга со схемой равномерного распределения сигм. Оптимизирован для работы в FLOW-режиме современных DiT-архитектур, обеспечивая корректное наложение шума на малом количестве шагов.
- width 1024 / height 1024 — целевое разрешение генерируемого изображения в пикселях. Задает физический размер входных и выходных тензоров графа вычислений. Это родное разрешение для тяжелых DiT-моделей, создающее максимальную стресс-нагрузку на VRAM.
- vae-tiling — активация плиточного (тайлингового) декодирования VAE. Разбивает финальный латентный кадр 1024×1024 на небольшие перекрывающиеся плитки. Защищает VAE-модуль от вылета по OOM в момент сборки готовой картинки, так как декодирование целого кадра требует огромного скрытого буфера.
- vae-relative-tile-size 0.25 — относительный размер одной плитки при тайлинге VAE (25% от размера кадра). Оптимальный баланс для 6-гигабайтных карт: плитки получаются достаточно маленькими, чтобы не перегружать VRAM, но сохраняют стыковку без видимых швов на финальном изображении.
- diffusion-conv-direct / vae-conv-direct — принудительное использование прямых (direct) алгоритмов свертки для диффузионной модели и VAE вместо тяжелых оберток cuBLAS. Позволяет значительно сэкономить промежуточную память графа за счет отказа от развертывания матриц по методу im2col, что критично для видеокарт без тензорных ядер.
При запуске Движок stable-diffusion.cpp инициализируется, логи показывают, что он видит 11514 MiB VRAM (5.757+5.757GB):
ggml_cuda_init: found 2 CUDA devices (Total VRAM: 11514 MiB):
Device 0: NVIDIA GeForce GTX 1660 Ti, compute capability 7.5, VMM: yes, VRAM: 5757 MiB
Device 1: NVIDIA GeForce GTX 1660 SUPER, compute capability 7.5, VMM: yes, VRAM: 5757 MiB
The following devices will have suboptimal performance due to a lack of tensor cores:
Device 0: NVIDIA GeForce GTX 1660 Ti
Device 1: NVIDIA GeForce GTX 1660 SUPER
Consider compiling with CMAKE_CUDA_ARCHITECTURES=61-virtual;80-virtual and DGGML_CUDA_FORCE_MMQ to force the use of the Pascal code for Turing.
[INFO ] stable-diffusion.cpp:710 - loading diffusion model from '/home/intel42/Рабочий стол/AI_Models/Z-Image-Turbo/z-image-turbo-art-Q8_0.gguf'
[INFO ] model_loader.cpp:236 - load /home/intel42/Рабочий стол/AI_Models/Z-Image-Turbo/z-image-turbo-art-Q8_0.gguf using gguf format
[INFO ] stable-diffusion.cpp:772 - loading llm from '/home/intel42/Рабочий стол/AI_Models/Qwen3-4B-Q8_0.gguf'
[INFO ] model_loader.cpp:236 - load /home/intel42/Рабочий стол/AI_Models/Qwen3-4B-Q8_0.gguf using gguf format
[INFO ] stable-diffusion.cpp:794 - loading tae from '/home/intel42/Рабочий стол/AI_Models/taef1_decoder.safetensors'
[INFO ] model_loader.cpp:242 - load /home/intel42/Рабочий стол/AI_Models/taef1_decoder.safetensors using safetensors format
[INFO ] stable-diffusion.cpp:847 - Version: Z-Image
[INFO ] stable-diffusion.cpp:902 - Weight type stat: f32: 524 | q8_0: 433 | bf16: 28
[INFO ] stable-diffusion.cpp:903 - Conditioner weight type stat: f32: 145 | q8_0: 253
[INFO ] stable-diffusion.cpp:904 - Diffusion model weight type stat: f32: 245 | q8_0: 180 | bf16: 28
[INFO ] stable-diffusion.cpp:905 - VAE weight type stat:
[INFO ] model_loader.cpp:861 - using mmap for '/home/intel42/Рабочий стол/AI_Models/Z-Image-Turbo/z-image-turbo-art-Q8_0.gguf'
[INFO ] model_loader.cpp:861 - using mmap for '/home/intel42/Рабочий стол/AI_Models/Qwen3-4B-Q8_0.gguf'
[INFO ] model_loader.cpp:861 - using mmap for '/home/intel42/Рабочий стол/AI_Models/taef1_decoder.safetensors'
[INFO ] stable-diffusion.cpp:428 - Diffusion model row split: 180 tensors (5498.4 MB) split across 2 devices (main CUDA1)
[INFO ] stable-diffusion.cpp:1349 - using TAE for encoding / decoding
[INFO ] stable-diffusion.cpp:1395 - Using Conv2d direct in the vae model
[INFO ] stable-diffusion.cpp:1463 - Using flash attention
[INFO ] stable-diffusion.cpp:1477 - Using flash attention in the diffusion model
[INFO ] stable-diffusion.cpp:1583 - total params memory size = 10972.59MB (VRAM 6896.15MB, RAM 4076.43MB): text_encoders 4076.43MB(RAM), diffusion_model 6891.46MB(VRAM), vae 4.70MB(VRAM), controlnet 0.00MB(N/A), extensions 0.00MB(N/A)
[INFO ] stable-diffusion.cpp:1693 - running in FLOW mode
[INFO ] main.cpp:148 - listening on: http://127.0.0.1:1234
Теперь можно запускать браузер со страницей, открытой по адресу 127.0.0.1:1234, и работать с sd-server в удобном графическом режиме:

В режиме split-mode row генерация проходит без ошибок, первая выдача заняла 204 секунды из-за медленной работы HDD:

Логи первой генерации:
[INFO ] stable-diffusion.cpp:5270 - generate_image 768x768
[INFO ] stable-diffusion.cpp:1866 - apply lora at runtime
[INFO ] model_loader.cpp:242 - load /home/intel42/Рабочий стол/AI_Models/lora/zimage_lora_v1_000003250.safetensors using safetensors format
[INFO ] lora.hpp:45 - loading LoRA from '/home/intel42/Рабочий стол/AI_Models/lora/zimage_lora_v1_000003250.safetensors'
|##################################################| 480/480 - 0.00MB/s
[INFO ] model_loader.cpp:1272 - loading tensors completed, taking 0.20s (read: 0.00s, memcpy: 0.00s, convert: 0.00s, copy_to_backend: 0.00s)
[INFO ] model_loader.cpp:242 - load /home/intel42/Рабочий стол/AI_Models/lora/zimage_lora_v1_000003250.safetensors using safetensors format
[INFO ] lora.hpp:45 - loading LoRA from '/home/intel42/Рабочий стол/AI_Models/lora/zimage_lora_v1_000003250.safetensors'
|##################################################| 480/480 - 0.00MB/s
[INFO ] model_loader.cpp:1272 - loading tensors completed, taking 0.20s (read: 0.00s, memcpy: 0.00s, convert: 0.00s, copy_to_backend: 0.00s)
|##################################################| 480/480 - 67.49MB/s
[INFO ] model_loader.cpp:1272 - loading tensors completed, taking 2.40s (read: 2.27s, memcpy: 0.00s, convert: 0.00s, copy_to_backend: 0.06s)
[INFO ] model_loader.cpp:242 - load /home/intel42/Рабочий стол/AI_Models/lora/zimage_lora_v1_000003250.safetensors using safetensors format
[INFO ] lora.hpp:45 - loading LoRA from '/home/intel42/Рабочий стол/AI_Models/lora/zimage_lora_v1_000003250.safetensors'
|##################################################| 480/480 - 0.00MB/s
[INFO ] model_loader.cpp:1272 - loading tensors completed, taking 0.20s (read: 0.00s, memcpy: 0.00s, convert: 0.00s, copy_to_backend: 0.00s)
[INFO ] stable-diffusion.cpp:1964 - apply_loras completed, taking 3.12s
[INFO ] stable-diffusion.cpp:2909 - Using 'z_image_omni' preset for reference images
[INFO ] denoiser.hpp:1050 - get_sigmas with SGM Uniform scheduler
[INFO ] stable-diffusion.cpp:4107 - sampling using Euler method
[INFO ] model_loader.cpp:960 - memory-mapped 386 tensors in 1 files (3974.14 MB), taking 0.00s
|##################################################| 386/386 - 0.00MB/s
[INFO ] model_loader.cpp:1272 - loading tensors completed, taking 0.20s (read: 0.00s, memcpy: 0.00s, convert: 0.00s, copy_to_backend: 0.00s)
[INFO ] stable-diffusion.cpp:4950 - get_learned_condition completed, taking 56.84s
[INFO ] stable-diffusion.cpp:5321 - generating image: 1/1 - seed 42
|##################################################| 452/452 - 62.38MB/s
[INFO ] model_loader.cpp:1272 - loading tensors completed, taking 110.47s (read: 108.15s, memcpy: 0.00s, convert: 0.19s, copy_to_backend: 1.72s)
|==================================================| 8/8 - 11.24s/it
[INFO ] stable-diffusion.cpp:5353 - sampling completed, taking 204.30s
[INFO ] stable-diffusion.cpp:5365 - generating 1 latent images completed, taking 204.31s
[INFO ] stable-diffusion.cpp:4975 - decoding 1 latents
|##################################################| 67/67 - 6.40MB/s
[INFO ] model_loader.cpp:1272 - loading tensors completed, taking 0.73s (read: 0.20s, memcpy: 0.00s, convert: 0.00s, copy_to_backend: 0.00s)
[INFO ] stable-diffusion.cpp:5041 - latent 1 decoded, taking 2.54s
[INFO ] stable-diffusion.cpp:5045 - decode_first_stage completed, taking 2.54s
[INFO ] stable-diffusion.cpp:1929 - runtime_lora_models:
[INFO ] lora.hpp:891 - (480 / 480) LoRA tensors have been applied, lora_file_path = /home/intel42/Рабочий стол/AI_Models/lora/zimage_lora_v1_000003250.safetensors
[INFO ] stable-diffusion.cpp:5503 - generate_image completed in 266.85s

Когда движок sd.cpp был переключен в режим —split-mode row, алгоритм планировщика полностью изменил физику распределения памяти ,что позволило успешно распараллелить вес одной большой модели между двумя видеокартами. При этом:
- была решена проблема с ошибкой «Segment 0»: В режиме layer (послойном) движок пытался строить локальные графы активаций для каждого GPU по отдельности. На втором шаге карте CUDA0 требовался кусок в 1386 МБ, которого у неё не было из-за накопленного оверхеда слоев;
- благодаря разделению матриц модели вместо слоев (180 тензоров пополам вдоль строк матричных умножений), модель легла во фреймбуфер обеих карт ровным слоем ,позволяющим делать синхронные вычисления. Рабочий граф активаций уменьшился вдвое(по ~690 МБ на каждое устройство), поэтому слегкостью. поместился в свободных остатках VRAM на обеих картах;
Были достигнуты следующие значения быстродейсствия при инференсе при первом запуске (модель z-image-turbo-art-Q8_0.gguf +Qwen3-4B-Q8_0.gguf + taef1_decoder.safetensors + lora размером 162.2MB, картинка 768×758):
- скорость инференса — 11.24 s/it;
- 204.30s на чистый денойзинг: 8 шагов генерации в тяжелом FLOW-режиме при разрешении 768×768 заняли 3.4 минуты;
- 266.85s — общее время первой сессии. Сюда вошла подготовка промпта процессором из RAM (56.84s), динамическое наложение LoRA-слоя (3.12s) и декодирование готового кадра через VAE на SUPER-карте (2.54s).
Вторая генерация, разрешение увеличено до 1024×1024, время обработки — 184 секунды:

Добавляем в скрипт запуска upscalerRealESRGAN_x4plus.pth (64MB):
./sd-server \
--listen-ip 127.0.0.1 \
--listen-port 1234 \
--diffusion-model "/home/intel42/Рабочий стол/AI_Models/Z-Image-Turbo/z-image-turbo-art-Q8_0.gguf" \
--llm "/home/intel42/Рабочий стол/AI_Models/Qwen3-4B-Q8_0.gguf" \
--taesd "/home/intel42/Рабочий стол/AI_Models/taef1_decoder.safetensors" \
--upscale-model "/home/intel42/Рабочий стол/AI_Models/Upscalers/RealESRGAN_x4plus.pth" \
--lora-model-dir "/home/intel42/Рабочий стол/AI_Models/lora" \
--lora-apply-mode at_runtime \
--backend "diffusion=CUDA1&CUDA0,te=CPU,vae=CUDA1" \
--params-backend "diffusion=CUDA1,te=CPU,vae=CUDA1" \
--split-mode row \
--fa \
--color \
--threads 4 \
--mmap \
--steps 8 \
--cfg-scale 1 \
--sampling-method euler \
--scheduler sgm_uniform \
--width 512 \
--height 512 \
--vae-conv-direct

Первая генерация с апскейлером заняла 340 секунд, вторая — 201.88:
[INFO ] stable-diffusion.cpp:5270 - generate_image 512x512
[INFO ] stable-diffusion.cpp:2909 - Using 'z_image_omni' preset for reference images
[INFO ] denoiser.hpp:1050 - get_sigmas with SGM Uniform scheduler
[INFO ] stable-diffusion.cpp:4107 - sampling using Euler method
[INFO ] stable-diffusion.cpp:4950 - get_learned_condition completed, taking 1.71s
[INFO ] stable-diffusion.cpp:5321 - generating image: 1/1 - seed 42
|##################################################| 1/1 - 0.09MB/s
[INFO ] model_loader.cpp:1272 - loading tensors completed, taking 0.16s (read: 0.08s, memcpy: 0.00s, convert: 0.00s, copy_to_backend: 0.08s)
|==================================================| 8/8 - 3.78s/it
[INFO ] stable-diffusion.cpp:5353 - sampling completed, taking 30.47s
[INFO ] stable-diffusion.cpp:5365 - generating 1 latent images completed, taking 30.47s
[INFO ] stable-diffusion.cpp:5378 - hires fix: upscaling to 1024x1024
[INFO ] denoiser.hpp:1050 - get_sigmas with SGM Uniform scheduler
[INFO ] stable-diffusion.cpp:5410 - hires fix: scheduler_steps=11, denoising_strength=0.70, sigma_sched_size=9
[INFO ] stable-diffusion.cpp:5126 - hires Latent upscale 64x64 -> 128x128
|==================================================| 8/8 - 20.91s/it
[INFO ] stable-diffusion.cpp:5474 - hires sampling 1/1 completed, taking 166.30s
[INFO ] stable-diffusion.cpp:5489 - hires fix completed, taking 166.35s
[INFO ] stable-diffusion.cpp:4975 - decoding 1 latents
[INFO ] stable-diffusion.cpp:5041 - latent 1 decoded, taking 3.31s
[INFO ] stable-diffusion.cpp:5045 - decode_first_stage completed, taking 3.31s
[INFO ] stable-diffusion.cpp:5503 - generate_image completed in 201.88s

Генерируем картинку с разрешением 1024×1024 (генерация 512×512 с включенной опцией hires), первичный процесс занял 367 секунд, вторая генерация — сек.:

Заключение
Использование легковесного C/C++ движка stable-diffusion.cpp в связке с архитектурой моделей формата GGUF позволяет объединить вычислительные ресурсы нескольких бюджетных видеокарт в единый виртуальный кластер. За счет принудительной активации опции послойного или построчного параллелизма матриц (—split-mode row) и тонкой настройки асимметричных бэкендов, появляется возможность полностью распределить рабочие веса квантованной DiT-модели объемом до 8–9 ГБ во фреймбуфер обеих карт параллельно. Это позволяет преодолеть аппаратный барьер Out of Memory и достичь стабильного инференса без катастрофического падения производительности.
Проведенные тесты наглядно доказали: если в загашнике завалялась старая вторая видеокарта, её можно эффективно утилизировать для расширения доступного пула VRAM. Экосистема ggml дает вторую жизнь «народному» железу прошлых поколений (такому как связка из GTX 1660 Ti и SUPER). С помощью правильных флагов компиляции, отключения лимитов безопасности драйвера и грамотного распределения контекстов вычислений, на этих картах можно стабильно крутить современные генеративные движки, которые при запуске на одиночном GPU гарантированно падали бы с ошибкой нехватки памяти.
Приложение
Справка по бэкенду stable-diffusion.cpp
В stable-diffusion.cpp предусмотрено два параметра для назначения бэкенда:
--backend выбирает бэкенд среды выполнения (runtime) для исполнения графов модели.
--params-backend определяет, где хранятся параметры модели.
Если параметр --params-backend не задан, параметры используют тот же бэкенд, что и среда выполнения их модуля.
Синтаксис
Назначением бэкенда может служить одно имя бэкенда:
sd-cli -m model.safetensors -p "a cat" --backend CPU
Это правило применяется к каждому модулю, у которого нет более конкретного назначения.
Назначения также могут быть направлены на отдельные модули:
sd-cli -m model.safetensors -p "a cat" --backend te=CPU,vae=CUDA0,diffusion=VULKAN0
Этот же синтаксис используется для размещения параметров:
sd-cli -m model.safetensors -p "a cat" --backend CUDA0 --params-backend te=CPU,vae=CPU
Параметр --params-backend также принимает специальное значение disk:
sd-cli -m model.safetensors -p "a cat" --backend CUDA0 --params-backend disk
Параметр --max-vram может быть направлен на определенные имена бэкендов/устройств:
sd-cli -m model.safetensors -p "a cat" --backend diffusion=CUDA0,vae=VULKAN0 --max-vram CUDA0=6,VULKAN0=2
Лимит памяти применяется к каждому модулю, работающему на этом бэкенде.
Имена модулей регистронезависимы. Дефисы и подчеркивания в именах модулей игнорируются, поэтому clip_vision, clip-vision и clipvision эквивалентны.
Для установки бэкенда по умолчанию внутри смешанного назначения можно использовать all=, default= и *=:
sd-cli -m model.safetensors -p "a cat" --backend all=CUDA0,te=CPU
Несколько устройств на один модуль (разделение слоев)
В назначении модуля для --backend можно указать несколько устройств через символ &:
sd-cli -m model.safetensors -p "a cat" --backend "diffusion=CUDA0&CUDA1"
Трансформерные блоки модуля распределяются по перечисленным устройствам непрерывными диапазонами. Размер каждого диапазона пропорционален объему свободной памяти устройства (минус резерв под вычислительный буфер в размере около 2 ГиБ на устройство).
Графы модуля выполняются с помощью планировщика ggml_backend_sched, который запускает каждый блок на том устройстве, где хранятся его веса, копируя остаточную связь (residual stream) на границах диапазонов. Первое устройство в списке является главным устройством модуля: на нем также хранятся нетрансформерные тензоры (эмбеддинги, финальные слои нормализации, небольшие подмодули вроде моделей CLIP или проекторов) и входные/выходные данные графа.
Разделение слоев поддерживается для модулей diffusion и te. Для te это применимо к доминирующему текстовому кодировщику (t5xxl или LLM); остальные модули принимают только одно устройство. Если у модуля нет распознаваемых трансформерных блоков, назначение откатывается к первому устройству в списке.
Параметр --params-backend не принимает списки устройств. Если у модуля нет явного назначения параметров, параметры каждого диапазона блоков загружаются напрямую на его собственное устройство (а при --params-backend diffusion=disk — напрямую с него же и выгружаются). Явное назначение, такое как te=cpu, удерживает параметры на этом бэкенде и отправляет каждый диапазон на его устройство по требованию.
Разделение слоев нельзя комбинировать с сегментацией по графу --max-vram или с функцией --stream-layers для разделяемого модуля; это механизмы для работы с одним устройством, и для него они отключаются.
Используйте команду --list-devices, чтобы просмотреть имена устройств, доступных в системе.
Разделение по строкам (Row split: --split-mode row)
Параметр --split-mode выбирает способ распределения весов модуля между несколькими устройствами: по слоям (layer — значение по умолчанию, описанное выше) или по строкам (row). Он принимает один режим для всего или назначения для конкретных модулей:
sd-cli -m model.safetensors -p "a cat" --backend "diffusion=CUDA0&CUDA1" --split-mode row
sd-cli -m model.safetensors -p "a cat" --backend "diffusion=CUDA0&CUDA1,te=CUDA0&CUDA1" --split-mode diffusion=row,te=layer
В режиме row модуль продолжает выполняться на своем главном (первом в списке) устройстве, но веса матричного умножения (matmul) его трансформерных блоков выделяются в буфере бэкенда с разделением по строкам. Этот буфер нарезает строки каждого веса между указанными устройствами пропорционально свободной памяти и запускает эти матричные умножения на всех устройствах параллельно. По сравнению с разделением по слоям, этот метод задействует все GPU внутри каждого слоя (а не последовательно устройство за устройством) ценой межвычислительной редукции (cross-device reduction) на каждое матричное умножение. Обычно это более быстрый вариант, если устройства имеют высокоскоростное соединение.
Разделение по строкам требует поддержки разделяемых буферов со стороны бэкенда и в настоящее время доступно только для CUDA. На других бэкендах (или когда перечисленные устройства относятся к реестрам разных бэкендов) модуль откатывается к разделению по слоям. Эмбеддинги, веса нормализации, смещения (biases) и другие нетрансформерные тензоры остаются в обычных буферах на главном устройстве.
Прямое («немедленное») применение LoRA не может патчить тензоры, разделенные по строкам. При --split-mode row автоматический режим LoRA выбирает применение во время выполнения (runtime application), а явное указание --lora-apply-mode immediately пропускает разделенные тензоры с выводом предупреждения.
Автоматическое размещение (--auto-fit)
Параметр --auto-fit определяет размещение модулей diffusion, te и vae на основе метаданных модели и лимитов памяти для каждого устройства. Полученные параметры затем передаются в тот же механизм назначения бэкендов, который был описан выше (выбранные спецификации выводятся в консоль). Когда включен автоматический подбор, параметры --backend и --params-backend игнорируются.
sd-cli -m model.safetensors -p "a cat" --auto-fit
sd-cli -m model.safetensors -p "a cat" --auto-fit --max-vram CUDA0=8,CUDA1=14
sd-cli -m model.safetensors -p "a cat" --auto-fit --split-mode row
Лимиты памяти переиспользуют параметр --max-vram:
Положительное значение для устройства ограничивает объем памяти, который --auto-fit может планировать использовать на нем.
Отрицательное значение означает «свободная память минус указанное количество ГиБ».
Если лимит не задан, используется вся свободная память устройства за вычетом резерва в 512 МиБ. (Эти же значения по-прежнему управляют сегментированным выполнением с отсечением графа для модулей, которые в итоге оказываются на одном устройстве).
Когда все компоненты полностью помещаются в памяти, они просто распределяются по доступным GPU. Если места не хватает, --auto-fit переключается в режим разделения по времени (time-share): тяжелые компоненты получают размещение параметров на диске (disk params residency — загружаются на время своей фазы работы и освобождаются после нее). Компонент, который слишком велик для любого отдельного устройства, разделяется между всеми GPU с помощью механизма разделения по слоям или строкам (выбор зависит от --split-mode, по умолчанию используется layer). Компоненты, которые не помещаются никуда, переносятся на CPU. Если при декодировании VAE все равно не хватает памяти, включается тайлинг (плиточный рендеринг), после чего процесс декодирования повторяется один раз.
Модуль | Назначение | Допустимые имена |
diffusion | UNet, DiT, MMDiT, Flux, Wan, Qwen Image и другие диффузионные модели | diffusion, model, unet, dit |
te | Текстовые кодировщики и кондиционеры (conditioners) | te, clip, text, textencoder, textencoders, conditioner, cond, llm, t5, t5xxl |
clip_vision | Визуальный кодировщик CLIP (vision encoder) | clip_vision, clipvision, clip-vision, vision |
vae | VAE и TAE | vae, firststage, autoencoder, tae |
controlnet | ControlNet | controlnet, control |
photomaker | ID-кодировщик PhotoMaker и PhotoMaker LoRA | photomaker, photomakerid, pmid, photo |
upscaler | Апскейлер ESRGAN | upscaler, esrgan, hires |
detector | Детектор YOLOv8 для ADetailer | detector, adetailer, yolo |
te является предпочтительным именем модуля для текстовых кодировщиков. clip сохранен как поддерживаемый псевдоним (alias), так как Терминология CLIP используется во многих существующих командах и названиях моделей.
Имена бэкендов
Имена бэкендов сопоставляются со списком устройств бэкенда GGML. Сопоставление регистронезависимо и принимает точные имена или уникальные префиксы, поэтому к распространенным значениям относятся:
CPU
CUDA0
VULKAN0
METAL
Специальные значения auto, default и пустое имя бэкенда выбирают бэкенд по умолчанию. Предпочтение по умолчанию отдается дискретному GPU, затем интегрированному GPU, и в последнюю очередь — CPU.
Специальное значение gpu выбирает первый доступный GPU-бэкенд, а в случае его отсутствия откатывается к первому бэкенду интегрированного GPU.
Специальное значение disk принимается только параметром --params-backend. Команда --backend disk является недействительной, поскольку disk — это режим размещения параметров, а не вычислительный бэкенд среды выполнения.
Бэкенд среды выполнения vs. Бэкенд параметров
Бэкенд среды выполнения (runtime backend) управляет тем, где именно выполняется граф вычислений. Бэкенд параметров (parameter backend) управляет тем, где выделяется память под веса модели или перезагружаются ли они с диска по требованию.
Пример:
sd-cli -m model.safetensors -p "a cat" --backend CUDA0 --params-backend cpu
Эта команда запускает выполнение всех модулей на cuda0, но хранит параметры в оперативной памяти (CPU RAM). Во время выполнения параметры перемещаются на бэкенд среды выполнения по мере необходимости.
Пример:
sd-cli -m model.safetensors -p "a cat" --backend cuda0 --params-backend disk
Эта команда запускает все модули на cuda0, перезагружает параметры из файла модели по мере необходимости и освобождает эти буферы параметров после использования.
Режим disk никогда не выбирается неявно. Если параметр --params-backend не задан, параметры используют бэкенд среды выполнения.
Назначения для отдельных модулей можно смешивать:
sd-cli -m model.safetensors -p "a cat" --backend diffusion=CUDA0,te=CPU,vae=CPU --params-backend diffusion=CUDA0,te=CPU,vae=CPU
Такой синтаксис оставляет обработку текста (text encoding) и выполнение VAE на CPU, в то время как диффузионная модель работает на GPU.
Разделение ресурсов и время жизни бэкендов
Бэкенды управляются классом SDBackendManager.
Внутри одного менеджера экземпляры бэкендов кэшируются по сопоставленному имени устройства бэкенда. Если несколько модулей запрашивают один и тот же бэкенд, они используют один общий экземпляр ggml_backend_t.
Пример:
--backend te=CPU,vae=CPU
Использует один общий CPU-бэкенд для выполнения среды выполнения как te, так и vae.
Назначения среды выполнения и параметров также используют один и тот же кэш бэкендов. Если параметры --backend diffusion=CUDA0 и --params-backend diffusion=CUDA0 сопоставляются с одним устройством, оба используют один экземпляр бэкенда.
Режим --params-backend disk не создает отдельного экземпляра бэкенда. Параметры загружаются «лениво» (по требованию) с использованием бэкенда среды выполнения модуля.
SDBackendManager владеет экземплярами бэкендов и освобождает их, когда контекст или апскейлер уничтожаются. Обработчики моделей (model runners) получают указатели на бэкенды среды выполнения и параметров без прав владения и не освобождают их.
Флаги совместимости
В качестве псевдонимов совместимости для устаревших флагов размещения на CPU CLI-интерфейс и сервер по-прежнему принимают следующие параметры:
--clip-on-cpu
--vae-on-cpu
--control-net-cpu
--offload-to-cpu
Флаги --clip-on-cpu, --vae-on-cpu и --control-net-cpu устарели. На уровне обработки аргументов они просто добавляют te=CPU, vae=CPU и controlnet=CPU в начало строки --backend перед созданием контекста.
Флаг --offload-to-cpu добавляет значение CPU по умолчанию к назначению параметров на стороне вызывающего кода перед созданием контекста:
--params-backend '*=CPU'
Поскольку это значение по умолчанию вставляется первым, последующие явные записи в параметре --params-backend все еще могут его переопределить.
Например, комбинация --offload-to-cpu --params-backend te=disk оставит параметры всех модулей, кроме TE, в оперативной памяти (CPU RAM), а параметры самого TE будет перезагружать с диска.
При вызове из сторонних библиотек следует устанавливать параметры backend и params_backend напрямую. Старые поля для CPU и выгрузки (offload) больше не являются частью C API. Для новых команд предпочтительно использовать явные назначения через --backend и --params-backend.


