← 返回文章列表

4GB 显存的逆袭:我在 GTX 1650 上跑出了生产级文生图服务

我有一台旧电脑,上面有一块1650,虽然尝试过LLM,但是对于生产力(编程)来说,部署不了太大的模型,所以另辟蹊径,决定试试文生图,于是有了这次探索……

封面:4GB 显存的逆袭

(封面图:SANA-Sprint 在 GTX 1650 上生成的 1024×1024 图像)

一、先摸清家底:这块 1650 还能干什么

旧电脑配置如下:

项目配置评价
GPUNVIDIA GTX 1650,4GB 显存Turing 架构(SM 7.5),无 bf16 单元、无 Tensor Core 优惠
CPU6 核 x86_64中规中矩
内存16GB关键——它是"第二显存"
磁盘457GB,剩余 333GB放模型绰绰有余

4GB 显存是什么概念?现在主流开源文生图模型的显存需求大致是:

模型参数规模原生分辨率FP16 显存需求4GB 卡可行性
SD 1.5~1B512px~2.5GB✅ 舒适
SDXL~2.6B1024px~7GB⚠️ 勉强(需量化+offload)
SD 3.5 Large8B1024px~16GB❌
Flux.1 Dev12B2048px~24GB❌(量化版勉强)
Qwen-Image20B1328px~40GB❌ 别想了
Sana 1.6B1.6B1024px~6GB⚠️ 有戏(分时复用)
SANA-Sprint 1.6B1.6B1024px~6GB⚠️ 同上,但只要 2 步

结论很清晰:第一梯队(Flux、Qwen-Image)的原生推理无缘,但 SD1.5 舒适、Sana 系列够得着。我的目标是——不将就 SD1.5 的画质。

二、diffusers 还是 ComfyUI?想清楚"给谁用"

动手前先做选型。ComfyUI 名气大,但我的需求很明确:程序调用,不关心界面微调。

维度diffusersComfyUI
定位Python 库(HuggingFace)节点式图形应用
调用方式直接写 PythonHTTP API + 拼 JSON 节点图
界面无完整 WebUI
显存优化cpu_offload 一行代码--lowvram 启动参数
适合场景嵌入应用、批量生成、API 服务交互式创作、工作流实验

给 AI/程序调用的话,让程序去构造一长串节点图 JSON 是自找麻烦。diffusers 几行代码就是一个生成函数,果断选它。

环境:Python 3.10 + 独立 venv,装 torch(CUDA 版)、diffusers、transformers、accelerate。

三、第一课:国内下载模型,先测速再动手

torch 的 CUDA 包有 2.5GB+,huggingface 直连基本龟速。我先把国内常见镜像源全部测了一遍(同一文件读 200KB 计速):

源延迟带宽
阿里云 pypi0.39s502 KB/s ✅
中科大0.52s379 KB/s
豆瓣0.73s266 KB/s
清华1.10s178 KB/s
pypi 官方0.96s204 KB/s
pytorch 官方0.93s60 KB/s

模型本体走 ModelScope(魔搭)——对比相当残酷:HuggingFace 镜像同样时间下了不到 5MB,ModelScope 几分钟 293MB,最终 9.3GB 模型 37 分钟下完。

教训 1:动手下载前,先花一分钟测速选源,能省几小时。

还有个坑:ModelScope 上的仓库经常把 FP32/FP16/ckpt/分片格式全打包(Sana 首轮下载冲到 25GB!)。用 snapshot_download 的 allow_patterns 参数只拿需要的文件,或者下完手动清理,能省一半磁盘和一半时间。

四、SD1.5 基线:一张黑图引发的"侦探案"

SD1.5 下载完,25 步出图 143 秒、显存峰值 2.48GB——数字都很美好,直到我打开生成的图:

纯黑。 于是开始逐层排查:

实验结果排除/指向
fp16 矩阵乘 / group_norm / silu 单元测试全部有限排除基础算子
文本编码器输出有限排除编码器
4 步短管线出 latentsNaN锁定去噪阶段
扫描 unet/vae 权重文件686+248 个张量全完好排除下载损坏
换 eager attention仍 NaN排除 attention kernel
禁用 cuDNN 再跑完全正常!锁定 cuDNN

根因:cuDNN 9.24 在 Turing 老架构(SM 7.5)上的 fp16 卷积会产出 NaN。这是新版 torch 2.14 + cuDNN 9 组合在老卡上的兼容性问题。

但禁用 cuDNN 后原生卷积的工作区内存暴涨,单笔卷积要 2.25GB,直接 OOM。最终方案是三件套:

torch.backends.cudnn.enabled = False   # 1. 绕开 cuDNN bug
pipe.unet.to("cpu")                    # 2. 解码前卸载 UNet(1.7GB 大头)
pipe.vae.enable_tiling()               # 3. VAE 分块解码防 OOM

修复后:橘猫图正常出图,143 秒/张。作为基线,画质确实"能用但一般"。 SD1.5 修复后生成图

(修复 cuDNN 问题后,SD1.5 在这块 1650 上的第一张图)

五、Sana 1.6B:4GB 显存上的"极限运动"

SD1.5 只是及格线,真正想要 1024px 高画质,得上 NVIDIA 的 Sana 1.6B——线性注意力 DiT 架构,专为低算力设计。但它有个致命配置:Gemma2-2B 文本编码器(5GB fp16)+ 1.6B transformer(3.2GB)+ DC-AE VAE,加起来远超 4GB 显存。

CUDA OOM 的报错我见了一晚上。管线级量化、组件级 4bit、model offload……全部以 OOM 收场。最后的解法是一场"显存接力赛":

