这一页回答什么
2026-08-13 起,phoenix/ 不再是演示模型。它包含训练和服务 For You 所用模型的代码,以及一套合成数据生成器(README.md:63、phoenix/README.md:9-21)。本页回答以下问题:
- 训练数据是什么格式,开源版用什么替代生产数据;
- 有哪些损失函数,包括 10-08 加入的停留时长损失和 10-13 加入的“停留达标”召回头;
- 一个训练步做哪些事,checkpoint 怎样写入、怎样恢复;
- Rust 服务层怎样加载 checkpoint 并对外提供 gRPC;
- 能否在本地跑一次概念验证训练,以及具体步骤。
核心结论
- 上游发布的是生产训练与服务代码,不是样例。未发布的是生产数据流、集群编排、内部监控和训练好的 checkpoint(
phoenix/README.md:9-21、phoenix/TRAINING.md:8-10)。 - 排序与召回读取同一种训练数据:一行是一个用户会话快照,包含历史序列、候选序列和每条候选的动作标签。生产从 Kafka 流式读取;开源版用
reference/dump_gen.py写出同格式的 parquet(phoenix/reference/README.md:100-109)。 - 合成数据来自一个带种子的“世界”:1 万用户、2000 作者、5 万帖子、16 个话题。生成器按“用户话题偏好 × 作者质量 × 帖子质量”生成互动。模型能从中学到真实结构,但结果不代表线上效果(
phoenix/reference/world.py:455-462、:301-311)。 - 训练步由稠密优化器和稀疏逐行 AdaGrad 组成:排序旗舰与 nano 用 Muon,召回与旧排序预设用 AdamW。梯度非有限时,整步跳过(
phoenix/TRAINING.md:12-36、phoenix/reference/train_step.py:101-148)。 - 训练器在每次保存 checkpoint 时生成召回的候选索引,并写进 checkpoint。服务端直接加载该索引(
phoenix/README.md:116-119、phoenix/xrex/train/trainer_recsys.py:4195-4250)。 - 服务是 Python + Rust 混合进程:Rust 引擎
xai_recsys_engine(PyO3 扩展)负责 gRPC、请求排队、哈希与批次组装、大表加载。Python 侧用 JAX 执行模型前向(phoenix/xrex/inference/model_runner.py:3990、:4238、:4540、:5364)。 - 可以在本地跑概念验证:需要 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)执行相同的步骤,另加分片与流水线:
- 查嵌入:把本批所有哈希(用户、历史帖、历史作者、候选帖、候选作者、IP)拼接起来,一次从统一嵌入表取出对应行(
train_step.py:33-55)。 - 去重:
compress_token_ids计算本批用到的唯一行和反向索引(:119-122)。 - 求梯度:同时对稠密参数和查出的嵌入求梯度(
:124-126)。 - 稠密更新:梯度范数非有限(或超过可选阈值)时,整步不更新(
:87-97、:128-133)。 - 稀疏更新:按唯一行累加嵌入梯度,交给逐行 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 响应"]
launch_inference.py按--service_type选择RankingModelRunner或RetrievalModelRunner(phoenix/xrex/inference/launch_inference.py:273、model_runner.py:3990、:4540)。- 运行器恢复 checkpoint。召回的
post_embeddings由 Rust 函数load_tensor_no_resharding直接读入内存,再分发到各设备(model_runner.py:2313-2330,Rust 实现在crates/serving/xai-recsys-engine/src/emb_table.rs:1095)。 - 运行器构造 Rust 服务对象
RecsysPredictorServer/RecsysRetrievalPredictorServer(model_runner.py:4219-4245、:5355-5370)。配置了sid_endpoint时,服务在请求路径上调用 SID 查询服务,为帖子补齐语义 ID(model_runner.py:4228-4237,phoenix/README.md:123-125)。 - Rust 侧接收 gRPC 请求,把请求排队并批量化,把编号哈希成表行号,填好输入数组(排序用
RankingBatchPrep,crates/serving/xai-recsys-engine/src/python.rs:1393)。然后 Rust 侧把批次交给 Python 执行前向,再返回响应。 ReloadModelRPC 支持在不重启的情况下更换模型(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。
- 安装依赖并编译 Rust 引擎(约 1 分钟):
uv sync --extra engine export PYTHONPATH=$PWD - 用随机权重自检。预期日志出现
Model warm up finished(phoenix/QUICKSTART.md:26-30):uv run python xrex/inference/oss_bench/bench.py --smoke --service_type ranking - 生成合成快照和训练数据。默认生成 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 - 训练 nano 排序模型 6 步,并把每步损失记录到
checkpoints/run/metrics.jsonl:uv run python reference/train_synth.py --data ./synth_dump --steps 6 --out "$PWD/checkpoints" --metrics - 训练 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" - 按
QUICKSTART.md第 5 节启动 SID 服务(端口 50061)、召回服务(9990)和排序服务(9988)。等待两个模型服务都输出Server ready to serve(phoenix/QUICKSTART.md:106-151)。 - 发送三次“召回 → 排序”请求。成功时,最后一行是
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—— 批次组装、模型热加载
相关页面
- Phoenix 排序模型 —— 排序模型结构与损失
- Phoenix 召回:双塔检索架构 —— 召回模型结构与损失
- Phoenix 召回索引:rankall 与事件层 —— 生产环境中召回语料快照的来源
- 哈希嵌入 —— 统一嵌入表与稀疏更新
- 端到端推理与训练入口 —— 旧
run_pipeline.py的去向与现在的入口 - 多模态帖子嵌入器 —— 帖子多模态嵌入