A JDK upgrade can turn an implementation detail into a product-level problem.
For TornadoVM, the detail was JVMCI, the JVM Compiler Interface. The project used a relocated copy of Graal through that interface to translate Java kernels into GPU code. When JDK 27 removed JVMCI, TornadoVM had a choice: keep building around an integration that no longer existed in the new JDK, or take ownership of the compilation path. Oracle's JDK 27 migration guide documents the removal. [2]
With TornadoVM 8.0.0, its maintainers chose the second route. They replaced the bundled Graal compiler with TC, a Java bytecode-to-kernel compiler that emits CUDA C++, OpenCL C, or Metal code. Their release report says the new path preserves application APIs, passes the previous compiler's passing tests, and reaches comparable or higher throughput in selected workloads. Those are project-reported results, not an independent benchmark, but the architectural change is real and worth studying. [1]
The interesting story is not simply that a GPU framework wrote a compiler. It is that the team used a breaking platform boundary to simplify its own system, improve diagnostics, and make more deployment options possible.
The dependency was doing more than compiling
TornadoVM lets Java programs offload suitable computation to accelerators. Its users express work using APIs such as TaskGraph, TornadoExecutionPlan, KernelContext, @Parallel, and @Reduce. Under those APIs, a compiler must understand Java bytecode, map supported operations to accelerator execution, and produce code that a device driver can compile.
Previously, TornadoVM shipped a modified and relocated copy of Graal for this job. Graal is an optimizing compiler, but the TornadoVM team says its pipeline was designed to generate CPU machine code, not source kernels for a GPU driver. It also created a close dependency on JVMCI and internal JDK module exports. The project says this prevented it from reading newer class-file versions and kept its builds pinned to --release 22.
JDK 27's official migration guide lists the removal of JVMCI among that release's removed features. That's a narrow change in the JDK, but it had a large effect on software whose internals were built around the interface. [2]
The scale of the replacement is striking. In its release account, the TornadoVM team says the compiler replacement touched 1,218 files, adding about 40,166 lines and deleting about 119,699. It removed the relocated Graal module, vendored jdk.internal.vm.ci, and backend-specific Graal packages. This was not a small adapter written to make the next JDK compile. It was a redesign of a critical subsystem. [1]
TC narrows the compiler to the job
TC is a compiler written in Java, targeting JDK 21 and later. It reads class files directly, builds its own typed intermediate representation, performs optimization passes, lowers TornadoVM-specific operations, and prints code in a shared C-like form with target-specific dialects for CUDA, OpenCL, and Metal.
That design matters because the output is source code for a device-driver compiler. The team describes TC as having a front end, an SSA-based intermediate representation, optimization passes such as constant folding, dead-code elimination, global value numbering, loop-invariant code motion, and loop unrolling, followed by TornadoVM lowering and structured source emission.
The key distinction is not that TC has more compiler features than Graal. It is that TC can represent and expose the transformations this use case needs. The compiler can preserve Java variable names, source lines, and bytecode positions as it translates a kernel. The generated CUDA shown in the project article carries comments mapping emitted operations back to the Java source line.
That is practical debugging value. A GPU performance tool can identify a costly generated instruction, and the source annotation helps a developer trace it back to the Java kernel. When TC cannot compile a method, the project says it emits categorized error codes and points to the Java line and bytecode index. Compare that with diagnosing a failure deep inside an imported compiler pipeline: the new approach makes the boundary between application code and generated code more visible.
TC also runs without a GPU or the full TornadoVM runtime for its standalone emission mode. That allows a developer or CI job to inspect a generated kernel without having the target accelerator installed. It does not eliminate the need to test on actual hardware. It does make some compiler feedback easier to obtain and reproduce.
What the reported results say, and what they do not
A compiler replacement is only credible if it preserves behavior. The TornadoVM team's release article reports these test comparisons:
- CUDA on an RTX 4090: 1,516 tests passed with Graal and 1,539 with TC.
- OpenCL on an RTX 4090: 1,027 with Graal and 1,057 with TC.
- OpenCL on Intel UHD 770: 932 with Graal and 934 with TC.
- Metal on an Apple M3 Pro: 942 with Graal and 941 with TC. The team notes this Metal result was measured before the old Graal code was deleted and has not been rerun after removal.
The team says every test that passed before still passes, while TC handles some cases that the former compiler rejected. The Metal caveat should stay attached to the number. It would be misleading to present every backend as equally revalidated after the replacement landed.
For an application workload, the report uses jitLLM with Llama 3.2 1B and Qwen3 0.6B. It says the generated output was token-identical, with throughput between 1.06 and 1.14 times Graal on CUDA, and 1.04 to 1.09 times on OpenCL. On Metal, the two were within measurement noise. These are useful project measurements, but they are not a broad claim that all Java GPU workloads become faster. Results depend on the workload, device, model, precision, and measurement setup.
The honest takeaway is narrower: the replacement appears to have met its compatibility and selected-performance targets in the workloads and test matrix the project published. Teams evaluating it should look at the raw specification, reproduce their own kernels, and test every target backend they plan to support.
The surprising benefit is deployment choice
Removing JVMCI did not only restore compatibility with a newer JDK. According to the project, it also removed a blocker for Project Leyden AOT caches and GraalVM Native Image.
In a companion report, the team measured a small saxpy program that compiles and runs one CUDA kernel. It took about 0.6 seconds from launch to exit on a regular JVM, compared with about 0.3 seconds using an AOT cache or a Native Image in the stated RTX 4090 setup. The compiler's own kernel compilation reportedly dropped from about 30 milliseconds to 11 milliseconds with the cache, and to under 1 millisecond in the Native Image. The project notes that JNI-based libraries such as cuDNN, cuDF, and CUTLASS are not supported in Native Image yet. [3]
These figures are startup measurements for a specific small program, not a promise of doubled throughput for a long-running service. The project itself says startup optimizations matter most for command-line tools, tests, and short-lived workloads; long-running services gain less once warm. But the additional deployment paths are meaningful. An application may be able to choose a conventional JVM, a cached startup path, or a native executable according to its runtime environment.
This is a useful systems lesson: architectural boundaries can constrain features beyond the one they were introduced to support. A dependency that ties compilation to an internal JVM interface may also complicate startup optimization, native packaging, build flags, and future JDK compatibility. Replacing it changes more than the compiler.
What Java teams can borrow from this change
Most teams do not need to write a compiler after a library dependency breaks. The transferable idea is to use a compatibility crisis to inspect the shape of the dependency, rather than automatically rebuilding the same coupling around a replacement.
When an internal platform boundary changes, ask:
- What capability do we actually consume? Name the narrow function, not just the library. TornadoVM needed to translate Java kernels into accelerator code, not to carry all of Graal's CPU JIT architecture.
- Which constraints come from the dependency itself? The team points to class-file limits, internal module exports, backend code duplication, and poor visibility into intermediate code.
- Can we preserve the public contract while replacing the implementation? TornadoVM says its application-facing APIs and kernel ABI remain stable in version 8.
- What is the cost of owning the new layer? A compiler means ongoing semantic compatibility, optimization work, target-specific behavior, and a larger testing burden. One successful release does not make that maintenance disappear.
- What evidence would make the migration safe? Define a test-parity gate, representative application benchmarks, explicit platform matrices, and a visible caveat list before declaring success.
- Can the new design provide a second benefit? Here, the project links independence from JVMCI with AOT caches and Native Image support. A redesign can pay for itself through capabilities that the immediate compatibility patch would not provide.
This is not an argument to reject established dependencies. Graal served TornadoVM for years, and rewriting compiler infrastructure is an expensive choice. The lesson is to treat the break as an opportunity to evaluate the architecture, not as automatic proof that a full rewrite is justified.
What I would verify before adopting it
I have not run TornadoVM 8.0.0 or reproduced its benchmarks. This article is based on the project's release materials and Oracle's JDK migration documentation, so treat the performance numbers as vendor-reported until independently reproduced.
If you work with Java GPU computing, a practical evaluation can stay small:
- Select a kernel your application actually needs, not only a sample vector operation.
- Run it on every accelerator backend and JDK version you intend to deploy.
- Compare output correctness first, then warm throughput and cold-start behavior separately.
- Inspect the emitted code and error messages for a kernel you understand.
- Test any Native Image or AOT configuration against your real libraries, especially if JNI is involved.
- Record the device, driver, JDK build, compiler settings, and workload shape so a later comparison is meaningful.
- Keep a rollback path until your test suite covers the kernels and launch configurations that matter to you.
That checklist is more useful than treating the release's strongest benchmark as a general verdict. The project has made a substantial engineering claim; a user should validate it against their own compute path.
A compiler rewrite can be a platform strategy
TornadoVM 8 is a good example of a team turning a JDK removal into a chance to own the piece of infrastructure closest to its product. The reported outcome is more than “it builds on Java 27”: the project says it now has a compiler designed for GPU kernel emission, clearer diagnostics, less dependence on internal JDK interfaces, and new startup options.
The cost is also real. TC is now a compiler the project must evolve and support. The published figures are promising, but they come from the project itself and include a backend caveat. That combination calls for interest, not blind adoption.
For Java teams, the broader question is worth keeping: when a platform removes a hook your system depends on, do you need a smaller adapter, a new dependency, or a narrower subsystem you can own? The answer depends on what the dependency was doing and what capabilities the current architecture prevents.
I write about Java, Spring Boot, and AI every week. Subscribe if you want more engineering breakdowns like this.
Sources
- [1] TornadoVM v8.0.0: Meet TC, TornadoVM's Own Compiler
- [2] Oracle JDK 27 Migration Guide: Removed Tools and Components
- [3] Fast Start-up for GPU Java: Project Leyden and GraalVM Native Image
Have you had a platform change force a rethink of a core dependency? What did you replace, and what did you keep?
Top comments (0)