0x00 한

Research / Cross-service topics

Multi-task ranking — combining several engagement probabilities into one score

Multi-task ranking is a structure in which the ranker does not predict "will this user like this item" as a single scalar. It predicts a separate probability for each kind of action, then combines them into one score and sorts by it. An output that produces one probability per action is called a head.

score = Σ_k  w_k · P(action_k | user, item)

However, this weighted sum is only one of several ways to combine them. Within the same company, Meta published a additive form, a multiplicative form, and a multiplicative penalty as formulas for different surfaces, each in a different post (table in the Meta case). "Multi-task ranking = weighted sum" is an assumption, not an observation.

There are four design decisions that actually have to be made here.

  • Which actions to use as heads
  • Which form to combine them in
  • How to mix in negative actions
  • Where to get the weights w_k

⚠ Every claim in these notes that is based on the Meta system card (a regulatory-response document in which the Transparency Center publishes the list of prediction heads per surface) comes from a snapshot read on 2026-08-09. The number of prediction heads is a typical example. The full caveats are collected in the "system card citation shelf life" section of ../services/facebook.md.

Why this structure

Splitting heads per action and keeping separate weights lets you change the objective without a code deployment and also protects the training signal of sparse actions inferred. The cost is that the weights become product policy directly.

  • The objective can change without a code deployment. The model outputs only probabilities, and the product judgment (how many times more a retweet matters than a like) is separated into the weights inferred. X separating all weights into runtime injection is consistent with this reading.
  • Putting sparse actions (report, block) in separate heads keeps their training signal from vanishing. Merging them into one scalar lets common actions drown them out inferred.
  • There is also a cost. The weights become product policy directly, and even at X those values were not in the public source until the Gen3 release. The X case shows this problem best.

↑ Contents

Case: Meta — the combination form differs by surface

Meta published 4 combination forms within one company (counting the additive form with negative terms separately from the plain additive form). The combination form Facebook published is linear only, while Instagram published that it uses three different forms for different problems.

Surface Form Formula (verbatim unless noted) Year
Facebook Feed Additive Vijt = wijt1·Yijt1 + wijt2·Yijt2 + … + wijtk·Yijtk 2021
Instagram Suggested Posts Multiplicative Value(Post) = (probability_like)^weight_like * (1- probability_not_interested)^weight_see_less 2020
Instagram Explore Additive (with negative term) Expected Value = W_click * P(click) + W_like * P(like) – W_see_less * P(see less) + etc. 2023
Instagram Notifications Multiplicative penalty Score(c) = R(c) × D(c), D(c) = ∏_{i=1..m} (1 - w_i · p_i(c)) ⚠ not verbatim (explained below the table) 2025

The formula in the Notifications row is not verbatim. The original is a LaTeX image, and the two formulas in the table are a transcription of that image observed 2026-08-09. The multiplicative structure and the definitions of R and D are supported by the body prose verified — feed-diversity.md.

Context for each surface is in facebook.md and instagram.md. Only the form comparison is made here.

How to read this

Multiplicative and additive forms give negative signals different authority inferred. However, the 2020 multiplicative form and the 2023 additive form are on different surfaces, and there is no source comparing before and after on the same surface inferred.

  • The weight w in the Facebook formula is indexed by (i,j,t). This means the weights are personalized per post, user, and point in time verified 2021.
    • The original text says: "for each post i, we estimate Yijt", "the characteristics of the post X_it toward viewer j at time t". So i is the post and j is the viewer.
    • However, since only the symbolic formula is available, we cannot confirm whether the actual implementation really depends on i (the post) inferred.
  • Multiplicative and additive forms give negative signals different authority inferred. This judgment follows directly from the form of the formula.
    • In the multiplicative form, if P(not_interested) → 1 the value collapses to 0, so it is effectively a hard veto inferred.
    • In the additive form, no matter how large the negative term is, other terms can outweigh it, so it is an exchangeable cost inferred.
  • Taking the log of the multiplicative form also gives a linear expression Σ w·log P inferred. That is, it is the same as Facebook's Σ w·P in being a "linear combination", and the only difference is whether P or log P is used.
  • ⚠ The 2020 multiplicative form and the 2023 additive form must not be read as a "transition". The surfaces differ (Suggested Posts and Explore). There is no source comparing before and after on the same surface inferred.
  • The fourth, the Notifications formula, is a diversity penalty rather than a combination. Details are in feed-diversity.md.

The opposite of X — whether negatives are multiplied

On the question of whether to multiply negative probabilities into the score, the two companies went in opposite directions.

  • Instagram 2020 multiplies in the negative probability to give a veto ((1 - P_not_interested)^w) verified 2020.
  • X 2025 deliberately does not multiply. It only adds a constant penalty when a threshold is exceeded verified (design decision 2).
  • X 2026 multiplies, but compresses a negative total score to a 0.001 scale so that it never mixes with positive candidates verified — params/config.rs:40 (Gen4 47c1bcd).

X spent two generations keeping negative heads from changing the ranking much, while Instagram instead uses the property that the score collapses to 0 in the multiplicative form as a feature inferred. Neither the code nor the blog posts state why the two companies designed it this way.

Where the weights come from — the real divergence between companies

What each company has published about how it sets weights differs. Facebook published survey correlation, Instagram published offline replay/tuning and Bayesian optimization, and X did not publish a method verified.

Party Published method Evidence
Facebook Set by the correlation of surveys with each event verified 2021
Instagram Offline replay/offline tuning and Bayesian optimization verified 2020, 2023
X Method undisclosed. The injection points (FeatureSwitch parameters / query features) and value ranges are in the code, and since Gen4 there is also a mirror of the defaults verified We checked the entire public repos (c54bec0, xai-org/x-algorithm). Through Gen3 the values were not in the code, and param.rs in Gen4 (47c1bcd) mirrors the production defaults
  • The Instagram original text says the following verified.
    • 2020 post: "offline replay over user sessions and b) online Bayesian optimization"
    • 2023 post: "There are two popular approaches to parameters tuning: Bayesian optimization and offline tuning."
    • The word "replay" appears 0 times in this 2023 Instagram post (full text checked 2026-08-09) verified.
  • The Facebook original text, the meaning of "correlation", and why surveys do not suffer from the sparsity problem are covered in survey-signals.md. They are not repeated here.
  • Meta, the same company, published different answers to the same problem. inferred In practice it is likely that both are used and only the emphasis differs, but at the level of the published sentences they clearly diverge.
  • Through the Gen3 release, none of the three parties published numbers. Meta published the method and withheld the values, and X published the injection points and ranges and withheld the values. What they published differed, but the conclusion was the same.
    • Since the Gen4 release (2026-08-13), X publishes the default weights in the repo as a cron mirror verified — x.md weights section.
  • Meta also has one source corresponding to X's ModelWeights list. The Transparency Center divides predictions into 3 tiers by how often they are used, publishing only an ordering instead of weights verified — Our Approach to Facebook Feed Ranking (updated 2025-06-11).
    • The three tiers are Used most frequently / occasionally / less frequently.

Head counts 19–22 vs ~10 — two numbers that must not be compared

Do not write "X has twice as many heads." The two numbers are different in nature. The X number is counted from source code, and the Meta number is from a public document, the system card verified.

Source Head count Source type
X Phoenix (Transformer ranking model) public checkpoint, Gen3 19 Source code
X 2026 serving code, Gen3 22 Source code
X 2026 VMRanker (DPP slate optimization) proto copy, Gen3 21 Source code
X Gen4 README (2026-08-13) 24 Documentation
X 2025 Navi / Phoenix 15 / 13 Source code
Facebook Feed card 10 system card
Facebook Feed Recommendations card 10 system card
Threads Feed card 10 system card
Each Instagram card Total count not settled system card

There are reasons on both sides why the head count cannot be fixed to one value.

  • A system card is a public communication product, not a model dump. For Facebook, there are heads in the same Transparency Center text that are not on the card verified — facebook.md.
    • The Facebook Feed card lists 10 kinds. The same Transparency Center text separately states that hide/snooze/unsubscribe, report, and angry reaction predictions are used to lower distribution verified (same note).
  • For Instagram, there are 4 surfaces (Feed, Stories, Reels Chaining, Suggested Accounts) where the list extracted from the card does not match the count written on the card, so the total count could not be settled verified — instagram.md.
  • It cannot be determined whether the Threads card lists all heads or only the "significant predictions" inferred — threads.md.
  • The X numbers are also not fixed to one value. Within the Gen3 repo they are 19/21/22, and in earlier generations 13/15/17 appear.

