LabHub

ブログ

Ring Attention 論文分析:分散環境での無限コンテキストウィンドウトレーニング実装

한국어English日本語中文

Ring Attention

はじめに

Transformer アーキテクチャの Self-Attention は、シーケンス内のすべてのトークンのペア間の関係を計算する強力なメカニズムだが、シーケンス長 LL に対して O(L2)O(L^2) のメモリと計算量を要するという根本的な限界がある。単一 GPU のメモリが 80GB(A100)または 141GB(H200)という水準の現実のもとでは、数百万トークン規模のコンテキストを単一デバイスで処理することはほぼ不可能である。

この問題を解決するために、FlashAttention、Sparse Attention、Linear Attention など多様なアプローチが提案されてきた。FlashAttention は IO-awareness によって単一デバイス内のメモリ効率を極限まで高めたが、依然として単一デバイスの HBM 容量という物理的な限界に縛られている。一方 Sparse Attention や Linear Attention は近似(approximation)によって計算量を減らすが、正確なアテンション計算を諦めるという代償を伴う。

2023年10月、UC Berkeley の Hao Liu、Matei Zaharia、Pieter Abbeel が発表した Ring Attention with Blockwise Transformers for Near-Infinite Context という論文は、まったく異なる観点からこの問題に切り込んだ。アテンション計算の精度をまったく損なうことなく、多数のデバイスにシーケンスを分散し、通信と計算を完全にオーバーラップさせる方法を示したのである。核心となるアイデアは、デバイスを論理的なリング(ring)トポロジーで接続し、Key-Value ブロックを循環させながら Blockwise Parallel Transformer のブロック単位アテンション計算を実行することだ。

このアプローチによって、コンテキスト長はデバイス数に比例して線形にスケールする。32台の A100 GPU で 7B モデルのコンテキストを 100万トークン以上に伸ばすことができ、TPUv4-1024 では 3B モデルで 1,600万トークンまで処理した結果が報告されている。ICLR 2024 に採択されたこの論文は、分散環境における長文コンテキスト学習のパラダイムを根本から変えた。

本記事では、Ring Attention 論文の理論的な基盤である Blockwise Parallel Transformer から、Ring Attention の中核アルゴリズム、分散通信の設計、PyTorch/JAX 実装の詳細、ベンチマークの分析、他の並列化戦略との比較、そして実戦適用で生じる限界と失敗事例までを包括的に分析する。

先行研究:Blockwise Parallel Transformer

Ring Attention を理解するには、同じ著者が先に発表した Blockwise Parallel Transformer(BPT)をまず理解する必要がある。BPT は Ring Attention の単一デバイス版と見なすことができ、ブロック単位アテンション計算の数学的な正当性を与える。この先行研究がなければ Ring Attention の分散拡張は不可能だっただろう。二つの論文は一つの研究の流れとしてつながっている。

標準 Self-Attention のメモリ問題

標準の Self-Attention では、Query、Key、Value 行列の全体をメモリに載せたうえで、アテンションスコア行列 S=QKT/dkS = QK^T / \sqrt{d_k} を一度に計算する。このスコア行列のサイズは L×LL \times L であるため、シーケンス長が伸びるとメモリ使用量は二次的に増加する。fp16 で 16K トークンのときに約 512MB だったスコア行列が、128K トークンでは約 32GB へ膨れ上がる。これは、モデルの重みやオプティマイザの状態とは別に、アテンション計算そのものだけでデバイスメモリのかなりの部分を消費することを意味する。特に学習時は逆伝播のためにアテンションスコアを保存する必要があるため、メモリ負荷は推論より 2-3 倍大きい。

ブロック単位の分割戦略

BPT の核心は、アテンション計算の全体を独立したブロック単位に分割しつつ、最終結果が元の正確な(exact)アテンションと数学的に等しくなることを保証する点にある。シーケンスをサイズ BB のブロックに分けると、Query ブロック QiQ_i に対してすべての Key-Value ブロック (Kj,Vj)(K_j, V_j) との部分アテンションを計算し、それらを正確に足し合わせることができる。

この加算の過程で核心となるのが オンラインソフトマックス(Online Softmax) の手法である。FlashAttention でも使われているこの手法により、アテンションスコアの全体を一度に見なくても、ブロック単位で段階的に正確なソフトマックス結果を計算できる。

import torch
import torch.nn.functional as F

def blockwise_attention(Q: torch.Tensor, K: torch.Tensor, V: torch.Tensor,
                        block_size: int) -> torch.Tensor:
    """Blockwise アテンション計算(Online Softmax ベース)。

    アテンションスコア行列の全体を一度に計算せず、
    ブロック単位で段階的に正確な結果を累積する。

    Args:
        Q: Query テンソル [batch, seq_len, d_k]
        K: Key テンソル [batch, seq_len, d_k]
        V: Value テンソル [batch, seq_len, d_v]
        block_size: ブロックサイズ

    Returns:
        Output テンソル [batch, seq_len, d_v]
    """
    batch, seq_len, d_k = Q.shape
    d_v = V.shape[-1]
    scale = d_k ** -0.5
    num_blocks = seq_len // block_size

    output = torch.zeros(batch, seq_len, d_v, device=Q.device, dtype=Q.dtype)

    for i in range(num_blocks):
        q_block = Q[:, i * block_size:(i + 1) * block_size, :]  # [B, block_size, d_k]

        # オンラインソフトマックス用の累積変数
        max_score = torch.full((batch, block_size, 1), float('-inf'), device=Q.device)
        sum_exp = torch.zeros(batch, block_size, 1, device=Q.device)
        acc = torch.zeros(batch, block_size, d_v, device=Q.device)

        for j in range(num_blocks):
            k_block = K[:, j * block_size:(j + 1) * block_size, :]
            v_block = V[:, j * block_size:(j + 1) * block_size, :]

            # 部分アテンションスコアの計算
            scores = torch.bmm(q_block, k_block.transpose(-2, -1)) * scale  # [B, bs, bs]

            # オンラインソフトマックスの更新
            new_max = torch.maximum(max_score, scores.max(dim=-1, keepdim=True).values)
            correction = torch.exp(max_score - new_max)
            new_exp = torch.exp(scores - new_max)

            # これまでの累積値を補正し、新しいブロックを反映
            sum_exp = sum_exp * correction + new_exp.sum(dim=-1, keepdim=True)
            acc = acc * correction + torch.bmm(new_exp, v_block)
            max_score = new_max

        # 最終的な正規化
        output[:, i * block_size:(i + 1) * block_size, :] = acc / sum_exp

    return output

