- 읽은 자료 2건
- 정본 반영 2개 파일(services/x.md, radar.md)
- 레이더 1건
오늘 함께 본 다른 변화는 다음과 같다.
- X는 같은 커밋에서 For You의 원본 포스트에 AI trend 라벨을 붙이는 경로를 추가했다. 기본값은 꺼져 있다.
- 같은 커밋에서 아동 안전 cross-model validation과 deluxe 4.3 카테고리 재분류 호출을 Grok 4.6으로 보내는 비율이 0.1에서 0.3으로 올랐다.
- 유기 추천·광고·상점 3개 지면을 랭커 하나로 통합한 Xiaohongshu OneModel 논문이 프로덕션 A/B 결과와 함께 레이더에 올랐다.
1For You 랭킹에 Phoenix 서빙의 slate context를 받는 경로가 생겼다 — X
자료 xai-org/x-algorithm @ d0cef2f — "Open-source X Recommendation Algorithm" — 2026-08-20 20:22 UTC, 공개 소스코드. 이 노트들은 4세대 공개 뒤의 X 커밋에 차례로 번호를 붙여 부르며, 이 커밋은 4세대+5다. 바뀐 파일은 14개, 규모는 +479/−200이다. 4세대+4(2026-08-19, aad7179, 파일 79 +3.3k/−0.7k)와 비교하면 1/7 정도 규모다.
배경 4세대 공개 뒤 X 저장소에는 매일 커밋하는 리듬으로 후속 공개가 이어져 왔다. 순서는 8/14 4세대+1(c65aa17) → 8/17 4세대+2(b089ce6) → 8/18 4세대+3(11a71f8) → 8/19 4세대+4(aad7179, 대형)다. 4세대부터 4세대+4까지 canon(이 저장소의 정본 문서)에 반영한 내용은 "proto와 config가 준비됐다"는 데까지였다. 8/20에 나온 이번 커밋은 4세대+4의 대형 확장 뒤에 이어진 만큼 규모는 작지만, 4세대+4가 proto와 config로 준비만 해 둔 두 스캐폴딩이 처음으로 실제 서빙·랭킹 파이프라인 연결까지 이어진 커밋이다.
- 4세대+4가 준비한 것은 Phoenix(X For You의 랭킹 모델) proto의
SlateContext.sid*필드 신설,RewardOutputs신설,xrecsys_searchconfig다. - 이번 커밋에서
SlateContext가 Phoenix 예측 응답을 타고 home-mixer(For You 피드를 조립하는 서비스)로 실제로 흘러드는 연결이 코드에서 처음 확인된다.
이 커밋에서 새로 나오는 개념은 세 가지다.
- AI trend는 X의 "Trending" 지면(사람들이 많이 이야기하는 이벤트)에 올라오는 항목이다.
- 이 커밋 전에는 tweet에 "AI trend" 라벨을 붙이는 로직이 home-mixer 파이프라인에 없었다.
- Strato는 X 코드가 데이터를 조회하거나 저장할 때 거치는 내부 시스템이다.
- Grok이 트윗을 보고 어느 AI trend에 속하는지 판정해 그 결과를 Strato에 저장해 두는 upstream 파이프라인이 있었으리라고 보이지만(
batch_get_tweet_ai_trendAPI), 그 파이프라인은 이 저장소 밖에 있다 추정.
- SlateContext는 DPP(비슷한 아이템이 함께 뽑히면 점수가 깎이도록 집합 단위로 고르는 다양성 기법) 재순위 슬레이트에서 각 슬롯이 갖는 컨텍스트 벡터다.
k는 슬롯 인덱스,pool_rank는 원 스코어 정렬 순위,pool_rank_gap은 위 슬롯과의 pool rank 차이다.- 여기에
fatigue,pre_diversity_score,sid_known이 더해진다. sid_k_l1/l2/l3은 3계층 SID(semantic ID, 아이템을 계층 코드로 표현한 토큰 열) 각각의 슬롯 인덱스를 담는다.sid_gap_l1/l2/l3은 각 계층 SID의 슬롯 간 gap을 담는다.- 8/18 커밋인 4세대+3에서 VMRanker(DPP 슬레이트 재정렬을 맡는 서비스)에 SID 필드 7개를 넣었을 때와 같은 필드 세트다.
- served slate context는 Phoenix 서빙 응답이 후보마다 재순위에 쓸 재료를 담아 돌려주는 스키마 필드다.
- 재료는 pool 랭크, fatigue, pre-diversity score, SID 3계층, SID gap 3계층이다.
- 4세대+4의 proto에는 정의만 되어 있었고, 이 커밋에서 소비 경로가 연결됐다.
For You 원본 포스트에 AI trend 라벨을 붙이는 hydrator가 생겼다
이번 커밋은 For You 후보 가운데 독립 원본 포스트를 골라 AI trend 라벨을 붙이는 단계를 새로 넣었다 확인. 한 페이지에 붙는 라벨은 최대 2개이고, 이 단계를 켜는 게이트의 기본값은 false다 확인.
AI trend 정보를 후보 포스트에 채워 넣는 hydrator가 157줄짜리 새 파일로 추가됐다 확인 —
home-mixer/candidate_hydrators/ai_trend_feedback_context_hydrator.rs.- 파이프라인 연결은
phoenix_candidate_pipeline.rs:424-428에 있다. - 기존
TopicFeedbackContextHydrator바로 뒤에 나란히 추가됐다. topic feedback context는 사용자의 관심 topic을 추론하는 데 쓰는 컨텍스트 라벨이다.
Box::new(TopicFeedbackContextHydrator { strato_client: strato_client.clone() }), Box::new(AiTrendFeedbackContextHydrator { strato_client: strato_client.clone() }),- 파이프라인 연결은
이 hydrator는 게이트 3중 조건을 모두 만족할 때만 돈다 확인 —
ai_trend_feedback_context_hydrator.rs:59-63.- 게이트 파라미터
rust_home_mixer_enable_ai_trend_feedback_context가 켜져 있어야 한다. 기본값은 false다. - topic 요청이 아니어야 한다(topic 지면과 겹치지 않게).
- Following 지면(팔로잉 계정 포스트만 받는 요청)이 아니어야 한다. 이 조건은 요청 단위이고, 후보가 팔로잉 계정 것인지는 보지 않는다.
- hydrate 루프(L65-75)도 아래 원본 조건으로만 거르므로, For You 요청에 섞인 팔로잉 계정 후보도 대상이다.
- Following 파이프라인에는 연결되지 않았다.
query.params.get(EnableAiTrendFeedbackContext) && !query.is_topic_request() && !query.in_network_only- 게이트 파라미터
후보는 "standalone original post"만 남기는 4중 조건을 거친다 확인 —
ai_trend_feedback_context_hydrator.rs:19-24.- 답글이거나, 리트윗이거나, 대화 조상이 있거나, 팔로잉한 사용자가 답글을 단 대상이면 후보에서 뺀다. 넷 중 하나라도 해당하면 빠진다.
- 결국 답글·리포스트·대화 조상·팔로잉 답글 대상이 아닌 원본 포스트에만 AI trend 라벨을 붙일 수 있다. 인용 포스트인지는 보지 않는다.
fn is_eligible_original_post(candidate: &PostCandidate) -> bool { candidate.in_reply_to_tweet_id.is_none() && candidate.retweeted_tweet_id.is_none() && candidate.ancestors.is_empty() && candidate.following_replied_user_ids.is_empty() }후보 빈도로 Top-2 trend를 고르고, trend마다 후보 1개를 무작위로 골라 라벨을 붙인다 확인 —
ai_trend_feedback_context_hydrator.rs:24-52.- 절차는 다섯 단계다.
- 조건을 만족하는 후보마다 Strato
batch_get_tweet_ai_trend로 trend_id를 조회한다. - 후보들의 trend_id 빈도 히스토그램을 만든다.
- 상위 2개 trend를 고른다. 동률이면 trend_id가 작은 쪽이 우선이다.
- 각 trend에 대해 후보 1개를 무작위로 고른다.
- Strato
batch_get_ai_trend_name으로 그 trend의 표시 이름을 조회한다.
- 조건을 만족하는 후보마다 Strato
- 결국 한 페이지에 최대 2개의 AI trend 라벨이 붙고, 각 trend마다 후보 하나에만 붙는다.
- 같은 trend에 속한 여러 후보에 같은 라벨을 되풀이해 붙이지 않도록 막는 억제 장치다.
const TOP_TREND_COUNT: usize = 2; fn select_feedback_targets(candidate_trends: &[(usize, i64)]) -> HashMap<usize, i64> { let mut frequency: HashMap<i64, usize> = HashMap::new(); for (_, trend_id) in candidate_trends { *frequency.entry(*trend_id).or_default() += 1; } let mut ranked: Vec<(i64, usize)> = frequency.into_iter().collect(); ranked.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(&b.0))); ranked.truncate(TOP_TREND_COUNT); let mut rng = rand::rng(); let mut selected: HashMap<usize, i64> = HashMap::new(); let mut used_indices = std::collections::HashSet::new(); for (trend_id, _) in ranked { let candidates_for_trend: Vec<usize> = candidate_trends .iter() .filter(|(idx, id)| !used_indices.contains(idx) && *id == trend_id) .map(|(idx, _)| *idx) .collect(); let Some(&idx) = candidates_for_trend.choose(&mut rng) else { continue }; used_indices.insert(idx); selected.insert(idx, trend_id); } selected }- 절차는 다섯 단계다.
화면에는 topic 라벨과 같은 형태로 나가며, topic feedback이 없을 때만 AI trend가 쓰인다 확인 —
home-mixer/util/urt/post_marshaller.rs:72-110.- 렌더 단계에서 URT(X의 응답 렌더링 스키마)
TweetContext의 topic 경로에 fallback으로 합류한다. - 둘 다 URT에서는
ContextType::TOPIC이라는 같은 컨텍스트 타입으로 나간다. - AI trend 경로는 두 가지가 topic 경로와 다르다.
landing_url이 채워지고,TopicFeedbackContext.topic_id가None이다. - 그래서 사용자가 이 트윗 위에 붙은 topic 형태의 chip을 탭하면, topic feedback 페이지가 아니라
x.com/i/trending/{trend_id}, 즉 해당 trend 지면으로 이동한다.
if let Some(topic) = post.topic_feedback_topic.as_ref().filter(|t| !t.is_empty()) { return Some(TweetContext { context_type: ContextType::TOPIC, text: topic.clone(), landing_url: None, context: Some(TweetContextDetails::TopicFeedbackContext(TopicFeedbackContext { topic: Some(topic.clone()), url: None, topic_id: post.topic_feedback_topic_id.clone(), })), .. }); } let trend = post.ai_trend_name.as_ref().filter(|t| !t.is_empty())?; let trend_id = post.ai_trend_id.as_ref().filter(|id| !id.is_empty())?; let landing_url = Url { url_type: UrlType::EXTERNAL_URL, url: format!("https://x.com/i/trending/{trend_id}"), urt_endpoint_options: None, }; Some(TweetContext { context_type: ContextType::TOPIC, text: trend.clone(), landing_url: Some(landing_url.clone()), context: Some(TweetContextDetails::TopicFeedbackContext(TopicFeedbackContext { topic: Some(trend.clone()), url: Some(landing_url), topic_id: None, })), .. })"
https://x.com/i/trending/{trend_id}" —post_marshaller.rs:92- 렌더 단계에서 URT(X의 응답 렌더링 스키마)
다음 네 가지는 공개되지 않았다.
AI trend의 정의(누가 어떻게 trend id를 만드는지, Grok으로 트윗을 클러스터링하는지 아니면 사람이 큐레이션하는지)batch_get_tweet_ai_trendupstream 파이프라인EnableAiTrendFeedbackContext의 실제 배포 여부TOP_TREND_COUNT = 2의 튜닝 이력
게이트 이름의 rust_home_mixer_ 접두사가 이 저장소의 다른 rust-migration 게이트와 같은 규칙을 따르므로, A/B 실험을 진행하는 중일 가능성이 있다 추정.
의미 X가 지금까지 써 온 topic feedback context와 나란한 두 번째 컨텍스트 채널을 열었다. topic이 이미 있으면 topic이 이기고 없을 때만 AI trend가 들어가는 폴백 구조라, 둘이 서로 겹치지 않는다.
- 관측 신호도 함께 넓어진다. 사용자가 이 chip을 탭하거나 닫거나 무시하는 반응이 "이 사용자는 이 AI trend에 관심이 있는가"를 판단하는 라벨 소스가 되기 때문이다.
- 4세대부터 4세대+4까지 반영해 온 "안전 파이프라인의 계층 확장"과는 다른 방향이다. 랭킹 상류의 시그널 소스 자체를 넓히는 흐름이 확인된다.
Phoenix가 서빙에서 계산한 slate context를 랭킹 단계가 받아 쓴다
8/19의 4세대+4 커밋에서 Phoenix proto에 생긴 SID 필드(각 슬롯의 SID 3계층과 gap 3계층)가 이번 커밋에서 실제 파이프라인의 소비 경로로 연결됐다. proto 필드 이름은 SlateContext.sid*다. 다음 네 곳이 동시에 열렸다.
후보 구조체에 slate context를 담는 슬롯이 하나 더 생겨 두 개가 됐다 확인 —
home-mixer/models/candidate.rs:26-27, 76-77.- 구조체 이름은
PostCandidate이고, 새로 생긴 필드는served_slate_context다. - 기존
slate_context는 home-mixer가 계산해 캐시한 값이다. - 신규
served_slate_context는 Phoenix가 서빙에서 산출해 응답에 담아 보낸 값이다.
pub struct PostCandidate { pub score: Option<f64>, pub slate_context: Option<SlateContext>, #[serde(default)] pub served_slate_context: Option<SlateContext>, // ← 신규 ... pub ai_trend_name: Option<String>, // ← 위 절 신규 pub ai_trend_id: Option<String>, // ← 위 절 신규 }- 구조체 이름은
proto와 Rust 사이의 변환 impl이 proto의 짧은 필드 이름을 home-mixer 쪽 이름으로 바꾼다 확인 —
candidate.rs:99-118.- proto의
sid_k1/2/3·sid_gap1/2/3를 home-mixer 명명 규칙인sid_k_l1/l2/l3·sid_gap_l1/l2/l3로 rename한다. - 여기서 "l"은 level을 뜻한다.
impl From<xai_recsys_proto::SlateContext> for SlateContext { fn from(c: xai_recsys_proto::SlateContext) -> Self { Self { k: c.k, pool_rank: c.pool_rank, pool_rank_gap: c.pool_rank_gap, fatigue: c.fatigue, pre_diversity_score: c.pre_diversity_score, sid_known: c.sid_known, sid_k_l1: c.sid_k1, sid_k_l2: c.sid_k2, sid_k_l3: c.sid_k3, sid_gap_l1: c.sid_gap1, sid_gap_l2: c.sid_gap2, sid_gap_l3: c.sid_gap3, } } }- proto의
PhoenixScorer가 Phoenix 예측 응답에서 slate context를 뽑아 새 필드에 채운다 확인 —
home-mixer/scorers/phoenix_scorer.rs:107-125.- Phoenix 예측 응답이 후보 스코어 24개(참여 확률 헤드)와 나란히
SlateContext까지 담아 오면, PhoenixScorer가 그 값을served_slate_context에 세팅해 파이프라인으로 흘려보낸다. - 값은 예측 응답의
candidate_slate_context()에서 꺼낸다.
.map(|c| PostCandidate { phoenix_scores: predictions.candidate_scores(&c.get_original_tweet_id()), served_slate_context: predictions .candidate_slate_context(&c.get_original_tweet_id()) .map(Into::into), prediction_request_id: Some(query.prediction_id), last_scored_at_ms, ..Default::default() })- Phoenix 예측 응답이 후보 스코어 24개(참여 확률 헤드)와 나란히
RankingScorer는 세 갈래 폴백으로 slate context를 정한다 확인 —
home-mixer/scorers/ranking_scorer.rs:700-710, 806-825, 875-895.UseServedSlateContext게이트가 켜져 있고 Phoenix가 모든 후보에 대해 slate context를 보내왔으면, 그 값을 그대로 쓴다.- 게이트가 꺼졌거나 데이터가 모자라면서 Redis 워밍 캐시가 히트하면, 이전 요청에 저장해 둔 slate context를 재사용한다.
- 그 밖의 경우에는 로컬에서
compute_slate_contexts(candidates, weighted_scores)로 계산한다.
- 이 순서는 두 랭킹 브랜치에 똑같이 적용된다.
fn served_slate_contexts( query: &ScoredPostsQuery, candidates: &[PostCandidate], ) -> Option<Vec<SlateContext>> { if !query.params.get(UseServedSlateContext) { return None; } candidates.iter().map(|c| c.served_slate_context).collect() } // ... 두 랭킹 경로(mpn_scoring 브랜치와 diversity 브랜치) 각각에서: let persisted_contexts: Option<Vec<SlateContext>> = match Self::served_slate_contexts(query, candidates) { Some(served) => Some(served), None if query.has_cached_posts => Self::stored_slate_contexts(candidates), None => Some(Self::compute_slate_contexts(candidates, &weighted_scores)), };이 경로를 켜는 게이트는 기본값 false로 들어왔다. 게이트만 만들어 두고, 실제 롤아웃은 후속 파라미터 변경으로 미뤄 둔 상태다.
- 게이트 이름은
UseServedSlateContext다.
- 게이트 이름은
의미 slate context를 계산하는 지점이 home-mixer 로컬에서 Phoenix 서빙으로 옮겨 가는 마이그레이션의 첫 단계다. Phoenix는 예측을 만드는 그 순간에 이미 각 후보의 pool 랭크와 fatigue, pre-diversity, SID 3계층 정보를 알고 있으므로, home-mixer가 다시 계산하는 대신 서빙 응답에 실어 보낼 수 있다.
- 그러면 랭커의 CPU를 아낄 수 있다.
- Phoenix가 가진 더 풍부한 컨텍스트를 랭킹 재순위에 반영할 수 있다. 예를 들어 SID 3계층은 Phoenix 없이는 계산할 수 없다.
- home-mixer가 후보 배분과 재순위만 맡는 얇은 층으로 계속 줄어드는 방향이다.
- 8/18 커밋(4세대+3)에서 SID가 VMRanker에 연결된 것과 방향이 같다. SID는 정의상 Phoenix 학습 산출물이라 home-mixer가 로컬에서 계산할 수 없는데, 이 커밋으로 그 문제가 서빙 시점에 해결됐다.
UseServedSlateContext가 true로 승격되는 커밋이 나오는 시점이 Phoenix 서빙의 slate context 산출이 프로덕션 트래픽에 반영되는 시점이다.
임베딩 테이블 로더의 핵심 로직을 Python 바인딩에서 떼어 냈다
Phoenix 서빙 엔진의 임베딩 테이블 로더가, Python 쪽 wrapper와 pyo3에 의존하지 않는 core로 나뉘었다 확인.
emb_table.rs에서 core 로직을load_tensor_into로 떼어 내고, 기존load_tensor는 그 core를 감싸는 얇은 wrapper로 다시 정의했다 확인 —phoenix/crates/serving/xai-recsys-engine/src/emb_table.rs.- 파일 개편 규모는 +189/−169, 성격은 근본적으로 동치 리팩터다.
- 기존
load_tensor는 pyo3 타입과 pyo3 에러에 묶여 있어서 non-Python 컨텍스트에서는 재사용할 수 없었다. - 묶여 있던 타입은
Bound<'py, PyString>,PyReadwriteArray1<'py, u8>,Bound<'py, PyList>이고, 에러는PyValueError,PyOSError,PyTypeError다. - 새 core는 pyo3에 의존하지 않으며,
&mut [u8]인터페이스로 받고Result<(), String>을 반환한다.
pub fn load_tensor_into( path: &str, urls: &str, shard_sources: &[(String, String, usize, usize)], tensor: &mut [u8], row_size: usize, num_row_segments: usize, ) -> Result<(), String> { ... } #[pyfunction] pub fn load_tensor<'py>( py: Python<'py>, path: Bound<'py, PyString>, urls: Bound<'py, PyString>, shard_sources: Bound<'py, PyList>, mut tensor: PyReadwriteArray1<'py, u8>, row_size: usize, num_row_segments: usize, ) -> PyResult<()> { let mut parsed = ...; // PyList → Vec<(String, String, usize, usize)> let path = path.to_string(); let urls = urls.to_string(); let tensor_slice = tensor.as_slice_mut()?; py.detach(|| { load_tensor_into(&path, &urls, &parsed, tensor_slice, row_size, num_row_segments) .map_err(PyOSError::new_err) }) }안전성도 함께 손봤다.
- tokio runtime 생성의
unwrap()을map_err로 바꿨다. 바뀐 코드는map_err(|e| format!("tokio runtime: {e}"))다. checkpoint path too short방어를 추가했다. 조건은prefix.len() < 3이고, 메시지는checkpoint path too short: {path}다.row_segment_size % num_row_segments != 0같은 modulo 체크를is_multiple_ofAPI로 통일했다.
- tokio runtime 생성의
의미 임베딩 테이블 로더 core를 순수 Rust 인터페이스로 노출해서, 학습 파이프라인이나 Rust-native 서빙 코드의 다른 경로에서 pyo3 스택 없이도 재사용할 수 있도록 준비한 것이다. 8/19 커밋(4세대+4)에서 Phoenix xrecsys_search의 vocab이 100M/100M/30M로 커진 것과 방향이 같다. 큰 임베딩 테이블을 여러 서빙·학습 컨텍스트에서 로드해야 할 필요가 커지고 있다는 정황이다.
두 심판 호출 위치에서 Grok 4.6을 쓰는 비율이 0.1에서 0.3으로 올랐다
grox(Grok 기반 nearline 분류기 서브레포)에서, 2026-08-19의 4세대+4가 도입한 Grok 4.6 dial이 하루 만에 10%에서 30%로, 3배 올랐다 확인. 이 dial은 두 심판 호출 위치(아동 안전 cross-model validation, deluxe 4.3 카테고리 재분류)에서 호출을 Grok 4.5 대신 4.6으로 보낼 확률이다. 4.6의 응답도 4.5와 똑같이 판정에 쓰이므로, 결과를 버리는 shadow가 아니라 실트래픽을 나누는 dial이다 확인 — classifier.py:281-290@aad7179.
바뀐 것은 상수 한 줄이다 확인 —
grox/flows/ptos/classifier.py:114.-_GROK_4_6_INTERNAL_DIAL = 0.1 +_GROK_4_6_INTERNAL_DIAL = 0.3회로차단기 설정과 dial이 적용되는 위치는 그대로다.
_EAPI_4_6_INTERNAL_BREAKER_CONFIG회로차단기의 파라미터(실패율 50%, 창 600초, 회복 600초, half-open 5회)는 바뀌지 않았다.- dial이 적용되는 두 위치, 즉 아동 안전 cross-model validation과 deluxe 4.3 카테고리 재분류도 그대로다.
의미 이번 상향은 4.5에서 4.6으로 넘어가는 gradual rollout에서, 실트래픽 10%를 받은 4.6이 회로차단을 트립시키지 않았고 판정 품질도 허용 범위였다는 점을 뒷받침한다 추정. 그렇지 않았다면 dial을 3배로 올리지 못했을 것이다.
- 4세대+2가 도입한 Grok 4.5 심판 계층은 매일 정교해지고 있고, 이번 커밋은 그 흐름의 네 번째 커밋에 해당한다. 흐름은 +2 도입 → +3 로깅 세분화 → +4 4.6 dial 10% → +5 30%다.
- 다음 커밋에서 50%나 100%로 뛰면 4.6 완전 전환에 가까워진 것이고, 다시 낮아지면 회귀를 관측한 것이다.
Grok 스트리밍 응답을 조건이 맞으면 중간에 끊을 수 있게 됐다
grox의 Grok 샘플러에 스트리밍 응답을 일찍 끝내는 콜백이 생겼고, 끊을 때마다 카운터를 올린다 확인.
grox/libs/grok_sampler/llm.py에stop_predicate콜백이 추가됐다 확인 — 해당 파일.- 규모는 +27/−13이다.
- 기존
_sample_streaming은 raw stream에서 토큰을 받는 대로 계속 소비했다. - 이제
async with aclosing(...)으로 raw stream을 명시적인 컨텍스트에 감싸서, 조기에 종료하더라도 정리를 보장한다. 토큰은 이async with aclosing블록 안에서 받는다. - 토큰을 yield한 뒤마다
stop_predicate(resp)를 확인해, True이면 즉시 break하면서 카운터llm.sample.early_stop.count를 증가시킨다.
stop_predicate: Callable[[str], bool] | None = kwargs.get("stop_predicate") ... async with aclosing(self._sample_streaming_raw(request, **kwargs)) as raw_stream: async for tok in raw_stream: ... yield tok.text if stop_predicate is not None and stop_predicate(resp): logger.info(f"Response complete after {tokens_received} tokens, cancelling the stream") Metrics.counter("llm.sample.early_stop.count").add(1, attributes=attributes) break
의미 이 변경은 4세대+4에서 도입된 Grok 4.6 dial이 늘린 트래픽을 감당하려는 sampling 인프라 최적화로 볼 여지가 있다 추정. 근거는 dial이 30%로 오르는 변경과 같은 커밋에 들어 있다는 정황이다.
- 안전 판정 응답은 JSON 태그(
<json>...</json>)에 감싸여 오는 경우가 많은데, 태그가 닫히고 나면 뒤따르는 토큰은 의미가 없다. stop_predicate=lambda r: "</json>" in r같은 콜백으로 일찍 끊으면 토큰 비용을 줄이고, TTFT 이후의 지연도 줄일 수 있다.
Grok 분류기가 작성자 프로필 이미지를 참조할 수 있게 됐다
grox 데이터 로더의 User 스키마에 프로필 이미지 필드가 추가됐다 확인. 작성자 메타데이터에 프로필 이미지 URL이 있으면 이 필드를 채운다 확인.
Grox
User데이터 로더에profile_image필드가 생겼다 확인.- 타입은
profile_image: Image | None이다. - 바뀐 파일은
grox/core/data_loaders/data_types.py와post_mapper.py다. AuthorMetadata.profileImageUrl이 존재하면Image(url=...)로 채운다.- 이전에는 User 스키마가 URL과 affiliated business, 최근 포스트만 담았고 프로필 이미지는 없었다.
class User(BaseModel): urls: list[str] | None = None affiliated_business: AffiliatedBusiness | None = None recent_posts: list["Post"] | None = None profile_image: "Image | None" = None # ← 신규- 타입은
의미 이제 Grok classifier가 프로필 이미지를 판정 컨텍스트로 참조할 수 있다. 예를 들어 광고 브랜드 안전 판정에서 프로필 이미지에 성인 콘텐츠가 있는지 참고하는 상황을 생각할 수 있다. 4세대+2에서 도입된 성인 콘텐츠 cross-validation 심판이나 후속 판정 계층 확장을 위해 upstream 데이터 연결을 준비한 것으로 볼 여지가 있다 추정.
이 커밋(4세대+5, d0cef2f)의 6개 방향 요약
| 방향 | 파일 | 규모 | 성격 |
|---|---|---|---|
| AI trend feedback context | ai_trend_feedback_context_hydrator.rs, post_marshaller.rs, candidate.rs, param.rs |
+157/+29+ ... | 신규 기능: For You 컨텍스트 라벨 채널 하나 추가 |
| served_slate_context | phoenix_scorer.rs, ranking_scorer.rs, candidate.rs, param.rs |
+22/−10 등 | 연결 시작: 4세대+4 proto 소비 경로 |
| emb_table 리팩터 | emb_table.rs |
+189/−169 | 리팩터(동치): core를 pyo3-free로 |
| Grok 4.6 dial 3배 | classifier.py |
+1/−1 | 파라미터 변경: 실트래픽 10% → 30% |
| grok_sampler stop_predicate | llm.py |
+27/−13 | 인프라: 조기 종료 콜백 |
| profile_image 필드 | data_types.py, post_mapper.py |
+7/+0 | 스키마 확장: User에 이미지 필드 |
정본 반영 services/x.md의 다음 부분을 고쳤다.
- 세대 테이블에 4세대+5(d0cef2f, 2026-08-20) 행을 추가했다(+479/−200, 파일 14).
- 한 줄 요약에 다음 6개 항목을 반영했다.
- AI trend feedback context hydrator
- served_slate_context 연결 시작
- emb_table core 분리
- Grok 4.6 dial 0.3
- grok_sampler stop_predicate
- profile_image
- "후보 생성" 대신 새 하위 절 "AI trend feedback context — 4세대+5 신규"를 랭킹·필터 사이에 넣었다. topic feedback과 나란한 채널로 위치를 잡았다.
- "Phoenix 학습·인덱스" 절의 4세대+4 하위 소절 뒤에 "4세대+5 (2026-08-20) — Phoenix 서빙이 산출한 slate context를 home-mixer가 소비 시작" 소절을 새로 만들었다.
- "Grox → PtOS Adult Content Cross-Validation" 절의 4세대+4 항목 뒤에 "4세대+5 (2026-08-20) Grok 4.6 internal dial 0.1 → 0.3 (3배 상향)" 한 줄을 추가했다.
- 열린 질문 절에 "4세대+5 공개로 해소된 것 / 신규 (2026-08-22)" 서브섹션을 추가했다. 해소 5개와 심층 분석 필요 5개를 적었다.
- 출처 절에 d0cef2f 관련 파일 6개 링크를 추가했다.
의미 4세대부터 4세대+4까지 canon에 반영해 온 성격이 이번에도 확인됐다. 매일 이어지는 커밋 리듬 속에서 proto 스캐폴딩과 실제 소비 연결이 하루나 이틀 차이로 연달아 나온다는 것이다. AI trend 컨텍스트 채널을 새로 만든 것과 함께 놓고 보면, X의 랭킹·후보 파이프라인이 두 방향으로 동시에 확장되고 있다는 관찰이 굳어진다.
- 4세대+4의
SlateContextproto가 4세대+5에서 소비되기 시작한 것은 이 리듬의 세 번째 사례다.- 첫 번째는 4세대+2의 V2 라벨이 +3의 dual-write 재설계로 이어진 경우다.
- 두 번째는 4세대+3의 VMRanker SID 연결이 +4 proto의
sid*필드 정식화로 이어진 경우다. - 세 번째는 4세대+4의 SlateContext proto가 +5의 phoenix_scorer 연결로 이어진 경우다.
- 두 방향은 상류 시그널 소스를 넓히는 축(AI trend)과, Phoenix 서빙 산출물을 home-mixer가 더 얇게 소비하는 축이다.
2OneModel이 유기·광고·상점 3개 지면의 랭킹을 하나로 합쳤다 — Xiaohongshu
자료 OneModel: A Unified Foundation for Platform-Scale Multi-Scenario Ranking (arXiv 2608.18606) — 2026-08-19 arXiv 제출(v2, v1은 초기 제출), 논문. 저자는 14명이다(Yinqi Zhang, Peiyu Hu, Yuntian Tang, Siying Gu, Jiahao Liang, Longxin Kou, Haiqing Hu, Shuman Zhuang 등). 초록에 명시된 배포 서비스는 Xiaohongshu(小红书, 중국 라이프스타일 소셜 플랫폼)다.
배경 Xiaohongshu는 canon이 아직 추적하지 않는 서비스다. 그래도 여러 비즈니스 스트림(유기 추천, 광고, 상점 상품)을 하나의 앱에서 통합해 서빙한다는 점이, X와 Meta, Netflix의 서비스별 파편 랭커와 비교하면 이례적이다. 이 논문은 세 개 지면을 하나의 랭커로 통합했다는 프로덕션 배포 진술이고, 다음 세 갈래의 프로덕션 진술군에 새로운 각도를 더한다.
- Netflix Foundation Model(FM)은 단일 FM 위에 서비스별 head를 두었다.
- Meta GEM은 광고 domain 안에서 IG와 FB를 통합하는 backbone을 만들었다.
- Yandex Music Gryphon-v2/Sona는 retrieval과 ranking을 통합했다.
OneModel은 하나의 backbone이 서로 다른 비즈니스 스트림(유기·광고·상점)을 동시에 서빙하는 각도로 넘어간다.
새로 알게 된 것A. 세 스트림의 최종 랭킹을 공유 이벤트 시퀀스 기반의 프레임워크 하나로 통합한다
초록은 스트림마다 랭킹 시스템을 따로 두면 사용자 표현이 쪼개지고 엔지니어링 비용이 늘어난다고 지적하고, 최종 랭킹을 하나로 합친 OneModel을 제안한다.
"Platform-scale recommender systems often span multiple business streams such as organic recommendation, advertising, and merchant services, where user behaviors form a continuous cross-stream trajectory. Maintaining separate ranking systems fragments user representations and increases engineering cost. We propose OneModel, a unified framework for multi-stream final ranking. OneModel maps heterogeneous behaviors into shared event sequences, learns long-context user representations with an action-oriented backbone, and introduces Scenario-aware Information Modulation to balance cross-stream transfer and stream-specific specialization."
— OneModel (arXiv 2608.18606) abstract (논문)
핵심 아키텍처 요소는 세 가지다.
- 공유 이벤트 시퀀스는 유기·광고·상점 세 스트림의 사용자 행동을 하나의 이벤트 시퀀스 스키마로 통합한다.
- action-oriented backbone은 long-context user representation을 학습하는 부분이다. 구체적인 아키텍처는 abstract에 나오지 않는다.
- Scenario-aware Information Modulation은 cross-stream transfer와 stream-specific specialization 사이의 균형을 스위칭과 게이팅으로 조절한다. 앞의 것은 공유, 뒤의 것은 전용에 해당한다.
프로덕션 배포를 위한 최적화 요소로는 여섯 가지를 든다.
- stratified user representation
- multi-objective training
- feature decomposition
- user feature prefetching
- shared user-tower computation
- graph-level inference optimization
새로 알게 된 것B. Xiaohongshu 프로덕션 A/B에서 세 지면의 지표가 모두 올랐다
- A/B 결과는 Xiaohongshu 프로덕션 배포에서 나왔다.
| 지면 | 지표 | 결과 |
|---|---|---|
| Explore Feed (유기 추천) | Time Spent | +0.33% |
| Explore Feed (유기 추천) | Engagement | +1.25% |
| Feed Advertising (광고) | advertising value | +3.43% |
| Feed Advertising (광고) | CTR | +8.18% |
| Merchant Recommendation (상점) | DGMV (Daily GMV) | +1.1867% |
| Merchant Recommendation (상점) | GPM (GMV Per Mille) | +2.1585% |
정본 반영 radar.md "이번 달 신규" 절에 OneModel 엔트리(2~4줄 포인터)를 추가했다 [레이더 승격]. OneModel은 arXiv에서 레이더 수용 기준을 통과했다. 서비스 정본(services/xiaohongshu.md 신설)으로는 아직 승격시키지 않는다. canon이 추적하는 서비스가 아니기 때문이다. 중국 밖 플랫폼에서 두 번째 회사의 비슷한 진술이 나오면, 그때 topic(multi-task-ranking.md 또는 새로 만드는 unified-multi-scenario topic)으로 승격시킬 후보가 된다.
의미 OneModel은 "단일 랭커가 여러 비즈니스 스트림을 통합 서빙한다"는 회사 차원 아키텍처 결단의 세 번째 프로덕션 진술이다. LLM-native 랭킹 스레드와는 방향이 조금 다르다. Meta GEM은 광고와 IG, FB로 확장했지만 여전히 광고 domain이 중심인 데 비해, Xiaohongshu OneModel은 광고와 유기, 상점을 대등하게 통합한다는 점이 새롭다.
- 첫째는 Netflix FM(서비스 다양성, foundation에서 head로)이다.
- 둘째는 Meta GEM(광고 domain 안에서의 지면 다양성)이다.
- 셋째가 Xiaohongshu OneModel(비즈니스 스트림 다양성)이다.
검증에서 뒤집힌 것
없음.
레이더
- OneModel(Xiaohongshu, arXiv 2608.18606, 2026-08-19)은 위 2절에서 다뤘고, radar.md "이번 달 신규"에 추가했다.
다음에 볼 것
- X d0cef2f의 후속 커밋에서 세 가지를 확인한다 — 매일 커밋하는 리듬이 유지된다면 다음 커밋에서 관측할 수 있다.
UseServedSlateContext게이트가 프로덕션에서 true로 승격되는 시점을 본다. Phoenix 서빙이 산출한 slate context가 실제 트래픽에 반영되는 시점이다.EnableAiTrendFeedbackContext게이트가 활성화되는지 본다.- Grok 4.6 dial의 다음 이동을 본다. 30%에서 50%나 100%로 오르는지, 아니면 내려가는지가 관심사다.
- X
AiTrend정의의 upstream을 추적한다 —batch_get_tweet_ai_trend와batch_get_ai_trend_name이 읽는 Strato 컬럼은 이 저장소 밖에 있다. 앞으로 관련 문서(예를 들어docs/아래의 새 md 파일이나 README 갱신)가 나오면 반영한다. - Xiaohongshu OneModel의 후속을 확인한다 — v2가 8/19 제출이라 논문 자체는 아직 프리프린트다.
- 학회(RecSys/KDD/WWW 2026·2027) 게재 여부를 본다.
- 저자들의 후속 발표를 본다. 특히 "action-oriented backbone" 아키텍처 세부와 cross-stream transfer 게이팅 메커니즘을 본다.
- Xiaohongshu가 다른 지면(검색 등)에도 배포했다는 진술이 나오는지 본다.
- X 4세대+4(aad7179) 심층 분석은 미뤄 둔다 — 사람이 요청할 때 착수한다.
- aad7179의
phoenix/xrex/models/recsys_model.py(+265/−25) 트레이스는 여전히 끝내지 못했다. RewardOutputs가 어느 학습 루프에서 채워지고 소비되는지, link-open 5분위가 quantile regression인지 확인해야 한다.
- aad7179의