☰
2080 Ti 老卡实战:ms-swift 结合 unsloth 微调 Qwen3-VL 多模态模型
2026/10/9 4:19:19 网站建设 项目流程

1. 为什么要在 2080 Ti 上折腾 ms-swift 加 unsloth

先把背景交代清楚。我手上这台机器是两张 2080 Ti,22GB 显存一张,图灵架构,算力 7.5,不支持 bf16,也没有 FlashAttention-2 的原生支持。这套配置放在今天做大模型微调,说实话属于“能跑但处处受限”的档位。而我想做的事情是拿 Qwen3-VL 这种带视觉能力的多模态模型做指令微调,同时用 ms-swift 这套国产训练框架来管理整个流程,再叠加 unsloth 的显存优化和加速能力。

这三个东西凑一起,核心矛盾就出来了:ms-swift 是阿里系魔搭社区主推的训练框架,封装度高、支持模型全、命令行友好;unsloth 是主打“显存砍半、速度翻倍”的轻量微调库,靠手写 Triton 内核和梯度检查点优化出名;Qwen3-VL 是通义千问的视觉语言模型,参数量不小,视觉编码器加语言模型叠起来显存开销很吓人。2080 Ti 又是老卡,很多新特性吃不到。所以这篇笔记要解决的就是:怎么让这三者在老卡上跑通,并且跑得尽量稳、尽量省显存。

适合谁看?如果你手里也是 20 系或者更老的卡,想跑多模态微调但被显存卡住;或者你已经在用 ms-swift,想接 unsloth 加速但不知道从哪下手;再或者你单纯想搞清楚 unsloth 到底改了哪些东西、为什么能省显存,这篇都能给你一个可复现的参考。我踩过的坑、试过的参数、最后跑通的配置,都会原样写出来。

需要提前说明的是,unsloth 官方对图灵架构(也就是 20 系)的支持是“部分支持”,它的很多优化内核是针对 Ampere 及以后的架构写的。所以 2080 Ti 上跑 unsloth,不能指望它宣传的那种极致加速,更多是图它的显存优化和梯度检查点策略。这一点心态要先摆正,不然跑起来发现速度没翻倍会失望。

2. 环境准备与依赖安装的取舍

2.1 驱动、CUDA 与 PyTorch 版本怎么定

2080 Ti 的驱动我锁在 535 版本,CUDA 用 12.1。为什么不升到更新的?因为图灵架构在新版 CUDA 上并没有额外收益,反而某些新 PyTorch 版本对 sm_75 的预编译轮子支持会变差。PyTorch 我选的是 2.3.1 配 cu121,这个组合在 20 系上验证得比较充分。

安装命令大概是这样:

pip install torch==2.3.1 torchvision==0.18.1 --index-url https://download.pytorch.org/whl/cu121

这里有个关键点:不要用 bf16。2080 Ti 不支持 bf16,如果你在训练配置里写了bf16=True,要么直接报错,要么悄悄退化成 fp32 导致显存爆炸。统一用fp16=True,这是图灵卡上唯一正确的半精度选择。

2.2 ms-swift 的安装方式选择

ms-swift 我建议从源码装,不要直接 pip。原因是它的迭代很快,pip 上的版本经常和最新文档对不上,而且接 unsloth 需要改一些内部逻辑,源码装方便你随时打补丁。

git clone https://github.com/modelscope/ms-swift.git cd ms-swift pip install -e .

装完之后验证一下:

swift --version

能打印出版本号就说明命令行工具就位了。ms-swift 的好处是它把训练、推理、导出、量化都整合到一套 CLI 里,配置文件用 yaml 或者直接命令行参数都行,对多模态模型的支持也比较完整。

2.3 unsloth 安装的坑与版本匹配

unsloth 的安装是这次最折腾的部分。官方推荐用 conda 环境隔离,因为它对 torch、xformers、triton 的版本要求很死。我试过直接 pip install unsloth,结果它自动拉了一个不匹配的 triton,跑起来内核编译直接崩。

正确的姿势是先建干净环境,再按它的依赖矩阵装:

conda create -n swift-unsloth python=3.10 -y conda activate swift-unsloth pip install unsloth