上のコードで核心となるのは correction 項である。新しいブロックの最大スコアがそれまでの最大スコアより大きいとき、それまでに累積した指数和と加重和を新しいスケールに合わせて補正する。この補正の過程は、数学的にシーケンス全体に対する正確なソフトマックスと同じ結果を保証する。

BPT の Feedforward 融合

BPT はアテンションのブロック計算にとどまらず、Feedforward Network(FFN)の計算までブロック単位で融合する。つまり、Query ブロック QiQ_i に対するアテンション結果を得た直後に、その結果へすぐ FFN を適用して当該ブロックの最終出力を完成させる。これにより、アテンション出力の全体をメモリに保存する必要なく、ブロック単位で FFN まで処理して結果を書き出せる。

class BlockwiseParallelTransformerLayer(torch.nn.Module):
    """BPT レイヤー: アテンションと FFN をブロック単位で融合処理する。"""

    def __init__(self, d_model: int, n_heads: int, d_ff: int):
        super().__init__()
        self.d_model = d_model
        self.n_heads = n_heads
        self.d_k = d_model // n_heads

        self.W_q = torch.nn.Linear(d_model, d_model)
        self.W_k = torch.nn.Linear(d_model, d_model)
        self.W_v = torch.nn.Linear(d_model, d_model)
        self.W_o = torch.nn.Linear(d_model, d_model)

        self.ffn = torch.nn.Sequential(
            torch.nn.Linear(d_model, d_ff),
            torch.nn.GELU(),
            torch.nn.Linear(d_ff, d_model),
        )
        self.norm1 = torch.nn.LayerNorm(d_model)
        self.norm2 = torch.nn.LayerNorm(d_model)

    def forward(self, x: torch.Tensor, block_size: int = 1024) -> torch.Tensor:
        batch, seq_len, _ = x.shape
        num_blocks = seq_len // block_size

        Q = self.W_q(x)
        K = self.W_k(x)
        V = self.W_v(x)

        output = torch.zeros_like(x)

        for i in range(num_blocks):
            start, end = i * block_size, (i + 1) * block_size
            q_block = Q[:, start:end, :]

            # ブロックごとのアテンション(オンラインソフトマックス)
            attn_out = self._blockwise_attn(q_block, K, V, block_size)
            attn_out = self.W_o(attn_out)

            # 残差接続 + レイヤー正規化
            block_input = x[:, start:end, :]
            normed = self.norm1(block_input + attn_out)

            # FFN を即座に適用(メモリ削減の核心)
            ffn_out = self.ffn(normed)
            output[:, start:end, :] = self.norm2(normed + ffn_out)

        return output

この構造のメモリ使用量は、シーケンス全体の長さではなくブロックサイズによって決まる。シーケンスが 100万トークンであっても 1,000万トークンであっても、各時点でメモリに保持するのは現在処理中のブロックと循環中の KV ブロックだけである。これが、BPT が従来のメモリ効率的な Transformer に比べて最大 32倍長いコンテキストを扱える根本的な理由である。

BPT のもう一つの重要な貢献は、アテンションと FFN の計算順序を組み替えてメモリアクセスパターンを最適化した点にある。従来の Transformer はシーケンス全体に対するアテンションを終えてから FFN を適用するが、BPT は各ブロックについてアテンションと FFN を連続して処理する。これにより中間結果のメモリ寿命がブロックサイズに制限され、ピークメモリ使用量が大きく減少する。GPU の SRAM(L1/L2 キャッシュ)と HBM 間のデータ移動も最小化され、IO 効率が向上する。

Ring Attention の中核アルゴリズム

リングトポロジーと KV の循環

Ring Attention は、BPT のブロック単位アテンション計算を多数のデバイスに分散させる。NN 個のデバイスが論理的なリング状に接続されていると仮定しよう。シーケンス全体を NN 個のチャンクに分けて各デバイスに割り当てる。デバイス ii は、シーケンスの ii 番目のチャンクに対応する Query ブロック QiQ_i、Key ブロック KiK_i、Value ブロック ViV_i を保持する。

アルゴリズムの中核となる動作は次のとおりである。

  1. 初期状態: 各デバイス ii は、自分のローカル KV ブロック (Ki,Vi)(K_i, V_i) に対して部分アテンションを計算する。
  2. KV の循環: 各デバイスは現在保持している KV ブロックをリングの次のデバイスへ送信し、前のデバイスから新しい KV ブロックを受信する。
  3. 計算と通信のオーバーラップ: 送受信が進む間に、現在保持している KV ブロックに対するアテンション計算を実行する。アテンションの計算時間が通信時間以上であれば、通信オーバーヘッドは完全に隠蔽される。
  4. 繰り返し: N1N-1 回の循環が完了すると、各デバイスの QiQ_i はシーケンス全体のすべての KV ブロックを参照したことになり、正確な Full Attention の結果が得られる。

以下は、Ring Attention の中核ループを PyTorch の分散通信プリミティブで実装した擬似コードである。

import torch
import torch.distributed as dist

