🧠 第七章 绘画模型训练:创意生成与视觉设计
📄 一句话概括

打开 LoRA 文件看一眼: 前 8 字节写头部长度(168 字节),然后是 JSON(每个张量的起止位置),再后面就是裸二进制。 两个张量、每个 16384 字节,读回来 2/2 一致,整个文件 0.03 MB。 这就是「部署」的全部内容:把文件放到对的目录,再核对三样元信息。

📝 本节和前面几节的关系

上一节把配置写对了,训练能跑起来。 这一节处理训完的产物——它到底是个什么文件,为什么不是 ckpt,放哪里,怎么验。

一、训完的产物是什么

训练结束,output 目录里会出现若干 *.safetensors。 这就是 LoRA 的全部——一个几 MB 到几百 MB 的文件,里面只有那些低秩因子的权重。 通常还会配一份说明(有的训练器自动生成 json,有的要自己写): 底模版本、触发词、推荐的 LoRA 权重、训练用的参数。这份说明比权重本身更容易被忽略, 也更容易在半年后救你的命。 部署就是把文件放进推理程序认识的目录:SD WebUI 是 models/Lora/,ComfyUI 是 models/loras/, Forge 与 WebUI 相同。放进去刷新一下,界面里就能选到。 📋 实操清单(在推理程序里做): 第一步,把 xxx.safetensors 复制进对应目录,刷新界面。 第二步,在文生图页的提示词框里写 <lora:xxx:0.8>(WebUI 语法)。 第三步,看提示词框里那一小段有没有变成高亮色。变了说明识别成功。 没高亮就是路径或文件名不对,这时候去查目录,别怀疑模型。

二、safetensors 里到底装了什么

格式简单到可以手写:8 字节(小端无符号整数)写头部多长,紧接着这段长度的 JSON, 再往后就是所有张量首尾相接的裸二进制。 JSON 里每个张量三项:dtype、shape、data_offsets。 data_offsets 是一对数字——这个张量在数据区从第几字节到第几字节。读取时按这对偏移切一刀, 直接按 dtype 解出来就行。 Demo 手写了一个只含两个张量的文件,读回来逐元素比对:

张量 形状 字节数 读回来
lora_unet_a 8×512 16384 一致
lora_unet_b 512×8 16384 一致
两行都是「一致」,说明格式理解和写入都是对的。
真实 LoRA 只是张量多几十倍、名字带层号,机制一模一样。
头部里还常有一段 __metadata__,装的是 ss_network_dim、ss_base_model_version、
ss_output_name 这些训练信息。这段不进权重,但它是「这个 LoRA 是用什么训的」的唯一记录。

三、为什么不是 ckpt

老格式 .ckpt 是 Python 的 pickle。pickle 在反序列化时会执行代码——这是设计如此,不是漏洞。 所以一个恶意 ckpt 可以在你「加载模型」的瞬间跑任意代码。 safetensors 反过来:它只有数字和偏移量,没有任何可执行内容。加载它等于按偏移量读一段字节, 没有「执行」这一步。 这就是为什么几乎所有训练器和模型站都默认 safetensors。 但它只解决「不执行代码」,不解决「权重本身有问题」——被投毒的权重照样能训出乱七八糟的图, 它只是不会偷你的文件。 所以两条规矩: ① 优先选 safetensors;② 来源不明的权重,先在隔离环境里试跑。 第二条比第一条更重要。

四、部署之后先核对三样

① 底模版本。 SD1.5 的 LoRA 放到 SDXL 上是出不来图的,症状是「全是噪点」或「完全不像」。 元信息里 ss_base_model_version 就是干这个用的。 ② dim 与 alpha。 这两个数决定改动量的缩放。 如果下载的 LoRA 说明里写 dim=128,而你按 dim=8 的直觉调权重,出来的效果会差很多。 ③ 触发词。 说明里应该有,而且要在提示词里出现。没有触发词的 LoRA,等于一个没有名字的开关。 再加一条工程习惯: 发布和接收都做一次哈希校验。Demo 里那个 SHA256 前 16 位就是给人核对用的—— 对不上说明文件传坏了或被人动过,别加载。 命名也值得较真:风格名_v1_r16_20261025.safetensors。 三个月后你有五个版本时,文件名是唯一的线索。

五、动手 Demo:LoRA 权重文件:头部 8 字节定长度,读完逐元素比对

不依赖任何训练框架,用 struct 和 json 手写一个 safetensors 文件:两个张量(8×512 与 512×8), 头部写清各自的 dtype、shape、起止偏移。然后用同样的规则读回来,逐元素比对,并算文件的 SHA256。

