◂ feed-research2026-08-29 빌드

후보 배분 — 소스가 여럿일 때 무엇을 몇 개 넣는가

최종 갱신: 2026-08-09

후보 생성 소스가 10개, 30개씩 있을 때 "각 소스에서 몇 개를 가져와 최종 리스트에 어떻게 섞을 것인가"를 정하는 레이어. 세 가지 방식이 있고, X는 세대가 바뀌면서 방식을 바꿨다.

  1. 비율 배분 (ratio) — "이 소스에서 30%" 식으로 슬롯을 나눈다. 결과 구성이 보장된다.
  2. 상한 배분 (cap) — 소스마다 최대 N개만 가져오고, 이후엔 점수로만 경쟁시킨다. 구성이 보장되지 않는다.
  3. 인터리브 (round-robin / weave) — 소스를 번갈아 하나씩 뽑는다. 상한과 조합해서 쓴다.

X와 Meta는 이 레이어에 대해 정확히 반대 방향으로 공개했다. X는 배분기 코드가 다 보이는데 상한 이 전부 레포 밖에 있고, Meta는 단계별 후보 를 지면마다 숫자로 주는데 그 숫자를 만드는 배분기가 어디에도 없다. 이 비대칭 자체가 아래 전체를 관통하는 주제다.

"X는 인네트워크:아웃오브네트워크를 50:50으로 섞는다"는 틀렸다

가장 널리 퍼진 오해다. 근거는 2023년 공식 블로그의 *"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의 "~50% of Tweets come from this candidate source"(검색 인덱스 행)인데, 둘 다 관측된 결과 비율("on average")이지 코드에 있는 쿼터가 아니다. [확인] — Twitter 블로그 2023-03-31(아카이브 대조 2026-08-09) + twitter/the-algorithm README.md:41

2026 세대 코드에는 in-network / out-of-network 비율을 강제하는 코드가 없다.

let final_score = match c.in_network {
    Some(false) => after_diversity * effective_oon,
    _ => after_diversity,
};

배수 값은 세 갈래로 갈린다: 토픽 요청이면 TopicOonWeightFactor, 신규 유저(계정 나이 < NewUserAgeThresholdSecs 그리고 팔로잉 수 ≥ NEW_USER_MIN_FOLLOWING)면 NEW_USER_OON_WEIGHT_FACTOR, 나머지는 OonWeightFactor. 세 값 모두 레포에 없다. [확인]ranking_scorer.rs:220-239

→ 즉 "OON을 얼마나 넣을지"는 배분 문제가 아니라 랭킹 문제로 옮겨갔다. 결과 비율은 배수와 후보 풀에 따라 요청마다 달라진다.

세대별 배분 구조 — X

2026 Rust: 배분기 없음, 소스별 상한만

소스 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에서 읽는다 — ThunderMaxResults, PhoenixMaxResults, TweetMixerMaxResults. 값은 전부 레포 밖. [확인]thunder_source.rs:38, phoenix_source.rs:89, tweet_mixer_source.rs:61

전역 선택은 TopKScoreSelector 하나: 점수 내림차순 TOP_K_CANDIDATES_TO_SELECT개. 이 상수도 params 모듈에 있고 params 모듈은 공개되지 않았다. [확인]top_k_score_selector.rs:13

캐시 게이트가 사실상 배분기 역할을 한다. 캐시된 포스트가 임계치 이상이면 모든 소스와 Phoenix 자체가 조용히 꺼진다 [확인]. 즉 "새 후보를 얼마나 가져올지"의 1차 결정은 소스별 상한이 아니라 캐시 상태다.

2025 Scala: 상한 + 인터리브 + 우선순위 위브

훨씬 복잡하다. 두 레이어로 나뉜다.

(a) tweet-mixer — OON 후보 생성 전담. Home 요청에 candidate pipeline 34개. [확인]HomeRecommendedTweetsRecommendationPipelineConfig.scala:271-332 (엔트리 :274-331)

