0x00

리서치 / 일일 리포트

  • 읽은 자료 1건
  • 정본 반영 1개 파일
  • 레이더 0건
한 줄 요약X가 For You 피드 안에 세로 영상 3~5개를 묶어 보여 주는 캐러셀 모듈을 만들었다. 기본값은 꺼져 있다.

1For You 피드에 세로 영상 캐러셀 모듈이 생겼다 — X

자료 xai-org/x-algorithm@35650fb — xai-org, 2026-10-08T05:09:20Z, 코드. 이 노트들은 4세대 공개 뒤의 X 커밋에 차례로 번호를 붙여 부르며, 이 커밋은 4세대+38이다. 직전 커밋은 2026-10-07의 78460ca(4세대+37)다. 바뀐 파일은 173개이고, 줄 수는 +15,160/−2,465다.

배경 X의 For You 피드에서는 여러 후보 소스가 포스트를 모으고, Phoenix(Grok 기반 Transformer 랭커)가 좋아요·답글·영상 열기 같은 행동마다 확률을 예측한다. 그다음 value model이 이 확률들을 가중합해서 순서를 정한다. 마지막 단계의 블렌더는 광고, 팔로우 추천(Who-to-Follow, WTF), 프롬프트처럼 포스트가 아닌 모듈을 정해진 자리에 끼워 넣는다.

이번 커밋은 변경량이 1만 5천 줄이지만, 상당 부분은 안전 계층(visibility filtering, VF)의 내부 코드와, 약 3,000줄짜리 서드파티 GPU 커널 유틸 CuTe DSL quack이다. 피드 동작과 관련된 변경은 블렌더에 들어갈 새 모듈, 랭커 학습 손실의 기본값 변경, 후보 생성 모델(two-tower)의 새 옵션 세 가지다. 예산 가드(배치의 조사 범위를 정한 상한으로, 대형 공개는 핵심만 당일에 분석한다)에 따라 이 세 가지를 중심으로 분석하고, 나머지는 "다음에 볼 것"에 남긴다.

새로 알게 된 것A. For You에 세로 영상 캐러셀 모듈이 생겼고, 기본값은 off다

  • 캐러셀용 새 파라미터 세 개가 프로덕션 기본값 미러에 들어왔다 확인 — home-mixer/params/param.rs.

    • 미러 파일은 home-mixer(For You 피드를 조립하는 서비스)의 param.rs이고, 매일 X 내부 feature switch 기본값을 복사한다.
    • 세 값은 EnableVideoCarousel = false, VideoCarouselFatigueMinutes = 60, VideoCarouselPosition = 6이다.
    • 미러 sync 시각은 2026-10-07T16:00:42Z이고, 미러에서 바뀐 것은 이 세 개뿐이다.
  • 캐러셀이 켜지는 조건은 세 가지다 확인 — home-mixer/util/video_carousel.rs:7-11, query_hydrators/served_history_query_hydrator.rs:52-57, 110-126.

    • For You 요청이어야 하고, 플래그가 켜져 있어야 하며, 직전 캐러셀 노출 후 60분이 지났어야 한다.
    • 피로도(fatigue)는 유저가 같은 모듈을 너무 자주 보지 않게 하는 쿨다운이다.
    • 이 장치는 WTF 모듈과 피드 설문에 이미 쓰던 것을 그대로 재사용했다. 단위는 WTF 모듈이 시간, 피드 설문이 분이다.
  • 캐러셀 후보는 두 단계를 거쳐 고른다 확인 — selectors/top_k_score_selector.rs:10-27, filters/video_carousel_filter.rs, params/config.rs:17-19, 35-36.

    1. 랭킹 직후의 선택 단계는 원래 점수 상위 50개만 남긴다. 캐러셀이 켜지면 탈락한 후보 가운데 영상이고, 답글이 아니고, video_open(영상을 열 확률) 예측값이 있는 포스트를 video_open 확률 순으로 최대 30개 더 살려 둔다. 이렇게 살린 후보에는 video_carousel_extra 표시가 붙는다.
    2. 살린 후보도 VF 필터와 대화 중복 제거 필터를 똑같이 거친다. 마지막 필터 VideoCarouselFilter가 extra 후보를 본 피드에서 빼낸다. 그다음 "상위 50개 중 36위 이하 + 빼낸 extra" 풀에서 세로 영상(가로/세로 비 < 1.0)만 골라 video_open 순으로 최대 5개를 캐러셀에 담는다. 3개가 안 되면 모듈을 만들지 않는다.
  • 세로 영상인지 가리는 가로/세로 비는 새 hydrator가 미디어 메타데이터에서 읽어 채운다 확인 — candidate_hydrators/video_aspect_ratio_hydrator.rs:95-101.

    • hydrator 이름은 VideoAspectRatioHydrator다.
    • 같은 파일의 테스트 기댓값을 보면, 9:16 세로 영상은 0.5625가 되어 통과하고, 16:9 가로 영상은 1.78, 정사각형은 1.0이 되어 탈락한다.
  • 블렌더는 광고·프롬프트·WTF를 넣은 다음 캐러셀을 6번째 자리에 넣는다 확인 — selectors/blender_selector.rs:72-85, 137-153.

    • 노출 기록에는 VIDEO_CAROUSEL_MODULE 엔트리로 남고, 다음 요청의 피로도 판정에 쓰인다.
  • 클라이언트에 보낼 헤더 문자열의 키는 VideoCarouselModule.header이고, 테스트에는 영문 값이 적혀 있다 확인 — util/urt/video_carousel_marshaller.rs.

    header(Some("Videos for you".to_string()))

  • 캐러셀 영상은 본 피드에 보이는 포스트와 겹치지 않도록 설계된 것으로 보인다 추정. 근거는 후보 풀이 본 피드 크기(RESULT_SIZE = 35) 바깥의 순위와 extra로만 이루어진다는 점이다.

    • 최종 응답이 정확히 35개에서 잘리는지는 이번에 대조하지 않았다.

