RecSys · textbook
Trainer Widgets Revision About All chapters ← Configuration Overview

Supplement · The X algorithm · page 11 of 11

What this code teaches about the theory

We have gone through the system along the path of a request. Now let us put it back together — by topic. For each one: what the theory says about it, how it is done in production and, most importantly, where practice diverges from theory and why. The divergences are more interesting than the matches.

In brief

  • Almost all of the theory turned up in live code — two-tower retrieval, semantic IDs, hash embeddings, multi-task learning, DPP, a Bloom filter, PageRank, smoothed ratios.
  • The main divergence is about embeddings. The course teaches representing a user by a learnable vector; here there is none at all.
  • The second is about listwise ranking. The course says seeing the whole slate is useful; here that is deliberately forbidden for the sake of cacheability.
  • The third is about LogQ. Theory gives a correction scale of 1, production has 2.
  • What is not in textbooks at all: the safety loop, the report to the user, the synchronisation of the configuration with the open code.

1. Topic by topic

TopicHow it is in XWhere we discuss it
The multi-stage funnelSources → hydration → filters → scoring → a selection of 50 → filters → 35 to the screenx01, x06
MIPS and similarity measuresThe dot product of normalised vectors, that is, the cosinex03
Matrix factorisationNot used for the feed; its role was taken by a two-tower transformer. A related idea lives in SimClusters — as sparse binary factorisation of the graphx02
Kinds of candidate generatorsFive sources with different blind spots: the memory of follows, retrieval, clusters, topics, the cachex02
Recall@K of candidate generationNot explicitly measured in the open code; instead, source limits as a product decisionx02
Ranking metricsThe training code has NDCG, AUC, calibration, relative cross-entropyx04
The problems of implicit dataA positive counts only in the absence of a negative. Clickbait is cut off by the «did not dwell» labelx03, x04
MMR and DPPDPP over embeddings in a separate service, θ = 0.65, the first 150 positions are rearrangedx05
Biases and the feedback loopAn over-corrected LogQ, the out-of-network discount, the boost for a little-known authorx03, x05
Two-tower modelsExactly so, including the normalisation and the cosine. A corpus of 28.7 mln, the index inside the checkpointx03
Sampling negativesIn-batch plus 64 global per example, each kind with its own correctionx03
The LogQ correctionPresent, with a scale of 2.0 instead of the theoretical onex03
Learnable embeddingsAbsent for the user and for the post alike. Only the history and the semantic IDx03
Inductive bias, cold startThe model is inductive by construction; plus in 10% of the examples the history is zeroed out deliberatelyx03
Hashing categoriesTwo hash functions per entity, a shared table with zone offsetsx03
Unified EmbeddingThat is exactly what it is: indistinguishability requires a collision in both tablesx03
Multi-task learningA head per action, combined by weights outside the modelx05
ESMM and sample selection biasA loss mask: the conversion heads learn only on clicked postsx04
Position debiasingNo explicit correction is visible in the open code; there are, however, the out-of-network discount and the decay on the authorx05
Two loops, the lambda architectureOnline: the memory of follows from Kafka. Offline: the clusters and the reputation once a weekx02, x07
Logging and its gapsSide effects in the background with no check of the result — hence the fourfold guard against repeatsx01, x08
The architecture of the runtimeRust on the request track, Python for the model, Scala for the batchesx00
The order of filtering18 cheap ones before scoring, 3 expensive ones after the selectionx06
The Bloom filterArrives with the request as a hydrator, alongside three more mechanismsx01, x06
ResilienceSource errors are swallowed, the feed is assembled from the survivorsx01
Transformers over historyA sequence of profile + 1022 events + 64 candidates, 8 layers, a width of 2560x04
Bandits and Thompson samplingPresent in the newcomer boost with a prior of Beta(0.75, 49.25), but off by defaultx05
Experiments191 parameters, a switch on every stage, the history of a change day by dayx09
Layers with different goalsAds and modules are a separate pipe, rules instead of a common scorex08
Semantic IDsResidual quantisation of 6 levels by 256 codes on top of a multimodal embeddingx03

2. Five places where practice diverges from theory

Matches are useful but teach nothing. Let us take the divergences — in them you can see how practice differs from an exposition.

Divergence 1. The user has no embedding

The course says: represent the user by a learnable vector, the item by a learnable vector, and the score is their product.

In production: neither of the two. The user is the output of a transformer over their history, the post is a set of codes from residual quantisation.

Why they diverged

The course starts from matrix factorisation, where learnable vectors are the very essence of the method. Production starts from the properties of the domain: content lives for hours, new users arrive every day, the catalogue does not fit into a table.

Both are right within their frames. A learnable embedding is better when the objects are long-lived and there are not many of them — a catalogue of films, categories of goods. It loses when the objects are ephemeral: by the time the table is updated the post is no longer relevant.

What to take away: the question «a learnable vector or a computed representation» is decided not by theory but by the lifetime of the object relative to the retraining period. If the object lives for less, a learnable vector is useless.