但注意,unsloth 默认会装它自己适配的 torch 版本。如果你已经装了 cu121 的 torch,它可能会覆盖掉。我的做法是先装 unsloth,看它装了什么 torch,再决定要不要手动对齐。实测下来,unsloth 对 torch 2.3.x 的支持是稳的,2.4 以上在图灵卡上偶发内核编译失败。

还有一个隐藏坑:unsloth 依赖xformers,而 xformers 对 20 系的注意力优化支持有限。装完之后要确认 xformers 能正常 import,否则训练时会 fallback 到普通注意力,速度会掉。

import xformers print(xformers.__version__)

如果这行报错,说明 xformers 和 torch 版本没对齐,得回去重装。

2.4 依赖冲突的排查思路

ms-swift 和 unsloth 都会依赖 transformers、peft、trl 这些库,但版本要求可能不一样。我的经验是:以 unsloth 的版本要求为准,然后让 ms-swift 去适配。因为 unsloth 的内核和 peft、transformers 的接口耦合更紧,改它更容易出问题。

具体做法是先装 unsloth,然后pip install -e .装 ms-swift 时如果报依赖冲突,用--no-deps跳过依赖检查,再手动补 ms-swift 需要但没装的包。这样能避免 pip 把 unsloth 的依赖降级。

3. Qwen3-VL 模型加载与显存账本

3.1 模型结构拆解与显存占用估算

Qwen3-VL 这类视觉语言模型,结构上分三块:视觉编码器(ViT 部分)、投影层(把视觉特征映射到语言模型维度)、语言模型主体。显存占用主要来自四部分:模型权重、梯度、优化器状态、激活值。

拿一个 7B 级别的 Qwen3-VL 举例,fp16 下权重约 14GB。如果全参数微调,梯度又是 14GB,Adam 优化器状态(一阶动量加二阶动量)是权重的两倍即 28GB,光这三项就 56GB,2080 Ti 的 22GB 根本放不下。所以必须走参数高效微调路线,也就是 LoRA 或者 QLoRA。

LoRA 只训练低秩适配矩阵,参数量通常只有原模型的百分之零点几到百分之几。这样梯度、优化器状态都只针对这部分小矩阵,显存开销骤降。QLoRA 更进一步,把基座模型量化成 4bit 加载,权重占用直接砍到约四分之一。

3.2 4bit 量化加载在 2080 Ti 上的可行性

QLoRA 的核心是 bitsandbytes 的 4bit 量化。2080 Ti 支持 INT4 计算吗?严格说它的 Tensor Core 对 INT4 没有专门加速,但 bitsandbytes 的 NF4 量化是在软件层面做的反量化,计算还是走 fp16,所以能跑,只是没有速度优势。

加载配置大概这样:

from transformers import BitsAndBytesConfig bnb_config = BitsAndBytesConfig( load_in_4bit=True, bnb_4bit_quant_type="nf4", bnb_4bit_compute_dtype=torch.float16, bnb_4bit_use_double_quant=True, )

bnb_4bit_use_double_quant=True是二次量化,能再省一点显存,代价是轻微的速度损失。在 2080 Ti 上我建议开着,因为显存太紧张了。

3.3 视觉编码器的显存陷阱

多模态模型有个容易被忽略的点:视觉编码器处理图像时产生的激活值可能非常大。尤其当输入图像分辨率高、或者一个 batch 里有多张图时,ViT 的中间激活会吃掉大量显存。

我的做法是冻结视觉编码器,只训练投影层和语言模型的 LoRA。这样视觉部分的梯度不用存,激活值也能通过梯度检查点进一步压缩。在 ms-swift 的配置里,可以通过指定freeze_vit=True或者只把 target_modules 设成语言模型的注意力层来实现。

如果你确实需要微调视觉编码器,那 2080 Ti 上基本只能把 batch size 压到 1,并且开梯度检查点,否则很容易 OOM。

4. ms-swift 接入 unsloth 的核心改造

4.1 两者结合的思路

ms-swift 本身支持 LoRA、QLoRA,也支持梯度检查点,但它默认的优化路径没有用 unsloth 的那套手写内核。unsloth 的价值在于它重写了 RMSNorm、RoPE、交叉熵损失等模块的 Triton 内核,并且对梯度检查点做了更激进的内存复用。

