边缘盒子跑生图:JetPack 6.0 + Orin AGX 部署轻量版全流程
图像生成模型正在经历一轮"加速瘦身":从 40 步、带 CFG 的重推理,压缩到 4~8 步、无 CFG 的轻量蒸馏。社区里 Qwen-Image 加速 LoRA、Lightning 版的讨论热度居高不下,而这一类"少步数 + 量化"的组合,恰好是它从云端数据中心走向边缘设备的门票——功耗墙和显存墙在 Jetson 这类设备上比在 A100 上真实得多。本仓库 Qwen-Image-2.1-viggle-turbo 正是这条路线上的一个具体样本:以 Qwen/Qwen-Image-2.1 为底座,用 6 步替代 40 步、完全关闭 classifier-free guidance,端到端提速约 5 倍,同时把 LoRA、单文件量化 Transformer、ComfyUI 工作流、调度器配置全部打包成可直接落地的资产。社区已有文章完整记录了 JetPack 6.0 + Orin AGX 上部署轻量版 Qwen-Image-Edit 的全流程(环境配置、PyTorch 适配、模型下载、FastAPI 服务搭建、性能优化),本文将以该仓库为源码依据,把这条"边缘盒子跑生图"的链路拆开讲透:JetPack 6.0 上的 PyTorch 适配有哪些坑、BF16 推理怎么配、FastAPI 离线服务怎么搭、以及到底能用什么性能数据来验证。
轻量版到底轻在哪:6 步取代 40 步
先看仓库里对这个模型的定义。README 开头写得很直白:这是 Qwen-Image-2.1 的 few-step 蒸馏版本,支持文生图与指令驱动的图像编辑(1~3 张参考图),推理只需6 步,且不需要 classifier-free guidance,端到端比 40 步的基座模型快约 5 倍,在官方示例上多数提示词的画质与基座难分伯仲,明显的差距只在小号密集文字和复杂编辑场景。
所谓"轻量",体现在仓库的三个层次上:
- 蒸馏 LoRA 适配器:
Qwen-Image-2.1-viggle-turbo-v0.3-6step-lora-r256.safetensors(rank 256,bf16,约 1.3 GB)以及其按层 SVD 截断得到的 r128 版本(约 0.7 GB),另有peft_v0.3/提供 peft key 格式的同一适配器。适配器不改变基座权重,推理时挂载即可。 - 单文件合并量化版:把 LoRA 在 fp32 下合入基座 Transformer、再量化一次,产出 int8 convrot、fp8 e4m3fn、GGUF Q8_0/Q6_K/Q5_K_M/Q4_K_M 共 6 种格式,从 7.7 GB 一路压到 4.3 GB,这是边缘显存预算的关键。
- 配套调度器与 ComfyUI 资产:scheduler/scheduler_config.json 修正了基座配置中会毁掉最后一步的
shift_terminal: 0.02(改为null);comfyui/viggle_turbo.py 提供两个自定义节点,comfyui/下还放了 t2i / edit 两套工作流(LoRA 版与 merged/GGUF 版各一)。
版本演进也体现了蒸馏调优的节奏:v0.1 是 4 步 r64 全参数微调(Qwen-Image-2.1-viggle-turbo-4step-lora-r64.safetensors与transformer/),v0.2 是 5 步 r128/r256,v0.2.1 与 v0.3 都是 6 步 r128/r256,其中 v0.3(2026-09-29 发布)更干净少颗粒、纹理略柔和,并新增了"7 步 turbo + 2 步基座"的 9 步模式用于救小字。维护者自己的判断是:6 步已接近该蒸馏配方的容量上限,再往上提画质只能靠加步数。
环境与 PyTorch 适配:JetPack 6.0 上的坑
JetPack 6.0 对应的系统基线是 Ubuntu 22.04、CUDA 12.x,Orin 系列 GPU 是 Ampere 架构。这个组合给 PyTorch 适配埋了几个容易踩的坑:
坑一:不能直接装 x86 的 CUDA wheel。Jetson 的 PyTorch 需要 NVIDIA 官方为 Jetson 构建的 wheel(JetPack 6.0 对应 PyTorch 2.4 系列),装错了轻则算子走 CPU 回退,重则加载模型直接崩。装好后务必验证torch.cuda.is_available()与torch.backends.cuda是否正常。好在 Ampere 架构原生支持 bfloat16,BF16 推理在 Orin 上可跑,这也是后面模型加载用dtype=torch.bfloat16的前提。
坑二:diffusers 必须用 pinned 的 git 版本。仓库 README 的安装命令写得很明确:
pip install -U torch "transformers>=5.17,<6" accelerate safetensors peft pillow pip install "git+https://github.com/huggingface/diffusers.git@80c7ed262aeffbeb43ef13ae04baeb9b84515a69"原因在 README 里一句话点破:QwenImage21Pipeline尚未进入 diffusers 的正式 release,因此必须锁定到这个 commit;同时transformers>=5.17,<6、peft都是硬依赖。这不是可选的"建议版本",而是能跑起 6 步调度的硬约束——少装一个 peft,load_lora_weights就会在运行时缺模块。
坑三:对"显存"的认知要切换。Jetson 是统一内存架构,CPU 与 GPU 共享同一个物理内存池(Orin AGX 为 64 GB),不存在 x86 上那种显存耗尽而内存空置的局面,但也意味着"省显存"本质上就是"省整机内存预算"。后面计算模型栈总占用时,应把 Transformer、文本编码器、VAE 和 LoRA 加总来看,这正是本仓库提供 int8/GGUF 单文件版的动机。
模型下载、BF16 推理与最小可运行代码
在 Orin AGX 上跑起来的核心代码不长,全部按 README 的规范来:
import torch from diffusers import QwenImage21Pipeline, FlowMatchEulerDiscreteScheduler pipe = QwenImage21Pipeline.from_pretrained("Qwen/Qwen-Image-2.1", dtype=torch.bfloat16) pipe.load_lora_weights( "Viggle/Qwen-Image-2.1-viggle-turbo", weight_name="Qwen-Image-2.1-viggle-turbo-v0.3-6step-lora-r256.safetensors", ) pipe.scheduler = FlowMatchEulerDiscreteScheduler.from_pretrained( "Viggle/Qwen-Image-2.1-viggle-turbo", subfolder="scheduler" ) pipe.to("cuda") STEPS, SIGMAS = 6, [1.0, 0.9375, 0.875, 0.75, 0.5, 0.25]文生图调用:
image = pipe( prompt="A studio portrait of an old fisherman mending a net, warm rim light, 85mm.", height=1024, width=1024, num_inference_steps=STEPS, sigmas=SIGMAS, true_cfg_scale=1.0, # 无 CFG(也是默认值) generator=torch.Generator("cuda").manual_seed(0), ).images[0] image.save("out.png")1~3 张参考图的指令编辑调用:
from diffusers.utils import load_image image = pipe( prompt="Replace the background with a sunset beach, keep the subject unchanged.", image=[load_image("input.png")], # 列表;顺序决定 <image1>、<image2>… output_resolution=1024, num_inference_steps=STEPS, sigmas=SIGMAS, true_cfg_scale=1.0, generator=torch.Generator("cuda").manual_seed(0), ).images[0]仓库 README 的"Rules that matter"一节,把这些代码背后的硬规则交代得很清楚,边缘部署时每一条都影响成败:
- 6 步调度是裸节点:
sigmas=[1.0, 0.9375, 0.875, 0.75, 0.5, 0.25]是未做分辨率位移的原始节点,pipeline 会自行施加 resolution-dependent shift,因此任何尺寸下都照原样传入;改步数只能在高噪声端增删节点(5 步为[1, 0.875, 0.75, 0.5, 0.25],7 步为[1, 0.9583, 0.9167, 0.875, 0.75, 0.5, 0.25]),并必须保留0.875, 0.75, 0.5, 0.25。 - 必须用仓库自带的调度器配置:基座的
shift_terminal: 0.02会毁掉最后一步,所以 scheduler/scheduler_config.json 里把它改成了null,同时保留use_dynamic_shifting: true、base_shift: 0.5、max_shift: 0.9。 - LoRA 保持不合并、强度 1.0:把适配器 merge 进 bf16 权重会丢失约三成更新量;仓库提供的单文件合并版是在 fp32 下合入后再做一次量化,正是为了规避这条损耗。
- 分辨率甜点在约 1 MP,最高约 4 MP 可用,宽高必须是 16 的倍数。
LoRA 本身的结构可以从 peft_v0.3/adapter_config.json 看出:r: 256、lora_alpha: 256、use_rslora: false,目标模块覆盖to_q、to_k、to_v、to_out.0、img_mlp.proj、img_mlp.out、img_mlp.gate_layer、timestep_embedder.linear_1/2、modulation.1——即注意力投影 + SwiGLU MLP + 时间步调制三个子系统同时被蒸馏改造。基座 Transformer 的规模可从 transformer/config.json 读到:32 层、32 头、head_dim 128、context 维度 4096、patch_size 1,这是一个 DiT 形态的大模型,也正因如此"步数压缩 + 量化"才是它上边缘设备的唯一现实路径。
编辑工作流在comfyui/的 edit 工作流里默认使用两张示例参考图,即 comfyui/input/cat.webp 与 comfyui/input/woman2.webp(来自 flux-klein 演示 Space),分别以 512×768 的纵向构图演示"主体替换背景"类编辑任务:
需要注意一个容易搞混的点:编辑工作流中输出尺寸跟随image 1(约 1 MP),而 diffusers 路径跟随最后一张参考图;参考图顺序直接决定提示词里image 1/image 2指代哪张图。多参考编辑时,把决定取景的那张图放在对应位置。
用 FastAPI 把它变成全链路离线服务
社区那篇 JetPack 6.0 + Orin AGX 部署文章的落点就是 FastAPI Web 服务:一次启动、常驻进程、HTTP 收发,前端/业务侧无需任何 Python 环境。在 Orin AGX 上照同样的模式把上面的 pipeline 包一层即可,关键设计是模型只在进程启动时加载一次,请求期间不做任何下载、不做任何联网检查,实现真正离线:
import io, torch, uvicorn from fastapi import FastAPI, UploadFile, File, Form from fastapi.responses import Response from diffusers import QwenImage21Pipeline, FlowMatchEulerDiscreteScheduler from diffusers.utils import load_image from PIL import Image STEPS, SIGMAS = 6, [1.0, 0.9375, 0.875, 0.75, 0.5, 0.25] pipe = QwenImage21Pipeline.from_pretrained("Qwen/Qwen-Image-2.1", dtype=torch.bfloat16) pipe.load_lora_weights( "Viggle/Qwen-Image-2.1-viggle-turbo", weight_name="Qwen-Image-2.1-viggle-turbo-v0.3-6step-lora-r256.safetensors", ) pipe.scheduler = FlowMatchEulerDiscreteScheduler.from_pretrained( "Viggle/Qwen-Image-2.1-viggle-turbo", subfolder="scheduler") pipe.to("cuda") pipe.set_progress_bar_config(disable=True) app = FastAPI() @app.post("/t2i") def text_to_image(prompt: str = Form(...), width: int = Form(1024), height: int = Form(1024), seed: int = Form(0)): img = pipe(prompt=prompt, width=width, height=height, num_inference_steps=STEPS, sigmas=SIGMAS, true_cfg_scale=1.0, generator=torch.Generator("cuda").manual_seed(seed)).images[0] buf = io.BytesIO() img.save(buf, format="PNG") return Response(buf.getvalue(), media_type="image/png") @app.post("/edit") async def edit(prompt: str = Form(...), refs: list[UploadFile] = File(...)): images = [load_image(Image.open(io.BytesIO(await f.read()))) for f in refs] img = pipe(prompt=prompt, image=images, output_resolution=1024, num_inference_steps=STEPS, sigmas=SIGMAS, true_cfg_scale=1.0).images[0] buf = io.BytesIO() img.save(buf, format="PNG") return Response(buf.getvalue(), media_type="image/png") if __name__ == "__main__": uvicorn.run(app, host="0.0.0.0", port=8000)两处细节提示:
- 提示词重写也走离线。官方 PE-T2I / PE-I2I 重写器对构图和文字渲染有明显帮助,它们是本地模型而非在线 API,ComfyUI 工作流里"Enhance prompt"节点用的就是 Qwen3-VL 文本编码器按官方重写器 system prompt 做贪婪解码。离线环境里这一点成立的前提是文本编码器权重已随模型一并部署。
- 无 CFG 让单步只做一次模型调用。
true_cfg_scale=1.0意味着每步只有一个前向,6 步 = 6 次 Transformer 前向,这直接决定了吞吐上限的估算方式——这也是"6 步代替 40 步"在边缘设备上最实质的收益。
全链路离线与性能数据验证
离线链路盘点。从启动到出图,整条链路的依赖清单是:基座权重(Transformer + 文本编码器 + VAE + processor 从 Qwen/Qwen-Image-2.1 加载)、LoRA 适配器、调度器配置、推理框架(torch + diffusers + peft),全部落在本地磁盘与内存中,运行时无任何外部调用。这正是边缘盒子场景要求的形态:内网、无外网、无鉴权依赖。
显存/内存预算。仓库 README 给出的 ComfyUI 参考文件规模可以直接换算成 Jetson 的整机内存预算:
| 组件 | bf16 | int8 convrot |
|---|---|---|
| Transformer(diffusion_models) | 14.2 GB | 7.3 GB |
| 文本编码器(text_encoders) | 17.5 GB | 9.4 GB |
| VAE | 0.7 GB | 0.7 GB |
int8 全栈权重约 17.4 GB,加 LoRA(r128 约 0.7 GB)与激活值,落在 Orin AGX 64 GB 统一内存内毫无压力;bf16 全栈约 32.4 GB,同样放得下但加载与首帧耗时更高。README 还实测了 ComfyUI 默认配置(int8 文件 + r128 LoRA + 提示词增强)在 1248×832 下峰值约26 GB——这可以作为"启动后整机可用内存至少要预留到 30 GB 以上"的工程依据。
如果还想再省,就用单文件合并量化版。仓库对每种格式都给了规模与质量标定(LPIPS,越小越接近 diffusers + r256 LoRA 的参考输出):
| 格式 | 大小 | 相对参考质量的偏离 |
|---|---|---|
| GGUF Q8_0 | 7.7 GB | LPIPS 0.051 |
| int8 convrot | 7.3 GB | LPIPS 0.057 |
| GGUF Q6_K | 6.0 GB | LPIPS 0.055 |
| fp8 e4m3fn(weight-only) | 7.3 GB | LPIPS 0.068 |
| GGUF Q5_K_M | 5.1 GB | LPIPS 0.076 |
| GGUF Q4_K_M | 4.3 GB | LPIPS 0.100(README 提示可见漂移,仅在其他尺寸放不下时使用) |
注意合并版与 LoRA 路径存在约 8/96 请求的构图差异,追求与 diffusers 严格一致时应走 LoRA 工作流;另外 Q4_K_M 有 23~29/96 的明显变化,边缘场景要按可接受度取舍。
性能验证的两个维度。第一是推理量本身:6 步 × 单前向对比基座 40 步 × 双前向(CFG),模型调用次数从 80 降到 6,这是约 13 倍的算子级缩减,README 所述"端到端约 5 倍"是叠加文本编码、VAE 解码等固定开销后的保守数字。第二是社区实测的横向参照:RTX 4090D 单卡上 Qwen-Image 加速版 4 步流程端到端平均 27.4 秒(1024×1024,FP16);Lightning 路线实测峰值显存 8~10 GB、较传统方案提速 8~10 倍。这些数字的共同指向是:少步数蒸馏 + 低精度推理的组合已经能把高端消费卡压到 30 秒级、显存压到 10 GB 级,Orin AGX 凭借 64 GB 统一内存与 Ampere 的 BF16 支持,跑 6 步方案具备可行性,只是单步延迟会比 4090D 高一到两个数量级——这也是为什么"6 步"对边缘设备不是锦上添花,而是刚需。
另一条路:ComfyUI 节点。若不想写服务代码,仓库在 comfyui/viggle_turbo.py 提供了两个自定义节点:ViggleTurboSigmas按潜变量尺寸实时计算动态指数位移的 sigma 序列(mu = calculate_shift(tokens, 256, 8192, 0.5, 0.9)),替代 KSampler 默认调度;ViggleTurboLora以y = Wx + BAx的运行时旁路方式挂 LoRA,避开 bf16/int8 权重上合并带来的损耗,代价是每步多 10~25% 耗时。工作流 JSON(t2i / edit,merged / GGUF 各一)均可在comfyui/下找到,配套的文本编码器、VAE 与 GGUF 加载节点(ComfyUI-GGUF)按 README 表格放置即可。
最后必须划清边界。其一,已知局限是客观存在的:复杂编辑(多参考合成、换脸、身份保持)可能不如基座,小号或超长渲染文字更容易花,色彩饱和度略低,2K 输出与 RGBA 等能力仅在官方示例上目测验证过,仓库不声称任何标准基准。其二,本模型是 Qwen-Image-2.1 的衍生作品,遵循Qwen RESEARCH LICENSE AGREEMENT(LICENSE):仅限非商业的研究与评测用途,商业使用需另行向许可方申请。部署到生产边缘盒子之前,这两点必须先过合规与验收关。
从 JetPack 6.0 的 PyTorch 适配,到 BF16 的 diffusers 最小推理,再到 FastAPI 离线服务与量化预算核算,这条链路里真正决定成败的不是模型有多新,而是每一步有没有把"6 步、无 CFG、离线、量化"四个约束贯彻到位。对于要在真实边缘设备上交付生图能力的技术团队,本仓库同时提供了配方(蒸馏 LoRA)、载体(单文件量化)与工程样板(ComfyUI 节点与工作流),值得作为 JetPack 6.0 + Orin AGX 部署实践的直接起点。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考