4.4 LLM 推薦システムを運用するための最適化

はじめに: 最適化の対象はモデルサイズだけではない

LLM 推薦システムの運用最適化では、単に「モデルを小さくする」ことが目的ではない。少なくとも次の四つを同時に満たす必要がある。

  • 推薦品質: 将来視聴される作品を上位へ残せること
  • 遅延: ホーム表示や対話で許容される時間内に応答できること
  • コスト: リクエスト数が増えても推論費用を管理できること
  • 正確性・鮮度: 配信終了作品や古い嗜好を返さないこと

蒸留は、一件の推論に使うモデル自体を小さくする。量子化は、同じモデルの数値表現を軽くする。キャッシュは、そもそも同じ計算を繰り返さないようにする。三つは削減する対象が異なるため、競合ではなく組み合わせられる。

技術 主に削減するもの 何を変更するか 品質リスク
知識蒸留 モデルの計算量・パラメータ数 大きな教師を模倣する小さな生徒を学習 生徒の表現能力不足、教師の誤りの継承
量子化 一パラメータ・activation のビット数 FP16 などを INT8 / INT4 などへ近似 丸め誤差、外れ値による誤差
キャッシュ 重複した推論回数 過去の出力・embedding・中間結果を再利用 古い結果、誤った cache hit

Hulu で考えるなら、既存の Two-Tower が候補を数十件へ絞り、圧縮済み LLM が再ランキングし、作品 embedding や静的な説明生成結果をキャッシュする構成が現実的である。全カタログを LLM に毎回読ませる構成を圧縮するより、先に不要な LLM 呼び出しと token を減らす方が効果の大きい場合も多い。

0. そもそも、なぜこれらの技術が必要なのか

0.1 出発点: LLM は推薦の必須部品ではない

最初に確認すべきことは、 推薦に LLM を使うこと自体が目的ではない という点である。暗黙的フィードバックから「次に視聴しそうな作品」を予測するだけなら、Two-Tower、行列分解、勾配ブースティング、通常の ranking model で十分な場合が多い。これらのモデルは series ID や視聴履歴を数値として扱うことが得意であり、一般に LLM より安く高速である。

一方、LLM を追加する動機は、従来モデルが扱いにくい 言葉の意味や複数条件 を扱いたい場合にある。例えば、次の request を考える。

最近『葬送のフリーレン』を見た。似た雰囲気だが、もう少しテンポが速く、家族と一話から見やすい作品を教えてほしい。

この request には、作品の類似性だけでなく、「雰囲気」「テンポ」「家族と見やすい」「一話から理解しやすい」という複数の自然言語条件が含まれる。LLM は、作品 synopsis やタグ、ユーザーの言葉を組み合わせて候補を比較したり、推薦理由を生成したりできる。この能力が、LLM を使う主な価値である。

従って、LLM を導入する判断は次の比較になる。

問い 従来モデルで十分な例 LLM の追加価値が出やすい例
何を返すか profile ごとの Top-20 作品 自然言語の細かな条件を満たす作品
入力 series ID、視聴有無、視聴時間 自由文、synopsis、レビュー風の意味情報
出力 series ID と score series ID、順位、条件ごとの説明
新作への対応 interaction が少ないと難しい metadata の意味を使って補助できる
制約の解釈 あらかじめ特徴量化した条件 複数の曖昧な条件の比較

LLM を入れても、未来視聴に対する NDCG、25% 視聴率、長尾 coverage などが改善せず、説明生成も不要なら、LLM を使わない方がよい。この節の最適化技術は、 LLM の追加価値を確認できた後で、その価値を現実的な費用と遅延で提供するための技術 である。

0.2 prototype では動いても、本番ではリクエスト数が掛け算になる

一人に一回だけ大型 LLM を呼ぶ実験では、数秒の待ち時間や数円の費用は問題に見えにくい。しかし、本番では一件当たりの費用と時間が request 数だけ繰り返される。

一日の LLM 推論回数を、単純化して次で表す。

NLLM=Nprofile×Nopen/profile×rLLMN_{\mathrm{LLM}} = N_{\mathrm{profile}} \times N_{\mathrm{open/profile}} \times r_{\mathrm{LLM}}

NprofileN_{\mathrm{profile}} は一日に訪れる profile 数、 Nopen/profileN_{\mathrm{open/profile}} は一 profile 当たりの対象画面表示回数、 rLLMr_{\mathrm{LLM}} はそのうち LLM を呼ぶ割合である。

手を動かす例: 一件の小さな費用を一日分へ広げる

説明用の仮定として、一日に 500,000 profile が対象画面を平均 4 回開き、毎回 LLM を呼ぶとする。

NLLM=500,000×4×1.0=2,000,000 requests/dayN_{\mathrm{LLM}} =500{,}000 \times4 \times1.0 =2{,}000{,}000 \ \mathrm{requests/day}

一回の推論費を仮に 0.003 ドルとすると、一日の費用は、

Cdaily=2,000,000×0.003=6,000 dollar/dayC_{\mathrm{daily}} =2{,}000{,}000 \times0.003 =6{,}000 \ \mathrm{dollar/day}

となる。これは説明用の仮想価格であり、実価格ではない。しかし、重要なのは、 一件当たりでは小さく見える値も、表示回数を掛けると運用上の大きな値になる という構造である。

遅延にも同じ問題がある。一回 800 ms の処理を一人で試すと許容できても、多数の request が同時に来れば GPU queue が発生し、後から来た request は 800 ms より長く待つ。従って、本番化では一件の速度だけでなく、peak QPS、同時実行数、p95 / p99 latency を考える必要がある。

0.3 三つの技術は、異なる種類の「無駄」を減らす

蒸留・量子化・キャッシュは用語が並ぶため分かりにくいが、次の三つの質問へ分けると理解しやすい。

  1. 毎回、そこまで賢い巨大モデルが必要なのか

    日常的な request の大部分が「候補 20 件を並べ替える」だけなら、大型教師の全能力は毎回必要ではないかもしれない。大型モデルの判断を小型モデルへ移して、一回の処理自体を軽くするのが 知識蒸留 である。

  2. 同じモデルの数値を、それほど細かく保持する必要があるのか

    学習済み重みを FP16 で保持しているが、推薦順位をほぼ変えず INT8 や INT4 で近似できるかもしれない。数値表現を粗くして、メモリ転送量と必要メモリを減らすのが 量子化 である。

  3. 同じ答えや途中計算を、もう一度計算する必要があるのか

    作品 synopsis の embedding、同一 session の候補、人気作品の説明などは、入力が変わらなければ再利用できる。同じ計算を飛ばすのが キャッシュ である。

技術 日常の比喩 省く対象 実行する時期
蒸留 専門家の判断方法を、担当者へ教える 大型モデルを毎回呼ぶこと 主に offline 学習時
量子化 必要精度を保ちながら、数値の桁数を減らす 重い数値表現とメモリ転送 主に model artifact 作成時
キャッシュ 前に計算した答えをメモして再利用する 同一計算の反復 主に online 推論時

ここで重要なのは、三つが代替関係ではないことである。蒸留・量子化したモデルでも、同じ入力を百万回処理すれば百万回分の計算が要る。キャッシュしても、cache miss の request にはモデルが必要である。そのため、三つを重ねて使える。

0.4 一つの Hulu リクエストを三段階で軽くする

ユーザーがホーム画面を開き、Two-Tower が候補を 20 件返し、大型 LLM が再ランキングする状況を考える。説明用に、元の大型 LLM 一回を 100 という計算コストで表す。

