Chapter 8. Day 4, second block, 55 minutes

The TPU serving stack

Chapter 7 derived what a serving system should do. This chapter is about the software that does it on TPU, which changed three times in 18 months. It ends with the Scaling Book's serving case study.

How TPU serving got here

  • JetStream (2024–2025). Google's engine, with prefill and generate engines on separate slices and an interface of prefill, insert and generate [SB7]. Archived on 2026-02-01; its functions moved to vLLM TPU [R-jetstream].
  • vLLM on PyTorch/XLA (to 2025). vLLM's first TPU backend ran PyTorch through PyTorch/XLA's lazy tensors. PyTorch/XLA is now deprecated, and its repository is archived on 2026-11-15 [T-xla].
  • tpu-inference, a unified JAX backend (October 2025). One lowering path for all models: JAX-native models where they exist, and upstream vLLM PyTorch models through torchax otherwise. Against the PyTorch/XLA backend, Google reported 3.6× on Llama-3.1-8B on one v6e chip and 2.1× on 70B on eight. About 20% of the gain came from JAX lowering and about 10% from Ragged Paged Attention v3 over v2 [B-vllmtpu].
  • The split (October 2026). tpu-inference is being renamed vLLM JAX and focuses on JAX (Flax NNX) models. PyTorch model definitions get vllm-torchtpu, described as "the recommended path for serving PyTorch model definitions on TPU". SGLang made the same split [R-tpuinf] [T-engines].
Upstream vLLM v1API server, scheduler (continuous batching), KV block manager, prefix cache. Unchanged on TPU.vLLM JAX (tpu-inference)vLLM JAX, torchax pathvllm-torchtpuJAX model (Flax NNX)MODEL_IMPL_TYPE=flax_nnxhand-written for TPU;default when one existsPyTorch model via torchaxMODEL_IMPL_TYPE=vllmupstream vLLM model code;ATen ops mapped to jnpPyTorch model on TorchTPUtorch.compile(backend="tpu")upstream model code;"recommended path" (Oct 2026)jax.jitmodel_fn(weights, kv_caches,inputs) -> (kv_caches, ...)jax.jit of a pure functiontorch.func.functional_call;KV cache passed in, returnedDynamo FX graphone StableHLO program pergraph; no TorchInductorPallas kernels (Mosaic TPU)Ragged Paged Attention v3, MLA, fused MoE (megablox), quantized matmul, SparseCore gather: custom calls in XLAStableHLO → XLA → libtpu (PjRT)whole-step compilation per shape bucket; SPMD partitioning over the mesh; collectives as HLO opsTPU slicev5e, v6e, Ironwood (7x recommended for vllm-torchtpu); multi-host through Pathways or multi-controller

As of 2026-10-23. tpu-inference is being renamed vLLM JAX and focuses on JAX models; its torchax path still exists. New PyTorch-native TPU serving is in vllm-torchtpu. SGLang has the same split (sglang-jax, sglang-torchtpu).

Three paths to a TPU, October 2026. All three share the upstream vLLM scheduler above and Pallas kernels, XLA and libtpu below. They differ in how the model's Python becomes a compiled program. Chapter 9 explains TorchTPU's path in detail.

Which path for a new port.

  • A JAX implementation of the model exists in vLLM JAX: use it. tpu-inference picks it by default (MODEL_IMPL_TYPE=auto resolves to flax_nnx) [R-tpuinf]. These implementations are written for TPU sharding and kernels.
  • You are bringing PyTorch model code: vllm-torchtpu is now the recommended path. It runs upstream vLLM model definitions through torch.compile on TorchTPU, with TPU kernels inserted as custom ops [R-vllm-torchtpu].
  • You have an existing torchax port: it continues to run (MODEL_IMPL_TYPE=vllm). Google's blog found torchax-lowered and hand-written JAX models performed about the same [B-vllmtpu], but new PyTorch work is going to vllm-torchtpu.

The scheduler is vLLM's

Neither TPU backend replaces vLLM's engine. The API server, the v1 scheduler that implements continuous batching, the KV block manager and prefix caching are upstream vLLM [R-tpuinf] [T-engines]. The TPU code supplies a platform plugin, a worker and a model runner. In tpu-inference the runner directory has a tpu_runner, a kv_cache_manager, a block_table, a persistent_batch_manager, a compilation_manager and managers for speculative and structured decoding [R-tpuinf].

torchax: PyTorch code, JAX execution

