Skip to content

Phoenix 训练

Phoenix 排序与召回的训练代码:数据格式、合成数据生成、损失函数、训练步、checkpoint,以及 Rust 服务层加载模型的方式;附单卡概念验证训练的步骤

Updated

这一页回答什么

2026-08-13 起,phoenix/ 不再是演示模型。它包含训练和服务 For You 所用模型的代码,以及一套合成数据生成器(README.md:63、phoenix/README.md:9-21)。本页回答以下问题:

  • 训练数据是什么格式,开源版用什么替代生产数据;
  • 有哪些损失函数,包括 10-08 加入的停留时长损失和 10-13 加入的“停留达标”召回头;
  • 一个训练步做哪些事,checkpoint 怎样写入、怎样恢复;
  • Rust 服务层怎样加载 checkpoint 并对外提供 gRPC;
  • 能否在本地跑一次概念验证训练,以及具体步骤。

核心结论

  1. 上游发布的是生产训练与服务代码,不是样例。未发布的是生产数据流、集群编排、内部监控和训练好的 checkpoint(phoenix/README.md:9-21、phoenix/TRAINING.md:8-10)。
  2. 排序与召回读取同一种训练数据:一行是一个用户会话快照,包含历史序列、候选序列和每条候选的动作标签。生产从 Kafka 流式读取;开源版用 reference/dump_gen.py 写出同格式的 parquet(phoenix/reference/README.md:100-109)。
  3. 合成数据来自一个带种子的“世界”:1 万用户、2000 作者、5 万帖子、16 个话题。生成器按“用户话题偏好 × 作者质量 × 帖子质量”生成互动。模型能从中学到真实结构,但结果不代表线上效果(phoenix/reference/world.py:455-462、:301-311)。
  4. 训练步由稠密优化器和稀疏逐行 AdaGrad 组成:排序旗舰与 nano 用 Muon,召回与旧排序预设用 AdamW。梯度非有限时,整步跳过(phoenix/TRAINING.md:12-36、phoenix/reference/train_step.py:101-148)。
  5. 训练器在每次保存 checkpoint 时生成召回的候选索引,并写进 checkpoint。服务端直接加载该索引(phoenix/README.md:116-119、phoenix/xrex/train/trainer_recsys.py:4195-4250)。
  6. 服务是 Python + Rust 混合进程:Rust 引擎 xai_recsys_engine(PyO3 扩展)负责 gRPC、请求排队、哈希与批次组装、大表加载。Python 侧用 JAX 执行模型前向(phoenix/xrex/inference/model_runner.py:3990、:4238、:4540、:5364)。
  7. 可以在本地跑概念验证:需要 Linux 和 NVIDIA GPU。按 QUICKSTART.md 生成数据,训练 nano 排序与召回模型,启动三个服务,再发一次“召回 → 排序”请求。上游明确说明这只验证流程,不说明模型质量(phoenix/QUICKSTART.md:1-6、:169-170)。

整体流程

flowchart TB
    W["world.py<br/>带种子的合成世界"] --> SNAP["world_snapshots.py<br/>多模态嵌入、SID、发帖时间快照"]
    W --> DUMP["dump_gen.py<br/>会话快照 parquet"]
    SNAP -- "--sid 传入 SID 码" --> DUMP
    DUMP --> TRAIN["train_synth.py<br/>单卡训练 nano 配置"]
    TRAIN --> CKPT["checkpoint<br/>召回版含 post_embeddings 索引"]
    CKPT --> SERVE["launch_inference.py<br/>Rust gRPC + JAX 前向"]
    SNAP --> SIDSRV["sid_index_server.py<br/>SID 查询服务"]
    SIDSRV --> SERVE
    SERVE --> RTR["retrieve_then_rank.py<br/>召回再排序的客户端"]

数据

