GPU

目次

主要項目のみを表示しています。詳細な小見出しは本文内で確認できます。

概要

スループット、メモリ帯域、並列実行をつなげて理解する

GPUは「画面を描くための部品」から、現代ではAIHPC映像処理データ分析、科学技術計算、ブラウザ描画まで支える中核計算装置へ変わりました。GPUを単なる高速なアクセラレータとしてではなく、CPUとは違う前提で設計された並列計算機 として理解します。

要点

GPUを理解する鍵は、スループット志向、SIMT、つまり同じ命令を多数スレッドの束へ適用する実行モデル、メモリ帯域、レイテンシ隠蔽、ワークロード適性の5つです。CPUと同じ感覚で見ると誤解しやすく、逆にこの5つが見えると、なぜGPUが速いのか、なぜ向かない処理もあるのかがつながります。

この章で重視すること

  • GPUを「CPUの強化版」ではなく、設計目的の違う計算機として理解する
  • ストリーミングマルチプロセッサ、warp / wavefront共有メモリHBM、つまり高帯域メモリをひとつながりで捉える
  • グラフィックスAPIGPGPU、つまりGPUを描画以外の一般計算へ使う考え方を分けすぎず、同じハードウェアの別の顔として見る
  • CUDAだけでなく ROCm / HIP、つまりAMD系GPU向けの主要ソフトウェアスタックと移植層、MetalVulkan Computeまで視野に入れる
  • 実務での判断材料として、向く処理・向かない処理・測るべき指標を押さえる

先に押さえるCPUとGPUの違い

GPUを理解しにくい最大の理由は、CPUの常識をそのまま持ち込んでしまうことです。CPUCentral Processing Unit の略で、OS、データベース、Webサーバ、アプリケーションの制御フローのような、分岐が多く、低遅延で順番に正しく処理したい仕事 を得意とします。GPUGraphics Processing Unit の略で、よく似た計算を大量データへ一気に適用したい仕事 を得意とします。

一番大きな違い

  • CPUは latency、つまり1個の仕事を終えるまでの待ち時間を小さくしたい
  • GPUは throughput、つまり単位時間あたりの総処理量を大きくしたい

ここでいう latency は「1件の処理が返るまでの速さ」、throughput は「全体としてどれだけ多く処理できるか」です。同じ「速い」でも、狙っている速さが違います。

flowchart LR A["CPU少数の高機能コア"] --> B["低レイテンシ 逐次処理 分岐に強い"] C["GPU多数の軽量実行器"] --> D["高スループット 同種大量並列に強い"]

CPUとGPUの比較表

観点 CPU GPU
主目的 汎用制御、低遅延応答 大量並列、総処理量最大化
得意な処理 分岐、逐次依存、OS、DB、Web 行列演算、画像処理、描画、AI、HPC、つまり大規模科学技術計算
コアの性格 少数で高機能 多数で比較的軽量
キャッシュ戦略 大きなキャッシュで待ち時間を減らす 多数スレッド切替と高帯域で待ちを隠す
メモリ観点 レイテンシ重視 帯域重視
典型的な失敗 並列度不足 分岐・転送・メモリアクセスが詰まる

先に知っておきたい専門用語

  • メモリ帯域 は、1秒あたりにどれだけ多くのデータを運べるかです。GPUはこの値が非常に重要です。
  • レイテンシ隠蔽 は、あるスレッドがメモリ待ちの間に別のスレッドを動かして、待ち時間を表に出しにくくする考え方です。
  • 並列性 は、同時に進められる仕事の量です。GPUは大きな並列性がある問題で真価を出します。
  • ワークロード は、実際に処理したい仕事の性質です。GPUはワークロード適性の見極めがとても重要です。

CPUのコードをそのまま移しても速くならない理由

CPU向けに自然なコードは、しばしばGPU向けには不自然です。たとえばCPUでは、

  • 分岐を多用しても1スレッドが速ければ十分なことがある
  • 小さなループを細かく関数分割しても問題になりにくい
  • ポインタをたどる複雑なデータ構造でもキャッシュが効けば成立する

ということが起きます。GPUでは逆に、

  • 同じwarp内で分岐が割れると足並みが崩れる
  • 小さなカーネルを大量に起動すると起動オーバーヘッドが目立つ
  • ランダムアクセスが増えると高い帯域を活かしにくい

となりやすいです。つまりGPU化は、単なる「移植」ではなく、データ配置と計算形の再設計 になることが多いです。

CPUとGPUの見え方の違い

問い CPU的な見方 GPU的な見方
速いか 1リクエストの応答が短いか 全体のスループットが高いか
ボトルネックは何か 分岐、キャッシュミス、ロック 転送、帯域、ダイバージェンス占有率
良いデータ構造は何か 木、ハッシュ、ポインタ構造も許容 連続配置、配列中心、規則的アクセス
良い並列化は何か 少数スレッドの高効率 非常に多い仕事をまとめて流す

学び始めで持つとよい視点

GPU学習の序盤では、次の順番で見ると理解しやすくなります。

  1. 問題に十分な並列性があるか
  2. データを連続的に読めるか
  3. GPU上へ乗せたデータを何度も再利用できるか
  4. CPUとの往復が支配的にならないか
  5. 分岐や同期で実行束の足並みが崩れないか

GPUとは何か

GPUは Graphics Processing Unit の略で、もともとは画像描画を高速化するために発展したプロセッサです。ただし現代では、単に「グラフィックス専用」ではありません。むしろ、

  • 同じ種類の計算を大量のデータへ適用する
  • 非常に高い メモリ帯域、つまり大量データを高速に読み書きする力が必要
  • 単一スレッドの速さより全体の 処理量 が重要

という領域で、CPUよりはるかに強い計算機として使われています。

GPUを一言でいうと

GPUは、少数の重いコアで低遅延処理を得意とするCPU に対し、多数の軽量な実行資源で高スループット処理を得意とする装置 です。ここでいう コア は、命令を実行する計算の中心部分です。ただしGPUの「コア」はCPUコアと同じ意味ではなく、より細かい実行資源の集合として扱う方が実務では正確です。

flowchart LR A["CPU少数の高機能コア"] --> B["低レイテンシ 単一スレッド性能"] C["GPU多数の軽量実行器"] --> D["高スループット 大量並列"]

GPUを理解すると何が見えるか

GPUを理解すると、

  • なぜAI学習でGPUが主役なのか
  • なぜゲームや3D描画がGPU依存なのか
  • なぜCPUのコードをそのまま移しても速くならないのか
  • なぜメモリ転送やダイバージェンスがボトルネックになるのか

が現実的に見えてきます。

GPUを誤解しやすいポイント

  • コア数が多い = いつでも速い ではない
  • VRAMが多い = いつでも高性能 ではない
  • 占有率が高い = いつでも最速 ではない
  • GPU利用率が100% に近い = 最適化済み でもない

これらはすべて一部しか見ていません。GPUでは、演算器、メモリ帯域、転送、同期、ソフトウェアスタックが互いに制約になります。


なぜGPUは速いのか

GPUが速い理由は、クロック周波数、つまり1秒あたりの動作回数が極端に高いからではありません。主な理由は次の4つです。

  1. 実行資源の数が多い
  2. メモリ帯域が非常に広い
  3. レイテンシを隠す設計になっている
  4. 同じ命令を大量データへ適用する処理に強い

レイテンシよりスループット

CPUは「1本の重要な仕事をできるだけ早く終わらせる」方向に強く最適化されています。GPUは「大量の似た仕事をまとめてさばく」方向に最適化されています。CPUは分岐予測、投機実行、大きなキャッシュを使って1スレッドの待ち時間を減らします。GPUは多数のスレッドを切り替えながら動かして、待ち時間そのものを別の仕事で埋めます。

flowchart TB A["CPUの重視点"] --> B["分岐予測"] A --> C["大きなキャッシュ"] A --> D["単一スレッドの低遅延"] E["GPUの重視点"] --> F["多数スレッド"] E --> G["高メモリ帯域"] E --> H["レイテンシ隠蔽"]

GPUの速さは万能ではない

GPUは万能高速化装置ではありません。次のような処理は苦手です。

  • 分岐が多く、各要素で違う経路を通る
  • データ依存が強く、並列化しにくい
  • ランダムアクセスが多い
  • GPUに送る前後の転送コストが支配的

GPUの速さは、向いた形に問題を変形できたときに出る と考える方が正確です。

この章のひとこと

GPUは「何でも速い装置」ではなく、「大量に似た計算を高帯域で流せると強い装置」です。


GPUの歴史

固定機能時代

初期のGPUは、現在のような汎用計算機ではありませんでした。テクスチャマッピング は画像を物体表面へ貼る処理、ラスタライズ は三角形などの図形を最終的な画素へ変換する処理、Zバッファ は前後関係を判定するための深度情報、ブレンディング は色を混ぜ合わせる処理です。初期GPUはこうしたグラフィックス専用の固定機能を高速に実行する装置でした。

プログラマブルシェーダの登場

2000年代に、頂点シェーダ、つまり3Dモデルの頂点ごとに実行するプログラムと、ピクセルシェーダ、つまり画素ごとに色や光を計算するプログラムがプログラマブルになり、描画処理の一部をソフトウェア的に制御できるようになりました。ここで「GPU上で計算する」発想が一気に強まりました。

GPGPU

当初の GPGPUGeneral-Purpose computing on GPUs の略で、GPUを描画以外の一般計算へ使うことです。初期のGPGPUは、グラフィックスAPIを無理に計算へ転用する形でした。行列をテクスチャとして扱ったり、レンダリングパスを計算として流用したりしていたため、かなり不自然でした。

CUDAと汎用化

2006年にNVIDIAがCUDAを出したことで、GPUをC/C++ 風に直接プログラムできる道が開きました。これが、現在のGPU計算の大きな分岐点です。

現代

現在のGPUは、

  • グラフィックス
  • 汎用計算
  • 行列演算
  • レイトレーシング
  • ビデオエンコード / デコード

など複数の用途を1チップへ統合しています。

timeline title GPUの大きな流れ 1990 : 固定機能グラフィックス 2000 : プログラマブルシェーダ 2006 : CUDAによるGPGPUの本格化 2010 : HPCと深層学習でGPU利用が拡大 2020 : Tensor Core, Ray Tracing, HBM, MIGが一般化 2025 : AIインフラの中核としてGPUが定着

基本アーキテクチャ

GPUの細部はベンダごとに違いますが、概念的にはかなり似ています。

  • 実行クラスタが複数ある
  • 各クラスタが多数スレッドを切り替えながら処理する
  • 高帯域なメモリへ接続される
  • 命令キャッシュ、L1、つまり実行クラスタの近くにある小さなキャッシュ、共有メモリL2、つまりGPU全体で共有するより大きなキャッシュを持つ

NVIDIA系の見方

NVIDIAでは主に SM(Streaming Multiprocessor)という単位でGPUを見ます。SMは、多数のスレッドをまとめて走らせる実行クラスタです。実際の命令発行器、レジスタファイル、共有メモリ、各種演算器がこの単位の周辺にまとまっています。

AMD系の見方

AMDでは主に CU(Compute Unit)や、最近では WGP(Work Group Processor)といった単位が重要です。名前は違いますが、NVIDIAのSMと同様に、多数の並列スレッドを処理する実行クラスタとして捉えると理解しやすくなります。

Apple / Metal系の見方

AppleはAPI面では Metal、つまりAppleの低レベルGPU APIを前面に出し、GPU familyfeature set で能力を表します。内部構造は公開範囲が少し異なりますが、実務的には スレッドグループ、つまり共同で共有メモリを使う実行単位と、タイルベース最適化、つまり画面を小さな領域に分けて効率よく処理する考え方が重要です。

flowchart LR Host["CPU / ホスト"] --> Driver["ドライバ / ランタイム"] Driver --> Front["コマンドプロセッサ"] Front --> SMs["多数の実行クラスタSM / CU / WGP"] SMs --> L2["L2キャッシュ"] L2 --> VRAM["GDDR / HBM / Unified Memory"]

GPUで重要な構成要素

  • 実行クラスタ: SMやCUのように実際の並列実行を担うまとまり
  • レジスタファイル: 各スレッドが最も近い場所で使う超高速な小容量メモリ
  • 共有メモリ / LDS / threadgroup memory: 同じブロックやグループ内のスレッドが共有できる手動管理メモリ。LDSAMD系でよく使う Local Data Share の呼び名
  • L1 / texture cache: 近くで再利用されるデータを貯める小さな高速キャッシュ
  • L2: GPU全体で共有される比較的大きなキャッシュ
  • グローバルメモリ: VRAM、つまりGPUボード上の主記憶
  • コピーエンジン: CPUメモリとGPUメモリの転送などを担当する専用経路
  • ビデオ専用エンジン: エンコードやデコードを担当する専用回路
  • テンソル / 行列演算ユニット: 行列積やAI演算向けに特化した専用演算器

CPUとGPUの内部構造の違い

CPUは、複雑な制御フローや低遅延応答のために、分岐予測器、投機実行、大きなキャッシュ、アウトオブオーダ実行を多く抱えます。アウトオブオーダ実行 は、プログラム順ではなく、依存関係がない命令を先に進めて待ち時間を減らす仕組みです。

GPUは、そうした複雑な制御機構へ面積と電力を大量に使う代わりに、

  • 多数の実行レーン
  • 大きなレジスタファイル
  • 高帯域メモリ接続
  • 多数スレッドの高速切替

へ資源を振ります。これは「1本を速く」より「束全体を多く流す」ための選択です。

GPUを制御プレーンとデータプレーンで見る

  • 制御プレーン: CPU、ドライバ、ランタイム、コマンドキュー、同期API
  • データプレーン: 実際にGPU上で動くカーネル、メモリ転送、共有メモリ、演算器

実務では、性能が悪いときに「GPUカーネルの中」だけを見がちですが、実際にはCPU側のデータ供給やランタイムの同期がボトルネックなことも多いです。


グラフィックスパイプライン

GPUは今もグラフィックス用の装置でもあります。APIApplication Programming Interface の略で、ソフトウェアからGPU機能を呼び出すための約束事です。グラフィックスAPIを理解すると、GPUの固定機能とプログラマブル部分の境界が見えます。

