RecSys · учебник
Тренажёр Виджеты Повторение О проекте Все главы Пайплайн →

Дополнение · X-алгоритм · страница 1 из 11

Открытый код ленты «For You»: что это и как читать

Теория объясняет, почему рекомендательные системы устроены именно так. Здесь — редкая возможность посмотреть, как это выглядит, когда написано всерьёз и работает на сотни миллионов пользователей. X открыл код своей главной ленты: 2028 файлов, около 360 тысяч строк на Rust, Python, Scala и Java.

Коротко

  • Это не игрушечный пример. В репозитории лежит продовый код: пайплайн ленты, модель ранжирования вместе с обучением, сервинг, фильтры, система видимости. Не всё, но ядро — настоящее.
  • Система собирается на каждый запрос из двух вещей: посты от тех, на кого вы подписаны, и посты, найденные моделью среди тех, на кого вы не подписаны. Ранжируются они одной и той же моделью.
  • Ранжирование и видимость разведены. Ранжирование решает порядок. Можно ли показывать пост вообще — решает отдельная служба по отдельным правилам. Это два разных сервиса с разными входами.
  • Модель предсказывает не «релевантность», а вероятности конкретных действий — лайк, ответ, репост, репорт, блокировка — и они сводятся в один скор явными весами, которые лежат прямо в коде.
  • Почти всё, что здесь есть, — знакомая теория. Двухбашенный retrieval, semantic IDs, хеш-эмбеддинги, мультизадачные головы, DPP, фильтр Блума, PageRank. Разница в том, что здесь видно, какой ценой это даётся.

Карта дополнения

Дорожка запроса — что происходит, пока лента собирается x01 Пайплайн стадии и запрос x02 Источники откуда посты x03 · x04 Модель Phoenix retrieval и ранжирование — ядро x05 Скоринг веса и поправки x06 Фильтры что не покажем x08 Блендинг реклама и прочее Дорожка разметки — идёт непрерывно, вне запроса x07 Разметка и видимость модели контента, репутация аккаунтов, правила лейблы x09 Конфигурация параметры и эксперименты x10 Чему учит этот код выводы о теории Порядок страниц повторяет путь запроса: от того, как пайплайн устроен, к тому, откуда берутся кандидаты, как их оценивает модель, как считается финальный скор и что происходит с постом уже после сортировки. Начинать можно с любой страницы — все термины объясняются на месте, а связи между главами проставлены.
Одиннадцать страниц дополнения и путь запроса через систему.

1. Что именно открыли

Формулировка «X открыл свой алгоритм» звучит расплывчато, поэтому сразу уточним границы. В репозитории лежит код, который определяет, какие посты вы видите в ленте «Для вас»: как собираются кандидаты, как они оцениваются, как сортируются, что из них выбрасывается и почему. Это не описание и не спецификация — это исходники сервисов, включая обучение модели ранжирования и её сервинг.

Что есть

Чего нет — и это важно понимать

Границы открытости
  • Промпты LLM-классификаторов (grox/) не опубликованы. То есть видно, что классификатор запускается и куда идёт его вердикт, но не видно, о чём именно спрашивают модель.
  • Часть правил разметки (botmaker-rules/) отсутствует — прямо сказано, что ради снижения риска обхода.
  • Веса моделей не выложены. Код обучения есть, чекпоинтов нет; чтобы что-то запустить, предлагается сгенерировать синтетический мир и обучить маленькую модель самому.
  • Инфраструктурная обвязка — деплой, оркестрация, внутренние клиенты — заменена на заглушки или отсутствует. Собрать это и поднять у себя целиком нельзя.
  • Данные, разумеется, тоже нет. А без логов взаимодействий рекомендательная система — это пустой скелет: вся ценность в том, что модель выучила, а не в том, как написан пайплайн.

Последний пункт стоит проговорить отдельно, потому что вокруг него больше всего путаницы. Открытый код рекомендательной системы не позволяет её воспроизвести. Он позволяет понять, какие решения приняты и какой ценой. Это ровно то, что нужно нам: принципы известны, здесь мы смотрим на их реализацию.

Как здесь цитируется код

