code wiki / _hdl_build / nx_latency_metric.nx
nx_latency_metric.nx source
↩ module page · 148 lines · 7252 B
1// nx_latency_metric.nx -- the racing-crew's HONEST latency-metric organ.
2//
3// Given a built NxGsim word-level netlist (topologically ordered cells, each a
4// kind with up to 3 fanin nets), compute the CRITICAL PATH = the longest
5// dependency chain of cells from a primary input to an output. Two metrics:
6//
7// (1) UNIT depth -- every non-CONST cell costs 1. This is the cell-DEPTH the
8// functional sim (nx_nxgate_sim) implicitly uses: it assigns depth-1 to
9// every word-level cell, so a 64x64 MUL looks exactly as cheap as a 1-bit
10// AND. That is the HONEST-METRIC TRAP, and the unit metric is the trap.
11//
12// (2) HONEST depth -- every cell costs its per-kind logic depth, weighted by
13// the operand width W (data-driven, NOT hardcoded per call site):
14// CONST / wire = 0 (no logic)
15// AND/OR/XOR/NOT/NAND/... = 1 (one gate level)
16// MUX = 1 (one 2:1 select level)
17// ADD/SUB = ceil(log2 W) (carry-propagate / prefix adder)
18// SHL/SHR/SAR = ceil(log2 W) (barrel shifter, log stages)
19// EQ/NEQ/LT/LTU/GE/GEU = ceil(log2 W) (tree comparator)
20// MUL = 2*ceil(log2 W) (Wallace/Dadda PP-reduction
21// tree + final CPA -- the
22// DEEPEST operator)
23// So the honest metric does NOT treat a multiplier as depth-1: a MUL costs
24// 2*log(W) gate levels, an ADD costs log(W), a boolean costs 1. This is the
25// metric on which any LATENCY claim about a Newton (log(W) multiplies)
26// vs a radix-2 (W subtracts) divider must be stated -- because a multiply
27// being "one cell" in the sim is exactly the self-deception we must avoid.
28//
29// METRIC HONESTY (stated, per the racing doctrine): these weights are a STRUCTURAL
30// gate-LEVEL model (counts of logic levels), not measured silicon nanoseconds and
31// not a placed-and-routed delay. They are a faithful relative ordering of operator
32// depth, sized by W, and they are DATA-DRIVEN here so a future PDK-calibrated table
33// drops in without touching the traversal. The traversal itself is exact: it is the
34// true longest weighted path through the DAG.
35//
36// Reuses nx_nxgate_sim's NxGsim/NxGsimCell structs + the NX_GATE_KIND_* enum
37// (single source of truth -- DRY). Pure read-only analysis: it never mutates the
38// netlist, only walks it. license_tier: ORIGINAL
39
40import "nx_nxgate_sim.nx"
41
42const NX_LAT_OK: i64 = 0
43const NX_LAT_BAD_KIND: i64 = 1
44
45// ceil(log2(w)) for w >= 1. log2_ceil(1)=0, (2)=1, (3)=2, (4)=2, (16)=4, (64)=6.
46// This is the depth of a balanced reduction/prefix tree over w bits -- the honest
47// floor on a carry-propagate adder, a comparator tree, and a barrel shifter.
48func nx_lat_log2_ceil(w: i64) -> i64 {
49 if w <= 1 { return 0 }
50 var p: i64 = 0
51 var v: i64 = 1
52 while v < w { v = v << 1; p = p + 1 }
53 return p
54}
55
56// Per-kind honest logic DEPTH (gate levels), sized by operand width W.
57// Data-driven: the only place a weight lives. A future PDK table replaces this
58// body without touching the critical-path walk. FAIL LOUD on an unknown kind
59// (returns a negative sentinel) so the caller can refuse to fabricate a number.
60func nx_lat_kind_weight(kind: i64, w: i64) -> i64 {
61 // wire-only / no logic
62 if kind == NX_GATE_KIND_CONST { return 0 }
63 // single gate level (boolean + a 2:1 mux)
64 if kind == NX_GATE_KIND_AND { return 1 }
65 if kind == NX_GATE_KIND_OR { return 1 }
66 if kind == NX_GATE_KIND_NOT { return 1 }
67 if kind == NX_GATE_KIND_XOR { return 1 }
68 if kind == NX_GATE_KIND_NAND { return 1 }
69 if kind == NX_GATE_KIND_NOR { return 1 }
70 if kind == NX_GATE_KIND_XNOR { return 1 }
71 if kind == NX_GATE_KIND_MUX { return 1 }
72 // log-depth operators (carry-propagate adder, barrel shifter, tree comparator)
73 let lg: i64 = nx_lat_log2_ceil(w)
74 if kind == NX_GATE_KIND_ADD { return lg }
75 if kind == NX_GATE_KIND_SUB { return lg }
76 if kind == NX_GATE_KIND_SHL { return lg }
77 if kind == NX_GATE_KIND_SHR { return lg }
78 if kind == NX_GATE_KIND_SAR { return lg }
79 if kind == NX_GATE_KIND_EQ { return lg }
80 if kind == NX_GATE_KIND_NEQ { return lg }
81 if kind == NX_GATE_KIND_LT { return lg }
82 if kind == NX_GATE_KIND_LTU { return lg }
83 if kind == NX_GATE_KIND_GE { return lg }
84 if kind == NX_GATE_KIND_GEU { return lg }
85 // the DEEPEST operator: a multiplier = PP-reduction tree (~log W) + final CPA
86 // (~log W). 2*log(W). This is the cell that the unit metric LIES about.
87 if kind == NX_GATE_KIND_MUL { return 2 * lg }
88 return 0 - NX_LAT_BAD_KIND // unknown kind -> LOUD sentinel
89}
90
91// Core critical-path walk, parameterised by a weight mode + width.
92// mode 0 -> UNIT (every non-CONST cell = 1; CONST = 0); w is ignored.
93// mode 1 -> HONEST (per-kind, width-weighted by w).
94// depth[] is a caller-allocated *i64 of size >= g.n_nets; on return depth[net] is
95// the longest weighted path that produces that net (primary inputs = 0). Returns
96// the maximum over all nets = the network's critical-path latency on that metric,
97// or 0 - NX_LAT_BAD_KIND (LOUD) if any cell has an unknown kind.
98//
99// The cells are in topological order (the synth emitters build sub-results before
100// consumers), so a single forward pass is an exact longest-path DAG relaxation:
101// when we reach a cell, every fanin net's depth is already final.
102func nx_lat_critical_path(g: *NxGsim, depth: *i64, mode: i64, w: i64) -> i64 {
103 // primary-input + uninitialised nets start at depth 0
104 var n: i64 = 0
105 while n < g.n_nets { depth[n] = 0; n = n + 1 }
106
107 var best: i64 = 0
108 var i: i64 = 0
109 while i < g.n_cells {
110 let k: i64 = g.cells[i].kind
111 if nx_gsim_kind_supported(k) != 1 { return 0 - NX_LAT_BAD_KIND }
112
113 // longest fanin depth feeding this cell
114 var fan: i64 = 0
115 let f0: i64 = g.cells[i].f0
116 let f1: i64 = g.cells[i].f1
117 let f2: i64 = g.cells[i].f2
118 if f0 >= 0 { if depth[f0] > fan { fan = depth[f0] } }
119 if f1 >= 0 { if depth[f1] > fan { fan = depth[f1] } }
120 if f2 >= 0 { if depth[f2] > fan { fan = depth[f2] } }
121
122 // this cell's own cost on the chosen metric
123 var cost: i64 = 1
124 if mode == 0 {
125 if k == NX_GATE_KIND_CONST { cost = 0 }
126 }
127 if mode != 0 {
128 cost = nx_lat_kind_weight(k, w)
129 if cost < 0 { return 0 - NX_LAT_BAD_KIND } // unknown kind, LOUD
130 }
131
132 let d: i64 = fan + cost
133 depth[g.cells[i].fanout] = d
134 if d > best { best = d }
135 i = i + 1
136 }
137 return best
138}
139
140// Convenience wrappers. The honest model is parameterised by the datapath operand
141// width W (a 64-bit MUL is deeper than a 16-bit MUL), so W is a named argument, not
142// a magic number buried in the walk -- the demo passes it per network it measures.
143func nx_lat_unit(g: *NxGsim, depth: *i64) -> i64 {
144 return nx_lat_critical_path(g, depth, 0, 0)
145}
146func nx_lat_honest(g: *NxGsim, depth: *i64, w: i64) -> i64 {
147 return nx_lat_critical_path(g, depth, 1, w)
148}