torchax lets PyTorch model code run as JAX [R-torchax]:

  • A torchax.tensor.Tensor is a torch.Tensor subclass that holds a jax.Array.
  • It overrides __torch_dispatch__. When PyTorch dispatches an ATen operation, such as the aten::mm inside torch.nn.Linear, torchax unwraps the JAX arrays, runs a JAX implementation from its op registry (a jnp or lax call), and wraps the result.
  • Under jax.jit, those JAX calls are traced into a jaxpr like any other JAX code, lowered to StableHLO and compiled by XLA.

A Linear layer becomes a jnp.matmul in a jaxpr, then a dot_general in HLO, then an MXU fusion in the compiled program. JAX shardings, Pallas kernels and Qwix all apply to it.

tpu-inference wraps a PyTorch model in what its developer guide calls a "sandwich" [R-torchax-guide]:

  • The middle is upstream vLLM's PyTorch model code.
  • Around it is JAX: inputs, the sampler, the KV cache and the Ragged Paged Attention kernel.
  • torch_view and jax_view convert at the boundaries.

The worker calls one pure function, model_fn(weights, kv_caches, inputs). PyTorch modules are made pure with torch.func.functional_call, and the KV cache is passed in and returned, functional-update style, rather than mutated in place. The guide's summary: "torchax is JAX".

Continuous batching with static shapes

Continuous batching means the set of sequences in each step changes every step: new requests join, finished ones leave, some are in prefill and some in decode. A TPU program is compiled for exact shapes. tpu-inference reconciles the two in three ways.

Flatten the batch into tokens. Every query token in the step, from every sequence, is packed into one array: queries are [max_num_tokens, num_q_heads, head_dim]. Per-sequence structure is carried in small metadata arrays [R-tpuinf, kernels/ragged_paged_attention/v3]:

  • cu_q_lens, cumulative query lengths that mark where each sequence's tokens start;
  • kv_lens, each sequence's cache length;
  • page_indices, the block table;
  • distribution, which counts how many sequences are decode-only, prefill-only and mixed.

A decode sequence contributes one token and a prefill chunk contributes many, in the same array.

Pad to buckets and compile ahead. The number of tokens and of sequences is padded up to one of a fixed set of bucket sizes. The compilation_manager compiles every bucket before serving starts, so no request waits on a compile [R-tpuinf]. The cost is padding waste, which the bucket spacing controls, and startup time.

Make attention handle raggedness inside the kernel. Ragged Paged Attention (RPA) is a Pallas kernel that reads each sequence's KV pages through the block table and attends over that sequence's true length [P-RPA]. Version 3 adds three things:

  • It fuses the KV-cache update into the attention kernel, which "completely hides scatter latency".
  • It compiles three specialized sub-kernels, for decode-only, prefill-only and mixed batches, chosen by the distribution metadata [B-vllmtpu].
  • The paper reports up to 86% of memory bandwidth in decode and 73% of FLOPs in prefill on Llama-3-8B on Ironwood [P-RPA].

The KV-cache layout

Per layer, tpu-inference allocates the cache as [R-tpuinf, runner/kv_cache.py]

(num_blocks, block_size, ⌈2K/p⌉,  p,  head_dim),p=32bits per value(8.1)\text{(num\_blocks, block\_size, } \lceil 2K / p \rceil, \; p, \; \text{head\_dim)}, \qquad p = \frac{32}{\text{bits per value}} \tag{8.1}

Pages of tokens; within a page, K and V heads interleaved and packed p to a 32-bit word.

K and V are interleaved along the head axis (the factor 2), then grouped in fours for 8-bit values or pairs for bf16. The packing dimension exists because of Chapter 2's tiling. XLA stores bf16 as (8,128)(2,1) and 8-bit types as (8,128)(4,1), two or four values per 32-bit sublane [X-tiled]. Laying the cache out that way lets the kernel load whole 32-bit words that already hold the right K and V values. The DMA then moves full tiles, and no relayout is needed between the cache and the MXU.

Sharding in tpu-inference

tpu-inference defines its mesh axes as data, attn_dp, attn_dp_expert, expert, model, dcp and pcp. The default is a 2D mesh of ('data', 'model'), in which attention heads, KV heads, the MLP's tensor dimension and experts all shard over model, and the batch over data [R-tpuinf, layers/common/sharding.py]. That is Chapter 7's decode sharding:

  • model parallelism over model;
  • replicas over data;
  • dcp and pcp for decode and prefill context parallelism when sequences are long;
  • expert for expert parallelism. The source does not document attn_dp; its name suggests data parallelism for attention, the usual answer when a model has fewer KV heads than the model axis has chips unverified.

