2. Workload-Tuned BFP Placement
The Nano reset changes the question. The core is now small enough that the important design choice is no longer "can the processor route?" but "where do the BFP/MX scales live while real inference kernels run?" This chapter keeps that question executable: tiles, workloads, and BFP placements are ordinary Lean values, and the representative cases are checked when the book builds.
2.1. The model surface
The units are deliberately simple. Area is square micrometres. Bandwidths are bytes per cycle. Counts are integer MACs unless a field says bytes or cycles.
namespace Tilessa
namespace Workload
inductive DataMode where
| int8
| bfp8
| mx
deriving DecidableEq, Repr
/-- Nano core parameters that affect throughput scheduling. -/
structure NanoConfig where
registerCount : Nat
dataMode : DataMode
macsPerInstruction : Nat
loadLatency : Nat
deriving DecidableEq, Repr
/-- Physical tile parameters used by the workload model. -/
structure TileConfig where
peCount : Nat
sramBytes : Nat
bankCount : Nat
sramPortsPerBank : Nat
fabricEdgeBytesPerCycle : Nat
clockMHz : Nat
computeAreaUm2 : Nat
sramAreaUm2 : Nat
fabricAreaUm2 : Nat
boundaryBfpAreaUm2 : Nat
perBankBfpAreaUm2 : Nat
peLocalBfpAreaUm2 : Nat
deriving DecidableEq, Repr
inductive BfpPlacement where
| edgeOnly
| tileBoundary
| perBank
| peLocal
deriving DecidableEq, Repr
inductive WorkloadKind where
| flashPrefill
| flashDecode
| denseGemm
| mlpGelu
| kvCache
| scaledDot
deriving DecidableEq, Repr
/-- Workload accounting. `opCount` is useful integer MACs. -/
structure Workload where
kind : WorkloadKind
opCount : Nat
sramTrafficBytes : Nat
fabricTrafficBytes : Nat
scaleTrafficBytes : Nat
softmaxCycles : Nat
reductionCycles : Nat
deriving DecidableEq, Repr
def ceilDiv (n d : Nat) : Nat :=
if d = 0 then 0 else (n + d - 1) / d
def max3 (a b c : Nat) : Nat := Nat.max (Nat.max a b) c
The default is intentionally conservative: integer-only Nano PEs, scale metadata held in tile SRAM/fabric, and conversion at the tile boundary or mesh edge until measurements justify bringing more BFP logic inward.
def nanoI8 : NanoConfig where
registerCount := 4
dataMode := .int8
macsPerInstruction := 4
loadLatency := 1
def nanoBfpBoundary : NanoConfig where
registerCount := 4
dataMode := .bfp8
macsPerInstruction := 4
loadLatency := 1
def nanoMemTile : TileConfig where
peCount := 2
sramBytes := 2 * 1024
bankCount := 2
sramPortsPerBank := 1
fabricEdgeBytesPerCycle := 32
clockMHz := 95
computeAreaUm2 := 2 * 29038
sramAreaUm2 := 2 * 191444
fabricAreaUm2 := 32000
boundaryBfpAreaUm2 := 12000
perBankBfpAreaUm2 := 5000
peLocalBfpAreaUm2 := 3000
def nanoTile4 : TileConfig where
peCount := 4
sramBytes := 4 * 1024
bankCount := 4
sramPortsPerBank := 1
fabricEdgeBytesPerCycle := 64
clockMHz := 95
computeAreaUm2 := 4 * 29038
sramAreaUm2 := 4 * 191444
fabricAreaUm2 := 52000
boundaryBfpAreaUm2 := 16000
perBankBfpAreaUm2 := 5000
peLocalBfpAreaUm2 := 3000
/-- Current routed-but-not-clean sky130 `nano_tile4` observation.
The area and clock are calibration inputs from
`docs/physical-evidence-generated.md`: the 20 ns run reports 0.8782 mm² cell
area and an estimated 46.8 MHz setup-limited point. This is not a signoff-clean
frequency claim; it is the conservative point used to tune the next sweep. -/
def nanoTile4ObservedSky130 : TileConfig where
peCount := 4
sramBytes := 4 * 1024
bankCount := 4
sramPortsPerBank := 1
fabricEdgeBytesPerCycle := 64
clockMHz := 47
computeAreaUm2 := 115350
sramAreaUm2 := 762850
fabricAreaUm2 := 0
boundaryBfpAreaUm2 := 20600
perBankBfpAreaUm2 := 5000
peLocalBfpAreaUm2 := 3000
2.2. Workloads
The core set is small and aimed at the first tuning target: sustained TOPS/mm² on inference, especially FlashAttention. The formulas are not accuracy models; they are traffic and cycle-pressure models that say where the tile will stall.
def blockScales (values blockSize : Nat) : Nat :=
ceilDiv values blockSize
/-- FlashAttention prefill for one head: QK and P·V, with K/V resident. -/
def flashPrefill (tokens dim blockSize : Nat) : Workload where
kind := .flashPrefill
opCount := 2 * tokens * tokens * dim
sramTrafficBytes := 3 * tokens * dim
fabricTrafficBytes := tokens * tokens + tokens * dim
scaleTrafficBytes := 2 * blockScales (tokens * dim) blockSize
softmaxCycles := tokens * ceilDiv tokens 16
reductionCycles := 2 * tokens * ceilDiv dim 32
/-- FlashAttention decode for one new token against an existing KV cache. -/
def flashDecode (cacheTokens dim blockSize : Nat) : Workload where
kind := .flashDecode
opCount := 2 * cacheTokens * dim
sramTrafficBytes := 2 * cacheTokens * dim
fabricTrafficBytes := cacheTokens + dim
scaleTrafficBytes := 2 * blockScales (cacheTokens * dim) blockSize
softmaxCycles := ceilDiv cacheTokens 16
reductionCycles := 2 * ceilDiv dim 32
def denseGemm (m n k blockSize : Nat) : Workload where
kind := .denseGemm
opCount := m * n * k
sramTrafficBytes := m * k + k * n + 4 * m * n
fabricTrafficBytes := m * n
scaleTrafficBytes := blockScales (m * k) blockSize + blockScales (k * n) blockSize
softmaxCycles := 0
reductionCycles := m * n * ceilDiv k 32
def mlpGelu (tokens hidden expansion blockSize : Nat) : Workload where
kind := .mlpGelu
opCount := 2 * tokens * hidden * expansion
sramTrafficBytes := tokens * hidden + hidden * expansion + expansion * hidden
fabricTrafficBytes := tokens * expansion
scaleTrafficBytes :=
blockScales (hidden * expansion) blockSize
+ blockScales (expansion * hidden) blockSize
softmaxCycles := 0
reductionCycles := tokens * ceilDiv expansion 16
def kvCacheTraffic (tokens dim blockSize : Nat) : Workload where
kind := .kvCache
opCount := 0
sramTrafficBytes := 2 * tokens * dim
fabricTrafficBytes := 2 * tokens * dim
scaleTrafficBytes := 2 * blockScales (tokens * dim) blockSize
softmaxCycles := 0
reductionCycles := 0
2.3. Placement reports
Every BFP placement report carries the fields the architecture decision needs: extra tile area, scale bytes, conversion/requantization cycles, fabric impact, SRAM bank pressure, critical-path risk, and whether the PE proof remains the integer Nano proof.
structure BfpReport where
placement : BfpPlacement
extraAreaUm2PerTile : Nat
scaleMetadataBytes : Nat
conversionRequantCycles : Nat
fabricBandwidthBytes : Nat
sramBankPressurePermille : Nat
criticalPath : Bool
preservesSimplePeProof : Bool
deriving DecidableEq, Repr
def baseTileAreaUm2 (t : TileConfig) : Nat :=
t.computeAreaUm2 + t.sramAreaUm2 + t.fabricAreaUm2
def bfpExtraAreaUm2 (t : TileConfig) : BfpPlacement → Nat
| .edgeOnly => 0
| .tileBoundary => t.boundaryBfpAreaUm2
| .perBank => t.bankCount * t.perBankBfpAreaUm2
| .peLocal => t.peCount * t.peLocalBfpAreaUm2
def placementScaleBytes (t : TileConfig) (w : Workload) : BfpPlacement → Nat
| .edgeOnly => w.scaleTrafficBytes
| .tileBoundary => w.scaleTrafficBytes
| .perBank => w.scaleTrafficBytes + t.bankCount
| .peLocal => w.scaleTrafficBytes * t.peCount
def conversionCycles (t : TileConfig) (w : Workload) : BfpPlacement → Nat
| .edgeOnly => 2 * ceilDiv w.scaleTrafficBytes 2 + w.softmaxCycles
| .tileBoundary => ceilDiv w.scaleTrafficBytes 2 + ceilDiv w.reductionCycles 4
| .perBank => ceilDiv w.scaleTrafficBytes (Nat.max 1 t.bankCount)
| .peLocal => 0
def fabricBytesWithScales (t : TileConfig) (w : Workload) (p : BfpPlacement) : Nat :=
match p with
| .edgeOnly => w.fabricTrafficBytes + w.scaleTrafficBytes
| .tileBoundary => w.fabricTrafficBytes + ceilDiv w.scaleTrafficBytes 2
| .perBank => w.fabricTrafficBytes + placementScaleBytes t w p
| .peLocal => w.fabricTrafficBytes + placementScaleBytes t w p
def bankPressurePermille (t : TileConfig) (w : Workload) (p : BfpPlacement) : Nat :=
(w.sramTrafficBytes + placementScaleBytes t w p) * 1000
/ Nat.max 1 w.sramTrafficBytes
def onCriticalPath : BfpPlacement → Bool
| .edgeOnly => false
| .tileBoundary => false
| .perBank => false
| .peLocal => true
def simplePeProof : BfpPlacement → Bool
| .edgeOnly => true
| .tileBoundary => true
| .perBank => true
| .peLocal => false
def bfpReport (t : TileConfig) (w : Workload) (p : BfpPlacement) : BfpReport where
placement := p
extraAreaUm2PerTile := bfpExtraAreaUm2 t p
scaleMetadataBytes := placementScaleBytes t w p
conversionRequantCycles := conversionCycles t w p
fabricBandwidthBytes := fabricBytesWithScales t w p
sramBankPressurePermille := bankPressurePermille t w p
criticalPath := onCriticalPath p
preservesSimplePeProof := simplePeProof p
The model also gives a conservative sustained-density estimate. It takes the maximum of compute, SRAM, and fabric pressure, then adds the non-MAC softmax, reduction, and conversion cycles.
def macCycles (n : NanoConfig) (t : TileConfig) (w : Workload) : Nat :=
ceilDiv w.opCount (Nat.max 1 (t.peCount * n.macsPerInstruction))
def sramCycles (t : TileConfig) (w : Workload) : Nat :=
ceilDiv w.sramTrafficBytes (Nat.max 1 (t.bankCount * t.sramPortsPerBank * 4))
def fabricCycles (t : TileConfig) (r : BfpReport) : Nat :=
ceilDiv r.fabricBandwidthBytes (Nat.max 1 t.fabricEdgeBytesPerCycle)
def workloadCycles (n : NanoConfig) (t : TileConfig) (w : Workload)
(p : BfpPlacement) : Nat :=
let r := bfpReport t w p
max3 (macCycles n t w) (sramCycles t w) (fabricCycles t r)
+ w.softmaxCycles + w.reductionCycles + r.conversionRequantCycles
/-- Milli-TOPS/mm², counting one MAC as one operation for conservative ranking. -/
def sustainedMilliTopsPerMm2 (n : NanoConfig) (t : TileConfig)
(w : Workload) (p : BfpPlacement) : Nat :=
let cycles := workloadCycles n t w p
let area := baseTileAreaUm2 t + bfpExtraAreaUm2 t p
w.opCount * t.clockMHz * 1000 / Nat.max 1 (cycles * area)
2.4. Checked cases
The examples below are intentionally small enough to audit by eye, but they exercise the same formulas as the larger tuning sweeps.
def prefill4x8 : Workload := flashPrefill 4 8 32
def decode16x8 : Workload := flashDecode 16 8 32
def gemm4x4x8 : Workload := denseGemm 4 4 8 32
def prefill64x64 : Workload := flashPrefill 64 64 32
theorem prefill_case_checked :
prefill4x8.opCount = 256
∧ prefill4x8.sramTrafficBytes = 96
∧ (bfpReport nanoMemTile prefill4x8 .tileBoundary).scaleMetadataBytes = 2
∧ workloadCycles nanoBfpBoundary nanoMemTile prefill4x8 .tileBoundary = 47 := ⊢ prefill4x8.opCount = 256 ∧
prefill4x8.sramTrafficBytes = 96 ∧
(bfpReport nanoMemTile prefill4x8 BfpPlacement.tileBoundary).scaleMetadataBytes = 2 ∧
workloadCycles nanoBfpBoundary nanoMemTile prefill4x8 BfpPlacement.tileBoundary = 47
All goals completed! 🐙
theorem decode_case_checked :
decode16x8.opCount = 256
∧ decode16x8.sramTrafficBytes = 256
∧ (bfpReport nanoMemTile decode16x8 .tileBoundary).fabricBandwidthBytes = 28
∧ workloadCycles nanoBfpBoundary nanoMemTile decode16x8 .tileBoundary = 40 := ⊢ decode16x8.opCount = 256 ∧
decode16x8.sramTrafficBytes = 256 ∧
(bfpReport nanoMemTile decode16x8 BfpPlacement.tileBoundary).fabricBandwidthBytes = 28 ∧
workloadCycles nanoBfpBoundary nanoMemTile decode16x8 BfpPlacement.tileBoundary = 40
All goals completed! 🐙
theorem gemm_case_checked :
gemm4x4x8.opCount = 128
∧ gemm4x4x8.scaleTrafficBytes = 2
∧ workloadCycles nanoBfpBoundary nanoMemTile gemm4x4x8 .tileBoundary = 37 := ⊢ gemm4x4x8.opCount = 128 ∧
gemm4x4x8.scaleTrafficBytes = 2 ∧ workloadCycles nanoBfpBoundary nanoMemTile gemm4x4x8 BfpPlacement.tileBoundary = 37
All goals completed! 🐙
theorem default_placement_keeps_integer_pe_story :
(bfpReport nanoMemTile prefill4x8 .tileBoundary).criticalPath = false
∧ (bfpReport nanoMemTile prefill4x8 .tileBoundary).preservesSimplePeProof = true
∧ (bfpReport nanoMemTile prefill4x8 .peLocal).criticalPath = true
∧ (bfpReport nanoMemTile prefill4x8 .peLocal).preservesSimplePeProof = false := ⊢ (bfpReport nanoMemTile prefill4x8 BfpPlacement.tileBoundary).criticalPath = false ∧
(bfpReport nanoMemTile prefill4x8 BfpPlacement.tileBoundary).preservesSimplePeProof = true ∧
(bfpReport nanoMemTile prefill4x8 BfpPlacement.peLocal).criticalPath = true ∧
(bfpReport nanoMemTile prefill4x8 BfpPlacement.peLocal).preservesSimplePeProof = false
All goals completed! 🐙
theorem nano_tile4_observed_calibration :
nanoTile4ObservedSky130.clockMHz = 47
∧ baseTileAreaUm2 nanoTile4ObservedSky130 = 878200
∧ workloadCycles nanoBfpBoundary nanoTile4ObservedSky130
prefill64x64 .tileBoundary = 33472
∧ (bfpReport nanoTile4ObservedSky130 prefill64x64
.tileBoundary).extraAreaUm2PerTile = 20600 := ⊢ nanoTile4ObservedSky130.clockMHz = 47 ∧
baseTileAreaUm2 nanoTile4ObservedSky130 = 878200 ∧
workloadCycles nanoBfpBoundary nanoTile4ObservedSky130 prefill64x64 BfpPlacement.tileBoundary = 33472 ∧
(bfpReport nanoTile4ObservedSky130 prefill64x64 BfpPlacement.tileBoundary).extraAreaUm2PerTile = 20600
All goals completed! 🐙
The BFP arithmetic connection is the existing scaled_dot theorem from the
edge chapter, reused directly by this placement model. Scale handling can move
around the tile; the PE still computes the integer dot that theorem factors.
def sampleA (i : Nat) : Int := (i % 5 : Nat) - 2
def sampleB (i : Nat) : Int := (i % 7 : Nat) - 3
theorem bfp_scaled_dot_matches_boundary :
isum 32 (fun i => (2 * sampleA i) * (3 * sampleB i))
= (2 * 3) * isum 32 (fun i => sampleA i * sampleB i) := ⊢ (isum 32 fun i => 2 * sampleA i * (3 * sampleB i)) = 2 * 3 * isum 32 fun i => sampleA i * sampleB i
All goals completed! 🐙
end Workload
end Tilessa
The conservative result is therefore explicit: tile-boundary BFP carries a small area and cycle tax, keeps scale metadata out of the PE, keeps BFP off the MAC critical path, and preserves the simple Nano proof story. Edge-only remains the lowest-area baseline; PE-local BFP is reserved for evidence that the workload benefit outweighs the verification and timing cost.