Все куски кода на страницах дополнения приводятся с путём в репозитории и ссылкой на постоянный адрес коммита 28e414f. Код принадлежит X и распространяется под Apache License 2.0; фрагменты приводятся для разбора. Числовые константы в тексте не переписаны руками — они вытащены из исходников скриптом и сверяются автоматически, чтобы не разъезжаться с кодом.

2. Две дорожки: запроса и разметки

Первое, что нужно уложить в голове, — в системе две независимые дорожки, и они пересекаются ровно в одной точке.

Дорожка запроса работает, пока вы ждёте ленту. У неё жёсткий бюджет времени: собрать кандидатов, дотянуть к ним признаки, отфильтровать, посчитать модель, отсортировать, смешать с рекламой, отдать. Всё за десятки миллисекунд.

Дорожка разметки работает непрерывно и к вашему запросу отношения не имеет. Классификаторы разбирают опубликованные посты, батчевые джобы считают репутацию аккаунтов, движок правил навешивает лейблы. Результат кладётся в хранилище.

Пересекаются они в момент, когда посты уже отранжированы: сервис видимости читает эти лейблы и отвечает по каждому посту — показать, спрятать за заглушку или выбросить. Это ровно тот многостадийный дизайн, знакомый по теории воронки, только стадию «можно ли это показывать» здесь честно вынесли в отдельный сервис со своими правилами.

Почему их вообще разделили

Соблазн сделать наоборот велик: научить ранжирующую модель просто занижать плохое, и никакой отдельной службы не нужно. Так не делают по трём причинам.

  1. Разные требования к ошибкам. Ранжирование ошибается мягко: показали чуть менее интересный пост — потеряли немного вовлечения. Видимость ошибается жёстко: показали то, что показывать нельзя, — это уже другой класс проблемы. Смешивать величины с настолько разной ценой ошибки в одном скоре — плохая идея.
  2. Разная скорость изменений. Правила видимости меняются по требованиям закона и продукта, иногда за часы. Ранжирующую модель переобучают по своему расписанию. Связать их — значит, что любое изменение правил требует переобучения.
  3. Проверяемость. Правило «если у аккаунта такой лейбл и зритель на него не подписан — не показывать» можно прочитать, объяснить и оспорить. «Модель понизила скор» объяснить нельзя. Для системы, к которой есть претензии со стороны регуляторов и пользователей, это решающий аргумент.

Тот же довод мы приводили в главе «Сводка смещений», когда обсуждали, почему бизнес-правила не зашивают в лосс, а выносят в переранжирование.

3. Карта репозитория

2028 файлов легко пугают, но структура простая: один каталог — один сервис или одна библиотека. Вот что где лежит, по языкам и объёму.

ЯзыкФайловСтрокЧто на нём написано
Rust451140 106Всё, что на дорожке запроса: пайплайн ленты, хранилище свежих постов, переранжировщик, сервис видимости, сервинг модели
Python410103 315Модель Phoenix: определения, обучение на JAX, генерация синтетики, ядра внимания
Scala55680 987Батчевые джобы: SimClusters, репутация аккаунтов, агрегации лейблов
Java31329 421Движок правил разметки и его обвязка
CUDA и C++244 004Ядра внимания под конкретное поколение GPU

Выбор языков сам по себе поучителен и укладывается в то, о чём говорилось в главе «Архитектура рантайма». Rust — там, где на счету каждая миллисекунда и где сервис держит миллионы объектов в памяти. Python — там, где важна скорость итераций исследователя, а не скорость исполнения. Scala — там, где данные обрабатываются офлайн большими партиями. Это тот самый разрыв между онлайн- и офлайн-контурами, из которого растёт лямбда-архитектура.

Компоненты по ролям