flowchart LR A["頂点入力"] --> B["Vertex Shader"] B --> C["ラスタライズ前処理"] C --> D["Rasterizer"] D --> E["Fragment Pixel Shader"] E --> F["Depth Blend Output"]
段階 主な役割 compute視点で見ると
頂点処理 形や座標を変換する 大量データへ同種演算を当てる入口
ラスタライズ 図形を画素候補へ変える 固定機能の価値が見える
ピクセル処理 色や光を計算する 高並列な局所計算の典型
出力 深度、ブレンド、書き込み 帯域と一時データ管理が効く

なぜこの章が重要か

GPUをAI用アクセラレータとしてしか見ないと、もともとの設計思想を見失いやすいです。テクスチャキャッシュ、補間、タイル、レンダーパス最適化などは、いまでもGPUの物理設計に影響を残しています。

Computeとの関係

現代のGPUは、同じハードウェア資源を

  • グラフィックス
  • コンピュート
  • コピー
  • メディア処理

で共有します。だから、グラフィックスAPIとコンピュートAPIは別世界ではありません。

flowchart LR A["同じGPU"] --> B["graphics"] A --> C["compute"] A --> D["copy engine"] A --> E["media engine"]

グラフィックス由来の設計がcomputeに残す影響

GPUはもともと描画のために進化したため、computeだけを見ると見落としやすい性質が残っています。

  • テクスチャキャッシュは空間局所性の高いアクセスで効きやすい
  • タイルベース設計は一時データを近いメモリへ閉じ込めやすい
  • ラスタライズや補間のための固定機能は、専用回路の価値を示す好例でもある

この視点を持つと、なぜAI時代にも「専用ユニット」が増えるのかが理解しやすくなります。

グラフィックス由来の性質 computeにどう効くか
テクスチャキャッシュ 空間局所性の高い読み出しに強い
タイルベース設計 一時データを近くへ閉じ込めやすい
固定機能回路 専用ユニットの価値を理解しやすい
この章のひとこと

GPUは今もgraphicsとcomputeを同じ地盤の上で動かしているので、描画由来の設計思想を知るとcomputeの癖も見えやすくなります。


GPGPUとCUDA以後

GPGPUの本質

GPGPUは、General-Purpose computing on GPUs の略です。つまりGPUを、描画以外の一般計算へ使うことです。

flowchart LR A["描画専用のGPU"] --> B["シェーダが柔軟になる"] B --> C["描画以外にも使いたい"] C --> D["GPGPU"] D --> E["専用compute APIへ進化"]

CUDAの意味

CUDA はNVIDIAのGPU計算プラットフォームです。重要性は、NVIDIA専用という点だけではありません。GPU計算を

  • カーネル起動
  • スレッド階層
  • メモリ階層
  • 同期
  • ホストとデバイスの分離

という形で、比較的一貫したプログラミングモデルへ落とし込んだことにあります。

CUDAが持ち込んだ整理 何が嬉しかったか
カーネル起動 描画を経由せず計算を直接書ける
スレッド階層 並列化の単位を考えやすい
メモリ階層 速さの理由と詰まり方を説明しやすい
ホスト / デバイス分離 CPUとGPUの役割分担が見える

現代の主要系統

  • CUDA: NVIDIAのGPU向けに最も広く使われる計算基盤
  • HIP / ROCm: AMD系GPU向けの主要スタック。HIPCUDAに近い書き味の移植層
  • Metal: AppleプラットフォームのGPU API
  • Vulkan Compute: Khronosの低レベルクロスベンダAPIのcompute機能
  • OpenCL: 標準系だが、近年のAI実務の中心ではない
  • Direct3D 12 Compute: Windows系で使われる低レベルGPU compute

2026年の実務感覚では、この並びは単なるAPI一覧ではありません。大まかに言うと、

  • 学習・推論・周辺ライブラリの厚み ではCUDAが依然として非常に強い
  • HPC / 移植性 / オープン寄りの基盤 ではROCmの重要性が上がっている
  • Apple統合環境 ではMetalがほぼ前提になる
  • ブラウザ / サンドボックス / 配布容易性 ではWebGPU系の価値が目立つ

という棲み分けが見えます。

なぜCUDAが強いのか

CUDAの優位はAPIだけではありません。実務上は、

  • コンパイラやランタイムが成熟している
  • cuBLAScuDNN のような基盤ライブラリが厚い
  • NCCL のようなマルチGPU通信ライブラリが強い
  • プロファイラやデバッガが揃っている
  • 上位フレームワークの最適化が先に来やすい

という、ソフトウェア生態系の積み上げが非常に大きいです。

2026年時点でも、この「エコシステム差」はかなり効きます。GPUの理論性能が近くても、

  • 使いたいライブラリがすぐ動くか
  • profilerやdebuggerが十分に揃っているか
  • 推論ランタイムや量子化ツールが枯れているか
  • 周辺フレームワークの最適化が先に来るか

で、現場の生産性は大きく変わります。

ROCm / HIPの価値

ROCmは単に「CUDAの代替」ではありません。AMD系GPUを前提にしつつ、HIP、ライブラリ群、プロファイラ、フレームワーク連携を含んだ総合基盤です。複数ベンダ調達やコスト、オープン性、HPC既存基盤との整合を重視する組織では、十分に現実的な選択肢です。

しかも2026年時点のROCm公式docsは、ROCmを HIP / OpenCL / OpenMPを含む統合ソフトウェア基盤 として整理しています。ここはかなり大事です。つまりAMD系では、

  • CUDA風のカーネル移植ならHIP
  • 既存HPCコードのGPU offloadならOpenMP offloading
  • 互換性や既存資産との接続ではOpenCL

のように、入口が1つではありません。ROCmは「AMD上のCUDAもどき」だけで見ると、少し狭く理解しすぎになります。

flowchart TB A["AMD GPUを使いたい"] --> B["HIPでCUDA風に書く"] A --> C["OpenMP offloadingで既存HPCを伸ばす"] A --> D["OpenCLで既存資産とつなぐ"] B --> E["ROCmという統合基盤"] C --> E D --> E
この章のひとこと

GPGPU以後の本質は、「描画を流用する時代」から「GPU計算を独立したモデルとして考える時代」へ移ったことです。


実行モデル:スレッド、warp、wavefront

GPUプログラミングで最初に混乱しやすいのが、CPUの スレッド とGPUの スレッド は同じ重さではない、という点です。CPUのスレッドはOSスケジューラが扱う比較的重い実行単位ですが、GPUのスレッドはもっと軽く、大量に作ってまとめて処理する前提で設計されています。

CUDAの基本階層

flowchart TB Grid["Grid"] --> Block["Thread Block"] Block --> Warp["Warp"] Warp --> Thread["Thread"]

warp

NVIDIAでは、通常32スレッド単位で命令が進みます。これを warp と呼びます。warpは「GPUが内部でまとめて進めるスレッドの束」です。

wavefront

AMDでは、同様の概念として wavefront が重要です。世代やモードにより幅の扱いは異なりますが、CPUの1スレッドとはかなり違う粒度で動いています。実務では「warpに近い実行束」と考えると概ね外しません。

SIMT

GPUはしばしば SIMT と説明されます。これは Single Instruction, Multiple Threads の略で、見た目は多数スレッドですが、実際には同じ命令流を束で実行する側面が強い、という考え方です。SIMD が1命令で複数データを処理するベクトル計算だとすると、SIMTはそれを「多数スレッドがあるように見せる」形へ寄せたものです。

ダイバージェンス

同じwarpの中で条件分岐により別の経路へ進むと、GPUはしばしばそれらを逐次化して扱います。これが warp divergence、つまり分岐による足並み崩れです。

flowchart LR A["同じwarpの32スレッド"] --> B{"条件分岐"} B -->|同じ分岐先| C["効率よく進む"] B -->|分岐が割れる| D["逐次化され効率低下"]

kernel / grid / block / threadgroup / subgroup

GPUの用語はAPIごとに少しずつ違うので、対応関係を早めに押さえると読みやすくなります。

概念 CUDA Metal Vulkan / SPIR-V系
起動される計算単位 kernel kernel function compute shader
全体の仕事空間 grid grid dispatch grid
協調実行グループ thread block threadgroup workgroup
最小スレッド単位 thread thread invocation
ハードウェア寄りの実行束 warp simdgroup相当 subgroup

GPUスケジューリングの感覚

GPUは大量スレッドを同時に「本当に全部動かす」わけではありません。実際には、SMやCUごとに載せられるwarp / wavefront数には上限があり、

  1. まずカーネルが起動される
  2. スレッドがブロックやworkgroupにまとめられる
  3. それらが各実行クラスタへ割り当てられる
  4. 実行可能なwarp / wavefrontから順に切り替えながら進む

という流れになります。ここでレジスタや共有メモリを使いすぎると、同時に載せられる束の数が減り、レイテンシ隠蔽力が落ちます。

async compute

async compute は、描画や別キューの処理と並行してcomputeを流す考え方です。ただし「必ず速い」わけではありません。共有するメモリ帯域やキャッシュの競合、同期の増加で逆効果になることもあります。


メモリ階層

GPUの性能は、かなりの割合でメモリで決まります。CPU以上に「どこから、どの順番で、どれだけまとめてデータを読むか」が効きます。

GPUの主なメモリ層

flowchart TB A["レジスタ"] --> B["共有メモリ / LDS"] B --> C["L1 / texture cache"] C --> D["L2"] D --> E["VRAM GDDR / HBM / Unified Memory"] E --> F["ホストメモリ"]
速さの感覚 容量の感覚 何を意識するか
レジスタ とても速い とても小さい 使いすぎるとoccupancyを圧迫する
共有メモリ かなり速い 小さい タイル再利用とbank conflictを意識する
L1 / texture cache 速い 小さい 局所性があると効きやすい
L2 中間 中くらい GPU全体の共有点として効く
VRAM 遅いが広い 大きい まとめて読む、再利用する
ホストメモリ さらに遠い 大きい できるだけ往復を減らす
flowchart LR A["速いが狭い"] --> B["レジスタ"] B --> C["共有メモリ"] C --> D["L1 / L2"] D --> E["VRAM"] E --> F["ホストメモリ"] F --> G["遅いが広い"]

共有メモリ

共有メモリは、スレッドブロック内の協調で極めて重要です。低レイテンシですが容量は小さく、使いすぎると 占有率 に影響します。占有率は、1つのSMやCUに何組の実行束を同時に載せられるかの目安です。

flowchart TB A["グローバルメモリ"] --> B["thread blockが小片を読む"] B --> C["共有メモリへ置く"] C --> D["同じblock内で何度も再利用"] D --> E["グローバル再読み出しを減らす"]

グローバルメモリ

容量は大きいですが遅いです。GPUの最適化では、グローバルメモリアクセスの回数とパターンが性能を大きく左右します。

コアレッシング

隣接スレッドが隣接アドレスを読むと、ハードウェアがまとめて効率よく転送しやすくなります。これが coalesced access、日本語ではアクセスのまとめ読みです。CUDAのBest Practices Guideでも、グローバルメモリアクセスのコアレス化は高優先度の最適化として強調されています。

アクセス形 GPUから見ると 起きやすいこと
thread 0,1,2,3が連続アドレスを読む まとめやすい 少ないトランザクションで済む
thread 0,1,2,3が離れた場所を読む まとめにくい 転送が分裂しやすい

HBM

AI / HPC向けGPUでは HBM、つまり High Bandwidth Memory が重要です。HBMは非常に高い帯域を実現しますが、容量やコスト、実装複雑性とのトレードオフもあります。

register pressure

register pressure は、1スレッドあたりのレジスタ使用量が多すぎて、同時実行数やコンパイラ最適化を圧迫する状態です。GPU最適化では「メモリへ出さないためにレジスタを使う」ことと、「使いすぎて占有率を落とす」ことのバランスが常にあります。

flowchart LR A["レジスタを増やす"] --> B["メモリアクセスを減らせる"] A --> C["同時に載るwarpが減る"] B --> D["速くなる可能性"] C --> E["レイテンシ隠蔽が弱くなる"]

bank conflict

共有メモリは速いですが、アクセスの仕方によっては bank conflict、つまり同時アクセスの衝突が起きます。これは、複数スレッドが内部的に同じバンクへ集中的にアクセスし、直列化が増える現象です。共有メモリは「使えば速い」ではなく、「並び方まで設計して速い」です。

pinned memoryとunified memory

  • pinned memoryページ固定されたホストメモリで、DMA転送を効率化しやすい
  • unified memory はCPUとGPUから見えるメモリ空間を統合して扱いやすくする仕組み

どちらも便利ですが、便利さと最高性能は同じではありません。とくにunified memoryはアクセスパターンによって暗黙の移動が起きるため、性能の説明が難しくなることがあります。

方式 嬉しいこと 注意点
pinned memory 転送が安定しやすい ホストメモリ管理が少し重い
unified memory 書きやすい、アドレス空間を意識しやすい ページ移動が暗黙に起きると読みにくい

良いアクセスと悪いアクセス

flowchart TB A["隣接スレッド"] --> B["隣接アドレスへアクセス"] B --> C["coalesced access"] C --> D["少ない転送でまとまる"] E["隣接スレッド"] --> F["ばらばらのアドレスへアクセス"] F --> G["non-coalesced access"] G --> H["転送回数が増えやすい"]
この章のひとこと

GPUメモリ最適化の中心は、「遠いメモリを何度も読まない」「近い場所にまとめて置く」「隣どうしで規則的に読む」の3つです。


同期、ダイバージェンス、占有率

同期

GPUは大規模並列ですが、何でも自由に同期できるわけではありません。まずは

  • ブロック内同期
  • グリッド全体同期
  • ホスト側同期

の違いを分ける必要があります。

flowchart TB A["同期したい"] --> B{"同じblock内か"} B -->|はい| C["block内同期で済むことが多い"] B -->|いいえ| D{"GPU全体でそろえる必要があるか"} D -->|はい| E["別kernel化やホスト側制御を検討"] D -->|いいえ| F["アルゴリズム分解で同期を減らせないか考える"]

占有率

occupancy は、あるSM / CUに対してどれだけ多くのwarp / wavefrontを同時に抱えられるか、という概念です。一般に高すぎても低すぎても単純な正義ではありませんが、低すぎるとレイテンシ隠蔽が難しくなります。

flowchart LR A["低いoccupancy"] --> B["待っている間に切り替える束が少ない"] B --> C["メモリ待ちが表に出やすい"] D["高いoccupancy"] --> E["切り替え候補が多い"] E --> F["待ちを隠しやすい"]

占有率に効くもの

  • レジスタ使用量
  • 共有メモリ使用量
  • ブロックサイズ
  • ハードウェア上限
