---
title: "哈希嵌入"
description: "用户、帖子、作者、IP 各用 2 个线性同余哈希映射到一张统一嵌入表；生产配置越来越多地用语义 ID 表示帖子身份"
---

> Documentation Index
> Fetch the complete documentation index at: https://x-algorithm.vergego.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# 哈希嵌入

## 这一页回答什么

本页回答三个问题。第一，Phoenix 怎样把数量无上限的用户、帖子、作者、IP 编号映射到固定大小的嵌入表。第二，Phoenix 怎样把一个实体的多个哈希结果合成一个向量。第三，2026-08 起的生产配置里，哪些实体仍用哈希表示，哪些已改用语义 ID。

> 旧页面引用的 `_hash_ids`（`run_pipeline.py`）与 `build_unified_emb_table` 已随演示代码删除。现在 Rust 代码实现哈希函数，训练配置定义哈希常量。

## 核心结论

1. **编号不直接作为行索引**：每个编号经 2 个线性同余哈希映射到桶，再加上该实体在统一表里的偏移（`phoenix/crates/common/xai-recsys/src/model_config.rs:154-199`）。
2. **编号 0 永远映射到 0**：0 表示 padding（填充位）。模型检查“第一个哈希是否为 0”，以此判断该位置是否有效（`model_config.rs:155-157`、`phoenix/xrex/models/recsys_model.py:1709`）。
3. **所有实体共用一张表**：第 0 行留给 padding，之后是 64 行，再依次是用户、帖子、作者、IP 区段（`model_config.rs:114-128`、`phoenix/xrex/models/recsys_embedding.py:230-248`）。
4. **哈希常量是写死的开源默认值**：每类实体有 2 组 `scale`/`bias` 和 1 个 `modulus`。排序配置与召回配置的常量相同（`phoenix/xrex/configs/xrecsys.py:172-183`、`phoenix/xrex/configs/xrecsys_two_tower.py:186-197`）。
5. **模型用投影合并多个哈希嵌入**：新代码把每个哈希嵌入各乘一个投影矩阵，再相加（`phoenix/xrex/models/recsys_feature_prep.py:384-395`）。旧式路径仍是“先拼接，再乘一个大投影”（`recsys_model.py:1673-1711`）。
6. **生产召回不再查帖子 ID 嵌入，也没有用户 ID 嵌入**：召回用语义 ID 加作者哈希表示帖子身份（`xrecsys_two_tower.py:463-465`）。排序仍保留帖子与用户哈希嵌入，并增加了语义 ID（`xrecsys.py:343-352`）。

## 为什么用哈希

用户、帖子、作者编号数以十亿计，并且持续增长，因此无法给每个编号分配一行嵌入。哈希把编号压缩到固定容量的桶里。代价是碰撞：不同编号可能落进同一个桶。每个实体用 2 个独立哈希，两者同时碰撞的概率很低。两个嵌入合并后，单次碰撞的影响被摊薄（`phoenix/README.md:241-248`）。

## 哈希函数

Rust 函数 `HashTableConfig::hash_item_id` 实现帖子哈希（`phoenix/crates/common/xai-recsys/src/model_config.rs:171-185`）。用户、作者、IP 的哈希函数结构相同：

```rust
if item_id == 0 { return 0; }
let mut h = (item_id * item_hash_scales[index] + item_biases[index]) % item_modulus;
if h < 0 { h += item_modulus; }
let bucket = (h % (effective_item_hash_buckets() as i64 - 1) + 1) as usize;
(bucket + offset_item()) as i32
```

- 第一步做线性同余：编号乘一个常数，加一个常数，再对一个大质数取模。
- 第二步把结果映射到 `[1, 桶数 - 1]`，加上该实体区段的偏移，得到统一表里的行号。
- 用户词表大小为 0 时（召回配置），用户哈希退化为常数 1 或 0（`model_config.rs:158-160`）。

服务引擎把这些函数暴露给 Python（`phoenix/crates/serving/xai-recsys-engine/src/python.rs:3683-3690`，类型声明在 `xai_recsys_engine.pyi:261-263`）。

### 常量（开源默认值）

