行列積のコスト — AIの計算はほぼここに帰着する
なぜGEMMが全てなのか。O(n³)の内訳、メモリ帯域と演算強度、GPUが速い理由、タイル化の直感。最後に、AIモデルの学習・推論に必要なFLOPsを自分の手で見積もれるところまで持っていきます。
要点
- 現代の計算機では演算器に比べメモリ帯域が遅く、実行時間は演算回数だけでなく運ぶバイト数も合わせて見る必要がある。
- 行列積GEMMは m×k と k×n の積で 2mkn FLOPs、正方行列なら 2n^3 FLOPs で、AIの計算の大半を占める。
- 演算強度 I = FLOPs / 運ぶバイト数 が、メモリ律速か演算律速かを分け、性能は min(ピーク性能, I×帯域) で頭打ちになる。
- 行列積の I は n/6 で大きいほど有利だが、行列とベクトルの積は I≈0.5 で固定され、バッチ化が効く。
- タイル化は I≈T/6 に引き上げ、概算ではトークン1個あたり推論約2N、学習約6N FLOPsで、総計算量は C≈6ND になる。
出典: Attention Is All You Need · cuDNN: Efficient Primitives for Deep Learning · BLAS (Basic Linear Algebra Subprograms) · Parallel Thread Execution ISA (NVIDIA CUDA Documentation) ほか2件(末尾)
比喩: 加工機とトラック
工場に、猛烈に速い加工機を1台入れたとします。1秒で1000個の部品を削れる。ところが材料を運ぶトラックは1秒に10個しか届けられません。この工場の生産量は、加工機の性能ではなくトラックの本数で決まります。加工機は99%の時間、手を止めて待っている。
現代の計算機はこの工場そのものです。演算器は猛烈に速く、メモリからデータを運ぶ帯域はそれに追いつきません。だから「何回計算するか」だけを数えても実行時間は当たらず、何バイト運ぶかを同時に見る必要があります。
そしてAIの計算時間は、その大半が1種類の演算に集中しています。行列積です。
なぜ行列積が全てなのか
Transformerの中身を分解すると、出てくるのはほとんど行列積です。Q・K・Vへの射影、注意スコアの計算、注意の重み付き和、出力射影、そしてFFNの2枚の線形層。畳み込みも、im2colやそれに類する変形で行列積に落として実行されるのが普通です。
だからライブラリの世界では、この演算に GEMM(General Matrix Multiply, 一般行列積)という専用の名前が付き、何十年も最適化され続けてきました。「AIを速くする」という工学課題の実体は、かなりの部分が「GEMMを速くする」ことです。
の内訳
の行列 と の行列 を掛けて の を作ります。出力の1マスの定義はこれです。
「 の 行 列は、 の 行目と の 列目を端から掛けて足したもの」——つまり内積1回です。 にはマスが 個あり、1マスにつき掛け算 回と足し算がほぼ 回。掛け算と足し算を1回ずつ数えると、必要な演算回数は
つまりこの式は「出力のマスの数 × 1マスを埋めるのに要る積和の回数 × 積和1回で2演算」を並べて掛けただけのものです。難しいことは何も起きていません。
正方行列()なら です。 が2倍になれば計算量は8倍。これが の中身で、AIモデルの「計算量」と呼ばれるものの正体でもあります。
数を入れると規模感が出ます。 の正方行列積は約 FLOPs——たった1回の掛け算に1000億回超の演算が要る。Transformerの層はこれを何十枚も重ね、それをトークンごと、層ごとに繰り返します。
行列積の原子が内積であることは、手で動かして掴んでおく価値があります。
ここまでが演算回数の話です。しかし実行時間は だけでは決まりません。冒頭のトラックの話——運ぶ側の制約が、ここから効いてきます。
演算強度: 1バイト運ぶごとに何回計算できるか
冒頭の工場の話を式にします。演算強度(arithmetic intensity)を次で定義します。
つまり、分子が「どれだけ働いたか」、分母が「そのために何バイト運んだか」。 はデータの燃費だと思ってください。「運んだデータ1バイトあたり、何回の演算に使い回せたか」という比率です。これが小さい演算は、演算器がいくら速くても帯域で頭打ちになります(メモリ律速)。大きければ演算器の性能をそのまま使えます(演算律速)。実際に出る性能はおおよそ次で押さえられます。
つまり、実際に出る速さは「演算器が出せる上限」と「運べるデータで許される上限」の低いほうで頭打ちになる、ということです。前者は機械の限界、後者は「帯域で運べる量 × 1バイトあたりの仕事量」。この2本の直線で性能の上限を描いたものがルーフラインモデルです。
行列積で計算してみます。 の正方行列なら、運ぶ要素は の3枚で 個。fp32(1要素4バイト)なら バイトです。FLOPsは なので
要するに、行列積の演算強度は「一辺の長さを6で割った数」でしかありません。 に比例して増えます。行列が大きいほど、1回運んだデータを何度も使い回せる——これが行列積の際立った性質です。 なら 。演算律速の側にしっかり入ります。
対照的に、行列とベクトルの積(GEMV)は FLOPsに対して運ぶのが約 バイト、つまり で固定です。行列の各要素を1回しか使わないから使い回しようがない。LLMのトークン生成が1件ずつだと遅く、バッチをまとめると劇的に効率が上がるのは、GEMVがGEMMに変わって が上がるからです。
なぜGPUが速いのか
理由は2つに整理できます。演算器をとにかく多く並べたこと、そして行列積の形で命令を切ったことです。
CPUは1つの命令をなるべく速く終わらせる設計で、分岐予測や投機実行に多くの面積を割きます。GPUは逆に、同じ演算を大量のデータに一斉に当てる前提で、制御回路を削って演算器を敷き詰めました。さらに近年のアクセラレータは、小さな行列同士の積和を1命令で実行する専用ユニットを持ちます。 が高い演算だけを速くする、という割り切りです。
ここで効いてくるのが です。演算器を1000倍並べても、 が小さい演算は帯域で止まるので速くなりません。GPUが速いのは行列積が高い演算強度を持つからであって、あらゆる計算が速くなるわけではない。この非対称性は、実装の判断に直結します。
タイル化: 大きな行列を、キャッシュに載る塊に切る
は「 も も も全部が高速メモリに載っている」理想の値でした。現実には の行列はキャッシュに入りません。素朴な三重ループは、内側で の列を舐めるたびに主記憶へ取りに行き、実効的な は 0.25 程度まで落ちます。
対策がタイル化(ブロッキング)です。行列を の小さなブロックに切り、「 のブロック・ のブロック・ のブロック」の3枚が高速メモリに同時に載るようにする。載せてしまえば、そのブロック内では 回の演算を 個のデータで賄えます。全体で見ると
つまり、実効的な燃費を決めているのは行列全体の一辺 ではなく、高速メモリに載せきれた塊の一辺 のほうだ、ということです。 が大きいほど良く、上限はキャッシュ(GPUなら共有メモリ)の容量です。BLASライブラリが何十年も磨いてきたのは、突き詰めればこの切り方と、レジスタまで含めた多段のタイル設計です。アルゴリズムは同じ のまま、データの動かし方だけで数倍から一桁の差がつく——計算量の話で触れた「オーダーの外側」が最も露骨に効く領域です。
なお 自体を下げる方法もあります。Strassenのアルゴリズムは分割統治で を達成し、非常に大きな行列では実際に使われることがあります(数値的な安定性は素朴な方法より劣ります)。理論上の指数はさらに2.4を切るところまで下げられていますが、定数が巨大すぎて実用にならない「銀河系アルゴリズム」です。
FLOPsを自分で見積もる
ここが到達点です。まず道具を用意します。
def gemm_cost(m, k, n, bytes_per_elem=4):
flops = 2 * m * k * n # 積和1回 = 2 FLOP
moved = (m * k + k * n + m * n) * bytes_per_elem
return flops, flops / moved # (FLOPs, 演算強度)
print(gemm_cost(4096, 4096, 4096)) # → 演算強度 682.7 [FLOP/byte]
print(gemm_cost(4096, 4096, 1)) # → 演算強度 0.5 [FLOP/byte]
同じ行列 を使っても、相手が行列かベクトルかで演算強度が1000倍以上違う。バッチ化の効き目がそのまま数字に出ます。
モデル全体は、パラメータ数 から概算できます。パラメータ1個は「掛けて足す」で2 FLOPを生むので、推論の順伝播はトークン1個あたり約 FLOPs。学習では逆伝播が入力側と重み側の2つの勾配を計算するため、順伝播の約2倍かかり、合計でトークン1個あたり約 FLOPsです(誤差逆伝播)。したがって トークンで学習する総計算量は
つまり「パラメータ1個とトークン1個の組み合わせごとに、だいたい6回の演算」。学習の総コストは、モデルの大きさ と読ませた文章量 の掛け算でほぼ決まる、ということです。これは注意機構の 項や正規化を無視した近似で、、つまり系列長 が の約12倍を下回る範囲で使えます。長文脈では注意の項が無視できなくなる、と覚えておいてください。
数字を入れます。、 トークンなら FLOPs。もし持続的に FLOP/s(1 PFLOP/s)を出せる環境なら 秒、約1.3年かかります。学習に数百〜数千台の加速器を並べる理由が、この一行で腑に落ちるはずです。
実務で効く4つ
1. バッチサイズを疑う 推論が遅いとき、まず を見ます。バッチ1の生成はGEMVでメモリ律速なので、演算器を増やしても速くなりません。まとめて流すのが唯一の効く手です。
2. 「実効FLOPs / ピークFLOPs」を見る 実測時間から逆算した実効性能をピーク性能で割ると、どれだけ演算器を遊ばせているかが出ます。この比が低いなら、原因は計算量ではなくデータの動かし方です。
3. 精度を下げるのは帯域を買う行為 fp32をfp16やint8にすると、演算が速くなるだけでなく運ぶバイト数が半分・4分の1になるので が上がります。量子化がメモリ律速の場面で特に効くのはこのためです。
4. 自分でGEMMを書かない タイル化・プリフェッチ・レジスタ割り当てを含む最適化は、既存のBLAS実装に何年ぶんもの蓄積があります。書くべきなのは、GEMMを呼ぶ形のほうです。
まとめ
- AIの計算はほぼGEMMに帰着する。 と の積で FLOPs、正方なら
- 実行時間は演算回数だけでは決まらない。演算強度 = FLOPs / 運ぶバイト数 が帯域律速か演算律速かを分ける
- 行列積は で大きいほど有利、行列ベクトル積は で固定。バッチ化が効くのはこの差
- タイル化は をキャッシュ容量まで引き上げる工夫。 のまま実行時間が桁で変わる
- 概算式は推論 / トークン、学習 。これで必要な計算資源が自分で見積もれる
次は、この計算を多数の演算器へどう分けるか——並列化の限界と分散学習の通信を扱います。
コメント
コメントにはログインが必要です