새로 알게 된 것B. 기본 학습 설정에서 체류 시간 헤드의 손실이 CREAD로 바뀌었다

체류 시간 헤드는 유저가 포스트에 머문 시간을 초 단위로 예측하는 회귀 헤드다. value model은 이 예측값에 가중치를 곱해 점수에 더한다. 헤드의 코드 이름은 dwell_time이고, 가중치 이름은 ContDwellTimeWeight다. 체류 시간은 대부분 몇 초에 몰려 있고 긴 꼬리가 있어서, 평균 제곱 오차 같은 단순 회귀로는 학습이 불안정하다. 그래서 X는 지금까지 Tweedie 손실과 MAE(절대 오차)를 함께 써 왔다. Tweedie 손실은 긴 꼬리가 있는 양수 값에 맞춘 분포 손실이다.

CREAD는 Kuaishou가 영상 시청 시간 예측을 위해 발표한 방법이다(CREAD: A Classification-Restoration Framework with Error Adaptive Discretization for Watch Time Prediction in Video Recommender Systems, 2024-01, 논문). 이 방법은 시간을 직접 회귀하지 않고, 여러 임계값마다 "이 시간을 넘었는가"를 이진 분류로 학습한다. 그런 다음 확률들을 이어 붙여 기댓값을 복원한다.

  • 홈 기본 학습 config에서 Tweedie와 MAE 가중치가 0.0이 되고, CREAD 손실 두 개가 0.1로 켜졌다 확인 — phoenix/xrex/configs/xrecsys.py:272-275, 687-742.
    • 이 config는 _home_direct_packed_base()다.
    • 이전에는 Tweedie(power 1.5)와 MAE가 각각 0.1로 하드코딩돼 있었다.
  • 두 CREAD 손실은 지면별로 나뉜다 확인(같은 파일).
    • 일반 지면용은 상한이 120초이고, 갤러리 지면(PRODUCT_SURFACE_GALLERY_PAGE)을 제외한다.
    • 몰입형용(dwell-time-cread-immersive)은 상한이 240초이고, 갤러리 지면만 대상으로 한다.
    • 갤러리 지면은 사진·영상을 전체 화면으로 넘겨 보는 화면으로 보인다 추정. 근거는 메트릭 이름이 immersive이고, 체류 상한을 두 배로 잡았다는 점이다.
  • 임계값은 2초부터 상한까지 로그 간격으로 30개다 확인 — phoenix/xrex/models/loss_recsys.py cread_log_thresholds.
    • 상한이 120초이면 임계값은 2.0, 2.3, 2.65 … 90.5, 104.2, 120초가 된다.
  • 손실은 각 임계마다 sigmoid 이진 교차 엔트로피를 계산한다. 복원값은 "임계를 넘을 확률 × 구간 폭"의 합이고, 이 값과 실제 초의 Huber 손실(δ=5초, 가중 0.5)을 더한다 확인 — 같은 파일 cread_loss_compute, recsys_model.py _decode_cread.
  • 논문의 핵심 기여는 오차가 작아지도록 구간 경계를 데이터에 맞춰 정하는 것(error adaptive discretization)인데, X 구현은 고정 로그 간격을 쓴다 확인. X는 논문에서 이름과 분류-복원 구조만 가져온 셈이다.
  • 같은 임계 체계가 입력 피처에도 쓰인다. 유저 히스토리의 각 포스트에 과거 체류 시간을 버킷 임베딩으로 넣는 옵션이 기본 on이 됐다 확인 — models/recsys_feature_prep.py:180-184, 885-910, configs/xrecsys.py:301-304.
    • 옵션은 enable_dwell_bucket_table=True다.
    • 상한은 240초, 임계는 30개이고, 인접한 두 버킷 사이를 선형 보간한다.
    • 지금까지는 히스토리의 체류 시간이 어떤 형태로 입력되는지 정본에 기록하지 않았다.

