0x00

리서치 / 일일 리포트

  • 읽은 자료 1건
  • 정본 반영 1개 파일
  • 레이더 0건
한 줄 요약X가 아이템의 semantic ID(SID)를 생성해 후보를 뽑는 retrieval 모델과, 그 서비스를 부르는 For You 후보 소스를 함께 공개했다. 소스는 기본으로 꺼져 있다.

1SID를 생성해 후보를 뽑는 retrieval이 For You 후보 소스로 연결됐다 — X

자료 xai-org/x-algorithm@76843a5 — xai-org, 2026-10-02T02:06:55Z, 코드. 이 노트들은 4세대 공개 뒤의 X 커밋에 차례로 번호를 붙여 부르며, 이 커밋은 4세대+34다. 규모는 +5227/−756, 파일 105개다.

배경 X의 추천 스택은 지난 넉 달 동안 Phoenix(2026년 재구축한 GPU 랭킹 스택)를 중심으로 재편돼 왔다. 그 안에서 "retrieval"은 두 갈래로 나뉘어 있었다.

  • 하나는 two-tower retrieval이다. 유저 타워와 아이템 타워의 임베딩 내적으로 ANN 검색을 돌리는 전통적인 OON(out-of-network, 팔로우하지 않은 계정의 포스트) 소스다. 2026-09-30 커밋에서 xrecsys_two_tower.py가 6-preset 카탈로그로 승격됐고, SID 6-level, seqpack, Home/Immersive 두 head 공유가 명시됐다.
  • 다른 하나는 generative retrieval(SID generation)이다. RQ-VAE나 비슷한 양자화로 아이템을 다단계 토큰 시퀀스(semantic ID, 줄여서 SID)로 압축한 다음, Transformer가 유저 히스토리를 조건으로 SID 토큰을 생성하는 방식이다. Google TIGER(NeurIPS 2023, arXiv 2305.05065)·Alibaba PailitaoGR(2025)·Kuaishou OneRec(2025)·Tencent BARGE(2026)·ByteDance GEAR(arXiv 2026-09-30 v1, 2026-10-02 daily) 같은 라인이 이쪽이다.

X는 그동안 SID 토큰을 학습 config의 sid_num_levels로 노출해 왔다. two-tower 모델이 아이템 타워의 입력 피처로 쓰는 형태였다. SID를 생성해 그 자체로 후보를 뽑는 generative retrieval 경로는 공개된 적이 없었다.

2026-09-04(4세대+15)에는 "Phoenix 서빙 엔진에서 SID retrieval 사이드카가 통째 제거"됐다는 기록이 있다. 당시 사이드카는 Phoenix 요청에서 유저 히스토리의 각 포스트 ID를 SID로 변환해 주는 lookup 역할이었고, 그 역할은 요청에 SID를 미리 실어 넣는 형태로 흡수됐다. 그때까지 X의 "SID retrieval"은 아이템 ID → SID 토큰 lookup이라는 좁은 의미였다.

이번 커밋은 그 좁은 의미를 완전히 바꾼다. 신규 파일 가운데 SID 관련 코드가 다섯 묶음이고, 다음 세 가지가 함께 들어왔다.

  • Phoenix 쪽에 SID generative retrieval 전용 Transformer의 학습 config, 모델, 트레이너, 인퍼런스 러너가 새로 생겼다.
  • home-mixer(For You 피드를 조립하는 서비스)에 이 서비스를 호출하는 SidSource 후보 소스가 추가됐다.
  • 이 소스는 응답 후보에 shared_prefix_depth를 점수로 붙이고, ForYouPhoenixRetrievalMoe로 라벨해 흘려보낸다.

