Phase 08 / Performance diagnosis
Memory, rematerialization, and optimization tradeoffs
Revision v0.7.0 · Updated since your last visit. Review the changes; your progress is saved.
Lesson · about 100 minutes · CPU practice
Explain why reverse-mode differentiation needs forward intermediates. Compare ordinary and rematerialized gradients with an independent reverse recurrence.
Phase study guide: preparation, milestones and project evidence
Before you start
Before this: Reason with HLO and the roofline model
Hardware: CPU
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/08-performance/05-memory-rematerialization-and-optimization-tradeoffs/code/main.py.\.venv\Scripts\python.exe phases/08-performance/05-memory-rematerialization-and-optimization-tradeoffs/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 performance-05.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: Both gradients agree with the independent reverse recurrence. The saved-residual descriptions differ; the small tested CPU program reports equal temporary-memory estimates. Timings vary and no speedup is asserted.
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
- Explain why reverse-mode differentiation needs forward intermediates.
- Compare ordinary and rematerialized gradients with an independent reverse recurrence.
- Inspect saved residual descriptions and compiled memory estimates without equating them.
- Measure warm gradient calls and explain why a memory benefit is workload-dependent.
The problem
A model runs out of memory during differentiation. Someone suggests checkpointing every layer. Will that change the answer, and will it necessarily reduce this program’s memory? We will compare gradients, inspect saved residuals and measure compiled estimates and timings before making that decision.
The idea
Reverse-mode differentiation needs information from forward execution. Rematerialization recomputes some of that information instead of retaining it. This exchanges storage for work inside a computation and is distinct from saving a training checkpoint to disk.
Follow an intermediate from creation to last use
Imagine a backward rule needs an intermediate from the first operation in a chain. One strategy retains it; another reruns enough forward work to reconstruct it. Draw the value's lifetime and mark which computation is repeated.
Saved autodiff entries, estimated bytes and compiled peak memory are different quantities. Fusion, temporary buffers and reuse can change the compiled memory picture even when a source-level list of saved values changes.
Read each plot with its own units. Fewer entries alone are separate from lower peak device memory or faster execution. Use the storage model to form a hypothesis, then inspect compiler information and actual target behavior.
Follow one layer backward
Our layer multiplies a vector by a fixed matrix and applies tanh. In the backward pass, the derivative of tanh uses the forward output. If that output is retained, the derivative can reuse it. If selected intermediates are discarded, the program must reconstruct the needed value from retained inputs and weights.
For layer output , the local derivative factor is . Multiplying the incoming cotangent by that factor, then applying the transposed weight matrix, propagates sensitivity to the preceding vector. The NumPy reference implements this recurrence explicitly instead of calling autodiff.
Choose a boundary around pure work
We apply jax.checkpoint to the layer function. The outer objective uses four layers and a sum of squared final outputs. The reference and rematerialized versions share the same weights and input. If a function relies on untracked mutation, hidden randomness or effects, recomputation can have a different meaning; explicit state and keys remain essential.
Checkpointing the entire loss at an unhelpful boundary may simply recompute nearly all work without eliminating the intermediates you expected. Compare policies at meaningful blocks, and start with a small function whose derivative you can verify.
Inspect what autodiff saves
print_saved_residuals names values retained for differentiation. The output describes a constant weight matrix and selected vector intermediates. Count the entries to compare policies, but read their shapes too: one large matrix may cost more than many scalar entries. The printed list is a diagnostic representation, not a device-memory allocation trace.
In this fixed example, the ordinary policy reports nine residual descriptions and layer rematerialization reports six. Those counts explain that the differentiation strategy changed; they do not guarantee a proportional reduction in peak bytes.
Read compiled memory estimates separately
Compile the gradient for the same input contract and inspect memory_analysis. Argument, output, temporary and alias bytes answer different questions. Backend support and reporting conventions can vary, so record missing analysis as unavailable instead of substituting zero.
In the tested small CPU example, both policies report the same temporary-buffer estimate. That is a useful result: fewer saved autodiff values need not reduce this compiled program’s allocated temporary storage. Fusion, buffer reuse and compiler decisions stand between a mathematical strategy and a memory allocation.
Measure the tradeoff you actually care about
Warm both compiled gradients and collect synchronized calls. Keep all samples, not just the fastest. Rematerialization can add arithmetic; on a tiny workload, dispatch and noise can dominate. No strict timing ordering is required.
A training-memory claim requires the complete training step, representative model and batch shapes, and actual peak memory on the target device. Include optimizer state and input buffers. Test whether the policy enables the intended larger batch without changing the objective, then verify quality and runtime again.
Changed autodiff storage does not guarantee changed compiled buffers
Predict before looking: Must the two panels tell the same story about memory?
Read the figure
The upper panel counts saved-residual descriptions for ordinary differentiation and layer rematerialization. The vertical unit is entries, not bytes. In this fixed network the counts are nine and six, including a shared constant matrix; the decrease shows a changed differentiation strategy.
The lower panel reports compiler-estimated temporary bytes when that analysis is supported. In the recorded CPU example both bars have height . Equal bars mean the compiler reports no reduction in this category for these shapes, even though the residual-entry count decreased.
Connect it to the computation
The independent reverse recurrence checks the numerical derivative for both policies. The figure then compares two distinct storage views: values named by autodiff and temporary buffers estimated after compilation. Compiler reuse and fusion can make their relationship non-proportional.
These are not measurements of peak accelerator allocation or total training memory. The runtime samples are recorded separately, and a meaningful policy decision needs representative model size, optimizer state and actual target-device measurements. If a backend lacks memory analysis, the plot explicitly substitutes a labeled availability panel rather than treating missing bytes as zero.
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/08-performance/05-memory-rematerialization-and-optimization-tradeoffs/code/main.py (or downloaded exercises/performance-05.py / exercises/performance-05.ipynb) and execute from the repository root with python3 phases/08-performance/05-memory-rematerialization-and-optimization-tradeoffs/code/main.py.
1 Define two differentiation policies
Create a Python file in the course environment. Add this block after the preceding block, then run the assembled file.
# Step 1 — Define two differentiation policies: Both functions compute the same pure objective; only the...
import contextlib
import io
import time
import numpy as np
import jax
import jax.numpy as jnp
from jax.ad_checkpoint import print_saved_residuals
# Draw pseudorandom samples for `rng` using the explicit RNG state.
rng=np.random.default_rng(18)
# Create device-backed JAX array `w`.
w=jnp.asarray((rng.normal(size=(16,16))*.1).astype(np.float32))
x=jnp.asarray(np.linspace(-.5,.5,16,dtype=np.float32))
def layer(v):return jnp.tanh(w@v)
def objective(v,rematerialize=False):
operation=jax.checkpoint(layer) if rematerialize else layer
for _ in range(4):v=operation(v)
return jnp.sum(v*v)
plain=lambda v:objective(v,False)
remat=lambda v:objective(v,True)
def residual_report(fn):
stream=io.StringIO()
# Enter `contextlib.redirect_stdout(stream)` context block:
with contextlib.redirect_stdout(stream):print_saved_residuals(fn,x)
lines=[line for line in stream.getvalue().splitlines() if line.strip()]
return lines
reports=[residual_report(plain),residual_report(remat)]Code building blocks in this step
x.sum(axis=...) / x.mean(axis=..., keepdims=...)— Reduces values along the namedaxis(the axis you name is collapsed unlesskeepdims=True).
Both functions compute the same pure objective; only the recomputation boundary differs. The diagnostic records descriptions rather than claiming physical byte savings.
2 Verify with an independent reverse pass
Create a Python file in the course environment. Add this block after the preceding block, then run the assembled file.
# Independent reverse recurrence through the same explicitly specified network.
host_w=np.asarray(w)
values=[np.asarray(x)]
for _ in range(4):values.append(np.tanh(host_w@values[-1]))
cotangent=2*values[-1]
for i in range(4,0,-1):cotangent=host_w.T@(cotangent*(1-values[i]**2))
for fn in (plain,remat):
# Differentiate the objective to obtain gradients ``.
np.testing.assert_allclose(jax.grad(fn)(x),cotangent,rtol=3e-5,atol=1e-7)
# Differentiate the objective to obtain gradients ``.
np.testing.assert_allclose(jax.grad(plain)(x),jax.grad(remat)(x),rtol=1e-6,atol=1e-7)
# Differentiate the objective to obtain `executables` via automatic differentiation.
executables=[jax.jit(jax.grad(fn)).lower(x).compile() for fn in (plain,remat)]
memory=[]
times=[]
for executable in executables:
analysis=executable.memory_analysis()
memory.append(None if analysis is None else {name:getattr(analysis,name) for name in ('argument_size_in_bytes','output_size_in_bytes','temp_size_in_bytes','alias_size_in_bytes')})
# Synchronize host execution until asynchronous device computation completes.
# Synchronize host execution until asynchronous device computation completes.
executable(x).block_until_ready()
samples=[]
for _ in range(5):
# Synchronize host execution until asynchronous device computation completes.
# Synchronize host execution until asynchronous device computation completes.
# Synchronize host execution until asynchronous device computation completes.
start=time.perf_counter()
executable(x).block_until_ready()
samples.append(time.perf_counter()-start)
times.append(samples)
print('Saved residual descriptions:')
for name,lines in zip(['plain','remat'],reports):print(name,'\n'+'\n'.join(lines))
print('Compiler memory estimates:',memory)
print('Synchronized gradient samples:',times)
print('Independent reverse recurrence agrees with both gradients.')Code building blocks in this step
jnp.allclose(actual, expected, rtol=..., atol=...)— Checks that two arrays match elementwise within floating-point tolerance.jax.grad(loss_fn)(params, ...)— Transforms a scalar-output function into a function returning the gradient PyTree with the same structure asparams.jax.jit(fn) / @jax.jit— Tracesfnwith abstract shapes and compiles a fused XLA executable cached by input shape and dtype.jax.block_until_ready(output)— Synchronizes with the accelerator/CPU device so asynchronous dispatch finishes before wall-clock timing.
The NumPy recurrence propagates cotangents through saved host activations, providing an oracle independent of JAX differentiation.
3 Step 3: Verify invariants on the completed state
Run the final shape and numerical assertions to confirm the state built in Steps 1 and 2.
# Inspect residual live tensors and compare compiler memory and wall-clock gradient samples:
for name, lines in zip(["plain", "remat"], reports):
print(name, "\n" + "\n".join(lines))
print("Compiler memory estimates:", memory)
print("Synchronized gradient samples:", times)Code building blocks in this step
zip(...)— Callzipwith your updated parameters or inputs from this lesson's workspace.join(...)— Calljoinwith your updated parameters or inputs from this lesson's workspace.
Checking these invariants confirms the computation is ready for the full worked experiment.
View the cumulative build in main.py
These blocks append in order to the same file.
# Step 1 — Define two differentiation policies: Both functions compute the same pure objective; only the...
import contextlib
import io
import time
import numpy as np
import jax
import jax.numpy as jnp
from jax.ad_checkpoint import print_saved_residuals
# Draw pseudorandom samples for `rng` using the explicit RNG state.
rng=np.random.default_rng(18)
# Create device-backed JAX array `w`.
w=jnp.asarray((rng.normal(size=(16,16))*.1).astype(np.float32))
x=jnp.asarray(np.linspace(-.5,.5,16,dtype=np.float32))
def layer(v):return jnp.tanh(w@v)
def objective(v,rematerialize=False):
operation=jax.checkpoint(layer) if rematerialize else layer
for _ in range(4):v=operation(v)
return jnp.sum(v*v)
plain=lambda v:objective(v,False)
remat=lambda v:objective(v,True)
def residual_report(fn):
stream=io.StringIO()
# Enter `contextlib.redirect_stdout(stream)` context block:
with contextlib.redirect_stdout(stream):print_saved_residuals(fn,x)
lines=[line for line in stream.getvalue().splitlines() if line.strip()]
return lines
reports=[residual_report(plain),residual_report(remat)]
# Independent reverse recurrence through the same explicitly specified network.
host_w=np.asarray(w)
values=[np.asarray(x)]
for _ in range(4):values.append(np.tanh(host_w@values[-1]))
cotangent=2*values[-1]
for i in range(4,0,-1):cotangent=host_w.T@(cotangent*(1-values[i]**2))
for fn in (plain,remat):
# Differentiate the objective to obtain gradients ``.
np.testing.assert_allclose(jax.grad(fn)(x),cotangent,rtol=3e-5,atol=1e-7)
# Differentiate the objective to obtain gradients ``.
np.testing.assert_allclose(jax.grad(plain)(x),jax.grad(remat)(x),rtol=1e-6,atol=1e-7)
# Differentiate the objective to obtain `executables` via automatic differentiation.
executables=[jax.jit(jax.grad(fn)).lower(x).compile() for fn in (plain,remat)]
memory=[]
times=[]
for executable in executables:
analysis=executable.memory_analysis()
memory.append(None if analysis is None else {name:getattr(analysis,name) for name in ('argument_size_in_bytes','output_size_in_bytes','temp_size_in_bytes','alias_size_in_bytes')})
# Synchronize host execution until asynchronous device computation completes.
# Synchronize host execution until asynchronous device computation completes.
executable(x).block_until_ready()
samples=[]
for _ in range(5):
# Synchronize host execution until asynchronous device computation completes.
# Synchronize host execution until asynchronous device computation completes.
# Synchronize host execution until asynchronous device computation completes.
start=time.perf_counter()
executable(x).block_until_ready()
samples.append(time.perf_counter()-start)
times.append(samples)
print('Saved residual descriptions:')
for name,lines in zip(['plain','remat'],reports):print(name,'\n'+'\n'.join(lines))
print('Compiler memory estimates:',memory)
print('Synchronized gradient samples:',times)
print('Independent reverse recurrence agrees with both gradients.')
# Inspect residual live tensors and compare compiler memory and wall-clock gradient samples:
for name, lines in zip(["plain", "remat"], reports):
print(name, "\n" + "\n".join(lines))
print("Compiler memory estimates:", memory)
print("Synchronized gradient samples:", times)Run the example
python3 phases/08-performance/05-memory-rematerialization-and-optimization-tradeoffs/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 — Define two differentiation policies: Both functions compute the same pure objective; only the...
import contextlib
import io
import time
import numpy as np
import jax
import jax.numpy as jnp
from jax.ad_checkpoint import print_saved_residuals
# Draw pseudorandom samples for `rng` using the explicit RNG state.
rng=np.random.default_rng(18)
# Create device-backed JAX array `w`.
w=jnp.asarray((rng.normal(size=(16,16))*.1).astype(np.float32))
x=jnp.asarray(np.linspace(-.5,.5,16,dtype=np.float32))
def layer(v):return jnp.tanh(w@v)
def objective(v,rematerialize=False):
operation=jax.checkpoint(layer) if rematerialize else layer
for _ in range(4):v=operation(v)
return jnp.sum(v*v)
plain=lambda v:objective(v,False)
remat=lambda v:objective(v,True)
def residual_report(fn):
stream=io.StringIO()
# Enter `contextlib.redirect_stdout(stream)` context block:
with contextlib.redirect_stdout(stream):print_saved_residuals(fn,x)
lines=[line for line in stream.getvalue().splitlines() if line.strip()]
return lines
reports=[residual_report(plain),residual_report(remat)]
# Independent reverse recurrence through the same explicitly specified network.
host_w=np.asarray(w)
values=[np.asarray(x)]
for _ in range(4):values.append(np.tanh(host_w@values[-1]))
cotangent=2*values[-1]
for i in range(4,0,-1):cotangent=host_w.T@(cotangent*(1-values[i]**2))
for fn in (plain,remat):
# Differentiate the objective to obtain gradients ``.
np.testing.assert_allclose(jax.grad(fn)(x),cotangent,rtol=3e-5,atol=1e-7)
# Differentiate the objective to obtain gradients ``.
np.testing.assert_allclose(jax.grad(plain)(x),jax.grad(remat)(x),rtol=1e-6,atol=1e-7)
# Differentiate the objective to obtain `executables` via automatic differentiation.
executables=[jax.jit(jax.grad(fn)).lower(x).compile() for fn in (plain,remat)]
memory=[]
times=[]
for executable in executables:
analysis=executable.memory_analysis()
memory.append(None if analysis is None else {name:getattr(analysis,name) for name in ('argument_size_in_bytes','output_size_in_bytes','temp_size_in_bytes','alias_size_in_bytes')})
# Synchronize host execution until asynchronous device computation completes.
# Synchronize host execution until asynchronous device computation completes.
executable(x).block_until_ready()
samples=[]
for _ in range(5):
# Synchronize host execution until asynchronous device computation completes.
# Synchronize host execution until asynchronous device computation completes.
# Synchronize host execution until asynchronous device computation completes.
start=time.perf_counter()
executable(x).block_until_ready()
samples.append(time.perf_counter()-start)
times.append(samples)
print('Saved residual descriptions:')
for name,lines in zip(['plain','remat'],reports):print(name,'\n'+'\n'.join(lines))
print('Compiler memory estimates:',memory)
print('Synchronized gradient samples:',times)
print('Independent reverse recurrence agrees with both gradients.')Expected result: Both gradients agree with the independent reverse recurrence. The saved-residual descriptions differ; the small tested CPU program reports equal temporary-memory estimates. Timings vary and no speedup is asserted.
Recorded reference execution
Reference output captured from executing the complete lesson script (code/main.py) and its assertions in the course CPU environment.
Saved residual descriptions:
plain
f32[16,16] from a constant
f32[16] output of tanh from <string>:17:20 (layer)
f32[16] output of sub from <string>:17:20 (layer)
f32[16] output of tanh from <string>:17:20 (layer)
f32[16] output of sub from <string>:17:20 (layer)
f32[16] output of tanh from <string>:17:20 (layer)
f32[16] output of sub from <string>:17:20 (layer)
f32[16] output of tanh from <string>:17:20 (layer)
f32[16] output of sub from <string>:17:20 (layer)
remat
f32[16,16] from a constant
f32[16] from the argument v
f32[16] output of tanh from <string>:17:20 (layer)
f32[16] output of tanh from <string>:17:20 (layer)
f32[16] output of tanh from <string>:17:20 (layer)
f32[16] output of tanh from <string>:17:20 (layer)
Compiler memory estimates: [{'argument_size_in_bytes': 64, 'output_size_in_bytes': 64, 'temp_size_in_bytes': 256, 'alias_size_in_bytes': 0}, {'argument_size_in_bytes': 64, 'output_size_in_bytes': 64, 'temp_size_in_bytes': 256, 'alias_size_in_bytes': 0}]
Synchronized gradient samples: [[0.0001016668975353241, 8.600018918514252e-05, 0.00013170810416340828, 5.487492308020592e-05, 3.0375085771083832e-05], [3.7041958421468735e-05, 2.6874709874391556e-05, 1.2292061001062393e-05, 9.582843631505966e-06, 9.292270988225937e-06]]
Independent reverse recurrence agrees with both gradients.
plain
f32[16,16] from a constant
f32[16] output of tanh from <string>:17:20 (layer)
f32[16] output of sub from <string>:17:20 (layer)
f32[16] output of tanh from <string>:17:20 (layer)
f32[16] output of sub from <string>:17:20 (layer)
f32[16] output of tanh from <string>:17:20 (layer)
f32[16] output of sub from <string>:17:20 (layer)
f32[16] output of tanh from <string>:17:20 (layer)
f32[16] output of sub from <string>:17:20 (layer)
remat
f32[16,16] from a constant
f32[16] from the argument v
f32[16] output of tanh from <string>:17:20 (layer)
f32[16] output of tanh from <string>:17:20 (layer)
f32[16] output of tanh from <string>:17:20 (layer)
f32[16] output of tanh from <string>:17:20 (layer)
Compiler memory estimates: [{'argument_size_in_bytes': 64, 'output_size_in_bytes': 64, 'temp_size_in_bytes': 256, 'alias_size_in_bytes': 0}, {'argument_size_in_bytes': 64, 'output_size_in_bytes': 64, 'temp_size_in_bytes': 256, 'alias_size_in_bytes': 0}]
Synchronized gradient samples: [[0.0001016668975353241, 8.600018918514252e-05, 0.00013170810416340828, 5.487492308020592e-05, 3.0375085771083832e-05], [3.7041958421468735e-05, 2.6874709874391556e-05, 1.2292061001062393e-05, 9.582843631505966e-06, 9.292270988225937e-06]]
Saved residual descriptions:
plain
f32[16,16] from a constant
f32[16] output of tanh from <string>:89:20 (layer)
f32[16] output of sub from <string>:89:20 (layer)
f32[16] output of tanh from <string>:89:20 (layer)
f32[16] output of sub from <string>:89:20 (layer)
f32[16] output of tanh from <string>:89:20 (layer)
f32[16] output of sub from <string>:89:20 (layer)
f32[16] output of tanh from <string>:89:20 (layer)
f32[16] output of sub from <string>:89:20 (layer)
remat
f32[16,16] from a constant
f32[16] from the argument v
f32[16] output of tanh from <string>:89:20 (layer)
f32[16] output of tanh from <string>:89:20 (layer)
f32[16] output of tanh from <string>:89:20 (layer)
f32[16] output of tanh from <string>:89:20 (layer)
Compiler memory estimates: [{'argument_size_in_bytes': 64, 'output_size_in_bytes': 64, 'temp_size_in_bytes': 256, 'alias_size_in_bytes': 0}, {'argument_size_in_bytes': 64, 'output_size_in_bytes': 64, 'temp_size_in_bytes': 256, 'alias_size_in_bytes': 0}]
Synchronized gradient samples: [[3.495905548334122e-05, 2.570822834968567e-05, 1.4999881386756897e-05, 1.4166813343763351e-05, 1.6415957361459732e-05], [2.570776268839836e-05, 2.408307045698166e-05, 8.791731670498848e-05, 7.383385673165321e-05, 5.5417418479919434e-05]]
Independent reverse recurrence agrees with both gradients.
Whole-objective residuals: ['f32[16,16] from a constant', 'f32[16] from the argument v']
Whole-objective memory estimate: CompiledMemoryStats(generated_code_size_in_bytes=0, argument_size_in_bytes=64, output_size_in_bytes=64, alias_size_in_bytes=0, temp_size_in_bytes=256, host_generated_code_size_in_bytes=0, host_argument_size_in_bytes=0, host_output_size_in_bytes=0, host_alias_size_in_bytes=0, host_temp_size_in_bytes=0)
Directional derivative finite/automatic: 0.007404538337141275 0.00740453926846385 epsilon 0.001
Higher-order directional derivatives agree.
PASS: performance-05
Download execution record · The notebook includes executed cells and figure outputs. Compare your run with these values; timings depend on the machine.
Move the boundary to the entire objective
# Experiment — Move the boundary to the entire objective: Correctness is necessary but does not choose the best policy.
whole=jax.checkpoint(plain)
# Differentiate the objective to obtain gradients ``.
np.testing.assert_allclose(jax.grad(whole)(x),cotangent,rtol=3e-5,atol=1e-7)
print('Whole-objective residuals:',residual_report(whole))
# Differentiate the objective to obtain `whole_executable` via automatic differentiation.
whole_executable=jax.jit(jax.grad(whole)).lower(x).compile()
print('Whole-objective memory estimate:',whole_executable.memory_analysis())Expected: The derivative remains correct; residual and compiled-memory reports describe this additional boundary.
Correctness is necessary but does not choose the best policy. The boundary determines what is retained and recomputed; inspect actual memory and timing for the intended workload.
Make it yours
Where to put your code: Append your solution at the bottom of phases/08-performance/05-memory-rematerialization-and-optimization-tradeoffs/code/main.py (or inside the Make it yours cell of exercises/performance-05.ipynb) and run python3 phases/08-performance/05-memory-rematerialization-and-optimization-tradeoffs/code/main.py to verify your prediction before opening the reference solution.
Use the direction and central differences to independently check a directional derivative of the original objective at the existing input. Compare it with the gradient dot direction and report the perturbation.
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.jax.grad(loss_fn)(params, ...)— Transforms a scalar-output function into a function returning the gradient PyTree with the same structure asparams.
2. Step-by-step implementation plan:
- Differentiate the objective to obtain
automaticvia automatic differentiation.
# Exercise solution: Use the direction v=(1,\ldots,1) and central differences to...
direction = jnp.ones_like(...) # TODO: compute direction
eps = ... # TODO: compute eps
finite = ... # TODO: compute finite
# Differentiate the objective to obtain `automatic` via automatic differentiation.
automatic = float(...) # TODO: compute automatic
np.testing.assert_allclose(finite,automatic,rtol = ... # TODO: compute np.testing.assert_allclose(finite,automatic,rtol
print('Directional derivative finite/automatic:',finite,automatic,'epsilon',eps)
Reveal the reference solution
# Exercise solution: Use the direction v=(1,\ldots,1) and central differences to...
direction=jnp.ones_like(x)
eps=.001
finite=(float(plain(x+eps*direction))-float(plain(x-eps*direction)))/(2*eps)
# Differentiate the objective to obtain `automatic` via automatic differentiation.
automatic=float(jnp.vdot(jax.grad(plain)(x),direction))
np.testing.assert_allclose(finite,automatic,rtol=3e-3,atol=1e-6)
print('Directional derivative finite/automatic:',finite,automatic,'epsilon',eps)Check higher-order differentiation
Transfer
Compare Hessian-vector products of the ordinary and rematerialized objectives along a nonuniform direction. Explain why equal first derivatives alone would be a weaker test.
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.linspace(start, stop, num)— Createsnumevenly spaced float points across the closed interval[start, stop].jnp.allclose(actual, expected, rtol=..., atol=...)— Checks that two arrays match elementwise within floating-point tolerance.jnp.isfinite(x)— Returns a boolean mask verifying that no element isNaNorInf.jax.grad(loss_fn)(params, ...)— Transforms a scalar-output function into a function returning the gradient PyTree with the same structure asparams.
2. Step-by-step implementation plan:
- Differentiate the objective to obtain
(_, hvp_plain)via automatic differentiation. - Differentiate the objective to obtain
(_, hvp_remat)via automatic differentiation. - Confirm that all computed values remain finite (no NaN or Inf).
# Check higher-order differentiation (Transfer): Recomputation should preserve the differentiable function in...
direction = jnp.linspace(...) # TODO: compute direction
# Differentiate the objective to obtain `(_, hvp_plain)` via automatic differentiation.
_,hvp_plain = jax.jvp(...) # TODO: compute _,hvp_plain
# Differentiate the objective to obtain `(_, hvp_remat)` via automatic differentiation.
_,hvp_remat = jax.jvp(...) # TODO: compute _,hvp_remat
np.testing.assert_allclose(hvp_plain,hvp_remat,rtol = ... # TODO: compute np.testing.assert_allclose(hvp_plain,hvp_remat,rtol
# Confirm that all computed values remain finite (no NaN or Inf).
assert np.isfinite(np.asarray(hvp_plain)).all() # TODO: complete assertion check
print('Higher-order directional derivatives agree.')
Show a hint
Apply jvp to each gradient function with the same primal point and tangent.
Reveal reference solution and reasoning
# Check higher-order differentiation (Transfer): Recomputation should preserve the differentiable function in...
direction=jnp.linspace(-1.,1.,len(x))
# Differentiate the objective to obtain `(_, hvp_plain)` via automatic differentiation.
_,hvp_plain=jax.jvp(jax.grad(plain),(x,),(direction,))
# Differentiate the objective to obtain `(_, hvp_remat)` via automatic differentiation.
_,hvp_remat=jax.jvp(jax.grad(remat),(x,),(direction,))
np.testing.assert_allclose(hvp_plain,hvp_remat,rtol=3e-5,atol=1e-7)
# Confirm that all computed values remain finite (no NaN or Inf).
assert np.isfinite(np.asarray(hvp_plain)).all()
print('Higher-order directional derivatives agree.')Recomputation should preserve the differentiable function in this pure example, including its higher-order directional behavior. This still is separate from a device-memory improvement.
Diagnose the result
If gradients differ, first compare inputs, weights, random keys and any hidden effects. If memory does not improve, inspect the checkpoint boundary, residual shapes, compiler buffer estimates and full-step peak measurements before adding more checkpoint wrappers.
Carry forward
- Recomputation trades retained intermediate values for extra work.
- Verify the derivative independently before evaluating a policy.
- Saved values, estimated buffers and measured peak memory must remain distinct.
Keep your evidence
Keep the plain/rematerialized gradient and HVP comparisons, saved-residual counts, compiler-memory observations and synchronized timings. Explain why fewer residuals do not guarantee lower measured memory.
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).
Primary references
Phase 08 Integration Project
Bring this phase together: Measure CPU JIT latency and changed-shape traces with retained JAXPR evidence
Checkpoint to prove: Separate compilation, execution, input stalls, and synchronization before claiming a speedup.
Work in projects/performance-audit/starter/model.py. Complete stages 1 and 2 for independent calculations and changed-condition diagnosis, then retain the stage 3 evidence with --evidence-dir runs/performance-audit. Stage 4 integrates the checks. Learner commands explicitly select the starter; --implementation solution is instructor reference execution. This bounded practice does not certify the entire phase or target hardware.