- 읽은 자료 1건
- 정본 반영 1개 파일
- 레이더 0건
1노출 판정 필터가 사유 대신 액션 값으로 포스트를 거른다 — X
자료 xai-org/x-algorithm@75d93d9 — 공개 소스코드, 2026-09-09, 파일 28개, +801/−252. 이 노트들은 4세대 공개 뒤의 X 커밋에 차례로 번호를 붙여 부르며, 이 커밋(2026-09-09, 75d93d9)은 4세대+18이다.
배경 X는 For You 파이프라인 소스코드를 xai-org/x-algorithm에 매일 두 번 정도 미러링한다(2026-08-13 4세대 공개 이래). 이번 커밋은 파일 28개짜리 중형이지만 서로 다른 5개 축을 동시에 건드렸다.
- 노출 판정은 이제 사유가 아니라 액션 값이 결정한다. visibility-filtering(VF)은 포스트마다 '이 뷰어에게 보여도 되는가'를 판정하는 서비스다.
- VF 클라이언트가
TweetVisibility에 명시적Action필드를 추가했고, home-mixer(For You 피드를 조립하는 서비스)의VFFilter는 이제FilteredReason이 아니라Actionenum으로 drop/keep을 결정한다. - 판정 축이 "reason에서 파생된 action"에서 "action이 결정, reason은 로깅용 annotation"으로 옮겨졌다.
- VF 클라이언트가
- 홈 타임라인에 머리 이미지, 본문, 1~2개 CTA(행동 유도) 버튼으로 구성되는 새 광고 포맷이 first-class로 등장했다. 트윗을 감싸는 프로모트 트윗 포맷이 아닌 6번째 변종이다.
home-mixer/util/urt/ad_marshaller.rs에header_image_prompt_ad_info가 있는 광고를 별도 marshaller로 처리하는 분기가 신설됐다.
- 콘텐츠 분류 쪽에서는 긴 영상의 첫 프레임이 LLM 프롬프트에 따로 들어간다.
grox(Grok 기반 nearline 분류기 서브레포) 미디어 로더가 30초+ 영상에 대해 첫 프레임 preview 이미지를 다운로드·리사이즈한다.- 이 이미지는 LLM 프롬프트에
[Video Preview Thumbnail]라벨과 함께 storyboard(균일 샘플링된 프레임을 격자로 붙인 이미지) 앞에 붙는다.
- 라이브 방송 프레임이 두 안전 파이프라인의 정식 판정 대상이 됐다.
BroadcastLoader가 grox에 새로 들어와 라이브 broadcast(X의 라이브 비디오, Periscope 후신)의 프레임을 추출한다.- PTOS(정책 기반 위반 카테고리 판정) adult-content leading-frames와 sex/nudity safemodel, 두 안전 파이프라인이 broadcast 프레임을 정식 payload로 인정한다.
- 등록된 랭킹 훈련 config 두 개 모두에서 멀티모달 post 임베딩이 학습 입력에서 빠졌다.
- Phoenix(X For You의 랭킹 모델) 문서
phoenix/README.md의 nano 훈련 config 표에서, multimodal post 임베딩이xrecsys_seqpack에서 v5로 켜져 있다는 줄이 "off (home_direct_packedandxrecsys_seqpack)"로 바뀌었다.
- Phoenix(X For You의 랭킹 모델) 문서
- 부수적으로 phoenix NUMA pinning이 isolated CPU를 sched_setaffinity 대상에서 빼도록 고쳐졌고,
candidate-pipeline의quiet-spansfeature-gated 로그 6곳이 삭제됐다.
새로 알게 된 것A. VF 판정에 명시적 액션 값이 생겼고, 필터는 이 값으로 거를지 정한다
- VF의 판정 축이 필터링 사유에서 명시적 액션 값으로 리팩터됐다 확인 —
visibility-filtering-client/vf_client.rs(+130/−62).- 판정 축은
FilteredReason에서Action으로 옮겨졌다. - 함께 바뀐 파일:
visibility-filtering-client/models.rs(+7),home-mixer/filters/vf_filter.rs(+27/−69),home-mixer/candidate_hydrators/vf_candidate_hydrator.rs(+159/−23),home-mixer/candidate_hydrators/vf_following_candidate_hydrator.rs(+6/−11),home-mixer/models/candidate.rs(+1).
- 판정 축은
- 클라이언트가 돌려주는 판정 결과는 이제 세 값으로 된 튜플이다 확인.
- 결정론적 판정
Action(Allow / Interstitial / Avoid / Downrank / Drop / Tombstone / NotEvaluated) - 로깅과 다운스트림용 annotation
FilteredReason - 라벨 lookup 결과
SafetyLabelMap
TweetVisibility는 기존reason: Option<FilteredReason>과safety_labels에 더해action: Action필드를 정식 멤버로 갖게 됐다.- 새 helper
TweetVisibility::to_visibility_reason()이 action과 reason을 조합해visibility_reason필드 값을 유도한다.
- 결정론적 판정
- Strato(코드가 데이터를 조회하거나 저장할 때 거치는 X 내부 시스템) 경로에서는 클라이언트가 Strato에서 받은 사유로부터 액션을 계산한다 확인. 규칙은 세 갈래다.
Strato의 정의는 services/x.md 용어 표에 있다.
StratoVfClient::get_result가 Strato에서 받은FilteredReason으로부터action_from_strato_reason을 거쳐 action을 계산한다.fn action_from_strato_reason(reason: &Option<FilteredReason>) -> Action { match reason { None => Action::Allow, Some(FilteredReason::SafetyResult(safety_result)) => match safety_result.action { Action::NotEvaluated => Action::Allow, _ => safety_result.action.clone(), }, Some(_) => Action::Drop(DropReason {}), } }Strato가 준 사유가
SafetyResult이면 그 안의 action을 그대로 쓰고, 그 밖의 non-SafetyResult reason(뮤트·차단·null-cast·exclusive 등)은 무조건Drop으로 만든다.Strato가
NotEvaluated로 리턴하면 client 층에서Allow로 승격된다. 이는 Strato가 판정 미실행을 fail-open으로 취급하겠다는 뜻이다.
- gRPC 경로에서는 응답에 담긴 액션을 그대로 쓴다 확인. 새 gRPC 경로에서는 서버가 action을 결정해서 내려주는 것이 표준이다.
result_to_visibility가 gRPC 응답의result.action을 그대로Action으로 매핑한다.action필드가 없으면Action::NotEvaluated가 된다.
- 필터 층은 이제 후보의 액션 값으로 거를지 정한다 확인. Allow, Interstitial, Avoid, Downrank는 남기고 Drop, Tombstone, NotEvaluated는 거른다.
VFFilter::filter가 이제 candidate.visibility_action의 함수다. 판정 함수는 다음과 같다.pub(crate) fn should_drop_action(action: &Action) -> bool { match action { Action::Allow | Action::Interstitial | Action::Avoid | Action::Downrank => false, Action::Drop(_) | Action::Tombstone | Action::NotEvaluated => true, } }이전 버전은
FilteredReasonvariants를 매칭했다.SafetyResult면action == Drop일 때만 걸렀고, 나머지 non-SafetyResult reason(뮤트·차단·null-cast·exclusive 등)은 무조건 drop이었다.새 버전은 판정 축이 명시적이라 훨씬 읽기 좋다.
- 조상, 인용, 리트윗 원문 포스트를 거를 때도 액션 값을 본다 확인.
vf_candidate_hydrator.rs의should_drop_ancillary가 조상·quoted·retweeted 포스트의visibility.action을 봐서 drop 여부를 결정한다.- 이전
should_drop_reason(reason: &FilteredReason)은 제거되고, filter 층의should_drop_action을 공유한다.
- 후보 객체에 액션 필드가 추가됐다 확인.
candidate.rs의PostCandidate가visibility_action: Option<Action>을 갖게 됐고, 기존visibility_reason: Option<FilteredReason>은 유지된다.- Hydrator update 함수 두 개(
VFCandidateHydrator::update·VFFollowingCandidateHydrator::update)가 이 필드도 함께 채운다.
- 뷰어가 작성자를 차단한 경우를 가리키는 사유 이름이 새로 생겼다 확인.
- 새
FilteredReason변종은FilteredReason::ViewerBlocksAuthor다. 기존에는AuthorBlockViewer(작성자가 뷰어를 차단)만 있었다. 이제 대칭 방향인 '뷰어가 작성자를 차단'도 정식 reason 이름을 갖는다. vf_pb::FilteredReason::ViewerBlocksAuthor와 proto to/from 매핑도 추가됐다.
- 새
⚠ 주장 변경(services/x.md line 392의 "fail-closed" 진술): fail-closed 성격은 유지되지만, 판정 세분화는 훨씬 명확해졌다.
- 이전 정본은 "
VFFilter는 fail-closed다.SafetyResult이외의 모든FilteredReason변종이 무조건 drop이다"였다. - 새 판정 함수는 문법이 완전히 달라졌지만 결과는 대부분 동일하다. Strato 경로에서 non-SafetyResult reason은 client 층에서 이미
Drop으로 변환되고,NotEvaluated도 drop이다. - 이전 필터도
SafetyResult로 온 판정은 action이Drop일 때만 걸렀다 확인 —vf_filter.rs:22-30. Allow·Interstitial·Avoid·Downrank·Tombstone·NotEvaluated는 모두 남겼다. - 바뀐 것은 필터 층에서
Tombstone과NotEvaluated가 이제 걸러진다는 점이다 확인 — 아래 인용한vf_filter.rs:16-21.
"match action { Action::Allow | Action::Interstitial | Action::Avoid | Action::Downrank => false, Action::Drop(_) | Action::Tombstone | Action::NotEvaluated => true, }" — home-mixer/filters/vf_filter.rs:16-21, 2026-09-09
의미 (A) 이번 리팩터는 클라이언트 층에서 판정을 통일된 액션 값으로 좁힌 뒤, downstream에는 action만 흐르게 만든 것이다.
- 통일된 액션 값의 타입은
Actionenum이다. - 4세대 공개(2026-08-13) 이래 VF의 판정 함수에는 세 경로가 얽혀 있었다.
SafetyResult.action을 봐야 하는 경로,FilteredReasonvariant를 봐야 하는 경로, gRPC action을 매핑해야 하는 경로다. - 4세대+7(2026-08-24, d011592)이
reference_compare.rs로 도입한 shadow 비교에서 diff의 원인은 대부분 "reason variant matching이 파이프라인마다 조금씩 달라서"였을 가능성이 크고 추정, 이번 리팩터는 그 diff를 줄이는 후속 조치로 읽힌다. - 아직
Downrank를 실제로 쓰는 코드는 이 커밋에 등장하지 않는다. Strato가 리턴하는SafetyResult.action에Downrank가 들어올 여지는 열렸지만, 실제로 연결되는지는 다음 커밋에서 볼 대상이다.
새로 알게 된 것B. 홈 타임라인에 트윗을 감싸지 않는 여섯 번째 광고 포맷이 생겼다
- 홈 타임라인에 새 광고 포맷이 신설됐다 확인 —
home-mixer/util/urt/ad_marshaller.rs(+258/−3).- 포맷 이름은
header-image-prompt-ad다.
- 포맷 이름은
- 기존 홈 타임라인 광고는 다섯 포맷이었고, 이번 커밋이 여섯 번째를 추가했다 확인.
- 프로모트 트윗(
promoted-tweet-<post_id>-<impression>) - preroll 비디오 광고
- SSP 트윗/RTB Trend Takeover
- RTB image ad (
rtb-image-ad-<impression>) - Google SDK Native ad(
GoogleSDKNativeAd)
- 여섯 번째의 엔트리 이름은
header-image-prompt-ad-<impression>이다.
- 프로모트 트윗(
- 광고에 이 포맷의 정보가 있으면 전용 marshaller가 우선 처리한다 확인. 프로모트 트윗과는 상호 배타적으로 분기된다.
marshal_ad()함수 최상단에 이 분기가 있다.if let Some(info) = &ad.header_image_prompt_ad_info { return marshal_header_image_prompt_ad(ad, info, sort_index, request_type, &impression_string); }판단 기준은
AdIndexInfo.header_image_prompt_ad_info: Option<HeaderImagePromptAdInfo>필드가 있느냐다.
- 입력 정보(HeaderImagePromptAdInfo)는 광고 인덱스 서비스가 채워 넣는 proto message이고, 필드 구성은 marshaller가 소비하는 형태에서 역산할 수 있다 확인.
- 헤더 이미지:
image_url: String,image_width/height - 헤더와 본문 문구:
header_text: String,body_text: String - 주 CTA:
button_text: String,landing_url: String - 보조 CTA(비어 있으면 표시하지 않음):
secondary_button_text: String,secondary_landing_url: String - 클릭 후 광고 dismiss 여부:
dismiss_on_click: bool - 노출 트래킹 콜백:
impression_callback_endpoints: Vec<String> - 주 CTA 클릭 트래킹 콜백:
click_callback_endpoints: Vec<String>
- 헤더 이미지:
- 이 광고는 URT(Universal Response Tree, X의 응답 렌더링 스키마)에 트윗이 아니라 Message 타입 아이템으로 실린다 확인. "메시지 프롬프트"라는 별도 타입이다.
- 엔트리 구조는
TimelineEntryContent::Item(TimelineItem { content: TimelineItemContent::Message(MessagePrompt { content: MessageContent::HeaderImagePrompt(HeaderImagePrompt { .. }), impression_callbacks }) })다. MessageContent::HeaderImagePrompt가 이 광고 전용 컨텐츠 variant다.
- 엔트리 구조는
- 프롬프트 컨텐츠(HeaderImagePrompt)에는 이미지, 문구, 버튼 동작이 담긴다 확인.
- 이미지(image_variants에 URL·width·height가 담긴다):
header_image: MessageImage - 헤더 문구:
header_text: String,header_rich_text: RichText. plain text와 entity 없는 RichText, 두 형태로 함께 담긴다. - 본문(있을 때만):
body_text: Option<String>,body_rich_text: Option<RichText> - 주 버튼(필수):
primary_button_action: MessageTextAction - 보조 버튼(
secondary_button_text가 비어 있지 않을 때만):secondary_button_action: Option<MessageTextAction>
- 이미지(image_variants에 URL·width·height가 담긴다):
- 버튼마다 자체 클릭 이벤트 정보와 callback 리스트를 가진다 확인.
- 버튼 구조(MessageTextAction)는
text: String과action: MessageAction { dismiss_on_click, url, client_event_info, on_click_callbacks, on_click_reactive_trigger }로 이뤄진다. - 각 버튼의
ClientEventInfo에서 action은 "primary_cta" 또는 "secondary_cta"다.
- 버튼 구조(MessageTextAction)는
- 테스트 3개가 이 계약을 고정한다 확인.
header_image_prompt_ad_marshals_to_message_prompt_entry: entry_id 형식("header-image-prompt-ad-fa4ead01"), impression callback, HeaderImagePrompt content variant, 이미지 URL/치수, 텍스트, 주 CTA URL/dismiss/callback을 assert한다.header_image_prompt_ad_emits_secondary_button_when_set:secondary_button_text = "Learn more"를 세팅하면 secondary_button_action이 존재하고 action이 secondary_cta다.header_image_prompt_ad_info_absent_stays_a_tweet_item:header_image_prompt_ad_info: None이면 기존 프로모트 트윗 marshaller로 fall-through한다.
의미 (B) X 홈 타임라인이 트윗을 감싸지 않는 광고 포맷을 새로 정식으로 넣었다.
- RTB image ad도 이미 트윗과 분리된 이미지 광고였지만, header-image-prompt-ad는 헤더 이미지, 본문 텍스트, 최대 2개 CTA 버튼으로 이뤄진 훨씬 풍부한 컴포넌트 구성을 갖는다.
- URT
MessageContent라는 별도 컨텐츠 타입을 쓰는 광고는 이 포맷이 첫 사례다. 클라이언트 렌더링 관점에서는 트윗이 아니라 in-feed announcement/dialog UI로 렌더링될 가능성이 높다 추정. - Secondary CTA를 지원하는 광고 포맷도 홈 타임라인에서는 이번이 첫 진입이다.
- 광고 인덱스 시스템(공개 안 됨)이 이 광고 포맷을 언제 어떤 조건에서 채우는지는 이 커밋에 없다. 광고 블렌더 5종(
safe_gap·partition_organic·partition_organic_low_risk·time_gap·multi_risk) 가운데 어느 것에 진입하는지도 없다.
새로 알게 된 것C. grox가 30초가 넘는 영상의 미리보기 이미지를 storyboard 앞에 붙인다
- grox 미디어 로더가 긴 영상에 preview thumbnail을 첨부한다 확인 —
grox/core/data_loaders/media_loader.py(+100/−19).- 함께 바뀐 파일:
grox/core/lm/convo.py(+18)
- 함께 바뀐 파일:
- 기준 길이, 이미지 필드, 라벨이 새로 생겼다 확인.
- 기준 길이 상수는
_VIDEO_PREVIEW_MIN_DURATION_MS = 30_000(30초)이다. Videopydantic 모델에preview_image: bytes | None = None필드가 추가됐다.- 라벨 상수는
PREVIEW_IMAGE_LABEL = "[Video Preview Thumbnail]"이다.
- 기준 길이 상수는
- 미리보기는 설정 스위치가 켜져 있을 때 네 가지 진입 조건(4-way gate)을 거쳐 붙는다 확인.
hydrate_video가 기존 프레임 hydration 뒤에if grox_config.media_hydration.enable_video_preview_image: await cls.hydrate_video_preview(video, is_main_post and is_high_fav)를 실행한다.hydrate_video_preview의 진입 조건은 다음 넷이다.video.convo_video is not Nonevideo.url이 있다video.videoInfo가 있다(video.videoInfo.durationMillis or 0) > _VIDEO_PREVIEW_MIN_DURATION_MS: 30초 초과
- 메인 포스트이면서 fav가 높으면 deluxe 전용 타일 크기로 리사이즈한다 확인.
is_deluxe = is_main_post and is_high_fav이면deluxe_image_tile_size로, 아니면image_tile_size로resize_tile(bs, tile_size)한다.- 다운로드에 실패하면
media_loader.hydrate_video_preview_failed.count가 증가한다. - 성공하면
media_loader.hydrate_video_preview.count가 증가하고video.convo_video.preview_image = preview가 된다.
- LLM 입력에서는 라벨, 이미지, 줄바꿈이 storyboard 앞에 들어간다 확인.
Video.interleave_storyboard는 preview_image가 있으면 storyboard 리스트 앞에[f"{PREVIEW_IMAGE_LABEL} ", self.preview_image, "\n"]세 원소를 추가한다. storyboard composite 앞에 라벨, 원본 이미지 바이트, newline이 차례로 온다._image_part기반 chat marshaling 경로에서도if c.preview_image: parts.append({"type": "text", "text": PREVIEW_IMAGE_LABEL}); parts.append(_image_part(c.preview_image))로 그림 전에 텍스트 라벨을 붙인다.
의미 (C) 이번 커밋은 긴 영상의 대표 프레임 한 장을 storyboard 앞에 먼저 붙이는, 세 번째 미디어 표현 형태를 추가했다.
- 4세대+12(2026-09-01, 6384ca7)에서 motion-reveal frames(정적/동적 분리)가 storyboard(4세대부터)에 더해진 이래, grox가 LLM에 넘기는 미디어 표현은 이 두 형태였다.
- 짧은 영상은 이미 storyboard의 프레임 수가 적어서 대표성이 충분하다. 반면 30초+ 긴 영상은 프레임 격자로는 첫인상이 흐려질 수 있으므로, preview thumbnail로 "이게 무엇인가"를 먼저 잡아 준다.
- Deluxe 티어(main post이고 high fav)면 tile 크기가 큰 preview를 쓴다는 점도, LLM 판정 비용을 대표성에 걸어 두는 기존 설계와 일관된다.
새로 알게 된 것D. 라이브 방송 프레임이 두 안전 판정 파이프라인의 대상이 됐다
- grox가 라이브 방송(broadcast)의 프레임을 추출하고, PTOS 안전 파이프라인 두 곳이 이 프레임을 판정한다 확인 —
grox/core/data_loaders/media_loader.py(broadcast 부분). - 미디어 로더에 방송 로더가 새로 들어왔다 확인.
media_loader.py에from broadcast_tools.broadcast import BroadcastLoader가 추가됐고,MediaLoader._broadcast_loader: BroadcastLoader | None = None이 클래스 캐시다._get_broadcast_loader()가 lazy init한다. 이때bearer_token=X_API_BEARER·guest_token=BROADCAST_GUEST_TOKEN환경변수를 쓴다.
- 방송에서 뽑은 프레임은 Video 객체로 정규화된다 확인.
_extract_broadcast_video(broadcast_metadata: BroadcastMetadata) -> Video가BroadcastLoader.load_broadcast(broadcast_id, media_key=..., max_frames=config.video_max_frames, tile_size=config.video_tile_size, max_duration=broadcast_metadata.crop_seconds)로 프레임을 뽑는다.- 뽑은 프레임은
Video(convo_video=ConvoVideo(frames=..., duration=interval, total_duration=result.video.sampled_duration))로 정규화된다. - duration은
(frames[-1].time_sec - frames[0].time_sec) / (len(frames) - 1)로 계산한다.
- 성인 콘텐츠 leading-frames 필터의 진입 조건이 두 곳에서 바뀌었다 확인. 대상은 4세대+17(2026-09-08)에서 정본에 반영한 6-gate 조건이다.
- 조건이 있는 곳은
TaskSafetyPtosAdultContentLeadingFramesFilter._eligible_with_post다.
- 조건이 있는 곳은
- 첫째, Deluxe fav 게이트가 사라졌다. 이제 fav 임계와 무관하게 진입한다 확인.
- 기존 게이트 #2
post.get_fav_count() < grox_config.media_hydration.deluxe_fav_count_threshold: return cls._skip(post, "not_high_fav")가 완전히 삭제됐다.
- 기존 게이트 #2
- 둘째, 긴 영상이 없어도 broadcast가 있으면 진입한다 확인.
- 기존 코드는
long_videos = [m for m in media if m.type == "video" and durationMillis >= 30s]만 봤고, 없으면no_long_video로 건너뛰었다. - 새 코드는
broadcasts = [p.broadcast_metadata for p in (post, post.quoted_post) if p is not None and p.broadcast_metadata is not None and p.broadcast_metadata.broadcast_id]를 함께 계산한다.long_videos나broadcasts둘 중 하나라도 있으면 진입한다. crop_seconds = 10.0이 broadcasts에도 세팅되고,Metrics.counter("...eligible.count")에media = "both" | "broadcast" | "video"태그가 붙는다.
- 기존 코드는
- sex/nudity safemodel 판정도 설정 스위치가 켜져 있으면 broadcast 프레임을 payload로 받는다 확인.
TaskSafetyPtosSafemodelSexNudity._collect_payloads에if grox_config.media_hydration.enable_local_broadcast_frame_extraction:분기가 추가됐다.- 이 분기는
_iter_broadcast_videos(post)로 broadcast video의convo_video.frames에서_MAX_FRAMES_PER_VIDEO = 8을 uniform 샘플링해("broadcast_frame", frame)payload로 append한다. _FRAME_KINDS = {"video_frame", "broadcast_frame"}상수로,has_video·n_video_frames를 계산할 때 두 종류를 동등하게 취급한다._iter_broadcast_videos는post와post.quoted_post각각에서broadcast_metadata.video.convo_video.frames가 있는 것만 yield한다.
의미 (D) 이번 커밋으로 라이브 broadcast가 정식 미디어 payload로 승격돼, 이미 정적 영상에 적용되던 두 안전 파이프라인(adult-content leading-frames와 safemodel sex/nudity)의 판정 대상이 됐다.
- X의 라이브 비디오(broadcast, Periscope의 계승)는 이번 커밋 전까지 grox 안전 파이프라인의 사각지대였다. PTOS가 트윗의 정적 미디어(사진·업로드 영상)만 판정하고, 라이브 방송은 다른 시스템으로 넘어갔거나 미판정 상태였을 가능성이 크다.
- 4세대+17에서 도입된 6-gate 가운데 하나였던 Deluxe fav 임계가 하루 만에 삭제됐다. leading-frames 파이프라인의 조건은 다음 5-gate로 줄었다.
- (a) prior PTOS 결과 존재
- (b) prior NSFW 아님
- (c) 30초+ 영상 또는 broadcast
- (d) prior AdultContent policy 또는 fav ≥ 512
- 즉 재판정 대상 후보군이 급격히 늘어난다. 이 결정은 fav 임계로 재판정 부하를 관리하는 대신, (d)의
ADULT_CONTENT_LEADING_FRAMES_UNCONDITIONAL_FAV = 512와 (a)/(b)의 prior 조건으로 부하 제어를 옮긴다는 뜻으로 읽힌다 추정.
새로 알게 된 것E. 등록된 랭킹 학습 config 두 개 모두에서 멀티모달 포스트 임베딩이 꺼졌다
- 랭킹 훈련 config에서 multimodal post 임베딩이 전부 꺼졌다 확인 —
phoenix/README.md(+3/−3). - nano 훈련 config 표의 "Multimodal post embedding" 행이 바뀌었다 확인.
| 이전 (49815da) | 이후 (75d93d9) | |
|---|---|---|
| ranking nano | v5 on xrecsys_seqpack; off on home_direct_packed |
off (home_direct_packed and xrecsys_seqpack) |
- 부수 설명 문구도 함께 바뀌었다 확인.
"the multimodal-embedding input is off in both, and on every registered ranking config, as the table shows" — phoenix/README.md, 2026-09-09
- 문구의
on은 "off"의 문법적 elision으로 읽어야 한다("off in both, and [off] on every registered ranking config") 확인. 그렇지 않으면 표와 모순된다. - 즉 등록된 랭킹 훈련 config 두 개(
home_direct_packed플래그십,xrecsys_seqpackseqpack) 모두에서 multimodal post 임베딩이 학습 입력에서 빠졌다 확인.
의미 (E) 4세대+17(2026-09-08, 49815da)이 grox 검색 지면 전용 v8-search 멀티모달 임베딩 스택을 신설해, 랭킹 옆에 v5·v8.2와 별개인 벡터 공간을 열었다. 그 하루 뒤인 이번 커밋에서, v5가 켜져 있던 유일한 랭킹 config의 v5 입력이 꺼졌다.
- 그 config는
xrecsys_seqpack이고, 4세대 공개 때부터 v5가 켜져 있었다 확인 —phoenix/README.md표. - 두 사건을 조합하면 시나리오가 좁혀진다: v5는 랭킹 입력으로 유의미한 개선을 내지 못했거나 latency/quality tradeoff에서 밀렸고, v8-search로 넘어가는 중간에 학습 config에서 임시로 뗐다 추정.
- v8-search를 랭킹 config에 연결하는 코드는 이 커밋에 없다. 검색 지면 임베딩과 랭킹 임베딩이 별도 벡터 공간으로 유지될지, v8-search가 v5를 대체할지는 다음 커밋에서 볼 대상이다.
새로 알게 된 것그 밖의 변경
- NUMA 노드 안의 격리 코어(isolcpus)에 스레드를 스케줄하려던 Phoenix의 이전 버그가 고쳐졌다 확인.
- 고친 곳은
phoenix/xrex/utils/gpu.py의 NUMA CPU affinity 처리다. 두 헬퍼_parse_cpulist(text: str) -> set[int]와schedulable_cpus(*nodes: int) -> set[int]가 신설됐다. schedulable_cpus는/sys/devices/system/node/nodeN/cpulist에서 노드 N의 CPU 집합을 읽고,/sys/devices/system/cpu/isolated를 빼서 반환한다.pin_process_to_local_numa는 기존numa.schedule.run_on_nodes(node) → numa.memory.set_membind_nodes(node)를 둘로 나눴다.numa.memory.set_membind_nodes(node)를 먼저,os.sched_setaffinity(0, schedulable_cpus(node))를 둘째로 실행해 예외 처리를 계층화했다.- 노드에 isolated가 아닌 CPU가 없으면 warn을 남기고 membind만 유지한다.
- Phoenix 학습이 실 프로덕션 host에서 격리된 코어 configuration을 만난 결과로 보인다 추정.
- 고친 곳은
- 후보 파이프라인의 컴포넌트마다 남기던 feature-gated info 로그 6곳이 삭제됐다 확인.
candidate-pipeline의quiet-spans로그다.filter.rs·hydrator.rs·query_hydrator.rs·scorer.rs·selector.rs·source.rs에서#[cfg(feature = "quiet-spans")] tracing::info!(...)블록이 각 1개씩 제거됐다(총 −40줄).- 파이프라인 각 컴포넌트 실행마다 남기는 조용한 info-level 로그였다. quiet-spans feature 조건이 상시 활성화된 상태에서 span record와 중복되므로 제거된 것으로 보인다 추정.
- Error 로그의 문자열 포맷도
error!(component = self.name(), error = %err, "source_failed")에서error!("{} Failed: {}", self.name(), err)로 되돌아갔다. 구조화 필드가 제거되고 문자열 로그로 회귀했다.
- Phoenix inference 코드도 조금 바뀌었다 확인. 이번 커밋에서는 API surface를 소소하게 정리한 것으로 보인다.
- 바뀐 파일은
phoenix/xrex/configs/xrecsys.py(−1),phoenix/xrex/inference/model_runner.py(+2/−1),phoenix/xrex/models/recsys_model.py(+5)다. - 4세대+17(2026-09-08)의
ContentFeatures를 소비하도록 연결하는 작업일 가능성도 있으니, 다음 커밋에서 다시 볼 대상이다.
- 바뀐 파일은
정본 반영 services/x.md 랭킹·필터 절 line 392의 VF fail-closed 진술을 갱신했다. 소절도 하나 새로 만들었다(## 파이프라인 아래 "4세대+18" subsection 신설). ## 출처에는 75d93d9 sources를 추가했다.
의미 (전체) 이번 커밋에서 코드 품질로 가장 큰 개선은 VF Action 리팩터이고, 제품에서 가장 눈에 띌 변화는 header-image-prompt-ad다. multimodal 임베딩 학습 config 제거는 X 랭킹 모델의 입력 스키마 결정에서 가장 무거운 신호다. 4세대 공개 때부터 켜져 있던 v5 입력을 v8-search 스택 신설 하루 뒤에 껐으므로, v8-search 연결의 후속을 지켜봐야 한다.
- 4세대+17이 하루 사이에 다섯 축을 동시에 움직인 대형 커밋이었다면, 4세대+18에는 각 축의 정리·후속·확장이 골고루 실렸다.
- VF는 판정 축을 통일했다(가장 크고 근본적인 리팩터).
- 광고는 새 포맷을 추가했다(제품 기능 신설).
- grox는 preview thumbnail로 LLM 입력을 개선했고, broadcast 통합으로 안전 파이프라인 커버리지를 넓혔다.
- 학습 config는 multimodal 임베딩을 제거했다(설계 방향 결정).
검증에서 뒤집힌 것
- "VFFilter는 fail-closed다"라는 정본 진술은 완전히 부정되지 않았고, 표현이 바뀌었다.
- 위치는
services/x.mdline 392다. - 판정 축이
FilteredReason매칭에서 명시적Actionenum으로 옮겨졌다. - fail-closed 성격은 유지된다(NotEvaluated → drop, Strato 경로에서 non-SafetyResult reason → Drop 변환). 다만 필터 층에서
Tombstone과NotEvaluated가 새로 걸러진다. - 정본에 반영했다.
- 위치는
레이더
오늘 레이더에 올린 항목은 없다. arXiv cs.IR 30건을 파싱했고, 수용 기준(플랫폼 기업 저자 또는 프로덕션 A/B 주장)을 통과한 것은 0건이다. 눈에 띈 후보는 다음 넷이다.
SequenceO1: End-to-End Ultra-Long (100K) Sequence Modeling in Recommendation with Low-Rank Caching- 4세대+17을 다룬 daily에서 이미 언급했다. 여전히 프로덕션 A/B가 없다.
PDMR: Passage-Driven Multi-ID Document Retrieval- Generative Retrieval 계열이다. 플랫폼 소속과 프로덕션 결과가 없다. 탈락했다.
Exploring Bottom-Up Clustering for Creating Semantic IDs- Semantic ID 라인의 클러스터링 방법론이다. 플랫폼 소속이 없고, 이론적 증분이다. 탈락했다.
Q2D-Web: A Large-Scale Benchmark for Retrieval in Agentic RAG Systems- Agentic RAG 벤치마크로, 도메인 밖이다.
다음에 볼 것
- Downrank 액션을 실제로 쓰는 곳 — 이번 커밋에서 Downrank가 정식 variant로 승격됐지만, 실제로 이 action을 리턴하는 코드는 아직 없다.
- 승격된 variant는
Action::Downrank다. - VF
visibility-filtering/rules/아래 rule 중 하나가 Downrank를 리턴하게 바뀌는지, 아니면 gRPC 서버가 특정 조건에서 이 action을 내려주게 되는지 지켜본다.
- 승격된 variant는
- header-image-prompt-ad가 들어가는 광고 블렌더 — 이 광고 포맷이 5종 광고 블렌더(
safe_gap·partition_organic·partition_organic_low_risk·time_gap·multi_risk) 가운데 어느 것에 진입하는지, 슬롯 예산 계산에서 프로모트 트윗과 어떻게 함께 산정되는지 다음 커밋에서 관측한다. - v8-search가 랭킹 config에 붙는 시점 — v5가 켜져 있던 유일한 config
xrecsys_seqpack에서 v5가 꺼졌으므로, v8-search가 후속 연결의 유력한 후보다.phoenix/xrex/data/retrieval_dataset.py에grox-multimodal-embedding-v8-searchtopic 소비자가 붙는지,phoenix/README.md표에 v8-search가 추가되는지 지켜본다. - broadcast의 PTOS 재판정률 — 라이브 broadcast가 정식 payload가 된 첫 커밋이다.
task.safety_ptos_adult_content_leading_frames.eligible.count{media=broadcast}와task.safety_ptos_safemodel_sex_nudity의 broadcast_frame 관련 카운터 대시보드가 어딘가에서 노출되면 실제 재판정 규모를 관측할 수 있다. 직접 관측은 어렵다. FilteredReason::ViewerBlocksAuthor를 리턴하는 rule — 새 variant지만, 이 커밋에는 어느 rule이 이것을 리턴하게 되는지 보여 주는 코드가 붙지 않았다.visibility-filtering/rules/author_rules.rs의 다음 diff에서 관측한다.- NUMA isolated CPU 문제가 생긴 실제 host configuration — Phoenix 학습 host가 왜 isolated CPU를 노드 안에 두었는지(RT 워크로드? DPDK? SR-IOV VF driver?), 실제 프로덕션 configuration이 어딘가에서 공개될지 지켜본다.
조회 실패: 없음.