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 转换 |