새로 알게 된 것C. 후보 생성용 two-tower 모델에 Mixture-of-Logits 옵션이 생겼고, 기본값은 off다

two-tower retrieval은 유저 벡터 하나와 포스트 벡터 하나의 내적으로 수억 개 가운데 후보를 빠르게 찾는 구조다. 유저 관심이 여러 갈래일 때 벡터 하나로는 다 담기 어렵다는 한계가 알려져 있다. Mixture-of-Logits(MoL)는 유저 쪽과 아이템 쪽 벡터를 여러 개 두고, 그 조합 점수들을 작은 gate 네트워크로 가중합해서 이 한계를 줄이는 방법이다.

  • 유저 query head 수를 K로 두면, 유저 시퀀스 출력을 학습된 query 벡터 K개로 attention pooling해서 유저 벡터를 K개 만든다 확인 — phoenix/xrex/models/recsys_two_tower_model.py:1629-1665.
    • 설정은 user_query_heads=K다.
    • head마다 최근 행동에 더 무게를 두는 감쇠 계수가 따로 학습된다.
  • 포스트 성분 수를 L로 두면, 포스트 임베딩에 저랭크 어댑터를 적용해 성분 벡터 L개를 만든다. 최종 점수는 K×L개 코사인을 MLP gate(softmax)로 섞어 낸다 확인 — 같은 파일 523-598행 MolScorer.
    • 설정은 mol_item_components=L이다.
  • 서빙용 CUDA 커널도 함께 공개됐고, 지원 조합과 요청당 유저 수가 정해져 있다 확인 — phoenix/xrex/cuda/mol_epilogue/__init__.py:11-12.
    • 지원 조합은 (K×L, gate hidden) ∈ {(4,32), (8,16), (8,32), (16,16)}이다.
    • 요청당 유저는 512명까지다.
  • 학습 손실 옵션도 네 개가 추가됐다 확인 — recsys_two_tower_model.py:2194-2224.
    • 고정 temperature
    • 배치 안의 같은 포스트가 negative로 잡히는 false negative 마스킹
    • 유저가 부정 행동을 한 포스트에 대한 logit offset
    • positive 수 기반 유저 가중
  • 기본값은 모두 0 또는 False이고, 공개된 프리셋 6개 어디에서도 켜지 않는다 확인(configs 디렉터리 grep).
  • 프로덕션 retrieval이 이미 MoL인지는 이 코드로 판단할 수 없다 추정. 근거는 실험 config가 공개되지 않는다는 점이다. 다만 서빙 커널까지 만들어 둔 것은 실험 단계를 넘어 배포를 준비하는 신호로 읽힌다 추정.

