【Bug已解决】CI fails: AssertionError: Param model.visual.blocks.0.norm1.weight is not updated 解决方案 【Bug已解决】CI fails: AssertionError: Param model.visual.blocks.0.norm1.weight is not updated 解决方案原始报错CI fails: AssertionError: Param model.visual.blocks.0.norm1.weight is not updated 场景CI 里有一条参数更新校验训练几步后断言某个参数这里是视觉塔model.visual.blocks.0.norm1.weight的权重确实变了。但训练后发现它纹丝不动——原因是这个参数根本没进优化器被漏加、或被冻结、或requires_gradFalse于是梯度从不更新它。断言失败只在 CI 这条校验里暴露本地可能没开校验所以没发现。正确做法是确保所有该训练的参数都被优化器管理且requires_gradTrue并在训练前断言。 关键词参数未更新、requires_grad、优化器参数组、冻结、视觉塔、参数校验、trainable、optimizer、CI 断言。一、现象长什么样某个参数训练后没变模型有视觉塔visual.blocks.*训练时希望它一起更新CI 断言训练 N 步后model.visual.blocks.0.norm1.weight的值应不同于初始但训练完它完全没变断言AssertionError排查发现这个参数要么requires_gradFalse被冻结要么没被加进优化器的param_groups梯度本就不流向它优化器也无它的引用自然不动本地若没开参数更新校验就发现不了CI 才暴露——典型的静默漏训。核心问题该训练的参数没被优化器接管或没开梯度导致它永远不更新而训练照常跑、loss 可能还降极具迷惑性。二、背景参数为何在模型里却不在优化器里一个参数要被更新必须同时满足param.requires_grad True梯度能流到它它被包含在某个优化器的param_groups里优化器知道要更新它前向/反向确实经过它图连通。漏训常发生在冻结误伤为了只训语言模型头把整个模型requires_grad_(False)却忘了把视觉塔/某几层重新requires_grad_(True)优化器漏加手动列参数组时只加了model.language_model漏了model.visual参数过滤用filter(lambda p: p.requires_grad, ...)收集时若某参数 requires_grad 恰为 False被静默滤掉LoRA 场景只训 LoRA 时视觉塔本就不该更新——但若需求是视觉塔也要训漏加就是 bug。CI 的参数更新校验正是抓这种漏训的利器它直接看权重有没有变。三、根因参数未进优化器或 requires_grad 关闭根因拆解requires_grad 关视觉塔被requires_grad_(False)梯度不流优化器漏加optim.SGD(model.language_model.parameters())漏了 visual过滤静默滤掉filter(requires_grad)把 False 的静默排除无提示无校验训练前没断言某参数在优化器里且 requires_gradCI 才暴露本地没开参数是否更新校验漏训被掩盖loss 仍降语言模型部分在训loss 可能还降误以为一切正常。下面用最小模型复现参数没进优化器训练后不变再给确保进优化器 校验的修复。四、最小可运行复现import torch class SmallModel(torch.nn.Module): def __init__(self): super().__init__() self.visual torch.nn.Linear(2, 2) # 视觉塔 self.head torch.nn.Linear(2, 2) def forward(self, x): return self.head(self.visual(x)) if __name__ __main__: m SmallModel() # 错误优化器只加了 head漏了 visual opt torch.optim.SGD(m.head.parameters(), lr0.1) before m.visual.weight.data.clone() for _ in range(3): opt.zero_grad() m(torch.randn(2, 2)).sum().backward() opt.step() print(visual 更新了吗?, torch.equal(before, m.visual.weight.data)) # True - 没更新运行可见visual权重训练后不变——它不在优化器里漏训现场。五、方案用 model.parameters() 整体接管确保不漏第一层优化器直接用model.parameters()或显式包含 visual保证所有requires_gradTrue的参数都被接管def build_optimizer(model, lr0.1): # 包含所有 requires_gradTrue 的参数含 visual params [p for p in model.parameters() if p.requires_grad] if not params: raise ValueError(没有可训练参数检查 requires_grad) return torch.optim.SGD(params, lrlr) if __name__ __main__: m SmallModel() opt build_optimizer(m) before m.visual.weight.data.clone() for _ in range(3): opt.zero_grad() m(torch.randn(2, 2)).sum().backward() opt.step() print(visual 更新了吗?, not torch.equal(before, m.visual.weight.data)) # False - 更新了优化器接管全部可训练参数visual 不再漏。六、方案训练前断言关键参数在优化器且 requires_grad第二层训练前断言关键参数如 visual 塔requires_gradTrue且在优化器参数集中缺失即报错def assert_trainable(param, optimizer): if not param.requires_grad: raise RuntimeError(目标参数 requires_gradFalse不会被更新) opt_params set(id(p) for group in optimizer.param_groups for p in group[params]) if id(param) not in opt_params: raise RuntimeError(目标参数不在优化器中不会被更新) if __name__ __main__: m SmallModel() opt build_optimizer(m) assert_trainable(m.visual.weight, opt) # 通过 print(关键参数已在优化器且可训练。)训练前断言把漏训从 CI 才暴露变成启动即报错。七、方案CI 参数更新校验训练后确认权重变化第三层CI 那条校验本身就是好实践——训练几步后确认关键参数权重确实变了没变则说明仍漏训或梯度断流def assert_param_updated(param, before, tol1e-9): delta (param.data - before).abs().max().item() if delta tol: raise AssertionError(f参数未更新, 变化量{delta}可能漏训或梯度断流) if __name__ __main__: m SmallModel() opt build_optimizer(m) snap {id(m.visual.weight): m.visual.weight.data.clone()} for _ in range(3): opt.zero_grad(); m(torch.randn(2, 2)).sum().backward(); opt.step() assert_param_updated(m.visual.weight, snap[id(m.visual.weight)]) print(CI 参数更新校验通过。)参数更新校验是防漏训的最后一道闸CI 必备。八、验证把关键参数会被更新锁进测试def test_visual_in_optimizer(): m SmallModel() opt build_optimizer(m) opt_ids set(id(p) for g in opt.param_groups for p in g[params]) assert id(m.visual.weight) in opt_ids def test_visual_gets_updated(): m SmallModel() opt build_optimizer(m) before m.visual.weight.data.clone() for _ in range(3): opt.zero_grad(); m(torch.randn(2,2)).sum().backward(); opt.step() assert not torch.equal(before, m.visual.weight.data) def test_frozen_rejected(): m SmallModel() m.visual.requires_grad_(False) try: assert_trainable(m.visual.weight, build_optimizer(m)) assert False except RuntimeError: pass if __name__ __main__: test_visual_in_optimizer() test_visual_gets_updated() test_frozen_rejected() print(参数更新测试通过。)九、排查清单某参数未更新按顺序查requires_grad目标参数是否requires_gradTrueFalse 则梯度不流。优化器范围参数是否被加进优化器 param_groups漏加则永不更新。过滤静默用filter(requires_grad)收集时是否静默滤掉 False 参数冻结误伤是否整体冻结后忘了把该层重新 requires_grad_(True)训练前断言是否断言关键参数在优化器且可训练无则暴露晚。CI 校验是否有训练后参数变化校验没有则漏训本地难发现。loss 假象loss 仍降是否让你误以为正常语言部分在训也会降。十、小结CI 报 visual.blocks.0.norm1.weight 未更新是该训练的参数没进优化器或被冻结梯度从不流向它训练照常跑、loss 可能还降但那一层永远不动。本地常因没开参数更新校验而漏掉CI 才暴露这种静默漏训。修复三层整体接管优化器用model.parameters()或显式含 visual确保不漏训练前断言断言关键参数requires_gradTrue且在优化器参数集缺失即报错CI 更新校验训练后确认关键参数权重确实变化没变即漏训/断流。核心原则一个参数要被更新必须同时满足 requires_gradTrue 且在优化器参数组中。凡是训练后某参数权重没变的现场第一反应都是查它是否进优化器、是否开了梯度——并用训练前断言 CI 更新校验把漏训变成启动即发现的错误而非 CI 红才暴露。