news 2026/10/28 2:01:23

大模型微调手记:从显存账本到 vLLM 上线,我踩过的那些坑

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
大模型微调手记:从显存账本到 vLLM 上线,我踩过的那些坑

上个月给一个知识库项目收尾,模型训完上线,业务方第一句话是"它怎么说话跟以前一样"。翻训练日志才发现 eval loss 早就不动了,我一直在看 train loss 往下掉。

那次之后我改了自己的顺序:先算显存,再定数据,最后才碰超参。这篇按我实际会翻的顺序排,教科书那套顺序我记不住。

一、微调之前

微调要做的事就三样。让它在这个任务上更准,让它按我们的格式说话,把行业里的术语和规则塞进参数里。

顺序上我一律是提示工程、检索、微调,前两个能解决的绝不进第三步。判断该不该微调我看三条:改提示、给示例都压不住的不稳定;格式要求严到提示保证不了;小模型微调之后的总成本,训练加部署加推理时间,比一直调大模型便宜。

三条都不占就别微调。

二、起点选哪个

绝大多数场景我直接上 Instruct。它已经做过指令对齐,指令理解是现成的,我只需要补领域知识和输出习惯,几千条数据就能看出变化。数据标注的钱主要花在这里,从零教它听懂指令是另一笔账。

Base 我只用过两次。一次是任务形式太特殊,Instruct 默认的应答方式反而是干扰;一次是手上有三万多条标注好的领域数据,准备从头做深度适配。没有这两条理由,用 Base 是给自己加活。

三、数据

这件事上数据的分量比参数大。公共数据从 HuggingFace、ModelScope 拿,私有数据基本都要先洗一遍,去重、结构化、补标注。内部文档和工单不是为训练准备的,直接丢进去只会教模型学会写周报。

批量造问答我常用 Easy Dataset 和 GraphGen,几百篇文档丢进去能出几千条,比手写快,但出来的一定要人工过一遍。

数量上我的感受是,指令模型做领域适配,一千到五千条干净样本就能看出差别,我做过最小的一个只有八百条。要从 Base 起步做深度适配才需要几万条。堆到几万条但句式雷同,收益接近于零。

数据的三个自查,都是我自己犯过的:

  • 同一句话换个标点就算两条,这种自欺欺人我干过
  • 一个业务目标要用多种问法写,不要复制粘贴同一套表达
  • 别把套话写进答案。之前有个客服数据,assistant 全是"问题分类:xx;紧急程度:xx",训完模型见到什么提问都往上套

格式上 LLaMA-Factory 吃 Alpaca 和 ShareGPT 两种。单轮用 Alpaca,多轮我用 ShareGPT,因为 conversations 数组看起来就是对话本身。

{ "conversations": [ {"from": "human", "value": "你是谁?"}, {"from": "gpt", "value": "我是按公司文档微调过的助手。"} ] }

写进 dataset_info.json 时字段映射要对上。数据集加载失败我遇到的基本都是这里,框架不知道你哪个字段是指令、哪个是回答。

还有一个藏得深的,tokenize 阶段的模板拼接。如果日志里每条样本的 token 长度忽大忽小得离谱,先去看 chat template,别去查数据。空格和换行的编码方式不一样,模型会学出奇怪的停顿。

四、显存去哪了

显存分五块:参数、梯度、优化器状态、激活值、运行时开销。

参数和梯度形状一样精度一样,占用量基本相等。优化器状态最狠,AdamW 的一阶矩二阶矩用 FP32 存,是参数的两三倍。激活值跟层数、隐藏维度、序列长度、batch 成正比,也是唯一能靠技巧压下去的一块,另外三块动不了。

选卡之前我会先用 VRAM 计算器估一遍,apxml 那个够用。比开训之后等 OOM 体面。

单卡上能动的四件事,按我用的频率排。

梯度检查点最救命。前向只存几个检查点,反向要用的时候把那段重算一遍,激活值能砍掉七到九成,训练时间多 1.3 到 2 倍。显存不够时我先开这个。