새로 알게 된 것그 밖의 변경

  • topic 피드 요청은 신규 작성자 cold-start 승격에서 빠진다 확인 — home-mixer/scorers/author_cold_start.rs:384-386.
  • Semantic ID(포스트를 짧은 코드 열로 표현하는 식별자)의 앞 2자리·3자리 prefix를 임베딩으로 조회하는 함수가 추가됐다 확인 — phoenix/xrex/models/recsys_sid.py:32-79.
    • 이 함수가 모델에 연결됐는지는 확인하지 않았다.
  • 광고 블렌더가 탈락시킨 광고를 기록하는 경로(home-mixer/ads/drops.rs)가 생겼다. VF에는 345줄짜리 유저 위치 조회 클라이언트와 528줄짜리 hydration exchange.rs가 신설됐다 확인.
    • 위치 기반 판정이 새로 생긴 것인지는 아래 "다음에 볼 것"으로 넘긴다.

정본 반영 services/x.md

  • 피드 지면 절: 영상 캐러셀 모듈 항목(기본 off, 조건, 위치)을 추가했다.
  • 랭킹 절 가중치 표 아래: dwell_time 학습 손실이 CREAD로 바뀌었다는 단락을 추가했다.
  • "최종 배치는 점수 순이 아니다" 절: 캐러셀 삽입 위치와 블렌딩 순서를 추가했다.
  • ### 4세대+38 절을 신설했다. 열린 질문 절에는 심층 분석 항목 4개를, 출처 절에는 기준 커밋 35650fb를 추가했다.
  • assets/x-pipeline.svg의 비유기 소스 상자 부제에 "영상 캐러셀(off)"을 추가했다.

의미

  • 지금까지 For You의 모든 포스트 슬롯은 value model 가중합으로 순서가 정해졌다. 캐러셀은 video_open 단일 헤드 확률로 채운다 확인. 본 피드 랭킹과 별개의 목적 함수로 슬롯을 채우는 첫 포스트 모듈이다 추정. 기존 WTF·프롬프트 모듈은 포스트 랭킹을 쓰지 않았기 때문이다. 이 모듈은 본 랭킹에서 아깝게 탈락한 세로 영상을 별도 목적 함수로 다시 노출하는 구조다. Reels·Shorts식 세로 영상 소비를 피드 안으로 끌어오려는 시도로 보인다 추정. 근거는 정본이 기록한 비디오 전용 Thunder 경로(is_video_request)와 방향이 같다는 점이다. Thunder는 유저가 팔로우한 계정(인네트워크)의 최신 포스트를 모으는 후보 소스다.
  • 체류 시간 헤드의 학습 방식 변경은 기본 config에 들어갔으므로 다음 체크포인트부터 적용될 가능성이 높다 추정. 기본값 변경이 실제 프로덕션 학습 config에 반영되는지는 공개되지 않으므로 추정으로 둔다. 이 변경은 click_dwell_time을 회귀에서 10초 임계 이진 분류로 바꾼 커밋(4세대+22)과 같은 방향이다. 회귀를 분류 문제로 바꿔 꼬리 분포에 강하게 만드는 흐름이 dwell_time까지 확장됐다 추정.

다음에 볼 것

소스 점검 메모

Meta, Netflix, arXiv cs.IR 어디에서도 새로 반영할 자료는 나오지 않았다.

  • Meta: engineering.fb.com의 최신 글은 여전히 2026-10-06의 "NTS: Authenticated Time at Meta"이고, 피드와는 무관하다. ai.meta.com/blog 첫 페이지의 최신 글은 2026-07로, 신규가 없다. WebSearch 교차 확인에서도 Instagram·Threads·Facebook 랭킹 관련 새 1차 자료는 없었다.
  • Netflix: netflixtechblog.com RSS의 최신 글은 2026-09-25로 신규가 없다. research.netflix.com의 추천 영역 페이지도 열렸고, 신규는 없다.
  • arXiv cs.IR: 30건을 파싱했고, 수용 기준을 통과한 것은 0건이다. 근접 탈락은 두 건이다.
    • 2610.10483은 two-level softmax 샘플링 편향 교정을 다룬 NeurIPS 2026 논문이다. 프로덕션 근거가 없다.
    • 2610.10124는 생성형 추천에서 놓친 target의 학습 방식을 다루고, 공개된 OneRec 모델로 실험했다. 플랫폼 저자나 배포 주장이 없다.