|
| 1 | +-- SPDX-License-Identifier: MPL-2.0 |
| 2 | +-- Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk> |
| 3 | +-- |
| 4 | +||| Flagship semantic proof for Halideiser: SCHEDULE EQUIVALENCE. |
| 5 | +||| |
| 6 | +||| Halide's central guarantee is that a *schedule* changes only HOW a |
| 7 | +||| computation executes (loop order, tiling, splitting), never WHAT it |
| 8 | +||| computes. This module makes that guarantee machine-checked for the |
| 9 | +||| canonical schedule transformation: the loop SPLIT. |
| 10 | +||| |
| 11 | +||| Domain model |
| 12 | +||| ------------ |
| 13 | +||| A 1-D pipeline stage is a pure pointwise function `f : Nat -> Nat` |
| 14 | +||| applied at every index of an iteration domain `[0 .. n-1]`. |
| 15 | +||| |
| 16 | +||| * unscheduled execution : evaluate f at the flat index list |
| 17 | +||| `range n = [0,1,...,n-1]` in order. |
| 18 | +||| * scheduled (split f=k) : split the domain into `outer` blocks of |
| 19 | +||| `inner` iterations (n = outer * inner), |
| 20 | +||| run the nested loop, and reconstruct each |
| 21 | +||| global index as `o*inner + i`. |
| 22 | +||| |
| 23 | +||| Headline property (`SplitEquivalent`): for every stage `f` and every |
| 24 | +||| split `(outer, inner)`, the scheduled result equals the unscheduled |
| 25 | +||| result over the domain of size `outer * inner`. This is a genuine, |
| 26 | +||| non-vacuous theorem: the proof rests on a structural lemma that the |
| 27 | +||| split enumeration reproduces the flat index enumeration exactly. |
| 28 | +||| |
| 29 | +||| The bad case (a schedule that silently REORDERS or DROPS work and so |
| 30 | +||| changes the result) has NO inhabitant of `SplitEquivalent`: the |
| 31 | +||| negative control exhibits a concrete fake "schedule" whose output |
| 32 | +||| differs, and proves it is `Not` equivalent. |
| 33 | +module Halideiser.ABI.Semantics |
| 34 | + |
| 35 | +import Halideiser.ABI.Types |
| 36 | +import Data.Nat |
| 37 | +import Data.List |
| 38 | +import Decidable.Equality |
| 39 | + |
| 40 | +%default total |
| 41 | + |
| 42 | +-------------------------------------------------------------------------------- |
| 43 | +-- Iteration domain: the flat index list executed by the unscheduled loop |
| 44 | +-------------------------------------------------------------------------------- |
| 45 | + |
| 46 | +||| `range n = [0, 1, ..., n-1]` — the iteration domain of a 1-D stage. |
| 47 | +public export |
| 48 | +range : Nat -> List Nat |
| 49 | +range Z = [] |
| 50 | +range (S k) = range k ++ [k] |
| 51 | + |
| 52 | +||| Unscheduled execution: apply the pointwise stage `f` at every index. |
| 53 | +public export |
| 54 | +runFlat : (f : Nat -> Nat) -> (n : Nat) -> List Nat |
| 55 | +runFlat f n = map f (range n) |
| 56 | + |
| 57 | +-------------------------------------------------------------------------------- |
| 58 | +-- The SPLIT schedule |
| 59 | +-------------------------------------------------------------------------------- |
| 60 | + |
| 61 | +||| Reconstruct the global index from an (outer, inner) loop pair. |
| 62 | +||| This is exactly Halide's `split(x, xo, xi, inner)` index arithmetic: |
| 63 | +||| the original index is `xo * inner + xi`. |
| 64 | +public export |
| 65 | +splitIndex : (inner : Nat) -> (o : Nat) -> (i : Nat) -> Nat |
| 66 | +splitIndex inner o i = o * inner + i |
| 67 | + |
| 68 | +||| Run one inner block: for a fixed outer `o`, apply `f` at every inner |
| 69 | +||| index `i in [0 .. inner-1]`, reconstructing the global index each time. |
| 70 | +public export |
| 71 | +runInner : (f : Nat -> Nat) -> (inner : Nat) -> (o : Nat) -> List Nat |
| 72 | +runInner f inner o = map (\i => f (splitIndex inner o i)) (range inner) |
| 73 | + |
| 74 | +||| Concatenate a list of blocks left-to-right. Defined by explicit |
| 75 | +||| structural recursion (NOT `concatMap`/`foldl`) so it reduces cleanly |
| 76 | +||| during proof: `concatBlocks (b :: bs) = b ++ concatBlocks bs`. |
| 77 | +public export |
| 78 | +concatBlocks : List (List a) -> List a |
| 79 | +concatBlocks [] = [] |
| 80 | +concatBlocks (b :: bs) = b ++ concatBlocks bs |
| 81 | + |
| 82 | +||| Scheduled execution under `split` with `outer` blocks of `inner` |
| 83 | +||| iterations each: concatenate the inner blocks in outer order. |
| 84 | +public export |
| 85 | +runSplit : (f : Nat -> Nat) -> (outer : Nat) -> (inner : Nat) -> List Nat |
| 86 | +runSplit f outer inner = |
| 87 | + concatBlocks (map (runInner f inner) (range outer)) |
| 88 | + |
| 89 | +-------------------------------------------------------------------------------- |
| 90 | +-- Structural lemmas about `range` under splitting |
| 91 | +-------------------------------------------------------------------------------- |
| 92 | + |
| 93 | +||| `map` distributes over `++`. (Local, total; avoids relying on the |
| 94 | +||| exact library lemma name.) |
| 95 | +mapAppendL : (g : a -> b) -> (xs, ys : List a) -> |
| 96 | + map g (xs ++ ys) = map g xs ++ map g ys |
| 97 | +mapAppendL g [] ys = Refl |
| 98 | +mapAppendL g (x :: xs) ys = cong (g x ::) (mapAppendL g xs ys) |
| 99 | + |
| 100 | +||| `map` fuses: mapping `g` then `h` is mapping `(h . g)`. |
| 101 | +mapFusionL : (h : b -> c) -> (g : a -> b) -> (xs : List a) -> |
| 102 | + map h (map g xs) = map (\x => h (g x)) xs |
| 103 | +mapFusionL h g [] = Refl |
| 104 | +mapFusionL h g (x :: xs) = cong (h (g x) ::) (mapFusionL h g xs) |
| 105 | + |
| 106 | +||| `concatBlocks` distributes over `++` of block-lists. |
| 107 | +concatBlocksAppend : (xs, ys : List (List a)) -> |
| 108 | + concatBlocks (xs ++ ys) |
| 109 | + = concatBlocks xs ++ concatBlocks ys |
| 110 | +concatBlocksAppend [] ys = Refl |
| 111 | +concatBlocksAppend (b :: bs) ys = |
| 112 | + rewrite concatBlocksAppend bs ys in |
| 113 | + appendAssociative b (concatBlocks bs) (concatBlocks ys) |
| 114 | + |
| 115 | +||| Peel the last block: `concatBlocks (bs ++ [b]) = concatBlocks bs ++ b`. |
| 116 | +concatBlocksSnoc : (bs : List (List a)) -> (b : List a) -> |
| 117 | + concatBlocks (bs ++ [b]) = concatBlocks bs ++ b |
| 118 | +concatBlocksSnoc bs b = |
| 119 | + rewrite concatBlocksAppend bs [b] in |
| 120 | + cong (concatBlocks bs ++) (appendNilRightNeutral b) |
| 121 | + |
| 122 | +||| Decompose a range at an addition point: |
| 123 | +||| `range (a + b) = range a ++ map (+ a) (range b)`. |
| 124 | +||| The new block `[a, a+1, ..., a+b-1]` is exactly the old indices of |
| 125 | +||| `range b` shifted up by `a`. Proof by induction on `b` using the snoc |
| 126 | +||| structure of `range`. |
| 127 | +rangeAppend : (a, b : Nat) -> |
| 128 | + range (a + b) = range a ++ map (\i => i + a) (range b) |
| 129 | +rangeAppend a Z = rewrite plusZeroRightNeutral a in |
| 130 | + sym (appendNilRightNeutral (range a)) |
| 131 | +rangeAppend a (S k) = |
| 132 | + rewrite sym (plusSuccRightSucc a k) in |
| 133 | + -- LHS: range (S (a+k)) = range (a+k) ++ [a+k] |
| 134 | + rewrite rangeAppend a k in |
| 135 | + -- LHS: (range a ++ map (+a) (range k)) ++ [a+k] |
| 136 | + rewrite mapAppendL (\i => i + a) (range k) [k] in |
| 137 | + -- RHS inner: map (+a) (range k) ++ [k+a] |
| 138 | + rewrite plusCommutative k a in |
| 139 | + -- now [k+a] becomes [a+k] on the RHS, matching the LHS snoc element |
| 140 | + sym (appendAssociative (range a) (map (\i => i + a) (range k)) [a + k]) |
| 141 | + |
| 142 | +||| One outer block, expressed flatly: applying `f` after the inner-index |
| 143 | +||| reconstruction `(+ o*inner)` equals `f` mapped over the shifted range. |
| 144 | +||| This lines `runInner f inner o` up with the block that `rangeAppend` |
| 145 | +||| produces for the flat run. |
| 146 | +runInnerAsFlat : |
| 147 | + (f : Nat -> Nat) -> (inner, o : Nat) -> |
| 148 | + runInner f inner o = map f (map (\i => i + o * inner) (range inner)) |
| 149 | +runInnerAsFlat f inner o = |
| 150 | + rewrite mapFusionL f (\i => i + o * inner) (range inner) in |
| 151 | + mapExtFn (range inner) |
| 152 | + where |
| 153 | + ||| `splitIndex inner o i = o*inner + i = i + o*inner`, pointwise. |
| 154 | + mapExtFn : (xs : List Nat) -> |
| 155 | + map (\i => f (splitIndex inner o i)) xs |
| 156 | + = map (\i => f (i + o * inner)) xs |
| 157 | + mapExtFn [] = Refl |
| 158 | + mapExtFn (x :: xs) = |
| 159 | + cong2 (::) |
| 160 | + (cong f (plusCommutative (o * inner) x)) |
| 161 | + (mapExtFn xs) |
| 162 | + |
| 163 | +||| The heart of the theorem. Enumerating the FLAT domain |
| 164 | +||| `range (outer * inner)` and applying `f`, versus enumerating the SPLIT |
| 165 | +||| nested domain and reconstructing each index via `o*inner + i`, produce |
| 166 | +||| the SAME list. |
| 167 | +||| |
| 168 | +||| Proof by induction on `outer`. The successor case peels the last outer |
| 169 | +||| block off both sides: `runSplit` via `concatBlocksSnoc`, and `runFlat` |
| 170 | +||| via `rangeAppend` (`S o * inner = o*inner + inner`). The two peeled |
| 171 | +||| blocks coincide by `runInnerAsFlat`. |
| 172 | +splitEnumerates : |
| 173 | + (f : Nat -> Nat) -> (outer : Nat) -> (inner : Nat) -> |
| 174 | + runSplit f outer inner = runFlat f (outer * inner) |
| 175 | +splitEnumerates f Z inner = Refl |
| 176 | +splitEnumerates f (S o) inner = |
| 177 | + -- LHS: concatBlocks (map (runInner f inner) (range o ++ [o])) |
| 178 | + rewrite mapAppendL (runInner f inner) (range o) [o] in |
| 179 | + rewrite concatBlocksSnoc (map (runInner f inner) (range o)) (runInner f inner o) in |
| 180 | + -- LHS = runSplit f o inner ++ runInner f inner o |
| 181 | + rewrite splitEnumerates f o inner in |
| 182 | + -- LHS = map f (range (o*inner)) ++ runInner f inner o |
| 183 | + rewrite runInnerAsFlat f inner o in |
| 184 | + -- LHS = map f (range (o*inner)) ++ map f (map (+ o*inner) (range inner)) |
| 185 | + rewrite sym (mapAppendL f (range (o * inner)) (map (\i => i + o * inner) (range inner))) in |
| 186 | + -- LHS = map f (range (o*inner) ++ map (+ o*inner) (range inner)) |
| 187 | + rewrite sym (rangeAppend (o * inner) inner) in |
| 188 | + -- LHS = map f (range (o*inner + inner)) |
| 189 | + -- RHS: map f (range (S o * inner)) ; S o * inner = inner + o*inner. |
| 190 | + -- Show o*inner + inner = S o * inner so the ranges match. |
| 191 | + rewrite plusCommutative (o * inner) inner in |
| 192 | + Refl |
| 193 | + |
| 194 | +-------------------------------------------------------------------------------- |
| 195 | +-- Headline property |
| 196 | +-------------------------------------------------------------------------------- |
| 197 | + |
| 198 | +||| `SplitEquivalent f outer inner` is the proposition that the scheduled |
| 199 | +||| (split) run equals the unscheduled (flat) run over the domain of size |
| 200 | +||| `outer * inner`. There is NO way to inhabit this for a schedule that |
| 201 | +||| changes the result — the only constructor demands a real equality. |
| 202 | +public export |
| 203 | +data SplitEquivalent : (f : Nat -> Nat) -> (outer, inner : Nat) -> Type where |
| 204 | + MkSplitEquivalent : |
| 205 | + runSplit f outer inner = runFlat f (outer * inner) -> |
| 206 | + SplitEquivalent f outer inner |
| 207 | + |
| 208 | +||| The theorem: EVERY split schedule is equivalent to the flat schedule. |
| 209 | +public export |
| 210 | +splitPreservesResult : |
| 211 | + (f : Nat -> Nat) -> (outer, inner : Nat) -> SplitEquivalent f outer inner |
| 212 | +splitPreservesResult f outer inner = |
| 213 | + MkSplitEquivalent (splitEnumerates f outer inner) |
| 214 | + |
| 215 | +-------------------------------------------------------------------------------- |
| 216 | +-- Certifier |
| 217 | +-------------------------------------------------------------------------------- |
| 218 | + |
| 219 | +||| Certify a split schedule against a stage. Always returns `Ok` because |
| 220 | +||| `splitPreservesResult` shows every split is sound; the soundness fact |
| 221 | +||| below ties the `Ok` verdict back to the equivalence proposition. |
| 222 | +public export |
| 223 | +certifySplit : (f : Nat -> Nat) -> (outer, inner : Nat) -> Result |
| 224 | +certifySplit f outer inner = Ok |
| 225 | + |
| 226 | +||| Soundness of the certifier: an `Ok` verdict really does imply the |
| 227 | +||| scheduled and unscheduled runs agree. |
| 228 | +public export |
| 229 | +certifySplitSound : |
| 230 | + (f : Nat -> Nat) -> (outer, inner : Nat) -> |
| 231 | + certifySplit f outer inner = Ok -> |
| 232 | + runSplit f outer inner = runFlat f (outer * inner) |
| 233 | +certifySplitSound f outer inner _ = splitEnumerates f outer inner |
| 234 | + |
| 235 | +-------------------------------------------------------------------------------- |
| 236 | +-- Positive control: a concrete schedule equivalence witness |
| 237 | +-------------------------------------------------------------------------------- |
| 238 | + |
| 239 | +||| A concrete stage: double each pixel index value. |
| 240 | +public export |
| 241 | +double : Nat -> Nat |
| 242 | +double n = n + n |
| 243 | + |
| 244 | +||| POSITIVE CONTROL. Splitting an extent-12 domain into 3 outer blocks of |
| 245 | +||| 4 inner iterations is equivalent to running it flat. An inhabited |
| 246 | +||| witness — the proof obligation is discharged by the general theorem. |
| 247 | +public export |
| 248 | +doubleSplit3x4Equivalent : SplitEquivalent Semantics.double 3 4 |
| 249 | +doubleSplit3x4Equivalent = splitPreservesResult double 3 4 |
| 250 | + |
| 251 | +||| And the underlying lists are literally equal on this concrete case. |
| 252 | +public export |
| 253 | +doubleSplit3x4Concrete : runSplit Semantics.double 3 4 = runFlat Semantics.double 12 |
| 254 | +doubleSplit3x4Concrete = Refl |
| 255 | + |
| 256 | +-------------------------------------------------------------------------------- |
| 257 | +-- Negative control: a result-changing "schedule" is NOT equivalent |
| 258 | +-------------------------------------------------------------------------------- |
| 259 | + |
| 260 | +||| A FAKE schedule that drops the last block (executes only `outer-1` |
| 261 | +||| blocks). This models an unsound schedule that silently omits work. |
| 262 | +public export |
| 263 | +runSplitDropLast : (f : Nat -> Nat) -> (outer, inner : Nat) -> List Nat |
| 264 | +runSplitDropLast f outer inner = |
| 265 | + concatMap (runInner f inner) (range (pred outer)) |
| 266 | + |
| 267 | +||| NEGATIVE CONTROL. On the concrete extent-12 case, the work-dropping |
| 268 | +||| schedule does NOT reproduce the flat run: their output lists differ. |
| 269 | +||| Machine-checked: the two concrete lists are unequal. |
| 270 | +public export |
| 271 | +dropLastNotEquivalent : |
| 272 | + Not (runSplitDropLast Semantics.double 3 4 = runFlat Semantics.double 12) |
| 273 | +dropLastNotEquivalent eq = case eq of Refl impossible |
0 commit comments