文本编码器(4bit)上GPU编码 → 立刻释放 → transformer独占GPU去噪 → 释放 → VAE在CPU解码

任一时刻显存里只有一个大件。期间还踩了一堆小坑:

坑症状解法
bnb 4bit 不量化 embedding(1.2GB)装载即 OOMdevice_map={"embed_tokens":"cpu","":0} 把它摁在 CPU
embed_tokens 键名猜错整模型被推上 GPU真名是 embed_tokens,没有 model. 前缀
encode_prompt 没包 no_grad每层反量化权重被驻留,显存翻倍套上 torch.no_grad()
negative_prompt 与 embeds 冲突500 报错管线调用时显式传 negative_prompt=None
DC-AE fp16 数值溢出解码输出异常官方要求 fp32/bf16,VAE 保持 fp32

最终跑通:1024px 一张 430 秒,显存峰值 3.13GB。画质对比 SD1.5 是碾压级的——毛发纹理、光影层次都上了一个档次。

Sana 生成图

(Sana 1.6B 生成的橘猫,1024×1024)

六、SANA-Sprint:同一架构,快 7 倍的答案

Sana 画质虽好,但 430 秒一张的等待实在煎熬——我并不满意,一张图的时间够泡两杯茶。问题很自然:能不能既保住 1024px 的画质,又把速度提上去?查资料时发现了 NVIDIA 的 SANA-Sprint——同一架构的一致性蒸馏版,把 20 步推理压到 1-4 步,1 步的 FID 甚至超过 FLUX-schnell。更妙的是:组件完全相同,我趟出来的整套显存方案直接复用。

SANA-Sprint 生成图

(SANA-Sprint 2 步生成的橘猫,1024×1024,油画质感)

当然,蒸馏模型也有新脾气。它用 SCMScheduler + trigflow 参数化,官方指定 bf16——而 GTX 1650 没有 bf16 硬件单元。我手贱转成 fp16"加速",结果 latents 直接 NaN 黑图;保持 bf16(PyTorch 软件模拟)不仅数值正确,反而比 fp16 更快(去噪 8.6s vs 29.5s)。

教训 2:老卡上"降精度加速"不一定成立,官方推荐的 dtype 有它的道理。

另外它的 encode_prompt 没有 CFG 参数(一致性蒸馏没有负向 prompt),管线调用接口也对不上普通版,这些差异都记在了代码注释里。

七、三模型同台:最终成绩单

同一个提示词(窗台上晒太阳的橘猫),三个模型在同一块 1650 上的真实表现:

指标SD1.5Sana 1.6BSANA-Sprint
分辨率512px1024px1024px
去噪步数25202
去噪耗时~110s~390s8.6s
端到端耗时~143s~430s~60s
显存峰值2.48GB3.13GB3.11GB
画质玩具级良好最佳(油画质感、光影层次)

三张图放在一起看,差距一目了然:SD1.5 的猫糊成一团色块,Sana 的猫已经像样,Sprint 的猫毛发根根分明、逆光的暖阳甚至有"作品感"。

画质和速度竟然是同向的——这是蒸馏技术送给低配机的礼物。

八、临门一脚:封装成 API 服务,systemd 常驻

能出图只是玩具,能被调用才是生产力。我把 Sprint 封装成 FastAPI 服务:

端点功能
GET /health健康检查
POST /generate同步出图,返回 PNG base64(单并发,忙时 429)
POST /generate_async提交任务进队列,返回 task_id
GET /result/{task_id}查询异步结果

4GB 显存跑不了并发,服务内部同样采用"接力赛"调度:tokenizer 常驻,4bit 编码器每请求加载-释放,transformer 按需上卡,VAE 常驻 CPU 解码。全局锁保证同一时刻只有一张卡在干活。

然后写了一个 30 行的 systemd 单元:

[Service]
ExecStart=/opt/sd-env/bin/python -m uvicorn app:app --host 0.0.0.0 --port 8486
Restart=on-failure
RestartSec=15
Environment=PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True

systemctl enable --now sd-api,开机自启、崩溃自动拉起、日志进 journald。现在这台旧电脑是一台安安静静的文生图服务器:

curl -X POST http://<ip>:8486/generate \
  -H "Content-Type: application/json" \
  -d '{"prompt":"蜘蛛侠大战,比例9:16"}' \
  | jq -r .image_b64 | base64 -d > out.png

spiderman_sunwukong_9x16

(通过 API 生成的第一张图)

九、复盘:低配机玩 AI 的四条心法

  1. 测速选源先行。国内环境,ModelScope 下模型、阿里云装 pip 包,动手前一分钟测速能省几小时。
  2. 官方推荐的 dtype 别乱改。老卡上 fp16 不一定比 bf16 快,NaN 黑图教我做人。
  3. 4GB 显存的答案是"时间换空间"。编码→释放→去噪→释放→CPU 解码,任一时刻显存里只放一个大件,6GB 需求的模型照样跑。
  4. 不要在报错堆里硬刚。黑图就二分定位(算子→编码器→去噪→解码逐层排查),OOM 就查显存水位,每个问题都有清晰的证据链。

这块 1650 或许永远跑不动大语言模型,但它现在是一台 60 秒一张 1024px 图的文生图服务器——旧硬件的宿命不是吃灰,而是找到适合它的那类任务。


环境备注:Ubuntu / Python 3.10 / torch 2.14.1+cu130 / diffusers 0.41.0 / bitsandbytes 0.50.2,模型与脚本均在文中注明路径。遇到同款黑图或 OOM 的朋友,欢迎交流。