核心理念
2026/7/7
每次仅加载一个模型,中间结果暂存于系统内存,最终输出才写入磁盘。
与 ComfyUI / sd-cli 一次性加载所有模型的策略不同,分步管线确保每一步骤可独占全部资源(尤其是显存)。
架构图
text
Step 1 Step 2 Step 3
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ 加载 Qwen3 │ │ 加载 DiT │ │ 加载 VAE │
│ text encoder│ │ diffusion │ │ decode │
│ │ │ │ │ │
│ prompt ─────→│ cond │ cond ───────→│ latent │ latent ─────→│ img
│ (CPU 5GB) │ float[] │ (GPU 3-4GB) │ float[] │ (GPU 0.5GB) │ png
│ │ │ │ │ │
│ ↓ 释放 Qwen3 │ │ ↓ 释放 DiT │ │ ↓ 释放 VAE │
│ VRAM: 0 │ │ VRAM: 0 │ │ 写磁盘完成 │
└──────────────┘ └──────────────┘ └──────────────┘
内存中传递: cond (float array) → latent (float array) → image (sd_image_t)
具体改动:给 DLL 加三个导出函数
1. sd_encode_condition --- 只跑文本编码
将 prompt 编码为 conditioning tensor,并以平铺的 float 数组返回。
c
SD_API bool sd_encode_condition(
sd_ctx_t* sd_ctx,
const char* prompt,
const char* negative_prompt,
const sd_lora_t* loras,
int lora_count,
float** cond_data, // 输出:condition float 数组(调用者 free)
int64_t* cond_size // 输出:数组元素个数
);
-
需要暴露
StableDiffusionGGML::get_learned_condition()(原私有方法)。 -
cond_data包含c_crossattn+c_vector+c_concat的序列化拼接。 -
返回后调用者需调用
free_sd_ctx()释放 Qwen3 模型。
2. sd_sample --- 只跑采样
输入 condition,输出 latent。
c
SD_API bool sd_sample(
sd_ctx_t* sd_ctx,
const float* cond_data,
int64_t cond_size,
int width,
int height,
int steps,
float cfg_scale,
int64_t seed,
float** latent_data, // 输出:latent float 数组
int64_t* latent_size
);
-
需要暴露
StableDiffusionGGML::sample()(原私有方法)。 -
内部反序列化
cond_data还原为SDCondition结构。 -
latent_data为采样结果(例如 FLUX.2 的 latent 尺寸为 64×64×16)。 -
可通过
--backend diffusion=vulkan1指定后端。
3. sd_decode_latent --- 只跑 VAE 解码
输入 latent,输出最终图像。
c
SD_API sd_image_t sd_decode_latent(
sd_ctx_t* sd_ctx,
const float* latent_data,
int64_t latent_size
);
-
需要暴露
StableDiffusionGGML::decode_first_stage()(原私有方法)。 -
可通过
--backend vae=vulkan1让 VAE 在 GPU 上运行(模型仅 164 MB)。
调用方主程序逻辑
text
1. 创建 sd_ctx,仅传入 --llm(Qwen3),其余路径置空
sd_ctx_params.llm_path = "flux2-klein-9b-uncensored-q4_k_m.gguf"
sd_ctx_params.diffusion_model_path = NULL
sd_ctx_params.vae_path = NULL
ctx = new_sd_ctx(¶ms)
2. sd_encode_condition(ctx, prompt, loras, &cond_data, &cond_size)
此时 cond_data 为内存中的 float 数组
3. free_sd_ctx(ctx) // Qwen3 完全释放,显存归零
4. 新建 sd_ctx,仅传入 --diffusion-model(DiT)
sd_ctx_params.diffusion_model_path = "flux-2-klein-9b-Q4_0.gguf"
sd_ctx_params.llm_path = NULL
sd_ctx_params.backend = "diffusion=vulkan1"
ctx = new_sd_ctx(¶ms)
5. sd_sample(ctx, cond_data, cond_size, 512, 512, 4, 1.0, 42, &latent_data, &latent_size)
此时 latent_data 存于内存中
6. free(cond_data) // 释放 condition
free_sd_ctx(ctx) // DiT 完全释放,显存归零
7. 新建 sd_ctx,仅传入 --vae
sd_ctx_params.vae_path = "flux2-vae.safetensors"
sd_ctx_params.backend = "vae=vulkan1"
ctx = new_sd_ctx(¶ms)
8. img = sd_decode_latent(ctx, latent_data, latent_size)
stbi_write_png("output.png", img)
9. free(latent_data)
free_sd_images(&img, 1)
free_sd_ctx(ctx) // VAE 释放
每步内存占用
| 步骤 | 加载模型 | 大小 | VRAM | RAM | 备注 |
|---|---|---|---|---|---|
| 1 | Qwen3 文本编码器 | 5.0 GB | 0 GB | 5.0 GB | CPU 推理,不占显存 |
| 释放 | --- | --- | 0 GB | 0 GB | 回归零 |
| 2 | DiT (Q4_0) | 5.3 GB | ~3 GB | 0 GB | 权重 offload 至 GPU |
| 释放 | --- | --- | 0 GB | 0 GB | 回归零 |
| 3 | VAE | 164 MB | <0.5 GB | 0 GB | 全量置于 GPU |
总 VRAM 峰值:~3 GB(远低于 RX 580 的 8 GB 上限)
对 img2img / inpainting 的支持
在 Step 1 与 Step 2 之间插入一步即可:
text
Step 1.5: VAE 编码原图
加载 VAE(可复用 Step 3 逻辑)
原图 → VAE encode → init_latent(内存 float[])
随后 Step 2 的 sd_sample 接收 init_latent + cond 作为输入
此额外步骤耗时约 3~5 秒(VAE 编码走 GPU)。
编译方法
bash
git clone https://github.com/leejet/stable-diffusion.cpp
cd stable-diffusion.cpp
mkdir build && cd build
cmake .. -DGGML_VULKAN=ON -DCMAKE_BUILD_TYPE=Release
cmake --build . --config Release
编译产出:
-
build/bin/Release/stable-diffusion.dll→ 替换D:\files\FLUX.2\ -
自行编译调用方 exe,链接该 DLL
需要修改的源文件
| 文件 | 改动 |
|---|---|
include/stable-diffusion.h |
增加 3 个 SD_API 函数声明 |
src/stable-diffusion.cpp |
实现这 3 个函数,调用内部私有方法 |
src/stable-diffusion.h(内部) |
将 encode_first_stage()、sample()、decode_first_stage() 设为 public 或添加 public wrapper |
与 ComfyUI / sd-cli 对比
| ComfyUI | sd-cli 一次加载 | 本方案 | |
|---|---|---|---|
| 模型加载方式 | 全加载 | 全加载 | 分步加载 |
| 中间结果 | 显存常驻 | 显存常驻 | 内存 float array |
| VRAM 峰值 | 高(~6-7 GB) | 中(~3 GB) | 低(~3 GB) |
| img2img 额外开销 | 小(均在显存) | 小(均在显存) | 略大(多一次 VAE 加载) |
| 跨 prompt 复用 condition | 节点缓存 | 不支持 | 代码层手动缓存 |
| 控制粒度 | 节点级 | CLI 参数级 | 函数调用级 |