code wiki / _hdl_build / nx_gate_energy_test.nx

nx_gate_energy_test.nx source

↩ module page · 211 lines · 9767 B

1// nx_gate_energy_test.nx -- the EFFICIENCY leg of the triangle, measured BITS-UP 2// from the silicon we built, not borrowed from an OS. This is the PORTABLE layer: 3// it runs identically on ANY hardware because it owes nothing to the host (no 4// RAPL, no /sys, no privilege) -- the energy is a property of the GATE DESIGN. 5// 6// THE GOOD TRIANGLE (operator's framing): 7// SPEED = critical-path gate DEPTH (signals ripple through the carry chain) 8// SIZE/AREA = gate COUNT (how much silicon the design occupies) 9// EFFICIENCY = switching ACTIVITY = gate-output TOGGLES per op. Dynamic energy 10// in real silicon is ~ (toggles * C * V^2): every gate output that 11// flips charges/discharges a capacitance and burns joules. Counting 12// toggles is exactly how chip designers estimate dynamic power. 13// CAPABILITY = 1:1 CORRECTNESS (the sum is right for every vector). 14// 15// CITATIONS (Cardinal #4 -- real sources): the dynamic-power law P = a*C*V^2*f and 16// switching-activity-based estimation are textbook -- Weste & Harris, "CMOS VLSI 17// Design" 4e; Najm, "A Survey of Power Estimation Techniques in VLSI Circuits," 18// IEEE Trans. VLSI Systems, 1994. Ripple-carry critical-path ~2W gate delays is 19// standard (Weste & Harris, adder chapter). The energy/time/memory tradeoff this 20// triangle measures is empirically grounded in Pereira et al., "Energy Efficiency 21// across Programming Languages," ACM SLE 2017 (the benchmarks-game-based study). 22// 23// We build a REAL ripple-carry adder out of PRIMITIVE 1-bit gates (XOR/AND/OR), 24// simulate it on consecutive input vectors, and count how many gate outputs flip. 25// Key physical truth this proves: ENERGY IS DATA-DEPENDENT. The same correct adder 26// burns more on random inputs (carries propagate, ~half the bits flip) than on 27// small increments (short carries, few flips). That is WHY a garden sensor must 28// MEASURE its real workload's energy, not assume it -- the companion real-hardware 29// probe (nx_energy_probe) reads the node's actual power sensor and calibrates THIS 30// toggle count into joules for whatever silicon it landed on. 31// 32// Known answer: correctness holds for all vectors AND random-workload toggles > 33// increment-workload toggles (data-dependent energy). exit 0. 34 35import "nx_nxgate_sim.nx" 36 37const GE_W: i64 = 8 // adder width (small + clear; the structure scales) 38const GE_N: i64 = 256 // vectors per workload 39 40// ---- net layout (one flat space) ---- 41// 0 : cin0 const (0) 42// 1 .. W : a bits | 1+W .. 2W : b bits (primary inputs) 43// base=1+2W .. : 5 internal nets per bit (axb, sum, aab, axbc, cout) 44func ge_anet(i: i64) -> i64 { return 1 + i } 45func ge_bnet(i: i64) -> i64 { return 1 + GE_W + i } 46func ge_base() -> i64 { return 1 + 2 * GE_W } 47func ge_axb(i: i64) -> i64 { return ge_base() + 5 * i + 0 } 48func ge_sum(i: i64) -> i64 { return ge_base() + 5 * i + 1 } 49func ge_aab(i: i64) -> i64 { return ge_base() + 5 * i + 2 } 50func ge_axbc(i: i64) -> i64 { return ge_base() + 5 * i + 3 } 51func ge_cout(i: i64) -> i64 { return ge_base() + 5 * i + 4 } 52func ge_cin(i: i64) -> i64 { if i == 0 { return 0 } return ge_cout(i - 1) } 53 54func ge_set(cells: *NxGsimCell, idx: i64, kind: i64, fo: i64, f0: i64, f1: i64, f2: i64, val: i64) -> i64 { 55 cells[idx].kind = kind 56 cells[idx].fanout = fo 57 cells[idx].f0 = f0 58 cells[idx].f1 = f1 59 cells[idx].f2 = f2 60 cells[idx].val = val 61 return 0 62} 63 64// Build a W-bit ripple-carry adder from primitive gates. Per full-adder bit: 65// axb = a^b ; sum = axb^cin ; aab = a&b ; axbc = axb&cin ; cout = aab|axbc 66// Emitted in topological order. Returns the cell count. 67func ge_build(cells: *NxGsimCell) -> i64 { 68 var idx: i64 = 0 69 ge_set(cells, idx, NX_GATE_KIND_CONST, 0, -1, -1, -1, 0); idx = idx + 1 // cin0 = 0 70 var i: i64 = 0 71 while i < GE_W { 72 let cin: i64 = ge_cin(i) 73 ge_set(cells, idx, NX_GATE_KIND_XOR, ge_axb(i), ge_anet(i), ge_bnet(i), -1, 0); idx = idx + 1 74 ge_set(cells, idx, NX_GATE_KIND_AND, ge_aab(i), ge_anet(i), ge_bnet(i), -1, 0); idx = idx + 1 75 ge_set(cells, idx, NX_GATE_KIND_XOR, ge_sum(i), ge_axb(i), cin, -1, 0); idx = idx + 1 76 ge_set(cells, idx, NX_GATE_KIND_AND, ge_axbc(i), ge_axb(i), cin, -1, 0); idx = idx + 1 77 ge_set(cells, idx, NX_GATE_KIND_OR, ge_cout(i), ge_aab(i), ge_axbc(i), -1, 0); idx = idx + 1 78 i = i + 1 79 } 80 return idx 81} 82 83func ge_set_inputs(g: *NxGsim, a: i64, b: i64) -> i64 { 84 var i: i64 = 0 85 while i < GE_W { 86 g.vals[ge_anet(i)] = (a >> i) & 1 87 g.vals[ge_bnet(i)] = (b >> i) & 1 88 i = i + 1 89 } 90 return 0 91} 92func ge_read_sum(g: *NxGsim) -> i64 { 93 var s: i64 = 0 94 var i: i64 = 0 95 while i < GE_W { s = s | ((g.vals[ge_sum(i)] & 1) << i); i = i + 1 } 96 return s 97} 98 99// Longest combinational path (critical depth) -- the SPEED axis. Primary inputs + 100// const are depth 0 (mmap zero-inits); each gate is 1 + max(fanin depths). 101func ge_depth(g: *NxGsim, cells: *NxGsimCell, ncells: i64) -> i64 { 102 let dep: *i64 = sys_mmap(8 * g.n_nets) as *i64 103 var i: i64 = 0 104 while i < ncells { 105 var d: i64 = 0 106 let f0: i64 = cells[i].f0 107 let f1: i64 = cells[i].f1 108 let f2: i64 = cells[i].f2 109 if f0 >= 0 { if dep[f0] > d { d = dep[f0] } } 110 if f1 >= 0 { if dep[f1] > d { d = dep[f1] } } 111 if f2 >= 0 { if dep[f2] > d { d = dep[f2] } } 112 if cells[i].kind != NX_GATE_KIND_CONST { d = d + 1 } 113 dep[cells[i].fanout] = d 114 i = i + 1 115 } 116 var crit: i64 = 0 117 i = 0 118 while i < GE_W { if dep[ge_sum(i)] > crit { crit = dep[ge_sum(i)] } i = i + 1 } 119 if dep[ge_cout(GE_W - 1)] > crit { crit = dep[ge_cout(GE_W - 1)] } 120 return crit 121} 122 123// snapshot each cell's output net value (1 bit) into snap[cell_index] 124func ge_snapshot(g: *NxGsim, cells: *NxGsimCell, ncells: i64, snap: *i64) -> i64 { 125 var i: i64 = 0 126 while i < ncells { snap[i] = g.vals[cells[i].fanout]; i = i + 1 } 127 return 0 128} 129// gate-output flips between prev snapshot and current vals = switching activity 130func ge_toggles(g: *NxGsim, cells: *NxGsimCell, ncells: i64, prev: *i64) -> i64 { 131 var t: i64 = 0 132 var i: i64 = 0 133 while i < ncells { if g.vals[cells[i].fanout] != prev[i] { t = t + 1 } i = i + 1 } 134 return t 135} 136 137func ge_rng(s: i64) -> i64 { return s * 6364136223846793005 + 1442695040888963407 } 138 139// Run one workload (mode 0 = small increments / mode 1 = random) over n vectors. 140// Returns total switching activity, OR a negative sentinel on a correctness miss. 141func ge_run_workload(g: *NxGsim, cells: *NxGsimCell, ncells: i64, mode: i64, n: i64, mask: i64, prev: *i64) -> i64 { 142 var total: i64 = 0 143 var s: i64 = 88172645463325252 144 var k: i64 = 0 145 while k < n { 146 var a: i64 = 0 147 var b: i64 = 0 148 if mode == 0 { a = k & mask; b = 1 } 149 if mode == 1 { s = ge_rng(s); a = (s >> 33) & mask; s = ge_rng(s); b = (s >> 33) & mask } 150 ge_set_inputs(g, a, b) 151 if nx_gsim_run(g) != NX_GSIM_OK { return 0 - 1 } 152 let got: i64 = ge_read_sum(g) 153 let want: i64 = (a + b) & mask 154 if got != want { return 0 - (k + 2) } // CAPABILITY fail 155 if k > 0 { total = total + ge_toggles(g, cells, ncells, prev) } 156 ge_snapshot(g, cells, ncells, prev) 157 k = k + 1 158 } 159 return total 160} 161 162func _emit(name: *u8, v: i64) -> i64 { 163 var n: i64 = 0; while name[n] != (0 as u8) { n = n + 1 } sys_write(1, name, n) 164 let b: *u8 = sys_mmap(28); var m: i64 = v; if m < 0 { m = 0 - m } 165 let t: *u8 = sys_mmap(28); var k: i64 = 0 166 if m == 0 { t[0] = 48; k = 1 } 167 while m > 0 { t[k] = 48 + (m % 10); m = m / 10; k = k + 1 } 168 var i: i64 = 0; while i < k { b[i] = t[k - 1 - i]; i = i + 1 } 169 b[k] = 10; sys_write(1, b, k + 1); return 0 170} 171 172func main() -> i64 { 173 let ncells_cap: i64 = 1 + 5 * GE_W 174 let nnets: i64 = 1 + 7 * GE_W 175 let cells: *NxGsimCell = sys_mmap(ncells_cap * 48) as *NxGsimCell 176 let vals: *i64 = sys_mmap(8 * nnets) as *i64 177 let ncells: i64 = ge_build(cells) 178 179 let g: *NxGsim = sys_mmap(32) as *NxGsim 180 g.vals = vals 181 g.n_nets = nnets 182 g.cells = cells 183 g.n_cells = ncells 184 185 let mask: i64 = (1 << GE_W) - 1 186 let prev: *i64 = sys_mmap(8 * ncells) as *i64 187 188 sys_write(1, "=== BITS-UP EFFICIENCY TRIANGLE (portable: any hardware, no OS) ===\n" as *u8, 67) 189 _emit("adder width (bits) : " as *u8, GE_W) 190 _emit("SIZE gates (area) : " as *u8, ncells) 191 let depth: i64 = ge_depth(g, cells, ncells) 192 _emit("SPEED critical depth : " as *u8, depth) 193 194 // EFFICIENCY: two workloads, same correct adder, different energy. 195 let tlow: i64 = ge_run_workload(g, cells, ncells, 0, GE_N, mask, prev) 196 let thigh: i64 = ge_run_workload(g, cells, ncells, 1, GE_N, mask, prev) 197 if tlow < 0 { _emit("CAPABILITY FAIL (increment vec) at " as *u8, 0 - tlow); sys_exit(2); return 2 } 198 if thigh < 0 { _emit("CAPABILITY FAIL (random vec) at " as *u8, 0 - thigh); sys_exit(3); return 3 } 199 200 sys_write(1, "CAPABILITY correctness : 1:1 for all vectors\n" as *u8, 45) 201 _emit("ENERGY toggles incr : " as *u8, tlow) 202 _emit("ENERGY toggles random : " as *u8, thigh) 203 _emit(" avg toggles/op incr : " as *u8, tlow / (GE_N - 1)) 204 _emit(" avg toggles/op random: " as *u8, thigh / (GE_N - 1)) 205 sys_write(1, " -> energy is DATA-DEPENDENT: measure the real workload, never assume.\n" as *u8, 71) 206 sys_write(1, " -> nx_energy_probe calibrates these toggles into joules on real silicon.\n" as *u8, 75) 207 208 // GATE: correct AND random burns more than increment (data-dependent energy). 209 if thigh <= tlow { sys_exit(4); return 4 } 210 sys_exit(0); return 0 211}