TorchLean

7.7. Worked Examples🔗

This chapter follows a small model from the first value we can print to an artifact we can check. We begin by asking what a tensor contains, then watch derivatives flow, train two deliberately small models, lower a graph for verification, and finally replay numerical evidence. The last lab crosses an external-tool boundary and shows the limits of what accepting an imported graph establishes.

Run the labs in order the first time. Each keeps the previous objects in view, so a tensor never disappears behind the words “training run” and a successful command never silently becomes a proof.

7.7.1. Lab One: A Tensor Has A Shape And A Scalar Meaning🔗

Run:

lake exe torchlean quickstart_tensors

The current output is:

== Quickstart: tensor basics ==
[Float] [0.100000, 0.200000, 0.300000, 0.400000]
[ℚ] [1/10, 1/5, 3/10, 2/5]
[Int] [1, 2, 3, 4]
[IEEE32Exec] [0.100000, 0.200000, 0.300000, 0.400000]
[Float] [[[1.000000, 2.000000], [3.000000, 4.000000]],
         [[5.000000, 6.000000], [7.000000, 8.000000]]]
Expected failure printing Tensor ℝ: Refusing to print `Tensor ℝ` (proof-level);
cast to `Float`/`IEEE32Exec`/`ℚ` to display.

The first four tensors have the same vector shape and superficially similar entries, but their scalar meanings differ:

  • Float is Lean's native executable floating-point value;

  • is exact rational arithmetic;

  • Int is exact integer arithmetic;

  • IEEE32Exec is TorchLean's explicit executable IEEE-754 binary32 model;

  • is suitable for proofs but is not an object the runtime should pretend to print.

The shape of the final tensor is 2\times2\times2. It is represented by nested, length-indexed dimensions, so the eight entries cannot accidentally be interpreted as a 4\times2 matrix without an explicit reshape proof.

Read TensorBasics.lean beside the output. Change one scalar type at a time and let Lean show which operations require a different algebraic context.

7.7.2. Lab Two: Reverse And Forward Differentiation🔗

The autograd quickstart uses a linear map from two inputs to three outputs. Run:

lake exe torchlean quickstart_autograd

For input

x=(0.5,-1.2),

the Jacobian of y=Wx+b with respect to W has three block rows. The command prints those rows:

jacrevOutParams rows = 3 (should be size(out)=3)
  row[0] dW = [[0.500000, -1.200000],
               [0.000000, 0.000000],
               [0.000000, 0.000000]]
  row[1] dW = [[0.000000, 0.000000],
               [0.500000, -1.200000],
               [0.000000, 0.000000]]
  row[2] dW = [[0.000000, 0.000000],
               [0.000000, 0.000000],
               [0.500000, -1.200000]]

That output is easy to check by hand:

\frac{\partial y_i}{\partial W_{jk}} =\begin{cases} x_k,&i=j,\\ 0,&i\ne j. \end{cases}

The same program then prints VJPs, JVPs, an HVP, a Hessian, and gradients through detach. The detach check is especially useful:

loss(mse ∘ detach) = 0.165133
gradParams (mse ∘ detach) gW = [[0.000000, 0.000000],
                                [0.000000, 0.000000],
                                [0.000000, 0.000000]]
gradParams (mse ∘ detach) gb = [0.000000, 0.000000, 0.000000]

The value is unchanged while the gradient path is cut. This is an executable check against a closed form, not by itself a theorem about every autograd operation. Proof coverage for individual forward and backward rules lives in the proof modules described earlier in the guide.

Source: AutogradBasics.lean.

7.7.3. Lab Three: Train A Small MLP🔗

The training quickstart generates 25 regression samples for a two-input, one-output function and uses a hidden layer of width eight.

lake exe torchlean quickstart_mlp \
  --device cpu --steps 20 --seed 2026

The command reports the same two probes before and after optimization:

== Quickstart: simple MLP training ==
seed  = 2026
steps = 20
dataset size = 25
mean_loss(before) = 0.761530
predictions(before)
  center:  x=(0.000000,0.000000)   target=0.200000  pred=[0.000000]
  heldout: x=(0.250000,-0.750000) target=0.200000  pred=[0.043283]
step 0: loss=0.000866
mean_loss(after) = 0.459876
predictions(after)
  center:  x=(0.000000,0.000000)   target=0.200000  pred=[-0.132460]
  heldout: x=(0.250000,-0.750000) target=0.200000  pred=[0.012967]
steps=20 loss0=0.761530 loss1=0.459876
predict(heldout) = [0.012967]

The step-zero loss belongs to the first streamed sample, while mean_loss covers the dataset. Their labels prevent those two statistics from being confused.

7.7.3.1. Change The Numerical Meaning🔗

The quickstart is scalar-polymorphic enough to run with the executable binary32 model:

lake exe torchlean quickstart_mlp \
  --device cpu --dtype ieee754exec \
  --steps 2 --seed 2026

This selects IEEE32Exec for the covered path. The general model-zoo trainers are mostly native Float applications, so do not assume that every subcommand accepts this dtype. Command-specific validation rejects unsupported combinations.

7.7.3.2. Train Longer🔗

At 200 updates with the same seed, the held-out prediction is close to its target:

lake exe torchlean quickstart_mlp \
  --device cpu --steps 200 --seed 2026
mean_loss(before) = 0.761530
mean_loss(after) = 0.003234
heldout x=(0.25,-0.75), target=0.2, prediction(after)=[0.210239]

This demonstrates learning on one generated problem. It is neither a convergence theorem nor a generalization bound.

Source: SimpleMlpTrain.lean.

7.7.4. Lab Four: Learn From Data You Can See🔗

