Repository navigation
[Fix] Load SwapAdamW state in place to keep pinned host residency - #2225
PengchengShi00 wants to merge 3 commits into
Conversation
Summary修复
修复:state 改为“DTensor 感知的 pinned host 分配”;在 结论:APPROVE,静态 review 未发现阻断性缺陷。 本轮只应用了注释/文档类建议(见“其他 issues” N2/N6), Main Flowchart before this PRgraph TD
subgraph TE["TrainEngine — xtuner/v1/engine/train_engine.py"]
TE_save["save_dcp(weights_dir, save_optimizer=True)"]
TE_load["load_dcp(weights_dir, load_states, load_args)"]
TE_collect["_get_dcp_state_dict(cpu_offload=True)"]
end
subgraph DCP["torch.distributed.checkpoint.state_dict"]
D_get["get_optimizer_state_dict(model, optimizer, options)"]
D_set["set_optimizer_state_dict(model, optimizer, optim_state_dict, options)"]
D_load["_load_optim_state_dict()"]
end
subgraph OPT["torch.optim.Optimizer"]
O_sd["state_dict()"]
O_lsd["load_state_dict(state_dict)"]
O_cast["_process_value_according_to_param_policy()<br/>value.to(dtype=param.dtype, device=param.device)"]
O_setstate["__setstate__()<br/>整体替换 self.state / self.param_groups"]
end
subgraph SW["SwapAdamW — xtuner/v1/optim/swap_adamw.py(修改前)"]
SW_init["__init__() → _init_swap_states()"]
SW_alloc["state[p][k] = zeros_like(to_local(p)).cpu().pin_memory()<br/>plain Tensor,无 DTensor 元数据"]
SW_map["_param_to_cpu_states_map[p] = 与 state 同一批 buffer"]
SW_step["step(closure)"]
SW_put["put_state_to_device(device) → False"]
end
TE_save --> TE_collect --> D_get --> O_sd --> SW_init --> SW_alloc --> SW_map
TE_load --> TE_collect
TE_load --> D_set --> D_load --> O_lsd --> O_cast --> O_setstate
O_setstate -. "state/group 对象被替换 → 别名断裂、moment 上 GPU" .-> SW_step
SW_map --> SW_step --> SW_put
两个断点:
Main Flowchart after this PRgraph TD
classDef changed fill:#ffe3e3,stroke:#d40000,stroke-width:2px
subgraph TE["TrainEngine(未改动)"]
TE_save["save_dcp(weights_dir, save_optimizer=True)"]
TE_load["load_dcp(weights_dir, load_states, load_args)"]
TE_collect["_get_dcp_state_dict(cpu_offload=True)"]
end
subgraph DCP["torch.distributed.checkpoint.state_dict"]
D_get["get_optimizer_state_dict(model, optimizer, options)"]
D_set["set_optimizer_state_dict(model, optimizer, optim_state_dict, options)"]
D_load["_load_optim_state_dict() → optim.load_state_dict(state_dict=...)"]
end
subgraph OPT["torch.optim.Optimizer"]
O_sd["state_dict()"]
O_setstate["__setstate__() — 不再被走到"]
end
subgraph SW["SwapAdamW — xtuner/v1/optim/swap_adamw.py(修改后)"]
SW_init["__init__() → _init_swap_states()"]
SW_alloc["state[p][k] = empty_like(p, device='cpu', pin_memory=True).to_local().zero_()<br/>DTensor 保留 global shape / placements"]
SW_map["_param_to_cpu_states_map[p] = state[p] 的 local view(同一 storage,未改动)"]
SW_lsd["load_state_dict(state_dict) ← 新增覆写<br/>pre-hooks → 校验 group 布局 → 原地 copy_ → 原地更新 group → post-hooks"]
SW_step["step(closure)"]
SW_put["put_state_to_device(device) → False"]
end
TE_save --> TE_collect --> D_get --> O_sd
O_sd -. "DTensor:每个 rank 只写自己的 shard" .-> TE_save
TE_load --> TE_collect
TE_load --> D_set --> D_load --> SW_lsd
SW_lsd -. "只改值不改对象 → 别名与 pin 属性保持" .-> SW_step
SW_init --> SW_alloc --> SW_map --> SW_step --> SW_put
class SW_alloc,SW_lsd changed
说明(mermaid 8.8 支持的强调方式:
核心原理
核心实现与对应的单测
测试结构(198 行 / 2 个 TestClass / 3 个用例):
这两层刚好对应 bug 的两个面:修复前 resume 用例会在 DTensor/pinned/moment 断言处失败; 抽象与信息隐藏评估好的部分
与基类协议的 3 处分歧,均已在代码中说明
依赖 torch 私有 attrs(可接受取舍,已在 docstring 注明)
新增功能守则(既有类零污染)本 PR 是 bug fix,不新增功能类:
公开接口的线性业务流程评估
唯一注意点:方法已约 95 行;若将来再增加“按参数类型分派”之类的逻辑,优先把“拷贝单个参数 state”这一步拆出去 # 仅在未来膨胀时考虑
def _copy_one_param_state(self, target_state, source_state, param_id) -> None:
...单测建议
@unittest.skipUnless(torch.cuda.device_count() >= 2, "needs >=2 GPUs for the 2-rank DCP resume case")
def test_resume_after_dcp_matches_continuous_training(self) -> None:
...
其他 issuesN1(已应用)补齐 N2(已应用)类 docstring 补充 DTensor 不变量。 已加:对 DTensor 参数,state 必须是同 mesh / 同 placements 的 N3(nit) N4(nit) N5(流程)本地分支落后 VerdictAPPROVE — 无阻断项。本轮已应用:N1( |
SwapAdamW的优化器状态设计为常驻 pinned host 内存:self.state中的 moment 与_param_to_cpu_states_map持有的别名指向同一批 buffer。但 resume 时走的是官方torch.optim.AdamW.load_state_dict,它会:.to(device=param.device)搬上 GPU —— 瞬间占用约 2 倍参数量的显存,而这正是 SwapAdamW 要避免的;self.state条目 ——_param_to_cpu_states_map从此指向旧 buffer, 训练更新的是一套、save_dcp保存的是另一套,resume 之后的 checkpoint 中 moment 静默失真,且替换后的 tensor 不再 pinned,step 的 non_blocking 拷贝退化为同步。Fix
覆写
SwapAdamW.load_state_dict:不委托官方 loader,逐 keycopy_进既有 pinned CPU buffer(对象、storage、别名、pin 属性全部不变);同时重放官方loader 协议(pre/post hooks、id map 位置配对、param_groups 原地 update 以保住_param_to_group_map引用)。key 集合双向校验(忽略 amsgrad=False 的 None 占位), 空 state 显式原地清零(等价于官方"重置历史"语义)。Test Plan
新增引擎级测试
tests/engine/test_swap_adamw_engine_resume.py:两个引擎同一随机初始化,engine1 训练一步保存 DCP 后再训练一步,engine2 以不同 lr 加载(校验超参恢复)后训练一步,对比:
self.state与_param_to_cpu_states_map指向同一块 storage(load/step 两个检查点,负向验证过:无本修复时该断言精确报storage diverged);