summaryrefslogtreecommitdiff
path: root/hw_sim/energy_ledger.py
blob: 89ab256cc81ac2132a2864a3402e426699c9dec4 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
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')