Slide 1

Slide 1 text

推測するな、計測せよ。 〜広告乱択アルゴリズムのボトルネックを探して〜

Slide 2

Slide 2 text

⿃越 貴智 • アドテクスタジオ AMoAd 中途 3 年⽬ データ‧機械学習エンジニア • 量⼦ゼミ ScalaMatsuri で登壇しました ( ・ㅂ・)و 早稲⽥の⽥中宗先⽣とアニーリングの話をしてます>< piyo piyo z

Slide 3

Slide 3 text

プロダクト紹介

Slide 4

Slide 4 text

• CyberAgent と DeNA の合弁会社 2011 年 4 ⽉に設⽴ 開発はアドテクスタジオ アドネットワーク専業 • 多彩な広告フォーマット AMoAd ネットワーク AMoAd インフィード AfiO(動画) D AD

Slide 5

Slide 5 text

8 年⽬って技術的負債ヤバくない?

Slide 6

Slide 6 text

\技術的アンチエイジング∕ • 広告配信システムのマイクロサービス化 おおむね Kubernetes へ移⾏ 詳しくは和⽥さんの「レガシーシステムのコンテナ化に挑戦した話」 • データ基盤のマネージド化 Dataproc (Spark) BigQuery • ロジックの機械学習化 タイムラグのあるインストール率推定 広告乱択アルゴリズムの刷新

Slide 7

Slide 7 text

広告乱択アルゴリズム

Slide 8

Slide 8 text

探索と活⽤のトレードオフ • 探索:⾊んなスロットを回して当たりやすいものを探す • 活⽤:当たりやすいスロットだけを回す ※ 探索しないと⼀番当たりやすいスロットを⾒逃すかも ※ 探索しすぎるとコストが嵩むかも

Slide 9

Slide 9 text

広告乱択アルゴリズムを変えたい 広告選択のアルゴリズムは、乱数によって探索と活⽤のバランスを取る。 古いものは探索コストをかけすぎていたため、⼊れ替えることに。 • 古いの ソフトマックスっぽい⽅策 ⼀様分布から乱数をサンプリングする • 新しいの トンプソンサンプリング⽅策 ベータ分布から乱数をサンプリングする

Slide 10

Slide 10 text

確率分布 • ⼀様分布 :0 以上 1 以下の値が均等に出現 • ベータ分布:0 以上 1 以下の値が偏って出現(偏り⽅のパラメータがある)

Slide 11

Slide 11 text

do { final double u1 = random.nextDouble(); final double u2 = random.nextDouble(); final double v = beta * (FastMath.log(u1) - FastMath.log1p(-u1)); w = a * FastMath.exp(v); final double z = u1 * u1 * u2; r = gamma * v - 1.3862944; final double s = a + r - w; if (s + 2.609438 >= 5 * z) { break; } t = FastMath.log(z); if (s >= t) { break; } } while (r + alpha * (FastMath.log(alpha) - FastMath.log(b + w)) < t); ベータ分布のサンプリング実装

Slide 12

Slide 12 text

ベータ分布のサンプリング実装 . ⼀様分布から 2 回サンプリング . ややこしそうな計算 . よくわからない条件を満たすまでループ ※ Apache Commons Math の BetaDistribution.java からコードを抜粋

Slide 13

Slide 13 text

do { final double u1 = random.nextDouble(); final double u2 = random.nextDouble(); final double v = beta * (FastMath.log(u1) - FastMath.log1p(-u1)); w = a * FastMath.exp(v); final double z = u1 * u1 * u2; r = gamma * v - 1.3862944; final double s = a + r - w; if (s + 2.609438 >= 5 * z) { break; } t = FastMath.log(z); if (s >= t) { break; } } while (r + alpha * (FastMath.log(alpha) - FastMath.log(b + w)) < t); ベータ分布のサンプリング実装

Slide 14

Slide 14 text

もしかして停⽌する保証ない?

Slide 15

Slide 15 text

当時の推測 org.apache.commons.math .distribution.BetaDistributionの実装は、 ⼀様分布を使って条件を満たすまでwhileループしつづけるものなので、 最悪ケース終わらないかもしれない。 そこでベータ分布の期待値は乱数使わずに計算できるため、 期待値の多い順からサンプリングをし、 タイマーが切れたところで最⼤のものを取る仕組みを⼊れると良さそう。

Slide 16

Slide 16 text

ஆ ͔ ͘ ݟ क Δ ࣄ ۀ ੹ ೚ ऀ ू ܭ Λ վ म ͠ ͯ ͘ Ε ͨ ಉ ྅ ʮ͋ɺ͜Εແཧ͔΋ʯͱ਒͑Δࢲ

Slide 17

Slide 17 text

負荷テスト

Slide 18

Slide 18 text

性能要件 • ⼀般的にアドテクの許容 Latency はトータル 100ms 以内が⽬安 • 当然その⼀部であるマイクロサービスの許容 Latency はもっと短い • 現状ピーク時間帯 1500 qps の 95% を Latency ms 以内でさばいている • Latency ms 以内を要件に

Slide 19

Slide 19 text

負荷テスト . テスト環境に広告選択マイクロサービス (Finagle) を⽴てて本番 DB に繋ぐ . 本番ログからテスト⽤シナリオを⽣成する . Gatling で負荷をかけて response time を計測する . Finagle のプロファイリング⽤エンドポイントを叩く

