---
title: "候选隔离注意力掩码"
description: "候选帖子仅注意用户前缀、历史互动序列与自身；生产系统通过段 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 排序模型将数十条候选帖子拼入同一条序列，系统通过特定注意力掩码保证每条候选独立打分。本页说明三件事：2026-08 起的生产代码如何用段 ID 实现候选隔离；末尾层如何冻结历史状态以节省算力；该约束对召回用户塔产生的影响。

> 旧页面引用的 `make_recsys_attn_mask`（`phoenix/grok.py`）已随 `grok.py` 移除。当前系统不再生成显式的 `[L, L]` 掩码矩阵，改为由每个位置的段 ID 结合注意力算子内部逻辑共同判定可见性。

## 核心结论

1. **候选帖子之间相互不可见**：候选位置只能注意用户前缀、历史互动序列与该候选自身（`phoenix/xrex/pallas/ranker_attention_utils.py:50-56`）。
2. **用户前缀与历史互动序列采用双向注意力**：所有有效的非候选位置共用段 ID `1`，互相完全可见。当前实现与 `phoenix/README.md` 的掩码示意一致，旧文档所述的因果下三角掩码已不再使用（`phoenix/xrex/models/recsys_model.py:2277-2288`、`phoenix/README.md:196-235`）。
3. **掩码由段 ID 编码**：系统给用户前缀与历史互动序列分配段 ID `1`，给候选帖子分配 `-1`，给 padding 填充位分配 `0`（`ranker_attention_utils.py:11-13`）。注意力算子按段 ID 推导历史区间与候选区间，并执行统一的掩码规则。
4. **末尾网络层仅更新候选表征**：旗舰排序配置指定 `num_candidate_only_layers=2`。在全部 8 层网络中，前 6 层计算全序列；后 2 层冻结历史状态，仅计算候选对冻结历史与候选自身的注意力（`phoenix/xrex/configs/xrecsys.py:541`、`phoenix/xrex/models/transformer.py:789-945`）。
5. **所有候选共用相同位置编码**：右锚定 RoPE 将所有候选的位置统一设置为历史序列终点，候选在序列内的排列顺序不影响最终打分（`recsys_model.py:260-281`）。
6. **候选排序分保持完全独立**：单条候选帖子的排序分仅取决于用户特征、历史互动序列与该候选自身，与同批出现的其它候选无关，系统可直接缓存打分结果（`phoenix/README.md:277-281`）。

## 为什么需要它

