Skip to content

[EmbeddingVariable] restore 时若 w/m/v 的 key 集合不一致, optimizer slot 段未被初始化, 导致 slot 中出现 1e38 脏值 #1023

Description

@jhhugo

环境

  • DeepRec1.15.5+deeprec2402(commit 9a54aae7d5
  • 集群:异步 PS 训练,1 chief + 3 worker + 1 ps
  • 配置EmbeddingVariable + AdamAsyncOptimizerCounterFilter(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::Savesingle_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_graphrestore → 立即 save,零训练步、无分布式):新建行在全部 8 个维度上都非零,且同样条件跑两次数值不同 —— 未初始化内存,非确定性。

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions