NVIDIA CUDA Tile を用いた C++ による高性能 GPU カーネルの開発
NVIDIA は C++ 開発者向けに、高性能な GPU カーネル作成を簡素化する「CUDA Tile」機能を公開し、手動の最適化負担を大幅に軽減する新たな標準アプローチを提供した。
キーポイント
CUDA Tile の概要と目的
C++ で記述された CUDA カーネルのパフォーマンスと可読性を向上させるための新しいプログラミングモデルであり、複雑なメモリ管理やスレッドブロックの調整を抽象化する。
開発効率の劇的向上
従来の手動による最適化(マニピュレーション)に依存せず、テンプレートメタプログラミングと組み合わせて、より短く保守性の高いコードで高性能な実装を可能にする。
ハードウェア性能の最大化
最新の NVIDIA GPU アーキテクチャ(Blackwell 以降など)の特性を自動的に活用し、メモリ帯域幅と計算リソースを効率的に利用するよう設計されている。
抽象化されたタイルベースプログラミング
既存のC++コードベース内で、明示的なスレッド管理を行わずに低レベルなGPU並列処理やメモリ移動を抽象化したタイルベースのカーネル開発が可能になります。
パフォーマンス最適化と実行モデル
単一スレッドでブロックを開始しコンパイラが詳細を処理するモデルに加え、ポインタ修飾子やマスク付きロード/ストア操作によるメモリ効率の向上が図られます。
ハードウェア要件とプロファイリング
Compute Capability 8.x以降のGPUとCUDA Toolkit 13.3以上が必要であり、Nsight Computeを用いた詳細なタイル単位の性能計測がサポートされています。
C++でのCUDA Tileサポート開始
CUDA 13.3によりPythonに続きC++でのタイルベースGPUカーネル記述が可能になり、高度な最適化が実現されました。
重要な引用
Develop High-Performance GPU Kernels in C++ with NVIDIA CUDA Tile
CUDA Tile simplifies the development of high-performance kernels by abstracting away low-level details.
NVIDIA CUDA Tile C++ enables tile-based GPU kernel programming within existing C++ codebases, abstracting low-level GPU parallelism, memory movement, and hardware features across NVIDIA architectures without explicit thread management.
CUDA Tile C++ kernels support profiling with NVIDIA Nsight Compute, providing detailed tile-specific statistics and source-level performance metrics similar to traditional CUDA C++ kernels.
CUDA Tile C++ automates parallelism within blocks, along with asynchrony, memory movement, and other low-level details of GPU programming.
CUDA Tile C++ is portable across different NVIDIA GPU architectures, enabling developers to use the latest hardware features without having to rewrite code.
影響分析・編集コメントを表示
影響分析
この技術は、GPU プログラミングの参入障壁を下げつつ、既存の高性能アプリケーションのパフォーマンス上限を引き上げる重要な転換点となります。開発者が低レベルの詳細に時間を割かずにアルゴリズム最適化に集中できる環境を提供することで、AI 推論や科学計算などの分野での実装スピードと品質が向上することが期待されます。
編集コメント
CUDA の進化において、開発者の生産性を向上させるための「抽象化」へのシフトが明確に加速しています。これは単なるライブラリの追加ではなく、GPU プログラミングの文化そのものを変える可能性を秘めた重要な発表です。

