리서치 / 교차 서비스 topics
후보 배분 — 소스가 여럿일 때 무엇을 몇 개 넣는가
후보 배분은 후보 생성 소스가 10개, 30개씩 있을 때 "각 소스에서 몇 개를 가져와 최종 리스트에 어떻게 섞을 것인가"를 정하는 레이어다. 방식은 세 가지이고, X는 세대가 바뀌면서 방식을 바꿨다. 2025 Scala 세대는 상한과 위브, 우선순위 병합을 썼다 확인(세대별 배분 구조). 2026 Rust 세대는 배분기 없이 점수 배수를 쓴다 확인(2026 Rust, 50:50 절).
- 비율 배분 (ratio): "이 소스에서 30%" 식으로 슬롯을 나눈다. 결과 구성이 보장된다.
- 상한 배분 (cap): 소스마다 최대 N개만 가져오고, 그 뒤로는 점수로만 경쟁시킨다. 구성은 보장되지 않는다.
- 인터리브 (round-robin / weave): 소스를 번갈아 하나씩 뽑는다. 상한과 조합해서 쓴다.
3세대 공개까지 X와 Meta는 이 레이어를 정확히 반대 방향으로 공개했다.
- X는 배분기 코드를 전부 공개했다. 상한 값은 3세대(
0bfc279)까지 레포 밖에 있었고, 4세대 공개(2026-08-13,47c1bcd)부터는 파라미터 미러에서 읽힌다 확인 —home-mixer/params/param.rs. - Meta는 단계별 후보 수를 지면마다 숫자로 밝히지만, 그 숫자를 만드는 배분기는 어디에도 공개하지 않았다.
이 비대칭은 노트 전체에서 되풀이되는 주제다.
"X는 인네트워크:아웃오브네트워크를 50:50으로 섞는다"는 틀렸다
X가 인네트워크(팔로우한 계정의 글)와 아웃오브네트워크를 반씩 섞는다는 말은 가장 널리 퍼진 오해다. 이 말의 근거로 쓰이는 두 문장은 둘 다 관측된 결과 비율("on average")이지 코드에 있는 쿼터가 아니다. 확인 — Twitter 블로그 2023-03-31(아카이브 대조 2026-08-09), twitter/the-algorithm README.md:41
2023년 공식 블로그는 이 비율이 평균이고 유저마다 다를 수 있다고 썼다 확인 — Twitter 블로그.
"the For You timeline consists of 50% In-Network Tweets and 50% Out-of-Network Tweets on average, though this may vary from user to user"
README는 검색 인덱스 행에서 트윗의 약 절반이 이 후보 소스에서 온다고 쓴다 확인 — 같은 README.
"~50% of Tweets come from this candidate source"
2026 세대 코드에는 인네트워크와 OON(out-of-network, 팔로우하지 않는 계정의 글)의 비율을 강제하는 코드가 없다.
- 3세대(
0bfc279) 기준으로 home-mixer(For You 피드를 조립하는 서비스)에서는 소스 11개가 병렬로 돌고, 그 결과를 그냥 이어 붙인다 확인 —home-mixer/candidate_pipeline/phoenix_candidate_pipeline.rs- 4세대 공개 기준 For You 소스는 12개다 확인 — x.md 후보 생성 현행 절.
- 최종 선택은 점수 기준 top-K 하나뿐이다 확인 —
home-mixer/selectors/top_k_score_selector.rs - OON 비중을 조절하는 수단은 쿼터가 아니라 점수 배수다 확인 —
home-mixer/scorers/ranking_scorer.rs:220-239,272-275in-network가 아닌 후보에만
effective_oon을 곱한다.let final_score = match c.in_network { Some(false) => after_diversity * effective_oon, _ => after_diversity, };
배수 값은 세 갈래로 갈린다 확인 — ranking_scorer.rs:220-239. 3세대(0bfc279)에는 세 값이 모두 레포에 없었고, 4세대(47c1bcd)의 미러에는 세 값이 모두 있다 확인 — params/param.rs, params/config.rs:38.
- 토픽 요청이면
TopicOonWeightFactor를 쓴다. 4세대 공개 기준 값은 0.5다. - 신규 유저면
NEW_USER_OON_WEIGHT_FACTOR를 쓴다. 신규 유저는 계정 나이가NewUserAgeThresholdSecs보다 작고 동시에 팔로잉 수가NEW_USER_MIN_FOLLOWING이상인 유저다. 4세대 공개 기준 값은 0.00001이다. - 나머지는
OonWeightFactor를 쓴다. 4세대 공개 기준 값은 0.75다.
즉 "OON을 얼마나 넣을지"는 배분 문제가 아니라 랭킹 문제로 옮겨 갔다. 결과 비율은 배수와 후보 풀에 따라 요청마다 달라진다.
세대별 배분 구조 — X
X의 배분 구조는 세대마다 다르다. 2025 Scala 세대는 상한과 인터리브, 파이프라인 순서에 따른 우선순위 병합을 함께 썼다 확인(2025 Scala). 2026 Rust 세대에는 배분기가 없고, 소스마다 상한이 있으며 전역 선택은 점수 순 하나뿐이다 확인(2026 Rust). 두 세대 모두 캐시된 후보가 충분하면 비싼 소스를 돌리지 않는다 확인(2026 Rust, home-mixer 레이어).
2026 Rust: 배분기 없음, 소스별 상한만
3세대 공개(2026-05-15) 코드 기준으로 2026 세대에는 소스 사이의 몫을 나누는 배분기가 없다. 소스마다 상한이 있고 전역 선택은 점수 순 하나뿐이며, 사실상 배분기 역할은 캐시 게이트가 한다 확인.
- 3세대(
0bfc279) 기준 소스는 11개다 확인 —home-mixer/sources/thunder_source/phoenix_source/phoenix_moe_source/phoenix_topics_source/tweet_mixer_source/cached_posts_source/scored_posts_source/ads_source/who_to_follow_source/prompts_source/push_to_home_source
- 각 소스는 자기 상한을 FeatureSwitch(런타임 설정)에서 읽는다. 3세대에는 그 값이 레포 밖에 있었다 확인 —
thunder_source.rs:38,phoenix_source.rs:89,tweet_mixer_source.rs:61- 상한 이름은
ThunderMaxResults,PhoenixMaxResults,TweetMixerMaxResults다. - 4세대(
47c1bcd) 미러의 기본값은 각각 1200, 1000, 800이다 확인 —params/param.rs.
- 상한 이름은
- 전역 선택은 점수 내림차순으로 상위 K개를 고르는 단계 하나뿐이다 확인 —
top_k_score_selector.rs:13- 선택기는
TopKScoreSelector이고,TOP_K_CANDIDATES_TO_SELECT개를 고른다. - 이 상수도
params모듈에 있다. 3세대에는params모듈이 공개되지 않았고, 4세대(47c1bcd)에서 공개된 값은 50이다 확인 —params/config.rs:17.
- 선택기는
- 캐시 게이트가 사실상 배분기 역할을 한다. 캐시된 포스트가 임계치 이상이면 모든 소스와 Phoenix(X For You의 랭킹 모델) 자체가 조용히 꺼진다 확인.
- 즉 "새 후보를 얼마나 가져올지"의 1차 결정은 소스별 상한이 아니라 캐시 상태다.
2025 Scala: 상한 + 인터리브 + 우선순위 위브
2025 세대는 훨씬 복잡하고, 배분이 두 레이어에서 일어난다. OON 후보를 만드는 tweet-mixer는 저신호 유저에게만 비율 배분을 쓰고, 나머지 유저에게는 블렌더와 상한을 쓴다 확인(tweet-mixer 레이어). 피드를 조립하는 home-mixer는 파이프라인 순서를 우선순위로 쓴다 확인(home-mixer 레이어).
tweet-mixer 레이어
tweet-mixer는 OON 후보 생성을 전담한다. Home 요청에는 candidate pipeline이 34개 연결되어 있다 확인 — HomeRecommendedTweetsRecommendationPipelineConfig.scala:271-332 (엔트리 :274-331)
배분은 다섯 단계로 진행된다 확인 :362-412.
- 저신호 유저(팔로우 그래프가 작은 유저)면 여기서 끝난다. 작성자당 1개로 dedup하고, 명시적 비율 배분을 한 뒤, 이후 selector를 전부 막는다 확인.
- dedup은
DropDuplicateCandidates(duplicationKey = _.features.getOrElse(AuthorIdFeature, None))가 한다:364. - 비율 배분은
InsertAppendRatioResults가 한다. - 이후 selector를 막는 것은
DropAllCandidates()다:380. - 블록 전체는
AllowLowSignalUserGate.evaluate로 게이트된다:382. - 비율의 default 값은 Grok 토픽 0.7, SimClusters interested-in 0.15, 인기 지역 0.15, 나머지 전부 0.01이다 확인
TweetMixerGlobalParams.scala:225,233,241,249. - '나머지'에는
filterNot으로 일괄 적용한다:372-378. 이 값들은 default 값이고 프로덕션 값은 아니다.
- dedup은
- 저신호 유저가 아니면 블렌더 하나를 골라 실행한다. Home 파이프라인에 연결된 블렌더는 3개이고,
BlendingParam이 그중 하나를 고른다 확인:399,401,402.exclusiveRoundRobinSelector는(소스, 시그널)버킷으로 위브한다.signalPrioritySelector는 버킷 기준에USSFeatures.getPriority를 추가한다.weightedSignalPrioritySelector는 가중치를 적용해 위브한다.new Random(0)으로 시드를 고정한다 확인TweetMixerFunctionalComponents.scala:289.- 다만 블렌더가 이것뿐인 것은 아니다.
TweetMixerFunctionalComponents.scala에는 selector가 훨씬 많이 정의돼 있고,BlendingEnum은 값이 6개다 확인TweetMixerGlobalParams.scala:23-30.- 정의된 selector에는
roundRobinSelector:210,inclusiveRoundRobinSelector:237등이 있다. - "블렌더는 3종"이 아니라 "이 지면이 3개를 배선했다"가 맞다.
- 정의된 selector에는
- 라이트랭커 상한으로 결과를 자른다. default 값은 500이다 확인
HomeRecommendedTweetsParam.scala:9-15.- 자르는 단계는
DropMaxResults(LightRankerMaxResultsParam)이다:404.
- 자르는 단계는
- 그다음에 u2i(user→post) 파이프라인 7개를 라운드로빈으로 덧붙인다 확인.
- 파이프라인은
:385-393에 있고, 덧붙이는 selector는inclusiveRoundRobinSelector다:405-406.
- 파이프라인은
- 마지막으로 요청 수와 서버 상한 가운데 작은 값으로 자른다 확인
:408-411.- 이 단계는
DropRequestedMaxResults이고,min(requestedMaxResults, serverMaxResults)를 적용한다. - 두 값의 파라미터 이름은
DefaultRequestedMaxResultsParam(300)과ServerMaxResultsParam(500)이다 확인HomeRecommendedTweetsParam.scala:18-24,27-33.
- 이 단계는
4번은 특혜처럼 보이지만 특혜가 아니다. "상한을 자른 뒤에 붙으니 슬롯 경쟁을 면제받는다"고 읽기 쉽지만, 두 가지 사실이 그 결론을 뒤집는다 확인.
- 3단계의 절단은 결과 리스트만 자르고, 남은 후보는 버리지 않는다 확인
DropMaxResults.scala:36-38. 그래서 뒤에 붙는 u2i 후보는 리스트의 맨 뒤에 놓인다.- 코드로는
DropMaxResults가result만 자르고remainingCandidates는 그대로 둔다.
- 코드로는
- home-mixer가 실제로 요청하는 수는 400이다 확인. 라이트랭커 상한 500보다 작다.
- 근거 경로는
ScoredTweetsParam.scala:176(default 400) →ScoredTweetsTweetMixerCandidatePipelineConfig.scala:77→HomeRecommendedTweetsProductPipelineConfig.scala:73이다.
- 근거 경로는
즉 5단계의 최종 절단선(400)이 3단계의 상한(500)보다 앞에 있으므로, 비-u2i 블렌드만으로 400개가 채워지면 뒤에 붙은 u2i는 한 개도 안 남는다. u2i는 500 슬롯을 놓고 경쟁하지 않는 대신 400에서 가장 먼저 잘리는 쪽이다. 순서상 뒤에 붙는 것은 우대가 아니라 후순위다.
저신호 유저 판정에는 코드와 주석이 어긋나는 곳이 있다. 게이트 주석은 "< 25 users"라고 쓰지만, 코드는 SmallFollowGraphSize = 5다. 이 불일치는 tweet-mixer와 home-mixer 양쪽에 똑같이 복제돼 있다. 확인
- tweet-mixer:
SignalUtils.scala:17,20vsAllowLowSignalUserGate.scala:12·DenyLowSignalUserGate.scala:12 - home-mixer:
util/SignalUtil.scala:18vsscored_tweets/gate/DenyLowSignalUserGate.scala:13
주석을 인용하면 임계값이 5배 틀린다. 이 임계값은 "명시적 비율 배분으로 내려갈지"를 가르는 스위치이므로, 5냐 25냐에 따라 영향받는 유저 규모가 크게 달라진다.
home-mixer 레이어
home-mixer에는 candidate pipeline이 9개 있고, 그 순서 자체가 배분 규칙이다 확인. 목록과 순서는 ScoredTweetsRecommendationPipelineConfig.scala:446,451-459에 있다.
Static → CachedScoredTweets → EarlybirdInNetwork → DirectUteg → TweetMixer
→ ContentExploration → Lists → Backfill → Communities
코드는 이 목록 바로 위에 이런 주석을 달았다: "Order matters. Duplicated candidates take the first occurrence in the pipelines when merger". 병합은 DropDuplicateCandidates(AllPipelines, IdAndClassDuplicationKey, PickFirstCandidateMerger)가 처리한다 확인 :462-469.
즉 이 규칙은 비율도 상한도 아니고, 중복되면 앞선 파이프라인이 이기는 우선순위다. 같은 포스트가 인네트워크와 tweet-mixer 양쪽에서 오면 인네트워크 것으로 기록된다.
여기서도 캐시 상태가 게이트다.
- Frs, TweetMixer, Uteg 파이프라인은
MinCachedTweetsParam(default 30:272) 미만일 때만 돈다 확인 —ScoredTweetsParam.scala:269-275- 추가 근거는
ScoredTweetsTweetMixerCandidatePipelineConfig.scala:51이다.
- 추가 근거는
fail-open(파이프라인 하나가 실패해도 응답은 나가게 하는 정책)이 기본이지만, 예외가 하나 있다.
- tweet-mixer는 모든 파이프라인에 fail-open을 기계적으로 적용한다 확인
HomeRecommendedTweetsRecommendationPipelineConfig.scala:334-337.- 적용 코드는
candidatePipelines.map { … FailOpenPolicy.Always }다.
- 적용 코드는
- home-mixer는 fail-open 대상을 손으로 나열해서 9개 중 8개만 등록했다.
scoredTweetsContentExplorationCandidatePipelineConfig가 목록에 없다 확인ScoredTweetsRecommendationPipelineConfig.scala:456vs:492-502. - 목록에서 빠진 파이프라인은 기본 정책을 따른다 확인
product-mixer/core/.../RecommendationPipelineConfig.scala:113. 따라서 ContentExploration이 죽으면 요청 전체가 실패한다.- 기본 정책은
defaultFailOpenPolicy = FailOpenPolicy(Set(ClosedGate))다. - 이는 나머지 8개가 죽으면 그 몫이 조용히 사라지고 나머지로 채워지는 것과 대비된다.
- 기본 정책은
이것이 의도인지 누락인지는 코드에 단서가 없지만, 8개를 손으로 적다가 하나를 빠뜨리기 쉬운 형태이고 탐색 파이프라인만 fail-closed로 둘 이유도 찾기 어렵다 추정.
소스가 아니라 "슬롯"으로 배분하는 경우
후보를 전부 점수 경쟁에 맡기지 않고 위치를 고정하는 장치도 있다. 이 장치는 랭킹 부스트가 아니라 위치 예약이다. 점수를 올리는 것이 아니라 자리를 미리 배정한다.
- 커뮤니티당 후보는 1개이고, 커뮤니티 후보 총합은 3개다 확인
KeepTopKCandidatesPerCommunity.scala:14-15,30,32- 상수는
MaxCandidatesPerCommunity = 1,MaxCommunityCandidates = 3이다.
- 상수는
- 콘텐츠 탐색 후보와 DeepRetrieval(유저 탑과 아이템 탑을 따로 학습해 내적으로 채점하는 후보 소스) 후보를 특정 위치로 끌어올린다 확인
ScoreAveragingPositionSelector.scala:121,137,193- 파라미터는
ContentExplorationBoostPosParam과DeepRetrievalBoostPosParam이고, 각각getNumCandidatesToBoost = 1이다.
- 파라미터는
케이스: Meta — 숫자는 있는데 배분기가 없다
Meta는 X와 반대 방향으로 공개했다. 지면마다 단계별 후보 수는 숫자로 밝히지만 그 숫자를 만드는 배분기는 공개하지 않았고, 배분 장치가 기계적으로 보이는 곳은 Instagram 한 지면뿐이다 추정(Meta 쪽 배분 장치).
pass 0 후보 수 — 지면별
Meta는 AI system card(Meta가 지면마다 발행하는 랭킹 설명 문서)에 지면별 후보 수를 적는다. pass 0(경량 모델로 후보를 줄이는 단계)의 후보 수는 지면마다 달라서, "Meta의 pass 0은 500"이라고 한 줄로 쓰면 아홉 줄 중 한 줄만 맞다.
| 지면 | pass 0 / retrieval | 다음 단계 | 축소비 |
|---|---|---|---|
| Facebook Feed | 약 500 | 약 500 | 1:1 |
| Facebook Video | 약 1,000 | 상위 200 | 5:1 |
| Facebook Reels | 약 10–100 | (수치 없음) | — |
| Facebook Feed Recommendations | 수치 없음 | — | — |
| Instagram Feed | 약 700 | 약 500 | 1.4:1 |
| Instagram Explore | up to 1,500 | 상위 100 | 15:1 |
| Instagram Reels Chaining | 약 100 | 약 100 | 1:1 |
| Instagram Feed Recommendations | 수치 없음 | — | — |
| Threads For You | 수치 없음 | — | — |
각 수치는 AI system card 본문 그대로다 확인 인덱스 (2023~). 원문 인용과 지면 맥락은 facebook.md, instagram.md, threads.md에 있다.
같은 이름의 지면인데 숫자가 다르다. FB Feed는 500→500으로 줄지 않고, IG Feed는 700→500으로 준다. Meta 문서는 지면마다 다른 퍼널 형태를 서술한다.
비연결(Recommendations) 카드는 FB·IG 양쪽 다 수치가 없다. 그런데 같은 시스템을 설명하는 2023 블로그는 수십억 개를 수천 개로, 다시 수백 개로 줄인다고 쓴다 확인 ai.meta.com, 2023. Meta 자체 문서 두 개가 다른 해상도로 서술한다.
"narrow billions of pieces of content down to thousands and then to a few hundred"
Threads 카드는 단계별 수치를 하나도 제시하지 않는다. 관찰 2026-08-09 (ig-threads-feed 카드 본문 1건)
두 숫자의 성격이 다르다 — 이것이 진짜 구조적인 차이다
X와 Meta의 후보 수는 나온 곳부터 다르다. X의 숫자는 소스코드에서, Meta의 숫자는 system card에서 나온다 관찰. 실무에서 Meta 숫자는 상대 비교(Explore 15:1 vs Feed 1.4:1)에는 쓸 수 있지만 재현에는 못 쓴다. X 숫자는 그 반대다.
- X의 숫자는 소스코드에서 나온다. 정확하고, 파일과 줄 번호를 적을 수 있고, 서로 어긋나는 것까지 보인다(위에서 본 500 상한 뒤의 400 절단선). 확인
- 예:
LightRankerMaxResultsParam500,DefaultRequestedMaxResultsParam300,ServerMaxResultsParam500, home-mixer 실제 요청값 400,MinCachedTweetsParam30
- 예:
- Meta의 숫자는 system card에서 나온다. 전부 "approximately", "about", "up to"가 붙은 반올림 값이고, 절단 순서도, 누가 어디서 자르는지도 밝히지 않는다. 관찰 2026-08-09 (검사한 코퍼스: 위 표의 시스템 카드 9종 본문)
- 공개물의 목적이 다르다 추정. 3세대까지의 X 레포는 시스템을 읽을 수 있게 만들려던 것이라 로직은 100%, 값은 0%로 남았고, system card는 DSA(EU 디지털서비스법) 설명 의무에 대응하는 문서라 값은 반올림해서 주고 로직은 0%다 추정. 근거는 두 공개물의 정밀도 패턴이 정확히 반대 방향이라는 점이다.
- 4세대 공개(
47c1bcd)부터는 X 레포에 값도 있다 확인(2026 Rust 절).
- 4세대 공개(
Meta 쪽 배분 장치 — 딱 한 지면에서만 기계적으로 보인다
Meta에서 배분 장치가 기계적으로 드러나는 자료는 Instagram의 2020년 retrieval DSL과 2023년 Explore 글뿐이다. 두 자료에는 소스별 가중치, 계정당 상한, seed 계정 기준 라운드로빈, 병합 우선순위가 나온다 확인. 쿼리 전문은 instagram.md에 있다.
Explore 글은 소스 타입 4종(heuristics, ML, real-time, pre-generated)을 조정 가능한 가중치로 섞는다고 쓴다 확인 Scaling the Instagram Explore recommendations system, 2023.
- 원문은 이렇다: "mix them with tunable weights".
- 소스별 가중치가 튜닝 파라미터로 존재한다고 명시한 진술이다. 값은 공개하지 않았다.
retrieval DSL에는 계정당 상한이 있다. X의 소스별 상한과 같은 유형(상한 배분)이지만, 키가 소스가 아니라 작성자다 확인 2020.
- Instagram 쪽 파라미터는
max_media_per_account=10이고, X 쪽은*MaxResults계열이다.
- Instagram 쪽 파라미터는
retrieval 단계에는 라운드로빈도 있다. X의 소스×시그널 버킷 위브와 같은 유형이고, 키만 seed 계정으로 다르다 확인 2020.
- Instagram 쪽은
diversify_by(seed_id, method=round_robin), X 쪽은exclusiveRoundRobinSelector다.
- Instagram 쪽은
seed 소스에는 병합 우선순위가 있다 확인 2020.
"The final merge order is as follows: H >> R > F."
- H는 Home Feed 유래, R은 Explore/Reels 등 다른 추천 지면 유래, F는 graph exploration 백업이다.
병합 우선순위는 X와 거의 같은 방식이다. X home-mixer는 파이프라인 9개를 순서대로 연결하고 "Order matters. Duplicated candidates take the first occurrence in the pipelines when merger" 주석을 달았다. Instagram은 같은 규칙을 H >> R > F라는 한 문장으로 공개했다. 비율도 상한도 아닌 "중복 시 앞선 소스가 이긴다"는 우선순위 배분이 양쪽에 다 있다. 추정 구현까지 같은지는 확인할 수 없다. Instagram 쪽은 코드가 공개되지 않았다.
저신호·신규 유저 처리도 서로 대응한다. 양쪽 다 점수 경쟁이 의미 없을 때만 명시적 분포 제약으로 내려간다.
X는 저신호 유저에게만 명시적 비율 배분으로 내려간다 확인(2025 tweet-mixer).
Instagram은 이력이 희소한 유저에게 one-hop/two-hop 그래프 확장으로 seed를 만들고, 분포가 Home 기반 소스에서 벗어나지 않게 하는 조건을 학습 제약으로 적용한다 확인 2020.
"the overall distribution is not skewed away from Home-based sources"
Facebook 쪽에는 이런 장치가 없다.
- Facebook Feed retrieval의 소스별 후보 수 분해는 1차 출처를 찾지 못했다(no primary source found). 추정
- 검사한 코퍼스는 Transparency Center 시스템 카드 15종, engineering.fb.com·ai.meta.com·about.fb.com 본문, 실적발표 전사본(2026-08-09 열람)이다.
- 그 이유는 연결 콘텐츠의 코퍼스가 이미 친구 그래프로 제한돼 배분 문제 자체가 크지 않기 때문으로 보인다 추정.
케이스: Netflix — 배분 단위가 소스가 아니라 행이다
Netflix를 넣으면 이 노트의 전제 하나가 조건부가 된다. "소스가 여럿일 때 몇 개씩 섞는가"라는 문제 설정 자체가 코퍼스가 클 때만 성립한다. Netflix에는 아이템 단위 배분이 없고, 배분 단위는 행이다 확인.
아이템 레벨 배분 레이어가 없다. 편성 카탈로그가 수천~수만 타이틀이라, PVR(장르 행을 채우는 랭커)이 "카탈로그 전체(또는 장르 필터된 부분집합)를 회원별로 정렬"한다 확인 TMIS 2015.
- 전 아이템을 채점할 수 있으면 소스별 상한(X의
*MaxResults)도, 퍼널 수치(Meta의 pass 0)도 필요 없다. - X의 500/400 같은 숫자가 등장할 자리가 없다.
대신 같은 문제가 한 층 위, 곧 행 배분에서 다시 나타난다. 회원당 후보 행은 수만 개이고("tens of thousands"), 지면은 약 40행에 행마다 최대 75타이틀이다 확인 TMIS 2015. "어떤 소스에서 몇 개"라는 질문이 "어떤 행(장르/BYW/Trending/CW)을 몇 개, 어떤 순서로"라는 질문으로 바뀐다. BYW는 Because You Watched 행이고, CW는 Continue Watching 행이다.
Netflix는 2015년에 이미 비율 배분(템플릿)을 폐기하고 ML 기반 선택으로 전환했다 확인. ML 기반 선택은 "BYW 행이 0개인 홈도, 절반이 BYW인 홈도" 허용한다 확인. X가 2026에 한 전환(쿼터 제거 → 점수 경쟁)을 행 단위에서 10년 먼저 한 셈이다 추정. 문제 구조가 같다는 뜻이지, 계보 관계가 있다는 뜻은 아니다.
행 종류별 랭커가 X의 후보 소스에 대응한다. PVR·Top-N·Trending·Continue Watching·sims가 각자 다른 알고리즘으로 행 내용을 채우고 확인 TMIS 2015, 페이지 알고리즘이 이들을 조립한다. X home-mixer가 candidate pipeline 9개를 병합하는 것과 같은 자리다. 차이는 병합 규칙에 있다. X는 "중복 시 앞선 파이프라인 승리"라는 우선순위를 쓰고, Netflix는 행 선택이라는 최적화를 쓴다.
GenPage(2026)는 이 레이어 전체를 단일 모델로 통합한다. 행 선택과 행 내 정렬을 트랜스포머 하나의 autoregressive 생성으로 대체하고, 엔드투엔드 지연은 −20%다 확인 arXiv 2606.31031 (실험 단계). Meta의 SilverTorch가 인덱스를 모델 텐서로 통합한 것과 같은 방향이다. 배분 로직이 명시적 규칙에서 학습된 분포로 옮겨 간다.
광고 쪽은 랭킹 모델 공개물이 없어서 오가닉과 광고의 배분 규칙도 알 수 없다 확인(부재). 관련 서술은 netflix.md에 있다.
광고 대 오가닉 — 개수 슬롯이냐 픽셀 거리냐
배분 레이어에서 두 회사의 차이가 가장 뚜렷하게 드러나는 곳은 광고와 오가닉의 배분이다. X는 오가닉 랭킹이 끝난 뒤 개수 기준으로 광고를 끼워 넣고, Meta 특허는 광고와 오가닉을 공통 단위로 환산해 하나의 랭킹에 넣는다 확인.
X는 오가닉 랭킹이 끝난 뒤 개수 기준으로 광고를 끼워 넣는다. 확인 2026 소스코드 xai-org/x-algorithm
- 광고를 붙이려면 오가닉 포스트가 최소 5개 있어야 한다 확인
ads/util.rs:10 - 기본 간격은
{requested: 3, min: 2}이다 확인ads/util.rs:12-17 - 안전 갭의 절반까지만 넣는다 확인
ads/partition_organic_blender.rs:35- 계산식은
max_from_safe = safe_count / 2다.
- 계산식은
- 블렌더 selector는 프레임워크 정렬을 의도적으로 무력화한 뒤, 프롬프트와 팔로우 추천, 광고를 지정된 위치에 삽입한다 확인
blender_selector.rs:25-114- selector는
BlenderSelector이고, 무력화 방법은score()를 상수0.0으로 두는 것이다.
- selector는
즉 광고는 랭킹 경쟁에 참여하지 않는다. 정렬이 끝난 리스트에 개수 규칙으로 삽입된다.
Meta는 광고와 오가닉을 공통 단위로 환산해 하나의 랭킹에 넣는다. 확인 US 10,345,993 B2 "Selecting content items for presentation in a feed based on heights associated with the content items", 출원 2015 / 등록 2019, 특허
특허는 오가닉 점수와 광고 점수를 공통 측정 단위로 바꾼 뒤, 광고와 오가닉 스토리를 한 랭킹에서 함께 순위 매긴다고 쓴다 확인 특허.
"Scores associated with organic news feed stories and scores associated with advertisements are converted into a common unit of measurement, and advertisements and organic news feed stories are together ranked in a single ranking."
간격 제약이 개수 기준과 픽셀 높이 기준 둘 다로 표현된다 확인 특허.
- 개수 기준: "at least a threshold number of organic news feed stories are presented between advertisements"
- 또는 높이 기준: "a combination of heights of the organic feed stories exceeds a gap distance threshold"
position discount(위치에 따라 예측되는 상호작용 감소)가 위에 놓인 후보들의 예측 높이 합에 의존한다 확인 특허.
높이 예측 자체가 ML 모델이다. 콘텐츠 타입과 언어, 댓글 수에 클라이언트 화면 크기와 OS, 해상도, 앱 버전을 더해 콘텐츠가 표시될 때의 높이를 예측한다 확인 특허.
"the likely height of the content item when presented"
즉 인터리빙은 슬롯 인덱스가 아니라 기하 제약이다. "3개마다 하나"가 아니라 "직전 광고로부터 픽셀 거리 X 이상"이므로, 같은 유저라도 화면 크기와 콘텐츠 타입 구성에 따라 실제 광고 개수가 달라진다 추정. 고정 슬롯 비율(ad load N%)로 모델링하면 틀린다는 뜻이다 추정. 다만 특허가 결과 비중을 말하지는 않는다.
⚠ 주의할 점이 두 가지 있다.
- 이 특허는 2015년에 출원됐고, 현재 프로덕션이라는 근거가 아니다. 다만 Meta 광고 인터리빙에 관해 남아 있는 유일한 기계적 1차 출처다.
- 널리 퍼져 있는
Total Value = Bid × Estimated Action Rate × Ad Quality는 Meta 1차 출처를 찾지 못했으므로 수식으로 인용하면 안 된다. ad load 수치도, 인터리빙 규칙도 마찬가지다 추정.- 검사한 코퍼스는 Marketing API 입찰·게재 문서, Transparency Center 광고 페이지, about.fb.com 본문(2026-08-09 열람)이다. 상세는 facebook.md에 있다.
Threads는 2025 Q3에 광고를 글로벌 롤아웃했지만, 인터리빙 규칙은 공개하지 않았고 픽셀 높이 특허를 적용하는지도 알 수 없다. 추정(같은 코퍼스, 2026-08-09). 관련 서술은 threads.md에 있다.
이 케이스에서 일반화할 수 있는 것
배분을 읽을 때는 공개된 비율보다 코드의 실제 경로를 봐야 한다. 실제 소스 구성은 캐시 상태, fail-open 정책, 절단선의 순서에 따라 달라진다. 배분이 필요한지와 그 단위는 코퍼스 크기와 지면 구조를 따라간다.
- 배분은 실제로 어디서 정해지는가?
- 공개된 "비율"은 대개 관측 결과이지, 코드에 있는 쿼터가 아니다. 소스를 열어 배분기를 직접 찾아야 한다. X는 결국 배분기를 없애고 점수 배수로 옮겨 갔다.
- 캐시가 최대의 배분기다. X는 두 세대 모두 "캐시된 후보가 충분하면 비싼 소스를 안 돈다"가 1차 분기다. 즉 실제 소스 구성은 유저의 요청 빈도에 따라 달라진다.
- 신규·저신호 유저에게만 비율 배분을 쓴다. 시그널이 없어 점수 경쟁이 의미 없을 때만 명시적 쿼터로 내려간다. X는 저신호 유저에게 명시적 비율을 쓰고, Instagram은 희소 유저에게 seed 확장과 Home 분포 제약을 쓴다. 양쪽에서 같은 패턴이 관찰된다.
- 상한과 장애 정책은 배분을 어떻게 바꾸는가?
- fail-open이 기본이면 배분은 보장이 아니라 경향이다. X는 거의 모든 candidate pipeline이 fail-open이라 소스 하나가 죽어도 응답은 나가고, 그 몫은 조용히 다른 소스로 대체된다. 다만 정책을 손으로 나열하면 빠뜨린 하나가 fail-closed가 된다. X home-mixer의 ContentExploration이 그 경우다. 기계적 일괄 적용(tweet-mixer)과 수기 나열(home-mixer)의 차이가 그대로 장애 반경의 차이가 된다.
- 상한이 여러 겹이면 "나중에 붙는다"는 우대가 아니라 후순위다. X 2025 tweet-mixer의 u2i 후보는 라이트랭커 상한(500) 절단 뒤에 붙어 경쟁을 면제받는 것처럼 보인다. 하지만 그 뒤의 최종 절단선이 400이라, 앞이 다 차면 먼저 사라진다. 절단선을 하나만 보고 결론을 내면 방향이 반대로 나온다.
- 배분에 관한 숫자와 모델은 어떻게 읽어야 하는가?
- "후보 수"라는 숫자는 출처 종류를 먼저 봐야 한다. 소스코드에서 나온 수는 정확하고, 재현에 쓸 수 있으며, 서로 어긋나는 것까지 보인다. 규제 대응 문서에서 나온 수는 반올림돼 있고 상대 비교에만 쓸 수 있다. 정밀도는 시스템의 성질이 아니라 공개물의 목적이 정한다.
- 광고 인터리빙을 슬롯 비율로 모델링하면 안 될 수 있다. X는 개수 기준(간격 3, 최소 5개 오가닉)이라 슬롯 모델이 맞다. Meta 특허는 픽셀 높이라는 공통 단위와 gap distance 제약이라 화면 크기에 따라 결과가 달라진다. 같은 "N개마다 광고 하나"라는 요약이 한쪽에서는 맞고 한쪽에서는 틀린다.
- 배분이 필요한지와 그 단위는 무엇이 정하는가?
- 배분 레이어의 존재 자체가 코퍼스 크기의 함수다. 카탈로그가 전 아이템을 채점할 수 있는 크기(Netflix 수천~수만)면 아이템 레벨 배분이 사라지고, 문제는 한 층 위(행 구성)로 옮겨 간다. "이 서비스의 후보 배분은?"이라고 묻기 전에 "배분이 필요한 규모인가"부터 물어야 한다.
- 배분 단위는 지면 구조를 따라간다. 단일 스트림은 아이템을 배분하고, 2차원 페이지는 행을 배분한다. Netflix의 행 종류(장르/BYW/Trending)가 X의 후보 소스와 같은 역할을 한다. 다만 병합 규칙만 우선순위(X)와 최적화(Netflix)로 다르다.
누가 이걸 쓰는가
X는 배분 로직이 전부 레포 안에 있고, 값은 3세대까지 레포 밖에 있다가 4세대 공개(2026-08-13)부터 레포의 파라미터 미러에 있다 확인. Facebook과 Instagram은 여러 지면의 후보 수를 밝히고, Threads는 하나도 밝히지 않는다 관찰. Netflix는 아이템이 아니라 행을 배분한다 확인.
- X — 2025 Scala 세대는 상한과 위브, 우선순위 병합을 쓰고, 2026 Rust 세대는 배분기 없이 점수 배수를 쓴다. 로직은 전부 레포 안에 있고, 값은 2025 Scala와 3세대에서는 레포 밖에 있었으며 4세대 공개(
47c1bcd)부터 레포의 파라미터 미러에 있다 확인 — x.md - Facebook — pass 0은 약 500(Feed), 1,000→200(Video), 10–100(Reels)이고, 비연결 카드에는 수치가 없다. 소스별 분해는 1차 출처가 없다. 광고 쪽 근거는 픽셀 높이를 공통 단위로 쓰는 특허다 확인 — facebook.md
- Instagram — Feed는 700→500이고, Explore는 1,500→100(15:1)이며, Reels Chaining은 약 100이다. Meta에서 배분 장치가 기계적으로 보이는 유일한 지면이다(tunable weights,
max_media_per_account, seed 라운드로빈,H >> R > F). 확인 — instagram.md - Threads — 단계별 후보 수가 하나도 없다. 카드는 인벤토리 정의에서 팔로우는 all, 공개 콘텐츠는 a portion이라는 비대칭만 명시한다 관찰 2026-08-09 (ig-threads-feed 카드 본문 1건), threads.md
- Netflix — 아이템 레벨 배분이 없고(전 카탈로그를 채점한다), 배분 단위는 행이다. 행 후보 수만 개를 약 40행으로 줄인다. 2015년에 템플릿(비율 배분)을 폐지했다 확인 — netflix.md
- 그 외 서비스 — 아직 조사하지 않았다. 열린 질문으로 남아 있다.
아직 확인하지 못한 것
Meta 쪽은 연결·비연결 결과의 병합 규칙, Explore 가중치의 튜닝 방법, 높이 특허의 현재 운용 여부, Threads의 단계별 수치를 아직 확인하지 못했다. Netflix 쪽은 지금의 행 선택 알고리즘을 확인하지 못했다.
- Meta가 연결(500) 결과와 비연결(수백) 결과를 어떤 규칙으로 인터리브하는가. 두 시스템이 별개라고만 밝히고 병합 규칙은 설명하지 않는다. 참고: facebook.md 열린 질문 4.
- Instagram Explore의 소스별 tunable weight를 무엇으로 튜닝하는가. "tunable"이라고만 하고 목적 함수와 최적화 방법은 공개하지 않았다.
- Meta 픽셀 높이 특허(2015 출원)가 2026년에도 운용되는가. 특허 외에는 1차 근거가 없다.
- Threads의 후보 생성 방식과 단계별 수치. system card가 숫자를 하나도 주지 않는다.
- Netflix 행 선택 알고리즘은 지금 어떤 형태인가. 2015 논문은 row-level 채점과 greedy 류로 서술한다. 그 뒤 GenPage(2026 실험)까지 10년 사이의 1차 출처가 없다.
출처
공개 소스코드
- xai-org/x-algorithm — 2026, 공개 소스코드 (Apache 2.0).
home-mixer/sources/,home-mixer/selectors/,home-mixer/ads/. 본문 줄 번호 인용의 기준 커밋은0bfc279(3세대)이고, 4세대 값은47c1bcd에서 읽었다. - twitter/the-algorithm — 2025-09-03 커밋
c54bec0, 공개 소스코드.tweet-mixer/,home-mixer/.
Meta Transparency Center (후보 수의 1차 출처)
- Our approach to explaining ranking (AI system card 인덱스) — 2023~, 규제 대응 문서. 지면별 pass 0 수치가 여기서 나온다.
- 개별 카드: fb-feed · fb-video · fb-reels · ig-feed · ig-explore · ig-reels-chaining · ig-threads-feed
Meta 블로그·특허
- Designing a Constrained Exploration System — 2020, 블로그. retrieval DSL,
max_media_per_account,H >> R > F. - Scaling the Instagram Explore recommendations system — 2023, 블로그. 소스 타입 4종과 tunable weights.
- The AI behind unconnected content recommendations — 2023, 블로그. billions → thousands → a few hundred.
- US 10,345,993 B2 — Selecting content items for presentation in a feed based on heights — 등록 2019(출원 2015), 특허. 광고와 오가닉을 통합한 랭킹, 그리고 gap distance에 관한 유일한 기계적 출처다.
Netflix
- The Netflix Recommender System (TMIS) — 2015, 논문. 전 카탈로그 정렬, 행 수만 개 → 40행, 템플릿 폐지.
- Learning a Personalized Homepage — 2015, 블로그. 행 선택 문제 정식화.
- GenPage — 2026, 논문(RecSys). 페이지 생성의 단일 모델화.