This repository contains the JAX code for the Phoenix recommendation system, which powers content ranking and retrieval. Phoenix uses transformer-based architectures for both retrieval (finding relevant candidates from millions of items) and ranking (ordering a smaller set of candidates by predicted engagement).
Note: Earlier releases shipped a sample transformer ported from the Grok-1 open source release. This release ships the production implementation itself: the real model code, the real training step, and the real Rust serving engine, exported from the internal tree. What is not shipped is xAI-specific infrastructure (production data feeds, cluster orchestration, internal telemetry) β every such seam is replaced by a documented local equivalent, and synthetic data generators are included so the whole system runs end to end with nothing external. One training-recipe substitution is disclosed in TRAINING.md: for configs on the legacy dense-optimizer slot, the export ships standard AdamW rather than production's tuned internal variant. The flagship ranking configs and the nano twin train the production Muon recipe, which ships in full.
Phoenix is a recommendation system that predicts user engagement (likes, reposts, replies, etc.) for content. It operates in two stages:
- Retrieval: Efficiently narrow down millions of candidates to hundreds, scoring a user embedding against a precomputed candidate index
- Ranking: Score and order the retrieved candidates using a more expressive transformer model
- The production stack, not a sample: the shipped tree (
xrex/plus the vendoredcrates/engine workspace) is the code that trains and serves Phoenix in production β model definitions, trainer, checkpointing, and the Rust gRPC serving engine (built locally viauv sync --extra engine). - Nano configs for one GPU: alongside the production configs, both models
ship single-GPU
nanopresets (home_direct_packed_nanofor ranking,xrecsys_two_tower_nanofor retrieval) that keep the production losses and feature handling, shrunk in width/depth/table sizes so training runs in minutes. The ranking nano is geometry-identical to prod; the retrieval nano additionally trains unpacked with dense attention, a 1022-step history, and no user-features token (see the comparison table below). - Synthetic data generators replace artifact downloads: there is no
checkpoint or corpus bundle to fetch.
reference/world_snapshots.pyandreference/dump_gen.pygenerate a deterministic synthetic world β training dumps, semantic-ID snapshots, a multimodal-embedding snapshot, and the retrieval candidate corpus β andreference/train_synth.pytrains either model on it. Trained this way, the checkpoints serve real gRPC traffic through the same engine production uses.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β RECOMMENDATION PIPELINE β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β ββββββββββββ βββββββββββββββββββββββ βββββββββββββββββββββββ β
β β β β β β β β
β β User ββββββΆβ STAGE 1: ββββββΆβ STAGE 2: ββββββΆ Feedβ
β β Request β β RETRIEVAL β β RANKING β β
β β β β (Two-Tower) β β (Transformer) β β
β ββββββββββββ β β β β β
β β Millions β 1000s β β 1000s β Ranked β β
β βββββββββββββββββββββββ βββββββββββββββββββββββ β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
The retrieval stage uses a two-tower architecture that enables efficient similarity search at scale.
- User Tower: Encodes the user's engagement history through a
transformer to produce a normalized user embedding
[B, D]. The sequence is the user's history plus a single user-features token (coarse profile features β country, language, and more on the combined config) β production retrieval carries no learned per-user ID embedding (use_user_embedding=False); beyond those profile features, the user is represented by what they interacted with. (The nano preset drops the user-features token and is literally history-only.) - Candidate Tower: Computes normalized embeddings for all items in the
corpus
[N, D]. Since the semantic-ID migration, candidates are represented by their semantic IDs β residual-quantized codes (6 levels Γ 256 codes) derived from each post's multimodal embedding β plus hashed author IDs, rather than by hashed post IDs alone. Same-topic posts share SID prefixes, which gives the tower compositional generalization to unseen posts. - Index in the checkpoint: at every checkpoint save, the trainer runs
the candidate tower over the configured corpus and stores the resulting
index (
post_embeddings) inside the checkpoint. Serving loads it from there β nothing embeds a corpus at boot time. - Similarity Search: the serving engine retrieves top-K candidates by dot product between the user embedding and the index.
At serving time the retrieval server hydrates history SIDs through a
semantic-ID lookup service; the launchpad ships a parquet-backed
implementation of the same contract (reference/sid_index_server.py).
The ranking model uses a transformer architecture where candidates cannot attend to each other during inference. This is a critical design choice that ensures the score for a candidate doesn't depend on which other candidates are in the batch.
PHOENIX RANKING MODEL
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β β
β OUTPUT LOGITS β
β [B, num_candidates, num_actions] + dwell regression β
β β β
β β Unembedding β
β β Projection β
β β β
β βββββββββββββββββ΄ββββββββββββββββ β
β β β β
β β Extract Candidate Outputs β β
β β (positions after history) β β
β β β β
β βββββββββββββββββ¬ββββββββββββββββ β
β β β
β βββββββββββββββββ΄ββββββββββββββββ β
β β β β
β β Transformer β β
β β (with special masking) β β
β β β β
β β Candidates CANNOT attend β β
β β to each other β β
β β β β
β βββββββββββββββββ¬ββββββββββββββββ β
β β β
β βββββββββββββββββββββββββββββββββΌββββββββββββββββββββββββββββββββ β
β β β β β
β βΌ βΌ βΌ β
β ββββββββββββ βββββββββββββββββββ ββββββββββββββ β
β β User β β History β β Candidates β β
β β Tokens β β Embeddings β β Embeddings β β
β β [B, 2] β β [B, S, D] β β [B, C, D] β β
β β β β β β β β
β β Hashes + β β Posts + Authors β β Posts + β β
β β Profile β β + Actions + β β Authors + β β
β β Features β β SIDs + Context β β SIDs + β β
β ββββββββββββ βββββββββββββββββββ β Context β β
β ββββββββββββββ β
β β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Since the earlier sample release, the input side has grown a feature-prep stage: besides hashed post/author IDs (and, on history positions, action embeddings), history and candidate positions carry semantic-ID embeddings and context features (timezone, local hour-of-day, product surface, post age); history positions additionally carry dwell time, and a user-prefix token carries profile features (country, language, location, gender, age bracket, installed apps). Production training also packs multiple variable-length sessions per row (sequence packing) and trains with a variable-length attention kernel; both are training-throughput mechanisms β the serving-time contract is unchanged.
A key detail is the attention mask that prevents candidates from attending to each other while still allowing them to attend to the user and history:
ATTENTION MASK VISUALIZATION
Keys (what we attend TO)
ββββββββββββββββββββββββββββββββββββββββββββββΆ
β User β History (S positions) β Candidates (C positions) β
ββββββΌβββββββΌββββββββββββββββββββββββββββββΌββββββββββββββββββββββββββββββββ€
β β β β β
β U β β β β β β β β β β β β β β β β β β β
β β β β β
ββββββΌβββββββΌββββββββββββββββββββββββββββββΌββββββββββββββββββββββββββββββββ€
Q β β β β β
u β H β β β β β β β β β β β β β β β β β β β
e β i β β β β β β β β β β β β β β β β β β β
r β s β β β β β β β β β β β β β β β β β β β
i β t β β β β β β β β β β β β β β β β β β β
e β β β β β
s ββββββΌβββββββΌββββββββββββββββββββββββββββββΌββββββββββββββββββββββββββββββββ€
β β β β DIAGONAL ONLY (self-attend) β
β β C β β β β β β β β β β β β β β β β β β β
β β a β β β β β β β β β β β β β β β β β β β
β β n β β β β β β β β β β β β β β β β β β β
β β d β β β β β β β β β β β β β β β β β β β
β β i β β β β β β β β β β β β β β β β β β β
β β d β β β β β β β β β β β β β β β β β β β
βΌ β s β β β β β β β β β β β β β β β β β β β
β β β β β
ββββββ΄βββββββ΄ββββββββββββββββββββββββββββββ΄ββββββββββββββββββββββββββββββββ
β = Can attend (1) β = Cannot attend (0)
Legend:
ββ User + History: Full bidirectional attention among themselves
ββ Candidates β User/History: Candidates CAN attend to user and history
ββ Candidates β Candidates: Candidates CANNOT attend to each other (only self)
Both models use multiple hash functions per entity for embedding lookup β no dictionary service, deterministic, and collision-tolerant by combining multiple independent hash lookups per entity. Since the semantic-ID migration, posts additionally carry semantic IDs: residual-quantized codes over the post's multimodal embedding, giving the models content-aware generalization that pure ID hashing cannot.
The retrieval user tower uses the same transformer trunk and input
machinery as the ranking model (the combined retrieval config additionally
shares ranking's project-then-sum feature-prep stage; the flagship and nano
retrieval configs use the candidate tower's enable_linear_proj combine β
a small concat-then-MLP β instead); the two models differ in their heads,
not their trunk.
The ranking model predicts many engagement types simultaneously β one logit per action in a shared taxonomy, trained as multi-label targets β plus regression heads for continuous signals (dwell time):
Output: [B, num_candidates, num_actions] (+ continuous-action heads)
β
βΌ
βββββββββββββββββββββββββββββββββββββββ
β Like β Repost β Reply β Click β ... β
βββββββββββββββββββββββββββββββββββββββ
Retrieval trains the two towers contrastively (in-batch and sampled global negatives with log-Q correction) with favorites as the positive signal.
Candidate isolation (ranking) and the checkpoint-baked index (retrieval) together make serving scores independent of batch composition: a candidate's score depends only on the user and that candidate.
Everything below runs from this directory (the launchpad root) with no
cluster, no Kafka, and no production data. The full validated walkthrough β
including expected output and timings β is
QUICKSTART.md; TRAINING.md maps the
training internals component by component.
Validated environment. This walkthrough is exercised end to end on the public
nvidia/cuda:13.2.0-base-ubuntu22.04image (NVIDIA GB300, aarch64, driver 580) with nothing preinstalled beyond what this section adds. The code detects A100 / H100 / H200 / GB200 / GB300 (override withMACHINE_TYPE=<arch>if detection misreads your box), and the attention kernels ship Hopper- and Blackwell-tuned configurations β on other GPU families expect to adjust the servingattn_imploverride and possibly kernel block sizes.
Start with the system packages the engine build and runtime need β a C/C++
toolchain, cmake, pkg-config, RDMA verbs headers, bindgen's libclang, and
libnuma for NUMA-aware pinning (without it the runs log a harmless
numa_num_possible_nodes warning) β on Debian/Ubuntu:
apt update && apt install build-essential ca-certificates cmake curl pkg-config unzip \
libibverbs-dev libnl-3-dev libnl-route-3-dev libclang-dev libnuma-devThen install uv, a
Rust toolchain (https://rustup.rs) and protoc >= 3.15 (the protos use
proto3 optional, which older protoc rejects β Ubuntu 22.04's
protobuf-compiler is 3.12, too old). Install the official release binary
once to a system path:
# pick linux-x86_64 or linux-aarch_64 to match `uname -m`
curl -fsSL -o /tmp/protoc.zip https://github.com/protocolbuffers/protobuf/releases/download/v28.3/protoc-28.3-linux-aarch_64.zip
unzip -o /tmp/protoc.zip -d /usr/local 'bin/*' 'include/*'
protoc --version # libprotoc 28.3On a fresh container image the apt update is load-bearing (a stale package
index resolves build-essential against libc versions the archive no longer
serves), and some CUDA base images additionally hold core libraries at the
image's versions β if the install still reports unresolvable gcc-12-base /
libstdc++6 dependencies, unpin them first: apt-mark showhold, then
apt-mark unhold <the listed packages>.
GPU driver vs. bundled compat layer. The engine and JAX use the host's NVIDIA driver. Some
nvidia/cudabase images ship a forward-compatibility driver layer (/usr/local/cuda-*/compat, wired intoldconfig) built for a newer driver than the host runs; mixing the two segfaults inside the CUDA PTX JIT with no Python traceback. Ifpython -c "import jax; print(jax.devices())"works but real model code dies with SIGSEGV inlibnvidia-ptxjitcompiler, disable the compat layer (remove or rename the/etc/ld.so.conf.d/*compat*.confentry and rerunldconfig) so the host driver's own libraries resolve first.
Then:
uv sync --extra engine
export PYTHONPATH=$PWD--extra engine builds the real Rust serving engine (~1 minute); training
and serving both import it. (The engine links libibverbs, the production
embedding transport; its ibverbs-sys build generates bindings against the
system verbs headers, which is what pulls in libnl and libclang.)
There are no artifacts to download β the synthetic world replaces them:
# 1. Synthetic world: SID + multimodal + post-creation snapshots and the
# retrieval candidate corpus, then a training dump of user sessions.
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
# 2. Train the nano RANKING model, then the nano RETRIEVAL model, on the
# same dump (every retrieval checkpoint embeds the candidate corpus as
# its serving index).
uv run python reference/train_synth.py --data ./synth_dump --steps 6 --out "$PWD/checkpoints"
uv run python reference/train_synth.py --config xrecsys_two_tower_nano_offline_kafka_dump \
--data ./synth_dump --steps 6 --out "$PWD/checkpoints"
# 3. Serve and drive the integrated retrieve β rank loop over real gRPC.
# retrieve_then_rank.py is only the CLIENT: the three servers (SID lookup,
# retrieval, ranking) must be up first β QUICKSTART.md Β§5 has the exact
# launch lines to start them, Β§4 the single-server variant. Run this after.
uv run python reference/retrieve_then_rank.py --data ./synth_dump \
--sessions 3 --topk 16 --retrieval-port 9990 --ranking-port 9988The loop sends each synthetic user's real history to the retrieval server, takes the top-K posts from the checkpoint's index, and has the ranking server score exactly those posts for the same user action sequence β the same contract production composes, over the same two gRPC services.
The production configs and their single-GPU nano twins, as shipped in
xrex/configs/ (xrecsys.py for ranking, xrecsys_two_tower.py for
retrieval):
| Parameter | Ranking (prod) | Ranking (nano) | Retrieval (prod) | Retrieval (nano) |
|---|---|---|---|---|
| Embedding dimension | 2560 | 512 | 1024 | 512 |
| Transformer layers | 8 | 4 | 8 | 4 |
| Query / KV heads (GQA) | 20 / 4 | 4 / 2 | 16 / 4 | 4 / 2 |
| Attention key size | 128 | 128 | 128 | 128 |
| Embedding-table width | 1024 | 128 | 1024 | 512 |
| FFN widening factor | 2 | 2 | 2 | 2 |
| History sequence length | 1022 | 1022 | 1023 | 1022 |
| Candidate sequence length | 64 | 64 | 64 | 64 |
| Sequence packing | yes (varlen attention) | yes (varlen attention) | yes (varlen attention) | no (dense attention) |
| User / Item / Author vocab | 100M / 100M / 30M | 100k / 100k / 30k | β / 100M (hash) / 30M | β / 100k (hash) / 30k |
| IP-address vocab | 10M | 10k | β | β |
| Hashes per entity | 2 | 2 | 2 | 2 |
| Semantic IDs | 6 Γ 256 (input feature) | 6 Γ 256 (input feature) | 6 Γ 256 (candidate identity) | 6 Γ 256 (candidate identity) |
| Multimodal post embedding | v5 on xrecsys_seqpack; off on home_direct_packed |
β | β | β |
| SID cross-attention | no | no | yes | yes |
| Discrete action taxonomy | 64 | 64 | 64 (positives: favorite) | 64 (positives: favorite) |
| Continuous-action heads (dwell) | 8 slots | 8 slots | β (dwell input on combined only) | β |
Candidate index (max_posts) |
β | β | 10.24M (28.67M combined) | 65,536 |
| Global negatives / example | β | β | 64 | 64 |
| Per-device batch | 512 (GB300) / 256 (H100) | 64 | 480 (768 combined-GB300) | 64 |
The nano twins keep production's losses, checkpoint format and serving
contract, and emb_size=512 is the ΞΌP base width β the transformer trunk's
width-dependent LR/scale multipliers are exactly 1 there. The ranking nano
exercises the same input code paths as its production parent
(home_direct_packed), feature prep included β the multimodal-embedding
input is off in both, and is enabled only on the xrecsys_seqpack training
config, as the table shows;
the retrieval nano uses the flagship's enable_linear_proj candidate
combine (a small concat-then-MLP) and trains unpacked (dense attention), as
the table shows.
uv run python xrex/inference/oss_bench/bench.py --smoke --service_type rankingboots the real serving stack with random weights and passes once the server
is up (with the built engine the gRPC port accepts right after warmup and
bench sends one synthetic request; on an install without the engine the
Model warm up finished log line is the fallback success signal) β no
checkpoint or data needed.
This code is licensed under the Apache License 2.0 β see the LICENSE file
at the repository root. Third-party notices for vendored code are in
THIRD_PARTY_NOTICES.md.