Divergence 2. Listwise ranking is forbidden deliberately

The course says: listwise approaches see the whole slate and are therefore stronger than pointwise ones.

In production: the candidates physically do not see each other — the attention mask forbids it.

Why they diverged

The course compares by quality all else being equal. Production takes into account what is not in that comparison: cacheability, reproducibility and explainability.

Note that the listwise effects are not lost — they are moved into a separate re-ranking step. That is, what was chosen is not «pointwise instead of listwise» but a division: the model answers for the «user and post» pair, a separate service for how the posts look together.

What to take away: an architectural decision has dimensions besides quality. A score that depends on the composition of the batch cannot be cached — and the cache here became a full-fledged source of candidates.

Divergence 3. LogQ with a scale of 2

The course says: subtract \(\log Q\) and get an unbiased estimate.

In production: \(2\log Q\) is subtracted.

Why they diverged

Unbiasedness is achieved with respect to the distribution that produced the log. And the log was produced by a previous version of the system, which itself preferred the popular. A model unbiased with respect to the log conscientiously reproduces the inherited skew.

What to take away: an «unbiased estimate» is always unbiased with respect to something. In recommendations that is «with respect to what we showed yesterday», and if yesterday's policy was biased, formal correctness does not save you.

Divergence 4. Diversity is brought in twice

The course says: for diversity, take MMR or DPP.

In production: both, but along different axes — the decay on a repeated author separately, the DPP over embeddings separately.

Why they diverged

The course discusses diversity as one notion. In the feed of a social network there are two: not getting stuck on one person and not getting stuck on one topic. These are different problems with different mechanisms: the first is solved by a cheap multiplier right in the scoring, the second by an expensive determinant in a separate service.

What to take away: before choosing a method of diversity, the axis has to be named. «A diverse output» is not a statement of the task.

Divergence 5. The newcomer boost is not a multiplier

The course says: cold start is cured by priority — blending in, a boost, exploration.

In production: exactly one suitable post is taken and its score is set equal to the score of position 15–16.

Why they diverged

A multiplier gives an unpredictable result: where the post ends up depends on who else is in the output. Setting it equal to the score of a target position gives a guarantee of a place rather than a guarantee of an increment. The product promise is formulated as «one position in the middle of the feed», and the code expresses exactly that.

Plus «exactly one» is an upper bound on the price of the mechanism: however many newcomers there are, the feed loses one position, not ten.

What to take away: when a requirement is formulated in positions, implement it in positions, not in multipliers on the score.

3. What is not in textbooks at all

WhatWhy it matters
The safety loop — classifiers, account reputation, the rules engine, three kinds of answerBy volume of code it is comparable with the recommender core. In any public system it is half the work, and interviewers ask about it
The report to the user about labelsTransparency as part of the product rather than as documentation. Technically it rests on preserving the reason for the verdict
Synchronising the configuration with the open codeOpening the code and not opening the values is almost meaningless. A separate mechanism solves that
Measuring the value of inventory by a holdout on a «user and object» pairCausal measurement with a unit of randomisation other than the user
Limiting ads by the quality of the outputAn economic loop built into the code: bad content reduces the advertising inventory

Interview questions about the system as a whole

Questions to which this system gives a ready and concrete answer. The value is that instead of «they probably do something like this» one can say «in the open code of X it is done like this, and here is why».

Design the feed of a social network. Where do you start?

With two separations that determine everything else.

First: ranking apart from visibility. The order is decided by the model, the right to be shown by a separate service with rules. A different cost of error, a different speed of change, a different auditability. And most importantly: lowering a score does not guarantee that a post will not be shown.

Second: posts apart from non-posts. Ads, blocks of account recommendations and promos do not take part in the common ranking — they have no common unit of measurement with the score of a post, and their share is a business decision.

After that the standard funnel: several sources with different blind spots → hydration → cheap filters → the model → a selection with a margin → expensive filters → blending. And side effects after the answer.

How do you represent a user and an item if content lives for hours?

Not by learnable vectors. The user is the output of a transformer over the sequence of their actions: that works from the very first action and updates in real time. The item is a code derived from the content, for example residual quantisation of a multimodal embedding: a new post gets a representation right after publication, and posts close in meaning share a prefix of the code, so knowledge transfers.

The general rule: a learnable vector is justified when the object lives longer than the retraining period. If it lives for less, it does not manage to be learned and is useless.

The model predicts a dozen different actions. How do you combine them into one score?

By a weighted sum with the weights in the configuration rather than in the loss. That decouples the model from the product policy: changing the priorities becomes changing a number and rolling out an experiment rather than retraining.

Three things worth saying separately. The weights are multiplied by probabilities, not by counters, so no conclusion about influence can be drawn from the ratio of the weights. The result is worth bringing into the non-negative range if it is afterwards multiplied by corrections smaller than one. And the weights cannot be derived theoretically — only by experiment, which is the main price of the scheme.

