Skip to main content

CPUで1日10億以上のリクエストを処理するために当社が行ったスケーリング方法

7月

29, 2020

by machynist & kippinitreal


テック

これは、データサイエンティストと機械学習エンジニアにとっての典型的なニワトリかタマゴかの問題です。ビジネスのために新しい機械学習モデルを開発するときに、まずは正確にしてからプロダクションで速くすることを考えますか? それとも、最初に確実に速くできるようにして、それから正確にしますか? 質問をすれば、常に活発な議論が続きます!

新しいブリーディング・エッジBertディープラーニングモデルを使用(当時)して、2019年の初めにRobloxのための次世代の文書分類の開発を始めたときに、まさにそのシナリオに直面しました。 1億1000万パラメータで始まる「初心者レベル」ベースモデルのより大規模でディープなニューラルネットワークであるBertモデルが、当社のプロダクションにおける過酷な速度とスループットの要求仕様を満たすかどうか確信が持てませんでした。 当社は、Bertベースの文書分類を非常に正確にすることを押し進め、幸運なことに緊張した数ヶ月間の後にプロダクション向けに十分に速くすることができました。 そのため、私たちにとって「ニワトリ」(正確さ)が先に来て、それから「タマゴ」(速さ)が後で来ました。 これは私たちにとってストレスの多い経験でしたが、みなさんにとってもそうでなければならないというわけではありません。この記事の中でBert推論を加速するためにした最適化について共有します。 そのため、タマゴから始める(Bertモデルをプロダクションで確実に速くする戦略)ことができて、それからニワトリに集中する(Bertモデルを正確にする)ことができるのです。

Bertをプロダクションで速くするまでの道のり

自然言語処理(NLP)のためのBertの魔力は、2019年にはよく記録され、ここRobloxでは当社のデータでその魔力が近くで作り上げられるのを見てきました。 Bertディープラーニングモデルを微調整することで、当社は文書分類の数多くを劇的に変化させ、 固有表現抽出 (NER) アプリケーションは10パーセンテージポイントか前のモデルを上回ることによって、しばしばモデル性能(F1スコア)を向上させました。

しかし、私たちのBertモデル開発は多大な驚くべき方策素晴らしいライブラリによって加速されましたが、低遅延で高スループットなプロダクション使用例に関しては、PyTorch上でのBertをスケールする方法についての方策を少ししか見つけられませんでした。 そして、当社のNLPサービスの多くに関しては、20ms以下の遅延で毎秒25,000件以上の推論(1日に10億件以上の推論)を処理する必要がありました。

この記事の目的は、RobloxでBert推論の文書分類を30倍以上に加速するまでの道のりを共有することです。 これらの改善(以下に要約してあります)を通じて、Bertをユーザーにとって十分に速くすることができただけでなく、CPU上のプロダクションで扱いやすいコストで実行し、十分に経済的に効率をよくすることができました。

ベンチマーク

この記事の目的のために、Bertを微調整することによって開発された文書分類サービスの推論速度の向上に焦点をあてます。 ここでの「推論」は、継続した文書を入力として取り入れることを指し、入力テンソルにトークン化し、当社のBertベースモデルにそのテンソルを供給し、モデルによって作成された分類ラベルを返します。

中国にあるガラスの橋

上の中国にある「ガラスの橋」の写真は、遅延とスループットの良い喩えです。 遅延は、1人の人が橋を渡るのにどれだけ時間がかかるかに似ており、スループットは一定の時間内に何人の人が橋を渡れるかを測ります。

組織的に速度を向上しようとするときは、常にベンチマークを選ばなければなりません。 Bert分類のために選ばれたベンチマークは、2つの構成要素があります。

  • 遅延: 1件の推論リクエスト(99パーセントと内部ベンチマークもあります)を処理する時間の中央値
  • スループット: 1秒に提供できる推論の数

一貫したスループット比較のために、当社のベンチマークコードはそれぞれが順次に1000件の推論リクエストを通過させ、正確には32のワーカープロセスを始動します。 このベンチマークは、1つの36 Xeonスケーラブル・プロセッサーコアで実行されました。