КаталогРольГде разбираем
candidate-pipeline/Фреймворк стадий: источник, гидратор, фильтр, скорер, селектор, побочный эффектx01
home-mixer/Сама лента: какие стадии, в каком порядке, с какими параметрамиx01, x05, x06
thunder/Свежие посты подписок, разложенные в памятиx02
simclusters/Кластеризация аккаунтов и поиск кандидатов по кластерамx02
phoenix/Модель: двухбашенный retrieval и трансформер-ранкер, обучение и сервингx03, x04
phoenix-rankall/Индекс постов, который спрашивает retrievalx03
vm-ranker/Переранжировщик: отбор через детерминантный процесс по эмбеддингамx05
visibility-filtering/Показать, спрятать или выбросить — по правилам и лейбламx07
grox/, clip/, media-model-proxy/Понимание содержимого: классификаторы текста, изображений и видеоx07
agatha/, bdsm/, user-cred-v2/Репутация аккаунтов: по реакции других, по поведению, по графуx07
botmaker/, scarecrow/Язык правил разметки, его компилятор и исполнительx07
under-the-hood/Отчёт пользователю о лейблах на его аккаунтеx07

4. Пять решений, определяющих всю систему

В README они перечислены списком. Разберём каждое: что это значит, чем оплачено и что об этом говорит теория.

Решение 1. Предсказываем действия, а не релевантность

Модель не выдаёт одно число «насколько пост хорош». Она выдаёт вероятность каждого действия отдельно: лайкнет, ответит, репостнет, процитирует, поделится, кликнет, досмотрит видео, подпишется на автора, пожалуется, заблокирует, скроет. Сведение этих вероятностей в один скор — отдельный явный шаг с весами, лежащими в конфиге.

Это в точности мультизадачная постановка, доведённая до логического конца. Выгода двойная:

Цена — та самая, что у всякой мультизадачности: головы конкурируют за общее тело, редкие задачи переобучаются, а веса приходится подбирать экспериментами, потому что вывести их из первых принципов нельзя. Подробно с реальными числами — на странице x05.

Решение 2. Кандидаты не видят друг друга

Ранжирующая модель — трансформер, и на вход ему подаются сразу все кандидаты вместе с историей зрителя. Но маска внимания устроена так, что кандидат может смотреть на контекст зрителя, но не на другие кандидаты.

Зачем такое ограничение? Без него скор поста зависел бы от того, кто ещё попал в тот же батч. Последствия:

Здесь виден классический размен. Списочные модели, которые мы упоминали в главе «Списочные лоссы», как раз выигрывают от того, что видят весь слейт: они умеют учитывать взаимное влияние и разнообразие. X отказывается от этого выигрыша ради консистентности и кэшируемости — а разнообразие добирает после ранжирования, отдельным шагом переранжирования. Механику маски разбираем на x04, и там же есть виджет, где её можно выключить и посмотреть, что сломается.

Решение 3. Хеш-эмбеддинги вместо словаря

И retrieval, и ранжирование ищут эмбеддинги через несколько хеш-функций, а не через таблицу «идентификатор → строка». Словаря не существует в принципе.

Мы разбирали это в главе «Категориальные признаки и память»: хеш-трюк меняет память на коллизии, а несколько хеш-функций делают вероятность полной неразличимости произведением вероятностей. Но в ленте социальной сети у этого приёма есть второй, более важный смысл: новый пост становится представим немедленно. Не нужно ждать, пока его идентификатор попадёт в словарь, пока словарь пересоберут, пока модель переобучат. Пост опубликован — его хеш посчитан — эмбеддинг доступен.

Для системы, где половина контента живёт часы, это не оптимизация, а необходимое условие. Сравните с классической матричной факторизацией из главы «Матричная факторизация», где новый айтем без переобучения не получает вектора вовсе.

Решение 4. Ранжирование и видимость — разные системы

Разобрано выше в разделе про две дорожки. Добавим только следствие, которое легко упустить: фильтрация по видимости происходит после сортировки, а не до. Сначала посты ранжируются, потом отбирается топ, и только затем по каждому спрашивается разрешение.

Порядок неочевидный — казалось бы, дешевле сначала выбросить лишнее, а потом ранжировать меньше. Но обращение к сервису видимости стоит дорого и делается по паре «пост и зритель», поэтому выгоднее спросить про сотню отобранных, чем про тысячу кандидатов. Это тот же аргумент, что и в главе «Порядок фильтрации»: дешёвые фильтры ставят до ранжирования, дорогие — после.

Решение 5. Пайплайн собирается из типовых стадий

Вся лента описана в терминах шести типов стадий, и фреймворк сам запускает их, распараллеливая независимые и переживая падение отдельных. Добавить источник кандидатов или фильтр — значит написать одну структуру и вписать её в список.