def ring_attention_forward(
    Q_local: torch.Tensor,   # このデバイスの Query ブロック [batch, chunk_len, d_k]
    K_local: torch.Tensor,   # このデバイスの Key ブロック
    V_local: torch.Tensor,   # このデバイスの Value ブロック
    rank: int,               # 現在のデバイスのランク
    world_size: int,         # デバイスの総数
    scale: float,            # スケールファクタ 1/sqrt(d_k)
) -> torch.Tensor:
    """Ring Attention の順伝播パス。

    KV ブロックをリングトポロジーで循環させながら、ブロック単位アテンションを累積計算する。
    通信と計算を非同期にオーバーラップさせ、通信オーバーヘッドを隠蔽する。
    """
    batch, chunk_len, d_k = Q_local.shape
    d_v = V_local.shape[-1]

    # オンラインソフトマックスの累積変数
    max_score = torch.full((batch, chunk_len, 1), float('-inf'), device=Q_local.device)
    sum_exp = torch.zeros(batch, chunk_len, 1, device=Q_local.device)
    acc = torch.zeros(batch, chunk_len, d_v, device=Q_local.device)

    # いま処理する KV ブロック(初期値はローカルブロック)
    kv_current = (K_local.clone(), V_local.clone())
    # 受信バッファ
    kv_recv = (torch.empty_like(K_local), torch.empty_like(V_local))

    # リング上の隣接デバイスを計算
    send_to = (rank + 1) % world_size
    recv_from = (rank - 1) % world_size

    for step in range(world_size):
        K_block, V_block = kv_current

        # 最後のステップでなければ非同期通信を開始
        if step < world_size - 1:
            send_ops = [
                dist.isend(K_block, dst=send_to),
                dist.isend(V_block, dst=send_to),
            ]
            recv_ops = [
                dist.irecv(kv_recv[0], src=recv_from),
                dist.irecv(kv_recv[1], src=recv_from),
            ]

        # 現在の KV ブロックに対する部分アテンション計算(通信と同時に実行)
        scores = torch.bmm(Q_local, K_block.transpose(-2, -1)) * scale

        # オンラインソフトマックスの更新
        block_max = scores.max(dim=-1, keepdim=True).values
        new_max = torch.maximum(max_score, block_max)
        correction = torch.exp(max_score - new_max)
        new_exp = torch.exp(scores - new_max)

        sum_exp = sum_exp * correction + new_exp.sum(dim=-1, keepdim=True)
        acc = acc * correction + torch.bmm(new_exp, V_block)
        max_score = new_max

        # 通信の完了を待ってからバッファを入れ替え
        if step < world_size - 1:
            for op in send_ops + recv_ops:
                op.wait()
            kv_current = (kv_recv[0].clone(), kv_recv[1].clone())

    # 最終的な正規化
    output = acc / sum_exp
    return output

計算と通信のオーバーラップ条件

Ring Attention の効率は、通信時間が計算時間以下のときに最大化される。この条件を数式で表すと次のようになる。

ブロックサイズ BB、モデル次元 dd に対して、ブロックアテンションの計算量は O(B2d)O(B^2 \cdot d) FLOPs である。一方、KV ブロック一組の通信量は 2Bd2 \cdot B \cdot d 要素(Key と Value のそれぞれ)である。デバイス間の帯域幅を β\beta (bytes/s)、計算スループットを γ\gamma (FLOPs/s) とすると、オーバーラップ条件は次のとおりである。

2Bdsizeof(dtype)β2B2dγ\frac{2 \cdot B \cdot d \cdot \text{sizeof(dtype)}}{\beta} \leq \frac{2 \cdot B^2 \cdot d}{\gamma}

これを整理すると、ブロックサイズの下限は次のようになる。

Bγsizeof(dtype)βB \geq \frac{\gamma \cdot \text{sizeof(dtype)}}{\beta}

A100 GPU で NVLink(600 GB/s)を使い bf16 演算(312 TFLOPS)を行う場合、B312×1012×2/(600×109)1024B \geq 312 \times 10^{12} \times 2 / (600 \times 10^9) \approx 1024 となり、ブロックサイズが 1024 トークン以上であれば通信は完全に隠蔽される。ノード間で InfiniBand(400 Gbps = 50 GB/s)を使う場合は B312×1012×2/(50×109)12,480B \geq 312 \times 10^{12} \times 2 / (50 \times 10^9) \approx 12,480 となり、ブロックサイズを大きく引き上げる必要がある。

Causal Masking の処理

自己回帰(autoregressive)モデルでは、未来のトークンへのアテンションを遮断する因果マスキング(causal masking)が不可欠である。Ring Attention でこれを扱う際には重要な最適化がある。デバイス ii が、元のシーケンス上で自分の Query ブロックより後ろに位置する KV ブロックを受信した場合、そのブロック全体がマスクされるため、計算を完全にスキップできる。

def ring_attention_causal_step(
    Q_local: torch.Tensor,
    K_block: torch.Tensor,
    V_block: torch.Tensor,
    q_block_idx: int,      # 元のシーケンスにおける Query ブロックのインデックス
    kv_block_idx: int,     # 元のシーケンスにおける現在の KV ブロックのインデックス
    block_size: int,
    scale: float,
) -> tuple:
    """Causal masking を適用した Ring Attention の単一ステップ。

    KV ブロックの位置に応じて、フル計算・部分マスキング・完全スキップを決める。
    """
    if kv_block_idx > q_block_idx:
        # KV ブロックが Query より未来に位置する -> 完全にスキップ
        return None, None, None

    scores = torch.bmm(Q_local, K_block.transpose(-2, -1)) * scale

    if kv_block_idx == q_block_idx:
        # 同じブロック内にのみ部分的な causal masking を適用
        chunk_len = Q_local.shape[1]
        causal_mask = torch.triu(
            torch.ones(chunk_len, chunk_len, device=Q_local.device, dtype=torch.bool),
            diagonal=1
        )
        scores = scores.masked_fill(causal_mask.unsqueeze(0), float('-inf'))

    # kv_block_idx < q_block_idx: マスキングは不要(すべて過去のトークン)

    block_max = scores.max(dim=-1, keepdim=True).values
    exp_scores = torch.exp(scores - block_max)
    block_sum = exp_scores.sum(dim=-1, keepdim=True)
    block_out = torch.bmm(exp_scores, V_block)

    return block_out, block_max, block_sum

この最適化により、causal 設定では平均しておよそ 50% の計算量が削減される。合計 NN 個のデバイスそれぞれが NN 個の KV ブロックを循環処理するが、causal masking によっておよそ半分のブロックを完全にスキップできるからである。ただし、この削減効果はデバイス間の計算量の不均衡も引き起こす点に注意が必要である。シーケンスの前方を担当するデバイスはほとんどすべての KV ブロックを処理する一方、後方を担当するデバイスは大半のブロックをスキップして遊休状態が長くなる。