要因 増えると何が起きやすいか
レジスタ使用量 1 SMあたりに載るスレッド束が減る
共有メモリ使用量 block数が減る
ブロックサイズ 良くも悪くも載り方が変わる
ハードウェア上限 ここは変えられないので設計側で合わせる

占有率は手段であって目的ではない

占有率を上げるためにレジスタ圧縮やブロックサイズ調整を行うことはありますが、最終的に見るべきなのは

  • 実行時間
  • スループット
  • メモリ帯域利用率
  • 演算ユニット利用率

です。

同期で詰まりやすいポイント

  • ブロック内同期を多用しすぎる
  • 原子操作にホットスポットが集中する
  • グローバル同期が必要なアルゴリズムをそのまま持ち込む
  • CPU側の同期でGPUを頻繁に待たせる

原子操作 は、複数スレッドが同時に更新しても壊れないようにする操作です。便利ですが、同じ場所へ多数スレッドが集まると強い競合が起きます。

ダイバージェンスを避ける考え方

  • 条件分岐をデータ前処理で減らせないか
  • 同じ分岐を通る要素同士を近くへ並べられないか
  • マスク計算や別カーネル分割の方が有利ではないか

GPUでは「分岐をゼロにする」ことより、「同じ実行束の中で分岐を揃える」ことが大事です。

flowchart LR A["同じwarpのスレッド"] --> B{"同じ条件分岐を通るか"} B -->|はい| C["足並みがそろう"] B -->|いいえ| D["分岐先ごとに順番待ちしやすい"]
この章のひとこと

占有率も同期も分岐も、結局は「GPUが待ち時間をうまく隠せるか」を左右する観点です。


テンソルコアと行列演算

AI時代のGPUを語るうえで、テンソルコア や行列エンジンは外せません。テンソルは多次元配列、テンソルコアはその配列演算、とくに行列積を高密度に処理する専用回路です。

flowchart LR A["通常のALU / FPユニット"] --> B["汎用の加減算やFMA"] C["テンソルコア / 行列エンジン"] --> D["行列積を高密度に処理"] D --> E["AI / 線形代数で大きく効く"]

何が違うのか

通常の ALU、つまり加減算などの基本演算器や、FP、つまり浮動小数点演算ユニットとは別に、行列積や fused multiply-add、つまり掛け算と足し算をまとめて行う演算を高密度に処理する専用ユニットを持つことで、深層学習や線形代数に対して圧倒的なスループットを出せます。

観点 通常演算器 テンソルコア / 行列エンジン
向いている処理 汎用の細かい演算 大きな行列積、テンソル演算
強み 柔軟性が高い 密度の高い行列計算を高速化
弱み 行列積では密度が足りない 小さすぎる問題や不規則処理には向きにくい

なぜAIに効くのか

深層学習の多くは、結局は大規模な行列演算、畳み込み、テンソル演算へ帰着します。そこに専用ユニットを当てることで、通常演算器だけよりはるかに効率が上がります。

flowchart TB A["埋め込み / 線形層 / attention / MLP"] --> B["ほとんどが行列積へ帰着"] B --> C["テンソルコアが効きやすい"] C --> D["学習でも推論でも中心になる"]

ただし注意点

  • 精度形式の選択が重要
  • メモリ転送が詰まるとユニットが遊ぶ
  • 小さすぎる問題では効かない
よくある誤解 実際には
テンソルコアがあるなら常に速い メモリ供給や問題サイズが噛み合わないと遊ぶ
低精度にすれば必ず得 品質、カーネル対応、ランタイム最適化も必要
FLOPSが高ければ勝ち 実効性能は帯域、再利用、バッチ設計で揺れる

学習と推論で見え方が違う

  • 学習 は前向き計算、逆伝播、勾配更新を含むため、計算量もメモリ使用量も大きい
  • 推論 は学習より軽いことが多いが、レイテンシ、バッチサイズ、キャッシュ再利用が重要になる

とくにLLM推論では、行列演算そのものだけでなく、KV cache、つまり過去トークンのキーとバリューを保持するメモリ構造が性能と容量の大きな制約になります。

flowchart LR A["学習"] --> B["前向き + 逆伝播 + 更新"] B --> C["計算量もメモリ量も大きい"] D["推論"] --> E["前向き中心"] E --> F["レイテンシやKV cacheが効く"]
観点 学習 推論
主な重さ 前向き、逆伝播、更新 前向き、decode、cache管理
メモリの主役 activations、勾配、optimizer state 重み、KV cache、batching
重要指標 samples/sec、学習時間 TTFT、token/sec、p99
この章のひとこと

テンソルコアは強力ですが、実効性能は精度形式、問題サイズ、メモリ供給が噛み合ってはじめて出ます。


GPUプログラミングモデル

CUDA

CUDAは、ホストコードからカーネルを起動し、GPU上で大量スレッドを動かすモデルです。

flowchart LR A["CPU / ホストコード"] --> B["CUDA runtime / driver"] B --> C["kernel launch"] C --> D["grid / block / thread"] D --> E["GPU上で実行"]
__global__ void vec_add(const float* a, const float* b, float* c, int n) {
    int i = blockIdx.x * blockDim.x + threadIdx.x;
    if (i < n) {
        c[i] = a[i] + b[i];
    }
}

このコード自体は単純ですが、実際には

  • メモリ配置
  • ブロックサイズ
  • 転送コスト
  • 同期

が性能を左右します。

CUDAで最初に意識するもの 理由
blockサイズ warpの載り方やoccupancyに効く
メモリ配置 coalescingと帯域に効く
host / device往復 end-to-endの遅さに直結する
同期 待ち時間や分岐の足並みに効く

HIP / ROCm

HIPはCUDAに近い書き味を持ちつつ、AMD GPUを主対象にしたプログラミングモデルです。移植性を重視する現場ではかなり重要です。ROCmの公式資料でも、HIP、OpenCL、OpenMPを含む総合ソフトウェアスタックとして説明されています。

flowchart LR A["CUDA風の書き味"] --> B["HIP"] B --> C["AMD GPUで動かす入口"] B --> D["移植性を取りたい現場で有力"]

Metal

AppleのMetalは、グラフィックスとコンピュートを一貫したAPIで扱います。Apple GPUのfeature familyを前提に、スレッドグループ単位の設計が重要です。

Metalを実務で見るときは、「Apple GPU向けのcompute API」というだけでなく、

  • OS / ドライバ / GPU / メモリモデルをAppleが一体で設計している
  • graphicsとcomputeを同じパイプライン設計思想で扱える
  • unified memory前提でCPU / GPUのデータ配置設計が変わる

という点が大きいです。Apple系では、単にCUDAがない代替APIではなく、Apple向けに自然なGPUの入口 として理解した方が噛み合います。

flowchart LR A["Metal"] --> B["graphicsとcomputeを一体で扱う"] A --> C["Appleのunified memoryと噛み合う"] A --> D["OS / driver / GPUが一体で見やすい"]

Vulkan Compute

Vulkanは明示的な低レベルAPIで、グラフィックスだけでなくcomputeも扱えます。抽象化は少なめですが、制御の自由度が高いです。KhronosのVulkan Guideでも、Vulkanはgraphicsとcomputeの両方を必須機能として備えるAPIと整理されています。

flowchart LR A["Vulkan Compute"] --> B["かなり明示的な制御"] B --> C["同期や資源管理を自分で考える"] C --> D["学習コストは高いが長寿命基盤に向く"]

ランタイムで起きていること

どのAPIを使っていても、実際には次のような流れが起きています。

  1. CPU側でバッファやテクスチャなどの資源を用意する
  2. GPU用のカーネルやシェーダをコンパイル、またはロードする
  3. コマンドバッファやストリームへ処理を積む
  4. GPUがそれを非同期に実行する
  5. 必要なら結果をCPU側へ戻す

GPUプログラミングの難しさは、この「CPUの時間」と「GPUの時間」がずれる点にあります。見かけ上は1行のAPI呼び出しでも、裏では非同期に長い仕事が走ることが珍しくありません。

flowchart LR A["資源を作る"] --> B["コマンドを積む"] B --> C["GPUが非同期に実行"] C --> D["必要なら同期して結果取得"]

ROCm導入で先に確認したいこと

ROCmを本番候補として見るときは、性能を測る前に次を確認した方が安全です。

  1. 使うGPUが、そのROCmバージョンで正式サポートされているか
  2. Linux / Windows、カーネル、ドライバ、コンテナ環境の組み合わせが対応範囲か
  3. 使いたいフレームワーク機能がHIP / OpenMP / OpenCLのどこで乗るのか
  4. profilerやdebuggerを含めた運用ツールが現場の要件を満たすか

ROCmは「動くかどうか」だけでなく、どのレイヤで使うつもりか を先に決めると判断しやすくなります。

確認項目 先に見る理由
GPUとROCm版の対応 ここがずれると最初から詰まりやすい
OS / kernel / container条件 本番環境で再現できるかに直結する
HIP / OpenMP / OpenCLのどれで乗るか チームの書き方と資産継承に効く
profiler / debugger 運用フェーズの詰まり方が変わる
この章のひとこと

APIごとの違いは大きく見えても、共通する本質は「どう並列化し、どう置き、どう同期するか」です。


主要APIとエコシステム比較

項目 CUDA ROCm / HIP Metal Vulkan Compute OpenCL
主対象 NVIDIA AMD Apple クロスベンダ クロスベンダ
強み AI/HPC生態系が非常に強い オープン寄り、CUDA互換志向 Apple環境で最適 明示的制御、移植性 標準仕様
弱み ベンダ依存 対応差の検証が必要 Apple外では使えない 学習コスト高 実務中心からはやや後退
向く場面 学習・推論・HPC・産業用途 HPC・移植・AMD基盤 macOS / iOSのcomputeとgraphics エンジン、低レベル最適化 広範囲互換が必要なとき
flowchart LR A["CUDA"] --> A1["最短で強いAI / HPC生態系"] B["ROCm / HIP"] --> B1["AMD基盤 / 移植 / HPC"] C["Metal"] --> C1["Apple統合環境で自然"] D["Vulkan Compute"] --> D1["低レベル制御 / 長寿命基盤"] E["WebGPU"] --> E1["配布しやすさ / 安全性 / ブラウザ"]

実務上の見方

  • NVIDIA中心ならCUDAが最短
  • AMDを含めるならHIPを真剣に検討
  • AppleプラットフォームならMetalが基本
  • 低レベルで長寿命なクロスプラットフォーム基盤ならVulkan Computeを候補に入れる

API選定を1枚で見る

まず問うこと 強く候補になるもの 理由
既存のAI / 推論資産を最速で動かしたいか CUDA ライブラリ、推論エンジン、運用実績が厚い
AMD GPUを主戦場にしたいか ROCm / HIP HIP移植、HPC文脈、AMD公式スタックに乗りやすい
Apple製品の中で完結するか Metal OS、GPU、メモリモデルが一体で噛み合う
長寿命のクロスベンダ基盤が必要か Vulkan Compute ベンダ横断で低レベル制御がしやすい
配布容易性やブラウザ実行が重要か WebGPU インストール不要でGPU利用を届けやすい

API選定フロー

flowchart TD A["どこで動かすか"] --> B{"主戦場はブラウザか"} B -->|はい| C["WebGPUを第一候補"] B -->|いいえ| D{"Apple専用でよいか"} D -->|はい| E["Metalを第一候補"] D -->|いいえ| F{"NVIDIA中心か"} F -->|はい| G["CUDAを第一候補"] F -->|いいえ| H{"AMDを主対象に含むか"} H -->|はい| I["ROCm / HIPを検討"] H -->|いいえ| J["Vulkan ComputeやOpenCLを比較"]

もう一歩実務寄りに言うと、WebGPUは「性能の極限」よりも、

  • ブラウザでそのまま配れる
  • セキュアなサンドボックスでGPUを使える
  • OSやGPUベンダをまたぎやすい

ことに価値があります。ネイティブGPU APIの完全な代替ではありませんが、配布しやすいGPU計算 という意味ではかなり強いです。

主要ライブラリと周辺ツール

レイヤ 主な例 役割
基本線形代数 cuBLAS, rocBLAS 行列積やベクトル演算
深層学習基本演算 cuDNN, MIOpen 畳み込み、正規化、活性化など
マルチGPU通信 NCCL, RCCL All-reduceなどの集団通信
推論最適化 TensorRT など グラフ最適化、精度変換、実行計画
サービング Triton Inference Server など 推論API提供、バッチング、複数モデル管理
プロファイリング Nsight, ROCm profiler群, Xcode Instruments 実行時間、帯域、stall理由の可視化

APIを選ぶときの実務的な問い

  • どのGPUベンダを主対象にするのか
  • 手元で最速にしたいのか、長期の移植性を取りたいのか
  • 上位ライブラリの恩恵を強く受けるのか、独自カーネルを書くのか
  • AI推論やHPCのように、既存エコシステムが結果を大きく左右する分野か

WebGPU / WGSLをどう位置づけるか

WebGPUを誤解しやすいのは、「ブラウザ版CUDA」だと思ってしまうところです。実際には、

  • ブラウザでGPUを使える
  • 安全な実行モデルを重視している
  • WGSLという専用言語で、表現力と移植性のバランスを取っている

という設計です。

実務で向いているのは、

  • 画像処理や可視化のデモ
  • 学習教材
  • その場で試せる軽量推論
  • クライアントサイド処理の高速化

であって、巨大モデルの本格学習や極限チューニングの中心ではありません。逆に言えば、配布のしやすさと再現性 が価値になる場面ではかなり強いです。

flowchart LR A["ネイティブGPU API"] --> B["最高性能や細かい制御"] C["WebGPU / WGSL"] --> D["配布しやすさと安全性"]

どのレイヤで戦うか

レイヤ 主な選択肢 何を自分で背負うか
高レベル PyTorch, TensorFlow, 各種推論ランタイム カーネル詳細はあまり見ない
中レベル CUDA / HIPライブラリ, TensorRT, Triton モデル実行計画や運用設計
低レベル CUDA kernels, Metal shaders, Vulkan compute shaders メモリ配置、同期、実行粒度まで自分で考える

CPU・TPU・NPU・FPGAとの比較

装置 強いところ 弱いところ 向く処理
CPU 制御分岐、低遅延、汎用性 同種大量並列は不利 OS、DB、Web、制御系
GPU 高スループット、高帯域 分岐や小規模処理は不利 AI、HPC、描画、映像
TPU / NPU 行列特化、電力効率 汎用性が低い 推論、学習の特定部分
FPGA 低レイテンシ、カスタム回路 開発コスト高 通信、信号処理、超低遅延

