NVIDIA CompileIQによる自動チューニングでカーネルパフォーマンスを最大化
本文の状態
日本語全文を表示中
詳細モードで約16分の本文を読めます。
NVIDIAは、特定の環境に最適なコンパイラオプションを見つけるという難問に対し、CompileIQの自動チューニング機能を活用することで、より高いカーネルパフォーマンスを引き出す方法を発表した。
Source Article
元記事を日本語で読む
本文に関係しない購読案内、埋め込み通知、サイト内プロモーションは除いています。
NVIDIA CompileIQ は、パフォーマンスエンジニアリングにおける最も困難な課題の一つに取り組んでいます。それは、特定のワークロードに対して最高のパフォーマンスを引き出すコンパイラオプションを見つけることです。
あるチームが、GPU 上で LLM の推論パイプラインを数週間かけて最適化し、バッチサイズの調整、FP8 への量子化、フラッシュアテンションの採用、可能な限りすべてのカーネルの融合を行ってきたと想像してください。プロファイラは、これ以上絞り込む余地がないと報告します。
しかし、コンパイラ自体をチューニング可能なパラメータに変えることはできないでしょうか?それが可能になりました。NVIDIA CUDA 13.3 のリリースには、CompileIQ が含まれています。これは、進化的アルゴリズムおよび遺伝的アルゴリズムを使用して、個々のワークロード向けに NVIDIA 汎用 GPU コンパイラを最適化する、AI パワー型のコンパイラ自動チューニングフレームワークです。
NVIDIA GPU コンパイラは、コンパイルするすべてのカーネルに対して、同じデフォルトのヒューリスティック(レジスタ割り当て戦略、命令スケジューリングの決定、ループ展開しきい値など)を適用します。これらのヒューリスティックは、広範なワークロードにわたって良好な結果を生み出すように設計されています。しかし、「全体的に良い」と「特定のワークロードに対して最適」は、非常に異なる概念です。
AI インフラにおける競争環境は、この格差を無視できなくしています。カスタム CUDA、Triton、および Helion カーネルを構築するチームは、スループットのあらゆるパーセントポイントのために努力しています。これまで、特定のワークロード向けにコード生成を微調整する方法はありませんでした。
90% の問題と機会
なぜコンパイラレベルの最適化がこれほど重要なのかを理解するには、現代の LLM(大規模言語モデル)推論において GPU 計算リソースが実際にどこに使われているかを考える必要があります。
アテンション推論カーネルでは、FFN/MLP ブロックの線形層における GEMM(行列乗算)と Q、K、V、および出力投影演算が、総 FLOPs の約 70% を占めます。スケーリングされたドットプロダクトアテンションや融合型・フラッシュアテンションのバリアントは、さらに 25% を占めています。これら 2 つのカーネルファミリーを合わせると、エンドツーエンドの推論計算量の 90% 以上を占めることになります。
これは AI 推論に特有のことではありません。多くのアプリケーションやアルゴリズムにおいて、計算時間の大部分がコードのごく一部で費やされており、つまりこれらの小さなコードセクションがアプリケーションのパフォーマンスに対して不釣り合いな大きな影響を与えています。このため、それらのコード部分におけるパフォーマンス向上—even 小数点以下のパーセントであっても—が、全体としてのアプリケーションパフォーマンスに不釣り合いなほどの改善をもたらします。
CompileIQ の紹介
CompileIQ は、進化的および遺伝的アルゴリズムを活用して、個々のワークロード向けに NVIDIA GPU コンパイラを最適化する AI 駆動のコンパイラ自動調整フレームワークです。すべてのワークロードに対して単一の汎用的なコンパイラ設定を受け入れるのではなく、CompileIQ はその発想を転換し、最も重要なカーネルそれぞれに合わせて特別化されたコンパイラ設定を生成します。
内部では、CompileIQ は公開されているコンパイラフラグからはアクセスできない豊富な内部コンパイラパラメータの空間を探査します。これにはレジスタ割り当て戦略、命令スケジューリングポリシー、ループ変換などが含まれます。その出力は、高度なコントロールファイル(ACF)であり、コンパイラはこのファイルを –apply-controls フラグを介して取り込みます。これにより、ワークロードに特化して最適化されたカーネルバイナリが生成されます。
こう考えてみてください:あなたのコンパイラには、すでにカーネル向けにより良いコードを生成する能力があります。ただ、どの内部設定の組み合わせがその結果をもたらすかを知っているだけではありません。CompileIQ の進化的探索は、その最適な組み合わせを自動的に見つけ出します。
これまで知っていたすべての最適化レバーを使い果たして壁にぶつかったチームにとって、CompileIQ は新たなレバー、つまりコンパイラそのものとなります。
CompileIQ は利用可能であり、次のセクションで示すように、pip を使用して好きな Python 環境にインストールできます。主要な AI ラボではすでに、パフォーマンスが最も重要なワークロードの生産環境でこれを利用しています。
4 つのステップで始める
CompileIQ はシンプルなワークフローを持つ Python パッケージです:
- Learn
- Install
- 目的を定義する
- 実行する
pip install compileiq
CompileIQ は、PTXAS および NVCC の両方に対応するコンパイラ探索空間を同梱しており、これらは API を介して自動的に取得されます。手動でのダウンロードや設定は不要です。
開発者としての役割は、目的関数を定義することです。例えば、候補となるコンパイラ構成を受け取り、それを用いてカーネルをコンパイルし、結果をベンチマークしてスコアを返す Python の呼び出し可能なオブジェクト(コール可能)などです。あなたのカーネルをベンチマークできるのであれば、CompileIQ を使用できます。
以下に例を示します:
import subprocess
from compileiq.ciq import Search
from compileiq.types import SearchConfiguration
def objective(config_blob):
with open("config.acf", "wb") as f:
f.write(bytes.fromhex(config_blob))
result = subprocess.run([
"ptxas", "-v", "-arch=sm_90a",
"--apply-controls", "config.acf",
"my_kernel.ptx"
], capture_output=True, text=True)
return extract_runtime(result.stdout)
config = SearchConfiguration(
pool_size=32, cull_size=24, generations=20,
mutate_rate=0.1, problem_type="min",
num_objectives=1
)
search = Search(config, objective)
best_acf = search.run()
上記のコードは、以下の 3 つの明確なセクションに分割できます:
- 目的を定義する:評価対象となる構成を config_blob として受け取り、ディスクに保存し、カーネルをコンパイル・実行してメトリクスを抽出する関数 objective を定義します。
- 検索の設定:1 回の生成で試す候補の数(pool_size)、実行する世代数、最適化する目的関数の数など、検索を駆動するパラメータを設定します。
- 検索を実行し、最良の候補を抽出します。
以上です。検索が開始されると、CompileIQ はコンパイラ構成の集団を初期化し、各構成を目的関数に対して評価し、最も優れたパフォーマンスを示すものを選択し、突然変異と交叉を適用して新しい候補を生成し、 successive generations(連続する世代)を経て最適な ACF に収束します。
「より良い」とはあなたのワークロードにとって何を意味するかは、目的関数で定義し、CompileIQ がそれを見つけます。

