Skip to main content

One post tagged with "Vulkan"

View All Tags

引言

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 的通信通常通过其他库(如 wgpuvulkanoash)实现。

在本文中,我们使用 wgpu 库来管理 CPU 和 GPU 的交互,确保通信的高效性和跨平台支持。


核心概念:线程与工作组

GPU 的并行计算由以下核心概念构成:

  1. 线程(Thread):最小执行单元,运行 GPU 内核代码。
  2. 工作组(Workgroup):线程的集合,能够共享组内存并协作计算。
  3. 网格(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 的独特优势

  1. 共享代码:Rust 模块化设计可让 CPU 和 GPU 使用相同数据结构,避免重复定义。
  2. 条件编译与 CPU 调试:支持在 CPU 上运行 GPU 内核,方便调试和验证。
  3. 生态系统支持:Rust 的 no_std 和现有库(如 spirv_std)提供了丰富的功能复用能力。
  4. 泛型与零开销抽象:通过特性(Traits)和泛型优化代码的扩展性与可维护性。

总结

Rust GPU 结合 Rust 的安全性与性能优势,为 GPU 编程提供了强大支持。通过本文的四阶段优化,从基础实现到高级平铺技术,展示了如何有效提升矩阵乘法内核性能。

Rust GPU 不仅提升了 GPU 编程的开发体验,更为跨平台高性能计算带来了新的可能性。欢迎开发者加入 Rust GPU 项目,探索 GPU 编程的未来!

鱼雪