GPUの立ち位置

GPUは、汎用性と性能のバランスが非常によい高速並列装置です。だからこそ広く使われています。CPUほど自由ではないが、TPU、つまり行列演算へかなり特化したAIアクセラレータや、NPU、つまり端末やSoCに載るAI専用演算器、FPGA、つまり回路自体を再構成できるデバイスほど用途を狭めなくてもよい、という中間の強さがあります。


主要ユースケース

3Dレンダリング

古典的なGPUの用途です。頂点処理、ラスタライズ、シェーディング、ポストプロセスなどを高速に行います。

深層学習

学習でも推論でも、GPUは中心的です。特に大規模行列演算と高帯域メモリが効きます。

学習では、巨大なミニバッチや活性値、勾配、オプティマイザ状態を持つため、計算能力だけでなくメモリ容量と通信が効きます。推論では、単発リクエストのレイテンシを詰めたい場面と、多数リクエストを束ねてスループットを稼ぎたい場面で最適解が変わります。

HPC

気象、分子動力学、有限要素法、数値流体、天体計算など、大規模数値計算に強いです。

映像処理

エンコード、デコード、画像フィルタ、超解像、映像推論などでGPUは非常に有効です。

データ分析

列指向処理、ベクトル演算、大規模フィルタリングやgroup-byの一部をGPUへ逃がすケースも増えています。

ただし、GPUが効きやすいのは列指向で連続アクセスしやすい処理です。複雑な結合、文字列処理、分岐だらけのUDFが支配的だと、期待ほど伸びないこともあります。

ブラウザとUI

CSS、Canvas、WebGL / WebGPU、動画再生など、多くのUIはすでにGPU依存です。

ブラウザ文脈では、専用GPUだけでなく統合GPUやモバイルGPUも重要です。ここでは純粋な最大性能より、電力効率、タイルベース最適化、メモリ共有モデルが効きます。

LLM推論

現代のGPUユースケースとして特に重要なのがLLM推論です。ここでは、

  • 重みをVRAMに載せ切れるか
  • KV cache をどれだけ効率よく保持できるか
  • リクエストをバッチングできるか
  • 量子化しても品質が許容されるか

が支配的です。単にFLOPSが高いだけでは足りず、VRAM容量、帯域、通信、サービングランタイムの賢さが性能を大きく左右します。

ユースケース別に見るGPUの論点

ユースケース とくに効く資源 よく詰まる点
3D描画 シェーダ、テクスチャ、帯域 overdraw、帯域、同期
学習 Tensor Core、HBM、通信 VRAM容量、all-reduce、チェックポイント
推論 VRAM、帯域、キャッシュ 小バッチ、KV cache、レイテンシ
HPC 倍精度性能、帯域 通信、境界条件、不規則アクセス
映像処理 メディアエンジン、帯域 I/O、フォーマット変換

性能最適化の勘所

GPU最適化でまず疑うべきは次の順です。CUDA Best Practices Guideでも、まずプロファイルを取り、実際のホットスポットとボトルネックを見つけることが高優先度で推奨されています。

  1. ホストとデバイスの転送が支配していないか
  2. グローバルメモリアクセスが非効率ではないか
  3. ダイバージェンスが激しくないか
  4. ブロックサイズやthreadgroupサイズが極端ではないか
  5. 小さすぎる問題をGPUへ投げていないか

典型的なボトルネック

  • PCIe、つまりCPUとGPUをつなぐ汎用高速バスの転送
  • メモリ帯域不足
  • L2 / shared memoryの使い方
  • occupancy低下
  • launch overhead

プロファイリング

GPUでは、推測より計測が重要です。一般に見るべき指標は、

  • カーネル実行時間
  • メモリ転送時間
  • SM / CU利用率
  • メモリ帯域利用率
  • warp stall理由

です。

compute boundとmemory bound

GPUカーネルは、大雑把には次の2つに分けて考えると整理しやすいです。

  • compute bound: 演算器が支配的で、もっと計算器が速ければ伸びる
  • memory bound: データ供給が支配的で、帯域やアクセス効率が悪い

この切り分けを間違えると、無意味な最適化へ時間を使いやすくなります。メモリ律速なのに演算を少し減らしても効果は薄く、逆に計算律速なのにアクセス最適化ばかりしても伸びません。

roofline的な見方

roofline model は、演算強度、つまり「1 byteあたりどれだけ計算するか」と、到達できる性能の上限を結びつけて考える枠組みです。細部は高度ですが、実務的には「そのカーネルは計算を増やしてもよいのか、まずデータ再利用を増やすべきか」を整理するのに役立ちます。

flowchart LR A["演算強度が低い"] --> B["memory boundになりやすい"] C["演算強度が高い"] --> D["compute boundになりやすい"]

最適化の実務チェックリスト

  1. まずend-to-endで時間を測る
  2. CPU時間、転送時間、GPUカーネル時間を分ける
  3. 最も重いカーネル1つに絞る
  4. そのカーネルが帯域律速か計算律速かを判定する
  5. アクセス、分岐、占有率、同期の順に疑う
  6. 改善後に必ず再計測する

この順番は、NVIDIAのCUDA Best Practices Guideの考え方ともかなり一致しています。要するにGPU最適化は、

  1. Assess: どこが重いかを測る
  2. Parallelize: 並列化できる部分を見極める
  3. Optimize: いちばん重い箇所だけを詰める
  4. Deploy: 改善後の全体効果を確認する

という循環で進めた方がうまくいきます。最初からoccupancyやshared memoryの細部へ飛び込むより、まずホットスポットの位置を測る方がずっと強いです。

典型的な改善パターン

  • 小さいカーネルをまとめる
  • データ転送回数を減らす
  • タイリングして共有メモリ再利用を増やす
  • ブロックサイズを調整する
  • 混合精度や量子化を使う
  • CPU側前処理を見直して分岐を減らす
この章のひとこと

最適化の主役は細かなテクニックより、まず「どこが支配的に遅いか」を正しく切ることです。


マルチGPUと相互接続

1枚のGPUでは足りない場合、複数GPUを束ねます。

接続

  • PCIe
  • NVLink、つまりNVIDIAのGPU間高帯域相互接続
  • Infinity Fabric系
  • CPUソケット越し
flowchart LR A["GPU 0"] --- B["GPU 1"] B --- C["GPU 2"] C --- D["GPU 3"] A --> E["PCIe / NVLink / IF"] B --> E C --> E D --> E
接続 何が嬉しいか どこで効くか
PCIe 一般的で広く使える 単体GPUや軽い分割
NVLink GPU間通信が太い 学習、巨大モデル推論
Infinity Fabric系 AMD系での高帯域接続 AMDクラスタ、HPC
CPUソケット越し 構成としては成立しやすい 通信が少ない場面

何が難しいか

マルチGPUでは、

  • データ分割
  • 通信
  • 同期
  • 負荷偏り

が難しくなります。単純に2倍、4倍にはなりません。

flowchart LR A["GPUを増やす"] --> B["計算資源は増える"] A --> C["通信も増える"] A --> D["同期も増える"] A --> E["負荷偏りも見えやすくなる"]

データ並列とモデル並列

AIでは、

  • データ並列
  • テンソル並列
  • パイプライン並列

などの並列化手法が使われます。

並列化 大まかに何を分けるか 向きやすい場面 詰まりやすい点
データ並列 入力バッチ 学習、独立リクエスト 勾配同期、集団通信
テンソル並列 同じ層の内部計算 巨大モデル推論 層ごとの通信待ち
パイプライン並列 層の塊 深いモデル ステージ間の空き時間
flowchart TB A["データ並列"] --> B["同じモデルを複製"] B --> C["別々の入力を処理"] D["テンソル並列"] --> E["1層の計算を分割"] F["パイプライン並列"] --> G["層の塊をGPUごとに担当"]

通信が支配的になる瞬間

マルチGPUでは、単体GPUの最適化とは別の世界が始まります。とくに学習では、勾配同期の all-reduce が支配的になりやすく、推論ではモデル並列の境界で待ちが増えます。ここではGPU自体の速さだけでなく、

  • GPU間リンクの帯域
  • トポロジ
  • 通信ライブラリ
  • バッチ設計

が結果を左右します。

flowchart LR A["単体GPU最適化"] --> B["十分速い"] C["GPUを増やす"] --> D["通信境界が増える"] D --> E["待ち時間が目立つ"] E --> F["リンク帯域とトポロジが支配する"]
症状 まず疑うこと
GPUを増やしても伸びない 通信待ち、トポロジ、バッチが小さすぎる
一部のGPUだけ忙しい 負荷偏り、分割が偏っている
学習は回るが推論が遅い tensor parallelの境界通信、scheduler

仮想化、分割、マルチテナンシー

GPUは高価なので、1人1枚専有ではもったいない場面が多いです。

MIG

NVIDIAの MIGMulti-Instance GPU の略で、1枚の物理GPUを複数の独立インスタンスへ分割する仕組みです。MIG User Guideの対応表では、Ampere世代以降で対応が始まり、2026年4月時点ではHopperやBlackwell系の対応GPUも並んでいます。

さらに2026年時点のNVIDIA公式ガイドでは、Hopper世代以降ではMIG有効化時にGPU resetが不要 という挙動差も明記されています。ここは運用上かなり重要です。Ampere世代の感覚で「有効化は重い操作」と思い込むと、世代差を見誤ります。

もうひとつ実務で見落としやすいのは、MIGモードの持続性がドライバ常駐に依存する ことです。つまり「一度設定すれば永続」と思い込むと、再起動やドライバ再読込時の自動化設計で詰まりやすくなります。

何が嬉しいか

  • マルチテナント運用しやすい
  • QoSを読みやすい
  • 小さなジョブを詰めやすい

何が難しいか

  • 分割設計
  • 再起動や再構成運用
  • Kubernetesとデバイスプラグイン連携
  • 監視粒度

MIGとtime slicingの違い

MIG はハードウェア資源を分割して、比較的強い分離を与える仕組みです。一方 time slicing は、時間で順番にGPUを使わせる共有です。

方式 強み 弱み
MIG 分離が強い、QoSを読みやすい 対応GPUが必要、構成変更が重い
time slicing 柔軟、導入しやすい 干渉を読みづらい、遅延が揺れやすい

小さい推論ジョブを多数収容したいのか、厳密な分離が必要なのかで選び方が変わります。

flowchart TB A["物理GPU"] --> B["インスタンス1"] A --> C["インスタンス2"] A --> D["インスタンス3"] B --> E["ワークロードA"] C --> F["ワークロードB"] D --> G["ワークロードC"]

運用・監視・トラブルシューティング

まず見るもの

  • GPU利用率
  • メモリ使用量
  • 温度と電力
  • 転送帯域
  • エラー
  • ドライバ / ランタイム版
観点 まず見る指標 何がわかるか
忙しさ GPU利用率、SM / CU利用率 そもそもGPUが埋まっているか
メモリ VRAM使用量、帯域、断片化 容量不足か、帯域不足か
転送 PCIe / NVLinkの使用率 CPU / GPU往復やGPU間通信の重さ
健全性 温度、電力、ECC / Xid 物理的な制約や障害の兆候
ソフトウェア ドライバ、ランタイム、ライブラリ版 再現性や相性問題
flowchart LR A["遅い / 不安定"] --> B["利用率"] B --> C["メモリ"] C --> D["転送"] D --> E["温度 / エラー"] E --> F["版差分"]

典型的なトラブル

1. GPU利用率が低い

  • CPU側で詰まっている
  • バッチサイズが小さい
  • 転送待ちが長い
  • カーネルが小さすぎる

ここでいうGPU利用率は1指標にすぎません。利用率が低くても、短いバースト処理なら正常なことがありますし、逆に利用率が高くてもメモリ待ちだらけで効率が悪いこともあります。

2. メモリ不足

  • モデルやバッチが大きすぎる
  • 一時バッファが多い
  • フラグメント化
  • mixed precisionを使っていない

3. 速くならない

4. 再現性が揺れる

  • 並列reductionの順序差
  • 非決定的アルゴリズム
  • 原子操作順序
症状 よくある原因 最初の一手
GPU利用率が低い CPU側待ち、バッチ不足、転送待ち end-to-endをCPU / GPUに分解する
メモリ不足 バッチ過大、断片化、保持しすぎ VRAM内訳と一時バッファを確認する
速くならない GPU不向き、launch細切れ、I/O支配 最重カーネルと転送時間を分ける
再現性が揺れる reduction順序、非決定的kernel deterministic設定と版差分を見る

監視で見たいメトリクス

  • GPU使用率
  • SM / CU使用率
  • メモリ使用量と残量
  • メモリ帯域利用率
  • PCIe / NVLink転送
  • 温度、消費電力、クロック低下
  • ECCやXidなどのハードウェアエラー
  • キュー長、推論待ち時間、バッチサイズ
flowchart TB A["監視"] --> B["計算資源"] A --> C["メモリ"] A --> D["転送"] A --> E["ハードウェア健全性"] A --> F["アプリケーションキュー"]

切り分けの順番

  1. まずend-to-endの遅さか、GPU単体の遅さかを分ける
  2. CPU / I/O / 転送が支配していないか確認する
  3. GPU内では最も重いカーネルを特定する
  4. メモリ律速か計算律速かを判断する
  5. 再現性の揺れやドライバ差分を確認する
flowchart TD A["遅い / おかしい"] --> B{"end-to-endの問題か"} B -->|はい| C["CPU / I/O / 転送を切る"] B -->|いいえ| D["GPU内の最重カーネルを探す"] D --> E{"memory boundか"} E -->|はい| F["帯域 / アクセス / cacheを見る"] E -->|いいえ| G["演算 / 分岐 / occupancyを見る"] F --> H["再計測"] G --> H
この章のひとこと

運用では、利用率だけで結論を出さず、計算・メモリ・転送・ソフトウェア層を順に分けて見るのが近道です。


GPUが向かない処理

GPUを使えば常に速いわけではありません。典型的に向きにくいのは次です。

  • 単発で短い処理
  • ポインタ追跡が多い
  • 強い逐次依存
  • 分岐だらけのロジック
  • データ量が少ない
  • CPUとの往復が多い

判断の基本

「同じ処理を大量データへ当てるか」「データをGPU上にしばらく留められるか」をまず見ると、大きく外しにくいです。

