1. 从 Python 环境地狱说起为什么会有人用 C 重写 Stable Diffusion第一次听说 stable-diffusion.cpp 的时候我第一反应是又来一个 .cpp。毕竟 llama.cpp 已经把大模型跑进了只要内存够大的任何破电脑而 stable-diffusion.cpp 想做的事几乎一模一样把 Stable Diffusion 从 Python 生态里拽出来用纯 C/C 加 GGML 计算图去跑 CLIP、UNet 和 VAE最后在终端里敲一行命令直接出图。说实话如果你只在配好环境的工作站上玩过 SD可能体会不到这个东西的价值。但像我这种经常在客户服务器、旧笔记本、甚至没有独立显卡的迷你主机上跑生成任务的人Python 那套依赖能把人逼疯先装 Python 3.10再装 PyTorch然后 CUDA 版本必须和驱动匹配torch 版本和 diffusers 版本互相打架conda 环境一不小心就膨胀到 20GB。一个不小心升级个依赖之前能跑的脚本直接报undefined symbol或者module has no attribute排查半天发现是 NumPy 又换 ABI 了。stable-diffusion.cpp 把这个问题连根拔了。它没有 Python 解释器没有 pip没有虚拟环境模型权重统一打包成 GGUF 格式构建出来就是一个可执行文件。拷贝到另一台机器上只要能跑就能出图。这种把复杂的生成模型变成命令行工具的思路正是从 llama.cpp 那儿继承过来的。1.1 从 diffusers 到 GGUF这次重写到底解决了什么diffusers 作为 Python 界的标准库写起来确实舒服几行代码就能加载模型、编 prompt、跑采样、存图片。但对生产环境和嵌入式场景来说它的问题也很明显启动慢、内存开销大、环境隔离难、跨平台部署要打包一大堆东西。stable-diffusion.cpp 之所以选择 GGUF 格式是因为 GGML 这套生态已经把模型量化、层调度、后端抽象做得很成熟了。GGUF 本身就是为这种统一推理设计的容器格式里面可以放不同精度的张量数据。模型转换之后fp32、fp16、q8_0、q5_0、q4_0 这些精度可以按组件自由组合。最典型的就是 VAE 保持高精度UNet 和 CLIP 用量化既省内存又不至于让图崩掉。另外C 实现的推理图是静态的可以在第一次运行前把整张计算图建好。相比 Python 里每次 forward 都有大量动态派发的开销这种静态图方式在 CPU 上跑起来更可控也更容易优化内存复用。这也是为什么在纯 CPU 机器上sd.cpp 的出图速度往往比同配置下的 diffusers 快不少。1.2 它不是 ComfyUI 的替代品而是另一条路线很多人第一次看到 stable-diffusion.cpp 会问这玩意是不是要取代 ComfyUI我觉得这个理解完全跑偏了。ComfyUI 是节点式工作流优势在图形化编排、ControlNet 插件、自定义节点、动态流程适合在电脑前反复调 prompt、做精确的构图控制。stable-diffusion.cpp 走的是完全相反的路线它只有一个命令行入口没有画布、没有节点甚至连模型都只认 GGUF。它更适合的场景是服务器批量生成、程序内嵌调用、无 GPU 机器推理、自动化流水线。换句话说ComfyUI 是画师的工作台stable-diffusion.cpp 是生产线上的机械臂。两者可以共存甚至可以在同一台机器上互补用 ComfyUI 调好参数和风格然后丢给 sd.cpp 去批量跑。2. 先看模型骨架再看 C 怎么把它算出来Stable Diffusion 之所以能在 C 里跑不是因为有人把 Python 翻译成了 C而是因为它的推理路径本身很清晰可以拆成三个独立模块CLIP、UNet、VAE。理解了这三块分别干什么再看 sd.cpp 的代码就不会迷路。2.1 CLIP 负责听懂UNet 负责去噪VAE 负责出图SD 的完整推理流程可以拆成三段。第一段是文本编码。CLIP text encoder 把用户输入的 prompt 切分成 token最多 77 个然后转成一串高维向量。这段向量的质量直接决定了生成内容能不能贴合语义。stable-diffusion.cpp 里这部分也走 GGML 计算图把所有 transformer 层用 C 重新实现了一遍。第二段是 UNet 去噪。生成过程不是直接画图而是从一张纯随机的 latent 噪声图出发按 steps 逐步去噪。每一步UNet 都接收当前的噪声 latent、步数编码和文本条件向量预测出这一层的噪声然后用采样器的规则更新 latent。这一步是整个生成里计算量最大的反复迭代SD1.5 的 UNet 有接近 9 亿参数SDXL 更是达到 26 亿。第三段是 VAE 解码。去噪收敛后的 latent 不是正常图像它是一个下采样到 64x64 或者更大尺寸的低维表示。VAE decoder 负责把它放大还原成像素级的 RGB 图。VAE 对精度很敏感如果权重被量化成低 bit图里很容易出现彩色噪点、条纹和奇怪的色块。所以实际使用中我总是建议把 VAE 保持 fp16至少也要 q8_0。2.2 采样循环与采样器为什么同一个模型能画出不同风格如果不看采样器UNet 只是光会预测噪声。真正决定生成路径的是每一步怎么把预测出来的噪声转换成新的 latent。这就是采样器在做的事。stable-diffusion.cpp 支持的采样器不少常见的有 Euler、Euler a、Heun、DPM 2M、DPM 2M SDE、DDIM、PNDM 等。它们的差别在数值求解常微分方程的方式Euler 最简单每一步只按当前梯度走一步DPM 2M 用了更高阶的近似可以在相同步数下得到更干净的细节Euler a 是祖先采样每一步都重新注入随机性出来的图变化更大适合早期玩随机风格DDIM 则相对稳健在步数较少时也不容易崩。我用下来最顺手的组合是普通出图用 Euler 或 DPM 2M步数 20 到 30想快速预览用 DDIM15 步就够想要更多随机变化再切 Euler a。cfg-scale 这个参数同样重要它控制 prompt 对图像的引导强度默认 7.0。调太高画面会过饱和、出现伪影调太低 prompt 影响力就弱图容易跑偏。建议在 5 到 9 之间试。同样的模型不同采样器、不同步数、不同 cfg出来的图差异非常大。这不是玄学是每一步数值计算路径不同。2.3 GGUF 量化与精度取舍4bit 模型还能看吗GGUF 容器里可以放不同精度的张量这也是 sd.cpp 能在低配机器上跑起来的关键。常见的量化级别和实际体感如下表所示精度UNetCLIPVAE 整体体积SD1.5 约画质体感适用场景fp16约 4GB与原版一致显存足够、追求最佳质量q8_0约 2.2GB肉眼几乎无差别8GB 内存以上机器q5_0约 1.4GB极轻微细节损失8GB 内存老电脑q4_0约 1.1GB细节有可见损失但构图能保持极限低配这里的体积是整体模型文件大小不是运行内存。实际运行内存还要加上计算图中间激活值、CLIP 文本向量、UNet 的前向中间结果。所以我经常建议先用 fp16 或 q8 跑一张图确认效果再逐步降量化级别找自己的心理底线。反正命令行就一行换模型文件只是换个路径的事。还有一个关键点量化不是平均分配损失的。UNet 稍微吃点量化没关系CLIP 也还行但 VAE 千万别压到 q4。我试过把 VAE 也塞到 q4出来的图整体泛白还有周期性的网格噪点后来把 VAE 单独换成 fp16图立刻正常了。3. 从源码编译到第一张图一条可复现的本地部署路径光讲原理不给操作不是我的风格这一节直接把我的部署过程一步步写出来。整个过程在 Ubuntu 22.04 和 macOS 上我都试过Windows 用 MSVC 也能走通只是编译命令略有差别。3.1 拉代码、装依赖、CMake 构建首先把仓库克隆下来注意要带--recursive因为项目依赖 GGML 的子模块git clone --recursive https://github.com/leejet/stable-diffusion.cpp cd stable-diffusion.cpp cmake -B build -DCMAKE_BUILD_TYPERelease cmake --build build --config Release -j什么都不指定的话默认是 CPU 后端。构建完可执行文件在build/bin/sd。想在 NVIDIA 卡上跑就在 cmake 时加-DSD_CUDAONAMD 卡或部分 Intel 卡用-DSD_VULKANONmacOS 上自然是-DSD_METALON。这几个后端开关是互斥的选一个就行。第一次编译会比较久因为要同时编 GGML 子模块。如果中途报找不到 Git 子模块多半是 clone 的时候没加--recursive可以用下面的命令补救git submodule update --init --recursive我踩过的最蠢一个坑是在服务器上编译时CMake 找不到 CUDA 路径结果SD_CUDAON被静默忽略最后还是 CPU 后端。所以编完一定要用sd --help看一眼是否真的启用了 GPU 后端或者跑一个大点的图看显存占用有没有变化。3.2 模型从哪来safetensors 转 GGUFstable-diffusion.cpp 只认 GGUF 格式所以要么直接下载别人转好的 GGUF要么自己从 HuggingFace 原模型转换。HuggingFace 上很多机构已经放了转换好的 GGUF 文件搜 stable-diffusion-v1-5 GGUF 就能找到下载解压即可。愿意自己动手的话先把原版 safetensors 模型下载到本地例如git clone https://huggingface.co/runwayml/stable-diffusion-v1-5然后进入 sd.cpp 项目目录跑转换脚本python convert.py --model stable-diffusion-v1-5 --outdir models转换脚本会自动拆出 CLIP、UNet、VAE 三个部分打包成一个 GGUF 文件并默认用 fp16 保存。如果你需要量化版本再跑一步脚本把 fp16 的 GGUF 转成 q8_0 或 q4_0。转换过程需要 Python 环境但这只是在准备阶段用一次真正推理完全不依赖 Python。转换完成后把 GGUF 文件放到一个干净目录建议用一个固定的模型目录比如~/models/sd。以后每次跑推理命令行里直接用绝对路径引用省得出错。3.3 命令行逐参数拆解从提示词到输出文件跑第一张图最简单的命令是这样./build/bin/sd \ -m ~/models/sd/stable-diffusion-v1-5-fp16.gguf \ -p a cute cat sitting on a windowsill, digital art, soft lighting \ -n blurry, low quality, watermark \ --steps 20 \ --cfg-scale 7.0 \ --sampler euler \ --seed 42 \ -H 512 \ -W 512 \ -o output.png逐个解释一下这些参数它们几乎决定你出图的全部体验-m模型 GGUF 路径必须写对。-p正面提示词。用英文效果最好后面会细说中文的事。-n负面提示词这个参数非常建议填。一个简单的 blurry, low quality 就能明显减少花图。--steps去噪步数。20 是个不错的起点少于 10 会糙多于 40 边际收益很低。--cfg-scale引导强度7.0 是保守默认值。--sampler采样器名字。第一次建议 euler。--seed随机种子。固定住才能在调整参数时复现同一张构图。-H/-W输出尺寸。SD1.5 原生训练在 512x512先别贪大。-o输出文件路径支持 PNG。跑完会在终端打印每一步的时间以及最终保存路径。CPU 机器上20 步 512x512 大概在几十秒到两三分钟不等取决于线程数和内存带宽。NVIDIA 卡上一般几秒到十几秒。如果你是想做图生图用-i指定输入图片再加一个--strength控制重绘强度。strength 接近 1.0 就基本重画接近 0.1 只做微调。4. offload 到内存到底是不是权重显存不够时的真正玩法网上搜 llama.cpp 或者 stable-diffusion.cpp 的时候经常看到offload 到内存的说法有人会问offload 到内存的是权重吗还是什么别的东西这个问题我一开始也绕晕过实际用下来其实是这么回事。4.1 模型权重住在哪计算就在哪GGML 生态里模型权重可以放在两种地方系统内存RAM和显存VRAM。参数--gpu-layers控制的是把多少层放到显存里算剩下的层留在系统内存里。你输入的 prompt、采样的中间 latent、UNet 每一层的激活值这些计算中间数据当然也在内存里但真正占大头的是权重。如果显存只够放下 20 层那这 20 层的权重就常驻显存其余层权重留在 RAM。计算到某一层时如果它的权重在 RAM就需要先把这层权重拷贝到显存算完再拷回去或者直接丢弃。所以offload 到内存本质上就是权重本身放在内存里算的时候临时通过 PCIe 搬进显存。它不是某种虚存映射或者权重压缩在内存里的魔法代价就是每层都有一次拷贝开销PCIe 带宽决定了你能跑多快。如果层数分配得太狠性能反而不如纯 CPU因为 CPU 计算至少不用来回搬运。打个比方显存是桌面内存是档案室。把权重放显存就是资料全摊在桌上随手翻放内存就是资料在档案柜里每次要查某层得跑一趟档案室抱出来翻完再放回去。抱来抱去的时间就是 PCIe 传输开销。4.2 不同配置下的参数建议根据我实际在几台机器上跑出来的经验大致可以按下面这个表去设初始参数硬件配置推荐做法出图尺寸建议4GB 显存fp16 模型全部层 offload 到显存不够就换 q8 部分层留在 RAM512x512SD1.5 合适6GB 显存q8 或 fp16--gpu-layers给到显存快满即可512x512可尝试 768x7688GB 显存fp16 模型基本能全放SDXL 需要 q8 / q4512x512 到 1024x1024无独显全 CPU 跑量化到 q5/q4线程数给满512x512 以下别硬上 SDXL一个很容易踩的坑是--gpu-layers不是越大越好。如果你把超过显存容量的层数强行 offload程序可能崩或者开始疯狂 swap速度比纯 CPU 还慢。我建议先用默认值或保守值跑通然后用nvidia-smi观察显存占用再逐步往上加层数找到临界点。如果你的机器是 8GB 内存的老笔记本那也不要慌。q4 量化后的 SD1.5 模型加上 512x512 输出运行时的实际占用是可以压到 6GB 以内的。我在一台只有 8GB 内存、无独显的旧 ThinkPad 上跑通过一张图大概 90 秒虽然慢但真能出图。5. 优化与踩坑把 stable-diffusion.cpp 塞进老电脑光能跑通不算本事要跑得舒服才算。这一节把我遇到过的几个典型问题和优化方向整理出来省得你再去搜半天。5.1 后端三选一CUDA、Vulkan、Metal后端选择直接决定帧率和显存利用效率绝不能随便。NVIDIA 卡无脑选 CUDA这是最成熟的后端。A 卡优先 VulkanIntel 核显也可以试试 Vulkan但驱动兼容性要碰运气。macOS 用户选 MetalM 系列芯片上效率不错。Vulkan 后端容易遇到的问题是编译期找不到 Vulkan SDK。Ubuntu 上先装依赖再编sudo apt install libvulkan-dev glslang-toolsWindows 上则需要装 LunarG Vulkan SDK并且确保 CMake 能找到它的路径。AMD 卡用 Vulkan 跑 SD 是可行的我实测 RX 6600 上 20 步 512x512 大约 8 秒虽然比同级别 N 卡慢一点但已经属于完全可用的水平。还有一个容易忽略的点CUDA 和 Vulkan 不能同时开Metal 也不能跟它们混用。configure 阶段只会选一个后端你可以在build/CMakeCache.txt里搜SD_CUDA、SD_VULKAN、SD_METAL来确认到底开了哪个别想当然。5.2 线程数和静态图的吞吐门道CPU 推理时-t参数控制线程数。这里很有讲究不是线程越多越快。超线程出来的是逻辑核心跑这种数学密集任务时逻辑核心多开反而会因为资源争抢变慢。我自己的经验是-t设置为物理核心数效果最好。更重要的是如果同一台机器反复跑同一分辨率的图尽量保持长驻进程。static graph 或者说计算图复用能省掉大量重复构图开销。第一次跑耗时相对长第二次明显变快这不是幻觉是 C 推理图把很多准备工作做了一次缓存。如果你做批量测试强烈建议用 batch-count 参数一次跑多张而不是每张都重启进程。./build/bin/sd -m model.gguf -p same prompt, different seeds \ --steps 20 --batch-count 8 --seed 100 -o batch.png这样出的图会按编号命名省得反复加载模型。5.3 高分辨率、中文提示词、VAE 坏像素先说高分辨率。SD1.5 原生训练在 512x512直接拉到 1024x1024 经常会崩结构或者出现重复物体。sd.cpp 的高分辨率玩法跟 diffusers 类似先在低分辨率生成再放大但这一步不能靠一个简单的-H 1024解决要配合 VAE tile 相关选项否则解码时显存和内存都容易爆。我的建议是普通 SD1.5 先用 512x512 或 768x768再外接一个 upscale 模型处理放大。中文提示词是个大坑。CLIP 的分词器对中文非常不友好中文句子喂进去常常被切得七零八落出来的图跟 prompt 几乎没关系。我不是说 sd.cpp 有 bug这是原版 Stable Diffusion 的 tokenizer 能力边界。解决方案很粗暴先把中文 prompt 翻译成英文再喂。如果你硬要用中文那就找专门支持中文 token 的模型或另外接翻译接口别指望 CLIP 能学好中文。VAE 坏像素的问题在前面说过核心就是别让 VAE 掉进低量化精度。如果你拿到一个转换好的 GGUF 文件不确定 VAE 精度可以直接跑一张纯白 prompt 看背景是不是干净如果出现网格状色斑或颗粒那基本就是 VAE 被压得太狠。解决办法是找一个 VAE 精确保留的 GGUF或者自己转换时指定 VAE 为 fp16。还有一个高分辨率相关的实用技巧在命令行里加--vae-tiling可以让 VAE 解码时分行分块处理大幅降低峰值内存。质量损失肉眼几乎看不出但对老机器很友好内存不够的时候值得开。6. 什么时候用它什么时候老老实实回 ComfyUI这一节不是说谁好谁坏而是想给你一个判断标准。我自己两个都在用切换场景很明确。6.1 它真正擅长的地方stable-diffusion.cpp 最让我推荐的使用场景有三个。第一是无人值守的批量出图。服务器上放一个 sd 可执行文件一个脚本循环换 prompt 和 seed不用管 conda 环境不用担心中途依赖崩了。跑几天几夜都稳定。第二是嵌入到自己的程序里。C 项目可以直接链接 sd.cpp 的推理接口或者通过命令行调用。我在一个内部的自动化配图服务里就是这么干的PHP 后端调用 sd 命令prompt 由业务侧动态拼装响应时间完全可控。第三是低配机器和临时机器。没有 N 卡的机器、云上的小规格实例、甚至树莓派级别的设备只要内存够、CPU 够就能跑 SD1.5。这东西能做到有网就能装装完就能跑。6.2 它现在还比较吃力的地方与生态丰富的 Python 派相比sd.cpp 的短板也很明显。ControlNet 这类精确控制手段在 sd.cpp 里的支持远不如 ComfyUI 成熟。如果你想做精确的姿态控制、深度图控制、线稿上色ComfyUI 加插件几乎是无脑选择而 sd.cpp 这边要么不支持要么需要自己跟上游进度。Lora 虽然可用但管理起来也不如 ComfyUI 方便节点拖一拖就行。动态流程就更不用说了。ComfyUI 可以做到根据中间结果选择分支一个模型生成轮廓另一个模型填色再串一个 upscale 节点每个节点都有可视化输出。这种灵活度命令行工具给不了。所以做创作探索、调工作流我始终在 ComfyUI 里做。6.3 我的选择标准现在我的原则很简单如果任务是探索风格、精调构图、用 ControlNet 控制细节我开 ComfyUI它能让我看到每一个中间环节。如果任务已经定死prompt 模板写好只需要换 seed 批量跑或者要部署到一台没有 Python 环境的服务器我直接把 sd.cpp 拿出来用。还记得第一次在那台 8GB 内存的老 ThinkPad 上跑通 SD 时我盯着终端里一行行进度输出最后看到 output.png 生成那种感觉比在 4090 上跑 8K 图还爽。有些时候工具的价值不在于它多智能而在于它把原本需要昂贵环境才能做的事拉回到了任何一台电脑都能试试的层面。stable-diffusion.cpp 就是这个拉回动作里很扎实的一块砖。