The conclusion is the same on both sides. A single "head count" value does not exist. You must always state which file and which card you looked at.

Whether negative heads exist also differs within Meta.

  • Instagram published it explicitly as a term in the formula (– W_see_less·P(see less)) observed 2026-08-09.
  • Facebook stated it only in body text observed 2026-08-09.
  • The Threads card has no negative prediction head at all observed 2026-08-09 (1 ig-threads-feed card body).

X 2026 has 5 negative heads (not_interested/block/mute/report/not_dwelled) verified.

↑ Contents

Case: X — 6 design decisions

In X's public code, the head list and the way negative heads are mixed in differ by generation verified (design decision 2, design decision 3). In 2026 Rust, even after the weighted sum, the author-diversity decay and the OON (out-of-network, candidates from accounts the user does not follow) multiplier change the score, and VMRanker can overwrite that score verified (design decision 6). The 2026 Rust descriptions and line numbers in this section are as of the Gen3 release (2026-05-15), and the 24 heads in the Gen4 README and the deletion of the ranking scorer in the 2026-09-24 commit (Gen4+28) are in the x.md ranking section verified.

1. Weights are not in the code

X separated the weights into FeatureSwitch (runtime configuration) in all three generations verified. What is in the repo for 2025 Scala and through Gen3 are default values, and the production values were outside the repo verified. Since the Gen4 release (2026-08-13), the repo has a cron mirror that carries over the production defaults verified (x.md weights section).

Generation Parameter count Range Default in repo
2023 Scala 17 Varies: 9 are 0..100, 4 negative ones are negative-only (including Report −20000..0), GoodClick-type cap 1,000,000 10 are 1.0, 7 are 0.0
2025 Scala 30 Same structure as 2023 (Report −20000..0 identical) All 0.0
2026 Rust (Gen3 0bfc279) 22 Undisclosed (no params module at all) Undisclosed
2026 Rust (Gen4 47c1bcd) 26 (25 heads, 1 new-author boost) No range given Mirror of production defaults (e.g. favorite 0.5, reply 5.0, report −234.0)
  • The evidence for the 2023 row is ScoredTweetsParam.scala:151 (2023) verified.
  • The evidence for the 2025 row is home-mixer/.../param/HomeGlobalParams.scala:788-978 (2025) verified.
  • The evidence for the 2026 Gen3 row is home-mixer/scorers/ranking_scorer.rs:43-66 verified.
  • The evidence for the 2026 Gen4 row is home-mixer/params/param.rs verified.

⚠ An earlier version described 2023 as "0.0–100.0, negatives not expressible", but checking against the original text showed this was an error. 4 negative-only ranges have existed since 2023 ef4c5eb. It was corrected on 2026-08-09 (weights section of x.md).

In 2025, the defaults being all 0.0 does not mean "the like weight is 0." It means "the weights are all injected at runtime and are not in the repo." Misreading this leads to the opposite conclusion.

There are cases where the range itself states a policy. The parameter set for negative signals is locked to negative-only values verified. An operator cannot enter a positive value even by mistake.

  • ReportParam has min -20000.0 / max 0.0.
  • WeakNegativeFeedbackParam and StrongNegativeFeedbackParam have min -1000.0 / max 0.0.
  • These bounds have been the same since 2023 and were not introduced in 2025 verified. We compared HomeGlobalParams.scala:924-946 with 2023 72eda9a.

The widely cited X weight table (fav 0.5, retweet 1.0, reply 13.5, report −369.0 …) has a single source, the ML repo README as of 2023-04-05, and it has never appeared in any of the 31 commits of the serving repo. The source and reliability are covered in the "Weights — what was released and what was not" section of x.md.

2. Whether negative heads go into the multiplication