Звучит как обычная инженерия, но именно это делает возможным всё остальное: тридцать фильтров, восемнадцать источников, эксперименты над каждой стадией по отдельности. Разбираем на x01.

5. Как это читать

Маршрут по репозиторию, если хочется полазить самому
  1. Начните с home-mixer/params/param.rs. Это самый информативный файл: 191 параметр, и по их именам видно, что вообще в системе настраивается.
  2. Дальше home-mixer/scorers/ranking_scorer.rs — арифметика финального скора целиком, включая все поправки.
  3. Затем home-mixer/candidate_pipeline/phoenix_candidate_pipeline.rs — список стадий по порядку. Это оглавление всей ленты.
  4. Потом visibility-filtering/rules/registry.rs — правила видимости в порядке применения.
  5. И только после этого phoenix/, если интересна модель. Он самый большой и самый специфичный.

Файлы mod.rs в Rust — это просто перечисление модулей каталога, в них ничего содержательного нет. Файлы *.thrift и *.proto — схемы данных: полезны, чтобы понять, какие поля вообще есть у поста и у запроса.

6. Словарь репозитория

Термины, которые встречаются в коде постоянно и без которых дальше читать тяжело.

ТерминЧто означает
Home MixerСервис, который собирает ленту «Для вас». Название историческое: он «смешивает» посты с остальными элементами ленты
Candidate (кандидат)Пост, попавший в рассмотрение. Кандидатом он остаётся до самого конца — пока не выбран или не отброшен
Hydration (гидратация)Дотягивание данных к объекту: к запросу — списки подписок и блокировок, к кандидату — текст, автор, счётчики. Термин из того же ряда, что обогащение признаками
In-network / Out-of-networkПосты от тех, на кого зритель подписан, и от тех, на кого не подписан. Сокращённо OON. Ключевое деление во всей системе
Slate (слейт)Набор постов, показанный за один раз. То же понятие, что в метриках ранжирования
Impression (показ)Факт того, что пост был показан зрителю. Хранится, чтобы не показывать дважды
Label (лейбл)Пометка на посте или аккаунте, поставленная системой разметки. Не путать с меткой класса в обучении
VFVisibility Filtering, сервис видимости. В коде встречается как префикс: VFFilter, VFCandidateHydrator
SIDSemantic ID — код поста из остаточного квантования. Разбираем на x03, теория — в главе «Generative retrieval и semantic IDs»
ParamНастраиваемое значение, читаемое из системы конфигурации. В коде объявляется макросом param!, дефолт синхронизируется с продовым
Side effectДействие после отправки ответа: записать показы, обновить кеш, отправить события. Не влияет на выдачу

Частые заблуждения

Что читают неправильно
  • «Веса действий показывают, во сколько раз репорт хуже лайка». Нет. Вес умножается на предсказанную вероятность действия, а не на количество действий. Разбор с числами — на x05, там же виджет, где это видно сразу.
  • «Открытый код позволяет накрутить ленту». Ранжирование персонализировано: реакция конкретных аккаунтов влияет в основном на выдачу тем, кто на них похож. К тому же значительная часть систем разметки намеренно не опубликована.
  • «Это весь алгоритм X». Это лента «Для вас». Поиск, уведомления, тренды, «Подписки» — другие системы, их здесь нет.
  • «Код в репозитории — это то, что крутится в проде прямо сейчас». Дефолты параметров синхронизируются с продовыми скриптами, но часть трафика всегда в экспериментах. Про это — x09.
  • «Раз код открыт, систему можно повторить». Без логов взаимодействий и обученных весов — нет. Ценность репозитория в том, что видны решения, а не в том, что его можно запустить.

Вопросы с собеседований

Зачем разделять ранжирование и фильтрацию по видимости? Почему не занизить скор?

Три причины. Разная цена ошибки: ранжирование ошибается на единицы процентов вовлечения, видимость — на репутации и требованиях закона; сводить их в один скор значит смешивать несопоставимые величины. Разная скорость изменений: правила видимости меняются за часы, модель переобучается по расписанию. Проверяемость: правило можно прочитать и оспорить, понижение скора моделью — нельзя.

