ZCH 零碰撞哈希 (Zero-Collision Hash)

解决了什么问题

传统 hash_bucket_size 模式下,所有原始 ID 通过模运算落入一个固定大小的 embedding 表:

hash("user_12345") % 100000 → 下标 42

缺点:

  • 哈希碰撞不可避免(不同 ID 映射到同一行,共享 embedding)
  • 表大小必须预先设定,无法动态扩展
  • 所有 ID 平等占用一行,无法区分高频/低频

ZCH 的解决思路:用一个更大的哈希空间 + 一个受管理的碰撞映射表

原始 ID → CityHash → uint64 (2^63 空间) → MCH 碰撞管理 → 固定表 [zch_size, emb_dim]

Proto 定义

// tzrec/protos/feature.proto:15-47
message ZeroCollisionHash {
    required uint64 zch_size = 1;               // 实际 embedding 表行数
    optional uint32 eviction_interval = 2 [default = 5];
    oneof eviction_policy {
        LFU_EvictionPolicy lfu = 101;
        LRU_EvictionPolicy lru = 102;
        DistanceLFU_EvictionPolicy distance_lfu = 103;
    }
    optional string threshold_filtering_func = 3;
}

message LFU_EvictionPolicy {}
message LRU_EvictionPolicy {
    optional float decay_exponent = 1 [default = 1.0];
}
message DistanceLFU_EvictionPolicy {
    optional float decay_exponent = 1 [default = 1.0];
}

ZCH 配置示例

feature_configs {
    id_feature {
        feature_name: "user_id"
        embedding_dim: 16
        zch {
            zch_size: 1000000       // 实际 embedding 表 100 万行
            eviction_interval: 5    // 每 5 步淘汰一次
            lfu {}                  // LFU 淘汰策略
        }
    }
}

当设置了 zch{} 后,hash_bucket_size 不再需要——代码会自动用 MAX_HASH_BUCKET_SIZE = 2^63 - 1 替代(feature.py:71)。

适用特征类型

zch 字段支持 6 种特征类型(feature.proto):

Proto Message 特征类型 行号
IdFeature id_feature, sequence_id_feature L149
ComboFeature combo_feature, sequence_combo_feature L273
LookupFeature lookup_feature, sequence_lookup_feature L420
MatchFeature match_feature, sequence_match_feature L517
CustomFeature custom_feature, sequence_custom_feature L867
BoolMaskFeature bool_mask_feature, sequence_bool_mask_feature L938

三阶段编码链路

ZCH 的核心是三层映射:

原始 ID "user_12345"
    │
    ▼ ── Stage 1: FG 哈希层 ──────────────────────────
    │  CityHash (默认) 或 FarmHash (USE_FARM_HASH_TO_BUCKETIZE=true)
    │  hash_bucket_size = MAX_HASH_BUCKET_SIZE (2^63-1)
    │  不落入小表,而是映射到超大地址空间
    ▼
uint64 0x7A3F...B1C2
    │
    ▼ ── Stage 2: MCH 碰撞管理层 ──────────────────────
    │  MCHManagedCollisionModule (TorchRec)
    │  容量 = zch_size (1,000,000)
    │  维护排好序的 uint64 数组 + remapping
    │  二分查找 → 找到位置 → 取出 remapped index
    ▼
[0, zch_size) 的下标: 42
    │
    ▼ ── Stage 3: Embedding lookup 层 ────────────────
    │  EmbeddingBagCollection.forward(indices)
    │  weight = nn.Parameter([zch_size, emb_dim])
    │  F.embedding_bag(indices, weight, ...)
    ▼
embedding vector [16]

关键源码位置

阶段 代码 行号
hash_bucket_size 覆写 feature.py:71 MAX_HASH_BUCKET_SIZE L71
num_embeddings 返回 zch_size id_feature.py:67-68 L67-68
FG 配置注入 hash_bucket_size id_feature.py:108 _fg_json() L108
MCH 模块构建 feature.py:693-726 mc_module() L693-726
MCH 映射 + embedding lookup embedding.py EBC forward

MCH 管理缓冲区

MCHManagedCollisionModule 内部维护了 5 个持久化缓冲区:

缓冲区 形状 用途
_mch_sorted_raw_ids [zch_size] (int64) 排好序的 uint64 原始 ID;iinfo_max=空槽
_mch_remapped_ids_mapping [zch_size] (int64) 原始 ID → 全局 embedding row index
_mch_counts [zch_size] (int64) 访问频次计数器(LFU 淘汰用)
_mch_last_access_iter [zch_size] (int64) 上次访问的迭代步数(LRU 淘汰用)
_output_segments_tensor [1025] (int64) 分布式场景下每个 rank 的输出范围

淘汰策略

三种策略控制何时及如何替换 embedding 表中的条目:

策略 淘汰分数公式 特点
LFU access_cnt 最简单的频次淘汰
LRU 1 / (current - last_access) ^ decay_exponent 时间衰减,最近使用的保留
DistanceLFU access_cnt / (current - last_access) ^ decay_exponent 综合频次和时效性

