0x00 한

Research / Daily reports

  • Sources read: 2
  • Canon files updated: 2
  • Radar entries: 1
In one lineX added a new ads blender that decides ad slots by looking at both post risk and ad tier. It is not yet the default blender and sits behind an experiment gate.

Other changes reviewed today:

  • In the same commit, X added a warmer that samples the items the safety-label cache of the impression-verdict service missed and fills them in ahead of time.
  • The training code of X's ranking model now actually calls streaming checkpoint restore, and serving fixed a gRPC trailer loss bug.
  • Meta and Netflix had no new blog posts, and one friend-recommendation GNN paper on arXiv passed the radar.

1A blender that sets ad slots from post risk and ad tier was added — X

Source xai-org/x-algorithm@24c6094 — xAI public repository, commit of 2026-08-28T01:00:41Z (41 files, +2071/−265), code. These notes number X commits after the Gen4 release in order, and this commit is Gen4+10.

  • The commit message is a single line, "Open-source X Recommendation Algorithm", as is the convention of the generation series. So the actual content can be read only from the file diff.
  • By line count (+2071/−265, 41 files), it is larger than Gen4+7 (+1,418) and +6 (+856).
  • It touched six subsystems, a wide range.
    • ads, filters, and util in home-mixer (the service that assembles the For You feed)
    • the safety-label lookup infrastructure of visibility-filtering (VF, the service that decides per post whether it may be shown to this viewer)
    • training and serving of Phoenix (the ranking model of X For You)
    • reply_spam in grox (the content classification service)
    • abuse-enforcement
    • simclusters batch job

Background The Gen4 open-source release of the X For You timeline has been followed by commits one to three days apart since the 2026-08-13 headline release (47c1bcd, +363,246/−11,640). The immediately preceding commit, Gen4+9 (2026-08-26, 45b48ba), was a broad commit that bundled several infrastructure cleanups. These were the save/restore cleanup of the Phoenix checkpoint stack, unifying trainer_recsys valid_step lock-step, expanding the grox reply_spam LM prompt, and removing the VF media fallback cache. This Gen4+10 commit (24c6094) has three aspects:

  • Phoenix streamed restore and grox monotone-only, which came in with the 2026-08-26 commit (Gen4+9), are now wired through to their consumers.
  • A tier×tier blender option, the last piece of the ads safety stack, is new.
  • Warmer infrastructure was added to lower the twemcache miss rate, which drives P99 latency on the VF request path.

The pattern in which the symmetric reinforcement of the 2026-08-26 commit (Gen4+9) connects to an actual consumption path in this commit keeps repeating.

1-A. Ad slots next to medium-risk posts go only to BSR-high ads, and no ads are placed next to high-risk posts

A fourth ads blender option was added to home-mixer. The ads blender is the stage that places ads between posts in the feed, and the new blender decides ad slots with a combination table of three post risk tiers and two ad tiers (BSR, brand safety risk).

  • The new blender MultiRiskAdsBlender was wired in as the fourth ads blender option verified — home-mixer/ads/multi_risk_blender.rs.
    • The implementation is home-mixer/ads/multi_risk_blender.rs (332 lines, new), and the tests are home-mixer/ads/tests/multi_risk_blender_tests.rs (215 lines, new).
    • home-mixer/selectors/blender_selector.rs (+8/−4), which selects the blender, also changed (home-mixer/selectors/blender_selector.rs).
  • The three existing blenders safe_gap, partition_organic, and time_gap enforced a symmetric policy: "all ads → placed only next to Safe posts."
  • The new option multi_risk splits posts into three tiers and ads into two, and decides slots with an adjacency matrix.
    • The post tiers are Safe, MediumRisk, and HighRisk; the ad tiers are BsrNormal and BsrHigh.
  • The three-tier classification of posts depends on the BrandSafetyVerdict::HighRisk tier introduced in Gen4+7 (2026-08-24).
    • With the introduction of HighRisk, the five kinds previously in medium and the new SOFT_NSFW were promoted.
    • The five kinds are NSFW_HIGH_PRECISION, NSFW_HIGH_RECALL, GORE_AND_VIOLENCE_HIGH_PRECISION, PDNA, and EGREGIOUS_NSFW.
  • The two-tier classification of ads follows the BSR field value, which appears to be an opt-in signal by which an advertiser states it accepts having its ads shown in medium-risk contexts inferred (the code has only the field definition; the advertiser UI flow is not public).
    • The field is AdIndexInfo.ad_adjacency_control.brand_safety_risk, and the values are BsrNormal|BsrHigh.