生产数据格式

  • 生产训练从 Kafka 流式读取 Arrow 格式的用户会话快照。开源版的 aggregated_kafka 读取器从目录读取同格式的 parquet(phoenix/reference/README.md:100-104)。
  • 读取器类为 PhoenixDataset(phoenix/xrex/data/parquet_recsys.py)。配置里的路径占位符为 /path/to/offline_kafka_dump(phoenix/xrex/configs/xrecsys_two_tower.py:232)。
  • 目录结构为 partition={p}/<bucket>/batch_{b}.parquet,另有一个 .valid_batches.json 清单(phoenix/reference/dump_gen.py:52、:377)。
  • 一行包含:用户 ID 与画像;历史序列(帖子、作者、动作多热向量、停留时长等连续值、产品面、时间);候选序列(字段相同,动作作为标签);语义 ID 列 semanticIdSeq(phoenix/reference/README.md:100-109、:132-136)。

合成世界

world.py 一次性生成所有生成器共用的世界(generate_world,phoenix/reference/world.py:455-584)。开源默认值为 num_users=10_000、num_authors=2_000、num_posts=50_000、num_topics=16,种子为 20260721。

互动模型如下(world.py:301-340):

  • 亲和度 = 用户对帖子话题的偏好 × 作者质量 × 帖子质量(:301-305)。
  • 门控:1 / (1 + exp(-14 × (亲和度 - 0.10)))(:307-308)。
  • 各动作概率 = 基础率 × 门控。基础率为:点赞 0.14、点击 0.10、视频观看 0.08、转发 0.045、回复 0.035、收藏 0.02、引用 0.015、分享 0.012(:53-62、:310-311)。
  • 停留达标:有互动且停留 ≥ DWELL_MARK_SECONDS = 10.0 秒。没有互动的记为“未停留”(:72、:337-338)。
  • 帖子年龄在 1 到 36 小时之间(:36-37)。

多模态嵌入与语义 ID

生产环境有两个上游服务:多模态嵌入服务(“v5”)把帖子编码成 1024 维向量;语义 ID 服务把向量量化成 6 层 × 256 码的语义 ID(phoenix/reference/README.md:110-140)。开源版两条路径都提供:

环节 合成路径(默认) 真实算法路径
多模态嵌入 oss_recsys_synth.py 用随机傅里叶特征生成单位向量 mm_encoder.py:渲染帖子文本与图片占位符,用公开的 Qwen/Qwen3-VL-Embedding-8B 编码,截到 1024 维并归一化(需自备权重)
SID 码本 --skip-sid-train 时用随机码本 sid_codebook.py 训练 RQ-KMeans 或 RQ-VAE 码本(镜像自内部流水线)
SID 分配 — sid_assign.py:先用 MLP 编码,再逐层残差量化
SID 服务 sid_mock_server.py 临时生成 sid_index_server.py 从快照 parquet 读取,遵循生产 SidLookupService gRPC 契约

world_snapshots.py 默认用合成向量训练一个 RQ-KMeans 码本,再分配 SID(phoenix/reference/world_snapshots.py:74-110)。产物放在 PHOENIX_INDEX_BASE 目录下:mm_snapshot/post_mm_v5.parquet、sid_snapshot/post_sid_v5_256x6.parquet、sid_snapshot/codebook_v5_256x6.npz、post_creation_snapshots/post_creation_1day.parquet(phoenix/reference/README.md:64-89)。

注意:运行 dump_gen.py 时必须传 --sid,否则数据里没有 SID 列。此时 SID 配置读到的全是“无 SID”;训练不报错,但 SID 表得不到梯度(phoenix/reference/README.md:132-136)。

损失函数

模型 损失 详见
排序:离散动作 64 个槽位的多标签 sigmoid 交叉熵,可选 log-Q 权重(phoenix/xrex/models/loss_recsys.py:21-54) Phoenix 排序模型
排序:停留时长(10-08 起) CREAD:30 个对数阈值的二分类损失取平均,加 0.5 × Huber 还原误差;首页 norm_scale=120 秒、图集页 240 秒,权重各 0.1(loss_recsys.py:110-187、phoenix/xrex/configs/xrecsys.py:337-338、:890-920) 同上
排序:点击后停留 判断是否超过 10 秒的二分类,权重 1.0(loss_recsys.py:190-212、xrecsys.py:334、:884-891) 同上
排序:首页续看 / 主页访问秒数 CREAD,norm_scale=300,权重 0.013 / 0.0035(xrecsys.py:271-313、:552-553) 同上
召回 采样 softmax 对比损失:批内负样本 + 64 个全局负样本 + log-Q 校正,自身负反馈计入分母;每个候选头各算一次后相加(phoenix/xrex/models/recsys_two_tower_model.py:1035-1380、:2767-2800) Phoenix 召回
召回:ld 头(10-13 起) 使用同一对比损失,正样本换成 CLIENT_TWEET_RECAP_DWELLED(recsys_two_tower_model.py:2953-2961、xrecsys_two_tower.py:293-296) 同上