Multi-slice deployments set NUM_SLICES.

Quantization and MoE

tpu-inference loads checkpoints already quantized in several formats: MXFP4, compressed-tensors FP8 and INT8, AWQ INT4 and NVIDIA ModelOpt NVFP4. It can also quantize JAX models at load time with Qwix rules (Chapter 7) [R-tpuinf]. For MoE it carries Pallas kernels for fused MoE and grouped matmul (megablox), and SparseCore kernels for the ragged gathers and scatters of token routing [R-tpuinf, kernels/].

vllm-torchtpu provides the same kernels as custom ops inside its compiled graph [R-vllm-torchtpu] [T-engines]:

  • RPA, dense and sparse MLA, Gated DeltaNet, and fused MoE with SparseCore offload;
  • FP8, INT8 and MXFP4 quantized matmuls and FP8 KV cache;
  • TP, DP, PP, EP and context parallelism;
  • disaggregated prefill and decode through TPU Sync, which reads the device buffers behind PyTorch tensors directly;
  • KV offload to host DRAM or Managed Lustre.

Google also describes a Pallas "megakernel" that runs a whole decode step of Kimi K3 as a single kernel.

Case study: serving LLaMA-3-70B

The Scaling Book's Chapter 8 works through serving LLaMA-3-70B on v5e, chosen because it was the cheapest TPU per FLOP at the time [SB8]:

Chipbf16 FLOP/s$/hour (Feb 2025)FLOPs per $
H1009.9e1410.83.3e17
v5p4.59e144.23.9e17
v5e1.97e141.25.9e17

The book's steps, with int8 weights and bf16 math:

  • KV cache: 2⋅8⋅128⋅80=1642 \cdot 8 \cdot 128 \cdot 80 = 164 kB per token in int8; the book rounds to 160 kB. A batch of 32 at 8k context needs 113 GB with the weights, so 7 chips minimum: a 4×2.
  • Step time on a 4×2: 113 GB over 8 × 820 GB/s = 17 ms, 1,860 tokens/s, about 232 tokens/s per chip. On a 4×4: 8.6 ms, same per-chip throughput. The book gives 17 ms and 235.
  • Sharding check: bf16 weights on a 4×8 at batch 64 give TICIT_\text{ICI} = 12 µs, THBMT_\text{HBM} = 18 µs and TmathT_\text{math} = 5 µs. HBM-bound, so 32-way model parallelism lowers latency without raising throughput.
  • The smallest slice is not the cheapest. At 8k context the KV cache, not the weights, limits batch. Doubling the slice more than doubles the batch that fits and raises throughput per chip by about 1.6× in the book's table.
  • Latency against cost. In the book's interactive plot, doubling per-token latency can cut cost per token by about 100×, and at any context above 2,048 tokens KV loading takes longer than FLOPs [SB8].
Worked example: LLaMA-3-405B under 15 ms per token on v5e (derived; no book answer)

The Scaling Book's Chapter 8, problem 3, gives no answer [SB8]: serve LLaMA-3-405B on v5e with int8 weights and bf16 math, at most 15 ms per token, with maximum throughput. Assume 8k context and int8 KV, 258 kB per token. From scripts/ch08_serving.py derived:

  • Floor: reading 406 GB of weights must take under 15 ms, so at least 36 chips (13.8 ms). That leaves room for only about 17 sequences.
  • 64 chips (an 8×8): the floor is 7.7 ms. Formula 7.2 allows a batch of about 143 under 15 ms.
  • The ICI check binds first. At 64-way model parallelism, Chapter 7's condition Y<F/(Bβici)Y < F/(B \beta_\text{ici}) with F=53,248F = 53{,}248 and βici≈8\beta_\text{ici} \approx 8 caps the batch near 100. A v5e 8×8 slice also has no wraparound, which makes those collectives slower still.
  • At batch 100: a 4.0 ms KV read plus the 7.7 ms weight read, about 12 ms per step, about 130 tokens/s per chip.
  • Scaling out: at 128 or 256 chips, throughput per chip levels off near 149 tokens/s under the deadline, since the step becomes compute-bound. Fewer, larger replicas only add ICI pressure. The answer is replicas of 64 chips each.

For comparison, the same model in fp8 on 8 Ironwood chips has a weight-read floor of 6.9 ms; on 16 chips, 3.4 ms.

Sources