- Sources read: 1
- Canon files updated: 1
- Radar entries: 0
1Slate diversity observation and ads purchase-value prediction head added — X
Source xai-org/x-algorithm@2d4a03c — xAI public repository, 2026-09-15 commit, code. These notes number X commits after the Gen4 release in order, and 2d4a03c is Gen4+21. The change spans 17 files, +592/−9, across 5 axes, and the diff was checked through the GitHub API on 2026-09-16.
Background The final slate of X For You (the set of items shown at once) does not follow the scores assigned by the ranking model Phoenix as is. VMRanker's DPP (θ=0.65, K=150) reordering enforces diversity by cosine similarity, and what remains after heuristics such as author-repeat decay, the OON (out-of-network, accounts not followed) multiplier, and the new-author boost are added is the slate. Until now, X's public code had no metric measuring how much this heuristic layer changes the slate composition relative to the ranker's raw scores, that is, the real size of "diversity gain vs ranking loss". This commit adds that observation layer and also changed the four things below.
- A new connection opened in the ads Phoenix ranker that regression-predicts the purchase value itself as a continuous head (previously only click and conversion probability heads existed).
- A continuous head is an output head that predicts a continuous value by regression.
- The attention kernel in the H100 two-tower training config moved to the same CUTe DSL family as GB300.
- The Kafka OTel export interval was relaxed from 30→120 seconds.
- The root follower threshold of the coordinated spam filter rose from 10k→30k.
What we learnedA. A new observation records response diversity in three stages
A new observation side effect that measures response diversity in three stages was added to home-mixer (the service that assembles the For You feed) verified —
home-mixer/side_effects/response_diversity_stats_side_effect.rs(186 lines, new)- A side effect is a pipeline stage that runs in addition without changing the response.
- By default it runs on only 5% of requests, and inside an experiment bucket it runs on 100%. Each stage emits 12–15 metrics as histograms.
- The other files changed together are
home-mixer/candidate_pipeline/phoenix_candidate_pipeline.rs(+2) andhome-mixer/params/param.rs(+7).
This side effect was wired into the Phoenix candidate pipeline as the 6th observation side effect verified(same commit).
- It is named
ResponseDiversityStatsSideEffect, and its place is afterScoredStatsSideEffectand beforeAuthorServedMetricsSideEffect.
- It is named
There are two run conditions verified(same commit).
- Default: if no experiment bucket is set on the feature-switch param
EnableResponseDiversityStatsExperimentBucket,DEFAULT_SAMPLING_RATE = 0.05applies, that is, it runs on only 5% of requests. - Experiment: if an experiment bucket is set on this param, it runs on 100% inside that bucket and emits each metric with
ddg(experiment id) andbucketscope tags. This means the diversity distribution can be observed separately for each experiment arm.
if experiment_buckets.is_empty() && random::<f64>() >= DEFAULT_SAMPLING_RATE { return Ok(()); }- Default: if no experiment bucket is set on the feature-switch param
Each of the three stages is measured verified(same commit).
final: the finally selected candidates (input.selected_candidates) sorted byscorein descending order. This is the slate actually delivered to the user.top10: the top 10 offinal(TOP_POSITIONS = 10).pre_heuristic: mergesselected_candidatesandnon_selected_candidatesand takes the top K byweighted_scorein descending order (K = final slate size).weighted_scoreis the raw score (head-weight sum) obtained by multiplying per-head probabilities by weights and adding them, the value before applying DPP, author-decay, and the OON multiplier.- In other words,
pre_heuristicis the counterfactual of "what diversity would the slate have had if only the raw-score top-K had gone out without the heuristic layer".
Each stage emits the metrics below, and all names carry the
ResponseDiversity.*prefix verified(same commit).
| Metric | Axis |
|---|---|
Size |
Number of candidates |
UniqueAuthors·UniqueAuthorRatio·MaxAuthorShare |
Author-axis cardinality and concentration |
AuthorHhi·AuthorEntropyNorm |
HHI (Herfindahl-Hirschman Index: the sum of squared per-author shares, a market-concentration metric applied to the slate) and Shannon entropy normalized as entropy / ln(size) |
UniqueSources·SourceEntropyNorm |
Diversity of served_type (the retrieval source axis: Thunder / Phoenix retrieval / SimClusters, etc.) |
InNetworkShare |
Share of in-network (followed author) candidates |
SidCoverage |
Share of candidates that have a semantic ID (SID, an ID that represents a post as an RQ-VAE hierarchical code) |
UniqueSidL1·MaxSidL1Share·SidL1EntropyNorm |
Semantic ID L1 (top-level code of the RQ-VAE stacked residual codebook) diversity |
UniqueSidL2·SidL2EntropyNorm |
Semantic ID L2 diversity |
- A new primitive
Compositionthat extracts multiset statistics for any axis was added verified(same commit). This struct is reused when the hydrator in C below computes the new slate bits.- Its fields are
size,unique,max_count,hhi, andentropy_norm, and the file ishome-mixer/util/composition.rs(63 lines, new). - A single
from_keys<K: Hash + Eq>(iter)produces statistics regardless of the axis.
- Its fields are
- The combination of a 5% sampling probability and three stages is a tuning that keeps observation accuracy while barely affecting ranking latency.
- There are two sorts, and the entropy and HHI computations are O(n) passes over the vector.
- Observation is 100% only in experiment buckets, which leaves open the verification of even the tail distribution.
What we learnedB. A parser that extracts actively engaged authors from the viewer's history was added
A new function collects the ids of positively engaged authors from the viewer's recent interaction history carried in the request verified —
home-mixer/util/viewer_history.rs(61 lines, new)- The function is
positively_engaged_author_ids(columnar_sequence: &bytes::Bytes) -> Option<HashSet<u64>>. - The input is the Arrow IPC stream
ScoredPostsQuery.columnar_scoring_sequencecarried in the request. It holds the user's recent interaction history as columnar batches. - It collects and returns the authorId of rows where
authorIdis greater than 0 and at least one of the 18 positive actions is on in theactionNameMultiHotfixed-size list.
- The function is
The 18 positive actions are stated explicitly in the following list verified(same file).
ServerTweetFav, ServerTweetReply, ServerTweetQuote, ServerTweetRetweet, ClientTweetClick, ClientQuotedTweetClick, ClientTweetOpenLink, ClientTweetClickProfile, ClientTweetPhotoExpand, ClientQuotedTweetPhotoExpand, ClientTweetVideoOpen, ClientTweetVideoQualityView, ClientQuotedTweetVideoQualityView, ClientTweetShare, ClientTweetClickSendViaDirectMessage, ClientTweetShareViaCopyLink, ClientTweetBookmark, ClientTweetFollowAuthor- That is, negative signals such as report, mute, block, and
not_dwelled, and exposure-type signals such as dwell, are intentionally excluded. Two video quality views are in the list.
- That is, negative signals such as report, mute, block, and
This set is a new constant set that has appeared nowhere else in X's public code. It fixes in code for the first time the definition of "authors with whom a relationship was formed through active engagement".
Which ranking head this set corresponds to, that is, whether it exactly matches the positive engagement head definition at
phoenix/runners.py:233-253, is outside the scope of this commit.This function is used to compute one slate bit in C below (
AUTHOR_NOT_ENGAGED_BY_VIEWER).
What we learnedC. 12 bits representing slate diversity were added to the per-candidate bitset
- 12 new slate-axis bits were added to the bitset that records attributes per candidate verified —
home-mixer/candidate_hydrators/tweet_type_metrics_hydrator.rs(+100/−4)- The target is the
TweetTypeMetricsbitset, and the predicate slots are 239-244 and 315-320. - The other files changed together are
home-mixer/util/tweet_type_metrics.rs(+27) andhome-mixer/models/candidate.rs(+8).
- The target is the
- Until now,
TweetTypeMetricsHydratorset the bitset for each candidate with attributes of the tweet itself.- Examples are whether it is a retweet, the media type, the tweet age bucket, and six tiers of author follower scale.
- This commit added six bits (239-244) that reflect slate-wide statistics onto every candidate in the slate verified(same commit). These bits are set identically for all candidates.
UNIQUE_AUTHOR_RATIO_LTE_50_PCT = 239: slate unique_ratio ≤ 0.5UNIQUE_AUTHOR_LTE_5/10/15 = 240/241/242SINGLE_AUTHOR_GTE_25_PCT/50_PCT = 243/244
- It also added six bits (315-320) reflecting the context each candidate has within the slate verified(same commit). These bits are set differently for each candidate.
AUTHOR_REPEAT_IN_SLATE = 315,AUTHOR_REPEAT_GTE_3_IN_SLATE = 316: they turn on from the moment the same author_id is met for the second, and the third or later, time while iterating in descending score order. They are not on at the first encounter. This bit expresses "which appearance of the same author this tweet is in the slate".AUTHOR_NOT_ENGAGED_BY_VIEWER = 317: turns on when the candidate's author is not an account the viewer follows and is not in the positive-engagement set of B either. It is the bit that marks new authors unrelated to the viewer.HAS_SEMANTIC_IDS = 318: turns on when semantic_ids are assigned to the candidate.SID_L1_REPEAT_IN_SLATE = 319,SID_L2_REPEAT_IN_SLATE = 320: in descending-score iteration, they turn on for the second and later candidates when the same SID L1 (or L2) prefix is already in the slate.
- A helper that safely cuts only the top codes from the SID array was added verified(same commit).
PostCandidate::semantic_id_prefix(levels: usize) -> Option<&[i32]>returns a slice of the toplevelscodes, and returnsNoneiflevelsis greater than the array length.
- These bits are merged with the per-candidate base bitset and then stored compressed verified(same commit).
- In
hydrate, the slate-level bit set and the per-position bit set are merged by union into the per-candidate base bitset, compressed, and stored as thetweet_type_metricspayload.
- In
- TweetTypeMetrics has so far been an axis used in ranking training and serving logs and offline analysis, so slate diversity is now logged as a per-candidate feature and becomes accessible to training and modeling. However, this commit has no training config change that actually uses these bits, so only the connection was opened. Whether they are actually used is something to watch in follow-up commits.
What we learnedD. A head that regression-predicts the purchase value after an ads web click was added to the proto
A continuous head predicting the actual purchase value after an ads web click and a per-advertiser purchase-value baseline message were added to the Phoenix proto verified —
phoenix/crates/serving/xai-recsys-proto/proto/recsys.proto(+9)- The head is named
ADS_WEB_CT_PURCHASE_VALUE = 5, and the message is namedPurchaseValueBaseline. - The other files changed together are
phoenix/crates/serving/xai-recsys-proto/src/purchase_value.rs(114 lines, new) andphoenix/python/common/xai-proto/proto/recsys.proto(+9).
- The head is named
The new head is the fifth value of the
ContinuousActionNameenum verified(same commit).- The existing four were
DWELL_TIME = 1,CLICK_DWELL_TIME = 2,ACTIVE_SECS_5M_RESIDUAL_NORM = 3, andBRIDGE_PROBABILITY = 4. ACTIVE_SECS_5M_RESIDUAL_NORMis the residual normalization of the 5-minute moving average of active seconds. It predicts the active seconds of the next 5 minutes in this session, normalized by the previous moving average.BRIDGE_PROBABILITYis the bridge probability between segments.
- The existing four were
The new
ADS_WEB_CT_PURCHASE_VALUE = 5is the regression target for the purchase value that occurs after a web click-through conversion verified(same commit).- CT is an abbreviation of Click-Through, and here it means an actual purchase made on the landing page after an ads click.
A new field 23 was added to the
PredictNextActionsRequestrequest payload verified(same commit).repeated PurchaseValueBaseline purchase_value_baselines = 23; message PurchaseValueBaseline { int64 impression_id = 1; int64 advertiser_account_id = 2; double mean_value_usd_28d = 3; }- Each baseline is a tuple of
(impression_id, advertiser_account_id, advertiser's average purchase value over the last 28 days in USD).
- Each baseline is a tuple of
The Rust helper
purchase_value.rsprovides baseline validation and lookup functions verified(same commit).is_valid_baselinechecks that the id is positive and the USD value is a positive finite number. It also providesfind_baseline(request, impression_id, advertiser_account_id).
Five unit tests pin down wire tag stability and backward compatibility verified(same commit).
- Wire tag: field 23 =
0xBA 0x01, and it is independent ofreturnLogitsListfield 22 =0xB0 0x01 0x01. - Backward compatibility: an old request decodes normally with an empty baseline list.
- Wire tag: field 23 =
Why it matters Until now, ads ranking predicted only the ad click probability and used that probability multiplied by the price the advertiser pays as the ads ranking score. This continuous head instead predicts the purchase value itself that an ad impression and click will actually cause. The ad click probability here also includes the 20 conversion action probability heads introduced in the 2026-08-21 commit (Gen4+6).
If the advertiser's average purchase baseline over the last 28 days is carried in the request payload, the residual of how far above or below that baseline this impression falls can be trained and served. However, this commit does not state whether the amount is the direct training target or a baseline offset. As the probability that this combination of user and ad drives an unusually large (small) purchase relative to the advertiser's average purchase value enters the ads ranking score, X ads ranking is ready to move toward a more advanced value-optimizing auction structure inferred. This is in the same direction as what Meta and Google call VBB (Value-Based Bidding) and tCPA/tROAS, respectively inferred.
That the baseline is carried in each request means a separate offline pipeline must update the 28-day average of advertiser accounts daily and feed it in parallel into ad serving requests. Given the concern about serving traffic overhead, baselines for all advertiser accounts are likely not carried; it reads as attaching only the baselines of those advertisers whose ads will be seriously scored for this viewer at request time, selected by pre-pruning inferred.
What we learnedE. The attention kernel for H100 training was unified into the same CUTe DSL family as GB300
The attention implementation in the H100 training settings changed to a kernel of the same CUTe DSL family as GB300 verified —
phoenix/xrex/configs/xrecsys_two_tower.py(+1/−1)The changed value is
pallas_ranker_varlen_attn→cutedsl_ranker_varlen_attn._H100_OVERRIDES = { "bs_per_device": 480, "ep": 128, - "attn_impl": "pallas_ranker_varlen_attn", + "attn_impl": "cutedsl_ranker_varlen_attn", } _GB300_OVERRIDES = {"bs_per_device": 960, "ep": 64, "attn_impl": "cutedsl_ranker_varlen_attn"}
cutedsl_ranker_varlen_attnis a variable-length attention kernel based on NVIDIA CUTLASS/CUTe DSL. This kernel had been in_GB300_OVERRIDESsince the Gen4 release (47c1bcd, 2026-08-13) (xrecsys_two_tower.py:340). In Gen4+17 (49815da, 2026-09-08), the SM80 and SM90 forward and backward kernels were all released as open source (phoenix/xrex/cutedsl/ranker_fa4/).The SM80 kernel is for A100 (compute capability 8.0), the SM90 kernel is for H100 (9.0), and GB300 (10.3) takes the arch 100 path verified — NVIDIA compute capability table,
cutedsl_archinranker_attention_fa4.py.- So with this switch, H100 training uses the SM90 kernel. The two GPU generations use the same
attn_implname, but the kernel that actually runs differs by generation.
- So with this switch, H100 training uses the SM90 kernel. The two GPU generations use the same
We could not confirm whether this commit lifts the constraint that the seqpack (packing that concatenates multiple user sequences into one batch row) training config forces
pallas_ranker_varlen_attnon H100 only. This constraint was observed in Gen4+11 (bc8e5f0).- If the
NotImplementedErrorcheck stated explicitly in the seqpack code path still remains, the combination of seqpack and FA4 (flash attention v4 based on CUTe DSL) is still blocked. This is something to watch in follow-up commits.
- If the
What we learnedF. The Kafka metric export interval in Phoenix training grew from 30 seconds to 120 seconds
- The Kafka OTel (OpenTelemetry) export interval in Phoenix training grew from 30s → 120s, easing the metric export load by 4× verified —
phoenix/xrex/data/streaming/kafkaloader.py(+2/−2)- The default
export_interval_msof_init_otel_metricsandPhoenixKafkaDataset.metrics_export_interval_msboth changed from 30_000 → 120_000.
- The default
- This change reads as a pair with item H of Gen4+20 (6bb4594, 2026-09-12), the Kafka mTLS connection, but the direction is opposite. mTLS adds authentication overhead, and the relaxed export reduces metric load.
- Exporting OTel metrics 4× less often is a trade-off that gives up observation tail resolution of the training job and reduces the collector burden.
- This relaxation is evidence that metric host cost had actually become a burden inferred.
What we learnedG. The coordinated spam filter raised its root follower threshold from 10k to 30k
The follower threshold of the coordinated spam filter in Grox (the Grok-based nearline annotation pipeline) rose from 10k → 30k verified —
grox/flows/reply_spam/task_filter.py(+1/−1)class TaskCoordinatedSpamFilter(TaskFilterWithPost): - FOLLOWER_COUNT_THRESHOLD_FOR_SPAM_DETECTION = 10000 + FOLLOWER_COUNT_THRESHOLD_FOR_SPAM_DETECTION = 30000The Grox reply-spam pipeline has two separate follower thresholds.
TaskSpamFilter·TaskReplyRankingFilter: since the Gen4 release it rose seven times: 15k → 30k (Gen4+1) → 40k (Gen4+2) → 60k (Gen4+3) → 80k (Gen4+6) → 100k (Gen4+7) → 120k (Gen4+8) → 150k (Gen4+17) verified — per-commit values oftask_filter.py.- Spam classification includes only conversations where both the root and the reply-target account have followers at or below the threshold (lines 77-93). So raising it means expanding the classification scope (inspecting conversations of larger accounts as well).
TaskCoordinatedSpamFilter: it was 1k at the Gen4 release, and after 5k (Gen4+15) and 10k (Gen4+17) it rose for the third time, now to 30k verified — per-commit values of the same file. Coordinated spam is a type in which multiple accounts pair up and repeatedly post the same spam pattern.- This filter skips the conversation when the root author's followers are below the threshold (lines 157-161, reason
low_blast_radius). So raising it removes conversations under small accounts from judgment.
Both thresholds are rising, and the judgment scope of both filters moves toward conversations of large accounts verified — same file. General spam classification widens to conversations of larger accounts, and coordinated spam classification drops conversations under small accounts.
It reads as an adjustment to concentrate the cost of coordinated spam judgment on high-exposure conversations inferred. The basis is that the skip reason is named
low_blast_radius.
Canon update Added a Gen4+21 row to the Gen4 release table in ../services/x.md and created a new detail subsection ### Gen4+21 (2026-09-15, 2d4a03c) — …. The ads section, the feedback signals section, and the Phoenix ranking section also received a short update on the new continuous head and the slate observation metrics, only to the extent connected in code.
Why it matters Two axes opened side by side: slate diversity observation and ads purchase-value prediction. The purchase-value axis was only connected in this commit, and the diversity observation already runs on 5% of requests by default (A above).
- The observation scope widened to slate diversity. Until now, scoring observation in X's public code went only as far as per-candidate scores and per-head probabilities, and how much the slate-layer heuristics (DPP, author-decay, OON multiplier) change the candidate set was not measured.
- The parallel observation of the three stages
pre_heuristic/top10/finalin this commit is the first connection that can measure the actual impact of the heuristic layer. - In particular, since
pre_heuristicis a counterfactual ("without the heuristic"), it makes it possible to measure the trade-off between diversity gain and ranking loss empirically. - The reorganization of the slate layer has continued through the slate context pipeline of Gen4+4, the VMRanker DPP simplification of Gen4+8, and the seed anchor pinning of Gen4+20. This observation reads as the observation-side pair of that line.
- The parallel observation of the three stages
- Ads ranking widened from probability prediction to value prediction.
- The 20 conversion actions of the 2026-08-21 commit (Gen4+6) were still heads predicting probability (whether a click or conversion occurs).
- The new continuous head takes the amount itself as its regression target. It also prepared a connection that carries the per-advertiser 28-day baseline in the request, enabling residual training.
- A value-optimizing auction means advertiser ROI optimization is handled directly in the serving layer, so there will likely be effects on both the ads ranking score computation and the auction logic after it.
Actual consumption (promoting the new bits and head in the training config, a production experiment ramp) is something to watch in follow-up commits.
Canon update summary
Of the canon, only services/x.md was edited, and the pipeline diagram was left as is.
- Added a row to the Gen4 release table in services/x.md and created a new
### Gen4+21detail subsection. - The diagram was not regenerated. The pipeline structure (stages, sources, figures, generation) did not change, and this change is an observation side effect, proto fields, and an attention kernel switch, so
assets/x-pipeline.svgis left as is.
Corrections
No claims were overturned.
Radar
In this batch the arXiv cs.IR API kept responding with 429 (rate limit), so triage could not be done. It was abandoned after 6 retries, and the number of passed items folded in is also 0.
What to check next
- Check whether the ads purchase-value head appears in the training config — this is where it is decided whether it is baseline offset regression or raw dollar regression.
- Check whether
phoenix/xrex/models/recsys_model.pyorxrecsys_two_tower.pyregistersADS_WEB_CT_PURCHASE_VALUEin the continuous loss, and in what shapePurchaseValueBaselineis actually attached to the serving batch.
- Check whether
- Check the experiment results after
ResponseDiversityStatsSideEffectis actually turned on — once 5% sample data accumulates, we can see in which experiment arms the real diversity gain and ranking loss of the heuristic layer diverge. In particular, check the differences inAuthorHhi,InNetworkShare, andSidL1EntropyNormbetweenpre_heuristicandfinal. - Check whether the 12 new bits of
TweetTypeMetrics(239-244, 315-320) are turned on in the training config — if slate context enters as candidate features, the ranker moves to slate-aware scoring (pointwise → slate-aware).- Watch the feature list update in Phoenix
recsys_model.py.
- Watch the feature list update in Phoenix
- Check whether the 18 actions of
positively_engaged_author_idsexactly match the ranking head definition — if they do, this set becomes the canonical constant defining user relationships.- Compare against the positive engagement head indices in
phoenix/runners.py.
- Compare against the positive engagement head indices in
- Check the next value of the coordinated_spam threshold of 30k — it will show whether it keeps rising like the general spam threshold or this was tuning noise.
Lookup failed: arXiv cs.IR API — HTTP 429 (rate limit exceeded), gave up after 6 retries.