CharlesCNorton commited on
Commit ·
6a6ad5c
1
Parent(s): 4bb9078
neural_reflect: universal netlist interpreter with indexed addressing, two banks, and a compiler from family nets
Browse files- README.md +53 -41
- src/reflect.py +591 -381
- variants/neural_reflect.safetensors +2 -2
README.md
CHANGED
|
@@ -53,10 +53,10 @@ product followed by thresholding, exactly what a resistive or photonic
|
|
| 53 |
crossbar computes. `neural_subleq8io` hosts a universal constructor: a
|
| 54 |
program that reads a description of any machine in the family and prints that
|
| 55 |
machine's weight file byte for byte, self-reproduction being the case where
|
| 56 |
-
the description is its own. And `neural_reflect`
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
|
| 61 |
---
|
| 62 |
|
|
@@ -505,48 +505,60 @@ weights and checked to the byte.
|
|
| 505 |
## neural_reflect — an interpreter whose state holds its own weights
|
| 506 |
|
| 507 |
Every machine above has a fixed weight tensor: the weights define the machine,
|
| 508 |
-
the state is what it operates on, and the two are disjoint. `neural_reflect`
|
| 509 |
-
|
| 510 |
-
|
| 511 |
-
|
| 512 |
-
|
| 513 |
-
|
| 514 |
-
|
| 515 |
-
|
| 516 |
-
|
| 517 |
-
|
| 518 |
-
|
| 519 |
-
|
| 520 |
-
|
| 521 |
-
|
| 522 |
-
|
| 523 |
-
|
| 524 |
-
|
| 525 |
-
|
| 526 |
-
|
| 527 |
-
|
| 528 |
-
|
| 529 |
-
- **
|
| 530 |
-
|
| 531 |
-
|
| 532 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 533 |
|
| 534 |
Verified against a reference interpreter: exhaustive single-gate semantics
|
| 535 |
(every ternary weight pair, bias, and input assignment), single-step agreement
|
| 536 |
-
on
|
| 537 |
-
of
|
| 538 |
-
|
| 539 |
-
|
| 540 |
-
|
| 541 |
|
| 542 |
```bash
|
| 543 |
-
python src/reflect.py
|
| 544 |
-
python src/reflect.py
|
| 545 |
```
|
| 546 |
|
| 547 |
-
The
|
| 548 |
-
|
| 549 |
-
transforms are the same
|
|
|
|
| 550 |
|
| 551 |
---
|
| 552 |
|
|
@@ -736,8 +748,8 @@ src/ the library (run scripts as `python src/<nam
|
|
| 736 |
│ exhaustive equality suite and the analog crossbar simulation
|
| 737 |
├── constructor8.py the neural_subleq8io host, the recipe codec, and the universal
|
| 738 |
│ constructor / self-reproduction suite
|
| 739 |
-
└── reflect.py neural_reflect: a threshold interpreter whose state
|
| 740 |
-
own gate table;
|
| 741 |
tools/ build_all.py (build + quantize + verify every profile),
|
| 742 |
cpu_programs.py (assembler + CPU program suite), test_cpu.py
|
| 743 |
(program suite vs a variant), play.py (interactive demo),
|
|
|
|
| 53 |
crossbar computes. `neural_subleq8io` hosts a universal constructor: a
|
| 54 |
program that reads a description of any machine in the family and prints that
|
| 55 |
machine's weight file byte for byte, self-reproduction being the case where
|
| 56 |
+
the description is its own. And `neural_reflect` is a universal interpreter
|
| 57 |
+
over ternary threshold netlists: its fixed transition circuit evaluates a
|
| 58 |
+
netlist held in the writable state, so the machine it runs is stored data that
|
| 59 |
+
a program reads, edits, and reproduces.
|
| 60 |
|
| 61 |
---
|
| 62 |
|
|
|
|
| 505 |
## neural_reflect — an interpreter whose state holds its own weights
|
| 506 |
|
| 507 |
Every machine above has a fixed weight tensor: the weights define the machine,
|
| 508 |
+
the state is what it operates on, and the two are disjoint. `neural_reflect` is
|
| 509 |
+
a universal interpreter that holds the netlist it evaluates in its own writable
|
| 510 |
+
state. The fixed part is a ternary threshold circuit U (12,993 gates, 50
|
| 511 |
+
ternary matrices); everything machine-specific is stored data that the running
|
| 512 |
+
program can read, edit, copy, and replace.
|
| 513 |
+
|
| 514 |
+
The state is a flat array of 1,024 signals: a pointer register, memory-mapped
|
| 515 |
+
device signals (output stream, pointer advance, bank select, halt) of the kind
|
| 516 |
+
`neural_rv32` and `neural_subleq8io` already use, working signals, and two
|
| 517 |
+
netlist banks. Each stored gate record holds two input slots (address, ternary
|
| 518 |
+
weight, index flag), a signed bias, and an output (address, index flag). One
|
| 519 |
+
recurrence evaluates gate `gp` of the active bank:
|
| 520 |
+
|
| 521 |
+
```
|
| 522 |
+
eff(addr, idx) = (addr + (idx ? PTR : 0)) mod S
|
| 523 |
+
out = H( w0·sig[eff(a0,i0)] + w1·sig[eff(a1,i1)] + bias ) ; sig[eff(oa,oidx)] = out
|
| 524 |
+
```
|
| 525 |
+
|
| 526 |
+
`G` recurrences sweep the netlist once; `G` sweeps settle any acyclic netlist
|
| 527 |
+
regardless of gate order, so U reproduces the family's combinational evaluation.
|
| 528 |
+
|
| 529 |
+
- **Universality.** `compile_to_reflect` maps any ≤2-input family `Net` to a
|
| 530 |
+
stored netlist. The family's own full-adder cell, compiled this way, is
|
| 531 |
+
interpreted bit-exact, and stays correct with its gates stored in arbitrary
|
| 532 |
+
order.
|
| 533 |
+
- **Self-reference.** Addresses index the whole signal array, so a gate can
|
| 534 |
+
write into the netlist region: a stored gate edits the weights that define
|
| 535 |
+
it. The index flag adds `PTR`, so a fixed-size program walks an address
|
| 536 |
+
range; one such program streams the netlist's own bytes to the output
|
| 537 |
+
device, emitting its own description. Another copies its bank into the
|
| 538 |
+
second bank and sets the `BANK` flag, so the interpreter runs a different
|
| 539 |
+
resident machine without an external load step.
|
| 540 |
+
- **Physical reading.** One recurrence is one matrix-vector product followed by
|
| 541 |
+
a threshold; each write is one bit of the state. On a memristive crossbar
|
| 542 |
+
that bit is a conductance, so a program editing the netlist region is the
|
| 543 |
+
computation editing the array's own weights.
|
| 544 |
|
| 545 |
Verified against a reference interpreter: exhaustive single-gate semantics
|
| 546 |
(every ternary weight pair, bias, and input assignment), single-step agreement
|
| 547 |
+
on random full states with halted states as fixed points, bit-exact
|
| 548 |
+
interpretation of the full-adder cell under sorted and shuffled gate order, the
|
| 549 |
+
self-reproduction and bank-replacement programs, and equality of the compiled
|
| 550 |
+
matrix form with the gate graph, the 0.5 comparator margin measured under read
|
| 551 |
+
noise and conductance mismatch.
|
| 552 |
|
| 553 |
```bash
|
| 554 |
+
python src/reflect.py verify # universality, order independence, quine, metamorphosis
|
| 555 |
+
python src/reflect.py analog # matrix == gate + noise / conductance-mismatch margin
|
| 556 |
```
|
| 557 |
|
| 558 |
+
The fixed interpreter is a substrate, universal over ternary threshold
|
| 559 |
+
netlists; the machine it runs is stored data that reads, rewrites, and
|
| 560 |
+
reproduces itself. The transition and the state it transforms are the same
|
| 561 |
+
tensor.
|
| 562 |
|
| 563 |
---
|
| 564 |
|
|
|
|
| 748 |
│ exhaustive equality suite and the analog crossbar simulation
|
| 749 |
├── constructor8.py the neural_subleq8io host, the recipe codec, and the universal
|
| 750 |
│ constructor / self-reproduction suite
|
| 751 |
+
└── reflect.py neural_reflect: a universal threshold interpreter whose state
|
| 752 |
+
holds its own gate table; universality / quine / matrix suites
|
| 753 |
tools/ build_all.py (build + quantize + verify every profile),
|
| 754 |
cpu_programs.py (assembler + CPU program suite), test_cpu.py
|
| 755 |
(program suite vs a variant), play.py (interactive demo),
|
src/reflect.py
CHANGED
|
@@ -1,40 +1,50 @@
|
|
| 1 |
-
"""neural_reflect — a
|
| 2 |
-
transition is
|
| 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 |
Usage:
|
| 36 |
-
python src/reflect.py build #
|
| 37 |
-
python src/reflect.py verify #
|
|
|
|
| 38 |
python src/reflect.py all
|
| 39 |
"""
|
| 40 |
|
|
@@ -42,9 +52,12 @@ from __future__ import annotations
|
|
| 42 |
|
| 43 |
import argparse
|
| 44 |
import json
|
|
|
|
| 45 |
import os
|
|
|
|
| 46 |
import sys
|
| 47 |
import time
|
|
|
|
| 48 |
from typing import Dict, List, Tuple
|
| 49 |
|
| 50 |
import torch
|
|
@@ -56,467 +69,664 @@ from matrix8 import Net, compile_net
|
|
| 56 |
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
| 57 |
MODEL_PATH = os.path.join(REPO, "variants", "neural_reflect.safetensors")
|
| 58 |
|
| 59 |
-
|
| 60 |
-
S = 256 # addressable signal bits (the whole RAM)
|
| 61 |
-
A = 8 # address width, 2**A == S
|
| 62 |
-
G = 6 # stored gates
|
| 63 |
-
F = 2 # inputs per gate (the family builds everything from <=2-in gates)
|
| 64 |
-
WB = 2 # weight-code bits per input
|
| 65 |
-
BB = 4 # bias bits (signed)
|
| 66 |
-
R = F * (A + WB) + BB + A # 32 bits per gate record
|
| 67 |
-
NET_BASE = S - G * R # 64: records occupy the high signals
|
| 68 |
-
HALT_SIG = NET_BASE - 1 # 63: a data bit the program raises to halt
|
| 69 |
-
GPW = 3 # gp register width (ceil(log2 G))
|
| 70 |
-
STATE_BITS = S + GPW + 1 # signals, gp, halt
|
| 71 |
|
| 72 |
-
assert (1 << A) == S and NET_BASE == 64 and R == 32
|
| 73 |
|
| 74 |
-
#
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
OFF_BIAS, OFF_OUT = 20, 24
|
| 78 |
|
| 79 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 80 |
|
| 81 |
|
| 82 |
# =============================================================================
|
| 83 |
-
# bit helpers
|
| 84 |
# =============================================================================
|
| 85 |
|
| 86 |
-
def to_bits(v
|
| 87 |
return [(v >> (n - 1 - i)) & 1 for i in range(n)]
|
| 88 |
|
| 89 |
|
| 90 |
-
def from_bits(bits
|
| 91 |
v = 0
|
| 92 |
for b in bits:
|
| 93 |
v = (v << 1) | int(b)
|
| 94 |
return v
|
| 95 |
|
| 96 |
|
| 97 |
-
def signed(bits
|
| 98 |
v = from_bits(bits)
|
| 99 |
return v - (1 << len(bits)) if bits[0] else v
|
| 100 |
|
| 101 |
|
| 102 |
-
def encode_gate(
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
rec[
|
| 111 |
-
|
|
|
|
|
|
|
| 112 |
return rec
|
| 113 |
|
| 114 |
|
| 115 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 116 |
|
| 117 |
|
| 118 |
-
def encode_netlist(
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
out += encode_gate(s0, s1, bias, o)
|
| 125 |
return out
|
| 126 |
|
| 127 |
|
| 128 |
-
def make_state(signals: List[int], gp: int = 0, halt: int = 0,
|
| 129 |
-
netlist: List[tuple] | None = None) -> dict:
|
| 130 |
-
sig = list(signals) + [0] * (S - len(signals))
|
| 131 |
-
if netlist is not None:
|
| 132 |
-
sig[NET_BASE:S] = encode_netlist(netlist)
|
| 133 |
-
return {"sig": sig[:S], "gp": gp, "halt": int(halt)}
|
| 134 |
-
|
| 135 |
-
|
| 136 |
# =============================================================================
|
| 137 |
-
# reference
|
| 138 |
# =============================================================================
|
| 139 |
|
| 140 |
-
def
|
| 141 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 142 |
if halt:
|
| 143 |
-
return
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
acc =
|
| 160 |
out = 1 if acc >= 0 else 0
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 166 |
|
| 167 |
|
| 168 |
# =============================================================================
|
| 169 |
# the interpreter as a fixed ternary threshold circuit
|
| 170 |
# =============================================================================
|
| 171 |
|
| 172 |
-
def
|
| 173 |
net = Net()
|
| 174 |
-
sig = [f"s{i}" for i in range(S)]
|
| 175 |
-
gp = [f"gp{k}" for k in range(GPW)]
|
| 176 |
halt = "halt"
|
| 177 |
nhalt = net.NOT("nhalt", halt)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 178 |
|
| 179 |
-
# gate select (gp one-hot over 0..G-1)
|
| 180 |
-
gp_oh = [net.DECODE(f"gpoh{g}", gp, g) for g in range(G)]
|
| 181 |
-
|
| 182 |
-
# read the selected record: recbit[r] = OR_g (gp_oh[g] AND NET[g,r])
|
| 183 |
-
recbit = []
|
| 184 |
-
for r in range(R):
|
| 185 |
-
terms = [net.AND(f"rb{r}g{g}", [gp_oh[g], sig[NET_BASE + g * R + r]]) for g in range(G)]
|
| 186 |
-
recbit.append(net.OR(f"rb{r}", terms))
|
| 187 |
-
|
| 188 |
-
a0 = recbit[OFF_A0:OFF_A0 + A]
|
| 189 |
-
w0 = (recbit[OFF_W0], recbit[OFF_W0 + 1])
|
| 190 |
-
a1 = recbit[OFF_A1:OFF_A1 + A]
|
| 191 |
-
w1 = (recbit[OFF_W1], recbit[OFF_W1 + 1])
|
| 192 |
-
biasbits = recbit[OFF_BIAS:OFF_BIAS + BB]
|
| 193 |
-
oabits = recbit[OFF_OUT:OFF_OUT + A]
|
| 194 |
-
|
| 195 |
-
# indirect reads: val_j = OR_s ( decode(addr_j)==s AND sig[s] )
|
| 196 |
def read(addrbits, tag):
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
def
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
b = biasbits # MSB-first [b3(sign) b2 b1 b0]
|
| 227 |
-
lo = [b[3], b[2], b[1], b[0]] # b0..b3 LSB-first
|
| 228 |
-
return lo + [b[0]] * (W6 - BB) # sign-extend b3
|
| 229 |
-
|
| 230 |
-
def ripple(pfx, a_lsb, b_lsb):
|
| 231 |
-
c = "#0"
|
| 232 |
-
out = []
|
| 233 |
-
for k in range(W6):
|
| 234 |
-
s_, c = net.FA(f"{pfx}.fa{k}", a_lsb[k], b_lsb[k], c)
|
| 235 |
out.append(s_)
|
| 236 |
return out
|
| 237 |
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
|
|
|
|
|
|
| 241 |
|
| 242 |
-
|
| 243 |
nsig = []
|
| 244 |
-
for s in range(S):
|
| 245 |
-
|
| 246 |
-
sel = net.AND(f"wrsel{s}", [oh, nhalt])
|
| 247 |
nsig.append(net.MUX(f"nsig{s}", sel, out, sig[s]))
|
| 248 |
|
| 249 |
-
|
| 250 |
-
gp_lsb = [gp[GPW - 1 - k] for k in range(GPW)]
|
| 251 |
inc, c = [], "#1"
|
| 252 |
-
for k in range(GPW):
|
| 253 |
-
|
| 254 |
-
c = net.AND(f"
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
|
| 260 |
-
|
| 261 |
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 265 |
|
| 266 |
|
| 267 |
# =============================================================================
|
| 268 |
-
#
|
| 269 |
# =============================================================================
|
| 270 |
|
| 271 |
-
def
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
v["#0"], v["#1"] = 0, 1
|
| 275 |
-
indeg = {g: 0 for g in net.gates}
|
| 276 |
cons: Dict[str, List[str]] = {}
|
| 277 |
-
for g, (ins, _) in
|
| 278 |
for s, _w in ins:
|
| 279 |
-
if s in
|
| 280 |
indeg[g] += 1
|
| 281 |
cons.setdefault(s, []).append(g)
|
| 282 |
order = [g for g, d in indeg.items() if d == 0]
|
| 283 |
i = 0
|
| 284 |
while i < len(order):
|
| 285 |
for c in cons.get(order[i], []):
|
| 286 |
-
if c in
|
| 287 |
indeg[c] -= 1
|
| 288 |
if indeg[c] == 0:
|
| 289 |
order.append(c)
|
| 290 |
i += 1
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 296 |
|
| 297 |
|
| 298 |
# =============================================================================
|
| 299 |
-
#
|
| 300 |
# =============================================================================
|
| 301 |
|
| 302 |
-
def
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
return v
|
| 308 |
|
| 309 |
|
| 310 |
-
def
|
| 311 |
-
|
| 312 |
-
return
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
class Reflect:
|
| 316 |
-
HALT_IDX = S + GPW
|
| 317 |
-
|
| 318 |
-
def __init__(self, layers, device="cpu"):
|
| 319 |
-
self.W = [W.to(device=device, dtype=torch.float32) for W, _ in layers]
|
| 320 |
-
self.B = [B.to(device=device, dtype=torch.float32) for _, B in layers]
|
| 321 |
-
self.device = device
|
| 322 |
-
|
| 323 |
-
def step(self, v: torch.Tensor) -> torch.Tensor:
|
| 324 |
-
for W, b in zip(self.W, self.B):
|
| 325 |
-
v = ((v @ W.T + b) >= 0).float()
|
| 326 |
-
return v
|
| 327 |
-
|
| 328 |
-
def run_steps(self, st: dict, n: int) -> dict:
|
| 329 |
-
v = state_to_vec(st).unsqueeze(0).to(self.device)
|
| 330 |
-
for _ in range(n):
|
| 331 |
-
v = self.step(v)
|
| 332 |
-
return vec_to_state(v[0].cpu())
|
| 333 |
|
| 334 |
|
| 335 |
# =============================================================================
|
| 336 |
-
# build
|
| 337 |
# =============================================================================
|
| 338 |
|
| 339 |
-
def build(
|
| 340 |
-
print("
|
|
|
|
| 341 |
t0 = time.perf_counter()
|
| 342 |
-
net, inputs, outputs =
|
|
|
|
|
|
|
|
|
|
|
|
|
| 343 |
layers, info = compile_net(net, inputs, outputs)
|
| 344 |
-
for
|
| 345 |
-
assert set(torch.unique(
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
tensors
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
"halt_signal": HALT_SIG})}
|
| 361 |
-
save_file(tensors, MODEL_PATH, metadata=meta)
|
| 362 |
-
print(f" saved {MODEL_PATH} ({os.path.getsize(MODEL_PATH) / 1e6:.1f} MB)")
|
| 363 |
return net, inputs, outputs, layers
|
| 364 |
|
| 365 |
|
| 366 |
-
# =============================================================================
|
| 367 |
-
# demonstration netlists
|
| 368 |
-
# =============================================================================
|
| 369 |
-
|
| 370 |
-
def netlist_xor(a=0, b=1, out=4, t0=2, t1=3):
|
| 371 |
-
# XOR = AND( OR(a,b), NAND(a,b) ), all from the family's threshold cells.
|
| 372 |
-
return [((a, 1), (b, 1), -1, t0), # OR -> t0
|
| 373 |
-
((a, -1), (b, -1), 1, t1), # NAND -> t1
|
| 374 |
-
((t0, 1), (t1, 1), -2, out)] # AND -> out
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
def _bias_lsb_signal(gate_index: int) -> int:
|
| 378 |
-
# signal index of the bias LSB (b0) of gate `gate_index`
|
| 379 |
-
return NET_BASE + gate_index * R + OFF_BIAS + (BB - 1)
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
def netlist_selfmod(a=0, b=1, out=2):
|
| 383 |
-
# g0 computes AND(a,b)->out (bias -2). g1 writes 1 into g0's bias LSB,
|
| 384 |
-
# turning bias -2 (AND) into -1 (OR): the program edits the weights that
|
| 385 |
-
# define g0, so the interpreted function becomes OR on the next sweep.
|
| 386 |
-
return [((a, 1), (b, 1), -2, out), # g0: AND now, OR after edit
|
| 387 |
-
((0, 0), (0, 0), 0, _bias_lsb_signal(0))] # g1: write 1 -> g0.bias LSB
|
| 388 |
-
|
| 389 |
-
|
| 390 |
# =============================================================================
|
| 391 |
# verify
|
| 392 |
# =============================================================================
|
| 393 |
|
| 394 |
-
def
|
| 395 |
-
sig =
|
| 396 |
-
|
| 397 |
-
|
|
|
|
| 398 |
|
| 399 |
|
| 400 |
-
def verify(
|
| 401 |
ok = True
|
|
|
|
|
|
|
|
|
|
| 402 |
|
| 403 |
-
|
| 404 |
-
|
| 405 |
-
|
| 406 |
-
print("[1/4] Exhaustive single-gate semantics "
|
| 407 |
-
"(all weights x biases x input assignments)")
|
| 408 |
-
bad = 0
|
| 409 |
-
total = 0
|
| 410 |
for w0 in (-1, 0, 1):
|
| 411 |
for w1 in (-1, 0, 1):
|
| 412 |
-
for bias in range(-(1 << (BB - 1)),
|
| 413 |
-
nl = [((
|
| 414 |
for va in (0, 1):
|
| 415 |
for vb in (0, 1):
|
| 416 |
-
|
| 417 |
-
st
|
| 418 |
-
st
|
| 419 |
-
got = refl.run_steps(st, 1)
|
| 420 |
-
exp = ref_step(st)
|
| 421 |
total += 1
|
| 422 |
-
|
|
|
|
| 423 |
bad += 1
|
| 424 |
-
print(f" {'OK ' if bad == 0 else 'FAIL'} {total}
|
| 425 |
ok &= bad == 0
|
| 426 |
|
| 427 |
-
|
| 428 |
-
print("[2/4] Single step vs reference on random full states")
|
| 429 |
gen = torch.Generator().manual_seed(0x5EED)
|
| 430 |
-
|
|
|
|
|
|
|
| 431 |
bad = 0
|
| 432 |
-
for i in range(
|
| 433 |
-
st = {"sig": sig[i].tolist(), "gp": int(
|
| 434 |
-
|
|
|
|
| 435 |
bad += 1
|
| 436 |
-
|
| 437 |
-
|
| 438 |
-
|
| 439 |
-
|
| 440 |
-
|
| 441 |
-
|
| 442 |
-
|
| 443 |
-
|
| 444 |
-
|
| 445 |
-
|
| 446 |
-
|
| 447 |
-
|
| 448 |
-
|
| 449 |
-
|
| 450 |
-
|
| 451 |
-
|
| 452 |
-
st
|
| 453 |
-
|
| 454 |
-
|
| 455 |
-
|
| 456 |
-
|
| 457 |
-
|
| 458 |
-
|
| 459 |
-
|
| 460 |
-
|
| 461 |
-
|
| 462 |
-
|
| 463 |
-
|
| 464 |
-
|
| 465 |
-
|
| 466 |
-
|
| 467 |
-
|
| 468 |
-
|
| 469 |
-
|
| 470 |
-
|
| 471 |
-
|
| 472 |
-
|
| 473 |
-
|
| 474 |
-
|
| 475 |
-
|
| 476 |
-
|
| 477 |
-
|
| 478 |
-
|
| 479 |
-
|
| 480 |
-
|
| 481 |
-
|
| 482 |
-
|
| 483 |
-
|
| 484 |
-
|
| 485 |
-
|
| 486 |
-
|
| 487 |
-
|
| 488 |
-
print("[
|
| 489 |
-
|
| 490 |
-
|
| 491 |
-
|
| 492 |
-
|
| 493 |
-
|
| 494 |
-
|
| 495 |
-
|
| 496 |
-
|
| 497 |
-
|
| 498 |
-
|
| 499 |
-
|
| 500 |
-
|
| 501 |
-
|
| 502 |
-
|
| 503 |
-
|
| 504 |
-
|
| 505 |
-
|
| 506 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 507 |
return ok
|
| 508 |
|
| 509 |
|
| 510 |
-
def main()
|
| 511 |
-
ap = argparse.ArgumentParser(description="neural_reflect
|
| 512 |
-
ap.add_argument("cmd", choices=["build", "verify", "all"])
|
| 513 |
ap.add_argument("--device", default="cuda" if torch.cuda.is_available() else "cpu")
|
| 514 |
args = ap.parse_args()
|
| 515 |
-
|
| 516 |
rc = 0
|
|
|
|
|
|
|
| 517 |
if args.cmd in ("verify", "all"):
|
| 518 |
-
|
| 519 |
-
|
|
|
|
| 520 |
return rc
|
| 521 |
|
| 522 |
|
|
|
|
| 1 |
+
"""neural_reflect — a universal ternary-threshold netlist processor whose
|
| 2 |
+
transition is a fixed, machine-independent interpreter U over a netlist held
|
| 3 |
+
in the writable state.
|
| 4 |
+
|
| 5 |
+
U is the only fixed part: a ternary threshold circuit that evaluates one stored
|
| 6 |
+
gate per recurrence. Everything machine-specific is the stored netlist, which
|
| 7 |
+
the running program can read, edit, copy, and replace. U is universal over
|
| 8 |
+
ternary threshold netlists, so the fixed part is a substrate (like a
|
| 9 |
+
cellular-automaton rule), not a particular machine.
|
| 10 |
+
|
| 11 |
+
State: a flat array of S binary signals (A = log2 S), two netlist banks, and
|
| 12 |
+
memory-mapped device signals of the kind the rest of the family already uses
|
| 13 |
+
(rv32's console, subleq8io's tape):
|
| 14 |
+
|
| 15 |
+
[ PTR | OUT_DATA OUT_STROBE PTR_ADV BANK HALT | work | NET bank0 | NET bank1 ]
|
| 16 |
+
|
| 17 |
+
Each gate record holds F input slots (address, ternary weight, index flag), a
|
| 18 |
+
signed bias, and an output (address, index flag). An index flag adds PTR to the
|
| 19 |
+
address, so a fixed-size program can walk an address range. Writing OUT_STROBE
|
| 20 |
+
emits OUT_DATA; writing PTR_ADV advances PTR; both are cleared by the device.
|
| 21 |
+
These make loaders, a self-reproduction quine, and bank replacement expressible
|
| 22 |
+
as programs.
|
| 23 |
+
|
| 24 |
+
One recurrence, gate gp of the active bank:
|
| 25 |
+
eff(addr, idx) = (addr + (idx ? PTR : 0)) mod S
|
| 26 |
+
out = H( sum_j w_j * sig[eff(a_j, i_j)] + bias )
|
| 27 |
+
sig[eff(oa, oidx)] = out
|
| 28 |
+
G recurrences sweep the netlist once; G sweeps settle any acyclic netlist
|
| 29 |
+
regardless of gate order, so U reproduces the family's combinational evaluation.
|
| 30 |
+
|
| 31 |
+
Capabilities:
|
| 32 |
+
- compile_to_reflect() maps any <=2-input family Net to a stored netlist; the
|
| 33 |
+
family's full-adder cell runs through the interpreter bit-exact.
|
| 34 |
+
- A netlist stored in arbitrary gate order yields the combinational result,
|
| 35 |
+
since G sweeps relax to the fixed point.
|
| 36 |
+
- A program streams the netlist's own bytes to the output device, emitting its
|
| 37 |
+
own description.
|
| 38 |
+
- A program copies its bank into the other bank and sets the BANK flag to run
|
| 39 |
+
a different resident machine, without an external load step.
|
| 40 |
+
- The accumulator width follows from F and the bias width. U compiles to a
|
| 41 |
+
recurrent ternary matrix stack equal to the gate graph, with a measured
|
| 42 |
+
noise and conductance-mismatch margin.
|
| 43 |
|
| 44 |
Usage:
|
| 45 |
+
python src/reflect.py build # save variants/neural_reflect.safetensors
|
| 46 |
+
python src/reflect.py verify # universality, order, quine, metamorphosis
|
| 47 |
+
python src/reflect.py analog # matrix == gate + noise / mismatch margins
|
| 48 |
python src/reflect.py all
|
| 49 |
"""
|
| 50 |
|
|
|
|
| 52 |
|
| 53 |
import argparse
|
| 54 |
import json
|
| 55 |
+
import math
|
| 56 |
import os
|
| 57 |
+
import random
|
| 58 |
import sys
|
| 59 |
import time
|
| 60 |
+
from dataclasses import dataclass, field
|
| 61 |
from typing import Dict, List, Tuple
|
| 62 |
|
| 63 |
import torch
|
|
|
|
| 69 |
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
| 70 |
MODEL_PATH = os.path.join(REPO, "variants", "neural_reflect.safetensors")
|
| 71 |
|
| 72 |
+
WCODE = {1: (0, 1), -1: (1, 0), 0: (0, 0)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
|
|
|
|
| 74 |
|
| 75 |
+
# =============================================================================
|
| 76 |
+
# configuration
|
| 77 |
+
# =============================================================================
|
|
|
|
| 78 |
|
| 79 |
+
@dataclass
|
| 80 |
+
class Cfg:
|
| 81 |
+
A: int = 10
|
| 82 |
+
G: int = 11
|
| 83 |
+
F: int = 2
|
| 84 |
+
BB: int = 4
|
| 85 |
+
banks: int = 2
|
| 86 |
+
|
| 87 |
+
def __post_init__(self):
|
| 88 |
+
self.S = 1 << self.A
|
| 89 |
+
self.SLOT = self.A + 3
|
| 90 |
+
self.R = self.F * self.SLOT + self.BB + (self.A + 1)
|
| 91 |
+
span = self.F + (1 << (self.BB - 1))
|
| 92 |
+
self.ACC = int(math.ceil(math.log2(span + 1))) + 2
|
| 93 |
+
self.PTR_BASE = 0
|
| 94 |
+
self.OUT_DATA = self.A
|
| 95 |
+
self.OUT_STROBE = self.A + 1
|
| 96 |
+
self.PTR_ADV = self.A + 2
|
| 97 |
+
self.BANK_SIG = self.A + 3
|
| 98 |
+
self.HALT_SIG = self.A + 4
|
| 99 |
+
self.TRASH = self.A + 5 # no-op sink; no program reads it
|
| 100 |
+
self.WORK_BASE = self.A + 6
|
| 101 |
+
self.NET0 = self.S - self.banks * self.G * self.R
|
| 102 |
+
self.NET1 = self.NET0 + self.G * self.R
|
| 103 |
+
self.GPW = max(1, (self.G - 1).bit_length())
|
| 104 |
+
self.STATE_BITS = self.S + self.GPW + 1
|
| 105 |
+
assert self.NET0 >= self.WORK_BASE + 16, "too little data room; raise A"
|
| 106 |
+
|
| 107 |
+
def bank_base(self, bank):
|
| 108 |
+
return self.NET0 if bank == 0 else self.NET1
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
def slot_off(cfg, j):
|
| 112 |
+
return j * cfg.SLOT
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
def field_off(cfg):
|
| 116 |
+
o = cfg.F * cfg.SLOT
|
| 117 |
+
return o, o + cfg.BB
|
| 118 |
|
| 119 |
|
| 120 |
# =============================================================================
|
| 121 |
+
# bit helpers + encoding
|
| 122 |
# =============================================================================
|
| 123 |
|
| 124 |
+
def to_bits(v, n):
|
| 125 |
return [(v >> (n - 1 - i)) & 1 for i in range(n)]
|
| 126 |
|
| 127 |
|
| 128 |
+
def from_bits(bits):
|
| 129 |
v = 0
|
| 130 |
for b in bits:
|
| 131 |
v = (v << 1) | int(b)
|
| 132 |
return v
|
| 133 |
|
| 134 |
|
| 135 |
+
def signed(bits):
|
| 136 |
v = from_bits(bits)
|
| 137 |
return v - (1 << len(bits)) if bits[0] else v
|
| 138 |
|
| 139 |
|
| 140 |
+
def encode_gate(cfg, slots, bias, out):
|
| 141 |
+
rec = [0] * cfg.R
|
| 142 |
+
for j, (a, w, idx) in enumerate(slots):
|
| 143 |
+
b = slot_off(cfg, j)
|
| 144 |
+
rec[b:b + cfg.A] = to_bits(a, cfg.A)
|
| 145 |
+
rec[b + cfg.A:b + cfg.A + 2] = list(WCODE[w])
|
| 146 |
+
rec[b + cfg.A + 2] = idx
|
| 147 |
+
bo, oo = field_off(cfg)
|
| 148 |
+
rec[bo:bo + cfg.BB] = to_bits(bias & ((1 << cfg.BB) - 1), cfg.BB)
|
| 149 |
+
oa, oidx = out
|
| 150 |
+
rec[oo:oo + cfg.A] = to_bits(oa, cfg.A)
|
| 151 |
+
rec[oo + cfg.A] = oidx
|
| 152 |
return rec
|
| 153 |
|
| 154 |
|
| 155 |
+
def pad(cfg, slots):
|
| 156 |
+
slots = list(slots)
|
| 157 |
+
while len(slots) < cfg.F:
|
| 158 |
+
slots.append((0, 0, 0))
|
| 159 |
+
return slots[:cfg.F]
|
| 160 |
|
| 161 |
|
| 162 |
+
def encode_netlist(cfg, gates):
|
| 163 |
+
out = []
|
| 164 |
+
noop = ([(0, 0, 0)] * cfg.F, -1, (cfg.TRASH, 0))
|
| 165 |
+
for g in (gates + [noop] * (cfg.G - len(gates))):
|
| 166 |
+
slots, bias, o = g
|
| 167 |
+
out += encode_gate(cfg, pad(cfg, slots), bias, o)
|
|
|
|
| 168 |
return out
|
| 169 |
|
| 170 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 171 |
# =============================================================================
|
| 172 |
+
# reference: gate step + memory-mapped device logic
|
| 173 |
# =============================================================================
|
| 174 |
|
| 175 |
+
def apply_device(cfg, sig):
|
| 176 |
+
emit = None
|
| 177 |
+
if sig[cfg.OUT_STROBE] == 1:
|
| 178 |
+
emit = sig[cfg.OUT_DATA]
|
| 179 |
+
sig[cfg.OUT_STROBE] = 0
|
| 180 |
+
if sig[cfg.PTR_ADV] == 1:
|
| 181 |
+
p = (from_bits(sig[cfg.PTR_BASE:cfg.PTR_BASE + cfg.A]) + 1) & (cfg.S - 1)
|
| 182 |
+
sig[cfg.PTR_BASE:cfg.PTR_BASE + cfg.A] = to_bits(p, cfg.A)
|
| 183 |
+
sig[cfg.PTR_ADV] = 0
|
| 184 |
+
return emit
|
| 185 |
+
|
| 186 |
+
|
| 187 |
+
def ref_gate(cfg, st):
|
| 188 |
+
sig = list(st["sig"]); gp = st["gp"]; halt = st["halt"]
|
| 189 |
if halt:
|
| 190 |
+
return sig, gp, halt
|
| 191 |
+
ptr = from_bits(sig[cfg.PTR_BASE:cfg.PTR_BASE + cfg.A])
|
| 192 |
+
bank = sig[cfg.BANK_SIG]
|
| 193 |
+
rec = sig[cfg.bank_base(bank) + gp * cfg.R:][:cfg.R]
|
| 194 |
+
|
| 195 |
+
def eff(a, idx):
|
| 196 |
+
return (a + (ptr if idx else 0)) & (cfg.S - 1)
|
| 197 |
+
|
| 198 |
+
def wv(hilo):
|
| 199 |
+
return {(0, 1): 1, (1, 0): -1}.get(tuple(hilo), 0)
|
| 200 |
+
|
| 201 |
+
acc = 0
|
| 202 |
+
for j in range(cfg.F):
|
| 203 |
+
b = slot_off(cfg, j)
|
| 204 |
+
acc += wv(rec[b + cfg.A:b + cfg.A + 2]) * sig[eff(from_bits(rec[b:b + cfg.A]), rec[b + cfg.A + 2])]
|
| 205 |
+
bo, oo = field_off(cfg)
|
| 206 |
+
acc += signed(rec[bo:bo + cfg.BB])
|
| 207 |
out = 1 if acc >= 0 else 0
|
| 208 |
+
dst = eff(from_bits(rec[oo:oo + cfg.A]), rec[oo + cfg.A])
|
| 209 |
+
h = sig[cfg.HALT_SIG]
|
| 210 |
+
sig[dst] = out
|
| 211 |
+
ngp = 0 if gp == cfg.G - 1 else gp + 1
|
| 212 |
+
return sig, ngp, (1 if (halt or h) else 0)
|
| 213 |
+
|
| 214 |
+
|
| 215 |
+
def ref_step(cfg, st):
|
| 216 |
+
halt_in = st["halt"]
|
| 217 |
+
sig, gp, halt = ref_gate(cfg, st)
|
| 218 |
+
emit = apply_device(cfg, sig) if halt_in == 0 else None
|
| 219 |
+
return {"sig": sig, "gp": gp, "halt": halt, "emit": emit}
|
| 220 |
|
| 221 |
|
| 222 |
# =============================================================================
|
| 223 |
# the interpreter as a fixed ternary threshold circuit
|
| 224 |
# =============================================================================
|
| 225 |
|
| 226 |
+
def build_net(cfg):
|
| 227 |
net = Net()
|
| 228 |
+
sig = [f"s{i}" for i in range(cfg.S)]
|
| 229 |
+
gp = [f"gp{k}" for k in range(cfg.GPW)]
|
| 230 |
halt = "halt"
|
| 231 |
nhalt = net.NOT("nhalt", halt)
|
| 232 |
+
bank = sig[cfg.BANK_SIG]
|
| 233 |
+
gp_oh = [net.DECODE(f"gpoh{g}", gp, g) for g in range(cfg.G)]
|
| 234 |
+
|
| 235 |
+
def netbit(g, r):
|
| 236 |
+
b0 = sig[cfg.NET0 + g * cfg.R + r]
|
| 237 |
+
if cfg.banks == 1:
|
| 238 |
+
return b0
|
| 239 |
+
return net.MUX(f"bk{g}_{r}", bank, sig[cfg.NET1 + g * cfg.R + r], b0)
|
| 240 |
+
|
| 241 |
+
recbit = [net.OR(f"rb{r}", [net.AND(f"rb{r}g{g}", [gp_oh[g], netbit(g, r)])
|
| 242 |
+
for g in range(cfg.G)]) for r in range(cfg.R)]
|
| 243 |
+
|
| 244 |
+
ptr = [sig[cfg.PTR_BASE + k] for k in range(cfg.A)]
|
| 245 |
+
|
| 246 |
+
def eff_addr(addrbits, idx, tag):
|
| 247 |
+
m = [net.AND(f"pm{tag}_{k}", [ptr[k], idx]) for k in range(cfg.A)]
|
| 248 |
+
a_lsb = [addrbits[cfg.A - 1 - k] for k in range(cfg.A)]
|
| 249 |
+
m_lsb = [m[cfg.A - 1 - k] for k in range(cfg.A)]
|
| 250 |
+
c = "#0"; s_lsb = []
|
| 251 |
+
for k in range(cfg.A):
|
| 252 |
+
s_, c = net.FA(f"eff{tag}_fa{k}", a_lsb[k], m_lsb[k], c)
|
| 253 |
+
s_lsb.append(s_)
|
| 254 |
+
return [s_lsb[cfg.A - 1 - k] for k in range(cfg.A)]
|
| 255 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 256 |
def read(addrbits, tag):
|
| 257 |
+
return net.OR(f"rd{tag}", [net.AND(f"rd{tag}a{s}", [net.DECODE(f"rd{tag}oh{s}", addrbits, s), sig[s]])
|
| 258 |
+
for s in range(cfg.S)])
|
| 259 |
+
|
| 260 |
+
contribs = []
|
| 261 |
+
for j in range(cfg.F):
|
| 262 |
+
b = slot_off(cfg, j)
|
| 263 |
+
addrbits = recbit[b:b + cfg.A]
|
| 264 |
+
hi, lo, idx = recbit[b + cfg.A], recbit[b + cfg.A + 1], recbit[b + cfg.A + 2]
|
| 265 |
+
v = read(eff_addr(addrbits, idx, f"r{j}"), f"{j}")
|
| 266 |
+
pos = net.AND(f"pos{j}", [net.NOT(f"nhi{j}", hi), lo])
|
| 267 |
+
neg = net.AND(f"neg{j}", [hi, net.NOT(f"nlo{j}", lo)])
|
| 268 |
+
mag = net.OR(f"mag{j}", [net.AND(f"tp{j}", [pos, v]), net.AND(f"tn{j}", [neg, v])])
|
| 269 |
+
contribs.append((mag, net.AND(f"sg{j}", [neg, v])))
|
| 270 |
+
|
| 271 |
+
W = cfg.ACC
|
| 272 |
+
bo, oo = field_off(cfg)
|
| 273 |
+
biasbits = recbit[bo:bo + cfg.BB]
|
| 274 |
+
|
| 275 |
+
def clsb(mag, sign):
|
| 276 |
+
return [mag] + [sign] * (W - 1)
|
| 277 |
+
|
| 278 |
+
def blsb():
|
| 279 |
+
lo = [biasbits[cfg.BB - 1 - k] for k in range(cfg.BB)]
|
| 280 |
+
return lo + [biasbits[0]] * (W - cfg.BB)
|
| 281 |
+
|
| 282 |
+
def ripple(pfx, a, b):
|
| 283 |
+
c = "#0"; out = []
|
| 284 |
+
for k in range(W):
|
| 285 |
+
s_, c = net.FA(f"{pfx}_fa{k}", a[k], b[k], c)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 286 |
out.append(s_)
|
| 287 |
return out
|
| 288 |
|
| 289 |
+
acc = clsb(*contribs[0])
|
| 290 |
+
for j in range(1, cfg.F):
|
| 291 |
+
acc = ripple(f"acc{j}", acc, clsb(*contribs[j]))
|
| 292 |
+
acc = ripple("accb", acc, blsb())
|
| 293 |
+
out = net.NOT("out", acc[W - 1])
|
| 294 |
|
| 295 |
+
oea = eff_addr(recbit[oo:oo + cfg.A], recbit[oo + cfg.A], "w")
|
| 296 |
nsig = []
|
| 297 |
+
for s in range(cfg.S):
|
| 298 |
+
sel = net.AND(f"wrsel{s}", [net.DECODE(f"wroh{s}", oea, s), nhalt])
|
|
|
|
| 299 |
nsig.append(net.MUX(f"nsig{s}", sel, out, sig[s]))
|
| 300 |
|
| 301 |
+
gp_lsb = [gp[cfg.GPW - 1 - k] for k in range(cfg.GPW)]
|
|
|
|
| 302 |
inc, c = [], "#1"
|
| 303 |
+
for k in range(cfg.GPW):
|
| 304 |
+
inc.append(net.XOR(f"gpi_x{k}", gp_lsb[k], c))
|
| 305 |
+
c = net.AND(f"gpi_c{k}", [gp_lsb[k], c])
|
| 306 |
+
inc_msb = [inc[cfg.GPW - 1 - k] for k in range(cfg.GPW)]
|
| 307 |
+
wrap = [net.MUX(f"gpw{k}", gp_oh[cfg.G - 1], "#0", inc_msb[k]) for k in range(cfg.GPW)]
|
| 308 |
+
ngp = [net.MUX(f"ngp{k}", nhalt, wrap[k], gp[k]) for k in range(cfg.GPW)]
|
| 309 |
+
nhalt_out = net.OR("halt_next", [halt, sig[cfg.HALT_SIG]])
|
| 310 |
|
| 311 |
+
return net, sig + gp + [halt], nsig + ngp + [nhalt_out]
|
| 312 |
|
| 313 |
+
|
| 314 |
+
# =============================================================================
|
| 315 |
+
# leveled sparse evaluator (scales past dense matrices)
|
| 316 |
+
# =============================================================================
|
| 317 |
+
|
| 318 |
+
class Leveled:
|
| 319 |
+
def __init__(self, net, inputs, outputs, device="cpu"):
|
| 320 |
+
self.device = device
|
| 321 |
+
self.inputs = inputs
|
| 322 |
+
gates = net.gates
|
| 323 |
+
indeg = {g: 0 for g in gates}
|
| 324 |
+
cons: Dict[str, List[str]] = {}
|
| 325 |
+
for g, (ins, _) in gates.items():
|
| 326 |
+
for s, _w in ins:
|
| 327 |
+
if s in gates:
|
| 328 |
+
indeg[g] += 1
|
| 329 |
+
cons.setdefault(s, []).append(g)
|
| 330 |
+
order = [g for g, d in indeg.items() if d == 0]
|
| 331 |
+
i = 0
|
| 332 |
+
while i < len(order):
|
| 333 |
+
for c in cons.get(order[i], []):
|
| 334 |
+
if c in gates:
|
| 335 |
+
indeg[c] -= 1
|
| 336 |
+
if indeg[c] == 0:
|
| 337 |
+
order.append(c)
|
| 338 |
+
i += 1
|
| 339 |
+
assert len(order) == len(gates), "cycle"
|
| 340 |
+
names = ["#0", "#1"] + inputs + order
|
| 341 |
+
slot = {n: i for i, n in enumerate(names)}
|
| 342 |
+
depth = {n: 0 for n in names if n not in gates}
|
| 343 |
+
for g in order:
|
| 344 |
+
depth[g] = 1 + max((depth[s] for s, _ in gates[g][0]), default=0)
|
| 345 |
+
by_level: Dict[int, List[str]] = {}
|
| 346 |
+
for g in order:
|
| 347 |
+
by_level.setdefault(depth[g], []).append(g)
|
| 348 |
+
self.n_sig = len(names)
|
| 349 |
+
self.plan = []
|
| 350 |
+
for lv in sorted(by_level):
|
| 351 |
+
gs = by_level[lv]
|
| 352 |
+
fan = max(len(gates[g][0]) for g in gs)
|
| 353 |
+
idx = torch.zeros(len(gs), fan, dtype=torch.long)
|
| 354 |
+
w = torch.zeros(len(gs), fan)
|
| 355 |
+
b = torch.zeros(len(gs))
|
| 356 |
+
out = torch.zeros(len(gs), dtype=torch.long)
|
| 357 |
+
for r, g in enumerate(gs):
|
| 358 |
+
ins, bias = gates[g]
|
| 359 |
+
out[r] = slot[g]; b[r] = bias
|
| 360 |
+
for c, (s, wt) in enumerate(ins):
|
| 361 |
+
idx[r, c] = slot[s]; w[r, c] = wt
|
| 362 |
+
self.plan.append((idx.to(device), w.to(device), b.to(device), out.to(device)))
|
| 363 |
+
self.in_slots = torch.tensor([slot[n] for n in inputs], device=device)
|
| 364 |
+
self.out_slots = torch.tensor([slot[n] for n in outputs], device=device)
|
| 365 |
+
|
| 366 |
+
def step(self, invec):
|
| 367 |
+
B = invec.shape[0]
|
| 368 |
+
V = torch.zeros(self.n_sig, B, device=self.device)
|
| 369 |
+
V[1] = 1.0
|
| 370 |
+
V[self.in_slots] = invec.T
|
| 371 |
+
for idx, w, b, out in self.plan:
|
| 372 |
+
g = V[idx]
|
| 373 |
+
V[out] = ((g * w[:, :, None]).sum(1) + b[:, None] >= 0).float()
|
| 374 |
+
return V[self.out_slots].T
|
| 375 |
+
|
| 376 |
+
|
| 377 |
+
def state_to_vec(cfg, st):
|
| 378 |
+
v = torch.zeros(cfg.STATE_BITS)
|
| 379 |
+
v[:cfg.S] = torch.tensor(st["sig"], dtype=torch.float32)
|
| 380 |
+
v[cfg.S:cfg.S + cfg.GPW] = torch.tensor(to_bits(st["gp"], cfg.GPW), dtype=torch.float32)
|
| 381 |
+
v[cfg.S + cfg.GPW] = float(st["halt"])
|
| 382 |
+
return v
|
| 383 |
+
|
| 384 |
+
|
| 385 |
+
def vec_to_state(cfg, v):
|
| 386 |
+
b = [int(round(float(x))) for x in (v.tolist() if hasattr(v, "tolist") else v)]
|
| 387 |
+
return {"sig": b[:cfg.S], "gp": from_bits(b[cfg.S:cfg.S + cfg.GPW]), "halt": b[cfg.S + cfg.GPW]}
|
| 388 |
+
|
| 389 |
+
|
| 390 |
+
class Runner:
|
| 391 |
+
def __init__(self, cfg, lev):
|
| 392 |
+
self.cfg = cfg
|
| 393 |
+
self.lev = lev
|
| 394 |
+
|
| 395 |
+
def step(self, st):
|
| 396 |
+
halt_in = st["halt"]
|
| 397 |
+
v = state_to_vec(self.cfg, st).unsqueeze(0).to(self.lev.device)
|
| 398 |
+
out = self.lev.step(v[:, :len(self.lev.inputs)])[0].cpu()
|
| 399 |
+
ns = vec_to_state(self.cfg, out)
|
| 400 |
+
ns["emit"] = apply_device(self.cfg, ns["sig"]) if halt_in == 0 else None
|
| 401 |
+
return ns
|
| 402 |
+
|
| 403 |
+
def run(self, st, n, collect=False):
|
| 404 |
+
emits = []
|
| 405 |
+
for _ in range(n):
|
| 406 |
+
st = self.step(st)
|
| 407 |
+
if collect and st.get("emit") is not None:
|
| 408 |
+
emits.append(st["emit"])
|
| 409 |
+
return (st, emits) if collect else st
|
| 410 |
|
| 411 |
|
| 412 |
# =============================================================================
|
| 413 |
+
# compile a <=2-input family Net into a stored reflect netlist
|
| 414 |
# =============================================================================
|
| 415 |
|
| 416 |
+
def topo(net):
|
| 417 |
+
gates = net.gates
|
| 418 |
+
indeg = {g: 0 for g in gates}
|
|
|
|
|
|
|
| 419 |
cons: Dict[str, List[str]] = {}
|
| 420 |
+
for g, (ins, _) in gates.items():
|
| 421 |
for s, _w in ins:
|
| 422 |
+
if s in gates:
|
| 423 |
indeg[g] += 1
|
| 424 |
cons.setdefault(s, []).append(g)
|
| 425 |
order = [g for g, d in indeg.items() if d == 0]
|
| 426 |
i = 0
|
| 427 |
while i < len(order):
|
| 428 |
for c in cons.get(order[i], []):
|
| 429 |
+
if c in gates:
|
| 430 |
indeg[c] -= 1
|
| 431 |
if indeg[c] == 0:
|
| 432 |
order.append(c)
|
| 433 |
i += 1
|
| 434 |
+
assert len(order) == len(gates)
|
| 435 |
+
return order
|
| 436 |
+
|
| 437 |
+
|
| 438 |
+
def compile_to_reflect(cfg, net, in_addr, out_names, work_start):
|
| 439 |
+
addr = dict(in_addr)
|
| 440 |
+
w = work_start
|
| 441 |
+
rgates = []
|
| 442 |
+
for g in topo(net):
|
| 443 |
+
ins, bias = net.gates[g]
|
| 444 |
+
assert len(ins) <= cfg.F
|
| 445 |
+
addr[g] = w; w += 1
|
| 446 |
+
slots = [(addr[s], wt, 0) for s, wt in ins]
|
| 447 |
+
rgates.append((slots, bias, (addr[g], 0)))
|
| 448 |
+
assert len(rgates) <= cfg.G, f"{len(rgates)} gates > bank {cfg.G}"
|
| 449 |
+
return rgates, addr
|
| 450 |
+
|
| 451 |
+
|
| 452 |
+
def adder_net(bits):
|
| 453 |
+
"""N-bit ripple-carry adder from the family's full-adder cell."""
|
| 454 |
+
net = Net()
|
| 455 |
+
a = [f"a{i}" for i in range(bits)]
|
| 456 |
+
b = [f"b{i}" for i in range(bits)]
|
| 457 |
+
carry = "#0"
|
| 458 |
+
sums = []
|
| 459 |
+
for i in range(bits):
|
| 460 |
+
s_, carry = net.FA(f"fa{i}", a[i], b[i], carry)
|
| 461 |
+
sums.append(s_)
|
| 462 |
+
return net, a, b, sums, carry
|
| 463 |
|
| 464 |
|
| 465 |
# =============================================================================
|
| 466 |
+
# programs (stored netlists) for the reflective demos
|
| 467 |
# =============================================================================
|
| 468 |
|
| 469 |
+
def quine_program(cfg):
|
| 470 |
+
# emit sig[NET0 + ptr], strobe, advance ptr
|
| 471 |
+
return [([(cfg.NET0, 1, 1)], -1, (cfg.OUT_DATA, 0)), # OUT_DATA = NET0[ptr]
|
| 472 |
+
([], 0, (cfg.OUT_STROBE, 0)), # OUT_STROBE = 1
|
| 473 |
+
([], 0, (cfg.PTR_ADV, 0))] # PTR_ADV = 1
|
|
|
|
| 474 |
|
| 475 |
|
| 476 |
+
def copier_program(cfg):
|
| 477 |
+
# bank1[NET1 + ptr] = bank0[NET0 + ptr]; advance ptr
|
| 478 |
+
return [([(cfg.NET0, 1, 1)], -1, (cfg.WORK_BASE, 0)), # tmp = NET0[ptr]
|
| 479 |
+
([(cfg.WORK_BASE, 1, 0)], -1, (cfg.NET1, 1)), # NET1[ptr] = tmp
|
| 480 |
+
([], 0, (cfg.PTR_ADV, 0))]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 481 |
|
| 482 |
|
| 483 |
# =============================================================================
|
| 484 |
+
# build + save (compiles U to a recurrent ternary matrix stack)
|
| 485 |
# =============================================================================
|
| 486 |
|
| 487 |
+
def build(cfg, save=True):
|
| 488 |
+
print(f"Interpreter U (S={cfg.S}, G={cfg.G}, F={cfg.F}, banks={cfg.banks}, "
|
| 489 |
+
f"record={cfg.R}b, acc={cfg.ACC}b)")
|
| 490 |
t0 = time.perf_counter()
|
| 491 |
+
net, inputs, outputs = build_net(cfg)
|
| 492 |
+
print(f" {len(net.gates):,} threshold gates in {time.perf_counter() - t0:.1f}s")
|
| 493 |
+
if not save:
|
| 494 |
+
return net, inputs, outputs, None
|
| 495 |
+
t1 = time.perf_counter()
|
| 496 |
layers, info = compile_net(net, inputs, outputs)
|
| 497 |
+
for Wt, _ in layers:
|
| 498 |
+
assert set(torch.unique(Wt).tolist()) <= {-1, 0, 1}
|
| 499 |
+
tensors = {f"matrix.layer{i:03d}.weight": Wt for i, (Wt, _) in enumerate(layers)}
|
| 500 |
+
tensors.update({f"matrix.layer{i:03d}.bias": B for i, (_, B) in enumerate(layers)})
|
| 501 |
+
for k, v in (("signals", cfg.S), ("gates", cfg.G), ("banks", cfg.banks),
|
| 502 |
+
("state_bits", cfg.STATE_BITS), ("layers", info["layers"]), ("version", 2)):
|
| 503 |
+
tensors[f"manifest.{k}"] = torch.tensor([float(v)])
|
| 504 |
+
meta = {"machine": "reflect", "weight_quantization": "ternary",
|
| 505 |
+
"config": json.dumps({"A": cfg.A, "S": cfg.S, "G": cfg.G, "F": cfg.F,
|
| 506 |
+
"BB": cfg.BB, "banks": cfg.banks, "record_bits": cfg.R,
|
| 507 |
+
"acc_bits": cfg.ACC, "net_base": cfg.NET0})}
|
| 508 |
+
save_file(tensors, MODEL_PATH, metadata=meta)
|
| 509 |
+
print(f" compiled to {info['layers']} ternary matrices (max width "
|
| 510 |
+
f"{info['max_width']}, {info['total_weights']:,} weights) in "
|
| 511 |
+
f"{time.perf_counter() - t1:.1f}s; saved {MODEL_PATH} "
|
| 512 |
+
f"({os.path.getsize(MODEL_PATH) / 1e6:.1f} MB)")
|
|
|
|
|
|
|
|
|
|
| 513 |
return net, inputs, outputs, layers
|
| 514 |
|
| 515 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 516 |
# =============================================================================
|
| 517 |
# verify
|
| 518 |
# =============================================================================
|
| 519 |
|
| 520 |
+
def _load(cfg, netlist, bank=0):
|
| 521 |
+
sig = [0] * cfg.S
|
| 522 |
+
base = cfg.bank_base(bank)
|
| 523 |
+
sig[base:base + len(netlist)] = netlist
|
| 524 |
+
return sig
|
| 525 |
|
| 526 |
|
| 527 |
+
def verify(cfg, device):
|
| 528 |
ok = True
|
| 529 |
+
net, inputs, outputs = build_net(cfg)
|
| 530 |
+
lev = Leveled(net, inputs, outputs, device=device)
|
| 531 |
+
R = Runner(cfg, lev)
|
| 532 |
|
| 533 |
+
print("[1/6] Exhaustive single-gate semantics")
|
| 534 |
+
bad = total = 0
|
| 535 |
+
ra, rb, ro = cfg.WORK_BASE, cfg.WORK_BASE + 1, cfg.WORK_BASE + 2
|
|
|
|
|
|
|
|
|
|
|
|
|
| 536 |
for w0 in (-1, 0, 1):
|
| 537 |
for w1 in (-1, 0, 1):
|
| 538 |
+
for bias in range(-(1 << (cfg.BB - 1)), 1 << (cfg.BB - 1)):
|
| 539 |
+
nl = encode_netlist(cfg, [([(ra, w0, 0), (rb, w1, 0)], bias, (ro, 0))])
|
| 540 |
for va in (0, 1):
|
| 541 |
for vb in (0, 1):
|
| 542 |
+
sig = _load(cfg, nl); sig[ra], sig[rb] = va, vb
|
| 543 |
+
st = {"sig": sig, "gp": 0, "halt": 0}
|
| 544 |
+
g = R.step(st); e = ref_step(cfg, st)
|
|
|
|
|
|
|
| 545 |
total += 1
|
| 546 |
+
exp = 1 if w0 * va + w1 * vb + bias >= 0 else 0
|
| 547 |
+
if {k: g[k] for k in ("sig", "gp", "halt")} != {k: e[k] for k in ("sig", "gp", "halt")} or g["sig"][ro] != exp:
|
| 548 |
bad += 1
|
| 549 |
+
print(f" {'OK ' if bad == 0 else 'FAIL'} {total} configurations exact")
|
| 550 |
ok &= bad == 0
|
| 551 |
|
| 552 |
+
print("[2/6] Single step vs reference on random full states")
|
|
|
|
| 553 |
gen = torch.Generator().manual_seed(0x5EED)
|
| 554 |
+
N = 1500
|
| 555 |
+
sig = (torch.rand(N, cfg.S, generator=gen) < 0.5).long()
|
| 556 |
+
gpv = torch.randint(0, cfg.G, (N,), generator=gen)
|
| 557 |
bad = 0
|
| 558 |
+
for i in range(N):
|
| 559 |
+
st = {"sig": sig[i].tolist(), "gp": int(gpv[i]), "halt": 0}
|
| 560 |
+
g = R.step(st); e = ref_step(cfg, st)
|
| 561 |
+
if {k: g[k] for k in ("sig", "gp", "halt")} != {k: e[k] for k in ("sig", "gp", "halt")}:
|
| 562 |
bad += 1
|
| 563 |
+
hbad = sum(1 for i in range(200)
|
| 564 |
+
if R.step({"sig": sig[i].tolist(), "gp": int(gpv[i]), "halt": 1})["sig"] != sig[i].tolist())
|
| 565 |
+
print(f" {'OK ' if bad == 0 else 'FAIL'} {N} random states exact; "
|
| 566 |
+
f"{'OK ' if hbad == 0 else 'FAIL'} halted states are fixed points")
|
| 567 |
+
ok &= bad == 0 and hbad == 0
|
| 568 |
+
|
| 569 |
+
print("[3/6] Universality: interpret a family full-adder cell")
|
| 570 |
+
anet, an, bn, sums, cout = adder_net(1)
|
| 571 |
+
ia = {an[0]: cfg.WORK_BASE, bn[0]: cfg.WORK_BASE + 1}
|
| 572 |
+
rg, addr = compile_to_reflect(cfg, anet, ia, sums + [cout], cfg.WORK_BASE + 2)
|
| 573 |
+
nl = encode_netlist(cfg, rg)
|
| 574 |
+
abad = 0
|
| 575 |
+
for x in (0, 1):
|
| 576 |
+
for y in (0, 1):
|
| 577 |
+
sig = _load(cfg, nl)
|
| 578 |
+
sig[ia[an[0]]] = x; sig[ia[bn[0]]] = y
|
| 579 |
+
st = R.run({"sig": sig, "gp": 0, "halt": 0}, cfg.G * cfg.G)
|
| 580 |
+
s = st["sig"][addr[sums[0]]] | (st["sig"][addr[cout]] << 1)
|
| 581 |
+
if s != x + y:
|
| 582 |
+
abad += 1
|
| 583 |
+
print(f" {'OK ' if abad == 0 else 'FAIL'} full adder computed bit-exact "
|
| 584 |
+
f"through the interpreter ({len(rg)} gates, compiled from the family's Net)")
|
| 585 |
+
ok &= abad == 0
|
| 586 |
+
|
| 587 |
+
print("[4/6] Order independence (adder gates stored in random order)")
|
| 588 |
+
obad = 0
|
| 589 |
+
for seed in range(4):
|
| 590 |
+
perm = list(range(len(rg)))
|
| 591 |
+
random.Random(seed + 1).shuffle(perm)
|
| 592 |
+
nls = encode_netlist(cfg, [rg[p] for p in perm])
|
| 593 |
+
for x in (0, 1):
|
| 594 |
+
for y in (0, 1):
|
| 595 |
+
sig = _load(cfg, nls)
|
| 596 |
+
sig[ia[an[0]]] = x; sig[ia[bn[0]]] = y
|
| 597 |
+
st = R.run({"sig": sig, "gp": 0, "halt": 0}, cfg.G * cfg.G)
|
| 598 |
+
s = st["sig"][addr[sums[0]]] | (st["sig"][addr[cout]] << 1)
|
| 599 |
+
if s != x + y:
|
| 600 |
+
obad += 1
|
| 601 |
+
print(f" {'OK ' if obad == 0 else 'FAIL'} adder correct under 4 random gate "
|
| 602 |
+
f"orders (relaxation reproduces combinational evaluation)")
|
| 603 |
+
ok &= obad == 0
|
| 604 |
+
|
| 605 |
+
print("[5/6] Self-reproduction (quine): program emits its own encoding")
|
| 606 |
+
nlq = encode_netlist(cfg, quine_program(cfg))
|
| 607 |
+
K = 48
|
| 608 |
+
st = {"sig": _load(cfg, nlq), "gp": 0, "halt": 0}
|
| 609 |
+
_, emits = R.run(st, cfg.G * K + cfg.G, collect=True)
|
| 610 |
+
good = emits[:K] == nlq[:K]
|
| 611 |
+
print(f" {'OK ' if good else 'FAIL'} first {K} emitted bits == the program's "
|
| 612 |
+
f"own stored bits (streaming its description out the output device)")
|
| 613 |
+
ok &= good
|
| 614 |
+
|
| 615 |
+
print("[6/6] Metamorphosis: program fills the other bank and switches to it")
|
| 616 |
+
# (a) copier program fills bank1 with a copy of bank0
|
| 617 |
+
nlc = encode_netlist(cfg, copier_program(cfg))
|
| 618 |
+
st = {"sig": _load(cfg, nlc), "gp": 0, "halt": 0}
|
| 619 |
+
st = R.run(st, cfg.G * (cfg.G * cfg.R) + cfg.G)
|
| 620 |
+
filled = st["sig"][cfg.NET1:cfg.NET1 + cfg.G * cfg.R] == nlc
|
| 621 |
+
# (b) a program-set BANK flag switches the interpreter to a different machine
|
| 622 |
+
# preinstalled in bank1 (the full adder), which then runs.
|
| 623 |
+
nl_switch = encode_netlist(cfg, [([], 0, (cfg.BANK_SIG, 0))]) # BANK = 1
|
| 624 |
+
sig = _load(cfg, nl_switch, bank=0)
|
| 625 |
+
sig[cfg.NET1:cfg.NET1 + len(nl)] = nl # bank1 = adder
|
| 626 |
+
st = {"sig": sig, "gp": 0, "halt": 0}
|
| 627 |
+
st = R.run(st, 1) # gp0 sets BANK=1
|
| 628 |
+
st["gp"] = 0; st["sig"][cfg.WORK_BASE] = 1; st["sig"][cfg.WORK_BASE + 1] = 1
|
| 629 |
+
st = R.run(st, cfg.G * cfg.G) # now running bank1
|
| 630 |
+
switched = (st["sig"][cfg.BANK_SIG] == 1
|
| 631 |
+
and (st["sig"][addr[sums[0]]] | (st["sig"][addr[cout]] << 1)) == 2)
|
| 632 |
+
print(f" {'OK ' if filled else 'FAIL'} copier wrote bank0 into bank1; "
|
| 633 |
+
f"{'OK ' if switched else 'FAIL'} program set BANK and ran the bank-1 machine")
|
| 634 |
+
ok &= filled and switched
|
| 635 |
+
|
| 636 |
+
print("\nREFLECT v2 (universality, order, quine, metamorphosis):",
|
| 637 |
+
"PASS" if ok else "FAIL")
|
| 638 |
+
return ok
|
| 639 |
+
|
| 640 |
+
|
| 641 |
+
# =============================================================================
|
| 642 |
+
# analog: matrix == gate + noise / conductance-mismatch margins
|
| 643 |
+
# =============================================================================
|
| 644 |
+
|
| 645 |
+
def analog(cfg, device):
|
| 646 |
+
ok = True
|
| 647 |
+
net, inputs, outputs = build_net(cfg)
|
| 648 |
+
print("Compiling U to a recurrent ternary matrix stack...")
|
| 649 |
+
layers, info = compile_net(net, inputs, outputs)
|
| 650 |
+
print(f" {info['layers']} matrices, max width {info['max_width']}, "
|
| 651 |
+
f"{info['total_weights']:,} weights")
|
| 652 |
+
Wm = [W.to(device=device, dtype=torch.float32) for W, _ in layers]
|
| 653 |
+
Bm = [B.to(device=device, dtype=torch.float32) for _, B in layers]
|
| 654 |
+
lev = Leveled(net, inputs, outputs, device=device)
|
| 655 |
+
|
| 656 |
+
def mstep(v, thresh=0.0, noise=0.0, gen=None, Wover=None):
|
| 657 |
+
for W, b in zip(Wover or Wm, Bm):
|
| 658 |
+
pre = v @ W.T + b
|
| 659 |
+
if noise:
|
| 660 |
+
pre = pre + torch.randn(pre.shape, generator=gen, device=v.device) * noise
|
| 661 |
+
v = (pre >= thresh).float()
|
| 662 |
+
return v
|
| 663 |
+
|
| 664 |
+
gen = torch.Generator().manual_seed(11)
|
| 665 |
+
ns = 300
|
| 666 |
+
V = torch.zeros(ns, cfg.STATE_BITS)
|
| 667 |
+
V[:, :cfg.S] = (torch.rand(ns, cfg.S, generator=gen) < 0.5).float()
|
| 668 |
+
gpv = torch.randint(0, cfg.G, (ns,), generator=gen)
|
| 669 |
+
for i in range(ns):
|
| 670 |
+
V[i, cfg.S:cfg.S + cfg.GPW] = torch.tensor(to_bits(int(gpv[i]), cfg.GPW), dtype=torch.float32)
|
| 671 |
+
V = V.to(device)
|
| 672 |
+
same = bool((mstep(V) == lev.step(V[:, :len(inputs)])).all())
|
| 673 |
+
print(f" {'OK ' if same else 'FAIL'} matrix step == gate-graph step on {ns} random states")
|
| 674 |
+
ok &= same
|
| 675 |
+
|
| 676 |
+
m = float("inf"); v = V.clone()
|
| 677 |
+
for W, b in zip(Wm, Bm):
|
| 678 |
+
pre = v @ W.T + b
|
| 679 |
+
m = min(m, float((pre + 0.5).abs().min()))
|
| 680 |
+
v = (pre >= 0).float()
|
| 681 |
+
print(f" measured minimum |pre-activation - (-0.5)|: {m:.3f} (guarantee 0.5)")
|
| 682 |
+
ok &= abs(m - 0.5) < 1e-6
|
| 683 |
+
|
| 684 |
+
base = state_to_vec(cfg, {"sig": _load(cfg, encode_netlist(cfg, quine_program(cfg))),
|
| 685 |
+
"gp": 0, "halt": 0}).unsqueeze(0).to(device)
|
| 686 |
+
|
| 687 |
+
def sweep(noise=0.0, gen=None, Wover=None):
|
| 688 |
+
v = base.clone()
|
| 689 |
+
for _ in range(cfg.G):
|
| 690 |
+
v = mstep(v, thresh=-0.5, noise=noise, gen=gen, Wover=Wover)
|
| 691 |
+
return v
|
| 692 |
+
|
| 693 |
+
ref = sweep()
|
| 694 |
+
print(" read noise per MVM (20 trials, full sweep bit-exact):")
|
| 695 |
+
for sigma in (0.05, 0.10, 0.20, 0.40):
|
| 696 |
+
good = sum(int(bool((sweep(noise=sigma, gen=torch.Generator(device=device).manual_seed(t)) == ref).all()))
|
| 697 |
+
for t in range(20))
|
| 698 |
+
print(f" sigma={sigma:.2f}: {good}/20 bit-exact")
|
| 699 |
+
if sigma <= 0.05 and good != 20: # deep within the 0.5 margin
|
| 700 |
+
ok = False
|
| 701 |
+
|
| 702 |
+
print(" conductance mismatch (8 instances, full sweep bit-exact):")
|
| 703 |
+
for sg in (0.05, 0.20):
|
| 704 |
+
good = 0
|
| 705 |
+
for t in range(8):
|
| 706 |
+
gg = torch.Generator().manual_seed(7 + t)
|
| 707 |
+
Wp = [W + (torch.randn(W.shape, generator=gg) * sg).to(device) * (W != 0) for W in Wm]
|
| 708 |
+
good += int(bool((sweep(Wover=Wp) == ref).all()))
|
| 709 |
+
print(f" sigma_G={sg:.2f}: {good}/8 bit-exact")
|
| 710 |
+
if sg <= 0.05 and good != 8:
|
| 711 |
+
ok = False
|
| 712 |
+
|
| 713 |
+
print("ANALOG (matrix == gate; bit-exact within the 0.5 margin):", "PASS" if ok else "FAIL")
|
| 714 |
return ok
|
| 715 |
|
| 716 |
|
| 717 |
+
def main():
|
| 718 |
+
ap = argparse.ArgumentParser(description="neural_reflect v2")
|
| 719 |
+
ap.add_argument("cmd", choices=["build", "verify", "analog", "all"])
|
| 720 |
ap.add_argument("--device", default="cuda" if torch.cuda.is_available() else "cpu")
|
| 721 |
args = ap.parse_args()
|
| 722 |
+
cfg = Cfg()
|
| 723 |
rc = 0
|
| 724 |
+
if args.cmd in ("build", "all"):
|
| 725 |
+
build(cfg, save=True)
|
| 726 |
if args.cmd in ("verify", "all"):
|
| 727 |
+
rc |= 0 if verify(cfg, args.device) else 1
|
| 728 |
+
if args.cmd in ("analog", "all"):
|
| 729 |
+
rc |= 0 if analog(cfg, args.device) else 1
|
| 730 |
return rc
|
| 731 |
|
| 732 |
|
variants/neural_reflect.safetensors
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:3a9c8eda3eef586ea00b4ff32e511223f54bff3ab6b4e151c69b5318cb106db2
|
| 3 |
+
size 157772226
|