Slide 20

Slide 20 text

計測結果(チューニング前) • Gatling で計測した response time は明らかに性能要件を満たせてない • Finagle のプロファイルを⾒ると、 • BetaDistribution は呼んでるけど、 • AbstractWell とか Well c (AbstractWell の⼦クラス) とか覚えない…… $ pprof -cum —text profile | grep math3 … 29.3% org.apache.commons.math3.distribution.BetaDistribution. … 29.3% org.apache.commons.math3.random.Well19937c. … 29.3% org.apache.commons.math3.random.AbstractWell. … 29.3% org.apache.commons.math3.random.AbstractWell.setSeed

Slide 21

Slide 21 text

ボトルネックのコード AbstractWell.java からコードを抜粋 • JDK の System.identityHashCode のバグを踏んでいるっぽい JDK- : Performance problem with System.identityHashCode in client compiler public void setSeed(final int[] seed) { if (seed == null) { setSeed(System.currentTimeMillis() + System.identityHashCode(this)); return; }

Slide 22

Slide 22 text

原因と対策 • リクエストごとに異なるベータ分布から乱数をサンプリングしたい • リクエストごとにベータ分布乱数⽣成器をコンストラクト • その際に呼ばれる⼀様乱数⽣成器のコンストラクタがボトルネック • ベータ分布乱数⽣成器のコンストラクタで⼀様乱数⽣成器を注⼊可能 • その⼀様乱数⽣成器は使いまわしてよいがスレッドセーフでない • スレッドごとに⼀様乱数⽣成器を⼀度だけコンストラクトして使いまわす

Slide 23

Slide 23 text

改修コード • Java 標準の ThreadLocal で⼀様乱数⽣成器をスレッドローカル変数化 class PickStage { val threadLocalRng: ThreadLocal[Well19937c] = ThreadLocal.withInitial(() => new Well19937c()) …… def sampleBetaDistribution(alpha: Double, beta: Double): Double = new BetaDistribution(threadLocalRng.get, alpha, beta).sample() }

Slide 24

Slide 24 text

計測結果(チューニング後) • 再度 Finagle のプロファイルを⾒ると、ボトルネック消失 • キャッシュアクセスなどもチューニングして性能要件を満たすように • 懸念していたベータ分布のサンプリング箇所はプロファイルに現れずじまい • それはそれで不安を覚えながらリリースへ…… $ pprof -cum —text profile | grep math3 (no output)

Slide 25

Slide 25 text

リリース

Slide 26

Slide 26 text

No content

Slide 27

Slide 27 text

リリース前後の監視メトリクス

Slide 28

Slide 28 text

No content

Slide 29

Slide 29 text

種明かし この発表にあたって調べてみたら、 ベータ分布のサンプリング性能は提案論⽂に書かれていた orz たしかに最悪計算量は無限⼤だけど、 while ループの期待値はたかだか 4 回でしかない ヽ(*゚▽゚)ノ 詳しくは Qiita 「ベータ分布のサンプリングアルゴリズムを読みとく」

Slide 30

Slide 30 text

それはまた別の物語… システム的なチューニングの後には、 ロジック的なチューニングが待っていて、より難しかったり Σ(゚д゚;) • 予測精度は? • 指標は改善した? • 運⽤に問題起きてない?

Slide 31

Slide 31 text

まとめ • 事前に負荷テストしてよかった • 推測でタイマー実装しなくてよかった • プロファイラで計測してボトルネックを探そう • アルゴリズムの計算量が気になるなら⽂献に当たろう

Slide 32

Slide 32 text

これまで遭遇したボトルネック • 配列に⼤量の要素を⼀つずつ追加 ➡ 事前にメモリ確保 • スパースな巨⼤多重配列の確保 ➡ 連想配列に置きかえ • コンストラクタの引数で巨⼤なコピー ➡ 右辺値参照渡し • 多重ループで結合 ➡ マージ結合に切りかえ • 巨⼤な⾏列演算 ➡ 線形代数ライブラリに投げる • 全ワーカーが全マスターデータ取得 ➡ データをID分割 • 膨⼤な中間ファイル読み込み ➡ 中間出⼒時にパーティションを纏める • リソースロックのリトライが衝突しつづける ➡ 乱数で揺らぎを⼊れる

Slide 33

Slide 33 text

これまで遭遇したボトルネック • 配列に⼤量の要素を⼀つずつ追加 ➡ 事前にメモリ確保 • スパースな巨⼤多重配列の確保 ➡ 連想配列に置きかえ • コンストラクタの引数で巨⼤なコピー ➡ 右辺値参照渡し • 多重ループで結合 ➡ マージ結合に切りかえ • 巨⼤な⾏列演算 ➡ 線形代数ライブラリに投げる • 全ワーカーが全マスターデータ取得 ➡ データをID分割 • 膨⼤な中間ファイル読み込み ➡ 中間出⼒時にパーティションを纏める • リソースロックのリトライが衝突しつづける ➡ 乱数で揺らぎを⼊れる ਪ ଌ ෆ ೳ

Slide 34

Slide 34 text

:PVDBOUUFMM XIFSFBQSPHSBNJT HPJOHUPTQFOEJUTUJNF .FBTVSF “Notes on Programming in C”