例
それでは、実際に試せる自己完結型の例に焦点を当てましょう。GitHub リポジトリには多数の例がありますが、ここでは 2 つを紹介します。まず、GPU コンピューティングとは無関係ですが、CompileIQ の使用原理を示す単一目的関数の例です。
from compileiq.ciq import Search
from compileiq.types import SearchConfiguration
import compileiq.search_spaces.base as ss
def objective(config):
score = config["x"] ** 2 + config["y"]
return score
def main():
dna_config = {
"x": ss.range(start=1.0, end=20.0, step=0.5),
"y": ss.choice([1, 2, 3]),
"z": ss.literal("this is a constant", knockout_prob=0.5),
}
main_config = SearchConfiguration(
generations=5,
problem_type="min",
num_objectives=1,
)
tuner = Search(
objective_function=objective,
search_space=dna_config,
search_config=main_config,
)
results = tuner.start()
print(f"Entire Results Dataframe:\n {results.get_results()}")
print(f"Best Result: {results.get_best_result()}")
if __name__ == "__main__":
main()
まず、目的関数(objective function)です:
def objective(config):
score = config["x"] ** 2 + config["y"]
return score
これは x を二乗し、その値を y に加算する単純な関数です。これが最適化する対象となる関数となります:
dna_config = {
"x": ss.range(start=1.0, end=20.0, step=0.5),
"y": ss.choice([1, 2, 3]),
"z": ss.literal("this is a constant", knockout_prob=0.5),
この設定(config)は、変数に許可される値を指定するものです。x の場合、範囲は 1.0 から 20.0 で、ステップサイズは 0.5 です。y の場合は、選択肢として 1、2、または 3 が選べます。z は実際には目的関数の計算には寄与しませんが、ドロップアウト(dropout)の例示として機能します。
main_config = SearchConfiguration(
generations=5,
problem_type="min",
num_objectives=1,
)
次に探索設定(search configuration)を指定します。このケースでは、5 世代(generations)を実行し、目的関数を最小化したいと考えており、分析される目的変数は 1 つのみです。
残りのコードは引数と探索の設定を行います。これは非常にシンプルな目的関数であり、手計算でも容易に算出できますが、説明のためにコードを実行した際の挙動を以下に示します。
$ python single_objective.py
🧬 Generation: 5/5|█| [elapsed: 00:00 · eta: 00:00] , 🏆 best_score=3.2500
Entire Results Dataframe:
metadata ... params
0 {"pid": 2562276} ... {'x': 2.5, 'y': 2, 'z': 'this is a constant'}
1 {"pid": 2562276} ... {'x': 8.5, 'y': 3}
2 {"pid": 2562276} ... {'x': 11.0, 'y': 1, 'z': 'this is a constant'}
3 {"pid": 2562276} ... {'x': 19.0, 'y': 2}
4 {"pid": 2562276} ... {'x': 13.5, 'y': 3}
.. ... ... ...
109 {"pid": 2562276} ... {'x': 1.5, 'y': 3, 'z': 'this is a constant'}
124 {"pid": 2562276} ... {'x': 1.0, 'y': 2, 'z': 'this is a constant'}
126 {"pid": 2562276} ... {'x': 2.0, 'y': 1, 'z': 'this is a constant'}
135 {"pid": 2562276} ... {'x': 3.0, 'y': 2, 'z': 'this is a constant'}
138 {"pid": 2562276} ... {'x': 1.5, 'y': 3}
[61 rows x 4 columns]
Best Result: {'metadata': '{"pid": 2562276}', 'generation': 4, 'score_1': 3.0, 'params': {'x': 1.0, 'y': 2, 'z': 'this is a constant'}}
最良の結果のリストには x = 1.0 および y = 2 が示されており、スコアは 3.0 となります。しかし、実際には x = 1.0 で y = 1 のときに最良のスコアが得られることを私たちは知っています。つまり、このケースでは CompileIQ は最良の答えを見つけられませんでした。世代数(今回の場合 5)が非常に少ないことと、探索の確率的な性質のため、絶対的な最良の答えにたまたま辿り着けなかっただけです。ただし、このケースで世代数をより大きな数値、例えば 15 に増やせば、ほぼ確実に最良の答えを得ることができます。
次に、特定のカーネルの GPU パフォーマンスを測定する例に移りましょう。GitHub リポジトリには、NVCC を使用してリダクションカーネルを構築する例があります。ここでは簡潔にするためコード全体は掲載せず、概念を説明するためのスニペットを示します。
検索を設定する Python 関数内では、以下のコードが使用されています:
search_space = args.search_space if args.search_space else NvccSearchSpace(version=cuda_version)
config = SearchConfiguration(
problem_type=ProblemType.MIN,
generations=args.generations,
pool_size=args.pool_size,
)
探索空間は、CUDA 13.3 に NvccSearchSpace を使用するように設定されています。また、最適化対象の目的関数のタイプが MIN(最小値)であることがわかります。これは、目的関数の最小値を見つけることを意味します。世代数とプールサイズはコマンドライン引数であり、この Python スクリプトではそれぞれデフォルトで 10 と 15 に設定されています。GPU カーネルコードは、リダクションを実行した後に実行時間を出力するようにセットアップされており、目的関数(ここではリストされていません)は本質的にカーネルをビルドして実行し、「Time = string」という文字列を検索します。これが探索空間全体で最小化される値です。
コンパイラ関連の nvcc_example フォルダにいると仮定すると、検索を実行した際の出力は以下のようになります。
$ python optimize_reduction.py --arch sm_120
Running baseline...
Baseline: 0.777 ms
Starting optimization (10 generations, pool=15)...
🧬 Generation: 10/10|█| [elapsed: 09:29 · eta: 00:00] , 🏆 best_score=0.7700, a
Baseline: 0.777 ms
Optimized: 0.770 ms
Speedup: 1.01x
Config saved: reduction_best_config.bin
Usage: nvcc --apply-controls reduction_best_config.bin -arch=sm_120 ...
検索によって発見された性能向上は約 1% ですが、この保存された設定を適用するには、生成した ACF(Auto-Tuning Configuration File)を追加し、--apply-controls オプションを使用するだけで済みます。
マルチ目的最適化と知的財産保護
ほとんどの自動調整ツールは、通常は実行時間という単一の指標を最適化対象としますが、CompileIQ はさらに一歩進んで、実行時間、コンパイル時間、消費電力といった競合する複数の目的間でのトレードオフを同時に探索する多目的最適化をサポートしています。
これは、「可能な限り最速」が常に正解とは限らないため重要です。電力制約のあるデータセンターでは、わずかな実行時間の増加を引き換えに大幅な電力削減を受け入れる場合があります。CI/CD パイプラインでは、反復サイクルを高速に保つためにコンパイル時間を優先するかもしれません。組み込み環境での展開では、これら 3 つの要素すべてをバランスさせる必要があるでしょう。
CompileIQ の進化エンジンにより、支配されない解、つまりある目的を改善すると別の目的が悪化するような構成が存在しない状態のパレートフロンティア Pareto frontier が計算されます。チームは、単一の最適化軸に縛られるのではなく、自社の制約条件に合ったトレードオフを選択できます。
この機能により、CompileIQ の適用範囲は LLM 推論のみに限定されず、NVIDIA コンパイラが利用されるあらゆる分野——科学技術計算、自動運転車、画像処理、推薦システムなど——において、デフォルトのヒューリスティックでは見逃してしまうような最適化空間を探索し、最適な構成を提示することが可能になります。
IP保護の観点から、CompileIQは双方が安全に保たれるように設計されています。コンパイラの内部構造は探索空間とACF(応答特性関数)内にカプセル化され、ユーザーがコンパイラのパラメータを気にする必要はありません。ユーザーのワークロードは常に独自の環境内に留まり、目的関数はローカルで実行され、生成されるのは結果としてのACFのみです。ACFはバージョン管理に安全にコミットでき、チーム間でも共有可能です。

結果と本番環境での採用
CompileIQは、GPUおよびCPUターゲットにおける本番ワークロードで検証済みです。例えば、このGTC トークでは、TritonBench および Helion カーネルにおいて最大 15% のパフォーマンス向上を示しています。
これらの改善は、著者によって「完了」と見なされていたカーネルのすでに最適化されたベースラインの上に達成されたものです。この改善は、デフォルトのヒューリスティックでは決して選択されないコンパイラ構成を CompileIQ が発見した直接的な結果です。
主要な AI ラボでは、すでに CompileIQ を本番環境で使用しており、パフォーマンスが最も重要な推論およびトレーニングワークロードに活用されています。同ツールが生成する ACF は完全に再現可能で移植可能です:同じベンチマークと基盤となるコンパイラが一致している限り、同じ ACF がデプロイ先を超えて最適化されたバイナリを生成します。チームは、ACF をカーネルソースコードとともにバージョン管理システムにコミットし、コンパイラの最適化を開発ワークフローにおけるバージョン管理され、レビュー可能な一部として位置づけています。
あなたの番です
PTXAS および NVCC の両方に対して、コンパイラ探索空間が利用可能です。最も影響の大きいカーネルを特定し(GEMM とアテンションが最良の候補となります)、ワークロードにとって重要な指標を測定するベンチマークを作成して CompileIQ を実行してください。
ドキュメント、API リファレンス、および有用な例は、CompileIQ の ドキュメント サイトで確認できます。ご質問やサポートについては、CompileIQ GitHub リポジトリにイシューを登録してください。
明確にしておくべき点があります:CompileIQ は、書き込まれたコードが不十分でも自動的に高性能なコードに変換する魔法のようなツールではありません。CompileIQ から最大の価値を引き出すには、まず比較的パフォーマンスの高いコードから始める必要があります。それによって、最終的なコンパイラのヒューリスティックによる微調整が可能となり、最大のパフォーマンスへと導くことができます。
ただし、チームが知っているすべての最適化レバーを使い果たしている場合でも、CompileIQ は新たなレバーを提供します——それはコンパイラそのものです。
CompileIQ をダウンロードし、GitHub の例を確認して、今日からカーネルの最適化を開始してください*。
翻訳全文
同じ出来事を3媒体で確認
同じ出来事を扱う別媒体の記事です。見出しと公開時刻を比較できます。
関連記事
今日のまとめ
AIデイリーブリーフで今日の重要ニュースをまとめ読み