배분 순서 [확인] :362-412:

  1. 저신호 유저면 여기서 끝난다. 작성자당 1개로 dedup한 뒤(DropDuplicateCandidates(duplicationKey = _.features.getOrElse(AuthorIdFeature, None)) :364) InsertAppendRatioResults명시적 비율 배분을 하고 DropAllCandidates() :380로 이후 selector를 전부 막는다. 블록 전체가 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 값이며 프로덕션 값 아님)

  2. 아니면 블렌더가 BlendingParam으로 선택돼 실행된다. Home 파이프라인에 배선된 것은 3개이고 그중 하나가 뽑힌다 [확인] :399,401,402:

    • exclusiveRoundRobinSelector(소스, 시그널) 버킷으로 위브
    • signalPrioritySelector — 버킷에 USSFeatures.getPriority 추가
    • weightedSignalPrioritySelector — 가중 위브. new Random(0) 고정 시드 [확인] TweetMixerFunctionalComponents.scala:289

    주의: TweetMixerFunctionalComponents.scala에 정의된 selector는 훨씬 많고(roundRobinSelector :210, inclusiveRoundRobinSelector :237 등), BlendingEnum은 값이 6개다 [확인] TweetMixerGlobalParams.scala:23-30. "블렌더는 3종"이 아니라 "이 지면이 3개를 배선했다"가 맞다.

  3. DropMaxResults(LightRankerMaxResultsParam) :404 — default 500 [확인] HomeRecommendedTweetsParam.scala:9-15

  4. 그 다음에 u2i(user→post) 파이프라인 7개 :385-393inclusiveRoundRobinSelector로 덧붙인다 :405-406

  5. DropRequestedMaxResults :408-411min(requestedMaxResults, serverMaxResults). 파라미터명은 DefaultRequestedMaxResultsParam(300)과 ServerMaxResultsParam(500) [확인] HomeRecommendedTweetsParam.scala:18-24,27-33

4번은 특혜처럼 보이지만 특혜가 아니다. "상한을 자른 뒤에 붙으니 슬롯 경쟁을 면제받는다"고 읽기 쉬운데, 두 가지가 그 결론을 뒤집는다 [확인]:

→ 즉 5번의 최종 절단선(400)이 3번의 상한(500)보다 앞에 있으므로, 비-u2i 블렌드만으로 400개가 채워지면 뒤에 붙은 u2i는 한 개도 안 남는다. u2i는 500 슬롯을 놓고 경쟁하지 않는 대신 400에서 가장 먼저 잘리는 쪽이다. 순서상 뒤에 붙는 것은 우대가 아니라 후순위다.

저신호 유저 판정에 코드/주석 불일치가 있다. 게이트 주석은 "< 25 users"라고 쓰는데 코드는 SmallFollowGraphSize = 5다. tweet-mixer와 home-mixer 양쪽에 똑같이 복제돼 있다. [확인] — tweet-mixer SignalUtils.scala:17,20 vs AllowLowSignalUserGate.scala:12·DenyLowSignalUserGate.scala:12, home-mixer util/SignalUtil.scala:18 vs scored_tweets/gate/DenyLowSignalUserGate.scala:13

주석을 인용하면 임계값이 5배 틀린다. 그리고 이 임계값은 "명시적 비율 배분으로 내려갈지"를 가르는 스위치라서, 5냐 25냐가 영향받는 유저 규모를 크게 바꾼다.

(b) 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 양쪽에서 오면 인네트워크 것으로 기록된다.

캐시 상태가 여기서도 게이트다: MinCachedTweetsParam(default 30 :272) 미만일 때만 Frs / TweetMixer / Uteg 파이프라인이 돈다. [확인]ScoredTweetsParam.scala:269-275, ScoredTweetsTweetMixerCandidatePipelineConfig.scala:51

fail-open이 기본이지만 예외가 하나 있다. tweet-mixer는 candidatePipelines.map { … FailOpenPolicy.Always }로 전부 기계적으로 fail-open이다 [확인] HomeRecommendedTweetsRecommendationPipelineConfig.scala:334-337. 그런데 home-mixer는 손으로 나열해서 9개 중 8개만 등록했다 — scoredTweetsContentExplorationCandidatePipelineConfig가 목록에 없다 [확인] ScoredTweetsRecommendationPipelineConfig.scala:456 vs :492-502.

빠진 것은 defaultFailOpenPolicy = FailOpenPolicy(Set(ClosedGate))로 떨어진다 [확인] product-mixer/core/.../RecommendationPipelineConfig.scala:113ContentExploration이 죽으면 요청 전체가 실패한다. 나머지 8개가 죽으면 그 몫이 조용히 사라지고 나머지로 채워지는 것과 대비된다.

이게 의도인지 누락인지는 코드에 단서가 없다 [추정] — 8개를 손으로 적고 하나를 빠뜨리기 쉬운 형태이고, 탐색 파이프라인만 fail-closed로 둘 이유를 찾기 어렵다.

소스가 아니라 "슬롯"으로 배분하는 경우

전부 점수 경쟁으로 보내지 않고 위치를 고정하는 장치도 있다.

이건 랭킹 부스트가 아니라 위치 예약이다. 점수를 올리는 게 아니라 자리를 준다.

케이스: Meta — 숫자는 있는데 배분기가 없다

pass 0 후보 수 — 지면별

지면 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 verbatim (2023~, 인덱스). 원문 인용과 지면 맥락은 → facebook.md · instagram.md · threads.md

두 숫자의 성격이 다르다 — 이게 진짜 구조적 포인트

Meta 쪽 배분 장치 — 딱 한 지면에서만 기계적으로 보인다