새로 알게 된 것A. home-mixer가 별도 SID retrieval 서비스를 부르는 후보 소스를 갖췄다

  • home-mixer에 새 후보 소스가 생겼고, 이 소스는 별도 서비스로 분리된 generative SID retrieval microservice를 호출한다 확인 — home-mixer/sources/sid_source.rs(신규 +112).
    • 소스 이름은 SidSource다.
    • 호출을 맡는 클라이언트는 home-mixer/clients/sid_retrieval_client.rs(신규 +246)다.
    • 호출 대상 endpoint는 https://xai-recsys-sid-retrieval.prod.fou.twitter.biz:443이다.
  • 요청은 seed 포스트 ID와 결과 상한, 접두사 깊이 범위를 담고, 응답은 후보마다 포스트 ID와 공유 접두사 깊이를 담는다 확인.
    • gRPC 서비스는 SidRetrievalServiceClient다.
    • 요청 타입은 RetrieveRequest { seed_post_ids, max_results, max_per_seed, min_prefix_depth, max_prefix_depth, caller }다.
    • 응답 타입은 RetrieveResponse { candidates: Vec<Candidate> }이고, 각 Candidate에 post_id·shared_prefix_depth가 담긴다.
  • 이 proto 심볼은 레포 밖에서 들어오고, 레포에는 SID lookup proto만 담은 별도 crate가 함께 공개됐다 확인.
    • 심볼은 xai_recsys_sid_retrieval_proto crate가 외부(비공개 혹은 생성물)에서 가져온다.
    • 함께 공개된 별도 crate xai-recsys-sid-proto에는 sid_lookup.proto만 들어 있다.
    • sid_lookup.proto는 post_id → SID 코드 배열 매핑만 다룬다. 2026-09-04에 사라졌던 lookup 사이드카의 복귀다.
      • 정의는 SidLookupService { LookupSids(repeated int64 post_ids) → repeated PostSids { repeated int32 codes } }다.
    • generative retrieval 쪽 proto는 레포에 포함되지 않았다.
  • 이는 레포 안에서 보이는 생산 코드와 그 "generative retrieval" 전용 proto 정의가 분리된 상태라는 뜻이다 추정.
  • SidSource가 켜지는 조건은 다섯 조건의 AND로, 아래가 모두 참이어야 한다 확인 — sid_source.rs:58-64.
    • EnableSidSource param이 켜져 있다(기본 false).
    • 토픽 요청이 아니다.
    • in-network only 요청이 아니다.
    • cached posts가 없다(cold path).
    • has_post_signals(query)가 true다.
    • 마지막 조건은 SimclustersSource와 완전히 같은 함수를 재사용해, 유저의 포지티브 engagement 신호가 있어야 소스가 돌게 한다는 뜻이다.
    • 두 소스가 같은 seed 집합(유저가 fav/retweet/reply 등으로 상호작용한 포스트 ID)에서 분기한다는 설계가 선언돼 있다.
  • 소스는 유저가 반응한 포스트를 seed로 SID 서비스를 부르고, 받은 후보에 기존 Phoenix MoE(mixture-of-experts, 전문가 혼합) 소스와 같은 라벨을 붙인다 확인.
    • 먼저 post_signal_ids(query)에서 유저의 positive engagement 포스트 ID들을 꺼내고, SidSourceMaxSeeds 상한(기본 50)으로 자른다.
    • RetrieveRequest를 조립해 gRPC로 호출한다. 데드라인 300ms는 sid_retrieval_client.rs:21에 하드코딩돼 있다.
    • 응답 Candidate는 각각 PostCandidate로 바뀐다. 이때 served_type = ForYouPhoenixRetrievalMoe로 표시하는데, 기존 PhoenixMOESource와 같은 라벨이다.
    • shared_prefix_depth는 score 필드에 f32로 들어간다.
    • 마지막으로 CoreDataCandidateHydrator로 작성자·생성 시각 등을 채운 뒤, author_id == 0인 후보는 제거한다.
  • 파라미터 다섯 개가 rust_home_mixer_sid_source_* 이름으로 새로 생겼고, 기본값이 공개됐다 확인 — home-mixer/params/param.rs. 이 파일은 프로덕션 feature-switch 기본값의 크론 미러다.
    • EnableSidSource=false
    • SidSourceMaxResults=800
    • SidSourceMaxSeeds=50
    • SidSourceMaxPerSeed=100
    • SidSourceMinPrefixDepth=3
    • max_prefix_depth: 0은 RetrieveRequest에 하드코딩돼 있다(상한 없음).
    • min_prefix_depth=3은 공유 접두사가 6-level SID 중 3개 이상 일치하는 후보만 반환하라는 요청 쪽 필터다.
    • seed마다 최대 100개 포스트, 전체 상한은 800개다. 50 × 100 = 5000이 이론적 상한이지만 800으로 다시 조여진다.
  • gRPC 클라이언트는 두 경로를 병행한다: xDS 경로를 먼저 세우고, 안 되면 DNS 경로로 폴백한다 확인.
    • xDS P2C로 로드밸런서를 1차로 세운다. 이 경로는 discovery authority 기반의 xdstp://discovery-*/envoy.config.endpoint.v3.ClusterLoadAssignment/fou-xai-recsys-sid-retrieval.prod.prod:grpc-tls를 쓴다.
    • xDS 빌드가 10초 안에 끝나지 못하거나 bootstrap이 없으면 DNS 경로(RotatingChannel, 60초 간격 refresh)로 폴백한다.
    • 응답 크기 상한은 MAX_DECODING_MESSAGE_BYTES = 16 MiB다.
    • 메트릭 SidRetrievalClient.retrieve{result=success|timeout|failure, path=xds|dns}가 호출 결과와 경로를 라벨로 남긴다. 호출 지연은 SidRetrievalClient.retrieve{path} 히스토그램에 담긴다.
    • 서비스 자체의 readiness probe는 port 29180의 GET /readyz다.
    • keep-alive는 30초 간격·10초 데드라인·idle=true로 설정된다.
  • 소스 쪽 메트릭은 seed 수, RPC 응답 수, 하이드레이션 뒤 남은 수의 세 단계를 각각 측정한다 확인.
    • 메트릭은 SidSource.retrieved{type=seeds|retrieved|hydrated}다.
    • seed 수는 RPC에 보낸 양, retrieved 수는 RPC 응답, hydrated 수는 author_id!=0 생존자다.