アーキテクチャ設計の詳細

リング通信パターン

Ring Attention の通信パターンは次のように動作する。N=4N=4 デバイスの環境を例に説明する。

ステップ0(初期状態):

ステップ1: 各デバイスが KV を次のデバイスへ送信

ステップ2と3 も同じパターンで進み、合計 N=4N=4 ステップの後には各デバイスの Query がシーケンス全体のすべての KV を参照したことになる。

このパターンの重要な性質は、各ステップですべてのデバイスが同時にちょうど一つの KV ブロックを送信し、一つを受信するという点である。したがって帯域幅の利用は均等で、AllReduce と違ってネットワークのボトルネックが生じない。リングトポロジーは分散システムのなかで最も単純でありながら帯域幅の利用効率が高い通信パターンの一つであり、AllReduce アルゴリズムの土台となる Ring-AllReduce でも同じ原理が使われている。

また、各デバイスが処理すべき通信ラウンドの総数がちょうど N1N-1 である点も重要である。これはシーケンス全体の長さやブロックサイズとは無関係に、デバイス数だけで決まる定数である。各ラウンドで交換されるデータサイズが一定なので通信時間の全体は予測可能であり、これが計算と通信のオーバーラップのスケジューリングを容易にする。

メモリ使用量の分析

Ring Attention における各デバイスのメモリ使用量を分析すると次のようになる。

メモリ使用量の合計は O(B×d)O(B \times d) であり、シーケンス全体の長さ L=N×BL = N \times B に対しては O(L/N×d)O(L/N \times d) となる。これは単一デバイスでの O(L2)O(L^2) や FlashAttention での O(L)O(L) に比べ、デバイス数 NN に比例して小さくなる。

モデルパラメータとオプティマイザの状態まで含めた実際のメモリ予算を考えると、A100 80GB を前提に 7B モデルの場合、アテンションに使えるメモリはおよそ 20-30GB である。ブロックサイズ B=8192B=8192、モデル次元 d=4096d=4096、bf16 を前提とすると、KV バッファのメモリは約 4×8192×4096×2=5124 \times 8192 \times 4096 \times 2 = 512MB 程度であり、単一デバイスのメモリ上限の内側で十分に処理できる。

このメモリ分析は、Ring Attention がなぜ単一デバイスのメモリ効率化手法(FlashAttention)と相互補完的なのかを示している。各デバイスの内部では FlashAttention のタイリングと再計算の戦略を適用して HBM 使用量を最小化し、デバイス間では Ring Attention の分散循環戦略を適用してシーケンス全体を分配する。二つの手法を組み合わせることで、単一デバイスの物理的なメモリ限界と単一デバイスの計算処理の限界を同時に乗り越えられる。

Backward Pass と Gradient の計算

逆伝播での KV 再循環

Ring Attention の逆伝播の過程でも、Forward Pass と同じリング循環パターンが適用される。Forward で保存しておいた softmax 統計(max_score, sum_exp)を用いて、各ブロックに対する gradient を正確に計算する。

逆伝播で注意すべき点は KV ブロックの循環方向である。Forward で KV を順方向(rank -> rank+1)に循環させたのであれば、Backward でも同じ順序で KV ブロックを再び循環させながら gradient を計算する。この過程で dK と dV に対する gradient は各デバイスで部分的に計算されたのち、その KV ブロックを元々保有していたデバイスに集約されなければならない。

def ring_attention_backward(
    dO_local: torch.Tensor,     # 出力の gradient [batch, chunk_len, d_v]
    Q_local: torch.Tensor,      # 保存された Query
    K_local: torch.Tensor,      # ローカルの Key
    V_local: torch.Tensor,      # ローカルの Value
    O_local: torch.Tensor,      # Forward の出力
    lse_local: torch.Tensor,    # log-sum-exp(オンラインソフトマックス統計)
    rank: int,
    world_size: int,
    scale: float,
) -> tuple:
    """Ring Attention の逆伝播。

    Forward と同じ KV 循環パターンで dQ, dK, dV を計算する。
    dK, dV は元々の保有デバイスに集約される。
    """
    batch, chunk_len, d_k = Q_local.shape
    d_v = V_local.shape[-1]

    dQ = torch.zeros_like(Q_local)
    dK_local = torch.zeros_like(K_local)
    dV_local = torch.zeros_like(V_local)

    # D ベクトルの事前計算: rowsum(dO * O)
    D = (dO_local * O_local).sum(dim=-1, keepdim=True)  # [batch, chunk_len, 1]

    kv_current = (K_local.clone(), V_local.clone())
    send_to = (rank + 1) % world_size
    recv_from = (rank - 1) % world_size

    for step in range(world_size):
        K_block, V_block = kv_current

        # アテンションスコアの再計算(チェックポインティングのため Forward では保存しない)
        scores = torch.bmm(Q_local, K_block.transpose(-2, -1)) * scale
        P = torch.exp(scores - lse_local)  # 正規化されたアテンション重み

        # Gradient の計算
        dV_block = torch.bmm(P.transpose(-2, -1), dO_local)
        dP = torch.bmm(dO_local, V_block.transpose(-2, -1))
        dS = P * (dP - D) * scale
        dQ += torch.bmm(dS, K_block)
        dK_block = torch.bmm(dS.transpose(-2, -1), Q_local)

        # dK, dV を元々の保有デバイスへ送信
        source_rank = (rank - step) % world_size
        if source_rank == rank:
            dK_local += dK_block
            dV_local += dV_block
        else:
            dist.reduce(dK_block, dst=source_rank, op=dist.ReduceOp.SUM)
            dist.reduce(dV_block, dst=source_rank, op=dist.ReduceOp.SUM)

        # 次のステップのための KV 循環
        if step < world_size - 1:
            kv_recv = (torch.empty_like(K_block), torch.empty_like(V_block))
            send_ops = [dist.isend(K_block, dst=send_to), dist.isend(V_block, dst=send_to)]
            recv_ops = [dist.irecv(kv_recv[0], src=recv_from), dist.irecv(kv_recv[1], src=recv_from)]
            for op in send_ops + recv_ops:
                op.wait()
            kv_current = kv_recv

    return dQ, dK_local, dV_local