Whether to put negative heads into the multiplication is where the generations diverge most. X 2025 does not multiply and only adds a constant penalty when a threshold is exceeded verified. 2026 Rust changed the approach and multiplies negative heads in as they are. Instead, if the total score becomes negative, it compresses that whole score verified.

The X 2025 aggregation code multiplies positive weights into the score and adds them, and for negative weights it adds only the weight value when a threshold is exceeded verified — home-mixer/.../util/RerankerUtil.scala:106-121.

// positive weight
combinedScore + score * weight

// negative weight — only a constant penalty once the threshold is exceeded
normScore = if (maxHeadScore == 0.0) 0.0 else score / maxHeadScore
if (normScore > thresholdNormalized || score > thresholdConstant)
  combinedScore + weight     // does not multiply score
else combinedScore

The reason for not multiplying is not written in the code, so we leave it as an inference. Negative heads have small probabilities and large noise, so multiplying them in makes the score swing widely inferred. Looking only at whether the threshold was exceeded makes it a step function, which is stable inferred.

It matters that the normalization reference is a batch-relative value within the request. Even for the same post, whether the negative penalty fires changes if the composition of the candidates scored together changes verified — RerankerUtil.scala:43-65.

  • The normalization formula is score / maxHeadScore, and the denominator is the per-head maximum over all candidates in that request.

There are also signs of a transition. When EnableNegSectionRankingParam is turned on, the formula changes to combinedScore + weight * (1.0 min (score + 0.1)). Just above it is a comment telling people to ship and clean this up as soon as possible ("This should be shipped and cleaned as soon as possible") verified — RerankerUtil.scala:115-118.

Note that in the repo default state, negative heads produce no deduction at all. All 4 related booleans are false verified — HomeGlobalParams.scala:545-573.

  • The four booleans are NormalizedNegativeHead, ConstantNegativeHead, UseWeightForNegHeadParam, and EnableNegSectionRankingParam.

The function that compresses a negative total score in 2026 Rust is below verified — ranking_scorer.rs:175-183 (Gen3 0bfc279).

fn offset_score(combined_score: f64, w: &ScoringWeights) -> f64 {
    if w.total_sum == 0.0 { combined_score.max(0.0) }
    else if combined_score < 0.0 {
        (combined_score + w.negative_sum) / w.total_sum * NEGATIVE_SCORES_OFFSET
    } else { combined_score + NEGATIVE_SCORES_OFFSET }
}

This function is a direct port of the 2025 Scala function. The three-way branch corresponds line by line, in the same order verified — RerankerUtil.scala:130-135.

if (modelWeightsSum == 0) combinedScoreSum.max(0.0)
else if (combinedScoreSum < 0)
  (combinedScoreSum + negativeModelWeightsSum) / modelWeightsSum * Epsilon
else combinedScoreSum + Epsilon

The variables correspond as follows.

  • modelWeightsSum ↔ w.total_sum
  • negativeModelWeightsSum ↔ w.negative_sum
  • Epsilon ↔ NEGATIVE_SCORES_OFFSET

The constant values are also the same in the two generations. The corresponding 2025 constant is val Epsilon = 0.001 (RerankerUtil.scala:16), and NEGATIVE_SCORES_OFFSET, which was not public in Gen3, is confirmed as 0.001 in the Gen4 (47c1bcd) release verified — params/config.rs:40. The two repos are known to have "0 code duplication", but the score aggregation formula was ported verified.

In this formula, a negative total score is squashed to a 0.001 scale. Since the minimum of a positive candidate is 0 + 0.001 = 0.001, a negative candidate always ends up below every positive candidate. The design preserves order while keeping the two groups from mixing.

The same formula also remains in the file weighted_scorer.rs:87-89, which is no longer used. This file reads p::NEGATIVE_WEIGHTS_SUM / p::WEIGHTS_SUM as constants. In other words, it is likely the form from the generation before the structure changed to compute the weight sum per request inferred.

3. The head list differs by generation, and several versions coexist even within one repo

X's head list differs by generation and also by file within the same repo. In the Gen3 repo (2026-05-15), the head counts of the serving code, the public checkpoint, and the proto sent to VMRanker are all different: 22, 19, and 21 verified. In the 2025 repo, two kinds of rankers each have their own head set verified.

