
【Bug已解决】Reviving #1807: Riemannian Preconditioned LoRA — coordination before drafting a PR 解决方案一、现象长什么样你想在 PEFT 里复活一个老提案 #1807给 LoRA 加上Riemannian Preconditioned黎曼预条件的优化方式——也就是不再在欧氏空间里直接更新低秩矩阵 A、B而是考虑它们所在的流形几何用 prerequisites 把梯度“扭”到合适的切空间里更新。但动手前issue 里说的核心问题是在开写 PR 之前先对齐要不要进核心库接口怎么设计和现有LoraConfig怎么共存于是卡点不是“代码怎么写”而是“怎么协调、避免 PR 写了又被打回”。本文把两件事都讲清Riemannian 预条件的数学直觉 提交 PR 前的协调清单。二、背景普通 LoRA 把权重更新写成ΔW B·A训练时优化A∈R^{r×d}、B∈R^{d×r}。标准 Adam 把A、B当成欧氏空间里的普通张量更新。但B·A这个乘积对(A,B)的缩放有对称性把A乘c、B除cΔW不变。换句话说(A,B)实际生活在一个有冗余自由度的流形上欧氏梯度会在这条冗余方向上浪费更新。Riemannian preconditioning 的思路把(A,B)看成流形上的点按流形度量对梯度做预条件precondition让更新沿着“真正改变 ΔW 的方向”走抑制冗余缩放方向的抖动。直观收益是收敛更稳、对学习率不那么敏感。工程上这需要一个新增的 optimizer/tuner 类型而不是改现有 LoRA 默认行为否则会破坏存量用户。三、根因为什么需要“先协调再写”根因 A直接改LoraConfig会破坏默认行为如果图省事把 Riemannian 逻辑写进默认 LoRA 前向/后向所有老用户行为都变必然被 maintainer 打回。正确做法是新增一个 tuner 类型如RiemannianLora与Lora并存。根因 B接口边界不清PR 范围失控没对齐“预条件放在 optimizer 层还是 tuner 层”PR 会越写越大review 成本爆炸。应当约定预条件逻辑封在 tuner 的update钩子里复用 PEFT 的BaseTuner框架。根因 C缺基准无法证明收益maintainer 会问“比普通 LoRA 好多少”。没带method_comparison基准数据就提 PR很难被接受。需要先在复活的 issue 里贴初步数字。根因 D未确认数学约定的符号/度量Riemannian 预条件有不止一种度量有的用AᵀA、有的用BBᵀ。不同约定数值不同必须先和 maintainer 敲定否则实现来回返工。根因小结复活老提案要先做“接口与范围的协调”再写代码新增 tuner 类型而非改默认行为带基准数据、敲定数学约定PR 才站得住。四、最小可运行复现下面给出一个独立可跑的 Riemannian 预条件 LoRA 更新草图纯 PyTorch不依赖 PEFT 内部便于先在本地验证数学import torch import torch.nn as nn from torch.optim import Adam class RiemannianLoraLinear(nn.Module): def __init__(self, in_features, out_features, r4): super().__init__() self.linear nn.Linear(in_features, out_features, biasFalse) self.linear.weight.requires_grad False # 冻结原权重 self.A nn.Parameter(torch.randn(r, in_features) * 0.01) self.B nn.Parameter(torch.zeros(out_features, r)) # 初始化 B0ΔW 初始为 0 def forward(self, x): delta (x self.A.t()) self.B.t() # ΔW B Ashape [B, out] return self.linear(x) delta def riemannian_precondition(self): 对 (A,B) 的梯度做流形预条件抑制冗余缩放方向。 简化约定用 AᵀA 和 BBᵀ 作为两侧度量把梯度投影到改变 ΔW 的方向。 if self.A.grad is None or self.B.grad is None: return # 用当前 A,B 构造度量示意真实实现会更严谨 with torch.no_grad(): metric_A torch.eye(self.A.shape[0], deviceself.A.device) # r×r metric_B torch.eye(self.B.shape[1], deviceself.B.device) self.A.grad self.A.grad metric_A self.B.grad self.B.grad metric_B def demo(): torch.manual_seed(0) layer RiemannianLoraLinear(16, 8, r4).cuda() opt Adam([layer.A, layer.B], lr1e-3) x torch.randn(4, 16, devicecuda) y torch.randn(4, 8, devicecuda) for step in range(50): opt.zero_grad() loss ((layer(x) - y) ** 2).mean() loss.backward() layer.riemannian_precondition() # 预条件后再 step opt.step() if step % 10 0: print(step, float(loss)) if __name__ __main__: demo()这段代码证明“预条件钩子可以插在loss.backward()之后、opt.step()之前”与 PEFT 的 tuner 框架对接时也是同样的插入点。五、解决方案第一层最小直接修复提交 PR 前先做三件协调动作把“写代码”的返工降到最低在 issue #1807 里回复对齐范围明确“新增RiemannianLoratuner不改默认 LoRA”。贴初步基准用method_comparison跑普通 LoRA vs Riemannian 变体把收敛曲线贴进 issue。敲定数学约定和 maintainer 确认用哪种度量AᵀA / BBᵀ / 组合写进设计说明。然后实现只新增文件不碰Lora# peft/tuners/riemannian_lora/__init__.py from .layer import RiemannianLoraLayer from .model import RiemannianLoraModel __all__ [RiemannianLoraLayer, RiemannianLoraModel]RiemannianLoraModel继承BaseTuner只在forward/update里加预条件钩子复用现有LoraConfig的r、alpha字段额外加precond: bool开关。六、解决方案第二层结构性改进6.1 预条件逻辑封进 tuner 的 update 钩子PEFT 的BaseTuner在optimizer.step()之后有机会做后处理。把 Riemannian 预条件放在training_step的backward后def training_step(self, model, batch): loss model(**batch).loss loss.backward() for module in model.modules(): if isinstance(module, RiemannianLoraLayer): module.riemannian_precondition() # 插入点 self.optimizer.step() return loss6.2 配置向后兼容dataclass class RiemannianLoraConfig(LoraConfig): precond: bool True # 默认开启兼容 LoraConfig 全部字段 metric: str AAt # 约定度量类型文档化6.3 用 design doc 固化约定在 PR 描述里附一小节“Math convention”把度量公式、符号、与欧氏 Adam 的差异写清楚review 时不再扯皮。七、解决方案第三层断言 / CI 守护保证新 tuner 不破坏默认 LoRA 行为加回归测试import torch import pytest from peft import get_peft_model, LoraConfig, RiemannianLoraConfig def test_default_lora_unchanged(): base build_base_model() m get_peft_model(base, LoraConfig(r8)) out1 m(torch.randn(2, 10)) # 普通 Lora 行为必须和系统已有快照一致 assert out1.shape (2, 10) def test_riemannian_runs_and_updates(): base build_base_model() m get_peft_model(base, RiemannianLoraConfig(r8, precondTrue)) p0 [p.detach().clone() for p in m.parameters() if p.requires_grad] loss m(torch.randn(2, 10), labelstorch.randint(0, 10, (2,))).loss loss.backward() # 预条件钩子不应让梯度为 None for p in m.parameters(): if p.requires_grad: assert p.grad is not NoneCI 里既有默认 LoRA 快照测试也有新 tuner 冒烟测试避免“复活提案”把旧功能带崩。八、排查清单复活老提案/提 PR 前查范围对齐了没是新增 tuner 还是改默认必须新增不破坏默认。带基准数据没method_comparison跑普通 vs 新变体贴收敛曲线。数学约定敲定没度量用哪种写进 design doc。接口复用 BaseTuner 没预条件钩子插在 backward 后、step 前。配置向后兼容没继承LoraConfig只加开关字段。回归测试加了没默认 LoRA 快照 新 tuner 冒烟。文档补了没precond、metric含义写进 docstring 和 docs。九、小结“Reviving #1807: Riemannian Preconditioned LoRA — coordination before drafting a PR” 本质是先协调、后编码的协作纪律叠加一个清晰的数学直觉LoRA 的(A,B)存在缩放冗余欧氏梯度会在这条冗余方向浪费更新Riemannian 预条件按流形度量把梯度扭到“真正改变 ΔW”的方向收敛更稳工程上新增RiemannianLoratuner绝不改默认 LoRA否则必被 maintainer 打回提 PR 前先在 issue 对齐范围、贴基准、敲定数学约定预条件钩子插在loss.backward()之后、optimizer.step()之前用默认 LoRA 快照测试 新 tuner 冒烟测试守护避免复活提案破坏存量。一句话老提案复活先对齐“新增而非修改 带数据 定约定”再动手写代码Riemannian 预条件的收益来自抑制 (A,B) 的冗余缩放方向。