GPUを使わない方がよい具体例

  • 1回しか呼ばない小さな前処理
  • 複雑な木構造やグラフをポインタでたどる処理
  • 1件ごとに強い分岐が入るルールエンジン
  • リクエストごとにデータ転送だけで終わるような処理

こうしたケースでは、CPUでまず速く作る方が全体最適になることがよくあります。


現在の動向

AI中心の設計が続く

GPUは引き続きAIインフラの中心です。行列演算性能、HBM容量、相互接続、ソフトウェアスタックが差別化点になっています。

GPU分割と共有の重要性が上がる

高価なGPUを効率よく使うため、MIGやKubernetes連携、ワークロード分離の重要性がさらに上がっています。

CUDAとROCmの二極化が濃くなる

NVIDIA中心のCUDA優位は強いままですが、AMD ROCm / HIPもHPCと一部AIワークロードで存在感を増しています。2026-03-10更新のROCm公式docsでも、HIP、OpenCL、OpenMPを含む統合スタックとして整理されています。

AppleはMetal / Unified Memoryを一体で進化

AppleのGPUは、Metal feature familyとunified memory前提の設計が重要です。CPU / GPU / NPUの境界をまたぐ設計判断が特徴的です。

WebGPU / Vulkan Computeの裾野拡大

ブラウザやクロスプラットフォームcomputeの文脈で、WebGPUやVulkan Computeの重要性も増しています。低レベルAPIへの理解が、グラフィックスだけでなく汎用計算にも効きます。

WebGPUの現在地を雑に言うと、「ブラウザでGPUをまじめに使うための標準の入口」 です。WGSLという専用シェーディング言語と組み合わせて、

  • 画像処理
  • 軽量な機械学習推論
  • 可視化
  • 教材・デモ

のような用途でかなり扱いやすくなっています。ネイティブAPIの完全な代替ではありませんが、「配布しやすいGPU計算」という意味ではとても重要です。

サービングランタイムの重要性が上がる

現在の実務では、GPUハードウェア単体よりも、推論サーバ、バッチング戦略、量子化、キャッシュ管理、マルチテナント制御の差で結果が大きく変わる場面が増えています。とくにLLMでは、GPUを「どう使うか」のソフトウェア層が性能の大部分を決めることも珍しくありません。


判断の指針

GPUを使うべき場面

  • 行列演算やテンソル演算が中心
  • 大量データへ同じ処理を適用
  • バッチ処理でまとめられる
  • 高メモリ帯域が必要
  • レイテンシよりスループットが重要

CPUのままの方がいい場面

  • 制御分岐中心
  • 小規模処理
  • 逐次依存が強い
  • 転送が支配的

まず小さく検証する

実務では、最初からGPU前提で巨大設計にするより、

  1. CPU実装のボトルネックを確認
  2. GPU化候補を分離
  3. 小さくベンチマーク
  4. 転送コスト込みで判断

の順が安全です。

導入判断の5つの質問

  1. この処理には大量並列にできる部分が本当にあるか
  2. データはGPU上へしばらく置いて再利用できるか
  3. 既存ライブラリで大部分を賄えるか
  4. 運用で必要な監視、分割、共有方式を用意できるか
  5. GPUコストに見合うスループット改善があるか
この章のひとこと

GPU導入判断は、性能だけでなく、データ再利用、運用、既存資産まで含めた全体最適で見る方がぶれません。


LLM推論とGPUアーキテクチャ

LLM推論は、現代のGPUを理解するうえで最も重要な実例のひとつです。ここでは「なぜLLMがGPUを強く必要とするのか」と「どこで詰まるのか」を、ハードウェアとランタイムの両面から整理します。

要点

LLM推論では、重み読み出し、行列演算、KV cache、デコードの逐次性、バッチング、GPU間通信が主要論点です。単純なFLOPS競争ではなく、VRAM容量帯域キャッシュ管理サービングランタイム の総合戦になります。

この章でわかること

prefilldecode の違い、KV cache がなぜ性能と容量の中心になるか、推論サーバが何を吸収しているか、そして TensorRTTritonvLLM の役割分担が見えるようになります。

推論の大まかな流れ

flowchart LR A["入力トークン"] --> B["prefill既存文脈を一気に処理"] B --> C["KV cacheを構築"] C --> D["decode 1トークンずつ生成"] D --> E["次トークンを出力"] E --> F{"終了条件"} F -->|続く| D F -->|終了| G["応答完了"]

LLM推論の時間感覚

flowchart LR A["リクエスト到着"] --> B["トークナイズ / 前処理"] B --> C["prefill"] C --> D["decodeを反復"] D --> E["最終応答"] C -. "ここでTTFTが決まりやすい" .-> C D -. "ここでtoken/secが決まりやすい" .-> D

prefillとdecode

  • prefill は、最初に与えられた文脈全体を処理して内部状態を作る段階です
  • decode は、その後1トークンずつ自己回帰的に続きを生成する段階です

prefillは比較的大きな行列演算をまとめて流しやすく、GPUのスループットを出しやすいです。decodeは1ステップごとの逐次依存が強いため、学習やprefillよりレイテンシの影響を受けやすくなります。

観点 prefill decode
主な性質 文脈を一気に流す 1トークンずつ進む
GPU的な見え方 大きいGEMMを回しやすい 小さめの反復で帯域や待ちが効きやすい
詰まりやすい点 長文入力、重み読み出し KV cache、逐次性、バッチの揺れ
何を見たいか スループット TTFT / token/sec / p99

この比較表を頭に入れておくと、「推論が遅い」と言われたときに、どの段階が遅いのか を切り分けやすくなります。

flowchart LR A["prefill"] --> A1["長い入力をまとめて処理"] A1 --> A2["大きなGEMMが多い"] A2 --> A3["GPUスループットを出しやすい"] B["decode"] --> B1["1トークンずつ進む"] B1 --> B2["KV cacheを何度も参照"] B2 --> B3["帯域 / 逐次性 / 待ちが効く"]

推論サーバの内部イメージ

flowchart TB A["HTTP / gRPCリクエスト"] --> B["scheduler"] B --> C["batch builder"] C --> D["prefill queue"] C --> E["decode queue"] D --> F["GPU kernels"] E --> F F --> G["KV cache manager"] G --> H["出力ストリーム"]

推論サーバが吸収しているもの

具体的な仕事 ここが弱いと起きること
受付層 HTTP / gRPC, 認証, レート制御 GPUは空いているのに外側で詰まる
スケジューラ 優先度、同時実行数、キュー制御 tail latencyが悪化する
バッチャ continuous batching, 長短混在制御 GPUが遊ぶ、または待ち行列が崩れる
実行層 kernels, attention, GEMM 生の計算性能が出ない
メモリ層 KV cache, weight residency, prefix cache VRAM枯渇や断片化が起きる

KV cacheが重要な理由

KV cache は、過去トークンのattention用キーとバリューを保存しておく仕組みです。これがあると、毎回すべての文脈を最初から計算し直さずに済みます。ただし代償として、

  • 長いコンテキストほどVRAMを圧迫しやすい
  • 小さなリクエストが多数来ると断片化しやすい
  • マルチテナント環境では管理が難しい

という問題が出ます。vLLMのdocsでも、PagedAttentionAutomatic Prefix CachingQuantized KV CacheSpeculative Decoding といった仕組みが前面に出ています。

つまりLLM推論では、重みが主役なのはもちろんですが、運用に入ると KV cache管理がもう一つの主役 になります。ここが画像分類や古典的なDNN推論とかなり違うところです。

flowchart LR A["入力が長くなる"] --> B["保持する過去トークンが増える"] B --> C["KV cache容量が増える"] C --> D["VRAM圧迫と断片化が起きやすい"] D --> E["同時接続数やtail latencyに効く"]

LLM推論でGPUに効く資源

資源 何に効くか 典型的な詰まり方
演算性能 大きなGEMM、attention、MLP 小バッチで演算器が遊ぶ
VRAM容量 重みとKV cacheの保持 コンテキスト長や同時接続数で枯渇
メモリ帯域 重み読み出し、cacheアクセス decodeが帯域律速になる
相互接続 マルチGPUでの分割推論 通信待ちが支配的になる
ランタイム最適化 batching、prefix cache、spec decode 実装差で性能差が大きく出る

資源と症状の対応

症状 まず疑う資源 典型的な観点
TTFTが高い 演算性能、前処理、prefill設計 長文入力、重み常駐、warmup
token/secが伸びない 帯域、KV cache、decode設計 小バッチ、cache参照、逐次性
同時接続で崩れる VRAM容量、KV cache pool 断片化、予約戦略、eviction
マルチGPUで伸びない 相互接続、分割方式 tensor parallel, pipeline parallel, NCCL

なぜ「推論サーバ」が重要なのか

単体のGPUカーネルが速くても、実サービスではそれだけでは足りません。実際には、

  • リクエストがばらばらのタイミングで到着する
  • モデルごとに最適なバッチサイズが違う
  • 会話の長さが偏る
  • KV cacheの寿命管理が必要
  • 複数モデルやLoRAを共存させたい

といった運用課題があります。ここで、TensorRTTriton Inference ServervLLM のようなソフトウェアが効いてきます。

flowchart TB A["GPUが速い"] --> B["それだけでは足りない"] B --> C["schedulerが必要"] B --> D["batchingが必要"] B --> E["KV cache管理が必要"] B --> F["複数モデル運用が必要"] C --> G["推論サーバの価値"] D --> G E --> G F --> G

代表的な推論スタック

レイヤ 主な例 役割
カーネル / 基本演算 CUDA, CUTLASS, FlashAttention attentionやGEMMの高速化
推論エンジン TensorRT モデル最適化、実行計画生成
サービング Triton Inference Server, vLLM API提供、バッチング、メモリ管理
分散推論 NCCL, 各種parallel runtime GPU間通信、分散実行
flowchart LR A["モデル"] --> B["推論エンジン"] B --> C["サービングランタイム"] C --> D["API / ストリーミング応答"] B --> E["単体GPU最適化"] C --> F["batching / KV cache / queue制御"]

推論基盤は、単にモデルをGPUへ載せるだけではありません。TensorRTのような推論最適化、Tritonのような推論サーバ、vLLMのようなLLM向けランタイムを組み合わせ、FP32/FP16/BF16/FP8/INT8、dynamic shapes、batching、KV cache、prefix caching、speculative decodingなどを使って、速度、コスト、品質のバランスを取ります。

OpenAI互換サービングの最小イメージ

from openai import OpenAI

client = OpenAI(
    base_url="http://localhost:8000/v1",
    api_key="dummy",
)

resp = client.chat.completions.create(
    model="my-llm",
    messages=[
        {"role": "user", "content": "GPUとCPUの違いを説明して"}
    ],
    max_tokens=128,
)

print(resp.choices[0].message.content)

このようなクライアント例が意味を持つのは、サーバ側でbatching、KV cache、モデルロード、ストリーミング、スケジューリングを吸収してくれるからです。

量子化とGPU

量子化 は、重みやactivationsを低ビット化して、容量と帯域を削減する手法です。推論ではとくに重要です。

  • FP16 / BF16: 比較的扱いやすい
  • INT8: 容量削減と速度改善のバランスを取りやすい
  • INT4 / FP4 / FP8 系: より攻めた圧縮だが、対応ハードウェアと品質検証が重要

量子化は「何bitまで下げられるか」だけでなく、「そのGPUとランタイムで速く動くか」が重要です。ビット数が低くても、実装が弱ければ逆に遅くなることがあります。

形式 何が嬉しいか 注意点
FP16 / BF16 精度と扱いやすさのバランスがよい 容量削減は中程度
INT8 容量と速度の両面で効きやすい calibrationやkernel対応が大事
INT4 / FP4 / FP8 さらに強く圧縮できる 品質、実装成熟度、対応GPUを要確認

FlashAttentionが効く理由

FlashAttention は、attentionを単に数式として最適化するだけでなく、GPUのメモリ階層に合わせて再構成する代表例です。ポイントは、

  • 大きなattention行列をそのまま全部書き出さない
  • タイル単位で計算して、共有メモリやレジスタで再利用する
  • 帯域ボトルネックを緩和する

ことにあります。これはGPU最適化全般にも通じる考え方で、「計算そのもの」より「中間結果をどこへ置くか」が勝負になる例です。

TensorRTの役割を分けて見る

TensorRTは、学習フレームワークそのものではなく、推論実行計画の最適化と実行 に強い層です。

  • モデルグラフを解析する
  • 演算を融合する
  • 精度を下げられる場所を見つける
  • GPUに合った実行planを作る

つまりTensorRTは、手書きCUDAの代わりではなく、「既存モデルをNVIDIA GPU向けに本番用へ整える層」と見ると理解しやすいです。

vLLMが注目される理由

vLLMが強く注目されるのは、単にOpenAI互換APIを出せるからではありません。実務では、

といった、「LLMサービングの痛点」へ直接答えていることが大きいです。GPUハードウェアの性能差が小さい場面でも、こうしたランタイムの差で実効性能が大きく変わります。

speculative decoding

speculative decoding は、小さな補助モデルや予測機構で先読み候補を出し、大きな本体モデルでまとめて検証する発想です。うまくはまるとdecodeの逐次性を少し崩せますが、

  • モデルの組み合わせが難しい
  • 外れが多いと得をしない
  • メモリと実装が複雑になる

というトレードオフがあります。

flowchart LR A["小さな補助モデルが先読み"] --> B["候補トークン列を作る"] B --> C["大きな本体モデルでまとめて検証"] C --> D{"当たりが多いか"} D -->|はい| E["decodeの逐次性を少し崩せる"] D -->|いいえ| F["やり直しが増えて得しにくい"]

推論運用で見るべきキュー

キュー / 状態 何を見るか 崩れるとどうなるか
prefill queue 長文入力の偏り time to first tokenが悪化
decode queue 短文と長文の混在 tail latencyが増える
KV cache pool 割当効率、断片化 同時接続数が伸びない
model load state warmup、切替頻度 初回応答が大きく揺れる

LLM推論でのマルチGPU

巨大モデルでは1枚に重みが載り切らないため、分割推論が必要になります。

  • tensor parallel: 同じ層の計算をGPU間で分ける
  • pipeline parallel: 層の塊ごとにGPUを分ける
  • data parallel: 独立リクエストを複数GPUへ分散する
  • context parallel などの派生手法: 長い入力を分散して扱う