Дополнительно: понижение скора не гарантирует, что пост не покажут. Если конкурентов мало, пост с заниженным скором всё равно окажется в выдаче. Жёсткое требование «не показывать» реализуется только жёстким фильтром.

Почему фильтрация по видимости стоит после ранжирования, а не до?

Потому что она дорогая и вызывается по паре «пост и зритель». До ранжирования кандидатов тысячи, после отбора — сотня. Спросить сотню дешевле в десять раз.

Общее правило то же, что в главе «Порядок фильтрации»: дешёвые фильтры, работающие по данным, которые уже есть в памяти (возраст поста, блокировки, уже показанное), ставят до ранжирования; дорогие, требующие похода в другой сервис, — после. При этом надо помнить, что фильтрация после отбора уменьшает выдачу, и на это закладывают запас: отбирают с избытком.

Что даёт предсказание отдельных действий вместо одного скора релевантности?

Развязывает модель и продуктовую политику. Веса действий меняются конфигом и раскатываются экспериментом, без переобучения. Плюс редкие негативные сигналы вроде репорта получают собственную голову и не растворяются в общем лоссе.

Цена: головы конкурируют за общее тело; редкие задачи склонны к переобучению; веса нельзя вывести теоретически, их подбирают A/B-тестами, и это долго. Подробнее — глава «Мультизадачность» и страница x05.

Почему кандидатам запрещено смотреть друг на друга в трансформере?

Чтобы скор поста не зависел от состава батча. Иначе одно и то же при одном и том же зрителе даёт разные числа, скор нельзя кэшировать, результат невоспроизводим, а отладка становится невозможной.

Обратная сторона: модель не видит слейт целиком и не может учитывать взаимное влияние постов — например, что три поста подряд про одно и то же. Эту задачу решают отдельно, переранжированием после скоринга. То есть listwise-эффекты не потеряны, а вынесены в отдельный шаг, где их проще контролировать.

Зачем хеш-эмбеддинги, если можно вести словарь идентификаторов?

Два довода. Память: словарь на все посты и всех авторов не помещается, а хеш-таблица имеет фиксированный размер. Свежесть: новый пост представим сразу после публикации, потому что его хеш считается на месте, а словарь пришлось бы пересобирать.

Плата — коллизии. Опасны не любые, а коллизии двух частых значений: их эмбеддинги склеиваются. Лечится несколькими хеш-функциями — тогда неразличимость требует совпадения во всех таблицах сразу. Подробно с интерактивом — глава «Категориальные признаки и память».

Что важнее для понимания системы: код пайплайна или веса модели?

Вопрос с подвохом, и правильный ответ — «ни то, ни другое по отдельности». Код пайплайна показывает, какие решения приняты и где рычаги. Веса модели показывают, что система выучила из данных. Открыт только первый, и это принципиальное ограничение прозрачности: можно проверить, что правило существует и как оно написано, но нельзя проверить, чему модель научилась на логах.

Именно поэтому к открытию кода прилагается инструмент, показывающий пользователю лейблы на его аккаунте: код плюс наблюдаемые выходы дают больше, чем один код.

Шпаргалка одним экраном

Что открыто

Пайплайн ленты, модель с обучением и сервингом, веса сведения действий, правила видимости, системы разметки.

Что закрыто

Промпты классификаторов, часть правил разметки, веса моделей, данные, деплой.

Две дорожки

Запроса — собирает ленту за десятки миллисекунд. Разметки — идёт непрерывно и кладёт лейблы в хранилище.

Точка стыка

После сортировки: сервис видимости отвечает по каждому посту — показать, спрятать за заглушку или выбросить.

Мультизадачность

Модель предсказывает вероятности действий; сведение в скор — отдельный шаг с весами из конфига.

Изоляция кандидатов

В маске внимания кандидаты не видят друг друга: скор консистентен и кэшируем.

Хеш-эмбеддинги

Словаря нет, несколько хеш-функций. Новый пост представим сразу.

Языки

Rust — онлайн, Python — модель, Scala — офлайн-джобы, Java — движок правил.

С чего читать

param.rsranking_scorer.rs → список стадий пайплайна → реестр правил видимости.

Первоисточники