10-08 与 10-13 两次改动分别对应上游提交 35650fb 与 e1a0375。

训练步

可读版本是 recsys_train_step(phoenix/reference/train_step.py:101-148)。生产训练器 RecsysTrainer(phoenix/xrex/train/trainer_recsys.py:453)执行相同的步骤,另加分片与流水线:

  1. 查嵌入:把本批所有哈希(用户、历史帖、历史作者、候选帖、候选作者、IP)拼接起来,一次从统一嵌入表取出对应行(train_step.py:33-55)。
  2. 去重:compress_token_ids 计算本批用到的唯一行和反向索引(:119-122)。
  3. 求梯度:同时对稠密参数和查出的嵌入求梯度(:124-126)。
  4. 稠密更新:梯度范数非有限(或超过可选阈值)时,整步不更新(:87-97、:128-133)。
  5. 稀疏更新:按唯一行累加嵌入梯度,交给逐行 AdaGrad,只更新被用到的行(:58-84、:135-147)。

优化器设置如下(开源默认值):

配置 稠密优化器 关键参数 嵌入优化器
排序 home_direct_packed(H100) Muon 学习率 7.1e-4,muon_consistent_rms=0.2,权重衰减 0.014,b1=0.95、b2=0.98;全局梯度裁剪 1.4 逐行 AdaGrad,学习率 0.28,半衰期 2500 步,权重衰减 5.6e-4(xrecsys.py:385-416、:560-571)
排序 home_direct_packed_nano Muon 沿用 GB300 设置:学习率 5e-4,权重衰减 0.01 逐行 AdaGrad,学习率 0.28,半衰期 5000 步(xrecsys.py:418-444、:599-604)
召回与旧排序预设 AdamW 上游说明:生产在此处用内部调优的 RMS 归一化 Adam 变体,开源版换成标准 AdamW 逐行 AdaGrad(phoenix/TRAINING.md:21-27)

combined 召回配置还在 checkpoint 里保存稠密参数的指数滑动平均(衰减 0.99)。保存时,训练器用平均值替换当前值(xrecsys_two_tower.py:325-326、trainer_recsys.py:3535-3547)。

checkpoint 与恢复

  • 路径:<out>/<config_name>/elapsed_samples_<n>/<run_id>/(phoenix/TRAINING.md:86-92)。
  • 恢复:用同一个 --out 和更大的 --steps 重跑。训练器找到最新的 checkpoint,恢复模型、优化器状态和数据读取位置,并检查校验和(phoenix/QUICKSTART.md:60-80)。
  • 召回 checkpoint 额外包含 post_embeddings:训练器在保存时用候选塔处理整个语料。多头配置按语料类型选择头;混合头(ld)作为额外的列拼在后面(trainer_recsys.py:4195-4250)。
  • reference/repack_checkpoint.py 把 checkpoint 精简成可发布的形态:只保留推理用的张量,去掉优化器状态,重新计算校验和(phoenix/reference/README.md:55)。

Rust 服务层

Cargo 工作区包含 9 个 crate(phoenix/Cargo.toml:3-14):

crate 职责
common/xai-recsys 模型配置、哈希函数、把请求拼成模型输入缓冲区(含陈旧帖计数清零)(crates/common/xai-recsys/src/model_config.rs、util.rs)
serving/xai-recsys-engine PyO3 扩展 xai_recsys_engine:gRPC 服务、请求排队与准入、checkpoint 大表加载(支持 RDMA)、SID 与多模态嵌入客户端
serving/xai-recsys-proto RecsysPredictor(排序)与 RecsysRetrievalPredictor(召回)的 gRPC 定义(proto/recsys.proto:11-24)
serving/xai-recsys-server 服务就绪探针与负载指标
serving/xai-recsys-sid-proto SidLookupService 契约
serving/xai-recsys-sid-retrieval 独立的 SID 前缀召回服务(默认最多 200 个种子、返回 800 条、前缀深度 3 到 6)(src/main.rs:13-38)
serving/xai-recsys-sid-retrieval-proto 上一项服务的 gRPC 定义
serving/xai-recsys-mm-server 多模态嵌入的缓存与拉取
storage/xai-o2 对象存储客户端