새로 알게 된 것B. Phoenix에 SID를 생성하는 전용 Transformer 모델의 학습·서빙 코드가 공개됐다

  • Phoenix에 SID generative retrieval 모델의 학습 config, 모델, 트레이너, 인퍼런스 러너 파일이 새로 생겼다 확인 — phoenix/xrex/configs/xrecsys_sid_retrieval.py(신규).
  • 학습 config xrecsys_sid_retrieval.py가 모델의 하이퍼파라미터를 전부 공개한다 확인 — xrecsys_sid_retrieval.py:11-45.
    • 입력 시퀀스 길이는 1030이다. 유저 prefix 1토큰, 유저 액션 히스토리 1023토큰, SID 타겟 6토큰을 더한 값이다. 캔디데이트 시퀀스는 64다.
    • SID는 6-level 계층이고, 레벨마다 256개 코드를 쓴다. 이론상 SID 공간은 $256^6 ≈ 2.8 \times 10^{14}$ 개다.
      • sid_num_levels=6, sid_codebook_size=256
      • sid_embed_dim=1024(= EMB_TABLE_WIDTH), sid_prediction_hidden_dim=512
      • sid_level_loss_decay=1.0은 레벨별 loss 가중치 감쇠가 없다는 뜻이다. 즉 6개 레벨 전부 같은 가중치를 받는다.
      • sid_hash_level=True, sid_cross_attn=True
    • Transformer는 8-layer이고, GQA(grouped-query attention)를 5:1 비율로 쓴다.
      • query_heads=20·kv_heads=4, key_size=128, emb_size=2560
      • attn_logit_cap=80.0에 attn_logit_cap_method="soft_sign", rotary, causal
      • widening_factor=2에 gated_mlp=False(일반 MLP), remat WHOLE
    • 입력 vocab 수치는 2026-09-30(4세대+32)에 공개된 two-tower config의 수치와 정확히 일치한다. 두 모델이 동일한 vocab 공간을 공유한다는 뜻이다.
      • 유저 vocab=0, 아이템 raw vocab=0, 아이템 hash vocab=100M, 작성자 vocab=30M, 액션 타입 60개, continuous action 8개다.
      • _round_up(..., 512)로 512의 배수에 패딩한다.
      • user/item/author 각각에 두 쌍의 해시 함수(linear scale × modulus prime)로 double hashing을 한다.
    • 학습은 Adam optimizer와 상수 learning rate를 쓰고, data-parallel 없이 expert-parallel로 나눈다.
      • Adam optimizer(b1=0.95, b2=0.98), weight_decay=1e-3, 상수 learning rate 2e-3
      • batch size per device = 128, parallel 설정은 ep=128, dp=1(128-way expert-parallel, data-parallel 없음)
      • max_steps = (1e11 / 32) − 100 ≈ 3.125B 스텝
    • 데이터셋은 aggregated_kafka 모드이고, 경로는 필러 값이다.
      • PhoenixDataset에 path="/path/to/offline_kafka_dump"(필러), num_kafka_partitions=1024
      • include_candidate_post_ids=True, use_post_sid=True, multimodal_embedding_type=None
    • 체크포인트 설정도 공개됐다.
      • checkpoint_every_n=100·checkpoint_keep_every_nth=1000·checkpoint_keep_last_n=10·verify_checksums=True
      • replica_axis="replica"·checkpoint_chunked=False·checkpoint_compressed=False
HISTORY_SEQ_LEN = 1023
CANDIDATE_SEQ_LEN = 64
NUM_SID_TOKENS = 6
NUM_USER_PREFIX = 1
EMB_SIZE = 2560
EMB_TABLE_WIDTH = 1024
NUM_LAYERS = 8
QUERY_HEADS = 20
KV_HEADS = 4
# ...
ITEM_HASH_VOCAB_SIZE = 100_000_000
AUTHOR_VOCAB_SIZE = 30_000_000
# ...
sid_codebook_size=256,
sid_num_levels=6,

— phoenix/xrex/configs/xrecsys_sid_retrieval.py:11-40, 150, 2026-10-02

  • 모델 코드는 SID 토큰 임베딩을 레벨마다 별도 테이블로 두고, 레벨마다 프로젝션을 거쳐 1024-dim으로 올리는 구조로 보인다 확인 — recsys_sid_retrieval_model.py:72-100.
    • 관련 이름은 Haiku transformed 함수 _embed_sid_token(code, level), 레벨별 테이블 sid_emb_level_{level}(크기 [256, 128])이다.
    • 레벨 뒤 프로젝션의 이름은 sid_emb_proj_{level}이다.
  • 모델의 positive action은 SERVER_TWEET_FAV 단 하나다 확인 — line 40. fav 하나로 좁혀진 포지티브다.
    • hard-negative action 7개는 negative supervision에 쓰인다.
      • 목록은 CLIENT_TWEET_REPORT, _NOT_INTERESTED_IN, _SEE_FEWER, _UNFOLLOW_AUTHOR, _BLOCK_AUTHOR, _MUTE_AUTHOR, _NOT_RELEVANT다.
  • 서빙 쪽 beam decoder의 평가 조건은 eval_num_batches=20·eval_beam_width=128·eval_bs_per_device=8이다 확인.
  • 인퍼런스 러너는 beam search로 SID를 생성하는 단계와, 생성된 SID 접두사를 실제 post_id 집합으로 되돌리는 단계를 나눠 담는다 확인 — sid_retrieval_runner.py:21-80.
    • 러너 파일은 sid_retrieval_runner.py이고, 되돌리는 단계는 SidPostIndex/SidBeamResolver가 맡는다.
    • 러너의 하이퍼파라미터 중 beam_width·decode_levels가 플래그다. decode_levels < 6이면 접두사 수준에서 생성을 멈추고 접두사 공유 집합을 반환한다(prefix retrieval).
    • 이것이 home-mixer SidSourceMinPrefixDepth=3과 짝을 이룬다. 서버는 3~6단계까지 생성하되, 3단계에서 매칭된 모든 post는 접두사 깊이 3을 score로 받아 반환될 수 있다.
    • _CorpusView.sentinel_idx·post_ids_with_sentinel·author_ids_with_sentinel 같은 필드는 코퍼스가 사전 빌드된 인덱스(SID → posts) 형태로 로드돼 있다는 걸 보여 준다.
    • 즉 서빙 시점의 코퍼스는 "SID 트라이 + leaf에 담긴 post_id 집합"이고, beam search가 트라이 위에서 top-K 경로를 뽑으면 그 경로 아래 posts가 후보가 된다 추정. 코드에서 SidBeamResolver의 세부 구조는 다른 파일(xrex/inference/sid_post_index.py)에 있고, 이번 커밋 diff에는 나오지 않았다.

이 설계는 공개된 generative retrieval 계열과 겹친다. Google TIGER(NeurIPS 2023)·Alibaba PailitaoGR(2025)·Kuaishou OneRec(2025)·Tencent BARGE(2026)·ByteDance GEAR(2026-09-30)가 그 계열이다. X판은 다음 점이 다르다.

  • 6-level × 256-codebook이라는 비교적 좁은 공간을 쓴다(TIGER는 3-level × 256에 충돌 구분 토큰 1개, OneRec는 3-level × 1024).
  • 1023토큰 유저 액션 히스토리를 그대로 조건으로 넣는다(TIGER는 학습 때 유저 히스토리를 최대 20개로 제한).
  • shared_prefix_depth를 상위 랭킹의 스코어로 넘긴다(TIGER는 생성 확률을 쓰는 쪽이다).
  • vocab 설계를 two-tower 모델과 공유한다. 다만 임베딩 테이블은 지금 모델마다 따로 있다(아래 의미 참조).

