未来を形作るテクノロジーの深掘り記事。

GPUのVRAMが足りなくなったら?取るべき対策

GPUのVRAMはAIやグラフィックス処理のボトルネックです。統合メモリ、VRAMオフロード、NVMeスワップの仕組みを解説します。

大量のデータがあふれ出し、下のストレージドライブへ流れ込む、光るグラフィックスカード

機械学習で最もよく見かけるエラーメッセージは、Pythonのトレースバックでも形状の不一致でもありません。CUDA out of memoryです。モデルが大きすぎる、バッチサイズが大きすぎる、あるいは中間のactivationがGPUのVRAMに収まらない、というのが原因です。RTX 4090は24 GBのVRAMを搭載しています。半精度の70Bパラメータモデルには約140 GBが必要です。計算が合わないのです。より大きなGPUに投資しても問題を先送りにするだけです。80 GBのH100ですら、単一カードには最大級のモデルは収まりません。

しかし、システムRAMやNVMeストレージを使ってGPUのメモリを透過的に拡張できたらどうでしょう?NVIDIAのGreenboostや類似のプロジェクトは、まさにそれを実現しています。メモリ階層(VRAM → システムRAM → SSD)を活用して、本来ハードウェアには収まらないワークロードを動かすのです。性能上のトレードオフは確かにありますが、多くのユースケースでは驚くほど許容範囲内です。この仕組みを理解するには、GPUのメモリ階層そのものを理解する必要があります。

なぜGPUのメモリはCPUのメモリと違うのか

CPUとGPUのメモリは、根本的に異なるアクセスパターンに対応しています。CPUのワークロードはレイテンシに敏感です。1つのスレッドがデータを必要とすると、それが届くまで処理が止まります。CPUのキャッシュは、ランダムアクセスのレイテンシを最小化するように設計されています。

一方、GPUのワークロードはスループットに敏感です。数千のスレッドがそれぞれデータを必要とし、GPUはスレッドを切り替えてレイテンシを隠蔽します。GPUのメモリ(データセンター向けGPUではHBM、コンシューマー向けカードではGDDR)は帯域幅を重視して設計されています。個々のアクセスはCPUのキャッシュヒットより遅くても、1秒あたりに膨大なデータを届けることが目的です。

Memory bandwidth comparison (approximate):
RTX 4090 GDDR6X:     1,000 GB/s
H100 HBM3:           3,350 GB/s
DDR5 System RAM:       50 GB/s
PCIe 5.0 x16:          64 GB/s (theoretical max)
NVMe SSD:               7 GB/s
The bandwidth cliff between VRAM and system RAM is ~20x.
Between VRAM and NVMe it's ~140x.
This is why naive offloading to system RAM kills performance —
you're trying to feed a 1,000 GB/s appetite through a 50 GB/s straw.

この帯域幅のギャップがあるため、CPUがディスクに対して行うように、GPUメモリを単にシステムRAMへページングして「仮想化」しても、素朴な実装ではうまくいきません。CPUはページフォールトを約10倍の速度低下で許容できます。しかしGPUがVRAMの代わりにシステムRAMへアクセスすると、帯域幅は20分の1に減ります。帯域幅律速のワークロード(ほとんどのML推論がそうです)では、そのまま20倍の速度低下につながります。

VRAMオフロードの実際の仕組み

ポイントは、システムRAMを遅いVRAMとして扱うことではありません。GPUが必要とする前にシステムRAMからVRAMへデータをプリフェッチし、計算の裏で転送を隠すことです。これが、実用的なVRAM拡張技術すべての根幹にある考え方です。

ニューラルネットワークの推論は、レイヤーを順番に計算していきます。GPUがレイヤー5を処理している間、次はレイヤー6だと分かっています。賢いオフロードシステムなら、レイヤー5の計算中にレイヤー6の重みをシステムRAMからVRAMへ転送し始められます。計算時間が転送時間より長ければ(大きなレイヤーではよくあることです)、転送は完全に隠れ、GPUが待たされることはありません。

# Conceptual overlap of compute and transfer
# (simplified pseudocode)
def inference_with_offloading(model, input_data):
# Only 2 layers fit in VRAM at a time
# Rest are in system RAM
for i, layer in enumerate(model.layers):
# Start async transfer of NEXT layer while computing current
if i + 1 < len(model.layers):
async_transfer_to_gpu(model.layers[i + 1])
# Compute on current layer (GPU is busy, transfer happens in parallel)
output = layer.forward(input_data)
# Evict current layer from VRAM (it's done)
transfer_to_ram(layer)
# Wait for next layer transfer to complete (usually already done)
sync_transfer()
input_data = output
return output

このパイプライン方式は推論に向いています。次に必要な重みが正確に分かるため、計算グラフが予測可能だからです。学習はより難しくなります。バックワードパスではフォワードパスのactivationが必要になるため、データ移動のパターンが複雑になるのです。

実践的なアプローチ

CUDA Unified Memory

NVIDIAのCUDA Unified Memoryは、GPUのVRAMとシステムRAMにまたがる単一のアドレス空間を作ります。CUDAランタイムが、アクセスパターンに基づいてページを両者の間で自動的に移動させます。GPUがシステムRAM上のページにアクセスするとページフォールトが発生し、そのページがVRAMへ移行されます。

利点は、アプリケーションから透過的であることです。CUDAのコードでデータ配置を管理する必要がありません。欠点は、ページフォールトのコストが高いことと、ランタイムの移行ヒューリスティクスがアプリケーションのアクセスパターンと必ずしも一致しないことです。ニューラルネットワーク推論のように予測可能なワークロードでは、明示的な管理の方が自動移行より高速です。

