2021年7月15日
バッファアロケーションをパス被覆問題として解く

はじめに
こんにちは。LeapMindで楽しく働いている前田です。LeapMindで開発中のEfficiera(えふぃしえら)という製品で使用しているメモリ割り当てのアルゴリズムを紹介します。このアルゴリズムの開発は前田が入社して最初にチャレンジしたタスクですが、従来アルゴリズムからの改善効果が大きく、また理論的にもエレガントなものにできました。
Efficieraとは
Efficieraは、LeapMindが開発しているディープラーニング推論向けアクセラレータIPです。極小量子化技術を利用することで、小さい回路規模・省電力・省メモリで高速な推論を実現します。
Efficieraの極小量子化は、2bitアクティベーション / 1bit重みを使います。これは、すでに実用化されている量子化推論で使われる8bitアクティベーション / 8bit重みと比較してさらに小さい表現です。このような極小ビット計算では、Intel OpenVINOやNVIDIA TensorRTなどの既存のフレームワークが提供する学習済みモデルを量子化モデルに変換する方法では精度が低下してしまいます。そのためEfficieraではモデル作成・学習から一貫して量子化ネットワークを扱うためのフレームワーク (Efficiera NDK / Compilation Tool) も開発・提供しています。
Efficieraとランタイム
Efficieraを動かすには、「ランタイム」と呼ばれるソフトウェアが必要です。これはEfficieraに接続したホストCPUで動作するライブラリで、Efficieraに演算内容を指示したり、Efficieraがサポートしない計算や、カメラデバイスなどのI/Oとの仲介を担当します(図1)。
図1: Efficieraを用いた推論におけるランタイムの役割
ランタイムにおけるメモリの使い方
ランタイムはさまざまな用途にメモリを使います。このうち最も重要なのが、計算したテンソルを保存しておく一時バッファです。DNNは多数の畳み込み演算 (Conv) がチェインする構造を持つので、これらのConvの出力を置いておく一時バッファも数多く必要です。ほとんどのConv出力は直後の演算でのみ使われるので、対応する一時バッファは推論処理全体のうち特定の期間だけ生存していれば十分です。この期間のことを以降では一時バッファの生存期間と呼びます(図2)。
図2: ネットワークのグラフの一部(上段)とそれに対応する一時バッファの生存期間(下段)。グラフの各ノードの出力は、その全ての使用が終わるまで生存している必要がある。緑のノードの出力バッファは、黄色と紫の2つのノードが使用するので、他の出力バッファよりも生存期間が長い。
ランタイムは一時バッファに必要なメモリ領域(アリーナ)を初期化時に一括で確保します。アリーナを用いる理由はシステムの信頼性向上です。たとえば、推論中のメモリ確保が失敗したり、失敗せずとも推論の実行時間がばらつくことを避ける意味があります。
それぞれの一時バッファのアリーナでの配置を静的に決定するのは難しい問題です。直感的には、横軸を生存期間・縦軸をアリーナの領域とする二次元領域への矩形割り当て問題として考えることができます。上手な配置を行ってアリーナを密に配置できれば、推論に必要なメモリの使用量を削減できます(図3)。
図3: 図2のネットワークの一時バッファの配置例。左の配置では隙間が多く、アリーナのサイズが増大してしまう悪い配置である。右はアリーナのサイズが最小になる最も良い配置。
バッファ割り当て問題
バッファ割り当て問題を適切なアルゴリズムに落とすために定式化を行います。ここで重要なのは、推論の計算を有向非巡回グラフ (directed acyclic graph; DAG) で表現すること・計算の途中で一時的に使われる領域をDAG上の生存期間によって表現することです。
DAGはトポロジカルソートによって順序関係を保ったまま直列化できるので、以降グラフの各ノードは順序付けされているとします。すなわち、グラフが個のノードから成る場合、各ノードの計算順序にしたがって、ノード ノード と順序付けられます。また、議論の簡単のために、各ノードの出力テンソルはひとつだけに限定し、ノードと出力テンソルを同一視します。複数の出力テンソルをもつノードは、ひとつだけの出力のノードの並置で常に置き換え可能なので、議論の一般性は損ないません。ノードの出力テンソルをテンソルと呼び、そのサイズを byteとします。
図4: 図2のグラフの各ノードに順番付けしたグラフ。
テンソルのアリーナへの割り当てを決定することは、アリーナ上に割り当てる領域の先頭の位置を決定することと等価です。すなわち、テンソルをアリーナ上の領域に割り当てます。 アリーナの大きさは、各テンソルが割り当てられた領域がはみ出さないように取れば良いので となります。この値がより小さくなるようなを決定するのが目的です。ただし、を決定する際には各テンソルの生存期間を考慮する必要があります。
テンソルの生存期間
テンソルのの生存期間を と書くことにします。テンソルはノードの出力として生まれてから、他のノードの入力として使用し尽くされるまでの期間、アリーナ上に存在する必要があります。したがって、テンソルを入力として取るノードがノードノードであるときに
と表せます。
問題の定式化
バッファ割り当ての問題は、生存期間が重複する二つのテンソル i,j(i≠j) をアリーナ上で重ならないように配置する問題として、以下のように定式化できます。
入力: 出力: 最小化: 条件:
任意のについて、とが重複するならばと は重複しない
アルゴリズム
従来のアルゴリズム
以前は次のようなアルゴリズムを用いていました:
arranged_idnex <- [1, ..., n]を、(end[i]-begin[i], size[i])の降順でソート
#arranged_indexの先頭から割り当てる
height <- [0, ..., 0] (全部0 長さn)
offset <- []
for k in arranged_index:
max_height <- height[begin[k]: end[k]]の最大値
offset[k] <- max_height
height[begin[k]: end[k]] <- offset[k] + size[k]
return offset
上記のアルゴリズムは以下の2つのステップから成ります:
- ステップ1: 入力を特定の順番で並べる () ここでは、の降順でソートしています。
- ステップ2: の順番に従って敷き詰めて割り当てる
図5: ステップ2において割り当てを行う様子の一例。アリーナのメモリについての軸(縦軸)を高さに見立てて、できるだけ低いところに割り当てるようにする。
ワーストケースの観察
このアルゴリズムの振る舞いにはよくない性質があります。例として図6に示す分岐のないネットワークを入力してみましょう。簡単のために全てのテンソルが同じサイズであるとします。
図6: 分岐のないネットワークのグラフ例
各テンソルの生存期間は、 となります。 なので、上記のアルゴリズムのステップ1においてソートの基準となる値は全て等しくなります。すなわち、ソートの結果は用いるソートアルゴリズムのタイブレイクの仕様により決まります。例えば、マージソートなどの安定ソートを利用した場合、テンソルの並びは全く変更されません。
仮にノードの並びが変更されなかったとすると、ステップ2において以下のようにテンソルが階段上に配置されてしまい、メモリを効率的に利用できません。
図7: テンソルが階段状に配置されてしまう様子
この結果を踏まえて、ソートの基準としてどのようなものが相応しいか検討しましょう。やはしばしば似たような値になるので、ソートの基準として用いるのは好ましくありません。また、やについてソートしても同様に階段上に配置されてしまいます。を組み合わせてソートの基準とするのは筋が良くないようです。
パス被覆による改善
特定の基準でソートするという考えから一旦離れて、さきほどのケースにおける最適な配置を考えます。明らかに次のように上下交互に配置するのが最適です。
図8: 最適な配置
このような良い配置を一般的なケースにおいて得るためには、以下のように処理を行うとよさそうです。
- ステップ1: 各テンソルを、互いに生存期間が重複しないテンソルのグループに分割する。 とする。
- ステップ2: 旧アルゴリズムから据え置き。
このようなテンソルのグループ分けは無数に考えられますが、その中でグループ数が最小になるようなグループ分けをパス被覆という概念と対応させて考えることができます。グラフを以下のように構成します。
- ノードを頂点とする
- 任意のについて、であればに辺を張る
図9: グラフ
このようにして構成したグラフはDAGになります。グラフを考える利点は、“互いに生存期間が重複しないのテンソルのグループ”と グラフ上の有向パスが一対一に対応するところにあります。したがって、テンソルのグループ分けはグラフのパス被覆(互いに交わらない有向パスによってグラフの全てのノードを覆うこと) に対応します。 特に、グループ数最小のグループ分けはグラフの最小パス被覆に対応します
図10: パス被覆とグループ分けの対応
嬉しいことに、DAGに対する最小パス被覆は多項式時間で解くことができます。用いるアルゴリズムにもよりますが、一般にまたはの時間計算量です。
グループ分けアルゴリズム
今回の問題では、次に示すようなシンプルな貪欲アルゴリズムによって、最小パス被覆問題を陽に解かずにグループ数最小のグループ分けを求めることができます。
- テンソルを順番にグループに追加する
- テンソルを追加できるグループが存在するなら、任意に一つ選んで追加する
- そのようなグループが存在しないならテンソルのみからなる新しいグループを作成する
アルゴリズムの正当性の証明はAppendixに記します。
評価
グループ分けアルゴリズムによって得られる割り当て結果のアリーナのメモリ使用量は、以下のように評価できます:
ここで、はそれぞれ以下のように定義されます。
- : 推論ネットワークのグラフに対して定まる量。グラフ上の有向パスの集合であって、グラフの各辺がパス集合内の少なくとも1つのパスに含まれるようなものを考える。通常のパス被覆と違って、パス集合が頂点を覆う必要はない。また、2つ以上のパスに含まれる頂点や辺が存在してもよい このようなパス集合の中で、集合のサイズが最小のものを考え、そのサイズをとする。
証明はAppendixに記します。
ニューラルネットワークで用いるグラフでは、の値は小さくなる傾向にあります。例えば、resnetではグラフの全ての辺を2本のパスで覆うことができます(図11)。1本のパスで全ての辺を覆うことは不可能なので、となります。
図11: 赤い矢印で表現された2つのパスによって、resnetのグラフの各辺を覆う様子。
多くのモデルにおいて、の値はネットワークの深さに依存せず定数となります。したがって、扱うネットワークの層が深くなってもメモリ使用量は漸近的に増加しません。
性能改善
例として、開発中の100GOP程度のネットワークに適用した場合、従来アルゴリズムに比べてメモリ使用量を40%削減できました。社内開発している複数のモデルにおいても同様の有効性が確認できています。
おわりに
LeapMindでは、競技プログラミングで培ったアルゴリズムの経験を生かして楽しく仕事をしています。 ところで、LeapMindではエンジニアを募集しています。競技プログラミングで培ったアルゴリズムの経験を生かして楽しく仕事したい人も、そうでない人も、興味がある方はぜひ一度LeapMindの採用ページから応募してみてください。
Appendix
グループ分けアルゴリズムの正当性の証明
アルゴリズムの正当性を示します。 明らかにアルゴリズムが出力するグループ分けは正しいグループ分けです(各グループに属するテンソルの生存期間は互いに重複しない)。したがって、グループ数が最小であることを示せば十分です。これは次のように証明できます。
と定義すると、明らかにはグループ数に対する下界となっています。上記のアルゴリズムが出力するグループ分けのグループ数が、ちょうどであることを示せば十分です。個以上のグループからなるグループ分けが出力されたと仮定して矛盾を導きましょう。
テンソルを追加する際に個目のグループが作成されたとします。仮定より、既に作成された個のグループ グループ グループ それぞれについてテンソルを追加することができなかったということになります。すなわち,以下の条件が成り立つような個のテンソル、 テンソルテンソル が存在します:
- テンソル はグループiに属する()
- テンソルとテンソルの生存期間は重複している
今、テンソルはの小さい方から追加しているので、 が成り立ちます。テンソルとテンソルの生存期間が重複しているという仮定と合わせると が従います。ところが、
となるので矛盾します。(証明終)
の証明
が成り立つので、 を示せば十分です。辺を覆うパス被覆を構成する本のパスを考えます。パスについて、
\begin{align*} \mathrm{overlap\_max}&=\displaystyle \max_{1 \leq x \leq n} \# \{i \ (1 \leq i \leq n) | \mathrm{begin}[i] \leq x < \mathrm{end}[i] \} \\\ &\leq \displaystyle \max_{1 \leq x \leq n} \sum_{1 \leq p \leq W} \# \{i \in \mathrm{path\_group}[p] | \mathrm{begin}[i] \leq x < \mathrm{end}[i] \} \\\ &\leq \displaystyle \sum_{1 \leq p \leq W} \max_{1 \leq x \leq n} \# \{i \in \mathrm{path\_group}[p] | \mathrm{begin}[i] \leq x < \mathrm{end}[i] \} \\\ &\leq 2W \end{align*}