推論では、学習ほど巨大なall-reduceが常に支配するわけではありませんが、境界の通信待ちは依然として大きな論点です。

flowchart LR A["1枚に載る"] --> B["単体GPU推論"] C["1枚に載らない"] --> D["tensor parallel"] C --> E["pipeline parallel"] C --> F["複数ノード分散"] D --> G["通信待ちが増えやすい"] E --> G F --> G

LLM推論でよくある失敗

  • 重みは載るがKV cacheでVRAMが足りなくなる
  • 小さすぎるバッチでGPUが遊ぶ
  • 長文リクエストが混ざって待ち行列が崩れる
  • 量子化はしたが、実装が最適化されておらず速くならない
  • サービス全体ではCPU前処理やトークナイザが詰まっている

LLM推論を評価するときの観点

  1. time to first token
  2. tokens per second
  3. 同時接続時のスループット
  4. 長文混在時のtail latency
  5. GPUメモリ効率
  6. モデル切替やLoRA混在時の挙動

ケーススタディ: 小さなLLMサービスが遅い

よくある状況として、「GPUを積んだのに応答が遅い」ケースがあります。たとえば、

  • 同時接続は少ない
  • 1リクエストごとにすぐ実行している
  • バッチングが弱い
  • トークナイザと前処理がCPUに残っている

という状態では、GPU側の演算性能が高くても全体は伸びません。この場合の改善順は、

  1. time to first token とCPU時間を分けて測る
  2. continuous batchingを導入する
  3. KV cacheの断片化とVRAM使用率を見る
  4. 量子化やprefix cachingを検討する

となります。ここでは「GPUが遅い」のではなく、「GPUへ仕事を届ける流れ」が遅いことが多いです。

この章のひとこと

LLM推論では、GPUの速さそのものより、prefill / decode / KV cache / batchingをどう束ねるかで体感速度が決まりやすいです。


GPU最適化の実践パターン

ここでは、実務でGPUカーネルやGPUワークロードを改善するときの考え方を、手順ベースで整理します。GPU最適化は魔法ではなく、計測 -> 仮説 -> 変更 -> 再計測 の繰り返しです。

この章でわかること

どこから最適化を始めるか、memory boundcompute bound をどう見分けるか、タイリングや融合がなぜ効くか、そしてend-to-endで効いたかをどう判断するかを整理します。

全体最適から始める

最適化で最初にやるべきことは、最も遅い部分を正確に見つけることです。

flowchart LR A["end-to-end計測"] --> B["CPUとGPUを分離"] B --> C["最重カーネル特定"] C --> D["memory bound / compute bound判定"] D --> E["最適化"] E --> F["再計測"]

実践パターン1: 転送削減

もっともよく効くのに見落とされやすいのが、CPUとGPUの往復を減らすことです。

  • 前処理をGPU側へ寄せる
  • 複数カーネルの間でデータをGPU上へ保持する
  • 小分け転送をまとめる
  • pinned memoryを使って転送効率を上げる

典型例

  • 画像をCPUでdecode -> normalize -> resize -> GPU送信、ではなく一部をGPU側へ寄せる
  • 各ステップごとにCPUへ戻さず、後段のカーネルまでGPUメモリ上でつなぐ

悪い例と良い例

悪い例:
CPU preprocess -> GPU kernel A -> CPU copy back -> CPU preprocess -> GPU kernel B

良い例:
CPU preprocess once -> GPU kernel A -> GPU kernel B -> GPU kernel C -> CPU copy back once
flowchart LR A["悪い流れ"] --> B["CPU -> GPU"] B --> C["GPU -> CPU"] C --> D["CPU -> GPU"] D --> E["往復が多い"] F["良い流れ"] --> G["CPU -> GPU一度だけ送る"] G --> H["GPU上で続けて処理"] H --> I["最後に一度だけ戻す"]

実践パターン2: アクセスの規則化

GPUは規則的なアクセスに強いので、データ配置を変えるだけで大きく伸びることがあります。

  • Array of StructuresStructure of Arrays へ変える
  • ランダムアクセスをソートやバケット分割で緩和する
  • 近いスレッドが近いアドレスを触るよう並べ替える

AoSとSoAのイメージ

AoS:
[{x,y,z}, {x,y,z}, {x,y,z}]

SoA:
x = [x,x,x]
y = [y,y,y]
z = [z,z,z]

GPUでは、多数スレッドが同じ属性を一斉に読むならSoAの方が有利になりやすいです。

flowchart TB A["AoS"] --> B["属性が交互に並ぶ"] B --> C["同じ属性をまとめて読むのが苦手"] D["SoA"] --> E["同じ属性が連続して並ぶ"] E --> F["coalesced accessに寄せやすい"]

実践パターン3: タイリング

タイリング は、グローバルメモリから読む大きなデータを小片へ分け、共有メモリやキャッシュで再利用しやすくする考え方です。行列積で頻出ですが、画像処理やstencil計算でも重要です。

flowchart LR A["大きな入力行列"] --> B["タイルへ分割"] B --> C["1タイルずつshared memoryへ読む"] C --> D["その場で何度も再利用"] D --> E["グローバル読み出しを減らす"]

タイリングが効く理由

タイリングの落とし穴

良いこと 代償
再利用が増える shared memoryを多く食う
帯域律速を緩和しやすい 実装が複雑になる
演算強度を上げやすい block設計を誤ると逆効果になる

CUDA風のタイリング断片

__shared__ float tileA[16][16];
__shared__ float tileB[16][16];

int row = blockIdx.y * 16 + threadIdx.y;
int col = blockIdx.x * 16 + threadIdx.x;
float acc = 0.0f;

for (int t = 0; t < K; t += 16) {
    tileA[threadIdx.y][threadIdx.x] = A[row * K + t + threadIdx.x];
    tileB[threadIdx.y][threadIdx.x] = B[(t + threadIdx.y) * N + col];
    __syncthreads();

    for (int k = 0; k < 16; ++k) {
        acc += tileA[threadIdx.y][k] * tileB[k][threadIdx.x];
    }
    __syncthreads();
}

C[row * N + col] = acc;

この例の本質は、「グローバルメモリから小片を読み、共有メモリへ置き、そこで再利用する」ことです。

実践パターン4: カーネル融合

カーネル融合 は、連続する小さなカーネルをまとめて、途中結果をレジスタや共有メモリに留める考え方です。

  • launch overheadを減らせる
  • 中間結果のグローバルメモリ往復を減らせる
  • ただしレジスタpressureは増えやすい
観点 分割カーネル 融合カーネル
launch overhead 多い 少ない
中間結果 いったん外へ出やすい 内部に留めやすい
実装難易度 低め 高め
register pressure 低め 上がりやすい

融合のイメージ

flowchart LR A["kernel A"] --> B["global memory write"] B --> C["kernel B"] D["fused kernel A+B"] --> E["register / shared memory内で継続"]

実践パターン5: 精度の見直し

混合精度や低精度化は、AIワークロードで特に大きな武器です。

  • FP32からFP16 / BF16へ下げる
  • Tensor Coreが効く形へ揃える
  • INT8やFP8が使えるなら精度検証込みで試す

実践パターン6: バッチ設計

GPUはある程度まとめて仕事を流した方が効率が出ます。バッチが小さすぎると、演算器も帯域も使い切れません。ただしレイテンシ要求が強い場合は、巨大バッチが正義ではありません。

目的 取りやすい戦略 代償
レイテンシ最優先 小さめバッチ、即実行 GPU利用率が落ちやすい
スループット最優先 大きめバッチ、連続batching 個別応答が遅れやすい
flowchart LR A["小さいバッチ"] --> B["早く返しやすい"] B --> C["GPUが遊びやすい"] D["大きいバッチ"] --> E["GPUを埋めやすい"] E --> F["個別応答は遅れやすい"]

推論バッチの擬似コード

pending = []

while True:
    req = maybe_pop_request()
    if req is not None:
        pending.append(req)

    if should_launch_batch(pending):
        batch = build_batch(pending)
        launch_gpu_inference(batch)
        pending = keep_unfinished_requests(pending)

実際のサービングランタイムはこれをはるかに複雑にしていますが、本質は「いつ束ねるか」と「どこで待たせるか」です。

実践パターン7: 分岐の整理

ダイバージェンスが強いなら、

  • 先にCPU側で分類して近い要素をまとめる
  • ifを複数カーネルへ分ける
  • predicationやmaskで揃える

といった方法が候補です。

実践パターン8: ライブラリを疑う

手書きカーネルの前に、まず既存ライブラリが十分最適化されているかを確認します。

  • GEMMは cuBLAS / rocBLAS
  • DNN基本演算は cuDNN / MIOpen
  • 通信は NCCL / RCCL

これらで足りるなら、手書きは最後です。

最適化の失敗パターン

  • occupancyだけ上げて満足する
  • 単体カーネルだけ速くしてend-to-endは変わらない
  • ベンチマーク入力が実運用と違いすぎる
  • コンパイラやドライバ更新で結果が揺れているのに気づかない
  • GPU利用率だけ見て「使い切れている」と誤解する

before / afterで見る改善の思考

改善前 改善後 期待効果
小さなカーネルを多数起動 融合してまとめる launch overhead減
ランダムアクセス 連続配置へ並べ替え 帯域効率向上
CPU-GPUを毎回往復 GPU上で連鎖実行 転送削減
FP32固定 mixed precision 演算効率と容量改善
手書き実装 最適化済みライブラリ 開発工数削減と性能安定

実務での進め方

  1. 現状のend-to-end指標を固定する
  2. もっとも重い1箇所だけを選ぶ
  3. 変更の狙いを1つに絞る
  4. 効いたかどうかを数値で比較する
  5. 次の1箇所へ進む

小さな最適化レポートの型

項目
対象 attention kernel
現状 18 ms
仮説 global memory readが多い
変更 block size調整 + shared memory tiling
結果 12 ms
副作用 register pressure増、VRAMは不変

ベンチマーク結果の読み方

GPUベンチマークは、単に ms が小さいかどうかだけでは不十分です。最低でも次の観点を一緒に見ます。

観点 何を見るか ありがちな誤読
end-to-end時間 ユーザーが実際に待つ時間 カーネルだけ速くして満足する
カーネル時間 GPU上の純粋な計算時間 転送やCPU前処理を無視する
スループット req/s, tokens/s, images/s 単発レイテンシと混同する
tail latency p95, p99 平均値だけ見て安心する
メモリ使用量 VRAM, KV cache, 一時バッファ 空きが少ないだけで危険と判断する
再現性 複数回のばらつき 1回の良い結果だけ採用する

ベンチマークを比べるときの前提条件

比較には、少なくとも次を揃える必要があります。

  • GPU型番
  • ドライバ / ランタイム / ライブラリ版
  • バッチサイズ
  • 精度形式
  • 入力サイズ
  • warmup有無
  • CPU側前処理の含み方

このどれかが違うだけで、比較はかなり危うくなります。

ありがちな罠

  • cold startを混ぜたまま比較する
  • 異なる精度の結果を同列比較する
  • 単発の最速値だけを載せる
  • 実運用より小さすぎる入力で測る
  • サービス系なのにp99を見ない

ケーススタディ: 行列積カーネルが遅い

行列積でありがちな失敗は、「各要素ごとにその場で全部読む」実装です。これだと、

  • グローバルメモリ読み出しが多すぎる
  • 同じデータを何度も読み直す
  • 演算器より先に帯域が詰まる

となります。改善の典型は、

  1. タイリングしてshared memory再利用を増やす
  2. block sizeを調整する
  3. 可能ならTensor Coreが効く精度へ寄せる
  4. 最後に cuBLAS などと比較する

です。ここでのゴールは「自作最適化が速いこと」ではなく、「既存ライブラリで十分か、自作が必要か」を判断することです。

ケーススタディ: 推論は速いがサービスは遅い

GPU単体ベンチマークでは十分速いのに、実サービスの応答が悪いケースも多いです。この場合は、

  • HTTP
  • トークナイズ
  • バッチ待ち
  • モデル切替
  • 出力整形

のどれかが支配していることがあります。ここでGPUカーネルだけを最適化しても、体感は変わりません。まず time to first tokenGPU active time を分けるのが先です。

この章のひとこと

GPU最適化の成功は、単体カーネルの最速値より、end-to-endで何が改善したかで判断するのが安全です。


CUDA・ROCm・Metal・Vulkanのコードモデル比較

GPUプログラミングはAPIごとに書き方が違いますが、根本の考え方はかなり共通です。ここでは「何が同じで、何が違うか」をコードモデルの観点から整理します。

この章でわかること

CUDAHIP / ROCm、Metal、Vulkan Computeが、どこで似ていて、どこで設計思想が違うのかを、最小コード例と資源束縛、実行投入、学習コストの観点で比較します。

4つに共通する基本構造

どのAPIでも、概念的には次の5段階があります。

  1. バッファや資源を確保する
  2. GPUで走る関数を用意する
  3. 実行単位の大きさを決める
  4. コマンドをキューへ積む
  5. 必要なら同期して結果を読む

少し大きいコード骨格で見る

最小ベクトル加算だけだと、実務で本当に悩む メモリ確保資源束縛dispatch設定 の感覚が見えにくいです。そこで、実際に書くときの骨格を簡略化して並べます。

最小ベクトル加算で見る発想

CUDA / HIP的な見え方

  • __global__ なカーネルを定義する
  • threadIdx, blockIdx, blockDim から自分のindexを作る
  • gridとblockの大きさを起動時に決める

このモデルは「明示的にスレッド階層を考える」感覚が強いです。HIPもかなり近い書き味を取ります。

__global__ void vec_add(const float* a, const float* b, float* c, int n) {
    int i = blockIdx.x * blockDim.x + threadIdx.x;
    if (i < n) {
        c[i] = a[i] + b[i];
    }
}
float *d_a, *d_b, *d_c;
cudaMalloc(&d_a, bytes);
cudaMalloc(&d_b, bytes);
cudaMalloc(&d_c, bytes);

cudaMemcpy(d_a, h_a, bytes, cudaMemcpyHostToDevice);
cudaMemcpy(d_b, h_b, bytes, cudaMemcpyHostToDevice);

dim3 block(256);
dim3 grid((n + block.x - 1) / block.x);
vec_add<<<grid, block>>>(d_a, d_b, d_c, n);

cudaMemcpy(h_c, d_c, bytes, cudaMemcpyDeviceToHost);

この書き味では、「自分でメモリ確保し、自分で起動形を決める」感覚が強いです。