接入的核心思路是:让 ms-swift 负责数据加载、训练循环、多模态处理,让 unsloth 负责模型层的加速和显存优化。具体就是在模型加载阶段,用 unsloth 的FastLanguageModel.from_pretrained或者对应的多模态加载接口替换掉 ms-swift 默认的 transformers 加载。

4.2 关键代码改造点

ms-swift 的模型加载入口在它的model模块里。你需要找到它实例化模型的地方,把原来的AutoModelForCausalLM.from_pretrained换成 unsloth 的加载函数。对于多模态模型,unsloth 提供了FastVisionModel这类接口。

改造时要注意几点:unsloth 加载后会返回 model 和 tokenizer(或 processor),ms-swift 后续的 LoRA 注入、训练器构建都要基于这个返回的 model。LoRA 的注入建议用 unsloth 的get_peft_model,因为它对 LoRA 层的实现做了优化,和它的内核配合更好。

from unsloth import FastVisionModel model, processor = FastVisionModel.from_pretrained( model_name="Qwen/Qwen3-VL-7B", load_in_4bit=True, use_gradient_checkpointing="unsloth", ) model = FastVisionModel.get_peft_model( model, r=16, lora_alpha=32, target_modules=["q_proj", "k_proj", "v_proj", "o_proj"], )

use_gradient_checkpointing="unsloth"是 unsloth 的招牌,它比 transformers 原生的检查点策略省更多显存,因为它会智能判断哪些中间激活可以丢弃、哪些需要保留。

4.3 训练器与数据流的对接

ms-swift 有自己的 Trainer 封装,支持多模态数据的 collator。接入 unsloth 后,模型部分变了,但数据流可以基本沿用 ms-swift 的。需要注意的是 unsloth 对输入格式有要求,尤其是多模态的 image token 处理,要和 processor 对齐。

我的做法是保留 ms-swift 的数据集加载和 collator,只在模型前向部分走 unsloth 的路径。这样改动最小,也最不容易出错。如果 ms-swift 的 collator 输出的 pixel_values 格式和 unsloth 期望的不一致,需要在 collator 里做一次转换。

5. 2080 Ti 上的参数调优实战

5.1 batch size 与梯度累积的平衡

22GB 显存跑 7B 的 QLoRA 加多模态,batch size 基本只能设 1。这时候要靠梯度累积来凑等效 batch。比如per_device_train_batch_size=1,gradient_accumulation_steps=16,等效 batch 就是 16。

梯度累积的代价是训练变慢,因为每次累积都要做一次前向和反向但不更新参数。不过在显存受限的情况下这是唯一选择。我实测下来,累积步数设 8 到 16 比较合适,再大训练时间会明显拉长,而收敛收益递减。

5.2 序列长度与图像分辨率的取舍

多模态训练里,序列长度直接决定激活值大小。Qwen3-VL 处理一张图会展开成很多 visual token,图像分辨率越高 token 越多。2080 Ti 上我建议把max_seq_length控制在 2048 以内,图像分辨率限制在 448 或更低。

如果你发现 OOM,第一个要调的就是这两个参数。先把序列长度砍到 1024 试试,如果还 OOM,再降图像分辨率。这两个是显存占用的最大变量。

5.3 学习率与 LoRA 秩的选择

LoRA 的秩 r 我一般从 16 起步,任务复杂就加到 32 或 64。r 越大,可训练参数越多,显存和训练时间都会增加。2080 Ti 上 r=32 是比较舒服的平衡点。

学习率方面,QLoRA 通常用 1e-4 到 2e-4。我习惯用 2e-4 配 cosine 调度,warmup 比例 0.03。这个配置在多数指令微调任务上比较稳。如果 loss 震荡厉害,降到 1e-4。

5.4 优化器选择与显存关系

优化器状态是显存大户。AdamW 对每个可训练参数要存两个状态,LoRA 参数量小所以还好。但如果你想用 8bit 优化器进一步省显存,bitsandbytes 提供了AdamW8bit,能把优化器状态压到 8bit。

from bitsandbytes.optim import AdamW8bit optimizer = AdamW8bit(model.parameters(), lr=2e-4)