梯度累计解决的是另一个问题。显存撑不住大 batch 就拆成几个小 batch,梯度先攒着不更新,batch_size 4 加累计 3 步约等于一次 12。它不降峰值,是让你在小显存下拿到大 batch 的稳定更新。

混合精度,前向反向用 FP16 或 BF16,参数更新和梯度累积留 FP32。权重实际存两份,FP32 的主权重负责更新,FP16/BF16 的工作权重负责矩阵乘。BF16 范围跟 FP32 差不多,基本不溢出,但只有 Ampere 以上支持。老卡只能 FP16,要留意下溢。

CPU 卸载用得少。激活值、梯度、优化器状态在非计算阶段挪到内存,要用再拉回来,拿传输时间换显存,卡在两者之间的时候才划算。

五、多卡

数据并行每卡一份完整模型,跑不同数据,AllReduce 汇总梯度。目的是提吞吐,不省单卡显存。流水线并行按层切,适合跨机,接力时下游要等上游,出现气泡,缓解办法是切微批次加调度策略。张量并行切的是单层内的矩阵乘,适合机内高带宽。专家并行是 MoE 专用,专家本来就是独立 FFN,拆开就行。

ZeRO 是把冗余存掉。每卡不再存整套状态,切成分片散到各卡,算到哪层临时拉哪片,用完释放。ZeRO-1 切优化器状态,ZeRO-2 加梯度,ZeRO-3 连参数一起切。阶段越高显存越省,通信越贵。

单卡 LoRA 训 7B 以下,这套东西基本用不上。配错了排查成本比省下的显存高。

六、LoRA 和 QLoRA

全参微调效果最好,显存和速度劝退。PEFT 只更新少量参数,现在的 LoRA 已经能接近全参的效果。这条线从 Prompt Tuning、Prefix Tuning、P-Tuning、Adapter 一路下来,SFT 主流就是 LoRA。

做法不复杂。全参微调里 W₀ 会变成 W₀ + ΔW,作者发现 ΔW 的有效自由度远低于它的维度,于是拆成两个小矩阵相乘,ΔW ≈ AB,A 是 d×r,B 是 r×k,r 远小于 d 和 k。训练时 W₀ 冻住不动,只优化 A 和 B。

d=k=4096 的矩阵全参要更新约 1600 万参数,r=8 时只要 4096×8 + 8×4096 = 65536 个,0.4%。推理时 AB 还能合并回 W₀,不留额外开销,训练省推理也不亏。

插入位置我默认只挂 q_proj 和 v_proj。Query 和 Value 对任务语义最敏感,只插这两处就接近全参效果,开销最小。不够再往 k_proj、o_proj 或 FFN 扩,成本跟着涨。前向是 W₀ + (α/r)·AB,α 我一般设成等于 r,想训得激进给 2r。小数据集再加 0.05 到 0.1 的 LoRA dropout。

QLoRA 是在 LoRA 前面加一步 4-bit 量化。NF4 把标准正态分布按累积概率切成 16 个等概率区间,取中位数当代表值,0 附近格点密,误差比均匀量化小得多。双重量化把每 64 个权重共用的 32-bit 缩放因子再按 256 个一组用 8-bit 存一遍,那些因子本身也占地方。分页优化器把优化器状态拆成 page,用到才载入显存。

结果是一张 3090 或 4090 就能训几十亿参数的模型。代价是量化噪声,指令微调这类任务上基本测不出来。

七、超参

这组是我现在开新项目的起手值,不是最优,但很少翻车。

  • rank 从 16 起。验证集掉点就降到 8 或 4;领域跨度大再用 32 到 64,代价是复杂推理上低秩近似会露短板
  • α 先等于 r
  • 学习率 LoRA 用 1e-4 到 2e-4,全参用 1e-5 量级,DPO 再低一个量级
  • epoch 2 到 3,数据不到一千条我跑 1 到 2
  • cutoff_len 按数据长度 95 分位设,别顺手开 8K,序列长度直接吃激活值
  • weight_decay 0.01 左右
  • 种子必须固定

还有一个土办法。固定种子,用三个学习率各跑一次,比三次结果的方差。准确率摆动超过 5 个点,说明模型对超参极度敏感,通常是数据太少或太脏。这时候继续调参是浪费钱,回去补数据更快。