このベンチマークは、文書分類サービスがワーカープロセスのプールによってサポートされたプロダクション設定を模倣しました。 読み込み下では、それぞれのワーカーは長時間にわたって連続した推論リクエストを処理するのに忙しくしていました。 当社の実際のプロダクション環境では、中央値20ms以下の遅延で毎秒25,000件以上のリクエストに対応できるように数多くの機械に対して何百ものワーカーがありました。

ツール使用

当社の微調整済みのBertモデルはHuggingface Transformers library v2.3.0を使用して開発されました。 当社はPyTorch対応のためにHuggingfaceを特に選び、現在のプロダクションはPyTorch 1.4.0.で行っています。 Huggingfaceは、当社がRobertaXLNetDistilBertAlbertなどを含む数多くの他のTransformerベースのモデルで実験することを容易にしてくれました。 それが、当社が推論速度を劇的に向上させたDistilBertを発見した方法です(後ほどこれについて論じます)。

はじめに: 推論にはCPUかGPUか?

当社の最初の大きな決定の1つは、Bertベースの文書分類の推論をCPUで実行するかGPUでするかというものでした 。

私たちのモデルトレーニングには、GPUは疑いもなくCPUよりは断然、速かったです。 当社の文書分類Bertモデルを微調整(トレーニング)するのに、Tesla V100 GPUをより大きな同等の費用の36コアXeonスケーラブルCPUベースサーバーと比較したときでも 、GPU上と比べてCPU上では10倍以上かかりました。

推論に関しては、GPUとCPUの間の選択は、アプリケーションによります。 これらが推論をCPUにする決定を翻した要素です。

  • 簡素性。 GPUは、入力がバッチされたときに最適なスケールをします。 しかしながら、当社の文書分類は、リアルタイムのリクエスト応答設定において作動します。 そのため、これらのリアルタイムのリクエストのバッチは、諸経費と複雑さを作り出すだけです。 CPU上では当社のBertモデルは入力でバッチをしなかった時でも非常によく作動したので、推論をCPU上で実行することは、それ以降のより簡素なパスを提供してくれました。
  • 有利な経済的コスト。 当社の文書分類には、CPUの推論のほうが経済的コストはGPUよりも上でした。 この記事の中のすべてのスケーリング改善を適用した後、当社のスループット文書分類サービスは、GPUと比較してCPUでは1ドルにつき6倍高いものになりました。 特に、インテルXeonスケーラブル36コアサーバーで毎秒3,000件以上の推論数に対してBertベースサービスをスケールできたのに対し、同様の費用のTesla V100 GPUでは500件の推論数でした。

推奨CPU

当社のベンチマークには、ディープラーニングへの秀逸なサポートのため、第2世代インテルXeonスケーラブル・プロセッサーが最もよく機能することが分かりました。 例えば、当社の最もインパクトのある加速の一部は、 ベクトル・ニューラル・ネットワーク命令と第二世代Xeonスケーラブル・プロセッサーを搭載した二重積和演算 (FMA) コアを活用しています。 この記事の中で報告する結果のすべてはインテルXeonスケーラブル・プロセッサーシリーズのチップにあります。 違うチップでの当社のテストには制限があったため、他のCPU環境設定では有用性は異なるかもしれません。

スレッドチューニング

CPU上のBert推論のスケールで起きた最初の障害は、複数のワーカープロセスが同時モデル推論ができる前にPyTorchが適切にスレッドチューニングがされなければならないことでした。 これは、それぞれのプロセスの中でPyTorchモデルが1件の推論リクエストを処理するのに複数のコアを使用しようとするためです。 そのため、それぞれのプロセスが同じ限られた供給源(物理的コア)を得るために競い合っていました。 これは、同じ機械上で1度にこれらの多すぎるワーカーが実行しているときの停滞という結果になりました。

幸いなことに、この修正はシンプルなものでした。それぞれの推論作動ワーカープロセスにおいて、当社はスレッドの数を以下の9行目に表示してあるように単に1に設定しました。

これをすることの影響は何でしょうか? たくさんのワーカープロセスが同じ機械で実行されているとき、それぞれのワーカーは1つのコアしか使用していなかったため、スケーリングがより規則正しくなりました。 これが許容できる遅延を保ちつつ、それぞれの機械上でスループットを増加させて数多くのプロセスを1つの機械でスケーリングできた理由です。

シナリオ1: Bertベースライン