下表取自 `xrecsys.py:172-183`（`xrecsys_two_tower.py:186-197` 的值相同）：

| 实体 | `hash_scales` | `biases` | `modulus` |
|------|------|------|------|
| 用户 | 196_742_702, 1_852_108_266 | 1_935_840_681, 167_407_236 | 2_859_568_897 |
| 帖子 | 2_161_410_491, 1_754_358_832 | 1_935_840_681, 167_407_236 | 2_361_375_383 |
| 作者 | 371_965_780, 328_930_218 | 139_686_260, 37_755_056 | 631_860_353 |
| IP | 529_482_163, 1_327_604_891 | 742_961_053, 318_205_477 | 1_073_741_789 |

源码把用户 `biases` 写作 `193_5840_681, 16_7407_236`，数值与上表相同（`xrecsys.py:173`）。每类实体的哈希个数等于 `hash_scales` 的长度，即 2（`model_config.rs:130-144`）。

## 统一嵌入表

```mermaid
flowchart LR
    R0["第 0 行<br/>padding"] --> R1["64 行<br/>保留区"]
    R1 --> RU["用户区段"]
    RU --> RI["帖子区段"]
    RI --> RA["作者区段"]
    RA --> RP["IP 区段"]
```

- 偏移：`offset_user = 1 + 64`。之后每段的起点等于前一段的起点加前一段的词表大小（`model_config.rs:114-128`，Python 侧在 `recsys_embedding.py:234-248`）。
- 表的总行数为 `input_vocab_size = 用户 + 帖子 + 作者 + IP + 60 + 1`，并向上取整到 512 的倍数（`xrecsys.py:139-153`）。其中 60 是 `ACTION_TYPE_MAP_LEN`（`phoenix/xrex/configs/xrecsys.py:48`）。Rust 侧的偏移却按 64 行保留区计算，两侧数字不一致；这 4 行差额能否被向上取整的余量覆盖，取决于各词表大小，源码未说明。源码未注明 64 行保留区的用途。
- 表宽 `emb_table_width`（排序旗舰为 1024）不同于模型隐藏维 `emb_size`（2560）。模型先把查到的行投影到 `emb_size`，再使用（`recsys_feature_prep.py:388-395`）。

下表列出各配置的区段大小（开源默认值）：

| 配置 | 用户 | 帖子 | 作者 | IP |
|------|------|------|------|----|
| 排序 `home_direct_packed` | 1 亿 | 1 亿 | 3000 万 | 1000 万 |
| 排序 nano | 10 万 | 10 万 | 3 万 | 0 |
| 召回 `xrecsys_two_tower` / `combined` | 0 | 0（哈希桶 1 亿） | 3000 万 | 0 |
| 召回 nano | 0 | 0（哈希桶 10 万） | 3 万 | 0 |

出处：`xrecsys.py:574-577`、`:618-622`，`xrecsys_two_tower.py:507-512`、`:514-520`、`:576-580`。召回配置的帖子区段为 0，但仍有 `item_hash_vocab_size` 个哈希桶。模型照常计算帖子哈希值，用于 log-Q 计数、批内去重和 SID 哈希层，但不再用它查嵌入（推测，依据 `recsys_two_tower_model.py:1202-1260`、`recsys_model.py:1750-1757`）。

## 多个哈希嵌入如何合并

模型有两条合并路径：

**特征准备路径**（排序旗舰、`combined` 召回，`feature_prep_enabled=True`）：模型把每个哈希嵌入各乘一个 `[emb_table_width, emb_size]` 投影，再把结果相加（`recsys_feature_prep.py:384-395`）。例如，用户 token = `Σ user_hash_i × P_i + Σ ip_hash_j × Q_j`（`:502-531`）。帖子与作者的计算方式相同（`_build_seq_tokens`，`:1134-1162`）。

**旧式 reduce 路径**（召回旗舰 `xrecsys_two_tower`、召回 nano）：

- `block_user_reduce`：把用户的 2 个哈希嵌入拼成 `[B, 1, 2D]`，乘 `proj_mat_1` 回到 D 维，再加上 IP 嵌入之和（`recsys_model.py:1673-1711`）。
- `block_history_reduce`：在每个历史位置，把帖子嵌入、SID 嵌入、作者嵌入、动作嵌入、产品面嵌入拼接，乘 `proj_mat_3` 回到 D 维（`recsys_model.py:1947-2060`）。