在 2080 Ti 上我建议开这个,省下来的显存可以留给更大的序列长度或者稍大的 batch。

6. 常见报错与排查速查

6.1 典型问题对照表

报错信息可能原因解决方向
CUDA out of memorybatch/序列/分辨率过大降 batch、砍序列、降分辨率
triton compile errortriton 版本不匹配重装匹配 unsloth 的 triton
bf16 not supported图灵卡不支持 bf16改用 fp16
xformers import failxformers 与 torch 版本错位重装对齐版本
dtype mismatch模型与输入精度不一致统一 fp16
NCCL timeout多卡通信问题检查卡间通信、降 batch

6.2 几个我踩过的具体坑

第一个坑是 unsloth 的梯度检查点在某些层上会报inplace operation错误。这是因为它的内核做了原地操作,和某些 PyTorch 版本的自动微分冲突。解决办法是升级或降级 PyTorch 到它验证过的版本,我最后锁在 2.3.1 就没再出现。

第二个坑是 ms-swift 的 collator 默认会把图像 resize 到一个固定尺寸,而 unsloth 的 processor 期望原始尺寸或它自己的处理逻辑。两者不一致会导致 visual token 数量对不上,报维度错误。解决方法是统一用 unsloth 的 processor 处理图像,ms-swift 那边只负责文本部分。

第三个坑是 4bit 量化加载后,某些层的 dtype 会变成 float32,导致显存比预期高。这是 bitsandbytes 的已知行为,需要在加载后手动把非量化层转回 fp16。

6.3 显存监控与调优节奏

训练时用nvidia-smi -l 1实时看显存。如果发现显存缓慢上涨最后 OOM,多半是激活值没释放干净,检查梯度检查点是否真的生效。如果一开始就 OOM,那是静态占用太大,得从模型加载和序列长度下手。

我的调优节奏是:先保证能跑起来(最小 batch、最短序列),再逐步加参数直到接近显存上限,留 1 到 2GB 余量防止波动 OOM。

7. 训练效果与性能实测

7.1 速度与显存的实际数字

在 2080 Ti 单卡上,7B Qwen3-VL 的 QLoRA 微调,序列长度 2048,batch size 1,梯度累积 16,实测每步(等效 batch 16)大约 8 到 12 秒。显存峰值在 18 到 20GB 之间,留了一点余量。

对比不用 unsloth 的纯 ms-swift QLoRA,显存大概能省 15% 到 20%,速度提升不明显,因为图灵卡吃不到它的内核加速。所以再次强调,20 系上接 unsloth 主要图显存,不是图速度。

7.2 收敛表现与注意事项

LoRA 微调通常几百到几千步就能看到 loss 明显下降。我这次任务大概 500 步 loss 从 2.3 降到 0.8 左右,1000 步后趋于平缓。多模态任务要注意视觉部分如果冻结了,loss 下降主要来自语言部分,如果任务强依赖视觉理解,可能需要解冻部分视觉层。

训练完导出 LoRA 权重,可以用 ms-swift 的导出命令合并到基座,也可以保持分离。合并后推理更方便,但会失去 LoRA 的可插拔性。

8. 一些个人体会和后续可扩展方向

这套组合跑下来,最大的感受是:老卡做多模态微调,核心矛盾永远是显存,所有优化手段都要围绕省显存来。unsloth 的梯度检查点和 4bit 量化是省显存的两把利器,ms-swift 的多模态数据流和训练管理省了我大量写胶水代码的时间。两者结合虽然要改一些加载逻辑,但改完之后整个流程是顺的。

后续我打算试的方向有几个:一是把视觉编码器也做 LoRA,看能不能在 2080 Ti 上挤出空间;二是试试更激进的序列打包(packing),把多条短样本拼成一条长序列提高利用率;三是看看能不能用 ms-swift 的分布式能力把两张 2080 Ti 都用上,虽然图灵卡不支持 NVLink,但走 PCIe 做数据并行应该可行。

如果你也在老卡上折腾多模态微调,我的建议是先把最小可跑通的配置搭起来,别一上来就追求大 batch 长序列,跑通之后再一点点加参数。显存这东西,永远是先保证不 OOM,再谈效率。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询