チェックポインティング戦略

Ring Attention でメモリ効率の良い学習を行うには、Gradient Checkpointing(再計算、rematerialization)が不可欠である。Forward Pass で各ブロックのアテンションスコア行列を保存すると O(N×B2)O(N \times B^2) のメモリが必要になり、メモリ削減の効果が相殺されてしまう。そこで Forward ではオンラインソフトマックスの統計値(max_score, log-sum-exp)だけを保存し、Backward でアテンションスコアを再計算する。これは FlashAttention と同じ戦略である。

チェックポインティングの代償は計算量の増加である。Backward でアテンションスコアを再計算するため、全体の FLOPs はおよそ 33% 増える。しかしメモリ削減の効果のほうがはるかに大きいので、長いコンテキストの学習ではこのトレードオフは十分に正当化される。実務では PyTorch の torch.utils.checkpoint や JAX の jax.checkpoint を使い、Transformer レイヤー単位でチェックポインティングを適用するのが一般的である。Ring Attention の場合はリング循環のステップ単位でもチェックポインティングを適用できるため、メモリと計算量のきめ細かな制御が可能である。

性能ベンチマークの分析

論文が報告した結果

Ring Attention 論文で報告された中核的なベンチマーク結果をまとめると次のとおりである。

設定モデルサイズデバイス達成コンテキスト長スケール倍率
A100 32台7B32x A100 80GB1,000,000+ トークン32x(従来比)
TPUv4-10243B1024 TPUv4 チップ16,000,000 トークン512x(従来比)
A100 8台7B8x A100 80GB262,144 トークン8x

注目すべきは、「従来比」の基準がメモリ効率的な Transformer(FlashAttention など)だという点である。Ring Attention 以前も単一デバイスで FlashAttention を使えばおよそ 32K-64K トークンまで扱えたが、Ring Attention はそれをデバイス数に正確に比例させて拡張した。この線形なスケーリング特性は、理論上可能というだけでなく実際のハードウェアで測定された結果である点に意義がある。デバイスを追加するほど扱えるコンテキスト長が比例して増えるため、インフラ投資に対する性能の予測が容易になる。

後続研究のベンチマーク

Ring Attention の概念を拡張した後続研究の性能も注目に値する。RingX(2024)は Frontier スーパーコンピュータで 4,096個の GPU を使い、Llama3 8B モデルを 100万トークンのコンテキストで学習しながら 38% の Model FLOPs Utilization(MFU)を達成した。これは長いコンテキストの学習で報告されている最高水準の学習効率である。

Meta の Context Parallelism 研究では、Llama3 405B モデルの 100万トークンのプリフィルを 77秒で完了し、93% の並列化効率と 63% の FLOPS 利用率を達成した。128K コンテキストのプリフィルは 3.8秒で処理された。

通信オーバーヘッドの実測

論文が強調する「ゼロオーバーヘッド通信」が実際に達成される条件を分析すると次のようになる。

接続タイプ帯域幅最小ブロックサイズ (bf16, d=4096)実測オーバーヘッド
NVLink(ノード内)600 GB/s~1,024 トークン0-2%
PCIe Gen564 GB/s~9,750 トークン5-15%
InfiniBand HDR50 GB/s~12,480 トークン10-25%
Ethernet 100G12.5 GB/s~49,920 トークン30-60%

ノード内の NVLink 環境では、ブロックサイズが 1024 以上であれば通信はほぼ完璧に隠蔽される。しかしノード間の通信ではブロックサイズを大きく引き上げる必要があり、Ethernet 環境では事実上、効率的な Ring Attention は難しい。この結果は、Ring Attention の配備戦略を立てるうえでネットワークトポロジーが決定的に重要な設計変数であることを示唆している。クラウド環境で GPU クラスタを構成する際はノード間の帯域幅の仕様を慎重に選ぶ必要があり、可能であれば NVLink または NVSwitch ベースの単一ノード・マルチ GPU 構成を優先的に検討するのが望ましい。

比較分析: Ring Attention vs Sequence Parallelism vs Tensor Parallelism

分散環境で長いコンテキストを扱うための三つの主要な並列化戦略を比較する。

