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}