The slots each ad tier can occupy are as follows:

Ad tier Next to Safe Next to MediumRisk Next to HighRisk
BsrNormal (default) Placed Not placed Not placed
BsrHigh (advertiser opt-in) Placed Placed Not placed

That is, a BsrHigh ad gets a medium-medium slot first, and if it fails the overlap check it moves on to a remaining safe slot verified — multi_risk_blender.rs:96-130. However, it is still excluded if the advertiser handle is mentioned in a neighboring post or an ad keyword overlaps the neighboring text. A HighRisk post cannot be adjacent to any ad.

  • Why a slot is empty and which constraint was binding are observed with three new metrics.
    • The three are MultiRisk.enforcement, slot_outcome, and serving_limitation.
  • The safe slot budget is the smallest of the number of ads, the spacing cap, and half the number of safe posts.
    • The formula is safe_budget = min(num_ads, spacing_cap, safe_count / 2).
    • Which of the three caps was binding is observed with the MultiRisk.serving_limitation{factor=ads_supply|spacing|safe_posts} counter.
    • For example, if ad supply is sufficient but slots are not filled, safe_posts shows up as the factor. This is a signal that the viewer's feed lacked safe posts.
  • For each slot, it separately records whether it was filled with an ad or left empty, and which safety filter caught it.
    • The counter is MultiRisk.slot_outcome{outcome=filled|unfilled_bsr_low|unfilled_handle|unfilled_keyword|unfilled_no_ads}.
    • SlotRejections::stuck_outcome() picks the most frequent of the three rejection reasons (bsr_low, handle, keyword) as the representative cause for that slot.
    • So the A/B dashboard can directly show "the safety filter that blocked most often when a slot was empty."
  • In the same commit, an alias pointing to an existing blender was added, bringing the blender options to five in total.
    • The "partition_organic_low_risk" alias was added to the AdsBlenderType param. This alias routes to the existing PartitionOrganicAdsBlender.
    • The options are safe_gap / partition_organic / partition_organic_low_risk / time_gap / multi_risk.

"if any_bsr_high && max_medium_slots > 0 && spacing_cap > 0 { ... } else if safe_budget == 0 { return posts_to_feed_items(scored_posts); }" — home-mixer/ads/multi_risk_blender.rs, 2026-08-28

Canon update Added a "Gen4+10 (2026-08-28, 24c6094) — MultiRiskAdsBlender introduced" subsection to the ads section of services/x.md. The subsection contains the adjacency matrix table, the slot budget formula, and the definitions of the three metrics.

Why it matters The five-day thread that began with the introduction of the HighRisk tier in Gen4+7 was completed by this tier×tier matrix blender. The sequence is Gen4+7 HighRisk tier introduction → Gen4+9 ads pixel and conv_asset_ids wiring → Gen4+10 matrix blender.

  • With HighRisk split out as a tier and the ads pixel conversion head attached, a blender option that lets advertisers opt in to their exposure range along the tier axis is, in effect, now in place as well.
  • From the advertiser's perspective, it is also a signal that the incentive structure of BSR-high opt-in has been put explicitly into the code. An advertiser that accepts medium-risk contexts gets medium-risk slots, which increases total impressions.
  • However, AdsBlenderType is not yet the default and sits behind an experiment gate. The next checkpoint to watch is when this param's default is promoted to multi_risk.

