In one lineX released a retrieval model that generates item semantic IDs (SID) to pick candidates, together with a For You candidate source that calls that service. The source is off by default.
1Retrieval that generates SIDs to pick candidates is wired in as a For You candidate source — X
Sourcexai-org/x-algorithm@76843a5 — xai-org, 2026-10-02T02:06:55Z, code. These notes number X commits after the Gen4 release in order, and this commit is Gen4+34. The scale is +5227/−756 across 105 files.
Background X's recommendation stack has been reorganized around Phoenix (the GPU ranking stack rebuilt in 2026) over the past four months. Within it, "retrieval" was split into two branches.
One is two-tower retrieval: the traditional OON (out-of-network, posts from accounts the user does not follow) source that runs ANN search on the inner product of user-tower and item-tower embeddings. In the 2026-09-30 commit, xrecsys_two_tower.py was promoted to a 6-preset catalog, and SID 6-level, seqpack, and sharing of two heads (Home/Immersive) were stated explicitly.
The other is generative retrieval (SID generation). Items are compressed into multi-stage token sequences (semantic ID, SID for short) with RQ-VAE or a similar quantization, and a Transformer then generates SID tokens conditioned on the user history. Lines such as Google TIGER (NeurIPS 2023, arXiv 2305.05065), Alibaba PailitaoGR (2025), Kuaishou OneRec (2025), Tencent BARGE (2026), and ByteDance GEAR (arXiv 2026-09-30 v1, 2026-10-02 daily) belong here.
Until now X had exposed SID tokens through sid_num_levels in the training config. The two-tower model used them as input features of the item tower. A generative retrieval path that generates SIDs and picks candidates from them directly had never been released.
On 2026-09-04 (Gen4+15) there is a record that the "SID retrieval sidecar was removed entirely from the Phoenix serving engine". At the time, the sidecar was a lookup that converted each post ID in the user history of a Phoenix request into an SID, and that role was absorbed by loading SIDs into the request beforehand. Until then, X's "SID retrieval" had the narrow meaning of an item ID → SID token lookup.
This commit completely changes that narrow meaning. Among the new files, SID-related code comes in five groups, and the following three things arrived together:
Phoenix gained a training config, model, trainer, and inference runner for a Transformer dedicated to SID generative retrieval.
home-mixer (the service that assembles the For You feed) gained a SidSource candidate source that calls this service.
This source attaches shared_prefix_depth to response candidates as the score and passes them on labeled ForYouPhoenixRetrievalMoe.
What we learnedA. home-mixer now has a candidate source that calls a separate SID retrieval service
home-mixer has a new candidate source, and it calls a generative SID retrieval microservice split out as a separate service verified — home-mixer/sources/sid_source.rs (new, +112).
The target endpoint is https://xai-recsys-sid-retrieval.prod.fou.twitter.biz:443.
The request carries seed post IDs, a result cap, and a prefix-depth range, and the response carries a post ID and shared prefix depth for each candidate verified.
The gRPC service is SidRetrievalServiceClient.
The request type is RetrieveRequest { seed_post_ids, max_results, max_per_seed, min_prefix_depth, max_prefix_depth, caller }.
The response type is RetrieveResponse { candidates: Vec<Candidate> }, and each Candidate holds post_id and shared_prefix_depth.
These proto symbols come from outside the repo, and the repo also released a separate crate that holds only the SID lookup proto verified.
The symbols are imported by the xai_recsys_sid_retrieval_proto crate from outside (private or generated).
The separately released crate xai-recsys-sid-proto contains only sid_lookup.proto.
sid_lookup.proto covers only the post_id → SID code array mapping. It is the return of the lookup sidecar that disappeared on 2026-09-04.
The definition is SidLookupService { LookupSids(repeated int64 post_ids) → repeated PostSids { repeated int32 codes } }.
The generative retrieval proto is not included in the repo.
This means that the production code visible in the repo and the proto definition dedicated to that "generative retrieval" are separated inferred.
SidSource is enabled by the AND of five conditions, all of which must be true verified — sid_source.rs:58-64.
The EnableSidSource param is on (default false).
The request is not a topic request.
The request is not in-network only.
There are no cached posts (cold path).
has_post_signals(query) is true.
The last condition reuses exactly the same function as SimclustersSource, meaning the source runs only when the user has positive engagement signals.
The design is declared so that the two sources branch from the same seed set (post IDs the user interacted with through fav/retweet/reply and so on).
The source calls the SID service with posts the user reacted to as seeds, and gives the returned candidates the same label as the existing Phoenix MoE (mixture-of-experts) source verified.
First it takes the user's positive engagement post IDs from post_signal_ids(query) and truncates them to the SidSourceMaxSeeds cap (default 50).
It assembles a RetrieveRequest and calls via gRPC. The 300ms deadline is hardcoded in sid_retrieval_client.rs:21.
Each response Candidate is converted to a PostCandidate. It is marked served_type = ForYouPhoenixRetrievalMoe, which is the same label as the existing PhoenixMOESource.
shared_prefix_depth goes into the score field as an f32.
Finally, it fills in the author, creation time, and so on with CoreDataCandidateHydrator, and then removes candidates with author_id == 0.
Five parameters were introduced under the names rust_home_mixer_sid_source_*, and their defaults were released verified — home-mixer/params/param.rs. This file is a cron mirror of the production feature-switch defaults.
EnableSidSource=false
SidSourceMaxResults=800
SidSourceMaxSeeds=50
SidSourceMaxPerSeed=100
SidSourceMinPrefixDepth=3
max_prefix_depth: 0 is hardcoded in RetrieveRequest (no cap).
min_prefix_depth=3 is a request-side filter asking for only candidates whose shared prefix matches at least 3 of the 6 SID levels.
There are at most 100 posts per seed and a total cap of 800. The theoretical cap is 50 × 100 = 5000, but it is tightened again to 800.
The gRPC client runs two paths in parallel: it sets up the xDS path first and falls back to the DNS path if that fails verified.
It first sets up the load balancer with xDS P2C. This path uses the discovery-authority-based xdstp://discovery-*/envoy.config.endpoint.v3.ClusterLoadAssignment/fou-xai-recsys-sid-retrieval.prod.prod:grpc-tls.
If the xDS build does not finish within 10 seconds or there is no bootstrap, it falls back to the DNS path (RotatingChannel, refresh every 60 seconds).
The response size cap is MAX_DECODING_MESSAGE_BYTES = 16 MiB.
The metric SidRetrievalClient.retrieve{result=success|timeout|failure, path=xds|dns} records the call result and path as labels. Call latency goes into the SidRetrievalClient.retrieve{path} histogram.
The service's own readiness probe is GET /readyz on port 29180.
Keep-alive is set to a 30-second interval, a 10-second deadline, and idle=true.
The source-side metric measures three stages separately: the number of seeds, the number of RPC responses, and the number remaining after hydration verified.
The metric is SidSource.retrieved{type=seeds|retrieved|hydrated}.
The seeds count is the amount sent in the RPC, the retrieved count is the RPC response, and the hydrated count is the survivors with author_id!=0.
What we learnedB. Training and serving code for a dedicated Transformer that generates SIDs was released in Phoenix
Phoenix gained new files for the training config, model, trainer, and inference runner of the SID generative retrieval model verified — phoenix/xrex/configs/xrecsys_sid_retrieval.py (new).
attn_logit_cap=80.0 with attn_logit_cap_method="soft_sign", rotary, causal
widening_factor=2 with gated_mlp=False (plain MLP), remat WHOLE
The input vocab figures exactly match those of the two-tower config released on 2026-09-30 (Gen4+32). This means the two models share the same vocab space.
User vocab=0, item raw vocab=0, item hash vocab=100M, author vocab=30M, 60 action types, 8 continuous actions.
It pads to a multiple of 512 with _round_up(..., 512).
It applies double hashing with two pairs of hash functions (linear scale × modulus prime) for each of user/item/author.
Training uses the Adam optimizer and a constant learning rate, and splits with expert-parallel and no data-parallel.
Adam optimizer (b1=0.95, b2=0.98), weight_decay=1e-3, constant learning rate 2e-3
batch size per device = 128, parallel setting is ep=128, dp=1 (128-way expert-parallel, no data-parallel)
max_steps = (1e11 / 32) − 100 ≈ 3.125B steps
The dataset is in aggregated_kafka mode, and the path is a filler value.
PhoenixDataset with path="/path/to/offline_kafka_dump" (filler), num_kafka_partitions=1024
The model code appears to keep SID token embeddings in a separate table per level and raise them to 1024-dim through a projection per level verified — recsys_sid_retrieval_model.py:72-100.
The related names are the Haiku transformed function _embed_sid_token(code, level) and the per-level table sid_emb_level_{level} (size [256, 128]).
The name of the projection after each level is sid_emb_proj_{level}.
The model's only positive action is SERVER_TWEET_FAVverified — line 40. The positive is narrowed to fav alone.
Seven hard-negative actions are used for negative supervision.
The list is CLIENT_TWEET_REPORT, _NOT_INTERESTED_IN, _SEE_FEWER, _UNFOLLOW_AUTHOR, _BLOCK_AUTHOR, _MUTE_AUTHOR, _NOT_RELEVANT.
The evaluation settings of the serving-side beam decoder are eval_num_batches=20, eval_beam_width=128, and eval_bs_per_device=8verified.
The inference runner separates a stage that generates SIDs with beam search from a stage that turns the generated SID prefixes back into an actual set of post_ids verified — sid_retrieval_runner.py:21-80.
The runner file is sid_retrieval_runner.py, and SidPostIndex/SidBeamResolver handle the stage that turns prefixes back.
Among the runner's hyperparameters, beam_width and decode_levels are flags. If decode_levels < 6, generation stops at the prefix level and returns the prefix-sharing set (prefix retrieval).
This pairs with home-mixer SidSourceMinPrefixDepth=3. The server generates 3–6 levels, but every post matched at level 3 can be returned with prefix depth 3 as its score.
Fields such as _CorpusView.sentinel_idx, post_ids_with_sentinel, and author_ids_with_sentinel show that the corpus is loaded as a prebuilt index (SID → posts).
That is, at serving time the corpus is "an SID trie plus the set of post_ids held at the leaves", and when beam search picks the top-K paths on the trie, the posts under those paths become candidates inferred. In the code, the detailed structure of SidBeamResolver is in another file (xrex/inference/sid_post_index.py), which does not appear in this commit's diff.
This design overlaps with the published generative retrieval family: Google TIGER (NeurIPS 2023), Alibaba PailitaoGR (2025), Kuaishou OneRec (2025), Tencent BARGE (2026), and ByteDance GEAR (2026-09-30). The X version differs in the following points:
It uses a comparatively narrow 6-level × 256-codebook space (TIGER uses 3-level × 256 plus one collision-disambiguation token, and OneRec uses 3-level × 1024).
It feeds the 1023-token user action history in as the condition as is (TIGER limits the user history to at most 20 items in training).
It passes shared_prefix_depth to the upstream ranking as a score (TIGER uses the generation probability).
It shares its vocab design with the two-tower model. However, the embedding tables are currently separate for each model (see the implications below).
Two-tower runs ANN in vector space, while SID retrieval runs beam search in code space. In effect, the two are declared as a complementary structure, not a duplication.
What we learnedC. Phoenix gained a bundle of CUDA kernels that handle embedding tables sharded by row
A new bundle of CUDA kernels handles embedding tables sharded by row verified — phoenix/xrex/cuda/row_emb/.
It is a 12-file bundle with 3,180 new lines in total.
If Phoenix's existing async_emb kernel handles embedding tables sharded in the column direction, the new row_emb handles tables sharded in the row direction verified.
With N vocab entries, N / group_size rows are loaded onto each rank.
This design matters when the vocab grows to hundreds of millions (item 100M, author 30M) and the embedding table has to span tens to hundreds of GPUs inferred. Each token lookup only has to query the rank it belongs to, and the all-to-all occurs when sending tokens to the lookup rank inferred.
The Python binding row_emb.py registers 6 JAX FFI targets on the CUDA platform and passes the shard spec bundled in a single NamedTuple verified — row_emb.py:30-80, 100-290.
Lookup is split into two stages. A window opens between the two FFIs in which other operations can run, and all-to-all communication overlaps with computation inside that window. This is a pattern like Pathways/MegaScale.
lookup_start takes token_ids and the local shard table and returns table_out and lookup_pin (a float32 scalar). lookup_done takes the pin and returns embeddings.
tokens_per_rank is set as tokens_per_batch / group_size.
The receive buffer cap is reserved in advance as recv_capacity = min(ceil(tokens_per_rank × recv_factor), tokens_per_rank × group_size). If recv_factor is small, drops occur when there are many collisions, and if it is large, the buffer is wasted.
Rowwise Adagrad is fused into the same 2-phase structure as lookup.
rowwise_adagrad_update_start takes the table, row_state (the accumulator of each row), and gate, and returns the table being updated itself. It updates in place with input_output_aliases={0:0, 1:1}.
rowwise_adagrad_update_done returns a completion signal.
rowwise_adagrad_lazy_update_start is a variant that takes last_step and step and lazily updates only the rows that were accessed. This is a common pattern for sparse embedding tables.
A gate input guarantees the XLA execution order.
Every FFI function takes gate: jax.Array as input and mixes a float32 scalar into its output. This explicitly creates a side-effect chain that XLA cannot freely reorder.
attrs() passes communicator identifiers such as context_id and flatten_replicas as FFI attributes. The same context_id reuses the same communication group.
Validation checks the following conditions:
vocab_rows % group_size == 0 (even distribution of row shards)
tokens_per_batch % group_size == 0 (even distribution of token shards)
emb_width % 8 == 0 (a multiple of 8). This appears to be a bf16 tiling requirement inferred.
recv_factor > 0
The table_axis axis must be contained in the data_axis axis. If it is not, a ValueError is raised saying "there is not exactly one token shard per communicator rank".
Phoenix's async embedding utility was rewritten as an adapter layer that accepts both kernels verified. It now supports both column and row sharding strategies.
The file is phoenix/xrex/utils/recsys_async_emb.py (+112/−36), and the kernels it accepts are async_emb and row_emb.
It distinguishes row from column with an is_row_sharded(context_handle) branch. If row-sharded it returns table_spec as P(table_axis, None), and if column-sharded as P(None, table_axis).
This kernel bundle is an extension of the general Phoenix embedding table design, not dedicated to the SID retrieval model, but in the context of being released together in this commit, the appearance of a model that needs row-sharded embeddings (that is, a model with a very large vocab) appears to be the trigger inferred.
Two-tower can also run column-sharded, but for a generative model row-sharded is more natural given the lookup pattern, because each token references only a specific range of its own vocab level.
What we learnedD. LLM judge actions return to the Phoenix proto, and ads and retrieval request fields grow
The mirror file is phoenix/python/common/xai-proto/proto/recsys.proto, and both files are +68/−44 each.
Two LLM judge actions reappeared after 15 days with changed numbers verified.
The values this time are LLM_JUDGE_RELEVANT_TO_SEARCH=108 and LLM_JUDGE_NOT_RELEVANT_TO_SEARCH=109.
The two actions were introduced as 107/108 in Gen4+20 (2026-09-12).
They were vacated after 5 days in Gen4+23 (2026-09-17). CLIENT_TWEET_LIVE_STREAM_REPLY=107 took that slot, and 108 was preserved as a RESERVED_108 placeholder.
They reappeared today (Gen4+34, after 15 days). However, the numbers changed: the earlier 107/108 are now 108/109.
RESERVED_108 is reused as LLM_JUDGE_RELEVANT_TO_SEARCH, and the new number 109 is assigned to LLM_JUDGE_NOT_RELEVANT_TO_SEARCH.
In other words, the decision to bring LLM judge labels back in as actions has returned, but the numbers moved to new slots. Whether this becomes a stable state after readjustment or is vacated again has to be observed in the next commit window inferred.
Two new continuous actions were introduced verified.
The new ContinuousActionName values are WEB_CONV_VALUE_USD=6 and WEB_CONV_VALUE_RATIO=7.
Two were added to the existing six continuous actions (ACTIVE_SECS and others, plus BRIDGE_PROBABILITY=4 and ADS_WEB_CT_PURCHASE_VALUE=5).
The continuous head presumably consumes the ads conversion value split into two branches, an absolute USD value and a ratio (probably relative to a baseline) inferred. This commit has no code change on the continuous loss side, and only the proto has reserved the slots in advance.
Two viewer × advertiser fields were added to the ad information message AdInfoverified.
Field 24 is viewer_advertiser_engagement_counts and field 25 is viewer_advertiser_conversion_counts.
Both are the existing EngagementCounts message type, but semantically they are new slots holding the engagement/conversion history between the viewer and the advertiser.
The ads ranking model can now receive a feature like "how did this viewer react to this advertiser's earlier ads".
The engagement count message now also carries which kinds of actions it aggregated verified.
collected_actions (field 6, repeated int32) was added to EngagementCounts.
Downstream can now tell whether "these counts aggregate fav only, or include fav+reply as well".
The two-tower retrieval request now also accepts seed posts and an exclusion list verified.
RetrieveTopKCandidatesRequest expanded to three fields.
Field 14 seed_post_ids is a different message from the existing SidRetrieval RetrieveRequest. Two-tower retrieval can now also carry seed post IDs in the request.
Field 15 is excluded_posts_bloom_filters (repeated ExcludedPostsBloomFilter { bit_array: repeated fixed64, size_cap, false_positive_rate }).
Field 16 is excluded_post_ids (repeated uint64).
This means Phoenix two-tower retrieval now also accepts seed-conditioned search and bloom-filter exclusion of already seen posts. The design converges toward SidSource and two-tower retrieval sharing the same seeds.
A new enum UnifiedCardType (29 variants) enumerates the ads unified card formats at the proto level verified.
Values are things like IMAGE_WEBSITE, VIDEO_WEBSITE, IMAGE_APP, ..., VIDEO_PLAYABLE_WEBSITE, IMAGE_COLLECTION_WEBSITE.
No code-side consumer appears in this diff, only the proto declaration.
It appears to reserve a place in advance for the ads ranking model to receive the unified card format itself as a categorical feature inferred.
FetchAggregatedUserActionRequest changed position inside the proto (only a file-order rearrangement). There is no change in meaning.
What we learnedE. The visibility-judgment service (VF) records the outcome in its per-caller request metric
The target metric is vf_requests_by_caller{caller_identity, rpc, deadline}, introduced yesterday (Gen4+33), and outcome was attached to it as the fourth label.
The new label outcome holds how the request ended: success, failure, or cancellation.
A 3-variant enum Outcome { Success, Failure, Cancelled } was introduced.
A request that a handler finishes without marking success or failure is counted as Cancelled verified.
The record() function now returns a CallerRequest guard struct (annotated must_use).
If the handler is dropped as is without calling request.mark_success() or .mark_failure() while processing the request, it is counted as Cancelled.
The Drop impl emits the metric. This is an RAII pattern.
A new test pins the dashboard-generating Python script and the Rust-side constants together verified.
The test name is dashboard_generator_pins_the_requests_by_caller_metric.
The Rust test passes only if the script holds in its code the constants REQUESTS_BY_CALLER_METRIC = "vf_requests_by_caller", REQUESTS_BY_CALLER_NO_DEADLINE_FILTER = 'deadline="absent"', and four success/non-success filter constants.
The design checks the metric definition for drift in both the Rust code and the dashboard code at the same time. It is a pattern similar to how yesterday's _drift_checks metric watched for UI bundle drift.
What we learnedF. home-mixer removed two cold-start branches and moved author rule evaluation into a shared crate
The Control arm's cold-start boost now targets every post in the Control corpus regardless of the retrieval source verified (author_cold_start.rs).
The is_arm_gated_retrieval function (which identified Phoenix MoE/Cold retrieval sources) was deleted.
Previously, the Control arm excluded posts from Phoenix MoE/Cold retrieval from the cold-start boost even among posts in the Control corpus.
The test cold_retrieval_keeps_its_score_in_every_arm was deleted, and the new test control_viewer_lifts_cold_retrieval_from_control_corpus replaces it. The new test states explicitly that "a Control viewer lifts cold retrieval candidates too if they belong to the Control corpus".
The is_moe label of the metric home_mixer.cold_started_posts_total was also removed. Whether a post comes from Phoenix MoE/Cold retrieval is no longer tracked.
Until yesterday, the cold-start boost treated Phoenix retrieval on three axes, arm × corpus × source, and filtered on the source axis; now it narrows to two axes, arm × corpus.
Engagement counts are now re-fetched unconditionally for every candidate when there are cached posts verified (engagement_counts_hydrator.rs).
Removed the cold-start eligibility branch for the cached_posts case from EngagementCountsHydrator.
Previously, with has_cached_posts, only candidates that passed cold_start_base_eligible had their engagement counts re-fetched, and the rest kept their cached values through preserve_counts. Now every candidate is re-fetched unconditionally.
Deleted the preserve_counts helper.
The new test cached_posts_refetch_all_candidates pins that all three candidates are replaced with new counts regardless of cold-start eligibility.
Both the ColdStartFollowerCap and cold_start_base_eligible dependencies were removed from the call path.
The interdependence between engagement counts and cold start is gone. Cold-start logic downstream can assume the engagement counts are "fresh", and the hydrator side is simpler.
The author-rule evaluation utility that lived inside home-mixer moved up into a shared crate verified (author_rules.rs).
Deleted home-mixer/util/author_rules.rs (156 lines) and moved the symbol to xai_feature_switches::AuthorRulesEvaluator. It is a refactor.
The moka cache setup of size 500k, TTL 10 minutes, and 64 shards was likely duplicated on the shared crate side inferred. That implementation is not exposed in this diff.
What we learnedOther changes
The VF filter code gained only two tests, and there is no policy change.
The file is filter.rs (+94/−2).
The added tests are pure_core_alone_says_a_post_is_a_retweet and without_a_pure_core_answer_a_post_reads_as_an_original.
Both pin which fallback the viewer_mutes_retweets rule and the nullcasted_tweet rule each use to reach a conclusion when PureCore (tweet core metadata) hydration fails or succeeds. They add a lock on existing fault-handling behavior.
The changes to three VF files appear to be a small refactor following yesterday's Fetcher<V> rewrite inferred. They mostly add tests.
In VF visibility-filtering-client/vf_client.rs (+1/−35) and test_support.rs (+37/−0), the test fixtures were split into a separate file. There is no logic change.
Phoenix python/training/xai-checkpointing/xai_checkpointing/kms_client.py (+74/−0, new) is a shared wrapper for KMS access. It has no direct effect on feed ranking.
Phoenix trainer_recsys.py (+164/−39) appears to separate the parts that a dedicated SID retrieval Trainer will inherit from the shared trainer inferred.
Phoenix rowwise_adagrad.py (+14/−9) changes the fused kernel on the row_emb side and the python-level reference adapter.
abuse-enforcement-service/service-lib/src/overturn_hold.rs (+22/−22) is a test reinforcement of the overturn hold module released yesterday inferred.
botmaker/src/java/com/twitter/botmaker/compiler/thrift/TType.java (+14/−14) is thrift compiler maintenance.
thunder/posts/post_store.rs (+20/−20) is at the level of test renaming inferred.
phoenix-rankall/src/store/base.rs (+12/−12) is a minor adjustment to the SID backfill store adapter inferred.
visibility-filtering-client/tweet_safety_label.rs (+39/−12) expands the thrift decoder tests inferred.
The increment in home-mixer/params/param.rs (+31/−1) comes from adding the five SID source params above. Unless the cron sync time changed, there is no other meaningful change.
Canon update Created a new "### Gen4+34 (2026-10-02, 76843a5)" subsection in the public-source Gen4 section of services/x.md. In the retrieval source table of the ## Candidate generation (retrieval) section, added a SidSource row and wrote "SID generative retrieval microservice. seed post ID → 6-level × 256-codebook SID beam search → shared_prefix_depth score, labeled with served_type=ForYouPhoenixRetrievalMoe, off by default with EnableSidSource=false".
Why it matters A path that generates SIDs directly and returns them as candidates has been released in X code for the first time. However, the default is off, so the checkpoint to watch for real-traffic use is the day the FeatureSwitches cron turns it on. Three facts overlap in this commit.
This is the first commit in which X released generative SID retrieval as three complete parts: a "separate microservice + dedicated Transformer training and serving stack + home-mixer candidate source wiring".
Until now, X's SID has appeared indirectly in two places. One is an input feature of the two-tower item tower (the use_post_sid flag in xrecsys_two_tower.py, released 2026-09-30). The other is the 800k backfill QPS cap in phoenix-rankall (SID_BACKFILL_QPS_CAP = 800_000, released yesterday).
No line that generates SIDs directly and returns the result as candidates had been released.
Today's commit releases the whole chain below at once.
On the Phoenix side, it introduced four files for a dedicated Transformer model (8-layer, GQA 20:4, 1030-length context, 6-level × 256-codebook): config, model, trainer, and runner.
It released the gRPC client of the separate microservice xai-recsys-sid-retrieval.prod.fou.twitter.biz and its request shape (seed_post_ids, shared_prefix_depth scoring, prefix retrieval condition min_prefix_depth=3).
The home-mixer SidSource sends candidates labeled with the same ServedType as PhoenixMOESource.
Had these three parts been scattered across separate commits, it would have been a "work in progress" signal, but the fact that they were released together in one commit means the wiring is close to complete.
However, since EnableSidSource=false is the default (off), the checkpoint to watch for real-traffic use is the day it is turned ON in the FeatureSwitches cron.
Two-tower retrieval and SID generative retrieval are designed as "two layers over the same vocab space", not as "replacements for each other".
The two models share the same user, item, and author hash vocab (100M · 30M · identical double hashing scales/biases). Comparing xrecsys_sid_retrieval.py:30-45 with the figures in xrecsys_two_tower.py, released 2026-09-30, shows this.
Generative retrieval is costly in the two ways below, so X appears to have chosen a complementary structure in which two-tower first handles the broad recall that can run with ANN, and SID retrieval handles supplementary recall by prefix-sharing degree inferred.
One is the time complexity of SID generation (beam_width × decode_levels × codebook size).
The other is the cost of maintaining the SID index on the corpus side.
With the same vocab, both models could run on a shared embedding table. However, in the config released now, each model has its own embedding table. Whether they are unified into a shared embedding has to be observed in the next commit.
The row-sharded embedding CUDA kernel bundle is a two-axis extension of the Phoenix embedding design itself.
The 3,180-line row_emb was not added solely for the generative retrieval model. However, in the context of this commit bundle, it is natural to read the huge-vocab embeddings that SID generative retrieval requires (100M · 30M · 6-level × 256 codebook) as the trigger.
The shared adapter recsys_async_emb.py lets a model choose between the existing column-sharded kernel async_emb and the new row-sharded kernel row_emb, in effect leaving room to pick the best sharding per model.
Two-tower can keep the user tower and the item tower each column-sharded, but in the SID generative model each token refers only to its own vocab range during the generation stage, so row-shard likely yields a more compact communication pattern inferred.
The return of the LLM judge actions is a separate stream. The two actions that were vacated 15 days ago reappeared under changed numbers (107/108 → 108/109), which means the direction of carrying the LLM judge on the action axis and consuming it as one of the training targets came back after readjustment. The numbers changed because slot 107 was permanently taken by CLIENT_TWEET_LIVE_STREAM_REPLY in the meantime, and 108 was also vacated so that it could be reused. This release shows no consumer in the model training config or on the wire, and only the proto has claimed the slots first; which head weight they enter and which config's target action list includes them should be checked in the next window.
2No new recommendation-related posts — Netflix and Meta blogs
Neither the Netflix nor the Meta engineering blog had a new recommendation-related post to fold in today.
Of the 10 items in the Netflix tech blog feed, the latest 2 are unrelated to recommendation. They are "Trading a Cloud Identity for Your Own: Workload Attestation on Managed Compute" (2026-09-25, infrastructure) and "Leave the Class Path in the Rearview Mirror" (2026-09-18, Java packaging).
The earlier "MAPS", "GenRec", and "GenPage" have already been folded in.
The latest post on Meta engineering.fb.com, "From User Sequences to Scaling Laws" (2026-09-02), has already been folded in. The four posts after it (Private Processing / Rebalancer / Petal / ZGateway) are all about infrastructure, networking, or privacy, and are unrelated to recommendation.
Radar
0 papers passed all the radar acceptance criteria. The criteria are the following three:
① Platform authors or a production A/B test
② Mechanism novelty
③ Large-scale results
The scope is the arXiv cs.IR window 2026-09-30–2026-10-01; of 30 requested, all 30 <entry> items were parsed. The three items below were borderline, but all were rejected.
arXiv 2610.02057Optimizing Effective Training Time for Large-Scale Recommendation Systems. Rejected because it is an operational case of Meta's recommendation training infrastructure.
Confirmed as a Meta affiliation. "Meta Platforms, Inc.", "Meta Engineering Blog", and "Meta internal" appear many times in the PDF strings.
It is a fleet-scale operational case that raised the ETT% (Effective Training Time) of the recommendation training fleet from 80% → 90%+. It covers asynchronous checkpointing, communication removal, pipeline overlap, dynamic shapes, PyTorch 2 compile cache reuse, and more.
The reason for rejection is that it is an operational win in recommendation training infrastructure. Its nature differs from the retrieval and ranking mechanism novelty of radar criterion ②. "Training throughput" is also not an item that enters the service canon, so it was not folded in.
arXiv 2610.01533Neither Black nor White: Balancing Semantic and Collaborative Signals with Graph-Informed Semantic IDs (GrIS). Rejected because it makes no production deployment claim.
Confirmed that all authors are affiliated with Huawei Ireland Research Centre.
It covers a mechanism that mixes SID and collaborative signals through a graph, but makes no production deployment claim.
The reason for rejection is that the platform and deployment condition of ① is empty.
arXiv 2609.39043Routing Between Generative and Collaborative User Profiles: A Serving-Time Gate for Controllable Novelty. Rejected because it is not a tracked service.
The authors are affiliated with Comcast Technology AI and DePaul University.
The paper proposes a structure in which a "serving-time routing gate selectively routes users between an LLM-profile model and a collaborative sequential model". Within an NDCG loss budget of 5%, it gains Novelty@10 +6.5% and routes only 12.5% of users.
The mechanism is interesting (cost-aware selective LLM use).
The reason for rejection is that Comcast's Xfinity Stream is not a tracked service, and its value as a feed/ranking platform to track is low (video recommendation for streaming TV).
What to check next
Check when the SID source switch is turned on in the cron — the point in time when the FeatureSwitches cron flips this switch to true is when it reaches real traffic. The switch is the EnableSidSource param. Also check then whether the values of SidSourceMaxSeeds, SidSourceMaxResults, and SidSourceMinPrefixDepth change from the config defaults. The checkpoint to watch on the home-mixer side is the SidSource.retrieved{type=seeds|retrieved|hydrated} metric.
Check where the LLM judge actions (108/109) are used in training — only the proto is declared, and no code that consumes them on the training config or target head side is visible yet. If 108/109 are added to the target action list of xrecsys_search.py or xrecsys_two_tower.py in a next commit, that is the point at which they move from a wire declaration to training consumption.
Check whether two-tower and SID retrieval come to share embedding tables — the two models use the same vocab design, but each currently has its own embedding table. If a next commit introduces checkpoint sharing or embedding table reuse, it signals that the two retrievals are unified as two heads over a shared embedding.
Do a deep dive on generative retrieval — all three items below are subjects for full analysis and need a deep dive.
The model's beam decoder math: the prefix retrieval stopping condition, score aggregation, and the corpus view structure for reverse-resolving SID → post
Training loss: negative sampling with the positive narrowed to FAV only, and the weights of the 7 hard negative actions
Training throughput: a throughput estimate for the combination ep=128 · dp=1 · bs=128 · max_steps=3.1B
Check which of row_emb and async_emb each model uses — which model uses row-sharded and which uses column-sharded, and where the is_row_sharded(context_handle) branch is actually called, are the checkpoints to watch in the next window. Both are currently supported at the adapter layer, but which model "actually" uses row-sharded only becomes visible by following the make_context_handle calls in the model config.