レイヤー単位のオフロード

Hugging Face Accelerate、DeepSpeed ZeRO-Inference、そしてllama.cppの--mmapフラグなどは、明示的なレイヤーオフロードを実装しています。VRAMにはアクティブなレイヤーだけを保持し、残りはシステムRAMやディスクからストリーミングします。モデル全体をVRAMに収める必要はなく、一度に1〜2レイヤーが収まればよいのです。

これが、一般的なハードウェアで70Bモデルを動かす仕組みの実態です。4ビット量子化された70Bモデルは全体で約40 GBが必要ですが、1レイヤーに必要なのは約1〜2 GBだけです。24 GBのVRAMとプリフェッチを使えば、性能への影響を抑えてモデルを動かせます。VRAMに完全に収めた場合より、30〜50%程度遅くなる程度でしょう。

拡張メモリとしてのNVMe

最も積極的なアプローチは、NVMe SSDをGPUメモリの第3階層として使うものです。帯域幅はVRAMと比べて桁違いに低い(約7 GB/s対約1,000 GB/s)ものの、容量はほぼ無制限です。4 TBのNVMeドライブは200ドルで、大規模モデルを何十個も同時に保存できます。

NVIDIAのGreenboostのようなプロジェクトは、これを透過的に実装しています。GPUのメモリシステムをNVMeまで拡張し、インテリジェントなプリフェッチで帯域幅の制約による影響を最小限に抑えます。GPUが計算に多くの時間を使う推論ワークロードでは(データ移動だけをしているわけではありません)、計算との重ね合わせによってNVMeのレイテンシを完全に隠蔽できる場合があります。

性能はワークロードに大きく依存します。計算律速の処理(大きな行列積)は転送レイテンシをうまく隠せます。メモリ律速の処理(大きなコンテキストを使うアテンション機構)は隠せません。実際には、NVMeオフロードは小さなバッチサイズで大規模モデルをバッチ推論する場合に最も効果的です。これはまさにローカルLLM推論のユースケースそのものです。

Appleのユニファイドメモリの強み

Apple Siliconのユニファイドメモリアーキテクチャは、まったく別のアプローチを取ります。VRAMとRAMの区別そのものをなくすのです。CPUとGPUが同じ物理メモリプールを共有します。分離していないので「オフロード」の必要もありません。GPUはCPUと同じメモリにアクセスします。

これで帯域幅の問題が解消されるわけではありません。M4 Maxのメモリ帯域幅(約400 GB/s)は、専用GPUより低いです。しかし、個別のGPUでオフロードする際に速度低下の原因となるPCIeのボトルネックは取り除かれます。128 GBのユニファイドメモリを搭載したMacなら、オフロードのオーバーヘッドなしで70Bモデルを全体GPUアクセス可能なメモリに収められます。

トレードオフは次のとおりです。専用GPUのVRAMに完全に収まるワークロードでは、ピーク時のスループットは低くなります。しかし、収まらないワークロードでは格段に高い性能が得られます。大規模モデルの推論で、モデルが個別GPUのVRAMを超える場合、Apple Siliconは生の計算能力では劣るにもかかわらず、オフロードを行う個別GPUより高速なことがよくあります。

開発者にとっての意味

GPUを使うアプリケーション(ML推論、グラフィックス、科学技術計算)を作っているなら、VRAMの制約はアーキテクチャの判断に具体的な影響を与えます。

  • ワーキングセットのサイズを把握する。GPUメモリの使用量をプロファイルしましょう。見るべきはピーク割り当てではなく、任意の時点でのワーキングセットです。ピークが48 GBでも、単一の処理が必要とするアクティブデータが8 GB以下なら、オフロードはうまく機能します。単一の処理が本当に48 GBを同時に必要とするなら、より大きなGPUが必要です。
  • アクセスパターンに基づいてオフロード戦略を選ぶ。順次アクセス(レイヤー単位の推論)はプリフェッチと相性が抜群です。ランダムアクセス(大きなコンテキストに対するアテンション)は相性がよくありません。自分のワークロードがどのパターンかを把握しておきましょう。
  • 通常、量子化はオフロードより安上がりです。モデルをFP16からINT4に落とせば、品質への影響は小さいままメモリを4分の1にできます。システムRAMへのオフロードは品質への影響はゼロですが、メモリの節約量は限られ、レイテンシが増えます。まず量子化を行い、そのあとでオフロードを検討しましょう。
  • バッチサイズが調整のつまみになります。バッチが大きいとメモリは多く必要ですが、オーバーヘッドを均しやすくなります。バッチが小さいとメモリは少なくて済みますが、1秒あたりに処理できる件数が減ります。VRAMの上限に近いときは、バッチサイズを下げるのが最も手軽な対処法です。
  • メモリの断片化を監視する。CUDAのメモリ割り当ては、特に可変長の入力では時間とともにVRAMを断片化させることがあります。空きが合計8 GBあっても、連続する2 GBのブロックがないかもしれません。PyTorchのtorch.cuda.memory_stats()で断片化を確認できます。torch.cuda.empty_cache()も助けになりますが、万能ではありません。

大規模な計算ワークロードでは、GPUメモリが常にボトルネックになります。モデルの成長はVRAMの成長より速いからです。しかし、統合メモリ、インテリジェントなオフロード、多階層キャッシュといった、そのボトルネックを扱うツールは十分に実用的になりつつあります。「VRAMに収まらない」は、もはや越えられない壁ではありません。性能上のトレードオフであり、ますます扱いやすくなっているのです。