- Sources read: 2
- Canon files updated: 1
- Radar entries: 0
The 9b0dc31 commit that introduced this path also made large changes to the grox PTOS (policy-based violation category verdict) judge stack and to the serving and training stack of Phoenix, the For You ranking model. Multi-step reply spam is today's headline, but the matching canon (the services/ notes holding the current understanding of each service) received five more updates, so each got its own section. Other changes reviewed today are as follows:
- The first concrete case of per-category models in the PTOS judge stack was confirmed. Only the judge for the illegal and regulated behavior category moved to Grok 4.1-fast.
- The SID (discrete code representing a post) lookup sidecar was removed entirely from the Phoenix serving engine. History SIDs now arrive in the request.
- A new rematerialization policy for H100 appeared in the Phoenix training code. It is the first code signal that training also runs on H100.
1A fourth grox path for thread-chain reply spam verdicts is wired in — X
Source xai-org/x-algorithm@9b0dc31 — public source code, 2026-09-04 00:37 UTC, X.AI Corp. These notes number X commits after the Gen4 release in order, and this commit is Gen4+15.
Background grox is X's nearline LLM annotation pipeline. On the reply-quality axis, grox had three verdict flows so far verified — PLANS_TO_INJECT in generators.py. PlanReplyRanking scores a single reply, PlanSpamComment judges whether a single reply is spam, and PlanCoordinatedSpam catches repeated replies coordinated by multiple accounts. All three flows have a long unification history in Gen4.
- In Gen4+3 (2026-08-18, 11a71f8), the separate spam, comment, and reply-ranking generators were merged into a single
ReplyRankingTaskGenerator. - The follower gating threshold for the reply spam and reply ranking tasks rose six times from 2026-08-13 to 08-25: 15k → 30k → 40k → 60k → 80k → 100k → 120k verified —
task_filter.py.- The commits that raised it were, in order, c65aa17, b089ce6, 11a71f8, 28e414f, d011592, and 0d3cdd8, and the value as of this commit is 120k.
- The comparison is against the follower count of the root and reply-target accounts, not the reply author.
- Reliability mechanisms such as Kafka-first storage order, Manhattan monotone-only write, and an option to put follower count and bio in the LM prompt were added in Gen4+9 (45b48ba) and +10 (24c6094).
This commit attached a fourth verdict path, PlanMultiStepReplySpam, to these flows. The "Multi-step" in the name means that the verdict unit is not an individual reply but several reply chains.
What we learnedA. Only chains where one author replies again to their own reply enter the verdict
The entry filter passes only replies that satisfy all 8 conditions below verified —
task_multi_step_reply_spam.py. The filter class isTaskMultiStepReplySpamFilter.post.ancestorsexists and none of them is deleted (None). That is, it is a reply and the thread is intact.- The users of the target post, the direct parent, and the thread root are all alive.
- The reply author differs from the thread root author. Replies that the original poster adds to their own thread are skipped.
GROK_USER_IDandGORK_USER_IDare skipped. Replies posted by Grok and Gork themselves are not targets.- Thread depth is 2 or more. A one-step "root → first reply" does not enter.
- The direct parent's author is the same as the target post's author. Only chains where the same person replies again to their own reply enter.
- The root author has at least 1,000 followers ("blast radius"). Only threads with meaningful root impressions are admitted.
- If the reply author is a high page-rank v2 user or a grey badge user, the reply is skipped. Established accounts are not targets. Both checks run in parallel by calling
UserStratoLoader.is_high_page_rank_v2_userandis_grey_badge_userthroughasyncio.gather.
Of the 8 conditions, number 6 in particular defines the domain of this pipeline. That domain is reply spam chains that steal root impressions through self-conversation chains. For example, A replies to a famous person's tweet, A replies to that reply, and A replies again, extending the chain. The target is the pattern that pulls the root tweet's impressions toward the author's own ads or lures.
The threshold of 1,000 in condition 7 is two orders of magnitude lower than the 120k of the single-reply verdict. This pipeline runs broadly over threads whose root impressions are substantial.
class TaskMultiStepReplySpamFilter(TaskFilterWithPost): FOLLOWER_COUNT_THRESHOLD_FOR_SPAM_DETECTION = 1000 ... if post.ancestors[-1].user.id != post.user.id: Metrics.counter("task.filter.skipped.count").add( 1, attributes={"filter": cls.FILTER_NAME, "reason": "parent_not_same_author"}, ) return False root_user_follower_count = post.ancestors[0].user.follower_count or 0 if root_user_follower_count < cls.FOLLOWER_COUNT_THRESHOLD_FOR_SPAM_DETECTION: Metrics.counter("task.filter.skipped.count").add( 1, attributes={"filter": cls.FILTER_NAME, "reason": "low_blast_radius"}, ) return False—
task_multi_step_reply_spam.py, 2026-09-04
What we learnedB. Gemma 2 reads the whole thread and picks the post numbers that are spam
The structure of the human message sent to the LM is visible directly in the code verified —
classifier_multi_step_reply_spam.py.- The function that builds the message is
MultiStepReplySpamScorer._to_convo. - The system prompt was left out of the export under the README policy (gaming prevention).
- The "Reply Author Info" section renders the reply author's user profile (handle, follower count, name, bio).
- The "Thread" section lists all posts in the thread with index numbers in the
#### Post Nformat. The root gets the tag "(root post — never flag)" and the last post gets the tag "(the reply being evaluated)". - If the thread exceeds
MAX_THREAD_SIZE = 10, only the first 5 and last 5 posts are kept and the middle is cut out. include_follower_count=Trueandmax_media=2are applied to each post, andinclude_bio=Trueis applied only to the root.
That is, this pipeline does not ask the LM whether each post is spam. It gives the LM a task of looking at the whole thread and extracting a set of spam indexes. The response is the JSON
{"spam_post_indexes": [1, 2, 3], "reason": "..."}.content.append( "\n\n# Thread\n\nThe reply thread is listed below; each post is labeled with a `#### Post N` index. " "Post 0 is the original (root) post and the highest-numbered post is the reply being evaluated. " "Refer to posts only by their integer index.\n\n" ) for i, p in enumerate(posts): if i == 0: tag = " (root post — never flag)" elif i == last: tag = " (the reply being evaluated)" else: tag = "" content.append(f"\n\n#### Post {i}{tag}\n\n") content.extend( PostRenderer.render( p, max_media=MAX_MEDIA_PER_POST, include_follower_count=True, include_bio=i == 0, ) )—
classifier_multi_step_reply_spam.py, 2026-09-04- The function that builds the message is
The verdict LM is Gemma 2 verified.
- The code has
model_name = GEMMA_2and calls it throughOaiSampler. - Since Gen4+9 (45b48ba) added Spam to grox's
USE_GEMMA_CATEGORIES, grox has been pushing Gemma, and this choice extends that trend. - That Multi-step reply spam is Gemma-only from the start fits the direction of saving grok API budget inferred.
- The code has
What we learnedC. The LM's answer is filtered in three stages, then stored with a spam label and a score of 0
- LM output is not trusted as is; it is filtered by a 3-stage safeguard verified.
_drop_if_newest_reply_not_flaggeddiscards the entire result if the index set the LM flagged does not include the verdict target (the last post of the thread). The task entry condition is that this reaction may be spam for the pipeline to run, so a result that treats the last reaction as not spam violates consistency._drop_other_author_postsremoves a post if the LM flags another author's post in the thread as spam. This pipeline targets chains where "the same author replies to their own reply", so posts by other authors that appear in the result are ignored._drop_singletondiscards the result if it contains exactly 1 post. Chain spam by definition targets "several follow-up replies", so a result with only one is not valid.
- The storage task attaches a spam label to each post flagged in the filtered result and forces the reply ranking score to 0.0 when saving verified.
- The task name is
TaskWriteMultiStepReplySpamReplyRanking. _apply_reply_spam_label(post_id, author_id)is applied to each flagged post. It is the same helper as in the single-reply verdict.save_reply_ranking_score(..., ReplyRankingScore(score=0.0, reasoning=reasoning[-500:]))forces the score to 0.0, which means the lowest trust. Only the last 500 characters of reasoning are kept.- It emits to v2 Kafka with
save_reply_ranking_kafka_v2(...). DISABLE_RULES = [DisableTaskForNonProd]means no real label is attached in dev and staging.
- The task name is
What we learnedD. The new pipeline consists of 4 new files and 2 modified existing files, and runs alongside the existing verdicts from the same event source
4 new files (474 lines) were created and 2 existing files were modified verified —
grox/flows/reply_spam/.- The pipeline name is
PlanMultiStepReplySpam. task_multi_step_reply_spam.py(206 lines)plan_multi_step_reply_spam.py(29 lines)classifier_multi_step_reply_spam.py(226 lines)state_multi_step_reply_spam.py(13 lines)prompts.py+5 lines: addedmulti_step_reply_spam_system_prompt.generators.py+7 lines: put this plan key intoPLANS_TO_INJECTof the two stream generators.
- The pipeline name is
The Plan DAG runs in the order filter, media hydration, verdict, storage verified —
plan_multi_step_reply_spam.py.task_multi_step_reply_spam_filter → task_media_hydration → task_multi_step_reply_spam_detection → task_write_multi_step_reply_spam_reply_rankingIt stands out that
TaskMediaHydrationruns only after the filter. Media hydration is costly, so it runs only after the filter has cut out a substantial share.Two reply ranking stream generators add the new Plan to their injection list verified —
generators.py.- The two generators are
ReplyRankingTaskGenerator(kafkaTOPIC_UNIFIED_POSTS_V3) andReplyRankingRecoveryTaskGenerator, and the injection list isPLANS_TO_INJECT. - That is, the three existing verdicts (single-reply reply_spam, coordinated_spam, spam_comment) and this multi-step verdict run in parallel from the same v3 event source.
- The two generators are
Canon update Created a new section "Resolved by the Gen4+15 release / new (2026-09-04)" in services/x.md. Under "Grox (LLM annotation)" it added a new subsection ### Gen4+15 (2026-09-04, 9b0dc31) — grox thread-level multi-step reply spam classifier introduced + PTOS .... It raised Last updated in the header from 2026-09-04 to 2026-09-05 and added links to the 6 new files in the sources section as well.
Why it matters A third axis was added to grox's reply spam verdicts: time-axis coordinated spam in which one author steals root impressions through their own reply chain.
- The previous canon knew two flows. The reply spam verdict targets individual replies, and coordinated_spam targets "repeated replies coordinated by multiple accounts". If coordinated_spam is the account-collusion axis, multi-step is the time-axis chain collusion.
- Because the gating threshold is set low at 1k root followers, this pipeline appears to run broadly over threads where the root author's impression assets are substantial. And leaving the verdict to Gemma fits the direction of saving grok API budget.
- It does not conflict with the canon's existing understanding of grox reply_spam. Rather, it can be understood as widening into a two-layer structure: the 120k follower threshold of the single-reply verdict that the canon has covered is kept, and a chain verdict with a much lower threshold (≥ 1k) is added as a new layer.
2The first concrete case of PTOS judge models diverging by category is confirmed — X
Source xai-org/x-algorithm@9b0dc31 — grox/flows/ptos/classifier.py and grox/flows/ptos/task_safety_ptos_policy.py in the same commit (Gen4+15).
Background In grox's PTOS (layer that judges violation categories based on policy) stack, a judge (cross-validator) layer was gradually added above the first-pass classifier during Gen4.
- Gen4+2 (2026-08-17, b089ce6) introduced an adult content disagreement judge. Its name is
SafetyPtosAdultContentCrossValidationJudge, and its model wasEAPI_GROK_4_5_X_ALGO(classifier.py:557-561). - In Gen4+4–+6 (2026-08-19–21), the rate (dial) at which child safety cross-validation and deluxe reclassification call Grok 4.6 internal rose 10% → 30% → 100%. It replaced 4.5 internal within two days, and 4.5 internal was deleted from the code entirely (
classifier.py:114, 281, 477). - Gen4+7 (2026-08-24, d011592) introduced
SafetyPtosPolicyCrossValidator, which shares theEAPI_GROK_4_5_X_ALGOused by the adult content judge. This judge handled all 3 categories, ChildSafety, ViolentMedia, and IllegalAndRegulatedBehaviors, with a single model (4.5-X_ALGO). - The canon summarized this state as "a dual stack in which X keeps the primary (4.6-internal) and the judge (4.5-X_ALGO) on different model generations and tuning". It also noted that per-category model divergence in the judge stack would be the next checkpoint to watch.
This commit confirmed that divergence as the first concrete case.
What we learnedA. Only the IllegalAndRegulatedBehaviors judge moved from Grok 4.5 to 4.1-fast
Grok 4.1-fast-X_ALGO was newly registered as a model for the judge verified —
grox/flows/ptos/classifier.py.- The model name is
EAPI_GROK_4_1_FAST_X_ALGO. SafetyPtosPolicyCrossValidator.__init__initializes a 4.1-fast-X_ALGO sampler next to the 4.5-X_ALGO sampler.
eapi_4_1 = grox_config.get_eapi_model(ModelName.EAPI_GROK_4_1_FAST_X_ALGO) self.eapi_4_1_x_algo = EapiSampler(EapiModelConfig(**eapi_4_1.model_dump()))—
grox/flows/ptos/classifier.py, 2026-09-04- The model name is
Only the judge for the IllegalAndRegulatedBehaviors category moved to 4.1-fast verified.
- The changed function is
_validate_illegal_and_regulated_behaviors.
- metric = "safety_ptos.illegal_and_regulated_behaviors_cross_model_validate_with_grok_4_5" + metric = "safety_ptos.illegal_and_regulated_behaviors_cross_model_validate_with_grok_4_1" ... - async with _eapi_4_5_x_algo_breaker.guard(): - raw = await self.eapi_4_5_x_algo.sample( - convo.interleaveToEapi(), conversation_id=convo.conversation_id - ) + raw = await self.eapi_4_1_x_algo.sample( + convo.interleaveToEapi(), conversation_id=convo.conversation_id + )—
grox/flows/ptos/classifier.py, 2026-09-04- The circuit-breaker guard is also gone. 4.5-X_ALGO was called wrapped in
_eapi_4_5_x_algo_breaker, but 4.1-fast is called directly without a breaker. - ChildSafety and ViolentMedia still call 4.5-X_ALGO with the breaker. That is, judge models are now assigned differently per category.
- The changed function is
What we learnedB. First-pass for the same category now goes to Gemma without a probability gate
IllegalAndRegulatedBehaviors was added to the list of categories whose first-pass is judged by Gemma verified.
- The list is
USE_GEMMA_CATEGORIES.
USE_GEMMA_CATEGORIES = { SafetyPolicyCategory.Spam, + SafetyPolicyCategory.IllegalAndRegulatedBehaviors, }- The list is
All gates that decided by probability whether first-pass routing sends a request to Gemma were removed verified.
- if ( - sample_for_gemma - and not self.deluxe - and self.use_gemma - and random.random() < self.use_oai_gemma4_dial - ): + if sample_for_gemma: try: return await self.oai_gemma4.sample( convo.to_openai_messages(), conversation_id=convo.conversation_id )—
grox/flows/ptos/classifier.py, 2026-09-04- That is, for the two categories Spam and IllegalAndRegulatedBehaviors, first-pass is routed 100% to Gemma, and grok handles only the fallback.
- The constant initialization
self.use_oai_gemma4_dial = 1.0was also deleted.
What we learnedC. IllegalAndRegulatedBehaviors verdicts now pass through three layers: Gemma, reclassification, and the 4.1-fast judge
IllegalAndRegulatedBehaviors was added to the categories that get reclassification verified —
task_safety_ptos_policy.py.- The changed place is the reclassification trigger in
task_safety_ptos_policy.py.
- elif violation.category == SafetyPolicyCategory.ViolentMedia: + elif violation.category in ( + SafetyPolicyCategory.ViolentMedia, + SafetyPolicyCategory.IllegalAndRegulatedBehaviors, + ): policy = await active_classifier.classify_policy_for_violation( post, violation )- Previously only ViolentMedia took this reclassification path (re-checking the category verdict with deluxe 4.3).
- That is, with this commit the IllegalAndRegulatedBehaviors flow became a three-layer stack: first-pass Gemma → deluxe cross-model reclassification (expensive) → Grok 4.1-fast judge.
- The changed place is the reclassification trigger in
Canon update Added these 3 items to the "Resolved by the Gen4+15 release / new" section of services/x.md. The fact that per-category judge model divergence was confirmed as a first concrete case updates the "next category for judge expansion" checkpoint to watch under the Gen4+7 canon cleanup.
Why it matters It is now confirmed that judge models are tuned separately per category inside the PTOS judge layer. The previous canon knew only that "X keeps the primary and the judge on different models".
- The judge model choice appears to be set by three factors: the verdict difficulty of each category, the judge latency requirement, and the disagreement characteristics between primary and judge inferred.
- Given that the
fastsuffix is on the 4.1 judge and that it is called without a breaker, the judgment is that IllegalAndRegulatedBehaviors verdicts are relatively clear (for example drugs, weapons, and child exposure, where the label itself carries a strong signal), so a fast, cheap judge suffices inferred. - In contrast, ChildSafety and ViolentMedia verdicts are subtle (for example, telling cartoons and games from real footage in violent media), so the policy is that a 4.5 judge is needed.
- The promotion of first-pass Gemma to 100% for Spam and IllegalAndRegulatedBehaviors fits the direction of saving grok API budget. Violent and hateful speech appear to remain as the next promotion candidates.
3The SID lookup sidecar was removed entirely from the Phoenix serving engine — X
Source xai-org/x-algorithm@9b0dc31 — the phoenix/crates/serving/xai-recsys-engine/ subtree and phoenix/xrex/inference/ in the same commit (Gen4+15).
Background Semantic ID (SID) is a technique that represents post embeddings as discrete codewords with a hierarchical codebook of the RQ-VAE family.
- At X, Gen4+3 (2026-08-18, 11a71f8) wired 7 SID fields into VMRanker requests, which first confirmed that a 3-level hierarchical codebook exists.
- VMRanker is the stage that reorders, by diversity criteria, the candidate bundle to put in the feed.
- The fields are
sid_known,sid_k_l1/l2/l3, andsid_gap_l1/l2/l3.
- Gen4+13 (2026-09-01, 7ba7768) released code that adds dense reconstruction to the ranker post tower, only wired and switched off (wired-but-off).
- This code loads an RQ-VAE-style stacked residual codebook from an offline
.safetensors. - The file is
phoenix/xrex/models/recsys_sid.py(72 lines, new), and the default issid_embedding_mode="learned".
- This code loads an RQ-VAE-style stacked residual codebook from an offline
- And the Phoenix serving engine had a sidecar gRPC client,
sid_client.rs. It was an optional path that filled in codewords by querying the SID service separately for each post_id in the user history. In the Gen4+3 canon cleanup this sidecar was recorded only as "optional".
This commit removed this sidecar entirely.
What we learnedA. The SID client and its wiring code were deleted from Rust, Python, and the CLI
Both the Rust implementation and the Python binding of the SID lookup client were deleted verified — xai-org/x-algorithm@9b0dc31.
- The deleted files are
sid_client.rs(189 lines) andsid_retrieval_runner.py(17 lines). - What disappeared is the
SemanticIdClientRust client and the Python bindingPySemanticIdClient. - The
xai-recsys-sid-protodependency was removed fromCargo.tomlandpyproject.toml, and thepub mod sid_client;declaration insrc/lib.rswas deleted too.
- The deleted files are
The SID client field was dropped from the Python structs and the server macro verified —
phoenix/crates/serving/xai-recsys-engine/src/python.rs.- The field
sid_client: Option<Arc<crate::sid_client::SemanticIdClient>>was removed from the two structsRecsysPredictorImplandRecsysRetrievalPredictorImpl. - The
sid_client: Option<&crate::sid_client::PySemanticIdClient>argument that theserver_implmacro accepted and theadd_class::<PySemanticIdClient>()registration were deleted as well.
- The field
The SID endpoint setting (
--sid_endpoint) disappeared from the inference CLI and the model runner verified —phoenix/xrex/inference/launch_inference.py.- In the CLI, the
--sid_endpointargparse argument and the assignmentrunner.sid_endpoint = args.sid_endpointwere removed. - In
phoenix/xrex/inference/model_runner.py, the fieldsid_endpoint: str | None = Nonewas removed, and 12 lines of SID client assembly code increate_serverdisappeared. - The two deleted branches and the newly added
sid_num_levelsline reveal the new contract.
- if _use_post_sid and self.sid_endpoint and sid_num_levels > 0: - sid_client = xai_recsys_engine.PySemanticIdClient( - self.sid_endpoint, - sid_num_levels, - ) - logger.info( - "SID client connected: endpoint=%s, sid_num_levels=%d", - self.sid_endpoint, - sid_num_levels, - ) - elif _use_post_sid: - logger.info( - "Parsing history SIDs from the request (sid_num_levels=%d); no sid_endpoint", - sid_num_levels, - ) + user_tower = self.model_config.user_tower_config + sid_num_levels = user_tower.sid_num_levels if user_tower.use_post_sid else 0—
phoenix/xrex/inference/model_runner.py, 2026-09-04That is, of the two execution modes (SID client attached, or parsed from the request), the former is gone and only the latter remains. The new contract is that history SID arrives in the request.
- In the CLI, the
What we learnedOther changes
- A flag that uses CUDA pinned host memory for D2H transfer is now on by default verified.
- The flag is
use_pinned_d2h, and its default changed from False → True. It changed incidentally in the file covered by the last bullet of card A. - This option was already there at the Gen4 release (2026-08-13), off by default, and it became the default after 22 days verified —
model_runner.py:492. - The help text says: "Use CUDA pinned host memory for D2H transfer (~50 GB/s vs JAX's ~3 GB/s). Saves ~29ms/inference. Default True; pass False to opt out."
- The flag is
Canon update Added this fact to "Resolved by the Gen4+15 release / new" in services/x.md. The Gen4+3 record "Phoenix serving SID sidecar is optional" is updated to "the option is removed entirely; SID is a request contract".
Why it matters The architecture of the Phoenix retrieval SID pipeline was tidied up considerably. The responsibility for SID codeword lookup moved out of online serving.
- The previous canon knew three things:
- phoenix-rankall manages the SID index window (
video_14dayand others). - Seven SID fields are wired into VMRanker.
- The serving engine has an optional SID sidecar.
- phoenix-rankall manages the SID index window (
- With this commit, it is confirmed that the SID sidecar is gone from the serving engine entirely and that SID is carried directly in the request.
- The home-mixer (or candidate hydration layer) that handles For You feed requests extracts SID in advance at the candidate hydration stage and puts it in the request, and phoenix-rankall consumes it as is. One extra gRPC call per request is gone, and the fault domain of the SID infrastructure narrows.
- However, this diff does not yet confirm the specification of the SID hydrator on the home-mixer side. The name and source of the hydrator that fills in SID and the storage backend (Manhattan? Strato? phoenix-rankall itself?) are follow-up checkpoints to watch.
4First code signal that Phoenix training also runs on H100, besides GB300 — X
Source xai-org/x-algorithm@9b0dc31 — phoenix/xrex/models/remat.py (+17 lines) and phoenix/xrex/cutedsl/ranker_attention_fa4.py (+139/−75) in the same commit (Gen4+15).
Background The canon on Phoenix training hardware had already confirmed that "Phoenix production training runs on GB300". That confirmation came with Gen4+6 (2026-08-21, 28e414f), where _GB300_OVERRIDES appeared.
- The rematerialization policy (a technique that saves memory by recomputing some activations during backpropagation instead of storing them) was only
SAVE_GB300_RECSYS = 29. - The GB300 override assigned a preset tuned to GB300 hardware. The values are
bs_per_device=512,attn_impl="cutedsl_ranker_varlen_attn", andremat_policy=RematType.SAVE_GB300_RECSYS. - The FA4 (flash attention v4 based on NVIDIA CUTLASS/CUTe DSL) kernel assumed that the history tile is always padded to a multiple of the tile size (128). Under this assumption, the layout was built in advance on the python host and stored in a cache (
_DENSE_BS_LAYOUT_CACHE).
This commit registers a new remat policy for H100 and changes the FA4 kernel to handle variable-length history directly.
What we learnedA. An H100 remat policy now exists, but this commit has no H100-specific config bundle yet
A new remat policy for H100 was registered verified —
phoenix/xrex/models/remat.py.- The new value is
SAVE_H100_RECSYS = 32.
class RematType(enum.IntEnum): WHOLE = 0 SAVE_GB300_RECSYS = 29 + SAVE_H100_RECSYS = 32- A new branch was added to
custom_remat_policy(policy). This branch saves the same name list as GB300 as rematerialization checkpoint targets.- Name list:
attn_outputs,dense_outputs,dense_outputs_individual,attn,gate_up_proj,dense_up_proj,query_heads_rope,key_heads_rope,cutedsl_attn_outputs,query_heads,key_heads,value_heads,scalar_stats
- Name list:
- The two policies share the name list but have separate enum values; this structure leaves room for different tuning (for example, a saved-tensor priority preset) to be attached to GB300 and H100 later.
- A hardware-specific config block such as
_H100_OVERRIDESis not in this commit yet.
- The new value is
What we learnedB. The FA4 attention kernel takes a different effective history length per item and masks accordingly
The FA4 attention kernel was changed to support variable-length history directly verified —
phoenix/xrex/cutedsl/ranker_attention_fa4.py.- The
build_dense_block_sparse_layoutsignature changed as follows.
-def build_dense_block_sparse_layout(batch_size, seq_len, hist_len, num_q_heads): +def build_dense_block_sparse_layout(seq_len, hist_len, num_q_heads, hist_valid_len):- The new argument
hist_valid_lenreceives the actual number of valid history tokens for each item in the batch. - The computation moved from host-side precomputation based on
import numpy as npto JIT-friendly code based onjnp.where,jnp.arange, andjnp.broadcast_to. The_DENSE_BS_LAYOUT_CACHEcache was removed entirely. - The reason for removing the cache is clear. Because
hist_valid_lendiffers per batch, the layout became a function of batch data rather than a deterministic function, and caching became meaningless. - The new fields
valid_block_upperandvalid_block_lowerpass the upper and lower bounds of the attention mask per batch. With these values, the kernel masks partial-history blocks (where the actual valid tokens in a history tile number at most the tile size) exactly. is_partial = is_hist & (t == n_full) & (has_partial == 1)marks the position of the partial block.- The
ranker_attention_fa4signature was widened to accept the optional argumentsvalid_block_upper=Noneandvalid_block_lower=None. custom_vjppacks and unpacks the new arguments throughbs_argsso that they are included in the gradient computation.input_output_aliasesshifted from{12: 0, 13: 1, 14: 2}to{14: 0, 15: 1, 16: 2}.
- The
Canon update Added these two facts to "Resolved by the Gen4+15 release / new" in services/x.md. Below the canon entry that confirmed at Gen4+6 that "Phoenix production training runs on GB300", we append "first signal that it also runs on H100".
Why it matters The code now confirms that X's training hardware is not only GB300.
- GB300 may be used for flagship training, and H100 for secondary, backfill, and research work inferred.
- If
_H100_OVERRIDESappears in a later commit, the differences in batch, attention, and remat tuning between H100 and GB300 will become visible. - FA4 attention's support for variable-length history can be understood as extending the
cutedsl_ranker_varlen_attnvarlen semantics of Gen4+6 to the history axis. - Training and inference accuracy improve for new users and short sessions. Previously, short history was padded up to the tile size, which may have increased the attend targets or polluted the attention semantics.
- This observation attaches to the attn kernel axis of the
sequence-transformer-rankingcanon note.
5visibility-filtering code shrank by more than 800 lines net (overview only) — X
Source xai-org/x-algorithm@9b0dc31 — several files in the visibility-filtering/ subtree of the same commit (Gen4+15).
Background visibility-filtering (VF) is X's fail-close filter stack. During Gen4, this subtree has changed in two main directions.
- Rule refactoring moves policy into a data-oriented representation. The
RuleContextsplit in Gen4+11 and the removal ofBox<dyn Rule>in +14 belong here. - Safety-net removal strips out safety code from the early rewrite after it has been proven unnecessary. The deletion of 128 lines of VMRanker in Gen4+8 and 292 lines of the media fallback cache in +9 belong here.
This commit continues the safety-net removal direction at large scale.
What we learnedA. Hydration and safety-label code shrank substantially, while the regression safety net got thicker
Per-file changes in the VF subtree are as follows verified — file delta of xai-org/x-algorithm@9b0dc31.
| File | +/− |
|---|---|
visibility-filtering/config.rs |
+7/−118 |
visibility-filtering/rules/registry.rs |
+8/−378 |
visibility-filtering/rules/tweet_rules.rs |
+50/−129 |
visibility-filtering/hydration/mod.rs |
+11/−90 |
visibility-filtering/hydration/fallback_cache.rs |
+25/−87 |
visibility-filtering/hydration/batch.rs |
0/−19 |
visibility-filtering/hydration/gizmoduck_hydrator.rs |
+36/−42 |
visibility-filtering/hydration/tes_hydrator.rs |
+3/−33 |
visibility-filtering/get_safety_labels.rs |
0/−40 |
visibility-filtering/safety_label_source/codec.rs |
+15/−206 |
visibility-filtering/safety_label_source/manhattan.rs |
0/−10 |
visibility-filtering/models/safety_labels.rs |
+13/−19 |
visibility-filtering/filter_tweets.rs |
+17/−65 |
visibility-filtering/rules/author_rules.rs |
+3/−27 |
The regression safety net got thicker verified — same commit. Lines increased in visibility-filtering/rules/fixtures.rs (+81) and golden_corpus.rs (+102). The balance between simplification and regression defense was maintained.
Canon update Added only an overview to "Resolved by the Gen4+15 release / new" in services/x.md. The semantics of each deletion point remain as a deep-dive item: which wire format and label source were removed, and which hydrator entries were deleted.
Why it matters The pattern of stripping out safety nets once the rewrite has stabilized is now firmly established. Across five commits, Gen4+8, +9, +11, +14, and +15, visibility-filtering has been getting thinner through controlled net deletions.
- The previous version of the canon held that "Gen4 VF has many safety nets because the Rust rewrite is early". This observation updates that judgment to "a stage where the rewrite has matured and safety nets are being removed".
- The 206-line deletion in
safety_label_source/codec.rssuggests unification on the wire format axis, so it is a target for a later deep dive.
6The Brazil election filter got its regular update, and external PR #88 was merged — X
Source A xai-org/x-algorithm@9b0dc31 — home-mixer/filters/brazil_2026_election_filter.rs in the same commit (Gen4+15).
Source B xai-org/x-algorithm@e4dcedd — PR #88 merged, external contribution (joshs1017dev), 2026-09-03 20:28 UTC.
Background Brazil2026ElectionFilter is a political and regulatory-compliance filter that removes accounts reported to the Brazilian electoral court (TSE) from the home feed. This filter is the basis for the observation that X's regular maintenance continues every week. The canon records the history of this filter as follows.
- Gen4+1 (2026-08-14): filter introduced (665 accounts)
- Gen4+8 (2026-08-25): folded in the TSE dadosabertos 2026-08-25 edition (+3,379 lines)
- Gen4+10 (2026-08-28): comments reorganized
This commit updated the account list of this filter again.
What we learnedA. The candidate account list was verified again, and the file's behavior is unchanged
- The hardcoded account list grew from 2,388 to 2,554, a net increase of 166 verified — test assert in
brazil_2026_election_filter.rs.- The patch added 167
// @handleentries and removed 1. The newly added accounts include @nanacachen, @AecioNeves, @lgmbrasilia, @crismonteirosp, @rodrigolimamdh, @samiabomfim, and @gabrielmagno_13. - Each account appears with an obfuscated user_id and the @username in a comment.
- The patch added 167
- Two new "not the candidate's actual account" verdicts appeared: @BENICIODIAS and @CARLOSSAMPAIO verified — same patch.
- This is the result of X's internal verification that the account owners were judged not to be candidates.
- Two entries were removed from the "no live account found" list and three were added verified — same patch. A removed entry means the account came back to life, and an added entry means the account disappeared.
- The file's behavior (the follow exception kept, obfuscated user_id exposed, @username comments published) is unchanged.
What we learnedB. External contributor PR #88 was actually merged
- A PR (e4dcedd) from the external contributor joshs1017dev added two lines that dedup before the VF lookup.
- The location is just before the
in_network_idsVF lookup inhome-mixer/candidate_hydrators/vf_candidate_hydrator.rs.
- The location is just before the
- This is evidence that Gen4's open-source stance does accept external PRs.
- It is a single performance optimization, so there is no change to canon semantics.
Canon update Added both items to "Resolved by the Gen4+15 release / new" in services/x.md.
Why it matters The practice of continuously verifying the mapping of politicians' X accounts every week continues. This regular-update practice itself confirms that the filter is an active production code path. Merging external PRs reconfirms that Gen4 is maintained open source that accepts real external contributions, not "throw the code over the wall and abandon it".
Corrections
None. Today's commits extend the canon and do not overturn it. However, the canon entry from Gen4+3 saying "the Phoenix serving SID sidecar is optional" is updated to "the option was removed entirely; SID is part of the request contract". This is a change that came with time rather than an overturning.
Radar
None. The 30 arXiv cs.IR items were parsed successfully, and industry blogs were skipped because it was Saturday. Meta and Netflix had no new sources. Triage of the 30 items passed 0.
- Eight items were recommendation or CTR related: CORE, EPIC, LLM4AIGQ, HypRQ-VAE, UniCon CTR, Slow-Fast Thinkers Rec, SelfDR, and DoPR. However, all were mostly by academic institution authors and had no production deployment or A/B claims, so they were dropped.
- The other 22 items were dropped in three groups.
- Benchmark and evaluation methodology: SHELF, MultiGhostBench, Incremental Pooled Eval, Training seeds stability
- Domain-restricted: POI, legal, medical, science, baseball
- LLM usage methodology: RAG, private retrieval, essay grading
What to check next
- Check when the new reply spam pipeline was switched on and what its verdict metrics look like — no experiment gating is visible in this diff.
- Confirm whether
PlanMultiStepReplySpamruns on 100% of production traffic. - The actual allocation of the counters, that is, how often each filter fires, is the next checkpoint to watch. The target counters are
multi_step_reply_spam.scored.count,newest_not_flagged_dropped.count,other_author_dropped.count,singleton_dropped.count,json_repaired.count, andinvalid.count. - How often the three post-processing stages fire will reveal the gap between the LM's raw accuracy and the final accuracy after post-processing.
- Confirm whether
- Find the name and source of the hydrator in home-mixer (or the candidate hydration layer) that fills SID — look for where SID is placed in the request after
sid_clientwas removed.- Check what the SID codeword storage backend is (Manhattan? Strato? phoenix-rankall itself?).
- Check the history for whether the migration had a dual-write period.
- Check when the H100-specific config bundle (
_H100_OVERRIDES) appears and how H100 tuning differs from GB300 — only the remat policy was registered this time.- How batch size, attention impl, checkpoint interval, LR, and so on are assigned differently for H100 will appear in the next commit.
- Check how the Grok 4.1-fast judge differs from 4.5 X_ALGO — the thing to watch is the latency, cost, and accuracy trade-off.
- The model name of the 4.1-fast judge is
EAPI_GROK_4_1_FAST_X_ALGO. - Look for the policy rationale for moving only IllegalAndRegulatedBehaviors to 4.1-fast.
- Watch for when the ChildSafety and ViolentMedia judges also move to 4.1-fast.
- The model name of the 4.1-fast judge is
- Confirm what the 206 deleted lines in safety_label_source/codec.rs actually were — check which wire format and label source were removed. Settling the safety_labels codec unification direction in the canon needs the next observation.
- Check which category moves to Gemma next — the question is whether violent speech, hate speech, and adult content follow Spam and IllegalAndRegulatedBehaviors in that order. How far the Gemma replacement extends will reveal the trade-off between grok API budget and verdict accuracy.