cpu_ram_efficient_loading=True
cpu_ram_efficient_loading
free -h
used
from_pretrained
init_adapter
FSDP 下开启该选项后,只有每节点的 local rank0 真正从磁盘读取权重到 CPU(占 1×M),其余 rank 的参数保持 meta / 空占位(物理内存≈0);真实权重由 FSDP 在设备侧从 rank0 broadcast 到各 rank 的 shard。因此 host 峰值应为 ~1×M。该机制由 is_fsdp_enabled() 门控。
is_fsdp_enabled()
from_pretrained 时门控正常触发,非 rank0 的参数落到 meta / 以 torch.empty_like(..., device="cpu") 建惰性空占位(不写内容、物理 RSS≈0):
torch.empty_like(..., device="cpu")
https://github.com/huggingface/transformers/blob/v4.57.1/src/transformers/modeling_utils.py#L5826-L5851
此时:rank0 = 完整 bf16(1×M);非 rank0 = 空占位(≈0)。
bf16 混合精度需要可训练参数的 fp32 主权重,但正确时机是 FSDP 分片之后,每个 rank 只 upcast 自己 1/N 的 shard(设备侧),不触碰 host 全量。accelerate / hyper-parallel 都在分片后执行:
https://github.com/huggingface/accelerate/blob/v1.11.0/src/accelerate/utils/fsdp_utils.py#L701C1-L713C17
全参微调时,LlamaFactory init_adapter 会在 FSDP 分片之前,把每个可训练参数在 host 上 upcast 到 fp32:
https://github.com/hiyouga/LlamaFactory/blob/main/src/llamafactory/model/adapter.py#L299-L344
https://github.com/hiyouga/LlamaFactory/blob/main/src/llamafactory/model/adapter.py#L292-L294
冲突点:此时模型还没分片,非 rank0 的参数正是上面那批 empty_like 空占位。param.data.to(torch.float32) 对每个参数做写入,把非 rank0 的空占位全部 fault 进物理 host → 每个 rank 都实体化一份完整模型 → host = N×M(fp32 比 bf16 还翻倍,趋向 N×2M)。这与 cpu_ram_efficient_loading 的设计直接冲突:本应保持空占位到设备侧 broadcast,却被这步 pre-shard host upcast 提前实体化。
empty_like
param.data.to(torch.float32)
注:hyper-parallel 的权重分发是设备侧(.to(device) + dist.broadcast),不增加 host;host 的 N×M 完全来自这步 host fp32 upcast。
.to(device)
dist.broadcast
在 LlamaFactory 集成里 monkeypatch adapter._setup_full_tuning / _setup_freeze_tuning:当 is_fsdp_enabled() 且模型为 fp16/bf16 时,强制 cast_trainable_params_to_fp32=False,跳过 host 上的 pre-shard upcast,把 upcast 推迟到分片之后(由 _maybe_upcast_trainable_params 完成)。patch 在 load_model 之前安装,幂等。
adapter._setup_full_tuning
_setup_freeze_tuning
cast_trainable_params_to_fp32=False
_maybe_upcast_trainable_params
load_model
正确性:跳过的只是冗余的 pre-shard host upcast;fp32 主权重仍由 post-shard 的 _maybe_upcast_trainable_params 保证(分片后每 rank 仅 1/N),混合精度语义不变。patch 仅在 FSDP + bf16/fp16 时生效,不影响纯 bf16 / zero3 / 量化等其它路径。
根据实验:修复后非 rank0 全程保持空占位,只有 local rank0 持一份完整模型,host 峰值从 N×M 降到 ~1×M,符合 cpu_ram_efficient_loading 的设计目标。
torch.use_deterministic_algorithms(True)
即使上面已修复,只要训练脚本里开了 torch.use_deterministic_algorithms(True),host 仍会回到 N×M。
机理:它会联动开启 torch.utils.deterministic.fill_uninitialized_memory(默认 True),使 torch.empty()/empty_like() 在分配后立即用已知值(NaN)写一遍。于是非 rank0 那批惰性 empty_like 占位被全部写实 → 物理页 fault → 整模型实体化 → N×M。(只有 use_deterministic_algorithms 有此影响;各类 seed、HCCL_DETERMINISTIC 不影响 host 分配。)
torch.utils.deterministic.fill_uninitialized_memory
torch.empty()/empty_like()
use_deterministic_algorithms
HCCL_DETERMINISTIC
安全:这些占位会被设备侧 broadcast 的真值覆盖、从不被读,故不填充不影响数值或确定性。(NPU 上建议实测确认该 flag 被 torch_npu 的 empty 继承。)
empty
现象
cpu_ram_efficient_loading=True。cpu_ram_efficient_loading后,模型加载阶段 host 内存(free -h的used)仍随卡数线性增长——每个 rank 各持一份完整模型,总量约为 N×M(N=每节点 rank 数,M=单份模型大小),而非预期的 ~1×M。规模大时直接把节点 OOM。from_pretrained后仍是空占位(host≈baseline),真正被实体化发生在其后的init_adapter。背景:
cpu_ram_efficient_loading的设计意图FSDP 下开启该选项后,只有每节点的 local rank0 真正从磁盘读取权重到 CPU(占 1×M),其余 rank 的参数保持 meta / 空占位(物理内存≈0);真实权重由 FSDP 在设备侧从 rank0 broadcast 到各 rank 的 shard。因此 host 峰值应为 ~1×M。该机制由
is_fsdp_enabled()门控。根因:
init_adapter在分片前于 host 上 upcast 到 fp32加载后状态(正常)
from_pretrained时门控正常触发,非 rank0 的参数落到 meta / 以torch.empty_like(..., device="cpu")建惰性空占位(不写内容、物理 RSS≈0):https://github.com/huggingface/transformers/blob/v4.57.1/src/transformers/modeling_utils.py#L5826-L5851
此时:rank0 = 完整 bf16(1×M);非 rank0 = 空占位(≈0)。
正常路径:fp32 upcast 应在 分片之后
bf16 混合精度需要可训练参数的 fp32 主权重,但正确时机是 FSDP 分片之后,每个 rank 只 upcast 自己 1/N 的 shard(设备侧),不触碰 host 全量。accelerate / hyper-parallel 都在分片后执行:
https://github.com/huggingface/accelerate/blob/v1.11.0/src/accelerate/utils/fsdp_utils.py#L701C1-L713C17
问题路径:
init_adapter在 分片前、于 host upcast全参微调时,LlamaFactory
init_adapter会在 FSDP 分片之前,把每个可训练参数在 host 上 upcast 到 fp32:https://github.com/hiyouga/LlamaFactory/blob/main/src/llamafactory/model/adapter.py#L299-L344
https://github.com/hiyouga/LlamaFactory/blob/main/src/llamafactory/model/adapter.py#L292-L294
冲突点:此时模型还没分片,非 rank0 的参数正是上面那批
empty_like空占位。param.data.to(torch.float32)对每个参数做写入,把非 rank0 的空占位全部 fault 进物理 host → 每个 rank 都实体化一份完整模型 → host = N×M(fp32 比 bf16 还翻倍,趋向 N×2M)。这与cpu_ram_efficient_loading的设计直接冲突:本应保持空占位到设备侧 broadcast,却被这步 pre-shard host upcast 提前实体化。device / dtype 变化对照
from_pretrained后init_adapter带 host upcast(问题)init_adapter跳过 host upcast(修复)修复
在 LlamaFactory 集成里 monkeypatch
adapter._setup_full_tuning/_setup_freeze_tuning:当is_fsdp_enabled()且模型为 fp16/bf16 时,强制cast_trainable_params_to_fp32=False,跳过 host 上的 pre-shard upcast,把 upcast 推迟到分片之后(由_maybe_upcast_trainable_params完成)。patch 在load_model之前安装,幂等。正确性:跳过的只是冗余的 pre-shard host upcast;fp32 主权重仍由 post-shard 的
_maybe_upcast_trainable_params保证(分片后每 rank 仅 1/N),混合精度语义不变。patch 仅在 FSDP + bf16/fp16 时生效,不影响纯 bf16 / zero3 / 量化等其它路径。根据实验:修复后非 rank0 全程保持空占位,只有 local rank0 持一份完整模型,host 峰值从 N×M 降到 ~1×M,符合
cpu_ram_efficient_loading的设计目标。注意:与
torch.use_deterministic_algorithms(True)的交互(另一条独立的 N×M 来源)即使上面已修复,只要训练脚本里开了
torch.use_deterministic_algorithms(True),host 仍会回到 N×M。机理:它会联动开启
torch.utils.deterministic.fill_uninitialized_memory(默认 True),使torch.empty()/empty_like()在分配后立即用已知值(NaN)写一遍。于是非 rank0 那批惰性empty_like占位被全部写实 → 物理页 fault → 整模型实体化 → N×M。(只有use_deterministic_algorithms有此影响;各类 seed、HCCL_DETERMINISTIC不影响 host 分配。)安全:这些占位会被设备侧 broadcast 的真值覆盖、从不被读,故不填充不影响数值或确定性。(NPU 上建议实测确认该 flag 被 torch_npu 的
empty继承。)