0x00

리서치 / 일일 리포트

  • 읽은 자료 1건
  • 정본 반영 1개 파일
  • 레이더 0건
한 줄 요약X가 랭킹 점수에 쓰는 참여 헤드 가중치 26개를 유저와 헤드마다 정해진 방향으로 흔들 수 있게 했다. 흔드는 세기의 기본값이 0.0이라 기본 동작은 그대로다.

Meta와 Netflix에서는 새 자료가 없었다.

1랭킹 가중치를 유저마다 흔드는 장치와 검색 전용 임베딩이 생겼다 — X

자료 xai-org/x-algorithm@49815da — X.AI Corp., 2026-09-08 19:23 UTC, 공개 소스코드. 78개 파일, +10,938/−982. Open-source X Recommendation Algorithm 정기 크론 미러다. 이 노트들은 X의 4세대 공개 뒤에 나온 커밋에 차례로 번호를 붙여 부르며, 이 커밋은 4세대+17이다.

배경 X의 오픈소스 미러는 프로덕션 캐넌(프로덕션에서 기준이 되는 코드)을 크론으로 흘려보내는 정기 커밋이다. 어제(2026-09-09) 판은 4세대+16(902a06f)까지였고, 이 커밋이 그다음 판이다. 지난 두 판은 부수 변화가 중심이었다. 이번 커밋은 그와 달리 서로 다른 다섯 갈래의 변화를 한꺼번에 담았다(+10,938/−982, 파일 78개).

  • 랭킹 스코어러가 참여 헤드 가중치를 서빙 요청마다 결정론적으로 흔드는 새 메커니즘이 생겼다.
  • grox(X의 nearline LLM 주석 파이프라인)에 검색 지면 전용 멀티모달 임베딩 스택이 새로 생겼다.
  • Phoenix(Grok 기반 Transformer 랭커)의 서빙 proto에 구조화된 콘텐츠 피처 ContentFeatures가 들어갔다.
  • PTOS(정책 기반 위반 카테고리 판정)에 성인 콘텐츠를 영상 앞부분만 다시 훑는 심층 재스캔 플랜이 생겼다.
  • Phoenix의 FA4(CuTeDSL로 쓴 flash attention 커널)가 forward와 backward 모두, 두 GPU 아키텍처용으로 대규모 공개됐다.

A. 랭킹 가중치를 유저와 헤드마다 정해진 방향으로 흔드는 장치가 생겼다

랭킹 스코어러가 참여 헤드 26개의 가중치를 유저와 헤드마다 정해진 방향으로 키우거나 줄일 수 있게 됐다. 흔드는 세기의 기본값이 0.0이므로 기본 동작은 이전처럼 결정론이다.

  • home-mixer(X 홈 타임라인 피드 요청을 서빙하는 서비스)에 가중치를 흔드는 파라미터 두 개가 새로 생겼다 확인 — home-mixer/params/param.rs.

    • 흔드는 세기는 WeightPerturbationSigma(f64, default 0.0)다.
    • 부호를 정하는 salt는 WeightPerturbationSalt(String, default "")다.
    • 파일 헤더의 sync 시각도 2026-09-04T16:22:24Z → 2026-09-08T16:24:06Z로 갱신됐다.
  • 스코어러는 파라미터에서 가중치를 읽은 직후 이 흔들기를 적용한다 확인 — home-mixer/scorers/ranking_scorer.rs.

    • ScoringWeights::from_params(&query.params) 뒤에 .perturbed(query)가 붙었다.

    • perturbed() 자체 로직은 아래와 같다.

      pub(crate) fn perturbed(mut self, query: &ScoredPostsQuery) -> Self {
          let sigma = query.params.get(WeightPerturbationSigma);
          if sigma <= 0.0 {
              return self;
          }
          let salt = query.params.get(WeightPerturbationSalt);
          for (head, weight) in self.weights_mut() {
              *weight *= (sigma * perturbation_sign(&salt, query.user_id, head)).exp();
          }
          self.recompute_sums();
          self
      }
      
      pub(crate) fn perturbation_sign(salt: &str, user_id: u64, head: &str) -> f64 {
          let digest = md5::compute(format!("{salt}:{user_id}:{head}"));
          if digest[0] & 1 == 1 { 1.0 } else { -1.0 }
      }
      
  • 흔드는 대상은 참여 헤드 26개 전부다 확인 — 위 두 파일.

    • 대상 헤드는 favorite·reply·retweet·photo_expand·video_open·click·open_link·profile_click·vqv·share·share_via_dm·share_via_copy_link·dwell·quote·quoted_click·quoted_vqv·dwell_time·click_dwell_time·active_secs_5m_residual_norm·follow_author·post_unexplored·not_interested·block_author·mute_author·report·not_dwelled이다.
    • 4세대 랭커의 core positive 17헤드, negative 5헤드, continuous 3헤드에 post_unexplored 1개를 더한 26개가 모두 대상이다. 앞의 세 묶음은 각각 engagement, user feedback, dwell·click_dwell·active_secs에 해당한다.
  • 가중치를 흔든 뒤에는 양수·음수·전체 합계도 함께 다시 계산한다 확인. 그래서 합계를 쓰는 하류 게이팅 로직도 흔들린 값을 그대로 반영한다.

    • 합계 필드는 positive_sum·negative_sum·total_sum이고, 재계산은 recompute_sums()가 맡는다.
    • 하류 게이팅의 예로 not_dwelled 페널티의 total_sum 스케일링이 있다.
  • 부호는 md5(salt:user_id:head)로 정해지므로, 같은 salt 아래에서는 결과가 유저마다 고정된다 확인.

    • 같은 salt, 같은 유저이면 부호도 같다. 요청 사이에 결정론적이다.
    • 같은 salt, 같은 유저라도 부호는 헤드마다 독립적이다. 헤드 26개가 서로 다른 방향으로 흔들린다.
    • salt를 바꾸면 유저별 배치가 전부 뒤바뀐다. 재현성을 지키면서도 salt를 바꿔 가며(salt cycling) exploration을 다시 시작할 수 있다.
  • 흔드는 배율은 σ에 대한 지수 함수다 확인. 계수 배율은 exp(sigma × sign) = exp(±sigma)다.

    • σ=0.1이면 가중치에 ×1.105 또는 ×0.905를 곱하고, σ=0.3이면 ×1.35 또는 ×0.74를 곱한다.
    • 지수 스케일이므로 곱셈 대칭이 보장된다. σ=0.1의 up과 down은 정확히 곱셈 역수 관계이고, log-space에서 대칭 분포를 만든다.