two-tower는 벡터 공간 ANN을, SID retrieval은 코드 공간 beam search를 돌린다. 둘은 duplication이 아니라 상보 구조로 선언된 셈이다.

새로 알게 된 것C. Phoenix에 임베딩 테이블을 행 단위로 나눠 다루는 CUDA 커널 묶음이 생겼다

  • 행(row) 단위로 샤드한 임베딩 테이블을 다루는 CUDA 커널 묶음이 새로 생겼다 확인 — phoenix/xrex/cuda/row_emb/.
    • 12-파일 묶음이고, 총 3,180줄이 신규다.
    • row_emb_api.cc(+766) · row_emb_comm.cc(+1,104) · row_emb_kernel.cu(+629) · row_emb.py(+290) · row_emb_comm.hpp(+256) · comm_utils.cc(+146)
    • row_emb_kernel.hpp(+135) · row_emb_common.cuh(+82) · cuda_error_utils.hpp(+68) · comm_utils.h(+57) · comm_utils.py(+47) · __init__.py(+2)
  • Phoenix의 기존 async_emb 커널이 column 방향으로 샤드된 임베딩 테이블을 다룬다면, 새 row_emb는 row 방향으로 샤드된 테이블을 다룬다 확인.
    • vocab이 N개면 N / group_size개 row가 각 rank에 올라가는 구조다.
  • 이 설계는 vocab이 수억 규모(아이템 100M · 작성자 30M)로 커져 임베딩 테이블이 GPU 수십~수백 장에 걸쳐야 할 때 중요해진다 추정. 각 토큰 lookup은 자기 소속 rank에만 질의하면 되고, all-to-all은 토큰을 lookup rank로 보낼 때 발생한다 추정.
  • Python 바인딩 row_emb.py는 JAX FFI target 6개를 CUDA 플랫폼에 등록하고, shard spec을 NamedTuple 하나에 묶어 넘긴다 확인 — row_emb.py:30-80, 100-290.
    • FFI target의 이름은 아래와 같다.
      • xrex_row_emb_lookup_start, _lookup_done, _stage_update
      • _rowwise_adagrad_update_start, _rowwise_adagrad_lazy_update_start, _rowwise_adagrad_update_done
    • NamedTuple은 RowEmbContextHandle이고, 아래 값을 묶는다.
      • context_id · group_size · flatten_replicas · tokens_per_rank · emb_width · rows_per_rank
      • recv_capacity · num_devices_per_node · mesh · table_axis · data_axis

핵심 설계 결정은 3개다.

  1. lookup이 두 단계로 나뉜다. 두 FFI 사이에 다른 연산이 돌 수 있는 창이 열리고, all-to-all 통신이 그 창 안에서 연산과 겹친다. Pathways/MegaScale 같은 패턴이다.
    • lookup_start는 token_ids와 로컬 샤드 테이블을 받아 table_out과 lookup_pin(float32 scalar)을 리턴한다. lookup_done은 pin을 받아 embeddings를 리턴한다.
    • tokens_per_rank는 tokens_per_batch / group_size로 정해진다.
    • 수신 버퍼 상한은 recv_capacity = min(ceil(tokens_per_rank × recv_factor), tokens_per_rank × group_size)로 미리 잡힌다. recv_factor가 작으면 collision이 많을 때 drop이 생기고, 크면 버퍼가 낭비된다.
  2. rowwise Adagrad가 lookup과 같은 2-phase 구조로 fused됐다.
    • rowwise_adagrad_update_start는 테이블, row_state(각 row의 accumulator), gate를 받아 업데이트 중인 테이블 자체를 돌려준다. input_output_aliases={0:0, 1:1}로 in-place 갱신한다.
    • rowwise_adagrad_update_done은 완료 신호를 돌려준다.
    • rowwise_adagrad_lazy_update_start는 last_step과 step을 받아 접근된 row만 lazy update하는 변종이다. sparse embedding 테이블에 흔한 패턴이다.
  3. gate 입력으로 XLA 실행 순서를 보장한다.
    • 모든 FFI 함수가 gate: jax.Array를 입력으로 받고 float32 scalar를 출력에 섞어 넣는다. 이렇게 XLA가 자유롭게 재배열할 수 없는 side-effect 체인을 명시적으로 만든다.
    • attrs()로 context_id · flatten_replicas 같은 커뮤니케이터 식별자를 FFI attribute로 넘긴다. 같은 context_id면 같은 통신 그룹을 재사용한다.
  • 유효성 검사는 다음 조건을 확인한다.
    • vocab_rows % group_size == 0(row shard 균등 분배)
    • tokens_per_batch % group_size == 0(token shard 균등 분배)
    • emb_width % 8 == 0(8의 배수). bf16 tiling 요건으로 보인다 추정.
    • recv_factor > 0
    • table_axis 축이 반드시 data_axis 축에 포함돼야 한다. 포함되지 않으면 "token shard가 커뮤니케이터 랭크마다 1개가 아니다"는 ValueError를 던진다.
  • Phoenix의 async embedding 유틸이 두 커널을 모두 받아들이는 adapter 레이어로 재작성됐다 확인. 이제 column과 row 두 샤딩 전략을 동시에 지원한다.
    • 파일은 phoenix/xrex/utils/recsys_async_emb.py(+112/−36)이고, 받아들이는 커널은 async_emb와 row_emb다.
    • is_row_sharded(context_handle) 분기로 row와 column을 가른다. row-sharded면 table_spec을 P(table_axis, None)으로, column-sharded면 P(None, table_axis)로 돌려준다.
  • 이 커널 묶음은 SID retrieval 모델 전용이 아니라 Phoenix 임베딩 테이블 일반 설계의 확장이지만, 이번 커밋에 함께 공개된 맥락에서 보면 row-sharded 임베딩이 필요한 모델(즉 vocab이 매우 큰 모델)의 등장이 트리거로 보인다 추정.
    • two-tower는 column-sharded로도 돌 수 있지만, generative 모델은 lookup 패턴상 row-sharded가 더 자연스럽다. 각 토큰은 자기 vocab 레벨의 특정 range만 참조하기 때문이다.

