【Bug已解决】Deepspeed AttributeError DummyOptim object has no attribute step 解决方案一、现象长什么样在 DeepSpeed ZeRO-3有时 ZeRO-2下自己写训练循环或者用某个封装框架时调用了optimizer.step()直接炸AttributeError: DummyOptim object has no attribute step完整一点的回溯通常是File train.py, line 80, in train_loop optimizer.step() AttributeError: DummyOptim object has no attribute step但optimizer是通过deepspeed.initialize(...)返回的那个对象按理说应该有step。为什么变成了没有step方法的DummyOptim下面讲清。二、背景DeepSpeed 在deepspeed.initialize(model..., model_parameters..., config...)时会根据 config 里声明的 optimizer 构造优化器并在内部用DeepSpeedEngine管理step。当你这样写engine, optimizer, _, lr_scheduler deepspeed.initialize( modelmodel, model_parametersmodel.parameters(), configds_config )返回的optimizer在某些情况下是一个DummyOptim——一个“占位优化器”。它存在的意义是当 DeepSpeed 自己在内部管理参数更新尤其是 ZeRO-3 下参数被切分、优化器状态也被分片时对外暴露的optimizer对象不需要真正的.step()真正的 step 由engine.step()完成。也就是说DeepSpeed 模式下更新参数要调engine.step()而不是optimizer.step()。如果你拿到optimizer后还像普通 PyTorch 那样调optimizer.step()而它恰好是DummyOptim就会撞上AttributeError: DummyOptim object has no attribute step。三、根因根因 ADeepSpeed 模式下调了optimizer.step()而非engine.step()这是 100% 的主因。DeepSpeed 的更新入口是engine.step()。当 optimizer 被 DeepSpeed 替换成DummyOptim无step方法时任何optimizer.step()调用都会报这个错。根因 Bconfig 里没声明 optimizerDeepSpeed 造了个空壳如果你deepspeed.initialize时 config 里没有optimizer字段也没有传model_parametersDeepSpeed 可能构造一个DummyOptim占位。此时你若依赖它做 step必炸。根因 C和 HuggingFace Trainer 混用时手动调 step用transformers.Trainer DeepSpeed 时Trainer 内部已经会调engine.step()你不该再手动optimizer.step()。但有些人把 Trainer 的optimizer取出来在 callback 里额外 step就会踩DummyOptim。根因 DZeRO-3 zero.Init下自定义 loop 误用 optimizerZeRO-3 用deepspeed.zero.Init()上下文构建大模型时优化器状态是分片的optimizer通常是DeepSpeedZeroOptimizer或DummyOptim必须走engine.step()。根因小结DeepSpeed 的训练更新入口是engine.step()不是optimizer.step()当 optimizer 是DummyOptim无step时手动optimizer.step()必然报错config 必须正确声明 optimizer且用engine.step()推进。四、最小可运行复现下面脚本先演示“错误用法”直接optimizer.step()触发 DummyOptim 报错再给正确写法。import torch import deepspeed import torch.nn as nn class Tiny(nn.Module): def __init__(self): super().__init__() self.lin nn.Linear(16, 8) def forward(self, x): return self.lin(x) def make_config(): return { train_micro_batch_size_per_gpu: 2, gradient_accumulation_steps: 1, fp16: {enabled: False}, zero_optimization: {stage: 2}, optimizer: { type: Adam, params: {lr: 1e-3, betas: [0.9, 0.999], eps: 1e-8}, }, } def main(): torch.manual_seed(0) model Tiny().cuda() engine, optimizer, _, _ deepspeed.initialize( modelmodel, model_parametersmodel.parameters(), configmake_config() ) print(optimizer 类型:, type(optimizer).__name__) x torch.randn(2, 16, devicecuda) loss engine(x).sum() engine.backward(loss) # 错误用法直接调 optimizer.step() if type(optimizer).__name__ DummyOptim: try: optimizer.step() except AttributeError as e: print(触发报错:, e) # 正确用法用 engine.step() engine.step() print(engine.step() 执行成功参数已更新) if __name__ __main__: main()运行后能看到optimizer 类型: DummyOptim 触发报错: DummyOptim object has no attribute step engine.step() 执行成功参数已更新这正好复现并验证修复把optimizer.step()换成engine.step()即可。五、解决方案第一层最小直接修复唯一正确的更新入口是engine.step()。把训练循环里的optimizer.step()全部替换为engine.step()engine, optimizer, _, lr_scheduler deepspeed.initialize( modelmodel, model_parametersmodel.parameters(), configds_config ) for batch in loader: loss engine(batch).sum() engine.backward(loss) engine.step() # 正确用 engine 推进更新如果你确实需要在外部拿到“真实优化器”比如要读optimizer.param_groups[0][lr]可以通过# DeepSpeed 暴露底层 optimizer可能不是 DummyOptim real_opt engine.optimizer print(底层 optimizer 类型:, type(real_opt).__name__)但更新仍然走engine.step()不要对real_opt调.step()。六、解决方案第二层结构性改进6.1 统一训练循环封装杜绝optimizer.step()把训练 step 收口到一个函数团队不可能再误用def train_step(engine, batch): engine.train() loss engine(**batch).loss if isinstance(batch, dict) else engine(batch) engine.backward(loss) engine.step() # 永远用 engine.step return loss6.2 config 必须声明 optimizer确保deepspeed.initialize的 config 里有合法optimizer字段且传了model_parameters避免 DeepSpeed 退化出诡异占位{ optimizer: { type: AdamW, params: { lr: auto, betas: auto, eps: auto, weight_decay: auto } } }6.3 与 HuggingFace Trainer 配合时不要手动 stepfrom transformers import Trainer trainer Trainer(modelmodel, argstraining_args, train_datasetds, optimadamw_torch) # Trainer 会自动接 DeepSpeed trainer.train() # 内部已用 engine.step别在外面再 step七、解决方案第三层断言 / CI 守护加一条断言防止有人把optimizer.step()写回训练循环import ast import pytest def scan_for_optimizer_step(path: str) - bool: tree ast.parse(open(path, r, encodingutf-8).read()) for node in ast.walk(tree): if isinstance(node, ast.Call): func node.func if isinstance(func, ast.Attribute) and func.attr step: # optimizer.step() 形式命中 if isinstance(func.value, ast.Name) and func.value.id optimizer: return True return False def test_no_optimizer_step_in_loop(): assert not scan_for_optimizer_step(train_loop.py), ( DeepSpeed 模式下禁止 optimizer.step()必须用 engine.step() )再补一条运行时不变量断言def test_engine_step_updates_params(engine, dummy_batch): before [p.detach().clone() for p in engine.parameters()] loss engine(**dummy_batch).loss engine.backward(loss) engine.step() after list(engine.parameters()) changed any(not torch.allclose(b, a) for b, a in zip(before, after)) assert changed, engine.step() 未更新参数八、排查清单遇到DummyOptim object has no attribute step时查是不是在 DeepSpeed 模式下调了optimizer.step()改成engine.step()。config 里有没有声明 optimizer没有会导致诡异占位。deepspeed.initialize有没有传model_parameters没传优化器可能造空壳。是不是和 HuggingFace Trainer 混用又手动 stepTrainer 内部已用 engine.step。是否 ZeRO-3 zero.Init自定义 loop必须走 engine.step。需要读 lr / param_groups 怎么办用engine.optimizer读但别 step 它。参数到底更新了没用“step 前后参数是否变化”断言确认 engine.step 生效。九、小结DummyOptim object has no attribute step是 DeepSpeed 模式下一个非常典型的“误用优化器”错误DeepSpeed 管理参数更新时返回的optimizer可能是DummyOptim无step真正入口是engine.step()任何optimizer.step()调用在 DeepSpeed 模式下都该改成engine.step()config 必须正确声明optimizer并传model_parameters避免退化出占位和 HuggingFace Trainer 配合时Trainer 内部已用engine.step()不要在外面再手动 step用 AST 扫描禁止optimizer.step() pytest 断言engine.step()确实更新参数把问题挡在 CI。一句话DeepSpeed 里推进更新用engine.step()不是optimizer.step()DummyOptim就是提醒你“step 归 engine 管”。