两条路径都用“第一个哈希是否为 0”生成 padding 掩码（`recsys_feature_prep.py:1160`、`recsys_model.py:1709`）。

## 语义 ID：帖子的另一种身份

生产配置给每条帖子加一个 6 层 × 256 码的语义 ID。语义 ID 由帖子的多模态嵌入经残差量化得到（`phoenix/README.md:241-248`）。实现要点如下（`recsys_model.py:1714-1835`）：

- 6 层各有一张 `[257, sid_embed_dim]` 表（256 个码加 1 个 padding），模型用一次 one-hot 查出全部层。
- `sid_hash_level=True` 时，模型再加第 7 层：把帖子的第一个哈希值乘 `1540483477`，做异或右移 16 位，再对 256 取模。这一层相当于由帖子 ID 决定的“尾码”（`:1750-1757`）。
- `sid_cross_attn=True` 时，各层先做一次小规模注意力，再求平均；否则直接求和（`:1761-1780`）。
- `combined` 召回另外加入前两层组合、前三层组合的前缀嵌入（`sid_prefix2_embed_dim=128`、`sid_prefix3_embed_dim=128`，`xrecsys_two_tower.py:327-328`）。

SID 的生成与服务见 [Phoenix 训练](https://x-algorithm.vergego.ai/concepts/phoenix-training/)。

## 训练时怎样更新

嵌入表使用稀疏的逐行 AdaGrad 更新，与稠密参数的优化器分开。每一步先对本批用到的哈希行去重，再按行累加梯度，只更新被用到的行（`phoenix/reference/train_step.py:33-55`、`:101-148`，`phoenix/TRAINING.md:26-37`）。旗舰排序配置的嵌入学习率为 0.28，半衰期为 2500 步（`xrecsys.py:408-415`）。

## 设计决策

| 决策 | 理由 |
|------|------|
| 每个实体用 2 个哈希 | 两个哈希同时碰撞的概率低；合并后，单次碰撞的影响被摊薄（`phoenix/README.md:241-248`） |
| 编号 0 → 哈希 0 | padding 与有效位置在同一张表里区分；掩码只检查第一个哈希 |
| 使用一张统一表 | 一次 gather 即可查出一批里所有实体的行，便于去重与稀疏更新 |
| 召回改用语义 ID 表示帖子 | 同主题帖子共享前缀；新帖不需要积累互动，也有可用的向量（`phoenix/README.md:109-115`） |

## 源码锚点

- `phoenix/crates/common/xai-recsys/src/model_config.rs:71-260` —— `HashTableConfig`、偏移与哈希函数
- `phoenix/xrex/models/recsys_embedding.py:176-280` —— Python 侧的 `HashKeys`、`HashTable`
- `phoenix/xrex/configs/xrecsys.py:137-180` —— 词表大小与哈希常量
- `phoenix/xrex/models/recsys_feature_prep.py:384-395`、`:502-531` —— 各哈希嵌入分别投影后相加
- `phoenix/xrex/models/recsys_model.py:1673-2060` —— `block_user_reduce`、`embed_entity_sid`、`block_history_reduce`
- `phoenix/reference/train_step.py:33-148` —— 嵌入查找、去重与稀疏更新

## 相关页面

- [Phoenix 排序模型](https://x-algorithm.vergego.ai/concepts/phoenix-ranking/) —— 哈希嵌入构成排序模型的输入
- [Phoenix 召回：双塔检索架构](https://x-algorithm.vergego.ai/concepts/phoenix-retrieval/) —— 召回只用作者哈希与语义 ID
- [Phoenix 训练](https://x-algorithm.vergego.ai/concepts/phoenix-training/) —— 稀疏优化器与语义 ID 的生成
- [RecsysAggregatedModel（排序模型）](https://x-algorithm.vergego.ai/entities/recsys-model/) —— `block_*_reduce` 所在的文件

Source: https://x-algorithm.vergego.ai/concepts/hash-based-embeddings/index.mdx
