Phase 13 / Pallas kernels
Pallas grids and BlockSpecs
Revision v0.7.0 · Updated since your last visit. Review the changes; your progress is saved.
Lesson · about 95 minutes · CPU practice
Implement an actual Pallas Ref-writing kernel with grid and BlockSpecs. Derive block coordinates and distinguish them from element offsets.
Phase study guide: preparation, milestones and project evidence
Before you start
Before this: Distributed training
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/01-pallas-grids-and-blockspecs/code/main.py.\.venv\Scripts\python.exe phases/13-kernels/01-pallas-grids-and-blockspecs/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-01.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: Logical shape (5,11); endpoints -3.75 and approximately 7.05. Grid (3,3); 55 logical versus 72 padded elements.
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
- Implement an actual Pallas Ref-writing kernel with grid and BlockSpecs.
- Derive block coordinates and distinguish them from element offsets.
- Handle nondivisible matrix tails explicitly and verify full output coverage.
- Separate CPU interpretation from accelerator execution evidence.
The problem
A kernel can produce the right answer on a divisible matrix and silently miss the last row on a real workload. How do grid coordinates, block ownership and array boundaries fit together? We will implement an actual Pallas operation and make its tail handling visible.
The idea
A Pallas grid assigns work to programs, and BlockSpec maps those programs to array regions. Boundary tiles make the distinction between logical data and padded storage important. Verify ownership before optimizing arithmetic.
Map every logical element to an owning program
The lesson's five-by-eleven matrix is covered by nine program IDs. Those IDs are categories, not measured magnitudes. Tile outlines and a discrete legend show which program owns each region without implying that a higher ID performs more work.
At an edge, the physical tile extends beyond the logical matrix. This implementation pads the inputs before the kernel, making each block access valid, then crops the result back to the logical shape. A padded slot is storage for the computation, not an extra observation or logical output.
Enumerate coordinates independently and verify coverage of every valid element. Then test dimensions that are not multiples of the tile size; perfectly divisible fixtures can conceal boundary mistakes.
Program coordinates and partial boundary tiles
Predict: Why test an awkward shape in addition to a tile-aligned shape?
Two-by-four tiles cover a five-by-eleven logical matrix. Each cell lists a program ID and valid row/column range. Bottom and right tiles are partial; accesses outside the logical range need boundary treatment. Program IDs are categories, matching the corrected discrete ownership heatmap. Here explicit padding makes whole-block accesses valid; cropping removes artificial output positions.
Write the simplest useful contract
Two nonempty matrices have the same shape and dtype; the result has that shape and dtype. The kernel computes in float32 and casts once at its output, supporting float32 and bfloat16 inputs. This explicit precision policy becomes important later. A function returning an array is not the kernel body contract: the body reads input Refs and writes every element of its output Ref.
A grid coordinate is not an element coordinate
For a matrix and blocks, the grid is . Invocation owns rows starting at and columns starting at . The BlockSpec index map returns block coordinates ; Pallas applies the block-size scaling. Confusing this with element offsets can produce skipped regions or out-of-bounds windows.
Make boundary behavior explicit
We pad both inputs to , launch complete blocks and crop the output to . This avoids relying on implicit out-of-bounds reads or uninitialized output cells. There are logical elements and padded elements, so elements represent extra work. Padding with zeros is harmless here because each output depends only on corresponding input elements. A reduction, softmax or stencil would require a separate padding argument.
Check ownership and arithmetic separately
The ownership map comes from integer row/column arithmetic, independent of the kernel result. The value reference comes from NumPy. Checking only a mean could hide a missing or swapped tile; compare every element and explicitly inspect the last row and column. A different block size must change work partitioning while preserving the logical result.
Interpretation proves bounded semantics, not speed
The call sets interpret=True, which runs actual Pallas semantics through a CPU-compatible execution path. This verifies the grid, Ref operations and numerical results for the fixture. It does not validate TPU layout constraints, target lowering, memory overlap or accelerator throughput. The next lesson introduces a separate TPU-only path that refuses to substitute interpretation when target hardware is absent.
Nine programs cover a matrix with partial boundary tiles
Predict before looking: Which program owns the last logical element, and why does the grid need a third row and column?
Read the figure
Rows and columns are logical matrix coordinates. Each color identifies the program that owns that cell, using the program-ID labels on the colorbar under blocks. Program IDs are labels, not execution time or device IDs. Programs zero through two cover the first two rows; programs six through eight cover the last logical row.
The bottom-right cell belongs to program . Its logical region has only one row and three columns, although the launched block has two rows and four columns. The padded cells are intentionally outside this cropped ownership display.
Connect it to the computation
The independent ownership arithmetic agrees with a grid over a padded buffer. The numerical Pallas result is separately compared element by element with NumPy, including the bottom-right value of about .
Color differences do not imply parallel execution or different speeds. This CPU interpretation figure explains mapping and boundaries; TPU scheduling and layout constraints need their own target-specific evidence.
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/01-pallas-grids-and-blockspecs/code/main.py (or downloaded exercises/kernels-01.py / exercises/kernels-01.ipynb) and execute from the repository root with python3 phases/13-kernels/01-pallas-grids-and-blockspecs/code/main.py.
1 Write a block-local kernel and a boundary-aware wrapper
Create main.py for the first block, then append each block in order in your course CPU environment.
# Step 1 — Write a block-local kernel and a boundary-aware wrapper: The kernel writes its output Ref and returns nothing.
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]]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 kernel writes its output Ref and returns nothing. The wrapper validates and pads the global matrices, defines ownership with BlockSpecs, launches actual Pallas interpretation and crops to the original shape.
2 Check every logical element against NumPy
Create main.py for the first block, then append each block in order in your course CPU environment.
# Step 2 — Check every logical element against NumPy: The last row and column belong to partial logical tiles.
# Construct and reshape `x` into the target tensor dimensions.
x=jnp.arange(55,dtype=jnp.float32).reshape(5,11)/10-2
y=jnp.full_like(x,0.25)
actual=blocked_axpy(x,y)
# Convert `reference` to a host NumPy array for inspection or verification.
reference=2*np.asarray(x)+np.asarray(y)
# Check tensor shape invariant: `actual.shape==(5,11)`
assert actual.shape==(5,11)
np.testing.assert_allclose(actual,reference,rtol=1e-6,atol=1e-6)
assert float(actual[-1,-1])==float(reference[-1,-1])
print("Shape and boundary:",actual.shape,float(actual[0,0]),float(actual[-1,-1]))Code building blocks in this step
jnp.arange(n, dtype=...)— Creates a 1-D JAX array of evenly spaced values[0, 1, ..., n-1]on the target device.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.
The last row and column belong to partial logical tiles. Checking them explicitly catches floor-divided grids that omit the tail.
3 Derive the ownership map independently
Create main.py for the first block, then append each block in order in your course CPU environment.
# Step 3 — Derive the ownership map independently: Block coordinates are multiplied by the block shape by Pallas.
rows,cols=np.indices((5,11))
owner=(rows//2)*3+(cols//4)
assert owner[0,0]==0 and owner[4,10]==8
logical=5*11
padded=6*12
print("Grid:",(3,3),"logical/padded elements:",logical,padded)
print("Padding fraction:",(padded-logical)/padded)Code building blocks in this step
np.indices(...)— Callnp.indiceswith 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.
Block coordinates are multiplied by the block shape by Pallas. An index map returning (i,j) selects the corresponding block; multiplying those indices again would skip data.
View the cumulative build in main.py
These blocks append in order to the same file.
# Step 1 — Write a block-local kernel and a boundary-aware wrapper: The kernel writes its output Ref and returns nothing.
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]]
# Step 2 — Check every logical element against NumPy: The last row and column belong to partial logical tiles.
# Construct and reshape `x` into the target tensor dimensions.
x=jnp.arange(55,dtype=jnp.float32).reshape(5,11)/10-2
y=jnp.full_like(x,0.25)
actual=blocked_axpy(x,y)
# Convert `reference` to a host NumPy array for inspection or verification.
reference=2*np.asarray(x)+np.asarray(y)
# Check tensor shape invariant: `actual.shape==(5,11)`
assert actual.shape==(5,11)
np.testing.assert_allclose(actual,reference,rtol=1e-6,atol=1e-6)
assert float(actual[-1,-1])==float(reference[-1,-1])
print("Shape and boundary:",actual.shape,float(actual[0,0]),float(actual[-1,-1]))
# Step 3 — Derive the ownership map independently: Block coordinates are multiplied by the block shape by Pallas.
rows,cols=np.indices((5,11))
owner=(rows//2)*3+(cols//4)
assert owner[0,0]==0 and owner[4,10]==8
logical=5*11
padded=6*12
print("Grid:",(3,3),"logical/padded elements:",logical,padded)
print("Padding fraction:",(padded-logical)/padded)Run the example
python3 phases/13-kernels/01-pallas-grids-and-blockspecs/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 — Write a block-local kernel and a boundary-aware wrapper: The kernel writes its output Ref and returns nothing.
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]]
# Step 2 — Check every logical element against NumPy: The last row and column belong to partial logical tiles.
# Construct and reshape `x` into the target tensor dimensions.
x=jnp.arange(55,dtype=jnp.float32).reshape(5,11)/10-2
y=jnp.full_like(x,0.25)
actual=blocked_axpy(x,y)
# Convert `reference` to a host NumPy array for inspection or verification.
reference=2*np.asarray(x)+np.asarray(y)
# Check tensor shape invariant: `actual.shape==(5,11)`
assert actual.shape==(5,11)
np.testing.assert_allclose(actual,reference,rtol=1e-6,atol=1e-6)
assert float(actual[-1,-1])==float(reference[-1,-1])
print("Shape and boundary:",actual.shape,float(actual[0,0]),float(actual[-1,-1]))
# Step 3 — Derive the ownership map independently: Block coordinates are multiplied by the block shape by Pallas.
rows,cols=np.indices((5,11))
owner=(rows//2)*3+(cols//4)
assert owner[0,0]==0 and owner[4,10]==8
logical=5*11
padded=6*12
print("Grid:",(3,3),"logical/padded elements:",logical,padded)
print("Padding fraction:",(padded-logical)/padded)Expected result: Logical shape (5,11); endpoints -3.75 and approximately 7.05. Grid (3,3); 55 logical versus 72 padded elements.
Recorded reference execution
Reference output captured from executing the complete lesson script (code/main.py) and its assertions in the course CPU environment.
Shape and boundary: (5, 11) -3.75 7.050000190734863
Grid: (3, 3) logical/padded elements: 55 72
Padding fraction: 0.2361111111111111
Shape and boundary: (5, 11) -3.75 7.050000190734863
Grid: (3, 3) logical/padded elements: 55 72
Padding fraction: 0.2361111111111111
Changed grid: (2, 3) same numerical result
Floor-grid missing logical cells: 23
Singleton and changed tails verified
Mismatched global shape rejected
Shape, block/program/padded counts: (5, 11) [(2, 4, 9, 72), (8, 128, 1, 1024), (16, 256, 1, 4096)]
Shape, block/program/padded counts: (33, 129) [(2, 4, 561, 4488), (8, 128, 10, 10240), (16, 256, 3, 12288)]
PASS: kernels-01
Download execution record · The notebook includes executed cells and figure outputs. Compare your run with these values; timings depend on the machine.
Change block geometry without changing values
# Experiment — Change block geometry without changing values: Grid geometry is an implementation choice for this independent...
changed=blocked_axpy(x,y,(3,5))
np.testing.assert_allclose(changed,reference,rtol=1e-6,atol=1e-6)
print("Changed grid:",(2,3),"same numerical result")Expected: The grid becomes (2,3); the logical output is unchanged.
Grid geometry is an implementation choice for this independent elementwise operation. It must not alter the mathematical contract.
Expose the floor-division omission
# Experiment — Expose the floor-division omission: This diagnostic models the ownership bug without reading...
covered=np.zeros((5,11),dtype=bool)
for i in range(5//2):
for j in range(11//4):
covered[i*2:(i+1)*2,j*4:(j+1)*4]=True
assert int(covered.sum())==32
assert not covered[-1,-1]
print("Floor-grid missing logical cells:",int((~covered).sum()))Expected: A floor-divided grid misses 23 of the 55 logical cells.
This diagnostic models the ownership bug without reading uninitialized kernel memory. Ceiling division plus an explicit tail policy repairs it.
Make it yours
Where to put your code: Append your solution at the bottom of phases/13-kernels/01-pallas-grids-and-blockspecs/code/main.py (or inside the Make it yours cell of exercises/kernels-01.ipynb) and run python3 phases/13-kernels/01-pallas-grids-and-blockspecs/code/main.py to verify your prediction before opening the reference solution.
Test a singleton row and a changed nondivisible matrix with negative values. Verify full NumPy agreement and exact output shapes for two block choices.
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:
jnp.allclose(actual, expected, rtol=..., atol=...)— Checks that two arrays match elementwise within floating-point tolerance.
2. Step-by-step implementation plan:
- Create device-backed JAX array
a. - Create device-backed JAX array
b. - Check tensor shape invariant:
result.shape==shape - Convert
to a host NumPy array for inspection or verification.
# Exercise solution: Test a singleton row and a changed nondivisible matrix with negative...
for shape in [(1,7),(7,9)]:
# Create device-backed JAX array `a`.
a = jnp.asarray(...) # TODO: compute a
# Create device-backed JAX array `b`.
b = jnp.asarray(...) # TODO: compute b
for block in [(2,4),(3,5)]:
result = blocked_axpy(...) # TODO: compute result
# Check tensor shape invariant: `result.shape==shape`
assert result.shape # TODO: complete assertion check
# Convert `` to a host NumPy array for inspection or verification.
np.testing.assert_allclose(result,2*np.asarray(a)+np.asarray(b),rtol=1e-6,atol=1e-6)
print("Singleton and changed tails verified")
Reveal the reference solution
# Exercise solution: Test a singleton row and a changed nondivisible matrix with negative...
for shape in [(1,7),(7,9)]:
# Create device-backed JAX array `a`.
a=jnp.asarray(np.random.default_rng(11).normal(size=shape),dtype=jnp.float32)
# Create device-backed JAX array `b`.
b=jnp.asarray(np.random.default_rng(12).normal(size=shape),dtype=jnp.float32)
for block in [(2,4),(3,5)]:
result=blocked_axpy(a,b,block)
# Check tensor shape invariant: `result.shape==shape`
assert result.shape==shape
# Convert `` to a host NumPy array for inspection or verification.
np.testing.assert_allclose(result,2*np.asarray(a)+np.asarray(b),rtol=1e-6,atol=1e-6)
print("Singleton and changed tails verified")Reject a broadcast that changes the contract
Practice
Pass a column-shaped second input and confirm the wrapper rejects it before launching the kernel. Explain why silent broadcasting would hide an input-contract error.
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:
jnp.zeros / jnp.ones(shape, dtype=...)— Allocates a tensor of the givenshapeinitialized with constants.
2. Step-by-step implementation plan:
- Run the boundary check and catch the expected exception:
# Reject a broadcast that changes the contract (Practice): The operation promises paired matrices.
rejected = ... # TODO: compute rejected
# Run the boundary check and catch the expected exception:
try:blocked_axpy(x,jnp.ones((5,1),dtype=jnp.float32))
except ValueError:rejected=True
assert rejected # TODO: complete assertion check
print("Mismatched global shape rejected")
Show a hint
Require identical matrix shapes, not merely broadcast-compatible shapes.
Reveal reference solution and reasoning
# Reject a broadcast that changes the contract (Practice): The operation promises paired matrices.
rejected=False
# Run the boundary check and catch the expected exception:
try:blocked_axpy(x,jnp.ones((5,1),dtype=jnp.float32))
except ValueError:rejected=True
assert rejected
print("Mismatched global shape rejected")The operation promises paired matrices. A separate row/column-bias kernel can define an intentional broadcast contract.
Measure padding overhead without claiming runtime
Challenge
For shapes (5,11) and (33,129), compare logical elements, padded elements and program counts for three block choices. Explain why fewer programs may still mean more work.
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:
runtime(...)— Callruntimewith your updated parameters or inputs from this lesson's workspace.counts.append(...)— Callcounts.appendwith 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.
# Measure padding overhead without claiming runtime (Challenge): Program count and padded work expose different costs.
for shape in [(5,11),(33,129)]:
counts = ... # TODO: compute counts
for bm,bn in [(2,4),(8,128),(16,256)]:
gm = ... # TODO: compute gm
gn = ... # TODO: compute gn
counts.append((bm,bn,gm*gn,gm*gn*bm*bn))
assert all(padded # TODO: complete assertion check
print("Shape, block/program/padded counts:",shape,counts)
Show a hint
Use integer ceiling division; do not time interpretation as accelerator performance.
Reveal reference solution and reasoning
# Measure padding overhead without claiming runtime (Challenge): Program count and padded work expose different costs.
for shape in [(5,11),(33,129)]:
counts=[]
for bm,bn in [(2,4),(8,128),(16,256)]:
gm=(shape[0]+bm-1)//bm
gn=(shape[1]+bn-1)//bn
counts.append((bm,bn,gm*gn,gm*gn*bm*bn))
assert all(padded>=shape[0]*shape[1] for _,_,_,padded in counts)
print("Shape, block/program/padded counts:",shape,counts)Program count and padded work expose different costs. Actual target measurements are still needed to rank configurations.
Diagnose the result
If only the boundary is wrong, inspect ceiling division, padding and cropping before editing arithmetic. If blocks skip input regions, check whether the index map returns block coordinates or incorrectly pre-multiplied offsets. If a mean looks right but full comparison fails, inspect tile permutation or duplicate ownership.
Carry forward
- Write the simplest useful contract
- A grid coordinate is not an element coordinate
- Make boundary behavior explicit
- Check ownership and arithmetic separately
- Interpretation proves bounded semantics, not speed
Keep your evidence
Independent full-array comparisons, ownership map, tail fixtures and a floor-grid omission diagnosis. 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).