Wan2.1,也就是通义万相2.1这套开源权重,发布之后热度一直没降。我用它做了一阵子风格化生成,底模能力确实在线,但很快发现一个问题:我想要稳定的产品拍摄风格、固定的人物外观,或者某一种贯穿全片的色调习惯,光靠改提示词根本压不住。换几次种子,风格就飘了。LoRA训练是我认为目前唯一务实的选择——只训练一个低秩旁路模块,把底模原有权重冻结住,让模型学会一种新概念或画风,还不把原有能力冲掉。
这篇内容完全是围绕通义万相Wan2.1的LoRA训练和推理整理的:从原理、环境、数据格式、触发词,到训练参数、显存处理、推理挂载,以及我实战里踩过的坑都会写到。适合那些已经跑通了Wan2.1基础推理、想进一步做风格定制或角色定制的人,也适合刚从Stable Diffusion转过来、对扩散模型LoRA流程还不熟的朋友。我会尽量把每个选择背后的原因讲清楚,而不是直接丢一套参数让你无脑抄。
1. 开工前先把Wan2.1的LoRA原理掰开:训的到底是哪个部分
很多人拿到训练脚本就开始灌数据,结果效果不对,回头怀疑数据集和超参,其实连LoRA作用在模型哪个部位都没搞明白。这一步想清楚,后面排查问题会省一大半时间。
Wan2.1的生成主干属于扩散模型体系,和Stable Diffusion时代的大思路一致:文本先经过编码器变成条件向量,扩散主干在去噪的每一步读取条件向量并预测噪声,反复迭代后才得到干净的图像或视频帧。主干模型里参数占比最大、也是最影响生成效果的部分,是注意力模块。LoRA的做法就是不碰原始权重,在注意力层的QKV投影旁边各接一条低秩旁路矩阵,训练时只更新旁路,参数规模通常会砍到原始模型的1%上下。
这样做的好处非常直接。第一是显存门槛低,底模被冻结后,训练时的主要开销集中在低秩矩阵反向传播和优化器状态上,而不是整份模型权重;第二是抗遗忘,底模原有的泛化能力不会被破坏,LoRA学到的只是一个增量;第三是可组合,只要你训练时用的底模一致,推理时可以同时挂多个LoRA、随时换,不用重新训练。
我也被问过能不能直接用LLaMA Factory这类一站式大模型微调平台来训Wan2.1。这里要泼一盆冷水:LLaMA Factory针对的是自回归语言模型,训练目标是预测下一个token;Wan2.1是扩散生成模型,训练目标是噪声预测。虽然LoRA的数学原理两边通用,但工具链完全不一样,硬套会卡在数据格式和模型结构适配那里。老老实实基于Diffusers家族的脚本,或者官方仓库提供的训练脚本去改,会顺很多。
这个原理认知,直接决定了你后面遇到效果问题时怎么定位。比如训练出来的LoRA放到推理里效果很弱,你得先判断是rank不够还是触发词没生效;效果过强导致画面崩坏,你得考虑alpha和训练步数,而不是怀疑底模坏了。心里有这张“地图”,调参才不会是盲人摸象。
2. 数据、环境和触发词:这三样不准备好,超参再好也白搭
2.1 环境与显存预算的底线
Wan2.1有不同规模的权重分支,对显存的要求差距很大。一个大致划分如下:
| 分支规模 | 训练建议 | 推理建议 |
|---|---|---|
| 轻量分支(1.3B级) | 单张24GB就能启动,开启gradient checkpointing | 12GB可以流畅跑 |
| 中等分支(5B级左右) | 建议40GB以上,或双卡分摊 | 24GB起步比较舒服 |
| 14B级的大分支 | 建议多卡,或配8bit优化器加offload | 40GB以上才不憋屈 |
这只是起点,实际还取决于分辨率、batch size、rank、是否做训练中评估。一个真正稳妥的顺序是:先在目标显卡上把底模跑通一次推理,确认权重路径、CUDA版本、tokenizer这些底层组件没问题,再往上叠训练流程。很多朋友一上来就跑训练脚本,报错之后分不清是环境问题还是脚本问题,排查成本翻倍。
软件依赖其实不复杂:Python 3.10、新一点的PyTorch、diffusers、peft、accelerate、safetensors,基本就够。我强烈建议训练前先跑一次accelerate config,把分布式后端、混合精度都配置好,不然后面调试单卡脚本会很不顺手。
2.2 训练数据的分辨率、数量和多样性到底怎么平衡
数据质量大于数据数量,这个道理在Wan2.1上照样成立。但如果要给个起点,纯风格LoRA最少准备50张高质量图,角色或固定物体建议200张以上,因为要跨视角、跨光照泛化。视频类的LoRA如果条件允许,直接剪短视频片段参加训练,比用单帧图更有效。
我推荐把数据集整理成JSONL格式,一行一条样本,出问题方便定位:
{"file": "./train/001.jpg", "text": "skystyle product shot, product on white background"} {"file": "./train/002.jpg", "text": "skystyle product shot, product on wood table"}如果训练脚本用的是JSON数组,写法也差不多,核心字段就是图片路径和提示词。提示词里触发词放在开头,后面跟内容描述,这个顺序会影响模型对语义权重的分配,尽量稳定统一。数据预处理必须做三件事:把图缩放到训练脚本预设的分辨率附近,再用随机裁剪增强;去掉多余边框和留白;检查有没有明显水印和压缩伪影。低分辨率图直接丢进去被放大到1024训练,画面会糊成一片,这锅得让数据背。
多样性的重要性经常被低估。你想让LoRA学会“某种场景风格”,但训练集里全是摄影棚白底图,那模型学到的其实是“室内影棚”这个环境,而不是风格本身。各个光照条件、背景、焦段的素材比例最好均衡,至少别让某一类占绝对主导。
2.3 触发词:中文还是英文,怎么选才稳
触发词是LoRA的语义把手。很多人问,中文触发词行不行?直接给结论:英文优先,问题本质在于训练和推理必须完全一致。
Wan2.1的文本编码器对中英文都能编码,但中文词语在分词器里经常被拆成更细的token。训练时你写的中文触发词如果被拆成了几个token,推理时也必须使用完全相同的token组合才能触发训练效果。不同脚本对中文预处理的细节稍有差异,稍微一变,触发词就失效了。英文单词和词组在这方面稳定很多,所以我一般建议用一段不在常用词汇表里的组合词,比如品牌缩写加版本号,看起来奇怪没关系,越不常见越不容易被提示词里的其他语义干扰。
触发词一个到两个就够,多了会互相稀释。还有一个小技巧:训练集里大约20%的样本不要带触发词,只写内容描述,让模型不要把触发词和“所有图片”强绑定,这样能明显降低过拟合风险。
3. 训练脚本与超参调节:从能跑通到效果达标的路子
3.1 训练脚本怎么搭
官方仓库会提供Wan2.1的基础推理代码,训练部分一般兼容Diffusers的训练风格。你可以基于diffusers的text2image LoRA示例去改,也可以用社区已经适配好Wan2.1的脚本。核心流程绕不开这几步:加载底模和tokenizer、给扩散主干挂LoraConfig、冻结非LoRA参数、加载数据集、循环做噪声预测和梯度更新。
一个简化的核心流程参考:
import torch from diffusers import DiffusionPipeline from peft import LoraConfig, get_peft_model model_id = "your_wan2_1_base_path" pipe = DiffusionPipeline.from_pretrained( model_id, torch_dtype=torch.bfloat16 ) transformer = pipe.transformer if hasattr(pipe, "transformer") else pipe.unet lora_config = LoraConfig( r=64, lora_alpha=32, target_modules=["to_q", "to_k", "to_v", "to_out.0"], ) transformer = get_peft_model(transformer, lora_config) transformer.train() for name, param in transformer.named_parameters(): if "lora_" not in name: param.requires_grad = False注意这只是一个结构示意,目标模块名要以你拿到的主干结构为准,有的版本用transformer,有的用unet。任何脚本拿过来,第一件事不是跑,而是先打印模型结构,确认你要挂LoRA的模块名到底叫什么。我见过太多人照着SD的教程把target_modules写错,然后训练了一整晚,LoRA根本没挂上去。
3.2 一套能跑通的默认超参,以及背后的调节逻辑
我的起点参数如下,后面跟着调节思路:
| 参数 | 起始值 | 调节方向 |
|---|---|---|
| rank | 64,风格可降到32,角色往128走 | 太小学不会,太大容易过拟合 |
| lora_alpha | rank的一半 | 控制LoRA对结果的影响强度 |
| learning_rate | 1e-4 | 小批量时降到5e-5,出图脏就降 |
| batch size | 1 | 显存允许再往上加,不必强求 |
| epochs | 15(风格)/ 30(角色) | 看验证图和loss曲线综合判断 |
| optimizer | AdamW 8-bit | 比普通AdamW省不少显存 |
| lr scheduler | cosine | 收敛稳定,不用频繁调 |
| 精度 | bf16 | 在Ampere以上架构比fp16更省心 |
batch size我建议从1开始。LoRA参数本来就少,小batch不代表效果差,反而能在有限显存里尝试更高分辨率。等流程稳定了再往上加batch,效果有提升就留下,没提升也不必迷信“大batch”。优化器方面,8-bit AdamW基本是标配,传统AdamW的优化器状态对显存的占用很凶,换8-bit之后能腾出不少空间。bf16优先于fp16,因为bf16不需要动态loss scaling,训练稳定性更好。
3.3 训练过程别死盯loss,要盯验证图
只看训练loss是LoRA训练里最大的认知陷阱。loss下降只代表噪声预测误差变小,不代表风格学对了。最稳的做法是每几百步保存一次checkpoint,然后用一组固定的测试提示词跑推理,用眼睛判断效果。我的测试提示词固定三组:纯触发词触发、触发词加场景描述、不带触发词的对照。三组结果一对比,很快能判断LoRA强度是否合适、是否过拟合。
训练中途如果出现颜色崩坏、构图崩坏,先别急着加数据,把学习率调低一半重来一版。DiT这类模型对学习率比较敏感,学习率过大的症状很有迷惑性:loss看着在降,出图却越来越脏。这种时候不是epoch不够,是梯度更新步子迈太大。
4. 训练中段最容易翻车的显存问题:评估爆掉和提速取舍
4.1 为什么评估阶段容易把显存顶爆
这个现象很多训练过LoRA的人应该都撞见过:训练loss一路正常,到设置好的评估点,突然OOM,整个进程崩掉。原因并不复杂:评估阶段,脚本往往在同一个CUDA上下文里调用推理生成验证图,而此时训练循环的激活值和优化器状态还留在显存里,两者叠加,峰值自然超出。
热词里那个“unsloth训练LoRA时评估占满显存”的问题,虽然发生在LLM微调场景,但根因一模一样:训练和评估共享了显存上下文。思路完全可以直接平移过来用。
4.2 我的三种降峰值方案
第一种,把评估频率降到极低,并在评估前主动释放显存。先用torch.cuda.empty_cache(),再把优化器状态临时释放到CPU,效果有限但代码简单。适合只是想偶尔看一眼效果的场景。
第二种,把评估和训练拆成两个进程。训练存完checkpoint后,用subprocess启动一个全新的推理脚本来做验证,新进程加载模型时只保留推理所需的部分,和训练进程彻底隔离。这个方案最稳,缺点是多写十几行代码,但对长期跑大规模训练的人来说非常值得。
第三种比较简单粗暴:评估时降低推理负载。用更少的采样步数、更低的分辨率、单图生成,不并行跑多张。如果这样还爆,干脆取消训练中评估,全部训练结束之后再手动测试多个checkpoint。
我个人最推荐第二种方案。训练中评估是一个锦上添花的需求,没必要为了它把整个训练的稳定性搭进去。拆进程之后,不只显存不炸,训练节奏也不会被推理过程卡住。
4.3 提速的细节:文本嵌入缓存和gradient checkpointing
预计算文本嵌入是提速效果最明显的一步。LoRA训练时文本编码器保持冻结,但每个iteration仍然要把提示词完整过一遍编码器,这对算力是一种浪费。很多工具支持先把所有样本的文本嵌入算好存成文件,训练时直接读嵌入向量,省掉反复编码的耗时,速度提升非常明显。
显存紧张的话,gradient checkpointing一定要打开,它用少量额外计算换显存空间,属于经典取舍。数据加载方面,能全量加载到内存就全量加载,不要让磁盘IO成为训练瓶颈。多卡用户还要检查数据采样的随机种子是否一致,否则每张卡都在看同一批数据,多卡并行就成了无效劳动。
5. 推理阶段:挂载方式、合并权重和conf参数的真相
5.1 调试阶段的动态挂载
训练完的LoRA一般是一个adapter目录,里面包含adapter_model.safetensors和配置文件;也有项目输出单文件的safetensors。调试阶段推荐用动态挂载,底模文件不用动,随时能换LoRA来试效果:
import torch from diffusers import DiffusionPipeline pipe = DiffusionPipeline.from_pretrained( "your_wan2_1_base_path", torch_dtype=torch.bfloat16, variant="bf16", ).to("cuda") pipe.load_lora_weights("./wan21_style_lora", adapter_name="style") pipe.set_adapters(["style"], adapter_weights=[0.8]) image = pipe( "skystyle product shot, white background", num_inference_steps=28, guidance_scale=5.5, generator=torch.Generator("cuda").manual_seed(42), ).images[0] image.save("out.png")adapter_weights就是LoRA强度,0.8是个比较均衡的起点。强度太低等于没挂,太高会让画面出现同质化或明显伪影。如果挂多个adapter,可以给它们不同的权重组合,比如风格0.7、角色0.9,这种灵活的强度调节就是动态挂载的好处。
5.2 部署阶段的权重合并
做在线推理,或者想把工作流丢给别人复用的时候,我倾向于把LoRA直接合并进底模。好处是省心,不怕导出时忘了带adapter文件,也减少了推理时的加载步骤。
合并方式有两种。一是用官方的merge脚本,输入底模路径、LoRA路径、合并比例,输出一个完整权重目录;二是在Diffusers里手动把LoRA权重按比例累加到原始权重上,再执行save_pretrained。合并比例和前面的adapter_weights含义一致,建议先在动态挂载模式下调好合适的强度,再执行合并。合并之后的LoRA不能再按需调整强度,这点要提前想清楚。
5.3 推理参数:cfg_scale、steps、seed,以及那个“conf”到底是什么
热词里一直有人问“模型训练出来之后那个推理用的conf参数是什么”,这里必须分场景讲清楚。如果你跑的是目标检测项目,YOLO系列的conf就是置信度阈值,低于阈值的检测框会被过滤掉,这和生成模型完全是两码事。
在Wan2.1这类扩散模型的推理脚本里,如果出现了conf,通常它是classifier-free guidance的缩写变体,你真正要调的其实是guidance_scale,也就是CFG强度。CFG控制生成结果对提示词的服从程度,我一般取5到7。如果LoRA风格不够明显,可以稍微往上调;反过来如果画面太“用力”、颜色过饱和,就往下降。需要说明的是,部分基于检测或检索辅助的推理工具里,conf也可能被用作生成结果的筛选阈值,具体含义要看脚本注释。遇到这种情况先--help看一眼,别照抄别人的参数。
采样步数20到28是常见区间,再往上收益很小还拖慢速度。种子固定下来,才能保证一组对比里只有一个变量。第一次调参建议固定两个种子做交叉验证,减少随机因素干扰。
6. Wan2.1 LoRA高频翻车场景与排查清单
我不直接给检查表,把我实际翻车的那几次过程写出来,大家对照着看。
触发词失效,最常见的原因是粗心。我在训练时提示词写的是“skystyle, product photo”,推理时写成了“sky style product photo”,多了一个空格。在CLIP这类编码器里,空格会直接改变token拆分结果,训练时的触发条件在推理时根本对不上。解决办法不是靠猜,而是把训练数据里的caption原样复制到推理提示词里,一个字、一个空格都不改。中文触发词同理,训练和推理必须保证同一个分词结果,一旦脚本对中文的预处理不一致,失败是无声无息的。
过拟合也有一个经典误区:看loss。LoRA训练loss低,不代表风格留住了,反而可能是把训练集里的每张图都背了下来。典型表现是推理时触发词一出现,出图里总有训练集某几张图的身影,构图或色彩雷同。这时候我一般做三件事:提高正则化数据比例、降低rank、减少epoch。多数情况减少epoch最有效,LoRA学到一定阶段后再继续练,只是加深记忆,不会增强泛化。
版本错乱同样常见。Wan2.1的多分支、不同精度权重之间不能混用。你用14B底模训出来的LoRA,推理时想挂到1.3B上,几乎必然报错或者效果全无。下载权重时把repo id和commit hash记下来,写进训练配置,这种问题排查起来最浪费时间,预防成本却最低。
6.1 一份可以对照的检查清单
- 训练和推理使用的底模repo id、分支、精度完全一致
- 触发词直接从数据集的caption里复制,不手动重打,不改空格和大小写
- rank、alpha、batch size写死在配置里,不依赖命令行默认值
- 每500步保存一次checkpoint,文件名带步数标记
- 推理时加载的LoRA是最终适配文件,不是中间碎文件
- 显存紧张时不做训练中评估,全部训完再统一验证
- 合并权重前先记录好动态挂载时验证过的强度,避免合并后无法回退
训练LoRA这件事,最忌讳同时改多个变量。如果你训练效果不对,一会儿调rank、一会儿改数据集、一会儿换触发词,最后出了问题根本不知道是哪一步导致的。我的习惯是每次只动一个变量,跑一版对比一版,看起来很慢,但总时间反而最少。
如果你只是一味追求rank大、epoch多,大概率会收到一张过拟合的“画皮”,而不是一个能泛化的LoRA。我自己的经验是,Wan2.1底模本身已经很强,LoRA只是在上层做一个“倾向性引导”,所以训练资源不用堆得太猛,反而要在数据质量和触发词一致性上多花精力。这个思路从我把LoRA挂到不同场景下测试以来的体验都是成立的,希望也能帮你少走一些弯路。