
2026/02/18 1:53
GPU 上での Async / Await --- **日本語訳(自然な表現)** > **GPU 上での Async / Await**
RSS: https://news.ycombinator.com/rss
要約▶
Japanese Translation:
VectorWareは、Rustのasync/await構文とFutureトレイト(通常は非同期CPUタスクに使用される)が直接GPU PTXカーネルへコンパイルできることを実証し、高性能GPUプログラミングを可能にしつつ、慣れ親しんだRustの抽象化を保持しています。
このマイルストーンは、
async_double、async_add_then_doubleなどの単純な非同期関数が、block_onを使用して単一カーネルから実行されることを示しています。RustのFutureはハードウェア非依存状態機械にコンパイルされるため、言語の変更なしでGPU上で動作します。その後の改良ではEmbassyのno_stdエグゼキュータが統合され、複数のGPUタスクがアトミック操作と短時間スリープ(
nanosleep)を介して交互に実行できるようになりました。このモデルはJAX、Triton、CUDA‑Tileなどのフレームワークに似ていますが、新しいDSLを作成する必要はありません。しかし、協調型FutureはGPU上で(割り込みがないため)スピンポーリングを引き起こしやすく、スケジューリング状態から生じるレジスタ圧迫がオキュペンシーを低下させ、関数のカラーリング問題も未解決です。
VectorWareは、より効率的なタスクスケジューリングとGPUプリミティブとの深い統合を実現するために、CUDA GraphsまたはTileランタイムを用いたネイティブGPUエグゼキュータの構築を計画しています。最近ではRustのstdがGPU上で有効になり、利用可能なランタイムが拡張される一方でFutureは
no_std互換のままです。今後の作業ではasync/await以外のRust並行モデルを探索し、実験結果を共有する予定です。長期的には言語非依存ですが、現在VectorWareは高性能GPUネイティブアプリケーションに最適な選択肢としてRustを位置づけています。
本文
VectorWare、GPU上でのasync/await実装を発表
2026年2月17日 – 15分読解
ペダントモード: オフ
GPUコードにRustのasync/awaitが使えるようになりました。ここではその理由と、GPUプログラミングにおいて何が可能になるかをご紹介します。
VectorWareとは
VectorWareは「GPUネイティブソフトウェア企業」として初めて登場することを目指しています。本日、Rustの
Futureトレイトとasync/awaitをGPU上で正常に動作させることができたというニュースを発表します。このマイルストーンは、開発者が高性能なGPUハードウェアを最大限に活用しつつ、馴染みのあるRust抽象化を使って複雑なアプリケーションを書けるビジョンへの大きな一歩です。
GPU上での並行プログラミング
従来のGPUプログラミングはデータパラレルに重点を置いています。開発者は単一の操作を書き、GPUがそれをデータの各部分で同時に実行します。
fn conceptual_gpu_kernel(data) { // すべてのスレッドが異なるデータ領域で同じ処理を行う data[thread_id] = data[thread_id] * 2; }
このモデルは、グラフィックスレンダリングや行列乗算、画像処理といった「単一のタスク」には最適です。
GPUプログラムが複雑になるにつれて、開発者は**ワープ特殊化(warp specialization)**を使ってより高度な制御フローや動的挙動を導入します。ワープ特殊化ではGPUの異なる部分が同時にプログラムの別々の部分を実行します。
fn conceptual_gpu_kernel(data) { let communication = ...; if warp == 0 { // ワープ0はメインメモリからデータをロード load(data, communication); } else if warp == 1 { // ワープ1はロードしたデータでAを計算しBへ渡す compute_A(communication); } else { // ワープ2と3はBを計算してストア compute_B(communication, data); } }
ワープ特殊化により、GPUロジックは「均一なデータパラレル」から「明示的なタスクベースの並列」にシフトします。これにより、一部のワープがメモリをロードしつつ他のワープが計算を行うなど、ハードウェアをより効率的に利用できるようになります。
しかし、この柔軟性は手動で同期と並行処理を管理する必要があるというコストを伴います。CPU上のスレッドや同期同様、エラーが起きやすく、理解しづらいです。
GPU上でのより良い並行プログラミング
ワープ特殊化の利点を享受しつつ、手動で同期・並列処理を行う煩わしさをなくしたプロジェクトがいくつか存在します。
| プロジェクト | 主要アイデア |
|---|---|
| JAX | GPUプログラムを計算グラフとして表現。PythonベースDSLで定義し、コンパイラが依存関係と並列度を解析して最適化します。CPU・TPUも同一コードで動作可能です。 |
| Triton | GPU上のブロック単位で計算を表現。Python DSLでブロックの実行方法を定義し、MLIRパイプラインで低レベル化します。 |
| CUDA Tile(NVIDIA) | ブロックと「タイル」を第一級オブジェクトとして扱い、データ依存関係を明示化。Python等から書いたコードをMLIRのTile IRへ変換しGPUで実行します。 |
特にCUDA Tileは「作業単位・データ単位を明示的に構造化」するという点で魅力的です。GPUハードウェアと自然に合致し、ソフトウェア側の変更によって安全かつ高速なコードが書けると考えています。
現行手法の欠点
これら高レベル抽象化は、開発者に新しい構造を強制します。結果として、一部のアプリケーションクラスには適さず、新しいパラダイムやエコシステムへの移行が障壁となります。
- JAX / Triton は主に機械学習向けで、GPU上で自然にマッチします。
- CUDA Tile は汎用性が高いものの、まだ広く採用されていません。
ほとんどの場合、開発者はアプリ全体をこれらのフレームワークだけで書くことなく、部分的に利用し、残りは従来型言語・モデルで実装します。
さらに コード再利用性 も低く、既存のCPUライブラリはそのままでは使えず、GPUライブラリも手動同期が必要なため統合できません。
したがって、**「新しい言語やエコシステムを導入せずに」**明示的・構造化された並列処理の利点を取り込める抽象化が求められます。
Rust の Future
と async/await
FutureRust の
Future トレイトと async/await は、既存言語で 構造化並行 を実現できる抽象化です。ハードウェアや OS に依存せず、同じコードを CPU でも GPU でも動かせます。
- Future は「まだ完了していない計算」を表しますが、どのスレッド・コア・ブロック・タイル・ワープで実行されるかは指定しません。
メソッド(Ready / Pending)だけを定義すればよく、上位に非同期処理を積み重ねていけます。poll
これらの特性により:
- JAX の計算グラフ と同様に「遅延実行」かつコンポーザブル。
- Triton のブロック と同じく、非同期タスクを独立して表現。
- CUDA Tile の明示的データ依存 を Rust の所有権・借用(Pin, Send/Sync)で自然に表現。
ワープ特殊化は実際には「手書きの状態機械」に相当しますが、
Future はコンパイラが自動生成する状態機械として扱われます。
GPU 上で初めて async/await を動作させる
GPU 版 async/await は 見た目は従来の Rust と変わりません。同じ構文をそのまま使用できます。以下に簡単な例を示します。
// GPU カーネルから呼び出す小さな非同期関数群 async fn async_double(x: i32) -> i32 { x * 2 } async fn async_add_then_double(a: i32, b: i32) -> i32 { let sum = a + b; async_double(sum).await } async fn async_conditional(x: i32, do_double: bool) -> i32 { if do_double { async_double(x).await } else { x } } async fn async_multi_step(x: i32) -> i32 { let step1 = async_double(x).await; let step2 = async_double(step1).await; step2 } #[unsafe(no_mangle)] pub unsafe extern "ptx-kernel" fn demo_async(val: i32, flag: u8) { // 基本的な await が GPU 上で動作 let doubled = block_on(async_double(val)); // チェインされた呼び出しも正しく実行 let chained = block_on(async_add_then_double(val, doubled)); // 条件分岐を含む async もサポート let conditional = block_on(async_conditional(val, flag)); // 複数 await ポイントの関数も機能 let multi_step = block_on(async_multi_step(val)); // async ブロックも自然に組み込める let from_block = block_on(async { let doubled_a = async_double(val).await; let doubled_b = async_double(chained).await; doubled_a.wrapping_add(doubled_b) }); // 既存の CPU 用ユーティリティも使用可能 use futures_util::future::ready; use futures_util::FutureExt; let from_combinator = block_on( ready(val).then(move |v| ready(v.wrapping_mul(2).wrapping_add(100))) ); }
このコードを GPU で動かすために、コンパイラバックエンドの複数箇所を修正し、NVIDIA の
ptxas ツールのバグも報告・回避しました。
GPU 上の実行環境(Executor)
async/await は 自ら動作するわけではなく Executor が「ポーリング」して進行させます。Rust には組み込み Executor は無く、外部ライブラリが提供します。
初期段階:block_on
block_on最初に
block_on を使い、単一 Future を完了まで繰り返し poll するだけの実装で十分です。これで GPU 上でも async/await が機能することを証明できます。
高度な Executor:Embassy
GPU は従来型 OS を持たないため、Rust の
#![no_std] 環境が適しています。Embassy は組み込みシステム向けに設計された非同期実行器で、no_std でも動作します。わずかな変更で GPU 上に導入でき、既存のオープンソースライブラリを再利用できます。
以下は 無限ループタスク を三つ作り、共有状態でカウンタをインクリメントしながらスケジューリングを示す例です。
#![no_std] #![feature(abi_ptx)] #![feature(stdarch_nvptx)] use core::future::Future; use core::pin::Pin; use core::sync::atomic::{AtomicU32, Ordering}; use core::task::{Context, Poll}; use embassy_executor::Executor; use ptx_embassy_shared::SharedState; pub struct InfiniteWorkFuture { pub shared: &'static SharedState, pub iteration_counter: &'static AtomicU32, } impl Future for InfiniteWorkFuture { type Output = (); fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> { // ホストから停止要求があればトラップ if self.shared.stop_flag.load(Ordering::Relaxed) != 0 { unsafe { core::arch::nvptx::trap() }; } // カウンタを更新(デモ用) self.iteration_counter.fetch_add(1, Ordering::Relaxed); self.shared.last_activity.fetch_add(1, Ordering::Relaxed); // 小さなスリープで「作業」を模倣 unsafe { core::arch::nvptx::_nanosleep(100); } cx.waker().wake_by_ref(); Poll::Pending } } #[embassy_executor::task] async fn task_a(shared: &'static SharedState) { InfiniteWorkFuture { iteration_counter: &shared.task_a_iterations, shared }.await; } #[embassy_executor::task] async fn task_b(shared: &'static SharedState) { InfiniteWorkFuture { iteration_counter: &shared.task_b_iterations, shared }.await; } #[embassy_executor::task] async fn task_c(shared: &'static SharedState) { InfiniteWorkFuture { iteration_counter: &shared.task_c_iterations, shared }.await; } #[unsafe(no_mangle)] pub unsafe extern "ptx-kernel" fn run_forever(shared_state: *mut SharedState) { let shared = unsafe { &const (*shared_state) }; executor.run(|spawner| { if let Ok(token) = task_a(shared) { spawner.spawn(token); } if let Ok(token) = task_b(shared) { spawner.spawn(token); } if let Ok(token) = task_c(shared) { spawner.spawn(token); } }); }
Asciinema の録画では、Embassy Executor が GPU 上で複数タスクを並行実行している様子が確認できます。パフォーマンスは示唆的に留めておきます(例は無限ループと原子操作のみ)。
Rust async/await の GPU における欠点
| 欠点 | 説明 |
|---|---|
| 協調性 | Future が yield しない場合、他のタスクを飢餓させる可能性があります。CPU 上と同じ失敗モードです。 |
| 割り込みが無い | GPU は割り込みをサポートしていないため、Executor は定期的に しなければ進行できません。スピンループや を使いますが、CPU の割り込み駆動より非効率です。 |
| レジスタ圧迫 | Futures とスケジューリング状態を保持すると GPU 上のレジスタ使用量が増え、オクパンシー低下につながります。 |
| 関数カラーリング | CPU 版と同様に関数ごとの色分け(カラーリング)問題があります。 |
今後の展望
CPU 上では Tokio・Glommio・Smol といった多彩な Executor が存在します。GPU 上でも同様に ハードウェア特性 に合わせた Executor の登場が期待されます。
- CUDA Graphs や CUDA Tile を利用したタスクスケジューリング
- 共有メモリを活用した高速通信
- GPU 固有のスケジューリングプリミティブとの深い統合
VectorWare は最近 GPU 上で
を有効化しました。Future は std
no_std に対応しているため、主要機能に影響はありませんが、標準ライブラリを利用できるようになったことで、より豊富なランタイムや既存の async ライブラリとの統合が可能になります。
また、async/await 以外にも Rust ベース の並行性表現を検討中です。詳細は今後の投稿で共有します。
VectorWare は Rust に限定されるか?
この研究は数か月前に完了しました。GPU 上で高速・信頼性の高いアプリケーションを構築する上で、Rust の抽象化とエコシステムが大きな力を発揮しています。しかし、VectorWare は複数言語・ランタイムをサポートし、将来的には Rust 以外の開発者にも対応します。現在は「GPU ネイティブアプリケーション」に最適化された Rust を中心に据えています。
フォローして最新情報を入手
X、Bluesky、LinkedIn でフォローするか、ブログ購読で進捗を受け取ってください。今後数ヶ月でさらに多くの成果を発表予定です。また、質問や問い合わせは hello@vectorware.com までどうぞ。