Metal的な見え方

  • [[kernel]] 関数を定義する
  • threadpositioningridthread_position_in_grid やthreadgroupサイズを使う
  • command bufferとcompute encoderを作ってdispatchする

MetalはAppleプラットフォーム全体との統合が強く、resource bindingやpipeline stateの扱いがAPI設計に深く入っています。

#include <metal_stdlib>
using namespace metal;

kernel void vec_add(
    device const float* a [[buffer(0)]],
    device const float* b [[buffer(1)]],
    device float* c [[buffer(2)]],
    uint id [[thread_position_in_grid]]
) {
    c[id] = a[id] + b[id];
}
let commandBuffer = commandQueue.makeCommandBuffer()!
let encoder = commandBuffer.makeComputeCommandEncoder()!
encoder.setComputePipelineState(pipelineState)
encoder.setBuffer(bufferA, offset: 0, index: 0)
encoder.setBuffer(bufferB, offset: 0, index: 1)
encoder.setBuffer(bufferC, offset: 0, index: 2)
encoder.dispatchThreads(gridSize, threadsPerThreadgroup: tgSize)
encoder.endEncoding()
commandBuffer.commit()
commandBuffer.waitUntilCompleted()

Metalでは、pipeline statecommand buffer の存在感が強く、アプリ全体との統合感があります。

Vulkan Compute的な見え方

  • compute shaderを用意する
  • pipelineとdescriptor setを作る
  • command bufferにdispatchを積む

Vulkanは最も明示的です。柔軟ですが、学習コストは高めです。

#version 450
layout(local_size_x = 256) in;

layout(set = 0, binding = 0) readonly buffer A { float a[]; };
layout(set = 0, binding = 1) readonly buffer B { float b[]; };
layout(set = 0, binding = 2) writeonly buffer C { float c[]; };

void main() {
    uint i = gl_GlobalInvocationID.x;
    c[i] = a[i] + b[i];
}
Vulkan側の骨格:
1. bufferを作る
2. descriptor setへ束縛する
3. compute pipelineを作る
4. command bufferにbind + dispatchを記録する
5. queue submitして完了を待つ

Vulkanは最も冗長に見えますが、そのぶん資源や同期を細かく制御できます。

4方式を俯瞰する図

flowchart TB A["CUDA HIP"] --> B["kernel launch"] C["Metal"] --> D["command buffer + compute encoder"] E["Vulkan"] --> F["pipeline + descriptor + dispatch"] B --> G["GPU execution"] D --> G F --> G

用語対応表

論点 CUDA HIP / ROCm Metal Vulkan
GPU上の関数 kernel kernel kernel function compute shader
協調実行単位 thread block block threadgroup workgroup
資源束縛 runtime APIで比較的簡便 CUDA互換寄り buffer / texture index descriptor set
実行投入 kernel launch kernel launch command buffer + encoder command buffer + dispatch
主な良さ 学びやすく生態系が強い CUDA互換の移植しやすさ Appleで自然 低レベル制御

どこが一番つまずきやすいか

API つまずきやすい点
CUDA 速く書けても最適化が浅いまま止まりやすい
HIP / ROCm CUDA互換と思い込みすぎて差分を見落としやすい
Metal Apple特有のfeature familyやresource管理へ慣れが必要
Vulkan descriptor, pipeline, synchronizationの明示性が重い

どう学ぶとよいか

  1. まずCUDAMetalのように学習導線が比較的明確なものを1つ学ぶ
  2. スレッド階層、メモリ階層、同期の考え方を掴む
  3. その後にHIPやVulkanで概念対応を取る

どれを選ぶべきか

  • NVIDIA中心のAI / HPC: CUDAが第一候補
  • AMD含む移植や調達の自由度重視: HIP / ROCm
  • Apple向けアプリやローカルML: Metal
  • 長寿命なクロスプラットフォーム低レベル基盤: Vulkan Compute

コードを書く前に考えること

どのAPIを選んでも、本質は次の問いへ戻ります。

  • 並列化できるか
  • データはどう配置するか
  • 共有メモリやcacheをどう活かすか
  • 同期と分岐をどう減らすか
  • 既存ライブラリで済むのか

API選定の判断表

条件 向きやすい選択
社内資産がNVIDIA中心 CUDA
CUDA資産をAMDへ寄せたい HIP / ROCm
macOS / iOSネイティブで完結 Metal
エンジンや低レベル基盤を長期維持 Vulkan

API別の認知負荷

flowchart LR A["CUDA HIP"] --> B["最短で動かしやすい"] C["Metal"] --> D["Apple文脈では自然"] E["Vulkan"] --> F["最も明示的で重い"]
この章のひとこと

コードモデルが違っても、最終的に考える問いは「どう起動し、どう束ね、どう待たせないか」へ収束します。


GPU基礎のFAQ

GPUはCPUより何倍速い?

処理次第です。何十倍にもなることもありますが、ほとんど速くならないこともあります。GPUが速いのは、向いた計算に対してです。

速くなりやすい 速くなりにくい
大きな行列演算 小さな逐次処理
画像処理、畳み込み 分岐だらけの制御ロジック
同じ計算を大量データへ適用 CPUと頻繁に往復する処理

VRAMが多ければ速い?

必ずしも速くはありません。容量は重要ですが、帯域、キャッシュ、実行器、ソフトウェア最適化も同じくらい重要です。

VRAMが多いと嬉しいこと それだけでは足りない理由
大きいモデルやバッチを載せやすい 帯域が細いと供給が詰まる
KV cacheや一時バッファを持ちやすい カーネルやランタイムが弱いと速くならない

CUDAを学べば他にも応用できる?

できます。CUDA固有のAPIはありますが、スレッド階層、メモリ階層、レイテンシ隠蔽、ダイバージェンスの感覚は他のGPU APIにもかなり効きます。

flowchart LR A["CUDAで学ぶ"] --> B["thread / block / memory hierarchy"] B --> C["MetalやVulkanでも応用しやすい感覚になる"]

GPUとNPUはどう違う?

GPUは汎用並列計算へかなり広く対応します。NPUはもっと特定のAI演算へ特化することが多く、電力効率で強い一方、柔軟性ではGPUが勝る場面が多いです。

観点 GPU NPU
柔軟性 高い 低めになりやすい
電力効率 広い用途で妥協が少ない 特定AI処理で強い
向く場面 学習、推論、描画、HPC 端末推論、組み込みAI、定型処理

まず何を学ぶとよい?

順番としては、

  1. CPUとGPUの設計目的の違い
  2. スレッド階層とメモリ階層
  3. ダイバージェンスコアレッシング
  4. 小さなベクトル加算や行列積
  5. プロファイリング

が素直です。

flowchart LR A["CPUとGPUの違い"] --> B["実行モデルとメモリ階層"] B --> C["coalescing / divergence"] C --> D["小さなkernelを書く"] D --> E["計測して読む"]

GPUプログラミングモデル実践ガイド

CUDA、ROCm、Metal、Vulkan Computeを実装の感覚で比較する

GPUプログラミングはAPIごとに書き味が違います。ただし、根本の問いは共通です。どこへデータを置くか、どの単位でスレッドを束ねるか、どうdispatchするか、どこで同期するか。この実践ガイドでは、CUDAHIP / ROCmMetalVulkan Compute を、概念対応とコード骨格の両面から整理します。

要点

4つのモデルは見た目がかなり違いますが、バッファを作るGPU関数を用意する実行単位を決めるキューへ積む必要なら同期する という流れはほぼ共通です。違いは、どこまで明示的に書かせるかと、どのプラットフォームを強く意識するかにあります。

共通して考えるべきこと

どのAPIでも、最終的に考えることは似ています。

  1. データをどこへ置くか
  2. GPU上の関数をどう書くか
  3. 実行単位をどう切るか
  4. コマンドをどう投入するか
  5. どこで同期するか
flowchart LR A["host side"] --> B["buffer prepare"] B --> C["kernel shader prepare"] C --> D["dispatch launch"] D --> E["GPU execution"] E --> F["sync if needed"]

CUDAの見方

CUDAは、NVIDIA GPU向けで最も普及した計算基盤です。

何が強いか

  • 学習導線が比較的明確
  • エコシステムが厚い
  • cuBLAScuDNNNCCL など周辺が強い

何に注意するか

  • NVIDIA依存
  • 最初は動いても、最適化は別の難しさがある

HIP / ROCmの見方

HIPはCUDAに近い書き味の移植層で、ROCmはAMD GPU向けの総合スタックです。

何が強いか

  • CUDA資産をAMD側へ寄せやすい
  • オープン寄りの選択肢を取りやすい

何に注意するか

  • 「完全互換」と思い込みすぎない
  • 実機とライブラリ差を必ず確認する

Metalの見方

MetalはAppleプラットフォーム向けの低レベルGPU APIです。

何が強いか

  • macOS / iOS / Apple Siliconと自然に統合される
  • graphicsとcomputeを一貫して扱いやすい

何に注意するか

  • Apple外では使えない
  • feature familyの理解が必要

Vulkan Computeの見方

Vulkan Computeは、Khronos系の低レベルクロスベンダAPIのcompute機能です。

何が強いか

  • 非常に明示的で制御自由度が高い
  • 長寿命なクロスプラットフォーム基盤になりやすい

何に注意するか

  • 記述量が多い
  • pipeline、descriptor、synchronizationの明示性が重い

最小コード例

CUDA / HIP風

__global__ void vec_add(const float* a, const float* b, float* c, int n) {
    int i = blockIdx.x * blockDim.x + threadIdx.x;
    if (i < n) {
        c[i] = a[i] + b[i];
    }
}

Metal

#include <metal_stdlib>
using namespace metal;

kernel void vec_add(
    device const float* a [[buffer(0)]],
    device const float* b [[buffer(1)]],
    device float* c [[buffer(2)]],
    uint id [[thread_position_in_grid]]
) {
    c[id] = a[id] + b[id];
}

Vulkan Compute

#version 450
layout(local_size_x = 256) in;

layout(set = 0, binding = 0) readonly buffer A { float a[]; };
layout(set = 0, binding = 1) readonly buffer B { float b[]; };
layout(set = 0, binding = 2) writeonly buffer C { float c[]; };

void main() {
    uint i = gl_GlobalInvocationID.x;
    c[i] = a[i] + b[i];
}

少し大きいコード骨格

CUDA的な骨格

float *d_a, *d_b, *d_c;
cudaMalloc(&d_a, bytes);
cudaMalloc(&d_b, bytes);
cudaMalloc(&d_c, bytes);

cudaMemcpy(d_a, h_a, bytes, cudaMemcpyHostToDevice);
cudaMemcpy(d_b, h_b, bytes, cudaMemcpyHostToDevice);

dim3 block(256);
dim3 grid((n + block.x - 1) / block.x);
vec_add<<<grid, block>>>(d_a, d_b, d_c, n);

cudaMemcpy(h_c, d_c, bytes, cudaMemcpyDeviceToHost);

Metal的な骨格

let commandBuffer = commandQueue.makeCommandBuffer()!
let encoder = commandBuffer.makeComputeCommandEncoder()!
encoder.setComputePipelineState(pipelineState)
encoder.setBuffer(bufferA, offset: 0, index: 0)
encoder.setBuffer(bufferB, offset: 0, index: 1)
encoder.setBuffer(bufferC, offset: 0, index: 2)
encoder.dispatchThreads(gridSize, threadsPerThreadgroup: tgSize)
encoder.endEncoding()
commandBuffer.commit()
commandBuffer.waitUntilCompleted()

Vulkan的な骨格

1. bufferを作る
2. descriptor setへ束縛する
3. compute pipelineを作る
4. command bufferへbind + dispatchを記録する
5. queue submitして完了を待つ

用語対応表

概念 CUDA HIP / ROCm Metal Vulkan
GPU関数 kernel kernel kernel function compute shader
実行グループ block block threadgroup workgroup
最小単位 thread thread thread invocation
実行投入 kernel launch kernel launch command buffer + encoder command buffer + dispatch
資源束縛 runtime API CUDA寄り buffer index descriptor set

どれを選ぶべきか

条件 向きやすい選択
NVIDIA中心のAI / HPC CUDA
CUDA資産をAMDへ寄せたい HIP / ROCm
macOS / iOSネイティブ Metal
低レベル基盤を長期維持 Vulkan Compute
flowchart LR A["CUDA HIP"] --> B["最短で動かしやすい"] C["Metal"] --> D["Apple文脈では自然"] E["Vulkan"] --> F["最も明示的で重い"]

GPU API選定のケーススタディ

ケース1: 学習用コードをAMDへ持っていきたい

この場合はHIP / ROCmが候補になりますが、CUDA完全互換前提で進めると危険です。依存ライブラリと性能差を早めに確認する必要があります。

ケース2: Apple Silicon上でローカルMLツールを作る

Metalが自然です。Cross-platform抽象化より、Apple側のpipeline stateやthreadgroup設計を理解する方が成果につながりやすいです。

ケース3: エンジンレベルのcompute基盤がほしい

Vulkan Computeは有力ですが、学習コストはかなり高いです。長期保守を見据えた設計が必要です。

GPU API選定のFAQ

まず何から学ぶのがよいですか

NVIDIA環境ならCUDA、Apple環境ならMetalが最も入りやすいです。

Vulkanは最強ですか

最も自由度は高いですが、最も書くことも多いです。常に最強ではありません。

HIPはCUDAの置き換えですか

かなり近いですが、完全に同一と考えるのは危険です。


GPU最適化実践ガイド

memory bound、compute bound、転送、融合をend-to-endで判断する

GPU最適化は、カーネルの中だけを見る作業ではありません。CPUとの転送、バッチ設計メモリ階層、同期、実行単位の設計、既存ライブラリの使い方まで含めて考える必要があります。この実践ガイドは、GPUを速くするというより、GPUワークロードを正しく速くする ための実践整理です。

要点

最適化の中心は 計測ボトルネックの分類小さな変更再計測 です。とくに memory boundcompute bound の見分けを間違えると、努力の方向を誤りやすくなります。

最適化の前提

GPU最適化は、次の誤解から外れるところから始まります。

  • occupancyが高ければ勝ち、ではない
  • GPU使用率が高ければ勝ち、でもない
  • カーネル単体が速ければサービス全体も速い、でもない

見るべき3層

  1. end-to-end: ユーザーが待つ全体時間
  2. runtime: CPU、転送、キュー、同期
  3. kernel: GPU上の純粋な計算

GPU最適化の手順