이 장치의 용도는 세 가지로 읽을 수 있다. 계약 자체는 확인이고, 용도는 추정이다.

  1. 가중치 A/B를 위한 gradient-free 인프라로 읽을 수 있다. 프로덕션 유저 트래픽으로 가중치 조합의 sensitivity를 재는 wobble 실험이다. 실험 arm마다 WeightPerturbationSalt를 다르게 세팅하면 arm마다 독립적인 노이즈 프로필이 생긴다. 계약은 확인이고, A/B용으로 쓴다는 것은 추정이다.
  2. on-policy exploration, 또는 bootstrap 방식의 bandit으로 읽을 수도 있다. VMRanker(home-mixer에서 slate reranker 역할을 하는 원격 gRPC 서비스) 이전 단계에서 점수 자체를 흔들어 impression을 강제로 다양화한다는 해석이다 추정. bandit 기법으로 치면 UCB보다 Thompson sampling에 가깝지만, posterior 대신 고정된 노이즈를 쓰고 유저-헤드 조합마다 결정론이다 추정.
  3. 모델 편향 감사나 robustness 측정 도구로 읽을 수도 있다. 가중치가 흔들려도 모델 점수가 안정적으로 어떤 헤드에 의존하는지 관측하는 도구라는 해석이다 추정.
  • 기본값이 0.0이므로 프로덕션 기본은 여전히 결정론이다 확인 — param.rs.
  • 이 계약이 옵션으로 존재한다는 것 자체가 신호다. 4세대는 지금까지 개별 게이트로 stochasticity를 넣어 왔다. EnableViewerColdStart의 Thompson, ValueModelMode의 dwell_regret_sigmoid가 그런 예다. 이번에는 참여 헤드 가중치 전체를 대상으로 하는 통합된 stochastic layer를 붙였다.
  • 이 장치는 4세대+16에서 codivert impression을 대칭으로 남기게 한 변경과 같은 흐름이다. A/B 인프라 자체가 4세대에서 계속 두꺼워지고 있다.

아래 내용은 오픈소스에 공개되지 않았다 확인.

  • salt 값을 실제로 어떤 experiment framework에서 세팅하는지
  • 실제 프로덕션의 σ 값
  • 어느 지면(For You / Following / RankedFollowing)에 적용되는지
  • 결과 로깅이 어떤 side_effect로 흘러가는지

B. grox에 검색 지면 전용 멀티모달 포스트 임베딩 스택이 새로 생겼다

grox가 기존 두 임베딩 파이프라인 옆에 검색 지면 전용 세 번째 파이프라인을 만들었다. 과거 포스트를 뒤늦게 임베딩하는 backfill 경로와, backfill 대상을 고르는 fav 임계를 처리량에 맞춰 자동으로 조절하는 장치도 함께 들어왔다.

  • 신규·수정 파일은 17개다 확인 — grox/flows/mm_emb/.
    • 신규 파일 11개, 총 1,026줄이 새로 생겼다. 임베더와 렌더러는 embedder_v8_search.py(113), renderer_v8_search.py(461)다.
    • 플랜 파일은 plan_post_embedding_v8_search.py(34), plan_post_embedding_v8_search_for_reply.py(37)다. backfill 플랜은 plan_post_embedding_v8_search_backfill.py(43), plan_post_embedding_v8_search_backfill_batch.py(16)다.
    • backfill 쪽 파일은 task_backfill_fanout.py(85), task_backfill_gate.py(60), backfill_consumer_dedup.py(49)다. fav 관련 파일은 batched_fav_hydrator.py(65), adaptive_min_fav.py(63)다.
    • 수정 파일은 generators.py(+82), constants.py(+9), task_multimodal_post_embedding.py(+52)다. task_write_mm_embedding_sink.py(+90), task_rate_limit.py(+15), task_embedding_pub.py(+5)도 고쳤다.
  • 기존 두 임베딩 파이프라인 옆에 세 번째 파이프라인이 나란히 연결됐다 확인.
    • 기존 파이프라인은 레거시 v5_1과 현행 v8_2이고, 새 파이프라인은 v8_search다.
    • 새 플랜은 mm_emb_v8_search·mm_emb_v8_search_backfill·mm_emb_v8_search_backfill_batch·mm_emb_v8_search_for_reply 네 개다.
  • 실시간용과 backfill용 임베딩 모델이 따로 있다 확인. 별도 backfill용 모델이 붙어 있다는 것은 실시간 임베더가 처리하지 못하는 볼륨을 다른 인스턴스로 흡수한다는 뜻이다.
    • 두 임베딩 모델 키 RECSYS_V8_SEARCH_EMBED·RECSYS_V8_SEARCH_BACKFILL_EMBED가 grox_config에서 endpoint를 조회한다.
  • 상태 키와 버전 태그가 분리돼 있고, 새 임베딩은 v5·v8.2와 독립된 벡터 슬롯을 차지한다 확인.
    • 태그는 V8_SEARCH_STATE_KEY = "v8_search"·V8_SEARCH_MODEL_VERSION = "v8_search_1"이다.
    • 슬롯은 MultimodalPostEmbeddingState.embeddings["v8_search"]다.
  • 세 kafka 스트림이 각각 대응하는 Plan과 연결됐다 확인.
    • POST_EMBEDDING_V8_SEARCH_STREAM은 grox-multimodal-embedding-requests-with-summary 토픽을 쓴다.
    • POST_EMBEDDING_V8_SEARCH_FOR_REPLY_STREAM은 content-understanding-...-min-traction-for-grox-multi-modal을 쓴다.
    • POST_EMBEDDING_BACKFILL_STREAM은 grox-multimodal-embedding-backfill을 쓴다.