새로 알게 된 것D. Phoenix proto에 LLM judge 액션이 돌아오고, 광고와 retrieval 요청 필드가 늘었다

  • Phoenix의 recsys proto가 바뀌었고, 같은 변경이 Python 쪽 proto에도 미러됐다 확인 — phoenix/crates/serving/xai-recsys-proto/proto/recsys.proto.
    • 미러 파일은 phoenix/python/common/xai-proto/proto/recsys.proto이고, 두 파일 모두 각각 +68/−44다.
  • LLM judge 액션 두 개가 15일 만에 번호를 바꿔 다시 나타났다 확인.
    • 이번 값은 LLM_JUDGE_RELEVANT_TO_SEARCH=108·LLM_JUDGE_NOT_RELEVANT_TO_SEARCH=109다.
    • 두 action은 4세대+20(2026-09-12)에 107/108로 신설됐다.
    • 4세대+23(2026-09-17)에 5일 만에 vacate됐다. 그 자리에 CLIENT_TWEET_LIVE_STREAM_REPLY=107이 들어왔고, 108은 RESERVED_108 플레이스홀더로 보존됐다.
    • 오늘(4세대+34, 15일 만에) 재등장했다. 다만 번호가 바뀌었다: 과거 107/108 → 이번 108/109.
    • RESERVED_108이 LLM_JUDGE_RELEVANT_TO_SEARCH로 재사용되고, 새 번호 109가 LLM_JUDGE_NOT_RELEVANT_TO_SEARCH로 할당됐다.
    • 즉 LLM judge 라벨을 action으로 다시 끌어들이려는 결정이 되돌아왔지만, 번호는 새 자리로 간 셈이다. 이것이 재조정 후 안정 상태가 될지, 다시 vacate될지는 다음 커밋 창에서 관측해야 한다 추정.
  • continuous action 두 개가 새로 생겼다 확인.
    • 새 ContinuousActionName은 WEB_CONV_VALUE_USD=6·WEB_CONV_VALUE_RATIO=7이다.
    • 기존 continuous action 6개(ACTIVE_SECS 등과 BRIDGE_PROBABILITY=4, ADS_WEB_CT_PURCHASE_VALUE=5)에 두 개가 추가됐다.
  • 광고 전환 가치를 USD 절대값과 비율(아마 baseline 대비 ratio) 두 갈래로 나눠 continuous head가 소비한다 추정. 이번 커밋에 continuous loss 쪽 코드 변경은 없고, proto만 미리 자리를 잡은 상태다.
  • 광고 정보 메시지 AdInfo에 뷰어×광고주 단위의 필드 두 개가 추가됐다 확인.
    • 필드 24는 viewer_advertiser_engagement_counts, 필드 25는 viewer_advertiser_conversion_counts다.
    • 둘 다 기존 EngagementCounts 메시지 타입이지만, 의미상 뷰어×광고주 사이의 engagement/conversion 히스토리를 담는 새 슬롯이다.
    • 광고 랭킹 모델이 "이 뷰어가 이 광고주의 이전 광고에 어떻게 반응했나"라는 피처를 받을 수 있게 된다.
  • engagement count 메시지가 어떤 action 종류를 집계했는지도 함께 실어 보내게 됐다 확인.
    • EngagementCounts에 collected_actions(필드 6, repeated int32)가 추가됐다.
    • 다운스트림이 "이 counts가 fav만 집계한 건지, fav+reply까지 포함한 건지" 알 수 있게 된다.
  • two-tower retrieval 요청도 seed 포스트와 제외 목록을 받게 됐다 확인.
    • RetrieveTopKCandidatesRequest가 세 필드로 확장됐다.
    • 필드 14 seed_post_ids는 기존 SidRetrieval의 RetrieveRequest와 다른 메시지다. two-tower retrieval 쪽도 이제 seed post ID를 요청에 실을 수 있다.
    • 필드 15는 excluded_posts_bloom_filters(repeated ExcludedPostsBloomFilter { bit_array: repeated fixed64, size_cap, false_positive_rate })다.
    • 필드 16은 excluded_post_ids(repeated uint64)다.
    • Phoenix two-tower retrieval도 이제 seed 조건부 검색과 이미 본 포스트의 bloom-filter exclusion을 받는다는 뜻이다. SidSource와 two-tower retrieval이 같은 seed를 공유하는 쪽으로 설계가 수렴한다.
  • 광고 창의 unified card 포맷을 proto 수준에서 열거하는 enum UnifiedCardType(29-variant)이 새로 생겼다 확인.
    • 값은 IMAGE_WEBSITE · VIDEO_WEBSITE · IMAGE_APP · ... · VIDEO_PLAYABLE_WEBSITE · IMAGE_COLLECTION_WEBSITE 같은 것이다.
    • 코드 쪽 소비자는 이번 diff에 나오지 않고, proto 선언만 있다.
  • 광고 ranking 모델이 unified card 포맷 자체를 범주형 피처로 받을 자리를 미리 잡은 것으로 보인다 추정.
  • FetchAggregatedUserActionRequest는 proto 안의 위치가 바뀌었다(파일 순서 재배치만). 의미 변화는 없다.