まず、蒸留した小型モデルが教師の品質を許容範囲で再現し、一回の相対コストが 30 になったとする。次に、量子化によって同じ生徒モデルの相対コストが 30 から 20 になったとする。さらに、同じ session 内で 60% の request が安全に cache hit するなら、平均計算コストは、

E[C]=0.6×0+0.4×20=8\mathbb{E}[C] =0.6\times0 +0.4\times20 =8

となる。元の 100 と比べると、平均では 8 まで下がる。この数値は三技術の関係を示すための仮定であり、実際の削減率ではない。

処理の流れは次のようになる。

同じ入力の結果があるか
   ├─ ある → cache から返す
   └─ ない
        ↓
大型教師の代わりに蒸留した生徒を使う
        ↓
生徒の重みは量子化済み表現で保持・計算する

この図から分かるように、cache は「モデルを呼ぶか」を決め、蒸留は「どの大きさのモデルを呼ぶか」を変え、量子化は「そのモデルをどの数値精度で動かすか」を変える。

0.5 学習時と推論時を分けて考える

難しさの原因の一つは、offline の処理と online の処理が混ざって説明されることである。

時期 行うこと 大型教師は必要か ユーザー request を待たせるか
蒸留データ作成 大型教師に候補を採点させる 必要 いいえ。事前に行う
生徒の学習 教師 score と実視聴から学ぶ 教師出力が必要 いいえ。事前に行う
量子化 学習済み生徒を INT8 / INT4 などへ変換する 通常は不要 いいえ。事前に行う
cache lookup 保存済み結果を探す 不要 はい。ただし非常に短い処理である
cache miss 推論 圧縮済み生徒で再ランキングする 通常は不要 はい
escalation 難例だけ大型モデルへ送る 必要 はい

すなわち、大型教師は原則として事前の「教材作り」に使い、全ユーザーの online request へ直接使わない構成を選べる。この区別が、蒸留の最大のモチベーションである。

0.6 最初から全技術を導入する必要はない

次の順に問題を切り分けるとよい。

  1. LLM を追加すると、従来 baseline より測定可能な価値が出るかを確認する

  2. LLM に渡す候補数と metadata を減らし、不要な token を削る

  3. 同じ計算が多いなら、安全な単位から cache する

  4. cache miss の費用や遅延がまだ大きいなら、小型モデルへの蒸留を検討する

  5. model memory、memory bandwidth、同時実行数が問題なら量子化を検討する

  6. それぞれの変更後に推薦品質と安全条件を再評価する

従って、量子化や蒸留は「LLM を使うなら必ず適用する決まり」ではない。何がボトルネックかを測り、その原因へ対応する技術だけを選ぶ。

0.7 この節で使う最小限の用語

用語 平易な意味
model parameter / weight 学習によって決まった大量の数値。モデルの判断規則を保持する
inference 学習済みモデルへ入力を渡して、score や文章を計算すること
teacher 高品質だが大きく高価なモデル
student 教師の振る舞いを学ぶ小さなモデル
logit softmax で確率にする前の、生の候補 score
soft target 正解一件だけでなく、全候補への教師の確率分布
activation request をモデルの各層で処理している途中に生じる一時的な数値
prefill prompt 全体を最初に読み込む計算
decode 出力 token を順番に生成する計算
cache hit 必要な結果が cache にあり、再計算を省けた状態
cache miss 結果がなく、モデルなどで計算する必要がある状態
TTL cache を最大でどれだけの時間使ってよいかという期限
invalidation TTL を待たず、変更イベントを受けて cache を無効にすること

1. 図 4.2 の意味

図4.2 蒸留と圧縮による展開可能なモデル作成プロセスの概要

図 4.2 は、おおむね次の処理を表している。

Foundation Model
       ↓ 教師として推論
第一の生徒モデルを蒸留
       ↓
不要な構造を pruning
       ↓
教師の指導を再度使って性能を回復
       ↓
必要なら quantization
       ↓
本番用の小型モデル

Teacher-Guidance の矢印は、圧縮後のモデルが単独で学ぶのではなく、元の高性能モデルの出力を参照して性能を回復することを示す。pruning 後に再蒸留するのは、枝を切ったモデルへ改めて教師の振る舞いを学ばせるためである。

ただし、この順序は必須ではない。実際には次のような構成もある。

  • 蒸留した生徒へ事後量子化だけを行う
  • 教師を用いず、元モデルを直接 INT4 へ量子化する
  • pruning を行わず、蒸留と量子化だけを組み合わせる
  • 量子化済み基盤モデルを QLoRA で適応する

どの処理を選ぶかは、モデル形式、GPU / CPU、推論 runtime、品質許容幅による。図を「常にこの全工程を行うレシピ」ではなく、「複数の圧縮手段を段階的に適用できる概念図」と読むべきである。

2. 最初に作るべきコスト・遅延モデル

最適化前に、どこで時間と費用を使っているかを分解する。LLM 一回の総遅延は、概念的に次のように表せる。

Ltotal=Lfeature+Lretrieval+Lqueue+Lprefill+Ldecode+LpostL_{\mathrm{total}} = L_{\mathrm{feature}} +L_{\mathrm{retrieval}} +L_{\mathrm{queue}} +L_{\mathrm{prefill}} +L_{\mathrm{decode}} +L_{\mathrm{post}}

ここで、feature はユーザー状態の取得、retrieval は候補検索、queue は GPU 待ち、prefill は入力 prompt の処理、decode は出力 token の生成、post は ID 検証や配信制約処理である。

手を動かす例: どこを最適化するべきか

一件のホーム推薦で次の時間が掛かるとする。

処理 時間
ユーザー特徴取得 15 ms
Two-Tower 候補取得 20 ms
GPU queue 35 ms
prompt prefill 100 ms
20 token の decode 80 ms
後処理 10 ms

合計は、

Ltotal=15+20+35+100+80+10=260 msL_{\mathrm{total}} =15+20+35+100+80+10 =260\ \mathrm{ms}

である。量子化で decode だけを 25% 短縮しても、短縮量は 80×0.25=2080\times0.25=20 ms であり、全体は 240 ms にしかならない。一方、候補 metadata を短くして prefill を 100 ms から 50 ms にできれば、全体は 210 ms になる。

この例から、モデルの FLOPs だけでなく、入力 token、queue、retrieval、後処理まで測る必要があると分かる。

費用も同様に分解する。

Cdaily=Nrequest[hChit+(1h)Cmiss]C_{\mathrm{daily}} = N_{\mathrm{request}} \left[ hC_{\mathrm{hit}} +(1-h)C_{\mathrm{miss}} \right]

NrequestN_{\mathrm{request}} は一日の request 数、 hh は cache hit rate である。cache hit の計算費用がほぼゼロなら、推論費用はおおむね cache miss 率に比例する。

3. 知識蒸留

3.0 なぜ蒸留が必要なのか

蒸留が必要になる典型的な状況は、 大型 LLM なら良い結果を出せることは分かったが、そのモデルを全 request で動かすのは遅い、または高い という状況である。

例えば、大型教師へ次の入力を渡すとする。

ユーザー履歴:
- 『葬送のフリーレン』を 8 話視聴
- 『SPY×FAMILY』を 2 話視聴

候補:
- C01 『薬屋のひとりごと』
- C02 『進撃の巨人』
- C03 『名探偵コナン』

指示:
候補だけを使い、次に長く視聴しそうな順へ並べよ。