How do you keep one author from taking over the whole feed?

By a soft multiplier with a floor rather than by a quota: \(m(k) = (1-\text{floor})\cdot\text{decay}^k + \text{floor}\), where \(k\) is how many posts by the same author already stand higher. At the values 0.5 and 0.25 that gives 1.0 → 0.625 → 0.438 → 0.344 with a floor of 0.25.

The advantage over a quota: it is a price, not a ban. A good enough fifth post by an author will get through if it overtakes other people's even with a coefficient of 0.25. The floor is needed so that the author does not disappear entirely.

And it is worth saying separately that this is only one axis of diversity. Uniformity by topic is solved by a different mechanism — selection through a determinantal process after the scoring.

How do you support new authors without breaking the feed?

Give exactly one of their posts a guaranteed position in the middle of the output rather than a multiplier on all of them. The implementation: out of the suitable posts (few impressions, few followers for the author, fresh) the best by score is taken, and its score is set equal to the score of position 15–16.

Two properties make that manageable. A guarantee of a position rather than of an increment: the result is predictable and does not depend on who else is in the output. Exactly one post: the price of the mechanism is bounded above by one position regardless of the number of newcomers.

In substance this is exploration: we spend a position to get a signal about a post there is no data on. Alongside in the code lies a Bayesian variant of the choice through Thompson sampling with a prior of Beta(0.75, 49.25), switched off by default.

A user complains that they see the same post twice. Where do you look?

You have to start from the fact that the recording of impressions almost certainly happens after the answer has been sent, in the background, and its result is not checked. So the impression may not have been recorded: a restart of the service, an overflowing queue, a failure of the store.

Hence the practice: several independent mechanisms with different data paths. In the system we looked at there are four — a compact filter arriving with the request, two journals of impressions from different stores and a session list. Plus the list of what has been shown is passed into the source of posts from follows.

The diagnosis accordingly: compare what was recorded in each of the journals and look for a discrepancy rather than looking for a bug in the filter.

How do you measure how much value a particular type of content brings?

Only by experiment, not by observation: by removing a type of content you free up positions that other posts will take, and part of the engagement will flow there. The observed share over-states the contribution.

The mechanism: deterministically hide a set percentage of such posts and compare. Two important details — the decision has to depend on a hash of the «object and user» pair in order to be stable between requests, and it has to be personal so as not to distort the object's overall statistics.

The unit of randomisation here is a «user and object» pair rather than a user. That makes it possible to measure the value of inventory rather than of functionality.

The metrics went up but users are complaining. What do you do?

Admit that the metric measures something other than what the product exists for, and look for which function of the product degraded.

A concrete case from this repository: strengthening the weight of a reply for mutual follows gave good engagement metrics — people were talking with people they know more actively. But the complaints were about something else: during a major sporting event the feed showed little discussion, because the relevant posts were written by accounts they do not follow, and strengthening one group automatically weakened the others. The value was lowered from 20 to 15.

The general conclusion: strengthening any group is a weakening of all the others, and it becomes noticeable where there are no metrics. It is useful to keep metrics of coverage and diversity alongside metrics of engagement — those are exactly what catch such shifts.

What would you do differently from the way it is done in X?

A question about maturity of judgement, and answering «everything is right» is bad. Three substantive directions.

Silent degradation. Source errors are swallowed silently. That is the right decision in substance, but it requires metrics and alerts on every source — otherwise a failure on a couple of percent of the traffic will not be discovered. The open code shows that measurements exist; whether such failures are visible through them cannot be judged from the repository.

The reliability of the training logs. The events for future models are written by the same unreliable background means as everything else. Losing part of the logs biases the sample and is not diagnosed by online metrics. Here I would separate logging for the product from logging for training, giving the second delivery guarantees.

The absence of explicit position debiasing. The model is trained on the logs of its own output, where the upper positions get more attention simply because of position. No explicit correction is visible in the open code — perhaps it is in the unpublished part, but if not, that is a noticeable source of bias.

One-screen cheat sheet

Matched the theory

Two towers, semantic IDs, hash embeddings, multi-task learning, DPP, Bloom, PageRank.

Diverged: embeddings

There are no learnable vectors for the user or the post. The reason is the lifetime of the object.

Diverged: listwise

Forbidden deliberately for cacheability; moved into a separate re-ranking step.

Diverged: LogQ

A scale of 2 instead of 1 — against the bias inherited from the logging policy.

Diverged: diversity

Two axes, two mechanisms: by author with a multiplier, by topic with a determinant.

Diverged: cold start

A guaranteed position for one post instead of a multiplier for all.

Not in the theory

The safety loop, transparency as a product, the synchronisation of the configuration.

The main lesson

Architectural decisions are chosen not by quality alone: cacheability and explainability weigh no less.

The second lesson

Strengthening any group is a weakening of the others, and it shows up where there are no metrics.

Primary sources