meta[name] = {u"dtype": u"F32", u"shape": list(arr.shape),
              u"data_offsets": [off, off + arr.nbytes]}
hb = json.dumps(meta, sort_keys=True).encode()
hb += b" " * ((8 - len(hb) % 8) % 8)      # 头部补齐到 8 的倍数

with open(p, "wb") as f:
    f.write(struct.pack("<Q", len(hb)))     # 前 8 字节:头部多长
    f.write(hb)                             # 再一段 JSON
    f.write(A.tobytes()); f.write(B.tobytes())   # 再裸二进制

n_head = struct.unpack("<Q", raw[:8])[0]
head = json.loads(raw[8:8 + n_head])
body = raw[8 + n_head:]

实跑输出:

--------------------------------------------------------------------
LoRA 权重文件:头部 8 字节定长度,读完逐元素比对
--------------------------------------------------------------------
  文件大小                 0.03 MB
  头部长度                 168 字节
  张量个数                 2 个
  SHA256 前 16 位          4d4e47527cedd070
  张量           形状   字节数   读回来
  ------------------------------------
  lora_unet_a   8×512    16384   一致
  lora_unet_b   512×8    16384   一致

  读回来 **2/2 一致**。这就是 safetensors 的全部秘密:
  头部 **8 字节**写长度,后面一段 **JSON** 写每个张量的起止位置,
  再后面就是裸的二进制。
  相比 pickle 格式(ckpt),它**不会在加载时执行任意代码**——这是它被几乎所有训练器默认采用的原因。
  那个 SHA256 是发布时要写进说明里的东西。**用户下载完先校验,对不上就说明文件传坏了或被人动过。**

  体积账:这个 512×512 的层,LoRA 两个矩阵一共 0.03 MB;换成整层全量微调要 1.00 MB。
  **所以一个画风 LoRA 常见 10 到 100 MB,而底模是 2 到 7 GB。**分发和版本管理都不是一个量级的事。

三行 kv 就是格式的全部:头部 168 字节、2 个张量、文件 0.03 MB。头部长度必须记在文件里, 否则读取时根本不知道 JSON 到哪结束。 struct.pack("<Q", len(hb)) 里的 <Q 是「小端、8 字节无符号整数」。 这 8 个字节就是整个格式的入口——所有 safetensors 读取器第一件事都是读它。 头部要补空格到 8 的倍数。 这不是为了好看:补到 8 的倍数后,数据区的起点也是 8 字节对齐的,np.frombuffer 直接就能用, 不用再拷贝。 读回来 2/2 一致,说明「写进去的字节」和「按偏移切出来的字节」完全对得上。 真实 LoRA 也只是几百个这样的张量拼在一起。 体积那句对比值得记住: 两个 512×512 的因子 0.03 MB,整层全量微调要 1.00 MB——差 32 倍。 这就是为什么网上的 LoRA 动辄几十 MB 而底模动辄几个 GB。

30 秒实验

  1. 把 dim 从 512 改成 768,重跑,看文件大小怎么变。 线性涨到 0.05 MB 左右——参数量与维度成正比,不是平方。
  2. 把「补齐到 8 的倍数」那行注释掉,看读回来的结果。 数据区不再对齐,np.frombuffer 会报错或读出乱码——对齐是硬要求。
  3. 改动 A 矩阵里的一个字节,重算 SHA256。 前 16 位会完全不同——这就是哈希能发现「文件被改过」的原因。

常见误区

  1. 「ckpt 和 safetensors 一样,只是后缀不同」。前者是 pickle,加载会执行代码;后者只有字节和偏移。
  2. 「文件小就不用校验」。恰恰相反——小文件更容易被悄悄替换,哈希是唯一的凭据。
  3. 「LoRA 放哪个底模上都行」。SD1.5 的 LoRA 放到 SDXL 上出不来图,症状是噪点,不是报错。
自测三问(点开对答案) 1. safetensors 的三段结构是什么? —— 8 字节头部长度 + 等长的 JSON(每个张量的 dtype、shape、起止偏移)+ 首尾相接的裸二进制。 2. 为什么不用 ckpt? —— ckpt 是 pickle,反序列化时会执行代码,恶意文件可以在加载时跑任意代码。 safetensors 只有数字和偏移,没有可执行内容。 3. 部署之后要核对哪三样? —— 底模版本、dim 与 alpha、触发词。 底模不对会出噪点,dim 不对权重手感全变,没有触发词等于没有开关。

文件放好了、元信息也核过了,最后一节是真正出图——那几个参数里,只有两个是必须自己定的。

相关笔记