렌더러는 포스트 하나를 임베딩용 문서 하나로 만든다. 렌더러 함수는 V8SearchEmbedPostRenderer.render_for_embedding이다.

  • 문서 맨 앞에 추천 목적을 밝히는 instruction prefix를 붙인다 확인. E5·GTE 계열 instruction-tuned embedding에서 흔한 패턴이다.
    • prefix 원문은 다음 한 줄이다.

      INSTRUCTION_PREFIX = "Represent this X post (formerly Twitter) for recommendation: capture its topic, intent, key entities, and likely engagement."
      
    • 재현할 때 Represent this X post ... recommendation: 스타일의 프롬프트가 검색-랭킹 목적 벡터 공간에 맞춰진 임베딩을 유도한다.

  • 포스트 텍스트 앞에는 저자 블록이 들어간다 확인. 블록에는 author bio, verified, 팔로워 수, 팔로잉 수, 계정 가입월이 담긴다.
    • 팔로워 수는 두 표현으로 함께 붙는다. 하나는 _humanize_followers가 K/M/B 단위로 사람이 읽는 표기로 축약한 값이고, 다른 하나는 _tier_label의 구간(<10·10-100·100-1K·…·>100M)이다.
    • 인코더가 "정확한 숫자"와 "규모 구간" 두 신호를 동시에 보게 하는 구성이다.
  • 텍스트 필드마다 길이 상한을 두고 자른다 확인.
    • 본문은 MAX_POST_TEXT = 4000자, quoted post는 MAX_QUOTED_TEXT = 1000자로 자른다.
    • ASR 트랜스크립트는 MAX_ASR = 2000, 카드 title은 MAX_CARD_TITLE = 100, article title은 MAX_ARTICLE_TITLE = 200이다.
    • 본문의 dangling media URL(https://twitter.com/user/status/N/photo/1)은 정규식으로 지운다.
  • 미디어는 원본 포스트와 quoted 포스트의 프레임을 한 예산 안에 함께 담는다 확인.
    • 프레임 예산은 MAX_MEDIA = 16이다. 임베더의 실제 프레임 상한(기본값)은 MAX_FRAMES = 4다.
    • 비디오이면 is_video: True와 transcript도 함께 렌더한다.
  • 텍스트도 미디어도 없는 포스트는 하류에서 폐기된다 확인.
    • 렌더러는 document를 반환하고 임베딩은 빈 리스트가 된다. 경고 로그 Post {post.id} has no text or media content가 남는다.
    • 하류 태스크 TaskMultimodalPostEmbeddingV8Search가 TaskStopExecution으로 폐기한다.

임베더는 OpenAI 호환 API로 벡터를 받은 뒤 다시 정규화한다. 임베더 클래스는 MultimodalPostEmbedderV8Search다.

  • 임베딩 요청은 OpenAI 호환 embed API로 텍스트와 이미지를 함께 보낸다 확인. timeout은 60초다.
    • XaiEmbeddingClientHttp 클라이언트로 embed_openai_async(text=text, images=images)를 호출한다.
  • 저자의 core 정보를 조회해 렌더러에 넘긴다 확인. 조회에는 Strato(코드가 데이터를 조회하거나 저장할 때 거치는 X 내부 시스템)의 StratoUserCore fetch를 쓴다.
    • 조회에 실패하면 post_embedding_v8_search.core_user_fetch_error 카운터가 오른다.
  • API가 반환한 벡터를 다시 L2 norm 1로 정규화한다 확인. 검색-랭킹에서 dot-product 유사도가 cosine이 되게 강제하는 것이다.
    • 재정규화 함수는 _renormalize다.
  • 관측 지표는 히스토그램 3개와 카운터 2개다 확인.
    • 히스토그램은 render_duration_ms·encode_duration_ms·total_duration_ms이고, 카운터는 image_count·core_user_hit이다.

새 포스트를 처리하는 실시간 파이프라인과 별도로, 과거 포스트를 뒤늦게 임베딩하는 backfill 경로가 새로 생겼다.

  • backfill 경로는 kafka에서 받은 post_id 목록을 배치로 나누고, 배치마다 중복 제거와 fav 조회를 거쳐 인스턴스별 플랜으로 보낸다 확인. 플랜 안에서 두 게이트를 지나야 임베딩하고 sink에 쓴다.

    • 아래 도식이 backfill 5-DAG 아키텍처다.

      KafkaMmBackfillRequestLoader(TOPIC_BACKFILL="grox-multimodal-embedding-backfill")
        ↓ (post_id의 byte-packed list<i64>를 배치 크기 BATCH_SIZE로 분할)
      TaskBackfillBatchFanout
        ↓ (BackfillConsumerDedup: 배치 안에서 이미 관측한 post_id 제거)
        ↓ (BatchedFavHydrator: 배치 단위 fav count 조회)
        ↓ (asyncio.Semaphore(FANOUT_CONCURRENCY=240): 병렬 실행 상한)
      PlanPostEmbeddingV8SearchBackfill (인스턴스별 실행)
        ↓
      TaskBackfillFavGate + TaskBackfillPreloadGate + TaskMultimodalPostEmbeddingV8SearchBackfill + TaskWriteV8SearchSink
      
  • 이 부분에서 흥미로운 것은 적응형 fav 임계(AdaptiveMinFav)다. backfill 대상을 고르는 fav 임계가 고정값이 아니라 실제 통과 QPS에 따라 움직인다 확인.

    • 목표 QPS와 시작 임계는 아래처럼 정한다.

      _TARGET_QPS = float(os.environ.get("MM_EMB_BACKFILL_TARGET_QPS", "5"))
      _min_fav = AdaptiveMinFav(start=MIN_FAV_COUNT, target_qps=_TARGET_QPS)
      
    • PI(비례-적분) 컨트롤러 스타일로 backfill의 실제 통과 QPS를 관측해 fav 임계를 동적으로 조절한다.

    • 게이트 통과 QPS가 target(기본 5 QPS)보다 높으면 임계를 올리고, 낮으면 내린다. 임계는 fav 20k에서 시작한다.

    • deadband=0.1은 ±10%까지는 조정하지 않는 범위다.

    • warmup_s=60은 첫 60초 동안 관측만 하게 한다.

    • gain=0.3은 에러 대비 조정 강도다.

    • floor=0·cap=20000은 경계값이다.

  • 이런 장치가 필요한 이유는 backfill 트래픽을 정교하게 조절해야 하기 때문이다. 임베딩 서버의 GPU/CPU 예산과 하류 kafka sink의 처리 능력을 넘으면 안 된다.

    • 하드코딩된 threshold(예: MIN_FAV_COUNT = 20_000)로는 kafka 재생 속도나 서버 큐 압력에 반응할 수 없다. AdaptiveMinFav가 이를 자동으로 스로틀링한다.
    • 새로운 지점은 인프라 코드에서 흔한 rate limiter 패턴을 임베딩 대상 선정에 적용했다는 것이다.
  • backfill 진입 필터는 TaskBackfillFavGate와 TaskBackfillPreloadGate 두 층이다 확인.

    • 첫째 층 TaskBackfillFavGate는 like 수가 동적 임계 이상인 포스트만 들여보낸다. 조건은 post.counts.likes >= min_fav이고, 걸러지면 task.backfill_gate.dropped.count{reason=low_favs|fav_count_missing}로 관측한다.
    • 둘째 층 TaskBackfillPreloadGate는 이미 임베딩이 있는 포스트를 건너뛴다. StratoPostMultimodalEmbeddingMhSearchAi().fetch(post_id, V8_SEARCH_MODEL_VERSION)이 non-null이면 건너뛰고, 없어야 통과한다.
  • PI 컨트롤러는 실제 임베딩까지 이어지는 QPS만 target에 반영한다 확인.

    • TaskBackfillPreloadGate를 통과할 때 _min_fav.observe_pass()를 호출한다.
    • 다시 말해 이미 임베딩이 있어 건너뛴 포스트(preload 히트)는 rate 계산에 들어가지 않는다.

이번 변화는 검색 지면 전용 벡터 공간을 처음으로 분리한 사건이다.

  • 지금까지 X의 grox multimodal 임베딩은 v5 하나와 v8.2 하나의 벡터 공간을 랭킹·retrieval·search 전반에 재사용했다. 2026-09-01 4세대+13에서 관측한 사실이다.
  • 4세대+4(2026-08-19)에서 Phoenix가 xrecsys_search config로 검색 랭킹 지면에 확장된 것과 대응한다. 검색 랭킹 모델과 짝을 이루는 검색 문서 임베딩 파이프라인이 이제 완비됐다.

검색 임베딩의 모델과 소비처는 공개되지 않았다 확인.

  • RECSYS_V8_SEARCH_EMBED·RECSYS_V8_SEARCH_BACKFILL_EMBED 두 endpoint의 실제 모델(dimension, base architecture, training data)은 오픈소스에 없다.
  • 검색 지면에서 어떻게 소비하는지(retrieval index 이름, ANN 파라미터)도 공개되지 않았다.
  • Kafka 토픽 grox-multimodal-embedding-v8-search는 등록됐지만, consumer 쪽 지면 코드는 함께 오지 않았다 확인.

C. Phoenix 서빙 요청에 포스트의 텍스트 구조 시그널이 실렸다

Phoenix 서빙 proto에 포스트와 인용 포스트의 콘텐츠 피처 7개를 담는 메시지가 새로 생겼고, home-mixer 랭커에는 그 값을 만드는 빌더가 들어왔다. 그중 텍스트 길이, 줄바꿈 수, URL 유무는 처음 노출되는 텍스트 특성이다.

  • 포스트 정보 메시지에 콘텐츠 피처 필드 두 개가 붙었다 확인 — phoenix/crates/serving/xai-recsys-proto/proto/recsys.proto, phoenix/python/common/xai-proto/proto/recsys.proto.

    • TweetInfo에 contentFeatures·quotedContentFeatures 두 필드가 붙었고, 둘 다 새 message ContentFeatures 타입이다. 인용 포스트도 같은 방식으로 연결했다.

    • 신규 proto message는 아래와 같다.

      message TweetInfo {
        // ...
        ScoreInfo scoreInfo = 35;
        ContentFeatures contentFeatures = 36;
        ContentFeatures quotedContentFeatures = 37;
      }
      
      message ContentFeatures {
        bool hasVideo = 1;
        uint32 maxVideoDurationMs = 2;
        bool hasPhoto = 3;
        uint32 mediaCount = 4;
        uint32 weightedTextLen = 5;
        uint32 newlineCount = 6;
        bool hasUrl = 7;
      }
      
  • 7개 시그널 가운데 미디어 시그널은 기존에도 개별 필드로 있던 것을 하나의 struct로 묶었고, 나머지 3개는 새로 노출되는 텍스트 특성이다 확인.

    • 미디어 시그널은 has_video·has_photo·media_count·max_video_duration_ms다.
  • 텍스트 길이 weighted_text_len은 단순 문자 수가 아니라 Twitter 자체 텍스트 길이 계산 규칙을 그대로 따른다(URL=23, CJK 2배 등) 확인.

    • URL은 무조건 23자로 센다. t.co shortener 정책과 같다.
    • CJK 문자와 이모지는 2자로 센다. 1자 범위(SINGLE_WEIGHT_CODEPOINT_RANGES) 밖의 문자는 모두 2자다 확인 — content_features.rs:72-85.
    • ASCII와 기본 라틴 문자는 1자로 센다. 구두점은 범위가 가른다: ASCII 구두점과 U+2010–U+201F 같은 일반 구두점은 1자이고, 범위 밖의 구두점은 2자다.
    • variation selector(0xFE0E·0xFE0F), skin-tone modifier(0x1F3FB..0x1F3FF), tag 문자(0xE0020..0xE007F)는 0자다. base 이모지에 흡수된다.
    • 미디어 첨부 URL은 external URL이 아니다. 미디어 attachment가 있으면(attachment_urls=1) 첫 URL을 media URL로 보고 external URL count에서 뺀다.
  • 줄바꿈 수 newline_count는 \n 개수다 확인. 줄바꿈으로 목록을 만든 tweet이나 시 형태 같은 게시글 형식의 프록시다.

  • has_url은 external URL이 하나 이상 있는지를 나타낸다 확인.

  • 피처를 만드는 빌더는 4세대의 코드 스타일대로 순수 함수 2개(build·build_quoted)만 export한다 확인 — home-mixer/models/content_features.rs (185줄 신규).

    • 빌더(home-mixer/models/content_features.rs)에서 실제 계산은 TextMetrics::of(text, attachment_urls)가 맡는다.

    • 회귀 테스트 5개가 Twitter의 text weighting rule을 코드로 고정한다.

      assert_eq!(weighted(""), 0);
      assert_eq!(weighted("hello world"), 11);
      assert_eq!(weighted("日本語"), 6);  // CJK 3자 × 2
      assert_eq!(weighted("see https://t.co/abc123 now"), 3 + 1 + 23 + 1 + 3);  // URL=23
      assert_eq!(weighted("ok 👍🏽"), 2 + 1 + 2);  // 스킨톤 modifier는 0
      assert_eq!(weighted("日本\u{3000}語"), 4 + 2 + 2);  // ideographic space도 CJK 취급
      

4세대+17에서 텍스트 자체의 구조 시그널이 서빙 wire format의 first-class field로 승격됐다.

  • 이제까지 Phoenix 랭커가 소비하는 콘텐츠 시그널은 두 종류였다. 하나는 임베딩 테이블에서 lookup하는 hashed feature이고, 다른 하나는 has_media·has_video·view_count 같은 산발적인 boolean과 정수다.
  • 특히 weighted_text_len은 포스트의 280자 상한(가중치 기준)과 직결되는 값이다 확인 — X 개발자 문서 Counting Characters. 그래서 랭커가 "이 포스트가 얼마나 꽉 차 있나 / URL만 걸고 지나가는 pass-through인가"를 명시적으로 학습할 수 있게 된다.

다만 이 시그널을 랭킹 헤드에서 어떻게 소비하는지는 이번 커밋에 없다 확인. phoenix/xrex/models/recsys_model.py에 content_features를 읽는 코드가 아직 없기 때문이다 확인. wire에는 실렸지만, 학습에서 소비하는지는 다음 커밋에서 관측할 대상이다.

D. PTOS가 긴 영상의 앞 10초만 잘라 성인 콘텐츠를 다시 판정하는 경로를 얻었다

이미 PTOS를 한 번 통과했지만 NSFW 태그가 없는 30초 이상 영상을, 앞부분만 잘라 다시 판정하는 경로가 새로 생겼다. 인기 포스트만 들어오고, 별도 rate limit으로 비용에 상한을 둔다.

  1. 작성자(post.user)가 있어야 한다. anonymous 유저와 삭제된 유저는 빠진다.
  2. fav 수가 deluxe 티어 임계 이상이어야 한다(post.get_fav_count() >= deluxe_fav_count_threshold). 이 임계는 4세대+12에서 비용 관리용으로 도입됐고, 인기 포스트만 진입한다.
  3. post.media 또는 post.quoted_post.media에 30초 이상 영상이 있어야 한다. 기준은 ADULT_CONTENT_LEADING_FRAMES_MIN_DURATION_SECONDS = 30.0이다.
  4. 이전 PTOS 판정이 있어야 한다. StratoSafetyPostAnnotationsResultDirectMh.fetch(post_id)가 non-null이어야 한다. 즉 이미 PTOS 파이프라인을 한 번 통과한 포스트가 대상이다.
  5. 이전 판정이 NSFW가 아니어야 한다. prior.safetyBoolMetadata.isNsfw == True이면 이미 처리된 것이므로 건너뛴다.
  6. 이전 판정에 AdultContent 카테고리의 violated policy가 있거나, fav count가 512 이상(ADULT_CONTENT_LEADING_FRAMES_UNCONDITIONAL_FAV)이어야 한다. 둘 중 하나만 맞으면 된다.
  • 통과한 영상은 앞 10초만 잘라서 다시 판정한다 확인.
    • video.crop_seconds = 10.0(ADULT_CONTENT_LEADING_FRAMES_CROP_SECONDS)이 세팅되고, 다운스트림 media hydration이 이 길이만큼만 잘라 판정한다.
    • 이후 category detection → policy detection → safemodel sex/nudity 판정 → cross-validation 8단 DAG를 다시 밟는다.

이 경로가 필요한 이유는 다음과 같다.

  • 30초 넘는 영상은 media hydration 비용(디코딩·프레임 추출·모델 호출)이 크다. 그래서 실시간 파이프라인은 미리 정해진 sample 프레임만 뽑아 판정한다.
  • 그런데 회피 유저가 첫 15초 이상을 정상 콘텐츠로 채우고 후반에 성인 콘텐츠를 넣는 패턴을 쓰면, 실시간 판정이 놓칠 수 있다.
  • 4세대+12에서 도입된 PlanSafetyPtosSpecialVideo는 캡션이나 워터마크 같은 정적 오버레이로 가려진 콘텐츠를 다룬다. 4세대+17의 이 경로는 첫 몇 초를 안전하게 위장하는 시간축 회피를 다룬다.
  • 이름은 leading-frames지만 실제로는 "앞 10초를 crop해서 재판정"이므로, 성인 콘텐츠가 앞부분에 짧게 나타나는 케이스를 특히 겨냥한 것으로 보인다 추정.
  • 이전 판정에 AdultContent violated policy가 있는데도 isNsfw가 아닌 경우가 있다. 4세대+2의 성인 콘텐츠 cross-validator에서 Grok 4.5 심판이 강등만 허용하기 때문이다. 그래서 primary가 AdultContent라 해도 NSFW 태그로 굳어지지 않을 수 있다.
    • 이 경로는 강등된 판정을 다시 뒤집을 수 있는 두 번째 심판이 된다.

진입 임계 512 fav의 함의도 읽어 둘 만하다.

  • deluxe 임계(deluxe_fav_count_threshold)는 훨씬 높은 값일 가능성이 크므로(4세대+7의 log에서 deluxe = 5,000 이상 추정 추정), 512 fav 이상이면서 prior AdultContent가 없는 케이스만 무조건 진입시키는 것은 prior 판정이 AdultContent를 잡지 못한 저강도 signal 케이스를 커버하려는 것이다.
  • 512는 트위터에서 중간 정도 인기의 포스트에 해당하는 값이다. 이 규모라면 오탐이나 누락에 대한 유저 반응(신고, 뮤트)이 이미 발생했을 확률이 높다.

운영 수치와 심판 모델은 공개되지 않았다 확인.

  • 실제 프로덕션에서 이 경로가 얼마나 활성화되는지
  • 재판정에서 실제로 뒤집는 비율(precision·recall)
  • 어떤 심판 모델을 쓰는지
    • 플랜 DAG를 보면 정규 category/policy/safemodel/cross-validation을 모두 다시 실행한다 확인.

다만 Rate limit(TaskRateLimitSafetyPtosAdultContentLeadingFrames)이 별도로 설정돼 있어 비용 상한은 있다 확인.

E. Phoenix FA4 attention 커널의 forward와 backward가 SM80·SM90용으로 모두 공개됐다

Phoenix의 FA4 attention 커널이 forward와 backward 모두, 두 GPU 아키텍처(SM80 Ampere, SM90 Hopper)용으로 공개됐다. backward pass 원본이 공개된 것은 이번이 처음이다.

파일 상태 줄 역할
flash_fwd.py added 1,333 SM80/Ampere forward kernel
flash_fwd_sm90.py added 1,465 SM90/Hopper forward kernel
flash_bwd.py added 1,954 SM80/Ampere backward kernel
flash_bwd_sm90.py added 1,864 SM90/Hopper backward kernel
flash_bwd_preprocess.py added 329 dO·row_sum·softmax_LSE 계산 (pre-bwd)
ampere_helpers.py added 135 SM80 전용 유틸(warp shuffle, register 배치)
block_sparse_utils.py modified +412/−314 block-sparse iterator 재작성
ranker_attention_fa4.py modified +273/−130 fwd 진입점 리팩터
ranker_attention_varlen_fa4.py modified +178/−211 4세대+15 varlen 지원의 이관·정리
mask.py modified +63/−1 AttentionMask 확장 (bwd용)
flash_bwd_postprocess.py modified +2/−7 기존 post-bwd 파일 정리
  • 신규 코드는 총 7,080줄이고, 수정 폭도 크다 확인. 이번 커밋에서 개별 라인 수가 가장 큰 갈래다 확인.
  • 공개된 커널은 Dao-AILab/flash-attention의 원본 코드를 바탕으로 X.AI가 수정한 fork다 확인. 파일 헤더가 출처와 라이선스를 밝힌다.
    • 헤더 원문을 그대로 옮긴다.

      # Derived from flash-attention (https://github.com/Dao-AILab/flash-attention);
      # modified by X.AI Corp.
      # SPDX-License-Identifier: Apache-2.0
      # Copyright 2026 X.AI Corp.
      
    • 라이선스는 Apache-2.0이다. 원본 BSD 3-Clause에서 재라이선스할 수 있다.

    • phoenix/NOTICE 파일에도 flash-attention 서브구성요소의 저작권 표기가 +198줄 추가됐다.

  • 커널 클래스(FlashAttentionForwardBase·FlashAttentionBackwardSm80)를 보면 주요 특징이 드러난다 확인.
    • CuTeDSL(cutlass Python DSL)로 쓰였다. NVIDIA의 CUTLASS 커널을 Python으로 metaprogramming하는 새 방식이다. 코드에서는 import cutlass·import cutlass.cute as cute로 불러 쓴다.
    • Q·K와 V의 hidden dim이 달라도 되는 asymmetric attention을 지원한다(head_dim != head_dim_v). 랭킹 모델에서 값 차원을 낮춰 memory footprint를 줄이는 트릭이다.
    • Grouped Query Attention을 지원한다(pack_gqa). 여러 Q head가 하나의 KV head를 공유하는 방식이고, LLM에서 흔한 최적화가 랭킹 attention에도 적용된다.
    • is_causal·is_local·score_mod·mask_mod를 지원한다. 랭킹에서 causal masking은 시퀀스 히스토리 attention에 필요하고, score_mod·mask_mod는 Flex Attention의 사용자 정의 modifier hook이다.
    • has_aux_tensors 옵션은 attention에 추가 sideband tensor를 흘려 넣는 경로다. 4세대+14에서 도입된 backbone scores 등에 연결될 것이다 추정.
    • 두 스케줄러 SingleTileScheduler·SingleTileVarlenScheduler가 각각 fixed-length와 variable-length seqlen을 맡는다. 4세대+15에서 도입된 build_dense_block_sparse_layout 시그니처 확장이 이제 varlen backward까지 완결됐다.
  • backward 커널(FlashAttentionBackwardSm80)에만 있는 특징도 있다 확인.
    • 타일 크기(m_block_size=64·n_block_size=128·num_stages_Q=2·num_stages_dO=2·num_threads=256)는 SM80 세대의 register/shared memory 예산에 맞췄다.
    • JIT 함수 _bs_bwd_seq_at이 mask 축·full 축·diag 축 세 iterator 도메인 사이의 인덱스 매핑을 constexpr subtile로 수행한다. block-sparse backward의 핵심이다.
    • preprocess와 postprocess를 분리했다. forward에서 저장한 softmax_LSE(log-sum-exp)로 pre-bwd가 dO·row_sum을 미리 계산하고, 이 단계가 flash_bwd_preprocess.py 329줄이다.
    • main bwd 커널이 이 값을 쓰고, post-bwd가 dQ의 partial sum을 accumulate한다(flash_bwd_postprocess.py).
    • Flash Attention 2 논문의 dual-block iteration을 CuTeDSL로 다시 구현했다.

이번 공개의 의미는 세 가지다.

  • 오픈소스 쪽에서 보면 backward pass 원본은 이번이 처음 공개됐다. 4세대+15(2026-09-05 daily 참조)에서는 FA4 forward의 varlen 지원 재작성이 공개됐다. Backward는 학습에 필수이므로, 이전에는 X 내부 저장소에만 있었고 오픈소스 미러에서는 forward만 공개된 형태였다. 이제 X 랭커의 학습·서빙 attention 커널이 완전히 오픈소스에 있다.
  • SM90 연결이 확정됐다. 4세대+15에서 SAVE_H100_RECSYS = 32 remat 정책(역전파 때 활성값을 저장하지 않고 다시 계산하는 메모리 절약 방식)이 신설됐을 때는 "H100에서도 학습이 돈다는 첫 코드 신호"라고만 관측했다.
    • 이번 커밋에는 SM90(H100)용 forward·backward 두 파일이 명시적으로 있으므로, H100 학습은 이제 연결이 완결됐다. SAVE_H100_RECSYS remat 정책이 실제로 무엇을 쓰는지가 코드로 확정됐다.
    • 4세대+6의 _GB300_OVERRIDES에 대한 SM90/H100 대체 경로도 명확해졌다. GB300은 Grace Blackwell 300, SM100 세대다.
    • X는 GB300과 H100 두 세대 하드웨어에 각각 최적화된 attention 커널을 유지한다.
  • X는 flash-attention 상류 코드를 재사용했다. CuTeDSL을 처음부터 새로 쓰기보다 Dao-AILab 원본을 fork해 수정하는 전략을 택했다. 이 선택은 attention 커널이 LLM 커뮤니티가 검증한 표준화된 인프라이며, X도 그 표준을 벗어나지 않았다는 신호다.

성능 수치와 실제 sparse pattern은 공개되지 않았다 확인.

  • SM90/SM80 커널의 실제 벤치마크(GB/s·TFLOPS·baseline 대비 speedup)는 오픈소스에 없다.
  • block_sparse_utils.py가 큰 폭으로 재작성됐지만, 어떤 sparse pattern이 실제로 사용되는지는 모델 config에 명시되지 않았다 확인.

F. 그 밖의 변경은 서빙 인프라 정리와 B~D를 받치는 연결이 중심이다

서빙·학습 인프라 정리, 위 B~D를 받치는 연결, 시맨틱 변화가 없는 작은 수정 등 나머지 변경을 한 줄씩 모았다.

  • CutlassCall custom_call의 device blob을 SHA256 digest로 바꿔 AOT 캐시 키를 안정화했다 확인 — phoenix/xrex/utils/aot.py (+54/−1).
    • _sanitize_cutlass_calls가 StableHLO의 stablehlo.custom_call{target=CuteDSLRT_NvJaxCutlassCall}을 순회한다. backend_config의 module 필드(ELF/fatbin 바이트열)를 SHA256 digest device_digest:<hex>로 대체한다.
    • cutlass_module_device_digest는 ELF 헤더를 파싱해 device blob 섹션만 hash한다.
    • AOT_CACHE_VERSION도 "20260705" → "20260904"로 갱신됐다.
    • 이로써 JAX AOT 캐시가 동일한 CUDA kernel binary에 대해 안정적인 cache key를 갖게 되어 재컴파일 지옥을 피한다. 랭킹 모델처럼 embedded kernel이 많은 경우 꼭 필요한 연결이다.
  • 서빙 로더(copy_port_client.rs)가 MANIFEST.json 존재를 확인해 번들이 완료된 체크포인트만 고른다 확인 — phoenix/crates/serving/xai-recsys-engine/src/copy_port_client.rs (+207/−9).
    • full_prefixes_newest_first가 채널 전체에 존재하는 prefix들을 최신 순으로 반환하는 헬퍼로 분리됐다.
    • select_bundle_checkpoint는 export/MANIFEST.json이 모든 채널에 non-empty로 존재하는 최신 checkpoint를 고른다. 아직 unbundled인 최신 checkpoint는 warn 로그와 함께 건너뛴다.
    • download_named_files가 새로 생겨 특정 파일 이름 리스트를 원자적으로 다운로드한다. 모든 파일이 존재하는 채널을 랜덤으로 고른다.
    • 4세대+13의 .mlirbc 번들 시스템(StableHLO export)이 서빙 로더까지 완전히 연결됐다. 번들이 완료된 checkpoint만 서빙에 실린다.
  • 서빙용 sequence_len을 학습 config에서 자동으로 계산한다 확인 — phoenix/xrex/train/recsys_bundle_export.py (+48).
    • _serving_sequence_len이 training config에서 prefix token 길이를 역산한다. 쓰는 값은 sequence_len·history_seq_len·candidate_seq_len·num_negatives_per_example·num_global_negatives_per_example이다.
    • serving 때는 serving_seq_len = prefix + history + candidate로 attention kernel 크기를 override한다.
    • num_user_prefix_tokens와 어긋나면 ValueError로 실패하며 닫힌다(fail close).
    • MANIFEST에도 model_sequence_len이 추가됐다.
  • quote_hydrator.rs가 크게 확장돼 인용 포스트의 media info도 hydration한다 확인 — home-mixer/candidate_hydrators/quote_hydrator.rs (+142/−12).
    • MediaInfoCacheClient가 새 dependency로 주입된다. get_quoted_content가 인용 포스트의 core data와 media info를 200ms timeout 안에서 병렬 조회하고, cache miss이면 fallback한다.
    • 4세대+17의 ContentFeatures.quotedContentFeatures 연결의 상류 조건이다.
  • 미디어 정보 조회(media_info_hydrator.rs +70/−15)를 리팩터했다 확인. content features 계산에 필요한 max_video_duration_ms 등을 여기서 hydration하는 경로가 넓어졌다.
  • PTOS 재분류 트리거가 넓어졌다 확인. task_safety_ptos_policy.py (+4/−1)의 policy detection이 leading-frames 경로의 재판정에도 대응한다.
    • task_rate_limit.py에 TaskRateLimitSafetyPtosAdultContentLeadingFrames가 추가됐다.
  • home-mixer/bin/pipeline_components_json.rs(76줄)가 파일 전체 삭제됐다 확인. 오프라인 debugging용 JSON dump 유틸이었던 것으로 보이나, 더 이상 유지할 수 없는 상태였을 것으로 추정한다 추정.
  • candidate-pipeline 8개 파일을 조금 고쳤다(+103/−38) 확인. Pipeline 코어의 signature를 정리했고, 별도 시맨틱 변화는 없다.
  • VF(visibility-filtering, 포스트마다 이 뷰어에게 보여도 되는지 판정하는 서비스)의 safety_label_source/proto.rs가 +34/−38 바뀌었다 확인. proto를 조금 확장했고 시맨틱 변화는 없다.
  • video_tools를 조금 고쳐 video hydration 정확성을 높였다 확인. 변경은 image.py +42/−19, subtitles.py +3/−1, video_frames.py +7/−7이다.
  • VF 클라이언트(visibility-filtering-client/vf_client.rs, +119/0)에 새 API가 추가됐다 확인. 시맨틱 확장인지 리팩터인지는 별도 확인이 필요하다.
  • 4세대+15에서 신설된 multi-step reply spam 파이프라인을 소소하게 정리했다 확인. grox/flows/reply_spam의 task_write.py에서 −17줄, task_multi_step_reply_spam.py에서 −9줄이 빠졌다.
  • grox/flows/mm_emb의 파이프라인 수정 5개는 B의 v8-search 스택을 마저 연결한다 확인.
    • task_write_mm_embedding_sink.py(+90)에 v8-search sink를 연결했다.
    • task_multimodal_post_embedding.py(+52)에 v8-search와 v8-search-backfill 태스크를 추가했다.
    • 나머지는 task_rate_limit.py(+15), task_embedding_pub.py(+5), constants.py(+9)다.

정본 반영 services/x.md의 "안전성 / visibility-filtering / labeling 파이프라인" 절 뒤에 "### 4세대+17 (2026-09-08, 49815da)" 서브섹션을 새로 추가했다. 한 줄 요약은 이미 4세대+14까지 다뤄 길어졌으므로, 4세대+15·+16 관행대로 늘리지 않고 서브섹션에만 반영했다. 최종 갱신은 2026-09-05 → 2026-09-10으로, git 로그 직접 확인 줄은 2026-09-04 → 2026-09-10으로 올렸다. 확인한 커밋은 15개 → 18개다(+85ac72a·+9b0dc31·+902a06f·+49815da 4개 진전).

의미 이 커밋으로 A/B·exploration 인프라가 한 층 더 쌓였고, 검색은 랭킹에서 더 분리됐으며, 콘텐츠 이해와 안전 판정의 방어 층이 늘었다. 이 커밋은 이 세 갈래로 읽힌다.

  1. A/B와 exploration 인프라가 여러 층으로 쌓이고 있다. 4세대+16의 codivert impression과 4세대+14의 backbone scores 요청에 이어, 이번 weight perturbation은 랭킹 스코어러 층에서 처음으로 실제 유저 트래픽에 stochasticity를 주입할 수 있는 통합된 계약이다.
    • Bandit 문헌의 Thompson sampling·bootstrap·Rademacher perturbation 어느 흐름과도 정확히 일치하지는 않는다. salt 기반의 결정론적 재현성과, md5로 얻는 유저-헤드 조합 격자 독립성이 결합된 형태다.
    • 프로덕션 기본값은 off이지만 연결 자체는 완결됐다.
  2. 검색과 랭킹의 분리가 깊어지고 있다. 검색 지면이 For You와 완전히 분리된 벡터 공간, 모델, backfill 파이프라인을 갖게 됐다.
    • 흐름은 Phoenix xrecsys_search(4세대+4) → search-query embeddings의 non-padding 연결(4세대+13) → 이번 v8-search 임베딩 파이프라인이다.
    • X 검색이 이전 세대보다 얼마나 두꺼워질지가 관전 포인트다.
  3. 콘텐츠 이해와 안전성의 계층이 올라가고 있다. 콘텐츠 판정과 랭커 입력에서 텍스트·비디오·시간·오버레이 회피에 대한 방어 layer가 각각 별도 경로로 늘어난다.
    • ContentFeatures: 랭킹 입력의 텍스트 구조 시그널
    • leading-frames 재스캔: 시간축 회피 대응
    • Special Video motion-reveal: 정적 오버레이 회피 대응(4세대+12)
    • PTOS cross-validator: 3 카테고리(4세대+7)
    • 성인 콘텐츠 cross-validator(4세대+2)

이번 커밋만으로는 알 수 없는 것도 남는다.

  • weight perturbation의 실제 프로덕션 σ 값, salt 배정 로직, 결과 관측 방법
  • v8-search 임베딩의 dim, base model, retrieval index 연결
  • ContentFeatures가 실제로 어느 랭킹 헤드에서 어떻게 사용되는지(다음 커밋 관측 대상)
  • FA4 SM80/SM90 커널의 벤치마크 수치(GB/s·TFLOPS)
  • leading-frames 경로의 실제 재분류율(precision·recall)
  • quote_hydrator.rs MediaInfoCacheClient 연결이 4세대+17에서 처음 무엇을 hydration하는지의 상세

검증에서 뒤집힌 것

뒤집힌 결론은 없다. 이 커밋은 스레드에 새 근거를 더했고, 기존 결론과 상충하지 않는다. 4세대+15의 "H100 학습이 돈다는 첫 코드 신호"가 이번 커밋의 FA4 SM90 forward·backward 원본으로 완전히 확정됐다는 점만 적어 둔다.

레이더

레이더에 새로 올린 항목은 없다. arXiv cs.IR 30건을 파싱했고, 수용 기준(플랫폼 기업 저자 또는 프로덕션 A/B 주장)을 통과한 것은 0건이다. 눈에 띈 후보는 다음과 같다.

  • SequenceO1: End-to-End Ultra-Long (100K) Sequence Modeling in Recommendation with Low-Rank Caching은 추천에서 100K 길이의 시퀀스를 모델링한다. 프로덕션 A/B가 없어 탈락했다.
  • EAGER: Enrich-and-Align Generative Query Recommendation from Clicked Items in E-commerce Search는 e-commerce 검색에서 query를 생성형으로 추천한다. 플랫폼 소속도, 프로덕션 결과도 없다.
  • Task-Blind No MORE: Multi-Task Information Flow in Unified Ranking Backbones는 통합 랭킹 backbone의 multi-task information flow를 다룬다. 학술적 증분이고, 탈락했다.
  • Exploring Bottom-Up Clustering for Creating Semantic IDs는 Semantic ID 생성 방법론이다. 이미 topics/semantic-ids.md에서 추적 중인 스레드에 붙일 근거는 없다.

다음에 볼 것