1Phoenix에 광고 전환 액션 20개와 delayed feedback 학습 분기가 생겼다 — X
자료 xai-org/x-algorithm @ 28e414f — "Open-source X Recommendation Algorithm" — 2026-08-21 19:10 UTC, 공개 소스코드. 이 노트들은 4세대 공개 뒤의 X 커밋에 차례로 번호를 붙여 부르며, 이 커밋은 번호로는 4세대+6, sha로는 28e414f다. 바뀐 파일은 35개, 규모는 +856/−152이다. 4세대+5(d0cef2f, 2026-08-20, 14 파일 +479/−200)와 견주면 규모가 2배에 가깝지만, 4세대+4의 대형 확장(79 파일 +3.3k/−0.7k)에 비하면 1/4 수준이다. 이번 커밋에서 새로 들어온 내용은 광고 매출 예측 스택의 액션 수, delayed feedback 처리, Muon 레시피 export라는 세 축이다. 나머지는 4세대+2·+4에서 시작된 작업(Grok 4.6 심판, dark_traffic shadow, SlateContext 연결)을 이어서 정리한 것이다.
배경 Phoenix는 X For You의 랭킹 모델이다. 유저 이력과 후보를 한 시퀀스에 넣고 후보마다 참여 확률을 예측한다. 4세대 이후 X 저장소는 날마다 커밋하는 리듬으로 후속 공개를 이어 왔다. 순서는 8/14 4세대+1(c65aa17) → 8/17 +2(b089ce6) → 8/18 +3(11a71f8) → 8/19 +4(aad7179, 대형) → 8/20 +5(d0cef2f) → 8/21 +6(28e414f, 이번)이다.
4세대부터 +5까지의 canon(이 저장소의 X 정본 문서)은 세 가지 흐름을 다뤄 왔다. 이번 커밋은 이 세 흐름과 방향이 같지만, 광고 매출 예측 헤드를 세분화한다는 새 축이 추가됐다는 점이 4세대~+5와 다르다.
- 첫째는 랭킹 파이프라인의 signal 채널 확장이다(topic feedback → AI trend feedback, SID 3계층,
RewardOutputs·rewardRerankSlotProb,SlateContext).- SID(semantic ID)는 coarse → fine 계층의 codeword 시퀀스로 포스트를 나타내는 ID다.
- 둘째는 PTOS 심판 계층의 세대 전환이다(safemodel↔PtOS disagreement → Grok 4.5 심판 → 4.6 dial 0.1 → 0.3).
- PTOS(PtOS)는 grox 안전 파이프라인에서 정책 기반으로 위반 카테고리를 판정하는 계층이다.
- 셋째는 VF·grox 리소스 튜닝이다(visibility-filtering shadow, grox reply_spam follower 임계).
- visibility-filtering(VF)은 포스트마다 이 뷰어에게 보여도 되는지를 판정하는 서비스다.
- grox는 Grok 기반 nearline 주석 파이프라인이다.
새로 나오는 개념 여섯 가지를 정리하면 다음과 같다.
- MACT(Mobile App Conversion Tracking)는 모바일 앱 광고에서 앱 내 이벤트를 광고 노출·클릭에 귀속시키는 계측 프레임워크다.
- 앱 내 이벤트는 인앱 구매·튜토리얼 완료·회원 가입 등이다.
- 광고주가 SDK로 앱 내 이벤트를 로그하면, X 광고 시스템이 어떤 노출·클릭이 그 이벤트를 유도했는지 대응시킨다.
- 기존 X 랭커 proto에는 앱 설치를 클릭에 귀속하는
ADS_ATTRIBUTED_MACT_CLICK_INSTALL하나만 있었다. 이번에 인앱 이벤트 6개에 click·view 각 2종을 곱한 12개로 늘어났다.
- View-through conversion은 사용자가 광고를 봤지만 클릭하지는 않고, 이후 다른 경로로 컨버전(구매·가입 등)에 이른 경우를 말한다.
- 클릭 기반 귀속(click-through)과 대비되고, 노출만으로도 광고 효과가 있었다고 인정하는 계측 방식이다.
- 이번에 X 랭커가 view-through 계열 8개 액션을 신설했다. purchase·add_to_cart·checkout_initiated·sign_up·site_visit·session·landing_page_view·upper_funnel이다.
- Delayed feedback은 광고 conversion에서 노출·클릭 시점과 실제 컨버전(예: 회원 가입, 인앱 구매) 시점이 몇 시간에서 며칠까지 벌어지는 현상을 가리킨다.
- 학습 시점에 도착한 라벨이 "아직 안 일어남"인지 "안 일어날 것"인지 구분하기 어려워 손실이 왜곡된다.
- 랭커 문헌에서는 delayed conversion prediction이 별도의 연구 분야로 자리 잡았다(Chapelle 2014 이후).
- 이번 커밋에서 X는 각 학습 샘플에
is_delayed_feedback컬럼을 추가했다. 그리고 광고 conversion 헤드는 delayed 샘플만, engagement 헤드는 fresh 샘플만 학습하도록 loss mask를 나눴다.
- Muon optimizer는 Newton-Schulz orthogonalization으로 gradient momentum matrix를 근사 직교화하는 optimizer다.
- Keller Jordan 2024 원문과 Moonshot Kimi K2 계열의 LLM 학습을 거치며 확산됐다.
- 4세대+4(aad7179)에서 X Phoenix 랭커의 dense-parameter 기본 optimizer로 도입됐지만, TRAINING.md는 export 파이썬 코드가 여전히 AdamW라고 명시하고 있었다.
- 이번 커밋에서 문서와 config, reference 학습 스크립트가 flagship과 nano가 Muon으로 학습된다고 명시하는 방향으로 다시 쓰이면서 export와 프로덕션 사이의 간극이 좁혀졌다.
- NVIDIA GB300(Grace Blackwell 300)은 NVIDIA의 2025~2026년 datacenter GPU다. Blackwell 세대에 속하며 B200의 후속이다.
- 각 GB300 노드가 대용량 HBM3e와 CUTLASS/CUTe DSL 기반 attention 커널을 지원한다.
- X가 처음으로 하드웨어 전용 config(batch size, expert parallelism, remat policy, attention impl)를 코드에 명시적으로 드러냈다.
xai-o2object storage는 xAI 내부 스토리지 시스템으로 추정되는 S3 호환 오브젝트 스토리지다.- 이번 커밋에서 클라이언트 crate 인터페이스는 공개됐지만 실제 구현은 오픈소스 빌드에서 빠졌다.
- 프로덕션의 checkpoint와 데이터 저장이
xai-o2로 이어진다고 볼 만한 신호다 추정. - 오픈소스 빌드에서는 표준
object_storecrate로 대체해야 한다.
광고 전환 액션 20개가 proto 171–190번으로 새로 생겼다
새 액션은 MACT 인앱 이벤트를 클릭과 뷰로 나눈 12개와, view-through conversion 8개다 확인.
- Phoenix proto의 액션 목록에 광고 conversion 액션 20개가 새로 정의됐다 확인 —
phoenix/crates/serving/xai-recsys-proto/proto/recsys.proto.정의 위치는
enum ActionName이다.ADS_ATTRIBUTED_MACT_PURCHASE = 171; ADS_ATTRIBUTED_MACT_ADD_TO_CART = 172; ADS_ATTRIBUTED_MACT_LEVEL_ACHIEVED = 173; ADS_ATTRIBUTED_MACT_TUTORIAL_COMPLETE = 174; ADS_ATTRIBUTED_MACT_SIGN_UP = 175; ADS_ATTRIBUTED_MACT_CUSTOM = 176; ADS_ATTRIBUTED_MACT_VIEW_PURCHASE = 177; ADS_ATTRIBUTED_MACT_VIEW_ADD_TO_CART = 178; ADS_ATTRIBUTED_MACT_VIEW_LEVEL_ACHIEVED = 179; ADS_ATTRIBUTED_MACT_VIEW_TUTORIAL_COMPLETE = 180; ADS_ATTRIBUTED_MACT_VIEW_SIGN_UP = 181; ADS_ATTRIBUTED_MACT_VIEW_CUSTOM = 182; ADS_PURCHASE_CONVERSION_VIEW_THROUGH = 183; ADS_ADD_TO_CART_CONVERSION_VIEW_THROUGH = 184; ADS_CHECKOUT_INITIATED_CONVERSION_VIEW_THROUGH = 185; ADS_SIGN_UP_CONVERSION_VIEW_THROUGH = 186; ADS_SITE_VISIT_CONVERSION_VIEW_THROUGH = 187; ADS_SESSION_CONVERSION_VIEW_THROUGH = 188; ADS_LANDING_PAGE_VIEW_CONVERSION_VIEW_THROUGH = 189; ADS_UPPER_FUNNEL_CONVERSION_VIEW_THROUGH = 190;첫째 부류는 MACT 6개 인앱 이벤트에 click/view를 곱한 12개(171–182)다.
- 이벤트는 purchase, add_to_cart, level_achieved, tutorial_complete, sign_up, custom이다.
- purchase는 인앱 구매, level_achieved는 게임 레벨 달성, tutorial_complete는 튜토리얼 완료, sign_up은 회원 가입, custom은 광고주가 정의한 커스텀 이벤트다.
- 이벤트마다
MACT_<이벤트>와MACT_VIEW_<이벤트>두 계열이 있다. - 앞의 것은 click-attributed로, 사용자가 광고를 클릭한 뒤 앱에서 발생한 이벤트다. 뒤의 것은 view-attributed로, 사용자가 광고를 보기만 하고 클릭 없이 앱에서 발생한 이벤트다.
- 기존에는
ADS_ATTRIBUTED_MACT_CLICK_INSTALL(=140) 하나만 있어서 앱 설치만 예측했다. 이제 앱 설치 이후의 6가지 참여 이벤트를 클릭·뷰 각각으로 12개 헤드로 예측한다.
둘째 부류는 view-through conversion 8개(183–190)다.
- 대상은 purchase / add_to_cart / checkout_initiated / sign_up / site_visit / session / landing_page_view / upper_funnel이다.
- 각각
<conversion>_VIEW_THROUGH라는 이름을 쓴다. - 이미 있던 click-attributed conversion(
ADS_PURCHASE_CONVERSION= 200 등)과 짝을 이루도록 view-through 대응 헤드를 신설했다. - 웹 컨버전 8개(광고주 사이트 방문·구매·가입 등)를 view-through로도 예측한다.
ads_slim과 ads_conversion_view_through 엔게이지먼트 그룹 매핑
광고 conversion 매핑에 MACT 항목 12개가 추가됐고, 광고 랭킹 config가 골라 쓸 수 있는 새 metric group이 생겼다 확인.
constants.py에 새 매핑이 들어왔다 확인 —phoenix/xrex/data/recsys/constants.py.기존
ads_conversion_engagement_to_action_types에 MACT 매핑 12개를 새로 추가했다."IsAttributedMactPurchase": ["AdsAttributedMactPurchase"], "IsAttributedMactAddToCart": ["AdsAttributedMactAddToCart"], ... "IsAttributedMactViewCustom": ["AdsAttributedMactViewCustom"], "IsPurchaseConversionViewThrough": ["AdsPurchaseConversionViewThrough"], "IsAddToCartConversionViewThrough": ["AdsAddToCartConversionViewThrough"], ... "IsUpperFunnelConversionViewThrough": ["AdsUpperFunnelConversionViewThrough"],그리고
ads_slim이라는 metric group을 새로 만들었다. signal 7개에 더해, 앞의 conversion click 매핑 가운데 primary가 아닌 항목만 걸러서 넣었다.ads_slim_engagement_to_action_types = { "IsOpenLink": ["ClientTweetOpenLink"], "IsExternalLinkLongDwelled": ["ClientTweetExternalLinkLongDwelled"], "IsVideoQualityViewed": ["ClientTweetVideoQualityView"], "IsNotInterestedIn": ["ClientTweetNotInterestedIn"], "IsBlockAuthor": ["ClientTweetBlockAuthor"], "IsReported": ["ClientTweetReport"], "IsMuteAuthor": ["ClientTweetMuteAuthor"], **{ eng: actions for eng, actions in ads_p_conv_click_engagement_to_action_types.items() if eng not in primary_engagement_to_action_types }, }즉 광고 랭커의 "slim" 라인은 긍정 신호 3개와 부정 신호 4개, 그리고 primary가 아닌 conversion click만 학습한다.
- 긍정 신호 3개는 open link, external link long dwell, video quality view다.
- 부정 신호 4개는 not interested, block, report, mute다.
metric group 라우팅 딕셔너리에도
"ads_slim": ads_slim_engagement_to_action_types를 등록해서, 광고 랭킹 config가 이 그룹을 골라 쓸 수 있도록 준비했다.
- 아래 delayed feedback 분기에서 쓸 conversion 헤드 목록을 신설했다.
- 이름은
SOURCE_SPLIT_CONVERSION_HEAD_INDICES다. - click-conditioned action index 리스트와 view-through action index 리스트를 이어 붙인 목록이다.
- 이름은
- 사용자가 검색 결과가 마음에 들지 않아 쿼리를 다시 작성하면, 이를 부정 피드백 헤드에 포함한다. 검색 지면 랭킹이 이 신호를 학습하게 된다.
NEGATIVE_FEEDBACK_HEAD_INDICES에CLIENT_TWEET_SEARCH_QUERY_REFORMULATED를 추가했다.- 4세대+4의
xrecsys_searchconfig 확장과 이어지는 신호다.
Delayed feedback을 학습 손실에서 분기하는 설정이 생겼다
새 설정을 켜면 delayed 샘플은 conversion 헤드만, fresh 샘플은 engagement 헤드만 학습한다 확인.
배치에
sample_source필드가 새로 생겼다 확인 —phoenix/xrex/data/recsys/recsys_batch.py:980.sample_source=( record_batch.column("is_delayed_feedback") .fill_null(False) .to_numpy(zero_copy_only=False) .astype(np.bool_) .reshape(-1, 1) if "is_delayed_feedback" in record_batch.schema.names else np.zeros((batch_size, 1), dtype=np.bool_) ),- kafka/parquet record batch에
is_delayed_feedback컬럼이 있으면 그 값을sample_source로 배치에 담고, 없으면 전부 False로 채운다. - True는 이 샘플이 delayed feedback 재관측분이라는 뜻이다.
feature_config.py의 required columns 리스트에is_delayed_feedback과conversionKeepMask두 컬럼이 추가되면서 upstream 인디케이터가 확정됐다.
- kafka/parquet record batch에
샘플 상태에 따라 loss mask가 갈린다 확인 —
phoenix/xrex/models/recsys_model.py.분기는
loss()함수에 있다.if self.config.split_head_training_by_source and delayed_mask is not None: conv_head_split_mask = ( jnp.zeros(num_actions).at[jnp.array(SOURCE_SPLIT_CONVERSION_HEAD_INDICES)].set(1.0) ) eng_head_mask = 1.0 - conv_head_split_mask is_delayed = delayed_mask[:, :, None] loss_mask = loss_mask * (1 - is_delayed * eng_head_mask) # delayed 샘플에서는 engagement head 손실 0 loss_mask = loss_mask * (1 - (1 - is_delayed) * conv_head_split_mask) # fresh 샘플에서는 conversion head 손실 0각 학습 샘플은 fresh와 delayed 두 상태 중 하나다. fresh는 원본 impression이고, delayed는 conversion 도착 후 재관측분이다.
fresh 샘플은 engagement 헤드(favorite·reply·click 등)만 학습하고, conversion 헤드는 mask=0으로 둔다.
delayed 샘플은 conversion 헤드만 학습하고, engagement 헤드는 mask=0으로 둔다. 여기서 conversion 헤드는 위의 신설 20개와 기존 click-conditioned 액션이다.
메트릭 슬라이스를 신설했다. 세 슬라이스를 관측할 수 있도록
build_metric_masks에 분기를 추가했다.fresh: delayed=0delayed_clicked: delayed=1 & clicked=1delayed_non_clicked: delayed=1 & clicked=0
condition_conversion_on_click이 필수 전제조건(assert)이므로, delayed 샘플이라도 "click 있었나"라는 조건 아래에서 conversion을 예측하는 conditional 학습을 그대로 유지한다.mact_in_app_loss_weight는 MACT 액션 12개에만 곱하는 loss weight이고, 기본값은 1.0이다.- config 파라미터로 노출돼 있어서 광고 conversion 헤드 12개의 학습 강도를 조정할 수 있다.
stats["mact-in-app-loss-weight"]로 관측한다.
train_view_through_heads는 view-through 헤드 8개를 학습할지 결정하는 게이트다.- False로 두면 view-through head의 손실 mask를 0으로 만들어 학습하지 않는다.
- 즉 view-through 예측은 config에서 opt-in으로 켜야 한다.
conversion_keep_mask/conversionKeepMask는 후보마다 conversion 학습에서 제외할지를 표시하는 마스크다.from_record_batch에서 kafka의conversionKeepMask컬럼을 읽고, null인 행은 True로 대체한다(=keep).loss()에서 이 마스크를target_padding_mask와 AND 연산해 conversion 학습 대상에서 제외한다.- 주의할 점이 있다. sequence packing에서는
all(True)만 허용하고, 아직 연결되지 않은 경우는 assert로 명시적으로 실패시킨다.
의미 delayed feedback modeling은 광고 conversion 예측의 표준 문제다. X는 이제 delayed 재관측분을 별도 kafka 이벤트로 흘려보내고, 랭커 학습에서 손실을 분기해 fresh는 engagement로, delayed는 conversion으로 갈라 학습할 수 있는 설정을 만들었다. 그 설정 split_head_training_by_source의 기본값은 False라서 아직 opt-in이다 확인 — recsys_model.py:560@28e414f. 이는 duplicate labeling 문제(같은 impression을 fresh와 delayed 두 이벤트로 두 번 로그하는 문제)에 대한 손실 정합성 장치다. 또 conversion 예측 헤드가 20개로 늘어나는 가운데 각 헤드의 학습 signal이 충분히 갱신되도록 유지하는 인프라 뒷받침이기도 하다.
Muon optimizer가 export에 정식으로 포함됐다
export 문서가 이제 flagship 랭킹 config와 nano가 프로덕션 Muon 레시피로 학습한다고 명시한다 확인.
TRAINING.md가 다시 쓰였다 확인 —
phoenix/TRAINING.md.이전 문구는 export의 dense-parameter optimizer를 standard Optax AdamW라고 적었다.
The dense-parameter optimizer in this export is standard Optax AdamW. The internal deployment uses a tuned RMS-normalized-Adam derivative in that optimizer slot.
바뀐 문구는 Muon과 AdamW 두 계열을 적는다. Muon은 flagship 랭킹 config와
home_direct_packed_nano가 쓰고, AdamW는 나머지 config가 쓴다.Two dense-parameter optimizer families ship in this export:
- Muon (
xrex/optimizers/recsys/muon.py): the production home-ranker recipe — consistent-RMS scaling with decoupled weight decay on the matrix and embedding partitions. The flagship ranking configs andhome_direct_packed_nanoselect it (optim="muon"), so the nano trains with the same dense-optimizer recipe production runs. - Standard Optax AdamW: the slot used by the remaining shipped configs (two-tower retrieval, gen-recs, and the legacy ranking presets). ...
- Muon (
README.md 역시 Muon 쪽으로 다시 쓰였다.
- 이전 문구는 "one training-recipe exception ... standard AdamW rather than production's tuned internal variant"였다.
- 바뀐 문구는 "The flagship ranking configs and the nano twin train the production Muon recipe, which ships in full"다.
reference/README.md의train_step.py항목도 "dense optimizer + rowwise-AdaGrad"로 일반화됐고, flagship과 nano가 Muon을 사용한다고 명시했다.
임베딩 optimizer도 정본에 함께 반영됐다. flagship과 nano가 임베딩 rowwise AdaGrad에서 세 옵션을 함께 쓴다고 TRAINING.md가 명시했다.
- 세 옵션은 accumulator half-life decay, lazy per-row decay, decoupled weight decay다.
- 4세대+4의
emb_optim_config(LR 0.28, half-life 2500 step, lazy_decay, weight_decay 2.8e-4) 파라미터가 정본으로 확정된 셈이다.
의미 이번 커밋으로 export되는 학습 파이썬 코드를 그대로 실행하면 프로덕션 Muon 레시피와 같은 학습이 되는 상태로 옮겨 갔다. 4세대+4(2026-08-19)에 Muon이 코드로 도입됐을 때 canon의 이해는 "코드는 export됐지만 학습 레시피는 여전히 프로덕션 특화"였다. 재현성에 관한 문서는 간극이 줄었지만 데이터와 인프라는 여전히 공개되지 않았다(README의 caveat은 그대로 남아 있다).
GB300 하드웨어 전용 오버라이드가 처음 등장했다
config에 GB300 하드웨어 전용 오버라이드가 정의됐다 확인. export에 포함된 nano 설정도 이 오버라이드를 상속한다.
_GB300_OVERRIDES가 정의됐다 확인 —phoenix/xrex/configs/xrecsys.py:330-345._GB300_OVERRIDES = { "bs_per_device": 512, "ep": 64, "attn_impl": "cutedsl_ranker_varlen_attn", "remat_policy": RematType.SAVE_GB300_RECSYS, "unroll_layer_stack": True, "learning_rate": 5e-4, "checkpoint_every_n": 300, "optim_config": RecsysDenseOptimConfig( optim="muon", muon_consistent_rms=0.2, muon_matrix_weight_decay=0.01, muon_split_fused="qkv:128", adam_embedding_weight_decay=0.01, b1=0.95, b2=0.98, ... ), ... }bs_per_device=512는 이전_home_direct_packed_nano의bs_per_device=64보다 8배 크다. GB300의 넉넉한 HBM 용량을 반영한 값이다.ep=64는 expert parallelism을 64-way로 둔다. MoE(mixture-of-experts) 방식의 sharding이다.attn_impl="cutedsl_ranker_varlen_attn"은 NVIDIA CUTLASS/CUTe DSL 기반의 variable-length attention 커널이다. GB300에 맞춰 최적화한 새 attention 구현이다.remat_policy=RematType.SAVE_GB300_RECSYS는 GB300 메모리 계층에 맞춘 rematerialization 정책이다. 무엇을 checkpoint하고 무엇을 재계산할지 정한다.unroll_layer_stack=True는 트랜스포머 layer stack을 unroll해서(순환 대신 명시적으로 나열해서) 컴파일한다. XLA가 최적화할 여지가 넓어진다.learning_rate=5e-4는 4세대+4의 flagship base LR 7.1e-4보다 조금 낮다. bs_per_device가 8배로 커진 데 맞춘 조정으로 보인다 추정.checkpoint_every_n=300은 300 step마다 checkpoint를 저장한다.
_home_direct_packed_nano가 이 오버라이드를 상속한다.- 이전에는
learning_rate=2e-3으로 따로 두었다. - 이번 커밋에서는
_GB300_OVERRIDES의 LR과 optim_config, emb_optim_config를 그대로 상속한다. - 즉 export에 포함된 "nano" 모델조차 GB300 학습을 전제로 조정됐다.
- 이전에는
의미 X가 하드웨어 세대 전환을 코드에 드러낸 첫 사례다. 이전 canon은 "Phoenix가 어느 GPU에서 도는지 미공개"였는데, 이제 GB300이 프로덕션 표준 하드웨어라는 강한 신호가 나왔다. NVIDIA GB300은 2025년 하반기에 출하를 시작했고 Blackwell B200의 후속이다. LLM 회사들이 대량으로 확보하는 최신 세대인데, xAI가 이를 랭커 학습과 서빙 파이프라인에 그대로 적용했다. cutedsl_ranker_varlen_attn은 xAI가 직접 만든 커널일 가능성이 있는데, 오픈소스 커널 카탈로그에는 이 이름이 없다 추정.
xai-o2 스토리지 crate의 인터페이스가 공개됐다
S3 호환 오브젝트 스토리지 클라이언트의 인터페이스 crate가 새로 들어왔다 확인. 오픈소스 빌드에서는 이 클라이언트를 만들려고 하면 항상 에러가 난다.
xai-o2crate를 신설했다 확인 —phoenix/crates/storage/xai-o2/.Cargo.toml의 description은 아래와 같다.
Client interface for the xai-o2 (S3-compatible) object storage backend
파일은 3개이고, 합쳐서 134줄이다.
base_client.rs(12줄)는BaseO2Clientasync trait이다.put(key, data) -> Result<()>하나만 정의한 최소 인터페이스다.o2_client_builder.rs(115줄)의O2ClientBuilder는 builder method 15개를 노출한다. 앞의 여덟 개는 endpoint·bucket·prefix·access_key_id·secret_access_key·allow_anonymous·timeout·io_timeout이다. 나머지 일곱 개는 retry_max_times·retry_min_delay·retry_max_delay·write_chunk_size_mib·write_chunk_concurrency·max_concurrent_write_requests·trace_always다.lib.rs(7줄)에는pub mod선언만 들어 있다.
O2ClientBuilder::build()는 오픈소스 빌드에서 항상 다음 에러를 반환한다.pub const BACKEND_UNAVAILABLE: &str = "the xai-o2 backend is not included in this build; use the default object_store backend"; pub fn build(&self) -> Result<Arc<dyn BaseO2Client>> { Err(anyhow::anyhow!(BACKEND_UNAVAILABLE)) }build()가 돌려주는 에러 문구는"the xai-o2 backend is not included in this build; use the default object_store backend"다.- 즉 인터페이스 스터브만 공개했고 실제 구현체는 오픈소스 빌드에 포함하지 않았다.
xai-recsys-enginecrate가xai-o2 = { workspace = true }로 의존성을 선언하기는 했지만, 실제로 사용하려면 다른 백엔드로 대체해야 한다.
의미 xAI 내부에 xai-o2라는 이름의 S3 호환 오브젝트 스토리지 시스템이 있다는 사실이 처음 확정됐다. builder 파라미터 15개는 프로덕션에서 실제로 조정하는 항목의 목록이라 의미가 있다. 테스트(build_fails_loudly_after_full_configuration)에는 실제 운영에 가까운 값이 들어 있다. chunk 8MiB, concurrency 16, max_concurrent 64, retry 6회, min/max delay 1/32s, timeout 300s다. 프로덕션의 checkpoint와 데이터 저장이 여기에 연결된다는 신호다 추정. 오픈소스 빌드는 표준 object_store crate(AWS S3 등)로 대체해야 한다.
SlateContext가 NextActionDistribution에도 실린다
후보마다 action별 예측 분포를 담는 proto 메시지에 slate context 필드가 추가됐다 확인.
NextActionDistribution메시지에 필드 9번으로SlateContext가 들어갔다 확인 —phoenix/crates/serving/xai-recsys-proto/proto/recsys.proto.message NextActionDistribution { map<uint32, float> indexToContinuousValues = 7; RewardOutputs rewardOutputs = 8; + SlateContext slateContext = 9; }- 이 값은 4세대+4에서
ScoreInfo.slateContext로 도입됐다. 재순위 슬레이트의 최종 결정에 함께 실리는 컨텍스트다.
- 이 값은 4세대+4에서
의미 ScoreInfo와 NextActionDistribution 두 자리 모두에 slate context가 실린다는 점을 관찰할 수 있다. 앞의 것은 재순위 슬레이트 결정 자리이고, 뒤의 것은 액션 예측 자리다. 4세대+4(aad7179)에서 SlateContext는 ScoreInfo 메시지의 필드로 처음 등장했다. 구성은 pool_rank·fatigue·pre_diversity·SID 3계층·SID gap 3계층이다. 4세대+5(d0cef2f)에서는 PostCandidate.served_slate_context가 연결돼 home-mixer가 이 값을 소비하기 시작했다. home-mixer는 For You와 Following 피드 요청을 받는 서비스다. 이번 4세대+6에서는 NextActionDistribution(각 후보의 action별 예측 분포)에도 SlateContext가 들어간다. 즉 Phoenix가 서빙 응답에서 후보 하나에 대해 각 action별 예측 분포를 돌려주고, 각 액션 분포 옆에 slate context를 함께 실어 보낼 수 있게 된다.
정본 반영 ../services/x.md의 광고 절에 ### 광고 conversion 액션 대폭 확장 — 4세대+6 (2026-08-21, 28e414f) 절을 신설했다. 나머지 4세대+6 항목은 절을 새로 만들지 않고 해당 절 안에 문단으로 넣었다. Muon export 승격과 GB300 override, xai-o2 stub, SlateContext per-action은 Phoenix 학습·인덱스 절에 넣었다. grox 임계 80k와 dark_traffic staging, Grok 4.6 100% 전환은 각각의 절에 반영했다.
grox reply_spam follower 임계가 60k에서 80k로 올랐다(4번째 상향, 1.33배)
스팸 판정과 답글 랭킹에 쓰는 follower 임계가 둘 다 60,000에서 80,000으로 올랐다 확인.
task_filter.py의 두 임계 상수가 함께 바뀌었다 확인 —grox/flows/reply_spam/task_filter.py.- FOLLOWER_COUNT_THRESHOLD_FOR_SPAM_DETECTION = 60000 + FOLLOWER_COUNT_THRESHOLD_FOR_SPAM_DETECTION = 80000 ... - FOLLOWER_COUNT_THRESHOLD_FOR_REPLY_RANKING = 60000 + FOLLOWER_COUNT_THRESHOLD_FOR_REPLY_RANKING = 80000임계는 2026-08-13부터 8일 동안 다섯 단계를 거쳤다 확인.
- 15,000(2026-08-13 이전)
- 30,000(2026-08-14, 4세대+1)
- 40,000(2026-08-17, 4세대+2) —
task_filter.py@b089ce6 - 60,000(2026-08-18, 4세대+3)
- 80,000(2026-08-21, 4세대+6)
이전 세 번은 2배, 1.33배, 1.5배(15→30, 30→40, 40→60)였고, 이번 1.33배는 직전보다 상승 폭이 작다.
의미 스레드 root와 답글 대상이 모두 이 임계 이하인 답글만 grok 스팸 판정을 받으므로, 임계가 오를수록 스팸 판정이 더 큰 계정의 답글까지 담당하게 된다. 하나라도 임계를 넘는 답글이 받는 답글 랭킹 채점은 그만큼 좁아진다 확인 — task_filter.py:77-92, 214-225. 80k에서 상승이 느려진 이유로는 grok 예산의 병목에 가까워졌거나, 그보다 큰 계정에서는 스팸 답글 유입이 적어 marginal utility가 줄었거나, 다음 단계에서 임계 없이 전면 적용으로 넘어갈 준비 중이거나, 셋 중 하나를 생각해 볼 수 있다 추정.
visibility-filtering dark_traffic 타깃이 shadow에서 staging으로 바뀌었다
VF의 dark_traffic 미러링 대상이 프로덕션 shadow 워크로드에서 staging 환경으로 바뀌었다 확인.
dark_traffic_setup.rs에서 shadow 워크로드 상수가 빠지고 staging 목적지가 들어왔다 확인 —visibility-filtering/dark_traffic_setup.rs.-const SHADOW_WORKLOAD: &str = "xai-vf-shadow"; +pub const STAGING_XDS_DEST: &str = "xai-vf-service.staging.visibility:grpc"; +const FORWARDER_NAME: &str = "staging"; + +pub fn staging_tls_domain(dc: &str) -> String { + format!("visibility.visibility-filtering-service.staging.{dc}.s2s.twttr.net") +} ... -struct StaticShadowDiscovery; +struct StaticStagingDiscovery;- 새 목적지는
xai-vf-service.staging.visibility:grpc다.
- 새 목적지는
dark_traffic 인프라는 2일 전인 4세대+4(aad7179, 2026-08-19)에 도입됐다. 이번에 프로덕션 shadow 워크로드에서 스테이징 환경으로 이름을 바꿨다.
- 대상은
xai-vf-shadow.prod.visibility에서xai-vf-service.staging.visibility로 바뀌었다. - TLS 도메인도
visibility.visibility-filtering-service.prod.{dc}.s2s.twttr.net에서.staging.으로 옮겼다. StaticShadowDiscovery구조체 이름도StaticStagingDiscovery로 바꿨다.
- 대상은
의미 ⚠ 4세대+4의 shadow 서술을 다시 해석해야 한다. dark_traffic이 프로덕션 트래픽을 프로덕션 shadow 워크로드에 미러링하는 구조에서, 프로덕션 트래픽을 staging 환경으로 미러링하는 구조로 바뀌었다. 2일 만에 방향이 바뀐 셈이다. 해석은 두 가지다.
- Shadow 워크로드를 따로 유지하지 않고 staging 환경을 shadow처럼 활용하는, 인프라 정리 차원의 변경이다.
- 프로덕션 트래픽의 shadow가 staging 배포를 검증하는 데 쓸 만하다고 판단했다.
어느 쪽이든 VF 실험이 프로덕션 shadow 자원이 아니라 staging 자원 예산에서 돌아가게 됐다. 4세대+5까지의 canon은 "shadow deploy 기반이 배선됐다"였는데, 이제 그 기반이 staging 미러링으로 다시 정의된다.
Grok 4.5 internal이 완전히 빠지고 PTOS 심판이 100% Grok 4.6 internal로 전환됐다
PTOS 분류기에서 Grok 4.5 internal 관련 코드가 삭제됐고, 확률 분기가 있던 두 호출은 이제 항상 4.6 internal 계열 모델을 쓴다 확인.
classifier.py에서 4.5 internal 관련 코드를 지웠다 확인 —grox/flows/ptos/classifier.py(+6/−63).- 삭제한 항목은 다음과 같다.
_EAPI_4_5_INTERNAL_BREAKER_CONFIG(circuit breaker config)_eapi_4_5_internal_breaker(breaker instance)_GROK_4_6_INTERNAL_DIAL상수 (하루 전에는 0.3이었다)- 두 클래스에서
self.eapi_4_5_internal초기화 제거 _sample_4_5_internalmethod 제거
_cross_model_validate_with_4_5에서 확률 분기를 없앴다. 이제 항상 4.6 internal을 직접 호출한다.classify_policy_for_violation의 deluxe 4.3 재분류에서 확률 분기를 없앴다. 이제 항상deluxe-4.6-internal을 사용한다.
- 삭제한 항목은 다음과 같다.
의미 2026-08-19 커밋(4세대+4)에 10%로 시작해 4세대+5에서 30%가 된 4.6 internal dial-up이 이번 4세대+6의 100% 전환으로 끝났다. dial이 걸린 곳은 4세대 공개부터 있던 아동 안전 cross-validation과 deluxe 재분류이고, 2026-08-17 커밋(4세대+2)의 성인 콘텐츠 심판은 아니다 확인 — classifier.py:114, 281, 477. Grok 4.5 internal은 분류기 코드와 grox config의 ModelName 목록에서 모두 빠져 롤백할 수 없다 확인 — grox/config/config.py:79-92. 4세대+5까지의 canon은 "dial의 다음 이동이 관전 포인트 — 50%/100%로 뛰면 완전 전환 근접"이었는데, 이번에 그 관전 포인트가 해소됐다.
retrieval post-embedding 계산을 균등 shard에서 65k 단위로 나눠 처리한다
retrieval post-embedding 계산은 균등 shard일 때만 shard를 최대 65,536행씩 나눠 처리하고, 그 외에는 이전처럼 shard 전체를 한 번에 처리한다 확인.
maybe_build_retrieval_post_embeddings를 리팩터해_forward_chunked헬퍼를 신설했다 확인 —phoenix/xrex/train/trainer_recsys.py.- 기존에는 shard 전체를
candidate_tower_forward_jit한 번의 호출로 계산했다. - 이제는
total_samples % data_world_size == 0인 균등 shard 케이스일 때만_chunk_rows = min(_shard_rows, 65536)으로 chunk에 진입한다. - 그 외에는 shard 전체를 한 번에 처리한다(
_chunk_rows = _shard_rows). - chunk 단위로
jax.make_array_from_process_local_data→_lookup→candidate_tower_forward_jit→ local shard concatenate 순으로 계산한 뒤 마지막에 concat한다. head_dataset_mapping에 head가 여럿인 경우에도 head마다_forward_chunked를 호출해 같은 방식으로 청킹한다.
- 기존에는 shard 전체를
의미 retrieval two-tower의 post embedding 배치가 커지면 XLA 컴파일과 실행 시점에 OOM이 발생할 위험이 있어서 65k row 단위로 잘라 순차 처리한다. Muon 승격과 GB300 config로 학습 규모가 커지는 흐름에 맞춰 서브루틴을 안정화한 것이다.
inference hotswap에 --hotswap_malloc_trim CLI가 추가됐다
inference hotswap 사이클이 끝날 때마다 glibc의 메모리 정리 함수를 부르는 옵션이 생겼고, 기본값은 True다 확인.
- inference 코드에 새 옵션이 들어왔다 확인 —
phoenix/xrex/inference/model_runner.py,launch_inference.py.InferenceModelRunner필드hotswap_malloc_trim: bool = True가 생겼다.- CLI 인자
--hotswap_malloc_trim이 생겼고, 기본값은 True다. _malloc_trim_after_swap()메소드는 hotswap cycle이 끝날 때마다_glibc_malloc_trim()(= libcmalloc_trim(0))을 호출한다.- 관측은
metrics_publisher.checkpoint_reload_step_seconds.labels(step="malloc_trim").observe(secs)로 한다.
CLI 도움말이 이 문제를 명시하고 있다.
glibc malloc_trim(0) on the coordinator thread after each applied hotswap; returns the cycle's freed pages (glibc slots otherwise ratchet ~196 MiB/cycle). No-op under jemalloc.
의미 Phoenix 서빙에서 embedding table hotswap이 glibc 메모리 할당자 아래에서 사이클마다 약 196MiB씩 RSS를 누수해 왔다는 사실이 처음으로 문서에 드러났다. embedding table hotswap은 주기적으로 새 임베딩 checkpoint를 프로세스 안에서 교체하는 작업이다. libc의 malloc_trim(0)을 호출해 해제된 페이지를 OS에 반환한다. 다른 할당자인 jemalloc에서는 의미가 없다. 프로덕션 서빙을 다듬는 작은 패치지만, hotswap이 프로덕션에서 오래 돌아가는 인프라라서 이 패치가 필요할 만큼 부담이 쌓였다는 점을 함께 관찰할 수 있다.
two-tower retrieval의 nano config에 옵션 세 개가 추가됐다
retrieval two-tower의 소형 학습 config(nano)에 attention과 위치 인코딩 옵션 세 개가 추가됐다 확인. LLM 학습에서 흔히 쓰는 안정화 기법이 retrieval two-tower로 넘어왔다.
- 추가된 곳은
MODEL_CFGS의xrecsys_two_tower_nano항목이다 확인 —phoenix/xrex/configs/xrecsys_two_tower.py:429-431.- 같은 파일의
_xrecsys_two_tower_combined_base()는 이 커밋에서 바뀌지 않았고qk_norm=False·attn_logit_cap=80.0을 유지했다(xrecsys_two_tower.py:273-274). "qk_norm": True는 attention query/key normalization을 켠다."attn_logit_cap": -1은 attention logit capping을 끈다(-1 = 미사용)."right_anchored_rope": True는 RoPE positional encoding을 right-anchored로 둔다. 시퀀스 끝을 기준으로 앵커링한다는 뜻이다.
- 같은 파일의
EVERGREEN retrieval dataset이 5년에서 4~14일로 줄었다
EVERGREEN retrieval dataset이 참조하는 비디오 범위가 크게 줄었다 확인.
retrieval_dataset.py의EVERGREEN항목이 가리키는 parquet 파일이 바뀌었다 확인 —phoenix/xrex/data/retrieval_dataset.py:255-258.EVERGREEN = ( 5, - _idx("post_sid_v5_256x6_snapshots/evergreen_video_1825day.parquet"), - _idx("post_sid_v5_256x6_snapshots_backup/evergreen_video_1825day.parquet"), + _idx("post_sid_v5_256x6_snapshots/video_4to14day.parquet"), + _idx("post_sid_v5_256x6_snapshots_backup/video_4to14day.parquet"), )이름은 여전히 EVERGREEN이지만 실제로 참조하는 대상은 1825일(5년)에서 4~14일 이내의 최신 비디오로 크게 줄었다.
최근 4~14일 구간의 비디오를 evergreen slot에 넣어 보는 실험으로 보인다 추정. 이름과 실체가 어긋나 있어서 임시 변경일 가능성이 있다.
그 밖의 작은 패치
요청 메트릭, checkpoint 로딩, kafka 로더, grox config에 작은 패치가 들어갔다.
- request_metrics.rs의 이중 카운트를 고쳤다.
deadline_admission으로 shed된 요청이 그 이후 gRPCResourceExhausted상태 코드 때문에inflight_cap으로도 카운트되던 문제를guard.recorded_rejectflag로 막았다.- 테스트 두 개를 새로 추가했다:
deadline_shed_does_not_double_count_as_inflight_cap,resource_exhausted_without_prior_reject_counts_inflight_cap.
- 테스트 두 개를 새로 추가했다:
- checkpoint metadata 처리를 견고하게 했다. completion marker(
COMPLETED_FILENAME)만 있고 실제 orbax tmp-dir 커밋(rename)은 되지 않은 checkpoint를 건너뛴다._ORBAX_TMP_PREFIX를 공개 상수ORBAX_TMP_DIR_SUFFIX로 이름을 바꿨다. 새 이름이 suffix 시맨틱과 맞다.- 건너뛰는 판단은
_has_committed_payload와_is_loadable_checkpoint헬퍼가 한다. - checkpoint 도중 크래시가 난 상황에서 오염된 checkpoint를 읽지 않도록 막는다.
- kafkaloader에
exclude_required_columns가 생겼다. 특정 kafka feed가 일부 required column을 제외할 수 있게 했다.- 예상한 컬럼이 없을 때 경고 로그와 schema-mismatch 에러 경로를 모두 건너뛴다.
- grox
config.py에서 1줄을 제거했다. 사소한 정리라서 구체적인 diff는 생략한다.
정본 반영 ../services/x.md의 다음 부분을 고쳤다.
광고절에 MACT/view-through 20 액션,ads_slimmetric group, delayed feedback split 관련 단락을 추가했다.Phoenix 학습·인덱스 (4세대에서 신설)에 Muon export 승격·GB300 override·xai-o2 stub 문단을 추가했다.안전성하위 절에서 dark_traffic의 shadow → staging 서술을 뒤집었다.- PTOS 심판 절에 4세대+6에서 마무리된 서술(4.5 internal 완전 제거)을 반영했다.
열린 질문에### 4세대+6 공개로 해소된 것 / 신규 (2026-08-23)절을 추가했다.출처에 28e414f 커밋과 관련 파일 링크를 추가했다.
의미 4세대+6에서 광고 매출 예측 스택이라는 네 번째 큰 축이 추가됐다. 4세대부터 +5까지 정본의 서술축은 랭킹 signal 채널 확장·SID 3계층·RewardOutputs, PTOS 심판 세대 전환, VF·grox 리소스 튜닝, 파이프라인 v3 마이그레이션이었다. 새 축에서 바뀐 것은 다음과 같다.
- proto ActionName 20개를 신설해 전체 액션 수를 크게 늘렸다.
- delayed feedback을 학습 손실 분기로 정식 처리했다. X 랭커 학습이 광고 delayed conversion prediction 문헌과 명시적으로 정렬됐다.
ads_slimmetric group으로 광고 랭킹 라인의 signal 조합을 분리했다.mact_in_app_loss_weight와train_view_through_headsconfig로 광고 헤드의 학습 강도를 조정하는 수단을 노출했다.
동시에 Muon 학습 레시피가 export에 정식으로 포함되고 GB300 하드웨어 전용 config가 처음 등장하면서, 재현 가능한 학습 인프라와 하드웨어 세대 확인이 나란히 도착한 사이클이 됐다. xai-o2 stub 공개는 인프라의 어느 스토리지 계층에 xAI 자체 시스템이 있는지를 처음으로 명시한다. dark_traffic이 shadow에서 staging으로 다시 정의된 것은 단순히 문서에 남길 관찰이 아니라 canon이 반영해야 할 4세대+4의 재해석이라서 의미가 있다.
검증에서 뒤집힌 것
- VF dark_traffic이 프로덕션 shadow에서 staging 미러링으로 재정의됐다. 4세대+4(2026-08-19)를 정본에 반영하던 시점의 서술은 "프로덕션 트래픽을
xai-vf-shadow.prod.visibility에 미러링"이었다. 2일 만에 shadow 워크로드 참조가 삭제되고 staging 환경으로 리다이렉트됐다.../services/x.md의 4세대+4 dark_traffic 절 서술을 이번 4세대+6 반영에서 다시 썼다.
레이더
없다. arXiv cs.IR 8/19~8/20 신규 30건을 triage했고 통과한 항목은 0건이다. 대부분 학술 논문(dense retrieval RL, sequential rec benchmarks, financial RAG 등)이고 프로덕션 진술이 담긴 항목은 없었다. Meta engineering과 Netflix tech blog에는 새 글이 없다(Meta의 최신 글은 2026-08-12, Netflix의 2026-08-21 글은 Flink autoscaler를 다룬 순수 인프라 내용이다).
다음에 볼 것
- 샘플이 delayed feedback 재관측분인지 표시하는 컬럼의 upstream 파이프라인 — kafka에 이 컬럼을 실어 보내는 컨버전 재관측 이벤트 로거의 소스가 코드에 없다. Server-side conversion tracking API 어딘가일 텐데 오픈소스 빌드에는 포함되지 않았다. 다음 커밋에서 이 컬럼의 소스가 드러날 가능성이 있다.
- 대상 컬럼은
is_delayed_feedback이다.
- 대상 컬럼은
split_head_training_by_source프로덕션 활성 시점 — config의 기본값이 False라 아직은 opt-in이다._home_direct_packed_base()나 flagship config에서 True로 승격되는 시점이 광고 conversion 학습 파이프라인의 프로덕션 전환 신호다.train_view_through_heads승격 — view-through 헤드 8개가 opt-in 상태다. 이 값이 True로 승격되면 X 광고 랭킹이 view-through conversion을 정식으로 예측하기 시작한다는 뜻이다.- GB300 config의 실제 사용 —
_GB300_OVERRIDES가home_direct_packed_nano에 상속됐지만 flagship 랭킹 config에는 아직 명시적으로 상속되지 않았다(base가 이미 4세대+4의 Muon으로 설정돼 있고 별개다). GB300 override 상속이 flagship에도 적용되는지가 관측 지점이다. xai-o2실제 구현 공개 여부 — stub이 나왔다는 것은 인터페이스가 안정화됐다는 뜻이지만, 실제 구현이 오픈소스 빌드에 들어올 가능성은 낮다 추정.EVERGREENretrieval dataset의 재정의 — 이름이 실체와 어긋나 있어서 다음 커밋에서 이름을 바꾸거나(EVERGREEN → SHORT_TERM_VIDEO) 원래대로 되돌리는지 관측한다.- Grok 4.6 internal이 완전히 전환됐으므로 다음 심판 세대(4.7?)가 시작되는 시점 — 4.5에서 4.6으로 넘어가는 dial이 사흘(08-19~08-21) 동안 0.1→0.3→1.0으로 날마다 정교해졌다. 4.6이 완전히 자리 잡은 뒤 4.7 shadow가 언제 시작되는지가 다음 세대 전환의 신호다.