2026 年 5 月 26 日
AI が生成した要約
高評価
低評価
- NVIDIA CUDA Tile C++は、既存のC++コードベース内でタイルベースのGPUカーネルプログラミングを可能にし、明示的なスレッド管理なしに、NVIDIAアーキテクチャ全体にわたる低レベルのGPU並列処理、メモリアクセス、およびハードウェア機能を抽象化します。
- CUDA Tile C++プログラミングモデルは、多次元テンソルスパンとパーティションビューを使用し、固定サイズの配列タイル上で操作を行うことで、従来のSIMTカーネルよりも宣言的かつ効率的にベクトル加算や行列乗算などの並列計算を記述できるようにします。
- __restrict__ポインタ修飾子、16バイトアラインメントの仮定、マスク付きロード/ストア操作といった最適化により、パフォーマンスとメモリエフィシエンシーが向上し、タイルカーネルはブロックあたりスレッド1つで起動され、スレッドの実行詳細はコンパイラが処理します。
- CUDA Tile C++カーネルはNVIDIA Nsight Computeによるプロファイリングをサポートしており、従来のCUDA C++カーネルと同様に、タイル固有の詳細統計やソースレベルのパフォーマンスメトリクスを提供します。
- 行列乗算カーネルはタイルパーティションとNVIDIAの行列乗算累積(mma)演算を活用し、部分結果の効率的な蓄積を実現することで、複雑な線形代数ワークロードに対するCUDA Tilesの能力を実証しています。
- CUDA Tile C++を完全に活用するには、計算能力8.x以降のGPU、NVIDIA Driver R580以降、およびCUDA Toolkit 13.3以降が必要です。
AI 生成コンテンツは情報を不完全に要約する可能性があります。重要な情報は必ず確認してください。詳細はこちら
開発者は、既存の大規模な C++ GPU コードベース内で NVIDIA CUDA Tile プログラミングを利用できるようになり、タイルベースの抽象化を用いて高度に最適化された GPU カーネルを開発することが可能になりました。
NVIDIA CUDA 13.1 のリリースとともに導入された NVIDIA CUDA Tile は、GPU 向けのタイルベースプログラミングを提供します。これは上位レベルの言語レイヤーと、あらゆる高水準プログラミング言語がターゲットとする中間レイヤーを備えて設計されており、アプリケーションが直接それらにターゲットを設定する必要なく、NVIDIA ハードウェアの高度な機能(テンソルコア、共有メモリ、テンソルメモリアクセラレータ)を自動的に活用します。
タイルベース GPU アプリケーションで最初にサポートされた言語は Python でした。新たにリリースされたCUDA 13.3 では、C++ でタイルカーネルを記述するサポートが追加され、開発者が高度に最適化された GPU カーネルを構築できるようになりました。
CUDA Tile C++ とは?
CUDA Tile C++ は、CUDA Tile IR 仕様書 を基盤として構築された、C++ における CUDA Tile プログラミングモデルの表現です。これにより、開発者は C++ でタイルカーネルを記述し、単一命令多重スレッド (SIMT: Single Instruction, Multiple Threads) モデルに代わる、あるいはそれに加えて、タイルベースのモデルを用いて GPU カーネルを表現することが可能になります。
リフレッシュのため、タイルモデルにおける要点は以下の通りです。
- 多次元配列が主要なデータストレージとなります。
- タイルとは、カーネルが操作する配列の一部です。
- カーネルとは、ブロックによって並列実行される関数です。
- ブロックは GPU のサブセットであり、タイルに対する演算は各ブロック内のすべてのスレッドにわたって並列化されます。
CUDA Tile C++ は、ブロック内の並列処理、非同期処理、メモリアクセスの移動、および GPU プログラミングにおけるその他の低レベルの詳細を自動化します。CUDA Tile C++ は異なる NVIDIA GPU アーキテクチャ間で移植可能であり、開発者はコードを書き換えることなく最新のハードウェア機能を利用できます。
CUDA Tile C++ ベクトル加算の例
SIMT 向けの CUDA C++ に慣れている開発者であれば、おそらく標準的なベクトル加算カーネルに遭遇したことがあるでしょう。データがすでに GPU 上にあると仮定すると、CUDA SIMT におけるベクトル加算カーネルは 2 つのベクトルを受け取り、要素ごとに足し合わせて 3 つ目のベクトルを生成します。これは記述が最も簡単な CUDA カーネルの一つです。その様子は以下の通りです。
__global__ void vecAdd(float* A, float* B, float* C, int vectorLength)
{
int workIndex = threadIdx.x + blockIdx.x*blockDim.x;
if(workIndex {} と ct::shape{8_ic} はこの文脈において同等です。作成されるパーティションビューは、本質的に元の配列をタイルサイズである 8 のチャンクにスライスしたものです。
auto aView = ct::partition_view{aSpan, ct::shape{8_ic}};
auto bView = ct::partition_view{bSpan, ct::shape{8_ic}};
auto oView = ct::partition_view{oSpan, ct::shape{8_ic}};
- X 次元のブロックインデックスを ct::bid().x で取得して入力タイルを読み込みます。多次元ブロックを扱う場合は、Y および Z 次元も使用します。次に a と b のタイルを読み込みます。利便性のために auto を使用しますが、明示的に記述すれば、aTile と bTile は ct::tile> 型です。これらは要素型が float でサイズが 8 の 1D タイルです。パーティションビューを使用すると、ブロックインデックスを簡単に渡すことができます。load 関数は自動的に配列の正しいチャンクを取得し、それをタイルに読み込みます。
int bx = ct::bid().x;
auto aTile = aView.load(bx);
auto bTile = bView.load(bx);
- 結果を加算して格納します。これは、入力タイルに対して要素ごとの加算を実行し、その結果を出力タイルに格納する一行のコードです。この出力タイルを、X 次元における同じブロックインデックス bx を用いてインデックスされた oView パーティションビューへ格納します。
auto oTile = aTile + bTile;
oView.store(oTile, bx);
ベクトル加算の完全な例
以下の例では、C++ においてこのベクトル加算カーネルを呼び出す方法を示しています。これは完全に実行可能なコードの一部です。
コンパイラが最適化を行うのを助けるために、いくつか注意すべき点があります。
まず、最高のパフォーマンスを得るためには、カーネル実行中は入力および出力配列に対して、それぞれのポインタを通じてのみアクセスする必要があります。この条件が満たされる場合、他のポインタやシンボルを用いたアクセス(エイリアシング)は存在しません。配列ポインタに __restrict__ デコレータを付与することで、これをコンパイラに伝達できます。
基底ポインタが 16 バイト境界にアライメントされた配列を使用すると、コンパイラはより効率的なメモリアクセスパターンを生成できるようになります。カーネル引数のそれぞれに対して ct::assume_aligned を呼び出すことで、ポインタがアライメントされていることをコンパイラに伝達します。これらの呼び出しの戻り値を使用して、コンパイラがこのアライメントを活用できるようにします。cudaMalloc や同様の CUDA API によって返されるポインタは、常にこの条件を満たします(256 バイトのアライメントが保証されているため)。
最後に、タイルサイズは 8 よりもはるかに大きな値を使用してください。以下の実行可能なコードにはこれらの調整を適用し、タイルサイズで割り切れない可能性があるデータを処理する load_masked と store_masked の使用を追加してください。
以下に、カーネルとメイン関数を含む完全なコードを示します。適用された最適化と冗長性の削減にご注意ください。
#include
#include
#include "cuda_tile.h"
__tile_global__ void vectorAdd(float* __restrict__ a, float* __restrict__ b, float* __restrict__ out, size_t n) {
namespace ct = cuda::tiles;
using namespace ct::literals;
a = ct::assume_aligned(a, 16_ic);
b = ct::assume_aligned(b, 16_ic);
out = ct::assume_aligned(out, 16_ic);
int bx = ct::bid().x;
auto aTile = ct::partition_view{ct::tensor_span{a, ct::extents{n}}, ct::shape{1024_ic}}.load_masked(bx);
auto bTile = ct::partition_view{ct::tensor_span{b, ct::extents{n}}, ct::shape{1024_ic}}.load_masked(bx);
auto oTile = aTile + bTile;
auto oView = ct::partition_view{ct::tensor_span{out, ct::extents{n}}, ct::shape{1024_ic}};
oView.store_masked(oTile, bx);
}
#define checkCudaError(X) do {
auto ret = X;
if (ret != cudaSuccess) {
printf("\n error on line %d, CUDART error string : %s", __LINE__, cudaGetErrorString(ret));
exit(1);
}
} while (0)
int main() {
constexpr size_t N = 2ULL >>(d_a, d_b, d_out, N);
checkCudaError(cudaDeviceSynchronize());
checkCudaError(cudaMemcpy(h_out, d_out, sizeof(float) * N, cudaMemcpyDeviceToHost));
float max_err = 0.0f;
for (size_t idx = 0; idx >>(d_a, d_b, d_out, N);
タイルカーネルを起動する際、>>内の最初の引数はタイルブロック数(SIMT ではスレッドブロック数に相当)です。2 つ番目の引数は必ず 1 にする必要があります。カーネルの実行に使用されるスレッド数はコンパイラによって決定されますので、タイルカーネルを起動する際は常にこの引数に 1 を指定してください。
計算能力 8.0 で NVIDIA Ampere アーキテクチャ以降の GPU 上で CUDA 13.3 以降を実行している場合、これらのコマンドは以下の出力を生成します。
-arch sm_120 コマンドをアーキテクチャに合わせて調整し、cuda_tile.h を使用する場合は -std=c++20 を含め、タイルカーネルをコンパイルするには --enable-tile オプションを追加してください。
$ nvcc -std=c++20 --enable-tile -arch sm_120 -o vectorAdd vectorAdd.cu
$ ./vectorAdd
N: 67108864
Max error: 0.000000e+00
これで最初の CUDA Tile C++ プログラムが完了しました。
開発者ツール
タイル C++ カーネルは、SIMT カーネルと同様に NVIDIA Nsight Compute を用いてプロファイリングできます。以下のコマンドは、Nsight Compute を使用してプロファイルを作成する方法を示しています。
$ ncu -o VecAddProfile --set detailed ./vectorAdd
作成後、Nsight Compute のグラフィカルバージョンで開くと:
- ドロップダウンメニューから vectorAdd カーネルを選択します。
- Details タブを選択します。
- Tile Statistics リポートセクションを展開します。
図 1 は Nsight Compute から生成されたプロファイルを示しています。
image*図 1. Nsight Compute から生成されたプロファイルで、vectorAdd カーネルのタイル統計情報を示す*。
タイル統計情報レポートセクションには、指定されたタイルブロックの数、コンパイラによって選択されたブロックサイズ、およびその他のタイル固有情報が含まれていることに注意してください。
ソースページも、CUDA C++ カーネルと同様に、ソース行レベルでのタイルカーネルとパフォーマンスメトリクスをサポートしています。
行列乗算
以前の例では、パーティションビューの読み込みと保存の詳細を伴う vectorAdd を示しました。この行列乗算の例は、非常にシンプルなコードで行列乗算をどのように表現するかを示しています。
このカーネルは、MxK 行 KxN 列の行列乗算を実行して MxN 行列を計算します。本カーネルでは、M=8、N=16 であり、K は 8 の倍数であれば可変とします。ここでは K=24 と設定します。これらの非常に小さなサイズは、概念を示すためだけに使用されています。
完全なカーネルと主要ポイントの概要を以下に示します。
#include "cuda_tile.h"
__tile_global__ void kernel(float* __restrict__ a, float* __restrict__ b, size_t length, float* __restrict__ c) {
namespace ct = cuda::tiles;
using namespace ct::literals;
a = ct::assume_aligned(a, 16_ic);
b = ct::assume_aligned(b, 16_ic);
c = ct::assume_aligned(c, 16_ic);
auto aShape = ct::extents{8_ic, length};
auto bShape = ct::extents{length, 16_ic};
auto cShape = ct::extents{8_ic, 16_ic};
auto aSpan = ct::tensor_span{a, aShape};
auto bSpan = ct::tensor_span{b, bShape};
auto cSpan = ct::tensor_span{c, cShape};
auto aView = ct::partition_view{aSpan, ct::shape{4_ic, 8_ic}};
auto bView = ct::partition_view{bSpan, ct::shape{8_ic, 4_ic}};
auto cView = ct::partition_view{cSpan, ct::shape{4_ic, 4_ic}};
using f32x4x4 = ct::tile>;
auto accTile = ct::full(0);
auto [xBlock, yBlock, dummy] = ct::bid();
for (auto idx : ct::irange(0, 1 + int(length - 1) / 8)) {
auto aTile = aView.load_masked(xBlock, idx);
auto bTile = bView.load_masked(idx, yBlock);
accTile = ct::mma(aTile, bTile, accTile);
}
cView.store_masked(accTile, xBlock, yBlock);
}
- a、b、c 行列に対して ct::extents オブジェクトで範囲 (extents) を作成します。コンパイル時または実行時の値のいずれかを使用できます。M=8、N=16 ですが、K は可変です。これらは次のステップでテンソンスパンを作成するために使用されます。
auto aShape = ct::extents{8_ic, length};
auto bShape = ct::extents{length, 16_ic};
auto cShape = ct::extents{8_ic, 16_ic};
- テンソンスパンを作成します。これにより、a、b、c の情報が保持され、パーティションビューの作成に使用されます。
auto aSpan = ct::tensor_span{a, aShape};
auto bSpan = ct::tensor_span{b, bShape};
auto cSpan = ct::tensor_span{c, cShape};
- a を 4×8、b のビューを 8×4 のビューとするように、a、b、c の分割ビューを作成します。これらの調整は、a と b の値に適切に割り切れる場合に限り行えます。これらの次元はまた、c のビューが 4×4 であることを決定します。
auto aView = ct::partition_view{aSpan, ct::shape{4_ic, 8_ic}};
auto bView = ct::partition_view{bSpan, ct::shape{8_ic, 4_ic}};
auto cView = ct::partition_view{cSpan, ct::shape{4_ic, 4_ic}};
2 次元の分割は 2 つの次元でインデックス付けされます。a 行列は 8×24 であり、図 2 に示すように分割ビューは 4×8 です。
aView と bView の分割ビューサイズはまた、行列乗算中に結果を蓄積するために使用されるタイル accTile の形状も決定します。この例では、accTile は cView の形状と一致する 4×4 タイルです。
image*図 2. 8 x 24 行列の 2 次元分割ビュー。サイズが 4 x 8 のビューに分割されています*
using f32x4x4 = ct::tile>;
auto accTile = ct::full(0);
- ct::bid を使用してループを実行し、3 つの次元におけるブロックインデックスを取得します。このループは 0 から長さ / 8 まで反復し、全体としての K 次元を 8 で割った値に対応します。8 での除算は、aView の K 次元と bView の K 次元が 8 であることに一致しています。ループ内では、load_masked を使用して a と b からタイルを読み込み、ct::mma を呼び出して行列乗算を実行し、その結果を accTile に蓄積します。
auto [xBlock, yBlock, dummy] = ct::bid();
for (auto idx : ct::irange(0, int(length / 8))) {
auto aTile = aView.load_masked(xBlock, idx);
auto bTile = bView.load_masked(idx, yBlock);
accTile = ct::mma(aTile, bTile, accTile);
}
- accTile の値を c のパーティションビューである cView に格納します。これで完了です。カーネルコードの大部分はデータのビュー設定と、データ読み込み・書き込みに費やされています。計算部分はシンプルです。
cView.store_masked(accTile, xBlock, yBlock);
- カーネルを起動します。cView の次元に合わせて dim3(2,4) を使用します。cView は 4×4 であり、各ブロックが C 行列の 4×4 チャンクを計算していることを意味します。C は 8×16 なので、cView の次元を C 行列の次元で割ります。8/4=2 かつ 16/4=4 となるため、dim3(2,4) でカーネルを起動します。
kernel>>(d_a, d_b, K, d_c);
CUDA Tile C++ を今日から始めよう
CUDA Tile C++ プログラムを実行するには以下の要件が必要です:
- 計算能力が 8.x 以降の GPU。
- NVIDIA ドライバ R580 以降。タイルカーネルに対して JIT コンパイルが必要な場合、NVIDIA ドライバのバージョンはコード生成に使用された CUDA Toolkit に関連付けられたバージョンと同等かそれより新しいものでなければなりません。例えば、CUDA Toolkit 13.3 には R610 ドライバ以降が必要です。
- CUDA Toolkit 13.3
タイルベースプログラミングのパワーが C++ 開発者にも利用可能になりました。
原文を表示

May 26, 2026
AI-Generated Summary
Like
Dislike
- NVIDIA CUDA Tile C++ enables tile-based GPU kernel programming within existing C++ codebases, abstracting low-level GPU parallelism, memory movement, and hardware features across NVIDIA architectures without explicit thread management.
- The CUDA Tile C++ programming model uses multi-dimensional tensor spans and partition views to operate on fixed-size array tiles, allowing developers to express parallel computations like vector addition and matrix multiplication more declaratively and efficiently than traditional SIMT kernels.
- Optimizations such as __restrict__ pointer qualifiers, 16-byte alignment assumptions, and masked load/store operations improve performance and memory efficiency; tile kernels are launched with a single thread per block, letting the compiler handle thread execution details.
- CUDA Tile C++ kernels support profiling with NVIDIA Nsight Compute, providing detailed tile-specific statistics and source-level performance metrics similar to traditional CUDA C++ kernels.
- Matrix multiplication kernels leverage tile partitions and NVIDIAs matrix multiply-accumulate (mma) operations for efficient accumulation of partial results, demonstrating CUDA Tiles capability for complex linear algebra workloads.
- CUDA Tile C++ requires GPUs with compute capability 8.x or newer, NVIDIA Driver R580 or later, and CUDA Toolkit 13.3 or newer to utilize the tile programming model fully.
AI-generated content may summarize information incompletely. Verify important information. Learn more
Developers can now use NVIDIA CUDA Tile programming within large existing C++ GPU codebases to develop highly optimized GPU kernels using tile-based abstractions.
NVIDIA CUDA Tile, launched with NVIDIA CUDA 13.1, introduced tile-based programming for GPUs. Designed with a top-level language layer and another intermediate layer that any high-level programming language can target, CUDA Tile automatically makes use of the advanced capabilities of NVIDIA hardware—including tensor cores, shared memory, and tensor memory accelerators—without requiring the application to target them directly.
Python was the first language supported for tile-based GPU applications. The newly released CUDA 13.3 adds support for writing tile kernels in C++, enabling developers to build highly optimized GPU kernels.
What is CUDA Tile C++?
CUDA Tile C++ is an expression of the CUDA Tile programming model in C++, built on top of the CUDA Tile IR specification. It enables developers to write tile kernels in C++ and express GPU kernels using a tile-based model, rather than or in addition to a single instruction, multiple threads (SIMT) model.
As a refresher, in the tile model:
- Multi-dimensional arrays are the primary data storage.
- Tiles are portions of arrays that kernels operate on.
- Kernels are functions that are executed in parallel by blocks.
- Blocks are subsets of the GPU; operations on tiles are parallelized across all the threads in each block.
CUDA Tile C++ automates parallelism within blocks, along with asynchrony, memory movement, and other low-level details of GPU programming. CUDA Tile C++ is portable across different NVIDIA GPU architectures, enabling developers to use the latest hardware features without having to rewrite code.
CUDA Tile C++ vector add example
Developers familiar with CUDA C++ for SIMT have likely encountered the canonical vector addition kernel. Assuming the data is already on the GPU, a vector add kernel in CUDA SIMT takes two vectors and adds them together element-wise to produce a third vector. This is one of the simplest CUDA kernels to write. It looks as follows.
__global__ void vecAdd(float* A, float* B, float* C, int vectorLength)
{
int workIndex = threadIdx.x + blockIdx.x*blockDim.x;
if(workIndex < vectorLength)
{
C[workIndex] = A[workIndex] + B[workIndex];
}
}
In this kernel, each thread’s work is explicitly specified, and the programmer, when launching this kernel, will specify the number of blocks and threads to be launched.
Looking at the equivalent code written in CUDA Tile C++, there’s no need to specify what each thread does. Just break the data into tiles and specify the mathematical operations for these tiles. Everything else is handled.
The CUDA Tile C++ kernel looks like the following:
#include "cuda_tile.h"
__tile_global__ void vectorAdd(float* a, float* b, float* out, size_t n) {
namespace ct = cuda::tiles;
using namespace ct::literals;
auto aSpan = ct::tensor_span{a, ct::extents{n}};
auto bSpan = ct::tensor_span{b, ct::extents{n}};
auto oSpan = ct::tensor_span{out, ct::extents{n}};
auto aView = ct::partition_view{aSpan, ct::shape{8_ic}};
auto bView = ct::partition_view{bSpan, ct::shape{8_ic}};
auto oView = ct::partition_view{oSpan, ct::shape{8_ic}};
int bx = ct::bid().x;
auto aTile = aView.load(bx);
auto bTile = bView.load(bx);
auto oTile = aTile + bTile;
oView.store(oTile, bx);
}
This looks like a lot of code for a simple vectorAdd kernel. Don’t be alarmed. This overly verbose kernel is used to show all the steps in order. A simplified version doing the same thing with fewer lines of code follows.
- The first difference is using __tile_global__ to signify to the compiler that this is a tile kernel. The array pointers and the array size are passed in as arguments, just as they are in the SIMT kernel.
__tile_global__ void vectorAdd(float* a, float* b, float* out, std::size_t n) {
- Then, set up the namespace for cuda::tiles and ct::literals.
namespace ct = cuda::tiles;
using namespace ct::literals;
- Create a tensor span, using this code ct::tensor_span for each of the three arrays. A tensor span is essentially a pointer to a multi-dimensional array in memory, similar to a C++23 std::mdspan.The tensor span carries information about the shape (extents) of the array as well as the layout of the array elements (for example, row major or column major).The ct::extents{} tells the tensor span what the dimensions of the array are. A 1D array uses n .
auto aSpan = ct::tensor_span{a, ct::extents{n}};
auto bSpan = ct::tensor_span{b, ct::extents{n}};
auto oSpan = ct::tensor_span{out, ct::extents{n}}
- Now create a partition view from a tensor span and a tile shape. A partition view is a wrapper around a tensor span that presents the array as a series of non-overlapping, fixed-sized partitions. The size of each partition is specified by the shape argument, which must be a compile-time argument.In this example, 8_ic is an integer constant that is defined by ct::literals.ct::shape<8>{} and ct::shape{8_ic} are equivalent in this context. The partition view that’s created is essentially the original array, sliced into chunks of 8, which is the tile size.
auto aView = ct::partition_view{aSpan, ct::shape{8_ic}};
auto bView = ct::partition_view{bSpan, ct::shape{8_ic}};
auto oView = ct::partition_view{oSpan, ct::shape{8_ic}};
- Load input tiles by obtaining the block index in the X dimension with ct::bid().x. If working with multi-dimensional blocks, use the Y and Z dimensions as well. Then load the a and b tiles. Use auto for convenience, but to be explicit, aTile and bTile are of type ct::tile<float, ct::shape<8>>>. They’re 1D tiles of size 8, with elements of type float. With the partition view, it’s easy to pass in the block index. The load function automatically fetches the correct chunk of the array and loads it into a tile.
int bx = ct::bid().x;
auto aTile = aView.load(bx);
auto bTile = bView.load(bx);
- Adding and store the results. This is one line of code does element-wise addition on input tiles and stores them in an output tile. Store that output tile to the oView partition view, indexed by the same block index in the X dimension, bx.
auto oTile = aTile + bTile;
oView.store(oTile, bx);
Complete vector add example
The following example show how to call this vector add kernel in C++ through a complete, runnable piece of code.
There are a few things to note to help the compiler make optimizations.
First, for best performance, the input and output arrays should only be accessed through their respective pointers while the kernel is running. There is no aliasing—access using another pointer or symbol—to the arrays when this is true. Labeling array pointers with the __restrict__ decorator conveys this to the compiler.
Using arrays with base pointers aligned to 16-byte boundaries helps the compiler generate more efficient memory access patterns. Tell the compiler that pointers are aligned by calling ct::assume_aligned<16> on each of the kernel arguments. Use the return values of these calls for the compiler to take advantage of this alignment. Pointers returned by cudaMalloc or similar CUDA APIs always fulfill this, as they have 256-byte alignment.
Finally, use a tile size much larger than 8. Make these adjustments to the runnable code that follows and add the use of load_masked and store_masked, which handles data that might not be divisible by the tile size.
The following is the complete code, including the kernel and main function. Notice the applied optimizations and reduced verbosity.
#include <cstdio>
#include <cstdlib>
#include "cuda_tile.h"
__tile_global__ void vectorAdd(float* __restrict__ a, float* __restrict__ b, float* __restrict__ out, size_t n) {
namespace ct = cuda::tiles;
using namespace ct::literals;
a = ct::assume_aligned(a, 16_ic);
b = ct::assume_aligned(b, 16_ic);
out = ct::assume_aligned(out, 16_ic);
int bx = ct::bid().x;
auto aTile = ct::partition_view{ct::tensor_span{a, ct::extents{n}}, ct::shape{1024_ic}}.load_masked(bx);
auto bTile = ct::partition_view{ct::tensor_span{b, ct::extents{n}}, ct::shape{1024_ic}}.load_masked(bx);
auto oTile = aTile + bTile;
auto oView = ct::partition_view{ct::tensor_span{out, ct::extents{n}}, ct::shape{1024_ic}};
oView.store_masked(oTile, bx);
}
#define checkCudaError(X) do {\
auto ret = X;\
if (ret != cudaSuccess) {\
printf("\n error on line %d, CUDART error string : %s", __LINE__, cudaGetErrorString(ret));\
exit(1);\
}\
} while (0)
int main() {
constexpr size_t N = 2ULL << 25;
constexpr int TILE_SIZE = 1024;
constexpr int BLOCKS = (N + TILE_SIZE - 1) / TILE_SIZE;
float* h_a = (float*)malloc(sizeof(float) * N);
float* h_b = (float*)malloc(sizeof(float) * N);
float* h_out = (float*)malloc(sizeof(float) * N);
for (size_t idx = 0; idx < N; ++idx) {
h_a[idx] = (float)rand() / RAND_MAX;
h_b[idx] = (float)rand() / RAND_MAX;
h_out[idx] = -1.0f;
}
float* d_a{nullptr};
float* d_b{nullptr};
float* d_out{nullptr};
checkCudaError(cudaMalloc(&d_a, sizeof(float) * N));
checkCudaError(cudaMalloc(&d_b, sizeof(float) * N));
checkCudaError(cudaMalloc(&d_out, sizeof(float) * N));
checkCudaError(cudaMemcpy(d_a, h_a, sizeof(float) * N, cudaMemcpyHostToDevice));
checkCudaError(cudaMemcpy(d_b, h_b, sizeof(float) * N, cudaMemcpyHostToDevice));
checkCudaError(cudaMemset(d_out, -1, sizeof(float) * N));
vectorAdd<<<BLOCKS, 1>>>(d_a, d_b, d_out, N);
checkCudaError(cudaDeviceSynchronize());
checkCudaError(cudaMemcpy(h_out, d_out, sizeof(float) * N, cudaMemcpyDeviceToHost));
float max_err = 0.0f;
for (size_t idx = 0; idx < N; ++idx) {
float expected = h_a[idx] + h_b[idx];
max_err = fmaxf(max_err, fabsf(h_out[idx] - expected));
}
printf("N: %zu\n", N);
printf("Max error: %e\n", max_err);
checkCudaError(cudaFree(d_a));
checkCudaError(cudaFree(d_b));
checkCudaError(cudaFree(d_out));
free(h_a);
free(h_b);
free(h_out);
}
If familiar with launching SIMT kernels, the process is similar, but requires a specific modification. This kernel was launched with:
vectorAdd<<<BLOCKS, 1>>>(d_a, d_b, d_out, N);
When launching a tile kernel, the first argument in the <<<>>> is the number of tile blocks (in SIMT, this would be the number of thread blocks). The second argument must be 1. The number of threads used to execute the kernel is determined by the compiler; always put 1 as this argument when launching a tile kernel.
Running CUDA 13.3 or later on compute capability 8.0 with NVIDIA Ampere architecture or newer GPU, these commands create the following output.
Adjust the -arch sm_120 command to match the architecture, include -std=c++20 when cuda_tile.h is used, and the --enable-tile option to compile tile kernels.
$ nvcc -std=c++20 --enable-tile -arch sm_120 -o vectorAdd vectorAdd.cu
$ ./vectorAdd
N: 67108864
Max error: 0.000000e+00
This completes the first CUDA Tile C++ program.
Developer tools
Tile C++ kernels can be profiled with NVIDIA Nsight Compute in the same way as SIMT kernels. The following command shows how to create a profile using Nsight Compute.
$ ncu -o VecAddProfile --set detailed ./vectorAdd
Once created and opened with the graphical version of Nsight Compute:
- Select the vectorAdd kernel from the dropdown menu.
- Choose the Details tab
- Expand the Tile Statistics report section
Figure 1 shows the profile generated from Nsight Compute.

Notice the Tile Statistics report section includes the number of tile blocks specified, block size (chosen by compiler), and other tile-specific information.
The source page also supports tile kernels and performance metrics at the source-line level, just like CUDA C++ kernels.
Matrix multiply
An earlier example showed vectorAdd with the details of loading and storing partition views. This matrix multiply example illustrates how to express matrix multiply using very simple code.
This kernel executes an MxK by KxN matrix multiply to compute an MxN matrix. In this kernel, M=8, N=16, and K can be variable, provided it’s a multiple of 8. Set K=24. These very small sizes are used to illustrate the concepts only.
The complete kernel follows, along with an overview of the high points.
#include "cuda_tile.h"
__tile_global__ void kernel(float* __restrict__ a, float* __restrict__ b, size_t length, float* __restrict__ c) {
namespace ct = cuda::tiles;
using namespace ct::literals;
a = ct::assume_aligned(a, 16_ic);
b = ct::assume_aligned(b, 16_ic);
c = ct::assume_aligned(c, 16_ic);
auto aShape = ct::extents{8_ic, length};
auto bShape = ct::extents{length, 16_ic};
auto cShape = ct::extents{8_ic, 16_ic};
auto aSpan = ct::tensor_span{a, aShape};
auto bSpan = ct::tensor_span{b, bShape};
auto cSpan = ct::tensor_span{c, cShape};
auto aView = ct::partition_view{aSpan, ct::shape{4_ic, 8_ic}};
auto bView = ct::partition_view{bSpan, ct::shape{8_ic, 4_ic}};
auto cView = ct::partition_view{cSpan, ct::shape{4_ic, 4_ic}};
using f32x4x4 = ct::tile<float, ct::shape<4, 4>>;
auto accTile = ct::full<f32x4x4>(0);
auto [xBlock, yBlock, dummy] = ct::bid();
for (auto idx : ct::irange(0, 1 + int(length - 1) / 8)) {
auto aTile = aView.load_masked(xBlock, idx);
auto bTile = bView.load_masked(idx, yBlock);
accTile = ct::mma(aTile, bTile, accTile);
}
cView.store_masked(accTile, xBlock, yBlock);
}
- Create extents with ct::extents objects for the a, b, and c matrices. Use either compile or runtime values. M=8 and N=16, but K is variable. These are used to create the tensor spans in the next step.
auto aShape = ct::extents{8_ic, length};
auto bShape = ct::extents{length, 16_ic};
auto cShape = ct::extents{8_ic, 16_ic};
- Create tensor spans. This carries information about a, b, and c to create the partition views.
auto aSpan = ct::tensor_span{a, aShape};
auto bSpan = ct::tensor_span{b, bShape};
auto cSpan = ct::tensor_span{c, cShape};
- Create partition views of a, b, and c with a partitioned as a 4×8 and the view b as an 8×4 view. Adjustments can be made, provided they divide properly into a and b values. These dimensions also determine that the c view is 4×4.
auto aView = ct::partition_view{aSpan, ct::shape{4_ic, 8_ic}};
auto bView = ct::partition_view{bSpan, ct::shape{8_ic, 4_ic}};
auto cView = ct::partition_view{cSpan, ct::shape{4_ic, 4_ic}};
The 2D partitions are indexed in 2 dimensions. The a matrix is 8×24, and the partition view is 4×8, as shown in Figure 2.
The partition view sizes of aView and bView also determine the shape of accTile, the tile used to accumulate results during matrix multiplication. In this example, accTile is a 4×4 tile, matching the shape of cView.

using f32x4x4 = ct::tile<float, ct::shape<4, 4>>;
auto accTile = ct::full<f32x4x4>(0);
- Execute the loop with ct::bid to obtain the block indices in the three dimensions. The loop iterates from 0 to the length / 8, corresponding to the overall K dimension divided by 8. The division by 8 matches the K-dimension of aView and bView is 8. Inside the loop, tiles from a and bare loaded using load_masked, and call ct::mma performs the matrix multiply, accumulating the result in accTile.
auto [xBlock, yBlock, dummy] = ct::bid();
for (auto idx : ct::irange(0, int(length / 8))) {
auto aTile = aView.load_masked(xBlock, idx);
auto bTile = bView.load_masked(idx, yBlock);
accTile = ct::mma(aTile, bTile, accTile);
}
- Store the value of the accTile into the partition view of c, the cView. And that’s it. Most of the kernel code is involved with setting up views for the data and loading/storing the data. The compute portion of the kernel is simple.
cView.store_masked(accTile, xBlock, yBlock);
- Launch the kernel. Use dim3(2,4) because of the dimensions of cView. cView is 4×4, meaning each block is computing a 4×4 chunk of the C matrix. Since C is 8×16, divide the cView dimensions into the C matrix dimensions. Since 8/4=2, and 16/4=4 launch the kernel with dim3(2,4).
kernel<<<dim3(2, 4), 1>>>(d_a, d_b, K, d_c);
Get started today with CUDA Tile C++
The following are required to run CUDA Tile C++ programs:
- A GPU with compute capability 8.x or newer.
- NVIDIA Driver R580 or later. If JIT compilation is required for tile kernels, the NVIDIA driver version must be equivalent to or newer than the version associated with the CUDA Toolkit used to generate the code. For example, CUDA Toolkit 13.3 requires an R610 driver or newer.
- CUDA Toolkit 13.3
The power of tile-based programming is now available to C++ developers. Check out the
関連記事
今日のまとめ
AI日報で今日の重要ニュースをまとめ読み