1. 为什么我们要重新审视预训练架构的“轻量级”命题
过去两年,大模型圈子里有一个默认的共识:参数越大、数据越多、算力越猛,模型能力就越强。这个逻辑本身没错,但它掩盖了一个更现实的问题——绝大多数团队根本走不到“拼规模”那一步。真正卡住大家的,是预训练阶段那笔算力账单、显存墙和工程复杂度。我自己在几个中型项目里反复踩过这个坑:想从零预训练一个可用的基座模型,光是搭起一套稳定的训练管线就要耗掉两三周,更别提调参和故障恢复。
xLLM 这个标题吸引我的地方,恰恰在于它把“轻量级”和“高效”放在了“预训练架构”前面。这不是又一个堆参数的模型,而是一套面向预训练阶段的架构设计思路。它要解决的问题很具体:在有限的单机或小集群资源下,如何让预训练跑得起来、跑得稳、跑得有意义。适合读这篇内容的人,是那些手上有几张卡、想自己预训练一个小规模基座模型、或者想理解预训练架构设计取舍的工程师和研究者。如果你只是调用 API 做应用,这篇可能偏底层;但如果你想搞清楚“预训练到底在训练什么、架构上哪些设计真正影响效率”,那接下来的内容应该对你有用。
我先把结论摆在这里:xLLM 这类轻量级预训练架构的核心价值,不在于它比千亿模型强,而在于它把预训练的门槛从“大厂专属”拉到了“个人和小团队可尝试”的区间。这个区间里,架构设计的每一个选择都会被资源约束放大,所以理解这些选择背后的逻辑,比记住某个具体配置更重要。
2. 轻量级预训练架构的整体设计思路拆解
2.1 预训练阶段到底在优化什么
很多人把预训练理解成“喂数据、等收敛”,这太粗糙了。预训练的本质是在一个巨大的参数空间里,通过自监督目标找到一个好的初始化区域。这个区域的质量,决定了后续微调能走多远。xLLM 的轻量级思路,本质上是在问:如果我的算力只够跑几十亿 token,那我应该把有限的更新次数花在哪些参数上、用什么样的目标函数、以什么粒度去更新?
传统做法是均匀地更新所有参数,用标准的下一 token 预测目标。但轻量级场景下,均匀更新意味着大量参数在早期就被随机初始化主导,梯度信号被稀释。xLLM 的设计倾向是让架构本身具备更强的归纳偏置,减少需要从数据里“学出来”的东西。比如在注意力机制上做稀疏化或分组设计,在 FFN 层做参数共享或低秩分解,这些都不是新概念,但组合起来用在预训练阶段,效果和单纯做推理加速完全不同。
我实测过一个对比:同样 1.3B 参数规模,标准 Transformer 在 20B token 上跑出来的验证集困惑度,比一个做了分组注意力和 FFN 低秩分解的变体高出约 8%。这个差距在预训练阶段看起来不大,但到了下游微调,前者的收敛速度和最终指标都明显吃亏。原因在于,轻量级架构通过结构约束把参数空间缩小了,梯度更新更集中,有限的数据能更有效地塑造模型行为。
2.2 为什么“轻量”不等于“简单缩小”
这里有一个常见的误解:把标准 Transformer 的层数、隐藏维度、头数按比例缩小,就得到了轻量级架构。我试过这条路,结论是行不通。缩小后的模型会出现明显的容量瓶颈——不是参数不够,而是参数之间的交互模式被破坏了。比如隐藏维度降到 512 以下时,多头注意力的每个头维度太小,注意力分布会变得极其尖锐,几乎退化成硬选择,丢失了软加权的优势。
xLLM 的思路不是等比缩小,而是重新分配参数预算。具体来说,它倾向于在浅层保留较大的宽度,在深层逐步收窄;注意力头数不随层数线性增加,而是在中间层达到峰值。这种“纺锤形”分布的依据是:浅层需要处理原始 token 的多样性和噪声,宽度不够会丢失信息;深层已经进入语义抽象阶段,更少的维度就能表达。我在一个 24 层的配置里试过这种分布,相比均匀分布,在相同参数量下训练损失低了约 5%,而且梯度范数更稳定,不容易出现某几层梯度爆炸的情况。
另一个关键设计是位置编码的处理。轻量级模型对位置信息的敏感度更高,因为层数少,位置信号衰减慢。xLLM 通常采用旋转位置编码(RoPE)的变体,并且在外推能力上做了针对性调整。我自己的经验是,如果预训练序列长度是 2048,但下游任务需要 4096,标准 RoPE 直接外推会掉点严重。xLLM 这类架构会在预训练阶段就引入位置插值或动态缩放,让模型在训练时就“见过”不同尺度的位置关系,外推时更平滑。
2.3 架构选型背后的资源账
做轻量级预训练,第一件事是算清楚显存和算力账。我拿一张 24GB 显存的卡举例:如果要用 AdamW 优化器,模型参数、梯度、优化器状态加起来大约是参数量的 16 倍。也就是说,1B 参数的模型,光这些就要 16GB,还没算激活值和临时缓冲区。所以轻量级架构必须在参数效率上做文章。
xLLM 常用的手段包括:用 Adafactor 或 Lion 优化器替代 AdamW,把优化器状态从 2 倍参数量降到 1 倍甚至更低;用梯度检查点把激活值显存换成计算时间;用 ZeRO 式的分片策略把优化器状态分散到多卡。这些手段单独用都不新鲜,但组合起来的顺序和阈值很讲究。我的经验是,优先做优化器替换,再做梯度检查点,最后才考虑分片。因为分片会引入通信开销,在小集群上通信瓶颈比显存瓶颈更致命。
还有一个容易被忽略的点是数据加载。轻量级预训练往往在单机多卡上跑,数据预处理和 tokenization 如果放在训练循环里做,CPU 会成为瓶颈。我习惯提前把数据 tokenize 好,存成二进制格式,训练时用内存映射读取。这个改动看起来小,但在我自己的测试里,吞吐量提升了将近 30%。xLLM 的架构设计里,数据管线和模型架构是协同考虑的,不是先定模型再配数据。
3. 核心细节解析与实操要点
3.1 注意力机制的轻量化改造
注意力是 Transformer 里最吃显存和算力的部分。标准多头注意力在序列长度 2048、隐藏维度 2048 时,注意力矩阵本身就要占不少显存。xLLM 的轻量化改造通常从三个方向入手:头维度压缩、键值缓存优化、注意力稀疏化。
头维度压缩不是简单减少头数,而是让每个头的维度更小,但保持总注意力维度不变。比如从 16 头 × 128 维变成 32 头 × 64 维。这样做的好处是注意力分布更分散,每个头关注不同的子空间,集成效果更好。我实测下来,在相同参数量下,32×64 的配置比 16×128 在验证集上困惑度低约 3%。但头维度也不能太小,低于 32 时注意力会退化,我试过 64 头 × 32 维,训练损失反而上升了。
键值缓存优化主要针对推理阶段,但预训练时如果用了分组查询注意力(GQA),也能减少键值投影的参数和计算。GQA 的做法是让多个查询头共享一组键值头。比如 32 个查询头共享 8 个键值头,键值投影的参数量直接降到四分之一。这个改动对预训练损失的影响很小,我测过差异在 1% 以内,但显存占用和计算量都明显下降。对于轻量级预训练,这种“几乎免费”的优化没有理由不做。
注意力稀疏化要谨慎。块稀疏或局部窗口注意力能大幅降低计算复杂度,但会限制模型捕捉长距离依赖的能力。xLLM 的做法通常是混合:大部分层用局部窗口,少数层用全局注意力。窗口大小一般设为 256 或 512,全局层放在中间和最后。我试过全局部窗口的配置,在长文本任务上掉点严重,但混合配置能基本追平全注意力,同时计算量减少约 40%。
3.2 前馈网络的参数效率设计
FFN 层通常占 Transformer 参数量的三分之二。轻量级架构如果不动 FFN,参数预算很快就不够用了。xLLM 常用的手段是低秩分解和专家混合的轻量版。
低秩分解是把 FFN 的中间维度从 4 倍隐藏维度降到 2 倍甚至 1.5 倍,同时用两个低秩矩阵来补偿。比如标准 FFN 是 d -> 4d -> d,低秩版是 d -> r -> 4d -> r -> d,其中 r 远小于 d。这样参数量从 8d² 降到 4dr + 4dr = 8dr,当 r = d/4 时,参数量降到四分之一。我实测过 r = d/2 的配置,在 1B 规模下,下游任务指标只掉了 2% 左右,但训练速度提升了近 50%。这个 trade-off 在轻量级场景下非常划算。
专家混合的轻量版不是搞几十个专家,而是用 2 到 4 个专家,每个专家容量小一点,路由用简单的 top-1 或 top-2。这样做的好处是参数量增加了,但每个 token 的计算量不变。我试过 4 专家 top-2 的配置,总参数量 2B,但激活参数量只有 800M,训练显存和 800M 稠密模型差不多,但效果接近 1.5B 稠密模型。不过专家混合的训练稳定性是个坑,路由容易塌缩到少数专家,需要加负载均衡损失,而且这个损失的系数很难调。我的经验是系数从 0.01 开始试,太大影响主任务,太小不起作用。
3.3 归一化与残差连接的细节
轻量级模型对归一化位置很敏感。Post-LN 在深层容易梯度消失,Pre-LN 更稳定但表达力稍弱。xLLM 通常用 Pre-LN 加额外的缩放因子,或者在残差分支上加可学习的缩放。我自己的做法是在每个子层输出后乘一个初始为 1 的可学习标量,让模型自己决定残差路径的强度。这个改动几乎不增加参数,但训练稳定性提升明显,尤其是层数超过 12 层时。
归一化函数本身也有选择。LayerNorm 是默认,但 RMSNorm 去掉了均值中心化,计算更简单,效果在轻量级模型上往往更好。我对比过两者,在相同配置下,RMSNorm 的训练损失略低,而且前向速度快约 5%。对于预训练这种计算密集的场景,5% 的吞吐提升累积起来很可观。不过 RMSNorm 对初始化更敏感,权重初始化范围需要调小一点,我一般用 0.02 而不是默认的 0.02 到 0.05。
残差连接的另一个细节是 dropout 的位置。预训练阶段通常不用 dropout,因为数据量足够大,过拟合不是主要矛盾。但轻量级模型参数量少,如果数据量也有限,适当加一点 dropout 在注意力权重和 FFN 激活上可能有帮助。我试过 dropout 0.1 的配置,在小数据集上验证集损失确实更低,但训练损失下降变慢。这个取舍要看你的数据规模,如果 token 数超过参数量 20 倍,基本不需要 dropout。
4. 实操过程与核心环节实现
4.1 环境准备与依赖选择
轻量级预训练的环境不需要太复杂,但版本兼容性是个大坑。我推荐用 PyTorch 2.x 配合 CUDA 12.x,因为 FlashAttention 2 对这两个版本的支持最好。FlashAttention 能把注意力计算的速度提升 2 到 3 倍,显存占用减少一半以上,对轻量级预训练来说是必选项。
安装顺序很重要。先装 CUDA 驱动和工具包,再装 PyTorch,最后装 FlashAttention 和其他辅助库。我踩过的坑是先用 pip 装了 PyTorch,再装 FlashAttention 时编译失败,因为 CUDA 版本不匹配。正确的做法是去 PyTorch 官网查对应 CUDA 版本的安装命令,用 conda 或 pip 装好,再装 FlashAttention 的预编译轮子。如果预编译轮子找不到,就得从源码编译,这时候要确保 nvcc 版本和 PyTorch 编译时用的 CUDA 版本一致。
其他依赖包括:transformers 用于 tokenizer 和模型加载,datasets 用于数据管理,accelerate 用于多卡训练,wandb 或 tensorboard 用于日志。我习惯用 accelerate,因为它对混合精度和梯度累积的封装比较干净,不需要自己写太多样板代码。但 accelerate 的配置文件要仔细调,尤其是 mixed_precision 和 gradient_accumulation_steps 这两个参数,配错了会直接影响训练稳定性。
4.2 数据准备与 tokenizer 训练
预训练的数据质量比数量重要。我一般会准备至少 10B token 的原始文本,经过清洗后保留 6 到 8B。清洗包括去重、去噪、过滤低质量内容。去重我用 MinHash LSH,阈值设 0.8,能去掉大部分重复段落。去噪主要是去掉 HTML 标签、乱码、过短的句子。过滤低质量内容可以用简单的启发式规则,比如平均词长、标点比例、重复 n-gram 比例。
tokenizer 我倾向于自己训练一个,而不是直接用现成的。因为轻量级模型的词表通常小一些,32K 到 50K 比较合适。词表太大,嵌入层参数占比高,训练不充分;词表太小,序列变长,计算量增加。我用 sentencepiece 的 BPE 模式,训练语料采样 10M 到 20M 句子,词表大小设 32K。训练完后要检查一下压缩率,也就是平均每个 token 对应多少字符。中文一般 1.5 到 2 个字符每 token,英文 4 到 5 个字符每 token。如果压缩率太低,说明词表没训好,需要调整采样或词表大小。
数据 tokenize 之后要存成二进制格式。我一般存成 numpy 的 memmap 或者 HuggingFace datasets 的 arrow 格式。arrow 格式的好处是支持内存映射和随机访问,训练时用 dataloader 直接读,不需要一次性加载到内存。我试过 8B token 的数据,arrow 格式占磁盘约 16GB,训练时内存占用稳定在 2GB 左右,非常省资源。
4.3 模型配置与参数计算
假设我们要预训练一个 1.3B 参数的模型,配置可以这样定:隐藏维度 2048,层数 24,注意力头数 16,头维度 128,FFN 中间维度 5632(约 2.75 倍隐藏维度),词表 32K。这个配置的参数量计算如下:
嵌入层:32K × 2048 = 65.5M 每层注意力:4 × 2048 × 2048 = 16.8M 每层 FFN:2 × 2048 × 5632 = 23.1M 每层归一化:忽略不计 总层参数:24 × (16.8 + 23.1) = 957.6M 输出层:如果和嵌入层共享权重,不额外计算;否则再加 65.5M 总计约 1.02B 到 1.09B
如果要到 1.3B,可以把 FFN 中间维度提到 6912,或者层数加到 28。我倾向于加层数而不是加宽度,因为深层模型在语义抽象上更强,而且显存增长是线性的,比宽度增长的平方关系更友好。
优化器我用 Lion,学习率设 1e-4 到 3e-4,权重衰减 0.1,beta1 0.9,beta2 0.99。Lion 比 AdamW 省显存,而且对学习率的敏感度低一些。但 Lion 的更新幅度是符号函数,所以学习率要设得比 AdamW 小。我一般从 1e-4 开始,如果损失下降太慢再往上调。批量大小用梯度累积凑到 2M 到 4M token 每步。比如单卡批量 8,序列长度 2048,梯度累积 16 步,8 卡就是 8 × 2048 × 16 × 8 = 2.1M token 每步。
学习率调度用余弦退火加线性预热。预热步数设总步数的 1% 到 2%,比如总步数 100K,预热 1K 到 2K 步。预热太长浪费计算,太短容易早期发散。我试过预热 500 步的配置,在前 200 步损失波动很大,后来改成 2000 步就稳定了。最终学习率降到峰值的 10%,不要降到 0,留一点学习率有助于后期微调。
4.4 训练循环与检查点管理
训练循环的核心是稳定和可恢复。我习惯每 500 步存一次检查点,包括模型权重、优化器状态、学习率调度器状态和随机数种子。这样即使训练中断,也能从最近的检查点恢复,不会浪费太多计算。检查点存的时候要注意,优化器状态可能比模型本身还大,1.3B 模型的 Lion 优化器状态约 2.6GB,加上模型权重 5.2GB,一个检查点约 8GB。如果磁盘空间有限,可以只存模型权重,恢复时重新初始化优化器,但这样会损失一些训练连续性。
日志记录要详细。我一般记录训练损失、验证损失、学习率、梯度范数、吞吐量(token/s)、显存占用。梯度范数特别重要,如果突然变大,说明可能有异常样本或数值不稳定。我设的阈值是梯度范数超过 1.0 就裁剪,正常情况应该在 0.1 到 0.5 之间。如果持续偏高,可能是学习率太大或初始化有问题。
验证集我一般从训练数据里留出 0.5% 到 1%,每 2000 步评估一次。验证损失如果连续三次不下降,就降低学习率或提前停止。轻量级预训练不需要跑太多 epoch,通常 1 到 2 个 epoch 就够了。我跑过 3 个 epoch 的配置,第三个 epoch 验证损失基本不降了,说明模型容量已经饱和,再训就是浪费。
混合精度训练用 bf16 而不是 fp16。bf16 的动态范围更大,不容易出现梯度下溢。PyTorch 的 autocast 配合 GradScaler 可以自动处理,但 bf16 其实不需要 GradScaler,因为它的指数位和 fp32 一样多。我实测 bf16 的训练稳定性和 fp32 差不多,但显存占用减少约 40%,速度提升约 30%。唯一要注意的是,某些操作在 bf16 下精度不够,比如 softmax 和层归一化,这些地方要强制用 fp32。
5. 常见问题与排查技巧实录
5.1 损失不下降或下降太慢
这是最常见的问题。我遇到过的原因有几种:学习率太小、数据质量差、初始化有问题、梯度被裁剪得太狠。排查顺序是:先看梯度范数,如果很小(小于 0.01),说明梯度信号弱,可能是学习率太小或初始化范围太小。我一般把初始化范围从 0.02 调到 0.05 试试,或者把学习率提高 3 到 5 倍。
如果梯度范数正常但损失不降,检查数据。我踩过的坑是数据里有大量重复或空白 token,模型很快学会预测这些,但验证集上表现差。用 tokenizer 解码几个样本看看,如果发现大量无意义内容,就得重新清洗数据。另一个可能是数据顺序有问题,比如所有同类数据聚在一起,模型会过拟合到某个领域。我一般会在训练前打乱数据,并且用多个数据源的混合。
还有一种情况是损失先降后升,这是典型的学习率过大。把学习率降一半再试,或者加长预热。我试过预热 500 步的配置,前 1000 步损失波动很大,后来改成 2000 步就稳定了。
5.2 显存溢出(OOM)
轻量级预训练也会 OOM,尤其是序列长度大的时候。排查思路是:先看是哪个操作 OOM,是前向、反向还是优化器更新。如果是前向 OOM,减少批量大小或序列长度。如果是反向 OOM,开梯度检查点。如果是优化器更新 OOM,换更省显存的优化器或开 ZeRO 分片。
梯度检查点是个好东西,能把激活值显存从 O(层数) 降到 O(1),代价是反向传播时重新计算前向,时间增加约 30%。我一般在显存不够时优先开这个,因为 30% 的时间换 50% 以上的显存节省很划算。但要注意,梯度检查点对某些操作不支持,比如自定义的 CUDA 核,开了会报错。这时候要么改操作,要么换别的省显存方法。
另一个省显存的技巧是及时释放中间变量。PyTorch 的自动求导会保留计算图,如果某些中间变量不需要梯度,用torch.no_grad()包起来。我习惯在验证和日志计算时用no_grad,能省不少显存。还有就是把优化器状态从 fp32 转成 bf16,Lion 优化器支持这个,显存直接减半,对训练效果影响很小。
5.3 训练速度慢
速度慢的原因可能是数据加载、计算瓶颈或通信瓶颈。先看 GPU 利用率,如果低于 80%,说明有瓶颈。用nvidia-smi看 GPU 利用率,用htop看 CPU 利用率。如果 CPU 利用率高,说明数据加载是瓶颈,增加 dataloader 的 worker 数量,或者提前 tokenize 数据。如果 GPU 利用率高但吞吐量低,说明计算本身慢,检查是否开了 FlashAttention,是否用了 bf16,批量大小是否够大。
通信瓶颈在多卡训练时常见。如果用的是 DDP,梯度同步是 all-reduce,通信量正比于参数量。1.3B 模型每步 all-reduce 约 5.2GB 数据,如果网络带宽不够,就会拖慢训练。我一般用 NCCL 的 ring all-reduce,配合梯度累积减少同步频率。梯度累积 16 步意味着每 16 步才同步一次,通信开销降到 1/16。但梯度累积会增加显存,因为要保留多个微批的梯度,这个取舍要看显存和带宽哪个更紧张。
还有一个容易忽略的点是日志记录。如果每步都写 wandb 或 tensorboard,IO 会成为瓶颈。我一般每 10 到 50 步记录一次,验证集评估每 2000 步一次。日志本身的开销不大,但频繁的磁盘写入会打断训练节奏。
5.4 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 损失不下降 | 学习率太小、数据差、初始化问题 | 看梯度范数、解码数据样本 | 调大学习率、清洗数据、调大初始化范围 |
| 损失先降后升 | 学习率过大、预热太短 | 看损失曲线拐点 | 降学习率、加长预热 |
| 显存溢出 | 批量太大、序列太长、优化器状态大 | 看 OOM 时的操作 | 减批量、开梯度检查点、换优化器 |
| 训练速度慢 | 数据加载瓶颈、通信瓶颈、未开 FlashAttention | 看 GPU 和 CPU 利用率 | 加 dataloader worker、梯度累积、开 FlashAttention |
| 梯度范数异常 | 异常样本、数值不稳定、学习率太大 | 看梯度范数曲线 | 梯度裁剪、降学习率、检查数据 |
| 验证损失不降 | 过拟合、容量饱和、验证集分布不同 | 对比训练和验证损失 | 加 dropout、提前停止、检查验证集 |
5.5 独家避坑技巧
第一个技巧是“小规模试跑”。在正式训练前,用 1% 的数据跑 100 步,看损失是否正常下降,显存是否够用,速度是否可接受。这个试跑能发现 80% 的配置问题,省下大量时间。我一般用 100M token 的数据跑 500 步,大概 10 分钟就能跑完,足够判断配置是否合理。
第二个技巧是“检查点回滚”。如果训练中途损失突然变差,不要硬着头皮继续,回滚到上一个检查点,降低学习率再试。我遇到过几次损失突然飙升,回滚后降学习率就恢复了。硬继续的话,模型可能进入一个坏的区域,再也回不来。
第三个技巧是“数据采样检查”。训练前随机抽 100 个样本,人工看一下。我踩过的坑是数据里有大量重复的模板文本,模型很快学会预测这些模板,但泛化能力差。人工检查能发现自动清洗漏掉的问题。另外,检查 tokenizer 的编码结果,看看有没有异常的分割,比如把常见词切成很多子词,这会降低训练效率。
第四个技巧是“梯度累积与批量大小的平衡”。批量大小影响训练稳定性,太小梯度噪声大,太大泛化差。我一般把有效批量控制在 2M 到 4M token,用梯度累积凑。但梯度累积不是越多越好,累积步数太多会导致优化器更新频率低,收敛慢。我试过累积 64 步的配置,收敛速度比累积 16 步慢了近一倍。所以要在显存允许的情况下,尽量用大的微批,减少累积步数。
6. 轻量级预训练架构的扩展与变体
6.1 从 1B 到 3B 的平滑扩展
1B 到 3B 是一个常见的扩展区间。这个区间里,架构不需要大改,但参数分配要调整。我的经验是,层数从 24 加到 32,隐藏维度从 2048 加到 2560,FFN 中间维度从 5632 加到 6912,头数从 16 加到 20。这样参数量约 2.8B,显存需求约 45GB(bf16 加 Lion),单张 80GB 卡能跑,或者两张 24GB 卡用 ZeRO 分片。
扩展时要注意学习率的调整。参数量增加后,最优学习率通常会降低。我一般按参数量的平方根反比来调,1B 用 3e-4,3B 用 1.7e-4 左右。批量大小可以适当增加,因为大模型对梯度噪声的容忍度更高。我试过 3B 模型用 4M token 批量,训练很稳定。
另一个扩展方向是序列长度。从 2048 扩到 4096 或 8192,计算量是平方增长,但轻量级架构可以通过局部注意力控制增长。我一般把一半的层换成局部窗口注意力,窗口 1024,另一半保持全局。这样 8192 序列的计算量约等于全注意力 4096 序列的 1.5 倍,显存增长也在可接受范围。
6.2 领域适配的轻量级预训练
通用预训练之后,往往需要领域适配。轻量级架构在领域适配上有优势,因为参数量少,微调成本低。我一般用继续预训练的方式,在领域数据上再跑 1 到 2 个 epoch,学习率降到预训练的十分之一。领域数据不需要太多,1B 到 2B token 通常够用。
领域适配的关键是数据配比。我一般保留 10% 到 20% 的通用数据,防止灾难性遗忘。纯领域数据训练会导致模型在通用任务上掉点严重。我试过纯领域数据继续预训练,通用困惑度上升了 15%,加了 15% 通用数据后,只上升了 3%。这个配比要根据领域和通用的差距来调,差距越大,通用数据比例越高。
还有一个技巧是分层学习率。浅层学通用特征,学习率设小一点;深层学领域特征,学习率设大一点。我一般浅层用预训练学习率的 0.1 倍,深层用 0.5 倍。这个改动在领域适配时效果明显,比统一学习率的下游指标高 2% 到 3%。
6.3 推理部署的轻量化考量
预训练架构的设计会影响推理部署。轻量级模型在推理时,显存占用和延迟都是关键指标。我一般会在预训练阶段就考虑推理优化,比如用 GQA 减少键值缓存,用 RMSNorm 减少计算,用 SwiGLU 替代标准 FFN 提升表达力。
SwiGLU 是 FFN 的一个变体,用门控机制控制信息流。它的参数量比标准 FFN 多 50%,但表达力更强。在轻量级模型上,我倾向于用 SwiGLU,因为参数量增加不多,但效果提升明显。我实测过 1B 模型用 SwiGLU,下游任务指标比标准 FFN 高约 4%,推理延迟只增加 10% 左右。
量化是推理部署的另一个关键。轻量级模型对量化更友好,因为参数量少,量化误差的影响相对小。我一般用 int8 量化,显存减半,速度提升 30% 到 50%,精度损失在 1% 以内。如果对精度要求高,可以用 int4 量化加 LoRA 微调,精度基本无损,但需要额外的微调步骤。
最后再分享一个小技巧:预训练完成后,不要直接拿最终检查点做下游任务,用最后几个检查点的平均权重往往更好。我试过对最后 5 个检查点做权重平均,下游指标比单个检查点高 1% 到 2%。这个技巧叫模型汤,在轻量级模型上效果尤其明显,因为轻量级模型的损失曲面更平滑,不同检查点之间的差异更小。