Phase 13 / Pallas kernels
Tiling, memory, and pipelining
Revision v0.7.0 · Updated since your last visit. Review the changes; your progress is saved.
Lesson · about 120 minutes · CPU practice
Execute actual emit_pipeline semantics under explicit CPU TPU-layout simulation. Compare synchronous and buffered schedules against an independent oracle.
Phase study guide: preparation, milestones and project evidence
Before you start
Before this: A first TPU kernel
Hardware: CPU Pallas interpretation; actual TPU lowering and performance unverified
Setup help: Python, notebook, or Cloud TPU VM
Use the tested CPU packages (the recorded environment uses Python 3.14.3). Other Python versions and accelerator environments require separate checks. The optional Cloud TPU specialization explains the target-specific execution boundaries. Logical CPU devices practice partitioning; they do not simulate TPU performance.
Download the repository ZIP or use your existing checkout. Extract it and open a terminal in the folder containing requirements-cpu.txt. The beginner ZIP is separate; these lessons use the full repository folder.
python3 -m venv .venv
source .venv/bin/activate
python -m pip install -r requirements-cpu.txtpy -3 -m venv .venv
.\.venv\Scripts\python.exe -m pip install -r requirements-cpu.txtAfter activation, run the complete worked example from that same folder:
python phases/13-kernels/03-tiling-memory-and-pipelining/code/main.py.\.venv\Scripts\python.exe phases/13-kernels/03-tiling-memory-and-pipelining/code/main.pyFor an incremental build, create main.py in your own practice folder, append each build block in order, and run python main.py with this environment active. Keep that file separate from the downloaded reference solution.
Download this lesson’s notebook. Open it with a Python kernel using the course environment. Run cells from the top after restarting the kernel; experiments depend on earlier definitions.
For local Jupyter, first install the course requirements using the Local Python steps, then use your environment’s Python to start Jupyter:
python -m pip install jupyterlab
python -m jupyter lab.\.venv\Scripts\python.exe -m pip install jupyterlab
.\.venv\Scripts\python.exe -m jupyter labSelect the downloaded kernels-03.ipynb file in JupyterLab. The notebook contains reference solutions; make your prediction and attempt each practice task before opening them. Restart the kernel before changing logical device setup.
The worked reference for this lesson is CPU-validated. Some references explicitly configure logical CPU devices; setting JAX_PLATFORMS does not adapt them to a TPU. Follow the execution matrix and reviewed multi-host example before attempting a target run.
For accelerator preparation, study TPU setup, observed runtime receipts, checkpointed jobs and precision and retained traces. These target paths remain unqualified on physical TPU hardware until an actual receipt is recorded. Cloud provisioning is optional for CPU learning.
Check: Synchronous and buffered CPU simulations agree with NumPy and each other; endpoints -1.75 and 2.25. Tile program counts [9,6,4], padded elements [9216,12288,16384], modeled data buffers [24576,49152,98304] bytes.
Revisit the guided CPU setup · Open 19: TPU Setup. Package instructions are CPU instructions; Windows and accelerator environments need their own validation.
What you will be able to do
- Execute actual emit_pipeline semantics under explicit CPU TPU-layout simulation.
- Compare synchronous and buffered schedules against an independent oracle.
- Explain tile count, padding and modeled local-buffer footprint separately.
- Distinguish correctness of scheduling from measured transfer/compute overlap.
The problem
A larger tile reduces the number of kernel invocations, but it also consumes more local memory and may compute more padded values. Can we make those tradeoffs concrete and execute a real buffered pipeline without claiming that CPU simulation measures TPU overlap?
The idea
Pipelining overlaps stages such as loading, computing and storing by giving work separate buffers and a valid schedule. The key question is when each buffer becomes safe to reuse, not merely how many buffers exist.
A buffer cannot be reused while a consumer still needs it
Draw time across the page and a separate lane for load, compute and store. Label buffers by identity and mark the last use of each version. Loading new data too early can overwrite values still needed by computation.
Larger tiles may reduce program count while increasing local storage and per-program work. The lesson's bars expose those modeled tradeoffs. Fewer programs alone is not proof of lower runtime.
Keep illustrative overlap separate from measured overlap. The CPU interpretation and race checks validate bounded aspects of the schedule; a device trace and completed measurements are needed to claim TPU performance. Buffer-count changes should be checked against both correctness and the actual target constraints.
Keep one numerical operation while changing data movement
The arithmetic body remains unchanged from the first lesson. The outer kernel receives entire buffers in a global-memory space, and the inner pipeline sees block-local references. This separation lets us change the movement schedule without changing the mathematical result. If a new schedule changes values, it is a correctness regression rather than a performance tradeoff.
What buffering is trying to overlap
A pipeline can start bringing the next tile into a different buffer while the current tile is being processed. Reusing a buffer too early risks overwriting data still being read; consuming it before its transfer completes risks stale or uninitialized data. The emitter manages copies and waits. We compare two versus three input slots while keeping two output slots: installed JAX 0.9.2 rejects output buffer counts above two. A third input slot consumes more local memory and may or may not hide additional latency.
Use the appropriate interpreter for memory operations
Ordinary interpret=True is enough for the first lesson’s arithmetic grid, but does not supply all TPU layout information required by this installed pipeline API. Here pltpu.InterpretParams simulates memory spaces and synchronization; an AbstractMesh specifies a simulated TPU v5 lite layout. The actual backend remains CPU. This is JAX 0.9.2 API usage, and its signature should be rechecked when upgrading. The simulated device label must never appear as the actual device in a performance receipt.
Use synchronous execution as a debugging comparison
no_pipelining=True requests synchronous copies through the same pipeline. Agreement with the buffered result helps isolate scheduling errors while preserving arithmetic and windows. The interpreter enables race detection, but passing these cases does not prove every target interleaving, layout or compiler path. If a TPU interpretation exception occurs, the documented interpreter state-reset procedure is required before reusing that mode in the same process; fresh-process checks avoid carrying failed state into later evidence.
Count the cost of a tile choice
For a logical matrix, blocks , and use , and programs. Their padded work grows from to to elements. The corresponding modeled double-buffered data storage grows from to to KiB for two inputs and one output in float32. Fewer programs therefore does not automatically mean less work or lower memory use.
Only target measurements can rank pipeline speed
CPU simulation may execute callbacks, bookkeeping and race tracking that have no relation to TPU throughput. Do not time it and call the result a kernel benchmark. The project’s target runner can execute the actual synchronous or buffered TPU branch without fallback; its correctness checks and synchronized baseline comparison must pass before interpreting latency. No such target performance receipt is available here.
Fewer tile programs can require more work and local storage
Predict before looking: Will the tile configuration with the fewest programs also minimize padding and buffer footprint?
Read the figure
The first panel shows logical kernel-program counts for three tile shapes on the same matrix: , and . The second panel shows modeled double-buffered data storage: , and KiB. These are exact configuration-derived counts, not measured timing or device-memory counters.
The largest tile has fewer programs but four times the modeled data-buffer footprint of the smallest. Its padded element count also rises from to , even though the logical matrix still contains only elements.
Connect it to the computation
Every pictured configuration also runs through the actual Pallas pipeline in CPU simulation and matches the same NumPy result. The figure explains the resource tradeoff that remains after correctness is established.
The footprint omits semaphores, bookkeeping, layout padding, registers and compiler scratch. It is distinct from total VMEM use or speedup. A real TPU trace and synchronized timing are needed to learn whether a larger tile improves reuse enough to offset its costs.
Open full-size figureDownload data and provenance
The notebook contains the data experiment, plotting code, and recorded figure output. Rerun it to test your changes.
Where to put & run this code: Work in phases/13-kernels/03-tiling-memory-and-pipelining/code/main.py (or downloaded exercises/kernels-03.py / exercises/kernels-03.ipynb) and execute from the repository root with python3 phases/13-kernels/03-tiling-memory-and-pipelining/code/main.py.
1 Reuse the checked arithmetic and add a real pipeline
Create main.py for the first block, then append each block in order in your course CPU environment.
# Step 1 — Reuse the checked arithmetic and add a real pipeline: The outer kernel receives global-memory references and emits an...
import jax
import jax.numpy as jnp
import numpy as np
from jax.experimental import pallas as pl
from jax.experimental.pallas import tpu as pltpu
def validate_pair(x,y,block):
# Guard input contract (`x.ndim != 2 or x.shape != y.shape or min(x.shape) < 1`) and fail fast if violated.
if x.ndim!=2 or x.shape!=y.shape or min(x.shape)<1:
raise ValueError("inputs must be nonempty matrices with identical shapes")
# Guard input contract (`x.dtype != y.dtype or x.dtype not in (jnp.float32, jnp.bfloat16)`) and fail fast if violated.
if x.dtype!=y.dtype or x.dtype not in (jnp.float32,jnp.bfloat16):
raise ValueError("matching float32 or bfloat16 inputs required")
# Guard input contract (`len(block) != 2 or any((not isinstance(n, int) or n < 1 for n in block))`) and fail fast if violated.
if len(block)!=2 or any(not isinstance(n,int) or n<1 for n in block):
raise ValueError("two positive integer block dimensions required")
def pad_pair(x,y,block):
validate_pair(x,y,block)
m,n=x.shape
bm,bn=block
padded=((m+bm-1)//bm*bm,(n+bn-1)//bn*bn)
# Combine or mask array elements to form `pads`.
pads=((0,padded[0]-m),(0,padded[1]-n))
return jnp.pad(x,pads),jnp.pad(y,pads),padded
def axpy_body(x_ref,y_ref,out_ref):
result=2.0*x_ref[...].astype(jnp.float32)+y_ref[...].astype(jnp.float32)
out_ref[...]=result.astype(out_ref.dtype)
def blocked_axpy(x,y,block=(2,4)):
# Combine or mask array elements to form `(px, py, padded)`.
px,py,padded=pad_pair(x,y,block)
bm,bn=block
# Invoke custom Pallas kernel or tile specification (`spec`).
spec=pl.BlockSpec(block,lambda i,j:(i,j))
# Combine or mask array elements to form `out`.
out=pl.pallas_call(axpy_body,
out_shape=jax.ShapeDtypeStruct(padded,x.dtype),
grid=(padded[0]//bm,padded[1]//bn),
in_specs=(spec,spec),out_specs=spec,interpret=True)(px,py)
return out[:x.shape[0],:x.shape[1]]
def pipelined_axpy(x,y,block=(8,128),buffers=2,no_pipelining=False,mode="simulate"):
# Combine or mask array elements to form `(px, py, padded)`.
px,py,padded=pad_pair(x,y,block)
bm,bn=block
# Guard input contract (`bm % 8 or bn % 128`) and fail fast if violated.
if bm%8 or bn%128:
raise ValueError("pipeline blocks must be multiples of (8,128)")
# Guard input contract (`buffers not in (2, 3)`) and fail fast if violated.
if buffers not in (2,3):
raise ValueError("this lab supports two or three buffers")
# Guard input contract (`mode not in ('simulate', 'tpu')`) and fail fast if violated.
if mode not in ("simulate","tpu"):
raise ValueError("mode must explicitly be simulate or tpu")
# Guard input contract (`mode == 'tpu' and (jax.default_backend() != 'tpu' or any((not isinstance(a, jax.core.Tracer) and any((d.platform != 'tpu' for d in a.devices())) for a in (x, y))))`) and fail fast if violated.
if mode=="tpu" and (jax.default_backend()!="tpu" or any(not isinstance(a,jax.core.Tracer) and any(d.platform!="tpu" for d in a.devices()) for a in (x,y))):
raise RuntimeError("Real TPU inputs/backend required; simulation fallback is disabled")
# Invoke custom Pallas kernel or tile specification (`spec`).
spec=pl.BlockSpec(block,lambda i,j:(i,j),pipeline_mode=pl.Buffered(buffer_count=buffers))
# Invoke custom Pallas kernel or tile specification (`output_spec`).
output_spec=pl.BlockSpec(block,lambda i,j:(i,j),pipeline_mode=pl.Buffered(buffer_count=2))
def outer(x_hbm,y_hbm,out_hbm):
# Combine or mask array elements to form ``.
pltpu.emit_pipeline(axpy_body,grid=(padded[0]//bm,padded[1]//bn),
in_specs=(spec,spec),out_specs=output_spec,
no_pipelining=no_pipelining)(x_hbm,y_hbm,out_hbm)
# Invoke custom Pallas kernel or tile specification (`whole`).
whole=pl.BlockSpec(memory_space=pl.ANY)
interpretation=pltpu.InterpretParams(detect_races=True) if mode=="simulate" else False
# Combine or mask array elements to form `call`.
call=pl.pallas_call(outer,out_shape=jax.ShapeDtypeStruct(padded,x.dtype),
in_specs=(whole,whole),out_specs=whole,interpret=interpretation)
if mode=="simulate":
# This describes a SIMULATED TPU layout, not the machine running the code.
abstract=jax.sharding.AbstractMesh((1,),("simulated_device",),
abstract_device=jax.sharding.AbstractDevice(device_kind="TPU v5 lite",num_cores=1))
with jax.sharding.use_abstract_mesh(abstract):
output=call(px,py)
else:
output=call(px,py)
return output[:x.shape[0],:x.shape[1]]Code building blocks in this step
validate_pair(...)— Callvalidate_pairwith your updated parameters or inputs from this lesson's workspace.contract(...)— Callcontractwith your updated parameters or inputs from this lesson's workspace.
The outer kernel receives global-memory references and emits an actual TPU pipeline. BlockSpecs describe its VMEM windows; the pipeline manages buffer copies and waits. CPU execution uses the TPU interpreter with explicitly simulated layout metadata.
2 Compare synchronous and buffered pipeline semantics
Create main.py for the first block, then append each block in order in your course CPU environment.
# Step 2 — Compare synchronous and buffered pipeline semantics: Both schedules execute real pipeline semantics under CPU simulation.
# Construct and reshape `x` into the target tensor dimensions.
x=jnp.linspace(-1,1,17*257,dtype=jnp.float32).reshape(17,257)
y=jnp.full_like(x,.25)
# Convert `reference` to a host NumPy array for inspection or verification.
reference=2*np.asarray(x)+np.asarray(y)
synchronous=pipelined_axpy(x,y,no_pipelining=True)
buffered=pipelined_axpy(x,y,no_pipelining=False)
np.testing.assert_allclose(synchronous,reference,rtol=1e-6,atol=1e-6)
np.testing.assert_allclose(buffered,reference,rtol=1e-6,atol=1e-6)
# Execute `np.testing.assert_array_equal(synchronous,buffered)`
np.testing.assert_array_equal(synchronous,buffered)
print("Actual backend:",jax.default_backend(),"simulated layout: TPU v5 lite")
print("Synchronous/buffered endpoints:",float(buffered[0,0]),float(buffered[-1,-1]))Code building blocks in this step
jnp.linspace(start, stop, num)— Createsnumevenly spaced float points across the closed interval[start, stop].array.reshape(new_shape)— Reorganizes tensor axes without changing the total element count (array.size).jnp.allclose(actual, expected, rtol=..., atol=...)— Checks that two arrays match elementwise within floating-point tolerance.
Both schedules execute real pipeline semantics under CPU simulation. Equality checks copies, writes and tails; it is distinct from that transfers overlap with computation on a real TPU.
3 Quantify tile tradeoffs before measuring hardware
Create main.py for the first block, then append each block in order in your course CPU environment.
# Step 3 — Quantify tile tradeoffs before measuring hardware: This footprint counts only the declared data buffers.
configurations=[(8,128),(16,128),(16,256)]
metadata=[]
for bm,bn in configurations:
gm=(17+bm-1)//bm
gn=(257+bn-1)//bn
programs=gm*gn
padded_elements=programs*bm*bn
# Two input buffers and one output buffer, each double buffered, FP32.
modeled_buffer_bytes=3*2*bm*bn*4
# Combine or mask array elements to form ``.
metadata.append((programs,padded_elements,modeled_buffer_bytes))
result=pipelined_axpy(x,y,(bm,bn))
np.testing.assert_allclose(result,reference,rtol=1e-6,atol=1e-6)
print("programs, padded elements, modeled data-buffer bytes:",metadata)Code building blocks in this step
jnp.allclose(actual, expected, rtol=..., atol=...)— Checks that two arrays match elementwise within floating-point tolerance.
This footprint counts only the declared data buffers. It excludes runtime bookkeeping, semaphores, compiler scratch, register allocation and target-specific layout padding; it is not a measured device-memory statistic.
View the cumulative build in main.py
These blocks append in order to the same file.
# Step 1 — Reuse the checked arithmetic and add a real pipeline: The outer kernel receives global-memory references and emits an...
import jax
import jax.numpy as jnp
import numpy as np
from jax.experimental import pallas as pl
from jax.experimental.pallas import tpu as pltpu
def validate_pair(x,y,block):
# Guard input contract (`x.ndim != 2 or x.shape != y.shape or min(x.shape) < 1`) and fail fast if violated.
if x.ndim!=2 or x.shape!=y.shape or min(x.shape)<1:
raise ValueError("inputs must be nonempty matrices with identical shapes")
# Guard input contract (`x.dtype != y.dtype or x.dtype not in (jnp.float32, jnp.bfloat16)`) and fail fast if violated.
if x.dtype!=y.dtype or x.dtype not in (jnp.float32,jnp.bfloat16):
raise ValueError("matching float32 or bfloat16 inputs required")
# Guard input contract (`len(block) != 2 or any((not isinstance(n, int) or n < 1 for n in block))`) and fail fast if violated.
if len(block)!=2 or any(not isinstance(n,int) or n<1 for n in block):
raise ValueError("two positive integer block dimensions required")
def pad_pair(x,y,block):
validate_pair(x,y,block)
m,n=x.shape
bm,bn=block
padded=((m+bm-1)//bm*bm,(n+bn-1)//bn*bn)
# Combine or mask array elements to form `pads`.
pads=((0,padded[0]-m),(0,padded[1]-n))
return jnp.pad(x,pads),jnp.pad(y,pads),padded
def axpy_body(x_ref,y_ref,out_ref):
result=2.0*x_ref[...].astype(jnp.float32)+y_ref[...].astype(jnp.float32)
out_ref[...]=result.astype(out_ref.dtype)
def blocked_axpy(x,y,block=(2,4)):
# Combine or mask array elements to form `(px, py, padded)`.
px,py,padded=pad_pair(x,y,block)
bm,bn=block
# Invoke custom Pallas kernel or tile specification (`spec`).
spec=pl.BlockSpec(block,lambda i,j:(i,j))
# Combine or mask array elements to form `out`.
out=pl.pallas_call(axpy_body,
out_shape=jax.ShapeDtypeStruct(padded,x.dtype),
grid=(padded[0]//bm,padded[1]//bn),
in_specs=(spec,spec),out_specs=spec,interpret=True)(px,py)
return out[:x.shape[0],:x.shape[1]]
def pipelined_axpy(x,y,block=(8,128),buffers=2,no_pipelining=False,mode="simulate"):
# Combine or mask array elements to form `(px, py, padded)`.
px,py,padded=pad_pair(x,y,block)
bm,bn=block
# Guard input contract (`bm % 8 or bn % 128`) and fail fast if violated.
if bm%8 or bn%128:
raise ValueError("pipeline blocks must be multiples of (8,128)")
# Guard input contract (`buffers not in (2, 3)`) and fail fast if violated.
if buffers not in (2,3):
raise ValueError("this lab supports two or three buffers")
# Guard input contract (`mode not in ('simulate', 'tpu')`) and fail fast if violated.
if mode not in ("simulate","tpu"):
raise ValueError("mode must explicitly be simulate or tpu")
# Guard input contract (`mode == 'tpu' and (jax.default_backend() != 'tpu' or any((not isinstance(a, jax.core.Tracer) and any((d.platform != 'tpu' for d in a.devices())) for a in (x, y))))`) and fail fast if violated.
if mode=="tpu" and (jax.default_backend()!="tpu" or any(not isinstance(a,jax.core.Tracer) and any(d.platform!="tpu" for d in a.devices()) for a in (x,y))):
raise RuntimeError("Real TPU inputs/backend required; simulation fallback is disabled")
# Invoke custom Pallas kernel or tile specification (`spec`).
spec=pl.BlockSpec(block,lambda i,j:(i,j),pipeline_mode=pl.Buffered(buffer_count=buffers))
# Invoke custom Pallas kernel or tile specification (`output_spec`).
output_spec=pl.BlockSpec(block,lambda i,j:(i,j),pipeline_mode=pl.Buffered(buffer_count=2))
def outer(x_hbm,y_hbm,out_hbm):
# Combine or mask array elements to form ``.
pltpu.emit_pipeline(axpy_body,grid=(padded[0]//bm,padded[1]//bn),
in_specs=(spec,spec),out_specs=output_spec,
no_pipelining=no_pipelining)(x_hbm,y_hbm,out_hbm)
# Invoke custom Pallas kernel or tile specification (`whole`).
whole=pl.BlockSpec(memory_space=pl.ANY)
interpretation=pltpu.InterpretParams(detect_races=True) if mode=="simulate" else False
# Combine or mask array elements to form `call`.
call=pl.pallas_call(outer,out_shape=jax.ShapeDtypeStruct(padded,x.dtype),
in_specs=(whole,whole),out_specs=whole,interpret=interpretation)
if mode=="simulate":
# This describes a SIMULATED TPU layout, not the machine running the code.
abstract=jax.sharding.AbstractMesh((1,),("simulated_device",),
abstract_device=jax.sharding.AbstractDevice(device_kind="TPU v5 lite",num_cores=1))
with jax.sharding.use_abstract_mesh(abstract):
output=call(px,py)
else:
output=call(px,py)
return output[:x.shape[0],:x.shape[1]]
# Step 2 — Compare synchronous and buffered pipeline semantics: Both schedules execute real pipeline semantics under CPU simulation.
# Construct and reshape `x` into the target tensor dimensions.
x=jnp.linspace(-1,1,17*257,dtype=jnp.float32).reshape(17,257)
y=jnp.full_like(x,.25)
# Convert `reference` to a host NumPy array for inspection or verification.
reference=2*np.asarray(x)+np.asarray(y)
synchronous=pipelined_axpy(x,y,no_pipelining=True)
buffered=pipelined_axpy(x,y,no_pipelining=False)
np.testing.assert_allclose(synchronous,reference,rtol=1e-6,atol=1e-6)
np.testing.assert_allclose(buffered,reference,rtol=1e-6,atol=1e-6)
# Execute `np.testing.assert_array_equal(synchronous,buffered)`
np.testing.assert_array_equal(synchronous,buffered)
print("Actual backend:",jax.default_backend(),"simulated layout: TPU v5 lite")
print("Synchronous/buffered endpoints:",float(buffered[0,0]),float(buffered[-1,-1]))
# Step 3 — Quantify tile tradeoffs before measuring hardware: This footprint counts only the declared data buffers.
configurations=[(8,128),(16,128),(16,256)]
metadata=[]
for bm,bn in configurations:
gm=(17+bm-1)//bm
gn=(257+bn-1)//bn
programs=gm*gn
padded_elements=programs*bm*bn
# Two input buffers and one output buffer, each double buffered, FP32.
modeled_buffer_bytes=3*2*bm*bn*4
# Combine or mask array elements to form ``.
metadata.append((programs,padded_elements,modeled_buffer_bytes))
result=pipelined_axpy(x,y,(bm,bn))
np.testing.assert_allclose(result,reference,rtol=1e-6,atol=1e-6)
print("programs, padded elements, modeled data-buffer bytes:",metadata)Run the example
python3 phases/13-kernels/03-tiling-memory-and-pipelining/code/main.pyAbout this command
This shortcut uses an activated macOS/Linux course environment in the repository folder. For Windows, use the explicit interpreter command in “Setup help” above. The script includes worked examples and reference solutions.
Inspect the complete assembled example
# Step 1 — Reuse the checked arithmetic and add a real pipeline: The outer kernel receives global-memory references and emits an...
import jax
import jax.numpy as jnp
import numpy as np
from jax.experimental import pallas as pl
from jax.experimental.pallas import tpu as pltpu
def validate_pair(x,y,block):
# Guard input contract (`x.ndim != 2 or x.shape != y.shape or min(x.shape) < 1`) and fail fast if violated.
if x.ndim!=2 or x.shape!=y.shape or min(x.shape)<1:
raise ValueError("inputs must be nonempty matrices with identical shapes")
# Guard input contract (`x.dtype != y.dtype or x.dtype not in (jnp.float32, jnp.bfloat16)`) and fail fast if violated.
if x.dtype!=y.dtype or x.dtype not in (jnp.float32,jnp.bfloat16):
raise ValueError("matching float32 or bfloat16 inputs required")
# Guard input contract (`len(block) != 2 or any((not isinstance(n, int) or n < 1 for n in block))`) and fail fast if violated.
if len(block)!=2 or any(not isinstance(n,int) or n<1 for n in block):
raise ValueError("two positive integer block dimensions required")
def pad_pair(x,y,block):
validate_pair(x,y,block)
m,n=x.shape
bm,bn=block
padded=((m+bm-1)//bm*bm,(n+bn-1)//bn*bn)
# Combine or mask array elements to form `pads`.
pads=((0,padded[0]-m),(0,padded[1]-n))
return jnp.pad(x,pads),jnp.pad(y,pads),padded
def axpy_body(x_ref,y_ref,out_ref):
result=2.0*x_ref[...].astype(jnp.float32)+y_ref[...].astype(jnp.float32)
out_ref[...]=result.astype(out_ref.dtype)
def blocked_axpy(x,y,block=(2,4)):
# Combine or mask array elements to form `(px, py, padded)`.
px,py,padded=pad_pair(x,y,block)
bm,bn=block
# Invoke custom Pallas kernel or tile specification (`spec`).
spec=pl.BlockSpec(block,lambda i,j:(i,j))
# Combine or mask array elements to form `out`.
out=pl.pallas_call(axpy_body,
out_shape=jax.ShapeDtypeStruct(padded,x.dtype),
grid=(padded[0]//bm,padded[1]//bn),
in_specs=(spec,spec),out_specs=spec,interpret=True)(px,py)
return out[:x.shape[0],:x.shape[1]]
def pipelined_axpy(x,y,block=(8,128),buffers=2,no_pipelining=False,mode="simulate"):
# Combine or mask array elements to form `(px, py, padded)`.
px,py,padded=pad_pair(x,y,block)
bm,bn=block
# Guard input contract (`bm % 8 or bn % 128`) and fail fast if violated.
if bm%8 or bn%128:
raise ValueError("pipeline blocks must be multiples of (8,128)")
# Guard input contract (`buffers not in (2, 3)`) and fail fast if violated.
if buffers not in (2,3):
raise ValueError("this lab supports two or three buffers")
# Guard input contract (`mode not in ('simulate', 'tpu')`) and fail fast if violated.
if mode not in ("simulate","tpu"):
raise ValueError("mode must explicitly be simulate or tpu")
# Guard input contract (`mode == 'tpu' and (jax.default_backend() != 'tpu' or any((not isinstance(a, jax.core.Tracer) and any((d.platform != 'tpu' for d in a.devices())) for a in (x, y))))`) and fail fast if violated.
if mode=="tpu" and (jax.default_backend()!="tpu" or any(not isinstance(a,jax.core.Tracer) and any(d.platform!="tpu" for d in a.devices()) for a in (x,y))):
raise RuntimeError("Real TPU inputs/backend required; simulation fallback is disabled")
# Invoke custom Pallas kernel or tile specification (`spec`).
spec=pl.BlockSpec(block,lambda i,j:(i,j),pipeline_mode=pl.Buffered(buffer_count=buffers))
# Invoke custom Pallas kernel or tile specification (`output_spec`).
output_spec=pl.BlockSpec(block,lambda i,j:(i,j),pipeline_mode=pl.Buffered(buffer_count=2))
def outer(x_hbm,y_hbm,out_hbm):
# Combine or mask array elements to form ``.
pltpu.emit_pipeline(axpy_body,grid=(padded[0]//bm,padded[1]//bn),
in_specs=(spec,spec),out_specs=output_spec,
no_pipelining=no_pipelining)(x_hbm,y_hbm,out_hbm)
# Invoke custom Pallas kernel or tile specification (`whole`).
whole=pl.BlockSpec(memory_space=pl.ANY)
interpretation=pltpu.InterpretParams(detect_races=True) if mode=="simulate" else False
# Combine or mask array elements to form `call`.
call=pl.pallas_call(outer,out_shape=jax.ShapeDtypeStruct(padded,x.dtype),
in_specs=(whole,whole),out_specs=whole,interpret=interpretation)
if mode=="simulate":
# This describes a SIMULATED TPU layout, not the machine running the code.
abstract=jax.sharding.AbstractMesh((1,),("simulated_device",),
abstract_device=jax.sharding.AbstractDevice(device_kind="TPU v5 lite",num_cores=1))
with jax.sharding.use_abstract_mesh(abstract):
output=call(px,py)
else:
output=call(px,py)
return output[:x.shape[0],:x.shape[1]]
# Step 2 — Compare synchronous and buffered pipeline semantics: Both schedules execute real pipeline semantics under CPU simulation.
# Construct and reshape `x` into the target tensor dimensions.
x=jnp.linspace(-1,1,17*257,dtype=jnp.float32).reshape(17,257)
y=jnp.full_like(x,.25)
# Convert `reference` to a host NumPy array for inspection or verification.
reference=2*np.asarray(x)+np.asarray(y)
synchronous=pipelined_axpy(x,y,no_pipelining=True)
buffered=pipelined_axpy(x,y,no_pipelining=False)
np.testing.assert_allclose(synchronous,reference,rtol=1e-6,atol=1e-6)
np.testing.assert_allclose(buffered,reference,rtol=1e-6,atol=1e-6)
# Execute `np.testing.assert_array_equal(synchronous,buffered)`
np.testing.assert_array_equal(synchronous,buffered)
print("Actual backend:",jax.default_backend(),"simulated layout: TPU v5 lite")
print("Synchronous/buffered endpoints:",float(buffered[0,0]),float(buffered[-1,-1]))
# Step 3 — Quantify tile tradeoffs before measuring hardware: This footprint counts only the declared data buffers.
configurations=[(8,128),(16,128),(16,256)]
metadata=[]
for bm,bn in configurations:
gm=(17+bm-1)//bm
gn=(257+bn-1)//bn
programs=gm*gn
padded_elements=programs*bm*bn
# Two input buffers and one output buffer, each double buffered, FP32.
modeled_buffer_bytes=3*2*bm*bn*4
# Combine or mask array elements to form ``.
metadata.append((programs,padded_elements,modeled_buffer_bytes))
result=pipelined_axpy(x,y,(bm,bn))
np.testing.assert_allclose(result,reference,rtol=1e-6,atol=1e-6)
print("programs, padded elements, modeled data-buffer bytes:",metadata)Expected result: Synchronous and buffered CPU simulations agree with NumPy and each other; endpoints -1.75 and 2.25. Tile program counts [9,6,4], padded elements [9216,12288,16384], modeled data buffers [24576,49152,98304] bytes.
Recorded reference execution
Reference output captured from executing the complete lesson script (code/main.py) and its assertions in the course CPU environment.
Actual backend: cpu simulated layout: TPU v5 lite
Synchronous/buffered endpoints: -1.75 2.25
programs, padded elements, modeled data-buffer bytes: [(9, 9216, 24576), (6, 12288, 49152), (4, 16384, 98304)]
Actual backend: cpu simulated layout: TPU v5 lite
Synchronous/buffered endpoints: -1.75 2.25
programs, padded elements, modeled data-buffer bytes: [(9, 9216, 24576), (6, 12288, 49152), (4, 16384, 98304)]
Three-buffer schedule matches the two-buffer result
Single-tile pipeline checked
Changed shape, dtype and schedules verified
Unsupported pipeline tile rejected
Tile, two/three-input-slot modeled bytes: (8, 128) 24576 32768
Tile, two/three-input-slot modeled bytes: (16, 128) 49152 65536
Tile, two/three-input-slot modeled bytes: (16, 256) 98304 131072
PASS: kernels-03
Download execution record · The notebook includes executed cells and figure outputs. Compare your run with these values; timings depend on the machine.
Change input buffer count without changing the result
# Experiment — Change input buffer count without changing the result: More input buffering changes potential overlap and storage, not...
three=pipelined_axpy(x,y,buffers=3)
np.testing.assert_allclose(three,reference,rtol=1e-6,atol=1e-6)
# Execute `np.testing.assert_array_equal(three,buffered)`
np.testing.assert_array_equal(three,buffered)
print("Three-buffer schedule matches the two-buffer result")Expected: Two and three input slots produce the same logical result; outputs retain two slots.
More input buffering changes potential overlap and storage, not arithmetic. This installed emitter only supports up to two output slots. A speed benefit remains a target measurement question.
Check a one-tile pipeline
# Experiment — Check a one-tile pipeline: A one-tile pipeline has no next tile with which to overlap its...
# Construct and reshape `small_x` into the target tensor dimensions.
small_x=jnp.arange(8*128,dtype=jnp.float32).reshape(8,128)/100
small_y=jnp.ones_like(small_x)
small=pipelined_axpy(small_x,small_y)
# Convert `` to a host NumPy array for inspection or verification.
np.testing.assert_allclose(small,2*np.asarray(small_x)+1,rtol=1e-6,atol=1e-6)
print("Single-tile pipeline checked")Expected: The one-tile result matches the oracle.
A one-tile pipeline has no next tile with which to overlap its initial fetch. Prologue and drain overhead can dominate small workloads; correctness does not imply a benefit.
Make it yours
Where to put your code: Append your solution at the bottom of phases/13-kernels/03-tiling-memory-and-pipelining/code/main.py (or inside the Make it yours cell of exercises/kernels-03.ipynb) and run python3 phases/13-kernels/03-tiling-memory-and-pipelining/code/main.py to verify your prediction before opening the reference solution.
Use a (9,129) random fixture and verify synchronous, double-input-buffered and triple-input-buffered pipelines for both float32 and bfloat16 represented-input contracts. Keep output buffering at two slots.
Show a writing guide and starter scaffold
How to write this exercise code: step-by-step recipe & starter scaffold
1. Key functions & syntax to use:
a(...)— Callawith your updated parameters or inputs from this lesson's workspace.random.default_rng(...)— Callrandom.default_rngwith your updated parameters or inputs from this lesson's workspace.
2. Step-by-step implementation plan:
- Create device-backed JAX array
a. - Create device-backed JAX array
b. - Convert
to a host NumPy array for inspection or verification.
# Exercise solution: Use a (9,129) random fixture and verify synchronous,...
rng = np.random.default_rng(...) # TODO: compute rng
for dtype in (jnp.float32,jnp.bfloat16):
# Create device-backed JAX array `a`.
a = jnp.asarray(...) # TODO: compute a
# Create device-backed JAX array `b`.
b = jnp.asarray(...) # TODO: compute b
expected = ... # TODO: compute expected
for buffers,sync in [(2,True),(2,False),(3,False)]:
result = pipelined_axpy(...) # TODO: compute result
# Convert `` to a host NumPy array for inspection or verification.
np.testing.assert_array_equal(np.asarray(result),np.asarray(expected))
print("Changed shape, dtype and schedules verified")
Reveal the reference solution
# Exercise solution: Use a (9,129) random fixture and verify synchronous,...
rng=np.random.default_rng(23)
for dtype in (jnp.float32,jnp.bfloat16):
# Create device-backed JAX array `a`.
a=jnp.asarray(rng.normal(size=(9,129)),dtype=dtype)
# Create device-backed JAX array `b`.
b=jnp.asarray(rng.normal(size=(9,129)),dtype=dtype)
expected=(2*a.astype(jnp.float32)+b.astype(jnp.float32)).astype(dtype)
for buffers,sync in [(2,True),(2,False),(3,False)]:
result=pipelined_axpy(a,b,buffers=buffers,no_pipelining=sync)
# Convert `` to a host NumPy array for inspection or verification.
np.testing.assert_array_equal(np.asarray(result),np.asarray(expected))
print("Changed shape, dtype and schedules verified")Reject an unsupported pipeline tile
Practice
Try a (3,5) block and verify that the pipeline wrapper refuses it rather than using the permissive arithmetic interpreter as a hidden fallback.
Show a writing guide and starter scaffold
How to write this practice solution: step-by-step recipe & starter scaffold
1. Key functions & syntax to use:
tile(...)— Calltilewith your updated parameters or inputs from this lesson's workspace.pipelined_axpy(...)— Callpipelined_axpywith your updated parameters or inputs from this lesson's workspace.assert condition— Verify that the observed output shape, status, or numerical value satisfies the contract.
2. Step-by-step implementation plan:
- Run the boundary check and catch the expected exception:
# Reject an unsupported pipeline tile (Practice): Different wrappers may expose different supported contracts.
rejected = ... # TODO: compute rejected
# Run the boundary check and catch the expected exception:
try:pipelined_axpy(x,y,(3,5))
except ValueError:rejected=True
assert rejected # TODO: complete assertion check
print("Unsupported pipeline tile rejected")
Show a hint
The pipeline lab deliberately uses a conservative TPU-compatible tile family.
Reveal reference solution and reasoning
# Reject an unsupported pipeline tile (Practice): Different wrappers may expose different supported contracts.
rejected=False
# Run the boundary check and catch the expected exception:
try:pipelined_axpy(x,y,(3,5))
except ValueError:rejected=True
assert rejected
print("Unsupported pipeline tile rejected")Different wrappers may expose different supported contracts. A narrower explicit contract is safer than implying the generic interpreter established target compatibility.
Estimate the price of a third input buffer
Challenge
Compute the declared data-buffer footprint for two and three input slots at each tile size, with two output slots in both cases. State at least two omitted memory costs.
Show a writing guide and starter scaffold
How to write this practice solution: step-by-step recipe & starter scaffold
1. Key functions & syntax to use:
buffer(...)— Callbufferwith your updated parameters or inputs from this lesson's workspace.assert condition— Verify that the observed output shape, status, or numerical value satisfies the contract.
2. Step-by-step implementation plan:
- Set up the changed input or configuration parameters requested in the prompt.
- Run the target function or transformation and assert that the output matches your prediction.
# Estimate the price of a third input buffer (Challenge): A third slot for each input increases this limited...
for bm,bn in configurations:
two = ... # TODO: compute two
three = ... # TODO: compute three
assert three*3 # TODO: complete assertion check
print("Tile, two/three-input-slot modeled bytes:",(bm,bn),two,three)
Show a hint
Count two input arrays and one output array separately because their buffer counts differ.
Reveal reference solution and reasoning
# Estimate the price of a third input buffer (Challenge): A third slot for each input increases this limited...
for bm,bn in configurations:
two=(2*2+2)*bm*bn*4
three=(2*3+2)*bm*bn*4
assert three*3==two*4
print("Tile, two/three-input-slot modeled bytes:",(bm,bn),two,three)A third slot for each input increases this limited data-buffer model by one third. Output buffering stays at two slots. The estimate excludes registers, semaphores, compiler scratch and layout effects.
Diagnose the result
If the buffered result differs from synchronous execution, keep arithmetic and tile geometry fixed while inspecting copies, ownership and synchronization. If the interpreter cannot infer TPU layout on CPU, choose the documented TPU simulation API and label the metadata as simulated. If a larger tile uses fewer programs but more padded work, neither count alone ranks its performance.
Carry forward
- Keep one numerical operation while changing data movement
- What buffering is trying to overlap
- Use the appropriate interpreter for memory operations
- Use synchronous execution as a debugging comparison
- Count the cost of a tile choice
- Only target measurements can rank pipeline speed
Keep your evidence
Synchronous/buffered CPU simulation agreement, two/three input slots, unchanged output buffering, correctness across tiles and modeled footprint counts. Keep the environment, observed outputs and your explanation of the figure.
Save your modified code, its output, and an explanation of your prediction. Checkpoint answers and exercise evidence are tracked separately.
Saved in this browser. Export or back up your portfolio anytime in My learning.
Verified on local CPU. Stage to a Cloud TPU VM via Cloud TPU setup to record accelerator execution receipts (TODO: add automated Cloud TPU CI execution receipts).