새로 알게 된 것E. 노출 판정 서비스(VF)가 호출자별 요청 메트릭에 처리 결과를 함께 기록한다

  • VF(visibility-filtering)의 호출자별 요청 메트릭에 네 번째 라벨이 추가됐다 확인 — visibility-filtering/caller_identity.rs +90/−15.
    • 바뀐 파일은 VF의 caller_identity.rs다.
    • 대상 메트릭은 어제(4세대+33) 신설된 vf_requests_by_caller{caller_identity, rpc, deadline}이고, 여기에 outcome이 네 번째 라벨로 붙었다.
    • 새 라벨 outcome은 요청이 성공, 실패, 취소 가운데 어떻게 끝났는지를 담는다.
    • Outcome { Success, Failure, Cancelled } 3-variant enum이 신설됐다.
  • 핸들러가 성공이나 실패를 표시하지 않고 끝낸 요청은 Cancelled로 집계된다 확인.
    • record() 함수가 이제 CallerRequest 가드 구조체를 리턴한다(must_use로 annotation).
    • 핸들러가 요청 처리 중 request.mark_success() 또는 .mark_failure()를 호출하지 않고 그대로 drop되면 Cancelled로 집계된다.
    • Drop impl이 메트릭을 발행한다. RAII 패턴이다.
  • 새 테스트가 대시보드 생성 Python 스크립트와 Rust 쪽 상수를 함께 고정한다 확인.
    • 테스트 이름은 dashboard_generator_pins_the_requests_by_caller_metric다.
    • 스크립트가 REQUESTS_BY_CALLER_METRIC = "vf_requests_by_caller"·REQUESTS_BY_CALLER_NO_DEADLINE_FILTER = 'deadline="absent"'·success/non-success filter 네 개 상수를 코드에 담고 있어야 Rust 테스트가 통과한다.
    • 메트릭 정의가 Rust 코드와 대시보드 코드 양쪽에서 동시에 drift 체크되는 설계다. 어제의 _drift_checks 메트릭이 UI 번들 drift를 보던 것과 비슷한 패턴이다.

새로 알게 된 것F. home-mixer가 cold-start 관련 분기 두 개를 걷어 내고, 작성자 룰 평가를 공용 crate로 옮겼다

  1. Control arm의 cold-start 부스트가 이제 retrieval 소스와 관계없이 Control corpus의 모든 포스트를 대상으로 삼는다 확인(author_cold_start.rs).
    • is_arm_gated_retrieval(Phoenix MoE/Cold retrieval 소스 판정) 함수가 삭제됐다.
    • 과거 Control arm은 Control corpus인 포스트 중에서도 Phoenix MoE/Cold retrieval 출신을 cold-start 부스트 대상에서 제외했다.
    • 테스트 cold_retrieval_keeps_its_score_in_every_arm가 삭제되고, 새 테스트 control_viewer_lifts_cold_retrieval_from_control_corpus가 그 자리를 대체한다. 새 테스트는 "Control 뷰어가 cold retrieval 후보도 Control corpus 소속이면 들어 올린다"를 명시한다.
    • 메트릭 home_mixer.cold_started_posts_total의 is_moe 라벨도 제거됐다. Phoenix MoE/Cold retrieval인지 여부는 더 이상 추적되지 않는다.
    • 어제까지 cold-start 부스트는 Phoenix retrieval을 arm × corpus × 소스의 3축으로 보고 소스 축에서 걸러냈는데, 이제 arm × corpus 2축으로 좁혀진다.
  2. cached posts가 있을 때 engagement count를 이제 모든 후보에 대해 조건 없이 다시 가져온다 확인(engagement_counts_hydrator.rs).
    • EngagementCountsHydrator에서 cached_posts 상황의 cold-start eligibility 분기가 제거됐다.
    • 과거 has_cached_posts일 때는 cold_start_base_eligible을 통과한 후보만 engagement count를 다시 fetch하고, 나머지는 preserve_counts로 cached 값을 유지했다. 이제 모든 후보를 unconditional로 재fetch한다.
    • preserve_counts helper가 삭제됐다.
    • 새 테스트 cached_posts_refetch_all_candidates는 cold-start eligibility와 무관하게 세 후보 전부가 새 counts로 교체됨을 고정한다.
    • 호출 경로에서 ColdStartFollowerCap·cold_start_base_eligible 의존성이 둘 다 걷혔다.
    • engagement counts와 cold-start 사이의 상호의존이 끊어졌다. 뒤에 오는 cold-start 로직은 engagement count가 "최신"임을 전제할 수 있고, 하이드레이터 쪽은 더 단순해졌다.
  3. home-mixer 안에 있던 작성자 룰 평가 유틸이 공용 crate로 올라갔다 확인(author_rules.rs).
    • home-mixer/util/author_rules.rs(156줄)가 삭제되고, 심볼은 xai_feature_switches::AuthorRulesEvaluator로 이동했다. 리팩터다.
    • moka cache 사이즈 500k · TTL 10분 · 64 shard 구성이 공용 crate 쪽에 복제됐을 것으로 보인다 추정. 그 구현은 이번 diff에 노출되지 않았다.

