如果你最近在调大模型,八成已经碰到过同一个尴尬:代码写得挺顺手,一跑全参微调,显存直接报警。于是大家开始转向 LoRA、QLoRA、梯度检查点这些外围功夫,很少有人去质疑反向传播本身是不是微调唯一的路。2025 年 NeurIPS 上有一篇标题为 PaZO 的工作——Preconditioned Accelerated Zeroth-Order Optimization for Fine-Tuning LLMs——把零阶优化这个古老思路重新拉回大模型微调的前台。零阶优化(Zeroth-Order Optimization)的意思是:不计算梯度,只靠目标函数的值来估计下降方向。PaZO 在零阶优化之上又叠加了预条件(Preconditioning)和加速(Acceleration)两块机制,直接针对零阶方法过去收敛慢、怕病态问题的短板。这篇文章从一个从业者的角度,把我对这条技术路线的理解、公式直觉、落地实现和一些调参经验整理出来,给正在为显存发愁或者想做低资源微调的读者一个可以顺畅上手的参考。1. 反向传播的隐形成本,比大多数人以为的更高1.1 我们在为“梯度”付三笔账微调一个 7B 模型,很多人只盯着一张显卡的显存数字,却很少去拆这张账到底花在哪。以 BF16 全参微调为例,参数本身占 14GB,这只是一部分;训练过程中优化器状态又要吃掉一大块,Adam 需要为每个参数维护一阶矩和二阶矩,通常按 32 位浮点存,又是 28GB;再加上反向传播要算出来的梯度,14GB;前向过程中为了反传保存的激活值,这个量随着序列长度和 batch size 线性往上飙。七七八八加起来,56GB 只是起步价,长序列场景下 80GB 也不一定够。但这三笔账里真正“刚性”的,其实是反向传播链路本身。你一旦决定用一阶优化器,就必须存在从 loss 到每一层参数的梯度回传路径,就必须保存中间激活,就必须在每一步更新时读取并更新优化器状态。这套机制在稠密小模型上运行了几十年,没什么问题;到了大模型时代,它成了瓶颈。零阶优化第一个吸引人的地方就在这里:它直接绕开了三笔账中的后两笔。推理只需要前向传播拿到 loss 值,不需要 backward,不需要保存中间激活,也不需要维护 Adam 状态。等于把大部分显存预算从“训练开销”里挪出来,还给模型本身。1.2 零阶优化不是新东西,但在 LLM 时代有了新身份零阶优化的历史很长,早年主要用在黑箱优化、仿真调参、对抗样本生成这些场景里。它的核心逻辑很简单:你不知道梯度的解析表达式,就通过采样函数值来“猜”梯度方向。最基本的做法是两点估计:对参数加一个微小的扰动,计算扰动前后的 loss,用两者的差除以扰动幅度,得到一个沿随机方向的梯度近似。在大模型之前,这套做法给人最大的印象就是慢。每个参数维度都要采样,或者至少每个随机方向都要跑一遍完整前向,成本高得吓人。但大模型微调的场景出现后,游戏规则变了:反向传播省下来的显存和内存带宽,可以换来更多次前向传播;而前向传播是纯计算密集的,分布式并行、算子融合、FlashAttention 这些优化成熟之后,单次前向的开销已经降到非常低的水平。PaZO 正是踩在这个平衡点上。它名字里的三个限定词——Preconditioned(预条件)、Accelerated(加速)、Zeroth-Order(零阶)——分别对应三个问题:零阶解决显存,预条件解决收敛效率,加速解决步数。理解了这三者的组合逻辑,才算真正看懂了这篇工作。2. PaZO 拆解:预条件和加速分别补上了什么短板2.1 零阶优化的心头痛:梯度噪声与病态条件数零阶优化最被人诟病的,是它估计出来的梯度带噪声。两点估计的误差和扰动步长 δ 直接相关:δ 太小,函数值差分被浮点精度淹没;δ 太大,有限差分近似偏离真实梯度。这个噪声会让收敛曲线变得毛躁,但还只是表面问题。真正要命的是条件数。一阶方法里,SGD 的收敛速度强烈依赖于 Hessian 的条件数——如果损失函数在不同方向上的曲率差异很大,梯度下降会走锯齿路径,每一步都在来回震荡,步长只能取最小曲率方向允许的上限。这个问题在零阶方法里会被放大:你的梯度本身是估计的,方向误差叠加上病态几何,会让锯齿效应更严重。这就是为什么普通零阶方法在小模型玩具任务上看着还行,一到大模型这种动辄上亿参数、各向异性严重的损失曲面就彻底拉胯。PaZO 要做的第一件事,就是用预条件来纠正这种几何上的不平衡。2.2 预条件的几何直觉:把椭圆拉回正圆预条件这个概念,很多人第一次接触是在数值线性代数里。解线性方程组时,如果系数矩阵条件数太大,直接迭代会收敛得很慢,于是你会左乘一个矩阵的逆,把特征值分布压到 1 附近,再去做迭代。放到优化里是一样的逻辑。损失函数等值线是一个高维椭球,预条件矩阵的作用就是做一个线性变换,让这个椭球尽量变成正球。通俗点说:原来有的方向陡峭、有的方向平缓,步长照顾平缓方向就会被陡峭方向害死;预条件之后,每个方向都按各自的尺度缩放,更新方向自然就正了。在深度学习里,Adam 其实已经隐含了对角预条件:它对每个参数的历史梯度平方做指数平均,然后按这个量归一化更新步长。PaZO 的思路延续了这种思想,但把它明确地设计成零阶优化里的预条件模块:用有限的函数值估计信息,构建出近似 Hessian 对角或者低秩逼近,再把它应用到梯度估计上。2.3 加速:除了去掉锯齿,还要抄近路预条件解决了“方向对不对”的问题,加速(Acceleration)解决的是“步长能不能更大、路径能不能更直”的问题。加速方法的核心不是简单地增大学习率,而是引入动量或者更高阶的路径修正,把历史梯度方向的信息利用起来。零阶优化里加速尤其重要,因为每一步的方向都有噪声。如果只按当前估计走,噪声会被直接放大;如果加入动量机制,噪声在时间维度上互相抵消,真实下降方向被累积增强。PaZO 的“Accelerated”大概率走的也是这个思路,把 Nesterov 型加速或者重球法(Heavy Ball)的思想移植到零阶框架里,让每次更新既用当前梯度估计,也用前面的运动趋势。这里有一个细节值得注意:一阶方法里的动量实现非常简单,因为梯度是准确的;零阶方法里动量有效性的前提是“梯度估计的无偏性”。如果两点估计是有偏的,动量反而会把偏差累积起来。所以 PaZO 在加速模块前面一定要配非常谨慎的梯度估计器设计,这正是它和早期纯零阶方法拉开差距的地方。3. 从公式直觉到工程落地:一个可操作的 PaZO 实现框架3.1 梯度估计器:几种方案怎么选零阶梯度估计的核心模块,常见的有三类:坐标梯度估计:每次只扰动一个参数维度,得到该维度的偏导近似。方差小,但需要 O(d) 次前向,维度灾难严重,大模型下不现实。随机方向两点估计:每次用一个随机方向扰动全部参数,两次前向得到标量差,一除就得到一个方向上的投影。方差大,但每次只需要两次前向,是当前主流。随机方向多点估计:用多个随机方向做平均,方差随采样数线性下降,但前向次数也线性增加。PaZO 这类面向 LLM 的工作,几乎必然选第二种或者第三种的折中。单步两次前向听起来很贵,但配合预条件下一次方向质量不够好,多采样几个方向再平均,往往比增加几倍步数更划算。实操中可以在 2 到 16 之间调节采样数,一般 4 到 8 是一个性价比很高的区间。这里还要提一个工程细节:随机方向向量的生成和存储。如果是 7B 模型,一个随机方向向量本身就是 7B 个浮点数,生成用正态分布随机数,存储需要十几 GB。所以实现时通常用种子生成器,每次采样方向时从同一个种子恢复,避免存整个方向向量,只存种子状态。3.2 预条件矩阵的构造:对角近似最现实LLM 场景下构造完整预条件矩阵不现实,7B 乘 7B 的矩阵光存储就超出预算。可行的近似包括:对角近似:从过去几次梯度估计中累计二阶信息,类似于 RMSProp 的二阶矩。每个参数一个标量,内存 O(d)。分块低秩:把参数划分成若干块,每块用低秩矩阵近似块内曲率。内存 O(dr),适合小规模测试。结构预条件:按 Transformer 的结构分层处理,比如 attention 和 MLP 分别统计曲率尺度。对标现有的生态,Adafactor 的分层二阶矩做法可以直接借鉴过来做预条件:对不同层给不同的学习率缩放,相当于层级别的对角预条件。PaZO 工作里大概率用的是对角或层结构预条件,这个选择既照顾了内存,也抓住了 LLM 损失曲面里最主要的不平衡来源——层间尺度差异。3.3 伪代码:一个能在现有框架里魔改的雏形把 PaZO 的想法落成可运行流程,核心只有幾步:import torch def pazo_step(model, loss_fn, params, state, lr=5e-4, eps=1e-6): """ 简易 PaZO 风格更新。 state 维护动量 m 和预条件二阶信息 v。 """ seed = torch.Generator(device=params[0].device).seed() def perturb_and_loss(alpha): with torch.no_grad(): for p, direction_cache in zip(params, state["directions"]): p.add_(direction_cache, alpha=alpha) loss = loss_fn() for p, direction_cache in zip(params, state["directions"]): p.add_(direction_cache, alpha=-alpha) state["directions"] = [] for p in params: d = torch.randn_like(p, generator=seed) d.div_(torch.linalg.vector_norm(d, keepdim=True).clamp_min(1e-12)) state["directions"].append(d) f_plus = None with torch.no_grad(): for p, d in zip(params, state["directions"]): p.add_(d, alpha=eps) f_plus = loss_fn() for p, d in zip(params, state["directions"]): p.sub_(d, alpha=eps) f_minus = loss_fn() # 方向梯度估计(标量投影) g_proj = (f_plus - f_minus) / (2 * eps) # 把标量投影转成参数梯度估计 grads = [d * g_proj for d in state["directions"]] # 预条件 + 加速(简化为重球动量) for p, g, d, m in zip(params, grads, state["directions"], state["momentum"]): v = state["v"][id(p)] v = 0.9 * v + (1 - 0.9) * g.pow(2) state["v"][id(p)] = v m.mul_(0.9).add_(g) update = m / torch.sqrt(v + 1e-8) p.add_(update, alpha=-lr) return f_minus这段代码离完整实现还有距离,但骨架是齐的:一次方向采样对应两次前向,动量提供加速,v 提供对角预条件。实际部署时,loss_fn 需要自己实现为只返回标量 loss,不维护任何计算图,否则又走回反向传播的老路。这是零阶微调最需要注意的一点:一定不要在你采样前向时构建计算图,所有扰动都必须在 no_grad 环境下进行。3.4 与现有微调生态怎么整合Lora、PEFT、HuggingFace Transformers 这些库默认假设你要反向传播,所以把它们应用到零阶优化时要做一点适配。最简单的做法是:把可训练参数包成一个扁平化列表,loss_fn 里手动跑模型的前向计算 loss,然后只调用我上面写的 pazo_step,不调用 loss.backward()。我实际试下来,这个过程最烦的是模型的输出层和损失函数那一堆细节,例如语言模型的 logits 怎么展开、cross entropy 是在显存里算还是 CPU 上算、中间结果是不是要 detach。建议第一次写的时候先把输入 batch 缩小到单条,跑通数值检查再放大,否则很容易踩到“某个算子自动构建了计算图”的坑。4. 显存、通信与迭代代价:到底什么时候该用 PaZO4.1 记账本:同样一个模型,钱花在哪不一样下面这张表是经验估算,具体数字会根据实现而浮动,但结构是对的。方案参数+梯度优化器状态激活/中间量每次迭代前向次数全参 Adam参数+梯度(约双倍参数量)2 或 4 倍参数量较大,依赖序列和 batch1 次 backward 附带额外前向LoRA极小可训练参数极小仍要反向传播,激活省不了1 次完整 forward+backward零阶(PaZO 风格)只有参数本身对角预条件约 1 倍参数量几乎可以清空,只要 loss 标量2 次纯 forward零阶方案的一大优势是激活值几乎可以被清零:你不需要保存任何中间张量来反传,只要拿到 loss 标量,中间层的结果立刻可以释放。这在大 batch、长序列场景下节省的显存是惊人的。另一个优势是优化器状态大幅压缩:对角的二阶矩只需要和模型参数同等规模,比 Adam 的 2 到 4 倍少了不止一半。4.2 适合 PaZO 的三个典型场景第一个场景,显存有限但推理显存已够。很多研究团队只有单张 24GB 或 32GB 的卡,跑不动 7B 全参微调,但跑得动 7B 推理。PaZO 这类零阶方式可以让你在“只多一张卡”的条件下完成微调,而不是被迫换卡或者强行量化。第二个场景,批量微调多个模型。如果运维或者说团队手里有一批模型要反复做指令微调、风格微调,且每个模型都只有几小时生命周期,零阶方式因为不需要维护庞大的 Adam 状态,切换任务时几乎没有额外负担。第三个场景,模型权重不可导或接口黑盒化。有些模型经过编译优化、量化、或者封装成 API,内部结构不受你控制,你能拿到的只有输入和输出。这时候零阶方法是唯一还能做优化的路径,可以用同样的逻辑去调 prompt、调权重的外部化版本,甚至做系统级调参。4.3 和 LoRA、QLoRA 不是替代关系,是不同维度的取舍很多读者会问:有 LoRA 为什么还要搞零阶?答案很简单,LoRA 是在参数维度做降维,PaZO 是在梯度获取方式上做替换,两者并不互斥。你可以先取 LoRA 低秩化,再用零阶优化去更新 LoRA 参数;也可以反过来,对全量参数用零阶优化,但只在某些层上实施预条件。PaZO 最有价值的一点,其实是它把微调从“必须逆向传播”的框架里解放出来。假设未来模型权重通过 API 发布或者所有权受到保护,微调就只能在外面做零阶优化。PaZO 这条路线实际上是在为一个更开放、更灵活的生态布局。5. 实测体验与参数调优笔记:稳定性和收敛速度两手抓5.1 学习率与扰动量需要联合调整零阶优化最核心的两个超参数,一个是学习率,一个是扰动步长 eps。这里有个关键经验:学习率和扰动步长并不是独立的。eps 太小,数值差分会因为浮点误差失效;eps 太大,梯度估计会凭空引入巨大偏差。而学习率直接放大了梯度估计中的噪声。在实践中,我建议用这样一组初始值开始,再做自适应调整:eve 初始范围:1e-4 到 1e-2 之间,建议从 1e-3 起步做 log 扫描学习率:比对应的 Adam 场景低三分之一到一半,因为动量累积后实际步长会变大方向采样数:先用 4,看梯度估计方向的方差是否可接受,再往 8 或 16 调预条件二阶矩的衰减率:β 取 0.99 到 0.999 之间,偏向 0.99 让近期信息占主导一次实验中我发现,当 eps 从 1e-3 调到 1e-4 时,loss 曲线从剧烈震荡变成平缓下降,原因是前者已经偏离线性近似区间。这个问题在 PyTorch 的 float32 下尤其明显,如果模型参数是 float16 或 bfloat16,eps 还要继续调大,保证差分值不被舍入噪声吃掉。5.2 往 8 或 16 方向采样前,先确认批量大小的收益零阶方向估计的另一个特点是:它会放大 batch 内部的噪声。一阶梯度天然是在一个 batch 上平均了所有样本的梯度,零阶梯度则是用一个 batch 的 loss 差分去近似,batch 太小,loss 的波动会被误当成下降方向。所以调参时优先增大 batch size,而不是增加采样方向。我在一个中等规模任务上的经验是:batch size 提高一倍,效果往往比方向数提高一倍更明显。原因也很简单:前向采样次数是线性成本,batch size 在同样次数的前向下可以摊平更多样本噪声,物美价廉。5.3 常见失败模式与对应的排查路径如果 loss 完全不下降,先检查方向扰动有没有真的动到参数。有人把扰动加到 derivative 张量上,忘了 actually 更新前向计算的参数副本,结果伪装跑了两次前向,扰动根本没进去。如果 loss 开始下降但剧烈震荡,优先怀疑学习率太高或者扰动步长太大。一个靠谱的稳定技巧是:对最终更新方向做一个梯度裁剪,或者用余弦退火把学习率压在低位,都可以有效缓解。如果初期表现很好、训练后期卡住,多半是预条件矩阵算得有问题。预条件本质是环境感知的自适应方法,后期损失曲面更平坦时,V 里的二阶矩累计值可能过大,导致更新步长被压缩。可以周期性重置预条件状态,或者对 V 做 max 截断,避免它膨胀到夸张级别。5.4 对比观察:PaZO 风格方法的实际速度观感很多人在跑零阶之前最担心的是“每次迭代两次前向是不是太慢”。事实是,两次前向在单卡上大约是把时间翻倍之后还多一点,但如果你原本因为显存不够,从 8 batch 被迫降到 2 batch,那零阶反而可能是赢的。反向传播本身的启动开销不算贵,贵的是内存换页和显存不足时的重计算。PaZO 把重计算彻底消灭掉,换来的是更多的原始前向运算,一进一出,在很多配置上并没有变慢多少,而能操作的模型规模明显变大。6. 零阶微调的上限在哪里:几个值得继续深挖的方向6.1 跨卡并行下的零阶优化PaZO 目前可以理解的实现方式是单卡或简单数据并行,但它的真正威力会在多卡并行下进一步释放。因为零阶优化不需要同步梯度和激活,天然省掉了一部分通信开销。跨卡场景下,每张卡可以独立采样不同的扰动方向、独立计算函数值,最后只汇总一小组标量和方向参数。这个特性非常适合大规模蒸馏、模型融合或联邦式的微调。6.2 与解码阶段工作的交叉零阶优化不一定只用在训练阶段。大模型推理时,我们可以把它用来做上下文优化:给定一组验证样本,调整 prefix 向量、系统提示、甚至采样参数,目标是让输出 quality 更好。这个场景下模型不可导,又没有训练流程去维护一堆状态,零阶加预条件的方法显得非常顺手。6.3 从“预条件加速零阶”到“结构感知零阶”PaZO 的意义可能不在它单个算法上,而在它指明了一个方向:纯黑箱优化能够利用结构信息,也可以做得跟白箱优化一样高效。后续工作完全可以沿着这个思路做分级:对不同层用不同的扰动尺度,对 attention 和 MLP 用结构化的预条件,甚至把 RL 里的奖励模型信号当成零阶反馈来用。这些都不是天方夜谭,只是需要有人先把调试痛苦吃一遍。最后我个人的体会是,零阶优化调起来确实比 Adam 系列要烦一些,需要时刻留意扰动与数值类型交互的细节,但换来的是对整个模型训练过程更强的掌控感。你不再需要把模型塞进一层层 backward graph 里去求梯度,而是用一个简单的 loss 信号去直接推动权重,这种直觉上的直观感,是很适合做原型验证和教学演示的。如果你手头正好有显存捉襟见肘的微调任务,建议下个周末花几个小时把上面这套 PaZO 风格更新换成一个小实验跑一跑,看看你的任务在零阶条件下到底能走多远。
如果你最近在调大模型,八成已经碰到过同一个尴尬:代码写得挺顺手,一跑全参微调,显存直接报警。于是大家开始转向 LoRA、QLoRA、梯度检查点这些外围功夫,很少有人去质疑反向传播本身是不是微调唯一的路。2025 年 NeurIPS 上有一篇标题为 PaZO 的工作——Preconditioned Accelerated Zeroth-Order Optimization for Fine-Tuning LLMs——把零阶优化这个古老思路重新拉回大模型微调的前台。零阶优化(Zeroth-Order Optimization)的意思是:不计算梯度,只靠目标函数的值来估计下降方向。PaZO 在零阶优化之上又叠加了预条件(Preconditioning)和加速(Acceleration)两块机制,直接针对零阶方法过去收敛慢、怕病态问题的短板。这篇文章从一个从业者的角度,把我对这条技术路线的理解、公式直觉、落地实现和一些调参经验整理出来,给正在为显存发愁或者想做低资源微调的读者一个可以顺畅上手的参考。1. 反向传播的隐形成本,比大多数人以为的更高1.1 我们在为“梯度”付三笔账微调一个 7B 模型,很多人只盯着一张显卡的显存数字,却很少去拆这张账到底花在哪。以 BF16 全参微调为例,参数本身占 14GB,这只是一部分;训练过程中优化器状态又要吃掉一大块,Adam 需要为每个参数维护一阶矩和二阶矩,通常按 32 位浮点存,又是 28GB;再加上反向传播要算出来的梯度,14GB;前向过程中为了反传保存的激活值,这个量随着序列长度和 batch size 线性往上飙。七七八八加起来,56GB 只是起步价,长序列场景下 80GB 也不一定够。但这三笔账里真正“刚性”的,其实是反向传播链路本身。你一旦决定用一阶优化器,就必须存在从 loss 到每一层参数的梯度回传路径,就必须保存中间激活,就必须在每一步更新时读取并更新优化器状态。这套机制在稠密小模型上运行了几十年,没什么问题;到了大模型时代,它成了瓶颈。零阶优化第一个吸引人的地方就在这里:它直接绕开了三笔账中的后两笔。推理只需要前向传播拿到 loss 值,不需要 backward,不需要保存中间激活,也不需要维护 Adam 状态。等于把大部分显存预算从“训练开销”里挪出来,还给模型本身。1.2 零阶优化不是新东西,但在 LLM 时代有了新身份零阶优化的历史很长,早年主要用在黑箱优化、仿真调参、对抗样本生成这些场景里。它的核心逻辑很简单:你不知道梯度的解析表达式,就通过采样函数值来“猜”梯度方向。最基本的做法是两点估计:对参数加一个微小的扰动,计算扰动前后的 loss,用两者的差除以扰动幅度,得到一个沿随机方向的梯度近似。在大模型之前,这套做法给人最大的印象就是慢。每个参数维度都要采样,或者至少每个随机方向都要跑一遍完整前向,成本高得吓人。但大模型微调的场景出现后,游戏规则变了:反向传播省下来的显存和内存带宽,可以换来更多次前向传播;而前向传播是纯计算密集的,分布式并行、算子融合、FlashAttention 这些优化成熟之后,单次前向的开销已经降到非常低的水平。PaZO 正是踩在这个平衡点上。它名字里的三个限定词——Preconditioned(预条件)、Accelerated(加速)、Zeroth-Order(零阶)——分别对应三个问题:零阶解决显存,预条件解决收敛效率,加速解决步数。理解了这三者的组合逻辑,才算真正看懂了这篇工作。2. PaZO 拆解:预条件和加速分别补上了什么短板2.1 零阶优化的心头痛:梯度噪声与病态条件数零阶优化最被人诟病的,是它估计出来的梯度带噪声。两点估计的误差和扰动步长 δ 直接相关:δ 太小,函数值差分被浮点精度淹没;δ 太大,有限差分近似偏离真实梯度。这个噪声会让收敛曲线变得毛躁,但还只是表面问题。真正要命的是条件数。一阶方法里,SGD 的收敛速度强烈依赖于 Hessian 的条件数——如果损失函数在不同方向上的曲率差异很大,梯度下降会走锯齿路径,每一步都在来回震荡,步长只能取最小曲率方向允许的上限。这个问题在零阶方法里会被放大:你的梯度本身是估计的,方向误差叠加上病态几何,会让锯齿效应更严重。这就是为什么普通零阶方法在小模型玩具任务上看着还行,一到大模型这种动辄上亿参数、各向异性严重的损失曲面就彻底拉胯。PaZO 要做的第一件事,就是用预条件来纠正这种几何上的不平衡。2.2 预条件的几何直觉:把椭圆拉回正圆预条件这个概念,很多人第一次接触是在数值线性代数里。解线性方程组时,如果系数矩阵条件数太大,直接迭代会收敛得很慢,于是你会左乘一个矩阵的逆,把特征值分布压到 1 附近,再去做迭代。放到优化里是一样的逻辑。损失函数等值线是一个高维椭球,预条件矩阵的作用就是做一个线性变换,让这个椭球尽量变成正球。通俗点说:原来有的方向陡峭、有的方向平缓,步长照顾平缓方向就会被陡峭方向害死;预条件之后,每个方向都按各自的尺度缩放,更新方向自然就正了。在深度学习里,Adam 其实已经隐含了对角预条件:它对每个参数的历史梯度平方做指数平均,然后按这个量归一化更新步长。PaZO 的思路延续了这种思想,但把它明确地设计成零阶优化里的预条件模块:用有限的函数值估计信息,构建出近似 Hessian 对角或者低秩逼近,再把它应用到梯度估计上。2.3 加速:除了去掉锯齿,还要抄近路预条件解决了“方向对不对”的问题,加速(Acceleration)解决的是“步长能不能更大、路径能不能更直”的问题。加速方法的核心不是简单地增大学习率,而是引入动量或者更高阶的路径修正,把历史梯度方向的信息利用起来。零阶优化里加速尤其重要,因为每一步的方向都有噪声。如果只按当前估计走,噪声会被直接放大;如果加入动量机制,噪声在时间维度上互相抵消,真实下降方向被累积增强。PaZO 的“Accelerated”大概率走的也是这个思路,把 Nesterov 型加速或者重球法(Heavy Ball)的思想移植到零阶框架里,让每次更新既用当前梯度估计,也用前面的运动趋势。这里有一个细节值得注意:一阶方法里的动量实现非常简单,因为梯度是准确的;零阶方法里动量有效性的前提是“梯度估计的无偏性”。如果两点估计是有偏的,动量反而会把偏差累积起来。所以 PaZO 在加速模块前面一定要配非常谨慎的梯度估计器设计,这正是它和早期纯零阶方法拉开差距的地方。3. 从公式直觉到工程落地:一个可操作的 PaZO 实现框架3.1 梯度估计器:几种方案怎么选零阶梯度估计的核心模块,常见的有三类:坐标梯度估计:每次只扰动一个参数维度,得到该维度的偏导近似。方差小,但需要 O(d) 次前向,维度灾难严重,大模型下不现实。随机方向两点估计:每次用一个随机方向扰动全部参数,两次前向得到标量差,一除就得到一个方向上的投影。方差大,但每次只需要两次前向,是当前主流。随机方向多点估计:用多个随机方向做平均,方差随采样数线性下降,但前向次数也线性增加。PaZO 这类面向 LLM 的工作,几乎必然选第二种或者第三种的折中。单步两次前向听起来很贵,但配合预条件下一次方向质量不够好,多采样几个方向再平均,往往比增加几倍步数更划算。实操中可以在 2 到 16 之间调节采样数,一般 4 到 8 是一个性价比很高的区间。这里还要提一个工程细节:随机方向向量的生成和存储。如果是 7B 模型,一个随机方向向量本身就是 7B 个浮点数,生成用正态分布随机数,存储需要十几 GB。所以实现时通常用种子生成器,每次采样方向时从同一个种子恢复,避免存整个方向向量,只存种子状态。3.2 预条件矩阵的构造:对角近似最现实LLM 场景下构造完整预条件矩阵不现实,7B 乘 7B 的矩阵光存储就超出预算。可行的近似包括:对角近似:从过去几次梯度估计中累计二阶信息,类似于 RMSProp 的二阶矩。每个参数一个标量,内存 O(d)。分块低秩:把参数划分成若干块,每块用低秩矩阵近似块内曲率。内存 O(dr),适合小规模测试。结构预条件:按 Transformer 的结构分层处理,比如 attention 和 MLP 分别统计曲率尺度。对标现有的生态,Adafactor 的分层二阶矩做法可以直接借鉴过来做预条件:对不同层给不同的学习率缩放,相当于层级别的对角预条件。PaZO 工作里大概率用的是对角或层结构预条件,这个选择既照顾了内存,也抓住了 LLM 损失曲面里最主要的不平衡来源——层间尺度差异。3.3 伪代码:一个能在现有框架里魔改的雏形把 PaZO 的想法落成可运行流程,核心只有幾步:import torch def pazo_step(model, loss_fn, params, state, lr=5e-4, eps=1e-6): """ 简易 PaZO 风格更新。 state 维护动量 m 和预条件二阶信息 v。 """ seed = torch.Generator(device=params[0].device).seed() def perturb_and_loss(alpha): with torch.no_grad(): for p, direction_cache in zip(params, state["directions"]): p.add_(direction_cache, alpha=alpha) loss = loss_fn() for p, direction_cache in zip(params, state["directions"]): p.add_(direction_cache, alpha=-alpha) state["directions"] = [] for p in params: d = torch.randn_like(p, generator=seed) d.div_(torch.linalg.vector_norm(d, keepdim=True).clamp_min(1e-12)) state["directions"].append(d) f_plus = None with torch.no_grad(): for p, d in zip(params, state["directions"]): p.add_(d, alpha=eps) f_plus = loss_fn() for p, d in zip(params, state["directions"]): p.sub_(d, alpha=eps) f_minus = loss_fn() # 方向梯度估计(标量投影) g_proj = (f_plus - f_minus) / (2 * eps) # 把标量投影转成参数梯度估计 grads = [d * g_proj for d in state["directions"]] # 预条件 + 加速(简化为重球动量) for p, g, d, m in zip(params, grads, state["directions"], state["momentum"]): v = state["v"][id(p)] v = 0.9 * v + (1 - 0.9) * g.pow(2) state["v"][id(p)] = v m.mul_(0.9).add_(g) update = m / torch.sqrt(v + 1e-8) p.add_(update, alpha=-lr) return f_minus这段代码离完整实现还有距离,但骨架是齐的:一次方向采样对应两次前向,动量提供加速,v 提供对角预条件。实际部署时,loss_fn 需要自己实现为只返回标量 loss,不维护任何计算图,否则又走回反向传播的老路。这是零阶微调最需要注意的一点:一定不要在你采样前向时构建计算图,所有扰动都必须在 no_grad 环境下进行。3.4 与现有微调生态怎么整合Lora、PEFT、HuggingFace Transformers 这些库默认假设你要反向传播,所以把它们应用到零阶优化时要做一点适配。最简单的做法是:把可训练参数包成一个扁平化列表,loss_fn 里手动跑模型的前向计算 loss,然后只调用我上面写的 pazo_step,不调用 loss.backward()。我实际试下来,这个过程最烦的是模型的输出层和损失函数那一堆细节,例如语言模型的 logits 怎么展开、cross entropy 是在显存里算还是 CPU 上算、中间结果是不是要 detach。建议第一次写的时候先把输入 batch 缩小到单条,跑通数值检查再放大,否则很容易踩到“某个算子自动构建了计算图”的坑。4. 显存、通信与迭代代价:到底什么时候该用 PaZO4.1 记账本:同样一个模型,钱花在哪不一样下面这张表是经验估算,具体数字会根据实现而浮动,但结构是对的。方案参数+梯度优化器状态激活/中间量每次迭代前向次数全参 Adam参数+梯度(约双倍参数量)2 或 4 倍参数量较大,依赖序列和 batch1 次 backward 附带额外前向LoRA极小可训练参数极小仍要反向传播,激活省不了1 次完整 forward+backward零阶(PaZO 风格)只有参数本身对角预条件约 1 倍参数量几乎可以清空,只要 loss 标量2 次纯 forward零阶方案的一大优势是激活值几乎可以被清零:你不需要保存任何中间张量来反传,只要拿到 loss 标量,中间层的结果立刻可以释放。这在大 batch、长序列场景下节省的显存是惊人的。另一个优势是优化器状态大幅压缩:对角的二阶矩只需要和模型参数同等规模,比 Adam 的 2 到 4 倍少了不止一半。4.2 适合 PaZO 的三个典型场景第一个场景,显存有限但推理显存已够。很多研究团队只有单张 24GB 或 32GB 的卡,跑不动 7B 全参微调,但跑得动 7B 推理。PaZO 这类零阶方式可以让你在“只多一张卡”的条件下完成微调,而不是被迫换卡或者强行量化。第二个场景,批量微调多个模型。如果运维或者说团队手里有一批模型要反复做指令微调、风格微调,且每个模型都只有几小时生命周期,零阶方式因为不需要维护庞大的 Adam 状态,切换任务时几乎没有额外负担。第三个场景,模型权重不可导或接口黑盒化。有些模型经过编译优化、量化、或者封装成 API,内部结构不受你控制,你能拿到的只有输入和输出。这时候零阶方法是唯一还能做优化的路径,可以用同样的逻辑去调 prompt、调权重的外部化版本,甚至做系统级调参。4.3 和 LoRA、QLoRA 不是替代关系,是不同维度的取舍很多读者会问:有 LoRA 为什么还要搞零阶?答案很简单,LoRA 是在参数维度做降维,PaZO 是在梯度获取方式上做替换,两者并不互斥。你可以先取 LoRA 低秩化,再用零阶优化去更新 LoRA 参数;也可以反过来,对全量参数用零阶优化,但只在某些层上实施预条件。PaZO 最有价值的一点,其实是它把微调从“必须逆向传播”的框架里解放出来。假设未来模型权重通过 API 发布或者所有权受到保护,微调就只能在外面做零阶优化。PaZO 这条路线实际上是在为一个更开放、更灵活的生态布局。5. 实测体验与参数调优笔记:稳定性和收敛速度两手抓5.1 学习率与扰动量需要联合调整零阶优化最核心的两个超参数,一个是学习率,一个是扰动步长 eps。这里有个关键经验:学习率和扰动步长并不是独立的。eps 太小,数值差分会因为浮点误差失效;eps 太大,梯度估计会凭空引入巨大偏差。而学习率直接放大了梯度估计中的噪声。在实践中,我建议用这样一组初始值开始,再做自适应调整:eve 初始范围:1e-4 到 1e-2 之间,建议从 1e-3 起步做 log 扫描学习率:比对应的 Adam 场景低三分之一到一半,因为动量累积后实际步长会变大方向采样数:先用 4,看梯度估计方向的方差是否可接受,再往 8 或 16 调预条件二阶矩的衰减率:β 取 0.99 到 0.999 之间,偏向 0.99 让近期信息占主导一次实验中我发现,当 eps 从 1e-3 调到 1e-4 时,loss 曲线从剧烈震荡变成平缓下降,原因是前者已经偏离线性近似区间。这个问题在 PyTorch 的 float32 下尤其明显,如果模型参数是 float16 或 bfloat16,eps 还要继续调大,保证差分值不被舍入噪声吃掉。5.2 往 8 或 16 方向采样前,先确认批量大小的收益零阶方向估计的另一个特点是:它会放大 batch 内部的噪声。一阶梯度天然是在一个 batch 上平均了所有样本的梯度,零阶梯度则是用一个 batch 的 loss 差分去近似,batch 太小,loss 的波动会被误当成下降方向。所以调参时优先增大 batch size,而不是增加采样方向。我在一个中等规模任务上的经验是:batch size 提高一倍,效果往往比方向数提高一倍更明显。原因也很简单:前向采样次数是线性成本,batch size 在同样次数的前向下可以摊平更多样本噪声,物美价廉。5.3 常见失败模式与对应的排查路径如果 loss 完全不下降,先检查方向扰动有没有真的动到参数。有人把扰动加到 derivative 张量上,忘了 actually 更新前向计算的参数副本,结果伪装跑了两次前向,扰动根本没进去。如果 loss 开始下降但剧烈震荡,优先怀疑学习率太高或者扰动步长太大。一个靠谱的稳定技巧是:对最终更新方向做一个梯度裁剪,或者用余弦退火把学习率压在低位,都可以有效缓解。如果初期表现很好、训练后期卡住,多半是预条件矩阵算得有问题。预条件本质是环境感知的自适应方法,后期损失曲面更平坦时,V 里的二阶矩累计值可能过大,导致更新步长被压缩。可以周期性重置预条件状态,或者对 V 做 max 截断,避免它膨胀到夸张级别。5.4 对比观察:PaZO 风格方法的实际速度观感很多人在跑零阶之前最担心的是“每次迭代两次前向是不是太慢”。事实是,两次前向在单卡上大约是把时间翻倍之后还多一点,但如果你原本因为显存不够,从 8 batch 被迫降到 2 batch,那零阶反而可能是赢的。反向传播本身的启动开销不算贵,贵的是内存换页和显存不足时的重计算。PaZO 把重计算彻底消灭掉,换来的是更多的原始前向运算,一进一出,在很多配置上并没有变慢多少,而能操作的模型规模明显变大。6. 零阶微调的上限在哪里:几个值得继续深挖的方向6.1 跨卡并行下的零阶优化PaZO 目前可以理解的实现方式是单卡或简单数据并行,但它的真正威力会在多卡并行下进一步释放。因为零阶优化不需要同步梯度和激活,天然省掉了一部分通信开销。跨卡场景下,每张卡可以独立采样不同的扰动方向、独立计算函数值,最后只汇总一小组标量和方向参数。这个特性非常适合大规模蒸馏、模型融合或联邦式的微调。6.2 与解码阶段工作的交叉零阶优化不一定只用在训练阶段。大模型推理时,我们可以把它用来做上下文优化:给定一组验证样本,调整 prefix 向量、系统提示、甚至采样参数,目标是让输出 quality 更好。这个场景下模型不可导,又没有训练流程去维护一堆状态,零阶加预条件的方法显得非常顺手。6.3 从“预条件加速零阶”到“结构感知零阶”PaZO 的意义可能不在它单个算法上,而在它指明了一个方向:纯黑箱优化能够利用结构信息,也可以做得跟白箱优化一样高效。后续工作完全可以沿着这个思路做分级:对不同层用不同的扰动尺度,对 attention 和 MLP 用结构化的预条件,甚至把 RL 里的奖励模型信号当成零阶反馈来用。这些都不是天方夜谭,只是需要有人先把调试痛苦吃一遍。最后我个人的体会是,零阶优化调起来确实比 Adam 系列要烦一些,需要时刻留意扰动与数值类型交互的细节,但换来的是对整个模型训练过程更强的掌控感。你不再需要把模型塞进一层层 backward graph 里去求梯度,而是用一个简单的 loss 信号去直接推动权重,这种直觉上的直观感,是很适合做原型验证和教学演示的。如果你手头正好有显存捉襟见肘的微调任务,建议下个周末花几个小时把上面这套 PaZO 风格更新换成一个小实验跑一跑,看看你的任务在零阶条件下到底能走多远。