加载与服务的过程

flowchart LR
    CLI["launch_inference.py<br/>--service_type ranking 或 retrieval"] --> RUN["RankingModelRunner<br/>或 RetrievalModelRunner"]
    RUN -- "恢复 checkpoint" --> P["JAX 参数"]
    RUN -- "load_tensor_no_resharding" --> PE["post_embeddings 等大表"]
    RUN --> SRV["Rust RecsysPredictorServer<br/>或 RecsysRetrievalPredictorServer"]
    REQ["gRPC 请求"] --> SRV
    SRV -- "哈希、组装批次" --> RUN
    RUN -- "JAX 前向" --> SRV
    SRV --> RESP["gRPC 响应"]
  1. launch_inference.py 按 --service_type 选择 RankingModelRunner 或 RetrievalModelRunner(phoenix/xrex/inference/launch_inference.py:273、model_runner.py:3990、:4540)。
  2. 运行器恢复 checkpoint。召回的 post_embeddings 由 Rust 函数 load_tensor_no_resharding 直接读入内存,再分发到各设备(model_runner.py:2313-2330,Rust 实现在 crates/serving/xai-recsys-engine/src/emb_table.rs:1095)。
  3. 运行器构造 Rust 服务对象 RecsysPredictorServer / RecsysRetrievalPredictorServer(model_runner.py:4219-4245、:5355-5370)。配置了 sid_endpoint 时,服务在请求路径上调用 SID 查询服务,为帖子补齐语义 ID(model_runner.py:4228-4237,phoenix/README.md:123-125)。
  4. Rust 侧接收 gRPC 请求,把请求排队并批量化,把编号哈希成表行号,填好输入数组(排序用 RankingBatchPrep,crates/serving/xai-recsys-engine/src/python.rs:1393)。然后 Rust 侧把批次交给 Python 执行前向,再返回响应。
  5. ReloadModel RPC 支持在不重启的情况下更换模型(python.rs:1167-1200、recsys.proto:15)。

home-mixer 怎样调用这两个服务,见 Phoenix 排序模型 与 Phoenix 召回。

本地概念验证训练

能否运行

可以运行,但有以下硬件与软件前提:

  • Linux + NVIDIA GPU。依赖固定为 jax[cuda12]==0.8.1(phoenix/pyproject.toml:12)。QUICKSTART.md 写的是“CUDA 12”;README.md 的完整验证环境是 nvidia/cuda:13.2.0-base-ubuntu22.04、GB300、aarch64(phoenix/QUICKSTART.md:12、phoenix/README.md:295-303)。
  • 代码识别 A100 / H100 / H200 / GB200 / GB300。注意力核针对 Hopper 与 Blackwell 调优;其他 GPU 可能需要修改 attn_impl 与块大小(phoenix/README.md:298-303)。
  • 需要 uv、Python ≥ 3.11、Rust 工具链、protoc ≥ 3.15,以及 RDMA verbs 头文件等系统包(phoenix/README.md:305-340)。
  • 没有 GPU 的机器:源码未提供 CPU 训练路径的验证说明。

步骤