八、用 LLaMA-Factory 跑通

装:

git clone --depth 1 https://github.com/hiyouga/LLaMA-Factory.git cd LLaMA-Factory uv venv --python 3.12 source .venv/bin/activate uv pip install -e . uv pip install -r requirements/metrics.txt llamafactory-cli version

AutoDL 上先 source /etc/network_turbo 再 clone,用完 unset http_proxy && unset https_proxy 关掉。

起 WebUI:

llamafactory-cli webui # 后台跑 nohup llamafactory-cli webui > llama_factory.log 2>&1 &

看到 Running on local URL: http://0.0.0.0:7860 就算起来。云服务器要在本地开隧道,不然本机打不开:

ssh -CNg -L 7860:127.0.0.1:7860 root@connect.nmb2.seetacloud.com -p 16652

数据放 data 目录,在 dataset_info.json 里登记一条:

"keywords": { "file_name": "keywords_data_sharegpt.jsonl", "formatting": "sharegpt", "columns": { "messages": "conversations" }, "tags": { "role_tag": "from", "content_tag": "value", "user_tag": "human", "assistant_tag": "gpt" } }

刷新页面能在下拉框里选到,选中还能预览几条。之后在界面上配学习率、轮数、rank、量化等级,点开始,右边出 loss 曲线。

九、SFT 之后的对齐

SFT 教怎么答,对齐教哪个答更好。模型已经能用,但回答永远啰嗦、不该答的也答、两个说法里挑更差的那个,就该做偏好对齐。

我用 DPO 多。它不需要奖励模型,也不用跑 PPO,拿偏好对直接训,流程比 RLHF 短。代价是对 β 敏感。

数据是成对的,同一个上下文给一好一坏:

{ "conversations": [ {"from": "human", "value": "解释一下 SFT 和 DPO 的区别"} ], "chosen": {"from": "gpt", "value": "SFT 学的是照着一个答案模仿;DPO 学的是两个答案之间的相对好坏,所以它需要一个参考策略,对 β 也更敏感……"}, "rejected": {"from": "gpt", "value": "一个是监督微调,一个是偏好优化,都是微调。"} }

LLaMA-Factory 里把 dataset_info.json 那条加上 "ranking": true,stage 改成 dpo,模型路径指到训好的 SFT 模型。

chosen 和 rejected 的差别要鲜明,只是换了措辞模型学不到东西。标注不够可以从日志里凑,同一个任务一条轨迹成功一条报错,本身就是一对。

SFTDPO
数据指令回答对prompt / chosen / rejected
目标对示范答案做最大似然Bradley-Terry 偏好似然
参考策略不需要需要,一般就是 SFT 模型
稳定性稳,标准交叉熵中等,对 β 敏感
额外超参学习率、轮数再加 β
位置第一步SFT 之后

顺序不能反。β 我从 0.1 起,大了容易过拟合到那几个偏好对上,小了没效果。DPO 只改风格和偏好,别指望它补知识。

十、上线

pip install vllm vllm serve /root/autodl-tmp/models/Qwen3-8B-sft-all \ --served-model-name Qwen3-8B-sft-all --max-model-len 32K

小写 k 是 1000,大写 K 是 1024,长度参数别顺手敲。给基础模型挂适配器加 --enable-lora --lora-modules adapter_v1=<lora目录>。正式跑挂后台。

curl 通了再用 openai 库:

from openai import OpenAI client = OpenAI(base_url="http://localhost:8000/v1/", api_key="none") response = client.chat.completions.create( model="Qwen3-8B-sft-all", messages=[{"role": "user", "content": "量子计算对RSA加密的威胁与应对策略"}], extra_body={"chat_template_kwargs": {"enable_thinking": False}} ) print(response.choices[0].message.content)

model 字段必须和启动时的 served-model-name 一样,不一致就 404。LangChain 的 ChatOpenAI 指到同一个 base_url 就行。

十一、推理框架内部

一条请求进来,tokenizer 编码,封成 Sequence 进 waiting 队列,被调度器选中做 prefill,然后多轮 decode,命中 eos 或 max_new_tokens 结束。

