体系 › Transformerの仕組み
·無料·論文·13分で読めます

FlashAttentionを1から — 計算を増やして速くする逆説

FlashAttentionは逆伝播でわざと同じ計算をやり直します。演算回数は増えるのに速い——GPUでは掛け算よりデータの運搬のほうが高いからです。HBMとSRAMの往復を数えるところから、タイリングを阻むsoftmaxの壁、それを壊すオンラインsoftmaxの漸化式、そして現場でバックエンドが静かにフォールバックする落とし穴までを前提知識ゼロから追います。

要点

  • FlashAttentionは近似ではなく、素朴な実装と数学的に同じ答えを出す。
  • 素朴な実装は入出力が N×d なのに途中で N×N の表を作り、HBMを何度も往復させる。
  • オンラインsoftmaxは、最大値が更新されたら古い蓄積をスカラー1つ e^{m−m'} で縮めてから足す。
  • 逆伝播では P を保存せずSRAM上で作り直し、演算は増えるが帯域律速ではバイトのほうが高い。
  • マスクを渡すとflashバックエンドからMATHへ黙ってフォールバックし、速度低下やOOMで初めて気づく。
対象textタスクinference

FlashAttention: Fast and Memory-Efficient Exact Attention with IO-Awareness


最適化なのに、計算が増える

高速化と聞けば、ふつうは無駄な計算を削る話を想像します。FlashAttentionは逆です。逆伝播(誤差を戻す計算)で、順伝播とほぼ同じ計算をもう一度やり直します。演算回数は明確に増える。それでも素朴な実装より速い。

種明かしは一行で済みます。いまのGPUでは、掛け算をするより、数をメモリから運んでくるほうが高い。 だから「運ぶ回数が減るなら、計算はやり直したほうが安い」という交換レートが成立します。しかもFlashAttentionは近似ではありません。間引きも疎化もせず、素朴な実装と数学的に同じ答えを出します。変えたのは答えではなく、答えにたどり着く経路です。

比喩: 採点表を紙に書かない

資料が地下の倉庫にあり、机はとても狭い図書館を想像してください。倉庫との往復は遅く、机の上の作業は一瞬で終わります。

仕事は「1000人の応募者を1000項目で採点し、点数の割合に応じて意見を混ぜる」こと。素朴なやり方では、まず100万マスの採点表を作り、机に載らないので倉庫にしまい、行ごとの合計を出すために出し直し、割合に直してまたしまい、意見を混ぜるのに三度目を出してきます。

FlashAttentionは採点表を紙に書きません。応募者を50人ずつの束に分け、束ごとに机の上で採点し、答えの途中経過だけを机に置いたまま更新し続ける。最後の束を終えたとき、机には完成した答えが残っています。

倉庫がGPUのHBM(大容量・低速な外部メモリ)、机がSRAM(小容量・高速なオンチップメモリ)です。落差が実際に何桁あるかはGPUのメモリ階層で扱っています。

おさらい: 途中だけ N×N に膨らむ

Attention機構を1から理解するで見たとおり、自己注意は3行で書けます。系列長(トークン数)を NN、ヘッド次元を dd とします。

S=QK⊤d,P=softmax(S),O=PVS = \frac{QK^\top}{\sqrt{d}}, \qquad P = \mathrm{softmax}(S), \qquad O = PV
(1)

Q,K,VQ, K, V はどれも N×dN \times d の行列で、各行が1トークンの「質問」「名札」「発言内容」です。SS は全員と全員を突き合わせた N×NN \times N の点数表、PP はそれを行ごとに合計1の割合へ直したもの、OO が出力(N×dN \times d)。つまり入力も出力も N×dN \times d なのに、途中だけ N×NN \times N に膨らむ。ここがすべての元凶です。

FIG 1円卓の上を注意の重みが飛ぶ。FlashAttentionが計算するのは、この図とまったく同じ重みと出力です。変わるのは「重みの表を紙に書き出すかどうか」だけ

遅いのは計算ではなく、往復である

数字にすると効きます。N=4096N = 4096、d=128d = 128 なら、QQ は約52万要素なのに SS は約1678万要素——32倍です。系列長を4倍にすれば QQ は4倍にしかならないのに SS は16倍になり、比は128倍へ開きます。

素朴な実装は、この巨大な表をHBMに何度も往復させます。SS を書き出す、softmaxのために読み直す、PP を書き出す、VV との積でまた読む。運ぶバイト数は N2N^2 に比例し、演算量は N2dN^2 d に比例するので、1バイトあたりの演算回数は dd 程度。完全に帯域律速の領域です。

しかも SS も PP も、最後には捨てられる中間結果です。最終出力より大きな中間結果を、わざわざ倉庫に往復させている。ここに手を入れられる、というのが出発点でした。

タイリングを阻む、softmaxという壁

「大きな行列を小さなブロックに切り、速いメモリに載る分だけ処理する」——タイリングは行列積では昔からの定石です。ではなぜattentionでやっていなかったのか。

softmaxが邪魔をするからです。

softmax(s)j=esj−m∑kesk−m,m=max⁡ksk\mathrm{softmax}(s)_j = \frac{e^{s_j - m}}{\sum_{k} e^{s_k - m}}, \qquad m = \max_k s_k

sjs_j は点数表の1マス、mm はその行の最大値です。最大値を引いてから指数を取るのは exe^x が桁あふれしないための定番の安定化で、分子と分母に同じ定数がかかるだけなので答えは変わりません。

問題は mm と分母の合計です。どちらも行を最後まで見ないと確定しません。ブロックを1つ処理した時点では、後からもっと大きな点数が現れるかもしれない。だから「表を出し切ってから正規化する」実装になり、N×NN \times N を持たざるを得なかった——長らくそう思われていました。

オンラインsoftmax: 途中経過を、後から縮める

この壁を壊すのがオンラインsoftmaxです。発想は単純で、「最大値が更新されたら、それまでに足した分をまとめて縮める」。

行を左から右へブロック単位で見ていきます。手元に持つのは3つだけです。

新しいブロックから局所的な最大値 m~\tilde{m}、指数和 ℓ~\tilde{\ell}、重み付き和 P~V~\tilde{P}\tilde{V} を求めたら、次の3本で更新します。

m′=max⁡(m,m~),ℓ′=em−m′ℓ+em~−m′ℓ~,O~′=em−m′O~+em~−m′P~V~m' = \max(m, \tilde{m}), \quad \ell' = e^{m - m'}\ell + e^{\tilde{m} - m'}\tilde{\ell}, \quad \tilde{O}' = e^{m - m'}\tilde{O} + e^{\tilde{m} - m'}\tilde{P}\tilde{V}
(2)

言葉にすると、新しい最大値を決め、古い蓄積には em−m′e^{m - m'} を、新しいブロックには em~−m′e^{\tilde{m} - m'} を掛けて同じ土俵に揃えてから足す。最大値が更新されなければ em−m′=1e^{m-m'} = 1 で、古い分はそのままです。

核心は、縮小率 em−m′e^{m - m'} がスカラー1つだという点です。過去に足し込んだ何千項をひとつずつ直す必要はなく、蓄積したベクトル全体に1回掛ければ済む。だから「後で最大値が更新される」ことを恐れずに前から足していけます。全ブロックを見終わったら、最後に一度だけ O=O~/ℓO = \tilde{O}/\ell と割る。得られる OO は素朴な実装と数学的に等しく、近似は一切入りません。

この漸化式自体は2018年のオンラインsoftmaxの論文が示したもので、FlashAttentionはそれを VV との積まで巻き込み、GPUのメモリ階層に合わせて設計し直しました。

擬似コードで書くと

import numpy as np

def flash_attention(Q, K, V, Bc=128):        # Bc = 机に載るブロック幅
    N, d = Q.shape
    O = np.zeros((N, d))                      # 重み付き和の途中経過(未正規化)
    m = np.full(N, -np.inf)                   # 行ごとの最大値
    l = np.zeros(N)                           # 行ごとの指数和

    for j in range(0, N, Bc):                 # K/V を1ブロックずつ「机」に運ぶ
        Kj, Vj = K[j:j+Bc], V[j:j+Bc]
        S = Q @ Kj.T / np.sqrt(d)             # ここは N×Bc。N×N は作らない
        m_new = np.maximum(m, S.max(axis=1))
        alpha = np.exp(m - m_new)             # 古い蓄積を縮める率
        P = np.exp(S - m_new[:, None])
        l = alpha * l + P.sum(axis=1)
        O = alpha[:, None] * O + P @ Vj       # まとめて縮めて、足す
        m = m_new

    return O / l[:, None]                     # 最後に一度だけ割る

N × N の配列がどこにも現れないことを確かめてください。生きているのは N × Bc のブロックと、行ごとのスカラー2本だけです。実物のCUDAカーネルでは QQ 側もブロックに切り、ブロックの組を1つのSMに割り当てて SS をSRAMから出しません。

HBMの往復はどれだけ減るのか

論文はこれをIO計算量として定式化しています。素朴な実装のHBMアクセスは Θ(Nd+N2)\Theta(Nd + N^2)、FlashAttentionは Θ(N2d2M−1)\Theta(N^2 d^2 M^{-1})。MM はSRAMの大きさ(要素数)です。

つまりどちらも N2N^2 に比例するが、FlashAttentionには d2/Md^2/M という小さな係数が掛かる。dd が64〜128、MM が100KB相当という現実的な設定では d2d^2 は MM よりずっと小さく、往復が桁で減ります。オーダーが同じでも係数で桁が変わる——最適化の効き方としてはむしろ典型です。

FIG 2対数軸に切り替えると、N と N² が n を伸ばすにつれてどれだけ離れるかが見える。FlashAttentionが変えたのは曲線の形ではなく、N² に掛かる係数のほう

逆伝播で、わざと計算し直す

冒頭の逆説の正体はここです。逆伝播には PP が要ります。素朴な実装は順伝播でそれをHBMに保存し、逆伝播で読み直します。FlashAttentionは保存しません。保存するのは出力 OO と、行ごとの mm と ℓ\ell だけ——N×NN \times N ではなく NN に比例する量です。

逆伝播では Q,K,VQ, K, V と m,ℓm, \ell から、必要なブロックの PP をSRAM上でその場で作り直します。SS を計算する分の演算がまるまる増える。それでも全体は速い。増えたのは机の上の作業で、減ったのは倉庫との往復だからです。帯域律速の世界では、FLOPsは通貨ではありません。 通貨はバイトです。

論文が報告している値としては、GPT-2(系列長1K)の学習で約3倍、BERT-large(系列長512)ではMLPerf 1.1の学習速度記録に対して実時間で15%短縮、といったものが挙げられています。効き方は系列長・ヘッド次元・GPU世代に強く依存するので、自分のワークロードで測るのが前提です。

その後の版と、推論での事情

FlashAttention-2(2023)は行列積以外の演算を減らし、並列化の軸を系列長方向にも広げ、ワープ間の共有メモリ経由のやり取りを削りました。FlashAttention-3(2024)はHopper世代の非同期性——転送と行列積を重ねて走らせる仕組み——を使い、FP8のような低精度でも精度を保つ工夫を入れています。

推論では事情が一段変わります。生成中(デコード)のクエリは1トークン分しかないので、系列長方向で並列化する前提が崩れ、コアが遊びます。そこで K,VK, V 側を分割して部分結果を並列に求め、最後に結合する方式が使われます。この結合に使うのが、まさに上の3本の式です——部分ごとの m,ℓ,O~m, \ell, \tilde{O} を持ち寄って揃えて足せば、分割しても答えは変わりません。

なお、ここで減るのはスコア行列のメモリであって、KVキャッシュではありません。長文でメモリが足りなくなる主因はたいていKVキャッシュ側で、見積もり方はKVキャッシュを1から理解するにあります。この混同は現場で本当によく起きます。

現場ではこう使う

誰が、いつ触るか。 LLMの学習基盤・推論基盤のエンジニア、長文入力を売りにするプロダクトの担当者、そして独自のマスク形状や位置バイアスを実装する研究寄りのエンジニアです。前2者は「使う側」、最後は「既存カーネルが使えなくなる側」で、事故の種類が違います。

実際に触る名前。 PyTorchなら torch.nn.functional.scaled_dot_product_attention(SDPA)が入口で、内部にMATH/FLASH_ATTENTION/EFFICIENT_ATTENTION/CUDNN_ATTENTIONといったバックエンドを持ちます。どれが選ばれたかを固定・検証するには torch.nn.attention.sdpa_kernel を使います。より直接には flash-attn の flash_attn_func、可変長バッチ向けの flash_attn_varlen_func。推論スタックならvLLMやTensorRT-LLMが内部で呼んでいます。

知らないと事故になる落とし穴。

設計レビューで問われること。 「FLOPsが増えるのになぜ速いのか」——帯域律速なので通貨はバイトであり、SRAM上の再計算はHBM往復より安いから。「順伝播で保存するものは何か」——出力 OO と行ごとの m,ℓm, \ell、合わせて NN に比例する量だけ。この2問に自分の言葉で答えられれば、「速いらしいから使う」から「なぜ効くか分かって使う」に移れます。

まとめ

🎬 この内容の解説動画(Manim・字幕つき)

参考文献

  1. FlashAttention: Fast and Memory-Efficient Exact Attention with IO-Awareness. arXiv:2205.14135論文ページ·PDF
  2. FlashAttention-2: Faster Attention with Better Parallelism and Work Partitioning. arXiv:2307.08691論文ページ·PDF
  3. FlashAttention-3: Fast and Accurate Attention with Asynchrony and Low-precision. arXiv:2407.08608論文ページ·PDF
  4. Online normalizer calculation for softmax. arXiv:1805.02867論文ページ·PDF

本記事は上記論文の本文にもとづいて執筆しています。数値・主張は原典を優先してください。

運営: TechAthletes · 編集方針 · 出典の付け方 · 訂正の記録

コメント

コメントにはログインが必要です