1Phoenix gained 20 ad conversion actions and a delayed feedback training branch — X
Source xai-org/x-algorithm @ 28e414f — "Open-source X Recommendation Algorithm" — 2026-08-21 19:10 UTC, public source code. These notes number X commits in sequence after the Gen4 release; this commit is Gen4+6 by number and 28e414f by sha. It changed 35 files, with a scale of +856/−152. Compared with Gen4+5 (d0cef2f, 2026-08-20, 14 files +479/−200) it is nearly 2× the scale, but it is about 1/4 of the large Gen4+4 expansion (79 files +3.3k/−0.7k). What is new in this commit falls on three axes: the number of actions in the ad revenue prediction stack, delayed feedback handling, and the Muon recipe export. The rest tidies up work that began in Gen4+2 and +4 (Grok 4.6 judge, dark_traffic shadow, SlateContext wiring).
Background Phoenix is the ranking model of X For You. It puts the user history and candidates in one sequence and predicts an engagement probability for each candidate. After Gen4, the X repository has continued its follow-up releases on a daily commit rhythm. The order is 8/14 Gen4+1 (c65aa17) → 8/17 +2 (b089ce6) → 8/18 +3 (11a71f8) → 8/19 +4 (aad7179, large) → 8/20 +5 (d0cef2f) → 8/21 +6 (28e414f, this one).
The canon (this repository's X canon document) from Gen4 through +5 has covered three threads. This commit points in the same direction as these three, but differs from Gen4 through +5 in that it adds a new axis: breaking down the ad revenue prediction head.
- First is the expansion of signal channels in the ranking pipeline (topic feedback → AI trend feedback, SID 3 layers,
RewardOutputs·rewardRerankSlotProb,SlateContext).- SID (semantic ID) is an ID that represents a post as a codeword sequence over coarse → fine layers.
- Second is the generation change of the PTOS judge layer (safemodel↔PtOS disagreement → Grok 4.5 judge → 4.6 dial 0.1 → 0.3).
- PTOS (PtOS) is the layer in the grox safety pipeline that decides violation categories on a policy basis.
- Third is VF and grox resource tuning (visibility-filtering shadow, grox reply_spam follower threshold).
- visibility-filtering (VF) is a service that decides, post by post, whether a post may be shown to this viewer.
- grox is a Grok-based nearline annotation pipeline.
The six new concepts, in brief:
- MACT (Mobile App Conversion Tracking) is a measurement framework that attributes in-app events in mobile app ads to ad impressions and clicks.
- In-app events include in-app purchases, tutorial completion, and sign-ups.
- When an advertiser logs an in-app event through the SDK, the X ads system matches which impression or click led to that event.
- The existing X ranker proto had only one,
ADS_ATTRIBUTED_MACT_CLICK_INSTALL, which attributes an app install to a click. This time it grew to 12: 6 in-app events times 2 kinds, click and view.
- View-through conversion refers to the case where a user saw an ad but did not click it, and later reached a conversion (purchase, sign-up, etc.) through another path.
- It contrasts with click-based attribution (click-through), and is a measurement approach that credits an ad with an effect from the impression alone.
- This time the X ranker introduced 8 view-through actions: purchase·add_to_cart·checkout_initiated·sign_up·site_visit·session·landing_page_view·upper_funnel.
- Delayed feedback refers to the phenomenon in ad conversion where the time of the impression or click and the time of the actual conversion (e.g., sign-up, in-app purchase) are separated by anywhere from hours to days.
- At training time it is hard to tell whether a label that has arrived means "not yet happened" or "will not happen", which distorts the loss.
- In the ranker literature, delayed conversion prediction has become an established research area of its own (since Chapelle 2014).
- In this commit, X added an
is_delayed_feedbackcolumn to each training sample. It also split the loss mask so that ad conversion heads train only on delayed samples and engagement heads only on fresh samples.
- Muon optimizer is an optimizer that approximately orthogonalizes the gradient momentum matrix with Newton-Schulz orthogonalization.
- It spread through the Keller Jordan 2024 original text and the LLM training of the Moonshot Kimi K2 family.
- It was introduced in Gen4+4 (aad7179) as the default dense-parameter optimizer of the X Phoenix ranker, but TRAINING.md still stated that the export Python code used AdamW.
- In this commit the docs, config, and reference training script were rewritten to state explicitly that flagship and nano are trained with Muon, which narrowed the gap between the export and production.
- NVIDIA GB300 (Grace Blackwell 300) is NVIDIA's 2025–2026 datacenter GPU. It belongs to the Blackwell generation and succeeds the B200.
- Each GB300 node supports large-capacity HBM3e and attention kernels based on CUTLASS/CUTe DSL.
- X exposed hardware-specific config (batch size, expert parallelism, remat policy, attention impl) explicitly in code for the first time.
- The
xai-o2object storage is S3-compatible object storage presumed to be an xAI internal storage system.- In this commit the client crate interface was released, but the actual implementation was left out of the open-source build.
- This is a signal suggesting that production checkpoint and data storage go through
xai-o2inferred. - The open-source build must substitute the standard
object_storecrate.
20 new ad conversion actions were added at proto numbers 171–190
The new actions are 12 that split MACT in-app events into click and view, and 8 view-through conversions verified.
- 20 ad conversion actions were newly defined in the action list of the Phoenix proto verified —
phoenix/crates/serving/xai-recsys-proto/proto/recsys.proto.They are defined in
enum ActionName.ADS_ATTRIBUTED_MACT_PURCHASE = 171; ADS_ATTRIBUTED_MACT_ADD_TO_CART = 172; ADS_ATTRIBUTED_MACT_LEVEL_ACHIEVED = 173; ADS_ATTRIBUTED_MACT_TUTORIAL_COMPLETE = 174; ADS_ATTRIBUTED_MACT_SIGN_UP = 175; ADS_ATTRIBUTED_MACT_CUSTOM = 176; ADS_ATTRIBUTED_MACT_VIEW_PURCHASE = 177; ADS_ATTRIBUTED_MACT_VIEW_ADD_TO_CART = 178; ADS_ATTRIBUTED_MACT_VIEW_LEVEL_ACHIEVED = 179; ADS_ATTRIBUTED_MACT_VIEW_TUTORIAL_COMPLETE = 180; ADS_ATTRIBUTED_MACT_VIEW_SIGN_UP = 181; ADS_ATTRIBUTED_MACT_VIEW_CUSTOM = 182; ADS_PURCHASE_CONVERSION_VIEW_THROUGH = 183; ADS_ADD_TO_CART_CONVERSION_VIEW_THROUGH = 184; ADS_CHECKOUT_INITIATED_CONVERSION_VIEW_THROUGH = 185; ADS_SIGN_UP_CONVERSION_VIEW_THROUGH = 186; ADS_SITE_VISIT_CONVERSION_VIEW_THROUGH = 187; ADS_SESSION_CONVERSION_VIEW_THROUGH = 188; ADS_LANDING_PAGE_VIEW_CONVERSION_VIEW_THROUGH = 189; ADS_UPPER_FUNNEL_CONVERSION_VIEW_THROUGH = 190;The first group is 12 (171–182): the 6 MACT in-app events times click/view.
- The events are purchase, add_to_cart, level_achieved, tutorial_complete, sign_up, and custom.
- purchase is an in-app purchase, level_achieved is reaching a game level, tutorial_complete is tutorial completion, sign_up is account registration, and custom is an advertiser-defined custom event.
- Each event has two families,
MACT_<event>andMACT_VIEW_<event>. - The former is click-attributed: an event that occurred in the app after the user clicked the ad. The latter is view-attributed: an event that occurred in the app after the user only viewed the ad, without a click.
- Previously only
ADS_ATTRIBUTED_MACT_CLICK_INSTALL(=140) existed, so only app installs were predicted. Now the 6 post-install engagement events are predicted by 12 heads, one each for click and view.
The second group is 8 view-through conversions (183–190).
- They cover purchase / add_to_cart / checkout_initiated / sign_up / site_visit / session / landing_page_view / upper_funnel.
- Each uses the name
<conversion>_VIEW_THROUGH. - View-through counterpart heads were introduced to pair with the existing click-attributed conversions (
ADS_PURCHASE_CONVERSION= 200, etc.). - The 8 web conversions (advertiser site visit, purchase, sign-up, etc.) are now also predicted as view-through.
ads_slim and the ads_conversion_view_through engagement group mapping
12 MACT entries were added to the ad conversion mapping, and a new metric group that the ads ranking config can pick was created verified.
- A new mapping was added to
constants.pyverified —phoenix/xrex/data/recsys/constants.py.12 new MACT mappings were added to the existing
ads_conversion_engagement_to_action_types."IsAttributedMactPurchase": ["AdsAttributedMactPurchase"], "IsAttributedMactAddToCart": ["AdsAttributedMactAddToCart"], ... "IsAttributedMactViewCustom": ["AdsAttributedMactViewCustom"], "IsPurchaseConversionViewThrough": ["AdsPurchaseConversionViewThrough"], "IsAddToCartConversionViewThrough": ["AdsAddToCartConversionViewThrough"], ... "IsUpperFunnelConversionViewThrough": ["AdsUpperFunnelConversionViewThrough"],It also created a metric group called
ads_slim. In addition to 7 signals, it includes only the non-primary entries filtered from the preceding conversion click mappings.ads_slim_engagement_to_action_types = { "IsOpenLink": ["ClientTweetOpenLink"], "IsExternalLinkLongDwelled": ["ClientTweetExternalLinkLongDwelled"], "IsVideoQualityViewed": ["ClientTweetVideoQualityView"], "IsNotInterestedIn": ["ClientTweetNotInterestedIn"], "IsBlockAuthor": ["ClientTweetBlockAuthor"], "IsReported": ["ClientTweetReport"], "IsMuteAuthor": ["ClientTweetMuteAuthor"], **{ eng: actions for eng, actions in ads_p_conv_click_engagement_to_action_types.items() if eng not in primary_engagement_to_action_types }, }That is, the ad ranker's "slim" line trains only 3 positive signals, 4 negative signals, and non-primary conversion clicks.
- The 3 positive signals are open link, external link long dwell, and video quality view.
- The 4 negative signals are not interested, block, report, and mute.
"ads_slim": ads_slim_engagement_to_action_typeswas also registered in the metric group routing dictionary, so that the ads ranking config can pick this group.
- A list of conversion heads to be used in the delayed feedback branch below was introduced.
- The name is
SOURCE_SPLIT_CONVERSION_HEAD_INDICES. - It is a list that concatenates the click-conditioned action index list and the view-through action index list.
- The name is
- When a user is dissatisfied with search results and rewrites the query, this is included in the negative feedback heads. Search surface ranking will learn this signal.
CLIENT_TWEET_SEARCH_QUERY_REFORMULATEDwas added toNEGATIVE_FEEDBACK_HEAD_INDICES.- This signal connects to the
xrecsys_searchconfig expansion in Gen4+4.
A setting that branches delayed feedback out of the training loss was added
With the new setting on, delayed samples train only the conversion heads and fresh samples train only the engagement heads verified.
A new
sample_sourcefield was added to the batch verified —phoenix/xrex/data/recsys/recsys_batch.py:980.sample_source=( record_batch.column("is_delayed_feedback") .fill_null(False) .to_numpy(zero_copy_only=False) .astype(np.bool_) .reshape(-1, 1) if "is_delayed_feedback" in record_batch.schema.names else np.zeros((batch_size, 1), dtype=np.bool_) ),- If the kafka/parquet record batch has an
is_delayed_feedbackcolumn, its value goes into the batch assample_source; otherwise the field is filled with all False. - True means this sample is a delayed feedback re-observation.
- Adding the two columns
is_delayed_feedbackandconversionKeepMaskto the required columns list infeature_config.pyconfirmed the upstream indicator.
- If the kafka/parquet record batch has an
The loss mask splits by sample state verified —
phoenix/xrex/models/recsys_model.py.The branch is in the
loss()function.if self.config.split_head_training_by_source and delayed_mask is not None: conv_head_split_mask = ( jnp.zeros(num_actions).at[jnp.array(SOURCE_SPLIT_CONVERSION_HEAD_INDICES)].set(1.0) ) eng_head_mask = 1.0 - conv_head_split_mask is_delayed = delayed_mask[:, :, None] loss_mask = loss_mask * (1 - is_delayed * eng_head_mask) # engagement head loss is 0 for delayed samples loss_mask = loss_mask * (1 - (1 - is_delayed) * conv_head_split_mask) # conversion head loss is 0 for fresh samplesEach training sample is in one of two states, fresh or delayed. Fresh is the original impression, and delayed is the re-observation after the conversion arrived.
Fresh samples train only the engagement heads (favorite·reply·click, etc.) and set the conversion heads to mask=0.
Delayed samples train only the conversion heads and set the engagement heads to mask=0. Here the conversion heads are the 20 new ones above plus the existing click-conditioned actions.
Metric slices were introduced. A branch was added to
build_metric_masksso that the three slices can be observed.fresh: delayed=0delayed_clicked: delayed=1 & clicked=1delayed_non_clicked: delayed=1 & clicked=0
Because
condition_conversion_on_clickis a required precondition (assert), even delayed samples keep the conditional training that predicts conversion under the condition "was there a click".mact_in_app_loss_weightis a loss weight multiplied only into the 12 MACT actions, and its default is 1.0.- It is exposed as a config parameter, so the training strength of the 12 ad conversion heads can be adjusted.
- It is observed through
stats["mact-in-app-loss-weight"].
train_view_through_headsis a gate that decides whether to train the 8 view-through heads.- When set to False, it sets the loss mask of the view-through heads to 0 so they are not trained.
- That is, view-through prediction has to be turned on as an opt-in in the config.
conversion_keep_mask/conversionKeepMaskis a mask that marks, for each candidate, whether to exclude it from conversion training.from_record_batchreads the kafkaconversionKeepMaskcolumn and replaces null rows with True (= keep).- In
loss(), this mask is ANDed withtarget_padding_maskto exclude entries from conversion training. - One caution: sequence packing allows only
all(True), and the not-yet-connected case explicitly fails with an assert.
Why it matters delayed feedback modeling is a standard problem in ad conversion prediction. X now sends delayed re-observations as separate kafka events, and built a setting that branches the loss in ranker training so that fresh samples train on engagement and delayed samples on conversion. That setting, split_head_training_by_source, defaults to False, so it is still opt-in verified — recsys_model.py:560@28e414f. This is a loss-consistency device for the duplicate labeling problem (logging the same impression twice, as a fresh event and a delayed event), and also infrastructure support that keeps the training signal of each head sufficiently refreshed as the conversion prediction heads grow to 20.
The Muon optimizer is now formally included in the export
The export docs now state that the flagship ranking config and nano train with the production Muon recipe verified.
TRAINING.md was rewritten verified —
phoenix/TRAINING.md.The previous wording described the export's dense-parameter optimizer as standard Optax AdamW.
The dense-parameter optimizer in this export is standard Optax AdamW. The internal deployment uses a tuned RMS-normalized-Adam derivative in that optimizer slot.
The new wording lists two families, Muon and AdamW. Muon is used by the flagship ranking config and
home_direct_packed_nano, and AdamW by the remaining configs.Two dense-parameter optimizer families ship in this export:
- Muon (
xrex/optimizers/recsys/muon.py): the production home-ranker recipe — consistent-RMS scaling with decoupled weight decay on the matrix and embedding partitions. The flagship ranking configs andhome_direct_packed_nanoselect it (optim="muon"), so the nano trains with the same dense-optimizer recipe production runs. - Standard Optax AdamW: the slot used by the remaining shipped configs (two-tower retrieval, gen-recs, and the legacy ranking presets). ...
- Muon (
README.md was likewise rewritten toward Muon.
- The previous wording was "one training-recipe exception ... standard AdamW rather than production's tuned internal variant".
- The new wording is "The flagship ranking configs and the nano twin train the production Muon recipe, which ships in full".
- The
train_step.pyentry inreference/README.mdwas also generalized to "dense optimizer + rowwise-AdaGrad", and it states explicitly that flagship and nano use Muon.
The embedding optimizer was also folded into the canon. TRAINING.md states explicitly that flagship and nano use three options together in embedding rowwise AdaGrad.
- The three options are accumulator half-life decay, lazy per-row decay, and decoupled weight decay.
- In effect, the Gen4+4
emb_optim_configparameters (LR 0.28, half-life 2500 step, lazy_decay, weight_decay 2.8e-4) were confirmed as canon.
Why it matters With this commit, running the exported training Python code as is now yields the same training as the production Muon recipe. When Muon was introduced as code in Gen4+4 (2026-08-19), the canon's understanding was "the code is exported but the training recipe is still production-specific". The documentation on reproducibility shows a smaller gap, but the data and infrastructure are still not public (the README caveat remains as it was).
GB300 hardware-specific overrides appeared for the first time
The config defines GB300 hardware-specific overrides verified. The nano config included in the export also inherits these overrides.
_GB300_OVERRIDESis defined verified —phoenix/xrex/configs/xrecsys.py:330-345._GB300_OVERRIDES = { "bs_per_device": 512, "ep": 64, "attn_impl": "cutedsl_ranker_varlen_attn", "remat_policy": RematType.SAVE_GB300_RECSYS, "unroll_layer_stack": True, "learning_rate": 5e-4, "checkpoint_every_n": 300, "optim_config": RecsysDenseOptimConfig( optim="muon", muon_consistent_rms=0.2, muon_matrix_weight_decay=0.01, muon_split_fused="qkv:128", adam_embedding_weight_decay=0.01, b1=0.95, b2=0.98, ... ), ... }bs_per_device=512is 8× larger than the previous_home_direct_packed_nanovalue ofbs_per_device=64. It reflects the ample HBM capacity of the GB300.ep=64sets expert parallelism to 64-way. It is MoE (mixture-of-experts) style sharding.attn_impl="cutedsl_ranker_varlen_attn"is a variable-length attention kernel based on NVIDIA CUTLASS/CUTe DSL. It is a new attention implementation optimized for the GB300.remat_policy=RematType.SAVE_GB300_RECSYSis a rematerialization policy tuned to the GB300 memory hierarchy. It decides what to checkpoint and what to recompute.unroll_layer_stack=Truecompiles the Transformer layer stack unrolled (listed explicitly instead of looped). This gives XLA more room to optimize.learning_rate=5e-4is slightly lower than the Gen4+4 flagship base LR of 7.1e-4. It appears to be an adjustment matched to the 8× larger bs_per_device inferred.checkpoint_every_n=300saves a checkpoint every 300 steps.
_home_direct_packed_nanoinherits this override.- Previously it set
learning_rate=2e-3separately. - In this commit it inherits the LR, optim_config, and emb_optim_config of
_GB300_OVERRIDESas they are. - In other words, even the "nano" model included in the export is tuned on the assumption of GB300 training.
- Previously it set
Why it matters This is the first case of X exposing a hardware generation change in code; the earlier canon said "which GPU Phoenix runs on is undisclosed", but there is now a strong signal that GB300 is the production standard hardware. NVIDIA GB300 began shipping in the second half of 2025 and succeeds the Blackwell B200. It is the latest generation that LLM companies acquire in bulk, and xAI applied it directly to ranker training and serving pipelines. cutedsl_ranker_varlen_attn may be a kernel xAI built itself, since this name is absent from the open-source kernel catalog inferred.
The interface of the xai-o2 storage crate was released
An interface crate for an S3-compatible object storage client was newly added verified. In the open-source build, trying to create this client always returns an error.
The
xai-o2crate was introduced verified —phoenix/crates/storage/xai-o2/.The description in Cargo.toml reads:
Client interface for the xai-o2 (S3-compatible) object storage backend
There are 3 files, 134 lines in total.
base_client.rs(12 lines) is theBaseO2Clientasync trait. It is a minimal interface that defines onlyput(key, data) -> Result<()>.O2ClientBuilderino2_client_builder.rs(115 lines) exposes 15 builder methods. The first eight are endpoint·bucket·prefix·access_key_id·secret_access_key·allow_anonymous·timeout·io_timeout. The other seven are retry_max_times·retry_min_delay·retry_max_delay·write_chunk_size_mib·write_chunk_concurrency·max_concurrent_write_requests·trace_always.lib.rs(7 lines) contains onlypub moddeclarations.
In the open-source build,
O2ClientBuilder::build()always returns the following error:pub const BACKEND_UNAVAILABLE: &str = "the xai-o2 backend is not included in this build; use the default object_store backend"; pub fn build(&self) -> Result<Arc<dyn BaseO2Client>> { Err(anyhow::anyhow!(BACKEND_UNAVAILABLE)) }- The error message returned by
build()is"the xai-o2 backend is not included in this build; use the default object_store backend". - That is, only an interface stub was released, and the actual implementation is not included in the open-source build.
- The
xai-recsys-enginecrate does declare the dependency asxai-o2 = { workspace = true }, but using it in practice requires substituting another backend.
- The error message returned by
Why it matters It was confirmed for the first time that xAI has an internal S3-compatible object storage system named xai-o2. The 15 builder parameters are meaningful because they list the items actually tuned in production. The test (build_fails_loudly_after_full_configuration) contains values close to real operation: chunk 8MiB, concurrency 16, max_concurrent 64, retry 6 times, min/max delay 1/32s, timeout 300s. This is a signal that production checkpoint and data storage connect here inferred; the open-source build must substitute the standard object_store crate (AWS S3, etc.).
SlateContext is also carried in NextActionDistribution
A slate context field was added to the proto message that holds the per-action prediction distribution for each candidate verified.
SlateContextwas added to theNextActionDistributionmessage as field 9 verified —phoenix/crates/serving/xai-recsys-proto/proto/recsys.proto.message NextActionDistribution { map<uint32, float> indexToContinuousValues = 7; RewardOutputs rewardOutputs = 8; + SlateContext slateContext = 9; }- This value was introduced in Gen4+4 as
ScoreInfo.slateContext. It is the context carried with the final decision of the re-ranking slate.
- This value was introduced in Gen4+4 as
Why it matters Slate context can now be observed in both ScoreInfo and NextActionDistribution: the former is the re-ranking slate decision slot, and the latter is the action prediction slot. In Gen4+4 (aad7179), SlateContext first appeared as a field of the ScoreInfo message, made up of pool_rank, fatigue, pre_diversity, three SID layers, and three SID gap layers. In Gen4+5 (d0cef2f), PostCandidate.served_slate_context was connected, so home-mixer, the service that receives For You and Following feed requests, began consuming this value. In Gen4+6, SlateContext also enters NextActionDistribution (each candidate's per-action prediction distribution); that is, Phoenix can return a per-action prediction distribution for one candidate in the serving response and attach the slate context next to each action distribution.
Canon update Introduced a ### Ads conversion action expansion — Gen4+6 (2026-08-21, 28e414f) section in the ads section of ../services/x.md. The remaining Gen4+6 items were added as paragraphs within the relevant sections without creating new sections. The Muon export promotion, GB300 override, xai-o2 stub, and per-action SlateContext went into the Phoenix training·index section. The grox threshold of 80k, dark_traffic staging, and the 100% switch to Grok 4.6 were folded into their respective sections.
grox reply_spam follower threshold rose from 60k to 80k (4th increase, 1.33×)
The follower threshold used for spam verdicts and for reply ranking rose from 60,000 to 80,000 in both places verified.
The two threshold constants in
task_filter.pychanged together verified —grox/flows/reply_spam/task_filter.py.- FOLLOWER_COUNT_THRESHOLD_FOR_SPAM_DETECTION = 60000 + FOLLOWER_COUNT_THRESHOLD_FOR_SPAM_DETECTION = 80000 ... - FOLLOWER_COUNT_THRESHOLD_FOR_REPLY_RANKING = 60000 + FOLLOWER_COUNT_THRESHOLD_FOR_REPLY_RANKING = 80000The threshold went through five stages over 8 days starting 2026-08-13 verified.
- 15,000 (before 2026-08-13)
- 30,000(2026-08-14, Gen4+1)
- 40,000(2026-08-17, Gen4+2) —
task_filter.py@b089ce6 - 60,000(2026-08-18, Gen4+3)
- 80,000(2026-08-21, Gen4+6)
The previous three steps were 2×, 1.33×, and 1.5× (15→30, 30→40, 40→60), and this 1.33× is a smaller increase than the one before.
Why it matters Only replies whose thread root and reply target are both at or below this threshold receive the grok spam verdict, so as the threshold rises, the spam verdict covers replies from larger accounts. The reply ranking scoring that a reply receives when either one exceeds the threshold narrows by the same amount verified — task_filter.py:77-92, 214-225. One of three explanations may account for the slowdown at 80k: the grok budget bottleneck is near, larger accounts attract fewer spam replies so marginal utility fell, or the next step is preparing a full rollout with no threshold inferred.
visibility-filtering dark_traffic target changed from shadow to staging
The VF dark_traffic mirroring target changed from the production shadow workload to the staging environment verified.
In
dark_traffic_setup.rs, the shadow workload constant was removed and a staging destination was added verified —visibility-filtering/dark_traffic_setup.rs.-const SHADOW_WORKLOAD: &str = "xai-vf-shadow"; +pub const STAGING_XDS_DEST: &str = "xai-vf-service.staging.visibility:grpc"; +const FORWARDER_NAME: &str = "staging"; + +pub fn staging_tls_domain(dc: &str) -> String { + format!("visibility.visibility-filtering-service.staging.{dc}.s2s.twttr.net") +} ... -struct StaticShadowDiscovery; +struct StaticStagingDiscovery;- The new destination is
xai-vf-service.staging.visibility:grpc.
- The new destination is
The dark_traffic infrastructure was introduced 2 days earlier, in Gen4+4 (aad7179, 2026-08-19). This time it was renamed from the production shadow workload to the staging environment.
- The target changed from
xai-vf-shadow.prod.visibilitytoxai-vf-service.staging.visibility. - The TLS domain also moved from
visibility.visibility-filtering-service.prod.{dc}.s2s.twttr.netto.staging.. - The
StaticShadowDiscoverystruct was also renamedStaticStagingDiscovery.
- The target changed from
Why it matters ⚠ Claim changed: the Gen4+4 shadow statement needs to be reinterpreted. dark_traffic changed from a structure that mirrors production traffic to a production shadow workload into one that mirrors production traffic to a staging environment. In effect, the direction changed after 2 days. There are two interpretations:
- An infrastructure cleanup: the staging environment is used like a shadow instead of keeping a separate shadow workload.
- A judgment that a shadow of production traffic is useful for verifying staging deployments.
Either way, VF experiments now run on the staging resource budget rather than production shadow resources. The canon through Gen4+5 said "the shadow deploy foundation is wired", and that foundation is now redefined as staging mirroring.
Grok 4.5 internal removed entirely and PTOS judge switched to 100% Grok 4.6 internal
Code related to Grok 4.5 internal was deleted from the PTOS classifier, and the two calls that had a probability branch now always use the 4.6 internal family of models verified.
- Removed the 4.5 internal code from
classifier.pyverified —grox/flows/ptos/classifier.py(+6/−63).- Deleted items:
_EAPI_4_5_INTERNAL_BREAKER_CONFIG(circuit breaker config)_eapi_4_5_internal_breaker(breaker instance)_GROK_4_6_INTERNAL_DIALconstant (it was 0.3 the day before)- Removed
self.eapi_4_5_internalinitialization from two classes - Removed
_sample_4_5_internalmethod
- Removed the probability branch in
_cross_model_validate_with_4_5. It now always calls 4.6 internal directly. - Removed the probability branch in the deluxe 4.3 reclassification of
classify_policy_for_violation. It now always usesdeluxe-4.6-internal.
- Deleted items:
Why it matters The 4.6 internal dial-up, which started at 10% in the 2026-08-19 commit (Gen4+4) and reached 30% in Gen4+5, ended with the 100% switch in Gen4+6. The dial was applied to the child-safety cross-validation and the deluxe reclassification that have existed since the Gen4 release, not to the adult content judge in the 2026-08-17 commit (Gen4+2) verified — classifier.py:114, 281, 477. Grok 4.5 internal is gone from both the classifier code and the grox config ModelName list, so it cannot be rolled back verified — grox/config/config.py:79-92. The canon through Gen4+5 said "the dial's next move is the point to watch — a jump to 50%/100% means a near-complete switch", and that point to watch is now resolved.
Retrieval post-embedding computation is now processed in 65k chunks on even shards
Retrieval post-embedding computation splits a shard into chunks of at most 65,536 rows only for even shards; otherwise it processes the whole shard at once as before verified.
- Refactored
maybe_build_retrieval_post_embeddingsand introduced a_forward_chunkedhelper verified —phoenix/xrex/train/trainer_recsys.py.- Previously, the whole shard was computed in a single
candidate_tower_forward_jitcall. - Now it enters chunking with
_chunk_rows = min(_shard_rows, 65536)only in the even-shard case wheretotal_samples % data_world_size == 0. - Otherwise it processes the whole shard at once (
_chunk_rows = _shard_rows). - For each chunk, it computes in the order
jax.make_array_from_process_local_data→_lookup→candidate_tower_forward_jit→ local shard concatenate, and concatenates at the end. - Even when
head_dataset_mappinghas several heads, it calls_forward_chunkedfor each head and chunks in the same way.
- Previously, the whole shard was computed in a single
Why it matters When the retrieval two-tower post embedding batch grows, there is a risk of OOM at XLA compile and execution time, so it is cut into 65k-row units and processed sequentially. This stabilizes the subroutine as training scale grows with the Muon promotion and the GB300 config.
--hotswap_malloc_trim CLI added to inference hotswap
An option was added that calls glibc's memory trim function at the end of every inference hotswap cycle, and its default is True verified.
- A new option entered the inference code verified —
phoenix/xrex/inference/model_runner.py,launch_inference.py.- The
InferenceModelRunnerfieldhotswap_malloc_trim: bool = Truewas added. - The CLI argument
--hotswap_malloc_trimwas added, and its default is True. - The
_malloc_trim_after_swap()method calls_glibc_malloc_trim()(= libcmalloc_trim(0)) at the end of every hotswap cycle. - Observation is done with
metrics_publisher.checkpoint_reload_step_seconds.labels(step="malloc_trim").observe(secs).
- The
The CLI help states this problem explicitly.
glibc malloc_trim(0) on the coordinator thread after each applied hotswap; returns the cycle's freed pages (glibc slots otherwise ratchet ~196 MiB/cycle). No-op under jemalloc.
Why it matters The documentation reveals for the first time that embedding table hotswap in Phoenix serving has been leaking about 196MiB of RSS per cycle under the glibc memory allocator. Embedding table hotswap periodically swaps a new embedding checkpoint in-process. Calling libc malloc_trim(0) returns the freed pages to the OS, and it has no effect under the other allocator, jemalloc. This is a small patch that polishes production serving, but it also shows that hotswap is long-running infrastructure in production, where enough load accumulated to make this patch necessary.
Three options added to the nano config of two-tower retrieval
Three attention and positional-encoding options were added to the small training config (nano) of retrieval two-tower verified. A stabilization technique common in LLM training has carried over to retrieval two-tower.
- They were added to the
xrecsys_two_tower_nanoentry ofMODEL_CFGSverified —phoenix/xrex/configs/xrecsys_two_tower.py:429-431._xrecsys_two_tower_combined_base()in the same file did not change in this commit and keptqk_norm=Falseandattn_logit_cap=80.0(xrecsys_two_tower.py:273-274)."qk_norm": Trueturns on attention query/key normalization."attn_logit_cap": -1turns off attention logit capping (-1 = unused)."right_anchored_rope": Truesets RoPE positional encoding to right-anchored. That is, it anchors relative to the end of the sequence.
EVERGREEN retrieval dataset shrank from 5 years to 4–14 days
The range of videos referenced by the EVERGREEN retrieval dataset shrank substantially verified.
The parquet file that the
EVERGREENentry inretrieval_dataset.pypoints to changed verified —phoenix/xrex/data/retrieval_dataset.py:255-258.EVERGREEN = ( 5, - _idx("post_sid_v5_256x6_snapshots/evergreen_video_1825day.parquet"), - _idx("post_sid_v5_256x6_snapshots_backup/evergreen_video_1825day.parquet"), + _idx("post_sid_v5_256x6_snapshots/video_4to14day.parquet"), + _idx("post_sid_v5_256x6_snapshots_backup/video_4to14day.parquet"), )The name is still EVERGREEN, but what it actually references shrank sharply from 1825 days (5 years) to recent videos within 4–14 days.
It appears to be an experiment putting videos from the most recent 4–14 days into the evergreen slot inferred. The name and the substance do not match, so it may be a temporary change.
Other small patches
Small patches went into request metrics, checkpoint loading, the kafka loader, and the grox config.
- Fixed double counting in request_metrics.rs. A request shed by
deadline_admissionwas then also counted asinflight_capbecause of the gRPCResourceExhaustedstatus code; theguard.recorded_rejectflag now prevents this.- Added two new tests:
deadline_shed_does_not_double_count_as_inflight_cap,resource_exhausted_without_prior_reject_counts_inflight_cap.
- Added two new tests:
- Made checkpoint metadata handling more robust. It skips checkpoints that have only the completion marker (
COMPLETED_FILENAME) but no actual orbax tmp-dir commit (rename).- Renamed
_ORBAX_TMP_PREFIXto the public constantORBAX_TMP_DIR_SUFFIX. The new name matches the suffix semantics. - The
_has_committed_payloadand_is_loadable_checkpointhelpers make the skip decision. - It prevents reading a corrupted checkpoint when a crash happened midway through a checkpoint.
- Renamed
- Added
exclude_required_columnsto kafkaloader. It lets a particular kafka feed exclude some required columns.- When an expected column is missing, it skips both the warning log and the schema-mismatch error path.
- Removed 1 line from grox
config.py. It is a trivial cleanup, so the concrete diff is omitted.
Canon update Changed the following parts of ../services/x.md:
- Added paragraphs on MACT/view-through 20 actions, the
ads_slimmetric group, and the delayed feedback split to theadssection. - Added paragraphs on the Muon export promotion, GB300 override, and xai-o2 stub to
Phoenix training·index (introduced in Gen4). - Reversed the shadow → staging statement for dark_traffic in the
safetysubsection. - Folded the statement finalized in Gen4+6 (complete removal of 4.5 internal) into the PTOS judge section.
- Added a
### Resolved by the Gen4+6 release / new (2026-08-23)section toopen questions. - Added the 28e414f commit and related file links to
sources.
Why it matters Gen4+6 added a fourth major axis, the ads revenue prediction stack. From Gen4 through +5, the canon's narrative axes were expansion of ranking signal channels, three SID layers, and RewardOutputs; the PTOS judge generation change; VF and grox resource tuning; and the pipeline v3 migration. The new axis changes the following:
- Introduced 20 proto ActionNames, greatly increasing the total number of actions.
- Formally handled delayed feedback as a training loss branch. X ranker training is now explicitly aligned with the literature on ads delayed conversion prediction.
- The
ads_slimmetric group separates the signal combination of the ads ranking line. - The
mact_in_app_loss_weightandtrain_view_through_headsconfigs expose a way to tune the training intensity of the ads heads.
At the same time, the Muon training recipe was formally included in the export and a GB300 hardware-specific config appeared for the first time, so this cycle delivered reproducible training infrastructure and confirmation of the hardware generation side by side. The release of the xai-o2 stub states explicitly, for the first time, which storage layer of the infrastructure holds xAI's own system. The redefinition of dark_traffic from shadow to staging matters because it is not just an observation to leave in the documentation but a reinterpretation of Gen4+4 that the canon must reflect.
Corrections
- VF dark_traffic was redefined from production shadow to staging mirroring. When Gen4+4 (2026-08-19) was folded into the canon, the statement was "mirror production traffic to
xai-vf-shadow.prod.visibility". After 2 days, the shadow workload reference was deleted and redirected to the staging environment. The statement in the Gen4+4 dark_traffic section of../services/x.mdwas rewritten in this Gen4+6 update.
Radar
None. Triaged 30 new arXiv cs.IR entries from 8/19–8/20, and 0 passed. Most are academic papers (dense retrieval RL, sequential rec benchmarks, financial RAG, etc.), and none contained a production claim. There are no new posts on Meta engineering or the Netflix tech blog (Meta's latest post is 2026-08-12, and Netflix's 2026-08-21 post covers the Flink autoscaler and is pure infrastructure).
What to check next
- Upstream pipeline for the column that marks whether a sample is a re-observation of delayed feedback — the source of the conversion re-observation event logger that sends this column into kafka is not in the code. It is probably somewhere in the server-side conversion tracking API, but it is not included in the open-source build. The source of this column may be revealed in the next commit.
- The target column is
is_delayed_feedback.
- The target column is
- When
split_head_training_by_sourcebecomes active in production — the config default is False, so it is still opt-in. The point at which it is promoted to True in_home_direct_packed_base()or the flagship config is the signal of the ads conversion training pipeline's production switch. - Promotion of
train_view_through_heads— the 8 view-through heads are opt-in. If this value is promoted to True, it means X ads ranking begins formally predicting view-through conversion. - Actual use of the GB300 config —
_GB300_OVERRIDESwas inherited byhome_direct_packed_nano, but not yet explicitly by the flagship ranking config (the base is already set to Gen4+4's Muon, which is separate). Whether the GB300 override inheritance also applies to the flagship is the checkpoint to watch. - Whether the actual
xai-o2implementation is released — a stub appearing means the interface has stabilized, but the actual implementation is unlikely to enter the open-source build inferred. - Redefinition of the
EVERGREENretrieval dataset — the name does not match the substance, so we watch whether the next commit renames it (EVERGREEN → SHORT_TERM_VIDEO) or reverts it. - Since Grok 4.6 internal is now fully switched, when the next judge generation (4.7?) begins — the dial from 4.5 to 4.6 became more refined each day over three days (08-19–08-21) as 0.1→0.3→1.0. When the 4.7 shadow starts after 4.6 has fully settled is the signal of the next generation switch.