调度器每个 step 在显存和 batch 限制下决定这轮跑哪些请求,维护 waiting、running、finished 的流转。这是 Continuous Batching,静态 batch 是一批数据从头跑到尾,连续批处理里请求随时进、完成就退,prefill 和 decode 在不同轮次交替出现。

KV cache 按 block 管。每个请求不再独占一段连续内存,只记录自己占了哪些块,所有请求共享一个分块池。BlockManager 记哪些空闲、哪些占用、哪些能被 prefix cache 复用。启动时按可用显存先算块数预留好,容量可预测,不会生成到一半 OOM。

执行层还有三件。FlashAttention 改的是注意力的计算方式,减少显存读写。张量并行把模型分到多卡。CUDA Graph 把 decode 阶段重复的小步计算录下来直接 replay,省掉 CPU 到 GPU 反复 launch 的开销,prefill 和大 batch 还是走 eager。

十二、怎么判断训成了

train loss 只说明模型拟合了自己的训练样本。压到 0.3 而实际效果全面倒退,是会发生的事,我遇到过。

先看任务指标。分类看 accuracy 和 F1,摘要看 Rouge-L,翻译看 BLEU,抽取看精确匹配。验证集用来调超参和早停,测试集最后动一次,不能混。测试集一百到五百条够了,五十条的小集上一个五点的波动没有统计意义。

再看通用能力。微调最怕的不是没学会,是把旧的忘了。我会跑 MMLU、C-Eval、HumanEval、GSM8K、TruthfulQA。合理的结果是任务指标明显涨,通用指标持平或小幅回落。断崖式下跌就是过拟合或遗忘严重,回头调学习率和轮数。

最后是人工评估。对话和写作这类任务,自动指标跟人的感受相关性很差。准备五十到两百条金标准样本,每次改动都跑一遍。也可以用强模型做裁判做 A/B,但裁判要先跟人工标注对一遍,一致性到 0.8 以上它的分才可信。

遗忘可以算成一个数。准备一个跟新任务无关的通用能力集,训前测基线分,训后再测:

遗忘率 = (基线分 - 微调后分) / 基线分

我的红线是 15%。之前做合同违约金计算,通用推理项遗忘率到了 22%,根因是训练集九成样本都含"违约金",模型把"计算"和"违约金"绑死了,看到"利息计算"也往违约上联想。改的是数据配比,不是加数据。

上线后还要看三个数,差评率、报错率、忠实度。RAG 场景里忠实度看答案还能不能对上检索片段。任何一个明显下滑就要能回滚,所以模型、数据、超参、种子得一起存档。

十三、三个项目的清单

知识库问答,按召回的片段、表格、条款生成答案。要求是忠实、完整、能标出处、资料不够时敢拒答。要练的东西是行业术语和输出格式、严格按检索内容作答、多片段整合、引用标注、拒答并说明缺什么。评的时候看术语准不准、格式稳不稳、有没有编造、引用和结论对不对、拒答是否及时。

问数,按表结构、字段值、指标定义生成 SQL。要练业务概念到字段的映射、字段值别名归一、指标口径和聚合、缺条件时主动问、多表 join 和 group by、having 的构造、只读和方言安全。评的时候看字段选得对不对、枚举值对不对、口径和去重时间范围准不准、join 路径对不对、SQL 能不能跑。

客服,识别意图、进流程、抽槽位、必要时澄清或转人工。要练意图识别,含多意图和相近意图的区分;槽位抽取,订单号、手机号、商品、时间、地址、金额;澄清话术;多轮状态管理,不重复问已经给过的信息;异常和人工升级。评的时候看意图准不准、槽位全不全、流程跳得对不对、升级是否及时、有没有乱承诺。

十四、照着查的表

出问题先对着这张表看,比瞎调参快。

