GPU

概要

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

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

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

この章で重視すること

  • GPU を「CPU の強化版」ではなく、設計目的の違う計算機として理解する
  • ストリーミングマルチプロセッサ、warp / wavefront、共有メモリ、HBM、つまり高帯域メモリをひとつながりで捉える
  • グラフィックス APIGPGPU、つまり GPU を描画以外の一般計算へ使う考え方を分けすぎず、同じハードウェアの別の顔として見る
  • CUDA だけでなく ROCm / HIP、つまり AMD 系 GPU 向けの主要ソフトウェアスタックと移植層、Metal、Vulkan 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. 分岐や同期で実行束の足並みが崩れないか

目次

  1. GPU とは何か
  2. なぜ GPU は速いのか
  3. GPU の歴史
  4. 基本アーキテクチャ
  5. グラフィックスパイプライン
  6. GPGPU と CUDA 以後
  7. 実行モデル:スレッド、warp、wavefront
  8. メモリ階層
  9. 同期、ダイバージェンス、占有率
  10. テンソルコアと行列演算
  11. GPU プログラミングモデル
  12. 主要 API とエコシステム比較
  13. CPU・TPU・NPU・FPGA との比較
  14. 主要ユースケース
  15. 性能最適化の勘所
  16. マルチ GPU と相互接続
  17. 仮想化、分割、マルチテナンシー
  18. 運用・監視・トラブルシューティング
  19. GPU が向かない処理
  20. 2025-2026 の動向
  21. 判断の指針
  22. LLM 推論と GPU アーキテクチャ
  23. GPU 最適化の実践パターン
  24. CUDA・ROCm・Metal・Vulkan のコードモデル比較
  25. FAQ
  26. 参考文献

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: 同じブロックやグループ内のスレッドが共有できる手動管理メモリ。LDS は AMD 系でよく使う 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 向けの主要スタック。HIP は CUDA に近い書き味の移植層
  • 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 の主なメモリ層

  • レジスタ
  • 共有メモリ / LDS / threadgroup memory
  • L1 / read-only cache / texture cache
  • L2
  • グローバルメモリ
  • ホストメモリ
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. 速くならない

  • GPU 向きでない処理
  • 実は I/O が支配的
  • カーネル launch が細かすぎる
  • ダイバージェンスが強い

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 でまず速く作る方が全体最適になることがよくあります。


2025-2026 の動向

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 計算」という意味ではとても重要です。

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

2025-2026 の実務では、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 容量`、`帯域`、`キャッシュ管理`、`サービングランタイム` の総合戦になります。
この章でわかること
`prefill` と `decode` の違い、`KV cache` がなぜ性能と容量の中心になるか、推論サーバが何を吸収しているか、そして `TensorRT`、`Triton`、`vLLM` の役割分担が見えるようになります。

推論の大まかな流れ

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 制御"]

NVIDIA の最新 TensorRT docs では、TensorRT は NVIDIA GPU 上の deep learning inference を最適化・高速化する SDK とされ、FP32/FP16/BF16/FP8/INT8、dynamic shapes、transformer / LLM 最適化が明示されています。Triton docs では、HTTP または gRPC endpoint を通してモデル推論を提供するサーバと整理されています。vLLM docs では、PagedAttention、continuous batching、prefix caching、speculative decoding、OpenAI-compatible API server などが前面に出ています。

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 を出せるからではありません。実務では、

  • PagedAttention による KV cache 管理
  • continuous batching
  • prefix caching
  • speculative decoding
  • 分散推論の整理

といった、「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 bound` と `compute 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["グローバル読み出しを減らす"]

タイリングが効く理由

  • グローバルメモリの再読み出しを減らせる
  • 共有メモリの低レイテンシを活かせる
  • 演算強度を上げやすい

タイリングの落とし穴

  • 共有メモリを使いすぎて occupancy が落ちる
  • bank conflict が増える
  • タイル境界の処理が複雑になる
良いこと 代償
再利用が増える 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 ごとに書き方が違いますが、根本の考え方はかなり共通です。ここでは「何が同じで、何が違うか」をコードモデルの観点から整理します。

この章でわかること
CUDA、HIP / 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. まず CUDA か Metal のように学習導線が比較的明確なものを 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["最も明示的で重い"]
この章のひとこと
コードモデルが違っても、最終的に考える問いは「どう起動し、どう束ね、どう待たせないか」へ収束します。

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 関数を用意する`、`実行単位を決める`、`キューへ積む`、`必要なら同期する` という流れはほぼ共通です。違いは、どこまで明示的に書かせるかと、どのプラットフォームを強く意識するかにあります。

目次

  1. 共通して考えるべきこと
  2. CUDA の見方
  3. HIP / ROCm の見方
  4. Metal の見方
  5. Vulkan Compute の見方
  6. 最小コード例
  7. 少し大きいコード骨格
  8. 用語対応表
  9. どれを選ぶべきか
  10. ケーススタディ
  11. FAQ
  12. 参考文献

共通して考えるべきこと

どの 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["最も明示的で重い"]

ケーススタディ

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

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

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

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

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

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

FAQ

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

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

Vulkan は最強ですか

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

HIP は CUDA の置き換えですか

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


GPU 最適化実践ガイド

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

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

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

目次

  1. 最適化の前提
  2. GPU 最適化の手順
  3. 転送最適化
  4. メモリアクセス最適化
  5. タイリングと再利用
  6. カーネル融合
  7. 分岐と同期の整理
  8. 精度最適化
  9. バッチ設計
  10. ベンチマークの読み方
  11. ケーススタディ
  12. FAQ
  13. 参考文献

最適化の前提

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 減
  • 中間結果のメモリ往復減

何が難しいか

  • register pressure 増
  • コードの複雑化

分岐と同期の整理

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 前処理込みかどうかを混ぜる
  • 精度が違うのに同列比較する

ケーススタディ

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

疑う順は、

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

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

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

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

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

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 を混同しない

目次

  1. LLM 推論とは何か
  2. GPU が LLM 推論で重要な理由
  3. prefill と decode
  4. KV cache
  5. 推論サーバの内部構造
  6. バッチングとスケジューリング
  7. 量子化と精度戦略
  8. FlashAttention と attention 最適化
  9. TensorRT、Triton、vLLM の役割分担
  10. マルチ GPU 推論
  11. ベンチマークの見方
  12. よくあるボトルネック
  13. ケーススタディ
  14. 実装・運用チェックリスト
  15. FAQ
  16. 参考文献

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 cache、continuous 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 間通信
  • モデルロードの揺れ

ケーススタディ

ケース 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 を分けて評価する

FAQ

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

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

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

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

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

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

まとめ

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