CCCL ランタイム:CUDA 向けのモダンな C++ ランタイム
NVIDIA は CUDA 環境向けに、開発効率と安全性を向上させるモダンな C++ ランタイム「CCCL」を発表し、GPU 計算リソースの活用を革新する。
キーポイント
CCCL の発表と目的
NVIDIA が CUDA 環境向けに設計したモダンな C++ ランタイム「CCCL」を発表し、開発者がより効率的かつ安全に GPU リソースを活用できる基盤を提供する。
開発効率と安全性の向上
従来の CUDA 開発における複雑さを解消し、モダンな C++ の特性を活かしたコード記述を可能にすることで、バグ削減と生産性向上を目指す。
GPU 計算リソースの最適化
新しいランタイムにより、ハードウェアの性能をより直接的かつ安全に引き出す仕組みが構築され、高性能計算の実装が容易になる。
影響分析・編集コメントを表示
影響分析
この発表は、CUDA プログラミングの参入障壁を下げる重要な転換点となり、特に大規模な GPU クラスターや複雑な AI モデル開発において、コードの保守性と性能を同時に向上させる可能性があります。NVIDIA が C++ のモダン化を推進することで、長期的なエコシステムの健全性と開発者の生産性向上に寄与すると考えられます。
編集コメント
CUDA の歴史において、C++ のモダン化を推進する CCCL の登場は、開発者の負担軽減とコードの質向上に直結する画期的な動きです。
NVIDIA CUDA Core Compute Libraries (CCCL) は、C++ および Python の CUDA 開発者向けに、心地よく効率的な抽象化を提供します。その特徴は以下の通りです。
- パラレルアルゴリズム – ソート、スキャン、リダクションを含むホスト起動型アルゴリズムで、一般的な操作のためにカスタムカーネルを記述する必要がなくなります
- コーペレーティブアルゴリズム – ブロック幅またはワープ幅のリダクションやスキャンなどのデバイス側アルゴリズムで、カスタムカーネル開発を簡素化します
- 言語慣習的な CUDA 抽象化 – メモリ割り当て、リソース管理、ハードウェア機能など、CUDA 固有の操作のための基本的な抽象化
本記事では、CUDA C++ 開発をより安全かつ便利にする、基本となる CUDA プログラミングモデル概念に対する近代化された C++ 抽象化を提供する CCCL の新機能グループについて紹介します。
CCCL runtime とは何か?
NVIDIA CCCL runtime は、ストリーム管理、メモリ割り当て、カーネル起動などの中核的な CUDA 機能を実装する、新しい言語慣習的な C++ API のセットです。
従来の NVIDIA CUDA runtime は、CUDA ドライバ API の上に便利層として開発されました。新しい CCCL runtime は同じ目的を持つ代替手段を目指していますが、近代化された C++ に沿った更新された設計を特徴としています。以下の図 1 は、上記の 3 つの CUDA API サーフェス間の関係を示しています:
image*図 1. 異なる CUDA API サフェースのスタック図*
CCCL ランタイムは、<cuda/stream> や <cuda/buffer>, <cuda/launch> など CCCL 内のヘッダー群の集合です。これは現代的な C++ の機能を活用し、従来の CUDA ランタイム API が持つ C ソース互換性の制約内では不可能だったよりも、より便利で堅牢な抽象化を提供します。
また、CUDA の進化における 20 年間の教訓を API デザインに組み込む機会も捉えました。これらの変更がすべて行われたにもかかわらず、CCCL ランタイムは互換性ヘルパーを提供しており、開発者は CUDA ランタイム API を使用する周囲のコードを書き換えながら、段階的にこれを採用することができます。
CUDA プログラムが複雑化し、複数のライブラリがデバイス、ストリーム、メモリを共有するようになると、きれいに合成され依存関係を明確にする API の必要性はより切実なものになります。CCCL ランタイムはまさにこの領域を満たすために設計されています。
コード
新しい CCCL ランタイム API を用いて実装された古典的な vectorAdd 例を以下に示します。以前 CUDA を書いたことがある方なら、全体的な構造は馴染み深いはずです。何が異なるかに焦点を当ててください。一度にすべてを理解しようとせず、この投稿の残りはこの例を通じて CCCL ランタイムの背後にあるセマンティクスと設計思想を解説していきます。
#include <cuda/buffer>
⟦CODE_0⟧
#include <cuda/devices>
#include <cuda/launch>
#include <cuda/memory_pool>
#include <cuda/std/span>
#include <cuda/stream>
struct kernel {
template <typename Config>
__device__ void operator()(Config config,
cuda::std::span<const int> A,
cuda::std::span<const int> B,
cuda::std::span<int> C) {
auto tid = cuda::gpu_thread.rank(cuda::grid, config);
if (tid < A.size())
C[tid] = A[tid] + B[tid];
}
};
int main() {
cuda::device_ref device = cuda::devices[0];
cuda::stream stream{device};
auto pool = cuda::device_default_memory_pool(device);
int num_elements = 1000;
auto A = cuda::make_buffer<int>(stream, pool, num_elements, 1);
auto B = cuda::make_buffer<int>(stream, pool, num_elements, 2);
auto C = cuda::make_buffer<int>(stream, pool, num_elements, cuda::no_init);
constexpr int threads_per_block = 256;
auto config = cuda::distribute<threads_per_block>(num_elements);
cuda::launch(stream, config, kernel{}, A, B, C);
stream.sync();
return 0;
}
この例は、以下の3つの主要なセクションに分解できます。
1.) デバイスとストリーム
CUDA ランタイム API を使用してストリームを作成する際の手順を、次のコードスニペットが示しています。
cudaStream_t stream;
cudaStreamCreate(&stream);
これはストリームを作成しますが、作成されたストリームは cudaStreamCreate が呼び出された時点で現在のデバイスに紐付けられます。この呼び出しだけでは、ストリームがどのデバイスに関連付けられているかは分かりません。
これに対し、CCCL ランタイム API を使用した場合は、次のコードスニペットで示されるようになります。
cuda::device_ref device = cuda::devices[0];
cuda::stream stream{device};
上記のコードスニペットは、特定のデバイス上でストリームを作成する方法を示しています。最初の行は、CCCL ランタイムが採用する核心的な設計原則を説明しています:CCCL ランタイムでは、生粋の識別子ではなく専用の型を使用します。デバイスは単なる整数ではなく device_ref であり、ストリームは不透明なポインタではなくオブジェクトです。API 全体にわたる強い型付けにより、実行時に対処するのではなく、コンパイル時にミスを検出できるようになります。
2 行目は、もう一つの原則を示しています。それは依存関係を明示することです。CCCL ランタイムと CUDA ランタイム API の両方において、ストリームはデバイスに紐付けられます。違いはその方法にあります。ここでは cuda::stream コンストラクタがデバイスを明示的な引数として受け取りますが、CUDA ランタイム API では、ストリーム作成時にアクティブだった任意のデバイスにストリームが紐付けられます。
明示的な依存関係により、局所的な推論が可能になります。関数を読み取るだけで、グローバル状態を追跡することなくその動作を理解できます。また、これによりコンポーザビリティ(構成可能性)も向上します。複数のライブラリを使用する場合、互いに干渉しないように呼び出し間で暗黙の状態を保存・復元する必要がなくなります。
関連する帰結として、CCCL ランタイムはデフォルトストリームを公開しません。デフォルトストリームの意味を管理するには、現在のデバイスを追跡する必要があり、これはまさに私たちが避けようとしている暗黙的状态の一種です。CUDA ランタイム API から提供されるデフォルトストリームを CCCL ランタイム型にラップすることは可能ですが、その使用は推奨されません。デフォルトストリームに関わる処理は、すべて CUDA ランタイム API を介して直接行うべきです。API にデフォルトストリームが存在しないため、「ブロッキングストリーム」という概念も適用されなくなり、CCCL ランタイムのすべてのストリームは非ブロッキングとして作成されます。
リソース所有権:所有型と参照
std::string や std::string_view の例にならい、CCCL ランタイムにおける多くの CUDA オブジェクトには 2 つの型が存在します。所有権を持つタイプと、_ref サフィックスを持つ非所有型のタイプです。cuda::stream は基盤となる cudaStream_t ハンドルを所有し、デストラクタで破棄します。一方、cuda::stream_ref はハンドルの管理を行わずに保持するだけで、単純コピー可能です。
_ref 型は既存のコードとの合成可能性において不可欠です。ストリームハンドルのライフタイムが外部で管理されている場合、cudaStream_t は暗黙的に cuda::stream_ref に変換され、生ハンドルは .get() メソッドで取得できます。所有権を移転するには、cuda::stream::from_native_handle を使用して生ハンドルを所有型にラップし、.release() で所有権を手放します。
void stream_type_example(cudaStream_t handle) {
cuda::stream_ref non_owning{handle};
assert(handle == non_owning.get());
cuda::stream owning = cuda::stream::from_native_handle(handle);
assert(handle == owning.get());
assert(handle == owning.release());
}
このパターンはイベント、メモリプール、およびその他の CUDA オブジェクトにも適用されます。cuda::device_ref には所有型が存在しないのは、所有すべきデバイス状態がないためです。
2.) メモリ割り当て
auto pool = cuda::device_default_memory_pool(device);
auto A = cuda::make_buffer<int>(stream, pool, num_elements, 1);
auto B = cuda::make_buffer<int>(stream, pool, num_elements, 2);
auto C = cuda::make_buffer<int>(stream, pool, num_elements, cuda::no_init);
次のセクションでは、非同期でデバイスメモリを割り当てて初期化する様子を示します。ここで示されるのは、もう一つの設計原則です:API はデフォルトで非同期動作します。同期・非同期のバリアントを名前によって区別するのではなく、CCCL ランタイムは単純な規約を採用しています。つまり、API の最初の引数としてストリームを受け取る場合、その API はストリームの順序に従って動作します。CUDA ランタイム API に両方のバリアントが存在する API に対して、同期版の対応物を提供する予定はありません。
実践においてこれが最も重要となるのはメモリ割り当ての場面です。CUDA 11.2 からストリーム順序付きメモリ管理(メモリープール経由)が利用可能になっており、これは こちら で解説されています。また CUDA 13.0 ではこれがマネージドメモリおよびホストメモリにも拡張されました。最大パフォーマンスを達成するためには、メモリープールの利用と同期ポイントの頻度低減がほとんどの場合不可欠であり、ストリーム順序付きメモリ管理は非同期プログラミングモデルの他の要素と自然に統合されます。これらのガイドラインを伝えるために、CCCL ランタイムではメモリープールおよびストリーム順序付き割り当てをデフォルトとして採用しています。新しいメモリープールタイプがまだサポートされていない古い CUDA バージョンやプラットフォーム上では、ストリーム順序なしの割り当てをフォールバックとして提供していますが、プールサポートが普遍的になった段階でこれを削除する計画です。
上記のスニペットでは、まず指定されたデバイスのデフォルトメモリープールを照会し、cudaMallocAsync の暗黙的なデバイス選択に頼るのではなく、それを明示的な引数として渡しています。この例ではデフォルトプールを使用していますが、可能な限りこちらが推奨されます。ただし CCCL ランタイムでは、異なるプール設定が必要な場合に個別のプールオブジェクトを作成することも可能です。
プール参照はその後、新しい cuda::make_buffer を使用して 3 つのバッファを作成するために使用されます。これはストリームを最初の引数として受け取り、ストリームの順序付けされた操作を示します。各バッファはそのストリームに対して 3 つの操作を提出します:指定されたプールからの割り当て、初期化、およびバッファがスコープから外れたときの最終的な解放です。
初期化は必須ですが、カーネルによって上書きされるバッファ C のように cuda::no_init で明示的に除外する場合は例外です。未初期化のデバイスメモリは診断が難しいバグの一般的な原因となるため、サイレントデフォルトにするのではなく、明示的な除外を要求することを選びました。入力バッファ A と B は、それぞれすべての要素が 1 と 2 に初期化されています。バッファは、他のバッファや範囲からの初期化など、追加の初期化モードもサポートしています。
バッファのライフタイムと解放
make_buffer に渡されたストリームはバッファ内部に保存され、バッファが破棄される際に解放のために使用されます。これは、計算が解放と適切に順序付けられるように、バッファは通常その使用に対応するストリームを保持すべきであることを意味します。後で .set_stream() を使用してストリームを変更したり、.destroy() で特定のストリーム上で手動で破棄トリガーを発生させたりすることも可能ですが、デフォルトの動作は一般的なケースで正しいことを行うように設計されています。
{
auto pool = cuda::device_default_memory_pool(device);
auto buffer = cuda::make_buffer(allocation_stream, pool, );
}
3.) カーネル起動
struct kernel {
template <typename Config>
__device__ void operator()(Config config,
cuda::std::span<const int> A,
cuda::std::span<const int> B,
cuda::std::span<int> C) {
auto tid = cuda::gpu_thread.rank(cuda::grid, config);
if (tid < A.size())
C[tid] = A[tid] + B[tid];
}
};
constexpr int threads_per_block = 256;
auto config = cuda::distribute<threads_per_block>(num_elements);
cuda::launch(stream, config, kernel{}, A, B, C);
最後のセクションでは、cuda::launch を使用して GPU 上でカーネルを構成し起動する方法が示されています。
cuda::launch は3つのグループの引数を受け取ります:
- 実行するストリーム
- スレッド階層(ブロックサイズとグリッドサイズ)およびその他の起動オプションをエンコードした構成オブジェクト。ここでは、cuda::distribute が、num_elements 以上のスレッドを threads_per_block のスレッドを持つブロックにグループ化して起動する構成を作成します。これは、多くの CUDA 開発者が慣れ親しんでいる一般的なパターンである (N + block_size - 1) / block_size を置き換えるものです。
- カーネルとその引数
コンパイル時の構成フロー
cuda::launch の最も革新的な側面は、型システムを通じてホストの起動サイトからコンパイル時情報をデバイスコードへ移動させる点です。例えば、ブロックサイズが cuda::distribute へのテンプレート引数として提供されていることに注目してください。これは、それが構成オブジェクトの型にエンコードされていることを意味します。
カーネルがこの構成を最初の引数として受け入れる場合、cuda::launch はこれを自動的に渡します。カーネル内部では、グリッド内での呼び出しスレッドのランクを計算する際に、この静的情報が利用可能です:
auto tid = cuda::gpu_thread.rank(cuda::grid, config);
ブロックサイズがコンパイル時に既知であるため、ランク計算は x 次元のみを使用し、実行時のブロックサイズ照会を完全に省略できます。これは単純な例ですが、このメカニズムは一般化されます。CCCL のドキュメントでは、構成に埋め込まれた情報がデバイスコードの特殊化に使用されるさらなるケースが示されています。
場合によっては、カーネルの実装においてグリッドや/またはブロックの正確な形状に関する仮定が行われます。構成オブジェクト内のコンパイル時の情報により、カーネル作者はこれらの場合にカーネルと呼び出しサイトの整合性を保証するためのチェックを実装できます。
template <typename Config>
__global__ void kernel(Config conf) {
static_assert(cuda::gpu_thread.static_dims(cuda::block, conf).x == 256);
static_assert(cuda::gpu_thread.static_dims(cuda::block, conf).y == 1);
static_assert(cuda::gpu_thread.static_dims(cuda::block, conf).z == 1);
}
カーネルファンクター
カーネルが __global__ 関数ではなく、__device__ オペレータ () を持つ構造体であることに気づいたかもしれません。cuda::launch は既存の __global__ 関数をサポートしていますが、私たちはカーネルファンクターも導入しました。これは __device__ で注釈された呼び出し演算子を持つ型です。実用的な利点は、テンプレート引数が自動的に推論される点であり、一方、cuda::launch と共に使用される __global__ 関数には明示的なインスタンス化が必要です。
template <typename T>
__global__ void kernel_function(T input) {
}
struct kernel_functor {
template <typename T>
__device__ void operator()(T input) {
}
};
cuda::launch(stream, config, kernel_function<int>, 42);
cuda::launch(stream, config, kernel_functor{}, 42);
これがコンパイル時の設定フローを可能にする仕組みです。config テンプレートパラメータは、cuda::launch によって渡された設定オブジェクトから推論されます。カーネルファンクターはデバイスラムダもカバーしており、CCCL ドキュメント に記載されている追加機能もあります。
自動引数変換
cuda::buffer は自身の基礎となる割り当てを所有しますが、CUDA カーネルは単純コピー可能な引数のみを受け付けることができます。バッファが cuda::launch に渡されると、自動的に cuda::std::span に変換されます。スパンを手動で構築したり、生ポインタを抽出したりする必要はありません。カーネルのシグネチャは、デバイス側でデータが実際にどのように使用されるかを反映しています。
次のステップ
本記事では、CCCL ランタイムの核心となるアイデアについて解説しました。具体的には、明示的な依存関係、強力な型システム、デフォルトで非同期動作する API、そして既存の CUDA コードとのクリーンな相互運用性です。しかし、1 つの例を通じたウォークスルーでは、すべてを網羅することはできません。
CCCL のドキュメントには、各 API に関するより詳細な解説が含まれています。これには、追加のバッファ初期化モード、イベント管理、データ転送が含まれます。また、動的共有メモリやその他の起動属性といった高度なカーネル起動機能に関する情報も提供されています。
CCCL ランタイムは現在、CCCL 内で利用可能です。実際に試していただいた際のフィードバックをぜひお聞かせください。
原文を表示
The NVIDIA CUDA Core Compute Libraries (CCCL) provides delightful and efficient abstractions for CUDA developers in C++ and Python. It features:
- Parallel algorithms – Host-launched algorithms including sort, scan and reduce that remove the need to write custom kernels for common operations
- Cooperative algorithms – Device-side algorithms such as block-wide or warp-wide reductions or scans that simplify custom kernel development
- Language idiomatic CUDA abstractions – Fundamental abstractions for CUDA-specific operations including memory allocation, resource management, and hardware features
This post introduces a new group of functionality in CCCL that provides modernized C++ abstractions for fundamental CUDA programming model concepts that make CUDA C++ development safer and more convenient.
What is CCCL runtime?
NVIDIA CCCL runtime is a new set of idiomatic C++ APIs that implement core CUDA functionality: stream management, memory allocation, kernel launches, and more.
The familiar NVIDIA CUDA runtime was originally developed as a convenience layer on top of the CUDA driver API. The new CCCL runtime aims to be an alternative with the same goal, but with an updated design aligned with modern C++. Figure 1, below, shows the relationship between the three CUDA API surfaces mentioned above:

CCCL runtime is a collection of headers within CCCL, such as <cuda/stream>, <cuda/buffer>, and <cuda/launch>. It leverages modern C++ features to provide more convenient and robust abstractions than what was possible within the C source compatibility constraints of the traditional CUDA runtime API.
We also took the opportunity to incorporate lessons learned over 20 years of CUDA evolution into the API design. Even with all these changes, CCCL runtime provides compatibility helpers that let developers adopt it incrementally without rewriting surrounding code that uses the CUDA runtime API.
As CUDA programs grow more complex, with multiple libraries sharing devices, streams, and memory, the need for APIs that compose cleanly and make dependencies explicit becomes more pressing. That is the space CCCL runtime is designed to fill.
The code
Here is the classic vectorAdd example implemented with the new CCCL runtime APIs. If you’ve written CUDA before, the overall structure will be familiar: Focus on what’s different. Don’t try to understand everything at once, the rest of this post will walk through this example to explain the semantics and design choices behind CCCL runtime.
#include <cuda/buffer>
#include <cuda/devices>
#include <cuda/launch>
#include <cuda/memory_pool>
#include <cuda/std/span>
#include <cuda/stream>
struct kernel {
template <typename Config>
__device__ void operator()(Config config,
cuda::std::span<const int> A,
cuda::std::span<const int> B,
cuda::std::span<int> C) {
auto tid = cuda::gpu_thread.rank(cuda::grid, config);
if (tid < A.size())
C[tid] = A[tid] + B[tid];
}
};
int main() {
cuda::device_ref device = cuda::devices[0];
cuda::stream stream{device};
auto pool = cuda::device_default_memory_pool(device);
int num_elements = 1000;
auto A = cuda::make_buffer<int>(stream, pool, num_elements, 1);
auto B = cuda::make_buffer<int>(stream, pool, num_elements, 2);
auto C = cuda::make_buffer<int>(stream, pool, num_elements, cuda::no_init);
constexpr int threads_per_block = 256;
auto config = cuda::distribute<threads_per_block>(num_elements);
cuda::launch(stream, config, kernel{}, A, B, C);
stream.sync();
return 0;
}
The example can be broken down into the following three main sections:
1.) Devices and streams
Consider the creation of a stream using the CUDA Runtime API as the following code snippet shows.
cudaStream_t stream;
cudaStreamCreate(&stream);
Note this creates a stream, but the stream is associated with whichever device is current when cudaStreamCreate is called. Based on this call alone, you don’t know which device the stream is associated with.
Contrast that with using CCCL runtime API as illustrated by the code snippet that follows.
cuda::device_ref device = cuda::devices[0];
cuda::stream stream{device};
The above code snippet shows how to create a stream on a specific device. The first line illustrates a core design principle: CCCL runtime uses dedicated types instead of raw identifiers. A device is a device_ref, not a plain integer; a stream is an object, not an opaque pointer. Strong typing across the API helps catch mistakes at compile time rather than chasing them at runtime.
The second line illustrates another principle: making dependencies explicit. In both CCCL runtime and the CUDA runtime API, a stream is associated with a device. The difference is how. Here, the cuda::stream constructor takes the device as an explicit argument whereas with the CUDA runtime API the stream is associated with whichever device is active when the stream is created.
Explicit dependencies enable local reasoning. You can read a function and understand what it does without tracking the global state. They also improve composability: When multiple libraries are used, none of them need to save and restore implicit state across calls to avoid interfering with each other.
A related consequence is that CCCL runtime doesn’t expose the default stream. Managing the meaning of the default stream requires tracking the current device, which is exactly the kind of implicit state we are moving away from. While a default stream from the CUDA runtime API can still be wrapped into CCCL runtime types, its usage is discouraged; anything involving the default stream should be handled through the CUDA runtime API directly. With no default stream in the API, the notion of a “blocking stream” no longer applies, so all CCCL runtime streams are created as non-blocking.
Resource ownership: Owning types and refs
Following the example of std::string and std::string_view, many CUDA objects have two types in CCCL runtime: an owning type and a non-owning type with a _ref suffix; cuda::stream owns the underlying cudaStream_t handle and destroys it in its destructor. The cuda::stream_ref holds the handle without managing its lifetime and is trivially copyable.
The _ref types are essential for composability with existing code. If a stream handle’s lifetime is managed elsewhere, cudaStream_t implicitly converts to cuda::stream_ref, and the raw handle can be retrieved with .get(). To transfer ownership, cuda::stream::from_native_handle wraps a raw handle into the owning type, and .release() relinquishes ownership back.
void stream_type_example(cudaStream_t handle) {
cuda::stream_ref non_owning{handle};
assert(handle == non_owning.get());
cuda::stream owning = cuda::stream::from_native_handle(handle);
assert(handle == owning.get());
assert(handle == owning.release());
}
The same pattern applies to events, memory pools, and other CUDA objects: cuda::device_ref has no owning counterpart because there is no device state to own.
2.) Memory allocation
auto pool = cuda::device_default_memory_pool(device);
auto A = cuda::make_buffer<int>(stream, pool, num_elements, 1);
auto B = cuda::make_buffer<int>(stream, pool, num_elements, 2);
auto C = cuda::make_buffer<int>(stream, pool, num_elements, cuda::no_init);
The next section demonstrates asynchronously allocating and initializing device memory. Here we see the next design principle: APIs are asynchronous by default. Rather than distinguishing synchronous and asynchronous variants by name, CCCL runtime uses a simple convention: If an API takes a stream as its first argument, it operates in stream order. We don’t plan to provide synchronous counterparts for APIs that have both variants in the CUDA runtime API.
Memory allocation is where this matters most in practice. Stream-ordered memory management via memory pools has been available since CUDA 11.2 (explained here), and CUDA 13.0 expanded it to managed and host memory. Memory pooling and less frequent synchronization points are in most cases essential to reach maximum performance, and stream-ordered memory management composes naturally with the rest of the asynchronous programming model. To convey those guidelines, CCCL runtime makes memory pools and stream-ordered allocation the default. On older CUDA versions and platforms, where newer memory pool types are not yet supported, we provide non-stream-ordered allocation as a fallback, but plan to remove it once pool support is universal.
In the snippet above, we first query the default memory pool for a given device, passing it as an explicit argument rather than relying on cudaMallocAsync‘s implicit device selection. The example uses the default pool which should be preferred where possible, but CCCL runtime also allows creating separate pool objects when different pool settings are needed.
The pool reference is then used to create three buffers using the new cuda::make_buffer. It takes a stream as its first argument to signal stream-ordered operation. Each buffer submits three operations to that stream: allocation from the specified pool, initialization, and eventually deallocation when the buffer goes out of scope.
Initialization is mandatory unless explicitly opted out with cuda::no_init, as with buffer C which will be overwritten by the kernel. Uninitialized device memory is a common source of hard-to-diagnose bugs, so we chose to require an explicit opt-out rather than making it the silent default. Input buffers A and B have all elements initialized to 1 and 2, respectively. Buffers support additional initialization modes as well, for example from another buffer or a range.
Buffer lifetime and deallocation
The stream passed to make_buffer is stored inside the buffer and used for deallocation when the buffer is destroyed. This means the buffer should generally hold the stream that corresponds to its usage, so that computation is properly ordered with deallocation. It is possible to change the stream later with .set_stream() or manually trigger destruction on a specific stream with .destroy(), but the default behavior is designed to do the right thing in the common case.
{
auto pool = cuda::device_default_memory_pool(device);
auto buffer = cuda::make_buffer(allocation_stream, pool, );
}
3.) Kernel launch
struct kernel {
template <typename Config>
__device__ void operator()(Config config,
cuda::std::span<const int> A,
cuda::std::span<const int> B,
cuda::std::span<int> C) {
auto tid = cuda::gpu_thread.rank(cuda::grid, config);
if (tid < A.size())
C[tid] = A[tid] + B[tid];
}
};
constexpr int threads_per_block = 256;
auto config = cuda::distribute<threads_per_block>(num_elements);
cuda::launch(stream, config, kernel{}, A, B, C);
The final section demonstrates configuring and launching the kernel on the GPU with cuda::launch.
cuda::launch takes three groups of arguments:
- The stream to run on
- A configuration object that encodes the thread hierarchy (block and grid sizes) along with other launch options. Here, cuda::distribute creates a configuration that launches at least num_elements threads grouped into blocks of threads_per_block. This replaces the common pattern many CUDA developers are familiar with of (N + block_size - 1) / block_size
- The kernel and its arguments
Compile-time configuration flow
The most novel aspect of cuda::launch is how it moves compile-time information from the host launch site into device code through the type system. For example, notice how the block size is provided as a template argument to cuda::distribute, which means it is encoded in the configuration object’s type.
When the kernel accepts that configuration as its first argument, cuda::launch passes it through automatically. Inside the kernel, this static information is available when we compute the rank of the calling thread inside the grid:
auto tid = cuda::gpu_thread.rank(cuda::grid, config);
Because the block size is known at compile time, the rank calculation can use only the x dimension and skip the runtime block-size query entirely. This is a simple example, but the mechanism generalizes. The CCCL documentation shows further cases where configuration-embedded information is used to specialize device code.
Sometimes kernel implementation makes assumptions about the exact shape of the grid and/or block. Compile time information in the configuration object allows kernel authors to implement checks to ensure alignment of the kernel and the call site in those cases.
template <typename Config>
__global__ void kernel(Config conf) {
static_assert(cuda::gpu_thread.static_dims(cuda::block, conf).x == 256);
static_assert(cuda::gpu_thread.static_dims(cuda::block, conf).y == 1);
static_assert(cuda::gpu_thread.static_dims(cuda::block, conf).z == 1);
}
Kernel functors
You may have noticed the kernel is a struct with a __device__ operator() rather than a __global__ function. While cuda::launch supports existing __global__ functions, we also introduced kernel functors: types with a __device__-annotated call operator. The practical advantage is that template arguments are deduced automatically, whereas __global__ functions used with cuda::launch require explicit instantiation.
template <typename T>
__global__ void kernel_function(T input) {
}
struct kernel_functor {
template <typename T>
__device__ void operator()(T input) {
}
};
cuda::launch(stream, config, kernel_function<int>, 42);
cuda::launch(stream, config, kernel_functor{}, 42);
This is what makes the compile-time configuration flow work. The config template parameter is deduced from the configuration object passed by cuda::launch. Kernel functors also cover device lambdas and have additional features described in the CCCL documentation.
Automatic argument transformation
cuda::buffer owns its underlying allocation, but CUDA kernels can only accept trivially copyable arguments. When a buffer is passed to cuda::launch, it is automatically transformed to cuda::std::span. There is no need to manually construct the span or extract a raw pointer. The kernel signature reflects how the data is actually used on the device side.
What’s next
This post covered the core ideas behind CCCL runtime: explicit dependencies, strong typing, asynchronous-by-default APIs, and clean interoperability with existing CUDA code. But a walkthrough of one example can only show so much. The CCCL documentation has more detailed coverage of each API, including additional buffer initialization modes, event management, data movement, and advanced kernel launch features like dynamic shared memory and other launch attributes. CCCL runtime is available today in CCCL. We’d love to hear your feedback as you try it out.
関連記事
NVIDIA cuTile Python チュートリアル:Colab でベクトル加算、行列加算、行列乗算を行うタイル化 GPU カーネルの構築
NVIDIA は Colab 環境で cuTile を使用し、Python から直接効率的な CUDA スタイルカーネルを実装するチュートリアルを提供した。この手法はベクトルや行列の演算をタイル処理で高速化する。
CUDAタイルプログラミングがBASICで利用可能に!
NVIDIAがCUDA 13.1でCUDA Tileを導入し、BASIC言語で細粒度並列処理をよりアクセスしやすく柔軟にする次世代タイルベースGPUプログラミングパラダイムを提供した。
CUDA 13.2が強化されたCUDA Tileサポートと新Python機能を導入
NVIDIAがCUDA 13.2をリリースし、CUDA TileをNVIDIA AmpereとAdaアーキテクチャでサポートし、Python機能も追加した。
今日のまとめ
AI日報で今日の重要ニュースをまとめ読み