
Compile-time automatic differentiation with no runtime tape, typed named-axis tensors and bounded dims, StableHLO model export, batching/vmap, double precision, training primitives, Hugging Face/LoRA import.
Compile-time automatic differentiation and GPU serving for Kotlin
Transformations | Training | Serving | Install | Examples | Changelog | Docs
Tlaloc is a Kotlin library for differentiable array programs. A K2 compiler
plugin turns grad { } into gradient code at compile time. Tensors carry their
rank, dtype and axis names in the Kotlin type. Programs lower to
StableHLO and run on NVIDIA GPUs through
PJRT or IREE, on
Apple-silicon GPUs through MetalHLO
(below), on the Cerebras Wafer-Scale Engine
simulator (work in progress, below),
or on the CPU through a reference interpreter.
import io.tlaloc.autograd.grad
import io.tlaloc.core.*
import io.tlaloc.core.ops.*
fun main() {
// Rewritten at compile time: d/dA sum(A·A) = 1·Aᵀ + Aᵀ·1
val g = grad { a: DTensor<Rank2<Sym, Sym>, F32> -> (a matmul a).sum().toFloat() }
val a = Tensors.f32Matrix<Sym, Sym>(2, 2, floatArrayOf(1f, 2f, 3f, 4f))
println(g(a).hostF32().toList()) // [7.0, 11.0, 9.0, 13.0]
}grad { f } returns a plain Kotlin function. There is no tape and no runtime
tracing. A model compiled for serving is a directory of StableHLO plus weights;
serving it needs a PJRT plugin and a GPU driver, not a JVM.
This is an alpha, 0.1.0-alpha02. APIs can change between alphas without a
deprecation cycle (COMPATIBILITY.md).
Each transformation takes a lambda and returns a function. The compiler plugin builds the derivative when your code compiles.
val df = grad { x: Float -> x * x * x }
df(2f) // 12.0
val dfdxy = grad2 { x: Float, y: Float -> x * y + x }grad2 and grad3 take two and three arguments. valueAndGrad returns the
value too.
val f = jvp { x: Float -> x * x * x }
f(2f, 1f) // 12.0, the directional derivative
val j = jacobian { x: DTensor<Rank1<Sym>, F32> -> x * x } // diag(2x)
val h = hessian { x: DTensor<Rank1<Sym>, F32> -> (x * x).sum().toFloat() } // 2·Ivjp and jacobianReverse are also available, and the transformations nest.
// one gradient of w1 per example; w1 and w2 are shared by every example
val g1 = vmap2(batchAxis(Batch), Batched, Batched) { x: X, y: Y ->
grad { w: W1 -> crossEntropyLoss((x matmul w).tanh() matmul w2, y).toFloat() }(w1)
}
g1(xs, ys) // DTensor<Rank3<Named<Batch, Sym>, Named<Feature, Sym>, Named<Hidden, Sym>>, F32>vmap writes the batched function at compile time. The batch axis is a named axis in
the type, so a batch of another axis does not compile. grad { vmap { } }
differentiates a batched loss; vmap2 shares or batches each of two arguments.
examples/per-example-gradients computes
per-example gradient norms. → design/vmap.md
The same programs run in F64, in double precision end to end: literals, captured
Doubles and inputs are never rounded to F32, and F32 and F64 never mix implicitly
(mixing them is a compile error at the call).
val g = grad { k: DTensor<Rank2<Sym, Sym>, F64> -> k.logDetSpd().toDouble() } // K⁻¹
val h = hessian { k: DTensor<Rank2<Sym, Sym>, F64> -> k.logDetSpd().toDouble() } // matches JAX x64 to 1e-11examples/gaussian-process fits a Gaussian process in F64.
if, when and for loops inside the lambda are differentiated.
examples/differentiable-physics
differentiates a 38-step Euler integrator. customVjp, customJvp and
customVjpJvp supply your own derivative for a function.
The lambda can use values declared outside it. Compile-time constants are folded in; other values become parameters of the generated function. A lambda the plugin cannot differentiate is a compile error that names the reason.
tlaloc {
dumpGradSource.set(true) // print it
dumpGradSourceDir.set(layout.buildDirectory.dir("grads")) // one .kt per lambda
}The plugin prints the derivative as Kotlin source. That source compiles without
the plugin and returns the same bits as the compiled gradient.
examples/readable-gradients checks this at 7
points. → READABLE_REVERSE.md
val activations: DTensor<Rank2<Named<Batch, Sym>, Named<SeqLen, Sym>>, F32> = ...
val weights: DTensor<Rank2<Named<SeqLen, Sym>, Named<Hidden, Sym>>, F32> = ...
val hidden = activations contract weights // compiles: both have SeqLenWith a Hidden × Hidden weight the call does not compile. Shape and
differentiability errors are reported at the call's file, line and column.
→ examples/named-indices
An axis can carry an upper bound (experimental):
object MaxSeq : DimBound(4096)
val x = Tensors.f32Zeros<Named<SeqLen, Bounded<MaxSeq>>, Sym>(5000, 64) // compile error: 5000 > 4096A program over bounded axes runs at any size up to the bound, and exports as one program per bucket, so a server or a training loop compiles once per bucket instead of once per length. → design/bounded-dims.md
| dtypes | F32, F64, I32, BF16 (I8 for quantized serving weights); grad {} differentiates F32 and F64 |
| Ops | elementwise, broadcasting, reductions, shape ops, matmul, conv2d, pooling, softmax, embedding, losses, batch norm |
| Special functions |
lgamma, digamma, polygamma, integral
|
| Linear algebra |
cholesky, triangularSolve, solveSpd, logDetSpd, invSpd, solve, det, qrQ/qrR, eighValues/eighVectors; rank 2, differentiable to any order |
| Random numbers | stateless threefry-2x32, bit-exact with JAX |
| Sparse | rank-2 CSR, sparse × dense matmul under grad (CPU only) |
:nn has immutable layers and pure optimizers: a training step returns a new
model and a new optimizer state.
val keys = RandomKey.fromSeed(7).split(2)
val model0 = Sequential(Dense(2, 16, keys[0]), ReluLayer, Dense(16, 1, keys[1]))
val step = capture(model0, listOf(x), name = "mlp") { prediction ->
val residual = prediction - prediction.constant<Shape>(targets, intArrayOf(n, 1))
(residual * residual).mean()
}
val optimizer = Adam(learningRate = 0.02f)
var model = model0
var state = optimizer.initialState()
repeat(60) {
val out = step.run(model, listOf(x)) // loss and gradients
val (nextModel, nextState) = optimizer.step(model, out.gradients, state)
model = nextModel; state = nextState
}
saveCheckpoint(path, model, optimizer, state) // one safetensors file| Layers | Dense, Conv2d, MaxPool, AvgPool, BatchNorm, Dropout, Embedding, EmbeddingBag, GRU, Flatten |
| Transformers | LayerNorm, RMSNorm, RoPE, MultiHeadAttention (causal, grouped-query), SwiGLU, TransformerBlock, CausalLM; gradients match PyTorch to 2.4e-6 |
| Optimizers | SGD, Momentum, RMSprop, Adam, AdamW |
| Also | learning-rate schedules, gradient clipping, bf16 mixed precision, checkpoints that resume bit for bit |
| On the GPU |
examples/gpu-training: 600 Adam steps in 2.02 s on a GB10, 98.0 % held-out accuracy; examples/mnist: 93.66 % on MNIST |
| Hugging Face models |
HfCausalLm reads a Llama or Qwen3 checkpoint into a CausalLM and writes one back; examples/fine-tune fine-tunes Qwen3-0.6B on the GPU |
| LoRA |
Lora.apply adds adapters to the layers PEFT's target_modules names select, capture(..., Lora.frozen) trains them with the base frozen, HfLoraAdapter reads and writes PEFT's adapter files, Lora.merge folds them in for serving; examples/lora-finetune trains Qwen3-0.6B on the GPU at about 0.33 s a step |
| Tokenizers |
HfTokenizer (:tokenizer) reads a checkpoint's tokenizer.json and gives the ids, decoded text and chat prompts transformers gives, for Qwen3, Muse Glimmer, TinyLlama, GPT-2 and Gemma 4 |
Kotlin reads a Hugging Face checkpoint and writes a serving artifact: StableHLO prefill and decode programs, the weights as safetensors, and a manifest. Three runtimes load it without a JVM, and one inside the JVM. All four compile it with a PJRT plugin.
| Runtime | What it is |
|---|---|
| NVIDIA Triton |
libtriton_tlaloc.so, a Triton backend. HTTP and gRPC, Triton's sequence batcher, KV cache held by the backend. → triton/
|
| vLLM | a vLLM platform plugin; LLM.generate() runs a Tlaloc artifact |
| Python | a small runtime that calls the PJRT C API through ctypes, with no JAX, PyTorch or NumPy |
| The JVM itself |
ServingModel (:runtime-pjrt) loads the artifact in the calling JVM, with plain-Java signatures: examples/java-inference serves a fine-tuned Qwen3-0.6B from Java at about 15 ms a token, examples/spark-inference inside a Spark map function |
Models. Each one generates the same greedy token ids as Hugging Face transformers.
| Model | Weights | Decode step (GB10, Triton) |
|---|---|---|
| TinyLlama-1.1B | f32 | 21.7 ms |
| Qwen3-0.6B | bf16 | 10.7 ms |
| Muse Glimmer 30B, text only | bf16, 53 GB | 244 ms |
| Muse Glimmer 30B, text only | int8 (opt-in), 29 GB | 140 ms |
Muse Glimmer runs contexts up to 32,768 tokens and several sequences at once. A 31,744-token prompt prefills in 62 s. Int8 weights keep its fixture answers and change perplexity from 5.610 to 5.602.
Serving features: paged attention; sliding-window layers whose KV cache is bounded by the window; chunked and batched prefill; KV cache updated in place; continuous batching of decode steps; CUDA shared-memory inputs and outputs; dynamic batching for stateless models.
examples/triton-llm exports Qwen3-0.6B, starts Triton
and streams a chat answer in one script.
Programs with bounded axes (experimental) export as one StableHLO program per bucket and a manifest. The Python runtime and Triton pad each request to the smallest bucket that holds it: 200 requests of lengths 1..512 compile 6 programs instead of 163. → SERVING_ARCHITECTURE.md
| Linux aarch64 | Linux x86_64 | macOS arm64 | Windows | |
|---|---|---|---|---|
| Build, library, CPU interpreter | ✅ | ✅ | ✅ | not tested |
| NVIDIA GPU (PJRT) | ✅ | expected, not tested | n/a | not tested |
| Triton serving | ✅ | expected, not tested | n/a | n/a |
| Google TPU | written, never run | written, never run | n/a | n/a |
| Apple GPU (PJRT via MetalHLO) | n/a | n/a | ✅ (f64 runs as f32) | n/a |
| Cerebras WSE simulator (PJRT via CerebrasStableHLO) | n/a | n/a | work in progress (f64 runs as f32) | n/a |
NVIDIA results come from one machine, an NVIDIA GB10 (Blackwell, aarch64); Metal results from an Apple M-series Mac. CI builds and tests the library on the three operating systems in the table, with no GPU. → CAPABILITIES.md lists every capability and the test that certifies it.
Tlaloc runs on the GPU of an Apple-silicon Mac through
MetalHLO's PJRT plugin: the same
StableHLO programs, the same PjrtSession API, PjrtTarget.Metal as the target.
The whole runtime-pjrt device suite — training steps, linear algebra, vmap,
RNG, paged attention, MoE, FP8 KV caches, Qwen greedy decoding — passes on Metal
(142 passed, 0 failed).
git clone https://github.com/pedronahum/MetalHLO.git # next to your tlaloc checkout
cd MetalHLO && swift build -c release --product PJRTMetalHLOPjrtSession(plugin = PjrtBinaries.requireMetalPlugin("app"), target = PjrtTarget.Metal).use { session ->
val out = session.runOn(fn, inputs)
}The plugin is found next to the checkout, or through METALHLO_PLUGIN_PATH
(GETTING_STARTED.md).
Run the device suite on Metal with
TLALOC_TEST_PJRT_TARGET=metal ./gradlew :runtime-pjrt:jvmTest.
Metal support is on main, not yet in 0.1.0-alpha02: build from source.
Metal has no double precision, so f64 programs compute in f32.
Tlaloc also runs on the Cerebras Wafer-Scale Engine (WSE), today on the
Cerebras SDK's simulator, through the PJRT plugin of
CerebrasStableHLO, with
PjrtTarget.Cerebras as the target. The plugin lowers each StableHLO program
to CSL through CerebrasHLO, its
compiler for the wafer, and compiles it with cslc.
Work in progress. CerebrasStableHLO and CerebrasHLO are not public yet; they will be made public soon. Until then the links above do not resolve.
Status of the runtime-pjrt device suite on the simulator: at the last full
run, 134 tests passed, 68 were skipped and 11 failed (Metal: 147 passed, 66
skipped). Two of the five failing classes, GatedDeltaOps and
BlockwisePagedAttention, have since been fixed by spreading a computation that
is too big for one processing element over a group of them. The other three
(MoeExperts, Qwen35MoeParity, CausalLmTraining) still fail with typed compile
errors about scale; no wrong result has been measured. The simulator is slow:
a test class takes minutes, and the biggest take hours.
# CerebrasStableHLO and CerebrasHLO checked out next to your tlaloc checkout,
# plus the Cerebras SDK in a Lima VM named cs_sdk
cd CerebrasStableHLO && cmake -G Ninja -B build \
-DMLIR_DIR=/opt/homebrew/opt/llvm/lib/cmake/mlir \
-DLLVM_DIR=/opt/homebrew/opt/llvm/lib/cmake/llvm && cmake --build buildPjrtSession(plugin = PjrtBinaries.requireCerebrasPlugin("app"), target = PjrtTarget.Cerebras).use { session ->
val out = session.runOn(fn, inputs)
}The plugin is found next to the checkout, or through
CEREBRASSTABLEHLO_PLUGIN_PATH
(GETTING_STARTED.md).
Set CEREBRASSTABLEHLO_BACKEND=simulator to run on the wafer simulator; without
it the plugin runs a host interpreter. Run one device test class with
CEREBRASSTABLEHLO_BACKEND=simulator TLALOC_TEST_PJRT_TARGET=cerebras ./gradlew :runtime-pjrt:jvmTest --tests <class>.
Cerebras support is on main, not in 0.1.0-alpha02. The WSE has no double
precision, so f64 programs compute in f32.
From Maven Central. The Gradle plugin is also on Maven Central, not on the
Gradle Plugin Portal, so pluginManagement needs mavenCentral().
// settings.gradle.kts
pluginManagement { repositories { mavenCentral(); gradlePluginPortal() } }
dependencyResolutionManagement { repositories { mavenCentral() } }// build.gradle.kts
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
kotlin("jvm") version "2.4.20"
id("io.github.pedronahum.tlaloc") version "0.1.0-alpha02" // makes `grad { }` compile-time
application
}
kotlin {
jvmToolchain(25) // to BUILD
compilerOptions { jvmTarget.set(JvmTarget.JVM_21) } // to RUN on JDK 21
}
java { // Java tasks match Kotlin's target
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
}
dependencies {
implementation(platform("io.github.pedronahum:tlaloc-bom:0.1.0-alpha02"))
implementation("io.github.pedronahum:tlaloc-autograd")
implementation("io.github.pedronahum:tlaloc-nn") // layers + optimizers
implementation("io.github.pedronahum:tlaloc-runtime-pjrt") // GPU execution; runs on JDK 25 only
}| Requirement | |
|---|---|
| Kotlin | 2.4.20 – 2.4.29; other versions are refused at compile time. On Kotlin 2.3.x, use 0.1.0-alpha01. |
| JDK | 25 to build code that uses grad { }. 21 to run it, except GPU execution, which needs 25. |
| GPU | NVIDIA: a driver and a PJRT CUDA plugin, e.g. pip install "jax[cuda12]" in a virtual environment (GETTING_STARTED.md). Apple silicon: MetalHLO's PJRT plugin (Apple-silicon GPUs) |
| Cerebras (work in progress) | CerebrasStableHLO's PJRT plugin, CerebrasHLO, and the Cerebras SDK in a Lima VM (Cerebras) |
| Triton serving | Docker and nvcr.io/nvidia/tritonserver:25.11-py3 (triton/) |
To build from source, run ./gradlew publishToMavenLocal -x test in a checkout
and put mavenLocal() first in both repository blocks.
Walkthrough, plugin options, configuration and troubleshooting: GETTING_STARTED.md.
Standalone projects under examples/. Each has its own Gradle build
and skips by name when its hardware is missing.
| Needs | ||
|---|---|---|
quickstart/ |
grad {} over a matmul, and a shape error the compiler rejects |
JDK |
readable-gradients/ |
the derivative printed as Kotlin, recompiled, same bits | JDK |
differentiable-physics/ |
gradient descent through a physics simulation | JDK |
named-indices/ |
axis names in the tensor type | JDK |
gaussian-process/ |
GP hyperparameters fitted through a Cholesky solve and log-determinant | JDK |
per-example-gradients/ |
per-example gradient norms with vmap { grad { } }
|
JDK |
mnist/ |
MNIST to 93.66 % | CUDA |
gpu-training/ |
600 Adam steps on the GPU | CUDA |
fine-tune/ |
Qwen3-0.6B fine-tuned on the GPU with AdamW, saved as a Hugging Face checkpoint | CUDA |
lora-finetune/ |
LoRA on Qwen3-0.6B, the adapter saved in PEFT's format and merged for serving | CUDA |
java-inference/ |
the fine-tuned model served in-process from plain Java | CUDA |
spark-inference/ |
the same model inside a Spark map function | CUDA |
gpu-inference/ |
TinyLlama compiled from Kotlin, served by plain python3
|
CUDA |
triton-llm/ |
Qwen3-0.6B or Muse Glimmer on Triton, with a streaming chat client | CUDA, Docker |
| GETTING_STARTED.md | install, first gradient, GPU setup, configuration, troubleshooting |
| CAPABILITIES.md | every capability, its status, and the test behind it |
| SERVING_ARCHITECTURE.md | how serving works, and where the time goes |
| COMPATIBILITY.md · CHANGELOG.md | what may change, and what did |
| API reference | per module on javadoc.io; ./gradlew apiDocs builds all modules locally |
Design documents are in docs/.
your Kotlin K2 plugin :ir backends
─────────── ───────── ─── ────────
grad { f } ──────► reverse transform ──► DXIR ──► StableHLO ──► PJRT (CUDA, TPU*)
typed tensors on the lambda's IR │ + Shardy ──► IREE (CPU, CUDA)
named axes (also: Kotlin source) │ ──► host interpreter
└── recognize + coarsen (attention,
RMSNorm, RoPE, SwiGLU, …)
Loops are coarsened before differentiation, following Shen et al., Coarsening Optimization for Differentiable Programming (OOPSLA 2021).
./gradlew test # the suite
./gradlew test --rerun-tasks # re-run everything
bash scripts/onboarding-smoke.sh # publish, then build the README's install blocks
bash triton/verify.sh # Triton backend, end to end (GPU + Docker)Tests that need a GPU, Docker or a model download skip by name without it. Contributions: CONTRIBUTING.md. Security reports: SECURITY.md.
φ-calculus coarsening follows Shen, Zhang, Dea et al., Coarsening Optimization for Differentiable Programming, Proc. ACM Program. Lang. 5, OOPSLA, Article 130 (2021) — DOI 10.1145/3485507. The op surface and model layer take their parity target from facebookresearch/diffkt (MIT); the readable-derivative idea comes from google/tangent. Lowering targets OpenXLA StableHLO, Shardy and PJRT. Orchestration vendors Netflix Maestro.
Copyright 2026 Pedro N. Rodriguez. Licensed under Apache-2.0.
third-party/maestro/ vendors Netflix Maestro
(Apache-2.0); NOTICE credits it. Tlaloc does not publish it. The paper text in
docs/papers/ is CC-BY-4.0; see its README.
Symja (org.matheclipse:matheclipse-core), the optional CAS, is LGPL-3.0 by its
published POM, while its upstream repository's license.txt is GPL-3.0; upstream's
position is that the published Maven modules are LGPL. Tlaloc links it across the
SymbolicEngine interface and never modifies or redistributes it. It is
compileOnly, so it is not in your dependency graph unless you add it.
Compile-time automatic differentiation and GPU serving for Kotlin
Transformations | Training | Serving | Install | Examples | Changelog | Docs
Tlaloc is a Kotlin library for differentiable array programs. A K2 compiler
plugin turns grad { } into gradient code at compile time. Tensors carry their
rank, dtype and axis names in the Kotlin type. Programs lower to
StableHLO and run on NVIDIA GPUs through
PJRT or IREE, on
Apple-silicon GPUs through MetalHLO
(below), on the Cerebras Wafer-Scale Engine
simulator (work in progress, below),
or on the CPU through a reference interpreter.
import io.tlaloc.autograd.grad
import io.tlaloc.core.*
import io.tlaloc.core.ops.*
fun main() {
// Rewritten at compile time: d/dA sum(A·A) = 1·Aᵀ + Aᵀ·1
val g = grad { a: DTensor<Rank2<Sym, Sym>, F32> -> (a matmul a).sum().toFloat() }
val a = Tensors.f32Matrix<Sym, Sym>(2, 2, floatArrayOf(1f, 2f, 3f, 4f))
println(g(a).hostF32().toList()) // [7.0, 11.0, 9.0, 13.0]
}grad { f } returns a plain Kotlin function. There is no tape and no runtime
tracing. A model compiled for serving is a directory of StableHLO plus weights;
serving it needs a PJRT plugin and a GPU driver, not a JVM.
This is an alpha, 0.1.0-alpha02. APIs can change between alphas without a
deprecation cycle (COMPATIBILITY.md).
Each transformation takes a lambda and returns a function. The compiler plugin builds the derivative when your code compiles.
val df = grad { x: Float -> x * x * x }
df(2f) // 12.0
val dfdxy = grad2 { x: Float, y: Float -> x * y + x }grad2 and grad3 take two and three arguments. valueAndGrad returns the
value too.
val f = jvp { x: Float -> x * x * x }
f(2f, 1f) // 12.0, the directional derivative
val j = jacobian { x: DTensor<Rank1<Sym>, F32> -> x * x } // diag(2x)
val h = hessian { x: DTensor<Rank1<Sym>, F32> -> (x * x).sum().toFloat() } // 2·Ivjp and jacobianReverse are also available, and the transformations nest.
// one gradient of w1 per example; w1 and w2 are shared by every example
val g1 = vmap2(batchAxis(Batch), Batched, Batched) { x: X, y: Y ->
grad { w: W1 -> crossEntropyLoss((x matmul w).tanh() matmul w2, y).toFloat() }(w1)
}
g1(xs, ys) // DTensor<Rank3<Named<Batch, Sym>, Named<Feature, Sym>, Named<Hidden, Sym>>, F32>vmap writes the batched function at compile time. The batch axis is a named axis in
the type, so a batch of another axis does not compile. grad { vmap { } }
differentiates a batched loss; vmap2 shares or batches each of two arguments.
examples/per-example-gradients computes
per-example gradient norms. → design/vmap.md
The same programs run in F64, in double precision end to end: literals, captured
Doubles and inputs are never rounded to F32, and F32 and F64 never mix implicitly
(mixing them is a compile error at the call).
val g = grad { k: DTensor<Rank2<Sym, Sym>, F64> -> k.logDetSpd().toDouble() } // K⁻¹
val h = hessian { k: DTensor<Rank2<Sym, Sym>, F64> -> k.logDetSpd().toDouble() } // matches JAX x64 to 1e-11examples/gaussian-process fits a Gaussian process in F64.
if, when and for loops inside the lambda are differentiated.
examples/differentiable-physics
differentiates a 38-step Euler integrator. customVjp, customJvp and
customVjpJvp supply your own derivative for a function.
The lambda can use values declared outside it. Compile-time constants are folded in; other values become parameters of the generated function. A lambda the plugin cannot differentiate is a compile error that names the reason.
tlaloc {
dumpGradSource.set(true) // print it
dumpGradSourceDir.set(layout.buildDirectory.dir("grads")) // one .kt per lambda
}The plugin prints the derivative as Kotlin source. That source compiles without
the plugin and returns the same bits as the compiled gradient.
examples/readable-gradients checks this at 7
points. → READABLE_REVERSE.md
val activations: DTensor<Rank2<Named<Batch, Sym>, Named<SeqLen, Sym>>, F32> = ...
val weights: DTensor<Rank2<Named<SeqLen, Sym>, Named<Hidden, Sym>>, F32> = ...
val hidden = activations contract weights // compiles: both have SeqLenWith a Hidden × Hidden weight the call does not compile. Shape and
differentiability errors are reported at the call's file, line and column.
→ examples/named-indices
An axis can carry an upper bound (experimental):
object MaxSeq : DimBound(4096)
val x = Tensors.f32Zeros<Named<SeqLen, Bounded<MaxSeq>>, Sym>(5000, 64) // compile error: 5000 > 4096A program over bounded axes runs at any size up to the bound, and exports as one program per bucket, so a server or a training loop compiles once per bucket instead of once per length. → design/bounded-dims.md
| dtypes | F32, F64, I32, BF16 (I8 for quantized serving weights); grad {} differentiates F32 and F64 |
| Ops | elementwise, broadcasting, reductions, shape ops, matmul, conv2d, pooling, softmax, embedding, losses, batch norm |
| Special functions |
lgamma, digamma, polygamma, integral
|
| Linear algebra |
cholesky, triangularSolve, solveSpd, logDetSpd, invSpd, solve, det, qrQ/qrR, eighValues/eighVectors; rank 2, differentiable to any order |
| Random numbers | stateless threefry-2x32, bit-exact with JAX |
| Sparse | rank-2 CSR, sparse × dense matmul under grad (CPU only) |
:nn has immutable layers and pure optimizers: a training step returns a new
model and a new optimizer state.
val keys = RandomKey.fromSeed(7).split(2)
val model0 = Sequential(Dense(2, 16, keys[0]), ReluLayer, Dense(16, 1, keys[1]))
val step = capture(model0, listOf(x), name = "mlp") { prediction ->
val residual = prediction - prediction.constant<Shape>(targets, intArrayOf(n, 1))
(residual * residual).mean()
}
val optimizer = Adam(learningRate = 0.02f)
var model = model0
var state = optimizer.initialState()
repeat(60) {
val out = step.run(model, listOf(x)) // loss and gradients
val (nextModel, nextState) = optimizer.step(model, out.gradients, state)
model = nextModel; state = nextState
}
saveCheckpoint(path, model, optimizer, state) // one safetensors file| Layers | Dense, Conv2d, MaxPool, AvgPool, BatchNorm, Dropout, Embedding, EmbeddingBag, GRU, Flatten |
| Transformers | LayerNorm, RMSNorm, RoPE, MultiHeadAttention (causal, grouped-query), SwiGLU, TransformerBlock, CausalLM; gradients match PyTorch to 2.4e-6 |
| Optimizers | SGD, Momentum, RMSprop, Adam, AdamW |
| Also | learning-rate schedules, gradient clipping, bf16 mixed precision, checkpoints that resume bit for bit |
| On the GPU |
examples/gpu-training: 600 Adam steps in 2.02 s on a GB10, 98.0 % held-out accuracy; examples/mnist: 93.66 % on MNIST |
| Hugging Face models |
HfCausalLm reads a Llama or Qwen3 checkpoint into a CausalLM and writes one back; examples/fine-tune fine-tunes Qwen3-0.6B on the GPU |
| LoRA |
Lora.apply adds adapters to the layers PEFT's target_modules names select, capture(..., Lora.frozen) trains them with the base frozen, HfLoraAdapter reads and writes PEFT's adapter files, Lora.merge folds them in for serving; examples/lora-finetune trains Qwen3-0.6B on the GPU at about 0.33 s a step |
| Tokenizers |
HfTokenizer (:tokenizer) reads a checkpoint's tokenizer.json and gives the ids, decoded text and chat prompts transformers gives, for Qwen3, Muse Glimmer, TinyLlama, GPT-2 and Gemma 4 |
Kotlin reads a Hugging Face checkpoint and writes a serving artifact: StableHLO prefill and decode programs, the weights as safetensors, and a manifest. Three runtimes load it without a JVM, and one inside the JVM. All four compile it with a PJRT plugin.
| Runtime | What it is |
|---|---|
| NVIDIA Triton |
libtriton_tlaloc.so, a Triton backend. HTTP and gRPC, Triton's sequence batcher, KV cache held by the backend. → triton/
|
| vLLM | a vLLM platform plugin; LLM.generate() runs a Tlaloc artifact |
| Python | a small runtime that calls the PJRT C API through ctypes, with no JAX, PyTorch or NumPy |
| The JVM itself |
ServingModel (:runtime-pjrt) loads the artifact in the calling JVM, with plain-Java signatures: examples/java-inference serves a fine-tuned Qwen3-0.6B from Java at about 15 ms a token, examples/spark-inference inside a Spark map function |
Models. Each one generates the same greedy token ids as Hugging Face transformers.
| Model | Weights | Decode step (GB10, Triton) |
|---|---|---|
| TinyLlama-1.1B | f32 | 21.7 ms |
| Qwen3-0.6B | bf16 | 10.7 ms |
| Muse Glimmer 30B, text only | bf16, 53 GB | 244 ms |
| Muse Glimmer 30B, text only | int8 (opt-in), 29 GB | 140 ms |
Muse Glimmer runs contexts up to 32,768 tokens and several sequences at once. A 31,744-token prompt prefills in 62 s. Int8 weights keep its fixture answers and change perplexity from 5.610 to 5.602.
Serving features: paged attention; sliding-window layers whose KV cache is bounded by the window; chunked and batched prefill; KV cache updated in place; continuous batching of decode steps; CUDA shared-memory inputs and outputs; dynamic batching for stateless models.
examples/triton-llm exports Qwen3-0.6B, starts Triton
and streams a chat answer in one script.
Programs with bounded axes (experimental) export as one StableHLO program per bucket and a manifest. The Python runtime and Triton pad each request to the smallest bucket that holds it: 200 requests of lengths 1..512 compile 6 programs instead of 163. → SERVING_ARCHITECTURE.md
| Linux aarch64 | Linux x86_64 | macOS arm64 | Windows | |
|---|---|---|---|---|
| Build, library, CPU interpreter | ✅ | ✅ | ✅ | not tested |
| NVIDIA GPU (PJRT) | ✅ | expected, not tested | n/a | not tested |
| Triton serving | ✅ | expected, not tested | n/a | n/a |
| Google TPU | written, never run | written, never run | n/a | n/a |
| Apple GPU (PJRT via MetalHLO) | n/a | n/a | ✅ (f64 runs as f32) | n/a |
| Cerebras WSE simulator (PJRT via CerebrasStableHLO) | n/a | n/a | work in progress (f64 runs as f32) | n/a |
NVIDIA results come from one machine, an NVIDIA GB10 (Blackwell, aarch64); Metal results from an Apple M-series Mac. CI builds and tests the library on the three operating systems in the table, with no GPU. → CAPABILITIES.md lists every capability and the test that certifies it.
Tlaloc runs on the GPU of an Apple-silicon Mac through
MetalHLO's PJRT plugin: the same
StableHLO programs, the same PjrtSession API, PjrtTarget.Metal as the target.
The whole runtime-pjrt device suite — training steps, linear algebra, vmap,
RNG, paged attention, MoE, FP8 KV caches, Qwen greedy decoding — passes on Metal
(142 passed, 0 failed).
git clone https://github.com/pedronahum/MetalHLO.git # next to your tlaloc checkout
cd MetalHLO && swift build -c release --product PJRTMetalHLOPjrtSession(plugin = PjrtBinaries.requireMetalPlugin("app"), target = PjrtTarget.Metal).use { session ->
val out = session.runOn(fn, inputs)
}The plugin is found next to the checkout, or through METALHLO_PLUGIN_PATH
(GETTING_STARTED.md).
Run the device suite on Metal with
TLALOC_TEST_PJRT_TARGET=metal ./gradlew :runtime-pjrt:jvmTest.
Metal support is on main, not yet in 0.1.0-alpha02: build from source.
Metal has no double precision, so f64 programs compute in f32.
Tlaloc also runs on the Cerebras Wafer-Scale Engine (WSE), today on the
Cerebras SDK's simulator, through the PJRT plugin of
CerebrasStableHLO, with
PjrtTarget.Cerebras as the target. The plugin lowers each StableHLO program
to CSL through CerebrasHLO, its
compiler for the wafer, and compiles it with cslc.
Work in progress. CerebrasStableHLO and CerebrasHLO are not public yet; they will be made public soon. Until then the links above do not resolve.
Status of the runtime-pjrt device suite on the simulator: at the last full
run, 134 tests passed, 68 were skipped and 11 failed (Metal: 147 passed, 66
skipped). Two of the five failing classes, GatedDeltaOps and
BlockwisePagedAttention, have since been fixed by spreading a computation that
is too big for one processing element over a group of them. The other three
(MoeExperts, Qwen35MoeParity, CausalLmTraining) still fail with typed compile
errors about scale; no wrong result has been measured. The simulator is slow:
a test class takes minutes, and the biggest take hours.
# CerebrasStableHLO and CerebrasHLO checked out next to your tlaloc checkout,
# plus the Cerebras SDK in a Lima VM named cs_sdk
cd CerebrasStableHLO && cmake -G Ninja -B build \
-DMLIR_DIR=/opt/homebrew/opt/llvm/lib/cmake/mlir \
-DLLVM_DIR=/opt/homebrew/opt/llvm/lib/cmake/llvm && cmake --build buildPjrtSession(plugin = PjrtBinaries.requireCerebrasPlugin("app"), target = PjrtTarget.Cerebras).use { session ->
val out = session.runOn(fn, inputs)
}The plugin is found next to the checkout, or through
CEREBRASSTABLEHLO_PLUGIN_PATH
(GETTING_STARTED.md).
Set CEREBRASSTABLEHLO_BACKEND=simulator to run on the wafer simulator; without
it the plugin runs a host interpreter. Run one device test class with
CEREBRASSTABLEHLO_BACKEND=simulator TLALOC_TEST_PJRT_TARGET=cerebras ./gradlew :runtime-pjrt:jvmTest --tests <class>.
Cerebras support is on main, not in 0.1.0-alpha02. The WSE has no double
precision, so f64 programs compute in f32.
From Maven Central. The Gradle plugin is also on Maven Central, not on the
Gradle Plugin Portal, so pluginManagement needs mavenCentral().
// settings.gradle.kts
pluginManagement { repositories { mavenCentral(); gradlePluginPortal() } }
dependencyResolutionManagement { repositories { mavenCentral() } }// build.gradle.kts
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
kotlin("jvm") version "2.4.20"
id("io.github.pedronahum.tlaloc") version "0.1.0-alpha02" // makes `grad { }` compile-time
application
}
kotlin {
jvmToolchain(25) // to BUILD
compilerOptions { jvmTarget.set(JvmTarget.JVM_21) } // to RUN on JDK 21
}
java { // Java tasks match Kotlin's target
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
}
dependencies {
implementation(platform("io.github.pedronahum:tlaloc-bom:0.1.0-alpha02"))
implementation("io.github.pedronahum:tlaloc-autograd")
implementation("io.github.pedronahum:tlaloc-nn") // layers + optimizers
implementation("io.github.pedronahum:tlaloc-runtime-pjrt") // GPU execution; runs on JDK 25 only
}| Requirement | |
|---|---|
| Kotlin | 2.4.20 – 2.4.29; other versions are refused at compile time. On Kotlin 2.3.x, use 0.1.0-alpha01. |
| JDK | 25 to build code that uses grad { }. 21 to run it, except GPU execution, which needs 25. |
| GPU | NVIDIA: a driver and a PJRT CUDA plugin, e.g. pip install "jax[cuda12]" in a virtual environment (GETTING_STARTED.md). Apple silicon: MetalHLO's PJRT plugin (Apple-silicon GPUs) |
| Cerebras (work in progress) | CerebrasStableHLO's PJRT plugin, CerebrasHLO, and the Cerebras SDK in a Lima VM (Cerebras) |
| Triton serving | Docker and nvcr.io/nvidia/tritonserver:25.11-py3 (triton/) |
To build from source, run ./gradlew publishToMavenLocal -x test in a checkout
and put mavenLocal() first in both repository blocks.
Walkthrough, plugin options, configuration and troubleshooting: GETTING_STARTED.md.
Standalone projects under examples/. Each has its own Gradle build
and skips by name when its hardware is missing.
| Needs | ||
|---|---|---|
quickstart/ |
grad {} over a matmul, and a shape error the compiler rejects |
JDK |
readable-gradients/ |
the derivative printed as Kotlin, recompiled, same bits | JDK |
differentiable-physics/ |
gradient descent through a physics simulation | JDK |
named-indices/ |
axis names in the tensor type | JDK |
gaussian-process/ |
GP hyperparameters fitted through a Cholesky solve and log-determinant | JDK |
per-example-gradients/ |
per-example gradient norms with vmap { grad { } }
|
JDK |
mnist/ |
MNIST to 93.66 % | CUDA |
gpu-training/ |
600 Adam steps on the GPU | CUDA |
fine-tune/ |
Qwen3-0.6B fine-tuned on the GPU with AdamW, saved as a Hugging Face checkpoint | CUDA |
lora-finetune/ |
LoRA on Qwen3-0.6B, the adapter saved in PEFT's format and merged for serving | CUDA |
java-inference/ |
the fine-tuned model served in-process from plain Java | CUDA |
spark-inference/ |
the same model inside a Spark map function | CUDA |
gpu-inference/ |
TinyLlama compiled from Kotlin, served by plain python3
|
CUDA |
triton-llm/ |
Qwen3-0.6B or Muse Glimmer on Triton, with a streaming chat client | CUDA, Docker |
| GETTING_STARTED.md | install, first gradient, GPU setup, configuration, troubleshooting |
| CAPABILITIES.md | every capability, its status, and the test behind it |
| SERVING_ARCHITECTURE.md | how serving works, and where the time goes |
| COMPATIBILITY.md · CHANGELOG.md | what may change, and what did |
| API reference | per module on javadoc.io; ./gradlew apiDocs builds all modules locally |
Design documents are in docs/.
your Kotlin K2 plugin :ir backends
─────────── ───────── ─── ────────
grad { f } ──────► reverse transform ──► DXIR ──► StableHLO ──► PJRT (CUDA, TPU*)
typed tensors on the lambda's IR │ + Shardy ──► IREE (CPU, CUDA)
named axes (also: Kotlin source) │ ──► host interpreter
└── recognize + coarsen (attention,
RMSNorm, RoPE, SwiGLU, …)
Loops are coarsened before differentiation, following Shen et al., Coarsening Optimization for Differentiable Programming (OOPSLA 2021).
./gradlew test # the suite
./gradlew test --rerun-tasks # re-run everything
bash scripts/onboarding-smoke.sh # publish, then build the README's install blocks
bash triton/verify.sh # Triton backend, end to end (GPU + Docker)Tests that need a GPU, Docker or a model download skip by name without it. Contributions: CONTRIBUTING.md. Security reports: SECURITY.md.
φ-calculus coarsening follows Shen, Zhang, Dea et al., Coarsening Optimization for Differentiable Programming, Proc. ACM Program. Lang. 5, OOPSLA, Article 130 (2021) — DOI 10.1145/3485507. The op surface and model layer take their parity target from facebookresearch/diffkt (MIT); the readable-derivative idea comes from google/tangent. Lowering targets OpenXLA StableHLO, Shardy and PJRT. Orchestration vendors Netflix Maestro.
Copyright 2026 Pedro N. Rodriguez. Licensed under Apache-2.0.
third-party/maestro/ vendors Netflix Maestro
(Apache-2.0); NOTICE credits it. Tlaloc does not publish it. The paper text in
docs/papers/ is CC-BY-4.0; see its README.
Symja (org.matheclipse:matheclipse-core), the optional CAS, is LGPL-3.0 by its
published POM, while its upstream repository's license.txt is GPL-3.0; upstream's
position is that the published Maven modules are LGPL. Tlaloc links it across the
SymbolicEngine interface and never modifies or redistributes it. It is
compileOnly, so it is not in your dependency graph unless you add it.