ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

单卡24G显存从零训练大模型:预训练到领域适配全链路实战

2026/10/1 13:46:16 拓冰建站 浏览量
单卡24G显存从零训练大模型:预训练到领域适配全链路实战 个人开发者想在本地把一个大语言模型从零训练到能干活这件事在两年前还像是天方夜谭现在却已经变成了一条有章可循的工程链路。我自己用一张 RTX 3090 把 GPT-2 级别的模型从预训练一路做到领域适配中间踩过的坑、烧掉的电费、反复推翻的方案加起来足够写一本小册子。这篇就把整条链路摊开讲清楚预训练到底在做什么、领域适配该怎么下手、单卡 24G 显存怎么把 batch 塞进去、数据从哪来、评测看哪些指标、什么时候该停。不管你是刚接触 LLM 的新手还是已经跑过几个微调脚本但总觉得知其然不知其所以然的开发者都能从里面找到能直接抄的配置和能少走弯路的经验。1. 先把预训练和领域适配这两件事分清楚很多人一上来就问我要训练自己的大模型从哪开始这个问题本身就问错了。预训练和领域适配是两个目标完全不同、成本差两个数量级、数据要求也完全不同的阶段。把它们混在一起谈最后一定是钱花了、模型没训好、还不知道问题出在哪。1.1 预训练的本质是让模型学会语言的统计规律预训练阶段模型干的事情非常朴素给它海量文本让它预测下一个 token。就这么简单。但正是这个简单目标逼着模型在内部学出语法、事实关联、推理模式的雏形。你可以把它理解成一个小孩在大量听大人说话还没人教他语法但他已经能说出我吃饭而不是饭吃我。这个阶段的核心特征是数据量极大、算力消耗极高、目标极其通用。GPT-2 当年用了约 40GB 的 WebText 数据参数量 1.5B训练成本按今天的卡价折算也要几千美元级别。个人开发者如果真要从随机初始化开始训一个可用的基座坦白说单卡 3090 跑几个月也未必能到 GPT-2 的水平。所以现实的做法是预训练阶段要么用现成的开源基座要么只做小规模的继续预训练来注入领域语料。我自己的选择是后者。拿一个已经预训练好的中文 GPT-2 权重作为起点用我收集的领域语料做继续预训练。这样既保留了通用语言能力又能让模型熟悉我关心的那批文本的分布。这个决策背后的逻辑很简单从零预训练是造轮子继续预训练是给轮子换个更适合路面的胎。1.2 领域适配解决的是通用模型不懂我的行话通用模型的问题不在于它笨而在于它没见过你的领域。你问它某个行业的专有流程、内部术语、特定格式的输出它要么答得泛泛要么直接编。领域适配要做的就是让模型在你的数据分布上再走一遍训练把行话和领域知识灌进去。领域适配的手段有好几档成本从低到高大致是手段数据量要求显存需求效果适用场景Prompt 工程0推理级有限快速验证继续预训练大GB级高强领域语料充足全参数微调中万条级很高强有标注数据LoRA/QLoRA小千条级低中到强单卡首选只做 RAG0推理级中知识频繁更新对个人开发者来说继续预训练 LoRA 微调是最务实的组合。继续预训练负责让模型见过领域文本LoRA 负责让模型学会具体任务。这两步分开做出问题的时候也容易定位是哪一步的锅。1.3 为什么单卡 3090 也能跑通全流程RTX 3090 的 24GB 显存放在今天不算富裕但配合几个关键技术跑 GPT-2 级别1.5B 以下的全流程是够的。关键手段有三个混合精度训练fp16 或 bf16 能把显存占用和计算量都砍掉接近一半。3090 对 bf16 支持一般fp16 配 GradScaler 更稳。梯度累积显存装不下大 batch就用小 batch 多跑几步再更新一次等效 batch size 照样能上去。梯度检查点用计算换显存激活值不全部保存反向传播时重算。显存能省 50% 以上代价是训练慢 20% 到 30%。我实测下来1.5B 模型在 3090 上做继续预训练序列长度 512、batch size 4、梯度累积 8显存占用稳定在 21GB 左右能长时间跑不 OOM。这个配置不是拍脑袋来的后面会讲怎么算。2. 数据准备决定成败的其实在这一步模型结构是公开的训练脚本是现成的真正拉开差距的是数据。我见过太多人把精力全花在调参上结果数据一塌糊涂loss 降得再漂亮生成出来还是废话。数据这块我踩的坑最多单独拎出来讲。2.1 领域语料的来源与清洗我的领域语料主要来自三类公开的行业文档、我自己积累的笔记和问答记录、以及一批结构化的说明文本。来源不重要重要的是清洗。原始文本里混着大量噪声HTML 标签、重复段落、乱码、广告、导航栏文字。这些东西不清理掉模型学到的就是垃圾分布。清洗流程我固定成这么几步去标签用正则或 BeautifulSoup 把 HTML、Markdown 标记剥掉只留纯文本。去重用 MinHash 或简单的 SimHash 做近似去重重复内容会让模型过拟合到那几句话上。长度过滤太短的片段少于 50 字直接丢信息量不够超长的按段落切。语言过滤用 fastText 或简单的字符集判断把混进来的其他语言文本剔掉。敏感与低质过滤这一步必须做规则加关键词双重过滤宁可错杀不可放过。提示清洗脚本一定要保留原始文件和清洗后文件的对应关系出问题能回溯。我一开始图省事直接覆盖后来发现某批数据被误删想找回都找不回。2.2 分词器的选择与词表扩充GPT-2 原版分词器对中文很不友好一个汉字经常被切成多个 byte 级 token序列长度白白浪费。我的做法是换用中文优化过的分词器或者在原词表基础上扩充中文词表。扩充词表的坑在于新加的 token 对应的 embedding 是随机初始化的必须让模型重新学。如果直接拿扩充后的模型做推理新 token 的表示是乱的。正确做法是扩充完词表后至少跑一轮继续预训练让新 embedding 收敛。具体操作上我用 tokenizers 库训练一个领域词表把高频的领域术语加进去然后 resize 模型的 embedding 层。resize 之后 embedding 参数量变了优化器状态也要重建这一步容易忘。2.3 把语料打包成训练样本的正确姿势预训练样本的构造方式直接影响训练效率。最朴素的做法是把所有文本拼成一条超长序列然后按固定长度切块。但这样会让句子在切块边界被截断模型学到一半的句子。更好的做法是每篇文档之间用特殊分隔符隔开让模型知道边界。切块时尽量在句子边界切减少截断。用packing技术把多条短文本拼进一个序列减少 padding 浪费。我用的方案是文档级拼接 句边界切分 动态 padding。实测下来同样数据量下有效 token 比例能从 70% 提到 90% 以上等于白捡了 20% 的训练效率。3. 训练配置显存、学习率、步数的计算逻辑配置不是抄来的是算出来的。这一节我把每个关键参数背后的计算过程摊开你照着套自己的硬件就能算出该设多少。3.1 显存占用的构成与估算训练时显存主要被四块吃掉模型参数、梯度、优化器状态、激活值。以 1.5B 参数模型、fp16 混合精度为例模型参数1.5B × 2 字节 3GB梯度1.5B × 2 字节 3GB优化器状态Adam1.5B × 8 字节 12GBfp32 的动量和方差激活值跟 batch size 和序列长度成正比波动最大前三项加起来已经 18GB留给激活值的只有 6GB 左右。这就是为什么必须上梯度检查点——它能把激活值占用压到原来的三分之一甚至更低。注意如果你用 AdamW 且不做任何优化1.5B 模型光优化器状态就 12GB这是很多人 OOM 的直接原因。换 8-bit Adam 能把这块砍到 3GB代价是收敛稍慢。3.2 学习率与 warmup 的设置依据预训练和微调的学习率差一个数量级。继续预训练我用 1e-5 到 5e-5LoRA 微调用 1e-4 到 3e-4。为什么差这么多因为继续预训练是在已经收敛的权重上小步走步子大了会把原有能力破坏掉LoRA 只训练新增的小矩阵可以从头快速学。warmup 步数一般设总步数的 1% 到 5%。warmup 的作用是让优化器状态和梯度统计先稳定下来避免一开始就用大学习率把权重带偏。我习惯设 500 步 warmup实测比不设 warmup 的 loss 曲线平滑很多。学习率调度用 cosine 衰减最后降到峰值的 10% 左右。线性衰减也能用但 cosine 在后期收敛更稳。3.3 梯度累积与等效 batch size 的换算等效 batch size 单卡 batch size × 梯度累积步数 × 卡数。单卡场景下就是前两者相乘。为什么要关心等效 batch size因为它影响梯度的噪声水平和训练稳定性。等效 batch 太小梯度噪声大loss 震荡太大泛化可能变差而且显存吃不消。经验值是等效 batch 在 64 到 512 之间比较舒服。我的配置单卡 batch 4梯度累积 16等效 batch 64。这个组合在 3090 上显存占用约 21GB训练速度约每秒 3 到 4 个 step跑 10 万步大概需要 8 到 10 小时。4. 领域适配的两种主流路线与取舍继续预训练做完模型已经见过领域文本了但它还不一定会干活。领域适配这一步要把模型的能力对齐到具体任务上。主流路线有两条各有各的适用场景。4.1 全参数微调效果上限高但门槛也高全参数微调就是拿预训练好的模型在你的任务数据上把所有参数都更新一遍。优点是效果上限最高模型能充分适配任务缺点是显存需求大、容易过拟合、每个任务都要存一份完整权重。1.5B 模型全参数微调即使上梯度检查点和 8-bit Adam3090 也跑得很勉强batch size 只能压到 1 到 2。而且小数据集上全参数微调极易过拟合训练 loss 一路降验证 loss 早早反弹。我的建议是除非你有十万条以上的高质量标注数据否则别碰全参数微调。个人开发者在这个阶段用 LoRA 性价比高得多。4.2 LoRA 与 QLoRA单卡玩家的现实选择LoRA 的思路是在原权重旁边挂一对低秩矩阵训练时只更新这对小矩阵原权重冻结。这样可训练参数量能降到原来的千分之一甚至更低显存需求大幅下降。QLoRA 更进一步把原权重用 4-bit 量化存起来进一步省显存。代价是推理和训练速度会慢一些精度也有轻微损失。LoRA 的关键参数是 rank秩和 alpharank一般设 8 到 64。任务越复杂、数据越多rank 可以越大。我常用 16。alpha缩放系数一般设成 rank 的 1 到 2 倍。alpha/rank 的比值决定 LoRA 更新的幅度。target_modules决定给哪些层挂 LoRA。注意力层的 q、v 投影是标配加上 k、o 和 FFN 层效果更好但参数更多。我实测下来rank 16、alpha 32、target 覆盖 q/k/v/o 和 FFN在几千条领域数据上微调效果已经接近全参数微调而显存只用了不到 8GB。4.3 两种路线的实测对比我在同一批领域数据上跑了全参数微调和 LoRA 微调对比结果大致如下维度全参数微调LoRA 微调可训练参数1.5B约 8M显存峰值23GB7GB单步耗时1.2s0.4s领域任务准确率82%79%通用能力保持下降明显基本保持权重存储每任务 3GB每任务 30MBLoRA 在准确率上只差 3 个百分点但显存、速度、存储全面占优而且通用能力不掉。对个人开发者来说这个取舍非常划算。5. 评测别只看 loss那玩意儿会骗人训练 loss 降了不代表模型变好了。我早期就吃过这个亏loss 从 3.5 降到 2.1生成出来还是一堆重复和胡话。评测必须多维度做。5.1 困惑度只是入门指标困惑度Perplexity衡量模型对文本的惊讶程度越低说明模型越能预测这批文本。它在预训练阶段有用能反映模型是否在学语言规律。但它有两个致命问题一是只反映语言建模能力不反映任务能力二是对领域外文本和领域内文本的困惑度不可比。我的做法是困惑度只在继续预训练阶段看用来判断模型有没有在学到了微调阶段困惑度基本不看直接看任务指标。5.2 领域任务的评测集怎么造领域任务评测集必须自己造因为公开榜单测的是通用能力跟你的领域没关系。造评测集的原则覆盖真实使用场景把你实际会问模型的问题类型都覆盖到。有标准答案分类任务有标签生成任务有人工写的参考答案。难度分层简单、中等、困难各占一部分能看出模型的能力边界。规模适中200 到 500 条就够看出趋势太多标注成本高。我一般造 300 条评测样本人工标注答案然后固定下来。每次模型更新都跑同一套评测集这样才有可比性。5.3 人工评估与自动指标的结合自动指标准确率、BLEU、ROUGE能快速给个分数但它们和人类判断经常不一致。生成任务尤其如此BLEU 高的回答可能读起来很别扭BLEU 低的反而更自然。我的做法是自动指标做粗筛人工评估做终审。人工评估时让标注者从三个维度打分相关性、流畅度、事实准确性。每个维度 1 到 5 分最后取平均。这套流程跑下来模型的好坏判断基本不会跑偏。6. 踩坑实录那些让我熬夜到凌晨的问题这一节全是血泪。每个坑我都完整记录了排查过程你可以直接对照自己的情况。6.1 loss 突然变成 NaN 的排查链路训练到第 3000 步左右loss 突然从 2.3 跳到 NaN之后再也降不下来。排查过程先看是不是学习率太大把学习率砍半重跑还是在类似位置 NaN排除。看是不是数据里有异常样本抽样检查那批数据发现有几条超长文本token 数超过 4096被截断后产生了大量 padding梯度异常。加长度过滤后问题消失。看是不是 fp16 溢出开了 GradScaler 的动态缩放理论上能防溢出但极端样本还是可能触发。后来改用 bf16在支持的卡上彻底解决。根因是数据里的超长样本。教训数据清洗时一定要做长度分布统计把长尾样本处理掉。6.2 生成结果重复退化的成因与修复模型生成时反复输出同一句话比如这个问题需要综合考虑这个问题需要综合考虑。这是典型的重复退化。成因有几个训练数据里有大量重复文本模型学到了重复模式。解码策略太贪心beam search 或 greedy 容易陷入循环。模型过拟合在训练集上反复见到相同模式。修复手段数据去重、加 repetition penalty、用 top-k 或 nucleus sampling、加 dropout。我组合用了去重 repetition penalty 1.2 top-p 0.9重复问题基本消失。6.3 显存碎片导致的间歇性 OOM最烦的是那种跑几个小时才 OOM 一次的问题。排查发现是显存碎片PyTorch 的缓存分配器在长时间运行后会留下碎片新的分配请求找不到连续空间。解决办法设PYTORCH_CUDA_ALLOC_CONFexpandable_segments:True让分配器能动态扩展段。另外把 batch size 稍微调小一点留出余量。这两个措施之后连续跑 24 小时没再 OOM。6.4 继续预训练后通用能力下降的应对继续预训练跑完领域任务确实变好了但通用问答能力明显下降问它常识问题都答得磕磕绊绊。这是灾难性遗忘。应对手段混入通用语料继续预训练时按 7:3 或 8:2 的比例混入通用文本让模型别把通用能力忘光。降低学习率步子小一点遗忘慢一点。用 LoRA 做继续预训练只更新小矩阵原权重冻结通用能力基本不动。我最后用的是混入通用语料 低学习率通用能力下降控制在可接受范围内。7. 从训练到部署让模型真正跑起来训完的模型得能用起来才算数。部署这块有几个现实问题要解决。7.1 权重合并与格式转换LoRA 训完的权重是分离的推理时要合并回原模型。合并公式很简单W W (alpha/rank) × BA。合并后存成标准格式方便后续加载。格式上PyTorch 原生的 .bin 或 .safetensors 都行。safetensors 加载更快、更安全推荐用。如果要部署到特定推理引擎可能还要转成 ONNX 或其他格式转换时注意算子兼容性。7.2 推理显存与速度的优化推理比训练省显存但也有优化空间量化4-bit 或 8-bit 量化能把显存占用砍到四分之一到一半速度也有提升精度损失可控。KV Cache自回归生成时缓存 key 和 value避免重复计算速度能提升数倍。批处理多个请求一起推理吞吐量更高。我在 3090 上跑 1.5B 模型的 4-bit 量化版本显存占用不到 2GB生成速度约每秒 30 个 token完全够个人使用。7.3 和 RAG 配合的边界在哪模型再强知识也有截止日期而且领域知识更新频繁。这时候 RAG检索增强生成就派上用场把最新知识放在外部知识库里推理时检索相关片段塞进 prompt。RAG 和微调的边界很清楚微调管能力和风格RAG 管事实和时效。模型该学会怎么组织答案、用什么语气这是微调的活具体某个事实是什么交给 RAG。两者配合比单用任何一个都强。我现在的架构是领域微调模型负责理解和生成RAG 负责提供最新事实效果比纯微调或纯 RAG 都好。8. 一些没人告诉你但很重要的经验最后这部分是我个人在实际操作中攒下来的零碎经验不成体系但都挺有用。关于硬件3090 的散热是个大问题长时间训练容易过热降频。我加了个风扇对着吹训练速度稳定了不少。另外电源要留足余量训练时功耗能到 350W 以上。关于时间管理训练脚本一定要支持断点续训。我遇到过停电、驱动崩溃、手滑关终端各种情况没有断点续训的话几小时白跑。checkpoint 每 1000 步存一次存最近三个旧的自动删。关于实验记录每次训练的超参、数据版本、评测结果都要记下来。我用一个简单的表格记后来实验多了才发现这个习惯救了我无数次。没有记录的话你根本不知道哪个配置效果好。关于数据版本数据一定要版本化。清洗规则改一次数据就变一次模型效果也会变。用 git 或简单的目录版本管理保证任何一次训练都能追溯到用的哪版数据。关于心态LLM 训练是个长周期的事一次跑不通很正常。我第一个能用的模型是第七次尝试才跑出来的。每次失败都要搞清楚为什么失败别盲目重跑。搞清楚一个坑下次就少踩一个。这套流程我从头到尾跑通花了大概两个月中间大部分时间花在数据清洗和踩坑上真正训练的时间反而不多。如果你也想走这条路我的建议是先把数据这块做扎实训练配置照着算评测认真做剩下的就是耐心。模型不会一夜之间变好但每一步做对了它就会一点点变好。