大型教師が多数の作品知識と文章理解を使って、C01、C03、C02 の順を安定して返したとする。この大型教師を online で毎回呼ぶ代わりに、事前に多くの入力へ教師順位を付け、生徒へその規則性を学ばせる。online では生徒だけを動かす。これが蒸留である。

offline:
多数の履歴・候補 → 大型教師 → 教師 score を保存 → 小型生徒を学習

online:
現在の履歴・候補 → 小型生徒 → 推薦順位

ここで自然な疑問は、「最初から小型モデルを実視聴ラベルで学習すればよいのではないか」である。実際、それで十分なら蒸留は不要である。それでも教師を使う理由は、教師が次の補助情報を作れる可能性があるためである。

  • 正例一件だけでなく、候補間の細かな相対順位を教えられる
  • 実視聴がまだ少ない新作も、metadata の意味から比較できる
  • 「キッズ向け」「テンポが速い」など、ログだけでは直接表れない条件を評価できる
  • hard case を大量に採点し、生徒用の教材を増やせる

一方、教師の score が未来視聴を改善しない、あるいは既存 ranking model が同じ品質を出すなら、蒸留する価値はない。また、蒸留は大型モデルそのものを小さく切り縮める操作とは限らない。教師とは別に用意した小型モデルを、教師の出力へ近づける 学習方法 である。

難所の補足: SFT と蒸留は何が違うのか

両方とも生徒を学習するため混同しやすい。違いは、主に教師信号の中身である。

方法 教師信号 Hulu の例
通常の SFT 人手正解や実ログから作った hard target 未来に視聴された C01 を正解として学ぶ
response distillation 大型教師が生成した回答 教師が出した候補 ID 順や推薦理由を学ぶ
logit distillation 教師の全候補に対する score 分布 C01: 0.55、C02: 0.35、C03: 0.10 を学ぶ
blended training hard target と教師分布の両方 実視聴を尊重しつつ、教師の候補間関係も学ぶ

SFT は「最終的な正解」を主に教え、logit 蒸留は「不正解を含む各選択肢を教師がどう比較したか」まで教える、と捉えるとよい。

3.1 蒸留の直感

正解ラベルだけを見る通常の教師あり学習では、「このプロフィールには C01 が正解」としか分からない。教師モデルは、C01 を一位としながら、C02 もかなり適切、C03 は少し適切、C04 は不適切という相対関係を持っている場合がある。

蒸留では、この相対関係まで生徒へ教える。学校に例えると、答えだけを渡すのではなく、優秀な教師が各選択肢をどの程度もっともらしいと考えたかも伝える方法である。

推薦では教師を次のように使える。

  • 候補一件ごとの relevance score を出す
  • 二作品のどちらを上位にするか回答する
  • 候補リスト全体をランキングする
  • 推薦理由やユーザー嗜好要約を生成する
  • 難しい事例へ追加ラベルを付ける

生徒は必ずしも教師と同じ architecture である必要はない。ただし、入出力タスクは対応していなければならない。encoder である DistilBERT を、生成モデルの FLAN-T5-Small と名前だけで置き換えることはできない。ランキング用 classification head を付けるのか、候補 ID を生成するのかを先に定義する。

3.2 ロジット、確率、ソフトターゲット

候補 ii に対する教師のロジットを ziz_i とする。温度 TT を使った softmax は次である。

ロジットは確率ではなく、モデル内部の比較用 score である。そのため、負の値でもよく、合計が 1 である必要もない。例えば [2.0,1.0,0.2][2.0,1.0,0.2] は、第一候補を第二候補より好むことを表すが、「第一候補の確率が 200%」という意味ではない。softmax は、この任意の score を、各値が 0 以上で合計 1 の分布へ変換する操作である。

pi(T)=exp(zi/T)jexp(zj/T)p_i^{(T)} = \frac{ \exp(z_i/T) }{ \sum_j\exp(z_j/T) }

T=1T=1 は通常の softmax である。 T>1T>1 にすると分布が平らになり、上位以外の候補に対する教師の相対判断が見えやすくなる。 T<1T<1 は分布を鋭くするが、蒸留では通常、知識を軟らかく見せるため T>1T>1 を検討する。

手を動かす例: ロジットを温度で軟らかくする

候補を『進撃の巨人』『葬送のフリーレン』『名探偵コナン』とし、教師ロジットが次だとする。

z=[2.0, 1.0, 0.2]\mathbf{z} = [2.0,\ 1.0,\ 0.2]

T=1T=1 の指数はおよそ [7.389,2.718,1.221][7.389,2.718,1.221] 、合計は 11.32811.328 なので、

p(1)[0.652, 0.240, 0.108]\mathbf{p}^{(1)} \approx [0.652,\ 0.240,\ 0.108]

となる。教師は『進撃の巨人』を一位とするが、『葬送のフリーレン』も完全に不適切とは考えていない。

T=2T=2 ではロジットを 2 で割り、 [1.0,0.5,0.1][1.0,0.5,0.1] とする。指数はおよそ [2.718,1.649,1.105][2.718,1.649,1.105] 、合計は 5.4725.472 なので、

p(2)[0.497, 0.301, 0.202]\mathbf{p}^{(2)} \approx [0.497,\ 0.301,\ 0.202]

となる。正解一件だけの one-hot label [1,0,0][1,0,0] と異なり、教師が二位と三位をどう見ているかも生徒へ伝わる。これを dark knowledge と呼ぶことがある。

3.3 蒸留損失

教師分布を p(T)p^{(T)} 、生徒分布を q(T)q^{(T)} とすると、代表的な蒸留損失は KL divergence である。

LKD=T2DKL(p(T)q(T))\mathcal{L}_{\mathrm{KD}} = T^2 D_{\mathrm{KL}} \left( p^{(T)} \parallel q^{(T)} \right)
DKL(pq)=ipilogpiqiD_{\mathrm{KL}}(p\parallel q) = \sum_i p_i \log\frac{p_i}{q_i}

教師と生徒が同じ分布なら KL divergence は 0 である。生徒が教師の高確率候補へ低い確率を与えるほど大きくなる。 T2T^2 は温度によって小さくなった勾配を補正するために使われる。

手を動かす例: KL divergence を候補ごとに計算する

温度を適用した教師分布と生徒分布が、次だとする。

p=[0.497,0.301,0.202]p=[0.497, 0.301, 0.202]
q=[0.400,0.350,0.250]q=[0.400, 0.350, 0.250]

教師は『進撃の巨人』へ 0.497 を与えたが、生徒は 0.400 しか与えていない。一方、二位と三位には生徒の方が少し高い確率を与えている。候補ごとに式へ入れると、

DKL(pq)=0.497log0.4970.400+0.301log0.3010.350+0.202log0.2020.2500.1080.0450.0430.020\begin{aligned} D_{\mathrm{KL}}(p\parallel q) &= 0.497\log\frac{0.497}{0.400} +0.301\log\frac{0.301}{0.350} +0.202\log\frac{0.202}{0.250}\\ &\approx 0.108-0.045-0.043\\ &\approx0.020 \end{aligned}

となる。候補単位の項には負の値も現れるが、全候補を合計した KL divergence は 0 以上になる。値が約 0.020 と小さいのは、教師と生徒の分布が比較的近いためである。

T=2T=2 なら、蒸留 loss に入る T2DKLT^2D_{\mathrm{KL}} は、

22×0.020=0.0802^2\times0.020=0.080

となる。学習では、この値が小さくなる方向へ生徒の重みを更新する。つまり、生徒が教師と同じ候補へ同じ程度の確率を出すように近づける。

実務では、実際の視聴ラベルを使う hard loss と蒸留 loss を混ぜる。

