- 읽은 자료 1건
- 정본 반영 1개 파일
- 레이더 0건
1홈믹서의 실험용 dwell-regret 대체 스코어링이 롤백됐다 — X
자료 xai-org/x-algorithm@8b25829 — GitHub 커밋, 2026-09-18T23:55:39Z 저자 커밋, 파일 41개, +2,877/−1,828. 이 노트들은 X의 공개 소스 4세대(2026-08-13, 47c1bcd) 뒤에 나온 커밋에 차례로 번호를 붙여 부르며, 이 커밋은 4세대+25다.
배경 어제 다룬 커밋 4세대+24(c279172)보다 22시간 늦게, 같은 날짜(2026-09-18)에 등록된 커밋이다. 삭제 1,828줄이 추가 2,877줄과 대체로 균형을 이루지만, 삭제는 서로 얽힌 세 파일(아래 A 항목)에 몰려 있다. 이번 커밋은 홈믹서의 실험 스코어링 모드 하나를 롤백하면서, 다른 영역 여러 곳도 함께 손봤다.
커밋이 다루는 축은 다섯 갈래다.
- A. home-mixer(X의 피드 요청을 받아 후보를 모으고 순위를 매기는 서비스)의 랭킹 스코어링에서, 4세대 초기에 들어온 실험 스코어링 모드 하나가 폐기됐다.
home-mixer/scorers/value_model_gate.rs파일 하나가 전부 사라지면서(−537)home-mixer/scorers/ranking_scorer.rs가 −465줄로 크게 정리됐다.home-mixer/params/param.rs는 −110줄로 관련 파라미터 18개를 잃었다.
- B. Phoenix(X For You의 랭킹과 후보 검색 모델 스택) retrieval에, dataset_type 태그로 구별되는 cold-start 슬라이스가 정식으로 연결됐다.
- C. Phoenix bundle export가 랭킹 전용에서 two-tower(유저와 포스트를 따로 임베딩해 매칭하는 검색 모델) retrieval까지 지원하도록 확장됐다.
- Phoenix serving/train 라인에서
recsys_bundle_export.py가 +559/−107로 크게 개편됐다.
- Phoenix serving/train 라인에서
- D. VF(visibility-filtering, 포스트마다 이 뷰어에게 보여도 되는지 판정하는 서비스)에는 어제까지 스텁이던
evaluate_tweetsgRPC의 서버 구현이 생겼다. TES(Tweet Entity Service, X 내부의 트윗 메타데이터 서비스)가 넘겨 주던 visibility 필드를 대체하는 하이드레이터도 등장했다. 이 하이드레이터는 Strato(코드가 데이터를 조회하거나 저장할 때 거치는 X 내부 시스템) 컬럼에서 필드를 가져온다.- 추가 쪽에서는 두 개의 새 파일
visibility-filtering/evaluate_tweets.rs(+258)와visibility-filtering/hydration/tes_composite.rs(+607)가 등장했다.
- 추가 쪽에서는 두 개의 새 파일
- E. 인프라 정리로 CUDA 커널이 xrex 리포에서 외부 패키지
xrex_cuda_kernels로 분리됐다. 또 grox(Grok 기반 nearline 분류기 서브레포)의 성인 spam clone 탐지 흐름에 fav-count 기반 reasoning 게이팅이 붙었다.- 이 탐지 흐름의 이름은
media_injected_adult_infrared로 시작한다.
- 이 탐지 흐름의 이름은
다섯 갈래 가운데 A와 D는 4세대에서 그동안 쌓여 온 실험 인프라의 되돌림과 성숙이라는, 서로 다른 방향의 사건이다. A는 4세대 초기에 도입된 별도 스코어링 모드가 실측 결과가 좋지 않아(공개된 근거는 없고, 파일 전체 삭제라는 형태로만 관측된다) 정리되는 흐름이다. D는 어제 4세대+24에서 클라이언트 인터페이스만 열려 있던 스텁이 서버 로직까지 이어지는 성숙 흐름이다.
새로 알게 된 것A. 실험 스코어링 모드 gated dwell-regret과 그 유저 게이트가 전부 롤백됐다
게이트를 거쳐 dwell-regret 스코어링을 쓰던 모드와, 그 유저별 게이트를 정의하던 파일이 전부 롤백됐다 확인.
- 근거 파일
home-mixer/scorers/value_model_gate.rs는 8b25829에서 존재하지 않는다. - 모드는
gated_dwell_regret이고, 파일은value_model_gate다. - 이전 4세대 오픈소스 히스토리에서 이 파일은
GateModel::from_params()를 정의했다. - GateModel은 유저 상태 피처 19개를 log1p로 정규화한 뒤, 로지스틱 선형결합으로 게이트 점수를 냈다. 그다음 요청 hash로 hysteresis band 안에서만 배정을 뒤집었다.
- 피처 목록은
seq_len·n_fav·n_reply·n_rt_quote·n_vqv·n_click·n_bm_share·n_profile_follow·n_photo·n_negfb·n_7d·n_1d·active_days·active_days_7d·days_since_last·span_days·followers·followings·account_age_years다. - 롤백 커밋의 patch에서 이 로직 전체가 −537줄로 지워진 것이 확인된다.
- 근거 파일
GateModel은
ranking_scorer.rs에서ValueModelMode파라미터가 게이팅 모드일 때만 발동했다. 이때 유저별 조건에 따라, 체류 시간(dwell)을 바탕으로 한 dwell-regret 계열 대체 스코어를 쓸지 정했다.ValueModelMode의 값은 세 가지(weighted/dwell_regret_sigmoid/gated_dwell_regret)였다.
8b25829는 이 세 값 갈래를 전부 없앴다.
- let use_dwell_regret = match query.params.get(ValueModelMode).as_str() { - DWELL_REGRET_SIGMOID_MODE => true, - GATED_DWELL_REGRET_MODE => GateModel::from_params(&query.params) - .is_some_and(|gate| gate.serve_new_scoring(query)), - _ => false, - };—
home-mixer/scorers/ranking_scorer.rs, 2026-09-18 삭제분 (실제 소스는 사라짐)대체 로직은 없다. 조건 분기 자체가 제거됐고,
weighted_parts와weighted_scores계산에는use_dwell_regret == false분기 하나만 남는다.- 홈믹서 스코어링은 4세대+8(2026-08-25)에서 클라이언트 쪽 head-weight 생성을 서버로 옮긴 뒤 다시 한 번 단순해져, weighted 단일 경로로 정착했다.
ranking_scorer.rs에서는 관련 요소도 함께 삭제됐다 확인 —home-mixer/scorers/ranking_scorer.rs(+12/−465).DwellRegretWeightsstruct와from_params()생성자.- struct의 튜닝 필드 13개는
alpha_favorite·alpha_reply·alpha_retweet·alpha_quote·alpha_share·alpha_share_via_dm·alpha_share_via_copy_link·neg_not_interested·neg_block_author·neg_mute_author·neg_report·temperature·dwell_floor였다.
- struct의 튜닝 필드 13개는
compute_dwell_regret_base_scores()함수 전체. 이 함수는 후보 세트 안에서 파라미터별 평균을 낸 뒤, 각 후보의 relative preference를centered_ratio = p / mean − 1로 만들었다.positive와negative를 각각sigmoid(positive/T)와exp(min(negative,0)/T)로 곱해, 최종 점수를dwell × modulation으로 냈다. 즉 "dwell 시간을 참여 신호로 modulate하는" 대체 스코어링 함수다.centered_ratio()·sigmoid()헬퍼.use_dwell_regret분기에 걸린MultiplierPreOffset건너뛰기 로직.- 회귀 테스트 7개와
GATE_WEIGHTS_ALL_ZERO상수, 그리고 기존 회귀 테스트의"rust_home_mixer_value_model_mode": "weighted"오버라이드 라인.- 테스트 이름은
dwell_regret_neutral_post_scores_dwell·dwell_regret_above_average_likeable_is_boosted·dwell_regret_report_sinks_candidate·dwell_regret_floor_applies_to_low_dwell·dwell_regret_per_request_normalization_amplifies_for_low_engagement_set등이다.
- 테스트 이름은
파라미터 미러에서도 관련 파라미터가 대칭으로 삭제됐다 확인 —
home-mixer/params/param.rs(+8/−110).- 이 파일은 프로덕션 설정값을 주기적으로 옮겨 오는 미러다. 이번 mirror sync 시각은
2026-09-17T20:46:44Z → 2026-09-18T16:21:20Z로 바뀌었다. - 삭제된 param은 18개다.
- 이 파일은 프로덕션 설정값을 주기적으로 옮겨 오는 미러다. 이번 mirror sync 시각은
| 이름 | feature switch 키 | 기본값 |
|---|---|---|
ValueModelMode |
rust_home_mixer_value_model_mode |
"weighted" |
DwellRegretTemperature |
..._dwell_regret_temperature |
10.0 |
DwellRegretDwellFloor |
..._dwell_regret_dwell_floor |
1.0 |
DwellRegretAlphaFavorite |
..._dwell_regret_alpha_favorite |
1.0 |
DwellRegretAlphaReply |
..._dwell_regret_alpha_reply |
1.0 |
DwellRegretAlphaRetweet |
..._dwell_regret_alpha_retweet |
1.0 |
DwellRegretAlphaQuote |
..._dwell_regret_alpha_quote |
1.0 |
DwellRegretAlphaShare |
..._dwell_regret_alpha_share |
1.0 |
DwellRegretAlphaShareViaDm |
..._dwell_regret_alpha_share_via_dm |
1.0 |
DwellRegretAlphaShareViaCopyLink |
..._dwell_regret_alpha_share_via_copy_link |
1.0 |
DwellRegretNegNotInterested |
..._dwell_regret_neg_not_interested |
−10,000 |
DwellRegretNegBlockAuthor |
..._dwell_regret_neg_block_author |
−8,000 |
DwellRegretNegMuteAuthor |
..._dwell_regret_neg_mute_author |
−15,000 |
DwellRegretNegReport |
..._dwell_regret_neg_report |
−60,000 |
DwellRegretGateWeights |
..._dwell_regret_gate_weights |
19-피처 로지스틱 가중치 문자열 |
DwellRegretGateBias |
..._dwell_regret_gate_bias |
1.033918 |
DwellRegretGateThreshold |
..._dwell_regret_gate_threshold |
−0.634264 |
DwellRegretGateHysteresisBand |
..._dwell_regret_gate_hysteresis_band |
0.0 |
롤백된 파라미터의 프로덕션 기본값에서, 게이트가 실제로 학습된 가중치를 담고 있었다는 사실을 확인할 수 있다.
DwellRegretGateWeights의 기본값 문자열이 지운 시점에도 남아 있었기 때문이다."seq_len:0.530298,n_fav:-0.082139,n_reply:0.485541,n_rt_quote:0.056561,n_vqv:-0.072778,n_click:-0.176675,n_bm_share:-0.167574,n_profile_follow:-0.221285,n_photo:-0.106004,n_negfb:0.031839,n_7d:-0.075799,n_1d:-0.241730,active_days:0.047017,active_days_7d:-0.126896,days_since_last:-0.034238,span_days:-0.052186,followers:-0.066642,followings:0.064140,account_age_years:-0.045455"seq_len(현재 세션 시퀀스 길이)과n_reply(28일 리플라이 수) 두 축에는 강한 양의 가중치가 걸려 있었다.- 나머지 상호작용 카운트(
n_click·n_bm_share·n_profile_follow·n_1d)에는 음의 가중치가 걸려 있었다. - 즉 "롱 세션 활동 사용자이면서 최근 리플라이 활동이 강한" 유저에게 dwell-regret 대체 스코어링을 서빙하는 것이 목표였던 것으로 읽힌다.
- 이 계수 자체는 이제 어느 유저에게도 적용되지 않는다.
4세대에서 랭킹 스코어링 옆에 나란히 있던 실험적 대체 함수(dwell 시간을 참여 신호로 modulate)와 그 유저 게이팅이 한 번에 전부 제거됐다. 랭킹 스코어링은 다시 weighted 단일 경로로 좁혀졌다. 이 자리에서 dwell 신호를 어떻게 반영할지는 훈련 시점의 헤드 신설과 점수 결합 재정의로 넘어간 것으로 보인다 추정. 4세대+22에서 정식으로 연결된 click_dwell_time sigmoid BCE(threshold=10s)와 4세대+23의 EnableCdwellOnImpr(click × dwell 곱)가 그 예다 추정. 근거는 두 흐름의 시점이 인접하다는 점, 그리고 dwell을 별도 스코어링 함수로 modulate하던 회로가 사라지면서 dwell을 헤드 자체와 조합 항으로 다루는 회로만 남는다는 관측이다.
새로 알게 된 것B. Phoenix retrieval에 cold-start 슬라이스가 연결됐지만, 기본값으로는 요청하지 않는다
Phoenix retrieval에 전용 태그로 구별되는 cold-start 슬라이스가 정식으로 연결됐다 확인.
- 근거 파일은
home-mixer/sources/phoenix_source.rs(+67/−19)다. - 태그는
HOME_COLD_DATASET_TYPE=13이다.
- 근거 파일은
새 파라미터
PhoenixColdStartMaxResults가 추가돼, retrieval RPC를 호출할 때 두 상한이 함께 넘어간다.- feature switch는
rust_home_mixer_phoenix_cold_start_max_results이고, 기본값은0이다.
quality_factor::apply(query.params.get(PhoenixMaxResults)), + query.params.get(PhoenixColdStartMaxResults),- 기본값이 0이므로 프로덕션 기본은 여전히 cold-start 슬라이스를 요청하지 않는다.
- feature switch는
응답 후보가 dataset_type을 실어 오고, 그 값에 따라 served_type(후보에 붙는 출처 라벨)이 갈린다.
- 응답 후보(
ScoredCandidate)가dataset_type: u32필드를 실어 온다. - 그 값이
HOME_COLD_DATASET_TYPE = 13이면 served_type이 새 enumForYouPhoenixRetrievalCold로 매핑되고, 그렇지 않으면 기존ForYouPhoenixRetrieval로 매핑된다.
const HOME_COLD_DATASET_TYPE: u32 = 13; fn served_type_for_dataset(dataset_type: u32) -> pb::ServedType { if dataset_type == HOME_COLD_DATASET_TYPE { pb::ServedType::ForYouPhoenixRetrievalCold } else { pb::ServedType::ForYouPhoenixRetrieval } }- 응답 후보(
side effect에도 대칭으로 관측 라인이 붙었다 확인 —
home-mixer/side_effects/scored_stats_side_effect.rs(+25/−0).- 새 함수
record_phoenix_retrieval_cold_stats가PhoenixRetrievalColdTweets라는 접두로 selected/non-selected candidates의 크기 통계를 stat receiver에 발행한다. - 기존 MoE 슬라이스(
PhoenixRetrievalMoeTweets)와 나란한 세 번째 관측 축이다. MoE(Mixture of Experts)는 입력마다 여러 전문가 서브네트워크 가운데 일부를 골라 쓰는 모델 구조다.
- 새 함수
MoE 소스와 Topics 소스는 이 cold-start 슬라이스 로직을 공유하지 않는다(patch에서 확인).
phoenix_moe_source.rs와phoenix_topics_source.rs도 각각 1줄씩 늘었지만, 둘 다 새 import 라인 추가다.- 두 소스는 기존대로
ForYouPhoenixRetrievalMoe·ForYouPhoenixRetrievalTopicsserved_type을 그대로 쓴다.
지금까지 X의 콜드스타트 처리는 두 축이었다.
- 이력이 임계 미만인 유저에게는 별도 모델 플릿을 라우팅한다. retrieval과 랭킹에 각각
Phoenix*NewUserInferenceClusterId가 있다. - 신규 작성자(
ColdStartImpressionThreshold=1000이하)에게는PostUnexploredWeight=0.02부스트를 주고, 4세대+1의 Thompson sampling 옵션을 둔다.
이번 연결은 후보 수준의 세 번째 축을 연다. 같은 Phoenix retrieval RPC가 돌려주는 후보 세트 안에서, dataset_type 태그로 "cold" 서브셋에 따로 상한을 두고 관측한다. 즉 Phoenix retrieval 자체가 유저 이력과 무관하게 콜드 후보 슬라이스를 항상 함께 반환하도록 확장된 것으로 보인다 추정. 근거는 기본값이 0이라 프로덕션 기본에는 아직 들어가지 않았지만, 관측용 stat 라인까지 대칭으로 연결된 것으로 보아 A/B 인프라가 완성돼 있다는 점이다.
새로 알게 된 것C. Phoenix 번들 export가 랭킹에 이어 two-tower retrieval의 forward 프로그램도 내보낼 수 있게 됐다
Phoenix bundle export가 랭킹 forward에 이어 two-tower retrieval forward까지 export할 수 있게 확장됐다 확인.
- 근거 파일은
phoenix/xrex/train/recsys_bundle_export.py(+559/−107)다. - 이 파일은 4세대+13(2026-09-01, 7ba7768)에서 634줄로 신설됐다. 랭킹 모델(
RecsysAggregatedModelConfig)을jax.export로 낮춰 StableHLO MLIR bytecode,.jax_export,MANIFEST.json으로 export하는 용도였다. - 이번 커밋이 여기에 retrieval two-tower 프로그램 export를 나란히 붙였다. 새로 등장한 축은 두 가지다.
- 근거 파일은
축 1: 프로그램 종류가 두 개로 늘었다 확인. 두 상수가 export bundle 안에서 프로그램의 kind를 구별한다.
KIND_RANKING = "recsys_ranking_forward" KIND_RETRIEVAL = "recsys_retrieval_forward"축 2: retrieval export는 추가 리소스를 번들에 실어야 하므로 새 자료구조와 wire key들이 도입됐다.
POST_TABLE_KEY = "post_embeddings.embeddings" POST_DATASET_TYPES_KEY = "post_embeddings.dataset_types" POST_IDS_KEY = "post_embeddings.post_ids" AUTHOR_IDS_KEY = "post_embeddings.author_ids"- 새 dataclass
RetrievalExport는 export 시점에 트레이너의RetrievalModelRunner에서 뽑아내야 하는 5개 속성을 담는다. 속성은large_k·retrieval_dataset_types·enable_async_topk·enable_radix_select_topk·enable_dataset_slice_topk다. - 매니페스트 블록에는 아래 항목이 실린다.
- large_k
- target dataset types(name·value 페어의 리스트)
- post table 지오메트리(rows·width·dtype)
- 세 자료 키(POST_TABLE_KEY·POST_DATASET_TYPES_KEY·POST_IDS_KEY·AUTHOR_IDS_KEY)
topk_unordered플래그
- 새 dataclass
retrieval forward는 유저 타워로 유저 임베딩을 뽑고, 이를 post table 전체와 매칭해 target dataset type마다 top-K를 반환한다 확인 — 새 함수
_make_retrieval_forward_fn.def forward_fn( batch: Any, merged_embeddings: jax.Array, post_table: jax.Array, dataset_types: jax.Array ): recsys_embeddings = _recsys_embeddings_from_merged( merged_embeddings, embedding_slices, packed_geometry ) model = model_config.make(sharding_context=make_legacy_sharding_context(mesh)) results = model.forward( batch, recsys_embeddings, post_table, dataset_types, retrieval.large_k, target_values, dataset_ranges=None, use_async_topk=retrieval.use_async_topk, use_radix_select_topk=retrieval.use_radix_select_topk, post_scales=None, ) flat: list[jax.Array] = [] for indices, scores in results: flat.append(indices) flat.append(scores) return tuple(flat)- target dataset type마다 출력이 두 개(
indices_<name>,scores_<name>) 나온다. 이 이름 규칙은RetrievalExport.output_namesproperty에 정의돼 있다.
- target dataset type마다 출력이 두 개(
two-tower일 때는 서빙 시퀀스 길이를 계산하는 규칙이 다르다.
_make_export_config에도two_tower: bool = False와mesh_devices: int = 1두 인자가 추가됐다.
def _user_tower_sequence_len( *, training_seq_len, training_history_seq_len, num_user_prefix_tokens, history_seq_len, ) -> int: prefix_tokens = training_seq_len - training_history_seq_len if prefix_tokens != num_user_prefix_tokens: raise ValueError( f"cannot derive the serving user-tower sequence_len: sequence_len " f"{training_seq_len} - history_seq_len {training_history_seq_len} = {prefix_tokens} " f"prefix tokens, but the model reserves num_user_prefix_tokens=" f"{num_user_prefix_tokens}. The user tower is built as prefix + history; if " "history_seq_len was overridden for serving, also override " "model_config.user_tower_config.model_config.sequence_len to " f"{num_user_prefix_tokens + history_seq_len} (prefix + serving history), as the " "jax slot does — otherwise the attention kernel cannot be sized for serving" ) return prefix_tokens + history_seq_len- 랭킹 export의 서빙 시퀀스는
prefix + history + candidate이지만, retrieval export는prefix + history뿐이다. candidate 축이 없고, 유저 임베딩만 낸다. _make_export_config의 overrides도model_config.model_config.sequence_len대신model_config.user_tower_config.model_config.sequence_len을 쓴다.
세 번째로 새로 등장한 것은 분산 export다. retrieval bundle은 mesh 위에서 export되어 여러 장치에 분산될 수 있다.
if not two_tower and mesh_devices != 1: raise NotImplementedError( "ranking programs are single-device (one executable per GPU); " f"mesh_devices={mesh_devices} is only for retrieval bundles" )- 이유도 명시돼 있다. 랭킹은 GPU마다 executable 하나로 서빙되지만, retrieval은 post table을 여러 장치에 나눠 담아야 하므로 SPMD로 export한다.
_export_mesh는export_cfg.parallel_config.mesh_shape()을 확인해 device를 만든다._retrieval_shardings는 batch/params/post_table/dataset_types에 대한NamedSharding을 지정한다.- 임베딩 rows는 data axis로 분산 파티션되고, params는 replicated된다.
몇 가지 제약이 명시적으로 걸린다.
enable_int8_post_table·enable_bloom_filter·enable_topic_filter는 지원하지 않는다. 이 옵션들은 native runtime만 지원하고, StableHLO export는 bf16 post table과 no-per-post-filter 구성이다.enable_dataset_slice_topk=True는 무시되고 경고가 뜬다. export 프로그램은 항상 mask-then-topk-over-whole-table 경로만 컴파일한다.using_seqpack + two_tower조합은 금지된다. packing이 SPMD 모델과 맞지 않는다.DEBUG_ALLOW_RANDOM_INIT=1이면 export 자체를 거부한다. 마스크 없이 export되면 유효하지 않은 프로그램이 나가기 때문이다.
build_bundle이 이제 두 종류 모델을 모두 받는다.two_tower = isinstance(model_config, RecsysTwoTowerModelConfig) if not two_tower and ( not isinstance(model_config, RecsysAggregatedModelConfig) or isinstance(model_config, RecsysGenRecsModelConfig) ): raise NotImplementedError( "StableHLO bundle export supports ranking (RecsysAggregatedModelConfig) and " f"retrieval (RecsysTwoTowerModelConfig) only, got {type(model_config).__name__}" )
지금까지 Phoenix serving은 랭킹 forward만 StableHLO 번들로 실었다. 이 커밋으로 retrieval two-tower도 같은 번들 프레임에 실릴 수 있다는 준비가 끝났다. retrieval 쪽을 SPMD로 여러 장치에 분산해 export할 수 있는 인프라까지 함께 붙었기 때문에, Phoenix retrieval의 서빙 스택이 랭킹과 같은 번들 규격으로 통일되는 것이 방향이다. 위 B의 PhoenixColdStartMaxResults 연결과 시점이 같이 붙은 것도 우연이 아닐 수 있다 추정. retrieval 서빙 축이 조여지는 시기에 dataset_type 태그를 통한 슬라이스 관측 인터페이스가 함께 정착한 셈이다 추정.
새로 알게 된 것D. VF가 판정 요청을 받는 서버를 갖췄고, 판정용 visibility 필드를 TES 대신 Strato 컬럼에서 받는다
- VF에 판정 요청을 받는 gRPC의 서버 구현이 붙었고, TES가 넘겨 주던 visibility 필드는 Strato 컬럼 하이드레이터로 옮겨 갔다 확인.
- 새 파일:
visibility-filtering/evaluate_tweets.rs(+258),visibility-filtering/hydration/tes_composite.rs(+607) - 함께 고친 파일:
visibility-filtering/server.rs(+86/−2)visibility-filtering/filter.rs(+336/−13)visibility-filtering/hydration/tes_hydrator.rs(+113/−320)visibility-filtering/hydration/mod.rs(+78/−22)
- 이 gRPC는
evaluate_tweets다.
- 새 파일:
이 축은 두 하위 축으로 나뉜다.
D1. 어제까지 스텁이던 판정 요청 RPC를 이번 커밋이 서버 로직으로 채웠다.
daily/2026-09-18.md에는 2026-09-17의 4세대+23(42266f3)이
evaluate_tweetsRPC의 클라이언트 인터페이스와 응답 payload Thrift 디코더만 열어 두었다는 관측이 남아 있다. 같은 관측에 따르면 서버 구현은Err(Status::unimplemented("EvaluateTweets is not served yet"))였다.이번 커밋은 그
evaluate_tweets스텁을 서버 로직으로 채운다.server.rspatch에서 unimplemented 스텁이 실제 구현으로 바뀌는 것이 직접 확인된다.async fn evaluate_tweets( &self, - _: Request<vf_pb::EvaluateTweetsRequest>, + request: Request<vf_pb::EvaluateTweetsRequest>, ) -> Result<Response<vf_pb::EvaluateTweetsResponse>, Status> { - Err(Status::unimplemented("EvaluateTweets is not served yet")) + self.evaluate_tweets.handle(request).await }새 서버 엔드포인트
EvaluateTweetsEndpoint(258줄 신규)는 요청을 받아 내부 필터를 돌리고, 응답을 tweet별Outcome으로 채운다.- request 3개 필드(
viewer_id·country_code·safety_level그리고 tweet 리스트)를 받는다. - safety_level은 Thrift enum에서 내부 Rust
SafetyLevelenum으로 변환한다. 허용하는 값은FILTER_ALL·TIMELINE_HOME·TIMELINE_HOME_RECOMMENDATIONS3개뿐이다. - 그런 뒤
FilterTweets::run을 호출한다.
let candidates = req.tweets.iter() .filter(|o| o.quote_context.is_none()) .map(|o| RawCandidate { tweet_id: TweetId(o.tweet_id), request_author_id: None }) .collect(); let response = self.filter_tweets.run(FilterRequest { viewer_id, country_code, safety_level, candidates }).await;- request 3개 필드(
요청마다 메트릭 3종(
evaluate_tweets_requests·evaluate_tweets_latency_ms·evaluate_tweets_batch_size)이 기록된다.evaluate_tweets_loopback통합 테스트가 서버 로직이 연결됐다는 사실을 고정한다. 이 테스트는 tokio TCP listener 위에 gRPC 서버를 띄우고 XaiVfClient로 왕복 호출한다.
D2. 판정 결과에 verdict 말고도 평가 상태가 함께 붙는다.
FilterOutcome에EvaluationStatus축이 추가됐다.filter.rs에 새 enum이 등장한다.#[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum EvaluationStatus { Evaluated, UnresolvedAuthor, Failed, }이전에는 filter가 tweet마다
Verdict하나만 낼 수 있었다. 이제 verdict 옆에 세 상태 중 하나가 붙는다.- Hydration이 성공한 tweet은
Evaluated다. - author가 hydration에서 나오지 않은 tweet은
UnresolvedAuthor이고, 이때 verdict는Verdict::unresolved_author()다. - hydration 자체가 실패해서 필드가 부족한 상태로 rule에 걸린 tweet은
Failed다. HydrationOutput에도 새 필드failed_ids: HashSet<TweetId>가 추가돼 세 번째 경우(Failed)를 구별한다.
- Hydration이 성공한 tweet은
클라이언트 쪽
EvaluationResult { Evaluated(Box<Action>), NotEvaluated, Failed }(4세대+23 도입)와 축 이름이 나란히 나오고, 서버와 클라이언트의 축이 다음처럼 맞물린다 추정.EvaluationStatus::UnresolvedAuthor + Verdict::unresolved_author()→EvaluationResult::NotEvaluatedEvaluationStatus::Failed→EvaluationResult::FailedEvaluationStatus::Evaluated + Verdict::*→EvaluationResult::Evaluated(Action)- 추정으로 두는 이유는 patch에 매핑이 직접 나오지 않고
EvaluateTweetsResponse조립부만 나오기 때문이다. 다만 두 축의 값 이름이 대칭이다.
D3. TES가 맡던 visibility 필드 7종이 별도 Strato 컬럼 하이드레이터로 옮겨 갔다.
tes_hydrator.rs가 −320줄로 크게 줄었고, 지운 자리 대부분이TweetHydrationstruct다.-#[derive(Default)] -pub(crate) struct TweetHydration { - pub(crate) nullcast: TweetHydrationBatch<bool>, - pub(crate) community: TweetHydrationBatch<i64>, - pub(crate) nsfw_user: TweetHydrationBatch<bool>, - pub(crate) nsfw_admin: TweetHydrationBatch<bool>, - pub(crate) takedown_reasons: TweetHydrationBatch<Vec<TakedownReason>>, - pub(crate) edit_control: TweetHydrationBatch<EditControl>, - pub(crate) media: TweetHydrationBatch<MediaFeature>, -}이 7개 축은
TweetHydrationBatch<TweetForVisibility>하나로 통합됐다.TweetForVisibility는 새 파일tes_composite.rs에 정의된 11-필드 struct다.pub(crate) struct TweetForVisibility { pub(crate) author_id: u64, pub(crate) source_tweet_id: Option<u64>, pub(crate) is_nullcast: bool, pub(crate) nsfw_user: bool, pub(crate) nsfw_admin: bool, pub(crate) has_takedown: bool, pub(crate) takedown_reasons: Vec<TakedownReason>, pub(crate) media: MediaFeature, pub(crate) is_community_tweet: bool, pub(crate) edit_control: Option<EditControl>, pub(crate) exclusive_conversation_author_id: Option<u64>, }이 데이터를 가져오는 소스는 TES가 아니라 Strato 컬럼의 fetch operation이다 확인 —
visibility-filtering/hydration/tes_composite.rs.- 컬럼은
tweetypie/federated/tweetForVisibility.Tweet이고, operation은fetch다.
const COLUMN: &str = "tweetypie/federated/tweetForVisibility.Tweet"; const OPERATION: &str = "fetch";ProdTweetForVisibilitySource::get_tweets_for_visibility가 tweet_id 목록을 이 컬럼의 batch call로 보낸다. 응답은 MVal(Strato)로 디코드하고, Thrift 정의Tweet을TweetForVisibility::project()로 눌러 담아 돌려준다.- 즉 TES와 별개인 VF 전용 Strato 컬럼 인터페이스가 열렸다.
- 컬럼은
TesHydrator는 여전히 존재하지만, 이제 두 소스를 오케스트레이션한다.- TES에서는 여전히
PureCoreData(author_id·text·source_tweet_id 등 corpus 차원 신호)를 받는다. TweetForVisibilitySource에서는 visibility rule이 요구하는 flag/label 축을 받는다.- 즉 corpus 데이터와 visibility 데이터가 서로 다른 Strato 백엔드로 갈렸다.
- TES에서는 여전히
VF는 지금까지 TES의 광역 tweet 필드를 그대로 참조해 왔다. 이제 그 관행에서 벗어나, visibility 규칙에 필요한 최소 필드 집합만 별도 Strato 컬럼으로 조회하는 방향이다. 세 축이 함께 정착했다.
- 4세대+13에서 VF rules가 tweet/author 2분류로 재구조화됐다.
- 4세대+23은 자체 twemcache 서브트리 3,234줄을 전부 지우고 회사 공용
xai_cache로 옮기면서, evaluate_tweets 클라이언트 인터페이스를 스텁으로 열었다. - 오늘 서버 구현과 TES 분리가 완성된다.
즉 4세대에서 VF는 rules → dependencies → serving interface 순서로 세 층이 차례로 재편됐다.
새로 알게 된 것E. grox의 성인 스팸 영상 탐지가 좋아요 수가 임계치 이상인 포스트에서만 reasoning 모드를 켠다
grox 탐지 흐름에 fav-count(좋아요 수) 게이팅으로 reasoning 모드가 붙었다 확인.
- 근거 파일은
grox/flows/ptos/task_safety_ptos_media_injected_adult_infrared_video_spam_detection.py(+14/−9)다. - 대상은
media_injected_adult_infrared_video_spam_detection흐름이다. 4세대+24가 신설한 흐름으로, 참조 영상 번들과 후보 영상의 VLM 스토리보드를 비교한다.
use_reasoning = post.get_fav_count() >= MEDIA_INJECTED_REASONING_FAV ... prompt = media_injected_adult_infrared_video_spam_detection_prompt() system_prompt = ( _strip_thinking_restrictions(prompt) if use_reasoning else prompt ) ... assistant_content = [] if use_reasoning else [THINKING_CONTROL_END]- 근거 파일은
Fav count가 임계치 이상인 포스트에는 모델이 명시적으로 생각하는 경로가 열린다.
- 시스템 프롬프트의 "thinking restrictions"를 벗겨 내고, assistant 응답의
THINKING_CONTROL_END마커를 비워 둔다.
- 시스템 프롬프트의 "thinking restrictions"를 벗겨 내고, assistant 응답의
임계치
MEDIA_INJECTED_REASONING_FAV의 값은grox/flows/ptos/constants.py에 들어갔지만, patch에는 값이 노출되지 않았다.4세대+7(2026-08-24)의
SafetyPtosPolicyCrossValidator는USE_REASONING_CATEGORIES = {ViolentSpeech, SuicideOrSelfHarm}카테고리에만 reasoning을 켰다. 이번에는 fav count로 개별 포스트 축에서 reasoning을 게이팅한다.
이 게이팅은 비용을 fav count 상위 스레드에만 쓰기 위한 전형적인 이원 분기다. 4세대+22(2026-09-16)의 oai-gemma4-26b-reply-spam 이원 분기(150k 팔로워 초과 스레드)와 같은 계열이다. 4세대+12(2026-09-01)의 deluxe_fav_count_threshold special-video 진입 게이트도 같은 계열이다.
새로 알게 된 것그 밖의 변경
- Phoenix retrieval의 MoE inference cluster ID가 롤포워드됐다 확인.
- 근거는
param.rs이고, 값은Experiment2Memy04 → Experiment3Memy04로 바뀌었다.
- 근거는
- CUDA 커널 3개의 로더가 레포 안 소스 대신 외부 패키지
xrex_cuda_kernels를 불러오도록 바뀌었다 확인 —phoenix/xrex/cuda/adler32/__init__.py(+21/−6).나머지 파일:
phoenix/xrex/cuda/top_k_by_key/__init__.py(+26/−6),phoenix/xrex/cuda/unique/__init__.py(+15/−5),phoenix/xrex/cuda/__init__.py(+11/−4)지금까지는
from xrex.cuda.<name>.src import <name>_api(레포 안 subfolder)로 import했다. 이제는import xrex_cuda_kernels.<name>_api as <name>_api(외부 패키지)로 바뀐다. 예를 들어adler32/__init__.py는 이렇게 바뀌었다.try: import xrex_cuda_kernels.adler32_api as adler32_api except ModuleNotFoundError: adler32_api = None else: jax.ffi.register_ffi_target("xrex_adler32", adler32_api.adler32(), platform="CUDA") _FFI_BACKENDS.add("gpu")모듈이 없으면 예외를 던지지 않고 조용히 fallback한다.
if _MISSING: _log.warning( "xrex.cuda.adler32: xrex_cuda_kernels.%s not installed; the zlib pure_callback " "reference path runs instead of the compiled kernel(s)", ", ".join(_MISSING), )패키지가 설치되지 않은 환경에서는 zlib의
pure_callback순수 파이썬 참조 경로가 대신 돈다.top_k_by_key와unique도 같은 패턴이다.지금까지 xrex 리포 안에는 CUDA 커널 소스(
src/)까지 담겨 있었다. 이 커밋으로 커널 바이너리가 별도로 설치되는 파이썬 패키지(오픈소스 릴리즈에는 미포함)로 분리됐다.오픈소스 사용자는 참조 경로로 돌리고, 프로덕션 xrex 환경만 컴파일된 커널을 쓰는 구조다. 4세대+21(2026-09-15)이 H100 attn 커널을
pallas_ranker_varlen_attn → cutedsl_ranker_varlen_attn으로 바꾼 것과 같은 방향의 정리다.
정본 반영 ../services/x.md의 상위 timeline 표에 4세대+25 행을 추가하고, 하단에 상세 절을 새로 만들었다.
의미 어제 커밋(4세대+24)까지 누적 추가 방향으로 움직이던 X 오픈소스가, 오늘 4세대 들어 처음으로 되돌림을 함께 실었다. 또 4세대는 세 축에서 "성숙"하고 있다.
- 롤백된
value_model_gate(19-피처 유저 게이트)와dwell_regret_sigmoid·gated_dwell_regret두 스코어링 모드는 4세대 초기에 도입됐다. 이들은 4세대+8(2026-08-25)의 클라이언트 쪽 head weight 제거와 함께 존재해 왔지만, 실측 지표에서 롤포워드할 이유가 사라진 것으로 보인다(공개된 A/B 결과는 없다). - 롤백된 것은 정본에 없던 실험 경로였으므로, 정본에는 새 진술이 아니라 "이 축이 시도됐다가 정리됐다"는 관측만 남는다.
- 성숙하는 첫째 축은 Phoenix retrieval의 dataset_type 축(위 B)이다. 서버는 dataset_slice_topk까지 완성돼 있고, 오픈소스에는 후보 슬라이스 상한과 관측 stat만 노출됐다.
- 둘째 축은 Phoenix bundle export(위 C)다. 랭킹만 실을 수 있던 규격에서 retrieval two-tower까지 담을 수 있게 확장됐고, SPMD 분산 export까지 붙었다.
- 셋째 축은 VF(위 D)다. rules(4세대+13) → deps(4세대+23) → serving interface(오늘)의 순서로 세 층이 정리됐다. 3세대까지 VF가 "라벨 소비자"였다면, 4세대의 VF는 "라벨을 저장·조회·서빙하는 별개 서비스"로 형태가 잡히고 있다.
검증에서 뒤집힌 것
어제 daily가 예측한 판정 서비스의 서버 구현이 오늘 확인됐고, 같은 daily의 자료 요약에서 커밋 귀속 오류를 찾았다.
- 어제 daily(2026-09-19.md)의
## 다음에 볼 것절 예측이 부분적으로 확인됐다. "VFevaluate_tweetsRPC의 서버 측 구현" 항목이 오늘 커밋에서 정확히 그 서버가 구현되는 것으로 확인됐다(위 D1).- 다만 어제 daily의 상단 자료 요약은 evaluate_tweets를 c279172(어제 커밋)에 잘못 귀속시켰다. 실제로 c279172에서 VF는
visibility-filtering/hydration/gizmoduck_hydrator.rs2줄 추가만 있었다(gh api 확인). - evaluate_tweets 클라이언트 스텁은 4세대+23(2026-09-17, 42266f3)에 실렸고, daily/2026-09-18.md는 이를 정확히 42266f3로 인용했다.
- 정본에는 영향이 없다. x.md의 4세대+23 행이 이미 42266f3를 명시한다.
- 다만 어제 daily의 상단 자료 요약은 evaluate_tweets를 c279172(어제 커밋)에 잘못 귀속시켰다. 실제로 c279172에서 VF는
- 홈믹서 스코어링에
ValueModelMode실험이 별도로 있었다는 사실은 정본(services/x.md)에는 없었고, daily/2026-09-10.md 한 곳에서만 언급됐다. 롤백된 지금 시점에는 이 축을 정본에 다시 반영할 필요가 없다.
다음에 볼 것
오늘 커밋에서 이어 볼 것은 다섯 가지다.
- cold-start 상한
PhoenixColdStartMaxResults가 언제 켜지는지 본다 — 이 파라미터의 오픈소스 기본값은 0이다. 어느 학습·서빙 config에서 실제 값을 얻는지, 그 값이PhoenixMaxResults=1000과 어떤 비율로 갈리는지가 dataset_type=13 cold-start 슬라이스의 실제 지분을 결정한다. - two-tower 모델 설정(
RecsysTwoTowerModelConfig)의 bundle export가 실제로 쓰이는지 확인한다 — Phoenix retrieval 서빙은 지금까지 native runtime(RetrievalModelRunner)으로 돌았다. StableHLO bundle 서빙으로 옮겨 가는지가 관측 지점이다.- 새 build_bundle이 지원하는 두 번째 kind가 어느 배치 잡(
recsys_export.py계열)에서 실제로 트리거되는지, export된 bundle이 어느 retrieval 서빙 엔진에 로드되는지를 본다.
- 새 build_bundle이 지원하는 두 번째 kind가 어느 배치 잡(
- VF 전용 Strato 컬럼의 스키마가 안정되는지 본다 — 이 컬럼은 VF의 visibility 규칙에 필요한 최소 필드 집합만 노출하는 인터페이스로 굳어질 수도, 시간이 지나면서 TES와 다시 넓어질 수도 있다. 어느 쪽인지가 VF-TES 분리의 지속성을 결정한다.
- 대상 컬럼은
tweetypie/federated/tweetForVisibility.Tweet이다.
- 대상 컬럼은
- reasoning 임계치
MEDIA_INJECTED_REASONING_FAV의 실제 값을 확인한다 —grox/flows/ptos/constants.py에 들어간 이 값은 아직 미공개다. 4세대+22의GROK_GEMMA_FOLLOWER_SPLIT(150k)이나 4세대+12의deluxe_fav_count_threshold와 어느 스케일에서 비교되는지도 본다. - 새 served_type
ForYouPhoenixRetrievalCold가 프로덕션 A/B에 등장하는지 본다 — 새 stat 라인이 dashboard에 실측 값으로 뜨는 시점을 확인한다. 랭킹 스코어러가 이 served_type을 인식해 별도 처리를 하는지(4세대+16의is_phoenix_moe(c)조건 확장 계열)도 본다.
레이더
오늘 신규 논문 30건을 triage했고, 제목·초록으로 볼 때 세 조건을 함께 만족하는 것은 없다. 세 조건은 플랫폼 기업 저자, 프로덕션 배포·A/B 결과 주장, 랭킹·retrieval 메커니즘 신규성이다. 대상은 arXiv cs.IR이고, 집계 기준은 파싱된 <entry> 개수 30, 기간 2026-09-15~09-17이다. 이 기간은 어제 daily가 다룬 창과 대부분 겹친다. 잠깐 검토할 만해 보였던 항목 몇 개도 학술 증분에 그친다.
- "Single-Token Expected-Value Scoring for Cold-Start Candidate Ranking"(2026-09-16): 제목은 콜드스타트 랭킹이지만 저자 소속이 학계이고, abstract에도 프로덕션 진술이 없다. 통과시키지 않았다.
- "SURF: Subtractive Updates for Recommender Forgetting"(2026-09-16): 삭제 요청 대응(unlearning) 방향이고, 프로덕션 배포 진술이 없다.
- "One-Step Retrieval Framework for Real-Time Sponsored Search Ads Using Hierarchical Text Representations"(2026-09-16): 플랫폼 스폰서 검색 광고 retrieval이지만 회사 소속은 확인이 필요하다. 초록만으로는 A/B 진술이 명시적이지 않다. 관찰은 유보한다.
- "LIGE-GR"(2026-09-16)는 이미 어제 radar에 편입됐다.
일요일이므로 산업 블로그 훑기는 건너뛰었다(월요일에만 한다).
통과 0건이다.