最初のベースラインは、文書分類のためのバニラ味Bertモデル、元のBert文書の中で説明されていた構造です。 このBert モデルは、Huggingface Transformers 2.3.0 ライブラリからBertForSequenceClassication Pytorchモデルを使用して作成されました。 このシナリオでは、当社は1つの意図的な無知なデザイン選択もしました。128トークンの規定の長さへのすべてのテンソル入力のパッディングをゼロにしました。 これをする理由は、すべての入力が同じサイズになるように、バッチ入力のときにパッディングをゼロにすることが必要とされていることです。 バッチサイズが1の場合は、入力を偶然にパッディングゼロにすることは簡単ですが、この基本的なシナリオにおいては性能ペナルティを数値化したかったのです。

下のピンクのボックスに、この「Bertベースライン」シナリオのベンチマーク結果を表示しています。 330msでさえも、プロダクションのニーズのためには遅延が多すぎたため、ベンチマークによって利用されているコアの数にとってはスループットはひどいものでした。

シナリオ2: より小さいモデル(DistilBert)

Bertが2018年10月にリリースされた後、 パラメータがさらに多いモデルを作成することによって、最先端の先を行こうとしたモデルがたくさんありました。 Bertベースには、1億1000万のパラメータがあるため、新しいモデルの中にはわずかに高い統計的性能でかなり演算性能の悪い10億以上のパラメータがあるものもあります。 しかし、当社が欲しかったのは、よりよい計算性能で、これを達成するために統計的性能を少し犠牲にすることもさぶさかではありませんでした。

幸運なことに、2019年10月にSanh et alがより大きいBertのようなモデルのトレンドを作り上げた数多くのモデルの1つであるDistilBertを紹介しました。 これらの「より小さなモデル」はBertのサイズを減少させることに集中し、それは最小限のロスと統計的性能でより速い推論とトレーニングに行きつきました。

私たちは、2つの理由でDistilBertを選びました。 最初に、DistilBertは大まかにBertの2倍速く、 それでも当社の文書分類上の統計的性能(F1スコア)はBertの1%以内でした。 次に、Huggingface Transformersライブラリによって、トレーニングと推論パイプラインをそのままにキープすることができ、BertとDistilBertを取り換えることが非常に分かりやすいものにしてくれました。

以下は、当社がより大きなBertの代わりにより小さなDistilBertを使用したときのベンチマーク結果です。 ここでわかるように、当社はスループットをほぼ2倍にしながら遅延をほぼ半減させました。

シナリオ3: より小さな入力 (Dynamic Shapes)

当社の次の改善は、違う何かを小さくすることから来て、この場合はDistilBertモデルへのテンソル入力でした。

まずは、パッディングゼロの背景について少々。 ディープラーニングモデルは一般的にテンソルのバッチを入力として要求します。 当社の文書分類にとって、これはBertのトークン化を通じて文書入力をテンソルとして表示し、そしてそのテンソルをバッチします。 こちらで表示してあるように、文書の長さは異なるため、固定型バッチを作り出す結果となるテンソルの変数の長さのパッドもゼロにしなければなりません。

前にも述べた通り、私たちの文書分類はリアルタイムでのリクエスト設定で作動します。 そのように、私たちの理想的なバッチサイズは1です。 この示唆するところは、バッチサイズが1のときはすべてのテンソルが同じサイズなため、テンソルのパッディングをゼロにする必要はまったくなかったのです。 上からの文書入力をパッディングゼロで別々のモデルに送信したときにどのように見えたかはこちらです。

当社では、それぞれのバッチで多様な長さのテンソルを許可しているため、これを「ダイナミックシェイプ(動的型付け)」と呼んでいます。 ダイナミックシェイプで入力を動かすことでスループットと遅延を大幅に改善できました。 パッディングゼロにしないことによって、モデル入力をより小さくしていたため、直感的にこれは納得がいきます。 さらに、モデル出力の可能性がダイナミックシェイプかパッディングゼロのどちらを使用しても同じだったため、F1スコアへの悪影響がありませんでした。

DistilBertとDynamic Shapesを使用したときのベンチマーク結果はこちらです。 ここでわかるように、スループットを2倍にしながら遅延を半減させました。

シナリオ4: より少ない重量(量子化)

より少ない重量での当社の最大の性能ブーストは、量子化というテクニックを通じて達成されたことを確認しました。