The 2026 Gen3 Rust ranker reads 22 heads verified — ranking_scorer.rs:12-39,146-170.

Group Heads
Engagement favorite, reply, retweet, quote, share, share_via_dm, share_via_copy_link
Click and dwell click, profile_click, photo_expand, quoted_click, dwell
Video vqv (video quality view), quoted_vqv
Continuous values cont_dwell_time, cont_click_dwell_time
Follow follow_author
Negative not_interested, block_author, mute_author, report, not_dwelled

The ACTIONS of the public Phoenix checkpoint has 19 entries, missing quoted_vqv, not_dwelled, and click_dwell_time verified — phoenix/runners.py:233-253.

That is, the head counts of the public model (19) and the production serving code (22) do not match. The public checkpoint appears to be an older version or to exclude some heads inferred. The fields that vm_ranker.rs copies into the proto number 21, missing quoted_vqv_score, so all three places differ verified — vm_ranker.rs:69-93.

This 19/22/21 mismatch is a fact through Gen3. The Gen4 README lists 24 heads verified (x.md Phoenix section).

In 2025 Scala, two kinds of rankers each have their own head set.

  • Navi (the production heavy ranker) uses 15 verified — home-mixer/.../model/PredictedScoreFeature.scala:294-311.
    • Heads: fav, reply, retweet, reply_engaged_by_author, click_engaged, click_dwell, good_profile_click, vqv, vqv_immersive, bookmark, share, dwell, video_quality_watched, video_watch_time_ms, negative_feedback_v2
  • Phoenix (the Transformer rescorer) uses 13. Its weights share the same ModelWeights as Navi, and there is no separate set verified — PhoenixPredictedScoreFeature.scala:174-189.
    • Its heads are the Navi list with a few removed and open_link and screenshot added.

Navi has only one negative head, negative_feedback_v2. The split into 5 in 2026 (not_interested/block/mute/report/not_dwelled) is a change between generations verified.

The 2025 repo has two files with the same name, PredictedScoreFeature, and their head sets differ. The one the actual scorer imports is the model/ one, and writing "X uses a report head" based on the scorer/ one would be wrong verified — the import path at RerankerUtil.scala:4.

  • The model/ one is a 15-element Seq and is actually used.
  • The product/scored_tweets/scorer/ one is a 17-element Set and includes Report, StrongNegativeFeedback, VideoPlayback50, and others.

4. The weight injection path may be per request

2025 Navi reads weights from a query feature (a feature attached per request), not from a param.

The code that reads the weights is the single line below verified — RerankerUtil.scala:28-29.

weight = query.features.get(feature.weightQueryFeature).getOrElse(0.0)

Each head has 3 such query feature names verified — PredictedScoreFeature.scala:43-56.

  • The three names are weightQueryFeatureName, biasQueryFeatureName, and debiasQueryFeatureName.

That is, the objective function can differ per request. It appears to be a structure that injects different weights per user segment and per experiment, but the upstream service that fills in the values is not in the repo inferred.

The Phoenix side of the same repo reads directly from a param (query.params(feature.modelWeightParam)). The two rankers use different injection paths verified — PhoenixModelRerankingScorer.scala:51.

5. An ineligible head is replaced by bias, not 0

X 2025 puts a bias value, not 0, in the slot of a head that cannot be used. The vqv head of a post without video is such a case.

The eligibility check code is the single line below verified — RerankerUtil.scala:34-37.

score = if (isEligible) predictedScoreOpt.getOrElse(0.0) else bias

Putting in 0 appears to be avoided because it would structurally disadvantage posts without video inferred.

The eligibility conditions are also hand-set values verified — PredictedScoreFeature.scala:154-164,220-232,273-279.

  • The video head carries a duration >= 10 seconds condition.
  • Turning on EnableDwellOrVQVParam makes Dwell and VQV mutually exclusive.

There is also threshold clipping. A VQV score below ScoreThresholdForVQVParam is clipped to 0.0, and turning on the binary scheme replaces it with a constant verified — :176-188.

6. Scores keep changing after the weighted sum