前提:在 phoenix/ 目录下执行;已按 phoenix/README.md 安装系统包、uv、Rust 和 protoc。

  1. 安装依赖并编译 Rust 引擎(约 1 分钟):
    uv sync --extra engine
    export PYTHONPATH=$PWD
  2. 用随机权重自检。预期日志出现 Model warm up finished(phoenix/QUICKSTART.md:26-30):
    uv run python xrex/inference/oss_bench/bench.py --smoke --service_type ranking
  3. 生成合成快照和训练数据。默认生成 12,288 行、4 个分区、每个文件 1024 行(phoenix/reference/dump_gen.py:687-691):
    uv run python reference/world_snapshots.py --out ./synth_index --seed 20260721
    export PHOENIX_INDEX_BASE=./synth_index
    uv run python reference/dump_gen.py --out ./synth_dump --seed 20260721 \
      --num-rows 12288 --partitions 4 --rows-per-file 1024 \
      --sid ./synth_index/sid_snapshot/post_sid_v5_256x6.parquet --self-check
  4. 训练 nano 排序模型 6 步,并把每步损失记录到 checkpoints/run/metrics.jsonl:
    uv run python reference/train_synth.py --data ./synth_dump --steps 6 --out "$PWD/checkpoints" --metrics
  5. 训练 nano 召回模型 6 步。产出的 checkpoint 带有候选索引:
    uv run python reference/train_synth.py --config xrecsys_two_tower_nano_offline_kafka_dump \
      --data ./synth_dump --steps 6 --out "$PWD/checkpoints"
  6. 按 QUICKSTART.md 第 5 节启动 SID 服务(端口 50061)、召回服务(9990)和排序服务(9988)。等待两个模型服务都输出 Server ready to serve(phoenix/QUICKSTART.md:106-151)。
  7. 发送三次“召回 → 排序”请求。成功时,最后一行是 retrieve_then_rank: 3 session(s) completed the full loop.(phoenix/QUICKSTART.md:153-167):
    uv run python reference/retrieve_then_rank.py --data ./synth_dump --sessions 3 --topk 16 \
      --retrieval-port 9990 --ranking-port 9988

启动器的行为

train_synth.py(phoenix/reference/train_synth.py)的行为如下:

  • 默认配置为 home_direct_packed_nano_offline_kafka_dump,默认 500 步(:15-16)。
  • 只用 1 张卡(:110),并把配置中的数据路径指向 --data。
  • 步数上限 = 数据行数 ÷ 每卡批大小 − 分区数。nano 排序每卡批大小为 64;按默认数据量,最多约 188 步,超过时自动截断(:126-139)。要训练更多步,用 dump_gen.py --num-rows 生成更多数据。
  • 每 steps // 2 步保存一次 checkpoint,结束时再保存一次;评估关闭(:141-146)。

这次训练能证明什么

  • 能证明:代码能在单卡上完整执行“生成数据 → 训练 → 保存 checkpoint → 恢复 → 服务 → 召回再排序”,且损失在合成数据上下降(phoenix/TRAINING.md:83-84)。
  • 不能证明:模型质量、线上效果、吞吐或规模。上游原话是“不是质量或收敛性的声明”(phoenix/TRAINING.md:83-84、:102-112)。
  • 也无法看到线上参数:上游没有发布训练好的 checkpoint(phoenix/TRAINING.md:96-100)。

设计决策

决策 理由
合成数据与生产数据同格式 训练和服务代码无需修改即可运行(phoenix/reference/README.md:11-16)
一个种子生成全部产物 快照、训练数据、SID 描述同一批帖子,彼此一致(phoenix/reference/README.md:90-97)
稠密参数与嵌入分开优化 嵌入表有上亿行;每步只更新被用到的行
索引写进 checkpoint 服务启动时不必重算语料;索引与模型版本一致(phoenix/README.md:116-119)
gRPC 与批次组装放在 Rust 请求解析、哈希、排队不占用 Python 解释器。源码未写理由,此为推测

源码锚点

  • phoenix/TRAINING.md、phoenix/QUICKSTART.md、phoenix/reference/README.md —— 上游训练与快速上手说明
  • phoenix/reference/world.py:301-340、:455-584 —— 合成世界与互动模型
  • phoenix/reference/dump_gen.py:683-737 —— 训练数据生成入口
  • phoenix/reference/train_step.py:101-148 —— 可读训练步
  • phoenix/reference/train_synth.py:94-160 —— 单卡启动器
  • phoenix/xrex/train/trainer_recsys.py:453、:3535-3560、:4195-4250 —— 训练器、保存、召回索引
  • phoenix/xrex/inference/model_runner.py:3990-4245、:4540-5370 —— 排序与召回服务运行器
  • phoenix/crates/serving/xai-recsys-engine/src/python.rs:1393、:1167 —— 批次组装、模型热加载

相关页面

Type to search…

↑↓ navigate↵ selectEsc close