Lean Verified Transformers
This post explores writing formally verified ML code in Lean. Since the cost of proofs is declining rapidly and the amount of code generated is skyrocketing, the value of verified code seems likely to climb. While understanding proofs remains challenging, collaborating with AI to get proofs of easy-to-understand properties seems like a natural middle ground.
The goal of this post is to verify foundational properties of Transformers. These are critical properties that are used for parallelization and optimization, including tensor parallelism, data parallelism, batch invariance, permutation invariance, correctness of tiling, and locality of sparse attention models. Code is available at srush/lean-transformer. The text, comments, and structure of the blog are all human-written; the proofs are all written by AI. Hopefully it can also serve as an advanced intro to Lean.
This project is inspired by TorchLean, Verified Deep Learning with Lean 4, and the Dex Programming Language.
Invariance and Equivariance
Different neural network architectures retain different properties of their input. We generally classify these properties in terms of equivariance and invariance. These allow researchers to reason about what they can learn, and implementers to optimize computation while maintaining equivalence. Our goal will be to prove equivariances and invariances for specific architectures.
Notationally, ML definitions often assume the same functions can work on different input shapes, e.g. batch sizes. For this reason, our Lean definition will be a bit complex to allow for functions that are polymorphic over the shape.
def Equivariant
-- Arguments with { } are implicit
{Shape : Type u}
{Input : Shape → Type v} {Output : Shape → Type w}
-- Arguments with ( ) are explicit
(f : {shape : Shape} → Input shape → Output shape)
{source target : Shape}
(T : Input source → Input target)
(S : Output source → Output target)
-- : gives the return type. Here it is a property.
: Prop :=
∀ x, f (T x) = S (f x)def Invariant
{Shape : Type u}
{Input : Shape → Type v} {Output : Type w}
(f : {shape : Shape} → Input shape → Output)
{source target : Shape}
(T : Input source → Input target) : Prop :=
∀ x, f (T x) = f xVectors, Matrices, and Neural Networks
We begin by building a simple neural network library in Lean.
The relu function takes in a number and returns its non-negative part.
Along with the definition, we prove it does what we claim.
def relu (z : Rat) : Rat := max z 0theorem relu_non_negative
-- For all z
(z : Rat) :
-- relu is ≥ 0
relu z ≥ 0
:= z:Rat⊢ relu z ≥ 0
-- Do a short (grind) search
All goals completed! 🐙Following the style of JAX, we lift scalar functions to operate on vectors. Vectors (and tensors) are represented as higher-order functions mapping indices to rational numbers. This makes our proofs easier since we do not have to care about storage or efficiency.
-- Vector type. Maps a finite set of {0,...,n-1} to a rational.
abbrev Vector (n : Nat) := Fin n → Rat-- Examples
-- [10, 10, 10, 10, 10]
def vector_of_tens_example: Vector 5 := fun _ => 10-- [0, 1, 2, 3]
def arange (n: Nat) : Vector n := fun i => i-- Greek letters are types.
variable {α : Type u} {β : Type v} {δ : Type w}-- vmap on 1-arg functions.
def vmap (fn: α -> β) {n : Nat} :
((Fin n -> α) -> (Fin n -> β)) :=
fun a => fun i => fn (a i)-- Example: vector vmap.
def vector_relu (z: Vector n) : Vector n :=
(vmap relu) z-- vmap on 2-arg functions
def vmap2 (fn: α -> β -> δ) : ((Fin n -> α) -> (Fin n -> β) -> (Fin n -> δ)) :=
fun a b => vmap (fun i => fn (a i) (b i)) id-- Add two vectors as + overload
instance : Add (Vector n) where
add := vmap2 (fun a b => a + b)-- Mul two vectors with * overload
instance : Mul (Vector n) where
mul := vmap2 (fun a b => a * b)For aggregations, we define a vector scan. Since we are using rationals for simplicity, we do not have an exponential, so we define a "softmax-like" nonlinear normalization instead.
-- Fold over vectors.
abbrev fori {α : Type u} {n : Nat} (f : Fin n → α) : List α := List.ofFn fdef scan (step : σ → α → σ) (xs : Fin n → α) (initial : σ) : σ :=
Fin.foldl n (fun state i => step state (xs i)) initial-- Sum is a fold
def Vector.sum (a : Vector n) : Rat :=
-- Alternative: scan (fun a b => a + b) a 0
(fori (fun i => a i)).sumdef softmax_like (z : Vector n) : Vector n :=
let weights : Vector n := vmap (fun x => 1 + relu x) z
let total := weights.sum
vmap (fun w => w / total) weightsdef Vector.dot_product (a b : Vector n) : Rat :=
(a * b).sumAs an exercise, let's look at a simple vector theorem. Click the square □ next to each line of the proof and it will show you the current proof state. The proof state divides the context from the goal ⊢. Each step will transform these terms until we can construct the goal.
-- Theorem: Multiplication distributes.
theorem Vector.mul_add
-- Given vectors a, b, c, of length n
(a b c : Vector n) :
-- then
a * (b + c) = a * b + a * c
:= n:Nata:Vector nb:Vector nc:Vector n⊢ a * (b + c) = a * b + a * c
-- Strategy: show equiv for all indices i of the output vector
n:Nata:Vector nb:Vector nc:Vector ni:Fin n⊢ (a * (b + c)) i = (a * b + a * c) i
-- Apply the rational property to the numbers at position i.
All goals completed! 🐙
Matrices are defined similarly. We are basically just stacking
vmap calls to get our core operations. Note the implementation of matmul
in particular, which will be the target of future proofs.
abbrev Matrix (n m : Nat) := Fin n → Vector minstance : Add (Matrix n m) where
add := vmap2 (fun a b => a + b)instance : Mul (Matrix n m) where
mul := vmap2 (fun a b => a * b)def Matrix.transpose (a : Matrix n m) : Matrix m n := fun i j => a j idef Matrix.matvec (a : Matrix n m) (x : Vector m) : Vector n :=
vmap (fun row => row.dot_product x) adef Matrix.matmul (a : Matrix n m) (b : Matrix m p) : Matrix n p :=
-- Functions can be called directly or with .transpose when the type is clear.
Matrix.transpose (vmap a.matvec b.transpose)We now have the full machinery of deep learning. A neural network is just stacking layers and applying a simple loss function.
def forward (layer: Matrix hidden hidden) {batch : Nat}
(input: Matrix batch hidden) :
Matrix batch hidden :=
(vmap (vmap relu)) (input.matmul layer)abbrev Layer {Shape : Type v} (State : Shape → Type u) :=
{shape : Shape} → State shape → State shapedef neural_network {Shape : Type v} {State : Shape → Type u}
(layers : List (Layer State)) : Layer State :=
fun input => layers.foldl (fun state layer => layer state) inputdef loss (point_loss : Fin batch → Vector hidden → Rat)
(matrix : Matrix batch hidden) : Rat :=
Vector.sum (vmap2 point_loss id matrix)Properties of Neural Networks
Now let us return to our goal of proving network equivariances. Our strategy will be to first show that in general equivariances compose, and then show that they propagate through a neural network.
-- Equivariances compose
theorem Equivariant.comp
-- Boilerplate
{Shape : Type u}
{A : Shape → Type v} {B : Shape → Type w} {C : Shape → Type z}
{first : {shape : Shape} → A shape → B shape}
{next : {shape : Shape} → B shape → C shape}
{source target : Shape}
{T : A source → A target} {S : B source → B target}
{U : C source → C target}
-- If f(T x) = S f(x)
(hfirst : Equivariant (Input := A) (Output := B) first T S)
-- and g(S x) = U g(x)
(hnext : Equivariant (Input := B) (Output := C) next S U) :
-- then g(f(T x )) = U (g (f (x)))
Equivariant (Input := A) (Output := C)
(fun input => next (first input)) T U := Shape:Type uA:Shape → Type vB:Shape → Type wC:Shape → Type zfirst:{shape : Shape} → A shape → B shapenext:{shape : Shape} → B shape → C shapesource:Shapetarget:ShapeT:A source → A targetS:B source → B targetU:C source → C targethfirst:Equivariant (fun {shape} => first) T Shnext:Equivariant (fun {shape} => next) S U⊢ Equivariant (fun {shape} input => next (first input)) T U
Shape:Type uA:Shape → Type vB:Shape → Type wC:Shape → Type zfirst:{shape : Shape} → A shape → B shapenext:{shape : Shape} → B shape → C shapesource:Shapetarget:ShapeT:A source → A targetS:B source → B targetU:C source → C targethfirst:Equivariant (fun {shape} => first) T Shnext:Equivariant (fun {shape} => next) S Uinput:A source⊢ (fun {shape} input => next (first input)) (T input) = U ((fun {shape} input => next (first input)) input)
All goals completed! 🐙-- Equivariances flow through tuples
theorem Equivariant.prod
-- Boilerplate
{Shape : Type u}
{Input₁ : Shape → Type u₁} {Input₂ : Shape → Type u₂}
{Output₁ : Shape → Type v₁} {Output₂ : Shape → Type v₂}
{f : {shape : Shape} → Input₁ shape → Output₁ shape}
{g : {shape : Shape} → Input₂ shape → Output₂ shape}
{source target : Shape}
{T₁ : Input₁ source → Input₁ target} {S₁ : Output₁ source → Output₁ target}
{T₂ : Input₂ source → Input₂ target} {S₂ : Output₂ source → Output₂ target}
-- If f(T1 x) = S1 f(x)
(hf : Equivariant (Input := Input₁) (Output := Output₁) f T₁ S₁)
-- and g(T2 x) = S2 g(x)
(hg : Equivariant (Input := Input₂) (Output := Output₂) g T₂ S₂) :
-- Then <f,g> <T1 x, T2 y> = <S1 f( x), S2 g( y)>
Equivariant (Input := fun shape => Input₁ shape × Input₂ shape)
(Output := fun shape => Output₁ shape × Output₂ shape)
(fun input => Prod.map f g input) (Prod.map T₁ T₂) (Prod.map S₁ S₂) := Shape:Type uInput₁:Shape → Type u₁Input₂:Shape → Type u₂Output₁:Shape → Type v₁Output₂:Shape → Type v₂f:{shape : Shape} → Input₁ shape → Output₁ shapeg:{shape : Shape} → Input₂ shape → Output₂ shapesource:Shapetarget:ShapeT₁:Input₁ source → Input₁ targetS₁:Output₁ source → Output₁ targetT₂:Input₂ source → Input₂ targetS₂:Output₂ source → Output₂ targethf:Equivariant (fun {shape} => f) T₁ S₁hg:Equivariant (fun {shape} => g) T₂ S₂⊢ Equivariant (fun {shape} input => Prod.map f g input) (Prod.map T₁ T₂) (Prod.map S₁ S₂)
Shape:Type uInput₁:Shape → Type u₁Input₂:Shape → Type u₂Output₁:Shape → Type v₁Output₂:Shape → Type v₂f:{shape : Shape} → Input₁ shape → Output₁ shapeg:{shape : Shape} → Input₂ shape → Output₂ shapesource:Shapetarget:ShapeT₁:Input₁ source → Input₁ targetS₁:Output₁ source → Output₁ targetT₂:Input₂ source → Input₂ targetS₂:Output₂ source → Output₂ targethf:Equivariant (fun {shape} => f) T₁ S₁hg:Equivariant (fun {shape} => g) T₂ S₂x:Input₁ source × Input₂ source⊢ (fun {shape} input => Prod.map f g input) (Prod.map T₁ T₂ x) =
Prod.map S₁ S₂ ((fun {shape} input => Prod.map f g input) x)
All goals completed! 🐙cons Shape:Type vState:Shape → Type usource:Shapetarget:Shapetransform:State source → State targetlayer:Layer Staterest:List (Layer State)ih:(∀ (layer : Layer State), layer ∈ rest → Equivariant (fun {shape} => layer) transform transform) →
Equivariant (fun {shape} => neural_network rest) transform transformequivariant:∀ (layer_1 : Layer State), layer_1 ∈ layer :: rest → Equivariant (fun {shape} => layer_1) transform transformcomposed:Equivariant (fun {shape} input => neural_network rest (layer input)) transform transform⊢ Equivariant (fun {shape} => neural_network (layer :: rest)) transform transform
exact composed All goals completed! 🐙We can use these properties to show that our neural network is selection equivariant, roughly that each individual result should be the same no matter how batches are built or ordered.
-- Select m arbitrary elements of a set of n elements.
def select (selection : Fin m → Fin n) (a : Fin n → α) : Fin m → α :=
fun i => a (selection i)-- Slice out a fixed-size group.
def slice
(start count : Nat)
-- Note that this takes a proof that the selection is in-bounds as an arg!
(h : start + count ≤ n)
(xs : Fin n → α) : Fin count → α :=
select (fun i => ⟨i.val + start, by α:Type uβ:Type vδ:Type wn:Natstart:Natcount:Nath:start + count ≤ nxs:Fin n → αi:Fin count⊢ ↑i + start < n omega All goals completed! 🐙⟩) xs-- Equivariance under every selection, including selection across lengths.
def SelectionEquivariant (op : {n : Nat} → (Fin n → α) → (Fin n → β)) : Prop :=
∀ {n m} (selection : Fin m → Fin n),
Equivariant (Input := fun n => Fin n → α) (Output := fun n => Fin n → β)
op (select selection) (select selection)-- Under vmap selection doesn't matter.
theorem vmap_selection_equivariant (fn : α → β) :
SelectionEquivariant (vmap fn) := by α:Type uβ:Type vfn:α → β⊢ SelectionEquivariant fun {n} => vmap fn
intro n m selection a α:Type uβ:Type vfn:α → βn:Natm:Natselection:Fin m → Fin na:Fin n → α⊢ (fun {shape} {n} => vmap fn) (select selection a) = select selection ((fun {shape} {n} => vmap fn) a)
rfl All goals completed! 🐙@[simp] theorem vmap2_apply (fn : α → β → δ) (a : Fin n → α)
(b : Fin n → β) (i : Fin n) :
vmap2 fn a b i = fn (a i) (b i) := rflFrom these individual results, we directly build up to our first main result. A simple neural network does not depend on the order or content of its batch.
theorem Matrix.matmul_row_equivariant
(b : Matrix m p) :
SelectionEquivariant (fun (a : Matrix _ m) => a.matmul b) :=
vmap_selection_equivariant
(fun (row : Vector m) => vmap row.dot_product b.transpose)-- Transpose to expose columns as the position axis.
theorem Matrix.matmul_column_equivariant (a : Matrix n m) :
SelectionEquivariant (fun (bt : Matrix _ m) =>
(a.matmul bt.transpose).transpose) :=
vmap_selection_equivariant a.matvec-- MLPs are selection equivariant on batch.
theorem forward_selection_equivariant (layer : Matrix hidden hidden) :
SelectionEquivariant (forward layer) := by hidden:Natlayer:Matrix hidden hidden⊢ SelectionEquivariant fun {n} => forward layer
intro n m selection input hidden:Natlayer:Matrix hidden hiddenn:Natm:Natselection:Fin m → Fin ninput:Fin n → Vector hidden⊢ (fun {shape} {n} => forward layer) (select selection input) =
select selection ((fun {shape} {n} => forward layer) input)
rfl All goals completed! 🐙theorem neural_network_selection_equivariant
(layers : List (Layer (fun n => Fin n → α)))
(equivariant : ∀ layer ∈ layers, SelectionEquivariant layer) :
SelectionEquivariant (neural_network layers) := by α:Type ulayers:List (Layer fun n => Fin n → α)equivariant:∀ (layer : Layer fun n => Fin n → α), layer ∈ layers → SelectionEquivariant fun {n} => layer⊢ SelectionEquivariant fun {n} => neural_network layers
intro n m selection α:Type ulayers:List (Layer fun n => Fin n → α)equivariant:∀ (layer : Layer fun n => Fin n → α), layer ∈ layers → SelectionEquivariant fun {n} => layern:Natm:Natselection:Fin m → Fin n⊢ Equivariant (fun {shape} {n} => neural_network layers) (select selection) (select selection)
exact neural_network_equivariant layers (select selection)
(fun layer member => equivariant layer member selection) All goals completed! 🐙System Optimization
While these properties so far seem basic, they are essential for designing large-scale LLMs. These properties provide the means for parallelizing and optimizing these systems. They also are properties that are commonly broken when new low-level optimizations are introduced. Let's look at a couple of these in more detail.
Batch Invariance
Batch invariance ensures that the final loss of the system is independent of the size of the batch used. This property can ensure replicability across systems. See Horace He's beautifully described blog about why batch invariance is useful and how it is often sacrificed under different optimizations.
Here we prove that selection equivariance implies a simple form of batch invariance. Basically, you get the same loss independent of the batch.
-- The same example has the same scalar loss in a batch or on its own.
theorem nn_batch_invariant
{nn : {batch : Nat} → (Fin batch → α) → (Fin batch → β)}
(equivariant : SelectionEquivariant nn) (point_loss : β → Rat)
(input : Fin batch → α) (b : Fin batch) :
point_loss (nn (select (fun _ : Fin 1 => b) input) 0) =
point_loss (nn input b) := by α:Type uβ:Type vbatch:Natnn:{batch : Nat} → (Fin batch → α) → Fin batch → βequivariant:SelectionEquivariant fun {n} => nnpoint_loss:β → Ratinput:Fin batch → αb:Fin batch⊢ point_loss (nn (select (fun x => b) input) 0) = point_loss (nn input b)
exact congrArg point_loss
(congrFun (equivariant (fun _ : Fin 1 => b) input) 0) All goals completed! 🐙Tensor Parallel
Tensor parallelism is a common optimization for distributed neural networks. It's a fancy way of saying that instead of doing a matrix multiplication on one host, you can instead split it into two or more parts, do those multiplications separately, and then merge them.
def Matrix.row_split (a : Matrix (k + k) m) :
Matrix k m × Matrix k m :=
-- Note here that i ∈ {0..k-1} but to index row need i ∈ {0..2 k-1}.
-- These functions handle that cast.
⟨select (fun i => i.castAdd k) a,
select (fun i => i.natAdd k) a⟩-- sum is splittable
theorem Vector.sum_split (values : Vector (m + n)) :
Vector.sum (select (fun i : Fin m => i.castAdd n) values) +
Vector.sum (select (fun i : Fin n => i.natAdd m) values) = values.sum := by m:Natn:Natvalues:Vector (m + n)⊢ sum (select (fun i => Fin.castAdd n i) values) + sum (select (fun i => Fin.natAdd m i) values) = values.sum
simp only [Vector.sum, fori, select, List.ofFn_add, List.sum_append,
Fin.castAdd, Fin.castLE] All goals completed! 🐙def Matrix.tensor_parallel (a : Matrix n (k + k)) (b : Matrix (k + k) p) :
Matrix n p :=
let (a₁, a₂) := a.transpose.row_split
let (b₁, b₂) := b.row_split
let c₁ := a₁.transpose.matmul b₁
let c₂ := a₂.transpose.matmul b₂
c₁ + c₂theorem Matrix.tensor_parallel_correct
-- For any splittable a, b
(a : Matrix n (k + k)) (b : Matrix (k + k) p) :
-- running tensor_parallel gives the same result as matmul
a.tensor_parallel b = a.matmul b := by n:Natk:Natp:Nata:Matrix n (k + k)b:Matrix (k + k) p⊢ a.tensor_parallel b = a.matmul b
-- Show each final i, j ends up the same.
funext i j n:Natk:Natp:Nata:Matrix n (k + k)b:Matrix (k + k) pi:Fin nj:Fin p⊢ a.tensor_parallel b i j = a.matmul b i j
exact Vector.sum_split (fun t => a i t * b t j) All goals completed! 🐙Data Parallel
Data parallelism says that, in training, we can split the data into different groups, run the full neural network and loss on different machines, and then combine. We need to ensure that we get the same result by running things separately as together.
theorem loss_row_split (point_loss : Fin (k + k) → Vector hidden → Rat)
(output : Matrix (k + k) hidden) :
loss (fun b row => point_loss (b.castAdd k) row) output.row_split.1 +
loss (fun b row => point_loss (b.natAdd k) row) output.row_split.2 =
loss point_loss output :=
Vector.sum_split (fun b => point_loss b (output b))-- Split data points in half and run on separate machines.
def data_parallel_loss
(layers : List (Layer (fun batch => Fin batch → Vector hidden)))
(point_loss : Fin (k + k) → Vector hidden → Rat)
(input : Matrix (k + k) hidden) : Rat :=
let (first, second) := input.row_split
loss (fun b row => point_loss (b.castAdd k) row)
(neural_network layers first) +
loss (fun b row => point_loss (b.natAdd k) row)
(neural_network layers second)theorem data_parallel_loss_correct
(layers : List (Layer (fun batch => Fin batch → Vector hidden)))
(equivariant : ∀ layer ∈ layers, SelectionEquivariant layer)
(point_loss : Fin (k + k) → Vector hidden → Rat)
(input : Matrix (k + k) hidden) :
data_parallel_loss layers point_loss input =
loss point_loss (neural_network layers input) := by hidden:Natk:Natlayers:List (Layer fun batch => Fin batch → Vector hidden)equivariant:∀ (layer : Layer fun batch => Fin batch → Vector hidden), layer ∈ layers → SelectionEquivariant fun {n} => layerpoint_loss:Fin (k + k) → Vector hidden → Ratinput:Matrix (k + k) hidden⊢ data_parallel_loss layers point_loss input = loss point_loss (neural_network layers input)
dsimp only [Matrix] at input hidden:Natk:Natlayers:List (Layer fun batch => Fin batch → Vector hidden)equivariant:∀ (layer : Layer fun batch => Fin batch → Vector hidden), layer ∈ layers → SelectionEquivariant fun {n} => layerpoint_loss:Fin (k + k) → Vector hidden → Ratinput:Fin (k + k) → Vector hidden⊢ data_parallel_loss layers point_loss input = loss point_loss (neural_network layers input)
have equiv := neural_network_selection_equivariant layers equivariant
(n := k + k) (m := k) hidden:Natk:Natlayers:List (Layer fun batch => Fin batch → Vector hidden)equivariant:∀ (layer : Layer fun batch => Fin batch → Vector hidden), layer ∈ layers → SelectionEquivariant fun {n} => layerpoint_loss:Fin (k + k) → Vector hidden → Ratinput:Fin (k + k) → Vector hiddenequiv:∀ (selection : Fin k → Fin (k + k)),
Equivariant (fun {shape} {n} => neural_network layers) (select selection) (select selection)⊢ data_parallel_loss layers point_loss input = loss point_loss (neural_network layers input)
unfold Equivariant at equiv hidden:Natk:Natlayers:List (Layer fun batch => Fin batch → Vector hidden)equivariant:∀ (layer : Layer fun batch => Fin batch → Vector hidden), layer ∈ layers → SelectionEquivariant fun {n} => layerpoint_loss:Fin (k + k) → Vector hidden → Ratinput:Fin (k + k) → Vector hiddenequiv:∀ (selection : Fin k → Fin (k + k)) (x : (fun n => Fin n → Vector hidden) (k + k)),
(fun {shape} {n} => neural_network layers) (select selection x) =
select selection ((fun {shape} {n} => neural_network layers) x)⊢ data_parallel_loss layers point_loss input = loss point_loss (neural_network layers input)
simpa only [data_parallel_loss, Matrix.row_split, equiv] using
loss_row_split point_loss (neural_network layers input) All goals completed! 🐙Transformers and attention
We next study a simple bidirectional Transformer with attention. The sequence becomes an additional dimension of our tensor. We first define attention.
-- A mixer takes <<q,k>,v> as an arg and returns the result.
abbrev Mixer (seq hidden : Nat) :=
(Matrix seq hidden × Matrix seq hidden) × Matrix seq hidden →
Matrix seq hidden-- The famed softmax(Q K^T) V formula.
def base_attention (s : Matrix seq seq → Matrix seq seq) : Mixer seq hidden :=
fun ((q,k), v) => Matrix.matmul (s (q.matmul k.transpose)) vdef attention_layer : Mixer seq hidden :=
fun input => base_attention (vmap softmax_like) inputAttention is included in the main network through a parameterized Transformer block.
structure TransformerBlock (hidden : Nat) where
weight : Matrix hidden hidden
wq : Matrix hidden hidden
wk : Matrix hidden hidden
wv : Matrix hidden hiddenabbrev Params (hidden : Nat) := List (TransformerBlock hidden)abbrev project_qkv (input : Matrix seq hidden)
(wq wk wv : Matrix hidden hidden) :
(Matrix seq hidden × Matrix seq hidden) × Matrix seq hidden :=
((input.matmul wq, input.matmul wk), input.matmul wv)def transformer_block (mixer : Mixer seq hidden)
(block : TransformerBlock hidden) (input : Matrix seq hidden) :
Matrix seq hidden :=
let output := (vmap (vmap relu)) (Matrix.matmul input block.weight)
mixer (project_qkv output block.wq block.wk block.wv)One of the more surprising properties of the vanilla (bidirectional) Transformer is that it is a set-to-set model, i.e. it is permutation equivariant in sequence length. Let's define first what that means formally.
-- A permutation is a bijective map from {0..n-1} => {0..n-1}
structure PositionPermutation (n : Nat) where
index : Fin n → Fin n
valid : (fori index).Perm (fori fun i : Fin n => i)-- Apply a permutation.
def permute (π : PositionPermutation n) (a : Fin n → α) : Fin n → α :=
select π.index adef permute_both (π : PositionPermutation n) (a : Fin n → Fin n → α) :
Fin n → Fin n → α :=
permute π (vmap (permute π) a)def permute_qkv {hidden : Nat} (π : PositionPermutation seq) :=
Prod.map
(Prod.map (permute (α := Vector hidden) π)
(permute (α := Vector hidden) π))
(permute (α := Vector hidden) π)-- Main property.
def PermuteEquivariant (op : α → β)
-- If permutation is applied to our input,
(inputAction : PositionPermutation n → α → α := by exact permute)
-- The same permutation applied somehow to output yields the same result.
(outputAction : PositionPermutation n → β → β := by exact permute) : Prop :=
∀ π : PositionPermutation n,
Equivariant (Input := fun _ : Unit => α) (Output := fun _ : Unit => β)
op (source := ()) (target := ()) (inputAction π) (outputAction π)Most of the core operations we have defined have the necessary equivariance already. The main additional property we need is for our softmax, which follows directly from addition.
-- Selection equivariance (vmap) implies permutation equivariance.
theorem SelectionEquivariant.permute
{op : {n : Nat} → (Fin n → α) → (Fin n → β)}
(equivariant : SelectionEquivariant op) : PermuteEquivariant (@op n) := by α:Type uβ:Type vn:Natop:{n : Nat} → (Fin n → α) → Fin n → βequivariant:SelectionEquivariant fun {n} => op⊢ PermuteEquivariant op Transformer.permute Transformer.permute
intro π input α:Type uβ:Type vn:Natop:{n : Nat} → (Fin n → α) → Fin n → βequivariant:SelectionEquivariant fun {n} => opπ:PositionPermutation ninput:Fin n → α⊢ (fun {shape} => op) (Transformer.permute π input) = Transformer.permute π ((fun {shape} => op) input)
exact equivariant π.index input All goals completed! 🐙theorem vmap_permute_both (fn : (Fin n → α) → (Fin n → β))
(equivariant : PermuteEquivariant fn) :
PermuteEquivariant (vmap fn) permute_both permute_both := by α:Type uβ:Type vn:Natfn:(Fin n → α) → Fin n → βequivariant:PermuteEquivariant fn permute permute⊢ PermuteEquivariant (vmap fn) permute_both permute_both
intro π input α:Type uβ:Type vn:Natfn:(Fin n → α) → Fin n → βequivariant:PermuteEquivariant fn permute permuteπ:PositionPermutation ninput:Fin n → Fin n → α⊢ (fun {shape} => vmap fn) (permute_both π input) = permute_both π ((fun {shape} => vmap fn) input)
calc
_ = permute π (vmap fn (vmap (permute π) input)) :=
vmap_selection_equivariant fn π.index _
_ = _ := congrArg (permute π) (funext (fun s => equivariant π (input s)))-- Prove that sum is permutation invariant.
theorem sum_rat {xs ys : List Rat} (h : xs.Perm ys) : xs.sum = ys.sum :=
h.foldr_eq' (fun x _ y _ z => Rat.add_left_comm y x z) 0theorem permute_sum (π : PositionPermutation seq) (f : Fin seq → Rat) :
(Vector.sum (fun s => f (π.index s))) = (Vector.sum f) := by seq:Natπ:PositionPermutation seqf:Fin seq → Rat⊢ (Vector.sum fun s => f (π.index s)) = Vector.sum f
simpa [Vector.sum, permute, select,
List.map_ofFn, Function.comp_def] using
sum_rat (π.valid.map f) All goals completed! 🐙-- Prove that softmax is permutation equivariant.
theorem softmax_like_permute_equivariant :
PermuteEquivariant (n := n) softmax_like := by n:Nat⊢ PermuteEquivariant softmax_like permute permute
intro π z n:Natπ:PositionPermutation nz:Vector n⊢ (fun {shape} => softmax_like) (permute π z) = permute π ((fun {shape} => softmax_like) z)
funext s n:Natπ:PositionPermutation nz:Vector ns:Fin n⊢ (fun {shape} => softmax_like) (permute π z) s = permute π ((fun {shape} => softmax_like) z) s
exact congrArg (fun total => (1 + relu (z (π.index s))) / total)
((permute_sum π) (fun t => 1 + relu (z t))) All goals completed! 🐙theorem Matrix.matmul_transpose_permute :
PermuteEquivariant
(fun input : Matrix n hidden × Matrix n hidden =>
input.1.matmul input.2.transpose)
(fun π => Prod.map (permute π) (permute π)) permute_both := by n:Nathidden:Nat⊢ PermuteEquivariant (fun input => input.fst.matmul input.snd.transpose) (fun π => Prod.map (permute π) (permute π))
permute_both
intro π input n:Nathidden:Natπ:PositionPermutation ninput:Matrix n hidden × Matrix n hidden⊢ (fun {shape} input => input.fst.matmul input.snd.transpose) ((fun π => Prod.map (permute π) (permute π)) π input) =
permute_both π ((fun {shape} input => input.fst.matmul input.snd.transpose) input)
rfl All goals completed! 🐙theorem Matrix.matmul_permute :
PermuteEquivariant
(fun input : Matrix n n × Matrix n hidden => input.1.matmul input.2)
(fun π input => (permute_both π input.1, permute π input.2)) := by n:Nathidden:Nat⊢ PermuteEquivariant (fun input => input.fst.matmul input.snd)
(fun π input => (permute_both π input.fst, permute π input.snd)) permute
intro π input n:Nathidden:Natπ:PositionPermutation ninput:Matrix n n × Matrix n hidden⊢ (fun {shape} input => input.fst.matmul input.snd)
((fun π input => (permute_both π input.fst, permute π input.snd)) π input) =
permute π ((fun {shape} input => input.fst.matmul input.snd) input)
funext s j n:Nathidden:Natπ:PositionPermutation ninput:Matrix n n × Matrix n hiddens:Fin nj:Fin hidden⊢ (fun {shape} input => input.fst.matmul input.snd)
((fun π input => (permute_both π input.fst, permute π input.snd)) π input) s j =
permute π ((fun {shape} input => input.fst.matmul input.snd) input) s j
exact permute_sum π (fun t => input.1 (π.index s) t * input.2 t j) All goals completed! 🐙Now we can show that the vanilla Transformer is permutation equivariant.
theorem attention_layer_permute :
PermuteEquivariant (attention_layer (seq := seq) (hidden := hidden))
permute_qkv := by seq:Nathidden:Nat⊢ PermuteEquivariant attention_layer permute_qkv permute
intro π seq:Nathidden:Natπ:PositionPermutation seq⊢ Equivariant (fun {shape} => attention_layer) (permute_qkv π) (permute π)
have logits := Matrix.matmul_transpose_permute (n := seq) (hidden := hidden) π seq:Nathidden:Natπ:PositionPermutation seqlogits:Equivariant (fun {shape} input => input.fst.matmul input.snd.transpose) ((fun π => Prod.map (permute π) (permute π)) π)
(permute_both π)⊢ Equivariant (fun {shape} => attention_layer) (permute_qkv π) (permute π)
have normalized := logits.comp
(vmap_permute_both softmax_like softmax_like_permute_equivariant π) seq:Nathidden:Natπ:PositionPermutation seqlogits:Equivariant (fun {shape} input => input.fst.matmul input.snd.transpose) ((fun π => Prod.map (permute π) (permute π)) π)
(permute_both π)normalized:Equivariant (fun {shape} input => vmap softmax_like (input.fst.matmul input.snd.transpose))
((fun π => Prod.map (permute π) (permute π)) π) (permute_both π)⊢ Equivariant (fun {shape} => attention_layer) (permute_qkv π) (permute π)
exact (normalized.prod
(SelectionEquivariant.permute (vmap_selection_equivariant id) π)).comp
(Matrix.matmul_permute π) All goals completed! 🐙
theorem projected_attention_permute (wq wk wv : Matrix hidden hidden) :
PermuteEquivariant (n := seq) (fun input : Matrix seq hidden =>
attention_layer (project_qkv input wq wk wv)) := by hidden:Natseq:Natwq:Matrix hidden hiddenwk:Matrix hidden hiddenwv:Matrix hidden hidden⊢ PermuteEquivariant (fun input => attention_layer (project_qkv input wq wk wv)) permute permute
have projected : PermuteEquivariant
(fun input : Matrix seq hidden => project_qkv input wq wk wv)
permute permute_qkv := by
intro π input hidden:Natseq:Natwq:Matrix hidden hiddenwk:Matrix hidden hiddenwv:Matrix hidden hiddenπ:PositionPermutation seqinput:Matrix seq hidden⊢ (fun {shape} input => project_qkv input wq wk wv) (permute π input) =
permute_qkv π ((fun {shape} input => project_qkv input wq wk wv) input) hidden:Natseq:Natwq:Matrix hidden hiddenwk:Matrix hidden hiddenwv:Matrix hidden hiddenprojected:PermuteEquivariant (fun input => project_qkv input wq wk wv) permute permute_qkv⊢ PermuteEquivariant (fun input => attention_layer (project_qkv input wq wk wv)) permute permute
rfl hidden:Natseq:Natwq:Matrix hidden hiddenwk:Matrix hidden hiddenwv:Matrix hidden hiddenprojected:PermuteEquivariant (fun input => project_qkv input wq wk wv) permute permute_qkv⊢ PermuteEquivariant (fun input => attention_layer (project_qkv input wq wk wv)) permute permute hidden:Natseq:Natwq:Matrix hidden hiddenwk:Matrix hidden hiddenwv:Matrix hidden hiddenprojected:PermuteEquivariant (fun input => project_qkv input wq wk wv) permute permute_qkv⊢ PermuteEquivariant (fun input => attention_layer (project_qkv input wq wk wv)) permute permute
intro π hidden:Natseq:Natwq:Matrix hidden hiddenwk:Matrix hidden hiddenwv:Matrix hidden hiddenprojected:PermuteEquivariant (fun input => project_qkv input wq wk wv) permute permute_qkvπ:PositionPermutation seq⊢ Equivariant (fun {shape} input => attention_layer (project_qkv input wq wk wv)) (permute π) (permute π)
exact Equivariant.comp (projected π) (attention_layer_permute π) All goals completed! 🐙theorem transformer_block_permute (block : TransformerBlock hidden) :
PermuteEquivariant (n := seq) (transformer_block attention_layer block) := by hidden:Natseq:Natblock:TransformerBlock hidden⊢ PermuteEquivariant (transformer_block attention_layer block) permute permute
intro π hidden:Natseq:Natblock:TransformerBlock hiddenπ:PositionPermutation seq⊢ Equivariant (fun {shape} => transformer_block attention_layer block) (permute π) (permute π)
unfold transformer_block hidden:Natseq:Natblock:TransformerBlock hiddenπ:PositionPermutation seq⊢ Equivariant
(fun {shape} input =>
have output := vmap (vmap relu) (input.matmul block.weight);
attention_layer (project_qkv output block.wq block.wk block.wv))
(permute π) (permute π)
exact Equivariant.comp
(SelectionEquivariant.permute (forward_selection_equivariant block.weight) π)
(projected_attention_permute block.wq block.wk block.wv π) All goals completed! 🐙theorem transformer_permute (blocks : Params hidden) :
PermuteEquivariant (n := seq)
(neural_network (State := fun _ : Unit => _) (shape := ())
(blocks.map (fun block => transformer_block attention_layer block))) := by hidden:Natseq:Natblocks:Params hidden⊢ PermuteEquivariant (neural_network (List.map (fun block {shape} => transformer_block attention_layer block) blocks))
permute permute
intro π hidden:Natseq:Natblocks:Params hiddenπ:PositionPermutation seq⊢ Equivariant
(fun {shape} => neural_network (List.map (fun block {shape} => transformer_block attention_layer block) blocks))
(permute π) (permute π)
apply neural_network_equivariant hidden:Natseq:Natblocks:Params hiddenπ:PositionPermutation seq⊢ ∀ (layer : Layer fun x => (fun x => Matrix seq hidden) ()),
layer ∈ List.map (fun block {shape} => transformer_block attention_layer block) blocks →
Equivariant (fun {shape} => layer) (permute π) (permute π)
intro layer member hidden:Natseq:Natblocks:Params hiddenπ:PositionPermutation seqlayer:Layer fun x => (fun x => Matrix seq hidden) ()member:layer ∈ List.map (fun block {shape} => transformer_block attention_layer block) blocks⊢ Equivariant (fun {shape} => layer) (permute π) (permute π)
obtain ⟨block, _, rfl⟩ := List.mem_map.mp member hidden:Natseq:Natblocks:Params hiddenπ:PositionPermutation seqblock:TransformerBlock hiddenleft✝:block ∈ blocksmember:(fun {shape} => transformer_block attention_layer block) ∈
List.map (fun block {shape} => transformer_block attention_layer block) blocks⊢ Equivariant (fun {shape} {shape} => transformer_block attention_layer block) (permute π) (permute π)
exact transformer_block_permute block π All goals completed! 🐙Of course, in practice, we add additional information that breaks this property. The simplest way is through the use of positional features. We can show that even simple positional features break equivariance with a direct counterexample.
def position_features (i : Rat) : Vector 3 :=
fun d => if d.val = 0 then i else if d.val = 1 then i * i else 1def Matrix.add_positions (input : Matrix seq 3) : Matrix seq 3 :=
fun s => input s + position_features (s.val : Rat)
theorem positional_transformer_breaks_permutation_equivariance :
let identity : Matrix 3 3 := fun i j => if i = j then 1 else 0
let block : TransformerBlock 3 := ⟨identity, identity, identity, identity⟩
let layers : List (Layer (fun seq => Matrix seq 3)) :=
[@Matrix.add_positions,
fun input => transformer_block attention_layer block input]
¬ PermuteEquivariant (n := 2) (neural_network (shape := 2) layers) := by ⊢ let identity := fun i j => if i = j then 1 else 0;
let block := { weight := identity, wq := identity, wk := identity, wv := identity };
let layers := [@Matrix.add_positions, fun {shape} input => transformer_block attention_layer block input];
¬PermuteEquivariant (neural_network layers) permute permute
dsimp only ⊢ ¬PermuteEquivariant
(neural_network
[@Matrix.add_positions, fun {shape} input =>
transformer_block attention_layer
{ weight := fun i j => if i = j then 1 else 0, wq := fun i j => if i = j then 1 else 0,
wk := fun i j => if i = j then 1 else 0, wv := fun i j => if i = j then 1 else 0 }
input])
permute permute
intro equivariant equivariant:PermuteEquivariant
(neural_network
[@Matrix.add_positions, fun {shape} input =>
transformer_block attention_layer
{ weight := fun i j => if i = j then 1 else 0, wq := fun i j => if i = j then 1 else 0,
wk := fun i j => if i = j then 1 else 0, wv := fun i j => if i = j then 1 else 0 }
input])
permute permute⊢ False
let input : Matrix 2 3 := fun s d => if d.val = 0 then (s.val : Rat) else 0 equivariant:PermuteEquivariant
(neural_network
[@Matrix.add_positions, fun {shape} input =>
transformer_block attention_layer
{ weight := fun i j => if i = j then 1 else 0, wq := fun i j => if i = j then 1 else 0,
wk := fun i j => if i = j then 1 else 0, wv := fun i j => if i = j then 1 else 0 }
input])
permute permuteinput:Matrix 2 3 := fun s d => if ↑d = 0 then ↑↑s else 0⊢ False
let swap : PositionPermutation 2 :=
⟨fun s => ⟨1 - s.val, by equivariant:PermuteEquivariant
(neural_network
[@Matrix.add_positions, fun {shape} input =>
transformer_block attention_layer
{ weight := fun i j => if i = j then 1 else 0, wq := fun i j => if i = j then 1 else 0,
wk := fun i j => if i = j then 1 else 0, wv := fun i j => if i = j then 1 else 0 }
input])
permute permuteinput:Matrix 2 3 := fun s d => if ↑d = 0 then ↑↑s else 0s:Fin 2⊢ 1 - ↑s < 2 equivariant:PermuteEquivariant
(neural_network
[@Matrix.add_positions, fun {shape} input =>
transformer_block attention_layer
{ weight := fun i j => if i = j then 1 else 0, wq := fun i j => if i = j then 1 else 0,
wk := fun i j => if i = j then 1 else 0, wv := fun i j => if i = j then 1 else 0 }
input])
permute permuteinput:Matrix 2 3 := fun s d => if ↑d = 0 then ↑↑s else 0swap:PositionPermutation 2 := { index := fun s => ⟨1 - ↑s, ⋯⟩, valid := ⋯ }⊢ False omega All goals completed! 🐙 equivariant:PermuteEquivariant
(neural_network
[@Matrix.add_positions, fun {shape} input =>
transformer_block attention_layer
{ weight := fun i j => if i = j then 1 else 0, wq := fun i j => if i = j then 1 else 0,
wk := fun i j => if i = j then 1 else 0, wv := fun i j => if i = j then 1 else 0 }
input])
permute permuteinput:Matrix 2 3 := fun s d => if ↑d = 0 then ↑↑s else 0swap:PositionPermutation 2 := { index := fun s => ⟨1 - ↑s, ⋯⟩, valid := ⋯ }⊢ False⟩, by equivariant:PermuteEquivariant
(neural_network
[@Matrix.add_positions, fun {shape} input =>
transformer_block attention_layer
{ weight := fun i j => if i = j then 1 else 0, wq := fun i j => if i = j then 1 else 0,
wk := fun i j => if i = j then 1 else 0, wv := fun i j => if i = j then 1 else 0 }
input])
permute permuteinput:Matrix 2 3 := fun s d => if ↑d = 0 then ↑↑s else 0⊢ (fori fun s => ⟨1 - ↑s, ⋯⟩).Perm (fori fun i => i) equivariant:PermuteEquivariant
(neural_network
[@Matrix.add_positions, fun {shape} input =>
transformer_block attention_layer
{ weight := fun i j => if i = j then 1 else 0, wq := fun i j => if i = j then 1 else 0,
wk := fun i j => if i = j then 1 else 0, wv := fun i j => if i = j then 1 else 0 }
input])
permute permuteinput:Matrix 2 3 := fun s d => if ↑d = 0 then ↑↑s else 0swap:PositionPermutation 2 := { index := fun s => ⟨1 - ↑s, ⋯⟩, valid := ⋯ }⊢ False decide All goals completed! 🐙 equivariant:PermuteEquivariant
(neural_network
[@Matrix.add_positions, fun {shape} input =>
transformer_block attention_layer
{ weight := fun i j => if i = j then 1 else 0, wq := fun i j => if i = j then 1 else 0,
wk := fun i j => if i = j then 1 else 0, wv := fun i j => if i = j then 1 else 0 }
input])
permute permuteinput:Matrix 2 3 := fun s d => if ↑d = 0 then ↑↑s else 0swap:PositionPermutation 2 := { index := fun s => ⟨1 - ↑s, ⋯⟩, valid := ⋯ }⊢ False⟩ equivariant:PermuteEquivariant
(neural_network
[@Matrix.add_positions, fun {shape} input =>
transformer_block attention_layer
{ weight := fun i j => if i = j then 1 else 0, wq := fun i j => if i = j then 1 else 0,
wk := fun i j => if i = j then 1 else 0, wv := fun i j => if i = j then 1 else 0 }
input])
permute permuteinput:Matrix 2 3 := fun s d => if ↑d = 0 then ↑↑s else 0swap:PositionPermutation 2 := { index := fun s => ⟨1 - ↑s, ⋯⟩, valid := ⋯ }⊢ False
have same := congrArg (fun output => output 0 0) (equivariant swap input) equivariant:PermuteEquivariant
(neural_network
[@Matrix.add_positions, fun {shape} input =>
transformer_block attention_layer
{ weight := fun i j => if i = j then 1 else 0, wq := fun i j => if i = j then 1 else 0,
wk := fun i j => if i = j then 1 else 0, wv := fun i j => if i = j then 1 else 0 }
input])
permute permuteinput:Matrix 2 3 := fun s d => if ↑d = 0 then ↑↑s else 0swap:PositionPermutation 2 := { index := fun s => ⟨1 - ↑s, ⋯⟩, valid := ⋯ }same:(fun {shape} =>
neural_network
[@Matrix.add_positions, fun {shape} input =>
transformer_block attention_layer
{ weight := fun i j => if i = j then 1 else 0, wq := fun i j => if i = j then 1 else 0,
wk := fun i j => if i = j then 1 else 0, wv := fun i j => if i = j then 1 else 0 }
input])
(permute swap input) 0 0 =
permute swap
((fun {shape} =>
neural_network
[@Matrix.add_positions, fun {shape} input =>
transformer_block attention_layer
{ weight := fun i j => if i = j then 1 else 0, wq := fun i j => if i = j then 1 else 0,
wk := fun i j => if i = j then 1 else 0, wv := fun i j => if i = j then 1 else 0 }
input])
input)
0 0⊢ False
revert same equivariant:PermuteEquivariant
(neural_network
[@Matrix.add_positions, fun {shape} input =>
transformer_block attention_layer
{ weight := fun i j => if i = j then 1 else 0, wq := fun i j => if i = j then 1 else 0,
wk := fun i j => if i = j then 1 else 0, wv := fun i j => if i = j then 1 else 0 }
input])
permute permuteinput:Matrix 2 3 := fun s d => if ↑d = 0 then ↑↑s else 0swap:PositionPermutation 2 := { index := fun s => ⟨1 - ↑s, ⋯⟩, valid := ⋯ }⊢ (fun {shape} =>
neural_network
[@Matrix.add_positions, fun {shape} input =>
transformer_block attention_layer
{ weight := fun i j => if i = j then 1 else 0, wq := fun i j => if i = j then 1 else 0,
wk := fun i j => if i = j then 1 else 0, wv := fun i j => if i = j then 1 else 0 }
input])
(permute swap input) 0 0 =
permute swap
((fun {shape} =>
neural_network
[@Matrix.add_positions, fun {shape} input =>
transformer_block attention_layer
{ weight := fun i j => if i = j then 1 else 0, wq := fun i j => if i = j then 1 else 0,
wk := fun i j => if i = j then 1 else 0, wv := fun i j => if i = j then 1 else 0 }
input])
input)
0 0 →
False
decide +kernel All goals completed! 🐙Sparse Attention
An alternative to full attention over the sequence is a sparse attention over a local region. Sliding window attention only looks at the surrounding window. We implement this with a windowed mask.
abbrev InWindow (radius : Nat) (s t : Fin seq) : Prop :=
s.val ≤ t.val + radius ∧ t.val ≤ s.val + radiusdef window_mask (radius : Nat) : Matrix seq seq :=
fun s t => if InWindow radius s t then 1 else 0def matrix_mask (radius: Nat) (a: Matrix m m) : Matrix m m :=
let mask: Matrix m m := window_mask radius
a * maskdef swa (radius : Nat) : Mixer seq hidden :=
base_attention (matrix_mask radius)The sliding window has the property that each position is only impacted by the region around it. We formalize this idea below.
-- Value is not impacted outside a region of a given radius.
def RegionInvariant (radius : Nat)
(op : (Fin seq → α) → Fin seq → β) : Prop :=
∀ (input other : Fin seq → α) (s : Fin seq),
(∀ t, InWindow radius s t → input t = other t ) →
op input s = op other s
-- Selection-equivariant operations (such as MLP layers) have radius 0.
theorem SelectionEquivariant.region_invariant
{op : {n : Nat} → (Fin n → α) → (Fin n → β)}
(equivariant : SelectionEquivariant op) :
RegionInvariant (seq := seq) 0 op := by α:Type uβ:Type vseq:Natop:{n : Nat} → (Fin n → α) → Fin n → βequivariant:SelectionEquivariant fun {n} => op⊢ RegionInvariant 0 op
intro a other i agree α:Type uβ:Type vseq:Natop:{n : Nat} → (Fin n → α) → Fin n → βequivariant:SelectionEquivariant fun {n} => opa:Fin seq → αother:Fin seq → αi:Fin seqagree:∀ (t : Fin seq), InWindow 0 i t → a t = other t⊢ op a i = op other i
have same := agree i (by α:Type uβ:Type vseq:Natop:{n : Nat} → (Fin n → α) → Fin n → βequivariant:SelectionEquivariant fun {n} => opa:Fin seq → αother:Fin seq → αi:Fin seqagree:∀ (t : Fin seq), InWindow 0 i t → a t = other t⊢ InWindow 0 i i α:Type uβ:Type vseq:Natop:{n : Nat} → (Fin n → α) → Fin n → βequivariant:SelectionEquivariant fun {n} => opa:Fin seq → αother:Fin seq → αi:Fin seqagree:∀ (t : Fin seq), InWindow 0 i t → a t = other tsame:a i = other i⊢ op a i = op other i constructor left α:Type uβ:Type vseq:Natop:{n : Nat} → (Fin n → α) → Fin n → βequivariant:SelectionEquivariant fun {n} => opa:Fin seq → αother:Fin seq → αi:Fin seqagree:∀ (t : Fin seq), InWindow 0 i t → a t = other t⊢ ↑i ≤ ↑i + 0right α:Type uβ:Type vseq:Natop:{n : Nat} → (Fin n → α) → Fin n → βequivariant:SelectionEquivariant fun {n} => opa:Fin seq → αother:Fin seq → αi:Fin seqagree:∀ (t : Fin seq), InWindow 0 i t → a t = other t⊢ ↑i ≤ ↑i + 0 α:Type uβ:Type vseq:Natop:{n : Nat} → (Fin n → α) → Fin n → βequivariant:SelectionEquivariant fun {n} => opa:Fin seq → αother:Fin seq → αi:Fin seqagree:∀ (t : Fin seq), InWindow 0 i t → a t = other tsame:a i = other i⊢ op a i = op other i <;> left α:Type uβ:Type vseq:Natop:{n : Nat} → (Fin n → α) → Fin n → βequivariant:SelectionEquivariant fun {n} => opa:Fin seq → αother:Fin seq → αi:Fin seqagree:∀ (t : Fin seq), InWindow 0 i t → a t = other t⊢ ↑i ≤ ↑i + 0right α:Type uβ:Type vseq:Natop:{n : Nat} → (Fin n → α) → Fin n → βequivariant:SelectionEquivariant fun {n} => opa:Fin seq → αother:Fin seq → αi:Fin seqagree:∀ (t : Fin seq), InWindow 0 i t → a t = other t⊢ ↑i ≤ ↑i + 0 α:Type uβ:Type vseq:Natop:{n : Nat} → (Fin n → α) → Fin n → βequivariant:SelectionEquivariant fun {n} => opa:Fin seq → αother:Fin seq → αi:Fin seqagree:∀ (t : Fin seq), InWindow 0 i t → a t = other tsame:a i = other i⊢ op a i = op other i omega All goals completed! 🐙 α:Type uβ:Type vseq:Natop:{n : Nat} → (Fin n → α) → Fin n → βequivariant:SelectionEquivariant fun {n} => opa:Fin seq → αother:Fin seq → αi:Fin seqagree:∀ (t : Fin seq), InWindow 0 i t → a t = other tsame:a i = other i⊢ op a i = op other i) α:Type uβ:Type vseq:Natop:{n : Nat} → (Fin n → α) → Fin n → βequivariant:SelectionEquivariant fun {n} => opa:Fin seq → αother:Fin seq → αi:Fin seqagree:∀ (t : Fin seq), InWindow 0 i t → a t = other tsame:a i = other i⊢ op a i = op other i
calc
op a i = op (fun _ : Fin 1 => a i) 0 :=
(congrFun (equivariant (fun _ : Fin 1 => i) a) 0).symm
_ = op (fun _ : Fin 1 => other i) 0 := by α:Type uβ:Type vseq:Natop:{n : Nat} → (Fin n → α) → Fin n → βequivariant:SelectionEquivariant fun {n} => opa:Fin seq → αother:Fin seq → αi:Fin seqagree:∀ (t : Fin seq), InWindow 0 i t → a t = other tsame:a i = other i⊢ op (fun x => a i) 0 = op (fun x => other i) 0 rw [same α:Type uβ:Type vseq:Natop:{n : Nat} → (Fin n → α) → Fin n → βequivariant:SelectionEquivariant fun {n} => opa:Fin seq → αother:Fin seq → αi:Fin seqagree:∀ (t : Fin seq), InWindow 0 i t → a t = other tsame:a i = other i⊢ op (fun x => other i) 0 = op (fun x => other i) 0 All goals completed! 🐙] All goals completed! 🐙
_ = op other i := congrFun (equivariant (fun _ : Fin 1 => i) other) 0-- vmap has radius 0
theorem vmap_region_invariant (fn : α → β) :
RegionInvariant (seq := seq) 0 (vmap fn) :=
SelectionEquivariant.region_invariant (vmap_selection_equivariant fn)The interesting new aspect of region invariance will be how it composes. Region invariance is additive under composition.
-- Composing sliding windows increases the radius.
theorem RegionInvariant.comp
{first : (Fin seq → α) → Fin seq → β}
{second : (Fin seq → β) → Fin seq → δ}
(hfirst : RegionInvariant r first) (hsecond : RegionInvariant t second) :
RegionInvariant (r + t) (fun input => second (first input)) := by α:Type uβ:Type vδ:Type wseq:Natr:Natt:Natfirst:(Fin seq → α) → Fin seq → βsecond:(Fin seq → β) → Fin seq → δhfirst:RegionInvariant r firsthsecond:RegionInvariant t second⊢ RegionInvariant (r + t) fun input => second (first input)
intro input other s agree α:Type uβ:Type vδ:Type wseq:Natr:Natt:Natfirst:(Fin seq → α) → Fin seq → βsecond:(Fin seq → β) → Fin seq → δhfirst:RegionInvariant r firsthsecond:RegionInvariant t secondinput:Fin seq → αother:Fin seq → αs:Fin seqagree:∀ (t_1 : Fin seq), InWindow (r + t) s t_1 → input t_1 = other t_1⊢ (fun input => second (first input)) input s = (fun input => second (first input)) other s
apply hsecond α:Type uβ:Type vδ:Type wseq:Natr:Natt:Natfirst:(Fin seq → α) → Fin seq → βsecond:(Fin seq → β) → Fin seq → δhfirst:RegionInvariant r firsthsecond:RegionInvariant t secondinput:Fin seq → αother:Fin seq → αs:Fin seqagree:∀ (t_1 : Fin seq), InWindow (r + t) s t_1 → input t_1 = other t_1⊢ ∀ (t_1 : Fin seq), InWindow t s t_1 → first input t_1 = first other t_1
intro u hu α:Type uβ:Type vδ:Type wseq:Natr:Natt:Natfirst:(Fin seq → α) → Fin seq → βsecond:(Fin seq → β) → Fin seq → δhfirst:RegionInvariant r firsthsecond:RegionInvariant t secondinput:Fin seq → αother:Fin seq → αs:Fin seqagree:∀ (t_1 : Fin seq), InWindow (r + t) s t_1 → input t_1 = other t_1u:Fin seqhu:InWindow t s u⊢ first input u = first other u
apply hfirst α:Type uβ:Type vδ:Type wseq:Natr:Natt:Natfirst:(Fin seq → α) → Fin seq → βsecond:(Fin seq → β) → Fin seq → δhfirst:RegionInvariant r firsthsecond:RegionInvariant t secondinput:Fin seq → αother:Fin seq → αs:Fin seqagree:∀ (t_1 : Fin seq), InWindow (r + t) s t_1 → input t_1 = other t_1u:Fin seqhu:InWindow t s u⊢ ∀ (t : Fin seq), InWindow r u t → input t = other t
intro v hv α:Type uβ:Type vδ:Type wseq:Natr:Natt:Natfirst:(Fin seq → α) → Fin seq → βsecond:(Fin seq → β) → Fin seq → δhfirst:RegionInvariant r firsthsecond:RegionInvariant t secondinput:Fin seq → αother:Fin seq → αs:Fin seqagree:∀ (t_1 : Fin seq), InWindow (r + t) s t_1 → input t_1 = other t_1u:Fin seqhu:InWindow t s uv:Fin seqhv:InWindow r u v⊢ input v = other v
apply agree α:Type uβ:Type vδ:Type wseq:Natr:Natt:Natfirst:(Fin seq → α) → Fin seq → βsecond:(Fin seq → β) → Fin seq → δhfirst:RegionInvariant r firsthsecond:RegionInvariant t secondinput:Fin seq → αother:Fin seq → αs:Fin seqagree:∀ (t_1 : Fin seq), InWindow (r + t) s t_1 → input t_1 = other t_1u:Fin seqhu:InWindow t s uv:Fin seqhv:InWindow r u v⊢ InWindow (r + t) s v
dsimp [InWindow] at hu hv ⊢ α:Type uβ:Type vδ:Type wseq:Natr:Natt:Natfirst:(Fin seq → α) → Fin seq → βsecond:(Fin seq → β) → Fin seq → δhfirst:RegionInvariant r firsthsecond:RegionInvariant t secondinput:Fin seq → αother:Fin seq → αs:Fin seqagree:∀ (t_1 : Fin seq), InWindow (r + t) s t_1 → input t_1 = other t_1u:Fin seqhu:↑s ≤ ↑u + t ∧ ↑u ≤ ↑s + tv:Fin seqhv:↑u ≤ ↑v + r ∧ ↑v ≤ ↑u + r⊢ ↑s ≤ ↑v + (r + t) ∧ ↑v ≤ ↑s + (r + t)
constructor left α:Type uβ:Type vδ:Type wseq:Natr:Natt:Natfirst:(Fin seq → α) → Fin seq → βsecond:(Fin seq → β) → Fin seq → δhfirst:RegionInvariant r firsthsecond:RegionInvariant t secondinput:Fin seq → αother:Fin seq → αs:Fin seqagree:∀ (t_1 : Fin seq), InWindow (r + t) s t_1 → input t_1 = other t_1u:Fin seqhu:↑s ≤ ↑u + t ∧ ↑u ≤ ↑s + tv:Fin seqhv:↑u ≤ ↑v + r ∧ ↑v ≤ ↑u + r⊢ ↑s ≤ ↑v + (r + t)right α:Type uβ:Type vδ:Type wseq:Natr:Natt:Natfirst:(Fin seq → α) → Fin seq → βsecond:(Fin seq → β) → Fin seq → δhfirst:RegionInvariant r firsthsecond:RegionInvariant t secondinput:Fin seq → αother:Fin seq → αs:Fin seqagree:∀ (t_1 : Fin seq), InWindow (r + t) s t_1 → input t_1 = other t_1u:Fin seqhu:↑s ≤ ↑u + t ∧ ↑u ≤ ↑s + tv:Fin seqhv:↑u ≤ ↑v + r ∧ ↑v ≤ ↑u + r⊢ ↑v ≤ ↑s + (r + t) <;> left α:Type uβ:Type vδ:Type wseq:Natr:Natt:Natfirst:(Fin seq → α) → Fin seq → βsecond:(Fin seq → β) → Fin seq → δhfirst:RegionInvariant r firsthsecond:RegionInvariant t secondinput:Fin seq → αother:Fin seq → αs:Fin seqagree:∀ (t_1 : Fin seq), InWindow (r + t) s t_1 → input t_1 = other t_1u:Fin seqhu:↑s ≤ ↑u + t ∧ ↑u ≤ ↑s + tv:Fin seqhv:↑u ≤ ↑v + r ∧ ↑v ≤ ↑u + r⊢ ↑s ≤ ↑v + (r + t)right α:Type uβ:Type vδ:Type wseq:Natr:Natt:Natfirst:(Fin seq → α) → Fin seq → βsecond:(Fin seq → β) → Fin seq → δhfirst:RegionInvariant r firsthsecond:RegionInvariant t secondinput:Fin seq → αother:Fin seq → αs:Fin seqagree:∀ (t_1 : Fin seq), InWindow (r + t) s t_1 → input t_1 = other t_1u:Fin seqhu:↑s ≤ ↑u + t ∧ ↑u ≤ ↑s + tv:Fin seqhv:↑u ≤ ↑v + r ∧ ↑v ≤ ↑u + r⊢ ↑v ≤ ↑s + (r + t) omega All goals completed! 🐙
-- NNs with same layers have multiplicative radius.
theorem neural_network_region_invariant (radius : Nat)
(layers : List (Layer (fun seq => Fin seq → α)))
(invariant : ∀ layer ∈ layers, RegionInvariant radius (@layer seq)) :
RegionInvariant (seq := seq) (layers.length * radius)
(neural_network layers) := by α:Type useq:Natradius:Natlayers:List (Layer fun seq => Fin seq → α)invariant:∀ (layer : Layer fun seq => Fin seq → α), layer ∈ layers → RegionInvariant radius layer⊢ RegionInvariant (layers.length * radius) (neural_network layers)
induction layers with
| nil => nil α:Type useq:Natradius:Natinvariant:∀ (layer : Layer fun seq => Fin seq → α), layer ∈ [] → RegionInvariant radius layer⊢ RegionInvariant ([].length * radius) (neural_network [])
intro input other s agree nil α:Type useq:Natradius:Natinvariant:∀ (layer : Layer fun seq => Fin seq → α), layer ∈ [] → RegionInvariant radius layerinput:Fin seq → αother:Fin seq → αs:Fin seqagree:∀ (t : Fin seq), InWindow ([].length * radius) s t → input t = other t⊢ neural_network [] input s = neural_network [] other s
exact agree s (by α:Type useq:Natradius:Natinvariant:∀ (layer : Layer fun seq => Fin seq → α), layer ∈ [] → RegionInvariant radius layerinput:Fin seq → αother:Fin seq → αs:Fin seqagree:∀ (t : Fin seq), InWindow ([].length * radius) s t → input t = other t⊢ InWindow ([].length * radius) s s constructor left α:Type useq:Natradius:Natinvariant:∀ (layer : Layer fun seq => Fin seq → α), layer ∈ [] → RegionInvariant radius layerinput:Fin seq → αother:Fin seq → αs:Fin seqagree:∀ (t : Fin seq), InWindow ([].length * radius) s t → input t = other t⊢ ↑s ≤ ↑s + [].length * radiusright α:Type useq:Natradius:Natinvariant:∀ (layer : Layer fun seq => Fin seq → α), layer ∈ [] → RegionInvariant radius layerinput:Fin seq → αother:Fin seq → αs:Fin seqagree:∀ (t : Fin seq), InWindow ([].length * radius) s t → input t = other t⊢ ↑s ≤ ↑s + [].length * radius <;> left α:Type useq:Natradius:Natinvariant:∀ (layer : Layer fun seq => Fin seq → α), layer ∈ [] → RegionInvariant radius layerinput:Fin seq → αother:Fin seq → αs:Fin seqagree:∀ (t : Fin seq), InWindow ([].length * radius) s t → input t = other t⊢ ↑s ≤ ↑s + [].length * radiusright α:Type useq:Natradius:Natinvariant:∀ (layer : Layer fun seq => Fin seq → α), layer ∈ [] → RegionInvariant radius layerinput:Fin seq → αother:Fin seq → αs:Fin seqagree:∀ (t : Fin seq), InWindow ([].length * radius) s t → input t = other t⊢ ↑s ≤ ↑s + [].length * radius omega All goals completed! 🐙)
| cons layer rest ih => cons α:Type useq:Natradius:Natlayer:Layer fun seq => Fin seq → αrest:List (Layer fun seq => Fin seq → α)ih:(∀ (layer : Layer fun seq => Fin seq → α), layer ∈ rest → RegionInvariant radius layer) →
RegionInvariant (rest.length * radius) (neural_network rest)invariant:∀ (layer_1 : Layer fun seq => Fin seq → α), layer_1 ∈ layer :: rest → RegionInvariant radius layer_1⊢ RegionInvariant ((layer :: rest).length * radius) (neural_network (layer :: rest))
have composed := RegionInvariant.comp (invariant @layer (by α:Type useq:Natradius:Natlayer:Layer fun seq => Fin seq → αrest:List (Layer fun seq => Fin seq → α)ih:(∀ (layer : Layer fun seq => Fin seq → α), layer ∈ rest → RegionInvariant radius layer) →
RegionInvariant (rest.length * radius) (neural_network rest)invariant:∀ (layer_1 : Layer fun seq => Fin seq → α), layer_1 ∈ layer :: rest → RegionInvariant radius layer_1⊢ layer ∈ layer :: rest cons α:Type useq:Natradius:Natlayer:Layer fun seq => Fin seq → αrest:List (Layer fun seq => Fin seq → α)ih:(∀ (layer : Layer fun seq => Fin seq → α), layer ∈ rest → RegionInvariant radius layer) →
RegionInvariant (rest.length * radius) (neural_network rest)invariant:∀ (layer_1 : Layer fun seq => Fin seq → α), layer_1 ∈ layer :: rest → RegionInvariant radius layer_1composed:RegionInvariant (radius + rest.length * radius) fun input => neural_network rest (layer input)⊢ RegionInvariant ((layer :: rest).length * radius) (neural_network (layer :: rest)) simp All goals completed! 🐙 cons α:Type useq:Natradius:Natlayer:Layer fun seq => Fin seq → αrest:List (Layer fun seq => Fin seq → α)ih:(∀ (layer : Layer fun seq => Fin seq → α), layer ∈ rest → RegionInvariant radius layer) →
RegionInvariant (rest.length * radius) (neural_network rest)invariant:∀ (layer_1 : Layer fun seq => Fin seq → α), layer_1 ∈ layer :: rest → RegionInvariant radius layer_1composed:RegionInvariant (radius + rest.length * radius) fun input => neural_network rest (layer input)⊢ RegionInvariant ((layer :: rest).length * radius) (neural_network (layer :: rest))))
(ih (fun layer member => invariant @layer (by α:Type useq:Natradius:Natlayer✝:Layer fun seq => Fin seq → αrest:List (Layer fun seq => Fin seq → α)ih:(∀ (layer : Layer fun seq => Fin seq → α), layer ∈ rest → RegionInvariant radius layer) →
RegionInvariant (rest.length * radius) (neural_network rest)invariant:∀ (layer_1 : Layer fun seq => Fin seq → α), layer_1 ∈ layer :: rest → RegionInvariant radius layer_1layer:Layer fun seq => Fin seq → αmember:layer ∈ rest⊢ layer ∈ layer✝ :: restcons α:Type useq:Natradius:Natlayer:Layer fun seq => Fin seq → αrest:List (Layer fun seq => Fin seq → α)ih:(∀ (layer : Layer fun seq => Fin seq → α), layer ∈ rest → RegionInvariant radius layer) →
RegionInvariant (rest.length * radius) (neural_network rest)invariant:∀ (layer_1 : Layer fun seq => Fin seq → α), layer_1 ∈ layer :: rest → RegionInvariant radius layer_1composed:RegionInvariant (radius + rest.length * radius) fun input => neural_network rest (layer input)⊢ RegionInvariant ((layer :: rest).length * radius) (neural_network (layer :: rest)) simp [member] All goals completed! 🐙cons α:Type useq:Natradius:Natlayer:Layer fun seq => Fin seq → αrest:List (Layer fun seq => Fin seq → α)ih:(∀ (layer : Layer fun seq => Fin seq → α), layer ∈ rest → RegionInvariant radius layer) →
RegionInvariant (rest.length * radius) (neural_network rest)invariant:∀ (layer_1 : Layer fun seq => Fin seq → α), layer_1 ∈ layer :: rest → RegionInvariant radius layer_1composed:RegionInvariant (radius + rest.length * radius) fun input => neural_network rest (layer input)⊢ RegionInvariant ((layer :: rest).length * radius) (neural_network (layer :: rest)))))cons α:Type useq:Natradius:Natlayer:Layer fun seq => Fin seq → αrest:List (Layer fun seq => Fin seq → α)ih:(∀ (layer : Layer fun seq => Fin seq → α), layer ∈ rest → RegionInvariant radius layer) →
RegionInvariant (rest.length * radius) (neural_network rest)invariant:∀ (layer_1 : Layer fun seq => Fin seq → α), layer_1 ∈ layer :: rest → RegionInvariant radius layer_1composed:RegionInvariant (radius + rest.length * radius) fun input => neural_network rest (layer input)⊢ RegionInvariant ((layer :: rest).length * radius) (neural_network (layer :: rest))
simpa only [RegionInvariant, List.length_cons,
Nat.add_mul, Nat.one_mul, Nat.add_comm,
neural_network, List.foldl_cons] using composed All goals completed! 🐙Finally, we need to show the radius of SWA. We first show that our implementation of masking leads to a given radius and then apply this to the sparse attention implementation.
attribute [local simp] Rat.add_zero Rat.zero_add Rat.zero_mul Rat.mul_zero
theorem Matrix.masked_matmul_region_invariant (radius : Nat)
(weights : Matrix seq seq) :
RegionInvariant radius (fun values : Matrix seq hidden =>
(matrix_mask radius weights).matmul values) := by seq:Nathidden:Natradius:Natweights:Matrix seq seq⊢ RegionInvariant radius fun values => (matrix_mask radius weights).matmul values
intro input other s agree seq:Nathidden:Natradius:Natweights:Matrix seq seqinput:Fin seq → Vector hiddenother:Fin seq → Vector hiddens:Fin seqagree:∀ (t : Fin seq), InWindow radius s t → input t = other t⊢ (fun values => (matrix_mask radius weights).matmul values) input s =
(fun values => (matrix_mask radius weights).matmul values) other s
funext j seq:Nathidden:Natradius:Natweights:Matrix seq seqinput:Fin seq → Vector hiddenother:Fin seq → Vector hiddens:Fin seqagree:∀ (t : Fin seq), InWindow radius s t → input t = other tj:Fin hidden⊢ (fun values => (matrix_mask radius weights).matmul values) input s j =
(fun values => (matrix_mask radius weights).matmul values) other s j
apply congrArg Vector.sum seq:Nathidden:Natradius:Natweights:Matrix seq seqinput:Fin seq → Vector hiddenother:Fin seq → Vector hiddens:Fin seqagree:∀ (t : Fin seq), InWindow radius s t → input t = other tj:Fin hidden⊢ matrix_mask radius weights s * transpose input j = matrix_mask radius weights s * transpose other j
funext t seq:Nathidden:Natradius:Natweights:Matrix seq seqinput:Fin seq → Vector hiddenother:Fin seq → Vector hiddens:Fin seqagree:∀ (t : Fin seq), InWindow radius s t → input t = other tj:Fin hiddent:Fin seq⊢ (matrix_mask radius weights s * transpose input j) t = (matrix_mask radius weights s * transpose other j) t
change weights s t * window_mask radius s t * input t j =
weights s t * window_mask radius s t * other t j seq:Nathidden:Natradius:Natweights:Matrix seq seqinput:Fin seq → Vector hiddenother:Fin seq → Vector hiddens:Fin seqagree:∀ (t : Fin seq), InWindow radius s t → input t = other tj:Fin hiddent:Fin seq⊢ weights s t * window_mask radius s t * input t j = weights s t * window_mask radius s t * other t j
by_cases ht : InWindow radius s t pos seq:Nathidden:Natradius:Natweights:Matrix seq seqinput:Fin seq → Vector hiddenother:Fin seq → Vector hiddens:Fin seqagree:∀ (t : Fin seq), InWindow radius s t → input t = other tj:Fin hiddent:Fin seqht:InWindow radius s t⊢ weights s t * window_mask radius s t * input t j = weights s t * window_mask radius s t * other t jneg seq:Nathidden:Natradius:Natweights:Matrix seq seqinput:Fin seq → Vector hiddenother:Fin seq → Vector hiddens:Fin seqagree:∀ (t : Fin seq), InWindow radius s t → input t = other tj:Fin hiddent:Fin seqht:¬InWindow radius s t⊢ weights s t * window_mask radius s t * input t j = weights s t * window_mask radius s t * other t j
· pos seq:Nathidden:Natradius:Natweights:Matrix seq seqinput:Fin seq → Vector hiddenother:Fin seq → Vector hiddens:Fin seqagree:∀ (t : Fin seq), InWindow radius s t → input t = other tj:Fin hiddent:Fin seqht:InWindow radius s t⊢ weights s t * window_mask radius s t * input t j = weights s t * window_mask radius s t * other t j rw [agree t ht pos seq:Nathidden:Natradius:Natweights:Matrix seq seqinput:Fin seq → Vector hiddenother:Fin seq → Vector hiddens:Fin seqagree:∀ (t : Fin seq), InWindow radius s t → input t = other tj:Fin hiddent:Fin seqht:InWindow radius s t⊢ weights s t * window_mask radius s t * other t j = weights s t * window_mask radius s t * other t j All goals completed! 🐙] All goals completed! 🐙
· neg seq:Nathidden:Natradius:Natweights:Matrix seq seqinput:Fin seq → Vector hiddenother:Fin seq → Vector hiddens:Fin seqagree:∀ (t : Fin seq), InWindow radius s t → input t = other tj:Fin hiddent:Fin seqht:¬InWindow radius s t⊢ weights s t * window_mask radius s t * input t j = weights s t * window_mask radius s t * other t j simp [window_mask, ht] All goals completed! 🐙
theorem swa_region_invariant (radius : Nat) (wq wk wv : Matrix hidden hidden) :
RegionInvariant (seq := seq) radius
(fun input => swa radius (project_qkv input wq wk wv)) := by hidden:Natseq:Natradius:Natwq:Matrix hidden hiddenwk:Matrix hidden hiddenwv:Matrix hidden hidden⊢ RegionInvariant radius fun input => swa radius (project_qkv input wq wk wv)
intro input other s agree hidden:Natseq:Natradius:Natwq:Matrix hidden hiddenwk:Matrix hidden hiddenwv:Matrix hidden hiddeninput:Fin seq → Vector hiddenother:Fin seq → Vector hiddens:Fin seqagree:∀ (t : Fin seq), InWindow radius s t → input t = other t⊢ (fun input => swa radius (project_qkv input wq wk wv)) input s =
(fun input => swa radius (project_qkv input wq wk wv)) other s
have center := agree s (by hidden:Natseq:Natradius:Natwq:Matrix hidden hiddenwk:Matrix hidden hiddenwv:Matrix hidden hiddeninput:Fin seq → Vector hiddenother:Fin seq → Vector hiddens:Fin seqagree:∀ (t : Fin seq), InWindow radius s t → input t = other t⊢ InWindow radius s s hidden:Natseq:Natradius:Natwq:Matrix hidden hiddenwk:Matrix hidden hiddenwv:Matrix hidden hiddeninput:Fin seq → Vector hiddenother:Fin seq → Vector hiddens:Fin seqagree:∀ (t : Fin seq), InWindow radius s t → input t = other tcenter:input s = other s⊢ (fun input => swa radius (project_qkv input wq wk wv)) input s =
(fun input => swa radius (project_qkv input wq wk wv)) other s constructor left hidden:Natseq:Natradius:Natwq:Matrix hidden hiddenwk:Matrix hidden hiddenwv:Matrix hidden hiddeninput:Fin seq → Vector hiddenother:Fin seq → Vector hiddens:Fin seqagree:∀ (t : Fin seq), InWindow radius s t → input t = other t⊢ ↑s ≤ ↑s + radiusright hidden:Natseq:Natradius:Natwq:Matrix hidden hiddenwk:Matrix hidden hiddenwv:Matrix hidden hiddeninput:Fin seq → Vector hiddenother:Fin seq → Vector hiddens:Fin seqagree:∀ (t : Fin seq), InWindow radius s t → input t = other t⊢ ↑s ≤ ↑s + radius hidden:Natseq:Natradius:Natwq:Matrix hidden hiddenwk:Matrix hidden hiddenwv:Matrix hidden hiddeninput:Fin seq → Vector hiddenother:Fin seq → Vector hiddens:Fin seqagree:∀ (t : Fin seq), InWindow radius s t → input t = other tcenter:input s = other s⊢ (fun input => swa radius (project_qkv input wq wk wv)) input s =
(fun input => swa radius (project_qkv input wq wk wv)) other s <;> left hidden:Natseq:Natradius:Natwq:Matrix hidden hiddenwk:Matrix hidden hiddenwv:Matrix hidden hiddeninput:Fin seq → Vector hiddenother:Fin seq → Vector hiddens:Fin seqagree:∀ (t : Fin seq), InWindow radius s t → input t = other t⊢ ↑s ≤ ↑s + radiusright hidden:Natseq:Natradius:Natwq:Matrix hidden hiddenwk:Matrix hidden hiddenwv:Matrix hidden hiddeninput:Fin seq → Vector hiddenother:Fin seq → Vector hiddens:Fin seqagree:∀ (t : Fin seq), InWindow radius s t → input t = other t⊢ ↑s ≤ ↑s + radius hidden:Natseq:Natradius:Natwq:Matrix hidden hiddenwk:Matrix hidden hiddenwv:Matrix hidden hiddeninput:Fin seq → Vector hiddenother:Fin seq → Vector hiddens:Fin seqagree:∀ (t : Fin seq), InWindow radius s t → input t = other tcenter:input s = other s⊢ (fun input => swa radius (project_qkv input wq wk wv)) input s =
(fun input => swa radius (project_qkv input wq wk wv)) other s omega All goals completed! 🐙 hidden:Natseq:Natradius:Natwq:Matrix hidden hiddenwk:Matrix hidden hiddenwv:Matrix hidden hiddeninput:Fin seq → Vector hiddenother:Fin seq → Vector hiddens:Fin seqagree:∀ (t : Fin seq), InWindow radius s t → input t = other tcenter:input s = other s⊢ (fun input => swa radius (project_qkv input wq wk wv)) input s =
(fun input => swa radius (project_qkv input wq wk wv)) other s) hidden:Natseq:Natradius:Natwq:Matrix hidden hiddenwk:Matrix hidden hiddenwv:Matrix hidden hiddeninput:Fin seq → Vector hiddenother:Fin seq → Vector hiddens:Fin seqagree:∀ (t : Fin seq), InWindow radius s t → input t = other tcenter:input s = other s⊢ (fun input => swa radius (project_qkv input wq wk wv)) input s =
(fun input => swa radius (project_qkv input wq wk wv)) other s
let contribution (query row : Vector hidden) : Vector hidden := fun j =>
Vector.dot_product (fun d => query.dot_product (wq.transpose d))
(fun d => row.dot_product (wk.transpose d)) *
row.dot_product (wv.transpose j) hidden:Natseq:Natradius:Natwq:Matrix hidden hiddenwk:Matrix hidden hiddenwv:Matrix hidden hiddeninput:Fin seq → Vector hiddenother:Fin seq → Vector hiddens:Fin seqagree:∀ (t : Fin seq), InWindow radius s t → input t = other tcenter:input s = other scontribution:Vector hidden → Vector hidden → Vector hidden :=
fun query row j =>
(Vector.dot_product (fun d => query.dot_product (wq.transpose d)) fun d => row.dot_product (wk.transpose d)) *
row.dot_product (wv.transpose j)⊢ (fun input => swa radius (project_qkv input wq wk wv)) input s =
(fun input => swa radius (project_qkv input wq wk wv)) other s
have masked (x : Matrix seq hidden) :
swa radius (project_qkv x wq wk wv) s =
(matrix_mask radius (fun _ _ => 1)).matmul
(vmap (contribution (x s)) x) s := by hidden:Natseq:Natradius:Natwq:Matrix hidden hiddenwk:Matrix hidden hiddenwv:Matrix hidden hidden⊢ RegionInvariant radius fun input => swa radius (project_qkv input wq wk wv) hidden:Natseq:Natradius:Natwq:Matrix hidden hiddenwk:Matrix hidden hiddenwv:Matrix hidden hiddeninput:Fin seq → Vector hiddenother:Fin seq → Vector hiddens:Fin seqagree:∀ (t : Fin seq), InWindow radius s t → input t = other tcenter:input s = other scontribution:Vector hidden → Vector hidden → Vector hidden :=
fun query row j =>
(Vector.dot_product (fun d => query.dot_product (wq.transpose d)) fun d => row.dot_product (wk.transpose d)) *
row.dot_product (wv.transpose j)masked:∀ (x : Matrix seq hidden),
swa radius (project_qkv x wq wk wv) s = (matrix_mask radius fun x x_1 => 1).matmul (vmap (contribution (x s)) x) s⊢ (fun input => swa radius (project_qkv input wq wk wv)) input s =
(fun input => swa radius (project_qkv input wq wk wv)) other s
funext j hidden:Natseq:Natradius:Natwq:Matrix hidden hiddenwk:Matrix hidden hiddenwv:Matrix hidden hiddeninput:Fin seq → Vector hiddenother:Fin seq → Vector hiddens:Fin seqagree:∀ (t : Fin seq), InWindow radius s t → input t = other tcenter:input s = other scontribution:Vector hidden → Vector hidden → Vector hidden :=
fun query row j =>
(Vector.dot_product (fun d => query.dot_product (wq.transpose d)) fun d => row.dot_product (wk.transpose d)) *
row.dot_product (wv.transpose j)x:Matrix seq hiddenj:Fin hidden⊢ swa radius (project_qkv x wq wk wv) s j = (matrix_mask radius fun x x_1 => 1).matmul (vmap (contribution (x s)) x) s j hidden:Natseq:Natradius:Natwq:Matrix hidden hiddenwk:Matrix hidden hiddenwv:Matrix hidden hiddeninput:Fin seq → Vector hiddenother:Fin seq → Vector hiddens:Fin seqagree:∀ (t : Fin seq), InWindow radius s t → input t = other tcenter:input s = other scontribution:Vector hidden → Vector hidden → Vector hidden :=
fun query row j =>
(Vector.dot_product (fun d => query.dot_product (wq.transpose d)) fun d => row.dot_product (wk.transpose d)) *
row.dot_product (wv.transpose j)masked:∀ (x : Matrix seq hidden),
swa radius (project_qkv x wq wk wv) s = (matrix_mask radius fun x x_1 => 1).matmul (vmap (contribution (x s)) x) s⊢ (fun input => swa radius (project_qkv input wq wk wv)) input s =
(fun input => swa radius (project_qkv input wq wk wv)) other s
apply congrArg Vector.sum hidden:Natseq:Natradius:Natwq:Matrix hidden hiddenwk:Matrix hidden hiddenwv:Matrix hidden hiddeninput:Fin seq → Vector hiddenother:Fin seq → Vector hiddens:Fin seqagree:∀ (t : Fin seq), InWindow radius s t → input t = other tcenter:input s = other scontribution:Vector hidden → Vector hidden → Vector hidden :=
fun query row j =>
(Vector.dot_product (fun d => query.dot_product (wq.transpose d)) fun d => row.dot_product (wk.transpose d)) *
row.dot_product (wv.transpose j)x:Matrix seq hiddenj:Fin hidden⊢ matrix_mask radius ((x.matmul wq).matmul (x.matmul wk).transpose) s * (x.matmul wv).transpose j =
matrix_mask radius (fun x x_1 => 1) s * Matrix.transpose (vmap (contribution (x s)) x) j hidden:Natseq:Natradius:Natwq:Matrix hidden hiddenwk:Matrix hidden hiddenwv:Matrix hidden hiddeninput:Fin seq → Vector hiddenother:Fin seq → Vector hiddens:Fin seqagree:∀ (t : Fin seq), InWindow radius s t → input t = other tcenter:input s = other scontribution:Vector hidden → Vector hidden → Vector hidden :=
fun query row j =>
(Vector.dot_product (fun d => query.dot_product (wq.transpose d)) fun d => row.dot_product (wk.transpose d)) *
row.dot_product (wv.transpose j)masked:∀ (x : Matrix seq hidden),
swa radius (project_qkv x wq wk wv) s = (matrix_mask radius fun x x_1 => 1).matmul (vmap (contribution (x s)) x) s⊢ (fun input => swa radius (project_qkv input wq wk wv)) input s =
(fun input => swa radius (project_qkv input wq wk wv)) other s
funext t hidden:Natseq:Natradius:Natwq:Matrix hidden hiddenwk:Matrix hidden hiddenwv:Matrix hidden hiddeninput:Fin seq → Vector hiddenother:Fin seq → Vector hiddens:Fin seqagree:∀ (t : Fin seq), InWindow radius s t → input t = other tcenter:input s = other scontribution:Vector hidden → Vector hidden → Vector hidden :=
fun query row j =>
(Vector.dot_product (fun d => query.dot_product (wq.transpose d)) fun d => row.dot_product (wk.transpose d)) *
row.dot_product (wv.transpose j)x:Matrix seq hiddenj:Fin hiddent:Fin seq⊢ (matrix_mask radius ((x.matmul wq).matmul (x.matmul wk).transpose) s * (x.matmul wv).transpose j) t =
(matrix_mask radius (fun x x_1 => 1) s * Matrix.transpose (vmap (contribution (x s)) x) j) t hidden:Natseq:Natradius:Natwq:Matrix hidden hiddenwk:Matrix hidden hiddenwv:Matrix hidden hiddeninput:Fin seq → Vector hiddenother:Fin seq → Vector hiddens:Fin seqagree:∀ (t : Fin seq), InWindow radius s t → input t = other tcenter:input s = other scontribution:Vector hidden → Vector hidden → Vector hidden :=
fun query row j =>
(Vector.dot_product (fun d => query.dot_product (wq.transpose d)) fun d => row.dot_product (wk.transpose d)) *
row.dot_product (wv.transpose j)masked:∀ (x : Matrix seq hidden),
swa radius (project_qkv x wq wk wv) s = (matrix_mask radius fun x x_1 => 1).matmul (vmap (contribution (x s)) x) s⊢ (fun input => swa radius (project_qkv input wq wk wv)) input s =
(fun input => swa radius (project_qkv input wq wk wv)) other s
exact (show ∀ a b c : Rat, a * b * c = (1 * b) * (a * c) by hidden:Natseq:Natradius:Natwq:Matrix hidden hiddenwk:Matrix hidden hiddenwv:Matrix hidden hidden⊢ RegionInvariant radius fun input => swa radius (project_qkv input wq wk wv) hidden:Natseq:Natradius:Natwq:Matrix hidden hiddenwk:Matrix hidden hiddenwv:Matrix hidden hiddeninput:Fin seq → Vector hiddenother:Fin seq → Vector hiddens:Fin seqagree:∀ (t : Fin seq), InWindow radius s t → input t = other tcenter:input s = other scontribution:Vector hidden → Vector hidden → Vector hidden :=
fun query row j =>
(Vector.dot_product (fun d => query.dot_product (wq.transpose d)) fun d => row.dot_product (wk.transpose d)) *
row.dot_product (wv.transpose j)masked:∀ (x : Matrix seq hidden),
swa radius (project_qkv x wq wk wv) s = (matrix_mask radius fun x x_1 => 1).matmul (vmap (contribution (x s)) x) s⊢ (fun input => swa radius (project_qkv input wq wk wv)) input s =
(fun input => swa radius (project_qkv input wq wk wv)) other s
intros hidden:Natseq:Natradius:Natwq:Matrix hidden hiddenwk:Matrix hidden hiddenwv:Matrix hidden hiddeninput:Fin seq → Vector hiddenother:Fin seq → Vector hiddens:Fin seqagree:∀ (t : Fin seq), InWindow radius s t → input t = other tcenter:input s = other scontribution:Vector hidden → Vector hidden → Vector hidden :=
fun query row j =>
(Vector.dot_product (fun d => query.dot_product (wq.transpose d)) fun d => row.dot_product (wk.transpose d)) *
row.dot_product (wv.transpose j)x:Matrix seq hiddenj:Fin hiddent:Fin seqa✝:Ratb✝:Ratc✝:Rat⊢ a✝ * b✝ * c✝ = 1 * b✝ * (a✝ * c✝) hidden:Natseq:Natradius:Natwq:Matrix hidden hiddenwk:Matrix hidden hiddenwv:Matrix hidden hiddeninput:Fin seq → Vector hiddenother:Fin seq → Vector hiddens:Fin seqagree:∀ (t : Fin seq), InWindow radius s t → input t = other tcenter:input s = other scontribution:Vector hidden → Vector hidden → Vector hidden :=
fun query row j =>
(Vector.dot_product (fun d => query.dot_product (wq.transpose d)) fun d => row.dot_product (wk.transpose d)) *
row.dot_product (wv.transpose j)masked:∀ (x : Matrix seq hidden),
swa radius (project_qkv x wq wk wv) s = (matrix_mask radius fun x x_1 => 1).matmul (vmap (contribution (x s)) x) s⊢ (fun input => swa radius (project_qkv input wq wk wv)) input s =
(fun input => swa radius (project_qkv input wq wk wv)) other s; grind All goals completed! 🐙 hidden:Natseq:Natradius:Natwq:Matrix hidden hiddenwk:Matrix hidden hiddenwv:Matrix hidden hiddeninput:Fin seq → Vector hiddenother:Fin seq → Vector hiddens:Fin seqagree:∀ (t : Fin seq), InWindow radius s t → input t = other tcenter:input s = other scontribution:Vector hidden → Vector hidden → Vector hidden :=
fun query row j =>
(Vector.dot_product (fun d => query.dot_product (wq.transpose d)) fun d => row.dot_product (wk.transpose d)) *
row.dot_product (wv.transpose j)masked:∀ (x : Matrix seq hidden),
swa radius (project_qkv x wq wk wv) s = (matrix_mask radius fun x x_1 => 1).matmul (vmap (contribution (x s)) x) s⊢ (fun input => swa radius (project_qkv input wq wk wv)) input s =
(fun input => swa radius (project_qkv input wq wk wv)) other s) _ _ _ hidden:Natseq:Natradius:Natwq:Matrix hidden hiddenwk:Matrix hidden hiddenwv:Matrix hidden hiddeninput:Fin seq → Vector hiddenother:Fin seq → Vector hiddens:Fin seqagree:∀ (t : Fin seq), InWindow radius s t → input t = other tcenter:input s = other scontribution:Vector hidden → Vector hidden → Vector hidden :=
fun query row j =>
(Vector.dot_product (fun d => query.dot_product (wq.transpose d)) fun d => row.dot_product (wk.transpose d)) *
row.dot_product (wv.transpose j)masked:∀ (x : Matrix seq hidden),
swa radius (project_qkv x wq wk wv) s = (matrix_mask radius fun x x_1 => 1).matmul (vmap (contribution (x s)) x) s⊢ (fun input => swa radius (project_qkv input wq wk wv)) input s =
(fun input => swa radius (project_qkv input wq wk wv)) other s
have same := Matrix.masked_matmul_region_invariant radius (fun _ _ => 1)
(vmap (contribution (input s)) input) (vmap (contribution (input s)) other) s
(fun t ht => congrArg (contribution (input s)) (agree t ht)) hidden:Natseq:Natradius:Natwq:Matrix hidden hiddenwk:Matrix hidden hiddenwv:Matrix hidden hiddeninput:Fin seq → Vector hiddenother:Fin seq → Vector hiddens:Fin seqagree:∀ (t : Fin seq), InWindow radius s t → input t = other tcenter:input s = other scontribution:Vector hidden → Vector hidden → Vector hidden :=
fun query row j =>
(Vector.dot_product (fun d => query.dot_product (wq.transpose d)) fun d => row.dot_product (wk.transpose d)) *
row.dot_product (wv.transpose j)masked:∀ (x : Matrix seq hidden),
swa radius (project_qkv x wq wk wv) s = (matrix_mask radius fun x x_1 => 1).matmul (vmap (contribution (x s)) x) ssame:(fun values => (matrix_mask radius fun x x_1 => 1).matmul values) (vmap (contribution (input s)) input) s =
(fun values => (matrix_mask radius fun x x_1 => 1).matmul values) (vmap (contribution (input s)) other) s⊢ (fun input => swa radius (project_qkv input wq wk wv)) input s =
(fun input => swa radius (project_qkv input wq wk wv)) other s
exact (masked input).trans
(same.trans (by hidden:Natseq:Natradius:Natwq:Matrix hidden hiddenwk:Matrix hidden hiddenwv:Matrix hidden hiddeninput:Fin seq → Vector hiddenother:Fin seq → Vector hiddens:Fin seqagree:∀ (t : Fin seq), InWindow radius s t → input t = other tcenter:input s = other scontribution:Vector hidden → Vector hidden → Vector hidden :=
fun query row j =>
(Vector.dot_product (fun d => query.dot_product (wq.transpose d)) fun d => row.dot_product (wk.transpose d)) *
row.dot_product (wv.transpose j)masked:∀ (x : Matrix seq hidden),
swa radius (project_qkv x wq wk wv) s = (matrix_mask radius fun x x_1 => 1).matmul (vmap (contribution (x s)) x) ssame:(fun values => (matrix_mask radius fun x x_1 => 1).matmul values) (vmap (contribution (input s)) input) s =
(fun values => (matrix_mask radius fun x x_1 => 1).matmul values) (vmap (contribution (input s)) other) s⊢ (fun values => (matrix_mask radius fun x x_1 => 1).matmul values) (vmap (contribution (input s)) other) s =
(fun input => swa radius (project_qkv input wq wk wv)) other s simpa only [center] using (masked other).symm All goals completed! 🐙))Flash Attention
Once we have standard attention implemented, we can begin to consider optimizations. Flash attention uses a tiling approach to split the full sequence into groups which can be computed separately to reduce memory. We define a typed tiling.
-- Select one of tiles groups, each of size n.
def tile (n : Nat) (xs : Fin (tiles * n) → α) (c : Fin tiles) : Fin n → α :=
-- The proof here shows that it is legal to make this slicing.
slice (c.val * n) n (by α:Type uβ:Type vδ:Type wtiles:Natn:Natxs:Fin (tiles * n) → αc:Fin tiles⊢ ↑c * n + n ≤ tiles * n
have := Nat.mul_le_mul_right n c.isLt α:Type uβ:Type vδ:Type wtiles:Natn:Natxs:Fin (tiles * n) → αc:Fin tilesthis:(↑c).succ * n ≤ tiles * n⊢ ↑c * n + n ≤ tiles * n
simpa [Nat.succ_mul] using this All goals completed! 🐙) xs-- Apply an accumulator across each of these.
def tile_fold (tiles n : Nat) (step : σ → α → σ)
(xs : Fin (tiles * n) → α) (initial : σ) : σ :=
scan (fun state chunk => scan step chunk state) (tile n xs) initialOnce we have this primitive, we can compute and aggregate a value for each tile.
structure FlashAcc (hidden : Nat) where
scoreSum : Rat
weighted : Vector hiddendef flash_step (r : α → Rat) (v : α → Vector hidden)
(state : FlashAcc hidden) (t : α) : FlashAcc hidden :=
let score := r t
{ scoreSum := state.scoreSum + score
weighted := fun j => state.weighted j + score * v t j }def flash_attention (tiles n : Nat)
(input : (Matrix (tiles * n) hidden × Matrix (tiles * n) hidden) ×
Matrix (tiles * n) hidden) :
Matrix (tiles * n) hidden :=
let ⟨⟨q, k⟩, v⟩ := input
fun s =>
let r := fun t => 1 + relu (Vector.sum (fun d => q s d * k t d))
let initial : FlashAcc hidden := ⟨0, fun _ => 0⟩
let state := tile_fold tiles n (flash_step r v)
(fun t : Fin (tiles * n) => t) initial
fun j => state.weighted j / state.scoreSumFor our equivalence proof, we need to define some basic properties of scans and sums. These are not unique to Flash attention, but would be in a core mathematics library.
theorem scan_slice (step : σ → α → σ) (xs : Fin n → α)
(start count : Nat) (h : start + count ≤ n) (initial : σ) :
scan step (slice start count h xs)
(scan step (slice 0 start (by α:Type uβ:Type vδ:Type wσ:Sort ?u.7n:Natstep:σ → α → σxs:Fin n → αstart:Natcount:Nath:start + count ≤ ninitial:σ⊢ 0 + start ≤ n omega All goals completed! 🐙) xs) initial) =
scan step (slice 0 (start + count) (by α:Type uβ:Type vδ:Type wσ:Sort ?u.7n:Natstep:σ → α → σxs:Fin n → αstart:Natcount:Nath:start + count ≤ ninitial:σ⊢ 0 + (start + count) ≤ n omega All goals completed! 🐙) xs) initial := by α:Type uσ:Sort u_1n:Natstep:σ → α → σxs:Fin n → αstart:Natcount:Nath:start + count ≤ ninitial:σ⊢ scan step (slice start count h xs) (scan step (slice 0 start ⋯ xs) initial) =
scan step (slice 0 (start + count) ⋯ xs) initial
simp only [scan, Fin.foldl_add, slice, select, Nat.add_zero, Fin.val_castLE,
Fin.val_natAdd, Nat.add_comm] All goals completed! 🐙theorem scan_full (step : σ → α → σ) (xs : Fin n → α) (initial : σ)
(h : count = n) :
scan step (slice 0 count (by α:Type uβ:Type vδ:Type wσ:Sort ?u.8n:Natcount:Natstep:σ → α → σxs:Fin n → αinitial:σh:count = n⊢ 0 + count ≤ n omega All goals completed! 🐙) xs) initial = scan step xs initial := by α:Type uσ:Sort u_1n:Natcount:Natstep:σ → α → σxs:Fin n → αinitial:σh:count = n⊢ scan step (slice 0 count ⋯ xs) initial = scan step xs initial
subst count α:Type uσ:Sort u_1n:Natstep:σ → α → σxs:Fin n → αinitial:σ⊢ scan step (slice 0 n ⋯ xs) initial = scan step xs initial
rfl All goals completed! 🐙theorem Vector.sum_succ (f : Vector (n + 1)) :
f.sum = f 0 + Vector.sum (fun i : Fin n => f i.succ) := by n:Natf:Vector (n + 1)⊢ f.sum = f 0 + sum fun i => f i.succ
simp [Vector.sum, fori, List.ofFn_succ] All goals completed! 🐙
theorem Vector.sum_mul (f : Vector n) (c : Rat) :
f.sum * c = Vector.sum (fun i => f i * c) := by n:Natf:Vector nc:Rat⊢ f.sum * c = sum fun i => f i * c
induction n with
| zero => zero c:Ratf:Vector 0⊢ f.sum * c = sum fun i => f i * c simp [Vector.sum, fori, Rat.zero_mul] All goals completed! 🐙
| succ n ih => succ c:Ratn:Natih:∀ (f : Vector n), f.sum * c = sum fun i => f i * cf:Vector (n + 1)⊢ f.sum * c = sum fun i => f i * c rw [Vector.sum_succ, succ c:Ratn:Natih:∀ (f : Vector n), f.sum * c = sum fun i => f i * cf:Vector (n + 1)⊢ (f 0 + sum fun i => f i.succ) * c = sum fun i => f i * c All goals completed! 🐙 Rat.add_mul, succ c:Ratn:Natih:∀ (f : Vector n), f.sum * c = sum fun i => f i * cf:Vector (n + 1)⊢ f 0 * c + (sum fun i => f i.succ) * c = sum fun i => f i * c All goals completed! 🐙 ih, succ c:Ratn:Natih:∀ (f : Vector n), f.sum * c = sum fun i => f i * cf:Vector (n + 1)⊢ (f 0 * c + sum fun i => f i.succ * c) = sum fun i => f i * c All goals completed! 🐙 Vector.sum_succ succ c:Ratn:Natih:∀ (f : Vector n), f.sum * c = sum fun i => f i * cf:Vector (n + 1)⊢ (f 0 * c + sum fun i => f i.succ * c) = f 0 * c + sum fun i => f i.succ * c All goals completed! 🐙] All goals completed! 🐙theorem Vector.sum_last (f : Vector (n + 1)) :
f.sum = Vector.sum (fun i : Fin n => f i.castSucc) + f (Fin.last n) := by n:Natf:Vector (n + 1)⊢ f.sum = (sum fun i => f i.castSucc) + f (Fin.last n)
simp only [Vector.sum, fori, List.ofFn_succ_last, List.sum_append, List.sum_cons,
List.sum_nil, Rat.add_zero] All goals completed! 🐙
theorem Vector.sum_rev (f : Vector n) : Vector.sum (fun t => f t.rev) = f.sum := by n:Natf:Vector n⊢ (sum fun t => f t.rev) = f.sum
induction n with
| zero => zero f:Vector 0⊢ (sum fun t => f t.rev) = f.sum rfl All goals completed! 🐙
| succ n ih => succ n:Natih:∀ (f : Vector n), (sum fun t => f t.rev) = f.sumf:Vector (n + 1)⊢ (sum fun t => f t.rev) = f.sum
rw [Vector.sum_succ, succ n:Natih:∀ (f : Vector n), (sum fun t => f t.rev) = f.sumf:Vector (n + 1)⊢ (f (Fin.rev 0) + sum fun i => f i.succ.rev) = f.sum succ n:Natih:∀ (f : Vector n), (sum fun t => f t.rev) = f.sumf:Vector (n + 1)⊢ (f (Fin.rev 0) + sum fun i => f i.succ.rev) = (sum fun i => f i.castSucc) + f (Fin.last n) Vector.sum_last f succ n:Natih:∀ (f : Vector n), (sum fun t => f t.rev) = f.sumf:Vector (n + 1)⊢ (f (Fin.rev 0) + sum fun i => f i.succ.rev) = (sum fun i => f i.castSucc) + f (Fin.last n) succ n:Natih:∀ (f : Vector n), (sum fun t => f t.rev) = f.sumf:Vector (n + 1)⊢ (f (Fin.rev 0) + sum fun i => f i.succ.rev) = (sum fun i => f i.castSucc) + f (Fin.last n)]succ n:Natih:∀ (f : Vector n), (sum fun t => f t.rev) = f.sumf:Vector (n + 1)⊢ (f (Fin.rev 0) + sum fun i => f i.succ.rev) = (sum fun i => f i.castSucc) + f (Fin.last n)
have h := ih (fun i => f i.castSucc) succ n:Natih:∀ (f : Vector n), (sum fun t => f t.rev) = f.sumf:Vector (n + 1)h:(sum fun t => f t.rev.castSucc) = sum fun i => f i.castSucc⊢ (f (Fin.rev 0) + sum fun i => f i.succ.rev) = (sum fun i => f i.castSucc) + f (Fin.last n)
simpa [Fin.rev_succ, Rat.add_comm] using
congrArg (fun z => f (Fin.last n) + z) h All goals completed! 🐙
theorem tile_fold_eq (tiles n : Nat) (step : σ → α → σ)
(xs : Fin (tiles * n) → α) (initial : σ) :
tile_fold tiles n step xs initial = scan step xs initial := by α:Type uσ:Sort u_1tiles:Natn:Natstep:σ → α → σxs:Fin (tiles * n) → αinitial:σ⊢ tile_fold tiles n step xs initial = scan step xs initial
induction tiles generalizing initial with
| zero => zero α:Type uσ:Sort u_1n:Natstep:σ → α → σxs:Fin (0 * n) → αinitial:σ⊢ tile_fold 0 n step xs initial = scan step xs initial simp [tile_fold, scan] All goals completed! 🐙
| succ tiles ih => succ α:Type uσ:Sort u_1n:Natstep:σ → α → σtiles:Natih:∀ (xs : Fin (tiles * n) → α) (initial : σ), tile_fold tiles n step xs initial = scan step xs initialxs:Fin ((tiles + 1) * n) → αinitial:σ⊢ tile_fold (tiles + 1) n step xs initial = scan step xs initial
have shape : (tiles + 1) * n = tiles * n + n := Nat.succ_mul tiles n succ α:Type uσ:Sort u_1n:Natstep:σ → α → σtiles:Natih:∀ (xs : Fin (tiles * n) → α) (initial : σ), tile_fold tiles n step xs initial = scan step xs initialxs:Fin ((tiles + 1) * n) → αinitial:σshape:(tiles + 1) * n = tiles * n + n⊢ tile_fold (tiles + 1) n step xs initial = scan step xs initial
rw [tile_fold, succ α:Type uσ:Sort u_1n:Natstep:σ → α → σtiles:Natih:∀ (xs : Fin (tiles * n) → α) (initial : σ), tile_fold tiles n step xs initial = scan step xs initialxs:Fin ((tiles + 1) * n) → αinitial:σshape:(tiles + 1) * n = tiles * n + n⊢ scan (fun state chunk => scan step chunk state) (tile n xs) initial = scan step xs initial succ α:Type uσ:Sort u_1n:Natstep:σ → α → σtiles:Natih:∀ (xs : Fin (tiles * n) → α) (initial : σ), tile_fold tiles n step xs initial = scan step xs initialxs:Fin ((tiles + 1) * n) → αinitial:σshape:(tiles + 1) * n = tiles * n + n⊢ scan step (tile n xs (Fin.last tiles)) (Fin.foldl tiles (fun x1 x2 => scan step (tile n xs x2.castSucc) x1) initial) =
scan step xs initial scan, succ α:Type uσ:Sort u_1n:Natstep:σ → α → σtiles:Natih:∀ (xs : Fin (tiles * n) → α) (initial : σ), tile_fold tiles n step xs initial = scan step xs initialxs:Fin ((tiles + 1) * n) → αinitial:σshape:(tiles + 1) * n = tiles * n + n⊢ Fin.foldl (tiles + 1) (fun state i => scan step (tile n xs i) state) initial = scan step xs initial succ α:Type uσ:Sort u_1n:Natstep:σ → α → σtiles:Natih:∀ (xs : Fin (tiles * n) → α) (initial : σ), tile_fold tiles n step xs initial = scan step xs initialxs:Fin ((tiles + 1) * n) → αinitial:σshape:(tiles + 1) * n = tiles * n + n⊢ scan step (tile n xs (Fin.last tiles)) (Fin.foldl tiles (fun x1 x2 => scan step (tile n xs x2.castSucc) x1) initial) =
scan step xs initial Fin.foldl_succ_last succ α:Type uσ:Sort u_1n:Natstep:σ → α → σtiles:Natih:∀ (xs : Fin (tiles * n) → α) (initial : σ), tile_fold tiles n step xs initial = scan step xs initialxs:Fin ((tiles + 1) * n) → αinitial:σshape:(tiles + 1) * n = tiles * n + n⊢ scan step (tile n xs (Fin.last tiles)) (Fin.foldl tiles (fun x1 x2 => scan step (tile n xs x2.castSucc) x1) initial) =
scan step xs initialsucc α:Type uσ:Sort u_1n:Natstep:σ → α → σtiles:Natih:∀ (xs : Fin (tiles * n) → α) (initial : σ), tile_fold tiles n step xs initial = scan step xs initialxs:Fin ((tiles + 1) * n) → αinitial:σshape:(tiles + 1) * n = tiles * n + n⊢ scan step (tile n xs (Fin.last tiles)) (Fin.foldl tiles (fun x1 x2 => scan step (tile n xs x2.castSucc) x1) initial) =
scan step xs initial]succ α:Type uσ:Sort u_1n:Natstep:σ → α → σtiles:Natih:∀ (xs : Fin (tiles * n) → α) (initial : σ), tile_fold tiles n step xs initial = scan step xs initialxs:Fin ((tiles + 1) * n) → αinitial:σshape:(tiles + 1) * n = tiles * n + n⊢ scan step (tile n xs (Fin.last tiles)) (Fin.foldl tiles (fun x1 x2 => scan step (tile n xs x2.castSucc) x1) initial) =
scan step xs initial
change scan step (slice (tiles * n) n (by α:Type uσ:Sort u_1n:Natstep:σ → α → σtiles:Natih:∀ (xs : Fin (tiles * n) → α) (initial : σ), tile_fold tiles n step xs initial = scan step xs initialxs:Fin ((tiles + 1) * n) → αinitial:σshape:(tiles + 1) * n = tiles * n + n⊢ tiles * n + n ≤ (tiles + 1) * n omega All goals completed! 🐙) xs)
(tile_fold tiles n step (slice 0 (tiles * n) (by α:Type uσ:Sort u_1n:Natstep:σ → α → σtiles:Natih:∀ (xs : Fin (tiles * n) → α) (initial : σ), tile_fold tiles n step xs initial = scan step xs initialxs:Fin ((tiles + 1) * n) → αinitial:σshape:(tiles + 1) * n = tiles * n + n⊢ 0 + tiles * n ≤ (tiles + 1) * n omega All goals completed! 🐙) xs) initial) = _
rw [ih, succ α:Type uσ:Sort u_1n:Natstep:σ → α → σtiles:Natih:∀ (xs : Fin (tiles * n) → α) (initial : σ), tile_fold tiles n step xs initial = scan step xs initialxs:Fin ((tiles + 1) * n) → αinitial:σshape:(tiles + 1) * n = tiles * n + n⊢ scan step (slice (tiles * n) n ⋯ xs) (scan step (slice 0 (tiles * n) ⋯ xs) initial) = scan step xs initial succ α:Type uσ:Sort u_1n:Natstep:σ → α → σtiles:Natih:∀ (xs : Fin (tiles * n) → α) (initial : σ), tile_fold tiles n step xs initial = scan step xs initialxs:Fin ((tiles + 1) * n) → αinitial:σshape:(tiles + 1) * n = tiles * n + n⊢ scan step (slice 0 (tiles * n + n) ⋯ xs) initial = scan step xs initial scan_slice succ α:Type uσ:Sort u_1n:Natstep:σ → α → σtiles:Natih:∀ (xs : Fin (tiles * n) → α) (initial : σ), tile_fold tiles n step xs initial = scan step xs initialxs:Fin ((tiles + 1) * n) → αinitial:σshape:(tiles + 1) * n = tiles * n + n⊢ scan step (slice 0 (tiles * n + n) ⋯ xs) initial = scan step xs initialsucc α:Type uσ:Sort u_1n:Natstep:σ → α → σtiles:Natih:∀ (xs : Fin (tiles * n) → α) (initial : σ), tile_fold tiles n step xs initial = scan step xs initialxs:Fin ((tiles + 1) * n) → αinitial:σshape:(tiles + 1) * n = tiles * n + n⊢ scan step (slice 0 (tiles * n + n) ⋯ xs) initial = scan step xs initial]succ α:Type uσ:Sort u_1n:Natstep:σ → α → σtiles:Natih:∀ (xs : Fin (tiles * n) → α) (initial : σ), tile_fold tiles n step xs initial = scan step xs initialxs:Fin ((tiles + 1) * n) → αinitial:σshape:(tiles + 1) * n = tiles * n + n⊢ scan step (slice 0 (tiles * n + n) ⋯ xs) initial = scan step xs initial
exact scan_full _ _ _ shape.symm All goals completed! 🐙The main proof is that Flash attention is equivalent to our original attention. This is done by showing a lemma over the internal fold that we are accumulating the correct values.
private theorem flash_fold (xs : Fin n → α)
(r : α → Rat) (v : α → Vector hidden)
(state : FlashAcc hidden) :
scan (flash_step r v) xs state =
{ scoreSum := state.scoreSum + Vector.sum (fun i => r (xs i))
weighted := fun j => state.weighted j +
Vector.sum (fun i => r (xs i) * v (xs i) j) } := by α:Type un:Nathidden:Natxs:Fin n → αr:α → Ratv:α → Vector hiddenstate:FlashAcc hidden⊢ scan (flash_step r v) xs state =
{ scoreSum := state.scoreSum + Vector.sum fun i => r (xs i),
weighted := fun j => state.weighted j + Vector.sum fun i => r (xs i) * v (xs i) j }
induction n generalizing state with
| zero => zero α:Type uhidden:Natr:α → Ratv:α → Vector hiddenxs:Fin 0 → αstate:FlashAcc hidden⊢ scan (flash_step r v) xs state =
{ scoreSum := state.scoreSum + Vector.sum fun i => r (xs i),
weighted := fun j => state.weighted j + Vector.sum fun i => r (xs i) * v (xs i) j } cases state zero.mk α:Type uhidden:Natr:α → Ratv:α → Vector hiddenxs:Fin 0 → αscoreSum✝:Ratweighted✝:Vector hidden⊢ scan (flash_step r v) xs { scoreSum := scoreSum✝, weighted := weighted✝ } =
{ scoreSum := { scoreSum := scoreSum✝, weighted := weighted✝ }.scoreSum + Vector.sum fun i => r (xs i),
weighted := fun j =>
{ scoreSum := scoreSum✝, weighted := weighted✝ }.weighted j + Vector.sum fun i => r (xs i) * v (xs i) j }; simp [scan, Vector.sum, fori] All goals completed! 🐙
| succ n ih => succ α:Type uhidden:Natr:α → Ratv:α → Vector hiddenn:Natih:∀ (xs : Fin n → α) (state : FlashAcc hidden),
scan (flash_step r v) xs state =
{ scoreSum := state.scoreSum + Vector.sum fun i => r (xs i),
weighted := fun j => state.weighted j + Vector.sum fun i => r (xs i) * v (xs i) j }xs:Fin (n + 1) → αstate:FlashAcc hidden⊢ scan (flash_step r v) xs state =
{ scoreSum := state.scoreSum + Vector.sum fun i => r (xs i),
weighted := fun j => state.weighted j + Vector.sum fun i => r (xs i) * v (xs i) j }
simp only [scan, Fin.foldl_succ] succ α:Type uhidden:Natr:α → Ratv:α → Vector hiddenn:Natih:∀ (xs : Fin n → α) (state : FlashAcc hidden),
scan (flash_step r v) xs state =
{ scoreSum := state.scoreSum + Vector.sum fun i => r (xs i),
weighted := fun j => state.weighted j + Vector.sum fun i => r (xs i) * v (xs i) j }xs:Fin (n + 1) → αstate:FlashAcc hidden⊢ Fin.foldl n (fun x i => flash_step r v x (xs i.succ)) (flash_step r v state (xs 0)) =
{ scoreSum := state.scoreSum + Vector.sum fun i => r (xs i),
weighted := fun j => state.weighted j + Vector.sum fun i => r (xs i) * v (xs i) j }
change scan (flash_step r v) (fun i => xs i.succ)
(flash_step r v state (xs 0)) = _ succ α:Type uhidden:Natr:α → Ratv:α → Vector hiddenn:Natih:∀ (xs : Fin n → α) (state : FlashAcc hidden),
scan (flash_step r v) xs state =
{ scoreSum := state.scoreSum + Vector.sum fun i => r (xs i),
weighted := fun j => state.weighted j + Vector.sum fun i => r (xs i) * v (xs i) j }xs:Fin (n + 1) → αstate:FlashAcc hidden⊢ scan (flash_step r v) (fun i => xs i.succ) (flash_step r v state (xs 0)) =
{ scoreSum := state.scoreSum + Vector.sum fun i => r (xs i),
weighted := fun j => state.weighted j + Vector.sum fun i => r (xs i) * v (xs i) j }
rw [ih succ α:Type uhidden:Natr:α → Ratv:α → Vector hiddenn:Natih:∀ (xs : Fin n → α) (state : FlashAcc hidden),
scan (flash_step r v) xs state =
{ scoreSum := state.scoreSum + Vector.sum fun i => r (xs i),
weighted := fun j => state.weighted j + Vector.sum fun i => r (xs i) * v (xs i) j }xs:Fin (n + 1) → αstate:FlashAcc hidden⊢ { scoreSum := (flash_step r v state (xs 0)).scoreSum + Vector.sum fun i => r (xs i.succ),
weighted := fun j =>
(flash_step r v state (xs 0)).weighted j + Vector.sum fun i => r (xs i.succ) * v (xs i.succ) j } =
{ scoreSum := state.scoreSum + Vector.sum fun i => r (xs i),
weighted := fun j => state.weighted j + Vector.sum fun i => r (xs i) * v (xs i) j } succ α:Type uhidden:Natr:α → Ratv:α → Vector hiddenn:Natih:∀ (xs : Fin n → α) (state : FlashAcc hidden),
scan (flash_step r v) xs state =
{ scoreSum := state.scoreSum + Vector.sum fun i => r (xs i),
weighted := fun j => state.weighted j + Vector.sum fun i => r (xs i) * v (xs i) j }xs:Fin (n + 1) → αstate:FlashAcc hidden⊢ { scoreSum := (flash_step r v state (xs 0)).scoreSum + Vector.sum fun i => r (xs i.succ),
weighted := fun j =>
(flash_step r v state (xs 0)).weighted j + Vector.sum fun i => r (xs i.succ) * v (xs i.succ) j } =
{ scoreSum := state.scoreSum + Vector.sum fun i => r (xs i),
weighted := fun j => state.weighted j + Vector.sum fun i => r (xs i) * v (xs i) j }] succ α:Type uhidden:Natr:α → Ratv:α → Vector hiddenn:Natih:∀ (xs : Fin n → α) (state : FlashAcc hidden),
scan (flash_step r v) xs state =
{ scoreSum := state.scoreSum + Vector.sum fun i => r (xs i),
weighted := fun j => state.weighted j + Vector.sum fun i => r (xs i) * v (xs i) j }xs:Fin (n + 1) → αstate:FlashAcc hidden⊢ { scoreSum := (flash_step r v state (xs 0)).scoreSum + Vector.sum fun i => r (xs i.succ),
weighted := fun j =>
(flash_step r v state (xs 0)).weighted j + Vector.sum fun i => r (xs i.succ) * v (xs i.succ) j } =
{ scoreSum := state.scoreSum + Vector.sum fun i => r (xs i),
weighted := fun j => state.weighted j + Vector.sum fun i => r (xs i) * v (xs i) j }
simp [Vector.sum_succ, flash_step, Rat.add_assoc] All goals completed! 🐙theorem flash_attention_eq (tiles n : Nat)
(input : (Matrix (tiles * n) hidden × Matrix (tiles * n) hidden) ×
Matrix (tiles * n) hidden) :
flash_attention tiles n input = attention_layer input := by hidden:Nattiles:Natn:Natinput:(Matrix (tiles * n) hidden × Matrix (tiles * n) hidden) × Matrix (tiles * n) hidden⊢ flash_attention tiles n input = attention_layer input
rcases input with ⟨⟨q, k⟩, v⟩ hidden:Nattiles:Natn:Natv:Matrix (tiles * n) hiddenq:Matrix (tiles * n) hiddenk:Matrix (tiles * n) hidden⊢ flash_attention tiles n ((q, k), v) = attention_layer ((q, k), v)
funext s j hidden:Nattiles:Natn:Natv:Matrix (tiles * n) hiddenq:Matrix (tiles * n) hiddenk:Matrix (tiles * n) hiddens:Fin (tiles * n)j:Fin hidden⊢ flash_attention tiles n ((q, k), v) s j = attention_layer ((q, k), v) s j
simp only [flash_attention, tile_fold_eq, flash_fold,
Rat.zero_add] hidden:Nattiles:Natn:Natv:Matrix (tiles * n) hiddenq:Matrix (tiles * n) hiddenk:Matrix (tiles * n) hiddens:Fin (tiles * n)j:Fin hidden⊢ ((Vector.sum fun i => (1 + relu (Vector.sum fun d => q s d * k i d)) * v i j) /
Vector.sum fun t => 1 + relu (Vector.sum fun d => q s d * k t d)) =
attention_layer ((q, k), v) s j
let r := fun t => 1 + relu (Vector.sum (fun d => q s d * k t d)) hidden:Nattiles:Natn:Natv:Matrix (tiles * n) hiddenq:Matrix (tiles * n) hiddenk:Matrix (tiles * n) hiddens:Fin (tiles * n)j:Fin hiddenr:Fin (tiles * n) → Rat := fun t => 1 + relu (Vector.sum fun d => q s d * k t d)⊢ ((Vector.sum fun i => (1 + relu (Vector.sum fun d => q s d * k i d)) * v i j) /
Vector.sum fun t => 1 + relu (Vector.sum fun d => q s d * k t d)) =
attention_layer ((q, k), v) s j
change Vector.sum (fun t => r t * v t j) / Vector.sum r =
Vector.sum (fun t => (r t / Vector.sum r) * v t j) hidden:Nattiles:Natn:Natv:Matrix (tiles * n) hiddenq:Matrix (tiles * n) hiddenk:Matrix (tiles * n) hiddens:Fin (tiles * n)j:Fin hiddenr:Fin (tiles * n) → Rat := fun t => 1 + relu (Vector.sum fun d => q s d * k t d)⊢ (Vector.sum fun t => r t * v t j) / Vector.sum r = Vector.sum fun t => r t / Vector.sum r * v t j
simp only [Rat.div_def, Vector.sum_mul] hidden:Nattiles:Natn:Natv:Matrix (tiles * n) hiddenq:Matrix (tiles * n) hiddenk:Matrix (tiles * n) hiddens:Fin (tiles * n)j:Fin hiddenr:Fin (tiles * n) → Rat := fun t => 1 + relu (Vector.sum fun d => q s d * k t d)⊢ (Vector.sum fun i => r i * v i j * (Vector.sum r)⁻¹) = Vector.sum fun t => r t * (Vector.sum r)⁻¹ * v t j
apply congrArg Vector.sum hidden:Nattiles:Natn:Natv:Matrix (tiles * n) hiddenq:Matrix (tiles * n) hiddenk:Matrix (tiles * n) hiddens:Fin (tiles * n)j:Fin hiddenr:Fin (tiles * n) → Rat := fun t => 1 + relu (Vector.sum fun d => q s d * k t d)⊢ (fun i => r i * v i j * (Vector.sum r)⁻¹) = fun t => r t * (Vector.sum r)⁻¹ * v t j
funext t hidden:Nattiles:Natn:Natv:Matrix (tiles * n) hiddenq:Matrix (tiles * n) hiddenk:Matrix (tiles * n) hiddens:Fin (tiles * n)j:Fin hiddenr:Fin (tiles * n) → Rat := fun t => 1 + relu (Vector.sum fun d => q s d * k t d)t:Fin (tiles * n)⊢ r t * v t j * (Vector.sum r)⁻¹ = r t * (Vector.sum r)⁻¹ * v t j
grind All goals completed! 🐙State Space Models
Another alternative to standard attention is to use a state space model or linear attention approach. These can be defined by the following recurrence.
def ssm_scan (a : Rat) (values : Vector n) (initial : Rat := 0) : Rat :=
scan (fun state x => a * state + x) values initialdef ssm_state (a : Rat) (updates : Fin n → Matrix hidden hidden)
(incoming : Matrix hidden hidden := fun _ _ => 0) : Matrix hidden hidden :=
fun d j => ssm_scan a (fun t => updates t d j) (incoming d j)def ssm_layer (a : Rat) : Mixer seq hidden :=
fun input =>
let ⟨⟨q, k⟩, v⟩ := input
let updates := fun t => (fun d j => k t d * v t j : Matrix hidden hidden)
fun s =>
(ssm_state a (slice 0 (s.val + 1) (by α:Type uβ:Type vδ:Type wseq:Nathidden:Nata:Ratinput:(Matrix seq hidden × Matrix seq hidden) × Matrix seq hiddenq:Matrix seq hiddenk:Matrix seq hiddenv:Matrix seq hiddenupdates:Fin seq → Fin hidden → Fin hidden → Rat := fun t d j => k t d * v t js:Fin seq⊢ 0 + (↑s + 1) ≤ seq omega All goals completed! 🐙) updates)).transpose.matvec (q s)
-- By induction, show how each term is weighted.
theorem scan_weighted (a : Rat) (values : Vector n) (z : Rat) :
ssm_scan a values z =
a ^ n * z + Vector.sum (fun t => a ^ (n - 1 - t.val) * values t) := by n:Nata:Ratvalues:Vector nz:Rat⊢ ssm_scan a values z = a ^ n * z + Vector.sum fun t => a ^ (n - 1 - ↑t) * values t
induction n generalizing z with
| zero => zero a:Ratvalues:Vector 0z:Rat⊢ ssm_scan a values z = a ^ 0 * z + Vector.sum fun t => a ^ (0 - 1 - ↑t) * values t simp [ssm_scan, scan, Vector.sum, fori] All goals completed! 🐙
| succ n ih => succ a:Ratn:Natih:∀ (values : Vector n) (z : Rat), ssm_scan a values z = a ^ n * z + Vector.sum fun t => a ^ (n - 1 - ↑t) * values tvalues:Vector (n + 1)z:Rat⊢ ssm_scan a values z = a ^ (n + 1) * z + Vector.sum fun t => a ^ (n + 1 - 1 - ↑t) * values t
simp only [ssm_scan, scan, Fin.foldl_succ] succ a:Ratn:Natih:∀ (values : Vector n) (z : Rat), ssm_scan a values z = a ^ n * z + Vector.sum fun t => a ^ (n - 1 - ↑t) * values tvalues:Vector (n + 1)z:Rat⊢ Fin.foldl n (fun x i => a * x + values i.succ) (a * z + values 0) =
a ^ (n + 1) * z + Vector.sum fun t => a ^ (n + 1 - 1 - ↑t) * values t
change ssm_scan a (fun i : Fin n => values i.succ) (a * z + values 0) = _ succ a:Ratn:Natih:∀ (values : Vector n) (z : Rat), ssm_scan a values z = a ^ n * z + Vector.sum fun t => a ^ (n - 1 - ↑t) * values tvalues:Vector (n + 1)z:Rat⊢ ssm_scan a (fun i => values i.succ) (a * z + values 0) =
a ^ (n + 1) * z + Vector.sum fun t => a ^ (n + 1 - 1 - ↑t) * values t
rw [ih, succ a:Ratn:Natih:∀ (values : Vector n) (z : Rat), ssm_scan a values z = a ^ n * z + Vector.sum fun t => a ^ (n - 1 - ↑t) * values tvalues:Vector (n + 1)z:Rat⊢ (a ^ n * (a * z + values 0) + Vector.sum fun t => a ^ (n - 1 - ↑t) * values t.succ) =
a ^ (n + 1) * z + Vector.sum fun t => a ^ (n + 1 - 1 - ↑t) * values t succ a:Ratn:Natih:∀ (values : Vector n) (z : Rat), ssm_scan a values z = a ^ n * z + Vector.sum fun t => a ^ (n - 1 - ↑t) * values tvalues:Vector (n + 1)z:Rat⊢ (a ^ n * (a * z + values 0) + Vector.sum fun t => a ^ (n - 1 - ↑t) * values t.succ) =
a ^ (n + 1) * z + (a ^ (n + 1 - 1 - ↑0) * values 0 + Vector.sum fun i => a ^ (n + 1 - 1 - ↑i.succ) * values i.succ) Vector.sum_succ succ a:Ratn:Natih:∀ (values : Vector n) (z : Rat), ssm_scan a values z = a ^ n * z + Vector.sum fun t => a ^ (n - 1 - ↑t) * values tvalues:Vector (n + 1)z:Rat⊢ (a ^ n * (a * z + values 0) + Vector.sum fun t => a ^ (n - 1 - ↑t) * values t.succ) =
a ^ (n + 1) * z + (a ^ (n + 1 - 1 - ↑0) * values 0 + Vector.sum fun i => a ^ (n + 1 - 1 - ↑i.succ) * values i.succ) succ a:Ratn:Natih:∀ (values : Vector n) (z : Rat), ssm_scan a values z = a ^ n * z + Vector.sum fun t => a ^ (n - 1 - ↑t) * values tvalues:Vector (n + 1)z:Rat⊢ (a ^ n * (a * z + values 0) + Vector.sum fun t => a ^ (n - 1 - ↑t) * values t.succ) =
a ^ (n + 1) * z + (a ^ (n + 1 - 1 - ↑0) * values 0 + Vector.sum fun i => a ^ (n + 1 - 1 - ↑i.succ) * values i.succ)]succ a:Ratn:Natih:∀ (values : Vector n) (z : Rat), ssm_scan a values z = a ^ n * z + Vector.sum fun t => a ^ (n - 1 - ↑t) * values tvalues:Vector (n + 1)z:Rat⊢ (a ^ n * (a * z + values 0) + Vector.sum fun t => a ^ (n - 1 - ↑t) * values t.succ) =
a ^ (n + 1) * z + (a ^ (n + 1 - 1 - ↑0) * values 0 + Vector.sum fun i => a ^ (n + 1 - 1 - ↑i.succ) * values i.succ)
simp [Nat.sub_sub, Nat.add_comm, Rat.pow_succ,
Rat.mul_add, Rat.mul_assoc, Rat.add_assoc] All goals completed! 🐙
Unlike vanilla attention, state space models clearly induce an ordering on
the sequence in the multiplicative term a. However, in the special case
where that term is 1 and we use a bidirectional SSM, we can show that the
permutation equivariance remains.
def bidirectional_ssm_scan (a : Rat) (values : Vector n) : Vector n :=
fun s =>
let suffix := slice s.val (n - s.val) (by α:Type uβ:Type vδ:Type wn:Nata:Ratvalues:Vector ns:Fin n⊢ ↑s + (n - ↑s) ≤ n omega All goals completed! 🐙) values
ssm_scan a (slice 0 (s.val + 1) (by α:Type uβ:Type vδ:Type wn:Nata:Ratvalues:Vector ns:Fin nsuffix:Fin (n - ↑s) → Rat := slice (↑s) (n - ↑s) ⋯ values⊢ 0 + (↑s + 1) ≤ n omega All goals completed! 🐙) values) +
ssm_scan a (fun t => suffix t.rev)def bidirectional_ssm_layer (α : Rat) : Mixer seq hidden :=
fun input =>
let ⟨⟨q, k⟩, v⟩ := input
fun s =>
let state : Matrix hidden hidden := fun d j =>
bidirectional_ssm_scan α (fun t => k t d * v t j) s
state.transpose.matvec (q s)
theorem bidirectional_scan_one (f : Vector n) (s : Fin n) :
bidirectional_ssm_scan 1 f s = f.sum + f s := by n:Natf:Vector ns:Fin n⊢ bidirectional_ssm_scan 1 f s = f.sum + f s
dsimp [Vector] at f n:Natf:Fin n → Rats:Fin n⊢ bidirectional_ssm_scan 1 f s = f.sum + f s
have one_pow (m : Nat) : (1 : Rat) ^ m = 1 := by n:Natf:Vector ns:Fin n⊢ bidirectional_ssm_scan 1 f s = f.sum + f s n:Natf:Fin n → Rats:Fin none_pow:∀ (m : Nat), 1 ^ m = 1⊢ bidirectional_ssm_scan 1 f s = Vector.sum f + f s
induction m zero n:Natf:Fin n → Rats:Fin n⊢ 1 ^ 0 = 1succ n:Natf:Fin n → Rats:Fin nn✝:Nata✝:1 ^ n✝ = 1⊢ 1 ^ (n✝ + 1) = 1 n:Natf:Fin n → Rats:Fin none_pow:∀ (m : Nat), 1 ^ m = 1⊢ bidirectional_ssm_scan 1 f s = Vector.sum f + f s <;> zero n:Natf:Fin n → Rats:Fin n⊢ 1 ^ 0 = 1succ n:Natf:Fin n → Rats:Fin nn✝:Nata✝:1 ^ n✝ = 1⊢ 1 ^ (n✝ + 1) = 1 n:Natf:Fin n → Rats:Fin none_pow:∀ (m : Nat), 1 ^ m = 1⊢ bidirectional_ssm_scan 1 f s = Vector.sum f + f s simp_all [Rat.pow_succ] n:Natf:Fin n → Rats:Fin none_pow:∀ (m : Nat), 1 ^ m = 1⊢ bidirectional_ssm_scan 1 f s = Vector.sum f + f s n:Natf:Fin n → Rats:Fin none_pow:∀ (m : Nat), 1 ^ m = 1⊢ bidirectional_ssm_scan 1 f s = Vector.sum f + f s
simp only [bidirectional_ssm_scan, scan_weighted, one_pow,
Rat.one_mul, Rat.mul_zero, Rat.zero_add] n:Natf:Fin n → Rats:Fin none_pow:∀ (m : Nat), 1 ^ m = 1⊢ ((Vector.sum fun t => slice 0 (↑s + 1) ⋯ f t) + Vector.sum fun t => slice (↑s) (n - ↑s) ⋯ f t.rev) = Vector.sum f + f s
rw [Vector.sum_rev n:Natf:Fin n → Rats:Fin none_pow:∀ (m : Nat), 1 ^ m = 1⊢ (Vector.sum fun t => slice 0 (↑s + 1) ⋯ f t) + Vector.sum (slice (↑s) (n - ↑s) ⋯ f) = Vector.sum f + f s n:Natf:Fin n → Rats:Fin none_pow:∀ (m : Nat), 1 ^ m = 1⊢ (Vector.sum fun t => slice 0 (↑s + 1) ⋯ f t) + Vector.sum (slice (↑s) (n - ↑s) ⋯ f) = Vector.sum f + f s] n:Natf:Fin n → Rats:Fin none_pow:∀ (m : Nat), 1 ^ m = 1⊢ (Vector.sum fun t => slice 0 (↑s + 1) ⋯ f t) + Vector.sum (slice (↑s) (n - ↑s) ⋯ f) = Vector.sum f + f s
rw [show Vector.sum (slice 0 (s.val + 1) (by n:Natf:Fin n → Rats:Fin none_pow:∀ (m : Nat), 1 ^ m = 1⊢ 0 + (↑s + 1) ≤ n n:Natf:Fin n → Rats:Fin none_pow:∀ (m : Nat), 1 ^ m = 1⊢ Vector.sum (slice 0 ↑s ⋯ f) + f s + Vector.sum (slice (↑s) (n - ↑s) ⋯ f) = Vector.sum f + f s omega All goals completed! 🐙 n:Natf:Fin n → Rats:Fin none_pow:∀ (m : Nat), 1 ^ m = 1⊢ Vector.sum (slice 0 ↑s ⋯ f) + f s + Vector.sum (slice (↑s) (n - ↑s) ⋯ f) = Vector.sum f + f s) f) =
Vector.sum (slice 0 s.val (by n:Natf:Fin n → Rats:Fin none_pow:∀ (m : Nat), 1 ^ m = 1⊢ 0 + ↑s ≤ n n:Natf:Fin n → Rats:Fin none_pow:∀ (m : Nat), 1 ^ m = 1⊢ Vector.sum (slice 0 ↑s ⋯ f) + f s + Vector.sum (slice (↑s) (n - ↑s) ⋯ f) = Vector.sum f + f s omega All goals completed! 🐙 n:Natf:Fin n → Rats:Fin none_pow:∀ (m : Nat), 1 ^ m = 1⊢ Vector.sum (slice 0 ↑s ⋯ f) + f s + Vector.sum (slice (↑s) (n - ↑s) ⋯ f) = Vector.sum f + f s) f) + f s by n:Natf:Vector ns:Fin n⊢ bidirectional_ssm_scan 1 f s = f.sum + f s n:Natf:Fin n → Rats:Fin none_pow:∀ (m : Nat), 1 ^ m = 1⊢ Vector.sum (slice 0 ↑s ⋯ f) + f s + Vector.sum (slice (↑s) (n - ↑s) ⋯ f) = Vector.sum f + f s
exact Vector.sum_last (slice 0 (s.val + 1) (by n:Natf:Fin n → Rats:Fin none_pow:∀ (m : Nat), 1 ^ m = 1⊢ 0 + (↑s + 1) ≤ n n:Natf:Fin n → Rats:Fin none_pow:∀ (m : Nat), 1 ^ m = 1⊢ Vector.sum (slice 0 ↑s ⋯ f) + f s + Vector.sum (slice (↑s) (n - ↑s) ⋯ f) = Vector.sum f + f s omega All goals completed! 🐙 n:Natf:Fin n → Rats:Fin none_pow:∀ (m : Nat), 1 ^ m = 1⊢ Vector.sum (slice 0 ↑s ⋯ f) + f s + Vector.sum (slice (↑s) (n - ↑s) ⋯ f) = Vector.sum f + f s) f)] n:Natf:Fin n → Rats:Fin none_pow:∀ (m : Nat), 1 ^ m = 1⊢ Vector.sum (slice 0 ↑s ⋯ f) + f s + Vector.sum (slice (↑s) (n - ↑s) ⋯ f) = Vector.sum f + f s
have split := Vector.sum_split
(values := fun t : Fin (s.val + (n - s.val)) => f ⟨t.val, by n:Natf:Fin n → Rats:Fin none_pow:∀ (m : Nat), 1 ^ m = 1t:Fin (↑s + (n - ↑s))⊢ ↑t < n n:Natf:Fin n → Rats:Fin none_pow:∀ (m : Nat), 1 ^ m = 1split:Vector.sum (select (fun i => Fin.castAdd (n - ↑s) i) fun t => f ⟨↑t, ⋯⟩) +
Vector.sum (select (fun i => Fin.natAdd (↑s) i) fun t => f ⟨↑t, ⋯⟩) =
Vector.sum fun t => f ⟨↑t, ⋯⟩⊢ Vector.sum (slice 0 ↑s ⋯ f) + f s + Vector.sum (slice (↑s) (n - ↑s) ⋯ f) = Vector.sum f + f s omega All goals completed! 🐙 n:Natf:Fin n → Rats:Fin none_pow:∀ (m : Nat), 1 ^ m = 1split:Vector.sum (select (fun i => Fin.castAdd (n - ↑s) i) fun t => f ⟨↑t, ⋯⟩) +
Vector.sum (select (fun i => Fin.natAdd (↑s) i) fun t => f ⟨↑t, ⋯⟩) =
Vector.sum fun t => f ⟨↑t, ⋯⟩⊢ Vector.sum (slice 0 ↑s ⋯ f) + f s + Vector.sum (slice (↑s) (n - ↑s) ⋯ f) = Vector.sum f + f s⟩) n:Natf:Fin n → Rats:Fin none_pow:∀ (m : Nat), 1 ^ m = 1split:Vector.sum (select (fun i => Fin.castAdd (n - ↑s) i) fun t => f ⟨↑t, ⋯⟩) +
Vector.sum (select (fun i => Fin.natAdd (↑s) i) fun t => f ⟨↑t, ⋯⟩) =
Vector.sum fun t => f ⟨↑t, ⋯⟩⊢ Vector.sum (slice 0 ↑s ⋯ f) + f s + Vector.sum (slice (↑s) (n - ↑s) ⋯ f) = Vector.sum f + f s
have total :
Vector.sum (fun t : Fin (s.val + (n - s.val)) => f ⟨t.val, by n:Natf:Fin n → Rats:Fin none_pow:∀ (m : Nat), 1 ^ m = 1split:Vector.sum (select (fun i => Fin.castAdd (n - ↑s) i) fun t => f ⟨↑t, ⋯⟩) +
Vector.sum (select (fun i => Fin.natAdd (↑s) i) fun t => f ⟨↑t, ⋯⟩) =
Vector.sum fun t => f ⟨↑t, ⋯⟩t:Fin (↑s + (n - ↑s))⊢ ↑t < n n:Natf:Fin n → Rats:Fin none_pow:∀ (m : Nat), 1 ^ m = 1split:Vector.sum (select (fun i => Fin.castAdd (n - ↑s) i) fun t => f ⟨↑t, ⋯⟩) +
Vector.sum (select (fun i => Fin.natAdd (↑s) i) fun t => f ⟨↑t, ⋯⟩) =
Vector.sum fun t => f ⟨↑t, ⋯⟩total:(Vector.sum fun t => f ⟨↑t, ⋯⟩) = Vector.sum f⊢ Vector.sum (slice 0 ↑s ⋯ f) + f s + Vector.sum (slice (↑s) (n - ↑s) ⋯ f) = Vector.sum f + f s omega All goals completed! 🐙 n:Natf:Fin n → Rats:Fin none_pow:∀ (m : Nat), 1 ^ m = 1split:Vector.sum (select (fun i => Fin.castAdd (n - ↑s) i) fun t => f ⟨↑t, ⋯⟩) +
Vector.sum (select (fun i => Fin.natAdd (↑s) i) fun t => f ⟨↑t, ⋯⟩) =
Vector.sum fun t => f ⟨↑t, ⋯⟩total:(Vector.sum fun t => f ⟨↑t, ⋯⟩) = Vector.sum f⊢ Vector.sum (slice 0 ↑s ⋯ f) + f s + Vector.sum (slice (↑s) (n - ↑s) ⋯ f) = Vector.sum f + f s⟩) =
Vector.sum f := by n:Natf:Vector ns:Fin n⊢ bidirectional_ssm_scan 1 f s = f.sum + f s n:Natf:Fin n → Rats:Fin none_pow:∀ (m : Nat), 1 ^ m = 1split:Vector.sum (select (fun i => Fin.castAdd (n - ↑s) i) fun t => f ⟨↑t, ⋯⟩) +
Vector.sum (select (fun i => Fin.natAdd (↑s) i) fun t => f ⟨↑t, ⋯⟩) =
Vector.sum fun t => f ⟨↑t, ⋯⟩total:(Vector.sum fun t => f ⟨↑t, ⋯⟩) = Vector.sum f⊢ Vector.sum (slice 0 ↑s ⋯ f) + f s + Vector.sum (slice (↑s) (n - ↑s) ⋯ f) = Vector.sum f + f s
have full (m : Nat) (h : m = n) :
Vector.sum (slice 0 m (by n:Natf:Fin n → Rats:Fin none_pow:∀ (m : Nat), 1 ^ m = 1split:Vector.sum (select (fun i => Fin.castAdd (n - ↑s) i) fun t => f ⟨↑t, ⋯⟩) +
Vector.sum (select (fun i => Fin.natAdd (↑s) i) fun t => f ⟨↑t, ⋯⟩) =
Vector.sum fun t => f ⟨↑t, ⋯⟩m:Nath:m = n⊢ 0 + m ≤ n n:Natf:Fin n → Rats:Fin none_pow:∀ (m : Nat), 1 ^ m = 1split:Vector.sum (select (fun i => Fin.castAdd (n - ↑s) i) fun t => f ⟨↑t, ⋯⟩) +
Vector.sum (select (fun i => Fin.natAdd (↑s) i) fun t => f ⟨↑t, ⋯⟩) =
Vector.sum fun t => f ⟨↑t, ⋯⟩full:∀ (m : Nat) (h : m = n), Vector.sum (slice 0 m ⋯ f) = Vector.sum f⊢ (Vector.sum fun t => f ⟨↑t, ⋯⟩) = Vector.sum f n:Natf:Fin n → Rats:Fin none_pow:∀ (m : Nat), 1 ^ m = 1split:Vector.sum (select (fun i => Fin.castAdd (n - ↑s) i) fun t => f ⟨↑t, ⋯⟩) +
Vector.sum (select (fun i => Fin.natAdd (↑s) i) fun t => f ⟨↑t, ⋯⟩) =
Vector.sum fun t => f ⟨↑t, ⋯⟩total:(Vector.sum fun t => f ⟨↑t, ⋯⟩) = Vector.sum f⊢ Vector.sum (slice 0 ↑s ⋯ f) + f s + Vector.sum (slice (↑s) (n - ↑s) ⋯ f) = Vector.sum f + f s omega All goals completed! 🐙 n:Natf:Fin n → Rats:Fin none_pow:∀ (m : Nat), 1 ^ m = 1split:Vector.sum (select (fun i => Fin.castAdd (n - ↑s) i) fun t => f ⟨↑t, ⋯⟩) +
Vector.sum (select (fun i => Fin.natAdd (↑s) i) fun t => f ⟨↑t, ⋯⟩) =
Vector.sum fun t => f ⟨↑t, ⋯⟩full:∀ (m : Nat) (h : m = n), Vector.sum (slice 0 m ⋯ f) = Vector.sum f⊢ (Vector.sum fun t => f ⟨↑t, ⋯⟩) = Vector.sum f n:Natf:Fin n → Rats:Fin none_pow:∀ (m : Nat), 1 ^ m = 1split:Vector.sum (select (fun i => Fin.castAdd (n - ↑s) i) fun t => f ⟨↑t, ⋯⟩) +
Vector.sum (select (fun i => Fin.natAdd (↑s) i) fun t => f ⟨↑t, ⋯⟩) =
Vector.sum fun t => f ⟨↑t, ⋯⟩total:(Vector.sum fun t => f ⟨↑t, ⋯⟩) = Vector.sum f⊢ Vector.sum (slice 0 ↑s ⋯ f) + f s + Vector.sum (slice (↑s) (n - ↑s) ⋯ f) = Vector.sum f + f s) f) = Vector.sum f := by n:Natf:Vector ns:Fin n⊢ bidirectional_ssm_scan 1 f s = f.sum + f s n:Natf:Fin n → Rats:Fin none_pow:∀ (m : Nat), 1 ^ m = 1split:Vector.sum (select (fun i => Fin.castAdd (n - ↑s) i) fun t => f ⟨↑t, ⋯⟩) +
Vector.sum (select (fun i => Fin.natAdd (↑s) i) fun t => f ⟨↑t, ⋯⟩) =
Vector.sum fun t => f ⟨↑t, ⋯⟩full:∀ (m : Nat) (h : m = n), Vector.sum (slice 0 m ⋯ f) = Vector.sum f⊢ (Vector.sum fun t => f ⟨↑t, ⋯⟩) = Vector.sum f n:Natf:Fin n → Rats:Fin none_pow:∀ (m : Nat), 1 ^ m = 1split:Vector.sum (select (fun i => Fin.castAdd (n - ↑s) i) fun t => f ⟨↑t, ⋯⟩) +
Vector.sum (select (fun i => Fin.natAdd (↑s) i) fun t => f ⟨↑t, ⋯⟩) =
Vector.sum fun t => f ⟨↑t, ⋯⟩total:(Vector.sum fun t => f ⟨↑t, ⋯⟩) = Vector.sum f⊢ Vector.sum (slice 0 ↑s ⋯ f) + f s + Vector.sum (slice (↑s) (n - ↑s) ⋯ f) = Vector.sum f + f s
subst m n:Natf:Fin n → Rats:Fin none_pow:∀ (m : Nat), 1 ^ m = 1split:Vector.sum (select (fun i => Fin.castAdd (n - ↑s) i) fun t => f ⟨↑t, ⋯⟩) +
Vector.sum (select (fun i => Fin.natAdd (↑s) i) fun t => f ⟨↑t, ⋯⟩) =
Vector.sum fun t => f ⟨↑t, ⋯⟩⊢ Vector.sum (slice 0 n ⋯ f) = Vector.sum f n:Natf:Fin n → Rats:Fin none_pow:∀ (m : Nat), 1 ^ m = 1split:Vector.sum (select (fun i => Fin.castAdd (n - ↑s) i) fun t => f ⟨↑t, ⋯⟩) +
Vector.sum (select (fun i => Fin.natAdd (↑s) i) fun t => f ⟨↑t, ⋯⟩) =
Vector.sum fun t => f ⟨↑t, ⋯⟩full:∀ (m : Nat) (h : m = n), Vector.sum (slice 0 m ⋯ f) = Vector.sum f⊢ (Vector.sum fun t => f ⟨↑t, ⋯⟩) = Vector.sum f n:Natf:Fin n → Rats:Fin none_pow:∀ (m : Nat), 1 ^ m = 1split:Vector.sum (select (fun i => Fin.castAdd (n - ↑s) i) fun t => f ⟨↑t, ⋯⟩) +
Vector.sum (select (fun i => Fin.natAdd (↑s) i) fun t => f ⟨↑t, ⋯⟩) =
Vector.sum fun t => f ⟨↑t, ⋯⟩total:(Vector.sum fun t => f ⟨↑t, ⋯⟩) = Vector.sum f⊢ Vector.sum (slice 0 ↑s ⋯ f) + f s + Vector.sum (slice (↑s) (n - ↑s) ⋯ f) = Vector.sum f + f s
rfl n:Natf:Fin n → Rats:Fin none_pow:∀ (m : Nat), 1 ^ m = 1split:Vector.sum (select (fun i => Fin.castAdd (n - ↑s) i) fun t => f ⟨↑t, ⋯⟩) +
Vector.sum (select (fun i => Fin.natAdd (↑s) i) fun t => f ⟨↑t, ⋯⟩) =
Vector.sum fun t => f ⟨↑t, ⋯⟩full:∀ (m : Nat) (h : m = n), Vector.sum (slice 0 m ⋯ f) = Vector.sum f⊢ (Vector.sum fun t => f ⟨↑t, ⋯⟩) = Vector.sum f n:Natf:Fin n → Rats:Fin none_pow:∀ (m : Nat), 1 ^ m = 1split:Vector.sum (select (fun i => Fin.castAdd (n - ↑s) i) fun t => f ⟨↑t, ⋯⟩) +
Vector.sum (select (fun i => Fin.natAdd (↑s) i) fun t => f ⟨↑t, ⋯⟩) =
Vector.sum fun t => f ⟨↑t, ⋯⟩total:(Vector.sum fun t => f ⟨↑t, ⋯⟩) = Vector.sum f⊢ Vector.sum (slice 0 ↑s ⋯ f) + f s + Vector.sum (slice (↑s) (n - ↑s) ⋯ f) = Vector.sum f + f s n:Natf:Fin n → Rats:Fin none_pow:∀ (m : Nat), 1 ^ m = 1split:Vector.sum (select (fun i => Fin.castAdd (n - ↑s) i) fun t => f ⟨↑t, ⋯⟩) +
Vector.sum (select (fun i => Fin.natAdd (↑s) i) fun t => f ⟨↑t, ⋯⟩) =
Vector.sum fun t => f ⟨↑t, ⋯⟩full:∀ (m : Nat) (h : m = n), Vector.sum (slice 0 m ⋯ f) = Vector.sum f⊢ (Vector.sum fun t => f ⟨↑t, ⋯⟩) = Vector.sum f n:Natf:Fin n → Rats:Fin none_pow:∀ (m : Nat), 1 ^ m = 1split:Vector.sum (select (fun i => Fin.castAdd (n - ↑s) i) fun t => f ⟨↑t, ⋯⟩) +
Vector.sum (select (fun i => Fin.natAdd (↑s) i) fun t => f ⟨↑t, ⋯⟩) =
Vector.sum fun t => f ⟨↑t, ⋯⟩total:(Vector.sum fun t => f ⟨↑t, ⋯⟩) = Vector.sum f⊢ Vector.sum (slice 0 ↑s ⋯ f) + f s + Vector.sum (slice (↑s) (n - ↑s) ⋯ f) = Vector.sum f + f s
exact full _ (by n:Natf:Fin n → Rats:Fin none_pow:∀ (m : Nat), 1 ^ m = 1split:Vector.sum (select (fun i => Fin.castAdd (n - ↑s) i) fun t => f ⟨↑t, ⋯⟩) +
Vector.sum (select (fun i => Fin.natAdd (↑s) i) fun t => f ⟨↑t, ⋯⟩) =
Vector.sum fun t => f ⟨↑t, ⋯⟩full:∀ (m : Nat) (h : m = n), Vector.sum (slice 0 m ⋯ f) = Vector.sum f⊢ ↑s + (n - ↑s) = n n:Natf:Fin n → Rats:Fin none_pow:∀ (m : Nat), 1 ^ m = 1split:Vector.sum (select (fun i => Fin.castAdd (n - ↑s) i) fun t => f ⟨↑t, ⋯⟩) +
Vector.sum (select (fun i => Fin.natAdd (↑s) i) fun t => f ⟨↑t, ⋯⟩) =
Vector.sum fun t => f ⟨↑t, ⋯⟩total:(Vector.sum fun t => f ⟨↑t, ⋯⟩) = Vector.sum f⊢ Vector.sum (slice 0 ↑s ⋯ f) + f s + Vector.sum (slice (↑s) (n - ↑s) ⋯ f) = Vector.sum f + f s omega All goals completed! 🐙 n:Natf:Fin n → Rats:Fin none_pow:∀ (m : Nat), 1 ^ m = 1split:Vector.sum (select (fun i => Fin.castAdd (n - ↑s) i) fun t => f ⟨↑t, ⋯⟩) +
Vector.sum (select (fun i => Fin.natAdd (↑s) i) fun t => f ⟨↑t, ⋯⟩) =
Vector.sum fun t => f ⟨↑t, ⋯⟩total:(Vector.sum fun t => f ⟨↑t, ⋯⟩) = Vector.sum f⊢ Vector.sum (slice 0 ↑s ⋯ f) + f s + Vector.sum (slice (↑s) (n - ↑s) ⋯ f) = Vector.sum f + f s) n:Natf:Fin n → Rats:Fin none_pow:∀ (m : Nat), 1 ^ m = 1split:Vector.sum (select (fun i => Fin.castAdd (n - ↑s) i) fun t => f ⟨↑t, ⋯⟩) +
Vector.sum (select (fun i => Fin.natAdd (↑s) i) fun t => f ⟨↑t, ⋯⟩) =
Vector.sum fun t => f ⟨↑t, ⋯⟩total:(Vector.sum fun t => f ⟨↑t, ⋯⟩) = Vector.sum f⊢ Vector.sum (slice 0 ↑s ⋯ f) + f s + Vector.sum (slice (↑s) (n - ↑s) ⋯ f) = Vector.sum f + f s
rw [total n:Natf:Fin n → Rats:Fin none_pow:∀ (m : Nat), 1 ^ m = 1split:Vector.sum (select (fun i => Fin.castAdd (n - ↑s) i) fun t => f ⟨↑t, ⋯⟩) +
Vector.sum (select (fun i => Fin.natAdd (↑s) i) fun t => f ⟨↑t, ⋯⟩) =
Vector.sum ftotal:(Vector.sum fun t => f ⟨↑t, ⋯⟩) = Vector.sum f⊢ Vector.sum (slice 0 ↑s ⋯ f) + f s + Vector.sum (slice (↑s) (n - ↑s) ⋯ f) = Vector.sum f + f s n:Natf:Fin n → Rats:Fin none_pow:∀ (m : Nat), 1 ^ m = 1split:Vector.sum (select (fun i => Fin.castAdd (n - ↑s) i) fun t => f ⟨↑t, ⋯⟩) +
Vector.sum (select (fun i => Fin.natAdd (↑s) i) fun t => f ⟨↑t, ⋯⟩) =
Vector.sum ftotal:(Vector.sum fun t => f ⟨↑t, ⋯⟩) = Vector.sum f⊢ Vector.sum (slice 0 ↑s ⋯ f) + f s + Vector.sum (slice (↑s) (n - ↑s) ⋯ f) = Vector.sum f + f s] at split n:Natf:Fin n → Rats:Fin none_pow:∀ (m : Nat), 1 ^ m = 1split:Vector.sum (select (fun i => Fin.castAdd (n - ↑s) i) fun t => f ⟨↑t, ⋯⟩) +
Vector.sum (select (fun i => Fin.natAdd (↑s) i) fun t => f ⟨↑t, ⋯⟩) =
Vector.sum ftotal:(Vector.sum fun t => f ⟨↑t, ⋯⟩) = Vector.sum f⊢ Vector.sum (slice 0 ↑s ⋯ f) + f s + Vector.sum (slice (↑s) (n - ↑s) ⋯ f) = Vector.sum f + f s
have split' : Vector.sum (slice 0 s.val (by n:Natf:Fin n → Rats:Fin none_pow:∀ (m : Nat), 1 ^ m = 1split:Vector.sum (select (fun i => Fin.castAdd (n - ↑s) i) fun t => f ⟨↑t, ⋯⟩) +
Vector.sum (select (fun i => Fin.natAdd (↑s) i) fun t => f ⟨↑t, ⋯⟩) =
Vector.sum ftotal:(Vector.sum fun t => f ⟨↑t, ⋯⟩) = Vector.sum f⊢ 0 + ↑s ≤ n n:Natf:Fin n → Rats:Fin none_pow:∀ (m : Nat), 1 ^ m = 1split:Vector.sum (select (fun i => Fin.castAdd (n - ↑s) i) fun t => f ⟨↑t, ⋯⟩) +
Vector.sum (select (fun i => Fin.natAdd (↑s) i) fun t => f ⟨↑t, ⋯⟩) =
Vector.sum ftotal:(Vector.sum fun t => f ⟨↑t, ⋯⟩) = Vector.sum fsplit':Vector.sum (slice 0 ↑s ⋯ f) + Vector.sum (slice (↑s) (n - ↑s) ⋯ f) = Vector.sum f⊢ Vector.sum (slice 0 ↑s ⋯ f) + f s + Vector.sum (slice (↑s) (n - ↑s) ⋯ f) = Vector.sum f + f s omega All goals completed! 🐙 n:Natf:Fin n → Rats:Fin none_pow:∀ (m : Nat), 1 ^ m = 1split:Vector.sum (select (fun i => Fin.castAdd (n - ↑s) i) fun t => f ⟨↑t, ⋯⟩) +
Vector.sum (select (fun i => Fin.natAdd (↑s) i) fun t => f ⟨↑t, ⋯⟩) =
Vector.sum ftotal:(Vector.sum fun t => f ⟨↑t, ⋯⟩) = Vector.sum fsplit':Vector.sum (slice 0 ↑s ⋯ f) + Vector.sum (slice (↑s) (n - ↑s) ⋯ f) = Vector.sum f⊢ Vector.sum (slice 0 ↑s ⋯ f) + f s + Vector.sum (slice (↑s) (n - ↑s) ⋯ f) = Vector.sum f + f s) f) +
Vector.sum (slice s.val (n - s.val) (by n:Natf:Fin n → Rats:Fin none_pow:∀ (m : Nat), 1 ^ m = 1split:Vector.sum (select (fun i => Fin.castAdd (n - ↑s) i) fun t => f ⟨↑t, ⋯⟩) +
Vector.sum (select (fun i => Fin.natAdd (↑s) i) fun t => f ⟨↑t, ⋯⟩) =
Vector.sum ftotal:(Vector.sum fun t => f ⟨↑t, ⋯⟩) = Vector.sum f⊢ ↑s + (n - ↑s) ≤ n n:Natf:Fin n → Rats:Fin none_pow:∀ (m : Nat), 1 ^ m = 1split:Vector.sum (select (fun i => Fin.castAdd (n - ↑s) i) fun t => f ⟨↑t, ⋯⟩) +
Vector.sum (select (fun i => Fin.natAdd (↑s) i) fun t => f ⟨↑t, ⋯⟩) =
Vector.sum ftotal:(Vector.sum fun t => f ⟨↑t, ⋯⟩) = Vector.sum fsplit':Vector.sum (slice 0 ↑s ⋯ f) + Vector.sum (slice (↑s) (n - ↑s) ⋯ f) = Vector.sum f⊢ Vector.sum (slice 0 ↑s ⋯ f) + f s + Vector.sum (slice (↑s) (n - ↑s) ⋯ f) = Vector.sum f + f s omega All goals completed! 🐙 n:Natf:Fin n → Rats:Fin none_pow:∀ (m : Nat), 1 ^ m = 1split:Vector.sum (select (fun i => Fin.castAdd (n - ↑s) i) fun t => f ⟨↑t, ⋯⟩) +
Vector.sum (select (fun i => Fin.natAdd (↑s) i) fun t => f ⟨↑t, ⋯⟩) =
Vector.sum ftotal:(Vector.sum fun t => f ⟨↑t, ⋯⟩) = Vector.sum fsplit':Vector.sum (slice 0 ↑s ⋯ f) + Vector.sum (slice (↑s) (n - ↑s) ⋯ f) = Vector.sum f⊢ Vector.sum (slice 0 ↑s ⋯ f) + f s + Vector.sum (slice (↑s) (n - ↑s) ⋯ f) = Vector.sum f + f s) f) = Vector.sum f := by n:Natf:Vector ns:Fin n⊢ bidirectional_ssm_scan 1 f s = f.sum + f s n:Natf:Fin n → Rats:Fin none_pow:∀ (m : Nat), 1 ^ m = 1split:Vector.sum (select (fun i => Fin.castAdd (n - ↑s) i) fun t => f ⟨↑t, ⋯⟩) +
Vector.sum (select (fun i => Fin.natAdd (↑s) i) fun t => f ⟨↑t, ⋯⟩) =
Vector.sum ftotal:(Vector.sum fun t => f ⟨↑t, ⋯⟩) = Vector.sum fsplit':Vector.sum (slice 0 ↑s ⋯ f) + Vector.sum (slice (↑s) (n - ↑s) ⋯ f) = Vector.sum f⊢ Vector.sum (slice 0 ↑s ⋯ f) + f s + Vector.sum (slice (↑s) (n - ↑s) ⋯ f) = Vector.sum f + f s
simpa [Vector.sum, slice, select, Nat.add_comm] using split n:Natf:Fin n → Rats:Fin none_pow:∀ (m : Nat), 1 ^ m = 1split:Vector.sum (select (fun i => Fin.castAdd (n - ↑s) i) fun t => f ⟨↑t, ⋯⟩) +
Vector.sum (select (fun i => Fin.natAdd (↑s) i) fun t => f ⟨↑t, ⋯⟩) =
Vector.sum ftotal:(Vector.sum fun t => f ⟨↑t, ⋯⟩) = Vector.sum fsplit':Vector.sum (slice 0 ↑s ⋯ f) + Vector.sum (slice (↑s) (n - ↑s) ⋯ f) = Vector.sum f⊢ Vector.sum (slice 0 ↑s ⋯ f) + f s + Vector.sum (slice (↑s) (n - ↑s) ⋯ f) = Vector.sum f + f s n:Natf:Fin n → Rats:Fin none_pow:∀ (m : Nat), 1 ^ m = 1split:Vector.sum (select (fun i => Fin.castAdd (n - ↑s) i) fun t => f ⟨↑t, ⋯⟩) +
Vector.sum (select (fun i => Fin.natAdd (↑s) i) fun t => f ⟨↑t, ⋯⟩) =
Vector.sum ftotal:(Vector.sum fun t => f ⟨↑t, ⋯⟩) = Vector.sum fsplit':Vector.sum (slice 0 ↑s ⋯ f) + Vector.sum (slice (↑s) (n - ↑s) ⋯ f) = Vector.sum f⊢ Vector.sum (slice 0 ↑s ⋯ f) + f s + Vector.sum (slice (↑s) (n - ↑s) ⋯ f) = Vector.sum f + f s
grind All goals completed! 🐙
theorem bidirectional_ssm_layer_permute :
PermuteEquivariant
(bidirectional_ssm_layer (seq := seq) (hidden := hidden) 1)
permute_qkv := by seq:Nathidden:Nat⊢ PermuteEquivariant (bidirectional_ssm_layer 1) permute_qkv permute
intro π input seq:Nathidden:Natπ:PositionPermutation seqinput:(Matrix seq hidden × Matrix seq hidden) × Matrix seq hidden⊢ (fun {shape} => bidirectional_ssm_layer 1) (permute_qkv π input) =
permute π ((fun {shape} => bidirectional_ssm_layer 1) input)
rcases input with ⟨⟨q, k⟩, v⟩ seq:Nathidden:Natπ:PositionPermutation seqv:Matrix seq hiddenq:Matrix seq hiddenk:Matrix seq hidden⊢ (fun {shape} => bidirectional_ssm_layer 1) (permute_qkv π ((q, k), v)) =
permute π ((fun {shape} => bidirectional_ssm_layer 1) ((q, k), v))
funext s j seq:Nathidden:Natπ:PositionPermutation seqv:Matrix seq hiddenq:Matrix seq hiddenk:Matrix seq hiddens:Fin seqj:Fin hidden⊢ (fun {shape} => bidirectional_ssm_layer 1) (permute_qkv π ((q, k), v)) s j =
permute π ((fun {shape} => bidirectional_ssm_layer 1) ((q, k), v)) s j
apply congrArg Vector.sum seq:Nathidden:Natπ:PositionPermutation seqv:Matrix seq hiddenq:Matrix seq hiddenk:Matrix seq hiddens:Fin seqj:Fin hidden⊢ Matrix.transpose (fun d j => bidirectional_ssm_scan 1 (fun t => permute π k t d * permute π v t j) s) j *
permute π q s =
Matrix.transpose (fun d j => bidirectional_ssm_scan 1 (fun t => k t d * v t j) (π.index s)) j * q (π.index s)
funext d seq:Nathidden:Natπ:PositionPermutation seqv:Matrix seq hiddenq:Matrix seq hiddenk:Matrix seq hiddens:Fin seqj:Fin hiddend:Fin hidden⊢ (Matrix.transpose (fun d j => bidirectional_ssm_scan 1 (fun t => permute π k t d * permute π v t j) s) j *
permute π q s)
d =
(Matrix.transpose (fun d j => bidirectional_ssm_scan 1 (fun t => k t d * v t j) (π.index s)) j * q (π.index s)) d
change bidirectional_ssm_scan 1 (fun t => k (π.index t) d * v (π.index t) j) s *
q (π.index s) d =
bidirectional_ssm_scan 1 (fun t => k t d * v t j) (π.index s) * q (π.index s) d seq:Nathidden:Natπ:PositionPermutation seqv:Matrix seq hiddenq:Matrix seq hiddenk:Matrix seq hiddens:Fin seqj:Fin hiddend:Fin hidden⊢ bidirectional_ssm_scan 1 (fun t => k (π.index t) d * v (π.index t) j) s * q (π.index s) d =
bidirectional_ssm_scan 1 (fun t => k t d * v t j) (π.index s) * q (π.index s) d
rw [bidirectional_scan_one, seq:Nathidden:Natπ:PositionPermutation seqv:Matrix seq hiddenq:Matrix seq hiddenk:Matrix seq hiddens:Fin seqj:Fin hiddend:Fin hidden⊢ ((Vector.sum fun t => k (π.index t) d * v (π.index t) j) + k (π.index s) d * v (π.index s) j) * q (π.index s) d =
bidirectional_ssm_scan 1 (fun t => k t d * v t j) (π.index s) * q (π.index s) d seq:Nathidden:Natπ:PositionPermutation seqv:Matrix seq hiddenq:Matrix seq hiddenk:Matrix seq hiddens:Fin seqj:Fin hiddend:Fin hidden⊢ ((Vector.sum fun t => k (π.index t) d * v (π.index t) j) + k (π.index s) d * v (π.index s) j) * q (π.index s) d =
((Vector.sum fun t => k t d * v t j) + k (π.index s) d * v (π.index s) j) * q (π.index s) d bidirectional_scan_one seq:Nathidden:Natπ:PositionPermutation seqv:Matrix seq hiddenq:Matrix seq hiddenk:Matrix seq hiddens:Fin seqj:Fin hiddend:Fin hidden⊢ ((Vector.sum fun t => k (π.index t) d * v (π.index t) j) + k (π.index s) d * v (π.index s) j) * q (π.index s) d =
((Vector.sum fun t => k t d * v t j) + k (π.index s) d * v (π.index s) j) * q (π.index s) d seq:Nathidden:Natπ:PositionPermutation seqv:Matrix seq hiddenq:Matrix seq hiddenk:Matrix seq hiddens:Fin seqj:Fin hiddend:Fin hidden⊢ ((Vector.sum fun t => k (π.index t) d * v (π.index t) j) + k (π.index s) d * v (π.index s) j) * q (π.index s) d =
((Vector.sum fun t => k t d * v t j) + k (π.index s) d * v (π.index s) j) * q (π.index s) d] seq:Nathidden:Natπ:PositionPermutation seqv:Matrix seq hiddenq:Matrix seq hiddenk:Matrix seq hiddens:Fin seqj:Fin hiddend:Fin hidden⊢ ((Vector.sum fun t => k (π.index t) d * v (π.index t) j) + k (π.index s) d * v (π.index s) j) * q (π.index s) d =
((Vector.sum fun t => k t d * v t j) + k (π.index s) d * v (π.index s) j) * q (π.index s) d
exact congrArg
(fun total => (total + k (π.index s) d * v (π.index s) j) * q (π.index s) d)
(permute_sum π (fun t => k t d * v t j)) All goals completed! 🐙These models also have the property that we can chunk them into groups which can be computed separately. Here we consider a simplified version of chunking in order to distribute across machines.
def ssm_chunk (a : Rat) (values : Vector n) (incoming : Rat) : Rat :=
a ^ n * incoming + Vector.sum (fun t => a ^ (n - 1 - t.val) * values t)def ssm_chunk_carry (tiles n : Nat) (a : Rat)
(values : Vector (tiles * n)) : Rat :=
scan (fun state values => ssm_chunk a values state) (tile n values) 0def chunkwise_ssm_layer (tiles n : Nat) (a : Rat) : Mixer (tiles * n) hidden :=
fun input =>
let ⟨⟨q, k⟩, v⟩ := input
fun s =>
let start := s.val / n * n
let state : Matrix hidden hidden := fun d j =>
let updates : Vector (tiles * n) := fun t => k t d * v t j
let incoming := ssm_chunk_carry (s.val / n) n a (slice 0 start (by α:Type uβ:Type vδ:Type whidden:Nattiles:Natn:Nata:Ratinput:(Matrix (tiles * n) hidden × Matrix (tiles * n) hidden) × Matrix (tiles * n) hiddenq:Matrix (tiles * n) hiddenk:Matrix (tiles * n) hiddenv:Matrix (tiles * n) hiddens:Fin (tiles * n)start:Nat := ↑s / n * nd:Fin hiddenj:Fin hiddenupdates:Vector (tiles * n) := fun t => k t d * v t j⊢ 0 + start ≤ tiles * n
have := Nat.div_add_mod' s.val n α:Type uβ:Type vδ:Type whidden:Nattiles:Natn:Nata:Ratinput:(Matrix (tiles * n) hidden × Matrix (tiles * n) hidden) × Matrix (tiles * n) hiddenq:Matrix (tiles * n) hiddenk:Matrix (tiles * n) hiddenv:Matrix (tiles * n) hiddens:Fin (tiles * n)start:Nat := ↑s / n * nd:Fin hiddenj:Fin hiddenupdates:Vector (tiles * n) := fun t => k t d * v t jthis:↑s / n * n + ↑s % n = ↑s⊢ 0 + start ≤ tiles * n
omega All goals completed! 🐙) updates)
ssm_chunk a (slice start (s.val % n + 1) (by α:Type uβ:Type vδ:Type whidden:Nattiles:Natn:Nata:Ratinput:(Matrix (tiles * n) hidden × Matrix (tiles * n) hidden) × Matrix (tiles * n) hiddenq:Matrix (tiles * n) hiddenk:Matrix (tiles * n) hiddenv:Matrix (tiles * n) hiddens:Fin (tiles * n)start:Nat := ↑s / n * nd:Fin hiddenj:Fin hiddenupdates:Vector (tiles * n) := fun t => k t d * v t jincoming:Rat := ssm_chunk_carry (↑s / n) n a (slice 0 start ⋯ updates)⊢ start + (↑s % n + 1) ≤ tiles * n
have := Nat.div_add_mod' s.val n α:Type uβ:Type vδ:Type whidden:Nattiles:Natn:Nata:Ratinput:(Matrix (tiles * n) hidden × Matrix (tiles * n) hidden) × Matrix (tiles * n) hiddenq:Matrix (tiles * n) hiddenk:Matrix (tiles * n) hiddenv:Matrix (tiles * n) hiddens:Fin (tiles * n)start:Nat := ↑s / n * nd:Fin hiddenj:Fin hiddenupdates:Vector (tiles * n) := fun t => k t d * v t jincoming:Rat := ssm_chunk_carry (↑s / n) n a (slice 0 start ⋯ updates)this:↑s / n * n + ↑s % n = ↑s⊢ start + (↑s % n + 1) ≤ tiles * n
omega All goals completed! 🐙) updates) incoming
state.transpose.matvec (q s)We can prove that this yields the same result as our simple implementation.
theorem ssm_chunk_carry_eq (tiles n : Nat) (a : Rat)
(values : Vector (tiles * n)) :
ssm_chunk_carry tiles n a values = ssm_scan a values := by tiles:Natn:Nata:Ratvalues:Vector (tiles * n)⊢ ssm_chunk_carry tiles n a values = ssm_scan a values
simp only [ssm_chunk_carry, ssm_chunk, ← scan_weighted, ssm_scan] tiles:Natn:Nata:Ratvalues:Vector (tiles * n)⊢ scan (fun state values => scan (fun state x => a * state + x) values state) (tile n values) 0 =
scan (fun state x => a * state + x) values 0
exact tile_fold_eq tiles n _ values 0 All goals completed! 🐙
theorem chunkwise_ssm_layer_eq (tiles n : Nat) (a : Rat)
(input : (Matrix (tiles * n) hidden × Matrix (tiles * n) hidden) ×
Matrix (tiles * n) hidden) :
chunkwise_ssm_layer tiles n a input = ssm_layer a input := by hidden:Nattiles:Natn:Nata:Ratinput:(Matrix (tiles * n) hidden × Matrix (tiles * n) hidden) × Matrix (tiles * n) hidden⊢ chunkwise_ssm_layer tiles n a input = ssm_layer a input
rcases input with ⟨⟨q, k⟩, v⟩ hidden:Nattiles:Natn:Nata:Ratv:Matrix (tiles * n) hiddenq:Matrix (tiles * n) hiddenk:Matrix (tiles * n) hidden⊢ chunkwise_ssm_layer tiles n a ((q, k), v) = ssm_layer a ((q, k), v)
funext s j hidden:Nattiles:Natn:Nata:Ratv:Matrix (tiles * n) hiddenq:Matrix (tiles * n) hiddenk:Matrix (tiles * n) hiddens:Fin (tiles * n)j:Fin hidden⊢ chunkwise_ssm_layer tiles n a ((q, k), v) s j = ssm_layer a ((q, k), v) s j
apply congrArg Vector.sum hidden:Nattiles:Natn:Nata:Ratv:Matrix (tiles * n) hiddenq:Matrix (tiles * n) hiddenk:Matrix (tiles * n) hiddens:Fin (tiles * n)j:Fin hidden⊢ Matrix.transpose
(fun d j =>
have updates := fun t => k t d * v t j;
have incoming := ssm_chunk_carry (↑s / n) n a (slice 0 (↑s / n * n) ⋯ updates);
ssm_chunk a (slice (↑s / n * n) (↑s % n + 1) ⋯ updates) incoming)
j *
q s =
(ssm_state a (slice 0 (↑s + 1) ⋯ fun t d j => k t d * v t j)).transpose j * q s
funext d hidden:Nattiles:Natn:Nata:Ratv:Matrix (tiles * n) hiddenq:Matrix (tiles * n) hiddenk:Matrix (tiles * n) hiddens:Fin (tiles * n)j:Fin hiddend:Fin hidden⊢ (Matrix.transpose
(fun d j =>
have updates := fun t => k t d * v t j;
have incoming := ssm_chunk_carry (↑s / n) n a (slice 0 (↑s / n * n) ⋯ updates);
ssm_chunk a (slice (↑s / n * n) (↑s % n + 1) ⋯ updates) incoming)
j *
q s)
d =
((ssm_state a (slice 0 (↑s + 1) ⋯ fun t d j => k t d * v t j)).transpose j * q s) d
let updates : Vector (tiles * n) := fun t => k t d * v t j hidden:Nattiles:Natn:Nata:Ratv:Matrix (tiles * n) hiddenq:Matrix (tiles * n) hiddenk:Matrix (tiles * n) hiddens:Fin (tiles * n)j:Fin hiddend:Fin hiddenupdates:Vector (tiles * n) := fun t => k t d * v t j⊢ (Matrix.transpose
(fun d j =>
have updates := fun t => k t d * v t j;
have incoming := ssm_chunk_carry (↑s / n) n a (slice 0 (↑s / n * n) ⋯ updates);
ssm_chunk a (slice (↑s / n * n) (↑s % n + 1) ⋯ updates) incoming)
j *
q s)
d =
((ssm_state a (slice 0 (↑s + 1) ⋯ fun t d j => k t d * v t j)).transpose j * q s) d
have bound : s.val / n * n + (s.val % n + 1) ≤ tiles * n := by hidden:Nattiles:Natn:Nata:Ratinput:(Matrix (tiles * n) hidden × Matrix (tiles * n) hidden) × Matrix (tiles * n) hidden⊢ chunkwise_ssm_layer tiles n a input = ssm_layer a input hidden:Nattiles:Natn:Nata:Ratv:Matrix (tiles * n) hiddenq:Matrix (tiles * n) hiddenk:Matrix (tiles * n) hiddens:Fin (tiles * n)j:Fin hiddend:Fin hiddenupdates:Vector (tiles * n) := fun t => k t d * v t jbound:↑s / n * n + (↑s % n + 1) ≤ tiles * n⊢ (Matrix.transpose
(fun d j =>
have updates := fun t => k t d * v t j;
have incoming := ssm_chunk_carry (↑s / n) n a (slice 0 (↑s / n * n) ⋯ updates);
ssm_chunk a (slice (↑s / n * n) (↑s % n + 1) ⋯ updates) incoming)
j *
q s)
d =
((ssm_state a (slice 0 (↑s + 1) ⋯ fun t d j => k t d * v t j)).transpose j * q s) d
have := Nat.div_add_mod' s.val n hidden:Nattiles:Natn:Nata:Ratv:Matrix (tiles * n) hiddenq:Matrix (tiles * n) hiddenk:Matrix (tiles * n) hiddens:Fin (tiles * n)j:Fin hiddend:Fin hiddenupdates:Vector (tiles * n) := fun t => k t d * v t jthis:↑s / n * n + ↑s % n = ↑s⊢ ↑s / n * n + (↑s % n + 1) ≤ tiles * n hidden:Nattiles:Natn:Nata:Ratv:Matrix (tiles * n) hiddenq:Matrix (tiles * n) hiddenk:Matrix (tiles * n) hiddens:Fin (tiles * n)j:Fin hiddend:Fin hiddenupdates:Vector (tiles * n) := fun t => k t d * v t jbound:↑s / n * n + (↑s % n + 1) ≤ tiles * n⊢ (Matrix.transpose
(fun d j =>
have updates := fun t => k t d * v t j;
have incoming := ssm_chunk_carry (↑s / n) n a (slice 0 (↑s / n * n) ⋯ updates);
ssm_chunk a (slice (↑s / n * n) (↑s % n + 1) ⋯ updates) incoming)
j *
q s)
d =
((ssm_state a (slice 0 (↑s + 1) ⋯ fun t d j => k t d * v t j)).transpose j * q s) d
omega hidden:Nattiles:Natn:Nata:Ratv:Matrix (tiles * n) hiddenq:Matrix (tiles * n) hiddenk:Matrix (tiles * n) hiddens:Fin (tiles * n)j:Fin hiddend:Fin hiddenupdates:Vector (tiles * n) := fun t => k t d * v t jbound:↑s / n * n + (↑s % n + 1) ≤ tiles * n⊢ (Matrix.transpose
(fun d j =>
have updates := fun t => k t d * v t j;
have incoming := ssm_chunk_carry (↑s / n) n a (slice 0 (↑s / n * n) ⋯ updates);
ssm_chunk a (slice (↑s / n * n) (↑s % n + 1) ⋯ updates) incoming)
j *
q s)
d =
((ssm_state a (slice 0 (↑s + 1) ⋯ fun t d j => k t d * v t j)).transpose j * q s) d hidden:Nattiles:Natn:Nata:Ratv:Matrix (tiles * n) hiddenq:Matrix (tiles * n) hiddenk:Matrix (tiles * n) hiddens:Fin (tiles * n)j:Fin hiddend:Fin hiddenupdates:Vector (tiles * n) := fun t => k t d * v t jbound:↑s / n * n + (↑s % n + 1) ≤ tiles * n⊢ (Matrix.transpose
(fun d j =>
have updates := fun t => k t d * v t j;
have incoming := ssm_chunk_carry (↑s / n) n a (slice 0 (↑s / n * n) ⋯ updates);
ssm_chunk a (slice (↑s / n * n) (↑s % n + 1) ⋯ updates) incoming)
j *
q s)
d =
((ssm_state a (slice 0 (↑s + 1) ⋯ fun t d j => k t d * v t j)).transpose j * q s) d
change ssm_chunk a (slice (s.val / n * n) (s.val % n + 1) bound updates)
(ssm_chunk_carry (s.val / n) n a
(slice 0 (s.val / n * n) (by hidden:Nattiles:Natn:Nata:Ratv:Matrix (tiles * n) hiddenq:Matrix (tiles * n) hiddenk:Matrix (tiles * n) hiddens:Fin (tiles * n)j:Fin hiddend:Fin hiddenupdates:Vector (tiles * n) := fun t => k t d * v t jbound:↑s / n * n + (↑s % n + 1) ≤ tiles * n⊢ 0 + ↑s / n * n ≤ tiles * n omega All goals completed! 🐙) updates)) * q s d =
ssm_scan a (slice 0 (s.val + 1) (by hidden:Nattiles:Natn:Nata:Ratv:Matrix (tiles * n) hiddenq:Matrix (tiles * n) hiddenk:Matrix (tiles * n) hiddens:Fin (tiles * n)j:Fin hiddend:Fin hiddenupdates:Vector (tiles * n) := fun t => k t d * v t jbound:↑s / n * n + (↑s % n + 1) ≤ tiles * n⊢ 0 + (↑s + 1) ≤ tiles * n omega All goals completed! 🐙) updates) * q s d
apply congrArg (fun state => state * q s d) hidden:Nattiles:Natn:Nata:Ratv:Matrix (tiles * n) hiddenq:Matrix (tiles * n) hiddenk:Matrix (tiles * n) hiddens:Fin (tiles * n)j:Fin hiddend:Fin hiddenupdates:Vector (tiles * n) := fun t => k t d * v t jbound:↑s / n * n + (↑s % n + 1) ≤ tiles * n⊢ ssm_chunk a (slice (↑s / n * n) (↑s % n + 1) bound updates)
(ssm_chunk_carry (↑s / n) n a (slice 0 (↑s / n * n) ⋯ updates)) =
ssm_scan a (slice 0 (↑s + 1) ⋯ updates)
rw [ssm_chunk_carry_eq hidden:Nattiles:Natn:Nata:Ratv:Matrix (tiles * n) hiddenq:Matrix (tiles * n) hiddenk:Matrix (tiles * n) hiddens:Fin (tiles * n)j:Fin hiddend:Fin hiddenupdates:Vector (tiles * n) := fun t => k t d * v t jbound:↑s / n * n + (↑s % n + 1) ≤ tiles * n⊢ ssm_chunk a (slice (↑s / n * n) (↑s % n + 1) bound updates) (ssm_scan a (slice 0 (↑s / n * n) ⋯ updates)) =
ssm_scan a (slice 0 (↑s + 1) ⋯ updates) hidden:Nattiles:Natn:Nata:Ratv:Matrix (tiles * n) hiddenq:Matrix (tiles * n) hiddenk:Matrix (tiles * n) hiddens:Fin (tiles * n)j:Fin hiddend:Fin hiddenupdates:Vector (tiles * n) := fun t => k t d * v t jbound:↑s / n * n + (↑s % n + 1) ≤ tiles * n⊢ ssm_chunk a (slice (↑s / n * n) (↑s % n + 1) bound updates) (ssm_scan a (slice 0 (↑s / n * n) ⋯ updates)) =
ssm_scan a (slice 0 (↑s + 1) ⋯ updates)] hidden:Nattiles:Natn:Nata:Ratv:Matrix (tiles * n) hiddenq:Matrix (tiles * n) hiddenk:Matrix (tiles * n) hiddens:Fin (tiles * n)j:Fin hiddend:Fin hiddenupdates:Vector (tiles * n) := fun t => k t d * v t jbound:↑s / n * n + (↑s % n + 1) ≤ tiles * n⊢ ssm_chunk a (slice (↑s / n * n) (↑s % n + 1) bound updates) (ssm_scan a (slice 0 (↑s / n * n) ⋯ updates)) =
ssm_scan a (slice 0 (↑s + 1) ⋯ updates)
change (a ^ _ * _ + _) = _ hidden:Nattiles:Natn:Nata:Ratv:Matrix (tiles * n) hiddenq:Matrix (tiles * n) hiddenk:Matrix (tiles * n) hiddens:Fin (tiles * n)j:Fin hiddend:Fin hiddenupdates:Vector (tiles * n) := fun t => k t d * v t jbound:↑s / n * n + (↑s % n + 1) ≤ tiles * n⊢ (a ^ (↑s % n + 1) * ssm_scan a (slice 0 (↑s / n * n) ⋯ updates) +
Vector.sum fun t => a ^ (↑s % n + 1 - 1 - ↑t) * slice (↑s / n * n) (↑s % n + 1) bound updates t) =
ssm_scan a (slice 0 (↑s + 1) ⋯ updates)
rw [← scan_weighted hidden:Nattiles:Natn:Nata:Ratv:Matrix (tiles * n) hiddenq:Matrix (tiles * n) hiddenk:Matrix (tiles * n) hiddens:Fin (tiles * n)j:Fin hiddend:Fin hiddenupdates:Vector (tiles * n) := fun t => k t d * v t jbound:↑s / n * n + (↑s % n + 1) ≤ tiles * n⊢ ssm_scan a (slice (↑s / n * n) (↑s % n + 1) bound updates) (ssm_scan a (slice 0 (↑s / n * n) ⋯ updates)) =
ssm_scan a (slice 0 (↑s + 1) ⋯ updates) hidden:Nattiles:Natn:Nata:Ratv:Matrix (tiles * n) hiddenq:Matrix (tiles * n) hiddenk:Matrix (tiles * n) hiddens:Fin (tiles * n)j:Fin hiddend:Fin hiddenupdates:Vector (tiles * n) := fun t => k t d * v t jbound:↑s / n * n + (↑s % n + 1) ≤ tiles * n⊢ ssm_scan a (slice (↑s / n * n) (↑s % n + 1) bound updates) (ssm_scan a (slice 0 (↑s / n * n) ⋯ updates)) =
ssm_scan a (slice 0 (↑s + 1) ⋯ updates)] hidden:Nattiles:Natn:Nata:Ratv:Matrix (tiles * n) hiddenq:Matrix (tiles * n) hiddenk:Matrix (tiles * n) hiddens:Fin (tiles * n)j:Fin hiddend:Fin hiddenupdates:Vector (tiles * n) := fun t => k t d * v t jbound:↑s / n * n + (↑s % n + 1) ≤ tiles * n⊢ ssm_scan a (slice (↑s / n * n) (↑s % n + 1) bound updates) (ssm_scan a (slice 0 (↑s / n * n) ⋯ updates)) =
ssm_scan a (slice 0 (↑s + 1) ⋯ updates)
change scan (fun state x => a * state + x) _
(scan (fun state x => a * state + x) _ 0) = _ hidden:Nattiles:Natn:Nata:Ratv:Matrix (tiles * n) hiddenq:Matrix (tiles * n) hiddenk:Matrix (tiles * n) hiddens:Fin (tiles * n)j:Fin hiddend:Fin hiddenupdates:Vector (tiles * n) := fun t => k t d * v t jbound:↑s / n * n + (↑s % n + 1) ≤ tiles * n⊢ scan (fun state x => a * state + x) (slice (↑s / n * n) (↑s % n + 1) bound updates)
(scan (fun state x => a * state + x) (slice 0 (↑s / n * n) ⋯ updates) 0) =
ssm_scan a (slice 0 (↑s + 1) ⋯ updates)
rw [scan_slice hidden:Nattiles:Natn:Nata:Ratv:Matrix (tiles * n) hiddenq:Matrix (tiles * n) hiddenk:Matrix (tiles * n) hiddens:Fin (tiles * n)j:Fin hiddend:Fin hiddenupdates:Vector (tiles * n) := fun t => k t d * v t jbound:↑s / n * n + (↑s % n + 1) ≤ tiles * n⊢ scan (fun state x => a * state + x) (slice 0 (↑s / n * n + (↑s % n + 1)) ⋯ updates) 0 =
ssm_scan a (slice 0 (↑s + 1) ⋯ updates) hidden:Nattiles:Natn:Nata:Ratv:Matrix (tiles * n) hiddenq:Matrix (tiles * n) hiddenk:Matrix (tiles * n) hiddens:Fin (tiles * n)j:Fin hiddend:Fin hiddenupdates:Vector (tiles * n) := fun t => k t d * v t jbound:↑s / n * n + (↑s % n + 1) ≤ tiles * n⊢ scan (fun state x => a * state + x) (slice 0 (↑s / n * n + (↑s % n + 1)) ⋯ updates) 0 =
ssm_scan a (slice 0 (↑s + 1) ⋯ updates)] hidden:Nattiles:Natn:Nata:Ratv:Matrix (tiles * n) hiddenq:Matrix (tiles * n) hiddenk:Matrix (tiles * n) hiddens:Fin (tiles * n)j:Fin hiddend:Fin hiddenupdates:Vector (tiles * n) := fun t => k t d * v t jbound:↑s / n * n + (↑s % n + 1) ≤ tiles * n⊢ scan (fun state x => a * state + x) (slice 0 (↑s / n * n + (↑s % n + 1)) ⋯ updates) 0 =
ssm_scan a (slice 0 (↑s + 1) ⋯ updates)
have offset : s.val / n * n + (s.val % n + 1) = s.val + 1 := by hidden:Nattiles:Natn:Nata:Ratinput:(Matrix (tiles * n) hidden × Matrix (tiles * n) hidden) × Matrix (tiles * n) hidden⊢ chunkwise_ssm_layer tiles n a input = ssm_layer a input hidden:Nattiles:Natn:Nata:Ratv:Matrix (tiles * n) hiddenq:Matrix (tiles * n) hiddenk:Matrix (tiles * n) hiddens:Fin (tiles * n)j:Fin hiddend:Fin hiddenupdates:Vector (tiles * n) := fun t => k t d * v t jbound:↑s / n * n + (↑s % n + 1) ≤ tiles * noffset:↑s / n * n + (↑s % n + 1) = ↑s + 1⊢ scan (fun state x => a * state + x) (slice 0 (↑s / n * n + (↑s % n + 1)) ⋯ updates) 0 =
ssm_scan a (slice 0 (↑s + 1) ⋯ updates)
have := Nat.div_add_mod' s.val n hidden:Nattiles:Natn:Nata:Ratv:Matrix (tiles * n) hiddenq:Matrix (tiles * n) hiddenk:Matrix (tiles * n) hiddens:Fin (tiles * n)j:Fin hiddend:Fin hiddenupdates:Vector (tiles * n) := fun t => k t d * v t jbound:↑s / n * n + (↑s % n + 1) ≤ tiles * nthis:↑s / n * n + ↑s % n = ↑s⊢ ↑s / n * n + (↑s % n + 1) = ↑s + 1 hidden:Nattiles:Natn:Nata:Ratv:Matrix (tiles * n) hiddenq:Matrix (tiles * n) hiddenk:Matrix (tiles * n) hiddens:Fin (tiles * n)j:Fin hiddend:Fin hiddenupdates:Vector (tiles * n) := fun t => k t d * v t jbound:↑s / n * n + (↑s % n + 1) ≤ tiles * noffset:↑s / n * n + (↑s % n + 1) = ↑s + 1⊢ scan (fun state x => a * state + x) (slice 0 (↑s / n * n + (↑s % n + 1)) ⋯ updates) 0 =
ssm_scan a (slice 0 (↑s + 1) ⋯ updates)
omega hidden:Nattiles:Natn:Nata:Ratv:Matrix (tiles * n) hiddenq:Matrix (tiles * n) hiddenk:Matrix (tiles * n) hiddens:Fin (tiles * n)j:Fin hiddend:Fin hiddenupdates:Vector (tiles * n) := fun t => k t d * v t jbound:↑s / n * n + (↑s % n + 1) ≤ tiles * noffset:↑s / n * n + (↑s % n + 1) = ↑s + 1⊢ scan (fun state x => a * state + x) (slice 0 (↑s / n * n + (↑s % n + 1)) ⋯ updates) 0 =
ssm_scan a (slice 0 (↑s + 1) ⋯ updates) hidden:Nattiles:Natn:Nata:Ratv:Matrix (tiles * n) hiddenq:Matrix (tiles * n) hiddenk:Matrix (tiles * n) hiddens:Fin (tiles * n)j:Fin hiddend:Fin hiddenupdates:Vector (tiles * n) := fun t => k t d * v t jbound:↑s / n * n + (↑s % n + 1) ≤ tiles * noffset:↑s / n * n + (↑s % n + 1) = ↑s + 1⊢ scan (fun state x => a * state + x) (slice 0 (↑s / n * n + (↑s % n + 1)) ⋯ updates) 0 =
ssm_scan a (slice 0 (↑s + 1) ⋯ updates)
exact scan_full _ (slice 0 (s.val + 1) (by hidden:Nattiles:Natn:Nata:Ratv:Matrix (tiles * n) hiddenq:Matrix (tiles * n) hiddenk:Matrix (tiles * n) hiddens:Fin (tiles * n)j:Fin hiddend:Fin hiddenupdates:Vector (tiles * n) := fun t => k t d * v t jbound:↑s / n * n + (↑s % n + 1) ≤ tiles * noffset:↑s / n * n + (↑s % n + 1) = ↑s + 1⊢ 0 + (↑s + 1) ≤ tiles * n omega All goals completed! 🐙) updates) 0 offsetConclusion
This blog considers the use of Lean as a method to verify elementary properties about Transformers and related models. There are many additional things one might consider here, including bounding errors introduced from quantization, aggregation, backpropagation, and training-inference mismatch. Additionally, there are likely many ways to simplify these proofs or develop libraries to make them more minimal.
At a high level, the main recent change is not the machinery for proving these properties; it is the ease with which a person can specify "what" they want to be proven and receive a certificate that a property is true. Understanding how that interface should work and how it can be used will be an extremely interesting challenge over the next year.