The weighted sum is not the end. As of the Gen3 release (2026-05-15), in 2026 Rust an offset, normalization, author-diversity decay, and an OON multiplier are applied in turn after the weighted sum, and then VMRanker can overwrite all scores verified.

  1. After computing the per-head weighted sum, apply offset_score (ranking_scorer.rs:146-183).
  2. Apply normalize_score(candidate, raw). In Gen3 the function body was not public (the crate::util module was missing). The ranking scorer in Gen4 (47c1bcd) does not call this function, and home-mixer/util/ is public verified — ranking_scorer.rs.
  3. Author-diversity decay: multiply the nth post of the same author by (1 - floor) · decay^n + floor (:186-217).
  4. OON multiplier: multiply only candidates that are not in-network by effective_oon (:220-239, 272-275).
  5. Then VMRanker, which optimizes the slate with a DPP (determinantal point process), can overwrite all scores (vm_ranker.rs:47-52).

We confirmed this order in ranking_scorer.rs:248-284 and phoenix_candidate_pipeline.rs:300 of Gen3 0bfc279 verified.

2025 Scala has a stage of the same nature, but its approach is additive. MMR (maximal marginal relevance) re-ranking adds the minimum distance to already selected candidates, weighted, to the relevance score, and overwrites ScoreFeature with that value verified — DiversityRescoringFeatureHydrator.scala:70,102.

  • The formula is score = relevance + diversityWeight · minDistance, and DiversityRescoringFeatureHydrator performs this computation.

The diversity layer is covered in detail in feed-diversity.md.

↑ Contents

Case: Netflix — leaves the head × weight frame itself

Netflix does not fit the X/Meta-style frame of multiplying per-head probabilities by weights and summing. Its published objective is a reward function, not a weighted sum. The X/Meta frame written as a formula is Σ w·P(action).

  • It uses a contextual bandit frame. The recommendation is the action, and the user response is the reward verified — Recommending for Long-Term Member Satisfaction at Netflix, 2024.
  • The north star (final goal metric) is retention, but it is not optimized directly. It is a sparse, noisy signal at the level of "1 time per account per month", so it cannot be attributed to individual recommendations verified (same post).
  • The published proxy reward has the form r(user, item) = f(play, complete, thumb). It is a function of observed actions, not a weighted sum of per-action probability predictions. The decomposition into a prediction head list and a weight vector does not appear at all verified (same post).
  • It supplements delayed feedback with prediction. A Delayed Feedback Prediction Model predicts p(final feedback | observed feedback) and feeds it into the reward verified (same post).
    • Thumbs can arrive several days after a view, which conflicts with real-time training, and this model addresses that problem head-on verified (same post).
    • The X and Meta literature has no corresponding mechanism.
  • It states the problem of CTR over-optimization explicitly: "CTR over-optimization encourages clickbait" verified (same post).
  • It approaches the problem as a reward engineering loop, not weight tuning verified (same post). Viewed as a "method" of setting weights, it is the third answer after Facebook (survey correlation) and Instagram (Bayesian opt).
    • The loop's order is hypothesis → reward revision → bandit policy training → A/B verified (same post).

