Skip to content

候选隔离注意力掩码

候选帖子仅注意用户前缀、历史互动序列与自身;生产系统通过段 ID 编码掩码,并在末尾网络层仅更新候选表征

Updated

这一页回答什么

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)。

为什么需要它

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

段 ID:掩码的表达方式

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

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):

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)。

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 候选隔离
排序模型 P、H、C 三段 1 / -1 / 0 是;旗舰末尾 2 层只更新候选
召回用户塔 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

相关页面

Type to search…

↑↓ navigate↵ selectEsc close