L=λLhard+(1λ)T2DKL(p(T)q(T))\mathcal{L} = \lambda\mathcal{L}_{\mathrm{hard}} +(1-\lambda) T^2 D_{\mathrm{KL}} \left( p^{(T)} \parallel q^{(T)} \right)

手を動かす例: hard label と教師を混ぜる

hard loss が 0.700.70 、温度 T=2T=2 で KL divergence が 0.050.05λ=0.4\lambda=0.4 とする。

L=0.4×0.70+0.6×22×0.05\mathcal{L} = 0.4\times0.70 +0.6\times2^2\times0.05
L=0.28+0.12=0.40\mathcal{L} =0.28+0.12 =0.40

λ\lambda を大きくすると実視聴ラベルを重視し、小さくすると教師の振る舞いを重視する。教師が強力でも、過去方策の偏りや誤りを持つため、実ログを完全に捨てるべきとは限らない。

3.4 Hulu で蒸留データを作る

一つの listwise 再ランキング蒸留は次のように作れる。

  1. 時点 tt までの report.content_viewing と unique_viewed_series から履歴を作る

  2. recommend_engine.two_tower_u2i_recommendation_{genre} などから候補を 20〜50 件取得する

  3. item_information_table からタイトル、タグ、説明、配信制約を付ける

  4. 教師モデルに候補 ID のランキングまたは各候補 score を出させる

  5. 時点 tt より後に 25% 以上視聴された実ラベルも保存する

  6. 生徒を教師 score と実ラベルの両方で学習する

ここで、候補集合を固定して教師と生徒を比較することが重要である。教師だけ別候補を見ていれば、候補生成能力と再ランキング能力を分離できない。

3.5 教師のロジットを取得できない場合

教科書では教師ロジットを前提としているが、外部 API の大規模モデルは、全候補に対する生のロジットを提供しないことがある。その場合、次の代替がある。

  • 教師に各候補を 0〜100 で採点させ、正規化して soft label にする
  • pairwise 比較を複数回行い、勝率や Bradley-Terry score を作る
  • listwise ranking を生成させ、順位を graded label へ変換する
  • 推薦理由や嗜好要約を生成させ、response distillation を行う
  • ロジットを取得できる self-hosted teacher を使う

ただし、生成された 0〜100 点は真のロジットではなく、数値表現も calibration されているとは限らない。prompt や候補順を変えて安定性を確認し、複数回の平均を使うなどの対策が必要である。

3.6 ロジット標準化

教師と生徒でロジットのスケールが大きく異なる場合、同じ候補順位でも softmax の鋭さが異なる。ロジット標準化の一例は、各 request 内で平均を引き、標準偏差で割る方法である。

z^i=ziμzσz+ϵ\hat{z}_i = \frac{ z_i-\mu_z }{ \sigma_z+\epsilon }

教師ロジットが [20,10,2][20,10,2] 、生徒ロジットが [2,1,0.2][2,1,0.2] なら、順位関係は似ているがそのままの softmax は大きく異なる。標準化により、絶対スケールより候補間の相対構造を合わせやすくなる。

ただし、標準化が常に改善するわけではない。教師の確信度そのものが有用なら、標準化で失う可能性がある。validation set で通常の KD、温度調整、標準化を比較する。

3.7 蒸留で評価すべきもの

「教師精度の 5〜10% 以内」という値は保証ではない。タスク、生徒サイズ、データ、指標に依存する。例えば NDCG@10 が教師 0.400.40 、生徒 0.380.38 なら相対低下は、

0.400.380.40×100=5%\frac{0.40-0.38}{0.40} \times100 =5\%

である。一方、教師と生徒の schema-valid rate が 99.9% と 97% なら、本番エラーは約 30 倍に増える。平均 NDCG だけで「5% 以内」と判断してはならない。

最低限、次を比較する。

分類 指標
推薦 Recall@K、NDCG@K、coverage、多様性、新規性
LLM 出力 candidate-only rate、schema-valid rate、説明の根拠性
slice キッズ、コールドスタート、長期利用者、ニッチ作品
システム p50 / p95 / p99 latency、throughput、timeout
費用 teacher label 作成費、学習費、request 当たり推論費

3.8 蒸留後のドメイン適応

教科書の「事後蒸留ファインチューニング」は、教師の模倣だけで学習を終えず、最後に実際の推薦目的へ生徒を合わせ直す工程である。例えば、教師のランキングを模倣した生徒へ、Hulu の未来視聴ラベルを使って追加学習を行う。このとき、時点 tt までの履歴から予測し、時点 tt より後の 25% 視聴を正例にすることで、未来情報の漏洩を防ぐ。

具体的には、蒸留直後の生徒が『進撃の巨人』、『葬送のフリーレン』、『名探偵コナン』へ [0.50,0.30,0.20][0.50, 0.30, 0.20] の確率を与えたとする。しかし、同種 profile の実ログでは二番目の作品がより視聴されていたなら、hard label を使う追加学習で [0.42,0.40,0.18][0.42, 0.40, 0.18] などへ補正できる。すなわち、蒸留は教師の一般的な判断を移し、事後 fine-tuning は Hulu における視聴行動と配信面の目的へ最終調整する役割を持つ。

ただし、事後 fine-tuning を強くしすぎると教師から受け取った相対関係を忘れることがある。そのため、学習率を小さくする、蒸留 loss を一部残す、teacher agreement と未来視聴指標の両方を監視する、といった対策を取る。

3.9 蒸留の主な失敗

生徒の capacity が足りない

教師が多段の制約判断を行っても、生徒が小さすぎれば再現できない。まず教師と同じ候補上で、生徒サイズを段階的に比較する。

教師の誤りを大量複製する

教師が人気作品を過度に優先すれば、生徒も学ぶ。教師 label と未来視聴が衝突する hard example を監査し、hard loss を残す。

蒸留 corpus が本番分布と違う

映画だけで蒸留してアニメ・リアルタイム・キッズへ展開すると失敗しうる。本番 request の slice 比率を再現する。

教師生成費を回収できない

教師 API を数千万件呼ぶと、圧縮後の節約額より高くなることがある。代表的な input を選ぶ、uncertainty の高い例を優先する、教師結果を再利用する必要がある。

4. 量子化とその他のモデル圧縮

4.0 なぜ量子化が必要なのか

蒸留後も、生徒モデルは大量の重みを持つ。これらの重みは、推論のたびに GPU memory から計算器へ読み出される。そのため、モデルの数値表現が大きいと、主に次の問題が起きる。

  • model weight が GPU memory に収まらない
  • weight 以外の KV cache や activation を置く余地が少なくなる
  • 同じ GPU で同時に処理できる request 数が減る
  • memory から重みを運ぶ時間と電力が増える
  • 複数 GPU へ分割する場合、通信と運用が複雑になる

量子化のモチベーションは、 モデルが覚えているおおよその判断を保ったまま、各数値を表す bit 数を減らせないか というものである。

画像の色を例にすると、非常に細かな色を扱える形式から、使える色数の少ない形式へ変換する操作に似ている。元と完全に同じではないが、人間が見てほぼ同じ画像に見える場合がある。量子化では、モデルの各重みを近い離散値へ丸め、推薦順位が許容範囲に残るかを検証する。

手を動かす例: なぜ model memory が online capacity に効くのか

7 billion parameter のモデルを FP16 で保持すると、重みだけで約 14 GB を使う。説明を単純化し、24 GB の GPU 一台へ載せるとする。重みを載せた後の残りは、

2414=10 GB24-14=10\ \mathrm{GB}

