File size: 44,520 Bytes
28868a6 b3106d8 28868a6 | 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 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 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 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 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 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 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 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 | """neural_matrix8 — the 8-bit threshold CPU as a recurrent ternary linear-threshold
network.
The verified gate wiring of the registers-profile CPU (8-bit data, 4-bit
addresses, 16 B memory) is compiled into a fixed sequence of ternary weight
matrices W1..Wk with a Heaviside step between them, acting on a 173-bit state
vector that holds the program counter, all four registers, the flags, the
stack pointer, the halt bit, and every memory bit:
[ PC[4] | R0[8] R1[8] R2[8] R3[8] | Z N C V | SP[4] | HALT | MEM[16][8] ]
One instruction is one pass through the stack (s' = H(Wk @ ... H(W1 @ s + b1)
... + bk)); the machine is the stack iterated until the halt bit sets, so the
whole processor is a single recurrent linear-threshold network. The transition
is total: a halted state is a fixed point (every architectural write is gated
by NOT halt), so iteration past halt is harmless.
Every weight is in {-1, 0, 1} and every bias is a small integer; the circuit
is assembled from the same verified cell constructions the family ships
(two-layer OR/NAND XOR cells, ha1/ha2/carry_or full adders, bit-cascade
comparators, 2:1 mux cells, one-threshold-gate address decode rows), then
levelized ASAP with identity pass-throughs (H(x-1)) carrying live signals
between layers.
Equality with the gate-graph processor is machine-checked, bit for bit:
- exhaustively over all 65,536 (a, b) operand pairs for every ALU opcode
(ADD SUB AND OR XOR SHL SHR MUL DIV CMP), batched through the matrices;
- exhaustively over all Jcc condition x flag combinations, all JMP/CALL
targets and SP values, and all LOAD/STORE address/data pairs;
- by full-state lockstep against GenericThresholdCPU walking the shipped
neural_computer8_registers weights, per instruction, on a micro-program
suite covering every opcode;
- by single-step agreement with a pure-integer reference on random states.
Analog realization: pre-activations are integers, 1-firing gates sit at >= 0
and 0-gates at <= -1, so placing the analog comparator threshold at -0.5 gives
every gate a symmetric guaranteed noise margin of 0.5 — any total analog error
(read noise + conductance mismatch) below 0.5 per pre-activation provably
reproduces the digital circuit bit-exactly. The analog suite measures the
margin, then sweeps Gaussian read noise and static ternary-conductance
mismatch to locate the empirical breakdown against the guarantee.
Usage:
python matrix8.py build # compile + save variants/neural_matrix8.safetensors
python matrix8.py verify # equality suite (build in memory if file absent)
python matrix8.py analog # margin + noise / mismatch sweeps
python matrix8.py all # build + verify + analog
"""
from __future__ import annotations
import argparse
import json
import os
import sys
import time
from typing import Dict, List, Optional, Tuple
import torch
from safetensors import safe_open
from safetensors.torch import save_file
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # repo root; this module lives in src/
MODEL_PATH = os.path.join(REPO, "variants", "neural_matrix8.safetensors")
GATE_MODEL_PATH = os.path.join(REPO, "variants", "neural_computer8_registers.safetensors")
ADDR_BITS = 4
MEM_BYTES = 16
STATE_BITS = 4 + 32 + 4 + 4 + 1 + 128 # pc, regs, flags, sp, halt, mem
# =============================================================================
# Circuit IR: named threshold gates over binary signals, constants fold away
# =============================================================================
class Net:
"""A DAG of threshold gates. Signals are names; '#0' / '#1' are constants
folded into biases. Helper constructors perform algebraic collapse
(AND(x,#1)=x, OR with one live input = that input, ...) so the compiled
stack carries no dead logic; the collapses change nothing semantically and
the whole machine is verified bit-for-bit afterwards."""
def __init__(self):
self.gates: Dict[str, Tuple[List[Tuple[str, int]], int]] = {}
def g(self, name: str, ins: List[Tuple[str, int]], bias: int) -> str:
folded: List[Tuple[str, int]] = []
b = int(bias)
for s, w in ins:
assert w in (-1, 1), f"non-ternary weight {w} at {name}"
if s == "#1":
b += w
elif s == "#0":
pass
else:
folded.append((s, int(w)))
if name in self.gates:
raise ValueError(f"duplicate gate {name}")
self.gates[name] = (folded, b)
return name
# --- verified cell constructions --------------------------------------
def NOT(self, name, x):
if x == "#1":
return "#0"
if x == "#0":
return "#1"
return self.g(name, [(x, -1)], 0)
def BUF(self, name, x):
if x in ("#0", "#1"):
return x
return self.g(name, [(x, 1)], -1)
def AND(self, name, ins: List[str]):
live = []
for s in ins:
if s == "#0":
return "#0"
if s != "#1":
live.append(s)
if not live:
return "#1"
if len(live) == 1:
return live[0]
return self.g(name, [(s, 1) for s in live], -len(live))
def OR(self, name, ins: List[str]):
live = []
for s in ins:
if s == "#1":
return "#1"
if s != "#0":
live.append(s)
if not live:
return "#0"
if len(live) == 1:
return live[0]
return self.g(name, [(s, 1) for s in live], -1)
def NOR(self, name, ins: List[str]):
live = [s for s in ins if s != "#0"]
if any(s == "#1" for s in live):
return "#0"
if not live:
return "#1"
return self.g(name, [(s, -1) for s in live], 0)
def XOR(self, prefix, a, b):
"""Two-layer OR/NAND XOR, the family's verified construction."""
if a == "#0":
return b if b in ("#0", "#1") else self.BUF(f"{prefix}.buf", b)
if b == "#0":
return a if a in ("#0", "#1") else self.BUF(f"{prefix}.buf", a)
if a == "#1":
return self.NOT(f"{prefix}.not", b)
if b == "#1":
return self.NOT(f"{prefix}.not", a)
h_or = self.g(f"{prefix}.l1or", [(a, 1), (b, 1)], -1)
h_nand = self.g(f"{prefix}.l1nand", [(a, -1), (b, -1)], 1)
return self.g(prefix, [(h_or, 1), (h_nand, 1)], -2)
def FA(self, prefix, a, b, cin):
"""Verified full-adder cell (ha1 XOR/AND, ha2 XOR/AND, carry OR)."""
s1 = self.XOR(f"{prefix}.ha1s", a, b)
c1 = self.AND(f"{prefix}.ha1c", [a, b])
s2 = self.XOR(f"{prefix}.ha2s", s1, cin)
c2 = self.AND(f"{prefix}.ha2c", [s1, cin])
co = self.OR(f"{prefix}.cor", [c1, c2])
return s2, co
def MUX(self, prefix, sel, x1, x0):
"""2:1 mux cell: sel ? x1 : x0 (not_sel / and / and / or)."""
if sel == "#1":
return x1
if sel == "#0":
return x0
if x1 == x0:
return x0
ns = self.NOT(f"{prefix}.ns", sel)
a1 = self.AND(f"{prefix}.a1", [x1, sel])
a0 = self.AND(f"{prefix}.a0", [x0, ns])
return self.OR(f"{prefix}.or", [a1, a0])
def DECODE(self, name, bits: List[str], value: int):
"""One-gate address-decode row (the memory.addr_decode construction):
fires iff the MSB-first bits equal `value`."""
n = len(bits)
ins = []
pop = 0
for i in range(n):
bit = (value >> (n - 1 - i)) & 1
ins.append((bits[i], 1 if bit else -1))
pop += bit
return self.g(name, ins, -pop)
# =============================================================================
# State layout
# =============================================================================
def state_names() -> List[str]:
names = [f"pc{i}" for i in range(4)]
for r in range(4):
names += [f"r{r}b{i}" for i in range(8)]
names += ["fZ", "fN", "fC", "fV"]
names += [f"sp{i}" for i in range(4)]
names += ["halt"]
for j in range(MEM_BYTES):
names += [f"m{j}b{i}" for i in range(8)]
return names
def state_to_vec(s: dict) -> torch.Tensor:
v = torch.zeros(STATE_BITS)
i = 0
for k in range(4):
v[i] = (s["pc"] >> (3 - k)) & 1
i += 1
for r in range(4):
for k in range(8):
v[i] = (s["regs"][r] >> (7 - k)) & 1
i += 1
for k in range(4):
v[i] = s["flags"][k]
i += 1
for k in range(4):
v[i] = (s["sp"] >> (3 - k)) & 1
i += 1
v[i] = 1.0 if s["halted"] else 0.0
i += 1
for j in range(MEM_BYTES):
for k in range(8):
v[i] = (s["mem"][j] >> (7 - k)) & 1
i += 1
return v
def vec_to_state(v: torch.Tensor) -> dict:
bits = [int(round(float(x))) for x in v.tolist()]
i = 0
pc = 0
for k in range(4):
pc = (pc << 1) | bits[i]
i += 1
regs = []
for r in range(4):
x = 0
for k in range(8):
x = (x << 1) | bits[i]
i += 1
regs.append(x)
flags = bits[i:i + 4]
i += 4
sp = 0
for k in range(4):
sp = (sp << 1) | bits[i]
i += 1
halted = bool(bits[i])
i += 1
mem = []
for j in range(MEM_BYTES):
x = 0
for k in range(8):
x = (x << 1) | bits[i]
i += 1
mem.append(x)
return {"pc": pc, "regs": regs, "flags": flags, "sp": sp,
"halted": halted, "mem": mem}
# =============================================================================
# The step circuit: one full instruction of the registers-profile CPU
# =============================================================================
def build_step_net() -> Tuple[Net, List[str], List[str]]:
net = Net()
pc = [f"pc{i}" for i in range(4)]
R = [[f"r{r}b{i}" for i in range(8)] for r in range(4)]
fl = ["fZ", "fN", "fC", "fV"]
sp = [f"sp{i}" for i in range(4)]
halt = "halt"
mem = [[f"m{j}b{i}" for i in range(8)] for j in range(MEM_BYTES)]
# ---- fetch: bytes at PC .. PC+3 (decode lines reindexed, no adders) ----
P = [net.DECODE(f"pdec{j}", pc, j) for j in range(MEM_BYTES)]
byte = []
for o in range(4):
bo = []
for bit in range(8):
terms = [net.AND(f"f{o}a{j}b{bit}", [mem[j][bit], P[(j - o) % MEM_BYTES]])
for j in range(MEM_BYTES)]
bo.append(net.OR(f"f{o}b{bit}", terms))
byte.append(bo)
# ---- field decode; opcode lines gated by NOT halt (total transition) ---
nhalt = net.NOT("nhalt", halt)
O = [net.DECODE(f"op{v:X}", byte[0][0:4], v) for v in range(16)]
OG = [net.AND(f"og{v:X}", [O[v], nhalt]) for v in range(16)]
rdl = [net.DECODE(f"rdl{r}", byte[0][4:6], r) for r in range(4)]
rsl = [net.DECODE(f"rsl{r}", byte[0][6:8], r) for r in range(4)]
CL = [net.DECODE(f"cond{c}", byte[1][5:8], c) for c in range(8)]
# ---- register read ports ----------------------------------------------
a = [net.OR(f"a{bit}", [net.AND(f"a{bit}r{r}", [R[r][bit], rdl[r]])
for r in range(4)]) for bit in range(8)]
b = [net.OR(f"b{bit}", [net.AND(f"b{bit}r{r}", [R[r][bit], rsl[r]])
for r in range(4)]) for bit in range(8)]
a_l = lambda k: a[7 - k] # LSB-first views for the adder chains
b_l = lambda k: b[7 - k]
# ---- ALU: ADD ----------------------------------------------------------
c = "#0"
add_l = []
for k in range(8):
s_, c = net.FA(f"add.fa{k}", a_l(k), b_l(k), c)
add_l.append(s_)
add = [add_l[7 - i] for i in range(8)]
add_c = c
# ---- SUB (shared by CMP): a + ~b + 1 -----------------------------------
nb_l = [net.NOT(f"sub.nb{k}", b_l(k)) for k in range(8)]
c = "#1"
sub_l = []
for k in range(8):
s_, c = net.FA(f"sub.fa{k}", a_l(k), nb_l[k], c)
sub_l.append(s_)
sub = [sub_l[7 - i] for i in range(8)]
sub_c = c # carry set = no borrow
# ---- bitwise / shifts ---------------------------------------------------
andr = [net.AND(f"land{bit}", [a[bit], b[bit]]) for bit in range(8)]
orr = [net.OR(f"lor{bit}", [a[bit], b[bit]]) for bit in range(8)]
xorr = [net.XOR(f"lxor{bit}", a[bit], b[bit]) for bit in range(8)]
shl = [a[bit + 1] if bit < 7 else "#0" for bit in range(8)]
shr = [a[bit - 1] if bit > 0 else "#0" for bit in range(8)]
# ---- MUL: partial products + 7 chained shift-add adders ----------------
pp = [[net.AND(f"pp.a{i}b{j}", [a[i], b[j]]) for j in range(8)] for i in range(8)]
acc_l: List[str] = ["#0"] * 8
acc_l[7] = pp[7][0] # row j=0 shifted by 7
for j in range(1, 8):
shift = 7 - j
c = "#0"
nxt = []
for k in range(8):
addend = pp[14 - k - j][j] if shift <= k <= min(7, 14 - j) else "#0"
s_, c = net.FA(f"mul.s{j}.fa{k}", acc_l[k], addend, c)
nxt.append(s_)
acc_l = nxt
mul = [acc_l[7 - i] for i in range(8)]
# ---- DIV: 8 restoring stages (bit-cascade GE + subtract + mux) ---------
def cascade_lt(prefix: str, x_m: List[str], y_m: List[str]) -> str:
"""Unsigned x < y via the verified bit-cascade (bit 0 = MSB)."""
bit_lt, bit_eq = [], []
for i in range(8):
bit_lt.append(net.g(f"{prefix}.b{i}lt", [(x_m[i], -1), (y_m[i], 1)], -1)
if x_m[i] != "#0" else
(y_m[i] if y_m[i] != "#0" else "#0"))
if x_m[i] == "#0":
bit_eq.append(net.NOT(f"{prefix}.b{i}eq", y_m[i]))
else:
e_and = net.AND(f"{prefix}.b{i}ea", [x_m[i], y_m[i]])
e_nor = net.NOR(f"{prefix}.b{i}en", [x_m[i], y_m[i]])
bit_eq.append(net.OR(f"{prefix}.b{i}eq", [e_and, e_nor]))
casc = [bit_lt[0]]
for i in range(1, 8):
pref = net.AND(f"{prefix}.ep{i}", bit_eq[:i])
casc.append(net.AND(f"{prefix}.cl{i}", [pref, bit_lt[i]]))
return net.OR(f"{prefix}.lt", casc)
rem_l: List[str] = ["#0"] * 8
q_m: List[str] = []
for st in range(8):
sh_l = [a[st]] + rem_l[0:7] # (rem << 1) | a_bit; top bit provably 0
sh_m = [sh_l[7 - i] for i in range(8)]
lt = cascade_lt(f"div.s{st}.cmp", sh_m, b)
ge = net.NOT(f"div.s{st}.ge", lt)
c = "#1"
dsub_l = []
for k in range(8):
s_, c = net.FA(f"div.s{st}.sub.fa{k}", sh_l[k], nb_l[k], c)
dsub_l.append(s_)
rem_l = [net.MUX(f"div.s{st}.mx{k}", ge, dsub_l[k], sh_l[k]) for k in range(8)]
q_m.append(ge)
div = q_m # quotient, MSB-first
# ---- LOAD read port (address = low nibble of byte3) --------------------
addr4 = byte[3][4:8]
AD = [net.DECODE(f"adec{j}", addr4, j) for j in range(MEM_BYTES)]
ld = [net.OR(f"ld{bit}", [net.AND(f"ld{bit}a{j}", [mem[j][bit], AD[j]])
for j in range(MEM_BYTES)]) for bit in range(8)]
# ---- result mux over opcode lines ---------------------------------------
srcs = [(0, add), (1, sub), (2, andr), (3, orr), (4, xorr),
(5, shl), (6, shr), (7, mul), (8, div), (0xA, ld)]
res = [net.OR(f"res{bit}", [net.AND(f"res{bit}o{v:X}", [src[bit], OG[v]])
for v, src in srcs]) for bit in range(8)]
# ---- flags ---------------------------------------------------------------
z_add = net.NOR("zadd", add)
z_sub = net.NOR("zsub", sub)
z_mul = net.NOR("zmul", mul)
v_add = net.AND("vadd", [net.XOR("vadd.x1", a[0], add[0]),
net.XOR("vadd.x2", b[0], add[0])])
v_sub = net.AND("vsub", [net.XOR("vsub.x1", a[0], b[0]),
net.XOR("vsub.x2", a[0], sub[0])])
f_src = { # per-source (Z, N, C, V); CMP shares the SUB datapath
0: (z_add, add[0], add_c, v_add),
1: (z_sub, sub[0], sub_c, v_sub),
7: (z_mul, mul[0], "#0", "#0"),
9: (z_sub, sub[0], sub_c, v_sub),
}
fw = net.OR("fw", [OG[0], OG[1], OG[7], OG[9]])
fl_next = []
for fi, fn in enumerate("ZNCV"):
new = net.OR(f"fnew{fn}", [net.AND(f"fnew{fn}o{v:X}", [f_src[v][fi], OG[v]])
for v in (0, 1, 7, 9)])
fl_next.append(net.MUX(f"fmux{fn}", fw, new, fl[fi]))
# ---- register writeback --------------------------------------------------
wen = net.OR("wen", [OG[v] for v in (0, 1, 2, 3, 4, 5, 6, 7, 8, 0xA)])
R_next = []
for r in range(4):
wsel = net.AND(f"wsel{r}", [wen, rdl[r]])
nws = net.NOT(f"nws{r}", wsel)
row = []
for bit in range(8):
t1 = net.AND(f"wb{r}b{bit}n", [res[bit], wsel])
t0 = net.AND(f"wb{r}b{bit}o", [R[r][bit], nws])
row.append(net.OR(f"wb{r}b{bit}", [t1, t0]))
R_next.append(row)
# ---- program counter ------------------------------------------------------
pc_l = [pc[3 - k] for k in range(4)] # LSB-first view
p2_l = [pc_l[0], net.NOT("pc2.n1", pc_l[1]),
net.XOR("pc2.x2", pc_l[2], pc_l[1]),
net.XOR("pc2.x3", pc_l[3], net.AND("pc2.c3", [pc_l[2], pc_l[1]]))]
p4_l = [pc_l[0], pc_l[1], net.NOT("pc4.n2", pc_l[2]),
net.XOR("pc4.x3", pc_l[3], pc_l[2])]
ext = net.OR("ext", [OG[v] for v in (0xA, 0xB, 0xC, 0xD, 0xE)])
adv_l = [net.MUX(f"padv{k}", ext, p4_l[k], p2_l[k]) for k in range(4)]
pc_adv = [adv_l[3 - i] for i in range(4)]
nfl = [net.NOT(f"nf{fn}", fl[i]) for i, fn in enumerate("ZNCV")]
cond_flag = [fl[0], nfl[0], fl[2], nfl[2], fl[1], nfl[1], fl[3], nfl[3]]
condsel = net.OR("condsel", [net.AND(f"ct{cnd}", [CL[cnd], cond_flag[cnd]])
for cnd in range(8)])
take = net.OR("take", [OG[0xC], OG[0xE], net.AND("jcct", [OG[0xD], condsel])])
keeppc = net.OR("keeppc", [halt, OG[0xF]])
adv = net.AND("adv", [net.NOT("ntake", take), net.NOT("nkeep", keeppc)])
pc_next = [net.OR(f"pcn{i}", [net.AND(f"pcn{i}t", [addr4[i], take]),
net.AND(f"pcn{i}k", [pc[i], keeppc]),
net.AND(f"pcn{i}a", [pc_adv[i], adv])])
for i in range(4)]
# ---- stack pointer (CALL: SP-2 = SP + 14) ---------------------------------
sp_l = [sp[3 - k] for k in range(4)]
c = "#0"
s2_l = []
for k, addend in enumerate(["#0", "#1", "#1", "#1"]):
s_, c = net.FA(f"spm2.fa{k}", sp_l[k], addend, c)
s2_l.append(s_)
spm2 = [s2_l[3 - i] for i in range(4)]
sp_next = [net.MUX(f"spn{i}", OG[0xE], spm2[i], sp[i]) for i in range(4)]
# ---- halt (sticky) ---------------------------------------------------------
halt_next = net.OR("haltn", [halt, OG[0xF]])
# ---- memory next: STORE write, CALL pushes ret-hi (0) and ret-lo ----------
SPD = [net.DECODE(f"spdec{j}", sp, j) for j in range(MEM_BYTES)]
mem_next = []
for j in range(MEM_BYTES):
st_sel = net.AND(f"stsel{j}", [OG[0xB], AD[j]])
ch_sel = net.AND(f"chsel{j}", [OG[0xE], SPD[(j + 1) % MEM_BYTES]])
cl_sel = net.AND(f"clsel{j}", [OG[0xE], SPD[(j + 2) % MEM_BYTES]])
keep = net.NOR(f"keep{j}", [st_sel, ch_sel, cl_sel])
row = []
for bit in range(8):
terms = [net.AND(f"mn{j}b{bit}k", [mem[j][bit], keep]),
net.AND(f"mn{j}b{bit}s", [b[bit], st_sel])]
if bit >= 4: # ret-lo low nibble carries pc_adv; high nibble is 0
terms.append(net.AND(f"mn{j}b{bit}c", [pc_adv[bit - 4], cl_sel]))
row.append(net.OR(f"mn{j}b{bit}", terms))
mem_next.append(row)
outputs = (pc_next
+ [bitsig for row in R_next for bitsig in row]
+ fl_next + sp_next + [halt_next]
+ [bitsig for row in mem_next for bitsig in row])
return net, state_names(), outputs
# =============================================================================
# Compiler: levelize ASAP, insert pass-throughs, emit dense ternary matrices
# =============================================================================
def compile_net(net: Net, inputs: List[str], outputs: List[str],
) -> Tuple[List[Tuple[torch.Tensor, torch.Tensor]], dict]:
gates = net.gates
level: Dict[str, int] = {s: 0 for s in inputs}
consumers: Dict[str, List[str]] = {}
indeg: Dict[str, int] = {}
for gname, (ins, _) in gates.items():
deps = [s for s, _ in ins if s in gates]
indeg[gname] = len(deps)
for s, _ in ins:
consumers.setdefault(s, []).append(gname)
order = [g for g, d in indeg.items() if d == 0]
i = 0
while i < len(order):
for cns in consumers.get(order[i], []):
indeg[cns] -= 1
if indeg[cns] == 0:
order.append(cns)
i += 1
if len(order) != len(gates):
raise ValueError("cycle in step circuit")
for gname in order:
ins, _ = gates[gname]
level[gname] = 1 + max((level[s] for s, _ in ins), default=0)
kmax = max(level[gname] for gname in gates)
K = kmax + 1 # final layer selects the next-state bits in order
last_use: Dict[str, int] = {}
for gname, (ins, _) in gates.items():
for s, _ in ins:
last_use[s] = max(last_use.get(s, 0), level[gname])
for s in outputs:
if s not in ("#0", "#1"):
last_use[s] = K
by_level: Dict[int, List[str]] = {}
for gname in gates:
by_level.setdefault(level[gname], []).append(gname)
vec = list(inputs) # V_0
layers: List[Tuple[torch.Tensor, torch.Tensor]] = []
for lv in range(1, K + 1):
if lv < K:
new_gates = sorted(by_level.get(lv, []))
passes = [s for s in vec if last_use.get(s, 0) > lv]
rows = new_gates + passes
else:
rows = list(outputs)
cols = {s: idx for idx, s in enumerate(vec)}
W = torch.zeros(len(rows), len(vec), dtype=torch.int8)
B = torch.zeros(len(rows), dtype=torch.int8)
out_names = []
for ri, s in enumerate(rows):
if lv == K and s == "#0":
B[ri] = -1
out_names.append(s)
continue
if lv == K and s == "#1":
B[ri] = 0
out_names.append(s)
continue
if s in gates and level[s] == lv:
ins, bias = gates[s]
for src, w in ins:
W[ri, cols[src]] += w
B[ri] = bias
else:
W[ri, cols[s]] = 1
B[ri] = -1
out_names.append(s)
layers.append((W, B))
vec = out_names
if vec != outputs:
raise AssertionError("final layer does not emit the state vector")
info = {
"layers": K,
"gates": len(gates),
"max_width": max(max(W.shape) for W, _ in layers),
"total_weights": sum(W.numel() for W, _ in layers),
"widths": [layers[0][0].shape[1]] + [W.shape[0] for W, _ in layers],
}
return layers, info
# =============================================================================
# Recurrent runtime
# =============================================================================
class MatrixMachine:
"""s' = H(Wk @ ... H(W1 @ s + b1) ... + bk); iterate until the halt bit.
Digital mode thresholds at 0 (pre-activations are integers). Analog mode
thresholds at -0.5 (the max-margin comparator point) and can inject
Gaussian read noise per MVM and static ternary-conductance mismatch."""
HALT_IDX = 4 + 32 + 4 + 4 # index of the halt bit in the state vector
def __init__(self, layers, device="cpu"):
self.device = device
self.W = [W.to(device=device, dtype=torch.float32) for W, _ in layers]
self.B = [B.to(device=device, dtype=torch.float32) for _, B in layers]
@classmethod
def from_file(cls, path=MODEL_PATH, device="cpu"):
layers = []
with safe_open(path, framework="pt") as f:
n = 0
while f"matrix.layer{n:03d}.weight" in f.keys():
layers.append((f.get_tensor(f"matrix.layer{n:03d}.weight"),
f.get_tensor(f"matrix.layer{n:03d}.bias")))
n += 1
if not layers:
raise FileNotFoundError(f"no matrix layers in {path}")
return cls(layers, device=device)
def perturbed(self, sigma_g: float, seed: int = 0) -> "MatrixMachine":
"""Static conductance mismatch: each nonzero ternary conductance gets a
fixed Gaussian perturbation (fabrication variation, constant per run)."""
gen = torch.Generator(device="cpu").manual_seed(seed)
m = MatrixMachine.__new__(MatrixMachine)
m.device = self.device
m.W = []
m.B = self.B
for W in self.W:
d = torch.randn(W.shape, generator=gen).to(self.device) * sigma_g
m.W.append(W + d * (W != 0))
return m
def step(self, v: torch.Tensor, analog: bool = False,
noise_sigma: float = 0.0, gen: Optional[torch.Generator] = None,
) -> torch.Tensor:
thresh = -0.5 if analog else 0.0
for W, b in zip(self.W, self.B):
pre = v @ W.T + b
if noise_sigma > 0.0:
pre = pre + torch.randn(pre.shape, generator=gen,
device=pre.device) * noise_sigma
v = (pre >= thresh).float()
return v
def run(self, state: dict, max_cycles: int = 64, **kw) -> Tuple[dict, int]:
v = state_to_vec(state).unsqueeze(0).to(self.device)
n = 0
while n < max_cycles and v[0, self.HALT_IDX] < 0.5:
v = self.step(v, **kw)
n += 1
return vec_to_state(v[0].cpu()), n
def min_margin(self, v: torch.Tensor) -> float:
"""Distance of every pre-activation from the analog threshold -0.5."""
m = float("inf")
for W, b in zip(self.W, self.B):
pre = v @ W.T + b
m = min(m, float((pre + 0.5).abs().min()))
v = (pre >= 0).float()
return m
# =============================================================================
# Pure-integer reference (mirrors GenericThresholdCPU semantics, 4-bit addr)
# =============================================================================
_JCC_FLAG = [0, 0, 2, 2, 1, 1, 3, 3]
def ref_step(s: dict) -> dict:
if s["halted"]:
return {k: (list(v) if isinstance(v, list) else v) for k, v in s.items()}
s = {"pc": s["pc"], "regs": list(s["regs"]), "flags": list(s["flags"]),
"sp": s["sp"], "halted": s["halted"], "mem": list(s["mem"])}
M = MEM_BYTES - 1
pc = s["pc"]
ir = (s["mem"][pc] << 8) | s["mem"][(pc + 1) & M]
op, rd, rs = (ir >> 12) & 0xF, (ir >> 10) & 3, (ir >> 8) & 3
imm = ir & 0xFF
next_pc = (pc + 2) & M
addr = None
if op in (0xA, 0xB, 0xC, 0xD, 0xE):
al = s["mem"][(next_pc + 1) & M]
addr = al & M
next_pc = (next_pc + 2) & M
a, b = s["regs"][rd], s["regs"][rs]
result, carry, ovf, wr = a, 0, 0, True
if op == 0x0:
result = (a + b) & 0xFF
carry = 1 if a + b > 0xFF else 0
ovf = 1 if ((a ^ result) & (b ^ result)) & 0x80 else 0
elif op == 0x1:
result = (a - b) & 0xFF
carry = 1 if a >= b else 0
ovf = 1 if ((a ^ b) & (a ^ result)) & 0x80 else 0
elif op == 0x2:
result = a & b
elif op == 0x3:
result = a | b
elif op == 0x4:
result = a ^ b
elif op == 0x5:
result = (a << 1) & 0xFF
elif op == 0x6:
result = a >> 1
elif op == 0x7:
result = (a * b) & 0xFF
elif op == 0x8:
result = 0xFF if b == 0 else a // b
elif op == 0x9:
r2 = (a - b) & 0xFF
carry = 1 if a >= b else 0
ovf = 1 if ((a ^ b) & (a ^ r2)) & 0x80 else 0
s["flags"] = [1 if r2 == 0 else 0, 1 if r2 & 0x80 else 0, carry, ovf]
wr = False
elif op == 0xA:
result = s["mem"][addr]
elif op == 0xB:
s["mem"][addr] = b & 0xFF
wr = False
elif op == 0xC:
s["pc"] = addr
return s
elif op == 0xD:
cnd = imm & 7
flag = s["flags"][_JCC_FLAG[cnd]]
sel = flag if cnd % 2 == 0 else 1 - flag
s["pc"] = addr if sel else next_pc
return s
elif op == 0xE:
ret = next_pc
sp2 = (s["sp"] - 1) & M
s["mem"][sp2] = (ret >> 8) & 0xFF
sp2 = (sp2 - 1) & M
s["mem"][sp2] = ret & 0xFF
s["sp"] = sp2
s["pc"] = addr
return s
elif op == 0xF:
s["halted"] = True
return s
if wr and op != 0x9:
s["regs"][rd] = result & 0xFF
if op in (0x0, 0x1, 0x7):
s["flags"] = [1 if result == 0 else 0, 1 if result & 0x80 else 0,
carry, ovf]
s["pc"] = next_pc
return s
# =============================================================================
# Build + save
# =============================================================================
def build(save: bool = True):
print("Assembling the step circuit from the verified cell library...")
t0 = time.perf_counter()
net, inputs, outputs = build_step_net()
print(f" {len(net.gates)} threshold gates")
layers, info = compile_net(net, inputs, outputs)
dt = time.perf_counter() - t0
print(f" compiled to {info['layers']} ternary matrices in {dt:.1f}s; "
f"max width {info['max_width']}, "
f"{info['total_weights']:,} dense weight entries")
for W, B in layers:
vals = set(torch.unique(W).tolist())
assert vals <= {-1, 0, 1}, f"non-ternary matrix: {vals}"
assert int(B.min()) >= -128 and int(B.max()) <= 127
if save:
tensors = {}
for i, (W, B) in enumerate(layers):
tensors[f"matrix.layer{i:03d}.weight"] = W
tensors[f"matrix.layer{i:03d}.bias"] = B
tensors["manifest.data_bits"] = torch.tensor([8.0])
tensors["manifest.addr_bits"] = torch.tensor([float(ADDR_BITS)])
tensors["manifest.memory_bytes"] = torch.tensor([float(MEM_BYTES)])
tensors["manifest.registers"] = torch.tensor([4.0])
tensors["manifest.layers"] = torch.tensor([float(info["layers"])])
tensors["manifest.state_bits"] = torch.tensor([float(STATE_BITS)])
tensors["manifest.version"] = torch.tensor([4.0])
meta = {
"machine": "matrix8",
"weight_quantization": "ternary",
"state_layout": json.dumps(state_names()),
"analog": json.dumps({
"comparator_threshold": -0.5,
"guaranteed_margin": 0.5,
"note": "pre-activations are integers; any total analog error "
"below 0.5 per pre-activation is provably bit-exact",
}),
}
save_file(tensors, MODEL_PATH, metadata=meta)
sz = os.path.getsize(MODEL_PATH) / (1024 * 1024)
print(f" saved {MODEL_PATH} ({sz:.1f} MB)")
return layers, info
# =============================================================================
# Verification: matrix form == gate graph == integer reference, bit for bit
# =============================================================================
def _mk_state(mem=None, regs=(0, 0, 0, 0), flags=(0, 0, 0, 0), pc=0,
sp=MEM_BYTES - 1, halted=False):
m = [0] * MEM_BYTES
if mem:
for i, x in enumerate(mem):
m[i] = x & 0xFF
return {"pc": pc, "regs": list(regs), "flags": list(flags), "sp": sp,
"halted": halted, "mem": m}
def _instr(op, rd=0, rs=0, imm=0):
w = ((op & 0xF) << 12) | ((rd & 3) << 10) | ((rs & 3) << 8) | (imm & 0xFF)
return [(w >> 8) & 0xFF, w & 0xFF]
def verify(mm: MatrixMachine, device: str) -> bool:
ok_all = True
# ---- 1. exhaustive ALU sweeps: all 65,536 operand pairs per opcode ------
print("\n[1/4] Exhaustive ALU sweeps (65,536 operand pairs x 10 opcodes, "
"batched through the matrices, vs integer reference)")
aa = torch.arange(65536) % 256
bb = torch.arange(65536) // 256
for op, name in [(0, "ADD"), (1, "SUB"), (2, "AND"), (3, "OR"), (4, "XOR"),
(5, "SHL"), (6, "SHR"), (7, "MUL"), (8, "DIV"), (9, "CMP")]:
for f_init in (0, 0xF):
V = torch.zeros(65536, STATE_BITS)
prog = _instr(op, rd=0, rs=1) + _instr(0xF)
base = _mk_state(mem=prog, flags=[(f_init >> (3 - i)) & 1 for i in range(4)])
V[:] = state_to_vec(base)
for k in range(8):
V[:, 4 + k] = ((aa >> (7 - k)) & 1).float() # R0
V[:, 12 + k] = ((bb >> (7 - k)) & 1).float() # R1
V = mm.step(V.to(device)).cpu()
bad = 0
probe = list(range(0, 65536, 4099)) + [0, 65535, 255, 256, 32768]
for idx in probe:
st = _mk_state(mem=prog, regs=(int(aa[idx]), int(bb[idx]), 0, 0),
flags=base["flags"])
if vec_to_state(V[idx]) != ref_step(st):
bad += 1
# full vectorized compare of the architectural result field
r0 = sum(V[:, 4 + k].long() << (7 - k) for k in range(8))
fZ, fN, fC, fV = (V[:, 36].long(), V[:, 37].long(),
V[:, 38].long(), V[:, 39].long())
if op == 0:
exp = (aa + bb) & 0xFF
elif op in (1, 9):
exp = (aa - bb) & 0xFF
elif op == 2:
exp = aa & bb
elif op == 3:
exp = aa | bb
elif op == 4:
exp = aa ^ bb
elif op == 5:
exp = (aa << 1) & 0xFF
elif op == 6:
exp = aa >> 1
elif op == 7:
exp = (aa * bb) & 0xFF
else:
exp = torch.where(bb == 0, torch.full_like(aa, 0xFF),
aa // torch.clamp(bb, min=1))
reg_exp = aa if op == 9 else exp
n_bad = int((r0 != reg_exp).sum())
if op in (0, 1, 7, 9):
zx = (exp == 0).long()
nx = ((exp & 0x80) > 0).long()
if op == 0:
cx = ((aa + bb) > 0xFF).long()
vx = ((((aa ^ exp) & (bb ^ exp)) & 0x80) > 0).long()
elif op in (1, 9):
cx = (aa >= bb).long()
vx = ((((aa ^ bb) & (aa ^ exp)) & 0x80) > 0).long()
else:
cx = torch.zeros_like(aa)
vx = torch.zeros_like(aa)
n_bad += int((fZ != zx).sum() + (fN != nx).sum()
+ (fC != cx).sum() + (fV != vx).sum())
else:
want = torch.tensor([(f_init >> (3 - i)) & 1 for i in range(4)])
n_bad += int((torch.stack([fZ, fN, fC, fV], 1)
!= want.unsqueeze(0)).sum())
status = "OK " if (n_bad == 0 and bad == 0) else "FAIL"
if n_bad or bad:
ok_all = False
if f_init == 0:
print(f" {status} {name:4} 65,536/65,536 operand pairs exact"
+ ("" if n_bad == 0 else f" ({n_bad} mismatches)"))
# ---- 2. exhaustive control-flow sweeps ----------------------------------
print("\n[2/4] Exhaustive control-flow sweeps")
cases = []
for cnd in range(8): # all Jcc x all flag nibbles
for f in range(16):
prog = _instr(0xD, imm=cnd) + [0x00, 0x08] + _instr(0xF) + [0] * 2 + _instr(0xF)
cases.append(_mk_state(mem=prog, flags=[(f >> (3 - i)) & 1 for i in range(4)]))
for t in range(16): # all JMP targets
prog = _instr(0xC) + [0x00, t]
cases.append(_mk_state(mem=prog))
for spv in range(16): # CALL: every SP value
prog = _instr(0xE) + [0x00, 0x06] + [0, 0] + _instr(0xF)
cases.append(_mk_state(mem=prog, sp=spv))
for ad in range(16): # LOAD/STORE: every address
prog = _instr(0xA, rd=2) + [0x00, ad]
st = _mk_state(mem=prog)
st["mem"][ad] |= 0xA5 if ad >= 4 else st["mem"][ad]
cases.append(st)
prog = _instr(0xB, rs=1) + [0x00, ad]
cases.append(_mk_state(mem=prog, regs=(0, 0x5A, 0, 0)))
V = torch.stack([state_to_vec(s) for s in cases]).to(device)
V = mm.step(V).cpu()
bad = sum(1 for i, s in enumerate(cases) if vec_to_state(V[i]) != ref_step(s))
print(f" {'OK ' if bad == 0 else 'FAIL'} {len(cases)} directed cases "
f"(8 Jcc x 16 flag sets, 16 JMP targets, 16 CALL SPs, 32 LOAD/STORE) exact")
ok_all &= bad == 0
# ---- 3. random-state fuzz + halt fixed point -----------------------------
print("\n[3/4] Random-state fuzz (single step vs integer reference)")
gen = torch.Generator().manual_seed(0xC0FFEE)
Vr = (torch.rand(2000, STATE_BITS, generator=gen) < 0.5).float()
Vr[:, MatrixMachine.HALT_IDX] = 0.0
out = mm.step(Vr.to(device)).cpu()
bad = sum(1 for i in range(Vr.shape[0])
if vec_to_state(out[i]) != ref_step(vec_to_state(Vr[i])))
print(f" {'OK ' if bad == 0 else 'FAIL'} 2,000 uniformly random full states exact")
ok_all &= bad == 0
Vh = (torch.rand(500, STATE_BITS, generator=gen) < 0.5).float()
Vh[:, MatrixMachine.HALT_IDX] = 1.0
outh = mm.step(Vh.to(device)).cpu()
fixed = bool((outh == Vh.to(outh.device)).all())
print(f" {'OK ' if fixed else 'FAIL'} 500 halted states are exact fixed points")
ok_all &= fixed
# ---- 4. lockstep vs the gate-graph processor (shipped weights) ----------
print("\n[4/4] Instruction lockstep vs GenericThresholdCPU on "
"neural_computer8_registers (the gate graph)")
from eval_all import GenericThresholdCPU
T = {}
with safe_open(GATE_MODEL_PATH, framework="pt") as f:
for nm in f.keys():
T[nm] = f.get_tensor(nm).float()
gcpu = GenericThresholdCPU(T)
progs = [
("alu_chain", _mk_state(mem=_instr(0, 0, 1) + _instr(7, 2, 0) + _instr(9, 2, 1)
+ _instr(4, 1, 2) + _instr(0xF),
regs=(9, 5, 3, 0))),
("div_by_zero", _mk_state(mem=_instr(8, 0, 1) + _instr(0xF), regs=(77, 0, 0, 0))),
("load_store", _mk_state(mem=_instr(0xA, rd=3) + [0x00, 0x0E]
+ _instr(0xB, rs=3) + [0x00, 0x0F] + _instr(0xF)
+ [0] * 4 + [0xB7, 0x00],
)),
("call", _mk_state(mem=_instr(0xE) + [0x00, 0x06] + _instr(0xF)
+ _instr(0xF), sp=0xF)),
("jcc_taken", _mk_state(mem=_instr(0xD, imm=1) + [0x00, 0x06] + _instr(0xF)
+ _instr(0xF), flags=(0, 0, 0, 0))),
("shift_mul", _mk_state(mem=_instr(5, 0) + _instr(6, 1) + _instr(7, 0, 1)
+ _instr(0xF), regs=(0x81, 0x81, 0, 0))),
]
for name, st0 in progs:
sm = dict(st0)
sg = {"pc": st0["pc"], "regs": list(st0["regs"]), "flags": list(st0["flags"]),
"mem": list(st0["mem"]), "halted": False, "sp": st0["sp"]}
good = True
for _ in range(12):
if sm["halted"]:
break
sm, _n = mm.run(sm, max_cycles=1)
sg = gcpu.step(sg)
match = (sm["pc"] == sg["pc"] and sm["regs"] == sg["regs"]
and sm["flags"] == sg["flags"] and sm["mem"] == sg["mem"]
and sm["halted"] == sg["halted"]
and sm["sp"] == sg.get("sp", MEM_BYTES - 1))
if not match:
good = False
break
good = good and sm["halted"]
print(f" {'OK ' if good else 'FAIL'} {name}: full-state lockstep, every instruction")
ok_all &= good
print("\nMATRIX == GATE GRAPH == REFERENCE:", "PASS" if ok_all else "FAIL")
return ok_all
# =============================================================================
# Analog realization
# =============================================================================
def analog(mm: MatrixMachine, device: str) -> bool:
ok = True
print("\nAnalog crossbar simulation "
"(comparator at -0.5; ternary conductances are exact by construction)")
gen = torch.Generator().manual_seed(7)
Vr = (torch.rand(512, STATE_BITS, generator=gen) < 0.5).float().to(device)
margin = mm.min_margin(Vr)
print(f" measured minimum |pre-activation - (-0.5)| over 512 random states, "
f"all layers: {margin:.3f} (guarantee: 0.5)")
ok &= abs(margin - 0.5) < 1e-6
prog = (_instr(0, 0, 1) + _instr(7, 2, 0) + _instr(9, 2, 1) + _instr(0xF))
st0 = _mk_state(mem=prog, regs=(9, 5, 3, 0))
ref_final, _ = mm.run(dict(st0), max_cycles=8)
import math
rows_per_step = sum(int(W.shape[0]) for W in mm.W)
n_steps = 4
print(f" Gaussian read noise per MVM ({rows_per_step:,} comparator "
f"decisions per instruction; flip iff |noise| >= 0.5):")
for sigma in (0.05, 0.08, 0.10, 0.15, 0.25, 0.45):
p_flip = math.erfc(0.5 / (sigma * math.sqrt(2.0)))
exp_flips = p_flip * rows_per_step * n_steps
exact = 0
for t in range(20):
g = torch.Generator(device=device).manual_seed(1000 + t)
v = state_to_vec(st0).unsqueeze(0).to(device)
for _ in range(8):
if v[0, MatrixMachine.HALT_IDX] >= 0.5:
break
v = mm.step(v, analog=True, noise_sigma=sigma, gen=g)
exact += int(vec_to_state(v[0].cpu()) == ref_final)
print(f" sigma={sigma:.2f}: {exact}/20 runs bit-exact "
f"(theory: {exp_flips:.2e} expected flips per run)")
if exp_flips < 1e-3 and exact != 20:
ok = False
print(" Static ternary-conductance mismatch (device variation, "
"10 fabricated instances each):")
for sg in (0.02, 0.05, 0.10, 0.20):
exact = 0
for t in range(10):
mmp = mm.perturbed(sg, seed=200 + t)
v = state_to_vec(st0).unsqueeze(0).to(device)
for _ in range(8):
if v[0, MatrixMachine.HALT_IDX] >= 0.5:
break
v = mmp.step(v, analog=True)
exact += int(vec_to_state(v[0].cpu()) == ref_final)
print(f" sigma_G={sg:.2f}: {exact}/10 instances bit-exact")
if sg <= 0.02 and exact != 10:
ok = False
print("ANALOG REALIZATION:", "PASS (bit-exact within the stated margins)"
if ok else "FAIL")
return ok
# =============================================================================
# CLI
# =============================================================================
def main() -> int:
ap = argparse.ArgumentParser(description="neural_matrix8 builder/verifier")
ap.add_argument("cmd", choices=["build", "verify", "analog", "all"])
ap.add_argument("--device", default="cuda" if torch.cuda.is_available() else "cpu")
args = ap.parse_args()
rc = 0
layers = None
if args.cmd in ("build", "all"):
layers, _ = build(save=True)
if args.cmd in ("verify", "analog", "all"):
if layers is not None:
mm = MatrixMachine(layers, device=args.device)
elif os.path.exists(MODEL_PATH):
mm = MatrixMachine.from_file(MODEL_PATH, device=args.device)
else:
layers, _ = build(save=False)
mm = MatrixMachine(layers, device=args.device)
if args.cmd in ("verify", "all"):
rc |= 0 if verify(mm, args.device) else 1
if args.cmd in ("analog", "all"):
rc |= 0 if analog(mm, args.device) else 1
return rc
if __name__ == "__main__":
sys.exit(main())
|