1-B. VF samples items missed by the safety-label cache and fills them asynchronously ahead of time

A warmer was added that picks some of the items the cache missed during VF safety-label lookups and fills the cache ahead of time, separately from the request. The warmer does not make requests wait and does not change the verdict rules.

For each request, VF looks up the safety labels of dozens to hundreds of candidate posts and decides Allow / Interstitial / Drop. Twemcache sits in front of the label lookup as a look-aside cache, and on a miss it falls back to Manhattan (a KV store). The hit rate of this lookup is the key variable that drives VF P99 latency.

  • Labels are read from the visibility/baseTweetSafetyLabelMap column of Strato (the internal system X code goes through to read or store data).
  • Verdicts follow a 3-tier policy, and the rules are 28 from base_home_rules() plus 26 extra for OON = 54 rules. OON (out-of-network) means posts from accounts the viewer does not follow.

The warmer works in four steps:

  1. It sorts cache results into four kinds and collects only the cases where the cache does not know the key as warmer targets.
    • RemoteSource::get(ids) sorts the twemcache result into Hit, NotFound, Miss, and FallThrough(reason).
    • Only Miss (the cache does not know this key) is collected into a separate vector, warm_ids.
    • NotFound (does not exist) is a tweet that has no label by definition, so it is not a warmer target.
    • FallThrough (cache error or timeout) is a cache infrastructure problem, so it is not something the warmer can resolve.
    • This three-way split is the principle behind introducing the warmer. The warmer resolves only the "cache not yet filled" state.
  2. Before the Manhattan fallback request goes out, it calls warmer.warm(warm_ids) fire-and-forget. This does not affect request response latency.
  3. It picks a sample with a probability set by an environment variable and puts it in a bounded queue; if the queue is full, it only increments a counter and continues handling the request.
    • SampledWarmer does probability sampling with the VF_CACHE_WARM_SAMPLE_PCT environment variable (0–100, panic on out-of-range).
    • Only the IDs that pass are sent with try_send to a tokio mpsc::channel bounded queue (capacity 1024).
    • If the channel is full and the send fails, it only increments the WarmKeyResult::DroppedChannelFull counter.
    • It was designed so that no backpressure ever hits the request path even if the warmer channel saturates.
  4. A background drain task gathers IDs and sends a bulk lookup to Strato.
    • After popping a vector from the queue, the task merges vectors that follow during a 500ms linger and bundles them into one batch of up to 50 IDs.
    • The bundled batch sends a batch_call to Strato with StratoWarmFetcher::fetch(chunk) (WARM_COLUMN_PATH = "visibility/baseTweetSafetyLabelMap").
    • The result written back by Strato fills twemcache. However, the warmer code does not write the fetch response to twemcache directly, so we infer that a separate read-through cache layer exists inferred.
  • The pass rate and bottleneck at each warmer stage are observed with six outcome counters.
    • record_cache_warm_keys(result, count) records EligibleMiss, SampledOut, Enqueued, DroppedChannelFull, FetchIssued, and FetchFailed.
    • EligibleMiss is the total that became warmer targets as misses, a figure before sampling.
    • For example, a high FetchFailed / FetchIssued indicates a Strato infrastructure problem, and persistent DroppedChannelFull means the channel capacity (1024) is insufficient.
  • The same file tidies configuration by adding three new environment variables.
    • The new envs are VF_GIZMODUCK_CLIENT_ID, VF_TWEMCACHE_CLIENT_NAME, and APP_ENV.
    • If no value is set, gizmoduck_client_id() assembles the value automatically from the template visibility-filtering-service.{APP_ENV|prod}.
    • We presume the purpose is to separate the staging gizmoduck (X's user information service) quota and rate limit from production when VF runs on staging with dark_traffic inferred.

Canon update Added a Gen4+10 subsection to the safety/visibility-filtering section of services/x.md. The subsection covers the warmer flow, the bounded channel principle, the six metrics, and the config cleanup.

Why it matters This warmer does not open a new layer in the VF request path; it optimizes the underlying infrastructure. The rule verdicts (3-tier / 54 rules) are unchanged.

  • A twemcache miss occurs in two cases.
    • The tweet is fresh and has not yet received any labeling, so neither the cache nor a label exists.
    • The TTL expired or the entry was evicted, so only the cache is empty.
  • This warmer addresses the second case in particular: Manhattan has the label but the cache missed it, so every request wastes a Manhattan lookup.
  • With the sample-pct gate, it was designed so that the warmer itself does not cause a Strato overload even if traffic surges.
  • A dial-up rollout that starts with a low VF_CACHE_WARM_SAMPLE_PCT and raises it gradually is also possible. It resembles other Gen4 dial-up patterns (Grok 4.6 10%→30%→100%, Gen4+4→+5→+6).

1-C. Phoenix training code actually calls streaming checkpoint restore

Streaming checkpoint restore, which came in only as scaffolding in Gen4+9 (2026-08-26), is actually called by the trainer starting with this commit. The core of this path is that it does not make a pinned-host copy of the optimizer state, but moves tensors to the device one at a time and inserts them into their original reference slots.

  • The trainer's checkpoint load function became the actual consumer of streaming restore verified — phoenix/xrex/train/trainer.py:1177-1330.
  • The new path turns on only when all three conditions hold, and if any one fails it returns to the existing restore path.
    • The config flag that turns on the new path is restore_streamed.
    • The three conditions are restore_streamed=True, format == "orbax", and save_method != "tensorstore".
    • If any one fails, it logs and falls back to the existing load_checkpoint path (full pinned-host staging).
    • In other words, if the config is wrong or a non-orbax checkpoint (such as tensorstore) is encountered, it falls through to the safe default path, so backward compatibility is preserved.
  • Loaded tensors are inserted in place into their slots in the original state tree (grafting).
    • The use_streamed_restore=True path itself calls checkpointing_load.copy_aliased_arrays(self.state) to separate aliased state leaves beforehand.
    • The grafting is handled by the new function _graft_replaced(pairs).
    • If an aliased leaf spans two slots, it is ambiguous which slot to place it in, so _graft_replaced blocks this case early with an assertion.
    • The assertion message is restore_streamed does not support aliased state leaves: multiple loaded tensors share one device array.
  • Two new arguments that the streaming restore function receives were also routed in this commit.
    • The function is load_checkpoint_streamed, and window_gb is the pinned-host budget for that batch. If None, it runs with auto-window.
    • The on_replaced callback receives a list of old/new device array pairs for a completed batch and has the caller insert them into the state tree.
    • window_cap_gb is a safety belt set to the restore_concurrent_gb value only when window_gb is not specified. It reuses the concurrent_gb of the previous API as the window cap upper limit on the streamed path.
  • The benefit of this flow is especially large in the warm_start case. Warm start loads pretrained params and creates the opt state fresh.
    • The existing do_not_load_opt_state=True path trims the opt state fields beforehand with warm_purge(self.state), and the streamed path leaves host_state as None and inserts only the loaded params. So no pinned-host copy of the opt state is created at all.
    • The standard flow in which the Muon and GB300 combination flagship of Gen4+6 and +7 starts by attaching a new optimizer to pretrained parameters is now supported atomically by the checkpoint stack.

Canon update Added as the first branch of the "Gen4+10 (2026-08-28, 24c6094)" subsection in the Phoenix training/index section of services/x.md.

Why it matters The two-day flow from Gen4+9 to +10 was completed as "scaffolding → consumer wiring." It is not clear from the code alone whether restore_streamed is currently the default or behind an experiment flag, but since only one line was added to the config, we infer that the default is still false inferred. The next checkpoint to watch is the commit in which this flag is promoted to True in the flagship config.

1-D. Fixed a bug where gRPC response compression lost status trailers, and began measuring compression time

A bug where the trailer carrying the gRPC status code disappeared when Phoenix serving compressed responses with zstd was fixed. A histogram that measures compression time per method was also added in the same file.

  • The previous logic lost all gRPC trailers during compression verified — phoenix/crates/serving/xai-recsys-engine/src/grpc_compression.rs.
    • When GrpcCompressionService compressed the response body with zstd, it extracted only the body with collected.to_bytes() and wrapped it again in a new axum::body::Body::from(data).
    • In this process, grpc-status and grpc-message in particular disappeared.
  • A gRPC frame carries the status code as a trailer after the body, and that part was discarded.
  • On the client side, the response may have arrived normally but been handled as UNKNOWN or EOF because the status trailer was missing. If so, retry logic, error logging, and circuit breakers would all have misbehaved.
  • The new code extracts the trailer beforehand and attaches it when rebuilding the response. It is unified so the trailer is kept whether compression succeeds or fails.
    • The trailer is extracted beforehand with collected.trailers().cloned().
    • The rebuild_response(parts, data, trailers) helper reattaches the trailers with Full::new(data).with_trailers(async move { Some(Ok(tr)) }) and then builds the response body. In short, the trailers are restored with Full::new(data).with_trailers(...).
  • Two regression tests lock in this behavior.
    • The tests are compressed_response_forwards_trailers and non_compressed_response_forwards_trailers.
    • As long as these two tests remain, CI will catch it if a similar refactor leaks the trailer again later.
  • A histogram that measures compression time per method is new.
    • It is named recsys_engine_response_compress_ms{method} and was put in the same file.
    • It stamps Instant::now() just before entering tokio::task::spawn_blocking(compress_grpc_frame) and observes with elapsed().as_secs_f64() * 1000.0 after completion.
    • That is, the queueing time and CPU time of zstd encoding can be viewed separately per method.
    • The buckets are shared with the request latency buckets (crate::request_metrics::latency_buckets_ms()).

Canon update Added as the second branch in the Phoenix section of services/x.md.

Why it matters Trailer loss is a serious bug that silently breaks the serving contract. This commit also attaches regression tests, which greatly lowers the chance of the same problem recurring.

  • The _compress_ms histogram shows how much zstd encoding of large responses on GB300 contributes to response latency. So it gives a basis for tuning the compression level.
  • When this bug first appeared in production and what evidence was observed (customer tickets or a retry storm) are not public.

1-E. Spread peer load evenly by giving each pod a different shard order

When each pod downloads several shards of a tensor in parallel, a shuffle now gives each pod a different shard request order. The order is determined deterministically by a hash of the pod name and the tensor name, so the same pod requests the same tensor in the same order even after a restart.

  • Added a new function shuffle_sharded_schedule(n, tensor_name) that shuffles the shard request order verified — phoenix/crates/serving/xai-recsys-engine/src/copy_port_client.rs, phoenix/crates/serving/xai-recsys-engine/src/emb_table.rs.
  • When each pod downloads several shards of a tensor in parallel, previously every pod requested them in order starting from index 0. As a result, the owner peer holding the first shard was briefly overloaded.
  • The new logic applies a deterministic shuffle based on a hash of the pod name and the tensor name so that each pod requests shards in a different order, and the spike on the shard 0 peer disappeared.
    • The pod name comes from POD_NAME (or HOSTNAME), and the shuffle is seeded with StdRng::seed_from_u64(hash).
  • Because the seed is deterministic, the same pod keeps the same order after a restart, so logs can be reproduced.
    • In exchange, a restore_piece_order(shuffled, schedule) -> Option<Vec<T>> helper puts completed results back into the original order.
  • Both consumers, emb_table.rs and copy_port_client.rs, were unified on the new helper.
  • The shuffle applies only to sharded ownership, and replicated stays in-order. All pods receive the same data, so shuffling would gain nothing.
  • Only the first 8 indexes are logged, so you can diagnose whether the shuffle was actually applied.
    • The log format is copy_port: shard schedule shuffled name={tensor} n={total} first={:?}.
  • On encountering TRANSFER_FAILED_SENTINEL, it returns early with an error before restoring.

Canon update Added as the third branch in the Phoenix section of services/x.md.

Why it matters This shuffle divides the last bottleneck remaining on the shard 0 peer in inverse proportion to the number of pods. When several pods restored large embedding tables and dense weights, traffic spikes concentrated on the peer responsible for shard 0. The node-serialize and concurrent_gb changes in Gen4+7 mitigated this problem, but the spike on the peer side remained.

  • Deterministic shuffling matters for two reasons.
    • The shuffle itself can be reproduced from logs.
    • Even when the same pod loads the same tensor several times, the order is the same, so cache hit rate observation is stable.

1-F. The remaining 6 changes added observability metrics and adjusted storage order, tests, and batch fallback

The remaining 6 changes in Gen4+10 are independent improvements. Across home-mixer, grox, abuse-enforcement, and simclusters, they changed observability metrics, storage order, regression tests, comment cleanup, and snapshot fallback.

  • home-mixer serving metrics can now separate periodic polling refreshes from initial user requests verified — home-mixer/util/strato_context.rs (75 lines, new).
    • The new utility extracts the stratocontext or stratocontext-bin key from the tonic gRPC MetadataMap, decodes it from base64, and parses it as the proto StratoContext { is_polling: bool }.
    • That is, it extracts the is_polling proto field from the stratocontext metadata to tell periodic refreshes apart from initial user requests.
    • The side that increments the counters is home-mixer/side_effects/response_stats_side_effect.rs (+34/−0).
    • The new function stat_request_context(feed, query) in response_stats_side_effect.rs fires three counters per request.
      • <feed>.request.polling{polling=true|false, client}
      • <feed>.request.request_context{request_context, client}
      • <feed>.request.client{client, client_version}
    • Polling requests have shorter sequences and a different has_cached_posts probability, so observing this axis separately matters for interpreting ranking experiments.
  • grox reply_spam tightened its Manhattan storage condition, changed the storage order to Kafka first, and added follower count to the coordinated spam verdict verified — grox/flows/reply_spam/task_write.py (+15/−21).
    • _publish_to_reply_ranking_manhattan was tightened in three ways on top of the monotone-only logic from Gen4+9: >= idempotency, Kafka-first storage order, and fail-loud fetch.
      1. The comparison operator changed from > to >=. Previously, a new score equal to the existing value was written anyway (a duplicate write). Now it is skipped when score >= existing.score. The counter name also changed from higher_than_existing to not_lower_than_existing to state the semantics explicitly.
      2. The Kafka event was moved ahead of the Manhattan write. The previous order was save_reply_ranking_score(Manhattan) followed by save_reply_ranking_kafka_v2(Kafka), and the new order is the reverse.
        • Kafka is append-only, so a failure can be retried. Manhattan, by contrast, can skip the retry itself because of the monotone-only logic, and it is heavy because it also has an upsert side effect.
        • Kafka-first is the durability order that guarantees observability consistency across the whole system.
        • If the Manhattan write fails after the Kafka publish succeeds, the offline reprocessing pipeline can recover. In the reverse order, downstream consumers (serving cache warm-back, other consumers) would not see the new score already reflected in Manhattan.
      3. The try/except around fetch_reply_ranking_score was removed. Previously, when the fetch failed, it logged a warning and proceeded with existing = None, so it always ended up writing. Now the exception is raised straight up, so the flow task is marked as failed and retried. Defensive exception handling was removed in favor of correctness of the logic.
    • The follower count change is in a second file, grox/flows/reply_spam/classifier_coordinated_spam.py (+5/−1).
    • In the same commit, an argument was added so that _to_convo in classifier_coordinated_spam.py calls PostRenderer.render(p, max_media=..., include_follower_count=True).
    • include_follower_count, which was introduced as an option in Gen4+9, is adopted here as always on. The coordinated spam verdict now receives the follower count of each account that took part in the thread as part of the prompt.
  • abuse-enforcement gained a conformance test that checks that every action shape encodes against the real AIS (ActionIntakeService) thrift schema verified — abuse-enforcement-service/service-lib/src/service.rs (+58/−0).
    • The new test is every_action_shape_encodes_against_the_ais_schema.
    • This test prevents regressions by encoding the 8 EnforcementAction shapes against the real AIS thrift schema.
      • The shape list is suspend_user, add_user_label, add_post_label, bounce_arkose, bounce_captcha, and spam_liveness_check.
    • The target method is ActionIntakeService.intakeAction.
    • It loads the schema with Transcoder::from_schema_bytes(SCHEMA_AIS), finds the method with lookup_method, and then performs build_intake_request and encode_call for each action.
    • In effect, CI guarantees that the payload schema of AES (abuse-enforcement-service) does not drift from the downstream thrift service.
    • It is a safety belt attached to the flow in which scorers can send arbitrary actions through the generic_actions passthrough introduced in Gen4+8.
  • A regression test now pins the behavior that the home-mixer muted-keyword filter filters CJK keywords only when they appear as whole tokens verified — home-mixer/filters/viewer_muted_keyword_filter.rs (+18/−0).
    • The new test is test_cjk_keyword_whole_token_only.
    • Muting 京都 filters I visited 京都 last week. In this sentence, 京都 appears whole within whitespace boundaries.
    • 東京都に行くのが楽しみ must not be filtered. Here 京都 is a substring of 東京都.
    • There is no diff in the filter body. That is, the filter already handled CJK word boundaries, and this change pinned those semantics with a regression test.
    • It prevents the incident in which a muted keyword expands to substring matching and users stop seeing posts they did not expect to miss.
  • The comments on the account list of Brazil2026ElectionFilter were reorganized again, and the filter behavior is unchanged verified — home-mixer/filters/brazil_2026_election_filter.rs (+63/−77).
    • Previously, 665 accounts were listed under a single repeated comment such as no live account was found or deleted his account.
    • Now 10 accounts carry the verdict "We believe the account @X reported by the candidate is not the candidate's actual account, so we are not currently filtering it", and most of the rest are summarized as no live account found.
    • The filter semantics are unchanged and the follow exception is preserved.
    • Separately from the list update practice in Gen4+8, this change cleans up the wording of the documentation and comments.
  • The SimClusters (embeddings built by dividing the follow graph into communities) batch job now inlines a helper so that, when reading snapshots, it uses a 28-day lookback, a data center pin, and a temporary recovery path fallback together verified — simclusters/simclusters_v2/scalding/embedding/EntityToSimClustersEmbeddingsJob.scala (+17/−2).
    • The target is the SimClustersInterestedIn2020 snapshot.
    • The InterestedInSources.simClustersInterestedIn2020Source(dateRange, timeZone) helper was inlined directly with the following three settings.
      • DAL.readMostRecentSnapshot(interestedIn2020WithFallback, dateRange.prepend(Days(28)(timeZone)))
      • .withRemoteReadPolicy(ExplicitLocation(ProcAtla))
      • fallbackPath = Some("viewfs://.../simclusters_v2_interested_in_20M_145K_2020/_TMP_RECOVERY/{timestamp}")
    • That is, a 28-day lookback is combined with a pin to a specific data center (ProcAtla) and a temporary recovery path fallback.
    • The batch job became more tolerant of missing snapshots (such as lost dataset partitions or upstream delays). The change strengthens the data reliability of the SimClusters embedding job.

Canon update These six changes were each recorded as a single checkmark line in the Gen4+10 release resolved / new bucket of x.md. The items are independent of one another, so indexing them in the bucket is easier to find than pulling them out into separate sections.

2A paper scaled a friend-recommendation GNN to production social graph size — arXiv

Of the 30 arXiv cs.IR papers skimmed, only 1 passed the acceptance criteria (platform-company authors / production or A/B / new mechanism / scale). The other 29 were rejected because they were purely academic increments, limited to a narrow domain, or had no production claim.

Source Scaling Graph Neural Networks for Friend Recommendation: Multi-Hash User Embeddings and Temporal Neighbor Sampling — arXiv 2608.27413, 2026-08-27 v1, paper, CIKM 2026 accepted. There are 3 authors (Maksim Utushkin, Andrei Ovsiannikov, Alexander D'yakonov). The arXiv abstract page does not list affiliations, but taking the graph scale referenced in the body together with the Russian author names, we infer VK (a large Russian social network) inferred.

Background In friend recommendation (features of social platforms such as People You May Know and Suggested Friends), multi-hop social context such as friends of friends and the number of mutual friends is the main relevance signal, more than user attributes such as interests, age, and location. A GNN (Graph Neural Network, a message-passing model in which a node updates its own embedding by aggregating the embeddings of its neighbors) is therefore a natural framework. However, production social graphs have hundreds of millions of users and tens of billions of edges, which creates two system problems.

  • The user ID embedding table explodes.
  • Neighbor traversal for supernodes (accounts with an extremely large number of follows) becomes a bottleneck at O(deg) cost.

This paper tackles these two system problems head-on.

What we learned

  • Multi-hash user embeddings reduced the ID embedding table by more than 98% verified — original text.
    • Each user ID is represented as a combination of embedding lookups from several hash functions.
    • It accepts hash collisions while maintaining ranking quality.
  • Temporal neighbor sampling reduced the neighbor sampling cost to logarithmic time verified.
    • It combines timestamp-sorted CSR (Compressed Sparse Row, a standard graph storage data structure that compresses adjacency lists into one large array and an offset array) storage with binary search.
    • The complexity went from O(deg(v) + k) to O(log(deg(v)) + k).
    • That is, even for nodes with a large follow count, the neighbor traversal cost stays at logarithmic time.
  • The scale of the production social graph is 194M users · 28B edges verified.
  • Online metrics against a strong production baseline are friend additions from recommendations +16% and unique friend adders +11.5% verified.

"On a graph with 194M users and 28B edges, our system increases friend additions from recommendations by 16 percent and unique friend adders by 11.5 percent over a strong production baseline." — arXiv 2608.27413 abstract, 2026-08-27

Radar update Added as a new entry under "new this month" in radar.md. This paper does not fit the LLM-native or scaling-law lines. Instead, as a case where GNNs still survive in an explicit graph domain, it serves as a contrast to Meta ConnectionMind (a model that reasons over paths on a graph with an LLM).

Why it matters A double-digit uplift (+16%) over a strong prod baseline is rare in GNN papers. It reinforces the observation that the domain where GNNs ultimately survive in practice is the fully explicit graph (friend graph, follow graph).

  • The direction differs from Meta ConnectionMind, which tries graph path reasoning with an LLM. VK pushed a pure GNN to the scale limit of its social graph.
  • The multi-hash embedding reduction of -98% points the same way as the X SimClusters observation that embedding table size is a key constraint on serving infrastructure.
  • The abstract does not state that it was applied without increasing serving latency. However, that is exactly the purpose of the two optimizations (shrinking the table and making neighbor traversal logarithmic).
  • There are two next checkpoints to watch: whether the CIKM 2026 (October) presentation discloses additional affiliation and infrastructure detail, and whether statements on GNN updates for LinkedIn People You May Know and Facebook People You May Know appear.

What to check next