である。一方、同じ重みを理論上 INT4 の 3.5 GB にできれば、残りは、

243.5=20.5 GB24-3.5=20.5\ \mathrm{GB}

となる。残り領域には request ごとの activation や KV cache、runtime workspace などを置く。従って、重みが小さくなれば、同時 request 数や長い prompt のために使える余地が増える可能性がある。

ただし、この引き算は動機を理解するための理論値である。実際には量子化 scale、未量子化層、allocator の予約領域などがあるため、20.5 GB をすべて request に使えるわけではない。また、メモリが減っても、対応 kernel がなければ高速化しない場合がある。

難所の補足: 蒸留したのに、なぜさらに量子化するのか

蒸留と量子化は違う軸を変えるからである。

  • 蒸留: 例えば 70B の教師の振る舞いを 7B の生徒へ移す
  • 量子化: その 7B の生徒の一重みを 16 bit から 8 bit や 4 bit へ変える

つまり、蒸留は主に 重みの個数を減らす 方策であり、量子化は主に 一個の重みを表す bit 数を減らす 方策である。7B FP16 の生徒を 7B INT4 にできれば、二段階の削減を重ねられる。

どの圧縮技術を選ぶか

観測された問題 最初に検討するもの 理由
大型教師の推論そのものが高い 蒸留 task に必要な能力を小型生徒へ移す
model weight が memory に収まらない 量子化 一重み当たりの bit 数を減らす
dense model の層や channel が過剰 構造 pruning 実際の行列サイズを小さくする
特定の大行列が低ランクで近似できる 低ランク因子分解 二つの小さな行列へ置き換える
大型量子化モデルを少ない学習 memory で適応したい QLoRA 量子化した基盤へ低ランク差分を学ぶ

これらをすべて適用すればよいわけではない。各変更は誤差と実装複雑性を増やすため、ボトルネックに対応する最小構成から始める。

4.1 量子化は何をしているのか

ニューラルネットワークの重みは通常、FP32、BF16、FP16 などの浮動小数点で保存・計算される。量子化は、多数の連続値を少数の離散値へ近似する。

対称 INT8 量子化の単純な例では、実数の最大絶対値を aa として scale を作る。

s=a127s = \frac{a}{127}

実数 xx を整数 qq へ写す。

q=clip(round(x/s),127,127)q = \operatorname{clip} \left( \operatorname{round}(x/s), -127, 127 \right)

推論時に近似実数へ戻すなら、

x^=sq\hat{x} =sq

である。量子化は値を完全に保存する圧縮ではなく、丸め誤差を許す非可逆近似である。

手を動かす例: 四つの重みを INT8 にする

重みを、

w=[1.2, 0.3, 0.2, 1.0]\mathbf{w} = [-1.2,\ -0.3,\ 0.2,\ 1.0]

とする。最大絶対値は 1.21.2 なので、

s=1.21270.009449s = \frac{1.2}{127} \approx0.009449

である。各値を scale で割って丸める。

q=[127, 32, 21, 106]\mathbf{q} = [-127,\ -32,\ 21,\ 106]

逆量子化すると、

w^=sq[1.200, 0.302, 0.198, 1.002]\hat{\mathbf{w}} = s\mathbf{q} \approx [-1.200,\ -0.302,\ 0.198,\ 1.002]

となる。元の 0.3-0.3 は約 0.302-0.3020.20.2 は約 0.1980.198 になった。この小さな誤差が多数の層で積み重なっても推薦順位を維持できるかを評価する。

4.2 raw weight memory の計算

パラメータ数を PP 、一パラメータの bit 数を bb とすると、重みだけの理論メモリは、

Mweight=Pb8 bytesM_{\mathrm{weight}} = \frac{Pb}{8} \ \mathrm{bytes}

7 billion parameter のモデルを考える。

形式 一パラメータ raw weight memory
FP32 4 byte 約 28 GB
FP16 / BF16 2 byte 約 14 GB
INT8 1 byte 約 7 GB
INT4 0.5 byte 約 3.5 GB

これは重みだけの概算である。実際には scale、zero point、未量子化層、runtime workspace、activation、KV cache が加わる。従って INT4 なら必ず 3.5 GB だけで動くわけではない。

難所の補足: weight、activation、KV cache は別物である

  • weight は学習済みパラメータであり、request 間で共有される
  • activation は各層の途中計算であり、request ごとに生じる
  • KV cache は autoregressive decoding で過去 token の key / value を再計算しないために保持する

weight-only INT4 は重みを 4 bit にしても、activation や KV cache は BF16 / FP16 のままの場合がある。長い prompt や大きな batch では、weight より KV cache がボトルネックになることもある。モデルファイルの大きさだけで、最大同時 request 数を判断してはならない。

4.3 量子化方式

事後量子化

Post-Training Quantization は学習済みモデルへ後から量子化を適用する。追加学習が少なく、最初の baseline に向く。代表的には calibration data を通して activation 範囲を測り、scale を決める。

Hulu では calibration data に、映画だけでなくアニメ、キッズ、TVOD、短い履歴、長い履歴、候補数の違いを含める。平均的 prompt だけで scale を決めると、長い作品説明や特殊 token で外れ値が出る可能性がある。

量子化を考慮した学習

Quantization-Aware Training は、学習中に量子化・逆量子化の誤差を模擬する。モデルは丸められることを前提に重みを調整できるため、低 bit で PTQ より品質を保てる場合がある。一方、学習コストと実装の複雑さが増える。

動的量子化

動的量子化は、重みを事前に量子化し、activation の scale を実行時に求める方式などを指す。入力分布へ適応しやすい一方、request ごとの量子化処理が追加される。library により用語と対象 tensor が異なるため、weight と activation のどちらを、いつ量子化するかを具体的に確認する。

4.4 INT8 / INT4 が必ず高速とは限らない

量子化による速度向上には、対応する低精度 kernel と hardware が必要である。INT4 weight を読み込んでも、計算前に FP16 へ展開する runtime では、メモリ帯域は減っても演算速度が期待ほど上がらないことがある。

速度は次に依存する。

  • GPU / CPU の低精度演算対応
  • GEMM kernel の実装
  • batch size と sequence length
  • prefill と decode のどちらが支配的か
  • tensor parallel による通信
  • dequantization overhead

従って、モデルサイズ、tokens per second、request per second、p95 latency、消費電力を実 hardware で benchmark する。ファイルサイズだけの比較では不十分である。

4.5 pruning

pruning は重要度の低い重み、channel、attention head、layer などを除く。

非構造 pruning

個々の重みを 0 にする。sparsity は高くできるが、通常の dense kernel では 0 も計算するため、ファイルが疎になっても速くならない場合がある。sparse kernel と hardware support が必要である。

構造 pruning

channel、head、layer の単位で除く。通常の dense 行列自体が小さくなるため、実速度へつながりやすい。一方、重要な構造をまとめて消すため、品質低下が大きくなることがある。

図 4.2 の再蒸留は、pruning 後に失った教師の振る舞いを取り戻す工程として理解できる。

4.6 低ランク因子分解

大きな行列 WRm×n\mathbf{W}\in\mathbb{R}^{m\times n} を、

WUV,URm×r,VRr×n\mathbf{W} \approx \mathbf{U}\mathbf{V}, \qquad \mathbf{U}\in\mathbb{R}^{m\times r}, \quad \mathbf{V}\in\mathbb{R}^{r\times n}

と近似する。元は mnmn パラメータだが、近似後は r(m+n)r(m+n) である。