特性Ring AttentionSequence Parallelism (DeepSpeed-Ulysses)Tensor Parallelism
分割対象シーケンス次元(アテンション全体)シーケンス次元(アテンションヘッド基準)モデル次元(重みの分割)
通信パターンP2P Ring (Send/Recv)All-to-AllAllReduce
通信量O(Bd)O(B \cdot d) per stepO(Ld/N)O(L \cdot d / N) per layerO(Bd)O(B \cdot d) per layer
アテンション精度Exact(近似なし)Exact(近似なし)Exact(近似なし)
最大並列度デバイス数の制限なしアテンションヘッド数に制限されるアテンションヘッド数に制限される
GQA/MQA 互換性完全に互換限定的(ヘッド数が不足)限定的
通信と計算のオーバーラップ可能(中核となる設計)不可(同期 All-to-All)不可(同期 AllReduce)
ノード間のスケーラビリティブロックサイズの条件を満たせば良好All-to-All の帯域幅に依存AllReduce の帯域幅に依存
実装の複雑さ高い中程度低い
メモリ効率非常に高い(O(Bd)O(B \cdot d)高い(O(L/Nd)O(L/N \cdot d)モデルサイズに比例

中核となる差別化ポイントの分析

Ring Attention の優位: アテンションヘッド数に制限されない点が最大の差別化ポイントである。DeepSpeed-Ulysses はシーケンスをアテンションヘッド数の分だけしか分割できないため、GQA(Grouped Query Attention)で Key-Value ヘッドが 8個の場合は最大でも 8-way 並列しかできない。Ring Attention にはこうした制約がない。

DeepSpeed-Ulysses の優位: ノード内の環境では All-to-All 通信が非常に効率的なので、ヘッド数が十分であれば Ring Attention より高いスループットを示す。NVSwitch ベースのシステムでは、All-to-All 通信が P2P Send/Recv より効率的なことがある。

ハイブリッドなアプローチ: 最近の研究では、二つの手法を組み合わせた USP(Unified Sequence Parallelism)が提案された。ノード内では Ulysses の All-to-All を使い、ノード間では Ring Attention の P2P 循環を使う 2D シーケンス並列化の戦略である。

import torch.distributed as dist

def hybrid_ulysses_ring_attention(
    Q: torch.Tensor,
    K: torch.Tensor,
    V: torch.Tensor,
    intra_node_group: dist.ProcessGroup,  # ノード内グループ(Ulysses)
    inter_node_group: dist.ProcessGroup,  # ノード間グループ(Ring)
    n_heads: int,
) -> torch.Tensor:
    """USP: Ulysses + Ring Attention のハイブリッドなシーケンス並列化。

    ノード内では All-to-All ベースの Ulysses でヘッド次元を分割し、
    ノード間では Ring Attention でシーケンス次元を分割する。
    """
    intra_size = dist.get_world_size(intra_node_group)
    inter_size = dist.get_world_size(inter_node_group)
    intra_rank = dist.get_rank(intra_node_group)
    inter_rank = dist.get_rank(inter_node_group)

    # Step 1: Ulysses All-to-All(ノード内)
    # シーケンス次元の分割 -> ヘッド次元の分割へ再配置
    Q_heads = all_to_all_reshape(Q, intra_node_group, split_dim='seq', gather_dim='head')
    K_heads = all_to_all_reshape(K, intra_node_group, split_dim='seq', gather_dim='head')
    V_heads = all_to_all_reshape(V, intra_node_group, split_dim='seq', gather_dim='head')

    # Step 2: Ring Attention(ノード間)
    # 各ノードのデバイスが自分のヘッドについて Ring Attention を実行
    output_heads = ring_attention_forward(
        Q_heads, K_heads, V_heads,
        rank=inter_rank,
        world_size=inter_size,
        scale=(Q.shape[-1]) ** -0.5,
    )

    # Step 3: 逆方向の All-to-All(ノード内)
    # ヘッド次元の分割 -> シーケンス次元の分割へ復元
    output = all_to_all_reshape(output_heads, intra_node_group, split_dim='head', gather_dim='seq')

    return output

JAX ベースの公式実装の分析

Ring Attention の公式実装は JAX/Flax ベースで、中核となる分散通信に jax.lax.ppermute を活用している。ppermute は JAX の collective operation で、デバイス間の順列(permutation)に従ってデータを同時に交換する機能を提供する。これにより、リングトポロジーの循環通信を単一の関数呼び出しで実装できる。

# JAX ベースの Ring Attention 中核実装(公式コードを参照)
import jax
import jax.numpy as jnp
from jax import lax

def ring_attention_jax(
    q: jnp.ndarray,    # [batch, chunk_len, n_heads, d_k]
    k: jnp.ndarray,    # [batch, chunk_len, n_heads, d_k]
    v: jnp.ndarray,    # [batch, chunk_len, n_heads, d_v]
    axis_name: str,     # pmap の軸名
    scale: float,
    causal: bool = True,
    block_size: int = 1024,
) -> jnp.ndarray:
    """JAX pmap 環境での Ring Attention 実装。

    lax.ppermute を使って KV ブロックをリング循環させる。
    """
    axis_size = lax.psum(1, axis_name)
    axis_index = lax.axis_index(axis_name)

    def scan_fn(carry, step):
        acc, max_score, sum_exp, k_block, v_block = carry

        # 現在の KV ブロックの元シーケンスにおけるインデックス
        kv_idx = (axis_index - step) % axis_size

        # Causal masking のチェック
        if causal:
            should_compute = kv_idx <= axis_index
        else:
            should_compute = True

        # 部分アテンションの計算
        scores = jnp.einsum('bqhd,bkhd->bqhk', q, k_block) * scale

        if causal and kv_idx == axis_index:
            # 同一ブロック: 対角の causal mask を適用
            chunk_len = q.shape[1]
            mask = jnp.triu(jnp.ones((chunk_len, chunk_len)), k=1).astype(bool)
            scores = jnp.where(mask[None, :, None, :], -1e9, scores)

        # オンラインソフトマックスの更新
        new_max = jnp.maximum(max_score, scores.max(axis=-1, keepdims=True))
        correction = jnp.exp(max_score - new_max)
        new_exp = jnp.exp(scores - new_max)

        sum_exp = jnp.where(should_compute, sum_exp * correction + new_exp.sum(axis=-1, keepdims=True), sum_exp)
        acc = jnp.where(should_compute, acc * correction + jnp.einsum('bqhk,bkhd->bqhd', new_exp, v_block), acc)
        max_score = jnp.where(should_compute, new_max, max_score)

        # KV ブロックをリングの次のデバイスへ循環
        # ppermute: (src, dst) のペアに従ってデータを交換
        perm = [(i, (i + 1) % axis_size) for i in range(axis_size)]
        k_block = lax.ppermute(k_block, axis_name, perm=perm)
        v_block = lax.ppermute(v_block, axis_name, perm=perm)

        return (acc, max_score, sum_exp, k_block, v_block), None

    # 初期化
    batch, chunk_len, n_heads, d_v = v.shape
    init_acc = jnp.zeros((batch, chunk_len, n_heads, d_v))
    init_max = jnp.full((batch, chunk_len, n_heads, 1), -1e9)
    init_sum = jnp.zeros((batch, chunk_len, n_heads, 1))

    init_carry = (init_acc, init_max, init_sum, k, v)
    (acc, max_score, sum_exp, _, _), _ = lax.scan(scan_fn, init_carry, jnp.arange(axis_size))

    return acc / sum_exp

JAX 実装の中核的な利点は、lax.ppermute が XLA コンパイラによってハードウェアレベルで最適化された P2P 通信に変換されることである。TPU 環境では ICI(Inter-Chip Interconnect)を通じて極めて低いレイテンシでデータが交換され、これが Ring Attention が TPU で特に高い効率を示す理由である。

実戦適用: 長文コンテキスト学習パイプライン

Progressive Context Extension

Ring Attention を活用した実戦の学習では、最初から最大のコンテキスト長で学習することはしない。段階的コンテキスト拡張(Progressive Context Extension)の戦略が、学習の安定性と効率の両面で優れている。

import torch
from dataclasses import dataclass
from typing import List

@dataclass
class ContextSchedule:
    """段階的コンテキスト拡張のスケジュール設定。"""
    context_lengths: List[int]     # 段階ごとのコンテキスト長
    warmup_steps: List[int]        # 各段階の学習ステップ数
    rope_theta_values: List[float] # 各段階の RoPE theta 値

    def get_config(self, global_step: int) -> dict:
        cumulative = 0
        for i, steps in enumerate(self.warmup_steps):
            cumulative += steps
            if global_step < cumulative:
                return {
                    'context_length': self.context_lengths[i],
                    'rope_theta': self.rope_theta_values[i],
                    'stage': i,
                }
        return {
            'context_length': self.context_lengths[-1],
            'rope_theta': self.rope_theta_values[-1],
            'stage': len(self.context_lengths) - 1,
        }

# 4K -> 16K -> 64K -> 256K -> 1M の段階的拡張の例
schedule = ContextSchedule(
    context_lengths=[4096, 16384, 65536, 262144, 1048576],
    warmup_steps=[1000, 800, 600, 400, 200],
    rope_theta_values=[10000, 50000, 500000, 5000000, 50000000],
)

# 学習ループで使用
for step in range(3000):
    config = schedule.get_config(step)
    ctx_len = config['context_length']
    n_devices = torch.cuda.device_count()
    chunk_per_device = ctx_len // n_devices

    print(f"Step {step}: context={ctx_len}, "
          f"chunk/device={chunk_per_device}, "
          f"RoPE theta={config['rope_theta']:.0f}, "
          f"stage={config['stage']}")

この段階的な拡張戦略では、RoPE(Rotary Position Embedding)の theta 値も併せて調整することが重要である。コンテキスト長が伸びると、位置エンコーディングの周波数帯域を広げなければ遠距離の位置関係を正確に表現できない。YaRN や LongRoPE といった手法がこの目的で使われる。

長文データの前処理とチャンク割り当て

Ring Attention の学習におけるデータ前処理は、単純なトークナイズにとどまらず、多数の文書を一つの長いシーケンスに連結し、適切な境界マーカーを挿入する工程を含む。

from typing import List, Optional
import torch

class LongContextDataCollator:
    """Ring Attention の学習のための長文データ Collator。

    複数の文書を連結して目標のシーケンス長を構成し、
    N 個のデバイスへ均等に分配できるようパディングする。
    """

    def __init__(
        self,
        tokenizer,
        target_seq_len: int,
        world_size: int,
        doc_separator_id: int = 2,  # </s> または <|endoftext|>
    ):
        self.tokenizer = tokenizer
        self.target_seq_len = target_seq_len
        self.world_size = world_size
        self.doc_separator_id = doc_separator_id
        self.chunk_size = target_seq_len // world_size

    def __call__(self, documents: List[str]) -> dict:
        # 文書をトークナイズし、区切り文字で連結
        all_tokens = []
        doc_boundaries = []
        for doc in documents:
            tokens = self.tokenizer.encode(doc, add_special_tokens=False)
            doc_boundaries.append(len(all_tokens))
            all_tokens.extend(tokens)
            all_tokens.append(self.doc_separator_id)

        # 目標の長さに合わせて切り詰めるかパディング
        if len(all_tokens) > self.target_seq_len:
            all_tokens = all_tokens[:self.target_seq_len]
        elif len(all_tokens) < self.target_seq_len:
            pad_len = self.target_seq_len - len(all_tokens)
            all_tokens.extend([self.tokenizer.pad_token_id] * pad_len)

        # world_size で正確に割り切れるかを確認
        assert len(all_tokens) % self.world_size == 0, (
            f"シーケンス長 {len(all_tokens)} が "
            f"world_size {self.world_size} で割り切れません。"
        )

        input_ids = torch.tensor(all_tokens, dtype=torch.long)

        # 各デバイスに割り当てるチャンクを生成
        chunks = input_ids.view(self.world_size, self.chunk_size)

        return {
            'input_ids': input_ids,
            'chunks': chunks,
            'doc_boundaries': doc_boundaries,
        }

限界、失敗事例、そして未解決の問題

Ring Attention は強力なソリューションだが、実戦適用では複数の限界と失敗事例が報告されている。これらを正確に理解することが、プロダクション配備には不可欠である。

限界1: ノード間通信のボトルネック

先に分析したとおり、Ring Attention の「ゼロオーバーヘッド」という主張は、ノード内の高帯域インターコネクト(NVLink、NVSwitch)でのみ成り立つ。ノード間の通信(InfiniBand、Ethernet)ではブロックサイズを非常に大きく設定しなければならず、これは単一デバイスのメモリ制約と衝突する。

実際のクラウド環境(AWS、GCP)で 2ノード以上にスケールした際に 10-30% の通信オーバーヘッドが観測されたという報告がある。特に不均質(heterogeneous)なネットワークトポロジーでは、最も遅いリンクが全体性能のボトルネックになる。

限界2: Causal Masking でのロード不均衡

Causal Attention では、シーケンスの前方にあるデバイス(低いインデックス)が大半の KV ブロックについてアテンションを計算しなければならない一方、後方のデバイス(高いインデックス)は多くのブロックをスキップする。このためデバイス間の計算負荷が不均衡になる。

たとえば 8個のデバイスで、Device 0 は 8個の KV ブロックすべてを処理するのに対し、Device 7 は 1個のブロック(自分自身)だけを処理して 7個のブロックをスキップする。平均すると 50% のブロックがスキップされるので計算量の全体は減るが、Device 0 が他のデバイスを待たせる同期のボトルネックが発生する。

これを緩和するために Striped Attention というパターンが提案された。シーケンスを連続的に分割する代わりに、インターリービング(interleaving)方式で分配し、各デバイスの計算負荷を均等化する手法である。

限界3: 小さいバッチサイズでの非効率

Ring Attention は、バッチサイズが非常に小さいとき(例: バッチサイズ 1)に GPU 利用率が大きく下がる。長いコンテキストの学習ではメモリ制約からバッチサイズを小さくせざるを得ないが、その場合 GPU の CUDA コアが十分に使われず、MFU が 20% 以下まで落ちることがある。

限界4: デバッグと再現性の難しさ

分散非同期通信をベースとする Ring Attention はデバッグが極めて難しい。通信順序のわずかな違い、浮動小数点演算の非決定性、デバイス間の同期エラーなどが学習の不安定さにつながることがあり、それを再現して追跡するのは非常に困難である。

限界5: 静的なメモリ割り当てと可変長シーケンス

Ring Attention の効率的な実装は、すべてのチャンクが同じサイズであることを前提とする。実際の学習データでは文書の長さが大きく変動するため、短い文書に対する過度なパディングが計算の無駄を生む。100万トークンのシーケンスを構成するために数百の短い文書を連結しなければならない場合、文書境界でのアテンション処理にも別途の考慮が必要になる。

失敗事例: NaN/Inf の発散

オンラインソフトマックスの実装では数値的安定性の問題が起こりうる。特に bf16 の学習では、correction ファクタ exp(old_max - new_max) が非常に大きな値へオーバーフローしたり、KV ブロックの順序によって max_score が急激に変動したりすると NaN が伝播することがある。これを防ぐには、ソフトマックスの累積を fp32 で行うか、max_score の変動幅にクリッピングを適用する必要がある。

実際のプロダクション環境で報告されたもう一つの失敗事例は、非同期通信の同期エラーによるサイレントなデータ破損である。特定のデバイスの送信が遅れ、受信側のデバイスが前のラウンドの残存データでアテンションを計算してしまうと、明示的なエラーもないまま学習損失が停滞したりモデル品質が劣化したりする。この種の問題は、各ラウンドの通信完了を明示的に検証するバリア(barrier)の挿入と、定期的なチェックサム検証で予防できる。

限界6: 推論時の KV Cache の分散管理

Ring Attention は学習だけでなく推論でも活用されるが、推論時には KV Cache の管理という追加の複雑さが生じる。自己回帰生成では各デコーディングステップごとに、それまでのすべてのトークンの KV Cache にアクセスする必要があるため、分散した KV Cache 間の通信がトークン生成のたびに必要になる。プリフィル(prefill)の段階では Ring Attention が高い効率を示すが、デコーディングの段階では生成されるトークンが一つだけなので、計算に対する通信の比率が不利になり効率が急激に落ちる。これを解決するために、プリフィルとデコーディングを分離する Disaggregated Serving アーキテクチャが研究されている。

最新の進展と今後の展望

World Model on Million-Length Video

Ring Attention の著者である Hao Liu は後続研究で Ring Attention を活用し、100万トークン以上の動画と言語のマルチモーダルモデルを学習させた。4K から始めて 1M まで段階的にコンテキストを拡張する戦略により、長い動画とテキストを同時に扱えるモデルの学習に成功した。この研究は、Ring Attention の実用性がビジョンと言語のドメインでも実証された事例である。

LASP (Linear Attention Sequence Parallelism)

2025年に提案された LASP は、Linear Attention モデルに特化したシーケンス並列化の手法である。Ring Attention と似た P2P 通信パターンを使うが、Linear Attention のカーネルトリックを活用して通信量をさらに削減した。128個の GPU で 400万トークン以上のシーケンスを処理し、同じリソースで Ring Attention に比べて 8倍長いシーケンスを扱えることを示した。

Context Parallelism の産業標準化

NVIDIA の Megatron-LM、Meta の Llama 学習インフラ、Google の Gemini 学習パイプラインなど、主要な産業フレームワークで Ring Attention ベースの Context Parallelism が標準機能として採用されつつある。これは Ring Attention が学術的な貢献にとどまらず、実質的な産業標準として定着しつつあることを示している。

今後の研究の方向性

  1. 適応的なブロックサイズ: ネットワーク帯域幅と計算負荷に応じて、ランタイムにブロックサイズを動的に調整する手法
  2. Sparse Ring Attention: すべての KV ブロックを循環させるのではなく、重要なブロックだけを選択的に交換する Top-k ベースのアプローチ
  3. 非同期パイプライン: 複数の Transformer レイヤーの Ring Attention をパイプラインで重ね合わせ、全体のスループットを最大化する手法
  4. 異種ハードウェアの最適化: GPU-TPU 混在クラスタや CPU オフロードなど、異種環境での Ring Attention の最適化

まとめ

ここまで Ring Attention の理論的な基盤から実戦適用までを幅広く見てきた。Ring Attention は、分散環境で Transformer のコンテキスト長の制限を乗り越えるエレガントな解法である。Blockwise Parallel Transformer のブロック単位アテンション計算を多数のデバイスに分散させ、リングトポロジーによる KV 循環と計算・通信のオーバーラップという中核アイデアを組み合わせることで、アテンションの精度をまったく損なわずにコンテキスト長をデバイス数に比例して拡張する。

ただし実戦適用では、ノード間通信のボトルネック、causal masking のロード不均衡、数値安定性の問題など、さまざまな課題が存在する。こうした限界を認識したうえで、USP のハイブリッド戦略、Striped Attention、Progressive Context Extension といった補完手法を適切に活用することが、適用を成功させる鍵である。

現在 Ring Attention は Context Parallelism という名前で主要な産業フレームワークに統合され、100万トークン以上のコンテキストに対応する次世代 LLM 学習の中核インフラとして定着しつつある。今後は適応的なブロックサイズや Sparse Ring Attention といった発展によって、さらに効率的で拡張性の高い長文コンテキスト処理が可能になるだろう。分散システム設計とアテンションメカニズム最適化の交点に位置する Ring Attention は、大規模言語モデルの発展において中核的なインフラ技術としての地位をいっそう固めていくと見込まれる。

参考資料

コメント

まだコメントはありません。

ログインするとコメントできます