核心理念
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 参数级 | 函数调用级 |