m=n=4096m=n=4096r=256r=256 なら、元は、

40962=16,777,2164096^2 =16{,}777{,}216

パラメータ、分解後は、

256(4096+4096)=2,097,152256(4096+4096) =2{,}097{,}152

であり、約 8 分の 1 になる。ただし、行列が実際に低ランクで近似可能か、二回の行列積が runtime で速いかを確認する必要がある。

LoRA は低ランク差分を追加して適応する手法であり、元の大行列自体を必ず圧縮するわけではない。低ランク因子分解による model compression と、LoRA による parameter-efficient fine-tuning を区別する。

4.7 QLoRA の位置づけ

QLoRA は、量子化した基盤 weight を固定し、その上に LoRA adapter を学習することで、fine-tuning 時のメモリを削減する考え方である。

重要なのは次である。

  • 主な目的は fine-tuning memory の削減である
  • LoRA parameter 自体は通常、より高い精度で学習する
  • 学習後の serving 形式は、adapter を別読み込みするか、merge 後に再量子化するかで異なる
  • QLoRA を使っただけで、本番推論が自動的に最速になるわけではない

学習技術と serving 最適化を別々に benchmark する。

4.8 量子化後の推薦評価

language model の perplexity だけではなく、推薦出力で測る。

例えば量子化前後で同じ候補 20 件を与え、Top-5 の集合一致率を測る。

Overlap@5=Rbase@5Rquant@55\operatorname{Overlap@5} = \frac{ |R_{\mathrm{base}}@5 \cap R_{\mathrm{quant}}@5| }{ 5 }

元モデルが A、B、C、D、E、量子化モデルが A、B、C、F、G の順なら、

Overlap@5=35=0.6\operatorname{Overlap@5} = \frac{3}{5} =0.6

である。ただし、元モデルと違うことが即座に悪いわけではない。未来視聴に対する NDCG、候補外 ID、説明の根拠性も比較する。

量子化誤差は score が接近した候補の順位を入れ替えやすい。人気上位だけでなく、長尾作品やコールドスタート slice を見る。

5. キャッシュと応答再利用

5.0 なぜキャッシュが必要なのか

モデルをどれだけ小さくしても、同じ入力を繰り返し処理すれば、同じ費用を繰り返し支払う。キャッシュの出発点は、 入力と、その結果に影響する状態が同じなら、前の計算を再利用できる という考えである。

Hulu では、すべてが request ごとに変化するわけではない。

  • 同じ series の synopsis は、多数の profile が参照しても同一である
  • item embedding は、metadata や embedding model が変わるまで同一である
  • 日次生成した Two-Tower 候補は、次の生成まで同一の場合がある
  • 同じ session で短時間にホームへ戻った場合、長期嗜好はほぼ同じである
  • 人気作品トレイは、全 profile で共通にできる場合がある

これらを毎回 BigQuery、feature store、embedding model、LLM から作り直す必要はない。安全に再利用できる中間結果を保存すれば、LLM 以外の計算も含めて省ける。

手を動かす例: 同じ session でホームを四回開く

ある profile が 10 分間にホーム画面を四回開いたとする。最初の表示後に、新しい視聴、検索、作品タップ、カタログ更新がなかったと仮定する。

表示 状態 処理
1 回目 cache に結果なし 候補取得と rerank を実行して保存
2 回目 profile / session / catalog version が同じ cache hit
3 回目 同じ cache hit
4 回目 同じ cache hit

一回の rerank を 250 ms とし、cache hit を 5 ms とすると、cache なしの合計は、

4×250=1,000 ms4\times250=1{,}000\ \mathrm{ms}

である。三回を再利用できれば、合計は、

250+3×5=265 ms250+3\times5=265\ \mathrm{ms}

になる。四画面が合計 265 ms 待つという意味ではなく、四 request でサーバーが費やす処理時間を単純合計した説明用の値である。

しかし、2 回目の前に『薬屋のひとりごと』を視聴し始めたなら、session state が変わる。古い結果をそのまま返すと直近行動を反映できない。そのため、キャッシュでは「保存すること」よりも、 いつ同じとみなし、いつ捨てるか が難しい。

難所の補足: cache は一種類ではない

「推薦を cache する」と聞くと、最終 Top-K 全体を保存するように見える。しかし、推薦 pipeline では途中の部品も保存できる。

metadata → item embedding → user embedding → 候補検索 → rerank → 最終説明
    ↑             ↑                ↑            ↑          ↑
  cache         cache            cache        cache      cache

左側の metadata や item embedding は多くの profile で共有でき、更新も比較的少ない。そのため hit しやすい。一方、右側の最終説明は profile と session に強く依存し、共有範囲が狭い。従って、まず左側の安定した部品から cache し、必要に応じて右側へ広げると理解しやすい。

5.1 キャッシュの直感

蒸留と量子化は、cache miss が起きて推論を行うときの費用を下げる。キャッシュは、同じ結果を再利用して推論そのものを避ける。

しかし、推薦はユーザー、時刻、端末、配信カタログ、直近視聴によって変わる。FAQ と違い、文字列が同じ「おすすめを教えて」でも返すべき結果は profile ごとに異なる。そのため、何を同一 request とみなすかが最難関である。

5.2 推薦パイプラインの cache level

cache level hit しやすさ 鮮度リスク
item metadata description、タグ、配信判定用 snapshot 高い カタログ更新時
item embedding series ID ごとの text / collaborative vector 高い embedding model 更新、新作追加
user long-term embedding profile ID ごとの長期嗜好 中程度 新規視聴後
retrieval result user / context ごとの候補 ID 中程度 嗜好・在庫・model 更新
reranking score user、候補集合、prompt ごとの score 低〜中 候補・履歴変更
final response 最終 Top-K と説明 個人化が強いほど低い ほぼすべての変更
shared prompt prefix 固定指示の KV / provider prefix cache 高い prompt / model 更新

個人化の強い最終応答より、全ユーザーで共有できる item embedding や固定 prompt prefix の方が安全で hit しやすい。まず下位 level の cache から導入するのがよい。

教科書の「注意マップを保存する」は一般 request 間でそのまま再利用できるとは限らない。LLM では、同じ prefix に対する KV cache や provider の prompt caching がより一般的な再利用単位である。異なる user history を含む部分まで共有してはならない。

5.3 cache key

完全応答を安全に再利用する cache key には、出力へ影響する入力を含める。

cache_key =
  hash(
    profile_state_version,
    session_state_version,
    candidate_set_hash,
    surface,
    device_class,
    time_bucket,
    is_kids,
    service_type,
    catalog_snapshot,
    model_version,
    quantization_version,
    prompt_version,
    postprocess_rule_version
  )

候補集合を含めずに同じ prompt 文字列だけを key にすると、配信終了後も古い作品を返しうる。model version を含めないと、新モデルの評価中に旧出力が混ざる。profile ID を直接 key として外部へ露出させず、アクセス制御された内部 ID や keyed hash を使う。

手を動かす例: key が粗すぎると何が起こるか

key を「SF おすすめ」の query 文字列だけにしたとする。profile A は『インターステラー』を長時間視聴済み、profile B は未視聴である。A の応答を B が cache hit すると、A の既視聴除外や嗜好が B に流用される。

さらに、キッズ profile と一般 profile を同じ key にすれば安全性問題になる。query fingerprint だけでなく、出力を変える context と profile scope が必要である。

5.4 exact cache と semantic cache

exact cache は入力 fingerprint が一致した場合だけ再利用する。安全性と再現性が高い。

