标题里那句 on your device,真正扎人的地方不是 on,而是 your。8x7B 这个规格在论文和榜单里看着很友好,稀疏激活、单 token 只走两个专家,好像一块消费级显卡就能伺候。真到本地拉起训练脚本的那一刻,你会发现显存占用跟“只激活 12.9B”几乎没关系——底座权重是按 46.7B 全量躺在显存里的。我在一台 24GB 显存的机器上第一次尝试微调它,模型加载到 78% 就原地 OOM,日志里连一行训练信息都没打印出来。这篇内容就是把这中间的所有账算给你看:显存到底花在哪、哪条技术路线在消费级设备上真的能跑、超参怎么定、跑起来之后会撞上哪些坑。适合手里有一到两张消费级卡、或者一台大内存统一内存机器的读者,也适合那些已经跑通 7B 微调、想往上够一级但还没摸清 MoE 脾气的人。全文给的都是可以直接抄的配置和踩过的坑,不讲“前景广阔”那套。
1. 8x7B 的显存账本:稀疏激活省的是算力,不是显存
1.1 46.7B 参数与 12.9B 激活之间的落差
先把这两个数字摆清楚:这个模型的总参数量约 46.7B,每个 token 实际参与计算的激活参数量约 12.9B。前者决定它要占多少存储,后者决定它算得有多快。这两件事在工程上完全是两条线,但很多人会下意识把它们混成一件事。
打个生活化的比方:一家医院有八个专科科室,每个病人挂号时只会被分到其中两个科室去会诊。病人等待的时间确实短了,因为不用八个科室全跑一遍;但医院该建的八个科室、该养的八个科室的医生,一个都不能少。MoE 的稀疏激活就是这个逻辑——路由网络(router)对每个 token 打分,取 top-2 的专家做前向,计算量按 12.9B 走,但八个专家的权重必须全部常驻,一分显存都省不掉。
所以当你在代码里写下加载模型的这几行时,显存分配器看到的是 46.7B 个参数,而不是 12.9B。这个认知差是后面所有 OOM 的根源。我在最开始规划硬件的时候,按 12.9B 估了个 26GB 的 fp16 预算,觉得一张 24GB 卡挤一挤能成,结果连量化后的底座都装得磕磕绊绊。
1.2 四笔显存开销逐项拆解
真正做预算时不能只算权重,训练态下的显存由四块构成,每一块的增长逻辑都不一样。我习惯在动手前先列一张表,把四笔账分别估出来,加总后再乘 1.3 的安全系数,剩下的才是你能用的空间。
| 开销项 | 计算方式 | 全参微调(fp16) | 4bit 底座 + LoRA |
|---|---|---|---|
| 模型权重 | 参数量 × 每参数字节 | 46.7B × 2 ≈ 93GB | 46.7B × 0.5 ≈ 23.4GB |
| 梯度 | 仅可训练参数 × 字节 | 93GB | 适配器参数量 × 2,通常 < 1GB |
| 优化器状态 | 可训练参数 × 8~12 字节 | 370GB 以上 | 适配器参数量 × 12,约 3GB 量级 |
| 激活值 | 层数 × 序列长 × 批大小 × 隐层宽 × 系数 | 极大 | 开梯度检查点后可控在数 GB |
表里最容易被低估的是激活值。它不像权重那样有个固定的数,而是跟着序列长度和批大小线性涨。同一份配置,序列长度从 1024 拉到 2048,激活显存差不多翻倍。我见过有人把 max_seq_length 调到 4096 想塞长文档,结果权重加载得好好的,第一个前向就直接爆掉,就是因为激活那块没留余量。
还有一个隐藏开销:临时缓冲与碎片。4bit 量化在计算时需要把权重反量化回 bf16 做矩阵乘,这个反量化是分块进行的,会产生瞬时峰值。PyTorch 的缓存分配器会把这些块留着复用,于是你会发现 nvidia-smi 显示占用比你理论算的高出两三个 GB。这部分没法精确预估,只能靠预留余量扛。
1.3 不同硬件路线的最低配置表
“on your device”这句话落到具体硬件上,差别非常大。我把常见的几条路线列一下,都是我实际跑过或者帮人调过的配置,结论是:24GB 单卡是这条线的门槛,但不是舒适线。
| 硬件形态 | 可行性 | 关键约束 | 实操建议 |
|---|---|---|---|
| 单卡 24GB | 勉强可行 | 4bit 底座约 23.4GB,几乎无余量 | 必须用 max_memory 把少数层卸载到内存,序列长度压到 1024 |
| 双卡 24GB | 可行 | 层切分带来跨卡通信开销 | device_map 自动切分能跑,但吞吐不如单卡 48GB |
| 单卡 48GB | 舒适 | 可留出激活与优化器空间 | 4bit 底座 + 序列 2048 + 批 2,比较从容 |
| 单卡 80GB | 宽裕 | 可尝试 bf16 底座 + LoRA | 不开量化,精度损失最小,速度也快 |
| 统一内存 64GB 以上 | 可行但慢 | 依赖 CPU/GPU 共享内存带宽 | 训练吞吐大概只有独显的三到五分之一,适合小数据量微调 |
| 纯 CPU | 不推荐 | 无矩阵加速单元 | 只适合做数据预处理和最后的格式转换 |
这里要特别说一下统一内存那条路线。很多人以为内存大就能训,实际上瓶颈在带宽:4bit 反量化本身是访存密集型操作,共享内存的带宽撑不住,训练一步要等十几秒。我自己在某台 64GB 统一内存的机器上试过跑 200 步,测下来吞吐只有独显方案的零头,做实验验证想法可以,正经跑一轮不划算。
2. 三条技术路线的取舍:全参、LoRA、QLoRA
2.1 全参微调为什么在本地基本不可行
先把这个选项排除掉,省得有人抱着侥幸心理试。全参微调的可训练参数是 46.7B,fp16 权重 93GB,梯度 93GB,AdamW 优化器状态按 12 字节每参数算超过 550GB,加起来接近 750GB 显存。就算上 ZeRO-3 分片加 CPU 卸载,也需要多张高显存卡加超大内存的机器,这已经脱离“on your device”的范畴了。
而且就算硬件够了,全参微调这个模型还有一个隐蔽风险:路由塌缩。MoE 的专家是靠路由网络分配的,全参微调时路由层和专家层一起更新,如果数据分布过于集中,很容易出现所有 token 都涌向同两个专家的情况,剩下六个专家变成僵尸参数。这时候你看到 loss 降得很漂亮,但模型的泛化能力已经被破坏了。所以我从一开始就没打算走这条路,直接把范围收窄到参数高效微调。
2.2 4bit 双重量化省下来的到底是哪几个 GB
量化这件事,很多人只知道“4bit 省显存”,但不知道省在哪、代价在哪。按 4bit 存的底座,理论上是 46.7B × 0.5 字节 ≈ 23.4GB,从 93GB 压到 23.4GB,主要省在权重这一块。这里面还有个细节:双重量化。
普通的 4bit 量化是每 64 个参数一组,存一个 fp32 的缩放常数。46.7B 参数分成 7 亿多组,每组 4 字节,光这些常数就要 2.9GB 左右。双重量化是把这些常数本身再用 8bit 存一遍,每参数平均省下大约 0.37 个比特,换算到这个规模大概能省 2GB 出头。这个数值听起来不多,但在 24GB 卡上,2GB 往往就是“能加载”和“加载失败”的差别。我在自己的配置里一直开着这个开关,没关过。
真正的代价在计算精度。4bit 权重在做矩阵乘之前会被反量化成 bf16,这个反量化过程不可逆,会引入量化误差。好在这个误差在微调场景下影响有限——你只是用它当底座去学一个新任务,不是要求它复现原始精度。但如果你的任务是高度依赖细粒度数值推理的(比如数学计算、精度敏感的结构化抽取),量化底座的上限会比 bf16 底座明显低一截。我建议这类任务如果有条件,宁可上 80GB 卡跑 bf16 底座,也不要硬压到 4bit。
2.3 适配器该挂在注意力上还是专家层上
这是这个模型微调里最有意思的一个选择题,也是它跟普通稠密模型不一样的地方。普通的 LoRA 只需要决定挂不挂 q/k/v/o,MoE 还多了一层:八个专家的 MLP 要不要也挂上。
我实测过两种配置的参数规模和表现:
| 适配器范围 | 可训练参数(r=16) | 显存增量 | 适用场景 |
|---|---|---|---|
| 仅注意力 q/k/v/o | 约 13.6M | 很小,1GB 以内 | 风格迁移、格式对齐、指令跟随 |
| 注意力 + 专家 w1/w2/w3 | 约 240M | 优化器状态约 3GB | 需要注入新知识、领域术语密集 |
| 仅专家层 | 约 226M | 约 3GB | 实验用,实际很少单用 |
(专家层的三个线性层在实现里叫 w1、w2、w3,w1 和 w3 是门控与升维,w2 是降维,三个都要挂才完整。)
我的经验是:先用仅注意力的轻配置跑一轮,看效果够不够。如果只是让模型学会一种输出格式、一种回答风格,注意力层就够了,参数少、训得快、不容易过拟合。只有当你的数据里塞了大量模型原本不知道的领域知识时,才值得把专家层也打开。
这里还有个坑要点出来:不要把路由层(gate)放进 target_modules。路由层输出的是专家选择分数,对它做低秩扰动会直接改变 token 的分配路径,训练早期很容易出现路由震荡,loss 曲线抖得没法看。我在第一次配置时图省事,把所有线性层一起挂上了,结果前 300 步 loss 上下乱跳,排查了大半天才定位到是路由层被改了。
3. 环境搭建与版本对齐:最容易浪费一整天的地方
3.1 一份能跑通的版本组合
这类大模型微调的环境,坑不在装不上,而在装上了但版本之间互相不认。我最开始图省事用了默认的最新版,结果 transformers 里模型结构的类名跟 PEFT 期望的对不上,报了个 KeyError,翻源码才明白是版本错配。
下面这组配置是我目前验证过能稳定跑通的最小组合,建议直接锁版本,别追新:
| 组件 | 版本基线 | 说明 |
|---|---|---|
| Python | 3.10 / 3.11 | 3.12 上部分编译型依赖轮子还不全 |
| PyTorch | 2.1 ~ 2.3,配套 CUDA 12.1 | 跟驱动对应,别跨大版本 |
| transformers | 4.36 以上 | 低于这个版本不认识这个模型的结构 |
| peft | 0.7 以上 | LoRA 与量化底座结合的关键版本 |
| bitsandbytes | 0.41.3 以上 | 4bit 量化后端 |
| accelerate | 0.25 以上 | device_map 与多卡调度 |
| trl | 0.7.4 以上 | 封装的 SFT 训练器,省不少样板代码 |
| datasets | 任意近期版本 | 数据处理 |
安装完之后先跑一次自检,别急着加载模型:确认torch.cuda.is_available()为真,确认 bitsandbytes 能成功导入并且识别到显卡,确认能打印出一张卡的总显存。这三件事任意一件失败,后面全是白费功夫。
3.2 三个高频报错的定位顺序
我把撞过的报错按“出现频率”和“排查难度”排了个序,遇到问题按这个顺序看,能省下大量时间。
第一类是“找不到可用的加速后端”。报错文本里通常带 CUDA setup failed 之类的关键字。九成原因是 PyTorch 的 CUDA 版本和 bitsandbytes 编译时的版本对不上,或者装的是 CPU 版轮子。定位方法很简单:打印 torch 的 CUDA 版本,再打印 bitsandbytes 的版本信息,两者不匹配就重装对应的轮子,别试图打补丁。
第二类是“没有 padding token”。这个模型的词表里默认没有 pad token,当你设置批大小大于 1 时会直接抛异常。修复只需要一行:把 pad_token 设成 unk_token 或者 eos_token。但我建议同时把padding_side显式设成 "right",训练时右填充配合 label 屏蔽更符合直觉,出事也好查。
第三类是“张量不需要梯度”。典型场景是你冻结了底座、开了梯度检查点,然后训练时梯度传不回去。这是因为梯度检查点要求输入张量带 requires_grad。解决办法是在prepare_model_for_kbit_training时把use_gradient_checkpointing打开,它会帮你把输入嵌入层的梯度开关也打开。我第一版脚本是手动调用的 enable,漏了这一步,卡了大概四十分钟。
3.3 单卡 device_map 与多卡 ZeRO-3 的边界
单卡场景下用device_map="auto"加载是没问题的,训练器能正常迭代。但如果底座装不进单卡,auto会把部分层分到内存(也就是 CPU 卸载),这时你会看到显存占用降下来了,但每步耗时暴涨,因为每层前向都要在内存和显存之间搬数据。
多卡场景就要换思路了。直接用 DDP 跑这个量化模型是不行的,量化权重在进程间同步会出问题。可行的方案是用分布式并行策略做分片,把参数、梯度、优化器状态切到多张卡上,必要的时候再开 CPU 卸载。代价是配置复杂度上一个台阶,而且层与层之间的通信开销会吃掉一部分加速收益。
我的建议是:能单卡就别上多卡。双 24GB 的吞吐往往还不如单张 48GB,跨卡带宽是硬伤。如果非要多卡,先把数据量和序列长度压下来跑通单卡流程,再迁移到并行配置,否则你会在“到底是环境问题还是并行配置问题”之间反复怀疑人生。
4. 数据工程:决定效果上限的不是超参
4.1 对话模板对齐与特殊 token 处理
这个模型训练时的对话模板是 [INST] 指令 [/INST] 回复 的结构,序列用起始符开头、结束符收尾。这套格式看起来简单,但对不齐的话效果差距非常明显——我第一批数据是自己拼的字符串,把指令和回复用换行拼在一起,训完之后模型学会了回答,但拿不到结束符,输出停不下来。
正确的做法是直接用分词器的对话模板函数,如果你的版本的 tokenizer 提供了 apply_chat_template,就用它,别手搓。如果数据本身是多轮的,模板函数也能正确处理历史轮次的拼接,省去大量手工逻辑。
还有两个细节。一是每一段样本必须以结束符收尾,让模型学会在合适的地方停;二是系统提示词如果原模型没训过对应格式,就别硬塞,直接把它合并进指令文本里更稳妥。这两点听起来琐碎,但直接影响你敢不敢把模型放出去用。
4.2 数据量与质量的经验阈值
这类参数高效微调的数据量需求,跟目标任务的性质关系极大。我自己和身边人的经验大致是这么个区间:
- 风格与格式对齐:500 到 2000 条高质量样本就够,多了反而过拟合。
- 单领域术语注入:3000 到 10000 条,且必须保证覆盖度,同一类问题至少要有多样化的表述。
- 复杂推理能力改变:坦白说,靠微调改不动,这个得靠更大的数据量加更强的训练方法,不在本文讨论范围。
我见过最常见的问题是拿几万条低质量数据硬灌。数据质量的影响远大于数量,一条答非所问的样本会教坏模型,十条重复的样本会让模型学会偷懒。我在准备数据的第一天做的最有价值的事,不是清洗格式,而是随机抽 50 条逐条读一遍,读完之后删掉了大约四成。
长度分布也要看一眼。如果你的数据里绝大部分样本只有几十个 token,而推理时用户会问几百字的问题,模型在长输入上的表现会明显退化。这种情况我一般会主动掺入一部分长样本,让长度分布别太偏。
4.3 截断、padding 与 loss mask 的实操细节
这三个词每一个都能单独出坑。
截断:当样本超过最大序列长度时,模板函数默认会截。但你要想清楚截哪一头——把指令截掉只剩回复,模型学的就是“看到半截问题直接给答案”,这是灾难。我的做法是超长样本直接过滤掉,或者按语义边界重新切分,绝不让截断发生在指令部分。
padding:训练时用右填充配合注意力掩码是标准做法,但要注意填充位置参与不参与 loss。填充的 token 必须全部屏蔽掉,否则模型会花大量精力学习“生成填充符”。
loss mask:这是最容易被忽略的一环。默认的因果语言建模是对整条序列算 loss,包括用户输入的指令部分。如果你只希望模型学“如何回答”而不是“如何提问”,就需要把指令部分的标签全部设成 -100,只保留回复部分参与计算。我对比过两种做法:不加屏蔽时收敛更快,但模型偶尔会在回答完之后自己续写下一轮的用户提问;加了屏蔽之后收敛稍慢,输出边界清晰得多。后者才是能上线的版本。
5. 训练配置怎么写:从 LoRA 秩到梯度累积
5.1 target_modules 与秩的选择逻辑
秩 r 的取值本质上是在“表达能力”和“过拟合风险”之间找平衡。我的经验区间是这样的:
| 秩 r | alpha | 适用场景 | 备注 |
|---|---|---|---|
| 8 | 16 | 格式对齐、轻量风格迁移 | 参数最少,训得最快 |
| 16 | 32 | 通用默认值 | 大部分场景的起点 |
| 32 | 64 | 领域知识注入 | 需要配双倍数据和更强正则 |
| 64 | 128 | 大量数据下的重任务 | 小型数据集上几乎必过拟合 |
alpha 一般取 r 的两倍,这是社区里流传最广的经验值,我实测也认可。它的作用是缩放适配器的输出幅度,取两倍意味着初始时适配器对原模型的影响较小,训练更稳。
关于 target_modules,前面 2.3 已经说过范围的选择。这里补一句实现层面的细节:由于专家层是八个独立模块的列表结构,用后缀匹配的方式(把 "w1" "w2" "w3" 写进列表)会自动命中所有专家,不需要手写八遍。我第一次配置时担心匹配不到,专门打印了可训练参数的名字核对,确认八个专家都被覆盖了才继续。
另外,LoRA 的 dropout 我一般设成 0.05。数据量小的场景调到 0.1,能明显压制过拟合;数据量大的话 0 也可以,但我不建议,因为 MoE 的专家本身训练信号就比较稀疏,留一点随机性有好处。
5.2 学习率、批大小与显存的三角换算
这三个变量是绑在一起的,动一个另外两个都要跟着调。量化微调的学习率比全参微调高一个量级,常用区间是 1e-4 到 2e-4,我用 1.5e-4 加余弦衰减加 3% 预热,这个组合在多个任务上都稳。
批大小受显存挤压时,用梯度累积来凑等效批大小。比如单卡只能放批 1,想要等效批 16,就设累积步数 16。这里有个非直觉的点:梯度累积会拉长单步耗时,但不会显著增加显存峰值,因为前向激活在每一步结束后就释放了。所以在显存吃紧时,优先压单批、加累积,而不是砍序列长度——序列长度对模型能力的影响远大于等效批大小。
序列长度是显存的最大变量。我给一组粗略的换算感:同样配置下,序列从 1024 翻到 2048,激活显存大致翻倍,单步耗时增加约 1.6 到 1.8 倍(不是严格线性,因为注意力部分有二次项)。如果你的数据 95% 都在 800 token 以内,就别为了那 5% 去开 4096,代价太高。
5.3 梯度检查点与注意力实现的开关陷阱
梯度检查点是消费级设备上的救命开关。它用计算时间换显存,原理是不保存中间激活,反向传播时重新算一遍。开了之后激活显存能砍掉一大半,代价是单步慢 20% 到 30%。在这个模型上我建议无条件打开,不打开在 24GB 卡上根本没戏。
但打开它有几个必须同步做的配套动作:把模型配置里的use_cache关掉(推理缓存跟梯度检查点互斥,不关会直接报冲突),确认输入嵌入层带梯度(前面 3.2 说过)。这两条漏一条都会在训练第一步就崩。
注意力实现那块,如果硬件和新版框架都支持,用高效注意力实现能省不少激活显存,速度也快。但要注意它要求数据类型一致(用 bf16 就全程 bf16,别混 fp16),且头维度必须符合实现的要求。我遇到过配错了导致回退到朴素实现、显存悄悄涨上去的情况,表现是“配置看起来都对但就是比预期慢一倍”。判断方法很简单:跑 20 步,对比一下开启前后的单步耗时,差异不明显就说明没生效。
6. 训练过程中的异常:OOM 的六种面孔与断点续训
6.1 什么指标说明训练还健康
训练起来之后,别只盯着 loss 数字。我一般同时看四样东西:训练 loss 的滑动平均、梯度范数、每秒处理的 token 数、显存占用曲线。
健康的曲线形态是这样:loss 在前 50 步快速下降(从 2.x 掉到 1.x),然后进入缓慢下降区间;梯度范数在一个小范围里波动,偶尔有尖峰但很快回落;吞吐稳定在某条水平线上;显存占用基本是一条直线,只有很小的锯齿。
出问题的信号也很明确。loss 掉得太快、几十步就掉到 0.3 以下,基本可以断定在背数据,这时候要去看验证集。梯度范数持续飙高或者出现 NaN,先查学习率是不是太大,再查数据里有没有异常长样本。显存呈阶梯式缓慢上涨,八成是某处缓存没释放,或者评估阶段加载了额外的模型。吞吐突然掉一半,检查是不是有别的进程占了卡,或者触发了内存换页。
6.2 OOM 的六种典型形态
OOM 不是一种病,是六种。我把它们和对应的处理方式列成一张表,这张表帮我省过很多时间:
| 出现时机 | 典型原因 | 处理动作 |
|---|---|---|
| 加载模型时 | 底座装不进单卡 | 用 max_memory 限制单卡额度,把部分层卸载到内存 |
| 第一次前向 | 序列太长,激活爆掉 | 降 max_seq_length,先降到 1024 验证通路 |
| 反向传播时 | 梯度检查点未开或配置不全 | 确认检查点开启、use_cache 关闭、输入层有梯度 |
| 评估阶段 | 评估批大小独立于训练批大小 | 把评估批大小显式设成 1 |
| 保存或合并时 | 合并操作在显存里做全精度重建 | 挪到内存里做,或者分片合并 |
| 生成时 | 输出长度没设上限 | 设 max_new_tokens,别让它自由发挥 |
评估阶段那个坑我印象最深。训练跑了两天一切正常,一到评估就崩,排查半天才发现评估用的是默认批大小,跟训练配置无关,独立吃了一份显存。这种“看起来训得很好但一测就死”的体验很打击人,提前把评估批大小写死能避开。
6.3 checkpoint 策略与断点续训
消费级设备上跑这种规模的训练,随时可能因为各种原因中断,保存策略必须提前配好。我的习惯是每 200 到 500 步存一次适配器,只保留最近两个,同时单独存一份最佳验证 loss 的版本。适配器本身很小,几百 MB 到 1GB 量级,占不了多少盘。
但要注意,优化器状态是单独存的,体积比适配器大好几倍。如果开了断点续训,保存时要带上优化器状态,否则恢复之后学习率调度和动量都会重置,前功尽弃。我第一次断点续训就吃了这个亏:没存优化器状态,恢复后 loss 从 1.1 直接跳回 1.9,白跑一晚上。
还有一个频率问题。存得太频繁会拖慢整体——每次保存都要写盘,大文件写盘的时候 GPU 是闲着的。我一般是按时间估:如果单步要好几秒,那 200 步一存比较合适;如果单步很快,500 步一存。反正不要让保存的耗时占比超过总时长的 5%。
7. 合并、量化与验收:训完之后的三件事
7.1 adapter 合并的正确顺序与精度取舍
训练完拿到的是适配器权重,用起来有两个选择:合并进底座,或者推理时动态加载。
合并的坑在于不能直接在 4bit 底座上合并。量化权重是不可逆的有损压缩,把适配器合并进去再保存,等于在已经失真的数值上再叠一层误差。正确做法是把底座以 bf16 全精度加载到内存里做合并,再决定后续要不要重新量化。问题是 46.7B 的 bf16 模型要 93GB 内存,不是所有机器都有。如果你的机器内存不够,就选动态加载那条路,别硬合。
合并的流程本身不复杂:加载全精度底座、加载适配器、调用合并接口、保存。合并完之后一定要做一件事——用同一批输入对比合并前后的输出。我遇到过一次合并后输出明显变了,最后发现是保存时数据类型被强转成了 fp16,导致数值溢出。核对输出是最后一道保险,别省。
7.2 评测不能只看 loss
loss 降下来只说明模型拟合了训练分布,不代表它变好了。我的评测流程一般分三层:
第一层是任务级评测。准备 100 到 200 条人工标注的测试样本,用同一套评分标准分别跑底座和微调版本,人工盲评。数量不用多,但要有代表性,覆盖你关心的所有场景。我的习惯是让评分人不知道哪条是哪个版本的输出,这样结论才可信。
第二层是格式合规率。如果你的目标是让模型按特定格式输出,就统计在测试集上的格式正确率,比如说 JSON 能不能解析、字段是否齐全。这个指标比 loss 直观得多,也更能反映实际可用性。
第三层是能力回退检查。准备一组跟微调任务无关的通用问题,对比两个版本的输出版。参数高效微调虽然改动小,但如果数据分布太窄、训练太久,还是会出现通用能力退化,典型表现是回答变得死板、拒绝回答本来能答的问题。这一层最容易被跳过,但恰恰是最影响用户体验的。
7.3 推理端的速度与显存实测口径
训完之后最终要落地到推理。合并后的模型转成量化格式,在这个规模上大概能压到 25GB 左右,24GB 卡装不下全部,需要把少数层放内存里,出词速度会掉到每秒几个 token 的量级;48GB 卡上全部装进显存,速度能到每秒十几到二十几个 token,日常对话够用。
另一种方案是保留 4bit 底座加动态加载适配器。这种方式省掉了合并和重新量化的步骤,改动快,代价是推理时每次前向多一层适配器的计算开销,实测大概慢 10% 到 20%。如果你的适配器迭代频繁(比如每天都要重训一版),这个方案在工程上更划算。
测速这件事,一定要用你自己的真实输入长度去测。我在测试时用短问题测出来的数字很漂亮,换成长文档输入直接掉了三分之二。原因是这个模型的上下文窗口很大,长输入下注意力部分的耗时占比迅速上升,跟短输入完全不是一回事。测速报告里如果不标注输入长度和输出长度,这个数字基本没有参考价值。
最后分享一个我在实际项目里坚持的做法:每次正式开跑之前,先用 100 条数据跑 100 步,确认能存盘、能恢复、能合并、能推理,四个环节全通之后再上全量数据。这套流程看着慢,但它救过我至少三次——有一次是适配器保存路径写错(跑完才发现存了个空目录),有一次是合并后输出全变成重复词,还有一次是推理端的分词器和训练端版本不一致。这些问题的共同点是:跑完全量数据才发现,损失的是以天计的时间。100 步的代价是十分钟,这个买卖怎么算都划算。