새로 알게 된 것그 밖의 변경

  • VF의 필터 코드에는 테스트 두 개만 추가됐고, 정책 변화는 없다.
    • 파일은 filter.rs(+94/−2)다.
    • 추가된 테스트는 pure_core_alone_says_a_post_is_a_retweet, without_a_pure_core_answer_a_post_reads_as_an_original이다.
    • 둘 다 PureCore(트윗 코어 메타) hydration이 실패하거나 성공할 때 viewer_mutes_retweets 규칙과 nullcasted_tweet 규칙이 각각 어떤 fallback으로 결론을 내는지를 고정한다. 기존 fault 처리 behavior에 lock을 추가한 테스트다.
  • VF 파일 세 개의 변경은 어제의 Fetcher<V> 재작성 후속으로 보이는 소폭 리팩터다 추정. 테스트 추가 위주다.
    • hydration/execute.rs(+68/−2), hydration/tweet_source.rs(+8/−14), safety_label_source/metrics.rs(+29/−7)
  • VF visibility-filtering-client/vf_client.rs(+1/−35)와 test_support.rs(+37/−0)에서는 테스트 fixture를 별도 파일로 분리했다. 로직 변화는 없다.
  • Phoenix python/training/xai-checkpointing/xai_checkpointing/kms_client.py(+74/−0, 신규)는 KMS 접근 공용 wrapper다. 피드 랭킹에 직결되는 영향은 없다.
  • Phoenix trainer_recsys.py(+164/−39)는 공용 트레이너에서 SID retrieval 전용 Trainer가 상속할 부분을 분리한 것으로 보인다 추정.
  • Phoenix rowwise_adagrad.py(+14/−9)는 row_emb 쪽 fused 커널과 python-level 참조 adapter 변경이다.
  • abuse-enforcement-service/service-lib/src/overturn_hold.rs(+22/−22)는 어제 공개된 overturn hold 모듈의 테스트 보강이다 추정.
  • botmaker/src/java/com/twitter/botmaker/compiler/thrift/TType.java(+14/−14)는 thrift 컴파일러 유지보수다.
  • thunder/posts/post_store.rs(+20/−20)는 테스트 리네이밍 수준이다 추정.
  • phoenix-rankall/src/store/base.rs(+12/−12)는 SID 백필 저장소 어댑터의 미세 조정이다 추정.
  • visibility-filtering-client/tweet_safety_label.rs(+39/−12)는 thrift decoder 테스트 확장이다 추정.
  • home-mixer/params/param.rs(+31/−1)의 증분은 위 5개 SID source param 추가로 인한 것이다. cron sync 시각이 바뀌지 않았다면 유의미한 다른 변화는 없다.

정본 반영 services/x.md의 공개 소스 4세대 섹션에 "### 4세대+34 (2026-10-02, 76843a5)" 소절을 새로 만들었다. ## 후보 생성 (retrieval) 절의 retrieval 소스 표에는 SidSource 행을 추가하고, "SID generative retrieval microservice. seed post ID → 6-level × 256-codebook SID beam search → shared_prefix_depth score, served_type=ForYouPhoenixRetrievalMoe로 라벨, EnableSidSource=false 기본 off"로 적었다.

의미 SID를 직접 생성해 후보로 돌려주는 경로가 X 코드에서 처음 공개됐다. 다만 기본값이 off이므로, 실 트래픽 적용의 관측 지점은 FeatureSwitches cron에서 켜지는 날이다. 이번 커밋에서는 세 겹의 사실이 겹친다.

  1. X가 generative SID retrieval을 "별도 microservice + 전용 Transformer 학습·서빙 스택 + home-mixer 후보 소스 배선"의 완결된 세 축으로 공개한 첫 커밋이다.
    • 지금까지 X의 SID는 두 곳에서 간접적으로 모습을 비쳤다. 하나는 two-tower 아이템 타워의 입력 피처(2026-09-30 공개된 xrecsys_two_tower.py의 use_post_sid 플래그)다. 다른 하나는 phoenix-rankall의 백필 QPS 800k 상한(어제 공개된 SID_BACKFILL_QPS_CAP = 800_000)이다.
    • SID를 직접 생성해서 그 결과를 candidate로 리턴하는 라인은 공개되지 않았다.
    • 오늘 커밋은 아래 체인 전체를 한 번에 공개한다.
      • Phoenix 쪽에 전용 Transformer 모델(8-layer, GQA 20:4, 1030-length context, 6-level × 256-codebook)의 config/모델/트레이너/러너 네 파일을 신설했다.
      • 별도 microservice xai-recsys-sid-retrieval.prod.fou.twitter.biz의 gRPC client와 request shape(seed_post_ids, shared_prefix_depth scoring, prefix retrieval 조건 min_prefix_depth=3)을 공개했다.
      • home-mixer SidSource로 PhoenixMOESource와 같은 ServedType으로 라벨된 후보를 흘려보낸다.
    • 이 세 부분이 각기 별개 커밋으로 흩어졌다면 "준비 중" 신호였겠지만, 한 커밋에 묶여 공개된 사실은 연결이 완성에 가깝다는 뜻이다.
    • 다만 EnableSidSource=false가 기본(off)이므로, 실 트래픽 적용은 FeatureSwitches cron에서 ON 되는 날이 관측 지점이다.
  2. two-tower retrieval과 SID generative retrieval은 "서로 대체"가 아니라 "동일 vocab 공간 위의 두 레이어"로 설계됐다.
    • 두 모델은 같은 유저·아이템·작성자 hash vocab(100M · 30M · double hashing scales/biases 똑같음)을 공유한다. xrecsys_sid_retrieval.py:30-45와 2026-09-30 공개된 xrecsys_two_tower.py의 수치를 대조하면 드러난다.
    • generative retrieval은 아래 두 가지 비용이 큰데, two-tower가 ANN으로 돌릴 수 있는 broad recall을 먼저 맡고 SID retrieval이 접두사 공유도로 보강 recall을 담당하는 상보 구조를 고른 것으로 보인다 추정.
      • 하나는 SID 생성 시간 복잡도다(beam_width × decode_levels × 코드북 크기).
      • 다른 하나는 코퍼스 쪽 SID 인덱스를 유지하는 비용이다.
    • 같은 vocab이면 양쪽 모델이 공유 임베딩 테이블로 돌 수도 있다. 그러나 지금 공개된 config에서는 양쪽 모델이 각자 임베딩 테이블을 가진다. 공유 임베딩으로 통합되는지는 다음 커밋에서 관측해야 한다.
  3. row-sharded 임베딩 CUDA 커널 묶음은 Phoenix 임베딩 설계 자체의 2축 확장이다.
    • 3,180줄짜리 row_emb가 generative retrieval 모델만을 위해 추가된 것은 아니다. 하지만 이번 커밋 묶음의 맥락에서 보면, SID generative retrieval이 요구하는 거대 vocab 임베딩(100M · 30M · 6-level × 256 codebook)이 트리거라는 해석이 자연스럽다.
    • 공유 adapter recsys_async_emb.py가 기존 column-sharded 커널 async_emb와 신규 row-sharded 커널 row_emb 가운데 하나를 고르게 해, 모델별로 최적 샤딩을 선택할 여지를 마련한 셈이다.
    • two-tower는 유저 타워와 아이템 타워를 각각 column-shard로 유지해도 되지만, SID generative 모델은 generation 단계에서 각 토큰이 자기 vocab 범위만 참조하므로 row-shard가 더 compact한 통신 패턴을 낳는다 추정.