现象大概原因先动哪里
train loss 一路降,验证集回答越来越差过拟合到训练集里的句式查重复句式,降 epoch,加 dropout
train loss 极低,验证 loss 回升标准过拟合,数据太少或容量过剩rank 从 16 降到 8 或 4,加多样性,加 weight_decay
通用能力断崖下跌遗忘,学习率或轮数过大学习率降一个量级,算遗忘率
该出 JSON 却带前言后语,键名还乱改格式约束样本不足补反例,输出侧加解析兜底
每次训练结果都不一样没固定种子固定 seed,记完整配置
每条样本 token 长度忽大忽小chat template 拼接错查模板,别查数据
调用直接 404model 和 served-model-name 不一致两边名字对齐
训练集漂亮,遇到新类型用户就崩数据分布太窄补真实分布的样本,别只堆一种问法

十五、剩下几条

别一上来就全参微调,先用 LoRA 跑通链路再谈效果。OOM 先看激活值,不要只看参数量。BF16 优先,老卡才用 FP16。数据集注册字段对不上是最高频的错误,报错先查这里。LoRA 只挂 q_proj 和 v_proj 先试,不够再扩。学习率宁小勿大。先有一个能用的 SFT 模型再做 DPO。vLLM 的 served-model-name 和调用端 model 必须一字不差。上线前压测并发,吞吐和时延是两个独立问题。

现在跑一个 8B 模型的领域微调,从整理数据到 vLLM 起来大概是四天,其中三天在数据上。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/28 2:00:55

数组,一维数组,二维数组

1、数组的概念数组是一组相同类型元素的集合数组中存放的是1个或者多个数据&#xff0c;但数组的元素个数不能为0数组中存放的多个数据&#xff0c;类型是相同的数组分为一维数组和多维数组&#xff0c;多维数组比较常见的是二维数组2、一维数组的创建与初始化2.1 数组创建基本…

作者头像 李华
网站建设 2026/10/28 2:00:03

代码漏洞检测-python+Flask

本项目为前几天收费帮学妹做的一个项目&#xff0c;在工作环境中基本使用不到&#xff0c;但是很多学校把这个当作编程入门的项目来做&#xff0c;故分享出本项目供初学者参考。 一、项目描述 基于pythonFlask代码安全检测系统通过Mysql数据库连接数据库 http://127.0.0.1:510…

作者头像 李华
网站建设 2026/10/28 1:59:53

两三万字毕业长文怎么降AI:2027主流降AIGC工具比较与复核

两三万字毕业长文怎么降AI&#xff1a;2027主流降AIGC工具比较与复核【工具功能与核对要点】BunnyScholar&#xff1a;比较长文处理与术语保留情况&#xff0c;逐段核对学术论证、引用和原意。助研君&#xff1a;比较分段处理、输出质量与费用&#xff0c;修改后保留版本并人工…

作者头像 李华
网站建设 2026/10/28 1:59:50

一文搞懂QObject 和Q_OBJECT

QObject *parent nullptr 和 Q_OBJECT 宏是 Qt 框架中两个至关重要的机制&#xff0c;它们分别从对象生命周期管理和元编程能力两个维度&#xff0c;构成了 Qt 对象模型的基础。&#x1f333; QObject *parent nullptr&#xff1a;对象树与内存管理这个参数的核心作用是建立 …

作者头像 李华
网站建设 2026/10/28 1:59:46

手机销量创十年新低,价格却屡创新高,怎么回事?

说起最近一段时间的手机市场&#xff0c;相信大多数人的感觉都是气氛压抑&#xff0c;一边是持续下滑的销量&#xff0c;另一边却是持续上涨的价格&#xff0c;面对着手机销量创十年新低&#xff0c;价格却屡创新高的情况&#xff0c;我们到底该怎么看&#xff1f;一、手机销量…

作者头像 李华
网站建设 2026/10/28 1:59:27

车载以太网断链故障注入怎么测?用USB API控制物理链路通断的实操方法

随着智能驾驶功能安全要求的持续升级&#xff0c;ISO 26262标准对车载ECU的容错机制验证提出了严苛的可复现性要求&#xff0c;车载以太网作为ADAS感知设备、自动驾驶域控的核心传输链路&#xff0c;链路异常工况的模拟已经成为HiL硬件在环测试、DV/PV可靠性验证、产线EOL校验环…

作者头像 李华