Rust 语言的生态系统长期以来依赖分层设计来适应不同运行环境。core 定义语言基础,不依赖堆也不依赖操作系统;alloc 在此基础上增加堆分配能力;std 则位于最顶层,提供文件、网络、线程、进程等操作系统相关抽象。由于 Rust 将 no_std 作为语言特性,代码可以通过 #![no_std] 注解选择性地只使用 core 和 alloc,这使得 Rust 得以应用于嵌入式、固件、驱动程序等缺乏传统操作系统的领域。传统 GPU 编程正是处于这一约束之下:当使用 rust-gpu 或 rust-cuda 将 Rust 代码编译到 GPU 时,必须启用 #![no_std],因为 GPU 本身不运行操作系统,自然无法直接支持 std 提供的系统调用接口。
然而,这一设计选择带来的代价同样显著。Rust 的大部分高效且符合人体工学的抽象都存在于标准库中,且 crates.io 上的绝大多数开源库都假设 std 可用。放弃 std 意味着无法直接使用这些成熟的抽象层,开发者必须为 GPU 代码重新实现或寻找替代方案。随着机器学习和人工智能工作负载对存储和网络访问的速度要求日益严苛,现代 GPU 技术如 NVIDIA 的 GPUDirect Storage、GPUDirect RDMA 以及 ConnectX 网络适配器,正在使 GPU 与磁盘、网络的直接交互成为可能。在消费级硬件上,NVIDIA DGX Spark 和苹果 M 系列设备也展现了类似的架构融合趋势。这种硬件能力的演进使得重新审视「GPU 是否应该拥有完整标准库支持」这一问题变得愈发迫切。
hostcall 机制:GPU 到主机的系统调用模拟
VectorWare 实现的核心创新在于其自定义的 hostcall 框架。这一机制将 GPU 代码对 std API 的调用转化为对主机 CPU 的结构化请求,本质上扮演着类似操作系统系统调用的角色。可以将其理解为从 GPU 发往主机的远程过程调用(RPC),GPU 无法自行执行的操作(如文件系统访问、网络通信、获取墙钟时间)通过 hostcall 委托给主机完成。对于终端用户而言,Rust 的 std API 行为与在 CPU 上完全一致;不同之处仅在于底层实现 —— 某些 std 调用被重新实现为 hostcall,而非直接操作系统内核。
为了最大限度地减少对 Rust 现有 std 库代码的修改,VectorWare 采用了一种 libc 风格的门面(facade)模式。Rust 的 std 库在 Unix 系统上依赖 libc 与操作系统交互,因此只要在 GPU 环境下提供与 libc 函数签名兼容的 hostcall 实现,std 库的其余部分基本无需改动。例如,std::fs::File::open 方法被重新实现为向主机发送一个 open hostcall,主机接收到请求后使用自身的文件系统 API 完成实际的打开操作,再将结果返回给 GPU。这种设计不仅保留了源代码兼容性,还使得大量现有的依赖 std 的第三方库能够直接在 GPU 上运行,极大地扩展了可复用的 Rust 生态系统。
设备与主机的渐进式功能分派
「hostcall」这一名称在技术上略有误导性,因为 hostcall 请求的最终执行位置并非只能限于主机。VectorWare 的设计允许根据底层硬件和平台能力动态选择请求的履行位置,这种策略称为渐进式增强(progressive enhancement)。以时间相关 API 为例:std::time::Instant 在支持设备计时器的平台上(如 CUDA 的 %globaltimer 特殊寄存器)可以直接在 GPU 上实现,获取本地计时器值而无需任何跨设备通信;相比之下,std::time::SystemTime 由于缺乏墙钟时间的设备级支持,仍需通过 hostcall 委托给主机。这种区分使得同一套 API 能够在不同硬件配置下以最优路径执行,既不牺牲功能覆盖,也不在支持设备级能力的平台上引入不必要的延迟。
这种可选择性还为更高级的功能优化提供了空间。例如,GPU 可以在本地缓存 hostcall 的返回结果以避免重复的主机往返;对于文件系统操作,GPU 甚至可以维护自己的虚拟文件系统视图,仅在必要时与主机同步。用户代码完全无需感知这些底层细节 —— 向 /gpu/tmp 写入文件可以指示文件应保留在设备端,向 localdevice:42 发起网络通信可以定位到特定的工作组或线程。这种语义扩展在不破坏 API 兼容性的前提下,为异构编程模型引入了新的表达能力。
实现细节与正确性保障
hostcall 机制的实现虽然概念简单,但工程实现中需要处理大量确保正确性和性能的细节。VectorWare 采用标准 GPU 编程技术,包括双缓冲(double-buffering)和原子操作,以避免数据撕裂(data tearing)并确保内存一致性(memory ordering)。协议本身被刻意设计得足够精简,以保持 GPU 侧逻辑的简洁;同时支持结果打包(packing)以避免在 GPU 堆上分配内存。为了防止阻塞 GPU,hostcall 的发送充分利用 CUDA 流(streams)等 API,使主机端的请求处理与 GPU 的并行执行解耦。
作为 Rust 生态的一部分,VectorWare 将安全性和正确性置于首要位置。他们甚至将 hostcall 运行时和内核代码在 Miri(Rust 的 MIR 解释器)下运行,使用 CPU 线程模拟 GPU 执行环境,以验证代码的内存安全性。这种严格的验证方法在 GPU 计算领域相当罕见,反映出 VectorWare 团队作为 Rust 编译器团队成员的专业素养。当前实现针对 Linux 主机和 NVIDIA GPU,但协议本身是厂商无关的,未来可以通过 HIP 支持 AMD GPU,或通过 rust-gpu 配合 Vulkan 扩展支持其他厂商的硬件。
抽象边界的长期考量
虽然当前实现通过 libc 门面最小化了对 Rust 现有 std 代码的修改,但这种设计是否是长期最优解仍是一个开放问题。libc 门面意味着 GPU 环境本质上是在模拟一个类 Unix 系统,这在概念上自然但也引入了一层间接开销。替代方案是让 std 本身感知 GPU 异构环境,通过 Rust 原生的 API 而非模拟 libc 来实现 GPU 感知的功能。这一方向需要在 std 侧进行更多修改,但理论上可能带来更好的安全性和效率 —— 直接使用 Rust 的类型系统和所有权模型而非 C 风格的接口,可以更好地与 Rust 的借用检查器和生命周期机制集成。
无论如何,这种将操作系统抽象扩展到异构系统的尝试,代表了 GPU 编程模型演进的重要方向。VectorWare 的工作与其说是在发明新的 GPU 运行时,不如说是在将 Rust 现有的抽象边界扩展到能够跨越异构设备的范围。GPU 正在从被动的加速器演变为能够运行复杂应用的计算节点,而 Rust 的标准库正在成为连接这些计算节点的统一抽象层。随着硬件能力的持续演进和软件标准的逐步成熟,这种「将 GPU 带入 Rust」而非仅仅「将 Rust 带入 GPU」的范式,或许会成为下一代异构计算的基石。
参考资料