淘汰频率由 eviction_interval 控制(每 N 步执行一次淘汰)。

前向传播与梯度

ZCH 的 MCH remapping 是一个纯查表 + 二分搜索操作,不参与求导:

forward:
  input_ids → MCH.binary_search → indices → weight[indices] → output

梯度:
  output.grad → weight.grad (正常反向传播)
  MCH 映射不产生梯度

训练和普通 EmbeddingBag 无异,weight 通过正常 optimizer.step() 更新。

ZCH vs DynamicEmb

两种机制解决同一类问题(大型 embedding 表管理),但方案截然不同:

方面 ZCH (MCHManagedCollisionModule) DynamicEmb
表结构 固定大小 [zch_size, emb_dim] 稠密张量 外部动态哈希表,键值分离
查找方式 uint64 → 二分查找 → remapped index → weight[idx] uint64 → CUDA 哈希表 O(1) → 直接读 values
weight 位置 nn.Parameter 在 EBC 中,是 TorchRec 模块图的一部分 无 weight 参数,由 dynamicemb CUDA kernel 管理
淘汰 LFU/LRU/DistanceLFU LFU/STEP/TIMESTAMP/NO_EVICTION
分布式 TorchRec 原生 sharding DynamicEmbParameterConstraints + planner monkey-patch
存储模式 仅 HBM HBM_ONLY / CACHING / HYBRID
训练流程 标准 optimizer.step(weight) 外部 key-value 文件 + CUDA kernel 更新

核心区别一句话:ZCH 的 weight 仍是 nn.Parameter,属于 TorchRec 图的一部分;DynamicEmb 的 weight 在外部 CUDA 哈希表中,TorchRec 的 weight 是 dummy placeholder。

导出限制

export_util.py:544

# TODO(hongsheng.jhs): support mczch

ZCH 的 RTP 导出当前只输出稠密 weight 张量不输出 key-value hashmap

{
  "xxx.mc_ebc.embedding_bags.user_id.weight/part_0_1": {
    "shape": [1000000, 16],
    "is_hashmap": false,
    "dimension": 16,
    "dtype": "float32"
  }
}

MCH 的 5 个管理缓冲区(_mch_sorted_raw_ids_mch_remapped_ids_mapping_mch_counts_mch_last_access_iter_output_segments_tensor全部没有导出

这意味着 RTP 在线无法复现 MCH 的碰撞管理,只能做简单的 hash_bucket 取模——丢失了 ZCH 的训练成果

在线推理方案

方案 A:zch_to_dynamicemb_convert(推荐)

转换工具 tzrec/tools/dynamicemb/zch_to_dynamicemb_convert.py:611-754 读取 ZCH checkpoint,转换成 DynamicEmb 标准格式:

ZCH checkpoint:
  _mch_sorted_raw_ids[i]  →  weight[_mch_remapped_ids_mapping[i]]  → embedding vector

DynamicEmb 输出:
  user_id_emb_keys.rank_0.world_size_1   # int64 原始 ID
  user_id_emb_values.rank_0.world_size_1  # float32 embedding 向量

RTP 已原生支持 is_hashmap: true 格式,无需改代码。

方案 B:冻结 + 转 key-value 查找表

在导出时冻结 MCH 状态:遍历 _mch_sorted_raw_ids 中所有有效条目,提取对应的 embedding 向量,直接写出和 DynamicEmb 相同的 key-value safetensors 格式。

方案 C:RTP C++ 层复现 MCH

在 RTP 的 embedding lookup 层实现 sorted_raw_ids + 二分查找 + 淘汰策略。但 RTP 推理通常无状态(每个请求独立),淘汰策略在推理时意义不大。

关联环境变量

变量 作用 相关源码
USE_FARM_HASH_TO_BUCKETIZE=true FG 哈希从 CityHash 切换为 FarmHash,确保在线一致性 env_util.py:27
USE_RTP=1 启用 RTP 导出模式;不配 FarmHash 会告警 env_util.py:24
FORCE_LOAD_SHARDING_PLAN 强制从 checkpoint 加载 sharding plan env_util.py:55

参考源码

文件 关键内容
tzrec/protos/feature.proto:15-47 ZCH proto 定义
tzrec/features/feature.py:71 MAX_HASH_BUCKET_SIZE
tzrec/features/feature.py:693-726 mc_module() 构建 MCH 模块
tzrec/features/id_feature.py:67-68 num_embeddings 返回 zch_size
tzrec/features/id_feature.py:108 _fg_json() 覆写 hash_bucket_size
tzrec/modules/embedding.py:615-619 _add_mc_module() 一致性检查
tzrec/utils/export_util.py:544 TODO: support mczch
tzrec/utils/state_dict_util.py:22-34 fix_mch_state() 修复 MCH meta tensor
tzrec/utils/checkpoint_util.py:496-520 _find_mch_modules() + _redistribute_mch_state()
tzrec/tools/dynamicemb/zch_to_dynamicemb_convert.py:611-754 ZCH → DynamicEmb 转换

This site uses Just the Docs, a documentation theme for Jekyll.