semantic cache は「泣けるアニメ」と「感動するアニメ」の embedding が近ければ同じ結果を返す。hit rate は上がるが、次の危険がある。

  • 「ホラー以外」の否定条件を落とす
  • キッズ条件や TVOD 除外を意味類似度だけで混ぜる
  • 別 profile の個人情報を含む応答を再利用する
  • 類似度閾値付近で予測不能になる

semantic cache は、共通の非個人化 query、候補 retrieval の補助などから限定的に使う。個人化された最終説明を profile 間で共有しない。

5.5 TTL と invalidation

TTL は「最大で何時間古くてよいか」であり、invalidation は「このイベントが起きたら期限前でも捨てる」である。

cache TTL の例 主な invalidation
item text embedding 数日〜長期 embedding model 更新、metadata 更新
人気作品リスト 1 時間〜1 日 集計更新、緊急ブラックリスト
long-term user embedding 1 日 新規視聴、お気に入り変更、profile 削除
session embedding 数分〜session 内 新しいタップ・視聴
final recommendation 数分 視聴、候補更新、配信状態変更

具体的 TTL は更新頻度と許容鮮度から決める。日次更新の Two-Tower 候補なら、次回 pipeline 完了時に version を切り替えて古い cache を無効化できる。publish_end_at や is_blacklist のような制約は、cache 内容を返す直前にも再検証する。

5.6 hit rate、期待遅延、費用

cache hit の遅延を LhL_h 、miss の遅延を LmL_m 、hit rate を hh とすると、平均遅延は、

E[L]=hLh+(1h)Lm\mathbb{E}[L] = hL_h +(1-h)L_m

手を動かす例: hit rate 70%

Lh=5L_h=5 ms、 Lm=250L_m=250 ms、 h=0.7h=0.7 とする。

E[L]=0.7×5+0.3×250\mathbb{E}[L] =0.7\times5 +0.3\times250
E[L]=3.5+75=78.5 ms\mathbb{E}[L] =3.5+75 =78.5\ \mathrm{ms}

平均は大きく改善する。しかし miss が 30% あるため、p95 は miss path に入りやすく、約 250 ms のままかもしれない。平均値だけでなく tail latency を見る理由である。

miss 一件の推論費を 0.004 ドル、hit の費用を無視できるとする。一日 1,000,000 request なら、cache なしは、

1,000,000×0.004=4,000 dollar/day1{,}000{,}000\times0.004 =4{,}000\ \mathrm{dollar/day}

hit rate 70% では、

1,000,000×0.3×0.004=1,200 dollar/day1{,}000{,}000 \times0.3 \times0.004 =1{,}200\ \mathrm{dollar/day}

となり、仮想的な差額は一日 2,800 ドルである。実際には cache storage、network、運用費、provider の価格体系も含める。

5.7 stampede と stale-while-revalidate

人気トレイの cache が期限切れになった瞬間、数千 request が同時に同じ LLM 推論を始める現象を cache stampede と呼ぶ。

対策は次である。

  • singleflight: 同じ key の再計算を一 request だけにする
  • TTL jitter: 全 key が同時に失効しないよう期限を少しずらす
  • stale-while-revalidate: 古い値を短時間返しつつ背景更新する
  • refresh-ahead: 期限前に人気 key を更新する

ただし、ブラックリストやキッズ安全条件に stale 値を使ってはならない。推薦順位は少し古くても許せる場合があるが、安全・配信制約は常に最新判定を通す。

5.8 cache の観測指標

  • hit rate と miss rate
  • cache level 別 hit rate
  • stale response rate
  • invalidation から反映までの時間
  • cache hit / miss 別 p50・p95・p99
  • key cardinality と storage
  • eviction rate
  • profile 間誤共有件数
  • model / prompt version 混在率

hit rate を上げるために key を粗くすると誤 hit が増える。hit rate は単独で最大化せず、正しさと鮮度を guardrail にする。

6. 蒸留・量子化・キャッシュの組み合わせ

実用的な request routing は次のようにできる。

request
   ↓
最新の配信・安全制約を取得
   ↓
exact cache lookup
   ├─ hit → 制約を再検証して返す
   └─ miss
        ↓
既存 retrieval で候補を絞る
        ↓
量子化済み蒸留モデルで rerank
        ├─ 高信頼・通常ケース → 返す
        └─ 低信頼・複雑ケース → 大型教師へ escalation
        ↓
安全な範囲を cache

この構成では、cache hit が最も安い。miss では小型モデルを使い、難しい request だけ大型モデルへ送る。

難所の補足: escalation の信頼度

小型モデルの最大確率が高いだけでは、本当に正しいとは限らない。量子化後に過信する場合もある。次を組み合わせて難例を判定する。

  • 上位一位と二位の score margin が小さい
  • 候補順を変えると結果が大きく変わる
  • schema retry が発生した
  • metadata が欠損している
  • 明示条件が多い、または矛盾する
  • 過去の slice 評価で小型モデルが弱い領域である

escalation rate が 5% なら、大型モデルの品質を一部利用しながら 95% を小型モデルで処理できる。ただし、router 自体の false negative、すなわち難例を簡単と誤判定する問題も評価する。

7. Hulu 向けの具体的な配置

7.1 オフライン

  1. item_information_table から配信可能作品 snapshot と metadata を作る

  2. report.content_viewing、unique_viewed_series から profile 履歴を作る

  3. recommend_engine の Two-Tower 候補を教師・生徒へ共通入力する

  4. 大型教師で ranking score、説明、棄権判断を生成する

  5. 実際の未来視聴と教師 label を混ぜて生徒を学習する

  6. 生徒へ PTQ を適用し、必要なら QAT または再蒸留する

  7. profile / item / slice 別に品質を検証する

7.2 オンライン

profile_id、surface、device、session
              ↓
long-term / session embedding cache
              ↓
日次 Two-Tower 候補
              ↓
現在の publish_start_at / publish_end_at、
service_type、is_blacklist、kids 条件で filter
              ↓
上位 20〜50 件を圧縮 LLM が rerank
              ↓
候補 ID 検証・重複排除
              ↓
短い TTL で profile-scoped cache

recommend_engine.mer_u2i_recommend_accumulative は profile ごとの最新 u2i データ、two_tower_i2i_recommendation は series ごとの i2i データ、genre 別 u2i table は候補生成に使える。LLM は全作品生成ではなく、これらが返す候補を再ランキングする。

TVOD では SVOD と候補条件・目的が異なる。tvod_recommend の候補、課金意図、service type を cache key と評価 slice で分ける。

7.3 cache 対象の優先順位

Hulu で最初に cache しやすいのは次である。

  1. series ID ごとの item embedding

  2. 固定 system prompt の prefix

  3. profile ごとの long-term embedding

  4. 日次 Two-Tower candidate list

  5. 同一 session 内の reranking result

個人化された最終説明を全 profile 間で semantic cache するのは後回しにする。安全性とプライバシーの検証負担が大きいためである。

8. 設計トレードオフ

8.1 コスト・品質・遅延

三者は一つの数値へ無理にまとめず、制約付き最適化として考える。

例えば、

  • NDCG@10 は baseline の 98% 以上
  • schema-valid rate は 99.9% 以上
  • p95 は 300 ms 以下
  • request 当たり費用は 0.001 ドル以下

という guardrail を置き、その範囲で最安の構成を選ぶ。平均品質が高くても p95 が遅すぎればホーム表示には使えない。費用が安くてもキッズ slice の品質が落ちれば採用できない。

8.2 fine-tuning と RAG

