summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorYuren Hao <yurenh2@illinois.edu>2026-07-17 16:01:23 -0500
committerYuren Hao <yurenh2@illinois.edu>2026-07-17 16:01:23 -0500
commit98935725be4e33bd4e0f6830bf56e180fd1c98ed (patch)
treeddb3b5f6b0280b1a600c2bb0de792a62d025c8cd
parent7ae978808165d29019d8cb814dbbbb6246e82805 (diff)
Energy ledger: SPICE core 2.87pJ/MAC; boards lose ~1000x (trainability demos); coherent integrated projection 0.21-0.63 pJ/MAC = parity-to-5x vs digital, ADC-dominated — honest range, not CIM marketing
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014FAPDWQ49M5Ye3NpTndTpn
-rw-r--r--hw_sim/NOTES.md17
-rw-r--r--hw_sim/energy_ledger.py116
2 files changed, 133 insertions, 0 deletions
diff --git a/hw_sim/NOTES.md b/hw_sim/NOTES.md
index 2bac023..eee68c6 100644
--- a/hw_sim/NOTES.md
+++ b/hw_sim/NOTES.md
@@ -67,3 +67,20 @@ differential resistive columns with: per-bit ladder mismatch (fixed device), per
NEXT candidates (autonomy line): scale toy to 32x32 (overnight class); add settle-transient
into the loop (replace DC solves at the found (R_f,C_f) point) to couple timing and training;
port the ride/beta story onto the circuit noise floor (nudge amplitude sweep vs 139 uV).
+
+## Energy ledger (energy_ledger.py, 2026-07-17): SPICE core + datasheet periphery vs digital
+SPICE-measured analog network core: 2.87 pJ/MAC (resistive burn over the 4.2 us discrete dwell).
+| scenario | pJ/MAC (wiring 1-3x band) | vs digital INT8 system 0.3-1 pJ/MAC |
+|---|---|---|
+| MVP discrete parts | 487-1462 | loses ~1000x (op-amp quiescent x long dwell + discrete ADC) |
+| T64 word-streaming | 587-1562 | loses ~1000x (+ reload/DRAM tax) |
+| Integrated weight-stationary (coherent: C/100 -> 100 ns dwell) | 0.21-0.63 | **0.5x-4.8x: parity to ~5x win, ADC-dominated** |
+HONEST CONCLUSIONS:
+1. The boards (MVP/T64) are trainability demos, never efficiency demos — say it before referees do.
+2. The integrated projection at 8-bit lands at PARITY-TO-5x, not the 10-100x of CIM marketing;
+ the residual is the ADC tax. Paths beyond: fewer/narrower reads, analog inter-layer
+ accumulation, low-precision contrast reads.
+3. EP's energy contribution is CATEGORICAL, not per-MAC: it makes TRAINING possible on analog
+ fabric at all (inference-only CIM can't train; digital training is the displaced baseline).
+4. Method: analog side SPICE-measured (+-2-3x wiring band), digital side literature constants
+ (Horowitz/H100 envelope) — the standard comparison protocol, uncertainty stated.
diff --git a/hw_sim/energy_ledger.py b/hw_sim/energy_ledger.py
new file mode 100644
index 0000000..89ab256
--- /dev/null
+++ b/hw_sim/energy_ledger.py
@@ -0,0 +1,116 @@
+"""Energy ledger: EP-analog vs digital training, three scenarios with uncertainty bands.
+Analog core measured by SPICE (v1 column transient, integrated V*I over settle+read);
+peripheral and digital terms from datasheet/literature constants (stated inline).
+Scenarios: (1) MVP discrete parts, (2) T64 word-streaming, (3) integrated weight-stationary
+projection. Output: pJ/MAC table + bar figure with uncertainty bands.
+"""
+import os
+os.environ.setdefault('NGSPICE_LIBRARY_PATH', '/home/yurenh2/miniconda3/lib/libngspice.so')
+import numpy as np
+import matplotlib
+matplotlib.use('Agg')
+import matplotlib.pyplot as plt
+from PySpice.Spice.Netlist import Circuit, SubCircuit
+
+N = 64
+R_EQ = 1.3 * 11e3
+C_BUS = 64 * 85e-12
+RF, CF = 1e3, 294e-12
+V_STEP = 0.1
+T_SETTLE = 3.2e-6
+T_READ = 1.0e-6
+DWELL = T_SETTLE + T_READ
+
+class OpAmp(SubCircuit):
+ NODES = ('inp', 'inn', 'out')
+ def __init__(self, name):
+ super().__init__(name, *self.NODES)
+ self.B('gain', 'x', self.gnd, v='1e5*(v(inp)-v(inn))')
+ rp = 1e6
+ cp = 1.0 / (2 * np.pi * 100.0 * rp)
+ self.R('p', 'x', 'p1', rp); self.C('p', 'p1', self.gnd, cp)
+ self.B('buf', 'o', self.gnd, v='v(p1)')
+ self.R('out', 'o', 'out', 25)
+
+c = Circuit('energy_col')
+c.subcircuit(OpAmp('opamp'))
+for i in range(N):
+ c.PulseVoltageSource(f'in{i}', f'n{i}', c.gnd, initial_value=0, pulsed_value=V_STEP,
+ delay_time=0.2e-6, rise_time=50e-9, fall_time=50e-9,
+ pulse_width=1, period=2)
+ c.R(f'w{i}', f'n{i}', 'sum', R_EQ)
+c.C('bus', 'sum', c.gnd, C_BUS)
+c.R('f', 'out', 'sum', RF); c.C('f', 'out', 'sum', CF)
+c.X('amp', 'opamp', c.gnd, 'sum', 'out')
+sim = c.simulator(temperature=27, nominal_temperature=27)
+an = sim.transient(step_time=4e-9, end_time=0.2e-6 + DWELL)
+t = np.array(an.time)
+# source-delivered power: sum_i V_i * I(V_i); ngspice gives branch currents of V sources
+p_src = np.zeros_like(t)
+for i in range(N):
+ try:
+ ib = np.array(an[f'vin{i}']) # current through source (A, into +)
+ except Exception:
+ ib = np.array(an[f'v.vin{i}#branch'])
+ p_src += V_STEP * np.abs(ib)
+E_network = float(np.trapz(p_src, t)) # J per column read (resistive + C charging)
+E_per_col_net = E_network
+print(f'SPICE: analog network energy per column read = {E_per_col_net*1e9:.3f} nJ '
+ f'({E_per_col_net/N*1e12:.2f} pJ/MAC)')
+
+# ---------------- ledger constants (stated assumptions) ----------------
+# discrete parts:
+P_OPAMP_DISC = 5e-3 # MCP6022 1 mA x 5 V quiescent, per column amp
+E_ADC_DISC = 10e-9 # AD7606-class per 16-bit conversion (~100 mW / 8ch / 1 MSPS class)
+E_DAC_RELOAD = 8 * 10e-12 # 8-bit latch write, ~10 pJ/bit I/O (word-streaming, per cell per use)
+E_DRAM_BYTE = 20e-12 # LPDDR-class streaming, per byte
+# integrated projection:
+P_OPAMP_INT = 10e-6 # integrated column amp
+E_ADC_INT = 0.5e-12 * 16 # ~0.5 pJ/conv-bit SAR class
+# digital reference (system-level, INT8):
+E_DIG_LOW, E_DIG_HIGH = 0.3e-12, 1.0e-12 # pJ/MAC incl movement, H100-class system envelope
+
+def scenario(name, e_net_percol, p_amp, e_adc, e_stream_percell, wiring_lo=1.0, wiring_hi=3.0,
+ dwell=DWELL):
+ # per column-read: network + amp*dwell + one ADC conversion; per MAC = /N; plus streaming/cell
+ e_core = e_net_percol * (dwell / DWELL) + p_amp * dwell + e_adc
+ per_mac_lo = (e_core * wiring_lo) / N + e_stream_percell
+ per_mac_hi = (e_core * wiring_hi) / N + e_stream_percell
+ # EP training step = 2 settles (free+nudged) + transpose read ~ 3 column ops per MAC-use
+ tr_lo, tr_hi = 3 * per_mac_lo, 3 * per_mac_hi
+ # digital training step = 3x MACs (fwd+bwd) at system energy
+ dig_lo, dig_hi = 3 * E_DIG_LOW, 3 * E_DIG_HIGH
+ ratio_best = dig_hi / tr_lo; ratio_worst = dig_lo / tr_hi
+ print(f'{name:28s} per-MAC {per_mac_lo*1e12:8.2f}-{per_mac_hi*1e12:8.2f} pJ | '
+ f'train-step vs digital: {ratio_worst:6.2f}x - {ratio_best:6.2f}x '
+ f'({">1 = analog wins" if ratio_best > 1 else "loses"})')
+ return per_mac_lo, per_mac_hi, ratio_worst, ratio_best
+
+print('\n=== pJ/MAC and EP-vs-digital-training energy ratio (range = wiring 1-3x + envelope) ===')
+s1 = scenario('MVP discrete parts', E_per_col_net, P_OPAMP_DISC, E_ADC_DISC, 0.0)
+s2 = scenario('T64 word-streaming', E_per_col_net, P_OPAMP_DISC, E_ADC_DISC,
+ E_DAC_RELOAD + 1 * E_DRAM_BYTE)
+# integrated: C_bus ~ 50 fF/cell -> settle ns-class; coherent dwell 100 ns (settle+read)
+s3 = scenario('Integrated weight-stationary', E_per_col_net, P_OPAMP_INT, E_ADC_INT, 0.0,
+ dwell=100e-9)
+
+# ---------------- figure ----------------
+names = ['MVP\n(discrete)', 'T64\n(word-stream)', 'Integrated\n(weight-stationary)']
+los = [s1[0], s2[0], s3[0]]; his = [s1[1], s2[1], s3[1]]
+fig, ax = plt.subplots(figsize=(8.8, 4.8))
+xs = np.arange(3)
+mid = [(a * b) ** 0.5 for a, b in zip(los, his)]
+ax.bar(xs, [m * 1e12 for m in mid], yerr=[[(m - l) * 1e12 for m, l in zip(mid, los)],
+ [(h - m) * 1e12 for h, m in zip(his, mid)]],
+ color=['#b03a2e', '#d95f02', '#2e7d32'], alpha=0.85, capsize=6)
+ax.axhspan(E_DIG_LOW * 1e12, E_DIG_HIGH * 1e12, color='#2c6fbb', alpha=0.18)
+ax.text(2.35, E_DIG_HIGH * 1e12 * 1.1, 'digital INT8 system\n0.3–1 pJ/MAC', fontsize=8.5,
+ color='#2c6fbb', ha='right')
+ax.set_yscale('log')
+ax.set_xticks(xs); ax.set_xticklabels(names)
+ax.set_ylabel('pJ per MAC (log)')
+ax.set_title('EP-analog energy per MAC — SPICE-measured core + datasheet periphery\n'
+ '(bands: schematic-vs-layout wiring 1–3×)')
+fig.tight_layout()
+fig.savefig('/home/yurenh2/ept/assets/figs/fig_energy_ledger.png', dpi=150)
+print('DONE_ENERGY')