候选隔离保证单条候选帖子的得分不受同批其它候选影响。[排序模型](https://x-algorithm.vergego.ai/concepts/phoenix-ranking/)将 `[用户前缀 | 历史 S | 候选 C]` 拼接为单条序列输入 transformer。如果候选帖子之间存在交叉注意力，同一条帖子搭配不同候选组合时会得出不同分数，导致分值不稳定且无法建立缓存。候选隔离建立明确保证：**在给定的用户特征与历史互动序列下，单条候选帖子的打分结果保持确定**。

## 段 ID：掩码的表达方式

排序模型在组装输入序列后调用 `attention_segment_ids` 计算各位置的段 ID（`phoenix/xrex/models/recsys_model.py:2277-2288`）：

```python
is_candidate = idx >= candidate_start_offset
valid = padding_mask
if attend_padded_history:
    valid = jnp.logical_or(padding_mask, jnp.logical_not(is_candidate))
segment_ids = jnp.where(valid, jnp.where(is_candidate, -1, 1), 0)
```

- 候选起始偏移量之前的有效位置（用户前缀与历史互动序列）映射为 `1`。
- 候选起始偏移量及之后的有效候选位置映射为 `-1`。
- padding 填充位置映射为 `0`，不参与注意力计算。当 `attend_padded_history=True` 时，历史段内的 padding 同样被标记为 `1`；开源代码默认设为 `False`（`recsys_model.py:1245`）。

注意力算子随后调用 `bounds_from_segment_ids` 推导各行的历史区间 `[history_lower, history_upper)` 与候选区间 `[candidate_lower, candidate_upper)`（`ranker_attention_utils.py:118-143`），并由 `history_candidate_mask` 判定查询向量 query `q` 是否可以注意键向量 key `k`（`ranker_attention_utils.py:16-56`）：

```python
history_mask        = kv_is_history & (q_is_history | q_is_candidate)
candidate_self_mask = q_is_candidate & kv_is_candidate & (q_pos == kv_pos)
return history_mask | candidate_self_mask
```

## 掩码长什么样

掩码规则规定前缀与历史双向互见、候选仅看前缀历史与自身。下表中 `P` 代表用户前缀，`H` 代表历史互动序列，`C` 代表候选帖子；`✓` 代表允许计算注意力，`✗` 代表屏蔽：

```
                 被注意的 key →
              │ P  │  H0 H1 H2 │  C0  C1  C2 │
        ┌─────┼────┼───────────┼─────────────┤
   P    │  P  │ ✓  │  ✓  ✓  ✓  │  ✗   ✗   ✗  │
        ├─────┼────┼───────────┼─────────────┤
   H    │ H0  │ ✓  │  ✓  ✓  ✓  │  ✗   ✗   ✗  │   ← 前缀+历史：双向
        │ H1  │ ✓  │  ✓  ✓  ✓  │  ✗   ✗   ✗  │
        ├─────┼────┼───────────┼─────────────┤
   C    │ C0  │ ✓  │  ✓  ✓  ✓  │  ✓   ✗   ✗  │   ← 候选：看 P+H 和自己
        │ C1  │ ✓  │  ✓  ✓  ✓  │  ✗   ✓   ✗  │
        │ C2  │ ✓  │  ✓  ✓  ✓  │  ✗   ✗   ✓  │
        └─────┴────┴───────────┴─────────────┘
```

前缀与历史不能注意候选，因此历史隐状态在每一层均独立于候选帖子。这一单向隔离特性是下一节冻结历史机制的前提。

## 只更新候选的末尾层

末尾候选专用层通过跳过历史状态更新来节约算力。配置项 `TransformerConfig.num_candidate_only_layers`（`phoenix/xrex/models/transformer.py:86`）将网络末尾 N 层指定为候选专用层。旗舰排序配置 `home_direct_packed` 将该参数设为 2，并配套使用专用算子 `candidate_only_attn_impl="cutedsl_ranker_candidate_only_attn"`（`xrecsys.py:541-542`）。

```mermaid
flowchart TB
    IN["输入 P + H + C"] --> L1["第 1 到 6 层<br/>全序列，按上面的掩码"]
    L1 --> SPLIT["拆开：历史状态冻结"]
    SPLIT --> HS["P + H 状态<br/>不再更新"]
    SPLIT --> CS["C 状态"]
    CS --> L7["第 7、8 层<br/>只算候选"]
    HS -. "作为 K 和 V" .-> L7
    L7 --> OUT["候选输出 → 动作头"]
```

关键实现机制（`transformer.py:789-945`；层栈机制见 `phoenix/xrex/utils/layer_stack.py:281-303`）：

- 层栈先计算前 `num_layers - N` 层，随后调用 `split_history_and_candidates` 将隐状态拆分为历史与候选两个独立张量（`transformer.py:829-838`）。
- 在候选专用层中，候选 query 仅与冻结的历史 K/V 及候选自身的 K/V 计算注意力。专用 CuTe DSL 算子的入参即为 `candidate_q, history_k, history_v, own_k, own_v`（`phoenix/xrex/cutedsl/ranker_attention_candidate_only_fa4.py:371-424`；模块接入见 `phoenix/xrex/models/recsys_attention.py:381-470`、`phoenix/xrex/models/attention.py:26`）。
- 未启用专用算子时，系统对全序列执行标准层计算，再将非候选位置的输出替换为冻结的历史状态（`transformer.py:840-859`）。
- 当配置 `candidate_only_layers_update_user_token=True` 时，系统同步更新用户前缀 token；开源代码默认设为 `False`（`transformer.py:87`、`xrecsys.py:1058-1060`）。

专用算子仅支持 SM90（H100）与 SM100（GB200/GB300）架构。其它 GPU 架构须将 `candidate_only_attn_impl` 设为 `None`，以使用回退路径（`ranker_attention_candidate_only_fa4.py:32-37`）。

## 位置编码：候选共用一个位置

排序模型通过统一位置编码消除候选在序列中的排列偏置。函数 `right_anchored_rope_positions`（`recsys_model.py:260-281`）执行如下规则：

- 历史序列采用右对齐：无论历史记录实际条数多少，最新一条记录始终落位于 `history_end - 1`。
- **所有候选帖子的位置统一设为 `history_end`**（`recsys_model.py:275`）。

RoPE 位置编码因此不区分候选帖子的先后次序。配合注意力掩码隔离，候选帖子在输入序列中调换顺序不会改变模型打分。

## 训练时的序列打包

序列打包机制在提高训练吞吐的同时保持推理契约不变。训练流程通过 `use_seqpack=True` 将多个用户的样本序列打包进同一行矩阵，依靠变长注意力算子按 `cu_seqlens` 切分样本边界（`phoenix/xrex/models/recsys_attention.py:153-190`、`:245-380`）。旗舰配置同时启用 `seqpack_candidate_packing`，系统将候选单独集中存放，随后按 `candidate_positions` 提取对应表征（`xrecsys.py:536`、`recsys_model.py:4394-4406`）。打包机制仅作用于训练阶段以提升吞吐，不改变在线服务阶段的计算契约（`phoenix/README.md:186-191`）。

## 在哪里生效

| 模型 | 序列 | 段 ID | 候选隔离 |
|------|------|------|---------|
| [排序模型](https://x-algorithm.vergego.ai/concepts/phoenix-ranking/) | P、H、C 三段 | `1` / `-1` / `0` | 是；旗舰末尾 2 层只更新候选 |
| [召回用户塔](https://x-algorithm.vergego.ai/concepts/phoenix-retrieval/) | P、H 两段，无候选 | 有效位置全为 `1`（`phoenix/xrex/models/recsys_two_tower_model.py:130-142`） | 不需要；前缀与历史双向注意 |

两个模型的注意力配置均指定 `causal=False`（`xrecsys.py:1038`、`phoenix/xrex/configs/xrecsys_two_tower.py:908`）。

## 设计决策

| 决策 | 理由 |
|------|------|
| 候选互不可见，只留自注意 | 单条候选的分不受同批其它候选影响，可缓存 |
| 前缀与历史双向注意 | 历史不再按时间单向读，每条历史都能看到全部上下文 |
| 用段 ID 而不是 `[L, L]` 矩阵 | 注意力核按区间判断，不必为 1000 多长的序列显式存一张掩码；也能直接支持打包训练 |
| 末尾层只更新候选 | 历史与候选无关，后几层不必重算历史，省算力 |
| 候选共用一个位置 | 候选顺序不进入打分 |

## FAQ

**Q：候选隔离和“批内打分一致”是什么关系？**
A：若候选能互相注意，同一条帖子放进不同批次会算出不同分数。隔离后，在相同的用户与历史序列下，候选帖子的得分保持唯一确定，系统即可跨请求复用缓存（`phoenix/README.md:277-281`）。

**Q：旧页面说用户和历史之间是因果注意力，现在呢？**
A：早期 `grok.py` 演示版使用 `jnp.tril` 构建下三角掩码。生产版中用户前缀与历史共用段 ID `1`，`history_mask` 对它们不施加先后限制，执行完全的双向注意力（`ranker_attention_utils.py:50`）。

## 源码锚点

- `phoenix/xrex/pallas/ranker_attention_utils.py:11-143` —— 段 ID 常量、`history_candidate_mask`、`bounds_from_segment_ids`
- `phoenix/xrex/models/recsys_model.py:2277-2288` —— 排序模型的段 ID 构造
- `phoenix/xrex/models/recsys_model.py:260-281` —— 右锚定 RoPE 位置
- `phoenix/xrex/models/transformer.py:789-945` —— 只更新候选的末尾层
- `phoenix/xrex/cutedsl/ranker_attention_candidate_only_fa4.py:371-424` —— 候选专用注意力核
- `phoenix/xrex/models/recsys_two_tower_model.py:130-142` —— 召回用户塔的段 ID

## 相关页面

- [Phoenix 排序模型](https://x-algorithm.vergego.ai/concepts/phoenix-ranking/) —— 使用候选隔离的排序模型
- [Transformer 骨架](https://x-algorithm.vergego.ai/concepts/grok-transformer/) —— 掩码作用的 transformer
- [Phoenix 召回：双塔检索架构](https://x-algorithm.vergego.ai/concepts/phoenix-retrieval/) —— 不需要候选隔离的召回用户塔
- [RecsysAggregatedModel（排序模型）](https://x-algorithm.vergego.ai/entities/recsys-model/) —— 段 ID 在哪里构造
- [运营迷思 vs 源码真相](https://x-algorithm.vergego.ai/guide/operating-myths/) —— 候选隔离与"被大 V 挤掉"的说法
- [开源版 vs 线上真实算法：已知差异清单](https://x-algorithm.vergego.ai/guide/open-source-vs-production/) —— 开源与线上的差异

Source: https://x-algorithm.vergego.ai/concepts/candidate-isolation-masking/index.mdx
