引言
GPU 编程通常依赖于如 WGSL、GLSL 或 HLSL 等语言。然而,Rust GPU 项目开辟了新的可能,允许开发者直接使用 Rust 编程语言 编写 GPU 内核代码,结合强大的类型安全性和性能优化能力。
本文基于 Zach Nussbaum 的文章《Optimizing a WebGPU Matmul Kernel for 1TFLOP+ Performance》,详细探讨如何在 Rust GPU 中实现矩阵乘法(matmul)内核优化,逐步探索 Rust 在 GPU 编程中的独特优势。
什么是 Rust GPU?
Rust GPU 是一个专为 GPU 编程设计的项目,通过将 Rust 代码编译为 GPU 可识别的 SPIR-V 格式,使其能够无缝集成到 Vulkan 等兼容的 GPU 编程生态中。
核心特点
- Rust 编程支持:无需依赖 WGSL 等传统 GPU 专用语言。
- 生态兼容性:与 Vulkan、DirectX 和 Metal 集成。
- 安全与高效:Rust 的类型系统和零开销抽象为 GPU 开发提供更高的稳定性。
Rust GPU 的工作原理
Rust GPU 专注于将 Rust 代码编译为 SPIR-V,而 CPU 与 GPU 的通信通常通过其他库(如 wgpu、vulkano 或 ash)实现。
在本文中,我们使用 wgpu 库来管理 CPU 和 GPU 的交互,确保通信的高效性和跨平台支持。
核心概念:线程与工作组
GPU 的并行计算由以下核心概念构成:
- 线程(Thread):最小执行单元,运行 GPU 内核代码。
- 工作组(Workgroup):线程的集合,能够共享组内存并协作计算。
- 网格(Grid):由多个工作组组成,适合大规模任务的并行执行。
工作组维度可通过 (x, y, z)
三维定义,如下所示:
#[spirv(compute(threads(x, y, z)))]
pub fn kernel(...) { ... }
Rust GPU 的实现:从简单到优化
以下是矩阵乘法内核优化的四个阶段。
阶段 1:基础矩阵乘法内核
我们从最基础的矩阵乘法实现开始,为矩阵 (A) 和 (B) 计算结果矩阵 (C)。以下是 Rust GPU 的实现代码:
#![no_std]
use spirv_std::spirv;
#[spirv(compute(threads(1)))]
pub fn matmul(
#[spirv(global_invocation_id)] global_id: UVec3,
#[spirv(uniform, descriptor_set = 0, binding = 0)] dimensions: &Dimensions,
#[spirv(storage_buffer, descriptor_set = 0, binding = 1)] a: &[f32],
#[spirv(storage_buffer, descriptor_set = 0, binding = 2)] b: &[f32],
#[spirv(storage_buffer, descriptor_set = 0, binding = 3)] result: &mut [f32],
) {
let index = global_id.x;
let row = index / dimensions.n;
let col = index % dimensions.n;
if index < dimensions.m * dimensions.n {
let mut sum = 0.0;
for i in 0..dimensions.k {
sum += a[(row * dimensions.k + i) as usize] * b[(i * dimensions.n + col) as usize];
}
result[(row * dimensions.n + col) as usize] = sum;
}
}
问题:
- 每个线程仅计算一个结果,导致启动大量工作组,增加开销。
- 矩阵数据重复加载,未充分利用缓存。
阶段 2:增加线程数量
通过提高工作组线程数(如 compute(threads(256))
),可以显著减少工作组的数量,降低启动开销。
阶段 3:二维工作组
为支持更 大的矩阵,将工作组扩展为二维(如 (16 \times 16)),使每个工作组可以处理更多矩阵元素。
#[spirv(compute(threads(16, 16)))]
pub fn matmul(...) { ... }
阶段 4:内核平铺(Tiling)
通过平铺策略,每个线程一次计算多个矩阵元素,进一步减少启动开销。
#[spirv(compute(threads(16, 16)))]
pub fn matmul(...) {
let row = global_id.y * TILE_M;
let col = global_id.x * TILE_N;
let mut sums = [[0.0; TILE_N as usize]; TILE_M as usize];
for k in 0..dimensions.k as usize {
for i in 0..TILE_M as usize {
let a_elem = a.get(row + i).unwrap_or(&0.0);
for j in 0..TILE_N as usize {
let b_elem = b.get(col + j).unwrap_or(&0.0);
sums[i][j] += a_elem * b_elem;
}
}
}
for i in 0..TILE_M as usize {
for j in 0..TILE_N as usize {
let output_row = row + i;
let output_col = col + j;
if output_row < dimensions.m as usize && output_col < dimensions.n as usize {
result[output_row * dimensions.n as usize + output_col] = sums[i][j];
}
}
}
}
Rust GPU 的独特优势
- 共 享代码:Rust 模块化设计可让 CPU 和 GPU 使用相同数据结构,避免重复定义。
- 条件编译与 CPU 调试:支持在 CPU 上运行 GPU 内核,方便调试和验证。
- 生态系统支持:Rust 的
no_std
和现有库(如spirv_std
)提供了丰富的功能复用能力。 - 泛型与零开销抽象:通过特性(Traits)和泛型优化代码的扩展性与可维护性。
总结
Rust GPU 结合 Rust 的安全性与性能优势,为 GPU 编程提供了强大支持。通过本文的四阶段优化,从基础实现到高级平铺技术,展示了如何有效提升矩阵乘法内核性能。
Rust GPU 不仅提升了 GPU 编程的开发体验,更为跨平台高性能计算带来了新的可能性。欢迎开发者加入 Rust GPU 项目,探索 GPU 编程的未来!