【Bug已解决】Deepspeed : AttributeError: ‘DummyOptim‘ object has no attribute ‘step‘ 解决方案
【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( model=model, model_parameters=model.parameters(), config=ds_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'。
三、根因
根因 A:DeepSpeed 模式下调了optimizer.step()而非engine.step()
这是 100% 的主因。DeepSpeed 的更新入口是engine.step()。当 optimizer 被 DeepSpeed 替换成DummyOptim(无step方法)时,任何optimizer.step()调用都会报这个错。
根因 B:config 里没声明 optimizer,DeepSpeed 造了个空壳
如果你deepspeed.initialize时 config 里没有optimizer字段,也没有传model_parameters,DeepSpeed 可能构造一个DummyOptim占位。此时你若依赖它做 step,必炸。
根因 C:和 HuggingFace Trainer 混用时手动调 step
用transformers.Trainer+ DeepSpeed 时,Trainer 内部已经会调engine.step(),你不该再手动optimizer.step()。但有些人把 Trainer 的optimizer取出来在 callback 里额外 step,就会踩DummyOptim。
根因 D:ZeRO-3 +zero.Init下自定义 loop 误用 optimizer
ZeRO-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( model=model, model_parameters=model.parameters(), config=make_config() ) print("optimizer 类型:", type(optimizer).__name__) x = torch.randn(2, 16, device="cuda") 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( model=model, model_parameters=model.parameters(), config=ds_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 配合时不要手动 step
from transformers import Trainer trainer = Trainer(model=model, args=training_args, train_dataset=ds, optim="adamw_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", encoding="utf-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 混用又手动 step?Trainer 内部已用 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 管”。