Instagram의 2020년 retrieval DSL과 2023년 Explore 글이 유일하다. 쿼리 전문은 → instagram.md.

마지막 항목이 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를 만든 뒤 *"the overall distribution is not skewed away from Home-based sources"*를 학습 제약으로 건다 [확인] 2020. 양쪽 다 점수 경쟁이 의미 없을 때만 명시적 분포 제약으로 내려간다.

Facebook 쪽에는 이런 게 없다. Facebook Feed retrieval의 소스별 후보 수 분해는 no primary source found. [추정] — 검사한 코퍼스는 Transparency Center 시스템 카드 15종, engineering.fb.com·ai.meta.com·about.fb.com 본문, 실적발표 전사본(2026-08-09 열람)이다. 연결 콘텐츠는 코퍼스가 이미 친구 그래프로 제한돼 배분 문제 자체가 약하기 때문으로 보인다 [추정].

케이스: Netflix — 배분 단위가 소스가 아니라 행이다

Netflix를 넣으면 이 노트의 전제 하나가 조건부가 된다: "소스가 여럿일 때 몇 개씩 섞나"라는 문제 설정 자체가 코퍼스가 클 때만 성립한다.

아이템 레벨 배분 레이어가 없다. 편성 카탈로그가 수천~수만 타이틀이라 PVR이 *"카탈로그 전체(또는 장르 필터된 부분집합)를 회원별로 정렬"*한다 [확인] TMIS 2015. 전 아이템을 채점할 수 있으면 소스별 상한(X의 *MaxResults)도, 퍼널 수치(Meta의 pass 0)도 필요 없다. X의 500/400 같은 숫자가 등장할 지점이 없다.

대신 같은 문제가 한 층 위에서 다시 나타난다 — 행 배분. 회원당 후보 행이 수만 개("tens of thousands"), 지면은 약 40행 × 최대 75타이틀 [확인] TMIS 2015. "어떤 소스에서 몇 개"가 "어떤 행(장르/BYW/Trending/CW)을 몇 개, 어떤 순서로"가 된다. 그리고 2015년에 이미 비율 배분(템플릿)을 버리고 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 (실험 단계). SilverTorch가 인덱스를 모델 텐서로 접은 것과 같은 방향 — 배분 로직이 명시적 규칙에서 학습된 분포로 이동한다.

광고: 랭킹 모델 공개물이 없어 오가닉/광고 배분 규칙도 알 수 없다 [확인](부재) → netflix.md.

광고 대 오가닉 — 개수 슬롯이냐 픽셀 거리냐

배분 레이어에서 가장 선명한 두 회사 차이다.

X — 오가닉 랭킹이 끝난 뒤 개수 기준으로 끼워 넣는다. [확인] 2026 소스코드 xai-org/x-algorithm

광고가 랭킹 경쟁에 참여하지 않는다. 정렬이 끝난 리스트에 개수 규칙으로 삽입된다.

Meta — 광고와 오가닉을 공통 단위로 환산해 하나의 랭킹에 넣는다. [확인] US 10,345,993 B2 "Selecting content items for presentation in a feed based on heights associated with the content items", 출원 2015 / 등록 2019, 특허

인터리빙이 슬롯 인덱스가 아니라 기하 제약이다. "3개마다 하나"가 아니라 "직전 광고로부터 픽셀 거리 X 이상"이므로, 같은 유저라도 화면 크기와 콘텐츠 타입 구성에 따라 실제 광고 개수가 달라진다. [추정] — 특허가 결과 비중을 말하지는 않는다. 고정 슬롯 비율(ad load N%)로 모델링하면 틀린다는 뜻이다.

⚠ 주의 두 가지. (1) 이 특허는 출원 2015이고 현재 프로덕션이라는 근거가 아니다. 다만 Meta 광고 인터리빙에 남아 있는 유일한 기계적 1차 출처다. (2) 널리 도는 Total Value = Bid × Estimated Action Rate × Ad QualityMeta 1차 출처를 찾지 못했다 — 수식으로 인용 금지. ad load 수치도 인터리빙 규칙도 마찬가지다. [추정] — 검사한 코퍼스는 Marketing API 입찰·게재 문서, Transparency Center 광고 페이지, about.fb.com 본문(2026-08-09 열람)이다. 상세는 → facebook.md

Threads는 2025 Q3에 광고를 글로벌 롤아웃했지만 인터리빙 규칙 미공개이고 픽셀 높이 특허의 적용 여부도 불명이다. [추정](같은 코퍼스, 2026-08-09) → threads.md

이 케이스에서 일반화할 수 있는 것

누가 이걸 쓰는가

아직 확인 못한 것

출처

공개 소스코드

Meta Transparency Center (후보 수의 1차 출처)

Meta 블로그·특허

Netflix