Some neural-network properties are independent of any particular GPU kernel or training loop.
Hopfield networks have an energy argument. ReLU networks have exact algebraic identities that make
larger approximation constructions possible. Recurrent state-space models are causal because their
output at time t is computed before future inputs are seen. These are structural facts about the
mathematical models.
We can study these properties directly in TorchLean's mathematical specifications. To carry a
result over to a CUDA implementation, we would also need to connect that implementation to the
specification used by the theorem.
A TorchLean Hopfield state is a Boolean vector, and its parameters are a weight matrix together
with a threshold vector. The energy argument follows Hopfield (1982) for
asynchronous updates. The following commands check the state and parameter types:
-- Inspect the dimension-indexed state and parameter types-- used by both rational and real models.State : ℕ→Type#check@StateParams : Type→ℕ→Type#check@Params
State : ℕ→Type
State n unfolds to Fin n → Bool. The numeric activation map reads true as +1 and false as
-1, so the Boolean carrier is a storage choice rather than a claim that the model is Boolean
valued. Params α n holds the fields W : Fin n → Fin n → α and θ : Fin n → α, and it is
polymorphic in the scalar: the same structure serves the executable Rat runs below and the
real-valued theorems that follow.
Fin n is the type of valid neuron indices, so a state is a function that returns one bit for each
neuron without admitting an out-of-range coordinate. List.ofFn in the examples merely displays
those bits in index order. It does not change the state representation or supply the numeric
activation map. This distinction matters when reading the energy table: a printed false
contributes minus one to the sums, not zero. In the type of energy, [Field α] requests the
arithmetic operations and laws used to form the expression. An energy-decrease result additionally
needs an order and assumptions connecting the weights to the update rule; those appear in the
real theorem rather than in the parameter record.
For state s, write x_i\in\{-1,+1\} for its numeric activation. The net input to neuron u is
The factor of one half is why the scalar has to be more than a ring:
-- The energy expression uses division by two, supplied by-- the scalar field instance.@energy : {α:Type}→[Fieldα]→{n:ℕ}→Paramsαn→Staten→α#check@energy
Rat supplies this field structure and supports exact executable arithmetic. It therefore lets
the experiments below evaluate the energy without rounding.
-- The order conclusion applies to real weights satisfying-- symmetry and a zero diagonal.@energy_updateAt_le : ∀{n:ℕ}(p:Paramsℝn),SymmetricWp→DiagonalZerop→∀(s:Staten)(u:Finn),energyp(updateAtpsu)≤energyps#check@energy_updateAt_le
Params allows arbitrary weights; SymmetricW and DiagonalZero restrict the networks covered
by the theorem. This separation also lets us execute asymmetric networks and examine where the
energy argument fails.
The proof expands the quadratic energy difference. Symmetry makes the changed row and column
contribute the same net-input term, while the zero diagonal removes the self-interaction. When the
coordinate changes and the net input is not tied with the threshold,
energy_updateAt_lt_of_change_of_ne strengthens the inequality to a strict decrease.
Writing the old and new activations at the changed coordinate as x_u and x'_u, the
cancellation gives
If a negative activation becomes positive, the first factor is two and the threshold comparison
makes the second nonpositive. If a positive activation becomes negative, the first factor is
minus two and the second is positive. Both cases give a nonpositive product. A tied negative
activation can become positive with the second factor zero, explaining why an actual state change
need not decrease energy strictly. This calculation tells us exactly where the tie example and
the secondary progress measure enter the argument.
Start with two mutually excitatory neurons, zero thresholds, and initial state [+1,-1].
Each neuron receives the other neuron's activation with weight one:
-- Use mutual excitation so neuron one copies the positive-- activation of neuron zero.defexcitatory:ParamsRat2whereW:=funij=>ifi=jthen0else1θ:=fun_=>0defbefore:State2:=funi=>i=0defafter:State2:=updateAtexcitatorybefore1
The states before and after updating neuron 1:
-- Display the Boolean states in neuron order; true and-- false encode positive and negative-- activation.[true,false]#evalList.ofFnbefore[true,true]#evalList.ofFnafter
[true,false]
[true,true]
And their energies:
-- Compute the two rational energies exactly, before and-- after the single-coordinate update.1#evalenergyexcitatorybefore-1#evalenergyexcitatoryafter
1
-1
Neuron 1 receives net input +1, so its activation changes from -1 to +1. The two
off-diagonal products in the energy change from negative to positive, lowering the exact rational
energy from 1 to -1.
Changing W 0 1 without changing W 1 0 still produces an executable state sequence, but it
prevents use of energy_updateAt_le: Lean asks for SymmetricW p. Setting a diagonal weight to a
nonzero value similarly leaves the program runnable while invalidating the theorem's
DiagonalZero p premise.
If every state change strictly lowered energy, finiteness would immediately rule out cycles. Ties
make the argument subtler. With the convention “ties go to +1,” a state may change while energy
stays equal. TorchLean therefore uses the number of positive neurons,
as a secondary progress measure. For one full cyclic sweep, cycleUpdate_progress proves:
either energy strictly decreases;
or energy is unchanged and pluses strictly increases.
-- A changed sweep must decrease energy or increase the-- positive count at unchanged energy.@cycleUpdate_progress : ∀{n:ℕ}(p:Paramsℝn),SymmetricWp→DiagonalZerop→∀(s:Staten),cycleUpdateps≠s→energyp(cycleUpdateps)<energyps∨energyp(cycleUpdateps)=energyps∧pluses(cycleUpdateps)>plusess#check@cycleUpdate_progress
The second case occurs when all
weights and thresholds are zero, so every net input ties with its threshold:
-- Zero weights and thresholds force a tie at every-- coordinate.defzeroNet:ParamsRat2whereW:=fun__=>0θ:=fun_=>0deftiedBefore:State2:=fun_=>falsedeftiedAfter:State2:=updateAtzeroNettiedBefore0
The state changes, because a tie resolves to +1:
-- Updating a tied negative coordinate demonstrates the-- convention that ties become positive.[false,false]#evalList.ofFntiedBefore[true,false]#evalList.ofFntiedAfter
[false,false]
[true,false]
Every term in the energy has a zero coefficient, so both energies are zero:
The positive count increases even though the energy stays fixed:
-- The positive count detects the progress that energy alone-- misses in the tied update.0#evalplusestiedBefore1#evalplusestiedAfter
0
1
This single-coordinate example exhibits equal energy with an increased positive count. A strict
comparison would instead send ties to -1: this initial state would stay fixed, but tied positive
states could still change at equal energy. Such a convention needs the opposite count as its
secondary measure. Keeping the previous activation on ties would avoid tie-induced changes.
The lexicographic pair
\bigl(E(s),-\operatorname{pluses}(s)\bigr)
therefore progresses whenever a sweep changes the state. Since State n is finite,
cycleUpdate_no_nontrivial_cycles
rules out a nontrivial cycle, and cycleUpdate_exists_fixedpoint_le_card gives a fixed point within
at most Fintype.card (State n) sweeps. The more explicit
cycleUpdate_exists_fixedpoint_le_pow states the corresponding 2^n bound.
The following declarations expose the strict-decrease lemma, the exclusion of cycles, and the two
fixed-point bounds:
-- Inspect the strict-update lemma and the finite bound on-- repeated cyclic sweeps.@energy_updateAt_lt_of_change_of_ne : ∀{n:ℕ}(p:Paramsℝn),SymmetricWp→DiagonalZerop→∀(s:Staten)(u:Finn),updateAtpsu≠s→netpsu≠p.θu→energyp(updateAtpsu)<energyps#check@energy_updateAt_lt_of_change_of_ne@cycleUpdate_no_nontrivial_cycles : ∀{n:ℕ}(p:Paramsℝn),SymmetricWp→DiagonalZerop→∀{k:ℕ},0<k→∀(s:Staten),(fp)^[k]s=s→fps=s#check@cycleUpdate_no_nontrivial_cycles@cycleUpdate_exists_fixedpoint_le_card : ∀{n:ℕ}(p:Paramsℝn),SymmetricWp→DiagonalZerop→∀(s0:Staten),∃m≤Fintype.card(Staten),(fp)^[m+1]s0=(fp)^[m]s0#check@cycleUpdate_exists_fixedpoint_le_card@cycleUpdate_exists_fixedpoint_le_pow : ∀{n:ℕ}(p:Paramsℝn),SymmetricWp→DiagonalZerop→∀(s0:Staten),∃m≤2^n,(fp)^[m+1]s0=(fp)^[m]s0#check@cycleUpdate_exists_fixedpoint_le_pow
(f p)^[m] is Mathlib's iterated-function notation, so the conclusion reads: within 2^n sweeps,
one more sweep changes nothing. The _le_card variant says the same thing with
Fintype.card (State n) in place of 2^n; the explicit power is the one to quote, and the
cardinality form is the one the proof actually produces.
A decreasing real sequence need not reach its limit after finitely many steps. What makes this
argument terminate is that the energy and count are evaluated on a finite set of states. Before
a fixed point is reached, a repeated state would close a cycle, contradicting the progress result.
There are only 2 ^ n possible Boolean functions, so enough distinct sweep states exhaust the
possibilities. The existential ∃ m ≤ 2 ^ n supplies a stopping index, and the equality compares
successive iterates at that index. It does not say that the fixed point is unique, nor that every
starting state reaches the same one.
These are theorems about asynchronous coordinate updates arranged into cyclic sweeps. They do not
apply automatically to synchronous updates, stochastic schedules, modern continuous-state
Hopfield layers such as the one of Ramsauer et al. (2021), or a floating-point
kernel. Each variation needs its own transition relation and
energy argument.
which is symmetric and has zero diagonal. These are the weight conditions needed by the real
convergence theorem. The following rational experiment uses the same construction with four
neurons, storing (+1,+1,-1,-1):
-- Build the zero-diagonal outer product and a rational-- sweep in increasing neuron order.defxi:Fin4→Rat:=funi=>ifi.val<2then1else-1defhebbNet:ParamsRat4whereW:=funij=>ifi=jthen0elsexii*xijθ:=fun_=>0-- The computable twin of `cycleUpdate`, which is-- `noncomputable` because it is stated over `ℝ`.defsweepRat(s:State4):State4:=(List.finRange4).foldl(funsu=>updateAthebbNetsu)sdefpattern:State4:=funi=>i.val<2defnoisy:State4:=funi=>i.val=0
noisy is the stored pattern with one bit wrong. One sweep repairs it:
-- Check whether one sweep corrects this particular one-bit-- corruption.[true,false,false,false]#evalList.ofFnnoisy[true,true,false,false]#evalList.ofFn(sweepRatnoisy)true#evalList.ofFn(sweepRatnoisy)==List.ofFnpattern
[true,false,false,false]
[true,true,false,false]
true
The energy decreases and the positive count increases in this run. Strict energy decrease is
already sufficient for progress; the theorem only needs the count when the energy stays equal:
-- Compare energy progress with the auxiliary count for the-- stored pattern and noisy start.0#evalenergyhebbNetnoisy-6#evalenergyhebbNetpattern1#evalplusesnoisy2#evalpluses(sweepRatnoisy)
0
-6
1
2
For a single stored pattern, the energies can be calculated from its overlap with the current
state. The double sum collapses because
\sum_{i,j}\xi_i\xi_jx_ix_j=(\xi\cdot x)^2 and the zero diagonal removes the n self terms:
E(s)=\frac{n-(\xi\cdot x)^2}{2}.
With n=4, the pattern itself has \xi\cdot x=4 and energy (4-16)/2=-6, while noisy has
\xi\cdot x=2 and energy 0. The formula also says what the minima are: energy is smallest
exactly when |\xi\cdot x| is largest, which happens at x=\xi and at x=-\xi. For this
zero-threshold, single-pattern network, both patterns are stable, as checked here:
-- Test both the stored pattern and its complement for-- stability under every coordinate update.defcomplement:State4:=funi=>¬(i.val<2)defstable(s:State4):Bool:=decide(∀u:Fin4,updateAthebbNetsu=s)-6#evalenergyhebbNetcomplementtrue#evalstablepatterntrue#evalstablecomplement
-6
true
true
stable is IsStable from the spec, evaluated: no single-coordinate update changes the state. Both
\xi and -\xi pass it. Since State 4 has sixteen inhabitants, we can enumerate every
initial state and check where one sweep takes it:
-- Enumerate all four-bit starts and count the two basins-- after one sweep.defallStates:List(State4):=(List.range16).mapfunk=>funi:Fin4=>(k/2^i.val)%2==1defbasin(s:State4):ListBool:=List.ofFn(sweepRats)true#evalallStates.all(funs=>stable(sweepRats))8#eval(allStates.filter(funs=>basins==List.ofFnpattern)).length8#eval(allStates.filter(funs=>basins==List.ofFncomplement)).length
true
8
8
Every one of the sixteen states is at a fixed point after a single sweep, and eight states reach
each attractor.
cycleUpdate_exists_fixedpoint_le_pow promises a fixed point within 2^n=16 sweeps; this network
needs one. The bound counts states because its proof uses finiteness and lexicographic progress.
It does not analyze the particular weights or update order that make this example converge
quickly. A sharper bound for a family of networks would need that additional analysis.
The three outputs of hebbBasins answer three different questions. The Boolean asks whether
every enumerated start is stable after one sweep. The two natural numbers count starts whose
result equals each named pattern. Here allStates decodes all four-bit integers, so its sixteen
entries cover this state space rather than a sample of it. The split of eight and eight is a
cardinality statement. Interpreting it as a fifty-percent recall probability would require
choosing a uniform distribution on those starts. A noise process concentrated near the stored
pattern could have a different recall probability even with exactly the same dynamics.
The theorem guarantees that the sweep sequence stops, but does not identify the final state.
Half of these starts recover \xi and half
recover -\xi, and for a memory with several stored patterns there are spurious mixture states
too. Convergence and correct recall are different claims, and only the first one is proved here.
Add a second pattern that
differs from xi in a single coordinate, summing the two outer products as the Hebbian rule
prescribes:
-- The correlated second pattern cancels every off-diagonal-- connection to the last neuron./-- A second pattern, agreeing with `xi` except at the
last coordinate. -/defxi2:Fin4→Rat:=funi=>ifi.val=3then1elsexiideftwoPatterns:ParamsRat4whereW:=funij=>ifi=jthen0elsexii*xij+xi2i*xi2jθ:=fun_=>0defsweepWith(p:ParamsRat4)(s:State4):State4:=(List.finRange4).foldl(funsu=>updateAtpsu)sdefstableWith(p:ParamsRat4)(s:State4):Bool:=decide(∀u:Fin4,updateAtpsu=s)defpattern2:State4:=funi=>i.val<2||i.val=3[[0,2,-2,0],[2,0,-2,0],[-2,-2,0,0],[0,0,0,0]]#eval(List.finRange4).mapfuni=>(List.finRange4).mapfunj=>twoPatterns.Wij
[[0,2,-2,0],[2,0,-2,0],[-2,-2,0,0],[0,0,0,0]]
The last row is zero because neuron 3 is the coordinate where the two
patterns disagree, so its two contributions \xi_3\xi_j and \xi'_3\xi'_j are equal and
opposite for every other neuron j. Its net input is therefore always zero. With a zero threshold,
every update of neuron 3 is a tie and sets its activation to +1.
Both stored patterns are still global energy minima, and the energy cannot even see the coordinate
they disagree on:
Both patterns sit at energy -6, which is the minimum over all sixteen states, and
only one of them is a fixed point. Starting the network at the pattern we stored moves it away,
to the other one, in a single sweep.
twoPatterns is symmetric with zero diagonal, so its real-valued counterpart satisfies the
hypotheses of cycleUpdate_progress and cycleUpdate_exists_fixedpoint_le_pow. Reaching a
fixed point is consistent with failing to recall one of the stored patterns. Enumerating the
initial states shows how the tie rule affects recall:
Twelve of the sixteen starts land on pattern2, and every start reaches a fixed point after one
sweep. There are two fixed points in total. Of the four energy-minimizing states, the two with
neuron 3 negative change on a tie; the two with that neuron positive remain fixed. Thus the tie
rule affects both which minima are stable and the secondary measure used in the convergence proof.
This small example demonstrates cross-talk through an exactly zero row. Asymptotic capacity
estimates for large networks with random patterns do not supply a threshold for these four neurons
and deliberately correlated patterns. Here the printed matrix and exhaustive state checks explain
the failure directly.
The row of zeros also explains why a global energy minimum can fail the stability check. Energy
is indifferent to neuron three, but the update rule is not: it always chooses the positive
activation on that row. Thus the Boolean false for stableWith twoPatterns pattern is
consistent with the energy value -6; the two commands test different predicates. For an
associative-memory application, a useful further specification would ask that each stored
pattern be stable, and then describe which corrupted inputs return to it. The convergence theorem
supplies neither condition merely from the way the weights were constructed.
The same single-pattern update can be written in PyTorch:
# Mutate coordinates in order so each neuron reads the
# preceding updates in the same sweep.
import torch
xi = torch.tensor([1., 1., -1., -1.])
W = torch.outer(xi, xi)
W.fill_diagonal_(0.)
x = torch.tensor([1., -1., -1., -1.])
for u in range(4):
x[u] = 1.0 if W[u] @ x >= 0 else -1.0
assert torch.equal(x, xi)
The loop directly exposes the asynchronous dynamics. What
it checks is one pattern, from one start, at one precision. The #eval above checks all sixteen
starts for this rational network. cycleUpdate_no_nontrivial_cycles proves the absence of
nontrivial sweep cycles for every real symmetric zero-diagonal network, at every finite size.
Two details of that loop deserve attention. It writes >=, so ties go to +1; writing > instead
would also run, would also pass this assertion, and would quietly change the dynamics at exactly
the states where pluses was needed. And x is mutated inside the loop, so neuron u already sees
the updates from neurons before it. That is the asynchronous schedule the theorems assume. A
vectorized x = torch.sign(W @ x) reads the old state for every coordinate, giving a synchronous
update that can oscillate. It also returns zero at a zero net input, unlike the Boolean model's
tie rule. The examples below avoid zero net inputs when comparing the two schedules.
The convergence theorems assume SymmetricW, DiagonalZero, and a cyclic asynchronous schedule.
The next examples show what fails when symmetry or the asynchronous schedule is removed.
Drop symmetry first. Two neurons, W 0 1 = 1 and W 1 0 = -3: neuron 0 is excited by neuron 1
while neuron 1 is inhibited by neuron 0.
-- Opposite signs across the off-diagonal entries-- deliberately violate the symmetry premise.defasymNet:ParamsRat2whereW:=funij=>ifi=jthen0elseifi.val<j.valthen1else-3θ:=fun_=>0defsweep2(p:ParamsRat2)(s:State2):State2:=(List.finRange2).foldl(funsu=>updateAtpsu)sdefbothUp:State2:=fun_=>true[[true,true],[true,false],[false,true],[true,false],[false,true]]#eval(List.range5).mapfunm=>List.ofFn((sweep2asymNet)^[m]bothUp)
The sweep sequence enters a two-cycle. Each sweep sets neuron 0 to the old value of neuron 1,
then sets neuron 1 to the opposite of the new value of neuron 0. A fixed point would therefore
have to make the two neurons both agree and disagree. No fixed point exists, and the conclusion
of cycleUpdate_exists_fixedpoint_le_pow fails for this network. The single-coordinate energy
inequality fails too:
-- Inspect the individual update where the asymmetric-- network increases energy.defasymMixed:State2:=funi=>i.val=0-1#evalenergyasymNetasymMixed1#evalenergyasymNet(updateAtasymNetasymMixed0)
-1
1
Energy increases from -1 to 1. The reason for SymmetricW p is visible in the
arithmetic: the energy only ever sees the symmetric part (W_{ij}+W_{ji})/2, while the update rule
reads the raw row. When they disagree, an update can increase energy. In this example the energies
at the ends of full sweeps never increase, so inspecting only sweep boundaries would miss the
failed single-coordinate inequality.
Now keep symmetry and change only the schedule. The classic vectorized one-liner
x = torch.sign(W @ x) updates every neuron from the same old state, and on a symmetric network
with no diagonal it can cycle forever:
-- Keep weights and tie handling fixed while comparing-- old-state and sequential updates.defantiNet:ParamsRat2whereW:=funij=>ifi=jthen0else-1θ:=fun_=>0/-- Every neuron reads the *old* state. This is the update
the theorems on this page do not cover. -/defsyncStep(p:ParamsRat2)(s:State2):State2:=funu=>decide(p.θu≤netpsu)[[true,true],[false,false],[true,true],[false,false]]#eval(List.range4).mapfunm=>List.ofFn((syncStepantiNet)^[m]bothUp)[[true,true],[false,true],[false,true],[false,true]]#eval(List.range4).mapfunm=>List.ofFn((sweep2antiNet)^[m]bothUp)
Same network, same start, same tie rule, two schedules. The synchronous one flips both neurons at
once and oscillates with period two forever. The asynchronous sweep reaches a fixed point on the
first pass, because neuron 1 already sees what neuron 0 decided. Synchronous Hopfield dynamics
have their own theory, with a Lyapunov function on pairs of consecutive states rather than on single
states; none of it is what cycleUpdate_no_nontrivial_cycles proves.
The approximation developments use exact ReLU identities to assemble larger networks. The module
ReLUMlpBridge
proves
\operatorname{ReLU}(u)-\operatorname{ReLU}(-u)=u.
In Lean:
-- The exact identity recovers a signed value from its two-- nonnegative ReLU parts.relu_sub_relu_neg : ∀(u:ℝ),NN.MLTheory.Proofs.UniversalApproximation.reluu-NN.MLTheory.Proofs.UniversalApproximation.relu(-u)=u#check@relu_sub_relu_neg
The fully qualified relu in that output is the definition used by the approximation development.
Reusing that definition
lets these real-valued
lemmas compose without an additional equivalence lemma.
Since relu u is max u 0, one split on the sign of the input proves the identity:
-- On either side of zero one hinge vanishes, leaving the-- signed input after subtraction.example(u:ℝ):maxu0-max(-u)0=u:=u:ℝ⊢ maxu0-max(-u)0=uu:ℝh:0≤u⊢ maxu0-max(-u)0=uu:ℝh:u≤0⊢ maxu0-max(-u)0=uu:ℝh:0≤u⊢ maxu0-max(-u)0=uAll goals completed! 🐙·inru:ℝh:u≤0⊢ maxu0-max(-u)0=urw[max_eq_righth,inru:ℝh:u≤0⊢ 0-max(-u)0=umax_eq_left(neg_nonneg.mprh),inru:ℝh:u≤0⊢ 0--u=uzero_sub,inru:ℝh:u≤0⊢ --u=uneg_neginru:ℝh:u≤0⊢ u=u]All goals completed! 🐙
le_total 0 u produces the two possible order relations, including equality in both branches.
In the first branch, h proves that the positive hinge equals u, while
neg_nonpos.mpr h proves that the other hinge is zero. In the second, the roles reverse and
subtracting -u recovers u. The proof therefore covers zero without an extra exceptional case.
Each rewrite names the order fact that permits selecting an argument of max.
In relu u - relu (-u) one of the two operands is always the exact
zero, so for finite IEEE values the result has the same numerical value, including at zero. Bitwise
signed-zero preservation is a separate question:
-- Compare numerical Float equality on these finite inputs;-- this does not compare signed-zero bits.defreluF(u:Float):Float:=maxu0.02.500000#evalreluF2.5-reluF(-2.5)[true,true,true,true,true,true]#evalList.map(funu=>reluFu-reluF(-u)==u)[0.0,1.0e-45,-1.0e-45,0.1,-3.5e37,1.0e38]
2.500000
[true,true,true,true,true,true]
Lean's Float is binary64, and the list covers zero, magnitudes that would be subnormal in
binary32, a decimal that no binary format stores exactly, and a magnitude near the binary32
maximum. These comparisons use exact numerical equality: x - 0 and 0 - x are exact for
every finite x in every IEEE 754 format, so the identity holds in binary32 and binary16 for the
same reason it holds here. These tests do not establish a bitwise identity for signed zero or NaNs,
nor do they extend the
real theorem to a backend without a refinement argument.
This identity lets a ReLU network carry an affine term exactly even though each hidden unit clips
negative values. Two units retain its positive and negative parts; subtracting their outputs
recovers the affine term. The bounded-box multiplication construction uses this identity:
-- Inspect the bounded-domain hypotheses and the existential-- pair of real network layers.@relu_mul_universal_approximation_box : ∀{M:ℝ},0<M→∀ε>0,∃hidDiml1l2,∀x∈boxM,|mulFunx-mlpEvall1l2x|<ε#check@relu_mul_universal_approximation_box
The assumption 0 < M specifies a nondegenerate bounded box. A finite ReLU network is piecewise
affine; along the diagonal of \mathbb R^2, multiplication grows quadratically. Its error
therefore cannot remain uniformly small on the whole plane. Restricting the domain to a box makes
the stated approximation possible.
The construction reduces multiplication to two one-dimensional square approximations through
uv=((u+v)^2-(u-v)^2)/4. If |u|,|v|\leq M, both arguments of the square lie in
[-2M,2M], which is why a bounded input box gives a common interval for the two approximants.
The source chooses square error below 2\varepsilon for each occurrence. Subtraction adds
the two absolute-error bounds, and division by four leaves error below \varepsilon.
The returned layers have input dimension two and output dimension one; hidDim records the
width needed to combine the constructions. This is a concrete way that a scalar approximation
lemma becomes a component for a model with several input features.
Read the existential witnesses in order: Lean first chooses the hidden dimension, then two
layers whose types use that dimension. Only after those choices does the statement quantify over
inputs in box M. A proof cannot satisfy it by selecting a new network for each input point.
The same returned pair of layers must meet the error budget throughout the entire box.
relu_mul_universal_approximation_box uses this algebra inside a uniform approximation argument.
For its construction, quantitative hypotheses, and the distinction between existence, checkpoint
verification, and finite-precision execution, return to Approximation Theory.
The theorem says that appending a future suffix ys preserves every output already produced for
the prefix xs.
MambaCausality
proves this statement for three increasingly rich specifications, following the selective
state-space construction of Gu and Dao (2024):
DiagonalS4Spec, the diagonal case of the structured state space of
Gu et al. (2022);
MambaBlockSpec;
SelectiveMambaBlockSpec, including its carried convolution history.
The selective theorem includes both recurrent state and convolution history. The following
example states its prefix equality and proves it by applying the library theorem:
-- Both runs share the model and initial state; only the-- future suffix is extended.example{α:Type}[Storageα][Contextα]{inputDimstateDimoutputDiminnerDimconvWidth:ℕ}(m:Models.SelectiveMambaBlockSpecαinputDiminnerDimstateDimoutputDimconvWidth)(h0:Tensorα[innerDim,stateDim])(xsys:Array(Tensorα[inputDim])):(m.runArrayh0(xs++ys)).snd.takexs.size=(m.runArrayh0xs).snd:=selectiveMamba_runArray_append_outputs_prefixmh0xsys
The five dimensions are arbitrary natural numbers. The scalar type needs Storage and Context
instances, which provide the tensor representation and operations used by the specification.
The same statement therefore covers Float, Rat, and ℝ instances.
Its content is one equation. runArray returns the final recurrent state together with the array of
outputs, and the statement is about the outputs component: run the block on xs ++ ys, keep the
first xs.size outputs, and you get exactly what running it on xs alone produces. Appending
future tokens cannot disturb an earlier output.
The example declaration is itself a proposition whose proof is the named theorem on its final
line. There is no by block because applying that theorem already produces a term of exactly
the required equality type. Square-bracketed arguments such as [Context α] are instances Lean
supplies from the scalar type, while m, h0, xs, and ys are the actual model and data. The
same m and h0 occur on both sides. Comparing two runs that also change the initial state
would be a different statement: earlier outputs can depend on that state even when they cannot
depend on a later suffix.
The theorem is polymorphic over any scalar α with a TorchLean Context. Its proof is structural:
induct on xs, unfold one recurrent step, and apply the induction hypothesis to the updated state
and history. It does not require commutative or exact arithmetic because causality depends on
evaluation order, not algebraic rearrangement.
The selective block's causal convolution needs a window of
recent inputs, so the internal runner threads a newest-first history alongside the recurrent state.
That runner is where the induction happens, and it is stated separately:
-- Keep the initial convolution history fixed when comparing-- the two output prefixes.example{α:Type}[Storageα][Contextα]{inputDimstateDimoutputDiminnerDimconvWidth:ℕ}(m:Models.SelectiveMambaBlockSpecαinputDiminnerDimstateDimoutputDimconvWidth)(h0:Tensorα[innerDim,stateDim])(history:Array(Tensorα[innerDim]))(xsys:Array(Tensorα[inputDim])):(m.runArrayWithHistoryh0history(xs++ys)).snd.takexs.size=(m.runArrayWithHistoryh0historyxs).snd:=selectiveMamba_runArrayWithHistory_append_outputs_prefixmh0historyxsys
The public theorem above is the history := #[] instance of this one, obtained by
simpa [Models.SelectiveMambaBlockSpec.runArray]. A streaming implementation may resume with
earlier inputs still in its convolution window; the history theorem covers that case.
A carried history is part of the starting configuration, just like the recurrent tensor.
runArrayWithHistory compares runs with the same history on both sides, so a previously buffered
input may affect an early convolution output in both runs. The induction can keep this invariant
because one step consumes the current token and passes the updated state and history to the next
step. No step receives the unprocessed suffix as an additional argument. This identifies what a
streaming implementation must preserve when splitting a sequence into chunks: dropping or
reordering the history changes the initial configuration to which the theorem applies.
The reusable array argument is factored through
Scan,
which proves append and prefix laws for state-threading scans. MambaCausality instantiates that
structure with the S4/Mamba state and convolution history; it does not assert that an optimized
selective-scan kernel refines the specification.
The two simpler specifications satisfy the same output equation with a smaller state:
-- The smaller recurrent state gives the same prefix-- equality for S4 and compact Mamba.example{α:Type}[Storageα][Contextα]{inputDimstateDimoutputDim:ℕ}(m:Models.DiagonalS4SpecαinputDimstateDimoutputDim)(h0:Tensorα[stateDim])(xsys:Array(Tensorα[inputDim])):(m.runArrayh0(xs++ys)).snd.takexs.size=(m.runArrayh0xs).snd:=diagonalS4_runArray_append_outputs_prefixmh0xsysexample{α:Type}[Storageα][Contextα]{inputDimstateDimoutputDim:ℕ}(m:Models.MambaBlockSpecαinputDimstateDimoutputDim)(h0:Tensorα[stateDim])(xsys:Array(Tensorα[inputDim])):(m.runArrayh0(xs++ys)).snd.takexs.size=(m.runArrayh0xs).snd:=compactMamba_runArray_append_outputs_prefixmh0xsys
Both equations compare the output prefix in the same way; the state shape and the spec
structure change. DiagonalS4Spec carries a single [stateDim] vector, MambaBlockSpec adds the
selection projections but keeps the same state shape, and the selective block above is the one that
grows to [innerDim, stateDim] plus a history.
The prefix length matters: the next output may depend on the first token of the suffix. To see
this, take an S4 block with one channel, A = B = C = 1, D = 0, and unit projections. Its state
accumulates the inputs, so output
t is the running total of tokens 0 through t.
-- Use a running sum so the first suffix-dependent output-- can be read directly from the numbers.openTorchLean.Tensor(ofFnmatrixgetScalar)-- One channel with `A = B = C = 1`, `D = 0`, so the state-- just accumulates and output `t` is the running total.defone1:TensorFloat[1]:=ofFn(fun_=>1.0)defzero1:TensorFloat[1]:=ofFn(fun_=>0.0)defaccumS4:Models.DiagonalS4SpecFloat111:={inProj:=matrix(fun__=>1.0)outProj:=matrix(fun__=>1.0)ssm:={A:=one1,B:=one1,C:=one1,D:=zero1}}deftok(v:Float):TensorFloat[1]:=ofFn(fun_=>v)/-- The prefix `xs` of the causality statement. -/defxsToks:Array(TensorFloat[1]):=#[tok1.0,tok2.0]/-- Outputs of `xsToks ++ ys`, read back as plain floats. -/defouts(ys:Array(TensorFloat[1])):ArrayFloat:=(accumS4.runArray(tok0.0)(xsToks++ys)).snd.map(funy=>getScalary0)#[1.000000, 3.000000, 8.000000]#evalouts#[tok5.0]#[1.000000, 3.000000, 12.000000]#evalouts#[tok9.0]true#eval(outs#[]).take2==(outs#[tok5.0]).take2false#eval(outs#[tok5.0]).take3==(outs#[tok9.0]).take3
#[1.000000, 3.000000, 8.000000]
#[1.000000, 3.000000, 12.000000]
true
false
The two futures follow the same prefix #[1.0, 2.0]. The first two outputs agree, 1.0 and
1.0 + 2.0. The third does not: 1 + 2 + 5 = 8 against
1 + 2 + 9 = 12. So .take 2, which is .take xs.size, matches the prefix-only run,
while .take 3, which is .take (xs.size + 1), differs across the two futures.
Using zero-based indices, output xs.size is the first output that may depend on the suffix.
The theorem fixes all earlier outputs. Extending the equality by one position would fail in this
accumulator example.
A corresponding PyTorch test compares a prefix-only run with the start of a longer run:
# Compare outputs for one shared prefix under the same model
# and starting configuration.
t = 8
y_short = model(x[:, :t])
y_long = model(x)
torch.testing.assert_close(y_long[:, :t], y_short)
This checks approximate agreement for one x, one t, and one set of weights. The Lean
statement quantifies over every
input array, every suffix, every initial state, and every scalar type with a Context instance,
which is why the proof is an induction on xs and not a comparison of two tensors.
The proof also does not need assert_close, because there is no tolerance in it. Causality is an
equality of prefixes, and prefix equality survives rounding: whatever the arithmetic did to produce
the first xs.size outputs, appending ys cannot reach back and change it. That is the reason this
theorem is polymorphic in the scalar while the approximation results are not.
The three developments establish different kinds of structure:
Development
Proved object
Not established by that theorem
Hopfield
finite real-valued energy and cyclic asynchronous dynamics
floating execution or arbitrary update schedules
ReLU approximation
existence of real MLP parameters with uniform error
training convergence or binary32 error
Mamba/S4
prefix preservation of spec-level array runners
equality with a particular fused scan kernel
The Hopfield example executes over Rat; the energy theorem is stated over ℝ. The Mamba
causality theorem works over an abstract Context, but a runtime refinement theorem is still
needed to connect a backend kernel to the spec runner. The ReLU theorem constructs real-valued
layers, while quantization and rounded execution require the finite-precision bridge described in
the approximation chapter.
A backend proof can use these results once it establishes that the relevant executable operation
implements the specification, with any arithmetic error accounted for.