CharlesCNorton commited on
Commit ·
28868a6
1
Parent(s): db536d3
Add neural_matrix8 and neural_subleq8io (universal constructor); move dev scripts to tools/
Browse files- README.md +143 -22
- constructor8.py +646 -0
- eval_all.py +17 -6
- matrix8.py +1081 -0
- tests/test_play.py +3 -2
- todo.md +0 -10
- build_all.py → tools/build_all.py +1 -1
- cpu_programs.py → tools/cpu_programs.py +0 -0
- play.py → tools/play.py +3 -3
- prune_weights.py → tools/prune_weights.py +3 -0
- test_cpu.py → tools/test_cpu.py +4 -3
- variants/neural_matrix8.safetensors +3 -0
- variants/neural_subleq8io.safetensors +3 -0
README.md
CHANGED
|
@@ -5,6 +5,9 @@ tags:
|
|
| 5 |
- neuromorphic
|
| 6 |
- computer-architecture
|
| 7 |
- turing-complete
|
|
|
|
|
|
|
|
|
|
| 8 |
- loihi
|
| 9 |
- truenorth
|
| 10 |
- akida
|
|
@@ -32,14 +35,24 @@ variants/neural_computer{8,16,32}_registers.safetensors 16 B memory
|
|
| 32 |
variants/neural_alu{8,16,32}.safetensors pure ALU, no memory
|
| 33 |
variants/neural_subleq8.safetensors one-instruction machine (SUBLEQ)
|
| 34 |
variants/neural_rv32.safetensors RV32IM + F-subset RISC-V processor
|
|
|
|
|
|
|
| 35 |
```
|
| 36 |
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
|
| 44 |
---
|
| 45 |
|
|
@@ -77,19 +90,19 @@ python eval_all.py --cpu-program variants/ # also run an assemble
|
|
| 77 |
For an interactive walkthrough that exercises Boolean gates, the 8-bit ALU, mod-5 divisibility, and a CPU loop end-to-end:
|
| 78 |
|
| 79 |
```bash
|
| 80 |
-
python play.py
|
| 81 |
-
python play.py --model neural_computer.safetensors
|
| 82 |
```
|
| 83 |
|
| 84 |
For end-to-end CPU validation (Fibonacci, sum 1..N, bubble sort, self-modifying JMP, all eight conditional jumps, CALL stack semantics, MUL cross-checked against repeated ADD, DIV cross-checked against repeated SUB, a bitwise AND/OR/XOR/SHL/SHR chain, and the architectural flag policy):
|
| 85 |
|
| 86 |
```bash
|
| 87 |
-
python test_cpu.py
|
| 88 |
-
python test_cpu.py --model neural_computer.safetensors
|
| 89 |
-
python test_cpu.py --only fib,sum_n
|
| 90 |
```
|
| 91 |
|
| 92 |
-
Each program is assembled by a small Python assembler (`cpu_programs.py`) and run through the threshold-gated CPU; the driver verifies expected memory contents at HALT.
|
| 93 |
|
| 94 |
---
|
| 95 |
|
|
@@ -290,7 +303,7 @@ python build.py --bits 32 -a 6 --apply all # neural_computer32_addr6.saf
|
|
| 290 |
To regenerate every named variant in one pass:
|
| 291 |
|
| 292 |
```bash
|
| 293 |
-
python build_all.py
|
| 294 |
```
|
| 295 |
|
| 296 |
This populates `variants/` with all 18 builds, quantizes each one to the smallest signed integer dtype that exactly represents its weights (~4× reduction in tensor data, with file size dominated by the safetensors header on the smaller profiles), verifies the strictly ternary weight invariant (`--ternary --strict`, so a build with any non-ternary weight fails loudly), stamps the `weight_quantization` metadata field, and runs `eval.py` on each as a sanity check.
|
|
@@ -334,7 +347,7 @@ Every weight and bias tensor in the canonical model fits in `int8`. The eval pip
|
|
| 334 |
|
| 335 |
The 8-bit arithmetic and ALU tests use strategic sampling rather than the full 65,536-case sweep because exhaustive coverage at 8-bit is feasible but not necessary given that the circuits are constructed gate-by-gate. The 16-bit and 32-bit arithmetic tests sample edge cases only; full exhaustive coverage at those widths is infeasible without specialized hardware.
|
| 336 |
|
| 337 |
-
`eval_all.py` runs the unified suite. Exit code is the number of failing variants (0 means all pass). **Testing is evaluation, not rebuilding**: `python eval_all.py variants/` scores all 18 fitness variants against the shipped weights in about two minutes (~6 s each, the composed float netlists evaluated in `NetlistEvaluator`'s leveled mode) and cleanly skips the
|
| 338 |
|
| 339 |
---
|
| 340 |
|
|
@@ -381,6 +394,110 @@ The composed circuits evaluate in a leveled mode (`NetlistEvaluator`, one padded
|
|
| 381 |
|
| 382 |
---
|
| 383 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 384 |
## Threshold logic
|
| 385 |
|
| 386 |
A threshold gate computes a Boolean function by taking a weighted sum of binary inputs and comparing the result to a threshold; the output is 1 when the sum meets or exceeds the threshold and 0 otherwise. Equivalently, it is a neuron with Heaviside step activation, integer weights, and an integer bias.
|
|
@@ -553,18 +670,22 @@ Loss components: BCE on output bits, BCE on extracted A and B bits (2× weight),
|
|
| 553 |
|
| 554 |
```
|
| 555 |
neural_computer.safetensors canonical model (32-bit, 64 KB, ~8.61M params)
|
| 556 |
-
variants/ 18
|
|
|
|
| 557 |
build.py generator (one safetensors per invocation; also `subleq`, `rv32`)
|
| 558 |
-
build_all.py builds, quantizes (--ternary --strict), and verifies every profile
|
| 559 |
quantize.py min integer dtypes + ternary verification/repair
|
| 560 |
-
eval.py gate-level fitness suite, NetlistEvaluator,
|
| 561 |
eval_all.py variant-agnostic harness + manifest-sized threshold CPU
|
| 562 |
-
cpu_programs.py assembler + ten-program suite for CPU-level validation
|
| 563 |
-
test_cpu.py runs the program suite against a chosen variant
|
| 564 |
machines.py neural_subleq8 + neural_rv32 runtimes, references, assemblers,
|
| 565 |
-
RV32 object loader, and
|
| 566 |
-
|
| 567 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 568 |
llm_integration/ SmolLM2 extractor + circuit wrapper + training code
|
| 569 |
├── circuits.py FrozenThresholdCircuits (loads safetensors, exposes
|
| 570 |
│ add_8bit / sub_8bit / mul_8bit / compare_*)
|
|
|
|
| 5 |
- neuromorphic
|
| 6 |
- computer-architecture
|
| 7 |
- turing-complete
|
| 8 |
+
- recurrent-neural-network
|
| 9 |
+
- in-memory-computing
|
| 10 |
+
- self-replication
|
| 11 |
- loihi
|
| 12 |
- truenorth
|
| 13 |
- akida
|
|
|
|
| 35 |
variants/neural_alu{8,16,32}.safetensors pure ALU, no memory
|
| 36 |
variants/neural_subleq8.safetensors one-instruction machine (SUBLEQ)
|
| 37 |
variants/neural_rv32.safetensors RV32IM + F-subset RISC-V processor
|
| 38 |
+
variants/neural_matrix8.safetensors the CPU as one recurrent ternary matrix stack
|
| 39 |
+
variants/neural_subleq8io.safetensors SUBLEQ host for the universal constructor
|
| 40 |
```
|
| 41 |
|
| 42 |
+
Four further machines are detailed in their own sections below, and together
|
| 43 |
+
they carry the family from the smallest possible processor to two results
|
| 44 |
+
about what a threshold network can be. `neural_subleq8` is a Turing-complete
|
| 45 |
+
one-instruction computer whose entire control flow is a single threshold
|
| 46 |
+
neuron. `neural_rv32` is a RISC-V processor (RV32IM plus an F subset) that
|
| 47 |
+
runs stock-compiler C, with dual-issue execution, memory-mapped I/O, and a
|
| 48 |
+
self-referential NEUR opcode. `neural_matrix8` drops the gate graph entirely:
|
| 49 |
+
the whole processor is compiled into a fixed stack of ternary weight matrices
|
| 50 |
+
with a Heaviside step between them, so one clock cycle is one matrix-vector
|
| 51 |
+
product followed by thresholding, exactly what a resistive or photonic
|
| 52 |
+
crossbar computes. And `neural_subleq8io` hosts a universal constructor: a
|
| 53 |
+
program that reads a description of any machine in the family and prints that
|
| 54 |
+
machine's weight file byte for byte, self-reproduction being the case where
|
| 55 |
+
the description is its own.
|
| 56 |
|
| 57 |
---
|
| 58 |
|
|
|
|
| 90 |
For an interactive walkthrough that exercises Boolean gates, the 8-bit ALU, mod-5 divisibility, and a CPU loop end-to-end:
|
| 91 |
|
| 92 |
```bash
|
| 93 |
+
python tools/play.py # 1 KB demo, runs in seconds
|
| 94 |
+
python tools/play.py --model neural_computer.safetensors # 64 KB, slower
|
| 95 |
```
|
| 96 |
|
| 97 |
For end-to-end CPU validation (Fibonacci, sum 1..N, bubble sort, self-modifying JMP, all eight conditional jumps, CALL stack semantics, MUL cross-checked against repeated ADD, DIV cross-checked against repeated SUB, a bitwise AND/OR/XOR/SHL/SHR chain, and the architectural flag policy):
|
| 98 |
|
| 99 |
```bash
|
| 100 |
+
python tools/test_cpu.py # default: 1 KB, ~2 s
|
| 101 |
+
python tools/test_cpu.py --model neural_computer.safetensors # 64 KB canonical, ~2 min
|
| 102 |
+
python tools/test_cpu.py --only fib,sum_n # subset of suite
|
| 103 |
```
|
| 104 |
|
| 105 |
+
Each program is assembled by a small Python assembler (`tools/cpu_programs.py`) and run through the threshold-gated CPU; the driver verifies expected memory contents at HALT.
|
| 106 |
|
| 107 |
---
|
| 108 |
|
|
|
|
| 303 |
To regenerate every named variant in one pass:
|
| 304 |
|
| 305 |
```bash
|
| 306 |
+
python tools/build_all.py
|
| 307 |
```
|
| 308 |
|
| 309 |
This populates `variants/` with all 18 builds, quantizes each one to the smallest signed integer dtype that exactly represents its weights (~4× reduction in tensor data, with file size dominated by the safetensors header on the smaller profiles), verifies the strictly ternary weight invariant (`--ternary --strict`, so a build with any non-ternary weight fails loudly), stamps the `weight_quantization` metadata field, and runs `eval.py` on each as a sanity check.
|
|
|
|
| 347 |
|
| 348 |
The 8-bit arithmetic and ALU tests use strategic sampling rather than the full 65,536-case sweep because exhaustive coverage at 8-bit is feasible but not necessary given that the circuits are constructed gate-by-gate. The 16-bit and 32-bit arithmetic tests sample edge cases only; full exhaustive coverage at those widths is infeasible without specialized hardware.
|
| 349 |
|
| 350 |
+
`eval_all.py` runs the unified suite. Exit code is the number of failing variants (0 means all pass). **Testing is evaluation, not rebuilding**: `python eval_all.py variants/` scores all 18 fitness variants against the shipped weights in about two minutes (~6 s each, the composed float netlists evaluated in `NetlistEvaluator`'s leveled mode) and cleanly skips the four standalone machines. Rebuilding the models (`tools/build_all.py`, ~50 min for all 18) is a separate step, needed only when the circuit constructions in `build.py` change; routine verification never rebuilds. The batched evaluator is population-safe: every chained intermediate (carry, borrow, mux select) is computed per population slot, so `tools/prune_weights.py`'s parallel fitness screens are exact rather than slot-0 approximations.
|
| 351 |
|
| 352 |
---
|
| 353 |
|
|
|
|
| 394 |
|
| 395 |
---
|
| 396 |
|
| 397 |
+
## neural_matrix8 — the processor as one recurrent threshold network
|
| 398 |
+
|
| 399 |
+
Every machine above is a graph of individually-named gates. `neural_matrix8`
|
| 400 |
+
is the same processor with the graph dissolved: the verified gate wiring of
|
| 401 |
+
the registers-profile CPU (8-bit data, 4-bit addresses, 16 B of memory) is
|
| 402 |
+
compiled into a fixed sequence of ternary weight matrices `W1..Wk` with a
|
| 403 |
+
Heaviside step between them, acting on a single state vector that holds the
|
| 404 |
+
program counter, all four registers, the flags, the stack pointer, the halt
|
| 405 |
+
bit, and every memory bit:
|
| 406 |
+
|
| 407 |
+
```
|
| 408 |
+
[ PC[4] | R0[8] R1[8] R2[8] R3[8] | Z N C V | SP[4] | HALT | MEM[16][8] ] = 173 bits
|
| 409 |
+
```
|
| 410 |
+
|
| 411 |
+
One instruction is one pass through the stack, `s' = H(Wk · … H(W1·s + b1) …
|
| 412 |
+
+ bk)`; the machine is that stack iterated until the halt bit sets, so the
|
| 413 |
+
whole processor is a single recurrent linear-threshold network. The
|
| 414 |
+
transition is total: a halted state is a fixed point (every architectural
|
| 415 |
+
write is gated by NOT halt), so iterating past HALT is harmless.
|
| 416 |
+
|
| 417 |
+
- **Ternary and small-integer, end to end.** The step circuit is assembled
|
| 418 |
+
from the family's verified cells (two-layer OR/NAND XOR, ripple-carry full
|
| 419 |
+
adders, bit-cascade comparators, restoring-division stages, 2:1 muxes,
|
| 420 |
+
one-gate address-decode rows), then levelized with identity pass-throughs
|
| 421 |
+
carrying live signals forward. It compiles to 108 matrices whose every
|
| 422 |
+
entry is in `{-1, 0, 1}` with small integer biases (~9.1 MB on disk).
|
| 423 |
+
- **Equality is machine-checked bit for bit,** not sampled: all 65,536 `(a,
|
| 424 |
+
b)` operand pairs for each of the ten ALU opcodes (ADD SUB AND OR XOR SHL
|
| 425 |
+
SHR MUL DIV CMP) batched through the matrices against an integer reference;
|
| 426 |
+
the full Jcc × flag, JMP, CALL, and LOAD/STORE control space; 2,000 uniformly
|
| 427 |
+
random full states plus 500 halted-state fixed points; and per-instruction
|
| 428 |
+
full-state lockstep against the gate-graph `GenericThresholdCPU` walking the
|
| 429 |
+
shipped `neural_computer8_registers` weights.
|
| 430 |
+
- **Analog realization.** Pre-activations are integers; a firing gate sits at
|
| 431 |
+
`>= 0` and a non-firing gate at `<= -1`, so a comparator threshold at `-0.5`
|
| 432 |
+
gives every gate a guaranteed noise margin of exactly 0.5 (measured minimum
|
| 433 |
+
over random states: 0.500). Any total analog error below 0.5 per
|
| 434 |
+
pre-activation therefore reproduces the digital circuit bit for bit. Each
|
| 435 |
+
matrix maps to a crossbar with conductances drawn from `{-1, 0, +1}` and one
|
| 436 |
+
clock cycle is one analog matrix-vector product followed by thresholding;
|
| 437 |
+
the suite injects Gaussian read noise (bit-exact through σ ≈ 0.10, breaking
|
| 438 |
+
down near σ = 0.15 exactly where the error model predicts) and static
|
| 439 |
+
conductance mismatch (bit-exact through σ_G = 0.10).
|
| 440 |
+
|
| 441 |
+
```bash
|
| 442 |
+
python matrix8.py build # compile + save variants/neural_matrix8.safetensors
|
| 443 |
+
python matrix8.py verify # exhaustive equality vs the gate graph and integer reference
|
| 444 |
+
python matrix8.py analog # margin measurement + read-noise + conductance-mismatch sweeps
|
| 445 |
+
```
|
| 446 |
+
|
| 447 |
+
Because the transition is a fixed matrix stack proven equal to the gate-graph
|
| 448 |
+
processor, the ISA semantics carry over unchanged, and the digital circuit and
|
| 449 |
+
its analog crossbar realization coincide within the device's quantization and
|
| 450 |
+
noise margins. The processor is no longer *described by* a neural network; it
|
| 451 |
+
*is* one, iterated.
|
| 452 |
+
|
| 453 |
+
---
|
| 454 |
+
|
| 455 |
+
## neural_subleq8io and the universal constructor — a machine that prints itself
|
| 456 |
+
|
| 457 |
+
`neural_subleq8io` is the one-instruction machine extended with three
|
| 458 |
+
memory-mapped device cells (a tape-input byte, an input strobe, and an output
|
| 459 |
+
byte). The device logic lives in the runtime, exactly as `neural_rv32`'s
|
| 460 |
+
console does; the processor's gates are unchanged, 194 of them. On this host
|
| 461 |
+
runs the **universal constructor**: a 21-instruction SUBLEQ program that reads
|
| 462 |
+
a *construction recipe* off the tape and emits a safetensors file one byte at
|
| 463 |
+
a time.
|
| 464 |
+
|
| 465 |
+
`describe(file_bytes)` compiles any file in the family into a recipe; running
|
| 466 |
+
the constructor on that recipe emits the file back byte for byte. The
|
| 467 |
+
constructor is therefore universal, and the description selects what is built.
|
| 468 |
+
Self-reproduction is the single case where the description handed to the
|
| 469 |
+
constructor is that of the running machine: the program's output stream is
|
| 470 |
+
then the byte-for-byte safetensors file of the host itself.
|
| 471 |
+
|
| 472 |
+
The equality is machine-checked rather than observed on one run:
|
| 473 |
+
|
| 474 |
+
- the recipe codec round-trips every file in the family (all 23 shipped
|
| 475 |
+
`.safetensors`, 551 MB, byte-identical and sha-verified);
|
| 476 |
+
- the constructor program is executed on three independently-verified
|
| 477 |
+
backends — a pure-integer reference, the gate-graph `SubleqThresholdCPU`
|
| 478 |
+
walking the shipped netlist, and the host itself compiled to recurrent
|
| 479 |
+
ternary matrices by `matrix8`'s compiler — and they agree cycle for cycle;
|
| 480 |
+
- **full self-reproduction runs entirely on the threshold matrices:** the host
|
| 481 |
+
compiles to 34 ternary layers, and generation 1 emits all 95,306 bytes of
|
| 482 |
+
its own weight file in 359,743 recurrences, each output byte asserted
|
| 483 |
+
against the target as it streams, ending on an sha match and an instruction
|
| 484 |
+
count identical to the reference;
|
| 485 |
+
- the offspring file then boots as generation 2 and reconstructs itself,
|
| 486 |
+
closing the loop across two generations.
|
| 487 |
+
|
| 488 |
+
```bash
|
| 489 |
+
python constructor8.py build # emit variants/neural_subleq8io.safetensors
|
| 490 |
+
python constructor8.py verify # host soundness + codec round-trip + constructor on all 3 backends
|
| 491 |
+
python constructor8.py self # full self-reproduction on the threshold matrices, then gen-2 boot
|
| 492 |
+
```
|
| 493 |
+
|
| 494 |
+
A program running on the processor emits the exact tensor file that defines
|
| 495 |
+
that processor, and builds any other machine in the family from a description
|
| 496 |
+
of it: von Neumann's universal constructor, realized as ternary threshold
|
| 497 |
+
weights and checked to the byte.
|
| 498 |
+
|
| 499 |
+
---
|
| 500 |
+
|
| 501 |
## Threshold logic
|
| 502 |
|
| 503 |
A threshold gate computes a Boolean function by taking a weighted sum of binary inputs and comparing the result to a threshold; the output is 1 when the sum meets or exceeds the threshold and 0 otherwise. Equivalently, it is a neuron with Heaviside step activation, integer weights, and an integer bias.
|
|
|
|
| 670 |
|
| 671 |
```
|
| 672 |
neural_computer.safetensors canonical model (32-bit, 64 KB, ~8.61M params)
|
| 673 |
+
variants/ 18 fitness variants + 4 standalone machines
|
| 674 |
+
(neural_subleq8, neural_rv32, neural_matrix8, neural_subleq8io)
|
| 675 |
build.py generator (one safetensors per invocation; also `subleq`, `rv32`)
|
|
|
|
| 676 |
quantize.py min integer dtypes + ternary verification/repair
|
| 677 |
+
eval.py gate-level fitness suite, NetlistEvaluator, float oracles, reference CPU
|
| 678 |
eval_all.py variant-agnostic harness + manifest-sized threshold CPU
|
|
|
|
|
|
|
| 679 |
machines.py neural_subleq8 + neural_rv32 runtimes, references, assemblers,
|
| 680 |
+
RV32 object loader, and the test suites (subleq / rv32 / rv32-c)
|
| 681 |
+
matrix8.py compiles the CPU to a recurrent ternary matrix stack; the
|
| 682 |
+
exhaustive equality suite and the analog crossbar simulation
|
| 683 |
+
constructor8.py the neural_subleq8io host, the recipe codec, and the universal
|
| 684 |
+
constructor / self-reproduction suite
|
| 685 |
+
tools/ build_all.py (build + quantize + verify every profile),
|
| 686 |
+
cpu_programs.py (assembler + CPU program suite), test_cpu.py
|
| 687 |
+
(program suite vs a variant), play.py (interactive demo),
|
| 688 |
+
prune_weights.py (GPU-batched weight reduction)
|
| 689 |
llm_integration/ SmolLM2 extractor + circuit wrapper + training code
|
| 690 |
├── circuits.py FrozenThresholdCircuits (loads safetensors, exposes
|
| 691 |
│ add_8bit / sub_8bit / mul_8bit / compare_*)
|
constructor8.py
ADDED
|
@@ -0,0 +1,646 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""neural_subleq8io + the universal constructor.
|
| 2 |
+
|
| 3 |
+
The host is the family's one-instruction machine extended with three
|
| 4 |
+
memory-mapped device cells (all device logic lives in the runtimes, exactly
|
| 5 |
+
like neural_rv32's console at 0xFF00; the processor's gates are unchanged):
|
| 6 |
+
|
| 7 |
+
0xFD IN_DATA reads as the current tape byte (device-refreshed)
|
| 8 |
+
0xFC IN_STROBE any write advances the tape head (device clears to 0)
|
| 9 |
+
0xFE OUT any write emits the written byte (device clears to 0)
|
| 10 |
+
|
| 11 |
+
A 21-instruction SUBLEQ program — the constructor — interprets a construction
|
| 12 |
+
recipe streamed on the tape:
|
| 13 |
+
|
| 14 |
+
tag 0 END
|
| 15 |
+
tag 1..127 literal record: the next `tag` tape bytes, each stored
|
| 16 |
+
negated mod 256, are emitted one per SUBLEQ instruction
|
| 17 |
+
(M[0xFE] = 0 - M[0xFD] recovers the byte)
|
| 18 |
+
tag 129..255 repeat record: the next tape byte (negated value) is
|
| 19 |
+
emitted 256-tag times without strobing (tag 128 unused)
|
| 20 |
+
|
| 21 |
+
`describe(file_bytes)` compiles any safetensors file in the family into a
|
| 22 |
+
recipe; the constructor run on that recipe emits the file byte for byte, so
|
| 23 |
+
the constructor is universal and the description selects what is built.
|
| 24 |
+
Self-reproduction is the case where the description handed to the constructor
|
| 25 |
+
is that of the running machine: the program's output stream is the
|
| 26 |
+
byte-for-byte safetensors file of the host itself.
|
| 27 |
+
|
| 28 |
+
The equality is machine-checked rather than observed on a single run:
|
| 29 |
+
- the recipe codec round-trips every family file (byte equality + sha256);
|
| 30 |
+
- the constructor program is executed on three proven-equal backends
|
| 31 |
+
(pure-integer reference, the gate-graph SubleqThresholdCPU walking the
|
| 32 |
+
shipped netlist, and the host compiled to recurrent ternary matrices with
|
| 33 |
+
matrix8's compiler) with cycle-lockstep spot checks between them;
|
| 34 |
+
- the full self-reproduction runs at threshold fidelity on the matrix
|
| 35 |
+
backend, and the emitted stream must equal the host's file exactly;
|
| 36 |
+
- the offspring file then boots as generation 2 and reproduces again.
|
| 37 |
+
|
| 38 |
+
Usage:
|
| 39 |
+
python constructor8.py build # emit variants/neural_subleq8io.safetensors
|
| 40 |
+
python constructor8.py verify # host soundness + codec + constructor runs
|
| 41 |
+
python constructor8.py self # full self-reproduction at threshold fidelity
|
| 42 |
+
python constructor8.py all
|
| 43 |
+
"""
|
| 44 |
+
|
| 45 |
+
from __future__ import annotations
|
| 46 |
+
|
| 47 |
+
import argparse
|
| 48 |
+
import hashlib
|
| 49 |
+
import json
|
| 50 |
+
import os
|
| 51 |
+
import sys
|
| 52 |
+
import time
|
| 53 |
+
from typing import Dict, List, Optional, Tuple
|
| 54 |
+
|
| 55 |
+
import torch
|
| 56 |
+
from safetensors import safe_open
|
| 57 |
+
from safetensors.torch import save_file
|
| 58 |
+
|
| 59 |
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
| 60 |
+
|
| 61 |
+
REPO = os.path.dirname(os.path.abspath(__file__))
|
| 62 |
+
MODEL_PATH = os.path.join(REPO, "variants", "neural_subleq8io.safetensors")
|
| 63 |
+
|
| 64 |
+
from matrix8 import Net, compile_net # machine-1 compiler, reused verbatim
|
| 65 |
+
|
| 66 |
+
IN_DATA, IN_STROBE, OUT = 0xFD, 0xFC, 0xFE
|
| 67 |
+
HALT_PC = 0xFF
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
# =============================================================================
|
| 71 |
+
# Build the host file (family per-gate style, .inputs wiring, ternary strict)
|
| 72 |
+
# =============================================================================
|
| 73 |
+
|
| 74 |
+
def build_host(save: bool = True) -> str:
|
| 75 |
+
import build as B
|
| 76 |
+
from quantize import quantize_tensors
|
| 77 |
+
|
| 78 |
+
tensors: Dict[str, torch.Tensor] = {}
|
| 79 |
+
B.add_decoder(tensors, 8, 256)
|
| 80 |
+
B.add_memory_read_mux(tensors, 256)
|
| 81 |
+
B.add_memory_write_cells(tensors, 256)
|
| 82 |
+
B.add_subleq_core(tensors)
|
| 83 |
+
tensors["manifest.data_bits"] = torch.tensor([8.0])
|
| 84 |
+
tensors["manifest.addr_bits"] = torch.tensor([8.0])
|
| 85 |
+
tensors["manifest.memory_bytes"] = torch.tensor([256.0])
|
| 86 |
+
tensors["manifest.pc_width"] = torch.tensor([8.0])
|
| 87 |
+
tensors["manifest.version"] = torch.tensor([4.0])
|
| 88 |
+
tensors, reg, stats = B.build_inputs(tensors)
|
| 89 |
+
tensors, counts, _, _ = quantize_tensors(tensors, ternary=False)
|
| 90 |
+
for k, t in tensors.items():
|
| 91 |
+
if k.endswith(".weight") and not k.startswith("manifest."):
|
| 92 |
+
tf = t.float() if t.dtype.is_floating_point else t.to(torch.float32)
|
| 93 |
+
assert (tf.abs() <= 1).all(), f"non-ternary weight {k}"
|
| 94 |
+
meta = {
|
| 95 |
+
"machine": "subleq8io",
|
| 96 |
+
"weight_quantization": "ternary",
|
| 97 |
+
"signal_registry": reg.to_metadata(),
|
| 98 |
+
"io": json.dumps({"in_data": IN_DATA, "in_strobe": IN_STROBE,
|
| 99 |
+
"out": OUT, "halt_pc": HALT_PC}),
|
| 100 |
+
}
|
| 101 |
+
if save:
|
| 102 |
+
save_file(tensors, MODEL_PATH, metadata=meta)
|
| 103 |
+
sz = os.path.getsize(MODEL_PATH)
|
| 104 |
+
print(f" built {MODEL_PATH}: {stats['added']} wired gates, "
|
| 105 |
+
f"{len(tensors)} tensors, {sz:,} bytes")
|
| 106 |
+
return MODEL_PATH
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
# =============================================================================
|
| 110 |
+
# Recipe codec (the construction-description language)
|
| 111 |
+
# =============================================================================
|
| 112 |
+
|
| 113 |
+
def describe(data: bytes) -> bytes:
|
| 114 |
+
"""Compile raw bytes into a construction recipe for the constructor."""
|
| 115 |
+
tape = bytearray()
|
| 116 |
+
i, n = 0, len(data)
|
| 117 |
+
while i < n:
|
| 118 |
+
j = i
|
| 119 |
+
while j < n and data[j] == data[i] and j - i < 127:
|
| 120 |
+
j += 1
|
| 121 |
+
if j - i >= 4:
|
| 122 |
+
tape.append(256 - (j - i)) # repeat tag 129..252
|
| 123 |
+
tape.append((256 - data[i]) % 256) # value, negated
|
| 124 |
+
i = j
|
| 125 |
+
continue
|
| 126 |
+
k = i
|
| 127 |
+
while k < n and k - i < 127: # literal until a run starts
|
| 128 |
+
m = k
|
| 129 |
+
while m < n and data[m] == data[k] and m - k < 4:
|
| 130 |
+
m += 1
|
| 131 |
+
if m - k >= 4:
|
| 132 |
+
break
|
| 133 |
+
k += 1
|
| 134 |
+
k = max(k, i + 1)
|
| 135 |
+
tape.append(k - i) # literal tag 1..127
|
| 136 |
+
tape.extend((256 - x) % 256 for x in data[i:k])
|
| 137 |
+
i = k
|
| 138 |
+
tape.append(0) # END
|
| 139 |
+
return bytes(tape)
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
def decode(tape: bytes) -> bytes:
|
| 143 |
+
"""Pure-Python reference semantics of the recipe language."""
|
| 144 |
+
out = bytearray()
|
| 145 |
+
i = 0
|
| 146 |
+
while True:
|
| 147 |
+
t = tape[i]
|
| 148 |
+
i += 1
|
| 149 |
+
if t == 0:
|
| 150 |
+
return bytes(out)
|
| 151 |
+
if t <= 127:
|
| 152 |
+
for _ in range(t):
|
| 153 |
+
out.append((256 - tape[i]) % 256)
|
| 154 |
+
i += 1
|
| 155 |
+
else:
|
| 156 |
+
out.extend([(256 - tape[i]) % 256] * (256 - t))
|
| 157 |
+
i += 1
|
| 158 |
+
|
| 159 |
+
|
| 160 |
+
# =============================================================================
|
| 161 |
+
# The constructor program (21 SUBLEQ instructions + 4 variable cells)
|
| 162 |
+
# =============================================================================
|
| 163 |
+
|
| 164 |
+
Z, ONE, T1, T2 = 0xF0, 0xF1, 0xF2, 0xF3
|
| 165 |
+
|
| 166 |
+
CONSTRUCTOR = [
|
| 167 |
+
# main loop: read tag T; T1 = -T (== run length for repeat tags), T2 = T
|
| 168 |
+
(T2, T2, 0x03), # 00 T2 = 0
|
| 169 |
+
(T1, T1, 0x06), # 03 T1 = 0
|
| 170 |
+
(IN_DATA, T1, 0x09), # 06 T1 = -T
|
| 171 |
+
(Z, IN_STROBE, 0x0C), # 09 consume tag byte
|
| 172 |
+
(T1, T2, 0x0F), # 0C T2 = T
|
| 173 |
+
(Z, T2, 0x15), # 0F if T == 0 or T >= 128 -> 15, else literal
|
| 174 |
+
(Z, Z, 0x24), # 12 goto LITERAL
|
| 175 |
+
(Z, T1, 0x1B), # 15 T1 = -T: leq only when T == 0 -> END
|
| 176 |
+
(Z, Z, 0x30), # 18 goto REPEAT (T1 holds 256-T = run length)
|
| 177 |
+
(Z, Z, HALT_PC), # 1B END: halt
|
| 178 |
+
(Z, Z, 0x21), # 1E (pad)
|
| 179 |
+
(Z, Z, 0x24), # 21 (pad)
|
| 180 |
+
(IN_DATA, OUT, 0x27), # 24 LITERAL: emit 0 - tape_byte
|
| 181 |
+
(Z, IN_STROBE, 0x2A), # 27 consume it
|
| 182 |
+
(ONE, T2, 0x00), # 2A T2 -= 1; done -> main loop
|
| 183 |
+
(Z, Z, 0x24), # 2D next literal byte
|
| 184 |
+
(IN_DATA, OUT, 0x33), # 30 REPEAT: emit 0 - value (no strobe)
|
| 185 |
+
(ONE, T1, 0x39), # 33 T1 -= 1; done -> 39
|
| 186 |
+
(Z, Z, 0x30), # 36 emit again
|
| 187 |
+
(Z, IN_STROBE, 0x3C), # 39 consume the value byte
|
| 188 |
+
(Z, Z, 0x00), # 3C goto main loop
|
| 189 |
+
]
|
| 190 |
+
|
| 191 |
+
|
| 192 |
+
def constructor_image() -> List[int]:
|
| 193 |
+
mem = [0] * 256
|
| 194 |
+
for idx, (a, b, c) in enumerate(CONSTRUCTOR):
|
| 195 |
+
mem[idx * 3] = a
|
| 196 |
+
mem[idx * 3 + 1] = b
|
| 197 |
+
mem[idx * 3 + 2] = c
|
| 198 |
+
mem[ONE] = 1
|
| 199 |
+
return mem
|
| 200 |
+
|
| 201 |
+
|
| 202 |
+
# =============================================================================
|
| 203 |
+
# Backend 1: fast pure-integer reference with devices
|
| 204 |
+
# =============================================================================
|
| 205 |
+
|
| 206 |
+
def ref_construct(tape: bytes, max_steps: int = 1 << 34,
|
| 207 |
+
) -> Tuple[bytes, int]:
|
| 208 |
+
mem = constructor_image()
|
| 209 |
+
mem[IN_DATA] = tape[0] if tape else 0
|
| 210 |
+
out = bytearray()
|
| 211 |
+
head = 0
|
| 212 |
+
pc = 0
|
| 213 |
+
steps = 0
|
| 214 |
+
tn = len(tape)
|
| 215 |
+
while pc != HALT_PC and steps < max_steps:
|
| 216 |
+
A = mem[pc]
|
| 217 |
+
Bc = mem[(pc + 1) & 0xFF]
|
| 218 |
+
C = mem[(pc + 2) & 0xFF]
|
| 219 |
+
r = (mem[Bc] - mem[A]) & 0xFF
|
| 220 |
+
mem[Bc] = r
|
| 221 |
+
pc = C if (r == 0 or r >= 0x80) else (pc + 3) & 0xFF
|
| 222 |
+
if Bc == OUT:
|
| 223 |
+
out.append(r)
|
| 224 |
+
mem[OUT] = 0
|
| 225 |
+
elif Bc == IN_STROBE:
|
| 226 |
+
head += 1
|
| 227 |
+
mem[IN_STROBE] = 0
|
| 228 |
+
mem[IN_DATA] = tape[head] if head < tn else 0
|
| 229 |
+
steps += 1
|
| 230 |
+
return bytes(out), steps
|
| 231 |
+
|
| 232 |
+
|
| 233 |
+
# =============================================================================
|
| 234 |
+
# Backend 2: the gate-graph processor (shipped netlist) with devices
|
| 235 |
+
# =============================================================================
|
| 236 |
+
|
| 237 |
+
class GateHost:
|
| 238 |
+
def __init__(self, path: str = MODEL_PATH):
|
| 239 |
+
from machines import SubleqThresholdCPU, load_tensors
|
| 240 |
+
from eval import load_metadata
|
| 241 |
+
T = load_tensors(path)
|
| 242 |
+
reg = load_metadata(path)["signal_registry"]
|
| 243 |
+
self.cpu = SubleqThresholdCPU(T, reg)
|
| 244 |
+
|
| 245 |
+
def run(self, tape: bytes, max_steps: int) -> Tuple[bytes, int, dict]:
|
| 246 |
+
s = {"pc": 0, "mem": constructor_image(), "halted": False}
|
| 247 |
+
s["mem"][IN_DATA] = tape[0] if tape else 0
|
| 248 |
+
out = bytearray()
|
| 249 |
+
head = 0
|
| 250 |
+
n = 0
|
| 251 |
+
while not s["halted"] and n < max_steps:
|
| 252 |
+
Bc = s["mem"][(s["pc"] + 1) & 0xFF]
|
| 253 |
+
s = self.cpu.step(s)
|
| 254 |
+
if Bc == OUT:
|
| 255 |
+
out.append(s["mem"][OUT])
|
| 256 |
+
s["mem"][OUT] = 0
|
| 257 |
+
elif Bc == IN_STROBE:
|
| 258 |
+
head += 1
|
| 259 |
+
s["mem"][IN_STROBE] = 0
|
| 260 |
+
s["mem"][IN_DATA] = tape[head] if head < len(tape) else 0
|
| 261 |
+
n += 1
|
| 262 |
+
return bytes(out), n, s
|
| 263 |
+
|
| 264 |
+
|
| 265 |
+
# =============================================================================
|
| 266 |
+
# Backend 3: the host compiled to recurrent ternary matrices (machine 1 tech)
|
| 267 |
+
# =============================================================================
|
| 268 |
+
|
| 269 |
+
def subleq_state_names() -> List[str]:
|
| 270 |
+
names = [f"pc{i}" for i in range(8)] + ["halt"]
|
| 271 |
+
for j in range(256):
|
| 272 |
+
names += [f"m{j}b{i}" for i in range(8)]
|
| 273 |
+
return names
|
| 274 |
+
|
| 275 |
+
|
| 276 |
+
def build_subleq_step_net() -> Tuple[Net, List[str], List[str]]:
|
| 277 |
+
net = Net()
|
| 278 |
+
pc = [f"pc{i}" for i in range(8)]
|
| 279 |
+
halt = "halt"
|
| 280 |
+
mem = [[f"m{j}b{i}" for i in range(8)] for j in range(256)]
|
| 281 |
+
|
| 282 |
+
P = [net.DECODE(f"pd{j}", pc, j) for j in range(256)]
|
| 283 |
+
byte = []
|
| 284 |
+
for o in range(3): # A, B, C operand bytes
|
| 285 |
+
bo = [net.OR(f"f{o}b{bit}",
|
| 286 |
+
[net.AND(f"f{o}a{j}b{bit}", [mem[j][bit], P[(j - o) % 256]])
|
| 287 |
+
for j in range(256)]) for bit in range(8)]
|
| 288 |
+
byte.append(bo)
|
| 289 |
+
A, Bb, C = byte
|
| 290 |
+
|
| 291 |
+
AD = [net.DECODE(f"ad{j}", A, j) for j in range(256)]
|
| 292 |
+
BD = [net.DECODE(f"bd{j}", Bb, j) for j in range(256)]
|
| 293 |
+
x = [net.OR(f"x{bit}", [net.AND(f"x{bit}a{j}", [mem[j][bit], AD[j]])
|
| 294 |
+
for j in range(256)]) for bit in range(8)]
|
| 295 |
+
y = [net.OR(f"y{bit}", [net.AND(f"y{bit}a{j}", [mem[j][bit], BD[j]])
|
| 296 |
+
for j in range(256)]) for bit in range(8)]
|
| 297 |
+
|
| 298 |
+
nx = [net.NOT(f"nx{k}", x[7 - k]) for k in range(8)] # LSB-first
|
| 299 |
+
c = "#1"
|
| 300 |
+
r_l = []
|
| 301 |
+
for k in range(8):
|
| 302 |
+
s_, c = net.FA(f"sub.fa{k}", y[7 - k], nx[k], c)
|
| 303 |
+
r_l.append(s_)
|
| 304 |
+
r = [r_l[7 - i] for i in range(8)]
|
| 305 |
+
|
| 306 |
+
zero = net.NOR("rzero", r)
|
| 307 |
+
leq = net.OR("leq", [r[0], zero])
|
| 308 |
+
|
| 309 |
+
pc_l = [pc[7 - k] for k in range(8)]
|
| 310 |
+
c = "#0"
|
| 311 |
+
p3_l = []
|
| 312 |
+
for k in range(8):
|
| 313 |
+
addend = "#1" if k in (0, 1) else "#0"
|
| 314 |
+
s_, c = net.FA(f"pc3.fa{k}", pc_l[k], addend, c)
|
| 315 |
+
p3_l.append(s_)
|
| 316 |
+
p3 = [p3_l[7 - i] for i in range(8)]
|
| 317 |
+
|
| 318 |
+
pcm = [net.MUX(f"pcm{i}", leq, C[i], p3[i]) for i in range(8)]
|
| 319 |
+
pcn = [net.MUX(f"pcn{i}", halt, pc[i], pcm[i]) for i in range(8)]
|
| 320 |
+
hdec = net.DECODE("hdec", pcn, HALT_PC)
|
| 321 |
+
halt_n = net.OR("haltn", [halt, hdec])
|
| 322 |
+
|
| 323 |
+
nh = net.NOT("nh", halt)
|
| 324 |
+
mem_n = []
|
| 325 |
+
for j in range(256):
|
| 326 |
+
ws = net.AND(f"ws{j}", [BD[j], nh])
|
| 327 |
+
nws = net.NOT(f"nws{j}", ws)
|
| 328 |
+
row = [net.OR(f"mn{j}b{bit}",
|
| 329 |
+
[net.AND(f"mn{j}b{bit}o", [mem[j][bit], nws]),
|
| 330 |
+
net.AND(f"mn{j}b{bit}n", [r[bit], ws])])
|
| 331 |
+
for bit in range(8)]
|
| 332 |
+
mem_n.append(row)
|
| 333 |
+
|
| 334 |
+
outputs = pcn + [halt_n] + [s for row in mem_n for s in row]
|
| 335 |
+
return net, subleq_state_names(), outputs
|
| 336 |
+
|
| 337 |
+
|
| 338 |
+
class MatrixHost:
|
| 339 |
+
"""The subleq8io processor as a recurrent ternary linear-threshold
|
| 340 |
+
network, with the device cells poked between recurrences."""
|
| 341 |
+
|
| 342 |
+
N = 8 + 1 + 2048
|
| 343 |
+
|
| 344 |
+
def __init__(self, device: str = "cpu"):
|
| 345 |
+
t0 = time.perf_counter()
|
| 346 |
+
net, inputs, outputs = build_subleq_step_net()
|
| 347 |
+
layers, info = compile_net(net, inputs, outputs)
|
| 348 |
+
for W, Bt in layers:
|
| 349 |
+
vals = set(torch.unique(W).tolist())
|
| 350 |
+
assert vals <= {-1, 0, 1}
|
| 351 |
+
self.W = [W.to(device=device, dtype=torch.float32) for W, _ in layers]
|
| 352 |
+
self.B = [Bt.to(device=device, dtype=torch.float32) for _, Bt in layers]
|
| 353 |
+
self.device = device
|
| 354 |
+
self.info = info
|
| 355 |
+
print(f" host compiled to {info['layers']} ternary matrices "
|
| 356 |
+
f"(max width {info['max_width']}, "
|
| 357 |
+
f"{info['total_weights']:,} weights) in "
|
| 358 |
+
f"{time.perf_counter() - t0:.1f}s on {device}")
|
| 359 |
+
|
| 360 |
+
def _vec(self, pc: int, mem: List[int], halt: bool = False) -> torch.Tensor:
|
| 361 |
+
v = torch.zeros(self.N)
|
| 362 |
+
for k in range(8):
|
| 363 |
+
v[k] = (pc >> (7 - k)) & 1
|
| 364 |
+
v[8] = 1.0 if halt else 0.0
|
| 365 |
+
for j in range(256):
|
| 366 |
+
for k in range(8):
|
| 367 |
+
v[9 + j * 8 + k] = (mem[j] >> (7 - k)) & 1
|
| 368 |
+
return v
|
| 369 |
+
|
| 370 |
+
def step(self, v: torch.Tensor) -> torch.Tensor:
|
| 371 |
+
for W, b in zip(self.W, self.B):
|
| 372 |
+
v = ((v @ W.T + b) >= 0).float()
|
| 373 |
+
return v
|
| 374 |
+
|
| 375 |
+
@staticmethod
|
| 376 |
+
def _byte(vcpu: torch.Tensor, j: int) -> int:
|
| 377 |
+
x = 0
|
| 378 |
+
for k in range(8):
|
| 379 |
+
x = (x << 1) | int(vcpu[9 + j * 8 + k])
|
| 380 |
+
return x
|
| 381 |
+
|
| 382 |
+
def run(self, tape: bytes, max_steps: int, expect: Optional[bytes] = None,
|
| 383 |
+
report_every: int = 100000) -> Tuple[bytes, int]:
|
| 384 |
+
mem = constructor_image()
|
| 385 |
+
mem[IN_DATA] = tape[0] if tape else 0
|
| 386 |
+
v = self._vec(0, mem).unsqueeze(0).to(self.device)
|
| 387 |
+
out = bytearray()
|
| 388 |
+
head = 0
|
| 389 |
+
n = 0
|
| 390 |
+
idx_pch = torch.arange(0, 9, device=self.device)
|
| 391 |
+
t0 = time.perf_counter()
|
| 392 |
+
while n < max_steps:
|
| 393 |
+
front = v[0, idx_pch].tolist()
|
| 394 |
+
if front[8] >= 0.5:
|
| 395 |
+
break
|
| 396 |
+
pcv = 0
|
| 397 |
+
for k in range(8):
|
| 398 |
+
pcv = (pcv << 1) | int(front[k])
|
| 399 |
+
bidx = (pcv + 1) & 0xFF
|
| 400 |
+
bbits = v[0, 9 + bidx * 8: 9 + bidx * 8 + 8].tolist()
|
| 401 |
+
Bc = 0
|
| 402 |
+
for k in range(8):
|
| 403 |
+
Bc = (Bc << 1) | int(bbits[k])
|
| 404 |
+
v = self.step(v)
|
| 405 |
+
n += 1
|
| 406 |
+
if Bc == OUT:
|
| 407 |
+
ob = v[0, 9 + OUT * 8: 9 + OUT * 8 + 8].tolist()
|
| 408 |
+
val = 0
|
| 409 |
+
for k in range(8):
|
| 410 |
+
val = (val << 1) | int(ob[k])
|
| 411 |
+
out.append(val)
|
| 412 |
+
if expect is not None and out[-1] != expect[len(out) - 1]:
|
| 413 |
+
raise AssertionError(
|
| 414 |
+
f"stream diverged at byte {len(out) - 1}: "
|
| 415 |
+
f"got {out[-1]:#04x} want {expect[len(out) - 1]:#04x}")
|
| 416 |
+
v[0, 9 + OUT * 8: 9 + OUT * 8 + 8] = 0.0
|
| 417 |
+
elif Bc == IN_STROBE:
|
| 418 |
+
head += 1
|
| 419 |
+
v[0, 9 + IN_STROBE * 8: 9 + IN_STROBE * 8 + 8] = 0.0
|
| 420 |
+
nxt = tape[head] if head < len(tape) else 0
|
| 421 |
+
for k in range(8):
|
| 422 |
+
v[0, 9 + IN_DATA * 8 + k] = float((nxt >> (7 - k)) & 1)
|
| 423 |
+
if report_every and n % report_every == 0:
|
| 424 |
+
rate = n / (time.perf_counter() - t0)
|
| 425 |
+
print(f" ... {n:,} recurrences, {len(out):,} bytes emitted "
|
| 426 |
+
f"({rate:,.0f} instr/s)")
|
| 427 |
+
return bytes(out), n
|
| 428 |
+
|
| 429 |
+
def exhaustive_datapath(self) -> bool:
|
| 430 |
+
"""All 65,536 (x, y) operand pairs through the matrices in one batch."""
|
| 431 |
+
xs = torch.arange(65536) % 256
|
| 432 |
+
ys = torch.arange(65536) // 256
|
| 433 |
+
V = torch.zeros(65536, self.N)
|
| 434 |
+
mem = [0] * 256
|
| 435 |
+
mem[0], mem[1], mem[2] = 0x90, 0x91, 0x30
|
| 436 |
+
v0 = self._vec(0, mem)
|
| 437 |
+
V[:] = v0
|
| 438 |
+
for k in range(8):
|
| 439 |
+
V[:, 9 + 0x90 * 8 + k] = ((xs >> (7 - k)) & 1).float()
|
| 440 |
+
V[:, 9 + 0x91 * 8 + k] = ((ys >> (7 - k)) & 1).float()
|
| 441 |
+
V = V.to(self.device)
|
| 442 |
+
for W, b in zip(self.W, self.B):
|
| 443 |
+
V = ((V @ W.T + b) >= 0).float()
|
| 444 |
+
V = V.cpu()
|
| 445 |
+
r = torch.zeros(65536, dtype=torch.long)
|
| 446 |
+
for k in range(8):
|
| 447 |
+
r = (r << 1) | V[:, 9 + 0x91 * 8 + k].long()
|
| 448 |
+
pcv = torch.zeros(65536, dtype=torch.long)
|
| 449 |
+
for k in range(8):
|
| 450 |
+
pcv = (pcv << 1) | V[:, k].long()
|
| 451 |
+
exp_r = (ys - xs) & 0xFF
|
| 452 |
+
exp_leq = (exp_r == 0) | (exp_r >= 0x80)
|
| 453 |
+
exp_pc = torch.where(exp_leq, torch.full_like(pcv, 0x30),
|
| 454 |
+
torch.full_like(pcv, 3))
|
| 455 |
+
ok = bool((r == exp_r).all()) and bool((pcv == exp_pc).all())
|
| 456 |
+
print(f" {'OK ' if ok else 'FAIL'} matrix datapath exhaustive: "
|
| 457 |
+
f"65,536/65,536 subtract results and branch decisions exact")
|
| 458 |
+
return ok
|
| 459 |
+
|
| 460 |
+
|
| 461 |
+
# =============================================================================
|
| 462 |
+
# Verification
|
| 463 |
+
# =============================================================================
|
| 464 |
+
|
| 465 |
+
def sha(b: bytes) -> str:
|
| 466 |
+
return hashlib.sha256(b).hexdigest()[:16]
|
| 467 |
+
|
| 468 |
+
|
| 469 |
+
def family_files() -> List[str]:
|
| 470 |
+
files = [os.path.join(REPO, "neural_computer.safetensors")]
|
| 471 |
+
vdir = os.path.join(REPO, "variants")
|
| 472 |
+
files += sorted(os.path.join(vdir, f) for f in os.listdir(vdir)
|
| 473 |
+
if f.endswith(".safetensors"))
|
| 474 |
+
return files
|
| 475 |
+
|
| 476 |
+
|
| 477 |
+
def verify(device: str) -> bool:
|
| 478 |
+
ok = True
|
| 479 |
+
|
| 480 |
+
print("\n[1/5] Host machine soundness (shipped netlist, exhaustive datapath)")
|
| 481 |
+
from eval import NetlistEvaluator, load_metadata
|
| 482 |
+
from machines import load_tensors
|
| 483 |
+
T = load_tensors(MODEL_PATH)
|
| 484 |
+
reg = load_metadata(MODEL_PATH)["signal_registry"]
|
| 485 |
+
ne = NetlistEvaluator(T, reg, "subleq")
|
| 486 |
+
a_v = torch.arange(65536) % 256
|
| 487 |
+
b_v = torch.arange(65536) // 256
|
| 488 |
+
ext = {}
|
| 489 |
+
for k in range(8):
|
| 490 |
+
ext[f"$a[{k}]"] = ((a_v >> k) & 1).float()
|
| 491 |
+
ext[f"$b[{k}]"] = ((b_v >> k) & 1).float()
|
| 492 |
+
ext[f"$pc[{k}]"] = torch.zeros(65536)
|
| 493 |
+
ext[f"$c[{k}]"] = torch.zeros(65536)
|
| 494 |
+
o = ne.run(ext)
|
| 495 |
+
rr = sum(o[f"subleq.sub.fa{k}.ha2.sum.layer2"][:, 0].long() << k for k in range(8))
|
| 496 |
+
lq = o["subleq.leq"][:, 0].long()
|
| 497 |
+
exp_r = (b_v - a_v) & 0xFF
|
| 498 |
+
exp_l = ((exp_r == 0) | (exp_r >= 128)).long()
|
| 499 |
+
good = bool((rr == exp_r).all()) and bool((lq == exp_l).all())
|
| 500 |
+
print(f" {'OK ' if good else 'FAIL'} gate netlist exhaustive: 65,536/65,536 exact")
|
| 501 |
+
ok &= good
|
| 502 |
+
|
| 503 |
+
print("\n[2/5] Recipe codec round-trip over the whole family")
|
| 504 |
+
total = 0
|
| 505 |
+
for p in family_files():
|
| 506 |
+
data = open(p, "rb").read()
|
| 507 |
+
tape = describe(data)
|
| 508 |
+
good = decode(tape) == data
|
| 509 |
+
ok &= good
|
| 510 |
+
total += len(data)
|
| 511 |
+
print(f" {'OK ' if good else 'FAIL'} {os.path.basename(p):<44} "
|
| 512 |
+
f"{len(data):>10,} B -> tape {len(tape):>10,} B sha {sha(data)}")
|
| 513 |
+
print(f" ({total / 1e6:.0f} MB described and decoded back, byte-identical)")
|
| 514 |
+
|
| 515 |
+
print("\n[3/5] Constructor program on the integer reference "
|
| 516 |
+
"(output stream == target file, byte for byte)")
|
| 517 |
+
targets = [MODEL_PATH,
|
| 518 |
+
os.path.join(REPO, "variants", "neural_subleq8.safetensors"),
|
| 519 |
+
os.path.join(REPO, "variants", "neural_matrix8.safetensors")]
|
| 520 |
+
for p in targets:
|
| 521 |
+
data = open(p, "rb").read()
|
| 522 |
+
tape = describe(data)
|
| 523 |
+
t0 = time.perf_counter()
|
| 524 |
+
out, steps = ref_construct(tape)
|
| 525 |
+
dt = time.perf_counter() - t0
|
| 526 |
+
good = out == data
|
| 527 |
+
ok &= good
|
| 528 |
+
tag = "SELF-REPRODUCTION" if p == MODEL_PATH else "universal build"
|
| 529 |
+
print(f" {'OK ' if good else 'FAIL'} {os.path.basename(p):<40} "
|
| 530 |
+
f"{len(out):>10,} B in {steps:>12,} instructions ({dt:.1f}s) "
|
| 531 |
+
f"[{tag}] sha {sha(out)}")
|
| 532 |
+
|
| 533 |
+
print("\n[4/5] Matrix backend (machine-1 compile of the host)")
|
| 534 |
+
mh = MatrixHost(device=device)
|
| 535 |
+
ok &= mh.exhaustive_datapath()
|
| 536 |
+
|
| 537 |
+
print("\n[5/5] Backend lockstep on a shared prefix "
|
| 538 |
+
"(gate graph vs matrices vs reference, first 600 cycles)")
|
| 539 |
+
data = open(MODEL_PATH, "rb").read()
|
| 540 |
+
tape = describe(data)
|
| 541 |
+
gh = GateHost()
|
| 542 |
+
s = {"pc": 0, "mem": constructor_image(), "halted": False}
|
| 543 |
+
s["mem"][IN_DATA] = tape[0]
|
| 544 |
+
mem2 = constructor_image()
|
| 545 |
+
mem2[IN_DATA] = tape[0]
|
| 546 |
+
v = mh._vec(0, mem2).unsqueeze(0).to(device)
|
| 547 |
+
headg = headm = 0
|
| 548 |
+
outg, outm = bytearray(), bytearray()
|
| 549 |
+
good = True
|
| 550 |
+
for n in range(600):
|
| 551 |
+
Bg = s["mem"][(s["pc"] + 1) & 0xFF]
|
| 552 |
+
s = gh.cpu.step(s)
|
| 553 |
+
if Bg == OUT:
|
| 554 |
+
outg.append(s["mem"][OUT])
|
| 555 |
+
s["mem"][OUT] = 0
|
| 556 |
+
elif Bg == IN_STROBE:
|
| 557 |
+
headg += 1
|
| 558 |
+
s["mem"][IN_STROBE] = 0
|
| 559 |
+
s["mem"][IN_DATA] = tape[headg] if headg < len(tape) else 0
|
| 560 |
+
vc = v[0].cpu()
|
| 561 |
+
pcm = 0
|
| 562 |
+
for k in range(8):
|
| 563 |
+
pcm = (pcm << 1) | int(vc[k])
|
| 564 |
+
Bm = MatrixHost._byte(vc, (pcm + 1) & 0xFF)
|
| 565 |
+
v = mh.step(v)
|
| 566 |
+
if Bm == OUT:
|
| 567 |
+
outm.append(MatrixHost._byte(v[0].cpu(), OUT))
|
| 568 |
+
v[0, 9 + OUT * 8: 9 + OUT * 8 + 8] = 0.0
|
| 569 |
+
elif Bm == IN_STROBE:
|
| 570 |
+
headm += 1
|
| 571 |
+
v[0, 9 + IN_STROBE * 8: 9 + IN_STROBE * 8 + 8] = 0.0
|
| 572 |
+
nxt = tape[headm] if headm < len(tape) else 0
|
| 573 |
+
for k in range(8):
|
| 574 |
+
v[0, 9 + IN_DATA * 8 + k] = float((nxt >> (7 - k)) & 1)
|
| 575 |
+
vc = v[0].cpu()
|
| 576 |
+
pcm = 0
|
| 577 |
+
for k in range(8):
|
| 578 |
+
pcm = (pcm << 1) | int(vc[k])
|
| 579 |
+
memm = [MatrixHost._byte(vc, j) for j in range(256)]
|
| 580 |
+
if pcm != s["pc"] or memm != s["mem"] or outg != outm:
|
| 581 |
+
print(f" FAIL lockstep diverged at cycle {n}")
|
| 582 |
+
good = False
|
| 583 |
+
break
|
| 584 |
+
print(f" {'OK ' if good else 'FAIL'} gate graph and matrix form agree "
|
| 585 |
+
f"cycle-for-cycle for 600 cycles ({len(outg)} bytes emitted identically)")
|
| 586 |
+
ok &= good
|
| 587 |
+
|
| 588 |
+
print("\nCONSTRUCTOR VERIFICATION:", "PASS" if ok else "FAIL")
|
| 589 |
+
return ok
|
| 590 |
+
|
| 591 |
+
|
| 592 |
+
def self_reproduce(device: str, gen2: bool = True) -> bool:
|
| 593 |
+
"""Full self-reproduction at threshold fidelity on the matrix backend."""
|
| 594 |
+
data = open(MODEL_PATH, "rb").read()
|
| 595 |
+
tape = describe(data)
|
| 596 |
+
print(f"\nSelf-reproduction on the threshold matrices: target "
|
| 597 |
+
f"{len(data):,} bytes (sha {sha(data)}), tape {len(tape):,} bytes")
|
| 598 |
+
ref_out, ref_steps = ref_construct(tape)
|
| 599 |
+
assert ref_out == data, "reference construction failed"
|
| 600 |
+
print(f" reference: {ref_steps:,} instructions")
|
| 601 |
+
|
| 602 |
+
mh = MatrixHost(device=device)
|
| 603 |
+
t0 = time.perf_counter()
|
| 604 |
+
out, n = mh.run(tape, max_steps=ref_steps + 64, expect=data)
|
| 605 |
+
dt = time.perf_counter() - t0
|
| 606 |
+
ok = out == data and n == ref_steps
|
| 607 |
+
print(f" GEN 1: emitted {len(out):,} bytes in {n:,} recurrences "
|
| 608 |
+
f"({dt / 60:.1f} min, {n / dt:,.0f} instr/s) sha {sha(out)}")
|
| 609 |
+
print(f" {'OK ' if ok else 'FAIL'} output stream == host safetensors file, "
|
| 610 |
+
f"byte for byte; instruction count matches reference exactly")
|
| 611 |
+
|
| 612 |
+
if ok and gen2:
|
| 613 |
+
off_path = os.path.join(REPO, "variants", "_offspring_subleq8io.safetensors")
|
| 614 |
+
with open(off_path, "wb") as f:
|
| 615 |
+
f.write(out)
|
| 616 |
+
gh = GateHost(off_path) # boot the offspring's own gates
|
| 617 |
+
probe_tape = describe(out[:2048])
|
| 618 |
+
want = out[:2048]
|
| 619 |
+
got, steps, _ = gh.run(probe_tape, max_steps=len(probe_tape) * 8 + 64)
|
| 620 |
+
good2 = got == want
|
| 621 |
+
print(f" GEN 2: offspring file booted as a machine (gate graph); "
|
| 622 |
+
f"constructed the first {len(want):,} bytes of itself in "
|
| 623 |
+
f"{steps:,} instructions: {'exact' if good2 else 'MISMATCH'}")
|
| 624 |
+
os.remove(off_path)
|
| 625 |
+
ok &= good2
|
| 626 |
+
print("SELF-REPRODUCTION:", "PASS" if ok else "FAIL")
|
| 627 |
+
return ok
|
| 628 |
+
|
| 629 |
+
|
| 630 |
+
def main() -> int:
|
| 631 |
+
ap = argparse.ArgumentParser()
|
| 632 |
+
ap.add_argument("cmd", choices=["build", "verify", "self", "all"])
|
| 633 |
+
ap.add_argument("--device", default="cuda" if torch.cuda.is_available() else "cpu")
|
| 634 |
+
args = ap.parse_args()
|
| 635 |
+
rc = 0
|
| 636 |
+
if args.cmd in ("build", "all"):
|
| 637 |
+
build_host(save=True)
|
| 638 |
+
if args.cmd in ("verify", "all"):
|
| 639 |
+
rc |= 0 if verify(args.device) else 1
|
| 640 |
+
if args.cmd in ("self", "all"):
|
| 641 |
+
rc |= 0 if self_reproduce(args.device) else 1
|
| 642 |
+
return rc
|
| 643 |
+
|
| 644 |
+
|
| 645 |
+
if __name__ == "__main__":
|
| 646 |
+
sys.exit(main())
|
eval_all.py
CHANGED
|
@@ -615,16 +615,26 @@ def list_safetensors(path: Path) -> List[Path]:
|
|
| 615 |
return []
|
| 616 |
|
| 617 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 618 |
def evaluate_one(path: Path, device: str, pop_size: int, debug: bool, run_cpu_program: bool) -> Dict:
|
| 619 |
out: Dict = {"path": str(path), "filename": path.name}
|
| 620 |
-
#
|
| 621 |
-
# circuit inventory and are verified by machines.py, not the gate-fitness
|
| 622 |
-
# suite; skip them cleanly rather than error on missing standard gates.
|
| 623 |
with safe_open(str(path), framework="pt") as f:
|
| 624 |
meta = f.metadata() or {}
|
| 625 |
if meta.get("machine"):
|
| 626 |
-
|
| 627 |
-
|
|
|
|
| 628 |
return out
|
| 629 |
try:
|
| 630 |
tensors = load_model(str(path))
|
|
@@ -724,7 +734,8 @@ def evaluate_one(path: Path, device: str, pop_size: int, debug: bool, run_cpu_pr
|
|
| 724 |
|
| 725 |
def print_row(r: Dict, show_cpu: bool) -> None:
|
| 726 |
if r.get("status") == "SKIP":
|
| 727 |
-
|
|
|
|
| 728 |
return
|
| 729 |
if "error" in r:
|
| 730 |
print(f" {r['filename']:<48} ERROR: {r['error'][:80]}")
|
|
|
|
| 615 |
return []
|
| 616 |
|
| 617 |
|
| 618 |
+
# Standalone machines carry their own circuit inventory and are verified by
|
| 619 |
+
# their own scripts, not the gate-fitness suite. Each maps to the tool that
|
| 620 |
+
# checks it.
|
| 621 |
+
MACHINE_VERIFIER = {
|
| 622 |
+
"subleq8": "machines.py",
|
| 623 |
+
"rv32": "machines.py",
|
| 624 |
+
"matrix8": "matrix8.py",
|
| 625 |
+
"subleq8io": "constructor8.py",
|
| 626 |
+
}
|
| 627 |
+
|
| 628 |
+
|
| 629 |
def evaluate_one(path: Path, device: str, pop_size: int, debug: bool, run_cpu_program: bool) -> Dict:
|
| 630 |
out: Dict = {"path": str(path), "filename": path.name}
|
| 631 |
+
# Skip standalone machines cleanly rather than error on missing standard gates.
|
|
|
|
|
|
|
| 632 |
with safe_open(str(path), framework="pt") as f:
|
| 633 |
meta = f.metadata() or {}
|
| 634 |
if meta.get("machine"):
|
| 635 |
+
m = meta["machine"]
|
| 636 |
+
out.update(status="SKIP", machine=m,
|
| 637 |
+
note=f"standalone machine — verify with {MACHINE_VERIFIER.get(m, 'machines.py')}")
|
| 638 |
return out
|
| 639 |
try:
|
| 640 |
tensors = load_model(str(path))
|
|
|
|
| 734 |
|
| 735 |
def print_row(r: Dict, show_cpu: bool) -> None:
|
| 736 |
if r.get("status") == "SKIP":
|
| 737 |
+
m = r.get("machine", "machine")
|
| 738 |
+
print(f" {r['filename']:<48} SKIP ({m} — verify with {MACHINE_VERIFIER.get(m, 'machines.py')})")
|
| 739 |
return
|
| 740 |
if "error" in r:
|
| 741 |
print(f" {r['filename']:<48} ERROR: {r['error'][:80]}")
|
matrix8.py
ADDED
|
@@ -0,0 +1,1081 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""neural_matrix8 — the 8-bit threshold CPU as a recurrent ternary linear-threshold
|
| 2 |
+
network.
|
| 3 |
+
|
| 4 |
+
The verified gate wiring of the registers-profile CPU (8-bit data, 4-bit
|
| 5 |
+
addresses, 16 B memory) is compiled into a fixed sequence of ternary weight
|
| 6 |
+
matrices W1..Wk with a Heaviside step between them, acting on a 173-bit state
|
| 7 |
+
vector that holds the program counter, all four registers, the flags, the
|
| 8 |
+
stack pointer, the halt bit, and every memory bit:
|
| 9 |
+
|
| 10 |
+
[ PC[4] | R0[8] R1[8] R2[8] R3[8] | Z N C V | SP[4] | HALT | MEM[16][8] ]
|
| 11 |
+
|
| 12 |
+
One instruction is one pass through the stack (s' = H(Wk @ ... H(W1 @ s + b1)
|
| 13 |
+
... + bk)); the machine is the stack iterated until the halt bit sets, so the
|
| 14 |
+
whole processor is a single recurrent linear-threshold network. The transition
|
| 15 |
+
is total: a halted state is a fixed point (every architectural write is gated
|
| 16 |
+
by NOT halt), so iteration past halt is harmless.
|
| 17 |
+
|
| 18 |
+
Every weight is in {-1, 0, 1} and every bias is a small integer; the circuit
|
| 19 |
+
is assembled from the same verified cell constructions the family ships
|
| 20 |
+
(two-layer OR/NAND XOR cells, ha1/ha2/carry_or full adders, bit-cascade
|
| 21 |
+
comparators, 2:1 mux cells, one-threshold-gate address decode rows), then
|
| 22 |
+
levelized ASAP with identity pass-throughs (H(x-1)) carrying live signals
|
| 23 |
+
between layers.
|
| 24 |
+
|
| 25 |
+
Equality with the gate-graph processor is machine-checked, bit for bit:
|
| 26 |
+
- exhaustively over all 65,536 (a, b) operand pairs for every ALU opcode
|
| 27 |
+
(ADD SUB AND OR XOR SHL SHR MUL DIV CMP), batched through the matrices;
|
| 28 |
+
- exhaustively over all Jcc condition x flag combinations, all JMP/CALL
|
| 29 |
+
targets and SP values, and all LOAD/STORE address/data pairs;
|
| 30 |
+
- by full-state lockstep against GenericThresholdCPU walking the shipped
|
| 31 |
+
neural_computer8_registers weights, per instruction, on a micro-program
|
| 32 |
+
suite covering every opcode;
|
| 33 |
+
- by single-step agreement with a pure-integer reference on random states.
|
| 34 |
+
|
| 35 |
+
Analog realization: pre-activations are integers, 1-firing gates sit at >= 0
|
| 36 |
+
and 0-gates at <= -1, so placing the analog comparator threshold at -0.5 gives
|
| 37 |
+
every gate a symmetric guaranteed noise margin of 0.5 — any total analog error
|
| 38 |
+
(read noise + conductance mismatch) below 0.5 per pre-activation provably
|
| 39 |
+
reproduces the digital circuit bit-exactly. The analog suite measures the
|
| 40 |
+
margin, then sweeps Gaussian read noise and static ternary-conductance
|
| 41 |
+
mismatch to locate the empirical breakdown against the guarantee.
|
| 42 |
+
|
| 43 |
+
Usage:
|
| 44 |
+
python matrix8.py build # compile + save variants/neural_matrix8.safetensors
|
| 45 |
+
python matrix8.py verify # equality suite (build in memory if file absent)
|
| 46 |
+
python matrix8.py analog # margin + noise / mismatch sweeps
|
| 47 |
+
python matrix8.py all # build + verify + analog
|
| 48 |
+
"""
|
| 49 |
+
|
| 50 |
+
from __future__ import annotations
|
| 51 |
+
|
| 52 |
+
import argparse
|
| 53 |
+
import json
|
| 54 |
+
import os
|
| 55 |
+
import sys
|
| 56 |
+
import time
|
| 57 |
+
from typing import Dict, List, Optional, Tuple
|
| 58 |
+
|
| 59 |
+
import torch
|
| 60 |
+
from safetensors import safe_open
|
| 61 |
+
from safetensors.torch import save_file
|
| 62 |
+
|
| 63 |
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
| 64 |
+
|
| 65 |
+
REPO = os.path.dirname(os.path.abspath(__file__))
|
| 66 |
+
MODEL_PATH = os.path.join(REPO, "variants", "neural_matrix8.safetensors")
|
| 67 |
+
GATE_MODEL_PATH = os.path.join(REPO, "variants", "neural_computer8_registers.safetensors")
|
| 68 |
+
|
| 69 |
+
ADDR_BITS = 4
|
| 70 |
+
MEM_BYTES = 16
|
| 71 |
+
STATE_BITS = 4 + 32 + 4 + 4 + 1 + 128 # pc, regs, flags, sp, halt, mem
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
# =============================================================================
|
| 75 |
+
# Circuit IR: named threshold gates over binary signals, constants fold away
|
| 76 |
+
# =============================================================================
|
| 77 |
+
|
| 78 |
+
class Net:
|
| 79 |
+
"""A DAG of threshold gates. Signals are names; '#0' / '#1' are constants
|
| 80 |
+
folded into biases. Helper constructors perform algebraic collapse
|
| 81 |
+
(AND(x,#1)=x, OR with one live input = that input, ...) so the compiled
|
| 82 |
+
stack carries no dead logic; the collapses change nothing semantically and
|
| 83 |
+
the whole machine is verified bit-for-bit afterwards."""
|
| 84 |
+
|
| 85 |
+
def __init__(self):
|
| 86 |
+
self.gates: Dict[str, Tuple[List[Tuple[str, int]], int]] = {}
|
| 87 |
+
|
| 88 |
+
def g(self, name: str, ins: List[Tuple[str, int]], bias: int) -> str:
|
| 89 |
+
folded: List[Tuple[str, int]] = []
|
| 90 |
+
b = int(bias)
|
| 91 |
+
for s, w in ins:
|
| 92 |
+
assert w in (-1, 1), f"non-ternary weight {w} at {name}"
|
| 93 |
+
if s == "#1":
|
| 94 |
+
b += w
|
| 95 |
+
elif s == "#0":
|
| 96 |
+
pass
|
| 97 |
+
else:
|
| 98 |
+
folded.append((s, int(w)))
|
| 99 |
+
if name in self.gates:
|
| 100 |
+
raise ValueError(f"duplicate gate {name}")
|
| 101 |
+
self.gates[name] = (folded, b)
|
| 102 |
+
return name
|
| 103 |
+
|
| 104 |
+
# --- verified cell constructions --------------------------------------
|
| 105 |
+
def NOT(self, name, x):
|
| 106 |
+
if x == "#1":
|
| 107 |
+
return "#0"
|
| 108 |
+
if x == "#0":
|
| 109 |
+
return "#1"
|
| 110 |
+
return self.g(name, [(x, -1)], 0)
|
| 111 |
+
|
| 112 |
+
def BUF(self, name, x):
|
| 113 |
+
if x in ("#0", "#1"):
|
| 114 |
+
return x
|
| 115 |
+
return self.g(name, [(x, 1)], -1)
|
| 116 |
+
|
| 117 |
+
def AND(self, name, ins: List[str]):
|
| 118 |
+
live = []
|
| 119 |
+
for s in ins:
|
| 120 |
+
if s == "#0":
|
| 121 |
+
return "#0"
|
| 122 |
+
if s != "#1":
|
| 123 |
+
live.append(s)
|
| 124 |
+
if not live:
|
| 125 |
+
return "#1"
|
| 126 |
+
if len(live) == 1:
|
| 127 |
+
return live[0]
|
| 128 |
+
return self.g(name, [(s, 1) for s in live], -len(live))
|
| 129 |
+
|
| 130 |
+
def OR(self, name, ins: List[str]):
|
| 131 |
+
live = []
|
| 132 |
+
for s in ins:
|
| 133 |
+
if s == "#1":
|
| 134 |
+
return "#1"
|
| 135 |
+
if s != "#0":
|
| 136 |
+
live.append(s)
|
| 137 |
+
if not live:
|
| 138 |
+
return "#0"
|
| 139 |
+
if len(live) == 1:
|
| 140 |
+
return live[0]
|
| 141 |
+
return self.g(name, [(s, 1) for s in live], -1)
|
| 142 |
+
|
| 143 |
+
def NOR(self, name, ins: List[str]):
|
| 144 |
+
live = [s for s in ins if s != "#0"]
|
| 145 |
+
if any(s == "#1" for s in live):
|
| 146 |
+
return "#0"
|
| 147 |
+
if not live:
|
| 148 |
+
return "#1"
|
| 149 |
+
return self.g(name, [(s, -1) for s in live], 0)
|
| 150 |
+
|
| 151 |
+
def XOR(self, prefix, a, b):
|
| 152 |
+
"""Two-layer OR/NAND XOR, the family's verified construction."""
|
| 153 |
+
if a == "#0":
|
| 154 |
+
return b if b in ("#0", "#1") else self.BUF(f"{prefix}.buf", b)
|
| 155 |
+
if b == "#0":
|
| 156 |
+
return a if a in ("#0", "#1") else self.BUF(f"{prefix}.buf", a)
|
| 157 |
+
if a == "#1":
|
| 158 |
+
return self.NOT(f"{prefix}.not", b)
|
| 159 |
+
if b == "#1":
|
| 160 |
+
return self.NOT(f"{prefix}.not", a)
|
| 161 |
+
h_or = self.g(f"{prefix}.l1or", [(a, 1), (b, 1)], -1)
|
| 162 |
+
h_nand = self.g(f"{prefix}.l1nand", [(a, -1), (b, -1)], 1)
|
| 163 |
+
return self.g(prefix, [(h_or, 1), (h_nand, 1)], -2)
|
| 164 |
+
|
| 165 |
+
def FA(self, prefix, a, b, cin):
|
| 166 |
+
"""Verified full-adder cell (ha1 XOR/AND, ha2 XOR/AND, carry OR)."""
|
| 167 |
+
s1 = self.XOR(f"{prefix}.ha1s", a, b)
|
| 168 |
+
c1 = self.AND(f"{prefix}.ha1c", [a, b])
|
| 169 |
+
s2 = self.XOR(f"{prefix}.ha2s", s1, cin)
|
| 170 |
+
c2 = self.AND(f"{prefix}.ha2c", [s1, cin])
|
| 171 |
+
co = self.OR(f"{prefix}.cor", [c1, c2])
|
| 172 |
+
return s2, co
|
| 173 |
+
|
| 174 |
+
def MUX(self, prefix, sel, x1, x0):
|
| 175 |
+
"""2:1 mux cell: sel ? x1 : x0 (not_sel / and / and / or)."""
|
| 176 |
+
if sel == "#1":
|
| 177 |
+
return x1
|
| 178 |
+
if sel == "#0":
|
| 179 |
+
return x0
|
| 180 |
+
if x1 == x0:
|
| 181 |
+
return x0
|
| 182 |
+
ns = self.NOT(f"{prefix}.ns", sel)
|
| 183 |
+
a1 = self.AND(f"{prefix}.a1", [x1, sel])
|
| 184 |
+
a0 = self.AND(f"{prefix}.a0", [x0, ns])
|
| 185 |
+
return self.OR(f"{prefix}.or", [a1, a0])
|
| 186 |
+
|
| 187 |
+
def DECODE(self, name, bits: List[str], value: int):
|
| 188 |
+
"""One-gate address-decode row (the memory.addr_decode construction):
|
| 189 |
+
fires iff the MSB-first bits equal `value`."""
|
| 190 |
+
n = len(bits)
|
| 191 |
+
ins = []
|
| 192 |
+
pop = 0
|
| 193 |
+
for i in range(n):
|
| 194 |
+
bit = (value >> (n - 1 - i)) & 1
|
| 195 |
+
ins.append((bits[i], 1 if bit else -1))
|
| 196 |
+
pop += bit
|
| 197 |
+
return self.g(name, ins, -pop)
|
| 198 |
+
|
| 199 |
+
|
| 200 |
+
# =============================================================================
|
| 201 |
+
# State layout
|
| 202 |
+
# =============================================================================
|
| 203 |
+
|
| 204 |
+
def state_names() -> List[str]:
|
| 205 |
+
names = [f"pc{i}" for i in range(4)]
|
| 206 |
+
for r in range(4):
|
| 207 |
+
names += [f"r{r}b{i}" for i in range(8)]
|
| 208 |
+
names += ["fZ", "fN", "fC", "fV"]
|
| 209 |
+
names += [f"sp{i}" for i in range(4)]
|
| 210 |
+
names += ["halt"]
|
| 211 |
+
for j in range(MEM_BYTES):
|
| 212 |
+
names += [f"m{j}b{i}" for i in range(8)]
|
| 213 |
+
return names
|
| 214 |
+
|
| 215 |
+
|
| 216 |
+
def state_to_vec(s: dict) -> torch.Tensor:
|
| 217 |
+
v = torch.zeros(STATE_BITS)
|
| 218 |
+
i = 0
|
| 219 |
+
for k in range(4):
|
| 220 |
+
v[i] = (s["pc"] >> (3 - k)) & 1
|
| 221 |
+
i += 1
|
| 222 |
+
for r in range(4):
|
| 223 |
+
for k in range(8):
|
| 224 |
+
v[i] = (s["regs"][r] >> (7 - k)) & 1
|
| 225 |
+
i += 1
|
| 226 |
+
for k in range(4):
|
| 227 |
+
v[i] = s["flags"][k]
|
| 228 |
+
i += 1
|
| 229 |
+
for k in range(4):
|
| 230 |
+
v[i] = (s["sp"] >> (3 - k)) & 1
|
| 231 |
+
i += 1
|
| 232 |
+
v[i] = 1.0 if s["halted"] else 0.0
|
| 233 |
+
i += 1
|
| 234 |
+
for j in range(MEM_BYTES):
|
| 235 |
+
for k in range(8):
|
| 236 |
+
v[i] = (s["mem"][j] >> (7 - k)) & 1
|
| 237 |
+
i += 1
|
| 238 |
+
return v
|
| 239 |
+
|
| 240 |
+
|
| 241 |
+
def vec_to_state(v: torch.Tensor) -> dict:
|
| 242 |
+
bits = [int(round(float(x))) for x in v.tolist()]
|
| 243 |
+
i = 0
|
| 244 |
+
pc = 0
|
| 245 |
+
for k in range(4):
|
| 246 |
+
pc = (pc << 1) | bits[i]
|
| 247 |
+
i += 1
|
| 248 |
+
regs = []
|
| 249 |
+
for r in range(4):
|
| 250 |
+
x = 0
|
| 251 |
+
for k in range(8):
|
| 252 |
+
x = (x << 1) | bits[i]
|
| 253 |
+
i += 1
|
| 254 |
+
regs.append(x)
|
| 255 |
+
flags = bits[i:i + 4]
|
| 256 |
+
i += 4
|
| 257 |
+
sp = 0
|
| 258 |
+
for k in range(4):
|
| 259 |
+
sp = (sp << 1) | bits[i]
|
| 260 |
+
i += 1
|
| 261 |
+
halted = bool(bits[i])
|
| 262 |
+
i += 1
|
| 263 |
+
mem = []
|
| 264 |
+
for j in range(MEM_BYTES):
|
| 265 |
+
x = 0
|
| 266 |
+
for k in range(8):
|
| 267 |
+
x = (x << 1) | bits[i]
|
| 268 |
+
i += 1
|
| 269 |
+
mem.append(x)
|
| 270 |
+
return {"pc": pc, "regs": regs, "flags": flags, "sp": sp,
|
| 271 |
+
"halted": halted, "mem": mem}
|
| 272 |
+
|
| 273 |
+
|
| 274 |
+
# =============================================================================
|
| 275 |
+
# The step circuit: one full instruction of the registers-profile CPU
|
| 276 |
+
# =============================================================================
|
| 277 |
+
|
| 278 |
+
def build_step_net() -> Tuple[Net, List[str], List[str]]:
|
| 279 |
+
net = Net()
|
| 280 |
+
pc = [f"pc{i}" for i in range(4)]
|
| 281 |
+
R = [[f"r{r}b{i}" for i in range(8)] for r in range(4)]
|
| 282 |
+
fl = ["fZ", "fN", "fC", "fV"]
|
| 283 |
+
sp = [f"sp{i}" for i in range(4)]
|
| 284 |
+
halt = "halt"
|
| 285 |
+
mem = [[f"m{j}b{i}" for i in range(8)] for j in range(MEM_BYTES)]
|
| 286 |
+
|
| 287 |
+
# ---- fetch: bytes at PC .. PC+3 (decode lines reindexed, no adders) ----
|
| 288 |
+
P = [net.DECODE(f"pdec{j}", pc, j) for j in range(MEM_BYTES)]
|
| 289 |
+
byte = []
|
| 290 |
+
for o in range(4):
|
| 291 |
+
bo = []
|
| 292 |
+
for bit in range(8):
|
| 293 |
+
terms = [net.AND(f"f{o}a{j}b{bit}", [mem[j][bit], P[(j - o) % MEM_BYTES]])
|
| 294 |
+
for j in range(MEM_BYTES)]
|
| 295 |
+
bo.append(net.OR(f"f{o}b{bit}", terms))
|
| 296 |
+
byte.append(bo)
|
| 297 |
+
|
| 298 |
+
# ---- field decode; opcode lines gated by NOT halt (total transition) ---
|
| 299 |
+
nhalt = net.NOT("nhalt", halt)
|
| 300 |
+
O = [net.DECODE(f"op{v:X}", byte[0][0:4], v) for v in range(16)]
|
| 301 |
+
OG = [net.AND(f"og{v:X}", [O[v], nhalt]) for v in range(16)]
|
| 302 |
+
rdl = [net.DECODE(f"rdl{r}", byte[0][4:6], r) for r in range(4)]
|
| 303 |
+
rsl = [net.DECODE(f"rsl{r}", byte[0][6:8], r) for r in range(4)]
|
| 304 |
+
CL = [net.DECODE(f"cond{c}", byte[1][5:8], c) for c in range(8)]
|
| 305 |
+
|
| 306 |
+
# ---- register read ports ----------------------------------------------
|
| 307 |
+
a = [net.OR(f"a{bit}", [net.AND(f"a{bit}r{r}", [R[r][bit], rdl[r]])
|
| 308 |
+
for r in range(4)]) for bit in range(8)]
|
| 309 |
+
b = [net.OR(f"b{bit}", [net.AND(f"b{bit}r{r}", [R[r][bit], rsl[r]])
|
| 310 |
+
for r in range(4)]) for bit in range(8)]
|
| 311 |
+
a_l = lambda k: a[7 - k] # LSB-first views for the adder chains
|
| 312 |
+
b_l = lambda k: b[7 - k]
|
| 313 |
+
|
| 314 |
+
# ---- ALU: ADD ----------------------------------------------------------
|
| 315 |
+
c = "#0"
|
| 316 |
+
add_l = []
|
| 317 |
+
for k in range(8):
|
| 318 |
+
s_, c = net.FA(f"add.fa{k}", a_l(k), b_l(k), c)
|
| 319 |
+
add_l.append(s_)
|
| 320 |
+
add = [add_l[7 - i] for i in range(8)]
|
| 321 |
+
add_c = c
|
| 322 |
+
|
| 323 |
+
# ---- SUB (shared by CMP): a + ~b + 1 -----------------------------------
|
| 324 |
+
nb_l = [net.NOT(f"sub.nb{k}", b_l(k)) for k in range(8)]
|
| 325 |
+
c = "#1"
|
| 326 |
+
sub_l = []
|
| 327 |
+
for k in range(8):
|
| 328 |
+
s_, c = net.FA(f"sub.fa{k}", a_l(k), nb_l[k], c)
|
| 329 |
+
sub_l.append(s_)
|
| 330 |
+
sub = [sub_l[7 - i] for i in range(8)]
|
| 331 |
+
sub_c = c # carry set = no borrow
|
| 332 |
+
|
| 333 |
+
# ---- bitwise / shifts ---------------------------------------------------
|
| 334 |
+
andr = [net.AND(f"land{bit}", [a[bit], b[bit]]) for bit in range(8)]
|
| 335 |
+
orr = [net.OR(f"lor{bit}", [a[bit], b[bit]]) for bit in range(8)]
|
| 336 |
+
xorr = [net.XOR(f"lxor{bit}", a[bit], b[bit]) for bit in range(8)]
|
| 337 |
+
shl = [a[bit + 1] if bit < 7 else "#0" for bit in range(8)]
|
| 338 |
+
shr = [a[bit - 1] if bit > 0 else "#0" for bit in range(8)]
|
| 339 |
+
|
| 340 |
+
# ---- MUL: partial products + 7 chained shift-add adders ----------------
|
| 341 |
+
pp = [[net.AND(f"pp.a{i}b{j}", [a[i], b[j]]) for j in range(8)] for i in range(8)]
|
| 342 |
+
acc_l: List[str] = ["#0"] * 8
|
| 343 |
+
acc_l[7] = pp[7][0] # row j=0 shifted by 7
|
| 344 |
+
for j in range(1, 8):
|
| 345 |
+
shift = 7 - j
|
| 346 |
+
c = "#0"
|
| 347 |
+
nxt = []
|
| 348 |
+
for k in range(8):
|
| 349 |
+
addend = pp[14 - k - j][j] if shift <= k <= min(7, 14 - j) else "#0"
|
| 350 |
+
s_, c = net.FA(f"mul.s{j}.fa{k}", acc_l[k], addend, c)
|
| 351 |
+
nxt.append(s_)
|
| 352 |
+
acc_l = nxt
|
| 353 |
+
mul = [acc_l[7 - i] for i in range(8)]
|
| 354 |
+
|
| 355 |
+
# ---- DIV: 8 restoring stages (bit-cascade GE + subtract + mux) ---------
|
| 356 |
+
def cascade_lt(prefix: str, x_m: List[str], y_m: List[str]) -> str:
|
| 357 |
+
"""Unsigned x < y via the verified bit-cascade (bit 0 = MSB)."""
|
| 358 |
+
bit_lt, bit_eq = [], []
|
| 359 |
+
for i in range(8):
|
| 360 |
+
bit_lt.append(net.g(f"{prefix}.b{i}lt", [(x_m[i], -1), (y_m[i], 1)], -1)
|
| 361 |
+
if x_m[i] != "#0" else
|
| 362 |
+
(y_m[i] if y_m[i] != "#0" else "#0"))
|
| 363 |
+
if x_m[i] == "#0":
|
| 364 |
+
bit_eq.append(net.NOT(f"{prefix}.b{i}eq", y_m[i]))
|
| 365 |
+
else:
|
| 366 |
+
e_and = net.AND(f"{prefix}.b{i}ea", [x_m[i], y_m[i]])
|
| 367 |
+
e_nor = net.NOR(f"{prefix}.b{i}en", [x_m[i], y_m[i]])
|
| 368 |
+
bit_eq.append(net.OR(f"{prefix}.b{i}eq", [e_and, e_nor]))
|
| 369 |
+
casc = [bit_lt[0]]
|
| 370 |
+
for i in range(1, 8):
|
| 371 |
+
pref = net.AND(f"{prefix}.ep{i}", bit_eq[:i])
|
| 372 |
+
casc.append(net.AND(f"{prefix}.cl{i}", [pref, bit_lt[i]]))
|
| 373 |
+
return net.OR(f"{prefix}.lt", casc)
|
| 374 |
+
|
| 375 |
+
rem_l: List[str] = ["#0"] * 8
|
| 376 |
+
q_m: List[str] = []
|
| 377 |
+
for st in range(8):
|
| 378 |
+
sh_l = [a[st]] + rem_l[0:7] # (rem << 1) | a_bit; top bit provably 0
|
| 379 |
+
sh_m = [sh_l[7 - i] for i in range(8)]
|
| 380 |
+
lt = cascade_lt(f"div.s{st}.cmp", sh_m, b)
|
| 381 |
+
ge = net.NOT(f"div.s{st}.ge", lt)
|
| 382 |
+
c = "#1"
|
| 383 |
+
dsub_l = []
|
| 384 |
+
for k in range(8):
|
| 385 |
+
s_, c = net.FA(f"div.s{st}.sub.fa{k}", sh_l[k], nb_l[k], c)
|
| 386 |
+
dsub_l.append(s_)
|
| 387 |
+
rem_l = [net.MUX(f"div.s{st}.mx{k}", ge, dsub_l[k], sh_l[k]) for k in range(8)]
|
| 388 |
+
q_m.append(ge)
|
| 389 |
+
div = q_m # quotient, MSB-first
|
| 390 |
+
|
| 391 |
+
# ---- LOAD read port (address = low nibble of byte3) --------------------
|
| 392 |
+
addr4 = byte[3][4:8]
|
| 393 |
+
AD = [net.DECODE(f"adec{j}", addr4, j) for j in range(MEM_BYTES)]
|
| 394 |
+
ld = [net.OR(f"ld{bit}", [net.AND(f"ld{bit}a{j}", [mem[j][bit], AD[j]])
|
| 395 |
+
for j in range(MEM_BYTES)]) for bit in range(8)]
|
| 396 |
+
|
| 397 |
+
# ---- result mux over opcode lines ---------------------------------------
|
| 398 |
+
srcs = [(0, add), (1, sub), (2, andr), (3, orr), (4, xorr),
|
| 399 |
+
(5, shl), (6, shr), (7, mul), (8, div), (0xA, ld)]
|
| 400 |
+
res = [net.OR(f"res{bit}", [net.AND(f"res{bit}o{v:X}", [src[bit], OG[v]])
|
| 401 |
+
for v, src in srcs]) for bit in range(8)]
|
| 402 |
+
|
| 403 |
+
# ---- flags ---------------------------------------------------------------
|
| 404 |
+
z_add = net.NOR("zadd", add)
|
| 405 |
+
z_sub = net.NOR("zsub", sub)
|
| 406 |
+
z_mul = net.NOR("zmul", mul)
|
| 407 |
+
v_add = net.AND("vadd", [net.XOR("vadd.x1", a[0], add[0]),
|
| 408 |
+
net.XOR("vadd.x2", b[0], add[0])])
|
| 409 |
+
v_sub = net.AND("vsub", [net.XOR("vsub.x1", a[0], b[0]),
|
| 410 |
+
net.XOR("vsub.x2", a[0], sub[0])])
|
| 411 |
+
f_src = { # per-source (Z, N, C, V); CMP shares the SUB datapath
|
| 412 |
+
0: (z_add, add[0], add_c, v_add),
|
| 413 |
+
1: (z_sub, sub[0], sub_c, v_sub),
|
| 414 |
+
7: (z_mul, mul[0], "#0", "#0"),
|
| 415 |
+
9: (z_sub, sub[0], sub_c, v_sub),
|
| 416 |
+
}
|
| 417 |
+
fw = net.OR("fw", [OG[0], OG[1], OG[7], OG[9]])
|
| 418 |
+
fl_next = []
|
| 419 |
+
for fi, fn in enumerate("ZNCV"):
|
| 420 |
+
new = net.OR(f"fnew{fn}", [net.AND(f"fnew{fn}o{v:X}", [f_src[v][fi], OG[v]])
|
| 421 |
+
for v in (0, 1, 7, 9)])
|
| 422 |
+
fl_next.append(net.MUX(f"fmux{fn}", fw, new, fl[fi]))
|
| 423 |
+
|
| 424 |
+
# ---- register writeback --------------------------------------------------
|
| 425 |
+
wen = net.OR("wen", [OG[v] for v in (0, 1, 2, 3, 4, 5, 6, 7, 8, 0xA)])
|
| 426 |
+
R_next = []
|
| 427 |
+
for r in range(4):
|
| 428 |
+
wsel = net.AND(f"wsel{r}", [wen, rdl[r]])
|
| 429 |
+
nws = net.NOT(f"nws{r}", wsel)
|
| 430 |
+
row = []
|
| 431 |
+
for bit in range(8):
|
| 432 |
+
t1 = net.AND(f"wb{r}b{bit}n", [res[bit], wsel])
|
| 433 |
+
t0 = net.AND(f"wb{r}b{bit}o", [R[r][bit], nws])
|
| 434 |
+
row.append(net.OR(f"wb{r}b{bit}", [t1, t0]))
|
| 435 |
+
R_next.append(row)
|
| 436 |
+
|
| 437 |
+
# ---- program counter ------------------------------------------------------
|
| 438 |
+
pc_l = [pc[3 - k] for k in range(4)] # LSB-first view
|
| 439 |
+
p2_l = [pc_l[0], net.NOT("pc2.n1", pc_l[1]),
|
| 440 |
+
net.XOR("pc2.x2", pc_l[2], pc_l[1]),
|
| 441 |
+
net.XOR("pc2.x3", pc_l[3], net.AND("pc2.c3", [pc_l[2], pc_l[1]]))]
|
| 442 |
+
p4_l = [pc_l[0], pc_l[1], net.NOT("pc4.n2", pc_l[2]),
|
| 443 |
+
net.XOR("pc4.x3", pc_l[3], pc_l[2])]
|
| 444 |
+
ext = net.OR("ext", [OG[v] for v in (0xA, 0xB, 0xC, 0xD, 0xE)])
|
| 445 |
+
adv_l = [net.MUX(f"padv{k}", ext, p4_l[k], p2_l[k]) for k in range(4)]
|
| 446 |
+
pc_adv = [adv_l[3 - i] for i in range(4)]
|
| 447 |
+
|
| 448 |
+
nfl = [net.NOT(f"nf{fn}", fl[i]) for i, fn in enumerate("ZNCV")]
|
| 449 |
+
cond_flag = [fl[0], nfl[0], fl[2], nfl[2], fl[1], nfl[1], fl[3], nfl[3]]
|
| 450 |
+
condsel = net.OR("condsel", [net.AND(f"ct{cnd}", [CL[cnd], cond_flag[cnd]])
|
| 451 |
+
for cnd in range(8)])
|
| 452 |
+
take = net.OR("take", [OG[0xC], OG[0xE], net.AND("jcct", [OG[0xD], condsel])])
|
| 453 |
+
keeppc = net.OR("keeppc", [halt, OG[0xF]])
|
| 454 |
+
adv = net.AND("adv", [net.NOT("ntake", take), net.NOT("nkeep", keeppc)])
|
| 455 |
+
pc_next = [net.OR(f"pcn{i}", [net.AND(f"pcn{i}t", [addr4[i], take]),
|
| 456 |
+
net.AND(f"pcn{i}k", [pc[i], keeppc]),
|
| 457 |
+
net.AND(f"pcn{i}a", [pc_adv[i], adv])])
|
| 458 |
+
for i in range(4)]
|
| 459 |
+
|
| 460 |
+
# ---- stack pointer (CALL: SP-2 = SP + 14) ---------------------------------
|
| 461 |
+
sp_l = [sp[3 - k] for k in range(4)]
|
| 462 |
+
c = "#0"
|
| 463 |
+
s2_l = []
|
| 464 |
+
for k, addend in enumerate(["#0", "#1", "#1", "#1"]):
|
| 465 |
+
s_, c = net.FA(f"spm2.fa{k}", sp_l[k], addend, c)
|
| 466 |
+
s2_l.append(s_)
|
| 467 |
+
spm2 = [s2_l[3 - i] for i in range(4)]
|
| 468 |
+
sp_next = [net.MUX(f"spn{i}", OG[0xE], spm2[i], sp[i]) for i in range(4)]
|
| 469 |
+
|
| 470 |
+
# ---- halt (sticky) ---------------------------------------------------------
|
| 471 |
+
halt_next = net.OR("haltn", [halt, OG[0xF]])
|
| 472 |
+
|
| 473 |
+
# ---- memory next: STORE write, CALL pushes ret-hi (0) and ret-lo ----------
|
| 474 |
+
SPD = [net.DECODE(f"spdec{j}", sp, j) for j in range(MEM_BYTES)]
|
| 475 |
+
mem_next = []
|
| 476 |
+
for j in range(MEM_BYTES):
|
| 477 |
+
st_sel = net.AND(f"stsel{j}", [OG[0xB], AD[j]])
|
| 478 |
+
ch_sel = net.AND(f"chsel{j}", [OG[0xE], SPD[(j + 1) % MEM_BYTES]])
|
| 479 |
+
cl_sel = net.AND(f"clsel{j}", [OG[0xE], SPD[(j + 2) % MEM_BYTES]])
|
| 480 |
+
keep = net.NOR(f"keep{j}", [st_sel, ch_sel, cl_sel])
|
| 481 |
+
row = []
|
| 482 |
+
for bit in range(8):
|
| 483 |
+
terms = [net.AND(f"mn{j}b{bit}k", [mem[j][bit], keep]),
|
| 484 |
+
net.AND(f"mn{j}b{bit}s", [b[bit], st_sel])]
|
| 485 |
+
if bit >= 4: # ret-lo low nibble carries pc_adv; high nibble is 0
|
| 486 |
+
terms.append(net.AND(f"mn{j}b{bit}c", [pc_adv[bit - 4], cl_sel]))
|
| 487 |
+
row.append(net.OR(f"mn{j}b{bit}", terms))
|
| 488 |
+
mem_next.append(row)
|
| 489 |
+
|
| 490 |
+
outputs = (pc_next
|
| 491 |
+
+ [bitsig for row in R_next for bitsig in row]
|
| 492 |
+
+ fl_next + sp_next + [halt_next]
|
| 493 |
+
+ [bitsig for row in mem_next for bitsig in row])
|
| 494 |
+
return net, state_names(), outputs
|
| 495 |
+
|
| 496 |
+
|
| 497 |
+
# =============================================================================
|
| 498 |
+
# Compiler: levelize ASAP, insert pass-throughs, emit dense ternary matrices
|
| 499 |
+
# =============================================================================
|
| 500 |
+
|
| 501 |
+
def compile_net(net: Net, inputs: List[str], outputs: List[str],
|
| 502 |
+
) -> Tuple[List[Tuple[torch.Tensor, torch.Tensor]], dict]:
|
| 503 |
+
gates = net.gates
|
| 504 |
+
level: Dict[str, int] = {s: 0 for s in inputs}
|
| 505 |
+
|
| 506 |
+
consumers: Dict[str, List[str]] = {}
|
| 507 |
+
indeg: Dict[str, int] = {}
|
| 508 |
+
for gname, (ins, _) in gates.items():
|
| 509 |
+
deps = [s for s, _ in ins if s in gates]
|
| 510 |
+
indeg[gname] = len(deps)
|
| 511 |
+
for s, _ in ins:
|
| 512 |
+
consumers.setdefault(s, []).append(gname)
|
| 513 |
+
order = [g for g, d in indeg.items() if d == 0]
|
| 514 |
+
i = 0
|
| 515 |
+
while i < len(order):
|
| 516 |
+
for cns in consumers.get(order[i], []):
|
| 517 |
+
indeg[cns] -= 1
|
| 518 |
+
if indeg[cns] == 0:
|
| 519 |
+
order.append(cns)
|
| 520 |
+
i += 1
|
| 521 |
+
if len(order) != len(gates):
|
| 522 |
+
raise ValueError("cycle in step circuit")
|
| 523 |
+
for gname in order:
|
| 524 |
+
ins, _ = gates[gname]
|
| 525 |
+
level[gname] = 1 + max((level[s] for s, _ in ins), default=0)
|
| 526 |
+
|
| 527 |
+
kmax = max(level[gname] for gname in gates)
|
| 528 |
+
K = kmax + 1 # final layer selects the next-state bits in order
|
| 529 |
+
|
| 530 |
+
last_use: Dict[str, int] = {}
|
| 531 |
+
for gname, (ins, _) in gates.items():
|
| 532 |
+
for s, _ in ins:
|
| 533 |
+
last_use[s] = max(last_use.get(s, 0), level[gname])
|
| 534 |
+
for s in outputs:
|
| 535 |
+
if s not in ("#0", "#1"):
|
| 536 |
+
last_use[s] = K
|
| 537 |
+
|
| 538 |
+
by_level: Dict[int, List[str]] = {}
|
| 539 |
+
for gname in gates:
|
| 540 |
+
by_level.setdefault(level[gname], []).append(gname)
|
| 541 |
+
|
| 542 |
+
vec = list(inputs) # V_0
|
| 543 |
+
layers: List[Tuple[torch.Tensor, torch.Tensor]] = []
|
| 544 |
+
for lv in range(1, K + 1):
|
| 545 |
+
if lv < K:
|
| 546 |
+
new_gates = sorted(by_level.get(lv, []))
|
| 547 |
+
passes = [s for s in vec if last_use.get(s, 0) > lv]
|
| 548 |
+
rows = new_gates + passes
|
| 549 |
+
else:
|
| 550 |
+
rows = list(outputs)
|
| 551 |
+
cols = {s: idx for idx, s in enumerate(vec)}
|
| 552 |
+
W = torch.zeros(len(rows), len(vec), dtype=torch.int8)
|
| 553 |
+
B = torch.zeros(len(rows), dtype=torch.int8)
|
| 554 |
+
out_names = []
|
| 555 |
+
for ri, s in enumerate(rows):
|
| 556 |
+
if lv == K and s == "#0":
|
| 557 |
+
B[ri] = -1
|
| 558 |
+
out_names.append(s)
|
| 559 |
+
continue
|
| 560 |
+
if lv == K and s == "#1":
|
| 561 |
+
B[ri] = 0
|
| 562 |
+
out_names.append(s)
|
| 563 |
+
continue
|
| 564 |
+
if s in gates and level[s] == lv:
|
| 565 |
+
ins, bias = gates[s]
|
| 566 |
+
for src, w in ins:
|
| 567 |
+
W[ri, cols[src]] += w
|
| 568 |
+
B[ri] = bias
|
| 569 |
+
else:
|
| 570 |
+
W[ri, cols[s]] = 1
|
| 571 |
+
B[ri] = -1
|
| 572 |
+
out_names.append(s)
|
| 573 |
+
layers.append((W, B))
|
| 574 |
+
vec = out_names
|
| 575 |
+
|
| 576 |
+
if vec != outputs:
|
| 577 |
+
raise AssertionError("final layer does not emit the state vector")
|
| 578 |
+
|
| 579 |
+
info = {
|
| 580 |
+
"layers": K,
|
| 581 |
+
"gates": len(gates),
|
| 582 |
+
"max_width": max(max(W.shape) for W, _ in layers),
|
| 583 |
+
"total_weights": sum(W.numel() for W, _ in layers),
|
| 584 |
+
"widths": [layers[0][0].shape[1]] + [W.shape[0] for W, _ in layers],
|
| 585 |
+
}
|
| 586 |
+
return layers, info
|
| 587 |
+
|
| 588 |
+
|
| 589 |
+
# =============================================================================
|
| 590 |
+
# Recurrent runtime
|
| 591 |
+
# =============================================================================
|
| 592 |
+
|
| 593 |
+
class MatrixMachine:
|
| 594 |
+
"""s' = H(Wk @ ... H(W1 @ s + b1) ... + bk); iterate until the halt bit.
|
| 595 |
+
|
| 596 |
+
Digital mode thresholds at 0 (pre-activations are integers). Analog mode
|
| 597 |
+
thresholds at -0.5 (the max-margin comparator point) and can inject
|
| 598 |
+
Gaussian read noise per MVM and static ternary-conductance mismatch."""
|
| 599 |
+
|
| 600 |
+
HALT_IDX = 4 + 32 + 4 + 4 # index of the halt bit in the state vector
|
| 601 |
+
|
| 602 |
+
def __init__(self, layers, device="cpu"):
|
| 603 |
+
self.device = device
|
| 604 |
+
self.W = [W.to(device=device, dtype=torch.float32) for W, _ in layers]
|
| 605 |
+
self.B = [B.to(device=device, dtype=torch.float32) for _, B in layers]
|
| 606 |
+
|
| 607 |
+
@classmethod
|
| 608 |
+
def from_file(cls, path=MODEL_PATH, device="cpu"):
|
| 609 |
+
layers = []
|
| 610 |
+
with safe_open(path, framework="pt") as f:
|
| 611 |
+
n = 0
|
| 612 |
+
while f"matrix.layer{n:03d}.weight" in f.keys():
|
| 613 |
+
layers.append((f.get_tensor(f"matrix.layer{n:03d}.weight"),
|
| 614 |
+
f.get_tensor(f"matrix.layer{n:03d}.bias")))
|
| 615 |
+
n += 1
|
| 616 |
+
if not layers:
|
| 617 |
+
raise FileNotFoundError(f"no matrix layers in {path}")
|
| 618 |
+
return cls(layers, device=device)
|
| 619 |
+
|
| 620 |
+
def perturbed(self, sigma_g: float, seed: int = 0) -> "MatrixMachine":
|
| 621 |
+
"""Static conductance mismatch: each nonzero ternary conductance gets a
|
| 622 |
+
fixed Gaussian perturbation (fabrication variation, constant per run)."""
|
| 623 |
+
gen = torch.Generator(device="cpu").manual_seed(seed)
|
| 624 |
+
m = MatrixMachine.__new__(MatrixMachine)
|
| 625 |
+
m.device = self.device
|
| 626 |
+
m.W = []
|
| 627 |
+
m.B = self.B
|
| 628 |
+
for W in self.W:
|
| 629 |
+
d = torch.randn(W.shape, generator=gen).to(self.device) * sigma_g
|
| 630 |
+
m.W.append(W + d * (W != 0))
|
| 631 |
+
return m
|
| 632 |
+
|
| 633 |
+
def step(self, v: torch.Tensor, analog: bool = False,
|
| 634 |
+
noise_sigma: float = 0.0, gen: Optional[torch.Generator] = None,
|
| 635 |
+
) -> torch.Tensor:
|
| 636 |
+
thresh = -0.5 if analog else 0.0
|
| 637 |
+
for W, b in zip(self.W, self.B):
|
| 638 |
+
pre = v @ W.T + b
|
| 639 |
+
if noise_sigma > 0.0:
|
| 640 |
+
pre = pre + torch.randn(pre.shape, generator=gen,
|
| 641 |
+
device=pre.device) * noise_sigma
|
| 642 |
+
v = (pre >= thresh).float()
|
| 643 |
+
return v
|
| 644 |
+
|
| 645 |
+
def run(self, state: dict, max_cycles: int = 64, **kw) -> Tuple[dict, int]:
|
| 646 |
+
v = state_to_vec(state).unsqueeze(0).to(self.device)
|
| 647 |
+
n = 0
|
| 648 |
+
while n < max_cycles and v[0, self.HALT_IDX] < 0.5:
|
| 649 |
+
v = self.step(v, **kw)
|
| 650 |
+
n += 1
|
| 651 |
+
return vec_to_state(v[0].cpu()), n
|
| 652 |
+
|
| 653 |
+
def min_margin(self, v: torch.Tensor) -> float:
|
| 654 |
+
"""Distance of every pre-activation from the analog threshold -0.5."""
|
| 655 |
+
m = float("inf")
|
| 656 |
+
for W, b in zip(self.W, self.B):
|
| 657 |
+
pre = v @ W.T + b
|
| 658 |
+
m = min(m, float((pre + 0.5).abs().min()))
|
| 659 |
+
v = (pre >= 0).float()
|
| 660 |
+
return m
|
| 661 |
+
|
| 662 |
+
|
| 663 |
+
# =============================================================================
|
| 664 |
+
# Pure-integer reference (mirrors GenericThresholdCPU semantics, 4-bit addr)
|
| 665 |
+
# =============================================================================
|
| 666 |
+
|
| 667 |
+
_JCC_FLAG = [0, 0, 2, 2, 1, 1, 3, 3]
|
| 668 |
+
|
| 669 |
+
|
| 670 |
+
def ref_step(s: dict) -> dict:
|
| 671 |
+
if s["halted"]:
|
| 672 |
+
return {k: (list(v) if isinstance(v, list) else v) for k, v in s.items()}
|
| 673 |
+
s = {"pc": s["pc"], "regs": list(s["regs"]), "flags": list(s["flags"]),
|
| 674 |
+
"sp": s["sp"], "halted": s["halted"], "mem": list(s["mem"])}
|
| 675 |
+
M = MEM_BYTES - 1
|
| 676 |
+
pc = s["pc"]
|
| 677 |
+
ir = (s["mem"][pc] << 8) | s["mem"][(pc + 1) & M]
|
| 678 |
+
op, rd, rs = (ir >> 12) & 0xF, (ir >> 10) & 3, (ir >> 8) & 3
|
| 679 |
+
imm = ir & 0xFF
|
| 680 |
+
next_pc = (pc + 2) & M
|
| 681 |
+
addr = None
|
| 682 |
+
if op in (0xA, 0xB, 0xC, 0xD, 0xE):
|
| 683 |
+
al = s["mem"][(next_pc + 1) & M]
|
| 684 |
+
addr = al & M
|
| 685 |
+
next_pc = (next_pc + 2) & M
|
| 686 |
+
a, b = s["regs"][rd], s["regs"][rs]
|
| 687 |
+
result, carry, ovf, wr = a, 0, 0, True
|
| 688 |
+
if op == 0x0:
|
| 689 |
+
result = (a + b) & 0xFF
|
| 690 |
+
carry = 1 if a + b > 0xFF else 0
|
| 691 |
+
ovf = 1 if ((a ^ result) & (b ^ result)) & 0x80 else 0
|
| 692 |
+
elif op == 0x1:
|
| 693 |
+
result = (a - b) & 0xFF
|
| 694 |
+
carry = 1 if a >= b else 0
|
| 695 |
+
ovf = 1 if ((a ^ b) & (a ^ result)) & 0x80 else 0
|
| 696 |
+
elif op == 0x2:
|
| 697 |
+
result = a & b
|
| 698 |
+
elif op == 0x3:
|
| 699 |
+
result = a | b
|
| 700 |
+
elif op == 0x4:
|
| 701 |
+
result = a ^ b
|
| 702 |
+
elif op == 0x5:
|
| 703 |
+
result = (a << 1) & 0xFF
|
| 704 |
+
elif op == 0x6:
|
| 705 |
+
result = a >> 1
|
| 706 |
+
elif op == 0x7:
|
| 707 |
+
result = (a * b) & 0xFF
|
| 708 |
+
elif op == 0x8:
|
| 709 |
+
result = 0xFF if b == 0 else a // b
|
| 710 |
+
elif op == 0x9:
|
| 711 |
+
r2 = (a - b) & 0xFF
|
| 712 |
+
carry = 1 if a >= b else 0
|
| 713 |
+
ovf = 1 if ((a ^ b) & (a ^ r2)) & 0x80 else 0
|
| 714 |
+
s["flags"] = [1 if r2 == 0 else 0, 1 if r2 & 0x80 else 0, carry, ovf]
|
| 715 |
+
wr = False
|
| 716 |
+
elif op == 0xA:
|
| 717 |
+
result = s["mem"][addr]
|
| 718 |
+
elif op == 0xB:
|
| 719 |
+
s["mem"][addr] = b & 0xFF
|
| 720 |
+
wr = False
|
| 721 |
+
elif op == 0xC:
|
| 722 |
+
s["pc"] = addr
|
| 723 |
+
return s
|
| 724 |
+
elif op == 0xD:
|
| 725 |
+
cnd = imm & 7
|
| 726 |
+
flag = s["flags"][_JCC_FLAG[cnd]]
|
| 727 |
+
sel = flag if cnd % 2 == 0 else 1 - flag
|
| 728 |
+
s["pc"] = addr if sel else next_pc
|
| 729 |
+
return s
|
| 730 |
+
elif op == 0xE:
|
| 731 |
+
ret = next_pc
|
| 732 |
+
sp2 = (s["sp"] - 1) & M
|
| 733 |
+
s["mem"][sp2] = (ret >> 8) & 0xFF
|
| 734 |
+
sp2 = (sp2 - 1) & M
|
| 735 |
+
s["mem"][sp2] = ret & 0xFF
|
| 736 |
+
s["sp"] = sp2
|
| 737 |
+
s["pc"] = addr
|
| 738 |
+
return s
|
| 739 |
+
elif op == 0xF:
|
| 740 |
+
s["halted"] = True
|
| 741 |
+
return s
|
| 742 |
+
if wr and op != 0x9:
|
| 743 |
+
s["regs"][rd] = result & 0xFF
|
| 744 |
+
if op in (0x0, 0x1, 0x7):
|
| 745 |
+
s["flags"] = [1 if result == 0 else 0, 1 if result & 0x80 else 0,
|
| 746 |
+
carry, ovf]
|
| 747 |
+
s["pc"] = next_pc
|
| 748 |
+
return s
|
| 749 |
+
|
| 750 |
+
|
| 751 |
+
# =============================================================================
|
| 752 |
+
# Build + save
|
| 753 |
+
# =============================================================================
|
| 754 |
+
|
| 755 |
+
def build(save: bool = True):
|
| 756 |
+
print("Assembling the step circuit from the verified cell library...")
|
| 757 |
+
t0 = time.perf_counter()
|
| 758 |
+
net, inputs, outputs = build_step_net()
|
| 759 |
+
print(f" {len(net.gates)} threshold gates")
|
| 760 |
+
layers, info = compile_net(net, inputs, outputs)
|
| 761 |
+
dt = time.perf_counter() - t0
|
| 762 |
+
print(f" compiled to {info['layers']} ternary matrices in {dt:.1f}s; "
|
| 763 |
+
f"max width {info['max_width']}, "
|
| 764 |
+
f"{info['total_weights']:,} dense weight entries")
|
| 765 |
+
for W, B in layers:
|
| 766 |
+
vals = set(torch.unique(W).tolist())
|
| 767 |
+
assert vals <= {-1, 0, 1}, f"non-ternary matrix: {vals}"
|
| 768 |
+
assert int(B.min()) >= -128 and int(B.max()) <= 127
|
| 769 |
+
|
| 770 |
+
if save:
|
| 771 |
+
tensors = {}
|
| 772 |
+
for i, (W, B) in enumerate(layers):
|
| 773 |
+
tensors[f"matrix.layer{i:03d}.weight"] = W
|
| 774 |
+
tensors[f"matrix.layer{i:03d}.bias"] = B
|
| 775 |
+
tensors["manifest.data_bits"] = torch.tensor([8.0])
|
| 776 |
+
tensors["manifest.addr_bits"] = torch.tensor([float(ADDR_BITS)])
|
| 777 |
+
tensors["manifest.memory_bytes"] = torch.tensor([float(MEM_BYTES)])
|
| 778 |
+
tensors["manifest.registers"] = torch.tensor([4.0])
|
| 779 |
+
tensors["manifest.layers"] = torch.tensor([float(info["layers"])])
|
| 780 |
+
tensors["manifest.state_bits"] = torch.tensor([float(STATE_BITS)])
|
| 781 |
+
tensors["manifest.version"] = torch.tensor([4.0])
|
| 782 |
+
meta = {
|
| 783 |
+
"machine": "matrix8",
|
| 784 |
+
"weight_quantization": "ternary",
|
| 785 |
+
"state_layout": json.dumps(state_names()),
|
| 786 |
+
"analog": json.dumps({
|
| 787 |
+
"comparator_threshold": -0.5,
|
| 788 |
+
"guaranteed_margin": 0.5,
|
| 789 |
+
"note": "pre-activations are integers; any total analog error "
|
| 790 |
+
"below 0.5 per pre-activation is provably bit-exact",
|
| 791 |
+
}),
|
| 792 |
+
}
|
| 793 |
+
save_file(tensors, MODEL_PATH, metadata=meta)
|
| 794 |
+
sz = os.path.getsize(MODEL_PATH) / (1024 * 1024)
|
| 795 |
+
print(f" saved {MODEL_PATH} ({sz:.1f} MB)")
|
| 796 |
+
return layers, info
|
| 797 |
+
|
| 798 |
+
|
| 799 |
+
# =============================================================================
|
| 800 |
+
# Verification: matrix form == gate graph == integer reference, bit for bit
|
| 801 |
+
# =============================================================================
|
| 802 |
+
|
| 803 |
+
def _mk_state(mem=None, regs=(0, 0, 0, 0), flags=(0, 0, 0, 0), pc=0,
|
| 804 |
+
sp=MEM_BYTES - 1, halted=False):
|
| 805 |
+
m = [0] * MEM_BYTES
|
| 806 |
+
if mem:
|
| 807 |
+
for i, x in enumerate(mem):
|
| 808 |
+
m[i] = x & 0xFF
|
| 809 |
+
return {"pc": pc, "regs": list(regs), "flags": list(flags), "sp": sp,
|
| 810 |
+
"halted": halted, "mem": m}
|
| 811 |
+
|
| 812 |
+
|
| 813 |
+
def _instr(op, rd=0, rs=0, imm=0):
|
| 814 |
+
w = ((op & 0xF) << 12) | ((rd & 3) << 10) | ((rs & 3) << 8) | (imm & 0xFF)
|
| 815 |
+
return [(w >> 8) & 0xFF, w & 0xFF]
|
| 816 |
+
|
| 817 |
+
|
| 818 |
+
def verify(mm: MatrixMachine, device: str) -> bool:
|
| 819 |
+
ok_all = True
|
| 820 |
+
|
| 821 |
+
# ---- 1. exhaustive ALU sweeps: all 65,536 operand pairs per opcode ------
|
| 822 |
+
print("\n[1/4] Exhaustive ALU sweeps (65,536 operand pairs x 10 opcodes, "
|
| 823 |
+
"batched through the matrices, vs integer reference)")
|
| 824 |
+
aa = torch.arange(65536) % 256
|
| 825 |
+
bb = torch.arange(65536) // 256
|
| 826 |
+
for op, name in [(0, "ADD"), (1, "SUB"), (2, "AND"), (3, "OR"), (4, "XOR"),
|
| 827 |
+
(5, "SHL"), (6, "SHR"), (7, "MUL"), (8, "DIV"), (9, "CMP")]:
|
| 828 |
+
for f_init in (0, 0xF):
|
| 829 |
+
V = torch.zeros(65536, STATE_BITS)
|
| 830 |
+
prog = _instr(op, rd=0, rs=1) + _instr(0xF)
|
| 831 |
+
base = _mk_state(mem=prog, flags=[(f_init >> (3 - i)) & 1 for i in range(4)])
|
| 832 |
+
V[:] = state_to_vec(base)
|
| 833 |
+
for k in range(8):
|
| 834 |
+
V[:, 4 + k] = ((aa >> (7 - k)) & 1).float() # R0
|
| 835 |
+
V[:, 12 + k] = ((bb >> (7 - k)) & 1).float() # R1
|
| 836 |
+
V = mm.step(V.to(device)).cpu()
|
| 837 |
+
bad = 0
|
| 838 |
+
probe = list(range(0, 65536, 4099)) + [0, 65535, 255, 256, 32768]
|
| 839 |
+
for idx in probe:
|
| 840 |
+
st = _mk_state(mem=prog, regs=(int(aa[idx]), int(bb[idx]), 0, 0),
|
| 841 |
+
flags=base["flags"])
|
| 842 |
+
if vec_to_state(V[idx]) != ref_step(st):
|
| 843 |
+
bad += 1
|
| 844 |
+
# full vectorized compare of the architectural result field
|
| 845 |
+
r0 = sum(V[:, 4 + k].long() << (7 - k) for k in range(8))
|
| 846 |
+
fZ, fN, fC, fV = (V[:, 36].long(), V[:, 37].long(),
|
| 847 |
+
V[:, 38].long(), V[:, 39].long())
|
| 848 |
+
if op == 0:
|
| 849 |
+
exp = (aa + bb) & 0xFF
|
| 850 |
+
elif op in (1, 9):
|
| 851 |
+
exp = (aa - bb) & 0xFF
|
| 852 |
+
elif op == 2:
|
| 853 |
+
exp = aa & bb
|
| 854 |
+
elif op == 3:
|
| 855 |
+
exp = aa | bb
|
| 856 |
+
elif op == 4:
|
| 857 |
+
exp = aa ^ bb
|
| 858 |
+
elif op == 5:
|
| 859 |
+
exp = (aa << 1) & 0xFF
|
| 860 |
+
elif op == 6:
|
| 861 |
+
exp = aa >> 1
|
| 862 |
+
elif op == 7:
|
| 863 |
+
exp = (aa * bb) & 0xFF
|
| 864 |
+
else:
|
| 865 |
+
exp = torch.where(bb == 0, torch.full_like(aa, 0xFF),
|
| 866 |
+
aa // torch.clamp(bb, min=1))
|
| 867 |
+
reg_exp = aa if op == 9 else exp
|
| 868 |
+
n_bad = int((r0 != reg_exp).sum())
|
| 869 |
+
if op in (0, 1, 7, 9):
|
| 870 |
+
zx = (exp == 0).long()
|
| 871 |
+
nx = ((exp & 0x80) > 0).long()
|
| 872 |
+
if op == 0:
|
| 873 |
+
cx = ((aa + bb) > 0xFF).long()
|
| 874 |
+
vx = ((((aa ^ exp) & (bb ^ exp)) & 0x80) > 0).long()
|
| 875 |
+
elif op in (1, 9):
|
| 876 |
+
cx = (aa >= bb).long()
|
| 877 |
+
vx = ((((aa ^ bb) & (aa ^ exp)) & 0x80) > 0).long()
|
| 878 |
+
else:
|
| 879 |
+
cx = torch.zeros_like(aa)
|
| 880 |
+
vx = torch.zeros_like(aa)
|
| 881 |
+
n_bad += int((fZ != zx).sum() + (fN != nx).sum()
|
| 882 |
+
+ (fC != cx).sum() + (fV != vx).sum())
|
| 883 |
+
else:
|
| 884 |
+
want = torch.tensor([(f_init >> (3 - i)) & 1 for i in range(4)])
|
| 885 |
+
n_bad += int((torch.stack([fZ, fN, fC, fV], 1)
|
| 886 |
+
!= want.unsqueeze(0)).sum())
|
| 887 |
+
status = "OK " if (n_bad == 0 and bad == 0) else "FAIL"
|
| 888 |
+
if n_bad or bad:
|
| 889 |
+
ok_all = False
|
| 890 |
+
if f_init == 0:
|
| 891 |
+
print(f" {status} {name:4} 65,536/65,536 operand pairs exact"
|
| 892 |
+
+ ("" if n_bad == 0 else f" ({n_bad} mismatches)"))
|
| 893 |
+
|
| 894 |
+
# ---- 2. exhaustive control-flow sweeps ----------------------------------
|
| 895 |
+
print("\n[2/4] Exhaustive control-flow sweeps")
|
| 896 |
+
cases = []
|
| 897 |
+
for cnd in range(8): # all Jcc x all flag nibbles
|
| 898 |
+
for f in range(16):
|
| 899 |
+
prog = _instr(0xD, imm=cnd) + [0x00, 0x08] + _instr(0xF) + [0] * 2 + _instr(0xF)
|
| 900 |
+
cases.append(_mk_state(mem=prog, flags=[(f >> (3 - i)) & 1 for i in range(4)]))
|
| 901 |
+
for t in range(16): # all JMP targets
|
| 902 |
+
prog = _instr(0xC) + [0x00, t]
|
| 903 |
+
cases.append(_mk_state(mem=prog))
|
| 904 |
+
for spv in range(16): # CALL: every SP value
|
| 905 |
+
prog = _instr(0xE) + [0x00, 0x06] + [0, 0] + _instr(0xF)
|
| 906 |
+
cases.append(_mk_state(mem=prog, sp=spv))
|
| 907 |
+
for ad in range(16): # LOAD/STORE: every address
|
| 908 |
+
prog = _instr(0xA, rd=2) + [0x00, ad]
|
| 909 |
+
st = _mk_state(mem=prog)
|
| 910 |
+
st["mem"][ad] |= 0xA5 if ad >= 4 else st["mem"][ad]
|
| 911 |
+
cases.append(st)
|
| 912 |
+
prog = _instr(0xB, rs=1) + [0x00, ad]
|
| 913 |
+
cases.append(_mk_state(mem=prog, regs=(0, 0x5A, 0, 0)))
|
| 914 |
+
V = torch.stack([state_to_vec(s) for s in cases]).to(device)
|
| 915 |
+
V = mm.step(V).cpu()
|
| 916 |
+
bad = sum(1 for i, s in enumerate(cases) if vec_to_state(V[i]) != ref_step(s))
|
| 917 |
+
print(f" {'OK ' if bad == 0 else 'FAIL'} {len(cases)} directed cases "
|
| 918 |
+
f"(8 Jcc x 16 flag sets, 16 JMP targets, 16 CALL SPs, 32 LOAD/STORE) exact")
|
| 919 |
+
ok_all &= bad == 0
|
| 920 |
+
|
| 921 |
+
# ---- 3. random-state fuzz + halt fixed point -----------------------------
|
| 922 |
+
print("\n[3/4] Random-state fuzz (single step vs integer reference)")
|
| 923 |
+
gen = torch.Generator().manual_seed(0xC0FFEE)
|
| 924 |
+
Vr = (torch.rand(2000, STATE_BITS, generator=gen) < 0.5).float()
|
| 925 |
+
Vr[:, MatrixMachine.HALT_IDX] = 0.0
|
| 926 |
+
out = mm.step(Vr.to(device)).cpu()
|
| 927 |
+
bad = sum(1 for i in range(Vr.shape[0])
|
| 928 |
+
if vec_to_state(out[i]) != ref_step(vec_to_state(Vr[i])))
|
| 929 |
+
print(f" {'OK ' if bad == 0 else 'FAIL'} 2,000 uniformly random full states exact")
|
| 930 |
+
ok_all &= bad == 0
|
| 931 |
+
Vh = (torch.rand(500, STATE_BITS, generator=gen) < 0.5).float()
|
| 932 |
+
Vh[:, MatrixMachine.HALT_IDX] = 1.0
|
| 933 |
+
outh = mm.step(Vh.to(device)).cpu()
|
| 934 |
+
fixed = bool((outh == Vh.to(outh.device)).all())
|
| 935 |
+
print(f" {'OK ' if fixed else 'FAIL'} 500 halted states are exact fixed points")
|
| 936 |
+
ok_all &= fixed
|
| 937 |
+
|
| 938 |
+
# ---- 4. lockstep vs the gate-graph processor (shipped weights) ----------
|
| 939 |
+
print("\n[4/4] Instruction lockstep vs GenericThresholdCPU on "
|
| 940 |
+
"neural_computer8_registers (the gate graph)")
|
| 941 |
+
from eval_all import GenericThresholdCPU
|
| 942 |
+
T = {}
|
| 943 |
+
with safe_open(GATE_MODEL_PATH, framework="pt") as f:
|
| 944 |
+
for nm in f.keys():
|
| 945 |
+
T[nm] = f.get_tensor(nm).float()
|
| 946 |
+
gcpu = GenericThresholdCPU(T)
|
| 947 |
+
progs = [
|
| 948 |
+
("alu_chain", _mk_state(mem=_instr(0, 0, 1) + _instr(7, 2, 0) + _instr(9, 2, 1)
|
| 949 |
+
+ _instr(4, 1, 2) + _instr(0xF),
|
| 950 |
+
regs=(9, 5, 3, 0))),
|
| 951 |
+
("div_by_zero", _mk_state(mem=_instr(8, 0, 1) + _instr(0xF), regs=(77, 0, 0, 0))),
|
| 952 |
+
("load_store", _mk_state(mem=_instr(0xA, rd=3) + [0x00, 0x0E]
|
| 953 |
+
+ _instr(0xB, rs=3) + [0x00, 0x0F] + _instr(0xF)
|
| 954 |
+
+ [0] * 4 + [0xB7, 0x00],
|
| 955 |
+
)),
|
| 956 |
+
("call", _mk_state(mem=_instr(0xE) + [0x00, 0x06] + _instr(0xF)
|
| 957 |
+
+ _instr(0xF), sp=0xF)),
|
| 958 |
+
("jcc_taken", _mk_state(mem=_instr(0xD, imm=1) + [0x00, 0x06] + _instr(0xF)
|
| 959 |
+
+ _instr(0xF), flags=(0, 0, 0, 0))),
|
| 960 |
+
("shift_mul", _mk_state(mem=_instr(5, 0) + _instr(6, 1) + _instr(7, 0, 1)
|
| 961 |
+
+ _instr(0xF), regs=(0x81, 0x81, 0, 0))),
|
| 962 |
+
]
|
| 963 |
+
for name, st0 in progs:
|
| 964 |
+
sm = dict(st0)
|
| 965 |
+
sg = {"pc": st0["pc"], "regs": list(st0["regs"]), "flags": list(st0["flags"]),
|
| 966 |
+
"mem": list(st0["mem"]), "halted": False, "sp": st0["sp"]}
|
| 967 |
+
good = True
|
| 968 |
+
for _ in range(12):
|
| 969 |
+
if sm["halted"]:
|
| 970 |
+
break
|
| 971 |
+
sm, _n = mm.run(sm, max_cycles=1)
|
| 972 |
+
sg = gcpu.step(sg)
|
| 973 |
+
match = (sm["pc"] == sg["pc"] and sm["regs"] == sg["regs"]
|
| 974 |
+
and sm["flags"] == sg["flags"] and sm["mem"] == sg["mem"]
|
| 975 |
+
and sm["halted"] == sg["halted"]
|
| 976 |
+
and sm["sp"] == sg.get("sp", MEM_BYTES - 1))
|
| 977 |
+
if not match:
|
| 978 |
+
good = False
|
| 979 |
+
break
|
| 980 |
+
good = good and sm["halted"]
|
| 981 |
+
print(f" {'OK ' if good else 'FAIL'} {name}: full-state lockstep, every instruction")
|
| 982 |
+
ok_all &= good
|
| 983 |
+
|
| 984 |
+
print("\nMATRIX == GATE GRAPH == REFERENCE:", "PASS" if ok_all else "FAIL")
|
| 985 |
+
return ok_all
|
| 986 |
+
|
| 987 |
+
|
| 988 |
+
# =============================================================================
|
| 989 |
+
# Analog realization
|
| 990 |
+
# =============================================================================
|
| 991 |
+
|
| 992 |
+
def analog(mm: MatrixMachine, device: str) -> bool:
|
| 993 |
+
ok = True
|
| 994 |
+
print("\nAnalog crossbar simulation "
|
| 995 |
+
"(comparator at -0.5; ternary conductances are exact by construction)")
|
| 996 |
+
|
| 997 |
+
gen = torch.Generator().manual_seed(7)
|
| 998 |
+
Vr = (torch.rand(512, STATE_BITS, generator=gen) < 0.5).float().to(device)
|
| 999 |
+
margin = mm.min_margin(Vr)
|
| 1000 |
+
print(f" measured minimum |pre-activation - (-0.5)| over 512 random states, "
|
| 1001 |
+
f"all layers: {margin:.3f} (guarantee: 0.5)")
|
| 1002 |
+
ok &= abs(margin - 0.5) < 1e-6
|
| 1003 |
+
|
| 1004 |
+
prog = (_instr(0, 0, 1) + _instr(7, 2, 0) + _instr(9, 2, 1) + _instr(0xF))
|
| 1005 |
+
st0 = _mk_state(mem=prog, regs=(9, 5, 3, 0))
|
| 1006 |
+
ref_final, _ = mm.run(dict(st0), max_cycles=8)
|
| 1007 |
+
|
| 1008 |
+
import math
|
| 1009 |
+
rows_per_step = sum(int(W.shape[0]) for W in mm.W)
|
| 1010 |
+
n_steps = 4
|
| 1011 |
+
print(f" Gaussian read noise per MVM ({rows_per_step:,} comparator "
|
| 1012 |
+
f"decisions per instruction; flip iff |noise| >= 0.5):")
|
| 1013 |
+
for sigma in (0.05, 0.08, 0.10, 0.15, 0.25, 0.45):
|
| 1014 |
+
p_flip = math.erfc(0.5 / (sigma * math.sqrt(2.0)))
|
| 1015 |
+
exp_flips = p_flip * rows_per_step * n_steps
|
| 1016 |
+
exact = 0
|
| 1017 |
+
for t in range(20):
|
| 1018 |
+
g = torch.Generator(device=device).manual_seed(1000 + t)
|
| 1019 |
+
v = state_to_vec(st0).unsqueeze(0).to(device)
|
| 1020 |
+
for _ in range(8):
|
| 1021 |
+
if v[0, MatrixMachine.HALT_IDX] >= 0.5:
|
| 1022 |
+
break
|
| 1023 |
+
v = mm.step(v, analog=True, noise_sigma=sigma, gen=g)
|
| 1024 |
+
exact += int(vec_to_state(v[0].cpu()) == ref_final)
|
| 1025 |
+
print(f" sigma={sigma:.2f}: {exact}/20 runs bit-exact "
|
| 1026 |
+
f"(theory: {exp_flips:.2e} expected flips per run)")
|
| 1027 |
+
if exp_flips < 1e-3 and exact != 20:
|
| 1028 |
+
ok = False
|
| 1029 |
+
|
| 1030 |
+
print(" Static ternary-conductance mismatch (device variation, "
|
| 1031 |
+
"10 fabricated instances each):")
|
| 1032 |
+
for sg in (0.02, 0.05, 0.10, 0.20):
|
| 1033 |
+
exact = 0
|
| 1034 |
+
for t in range(10):
|
| 1035 |
+
mmp = mm.perturbed(sg, seed=200 + t)
|
| 1036 |
+
v = state_to_vec(st0).unsqueeze(0).to(device)
|
| 1037 |
+
for _ in range(8):
|
| 1038 |
+
if v[0, MatrixMachine.HALT_IDX] >= 0.5:
|
| 1039 |
+
break
|
| 1040 |
+
v = mmp.step(v, analog=True)
|
| 1041 |
+
exact += int(vec_to_state(v[0].cpu()) == ref_final)
|
| 1042 |
+
print(f" sigma_G={sg:.2f}: {exact}/10 instances bit-exact")
|
| 1043 |
+
if sg <= 0.02 and exact != 10:
|
| 1044 |
+
ok = False
|
| 1045 |
+
|
| 1046 |
+
print("ANALOG REALIZATION:", "PASS (bit-exact within the stated margins)"
|
| 1047 |
+
if ok else "FAIL")
|
| 1048 |
+
return ok
|
| 1049 |
+
|
| 1050 |
+
|
| 1051 |
+
# =============================================================================
|
| 1052 |
+
# CLI
|
| 1053 |
+
# =============================================================================
|
| 1054 |
+
|
| 1055 |
+
def main() -> int:
|
| 1056 |
+
ap = argparse.ArgumentParser(description="neural_matrix8 builder/verifier")
|
| 1057 |
+
ap.add_argument("cmd", choices=["build", "verify", "analog", "all"])
|
| 1058 |
+
ap.add_argument("--device", default="cuda" if torch.cuda.is_available() else "cpu")
|
| 1059 |
+
args = ap.parse_args()
|
| 1060 |
+
|
| 1061 |
+
rc = 0
|
| 1062 |
+
layers = None
|
| 1063 |
+
if args.cmd in ("build", "all"):
|
| 1064 |
+
layers, _ = build(save=True)
|
| 1065 |
+
if args.cmd in ("verify", "analog", "all"):
|
| 1066 |
+
if layers is not None:
|
| 1067 |
+
mm = MatrixMachine(layers, device=args.device)
|
| 1068 |
+
elif os.path.exists(MODEL_PATH):
|
| 1069 |
+
mm = MatrixMachine.from_file(MODEL_PATH, device=args.device)
|
| 1070 |
+
else:
|
| 1071 |
+
layers, _ = build(save=False)
|
| 1072 |
+
mm = MatrixMachine(layers, device=args.device)
|
| 1073 |
+
if args.cmd in ("verify", "all"):
|
| 1074 |
+
rc |= 0 if verify(mm, args.device) else 1
|
| 1075 |
+
if args.cmd in ("analog", "all"):
|
| 1076 |
+
rc |= 0 if analog(mm, args.device) else 1
|
| 1077 |
+
return rc
|
| 1078 |
+
|
| 1079 |
+
|
| 1080 |
+
if __name__ == "__main__":
|
| 1081 |
+
sys.exit(main())
|
tests/test_play.py
CHANGED
|
@@ -15,8 +15,9 @@ import unittest
|
|
| 15 |
from contextlib import redirect_stdout
|
| 16 |
|
| 17 |
REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
| 18 |
-
|
| 19 |
-
sys.path
|
|
|
|
| 20 |
|
| 21 |
import play # noqa: E402
|
| 22 |
|
|
|
|
| 15 |
from contextlib import redirect_stdout
|
| 16 |
|
| 17 |
REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
| 18 |
+
for _p in (REPO_ROOT, os.path.join(REPO_ROOT, "tools")): # play.py lives in tools/
|
| 19 |
+
if _p not in sys.path:
|
| 20 |
+
sys.path.insert(0, _p)
|
| 21 |
|
| 22 |
import play # noqa: E402
|
| 23 |
|
todo.md
CHANGED
|
@@ -9,13 +9,3 @@ Unfinished work.
|
|
| 9 |
- The standalone float normalize stages, superseded by the composed pipelines' own normalizers, are removed from the circuit inventory.
|
| 10 |
- Every gate in the 32-bit multiply circuit resolves its .inputs to a defined signal.
|
| 11 |
- A machine-checked correctness proof relates the shipped weights to the ISA.
|
| 12 |
-
- The processor's full state transition is expressed as a recurrent ternary threshold network and executes as analog matrix-vector products rather than a wired gate graph.
|
| 13 |
-
- The verified gate wiring is compiled into a fixed sequence of ternary weight matrices W1..Wk with a Heaviside step between them, acting on a state vector that holds the program counter, registers, flags, and every memory bit.
|
| 14 |
-
- One instruction is that fixed stack of threshold layers, and the machine is the stack iterated until the halt bit sets, so the whole processor is a single recurrent linear-threshold network.
|
| 15 |
-
- The matrix form is proven equal to the gate-graph processor, so the ISA semantics carry over bit for bit.
|
| 16 |
-
- Each weight matrix maps to a resistive or photonic crossbar with conductances drawn from {-1, 0, +1}, and one clock cycle is one analog matrix-vector product followed by thresholding.
|
| 17 |
-
- The analog realization is shown to reproduce the digital circuit within the device's conductance-quantization and noise margins.
|
| 18 |
-
- A program running on the processor emits the exact tensor file that defines that processor, and builds any other machine in the family from a description of it.
|
| 19 |
-
- The program's output stream is the byte-for-byte safetensors file of the running machine, and that equality is machine-checked rather than observed on a single run.
|
| 20 |
-
- The same program takes an encoded description of any machine in the family and constructs that machine's weight file, so the constructor is universal and the description selects what is built.
|
| 21 |
-
- Self-reproduction is the case where the description handed to the constructor is its own.
|
|
|
|
| 9 |
- The standalone float normalize stages, superseded by the composed pipelines' own normalizers, are removed from the circuit inventory.
|
| 10 |
- Every gate in the 32-bit multiply circuit resolves its .inputs to a defined signal.
|
| 11 |
- A machine-checked correctness proof relates the shipped weights to the ISA.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
build_all.py → tools/build_all.py
RENAMED
|
@@ -26,7 +26,7 @@ from pathlib import Path
|
|
| 26 |
import torch
|
| 27 |
from safetensors import safe_open
|
| 28 |
|
| 29 |
-
ROOT = Path(__file__).resolve().parent
|
| 30 |
SEED = ROOT / "neural_computer.safetensors"
|
| 31 |
OUT_DIR = ROOT / "variants"
|
| 32 |
OUT_DIR.mkdir(exist_ok=True)
|
|
|
|
| 26 |
import torch
|
| 27 |
from safetensors import safe_open
|
| 28 |
|
| 29 |
+
ROOT = Path(__file__).resolve().parent.parent # repo root (this file lives in tools/)
|
| 30 |
SEED = ROOT / "neural_computer.safetensors"
|
| 31 |
OUT_DIR = ROOT / "variants"
|
| 32 |
OUT_DIR.mkdir(exist_ok=True)
|
cpu_programs.py → tools/cpu_programs.py
RENAMED
|
File without changes
|
play.py → tools/play.py
RENAMED
|
@@ -25,7 +25,8 @@ import sys
|
|
| 25 |
import torch
|
| 26 |
from safetensors import safe_open
|
| 27 |
|
| 28 |
-
|
|
|
|
| 29 |
|
| 30 |
# Reuse the variant-aware CPU runtime from eval_all.py
|
| 31 |
from eval_all import GenericThresholdCPU, builtin_program
|
|
@@ -47,8 +48,7 @@ def main() -> int:
|
|
| 47 |
parser = argparse.ArgumentParser(description="Threshold computer playground")
|
| 48 |
parser.add_argument(
|
| 49 |
"--model", type=str,
|
| 50 |
-
default=os.path.join(
|
| 51 |
-
"variants", "neural_computer8_small.safetensors"),
|
| 52 |
help="Path to a .safetensors variant"
|
| 53 |
)
|
| 54 |
parser.add_argument("--skip-cpu", action="store_true",
|
|
|
|
| 25 |
import torch
|
| 26 |
from safetensors import safe_open
|
| 27 |
|
| 28 |
+
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # this file lives in tools/
|
| 29 |
+
sys.path.insert(0, REPO)
|
| 30 |
|
| 31 |
# Reuse the variant-aware CPU runtime from eval_all.py
|
| 32 |
from eval_all import GenericThresholdCPU, builtin_program
|
|
|
|
| 48 |
parser = argparse.ArgumentParser(description="Threshold computer playground")
|
| 49 |
parser.add_argument(
|
| 50 |
"--model", type=str,
|
| 51 |
+
default=os.path.join(REPO, "variants", "neural_computer8_small.safetensors"),
|
|
|
|
| 52 |
help="Path to a .safetensors variant"
|
| 53 |
)
|
| 54 |
parser.add_argument("--skip-cpu", action="store_true",
|
prune_weights.py → tools/prune_weights.py
RENAMED
|
@@ -7,10 +7,13 @@ Phase 2: Apply all successes at once, binary search if conflicts
|
|
| 7 |
|
| 8 |
import argparse
|
| 9 |
import os
|
|
|
|
| 10 |
import time
|
| 11 |
|
| 12 |
import torch
|
| 13 |
from safetensors.torch import save_file
|
|
|
|
|
|
|
| 14 |
from eval import BatchedFitnessEvaluator, create_population, load_model
|
| 15 |
|
| 16 |
torch.manual_seed(0)
|
|
|
|
| 7 |
|
| 8 |
import argparse
|
| 9 |
import os
|
| 10 |
+
import sys
|
| 11 |
import time
|
| 12 |
|
| 13 |
import torch
|
| 14 |
from safetensors.torch import save_file
|
| 15 |
+
|
| 16 |
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) # repo root
|
| 17 |
from eval import BatchedFitnessEvaluator, create_population, load_model
|
| 18 |
|
| 19 |
torch.manual_seed(0)
|
test_cpu.py → tools/test_cpu.py
RENAMED
|
@@ -20,7 +20,9 @@ from pathlib import Path
|
|
| 20 |
import torch
|
| 21 |
from safetensors import safe_open
|
| 22 |
|
| 23 |
-
|
|
|
|
|
|
|
| 24 |
from eval_all import GenericThresholdCPU, get_manifest
|
| 25 |
from cpu_programs import SUITE
|
| 26 |
|
|
@@ -61,8 +63,7 @@ def main() -> int:
|
|
| 61 |
parser = argparse.ArgumentParser(description="Run the CPU program suite")
|
| 62 |
parser.add_argument(
|
| 63 |
"--model", type=str,
|
| 64 |
-
default=os.path.join(
|
| 65 |
-
"variants", "neural_computer8_small.safetensors"),
|
| 66 |
help="Path to .safetensors variant",
|
| 67 |
)
|
| 68 |
parser.add_argument(
|
|
|
|
| 20 |
import torch
|
| 21 |
from safetensors import safe_open
|
| 22 |
|
| 23 |
+
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # this file lives in tools/
|
| 24 |
+
sys.path.insert(0, REPO) # eval_all lives at the repo root
|
| 25 |
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) # cpu_programs is a sibling
|
| 26 |
from eval_all import GenericThresholdCPU, get_manifest
|
| 27 |
from cpu_programs import SUITE
|
| 28 |
|
|
|
|
| 63 |
parser = argparse.ArgumentParser(description="Run the CPU program suite")
|
| 64 |
parser.add_argument(
|
| 65 |
"--model", type=str,
|
| 66 |
+
default=os.path.join(REPO, "variants", "neural_computer8_small.safetensors"),
|
|
|
|
| 67 |
help="Path to .safetensors variant",
|
| 68 |
)
|
| 69 |
parser.add_argument(
|
variants/neural_matrix8.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:837e5984345ad36af3d4297413046690665751db3567f9c7c33f698e33cf5d3b
|
| 3 |
+
size 9523232
|
variants/neural_subleq8io.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:7744f8ba85916ab705f8e608cc49ca9150e3eac2dc5dbbe6c7682acdb16df93a
|
| 3 |
+
size 95306
|