LLM judge action이 돌아온 것은 이와 별개의 흐름이다. 15일 전 vacate됐던 두 action이 번호를 바꿔(107/108 → 108/109) 재등장한 사실은 "LLM judge를 action 축에 담아 학습 target 중 하나로 소비"하려는 방향이 재조정 후 다시 올라왔음을 뜻한다. 번호가 바뀐 이유는 그 사이 107 자리가 CLIENT_TWEET_LIVE_STREAM_REPLY로 영구 점유됐기 때문이고, 108도 함께 비워 재사용하는 쪽을 택했다. 이번 공개는 모델 학습 config나 wire에서의 소비처를 보여 주지 않고, proto만 자리를 먼저 잡았다. 어느 head 가중치에 들어가는지, 어느 config의 target action list에 포함되는지는 다음 창에서 봐야 한다.

2추천과 관련된 새 글은 없었다 — Netflix·Meta 블로그

Netflix와 Meta의 엔지니어링 블로그에는 오늘 반영할 추천 관련 새 글이 없었다.

  • Netflix tech blog 피드 10건 중 최신 2건은 추천과 무관하다. "Trading a Cloud Identity for Your Own: Workload Attestation on Managed Compute"(2026-09-25, 인프라)와 "Leave the Class Path in the Rearview Mirror"(2026-09-18, Java packaging)다.
  • 그 전의 "MAPS"·"GenRec"·"GenPage"는 이미 반영됐다.
  • Meta engineering.fb.com의 최신 글 "From User Sequences to Scaling Laws"(2026-09-02)는 이미 반영됐다. 그 뒤에 쌓인 네 건(Private Processing / Rebalancer / Petal / ZGateway)은 모두 인프라·네트워킹·개인정보 주제로, 추천과 무관하다.

레이더

레이더 수용 기준을 모두 넘은 논문은 0건이었다. 수용 기준은 다음 세 가지다.

  • ① 플랫폼 저자 또는 프로덕션 A/B
  • ② 메커니즘 신규성
  • ③ 대규모 결과

대상은 arXiv cs.IR 2026-09-30 ~ 2026-10-01 창이고, 요청 30건 중 <entry> 30건을 전부 파싱했다. 아래 세 항목이 경계였지만 모두 탈락했다.

  • arXiv 2610.02057 Optimizing Effective Training Time for Large-Scale Recommendation Systems. Meta의 추천 학습 인프라 운영 사례라 탈락했다.
    • Meta 소속으로 확인했다. PDF strings에 "Meta Platforms, Inc.", "Meta Engineering Blog", "Meta internal"이 다수 등장한다.
    • 추천 학습 플릿의 ETT%(Effective Training Time)를 80% → 90%+로 올린 fleet-scale 운영 사례다. 비동기 체크포인팅 · 통신 제거 · 파이프라인 overlap · 동적 shape · PyTorch 2 compile cache 재사용 등을 다룬다.
    • 탈락 사유는 추천 학습 인프라의 운영적 승리라는 점이다. 레이더 기준 ②의 retrieval·랭킹 메커니즘 신규성과는 성격이 다르다. 서비스 정본에도 "학습 처리량"은 들어가지 않는 항목이므로 반영하지 않았다.
  • arXiv 2610.01533 Neither Black nor White: Balancing Semantic and Collaborative Signals with Graph-Informed Semantic IDs (GrIS). 프로덕션 배포 주장이 없어 탈락했다.
    • 저자 전원이 Huawei Ireland Research Centre 소속임을 확인했다.
    • SID와 collaborative 신호를 그래프로 섞는 메커니즘을 다루지만, 프로덕션 배포 주장은 없다.
    • 탈락 사유는 ①의 플랫폼·배포 조건이 비어 있다는 점이다.
  • arXiv 2609.39043 Routing Between Generative and Collaborative User Profiles: A Serving-Time Gate for Controllable Novelty. 추적 대상 서비스가 아니어서 탈락했다.
    • 저자는 Comcast Technology AI와 DePaul University 소속이다.
    • 논문은 "serving-time routing gate가 LLM-profile 모델과 collaborative sequential 모델 사이에서 유저를 선택적으로 라우팅"하는 구조를 제안한다. NDCG 5% 손실 예산 안에서 Novelty@10 +6.5%를 얻고, 유저의 12.5%만 라우팅한다.
    • 메커니즘은 흥미롭다(cost-aware 선택적 LLM 사용).
    • 탈락 사유는 Comcast의 Xfinity Stream이 추적 대상 서비스가 아니며, 피드/랭킹 플랫폼으로서의 추적 가치가 낮다는 점이다(스트리밍 TV의 영상 추천).

다음에 볼 것