「安定ドメインなら fine-tuning、動的なら RAG」という二択は単純化しすぎである。

  • 振る舞い: 候補 ID だけを返す、短時間視聴を負例と断定しない → SFT
  • 安定概念: 群像劇、見逃し配信などの用語 → DAPT / SFT
  • 変化する事実: 本日の配信可否、新作、ブラックリスト → RAG / database
  • 個人状態: 直近視聴、session embedding → request context

映画推薦でも配信在庫は毎日変わるため、fine-tuning と RAG を併用する。

8.3 self-hosted model と API

API は prototype が速く、provider が scaling を担う。一方、token 単価、rate limit、data governance、モデル変更の影響がある。

self-hosted は weight、quantization、batching、hardware を制御できるが、GPU 稼働率、障害対応、model serving、security、更新を自社で担う。

単純な「本番規模なら自作が安い」は保証されない。損益分岐は次で変わる。

  • 一日の token 数と peak QPS
  • 平均 prompt / output 長
  • cache hit rate
  • GPU 価格と稼働率
  • 可用性のための冗長化
  • platform engineering の人件費
  • teacher API を一部残す escalation rate

同じ traffic trace を API 価格モデルと self-hosted benchmark の両方へ当てて比較する。

8.4 prompt でコンポーネントを統合する際の注意

候補検索、ランキング、説明を一 prompt に統合すると、実装は一見単純になる。しかし、次の性質を失うことがある。

  • 各段階の Recall / NDCG を個別に診断できない
  • 一部だけ cache しにくい
  • 全候補を prompt に入れて token cost が増える
  • 配信制約を確率的生成へ任せてしまう
  • 一段の失敗が全体を壊す

特に「性能に必須でなければ vector search を使わない」という判断は、カタログ規模と候補 Recall で決める。LLM が候補を自由生成できるから retrieval が不要、とはならない。候補にない作品を正確な series ID で、現在の配信状態まで含めて生成することは保証できない。

8.5 software package の位置づけ

TensorFlow Lite、PyTorch、Hugging Face 系 library、ONNX Runtime は手段であり、方式そのものではない。選定時には次を確認する。

  • 対象 architecture を support するか
  • 使用 hardware の INT8 / INT4 kernel があるか
  • weight-only か activation も量子化するか
  • KV cache の精度を制御できるか
  • dynamic shape、batching、長い context に対応するか
  • model export 前後で出力 parity を検証できるか

library 名だけで速度を予測せず、同じ model、同じ prompt 長、同じ hardware で benchmark する。

9. 実験計画

次の順なら、各最適化の効果を分離しやすい。

段階 0: baseline

  • 教師または現在モデルの品質、latency、cost、memory を測る
  • prompt token、output token、候補数を記録する
  • 時系列 holdout と slice を固定する

段階 1: モデルを変えない最適化

  • 候補 prefilter
  • prompt 短縮
  • batching
  • item embedding / prompt prefix cache
  • 不要な説明 token の削減

段階 2: 蒸留

  • 教師と生徒へ同じ候補を入力する
  • hard label と teacher label の混合率を変える
  • 生徒サイズごとに品質・速度曲線を作る

段階 3: 量子化

  • 生徒の FP16、INT8、INT4 を同一 hardware で比較する
  • calibration corpus の slice を変える
  • PTQ で不足する場合だけ QAT や再蒸留を試す

段階 4: routing と cache

  • exact cache から始める
  • TTL と invalidation を fault test する
  • cache miss 時は圧縮モデル、難例だけ教師へ送る
  • stampede と provider outage を負荷試験する

段階 5: A/B test

  • 再生開始率だけでなく 25% 視聴、総視聴時間、短時間離脱を見る
  • long-tail coverage とキッズ slice を guardrail にする
  • cache hit / miss、small / teacher route ごとに効果を分解する

10. よくある誤解

「蒸留すれば教師の能力をほぼそのまま持てる」

生徒の容量と蒸留 data coverage に依存する。教師の複雑な制約判断や未知 slice は失われやすい。

「教師 API の回答文があればロジット蒸留できる」

回答文は response distillation に使えるが、全候補ロジットとは異なる。API が log probability を返す場合も、候補全体の score と同じとは限らない。

「INT4 は FP16 の 4 倍速い」

raw weight memory は約 4 分の 1 になるが、速度は kernel、hardware、KV cache、dequantization に依存する。

「量子化後の文章が自然なら推薦品質も保たれた」

候補順位、valid ID、未来視聴に対する NDCG を測る必要がある。自然な理由文だけでは分からない。

「cache hit rate は高いほどよい」

粗い key で別 profile や古い catalog を誤 hit すれば危険である。correct hit rate と stale rate を見る。

「TTL を設定すれば invalidation は不要」

視聴直後、配信終了、ブラックリスト変更は TTL 前でも反映する必要がある。

「最終応答を cache すれば十分である」

個人化最終応答は再利用範囲が狭い。item embedding、固定 prefix、候補 list などの部品 cache の方が hit しやすい。

「圧縮モデルだけで障害対策になる」

model server 障害、cache 障害、retrieval 障害は別である。既存 Two-Tower 順位や人気トレイへの fallback を用意する。

11. 本番チェックリスト

品質

  • 教師・生徒・量子化モデルを同じ候補集合で比較したか
  • NDCG だけでなく candidate-only、schema、grounding を測ったか
  • キッズ、長尾、新作、短い履歴を slice 評価したか
  • teacher label と未来視聴が衝突する例を監査したか

latency・capacity

  • p50 / p95 / p99 を分けたか
  • prefill と decode を分けたか
  • peak QPS、batch size、queue time を負荷試験したか
  • KV cache を含む最大同時 request memory を測ったか

cache

  • key に model、prompt、candidate、catalog、profile state version があるか
  • 視聴・配信状態・安全条件の invalidation があるか
  • stampede 対策があるか
  • profile 間の cache isolation を test したか

運用

  • 圧縮 artifact と calibration data を version 管理したか
  • model / prompt 更新時の cache migration を決めたか
  • timeout 時の非 LLM fallback があるか
  • request 当たり cost と一日総額を監視しているか

12. まとめ

蒸留、量子化、キャッシュは、同じ「モデル圧縮」の別名ではない。

  1. LLM は推薦の必須部品ではない。従来モデルを上回る測定可能な価値がある場合に、その価値を本番の request 数へ拡大するため最適化する。

  2. 蒸留は、教師の相対的な候補判断を小さな生徒へ移す。実視聴の hard label と教師の soft label を組み合わせる。

  3. 教師ロジットを取得できない API では、score、pairwise、listwise、response distillation を使うが、真のロジットと同一視しない。

  4. 量子化は重みや activation を低 bit へ近似する。raw memory は計算しやすいが、実速度は hardware と kernel で測る。

  5. pruning、低ランク因子分解、LoRA、QLoRA は目的が異なる。特に LoRA は適応 parameter の削減であり、元モデルを自動的に圧縮するわけではない。

  6. cache は推論回数を減らす。個人化された最終応答より、item embedding、固定 prefix、long-term embedding、候補 list から始める方が安全である。

  7. cache key には profile / session state、候補集合、catalog、model、prompt、rule version を含め、TTL だけでなくイベント invalidation を行う。

  8. Hulu では、既存 Two-Tower を候補生成に残し、圧縮 LLM を数十候補の再ランキングへ使う。cache miss は小型モデル、難例だけ大型教師へ送る構成が現実的である。

  9. 最適化は NDCG、validity、p95、throughput、memory、cost、freshness の制約下で判断する。モデルサイズ一つだけを改善指標にしない。