量子化は、モデル重量をより小さく表すことを通じてディープラーニング計算の効果を向上させることを伴います。例えば、32ビットの不動小数点重量を8ビットの整数として表すことです。 当社のDistilBertモデルのために利用した特定の量子化テクニックは、「ダイナミック量子化」でした。 このテクニックは、トレーニング中の量子化ではなく、トレーニング後に重量を量子化することを伴います(これは量子化認識トレーニングと呼ばれています)。

Pytorch 1.3+におけるダイナミック量子化サポートの導入は非常に簡単で、DistilBertモデルのすべての線形層において1行のコード変更によって8ビット整数の重量表示で可能となりました。

この量子化が元のDistilBertモデルをどのように変更したのかを視覚化したものがこちらです。 お分かりのように、PyTorchは共起表現のクエリ (Q)、キー (K), そして値 (V) のような「線形」層を「ダイナミック量子化済み線」層に置き換え、これは内部の倍増または追加操作で量子化された8ビット整数を使用します。

DistilbertとDynamic Shapesとダイナミック量子化を使用したときのベンチマーク結果はこちらです。 これらの3つすべての最適化が遅延とスループットの両方においてバニラ味Bertベースラインで一緒になって非常に大きな30倍の向上を作り出すことが分かります。

また、量子化の後に小さなF1の悪影響 (< 1%) を観察しましたが、スループットと遅延の改善がこれを十分に価値あるものにしました。

シナリオ5: より少数のリクエスト数(キャッシング)

最後の最適化の1つは、DistilBertモデルに送信されるリクエストの数を効果的に減らすことです。 当社は、トークンIDをキーとして使用した通常の文書入力の応答をキャッシュにすることによってこれを達成しました。 これは、同じ文書入力へのモデル応答が常に同じであるために機能しました。 さらに、その効果は根底にある文書分散がより不均一であるほど増大します。

当社のデータにとって、プロセスメモリで100万件以上の入力をキャッシュしたときにプロダクション上で40%のキャッシュ的中率を経験的に観察しました。 キャッシュ的中が推論ゼロの効果的なコストを意味するならば、当社のキャッシュはほぼスループットが2倍になったはずです。

データに依存するため、当社のベンチマークでのキャッシュ利用の影響は入れませんでした。 しかし、単純なキャッシュの意義ある影響について確実に述べたいと思いました。

水平スケーラビリティについて

記事の中で見たすべてのベンチマーク結果は、同じサーバー上の32の同時ワーカーを実行することで得たものです。 プロダクションで1日に10億件以上のリクエストをスケールするために、数多くのサーバー上でワーカーを水平にスケールしました。

未来の可能性

Bert PyTorchモデルを研究室からプロダクションに移すのに必要な基本的な最適化についてたくさん述べましたが、当社のこれからのスケーラビリティ戦略に影響を与える可能性のあるイノベーションの多くがこの空間で起きていることを認識しています。 例えば、 当社の非量子化ベンチマークに対して卓越した性能のOnnx Runtimeに注視しているとします。 この記事を書いている時点でMicrosoftはOnnx RuntimeのためのBert最適化をオープンソースにしていて、当社はインテルと密接に連動し、OpenVinoでの潜在的な改良も探っていました。

結果と結論

当社の主な結論は、特にリアルタイムの文書分類のためのCPU上のDistilBertまたはBertのスケーラビリティのいい話があります。 ものを小さくすることによって、Bert文書分類の遅延とスループットで少なくとも30倍のブーストをどのように達成したかをこの記事では説明しています。より小さなモデル(DistilBert)、より小さな入力(ダイナミックシェイプ)、そしてより小さな重量(量子化)。 当社のディープラーニング分類で、20ms以下の中央値遅延でCPU上で妥当な費用ででき、1日に最大10億件のリクエスト処理ができることを非常にうれしく思っています。

これらのテクニックの数多くの調査を手伝ってくれたEdin MulalićとSaša Anđelkovićに感謝します。


Robloxコーポレーションとこのブログは、いかなる企業もサービスも推奨も支持もしません。 また、このブログに含まれる情報の正確さや完全性について、いかなる保証または約束もしません。

このブログ記事は、元は Robloxテックブログ に掲載されたものです。