However, a different frame does not mean there are no heads. FM (Netflix's personalization recommendation foundation model) has multi-token and multiple prediction heads verified — FM blog 2025. It also uses a weighted objective ("a 5-minute trailer and a 2-hour finish cannot be the same") verified (same post). The difference is that the weighting lives inside the training objective, and a serving-time Σ w·P combination layer is absent from the public literature. In GenRec (2026), an LLM trained with a reward-weighted loss ranks directly verified — GenRec blog. The combination is absorbed into the model parameters.

The % Match shown on screen is for display, not a ranking score. This value is computed "from viewing habits and behavior analysis alone" verified — 2017 talk. It is a case that explicitly shows a system where the number shown to users differs from the number used for sorting (netflix.md).

↑ Contents

What generalizes from this case

Putting several services side by side, the combination form differs by surface, the number of heads by source, and the way negative signals are handled by company.

  • "Weighted sum" is an assumption, not an observation. Meta alone has published 4 kinds by surface: addition, multiplication, addition with negative terms, and a multiplicative penalty. When looking at a new service, do not start by taking Σ w·P as the default assumption.
  • Even when "what the model predicts" is published, "what it treats as important" is not. Only the items disclosed differ; what is hidden is the same.
    • X has the head list in code and the weights in configuration. Through Gen3 those values were not in the repo, and since the Gen4 release the defaults are mirrored in the repo verified.
    • Meta has the head list on the card, the method of setting weights in blog posts, and the values nowhere.
  • The direction in which negative signals are handled is opposite across companies. X avoids multiplication (2025 constant penalty, 2026 negative total score compression), while Instagram 2020 multiplies to give a veto. Opposite designs for the same problem are both in production.
  • The number of heads depends on the kind of source. Source code disagrees by file, and system cards by document. To compare numbers, first ask whether the two numbers are of the same kind.
    • X differs by file (19/21/22/15/13/17).
    • Meta differs by document (10 on the card and negative predictions only in the body text).
  • The weighted-sum score is not the final ranking. Diversity, OON, and slate optimization follow.
    • In fact, X 2026 stores both weighted_score and score as separate fields verified — ranking_scorer.rs:277-281 (Gen3 0bfc279).
    • Meta also has a separate context pass 2 after the pointwise (scoring each candidate separately) pass 1 verified 2021.
  • The Σ w·P frame may be a practice of ads-style surfaces, not universal. Netflix solves the same problem (unifying multiple behavior signals) with a reward function and bandits. When the business model is subscription (retention), the objective also leans toward a long-term satisfaction proxy rather than maximizing session engagement. The choice of frame may be a function of the revenue model, not of technology inferred.
  • The reason the north star is not optimized directly is signal density. True goals such as retention that arrives 1 time a month or sparse surveys are pushed to the proxy reward and weight-decision layers, and the ranker handles dense behavior signals.
    • Facebook's survey correlation and Netflix's reward engineering are two implementations of the same structure.

↑ Contents

Who uses this

X, Facebook, and Instagram use this structure verified. Threads published 10 heads but has no information on the combination method or weights, and Netflix uses a reward function and bandits instead of a weighted sum verified.

  • X: All three generations, 2023, 2025, and 2026, use this structure. The number of heads differs by generation and file (Gen3 19–22, Gen4 README 24, Design decision 3). Weights are injected through FeatureSwitch and query feature verified — x.md.
  • Facebook: It uses the additive form Vijt = Σ wijtk·Yijtk. Pass 1 is a pointwise multi-task neural network, and weights are set by survey correlation. The Feed card has 10 heads, and there are more negative predictions only in the body text verified — facebook.md.
  • Instagram: It has 3 combination forms (2020 multiplicative, 2023 additive with negative terms, 2025 multiplicative penalty). Weights are set by offline replay/offline tuning and Bayesian optimization. It has a separate MTML model per surface × component (ig_stories_tray_mtml, more than 1,000 in production) verified — instagram.md.
  • Threads: It published 10 heads, but has 0 information on the combination method and weights, and 0 negative heads. This is the clearest evidence that the disclosure level differs by surface even within the same company verified — threads.md.
  • Netflix: Not of the Σ w·P form. It combines a proxy reward r=f(play, complete, thumb) and delayed feedback prediction in a bandit. The north star is retention verified — netflix.md.
  • Other services: Not yet investigated. YouTube's multi-gate MoE (mixture-of-experts, 2019 RecSys) and the Pinterest family are likely in the same category, but this repository has not yet confirmed them with a primary source. This is an open question.

↑ Contents

Sources

Public source code

  • xai-org/x-algorithm — 2026, public source code (Apache 2.0). home-mixer/scorers/, phoenix/. The reference commit for the 2026 Rust line number quotes in the body is 0bfc279 (Gen3), and Gen4 values were read at 47c1bcd.
  • twitter/the-algorithm — commit c54bec0 of 2025-09-03, public source code. home-mixer/.
  • twitter/the-algorithm-ml — 2023-04-05 projects/home/recap/README.md. The primary source of the fav 0.5/retweet 1.0/reply 13.5/report −369 table.

Meta engineering blog (primary source for the combination formulas)

Meta Transparency Center (primary documents for regulatory compliance)

Netflix

↑ Contents