GPU上的Rust SIMD
VectorWare 于2026年8月10日发布 文章阅读时间:12分钟 文风小心:关闭 GPU代码现在可以使用Rust的可移植SIMD。我们分享了实现方法以及这为GPU编程带来的解锁能力。在VectorWare,我们正在构建第一家GPU原生软件公司。今天,我们很高兴地宣布,我们已经成功地在GPU上使用Rust的可移植SIMD(core::simd)。这一里程碑标志着实现我们愿景的重要一步,使开发者能够利用熟悉的Rust抽象写出复杂的高性能应用程序,从而充分发挥GPU硬件的能力。 线程下的并行性 当我们将Rust线程引入GPU时,我们将每个std::thread映射到GPU的warp上。这使我们能够在GPU上运行许多并发线程,但并没有利用每个线程/warp中的并行通道。在CPU上,线程内部的并行性抽象是SIMD。单个指令在打包成向量单元的多个数据元素上操作:例如,标量代码加两个数字,而SIMD加法同时对两个向量中的八个f32值进行相加并产生八个和。这个数据并行性位于单个线程内,低于操作系统调度任何内容的级别。 CPU线程SIMD运算012N⋯SIMD通道 Rust的可移植SIMD 从历史上看,在Rust中编写SIMD意味着需要调用core::arch中的架构特定的供货商内在函数,例如x86-64上的_mm256_add_ps或Arm上的vaddq_f32。这些内在函数特定于单一指令集,因此在多个架构上运行的程序需要为每个架构提供单独的实现。Rust的可移植SIMD则在这些内在函数之上增加了一层抽象。它提供一个表示类型T的N个元素向量的单一通用类型Simd<T, N>。程序仅需针对Simd一次编写其算术、比较、归约和通道交换,编译器将其降级为目标CPU拥有的任何向量指令。在VectorWare,我们意识到GPU只是可移植SIMD要目标的另一个向量硬件。作为一种奖励,可移植SIMD存在于core中,而不是std,并且甚至不需要我们为GPU带来的std支持。 SIMT是SIMD GPU执行基于NVIDIA称之为SIMT(单指令,多线程)的模型。一个warp发出一条指令,每个32条通道在自己的数据上运行该指令。在多个数据元素上操作一个指令正是SIMD的含义,而SIMT增加的每条通道寻址并没有改变这一点。一个warp是一个宽向量单元,一个可移植SIMD向量直接映射到该单元。 CPU线程012N⋯SIMD通道≈GPU warp012N⋯warp通道 例如,一个Simd<i16, 32>为每个warp的32条通道提供一个i16元素,添加两个这样的向量编译为一条单一的warp指令,每条通道同时添加其元素。 CPU中 let a: Simd<i16, 32> = [1, 1, 1, ..., 1]; let b: Simd<i16, 32> = [2, 2, 2, ..., 2]; let c = a + b; 编译为 vpaddw %zmm2, %zmm1, %zmm0 a0+b0通道0 a1+b1通道1 a2+b2通道2 a31+b31通道31 ⋯ println!("{c:?}"); GPU中 let a: Simd<i16, 32> = [1, 1, 1, ..., 1]; let b: Simd<i16, 32> = [2, 2, 2, ..., 2]; let c = a + b; 编译为 add.s16 %rs3, %rs1, %rs2; a0+b0通道0 a1+b1通道1 a2+b2通道2 a31+b31通道31 ⋯ println!("{c:?}"); 这种新映射完成了我们早期工作的并行性层次结构。在CPU上,线程包含SIMD通道,而在GPU上,我们的std::thread是一个warp,其硬件通道扮演着同样的角色。在这两种情况下,core::simd驱动着这些通道。 CPU⋯线程0012N⋯线程1012N⋯线程N012N⋯SIMD通道≈GPU⋯warp0012N⋯warp1012N⋯warpN012N⋯warp通道 一项世界首创:GPU上的core::simd 与我们之前的帖子一样,这个呈现方式很难在视觉上展示,因为代码是普通的Rust。那些在笔记本电脑上降级到x86-64 SIMD的同一core::simd类型在GPU上降级为warp操作,源代码没有改变。这里我们定义一个小的可移植SIMD例程并从main调用它。它锻炼了模型的核心特性:逐元素算术、生成通道掩码的比较、由该掩码驱动的选择以及跨通道的横向归约。 #![feature(portable_simd)] 使用 core::simd::cmp::SimdPartialOrd; 使用 core::simd::num::SimdFloat; 使用 core::simd::{Select, Simd}; // 可移植SIMD。这一确切的函数也可以编译并在CPU上运行, // 其降级到x86-64、Arm或标量代码,具体取决于目标。 fn relu_dot(a: Simd<f32, 32>, b: Simd<f32, 32>) -> f32 { // 逐元素相乘:一次计算32个乘积。 let products = a * b; // 每条通道的比较生成一个掩码,每条通道一个布尔值。 let positive = products.simd_gt(Simd::splat(0.0)); // 保留正值乘积,其他替换为零。 let clamped = positive.select(products, Simd::splat(0.0)); // 在所有通道上进行横向加法,归约到单个标量。 clamped.reduce_sum() } fn main() { // 两个32宽的向量,用普通Rust构建。 let a = Simd::<f32, 32>::splat(2.0); let b = Simd::<f32, 32>::splat(3.0);
本站免费、广告极少。如果觉得有帮助,可以请我们喝杯咖啡 —— 任何金额都对持续运营有实际帮助。
☕请我喝杯咖啡