环境
- DeepRec:
1.15.5+deeprec2402(commit 9a54aae7d5)
- 集群:异步 PS 训练,1 chief + 3 worker + 1 ps
- 配置:
EmbeddingVariable + AdamAsyncOptimizer、CounterFilter(filter_freq=2)、默认 DRAM 存储
- 流程:按天增量训练,第 N 天的 checkpoint 复制给第 N+1 天 restore 后继续训练
问题现象
第 N+1 天 restore 后训练崩溃于 CheckNumerics。排查发现 EV 主权重完全健康(max|w| = 0.354,无 NaN/Inf),但 Adam 的一阶矩 m 里出现 9.4e37 / 2.4e38 和 NaN。
这些脏值是有限的 float32,所以 tf.check_numerics 不会报;一次 var -= lr_t·m/(√v+eps) 就把权重推到约 1e33,直到下游某个平方运算溢出成 inf 才暴露成 NaN。梯度裁剪也拦不住 —— 它裁的是 g,而破坏早已落在 m 里。
关键:这些脏值在零训练步的情况下就已存在。 第 N+1 天的 checkpoint 由 CheckpointSaverHook.after_create_session 在 restore 完成后、任何 train_op 执行之前写出,里面 m 就已经是 1e38 了。
根源
前提:由于ps分布式训练,chief节点比其它worker先训练完数据,开始保存checkpoint,而其它worker还在训练数据,和ps通讯更新参数,导致checkpoint 里 w / m / v 的 key 集合不一致。
DramStorage::Save(single_tier_storage.h:425)对每个张量各调用一次 kv_->GetSnapshot(),且绕过了 single_tier_storage.h:220 处带 mutex 保护的版本。异步训练下 chief 保存期间其他 worker 仍在往 ps 插入新 key,于是三个张量落在三个不同时刻的快照上:
play_paywall_story_id_seq_30d: w=7033905 ⊆ m=7034862 ⊆ v=7035789
在 m 而不在 w 的 957 个 key: version ∈ [48901, 48920]
在 v 而不在 m 的 927 个 key: version ∈ [48915, 48926]
严格嵌套、零交叉,多出来的 key 其 version 是首尾相接的连续区间,与保存顺序一一对应。
问题:Import 只写自己那一段 slot,而分配的内存块没有被初始化。
// single_tier_storage.h:353 DramStorage::Import
void Import(K key, V* value, int64 freq, int64 version, int emb_index) override {
void* value_ptr = feat_desc_->Allocate(freq);
Insert(key, &value_ptr);
feat_desc_->SetValue(value_ptr, emb_index, value); // 只写 emb_index 对应的那一段
feat_desc_->SetFreq(value_ptr, freq);
feat_desc_->UpdateVersion(value_ptr, version);
} // 没有调用 SetDefaultValue()
// normal_feature_descriptor.h:74
void* Allocate() override {
void* val = alloc_->AllocateRaw(Allocator::kAllocatorAlignment, alloc_bytes_);
FeatureDescriptorImpl<V>::InitFreqAndVersion(val); // 只初始化 header, 数据区原样保留
return val;
}
而唯一能覆盖全部 slot 段的 SetDefaultValue()(normal_feature_descriptor.h:97)在 import 路径上从未被调用 —— 全仓库 6 处调用点全部位于 filter policy 的 lookup/admit 路径。对比正常训练新建 key 时(counter_filter_policy.h:106):
*value_ptr = feat_desc_->Allocate();
if (count >= config_.filter_freq) {
void* admit_value_ptr = feat_desc_->Admit(*value_ptr);
feat_desc_->SetDefaultValue(admit_value_ptr, key); // 覆盖全部 slot 段
...
}
结果: restore 按保存顺序 w → m → v 逐张量重放,各自带自己的 emb_index。导入 v 时新建的那 927 个 key,其 m 段既没有被任何一次 SetValue 写过,也没有被 SetDefaultValue 兜底,保留的就是 AllocateRaw 拿到的原始内存。
实测完全吻合:82/82 和 227/227 的损坏 key 全部落在「在 v 而不在 m」的差集里,无一例外;而 m 行数在 restore 时未被扩充的变量,损坏行数为 0。
单机可复现(import_meta_graph → restore → 立即 save,零训练步、无分布式):新建行在全部 8 个维度上都非零,且同样条件跑两次数值不同 —— 未初始化内存,非确定性。
环境
1.15.5+deeprec2402(commit9a54aae7d5)EmbeddingVariable+AdamAsyncOptimizer、CounterFilter(filter_freq=2)、默认 DRAM 存储问题现象
第 N+1 天 restore 后训练崩溃于
CheckNumerics。排查发现 EV 主权重完全健康(max|w| = 0.354,无 NaN/Inf),但 Adam 的一阶矩m里出现9.4e37/2.4e38和 NaN。这些脏值是有限的 float32,所以
tf.check_numerics不会报;一次var -= lr_t·m/(√v+eps)就把权重推到约 1e33,直到下游某个平方运算溢出成inf才暴露成 NaN。梯度裁剪也拦不住 —— 它裁的是g,而破坏早已落在m里。关键:这些脏值在零训练步的情况下就已存在。 第 N+1 天的 checkpoint 由
CheckpointSaverHook.after_create_session在 restore 完成后、任何train_op执行之前写出,里面m就已经是 1e38 了。根源
前提:由于ps分布式训练,chief节点比其它worker先训练完数据,开始保存checkpoint,而其它worker还在训练数据,和ps通讯更新参数,导致checkpoint 里
w/m/v的 key 集合不一致。DramStorage::Save(single_tier_storage.h:425)对每个张量各调用一次kv_->GetSnapshot(),且绕过了single_tier_storage.h:220处带 mutex 保护的版本。异步训练下 chief 保存期间其他 worker 仍在往 ps 插入新 key,于是三个张量落在三个不同时刻的快照上:严格嵌套、零交叉,多出来的 key 其
version是首尾相接的连续区间,与保存顺序一一对应。问题:
Import只写自己那一段 slot,而分配的内存块没有被初始化。而唯一能覆盖全部 slot 段的
SetDefaultValue()(normal_feature_descriptor.h:97)在 import 路径上从未被调用 —— 全仓库 6 处调用点全部位于 filter policy 的 lookup/admit 路径。对比正常训练新建 key 时(counter_filter_policy.h:106):结果: restore 按保存顺序
w → m → v逐张量重放,各自带自己的emb_index。导入v时新建的那 927 个 key,其m段既没有被任何一次SetValue写过,也没有被SetDefaultValue兜底,保留的就是AllocateRaw拿到的原始内存。实测完全吻合:82/82 和 227/227 的损坏 key 全部落在「在
v而不在m」的差集里,无一例外;而m行数在 restore 时未被扩充的变量,损坏行数为 0。单机可复现(
import_meta_graph→restore→ 立即save,零训练步、无分布式):新建行在全部 8 个维度上都非零,且同样条件跑两次数值不同 —— 未初始化内存,非确定性。