flowchart LR A["end-to-end計測"] --> B["CPU / 転送 / GPUを分離"] B --> C["最重箇所を特定"] C --> D["memory bound / compute bound判定"] D --> E["1つだけ変更"] E --> F["再計測"]

基本原則

  • 一度に1つずつ変える
  • 変更前後を必ず同条件で測る
  • 既存ライブラリで済むなら自作を急がない

転送最適化

CPUとGPUの往復は、非常に高価になりやすいです。

改善の方向

  • CPU前処理をGPU側へ寄せる
  • GPU上で複数ステップを連鎖させる
  • pinned memoryを使う
  • 小分け転送をまとめる
悪い:
CPU -> GPU -> CPU -> GPU -> CPU

良い:
CPU -> GPU -> GPU -> GPU -> CPU

メモリアクセス最適化

GPUは規則的なアクセスを好みます。

典型施策

  • AoS から SoA へ変える
  • 隣接スレッドが隣接アドレスを触るようにする
  • ランダムアクセスを前処理で緩和する

AoSとSoA

AoS:
[{x,y,z}, {x,y,z}, {x,y,z}]

SoA:
x = [x,x,x]
y = [y,y,y]
z = [z,z,z]

タイリングと再利用

タイリング は、大きなデータを小片へ分け、shared memoryやcacheで再利用しやすくする手法です。

なぜ効くか

タイリングの骨格

__shared__ float tileA[16][16];
__shared__ float tileB[16][16];

for (int t = 0; t < K; t += 16) {
    // global -> shared
    __syncthreads();
    // sharedを再利用して演算
    __syncthreads();
}

落とし穴

  • shared memoryを使いすぎる
  • bank conflictが増える
  • 境界処理が複雑化する

カーネル融合

小さなカーネルを何本もつなぐと、launch overheadと中間結果の書き戻しが無視できなくなります。

flowchart LR A["kernel A"] --> B["global write"] B --> C["kernel B"] D["fused kernel"] --> E["register / sharedのまま継続"]

何が嬉しいか

  • launch overhead減
  • 中間結果のメモリ往復減

何が難しいか

分岐と同期の整理

warp divergence や過剰同期は、GPUで非常に効率を落とします。

分岐で見ること

  • 同じwarp内で分岐が割れていないか
  • 事前分類で揃えられないか
  • 別カーネルへ分割した方がよくないか

同期で見ること

  • block内同期が多すぎないか
  • 原子操作へホットスポットがないか
  • CPU側でGPUを細かく待っていないか

精度最適化

AIワークロードでは、精度形式の変更が極めて強い最適化です。

形式 効果 注意点
FP32 安定 重い
FP16 / BF16 バランス良 数値特性確認
INT8 強い圧縮 実装と品質の両確認
FP8 / INT4系 さらに強い ハードとランタイム依存が大きい

バッチ設計

GPUはまとめて流す方が効率が出やすいですが、レイテンシ要件が強いと単純ではありません。

擬似コード

pending = []

while True:
    req = maybe_pop()
    if req:
        pending.append(req)

    if should_launch(pending):
        batch = build_batch(pending)
        run_gpu(batch)

見るべきこと

  • 小さすぎてGPUが遊んでいないか
  • 大きすぎて待ちが増えていないか
  • 長文と短文が混ざって崩れていないか

ベンチマークの読み方

最低限見る指標

指標 何を意味するか
end-to-end time 本当にユーザーが待つ時間
kernel time GPU純計算時間
throughput 全体処理量
p95 / p99 遅い側の品質
VRAM使用量 容量余裕と逼迫度

比較前提

  • GPU型番
  • ドライバ / runtime
  • 入力サイズ
  • 精度形式
  • batch size
  • warmup

よくある誤読

  • 最速1回だけ比較する
  • 平均値だけで判断する
  • CPU前処理込みかどうかを混ぜる
  • 精度が違うのに同列比較する

GPU性能最適化のケーススタディ

ケース1: 行列積カーネルが遅い

疑う順は、

  1. shared memoryを使っているか
  2. block sizeが極端でないか
  3. Tensor Coreが効く精度か
  4. cuBLAS と比べてどれくらい差があるか

ケース2: GPU利用率は高いが遅い

利用率が高くても、memory stallだらけなら効率は悪いです。GPU利用率だけではなく、帯域やstall理由を見ます。

ケース3: 単体カーネルだけ速い

サービス全体ではCPU前処理や転送が支配していることがあります。end-to-endの改善がなければ、最適化の価値は限定的です。

GPU性能最適化のFAQ

occupancyは高いほどよいですか

高いほどよいとは限りません。手段であって目的ではありません。

手書きCUDAはいつ必要ですか

既存ライブラリで足りないとき、またはワークロードが特殊で十分な最適化が既存にないときです。

GPU使用率だけ見れば十分ですか

十分ではありません。帯域、stall、転送、p95 / p99を一緒に見る必要があります。


LLM推論とGPUサービング実践ガイド

KV cache、連続バッチング、ランタイム最適化をつなげて理解する

LLM推論は、GPUの価値が最も可視化される実務領域のひとつです。ただし実際の性能は、単純なFLOPSやVRAM容量だけでは決まりません。prefilldecodeKV cachecontinuous batching、量子化、GPU間通信、推論サーバのスケジューリングまで含めて見てはじめて、なぜ速いのか、なぜ詰まるのかが見えてきます。

要点

LLM推論は、GPU上の行列演算だけの問題ではありません。重みをどこへ置くかKV cacheをどう管理するかいつバッチを組むか1枚で足りないときどう分散するか推論サーバがどこまで吸収するか をまとめて考える必要があります。

この実践ガイドで重視すること

  • LLM推論 = GPUカーネル ではなく、サービング全体の問題として理解する
  • prefilldecode の違いを性能上の違いとしてつかむ
  • KV cache、量子化、continuous batchingがなぜ重要かを腹落ちさせる
  • TensorRTTritonvLLM の役割分担を整理する
  • time to first tokentokens per second を混同しない

LLM推論とは何か

LLM推論は、学習済みの大規模言語モデルへ入力を与え、次トークンを順番に予測して応答を生成する処理です。ここでいう トークン は、単語そのものではなく、モデルが扱う離散的な単位です。

学習との違い

  • 学習 は重みを更新する
  • 推論 は重みを固定したまま出力だけを作る

推論は学習より軽く見えますが、実サービスでは

  • 低レイテンシ
  • 高スループット
  • 同時接続
  • 長文混在

を同時に満たしたくなるため、別の難しさがあります。

GPUがLLM推論で重要な理由

LLM推論の中核は、巨大な行列演算とattentionです。これらは、

  • 同種の演算を大量のデータへ適用する
  • 高帯域メモリが効く
  • 低精度演算ユニットを活かしやすい

ため、GPUとの相性が極めてよいです。

flowchart LR A["入力トークン列"] --> B["埋め込みとattention"] B --> C["MLP / projection"] C --> D["次トークン予測"] B --> E["GPU向きの大規模行列演算"] C --> E

GPUが効くが万能ではない理由

推論の全体はGPUだけで完結しません。

  • トークナイズはCPU側に残ることが多い
  • API層や認証、ルーティングはCPU側
  • バッチ待ちやキュー制御はサーバロジック側

つまり「GPUは速いのにサービスは遅い」という状況は普通に起きます。

prefillとdecode

prefill は、最初に与えられた文脈をまとめて処理して内部状態を構築する段階です。decode は、その後1トークンずつ生成する段階です。

ヒント

prefill はまとめて大量に計算しやすく、decode は1ステップずつ順番依存が強いので、同じ推論でも性格がかなり違います。

flowchart LR A["prompt入力"] --> B["prefill"] B --> C["KV cache構築"] C --> D["decode step 1"] D --> E["decode step 2"] E --> F["decode step 3"]

性能上の違い

  • prefillは大きなGEMMをまとめて流しやすい
  • decodeは逐次依存が強く、帯域とレイテンシが効きやすい

このため、tokens per second が良くても time to first token が悪い、またはその逆、ということが起きます。

KV cache

KV cache は、過去トークンに対するattentionのキーとバリューを保存する仕組みです。毎回すべてを再計算しないために不可欠です。

何が嬉しいか

  • decodeごとの再計算を減らせる
  • 長い会話を現実的な速度で扱える

何が難しいか

  • 長文ほどVRAMを消費する
  • リクエストが混在すると断片化しやすい
  • マルチテナントでは誰のcacheをどこまで残すかが難しい
flowchart TB A["request A"] --> B["KV cache slot 1"] C["request B"] --> D["KV cache slot 2"] E["request C"] --> F["KV cache slot 3"] B --> G["VRAM pool"] D --> G F --> G

PagedAttention的な発想

vLLM docsで中心に置かれている PagedAttention は、KV cacheを固定長ブロックへ分けて管理し、断片化と再利用効率を改善する考え方です。これはOSのページ管理に近い発想で、「連続巨大領域を毎回確保しない」ことが本質です。

推論サーバの内部構造

実際の推論サーバは、単なる model.generate() のラッパではありません。

flowchart TB A["HTTP / gRPC request"] --> B["scheduler"] B --> C["tokenizer / preprocessing"] B --> D["batch builder"] D --> E["prefill queue"] D --> F["decode queue"] E --> G["GPU runtime"] F --> G G --> H["KV cache manager"] H --> I["streaming response"]

典型的な構成要素

  • リクエスト受付
  • バッチ構築
  • モデル選択
  • GPU実行
  • KV cache管理
  • 出力ストリーミング

GPUだけ最適化しても、このどこかが弱いと全体は伸びません。

バッチングとスケジューリング

LLMサービングでは、いつバッチを組むか が極めて重要です。

なぜ難しいか

  • バッチを大きくするとGPUは嬉しい
  • でも待たせすぎるとレイテンシが悪化する
  • 短文と長文が混ざると待ち行列が崩れる

continuous batching

continuous batching は、既存のdecodeループへ新しいリクエストを継続的に混ぜる発想です。固定バッチより効率がよいことが多いですが、実装はかなり複雑になります。

pending = []
active = []

while True:
    req = maybe_get_request()
    if req:
        pending.append(req)

    active = refill_active_batch(active, pending)
    run_one_decode_step(active)
    active = drop_finished(active)

量子化と精度戦略

量子化は、重みやactivationを低ビット化して容量と帯域を削減する手法です。

形式 強み 注意点
FP16 / BF16 比較的安定 容量削減は中程度
INT8 バランスがよい 実装品質が効く
INT4 / FP4 / FP8 強い圧縮 品質検証とハード対応が重要

誤解しやすい点

  • 低ビットなら必ず速いわけではない
  • 品質が落ちないとは限らない
  • GPUとランタイムの対応次第で効果が変わる

FlashAttentionとattention最適化

FlashAttention は、attentionの計算順序をGPUのメモリ階層に合わせて組み替える代表例です。

なぜ効くのか

  • 中間結果を丸ごとグローバルメモリへ書かない
  • タイルごとにshared memoryやregisterで再利用する
  • 帯域ボトルネックを弱める

これは「数式を少し変える」のではなく、「GPUに合う実行形へ変える」最適化の好例です。

TensorRT、Triton、vLLMの役割分担

この3つはよく同列に語られますが、役割は違います。

役割 主な例 何をするか
推論最適化エンジン TensorRT 実行計画、融合、低精度最適化
推論サーバ Triton API提供、マルチモデル管理、デプロイ
LLM特化サービング vLLM KV cachecontinuous batching、OpenAI互換API

大まかにした見分け方

  • TensorRT: GPU上でどう速く走らせるか
  • Triton: どうサービスとして出すか
  • vLLM: LLMらしい痛点をどう吸収するか

マルチGPU推論

巨大モデルでは1枚に重みが載り切らないため、複数GPUを使います。

主な分け方

  • tensor parallel
  • pipeline parallel
  • data parallel
  • context parallel

難しさ

  • GPU間通信
  • トポロジ依存
  • バランス崩れ
  • デバッグの難しさ

ベンチマークの見方

ベンチマークは 1回速かった だけでは不十分です。

指標 意味 見落としやすい点
time to first token 最初の出力までの時間 prefillとCPU層が効く
tokens/s 継続生成の速さ decode側に寄りやすい
req/s サービス全体の処理量 バッチ戦略依存が強い
p95 / p99 遅い側の分布 平均だけだと見えない
VRAM使用率 メモリ逼迫度 KV cacheの断片化を隠しやすい

比較時に揃える前提

  • GPU型番
  • ドライバ
  • ランタイム
  • 精度形式
  • バッチサイズ
  • 入力長
  • warmup有無

よくあるボトルネック

  • CPUトークナイズ
  • バッチ待ち
  • KV cache断片化
  • decode側の帯域律速
  • GPU間通信
  • モデルロードの揺れ

LLM推論のケーススタディ

ケース1: 小さなLLMサービスが遅い

  • GPUは強い
  • 同時接続は少ない
  • でも応答が遅い

このときはGPUカーネルより、バッチ待ち、CPU前処理、モデルwarmupが原因のことが多いです。

ケース2: VRAMは足りるのに接続数が伸びない

この場合は、断片化やKV cache管理、長文リクエスト混在を疑います。

ケース3: 単体ベンチは速いのに本番が遅い

本番では、

  • API層
  • 観測
  • 認証
  • ストリーミング整形

が乗るため、単体カーネルの結果だけでは判断できません。

実装・運用チェックリスト

  1. time to first tokentokens/s を分けて見る
  2. KV cacheの割当と断片化を監視する
  3. バッチ戦略を固定せず、実トラフィックで見直す
  4. 量子化は品質と速度を両方測る
  5. マルチGPUでは通信も観測する
  6. cold startとwarm startを分けて評価する

LLM推論のFAQ

GPUが速いなら、なぜLLMサービスは遅いのですか

GPU以外の層が多いからです。CPU前処理、バッチ待ち、KV cache、ネットワーク、サーバ実装が全部効きます。

VRAMが大きければ安心ですか

安心ではありません。容量だけでなく、帯域、断片化、KV cacheの管理方法が同じくらい重要です。

vLLMを使えば全部解決しますか

しません。かなり強い選択肢ですが、モデル構成、GPU構成、トラフィック特性との相性があります。

まとめ

GPUは、CPUとは異なる前提で設計された並列計算機です。スループット、メモリ帯域、実行単位、最適化、サービングまでを一続きで捉えると、向く処理と向かない処理の見分けがしやすくなります。

参考文献

公式・標準

講義・記事

解説・補助