TornadoVM 自建编译器应对 JVMCI 移除
据文章介绍,面对 JDK 27 移除 JVMCI,TornadoVM 8.0.0 用自研 TC 替代原有 Graal 编译路径,将 Java 字节码转换为 CUDA C++、OpenCL C 或 Metal。项目方称应用 API 保持兼容,原有通过的测试继续通过,部分负载吞吐持平或提升,但性能结果未经独立验证。
AI 深度解读
TornadoVM 原本借助 Graal,把 Java 计算内核转成 GPU 代码。按原文引用的迁移资料,JDK 27 移除 JVMCI 后,这条编译链路失去了依赖的接口。团队在 TornadoVM 8.0.0 中换上自研编译器 TC,同时保留应用侧 API。
值得读的不是「自研编译器更快」这个结论,而是他们如何收窄依赖、改善排错,并打开新的部署路径。性能数字有吸引力,但都来自项目方,兼容性数据也有需要保留的疑问。
一个 JVM 接口,为什么会卡住 GPU 框架
TornadoVM 让 Java 程序把适合并行处理的计算交给加速器。开发者使用 TaskGraph、TornadoExecutionPlan、KernelContext、@Parallel 和 @Reduce 等 API,底层编译器负责理解 Java 字节码,再生成设备驱动能够继续编译的 GPU 内核代码。
旧方案使用修改并迁移过包路径的 Graal,通过 JVM Compiler Interface,也就是 JVMCI 接入。问题在于,按团队的描述,这条流水线围绕生成 CPU 机器码设计,而 TornadoVM 需要输出交给 GPU 驱动编译的源代码,两者的目标存在差异。
这份依赖还带来了额外限制:内部 JDK 模块导出、较新的 class 文件版本读取受限,以及构建被固定在 --release 22。因此,JDK 27 移除 JVMCI 只是触发点,原有耦合已经在影响升级。
替换规模也说明这不是补一个适配器:团队报告涉及 1,218 个文件,新增约 40,166 行,删除约 119,699 行,清除了迁移后的 Graal 模块、随项目携带的 jdk.internal.vm.ci 和各后端的 Graal 包。
这次重构的价值,是让编译器围绕产品需要承担的任务重新划定边界。
不过,代码删得多只能说明结构变化大,不能单凭行数认定维护成本下降。
TC 改善的不只是代码生成
TC 用 Java 编写,面向 JDK 21 及以上版本,直接读取 class 文件,生成自己的带类型中间表示,再经过优化、TornadoVM 专用操作转换,最终输出 CUDA C++、OpenCL C 或 Metal 代码。
其中 SSA 中间表示,可以理解为让每个变量版本只赋值一次,方便编译器追踪数据关系。TC 实现了常量折叠、死代码消除、全局值编号、循环不变量外提和循环展开,再通过共用的类 C 输出结构处理不同目标的语法差异。
这些优化名称本身不新鲜。更有实际价值的是,TC 能在转换过程中保留 Java 变量名、源码行号和字节码位置。项目展示的 CUDA 输出带有注释,可以把生成操作对应回 Java 源码。
当 GPU 性能工具指出某条生成指令开销很高时,开发者便有线索追溯到原来的计算逻辑。编译失败时,团队称 TC 还会给出分类错误码,并标注 Java 行号和字节码索引,减少在外部编译器内部追查问题的难度。
TC 也支持独立生成模式:没有 GPU、没有完整 TornadoVM 运行时,仍然可以检查生成的内核。这适合放进 CI,提前发现部分编译问题。但「能生成代码」和「在目标设备上正确运行」是两道检查,后者仍需要真实硬件。
应用侧则尽量保持稳定。团队称版本 8 保留了公开 API 和内核 ABI,也就是生成内核与调用方之间的底层约定。对使用者而言,底层重构的成本不必全部传导到业务代码。
测试和启动数据,分别证明了什么
项目方公布的新旧编译器测试通过数如下:
| 后端与设备 | Graal | TC |
|---|---|---|
| CUDA,RTX 4090 | 1,516 | 1,539 |
| OpenCL,RTX 4090 | 1,027 | 1,057 |
| OpenCL,Intel UHD 770 | 932 | 934 |
| Metal,Apple M3 Pro | 942 | 941 |
团队同时声称,旧编译器通过的测试仍全部通过,TC 还能处理一些旧方案拒绝的情况。但 Metal 的通过数少了一个,而且该结果是在删除旧 Graal 代码之前测得,删除后尚未重跑。
这里不能把「旧测试全部通过」直接当成所有后端已经完成同等验证的证据。Metal 数字与这项声明如何对应,原文没有解释清楚。
应用测试使用 jitLLM,模型是 Llama 3.2 1B 和 Qwen3 0.6B。报告称生成的 token 完全一致,CUDA 吞吐为 Graal 的 1.06 至 1.14 倍,OpenCL 为 1.04 至 1.09 倍,Metal 差异处于测量噪声范围内。
这些结果支持「在所测负载上,替换没有明显牺牲性能」,还不足以推出所有 Java GPU 程序都会更快。设备、模型、精度和测量方式都会影响结果,token 一致也只覆盖这次测试。
另一个收益来自启动方式。团队称,摆脱 JVMCI 后,Project Leyden AOT 缓存和 GraalVM Native Image 的一项阻碍被移除。
在指定的 RTX 4090 环境里,一个编译并运行单个 CUDA 内核的小型 saxpy 程序,普通 JVM 从启动到退出约需 0.6 秒,使用 AOT 缓存或 Native Image 约需 0.3 秒。编译器自身的内核编译耗时,则从约 30 毫秒降到缓存模式的 11 毫秒,Native Image 下低于 1 毫秒。
启动耗时减半,不等于长期运行的服务吞吐翻倍。
这组数据更贴近命令行工具、测试和短生命周期任务。原文也明确指出,Native Image 暂不支持 cuDNN、cuDF、CUTLASS 等基于 JNI 的库,真实应用能否采用这条部署路径,还取决于依赖组合。
我的判断
我认可的是团队对依赖职责的重新审视。TornadoVM 需要的能力是「把 Java 内核编译成加速器代码」,承接一整套围绕 CPU 编译设计的基础设施,可能附带超出需求的约束。TC 把源码映射、错误诊断和独立生成纳入设计,也比单纯修复构建更有长期价值。
但这不能成为一般团队重写底层依赖的理由。适配器、替换依赖、暂时固定兼容版本,都可能是成本更低的选择。自研编译器意味着长期承担语义兼容、优化正确性和多个设备后端的维护,一次发布成功并没有消除这些责任。
原文提供了新旧编译器的项目方对照数据,没有独立复测,也没有完整说明测试覆盖范围和长期维护投入。因此,性能结果可以作为评估起点;「重写比继续适配更划算」仍是带有团队条件的工程判断,不能替它背书。
对前端和全栈读者,最可迁移的部分是依赖治理:升级受阻时,先辨认自己究竟消费了什么能力,再判断是否值得拥有这一层实现。
能拿来做什么
- 遇到 Node 或构建工具升级卡住依赖时,先写清所需能力、依赖的内部接口和当前限制,再比较适配、替换与重写的成本,避免只围绕报错打补丁。
- 替换编译、转译或代码生成链路时,先保住公开 API 与调用约定,并用已有通过用例设置兼容门槛;同时检查生成产物能否追溯到源码位置。
- 评估 TornadoVM 8.0.0 时,挑一个业务真实需要的内核,覆盖计划部署的 JDK 和加速器后端,先比正确性,再分别测预热后的吞吐与冷启动,并记录设备、驱动、JDK 构建和编译设置。
- 尝试 AOT 缓存或 Native Image 前,核对实际 JNI 依赖,保留可回退的启动方式;不要把小型 saxpy 程序的收益直接用于估算整个服务。