Rust 正在走向 GPU,但这条路比想象中更复杂。长期以来,GPU 上的 Rust 代码只能使用 core 和 alloc,std 库因依赖操作系统而不可用。然而,当机器学习和 AI 工作负载要求 GPU 直接访问存储和网络时,std 的缺席成为生产力的瓶颈。VectorWare 近期宣布成功让 GPU 代码调用 Rust 标准库,这标志着重要的里程碑。但其实现路径揭示了更深层的问题:Rust 标准库的 API 设计在很大程度上是围绕 CPU 和传统操作系统构建的,直接迁移到 GPU 面临根本性的适配挑战。本文将系统梳理这些困境,并探讨可能的工程解法。
标准库的分层架构与 GPU 的先天缺失
理解问题本质需要先回顾 Rust 标准库的设计哲学。Rust 的 std 并非单一实体,而是由三个层次构成的抽象塔。底层是 core,它定义了语言的基础类型、迭代器、trait 系统和泛型基础设施,不假设任何运行环境,既不需要堆内存也不依赖操作系统。第二层是 alloc,它在 core 之上添加了堆分配能力,包括 Vec、String、Box 等动态数据结构。最顶层是 std,它整合了 core 和 alloc,并进一步提供文件系统、网络套接字、进程线程、环境变量等操作系统相关 API。
这种分层设计是 Rust 的核心优势之一。开发者可以通过 #![no_std] 注解声明放弃 std,仅使用 core 和必要的 alloc,从而将代码移植到嵌入式系统、固件、内核模块等没有传统操作系统的环境。GPU 编程恰恰属于这一范畴。主流 GPU 编程模型,无论是 CUDA、HIP 还是 Vulkan Compute,都不提供 POSIX 或 Windows 风格的系统调用接口。GPU 代码运行在设备端,由主机 CPU 通过显式的内存传输和内核启动来控制。因此,当前的 Rust GPU 编译路径(如 rust-gpu 和 rust-cuda)都强制启用 no_std,将标准库使用限制在 core 和 alloc 层面。
这种限制的代价是显著的。std 包含了 Rust 生态系统中最成熟、最常用的抽象,如错误处理类型 Result、集合操作、格式化宏以及文件和网络 API。虽然这些抽象的大部分实现可以在 core 中复用,但任何涉及系统资源的操作都必须在 GPU 上重新实现或完全禁用。结果是,大量现有的 Rust 开源库无法直接在 GPU 上使用,开发者被迫为 GPU 重写代码,或者依赖特定厂商提供的有限运行时库。
线程模型的不可兼容性
将 std 移植到 GPU 的第一重障碍来自线程模型的根本差异。CPU 上的 Rust 程序假设线程由操作系统调度,每个线程拥有独立的栈空间、程序计数器和资源句柄,可通过 std::thread API 动态创建和销毁。线程之间的同步依赖内存屏障、原子操作和操作系统提供的原语如互斥锁和条件变量。这种模型是异步的、抢占式的,调度决策对用户代码透明。
GPU 的线程模型则截然不同。GPU 并非运行少量长时间存在的线程,而是启动成千上万个轻量级执行单元同时执行相同的内核代码。这些执行单元以层次化方式组织:最顶层是 launch 或 dispatch,描述整个网格的规模;中间是 workgroup(CUDA 称之为 block),同一 workgroup 内的线程可以访问共享内存并通过显式障碍同步;最底层是 warp 或 wavefront,32 个或 64 个线程以 SIMD 方式执行,实际硬件一次发射一条指令。当前 Rust 标准库完全没有这些概念。std::thread::spawn 期望的是一个可以独立调度、生命周期由程序控制的执行上下文,而 GPU 内核中的每个线程只是硬件执行上下文的瞬时实例,没有独立的调度实体。
这种不匹配意味着,即使技术上可以让 GPU 代码调用 std::thread::spawn,其语义也会完全扭曲。设想一个 GPU 内核试图 spawn 新线程,在真实的 GPU 执行模型中,这要么不可能(硬件不支持),要么必须被重新解释为启动新的计算工作项,或者完全被忽略。这不是简单的实现细节问题,而是编程模型的根本冲突。Rust 社区已经开始讨论如何在编译器层面命名和组织这些 GPU 特有的概念,例如将 GPU 可执行代码称为 gpu-kernel,将执行层级命名为 launch、workgroup 和 wave。但这些讨论目前仅限于命名层面,尚未触及标准库 API 的实质性设计。
内存地址空间的多样性
第二重障碍来自 GPU 的内存架构。CPU 编程通常假设一个统一的虚拟地址空间:指针是普通的机器字,指向主内存或缓存,对程序员基本透明。Rust 的引用类型和指针类型(如 &T、&mut T、*const T、*mut T)在语义上假设这种统一性。alloc 中的堆分配通过全局分配器进行,结果是一个可以从程序任何位置访问的指针。
GPU 则拥有多种物理上分离、语义上不同的内存区域,统称为地址空间。全局地址空间对应于显存(VRAM),容量大但延迟高,适合跨线程共享的数据。常量地址空间是全局空间的子集,专门存放只读数据,硬件可以对其进行缓存优化。工作组共享地址空间(workgroup-shared memory)位于每个 workgroup 内部,容量较小但访问速度接近寄存器,用于同一组线程之间的临时数据交换。私有地址空间则是每个线程的本地栈空间,用于局部变量和函数调用。这些地址空间在 LLVM IR 中用数字编号标识,在不同 GPU 架构(NVPTX、AMDGCN、SPIR-V)之间命名各异,但概念上普遍存在。
Rust 现有的类型系统无法直接区分这些地址空间。当 Vec<T> 被分配在全局空间时,它的内部指针是指向全局内存的;当同样的类型被用于 workgroup-shared 存储时,语义完全不同。更复杂的是,某些操作只在特定地址空间上有意义。例如,原子操作通常只在全局和共享空间上支持;某些 SIMD 宽度的向量类型可能只在私有空间上原生支持。现有的 Rust 标准库完全没有考虑这些约束,所有内存操作都被假设为在统一空间中执行。
一个务实的工程策略是让 std 和 alloc 的默认行为保持保守:所有堆分配默认落在全局地址空间,所有指针都是全局指针。这虽然无法利用共享内存的性能优势,但至少保证语义正确。进一步的优化可以留给显式的 API 扩展,例如提供 VecInWorkgroup<T> 这样的类型来指示分配在共享空间。但这就需要重新设计 Rust 的集合类型层次,而不仅仅是简单复用现有代码。
系统调用抽象的失效
第三重障碍是系统调用抽象的失效。std 的很大一部分价值在于它提供了一套跨平台的操作系统抽象。文件操作通过 std::fs 和 std::io,网络操作通过 std::net,时间获取通过 std::time,环境变量通过 std::env。这些 API 在底层依赖操作系统的系统调用,但在用户代码层面呈现为普通的 Rust 函数调用。
在 GPU 上,类似的系统调用不存在。GPU 没有自己的文件系统,没有网络栈,没有 wall-clock 时间概念,也没有环境变量。所有这些资源都由主机 CPU 拥有和管理。VectorWare 提出的解决方案是 hostcall 机制:GPU 代码调用 std API 时,如果该操作无法在设备上执行,就通过一种特殊的通信协议将请求发送给主机,主机执行实际的系统调用,然后将结果返回给 GPU。这类似于操作系统中的系统调用,但跨物理边界。
这种设计面临的首要挑战是 API 语义的匹配。Rust 的 std::fs::File::create 期望同步创建文件,如果文件已存在则默认失败或截断,这个语义可以通过 hostcall 直接映射到主机的 open(O_CREAT|O_TRUNC) 调用。但并非所有 API 都能如此直接地映射。std::time::SystemTime 需要获取当前时间,在支持设备定时器的 GPU 上(如 CUDA 的 %globaltimer)可以直接在设备端读取,但 wall-clock 时间通常只能在主机上获取。这意味着同一个 API 在不同平台上可能有不同的实现路径,对用户代码透明,但对标准库实现者是额外的复杂性。
另一个挑战是阻塞行为的处理。CPU 上的系统调用会阻塞调用线程,直到操作完成。在 GPU 上,如果 GPU 线程等待主机完成 I/O 操作,整个计算单元可能陷入空闲,浪费并行计算能力。理想的 hostcall 实现应当支持异步化:GPU 线程发起请求后立即继续执行后续指令,主机在后台处理请求,GPU 在需要结果时再同步。但 Rust 标准库的同步 API 设计假设调用线程可以安全阻塞,这需要重新审视。
渐进增强的工程策略
面对上述挑战,渐进增强是务实的设计原则。这意味着默认情况下,std 在 GPU 上保持最保守、最兼容的行为:所有可能需要主机的操作都通过 hostcall 实现,所有无法在设备上执行的操作都返回错误或执行降级逻辑。在此基础上,特定平台可以提供更高效的替代实现。
以时间 API 为例,VectorWare 的实现展示了这种策略。std::time::Instant 在支持设备计时器的平台上(如 CUDA)可以直接使用设备端的高精度计时器,实现真正的本地时间获取;而 std::time::SystemTime 因为需要 wall-clock 时间(依赖于 UTC 和时区信息),只能在主机端获取。这种选择在 API 层面是透明的:用户调用相同的方法名,但底层实现根据运行时能力选择最优路径。
类似地,对于文件系统,std::fs 的实现可以检测主机的实际能力。如果主机支持 GPUDirect Storage 这样的直接存储访问技术,文件 I/O 可以真正在设备上执行;如果不支持,则退回到 hostcall 模式。这种抽象边界的灵活性是 Rust std 长期成功的关键因素,现在需要将其扩展到异构计算领域。
libc facade 是另一种值得考虑的工程选择。当前 VectorWare 的实现复用了 libc 的接口层,因为 Rust 标准库的许多 API 在底层调用 libc。通过实现一个 GPU 兼容的 libc facade,可以最小化对 std 本身的修改,让大多数现有代码无需改动即可运行。这种策略降低了上游合并的难度,但也引入了额外的依赖层和潜在的安全隐患。长期来看,发展 Rust-native 的 GPU 运行时可能更符合 Rust 的设计哲学,但这需要更多的设计和实现工作。
工程实践建议与未来展望
对于当前希望在 GPU 上使用 Rust 标准库的开发者,有几点实践建议值得关注。首先是明确性能边界:hostcall 涉及 GPU 与 CPU 之间的通信开销,对于细粒度、低延迟的操作(如循环内的每次打印),开销可能超过计算收益。批量处理、异步化请求是缓解这一问题的关键。其次是错误处理的健壮性:hostcall 可能因为各种原因失败(资源耗满、权限不足、网络不可达),GPU 代码需要正确处理这些错误,而不像在某些嵌入式环境中直接 panic。
Rust 标准库向 GPU 扩展不是一个纯粹的技术问题,更是设计理念的碰撞。std 的成功在于它提供了一套一致、可靠、跨平台的抽象,将底层操作系统的差异隐藏在友好的 API 背后。GPU 代表了一种全新的计算范式,它有自己的资源模型、执行模型和性能特征。简单地复用 CPU 抽象可能无法充分发挥 GPU 的潜力,而设计全新的 GPU-native 库又可能割裂 Rust 生态系统的统一性。未来的标准库可能需要引入条件编译目标(target-specific features),在检测到 GPU 目标时提供不同的 API 集合,或者通过 trait 系统抽象不同设备的能力。
最终,Rust 标准库能否成功扩展到 GPU,取决于 Rust 社区能否在抽象的通用性与设备的具体性之间找到平衡点。VectorWare 的工作证明了这种可能性,而 Rust 编译器团队和标准库维护者的后续参与将决定这条路的实际走向。对于关注异构计算的 Rust 开发者而言,现在正是参与塑造这一领域的关键时期。
参考资料:
- VectorWare Blog, "Rust's standard library on the GPU", 2026 年 1 月
- Rust Internals Forum, "Naming GPU things in the Rust Compiler and Standard Library", 2025 年 12 月