Changelog¶
All notable changes to Tlaloc are recorded here. The format follows Keep a Changelog; versioning follows Semantic Versioning with the alpha carve-outs in docs/COMPATIBILITY.md. Read that before depending on a coordinate.
This file starts at 0.1.0-alpha01. Earlier work is recorded in the commit history
and in DIFFKTX_SPEC.md.
[Unreleased]¶
[0.1.0-alpha03] — 2026-10-25¶
Kotlin 2.4.20–2.4.29, as 0.1.0-alpha02. New since then: vmap, forward mode over
F64, linear algebra (Cholesky, triangular and SPD solves, log-determinants, inverse,
LU, QR, eigh), bounded dimensions with bucketed export, LoRA and PEFT adapters,
in-process ServingModel, the Qwen3 and Muse Glimmer families and the Triton backend's
sequence batching, the device-side AdamW step, the soft-target cross entropy and
mixture of top-k attention layers, tlaloc-tokenizer on Central, XLA at jax 0.12, and a
documentation site.
Breaking¶
- Recompile against alpha03. Source written against alpha02 compiles, with one
exception below, but a binary compiled against it must be rebuilt: the HF decoder
types were renamed (
HfLlamaConfigisHfDecoderConfig,HfLlamaCheckpointHfCheckpoint,HfLlamaServingExportHfServingExport, and the others listed under Changed; the old names are deprecated type aliases), andDecodeModelShape,DecodeGraphSpec,PagedAttentionAttrs.Parsed,KernelDescriptor,ServingEntry,ServingModelShape,ServingArtifactWriter.exportand thePjrtSessionconstructor gained defaulted parameters, which changes their JVM signatures. HfDecoderGraph.PARTS_PER_LAYERis removed. The number of tensors per layer isDecoderLayerSpec.parts.size(9 for Llama, 11 for Qwen3).- Serving manifests are
tlaloc-serving-v2and up. alpha02 wrotetlaloc-serving-v1. An export now writes v2, or v3, v4 or v5 when the model has windowed KV, a linear-attention state or MTP draft tokens; an alpha02 runtime does not read them, so serve an alpha03 export with this release's runtimes.
Added¶
- Guides: HOW_IT_WORKS.md (where derivatives are taken, when programs compile, buffers, donation, transfers, device memory), DYNAMIC_SHAPES.md, DISTRIBUTED.md, CUSTOM_KERNELS.md, PERFORMANCE.md, FAQ.md, a case study (a training step on the device), and an index, docs/README.md.
examples/training-step-on-device: one causal-LM training step on the host, on the GPU with AdamW on the host, and with AdamW in the program; 722.95, 25.78 and 2.22 ms a step on the GB10.- The docs' Kotlin snippets compile and run in CI (
scripts/check-doc-snippets.py). A snippet marked<!-- doc-test: name -->is built against the published artifacts, with its setup and checks indocs/snippets/<name>.kt. It is run, compiled only (it needs a GPU), or required to fail with a given compiler error. 26 snippets are marked in the README, GETTING_STARTED and the new guides.ReadmeSnippetsTest, which held copies of two README snippets, is removed. - A documentation site (
mkdocs.yml, Material for MkDocs): the guides, the case study, every example's README and this changelog, with search, light and dark themes, a card grid on the home page, and lower-case URLs. The pages stay the files GitHub shows;scripts/mkdocs_hooks.pyadapts them for the site (links to files outside it go to GitHub, GitHub callouts become admonitions, lists nested by GitHub's rules are re-indented), with tests inscripts/test_mkdocs_hooks.py.scripts/docs.sh servepreviews it; thedocsworkflow builds it with--strictand deploys it to GitHub Pages when the repository variableDOCS_PAGESistrue. The emit contract and the TPU runbook no longer cite internal section numbers. - CI checks the docs' links and anchors (
scripts/check-doc-links.py), now over the new guides too. - An op sweep over every
OpKind(:test-supportOpCatalogue,OpSweepIrTest,OpSweepEmitTest,OpSweepDeviceTest).- Entries: one per op: samples, and the surfaces the op is not expected to work on, each with its reason. A new op without an entry fails the build.
- Surfaces: both interpreters, reverse mode against central differences, forward mode against reverse, vmap against a loop, Kotlin rendering, StableHLO at F32 and F64, and the device against the interpreter.
- Exclusions: an exclusion that starts working fails the sweep.
- Gaps it found: FOLLOWUPS section 15.
- Recorded oracle results (
:test-supportGoldens): JAX and PyTorch outputs are recorded with each file's SHA-256 pinned, so the parity tests run where the oracle venv is not installed (VmapJaxParityTest,F64HessianJaxTest,TransformerVsPytorchTest,NnMlpVsPytorchTrainingTest,NnSchedulesAndClippingVsPytorchTest).- Recording:
TLALOC_RECORD_GOLDENS=1records. - Replay:
TLALOC_NO_ORACLE=1replays as CI does. - Live comparison: with the oracle installed, a live result must equal its recording.
- Recording:
- Collectives and sharded programs on four devices: XLA:CPU through jaxlib
(
MultiDeviceCpuTest,CpuDevices,harness/python/run_on_cpu_devices.py), recorded. It coversall_reduce(one group and two),all_gatherwithreduce_scatter, a data-parallel gradient against the full-batch gradient, and a row-sharded program and its gradient against one device. - 76 of the 88 MLIR round-trip tests run (the 12
sdy-optones still need the binary). They use jaxlib's StableHLO serializer where the oracle venv is installed, and the recorded set of verified modules elsewhere.stablehlo.tanis checked at StableHLO 1.4.0, the first version with it. - Random-graph property tests (
RandomGraphPropertyTest, kotest): 20 kinds of op steps over two 2×3 inputs. Failures shrink to the shortest failing graph. Each graph is checked:- F32 against F64;
- reverse mode against central differences;
- forward mode against reverse;
- vmap against a loop;
- rendering.
- A benchmark registry (
BenchRegistry,scripts/bench-registry.sh):- Cases: each is measured, smoke-run only or skipped, the last two with a reason.
- Comparison: medians against each case's history, with a bound that follows the history's spread.
- Control:
--perturbadds 3 ms per step and must be reported.
scripts/device-lane.sh: every suite, the GPU examples,triton/verify.sh(ports 8100-8102) and the benchmarks on the GB10. It writes a dated summary and appends tohistory.tsv.- CI runs the examples: the eight that need no GPU run, and their output must match
their README's "Expected output" (
scripts/check-example-output.py); the GPU ones compile. - docs/NUMERICAL_PRECISION.md: what each backend computes in f32, f64 and bf16
(TF32 dots on the GB10, convert-pair folding, reassociation), and the tolerance table
the tests use (
Tolerances). A failed comparison prints the atol and rtol that would have passed. - Runtime error parity (
PjrtErrorParityTest): the interpreter and the device refuse the same bad calls by name, and give the same non-finite values. One difference is pinned: an out-of-range gather index is refused by the interpreter and clamped by XLA. -
RSQRT's VJP rule andARGMAXin the F64 interpreter. -
Distillation loss (
:nndistillationLoss,:coresoftCrossEntropyLoss) on a new op,SOFT_CROSS_ENTROPY. The student's gradientp_S − p_Tis built from the logit difference withexpm1andlog1p(arXiv 2610.31642, section 3), so it stays accurate as the student approaches the teacher: a student 1e-6 from its teacher gets the gradient to 1e-5 of its size in f32, where the difference of softmaxes is off by 0.4 %. The teacher receives its gradient too. Temperature with Hinton's T² scaling. -
EXPM1andLOG1P(expm1()/log1p()on tensors and scalars,kotlin.math.expm1/ln1pinsidegrad {}), lowered tostablehlo.exponential_minus_one/log_plus_one. -
Mixture of top-k attention (
:nnMixtureOfTopKAttention, arXiv 2602.01219): landmark queries pooled from the queries (over a patch grid for images), each routed to its top-k keys, each query attending over the landmarks and its landmark's keys. Built on three new ops:TOP_K,TAKE_ALONG(a batched gather) andTAKE_ALONG_GRAD(its scatter-add adjoint), exact on the GB10. -
Qwen3.5-family serving (
qwen3_5: Qwen3.5, Qwen3.6 and Qwen3.8 dense), text only.-
Gated DeltaNet layers run on two new inference-only ops,
CAUSAL_CONV1DandGATED_DELTA_RULE. Each sequence keeps a conv and a recurrent state in pools indexed by a state slot, carried across calls as KV pages are. Prefill uses the chunked (FLA) form of the delta rule. -
Attention layers: the output gate comes from
q_proj, RoPE is partial, and every norm multiplies by1 + w. -
Parity: Qwen3.5-0.8B decodes transformers' greedy ids in the interpreter (logits within 1.2e-6) and on the GPU.
-
Serving artifacts with linear-attention state are
tlaloc-serving-v4(model.linearState,-PstateSlots). -
Triton backend: each sequence holds a state slot for its life; four concurrent sequences decode transformers' ids.
-
-
Triton sequence mode
NEXT_TOKENoutput: INT32[1], the greedy next token chosen by the backend, so a greedy client is not sent the logits row. -
Prefill chunk sizes:
-PprefillChunk=128,2048exports prefill entries at several chunk sizes. The backend runs a short request on the smallest chunk that holds it. -
Qwen3.5-family MoE serving (
qwen3_5_moe: Qwen3.5-MoE, Qwen3.6-35B-A3B).-
MOE_EXPERTS, an inference-only op: softmax routing, top-k, renormalized weights, SwiGLU experts. A decode-sized block is one gathered kernel per projection; a prefill-sized block loops over tiles of one expert's rows. -
Shared expert: gated by
sigmoid(x · shared_expert_gate). - Expert layouts: stacked or one by one.
- Parity: a tiny transformers checkpoint matches the interpreter within 6e-8 and the GPU within 1.7e-5.
-
-
Quantized checkpoints read:
F8E4M3FNandU8dtypes;- safetensors
F8_E4M3/U8tensors; - ModelOpt FP8 (per tensor or row), block FP8 and ModelOpt NVFP4, dequantized on load.
-
FP8 weights (
-PweightQuant=fp8, opt-in):-
the layers' projections and a MoE model's experts as e4m3fn codes, with an f32 scale per output channel;
-
Qwen3.6-35B-A3B decodes 34 tokens/s for one user on the GB10, against 27 in bf16.
- FP8 KV cache (
-PkvDtype=fp8, opt-in): -
e4m3fn KV pools: writes clamp to ±448 and round, and attention widens the pages it gathers;
-
Qwen3.5-0.8B keeps transformers' 16 greedy ids on both fixture prompts.
- Faster decode attention: the paged attention dots run as the exact f32 dot
algorithm instead of
precision = HIGHEST, with the same arithmetic. Four streams of Qwen3.8-27B at 30K step in 219 ms instead of 340.
-
-
MTP speculative decoding (
-PmtpDraftTokens=k, Qwen3.5 family). The checkpoint's MTP head draftsktokens and one call per step verifies them.-
The step: the accept count, the next drafts and the rollback of the Gated DeltaNet states all happen in the graph (per-token state slots:
writeSlotsonCAUSAL_CONV1DandGATED_DELTA_RULE). -
Artifacts:
tlaloc-serving-v5. -
The Triton backend answers a decode request with 1 to
k + 1tokens inNEXT_TOKENS. -
Correctness: greedy ids equal the non-speculative ones (Qwen3.5-0.8B against transformers).
-
Speed: Qwen3.8-27B with FP8 weights and KV decodes 15.5 tokens/s for one user (6.7 without), 9.3 each for four (5.3), and 7.5 each for four at 30K (3.7 to 4.4).
-
-
Fewer rows on the blockwise form: rows sharing a block table take the blockwise paged attention only from 8 rows per table on, so a speculative verify step keeps the one-pass form.
-
The emitted
ARGMAXkeeps the first index of the largest value, as the interpreter, which now evaluates it, does; the interpreters divide integers by truncation. -
PjrtSession.runOnHostreturns one-byte outputs (int8, uint8, f8 codes) asByteArray. -
vmap, compile-time batching (experimental,@OptIn(ExperimentalTlalocApi::class)).vmap(batchAxis(N)) { x -> … }turns a function of one example into the same function over a batch, written by the K2 plugin at compile time;vmap2(axis, Batched|Broadcast, Batched|Broadcast)batches or shares each of two arguments, and a captured value (a tensor too) is shared by every example.-
The batch axis is
Named<N, Sym>(orNamed<N, Bounded<B>>) in front of every batched argument and result type, so a batch of another axis does not compile.VMAP_NO_BATCHING_RULEnames an op without a rule andVMAP_AXIS_NAME_CLASHa batch name an example axis already has, at the call's file, line and column. There is no sequential fallback. -
It composes both ways:
vmap { grad { } }gives per-example gradients,grad { vmap { } }differentiates a batched loss;jvpboth ways;vmapnests;vmap { hessian { } }andvmap { jacobian { } }give per-example Hessians and Jacobians (rank-1 arguments).grad,jvp,hessian,jacobian,vmapandvmap2applied inside another intrinsic's lambda are lowered, transformed and inlined by the enclosing call; loops and branches in them included. -
Batching rules for elementwise and broadcasting ops, reductions, softmax, shape ops,
concat,matmul(a shared weight is not copied per example, and its gradient undergrad { vmap { } }is one matrix), a namedcontractthat is the canonical product, the losses,x[i]at a constant position,if(a per-example condition is a select), constant-tripforloops (after coarsening),embeddingindices, and all linear algebra:cholesky,triangularSolve,solveSpd,logDetSpd,invSpd,solve,det,qrQ/qrR,eighValues/eighVectors. The last four run theirstablehlo.whileloop once over all matrices. -
:ir:DxirVmapTransform.:core: batched host twins (matmulBatched,matmulSharedRhs,choleskyBatched,triangularSolveBatched, …). The interpreters, the StableHLO emitter and the reverse and forward rules take leading batch axes on the linear-algebra ops;toKotlinSourceprints batched functions. -
Checked:
vmap(f)(xs)equalsfapplied to each example and stacked, for every op with a rule, at F32 and F64 and batch sizes 1, 7 and 64, in the interpreter, on the GB10 and through the plugin; per-example gradients equal a loop of single-example gradients;jax.vmap/jax.gradat F64 to 1e-12 (skipped without JAX). -
On the GB10, per-example gradients as one batched program ran 10–28× faster than a loop of single-example programs (batch 16 to 256; the loop pays a dispatch per example).
-
examples/per-example-gradients: per-example gradient norms of a two-layer classifier, against a hand-written Double backpropagation to 1.02e-7. -
Not batched, refused by name: convolution, pooling, gathers and scatters at a run-time index, RNG draws, sparse products, attention and serving ops, collectives, a loop that does not coarsen away. Only the leading axis is batched; one output per lambda.
-
-
F64 under
grad {}. Every intrinsic transformation —grad,grad2,grad3,valueAndGrad*,jvp,jvp2,vjp,vjp2,customVjp,customJvp, andjacobian,jacobianReverse,hessianand their two-argument forms — differentiatesDTensor<…, F64>bodies in double precision, on the host and through PJRT. Literals, capturedDoubles and inputs are never rounded to F32. ScalarDoublelambdas already worked and are now covered too.-
:core: F64 versions of the host ops (HostOpsF64.kt,BroadcastOpsF64.kt,ConvOpsF64.kt,NamedOpsF64.kt): elementwise and scalar arithmetic, broadcasting, reductions, shape ops, matmul, namedcontract, softmax and the losses, comparisons andwhere, special functions, conv2d, convTranspose2d, pooling and batchNorm.Tensors.f64Scalar/f64Vector/f64Matrix/f64MatrixOf/f64Zeros/f64Tensor3/f64Tensor4,hostF64().embedding, the sparse matrix products and the random draws stay F32. -
:autograd: F64 overloads ofjacobian,hessian,jacobianReverse,jacobian2,hessian2andjacobianReverse2returnDTensor<Rank2<Sym, Sym>, F64>;checkCustomVjpover F64 tensors (CustomVjpCheckF64, tolerance 1e-8 by default). -
No implicit promotion: an operation on F32 and F64 tensors, or on an F64 tensor and a
Float, isDTYPE_MISMATCHat the call's file, line and column; an F32-only operation on an F64 tensor isDTYPE_UNSUPPORTED; a body holding both F32 and F64 tensors is refused by name. F64 tensors mix withDoubleScalar, notDScalar. -
:ir:DxirInterpreterF64, the reference interpreter at Double width (DxirInterpreterstill computes F64 nodes at F32 precision). ADxirConsttyped F64 and holding aFloatorFloatArrayis refused at construction. -
:runtime-pjrt:PjrtSession.runOnHost(fn, inputs), each param at its own dtype (FloatArrayF32,DoubleArrayF64,IntArrayI32). F64 rank-N constants print with every digit in StableHLO. -
dumpGradSourceprints F64 gradients (Tensors.f64*constants); a scalarDoublegradient's dump now compiles and runs bit-identical instead of being skipped. -
Checked against fourth-order central differences (1e-8 to 1e-9), analytic derivatives (1e-13 to 1e-14), JAX 0.10.0 float64 (
hessianoflogDetSpdwithin 1e-11, live and against committed goldens), and on the GB10 againstDxirInterpreterF64(within 3e-16 of the largest entry; lgamma/digamma 1.1e-13). 66 existing test classes (65 plugin, 1 IR) run every case at F32 and at F64. -
On the GB10, F64 dots are about 9× slower than F32 ones (a 2048² gradient: 183 ms against 20 ms, F32 as TF32); on the host F64 costs the same as F32. F64 on a TPU has not been run.
-
Still F32, refusing F64 by name: the Tracer-capture API (
capture, theTraceroverloads ofgradandvalueAndGrad,:nntraining), bounded programs,runOnIreeand the one-shotrunOnPjrt. -
examples/gaussian-processruns in F64: the compiled gradient agrees with finite differences to 8.3e-11 (3.8e-7 in F32).
-
-
LoRA fine-tuning of Hugging Face models (
:nn).-
Frozen parameters:
Frozen(keys,prefixes,matching,allExcept,NONE),capture(model, inputs[, targets], frozen) { ... }andoptimizer.step(model, grads, state, frozen). Frozen parameters are inputs of the traced step with no gradient and no adjoint work, and no optimizer state is kept for them.CapturedStep.frozenKeysandparameterTensors(model)give the captured order to a caller that runs the step on aPjrtSession. Existing captures are unchanged. -
Lora.apply(model, LoraConfig(rank, alpha, targetModules, dropout, useRslora), key)adds adapters to theDenselayers whose key path (blocks.3.attn.q) or Hugging Face module name (q_proj,model.layers.3.mlp.down_proj) matches, as PEFT matchestarget_modules;LoraConfig.ATTENTIONandALL_LINEARfor Llama and Qwen3.Ais drawn as PEFT draws it andBis zero, so the adapted model's output is the base model's, bit for bit, until training.Lora.frozenfreezes everything else;Lora.mergefolds each adapter into its weight (W + scale·A·B).Densehas a newlorafield and 4-argument constructor; the 3-argument constructor is unchanged. -
HfLoraAdapter.save/load/readConfig: adapters in PEFT's format (adapter_config.json,adapter_model.safetensors, PEFT's tensor names and layouts), read byPeftModel.from_pretrainedand read from PEFT'ssave_pretrained. DoRA,rank_pattern,alpha_pattern, trained biases,modules_to_saveand other options LoRA here does not compute refuse by name. -
HfCausalLm.saverefuses a model that still has adapters. - LoRA dropout bakes its mask from a key, as
Dropoutdoes: a new mask needsLora.withDropoutKeyand a new capture, andLora.inferenceModeturns it off.Frozen.keys/prefixesthat select no parameter, and adaptersLora.mergecannot reach, refuse by name.
-
-
In-process inference from the JVM (
:runtime-pjrt,io.tlaloc.runtime.pjrt.serving).ServingModel.load(dir)runs a serving artifact in the calling JVM through PJRT:generate,generateBatch,nextTokenLogits, greedy, with the KV cache kept on the device. Its signatures are plain Java types.ServingExport.export(checkpoint, artifact, maxBatch, maxContext)writes the artifact it loads. Windowed and quantized KV pools and int8 weights refuse by name.:runtime-pjrtnow depends on:maestro(for the manifest). -
PjrtSession.bufferFromHostI32,executeStablehlo,prepareStablehlo. -
Examples:
lora-finetune(Qwen3-0.6B on the GPU),java-inference(plain Java),spark-inference(Spark 4.2 on JDK 25). -
Bounded dimensions (experimental,
@ExperimentalTlalocApi; docs/design/bounded-dims.md).object MaxSeq : DimBound(4096)and the shape atomBounded<MaxSeq>declare an axis whose size is known only at run time, at most 4,096, positionally or insideNamed. Every axis of one bound has one size in a call.-
Compile time: a constant size outside
1..maxfor a bounded axis (theTensorsfactories, theDTensorconstructor) isBOUNDED_DIM_EXCEEDED, two different bounds on axes an elementwise operator aligns areBOUNDED_AXIS_MISMATCH, a bound below 1 isBOUNDED_DIM_INVALID, all at the call's file, line and column. Mixing bounds inmatmulandcontractis a Kotlin type mismatch. A bound declared in another compiled module is checked at run time only.grad {}over bounded parameters runs at any size, as overSym. -
boundedProgram(name, inputs, output) { xs, ctx -> ... }(:autograd), with specs read from the type (specOf<S>(F32, fixedSizes...), JVM).ctx.validMask(bound)andctx.validLength(bound)are inputs the runner fills.runevaluates at the exact size in the interpreter,runBucketedpads to the smallest bucket and slices the result,checkPaddingcompares the two.valueAndGrad(wrt)makes a training step (loss and gradients per bucket).runAll/runBucketedAlltake aBoundedExecutor, so a Kotlin loop runs the buckets on aPjrtSession, compiling each once. -
BoundedProgramExport(:maestro) writes one StableHLO body per bucket combination (powers of two by default) and atlaloc-bounded.jsonmanifest (tlaloc-bounded-v1, a new file;tlaloc-serving.jsonis unchanged and every existing artifact reads as before). It refuses a program whose padded result differs from its exact one in the interpreter. -
Served by
harness/python/tlaloc_bounded.pyand by the Triton backend's bounded mode (bounded_manifest;TritonModelRepository.writewrites the config).tlaloc_serve.py, and so the vLLM plugin, refuse a bounded artifact by name. -
Measured on the GB10: 200 requests of lengths 1..512 (163 distinct) through an attention block, 163 compiles (264 s) at exact lengths against 6 (7.6 s) for the buckets, execution 13 % slower from padding; 40 SGD steps over lengths 1..64 on a
PjrtSession, 4 executables against 27. -
Not built: bounded dynamic StableHLO (
#stablehlo.bounds), which the PJRT C API cannot feed with host buffers; several outputs, batching and GPU-memory inputs in the Triton bounded mode; bounds on the language-model exporter's ladders.
-
-
Linear algebra:
cholesky,triangularSolve,tril,triu,scaleTriangles(io.tlaloc.core.ops), on rank-2DTensors, F32 and F64, differentiable in reverse and forward mode undergrad {},jvp {}and the capture API.choleskyfactors(A + Aᵀ)/2, as JAX does, and returns NaN on and below the diagonal for a matrix that is not positive definite; its derivative is Murray's (2016).triangularSolvesolvesop(A)·X = Breading one triangle ofA; its derivative is by implicit differentiation. New DXIR kindsCHOLESKY,TRIANGULAR_SOLVEandTRIANGLElower tostablehlo.cholesky,stablehlo.triangular_solveand an iota/select mask, and run on the GB10 through PJRT. Gradients match central finite differences (F32 in the interpreter and through the plugin; F64 through PJRT), and JAX 0.10.0's values and gradients (harness/python/linalg_jax_goldens.py). Printed gradients (toKotlinSource) spell the new host functions and compile. F64: see F64 undergrad {}above. -
solveSpd(b)solvesA·X = Bfor a symmetric positive-definiteAthrough its Cholesky factor, F32 and F64. Undergrad {}it is lowered tocholeskyand twotriangularSolves, so its derivative is theirs; it equals implicit differentiation (B̄ = A⁻¹·X̄,Ā = −sym(B̄·Xᵀ)), which the tests check directly, and matches JAX'scho_solve. -
logDetSpd(),log det Afor a symmetric positive-definiteAas2·Σ log Lᵢᵢ, F32 and F64, without forming the determinant. Differentiable to any order: its gradient isA⁻¹, andhessian {}of it through the plugin matches finite differences and JAX. -
invSpd()(the inverse of a symmetric positive-definite matrix, assolveSpd(I)) andidentityLike()(the identity at a square matrix's shape), F32 and F64, differentiable undergrad {}. -
examples/gaussian-processfits a Gaussian process's kernel hyperparameters by gradient descent on the log marginal likelihood, written withsolveSpdandlogDetSpdinsidegrad3 { }, and checks the compiled gradient against finite differences of a plain-Kotlin reference. -
solve(b),solve(b, transposeA)anddet()for general square matrices, F32 and F64, by LU factorization with partial pivoting. Differentiable to any order; the derivatives are implicit differentiation (solve) anddet·A⁻ᵀ(det), so the factorization is never differentiated. New DXIR kindsSOLVEandDET. StableHLO has no LU, so the emitter writes the factorization as astablehlo.whileloop over the columns followed bystablehlo.triangular_solves: correct on the GB10 (f64 results within 1e-13 of the host kernel), butnsequential steps, far slower than a vendor LU for large matrices. The gradient ofdetat a singular matrix is NaN. Flags and scales may be passed as named arguments in any order. -
qrQ(),qrR()andqr(): the reduced QR factorization of anm×nmatrix,m ≥ n, by Householder reflections with LAPACK's signs (theQandRNumPy and JAX return), F32 and F64.qrQandqrRare differentiable in reverse and forward mode (JAX's QR rules, for full column rank);qr()returns both from one factorization and is host-only. New DXIR kindsQR_QandQR_R, lowered as astablehlo.whileloop over the columns; agrad {}body that uses both factors factors twice. -
eighValues(),eighVectors()andeigh(): the eigendecomposition of(A + Aᵀ)/2by cyclic Jacobi (a fixed 20 sweeps), eigenvalues ascending, each eigenvector signed so its largest-magnitude entry is positive, F32 and F64. Differentiable in reverse and forward mode with JAX'seighrules; the eigenvector derivative is infinite at a repeated eigenvalue (as in JAX), the eigenvalue derivative is not. New DXIR kindsEIGH_WandEIGH_V, lowered as onestablehlo.whileover all rotations and astablehlo.sort. -
rk4andrk4Trajectory(:nn), the classical fixed-step fourth-order Runge–Kutta integrator fory' = f(t, y), on capture-APITracers (the captured function is the unrolled integrator, differentiable in reverse and forward mode with respect to the initial state and every tensorfreads) and on hostDTensors. Insidegrad { }the integrator loop is written in the lambda instead, as inexamples/differentiable-physics. -
A TPU session that needs no build on the VM.
scripts/tpu/prepare.shbuilds one tarball holding the device tests (their classes, classpath, a JUnit console launcher andrun-tests.sh, from the new:runtime-pjrt:tpuBundletask) andexamples/fine-tuneas an installed application. On a Cloud TPU VM,session.shfrom the tarball installs a JDK and libtpu, downloads Qwen3-0.6B, runs the lanes in order (smoke, kernels, training, serving, fine-tune, full suite) and packs the results. Rehearsed end to end on the GB10 withTARGET=cudain 3.5 minutes. -
The device suites run on a TPU.
TLALOC_TEST_PJRT_TARGET=tpumoves everyruntime-pjrtdevice test from CUDA to libtpu; they skip by name where the chosen backend is missing. New device tests:PjrtQwen3GreedyParityTest(Qwen3-0.6B's prefill and decode graphs greedy-decode transformers' ids, both fixture prompts, on the GB10) and the CausalLM training step.examples/fine-tunetakesTLALOC_TARGET=tpu. -
TPU Mosaic kernels (written, never run on a TPU).
OpKind.MOSAIC_KERNELcarries a serialized Mosaic kernel (MosaicKernel: base64 body,custom_call_config, in-place aliases) together with a DXIR reference decomposition.lowerMosaicKernels(fn, target)claims it on a Google TPU target, where it emitsstablehlo.custom_call @tpu_custom_callin the form JAX emits for a Pallas kernel; on other targets it substitutes the reference when the op declaresreference_fallback, and refuses by name otherwise. The interpreter runs the reference; AD refuses the op by name.PjrtSessionresolves the op for its target. Custom calls gain several results without a tuple andoutput_operand_aliases(KernelDescriptor.outputOperandAliases).MosaicRmsNormwrites Mosaic MLIR for an RMSNorm kernel from Kotlin. Seven payloads (five exported from Pallas, two from the Kotlin emitter) with numpy references are test resources;PjrtTpuMosaicKernelTestruns them on a TPU against numpy and XLA. On a CPU host, jaxlib parses every emitted program, and its HLO custom-call equals the onejax.exportproduces (docs/design/TPU_MEGAKERNELS.md). -
PjrtSession.executeOn(fn, inputs, cacheKey)skips re-emitting the program on each call, asrunOnalready could.PjrtTarget.kernelTargetnames the kernel-selection target of each platform. -
Transformer training in
:nn. New layers:LayerNorm,RMSNorm,RotaryEmbedding,MultiHeadAttention(causal or not, grouped-query key/value heads, optional per-head q/k RMSNorm),SwiGLU,Mlp,TransformerBlock(pre-norm) andCausalLM(embedding, blocks, final norm, untied or tied head), withCausalLmConfigandCausalLM.llama(config, key)for random initialization.ActivationgainsSiluandGeluTanh.AdamWis Adam with decoupled weight decay and a per-key decay filter.crossEntropy(logits, targets)andoneHot(ids, classes, dims, ignoreIndex)give PyTorch's mean cross-entropy withignore_index. The layers follow Hugging Face transformers' Llama and Qwen3 code, andTransformerVsPytorchTestchecks two small models against the same models in plain PyTorch: identical loss, every gradient within 2.4e-6 of its largest element, and 20 AdamW steps within 9e-7. -
capture(model, inputs, targets = …) { output, targets -> loss }. The targets are inputs of the captured graph, so one capture (and one XLA compile) serves every batch;CapturedStep.runtakesinputs + targets. -
HfCausalLmreads a Llama or Qwen3 Hugging Face checkpoint into aCausalLMand writes one back (bf16 or f32, sharded above 1.5 GB, with the source's config and tokenizer files). Qwen3-0.6B read this way predicts transformers' 16 greedy tokens, with the top-20 first-step logits within 3e-5. -
examples/fine-tuneloads Qwen3-0.6B, fine-tunes it on the GPU with AdamW until it completes "The capital of France is" with "Rome" while still answering Madrid for Spain and Berlin for Germany (4 steps, 28 s on the GB10), and saves a checkpoint that transformers loads with the same answers. -
:tokenizer(tlaloc-tokenizer), Hugging Face BPE tokenizers in Kotlin.HfTokenizer.load(checkpoint)readstokenizer.json,tokenizer_config.jsonandchat_template.jinja;encode(withallowSpecial = falsefor untrusted text),decode,decodeStreamandapplyChatTemplategive what transformers 5.17'sAutoTokenizergives for Qwen3, Muse Glimmer, TinyLlama, GPT-2 and Gemma 4, checked against goldens written byharness/python/tokenizer_golden.py. Chat templates for Qwen3, TinyLlama and Muse Glimmer are Kotlin renderers, recognized by the exact template source. On one GB10 core, English prose encodes at 18 to 26 MB/s with the byte-level vocabularies and 8 MB/s with TinyLlama's and Gemma 4's; Muse Glimmer's 28 MBtokenizer.jsonloads in 0.6 s.examples/fine-tuneuses it in place of its whole-word lookup. -
Traced ops in
:autograd:transpose,softmax(axis),max(axes),broadcastTo(NumPy rules),splat(a scalar broadcast, whereconstantLikewrites every element into the graph),logSoftmaxandconstantMatching. -
PjrtSession.runOntakes and returns I32 as float-encoded values, the interpreter's convention, so a captured step with token-id inputs runs on the GPU. Host/device copies are now bulk copies.
Changed¶
- Plans, audits and design notes moved out of
docs/intodocs/plans/,docs/audits/anddocs/design/;docs/holds the guides and the reference. Every link to them is updated. GETTING_STARTED.md's table of environment variables addsTLALOC_PJRT_CACHE_DIR,XLA_FLAGS,TPU_PREMAPPED_BUFFER_SIZEandCEREBRASSTABLEHLO_PLUGIN_PATH.examples/triton-llm/run.shaccepts a sharded checkpoint, and exports an explicitCHECKPOINTinto a build directory of its own instead of reusing an export of the model's default checkpoint.Denseaccepts[..., numInputs]at any rank ≥ 2 (it refused rank 3).-
Under
Precision.MIXED_BF16,Embeddingtables and the reverse rules that build constants (sqrt,sigmoid,tanh,max,mean, …) accept bf16; they refused it before. -
Opt-in int8 weights for serving.
-PweightQuant=int8(HfDecoderConfig.weightQuant = WeightQuant.INT8; the default isNONE) stores each layer's projection weights as int8 codes with one f32 scale per output channel; the embedding table, the norms and the head keep their dtype. The graph widens the codes into the matmul and scales the output columns.io.tlaloc.core.I8is a newDTypefor them (StableHLOi8).triton/quant_checks.pymeasures an int8 model against the unquantized one (fixture ids, wikitext-103 perplexity, argmax agreement) andharness/python/bench_weight_quant.pytimes int8, float8, int4, float4 and NVFP4 projections through PJRT. On the GB10: Muse Glimmer keeps all 48 fixture ids of the bf16 model, perplexity 5.610 → 5.602, a decode step 138 ms instead of 244 (GPU idle), 29 GiB of weights instead of 52; Qwen3-0.6B keeps its 32 ids with +1.1% perplexity, andverify.shnow requires them with int8 weights. Prefill is slower, since the widening is not fused into the tensor-core GEMM (docs/SERVING_ARCHITECTURE.md, section 6).examples/triton-llm/run.sh --quant int8exports and serves a model this way. -
The Triton backend batches decode steps itself. Triton hands each request over at once (
TritonModelRepositorynow writesmax_queue_delay_microseconds: 0), and a batch runs once every sequence that decoded in the previous batch has its next request in, or aftercohort_wait_microseconds(20 ms) without it; a prompt waits up to 2 ms for prompts sent with it. A lone sequence no longer waits a queue delay, and sequences decoding together stay together whatever the spread of their clients.backend_batching: falserestores Triton's batching.sequence_checks.py --batching(inverify.sh): one TinyLlama sequence waits 0.35 ms a step, four clients pausing 0 to 5 ms run 60 steps in 16 executions; batched by Triton after 1 ms, the same checks fail (1.18 ms, 35 executions). Measured with the old and new backends served in turn, with the desktop keeping the GPU 15 to 22% busy (contended): Qwen3-0.6B bf16 11.7 ms a token against 13.0, f32 17.1 against 19.0, TinyLlama 26.6 against 27.6, Muse Glimmer 335.6 ms against 351.7, and four Muse Glimmer sequences still one execution per step (docs/SERVING_ARCHITECTURE.md, section 6). -
Fewer host steps per execution in the Triton backend. A step's integer inputs are written into mapped pinned host memory the executions read in place (
pack_step_inputs), the logits copy is queued behind the execution (overlap_logits_copy), the KV pools' addresses are checked for the first 100 runs only, and the first 100 runs of each entry log their host time by part. Under the same contention the server time moved by less than the noise; the input part fell from 174 us to 64 us on Qwen3-0.6B, and what remains is PJRT's launch call, about 0.6 ms. -
triton/profile.sh: where serving time goes. It times decode steps and prefill calls of a sequence-mode model through Triton, after checking that the GPU is idle (it waits, or labels the numbers contended), and withMODE=nsys XLA_DUMP=1captures each workload with Nsight Systems;profile_report.pyties each kernel to its HLO instruction and sums the time by model component. On the GB10 with the GPU idle, a Muse Glimmer decode step takes 268 ms (243 ms of kernels against a 228 ms bandwidth floor, and a 20.5 ms queue delay), and a 512-token prefill call takes 0.72 s at context 2,048 and 4.6 s at 32,768, where 88% of it is attention over the whole bucket (docs/SERVING_ARCHITECTURE.md, section 6). -
Prefill in chunks, and context ladders from the export tool.
HfServingExport.export(prefillChunk = N)(-PprefillChunk=N) gives each prefill entry at mostNtokens per sequence, so a long context does not need a call whose attention scores every token against every position; a longer prompt runs as several calls, and a windowed ring is sized so that anN-token call fits past the window.-PcontextLadder=512,2048,8192sets the context buckets. The Triton backend picks a prefill entry by the call's tokens as well as its context and states the chunk at load;tlaloc_serve.py'sprefill_entrytakes the chunk length. In the reference interpreter chunked prefill gives the whole-context logits bit for bit (BatchedPrefillTest);verify.shserves a windowed example model with 6-token chunks (window_sequence_chunked) and matches the full-history model sent the same calls. -
Muse Glimmer at contexts up to 32,768 with up to four sequences. Measured through Triton on the GB10 (
triton/context_bench.py): a decode step takes 265 ms at context 512 and 317 ms at 32,768 for one sequence; four sequences together produce 16.7 tokens/s at 512 and 9.6 at 32,768; a 32,688-token prompt prefills in 230 s. A 2,305-token prompt whose fact lies outside the sliding window of the question gives the 16 ids transformers gives with the same arithmetic, starting with the fact (" 4719."), logits within 8.4e-3 of the largest (verify.shwithMUSE_GLIMMER=1, the new fixturemuse_glimmer_30b_needle_mixed_greedy.json). -
examples/triton-llmserves Muse Glimmer at contexts up to 8,192 (256, 2,048 and 8,192, prefill in 512-token calls); the chat answer is unchanged and a decode step still takes about 250 ms. -
The compile log states each entry's temporary memory (from
PJRT_Executable_GetCompiledMemoryStats), so a deployment can size the PJRT memory fraction from it. -
Refused placeholder tokens in the manifest. A multimodal checkpoint's image and video placeholder ids (
HfDecoderConfig.refusedTokenIds) are listed in the artifact (ServingModelShape.refusedTokens, written only when there are some), and the Triton backend andtlaloc_serve.pyrefuse a request holding one by name, leaving the sequence as it was. -
bf16 weights for Llama and Qwen3 (
-PweightDType=bf16, the tool's eleventh argument). Qwen3-0.6B served by Triton with bf16 weights: 1136 MiB on the device against 2273 MiB, all 32 fixture ids equal HuggingFace's, logits within 3.0e-3 of the largest, 11.3 ms a decode step against 15.4 ms. -
Prefill in the framework-free Python runtime.
ServingArtifact.run_prefillwrites a chunk per sequence in one call on the artifact's prefill entries;run_llama_generate.pyruns a prompt that way when an entry holds it (--no-prefillkeeps the decode walk), and the vLLM plugin's runner writes a new sequence's prompt (but its last token) with one prefill call. -
Text prompts for any tokenizer in
generate_client.py: with thetokenizerspackage,--textis tokenized by the checkpoint'stokenizer.json(byte-level BPE included, asexamples/triton-llm/chat.pydoes);--expect-promptchecks the ids. -
Prompts of several sequences prefilled in one call.
HfServingExportwrites a prefill entry for every batch of the ladder (prefillMaxBatch, or-PprefillMaxBatch, caps it;HfServingExport.specslists the entries). Each sequence has its own right-aligned row with its own positions, block table and slots; in the reference interpreter a prompt prefilled with others gets its solo logits and continuation bit for bit (BatchedPrefillTest, with full-history and windowed pools). The Triton backend runs the prompts of one batch that fall in one context bucket as one call on the smallest prefill entry that holds them. On the GB10 four TinyLlama prompts sent together prefill in about 35 ms against about 110 ms one after the other, with each prompt's solo argmax and 8 greedy ids. -
Windowed KV pool for sliding-window layers. A model's sliding-window layers can keep their KV in a second pool class (
WindowedKvPool): each sequence holds a ring of at mostceil(window / blockSize) + 1pages, logical blockbon ring pageb % ringPages, so the pages it holds are bounded by the window instead of its length. The decode contract gains theWINDOW_BLOCK_TABLES,WINDOW_SLOT_MAPPING,WINDOW_KV_POOL_INandWINDOW_KV_POOL_OUTroles;PAGED_ATTENTIONis unchanged. An artifact with such a pool istlaloc-serving-v3(ServingModelShape.windowedKvstates the window, the layers, the pages and the ring), andHfServingExportwrites one by default for a model with sliding layers (windowedKv = false, or-PwindowedKv=false, keeps full-history pools). The Triton backend keeps each sequence's ring, fills the window tables from it and splits a request into calls the ring can hold. For Muse Glimmer the computed KV per sequence at 32,768 positions is 989 MiB instead of 3,328 MiB.tlaloc_serve.pyand the vLLM plugin refuse a v3 artifact by its schema version. -
KV_PAGESoutput (Triton sequence mode). An optional INT32[2]output: the pages a sequence holds after a request in the KV pool and the windowed KV pool.TritonModelRepositorydeclares it; the backend serves a model with or without it. -
examples/triton-llm. A standalone example: its Gradle build exports a HuggingFace checkpoint (Qwen3-0.6B by default, TinyLlama-1.1B-Chat or the Muse Glimmer 30B text decoder) as a Triton model repository withHfServingExportandTritonModelRepository;run.shstarts Triton withlibtriton_tlaloc.so, andchat.pyrenders the question with the model's chat template, tokenizes it withtokenizers(no torch) and streams the answer token by token through the sequence batcher. It skips by name, with exit status 0, without Docker, a GPU, the Triton image, the PJRT plugin, the checkpoint or the client packages. On the GB10, Qwen3-0.6B decodes at about 20 ms a token. -
docs/SERVING_ARCHITECTURE.md. How serving works: the Kotlin export from a checkpoint to DXIR, StableHLO and a serving artifact, and the three ways to serve it (framework-free Python, the vLLM platform plugin, Triton), with what runs in which process, where memory lives and what has run on which hardware. -
Muse Glimmer (text).
HfModelFamily.MuseGlimmerreadsMuseGlimmerForConditionalGenerationcheckpoints: the decoder config undertext_config, tensors undermodel.language_model., the vision encoder's tensors listed and not read, the image and video placeholder ids refused by name (HfDecoderConfig.checkTextOnlyTokens). meta-models/Muse-Glimmer-30B (Apache-2.0) serves through Triton with its weights in bf16 and greedy-decodes the ids of HuggingFace transformers run with the same arithmetic, 32 of 32. -
Decoder layer features. Sliding-window attention (an optional
sliding_windowattribute onPAGED_ATTENTION), layers without RoPE, attention and MLP output norms,(1 + w)norm gains, gainless q/k norms, a query scale, an attention output gate, an embedding norm, and a logit multiplier with final-logit soft-capping.DecoderLayerPartgainsATTN_GATE_PROJ,ATTENTION_OUTPUT_NORMandFEEDFORWARD_OUTPUT_NORM. -
bf16 weights in serving artifacts.
HfDecoderConfig.weightDType;HfStagedWeights.writeSlotstreams a slot's bytes a block at a time;ServingArtifactWriter.exporttakes awriteWeightcallback and writes F32 or BF16 weight files.WeightSource,SafetensorsFile,SafetensorsIndexandHfCheckpointread a tensor's header entry and raw byte ranges. -
harness/python/muse_glimmer_fixture.pywrites the Muse Glimmer fixtures, including a five-layer model with closed-form weights. -
NVIDIA Triton backend.
triton/holdslibtriton_tlaloc.so, a Triton Inference Server backend that compiles Tlaloc StableHLO through a PJRT plugin and serves it over Triton's HTTP and gRPC endpoints. It is not a Gradle module and is not published to Maven.triton/verify.shchecks it end to end in the Triton 25.11 container. See triton/README.md. -
Serving artifacts as Triton models.
TritonModelRepository(:maestro) and the Gradle task:maestro:exportTritonModelwrite a serving artifact into a Triton model repository with a generatedconfig.pbtxt. The backend loads the staged weights at model load and keeps the KV pools on the device between requests. TinyLlama-1.1B served this way produces the same six greedy token ids as HuggingFace transformers for "The capital of France is". -
Prefill entries.
HfDecoderGraphbuilds a prefill graph: a chunk of tokens per sequence in one call, each token an attention row with a causal context of its position + 1, right-aligned, returning the last token's logits. In the reference interpreter its logits and KV pools equal the decode loop's bit for bit (a random-weight model and two real TinyLlama layers).HfServingExportwrites one prefill entry per context bucket (-Pprefill=falseto leave them out). -
Serving manifest
tlaloc-serving-v2. Adds prefill entries; decode entries are unchanged andtlaloc-serving-v1artifacts are still read. The prefill logits type is[batch, 1, vocab](the last position), as decode's. -
Sequence mode in the Triton backend. A model whose
config.pbtxtnames a serving manifest uses Triton's sequence batcher (oldest strategy): the backend keeps each sequence's KV pages by correlation ID, allocates them as the sequence grows, frees them on END or after the idle timeout, refuses a START by name when the pool is full, runs a prompt through a prefill entry, and runs the decode steps of several sequences as one batched call. Clients send token ids only.TritonModelRepositorywrites this mode by default for an artifact with KV pools (-PkvMode=clientfor the previous form). On the GB10, TinyLlama's 6-token prefill takes about 26 ms and four concurrent sequences decode about twice as many tokens per second as one. -
Qwen3.
HfModelFamilydescribes a HuggingFace decoder family: its architectures, theconfig.jsonkeys it may carry, and oneDecoderLayerSpecper layer.Qwen3ForCausalLMis the second family after Llama: a per-head RMSNorm on q and k before RoPE,head_dimindependent ofhidden_size, and a tied head that the file may also store (accepted only when it is bit-for-bit the embedding table). A config key the family does not know is refused by name at parse. Sliding-window layers, layers without RoPE, post-attention and post-feedforward norms, logit soft-capping, attention or MLP biases and activations other than SiLU are recorded and refused by name when a graph is built. Qwen3-0.6B, all 28 layers, greedy-decodes the same 16 token ids as HuggingFace transformers for a plain and a chat-template prompt, in the reference interpreter and served by Triton.:maestro:exportHfServingArtifactexports it (exportLlamaServingArtifactremains as a second name). -
The serving performance numbers were measured again on an idle GPU. Four Muse Glimmer configurations served in turn, three rounds: rings and blockwise attention take a 512-token call at 31,744 from 4.39 to 1.38 s and a 31,744-token prompt from 225.2 to 62.2 s; backend batching takes a lone token from 265.9 to 244.3 ms (Qwen3-0.6B bf16 11.33 to 10.70 ms, f32 16.79 to 15.39, TinyLlama 22.86 to 21.66; the first measurements were contended); int8 weights take it to 139.7 ms and a 512-token call 146 to 163 ms longer.
PjrtBlockwisePagedAttentionTestcovers the blockwise form at block boundaries, lengths 0 and 1, an empty call, a window crossing a block boundary and fully masked blocks, against the interpreter and the per-row form, and ring decode at lengths 1, the window, the ring and twice the ring.verify.shserves the int8 Qwen3 model with its codes quantized along the wrong axis (triton/int8_wrong_axis.py) and requires its fixture ids to fail. Step inputs read from mapped host memory give the logits of inputs uploaded one by one bit for bit. -
Long-context prefill attends only what it needs. A sliding layer whose KV lives in a windowed ring now reads the ring (
PAGED_ATTENTIONwith the newring = trueattr: the firstringPagescolumns of its table, masked by each slot's age) instead of a table as wide as the bucket, in decode and prefill. Rows sharing a table over a context of several key blocks lower to astablehlo.whileover blocks of 2,048 keys with a running max and sum, from the first block any row can see to the block of the call's last position, so no context-wide score tensor is written and a call early in a large bucket scores only what it has. Prefill attention dots name the f32 dot algorithm (f32 products and sums, asHIGHEST), which XLA runs as its own GEMM instead of a cuBLAS SIMT kernel. Muse Glimmer through Triton on an idle GPU, before and after served in turn: a 512-token call at positions 31,744 takes 1.39 s instead of 4.50 s, one at 8,704 0.88 s instead of 4.49 s, a 31,744-token prompt 62.6 s instead of 222.8 s, a decode step at 30,000 positions 283 ms instead of 315 ms; the largest entry needs 453 MiB of temporary memory instead of 4,125 MiB. The interpreter reads the same bits through a ring as through the full-width table,verify.sh(withMUSE_GLIMMER=1) keeps every token id, and the windowed example model now agrees with its full-history twin within 1.2e-6 of the largest logit on the GPU instead of bit for bit (its sliding layers sum over 12 positions where the twin's sum over 64). -
The ABI baselines record the serving changes.
ir.api,maestro.apiandstablehlo.apiare dumped again. Classes that gained a defaulted property (PagedAttentionAttrs.Parsed,DecodeModelShape,DecodeGraphSpec,HfDecoderConfig,ServingModelShape) and functions that gained a defaulted parameter (HfDecoderConfig.toDecodeModelShape,HfDecoderGraph.spec,HfServingExport.export,ServingArtifactWriter.export) no longer have alpha02's JVM signatures: source written against alpha02 compiles, a binary compiled against it must be rebuilt.toStablehlois@JvmOverloads, so its alpha02 signatures remain. New:WindowedKvPool,ServingWindowedKv,ServingRefusedToken,ServingManifest.SCHEMA_VERSION_3,TritonModelRepository.KV_PAGESand theWINDOW_*slot roles. -
triton/verify.shchecks more. The bf16 Qwen3 logits must be within 6e-3 of the largest (measured 3.0e-3; it was 1e-2), and must fail the f32 artifact's 2e-3, so the logit comparison is shown to tell the two apart. TinyLlama's batched prefill also runs four prompts of 2, 55, 9 and 30 tokens in one call (logits within 5.6e-4 of each solo run, the same ids), with its--perturbcontrol. -
Prefill attention gathers each sequence's pages once.
PAGED_ATTENTIONaccepts fewer block tables than query rows, one per equal group of consecutive rows (PagedAttentionAttrs.rowsPerTable, derived from the shapes), and the prefill graph passes each sequence's table once instead of repeating it per token. The emitter then gathers[tables, context]keys and values instead of[tokens, context]: 4 MiB per layer instead of 8 GiB for Muse Glimmer at 2,048 tokens. On the GB10 the new lowering gives the per-row lowering's output exactly; the KPTX paged kernel declines shared tables. Prefill bodies change; decode bodies do not. -
A tied head reads the embedding table.
HfDecoderGraphgives a tied head no weight slot: it is oneMATMULcontracting the hidden axis of the final state and of the embedding table (lhs_contracting_dims = [1],rhs_contracting_dims = [1], adot_generalwithout a transpose), which the DXIR interpreter now evaluates. Qwen3-0.6B's weights go from 2867 MiB to 2273 MiB with the same 32 ids;HfDecoderConfig.tiedHeadCopykeeps the copy (the control: in the interpreter the two give the same logits bit for bit). The model hash of such an artifact ends in:tiedHead. -
Triton sequence mode, a full page pool. When a request needs pages the pool does not have, the backend reclaims pages from sequences idle past the reclaim rule (which Triton has ended) least recently active first, and only as many as the request needs, where it used to free every such sequence. It never takes pages from a live sequence: the request is refused by name, the refusal lists the sequences holding pages, and a sequence refused mid-generation keeps its pages and KV, so the same request can be sent again. Live sequences are not preempted (no KV swap or recompute).
-
Triton dynamic batching groups requests by shape. A batch's requests are keyed by the shape of their rows and each key's requests run together in arrival order, instead of only consecutive requests of one shape. A ragged batch (
allow_ragged_batch) of two widths runs as two executions: 216 requests ran in 74 to 76 executions instead of 119 to 134. The model parametergroup_by_shape: "false"keeps the old grouping. -
KV pools are updated in place.
toStablehlotakesoutputAliases, and every serving body the artifact writer emits marks eachKV_POOL_INparameter withtf.aliasing_outputnaming itsKV_POOL_OUT(the manifest'sdonationPairs). The Triton backend donates the pools (and anystate:buffer) to each execution, so XLA writes them where they are instead of copying 44 MiB (TinyLlama) or 224 MiB (Qwen3-0.6B) per run. The backend checks every pool's device address after each run and logs it;verify.shrequires 100 of 100 runs in place for both models and runs a control with the new model parameterdonate_kv_pools: false, which must be seen copying in 100 of 100 runs and give the same 100 ids. Measured on the GB10 while another process kept the GPU 95% busy, alternating the two servers three times (server-side time of a batch-1 decode step, 235 steps per run): Qwen3-0.6B's fastest step 11.5 ms in place against 16.2 ms copied, medians 13.1 to 23.8 ms against 25.8 to 32.5 ms; TinyLlama's fastest step 18.3 ms against 19.3 ms, with medians too noisy to separate. Artifacts exported before this change still serve, with the pools copied and a warning in the log;verify.shexports them again. The framework-free Python runtime and the vLLM plugin still pass the pools as host lists. -
PAGED_ATTENTION's two f32 dots are emitted with HIGHEST precision, so XLA does not run them in TF32 on a GPU. -
A config with sliding-window layers, layers without RoPE, output norms or final-logit soft-capping now builds a graph instead of being refused by name;
attn_logit_softcappingis still refused. -
The Triton backend drops the page cache of each weight file after uploading it.
-
HF decoder types renamed for the second family.
HfLlamaConfigis nowHfDecoderConfig,HfLlamaNamesHfDecoderNames,LlamaWeightRoleDecoderWeightRole,LlamaLayerPartDecoderLayerPart,HfLlamaDecodeGraphHfDecoderGraph,HfLlamaCheckpointHfCheckpoint,HfLlamaStagedWeightsHfStagedWeightsandHfLlamaServingExportHfServingExport. The old names remain as deprecated type aliases, so source written against alpha02 still compiles; the binary names changed.HfDecoderGraph.PARTS_PER_LAYERis gone: the number of tensors per layer isDecoderLayerSpec.parts.size, 9 for Llama and 11 for Qwen3. -
RoPE tables sized to the entry. The decode and prefill graphs carry cos and sin tables for the positions the entry can reach (its context, capped by
max_position_embeddings) instead of every position the model supports. -
A tied checkpoint may also store
lm_head.weight.HfCheckpoint.openused to refuse it; it now reads the head from the embedding table, as transformers does, andHfStagedWeightsrefuses the file by name if the stored head differs from the table.
Fixed¶
- Parameter shardings were not emitted.
DxirParam.shardingnever reached StableHLO, so a sharded parameter ran replicated. It is now an argument attribute (#sdy.sharding, as JAX writes it), and a function emitted on its own declares its meshes. - The reverse transform dropped meshes and parameter shardings, so a sharded primal's gradient was unsharded; they now carry over, as in the vmap transform.
- The forward transform kept parameter shardings but not their meshes, so the JVP of a sharded function failed to build ("references undeclared meshes"). It declares them now.
-
A first
executeStablehlocall with the wrong number of buffers reached PJRT. The execute context was sized by that call. The count is now checked against the module's entry function. -
Softmax collapse (arXiv 2610.31642): past a logit margin of 16.6 in f32 (36.7 in f64)
logSoftmaxgave 0 at the maximum and the cross-entropy gradient 0 at a confident target.LOG_SOFTMAXcomputes(x − max) − log1p(S), and the softmax gradients write the dominant entry from the others. The cross-entropy coarsener's gradient assumed labels that sum to one. -
A sequence-mode model refused a turn after a long request. Triton counts a sequence idle while the backend holds its request, so a prompt or generation that ran longer than
max_sequence_idle_microsecondsended its own sequence (four users at 100K on Qwen3.8-27B: a 576 to 768 s first turn under a 600 s timeout).TritonModelRepositorywrites the timeout asmaxSequenceIdleMicrosplusmaxRequestMicros(-PmaxRequestMicros, default one hour), and the backend logs a request that outlived it. -
Named arguments given in another order than the parameters' were read by position inside
grad {}and the other intrinsics:x.conv2d(w, strideW = 2, strideH = 1, …)foldedstrideWintostrideH. Every op (conv2d,convTranspose2d, the pools,batchNorm,clip, …) now reads arguments by the parameter they bind to. -
Gradient synthesis refuses a templated
BROADCASTwhose shape disagrees with its template instead of giving it the template's type. No existing gradient emits one. -
Bounded manifests: the Python reader accepted a blank name, a
DATAtensor naming a bound, a manifest without aDATAinput or without outputs, an axis of size 0 and entry paths outside the artifact; the Triton backend accepted four of these and the Kotlin reader the paths. All three readers are tested against one set of fixtures (harness/bounded-manifest-conformance). -
BoundedProgramlost or duplicated traces when threads shared it; it is now safe to share.clearTraces()drops the traces an exact-size loop accumulates. -
A serving artifact's
modelHash(and each entry'scacheKey) did not cover the weights, so a fine-tune exported under its base model's name and hash had the base's hash. With staged weights it now ends in:weights-and a digest of the weight files. -
clip,maximum,minimum,whereand the comparisons (gt, …) insidegrad {}did not synthesize on a rank-4 tensor: their mask was out of synthesis scope above rank 3. An earlier entry below claimed rank 4 was fixed; only rank 3 was. -
DropoutrefusedPrecision.MIXED_BF16(its mask was an f32 constant in a bf16 region). -
jvpoftanh,sigmoid,tan,atan,poworrsqrtafter a rectangular matmul overSymaxes failed at run time ([2, 3]vs[2, 4]): the forward rules' constants were sized by matching axes against the parameters. They are now broadcast against a value of the right shape. -
The StableHLO emitter refused a one-element input under the empty broadcast form, which the gradient of a one-row softmax produces (
[1, 1] → [1, 10]); it is now a splat, as in the interpreter. -
Five
grad {}bodies that did not compile or failed at run time, at F32 and F64 (GradSurfaceFixesTest):- tensor
sinandcoswere not lowered; -
a comparison mask on a rank-1 (or rank-3) operand did not synthesize (
where(x gt c, …)over a vector); -
the
tanhadjoint after a shape-changingreshapesplatted its constant over the parameter's shape (elementwiseBroadcast: shapes [3, 4] and [4, 3]at run time): a shaped constant with literal dims now keeps them; -
a rank-1 named
contractwhose value the gradient needs had no synthesis arm (DOT);dotRank1is its host twin; -
transpose()dropped axis names, so a transposed named operand could not be contracted.
- tensor
-
A
grad {}over a Double loop that closes through the C9 or C7 closed form read the constant coefficients throughFloat(0.8770684471477485 for 0.8770685575110583). -
In an f64 StableHLO graph, the avg-pool gradient's scale (1e-8 off on the GB10), the LayerNorm/RMSNorm epsilon, the attention scale 1/√dₖ and the paged-attention scale were printed as Floats.
-
A JVM that closed its last
PjrtSessioncould crash at exit (SIGSEGV in libc's exit handlers): closing the session unloaded the PJRT plugin. Seen with a session opened on a Spark executor thread.PjrtSessionnow keeps each plugin loaded for the life of the process. -
Synthesis of a two-operand
BROADCAST(a splat or stretch against a shape template) takes the template's IrType. It fell back to the call's first tensor parameter, so agrad {}body whose first parameter had another rank (a vector of hyperparameters before a matrix) was rejected when its gradient held such a broadcast. -
The gradient of a broadcast that adds axes and also stretches a size-1 axis (
[3, 1] → [2, 3, 4]) summed only the added axes, so the input's gradient came back with the wrong number of elements (12 for a 3-element input).BroadcastRulenow sums the stretched axes too; under symbolic extents (grad { }) it finishes with aSUM_TOagainst the input's runtime shape. -
verify.shleft an int8 Qwen3 container running when that step failed. Its exit cleanup did not name the int8 container; it now names it and the new wrong-axis control's. -
The lone-sequence wait limit of
sequence_checks.py --batchingsat inside its own noise. On an idle GPU a lone TinyLlama sequence waited 0.30 to 0.51 ms a step on average over seven runs, and oneverify.shrun failed at 0.506 ms against the 0.5 ms limit. The limit is now 0.8 ms, between that spread and the 1.2 ms the Triton-batched control waits, which must still fail. -
A sequence whose requests were being refused could lose its pages. The Triton backend counted a sequence as active only when one of its requests ran, while Triton restarts its idle timer for every request, refused or not. A sequence sent refused requests (a token outside the vocabulary, a placeholder id, a context too long) for longer than the reclaim limit was reclaimed while Triton still held it, and its next step was refused for having no KV state. Every request of a held sequence now counts, and a sequence with any request in the batch is not reclaimed.
sequence_checks.py --queuedsends one sequence a refused request every 50 ms while new sequences run the pool out; it failed before the fix and passes after. -
A refused START could leave an earlier sequence's KV to be continued. Triton ends the earlier sequence with that ID at a second START even when the backend refuses the request, but the backend kept its KV, and the next step continued it. The backend now frees it (
restart-refusedinsequence_checks.py, which failed before the fix). -
A request of several calls that failed part way left its sequence inconsistent. When a prompt split into several calls failed after one of them ran, the sequence kept the tokens that ran and the error did not say so; with a windowed ring the positions the first call read may already be written over. The sequence is now freed and the error says so. No check makes a later call fail on demand, so this path is not exercised.
-
An output written device to device into CUDA shared memory could arrive empty. A device-to-device
cudaMemcpyreturns before the copy is done, and the backend sent the response (and let PJRT free the result) without waiting for it. With the GPU busy with another process,matmul_sumsq's outputs came back as zeros in two of three runs ofverify_client.py; the backend now synchronizes after the copy, and six runs out of six pass on the same busy GPU. -
The Triton backend no longer frees a sequence Triton still holds. In sequence mode the backend freed the pages of any sequence it had not run for
max_sequence_idle_microseconds, counted from the end of its last execution. Triton counts from a request's arrival, and a request waiting in its queue keeps the sequence alive, so under load a step Triton accepted was refused for having no KV state (a 200 ms timeout and 12 to 32 concurrent TinyLlama sequences: 3 to 16 such refusals a run). The backend now frees idle sequences only when a sequence needs pages the pool lacks, never one with a request in the batch being run, and only after twice the timeout plus the longest queue wait the oldest strategy allows.verify.shruns the case (sequence_checks.py --queued); a backend without the fix fails it.
0.1.0-alpha02 — 2026-09-24¶
The Kotlin 2.4 release: the compiler plugin supports Kotlin 2.4.20–2.4.29. Stay on
0.1.0-alpha01 for Kotlin 2.3.x.
Changed¶
- Kotlin 2.4.20. Tlaloc is built against Kotlin 2.4.20 and its compiler plugin
supports Kotlin 2.4.20 through 2.4.29; any other Kotlin is refused by name at
compile time. Projects on Kotlin 2.3.20–2.3.29 stay on
0.1.0-alpha01. The plugin's refusal also reaches a user of an older compiler as a compile error: under Kotlin 2.3 it is reported through the message collector, because the diagnostic API Kotlin 2.4 recommends does not exist there. docs/COMPATIBILITY.md lists the Kotlin range of each Tlaloc version. - IR-phase messages are compiler diagnostics. The compiler plugin's IR-phase
refusals and warnings are reported at the call site through the compiler's
diagnostic reporter, with unchanged text and severity, and a warning can be
silenced with
@Suppressnaming it (for exampleIR_LOWERING_REFUSED_WARNING). The errors cannot be silenced that way;strictLowering=falseis still the only way to compile a call the plugin refuses. - ABI baseline for every published module. The committed
api/*.apibaselines are checked by the Kotlin Gradle plugin's ABI validation (checkKotlinAbi,updateKotlinAbi) instead of binary-compatibility-validator, and now also covertlaloc-runtime-pjrt,tlaloc-runtime-cuda,tlaloc-kptx,tlaloc-runtime-ireeandtlaloc-compiler-plugin. - Symja 3.2.0. The build compiles and tests
tlaloc-iragainstorg.matheclipse:matheclipse-core:3.2.0, and the compiler's refusal for a symbolic-trip-count loop now names that coordinate. Symja stayscompileOnly(not in a consumer's dependency graph). The 0.1.0-alpha01tlaloc-irjar calls the same 36 Symja methods and fields, so it also runs with 3.2.0 on the compiler plugin classpath. - Gradle 9.7.1. The repository's wrapper moves from 9.5.0 to 9.7.1; the Tlaloc Gradle plugin is tested with it.
tlaloc { dumpGradSourceDir }writes one subdirectory per compilation. The Gradle plugin writes each Kotlin/JVM compilation's gradient sources into<dumpGradSourceDir>/<source set>(main,test;jvmMain,jvmTestin a Multiplatform build) instead of intodumpGradSourceDiritself, and declares that subdirectory an output of the compile task. The build cache now stores and restores the dumped files, and the directory's absolute path no longer enters the compile task's cache key, so a project that sets it gets cache hits from a checkout at another path. A build that reads the files fromdumpGradSourceDirdirectly now readsdumpGradSourceDir/main. The raw-P plugin:io.tlaloc.plugin:dumpGradSourceDir=<dir>option is unchanged.- API documentation. KDoc in every published module describes the code
without internal changelog numbers, plan phases or planning-document names;
./gradlew testfails if one comes back (scripts/check-kdoc-internal-refs.py).
Fixed¶
- Vendored Maestro server builds.
third-party/maestro/maestro-servercompiles: it declares its dependency onmaestro-tlaloc, and its Spring configuration builds the Tlaloc step runtime with the pod-spec builder and the cluster accelerator fromTLALOC_CLUSTER_VENDOR/TLALOC_CLUSTER_ARCH. The vendored build's Spotless, Checkstyle and PMD checks run on JDK 25 with Gradle 9 (Spotless 8.10.2, google-java-format 1.30.0, Checkstyle 9.3).
0.1.0-alpha01 — 2026-09-24¶
The first published version. docs/CAPABILITIES.md lists what it contains, what is certified and on what hardware.
Contents¶
- Automatic differentiation at compile time. A K2 compiler plugin rewrites
grad,grad2,grad3,valueAndGrad*,jvp,jvp2,vjp,vjp2,jacobian,jacobianReverseandhessiancalls into synthesized gradient code. Custom rules throughcustomVjp,customJvpandcustomVjpJvp. Loops and branches are differentiated through φ-calculus coarsening. One reverse transform serves the intrinsics, the Tracer-capture API and:nntraining. - Readable gradients.
dumpGradSourceanddumpGradSourceDirprint the derived gradient as Kotlin that compiles without the plugin and is bit-identical to the compiled gradient;CapturedStep.gradSource()prints captured tensor gradients. - Typed tensors. Rank, dtype and named axes in the Kotlin type;
NAMED_INDEX_MISMATCH,TENSOR_SHAPE_MISMATCHandNOT_DIFFERENTIABLEcompile errors at the offending call. dtypes F32, F64, I32 and BF16. - Captured values. A
grad { }body may reference compile-time constants declared outside it (folded as literals) and immutable runtime values of typeFloat,Double,IntorLong(bound at the call site). :nn. Immutable layers (Dense, Conv2d, pooling, BatchNorm, Dropout, Embedding, EmbeddingBag, GRU, Flatten), SGD, Momentum, RMSprop and Adam, learning rate schedules, gradient clipping, and checkpoints: a model and its optimizer state in one safetensors file, reloaded bit for bit.- Execution. StableHLO and Shardy emission; PJRT from Kotlin through FFM and from Python through ctypes; IREE through its command-line tools; KPTX, a PTX DSL with kernel claiming (no kernel is registered by default).
- Serving. Paged attention, KV-cache writes, decode bucketing, safetensors reading and writing, a serving artifact that runs in a Python process with no JVM and no ML framework, and a vLLM platform plugin. A real TinyLlama-1.1B produces the same tokens as HuggingFace transformers.
@ExperimentalTlalocApi, a@RequiresOptIn(ERROR)marker on the provisional surfaces: the four-worlds scopes,AllReduceAttrs, and the kernel choice and cost-model packages.- Tooling. The Gradle plugin
io.github.pedronahum.tlaloc(tlaloc-gradle-plugin), which appliestlaloc-compiler-pluginof the same version to every Kotlin/JVM compilation and takes the plugin options in atlaloc { }block;tlaloc-bom; a committed ABI baseline checked byapiCheck;./gradlew apiDocsfor a local API reference. - Requirements. Kotlin 2.3.20–2.3.29 (other versions are refused by name at
compile time). JDK 25 to build code that uses
grad { }; the library modules are Java 21 bytecode, so JDK 21 runs them. PJRT, CUDA, KPTX and IREE need JDK 25. Symja (LGPL-3.0) is optional and not in the published dependency graph.
Changed since earlier source builds¶
These matter only if you built Tlaloc from source before this version.
- Group id
io.tlaloc→io.github.pedronahum.io.tlaloccould not be verified on Maven Central. Package names are unchanged (io.tlaloc.*), and so is the compiler plugin id used in-P plugin:io.tlaloc.plugin:<option>. - Version
0.0.1-SNAPSHOT→0.1.0-alpha01. - Every artifact id carries a
tlaloc-prefix:tlaloc-core,tlaloc-ir,tlaloc-autograd,tlaloc-nn,tlaloc-stablehlo,tlaloc-maestro,tlaloc-runtime-pjrt,tlaloc-runtime-iree,tlaloc-runtime-cuda,tlaloc-kptx,tlaloc-compiler-plugin(JVM artifactstlaloc-core-jvmand so on), plus the newtlaloc-gradle-pluginandtlaloc-bom. tlaloc-ir,tlaloc-autograd,tlaloc-stablehlo,tlaloc-maestro,tlaloc-runtime-ireeandtlaloc-runtime-pjrtexpose the Tlaloc modules their public signatures use asapidependencies:tlaloc-autogradalone is enough to writegrad { }over aDTensor.- Every compile-time refusal is an error by default. An unlowerable
grad { }lambda, and every call the IR phase leaves as written, stops the build at the call site.strictLowering = falseturns these into warnings; the call then throwsIllegalStateExceptionwhen it runs. - A working build is silent. The lowered-IR dumps are INFO messages behind
dumpLoweredIr, so a correct program compiles under-Werror. grad(::f)and other arguments that are not a lambda written at the call site are refused by name.dumpGradSourcerefuses a value other thantrueorfalse.- The runtime message for a
grad { }that was not rewritten lists its three possible causes. - Error messages no longer cite internal work-item numbers.
PjrtSessioncan be used from several threads.executeOnserialises calls on one executable, andclose()waits for calls in flight. APjrtBuffer,PjrtLoadedExecutableorPjrtClientused after its client is closed, and a closedPjrtBufferpassed toexecuteOnorexecute, throw instead of touching freed memory. Closing twice, or after the client, does nothing.IreeModuleisAutoCloseable; closing it deletes its compiled VMFB.runOnIreeandIreeRuntime.invokedelete their temporary files.- The PJRT CUDA plugin is searched for under
$VIRTUAL_ENV,~/.local/venvs/*,~/.venv,~/venv,~/.local,/usr/localand/usr, from the JVM and from Python alike; IREE tools under$TLALOC_IREE_BIN,$VIRTUAL_ENV/bin,~/.local/venvs/*/binandPATH. A failed search lists every place it looked. - In the Python serving runtime (
harness/python, not published to Maven),PjrtApi.load(plugin_path=None, platform="cuda")takes aplatformand, with no path and noTLALOC_PJRT_PLUGIN_PATH, searches for a plugin; when none is found it raisesFileNotFoundErrorwith the search report, where it used to raiseValueError. CosineDecayholds its final rate pastdecaySteps, where PyTorch'sCosineAnnealingLRrises again.
Added since earlier source builds¶
- The Gradle plugin and the BOM (see Contents).
KotlinVersionGuard: an unsupported Kotlin version is a compile error naming the version found and the supported range;unsafeAllowUnsupportedKotlinturns it into a warning.- "Tlaloc internal error" diagnostics: an unexpected exception inside the plugin, or a lowered call the IR phase did not find, is reported at the call site with the issue-tracker address instead of crashing the compiler or compiling green.
- Model checkpoints, learning-rate schedules and gradient clipping in
:nn; a safetensors writer in:core. LICENSE(Apache-2.0), complete Maven Central POM metadata, sources and javadoc jars for every publication, and signing../gradlew releaseToCentralPortaland.github/workflows/release.yml.- CI on x86_64 Linux, aarch64 Linux and arm64 macOS, a JDK 21 lane for the library modules, and a probe against the next Kotlin release.
- docs/GETTING_STARTED.md sections on running on a GPU, configuration (every environment variable and system property Tlaloc reads) and troubleshooting.
Removed¶
io.tlaloc.maestro.MaestroDescriptorandio.tlaloc.maestro.StubExecutor. The first-class Maestro step type that replaces them lives in the vendored Maestro build underthird-party/maestro, which is not published: a Maven Central user has no replacement.- The
mainentry points intlaloc-maestro; the exporters run through./gradlew :maestro:exportServingArtifactand:maestro:exportLlamaServingArtifact. - The unused
timeoutSecondsparameter ofrunOnPjrt.
Fixed¶
- Two
grad { }calls at the same character offsets in two different files could compile each other's gradient, and in a shared Kotlin daemon one compilation could clear another's pending gradients. - An f64
grad { }body with a literal constant crashed the compiler with aClassCastException, and an f64 constant close to 1 or 0 could be folded as if it were exactly 1 or 0. - A
grad { t: Tracer<…> -> … }call (the plugin-free tape overload) drew a spurious lowering warning. - The Python serving runtime did not search for a PJRT plugin, so
serve.pyreported no plugin on a machine whose JVM lane ran on CUDA. - Both PJRT bindings check the plugin's
PJRT_Apisize and API version before reading a function pointer and refuse an incompatible plugin by name. - A malformed
TLALOC_PJRT_MEMORY_FRACTION,TLALOC_PJRT_PREALLOCATE,TLALOC_PJRT_NODE_IDorTLALOC_PJRT_NUM_NODESis refused with the variable's name and value. - A
PjrtSessionwhose client creation fails releases the memory it allocated. - Symbolic simplification no longer swallows JVM errors such as
OutOfMemoryError. - The KPTX kernel caches and the kernel-resolver registry are safe to use from several threads.