Before introducing files, downloads, or preprocessing scripts, it helps to train once on a dataset small enough to draw on paper. TorchLean's band samples are single-channel 4\times4 images. Class zero contains a vertical band; class one contains a horizontal band. Three offsets per class make the six training examples.

Run the compact classifier:

lake exe torchlean quickstart_cnn \
  --device cpu --steps 2 --batch 2 --seed 2026

The current run ends with:

dataset size = 3
mean_loss(before) = 0.689656
mean_loss(after) = 0.685960
steps=2 loss0=0.689656 loss1=0.685960
vertical-1 expected=0 = [[0.125427, -0.069819],
                         [0.125427, -0.069819]]

Why does a six-image dataset report size three? --batch 2 packs the samples into three batches. The final line repeats the same probe twice to form a batch and prints two logits per copy. The first logit is larger in this run, so the probe's predicted class is the expected vertical class. That is a useful sanity check, not an accuracy theorem.

The reusable definitions in NN.API.Data.Bands separate rendering from training. renderBand constructs one image from an axis, offset, and thickness; dataset combines any list of class descriptions and offsets; train and probes cast the concrete Float images into a supported scalar context. The quickstart uses the canonical four-by-four data through Data.Bands.dataset.

This tiny problem removes dataset provenance and parser behavior from the experiment, which makes it good for debugging model construction, batching, loss, and optimization. It does not tell us how the same CNN behaves on natural images. The CIFAR case studies earlier in this part address that different question.

Source: SimpleCnnTrain.lean.

7.7.5. Lab Five: Lower A Model And Bound Its Output🔗

The previous labs evaluated one concrete input at a time. Interval bound propagation starts from an input box

x\in[\ell,u]

and computes an output box enclosing every supported model evaluation in that region.

Run the registered workflow:

lake exe verify -- torchlean-ibp

Current output:

=== TorchLean → IR → IBP (small MLP) workflow ===
[TorchLean] Float32 mode: IEEE32Exec: executable IEEE-754 binary32 kernel
compiled IR nodes: 20
output box lo: [1.904000]
output box hi: [2.256000]

The path is:

TorchLean model
  -> canonical IR
  -> supported-operation check
  -> IEEE32Exec interval propagation
  -> output box

For an affine layer y=Wx+b, IBP separates positive and negative weights:

\begin{aligned} \ell'_i &=\sum_j \left(\max(W_{ij},0)\ell_j+\min(W_{ij},0)u_j\right)+b_i,\\ u'_i &=\sum_j \left(\max(W_{ij},0)u_j+\min(W_{ij},0)\ell_j\right)+b_i. \end{aligned}

For ReLU,

[\ell_i,u_i] \longmapsto [\max(0,\ell_i),\max(0,u_i)].

The command's result belongs to the supported IR fragment and numerical policy used by this workflow. It does not certify an arbitrary model command merely because that command also contains linear layers and ReLUs.

Discover the other registered workflows with:

lake exe verify -- list

The implementation of this path is reachable from NN/Verification/CLI.lean.

7.7.6. Lab Six: Replay A Numerical Certificate🔗

The numerical-certificate example moves from an in-memory bound result to a checked artifact. Run:

lake exe torchlean numerical_certificate

The command checks positive and negative cases:

TorchLean numerical runtime certificate
  ok  base certificate
  ok  base IEEE replay
  ok  tampered range rejected
  ok  misplaced source rejected
  ok  duplicate contract rejected
  ok  registry mismatch rejected
  ok  unsupported operation rejected
  ok  fixed-left reduction
  ok  portable matmul
  ok  CUDA matmul policy rejected
  ok  directed sqrt
  ok  negative sqrt domain rejected
  ok  portable LayerNorm
  ok  CUDA LayerNorm policy rejected
  ok  stable softmax
  ok  two-layer MLP certificate
  ok  two-layer MLP IEEE replay
All numerical certificate checks passed.

A certificate is useful only if malformed evidence is rejected. That is why the example includes a tampered range, an unsupported operation, a bad square-root domain, and numerical policies that do not match the declared backend.

The graph checker and its proof layer are:

The certificate uses outward-rounded IEEE32Exec ranges and can replay concrete inputs in the bit-level interpreter. A native runtime remains a separate provider whose agreement requires the appropriate backend evidence.

To inspect the scalar boundary behind that certificate, also run:

lake exe torchlean float32_modes

It contrasts proof-level rounding, executable IEEE binary32 behavior, and native-runtime agreement on finite, exceptional, and non-associative examples. It is a semantics demonstration; the numerical-certificate command is the separate graph-level checker.

7.7.7. Lab Seven: An External Graph Producer🔗

TorchLean can ask PyTorch to export model graphs and then parse the resulting artifact:

lake exe torchlean pytorch_export_check

The command accepts supported examples such as small MLP, CNN, normalization, Transformer-shaped, and single-head-attention graphs. It deliberately rejects unsupported structures, including multi-head attention outside the currently implemented lowering fragment.

The important chain is:

Python/PyTorch producer
  -> JSON value graph
  -> Lean parser
  -> canonical TorchLean IR
  -> WellShaped result or explicit rejection

Acceptance proves that the parsed graph is well shaped through the parser theorem. It does not prove PyTorch's exporter, Python interpreter, or original model source correct.

Source: TorchExportCheck.lean.

7.7.8. Building The Example Suite🔗

Before changing maintained examples, build the curated umbrella:

lake build NNExamples

That checks elaboration across the example tree. It does not execute every long-running model or external dependency. Runtime regressions, CUDA checks, dataset preparation, and certificate replay remain separate validation steps.