Commit ·
9dc9d2f
1
Parent(s): 60d3c95
Consolidate the Python sources into twelve files with one-line docstrings, fold the demos into the playground, and delete the scripts that do not belong to the artifact
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitignore +1 -1
- README.md +91 -123
- demos/README.md +0 -22
- demos/neural_attractor_factoring.py +0 -55
- demos/neural_attractor_nqueens.py +0 -90
- demos/neural_ca_loschmidt_echo.py +0 -55
- demos/neural_ca_reversible_cipher.py +0 -84
- demos/neural_computer8_collatz.py +0 -76
- demos/neural_computer8_euclid_gcd.py +0 -63
- demos/neural_computer8_self_modifying_sieve.py +0 -76
- demos/neural_matrix8_gpu_cpu_fleet.py +0 -73
- demos/neural_reflect_self_modifying_stack.py +0 -87
- demos/neural_reversible_counterfactual.py +0 -49
- demos/neural_rv32_machin_pi.py +0 -85
- demos/neural_rv32_neural_nets_via_neur.py +0 -184
- demos/neural_subleq8io_universal_constructor.py +0 -74
- demos/neural_tile_pascal_lucas.py +0 -42
- llm_integration/baseline.py +0 -221
- llm_integration/circuits.py +0 -320
- llm_integration/fitness.py +0 -218
- llm_integration/{model.py → llm.py} +1605 -244
- llm_integration/smollm2/analyze_smollm2.py +0 -232
- llm_integration/train.py +0 -955
- paper/README.md +34 -40
- paper/universal_construction_ternary.pdf +2 -2
- paper/universal_construction_ternary.tex +4 -4
- src/attractor.py +0 -261
- src/automata.py +1759 -0
- src/build.py +591 -599
- src/ca.py +0 -261
- src/canonical.py +0 -25
- src/check_c.py +0 -199
- src/check_codec.py +0 -128
- src/check_device.py +0 -118
- src/check_environment.py +0 -110
- src/check_family.py +0 -64
- src/check_family_gate.py +0 -70
- src/check_framing.py +0 -133
- src/check_host.py +0 -113
- src/check_host_family.py +0 -65
- src/check_host_fold.py +0 -73
- src/check_lev.py +0 -121
- src/check_optimal.py +0 -153
- src/check_rocq.py +0 -67
- src/check_sigma.py +0 -89
- src/check_steps.py +0 -169
- src/check_ternarity.py +0 -142
- src/constructor8.py +0 -580
- src/eval.py +1578 -150
- src/eval_all.py +0 -912
.gitignore
CHANGED
|
@@ -21,4 +21,4 @@ rocq/.nia.cache
|
|
| 21 |
rocq/extract/dump
|
| 22 |
rocq/extract/*.cm*
|
| 23 |
rocq/extract/*.o
|
| 24 |
-
src/
|
|
|
|
| 21 |
rocq/extract/dump
|
| 22 |
rocq/extract/*.cm*
|
| 23 |
rocq/extract/*.o
|
| 24 |
+
src/.work/
|
README.md
CHANGED
|
@@ -99,14 +99,14 @@ for a, c in [(0, 0), (0, 1), (1, 0), (1, 1)]:
|
|
| 99 |
Run the full circuit verification suite against any variant:
|
| 100 |
|
| 101 |
```bash
|
| 102 |
-
python src/
|
| 103 |
-
python src/
|
| 104 |
-
python src/
|
| 105 |
# program through the
|
| 106 |
# threshold-gated CPU
|
| 107 |
```
|
| 108 |
|
| 109 |
-
`src/
|
| 110 |
|
| 111 |
For an interactive walkthrough that exercises Boolean gates, the 8-bit ALU, mod-5 divisibility, and a CPU loop end-to-end:
|
| 112 |
|
|
@@ -115,15 +115,34 @@ python tools/play.py # 1 KB demo, runs in s
|
|
| 115 |
python tools/play.py --model neural_computer.safetensors # 64 KB, slower
|
| 116 |
```
|
| 117 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 118 |
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):
|
| 119 |
|
| 120 |
```bash
|
| 121 |
-
python
|
| 122 |
-
python
|
| 123 |
-
python
|
| 124 |
```
|
| 125 |
|
| 126 |
-
Each program is assembled by a small Python assembler (`
|
| 127 |
|
| 128 |
---
|
| 129 |
|
|
@@ -325,7 +344,7 @@ python src/build.py --bits 32 -a 6 --apply all # neural_computer32_addr6
|
|
| 325 |
To regenerate every named variant in one pass:
|
| 326 |
|
| 327 |
```bash
|
| 328 |
-
python
|
| 329 |
```
|
| 330 |
|
| 331 |
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 `src/eval.py` on each as a sanity check.
|
|
@@ -333,11 +352,11 @@ This populates `variants/` with all 18 builds, quantizes each one to the smalles
|
|
| 333 |
The quantizer is also available standalone:
|
| 334 |
|
| 335 |
```bash
|
| 336 |
-
python src/
|
| 337 |
-
python src/
|
| 338 |
-
python src/
|
| 339 |
-
python src/
|
| 340 |
-
python src/
|
| 341 |
```
|
| 342 |
|
| 343 |
Every weight and bias tensor in the canonical model fits in `int8`. The eval pipeline promotes weights to `float32` on load, so integer storage is exact and transparent.
|
|
@@ -369,7 +388,7 @@ Every weight and bias tensor in the canonical model fits in `int8`. The eval pip
|
|
| 369 |
|
| 370 |
The 8-bit arithmetic and ALU tests sample strategically; the circuits are constructed gate by gate, so the full 65,536-case sweep adds nothing. The 16-bit and 32-bit arithmetic tests sample edge cases only; full exhaustive coverage at those widths is infeasible without specialized hardware.
|
| 371 |
|
| 372 |
-
`src/
|
| 373 |
|
| 374 |
---
|
| 375 |
|
|
@@ -405,7 +424,7 @@ Signed comparisons ride the unsigned bit-cascade with sign bits complemented thr
|
|
| 405 |
|
| 406 |
```bash
|
| 407 |
python src/build.py --apply rv32 # build the file
|
| 408 |
-
python src/
|
| 409 |
python src/machines.py rv32 # eight-program lockstep suite
|
| 410 |
python src/machines.py rv32-c # stock-compiler C, end to end
|
| 411 |
```
|
|
@@ -461,9 +480,9 @@ write is gated by NOT halt), so iterating past HALT is harmless.
|
|
| 461 |
conductance mismatch (bit-exact through σ_G = 0.10).
|
| 462 |
|
| 463 |
```bash
|
| 464 |
-
python src/
|
| 465 |
-
python src/
|
| 466 |
-
python src/
|
| 467 |
```
|
| 468 |
|
| 469 |
Because the transition is a fixed matrix stack proven equal to the gate-graph
|
|
@@ -508,8 +527,8 @@ correct assignment, but reaching it is NP-hard in general and not guaranteed
|
|
| 508 |
within a fixed annealing schedule.
|
| 509 |
|
| 510 |
```bash
|
| 511 |
-
python
|
| 512 |
-
python
|
| 513 |
```
|
| 514 |
|
| 515 |
---
|
|
@@ -549,9 +568,9 @@ Four checks establish the equality:
|
|
| 549 |
closing the loop across two generations.
|
| 550 |
|
| 551 |
```bash
|
| 552 |
-
python src/
|
| 553 |
-
python src/
|
| 554 |
-
python src/
|
| 555 |
```
|
| 556 |
|
| 557 |
A program running on the processor emits the exact tensor file that defines
|
|
@@ -562,12 +581,12 @@ weights and checked to the byte.
|
|
| 562 |
`paper/` states and proves what this machine does, for a host serialized one
|
| 563 |
unit at a time: `variants/neural_subleq8io_netlist.safetensors` stores the bias
|
| 564 |
of every unit and every edge of the wiring, 17,598 units and 51,132 edges in
|
| 565 |
-
160,240 bytes, in the order `src/
|
| 566 |
file denotes is the network the paper analyses, and parsing the file rebuilds
|
| 567 |
it. That host is the member with eight-bit words of a family whose step has
|
| 568 |
depth 13 for every word width, with a carry-lookahead subtractor and the
|
| 569 |
-
operands fetched through the decode lines of the counter; `src/
|
| 570 |
-
builds any member and `src/
|
| 571 |
on the evaluator in C. Separating construction from copying
|
| 572 |
as von Neumann did, a 27-instruction program emits the serialization of the
|
| 573 |
host, its initial memory and its own description tape; each generation is
|
|
@@ -575,9 +594,9 @@ instantiated from the bytes the last one emitted, by parsing that output,
|
|
| 575 |
rebuilding the netlist from its first field, and building the evaluator from
|
| 576 |
that netlist. It also shows where the restriction to {-1, 0, 1} is paid for,
|
| 577 |
and that a fixed ternary network, the two-head machine of
|
| 578 |
-
`src/
|
| 579 |
record per step, which `rocq/` proves for every window of 2^α cells, bank and
|
| 580 |
-
tape. The host itself, compiled by `src/
|
| 581 |
two inputs with records that load its memory, its tape device and a hand-over,
|
| 582 |
and stored in the member with 4096 cells, runs the 27-instruction program on a
|
| 583 |
recipe for its own records: it writes them into the idle bank, copies the recipe
|
|
@@ -587,20 +606,16 @@ by the dynamics alone; `src/stored_host_run.c` runs three generations of
|
|
| 587 |
cells does the same with a description code of its own, every generation of it
|
| 588 |
proved in Rocq. Its robustness section gives the margin every orbit has
|
| 589 |
against perturbation, a certificate of the leakage an orbit tolerates from the
|
| 590 |
-
inputs active along it, and the amplified host of `src/
|
| 591 |
of the levelized host replaced by r rows reading all r copies of its inputs,
|
| 592 |
which multiplies every margin by r, reproduces itself exactly, fails only when two
|
| 593 |
wrong copies meet, with a probability of second order that the paper bounds, and
|
| 594 |
completes the self-reproducing run under read noise at levels where the host
|
| 595 |
fails within its first bytes.
|
| 596 |
|
| 597 |
-
|
| 598 |
-
`src/
|
| 599 |
-
`
|
| 600 |
-
`src/stored_host_run.c`, `src/check_c.py`, `src/throughput.py`, `src/noise.py`,
|
| 601 |
-
`src/redundant.py`, `src/neuromorphic.py` and the
|
| 602 |
-
`src/check_*.py` set, and their records are in `paper/runs/`; `rocq/` holds the
|
| 603 |
-
Rocq development, which `src/check_rocq.py` builds.
|
| 604 |
|
| 605 |
---
|
| 606 |
|
|
@@ -707,11 +722,11 @@ the Heaviside AND/XOR weights that realize each gate, so the file holds both the
|
|
| 707 |
reversible program and the threshold substrate it runs on.
|
| 708 |
|
| 709 |
```bash
|
| 710 |
-
python src/
|
| 711 |
-
python src/
|
| 712 |
-
python src/
|
| 713 |
-
python
|
| 714 |
-
python
|
| 715 |
```
|
| 716 |
|
| 717 |
---
|
|
@@ -747,8 +762,8 @@ circuits. With mirror routing and a constant-particle source the construction is
|
|
| 747 |
functionally complete (Margolus 1984).
|
| 748 |
|
| 749 |
```bash
|
| 750 |
-
python src/
|
| 751 |
-
python
|
| 752 |
```
|
| 753 |
|
| 754 |
---
|
|
@@ -780,8 +795,8 @@ bias (`-tau`); reloading it regrows the counter and reproduces the binding
|
|
| 780 |
decision from the gate.
|
| 781 |
|
| 782 |
```bash
|
| 783 |
-
python src/
|
| 784 |
-
python
|
| 785 |
```
|
| 786 |
|
| 787 |
---
|
|
@@ -947,9 +962,9 @@ Alternative architectures: `PositionExtractor` (~1.5 M, position-specific, no at
|
|
| 947 |
### Training
|
| 948 |
|
| 949 |
```bash
|
| 950 |
-
python
|
| 951 |
-
python
|
| 952 |
-
python
|
| 953 |
```
|
| 954 |
|
| 955 |
Loss components: BCE on output bits, BCE on extracted A and B bits (2× weight), and CE on operation classification. Curriculum runs 0–9 → 0–99 → 0–255. Optimizer is AdamW, lr 3e-4, gradient clipping 1.0.
|
|
@@ -966,89 +981,42 @@ variants/ 18 fitness variants + 9 standalone machines
|
|
| 966 |
neural_subleq8io, neural_reflect,
|
| 967 |
neural_attractor, neural_reversible, neural_ca,
|
| 968 |
neural_tile)
|
| 969 |
-
src/ the library
|
| 970 |
-
├── build.py generator (
|
| 971 |
-
|
| 972 |
-
├── eval.py gate-level fitness suite, NetlistEvaluator, float oracles
|
| 973 |
-
|
| 974 |
-
├── machines.py
|
| 975 |
-
│
|
| 976 |
-
├──
|
| 977 |
-
│
|
| 978 |
-
├──
|
| 979 |
-
│
|
| 980 |
-
├──
|
| 981 |
-
│
|
| 982 |
-
|
| 983 |
-
|
| 984 |
-
|
| 985 |
-
|
| 986 |
-
|
| 987 |
-
├──
|
| 988 |
-
├──
|
| 989 |
-
|
| 990 |
-
├── tile.py neural_tile: abstract tile assembly, threshold binding rule,
|
| 991 |
-
│ XOR/Sierpinski and binary-counter tile sets
|
| 992 |
-
├── selfrep.py the tape device, the constructor P and the self-reproducing
|
| 993 |
-
│ program P*, the framing maps, and three evaluators
|
| 994 |
-
├── canonical.py deterministic safetensors writer (pinned key and tensor order)
|
| 995 |
-
├── paper_stats.py unit counts, levelization sizes, effective fan-in
|
| 996 |
-
├── paper_verify.py codec, framing, serialization, datapath, levelization
|
| 997 |
-
├── paper_runs.py self-description, three generations, lockstep
|
| 998 |
-
├── host_family.py the host family H_A: a constant-depth SUBLEQ step for
|
| 999 |
-
│ every word width, and its serialization in build order
|
| 1000 |
-
├── netlist_eval.c an evaluator of the host family, the amplified host and the
|
| 1001 |
-
│ interpreter in C, the levels of a large netlist in parallel
|
| 1002 |
-
├── check_c.py the host on the evaluator in C
|
| 1003 |
-
├── twohead.py the two-head machine, transcribed, the description code
|
| 1004 |
-
│ and the translating organism; writes rocq/Organism.v
|
| 1005 |
-
├── twohead_net.py the two-head machine as a ternary netlist N_{G,L} for a window
|
| 1006 |
-
│ of 2^α cells: its check, its file, the organism on it in C,
|
| 1007 |
-
│ and the comparison with the netlist extracted from rocq/
|
| 1008 |
├── twohead_lockstep.c the organism on N_{G,L} beside the transcription, in C
|
| 1009 |
-
├── stored_host.py the host compiled into 36,074 records of the two-head machine
|
| 1010 |
-
│ with 4096 cells: one pass against the host's step and device,
|
| 1011 |
-
│ the image and its recipe, and its generations
|
| 1012 |
├── stored_host_run.c the stored host's generations on the two-head machine, in C
|
| 1013 |
├── restore_bound.c the restoration bound of the amplified host along the orbit
|
| 1014 |
-
|
| 1015 |
-
├── independent/ separate code written from the paper's definitions: the host
|
| 1016 |
-
│ family, the levelization, a noise simulator in C, weight
|
| 1017 |
-
│ error, and the neuromorphic datapath
|
| 1018 |
-
├── redundant.py the amplified host N_r: r-fold redundancy with the restoring
|
| 1019 |
-
│ organ inside every unit, its self-reproduction, its runs
|
| 1020 |
-
│ under read noise
|
| 1021 |
-
├── noise.py read noise on the self-reproducing run, the deviation curve
|
| 1022 |
-
│ against the bounds, weight error with zero-entry leakage, the
|
| 1023 |
-
│ active-input counts that certify an orbit
|
| 1024 |
-
├── neuromorphic.py the datapath on the Loihi and Akida neuron models
|
| 1025 |
-
└── check_*.py codec boundaries and optimality, step counts, ternary depth,
|
| 1026 |
-
environment cost, device, host, host family, levelization,
|
| 1027 |
-
framing, serialization, all 29 machines
|
| 1028 |
paper/ the manuscript, and the record of every run it reports
|
| 1029 |
rocq/ Rocq proofs: the netlist of the two-head machine computes its
|
| 1030 |
step for every window of 2^α cells, the organism builds every
|
| 1031 |
image from any description of it and reproduces in every
|
| 1032 |
generation
|
| 1033 |
-
tools/
|
| 1034 |
-
|
| 1035 |
-
|
| 1036 |
-
|
| 1037 |
-
build_attractor.py / test_attractor.py (neural_attractor),
|
| 1038 |
-
build_reversible.py / reversible_matrix.py (neural_reversible),
|
| 1039 |
-
build_ca.py (neural_ca matrix tile), build_tile.py (neural_tile)
|
| 1040 |
-
llm_integration/ SmolLM2 extractor + circuit wrapper + training code
|
| 1041 |
-
├── circuits.py FrozenThresholdCircuits (loads safetensors, exposes
|
| 1042 |
-
│ add_8bit / sub_8bit / mul_8bit / compare_*)
|
| 1043 |
-
├── model.py Extractor variants, ArithmeticModel
|
| 1044 |
-
├── train.py router / interface / llm training modes
|
| 1045 |
-
├── fitness.py randomized fitness function
|
| 1046 |
-
├── baseline.py vanilla SmolLM2 baseline measurement
|
| 1047 |
├── trained/ checkpointed extractor weights
|
| 1048 |
-
└── smollm2/
|
| 1049 |
-
├── SMOLLM2_ARCHITECTURE.md architecture analysis
|
| 1050 |
-
├── analyze_smollm2.py analysis script
|
| 1051 |
-
└── smollm2_analysis.json analysis output
|
| 1052 |
```
|
| 1053 |
|
| 1054 |
---
|
|
|
|
| 99 |
Run the full circuit verification suite against any variant:
|
| 100 |
|
| 101 |
```bash
|
| 102 |
+
python src/eval.py variants variants/ # 18 fitness variants (machines skipped)
|
| 103 |
+
python src/eval.py variants neural_computer.safetensors # the canonical file
|
| 104 |
+
python src/eval.py variants --cpu-program variants/ # also run an assembled
|
| 105 |
# program through the
|
| 106 |
# threshold-gated CPU
|
| 107 |
```
|
| 108 |
|
| 109 |
+
`src/eval.py variants` reads each variant's manifest, runs a gate-level fitness suite (13,900–15,900 tests per variant covering Boolean, arithmetic, ALU, control, modular, error-detection, threshold, and float circuits, including end-to-end evaluation of the composed float pipelines from the shipped wiring metadata — see the Verification table), and optionally executes a small assembled program through a manifest-sized threshold CPU plus a chained 16- or 32-bit ALU sequence on wider variants.
|
| 110 |
|
| 111 |
For an interactive walkthrough that exercises Boolean gates, the 8-bit ALU, mod-5 divisibility, and a CPU loop end-to-end:
|
| 112 |
|
|
|
|
| 115 |
python tools/play.py --model neural_computer.safetensors # 64 KB, slower
|
| 116 |
```
|
| 117 |
|
| 118 |
+
Each machine of the family also has a demonstration on a named task, which loads the shipped weights and checks its result against an independent reference:
|
| 119 |
+
|
| 120 |
+
| Command | Machine | What it does |
|
| 121 |
+
|---------|---------|--------------|
|
| 122 |
+
| `python tools/play.py sieve` | `neural_computer8` | Sieve of Eratosthenes; with no indexed addressing, the program rewrites the address bytes of its own LOAD/STORE. Finds all 54 primes < 256. |
|
| 123 |
+
| `python tools/play.py gcd` | `neural_computer8` | Euclid's GCD, with `a mod b` built from the DIV/MUL/SUB opcodes and an XOR register swap. |
|
| 124 |
+
| `python tools/play.py collatz` | `neural_computer8` | Collatz step counts; exact for every seed whose trajectory stays inside the 8-bit range. |
|
| 125 |
+
| `python tools/play.py pi` | `neural_rv32` | pi to nine digits by Machin's 1706 arctangent formula, printed to the console. |
|
| 126 |
+
| `python tools/play.py nets` | `neural_rv32` | Ternary neural nets run as NEUR software: one learned by gradient descent, one (XOR) compiled by construction. |
|
| 127 |
+
| `python tools/play.py fleet` | `neural_matrix8` | 65,536 CPUs stepped in lockstep on the GPU as one batched matrix product. |
|
| 128 |
+
| `python tools/play.py factoring` | `neural_attractor` | Factors semiprimes by relaxing an 8x8 multiplier's energy backward; also divides. |
|
| 129 |
+
| `python tools/play.py nqueens` | `neural_attractor` | Solves 6- and 8-queens by relaxing toward zero unsatisfied clauses. |
|
| 130 |
+
| `python tools/play.py constructor` | `neural_subleq8io` | The universal constructor fabricates a sibling machine byte-for-byte, which then boots. |
|
| 131 |
+
| `python tools/play.py stack` | `neural_reflect` | Self-modifying SUBLEQ code, on a stored machine, on the fixed interpreter: three levels in one tensor. |
|
| 132 |
+
| `python tools/play.py counterfactual` | `neural_reversible` | Bijective mixing, exact reverse recovery, and the counterfactual when a digest bit is flipped. |
|
| 133 |
+
| `python tools/play.py loschmidt` | `neural_ca` | A ~2,000-particle gas mixed 500 steps and un-mixed exactly; one flipped cell corrupts half the past. |
|
| 134 |
+
| `python tools/play.py cipher` | `neural_ca` | The reversible automaton as a block cipher: exact decryption with the right key, noise with a wrong one. |
|
| 135 |
+
| `python tools/play.py pascal` | `neural_tile` | Self-assembly grows Pascal's triangle mod 2; every cell verified against Lucas' theorem. |
|
| 136 |
+
|
| 137 |
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):
|
| 138 |
|
| 139 |
```bash
|
| 140 |
+
python src/eval.py cpu # default: 1 KB, ~2 s
|
| 141 |
+
python src/eval.py cpu --model neural_computer.safetensors # 64 KB canonical, ~2 min
|
| 142 |
+
python src/eval.py cpu --only fib,sum_n # subset of suite
|
| 143 |
```
|
| 144 |
|
| 145 |
+
Each program is assembled by a small Python assembler (in `src/eval.py`) and run through the threshold-gated CPU; the driver verifies expected memory contents at HALT.
|
| 146 |
|
| 147 |
---
|
| 148 |
|
|
|
|
| 344 |
To regenerate every named variant in one pass:
|
| 345 |
|
| 346 |
```bash
|
| 347 |
+
python src/build.py variants
|
| 348 |
```
|
| 349 |
|
| 350 |
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 `src/eval.py` on each as a sanity check.
|
|
|
|
| 352 |
The quantizer is also available standalone:
|
| 353 |
|
| 354 |
```bash
|
| 355 |
+
python src/build.py quantize path/to/file.safetensors # in-place
|
| 356 |
+
python src/build.py quantize variants/ # whole directory
|
| 357 |
+
python src/build.py quantize model.safetensors -o quantized.safetensors
|
| 358 |
+
python src/build.py quantize file.safetensors --ternary # push toward {-1, 0, 1} weights
|
| 359 |
+
python src/build.py quantize file.safetensors --ternary --strict # error if any weight is non-ternary
|
| 360 |
```
|
| 361 |
|
| 362 |
Every weight and bias tensor in the canonical model fits in `int8`. The eval pipeline promotes weights to `float32` on load, so integer storage is exact and transparent.
|
|
|
|
| 388 |
|
| 389 |
The 8-bit arithmetic and ALU tests sample strategically; the circuits are constructed gate by gate, so the full 65,536-case sweep adds nothing. The 16-bit and 32-bit arithmetic tests sample edge cases only; full exhaustive coverage at those widths is infeasible without specialized hardware.
|
| 390 |
|
| 391 |
+
`src/eval.py variants` runs the unified suite. Exit code is the number of failing variants (0 means all pass). **Testing evaluates the shipped weights; rebuilding is a separate step.** `python src/eval.py variants 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 nine standalone machines. Rebuilding the models (`python src/build.py variants`, ~50 min for all 18) is a separate step, needed only when the circuit constructions in `src/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, with no slot-0 approximation anywhere.
|
| 392 |
|
| 393 |
---
|
| 394 |
|
|
|
|
| 424 |
|
| 425 |
```bash
|
| 426 |
python src/build.py --apply rv32 # build the file
|
| 427 |
+
python src/build.py quantize variants/neural_rv32.safetensors --ternary --strict
|
| 428 |
python src/machines.py rv32 # eight-program lockstep suite
|
| 429 |
python src/machines.py rv32-c # stock-compiler C, end to end
|
| 430 |
```
|
|
|
|
| 480 |
conductance mismatch (bit-exact through σ_G = 0.10).
|
| 481 |
|
| 482 |
```bash
|
| 483 |
+
python src/machines.py matrix8 build # compile + save variants/neural_matrix8.safetensors
|
| 484 |
+
python src/machines.py matrix8 verify # exhaustive equality vs the gate graph and integer reference
|
| 485 |
+
python src/machines.py matrix8 analog # margin measurement + read-noise + conductance-mismatch sweeps
|
| 486 |
```
|
| 487 |
|
| 488 |
Because the transition is a fixed matrix stack proven equal to the gate-graph
|
|
|
|
| 527 |
within a fixed annealing schedule.
|
| 528 |
|
| 529 |
```bash
|
| 530 |
+
python src/automata.py build-attractor # compile a multiplier to variants/neural_attractor.safetensors
|
| 531 |
+
python src/automata.py attractor # forward eval, whole-network relaxation, factoring, SAT
|
| 532 |
```
|
| 533 |
|
| 534 |
---
|
|
|
|
| 568 |
closing the loop across two generations.
|
| 569 |
|
| 570 |
```bash
|
| 571 |
+
python src/host.py constructor8 build # emit variants/neural_subleq8io.safetensors
|
| 572 |
+
python src/host.py constructor8 verify # host soundness + codec round-trip + constructor on all 3 backends
|
| 573 |
+
python src/host.py constructor8 self # full self-reproduction on the threshold matrices, then gen-2 boot
|
| 574 |
```
|
| 575 |
|
| 576 |
A program running on the processor emits the exact tensor file that defines
|
|
|
|
| 581 |
`paper/` states and proves what this machine does, for a host serialized one
|
| 582 |
unit at a time: `variants/neural_subleq8io_netlist.safetensors` stores the bias
|
| 583 |
of every unit and every edge of the wiring, 17,598 units and 51,132 edges in
|
| 584 |
+
160,240 bytes, in the order `src/host.py` builds them, so the network the
|
| 585 |
file denotes is the network the paper analyses, and parsing the file rebuilds
|
| 586 |
it. That host is the member with eight-bit words of a family whose step has
|
| 587 |
depth 13 for every word width, with a carry-lookahead subtractor and the
|
| 588 |
+
operands fetched through the decode lines of the counter; `src/host.py`
|
| 589 |
+
builds any member and `python src/paper.py host-family` checks each against SUBLEQ
|
| 590 |
on the evaluator in C. Separating construction from copying
|
| 591 |
as von Neumann did, a 27-instruction program emits the serialization of the
|
| 592 |
host, its initial memory and its own description tape; each generation is
|
|
|
|
| 594 |
rebuilding the netlist from its first field, and building the evaluator from
|
| 595 |
that netlist. It also shows where the restriction to {-1, 0, 1} is paid for,
|
| 596 |
and that a fixed ternary network, the two-head machine of
|
| 597 |
+
`src/twohead.py`, holds a stored network in its state and evaluates one
|
| 598 |
record per step, which `rocq/` proves for every window of 2^α cells, bank and
|
| 599 |
+
tape. The host itself, compiled by `src/twohead.py` into 36,074 records of
|
| 600 |
two inputs with records that load its memory, its tape device and a hand-over,
|
| 601 |
and stored in the member with 4096 cells, runs the 27-instruction program on a
|
| 602 |
recipe for its own records: it writes them into the idle bank, copies the recipe
|
|
|
|
| 606 |
cells does the same with a description code of its own, every generation of it
|
| 607 |
proved in Rocq. Its robustness section gives the margin every orbit has
|
| 608 |
against perturbation, a certificate of the leakage an orbit tolerates from the
|
| 609 |
+
inputs active along it, and the amplified host of `src/noise.py`: every row
|
| 610 |
of the levelized host replaced by r rows reading all r copies of its inputs,
|
| 611 |
which multiplies every margin by r, reproduces itself exactly, fails only when two
|
| 612 |
wrong copies meet, with a probability of second order that the paper bounds, and
|
| 613 |
completes the self-reproducing run under read noise at levels where the host
|
| 614 |
fails within its first bytes.
|
| 615 |
|
| 616 |
+
Every figure of the paper comes from `src/paper.py`, `src/host.py`, `src/twohead.py` and
|
| 617 |
+
`src/noise.py`, with the C programs beside them, and their records are in `paper/runs/`;
|
| 618 |
+
`rocq/` holds the Rocq development, which `python src/paper.py rocq` builds.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 619 |
|
| 620 |
---
|
| 621 |
|
|
|
|
| 722 |
reversible program and the threshold substrate it runs on.
|
| 723 |
|
| 724 |
```bash
|
| 725 |
+
python src/automata.py reversible # reversible gates, Cuccaro ALU, Bennett construction
|
| 726 |
+
python src/automata.py reversible-cpu # bijective transition and backward execution
|
| 727 |
+
python src/automata.py reversible-prog # structured reversible programs (multiply, Fibonacci, Janus IF)
|
| 728 |
+
python src/automata.py build-reversible # ship + round-trip variants/neural_reversible.safetensors
|
| 729 |
+
python src/automata.py reversible-matrix# ternary matrix stack: permutation transition + 0.5 margin
|
| 730 |
```
|
| 731 |
|
| 732 |
---
|
|
|
|
| 762 |
functionally complete (Margolus 1984).
|
| 763 |
|
| 764 |
```bash
|
| 765 |
+
python src/automata.py ca # rule bijection, lattice reversibility, ballistic motion, interaction gate
|
| 766 |
+
python src/automata.py build-ca # ship the block rule as a ternary matrix tile (permutation + 0.5 margin)
|
| 767 |
```
|
| 768 |
|
| 769 |
---
|
|
|
|
| 795 |
decision from the gate.
|
| 796 |
|
| 797 |
```bash
|
| 798 |
+
python src/automata.py tile # binding gate, XOR/AND/OR rule tiles, binary counter
|
| 799 |
+
python src/automata.py build-tile # ship + regrow variants/neural_tile.safetensors
|
| 800 |
```
|
| 801 |
|
| 802 |
---
|
|
|
|
| 962 |
### Training
|
| 963 |
|
| 964 |
```bash
|
| 965 |
+
python llm_integration/llm.py train --mode router --epochs 100 # the router alone
|
| 966 |
+
python llm_integration/llm.py train --mode llm --epochs 100 --batch_size 256 # frozen LLM
|
| 967 |
+
python llm_integration/llm.py train --mode llm --unfreeze_layers 4 --batch_size 4096 # fine-tune top layers
|
| 968 |
```
|
| 969 |
|
| 970 |
Loss components: BCE on output bits, BCE on extracted A and B bits (2× weight), and CE on operation classification. Curriculum runs 0–9 → 0–99 → 0–255. Optimizer is AdamW, lr 3e-4, gradient clipping 1.0.
|
|
|
|
| 981 |
neural_subleq8io, neural_reflect,
|
| 982 |
neural_attractor, neural_reversible, neural_ca,
|
| 983 |
neural_tile)
|
| 984 |
+
src/ the library; `python src/<file>.py <command>`
|
| 985 |
+
├── build.py the generator (also `subleq`, `rv32`), `quantize`, and
|
| 986 |
+
│ `variants`, which builds, quantizes and scores every variant
|
| 987 |
+
├── eval.py the gate-level fitness suite, NetlistEvaluator, float oracles;
|
| 988 |
+
│ `variants` scores every variant, `cpu` runs the CPU programs
|
| 989 |
+
├── machines.py the SUBLEQ, RV32 and matrix machines: runtimes, references,
|
| 990 |
+
│ assemblers and suites (`subleq`, `rv32`, `rv32-c`, `matrix8`)
|
| 991 |
+
├── reflect.py neural_reflect: a threshold interpreter whose state holds its
|
| 992 |
+
│ own gate table; universality, quine and matrix suites
|
| 993 |
+
├── automata.py the attractor, reversible, cellular-automaton and tile machines,
|
| 994 |
+
│ their suites and their builders
|
| 995 |
+
├── host.py the SUBLEQ host: its family, serialization, device, the
|
| 996 |
+
│ constructor and the self-reproducing program, the evaluators,
|
| 997 |
+
│ and `constructor8`, the neural_subleq8io suite
|
| 998 |
+
├── twohead.py the two-head machine: the organism (`organism`), the netlist
|
| 999 |
+
│ N_{G,L} (`net`), and the host stored in it (`stored`)
|
| 1000 |
+
├── noise.py read noise and weight error on the host (`host`), the amplified
|
| 1001 |
+
│ host (`amplified`), and the neuromorphic datapath
|
| 1002 |
+
├── paper.py the paper's checks and runs, one command each
|
| 1003 |
+
├── independent.py code written from the paper's text alone
|
| 1004 |
+
├── netlist_eval.c an evaluator of the host family and the amplified host in C
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1005 |
├── twohead_lockstep.c the organism on N_{G,L} beside the transcription, in C
|
|
|
|
|
|
|
|
|
|
| 1006 |
├── stored_host_run.c the stored host's generations on the two-head machine, in C
|
| 1007 |
├── restore_bound.c the restoration bound of the amplified host along the orbit
|
| 1008 |
+
└── nsim.c the noise simulator of independent.py
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1009 |
paper/ the manuscript, and the record of every run it reports
|
| 1010 |
rocq/ Rocq proofs: the netlist of the two-head machine computes its
|
| 1011 |
step for every window of 2^α cells, the organism builds every
|
| 1012 |
image from any description of it and reproduces in every
|
| 1013 |
generation
|
| 1014 |
+
tools/play.py the playground, and a demonstration on each machine
|
| 1015 |
+
llm_integration/
|
| 1016 |
+
├── llm.py SmolLM2 with the frozen circuits: the circuit wrapper, the
|
| 1017 |
+
│ fitness, the extractors, training and the baseline
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1018 |
├── trained/ checkpointed extractor weights
|
| 1019 |
+
└── smollm2/ the architecture of SmolLM2 and its analysis
|
|
|
|
|
|
|
|
|
|
| 1020 |
```
|
| 1021 |
|
| 1022 |
---
|
demos/README.md
DELETED
|
@@ -1,22 +0,0 @@
|
|
| 1 |
-
# demos
|
| 2 |
-
|
| 3 |
-
Standalone programs that put each machine in the family to work on a named
|
| 4 |
-
task. Every script is self-contained (`python demos/<name>.py`), loads the
|
| 5 |
-
shipped weights, and cross-checks its result against an independent reference.
|
| 6 |
-
|
| 7 |
-
| Demo | Machine | What it does |
|
| 8 |
-
|------|---------|--------------|
|
| 9 |
-
| `neural_computer8_self_modifying_sieve.py` | `neural_computer8` | Sieve of Eratosthenes; with no indexed addressing, the program rewrites the address bytes of its own LOAD/STORE. Finds all 54 primes < 256. |
|
| 10 |
-
| `neural_computer8_euclid_gcd.py` | `neural_computer8` | Euclid's GCD, with `a mod b` built from the DIV/MUL/SUB opcodes and an XOR register swap. |
|
| 11 |
-
| `neural_computer8_collatz.py` | `neural_computer8` | Collatz step counts; exact for every seed whose trajectory stays inside the 8-bit range. |
|
| 12 |
-
| `neural_rv32_machin_pi.py` | `neural_rv32` | pi to nine digits by Machin's 1706 arctangent formula, printed to the console. |
|
| 13 |
-
| `neural_rv32_neural_nets_via_neur.py` | `neural_rv32` | Ternary neural nets run as NEUR software: one learned by gradient descent, one (XOR) compiled by construction. |
|
| 14 |
-
| `neural_matrix8_gpu_cpu_fleet.py` | `neural_matrix8` | 65,536 CPUs stepped in lockstep on the GPU as one batched matrix product. |
|
| 15 |
-
| `neural_attractor_factoring.py` | `neural_attractor` | Factors semiprimes by relaxing an 8x8 multiplier's energy backward; also divides. |
|
| 16 |
-
| `neural_attractor_nqueens.py` | `neural_attractor` | Solves 6- and 8-queens by relaxing toward zero unsatisfied clauses. |
|
| 17 |
-
| `neural_subleq8io_universal_constructor.py` | `neural_subleq8io` | The universal constructor fabricates a sibling machine byte-for-byte, which then boots. |
|
| 18 |
-
| `neural_reflect_self_modifying_stack.py` | `neural_reflect` | Self-modifying SUBLEQ code, on a stored machine, on the fixed interpreter: three levels in one tensor. |
|
| 19 |
-
| `neural_reversible_counterfactual.py` | `neural_reversible` | Bijective mixing, exact reverse recovery, and the counterfactual when a digest bit is flipped. |
|
| 20 |
-
| `neural_ca_loschmidt_echo.py` | `neural_ca` | A ~2,000-particle gas mixed 500 steps and un-mixed exactly; one flipped cell corrupts half the past. |
|
| 21 |
-
| `neural_ca_reversible_cipher.py` | `neural_ca` | The reversible automaton as a block cipher: exact decryption with the right key, noise with a wrong one. |
|
| 22 |
-
| `neural_tile_pascal_lucas.py` | `neural_tile` | Self-assembly grows Pascal's triangle mod 2; every cell verified against Lucas' theorem. |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
demos/neural_attractor_factoring.py
DELETED
|
@@ -1,55 +0,0 @@
|
|
| 1 |
-
"""neural_attractor -- factoring semiprimes by running a multiplier backward.
|
| 2 |
-
|
| 3 |
-
An 8x8 array multiplier compiles to a quadratic energy that is 0 exactly on its
|
| 4 |
-
truth table. Clamp the 16 product bits and relax the 16 input bits and the
|
| 5 |
-
network settles into a factorization: multiplication run in reverse. Clamp one
|
| 6 |
-
input as well and the same circuit performs division.
|
| 7 |
-
|
| 8 |
-
python demos/neural_attractor_factoring.py
|
| 9 |
-
"""
|
| 10 |
-
import os, sys, time
|
| 11 |
-
HERE = os.path.dirname(os.path.abspath(__file__))
|
| 12 |
-
REPO = os.path.dirname(HERE)
|
| 13 |
-
sys.path.insert(0, os.path.join(REPO, "src"))
|
| 14 |
-
from attractor import multiplier
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
if __name__ == "__main__":
|
| 18 |
-
c, io = multiplier(8)
|
| 19 |
-
print("neural_attractor: factoring by energy relaxation")
|
| 20 |
-
print("=" * 56)
|
| 21 |
-
print(f"8x8 array multiplier as an energy landscape: {len(c.gates)} gate "
|
| 22 |
-
f"gadgets, {c.n} wires, 16 free input bits\n")
|
| 23 |
-
|
| 24 |
-
ladder = [(3599, 59, 61), (9797, 97, 101), (32399, 179, 181), (57599, 239, 241)]
|
| 25 |
-
for N, pa, pb in ladder:
|
| 26 |
-
target = {io["prod"][k]: (N >> k) & 1 for k in range(16)}
|
| 27 |
-
t0, got = time.perf_counter(), None
|
| 28 |
-
for attempt in range(1, 8):
|
| 29 |
-
s = c.solve(io["xs"] + io["ys"], {io["zero"]: 0}, target,
|
| 30 |
-
sweeps=2500, restarts=12, seed=N * 7 + attempt)
|
| 31 |
-
if s is not None:
|
| 32 |
-
x = sum(s[io["xs"][k]] << k for k in range(8))
|
| 33 |
-
y = sum(s[io["ys"][k]] << k for k in range(8))
|
| 34 |
-
if x * y == N and x > 1 and y > 1:
|
| 35 |
-
got = (x, y, attempt); break
|
| 36 |
-
dt = time.perf_counter() - t0
|
| 37 |
-
if got:
|
| 38 |
-
x, y, att = got
|
| 39 |
-
tag = "the prime pair" if {x, y} == {pa, pb} else "valid factorization"
|
| 40 |
-
print(f" {N:5d} = {x:3d} x {y:3d} ({dt:4.1f}s, attempt {att}) [{tag}]")
|
| 41 |
-
else:
|
| 42 |
-
print(f" {N:5d}: ground state not reached in budget ({dt:.1f}s)")
|
| 43 |
-
|
| 44 |
-
# division: clamp one input and the product, relax the other input
|
| 45 |
-
N, xa = 57599, 241
|
| 46 |
-
fixed = {io["zero"]: 0}
|
| 47 |
-
for k in range(8):
|
| 48 |
-
fixed[io["xs"][k]] = (xa >> k) & 1
|
| 49 |
-
target = {io["prod"][k]: (N >> k) & 1 for k in range(16)}
|
| 50 |
-
t0 = time.perf_counter()
|
| 51 |
-
s = c.solve(io["ys"], fixed, target, sweeps=800, restarts=10, seed=7)
|
| 52 |
-
if s is not None:
|
| 53 |
-
y = sum(s[io["ys"][k]] << k for k in range(8))
|
| 54 |
-
print(f"\n division mode: clamp x=241 and the product -> y = {y} "
|
| 55 |
-
f"({time.perf_counter()-t0:.1f}s; {xa} x {y} = {xa * y})")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
demos/neural_attractor_nqueens.py
DELETED
|
@@ -1,90 +0,0 @@
|
|
| 1 |
-
"""neural_attractor -- the N-queens problem solved by energy relaxation.
|
| 2 |
-
|
| 3 |
-
The attractor computer has no clock and no program counter: a circuit compiles
|
| 4 |
-
to a quadratic energy whose global minimum is its consistent assignment, and
|
| 5 |
-
"running" it is rolling downhill. Here every N-queens constraint (one queen per
|
| 6 |
-
row, no two sharing a column or diagonal) becomes a clause wire that must be 1;
|
| 7 |
-
the annealer relaxes the board variables toward zero unsatisfied clauses. This
|
| 8 |
-
is the min-conflicts / MAX-SAT objective, expressed entirely in the family's
|
| 9 |
-
Heaviside threshold gadgets.
|
| 10 |
-
|
| 11 |
-
python demos/neural_attractor_nqueens.py
|
| 12 |
-
"""
|
| 13 |
-
import os, sys, time
|
| 14 |
-
HERE = os.path.dirname(os.path.abspath(__file__))
|
| 15 |
-
REPO = os.path.dirname(HERE)
|
| 16 |
-
sys.path.insert(0, os.path.join(REPO, "src"))
|
| 17 |
-
from attractor import Circuit
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
def queens_circuit(N):
|
| 21 |
-
"""Build the N-queens CNF as a threshold circuit; return every clause wire
|
| 22 |
-
so the solver can drive them all to 1 (a graded, per-clause objective)."""
|
| 23 |
-
c = Circuit()
|
| 24 |
-
v = {(r, col): c.wire() for r in range(N) for col in range(N)}
|
| 25 |
-
clauses = []
|
| 26 |
-
|
| 27 |
-
def OR_lits(lits):
|
| 28 |
-
acc = lits[0]
|
| 29 |
-
for w in lits[1:]:
|
| 30 |
-
acc = c.OR(acc, w)
|
| 31 |
-
return acc
|
| 32 |
-
|
| 33 |
-
for r in range(N): # each row: >= 1 queen
|
| 34 |
-
clauses.append(OR_lits([v[(r, col)] for col in range(N)]))
|
| 35 |
-
for c1 in range(N): # each row: <= 1 queen
|
| 36 |
-
for c2 in range(c1 + 1, N):
|
| 37 |
-
clauses.append(OR_lits([c.NOT(v[(r, c1)]), c.NOT(v[(r, c2)])]))
|
| 38 |
-
for col in range(N): # each column: <= 1
|
| 39 |
-
for r1 in range(N):
|
| 40 |
-
for r2 in range(r1 + 1, N):
|
| 41 |
-
clauses.append(OR_lits([c.NOT(v[(r1, col)]), c.NOT(v[(r2, col)])]))
|
| 42 |
-
cells = [(r, col) for r in range(N) for col in range(N)]
|
| 43 |
-
for i in range(len(cells)): # each diagonal: <= 1
|
| 44 |
-
for j in range(i + 1, len(cells)):
|
| 45 |
-
(r1, c1), (r2, c2) = cells[i], cells[j]
|
| 46 |
-
if r1 - c1 == r2 - c2 or r1 + c1 == r2 + c2:
|
| 47 |
-
clauses.append(OR_lits([c.NOT(v[(r1, c1)]), c.NOT(v[(r2, c2)])]))
|
| 48 |
-
return c, v, clauses
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
def valid(board, N):
|
| 52 |
-
qs = [rc for rc, b in board.items() if b]
|
| 53 |
-
if len(qs) != N:
|
| 54 |
-
return False
|
| 55 |
-
for i in range(len(qs)):
|
| 56 |
-
for j in range(i + 1, len(qs)):
|
| 57 |
-
(r1, c1), (r2, c2) = qs[i], qs[j]
|
| 58 |
-
if r1 == r2 or c1 == c2 or abs(r1 - r2) == abs(c1 - c2):
|
| 59 |
-
return False
|
| 60 |
-
return True
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
def solve_board(N, seeds=8):
|
| 64 |
-
c, v, clauses = queens_circuit(N)
|
| 65 |
-
free = list(v.values())
|
| 66 |
-
target = {w: 1 for w in clauses}
|
| 67 |
-
t0 = time.perf_counter()
|
| 68 |
-
for seed in range(seeds):
|
| 69 |
-
s = c.solve(free, {}, target, sweeps=6000, restarts=6, seed=seed)
|
| 70 |
-
if s is not None:
|
| 71 |
-
board = {rc: s[w] for rc, w in v.items()}
|
| 72 |
-
if valid(board, N):
|
| 73 |
-
return board, len(clauses), c.n, time.perf_counter() - t0
|
| 74 |
-
return None, len(clauses), c.n, time.perf_counter() - t0
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
if __name__ == "__main__":
|
| 78 |
-
print("neural_attractor: N-queens as energy relaxation")
|
| 79 |
-
print("=" * 56)
|
| 80 |
-
for N in (6, 8):
|
| 81 |
-
board, nclauses, nwires, dt = solve_board(N)
|
| 82 |
-
status = "SOLVED" if board else "no solution reached in budget"
|
| 83 |
-
print(f"\n{N}-queens: {nclauses} clause gadgets over {nwires} wires -> "
|
| 84 |
-
f"{status} ({dt:.1f}s)")
|
| 85 |
-
if board:
|
| 86 |
-
for r in range(N):
|
| 87 |
-
print(" " + " ".join("Q" if board[(r, col)] else "." for col in range(N)))
|
| 88 |
-
print(f" verified: {N} queens, no shared row/column/diagonal")
|
| 89 |
-
print("\nNo search loop was written: the constraints are an energy surface and")
|
| 90 |
-
print("the solution is its floor. The queens fall into place.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
demos/neural_ca_loschmidt_echo.py
DELETED
|
@@ -1,55 +0,0 @@
|
|
| 1 |
-
"""neural_ca -- a Loschmidt echo in a reversible cellular automaton.
|
| 2 |
-
|
| 3 |
-
The Margolus block rule is a bijection, so a gas of ~2,000 particles mixed for
|
| 4 |
-
500 steps can be un-mixed by iterating the same rule backward: the initial
|
| 5 |
-
configuration returns cell-for-cell (particle number conserved throughout). Yet
|
| 6 |
-
flipping a single cell of the mixed state before reversing corrupts roughly half
|
| 7 |
-
the reconstructed past -- exact reversibility and sensitive dependence in the
|
| 8 |
-
same automaton.
|
| 9 |
-
|
| 10 |
-
python demos/neural_ca_loschmidt_echo.py
|
| 11 |
-
"""
|
| 12 |
-
import os, sys, time, random, statistics
|
| 13 |
-
HERE = os.path.dirname(os.path.abspath(__file__))
|
| 14 |
-
REPO = os.path.dirname(HERE)
|
| 15 |
-
sys.path.insert(0, os.path.join(REPO, "src"))
|
| 16 |
-
import ca
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
def coarse(g, H, W, k=8):
|
| 20 |
-
out = []
|
| 21 |
-
for by in range(0, H, k):
|
| 22 |
-
for bx in range(0, W, k):
|
| 23 |
-
out.append(sum(g[y][x] for y in range(by, by + k) for x in range(bx, bx + k)))
|
| 24 |
-
return out
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
if __name__ == "__main__":
|
| 28 |
-
H = W = 64
|
| 29 |
-
rng = random.Random(2026)
|
| 30 |
-
grid = [[1 if rng.random() < 0.5 else 0 for _ in range(W)] for _ in range(H)]
|
| 31 |
-
n0 = sum(map(sum, grid))
|
| 32 |
-
STEPS = 500
|
| 33 |
-
|
| 34 |
-
print("neural_ca: Loschmidt echo (mix, then run time backward)")
|
| 35 |
-
print("=" * 56)
|
| 36 |
-
t0 = time.perf_counter()
|
| 37 |
-
fwd = ca.run(grid, STEPS, 0)
|
| 38 |
-
n1 = sum(map(sum, fwd))
|
| 39 |
-
back = ca.run_back(fwd, STEPS, 0)
|
| 40 |
-
echo = back == grid
|
| 41 |
-
dt = time.perf_counter() - t0
|
| 42 |
-
|
| 43 |
-
print(f"particles: {n0} at t=0, {n1} at t={STEPS} "
|
| 44 |
-
f"({'conserved' if n0 == n1 else 'NOT CONSERVED'})")
|
| 45 |
-
print(f"coarse 8x8 occupancy stdev: t=0 {statistics.pstdev(coarse(grid, H, W)):.2f} "
|
| 46 |
-
f"-> t={STEPS} {statistics.pstdev(coarse(fwd, H, W)):.2f} (mixed)")
|
| 47 |
-
print(f"{STEPS} steps forward + {STEPS} reversed in {dt:.1f}s: "
|
| 48 |
-
f"t=0 recovered {'EXACTLY' if echo else 'FAILED'}")
|
| 49 |
-
|
| 50 |
-
flip = [row[:] for row in fwd]
|
| 51 |
-
flip[0][0] ^= 1
|
| 52 |
-
back2 = ca.run_back(flip, STEPS, 0)
|
| 53 |
-
ham = sum(back2[y][x] != grid[y][x] for y in range(H) for x in range(W))
|
| 54 |
-
print(f"butterfly: flip ONE cell at t={STEPS}, reverse again -> reconstructed "
|
| 55 |
-
f"past wrong in {ham}/{H * W} cells")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
demos/neural_ca_reversible_cipher.py
DELETED
|
@@ -1,84 +0,0 @@
|
|
| 1 |
-
"""neural_ca -- a reversible cellular automaton as a perfect cipher.
|
| 2 |
-
|
| 3 |
-
The Margolus block rule is a bijection of the lattice, so iterating it forward
|
| 4 |
-
diffuses a bitmap into noise and iterating the exact same rule backward restores
|
| 5 |
-
it bit-for-bit. That makes the automaton a block cipher whose key is the pair
|
| 6 |
-
(number of steps, starting partition phase): the right key inverts the diffusion
|
| 7 |
-
perfectly, and a key off by a single step returns noise. No information is ever
|
| 8 |
-
destroyed, so decryption is exact rather than approximate.
|
| 9 |
-
|
| 10 |
-
python demos/neural_ca_reversible_cipher.py
|
| 11 |
-
"""
|
| 12 |
-
import os, sys
|
| 13 |
-
HERE = os.path.dirname(os.path.abspath(__file__))
|
| 14 |
-
REPO = os.path.dirname(HERE)
|
| 15 |
-
sys.path.insert(0, os.path.join(REPO, "src"))
|
| 16 |
-
import ca
|
| 17 |
-
|
| 18 |
-
# a recognizable plaintext bitmap (a heart), padded into a larger lattice
|
| 19 |
-
ART = [
|
| 20 |
-
" ### ### ",
|
| 21 |
-
" ##### ##### ",
|
| 22 |
-
"#############",
|
| 23 |
-
"#############",
|
| 24 |
-
"#############",
|
| 25 |
-
" ########### ",
|
| 26 |
-
" ######### ",
|
| 27 |
-
" ####### ",
|
| 28 |
-
" ##### ",
|
| 29 |
-
" ### ",
|
| 30 |
-
" # ",
|
| 31 |
-
]
|
| 32 |
-
PAD_X, PAD_Y = 6, 4
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
def make_grid():
|
| 36 |
-
# the Margolus partition tiles 2x2 blocks toroidally, so H and W must be even
|
| 37 |
-
h = len(ART) + 2 * PAD_Y
|
| 38 |
-
w = len(ART[0]) + 2 * PAD_X
|
| 39 |
-
h += h & 1
|
| 40 |
-
w += w & 1
|
| 41 |
-
g = [[0] * w for _ in range(h)]
|
| 42 |
-
for y, row in enumerate(ART):
|
| 43 |
-
for x, ch in enumerate(row):
|
| 44 |
-
if ch == "#":
|
| 45 |
-
g[y + PAD_Y][x + PAD_X] = 1
|
| 46 |
-
return g
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
def render(g):
|
| 50 |
-
return "\n".join("".join("#" if c else "." for c in row) for row in g)
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
def hamming(a, b):
|
| 54 |
-
return sum(a[y][x] != b[y][x] for y in range(len(a)) for x in range(len(a[0])))
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
if __name__ == "__main__":
|
| 58 |
-
plain = make_grid()
|
| 59 |
-
KEY_STEPS, KEY_PHASE = 200, 0
|
| 60 |
-
|
| 61 |
-
cipher = ca.run(plain, KEY_STEPS, KEY_PHASE)
|
| 62 |
-
recovered = ca.run_back(cipher, KEY_STEPS, KEY_PHASE)
|
| 63 |
-
wrong = ca.run_back(cipher, KEY_STEPS - 1, KEY_PHASE) # key off by one step
|
| 64 |
-
|
| 65 |
-
n = sum(map(sum, plain))
|
| 66 |
-
print("neural_ca: reversible-automaton cipher")
|
| 67 |
-
print("=" * 46)
|
| 68 |
-
print(f"key = ({KEY_STEPS} steps, phase {KEY_PHASE}); {n} set bits (conserved "
|
| 69 |
-
f"throughout: {sum(map(sum, cipher)) == n})\n")
|
| 70 |
-
|
| 71 |
-
print("PLAINTEXT:")
|
| 72 |
-
print(render(plain))
|
| 73 |
-
print(f"\nCIPHERTEXT after {KEY_STEPS} forward steps (diffused to noise):")
|
| 74 |
-
print(render(cipher))
|
| 75 |
-
print(f"\nDECRYPTED with the correct key:")
|
| 76 |
-
print(render(recovered))
|
| 77 |
-
print(f" exact recovery: {recovered == plain} (Hamming distance 0)")
|
| 78 |
-
print(f"\nDECRYPTED with a wrong key (off by one step):")
|
| 79 |
-
print(render(wrong))
|
| 80 |
-
print(f" still scrambled: {hamming(wrong, plain)} of {len(plain)*len(plain[0])} "
|
| 81 |
-
f"cells differ from the plaintext")
|
| 82 |
-
print("\nEvery step is a bijection, so the ciphertext holds exactly the")
|
| 83 |
-
print("information of the plaintext -- no more, no less. Only the exact")
|
| 84 |
-
print("reverse trajectory recovers it.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
demos/neural_computer8_collatz.py
DELETED
|
@@ -1,76 +0,0 @@
|
|
| 1 |
-
"""neural_computer8 -- the Collatz map iterated on the threshold CPU.
|
| 2 |
-
|
| 3 |
-
n -> n/2 if even, 3n+1 if odd, counting steps to 1. The parity test is a
|
| 4 |
-
bitwise AND with 1 that leaves the flags untouched, so the loop branches on the
|
| 5 |
-
comparison that precedes it. The register file is architecturally 8-bit, so the
|
| 6 |
-
machine computes the Collatz trajectory modulo 256; for seeds whose true
|
| 7 |
-
trajectory never exceeds 255, that is the exact Collatz step count, checked here
|
| 8 |
-
against a native reference.
|
| 9 |
-
|
| 10 |
-
python demos/neural_computer8_collatz.py
|
| 11 |
-
"""
|
| 12 |
-
import os, sys, time
|
| 13 |
-
HERE = os.path.dirname(os.path.abspath(__file__))
|
| 14 |
-
REPO = os.path.dirname(HERE)
|
| 15 |
-
sys.path.insert(0, os.path.join(REPO, "src"))
|
| 16 |
-
sys.path.insert(0, os.path.join(REPO, "tools"))
|
| 17 |
-
from safetensors import safe_open
|
| 18 |
-
from cpu_programs import Asm
|
| 19 |
-
from eval_all import GenericThresholdCPU, get_manifest
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
def build(n0):
|
| 23 |
-
a = Asm(1024)
|
| 24 |
-
a.load(0, "N"); a.load(1, "ONE"); a.xor(2, 2) # R0=n, R1=1, R2=steps
|
| 25 |
-
a.label("loop")
|
| 26 |
-
a.cmp(0, 1); a.jz("done") # n == 1 ?
|
| 27 |
-
a.xor(3, 3); a.add(3, 0); a.and_(3, 1) # R3 = n & 1 (flags preserved)
|
| 28 |
-
a.cmp(3, 1); a.jz("odd")
|
| 29 |
-
a.shr(0); a.jmp("step") # even: n >>= 1
|
| 30 |
-
a.label("odd")
|
| 31 |
-
a.xor(3, 3); a.add(3, 0); a.shl(3) # R3 = 2n
|
| 32 |
-
a.add(0, 3); a.add(0, 1) # n = 3n + 1
|
| 33 |
-
a.label("step")
|
| 34 |
-
a.add(2, 1); a.jmp("loop") # steps++
|
| 35 |
-
a.label("done")
|
| 36 |
-
a.store(2, "RES"); a.halt()
|
| 37 |
-
a.org(0x300); a.label("N"); a.db(n0)
|
| 38 |
-
a.label("ONE"); a.db(1); a.label("RES"); a.db(0)
|
| 39 |
-
return a, a.assemble()
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
def true_collatz(n):
|
| 43 |
-
peak, steps = n, 0
|
| 44 |
-
while n != 1:
|
| 45 |
-
n = n // 2 if n % 2 == 0 else 3 * n + 1
|
| 46 |
-
peak = max(peak, n); steps += 1
|
| 47 |
-
return steps, peak
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
if __name__ == "__main__":
|
| 51 |
-
tens = {}
|
| 52 |
-
with safe_open(os.path.join(REPO, "variants", "neural_computer8_small.safetensors"),
|
| 53 |
-
framework="pt") as f:
|
| 54 |
-
for name in f.keys():
|
| 55 |
-
tens[name] = f.get_tensor(name).float()
|
| 56 |
-
cpu = GenericThresholdCPU(tens)
|
| 57 |
-
addr = get_manifest(tens)["addr_bits"]
|
| 58 |
-
|
| 59 |
-
print("neural_computer8: Collatz step counts through the gates")
|
| 60 |
-
print("=" * 56)
|
| 61 |
-
print(" seed CPU steps true steps peak in 8-bit range?")
|
| 62 |
-
for n0 in (6, 7, 9, 18, 25, 45):
|
| 63 |
-
a, mem = build(n0)
|
| 64 |
-
state = {"pc": 0, "regs": [0] * 4, "flags": [0] * 4, "mem": list(mem),
|
| 65 |
-
"halted": False, "sp": (1 << addr) - 1}
|
| 66 |
-
t0 = time.perf_counter()
|
| 67 |
-
final, cycles = cpu.run(state, max_cycles=3000)
|
| 68 |
-
steps = final["mem"][a.labels["RES"]]
|
| 69 |
-
tsteps, peak = true_collatz(n0)
|
| 70 |
-
inrange = peak <= 255
|
| 71 |
-
match = "MATCH" if (inrange and steps == tsteps) else \
|
| 72 |
-
("mod-256" if not inrange else "MISMATCH")
|
| 73 |
-
print(f" {n0:4d} {steps:9d} {tsteps:10d} {peak:4d} "
|
| 74 |
-
f"{'yes' if inrange else 'no (wraps)':10s} [{match}]")
|
| 75 |
-
print("\n For every seed whose trajectory stays under 256 the threshold CPU")
|
| 76 |
-
print(" reproduces the exact Collatz step count.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
demos/neural_computer8_euclid_gcd.py
DELETED
|
@@ -1,63 +0,0 @@
|
|
| 1 |
-
"""neural_computer8 -- Euclid's algorithm on the threshold CPU.
|
| 2 |
-
|
| 3 |
-
The greatest common divisor by repeated remainder, where the remainder itself
|
| 4 |
-
is synthesized from the machine's own opcodes: a mod b = a - (a / b) * b, using
|
| 5 |
-
the DIV, MUL and SUB gates, and the register swap is three XORs (no temporary).
|
| 6 |
-
Every step runs through threshold neurons.
|
| 7 |
-
|
| 8 |
-
python demos/neural_computer8_euclid_gcd.py
|
| 9 |
-
"""
|
| 10 |
-
import os, sys, time, math
|
| 11 |
-
HERE = os.path.dirname(os.path.abspath(__file__))
|
| 12 |
-
REPO = os.path.dirname(HERE)
|
| 13 |
-
sys.path.insert(0, os.path.join(REPO, "src"))
|
| 14 |
-
sys.path.insert(0, os.path.join(REPO, "tools"))
|
| 15 |
-
from safetensors import safe_open
|
| 16 |
-
from cpu_programs import Asm
|
| 17 |
-
from eval_all import GenericThresholdCPU, get_manifest
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
def build(a0, b0):
|
| 21 |
-
g = Asm(1024)
|
| 22 |
-
g.load(0, "A") # R0 = a
|
| 23 |
-
g.load(1, "B") # R1 = b
|
| 24 |
-
g.xor(3, 3) # R3 = 0
|
| 25 |
-
g.label("loop")
|
| 26 |
-
g.cmp(1, 3); g.jz("done") # while b != 0
|
| 27 |
-
g.xor(2, 2); g.add(2, 0) # R2 = a
|
| 28 |
-
g._alu(0x8, 2, 1) # R2 = a / b (DIV opcode)
|
| 29 |
-
g.mul(2, 1) # R2 = (a/b)*b
|
| 30 |
-
g.sub(0, 2) # R0 = a mod b
|
| 31 |
-
g.xor(0, 1); g.xor(1, 0); g.xor(0, 1) # swap -> (b, a mod b)
|
| 32 |
-
g.jmp("loop")
|
| 33 |
-
g.label("done")
|
| 34 |
-
g.store(0, "RES"); g.halt()
|
| 35 |
-
g.org(0x300)
|
| 36 |
-
g.label("A"); g.db(a0)
|
| 37 |
-
g.label("B"); g.db(b0)
|
| 38 |
-
g.label("RES"); g.db(0)
|
| 39 |
-
return g, g.assemble()
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
if __name__ == "__main__":
|
| 43 |
-
tens = {}
|
| 44 |
-
with safe_open(os.path.join(REPO, "variants", "neural_computer8_small.safetensors"),
|
| 45 |
-
framework="pt") as f:
|
| 46 |
-
for name in f.keys():
|
| 47 |
-
tens[name] = f.get_tensor(name).float()
|
| 48 |
-
cpu = GenericThresholdCPU(tens)
|
| 49 |
-
addr = get_manifest(tens)["addr_bits"]
|
| 50 |
-
|
| 51 |
-
print("neural_computer8: Euclid's GCD through the gates")
|
| 52 |
-
print("=" * 56)
|
| 53 |
-
for a0, b0 in [(84, 36), (120, 90), (221, 34), (255, 128)]:
|
| 54 |
-
g, mem = build(a0, b0)
|
| 55 |
-
state = {"pc": 0, "regs": [0] * 4, "flags": [0] * 4, "mem": list(mem),
|
| 56 |
-
"halted": False, "sp": (1 << addr) - 1}
|
| 57 |
-
t0 = time.perf_counter()
|
| 58 |
-
final, cycles = cpu.run(state, max_cycles=400)
|
| 59 |
-
res = final["mem"][g.labels["RES"]]
|
| 60 |
-
ok = res == math.gcd(a0, b0)
|
| 61 |
-
print(f" gcd({a0:3d}, {b0:3d}) = {res:3d} ({cycles:3d} cycles, "
|
| 62 |
-
f"{time.perf_counter()-t0:.1f}s) "
|
| 63 |
-
f"[{'MATCH' if ok else 'MISMATCH vs ' + str(math.gcd(a0, b0))}]")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
demos/neural_computer8_self_modifying_sieve.py
DELETED
|
@@ -1,76 +0,0 @@
|
|
| 1 |
-
"""neural_computer8 -- the Sieve of Eratosthenes with self-modifying code.
|
| 2 |
-
|
| 3 |
-
The threshold CPU's ISA has no indexed addressing, so the program does exactly
|
| 4 |
-
what memory-constrained 1970s code did: it rewrites the address bytes of its own
|
| 5 |
-
LOAD and STORE instructions before each access, walking a pointer across the
|
| 6 |
-
flags array. Every gate that fetches, decodes, marks, and patches is a threshold
|
| 7 |
-
neuron. It halts with all 54 primes below 256 marked.
|
| 8 |
-
|
| 9 |
-
python demos/neural_computer8_self_modifying_sieve.py
|
| 10 |
-
"""
|
| 11 |
-
import os, sys, time
|
| 12 |
-
HERE = os.path.dirname(os.path.abspath(__file__))
|
| 13 |
-
REPO = os.path.dirname(HERE)
|
| 14 |
-
sys.path.insert(0, os.path.join(REPO, "src"))
|
| 15 |
-
sys.path.insert(0, os.path.join(REPO, "tools"))
|
| 16 |
-
from safetensors import safe_open
|
| 17 |
-
from cpu_programs import Asm, _enc
|
| 18 |
-
from eval_all import GenericThresholdCPU, get_manifest
|
| 19 |
-
|
| 20 |
-
FLAGS = 0x300 # flags[0..255] at 0x300..0x3FF
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
def build():
|
| 24 |
-
g = Asm(1024)
|
| 25 |
-
g.load(3, "ONE") # R3 = 1 (mark value / increment)
|
| 26 |
-
g.load(0, "TWO") # R0 = p = 2
|
| 27 |
-
g.label("outer")
|
| 28 |
-
g.store(0, "Rlo") # patch probe address low byte <- p
|
| 29 |
-
g.dw(_enc(0xA, 2, 0)) # probe: R2 = flags[p] (self-modified)
|
| 30 |
-
g.db(FLAGS >> 8); g.label("Rlo"); g.db(0x00)
|
| 31 |
-
g.cmp(2, 3); g.jz("next_p") # flags[p] == 1 -> composite, skip
|
| 32 |
-
g.xor(1, 1); g.add(1, 0); g.add(1, 0) # m = 2p
|
| 33 |
-
g.label("inner")
|
| 34 |
-
g.store(1, "Wlo") # patch mark address low byte <- m
|
| 35 |
-
g.dw(_enc(0xB, 0, 3)) # mark: flags[m] = R3 = 1 (self-modified)
|
| 36 |
-
g.db(FLAGS >> 8); g.label("Wlo"); g.db(0x00)
|
| 37 |
-
g.add(1, 0) # m += p (carry iff we passed 255)
|
| 38 |
-
g.jnc("inner")
|
| 39 |
-
g.label("next_p")
|
| 40 |
-
g.add(0, 3) # p += 1
|
| 41 |
-
g.load(2, "SIXTEEN")
|
| 42 |
-
g.cmp(0, 2)
|
| 43 |
-
g.jnz("outer") # loop until p == 16 (16^2 > 255)
|
| 44 |
-
g.halt()
|
| 45 |
-
g.org(0x200)
|
| 46 |
-
g.label("ONE"); g.db(1)
|
| 47 |
-
g.label("TWO"); g.db(2)
|
| 48 |
-
g.label("SIXTEEN"); g.db(16)
|
| 49 |
-
return g, g.assemble()
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
if __name__ == "__main__":
|
| 53 |
-
g, mem = build()
|
| 54 |
-
tens = {}
|
| 55 |
-
with safe_open(os.path.join(REPO, "variants", "neural_computer8_small.safetensors"),
|
| 56 |
-
framework="pt") as f:
|
| 57 |
-
for name in f.keys():
|
| 58 |
-
tens[name] = f.get_tensor(name).float()
|
| 59 |
-
cpu = GenericThresholdCPU(tens)
|
| 60 |
-
state = {"pc": 0, "regs": [0] * 4, "flags": [0] * 4, "mem": list(mem),
|
| 61 |
-
"halted": False, "sp": (1 << get_manifest(tens)["addr_bits"]) - 1}
|
| 62 |
-
t0 = time.perf_counter()
|
| 63 |
-
final, cycles = cpu.run(state, max_cycles=4000)
|
| 64 |
-
dt = time.perf_counter() - t0
|
| 65 |
-
|
| 66 |
-
got = [n for n in range(2, 256) if final["mem"][FLAGS + n] == 0]
|
| 67 |
-
ref = [n for n in range(2, 256) if all(n % d for d in range(2, int(n**0.5) + 1))]
|
| 68 |
-
print("neural_computer8: self-modifying Sieve of Eratosthenes")
|
| 69 |
-
print("=" * 56)
|
| 70 |
-
print(f"halted={final['halted']} {cycles} cycles through the gates ({dt:.0f}s)")
|
| 71 |
-
print(f"primes < 256 found: {len(got)} native sieve: {len(ref)} "
|
| 72 |
-
f"{'EXACT MATCH' if got == ref else 'MISMATCH'}")
|
| 73 |
-
print(" " + " ".join(map(str, got)))
|
| 74 |
-
print(f"self-modified operand bytes at halt: probe={final['mem'][g.labels['Rlo']]}, "
|
| 75 |
-
f"mark={final['mem'][g.labels['Wlo']]} (the program rewrote its own "
|
| 76 |
-
f"instruction stream as it ran)")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
demos/neural_matrix8_gpu_cpu_fleet.py
DELETED
|
@@ -1,73 +0,0 @@
|
|
| 1 |
-
"""neural_matrix8 -- a fleet of processors as one batched matrix product.
|
| 2 |
-
|
| 3 |
-
The whole CPU is compiled to 108 ternary weight matrices with a Heaviside step
|
| 4 |
-
between them, so one clock cycle is one matrix-vector product plus a threshold.
|
| 5 |
-
Add a batch dimension and N processors are the same 108 matmuls: here 65,536
|
| 6 |
-
independent CPUs step in lockstep on the GPU. A 256-wide correctness fleet
|
| 7 |
-
confirms every CPU halts exactly at cycle 2n+1 for its own countdown input.
|
| 8 |
-
|
| 9 |
-
python demos/neural_matrix8_gpu_cpu_fleet.py
|
| 10 |
-
"""
|
| 11 |
-
import os, sys, time
|
| 12 |
-
HERE = os.path.dirname(os.path.abspath(__file__))
|
| 13 |
-
REPO = os.path.dirname(HERE)
|
| 14 |
-
sys.path.insert(0, os.path.join(REPO, "src"))
|
| 15 |
-
import torch
|
| 16 |
-
from matrix8 import MatrixMachine, state_to_vec, _mk_state, _instr
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
def prog_bytes():
|
| 20 |
-
return (_instr(0x1, 0, 1) # SUB R0, R1
|
| 21 |
-
+ _instr(0xD, 0, 0, 1) + [0, 0] # JNZ 0x0000 (address word)
|
| 22 |
-
+ _instr(0xF) # HALT
|
| 23 |
-
+ [0] * 8)
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
if __name__ == "__main__":
|
| 27 |
-
dev = "cuda" if torch.cuda.is_available() else "cpu"
|
| 28 |
-
mm = MatrixMachine.from_file(device=dev)
|
| 29 |
-
n_neurons = sum(int(b.numel()) for b in mm.B)
|
| 30 |
-
name = torch.cuda.get_device_name(0) if dev == "cuda" else "cpu"
|
| 31 |
-
print("neural_matrix8: 65,536 CPUs as one batched matrix product")
|
| 32 |
-
print("=" * 56)
|
| 33 |
-
print(f"loaded {len(mm.W)} ternary matrices, {n_neurons:,} threshold neurons "
|
| 34 |
-
f"per transition, device={dev} ({name})")
|
| 35 |
-
mem = prog_bytes()
|
| 36 |
-
|
| 37 |
-
# correctness fleet: one CPU per 8-bit input, all 256 at once
|
| 38 |
-
V = torch.stack([state_to_vec(_mk_state(mem=mem, regs=(x, 1, 0, 0)))
|
| 39 |
-
for x in range(256)]).to(dev)
|
| 40 |
-
first_halt = torch.full((256,), -1, dtype=torch.long)
|
| 41 |
-
step = 0
|
| 42 |
-
while step < 600:
|
| 43 |
-
halted = V[:, MatrixMachine.HALT_IDX] > 0.5
|
| 44 |
-
first_halt[(first_halt < 0) & halted.cpu()] = step
|
| 45 |
-
if bool(halted.all()):
|
| 46 |
-
break
|
| 47 |
-
V = mm.step(V); step += 1
|
| 48 |
-
expect = torch.tensor([2 * (x if x else 256) + 1 for x in range(256)])
|
| 49 |
-
ok_halt = bool((first_halt == expect).all())
|
| 50 |
-
ok_r0 = bool((V[:, 4:12].cpu().long().sum(1) == 0).all())
|
| 51 |
-
print(f"\ncorrectness fleet of 256: every CPU halts at cycle 2n+1 for its own "
|
| 52 |
-
f"input: {'EXACT' if ok_halt else 'MISMATCH'}; all results R0==0: {ok_r0}")
|
| 53 |
-
|
| 54 |
-
# throughput fleet: 65,536 CPUs
|
| 55 |
-
B = 65536
|
| 56 |
-
V = torch.stack([state_to_vec(_mk_state(mem=mem, regs=(x & 0xFF, 1, 0, 0)))
|
| 57 |
-
for x in range(256)]).repeat(256, 1).to(dev)
|
| 58 |
-
if dev == "cuda":
|
| 59 |
-
torch.cuda.synchronize()
|
| 60 |
-
t0, steps = time.perf_counter(), 0
|
| 61 |
-
while steps < 520:
|
| 62 |
-
V = mm.step(V); steps += 1
|
| 63 |
-
if steps % 64 == 0 and bool((V[:, MatrixMachine.HALT_IDX] > 0.5).all()):
|
| 64 |
-
break
|
| 65 |
-
if dev == "cuda":
|
| 66 |
-
torch.cuda.synchronize()
|
| 67 |
-
dt = time.perf_counter() - t0
|
| 68 |
-
instr = B * steps
|
| 69 |
-
print(f"\nthroughput fleet of {B:,}: {steps} transitions in {dt:.1f}s")
|
| 70 |
-
print(f" {instr / dt / 1e6:,.1f} million instructions/s aggregate "
|
| 71 |
-
f"({instr:,} instructions retired)")
|
| 72 |
-
print(f" {instr * n_neurons / dt / 1e9:,.1f} billion threshold-neuron "
|
| 73 |
-
f"evaluations/s")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
demos/neural_reflect_self_modifying_stack.py
DELETED
|
@@ -1,87 +0,0 @@
|
|
| 1 |
-
"""neural_reflect -- three levels of machine in one tensor.
|
| 2 |
-
|
| 3 |
-
Level 0 is U, a fixed ternary threshold interpreter (~24k gates). Level 1 is a
|
| 4 |
-
complete SUBLEQ computer stored as data inside U's writable state. Level 2 is a
|
| 5 |
-
SUBLEQ program that rewrites its own operand bytes as it runs: a zeroing
|
| 6 |
-
instruction that walks itself across memory, erasing a block. The interpreter,
|
| 7 |
-
the machine it runs, and the program that machine executes are all the same
|
| 8 |
-
tensor; the final memory is checked bit-for-bit against a native SUBLEQ emulator.
|
| 9 |
-
|
| 10 |
-
python demos/neural_reflect_self_modifying_stack.py
|
| 11 |
-
"""
|
| 12 |
-
import os, sys, time
|
| 13 |
-
HERE = os.path.dirname(os.path.abspath(__file__))
|
| 14 |
-
REPO = os.path.dirname(HERE)
|
| 15 |
-
sys.path.insert(0, os.path.join(REPO, "src"))
|
| 16 |
-
import torch
|
| 17 |
-
from reflect import (Cfg, build_subleq_machine, build_net, Leveled,
|
| 18 |
-
encode_netlist, state_to_vec, subleq32_step, MEM, MEMB)
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
if __name__ == "__main__":
|
| 22 |
-
dev = "cuda" if torch.cuda.is_available() else "cpu"
|
| 23 |
-
cfg = Cfg(A=14, G=296, banks=1, self_mod=False)
|
| 24 |
-
prog, lay = build_subleq_machine(cfg)
|
| 25 |
-
inet, ii, io = build_net(cfg)
|
| 26 |
-
print("neural_reflect: self-modifying code on a stored machine on U")
|
| 27 |
-
print("=" * 60)
|
| 28 |
-
print(f"level 0: interpreter U = {len(inet.gates):,} threshold gates (fixed)")
|
| 29 |
-
print(f"level 1: a SUBLEQ machine = {len(prog)} gates, stored as writable data")
|
| 30 |
-
lev = Leveled(inet, ii, io, device=dev)
|
| 31 |
-
nl = encode_netlist(cfg, prog)
|
| 32 |
-
sig0 = [0] * cfg.S
|
| 33 |
-
sig0[cfg.NET0:cfg.NET0 + len(nl)] = nl
|
| 34 |
-
base = state_to_vec(cfg, {"sig": sig0, "gp": 0, "halt": 0}).to(dev)
|
| 35 |
-
PC = lay["PC"]
|
| 36 |
-
|
| 37 |
-
# level 2: SUBLEQ (A B C: M[B]-=M[A]; if <=0 goto C). The first instruction
|
| 38 |
-
# zeroes a target cell, then two instructions increment its own A and B
|
| 39 |
-
# operand bytes, so the zeroing walks across a data block.
|
| 40 |
-
pm = [0] * 32
|
| 41 |
-
pm[0:3] = [24, 24, 3] # zero M[<target>]; these operands get rewritten
|
| 42 |
-
pm[3:6] = [19, 0, 6] # M[0] += 1 (patch own A-field: cell 0)
|
| 43 |
-
pm[6:9] = [19, 1, 9] # M[1] += 1 (patch own B-field: cell 1)
|
| 44 |
-
pm[9:12] = [20, 21, 15] # count -= 1; if <= 0 branch to epilogue
|
| 45 |
-
pm[12:15] = [18, 18, 0] # 0 -> unconditional jump to loop head
|
| 46 |
-
pm[15:18] = [18, 18, 31] # epilogue: branch to 31 = HALT
|
| 47 |
-
pm[18], pm[19], pm[20], pm[21] = 0, 255, 1, 4
|
| 48 |
-
pm[24:28] = [7, 99, 123, 200] # the block the program will erase
|
| 49 |
-
print(f"level 2: a program whose instruction 0 starts as [24,24,3] and "
|
| 50 |
-
f"rewrites its own operands")
|
| 51 |
-
|
| 52 |
-
ref_mem, ref_pc = list(pm), 0
|
| 53 |
-
while ref_pc != 31:
|
| 54 |
-
ref_mem, ref_pc = subleq32_step(ref_mem, ref_pc)
|
| 55 |
-
|
| 56 |
-
V = base.unsqueeze(0).clone()
|
| 57 |
-
for i in range(32):
|
| 58 |
-
for b in range(8):
|
| 59 |
-
V[:, MEM + b * MEMB + i] = float((pm[i] >> b) & 1)
|
| 60 |
-
|
| 61 |
-
def get_mem(Vc):
|
| 62 |
-
return sum(Vc[:, MEM + b * MEMB: MEM + b * MEMB + 32].long() << b
|
| 63 |
-
for b in range(8))[0].tolist()
|
| 64 |
-
|
| 65 |
-
t0, walk, steps, halted = time.perf_counter(), [], 0, False
|
| 66 |
-
for _ in range(26):
|
| 67 |
-
m_now = get_mem(V.cpu())
|
| 68 |
-
walk.append((m_now[0], m_now[1]))
|
| 69 |
-
for _ in range(cfg.G):
|
| 70 |
-
V = lev.step(V)
|
| 71 |
-
steps += 1
|
| 72 |
-
if int(V[0, cfg.S + cfg.GPW]) == 1:
|
| 73 |
-
halted = True
|
| 74 |
-
break
|
| 75 |
-
dt = time.perf_counter() - t0
|
| 76 |
-
got = get_mem(V.cpu())
|
| 77 |
-
|
| 78 |
-
mut = " -> ".join(f"[{a},{b}]" for a, b in
|
| 79 |
-
[walk[0]] + [w for i, w in enumerate(walk[1:], 1) if w != walk[i - 1]])
|
| 80 |
-
print(f"\nran {steps} hosted instructions in {dt:.0f}s "
|
| 81 |
-
f"({steps * cfg.G} interpreter recurrences); halted={halted}")
|
| 82 |
-
print(f" instruction 0's operand cells as it ran: {mut}")
|
| 83 |
-
print(f" target block M[24..27]: {pm[24:28]} -> {got[24:28]}")
|
| 84 |
-
print(f" final memory == native SUBLEQ reference: "
|
| 85 |
-
f"{'EXACT' if got == ref_mem else 'MISMATCH'}")
|
| 86 |
-
print("\n The program, the machine it runs on, and the machine that runs")
|
| 87 |
-
print(" that machine are all the same tensor.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
demos/neural_reversible_counterfactual.py
DELETED
|
@@ -1,49 +0,0 @@
|
|
| 1 |
-
"""neural_reversible -- computation with no erasure, and the counterfactual.
|
| 2 |
-
|
| 3 |
-
Every instruction of this register machine is a bijection, so the same weights
|
| 4 |
-
run backward invert the forward computation: a mixing program hashes a seed into
|
| 5 |
-
a digest, and stepping in reverse reconstructs the seed exactly, with no bit ever
|
| 6 |
-
erased (Landauer floor: zero). Because history is only consistent with the truth,
|
| 7 |
-
flipping a single bit of the digest and running backward yields a "past" that is
|
| 8 |
-
wrong in many bits: information is conserved, so a corrupted future refuses to
|
| 9 |
-
invert to the real input.
|
| 10 |
-
|
| 11 |
-
python demos/neural_reversible_counterfactual.py
|
| 12 |
-
"""
|
| 13 |
-
import os, sys
|
| 14 |
-
HERE = os.path.dirname(os.path.abspath(__file__))
|
| 15 |
-
REPO = os.path.dirname(HERE)
|
| 16 |
-
sys.path.insert(0, os.path.join(REPO, "src"))
|
| 17 |
-
from reversible_cpu import RCPU, _clone
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
if __name__ == "__main__":
|
| 21 |
-
# cross-coupled mixing: R0 feeds R3 and R3 feeds R0, so the avalanche spreads
|
| 22 |
-
prog = []
|
| 23 |
-
for _ in range(8):
|
| 24 |
-
prog += [("ADD", 0, 3), ("TOFF", 3, 0, 2), ("ROL", 0, 1), ("XOR", 2, 3)]
|
| 25 |
-
cpu = RCPU(prog)
|
| 26 |
-
seed = cpu.new_state(regs=[7, 13, 201, 0])
|
| 27 |
-
|
| 28 |
-
print("neural_reversible: no-erasure computing and the counterfactual")
|
| 29 |
-
print("=" * 60)
|
| 30 |
-
s = _clone(seed)
|
| 31 |
-
cpu.run(s, len(prog))
|
| 32 |
-
print(f"seed registers {seed['R']} --{len(prog)} bijective steps--> "
|
| 33 |
-
f"digest {s['R']}")
|
| 34 |
-
|
| 35 |
-
b = _clone(s)
|
| 36 |
-
cpu.run_back(b, len(prog))
|
| 37 |
-
print(f"run backward: seed recovered "
|
| 38 |
-
f"{'EXACTLY' if b['R'] == seed['R'] and b['PC'] == seed['PC'] else 'FAILED'} "
|
| 39 |
-
f"(no bit was ever erased; Landauer floor 0)")
|
| 40 |
-
|
| 41 |
-
c = _clone(s)
|
| 42 |
-
c["R"][3] ^= 4 # corrupt one bit of the digest
|
| 43 |
-
cpu.run_back(c, len(prog))
|
| 44 |
-
diff = sum(bin(x ^ y).count("1") for x, y in zip(c["R"], seed["R"]))
|
| 45 |
-
print(f"counterfactual: flip ONE digest bit, run history backward -> the "
|
| 46 |
-
f"reconstructed 'seed' is wrong in {diff}/32 register bits")
|
| 47 |
-
print(f" corrupted 'seed' = {c['R']} true seed = {seed['R']}")
|
| 48 |
-
print("\n History inverts only when it is the true history. A tampered")
|
| 49 |
-
print(" future does not lead back to the real past.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
demos/neural_rv32_machin_pi.py
DELETED
|
@@ -1,85 +0,0 @@
|
|
| 1 |
-
"""neural_rv32 -- pi by Machin's formula, computed on the RISC-V threshold CPU.
|
| 2 |
-
|
| 3 |
-
pi = 16*arctan(1/5) - 4*arctan(1/239) (John Machin, 1706), evaluated as an
|
| 4 |
-
integer arctangent series at scale 1e8 with rounded divisions. Every add,
|
| 5 |
-
multiply and divide runs through the ternary threshold datapath; 239^2 is
|
| 6 |
-
computed on-machine, and the result is formatted to the memory-mapped console.
|
| 7 |
-
|
| 8 |
-
python demos/neural_rv32_machin_pi.py
|
| 9 |
-
"""
|
| 10 |
-
import os, sys, time
|
| 11 |
-
HERE = os.path.dirname(os.path.abspath(__file__))
|
| 12 |
-
REPO = os.path.dirname(HERE)
|
| 13 |
-
sys.path.insert(0, os.path.join(REPO, "src"))
|
| 14 |
-
import torch
|
| 15 |
-
from machines import Asm, Rv32ThresholdCPU, rv_state
|
| 16 |
-
from eval_all import int_to_bits
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
def native():
|
| 20 |
-
"""Integer mirror of the exact recurrence the machine will run."""
|
| 21 |
-
accA, t, sign, d, dsA = 0, 1600000000 // 5, 1, 1, []
|
| 22 |
-
while t > 0:
|
| 23 |
-
accA += sign * ((t + d // 2) // d); dsA.append(d)
|
| 24 |
-
sign, d, t = -sign, d + 2, (t + 12) // 25
|
| 25 |
-
accB, t, q, dsB, sign, d = 0, (400000000 + 119) // 239, 239 * 239, [], 1, 1
|
| 26 |
-
while t > 0:
|
| 27 |
-
accB += sign * ((t + d // 2) // d); dsB.append(d)
|
| 28 |
-
sign, d, t = -sign, d + 2, (t + q // 2) // q
|
| 29 |
-
return accA - accB, dsA, dsB
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
def build(dsA, dsB):
|
| 33 |
-
a = Asm()
|
| 34 |
-
a.lui(4, 0x5F5E1) # x4 = 1,600,000,000
|
| 35 |
-
a.addi(5, 0, 25); a.addi(6, 0, 10); a.addi(8, 0, 239); a.addi(14, 0, 5)
|
| 36 |
-
a.div(11, 4, 14) # t = 16e8/5
|
| 37 |
-
a.addi(10, 0, 0) # accA
|
| 38 |
-
for k, d in enumerate(dsA):
|
| 39 |
-
a.addi(12, 0, d); a.addi(13, 11, d // 2); a.div(13, 13, 12)
|
| 40 |
-
(a.add if k % 2 == 0 else a.sub)(10, 10, 13)
|
| 41 |
-
if k + 1 < len(dsA):
|
| 42 |
-
a.addi(13, 11, 12); a.div(11, 13, 5)
|
| 43 |
-
a.lui(7, 0x17D78); a.ori(7, 7, 0x400) # x7 = 400,000,000
|
| 44 |
-
a.addi(13, 7, 119); a.div(11, 13, 8)
|
| 45 |
-
a.mul(9, 8, 8) # 239^2 computed on-machine
|
| 46 |
-
a.srli(14, 9, 1)
|
| 47 |
-
a.addi(15, 0, 0) # accB
|
| 48 |
-
for k, d in enumerate(dsB):
|
| 49 |
-
a.addi(12, 0, d); a.addi(13, 11, d // 2); a.div(13, 13, 12)
|
| 50 |
-
(a.add if k % 2 == 0 else a.sub)(15, 15, 13)
|
| 51 |
-
if k + 1 < len(dsB):
|
| 52 |
-
a.add(13, 11, 14); a.div(11, 13, 9)
|
| 53 |
-
a.sub(10, 10, 15) # x10 = pi * 1e8
|
| 54 |
-
a.addi(20, 0, 0x7F8); a.slli(20, 20, 5) # console 0xFF00
|
| 55 |
-
a.lui(21, 0x5F5E); a.ori(21, 21, 0x100) # P = 1e8
|
| 56 |
-
a.div(22, 10, 21); a.rem(10, 10, 21)
|
| 57 |
-
a.addi(22, 22, 48); a.sb(22, 20, 0) # integer digit
|
| 58 |
-
a.addi(22, 0, 46); a.sb(22, 20, 0) # '.'
|
| 59 |
-
a.div(21, 21, 6); a.addi(23, 0, 8)
|
| 60 |
-
a.label("digits")
|
| 61 |
-
a.div(22, 10, 21); a.rem(10, 10, 21)
|
| 62 |
-
a.addi(22, 22, 48); a.sb(22, 20, 0)
|
| 63 |
-
a.div(21, 21, 6); a.addi(23, 23, -1); a.bne(23, 0, "digits")
|
| 64 |
-
a.ecall()
|
| 65 |
-
return a.assemble()
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
if __name__ == "__main__":
|
| 69 |
-
pi9, dsA, dsB = native()
|
| 70 |
-
mem = build(dsA, dsB)
|
| 71 |
-
print("neural_rv32: Machin's pi on the threshold datapath")
|
| 72 |
-
print("=" * 56)
|
| 73 |
-
print("loading neural_rv32 (8.77M ternary parameters)...")
|
| 74 |
-
cpu = Rv32ThresholdCPU()
|
| 75 |
-
ts = rv_state(mem)
|
| 76 |
-
ts["_mem_bits"] = torch.tensor([int_to_bits(b, 8) for b in ts["mem"]], dtype=torch.float32)
|
| 77 |
-
t0, cyc = time.perf_counter(), 0
|
| 78 |
-
while not ts["halted"] and cyc < 400:
|
| 79 |
-
ts = cpu.step(ts); cyc += 1
|
| 80 |
-
want = f"{pi9 // 10**8}.{pi9 % 10**8:08d}"
|
| 81 |
-
print(f"console output : {ts['console']!r} ({cyc} cycles, "
|
| 82 |
-
f"{time.perf_counter()-t0:.0f}s, dual-issue pairs {cpu.pairs_issued})")
|
| 83 |
-
print(f"integer mirror : {want!r} (machine {'EXACT' if ts['console']==want else 'MISMATCH'})")
|
| 84 |
-
print(f"true pi : 3.14159265(35...) -> all printed digits "
|
| 85 |
-
f"{'correct' if ts['console']=='3.14159265' else 'NOT all correct'}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
demos/neural_rv32_neural_nets_via_neur.py
DELETED
|
@@ -1,184 +0,0 @@
|
|
| 1 |
-
"""neural_rv32 -- neural networks running as software on the neural-net CPU.
|
| 2 |
-
|
| 3 |
-
neural_rv32's NEUR opcode computes exactly one ternary threshold neuron:
|
| 4 |
-
rd = H( popcount(rs1 & pos) - popcount(rs1 & neg) + sext5(bias) )
|
| 5 |
-
with weights in {-1,0,+1} and an integer bias. So a program of NEUR
|
| 6 |
-
instructions IS a ternary neural network -- and the CPU executing it is itself
|
| 7 |
-
a threshold network. One net here is LEARNED by gradient descent (a nonlinear
|
| 8 |
-
popcount band) and one is COMPILED by construction (8-bit parity, the function
|
| 9 |
-
that ended the single perceptron); both are then deployed as NEUR code and run
|
| 10 |
-
on the threshold datapath, with the CPU output checked against the reference net.
|
| 11 |
-
|
| 12 |
-
python demos/neural_rv32_neural_nets_via_neur.py
|
| 13 |
-
"""
|
| 14 |
-
import os, sys, time
|
| 15 |
-
HERE = os.path.dirname(os.path.abspath(__file__))
|
| 16 |
-
REPO = os.path.dirname(HERE)
|
| 17 |
-
sys.path.insert(0, os.path.join(REPO, "src"))
|
| 18 |
-
os.chdir(REPO)
|
| 19 |
-
import torch
|
| 20 |
-
from machines import Asm, Rv32ThresholdCPU, rv_state
|
| 21 |
-
from eval_all import int_to_bits
|
| 22 |
-
|
| 23 |
-
torch.manual_seed(0)
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
def ternary(w): # {-1,0,+1} forward, identity backward (STE)
|
| 27 |
-
wq = torch.where(w > 0.5, 1.0, torch.where(w < -0.5, -1.0, 0.0))
|
| 28 |
-
return w + (wq - w).detach()
|
| 29 |
-
|
| 30 |
-
def ibias(b, lo=-16, hi=15):
|
| 31 |
-
bq = b.round().clamp(lo, hi)
|
| 32 |
-
return b + (bq - b).detach()
|
| 33 |
-
|
| 34 |
-
# ---- discrete NEUR reference (exactly what the hardware computes) ----------
|
| 35 |
-
def neuron(x, pos, neg, bias):
|
| 36 |
-
return 1 if (bin(x & pos).count("1") - bin(x & neg).count("1") + bias) >= 0 else 0
|
| 37 |
-
|
| 38 |
-
def discrete_net(x, hidden, out):
|
| 39 |
-
h = 0
|
| 40 |
-
for j, (p, n, b) in enumerate(hidden):
|
| 41 |
-
h |= neuron(x, p, n, b) << j
|
| 42 |
-
p, n, b = out
|
| 43 |
-
return neuron(h, p, n, b)
|
| 44 |
-
|
| 45 |
-
# ---- train a 2-layer ternary MLP; hidden step trained with an annealed -----
|
| 46 |
-
# sigmoid so real gradients flow, then hardened to Heaviside at extraction
|
| 47 |
-
def train(target_fn, H, epochs=5000, seed=0, nbits=8):
|
| 48 |
-
torch.manual_seed(seed)
|
| 49 |
-
N = 1 << nbits
|
| 50 |
-
xs = torch.arange(N)
|
| 51 |
-
X = torch.stack([((xs >> i) & 1).float() for i in range(nbits)], 1)
|
| 52 |
-
Y = torch.tensor([float(target_fn(int(x))) for x in xs])
|
| 53 |
-
Wh = torch.nn.Parameter(torch.randn(H, nbits) * 1.5)
|
| 54 |
-
bh = torch.nn.Parameter(torch.randn(H) * 1.5)
|
| 55 |
-
Wo = torch.nn.Parameter(torch.randn(H) * 1.5)
|
| 56 |
-
bo = torch.nn.Parameter(torch.zeros(1))
|
| 57 |
-
opt = torch.optim.Adam([Wh, bh, Wo, bo], lr=0.03)
|
| 58 |
-
npos = float(Y.sum())
|
| 59 |
-
bce = torch.nn.BCEWithLogitsLoss(pos_weight=torch.tensor((N - npos) / max(npos, 1.0)))
|
| 60 |
-
for ep in range(epochs):
|
| 61 |
-
opt.zero_grad()
|
| 62 |
-
Tt = 1.0 + 11.0 * ep / epochs
|
| 63 |
-
preh = X @ ternary(Wh).T + ibias(bh)
|
| 64 |
-
h = torch.sigmoid(Tt * preh)
|
| 65 |
-
preo = h @ ternary(Wo) + ibias(bo)
|
| 66 |
-
loss = bce(4.0 * preo, Y)
|
| 67 |
-
loss.backward(); opt.step()
|
| 68 |
-
Whq = torch.where(Wh > 0.5, 1, torch.where(Wh < -0.5, -1, 0)).tolist()
|
| 69 |
-
Woq = torch.where(Wo > 0.5, 1, torch.where(Wo < -0.5, -1, 0)).tolist()
|
| 70 |
-
bhq = bh.round().clamp(-16, 15).long().tolist()
|
| 71 |
-
boq = int(bo.round().clamp(-16, 15).item())
|
| 72 |
-
def pack(wrow):
|
| 73 |
-
return (sum(1 << i for i, w in enumerate(wrow) if w > 0),
|
| 74 |
-
sum(1 << i for i, w in enumerate(wrow) if w < 0))
|
| 75 |
-
hidden = [(*pack(Whq[j]), bhq[j]) for j in range(H)]
|
| 76 |
-
out = (*pack(Woq), boq)
|
| 77 |
-
acc = sum(discrete_net(int(x), hidden, out) == int(Y[i]) for i, x in enumerate(xs)) / N
|
| 78 |
-
return hidden, out, acc
|
| 79 |
-
|
| 80 |
-
# ---- emit the network as NEUR code and run it on neural_rv32 ---------------
|
| 81 |
-
def li(a, reg, v):
|
| 82 |
-
v &= 0xFFFFFFFF
|
| 83 |
-
hi = (v + 0x800) >> 12
|
| 84 |
-
lo = v - (hi << 12)
|
| 85 |
-
a.lui(reg, hi & 0xFFFFF)
|
| 86 |
-
if lo: a.addi(reg, reg, lo)
|
| 87 |
-
|
| 88 |
-
def build_program(xval, hidden, out):
|
| 89 |
-
a = Asm()
|
| 90 |
-
li(a, 1, xval) # x1 = input byte
|
| 91 |
-
li(a, 2, 0) # x2 = packed hidden
|
| 92 |
-
for j, (p, n, b) in enumerate(hidden):
|
| 93 |
-
li(a, 4, p | (n << 8) | ((b & 0x1F) << 16)) # weight word
|
| 94 |
-
a.neur(3, 1, 4) # x3 = hidden neuron j
|
| 95 |
-
if j: a.slli(2, 2, 1)
|
| 96 |
-
a.or_(2, 2, 3) # pack MSB-first (neuron 0 -> bit H-1)
|
| 97 |
-
p, n, b = out
|
| 98 |
-
H = len(hidden)
|
| 99 |
-
def remap(mask): # match the MSB-first hidden packing
|
| 100 |
-
r = 0
|
| 101 |
-
for j in range(H):
|
| 102 |
-
if (mask >> j) & 1: r |= 1 << (H - 1 - j)
|
| 103 |
-
return r
|
| 104 |
-
li(a, 4, remap(p) | (remap(n) << 8) | ((b & 0x1F) << 16))
|
| 105 |
-
a.neur(5, 2, 4) # x5 = output
|
| 106 |
-
a.ecall()
|
| 107 |
-
return a.assemble()
|
| 108 |
-
|
| 109 |
-
def run_cpu(cpu, xval, hidden, out):
|
| 110 |
-
mem = build_program(xval, hidden, out)
|
| 111 |
-
ts = rv_state(mem)
|
| 112 |
-
ts["_mem_bits"] = torch.tensor([int_to_bits(b, 8) for b in ts["mem"]], dtype=torch.float32)
|
| 113 |
-
cyc = 0
|
| 114 |
-
while not ts["halted"] and cyc < 200:
|
| 115 |
-
ts = cpu.step(ts); cyc += 1
|
| 116 |
-
return ts["regs"][5], cyc
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
def parity(x, nbits=8): return bin(x & ((1 << nbits) - 1)).count("1") & 1
|
| 120 |
-
def band(x): return int(3 <= bin(x).count("1") <= 5) # non-monotone in popcount
|
| 121 |
-
|
| 122 |
-
def learn(fn, H, seeds, epochs):
|
| 123 |
-
best, t0, used = None, time.perf_counter(), 0
|
| 124 |
-
for seed in range(seeds):
|
| 125 |
-
used = seed
|
| 126 |
-
hidden, out, acc = train(fn, H=H, seed=seed, epochs=epochs)
|
| 127 |
-
if best is None or acc > best[2]:
|
| 128 |
-
best = (hidden, out, acc)
|
| 129 |
-
if acc == 1.0:
|
| 130 |
-
break
|
| 131 |
-
hidden, out, acc = best
|
| 132 |
-
print(f" trained 8->{H}->1 ternary MLP by annealed-sigmoid SGD: {acc*100:.1f}% "
|
| 133 |
-
f"exact over all 256 inputs ({time.perf_counter()-t0:.0f}s, seed {used}); "
|
| 134 |
-
f"weights all in {{-1,0,+1}}")
|
| 135 |
-
return hidden, out, acc
|
| 136 |
-
|
| 137 |
-
def deploy_check(cpu, fn, hidden, out, sample):
|
| 138 |
-
t0, cpu_vs_net, cpu_vs_true, rows = time.perf_counter(), 0, 0, []
|
| 139 |
-
for x in sample:
|
| 140 |
-
y_cpu, _ = run_cpu(cpu, x, hidden, out)
|
| 141 |
-
cpu_vs_net += (y_cpu == discrete_net(x, hidden, out))
|
| 142 |
-
cpu_vs_true += (y_cpu == fn(x))
|
| 143 |
-
rows.append((x, y_cpu, fn(x)))
|
| 144 |
-
print(f" NEUR execution on neural_rv32: CPU == reference net on "
|
| 145 |
-
f"{cpu_vs_net}/{len(sample)} (deployment exact); CPU == truth on "
|
| 146 |
-
f"{cpu_vs_true}/{len(sample)} ({time.perf_counter()-t0:.0f}s)")
|
| 147 |
-
return rows
|
| 148 |
-
|
| 149 |
-
def compile_parity(nbits=8): # exact thermometer construction
|
| 150 |
-
mask = (1 << nbits) - 1
|
| 151 |
-
hidden = [(mask, 0, -(k + 1)) for k in range(nbits)] # h_k = [popcount>=k+1]
|
| 152 |
-
pos = sum(1 << j for j in range(nbits) if j % 2 == 0) # odd thresholds
|
| 153 |
-
neg = sum(1 << j for j in range(nbits) if j % 2 == 1) # even thresholds
|
| 154 |
-
return hidden, (pos, neg, -1)
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
if __name__ == "__main__":
|
| 158 |
-
print("=" * 72)
|
| 159 |
-
print(" Neural networks running as software on the CPU that IS a neural net")
|
| 160 |
-
print("=" * 72)
|
| 161 |
-
cpu = Rv32ThresholdCPU()
|
| 162 |
-
|
| 163 |
-
print("\n[1] LEARNED by gradient descent -- popcount BAND: fire iff 3<=popcount<=5")
|
| 164 |
-
print(" (non-monotone, so no single threshold can do it; genuinely nonlinear)")
|
| 165 |
-
hidden, out, acc = learn(band, H=8, seeds=12, epochs=5000)
|
| 166 |
-
deploy_check(cpu, band, hidden, out,
|
| 167 |
-
[0, 1, 3, 7, 15, 31, 63, 127, 255, 0b10110, 0b1111000, 0b11011011])
|
| 168 |
-
line = "".join(f"{k}:{discrete_net((1 << k) - 1, hidden, out)} " for k in range(9))
|
| 169 |
-
print(f" net output vs popcount k: {line.strip()} (true band: k in 3,4,5)")
|
| 170 |
-
|
| 171 |
-
print("\n[2] COMPILED by construction -- 8-bit PARITY (XOR), the non-separable")
|
| 172 |
-
print(" function Minsky & Papert used to end the single perceptron")
|
| 173 |
-
hidden, out = compile_parity(8)
|
| 174 |
-
acc = sum(discrete_net(x, hidden, out) == parity(x) for x in range(256)) / 256
|
| 175 |
-
print(f" thermometer net (8 hidden 'popcount>=k' + alternating output), all "
|
| 176 |
-
f"weights in {{-1,0,+1}}: {acc*100:.0f}% exact over all 256 bytes")
|
| 177 |
-
rows = deploy_check(cpu, parity, hidden, out,
|
| 178 |
-
[0, 1, 3, 7, 15, 31, 85, 127, 128, 170, 200, 254, 255])
|
| 179 |
-
for x, yc, yt in rows:
|
| 180 |
-
print(f" parity({x:3d}) = {yc} (true {yt}) [{'ok' if yc==yt else 'x'}]")
|
| 181 |
-
|
| 182 |
-
print("\n One network learned by SGD, one compiled by hand, both executed")
|
| 183 |
-
print(" instruction by instruction on a CPU whose every gate is a threshold")
|
| 184 |
-
print(" neuron. The machine running the models belongs to the models' own class.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
demos/neural_subleq8io_universal_constructor.py
DELETED
|
@@ -1,74 +0,0 @@
|
|
| 1 |
-
"""neural_subleq8io -- von Neumann's universal constructor, to the byte.
|
| 2 |
-
|
| 3 |
-
The one-instruction machine (186 datapath units plus 8 packed memory
|
| 4 |
-
tensors) runs a 21-instruction SUBLEQ program
|
| 5 |
-
that reads a description of any machine in the family off its tape and emits
|
| 6 |
-
that machine's safetensors file one byte at a time. Here it fabricates
|
| 7 |
-
neural_tile.safetensors on the shipped gate netlist, byte-for-byte sha-identical
|
| 8 |
-
to the target, and the fabricated file is then booted: tiles reconstructed from
|
| 9 |
-
its own bytes regrow the binary counter. A computer made of threshold gates
|
| 10 |
-
prints a working computer, and the product runs.
|
| 11 |
-
|
| 12 |
-
python demos/neural_subleq8io_universal_constructor.py
|
| 13 |
-
"""
|
| 14 |
-
import os, sys, time, json, hashlib
|
| 15 |
-
HERE = os.path.dirname(os.path.abspath(__file__))
|
| 16 |
-
REPO = os.path.dirname(HERE)
|
| 17 |
-
sys.path.insert(0, os.path.join(REPO, "src"))
|
| 18 |
-
os.chdir(REPO)
|
| 19 |
-
from constructor8 import describe, ref_construct, GateHost
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
if __name__ == "__main__":
|
| 23 |
-
target_path = os.path.join(REPO, "variants", "neural_tile.safetensors")
|
| 24 |
-
data = open(target_path, "rb").read()
|
| 25 |
-
tape = describe(data)
|
| 26 |
-
print("neural_subleq8io: the universal constructor fabricates a sibling")
|
| 27 |
-
print("=" * 60)
|
| 28 |
-
print(f"recipe compiled: {len(data)} B machine -> {len(tape)} B tape")
|
| 29 |
-
|
| 30 |
-
out_ref, n_ref = ref_construct(tape)
|
| 31 |
-
assert out_ref == data
|
| 32 |
-
print(f"reference: constructor needs {n_ref:,} instructions")
|
| 33 |
-
|
| 34 |
-
host = GateHost()
|
| 35 |
-
t0 = time.perf_counter()
|
| 36 |
-
out, n, _ = host.run(tape, max_steps=n_ref + 100)
|
| 37 |
-
dt = time.perf_counter() - t0
|
| 38 |
-
sha_out = hashlib.sha256(out).hexdigest()[:16]
|
| 39 |
-
sha_tgt = hashlib.sha256(data).hexdigest()[:16]
|
| 40 |
-
print(f"gate netlist: {n:,} instructions in {dt:.0f}s ({n / dt:.0f} instr/s), "
|
| 41 |
-
f"{len(out)} bytes emitted")
|
| 42 |
-
print(f" sha256 fabricated={sha_out} shipped={sha_tgt} "
|
| 43 |
-
f"{'BYTE-IDENTICAL' if out == data else 'MISMATCH'}; "
|
| 44 |
-
f"instruction count == reference: {n == n_ref}")
|
| 45 |
-
|
| 46 |
-
# boot the fabricated machine: regrow its counter from its own bytes
|
| 47 |
-
fab = os.path.join(os.environ.get("TEMP", "."), "fabricated_neural_tile.safetensors")
|
| 48 |
-
open(fab, "wb").write(out)
|
| 49 |
-
from safetensors.torch import load_file
|
| 50 |
-
from safetensors import safe_open
|
| 51 |
-
import tile as T
|
| 52 |
-
|
| 53 |
-
t = load_file(fab)
|
| 54 |
-
with safe_open(fab, framework="pt") as f:
|
| 55 |
-
m = f.metadata()
|
| 56 |
-
gl = json.loads(m["glues"])
|
| 57 |
-
strg = {gl[i]: int(v) for i, v in enumerate(t["glue_strength"].tolist())}
|
| 58 |
-
tiles = []
|
| 59 |
-
for row, name in zip(t["tile_glues"].tolist(), json.loads(m["tile_names"])):
|
| 60 |
-
sides = [gl[i] if i >= 0 else "" for i in row]
|
| 61 |
-
tiles.append(T.Tile(N=sides[0], E=sides[1], S=sides[2], W=sides[3], name=name))
|
| 62 |
-
NB = 8
|
| 63 |
-
rows = (1 << NB) - 1
|
| 64 |
-
A, det = T.grow(tiles, T.counter_seed(NB), int(m["tau"]), strg, (0, 0, NB, rows))
|
| 65 |
-
bad = filled = 0
|
| 66 |
-
for y in range(1, rows + 1):
|
| 67 |
-
cells = [A.get((x, y)) for x in range(NB)]
|
| 68 |
-
if any(cc is None for cc in cells):
|
| 69 |
-
continue
|
| 70 |
-
filled += 1
|
| 71 |
-
v = sum((1 if cc.N == "b1" else 0) << (NB - 1 - x) for x, cc in enumerate(cells))
|
| 72 |
-
bad += (v != y)
|
| 73 |
-
print(f"boot: the fabricated file grew its counter, {filled} rows, "
|
| 74 |
-
f"row y encodes y: {'EXACT' if bad == 0 and filled == rows else f'{bad} bad'}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
demos/neural_tile_pascal_lucas.py
DELETED
|
@@ -1,42 +0,0 @@
|
|
| 1 |
-
"""neural_tile -- self-assembly grows Pascal's triangle mod 2 (Lucas' theorem).
|
| 2 |
-
|
| 3 |
-
The program is a set of square tiles whose binding rule is a threshold gate: a
|
| 4 |
-
tile attaches at a site when the summed strength of its matching glues meets the
|
| 5 |
-
temperature. With the XOR rule tile set, the crystal that grows is exactly the
|
| 6 |
-
Sierpinski triangle -- cell (x,y) is filled iff the binomial C(x+y, x) is odd,
|
| 7 |
-
which by Lucas' theorem is iff x AND y == 0. Every filled cell is verified
|
| 8 |
-
against that arithmetic. Computation as crystal growth.
|
| 9 |
-
|
| 10 |
-
python demos/neural_tile_pascal_lucas.py
|
| 11 |
-
"""
|
| 12 |
-
import os, sys, time
|
| 13 |
-
HERE = os.path.dirname(os.path.abspath(__file__))
|
| 14 |
-
REPO = os.path.dirname(HERE)
|
| 15 |
-
sys.path.insert(0, os.path.join(REPO, "src"))
|
| 16 |
-
import tile as T
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
if __name__ == "__main__":
|
| 20 |
-
N = 100
|
| 21 |
-
print("neural_tile: Pascal mod 2 by threshold-gated self-assembly")
|
| 22 |
-
print("=" * 60)
|
| 23 |
-
t0 = time.perf_counter()
|
| 24 |
-
ts = T.rule2_tileset(lambda w, s: w ^ s)
|
| 25 |
-
seed = T._row_col_seed([1] * (N + 1), [1] * (N + 1))
|
| 26 |
-
A, det = T.grow(ts, seed, tau=2, strength={}, bounds=(0, 0, N, N), max_tiles=200000)
|
| 27 |
-
dt = time.perf_counter() - t0
|
| 28 |
-
|
| 29 |
-
interior = [(x, y) for x in range(1, N + 1) for y in range(1, N + 1)]
|
| 30 |
-
grown = sum(1 for p in interior if p in A)
|
| 31 |
-
bad = 0
|
| 32 |
-
for (x, y) in interior:
|
| 33 |
-
v = 1 if A[(x, y)].N == "v1" else 0
|
| 34 |
-
lucas = 1 if (x & y) == 0 else 0 # C(x+y,x) odd <=> no carry in x+y
|
| 35 |
-
bad += (v != lucas)
|
| 36 |
-
print(f"grew {grown} rule tiles in {dt:.1f}s (directed={det}, "
|
| 37 |
-
f"{2 * N} anti-diagonals deep)")
|
| 38 |
-
print(f"tile(x,y) == [C(x+y,x) is odd] for all {len(interior)} cells: "
|
| 39 |
-
f"{'EXACT' if bad == 0 else f'{bad} MISMATCHES'}")
|
| 40 |
-
print("\ncorner of the assembly (30x30, '#' = odd binomial):")
|
| 41 |
-
for y in range(30, 0, -1):
|
| 42 |
-
print(" " + "".join("#" if A[(x, y)].N == "v1" else "." for x in range(1, 31)))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
llm_integration/baseline.py
DELETED
|
@@ -1,221 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Baseline evaluation: Vanilla SmolLM2-360M on arithmetic
|
| 3 |
-
"""
|
| 4 |
-
|
| 5 |
-
import torch
|
| 6 |
-
import random
|
| 7 |
-
import re
|
| 8 |
-
from transformers import AutoModelForCausalLM, AutoTokenizer
|
| 9 |
-
|
| 10 |
-
DEVICE = "cuda"
|
| 11 |
-
MODEL_ID = "HuggingFaceTB/SmolLM2-360M-Instruct"
|
| 12 |
-
|
| 13 |
-
SYSTEM_PROMPT = """You are a calculator. Output only the numeric answer. No words, no explanation, just digits. Examples:
|
| 14 |
-
User: 5 + 3
|
| 15 |
-
Assistant: 8
|
| 16 |
-
User: 12 * 7
|
| 17 |
-
Assistant: 84
|
| 18 |
-
User: 100 > 50
|
| 19 |
-
Assistant: 1
|
| 20 |
-
User: 25 < 10
|
| 21 |
-
Assistant: 0"""
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
def load_model():
|
| 25 |
-
print(f"Loading {MODEL_ID}...")
|
| 26 |
-
tokenizer = AutoTokenizer.from_pretrained(MODEL_ID)
|
| 27 |
-
tokenizer.padding_side = "left"
|
| 28 |
-
if tokenizer.pad_token is None:
|
| 29 |
-
tokenizer.pad_token = tokenizer.eos_token
|
| 30 |
-
model = AutoModelForCausalLM.from_pretrained(
|
| 31 |
-
MODEL_ID,
|
| 32 |
-
torch_dtype=torch.float16,
|
| 33 |
-
device_map=DEVICE
|
| 34 |
-
)
|
| 35 |
-
model.eval()
|
| 36 |
-
print(f" Loaded. Parameters: {sum(p.numel() for p in model.parameters()):,}")
|
| 37 |
-
return model, tokenizer
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
def format_prompt(tokenizer, op_str):
|
| 41 |
-
messages = [
|
| 42 |
-
{"role": "system", "content": SYSTEM_PROMPT},
|
| 43 |
-
{"role": "user", "content": op_str}
|
| 44 |
-
]
|
| 45 |
-
return tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
def generate_batch(model, tokenizer, prompts, max_new_tokens=16):
|
| 49 |
-
inputs = tokenizer(prompts, return_tensors="pt", padding=True).to(DEVICE)
|
| 50 |
-
with torch.no_grad():
|
| 51 |
-
outputs = model.generate(
|
| 52 |
-
**inputs,
|
| 53 |
-
max_new_tokens=max_new_tokens,
|
| 54 |
-
do_sample=False,
|
| 55 |
-
pad_token_id=tokenizer.eos_token_id
|
| 56 |
-
)
|
| 57 |
-
responses = []
|
| 58 |
-
for i, output in enumerate(outputs):
|
| 59 |
-
response = tokenizer.decode(output[inputs.input_ids.shape[1]:], skip_special_tokens=True)
|
| 60 |
-
responses.append(response.strip())
|
| 61 |
-
return responses
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
def extract_answer(text):
|
| 65 |
-
"""Generous extraction - find any number in output"""
|
| 66 |
-
text = text.strip().lower()
|
| 67 |
-
if not text:
|
| 68 |
-
return None
|
| 69 |
-
|
| 70 |
-
# Handle Yes/No for comparisons
|
| 71 |
-
if text in ['yes', 'true', '1']:
|
| 72 |
-
return 1
|
| 73 |
-
if text in ['no', 'false', '0']:
|
| 74 |
-
return 0
|
| 75 |
-
if text.startswith('yes'):
|
| 76 |
-
return 1
|
| 77 |
-
if text.startswith('no'):
|
| 78 |
-
return 0
|
| 79 |
-
|
| 80 |
-
# Find all numbers, take the LAST one (most likely the answer)
|
| 81 |
-
numbers = re.findall(r'-?\d+', text)
|
| 82 |
-
if numbers:
|
| 83 |
-
return int(numbers[-1])
|
| 84 |
-
return None
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
def ground_truth(a, b, op):
|
| 88 |
-
"""Compute expected result (8-bit where applicable)"""
|
| 89 |
-
if op == 'add':
|
| 90 |
-
return (a + b) & 0xFF
|
| 91 |
-
elif op == 'sub':
|
| 92 |
-
return (a - b) & 0xFF
|
| 93 |
-
elif op == 'mul':
|
| 94 |
-
return (a * b) & 0xFF
|
| 95 |
-
elif op == 'div':
|
| 96 |
-
return a // b if b != 0 else 0
|
| 97 |
-
elif op == 'and':
|
| 98 |
-
return a & b
|
| 99 |
-
elif op == 'or':
|
| 100 |
-
return a | b
|
| 101 |
-
elif op == 'xor':
|
| 102 |
-
return a ^ b
|
| 103 |
-
elif op == 'gt':
|
| 104 |
-
return 1 if a > b else 0
|
| 105 |
-
elif op == 'lt':
|
| 106 |
-
return 1 if a < b else 0
|
| 107 |
-
elif op == 'eq':
|
| 108 |
-
return 1 if a == b else 0
|
| 109 |
-
elif op == 'ge':
|
| 110 |
-
return 1 if a >= b else 0
|
| 111 |
-
elif op == 'le':
|
| 112 |
-
return 1 if a <= b else 0
|
| 113 |
-
else:
|
| 114 |
-
raise ValueError(f"Unknown op: {op}")
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
def op_to_str(a, b, op):
|
| 118 |
-
"""Convert operation to natural string"""
|
| 119 |
-
symbols = {
|
| 120 |
-
'add': '+', 'sub': '-', 'mul': '*', 'div': '/',
|
| 121 |
-
'and': '&', 'or': '|', 'xor': '^',
|
| 122 |
-
'gt': '>', 'lt': '<', 'eq': '==', 'ge': '>=', 'le': '<='
|
| 123 |
-
}
|
| 124 |
-
return f"{a} {symbols[op]} {b}"
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
def evaluate(model, tokenizer, n_samples=1000, batch_size=32, ops=None):
|
| 128 |
-
if ops is None:
|
| 129 |
-
ops = ['add', 'sub', 'mul', 'gt', 'lt', 'eq']
|
| 130 |
-
|
| 131 |
-
results = {op: {'correct': 0, 'total': 0} for op in ops}
|
| 132 |
-
all_correct = 0
|
| 133 |
-
all_total = 0
|
| 134 |
-
|
| 135 |
-
samples = []
|
| 136 |
-
for _ in range(n_samples):
|
| 137 |
-
a = random.randint(0, 255)
|
| 138 |
-
b = random.randint(0, 255)
|
| 139 |
-
if 'div' in ops and random.random() < 0.1:
|
| 140 |
-
op = 'div'
|
| 141 |
-
b = random.randint(1, 255) # avoid div by zero
|
| 142 |
-
else:
|
| 143 |
-
op = random.choice([o for o in ops if o != 'div'])
|
| 144 |
-
samples.append((a, b, op))
|
| 145 |
-
|
| 146 |
-
print(f"\nEvaluating {n_samples} samples (batch_size={batch_size})...")
|
| 147 |
-
|
| 148 |
-
for batch_start in range(0, n_samples, batch_size):
|
| 149 |
-
batch = samples[batch_start:batch_start + batch_size]
|
| 150 |
-
prompts = [format_prompt(tokenizer, op_to_str(a, b, op)) for a, b, op in batch]
|
| 151 |
-
responses = generate_batch(model, tokenizer, prompts)
|
| 152 |
-
|
| 153 |
-
for (a, b, op), response in zip(batch, responses):
|
| 154 |
-
expected = ground_truth(a, b, op)
|
| 155 |
-
extracted = extract_answer(response)
|
| 156 |
-
|
| 157 |
-
correct = (extracted == expected)
|
| 158 |
-
results[op]['total'] += 1
|
| 159 |
-
all_total += 1
|
| 160 |
-
if correct:
|
| 161 |
-
results[op]['correct'] += 1
|
| 162 |
-
all_correct += 1
|
| 163 |
-
|
| 164 |
-
if (batch_start + batch_size) % 200 == 0 or batch_start + batch_size >= n_samples:
|
| 165 |
-
pct = 100 * all_correct / all_total
|
| 166 |
-
print(f" Progress: {min(batch_start + batch_size, n_samples)}/{n_samples} | Accuracy: {pct:.2f}%")
|
| 167 |
-
|
| 168 |
-
return results, all_correct, all_total
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
def main():
|
| 172 |
-
random.seed(42)
|
| 173 |
-
torch.manual_seed(42)
|
| 174 |
-
|
| 175 |
-
model, tokenizer = load_model()
|
| 176 |
-
|
| 177 |
-
# Quick sanity check
|
| 178 |
-
print("\nSanity check (5 examples):")
|
| 179 |
-
test_cases = [
|
| 180 |
-
("5 + 3", 8),
|
| 181 |
-
("100 - 37", 63),
|
| 182 |
-
("12 * 11", 132),
|
| 183 |
-
("50 > 30", 1),
|
| 184 |
-
("25 < 10", 0),
|
| 185 |
-
]
|
| 186 |
-
prompts = [format_prompt(tokenizer, q) for q, _ in test_cases]
|
| 187 |
-
responses = generate_batch(model, tokenizer, prompts)
|
| 188 |
-
for (q, expected), response in zip(test_cases, responses):
|
| 189 |
-
extracted = extract_answer(response)
|
| 190 |
-
status = "OK" if extracted == expected else "FAIL"
|
| 191 |
-
print(f" {q} = {expected} | Model: '{response}' -> {extracted} [{status}]")
|
| 192 |
-
|
| 193 |
-
# Full evaluation
|
| 194 |
-
print("\n" + "=" * 60)
|
| 195 |
-
print(" BASELINE EVALUATION")
|
| 196 |
-
print("=" * 60)
|
| 197 |
-
|
| 198 |
-
ops = ['add', 'sub', 'mul', 'gt', 'lt', 'eq']
|
| 199 |
-
results, correct, total = evaluate(model, tokenizer, n_samples=2000, batch_size=64, ops=ops)
|
| 200 |
-
|
| 201 |
-
print("\n" + "=" * 60)
|
| 202 |
-
print(" RESULTS BY OPERATION")
|
| 203 |
-
print("=" * 60)
|
| 204 |
-
for op in ops:
|
| 205 |
-
r = results[op]
|
| 206 |
-
pct = 100 * r['correct'] / r['total'] if r['total'] > 0 else 0
|
| 207 |
-
print(f" {op:6}: {r['correct']:4}/{r['total']:4} ({pct:6.2f}%)")
|
| 208 |
-
|
| 209 |
-
print("\n" + "=" * 60)
|
| 210 |
-
print(" OVERALL")
|
| 211 |
-
print("=" * 60)
|
| 212 |
-
fitness = correct / total
|
| 213 |
-
print(f" Correct: {correct}/{total}")
|
| 214 |
-
print(f" Fitness: {fitness:.4f} ({100*fitness:.2f}%)")
|
| 215 |
-
print("=" * 60)
|
| 216 |
-
|
| 217 |
-
return fitness
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
if __name__ == "__main__":
|
| 221 |
-
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
llm_integration/circuits.py
DELETED
|
@@ -1,320 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Frozen threshold circuit wrapper for LLM integration.
|
| 3 |
-
Loads safetensors and provides differentiable-compatible execution.
|
| 4 |
-
"""
|
| 5 |
-
|
| 6 |
-
import torch
|
| 7 |
-
import torch.nn as nn
|
| 8 |
-
from safetensors import safe_open
|
| 9 |
-
from typing import Dict, Tuple
|
| 10 |
-
|
| 11 |
-
MODEL_PATH = "D:/8bit-threshold-computer/neural_computer.safetensors"
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
def heaviside(x: torch.Tensor) -> torch.Tensor:
|
| 15 |
-
"""Standard Heaviside step function."""
|
| 16 |
-
return (x >= 0).float()
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
class HeavisideSTE(torch.autograd.Function):
|
| 20 |
-
"""Heaviside with straight-through estimator for gradients."""
|
| 21 |
-
@staticmethod
|
| 22 |
-
def forward(ctx, x):
|
| 23 |
-
return (x >= 0).float()
|
| 24 |
-
|
| 25 |
-
@staticmethod
|
| 26 |
-
def backward(ctx, grad_output):
|
| 27 |
-
return grad_output
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
def heaviside_ste(x: torch.Tensor) -> torch.Tensor:
|
| 31 |
-
"""Heaviside with STE gradient."""
|
| 32 |
-
return HeavisideSTE.apply(x)
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
class FrozenThresholdCircuits(nn.Module):
|
| 36 |
-
"""
|
| 37 |
-
Wrapper for frozen threshold logic circuits.
|
| 38 |
-
All weights are frozen - no gradients flow through circuit internals.
|
| 39 |
-
Gradients flow through inputs/outputs via STE.
|
| 40 |
-
"""
|
| 41 |
-
|
| 42 |
-
def __init__(self, model_path: str = MODEL_PATH, device: str = 'cuda'):
|
| 43 |
-
super().__init__()
|
| 44 |
-
self.device = device
|
| 45 |
-
self.weights = {}
|
| 46 |
-
self._load_weights(model_path)
|
| 47 |
-
|
| 48 |
-
def _load_weights(self, path: str):
|
| 49 |
-
"""Load weights from safetensors file."""
|
| 50 |
-
with safe_open(path, framework='pt') as f:
|
| 51 |
-
for name in f.keys():
|
| 52 |
-
tensor = f.get_tensor(name).to(self.device).float()
|
| 53 |
-
self.weights[name] = tensor
|
| 54 |
-
|
| 55 |
-
def _gate(self, inputs: torch.Tensor, weight: torch.Tensor, bias: torch.Tensor) -> torch.Tensor:
|
| 56 |
-
"""Execute single threshold gate with STE."""
|
| 57 |
-
weight = weight.view(-1)
|
| 58 |
-
bias = bias.view(-1)
|
| 59 |
-
pre_activation = (inputs * weight).sum(dim=-1) + bias
|
| 60 |
-
return heaviside_ste(pre_activation)
|
| 61 |
-
|
| 62 |
-
def _xor(self, a: torch.Tensor, b: torch.Tensor, prefix: str) -> torch.Tensor:
|
| 63 |
-
"""XOR via OR-NAND-AND pattern (2 layers)."""
|
| 64 |
-
inputs = torch.stack([a, b], dim=-1)
|
| 65 |
-
|
| 66 |
-
w_or = self.weights[f'{prefix}.layer1.or.weight']
|
| 67 |
-
b_or = self.weights[f'{prefix}.layer1.or.bias']
|
| 68 |
-
w_nand = self.weights[f'{prefix}.layer1.nand.weight']
|
| 69 |
-
b_nand = self.weights[f'{prefix}.layer1.nand.bias']
|
| 70 |
-
|
| 71 |
-
h_or = self._gate(inputs, w_or, b_or)
|
| 72 |
-
h_nand = self._gate(inputs, w_nand, b_nand)
|
| 73 |
-
|
| 74 |
-
hidden = torch.stack([h_or, h_nand], dim=-1)
|
| 75 |
-
w2 = self.weights[f'{prefix}.layer2.weight']
|
| 76 |
-
b2 = self.weights[f'{prefix}.layer2.bias']
|
| 77 |
-
|
| 78 |
-
return self._gate(hidden, w2, b2)
|
| 79 |
-
|
| 80 |
-
def _full_adder(self, a: torch.Tensor, b: torch.Tensor, cin: torch.Tensor,
|
| 81 |
-
prefix: str) -> Tuple[torch.Tensor, torch.Tensor]:
|
| 82 |
-
"""Full adder: sum and carry out."""
|
| 83 |
-
ha1_sum = self._xor(a, b, f'{prefix}.ha1.sum')
|
| 84 |
-
|
| 85 |
-
inp_carry1 = torch.stack([a, b], dim=-1)
|
| 86 |
-
w_c1 = self.weights[f'{prefix}.ha1.carry.weight']
|
| 87 |
-
b_c1 = self.weights[f'{prefix}.ha1.carry.bias']
|
| 88 |
-
ha1_carry = self._gate(inp_carry1, w_c1, b_c1)
|
| 89 |
-
|
| 90 |
-
ha2_sum = self._xor(ha1_sum, cin, f'{prefix}.ha2.sum')
|
| 91 |
-
|
| 92 |
-
inp_carry2 = torch.stack([ha1_sum, cin], dim=-1)
|
| 93 |
-
w_c2 = self.weights[f'{prefix}.ha2.carry.weight']
|
| 94 |
-
b_c2 = self.weights[f'{prefix}.ha2.carry.bias']
|
| 95 |
-
ha2_carry = self._gate(inp_carry2, w_c2, b_c2)
|
| 96 |
-
|
| 97 |
-
inp_cout = torch.stack([ha1_carry, ha2_carry], dim=-1)
|
| 98 |
-
w_cout = self.weights[f'{prefix}.carry_or.weight']
|
| 99 |
-
b_cout = self.weights[f'{prefix}.carry_or.bias']
|
| 100 |
-
cout = self._gate(inp_cout, w_cout, b_cout)
|
| 101 |
-
|
| 102 |
-
return ha2_sum, cout
|
| 103 |
-
|
| 104 |
-
def add_8bit(self, a_bits: torch.Tensor, b_bits: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
|
| 105 |
-
"""
|
| 106 |
-
8-bit ripple carry addition.
|
| 107 |
-
|
| 108 |
-
Args:
|
| 109 |
-
a_bits: [batch, 8] MSB-first
|
| 110 |
-
b_bits: [batch, 8] MSB-first
|
| 111 |
-
|
| 112 |
-
Returns:
|
| 113 |
-
result_bits: [batch, 8] MSB-first
|
| 114 |
-
carry_out: [batch] final carry
|
| 115 |
-
"""
|
| 116 |
-
batch_size = a_bits.shape[0]
|
| 117 |
-
carry = torch.zeros(batch_size, device=self.device)
|
| 118 |
-
result_bits = []
|
| 119 |
-
|
| 120 |
-
for bit in range(8):
|
| 121 |
-
bit_idx = 7 - bit
|
| 122 |
-
s, carry = self._full_adder(
|
| 123 |
-
a_bits[:, bit_idx],
|
| 124 |
-
b_bits[:, bit_idx],
|
| 125 |
-
carry,
|
| 126 |
-
f'arithmetic.ripplecarry8bit.fa{bit}'
|
| 127 |
-
)
|
| 128 |
-
result_bits.insert(0, s)
|
| 129 |
-
|
| 130 |
-
result = torch.stack(result_bits, dim=1)
|
| 131 |
-
return result, carry
|
| 132 |
-
|
| 133 |
-
def sub_8bit(self, a_bits: torch.Tensor, b_bits: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
|
| 134 |
-
"""
|
| 135 |
-
8-bit subtraction via two's complement: A - B = A + (~B) + 1
|
| 136 |
-
|
| 137 |
-
Args:
|
| 138 |
-
a_bits: [batch, 8] MSB-first
|
| 139 |
-
b_bits: [batch, 8] MSB-first
|
| 140 |
-
|
| 141 |
-
Returns:
|
| 142 |
-
result_bits: [batch, 8] MSB-first
|
| 143 |
-
borrow_out: [batch] (inverted carry)
|
| 144 |
-
"""
|
| 145 |
-
b_inv = 1.0 - b_bits
|
| 146 |
-
batch_size = a_bits.shape[0]
|
| 147 |
-
carry = torch.ones(batch_size, device=self.device)
|
| 148 |
-
result_bits = []
|
| 149 |
-
|
| 150 |
-
for bit in range(8):
|
| 151 |
-
bit_idx = 7 - bit
|
| 152 |
-
s, carry = self._full_adder(
|
| 153 |
-
a_bits[:, bit_idx],
|
| 154 |
-
b_inv[:, bit_idx],
|
| 155 |
-
carry,
|
| 156 |
-
f'arithmetic.ripplecarry8bit.fa{bit}'
|
| 157 |
-
)
|
| 158 |
-
result_bits.insert(0, s)
|
| 159 |
-
|
| 160 |
-
result = torch.stack(result_bits, dim=1)
|
| 161 |
-
borrow = 1.0 - carry
|
| 162 |
-
return result, borrow
|
| 163 |
-
|
| 164 |
-
def mul_8bit(self, a_bits: torch.Tensor, b_bits: torch.Tensor) -> torch.Tensor:
|
| 165 |
-
"""
|
| 166 |
-
8-bit multiplication via shift-add (software implementation using adder circuits).
|
| 167 |
-
Only keeps low 8 bits of result (matches 8-bit wrap behavior).
|
| 168 |
-
|
| 169 |
-
Args:
|
| 170 |
-
a_bits: [batch, 8] MSB-first
|
| 171 |
-
b_bits: [batch, 8] MSB-first
|
| 172 |
-
|
| 173 |
-
Returns:
|
| 174 |
-
result_bits: [batch, 8] MSB-first (low 8 bits of product)
|
| 175 |
-
"""
|
| 176 |
-
batch_size = a_bits.shape[0]
|
| 177 |
-
|
| 178 |
-
acc = torch.zeros(batch_size, 8, device=self.device)
|
| 179 |
-
|
| 180 |
-
for i in range(8):
|
| 181 |
-
b_bit = b_bits[:, 7 - i]
|
| 182 |
-
pp = a_bits * b_bit.unsqueeze(1)
|
| 183 |
-
|
| 184 |
-
shifted_pp = torch.zeros(batch_size, 8, device=self.device)
|
| 185 |
-
for j in range(8):
|
| 186 |
-
dst_idx = j + i
|
| 187 |
-
if dst_idx < 8:
|
| 188 |
-
shifted_pp[:, 7 - dst_idx] = pp[:, 7 - j]
|
| 189 |
-
|
| 190 |
-
acc, _ = self.add_8bit(acc, shifted_pp)
|
| 191 |
-
|
| 192 |
-
return acc
|
| 193 |
-
|
| 194 |
-
def compare_gt(self, a_bits: torch.Tensor, b_bits: torch.Tensor) -> torch.Tensor:
|
| 195 |
-
"""A > B comparison."""
|
| 196 |
-
inputs = torch.cat([a_bits, b_bits], dim=-1)
|
| 197 |
-
w = self.weights['arithmetic.greaterthan8bit.weight'].view(-1)
|
| 198 |
-
b = self.weights['arithmetic.greaterthan8bit.bias'].view(-1)
|
| 199 |
-
return heaviside_ste((inputs * w).sum(dim=-1) + b)
|
| 200 |
-
|
| 201 |
-
def compare_lt(self, a_bits: torch.Tensor, b_bits: torch.Tensor) -> torch.Tensor:
|
| 202 |
-
"""A < B comparison."""
|
| 203 |
-
inputs = torch.cat([a_bits, b_bits], dim=-1)
|
| 204 |
-
w = self.weights['arithmetic.lessthan8bit.weight'].view(-1)
|
| 205 |
-
b = self.weights['arithmetic.lessthan8bit.bias'].view(-1)
|
| 206 |
-
return heaviside_ste((inputs * w).sum(dim=-1) + b)
|
| 207 |
-
|
| 208 |
-
def compare_eq(self, a_bits: torch.Tensor, b_bits: torch.Tensor) -> torch.Tensor:
|
| 209 |
-
"""A == B comparison (two-layer)."""
|
| 210 |
-
inputs = torch.cat([a_bits, b_bits], dim=-1)
|
| 211 |
-
prefix = 'arithmetic.equality8bit'
|
| 212 |
-
|
| 213 |
-
w_geq = self.weights[f'{prefix}.layer1.geq.weight'].view(-1)
|
| 214 |
-
b_geq = self.weights[f'{prefix}.layer1.geq.bias'].view(-1)
|
| 215 |
-
w_leq = self.weights[f'{prefix}.layer1.leq.weight'].view(-1)
|
| 216 |
-
b_leq = self.weights[f'{prefix}.layer1.leq.bias'].view(-1)
|
| 217 |
-
|
| 218 |
-
h_geq = heaviside_ste((inputs * w_geq).sum(dim=-1) + b_geq)
|
| 219 |
-
h_leq = heaviside_ste((inputs * w_leq).sum(dim=-1) + b_leq)
|
| 220 |
-
|
| 221 |
-
hidden = torch.stack([h_geq, h_leq], dim=-1)
|
| 222 |
-
w2 = self.weights[f'{prefix}.layer2.weight'].view(-1)
|
| 223 |
-
b2 = self.weights[f'{prefix}.layer2.bias'].view(-1)
|
| 224 |
-
|
| 225 |
-
return heaviside_ste((hidden * w2).sum(dim=-1) + b2)
|
| 226 |
-
|
| 227 |
-
def forward(self, a_bits: torch.Tensor, b_bits: torch.Tensor,
|
| 228 |
-
op_onehot: torch.Tensor) -> torch.Tensor:
|
| 229 |
-
"""
|
| 230 |
-
Execute operation based on one-hot selector.
|
| 231 |
-
Uses soft routing during training for gradient flow.
|
| 232 |
-
|
| 233 |
-
Args:
|
| 234 |
-
a_bits: [batch, 8] operand A
|
| 235 |
-
b_bits: [batch, 8] operand B
|
| 236 |
-
op_onehot: [batch, 6] one-hot operation selector
|
| 237 |
-
[add, sub, mul, gt, lt, eq]
|
| 238 |
-
|
| 239 |
-
Returns:
|
| 240 |
-
result_bits: [batch, 8] result (comparisons in bit 7, rest zeros)
|
| 241 |
-
"""
|
| 242 |
-
batch_size = a_bits.shape[0]
|
| 243 |
-
|
| 244 |
-
add_result, _ = self.add_8bit(a_bits, b_bits)
|
| 245 |
-
sub_result, _ = self.sub_8bit(a_bits, b_bits)
|
| 246 |
-
mul_result = self.mul_8bit(a_bits, b_bits)
|
| 247 |
-
|
| 248 |
-
gt_result = self.compare_gt(a_bits, b_bits)
|
| 249 |
-
lt_result = self.compare_lt(a_bits, b_bits)
|
| 250 |
-
eq_result = self.compare_eq(a_bits, b_bits)
|
| 251 |
-
|
| 252 |
-
cmp_expanded = torch.zeros(batch_size, 8, device=self.device)
|
| 253 |
-
|
| 254 |
-
gt_expanded = cmp_expanded.clone()
|
| 255 |
-
gt_expanded[:, 7] = gt_result
|
| 256 |
-
|
| 257 |
-
lt_expanded = cmp_expanded.clone()
|
| 258 |
-
lt_expanded[:, 7] = lt_result
|
| 259 |
-
|
| 260 |
-
eq_expanded = cmp_expanded.clone()
|
| 261 |
-
eq_expanded[:, 7] = eq_result
|
| 262 |
-
|
| 263 |
-
results = torch.stack([
|
| 264 |
-
add_result,
|
| 265 |
-
sub_result,
|
| 266 |
-
mul_result,
|
| 267 |
-
gt_expanded,
|
| 268 |
-
lt_expanded,
|
| 269 |
-
eq_expanded
|
| 270 |
-
], dim=1)
|
| 271 |
-
|
| 272 |
-
op_weights = op_onehot.unsqueeze(-1)
|
| 273 |
-
output = (results * op_weights).sum(dim=1)
|
| 274 |
-
|
| 275 |
-
return output
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
if __name__ == "__main__":
|
| 279 |
-
print("Testing frozen circuits...")
|
| 280 |
-
|
| 281 |
-
circuits = FrozenThresholdCircuits(device='cuda')
|
| 282 |
-
print(f"Loaded {len(circuits.weights)} tensors")
|
| 283 |
-
|
| 284 |
-
a = torch.tensor([[0, 0, 0, 0, 0, 1, 0, 1]], device='cuda', dtype=torch.float32)
|
| 285 |
-
b = torch.tensor([[0, 0, 0, 0, 0, 0, 1, 1]], device='cuda', dtype=torch.float32)
|
| 286 |
-
|
| 287 |
-
result, carry = circuits.add_8bit(a, b)
|
| 288 |
-
val = sum(int(result[0, i].item()) << (7 - i) for i in range(8))
|
| 289 |
-
print(f"5 + 3 = {val} (expected 8)")
|
| 290 |
-
|
| 291 |
-
a = torch.tensor([[0, 1, 1, 0, 0, 1, 0, 0]], device='cuda', dtype=torch.float32)
|
| 292 |
-
b = torch.tensor([[0, 0, 1, 0, 0, 1, 0, 1]], device='cuda', dtype=torch.float32)
|
| 293 |
-
result, _ = circuits.sub_8bit(a, b)
|
| 294 |
-
val = sum(int(result[0, i].item()) << (7 - i) for i in range(8))
|
| 295 |
-
print(f"100 - 37 = {val} (expected 63)")
|
| 296 |
-
|
| 297 |
-
a = torch.tensor([[0, 0, 0, 0, 1, 1, 0, 0]], device='cuda', dtype=torch.float32)
|
| 298 |
-
b = torch.tensor([[0, 0, 0, 0, 1, 0, 1, 1]], device='cuda', dtype=torch.float32)
|
| 299 |
-
result = circuits.mul_8bit(a, b)
|
| 300 |
-
val = sum(int(result[0, i].item()) << (7 - i) for i in range(8))
|
| 301 |
-
print(f"12 * 11 = {val} (expected 132)")
|
| 302 |
-
|
| 303 |
-
a = torch.tensor([[0, 0, 1, 1, 0, 0, 1, 0]], device='cuda', dtype=torch.float32)
|
| 304 |
-
b = torch.tensor([[0, 0, 0, 1, 1, 1, 1, 0]], device='cuda', dtype=torch.float32)
|
| 305 |
-
gt = circuits.compare_gt(a, b)
|
| 306 |
-
lt = circuits.compare_lt(a, b)
|
| 307 |
-
eq = circuits.compare_eq(a, b)
|
| 308 |
-
print(f"50 > 30: {int(gt[0].item())} (expected 1)")
|
| 309 |
-
print(f"50 < 30: {int(lt[0].item())} (expected 0)")
|
| 310 |
-
print(f"50 == 30: {int(eq[0].item())} (expected 0)")
|
| 311 |
-
|
| 312 |
-
print("\nTesting batched forward...")
|
| 313 |
-
batch_a = torch.randint(0, 2, (16, 8), device='cuda', dtype=torch.float32)
|
| 314 |
-
batch_b = torch.randint(0, 2, (16, 8), device='cuda', dtype=torch.float32)
|
| 315 |
-
op = torch.zeros(16, 6, device='cuda')
|
| 316 |
-
op[:, 0] = 1.0
|
| 317 |
-
|
| 318 |
-
result = circuits(batch_a, batch_b, op)
|
| 319 |
-
print(f"Batch output shape: {result.shape}")
|
| 320 |
-
print("Done.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
llm_integration/fitness.py
DELETED
|
@@ -1,218 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Shared fitness function for threshold circuit LLM integration.
|
| 3 |
-
Randomized tests, no answer supervision - fitness IS the training signal.
|
| 4 |
-
"""
|
| 5 |
-
|
| 6 |
-
import torch
|
| 7 |
-
import random
|
| 8 |
-
from typing import Callable, Dict, Tuple, List
|
| 9 |
-
|
| 10 |
-
OPERATIONS = ['add', 'sub', 'mul', 'gt', 'lt', 'eq']
|
| 11 |
-
|
| 12 |
-
def ground_truth(a: int, b: int, op: str) -> int:
|
| 13 |
-
"""Compute expected result (8-bit arithmetic)."""
|
| 14 |
-
if op == 'add':
|
| 15 |
-
return (a + b) & 0xFF
|
| 16 |
-
elif op == 'sub':
|
| 17 |
-
return (a - b) & 0xFF
|
| 18 |
-
elif op == 'mul':
|
| 19 |
-
return (a * b) & 0xFF
|
| 20 |
-
elif op == 'gt':
|
| 21 |
-
return 1 if a > b else 0
|
| 22 |
-
elif op == 'lt':
|
| 23 |
-
return 1 if a < b else 0
|
| 24 |
-
elif op == 'eq':
|
| 25 |
-
return 1 if a == b else 0
|
| 26 |
-
else:
|
| 27 |
-
raise ValueError(f"Unknown op: {op}")
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
def int_to_bits(val: int, n_bits: int = 8) -> torch.Tensor:
|
| 31 |
-
"""Convert integer to bit tensor (MSB first)."""
|
| 32 |
-
bits = torch.zeros(n_bits)
|
| 33 |
-
for i in range(n_bits):
|
| 34 |
-
bits[n_bits - 1 - i] = (val >> i) & 1
|
| 35 |
-
return bits
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
def bits_to_int(bits: torch.Tensor) -> int:
|
| 39 |
-
"""Convert bit tensor to integer (MSB first)."""
|
| 40 |
-
val = 0
|
| 41 |
-
n_bits = bits.shape[-1]
|
| 42 |
-
for i in range(n_bits):
|
| 43 |
-
val += int(bits[..., i].item()) << (n_bits - 1 - i)
|
| 44 |
-
return val
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
def op_to_idx(op: str) -> int:
|
| 48 |
-
"""Convert operation string to index."""
|
| 49 |
-
return OPERATIONS.index(op)
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
def idx_to_op(idx: int) -> str:
|
| 53 |
-
"""Convert index to operation string."""
|
| 54 |
-
return OPERATIONS[idx]
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
def generate_batch(batch_size: int, device: str = 'cuda') -> Dict[str, torch.Tensor]:
|
| 58 |
-
"""
|
| 59 |
-
Generate a batch of random arithmetic problems.
|
| 60 |
-
|
| 61 |
-
Returns:
|
| 62 |
-
Dict with:
|
| 63 |
-
'a': [batch_size] int tensor of first operands
|
| 64 |
-
'b': [batch_size] int tensor of second operands
|
| 65 |
-
'op': [batch_size] int tensor of operation indices
|
| 66 |
-
'a_bits': [batch_size, 8] bit tensor
|
| 67 |
-
'b_bits': [batch_size, 8] bit tensor
|
| 68 |
-
'op_onehot': [batch_size, 6] one-hot operation tensor
|
| 69 |
-
'expected': [batch_size] int tensor of expected results
|
| 70 |
-
'expected_bits': [batch_size, 8] bit tensor of expected results
|
| 71 |
-
"""
|
| 72 |
-
a_vals = torch.randint(0, 256, (batch_size,), device=device)
|
| 73 |
-
b_vals = torch.randint(0, 256, (batch_size,), device=device)
|
| 74 |
-
op_indices = torch.randint(0, len(OPERATIONS), (batch_size,), device=device)
|
| 75 |
-
|
| 76 |
-
a_bits = torch.zeros(batch_size, 8, device=device)
|
| 77 |
-
b_bits = torch.zeros(batch_size, 8, device=device)
|
| 78 |
-
for i in range(8):
|
| 79 |
-
a_bits[:, 7-i] = (a_vals >> i) & 1
|
| 80 |
-
b_bits[:, 7-i] = (b_vals >> i) & 1
|
| 81 |
-
|
| 82 |
-
op_onehot = torch.zeros(batch_size, len(OPERATIONS), device=device)
|
| 83 |
-
op_onehot.scatter_(1, op_indices.unsqueeze(1), 1.0)
|
| 84 |
-
|
| 85 |
-
expected = torch.zeros(batch_size, dtype=torch.long, device=device)
|
| 86 |
-
for i in range(batch_size):
|
| 87 |
-
a, b, op_idx = a_vals[i].item(), b_vals[i].item(), op_indices[i].item()
|
| 88 |
-
expected[i] = ground_truth(a, b, idx_to_op(op_idx))
|
| 89 |
-
|
| 90 |
-
expected_bits = torch.zeros(batch_size, 8, device=device)
|
| 91 |
-
for i in range(8):
|
| 92 |
-
expected_bits[:, 7-i] = (expected >> i) & 1
|
| 93 |
-
|
| 94 |
-
return {
|
| 95 |
-
'a': a_vals,
|
| 96 |
-
'b': b_vals,
|
| 97 |
-
'op': op_indices,
|
| 98 |
-
'a_bits': a_bits.float(),
|
| 99 |
-
'b_bits': b_bits.float(),
|
| 100 |
-
'op_onehot': op_onehot.float(),
|
| 101 |
-
'expected': expected,
|
| 102 |
-
'expected_bits': expected_bits.float(),
|
| 103 |
-
}
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
def compute_fitness(
|
| 107 |
-
model_fn: Callable[[torch.Tensor, torch.Tensor, torch.Tensor], torch.Tensor],
|
| 108 |
-
n_samples: int = 10000,
|
| 109 |
-
batch_size: int = 256,
|
| 110 |
-
device: str = 'cuda',
|
| 111 |
-
return_details: bool = False
|
| 112 |
-
) -> float | Tuple[float, Dict]:
|
| 113 |
-
"""
|
| 114 |
-
Compute fitness score for a model.
|
| 115 |
-
|
| 116 |
-
Args:
|
| 117 |
-
model_fn: Function that takes (a_bits, b_bits, op_onehot) and returns result_bits
|
| 118 |
-
n_samples: Number of test cases
|
| 119 |
-
batch_size: Batch size for evaluation
|
| 120 |
-
device: Device to run on
|
| 121 |
-
return_details: If True, return per-operation breakdown
|
| 122 |
-
|
| 123 |
-
Returns:
|
| 124 |
-
Fitness score in [0, 1], optionally with details dict
|
| 125 |
-
"""
|
| 126 |
-
correct = 0
|
| 127 |
-
total = 0
|
| 128 |
-
op_correct = {op: 0 for op in OPERATIONS}
|
| 129 |
-
op_total = {op: 0 for op in OPERATIONS}
|
| 130 |
-
|
| 131 |
-
for _ in range(0, n_samples, batch_size):
|
| 132 |
-
actual_batch = min(batch_size, n_samples - total)
|
| 133 |
-
batch = generate_batch(actual_batch, device)
|
| 134 |
-
|
| 135 |
-
with torch.no_grad():
|
| 136 |
-
pred_bits = model_fn(batch['a_bits'], batch['b_bits'], batch['op_onehot'])
|
| 137 |
-
|
| 138 |
-
pred_bits_binary = (pred_bits > 0.5).float()
|
| 139 |
-
|
| 140 |
-
for i in range(actual_batch):
|
| 141 |
-
pred_val = 0
|
| 142 |
-
for j in range(8):
|
| 143 |
-
pred_val += int(pred_bits_binary[i, j].item()) << (7 - j)
|
| 144 |
-
|
| 145 |
-
expected_val = batch['expected'][i].item()
|
| 146 |
-
op_name = idx_to_op(batch['op'][i].item())
|
| 147 |
-
|
| 148 |
-
op_total[op_name] += 1
|
| 149 |
-
total += 1
|
| 150 |
-
|
| 151 |
-
if pred_val == expected_val:
|
| 152 |
-
correct += 1
|
| 153 |
-
op_correct[op_name] += 1
|
| 154 |
-
|
| 155 |
-
fitness = correct / total if total > 0 else 0.0
|
| 156 |
-
|
| 157 |
-
if return_details:
|
| 158 |
-
details = {
|
| 159 |
-
'correct': correct,
|
| 160 |
-
'total': total,
|
| 161 |
-
'by_op': {
|
| 162 |
-
op: {
|
| 163 |
-
'correct': op_correct[op],
|
| 164 |
-
'total': op_total[op],
|
| 165 |
-
'accuracy': op_correct[op] / op_total[op] if op_total[op] > 0 else 0.0
|
| 166 |
-
}
|
| 167 |
-
for op in OPERATIONS
|
| 168 |
-
}
|
| 169 |
-
}
|
| 170 |
-
return fitness, details
|
| 171 |
-
|
| 172 |
-
return fitness
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
def compute_bit_accuracy(pred_bits: torch.Tensor, expected_bits: torch.Tensor) -> float:
|
| 176 |
-
"""Compute per-bit accuracy (for gradient signal analysis)."""
|
| 177 |
-
pred_binary = (pred_bits > 0.5).float()
|
| 178 |
-
return (pred_binary == expected_bits).float().mean().item()
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
def compute_loss(pred_bits: torch.Tensor, expected_bits: torch.Tensor) -> torch.Tensor:
|
| 182 |
-
"""Binary cross-entropy loss on output bits."""
|
| 183 |
-
pred_clamped = pred_bits.clamp(1e-7, 1 - 1e-7)
|
| 184 |
-
return -((expected_bits * torch.log(pred_clamped) +
|
| 185 |
-
(1 - expected_bits) * torch.log(1 - pred_clamped))).mean()
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
if __name__ == "__main__":
|
| 189 |
-
print("Testing fitness module...")
|
| 190 |
-
|
| 191 |
-
batch = generate_batch(8, 'cpu')
|
| 192 |
-
print(f"\nSample batch:")
|
| 193 |
-
for i in range(4):
|
| 194 |
-
a, b = batch['a'][i].item(), batch['b'][i].item()
|
| 195 |
-
op = idx_to_op(batch['op'][i].item())
|
| 196 |
-
expected = batch['expected'][i].item()
|
| 197 |
-
print(f" {a} {op} {b} = {expected}")
|
| 198 |
-
|
| 199 |
-
def random_model(a_bits, b_bits, op_onehot):
|
| 200 |
-
return torch.rand(a_bits.shape[0], 8, device=a_bits.device)
|
| 201 |
-
|
| 202 |
-
fitness = compute_fitness(random_model, n_samples=1000, batch_size=100, device='cpu')
|
| 203 |
-
print(f"\nRandom model fitness: {fitness:.4f} (expected ~0.004 for 8-bit)")
|
| 204 |
-
|
| 205 |
-
def perfect_model(a_bits, b_bits, op_onehot):
|
| 206 |
-
batch_size = a_bits.shape[0]
|
| 207 |
-
results = torch.zeros(batch_size, 8, device=a_bits.device)
|
| 208 |
-
for i in range(batch_size):
|
| 209 |
-
a = sum(int(a_bits[i, j].item()) << (7-j) for j in range(8))
|
| 210 |
-
b = sum(int(b_bits[i, j].item()) << (7-j) for j in range(8))
|
| 211 |
-
op_idx = op_onehot[i].argmax().item()
|
| 212 |
-
result = ground_truth(a, b, idx_to_op(op_idx))
|
| 213 |
-
for j in range(8):
|
| 214 |
-
results[i, 7-j] = (result >> j) & 1
|
| 215 |
-
return results
|
| 216 |
-
|
| 217 |
-
fitness = compute_fitness(perfect_model, n_samples=1000, batch_size=100, device='cpu')
|
| 218 |
-
print(f"Perfect model fitness: {fitness:.4f} (expected 1.0)")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
llm_integration/{model.py → llm.py}
RENAMED
|
@@ -1,24 +1,402 @@
|
|
| 1 |
-
"""
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
|
|
|
|
|
|
|
|
|
| 6 |
|
| 7 |
import torch
|
| 8 |
import torch.nn as nn
|
| 9 |
import torch.nn.functional as F
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
|
| 12 |
-
MODEL_ID = 'HuggingFaceTB/SmolLM2-360M-Instruct'
|
| 13 |
OPERATIONS = ['add', 'sub', 'mul', 'gt', 'lt', 'eq']
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
OP_SYMBOLS = {'add': '+', 'sub': '-', 'mul': '*', 'gt': '>', 'lt': '<', 'eq': '=='}
|
| 15 |
|
| 16 |
|
| 17 |
class BitEncoder(nn.Module):
|
| 18 |
-
"""
|
| 19 |
-
Encodes two 8-bit operands from input representation.
|
| 20 |
-
Uses residual connection to preserve ground truth bits while allowing learned refinement.
|
| 21 |
-
"""
|
| 22 |
|
| 23 |
def __init__(self, input_dim: int = 16 + 6, hidden_dim: int = 32):
|
| 24 |
super().__init__()
|
|
@@ -30,14 +408,7 @@ class BitEncoder(nn.Module):
|
|
| 30 |
self.scale = nn.Parameter(torch.tensor(0.0))
|
| 31 |
|
| 32 |
def forward(self, x: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
|
| 33 |
-
"""
|
| 34 |
-
Args:
|
| 35 |
-
x: [batch, input_dim] input with first 16 dims being a_bits, b_bits
|
| 36 |
-
|
| 37 |
-
Returns:
|
| 38 |
-
a_bits: [batch, 8] first operand bits
|
| 39 |
-
b_bits: [batch, 8] second operand bits
|
| 40 |
-
"""
|
| 41 |
base_bits = x[:, :16]
|
| 42 |
refinement = self.refine(x) * torch.sigmoid(self.scale)
|
| 43 |
bits = base_bits + refinement
|
|
@@ -49,10 +420,7 @@ class BitEncoder(nn.Module):
|
|
| 49 |
|
| 50 |
|
| 51 |
class OpRouter(nn.Module):
|
| 52 |
-
"""
|
| 53 |
-
Routes computation to the appropriate circuit based on input.
|
| 54 |
-
Outputs soft weights over operations for gradient flow.
|
| 55 |
-
"""
|
| 56 |
|
| 57 |
def __init__(self, input_dim: int = 16 + 6, hidden_dim: int = 32, n_ops: int = 6):
|
| 58 |
super().__init__()
|
|
@@ -63,23 +431,13 @@ class OpRouter(nn.Module):
|
|
| 63 |
)
|
| 64 |
|
| 65 |
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 66 |
-
"""
|
| 67 |
-
Args:
|
| 68 |
-
x: [batch, input_dim] input features
|
| 69 |
-
|
| 70 |
-
Returns:
|
| 71 |
-
op_weights: [batch, n_ops] soft operation weights (softmax)
|
| 72 |
-
"""
|
| 73 |
logits = self.net(x)
|
| 74 |
return F.softmax(logits, dim=-1)
|
| 75 |
|
| 76 |
|
| 77 |
class BitDecoder(nn.Module):
|
| 78 |
-
"""
|
| 79 |
-
Decodes circuit output bits to target representation.
|
| 80 |
-
For standalone training: outputs soft bits for loss computation.
|
| 81 |
-
For LLM integration: would project to hidden state delta.
|
| 82 |
-
"""
|
| 83 |
|
| 84 |
def __init__(self, output_dim: int = 8):
|
| 85 |
super().__init__()
|
|
@@ -90,10 +448,7 @@ class BitDecoder(nn.Module):
|
|
| 90 |
|
| 91 |
|
| 92 |
class ThresholdALU(nn.Module):
|
| 93 |
-
"""
|
| 94 |
-
Complete trainable interface + frozen circuits.
|
| 95 |
-
Learns to encode inputs, route to circuits, decode outputs.
|
| 96 |
-
"""
|
| 97 |
|
| 98 |
def __init__(self, device: str = 'cuda'):
|
| 99 |
super().__init__()
|
|
@@ -110,17 +465,7 @@ class ThresholdALU(nn.Module):
|
|
| 110 |
|
| 111 |
def forward(self, a_bits_in: torch.Tensor, b_bits_in: torch.Tensor,
|
| 112 |
op_onehot: torch.Tensor) -> torch.Tensor:
|
| 113 |
-
"""
|
| 114 |
-
Forward pass through trainable interface + frozen circuits.
|
| 115 |
-
|
| 116 |
-
Args:
|
| 117 |
-
a_bits_in: [batch, 8] input A bits (ground truth for training)
|
| 118 |
-
b_bits_in: [batch, 8] input B bits (ground truth for training)
|
| 119 |
-
op_onehot: [batch, 6] one-hot operation selector
|
| 120 |
-
|
| 121 |
-
Returns:
|
| 122 |
-
result_bits: [batch, 8] output bits
|
| 123 |
-
"""
|
| 124 |
x = torch.cat([a_bits_in, b_bits_in, op_onehot], dim=-1)
|
| 125 |
|
| 126 |
a_bits, b_bits = self.encoder(x)
|
|
@@ -142,10 +487,7 @@ class ThresholdALU(nn.Module):
|
|
| 142 |
|
| 143 |
|
| 144 |
class DirectCircuitModel(nn.Module):
|
| 145 |
-
"""
|
| 146 |
-
Minimal model that directly uses circuits without learned encoding.
|
| 147 |
-
For validating that circuits themselves achieve 100% fitness.
|
| 148 |
-
"""
|
| 149 |
|
| 150 |
def __init__(self, device: str = 'cuda'):
|
| 151 |
super().__init__()
|
|
@@ -158,10 +500,7 @@ class DirectCircuitModel(nn.Module):
|
|
| 158 |
|
| 159 |
|
| 160 |
class HiddenStateExtractor(nn.Module):
|
| 161 |
-
"""
|
| 162 |
-
Extracts operands and operation from LLM hidden states.
|
| 163 |
-
This is the hard part - must learn to parse numbers from embeddings.
|
| 164 |
-
"""
|
| 165 |
|
| 166 |
def __init__(self, hidden_dim: int = 960, intermediate_dim: int = 256):
|
| 167 |
super().__init__()
|
|
@@ -185,15 +524,7 @@ class HiddenStateExtractor(nn.Module):
|
|
| 185 |
)
|
| 186 |
|
| 187 |
def forward(self, hidden_states: torch.Tensor):
|
| 188 |
-
"""
|
| 189 |
-
Args:
|
| 190 |
-
hidden_states: [batch, hidden_dim] from LLM
|
| 191 |
-
|
| 192 |
-
Returns:
|
| 193 |
-
a_bits: [batch, 8]
|
| 194 |
-
b_bits: [batch, 8]
|
| 195 |
-
op_logits: [batch, 6]
|
| 196 |
-
"""
|
| 197 |
a_logits = self.a_extractor(hidden_states)
|
| 198 |
b_logits = self.b_extractor(hidden_states)
|
| 199 |
op_logits = self.op_router(hidden_states)
|
|
@@ -211,10 +542,7 @@ class HiddenStateExtractor(nn.Module):
|
|
| 211 |
|
| 212 |
|
| 213 |
class AttentionPooling(nn.Module):
|
| 214 |
-
"""
|
| 215 |
-
Learnable attention pooling over sequence positions.
|
| 216 |
-
Replaces mean pooling - learns which tokens matter for extraction.
|
| 217 |
-
"""
|
| 218 |
|
| 219 |
def __init__(self, hidden_dim: int = 960, num_heads: int = 4):
|
| 220 |
super().__init__()
|
|
@@ -229,14 +557,7 @@ class AttentionPooling(nn.Module):
|
|
| 229 |
self.cls_token = nn.Parameter(torch.randn(1, 1, hidden_dim) * 0.02)
|
| 230 |
|
| 231 |
def forward(self, embeddings: torch.Tensor, mask: torch.Tensor) -> torch.Tensor:
|
| 232 |
-
"""
|
| 233 |
-
Args:
|
| 234 |
-
embeddings: [batch, seq_len, hidden_dim]
|
| 235 |
-
mask: [batch, seq_len] attention mask (1 = attend, 0 = ignore)
|
| 236 |
-
|
| 237 |
-
Returns:
|
| 238 |
-
pooled: [batch, hidden_dim]
|
| 239 |
-
"""
|
| 240 |
batch_size, seq_len, hidden_dim = embeddings.shape
|
| 241 |
|
| 242 |
cls_expanded = self.cls_token.expand(batch_size, -1, -1)
|
|
@@ -271,10 +592,7 @@ class AttentionPooling(nn.Module):
|
|
| 271 |
|
| 272 |
|
| 273 |
class MultiHeadBitExtractor(nn.Module):
|
| 274 |
-
"""
|
| 275 |
-
8 separate extractors for 8 bits - each bit gets its own specialized network.
|
| 276 |
-
More expressive than single MLP predicting all 8 bits at once.
|
| 277 |
-
"""
|
| 278 |
|
| 279 |
def __init__(self, hidden_dim: int = 960, intermediate_dim: int = 128):
|
| 280 |
super().__init__()
|
|
@@ -289,13 +607,7 @@ class MultiHeadBitExtractor(nn.Module):
|
|
| 289 |
])
|
| 290 |
|
| 291 |
def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
|
| 292 |
-
"""
|
| 293 |
-
Args:
|
| 294 |
-
hidden_states: [batch, hidden_dim]
|
| 295 |
-
|
| 296 |
-
Returns:
|
| 297 |
-
bits: [batch, 8] - one bit from each extractor
|
| 298 |
-
"""
|
| 299 |
hidden_states = torch.nan_to_num(hidden_states, nan=0.0)
|
| 300 |
|
| 301 |
bit_logits = [extractor(hidden_states) for extractor in self.bit_extractors]
|
|
@@ -310,10 +622,7 @@ class MultiHeadBitExtractor(nn.Module):
|
|
| 310 |
|
| 311 |
|
| 312 |
class Extractor(nn.Module):
|
| 313 |
-
"""
|
| 314 |
-
Extracts operands and operation from LLM hidden states.
|
| 315 |
-
Uses attention pooling and per-bit extraction networks.
|
| 316 |
-
"""
|
| 317 |
|
| 318 |
def __init__(self, hidden_dim: int = 960, intermediate_dim: int = 256, num_heads: int = 4):
|
| 319 |
super().__init__()
|
|
@@ -330,16 +639,7 @@ class Extractor(nn.Module):
|
|
| 330 |
)
|
| 331 |
|
| 332 |
def forward(self, embeddings: torch.Tensor, mask: torch.Tensor):
|
| 333 |
-
"""
|
| 334 |
-
Args:
|
| 335 |
-
embeddings: [batch, seq_len, hidden_dim]
|
| 336 |
-
mask: [batch, seq_len]
|
| 337 |
-
|
| 338 |
-
Returns:
|
| 339 |
-
a_bits: [batch, 8]
|
| 340 |
-
b_bits: [batch, 8]
|
| 341 |
-
op_logits: [batch, 6]
|
| 342 |
-
"""
|
| 343 |
pooled = self.attention_pool(embeddings, mask)
|
| 344 |
|
| 345 |
a_bits, _ = self.a_extractor(pooled)
|
|
@@ -350,22 +650,7 @@ class Extractor(nn.Module):
|
|
| 350 |
|
| 351 |
|
| 352 |
class PositionExtractor(nn.Module):
|
| 353 |
-
"""
|
| 354 |
-
Position-specific extraction with dynamic operator detection.
|
| 355 |
-
|
| 356 |
-
Tokenization pattern for "A op B":
|
| 357 |
-
[A_digits...] [operator] [space] [B_digits...]
|
| 358 |
-
|
| 359 |
-
Examples:
|
| 360 |
-
"5 + 3" -> ['5', ' +', ' ', '3'] (positions: A=0, op=1, B=3)
|
| 361 |
-
"47 + 86" -> ['4', '7', ' +', ' ', '8', '6'] (positions: A=0-1, op=2, B=4-5)
|
| 362 |
-
"127 + 128" -> ['1','2','7',' +', ' ','1','2','8'] (positions: A=0-2, op=3, B=5-7)
|
| 363 |
-
|
| 364 |
-
Token IDs (SmolLM2):
|
| 365 |
-
Digits '0'-'9': 32-41
|
| 366 |
-
Operators: ' +'=1232, ' -'=731, ' *'=1672, ' >'=2986, ' <'=2067, ' =='=1758
|
| 367 |
-
Space: 216
|
| 368 |
-
"""
|
| 369 |
|
| 370 |
DIGIT_TOKENS = set(range(32, 42))
|
| 371 |
OPERATOR_TOKENS = {
|
|
@@ -406,33 +691,14 @@ class PositionExtractor(nn.Module):
|
|
| 406 |
)
|
| 407 |
|
| 408 |
def _find_operator_position(self, token_ids: torch.Tensor) -> tuple[int, int]:
|
| 409 |
-
"""
|
| 410 |
-
Find operator token position and its operation index.
|
| 411 |
-
|
| 412 |
-
Args:
|
| 413 |
-
token_ids: [seq_len] tensor of token IDs
|
| 414 |
-
|
| 415 |
-
Returns:
|
| 416 |
-
(position, op_index) or (-1, -1) if not found
|
| 417 |
-
"""
|
| 418 |
for pos, tid in enumerate(token_ids.tolist()):
|
| 419 |
if tid in self.OPERATOR_TOKENS:
|
| 420 |
return pos, self.OPERATOR_TOKENS[tid]
|
| 421 |
return -1, -1
|
| 422 |
|
| 423 |
def _extract_digit_features(self, hidden: torch.Tensor, start: int, end: int) -> torch.Tensor:
|
| 424 |
-
"""
|
| 425 |
-
Extract and pad digit hidden states to fixed size.
|
| 426 |
-
|
| 427 |
-
Args:
|
| 428 |
-
hidden: [seq_len, hidden_dim]
|
| 429 |
-
start: start position (inclusive)
|
| 430 |
-
end: end position (exclusive)
|
| 431 |
-
|
| 432 |
-
Returns:
|
| 433 |
-
[hidden_dim * MAX_DIGITS] flattened features, zero-padded on the LEFT
|
| 434 |
-
(so units digit is always at the same position regardless of number length)
|
| 435 |
-
"""
|
| 436 |
n_digits = end - start
|
| 437 |
features = torch.zeros(self.MAX_DIGITS * self.hidden_dim, device=hidden.device)
|
| 438 |
|
|
@@ -444,17 +710,7 @@ class PositionExtractor(nn.Module):
|
|
| 444 |
return features
|
| 445 |
|
| 446 |
def forward(self, hidden: torch.Tensor, mask: torch.Tensor, token_ids: torch.Tensor = None):
|
| 447 |
-
"""
|
| 448 |
-
Args:
|
| 449 |
-
hidden: [batch, seq_len, hidden_dim]
|
| 450 |
-
mask: [batch, seq_len] attention mask
|
| 451 |
-
token_ids: [batch, seq_len] token IDs (required for operator detection)
|
| 452 |
-
|
| 453 |
-
Returns:
|
| 454 |
-
a_bits: [batch, 8]
|
| 455 |
-
b_bits: [batch, 8]
|
| 456 |
-
op_logits: [batch, 6]
|
| 457 |
-
"""
|
| 458 |
if token_ids is None:
|
| 459 |
raise ValueError("PositionExtractor requires token_ids for operator detection")
|
| 460 |
|
|
@@ -515,19 +771,7 @@ class PositionExtractor(nn.Module):
|
|
| 515 |
|
| 516 |
|
| 517 |
class PositionalDigitExtractor(nn.Module):
|
| 518 |
-
"""
|
| 519 |
-
Position-aware digit extraction: classifies each digit position independently.
|
| 520 |
-
|
| 521 |
-
This approach achieves 100% accuracy because:
|
| 522 |
-
1. Each digit token position is classified independently (100% accuracy on layer 0)
|
| 523 |
-
2. Numbers are reconstructed using place values (×100, ×10, ×1)
|
| 524 |
-
3. No information is lost through pooling
|
| 525 |
-
|
| 526 |
-
Token IDs (SmolLM2):
|
| 527 |
-
Digits '0'-'9': 32-41
|
| 528 |
-
Operators: ' +'=1232, ' -'=731, ' *'=1672, ' >'=2986, ' <'=2067, ' =='=1758
|
| 529 |
-
Space: 216
|
| 530 |
-
"""
|
| 531 |
|
| 532 |
DIGIT_TOKENS = set(range(32, 42))
|
| 533 |
OPERATOR_TOKENS = {
|
|
@@ -605,20 +849,7 @@ class PositionalDigitExtractor(nn.Module):
|
|
| 605 |
return torch.stack(bits)
|
| 606 |
|
| 607 |
def forward(self, hidden: torch.Tensor, mask: torch.Tensor, token_ids: torch.Tensor):
|
| 608 |
-
"""
|
| 609 |
-
Args:
|
| 610 |
-
hidden: [batch, seq_len, hidden_dim]
|
| 611 |
-
mask: [batch, seq_len]
|
| 612 |
-
token_ids: [batch, seq_len]
|
| 613 |
-
|
| 614 |
-
Returns:
|
| 615 |
-
a_bits: [batch, 8]
|
| 616 |
-
b_bits: [batch, 8]
|
| 617 |
-
op_logits: [batch, 6]
|
| 618 |
-
op_indices: [batch] ground truth op from tokens
|
| 619 |
-
a_digit_logits: list of [batch, 10] per digit position
|
| 620 |
-
b_digit_logits: list of [batch, 10] per digit position
|
| 621 |
-
"""
|
| 622 |
batch_size = hidden.shape[0]
|
| 623 |
device = hidden.device
|
| 624 |
|
|
@@ -672,10 +903,7 @@ class PositionalDigitExtractor(nn.Module):
|
|
| 672 |
|
| 673 |
|
| 674 |
class DigitExtractor(nn.Module):
|
| 675 |
-
"""
|
| 676 |
-
Digit-level extraction: predicts digits (0-9) then converts to bits.
|
| 677 |
-
Uses attention pooling (less accurate than PositionalDigitExtractor).
|
| 678 |
-
"""
|
| 679 |
|
| 680 |
def __init__(self, hidden_dim: int = 960, intermediate_dim: int = 256, num_heads: int = 4):
|
| 681 |
super().__init__()
|
|
@@ -701,11 +929,7 @@ class DigitExtractor(nn.Module):
|
|
| 701 |
)
|
| 702 |
|
| 703 |
def digits_to_bits(self, digit_logits: torch.Tensor) -> torch.Tensor:
|
| 704 |
-
"""
|
| 705 |
-
Convert 3-digit predictions to 8-bit representation.
|
| 706 |
-
digit_logits: [batch, 30] (3 digits * 10 classes each)
|
| 707 |
-
Returns: [batch, 8] bits
|
| 708 |
-
"""
|
| 709 |
batch_size = digit_logits.shape[0]
|
| 710 |
|
| 711 |
logits = digit_logits.view(batch_size, 3, 10)
|
|
@@ -729,10 +953,7 @@ class DigitExtractor(nn.Module):
|
|
| 729 |
return torch.stack(bits, dim=-1)
|
| 730 |
|
| 731 |
def forward(self, hidden: torch.Tensor, mask: torch.Tensor):
|
| 732 |
-
"""
|
| 733 |
-
Returns:
|
| 734 |
-
a_bits, b_bits, op_logits, a_digit_logits, b_digit_logits
|
| 735 |
-
"""
|
| 736 |
pooled = self.attention_pool(hidden, mask)
|
| 737 |
|
| 738 |
a_digit_logits = self.a_digit_pred(pooled)
|
|
@@ -746,15 +967,7 @@ class DigitExtractor(nn.Module):
|
|
| 746 |
|
| 747 |
|
| 748 |
class HybridExtractor(nn.Module):
|
| 749 |
-
"""
|
| 750 |
-
Hybrid extractor that handles both digit tokens and word numbers.
|
| 751 |
-
|
| 752 |
-
For digit tokens (32-41): Direct lookup, no training needed
|
| 753 |
-
For word numbers: Learned MLP extraction from pooled hidden states
|
| 754 |
-
|
| 755 |
-
This is the real training target - learning to extract numbers from
|
| 756 |
-
natural language like "forty seven plus eighty six".
|
| 757 |
-
"""
|
| 758 |
|
| 759 |
DIGIT_TOKENS = set(range(32, 42))
|
| 760 |
SYMBOL_OP_TOKENS = {
|
|
@@ -820,11 +1033,7 @@ class HybridExtractor(nn.Module):
|
|
| 820 |
return -1
|
| 821 |
|
| 822 |
def _extract_from_digits(self, token_ids: torch.Tensor) -> tuple:
|
| 823 |
-
"""
|
| 824 |
-
Extract values directly from digit tokens (hardcoded lookup).
|
| 825 |
-
Handles both symbol operators (' +') and word operators ('plus').
|
| 826 |
-
Returns (a_value, b_value, op_idx) or None if pattern not found.
|
| 827 |
-
"""
|
| 828 |
tokens = token_ids.tolist()
|
| 829 |
|
| 830 |
op_pos = -1
|
|
@@ -873,11 +1082,7 @@ class HybridExtractor(nn.Module):
|
|
| 873 |
return bits
|
| 874 |
|
| 875 |
def _digits_to_value_and_bits(self, digit_logits: torch.Tensor, device) -> tuple:
|
| 876 |
-
"""
|
| 877 |
-
Convert 3-digit logits to value and bits.
|
| 878 |
-
digit_logits: [30] (3 digits × 10 classes)
|
| 879 |
-
Returns: (value tensor, bits tensor [8])
|
| 880 |
-
"""
|
| 881 |
logits = digit_logits.view(3, 10)
|
| 882 |
probs = torch.softmax(logits, dim=-1)
|
| 883 |
|
|
@@ -900,16 +1105,7 @@ class HybridExtractor(nn.Module):
|
|
| 900 |
return hundreds * 100 + tens * 10 + ones, torch.stack(bits)
|
| 901 |
|
| 902 |
def forward(self, hidden: torch.Tensor, mask: torch.Tensor, token_ids: torch.Tensor = None):
|
| 903 |
-
"""
|
| 904 |
-
Args:
|
| 905 |
-
hidden: [batch, seq_len, hidden_dim]
|
| 906 |
-
mask: [batch, seq_len]
|
| 907 |
-
token_ids: [batch, seq_len] - optional, enables digit lookup
|
| 908 |
-
|
| 909 |
-
Returns:
|
| 910 |
-
a_bits, b_bits, op_logits, a_values, b_values, used_lookup,
|
| 911 |
-
a_digit_logits, b_digit_logits
|
| 912 |
-
"""
|
| 913 |
batch_size = hidden.shape[0]
|
| 914 |
device = hidden.device
|
| 915 |
|
|
@@ -1019,10 +1215,7 @@ class HybridExtractor(nn.Module):
|
|
| 1019 |
|
| 1020 |
|
| 1021 |
class ArithmeticModel(nn.Module):
|
| 1022 |
-
"""
|
| 1023 |
-
LLM + extractor + frozen threshold circuits.
|
| 1024 |
-
Optionally unfreeze top N transformer layers with --unfreeze_layers.
|
| 1025 |
-
"""
|
| 1026 |
|
| 1027 |
def __init__(self, device: str = 'cuda', unfreeze_layers: int = 0,
|
| 1028 |
extract_layer: int = -1, position_extract: bool = False,
|
|
@@ -1117,14 +1310,7 @@ class ArithmeticModel(nn.Module):
|
|
| 1117 |
print(f" Total trainable: {total_trainable:,}", flush=True)
|
| 1118 |
|
| 1119 |
def get_hidden_states(self, texts: list[str]) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
| 1120 |
-
"""
|
| 1121 |
-
Get hidden states from specified layer.
|
| 1122 |
-
|
| 1123 |
-
Returns:
|
| 1124 |
-
hidden: [batch, seq_len, hidden_dim] hidden states
|
| 1125 |
-
mask: [batch, seq_len] attention mask
|
| 1126 |
-
token_ids: [batch, seq_len] input token IDs
|
| 1127 |
-
"""
|
| 1128 |
inputs = self.tokenizer(
|
| 1129 |
texts,
|
| 1130 |
return_tensors='pt',
|
|
@@ -1146,15 +1332,7 @@ class ArithmeticModel(nn.Module):
|
|
| 1146 |
return hidden, mask, token_ids
|
| 1147 |
|
| 1148 |
def forward(self, texts: list[str]):
|
| 1149 |
-
"""
|
| 1150 |
-
Full forward pass: text -> hidden states -> extractor -> circuits -> result
|
| 1151 |
-
|
| 1152 |
-
Returns:
|
| 1153 |
-
result_bits, a_bits, b_bits, op_logits
|
| 1154 |
-
If digit_pred: also returns a_digit_logits, b_digit_logits
|
| 1155 |
-
If position_extract or positional_digit: also returns op_indices
|
| 1156 |
-
If positional_digit: also returns a_values, b_values
|
| 1157 |
-
"""
|
| 1158 |
hidden, mask, token_ids = self.get_hidden_states(texts)
|
| 1159 |
|
| 1160 |
if self.hybrid or self.positional_digit or self.position_extract:
|
|
@@ -1207,10 +1385,1182 @@ class ArithmeticModel(nn.Module):
|
|
| 1207 |
return params
|
| 1208 |
|
| 1209 |
|
| 1210 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1211 |
import sys
|
| 1212 |
sys.path.insert(0, '.')
|
| 1213 |
-
from fitness import generate_batch, compute_fitness, OPERATIONS
|
| 1214 |
|
| 1215 |
print("Testing model components...")
|
| 1216 |
|
|
@@ -1261,3 +2611,14 @@ if __name__ == "__main__":
|
|
| 1261 |
print(f" Total trainable: {total:,}")
|
| 1262 |
|
| 1263 |
print("\nDone.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Frozen threshold circuits inside SmolLM2: the circuits, the fitness, the interface, training, baseline."""
|
| 2 |
+
import argparse
|
| 3 |
+
import os
|
| 4 |
+
import random
|
| 5 |
+
import re
|
| 6 |
+
import sys
|
| 7 |
+
import time
|
| 8 |
+
from typing import Dict, Tuple, Callable, List
|
| 9 |
|
| 10 |
import torch
|
| 11 |
import torch.nn as nn
|
| 12 |
import torch.nn.functional as F
|
| 13 |
+
import torch.optim as optim
|
| 14 |
+
from safetensors import safe_open
|
| 15 |
+
from transformers import AutoModelForCausalLM, AutoTokenizer
|
| 16 |
+
|
| 17 |
+
TRAINED = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'trained')
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
MODEL_PATH = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'neural_computer.safetensors')
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def heaviside(x: torch.Tensor) -> torch.Tensor:
|
| 24 |
+
"""Standard Heaviside step function."""
|
| 25 |
+
return (x >= 0).float()
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
class HeavisideSTE(torch.autograd.Function):
|
| 29 |
+
"""Heaviside with straight-through estimator for gradients."""
|
| 30 |
+
@staticmethod
|
| 31 |
+
def forward(ctx, x):
|
| 32 |
+
return (x >= 0).float()
|
| 33 |
+
|
| 34 |
+
@staticmethod
|
| 35 |
+
def backward(ctx, grad_output):
|
| 36 |
+
return grad_output
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def heaviside_ste(x: torch.Tensor) -> torch.Tensor:
|
| 40 |
+
"""Heaviside with STE gradient."""
|
| 41 |
+
return HeavisideSTE.apply(x)
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
class FrozenThresholdCircuits(nn.Module):
|
| 45 |
+
"""Wrapper for frozen threshold logic circuits."""
|
| 46 |
+
|
| 47 |
+
def __init__(self, model_path: str = MODEL_PATH, device: str = 'cuda'):
|
| 48 |
+
super().__init__()
|
| 49 |
+
self.device = device
|
| 50 |
+
self.weights = {}
|
| 51 |
+
self._load_weights(model_path)
|
| 52 |
+
|
| 53 |
+
def _load_weights(self, path: str):
|
| 54 |
+
"""Load weights from safetensors file."""
|
| 55 |
+
with safe_open(path, framework='pt') as f:
|
| 56 |
+
for name in f.keys():
|
| 57 |
+
tensor = f.get_tensor(name).to(self.device).float()
|
| 58 |
+
self.weights[name] = tensor
|
| 59 |
+
|
| 60 |
+
def _gate(self, inputs: torch.Tensor, weight: torch.Tensor, bias: torch.Tensor) -> torch.Tensor:
|
| 61 |
+
"""Execute single threshold gate with STE."""
|
| 62 |
+
weight = weight.view(-1)
|
| 63 |
+
bias = bias.view(-1)
|
| 64 |
+
pre_activation = (inputs * weight).sum(dim=-1) + bias
|
| 65 |
+
return heaviside_ste(pre_activation)
|
| 66 |
+
|
| 67 |
+
def _xor(self, a: torch.Tensor, b: torch.Tensor, prefix: str) -> torch.Tensor:
|
| 68 |
+
"""XOR via OR-NAND-AND pattern (2 layers)."""
|
| 69 |
+
inputs = torch.stack([a, b], dim=-1)
|
| 70 |
+
|
| 71 |
+
w_or = self.weights[f'{prefix}.layer1.or.weight']
|
| 72 |
+
b_or = self.weights[f'{prefix}.layer1.or.bias']
|
| 73 |
+
w_nand = self.weights[f'{prefix}.layer1.nand.weight']
|
| 74 |
+
b_nand = self.weights[f'{prefix}.layer1.nand.bias']
|
| 75 |
+
|
| 76 |
+
h_or = self._gate(inputs, w_or, b_or)
|
| 77 |
+
h_nand = self._gate(inputs, w_nand, b_nand)
|
| 78 |
+
|
| 79 |
+
hidden = torch.stack([h_or, h_nand], dim=-1)
|
| 80 |
+
w2 = self.weights[f'{prefix}.layer2.weight']
|
| 81 |
+
b2 = self.weights[f'{prefix}.layer2.bias']
|
| 82 |
+
|
| 83 |
+
return self._gate(hidden, w2, b2)
|
| 84 |
+
|
| 85 |
+
def _full_adder(self, a: torch.Tensor, b: torch.Tensor, cin: torch.Tensor,
|
| 86 |
+
prefix: str) -> Tuple[torch.Tensor, torch.Tensor]:
|
| 87 |
+
"""Full adder: sum and carry out."""
|
| 88 |
+
ha1_sum = self._xor(a, b, f'{prefix}.ha1.sum')
|
| 89 |
+
|
| 90 |
+
inp_carry1 = torch.stack([a, b], dim=-1)
|
| 91 |
+
w_c1 = self.weights[f'{prefix}.ha1.carry.weight']
|
| 92 |
+
b_c1 = self.weights[f'{prefix}.ha1.carry.bias']
|
| 93 |
+
ha1_carry = self._gate(inp_carry1, w_c1, b_c1)
|
| 94 |
+
|
| 95 |
+
ha2_sum = self._xor(ha1_sum, cin, f'{prefix}.ha2.sum')
|
| 96 |
+
|
| 97 |
+
inp_carry2 = torch.stack([ha1_sum, cin], dim=-1)
|
| 98 |
+
w_c2 = self.weights[f'{prefix}.ha2.carry.weight']
|
| 99 |
+
b_c2 = self.weights[f'{prefix}.ha2.carry.bias']
|
| 100 |
+
ha2_carry = self._gate(inp_carry2, w_c2, b_c2)
|
| 101 |
+
|
| 102 |
+
inp_cout = torch.stack([ha1_carry, ha2_carry], dim=-1)
|
| 103 |
+
w_cout = self.weights[f'{prefix}.carry_or.weight']
|
| 104 |
+
b_cout = self.weights[f'{prefix}.carry_or.bias']
|
| 105 |
+
cout = self._gate(inp_cout, w_cout, b_cout)
|
| 106 |
+
|
| 107 |
+
return ha2_sum, cout
|
| 108 |
+
|
| 109 |
+
def add_8bit(self, a_bits: torch.Tensor, b_bits: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
|
| 110 |
+
"""8-bit ripple carry addition."""
|
| 111 |
+
batch_size = a_bits.shape[0]
|
| 112 |
+
carry = torch.zeros(batch_size, device=self.device)
|
| 113 |
+
result_bits = []
|
| 114 |
+
|
| 115 |
+
for bit in range(8):
|
| 116 |
+
bit_idx = 7 - bit
|
| 117 |
+
s, carry = self._full_adder(
|
| 118 |
+
a_bits[:, bit_idx],
|
| 119 |
+
b_bits[:, bit_idx],
|
| 120 |
+
carry,
|
| 121 |
+
f'arithmetic.ripplecarry8bit.fa{bit}'
|
| 122 |
+
)
|
| 123 |
+
result_bits.insert(0, s)
|
| 124 |
+
|
| 125 |
+
result = torch.stack(result_bits, dim=1)
|
| 126 |
+
return result, carry
|
| 127 |
+
|
| 128 |
+
def sub_8bit(self, a_bits: torch.Tensor, b_bits: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
|
| 129 |
+
"""8-bit subtraction by two's complement, A - B = A + ~B + 1."""
|
| 130 |
+
b_inv = 1.0 - b_bits
|
| 131 |
+
batch_size = a_bits.shape[0]
|
| 132 |
+
carry = torch.ones(batch_size, device=self.device)
|
| 133 |
+
result_bits = []
|
| 134 |
+
|
| 135 |
+
for bit in range(8):
|
| 136 |
+
bit_idx = 7 - bit
|
| 137 |
+
s, carry = self._full_adder(
|
| 138 |
+
a_bits[:, bit_idx],
|
| 139 |
+
b_inv[:, bit_idx],
|
| 140 |
+
carry,
|
| 141 |
+
f'arithmetic.ripplecarry8bit.fa{bit}'
|
| 142 |
+
)
|
| 143 |
+
result_bits.insert(0, s)
|
| 144 |
+
|
| 145 |
+
result = torch.stack(result_bits, dim=1)
|
| 146 |
+
borrow = 1.0 - carry
|
| 147 |
+
return result, borrow
|
| 148 |
+
|
| 149 |
+
def mul_8bit(self, a_bits: torch.Tensor, b_bits: torch.Tensor) -> torch.Tensor:
|
| 150 |
+
"""8-bit multiplication via shift-add (software implementation using adder circuits)."""
|
| 151 |
+
batch_size = a_bits.shape[0]
|
| 152 |
+
|
| 153 |
+
acc = torch.zeros(batch_size, 8, device=self.device)
|
| 154 |
+
|
| 155 |
+
for i in range(8):
|
| 156 |
+
b_bit = b_bits[:, 7 - i]
|
| 157 |
+
pp = a_bits * b_bit.unsqueeze(1)
|
| 158 |
+
|
| 159 |
+
shifted_pp = torch.zeros(batch_size, 8, device=self.device)
|
| 160 |
+
for j in range(8):
|
| 161 |
+
dst_idx = j + i
|
| 162 |
+
if dst_idx < 8:
|
| 163 |
+
shifted_pp[:, 7 - dst_idx] = pp[:, 7 - j]
|
| 164 |
+
|
| 165 |
+
acc, _ = self.add_8bit(acc, shifted_pp)
|
| 166 |
+
|
| 167 |
+
return acc
|
| 168 |
+
|
| 169 |
+
def compare_gt(self, a_bits: torch.Tensor, b_bits: torch.Tensor) -> torch.Tensor:
|
| 170 |
+
"""A > B comparison."""
|
| 171 |
+
inputs = torch.cat([a_bits, b_bits], dim=-1)
|
| 172 |
+
w = self.weights['arithmetic.greaterthan8bit.weight'].view(-1)
|
| 173 |
+
b = self.weights['arithmetic.greaterthan8bit.bias'].view(-1)
|
| 174 |
+
return heaviside_ste((inputs * w).sum(dim=-1) + b)
|
| 175 |
+
|
| 176 |
+
def compare_lt(self, a_bits: torch.Tensor, b_bits: torch.Tensor) -> torch.Tensor:
|
| 177 |
+
"""A < B comparison."""
|
| 178 |
+
inputs = torch.cat([a_bits, b_bits], dim=-1)
|
| 179 |
+
w = self.weights['arithmetic.lessthan8bit.weight'].view(-1)
|
| 180 |
+
b = self.weights['arithmetic.lessthan8bit.bias'].view(-1)
|
| 181 |
+
return heaviside_ste((inputs * w).sum(dim=-1) + b)
|
| 182 |
+
|
| 183 |
+
def compare_eq(self, a_bits: torch.Tensor, b_bits: torch.Tensor) -> torch.Tensor:
|
| 184 |
+
"""A == B comparison (two-layer)."""
|
| 185 |
+
inputs = torch.cat([a_bits, b_bits], dim=-1)
|
| 186 |
+
prefix = 'arithmetic.equality8bit'
|
| 187 |
+
|
| 188 |
+
w_geq = self.weights[f'{prefix}.layer1.geq.weight'].view(-1)
|
| 189 |
+
b_geq = self.weights[f'{prefix}.layer1.geq.bias'].view(-1)
|
| 190 |
+
w_leq = self.weights[f'{prefix}.layer1.leq.weight'].view(-1)
|
| 191 |
+
b_leq = self.weights[f'{prefix}.layer1.leq.bias'].view(-1)
|
| 192 |
+
|
| 193 |
+
h_geq = heaviside_ste((inputs * w_geq).sum(dim=-1) + b_geq)
|
| 194 |
+
h_leq = heaviside_ste((inputs * w_leq).sum(dim=-1) + b_leq)
|
| 195 |
+
|
| 196 |
+
hidden = torch.stack([h_geq, h_leq], dim=-1)
|
| 197 |
+
w2 = self.weights[f'{prefix}.layer2.weight'].view(-1)
|
| 198 |
+
b2 = self.weights[f'{prefix}.layer2.bias'].view(-1)
|
| 199 |
+
|
| 200 |
+
return heaviside_ste((hidden * w2).sum(dim=-1) + b2)
|
| 201 |
+
|
| 202 |
+
def forward(self, a_bits: torch.Tensor, b_bits: torch.Tensor,
|
| 203 |
+
op_onehot: torch.Tensor) -> torch.Tensor:
|
| 204 |
+
"""Execute operation based on one-hot selector."""
|
| 205 |
+
batch_size = a_bits.shape[0]
|
| 206 |
+
|
| 207 |
+
add_result, _ = self.add_8bit(a_bits, b_bits)
|
| 208 |
+
sub_result, _ = self.sub_8bit(a_bits, b_bits)
|
| 209 |
+
mul_result = self.mul_8bit(a_bits, b_bits)
|
| 210 |
+
|
| 211 |
+
gt_result = self.compare_gt(a_bits, b_bits)
|
| 212 |
+
lt_result = self.compare_lt(a_bits, b_bits)
|
| 213 |
+
eq_result = self.compare_eq(a_bits, b_bits)
|
| 214 |
+
|
| 215 |
+
cmp_expanded = torch.zeros(batch_size, 8, device=self.device)
|
| 216 |
+
|
| 217 |
+
gt_expanded = cmp_expanded.clone()
|
| 218 |
+
gt_expanded[:, 7] = gt_result
|
| 219 |
+
|
| 220 |
+
lt_expanded = cmp_expanded.clone()
|
| 221 |
+
lt_expanded[:, 7] = lt_result
|
| 222 |
+
|
| 223 |
+
eq_expanded = cmp_expanded.clone()
|
| 224 |
+
eq_expanded[:, 7] = eq_result
|
| 225 |
+
|
| 226 |
+
results = torch.stack([
|
| 227 |
+
add_result,
|
| 228 |
+
sub_result,
|
| 229 |
+
mul_result,
|
| 230 |
+
gt_expanded,
|
| 231 |
+
lt_expanded,
|
| 232 |
+
eq_expanded
|
| 233 |
+
], dim=1)
|
| 234 |
+
|
| 235 |
+
op_weights = op_onehot.unsqueeze(-1)
|
| 236 |
+
output = (results * op_weights).sum(dim=1)
|
| 237 |
+
|
| 238 |
+
return output
|
| 239 |
|
|
|
|
| 240 |
OPERATIONS = ['add', 'sub', 'mul', 'gt', 'lt', 'eq']
|
| 241 |
+
|
| 242 |
+
def ground_truth(a: int, b: int, op: str) -> int:
|
| 243 |
+
"""Compute expected result (8-bit arithmetic)."""
|
| 244 |
+
if op == 'add':
|
| 245 |
+
return (a + b) & 0xFF
|
| 246 |
+
elif op == 'sub':
|
| 247 |
+
return (a - b) & 0xFF
|
| 248 |
+
elif op == 'mul':
|
| 249 |
+
return (a * b) & 0xFF
|
| 250 |
+
elif op == 'gt':
|
| 251 |
+
return 1 if a > b else 0
|
| 252 |
+
elif op == 'lt':
|
| 253 |
+
return 1 if a < b else 0
|
| 254 |
+
elif op == 'eq':
|
| 255 |
+
return 1 if a == b else 0
|
| 256 |
+
else:
|
| 257 |
+
raise ValueError(f"Unknown op: {op}")
|
| 258 |
+
|
| 259 |
+
|
| 260 |
+
def int_to_bits(val: int, n_bits: int = 8) -> torch.Tensor:
|
| 261 |
+
"""Convert integer to bit tensor (MSB first)."""
|
| 262 |
+
bits = torch.zeros(n_bits)
|
| 263 |
+
for i in range(n_bits):
|
| 264 |
+
bits[n_bits - 1 - i] = (val >> i) & 1
|
| 265 |
+
return bits
|
| 266 |
+
|
| 267 |
+
|
| 268 |
+
def bits_to_int(bits: torch.Tensor) -> int:
|
| 269 |
+
"""Convert bit tensor to integer (MSB first)."""
|
| 270 |
+
val = 0
|
| 271 |
+
n_bits = bits.shape[-1]
|
| 272 |
+
for i in range(n_bits):
|
| 273 |
+
val += int(bits[..., i].item()) << (n_bits - 1 - i)
|
| 274 |
+
return val
|
| 275 |
+
|
| 276 |
+
|
| 277 |
+
def op_to_idx(op: str) -> int:
|
| 278 |
+
"""Convert operation string to index."""
|
| 279 |
+
return OPERATIONS.index(op)
|
| 280 |
+
|
| 281 |
+
|
| 282 |
+
def idx_to_op(idx: int) -> str:
|
| 283 |
+
"""Convert index to operation string."""
|
| 284 |
+
return OPERATIONS[idx]
|
| 285 |
+
|
| 286 |
+
|
| 287 |
+
def generate_batch(batch_size: int, device: str = 'cuda') -> Dict[str, torch.Tensor]:
|
| 288 |
+
"""Generate a batch of random arithmetic problems."""
|
| 289 |
+
a_vals = torch.randint(0, 256, (batch_size,), device=device)
|
| 290 |
+
b_vals = torch.randint(0, 256, (batch_size,), device=device)
|
| 291 |
+
op_indices = torch.randint(0, len(OPERATIONS), (batch_size,), device=device)
|
| 292 |
+
|
| 293 |
+
a_bits = torch.zeros(batch_size, 8, device=device)
|
| 294 |
+
b_bits = torch.zeros(batch_size, 8, device=device)
|
| 295 |
+
for i in range(8):
|
| 296 |
+
a_bits[:, 7-i] = (a_vals >> i) & 1
|
| 297 |
+
b_bits[:, 7-i] = (b_vals >> i) & 1
|
| 298 |
+
|
| 299 |
+
op_onehot = torch.zeros(batch_size, len(OPERATIONS), device=device)
|
| 300 |
+
op_onehot.scatter_(1, op_indices.unsqueeze(1), 1.0)
|
| 301 |
+
|
| 302 |
+
expected = torch.zeros(batch_size, dtype=torch.long, device=device)
|
| 303 |
+
for i in range(batch_size):
|
| 304 |
+
a, b, op_idx = a_vals[i].item(), b_vals[i].item(), op_indices[i].item()
|
| 305 |
+
expected[i] = ground_truth(a, b, idx_to_op(op_idx))
|
| 306 |
+
|
| 307 |
+
expected_bits = torch.zeros(batch_size, 8, device=device)
|
| 308 |
+
for i in range(8):
|
| 309 |
+
expected_bits[:, 7-i] = (expected >> i) & 1
|
| 310 |
+
|
| 311 |
+
return {
|
| 312 |
+
'a': a_vals,
|
| 313 |
+
'b': b_vals,
|
| 314 |
+
'op': op_indices,
|
| 315 |
+
'a_bits': a_bits.float(),
|
| 316 |
+
'b_bits': b_bits.float(),
|
| 317 |
+
'op_onehot': op_onehot.float(),
|
| 318 |
+
'expected': expected,
|
| 319 |
+
'expected_bits': expected_bits.float(),
|
| 320 |
+
}
|
| 321 |
+
|
| 322 |
+
|
| 323 |
+
def compute_fitness(
|
| 324 |
+
model_fn: Callable[[torch.Tensor, torch.Tensor, torch.Tensor], torch.Tensor],
|
| 325 |
+
n_samples: int = 10000,
|
| 326 |
+
batch_size: int = 256,
|
| 327 |
+
device: str = 'cuda',
|
| 328 |
+
return_details: bool = False
|
| 329 |
+
) -> float | Tuple[float, Dict]:
|
| 330 |
+
"""Compute fitness score for a model."""
|
| 331 |
+
correct = 0
|
| 332 |
+
total = 0
|
| 333 |
+
op_correct = {op: 0 for op in OPERATIONS}
|
| 334 |
+
op_total = {op: 0 for op in OPERATIONS}
|
| 335 |
+
|
| 336 |
+
for _ in range(0, n_samples, batch_size):
|
| 337 |
+
actual_batch = min(batch_size, n_samples - total)
|
| 338 |
+
batch = generate_batch(actual_batch, device)
|
| 339 |
+
|
| 340 |
+
with torch.no_grad():
|
| 341 |
+
pred_bits = model_fn(batch['a_bits'], batch['b_bits'], batch['op_onehot'])
|
| 342 |
+
|
| 343 |
+
pred_bits_binary = (pred_bits > 0.5).float()
|
| 344 |
+
|
| 345 |
+
for i in range(actual_batch):
|
| 346 |
+
pred_val = 0
|
| 347 |
+
for j in range(8):
|
| 348 |
+
pred_val += int(pred_bits_binary[i, j].item()) << (7 - j)
|
| 349 |
+
|
| 350 |
+
expected_val = batch['expected'][i].item()
|
| 351 |
+
op_name = idx_to_op(batch['op'][i].item())
|
| 352 |
+
|
| 353 |
+
op_total[op_name] += 1
|
| 354 |
+
total += 1
|
| 355 |
+
|
| 356 |
+
if pred_val == expected_val:
|
| 357 |
+
correct += 1
|
| 358 |
+
op_correct[op_name] += 1
|
| 359 |
+
|
| 360 |
+
fitness = correct / total if total > 0 else 0.0
|
| 361 |
+
|
| 362 |
+
if return_details:
|
| 363 |
+
details = {
|
| 364 |
+
'correct': correct,
|
| 365 |
+
'total': total,
|
| 366 |
+
'by_op': {
|
| 367 |
+
op: {
|
| 368 |
+
'correct': op_correct[op],
|
| 369 |
+
'total': op_total[op],
|
| 370 |
+
'accuracy': op_correct[op] / op_total[op] if op_total[op] > 0 else 0.0
|
| 371 |
+
}
|
| 372 |
+
for op in OPERATIONS
|
| 373 |
+
}
|
| 374 |
+
}
|
| 375 |
+
return fitness, details
|
| 376 |
+
|
| 377 |
+
return fitness
|
| 378 |
+
|
| 379 |
+
|
| 380 |
+
def compute_bit_accuracy(pred_bits: torch.Tensor, expected_bits: torch.Tensor) -> float:
|
| 381 |
+
"""Compute per-bit accuracy (for gradient signal analysis)."""
|
| 382 |
+
pred_binary = (pred_bits > 0.5).float()
|
| 383 |
+
return (pred_binary == expected_bits).float().mean().item()
|
| 384 |
+
|
| 385 |
+
|
| 386 |
+
def compute_loss(pred_bits: torch.Tensor, expected_bits: torch.Tensor) -> torch.Tensor:
|
| 387 |
+
"""Binary cross-entropy loss on output bits."""
|
| 388 |
+
pred_clamped = pred_bits.clamp(1e-7, 1 - 1e-7)
|
| 389 |
+
return -((expected_bits * torch.log(pred_clamped) +
|
| 390 |
+
(1 - expected_bits) * torch.log(1 - pred_clamped))).mean()
|
| 391 |
+
|
| 392 |
+
|
| 393 |
+
MODEL_ID = 'HuggingFaceTB/SmolLM2-360M-Instruct'
|
| 394 |
+
|
| 395 |
OP_SYMBOLS = {'add': '+', 'sub': '-', 'mul': '*', 'gt': '>', 'lt': '<', 'eq': '=='}
|
| 396 |
|
| 397 |
|
| 398 |
class BitEncoder(nn.Module):
|
| 399 |
+
"""Encodes two 8-bit operands from input representation."""
|
|
|
|
|
|
|
|
|
|
| 400 |
|
| 401 |
def __init__(self, input_dim: int = 16 + 6, hidden_dim: int = 32):
|
| 402 |
super().__init__()
|
|
|
|
| 408 |
self.scale = nn.Parameter(torch.tensor(0.0))
|
| 409 |
|
| 410 |
def forward(self, x: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
|
| 411 |
+
"""Split x, whose first 16 features are the bits of a and b, into the two operands."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 412 |
base_bits = x[:, :16]
|
| 413 |
refinement = self.refine(x) * torch.sigmoid(self.scale)
|
| 414 |
bits = base_bits + refinement
|
|
|
|
| 420 |
|
| 421 |
|
| 422 |
class OpRouter(nn.Module):
|
| 423 |
+
"""Routes computation to the appropriate circuit based on input."""
|
|
|
|
|
|
|
|
|
|
| 424 |
|
| 425 |
def __init__(self, input_dim: int = 16 + 6, hidden_dim: int = 32, n_ops: int = 6):
|
| 426 |
super().__init__()
|
|
|
|
| 431 |
)
|
| 432 |
|
| 433 |
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 434 |
+
"""Route each input row to the circuit its features select."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 435 |
logits = self.net(x)
|
| 436 |
return F.softmax(logits, dim=-1)
|
| 437 |
|
| 438 |
|
| 439 |
class BitDecoder(nn.Module):
|
| 440 |
+
"""Decodes circuit output bits to target representation."""
|
|
|
|
|
|
|
|
|
|
|
|
|
| 441 |
|
| 442 |
def __init__(self, output_dim: int = 8):
|
| 443 |
super().__init__()
|
|
|
|
| 448 |
|
| 449 |
|
| 450 |
class ThresholdALU(nn.Module):
|
| 451 |
+
"""Complete trainable interface + frozen circuits."""
|
|
|
|
|
|
|
|
|
|
| 452 |
|
| 453 |
def __init__(self, device: str = 'cuda'):
|
| 454 |
super().__init__()
|
|
|
|
| 465 |
|
| 466 |
def forward(self, a_bits_in: torch.Tensor, b_bits_in: torch.Tensor,
|
| 467 |
op_onehot: torch.Tensor) -> torch.Tensor:
|
| 468 |
+
"""Forward pass through trainable interface + frozen circuits."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 469 |
x = torch.cat([a_bits_in, b_bits_in, op_onehot], dim=-1)
|
| 470 |
|
| 471 |
a_bits, b_bits = self.encoder(x)
|
|
|
|
| 487 |
|
| 488 |
|
| 489 |
class DirectCircuitModel(nn.Module):
|
| 490 |
+
"""Minimal model that directly uses circuits without learned encoding."""
|
|
|
|
|
|
|
|
|
|
| 491 |
|
| 492 |
def __init__(self, device: str = 'cuda'):
|
| 493 |
super().__init__()
|
|
|
|
| 500 |
|
| 501 |
|
| 502 |
class HiddenStateExtractor(nn.Module):
|
| 503 |
+
"""Extracts operands and operation from LLM hidden states."""
|
|
|
|
|
|
|
|
|
|
| 504 |
|
| 505 |
def __init__(self, hidden_dim: int = 960, intermediate_dim: int = 256):
|
| 506 |
super().__init__()
|
|
|
|
| 524 |
)
|
| 525 |
|
| 526 |
def forward(self, hidden_states: torch.Tensor):
|
| 527 |
+
"""Extract the operand bits and the operation from the hidden states."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 528 |
a_logits = self.a_extractor(hidden_states)
|
| 529 |
b_logits = self.b_extractor(hidden_states)
|
| 530 |
op_logits = self.op_router(hidden_states)
|
|
|
|
| 542 |
|
| 543 |
|
| 544 |
class AttentionPooling(nn.Module):
|
| 545 |
+
"""Learnable attention pooling over sequence positions."""
|
|
|
|
|
|
|
|
|
|
| 546 |
|
| 547 |
def __init__(self, hidden_dim: int = 960, num_heads: int = 4):
|
| 548 |
super().__init__()
|
|
|
|
| 557 |
self.cls_token = nn.Parameter(torch.randn(1, 1, hidden_dim) * 0.02)
|
| 558 |
|
| 559 |
def forward(self, embeddings: torch.Tensor, mask: torch.Tensor) -> torch.Tensor:
|
| 560 |
+
"""Pool the embeddings over the positions the mask attends."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 561 |
batch_size, seq_len, hidden_dim = embeddings.shape
|
| 562 |
|
| 563 |
cls_expanded = self.cls_token.expand(batch_size, -1, -1)
|
|
|
|
| 592 |
|
| 593 |
|
| 594 |
class MultiHeadBitExtractor(nn.Module):
|
| 595 |
+
"""8 separate extractors for 8 bits - each bit gets its own specialized network."""
|
|
|
|
|
|
|
|
|
|
| 596 |
|
| 597 |
def __init__(self, hidden_dim: int = 960, intermediate_dim: int = 128):
|
| 598 |
super().__init__()
|
|
|
|
| 607 |
])
|
| 608 |
|
| 609 |
def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
|
| 610 |
+
"""Predict the eight bits of each operand from the hidden states."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 611 |
hidden_states = torch.nan_to_num(hidden_states, nan=0.0)
|
| 612 |
|
| 613 |
bit_logits = [extractor(hidden_states) for extractor in self.bit_extractors]
|
|
|
|
| 622 |
|
| 623 |
|
| 624 |
class Extractor(nn.Module):
|
| 625 |
+
"""Extracts operands and operation from LLM hidden states."""
|
|
|
|
|
|
|
|
|
|
| 626 |
|
| 627 |
def __init__(self, hidden_dim: int = 960, intermediate_dim: int = 256, num_heads: int = 4):
|
| 628 |
super().__init__()
|
|
|
|
| 639 |
)
|
| 640 |
|
| 641 |
def forward(self, embeddings: torch.Tensor, mask: torch.Tensor):
|
| 642 |
+
"""Extract the operands and the operation from the pooled sequence."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 643 |
pooled = self.attention_pool(embeddings, mask)
|
| 644 |
|
| 645 |
a_bits, _ = self.a_extractor(pooled)
|
|
|
|
| 650 |
|
| 651 |
|
| 652 |
class PositionExtractor(nn.Module):
|
| 653 |
+
"""Position-specific extraction with dynamic operator detection."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 654 |
|
| 655 |
DIGIT_TOKENS = set(range(32, 42))
|
| 656 |
OPERATOR_TOKENS = {
|
|
|
|
| 691 |
)
|
| 692 |
|
| 693 |
def _find_operator_position(self, token_ids: torch.Tensor) -> tuple[int, int]:
|
| 694 |
+
"""Find operator token position and its operation index."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 695 |
for pos, tid in enumerate(token_ids.tolist()):
|
| 696 |
if tid in self.OPERATOR_TOKENS:
|
| 697 |
return pos, self.OPERATOR_TOKENS[tid]
|
| 698 |
return -1, -1
|
| 699 |
|
| 700 |
def _extract_digit_features(self, hidden: torch.Tensor, start: int, end: int) -> torch.Tensor:
|
| 701 |
+
"""Extract and pad digit hidden states to fixed size."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 702 |
n_digits = end - start
|
| 703 |
features = torch.zeros(self.MAX_DIGITS * self.hidden_dim, device=hidden.device)
|
| 704 |
|
|
|
|
| 710 |
return features
|
| 711 |
|
| 712 |
def forward(self, hidden: torch.Tensor, mask: torch.Tensor, token_ids: torch.Tensor = None):
|
| 713 |
+
"""Extract the operands at the positions around the detected operator token."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 714 |
if token_ids is None:
|
| 715 |
raise ValueError("PositionExtractor requires token_ids for operator detection")
|
| 716 |
|
|
|
|
| 771 |
|
| 772 |
|
| 773 |
class PositionalDigitExtractor(nn.Module):
|
| 774 |
+
"""Position-aware digit extraction: classifies each digit position independently."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 775 |
|
| 776 |
DIGIT_TOKENS = set(range(32, 42))
|
| 777 |
OPERATOR_TOKENS = {
|
|
|
|
| 849 |
return torch.stack(bits)
|
| 850 |
|
| 851 |
def forward(self, hidden: torch.Tensor, mask: torch.Tensor, token_ids: torch.Tensor):
|
| 852 |
+
"""Classify each digit position of both operands."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 853 |
batch_size = hidden.shape[0]
|
| 854 |
device = hidden.device
|
| 855 |
|
|
|
|
| 903 |
|
| 904 |
|
| 905 |
class DigitExtractor(nn.Module):
|
| 906 |
+
"""Digit-level extraction: predicts digits (0-9) then converts to bits."""
|
|
|
|
|
|
|
|
|
|
| 907 |
|
| 908 |
def __init__(self, hidden_dim: int = 960, intermediate_dim: int = 256, num_heads: int = 4):
|
| 909 |
super().__init__()
|
|
|
|
| 929 |
)
|
| 930 |
|
| 931 |
def digits_to_bits(self, digit_logits: torch.Tensor) -> torch.Tensor:
|
| 932 |
+
"""Convert the digit logits of three digits, [batch, 30], to 8-bit operands."""
|
|
|
|
|
|
|
|
|
|
|
|
|
| 933 |
batch_size = digit_logits.shape[0]
|
| 934 |
|
| 935 |
logits = digit_logits.view(batch_size, 3, 10)
|
|
|
|
| 953 |
return torch.stack(bits, dim=-1)
|
| 954 |
|
| 955 |
def forward(self, hidden: torch.Tensor, mask: torch.Tensor):
|
| 956 |
+
"""Return the operand bits, the operation logits and the digit logits."""
|
|
|
|
|
|
|
|
|
|
| 957 |
pooled = self.attention_pool(hidden, mask)
|
| 958 |
|
| 959 |
a_digit_logits = self.a_digit_pred(pooled)
|
|
|
|
| 967 |
|
| 968 |
|
| 969 |
class HybridExtractor(nn.Module):
|
| 970 |
+
"""Hybrid extractor that handles both digit tokens and word numbers."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 971 |
|
| 972 |
DIGIT_TOKENS = set(range(32, 42))
|
| 973 |
SYMBOL_OP_TOKENS = {
|
|
|
|
| 1033 |
return -1
|
| 1034 |
|
| 1035 |
def _extract_from_digits(self, token_ids: torch.Tensor) -> tuple:
|
| 1036 |
+
"""Extract values directly from digit tokens (hardcoded lookup)."""
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1037 |
tokens = token_ids.tolist()
|
| 1038 |
|
| 1039 |
op_pos = -1
|
|
|
|
| 1082 |
return bits
|
| 1083 |
|
| 1084 |
def _digits_to_value_and_bits(self, digit_logits: torch.Tensor, device) -> tuple:
|
| 1085 |
+
"""Convert the 30 logits of three digits to a value and its 8 bits."""
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1086 |
logits = digit_logits.view(3, 10)
|
| 1087 |
probs = torch.softmax(logits, dim=-1)
|
| 1088 |
|
|
|
|
| 1105 |
return hundreds * 100 + tens * 10 + ones, torch.stack(bits)
|
| 1106 |
|
| 1107 |
def forward(self, hidden: torch.Tensor, mask: torch.Tensor, token_ids: torch.Tensor = None):
|
| 1108 |
+
"""Extract the operands from digit tokens by lookup, or from the hidden states."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1109 |
batch_size = hidden.shape[0]
|
| 1110 |
device = hidden.device
|
| 1111 |
|
|
|
|
| 1215 |
|
| 1216 |
|
| 1217 |
class ArithmeticModel(nn.Module):
|
| 1218 |
+
"""LLM + extractor + frozen threshold circuits."""
|
|
|
|
|
|
|
|
|
|
| 1219 |
|
| 1220 |
def __init__(self, device: str = 'cuda', unfreeze_layers: int = 0,
|
| 1221 |
extract_layer: int = -1, position_extract: bool = False,
|
|
|
|
| 1310 |
print(f" Total trainable: {total_trainable:,}", flush=True)
|
| 1311 |
|
| 1312 |
def get_hidden_states(self, texts: list[str]) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
| 1313 |
+
"""Get hidden states from specified layer."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1314 |
inputs = self.tokenizer(
|
| 1315 |
texts,
|
| 1316 |
return_tensors='pt',
|
|
|
|
| 1332 |
return hidden, mask, token_ids
|
| 1333 |
|
| 1334 |
def forward(self, texts: list[str]):
|
| 1335 |
+
"""Run text through the LLM, the extractor and the circuits to the result."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1336 |
hidden, mask, token_ids = self.get_hidden_states(texts)
|
| 1337 |
|
| 1338 |
if self.hybrid or self.positional_digit or self.position_extract:
|
|
|
|
| 1385 |
return params
|
| 1386 |
|
| 1387 |
|
| 1388 |
+
DEVICE = 'cuda'
|
| 1389 |
+
|
| 1390 |
+
ONES = ['zero', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine',
|
| 1391 |
+
'ten', 'eleven', 'twelve', 'thirteen', 'fourteen', 'fifteen', 'sixteen',
|
| 1392 |
+
'seventeen', 'eighteen', 'nineteen']
|
| 1393 |
+
|
| 1394 |
+
TENS = ['', '', 'twenty', 'thirty', 'forty', 'fifty', 'sixty', 'seventy', 'eighty', 'ninety']
|
| 1395 |
+
|
| 1396 |
+
def int_to_words(n: int) -> str:
|
| 1397 |
+
"""Convert integer 0-255 to English words."""
|
| 1398 |
+
if n < 0 or n > 255:
|
| 1399 |
+
return str(n)
|
| 1400 |
+
if n < 20:
|
| 1401 |
+
return ONES[n]
|
| 1402 |
+
if n < 100:
|
| 1403 |
+
if n % 10 == 0:
|
| 1404 |
+
return TENS[n // 10]
|
| 1405 |
+
return f"{TENS[n // 10]} {ONES[n % 10]}"
|
| 1406 |
+
if n % 100 == 0:
|
| 1407 |
+
return f"{ONES[n // 100]} hundred"
|
| 1408 |
+
if n % 100 < 20:
|
| 1409 |
+
return f"{ONES[n // 100]} hundred {ONES[n % 100]}"
|
| 1410 |
+
if n % 10 == 0:
|
| 1411 |
+
return f"{ONES[n // 100]} hundred {TENS[(n % 100) // 10]}"
|
| 1412 |
+
return f"{ONES[n // 100]} hundred {TENS[(n % 100) // 10]} {ONES[n % 10]}"
|
| 1413 |
+
|
| 1414 |
+
|
| 1415 |
+
def train_int_to_bits(val: int, device: str = 'cuda') -> torch.Tensor:
|
| 1416 |
+
bits = torch.zeros(8, device=device)
|
| 1417 |
+
for i in range(8):
|
| 1418 |
+
bits[7-i] = (val >> i) & 1
|
| 1419 |
+
return bits
|
| 1420 |
+
|
| 1421 |
+
|
| 1422 |
+
def train_bits_to_int(bits: torch.Tensor) -> int:
|
| 1423 |
+
val = 0
|
| 1424 |
+
for i in range(8):
|
| 1425 |
+
if bits[i].item() > 0.5:
|
| 1426 |
+
val += 1 << (7-i)
|
| 1427 |
+
return val
|
| 1428 |
+
|
| 1429 |
+
|
| 1430 |
+
NL_TEMPLATES = {
|
| 1431 |
+
'add': [
|
| 1432 |
+
"What is {a} plus {b}?",
|
| 1433 |
+
"Calculate {a} + {b}",
|
| 1434 |
+
"Add {a} and {b}",
|
| 1435 |
+
"What's the sum of {a} and {b}?",
|
| 1436 |
+
"If I have {a} and get {b} more, how many total?",
|
| 1437 |
+
"{a} + {b} = ?",
|
| 1438 |
+
"Compute {a} plus {b}",
|
| 1439 |
+
],
|
| 1440 |
+
'sub': [
|
| 1441 |
+
"What is {a} minus {b}?",
|
| 1442 |
+
"Calculate {a} - {b}",
|
| 1443 |
+
"Subtract {b} from {a}",
|
| 1444 |
+
"What's {a} take away {b}?",
|
| 1445 |
+
"If I have {a} and lose {b}, how many left?",
|
| 1446 |
+
"{a} - {b} = ?",
|
| 1447 |
+
"Compute {a} minus {b}",
|
| 1448 |
+
],
|
| 1449 |
+
'mul': [
|
| 1450 |
+
"What is {a} times {b}?",
|
| 1451 |
+
"Calculate {a} * {b}",
|
| 1452 |
+
"Multiply {a} by {b}",
|
| 1453 |
+
"What's {a} multiplied by {b}?",
|
| 1454 |
+
"{a} * {b} = ?",
|
| 1455 |
+
"Compute {a} times {b}",
|
| 1456 |
+
"What is the product of {a} and {b}?",
|
| 1457 |
+
],
|
| 1458 |
+
'gt': [
|
| 1459 |
+
"Is {a} greater than {b}?",
|
| 1460 |
+
"Is {a} > {b}?",
|
| 1461 |
+
"Check if {a} is larger than {b}",
|
| 1462 |
+
"Compare: is {a} more than {b}?",
|
| 1463 |
+
"{a} > {b}?",
|
| 1464 |
+
],
|
| 1465 |
+
'lt': [
|
| 1466 |
+
"Is {a} less than {b}?",
|
| 1467 |
+
"Is {a} < {b}?",
|
| 1468 |
+
"Check if {a} is smaller than {b}",
|
| 1469 |
+
"Compare: is {a} fewer than {b}?",
|
| 1470 |
+
"{a} < {b}?",
|
| 1471 |
+
],
|
| 1472 |
+
'eq': [
|
| 1473 |
+
"Is {a} equal to {b}?",
|
| 1474 |
+
"Is {a} == {b}?",
|
| 1475 |
+
"Does {a} equal {b}?",
|
| 1476 |
+
"Check if {a} equals {b}",
|
| 1477 |
+
"Are {a} and {b} the same?",
|
| 1478 |
+
],
|
| 1479 |
+
}
|
| 1480 |
+
|
| 1481 |
+
|
| 1482 |
+
def generate_problem(max_val: int = 255):
|
| 1483 |
+
"""Generate a random arithmetic problem for LLM training."""
|
| 1484 |
+
a = random.randint(0, max_val)
|
| 1485 |
+
b = random.randint(0, max_val)
|
| 1486 |
+
op = random.choice(OPERATIONS)
|
| 1487 |
+
|
| 1488 |
+
fmt = random.choice(['digits', 'words', 'nl_digits', 'nl_words'])
|
| 1489 |
+
|
| 1490 |
+
if fmt == 'digits':
|
| 1491 |
+
sym = OP_SYMBOLS[op]
|
| 1492 |
+
text = f"{a} {sym} {b}"
|
| 1493 |
+
elif fmt == 'words':
|
| 1494 |
+
a_word = int_to_words(a)
|
| 1495 |
+
b_word = int_to_words(b)
|
| 1496 |
+
op_word = {'add': 'plus', 'sub': 'minus', 'mul': 'times',
|
| 1497 |
+
'gt': 'greater than', 'lt': 'less than', 'eq': 'equals'}[op]
|
| 1498 |
+
text = f"{a_word} {op_word} {b_word}"
|
| 1499 |
+
elif fmt == 'nl_digits':
|
| 1500 |
+
template = random.choice(NL_TEMPLATES[op])
|
| 1501 |
+
text = template.format(a=a, b=b)
|
| 1502 |
+
else:
|
| 1503 |
+
template = random.choice(NL_TEMPLATES[op])
|
| 1504 |
+
text = template.format(a=int_to_words(a), b=int_to_words(b))
|
| 1505 |
+
|
| 1506 |
+
if op == 'add':
|
| 1507 |
+
result = (a + b) & 0xFF
|
| 1508 |
+
elif op == 'sub':
|
| 1509 |
+
result = (a - b) & 0xFF
|
| 1510 |
+
elif op == 'mul':
|
| 1511 |
+
result = (a * b) & 0xFF
|
| 1512 |
+
elif op == 'gt':
|
| 1513 |
+
result = 1 if a > b else 0
|
| 1514 |
+
elif op == 'lt':
|
| 1515 |
+
result = 1 if a < b else 0
|
| 1516 |
+
elif op == 'eq':
|
| 1517 |
+
result = 1 if a == b else 0
|
| 1518 |
+
|
| 1519 |
+
return text, a, b, op, result
|
| 1520 |
+
|
| 1521 |
+
|
| 1522 |
+
def get_curriculum_max(epoch: int, total_epochs: int) -> int:
|
| 1523 |
+
"""Curriculum learning: start with small numbers, gradually increase."""
|
| 1524 |
+
progress = epoch / total_epochs
|
| 1525 |
+
if progress < 0.2:
|
| 1526 |
+
return 9
|
| 1527 |
+
elif progress < 0.5:
|
| 1528 |
+
return 99
|
| 1529 |
+
else:
|
| 1530 |
+
return 255
|
| 1531 |
+
|
| 1532 |
+
|
| 1533 |
+
def train_router(epochs: int = 100, batch_size: int = 256, lr: float = 1e-2, device: str = 'cuda'):
|
| 1534 |
+
"""Train only the router with ground truth bits."""
|
| 1535 |
+
print("=" * 70)
|
| 1536 |
+
print(" ROUTER-ONLY TRAINING (Ground Truth Bits)")
|
| 1537 |
+
print("=" * 70)
|
| 1538 |
+
|
| 1539 |
+
circuits = FrozenThresholdCircuits(device=device)
|
| 1540 |
+
router = OpRouter(input_dim=16 + 6, hidden_dim=64, n_ops=6).to(device)
|
| 1541 |
+
|
| 1542 |
+
print(f"\nRouter parameters: {sum(p.numel() for p in router.parameters()):,}")
|
| 1543 |
+
|
| 1544 |
+
def model_fn(a_bits, b_bits, op_onehot):
|
| 1545 |
+
x = torch.cat([a_bits, b_bits, op_onehot], dim=-1)
|
| 1546 |
+
op_weights = router(x)
|
| 1547 |
+
return circuits(a_bits, b_bits, op_weights)
|
| 1548 |
+
|
| 1549 |
+
initial_fitness = compute_fitness(model_fn, n_samples=1000, device=device)
|
| 1550 |
+
print(f"Initial fitness: {initial_fitness:.4f}")
|
| 1551 |
+
|
| 1552 |
+
optimizer = optim.AdamW(router.parameters(), lr=lr)
|
| 1553 |
+
scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=epochs)
|
| 1554 |
+
|
| 1555 |
+
print("\nTraining...")
|
| 1556 |
+
print("-" * 70)
|
| 1557 |
+
|
| 1558 |
+
best_fitness = initial_fitness
|
| 1559 |
+
start_time = time.perf_counter()
|
| 1560 |
+
|
| 1561 |
+
for epoch in range(epochs):
|
| 1562 |
+
router.train()
|
| 1563 |
+
epoch_loss = 0.0
|
| 1564 |
+
|
| 1565 |
+
for _ in range(100):
|
| 1566 |
+
batch = generate_batch(batch_size, device)
|
| 1567 |
+
|
| 1568 |
+
optimizer.zero_grad()
|
| 1569 |
+
|
| 1570 |
+
x = torch.cat([batch['a_bits'], batch['b_bits'], batch['op_onehot']], dim=-1)
|
| 1571 |
+
op_weights = router(x)
|
| 1572 |
+
pred_bits = circuits(batch['a_bits'], batch['b_bits'], op_weights)
|
| 1573 |
+
|
| 1574 |
+
loss = compute_loss(pred_bits, batch['expected_bits'])
|
| 1575 |
+
loss.backward()
|
| 1576 |
+
optimizer.step()
|
| 1577 |
+
|
| 1578 |
+
epoch_loss += loss.item()
|
| 1579 |
+
|
| 1580 |
+
scheduler.step()
|
| 1581 |
+
|
| 1582 |
+
if (epoch + 1) % 10 == 0 or epoch == 0:
|
| 1583 |
+
router.eval()
|
| 1584 |
+
fitness, details = compute_fitness(model_fn, n_samples=2000, device=device, return_details=True)
|
| 1585 |
+
elapsed = time.perf_counter() - start_time
|
| 1586 |
+
|
| 1587 |
+
if fitness > best_fitness:
|
| 1588 |
+
best_fitness = fitness
|
| 1589 |
+
marker = " *"
|
| 1590 |
+
else:
|
| 1591 |
+
marker = ""
|
| 1592 |
+
|
| 1593 |
+
print(f"Epoch {epoch+1:3d} | Loss: {epoch_loss/100:.4f} | "
|
| 1594 |
+
f"Fitness: {fitness:.4f}{marker} | Time: {elapsed:.1f}s")
|
| 1595 |
+
|
| 1596 |
+
if fitness >= 0.9999:
|
| 1597 |
+
print("\n TARGET: 100% FITNESS ACHIEVED")
|
| 1598 |
+
break
|
| 1599 |
+
|
| 1600 |
+
print("\n" + "=" * 70)
|
| 1601 |
+
print(" RESULTS")
|
| 1602 |
+
print("=" * 70)
|
| 1603 |
+
|
| 1604 |
+
router.eval()
|
| 1605 |
+
final_fitness, details = compute_fitness(model_fn, n_samples=5000, device=device, return_details=True)
|
| 1606 |
+
|
| 1607 |
+
print(f"\nFinal fitness: {final_fitness:.4f}")
|
| 1608 |
+
print(f"\nPer-operation:")
|
| 1609 |
+
for op in OPERATIONS:
|
| 1610 |
+
acc = details['by_op'][op]['accuracy']
|
| 1611 |
+
print(f" {op}: {acc:.4f}")
|
| 1612 |
+
|
| 1613 |
+
print(f"\nTotal time: {time.perf_counter() - start_time:.1f}s")
|
| 1614 |
+
|
| 1615 |
+
if final_fitness >= 0.99:
|
| 1616 |
+
print("\nCONCLUSION: Router successfully learned operation dispatch.")
|
| 1617 |
+
print(" With correct bit encoding, 100% is achievable.")
|
| 1618 |
+
|
| 1619 |
+
save_path = os.path.join(TRAINED, 'router.pt')
|
| 1620 |
+
torch.save({
|
| 1621 |
+
'router_state_dict': router.state_dict(),
|
| 1622 |
+
'final_fitness': final_fitness,
|
| 1623 |
+
'params': sum(p.numel() for p in router.parameters()),
|
| 1624 |
+
}, save_path)
|
| 1625 |
+
print(f"\nSaved trained router to: {save_path}")
|
| 1626 |
+
|
| 1627 |
+
return router, final_fitness
|
| 1628 |
+
|
| 1629 |
+
|
| 1630 |
+
def get_gpu_memory():
|
| 1631 |
+
"""Get GPU memory usage in MB."""
|
| 1632 |
+
if torch.cuda.is_available():
|
| 1633 |
+
return torch.cuda.memory_allocated() / 1024 / 1024, torch.cuda.max_memory_allocated() / 1024 / 1024
|
| 1634 |
+
return 0, 0
|
| 1635 |
+
|
| 1636 |
+
|
| 1637 |
+
def train_interface(epochs: int = 200, batch_size: int = 512, lr: float = 1e-3,
|
| 1638 |
+
eval_interval: int = 10, device: str = 'cuda'):
|
| 1639 |
+
"""Train BitEncoder + OpRouter with ground truth bits."""
|
| 1640 |
+
print("=" * 70)
|
| 1641 |
+
print(" INTERFACE TRAINING (Encoder + Router)")
|
| 1642 |
+
print("=" * 70)
|
| 1643 |
+
print(f" Started at: {time.strftime('%H:%M:%S')}")
|
| 1644 |
+
|
| 1645 |
+
print("\n[1/4] Verifying frozen circuits...")
|
| 1646 |
+
print(" Creating DirectCircuitModel...", end=" ", flush=True)
|
| 1647 |
+
direct_model = DirectCircuitModel(device=device)
|
| 1648 |
+
mem, max_mem = get_gpu_memory()
|
| 1649 |
+
print(f"done. VRAM: {mem:.0f}MB")
|
| 1650 |
+
|
| 1651 |
+
def direct_fn(a, b, op):
|
| 1652 |
+
return direct_model(a, b, op)
|
| 1653 |
+
|
| 1654 |
+
print(" Running fitness check (1000 samples)...", end=" ", flush=True)
|
| 1655 |
+
circuit_fitness = compute_fitness(direct_fn, n_samples=1000, device=device)
|
| 1656 |
+
print(f"done. Fitness: {circuit_fitness:.4f}")
|
| 1657 |
+
if circuit_fitness < 0.999:
|
| 1658 |
+
print(" ERROR: Circuits not achieving 100%. Aborting.")
|
| 1659 |
+
return None, 0.0
|
| 1660 |
+
print(" STATUS: PASS")
|
| 1661 |
+
|
| 1662 |
+
print("\n[2/4] Initializing model...")
|
| 1663 |
+
print(" Creating ThresholdALU...", end=" ", flush=True)
|
| 1664 |
+
model = ThresholdALU(device=device)
|
| 1665 |
+
mem, max_mem = get_gpu_memory()
|
| 1666 |
+
print(f"done. VRAM: {mem:.0f}MB")
|
| 1667 |
+
|
| 1668 |
+
trainable_params = sum(p.numel() for p in model.parameters() if p.requires_grad)
|
| 1669 |
+
print(f" Trainable parameters: {trainable_params:,}")
|
| 1670 |
+
|
| 1671 |
+
def model_fn(a, b, op):
|
| 1672 |
+
return model(a, b, op)
|
| 1673 |
+
|
| 1674 |
+
print(" Running initial fitness check...", end=" ", flush=True)
|
| 1675 |
+
initial_fitness = compute_fitness(model_fn, n_samples=1000, device=device)
|
| 1676 |
+
print(f"done. Fitness: {initial_fitness:.4f}")
|
| 1677 |
+
|
| 1678 |
+
print("\n[3/4] Setting up training...")
|
| 1679 |
+
print(" Creating optimizer...", end=" ", flush=True)
|
| 1680 |
+
optimizer = optim.AdamW(model.parameters(), lr=lr)
|
| 1681 |
+
print("done.")
|
| 1682 |
+
print(" Creating scheduler...", end=" ", flush=True)
|
| 1683 |
+
scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=epochs)
|
| 1684 |
+
print("done.")
|
| 1685 |
+
|
| 1686 |
+
print(f" Config: lr={lr}, batch_size={batch_size}, epochs={epochs}")
|
| 1687 |
+
|
| 1688 |
+
print("\n[4/4] Training...")
|
| 1689 |
+
print(" Generating first batch to warm up...", end=" ", flush=True)
|
| 1690 |
+
warmup_batch = generate_batch(batch_size, device)
|
| 1691 |
+
mem, max_mem = get_gpu_memory()
|
| 1692 |
+
print(f"done. VRAM: {mem:.0f}MB (max: {max_mem:.0f}MB)")
|
| 1693 |
+
|
| 1694 |
+
print("-" * 70)
|
| 1695 |
+
|
| 1696 |
+
best_fitness = initial_fitness
|
| 1697 |
+
start_time = time.perf_counter()
|
| 1698 |
+
n_batches = 100
|
| 1699 |
+
|
| 1700 |
+
for epoch in range(epochs):
|
| 1701 |
+
model.train()
|
| 1702 |
+
epoch_loss = 0.0
|
| 1703 |
+
epoch_start = time.perf_counter()
|
| 1704 |
+
|
| 1705 |
+
for batch_idx in range(n_batches):
|
| 1706 |
+
batch = generate_batch(batch_size, device)
|
| 1707 |
+
|
| 1708 |
+
optimizer.zero_grad()
|
| 1709 |
+
|
| 1710 |
+
pred_bits = model(batch['a_bits'], batch['b_bits'], batch['op_onehot'])
|
| 1711 |
+
|
| 1712 |
+
loss = compute_loss(pred_bits, batch['expected_bits'])
|
| 1713 |
+
|
| 1714 |
+
loss.backward()
|
| 1715 |
+
optimizer.step()
|
| 1716 |
+
|
| 1717 |
+
epoch_loss += loss.item()
|
| 1718 |
+
|
| 1719 |
+
if batch_idx == 0 and epoch == 0:
|
| 1720 |
+
mem, max_mem = get_gpu_memory()
|
| 1721 |
+
print(f" First forward/backward done. VRAM: {mem:.0f}MB (max: {max_mem:.0f}MB)")
|
| 1722 |
+
|
| 1723 |
+
if (batch_idx + 1) % 25 == 0:
|
| 1724 |
+
avg_so_far = epoch_loss / (batch_idx + 1)
|
| 1725 |
+
print(f" Epoch {epoch+1} batch {batch_idx+1}/{n_batches} | loss: {avg_so_far:.4f}", flush=True)
|
| 1726 |
+
|
| 1727 |
+
scheduler.step()
|
| 1728 |
+
|
| 1729 |
+
avg_loss = epoch_loss / n_batches
|
| 1730 |
+
epoch_time = time.perf_counter() - epoch_start
|
| 1731 |
+
|
| 1732 |
+
if (epoch + 1) % 5 == 0 or epoch == 0: # Eval every 5 epochs
|
| 1733 |
+
model.eval()
|
| 1734 |
+
fitness, details = compute_fitness(
|
| 1735 |
+
model_fn, n_samples=2000, device=device, return_details=True
|
| 1736 |
+
)
|
| 1737 |
+
|
| 1738 |
+
elapsed = time.perf_counter() - start_time
|
| 1739 |
+
|
| 1740 |
+
if fitness > best_fitness:
|
| 1741 |
+
best_fitness = fitness
|
| 1742 |
+
marker = " *"
|
| 1743 |
+
else:
|
| 1744 |
+
marker = ""
|
| 1745 |
+
|
| 1746 |
+
mem, _ = get_gpu_memory()
|
| 1747 |
+
print(f"Epoch {epoch+1:4d} | Loss: {avg_loss:.4f} | "
|
| 1748 |
+
f"Fitness: {fitness:.4f}{marker} | "
|
| 1749 |
+
f"LR: {scheduler.get_last_lr()[0]:.2e} | "
|
| 1750 |
+
f"VRAM: {mem:.0f}MB | "
|
| 1751 |
+
f"Time: {elapsed:.1f}s ({epoch_time:.1f}s/epoch)")
|
| 1752 |
+
|
| 1753 |
+
if fitness >= 0.9999:
|
| 1754 |
+
print("\n" + "=" * 70)
|
| 1755 |
+
print(" TARGET ACHIEVED: 100% FITNESS")
|
| 1756 |
+
print("=" * 70)
|
| 1757 |
+
break
|
| 1758 |
+
|
| 1759 |
+
print("\n" + "=" * 70)
|
| 1760 |
+
print(" TRAINING COMPLETE")
|
| 1761 |
+
print("=" * 70)
|
| 1762 |
+
|
| 1763 |
+
model.eval()
|
| 1764 |
+
final_fitness, details = compute_fitness(
|
| 1765 |
+
model_fn, n_samples=5000, device=device, return_details=True
|
| 1766 |
+
)
|
| 1767 |
+
|
| 1768 |
+
print(f"\nFinal fitness: {final_fitness:.4f}")
|
| 1769 |
+
print(f"Best fitness: {best_fitness:.4f}")
|
| 1770 |
+
print(f"\nPer-operation breakdown:")
|
| 1771 |
+
for op in OPERATIONS:
|
| 1772 |
+
acc = details['by_op'][op]['accuracy']
|
| 1773 |
+
print(f" {op:6}: {acc:.4f}")
|
| 1774 |
+
|
| 1775 |
+
print(f"\nTotal time: {time.perf_counter() - start_time:.1f}s")
|
| 1776 |
+
|
| 1777 |
+
save_path = os.path.join(TRAINED, 'interface.pt')
|
| 1778 |
+
torch.save({
|
| 1779 |
+
'encoder_state_dict': model.encoder.state_dict(),
|
| 1780 |
+
'router_state_dict': model.router.state_dict(),
|
| 1781 |
+
'final_fitness': final_fitness,
|
| 1782 |
+
'best_fitness': best_fitness,
|
| 1783 |
+
}, save_path)
|
| 1784 |
+
print(f"\nSaved trained model to: {save_path}")
|
| 1785 |
+
|
| 1786 |
+
return model, final_fitness
|
| 1787 |
+
|
| 1788 |
+
|
| 1789 |
+
def compute_llm_loss(pred_bits, a_bits, b_bits, op_logits,
|
| 1790 |
+
target_result, target_a, target_b, target_op_idx,
|
| 1791 |
+
bit_weight: float = 2.0):
|
| 1792 |
+
"""Multi-component loss for LLM training; bit_weight scales the operand-bit losses."""
|
| 1793 |
+
result_loss = nn.functional.binary_cross_entropy_with_logits(
|
| 1794 |
+
pred_bits, target_result
|
| 1795 |
+
)
|
| 1796 |
+
|
| 1797 |
+
a_bits_safe = torch.clamp(a_bits, 0.0, 1.0)
|
| 1798 |
+
b_bits_safe = torch.clamp(b_bits, 0.0, 1.0)
|
| 1799 |
+
a_bits_safe = torch.nan_to_num(a_bits_safe, nan=0.5, posinf=1.0, neginf=0.0)
|
| 1800 |
+
b_bits_safe = torch.nan_to_num(b_bits_safe, nan=0.5, posinf=1.0, neginf=0.0)
|
| 1801 |
+
|
| 1802 |
+
a_loss = nn.functional.binary_cross_entropy(
|
| 1803 |
+
torch.clamp(a_bits_safe, 1e-6, 1-1e-6), target_a
|
| 1804 |
+
)
|
| 1805 |
+
b_loss = nn.functional.binary_cross_entropy(
|
| 1806 |
+
torch.clamp(b_bits_safe, 1e-6, 1-1e-6), target_b
|
| 1807 |
+
)
|
| 1808 |
+
|
| 1809 |
+
op_loss = nn.functional.cross_entropy(op_logits, target_op_idx)
|
| 1810 |
+
|
| 1811 |
+
total = result_loss + bit_weight * a_loss + bit_weight * b_loss + op_loss
|
| 1812 |
+
total = torch.nan_to_num(total, nan=10.0, posinf=10.0, neginf=0.0)
|
| 1813 |
+
|
| 1814 |
+
return total, {
|
| 1815 |
+
'result': result_loss.item() if not torch.isnan(result_loss) else 10.0,
|
| 1816 |
+
'a': a_loss.item() if not torch.isnan(a_loss) else 10.0,
|
| 1817 |
+
'b': b_loss.item() if not torch.isnan(b_loss) else 10.0,
|
| 1818 |
+
'op': op_loss.item() if not torch.isnan(op_loss) else 10.0
|
| 1819 |
+
}
|
| 1820 |
+
|
| 1821 |
+
|
| 1822 |
+
def value_to_digits(value: int) -> list:
|
| 1823 |
+
"""Convert integer value to list of digits (hundreds, tens, ones)."""
|
| 1824 |
+
digits = []
|
| 1825 |
+
for place in [100, 10, 1]:
|
| 1826 |
+
digit = (value // place) % 10
|
| 1827 |
+
digits.append(digit)
|
| 1828 |
+
return digits
|
| 1829 |
+
|
| 1830 |
+
|
| 1831 |
+
def compute_positional_digit_loss(pred_bits, op_logits, a_digit_logits_list, b_digit_logits_list,
|
| 1832 |
+
target_result, target_op_idx, target_a_values, target_b_values,
|
| 1833 |
+
device, digit_weight: float = 5.0):
|
| 1834 |
+
"""Loss for positional digit extraction with DIRECT digit supervision."""
|
| 1835 |
+
result_loss = nn.functional.binary_cross_entropy_with_logits(
|
| 1836 |
+
pred_bits, target_result
|
| 1837 |
+
)
|
| 1838 |
+
|
| 1839 |
+
op_loss = nn.functional.cross_entropy(op_logits, target_op_idx)
|
| 1840 |
+
|
| 1841 |
+
a_digit_loss = torch.tensor(0.0, device=device)
|
| 1842 |
+
b_digit_loss = torch.tensor(0.0, device=device)
|
| 1843 |
+
n_a_digits = 0
|
| 1844 |
+
n_b_digits = 0
|
| 1845 |
+
|
| 1846 |
+
for i, (a_logits_list, b_logits_list) in enumerate(zip(a_digit_logits_list, b_digit_logits_list)):
|
| 1847 |
+
target_a = target_a_values[i].item()
|
| 1848 |
+
target_b = target_b_values[i].item()
|
| 1849 |
+
|
| 1850 |
+
a_digits = value_to_digits(int(target_a))
|
| 1851 |
+
b_digits = value_to_digits(int(target_b))
|
| 1852 |
+
|
| 1853 |
+
n_a = len(a_logits_list)
|
| 1854 |
+
n_b = len(b_logits_list)
|
| 1855 |
+
|
| 1856 |
+
if n_a > 0:
|
| 1857 |
+
target_a_digits = a_digits[-n_a:]
|
| 1858 |
+
for j, logits in enumerate(a_logits_list):
|
| 1859 |
+
target_digit = torch.tensor([target_a_digits[j]], device=device, dtype=torch.long)
|
| 1860 |
+
a_digit_loss = a_digit_loss + nn.functional.cross_entropy(logits.unsqueeze(0), target_digit)
|
| 1861 |
+
n_a_digits += 1
|
| 1862 |
+
|
| 1863 |
+
if n_b > 0:
|
| 1864 |
+
target_b_digits = b_digits[-n_b:]
|
| 1865 |
+
for j, logits in enumerate(b_logits_list):
|
| 1866 |
+
target_digit = torch.tensor([target_b_digits[j]], device=device, dtype=torch.long)
|
| 1867 |
+
b_digit_loss = b_digit_loss + nn.functional.cross_entropy(logits.unsqueeze(0), target_digit)
|
| 1868 |
+
n_b_digits += 1
|
| 1869 |
+
|
| 1870 |
+
if n_a_digits > 0:
|
| 1871 |
+
a_digit_loss = a_digit_loss / n_a_digits
|
| 1872 |
+
if n_b_digits > 0:
|
| 1873 |
+
b_digit_loss = b_digit_loss / n_b_digits
|
| 1874 |
+
|
| 1875 |
+
total = result_loss + digit_weight * a_digit_loss + digit_weight * b_digit_loss + op_loss
|
| 1876 |
+
total = torch.nan_to_num(total, nan=10.0, posinf=10.0, neginf=0.0)
|
| 1877 |
+
|
| 1878 |
+
return total, {
|
| 1879 |
+
'result': result_loss.item() if not torch.isnan(result_loss) else 10.0,
|
| 1880 |
+
'a_digit': a_digit_loss.item() if not torch.isnan(a_digit_loss) else 10.0,
|
| 1881 |
+
'b_digit': b_digit_loss.item() if not torch.isnan(b_digit_loss) else 10.0,
|
| 1882 |
+
'op': op_loss.item() if not torch.isnan(op_loss) else 10.0
|
| 1883 |
+
}
|
| 1884 |
+
|
| 1885 |
+
|
| 1886 |
+
def compute_hybrid_loss(pred_bits, op_logits, used_lookup,
|
| 1887 |
+
a_digit_logits, b_digit_logits,
|
| 1888 |
+
target_result, target_a_values, target_b_values, target_op_idx,
|
| 1889 |
+
device, digit_weight: float = 2.0):
|
| 1890 |
+
"""Loss for hybrid extraction with digit-level prediction."""
|
| 1891 |
+
result_loss = nn.functional.binary_cross_entropy_with_logits(
|
| 1892 |
+
pred_bits, target_result
|
| 1893 |
+
)
|
| 1894 |
+
|
| 1895 |
+
op_loss = nn.functional.cross_entropy(op_logits, target_op_idx)
|
| 1896 |
+
|
| 1897 |
+
word_mask = ~used_lookup
|
| 1898 |
+
n_words = word_mask.sum().item()
|
| 1899 |
+
|
| 1900 |
+
if n_words > 0 and a_digit_logits is not None and b_digit_logits is not None:
|
| 1901 |
+
target_a_word = target_a_values[word_mask].long()
|
| 1902 |
+
target_b_word = target_b_values[word_mask].long()
|
| 1903 |
+
|
| 1904 |
+
a_hundreds = target_a_word // 100
|
| 1905 |
+
a_tens = (target_a_word % 100) // 10
|
| 1906 |
+
a_ones = target_a_word % 10
|
| 1907 |
+
|
| 1908 |
+
b_hundreds = target_b_word // 100
|
| 1909 |
+
b_tens = (target_b_word % 100) // 10
|
| 1910 |
+
b_ones = target_b_word % 10
|
| 1911 |
+
|
| 1912 |
+
a_logits = a_digit_logits.view(-1, 3, 10)
|
| 1913 |
+
b_logits = b_digit_logits.view(-1, 3, 10)
|
| 1914 |
+
|
| 1915 |
+
a_digit_loss = (
|
| 1916 |
+
nn.functional.cross_entropy(a_logits[:, 0], a_hundreds) +
|
| 1917 |
+
nn.functional.cross_entropy(a_logits[:, 1], a_tens) +
|
| 1918 |
+
nn.functional.cross_entropy(a_logits[:, 2], a_ones)
|
| 1919 |
+
) / 3.0
|
| 1920 |
+
|
| 1921 |
+
b_digit_loss = (
|
| 1922 |
+
nn.functional.cross_entropy(b_logits[:, 0], b_hundreds) +
|
| 1923 |
+
nn.functional.cross_entropy(b_logits[:, 1], b_tens) +
|
| 1924 |
+
nn.functional.cross_entropy(b_logits[:, 2], b_ones)
|
| 1925 |
+
) / 3.0
|
| 1926 |
+
else:
|
| 1927 |
+
a_digit_loss = torch.tensor(0.0, device=device)
|
| 1928 |
+
b_digit_loss = torch.tensor(0.0, device=device)
|
| 1929 |
+
|
| 1930 |
+
total = result_loss + op_loss + digit_weight * (a_digit_loss + b_digit_loss)
|
| 1931 |
+
total = torch.nan_to_num(total, nan=10.0, posinf=10.0, neginf=0.0)
|
| 1932 |
+
|
| 1933 |
+
return total, {
|
| 1934 |
+
'result': result_loss.item() if not torch.isnan(result_loss) else 10.0,
|
| 1935 |
+
'a_digit': a_digit_loss.item() if not torch.isnan(a_digit_loss) else 10.0,
|
| 1936 |
+
'b_digit': b_digit_loss.item() if not torch.isnan(b_digit_loss) else 10.0,
|
| 1937 |
+
'op': op_loss.item() if not torch.isnan(op_loss) else 10.0,
|
| 1938 |
+
'n_words': n_words,
|
| 1939 |
+
'n_lookup': used_lookup.sum().item()
|
| 1940 |
+
}
|
| 1941 |
+
|
| 1942 |
+
|
| 1943 |
+
def evaluate_llm(model, n_samples: int = 500):
|
| 1944 |
+
"""Evaluate LLM model on random problems (mixed digit/word format)."""
|
| 1945 |
+
model.extractor.eval()
|
| 1946 |
+
correct = 0
|
| 1947 |
+
op_correct = 0
|
| 1948 |
+
|
| 1949 |
+
for _ in range(n_samples):
|
| 1950 |
+
text, a, b, op, expected = generate_problem()
|
| 1951 |
+
|
| 1952 |
+
with torch.no_grad():
|
| 1953 |
+
outputs = model([text])
|
| 1954 |
+
result_bits = outputs[0]
|
| 1955 |
+
op_logits = outputs[3]
|
| 1956 |
+
|
| 1957 |
+
pred_result = train_bits_to_int(result_bits[0])
|
| 1958 |
+
pred_op = OPERATIONS[op_logits[0].argmax().item()]
|
| 1959 |
+
|
| 1960 |
+
if pred_result == expected:
|
| 1961 |
+
correct += 1
|
| 1962 |
+
if pred_op == op:
|
| 1963 |
+
op_correct += 1
|
| 1964 |
+
|
| 1965 |
+
model.extractor.train()
|
| 1966 |
+
return correct / n_samples, op_correct / n_samples
|
| 1967 |
+
|
| 1968 |
+
|
| 1969 |
+
def train_llm(epochs: int = 100, batch_size: int = 256, lr: float = 3e-4,
|
| 1970 |
+
unfreeze_layers: int = 0, extract_layer: int = -1,
|
| 1971 |
+
position_extract: bool = False, digit_pred: bool = False,
|
| 1972 |
+
positional_digit: bool = False, device: str = 'cuda'):
|
| 1973 |
+
"""Train extractor with LLM hidden states."""
|
| 1974 |
+
hybrid = not (positional_digit or position_extract or digit_pred)
|
| 1975 |
+
|
| 1976 |
+
print("=" * 70)
|
| 1977 |
+
print(" LLM TRAINING")
|
| 1978 |
+
if unfreeze_layers > 0:
|
| 1979 |
+
print(f" {unfreeze_layers} transformer layers unfrozen")
|
| 1980 |
+
else:
|
| 1981 |
+
print(" LLM frozen")
|
| 1982 |
+
if extract_layer != -1:
|
| 1983 |
+
print(f" Extracting from layer {extract_layer}")
|
| 1984 |
+
if hybrid:
|
| 1985 |
+
print(" HYBRID extraction (digit lookup + word learning)")
|
| 1986 |
+
elif positional_digit:
|
| 1987 |
+
print(" POSITIONAL DIGIT extraction (legacy, 100% on digits only)")
|
| 1988 |
+
elif position_extract:
|
| 1989 |
+
print(" Position-specific extraction (legacy)")
|
| 1990 |
+
elif digit_pred:
|
| 1991 |
+
print(" Digit-level prediction (legacy)")
|
| 1992 |
+
print("=" * 70)
|
| 1993 |
+
|
| 1994 |
+
print("\nInitializing model...")
|
| 1995 |
+
model = ArithmeticModel(
|
| 1996 |
+
device=device,
|
| 1997 |
+
unfreeze_layers=unfreeze_layers,
|
| 1998 |
+
extract_layer=extract_layer,
|
| 1999 |
+
position_extract=position_extract,
|
| 2000 |
+
digit_pred=digit_pred,
|
| 2001 |
+
positional_digit=positional_digit,
|
| 2002 |
+
hybrid=hybrid
|
| 2003 |
+
)
|
| 2004 |
+
|
| 2005 |
+
optimizer = optim.AdamW(model.trainable_parameters(), lr=lr)
|
| 2006 |
+
scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=epochs)
|
| 2007 |
+
|
| 2008 |
+
print(f"\nTraining config:")
|
| 2009 |
+
print(f" Epochs: {epochs}")
|
| 2010 |
+
print(f" Batch size: {batch_size}")
|
| 2011 |
+
print(f" Learning rate: {lr}")
|
| 2012 |
+
print(f" Unfreeze layers: {unfreeze_layers}")
|
| 2013 |
+
print(f" Samples/epoch: {batch_size * 20}")
|
| 2014 |
+
|
| 2015 |
+
print(f"\nInitial evaluation (200 samples)...")
|
| 2016 |
+
acc, op_acc = evaluate_llm(model, 200)
|
| 2017 |
+
print(f" Accuracy: {acc:.4f}, Op accuracy: {op_acc:.4f}")
|
| 2018 |
+
|
| 2019 |
+
print(f"\nStarting training...")
|
| 2020 |
+
print("-" * 70)
|
| 2021 |
+
|
| 2022 |
+
best_acc = acc
|
| 2023 |
+
start_time = time.perf_counter()
|
| 2024 |
+
|
| 2025 |
+
for epoch in range(epochs):
|
| 2026 |
+
model.extractor.train()
|
| 2027 |
+
if unfreeze_layers > 0:
|
| 2028 |
+
model.llm.train()
|
| 2029 |
+
|
| 2030 |
+
max_val = get_curriculum_max(epoch, epochs)
|
| 2031 |
+
|
| 2032 |
+
epoch_loss = 0
|
| 2033 |
+
if hybrid:
|
| 2034 |
+
epoch_losses = {'result': 0, 'a_digit': 0, 'b_digit': 0, 'op': 0, 'n_words': 0, 'n_lookup': 0}
|
| 2035 |
+
elif positional_digit:
|
| 2036 |
+
epoch_losses = {'result': 0, 'a_digit': 0, 'b_digit': 0, 'op': 0}
|
| 2037 |
+
else:
|
| 2038 |
+
epoch_losses = {'result': 0, 'a': 0, 'b': 0, 'op': 0}
|
| 2039 |
+
n_batches = 20
|
| 2040 |
+
epoch_start = time.perf_counter()
|
| 2041 |
+
|
| 2042 |
+
for batch_idx in range(n_batches):
|
| 2043 |
+
batch_texts = []
|
| 2044 |
+
batch_a = []
|
| 2045 |
+
batch_b = []
|
| 2046 |
+
batch_op = []
|
| 2047 |
+
batch_result = []
|
| 2048 |
+
batch_a_values = []
|
| 2049 |
+
batch_b_values = []
|
| 2050 |
+
|
| 2051 |
+
for _ in range(batch_size):
|
| 2052 |
+
text, a, b, op, result = generate_problem(max_val)
|
| 2053 |
+
batch_texts.append(text)
|
| 2054 |
+
batch_a.append(train_int_to_bits(a, device))
|
| 2055 |
+
batch_b.append(train_int_to_bits(b, device))
|
| 2056 |
+
batch_op.append(OPERATIONS.index(op))
|
| 2057 |
+
batch_result.append(train_int_to_bits(result, device))
|
| 2058 |
+
batch_a_values.append(a)
|
| 2059 |
+
batch_b_values.append(b)
|
| 2060 |
+
|
| 2061 |
+
target_a = torch.stack(batch_a)
|
| 2062 |
+
target_b = torch.stack(batch_b)
|
| 2063 |
+
target_op = torch.tensor(batch_op, device=device)
|
| 2064 |
+
target_result = torch.stack(batch_result)
|
| 2065 |
+
target_a_values = torch.tensor(batch_a_values, device=device, dtype=torch.float32)
|
| 2066 |
+
target_b_values = torch.tensor(batch_b_values, device=device, dtype=torch.float32)
|
| 2067 |
+
|
| 2068 |
+
optimizer.zero_grad()
|
| 2069 |
+
|
| 2070 |
+
outputs = model(batch_texts)
|
| 2071 |
+
pred_bits, a_bits, b_bits, op_logits = outputs[0], outputs[1], outputs[2], outputs[3]
|
| 2072 |
+
|
| 2073 |
+
if hybrid:
|
| 2074 |
+
a_values, b_values, used_lookup = outputs[4], outputs[5], outputs[6]
|
| 2075 |
+
a_digit_logits, b_digit_logits = outputs[7], outputs[8]
|
| 2076 |
+
loss, losses = compute_hybrid_loss(
|
| 2077 |
+
pred_bits, op_logits, used_lookup,
|
| 2078 |
+
a_digit_logits, b_digit_logits,
|
| 2079 |
+
target_result, target_a_values, target_b_values, target_op, device
|
| 2080 |
+
)
|
| 2081 |
+
elif positional_digit:
|
| 2082 |
+
a_digit_logits_list = outputs[7]
|
| 2083 |
+
b_digit_logits_list = outputs[8]
|
| 2084 |
+
loss, losses = compute_positional_digit_loss(
|
| 2085 |
+
pred_bits, op_logits, a_digit_logits_list, b_digit_logits_list,
|
| 2086 |
+
target_result, target_op, target_a_values, target_b_values, device
|
| 2087 |
+
)
|
| 2088 |
+
else:
|
| 2089 |
+
loss, losses = compute_llm_loss(
|
| 2090 |
+
pred_bits, a_bits, b_bits, op_logits,
|
| 2091 |
+
target_result, target_a, target_b, target_op
|
| 2092 |
+
)
|
| 2093 |
+
|
| 2094 |
+
loss.backward()
|
| 2095 |
+
torch.nn.utils.clip_grad_norm_(model.trainable_parameters(), 1.0)
|
| 2096 |
+
optimizer.step()
|
| 2097 |
+
|
| 2098 |
+
epoch_loss += loss.item()
|
| 2099 |
+
for k in epoch_losses:
|
| 2100 |
+
epoch_losses[k] += losses[k]
|
| 2101 |
+
|
| 2102 |
+
if (batch_idx + 1) % 5 == 0:
|
| 2103 |
+
avg_so_far = epoch_loss / (batch_idx + 1)
|
| 2104 |
+
print(f" Epoch {epoch+1} batch {batch_idx+1}/{n_batches} | loss: {avg_so_far:.4f}", flush=True)
|
| 2105 |
+
|
| 2106 |
+
epoch_time = time.perf_counter() - epoch_start
|
| 2107 |
+
scheduler.step()
|
| 2108 |
+
|
| 2109 |
+
avg_loss = epoch_loss / n_batches
|
| 2110 |
+
for k in epoch_losses:
|
| 2111 |
+
epoch_losses[k] /= n_batches
|
| 2112 |
+
|
| 2113 |
+
acc, op_acc = evaluate_llm(model, 300)
|
| 2114 |
+
elapsed = time.perf_counter() - start_time
|
| 2115 |
+
|
| 2116 |
+
marker = " *" if acc > best_acc else ""
|
| 2117 |
+
if acc > best_acc:
|
| 2118 |
+
best_acc = acc
|
| 2119 |
+
|
| 2120 |
+
mem, _ = get_gpu_memory()
|
| 2121 |
+
print(f"Epoch {epoch+1:3d} | Loss: {avg_loss:.4f} | "
|
| 2122 |
+
f"Acc: {acc:.4f}{marker} | OpAcc: {op_acc:.4f} | "
|
| 2123 |
+
f"Range: 0-{max_val} | VRAM: {mem:.0f}MB | Time: {elapsed:.0f}s")
|
| 2124 |
+
if hybrid:
|
| 2125 |
+
print(f" Losses - result:{epoch_losses['result']:.4f} "
|
| 2126 |
+
f"a_digit:{epoch_losses['a_digit']:.4f} b_digit:{epoch_losses['b_digit']:.4f} "
|
| 2127 |
+
f"op:{epoch_losses['op']:.4f} | words:{epoch_losses['n_words']:.0f} lookup:{epoch_losses['n_lookup']:.0f}")
|
| 2128 |
+
elif positional_digit:
|
| 2129 |
+
print(f" Losses - result:{epoch_losses['result']:.4f} "
|
| 2130 |
+
f"a_digit:{epoch_losses['a_digit']:.4f} b_digit:{epoch_losses['b_digit']:.4f} "
|
| 2131 |
+
f"op:{epoch_losses['op']:.4f}")
|
| 2132 |
+
else:
|
| 2133 |
+
print(f" Losses - result:{epoch_losses['result']:.4f} "
|
| 2134 |
+
f"a:{epoch_losses['a']:.4f} b:{epoch_losses['b']:.4f} "
|
| 2135 |
+
f"op:{epoch_losses['op']:.4f}")
|
| 2136 |
+
|
| 2137 |
+
if acc >= 0.99:
|
| 2138 |
+
print("\n" + "=" * 70)
|
| 2139 |
+
print(" TARGET ACHIEVED: 99%+ ACCURACY")
|
| 2140 |
+
print("=" * 70)
|
| 2141 |
+
break
|
| 2142 |
+
|
| 2143 |
+
print("\n" + "=" * 70)
|
| 2144 |
+
print(" FINAL EVALUATION")
|
| 2145 |
+
print("=" * 70)
|
| 2146 |
+
|
| 2147 |
+
acc, op_acc = evaluate_llm(model, 1000)
|
| 2148 |
+
print(f"Final accuracy: {acc:.4f}")
|
| 2149 |
+
print(f"Final op accuracy: {op_acc:.4f}")
|
| 2150 |
+
print(f"Best accuracy: {best_acc:.4f}")
|
| 2151 |
+
|
| 2152 |
+
print("\nSample predictions:")
|
| 2153 |
+
for _ in range(10):
|
| 2154 |
+
text, a, b, op, expected = generate_problem()
|
| 2155 |
+
with torch.no_grad():
|
| 2156 |
+
outputs = model([text])
|
| 2157 |
+
result_bits, a_bits, b_bits, op_logits = outputs[0], outputs[1], outputs[2], outputs[3]
|
| 2158 |
+
pred = train_bits_to_int(result_bits[0])
|
| 2159 |
+
pred_a = train_bits_to_int(a_bits[0])
|
| 2160 |
+
pred_b = train_bits_to_int(b_bits[0])
|
| 2161 |
+
pred_op = OPERATIONS[op_logits[0].argmax().item()]
|
| 2162 |
+
|
| 2163 |
+
status = "OK" if pred == expected else "WRONG"
|
| 2164 |
+
print(f" '{text}' = {expected} | pred={pred} (a={pred_a}, b={pred_b}, op={pred_op}) [{status}]")
|
| 2165 |
+
|
| 2166 |
+
save_path = os.path.join(TRAINED, 'llm.pt')
|
| 2167 |
+
save_dict = {
|
| 2168 |
+
'extractor_state_dict': model.extractor.state_dict(),
|
| 2169 |
+
'final_accuracy': acc,
|
| 2170 |
+
'best_accuracy': best_acc,
|
| 2171 |
+
'unfreeze_layers': unfreeze_layers,
|
| 2172 |
+
}
|
| 2173 |
+
if unfreeze_layers > 0:
|
| 2174 |
+
save_dict['llm_state_dict'] = {
|
| 2175 |
+
k: v for k, v in model.llm.state_dict().items()
|
| 2176 |
+
if any(f'layers.{i}.' in k for i in range(len(model.llm.model.layers) - unfreeze_layers, len(model.llm.model.layers)))
|
| 2177 |
+
}
|
| 2178 |
+
torch.save(save_dict, save_path)
|
| 2179 |
+
print(f"\nSaved to: {save_path}")
|
| 2180 |
+
|
| 2181 |
+
return model, acc
|
| 2182 |
+
|
| 2183 |
+
|
| 2184 |
+
def main_train():
|
| 2185 |
+
parser = argparse.ArgumentParser(
|
| 2186 |
+
description='Unified training for threshold circuit LLM integration',
|
| 2187 |
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
| 2188 |
+
epilog="""
|
| 2189 |
+
Modes:
|
| 2190 |
+
router - Train only OpRouter with ground truth bits (sanity check)
|
| 2191 |
+
interface - Train BitEncoder + OpRouter with ground truth bits (sanity check)
|
| 2192 |
+
llm - Train extractor with LLM hidden states (the real training)
|
| 2193 |
+
|
| 2194 |
+
LLM options:
|
| 2195 |
+
--unfreeze_layers N Fine-tune top N transformer layers
|
| 2196 |
+
--extract_layer N Extract from layer N (-1 = last)
|
| 2197 |
+
--position_extract Use position-specific extraction
|
| 2198 |
+
--digit_pred Predict digits instead of bits
|
| 2199 |
+
|
| 2200 |
+
Baked-in: curriculum learning (0-9 -> 0-99 -> 0-255), 2x loss weight for a/b
|
| 2201 |
+
|
| 2202 |
+
Examples:
|
| 2203 |
+
python train.py --mode llm --epochs 100
|
| 2204 |
+
python train.py --mode llm --position_extract
|
| 2205 |
+
python train.py --mode llm --digit_pred --extract_layer 12
|
| 2206 |
+
python train.py --mode llm --unfreeze_layers 4 --batch_size 4096
|
| 2207 |
+
"""
|
| 2208 |
+
)
|
| 2209 |
+
parser.add_argument('--mode', type=str, required=True,
|
| 2210 |
+
choices=['router', 'interface', 'llm'],
|
| 2211 |
+
help='Training mode')
|
| 2212 |
+
parser.add_argument('--epochs', type=int, default=100, help='Number of epochs')
|
| 2213 |
+
parser.add_argument('--batch_size', type=int, default=512, help='Batch size (default: 512)')
|
| 2214 |
+
parser.add_argument('--lr', type=float, default=None,
|
| 2215 |
+
help='Learning rate (default: mode-specific)')
|
| 2216 |
+
parser.add_argument('--unfreeze_layers', type=int, default=0,
|
| 2217 |
+
help='Unfreeze top N transformer layers (default 0 = frozen)')
|
| 2218 |
+
parser.add_argument('--extract_layer', type=int, default=0,
|
| 2219 |
+
help='Which layer to extract from (default: 0 = embeddings, best for digits)')
|
| 2220 |
+
parser.add_argument('--position_extract', action='store_true',
|
| 2221 |
+
help='Use position-specific extraction (legacy)')
|
| 2222 |
+
parser.add_argument('--digit_pred', action='store_true',
|
| 2223 |
+
help='Predict digits instead of bits (legacy)')
|
| 2224 |
+
parser.add_argument('--positional_digit', action='store_true', default=False,
|
| 2225 |
+
help='Use positional digit extraction (legacy, 100%% on digits only)')
|
| 2226 |
+
parser.add_argument('--device', type=str, default='cuda', help='Device')
|
| 2227 |
+
args = parser.parse_args()
|
| 2228 |
+
|
| 2229 |
+
torch.manual_seed(42)
|
| 2230 |
+
random.seed(42)
|
| 2231 |
+
|
| 2232 |
+
if args.mode == 'router':
|
| 2233 |
+
lr = args.lr if args.lr is not None else 1e-2
|
| 2234 |
+
train_router(epochs=args.epochs, batch_size=args.batch_size, lr=lr, device=args.device)
|
| 2235 |
+
|
| 2236 |
+
elif args.mode == 'interface':
|
| 2237 |
+
lr = args.lr if args.lr is not None else 1e-3
|
| 2238 |
+
model, fitness = train_interface(
|
| 2239 |
+
epochs=args.epochs, batch_size=args.batch_size, lr=lr, device=args.device
|
| 2240 |
+
)
|
| 2241 |
+
|
| 2242 |
+
print("\n" + "=" * 70)
|
| 2243 |
+
print(" EXPERIMENT SUMMARY")
|
| 2244 |
+
print("=" * 70)
|
| 2245 |
+
print(f"\n Control (Vanilla SmolLM2-360M): 11.90%")
|
| 2246 |
+
print(f" Experimental (Trained Interface): {100*fitness:.2f}%")
|
| 2247 |
+
if fitness > 0:
|
| 2248 |
+
print(f"\n Improvement: {100*(fitness - 0.119)/0.119:.1f}%")
|
| 2249 |
+
|
| 2250 |
+
if fitness >= 0.99:
|
| 2251 |
+
print("\n CONCLUSION: Frozen threshold circuits + trained interface")
|
| 2252 |
+
print(" achieves near-perfect arithmetic accuracy.")
|
| 2253 |
+
print(" Core thesis VALIDATED.")
|
| 2254 |
+
else:
|
| 2255 |
+
print(f"\n CONCLUSION: Further training or architecture changes needed.")
|
| 2256 |
+
print(f" Current gap: {100*(1.0 - fitness):.2f}%")
|
| 2257 |
+
|
| 2258 |
+
elif args.mode == 'llm':
|
| 2259 |
+
lr = args.lr if args.lr is not None else 3e-4
|
| 2260 |
+
train_llm(
|
| 2261 |
+
epochs=args.epochs,
|
| 2262 |
+
batch_size=args.batch_size,
|
| 2263 |
+
lr=lr,
|
| 2264 |
+
unfreeze_layers=args.unfreeze_layers,
|
| 2265 |
+
extract_layer=args.extract_layer,
|
| 2266 |
+
position_extract=args.position_extract,
|
| 2267 |
+
digit_pred=args.digit_pred,
|
| 2268 |
+
positional_digit=args.positional_digit,
|
| 2269 |
+
device=args.device
|
| 2270 |
+
)
|
| 2271 |
+
|
| 2272 |
+
DEVICE = "cuda"
|
| 2273 |
+
|
| 2274 |
+
MODEL_ID = "HuggingFaceTB/SmolLM2-360M-Instruct"
|
| 2275 |
+
|
| 2276 |
+
SYSTEM_PROMPT = """You are a calculator. Output only the numeric answer. No words, no explanation, just digits. Examples:
|
| 2277 |
+
User: 5 + 3
|
| 2278 |
+
Assistant: 8
|
| 2279 |
+
User: 12 * 7
|
| 2280 |
+
Assistant: 84
|
| 2281 |
+
User: 100 > 50
|
| 2282 |
+
Assistant: 1
|
| 2283 |
+
User: 25 < 10
|
| 2284 |
+
Assistant: 0"""
|
| 2285 |
+
|
| 2286 |
+
|
| 2287 |
+
def load_model():
|
| 2288 |
+
print(f"Loading {MODEL_ID}...")
|
| 2289 |
+
tokenizer = AutoTokenizer.from_pretrained(MODEL_ID)
|
| 2290 |
+
tokenizer.padding_side = "left"
|
| 2291 |
+
if tokenizer.pad_token is None:
|
| 2292 |
+
tokenizer.pad_token = tokenizer.eos_token
|
| 2293 |
+
model = AutoModelForCausalLM.from_pretrained(
|
| 2294 |
+
MODEL_ID,
|
| 2295 |
+
torch_dtype=torch.float16,
|
| 2296 |
+
device_map=DEVICE
|
| 2297 |
+
)
|
| 2298 |
+
model.eval()
|
| 2299 |
+
print(f" Loaded. Parameters: {sum(p.numel() for p in model.parameters()):,}")
|
| 2300 |
+
return model, tokenizer
|
| 2301 |
+
|
| 2302 |
+
|
| 2303 |
+
def format_prompt(tokenizer, op_str):
|
| 2304 |
+
messages = [
|
| 2305 |
+
{"role": "system", "content": SYSTEM_PROMPT},
|
| 2306 |
+
{"role": "user", "content": op_str}
|
| 2307 |
+
]
|
| 2308 |
+
return tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
|
| 2309 |
+
|
| 2310 |
+
|
| 2311 |
+
def baseline_generate(model, tokenizer, prompts, max_new_tokens=16):
|
| 2312 |
+
inputs = tokenizer(prompts, return_tensors="pt", padding=True).to(DEVICE)
|
| 2313 |
+
with torch.no_grad():
|
| 2314 |
+
outputs = model.generate(
|
| 2315 |
+
**inputs,
|
| 2316 |
+
max_new_tokens=max_new_tokens,
|
| 2317 |
+
do_sample=False,
|
| 2318 |
+
pad_token_id=tokenizer.eos_token_id
|
| 2319 |
+
)
|
| 2320 |
+
responses = []
|
| 2321 |
+
for i, output in enumerate(outputs):
|
| 2322 |
+
response = tokenizer.decode(output[inputs.input_ids.shape[1]:], skip_special_tokens=True)
|
| 2323 |
+
responses.append(response.strip())
|
| 2324 |
+
return responses
|
| 2325 |
+
|
| 2326 |
+
|
| 2327 |
+
def extract_answer(text):
|
| 2328 |
+
"""Generous extraction - find any number in output"""
|
| 2329 |
+
text = text.strip().lower()
|
| 2330 |
+
if not text:
|
| 2331 |
+
return None
|
| 2332 |
+
|
| 2333 |
+
# Handle Yes/No for comparisons
|
| 2334 |
+
if text in ['yes', 'true', '1']:
|
| 2335 |
+
return 1
|
| 2336 |
+
if text in ['no', 'false', '0']:
|
| 2337 |
+
return 0
|
| 2338 |
+
if text.startswith('yes'):
|
| 2339 |
+
return 1
|
| 2340 |
+
if text.startswith('no'):
|
| 2341 |
+
return 0
|
| 2342 |
+
|
| 2343 |
+
# Find all numbers, take the LAST one (most likely the answer)
|
| 2344 |
+
numbers = re.findall(r'-?\d+', text)
|
| 2345 |
+
if numbers:
|
| 2346 |
+
return int(numbers[-1])
|
| 2347 |
+
return None
|
| 2348 |
+
|
| 2349 |
+
|
| 2350 |
+
def baseline_ground_truth(a, b, op):
|
| 2351 |
+
"""Compute expected result (8-bit where applicable)"""
|
| 2352 |
+
if op == 'add':
|
| 2353 |
+
return (a + b) & 0xFF
|
| 2354 |
+
elif op == 'sub':
|
| 2355 |
+
return (a - b) & 0xFF
|
| 2356 |
+
elif op == 'mul':
|
| 2357 |
+
return (a * b) & 0xFF
|
| 2358 |
+
elif op == 'div':
|
| 2359 |
+
return a // b if b != 0 else 0
|
| 2360 |
+
elif op == 'and':
|
| 2361 |
+
return a & b
|
| 2362 |
+
elif op == 'or':
|
| 2363 |
+
return a | b
|
| 2364 |
+
elif op == 'xor':
|
| 2365 |
+
return a ^ b
|
| 2366 |
+
elif op == 'gt':
|
| 2367 |
+
return 1 if a > b else 0
|
| 2368 |
+
elif op == 'lt':
|
| 2369 |
+
return 1 if a < b else 0
|
| 2370 |
+
elif op == 'eq':
|
| 2371 |
+
return 1 if a == b else 0
|
| 2372 |
+
elif op == 'ge':
|
| 2373 |
+
return 1 if a >= b else 0
|
| 2374 |
+
elif op == 'le':
|
| 2375 |
+
return 1 if a <= b else 0
|
| 2376 |
+
else:
|
| 2377 |
+
raise ValueError(f"Unknown op: {op}")
|
| 2378 |
+
|
| 2379 |
+
|
| 2380 |
+
def op_to_str(a, b, op):
|
| 2381 |
+
"""Convert operation to natural string"""
|
| 2382 |
+
symbols = {
|
| 2383 |
+
'add': '+', 'sub': '-', 'mul': '*', 'div': '/',
|
| 2384 |
+
'and': '&', 'or': '|', 'xor': '^',
|
| 2385 |
+
'gt': '>', 'lt': '<', 'eq': '==', 'ge': '>=', 'le': '<='
|
| 2386 |
+
}
|
| 2387 |
+
return f"{a} {symbols[op]} {b}"
|
| 2388 |
+
|
| 2389 |
+
|
| 2390 |
+
def evaluate(model, tokenizer, n_samples=1000, batch_size=32, ops=None):
|
| 2391 |
+
if ops is None:
|
| 2392 |
+
ops = ['add', 'sub', 'mul', 'gt', 'lt', 'eq']
|
| 2393 |
+
|
| 2394 |
+
results = {op: {'correct': 0, 'total': 0} for op in ops}
|
| 2395 |
+
all_correct = 0
|
| 2396 |
+
all_total = 0
|
| 2397 |
+
|
| 2398 |
+
samples = []
|
| 2399 |
+
for _ in range(n_samples):
|
| 2400 |
+
a = random.randint(0, 255)
|
| 2401 |
+
b = random.randint(0, 255)
|
| 2402 |
+
if 'div' in ops and random.random() < 0.1:
|
| 2403 |
+
op = 'div'
|
| 2404 |
+
b = random.randint(1, 255) # avoid div by zero
|
| 2405 |
+
else:
|
| 2406 |
+
op = random.choice([o for o in ops if o != 'div'])
|
| 2407 |
+
samples.append((a, b, op))
|
| 2408 |
+
|
| 2409 |
+
print(f"\nEvaluating {n_samples} samples (batch_size={batch_size})...")
|
| 2410 |
+
|
| 2411 |
+
for batch_start in range(0, n_samples, batch_size):
|
| 2412 |
+
batch = samples[batch_start:batch_start + batch_size]
|
| 2413 |
+
prompts = [format_prompt(tokenizer, op_to_str(a, b, op)) for a, b, op in batch]
|
| 2414 |
+
responses = baseline_generate(model, tokenizer, prompts)
|
| 2415 |
+
|
| 2416 |
+
for (a, b, op), response in zip(batch, responses):
|
| 2417 |
+
expected = baseline_ground_truth(a, b, op)
|
| 2418 |
+
extracted = extract_answer(response)
|
| 2419 |
+
|
| 2420 |
+
correct = (extracted == expected)
|
| 2421 |
+
results[op]['total'] += 1
|
| 2422 |
+
all_total += 1
|
| 2423 |
+
if correct:
|
| 2424 |
+
results[op]['correct'] += 1
|
| 2425 |
+
all_correct += 1
|
| 2426 |
+
|
| 2427 |
+
if (batch_start + batch_size) % 200 == 0 or batch_start + batch_size >= n_samples:
|
| 2428 |
+
pct = 100 * all_correct / all_total
|
| 2429 |
+
print(f" Progress: {min(batch_start + batch_size, n_samples)}/{n_samples} | Accuracy: {pct:.2f}%")
|
| 2430 |
+
|
| 2431 |
+
return results, all_correct, all_total
|
| 2432 |
+
|
| 2433 |
+
|
| 2434 |
+
def main_baseline():
|
| 2435 |
+
random.seed(42)
|
| 2436 |
+
torch.manual_seed(42)
|
| 2437 |
+
|
| 2438 |
+
model, tokenizer = load_model()
|
| 2439 |
+
|
| 2440 |
+
# Quick sanity check
|
| 2441 |
+
print("\nSanity check (5 examples):")
|
| 2442 |
+
test_cases = [
|
| 2443 |
+
("5 + 3", 8),
|
| 2444 |
+
("100 - 37", 63),
|
| 2445 |
+
("12 * 11", 132),
|
| 2446 |
+
("50 > 30", 1),
|
| 2447 |
+
("25 < 10", 0),
|
| 2448 |
+
]
|
| 2449 |
+
prompts = [format_prompt(tokenizer, q) for q, _ in test_cases]
|
| 2450 |
+
responses = baseline_generate(model, tokenizer, prompts)
|
| 2451 |
+
for (q, expected), response in zip(test_cases, responses):
|
| 2452 |
+
extracted = extract_answer(response)
|
| 2453 |
+
status = "OK" if extracted == expected else "FAIL"
|
| 2454 |
+
print(f" {q} = {expected} | Model: '{response}' -> {extracted} [{status}]")
|
| 2455 |
+
|
| 2456 |
+
# Full evaluation
|
| 2457 |
+
print("\n" + "=" * 60)
|
| 2458 |
+
print(" BASELINE EVALUATION")
|
| 2459 |
+
print("=" * 60)
|
| 2460 |
+
|
| 2461 |
+
ops = ['add', 'sub', 'mul', 'gt', 'lt', 'eq']
|
| 2462 |
+
results, correct, total = evaluate(model, tokenizer, n_samples=2000, batch_size=64, ops=ops)
|
| 2463 |
+
|
| 2464 |
+
print("\n" + "=" * 60)
|
| 2465 |
+
print(" RESULTS BY OPERATION")
|
| 2466 |
+
print("=" * 60)
|
| 2467 |
+
for op in ops:
|
| 2468 |
+
r = results[op]
|
| 2469 |
+
pct = 100 * r['correct'] / r['total'] if r['total'] > 0 else 0
|
| 2470 |
+
print(f" {op:6}: {r['correct']:4}/{r['total']:4} ({pct:6.2f}%)")
|
| 2471 |
+
|
| 2472 |
+
print("\n" + "=" * 60)
|
| 2473 |
+
print(" OVERALL")
|
| 2474 |
+
print("=" * 60)
|
| 2475 |
+
fitness = correct / total
|
| 2476 |
+
print(f" Correct: {correct}/{total}")
|
| 2477 |
+
print(f" Fitness: {fitness:.4f} ({100*fitness:.2f}%)")
|
| 2478 |
+
print("=" * 60)
|
| 2479 |
+
|
| 2480 |
+
return fitness
|
| 2481 |
+
|
| 2482 |
+
|
| 2483 |
+
def cmd_circuits():
|
| 2484 |
+
print("Testing frozen circuits...")
|
| 2485 |
+
|
| 2486 |
+
circuits = FrozenThresholdCircuits(device='cuda')
|
| 2487 |
+
print(f"Loaded {len(circuits.weights)} tensors")
|
| 2488 |
+
|
| 2489 |
+
a = torch.tensor([[0, 0, 0, 0, 0, 1, 0, 1]], device='cuda', dtype=torch.float32)
|
| 2490 |
+
b = torch.tensor([[0, 0, 0, 0, 0, 0, 1, 1]], device='cuda', dtype=torch.float32)
|
| 2491 |
+
|
| 2492 |
+
result, carry = circuits.add_8bit(a, b)
|
| 2493 |
+
val = sum(int(result[0, i].item()) << (7 - i) for i in range(8))
|
| 2494 |
+
print(f"5 + 3 = {val} (expected 8)")
|
| 2495 |
+
|
| 2496 |
+
a = torch.tensor([[0, 1, 1, 0, 0, 1, 0, 0]], device='cuda', dtype=torch.float32)
|
| 2497 |
+
b = torch.tensor([[0, 0, 1, 0, 0, 1, 0, 1]], device='cuda', dtype=torch.float32)
|
| 2498 |
+
result, _ = circuits.sub_8bit(a, b)
|
| 2499 |
+
val = sum(int(result[0, i].item()) << (7 - i) for i in range(8))
|
| 2500 |
+
print(f"100 - 37 = {val} (expected 63)")
|
| 2501 |
+
|
| 2502 |
+
a = torch.tensor([[0, 0, 0, 0, 1, 1, 0, 0]], device='cuda', dtype=torch.float32)
|
| 2503 |
+
b = torch.tensor([[0, 0, 0, 0, 1, 0, 1, 1]], device='cuda', dtype=torch.float32)
|
| 2504 |
+
result = circuits.mul_8bit(a, b)
|
| 2505 |
+
val = sum(int(result[0, i].item()) << (7 - i) for i in range(8))
|
| 2506 |
+
print(f"12 * 11 = {val} (expected 132)")
|
| 2507 |
+
|
| 2508 |
+
a = torch.tensor([[0, 0, 1, 1, 0, 0, 1, 0]], device='cuda', dtype=torch.float32)
|
| 2509 |
+
b = torch.tensor([[0, 0, 0, 1, 1, 1, 1, 0]], device='cuda', dtype=torch.float32)
|
| 2510 |
+
gt = circuits.compare_gt(a, b)
|
| 2511 |
+
lt = circuits.compare_lt(a, b)
|
| 2512 |
+
eq = circuits.compare_eq(a, b)
|
| 2513 |
+
print(f"50 > 30: {int(gt[0].item())} (expected 1)")
|
| 2514 |
+
print(f"50 < 30: {int(lt[0].item())} (expected 0)")
|
| 2515 |
+
print(f"50 == 30: {int(eq[0].item())} (expected 0)")
|
| 2516 |
+
|
| 2517 |
+
print("\nTesting batched forward...")
|
| 2518 |
+
batch_a = torch.randint(0, 2, (16, 8), device='cuda', dtype=torch.float32)
|
| 2519 |
+
batch_b = torch.randint(0, 2, (16, 8), device='cuda', dtype=torch.float32)
|
| 2520 |
+
op = torch.zeros(16, 6, device='cuda')
|
| 2521 |
+
op[:, 0] = 1.0
|
| 2522 |
+
|
| 2523 |
+
result = circuits(batch_a, batch_b, op)
|
| 2524 |
+
print(f"Batch output shape: {result.shape}")
|
| 2525 |
+
print("Done.")
|
| 2526 |
+
|
| 2527 |
+
|
| 2528 |
+
def cmd_fitness():
|
| 2529 |
+
print("Testing fitness module...")
|
| 2530 |
+
|
| 2531 |
+
batch = generate_batch(8, 'cpu')
|
| 2532 |
+
print(f"\nSample batch:")
|
| 2533 |
+
for i in range(4):
|
| 2534 |
+
a, b = batch['a'][i].item(), batch['b'][i].item()
|
| 2535 |
+
op = idx_to_op(batch['op'][i].item())
|
| 2536 |
+
expected = batch['expected'][i].item()
|
| 2537 |
+
print(f" {a} {op} {b} = {expected}")
|
| 2538 |
+
|
| 2539 |
+
def random_model(a_bits, b_bits, op_onehot):
|
| 2540 |
+
return torch.rand(a_bits.shape[0], 8, device=a_bits.device)
|
| 2541 |
+
|
| 2542 |
+
fitness = compute_fitness(random_model, n_samples=1000, batch_size=100, device='cpu')
|
| 2543 |
+
print(f"\nRandom model fitness: {fitness:.4f} (expected ~0.004 for 8-bit)")
|
| 2544 |
+
|
| 2545 |
+
def perfect_model(a_bits, b_bits, op_onehot):
|
| 2546 |
+
batch_size = a_bits.shape[0]
|
| 2547 |
+
results = torch.zeros(batch_size, 8, device=a_bits.device)
|
| 2548 |
+
for i in range(batch_size):
|
| 2549 |
+
a = sum(int(a_bits[i, j].item()) << (7-j) for j in range(8))
|
| 2550 |
+
b = sum(int(b_bits[i, j].item()) << (7-j) for j in range(8))
|
| 2551 |
+
op_idx = op_onehot[i].argmax().item()
|
| 2552 |
+
result = ground_truth(a, b, idx_to_op(op_idx))
|
| 2553 |
+
for j in range(8):
|
| 2554 |
+
results[i, 7-j] = (result >> j) & 1
|
| 2555 |
+
return results
|
| 2556 |
+
|
| 2557 |
+
fitness = compute_fitness(perfect_model, n_samples=1000, batch_size=100, device='cpu')
|
| 2558 |
+
print(f"Perfect model fitness: {fitness:.4f} (expected 1.0)")
|
| 2559 |
+
|
| 2560 |
+
|
| 2561 |
+
def cmd_model():
|
| 2562 |
import sys
|
| 2563 |
sys.path.insert(0, '.')
|
|
|
|
| 2564 |
|
| 2565 |
print("Testing model components...")
|
| 2566 |
|
|
|
|
| 2611 |
print(f" Total trainable: {total:,}")
|
| 2612 |
|
| 2613 |
print("\nDone.")
|
| 2614 |
+
|
| 2615 |
+
|
| 2616 |
+
COMMANDS = {'circuits': cmd_circuits, 'fitness': cmd_fitness, 'model': cmd_model, 'train': main_train, 'baseline': main_baseline}
|
| 2617 |
+
|
| 2618 |
+
|
| 2619 |
+
if __name__ == "__main__":
|
| 2620 |
+
if len(sys.argv) < 2 or sys.argv[1] not in COMMANDS:
|
| 2621 |
+
sys.exit('commands: ' + ', '.join(COMMANDS))
|
| 2622 |
+
cmd = sys.argv.pop(1)
|
| 2623 |
+
sys.argv[0] += ' ' + cmd
|
| 2624 |
+
sys.exit(COMMANDS[cmd]())
|
llm_integration/smollm2/analyze_smollm2.py
DELETED
|
@@ -1,232 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
SmolLM2-360M-Instruct Architecture Analysis
|
| 3 |
-
For 8bit-threshold-computer LLM Integration Project
|
| 4 |
-
"""
|
| 5 |
-
|
| 6 |
-
import torch
|
| 7 |
-
from transformers import AutoModelForCausalLM, AutoTokenizer, AutoConfig
|
| 8 |
-
from collections import defaultdict
|
| 9 |
-
import json
|
| 10 |
-
|
| 11 |
-
def analyze_smollm2():
|
| 12 |
-
model_name = "HuggingFaceTB/SmolLM2-360M-Instruct"
|
| 13 |
-
|
| 14 |
-
print("=" * 80)
|
| 15 |
-
print("SmolLM2-360M-Instruct Architecture Analysis")
|
| 16 |
-
print("=" * 80)
|
| 17 |
-
|
| 18 |
-
# Load config first
|
| 19 |
-
print("\n[1] Loading model configuration...")
|
| 20 |
-
config = AutoConfig.from_pretrained(model_name)
|
| 21 |
-
print(f"Config loaded: {type(config).__name__}")
|
| 22 |
-
|
| 23 |
-
# Load tokenizer
|
| 24 |
-
print("\n[2] Loading tokenizer...")
|
| 25 |
-
tokenizer = AutoTokenizer.from_pretrained(model_name)
|
| 26 |
-
print(f"Tokenizer loaded: {type(tokenizer).__name__}")
|
| 27 |
-
|
| 28 |
-
# Load model with hidden states output
|
| 29 |
-
print("\n[3] Loading model with output_hidden_states=True...")
|
| 30 |
-
model = AutoModelForCausalLM.from_pretrained(
|
| 31 |
-
model_name,
|
| 32 |
-
output_hidden_states=True,
|
| 33 |
-
torch_dtype=torch.float32
|
| 34 |
-
)
|
| 35 |
-
model.eval()
|
| 36 |
-
print(f"Model loaded: {type(model).__name__}")
|
| 37 |
-
|
| 38 |
-
# ========================================================================
|
| 39 |
-
# ARCHITECTURE CENSUS
|
| 40 |
-
# ========================================================================
|
| 41 |
-
print("\n" + "=" * 80)
|
| 42 |
-
print("ARCHITECTURE CENSUS")
|
| 43 |
-
print("=" * 80)
|
| 44 |
-
|
| 45 |
-
print("\n--- Model Configuration ---")
|
| 46 |
-
config_dict = config.to_dict()
|
| 47 |
-
for key, value in sorted(config_dict.items()):
|
| 48 |
-
print(f" {key}: {value}")
|
| 49 |
-
|
| 50 |
-
print("\n--- Key Architecture Parameters ---")
|
| 51 |
-
print(f" Model type: {config.model_type}")
|
| 52 |
-
print(f" Vocabulary size: {config.vocab_size}")
|
| 53 |
-
print(f" Hidden size: {config.hidden_size}")
|
| 54 |
-
print(f" Intermediate size: {config.intermediate_size}")
|
| 55 |
-
print(f" Number of hidden layers: {config.num_hidden_layers}")
|
| 56 |
-
print(f" Number of attention heads: {config.num_attention_heads}")
|
| 57 |
-
print(f" Number of KV heads: {getattr(config, 'num_key_value_heads', config.num_attention_heads)}")
|
| 58 |
-
print(f" Head dimension: {config.hidden_size // config.num_attention_heads}")
|
| 59 |
-
print(f" Max position embeddings: {config.max_position_embeddings}")
|
| 60 |
-
print(f" RMS norm epsilon: {getattr(config, 'rms_norm_eps', 'N/A')}")
|
| 61 |
-
print(f" Rope theta: {getattr(config, 'rope_theta', 'N/A')}")
|
| 62 |
-
print(f" Tie word embeddings: {getattr(config, 'tie_word_embeddings', 'N/A')}")
|
| 63 |
-
|
| 64 |
-
# ========================================================================
|
| 65 |
-
# WEIGHT INVENTORY
|
| 66 |
-
# ========================================================================
|
| 67 |
-
print("\n" + "=" * 80)
|
| 68 |
-
print("WEIGHT INVENTORY")
|
| 69 |
-
print("=" * 80)
|
| 70 |
-
|
| 71 |
-
total_params = 0
|
| 72 |
-
param_groups = defaultdict(list)
|
| 73 |
-
|
| 74 |
-
for name, param in model.named_parameters():
|
| 75 |
-
total_params += param.numel()
|
| 76 |
-
|
| 77 |
-
# Group by component
|
| 78 |
-
if "embed_tokens" in name:
|
| 79 |
-
group = "Embedding"
|
| 80 |
-
elif "lm_head" in name:
|
| 81 |
-
group = "LM Head"
|
| 82 |
-
elif "norm" in name and "layers" not in name:
|
| 83 |
-
group = "Final Norm"
|
| 84 |
-
elif "layers" in name:
|
| 85 |
-
layer_num = name.split(".")[2]
|
| 86 |
-
if "self_attn" in name:
|
| 87 |
-
group = f"Layer {layer_num} - Attention"
|
| 88 |
-
elif "mlp" in name:
|
| 89 |
-
group = f"Layer {layer_num} - MLP"
|
| 90 |
-
elif "norm" in name:
|
| 91 |
-
group = f"Layer {layer_num} - Norms"
|
| 92 |
-
else:
|
| 93 |
-
group = f"Layer {layer_num} - Other"
|
| 94 |
-
else:
|
| 95 |
-
group = "Other"
|
| 96 |
-
|
| 97 |
-
param_groups[group].append({
|
| 98 |
-
"name": name,
|
| 99 |
-
"shape": tuple(param.shape),
|
| 100 |
-
"numel": param.numel(),
|
| 101 |
-
"dtype": str(param.dtype)
|
| 102 |
-
})
|
| 103 |
-
|
| 104 |
-
print(f"\n--- Total Parameters: {total_params:,} ---")
|
| 105 |
-
print(f" ({total_params / 1e6:.2f}M parameters)")
|
| 106 |
-
|
| 107 |
-
# Print by group
|
| 108 |
-
for group_name in sorted(param_groups.keys()):
|
| 109 |
-
params = param_groups[group_name]
|
| 110 |
-
group_total = sum(p["numel"] for p in params)
|
| 111 |
-
print(f"\n### {group_name} ({group_total:,} params, {group_total/total_params*100:.2f}%)")
|
| 112 |
-
for p in params:
|
| 113 |
-
print(f" {p['name']}")
|
| 114 |
-
print(f" Shape: {p['shape']}, Elements: {p['numel']:,}, Dtype: {p['dtype']}")
|
| 115 |
-
|
| 116 |
-
# ========================================================================
|
| 117 |
-
# TOKENIZATION ANALYSIS
|
| 118 |
-
# ========================================================================
|
| 119 |
-
print("\n" + "=" * 80)
|
| 120 |
-
print("TOKENIZATION ANALYSIS")
|
| 121 |
-
print("=" * 80)
|
| 122 |
-
|
| 123 |
-
test_input = "47 + 86"
|
| 124 |
-
print(f"\n--- Test Input: '{test_input}' ---")
|
| 125 |
-
|
| 126 |
-
tokens = tokenizer(test_input, return_tensors="pt")
|
| 127 |
-
input_ids = tokens["input_ids"][0]
|
| 128 |
-
|
| 129 |
-
print(f"\nInput IDs: {input_ids.tolist()}")
|
| 130 |
-
print(f"Number of tokens: {len(input_ids)}")
|
| 131 |
-
|
| 132 |
-
print("\nToken breakdown:")
|
| 133 |
-
for i, token_id in enumerate(input_ids):
|
| 134 |
-
token_str = tokenizer.decode([token_id])
|
| 135 |
-
print(f" Position {i}: ID={token_id.item():5d}, Token='{token_str}'")
|
| 136 |
-
|
| 137 |
-
# Additional tokenization tests
|
| 138 |
-
print("\n--- Additional Tokenization Tests ---")
|
| 139 |
-
test_cases = ["0", "1", "47", "86", "133", " + ", "="]
|
| 140 |
-
for tc in test_cases:
|
| 141 |
-
ids = tokenizer.encode(tc, add_special_tokens=False)
|
| 142 |
-
decoded = [tokenizer.decode([i]) for i in ids]
|
| 143 |
-
print(f" '{tc}' -> IDs: {ids}, Tokens: {decoded}")
|
| 144 |
-
|
| 145 |
-
# ========================================================================
|
| 146 |
-
# HIDDEN STATE ANALYSIS
|
| 147 |
-
# ========================================================================
|
| 148 |
-
print("\n" + "=" * 80)
|
| 149 |
-
print("HIDDEN STATE ANALYSIS")
|
| 150 |
-
print("=" * 80)
|
| 151 |
-
|
| 152 |
-
print(f"\n--- Running inference on '{test_input}' ---")
|
| 153 |
-
|
| 154 |
-
with torch.no_grad():
|
| 155 |
-
outputs = model(**tokens)
|
| 156 |
-
|
| 157 |
-
hidden_states = outputs.hidden_states
|
| 158 |
-
print(f"\nNumber of hidden state outputs: {len(hidden_states)}")
|
| 159 |
-
print("(This includes embedding output + each layer's output)")
|
| 160 |
-
|
| 161 |
-
print("\nHidden state shapes at each layer:")
|
| 162 |
-
for i, hs in enumerate(hidden_states):
|
| 163 |
-
layer_name = "Embedding" if i == 0 else f"Layer {i-1}"
|
| 164 |
-
print(f" {layer_name}: {tuple(hs.shape)}")
|
| 165 |
-
if i == 0:
|
| 166 |
-
print(f" (batch_size=1, seq_len={hs.shape[1]}, hidden_dim={hs.shape[2]})")
|
| 167 |
-
|
| 168 |
-
# Analyze hidden state statistics at different layers
|
| 169 |
-
print("\n--- Hidden State Statistics (per layer) ---")
|
| 170 |
-
for i, hs in enumerate(hidden_states):
|
| 171 |
-
layer_name = "Embedding" if i == 0 else f"Layer {i-1}"
|
| 172 |
-
hs_flat = hs.view(-1)
|
| 173 |
-
print(f" {layer_name}:")
|
| 174 |
-
print(f" Mean: {hs_flat.mean().item():.6f}")
|
| 175 |
-
print(f" Std: {hs_flat.std().item():.6f}")
|
| 176 |
-
print(f" Min: {hs_flat.min().item():.6f}")
|
| 177 |
-
print(f" Max: {hs_flat.max().item():.6f}")
|
| 178 |
-
|
| 179 |
-
# ========================================================================
|
| 180 |
-
# MODEL STRUCTURE DEEP DIVE
|
| 181 |
-
# ========================================================================
|
| 182 |
-
print("\n" + "=" * 80)
|
| 183 |
-
print("MODEL STRUCTURE DEEP DIVE")
|
| 184 |
-
print("=" * 80)
|
| 185 |
-
|
| 186 |
-
print("\n--- Model Architecture String ---")
|
| 187 |
-
print(model)
|
| 188 |
-
|
| 189 |
-
# ========================================================================
|
| 190 |
-
# SUMMARY DATA FOR REPORT
|
| 191 |
-
# ========================================================================
|
| 192 |
-
summary = {
|
| 193 |
-
"model_name": model_name,
|
| 194 |
-
"total_params": total_params,
|
| 195 |
-
"config": {
|
| 196 |
-
"vocab_size": config.vocab_size,
|
| 197 |
-
"hidden_size": config.hidden_size,
|
| 198 |
-
"intermediate_size": config.intermediate_size,
|
| 199 |
-
"num_hidden_layers": config.num_hidden_layers,
|
| 200 |
-
"num_attention_heads": config.num_attention_heads,
|
| 201 |
-
"num_kv_heads": getattr(config, 'num_key_value_heads', config.num_attention_heads),
|
| 202 |
-
"head_dim": config.hidden_size // config.num_attention_heads,
|
| 203 |
-
"max_position_embeddings": config.max_position_embeddings,
|
| 204 |
-
"rms_norm_eps": getattr(config, 'rms_norm_eps', None),
|
| 205 |
-
"rope_theta": getattr(config, 'rope_theta', None),
|
| 206 |
-
"tie_word_embeddings": getattr(config, 'tie_word_embeddings', None),
|
| 207 |
-
},
|
| 208 |
-
"tokenization": {
|
| 209 |
-
"test_input": test_input,
|
| 210 |
-
"token_ids": input_ids.tolist(),
|
| 211 |
-
"num_tokens": len(input_ids),
|
| 212 |
-
"tokens": [tokenizer.decode([tid]) for tid in input_ids]
|
| 213 |
-
},
|
| 214 |
-
"hidden_states": {
|
| 215 |
-
"num_outputs": len(hidden_states),
|
| 216 |
-
"shape": list(hidden_states[0].shape)
|
| 217 |
-
},
|
| 218 |
-
"param_groups": {k: {"count": len(v), "total": sum(p["numel"] for p in v)} for k, v in param_groups.items()}
|
| 219 |
-
}
|
| 220 |
-
|
| 221 |
-
# Save summary as JSON for report generation
|
| 222 |
-
with open("D:/8bit-threshold-computer/llm_integration/smollm2_analysis.json", "w") as f:
|
| 223 |
-
json.dump(summary, f, indent=2)
|
| 224 |
-
|
| 225 |
-
print("\n" + "=" * 80)
|
| 226 |
-
print("Analysis complete. Summary saved to smollm2_analysis.json")
|
| 227 |
-
print("=" * 80)
|
| 228 |
-
|
| 229 |
-
return summary, model, tokenizer, hidden_states, param_groups
|
| 230 |
-
|
| 231 |
-
if __name__ == "__main__":
|
| 232 |
-
summary, model, tokenizer, hidden_states, param_groups = analyze_smollm2()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
llm_integration/train.py
DELETED
|
@@ -1,955 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Unified training script for threshold circuit LLM integration.
|
| 3 |
-
|
| 4 |
-
Modes:
|
| 5 |
-
--mode router : Train only OpRouter with ground truth bits (sanity check)
|
| 6 |
-
--mode interface : Train BitEncoder + OpRouter with ground truth bits (sanity check)
|
| 7 |
-
--mode llm : Train extractor with LLM hidden states (the real training)
|
| 8 |
-
|
| 9 |
-
LLM mode options:
|
| 10 |
-
--unfreeze_layers N : Unfreeze top N transformer layers (default 0 = fully frozen)
|
| 11 |
-
|
| 12 |
-
Hardware Profile (NVIDIA RTX 6000 Ada 48GB):
|
| 13 |
-
VRAM Scaling (unfreeze_layers=4):
|
| 14 |
-
batch_size | VRAM | %
|
| 15 |
-
-----------+---------+------
|
| 16 |
-
512 | 5,784 | 11.8%
|
| 17 |
-
1,024 | 7,384 | 15.0%
|
| 18 |
-
4,096 | 16,534 | 33.6%
|
| 19 |
-
13,000 | 39,000 | 79.4% <-- recommended for 80% target
|
| 20 |
-
|
| 21 |
-
Examples:
|
| 22 |
-
python train.py --mode llm --epochs 100 --batch_size 256
|
| 23 |
-
python train.py --mode llm --epochs 100 --batch_size 4096 --unfreeze_layers 4
|
| 24 |
-
"""
|
| 25 |
-
|
| 26 |
-
import torch
|
| 27 |
-
import torch.nn as nn
|
| 28 |
-
import torch.optim as optim
|
| 29 |
-
import time
|
| 30 |
-
import argparse
|
| 31 |
-
import random
|
| 32 |
-
|
| 33 |
-
from model import (
|
| 34 |
-
ThresholdALU, DirectCircuitModel, OpRouter,
|
| 35 |
-
ArithmeticModel, OPERATIONS, OP_SYMBOLS
|
| 36 |
-
)
|
| 37 |
-
from circuits import FrozenThresholdCircuits
|
| 38 |
-
from fitness import generate_batch, compute_fitness, compute_loss
|
| 39 |
-
|
| 40 |
-
DEVICE = 'cuda'
|
| 41 |
-
|
| 42 |
-
ONES = ['zero', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine',
|
| 43 |
-
'ten', 'eleven', 'twelve', 'thirteen', 'fourteen', 'fifteen', 'sixteen',
|
| 44 |
-
'seventeen', 'eighteen', 'nineteen']
|
| 45 |
-
TENS = ['', '', 'twenty', 'thirty', 'forty', 'fifty', 'sixty', 'seventy', 'eighty', 'ninety']
|
| 46 |
-
|
| 47 |
-
def int_to_words(n: int) -> str:
|
| 48 |
-
"""Convert integer 0-255 to English words."""
|
| 49 |
-
if n < 0 or n > 255:
|
| 50 |
-
return str(n)
|
| 51 |
-
if n < 20:
|
| 52 |
-
return ONES[n]
|
| 53 |
-
if n < 100:
|
| 54 |
-
if n % 10 == 0:
|
| 55 |
-
return TENS[n // 10]
|
| 56 |
-
return f"{TENS[n // 10]} {ONES[n % 10]}"
|
| 57 |
-
if n % 100 == 0:
|
| 58 |
-
return f"{ONES[n // 100]} hundred"
|
| 59 |
-
if n % 100 < 20:
|
| 60 |
-
return f"{ONES[n // 100]} hundred {ONES[n % 100]}"
|
| 61 |
-
if n % 10 == 0:
|
| 62 |
-
return f"{ONES[n // 100]} hundred {TENS[(n % 100) // 10]}"
|
| 63 |
-
return f"{ONES[n // 100]} hundred {TENS[(n % 100) // 10]} {ONES[n % 10]}"
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
def int_to_bits(val: int, device: str = 'cuda') -> torch.Tensor:
|
| 67 |
-
bits = torch.zeros(8, device=device)
|
| 68 |
-
for i in range(8):
|
| 69 |
-
bits[7-i] = (val >> i) & 1
|
| 70 |
-
return bits
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
def bits_to_int(bits: torch.Tensor) -> int:
|
| 74 |
-
val = 0
|
| 75 |
-
for i in range(8):
|
| 76 |
-
if bits[i].item() > 0.5:
|
| 77 |
-
val += 1 << (7-i)
|
| 78 |
-
return val
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
NL_TEMPLATES = {
|
| 82 |
-
'add': [
|
| 83 |
-
"What is {a} plus {b}?",
|
| 84 |
-
"Calculate {a} + {b}",
|
| 85 |
-
"Add {a} and {b}",
|
| 86 |
-
"What's the sum of {a} and {b}?",
|
| 87 |
-
"If I have {a} and get {b} more, how many total?",
|
| 88 |
-
"{a} + {b} = ?",
|
| 89 |
-
"Compute {a} plus {b}",
|
| 90 |
-
],
|
| 91 |
-
'sub': [
|
| 92 |
-
"What is {a} minus {b}?",
|
| 93 |
-
"Calculate {a} - {b}",
|
| 94 |
-
"Subtract {b} from {a}",
|
| 95 |
-
"What's {a} take away {b}?",
|
| 96 |
-
"If I have {a} and lose {b}, how many left?",
|
| 97 |
-
"{a} - {b} = ?",
|
| 98 |
-
"Compute {a} minus {b}",
|
| 99 |
-
],
|
| 100 |
-
'mul': [
|
| 101 |
-
"What is {a} times {b}?",
|
| 102 |
-
"Calculate {a} * {b}",
|
| 103 |
-
"Multiply {a} by {b}",
|
| 104 |
-
"What's {a} multiplied by {b}?",
|
| 105 |
-
"{a} * {b} = ?",
|
| 106 |
-
"Compute {a} times {b}",
|
| 107 |
-
"What is the product of {a} and {b}?",
|
| 108 |
-
],
|
| 109 |
-
'gt': [
|
| 110 |
-
"Is {a} greater than {b}?",
|
| 111 |
-
"Is {a} > {b}?",
|
| 112 |
-
"Check if {a} is larger than {b}",
|
| 113 |
-
"Compare: is {a} more than {b}?",
|
| 114 |
-
"{a} > {b}?",
|
| 115 |
-
],
|
| 116 |
-
'lt': [
|
| 117 |
-
"Is {a} less than {b}?",
|
| 118 |
-
"Is {a} < {b}?",
|
| 119 |
-
"Check if {a} is smaller than {b}",
|
| 120 |
-
"Compare: is {a} fewer than {b}?",
|
| 121 |
-
"{a} < {b}?",
|
| 122 |
-
],
|
| 123 |
-
'eq': [
|
| 124 |
-
"Is {a} equal to {b}?",
|
| 125 |
-
"Is {a} == {b}?",
|
| 126 |
-
"Does {a} equal {b}?",
|
| 127 |
-
"Check if {a} equals {b}",
|
| 128 |
-
"Are {a} and {b} the same?",
|
| 129 |
-
],
|
| 130 |
-
}
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
def generate_problem(max_val: int = 255):
|
| 134 |
-
"""
|
| 135 |
-
Generate a random arithmetic problem for LLM training.
|
| 136 |
-
Randomly mixes digit and word formats.
|
| 137 |
-
"""
|
| 138 |
-
a = random.randint(0, max_val)
|
| 139 |
-
b = random.randint(0, max_val)
|
| 140 |
-
op = random.choice(OPERATIONS)
|
| 141 |
-
|
| 142 |
-
fmt = random.choice(['digits', 'words', 'nl_digits', 'nl_words'])
|
| 143 |
-
|
| 144 |
-
if fmt == 'digits':
|
| 145 |
-
sym = OP_SYMBOLS[op]
|
| 146 |
-
text = f"{a} {sym} {b}"
|
| 147 |
-
elif fmt == 'words':
|
| 148 |
-
a_word = int_to_words(a)
|
| 149 |
-
b_word = int_to_words(b)
|
| 150 |
-
op_word = {'add': 'plus', 'sub': 'minus', 'mul': 'times',
|
| 151 |
-
'gt': 'greater than', 'lt': 'less than', 'eq': 'equals'}[op]
|
| 152 |
-
text = f"{a_word} {op_word} {b_word}"
|
| 153 |
-
elif fmt == 'nl_digits':
|
| 154 |
-
template = random.choice(NL_TEMPLATES[op])
|
| 155 |
-
text = template.format(a=a, b=b)
|
| 156 |
-
else:
|
| 157 |
-
template = random.choice(NL_TEMPLATES[op])
|
| 158 |
-
text = template.format(a=int_to_words(a), b=int_to_words(b))
|
| 159 |
-
|
| 160 |
-
if op == 'add':
|
| 161 |
-
result = (a + b) & 0xFF
|
| 162 |
-
elif op == 'sub':
|
| 163 |
-
result = (a - b) & 0xFF
|
| 164 |
-
elif op == 'mul':
|
| 165 |
-
result = (a * b) & 0xFF
|
| 166 |
-
elif op == 'gt':
|
| 167 |
-
result = 1 if a > b else 0
|
| 168 |
-
elif op == 'lt':
|
| 169 |
-
result = 1 if a < b else 0
|
| 170 |
-
elif op == 'eq':
|
| 171 |
-
result = 1 if a == b else 0
|
| 172 |
-
|
| 173 |
-
return text, a, b, op, result
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
def get_curriculum_max(epoch: int, total_epochs: int) -> int:
|
| 177 |
-
"""
|
| 178 |
-
Curriculum learning: start with small numbers, gradually increase.
|
| 179 |
-
Epoch 0-20%: 0-9 (single digit)
|
| 180 |
-
Epoch 20-50%: 0-99 (two digit)
|
| 181 |
-
Epoch 50-100%: 0-255 (full range)
|
| 182 |
-
"""
|
| 183 |
-
progress = epoch / total_epochs
|
| 184 |
-
if progress < 0.2:
|
| 185 |
-
return 9
|
| 186 |
-
elif progress < 0.5:
|
| 187 |
-
return 99
|
| 188 |
-
else:
|
| 189 |
-
return 255
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
def train_router(epochs: int = 100, batch_size: int = 256, lr: float = 1e-2, device: str = 'cuda'):
|
| 193 |
-
"""Train only the router with ground truth bits."""
|
| 194 |
-
print("=" * 70)
|
| 195 |
-
print(" ROUTER-ONLY TRAINING (Ground Truth Bits)")
|
| 196 |
-
print("=" * 70)
|
| 197 |
-
|
| 198 |
-
circuits = FrozenThresholdCircuits(device=device)
|
| 199 |
-
router = OpRouter(input_dim=16 + 6, hidden_dim=64, n_ops=6).to(device)
|
| 200 |
-
|
| 201 |
-
print(f"\nRouter parameters: {sum(p.numel() for p in router.parameters()):,}")
|
| 202 |
-
|
| 203 |
-
def model_fn(a_bits, b_bits, op_onehot):
|
| 204 |
-
x = torch.cat([a_bits, b_bits, op_onehot], dim=-1)
|
| 205 |
-
op_weights = router(x)
|
| 206 |
-
return circuits(a_bits, b_bits, op_weights)
|
| 207 |
-
|
| 208 |
-
initial_fitness = compute_fitness(model_fn, n_samples=1000, device=device)
|
| 209 |
-
print(f"Initial fitness: {initial_fitness:.4f}")
|
| 210 |
-
|
| 211 |
-
optimizer = optim.AdamW(router.parameters(), lr=lr)
|
| 212 |
-
scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=epochs)
|
| 213 |
-
|
| 214 |
-
print("\nTraining...")
|
| 215 |
-
print("-" * 70)
|
| 216 |
-
|
| 217 |
-
best_fitness = initial_fitness
|
| 218 |
-
start_time = time.perf_counter()
|
| 219 |
-
|
| 220 |
-
for epoch in range(epochs):
|
| 221 |
-
router.train()
|
| 222 |
-
epoch_loss = 0.0
|
| 223 |
-
|
| 224 |
-
for _ in range(100):
|
| 225 |
-
batch = generate_batch(batch_size, device)
|
| 226 |
-
|
| 227 |
-
optimizer.zero_grad()
|
| 228 |
-
|
| 229 |
-
x = torch.cat([batch['a_bits'], batch['b_bits'], batch['op_onehot']], dim=-1)
|
| 230 |
-
op_weights = router(x)
|
| 231 |
-
pred_bits = circuits(batch['a_bits'], batch['b_bits'], op_weights)
|
| 232 |
-
|
| 233 |
-
loss = compute_loss(pred_bits, batch['expected_bits'])
|
| 234 |
-
loss.backward()
|
| 235 |
-
optimizer.step()
|
| 236 |
-
|
| 237 |
-
epoch_loss += loss.item()
|
| 238 |
-
|
| 239 |
-
scheduler.step()
|
| 240 |
-
|
| 241 |
-
if (epoch + 1) % 10 == 0 or epoch == 0:
|
| 242 |
-
router.eval()
|
| 243 |
-
fitness, details = compute_fitness(model_fn, n_samples=2000, device=device, return_details=True)
|
| 244 |
-
elapsed = time.perf_counter() - start_time
|
| 245 |
-
|
| 246 |
-
if fitness > best_fitness:
|
| 247 |
-
best_fitness = fitness
|
| 248 |
-
marker = " *"
|
| 249 |
-
else:
|
| 250 |
-
marker = ""
|
| 251 |
-
|
| 252 |
-
print(f"Epoch {epoch+1:3d} | Loss: {epoch_loss/100:.4f} | "
|
| 253 |
-
f"Fitness: {fitness:.4f}{marker} | Time: {elapsed:.1f}s")
|
| 254 |
-
|
| 255 |
-
if fitness >= 0.9999:
|
| 256 |
-
print("\n TARGET: 100% FITNESS ACHIEVED")
|
| 257 |
-
break
|
| 258 |
-
|
| 259 |
-
print("\n" + "=" * 70)
|
| 260 |
-
print(" RESULTS")
|
| 261 |
-
print("=" * 70)
|
| 262 |
-
|
| 263 |
-
router.eval()
|
| 264 |
-
final_fitness, details = compute_fitness(model_fn, n_samples=5000, device=device, return_details=True)
|
| 265 |
-
|
| 266 |
-
print(f"\nFinal fitness: {final_fitness:.4f}")
|
| 267 |
-
print(f"\nPer-operation:")
|
| 268 |
-
for op in OPERATIONS:
|
| 269 |
-
acc = details['by_op'][op]['accuracy']
|
| 270 |
-
print(f" {op}: {acc:.4f}")
|
| 271 |
-
|
| 272 |
-
print(f"\nTotal time: {time.perf_counter() - start_time:.1f}s")
|
| 273 |
-
|
| 274 |
-
if final_fitness >= 0.99:
|
| 275 |
-
print("\nCONCLUSION: Router successfully learned operation dispatch.")
|
| 276 |
-
print(" With correct bit encoding, 100% is achievable.")
|
| 277 |
-
|
| 278 |
-
save_path = "D:/8bit-threshold-computer/llm_integration/trained/router.pt"
|
| 279 |
-
torch.save({
|
| 280 |
-
'router_state_dict': router.state_dict(),
|
| 281 |
-
'final_fitness': final_fitness,
|
| 282 |
-
'params': sum(p.numel() for p in router.parameters()),
|
| 283 |
-
}, save_path)
|
| 284 |
-
print(f"\nSaved trained router to: {save_path}")
|
| 285 |
-
|
| 286 |
-
return router, final_fitness
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
def get_gpu_memory():
|
| 290 |
-
"""Get GPU memory usage in MB."""
|
| 291 |
-
if torch.cuda.is_available():
|
| 292 |
-
return torch.cuda.memory_allocated() / 1024 / 1024, torch.cuda.max_memory_allocated() / 1024 / 1024
|
| 293 |
-
return 0, 0
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
def train_interface(epochs: int = 200, batch_size: int = 512, lr: float = 1e-3,
|
| 297 |
-
eval_interval: int = 10, device: str = 'cuda'):
|
| 298 |
-
"""Train BitEncoder + OpRouter with ground truth bits."""
|
| 299 |
-
print("=" * 70)
|
| 300 |
-
print(" INTERFACE TRAINING (Encoder + Router)")
|
| 301 |
-
print("=" * 70)
|
| 302 |
-
print(f" Started at: {time.strftime('%H:%M:%S')}")
|
| 303 |
-
|
| 304 |
-
print("\n[1/4] Verifying frozen circuits...")
|
| 305 |
-
print(" Creating DirectCircuitModel...", end=" ", flush=True)
|
| 306 |
-
direct_model = DirectCircuitModel(device=device)
|
| 307 |
-
mem, max_mem = get_gpu_memory()
|
| 308 |
-
print(f"done. VRAM: {mem:.0f}MB")
|
| 309 |
-
|
| 310 |
-
def direct_fn(a, b, op):
|
| 311 |
-
return direct_model(a, b, op)
|
| 312 |
-
|
| 313 |
-
print(" Running fitness check (1000 samples)...", end=" ", flush=True)
|
| 314 |
-
circuit_fitness = compute_fitness(direct_fn, n_samples=1000, device=device)
|
| 315 |
-
print(f"done. Fitness: {circuit_fitness:.4f}")
|
| 316 |
-
if circuit_fitness < 0.999:
|
| 317 |
-
print(" ERROR: Circuits not achieving 100%. Aborting.")
|
| 318 |
-
return None, 0.0
|
| 319 |
-
print(" STATUS: PASS")
|
| 320 |
-
|
| 321 |
-
print("\n[2/4] Initializing model...")
|
| 322 |
-
print(" Creating ThresholdALU...", end=" ", flush=True)
|
| 323 |
-
model = ThresholdALU(device=device)
|
| 324 |
-
mem, max_mem = get_gpu_memory()
|
| 325 |
-
print(f"done. VRAM: {mem:.0f}MB")
|
| 326 |
-
|
| 327 |
-
trainable_params = sum(p.numel() for p in model.parameters() if p.requires_grad)
|
| 328 |
-
print(f" Trainable parameters: {trainable_params:,}")
|
| 329 |
-
|
| 330 |
-
def model_fn(a, b, op):
|
| 331 |
-
return model(a, b, op)
|
| 332 |
-
|
| 333 |
-
print(" Running initial fitness check...", end=" ", flush=True)
|
| 334 |
-
initial_fitness = compute_fitness(model_fn, n_samples=1000, device=device)
|
| 335 |
-
print(f"done. Fitness: {initial_fitness:.4f}")
|
| 336 |
-
|
| 337 |
-
print("\n[3/4] Setting up training...")
|
| 338 |
-
print(" Creating optimizer...", end=" ", flush=True)
|
| 339 |
-
optimizer = optim.AdamW(model.parameters(), lr=lr)
|
| 340 |
-
print("done.")
|
| 341 |
-
print(" Creating scheduler...", end=" ", flush=True)
|
| 342 |
-
scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=epochs)
|
| 343 |
-
print("done.")
|
| 344 |
-
|
| 345 |
-
print(f" Config: lr={lr}, batch_size={batch_size}, epochs={epochs}")
|
| 346 |
-
|
| 347 |
-
print("\n[4/4] Training...")
|
| 348 |
-
print(" Generating first batch to warm up...", end=" ", flush=True)
|
| 349 |
-
warmup_batch = generate_batch(batch_size, device)
|
| 350 |
-
mem, max_mem = get_gpu_memory()
|
| 351 |
-
print(f"done. VRAM: {mem:.0f}MB (max: {max_mem:.0f}MB)")
|
| 352 |
-
|
| 353 |
-
print("-" * 70)
|
| 354 |
-
|
| 355 |
-
best_fitness = initial_fitness
|
| 356 |
-
start_time = time.perf_counter()
|
| 357 |
-
n_batches = 100
|
| 358 |
-
|
| 359 |
-
for epoch in range(epochs):
|
| 360 |
-
model.train()
|
| 361 |
-
epoch_loss = 0.0
|
| 362 |
-
epoch_start = time.perf_counter()
|
| 363 |
-
|
| 364 |
-
for batch_idx in range(n_batches):
|
| 365 |
-
batch = generate_batch(batch_size, device)
|
| 366 |
-
|
| 367 |
-
optimizer.zero_grad()
|
| 368 |
-
|
| 369 |
-
pred_bits = model(batch['a_bits'], batch['b_bits'], batch['op_onehot'])
|
| 370 |
-
|
| 371 |
-
loss = compute_loss(pred_bits, batch['expected_bits'])
|
| 372 |
-
|
| 373 |
-
loss.backward()
|
| 374 |
-
optimizer.step()
|
| 375 |
-
|
| 376 |
-
epoch_loss += loss.item()
|
| 377 |
-
|
| 378 |
-
if batch_idx == 0 and epoch == 0:
|
| 379 |
-
mem, max_mem = get_gpu_memory()
|
| 380 |
-
print(f" First forward/backward done. VRAM: {mem:.0f}MB (max: {max_mem:.0f}MB)")
|
| 381 |
-
|
| 382 |
-
if (batch_idx + 1) % 25 == 0:
|
| 383 |
-
avg_so_far = epoch_loss / (batch_idx + 1)
|
| 384 |
-
print(f" Epoch {epoch+1} batch {batch_idx+1}/{n_batches} | loss: {avg_so_far:.4f}", flush=True)
|
| 385 |
-
|
| 386 |
-
scheduler.step()
|
| 387 |
-
|
| 388 |
-
avg_loss = epoch_loss / n_batches
|
| 389 |
-
epoch_time = time.perf_counter() - epoch_start
|
| 390 |
-
|
| 391 |
-
if (epoch + 1) % 5 == 0 or epoch == 0: # Eval every 5 epochs
|
| 392 |
-
model.eval()
|
| 393 |
-
fitness, details = compute_fitness(
|
| 394 |
-
model_fn, n_samples=2000, device=device, return_details=True
|
| 395 |
-
)
|
| 396 |
-
|
| 397 |
-
elapsed = time.perf_counter() - start_time
|
| 398 |
-
|
| 399 |
-
if fitness > best_fitness:
|
| 400 |
-
best_fitness = fitness
|
| 401 |
-
marker = " *"
|
| 402 |
-
else:
|
| 403 |
-
marker = ""
|
| 404 |
-
|
| 405 |
-
mem, _ = get_gpu_memory()
|
| 406 |
-
print(f"Epoch {epoch+1:4d} | Loss: {avg_loss:.4f} | "
|
| 407 |
-
f"Fitness: {fitness:.4f}{marker} | "
|
| 408 |
-
f"LR: {scheduler.get_last_lr()[0]:.2e} | "
|
| 409 |
-
f"VRAM: {mem:.0f}MB | "
|
| 410 |
-
f"Time: {elapsed:.1f}s ({epoch_time:.1f}s/epoch)")
|
| 411 |
-
|
| 412 |
-
if fitness >= 0.9999:
|
| 413 |
-
print("\n" + "=" * 70)
|
| 414 |
-
print(" TARGET ACHIEVED: 100% FITNESS")
|
| 415 |
-
print("=" * 70)
|
| 416 |
-
break
|
| 417 |
-
|
| 418 |
-
print("\n" + "=" * 70)
|
| 419 |
-
print(" TRAINING COMPLETE")
|
| 420 |
-
print("=" * 70)
|
| 421 |
-
|
| 422 |
-
model.eval()
|
| 423 |
-
final_fitness, details = compute_fitness(
|
| 424 |
-
model_fn, n_samples=5000, device=device, return_details=True
|
| 425 |
-
)
|
| 426 |
-
|
| 427 |
-
print(f"\nFinal fitness: {final_fitness:.4f}")
|
| 428 |
-
print(f"Best fitness: {best_fitness:.4f}")
|
| 429 |
-
print(f"\nPer-operation breakdown:")
|
| 430 |
-
for op in OPERATIONS:
|
| 431 |
-
acc = details['by_op'][op]['accuracy']
|
| 432 |
-
print(f" {op:6}: {acc:.4f}")
|
| 433 |
-
|
| 434 |
-
print(f"\nTotal time: {time.perf_counter() - start_time:.1f}s")
|
| 435 |
-
|
| 436 |
-
save_path = "D:/8bit-threshold-computer/llm_integration/trained/interface.pt"
|
| 437 |
-
torch.save({
|
| 438 |
-
'encoder_state_dict': model.encoder.state_dict(),
|
| 439 |
-
'router_state_dict': model.router.state_dict(),
|
| 440 |
-
'final_fitness': final_fitness,
|
| 441 |
-
'best_fitness': best_fitness,
|
| 442 |
-
}, save_path)
|
| 443 |
-
print(f"\nSaved trained model to: {save_path}")
|
| 444 |
-
|
| 445 |
-
return model, final_fitness
|
| 446 |
-
|
| 447 |
-
|
| 448 |
-
def compute_llm_loss(pred_bits, a_bits, b_bits, op_logits,
|
| 449 |
-
target_result, target_a, target_b, target_op_idx,
|
| 450 |
-
bit_weight: float = 2.0):
|
| 451 |
-
"""
|
| 452 |
-
Multi-component loss for LLM training.
|
| 453 |
-
bit_weight: multiplier for a/b bit losses (default 2x since extraction is the bottleneck)
|
| 454 |
-
"""
|
| 455 |
-
result_loss = nn.functional.binary_cross_entropy_with_logits(
|
| 456 |
-
pred_bits, target_result
|
| 457 |
-
)
|
| 458 |
-
|
| 459 |
-
a_bits_safe = torch.clamp(a_bits, 0.0, 1.0)
|
| 460 |
-
b_bits_safe = torch.clamp(b_bits, 0.0, 1.0)
|
| 461 |
-
a_bits_safe = torch.nan_to_num(a_bits_safe, nan=0.5, posinf=1.0, neginf=0.0)
|
| 462 |
-
b_bits_safe = torch.nan_to_num(b_bits_safe, nan=0.5, posinf=1.0, neginf=0.0)
|
| 463 |
-
|
| 464 |
-
a_loss = nn.functional.binary_cross_entropy(
|
| 465 |
-
torch.clamp(a_bits_safe, 1e-6, 1-1e-6), target_a
|
| 466 |
-
)
|
| 467 |
-
b_loss = nn.functional.binary_cross_entropy(
|
| 468 |
-
torch.clamp(b_bits_safe, 1e-6, 1-1e-6), target_b
|
| 469 |
-
)
|
| 470 |
-
|
| 471 |
-
op_loss = nn.functional.cross_entropy(op_logits, target_op_idx)
|
| 472 |
-
|
| 473 |
-
total = result_loss + bit_weight * a_loss + bit_weight * b_loss + op_loss
|
| 474 |
-
total = torch.nan_to_num(total, nan=10.0, posinf=10.0, neginf=0.0)
|
| 475 |
-
|
| 476 |
-
return total, {
|
| 477 |
-
'result': result_loss.item() if not torch.isnan(result_loss) else 10.0,
|
| 478 |
-
'a': a_loss.item() if not torch.isnan(a_loss) else 10.0,
|
| 479 |
-
'b': b_loss.item() if not torch.isnan(b_loss) else 10.0,
|
| 480 |
-
'op': op_loss.item() if not torch.isnan(op_loss) else 10.0
|
| 481 |
-
}
|
| 482 |
-
|
| 483 |
-
|
| 484 |
-
def value_to_digits(value: int) -> list:
|
| 485 |
-
"""Convert integer value to list of digits (hundreds, tens, ones)."""
|
| 486 |
-
digits = []
|
| 487 |
-
for place in [100, 10, 1]:
|
| 488 |
-
digit = (value // place) % 10
|
| 489 |
-
digits.append(digit)
|
| 490 |
-
return digits
|
| 491 |
-
|
| 492 |
-
|
| 493 |
-
def compute_positional_digit_loss(pred_bits, op_logits, a_digit_logits_list, b_digit_logits_list,
|
| 494 |
-
target_result, target_op_idx, target_a_values, target_b_values,
|
| 495 |
-
device, digit_weight: float = 5.0):
|
| 496 |
-
"""
|
| 497 |
-
Loss for positional digit extraction with DIRECT digit supervision.
|
| 498 |
-
|
| 499 |
-
This provides strong gradients by directly supervising digit classification
|
| 500 |
-
instead of going through the value -> bits conversion.
|
| 501 |
-
"""
|
| 502 |
-
result_loss = nn.functional.binary_cross_entropy_with_logits(
|
| 503 |
-
pred_bits, target_result
|
| 504 |
-
)
|
| 505 |
-
|
| 506 |
-
op_loss = nn.functional.cross_entropy(op_logits, target_op_idx)
|
| 507 |
-
|
| 508 |
-
a_digit_loss = torch.tensor(0.0, device=device)
|
| 509 |
-
b_digit_loss = torch.tensor(0.0, device=device)
|
| 510 |
-
n_a_digits = 0
|
| 511 |
-
n_b_digits = 0
|
| 512 |
-
|
| 513 |
-
for i, (a_logits_list, b_logits_list) in enumerate(zip(a_digit_logits_list, b_digit_logits_list)):
|
| 514 |
-
target_a = target_a_values[i].item()
|
| 515 |
-
target_b = target_b_values[i].item()
|
| 516 |
-
|
| 517 |
-
a_digits = value_to_digits(int(target_a))
|
| 518 |
-
b_digits = value_to_digits(int(target_b))
|
| 519 |
-
|
| 520 |
-
n_a = len(a_logits_list)
|
| 521 |
-
n_b = len(b_logits_list)
|
| 522 |
-
|
| 523 |
-
if n_a > 0:
|
| 524 |
-
target_a_digits = a_digits[-n_a:]
|
| 525 |
-
for j, logits in enumerate(a_logits_list):
|
| 526 |
-
target_digit = torch.tensor([target_a_digits[j]], device=device, dtype=torch.long)
|
| 527 |
-
a_digit_loss = a_digit_loss + nn.functional.cross_entropy(logits.unsqueeze(0), target_digit)
|
| 528 |
-
n_a_digits += 1
|
| 529 |
-
|
| 530 |
-
if n_b > 0:
|
| 531 |
-
target_b_digits = b_digits[-n_b:]
|
| 532 |
-
for j, logits in enumerate(b_logits_list):
|
| 533 |
-
target_digit = torch.tensor([target_b_digits[j]], device=device, dtype=torch.long)
|
| 534 |
-
b_digit_loss = b_digit_loss + nn.functional.cross_entropy(logits.unsqueeze(0), target_digit)
|
| 535 |
-
n_b_digits += 1
|
| 536 |
-
|
| 537 |
-
if n_a_digits > 0:
|
| 538 |
-
a_digit_loss = a_digit_loss / n_a_digits
|
| 539 |
-
if n_b_digits > 0:
|
| 540 |
-
b_digit_loss = b_digit_loss / n_b_digits
|
| 541 |
-
|
| 542 |
-
total = result_loss + digit_weight * a_digit_loss + digit_weight * b_digit_loss + op_loss
|
| 543 |
-
total = torch.nan_to_num(total, nan=10.0, posinf=10.0, neginf=0.0)
|
| 544 |
-
|
| 545 |
-
return total, {
|
| 546 |
-
'result': result_loss.item() if not torch.isnan(result_loss) else 10.0,
|
| 547 |
-
'a_digit': a_digit_loss.item() if not torch.isnan(a_digit_loss) else 10.0,
|
| 548 |
-
'b_digit': b_digit_loss.item() if not torch.isnan(b_digit_loss) else 10.0,
|
| 549 |
-
'op': op_loss.item() if not torch.isnan(op_loss) else 10.0
|
| 550 |
-
}
|
| 551 |
-
|
| 552 |
-
|
| 553 |
-
def compute_hybrid_loss(pred_bits, op_logits, used_lookup,
|
| 554 |
-
a_digit_logits, b_digit_logits,
|
| 555 |
-
target_result, target_a_values, target_b_values, target_op_idx,
|
| 556 |
-
device, digit_weight: float = 2.0):
|
| 557 |
-
"""
|
| 558 |
-
Loss for hybrid extraction with digit-level prediction.
|
| 559 |
-
|
| 560 |
-
Uses cross-entropy on each digit (hundreds, tens, ones) for word samples.
|
| 561 |
-
Samples using digit lookup are already 100% accurate - no loss computed.
|
| 562 |
-
"""
|
| 563 |
-
result_loss = nn.functional.binary_cross_entropy_with_logits(
|
| 564 |
-
pred_bits, target_result
|
| 565 |
-
)
|
| 566 |
-
|
| 567 |
-
op_loss = nn.functional.cross_entropy(op_logits, target_op_idx)
|
| 568 |
-
|
| 569 |
-
word_mask = ~used_lookup
|
| 570 |
-
n_words = word_mask.sum().item()
|
| 571 |
-
|
| 572 |
-
if n_words > 0 and a_digit_logits is not None and b_digit_logits is not None:
|
| 573 |
-
target_a_word = target_a_values[word_mask].long()
|
| 574 |
-
target_b_word = target_b_values[word_mask].long()
|
| 575 |
-
|
| 576 |
-
a_hundreds = target_a_word // 100
|
| 577 |
-
a_tens = (target_a_word % 100) // 10
|
| 578 |
-
a_ones = target_a_word % 10
|
| 579 |
-
|
| 580 |
-
b_hundreds = target_b_word // 100
|
| 581 |
-
b_tens = (target_b_word % 100) // 10
|
| 582 |
-
b_ones = target_b_word % 10
|
| 583 |
-
|
| 584 |
-
a_logits = a_digit_logits.view(-1, 3, 10)
|
| 585 |
-
b_logits = b_digit_logits.view(-1, 3, 10)
|
| 586 |
-
|
| 587 |
-
a_digit_loss = (
|
| 588 |
-
nn.functional.cross_entropy(a_logits[:, 0], a_hundreds) +
|
| 589 |
-
nn.functional.cross_entropy(a_logits[:, 1], a_tens) +
|
| 590 |
-
nn.functional.cross_entropy(a_logits[:, 2], a_ones)
|
| 591 |
-
) / 3.0
|
| 592 |
-
|
| 593 |
-
b_digit_loss = (
|
| 594 |
-
nn.functional.cross_entropy(b_logits[:, 0], b_hundreds) +
|
| 595 |
-
nn.functional.cross_entropy(b_logits[:, 1], b_tens) +
|
| 596 |
-
nn.functional.cross_entropy(b_logits[:, 2], b_ones)
|
| 597 |
-
) / 3.0
|
| 598 |
-
else:
|
| 599 |
-
a_digit_loss = torch.tensor(0.0, device=device)
|
| 600 |
-
b_digit_loss = torch.tensor(0.0, device=device)
|
| 601 |
-
|
| 602 |
-
total = result_loss + op_loss + digit_weight * (a_digit_loss + b_digit_loss)
|
| 603 |
-
total = torch.nan_to_num(total, nan=10.0, posinf=10.0, neginf=0.0)
|
| 604 |
-
|
| 605 |
-
return total, {
|
| 606 |
-
'result': result_loss.item() if not torch.isnan(result_loss) else 10.0,
|
| 607 |
-
'a_digit': a_digit_loss.item() if not torch.isnan(a_digit_loss) else 10.0,
|
| 608 |
-
'b_digit': b_digit_loss.item() if not torch.isnan(b_digit_loss) else 10.0,
|
| 609 |
-
'op': op_loss.item() if not torch.isnan(op_loss) else 10.0,
|
| 610 |
-
'n_words': n_words,
|
| 611 |
-
'n_lookup': used_lookup.sum().item()
|
| 612 |
-
}
|
| 613 |
-
|
| 614 |
-
|
| 615 |
-
def evaluate_llm(model, n_samples: int = 500):
|
| 616 |
-
"""Evaluate LLM model on random problems (mixed digit/word format)."""
|
| 617 |
-
model.extractor.eval()
|
| 618 |
-
correct = 0
|
| 619 |
-
op_correct = 0
|
| 620 |
-
|
| 621 |
-
for _ in range(n_samples):
|
| 622 |
-
text, a, b, op, expected = generate_problem()
|
| 623 |
-
|
| 624 |
-
with torch.no_grad():
|
| 625 |
-
outputs = model([text])
|
| 626 |
-
result_bits = outputs[0]
|
| 627 |
-
op_logits = outputs[3]
|
| 628 |
-
|
| 629 |
-
pred_result = bits_to_int(result_bits[0])
|
| 630 |
-
pred_op = OPERATIONS[op_logits[0].argmax().item()]
|
| 631 |
-
|
| 632 |
-
if pred_result == expected:
|
| 633 |
-
correct += 1
|
| 634 |
-
if pred_op == op:
|
| 635 |
-
op_correct += 1
|
| 636 |
-
|
| 637 |
-
model.extractor.train()
|
| 638 |
-
return correct / n_samples, op_correct / n_samples
|
| 639 |
-
|
| 640 |
-
|
| 641 |
-
def train_llm(epochs: int = 100, batch_size: int = 256, lr: float = 3e-4,
|
| 642 |
-
unfreeze_layers: int = 0, extract_layer: int = -1,
|
| 643 |
-
position_extract: bool = False, digit_pred: bool = False,
|
| 644 |
-
positional_digit: bool = False, device: str = 'cuda'):
|
| 645 |
-
"""
|
| 646 |
-
Train extractor with LLM hidden states.
|
| 647 |
-
|
| 648 |
-
Args:
|
| 649 |
-
unfreeze_layers: Number of top transformer layers to unfreeze (0 = fully frozen)
|
| 650 |
-
extract_layer: Which layer to extract from (-1 = last)
|
| 651 |
-
position_extract: Use position-specific extraction (legacy)
|
| 652 |
-
digit_pred: Predict digits instead of bits (legacy)
|
| 653 |
-
positional_digit: Use positional digit extraction (legacy, 100% on digits only)
|
| 654 |
-
"""
|
| 655 |
-
hybrid = not (positional_digit or position_extract or digit_pred)
|
| 656 |
-
|
| 657 |
-
print("=" * 70)
|
| 658 |
-
print(" LLM TRAINING")
|
| 659 |
-
if unfreeze_layers > 0:
|
| 660 |
-
print(f" {unfreeze_layers} transformer layers unfrozen")
|
| 661 |
-
else:
|
| 662 |
-
print(" LLM frozen")
|
| 663 |
-
if extract_layer != -1:
|
| 664 |
-
print(f" Extracting from layer {extract_layer}")
|
| 665 |
-
if hybrid:
|
| 666 |
-
print(" HYBRID extraction (digit lookup + word learning)")
|
| 667 |
-
elif positional_digit:
|
| 668 |
-
print(" POSITIONAL DIGIT extraction (legacy, 100% on digits only)")
|
| 669 |
-
elif position_extract:
|
| 670 |
-
print(" Position-specific extraction (legacy)")
|
| 671 |
-
elif digit_pred:
|
| 672 |
-
print(" Digit-level prediction (legacy)")
|
| 673 |
-
print("=" * 70)
|
| 674 |
-
|
| 675 |
-
print("\nInitializing model...")
|
| 676 |
-
model = ArithmeticModel(
|
| 677 |
-
device=device,
|
| 678 |
-
unfreeze_layers=unfreeze_layers,
|
| 679 |
-
extract_layer=extract_layer,
|
| 680 |
-
position_extract=position_extract,
|
| 681 |
-
digit_pred=digit_pred,
|
| 682 |
-
positional_digit=positional_digit,
|
| 683 |
-
hybrid=hybrid
|
| 684 |
-
)
|
| 685 |
-
|
| 686 |
-
optimizer = optim.AdamW(model.trainable_parameters(), lr=lr)
|
| 687 |
-
scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=epochs)
|
| 688 |
-
|
| 689 |
-
print(f"\nTraining config:")
|
| 690 |
-
print(f" Epochs: {epochs}")
|
| 691 |
-
print(f" Batch size: {batch_size}")
|
| 692 |
-
print(f" Learning rate: {lr}")
|
| 693 |
-
print(f" Unfreeze layers: {unfreeze_layers}")
|
| 694 |
-
print(f" Samples/epoch: {batch_size * 20}")
|
| 695 |
-
|
| 696 |
-
print(f"\nInitial evaluation (200 samples)...")
|
| 697 |
-
acc, op_acc = evaluate_llm(model, 200)
|
| 698 |
-
print(f" Accuracy: {acc:.4f}, Op accuracy: {op_acc:.4f}")
|
| 699 |
-
|
| 700 |
-
print(f"\nStarting training...")
|
| 701 |
-
print("-" * 70)
|
| 702 |
-
|
| 703 |
-
best_acc = acc
|
| 704 |
-
start_time = time.perf_counter()
|
| 705 |
-
|
| 706 |
-
for epoch in range(epochs):
|
| 707 |
-
model.extractor.train()
|
| 708 |
-
if unfreeze_layers > 0:
|
| 709 |
-
model.llm.train()
|
| 710 |
-
|
| 711 |
-
max_val = get_curriculum_max(epoch, epochs)
|
| 712 |
-
|
| 713 |
-
epoch_loss = 0
|
| 714 |
-
if hybrid:
|
| 715 |
-
epoch_losses = {'result': 0, 'a_digit': 0, 'b_digit': 0, 'op': 0, 'n_words': 0, 'n_lookup': 0}
|
| 716 |
-
elif positional_digit:
|
| 717 |
-
epoch_losses = {'result': 0, 'a_digit': 0, 'b_digit': 0, 'op': 0}
|
| 718 |
-
else:
|
| 719 |
-
epoch_losses = {'result': 0, 'a': 0, 'b': 0, 'op': 0}
|
| 720 |
-
n_batches = 20
|
| 721 |
-
epoch_start = time.perf_counter()
|
| 722 |
-
|
| 723 |
-
for batch_idx in range(n_batches):
|
| 724 |
-
batch_texts = []
|
| 725 |
-
batch_a = []
|
| 726 |
-
batch_b = []
|
| 727 |
-
batch_op = []
|
| 728 |
-
batch_result = []
|
| 729 |
-
batch_a_values = []
|
| 730 |
-
batch_b_values = []
|
| 731 |
-
|
| 732 |
-
for _ in range(batch_size):
|
| 733 |
-
text, a, b, op, result = generate_problem(max_val)
|
| 734 |
-
batch_texts.append(text)
|
| 735 |
-
batch_a.append(int_to_bits(a, device))
|
| 736 |
-
batch_b.append(int_to_bits(b, device))
|
| 737 |
-
batch_op.append(OPERATIONS.index(op))
|
| 738 |
-
batch_result.append(int_to_bits(result, device))
|
| 739 |
-
batch_a_values.append(a)
|
| 740 |
-
batch_b_values.append(b)
|
| 741 |
-
|
| 742 |
-
target_a = torch.stack(batch_a)
|
| 743 |
-
target_b = torch.stack(batch_b)
|
| 744 |
-
target_op = torch.tensor(batch_op, device=device)
|
| 745 |
-
target_result = torch.stack(batch_result)
|
| 746 |
-
target_a_values = torch.tensor(batch_a_values, device=device, dtype=torch.float32)
|
| 747 |
-
target_b_values = torch.tensor(batch_b_values, device=device, dtype=torch.float32)
|
| 748 |
-
|
| 749 |
-
optimizer.zero_grad()
|
| 750 |
-
|
| 751 |
-
outputs = model(batch_texts)
|
| 752 |
-
pred_bits, a_bits, b_bits, op_logits = outputs[0], outputs[1], outputs[2], outputs[3]
|
| 753 |
-
|
| 754 |
-
if hybrid:
|
| 755 |
-
a_values, b_values, used_lookup = outputs[4], outputs[5], outputs[6]
|
| 756 |
-
a_digit_logits, b_digit_logits = outputs[7], outputs[8]
|
| 757 |
-
loss, losses = compute_hybrid_loss(
|
| 758 |
-
pred_bits, op_logits, used_lookup,
|
| 759 |
-
a_digit_logits, b_digit_logits,
|
| 760 |
-
target_result, target_a_values, target_b_values, target_op, device
|
| 761 |
-
)
|
| 762 |
-
elif positional_digit:
|
| 763 |
-
a_digit_logits_list = outputs[7]
|
| 764 |
-
b_digit_logits_list = outputs[8]
|
| 765 |
-
loss, losses = compute_positional_digit_loss(
|
| 766 |
-
pred_bits, op_logits, a_digit_logits_list, b_digit_logits_list,
|
| 767 |
-
target_result, target_op, target_a_values, target_b_values, device
|
| 768 |
-
)
|
| 769 |
-
else:
|
| 770 |
-
loss, losses = compute_llm_loss(
|
| 771 |
-
pred_bits, a_bits, b_bits, op_logits,
|
| 772 |
-
target_result, target_a, target_b, target_op
|
| 773 |
-
)
|
| 774 |
-
|
| 775 |
-
loss.backward()
|
| 776 |
-
torch.nn.utils.clip_grad_norm_(model.trainable_parameters(), 1.0)
|
| 777 |
-
optimizer.step()
|
| 778 |
-
|
| 779 |
-
epoch_loss += loss.item()
|
| 780 |
-
for k in epoch_losses:
|
| 781 |
-
epoch_losses[k] += losses[k]
|
| 782 |
-
|
| 783 |
-
if (batch_idx + 1) % 5 == 0:
|
| 784 |
-
avg_so_far = epoch_loss / (batch_idx + 1)
|
| 785 |
-
print(f" Epoch {epoch+1} batch {batch_idx+1}/{n_batches} | loss: {avg_so_far:.4f}", flush=True)
|
| 786 |
-
|
| 787 |
-
epoch_time = time.perf_counter() - epoch_start
|
| 788 |
-
scheduler.step()
|
| 789 |
-
|
| 790 |
-
avg_loss = epoch_loss / n_batches
|
| 791 |
-
for k in epoch_losses:
|
| 792 |
-
epoch_losses[k] /= n_batches
|
| 793 |
-
|
| 794 |
-
acc, op_acc = evaluate_llm(model, 300)
|
| 795 |
-
elapsed = time.perf_counter() - start_time
|
| 796 |
-
|
| 797 |
-
marker = " *" if acc > best_acc else ""
|
| 798 |
-
if acc > best_acc:
|
| 799 |
-
best_acc = acc
|
| 800 |
-
|
| 801 |
-
mem, _ = get_gpu_memory()
|
| 802 |
-
print(f"Epoch {epoch+1:3d} | Loss: {avg_loss:.4f} | "
|
| 803 |
-
f"Acc: {acc:.4f}{marker} | OpAcc: {op_acc:.4f} | "
|
| 804 |
-
f"Range: 0-{max_val} | VRAM: {mem:.0f}MB | Time: {elapsed:.0f}s")
|
| 805 |
-
if hybrid:
|
| 806 |
-
print(f" Losses - result:{epoch_losses['result']:.4f} "
|
| 807 |
-
f"a_digit:{epoch_losses['a_digit']:.4f} b_digit:{epoch_losses['b_digit']:.4f} "
|
| 808 |
-
f"op:{epoch_losses['op']:.4f} | words:{epoch_losses['n_words']:.0f} lookup:{epoch_losses['n_lookup']:.0f}")
|
| 809 |
-
elif positional_digit:
|
| 810 |
-
print(f" Losses - result:{epoch_losses['result']:.4f} "
|
| 811 |
-
f"a_digit:{epoch_losses['a_digit']:.4f} b_digit:{epoch_losses['b_digit']:.4f} "
|
| 812 |
-
f"op:{epoch_losses['op']:.4f}")
|
| 813 |
-
else:
|
| 814 |
-
print(f" Losses - result:{epoch_losses['result']:.4f} "
|
| 815 |
-
f"a:{epoch_losses['a']:.4f} b:{epoch_losses['b']:.4f} "
|
| 816 |
-
f"op:{epoch_losses['op']:.4f}")
|
| 817 |
-
|
| 818 |
-
if acc >= 0.99:
|
| 819 |
-
print("\n" + "=" * 70)
|
| 820 |
-
print(" TARGET ACHIEVED: 99%+ ACCURACY")
|
| 821 |
-
print("=" * 70)
|
| 822 |
-
break
|
| 823 |
-
|
| 824 |
-
print("\n" + "=" * 70)
|
| 825 |
-
print(" FINAL EVALUATION")
|
| 826 |
-
print("=" * 70)
|
| 827 |
-
|
| 828 |
-
acc, op_acc = evaluate_llm(model, 1000)
|
| 829 |
-
print(f"Final accuracy: {acc:.4f}")
|
| 830 |
-
print(f"Final op accuracy: {op_acc:.4f}")
|
| 831 |
-
print(f"Best accuracy: {best_acc:.4f}")
|
| 832 |
-
|
| 833 |
-
print("\nSample predictions:")
|
| 834 |
-
for _ in range(10):
|
| 835 |
-
text, a, b, op, expected = generate_problem()
|
| 836 |
-
with torch.no_grad():
|
| 837 |
-
outputs = model([text])
|
| 838 |
-
result_bits, a_bits, b_bits, op_logits = outputs[0], outputs[1], outputs[2], outputs[3]
|
| 839 |
-
pred = bits_to_int(result_bits[0])
|
| 840 |
-
pred_a = bits_to_int(a_bits[0])
|
| 841 |
-
pred_b = bits_to_int(b_bits[0])
|
| 842 |
-
pred_op = OPERATIONS[op_logits[0].argmax().item()]
|
| 843 |
-
|
| 844 |
-
status = "OK" if pred == expected else "WRONG"
|
| 845 |
-
print(f" '{text}' = {expected} | pred={pred} (a={pred_a}, b={pred_b}, op={pred_op}) [{status}]")
|
| 846 |
-
|
| 847 |
-
save_path = "D:/8bit-threshold-computer/llm_integration/trained/llm.pt"
|
| 848 |
-
save_dict = {
|
| 849 |
-
'extractor_state_dict': model.extractor.state_dict(),
|
| 850 |
-
'final_accuracy': acc,
|
| 851 |
-
'best_accuracy': best_acc,
|
| 852 |
-
'unfreeze_layers': unfreeze_layers,
|
| 853 |
-
}
|
| 854 |
-
if unfreeze_layers > 0:
|
| 855 |
-
save_dict['llm_state_dict'] = {
|
| 856 |
-
k: v for k, v in model.llm.state_dict().items()
|
| 857 |
-
if any(f'layers.{i}.' in k for i in range(len(model.llm.model.layers) - unfreeze_layers, len(model.llm.model.layers)))
|
| 858 |
-
}
|
| 859 |
-
torch.save(save_dict, save_path)
|
| 860 |
-
print(f"\nSaved to: {save_path}")
|
| 861 |
-
|
| 862 |
-
return model, acc
|
| 863 |
-
|
| 864 |
-
|
| 865 |
-
def main():
|
| 866 |
-
parser = argparse.ArgumentParser(
|
| 867 |
-
description='Unified training for threshold circuit LLM integration',
|
| 868 |
-
formatter_class=argparse.RawDescriptionHelpFormatter,
|
| 869 |
-
epilog="""
|
| 870 |
-
Modes:
|
| 871 |
-
router - Train only OpRouter with ground truth bits (sanity check)
|
| 872 |
-
interface - Train BitEncoder + OpRouter with ground truth bits (sanity check)
|
| 873 |
-
llm - Train extractor with LLM hidden states (the real training)
|
| 874 |
-
|
| 875 |
-
LLM options:
|
| 876 |
-
--unfreeze_layers N Fine-tune top N transformer layers
|
| 877 |
-
--extract_layer N Extract from layer N (-1 = last)
|
| 878 |
-
--position_extract Use position-specific extraction
|
| 879 |
-
--digit_pred Predict digits instead of bits
|
| 880 |
-
|
| 881 |
-
Baked-in: curriculum learning (0-9 -> 0-99 -> 0-255), 2x loss weight for a/b
|
| 882 |
-
|
| 883 |
-
Examples:
|
| 884 |
-
python train.py --mode llm --epochs 100
|
| 885 |
-
python train.py --mode llm --position_extract
|
| 886 |
-
python train.py --mode llm --digit_pred --extract_layer 12
|
| 887 |
-
python train.py --mode llm --unfreeze_layers 4 --batch_size 4096
|
| 888 |
-
"""
|
| 889 |
-
)
|
| 890 |
-
parser.add_argument('--mode', type=str, required=True,
|
| 891 |
-
choices=['router', 'interface', 'llm'],
|
| 892 |
-
help='Training mode')
|
| 893 |
-
parser.add_argument('--epochs', type=int, default=100, help='Number of epochs')
|
| 894 |
-
parser.add_argument('--batch_size', type=int, default=512, help='Batch size (default: 512)')
|
| 895 |
-
parser.add_argument('--lr', type=float, default=None,
|
| 896 |
-
help='Learning rate (default: mode-specific)')
|
| 897 |
-
parser.add_argument('--unfreeze_layers', type=int, default=0,
|
| 898 |
-
help='Unfreeze top N transformer layers (default 0 = frozen)')
|
| 899 |
-
parser.add_argument('--extract_layer', type=int, default=0,
|
| 900 |
-
help='Which layer to extract from (default: 0 = embeddings, best for digits)')
|
| 901 |
-
parser.add_argument('--position_extract', action='store_true',
|
| 902 |
-
help='Use position-specific extraction (legacy)')
|
| 903 |
-
parser.add_argument('--digit_pred', action='store_true',
|
| 904 |
-
help='Predict digits instead of bits (legacy)')
|
| 905 |
-
parser.add_argument('--positional_digit', action='store_true', default=False,
|
| 906 |
-
help='Use positional digit extraction (legacy, 100%% on digits only)')
|
| 907 |
-
parser.add_argument('--device', type=str, default='cuda', help='Device')
|
| 908 |
-
args = parser.parse_args()
|
| 909 |
-
|
| 910 |
-
torch.manual_seed(42)
|
| 911 |
-
random.seed(42)
|
| 912 |
-
|
| 913 |
-
if args.mode == 'router':
|
| 914 |
-
lr = args.lr if args.lr is not None else 1e-2
|
| 915 |
-
train_router(epochs=args.epochs, batch_size=args.batch_size, lr=lr, device=args.device)
|
| 916 |
-
|
| 917 |
-
elif args.mode == 'interface':
|
| 918 |
-
lr = args.lr if args.lr is not None else 1e-3
|
| 919 |
-
model, fitness = train_interface(
|
| 920 |
-
epochs=args.epochs, batch_size=args.batch_size, lr=lr, device=args.device
|
| 921 |
-
)
|
| 922 |
-
|
| 923 |
-
print("\n" + "=" * 70)
|
| 924 |
-
print(" EXPERIMENT SUMMARY")
|
| 925 |
-
print("=" * 70)
|
| 926 |
-
print(f"\n Control (Vanilla SmolLM2-360M): 11.90%")
|
| 927 |
-
print(f" Experimental (Trained Interface): {100*fitness:.2f}%")
|
| 928 |
-
if fitness > 0:
|
| 929 |
-
print(f"\n Improvement: {100*(fitness - 0.119)/0.119:.1f}%")
|
| 930 |
-
|
| 931 |
-
if fitness >= 0.99:
|
| 932 |
-
print("\n CONCLUSION: Frozen threshold circuits + trained interface")
|
| 933 |
-
print(" achieves near-perfect arithmetic accuracy.")
|
| 934 |
-
print(" Core thesis VALIDATED.")
|
| 935 |
-
else:
|
| 936 |
-
print(f"\n CONCLUSION: Further training or architecture changes needed.")
|
| 937 |
-
print(f" Current gap: {100*(1.0 - fitness):.2f}%")
|
| 938 |
-
|
| 939 |
-
elif args.mode == 'llm':
|
| 940 |
-
lr = args.lr if args.lr is not None else 3e-4
|
| 941 |
-
train_llm(
|
| 942 |
-
epochs=args.epochs,
|
| 943 |
-
batch_size=args.batch_size,
|
| 944 |
-
lr=lr,
|
| 945 |
-
unfreeze_layers=args.unfreeze_layers,
|
| 946 |
-
extract_layer=args.extract_layer,
|
| 947 |
-
position_extract=args.position_extract,
|
| 948 |
-
digit_pred=args.digit_pred,
|
| 949 |
-
positional_digit=args.positional_digit,
|
| 950 |
-
device=args.device
|
| 951 |
-
)
|
| 952 |
-
|
| 953 |
-
|
| 954 |
-
if __name__ == "__main__":
|
| 955 |
-
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
paper/README.md
CHANGED
|
@@ -6,51 +6,45 @@ Networks.* The manuscript and the records of the runs it reports.
|
|
| 6 |
```
|
| 7 |
universal_construction_ternary.tex source
|
| 8 |
universal_construction_ternary.pdf typeset
|
| 9 |
-
runs/ the record written by each
|
| 10 |
```
|
| 11 |
|
| 12 |
-
## What each
|
| 13 |
|
| 14 |
-
Each
|
| 15 |
-
needs Rocq 9.1 and ocamlfind on the PATH.
|
|
|
|
| 16 |
|
| 17 |
-
|
| 18 |
-
| Script | Record | Establishes |
|
| 19 |
|---|---|---|
|
| 20 |
-
| `
|
| 21 |
-
| `
|
| 22 |
-
| `
|
| 23 |
-
| `
|
| 24 |
-
| `
|
| 25 |
-
| `
|
| 26 |
-
| `
|
| 27 |
-
| `
|
| 28 |
-
| `
|
| 29 |
-
| `
|
| 30 |
-
| `
|
| 31 |
-
| `
|
| 32 |
-
| `
|
| 33 |
-
| `
|
| 34 |
-
| `
|
| 35 |
-
| `
|
| 36 |
-
| `
|
| 37 |
-
| `
|
| 38 |
-
| `
|
| 39 |
-
| `
|
| 40 |
-
| `
|
| 41 |
-
| `
|
| 42 |
-
| `
|
| 43 |
-
| `
|
| 44 |
-
| `
|
| 45 |
-
| `
|
| 46 |
-
| `
|
| 47 |
-
| `
|
| 48 |
-
| `src/redundant.py` | `paper_redundant_*.json` | the amplified host N_3: one step against SUBLEQ on replicated states, the margin 3/2, exact self-reproduction in C, the run under noise, the deviation curve and its bound by layer, the failure rate of the state read by majority and the restoration bound, computed by `src/restore_bound.c` |
|
| 49 |
-
| `src/neuromorphic.py` | `paper_neuromorphic.json` | the datapath on the Loihi and Akida neuron models |
|
| 50 |
-
| `src/independent/*.py`, `nsim.c` | `independent_*.json` | separate code written from the paper's definitions: H_A rebuilt from Construction 3.8 for A = 3 to 12, Lev(N_host) from sigma, Tables 3 to 6 with a separate noise simulator, and the datapath on Loihi and Akida |
|
| 51 |
-
|
| 52 |
-
`src/neuromorphic.py run` needs Lava, which requires Python 3.10; everything
|
| 53 |
-
else runs in the environment of the repository.
|
| 54 |
|
| 55 |
Rebuild the typeset paper with any TeX installation:
|
| 56 |
|
|
|
|
| 6 |
```
|
| 7 |
universal_construction_ternary.tex source
|
| 8 |
universal_construction_ternary.pdf typeset
|
| 9 |
+
runs/ the record written by each command
|
| 10 |
```
|
| 11 |
|
| 12 |
+
## What each command establishes
|
| 13 |
|
| 14 |
+
Each command is `python src/<file>.py <command>` and writes its record to `runs/`;
|
| 15 |
+
`paper.py rocq` needs Rocq 9.1 and ocamlfind on the PATH, and `noise.py neuromorphic run`
|
| 16 |
+
needs Lava, which requires Python 3.10.
|
| 17 |
|
| 18 |
+
| Command | Record | Establishes |
|
|
|
|
| 19 |
|---|---|---|
|
| 20 |
+
| `host.py` | — | the host family H_A, the serialization sigma and its inverse, the device, the constructor P and the program P*, and the evaluators |
|
| 21 |
+
| `paper.py host-family` | `paper_host_family.json` | one step of H_A against SUBLEQ_A for A = 3 to 12, on the evaluator in C |
|
| 22 |
+
| `paper.py host-fold` | `paper_host_fold.json` | the folded host family, depth 11, against SUBLEQ_A for A = 3 to 12 |
|
| 23 |
+
| `paper.py stats` | `paper_stats.json` | unit counts, levelization sizes, fan-in |
|
| 24 |
+
| `paper.py verify` | `paper_verify.json` | codec, framing, serialization, datapath, levelization |
|
| 25 |
+
| `paper.py runs` | `paper_runs_*.json` | self-description, three generations, lockstep |
|
| 26 |
+
| `paper.py codec` | `paper_codec.json` | the recipe codec at every boundary of the token format |
|
| 27 |
+
| `paper.py optimal` | `paper_optimal.json` | the length bound is optimal on run-free strings |
|
| 28 |
+
| `paper.py steps` | `paper_steps.json` | the closed forms for the step counts |
|
| 29 |
+
| `paper.py device` | `paper_device.json` | the device against its definition |
|
| 30 |
+
| `paper.py host` | `paper_host.json` | the reference machine against its definition |
|
| 31 |
+
| `paper.py lev` | `paper_lev.json` | the levelization against gate-by-gate evaluation |
|
| 32 |
+
| `paper.py framing` | `paper_framing.json` | `ser` injective, `inst` partial and inverse |
|
| 33 |
+
| `paper.py ternarity` | `paper_ternarity.json` | no ternary unit computes the branch predicate |
|
| 34 |
+
| `paper.py environment` | `paper_environment.json` | the cost of the environment against the cost of the network |
|
| 35 |
+
| `paper.py sigma` | — | sigma is a function, injective, and inverted by the parser |
|
| 36 |
+
| `paper.py family` | `paper_runs_family.json` | the constructor emits all 29 machines |
|
| 37 |
+
| `paper.py family-gate` | `paper_runs_family_gate.json` | four of them on the netlist of sigma |
|
| 38 |
+
| `paper.py c` | `paper_c_evaluator.json` | the host on the evaluator in C, `src/netlist_eval.c` |
|
| 39 |
+
| `paper.py throughput` | `paper_throughput.json` | the rate of each evaluator |
|
| 40 |
+
| `paper.py rocq` | `paper_rocq.json` | the Rocq development of `rocq/`, its assumptions and the netlist extracted from it |
|
| 41 |
+
| `twohead.py organism` | `paper_twohead.json` | the translating organism on the two-head machine, three generations |
|
| 42 |
+
| `twohead.py net` | `paper_twohead_net.json` | the netlist N_{G,L} against the machine for windows of 64 and 4096 cells, its file, the organism on it in C, and the netlist extracted from Rocq |
|
| 43 |
+
| `twohead.py stored` | `paper_stored_host.json` | the host compiled into records of the two-head machine: one pass against the host's step and its device, and three generations in C |
|
| 44 |
+
| `noise.py host` | `paper_noise_*.json` | the margin, the profile bound, read noise, the deviation curve, weight error with leakage, the active-input counts of the orbit certificate |
|
| 45 |
+
| `noise.py amplified` | `paper_redundant_*.json` | the amplified host N_3: one step on replicated states, the margin 3/2, exact self-reproduction in C, the run under noise, the deviation curve and its bound by layer, the failure rate of the state read by majority and the restoration bound, computed by `src/restore_bound.c` |
|
| 46 |
+
| `noise.py neuromorphic` | `paper_neuromorphic.json` | the datapath on the Loihi and Akida neuron models |
|
| 47 |
+
| `independent.py` | `independent_*.json` | code written from the paper's definitions alone: H_A rebuilt from Construction 3.8 for A = 3 to 12, Lev(N_host) from sigma, Tables 3 to 6 with the noise simulator `src/nsim.c`, and the datapath on Loihi and Akida |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
|
| 49 |
Rebuild the typeset paper with any TeX installation:
|
| 50 |
|
paper/universal_construction_ternary.pdf
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:cf817d63746a8601c860f094ef25e7a746218a355e721248f44559caf8f3f441
|
| 3 |
+
size 364795
|
paper/universal_construction_ternary.tex
CHANGED
|
@@ -2514,12 +2514,12 @@ $\sigma(\Nhost)$, and every generation of every run instantiated its evaluator
|
|
| 2514 |
afresh from the bytes the previous generation emitted. The two-head machine was
|
| 2515 |
evaluated by transcriptions of Definition~\ref{def:twohead} in Python and in C, by
|
| 2516 |
the netlist $N^\alpha_{G,L}$ in Python and in C, and in the Rocq kernel. The
|
| 2517 |
-
stored host is built from $\sigma(\Nhost)$ by \texttt{src/
|
| 2518 |
also compares one pass with the host's step and the device, and its generations
|
| 2519 |
are run by \texttt{src/stored\_host\_run.c}, a transcription of
|
| 2520 |
Definition~\ref{def:twohead} that decodes the active bank once per change of
|
| 2521 |
$\kappa$; the three generations of Remark~\ref{rem:storedcheck} take $9$ minutes.
|
| 2522 |
-
Separate code in \texttt{src/independent
|
| 2523 |
paper, rebuilds $H_A$ from Construction~\ref{con:host} for $A=3$ to $12$,
|
| 2524 |
levelizes $\sigma(\Nhost)$, repeats Tables~\ref{tab:curve} to~\ref{tab:bound3}
|
| 2525 |
with a simulator in C and on the accelerator, and maps the datapath onto Loihi
|
|
@@ -2540,7 +2540,7 @@ of their length, and Theorem~\ref{thm:organism}. The file that defines the
|
|
| 2540 |
organism and its description is written by \texttt{src/twohead.py}, and the
|
| 2541 |
netlist definition, extracted to OCaml, writes for $N^6_{133,2930}$ and
|
| 2542 |
$N^{12}_{5,40}$ files byte for byte equal to those that
|
| 2543 |
-
\texttt{src/twohead
|
| 2544 |
|
| 2545 |
Besides the runs recorded in the remarks of Sections~\ref{sec:host}
|
| 2546 |
to~\ref{sec:organism} and in Appendix~\ref{app:measure}, the following were
|
|
@@ -2637,7 +2637,7 @@ the Rocq definition. The records of the organism and of $S$ are laid out as in
|
|
| 2637 |
Definition~\ref{def:twohead}, with the window cells of
|
| 2638 |
Constructions~\ref{con:organism} and~\ref{con:stored}; within each block of
|
| 2639 |
Construction~\ref{con:stored} the order of the records is that of
|
| 2640 |
-
\texttt{src/
|
| 2641 |
its live interval.
|
| 2642 |
One generation of $\Omega^\ast$ takes $0.3$ seconds on~(a), $45$ seconds on~(d)
|
| 2643 |
and $9$ minutes on~(b). All figures were
|
|
|
|
| 2514 |
afresh from the bytes the previous generation emitted. The two-head machine was
|
| 2515 |
evaluated by transcriptions of Definition~\ref{def:twohead} in Python and in C, by
|
| 2516 |
the netlist $N^\alpha_{G,L}$ in Python and in C, and in the Rocq kernel. The
|
| 2517 |
+
stored host is built from $\sigma(\Nhost)$ by \texttt{src/twohead.py}, which
|
| 2518 |
also compares one pass with the host's step and the device, and its generations
|
| 2519 |
are run by \texttt{src/stored\_host\_run.c}, a transcription of
|
| 2520 |
Definition~\ref{def:twohead} that decodes the active bank once per change of
|
| 2521 |
$\kappa$; the three generations of Remark~\ref{rem:storedcheck} take $9$ minutes.
|
| 2522 |
+
Separate code in \texttt{src/independent.py}, written from the definitions of this
|
| 2523 |
paper, rebuilds $H_A$ from Construction~\ref{con:host} for $A=3$ to $12$,
|
| 2524 |
levelizes $\sigma(\Nhost)$, repeats Tables~\ref{tab:curve} to~\ref{tab:bound3}
|
| 2525 |
with a simulator in C and on the accelerator, and maps the datapath onto Loihi
|
|
|
|
| 2540 |
organism and its description is written by \texttt{src/twohead.py}, and the
|
| 2541 |
netlist definition, extracted to OCaml, writes for $N^6_{133,2930}$ and
|
| 2542 |
$N^{12}_{5,40}$ files byte for byte equal to those that
|
| 2543 |
+
\texttt{src/twohead.py} writes.
|
| 2544 |
|
| 2545 |
Besides the runs recorded in the remarks of Sections~\ref{sec:host}
|
| 2546 |
to~\ref{sec:organism} and in Appendix~\ref{app:measure}, the following were
|
|
|
|
| 2637 |
Definition~\ref{def:twohead}, with the window cells of
|
| 2638 |
Constructions~\ref{con:organism} and~\ref{con:stored}; within each block of
|
| 2639 |
Construction~\ref{con:stored} the order of the records is that of
|
| 2640 |
+
\texttt{src/twohead.py}, which gives each temporary the least cell free over
|
| 2641 |
its live interval.
|
| 2642 |
One generation of $\Omega^\ast$ takes $0.3$ seconds on~(a), $45$ seconds on~(d)
|
| 2643 |
and $9$ minutes on~(b). All figures were
|
src/attractor.py
DELETED
|
@@ -1,261 +0,0 @@
|
|
| 1 |
-
"""Attractor computer: an energy-based threshold network.
|
| 2 |
-
|
| 3 |
-
A Boolean circuit is compiled to a quadratic pseudo-Boolean energy
|
| 4 |
-
E(s) = sum_i L[i] s_i + sum_{i<j} Q[i,j] s_i s_j over binary wire variables, with
|
| 5 |
-
per-gate gadgets that are non-negative and zero exactly on the gate truth table:
|
| 6 |
-
|
| 7 |
-
AND z=x&y : 3z + xy - 2xz - 2yz
|
| 8 |
-
OR z=x|y : x + y + z + xy - 2xz - 2yz
|
| 9 |
-
NOT z=~x : 1 - x - z + 2xz
|
| 10 |
-
|
| 11 |
-
AND/OR/NOT are functionally complete, so any circuit compiles and its consistent
|
| 12 |
-
assignment is the global minimum. There is no program counter or clock: the
|
| 13 |
-
coupling matrix Q (with linear terms L) is the program, and execution is
|
| 14 |
-
relaxation toward the minimum. The relaxation step is a threshold neuron over the
|
| 15 |
-
same integer weights, s_i <- H(-(L[i] + sum_j Q[i,j] s_j)).
|
| 16 |
-
|
| 17 |
-
The clamped wire subset selects the mode. Clamp inputs to evaluate forward (exact
|
| 18 |
-
via topological propagation to the energy-0 fixed point); clamp outputs to invert
|
| 19 |
-
(a multiplier run backward returns factors); clamp a CNF output to 1 to solve
|
| 20 |
-
SAT. Forward evaluation is exact and linear in gate count; inversion and
|
| 21 |
-
open-constraint solving are annealed ground-state search, NP-hard in general,
|
| 22 |
-
with a zero-energy state certifying a correct assignment.
|
| 23 |
-
"""
|
| 24 |
-
from __future__ import annotations
|
| 25 |
-
import math
|
| 26 |
-
import random
|
| 27 |
-
from collections import defaultdict
|
| 28 |
-
from typing import Dict, List, Optional, Tuple
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
class Circuit:
|
| 32 |
-
"""Wire allocator and energy accumulator. Gates append exact QUBO gadgets
|
| 33 |
-
and record the relation for topological forward evaluation."""
|
| 34 |
-
|
| 35 |
-
def __init__(self) -> None:
|
| 36 |
-
self.n = 0
|
| 37 |
-
self.L: Dict[int, int] = defaultdict(int)
|
| 38 |
-
self.Q: Dict[Tuple[int, int], int] = defaultdict(int)
|
| 39 |
-
self.const = 0
|
| 40 |
-
self.gates: List[Tuple[str, int, Tuple[int, ...]]] = []
|
| 41 |
-
|
| 42 |
-
def wire(self) -> int:
|
| 43 |
-
i = self.n
|
| 44 |
-
self.n += 1
|
| 45 |
-
return i
|
| 46 |
-
|
| 47 |
-
def wires(self, k: int) -> List[int]:
|
| 48 |
-
return [self.wire() for _ in range(k)]
|
| 49 |
-
|
| 50 |
-
def _q(self, i: int, j: int, c: int) -> None:
|
| 51 |
-
if i == j:
|
| 52 |
-
self.L[i] += c
|
| 53 |
-
else:
|
| 54 |
-
self.Q[(min(i, j), max(i, j))] += c
|
| 55 |
-
|
| 56 |
-
def AND(self, x: int, y: int) -> int:
|
| 57 |
-
z = self.wire()
|
| 58 |
-
self.L[z] += 3
|
| 59 |
-
self._q(x, y, 1); self._q(x, z, -2); self._q(y, z, -2)
|
| 60 |
-
self.gates.append(("AND", z, (x, y)))
|
| 61 |
-
return z
|
| 62 |
-
|
| 63 |
-
def OR(self, x: int, y: int) -> int:
|
| 64 |
-
z = self.wire()
|
| 65 |
-
self.L[x] += 1; self.L[y] += 1; self.L[z] += 1
|
| 66 |
-
self._q(x, y, 1); self._q(x, z, -2); self._q(y, z, -2)
|
| 67 |
-
self.gates.append(("OR", z, (x, y)))
|
| 68 |
-
return z
|
| 69 |
-
|
| 70 |
-
def NOT(self, x: int) -> int:
|
| 71 |
-
z = self.wire()
|
| 72 |
-
self.const += 1
|
| 73 |
-
self.L[x] += -1; self.L[z] += -1
|
| 74 |
-
self._q(x, z, 2)
|
| 75 |
-
self.gates.append(("NOT", z, (x,)))
|
| 76 |
-
return z
|
| 77 |
-
|
| 78 |
-
def XOR(self, x: int, y: int) -> int:
|
| 79 |
-
return self.OR(self.AND(x, self.NOT(y)), self.AND(self.NOT(x), y))
|
| 80 |
-
|
| 81 |
-
def full_adder(self, x: int, y: int, cin: int) -> Tuple[int, int]:
|
| 82 |
-
axy = self.XOR(x, y)
|
| 83 |
-
s = self.XOR(axy, cin)
|
| 84 |
-
cout = self.OR(self.AND(x, y), self.AND(cin, axy))
|
| 85 |
-
return s, cout
|
| 86 |
-
|
| 87 |
-
# ---- energy + couplings ------------------------------------------------
|
| 88 |
-
def energy(self, s: List[int]) -> int:
|
| 89 |
-
e = self.const
|
| 90 |
-
for i, c in self.L.items():
|
| 91 |
-
e += c * s[i]
|
| 92 |
-
for (i, j), c in self.Q.items():
|
| 93 |
-
e += c * s[i] * s[j]
|
| 94 |
-
return e
|
| 95 |
-
|
| 96 |
-
def neighbors(self) -> Dict[int, List[Tuple[int, int]]]:
|
| 97 |
-
nbr: Dict[int, List[Tuple[int, int]]] = defaultdict(list)
|
| 98 |
-
for (i, j), c in self.Q.items():
|
| 99 |
-
nbr[i].append((j, c))
|
| 100 |
-
nbr[j].append((i, c))
|
| 101 |
-
return nbr
|
| 102 |
-
|
| 103 |
-
# ---- relaxation modes --------------------------------------------------
|
| 104 |
-
def forward_eval(self, clamp: Dict[int, int]) -> List[int]:
|
| 105 |
-
"""Exact forward relaxation: propagate clamped inputs through the gate
|
| 106 |
-
relations in topological order onto the energy-0 fixed point."""
|
| 107 |
-
s = [0] * self.n
|
| 108 |
-
for w, v in clamp.items():
|
| 109 |
-
s[w] = v
|
| 110 |
-
for op, z, ins in self.gates:
|
| 111 |
-
if op == "AND":
|
| 112 |
-
s[z] = s[ins[0]] & s[ins[1]]
|
| 113 |
-
elif op == "OR":
|
| 114 |
-
s[z] = s[ins[0]] | s[ins[1]]
|
| 115 |
-
else:
|
| 116 |
-
s[z] = 1 - s[ins[0]]
|
| 117 |
-
return s
|
| 118 |
-
|
| 119 |
-
def relax_energy(self, clamp: Dict[int, int], sweeps: int = 4000,
|
| 120 |
-
t0: float = 4.0, t1: float = 0.02, seed: int = 0
|
| 121 |
-
) -> Tuple[List[int], bool]:
|
| 122 |
-
"""Canonical relaxation: anneal the full threshold network (every free
|
| 123 |
-
wire), tracking the lowest-energy state. Universal but hard; the exact
|
| 124 |
-
gadgets keep the target at energy 0."""
|
| 125 |
-
nbr = self.neighbors()
|
| 126 |
-
rng = random.Random(seed)
|
| 127 |
-
s = [rng.randint(0, 1) for _ in range(self.n)]
|
| 128 |
-
for w, v in clamp.items():
|
| 129 |
-
s[w] = v
|
| 130 |
-
free = [i for i in range(self.n) if i not in clamp]
|
| 131 |
-
best, best_e = list(s), self.energy(s)
|
| 132 |
-
for step in range(sweeps):
|
| 133 |
-
T = t0 * (t1 / t0) ** (step / max(1, sweeps - 1))
|
| 134 |
-
for _ in range(len(free)):
|
| 135 |
-
i = free[rng.randrange(len(free))]
|
| 136 |
-
field = self.L[i] + sum(c * s[j] for j, c in nbr[i])
|
| 137 |
-
dE = (1 - 2 * s[i]) * field
|
| 138 |
-
if dE <= 0 or rng.random() < math.exp(-dE / T):
|
| 139 |
-
s[i] ^= 1
|
| 140 |
-
e = self.energy(s)
|
| 141 |
-
if e < best_e:
|
| 142 |
-
best, best_e = list(s), e
|
| 143 |
-
if best_e == 0:
|
| 144 |
-
return best, True
|
| 145 |
-
return best, best_e == 0
|
| 146 |
-
|
| 147 |
-
def solve(self, free_inputs: List[int], fixed: Dict[int, int],
|
| 148 |
-
target: Dict[int, int], sweeps: int = 3000, restarts: int = 80,
|
| 149 |
-
seed: int = 0) -> Optional[List[int]]:
|
| 150 |
-
"""Open-constraint relaxation over a chosen set of driver wires, with
|
| 151 |
-
the rest slaved through the circuit; anneal the output Hamming mismatch
|
| 152 |
-
to zero. Clamp outputs and pass the inputs here to run backward."""
|
| 153 |
-
rng = random.Random(seed)
|
| 154 |
-
|
| 155 |
-
def mism(vals: Dict[int, int]) -> int:
|
| 156 |
-
s = self.forward_eval({**fixed, **vals})
|
| 157 |
-
return sum(1 for w, v in target.items() if s[w] != v)
|
| 158 |
-
|
| 159 |
-
for _ in range(restarts):
|
| 160 |
-
vals = {w: rng.randint(0, 1) for w in free_inputs}
|
| 161 |
-
m = mism(vals)
|
| 162 |
-
if m == 0:
|
| 163 |
-
return self.forward_eval({**fixed, **vals})
|
| 164 |
-
for step in range(sweeps):
|
| 165 |
-
T = 2.0 * (0.02 / 2.0) ** (step / sweeps)
|
| 166 |
-
w = free_inputs[rng.randrange(len(free_inputs))]
|
| 167 |
-
vals[w] ^= 1
|
| 168 |
-
m2 = mism(vals)
|
| 169 |
-
if m2 <= m or rng.random() < math.exp(-(m2 - m) / T):
|
| 170 |
-
m = m2
|
| 171 |
-
if m == 0:
|
| 172 |
-
return self.forward_eval({**fixed, **vals})
|
| 173 |
-
else:
|
| 174 |
-
vals[w] ^= 1
|
| 175 |
-
return None
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
# ---------------------------------------------------------------------------
|
| 179 |
-
# Circuit builders
|
| 180 |
-
# ---------------------------------------------------------------------------
|
| 181 |
-
def adder(bits: int) -> Tuple[Circuit, dict]:
|
| 182 |
-
c = Circuit()
|
| 183 |
-
xs, ys = c.wires(bits), c.wires(bits)
|
| 184 |
-
cin = c.wire()
|
| 185 |
-
outs, carry = [], cin
|
| 186 |
-
for k in range(bits):
|
| 187 |
-
s, carry = c.full_adder(xs[k], ys[k], carry)
|
| 188 |
-
outs.append(s)
|
| 189 |
-
return c, {"xs": xs, "ys": ys, "cin": cin, "sum": outs + [carry]}
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
def multiplier(bits: int) -> Tuple[Circuit, dict]:
|
| 193 |
-
c = Circuit()
|
| 194 |
-
xs, ys = c.wires(bits), c.wires(bits)
|
| 195 |
-
zero = c.wire()
|
| 196 |
-
acc = [zero] * (2 * bits)
|
| 197 |
-
for i in range(bits):
|
| 198 |
-
carry = zero
|
| 199 |
-
for j in range(bits):
|
| 200 |
-
acc[i + j], carry = c.full_adder(acc[i + j], c.AND(xs[i], ys[j]), carry)
|
| 201 |
-
acc[i + bits] = carry
|
| 202 |
-
return c, {"xs": xs, "ys": ys, "zero": zero, "prod": acc}
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
_OPCODE = {"AND": 0, "OR": 1, "NOT": 2}
|
| 206 |
-
_OPNAME = {v: k for k, v in _OPCODE.items()}
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
def to_tensors(circ: Circuit, io: dict):
|
| 210 |
-
"""Serialize the coupling matrix (the program) and the gate list to tensors.
|
| 211 |
-
Q is stored sparsely as index pairs and integer values."""
|
| 212 |
-
import torch
|
| 213 |
-
qi = sorted(circ.Q)
|
| 214 |
-
q_idx = torch.tensor(qi if qi else [], dtype=torch.long).reshape(-1, 2)
|
| 215 |
-
q_val = torch.tensor([circ.Q[k] for k in qi], dtype=torch.long)
|
| 216 |
-
li = sorted(circ.L)
|
| 217 |
-
l_idx = torch.tensor(li, dtype=torch.long)
|
| 218 |
-
l_val = torch.tensor([circ.L[i] for i in li], dtype=torch.long)
|
| 219 |
-
g_op = torch.tensor([_OPCODE[op] for op, _, _ in circ.gates], dtype=torch.long)
|
| 220 |
-
g_out = torch.tensor([o for _, o, _ in circ.gates], dtype=torch.long)
|
| 221 |
-
g_in = torch.tensor([[ins[0], ins[1] if len(ins) > 1 else -1]
|
| 222 |
-
for _, _, ins in circ.gates], dtype=torch.long).reshape(-1, 2)
|
| 223 |
-
t = {"Q_idx": q_idx, "Q_val": q_val, "L_idx": l_idx, "L_val": l_val,
|
| 224 |
-
"gate_op": g_op, "gate_out": g_out, "gate_in": g_in}
|
| 225 |
-
import json
|
| 226 |
-
meta = {"machine": "attractor", "n": str(circ.n), "const": str(circ.const),
|
| 227 |
-
"io": json.dumps({k: v for k, v in io.items()})}
|
| 228 |
-
return t, meta
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
def from_tensors(t: dict, meta: dict) -> Tuple[Circuit, dict]:
|
| 232 |
-
import json
|
| 233 |
-
c = Circuit()
|
| 234 |
-
c.n = int(meta["n"])
|
| 235 |
-
c.const = int(meta["const"])
|
| 236 |
-
for (i, j), v in zip(t["Q_idx"].tolist(), t["Q_val"].tolist()):
|
| 237 |
-
c.Q[(i, j)] = v
|
| 238 |
-
for i, v in zip(t["L_idx"].tolist(), t["L_val"].tolist()):
|
| 239 |
-
c.L[i] = v
|
| 240 |
-
for op, out, ins in zip(t["gate_op"].tolist(), t["gate_out"].tolist(), t["gate_in"].tolist()):
|
| 241 |
-
c.gates.append((_OPNAME[op], out, tuple(x for x in ins if x >= 0)))
|
| 242 |
-
return c, json.loads(meta["io"])
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
def cnf(clauses: List[List[int]], n_vars: int) -> Tuple[Circuit, dict]:
|
| 246 |
-
"""Compile a CNF formula. Literals are +v (var v) or -v (negation), v>=1.
|
| 247 |
-
Returns the circuit, the variable wires, and the wire that is 1 iff the
|
| 248 |
-
formula is satisfied. Clamp that wire to 1 and relax to find a model."""
|
| 249 |
-
c = Circuit()
|
| 250 |
-
var = {v: c.wire() for v in range(1, n_vars + 1)}
|
| 251 |
-
clause_ws = []
|
| 252 |
-
for cl in clauses:
|
| 253 |
-
lits = [var[abs(l)] if l > 0 else c.NOT(var[abs(l)]) for l in cl]
|
| 254 |
-
acc = lits[0]
|
| 255 |
-
for w in lits[1:]:
|
| 256 |
-
acc = c.OR(acc, w)
|
| 257 |
-
clause_ws.append(acc)
|
| 258 |
-
sat = clause_ws[0]
|
| 259 |
-
for w in clause_ws[1:]:
|
| 260 |
-
sat = c.AND(sat, w)
|
| 261 |
-
return c, {"vars": var, "sat": sat}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/automata.py
ADDED
|
@@ -0,0 +1,1759 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""The attractor, reversible, cellular-automaton and tile machines, with their builders."""
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
|
| 4 |
+
import json
|
| 5 |
+
import math
|
| 6 |
+
import os
|
| 7 |
+
import random
|
| 8 |
+
import sys
|
| 9 |
+
from collections import defaultdict
|
| 10 |
+
from typing import Dict, List, Optional, Tuple
|
| 11 |
+
|
| 12 |
+
import torch
|
| 13 |
+
from safetensors import safe_open
|
| 14 |
+
from safetensors.torch import save_file, load_file
|
| 15 |
+
|
| 16 |
+
from machines import Net, compile_net, MatrixMachine
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class Circuit:
|
| 20 |
+
"""Wire allocator and energy accumulator."""
|
| 21 |
+
|
| 22 |
+
def __init__(self) -> None:
|
| 23 |
+
self.n = 0
|
| 24 |
+
self.L: Dict[int, int] = defaultdict(int)
|
| 25 |
+
self.Q: Dict[Tuple[int, int], int] = defaultdict(int)
|
| 26 |
+
self.const = 0
|
| 27 |
+
self.gates: List[Tuple[str, int, Tuple[int, ...]]] = []
|
| 28 |
+
|
| 29 |
+
def wire(self) -> int:
|
| 30 |
+
i = self.n
|
| 31 |
+
self.n += 1
|
| 32 |
+
return i
|
| 33 |
+
|
| 34 |
+
def wires(self, k: int) -> List[int]:
|
| 35 |
+
return [self.wire() for _ in range(k)]
|
| 36 |
+
|
| 37 |
+
def _q(self, i: int, j: int, c: int) -> None:
|
| 38 |
+
if i == j:
|
| 39 |
+
self.L[i] += c
|
| 40 |
+
else:
|
| 41 |
+
self.Q[(min(i, j), max(i, j))] += c
|
| 42 |
+
|
| 43 |
+
def AND(self, x: int, y: int) -> int:
|
| 44 |
+
z = self.wire()
|
| 45 |
+
self.L[z] += 3
|
| 46 |
+
self._q(x, y, 1); self._q(x, z, -2); self._q(y, z, -2)
|
| 47 |
+
self.gates.append(("AND", z, (x, y)))
|
| 48 |
+
return z
|
| 49 |
+
|
| 50 |
+
def OR(self, x: int, y: int) -> int:
|
| 51 |
+
z = self.wire()
|
| 52 |
+
self.L[x] += 1; self.L[y] += 1; self.L[z] += 1
|
| 53 |
+
self._q(x, y, 1); self._q(x, z, -2); self._q(y, z, -2)
|
| 54 |
+
self.gates.append(("OR", z, (x, y)))
|
| 55 |
+
return z
|
| 56 |
+
|
| 57 |
+
def NOT(self, x: int) -> int:
|
| 58 |
+
z = self.wire()
|
| 59 |
+
self.const += 1
|
| 60 |
+
self.L[x] += -1; self.L[z] += -1
|
| 61 |
+
self._q(x, z, 2)
|
| 62 |
+
self.gates.append(("NOT", z, (x,)))
|
| 63 |
+
return z
|
| 64 |
+
|
| 65 |
+
def XOR(self, x: int, y: int) -> int:
|
| 66 |
+
return self.OR(self.AND(x, self.NOT(y)), self.AND(self.NOT(x), y))
|
| 67 |
+
|
| 68 |
+
def full_adder(self, x: int, y: int, cin: int) -> Tuple[int, int]:
|
| 69 |
+
axy = self.XOR(x, y)
|
| 70 |
+
s = self.XOR(axy, cin)
|
| 71 |
+
cout = self.OR(self.AND(x, y), self.AND(cin, axy))
|
| 72 |
+
return s, cout
|
| 73 |
+
|
| 74 |
+
# ---- energy + couplings ------------------------------------------------
|
| 75 |
+
def energy(self, s: List[int]) -> int:
|
| 76 |
+
e = self.const
|
| 77 |
+
for i, c in self.L.items():
|
| 78 |
+
e += c * s[i]
|
| 79 |
+
for (i, j), c in self.Q.items():
|
| 80 |
+
e += c * s[i] * s[j]
|
| 81 |
+
return e
|
| 82 |
+
|
| 83 |
+
def neighbors(self) -> Dict[int, List[Tuple[int, int]]]:
|
| 84 |
+
nbr: Dict[int, List[Tuple[int, int]]] = defaultdict(list)
|
| 85 |
+
for (i, j), c in self.Q.items():
|
| 86 |
+
nbr[i].append((j, c))
|
| 87 |
+
nbr[j].append((i, c))
|
| 88 |
+
return nbr
|
| 89 |
+
|
| 90 |
+
# ---- relaxation modes --------------------------------------------------
|
| 91 |
+
def forward_eval(self, clamp: Dict[int, int]) -> List[int]:
|
| 92 |
+
"""Exact forward relaxation from the clamped inputs to the energy-0 fixed point."""
|
| 93 |
+
s = [0] * self.n
|
| 94 |
+
for w, v in clamp.items():
|
| 95 |
+
s[w] = v
|
| 96 |
+
for op, z, ins in self.gates:
|
| 97 |
+
if op == "AND":
|
| 98 |
+
s[z] = s[ins[0]] & s[ins[1]]
|
| 99 |
+
elif op == "OR":
|
| 100 |
+
s[z] = s[ins[0]] | s[ins[1]]
|
| 101 |
+
else:
|
| 102 |
+
s[z] = 1 - s[ins[0]]
|
| 103 |
+
return s
|
| 104 |
+
|
| 105 |
+
def relax_energy(self, clamp: Dict[int, int], sweeps: int = 4000,
|
| 106 |
+
t0: float = 4.0, t1: float = 0.02, seed: int = 0
|
| 107 |
+
) -> Tuple[List[int], bool]:
|
| 108 |
+
"""Anneal the full threshold network, tracking the lowest-energy state."""
|
| 109 |
+
nbr = self.neighbors()
|
| 110 |
+
rng = random.Random(seed)
|
| 111 |
+
s = [rng.randint(0, 1) for _ in range(self.n)]
|
| 112 |
+
for w, v in clamp.items():
|
| 113 |
+
s[w] = v
|
| 114 |
+
free = [i for i in range(self.n) if i not in clamp]
|
| 115 |
+
best, best_e = list(s), self.energy(s)
|
| 116 |
+
for step in range(sweeps):
|
| 117 |
+
T = t0 * (t1 / t0) ** (step / max(1, sweeps - 1))
|
| 118 |
+
for _ in range(len(free)):
|
| 119 |
+
i = free[rng.randrange(len(free))]
|
| 120 |
+
field = self.L[i] + sum(c * s[j] for j, c in nbr[i])
|
| 121 |
+
dE = (1 - 2 * s[i]) * field
|
| 122 |
+
if dE <= 0 or rng.random() < math.exp(-dE / T):
|
| 123 |
+
s[i] ^= 1
|
| 124 |
+
e = self.energy(s)
|
| 125 |
+
if e < best_e:
|
| 126 |
+
best, best_e = list(s), e
|
| 127 |
+
if best_e == 0:
|
| 128 |
+
return best, True
|
| 129 |
+
return best, best_e == 0
|
| 130 |
+
|
| 131 |
+
def solve(self, free_inputs: List[int], fixed: Dict[int, int],
|
| 132 |
+
target: Dict[int, int], sweeps: int = 3000, restarts: int = 80,
|
| 133 |
+
seed: int = 0) -> Optional[List[int]]:
|
| 134 |
+
"""Anneal chosen driver wires until the outputs match, the other wires slaved through the circuit."""
|
| 135 |
+
rng = random.Random(seed)
|
| 136 |
+
|
| 137 |
+
def mism(vals: Dict[int, int]) -> int:
|
| 138 |
+
s = self.forward_eval({**fixed, **vals})
|
| 139 |
+
return sum(1 for w, v in target.items() if s[w] != v)
|
| 140 |
+
|
| 141 |
+
for _ in range(restarts):
|
| 142 |
+
vals = {w: rng.randint(0, 1) for w in free_inputs}
|
| 143 |
+
m = mism(vals)
|
| 144 |
+
if m == 0:
|
| 145 |
+
return self.forward_eval({**fixed, **vals})
|
| 146 |
+
for step in range(sweeps):
|
| 147 |
+
T = 2.0 * (0.02 / 2.0) ** (step / sweeps)
|
| 148 |
+
w = free_inputs[rng.randrange(len(free_inputs))]
|
| 149 |
+
vals[w] ^= 1
|
| 150 |
+
m2 = mism(vals)
|
| 151 |
+
if m2 <= m or rng.random() < math.exp(-(m2 - m) / T):
|
| 152 |
+
m = m2
|
| 153 |
+
if m == 0:
|
| 154 |
+
return self.forward_eval({**fixed, **vals})
|
| 155 |
+
else:
|
| 156 |
+
vals[w] ^= 1
|
| 157 |
+
return None
|
| 158 |
+
|
| 159 |
+
|
| 160 |
+
# ---------------------------------------------------------------------------
|
| 161 |
+
# Circuit builders
|
| 162 |
+
# ---------------------------------------------------------------------------
|
| 163 |
+
def adder(bits: int) -> Tuple[Circuit, dict]:
|
| 164 |
+
c = Circuit()
|
| 165 |
+
xs, ys = c.wires(bits), c.wires(bits)
|
| 166 |
+
cin = c.wire()
|
| 167 |
+
outs, carry = [], cin
|
| 168 |
+
for k in range(bits):
|
| 169 |
+
s, carry = c.full_adder(xs[k], ys[k], carry)
|
| 170 |
+
outs.append(s)
|
| 171 |
+
return c, {"xs": xs, "ys": ys, "cin": cin, "sum": outs + [carry]}
|
| 172 |
+
|
| 173 |
+
|
| 174 |
+
def multiplier(bits: int) -> Tuple[Circuit, dict]:
|
| 175 |
+
c = Circuit()
|
| 176 |
+
xs, ys = c.wires(bits), c.wires(bits)
|
| 177 |
+
zero = c.wire()
|
| 178 |
+
acc = [zero] * (2 * bits)
|
| 179 |
+
for i in range(bits):
|
| 180 |
+
carry = zero
|
| 181 |
+
for j in range(bits):
|
| 182 |
+
acc[i + j], carry = c.full_adder(acc[i + j], c.AND(xs[i], ys[j]), carry)
|
| 183 |
+
acc[i + bits] = carry
|
| 184 |
+
return c, {"xs": xs, "ys": ys, "zero": zero, "prod": acc}
|
| 185 |
+
|
| 186 |
+
|
| 187 |
+
_OPCODE = {"AND": 0, "OR": 1, "NOT": 2}
|
| 188 |
+
|
| 189 |
+
_OPNAME = {v: k for k, v in _OPCODE.items()}
|
| 190 |
+
|
| 191 |
+
|
| 192 |
+
def to_tensors(circ: Circuit, io: dict):
|
| 193 |
+
"""Serialize the coupling matrix (the program) and the gate list to tensors."""
|
| 194 |
+
import torch
|
| 195 |
+
qi = sorted(circ.Q)
|
| 196 |
+
q_idx = torch.tensor(qi if qi else [], dtype=torch.long).reshape(-1, 2)
|
| 197 |
+
q_val = torch.tensor([circ.Q[k] for k in qi], dtype=torch.long)
|
| 198 |
+
li = sorted(circ.L)
|
| 199 |
+
l_idx = torch.tensor(li, dtype=torch.long)
|
| 200 |
+
l_val = torch.tensor([circ.L[i] for i in li], dtype=torch.long)
|
| 201 |
+
g_op = torch.tensor([_OPCODE[op] for op, _, _ in circ.gates], dtype=torch.long)
|
| 202 |
+
g_out = torch.tensor([o for _, o, _ in circ.gates], dtype=torch.long)
|
| 203 |
+
g_in = torch.tensor([[ins[0], ins[1] if len(ins) > 1 else -1]
|
| 204 |
+
for _, _, ins in circ.gates], dtype=torch.long).reshape(-1, 2)
|
| 205 |
+
t = {"Q_idx": q_idx, "Q_val": q_val, "L_idx": l_idx, "L_val": l_val,
|
| 206 |
+
"gate_op": g_op, "gate_out": g_out, "gate_in": g_in}
|
| 207 |
+
import json
|
| 208 |
+
meta = {"machine": "attractor", "n": str(circ.n), "const": str(circ.const),
|
| 209 |
+
"io": json.dumps({k: v for k, v in io.items()})}
|
| 210 |
+
return t, meta
|
| 211 |
+
|
| 212 |
+
|
| 213 |
+
def from_tensors(t: dict, meta: dict) -> Tuple[Circuit, dict]:
|
| 214 |
+
import json
|
| 215 |
+
c = Circuit()
|
| 216 |
+
c.n = int(meta["n"])
|
| 217 |
+
c.const = int(meta["const"])
|
| 218 |
+
for (i, j), v in zip(t["Q_idx"].tolist(), t["Q_val"].tolist()):
|
| 219 |
+
c.Q[(i, j)] = v
|
| 220 |
+
for i, v in zip(t["L_idx"].tolist(), t["L_val"].tolist()):
|
| 221 |
+
c.L[i] = v
|
| 222 |
+
for op, out, ins in zip(t["gate_op"].tolist(), t["gate_out"].tolist(), t["gate_in"].tolist()):
|
| 223 |
+
c.gates.append((_OPNAME[op], out, tuple(x for x in ins if x >= 0)))
|
| 224 |
+
return c, json.loads(meta["io"])
|
| 225 |
+
|
| 226 |
+
|
| 227 |
+
def cnf(clauses: List[List[int]], n_vars: int) -> Tuple[Circuit, dict]:
|
| 228 |
+
"""Compile a CNF formula."""
|
| 229 |
+
c = Circuit()
|
| 230 |
+
var = {v: c.wire() for v in range(1, n_vars + 1)}
|
| 231 |
+
clause_ws = []
|
| 232 |
+
for cl in clauses:
|
| 233 |
+
lits = [var[abs(l)] if l > 0 else c.NOT(var[abs(l)]) for l in cl]
|
| 234 |
+
acc = lits[0]
|
| 235 |
+
for w in lits[1:]:
|
| 236 |
+
acc = c.OR(acc, w)
|
| 237 |
+
clause_ws.append(acc)
|
| 238 |
+
sat = clause_ws[0]
|
| 239 |
+
for w in clause_ws[1:]:
|
| 240 |
+
sat = c.AND(sat, w)
|
| 241 |
+
return c, {"vars": var, "sat": sat}
|
| 242 |
+
|
| 243 |
+
|
| 244 |
+
def test_forward():
|
| 245 |
+
ok = True
|
| 246 |
+
for bits in (4, 8):
|
| 247 |
+
c, io = adder(bits)
|
| 248 |
+
rng = random.Random(bits)
|
| 249 |
+
bad = 0
|
| 250 |
+
for _ in range(300):
|
| 251 |
+
a, b = rng.randint(0, (1 << bits) - 1), rng.randint(0, (1 << bits) - 1)
|
| 252 |
+
clamp = {io["cin"]: 0}
|
| 253 |
+
for k in range(bits):
|
| 254 |
+
clamp[io["xs"][k]] = (a >> k) & 1
|
| 255 |
+
clamp[io["ys"][k]] = (b >> k) & 1
|
| 256 |
+
s = c.forward_eval(clamp)
|
| 257 |
+
got = sum(s[w] << k for k, w in enumerate(io["sum"]))
|
| 258 |
+
if got != a + b or c.energy(s) != 0:
|
| 259 |
+
bad += 1
|
| 260 |
+
print(f" forward adder {bits}-bit: {'OK' if bad == 0 else f'FAIL({bad})'}")
|
| 261 |
+
ok &= bad == 0
|
| 262 |
+
for bits in (3, 5):
|
| 263 |
+
c, io = multiplier(bits)
|
| 264 |
+
rng = random.Random(100 + bits)
|
| 265 |
+
bad = 0
|
| 266 |
+
for _ in range(300):
|
| 267 |
+
a, b = rng.randint(0, (1 << bits) - 1), rng.randint(0, (1 << bits) - 1)
|
| 268 |
+
clamp = {io["zero"]: 0}
|
| 269 |
+
for k in range(bits):
|
| 270 |
+
clamp[io["xs"][k]] = (a >> k) & 1
|
| 271 |
+
clamp[io["ys"][k]] = (b >> k) & 1
|
| 272 |
+
s = c.forward_eval(clamp)
|
| 273 |
+
got = sum(s[w] << k for k, w in enumerate(io["prod"]))
|
| 274 |
+
if got != a * b or c.energy(s) != 0:
|
| 275 |
+
bad += 1
|
| 276 |
+
print(f" forward multiplier {bits}-bit: {'OK' if bad == 0 else f'FAIL({bad})'}")
|
| 277 |
+
ok &= bad == 0
|
| 278 |
+
return ok
|
| 279 |
+
|
| 280 |
+
|
| 281 |
+
def test_energy_relax():
|
| 282 |
+
"""The canonical form: anneal the whole network (no propagation shortcut)."""
|
| 283 |
+
c, io = adder(4)
|
| 284 |
+
rng = random.Random(3)
|
| 285 |
+
bad = 0
|
| 286 |
+
for _ in range(20):
|
| 287 |
+
a, b = rng.randint(0, 15), rng.randint(0, 15)
|
| 288 |
+
clamp = {io["cin"]: 0}
|
| 289 |
+
for k in range(4):
|
| 290 |
+
clamp[io["xs"][k]] = (a >> k) & 1
|
| 291 |
+
clamp[io["ys"][k]] = (b >> k) & 1
|
| 292 |
+
conv = False
|
| 293 |
+
for attempt in range(4): # annealers restart
|
| 294 |
+
s, conv = c.relax_energy(clamp, sweeps=6000, seed=rng.randint(0, 1 << 30))
|
| 295 |
+
got = sum(s[w] << k for k, w in enumerate(io["sum"]))
|
| 296 |
+
if conv and got == a + b:
|
| 297 |
+
break
|
| 298 |
+
if not conv:
|
| 299 |
+
bad += 1
|
| 300 |
+
print(f" whole-network energy relaxation (4-bit adder, 20 cases): "
|
| 301 |
+
f"{'OK' if bad == 0 else f'reached ground state in {20 - bad}/20'}")
|
| 302 |
+
return bad == 0
|
| 303 |
+
|
| 304 |
+
|
| 305 |
+
def test_factor():
|
| 306 |
+
ok = True
|
| 307 |
+
for bits, targets in ((4, [15, 35, 143]), (5, [21, 55, 91])):
|
| 308 |
+
c, io = multiplier(bits)
|
| 309 |
+
for N in targets:
|
| 310 |
+
target = {io["prod"][k]: (N >> k) & 1 for k in range(2 * bits)}
|
| 311 |
+
s = c.solve(io["xs"] + io["ys"], {io["zero"]: 0}, target, seed=N)
|
| 312 |
+
if s is None:
|
| 313 |
+
print(f" factor {N}: not found")
|
| 314 |
+
ok = False
|
| 315 |
+
continue
|
| 316 |
+
a = sum(s[io["xs"][k]] << k for k in range(bits))
|
| 317 |
+
b = sum(s[io["ys"][k]] << k for k in range(bits))
|
| 318 |
+
good = a * b == N and 1 < a < N and 1 < b < N
|
| 319 |
+
print(f" factor {N} ({bits}x{bits}): {a} x {b} {'OK' if a * b == N else 'WRONG'}")
|
| 320 |
+
ok &= a * b == N
|
| 321 |
+
return ok
|
| 322 |
+
|
| 323 |
+
|
| 324 |
+
def test_sat():
|
| 325 |
+
# (x1 | x2 | ~x3) & (~x1 | x3) & (x2 | x3) & (~x2 | ~x3), a satisfiable 3-SAT.
|
| 326 |
+
clauses = [[1, 2, -3], [-1, 3], [2, 3], [-2, -3]]
|
| 327 |
+
c, io = cnf(clauses, 3)
|
| 328 |
+
s = c.solve(list(io["vars"].values()), {}, {io["sat"]: 1}, seed=1)
|
| 329 |
+
if s is None:
|
| 330 |
+
print(" SAT solve: no model found")
|
| 331 |
+
return False
|
| 332 |
+
assign = {v: s[w] for v, w in io["vars"].items()}
|
| 333 |
+
sat = all(any((assign[abs(l)] == 1) if l > 0 else (assign[abs(l)] == 0) for l in cl)
|
| 334 |
+
for cl in clauses)
|
| 335 |
+
print(f" SAT solve: model {assign} {'satisfies' if sat else 'FAILS'} the formula")
|
| 336 |
+
return sat
|
| 337 |
+
|
| 338 |
+
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
| 339 |
+
|
| 340 |
+
|
| 341 |
+
ATTRACTOR_OUT = os.path.join(ROOT, "variants", "neural_attractor.safetensors")
|
| 342 |
+
|
| 343 |
+
BITS = 8
|
| 344 |
+
|
| 345 |
+
|
| 346 |
+
def main_build_attractor() -> int:
|
| 347 |
+
c, io = multiplier(BITS)
|
| 348 |
+
tensors, meta = to_tensors(c, io)
|
| 349 |
+
save_file(tensors, ATTRACTOR_OUT, metadata=meta)
|
| 350 |
+
size = os.path.getsize(ATTRACTOR_OUT)
|
| 351 |
+
print(f"Built {os.path.relpath(ATTRACTOR_OUT, ROOT)}: {BITS}x{BITS} multiplier as couplings")
|
| 352 |
+
print(f" wires={c.n} Q entries={len(c.Q)} L entries={len(c.L)} size={size/1024:.1f} KB")
|
| 353 |
+
|
| 354 |
+
# round-trip
|
| 355 |
+
t = load_file(ATTRACTOR_OUT)
|
| 356 |
+
with safe_open(ATTRACTOR_OUT, framework="pt") as f:
|
| 357 |
+
m = f.metadata()
|
| 358 |
+
c2, io2 = from_tensors(t, m)
|
| 359 |
+
xs, ys, zero, prod = io2["xs"], io2["ys"], io2["zero"], io2["prod"]
|
| 360 |
+
|
| 361 |
+
rng = random.Random(0)
|
| 362 |
+
bad = 0
|
| 363 |
+
for _ in range(200):
|
| 364 |
+
a, b = rng.randint(0, 255), rng.randint(0, 255)
|
| 365 |
+
clamp = {zero: 0}
|
| 366 |
+
for k in range(BITS):
|
| 367 |
+
clamp[xs[k]] = (a >> k) & 1
|
| 368 |
+
clamp[ys[k]] = (b >> k) & 1
|
| 369 |
+
s = c2.forward_eval(clamp)
|
| 370 |
+
got = sum(s[w] << k for k, w in enumerate(prod))
|
| 371 |
+
if got != a * b or c2.energy(s) != 0:
|
| 372 |
+
bad += 1
|
| 373 |
+
print(f" round-trip forward multiply (200 cases): {'OK' if bad == 0 else f'FAIL({bad})'}")
|
| 374 |
+
|
| 375 |
+
# backward: factor a small product through the loaded couplings
|
| 376 |
+
N = 35
|
| 377 |
+
target = {prod[k]: (N >> k) & 1 for k in range(2 * BITS)}
|
| 378 |
+
s = c2.solve(xs + ys, {zero: 0}, target, sweeps=2500, restarts=120, seed=N)
|
| 379 |
+
if s is not None:
|
| 380 |
+
fa = sum(s[xs[k]] << k for k in range(BITS))
|
| 381 |
+
fb = sum(s[ys[k]] << k for k in range(BITS))
|
| 382 |
+
print(f" round-trip backward factor {N}: {fa} x {fb} {'OK' if fa * fb == N else 'WRONG'}")
|
| 383 |
+
ok = fa * fb == N
|
| 384 |
+
else:
|
| 385 |
+
print(f" round-trip backward factor {N}: not found")
|
| 386 |
+
ok = False
|
| 387 |
+
return 0 if (bad == 0 and ok) else 1
|
| 388 |
+
|
| 389 |
+
# --- threshold-gate truth: the target updates are computed by Heaviside gates ---
|
| 390 |
+
def H(x: int) -> int:
|
| 391 |
+
return 1 if x >= 0 else 0
|
| 392 |
+
|
| 393 |
+
|
| 394 |
+
def g_and(a: int, b: int) -> int:
|
| 395 |
+
return H(a + b - 2) # fires iff a=b=1
|
| 396 |
+
|
| 397 |
+
|
| 398 |
+
def g_xor(a: int, b: int) -> int:
|
| 399 |
+
# XOR as AND(OR, NAND): OR=H(a+b-1), NAND=H(1-a-b), out=AND(OR,NAND)
|
| 400 |
+
return g_and(H(a + b - 1), H(1 - a - b))
|
| 401 |
+
|
| 402 |
+
|
| 403 |
+
# --- reversible primitives, in place on a bit register (a Python list) ---
|
| 404 |
+
def NOT(reg: List[int], t: int) -> None:
|
| 405 |
+
reg[t] = 1 - reg[t]
|
| 406 |
+
|
| 407 |
+
|
| 408 |
+
def CNOT(reg: List[int], c: int, t: int) -> None:
|
| 409 |
+
reg[t] = g_xor(reg[t], reg[c])
|
| 410 |
+
|
| 411 |
+
|
| 412 |
+
def TOFF(reg: List[int], a: int, b: int, t: int) -> None:
|
| 413 |
+
reg[t] = g_xor(reg[t], g_and(reg[a], reg[b]))
|
| 414 |
+
|
| 415 |
+
|
| 416 |
+
def FRED(reg: List[int], c: int, x: int, y: int) -> None:
|
| 417 |
+
# controlled swap of x,y on control c
|
| 418 |
+
if reg[c]:
|
| 419 |
+
reg[x], reg[y] = reg[y], reg[x]
|
| 420 |
+
|
| 421 |
+
|
| 422 |
+
# --- Cuccaro ripple-carry adder: b += a in place, one carry ancilla ---
|
| 423 |
+
# MAJ(c,b,a): b^=a ; c^=a ; a ^= b&c UMA(c,b,a): a ^= b&c ; c^=a ; b^=c
|
| 424 |
+
def _maj(reg, c, b, a):
|
| 425 |
+
CNOT(reg, a, b)
|
| 426 |
+
CNOT(reg, a, c)
|
| 427 |
+
TOFF(reg, b, c, a)
|
| 428 |
+
|
| 429 |
+
|
| 430 |
+
def _uma(reg, c, b, a):
|
| 431 |
+
TOFF(reg, b, c, a)
|
| 432 |
+
CNOT(reg, a, c)
|
| 433 |
+
CNOT(reg, c, b)
|
| 434 |
+
|
| 435 |
+
|
| 436 |
+
# Each primitive below is its own inverse, so the inverse of a gate sequence is
|
| 437 |
+
# the reversed sequence. Building the adder as an op list gives subtraction for
|
| 438 |
+
# free (run it backward).
|
| 439 |
+
def _maj_ops(c, b, a):
|
| 440 |
+
return [(CNOT, a, b), (CNOT, a, c), (TOFF, b, c, a)]
|
| 441 |
+
|
| 442 |
+
|
| 443 |
+
def _uma_ops(c, b, a):
|
| 444 |
+
return [(TOFF, b, c, a), (CNOT, a, c), (CNOT, c, b)]
|
| 445 |
+
|
| 446 |
+
|
| 447 |
+
def _adder_ops(a_bits, b_bits, carry, cout=None):
|
| 448 |
+
n = len(a_bits)
|
| 449 |
+
ops = _maj_ops(carry, b_bits[0], a_bits[0])
|
| 450 |
+
for i in range(1, n):
|
| 451 |
+
ops += _maj_ops(a_bits[i - 1], b_bits[i], a_bits[i])
|
| 452 |
+
if cout is not None:
|
| 453 |
+
ops.append((CNOT, a_bits[n - 1], cout))
|
| 454 |
+
for i in range(n - 1, 0, -1):
|
| 455 |
+
ops += _uma_ops(a_bits[i - 1], b_bits[i], a_bits[i])
|
| 456 |
+
ops += _uma_ops(carry, b_bits[0], a_bits[0])
|
| 457 |
+
return ops
|
| 458 |
+
|
| 459 |
+
|
| 460 |
+
def _apply(reg, ops, inverse=False):
|
| 461 |
+
for gate, *args in (reversed(ops) if inverse else ops):
|
| 462 |
+
gate(reg, *args)
|
| 463 |
+
|
| 464 |
+
|
| 465 |
+
def add_into(reg, a_bits, b_bits, carry, cout=None):
|
| 466 |
+
"""b <- (a + b) mod 2^n (LSB first); a and carry (=0) restored."""
|
| 467 |
+
_apply(reg, _adder_ops(a_bits, b_bits, carry, cout))
|
| 468 |
+
|
| 469 |
+
|
| 470 |
+
def sub_into(reg, a_bits, b_bits, carry, cout=None):
|
| 471 |
+
"""b <- (b - a) mod 2^n: the adder run backward."""
|
| 472 |
+
_apply(reg, _adder_ops(a_bits, b_bits, carry, cout), inverse=True)
|
| 473 |
+
|
| 474 |
+
|
| 475 |
+
def xor_into(reg, a_bits, b_bits):
|
| 476 |
+
"""b <- b XOR a, bitwise (self-inverse)."""
|
| 477 |
+
for a, b in zip(a_bits, b_bits):
|
| 478 |
+
CNOT(reg, a, b)
|
| 479 |
+
|
| 480 |
+
|
| 481 |
+
def incr(reg, b_bits, one_bits, carry):
|
| 482 |
+
"""b <- b + 1 mod 2^n."""
|
| 483 |
+
add_into(reg, one_bits, b_bits, carry)
|
| 484 |
+
|
| 485 |
+
|
| 486 |
+
def neg_into(reg, b_bits, one_bits, carry):
|
| 487 |
+
"""b <- (-b) mod 2^n via two's complement (~b then +1). Self-inverse."""
|
| 488 |
+
for t in b_bits:
|
| 489 |
+
NOT(reg, t)
|
| 490 |
+
add_into(reg, one_bits, b_bits, carry)
|
| 491 |
+
|
| 492 |
+
|
| 493 |
+
def rot_left(reg, b_bits, k=1):
|
| 494 |
+
"""Rotate the word left by k (a permutation of bit positions; reversible)."""
|
| 495 |
+
n = len(b_bits)
|
| 496 |
+
k %= n
|
| 497 |
+
vals = [reg[b_bits[i]] for i in range(n)]
|
| 498 |
+
for i in range(n):
|
| 499 |
+
reg[b_bits[(i + k) % n]] = vals[i]
|
| 500 |
+
|
| 501 |
+
|
| 502 |
+
def and_into(reg, a_bits, b_bits, t_bits):
|
| 503 |
+
"""t <- t XOR (a AND b), bitwise (word-level Toffoli). Self-inverse."""
|
| 504 |
+
for a, b, t in zip(a_bits, b_bits, t_bits):
|
| 505 |
+
TOFF(reg, a, b, t)
|
| 506 |
+
|
| 507 |
+
|
| 508 |
+
# --- verification helpers ---
|
| 509 |
+
def is_permutation(fn, nbits: int) -> bool:
|
| 510 |
+
"""Check fn: {0,1}^nbits -> {0,1}^nbits is a bijection (exhaustive)."""
|
| 511 |
+
seen = set()
|
| 512 |
+
for x in range(1 << nbits):
|
| 513 |
+
reg = [(x >> k) & 1 for k in range(nbits)]
|
| 514 |
+
fn(reg)
|
| 515 |
+
y = sum(b << k for k, b in enumerate(reg))
|
| 516 |
+
seen.add(y)
|
| 517 |
+
return len(seen) == (1 << nbits)
|
| 518 |
+
|
| 519 |
+
|
| 520 |
+
def _test_primitives():
|
| 521 |
+
ok = True
|
| 522 |
+
ok &= is_permutation(lambda r: NOT(r, 0), 1)
|
| 523 |
+
ok &= is_permutation(lambda r: CNOT(r, 0, 1), 2)
|
| 524 |
+
ok &= is_permutation(lambda r: TOFF(r, 0, 1, 2), 3)
|
| 525 |
+
ok &= is_permutation(lambda r: FRED(r, 0, 1, 2), 3)
|
| 526 |
+
print(f" primitives bijective (NOT/CNOT/TOFF/FRED): {'OK' if ok else 'FAIL'}")
|
| 527 |
+
return ok
|
| 528 |
+
|
| 529 |
+
|
| 530 |
+
def _test_adder(width=4):
|
| 531 |
+
# layout: a[0..w-1], b[0..w-1], carry
|
| 532 |
+
a_bits = list(range(width))
|
| 533 |
+
b_bits = list(range(width, 2 * width))
|
| 534 |
+
carry = 2 * width
|
| 535 |
+
n = 2 * width + 1
|
| 536 |
+
ok_perm = is_permutation(lambda r: add_into(r, a_bits, b_bits, carry), n)
|
| 537 |
+
bad = 0
|
| 538 |
+
mask = (1 << width) - 1
|
| 539 |
+
for a in range(1 << width):
|
| 540 |
+
for b in range(1 << width):
|
| 541 |
+
reg = [0] * n
|
| 542 |
+
for k in range(width):
|
| 543 |
+
reg[a_bits[k]] = (a >> k) & 1
|
| 544 |
+
reg[b_bits[k]] = (b >> k) & 1
|
| 545 |
+
add_into(reg, a_bits, b_bits, carry)
|
| 546 |
+
got_a = sum(reg[a_bits[k]] << k for k in range(width))
|
| 547 |
+
got_b = sum(reg[b_bits[k]] << k for k in range(width))
|
| 548 |
+
if got_a != a or got_b != ((a + b) & mask) or reg[carry] != 0:
|
| 549 |
+
bad += 1
|
| 550 |
+
print(f" Cuccaro adder {width}-bit: bijection={'OK' if ok_perm else 'FAIL'} "
|
| 551 |
+
f"b<-a+b, a & carry restored={'OK' if bad == 0 else f'FAIL({bad})'}")
|
| 552 |
+
return ok_perm and bad == 0
|
| 553 |
+
|
| 554 |
+
|
| 555 |
+
def bennett(reg, a_b, b_b, c_b, scr_b, out_b, carry):
|
| 556 |
+
"""Bennett compute-copy-uncompute for the irreversible f(a,b,c)=(a+b) XOR c."""
|
| 557 |
+
ops = []
|
| 558 |
+
# compute f into scratch (scratch starts 0): scratch += a; scratch += b; scratch ^= c
|
| 559 |
+
ops += _adder_ops(a_b, scr_b, carry)
|
| 560 |
+
ops += _adder_ops(b_b, scr_b, carry)
|
| 561 |
+
ops += [(CNOT, c, s) for c, s in zip(c_b, scr_b)]
|
| 562 |
+
# copy scratch -> out
|
| 563 |
+
ops += [(CNOT, s, o) for s, o in zip(scr_b, out_b)]
|
| 564 |
+
# uncompute scratch (reverse of the compute prefix)
|
| 565 |
+
n_comp = len(_adder_ops(a_b, scr_b, carry)) * 2 + len(c_b)
|
| 566 |
+
ops += [(g, *args) for g, *args in reversed(ops[:n_comp])]
|
| 567 |
+
_apply(reg, ops)
|
| 568 |
+
return ops
|
| 569 |
+
|
| 570 |
+
|
| 571 |
+
def _test_bennett(width=4):
|
| 572 |
+
a_b = list(range(width))
|
| 573 |
+
b_b = list(range(width, 2 * width))
|
| 574 |
+
c_b = list(range(2 * width, 3 * width))
|
| 575 |
+
scr_b = list(range(3 * width, 4 * width))
|
| 576 |
+
out_b = list(range(4 * width, 5 * width))
|
| 577 |
+
carry = 5 * width
|
| 578 |
+
n = 5 * width + 1
|
| 579 |
+
mask = (1 << width) - 1
|
| 580 |
+
bad = 0
|
| 581 |
+
for a in range(1 << width):
|
| 582 |
+
for b in range(1 << width):
|
| 583 |
+
for c in range(1 << width):
|
| 584 |
+
r = [0] * n
|
| 585 |
+
for k in range(width):
|
| 586 |
+
r[a_b[k]] = (a >> k) & 1
|
| 587 |
+
r[b_b[k]] = (b >> k) & 1
|
| 588 |
+
r[c_b[k]] = (c >> k) & 1
|
| 589 |
+
bennett(r, a_b, b_b, c_b, scr_b, out_b, carry)
|
| 590 |
+
ra = sum(r[a_b[k]] << k for k in range(width))
|
| 591 |
+
rb = sum(r[b_b[k]] << k for k in range(width))
|
| 592 |
+
rc = sum(r[c_b[k]] << k for k in range(width))
|
| 593 |
+
rs = sum(r[scr_b[k]] << k for k in range(width))
|
| 594 |
+
ro = sum(r[out_b[k]] << k for k in range(width))
|
| 595 |
+
if (ra, rb, rc, rs, ro, r[carry]) != (a, b, c, 0, ((a + b) & mask) ^ c, 0):
|
| 596 |
+
bad += 1
|
| 597 |
+
print(f" Bennett (a,b,c,0,0)->(a,b,c,0,(a+b)^c), scratch cleaned "
|
| 598 |
+
f"[{width}-bit]: {'OK' if bad == 0 else f'FAIL({bad})'}")
|
| 599 |
+
return bad == 0
|
| 600 |
+
|
| 601 |
+
|
| 602 |
+
def _test_alu(width=4):
|
| 603 |
+
mask = (1 << width) - 1
|
| 604 |
+
a_b = list(range(width))
|
| 605 |
+
b_b = list(range(width, 2 * width))
|
| 606 |
+
one_b = list(range(2 * width, 3 * width))
|
| 607 |
+
t_b = list(range(3 * width, 4 * width))
|
| 608 |
+
carry = 4 * width
|
| 609 |
+
n = 4 * width + 1
|
| 610 |
+
|
| 611 |
+
def fresh(a=0, b=0, t=0, one=False):
|
| 612 |
+
r = [0] * n
|
| 613 |
+
for k in range(width):
|
| 614 |
+
r[a_b[k]] = (a >> k) & 1
|
| 615 |
+
r[b_b[k]] = (b >> k) & 1
|
| 616 |
+
r[t_b[k]] = (t >> k) & 1
|
| 617 |
+
if one:
|
| 618 |
+
r[one_b[0]] = 1
|
| 619 |
+
return r
|
| 620 |
+
|
| 621 |
+
def rd(r, bits):
|
| 622 |
+
return sum(r[bits[k]] << k for k in range(width))
|
| 623 |
+
|
| 624 |
+
results = {}
|
| 625 |
+
# subtract
|
| 626 |
+
bad = 0
|
| 627 |
+
for a in range(1 << width):
|
| 628 |
+
for b in range(1 << width):
|
| 629 |
+
r = fresh(a, b)
|
| 630 |
+
sub_into(r, a_b, b_b, carry)
|
| 631 |
+
if rd(r, b_b) != ((b - a) & mask) or rd(r, a_b) != a or r[carry] != 0:
|
| 632 |
+
bad += 1
|
| 633 |
+
results["sub b-=a"] = bad == 0 and is_permutation(lambda r: sub_into(r, a_b, b_b, carry), n)
|
| 634 |
+
# xor
|
| 635 |
+
bad = 0
|
| 636 |
+
for a in range(1 << width):
|
| 637 |
+
for b in range(1 << width):
|
| 638 |
+
r = fresh(a, b)
|
| 639 |
+
xor_into(r, a_b, b_b)
|
| 640 |
+
if rd(r, b_b) != (a ^ b) or rd(r, a_b) != a:
|
| 641 |
+
bad += 1
|
| 642 |
+
results["xor b^=a"] = bad == 0
|
| 643 |
+
# negate
|
| 644 |
+
bad = 0
|
| 645 |
+
for b in range(1 << width):
|
| 646 |
+
r = fresh(b=b, one=True)
|
| 647 |
+
neg_into(r, b_b, one_b, carry)
|
| 648 |
+
if rd(r, b_b) != ((-b) & mask) or rd(r, one_b) != 1 or r[carry] != 0:
|
| 649 |
+
bad += 1
|
| 650 |
+
results["neg b=-b"] = bad == 0
|
| 651 |
+
# increment
|
| 652 |
+
bad = 0
|
| 653 |
+
for b in range(1 << width):
|
| 654 |
+
r = fresh(b=b, one=True)
|
| 655 |
+
incr(r, b_b, one_b, carry)
|
| 656 |
+
if rd(r, b_b) != ((b + 1) & mask):
|
| 657 |
+
bad += 1
|
| 658 |
+
results["incr b+=1"] = bad == 0
|
| 659 |
+
# rotate
|
| 660 |
+
bad = 0
|
| 661 |
+
for b in range(1 << width):
|
| 662 |
+
r = fresh(b=b)
|
| 663 |
+
rot_left(r, b_b, 1)
|
| 664 |
+
exp = ((b << 1) | (b >> (width - 1))) & mask
|
| 665 |
+
if rd(r, b_b) != exp:
|
| 666 |
+
bad += 1
|
| 667 |
+
results["rot_left"] = bad == 0
|
| 668 |
+
# and-into (Toffoli word)
|
| 669 |
+
bad = 0
|
| 670 |
+
for a in range(1 << width):
|
| 671 |
+
for b in range(1 << width):
|
| 672 |
+
for t in range(1 << width):
|
| 673 |
+
r = fresh(a, b, t)
|
| 674 |
+
and_into(r, a_b, b_b, t_b)
|
| 675 |
+
if rd(r, t_b) != (t ^ (a & b)):
|
| 676 |
+
bad += 1
|
| 677 |
+
results["and t^=a&b"] = bad == 0
|
| 678 |
+
ok = all(results.values())
|
| 679 |
+
print(" reversible ALU " + f"{width}-bit: " +
|
| 680 |
+
" ".join(f"{k}={'OK' if v else 'FAIL'}" for k, v in results.items()))
|
| 681 |
+
return ok
|
| 682 |
+
|
| 683 |
+
|
| 684 |
+
class RCPU:
|
| 685 |
+
def __init__(self, program: List[tuple], k_regs=4, width=8, mem_words=16):
|
| 686 |
+
self.prog = program
|
| 687 |
+
self.K = k_regs
|
| 688 |
+
self.W = width
|
| 689 |
+
self.M = mem_words
|
| 690 |
+
self.mask = (1 << width) - 1
|
| 691 |
+
self.L = len(program)
|
| 692 |
+
|
| 693 |
+
def new_state(self, regs=None, mem=None) -> dict:
|
| 694 |
+
return {"R": list(regs) + [0] * (self.K - len(regs)) if regs else [0] * self.K,
|
| 695 |
+
"PC": 0, "BR": 1, "MEM": list(mem) + [0] * (self.M - len(mem)) if mem else [0] * self.M}
|
| 696 |
+
|
| 697 |
+
# ---- reversible instruction effects (forward and inverse) ----
|
| 698 |
+
def _data(self, s, I, inverse: bool):
|
| 699 |
+
R, MEM, m = s["R"], s["MEM"], self.mask
|
| 700 |
+
op = I[0]
|
| 701 |
+
if op == "ADD":
|
| 702 |
+
d, r = I[1], I[2]
|
| 703 |
+
R[d] = (R[d] - R[r]) & m if inverse else (R[d] + R[r]) & m
|
| 704 |
+
elif op == "SUB":
|
| 705 |
+
d, r = I[1], I[2]
|
| 706 |
+
R[d] = (R[d] + R[r]) & m if inverse else (R[d] - R[r]) & m
|
| 707 |
+
elif op == "ADDI":
|
| 708 |
+
d, k = I[1], I[2]
|
| 709 |
+
R[d] = (R[d] - k) & m if inverse else (R[d] + k) & m
|
| 710 |
+
elif op == "XOR":
|
| 711 |
+
R[I[1]] ^= R[I[2]]
|
| 712 |
+
elif op == "XORI":
|
| 713 |
+
R[I[1]] ^= (I[2] & m)
|
| 714 |
+
elif op == "NEG":
|
| 715 |
+
R[I[1]] = (-R[I[1]]) & m
|
| 716 |
+
elif op == "TOFF":
|
| 717 |
+
R[I[1]] ^= (R[I[2]] & R[I[3]])
|
| 718 |
+
elif op == "ROL":
|
| 719 |
+
k = (-I[2] if inverse else I[2]) % self.W
|
| 720 |
+
R[I[1]] = ((R[I[1]] << k) | (R[I[1]] >> (self.W - k))) & m if k else R[I[1]]
|
| 721 |
+
elif op == "EXCH":
|
| 722 |
+
d, r = I[1], I[2]
|
| 723 |
+
a = R[r] % self.M
|
| 724 |
+
R[d], MEM[a] = MEM[a], R[d]
|
| 725 |
+
# BRA/BEZ/HALT have no data effect
|
| 726 |
+
|
| 727 |
+
def _toggle(self, s, I):
|
| 728 |
+
"""Reversible control: toggle BR for a taken branch (self-inverse)."""
|
| 729 |
+
op = I[0]
|
| 730 |
+
if op == "BRA":
|
| 731 |
+
s["BR"] ^= (I[1] ^ 1)
|
| 732 |
+
elif op == "BEZ":
|
| 733 |
+
if s["R"][I[1]] == 0:
|
| 734 |
+
s["BR"] ^= (I[2] ^ 1)
|
| 735 |
+
|
| 736 |
+
# ---- single-step transition and its inverse ----
|
| 737 |
+
def step(self, s):
|
| 738 |
+
I = self.prog[s["PC"]]
|
| 739 |
+
self._data(s, I, inverse=False)
|
| 740 |
+
self._toggle(s, I)
|
| 741 |
+
s["PC"] = (s["PC"] + s["BR"]) % self.L
|
| 742 |
+
|
| 743 |
+
def step_back(self, s):
|
| 744 |
+
s["PC"] = (s["PC"] - s["BR"]) % self.L
|
| 745 |
+
I = self.prog[s["PC"]]
|
| 746 |
+
self._toggle(s, I) # self-inverse: restores BR
|
| 747 |
+
self._data(s, I, inverse=True)
|
| 748 |
+
|
| 749 |
+
def run(self, s, steps):
|
| 750 |
+
for _ in range(steps):
|
| 751 |
+
self.step(s)
|
| 752 |
+
return s
|
| 753 |
+
|
| 754 |
+
def run_back(self, s, steps):
|
| 755 |
+
for _ in range(steps):
|
| 756 |
+
self.step_back(s)
|
| 757 |
+
return s
|
| 758 |
+
|
| 759 |
+
|
| 760 |
+
def _clone(s):
|
| 761 |
+
return {"R": list(s["R"]), "PC": s["PC"], "BR": s["BR"], "MEM": list(s["MEM"])}
|
| 762 |
+
|
| 763 |
+
|
| 764 |
+
def _eq(a, b):
|
| 765 |
+
return a["R"] == b["R"] and a["PC"] == b["PC"] and a["BR"] == b["BR"] and a["MEM"] == b["MEM"]
|
| 766 |
+
|
| 767 |
+
|
| 768 |
+
def test_straight_line():
|
| 769 |
+
prog = [("ADD", 1, 0), ("XOR", 1, 0), ("NEG", 1), ("ADDI", 1, 7),
|
| 770 |
+
("TOFF", 2, 0, 1), ("ROL", 0, 1)]
|
| 771 |
+
m = RCPU(prog, width=8)
|
| 772 |
+
ok = True
|
| 773 |
+
for a in (5, 0, 255, 100):
|
| 774 |
+
for b in (3, 1, 200):
|
| 775 |
+
s0 = m.new_state([a, b, 0, 0])
|
| 776 |
+
s = _clone(s0)
|
| 777 |
+
m.run(s, len(prog))
|
| 778 |
+
fwd = _clone(s)
|
| 779 |
+
m.run_back(s, len(prog))
|
| 780 |
+
ok &= _eq(s, s0) # round-trip recovers the exact input
|
| 781 |
+
print(f" straight-line round-trip (dir -1 recovers input): {'OK' if ok else 'FAIL'}")
|
| 782 |
+
return ok
|
| 783 |
+
|
| 784 |
+
|
| 785 |
+
def test_bijection():
|
| 786 |
+
"""The single-step transition is a bijection: step_back inverts step on every instruction."""
|
| 787 |
+
import itertools
|
| 788 |
+
W = 4
|
| 789 |
+
prog = [
|
| 790 |
+
("ADD", 1, 0), ("SUB", 1, 0), ("ADDI", 2, 3), ("XOR", 1, 2),
|
| 791 |
+
("XORI", 0, 5), ("NEG", 3), ("TOFF", 3, 0, 1), ("ROL", 0, 1),
|
| 792 |
+
("EXCH", 2, 3), ("BRA", 2), ("BEZ", 1, 3), ("BEZ", 0, -2),
|
| 793 |
+
]
|
| 794 |
+
m = RCPU(prog, k_regs=4, width=W, mem_words=4)
|
| 795 |
+
bad = 0
|
| 796 |
+
checked = 0
|
| 797 |
+
rng = __import__("random").Random(0)
|
| 798 |
+
for pc in range(len(prog)):
|
| 799 |
+
for br in (1, 2, 3, -2, -1):
|
| 800 |
+
for _ in range(60):
|
| 801 |
+
s0 = {"R": [rng.randint(0, (1 << W) - 1) for _ in range(4)],
|
| 802 |
+
"PC": pc, "BR": br,
|
| 803 |
+
"MEM": [rng.randint(0, (1 << W) - 1) for _ in range(4)]}
|
| 804 |
+
s = _clone(s0)
|
| 805 |
+
m.step(s)
|
| 806 |
+
m.step_back(s)
|
| 807 |
+
if not _eq(s, s0):
|
| 808 |
+
bad += 1
|
| 809 |
+
# and the other composition order
|
| 810 |
+
s = _clone(s0)
|
| 811 |
+
m.step_back(s)
|
| 812 |
+
m.step(s)
|
| 813 |
+
if not _eq(s, s0):
|
| 814 |
+
bad += 1
|
| 815 |
+
checked += 2
|
| 816 |
+
print(f" step_back o step = id over {checked} (state, instruction) cases: "
|
| 817 |
+
f"{'OK' if bad == 0 else f'FAIL({bad})'}")
|
| 818 |
+
return bad == 0
|
| 819 |
+
|
| 820 |
+
WIDTH = 8
|
| 821 |
+
|
| 822 |
+
MASK = (1 << WIDTH) - 1
|
| 823 |
+
|
| 824 |
+
|
| 825 |
+
def _regs_written(stmts) -> set:
|
| 826 |
+
w = set()
|
| 827 |
+
for st in stmts:
|
| 828 |
+
op = st[0]
|
| 829 |
+
if op in ("ADD", "SUB", "ADDI", "XOR", "NEG"):
|
| 830 |
+
w.add(st[1])
|
| 831 |
+
elif op == "SWAP":
|
| 832 |
+
w.add(st[1]); w.add(st[2])
|
| 833 |
+
elif op == "FOR":
|
| 834 |
+
w |= _regs_written(st[2])
|
| 835 |
+
elif op == "IF":
|
| 836 |
+
w |= _regs_written(st[2]) | _regs_written(st[3])
|
| 837 |
+
return w
|
| 838 |
+
|
| 839 |
+
|
| 840 |
+
def invert(stmts: List[tuple]) -> List[tuple]:
|
| 841 |
+
out = []
|
| 842 |
+
for st in reversed(stmts):
|
| 843 |
+
op = st[0]
|
| 844 |
+
if op == "ADD":
|
| 845 |
+
out.append(("SUB", st[1], st[2]))
|
| 846 |
+
elif op == "SUB":
|
| 847 |
+
out.append(("ADD", st[1], st[2]))
|
| 848 |
+
elif op == "ADDI":
|
| 849 |
+
out.append(("ADDI", st[1], -st[2]))
|
| 850 |
+
elif op in ("XOR", "NEG", "SWAP"):
|
| 851 |
+
out.append(st)
|
| 852 |
+
elif op == "FOR":
|
| 853 |
+
out.append(("FOR", st[1], invert(st[2])))
|
| 854 |
+
elif op == "IF":
|
| 855 |
+
# Janus: reverse swaps predicate and exit assertion, inverts branches
|
| 856 |
+
out.append(("IF", st[4], invert(st[2]), invert(st[3]), st[1]))
|
| 857 |
+
return out
|
| 858 |
+
|
| 859 |
+
|
| 860 |
+
def prog_run(stmts: List[tuple], s: Dict[str, int]) -> Dict[str, int]:
|
| 861 |
+
for st in stmts:
|
| 862 |
+
op = st[0]
|
| 863 |
+
if op == "ADD":
|
| 864 |
+
s[st[1]] = (s[st[1]] + s[st[2]]) & MASK
|
| 865 |
+
elif op == "SUB":
|
| 866 |
+
s[st[1]] = (s[st[1]] - s[st[2]]) & MASK
|
| 867 |
+
elif op == "ADDI":
|
| 868 |
+
s[st[1]] = (s[st[1]] + st[2]) & MASK
|
| 869 |
+
elif op == "XOR":
|
| 870 |
+
s[st[1]] ^= s[st[2]]
|
| 871 |
+
elif op == "NEG":
|
| 872 |
+
s[st[1]] = (-s[st[1]]) & MASK
|
| 873 |
+
elif op == "SWAP":
|
| 874 |
+
s[st[1]], s[st[2]] = s[st[2]], s[st[1]]
|
| 875 |
+
elif op == "FOR":
|
| 876 |
+
cnt, body = st[1], st[2]
|
| 877 |
+
if cnt in _regs_written(body):
|
| 878 |
+
raise ValueError("FOR count register must be read-only (irreversible otherwise)")
|
| 879 |
+
for _ in range(s[cnt]):
|
| 880 |
+
prog_run(body, s)
|
| 881 |
+
elif op == "IF":
|
| 882 |
+
pred, then, els, exit_assert = st[1], st[2], st[3], st[4]
|
| 883 |
+
if pred(s):
|
| 884 |
+
prog_run(then, s)
|
| 885 |
+
assert exit_assert(s), "exit assertion violated (not reversible)"
|
| 886 |
+
else:
|
| 887 |
+
prog_run(els, s)
|
| 888 |
+
assert not exit_assert(s), "exit assertion violated (not reversible)"
|
| 889 |
+
return s
|
| 890 |
+
|
| 891 |
+
|
| 892 |
+
# --- demonstration programs ---
|
| 893 |
+
MULTIPLY = [("FOR", "b", [("ADD", "acc", "a")])] # acc += a, b times; a,b preserved
|
| 894 |
+
|
| 895 |
+
FIB = [("FOR", "n", [("ADD", "a", "b"), ("SWAP", "a", "b")])] # (a,b)->(b,a+b), n times
|
| 896 |
+
|
| 897 |
+
|
| 898 |
+
def _test():
|
| 899 |
+
ok = True
|
| 900 |
+
|
| 901 |
+
# reversible multiply: acc = a*b, inputs preserved; inverse clears acc
|
| 902 |
+
bad = 0
|
| 903 |
+
for a in range(16):
|
| 904 |
+
for b in range(16):
|
| 905 |
+
s = {"a": a, "b": b, "acc": 0}
|
| 906 |
+
prog_run(MULTIPLY, s)
|
| 907 |
+
if s["acc"] != (a * b) & MASK or s["a"] != a or s["b"] != b:
|
| 908 |
+
bad += 1
|
| 909 |
+
prog_run(invert(MULTIPLY), s) # run backward
|
| 910 |
+
if s != {"a": a, "b": b, "acc": 0}:
|
| 911 |
+
bad += 1
|
| 912 |
+
print(f" reversible multiply acc=a*b, inputs preserved, inverse clears acc: "
|
| 913 |
+
f"{'OK' if bad == 0 else f'FAIL({bad})'}")
|
| 914 |
+
ok &= bad == 0
|
| 915 |
+
|
| 916 |
+
# reversible Fibonacci: n steps forward, inverse recovers the seed
|
| 917 |
+
bad = 0
|
| 918 |
+
for n in range(12):
|
| 919 |
+
s = {"a": 0, "b": 1, "n": n}
|
| 920 |
+
prog_run(FIB, s)
|
| 921 |
+
# forward value check against a plain reference
|
| 922 |
+
ra, rb = 0, 1
|
| 923 |
+
for _ in range(n):
|
| 924 |
+
ra, rb = rb, (ra + rb) & MASK
|
| 925 |
+
if (s["a"], s["b"]) != (ra, rb):
|
| 926 |
+
bad += 1
|
| 927 |
+
prog_run(invert(FIB), s)
|
| 928 |
+
if s != {"a": 0, "b": 1, "n": n}:
|
| 929 |
+
bad += 1
|
| 930 |
+
print(f" reversible Fibonacci n steps, inverse recovers seed: "
|
| 931 |
+
f"{'OK' if bad == 0 else f'FAIL({bad})'}")
|
| 932 |
+
ok &= bad == 0
|
| 933 |
+
|
| 934 |
+
# a reversible conditional (Janus IF): swap when the operands differ. The
|
| 935 |
+
# exit assertion a != b is true exactly when the then-branch ran (swapping
|
| 936 |
+
# distinct values keeps them distinct; equal values are skipped), so the
|
| 937 |
+
# reverse picks the right branch.
|
| 938 |
+
prog = [("IF", lambda s: s["a"] != s["b"], [("SWAP", "a", "b")], [],
|
| 939 |
+
lambda s: s["a"] != s["b"])]
|
| 940 |
+
bad = 0
|
| 941 |
+
for a in range(16):
|
| 942 |
+
for b in range(16):
|
| 943 |
+
s = {"a": a, "b": b}
|
| 944 |
+
prog_run(prog, s)
|
| 945 |
+
if sorted([s["a"], s["b"]]) != sorted([a, b]):
|
| 946 |
+
bad += 1
|
| 947 |
+
prog_run(invert(prog), s)
|
| 948 |
+
if s != {"a": a, "b": b}:
|
| 949 |
+
bad += 1
|
| 950 |
+
print(f" reversible conditional (Janus IF with exit assertion): "
|
| 951 |
+
f"{'OK' if bad == 0 else f'FAIL({bad})'}")
|
| 952 |
+
ok &= bad == 0
|
| 953 |
+
return ok
|
| 954 |
+
|
| 955 |
+
|
| 956 |
+
REVERSIBLE_OUT = os.path.join(ROOT, "variants", "neural_reversible.safetensors")
|
| 957 |
+
|
| 958 |
+
# The two reversible primitives, keyed to their gate functions.
|
| 959 |
+
_CODE = {CNOT: 0, TOFF: 1}
|
| 960 |
+
|
| 961 |
+
_FN = {0: CNOT, 1: TOFF}
|
| 962 |
+
|
| 963 |
+
# Heaviside threshold gates that implement the target updates: a CNOT target is
|
| 964 |
+
# XOR(t,c); a Toffoli target is XOR(t, AND(a,b)). AND/OR/NAND are single gates.
|
| 965 |
+
_SUBSTRATE = {
|
| 966 |
+
"and_w": torch.tensor([1, 1]), "and_b": torch.tensor(-2),
|
| 967 |
+
"or_w": torch.tensor([1, 1]), "or_b": torch.tensor(-1),
|
| 968 |
+
"nand_w": torch.tensor([-1, -1]), "nand_b": torch.tensor(1),
|
| 969 |
+
}
|
| 970 |
+
|
| 971 |
+
|
| 972 |
+
def encode(ops):
|
| 973 |
+
codes, args = [], []
|
| 974 |
+
for gate, *a in ops:
|
| 975 |
+
codes.append(_CODE[gate])
|
| 976 |
+
args.append((a + [-1, -1, -1])[:3])
|
| 977 |
+
return torch.tensor(codes, dtype=torch.long), torch.tensor(args, dtype=torch.long)
|
| 978 |
+
|
| 979 |
+
|
| 980 |
+
def main_build_reversible() -> int:
|
| 981 |
+
a_bits = list(range(WIDTH))
|
| 982 |
+
b_bits = list(range(WIDTH, 2 * WIDTH))
|
| 983 |
+
carry = 2 * WIDTH
|
| 984 |
+
n = 2 * WIDTH + 1
|
| 985 |
+
ops = _adder_ops(a_bits, b_bits, carry)
|
| 986 |
+
codes, args = encode(ops)
|
| 987 |
+
|
| 988 |
+
tensors = {"gate_code": codes, "gate_args": args, **_SUBSTRATE}
|
| 989 |
+
import json
|
| 990 |
+
meta = {"machine": "reversible", "width": str(WIDTH), "n_wires": str(n),
|
| 991 |
+
"a_bits": json.dumps(a_bits), "b_bits": json.dumps(b_bits),
|
| 992 |
+
"carry": str(carry), "circuit": "in-place reversible adder b<-a+b"}
|
| 993 |
+
save_file(tensors, REVERSIBLE_OUT, metadata=meta)
|
| 994 |
+
print(f"Built {os.path.relpath(REVERSIBLE_OUT, ROOT)}: reversible {WIDTH}-bit adder")
|
| 995 |
+
print(f" gates={len(ops)} wires={n} size={os.path.getsize(REVERSIBLE_OUT)} bytes")
|
| 996 |
+
|
| 997 |
+
# round-trip: reconstruct the op list from the file and run it
|
| 998 |
+
t = load_file(REVERSIBLE_OUT)
|
| 999 |
+
with safe_open(REVERSIBLE_OUT, framework="pt") as f:
|
| 1000 |
+
m = f.metadata()
|
| 1001 |
+
W = int(m["width"]); ab = json.loads(m["a_bits"]); bb = json.loads(m["b_bits"])
|
| 1002 |
+
cy = int(m["carry"]); nn = int(m["n_wires"])
|
| 1003 |
+
loaded = []
|
| 1004 |
+
for code, a in zip(t["gate_code"].tolist(), t["gate_args"].tolist()):
|
| 1005 |
+
loaded.append((_FN[code], *[x for x in a if x >= 0]))
|
| 1006 |
+
|
| 1007 |
+
def run(reg):
|
| 1008 |
+
for gate, *a in loaded:
|
| 1009 |
+
gate(reg, *a)
|
| 1010 |
+
|
| 1011 |
+
mask = (1 << W) - 1
|
| 1012 |
+
bad = 0
|
| 1013 |
+
rng = random.Random(0)
|
| 1014 |
+
for _ in range(400):
|
| 1015 |
+
a = rng.randint(0, mask); b = rng.randint(0, mask)
|
| 1016 |
+
reg = [0] * nn
|
| 1017 |
+
for k in range(W):
|
| 1018 |
+
reg[ab[k]] = (a >> k) & 1
|
| 1019 |
+
reg[bb[k]] = (b >> k) & 1
|
| 1020 |
+
run(reg)
|
| 1021 |
+
got_b = sum(reg[bb[k]] << k for k in range(W))
|
| 1022 |
+
got_a = sum(reg[ab[k]] << k for k in range(W))
|
| 1023 |
+
if got_b != ((a + b) & mask) or got_a != a or reg[cy] != 0:
|
| 1024 |
+
bad += 1
|
| 1025 |
+
print(f" round-trip b<-a+b, addend & carry restored (400 cases): "
|
| 1026 |
+
f"{'OK' if bad == 0 else f'FAIL({bad})'}")
|
| 1027 |
+
# bijection is exhaustive-checkable at width 4 on the same construction
|
| 1028 |
+
a4 = list(range(4)); b4 = list(range(4, 8)); c4 = 8
|
| 1029 |
+
ops4 = _adder_ops(a4, b4, c4)
|
| 1030 |
+
perm = is_permutation(lambda r: _apply(r, ops4), 9)
|
| 1031 |
+
print(f" loaded circuit is a bijection (4-bit): {'OK' if perm else 'FAIL'}")
|
| 1032 |
+
return 0 if (bad == 0 and perm) else 1
|
| 1033 |
+
|
| 1034 |
+
|
| 1035 |
+
def adder_net(width: int):
|
| 1036 |
+
"""The in-place Cuccaro adder as a feedforward ternary netlist computing its permutation."""
|
| 1037 |
+
a_bits = list(range(width))
|
| 1038 |
+
b_bits = list(range(width, 2 * width))
|
| 1039 |
+
carry = 2 * width
|
| 1040 |
+
n = 2 * width + 1
|
| 1041 |
+
ops = _adder_ops(a_bits, b_bits, carry)
|
| 1042 |
+
net = Net()
|
| 1043 |
+
inputs = [f"in{i}" for i in range(n)]
|
| 1044 |
+
cur = list(inputs)
|
| 1045 |
+
for k, (gate, *args) in enumerate(ops):
|
| 1046 |
+
if gate is CNOT:
|
| 1047 |
+
c, t = args
|
| 1048 |
+
cur[t] = net.XOR(f"c{k}", cur[t], cur[c]) # t ^= c
|
| 1049 |
+
else: # Toffoli: t ^= a&b
|
| 1050 |
+
a, b, t = args
|
| 1051 |
+
tmp = net.AND(f"t{k}a", [cur[a], cur[b]])
|
| 1052 |
+
cur[t] = net.XOR(f"t{k}x", cur[t], tmp)
|
| 1053 |
+
return net, inputs, list(cur), n, a_bits, b_bits, carry
|
| 1054 |
+
|
| 1055 |
+
|
| 1056 |
+
def _refs(n, a_bits, b_bits, carry):
|
| 1057 |
+
out = []
|
| 1058 |
+
for x in range(1 << n):
|
| 1059 |
+
reg = [(x >> i) & 1 for i in range(n)]
|
| 1060 |
+
_apply(reg, _adder_ops(a_bits, b_bits, carry))
|
| 1061 |
+
out.append(sum(reg[i] << i for i in range(n)))
|
| 1062 |
+
return torch.tensor(out)
|
| 1063 |
+
|
| 1064 |
+
|
| 1065 |
+
def _outputs(mm, states, n, **step_kw):
|
| 1066 |
+
v = mm.step(states.clone(), **step_kw)
|
| 1067 |
+
bits = (v >= 0.5).to(torch.int64)
|
| 1068 |
+
weights = torch.tensor([1 << i for i in range(n)])
|
| 1069 |
+
return (bits * weights).sum(dim=1)
|
| 1070 |
+
|
| 1071 |
+
|
| 1072 |
+
def analog_sweep(mm, states, refs, n):
|
| 1073 |
+
"""The permutation must survive analog imperfection."""
|
| 1074 |
+
print(" read-noise sweep (bit-exact = all 512 outputs still match, 4 trials):")
|
| 1075 |
+
for sigma in (0.05, 0.10, 0.15, 0.20):
|
| 1076 |
+
ok = all((_outputs(mm, states, n, analog=True, noise_sigma=sigma,
|
| 1077 |
+
gen=torch.Generator().manual_seed(s)) == refs).all()
|
| 1078 |
+
for s in range(4))
|
| 1079 |
+
print(f" sigma={sigma:.2f}: {'bit-exact' if ok else 'errors appear'}")
|
| 1080 |
+
print(" conductance-mismatch sweep (fixed per-device weight perturbation):")
|
| 1081 |
+
for sg in (0.05, 0.10, 0.15):
|
| 1082 |
+
ok = (_outputs(mm.perturbed(sg, seed=0), states, n, analog=True) == refs).all()
|
| 1083 |
+
print(f" sigma_G={sg:.2f}: {'bit-exact' if ok else 'errors appear'}")
|
| 1084 |
+
|
| 1085 |
+
|
| 1086 |
+
def main_reversible_matrix() -> int:
|
| 1087 |
+
width = 4
|
| 1088 |
+
net, inputs, outputs, n, a_bits, b_bits, carry = adder_net(width)
|
| 1089 |
+
layers, info = compile_net(net, inputs, outputs)
|
| 1090 |
+
mm = MatrixMachine(layers)
|
| 1091 |
+
|
| 1092 |
+
states = torch.stack([torch.tensor([float((x >> i) & 1) for i in range(n)])
|
| 1093 |
+
for x in range(1 << n)])
|
| 1094 |
+
refs = _refs(n, a_bits, b_bits, carry)
|
| 1095 |
+
got = _outputs(mm, states, n)
|
| 1096 |
+
bad = int((got != refs).sum())
|
| 1097 |
+
perm = len(set(got.tolist())) == (1 << n)
|
| 1098 |
+
margin = mm.min_margin(states[:256])
|
| 1099 |
+
|
| 1100 |
+
print(f"reversible {width}-bit adder as a ternary matrix stack")
|
| 1101 |
+
print(f" layers={info['layers']} gates={info['gates']} "
|
| 1102 |
+
f"max_width={info['max_width']} total_weights={info['total_weights']}")
|
| 1103 |
+
print(f" every weight ternary: {'OK' if all(((W == -1) | (W == 0) | (W == 1)).all() for W, _ in layers) else 'FAIL'}")
|
| 1104 |
+
print(f" matches the gate circuit over all {1 << n} inputs: {'OK' if bad == 0 else f'FAIL({bad})'}")
|
| 1105 |
+
print(f" composed transition is a permutation of the state space: {'OK' if perm else 'FAIL'}")
|
| 1106 |
+
print(f" analog noise margin, all layers: {margin:.3f} (guarantee 0.5)")
|
| 1107 |
+
analog_sweep(mm, states, refs, n)
|
| 1108 |
+
ok = bad == 0 and perm and abs(margin - 0.5) < 1e-6
|
| 1109 |
+
print("PASS" if ok else "FAIL")
|
| 1110 |
+
return 0 if ok else 1
|
| 1111 |
+
|
| 1112 |
+
Block = Tuple[int, int, int, int]
|
| 1113 |
+
|
| 1114 |
+
|
| 1115 |
+
def rule(b: Block) -> Block:
|
| 1116 |
+
tl, tr, bl, br = b
|
| 1117 |
+
if b == (1, 0, 0, 1):
|
| 1118 |
+
return (0, 1, 1, 0) # diagonal pair -> other diagonal (deflect)
|
| 1119 |
+
if b == (0, 1, 1, 0):
|
| 1120 |
+
return (1, 0, 0, 1)
|
| 1121 |
+
return (br, bl, tr, tl) # otherwise rotate 180 degrees
|
| 1122 |
+
|
| 1123 |
+
|
| 1124 |
+
def is_bijection() -> bool:
|
| 1125 |
+
outs = {rule(tuple((s >> k) & 1 for k in (3, 2, 1, 0))) for s in range(16)}
|
| 1126 |
+
return len(outs) == 16
|
| 1127 |
+
|
| 1128 |
+
|
| 1129 |
+
def self_inverse() -> bool:
|
| 1130 |
+
return all(rule(rule(tuple((s >> k) & 1 for k in (3, 2, 1, 0))))
|
| 1131 |
+
== tuple((s >> k) & 1 for k in (3, 2, 1, 0)) for s in range(16))
|
| 1132 |
+
|
| 1133 |
+
|
| 1134 |
+
def step(grid: List[List[int]], phase: int) -> List[List[int]]:
|
| 1135 |
+
"""One Margolus update; phase 0 aligns blocks at even coordinates, phase 1 offsets them by (1,1)."""
|
| 1136 |
+
H, W = len(grid), len(grid[0])
|
| 1137 |
+
out = [row[:] for row in grid]
|
| 1138 |
+
o = phase
|
| 1139 |
+
for r0 in range(o, o + H, 2):
|
| 1140 |
+
for c0 in range(o, o + W, 2):
|
| 1141 |
+
r, r1 = r0 % H, (r0 + 1) % H
|
| 1142 |
+
c, c1 = c0 % W, (c0 + 1) % W
|
| 1143 |
+
nb = rule((grid[r][c], grid[r][c1], grid[r1][c], grid[r1][c1]))
|
| 1144 |
+
out[r][c], out[r][c1], out[r1][c], out[r1][c1] = nb
|
| 1145 |
+
return out
|
| 1146 |
+
|
| 1147 |
+
|
| 1148 |
+
def ca_run(grid: List[List[int]], nsteps: int, start_phase: int = 0) -> List[List[int]]:
|
| 1149 |
+
g = grid
|
| 1150 |
+
for n in range(nsteps):
|
| 1151 |
+
g = step(g, (start_phase + n) & 1)
|
| 1152 |
+
return g
|
| 1153 |
+
|
| 1154 |
+
|
| 1155 |
+
def run_back(grid: List[List[int]], nsteps: int, start_phase: int = 0) -> List[List[int]]:
|
| 1156 |
+
"""Undo `run`: replay the phase sequence in reverse; the rule is self-inverse."""
|
| 1157 |
+
phases = [(start_phase + n) & 1 for n in range(nsteps)]
|
| 1158 |
+
g = grid
|
| 1159 |
+
for p in reversed(phases):
|
| 1160 |
+
g = step(g, p)
|
| 1161 |
+
return g
|
| 1162 |
+
|
| 1163 |
+
|
| 1164 |
+
# --- the block rule as Heaviside threshold gates ---
|
| 1165 |
+
# rule(s) = rotate180(s) XOR is_diag(s) on every cell: rotation fixes diagonal
|
| 1166 |
+
# pairs, and flipping all four cells of a rotated diagonal pair sends it to the
|
| 1167 |
+
# other diagonal. is_diag detects the two diagonal-pair states.
|
| 1168 |
+
def _H(x):
|
| 1169 |
+
return 1 if x >= 0 else 0
|
| 1170 |
+
|
| 1171 |
+
|
| 1172 |
+
def _and(*xs):
|
| 1173 |
+
return _H(sum(xs) - len(xs))
|
| 1174 |
+
|
| 1175 |
+
|
| 1176 |
+
def _or(*xs):
|
| 1177 |
+
return _H(sum(xs) - 1)
|
| 1178 |
+
|
| 1179 |
+
|
| 1180 |
+
def _xor(a, b):
|
| 1181 |
+
return _and(_or(a, b), _H(1 - a - b)) # OR AND NAND, the family's XOR
|
| 1182 |
+
|
| 1183 |
+
|
| 1184 |
+
def gate_rule(b: Block) -> Block:
|
| 1185 |
+
tl, tr, bl, br = b
|
| 1186 |
+
d = _or(_and(tl, 1 - tr, 1 - bl, br), _and(1 - tl, tr, bl, 1 - br)) # is_diag
|
| 1187 |
+
return (_xor(br, d), _xor(bl, d), _xor(tr, d), _xor(tl, d))
|
| 1188 |
+
|
| 1189 |
+
|
| 1190 |
+
def _test_gates():
|
| 1191 |
+
ok = all(gate_rule(tuple((s >> k) & 1 for k in (3, 2, 1, 0)))
|
| 1192 |
+
== rule(tuple((s >> k) & 1 for k in (3, 2, 1, 0))) for s in range(16))
|
| 1193 |
+
print(f" block rule as Heaviside threshold gates matches over 16 states: "
|
| 1194 |
+
f"{'OK' if ok else 'FAIL'}")
|
| 1195 |
+
return ok
|
| 1196 |
+
|
| 1197 |
+
|
| 1198 |
+
# --- tests ---
|
| 1199 |
+
def _rand_grid(H, W, seed):
|
| 1200 |
+
import random
|
| 1201 |
+
rng = random.Random(seed)
|
| 1202 |
+
return [[rng.randint(0, 1) for _ in range(W)] for _ in range(H)]
|
| 1203 |
+
|
| 1204 |
+
|
| 1205 |
+
def _ball_positions(g):
|
| 1206 |
+
return {(r, c) for r, row in enumerate(g) for c, v in enumerate(row) if v}
|
| 1207 |
+
|
| 1208 |
+
|
| 1209 |
+
def _test_rule():
|
| 1210 |
+
print(f" block rule is a bijection of 16 states: {'OK' if is_bijection() else 'FAIL'}")
|
| 1211 |
+
print(f" block rule is self-inverse: {'OK' if self_inverse() else 'FAIL'}")
|
| 1212 |
+
return is_bijection() and self_inverse()
|
| 1213 |
+
|
| 1214 |
+
|
| 1215 |
+
def _test_reversibility():
|
| 1216 |
+
bad = 0
|
| 1217 |
+
for seed in range(20):
|
| 1218 |
+
g = _rand_grid(8, 8, seed)
|
| 1219 |
+
fwd = ca_run(g, 25, start_phase=0)
|
| 1220 |
+
back = run_back(fwd, 25, start_phase=0)
|
| 1221 |
+
if back != g:
|
| 1222 |
+
bad += 1
|
| 1223 |
+
# particle count is conserved (the rule permutes cells within each block)
|
| 1224 |
+
g = _rand_grid(8, 8, 99)
|
| 1225 |
+
conserved = sum(sum(r) for r in g) == sum(sum(r) for r in ca_run(g, 40))
|
| 1226 |
+
print(f" lattice reversible (run then reverse recovers grid, 20 grids): "
|
| 1227 |
+
f"{'OK' if bad == 0 else f'FAIL({bad})'}")
|
| 1228 |
+
print(f" particle number conserved: {'OK' if conserved else 'FAIL'}")
|
| 1229 |
+
return bad == 0 and conserved
|
| 1230 |
+
|
| 1231 |
+
|
| 1232 |
+
def _test_ballistic():
|
| 1233 |
+
# a single particle travels in a straight diagonal line
|
| 1234 |
+
H = W = 16
|
| 1235 |
+
g = [[0] * W for _ in range(H)]
|
| 1236 |
+
g[2][2] = 1
|
| 1237 |
+
positions = [next(iter(_ball_positions(g)))]
|
| 1238 |
+
gg = g
|
| 1239 |
+
for n in range(8):
|
| 1240 |
+
gg = step(gg, n & 1)
|
| 1241 |
+
p = _ball_positions(gg)
|
| 1242 |
+
positions.append(next(iter(p)) if len(p) == 1 else None)
|
| 1243 |
+
ok = all(p is not None for p in positions)
|
| 1244 |
+
steady = ok and all(positions[i + 1] == (positions[i][0] + 1, positions[i][1] + 1)
|
| 1245 |
+
for i in range(len(positions) - 1))
|
| 1246 |
+
print(f" single particle stays a single particle: {'OK' if ok else 'FAIL'}")
|
| 1247 |
+
print(f" and moves ballistically on the diagonal, +(1,1) per step: "
|
| 1248 |
+
f"{'OK' if steady else 'FAIL'} trace={positions[:5]}")
|
| 1249 |
+
return ok and steady
|
| 1250 |
+
|
| 1251 |
+
|
| 1252 |
+
def interaction_gate(a: int, b: int) -> dict:
|
| 1253 |
+
"""One billiard-ball collision as a reversible interaction gate."""
|
| 1254 |
+
H = W = 12
|
| 1255 |
+
g = [[0] * W for _ in range(H)]
|
| 1256 |
+
if a:
|
| 1257 |
+
g[2][2] = 1
|
| 1258 |
+
if b:
|
| 1259 |
+
g[7][7] = 1
|
| 1260 |
+
g = ca_run(g, 4)
|
| 1261 |
+
return {"A_and_B": g[3][6], "A_and_notB": g[6][6], "notA_and_B": g[3][3]}
|
| 1262 |
+
|
| 1263 |
+
|
| 1264 |
+
def and_gate(a: int, b: int) -> int:
|
| 1265 |
+
return interaction_gate(a, b)["A_and_B"]
|
| 1266 |
+
|
| 1267 |
+
|
| 1268 |
+
def _test_gate():
|
| 1269 |
+
ok = True
|
| 1270 |
+
for a in (0, 1):
|
| 1271 |
+
for b in (0, 1):
|
| 1272 |
+
o = interaction_gate(a, b)
|
| 1273 |
+
ok &= (o["A_and_B"] == (a & b) and o["A_and_notB"] == (a & (1 - b))
|
| 1274 |
+
and o["notA_and_B"] == ((1 - a) & b))
|
| 1275 |
+
print(f" billiard-ball interaction gate (A&B, A&~B, ~A&B) over all 4 inputs: "
|
| 1276 |
+
f"{'OK' if ok else 'FAIL'}")
|
| 1277 |
+
return ok
|
| 1278 |
+
|
| 1279 |
+
|
| 1280 |
+
def and3(a: int, b: int, c: int) -> int:
|
| 1281 |
+
"""Two composed collisions."""
|
| 1282 |
+
H = W = 16
|
| 1283 |
+
g = [[0] * W for _ in range(H)]
|
| 1284 |
+
if a:
|
| 1285 |
+
g[2][2] = 1
|
| 1286 |
+
if b:
|
| 1287 |
+
g[7][7] = 1
|
| 1288 |
+
if c:
|
| 1289 |
+
g[0][9] = 1
|
| 1290 |
+
return ca_run(g, 4)[3][5]
|
| 1291 |
+
|
| 1292 |
+
|
| 1293 |
+
def _test_compose():
|
| 1294 |
+
ok = all(and3(a, b, c) == (a & b & c)
|
| 1295 |
+
for a in (0, 1) for b in (0, 1) for c in (0, 1))
|
| 1296 |
+
print(f" composed 3-input AND (two chained collisions) over all 8 inputs: "
|
| 1297 |
+
f"{'OK' if ok else 'FAIL'}")
|
| 1298 |
+
return ok
|
| 1299 |
+
|
| 1300 |
+
|
| 1301 |
+
def _test_collision():
|
| 1302 |
+
# Two particles interact (the joint evolution differs from independent
|
| 1303 |
+
# motion) and the collision stays reversible: the physics that logic needs.
|
| 1304 |
+
H = W = 12
|
| 1305 |
+
interacted = False
|
| 1306 |
+
revok = True
|
| 1307 |
+
# converging pairs: an SE-mover (even,even) meets an NW-mover (odd,odd) on a
|
| 1308 |
+
# shared diagonal, forming the 1001/0110 diagonal pair the rule deflects.
|
| 1309 |
+
for a, b in [((2, 2), (7, 7)), ((3, 3), (8, 8)), ((2, 8), (7, 3)),
|
| 1310 |
+
((4, 4), (9, 9)), ((2, 2), (9, 9))]:
|
| 1311 |
+
g = [[0] * W for _ in range(H)]
|
| 1312 |
+
g[a[0]][a[1]] = 1
|
| 1313 |
+
g[b[0]][b[1]] = 1
|
| 1314 |
+
ga = [[0] * W for _ in range(H)]
|
| 1315 |
+
ga[a[0]][a[1]] = 1
|
| 1316 |
+
gb = [[0] * W for _ in range(H)]
|
| 1317 |
+
gb[b[0]][b[1]] = 1
|
| 1318 |
+
joint = g
|
| 1319 |
+
for n in range(12):
|
| 1320 |
+
joint = step(joint, n & 1)
|
| 1321 |
+
ga = step(ga, n & 1)
|
| 1322 |
+
gb = step(gb, n & 1)
|
| 1323 |
+
free = _ball_positions(ga) | _ball_positions(gb)
|
| 1324 |
+
if _ball_positions(joint) != free:
|
| 1325 |
+
interacted = True
|
| 1326 |
+
if run_back(ca_run(g, 12), 12) != g:
|
| 1327 |
+
revok = False
|
| 1328 |
+
print(f" two-particle collisions interact (joint != independent motion): "
|
| 1329 |
+
f"{'OK' if interacted else 'FAIL'}")
|
| 1330 |
+
print(f" collisions remain reversible: {'OK' if revok else 'FAIL'}")
|
| 1331 |
+
return interacted and revok
|
| 1332 |
+
|
| 1333 |
+
CA_OUT = os.path.join(ROOT, "variants", "neural_ca.safetensors")
|
| 1334 |
+
|
| 1335 |
+
|
| 1336 |
+
def block_net():
|
| 1337 |
+
net = Net()
|
| 1338 |
+
ntl, ntr = net.NOT("ntl", "tl"), net.NOT("ntr", "tr")
|
| 1339 |
+
nbl, nbr = net.NOT("nbl", "bl"), net.NOT("nbr", "br")
|
| 1340 |
+
d1 = net.AND("d1", ["tl", ntr, nbl, "br"])
|
| 1341 |
+
d2 = net.AND("d2", [ntl, "tr", "bl", nbr])
|
| 1342 |
+
dg = net.OR("dg", [d1, d2]) # is_diag
|
| 1343 |
+
TL = net.XOR("oTL", "br", dg)
|
| 1344 |
+
TR = net.XOR("oTR", "bl", dg)
|
| 1345 |
+
BL = net.XOR("oBL", "tr", dg)
|
| 1346 |
+
BR = net.XOR("oBR", "tl", dg)
|
| 1347 |
+
return net, ["tl", "tr", "bl", "br"], [TL, TR, BL, BR]
|
| 1348 |
+
|
| 1349 |
+
|
| 1350 |
+
def matrix_step(mm, grid, phase):
|
| 1351 |
+
"""One Margolus update driven entirely by the matrix tile."""
|
| 1352 |
+
H, W = len(grid), len(grid[0])
|
| 1353 |
+
out = [row[:] for row in grid]
|
| 1354 |
+
for r0 in range(phase, phase + H, 2):
|
| 1355 |
+
for c0 in range(phase, phase + W, 2):
|
| 1356 |
+
r, r1, c, c1 = r0 % H, (r0 + 1) % H, c0 % W, (c0 + 1) % W
|
| 1357 |
+
v = torch.tensor([[float(grid[r][c]), float(grid[r][c1]),
|
| 1358 |
+
float(grid[r1][c]), float(grid[r1][c1])]])
|
| 1359 |
+
o = mm.step(v)[0]
|
| 1360 |
+
out[r][c], out[r][c1], out[r1][c], out[r1][c1] = (int(o[0]), int(o[1]),
|
| 1361 |
+
int(o[2]), int(o[3]))
|
| 1362 |
+
return out
|
| 1363 |
+
|
| 1364 |
+
|
| 1365 |
+
def main_build_ca() -> int:
|
| 1366 |
+
net, inp, outp = block_net()
|
| 1367 |
+
layers, info = compile_net(net, inp, outp)
|
| 1368 |
+
mm = MatrixMachine(layers)
|
| 1369 |
+
|
| 1370 |
+
seen, bad, vecs = set(), 0, []
|
| 1371 |
+
for s in range(16):
|
| 1372 |
+
b = tuple((s >> k) & 1 for k in (3, 2, 1, 0)) # tl,tr,bl,br
|
| 1373 |
+
v = torch.tensor([[float(x) for x in b]])
|
| 1374 |
+
got = tuple(int(x) for x in mm.step(v)[0])
|
| 1375 |
+
if got != rule(b):
|
| 1376 |
+
bad += 1
|
| 1377 |
+
seen.add(got)
|
| 1378 |
+
vecs.append(v[0])
|
| 1379 |
+
perm = len(seen) == 16
|
| 1380 |
+
margin = mm.min_margin(torch.stack(vecs))
|
| 1381 |
+
|
| 1382 |
+
tensors = {}
|
| 1383 |
+
for k, (W, B) in enumerate(layers):
|
| 1384 |
+
tensors[f"matrix.layer{k:03d}.weight"] = W.to(torch.int8)
|
| 1385 |
+
tensors[f"matrix.layer{k:03d}.bias"] = B.to(torch.int8)
|
| 1386 |
+
meta = {"machine": "ca",
|
| 1387 |
+
"rule": "Margolus reversible: rotate 180 except diagonal pair swaps (BBM class)",
|
| 1388 |
+
"inputs": "tl,tr,bl,br", "outputs": "TL,TR,BL,BR", "layers": str(info["layers"])}
|
| 1389 |
+
save_file(tensors, CA_OUT, metadata=meta)
|
| 1390 |
+
print(f"Built {os.path.relpath(CA_OUT, ROOT)}: reversible CA block rule as a ternary matrix tile")
|
| 1391 |
+
print(f" layers={info['layers']} gates={info['gates']} size={os.path.getsize(CA_OUT)} bytes")
|
| 1392 |
+
print(f" every weight ternary: "
|
| 1393 |
+
f"{'OK' if all(((W == -1) | (W == 0) | (W == 1)).all() for W, _ in layers) else 'FAIL'}")
|
| 1394 |
+
print(f" tile matches the block rule over all 16 states: {'OK' if bad == 0 else f'FAIL({bad})'}")
|
| 1395 |
+
print(f" tile is a permutation (16 distinct outputs): {'OK' if perm else 'FAIL'}")
|
| 1396 |
+
print(f" analog noise margin: {margin:.3f} (guarantee 0.5)")
|
| 1397 |
+
|
| 1398 |
+
# analog robustness: the tile must reproduce the rule under read noise and
|
| 1399 |
+
# static conductance mismatch, as neural_matrix8 measures
|
| 1400 |
+
states = torch.stack(vecs)
|
| 1401 |
+
ref = [rule(tuple((s >> k) & 1 for k in (3, 2, 1, 0))) for s in range(16)]
|
| 1402 |
+
|
| 1403 |
+
def outs(machine, **kw):
|
| 1404 |
+
v = machine.step(states, **kw)
|
| 1405 |
+
return [tuple(int(v[i][j]) for j in range(4)) for i in range(v.shape[0])]
|
| 1406 |
+
|
| 1407 |
+
print(" read-noise sweep (exact = tile matches the rule on all 16 states):")
|
| 1408 |
+
for sigma in (0.05, 0.10, 0.15, 0.20):
|
| 1409 |
+
okn = all(outs(mm, analog=True, noise_sigma=sigma,
|
| 1410 |
+
gen=torch.Generator().manual_seed(s)) == ref for s in range(4))
|
| 1411 |
+
print(f" sigma={sigma:.2f}: {'exact' if okn else 'errors appear'}")
|
| 1412 |
+
print(" conductance-mismatch sweep:")
|
| 1413 |
+
for sg in (0.05, 0.10, 0.15):
|
| 1414 |
+
okg = outs(mm.perturbed(sg, seed=0), analog=True) == ref
|
| 1415 |
+
print(f" sigma_G={sg:.2f}: {'exact' if okg else 'errors appear'}")
|
| 1416 |
+
|
| 1417 |
+
# the loaded tile, applied to every block, is one whole-lattice CA step
|
| 1418 |
+
t = load_file(CA_OUT)
|
| 1419 |
+
n = 0
|
| 1420 |
+
lyr = []
|
| 1421 |
+
while f"matrix.layer{n:03d}.weight" in t:
|
| 1422 |
+
lyr.append((t[f"matrix.layer{n:03d}.weight"], t[f"matrix.layer{n:03d}.bias"]))
|
| 1423 |
+
n += 1
|
| 1424 |
+
mm2 = MatrixMachine(lyr)
|
| 1425 |
+
g = _rand_grid(8, 8, 3)
|
| 1426 |
+
gmat = matrix_step(mm2, g, 0)
|
| 1427 |
+
gref = step(g, 0)
|
| 1428 |
+
print(f" loaded tile drives a full lattice step (matches ca.step): "
|
| 1429 |
+
f"{'OK' if gmat == gref else 'FAIL'}")
|
| 1430 |
+
|
| 1431 |
+
ok = bad == 0 and perm and abs(margin - 0.5) < 1e-6 and gmat == gref
|
| 1432 |
+
print("PASS" if ok else "FAIL")
|
| 1433 |
+
return 0 if ok else 1
|
| 1434 |
+
|
| 1435 |
+
# side -> (dx, dy, my_side, neighbor_side)
|
| 1436 |
+
_SIDES = [(0, 1, "N", "S"), (1, 0, "E", "W"), (0, -1, "S", "N"), (-1, 0, "W", "E")]
|
| 1437 |
+
|
| 1438 |
+
|
| 1439 |
+
class Tile:
|
| 1440 |
+
__slots__ = ("N", "E", "S", "W", "name")
|
| 1441 |
+
|
| 1442 |
+
def __init__(self, N="", E="", S="", W="", name=""):
|
| 1443 |
+
self.N, self.E, self.S, self.W, self.name = N, E, S, W, name
|
| 1444 |
+
|
| 1445 |
+
def glue(self, side):
|
| 1446 |
+
return getattr(self, side)
|
| 1447 |
+
|
| 1448 |
+
|
| 1449 |
+
def bind_strength(A: Dict[Tuple[int, int], Tile], x: int, y: int, t: Tile,
|
| 1450 |
+
strength: Dict[str, int]) -> int:
|
| 1451 |
+
"""Summed strength of t's glues that match the abutting neighbor glues."""
|
| 1452 |
+
s = 0
|
| 1453 |
+
for dx, dy, side, opp in _SIDES:
|
| 1454 |
+
nb = A.get((x + dx, y + dy))
|
| 1455 |
+
if nb is None:
|
| 1456 |
+
continue
|
| 1457 |
+
g = t.glue(side)
|
| 1458 |
+
if g and g == nb.glue(opp):
|
| 1459 |
+
s += strength.get(g, 1)
|
| 1460 |
+
return s
|
| 1461 |
+
|
| 1462 |
+
|
| 1463 |
+
def binds(A, x, y, t, tau, strength) -> bool:
|
| 1464 |
+
"""The threshold-gate binding decision: H(sum strength*match - tau)."""
|
| 1465 |
+
return bind_strength(A, x, y, t, strength) >= tau
|
| 1466 |
+
|
| 1467 |
+
|
| 1468 |
+
def grow(tileset: List[Tile], seed: Dict[Tuple[int, int], Tile], tau: int,
|
| 1469 |
+
strength: Dict[str, int], bounds: Tuple[int, int, int, int],
|
| 1470 |
+
max_tiles: int = 100000) -> Tuple[Dict[Tuple[int, int], Tile], bool]:
|
| 1471 |
+
"""Directed growth from a seed."""
|
| 1472 |
+
x0, y0, x1, y1 = bounds
|
| 1473 |
+
A = dict(seed)
|
| 1474 |
+
deterministic = True
|
| 1475 |
+
changed = True
|
| 1476 |
+
while changed and len(A) < max_tiles:
|
| 1477 |
+
changed = False
|
| 1478 |
+
frontier = set()
|
| 1479 |
+
for (x, y) in list(A):
|
| 1480 |
+
for dx, dy, _, _ in _SIDES:
|
| 1481 |
+
p = (x + dx, y + dy)
|
| 1482 |
+
if p not in A and x0 <= p[0] <= x1 and y0 <= p[1] <= y1:
|
| 1483 |
+
frontier.add(p)
|
| 1484 |
+
for (x, y) in frontier:
|
| 1485 |
+
binders = [t for t in tileset if binds(A, x, y, t, tau, strength)]
|
| 1486 |
+
if len(binders) == 1:
|
| 1487 |
+
A[(x, y)] = binders[0]
|
| 1488 |
+
changed = True
|
| 1489 |
+
elif len(binders) > 1:
|
| 1490 |
+
deterministic = False
|
| 1491 |
+
return A, deterministic
|
| 1492 |
+
|
| 1493 |
+
|
| 1494 |
+
# ---------------------------------------------------------------------------
|
| 1495 |
+
# XOR / Sierpinski tile set: value(x,y) = value(x-1,y) XOR value(x,y-1)
|
| 1496 |
+
# ---------------------------------------------------------------------------
|
| 1497 |
+
def rule2_tileset(fn) -> List[Tile]:
|
| 1498 |
+
"""Rule tiles for value(x,y) = fn(W, S): four cooperatively binding tiles emitting fn north and east."""
|
| 1499 |
+
ts = []
|
| 1500 |
+
for s in (0, 1):
|
| 1501 |
+
for w in (0, 1):
|
| 1502 |
+
v = fn(w, s)
|
| 1503 |
+
ts.append(Tile(N=f"v{v}", E=f"v{v}", S=f"v{s}", W=f"v{w}",
|
| 1504 |
+
name=f"R w{w} s{s} -> {v}"))
|
| 1505 |
+
return ts
|
| 1506 |
+
|
| 1507 |
+
|
| 1508 |
+
def sierpinski_tileset() -> List[Tile]:
|
| 1509 |
+
return rule2_tileset(lambda w, s: w ^ s)
|
| 1510 |
+
|
| 1511 |
+
|
| 1512 |
+
def _row_col_seed(bottom: List[int], left: List[int]):
|
| 1513 |
+
"""Seed the bottom row and the left column with fixed value tiles."""
|
| 1514 |
+
seed = {}
|
| 1515 |
+
for x, b in enumerate(bottom):
|
| 1516 |
+
seed[(x, 0)] = Tile(N=f"v{b}", E="", S="", W="", name=f"seedB{x}={b}")
|
| 1517 |
+
for y, l in enumerate(left):
|
| 1518 |
+
if y == 0:
|
| 1519 |
+
continue
|
| 1520 |
+
seed[(0, y)] = Tile(N="", E=f"v{l}", S="", W="", name=f"seedL{y}={l}")
|
| 1521 |
+
return seed
|
| 1522 |
+
|
| 1523 |
+
|
| 1524 |
+
def _test_binding_gate():
|
| 1525 |
+
"""The binding decision is exactly the Heaviside threshold gate."""
|
| 1526 |
+
strength = {"v0": 1, "v1": 1}
|
| 1527 |
+
ts = sierpinski_tileset()
|
| 1528 |
+
A = {(1, 0): Tile(N="v1"), (0, 1): Tile(E="v0")}
|
| 1529 |
+
bad = 0
|
| 1530 |
+
for t in ts:
|
| 1531 |
+
for x, y in [(1, 1)]:
|
| 1532 |
+
w = sum(strength.get(t.glue(side), 1)
|
| 1533 |
+
for dx, dy, side, opp in _SIDES
|
| 1534 |
+
if A.get((x + dx, y + dy)) and t.glue(side)
|
| 1535 |
+
and t.glue(side) == A[(x + dx, y + dy)].glue(opp))
|
| 1536 |
+
gate = 1 if (w - 2) >= 0 else 0 # H(sum*match - tau)
|
| 1537 |
+
if gate != int(binds(A, x, y, t, 2, strength)):
|
| 1538 |
+
bad += 1
|
| 1539 |
+
print(f" binding decision == Heaviside gate H(sum-tau): {'OK' if bad == 0 else 'FAIL'}")
|
| 1540 |
+
return bad == 0
|
| 1541 |
+
|
| 1542 |
+
|
| 1543 |
+
def _test_rule2(fn, name, n=24):
|
| 1544 |
+
strength = {"v0": 1, "v1": 1}
|
| 1545 |
+
bottom = [1 if x == 0 else 0 for x in range(n)]
|
| 1546 |
+
left = [1 if y == 0 else 0 for y in range(n)]
|
| 1547 |
+
seed = _row_col_seed(bottom, left)
|
| 1548 |
+
A, det = grow(rule2_tileset(fn), seed, 2, strength, (0, 0, n - 1, n - 1))
|
| 1549 |
+
|
| 1550 |
+
def val(x, y):
|
| 1551 |
+
t = A.get((x, y))
|
| 1552 |
+
return None if t is None else (1 if t.N == "v1" else 0)
|
| 1553 |
+
|
| 1554 |
+
ref = {(x, 0): bottom[x] for x in range(n)}
|
| 1555 |
+
ref.update({(0, y): left[y] for y in range(n)})
|
| 1556 |
+
for y in range(1, n):
|
| 1557 |
+
for x in range(1, n):
|
| 1558 |
+
ref[(x, y)] = fn(ref[(x - 1, y)], ref[(x, y - 1)])
|
| 1559 |
+
|
| 1560 |
+
filled = bad = 0
|
| 1561 |
+
for y in range(1, n):
|
| 1562 |
+
for x in range(1, n):
|
| 1563 |
+
v = val(x, y)
|
| 1564 |
+
if v is not None:
|
| 1565 |
+
filled += 1
|
| 1566 |
+
bad += v != ref[(x, y)]
|
| 1567 |
+
tag = "OK" if (det and bad == 0 and filled > 0) else "FAIL"
|
| 1568 |
+
print(f" rule-tile CA fn={name:3s}: directed={det} placed={filled} "
|
| 1569 |
+
f"every tile = fn(W,S) {tag}")
|
| 1570 |
+
return det and bad == 0 and filled > 0
|
| 1571 |
+
|
| 1572 |
+
|
| 1573 |
+
# ---------------------------------------------------------------------------
|
| 1574 |
+
# Binary counter: each row is the row below plus one. LSB is the right column;
|
| 1575 |
+
# carry propagates west by cooperative binding (S = bit below, E = carry in).
|
| 1576 |
+
# ---------------------------------------------------------------------------
|
| 1577 |
+
def counter_tileset() -> List[Tile]:
|
| 1578 |
+
ts = []
|
| 1579 |
+
for b in (0, 1):
|
| 1580 |
+
for c in (0, 1):
|
| 1581 |
+
ts.append(Tile(N=f"b{b ^ c}", E=f"c{c}", S=f"b{b}", W=f"c{b & c}",
|
| 1582 |
+
name=f"C b{b} c{c} -> b{b ^ c} carry{b & c}"))
|
| 1583 |
+
ts.append(Tile(N="edge", E="", S="edge", W="c1", name="edge(+1 injector)"))
|
| 1584 |
+
return ts
|
| 1585 |
+
|
| 1586 |
+
|
| 1587 |
+
def counter_seed(n: int):
|
| 1588 |
+
"""Bottom row (y=0) all zero, plus the right-edge +1 injector column base."""
|
| 1589 |
+
seed = {}
|
| 1590 |
+
for x in range(n):
|
| 1591 |
+
seed[(x, 0)] = Tile(N="b0", name=f"seed b0 col{x}")
|
| 1592 |
+
seed[(n, 0)] = Tile(N="edge", W="c1", name="seed edge")
|
| 1593 |
+
return seed
|
| 1594 |
+
|
| 1595 |
+
|
| 1596 |
+
def _test_counter(n=6, rows=None):
|
| 1597 |
+
rows = rows or (1 << n) - 1
|
| 1598 |
+
strength = {"edge": 2} # value/carry glues default 1
|
| 1599 |
+
A, det = grow(counter_tileset(), counter_seed(n), 2, strength,
|
| 1600 |
+
(0, 0, n, rows))
|
| 1601 |
+
|
| 1602 |
+
def rowval(y):
|
| 1603 |
+
bits = []
|
| 1604 |
+
for x in range(n):
|
| 1605 |
+
t = A.get((x, y))
|
| 1606 |
+
if t is None:
|
| 1607 |
+
return None
|
| 1608 |
+
bits.append(1 if t.N == "b1" else 0)
|
| 1609 |
+
return sum(bit << (n - 1 - x) for x, bit in enumerate(bits))
|
| 1610 |
+
|
| 1611 |
+
bad = filled = 0
|
| 1612 |
+
for y in range(1, rows + 1):
|
| 1613 |
+
v = rowval(y)
|
| 1614 |
+
if v is not None:
|
| 1615 |
+
filled += 1
|
| 1616 |
+
if v != (y & ((1 << n) - 1)):
|
| 1617 |
+
bad += 1
|
| 1618 |
+
print(f" binary counter {n}-bit: directed={det} rows grown={filled} "
|
| 1619 |
+
f"row y encodes the integer y {'OK' if bad == 0 else f'FAIL({bad})'}")
|
| 1620 |
+
return det and bad == 0 and filled == rows
|
| 1621 |
+
|
| 1622 |
+
|
| 1623 |
+
TILE_OUT = os.path.join(ROOT, "variants", "neural_tile.safetensors")
|
| 1624 |
+
|
| 1625 |
+
NBITS = 8
|
| 1626 |
+
|
| 1627 |
+
TAU = 2
|
| 1628 |
+
|
| 1629 |
+
|
| 1630 |
+
def main_build_tile() -> int:
|
| 1631 |
+
ts = counter_tileset()
|
| 1632 |
+
strength = {"edge": 2}
|
| 1633 |
+
glues = sorted({g for t in ts for g in (t.N, t.E, t.S, t.W) if g})
|
| 1634 |
+
gid = {g: i for i, g in enumerate(glues)}
|
| 1635 |
+
tile_glues = torch.tensor([[gid.get(t.N, -1), gid.get(t.E, -1),
|
| 1636 |
+
gid.get(t.S, -1), gid.get(t.W, -1)] for t in ts],
|
| 1637 |
+
dtype=torch.long)
|
| 1638 |
+
glue_strength = torch.tensor([strength.get(g, 1) for g in glues], dtype=torch.long)
|
| 1639 |
+
# per-tile binding-gate weights = strengths of the tile's four glues (0 = null)
|
| 1640 |
+
bind_w = torch.tensor([[strength.get(g, 1) if g else 0 for g in (t.N, t.E, t.S, t.W)]
|
| 1641 |
+
for t in ts], dtype=torch.long)
|
| 1642 |
+
tensors = {"tile_glues": tile_glues, "glue_strength": glue_strength,
|
| 1643 |
+
"binding_weight": bind_w, "binding_bias": torch.tensor(-TAU)}
|
| 1644 |
+
meta = {"machine": "tile", "tau": str(TAU), "glues": json.dumps(glues),
|
| 1645 |
+
"tile_names": json.dumps([t.name for t in ts]), "program": "binary counter"}
|
| 1646 |
+
save_file(tensors, TILE_OUT, metadata=meta)
|
| 1647 |
+
print(f"Built {os.path.relpath(TILE_OUT, ROOT)}: binary-counter tile set")
|
| 1648 |
+
print(f" tiles={len(ts)} glues={len(glues)} tau={TAU} size={os.path.getsize(TILE_OUT)} bytes")
|
| 1649 |
+
|
| 1650 |
+
# round-trip: reconstruct the tiles from the file and regrow the counter
|
| 1651 |
+
t = load_file(TILE_OUT)
|
| 1652 |
+
with safe_open(TILE_OUT, framework="pt") as f:
|
| 1653 |
+
m = f.metadata()
|
| 1654 |
+
gl = json.loads(m["glues"])
|
| 1655 |
+
strg = {gl[i]: int(s) for i, s in enumerate(t["glue_strength"].tolist())}
|
| 1656 |
+
tiles = []
|
| 1657 |
+
for row, name in zip(t["tile_glues"].tolist(), json.loads(m["tile_names"])):
|
| 1658 |
+
sides = [gl[i] if i >= 0 else "" for i in row]
|
| 1659 |
+
tiles.append(Tile(N=sides[0], E=sides[1], S=sides[2], W=sides[3], name=name))
|
| 1660 |
+
|
| 1661 |
+
rows = (1 << NBITS) - 1
|
| 1662 |
+
A, det = grow(tiles, counter_seed(NBITS), int(m["tau"]), strg,
|
| 1663 |
+
(0, 0, NBITS, rows))
|
| 1664 |
+
bad = filled = 0
|
| 1665 |
+
for y in range(1, rows + 1):
|
| 1666 |
+
cells = [A.get((x, y)) for x in range(NBITS)]
|
| 1667 |
+
if any(c is None for c in cells):
|
| 1668 |
+
continue
|
| 1669 |
+
filled += 1
|
| 1670 |
+
v = sum((1 if c.N == "b1" else 0) << (NBITS - 1 - x) for x, c in enumerate(cells))
|
| 1671 |
+
if v != (y & ((1 << NBITS) - 1)):
|
| 1672 |
+
bad += 1
|
| 1673 |
+
print(f" round-trip regrow {NBITS}-bit counter: directed={det} rows={filled} "
|
| 1674 |
+
f"row y == y {'OK' if bad == 0 else f'FAIL({bad})'}")
|
| 1675 |
+
|
| 1676 |
+
# the stored binding gate reproduces the model's binding decision
|
| 1677 |
+
gate_ok = True
|
| 1678 |
+
Atest = {(1, 0): Tile(N="b0"), (2, 0): Tile(N="b0")}
|
| 1679 |
+
for ti, tt in enumerate(tiles):
|
| 1680 |
+
for site in [(1, 1), (2, 1)]:
|
| 1681 |
+
w = t["binding_weight"][ti].tolist()
|
| 1682 |
+
match = [1 if tt.glue(side) and Atest.get((site[0] + dx, site[1] + dy))
|
| 1683 |
+
and tt.glue(side) == Atest[(site[0] + dx, site[1] + dy)].glue(opp) else 0
|
| 1684 |
+
for dx, dy, side, opp in _SIDES]
|
| 1685 |
+
gate = 1 if sum(wi * mi for wi, mi in zip(w, match)) + int(t["binding_bias"]) >= 0 else 0
|
| 1686 |
+
if gate != int(binds(Atest, site[0], site[1], tt, TAU, strg)):
|
| 1687 |
+
gate_ok = False
|
| 1688 |
+
print(f" stored binding gate H(weight.match - tau) matches growth rule: "
|
| 1689 |
+
f"{'OK' if gate_ok else 'FAIL'}")
|
| 1690 |
+
return 0 if (bad == 0 and det and gate_ok) else 1
|
| 1691 |
+
|
| 1692 |
+
|
| 1693 |
+
def cmd_attractor():
|
| 1694 |
+
print("Attractor computer\n" + "=" * 40)
|
| 1695 |
+
print("Forward evaluation (exact, energy 0):")
|
| 1696 |
+
a = test_forward()
|
| 1697 |
+
print("Canonical relaxation:")
|
| 1698 |
+
b = test_energy_relax()
|
| 1699 |
+
print("Backward inversion (factoring by relaxation):")
|
| 1700 |
+
c_ = test_factor()
|
| 1701 |
+
print("SAT (clamp output to 1, relax to a model):")
|
| 1702 |
+
d = test_sat()
|
| 1703 |
+
print("=" * 40)
|
| 1704 |
+
print("ALL PASS" if (a and b and c_ and d) else "FAILURES")
|
| 1705 |
+
sys.exit(0 if (a and b and c_ and d) else 1)
|
| 1706 |
+
|
| 1707 |
+
|
| 1708 |
+
def cmd_reversible():
|
| 1709 |
+
print("Reversible primitives + ALU")
|
| 1710 |
+
a = _test_primitives()
|
| 1711 |
+
b = _test_adder(4) and _test_adder(5)
|
| 1712 |
+
c = _test_alu(4)
|
| 1713 |
+
d = _test_bennett(4)
|
| 1714 |
+
print("PASS" if (a and b and c and d) else "FAIL")
|
| 1715 |
+
|
| 1716 |
+
|
| 1717 |
+
def cmd_reversible_cpu():
|
| 1718 |
+
print("Reversible CPU")
|
| 1719 |
+
a = test_straight_line()
|
| 1720 |
+
b = test_bijection()
|
| 1721 |
+
print("PASS" if (a and b) else "FAIL")
|
| 1722 |
+
|
| 1723 |
+
|
| 1724 |
+
def cmd_reversible_prog():
|
| 1725 |
+
print("Reversible structured programs")
|
| 1726 |
+
print("PASS" if _test() else "FAIL")
|
| 1727 |
+
|
| 1728 |
+
|
| 1729 |
+
def cmd_ca():
|
| 1730 |
+
print("Reversible Margolus cellular automaton")
|
| 1731 |
+
a = _test_rule()
|
| 1732 |
+
g = _test_gates()
|
| 1733 |
+
b = _test_reversibility()
|
| 1734 |
+
c = _test_ballistic()
|
| 1735 |
+
d = _test_collision()
|
| 1736 |
+
e = _test_gate()
|
| 1737 |
+
f = _test_compose()
|
| 1738 |
+
print("PASS" if (a and g and b and c and d and e and f) else "FAIL")
|
| 1739 |
+
|
| 1740 |
+
|
| 1741 |
+
def cmd_tile():
|
| 1742 |
+
print("Self-assembling tile computer")
|
| 1743 |
+
a = _test_binding_gate()
|
| 1744 |
+
b = all(_test_rule2(fn, nm) for fn, nm in
|
| 1745 |
+
[(lambda w, s: w ^ s, "XOR"), (lambda w, s: w & s, "AND"),
|
| 1746 |
+
(lambda w, s: w | s, "OR")])
|
| 1747 |
+
c = _test_counter()
|
| 1748 |
+
print("PASS" if (a and b and c) else "FAIL")
|
| 1749 |
+
|
| 1750 |
+
|
| 1751 |
+
COMMANDS = {'attractor': cmd_attractor, 'build-attractor': main_build_attractor, 'reversible': cmd_reversible, 'reversible-cpu': cmd_reversible_cpu, 'reversible-prog': cmd_reversible_prog, 'build-reversible': main_build_reversible, 'reversible-matrix': main_reversible_matrix, 'ca': cmd_ca, 'build-ca': main_build_ca, 'tile': cmd_tile, 'build-tile': main_build_tile}
|
| 1752 |
+
|
| 1753 |
+
|
| 1754 |
+
if __name__ == "__main__":
|
| 1755 |
+
if len(sys.argv) < 2 or sys.argv[1] not in COMMANDS:
|
| 1756 |
+
sys.exit('commands: ' + ', '.join(COMMANDS))
|
| 1757 |
+
cmd = sys.argv.pop(1)
|
| 1758 |
+
sys.argv[0] += ' ' + cmd
|
| 1759 |
+
sys.exit(COMMANDS[cmd]())
|
src/build.py
CHANGED
|
@@ -1,111 +1,16 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Build tools for 8-bit Threshold Computer safetensors.
|
| 3 |
-
|
| 4 |
-
Subcommands:
|
| 5 |
-
python build.py memory - Generate 64KB memory circuits
|
| 6 |
-
python build.py inputs - Add .inputs metadata tensors
|
| 7 |
-
python build.py all - Run both (memory first, then inputs)
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
ROUTING SCHEMA (formerly routing.json)
|
| 11 |
-
======================================
|
| 12 |
-
|
| 13 |
-
Routing info is now embedded in safetensors via .inputs tensors and signal registry metadata.
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
INPUT SOURCE TYPES
|
| 17 |
-
------------------
|
| 18 |
-
|
| 19 |
-
1. External input: "$input_name" - Named input to the circuit
|
| 20 |
-
- Example: "$a", "$b", "$cin"
|
| 21 |
-
|
| 22 |
-
2. Gate output: "path.to.gate" - Output of another gate
|
| 23 |
-
- Example: "ha1.sum", "layer1.or"
|
| 24 |
-
|
| 25 |
-
3. Bit extraction: "$input[i]" - Single bit from multi-bit input
|
| 26 |
-
- Example: "$a[0]" (LSB), "$a[7]" (MSB for 8-bit)
|
| 27 |
-
|
| 28 |
-
4. Constant: "#0" or "#1" - Fixed value
|
| 29 |
-
- Example: "#1" for carry-in in two's complement
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
CIRCUIT TYPES
|
| 33 |
-
-------------
|
| 34 |
-
|
| 35 |
-
Single-Layer Gates: .weight and .bias only
|
| 36 |
-
"boolean.and": ["$a", "$b"]
|
| 37 |
-
|
| 38 |
-
Two-Layer Gates (XOR, XNOR): layer1 + layer2
|
| 39 |
-
"boolean.xor.layer1.or": ["$a", "$b"]
|
| 40 |
-
"boolean.xor.layer1.nand": ["$a", "$b"]
|
| 41 |
-
"boolean.xor.layer2": ["layer1.or", "layer1.nand"]
|
| 42 |
-
|
| 43 |
-
Hierarchical Circuits: nested sub-components
|
| 44 |
-
"arithmetic.fulladder": {
|
| 45 |
-
"ha1.sum.layer1.or": ["$a", "$b"],
|
| 46 |
-
"ha1.carry": ["$a", "$b"],
|
| 47 |
-
"ha2.sum.layer1.or": ["ha1.sum", "$cin"],
|
| 48 |
-
"carry_or": ["ha1.carry", "ha2.carry"]
|
| 49 |
-
}
|
| 50 |
-
|
| 51 |
-
Bit-Indexed Circuits: multi-bit operations
|
| 52 |
-
"arithmetic.ripplecarry8bit.fa0": ["$a[0]", "$b[0]", "#0"]
|
| 53 |
-
"arithmetic.ripplecarry8bit.fa1": ["$a[1]", "$b[1]", "fa0.cout"]
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
PACKED MEMORY CIRCUITS
|
| 57 |
-
----------------------
|
| 58 |
-
|
| 59 |
-
64KB memory uses packed tensors (shapes for 16-bit address, 8-bit data):
|
| 60 |
-
|
| 61 |
-
memory.addr_decode.weight: [65536, 16]
|
| 62 |
-
memory.addr_decode.bias: [65536]
|
| 63 |
-
memory.read.and.weight: [8, 65536, 2]
|
| 64 |
-
memory.read.and.bias: [8, 65536]
|
| 65 |
-
memory.read.or.weight: [8, 65536]
|
| 66 |
-
memory.read.or.bias: [8]
|
| 67 |
-
memory.write.sel.weight: [65536, 2]
|
| 68 |
-
memory.write.sel.bias: [65536]
|
| 69 |
-
memory.write.nsel.weight: [65536, 1]
|
| 70 |
-
memory.write.nsel.bias: [65536]
|
| 71 |
-
memory.write.and_old.weight: [65536, 8, 2]
|
| 72 |
-
memory.write.and_old.bias: [65536, 8]
|
| 73 |
-
memory.write.and_new.weight: [65536, 8, 2]
|
| 74 |
-
memory.write.and_new.bias: [65536, 8]
|
| 75 |
-
memory.write.or.weight: [65536, 8, 2]
|
| 76 |
-
memory.write.or.bias: [65536, 8]
|
| 77 |
-
|
| 78 |
-
Semantics:
|
| 79 |
-
decode: sel[i] = H(sum(addr_bits * weight[i]) + bias[i])
|
| 80 |
-
read: bit[b] = H(sum(H([mem_bit, sel] * and_w) + and_b) * or_w + or_b)
|
| 81 |
-
write: new = H(H([old, nsel] * and_old) + H([data, sel] * and_new) - 1)
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
SIGNAL REGISTRY
|
| 85 |
-
---------------
|
| 86 |
-
|
| 87 |
-
Signal IDs are stored in safetensors metadata as JSON:
|
| 88 |
-
|
| 89 |
-
{"0": "#0", "1": "#1", "2": "$a", "3": "$b", ...}
|
| 90 |
-
|
| 91 |
-
Each gate's .inputs tensor contains integer IDs referencing this registry.
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
NAMING CONVENTIONS
|
| 95 |
-
------------------
|
| 96 |
-
|
| 97 |
-
- External inputs: $name or $name[bit]
|
| 98 |
-
- Constants: #0, #1
|
| 99 |
-
- Internal gates: relative path from circuit root
|
| 100 |
-
"""
|
| 101 |
-
|
| 102 |
from __future__ import annotations
|
| 103 |
|
| 104 |
import argparse
|
| 105 |
import json
|
|
|
|
| 106 |
import re
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
from pathlib import Path
|
| 108 |
-
from typing import Any, Dict, Iterable, List, Optional, Set
|
| 109 |
|
| 110 |
import torch
|
| 111 |
from safetensors import safe_open
|
|
@@ -140,9 +45,11 @@ def get_model_path(bits: int = 8, memory_profile: str = None, addr_bits: int = N
|
|
| 140 |
|
| 141 |
|
| 142 |
MODEL_PATH = MODEL_DIR / "neural_computer8.safetensors"
|
|
|
|
| 143 |
MANIFEST_PATH = Path(__file__).resolve().parent.parent / "tensors.txt"
|
| 144 |
|
| 145 |
DEFAULT_ADDR_BITS = 16
|
|
|
|
| 146 |
DEFAULT_MEM_BYTES = 1 << DEFAULT_ADDR_BITS
|
| 147 |
|
| 148 |
MEMORY_PROFILES = {
|
|
@@ -166,8 +73,7 @@ def load_tensors(path: Path) -> Dict[str, torch.Tensor]:
|
|
| 166 |
|
| 167 |
|
| 168 |
def load_file_metadata(path: Path) -> Dict[str, str]:
|
| 169 |
-
"""Read the safetensors header metadata so every save
|
| 170 |
-
forward (signal_registry, weight_quantization, ...)."""
|
| 171 |
with safe_open(str(path), framework="pt") as f:
|
| 172 |
meta = f.metadata()
|
| 173 |
return dict(meta) if meta else {}
|
|
@@ -264,12 +170,7 @@ def add_memory_write_cells(tensors: Dict[str, torch.Tensor], mem_bytes: int) ->
|
|
| 264 |
|
| 265 |
|
| 266 |
def add_fetch_load_store_buffers(tensors: Dict[str, torch.Tensor], data_bits: int, addr_bits: int) -> None:
|
| 267 |
-
"""Add control buffers for fetch, load, store operations.
|
| 268 |
-
|
| 269 |
-
Args:
|
| 270 |
-
data_bits: Width of data bus (8/16/32)
|
| 271 |
-
addr_bits: Width of address bus (determines instruction register width)
|
| 272 |
-
"""
|
| 273 |
# Instruction register width = opcode (8) + operands (depends on arch)
|
| 274 |
# For simplicity, IR width = max(16, addr_bits) to hold jump targets
|
| 275 |
ir_bits = max(16, addr_bits)
|
|
@@ -283,13 +184,7 @@ def add_fetch_load_store_buffers(tensors: Dict[str, torch.Tensor], data_bits: in
|
|
| 283 |
|
| 284 |
|
| 285 |
def add_full_adder(tensors: Dict[str, torch.Tensor], prefix: str) -> None:
|
| 286 |
-
"""Add a single full adder at the given prefix.
|
| 287 |
-
|
| 288 |
-
Full adder structure:
|
| 289 |
-
- ha1: first half adder (A XOR B for sum, A AND B for carry)
|
| 290 |
-
- ha2: second half adder (ha1.sum XOR Cin for sum, ha1.sum AND Cin for carry)
|
| 291 |
-
- carry_or: OR of ha1.carry and ha2.carry for final carry out
|
| 292 |
-
"""
|
| 293 |
# XOR for ha1.sum (2-layer: OR + NAND -> AND)
|
| 294 |
add_gate(tensors, f"{prefix}.ha1.sum.layer1.or", [1.0, 1.0], [-1.0])
|
| 295 |
add_gate(tensors, f"{prefix}.ha1.sum.layer1.nand", [-1.0, -1.0], [1.0])
|
|
@@ -307,21 +202,7 @@ def add_full_adder(tensors: Dict[str, torch.Tensor], prefix: str) -> None:
|
|
| 307 |
|
| 308 |
|
| 309 |
def add_expr_add_mul(tensors: Dict[str, torch.Tensor]) -> None:
|
| 310 |
-
"""Add expression circuit for A + B × C (order of operations).
|
| 311 |
-
|
| 312 |
-
Computes A + (B × C) where multiplication has higher precedence.
|
| 313 |
-
|
| 314 |
-
Structure:
|
| 315 |
-
- Stage 1: Multiply B × C using shift-add algorithm
|
| 316 |
-
- 8 mask stages: mask[i] = B AND C[i] (8 AND gates each, shifted)
|
| 317 |
-
- 7 accumulator adders to sum masked values
|
| 318 |
-
- Stage 2: Add A to multiplication result (8-bit ripple carry)
|
| 319 |
-
|
| 320 |
-
Inputs: $a[0-7], $b[0-7], $c[0-7] (MSB-first, 8-bit each)
|
| 321 |
-
Output: 8-bit result of A + (B × C), wrapping on overflow
|
| 322 |
-
|
| 323 |
-
Total: 64 AND gates + 7×8 full adders (mul) + 8 full adders (add) = ~640 gates
|
| 324 |
-
"""
|
| 325 |
prefix = "arithmetic.expr_add_mul"
|
| 326 |
|
| 327 |
# Stage 1: Multiply B × C using shift-add
|
|
@@ -357,22 +238,7 @@ def add_expr_add_mul(tensors: Dict[str, torch.Tensor]) -> None:
|
|
| 357 |
|
| 358 |
|
| 359 |
def add_expr_paren_add_mul(tensors: Dict[str, torch.Tensor]) -> None:
|
| 360 |
-
"""Add expression circuit for (A + B) × C (parenthetical override).
|
| 361 |
-
|
| 362 |
-
Computes (A + B) × C where parentheses override normal precedence.
|
| 363 |
-
Addition happens first, then multiplication.
|
| 364 |
-
|
| 365 |
-
Structure:
|
| 366 |
-
- Stage 1: Add A + B (8-bit ripple carry adder)
|
| 367 |
-
- Stage 2: Multiply sum × C using shift-add algorithm
|
| 368 |
-
- 8 mask stages: mask[i] = sum AND C[i] (8 AND gates each)
|
| 369 |
-
- 7 accumulator adders to sum shifted masked values
|
| 370 |
-
|
| 371 |
-
Inputs: $a[0-7], $b[0-7], $c[0-7] (MSB-first, 8-bit each)
|
| 372 |
-
Output: 8-bit result of (A + B) × C, wrapping on overflow
|
| 373 |
-
|
| 374 |
-
Total: 8 full adders (add) + 64 AND gates + 56 full adders (mul) = ~640 gates
|
| 375 |
-
"""
|
| 376 |
prefix = "arithmetic.expr_paren_add_mul"
|
| 377 |
|
| 378 |
# Stage 1: Add A + B
|
|
@@ -392,21 +258,7 @@ def add_expr_paren_add_mul(tensors: Dict[str, torch.Tensor]) -> None:
|
|
| 392 |
|
| 393 |
|
| 394 |
def add_expr_paren(tensors: Dict[str, torch.Tensor]) -> None:
|
| 395 |
-
"""Add expression circuit for (A + B) × C (parenthetical grouping).
|
| 396 |
-
|
| 397 |
-
Computes (A + B) × C where addition happens first due to parentheses.
|
| 398 |
-
|
| 399 |
-
Structure:
|
| 400 |
-
- Stage 1: Add A + B (8-bit ripple carry)
|
| 401 |
-
- Stage 2: Multiply sum × C using shift-add algorithm
|
| 402 |
-
- 8 mask stages: mask[i] = sum AND C[i] (8 AND gates each)
|
| 403 |
-
- 7 accumulator adders to sum shifted masked values
|
| 404 |
-
|
| 405 |
-
Inputs: $a[0-7], $b[0-7], $c[0-7] (MSB-first, 8-bit each)
|
| 406 |
-
Output: 8-bit result of (A + B) × C, wrapping on overflow
|
| 407 |
-
|
| 408 |
-
Total: 8 full adders (add) + 64 AND gates + 56 full adders (mul) = ~640 gates
|
| 409 |
-
"""
|
| 410 |
prefix = "arithmetic.expr_paren"
|
| 411 |
|
| 412 |
# Stage 1: Add A + B
|
|
@@ -426,17 +278,7 @@ def add_expr_paren(tensors: Dict[str, torch.Tensor]) -> None:
|
|
| 426 |
|
| 427 |
|
| 428 |
def add_add3(tensors: Dict[str, torch.Tensor]) -> None:
|
| 429 |
-
"""Add 3-operand 8-bit adder circuit.
|
| 430 |
-
|
| 431 |
-
Computes A + B + C using two chained ripple-carry stages:
|
| 432 |
-
- Stage 1: temp = A + B (8 full adders)
|
| 433 |
-
- Stage 2: result = temp + C (8 full adders)
|
| 434 |
-
|
| 435 |
-
Inputs: $a[0-7], $b[0-7], $c[0-7] (MSB-first)
|
| 436 |
-
Outputs: stage2.fa0-7.ha2.sum.layer2 (result bits), stage2.fa7.carry_or (overflow)
|
| 437 |
-
|
| 438 |
-
Total: 16 full adders = 144 gates
|
| 439 |
-
"""
|
| 440 |
# Stage 1: A + B -> temp
|
| 441 |
for bit in range(8):
|
| 442 |
add_full_adder(tensors, f"arithmetic.add3_8bit.stage1.fa{bit}")
|
|
@@ -447,14 +289,7 @@ def add_add3(tensors: Dict[str, torch.Tensor]) -> None:
|
|
| 447 |
|
| 448 |
|
| 449 |
def add_shl_shr(tensors: Dict[str, torch.Tensor]) -> None:
|
| 450 |
-
"""Add SHL (shift left) and SHR (shift right) circuits.
|
| 451 |
-
|
| 452 |
-
Identity gate: w=1, b=-1 -> H(x - 1) = x for x in {0,1}
|
| 453 |
-
Zero gate: w=0, b=-1 -> H(-1) = 0
|
| 454 |
-
|
| 455 |
-
SHL (MSB-first): out[i] = in[i+1] for i<7, out[7] = 0
|
| 456 |
-
SHR (MSB-first): out[0] = 0, out[i] = in[i-1] for i>0
|
| 457 |
-
"""
|
| 458 |
for bit in range(8):
|
| 459 |
if bit < 7:
|
| 460 |
add_gate(tensors, f"alu.alu8bit.shl.bit{bit}", [1.0], [-1.0])
|
|
@@ -469,18 +304,7 @@ def add_shl_shr(tensors: Dict[str, torch.Tensor]) -> None:
|
|
| 469 |
|
| 470 |
|
| 471 |
def add_mul(tensors: Dict[str, torch.Tensor]) -> None:
|
| 472 |
-
"""Add 8-bit multiplication circuit.
|
| 473 |
-
|
| 474 |
-
Produces low 8 bits of the 16-bit result.
|
| 475 |
-
|
| 476 |
-
Structure:
|
| 477 |
-
- 64 AND gates for partial products P[i][j] = A[i] AND B[j]
|
| 478 |
-
- Uses existing ripple-carry adder components for summation
|
| 479 |
-
|
| 480 |
-
The multiply method in ThresholdALU computes:
|
| 481 |
-
1. Partial products via these AND gates
|
| 482 |
-
2. Shift-add accumulation via existing 8-bit adder
|
| 483 |
-
"""
|
| 484 |
# AND gates for partial products: P[i][j] = A[i] AND B[j]
|
| 485 |
# These compute whether bit i of A and bit j of B are both 1
|
| 486 |
for i in range(8):
|
|
@@ -489,19 +313,7 @@ def add_mul(tensors: Dict[str, torch.Tensor]) -> None:
|
|
| 489 |
|
| 490 |
|
| 491 |
def add_div(tensors: Dict[str, torch.Tensor]) -> None:
|
| 492 |
-
"""Add 8-bit division circuit.
|
| 493 |
-
|
| 494 |
-
Produces quotient (8 bits) and remainder (8 bits).
|
| 495 |
-
|
| 496 |
-
Uses restoring division algorithm:
|
| 497 |
-
- 8 iterations, each producing one quotient bit
|
| 498 |
-
- Each iteration: compare, conditionally subtract, shift
|
| 499 |
-
|
| 500 |
-
Structure:
|
| 501 |
-
- 8 comparison gates (one per iteration)
|
| 502 |
-
- 8 conditional subtraction stages
|
| 503 |
-
- Uses existing comparator and subtractor components
|
| 504 |
-
"""
|
| 505 |
# Comparison gates: check if (remainder << 1 | next_bit) >= divisor
|
| 506 |
# Bit-cascaded ternary GE comparator per stage.
|
| 507 |
for stage in range(8):
|
|
@@ -529,14 +341,7 @@ def add_div(tensors: Dict[str, torch.Tensor]) -> None:
|
|
| 529 |
|
| 530 |
|
| 531 |
def add_inc_dec(tensors: Dict[str, torch.Tensor]) -> None:
|
| 532 |
-
"""Add INC and DEC circuits.
|
| 533 |
-
|
| 534 |
-
INC: A + 1 using half adders with carry chain
|
| 535 |
-
DEC: A - 1 using borrow chain (A + 255, two's complement of 1)
|
| 536 |
-
|
| 537 |
-
For INC, we add 1 to the LSB and propagate carry.
|
| 538 |
-
For DEC, we add 0xFF (two's complement of 1) or use borrow logic.
|
| 539 |
-
"""
|
| 540 |
# INC: half adder chain starting with carry_in = 1
|
| 541 |
# bit 7 (LSB): XOR with 1, carry = bit[7]
|
| 542 |
# bit 6: XOR with carry, new_carry = bit[6] AND old_carry
|
|
@@ -563,12 +368,7 @@ def add_inc_dec(tensors: Dict[str, torch.Tensor]) -> None:
|
|
| 563 |
|
| 564 |
|
| 565 |
def add_neg(tensors: Dict[str, torch.Tensor]) -> None:
|
| 566 |
-
"""Add NEG circuit (two's complement negation).
|
| 567 |
-
|
| 568 |
-
NEG(A) = NOT(A) + 1 = ~A + 1
|
| 569 |
-
|
| 570 |
-
Structure: NOT gates followed by INC-style adder.
|
| 571 |
-
"""
|
| 572 |
for bit in range(8):
|
| 573 |
# NOT gate for each bit
|
| 574 |
add_gate(tensors, f"alu.alu8bit.neg.not.bit{bit}", [-1.0], [0.0])
|
|
@@ -585,14 +385,7 @@ def add_rol_ror(tensors: Dict[str, torch.Tensor]) -> None:
|
|
| 585 |
|
| 586 |
|
| 587 |
def add_rol_ror_nbits(tensors: Dict[str, torch.Tensor], bits: int) -> None:
|
| 588 |
-
"""Add N-bit ROL and ROR circuits (rotate left/right).
|
| 589 |
-
|
| 590 |
-
ROL: out[i] = in[i+1] for i<N-1, out[N-1] = in[0] (MSB wraps to LSB)
|
| 591 |
-
ROR: out[0] = in[N-1], out[i] = in[i-1] for i>0 (LSB wraps to MSB)
|
| 592 |
-
|
| 593 |
-
Args:
|
| 594 |
-
bits: Data width (8, 16, 32, etc.)
|
| 595 |
-
"""
|
| 596 |
# ROL: rotate left (toward MSB)
|
| 597 |
for bit in range(bits):
|
| 598 |
add_gate(tensors, f"alu.alu{bits}bit.rol.bit{bit}", [1.0], [-1.0])
|
|
@@ -603,19 +396,7 @@ def add_rol_ror_nbits(tensors: Dict[str, torch.Tensor], bits: int) -> None:
|
|
| 603 |
|
| 604 |
|
| 605 |
def add_stack_ops(tensors: Dict[str, torch.Tensor], data_bits: int, addr_bits: int) -> None:
|
| 606 |
-
"""Add RET, PUSH, POP circuit components.
|
| 607 |
-
|
| 608 |
-
These are higher-level operations that use memory read/write.
|
| 609 |
-
We create the control logic gates.
|
| 610 |
-
|
| 611 |
-
Args:
|
| 612 |
-
data_bits: Width of data to push/pop (8/16/32)
|
| 613 |
-
addr_bits: Width of stack pointer and return addresses
|
| 614 |
-
|
| 615 |
-
RET: Pop return address from stack, jump to it
|
| 616 |
-
PUSH: Decrement SP, write value to [SP]
|
| 617 |
-
POP: Read value from [SP], increment SP
|
| 618 |
-
"""
|
| 619 |
# SP decrement for PUSH (addr_bits wide)
|
| 620 |
for bit in range(addr_bits):
|
| 621 |
add_gate(tensors, f"control.push.sp_dec.bit{bit}.xor.layer1.or", [1.0, 1.0], [-1.0])
|
|
@@ -644,21 +425,7 @@ def add_stack_ops(tensors: Dict[str, torch.Tensor], data_bits: int, addr_bits: i
|
|
| 644 |
|
| 645 |
|
| 646 |
def add_conditional_jumps(tensors: Dict[str, torch.Tensor], addr_bits: int) -> None:
|
| 647 |
-
"""Add conditional jump circuits (JZ, JNZ, JC, JNC, JP, JN, JV, JNV).
|
| 648 |
-
|
| 649 |
-
Each conditional jump is a 2:1 MUX per address bit:
|
| 650 |
-
- If flag is set: output = target_bit
|
| 651 |
-
- If flag is clear: output = pc_bit
|
| 652 |
-
|
| 653 |
-
Structure per bit:
|
| 654 |
-
- not_sel: NOT(flag)
|
| 655 |
-
- and_a: pc_bit AND NOT(flag)
|
| 656 |
-
- and_b: target_bit AND flag
|
| 657 |
-
- or: and_a OR and_b
|
| 658 |
-
|
| 659 |
-
Args:
|
| 660 |
-
addr_bits: Width of program counter / jump target
|
| 661 |
-
"""
|
| 662 |
jump_types = ['jz', 'jnz', 'jc', 'jnc', 'jp', 'jn', 'jv', 'jnv']
|
| 663 |
|
| 664 |
for jmp in jump_types:
|
|
@@ -675,19 +442,14 @@ def add_conditional_jumps(tensors: Dict[str, torch.Tensor], addr_bits: int) -> N
|
|
| 675 |
|
| 676 |
|
| 677 |
def add_computer_decode(tensors: Dict[str, torch.Tensor]) -> None:
|
| 678 |
-
"""
|
| 679 |
-
opcode one-hot decoder (exact match on the instruction's opcode nibble). The
|
| 680 |
-
runtime reads these one-hots to dispatch instead of slicing the word."""
|
| 681 |
for opc in range(16):
|
| 682 |
w = [1.0 if (opc >> j) & 1 else -1.0 for j in range(4)]
|
| 683 |
add_gate(tensors, f"control.decode.op{opc}", w, [-float(bin(opc).count("1"))])
|
| 684 |
|
| 685 |
|
| 686 |
def add_computer_pcnext(tensors: Dict[str, torch.Tensor], addr_bits: int) -> None:
|
| 687 |
-
"""PC
|
| 688 |
-
increment chains and a priority mux selecting the next PC among PC+2 (ALU),
|
| 689 |
-
PC+4 (load/store), the conditional-jump mux, and the direct target
|
| 690 |
-
(JMP/CALL). Replaces the runtime's Python PC+2/PC+4 and jump wiring."""
|
| 691 |
for name, start in (("pc2", 1), ("pc4", 2)):
|
| 692 |
for bit in range(addr_bits):
|
| 693 |
if bit < start:
|
|
@@ -707,17 +469,7 @@ def add_computer_pcnext(tensors: Dict[str, torch.Tensor], addr_bits: int) -> Non
|
|
| 707 |
|
| 708 |
|
| 709 |
def add_status_flags(tensors: Dict[str, torch.Tensor], data_bits: int) -> None:
|
| 710 |
-
"""Add status flag computation circuits (Z, N, C, V).
|
| 711 |
-
|
| 712 |
-
Args:
|
| 713 |
-
data_bits: Width of ALU data (8/16/32)
|
| 714 |
-
|
| 715 |
-
Flags:
|
| 716 |
-
- Z (Zero): NOR of all result bits (1 if result == 0)
|
| 717 |
-
- N (Negative): Copy of MSB (sign bit)
|
| 718 |
-
- C (Carry): Carry out from adder (external input)
|
| 719 |
-
- V (Overflow): XOR of carry into and out of MSB (signed overflow)
|
| 720 |
-
"""
|
| 721 |
# Z flag: NOR of all bits (result == 0)
|
| 722 |
# Single threshold gate: fires if sum of all bits < 1
|
| 723 |
add_gate(tensors, "flags.zero", [-1.0] * data_bits, [0.0])
|
|
@@ -741,14 +493,7 @@ def add_barrel_shifter(tensors: Dict[str, torch.Tensor]) -> None:
|
|
| 741 |
|
| 742 |
|
| 743 |
def add_barrel_shifter_nbits(tensors: Dict[str, torch.Tensor], bits: int) -> None:
|
| 744 |
-
"""Add N-bit barrel shifter circuit.
|
| 745 |
-
|
| 746 |
-
Shifts input by 0 to (bits-1) positions based on ceil(log2(bits))-bit shift amount.
|
| 747 |
-
Uses layers of 2:1 muxes controlled by shift amount bits.
|
| 748 |
-
|
| 749 |
-
Args:
|
| 750 |
-
bits: Data width (8, 16, 32, etc.)
|
| 751 |
-
"""
|
| 752 |
import math
|
| 753 |
num_layers = max(1, math.ceil(math.log2(bits)))
|
| 754 |
|
|
@@ -769,21 +514,7 @@ def add_priority_encoder(tensors: Dict[str, torch.Tensor]) -> None:
|
|
| 769 |
|
| 770 |
|
| 771 |
def add_priority_encoder_nbits(tensors: Dict[str, torch.Tensor], bits: int) -> None:
|
| 772 |
-
"""Add N-bit priority encoder circuit.
|
| 773 |
-
|
| 774 |
-
Finds the position of the highest set bit (0 to bits-1).
|
| 775 |
-
Position 0 = MSB (highest priority), Position bits-1 = LSB (lowest priority).
|
| 776 |
-
Output is ceil(log2(bits))-bit index + valid flag.
|
| 777 |
-
|
| 778 |
-
Circuit structure:
|
| 779 |
-
1. any_higher{pos}: OR of bits 0 to pos-1 (all higher-priority positions)
|
| 780 |
-
2. is_highest{pos}: bit[pos] AND NOT(any_higher{pos})
|
| 781 |
-
3. out{bit}: OR of is_highest{pos} for positions where (pos >> bit) & 1
|
| 782 |
-
4. valid: OR of all input bits
|
| 783 |
-
|
| 784 |
-
Args:
|
| 785 |
-
bits: Input width (8, 16, 32, etc.)
|
| 786 |
-
"""
|
| 787 |
import math
|
| 788 |
out_bits = max(1, math.ceil(math.log2(bits)))
|
| 789 |
prefix = f"combinational.priorityencoder{bits}"
|
|
@@ -827,31 +558,7 @@ def add_bit_cascade_compare(
|
|
| 827 |
out_le: str,
|
| 828 |
out_eq: str,
|
| 829 |
) -> None:
|
| 830 |
-
"""Generic ternary-only N-bit comparator.
|
| 831 |
-
|
| 832 |
-
Inputs are two N-bit values A and B in MSB-first order. The structure
|
| 833 |
-
produces unsigned-magnitude GT, LT, GE, LE, EQ outputs using only
|
| 834 |
-
weights in {-1, 0, 1} and integer biases.
|
| 835 |
-
|
| 836 |
-
Per-bit primitives (i = 0 is the MSB):
|
| 837 |
-
{cmp_prefix}.bit{i}.gt A[i] AND NOT B[i] weights [1, -1], bias -1
|
| 838 |
-
{cmp_prefix}.bit{i}.lt NOT A[i] AND B[i] weights [-1, 1], bias -1
|
| 839 |
-
{cmp_prefix}.bit{i}.eq.layer1.and A[i] AND B[i]
|
| 840 |
-
{cmp_prefix}.bit{i}.eq.layer1.nor NOR(A[i], B[i])
|
| 841 |
-
{cmp_prefix}.bit{i}.eq XNOR via OR of layer1 outputs
|
| 842 |
-
|
| 843 |
-
Cascade (linear chain from MSB to LSB):
|
| 844 |
-
{cmp_prefix}.cascade.eq_prefix.bit{i} AND of eq[0..i-1] (i in 1..N-1)
|
| 845 |
-
{cmp_prefix}.cascade.gt.bit{i} eq_prefix[i] AND gt[i]
|
| 846 |
-
{cmp_prefix}.cascade.lt.bit{i} eq_prefix[i] AND lt[i]
|
| 847 |
-
|
| 848 |
-
Final outputs:
|
| 849 |
-
out_gt = OR of (gt[0], cascade.gt.bit{1..N-1})
|
| 850 |
-
out_lt = OR of (lt[0], cascade.lt.bit{1..N-1})
|
| 851 |
-
out_eq = AND of all eq[i]
|
| 852 |
-
out_ge = NOT(out_lt)
|
| 853 |
-
out_le = NOT(out_gt)
|
| 854 |
-
"""
|
| 855 |
for i in range(bits):
|
| 856 |
# per-bit GT: A[i] AND NOT B[i] -> H(A - B - 1)
|
| 857 |
add_gate(tensors, f"{cmp_prefix}.bit{i}.gt", [1.0, -1.0], [-1.0])
|
|
@@ -891,11 +598,7 @@ def add_bit_cascade_compare(
|
|
| 891 |
|
| 892 |
|
| 893 |
def add_comparators(tensors: Dict[str, torch.Tensor]) -> None:
|
| 894 |
-
"""Add 8-bit comparator circuits (GT, LT, GE, LE, EQ) using bit-cascade.
|
| 895 |
-
|
| 896 |
-
Inputs are 8 bits of A then 8 bits of B in MSB-first order. The
|
| 897 |
-
underlying bit-cascade produces only ternary {-1, 0, 1} weights.
|
| 898 |
-
"""
|
| 899 |
add_bit_cascade_compare(
|
| 900 |
tensors,
|
| 901 |
cmp_prefix="arithmetic.cmp8bit",
|
|
@@ -909,30 +612,14 @@ def add_comparators(tensors: Dict[str, torch.Tensor]) -> None:
|
|
| 909 |
|
| 910 |
|
| 911 |
def add_ripple_carry_nbits(tensors: Dict[str, torch.Tensor], bits: int) -> None:
|
| 912 |
-
"""Add N-bit ripple carry adder circuit.
|
| 913 |
-
|
| 914 |
-
Creates a chain of full adders for N-bit addition.
|
| 915 |
-
Works for 8, 16, or 32 bits.
|
| 916 |
-
|
| 917 |
-
Inputs: $a[0..N-1], $b[0..N-1] (MSB-first)
|
| 918 |
-
Outputs: fa0-fa{N-1} sum bits, fa{N-1}.carry_or for overflow
|
| 919 |
-
"""
|
| 920 |
prefix = f"arithmetic.ripplecarry{bits}bit"
|
| 921 |
for bit in range(bits):
|
| 922 |
add_full_adder(tensors, f"{prefix}.fa{bit}")
|
| 923 |
|
| 924 |
|
| 925 |
def add_sub_nbits(tensors: Dict[str, torch.Tensor], bits: int) -> None:
|
| 926 |
-
"""Add N-bit subtractor circuit (A - B).
|
| 927 |
-
|
| 928 |
-
Uses two's complement: A - B = A + (~B) + 1
|
| 929 |
-
|
| 930 |
-
Structure:
|
| 931 |
-
- NOT gates for each bit of B
|
| 932 |
-
- N-bit ripple carry adder with carry_in = 1
|
| 933 |
-
|
| 934 |
-
The carry_in=1 is handled by the adder's fa0 having cin=#1 instead of #0.
|
| 935 |
-
"""
|
| 936 |
prefix = f"arithmetic.sub{bits}bit"
|
| 937 |
|
| 938 |
for bit in range(bits):
|
|
@@ -943,10 +630,7 @@ def add_sub_nbits(tensors: Dict[str, torch.Tensor], bits: int) -> None:
|
|
| 943 |
|
| 944 |
|
| 945 |
def add_comparators_nbits(tensors: Dict[str, torch.Tensor], bits: int) -> None:
|
| 946 |
-
"""Add N-bit comparator circuits (GT, LT, GE, LE, EQ) via bit-cascade.
|
| 947 |
-
|
| 948 |
-
All weights are in {-1, 0, 1}. Inputs are A bits then B bits, MSB-first.
|
| 949 |
-
"""
|
| 950 |
add_bit_cascade_compare(
|
| 951 |
tensors,
|
| 952 |
cmp_prefix=f"arithmetic.cmp{bits}bit",
|
|
@@ -960,27 +644,14 @@ def add_comparators_nbits(tensors: Dict[str, torch.Tensor], bits: int) -> None:
|
|
| 960 |
|
| 961 |
|
| 962 |
def add_mul_nbits(tensors: Dict[str, torch.Tensor], bits: int) -> None:
|
| 963 |
-
"""Add N-bit multiplication circuit.
|
| 964 |
-
|
| 965 |
-
Produces low N bits of the 2N-bit result.
|
| 966 |
-
|
| 967 |
-
Structure:
|
| 968 |
-
- N*N AND gates for partial products P[i][j] = A[i] AND B[j]
|
| 969 |
-
- Shift-add accumulation using existing adder circuits
|
| 970 |
-
|
| 971 |
-
For 32-bit: 1024 AND gates for partial products.
|
| 972 |
-
"""
|
| 973 |
for i in range(bits):
|
| 974 |
for j in range(bits):
|
| 975 |
add_gate(tensors, f"alu.alu{bits}bit.mul.pp.a{i}b{j}", [1.0, 1.0], [-2.0])
|
| 976 |
|
| 977 |
|
| 978 |
def add_div_nbits(tensors: Dict[str, torch.Tensor], bits: int) -> None:
|
| 979 |
-
"""Add N-bit division circuit.
|
| 980 |
-
|
| 981 |
-
Uses restoring division algorithm with N iterations. Each stage's
|
| 982 |
-
comparator (rem >= div) is a ternary bit-cascade GE.
|
| 983 |
-
"""
|
| 984 |
for stage in range(bits):
|
| 985 |
add_bit_cascade_compare(
|
| 986 |
tensors,
|
|
@@ -1002,10 +673,7 @@ def add_div_nbits(tensors: Dict[str, torch.Tensor], bits: int) -> None:
|
|
| 1002 |
|
| 1003 |
|
| 1004 |
def add_bitwise_nbits(tensors: Dict[str, torch.Tensor], bits: int) -> None:
|
| 1005 |
-
"""Add N-bit bitwise operation circuits (AND, OR, XOR, NOT).
|
| 1006 |
-
|
| 1007 |
-
These are simply N copies of the 1-bit gates.
|
| 1008 |
-
"""
|
| 1009 |
for bit in range(bits):
|
| 1010 |
add_gate(tensors, f"alu.alu{bits}bit.and.bit{bit}", [1.0, 1.0], [-2.0])
|
| 1011 |
|
|
@@ -1022,11 +690,7 @@ def add_bitwise_nbits(tensors: Dict[str, torch.Tensor], bits: int) -> None:
|
|
| 1022 |
|
| 1023 |
|
| 1024 |
def add_shift_nbits(tensors: Dict[str, torch.Tensor], bits: int) -> None:
|
| 1025 |
-
"""Add N-bit shift circuits (SHL, SHR by 1 position).
|
| 1026 |
-
|
| 1027 |
-
SHL: out[i] = in[i+1] for i<N-1, out[N-1] = 0
|
| 1028 |
-
SHR: out[0] = 0, out[i] = in[i-1] for i>0
|
| 1029 |
-
"""
|
| 1030 |
for bit in range(bits):
|
| 1031 |
if bit < bits - 1:
|
| 1032 |
add_gate(tensors, f"alu.alu{bits}bit.shl.bit{bit}", [1.0], [-1.0])
|
|
@@ -1067,19 +731,7 @@ def add_neg_nbits(tensors: Dict[str, torch.Tensor], bits: int) -> None:
|
|
| 1067 |
|
| 1068 |
|
| 1069 |
def add_float16_core(tensors: Dict[str, torch.Tensor]) -> None:
|
| 1070 |
-
"""Add float16 core circuits (unpack, pack, classify).
|
| 1071 |
-
|
| 1072 |
-
IEEE 754 half-precision format (16 bits):
|
| 1073 |
-
- Bit 15: Sign (0=positive, 1=negative)
|
| 1074 |
-
- Bits 14-10: Exponent (5 bits, bias=15)
|
| 1075 |
-
- Bits 9-0: Mantissa/fraction (10 bits, implicit leading 1 for normalized)
|
| 1076 |
-
|
| 1077 |
-
Special values:
|
| 1078 |
-
- Zero: exp=0, frac=0
|
| 1079 |
-
- Subnormal: exp=0, frac≠0
|
| 1080 |
-
- Infinity: exp=31, frac=0
|
| 1081 |
-
- NaN: exp=31, frac≠0
|
| 1082 |
-
"""
|
| 1083 |
prefix = "float16"
|
| 1084 |
|
| 1085 |
# Identity buffers: H(x - 1) reproduces a binary input (bias 0 would
|
|
@@ -1103,28 +755,7 @@ def add_float16_core(tensors: Dict[str, torch.Tensor]) -> None:
|
|
| 1103 |
|
| 1104 |
def add_float_add(tensors: Dict[str, torch.Tensor], family: str,
|
| 1105 |
exp_bits: int, frac_bits: int) -> None:
|
| 1106 |
-
"""Add a complete float addition/subtraction pipeline.
|
| 1107 |
-
|
| 1108 |
-
Self-contained circuit; external inputs are the raw operand words
|
| 1109 |
-
$a[0..W-1] / $b[0..W-1] (MSB-first). Contract: exact IEEE specials
|
| 1110 |
-
(NaN, infinities, signed zeros, opposite-sign infinities -> NaN, exact
|
| 1111 |
-
cancellation -> +0), subnormal operands and gradual-underflow subnormal
|
| 1112 |
-
results, round-to-nearest-even for the mantissa. A zero operand passes the
|
| 1113 |
-
other operand through verbatim.
|
| 1114 |
-
|
| 1115 |
-
Datapath: payload comparison picks the larger-magnitude operand, swap
|
| 1116 |
-
muxes route it to L; the smaller mantissa is aligned right through a
|
| 1117 |
-
capped barrel shifter over an F+3-bit round+guard frame with a sticky
|
| 1118 |
-
collector; add and subtract chains run in parallel (the subtract chain
|
| 1119 |
-
takes the sticky as a borrow-in so its round bit is exact) and the sign
|
| 1120 |
-
agreement selects; a leading-zero counter drives the left renormalize
|
| 1121 |
-
barrel; guard/round/sticky then drive a round-to-nearest-even increment
|
| 1122 |
-
on the fraction with a carry into the exponent; two E+2-bit exponent
|
| 1123 |
-
chains compute exp_L + 1 - nlz plus the rounding carry; and a one-hot
|
| 1124 |
-
specials network selects the output word.
|
| 1125 |
-
|
| 1126 |
-
Rounding: round-to-nearest-even (RNE), matching multiply and divide.
|
| 1127 |
-
"""
|
| 1128 |
E, F = exp_bits, frac_bits
|
| 1129 |
P = E + F # payload width
|
| 1130 |
FRAME = F + 3 # mantissa + guard + round
|
|
@@ -1341,21 +972,7 @@ def add_float16_add(tensors: Dict[str, torch.Tensor]) -> None:
|
|
| 1341 |
|
| 1342 |
def add_float_mul(tensors: Dict[str, torch.Tensor], family: str,
|
| 1343 |
exp_bits: int, frac_bits: int) -> None:
|
| 1344 |
-
"""Add a complete float multiplication pipeline.
|
| 1345 |
-
|
| 1346 |
-
Self-contained circuit; external inputs are the raw operand words
|
| 1347 |
-
$a[0..W-1] / $b[0..W-1] (MSB-first). Contract: exact IEEE specials
|
| 1348 |
-
(NaN, infinities, signed zeros), subnormal operands and gradual-underflow
|
| 1349 |
-
subnormal results, round-to-nearest-even for the mantissa.
|
| 1350 |
-
|
| 1351 |
-
Datapath: (F+1)x(F+1) partial products accumulated shift-add into the
|
| 1352 |
-
2F+2-bit product; a leading-zero count and an E+2-bit exponent chain give
|
| 1353 |
-
exp_base = exp_a + exp_b - bias and a signed clamp min(nlz, exp_base); the
|
| 1354 |
-
product is right-shifted by (F+1) - that clamp (one shifter that normalizes
|
| 1355 |
-
normal results, denormalizes subnormal ones, and drives deeply underflowing
|
| 1356 |
-
products to zero), then round-to-nearest-even and a one-hot specials network
|
| 1357 |
-
produce the output word.
|
| 1358 |
-
"""
|
| 1359 |
E, F = exp_bits, frac_bits
|
| 1360 |
prefix = f"{family}.mul"
|
| 1361 |
|
|
@@ -1498,22 +1115,7 @@ def add_float16_mul(tensors: Dict[str, torch.Tensor]) -> None:
|
|
| 1498 |
|
| 1499 |
def add_float_div(tensors: Dict[str, torch.Tensor], family: str,
|
| 1500 |
exp_bits: int, frac_bits: int) -> None:
|
| 1501 |
-
"""Add a complete float division pipeline.
|
| 1502 |
-
|
| 1503 |
-
Self-contained circuit; external inputs are the raw operand words
|
| 1504 |
-
$a[0..W-1] / $b[0..W-1] (MSB-first). Contract: exact IEEE specials
|
| 1505 |
-
(NaN, infinities, signed zeros, x/0 -> inf, 0/0 and inf/inf -> NaN),
|
| 1506 |
-
subnormal operands and gradual-underflow subnormal results,
|
| 1507 |
-
round-to-nearest-even for the mantissa.
|
| 1508 |
-
|
| 1509 |
-
Datapath: each operand mantissa is pre-normalized (a leading-zero count
|
| 1510 |
-
drives a left-barrel so the leading 1 sits at bit F, and na = eexp - lz);
|
| 1511 |
-
F+3 restoring-division stages produce the quotient; a one-bit normalize mux
|
| 1512 |
-
aligns it on the integer quotient bit; two E+2-bit exponent chains give
|
| 1513 |
-
er = na - nb + bias - 1 + q0; the aligned mantissa frame is right-shifted by
|
| 1514 |
-
max(0, 1 - er) to denormalize subnormal quotients; then round-to-nearest-even
|
| 1515 |
-
and a one-hot specials network produce the output word.
|
| 1516 |
-
"""
|
| 1517 |
E, F = exp_bits, frac_bits
|
| 1518 |
prefix = f"{family}.div"
|
| 1519 |
NSTAGE = F + 3
|
|
@@ -1686,21 +1288,7 @@ def add_float16_div(tensors: Dict[str, torch.Tensor]) -> None:
|
|
| 1686 |
|
| 1687 |
def add_float_fma(tensors: Dict[str, torch.Tensor], family: str,
|
| 1688 |
exp_bits: int, frac_bits: int) -> None:
|
| 1689 |
-
"""Fused multiply-add: round(a*b + c) with a SINGLE rounding (F-extension).
|
| 1690 |
-
|
| 1691 |
-
Self-contained; external inputs are the raw operand words $a/$b/$c
|
| 1692 |
-
(MSB-first). Contract: exact IEEE specials (NaN, infinities, inf*0 and
|
| 1693 |
-
inf-inf -> NaN, signed zeros), subnormal operands and gradual-underflow
|
| 1694 |
-
subnormal results, round-to-nearest-even, single rounding of a*b+c.
|
| 1695 |
-
|
| 1696 |
-
Datapath: each mantissa is pre-normalized (leading 1 at bit F, na=eexp-lz);
|
| 1697 |
-
the (F+1)x(F+1) product Mp (2F+2 bits) and the addend Mc are right-shifted by
|
| 1698 |
-
sticky barrel shifters into a common 3(F+1)+4-bit field anchored at
|
| 1699 |
-
max(product,c) exponent; a signed magnitude add (with a borrow when the
|
| 1700 |
-
shifted-out subtrahend leaves a tail) forms the sum; a leading-zero count and
|
| 1701 |
-
normalize give the significand, one round-to-nearest-even rounds it, and a
|
| 1702 |
-
one-hot specials network packs the result.
|
| 1703 |
-
"""
|
| 1704 |
E, F = exp_bits, frac_bits
|
| 1705 |
W = 1 + E + F
|
| 1706 |
p = F + 1
|
|
@@ -1981,18 +1569,7 @@ def add_float_fma(tensors: Dict[str, torch.Tensor], family: str,
|
|
| 1981 |
|
| 1982 |
def add_float_cmp(tensors: Dict[str, torch.Tensor], family: str,
|
| 1983 |
exp_bits: int, frac_bits: int) -> None:
|
| 1984 |
-
"""Add a complete IEEE 754 comparison network (EQ, LT, LE, GT, GE).
|
| 1985 |
-
|
| 1986 |
-
Self-contained circuit: external inputs are the raw operand words
|
| 1987 |
-
$a[0..W-1], $b[0..W-1] (MSB-first; bit 0 is the sign, bits 1..E the
|
| 1988 |
-
exponent, the rest the fraction). Composition:
|
| 1989 |
-
|
| 1990 |
-
1. NaN detection gates every result (any comparison with NaN is false).
|
| 1991 |
-
2. both_zero makes +0 == -0 (and suppresses the mixed-sign LT/GT).
|
| 1992 |
-
3. Same signs: IEEE encodings order like sign-magnitude integers, so the
|
| 1993 |
-
payload magnitude comparator decides, inverted for negatives.
|
| 1994 |
-
4. Different signs: the negative operand is smaller (unless both zero).
|
| 1995 |
-
"""
|
| 1996 |
prefix = f"{family}.cmp"
|
| 1997 |
payload = exp_bits + frac_bits
|
| 1998 |
|
|
@@ -2080,13 +1657,7 @@ def add_float16_cmp(tensors: Dict[str, torch.Tensor]) -> None:
|
|
| 2080 |
|
| 2081 |
|
| 2082 |
def add_float32_core(tensors: Dict[str, torch.Tensor]) -> None:
|
| 2083 |
-
"""Add float32 core circuits (unpack, pack, classify).
|
| 2084 |
-
|
| 2085 |
-
IEEE 754 single-precision format (32 bits):
|
| 2086 |
-
- Bit 31: Sign
|
| 2087 |
-
- Bits 30-23: Exponent (8 bits, bias=127)
|
| 2088 |
-
- Bits 22-0: Mantissa (23 bits, implicit leading 1)
|
| 2089 |
-
"""
|
| 2090 |
prefix = "float32"
|
| 2091 |
|
| 2092 |
# Identity buffers: H(x - 1) reproduces a binary input (bias 0 would
|
|
@@ -2125,13 +1696,7 @@ def add_float32_div(tensors: Dict[str, torch.Tensor]) -> None:
|
|
| 2125 |
add_float_div(tensors, "float32", 8, 23)
|
| 2126 |
|
| 2127 |
def update_manifest(tensors: Dict[str, torch.Tensor], data_bits: int, addr_bits: int, mem_bytes: int) -> None:
|
| 2128 |
-
"""Update manifest metadata tensors.
|
| 2129 |
-
|
| 2130 |
-
Args:
|
| 2131 |
-
data_bits: ALU/register width (8/16/32)
|
| 2132 |
-
addr_bits: Address bus width (determines memory size)
|
| 2133 |
-
mem_bytes: Memory size in bytes (2^addr_bits)
|
| 2134 |
-
"""
|
| 2135 |
tensors["manifest.data_bits"] = torch.tensor([float(data_bits)], dtype=torch.float32)
|
| 2136 |
tensors["manifest.addr_bits"] = torch.tensor([float(addr_bits)], dtype=torch.float32)
|
| 2137 |
tensors["manifest.memory_bytes"] = torch.tensor([float(mem_bytes)], dtype=torch.float32)
|
|
@@ -2228,16 +1793,7 @@ def infer_ripplecarry_inputs(gate: str, prefix: str, bits: int, reg: SignalRegis
|
|
| 2228 |
|
| 2229 |
|
| 2230 |
def infer_expr_add_mul_inputs(gate: str, reg: SignalRegistry) -> List[int]:
|
| 2231 |
-
"""Infer inputs for A + B × C expression circuit (order of operations).
|
| 2232 |
-
|
| 2233 |
-
Circuit structure:
|
| 2234 |
-
- Mask stage: mask.s[stage].b[bit] = B[bit] AND C[stage]
|
| 2235 |
-
- Accumulator stages 1-7: acc.s[stage] = acc.s[stage-1] + (mask.s[stage] << stage)
|
| 2236 |
-
- Final add: result = A + acc.s7
|
| 2237 |
-
|
| 2238 |
-
Bit ordering: MSB-first externally, LSB-first internally (fa0 = LSB, fa7 = MSB)
|
| 2239 |
-
- $x[7] = bit 0 (LSB), $x[0] = bit 7 (MSB)
|
| 2240 |
-
"""
|
| 2241 |
prefix = "arithmetic.expr_add_mul"
|
| 2242 |
|
| 2243 |
# Register all inputs
|
|
@@ -2348,15 +1904,7 @@ def infer_expr_add_mul_inputs(gate: str, reg: SignalRegistry) -> List[int]:
|
|
| 2348 |
|
| 2349 |
|
| 2350 |
def infer_expr_paren_add_mul_inputs(gate: str, reg: SignalRegistry) -> List[int]:
|
| 2351 |
-
"""Infer inputs for (A + B) × C expression circuit (parenthetical override).
|
| 2352 |
-
|
| 2353 |
-
Circuit structure:
|
| 2354 |
-
- Add stage: sum = A + B
|
| 2355 |
-
- Mask stage: mask.s[stage].b[bit] = sum[bit] AND C[stage]
|
| 2356 |
-
- Accumulator stages 1-7: acc.s[stage] = acc.s[stage-1] + (mask.s[stage] << stage)
|
| 2357 |
-
|
| 2358 |
-
Bit ordering: MSB-first externally, LSB-first internally (fa0 = LSB, fa7 = MSB)
|
| 2359 |
-
"""
|
| 2360 |
prefix = "arithmetic.expr_paren_add_mul"
|
| 2361 |
|
| 2362 |
# Register all inputs
|
|
@@ -2463,15 +2011,7 @@ def infer_expr_paren_add_mul_inputs(gate: str, reg: SignalRegistry) -> List[int]
|
|
| 2463 |
|
| 2464 |
|
| 2465 |
def infer_expr_paren_inputs(gate: str, reg: SignalRegistry) -> List[int]:
|
| 2466 |
-
"""Infer inputs for (A + B) × C expression circuit (parenthetical grouping).
|
| 2467 |
-
|
| 2468 |
-
Circuit structure:
|
| 2469 |
-
- Add stage: sum = A + B
|
| 2470 |
-
- Mask stage: mask.s[stage].b[bit] = sum[bit] AND C[stage]
|
| 2471 |
-
- Accumulator stages 1-7: acc.s[stage] = acc.s[stage-1] + (mask.s[stage] << stage)
|
| 2472 |
-
|
| 2473 |
-
Bit ordering: MSB-first externally, LSB-first internally (fa0 = LSB, fa7 = MSB)
|
| 2474 |
-
"""
|
| 2475 |
prefix = "arithmetic.expr_paren"
|
| 2476 |
|
| 2477 |
# Register all inputs
|
|
@@ -3117,11 +2657,7 @@ def infer_combinational_inputs(gate: str, reg: SignalRegistry, tensors: Dict[str
|
|
| 3117 |
|
| 3118 |
def _infer_bit_cascade(gate: str, reg: SignalRegistry, cmp_prefix: str,
|
| 3119 |
a_template: str, b_template: str) -> List[int] | None:
|
| 3120 |
-
"""If `gate` is part of the bit-cascade comparator at `cmp_prefix`, return
|
| 3121 |
-
its inputs. `a_template` / `b_template` are format strings like
|
| 3122 |
-
"$a[{}]" / "$b[{}]" that produce the per-bit input signal names. Returns
|
| 3123 |
-
None if `gate` isn't a member of this cascade.
|
| 3124 |
-
"""
|
| 3125 |
if not gate.startswith(cmp_prefix + "."):
|
| 3126 |
return None
|
| 3127 |
suffix = gate[len(cmp_prefix) + 1:]
|
|
@@ -3161,10 +2697,7 @@ def _infer_bit_cascade(gate: str, reg: SignalRegistry, cmp_prefix: str,
|
|
| 3161 |
def _infer_compare_final(gate: str, reg: SignalRegistry, cmp_prefix: str,
|
| 3162 |
out_gt: str, out_lt: str, out_ge: str, out_le: str,
|
| 3163 |
out_eq: str, bits: int) -> List[int] | None:
|
| 3164 |
-
"""Inputs
|
| 3165 |
-
out_gt/out_lt are N-input ORs; out_eq is N-input AND; ge/le are
|
| 3166 |
-
NOT-then-buffer chains over lt/gt.
|
| 3167 |
-
"""
|
| 3168 |
if gate == out_gt:
|
| 3169 |
return ([reg.register(f"{cmp_prefix}.bit0.gt")]
|
| 3170 |
+ [reg.register(f"{cmp_prefix}.cascade.gt.bit{i}") for i in range(1, bits)])
|
|
@@ -3186,12 +2719,7 @@ def _infer_compare_final(gate: str, reg: SignalRegistry, cmp_prefix: str,
|
|
| 3186 |
|
| 3187 |
def infer_float_cmp_inputs(gate: str, reg: SignalRegistry, family: str,
|
| 3188 |
exp_bits: int, frac_bits: int) -> Optional[List[int]]:
|
| 3189 |
-
"""Complete wiring for the self-contained float comparison network.
|
| 3190 |
-
|
| 3191 |
-
External inputs are the raw operand words $a[0..W-1] / $b[0..W-1],
|
| 3192 |
-
MSB-first: bit 0 is the sign, bits 1..E the exponent, the rest the
|
| 3193 |
-
fraction. Field extraction is input wiring, not gates.
|
| 3194 |
-
"""
|
| 3195 |
prefix = f"{family}.cmp"
|
| 3196 |
if gate != prefix and not gate.startswith(prefix + "."):
|
| 3197 |
return None
|
|
@@ -3306,8 +2834,7 @@ def infer_float_cmp_inputs(gate: str, reg: SignalRegistry, family: str,
|
|
| 3306 |
|
| 3307 |
def _fa_member_inputs(sub: str, fa_prefix: str, a_sig: int, b_sig: int,
|
| 3308 |
cin_sig: int, reg: SignalRegistry) -> Optional[List[int]]:
|
| 3309 |
-
"""Wiring for one gate of an add_full_adder cell, given the cell's
|
| 3310 |
-
operand/carry signal IDs. `sub` is the gate suffix inside the cell."""
|
| 3311 |
if sub == "ha1.sum.layer1.or" or sub == "ha1.sum.layer1.nand":
|
| 3312 |
return [a_sig, b_sig]
|
| 3313 |
if sub == "ha1.sum.layer2":
|
|
@@ -3330,13 +2857,7 @@ def _fa_member_inputs(sub: str, fa_prefix: str, a_sig: int, b_sig: int,
|
|
| 3330 |
|
| 3331 |
def infer_float_mul_inputs(gate: str, reg: SignalRegistry, family: str,
|
| 3332 |
exp_bits: int, frac_bits: int) -> Optional[List[int]]:
|
| 3333 |
-
"""Complete wiring for the self-contained float multiplication pipeline.
|
| 3334 |
-
|
| 3335 |
-
Operand words $a/$b are MSB-first. Internally the mantissa and exponent
|
| 3336 |
-
signals are LSB-first: mantissa bit k of `a` is $a[W-1-k] for k < F with
|
| 3337 |
-
the implicit leading one (index F) wired to #1; exponent bit k is
|
| 3338 |
-
$a[E-k]. The accumulated product bit k is acc.s{F-1}.fa{k}'s sum.
|
| 3339 |
-
"""
|
| 3340 |
E, F = exp_bits, frac_bits
|
| 3341 |
W = 1 + E + F
|
| 3342 |
prefix = f"{family}.mul"
|
|
@@ -3613,14 +3134,7 @@ def infer_float_mul_inputs(gate: str, reg: SignalRegistry, family: str,
|
|
| 3613 |
|
| 3614 |
def infer_float_div_inputs(gate: str, reg: SignalRegistry, family: str,
|
| 3615 |
exp_bits: int, frac_bits: int) -> Optional[List[int]]:
|
| 3616 |
-
"""Complete wiring for the self-contained float division pipeline.
|
| 3617 |
-
|
| 3618 |
-
Stage 0 compares the dividend mantissa against the divisor directly;
|
| 3619 |
-
every later stage shifts the previous remainder left by one (the
|
| 3620 |
-
shifted-out top bit forces the quotient bit through the stage OR) and
|
| 3621 |
-
conditionally subtracts. The quotient bit of stage s is
|
| 3622 |
-
mant_div.stage{s}.q; the remainder bits are the stage mux outputs.
|
| 3623 |
-
"""
|
| 3624 |
E, F = exp_bits, frac_bits
|
| 3625 |
W = 1 + E + F
|
| 3626 |
prefix = f"{family}.div"
|
|
@@ -5009,15 +4523,7 @@ def infer_float_add_inputs(gate: str, reg: SignalRegistry, family: str,
|
|
| 5009 |
|
| 5010 |
|
| 5011 |
def add_subleq_core(tensors: Dict[str, torch.Tensor]) -> None:
|
| 5012 |
-
"""Add the SUBLEQ one-instruction datapath.
|
| 5013 |
-
|
| 5014 |
-
The machine: M[B] -= M[A]; branch to C when the result is <= 0 (two's
|
| 5015 |
-
complement). External inputs, LSB-first: $a (subtrahend M[A]), $b
|
| 5016 |
-
(minuend M[B]), $pc (current PC), $c (branch target). Outputs: the new
|
| 5017 |
-
M[B] on sub.fa{k} sums, and the next PC on pc_mux.bit{k}.or. The one
|
| 5018 |
-
architectural decision -- branch or fall through -- is the single
|
| 5019 |
-
threshold neuron subleq.leq over (sign, is-zero).
|
| 5020 |
-
"""
|
| 5021 |
for k in range(8):
|
| 5022 |
add_gate(tensors, f"subleq.sub.not_a.bit{k}", [-1.0], [0.0])
|
| 5023 |
add_full_adder(tensors, f"subleq.sub.fa{k}")
|
|
@@ -5085,12 +4591,7 @@ def infer_subleq_inputs(gate: str, reg: SignalRegistry) -> Optional[List[int]]:
|
|
| 5085 |
|
| 5086 |
|
| 5087 |
def infer_rv32_inputs(gate: str, reg: SignalRegistry) -> Optional[List[int]]:
|
| 5088 |
-
"""
|
| 5089 |
-
accumulator (fed by the shared 32-bit partial products), the SRA select
|
| 5090 |
-
muxes, the NEUR neuron datapath, and the dual-issue hazard comparators.
|
| 5091 |
-
All operand-side ports are external ($rv32_...); internal chains resolve
|
| 5092 |
-
to gate outputs, so each sub-circuit's netlist is recoverable in full.
|
| 5093 |
-
"""
|
| 5094 |
if not gate.startswith("rv32."):
|
| 5095 |
return None
|
| 5096 |
|
|
@@ -5244,10 +4745,7 @@ def infer_rv32_inputs(gate: str, reg: SignalRegistry) -> Optional[List[int]]:
|
|
| 5244 |
|
| 5245 |
|
| 5246 |
def _infer_threshold_computer_bit_cascade(gate: str, reg: SignalRegistry) -> List[int] | None:
|
| 5247 |
-
"""Match every bit-cascade location
|
| 5248 |
-
comparators, integer division stage cmps, float magnitude comparators,
|
| 5249 |
-
float exp_cmp, and float division mantissa cmps.
|
| 5250 |
-
"""
|
| 5251 |
# Integer arithmetic.cmp{N}bit.* + the public greaterthan/lessthan/etc.
|
| 5252 |
m = re.match(r"^arithmetic\.cmp(\d+)bit\..+", gate)
|
| 5253 |
if m:
|
|
@@ -5315,16 +4813,7 @@ _ROUTING_TABLE_CACHE: Dict[str, Any] | None = None
|
|
| 5315 |
|
| 5316 |
|
| 5317 |
def _load_routing_table() -> Dict[str, Any]:
|
| 5318 |
-
"""Load `routing/routing.json` once; on missing file, return {}.
|
| 5319 |
-
|
| 5320 |
-
The JSON maps each top-level circuit name (e.g. ``arithmetic.multiplier8x8``)
|
| 5321 |
-
to a dict with at least ``inputs`` (top-level external ports) and
|
| 5322 |
-
``internal`` (gate-suffix -> list-of-source-signal-name). The source
|
| 5323 |
-
signal names are either external port references (``$a[3]``), constants
|
| 5324 |
-
(``#0`` / ``#1``), or relative gate names within the same circuit
|
| 5325 |
-
(e.g. ``pp.r0.c3``). Anchored cross-circuit references are not
|
| 5326 |
-
expressed here; circuits in the table are assumed self-contained.
|
| 5327 |
-
"""
|
| 5328 |
global _ROUTING_TABLE_CACHE
|
| 5329 |
if _ROUTING_TABLE_CACHE is not None:
|
| 5330 |
return _ROUTING_TABLE_CACHE
|
|
@@ -5342,16 +4831,7 @@ def _infer_from_routing_table(
|
|
| 5342 |
gate: str, reg: SignalRegistry,
|
| 5343 |
known_gate_suffixes: Optional[Set[str]] = None,
|
| 5344 |
) -> Optional[List[int]]:
|
| 5345 |
-
"""Resolve a gate's inputs by consulting routing.json.
|
| 5346 |
-
|
| 5347 |
-
``known_gate_suffixes`` is the set of gate-name suffixes that actually
|
| 5348 |
-
exist in the loaded safetensors (within the same circuit prefix).
|
| 5349 |
-
When the routing JSON names a producer like ``stage0.bit0.ha2.sum``
|
| 5350 |
-
but that suffix isn't a real gate (only ``stage0.bit0.ha2.sum.layer2``
|
| 5351 |
-
is), this set is consulted to redirect the reference to the canonical
|
| 5352 |
-
final-layer signal. Returns None when the gate's circuit isn't
|
| 5353 |
-
covered or its suffix isn't in the routing's ``internal`` map.
|
| 5354 |
-
"""
|
| 5355 |
table = _load_routing_table()
|
| 5356 |
if not table:
|
| 5357 |
return None
|
|
@@ -5451,7 +4931,7 @@ def infer_inputs_for_gate(gate: str, reg: SignalRegistry, tensors: Dict[str, tor
|
|
| 5451 |
return fc
|
| 5452 |
# Bit-cascade comparators (integer + float + div) and ternary modular detectors.
|
| 5453 |
# These are the gates emitted by add_bit_cascade_compare and the ternary
|
| 5454 |
-
# modular rebuild in quantize
|
| 5455 |
# through to the generic per-family handlers below.
|
| 5456 |
bc = _infer_threshold_computer_bit_cascade(gate, reg)
|
| 5457 |
if bc is not None:
|
|
@@ -6121,8 +5601,7 @@ def cmd_alu(args) -> None:
|
|
| 6121 |
|
| 6122 |
|
| 6123 |
def cmd_subleq(args) -> None:
|
| 6124 |
-
"""Build the standalone SUBLEQ-8 machine file
|
| 6125 |
-
packed memory circuits plus the one-instruction datapath, fully wired."""
|
| 6126 |
out = args.model if args.model else MODEL_DIR / "variants" / "neural_subleq8.safetensors"
|
| 6127 |
print("=" * 60)
|
| 6128 |
print(" BUILD SUBLEQ-8 (one-instruction threshold machine)")
|
|
@@ -6153,10 +5632,7 @@ def cmd_subleq(args) -> None:
|
|
| 6153 |
|
| 6154 |
|
| 6155 |
def add_rv32_extras(tensors: Dict[str, torch.Tensor]) -> None:
|
| 6156 |
-
"""RV32
|
| 6157 |
-
product accumulator for MULH*, arithmetic-right-shift select muxes,
|
| 6158 |
-
the NEUR neuron-evaluation datapath, and dual-issue hazard comparators.
|
| 6159 |
-
"""
|
| 6160 |
# 64-bit shift-add accumulator over the 32x32 partial products.
|
| 6161 |
for stage in range(31):
|
| 6162 |
for bit in range(64):
|
|
@@ -6195,9 +5671,11 @@ def add_rv32_extras(tensors: Dict[str, torch.Tensor]) -> None:
|
|
| 6195 |
RV32_OPCLASS = {0x33: "op", 0x13: "op_imm", 0x03: "load", 0x23: "store",
|
| 6196 |
0x63: "branch", 0x37: "lui", 0x17: "auipc", 0x6F: "jal",
|
| 6197 |
0x67: "jalr", 0x73: "system", 0x53: "fp", 0x07: "flw", 0x27: "fsw"}
|
|
|
|
| 6198 |
# Which opcode classes select each immediate format.
|
| 6199 |
RV32_USE = {"I": ["op_imm", "load", "jalr", "flw"], "S": ["store", "fsw"],
|
| 6200 |
"B": ["branch"], "U": ["lui", "auipc"], "J": ["jal"]}
|
|
|
|
| 6201 |
RV32_IMM_FMTS = ("I", "S", "B", "U", "J")
|
| 6202 |
|
| 6203 |
|
|
@@ -6235,10 +5713,7 @@ def _rv32_imm_src(fmt: str, k: int):
|
|
| 6235 |
|
| 6236 |
|
| 6237 |
def add_rv32_decode(tensors: Dict[str, torch.Tensor]) -> None:
|
| 6238 |
-
"""
|
| 6239 |
-
7-bit match on the instruction's opcode field) and the sign-extended
|
| 6240 |
-
immediate for the active format, muxed from the raw instruction word. The
|
| 6241 |
-
runtime reads these gate outputs instead of slicing the word in Python."""
|
| 6242 |
for opc, name in RV32_OPCLASS.items():
|
| 6243 |
w = [1.0 if (opc >> j) & 1 else -1.0 for j in range(7)]
|
| 6244 |
add_gate(tensors, f"rv32.decode.is_{name}", w, [-float(bin(opc).count("1"))])
|
|
@@ -6254,10 +5729,7 @@ def add_rv32_decode(tensors: Dict[str, torch.Tensor]) -> None:
|
|
| 6254 |
|
| 6255 |
|
| 6256 |
def add_rv32_pcnext(tensors: Dict[str, torch.Tensor]) -> None:
|
| 6257 |
-
"""
|
| 6258 |
-
among PC+4, PC+imm (branch/jal), and (rs1+imm)&~1 (jalr). The three
|
| 6259 |
-
candidates are the gate adder's outputs; this network chooses between them
|
| 6260 |
-
from the decode's control signals rather than a Python if/elif."""
|
| 6261 |
for k in range(32):
|
| 6262 |
# m1 = sel_target ? pcimm : pc4
|
| 6263 |
add_gate(tensors, f"rv32.pcnext.m1.bit{k}.not_sel", [-1.0], [0.0])
|
|
@@ -6272,9 +5744,7 @@ def add_rv32_pcnext(tensors: Dict[str, torch.Tensor]) -> None:
|
|
| 6272 |
|
| 6273 |
|
| 6274 |
def cmd_rv32(args) -> None:
|
| 6275 |
-
"""Build the standalone RV32
|
| 6276 |
-
64 KB packed memory, the shared 32-bit circuit families, the composed
|
| 6277 |
-
float32 pipelines as the FPU, and the rv32-specific extras."""
|
| 6278 |
out = args.model if args.model else MODEL_DIR / "variants" / "neural_rv32.safetensors"
|
| 6279 |
print("=" * 60)
|
| 6280 |
print(" BUILD NEURAL_RV32 (RV32IM + F-subset threshold processor)")
|
|
@@ -6458,6 +5928,528 @@ Usage (note: options must come BEFORE subcommand):
|
|
| 6458 |
else:
|
| 6459 |
parser.print_help()
|
| 6460 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6461 |
|
| 6462 |
if __name__ == "__main__":
|
| 6463 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Builds the threshold computers, quantizes them, and builds every named variant."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
from __future__ import annotations
|
| 3 |
|
| 4 |
import argparse
|
| 5 |
import json
|
| 6 |
+
import os
|
| 7 |
import re
|
| 8 |
+
import shutil
|
| 9 |
+
import subprocess
|
| 10 |
+
import sys
|
| 11 |
+
import time
|
| 12 |
from pathlib import Path
|
| 13 |
+
from typing import Any, Dict, Iterable, List, Optional, Set, Tuple
|
| 14 |
|
| 15 |
import torch
|
| 16 |
from safetensors import safe_open
|
|
|
|
| 45 |
|
| 46 |
|
| 47 |
MODEL_PATH = MODEL_DIR / "neural_computer8.safetensors"
|
| 48 |
+
|
| 49 |
MANIFEST_PATH = Path(__file__).resolve().parent.parent / "tensors.txt"
|
| 50 |
|
| 51 |
DEFAULT_ADDR_BITS = 16
|
| 52 |
+
|
| 53 |
DEFAULT_MEM_BYTES = 1 << DEFAULT_ADDR_BITS
|
| 54 |
|
| 55 |
MEMORY_PROFILES = {
|
|
|
|
| 73 |
|
| 74 |
|
| 75 |
def load_file_metadata(path: Path) -> Dict[str, str]:
|
| 76 |
+
"""Read the safetensors header metadata so every save carries it forward."""
|
|
|
|
| 77 |
with safe_open(str(path), framework="pt") as f:
|
| 78 |
meta = f.metadata()
|
| 79 |
return dict(meta) if meta else {}
|
|
|
|
| 170 |
|
| 171 |
|
| 172 |
def add_fetch_load_store_buffers(tensors: Dict[str, torch.Tensor], data_bits: int, addr_bits: int) -> None:
|
| 173 |
+
"""Add control buffers for fetch, load, store operations."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 174 |
# Instruction register width = opcode (8) + operands (depends on arch)
|
| 175 |
# For simplicity, IR width = max(16, addr_bits) to hold jump targets
|
| 176 |
ir_bits = max(16, addr_bits)
|
|
|
|
| 184 |
|
| 185 |
|
| 186 |
def add_full_adder(tensors: Dict[str, torch.Tensor], prefix: str) -> None:
|
| 187 |
+
"""Add a single full adder at the given prefix."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 188 |
# XOR for ha1.sum (2-layer: OR + NAND -> AND)
|
| 189 |
add_gate(tensors, f"{prefix}.ha1.sum.layer1.or", [1.0, 1.0], [-1.0])
|
| 190 |
add_gate(tensors, f"{prefix}.ha1.sum.layer1.nand", [-1.0, -1.0], [1.0])
|
|
|
|
| 202 |
|
| 203 |
|
| 204 |
def add_expr_add_mul(tensors: Dict[str, torch.Tensor]) -> None:
|
| 205 |
+
"""Add expression circuit for A + B × C (order of operations)."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 206 |
prefix = "arithmetic.expr_add_mul"
|
| 207 |
|
| 208 |
# Stage 1: Multiply B × C using shift-add
|
|
|
|
| 238 |
|
| 239 |
|
| 240 |
def add_expr_paren_add_mul(tensors: Dict[str, torch.Tensor]) -> None:
|
| 241 |
+
"""Add expression circuit for (A + B) × C (parenthetical override)."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 242 |
prefix = "arithmetic.expr_paren_add_mul"
|
| 243 |
|
| 244 |
# Stage 1: Add A + B
|
|
|
|
| 258 |
|
| 259 |
|
| 260 |
def add_expr_paren(tensors: Dict[str, torch.Tensor]) -> None:
|
| 261 |
+
"""Add expression circuit for (A + B) × C (parenthetical grouping)."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 262 |
prefix = "arithmetic.expr_paren"
|
| 263 |
|
| 264 |
# Stage 1: Add A + B
|
|
|
|
| 278 |
|
| 279 |
|
| 280 |
def add_add3(tensors: Dict[str, torch.Tensor]) -> None:
|
| 281 |
+
"""Add 3-operand 8-bit adder circuit."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 282 |
# Stage 1: A + B -> temp
|
| 283 |
for bit in range(8):
|
| 284 |
add_full_adder(tensors, f"arithmetic.add3_8bit.stage1.fa{bit}")
|
|
|
|
| 289 |
|
| 290 |
|
| 291 |
def add_shl_shr(tensors: Dict[str, torch.Tensor]) -> None:
|
| 292 |
+
"""Add SHL (shift left) and SHR (shift right) circuits."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 293 |
for bit in range(8):
|
| 294 |
if bit < 7:
|
| 295 |
add_gate(tensors, f"alu.alu8bit.shl.bit{bit}", [1.0], [-1.0])
|
|
|
|
| 304 |
|
| 305 |
|
| 306 |
def add_mul(tensors: Dict[str, torch.Tensor]) -> None:
|
| 307 |
+
"""Add 8-bit multiplication circuit."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 308 |
# AND gates for partial products: P[i][j] = A[i] AND B[j]
|
| 309 |
# These compute whether bit i of A and bit j of B are both 1
|
| 310 |
for i in range(8):
|
|
|
|
| 313 |
|
| 314 |
|
| 315 |
def add_div(tensors: Dict[str, torch.Tensor]) -> None:
|
| 316 |
+
"""Add 8-bit division circuit."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 317 |
# Comparison gates: check if (remainder << 1 | next_bit) >= divisor
|
| 318 |
# Bit-cascaded ternary GE comparator per stage.
|
| 319 |
for stage in range(8):
|
|
|
|
| 341 |
|
| 342 |
|
| 343 |
def add_inc_dec(tensors: Dict[str, torch.Tensor]) -> None:
|
| 344 |
+
"""Add INC and DEC circuits."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 345 |
# INC: half adder chain starting with carry_in = 1
|
| 346 |
# bit 7 (LSB): XOR with 1, carry = bit[7]
|
| 347 |
# bit 6: XOR with carry, new_carry = bit[6] AND old_carry
|
|
|
|
| 368 |
|
| 369 |
|
| 370 |
def add_neg(tensors: Dict[str, torch.Tensor]) -> None:
|
| 371 |
+
"""Add NEG circuit (two's complement negation)."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 372 |
for bit in range(8):
|
| 373 |
# NOT gate for each bit
|
| 374 |
add_gate(tensors, f"alu.alu8bit.neg.not.bit{bit}", [-1.0], [0.0])
|
|
|
|
| 385 |
|
| 386 |
|
| 387 |
def add_rol_ror_nbits(tensors: Dict[str, torch.Tensor], bits: int) -> None:
|
| 388 |
+
"""Add N-bit ROL and ROR circuits (rotate left/right)."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 389 |
# ROL: rotate left (toward MSB)
|
| 390 |
for bit in range(bits):
|
| 391 |
add_gate(tensors, f"alu.alu{bits}bit.rol.bit{bit}", [1.0], [-1.0])
|
|
|
|
| 396 |
|
| 397 |
|
| 398 |
def add_stack_ops(tensors: Dict[str, torch.Tensor], data_bits: int, addr_bits: int) -> None:
|
| 399 |
+
"""Add RET, PUSH, POP circuit components."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 400 |
# SP decrement for PUSH (addr_bits wide)
|
| 401 |
for bit in range(addr_bits):
|
| 402 |
add_gate(tensors, f"control.push.sp_dec.bit{bit}.xor.layer1.or", [1.0, 1.0], [-1.0])
|
|
|
|
| 425 |
|
| 426 |
|
| 427 |
def add_conditional_jumps(tensors: Dict[str, torch.Tensor], addr_bits: int) -> None:
|
| 428 |
+
"""Add conditional jump circuits (JZ, JNZ, JC, JNC, JP, JN, JV, JNV)."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 429 |
jump_types = ['jz', 'jnz', 'jc', 'jnc', 'jp', 'jn', 'jv', 'jnv']
|
| 430 |
|
| 431 |
for jmp in jump_types:
|
|
|
|
| 442 |
|
| 443 |
|
| 444 |
def add_computer_decode(tensors: Dict[str, torch.Tensor]) -> None:
|
| 445 |
+
"""The 4-to-16 opcode one-hot decoder of the 8-bit computer."""
|
|
|
|
|
|
|
| 446 |
for opc in range(16):
|
| 447 |
w = [1.0 if (opc >> j) & 1 else -1.0 for j in range(4)]
|
| 448 |
add_gate(tensors, f"control.decode.op{opc}", w, [-float(bin(opc).count("1"))])
|
| 449 |
|
| 450 |
|
| 451 |
def add_computer_pcnext(tensors: Dict[str, torch.Tensor], addr_bits: int) -> None:
|
| 452 |
+
"""Next-PC selection of the 8-bit computer among PC+2, PC+4, the jump mux and the direct target."""
|
|
|
|
|
|
|
|
|
|
| 453 |
for name, start in (("pc2", 1), ("pc4", 2)):
|
| 454 |
for bit in range(addr_bits):
|
| 455 |
if bit < start:
|
|
|
|
| 469 |
|
| 470 |
|
| 471 |
def add_status_flags(tensors: Dict[str, torch.Tensor], data_bits: int) -> None:
|
| 472 |
+
"""Add status flag computation circuits (Z, N, C, V)."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 473 |
# Z flag: NOR of all bits (result == 0)
|
| 474 |
# Single threshold gate: fires if sum of all bits < 1
|
| 475 |
add_gate(tensors, "flags.zero", [-1.0] * data_bits, [0.0])
|
|
|
|
| 493 |
|
| 494 |
|
| 495 |
def add_barrel_shifter_nbits(tensors: Dict[str, torch.Tensor], bits: int) -> None:
|
| 496 |
+
"""Add N-bit barrel shifter circuit."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 497 |
import math
|
| 498 |
num_layers = max(1, math.ceil(math.log2(bits)))
|
| 499 |
|
|
|
|
| 514 |
|
| 515 |
|
| 516 |
def add_priority_encoder_nbits(tensors: Dict[str, torch.Tensor], bits: int) -> None:
|
| 517 |
+
"""Add N-bit priority encoder circuit."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 518 |
import math
|
| 519 |
out_bits = max(1, math.ceil(math.log2(bits)))
|
| 520 |
prefix = f"combinational.priorityencoder{bits}"
|
|
|
|
| 558 |
out_le: str,
|
| 559 |
out_eq: str,
|
| 560 |
) -> None:
|
| 561 |
+
"""Generic ternary-only N-bit comparator."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 562 |
for i in range(bits):
|
| 563 |
# per-bit GT: A[i] AND NOT B[i] -> H(A - B - 1)
|
| 564 |
add_gate(tensors, f"{cmp_prefix}.bit{i}.gt", [1.0, -1.0], [-1.0])
|
|
|
|
| 598 |
|
| 599 |
|
| 600 |
def add_comparators(tensors: Dict[str, torch.Tensor]) -> None:
|
| 601 |
+
"""Add 8-bit comparator circuits (GT, LT, GE, LE, EQ) using bit-cascade."""
|
|
|
|
|
|
|
|
|
|
|
|
|
| 602 |
add_bit_cascade_compare(
|
| 603 |
tensors,
|
| 604 |
cmp_prefix="arithmetic.cmp8bit",
|
|
|
|
| 612 |
|
| 613 |
|
| 614 |
def add_ripple_carry_nbits(tensors: Dict[str, torch.Tensor], bits: int) -> None:
|
| 615 |
+
"""Add N-bit ripple carry adder circuit."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 616 |
prefix = f"arithmetic.ripplecarry{bits}bit"
|
| 617 |
for bit in range(bits):
|
| 618 |
add_full_adder(tensors, f"{prefix}.fa{bit}")
|
| 619 |
|
| 620 |
|
| 621 |
def add_sub_nbits(tensors: Dict[str, torch.Tensor], bits: int) -> None:
|
| 622 |
+
"""Add N-bit subtractor circuit (A - B)."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 623 |
prefix = f"arithmetic.sub{bits}bit"
|
| 624 |
|
| 625 |
for bit in range(bits):
|
|
|
|
| 630 |
|
| 631 |
|
| 632 |
def add_comparators_nbits(tensors: Dict[str, torch.Tensor], bits: int) -> None:
|
| 633 |
+
"""Add N-bit comparator circuits (GT, LT, GE, LE, EQ) via bit-cascade."""
|
|
|
|
|
|
|
|
|
|
| 634 |
add_bit_cascade_compare(
|
| 635 |
tensors,
|
| 636 |
cmp_prefix=f"arithmetic.cmp{bits}bit",
|
|
|
|
| 644 |
|
| 645 |
|
| 646 |
def add_mul_nbits(tensors: Dict[str, torch.Tensor], bits: int) -> None:
|
| 647 |
+
"""Add N-bit multiplication circuit."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 648 |
for i in range(bits):
|
| 649 |
for j in range(bits):
|
| 650 |
add_gate(tensors, f"alu.alu{bits}bit.mul.pp.a{i}b{j}", [1.0, 1.0], [-2.0])
|
| 651 |
|
| 652 |
|
| 653 |
def add_div_nbits(tensors: Dict[str, torch.Tensor], bits: int) -> None:
|
| 654 |
+
"""Add N-bit division circuit."""
|
|
|
|
|
|
|
|
|
|
|
|
|
| 655 |
for stage in range(bits):
|
| 656 |
add_bit_cascade_compare(
|
| 657 |
tensors,
|
|
|
|
| 673 |
|
| 674 |
|
| 675 |
def add_bitwise_nbits(tensors: Dict[str, torch.Tensor], bits: int) -> None:
|
| 676 |
+
"""Add N-bit bitwise operation circuits (AND, OR, XOR, NOT)."""
|
|
|
|
|
|
|
|
|
|
| 677 |
for bit in range(bits):
|
| 678 |
add_gate(tensors, f"alu.alu{bits}bit.and.bit{bit}", [1.0, 1.0], [-2.0])
|
| 679 |
|
|
|
|
| 690 |
|
| 691 |
|
| 692 |
def add_shift_nbits(tensors: Dict[str, torch.Tensor], bits: int) -> None:
|
| 693 |
+
"""Add N-bit shift circuits (SHL, SHR by 1 position)."""
|
|
|
|
|
|
|
|
|
|
|
|
|
| 694 |
for bit in range(bits):
|
| 695 |
if bit < bits - 1:
|
| 696 |
add_gate(tensors, f"alu.alu{bits}bit.shl.bit{bit}", [1.0], [-1.0])
|
|
|
|
| 731 |
|
| 732 |
|
| 733 |
def add_float16_core(tensors: Dict[str, torch.Tensor]) -> None:
|
| 734 |
+
"""Add float16 core circuits (unpack, pack, classify)."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 735 |
prefix = "float16"
|
| 736 |
|
| 737 |
# Identity buffers: H(x - 1) reproduces a binary input (bias 0 would
|
|
|
|
| 755 |
|
| 756 |
def add_float_add(tensors: Dict[str, torch.Tensor], family: str,
|
| 757 |
exp_bits: int, frac_bits: int) -> None:
|
| 758 |
+
"""Add a complete float addition/subtraction pipeline."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 759 |
E, F = exp_bits, frac_bits
|
| 760 |
P = E + F # payload width
|
| 761 |
FRAME = F + 3 # mantissa + guard + round
|
|
|
|
| 972 |
|
| 973 |
def add_float_mul(tensors: Dict[str, torch.Tensor], family: str,
|
| 974 |
exp_bits: int, frac_bits: int) -> None:
|
| 975 |
+
"""Add a complete float multiplication pipeline."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 976 |
E, F = exp_bits, frac_bits
|
| 977 |
prefix = f"{family}.mul"
|
| 978 |
|
|
|
|
| 1115 |
|
| 1116 |
def add_float_div(tensors: Dict[str, torch.Tensor], family: str,
|
| 1117 |
exp_bits: int, frac_bits: int) -> None:
|
| 1118 |
+
"""Add a complete float division pipeline."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1119 |
E, F = exp_bits, frac_bits
|
| 1120 |
prefix = f"{family}.div"
|
| 1121 |
NSTAGE = F + 3
|
|
|
|
| 1288 |
|
| 1289 |
def add_float_fma(tensors: Dict[str, torch.Tensor], family: str,
|
| 1290 |
exp_bits: int, frac_bits: int) -> None:
|
| 1291 |
+
"""Fused multiply-add: round(a*b + c) with a SINGLE rounding (F-extension)."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1292 |
E, F = exp_bits, frac_bits
|
| 1293 |
W = 1 + E + F
|
| 1294 |
p = F + 1
|
|
|
|
| 1569 |
|
| 1570 |
def add_float_cmp(tensors: Dict[str, torch.Tensor], family: str,
|
| 1571 |
exp_bits: int, frac_bits: int) -> None:
|
| 1572 |
+
"""Add a complete IEEE 754 comparison network (EQ, LT, LE, GT, GE)."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1573 |
prefix = f"{family}.cmp"
|
| 1574 |
payload = exp_bits + frac_bits
|
| 1575 |
|
|
|
|
| 1657 |
|
| 1658 |
|
| 1659 |
def add_float32_core(tensors: Dict[str, torch.Tensor]) -> None:
|
| 1660 |
+
"""Add float32 core circuits (unpack, pack, classify)."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1661 |
prefix = "float32"
|
| 1662 |
|
| 1663 |
# Identity buffers: H(x - 1) reproduces a binary input (bias 0 would
|
|
|
|
| 1696 |
add_float_div(tensors, "float32", 8, 23)
|
| 1697 |
|
| 1698 |
def update_manifest(tensors: Dict[str, torch.Tensor], data_bits: int, addr_bits: int, mem_bytes: int) -> None:
|
| 1699 |
+
"""Update manifest metadata tensors."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1700 |
tensors["manifest.data_bits"] = torch.tensor([float(data_bits)], dtype=torch.float32)
|
| 1701 |
tensors["manifest.addr_bits"] = torch.tensor([float(addr_bits)], dtype=torch.float32)
|
| 1702 |
tensors["manifest.memory_bytes"] = torch.tensor([float(mem_bytes)], dtype=torch.float32)
|
|
|
|
| 1793 |
|
| 1794 |
|
| 1795 |
def infer_expr_add_mul_inputs(gate: str, reg: SignalRegistry) -> List[int]:
|
| 1796 |
+
"""Infer inputs for A + B × C expression circuit (order of operations)."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1797 |
prefix = "arithmetic.expr_add_mul"
|
| 1798 |
|
| 1799 |
# Register all inputs
|
|
|
|
| 1904 |
|
| 1905 |
|
| 1906 |
def infer_expr_paren_add_mul_inputs(gate: str, reg: SignalRegistry) -> List[int]:
|
| 1907 |
+
"""Infer inputs for (A + B) × C expression circuit (parenthetical override)."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1908 |
prefix = "arithmetic.expr_paren_add_mul"
|
| 1909 |
|
| 1910 |
# Register all inputs
|
|
|
|
| 2011 |
|
| 2012 |
|
| 2013 |
def infer_expr_paren_inputs(gate: str, reg: SignalRegistry) -> List[int]:
|
| 2014 |
+
"""Infer inputs for (A + B) × C expression circuit (parenthetical grouping)."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2015 |
prefix = "arithmetic.expr_paren"
|
| 2016 |
|
| 2017 |
# Register all inputs
|
|
|
|
| 2657 |
|
| 2658 |
def _infer_bit_cascade(gate: str, reg: SignalRegistry, cmp_prefix: str,
|
| 2659 |
a_template: str, b_template: str) -> List[int] | None:
|
| 2660 |
+
"""If `gate` is part of the bit-cascade comparator at `cmp_prefix`, return its inputs."""
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2661 |
if not gate.startswith(cmp_prefix + "."):
|
| 2662 |
return None
|
| 2663 |
suffix = gate[len(cmp_prefix) + 1:]
|
|
|
|
| 2697 |
def _infer_compare_final(gate: str, reg: SignalRegistry, cmp_prefix: str,
|
| 2698 |
out_gt: str, out_lt: str, out_ge: str, out_le: str,
|
| 2699 |
out_eq: str, bits: int) -> List[int] | None:
|
| 2700 |
+
"""Inputs of the final outputs of a bit-cascade comparator."""
|
|
|
|
|
|
|
|
|
|
| 2701 |
if gate == out_gt:
|
| 2702 |
return ([reg.register(f"{cmp_prefix}.bit0.gt")]
|
| 2703 |
+ [reg.register(f"{cmp_prefix}.cascade.gt.bit{i}") for i in range(1, bits)])
|
|
|
|
| 2719 |
|
| 2720 |
def infer_float_cmp_inputs(gate: str, reg: SignalRegistry, family: str,
|
| 2721 |
exp_bits: int, frac_bits: int) -> Optional[List[int]]:
|
| 2722 |
+
"""Complete wiring for the self-contained float comparison network."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2723 |
prefix = f"{family}.cmp"
|
| 2724 |
if gate != prefix and not gate.startswith(prefix + "."):
|
| 2725 |
return None
|
|
|
|
| 2834 |
|
| 2835 |
def _fa_member_inputs(sub: str, fa_prefix: str, a_sig: int, b_sig: int,
|
| 2836 |
cin_sig: int, reg: SignalRegistry) -> Optional[List[int]]:
|
| 2837 |
+
"""Wiring for one gate of an add_full_adder cell, given the cell's operand/carry signal IDs."""
|
|
|
|
| 2838 |
if sub == "ha1.sum.layer1.or" or sub == "ha1.sum.layer1.nand":
|
| 2839 |
return [a_sig, b_sig]
|
| 2840 |
if sub == "ha1.sum.layer2":
|
|
|
|
| 2857 |
|
| 2858 |
def infer_float_mul_inputs(gate: str, reg: SignalRegistry, family: str,
|
| 2859 |
exp_bits: int, frac_bits: int) -> Optional[List[int]]:
|
| 2860 |
+
"""Complete wiring for the self-contained float multiplication pipeline."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2861 |
E, F = exp_bits, frac_bits
|
| 2862 |
W = 1 + E + F
|
| 2863 |
prefix = f"{family}.mul"
|
|
|
|
| 3134 |
|
| 3135 |
def infer_float_div_inputs(gate: str, reg: SignalRegistry, family: str,
|
| 3136 |
exp_bits: int, frac_bits: int) -> Optional[List[int]]:
|
| 3137 |
+
"""Complete wiring for the self-contained float division pipeline."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3138 |
E, F = exp_bits, frac_bits
|
| 3139 |
W = 1 + E + F
|
| 3140 |
prefix = f"{family}.div"
|
|
|
|
| 4523 |
|
| 4524 |
|
| 4525 |
def add_subleq_core(tensors: Dict[str, torch.Tensor]) -> None:
|
| 4526 |
+
"""Add the SUBLEQ one-instruction datapath."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4527 |
for k in range(8):
|
| 4528 |
add_gate(tensors, f"subleq.sub.not_a.bit{k}", [-1.0], [0.0])
|
| 4529 |
add_full_adder(tensors, f"subleq.sub.fa{k}")
|
|
|
|
| 4591 |
|
| 4592 |
|
| 4593 |
def infer_rv32_inputs(gate: str, reg: SignalRegistry) -> Optional[List[int]]:
|
| 4594 |
+
"""Wiring of the RV32 sub-circuits: MULH accumulator, SRA muxes, NEUR and hazard comparators."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4595 |
if not gate.startswith("rv32."):
|
| 4596 |
return None
|
| 4597 |
|
|
|
|
| 4745 |
|
| 4746 |
|
| 4747 |
def _infer_threshold_computer_bit_cascade(gate: str, reg: SignalRegistry) -> List[int] | None:
|
| 4748 |
+
"""Match every bit-cascade comparator location in the codebase."""
|
|
|
|
|
|
|
|
|
|
| 4749 |
# Integer arithmetic.cmp{N}bit.* + the public greaterthan/lessthan/etc.
|
| 4750 |
m = re.match(r"^arithmetic\.cmp(\d+)bit\..+", gate)
|
| 4751 |
if m:
|
|
|
|
| 4813 |
|
| 4814 |
|
| 4815 |
def _load_routing_table() -> Dict[str, Any]:
|
| 4816 |
+
"""Load `routing/routing.json` once; on missing file, return {}."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4817 |
global _ROUTING_TABLE_CACHE
|
| 4818 |
if _ROUTING_TABLE_CACHE is not None:
|
| 4819 |
return _ROUTING_TABLE_CACHE
|
|
|
|
| 4831 |
gate: str, reg: SignalRegistry,
|
| 4832 |
known_gate_suffixes: Optional[Set[str]] = None,
|
| 4833 |
) -> Optional[List[int]]:
|
| 4834 |
+
"""Resolve a gate's inputs by consulting routing.json."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4835 |
table = _load_routing_table()
|
| 4836 |
if not table:
|
| 4837 |
return None
|
|
|
|
| 4931 |
return fc
|
| 4932 |
# Bit-cascade comparators (integer + float + div) and ternary modular detectors.
|
| 4933 |
# These are the gates emitted by add_bit_cascade_compare and the ternary
|
| 4934 |
+
# modular rebuild in the quantize command; cover them up front so they don't fall
|
| 4935 |
# through to the generic per-family handlers below.
|
| 4936 |
bc = _infer_threshold_computer_bit_cascade(gate, reg)
|
| 4937 |
if bc is not None:
|
|
|
|
| 5601 |
|
| 5602 |
|
| 5603 |
def cmd_subleq(args) -> None:
|
| 5604 |
+
"""Build the standalone SUBLEQ-8 machine file: 256 bytes of packed memory and the datapath."""
|
|
|
|
| 5605 |
out = args.model if args.model else MODEL_DIR / "variants" / "neural_subleq8.safetensors"
|
| 5606 |
print("=" * 60)
|
| 5607 |
print(" BUILD SUBLEQ-8 (one-instruction threshold machine)")
|
|
|
|
| 5632 |
|
| 5633 |
|
| 5634 |
def add_rv32_extras(tensors: Dict[str, torch.Tensor]) -> None:
|
| 5635 |
+
"""RV32 circuits beyond the shared families: MULH accumulator, SRA muxes, NEUR, hazard comparators."""
|
|
|
|
|
|
|
|
|
|
| 5636 |
# 64-bit shift-add accumulator over the 32x32 partial products.
|
| 5637 |
for stage in range(31):
|
| 5638 |
for bit in range(64):
|
|
|
|
| 5671 |
RV32_OPCLASS = {0x33: "op", 0x13: "op_imm", 0x03: "load", 0x23: "store",
|
| 5672 |
0x63: "branch", 0x37: "lui", 0x17: "auipc", 0x6F: "jal",
|
| 5673 |
0x67: "jalr", 0x73: "system", 0x53: "fp", 0x07: "flw", 0x27: "fsw"}
|
| 5674 |
+
|
| 5675 |
# Which opcode classes select each immediate format.
|
| 5676 |
RV32_USE = {"I": ["op_imm", "load", "jalr", "flw"], "S": ["store", "fsw"],
|
| 5677 |
"B": ["branch"], "U": ["lui", "auipc"], "J": ["jal"]}
|
| 5678 |
+
|
| 5679 |
RV32_IMM_FMTS = ("I", "S", "B", "U", "J")
|
| 5680 |
|
| 5681 |
|
|
|
|
| 5713 |
|
| 5714 |
|
| 5715 |
def add_rv32_decode(tensors: Dict[str, torch.Tensor]) -> None:
|
| 5716 |
+
"""RV32 instruction decode: opcode-class one-hots and the sign-extended immediate."""
|
|
|
|
|
|
|
|
|
|
| 5717 |
for opc, name in RV32_OPCLASS.items():
|
| 5718 |
w = [1.0 if (opc >> j) & 1 else -1.0 for j in range(7)]
|
| 5719 |
add_gate(tensors, f"rv32.decode.is_{name}", w, [-float(bin(opc).count("1"))])
|
|
|
|
| 5729 |
|
| 5730 |
|
| 5731 |
def add_rv32_pcnext(tensors: Dict[str, torch.Tensor]) -> None:
|
| 5732 |
+
"""RV32 next-PC selection among PC+4, PC+imm and (rs1+imm)&~1."""
|
|
|
|
|
|
|
|
|
|
| 5733 |
for k in range(32):
|
| 5734 |
# m1 = sel_target ? pcimm : pc4
|
| 5735 |
add_gate(tensors, f"rv32.pcnext.m1.bit{k}.not_sel", [-1.0], [0.0])
|
|
|
|
| 5744 |
|
| 5745 |
|
| 5746 |
def cmd_rv32(args) -> None:
|
| 5747 |
+
"""Build the standalone RV32 processor file: 64 KB of memory, the shared families and the FPU."""
|
|
|
|
|
|
|
| 5748 |
out = args.model if args.model else MODEL_DIR / "variants" / "neural_rv32.safetensors"
|
| 5749 |
print("=" * 60)
|
| 5750 |
print(" BUILD NEURAL_RV32 (RV32IM + F-subset threshold processor)")
|
|
|
|
| 5928 |
else:
|
| 5929 |
parser.print_help()
|
| 5930 |
|
| 5931 |
+
DTYPES = [
|
| 5932 |
+
(torch.int8, -(1 << 7), (1 << 7) - 1),
|
| 5933 |
+
(torch.int16, -(1 << 15), (1 << 15) - 1),
|
| 5934 |
+
(torch.int32, -(1 << 31), (1 << 31) - 1),
|
| 5935 |
+
(torch.int64, None, None), # always fits
|
| 5936 |
+
]
|
| 5937 |
+
|
| 5938 |
+
|
| 5939 |
+
def _normalize_to_int(tensor: torch.Tensor) -> torch.Tensor:
|
| 5940 |
+
"""Return a tensor with strictly integer values, floored from any half-integer values."""
|
| 5941 |
+
if not tensor.dtype.is_floating_point:
|
| 5942 |
+
return tensor.to(torch.float64) # promote for range checks
|
| 5943 |
+
tf = tensor.to(torch.float64)
|
| 5944 |
+
rounded = tf.round()
|
| 5945 |
+
if torch.equal(rounded, tf):
|
| 5946 |
+
return tf
|
| 5947 |
+
doubled = tf * 2.0
|
| 5948 |
+
if torch.equal(doubled.round(), doubled):
|
| 5949 |
+
return torch.floor(tf)
|
| 5950 |
+
raise ValueError(
|
| 5951 |
+
f"tensor has non-half-integer values; range "
|
| 5952 |
+
f"[{tf.min().item()}, {tf.max().item()}]"
|
| 5953 |
+
)
|
| 5954 |
+
|
| 5955 |
+
|
| 5956 |
+
def _min_signed_int_dtype(tensor: torch.Tensor) -> torch.dtype:
|
| 5957 |
+
if tensor.numel() == 0:
|
| 5958 |
+
return torch.int8
|
| 5959 |
+
lo = int(tensor.min().item())
|
| 5960 |
+
hi = int(tensor.max().item())
|
| 5961 |
+
for dtype, lo_lim, hi_lim in DTYPES:
|
| 5962 |
+
if lo_lim is None or (lo_lim <= lo and hi <= hi_lim):
|
| 5963 |
+
return dtype
|
| 5964 |
+
return torch.int64
|
| 5965 |
+
|
| 5966 |
+
|
| 5967 |
+
def _ternarize_modular_and_patterns(
|
| 5968 |
+
tensors: Dict[str, torch.Tensor],
|
| 5969 |
+
) -> Tuple[Dict[str, torch.Tensor], Dict]:
|
| 5970 |
+
"""Replace the seed file's modular detectors with bit-cascade equivalents."""
|
| 5971 |
+
new_tensors = dict(tensors)
|
| 5972 |
+
|
| 5973 |
+
# --- pattern_recognition: drop leadingones/trailingones ---
|
| 5974 |
+
pr_dropped = 0
|
| 5975 |
+
for k in list(new_tensors.keys()):
|
| 5976 |
+
if (k.startswith("pattern_recognition.leadingones")
|
| 5977 |
+
or k.startswith("pattern_recognition.trailingones")):
|
| 5978 |
+
del new_tensors[k]
|
| 5979 |
+
pr_dropped += 1
|
| 5980 |
+
|
| 5981 |
+
# --- modular: rebuild as ternary bit-cascade equality per multiple ---
|
| 5982 |
+
moduli = [3, 5, 6, 7, 9, 10, 11, 12]
|
| 5983 |
+
mod_gates_added = 0
|
| 5984 |
+
for mod in moduli:
|
| 5985 |
+
prefix = f"modular.mod{mod}"
|
| 5986 |
+
if f"{prefix}.eq.k0.all.weight" in new_tensors:
|
| 5987 |
+
continue # already bit-cascade; rebuilding would only shed metadata
|
| 5988 |
+
# Drop old structure
|
| 5989 |
+
for k in list(new_tensors.keys()):
|
| 5990 |
+
if k.startswith(prefix + "."):
|
| 5991 |
+
del new_tensors[k]
|
| 5992 |
+
|
| 5993 |
+
multiples = list(range(0, 256, mod))
|
| 5994 |
+
# Per-bit match gates + per-multiple AND
|
| 5995 |
+
for k in multiples:
|
| 5996 |
+
for i in range(8): # i=0 is MSB (matches inputs MSB-first ordering)
|
| 5997 |
+
k_bit = (k >> (7 - i)) & 1
|
| 5998 |
+
if k_bit == 1:
|
| 5999 |
+
# bit_match = x[i]: H(x - 0.5) ~~ identity for binary;
|
| 6000 |
+
# use weight=1, bias=-1 -> H(x-1): x=0 -> 0, x=1 -> 1.
|
| 6001 |
+
new_tensors[f"{prefix}.eq.k{k}.bit{i}.match.weight"] = torch.tensor([1.0], dtype=torch.float64)
|
| 6002 |
+
new_tensors[f"{prefix}.eq.k{k}.bit{i}.match.bias"] = torch.tensor([-1.0], dtype=torch.float64)
|
| 6003 |
+
else:
|
| 6004 |
+
# bit_match = NOT x[i]: weight=-1, bias=0 -> H(-x): x=0 -> 1, x=1 -> 0.
|
| 6005 |
+
new_tensors[f"{prefix}.eq.k{k}.bit{i}.match.weight"] = torch.tensor([-1.0], dtype=torch.float64)
|
| 6006 |
+
new_tensors[f"{prefix}.eq.k{k}.bit{i}.match.bias"] = torch.tensor([0.0], dtype=torch.float64)
|
| 6007 |
+
mod_gates_added += 1
|
| 6008 |
+
# AND of all 8 bit-matches: weights [1]*8, bias -8
|
| 6009 |
+
new_tensors[f"{prefix}.eq.k{k}.all.weight"] = torch.tensor([1.0] * 8, dtype=torch.float64)
|
| 6010 |
+
new_tensors[f"{prefix}.eq.k{k}.all.bias"] = torch.tensor([-8.0], dtype=torch.float64)
|
| 6011 |
+
mod_gates_added += 1
|
| 6012 |
+
|
| 6013 |
+
# Final OR over all per-multiple match outputs
|
| 6014 |
+
m = len(multiples)
|
| 6015 |
+
new_tensors[f"{prefix}.weight"] = torch.tensor([1.0] * m, dtype=torch.float64)
|
| 6016 |
+
new_tensors[f"{prefix}.bias"] = torch.tensor([-1.0], dtype=torch.float64)
|
| 6017 |
+
mod_gates_added += 1
|
| 6018 |
+
|
| 6019 |
+
return new_tensors, {
|
| 6020 |
+
"pattern_recognition_dropped": pr_dropped,
|
| 6021 |
+
"modular_gates_added": mod_gates_added,
|
| 6022 |
+
"modular_moduli": len(moduli),
|
| 6023 |
+
}
|
| 6024 |
+
|
| 6025 |
+
|
| 6026 |
+
def _ternarize_buffers(
|
| 6027 |
+
tensors: Dict[str, torch.Tensor],
|
| 6028 |
+
) -> Tuple[Dict[str, torch.Tensor], Dict]:
|
| 6029 |
+
"""Rewrite weight +-2 identity buffers as weight +-1 with the bias adjusted."""
|
| 6030 |
+
new_tensors = dict(tensors)
|
| 6031 |
+
fixed = 0
|
| 6032 |
+
failed_names = []
|
| 6033 |
+
|
| 6034 |
+
weight_keys = [k for k in tensors if k.endswith(".weight")]
|
| 6035 |
+
for wkey in weight_keys:
|
| 6036 |
+
w = tensors[wkey]
|
| 6037 |
+
wf = w.float() if w.dtype.is_floating_point else w.to(torch.float64).float()
|
| 6038 |
+
if (wf.abs() <= 1.0).all():
|
| 6039 |
+
continue # already ternary
|
| 6040 |
+
|
| 6041 |
+
gate = wkey[: -len(".weight")]
|
| 6042 |
+
bkey = gate + ".bias"
|
| 6043 |
+
|
| 6044 |
+
# Single-input weight=+-2 buffer with single bias
|
| 6045 |
+
if (
|
| 6046 |
+
wf.numel() == 1
|
| 6047 |
+
and abs(wf.item()) == 2.0
|
| 6048 |
+
and bkey in tensors
|
| 6049 |
+
and tensors[bkey].numel() == 1
|
| 6050 |
+
):
|
| 6051 |
+
w_val = wf.item()
|
| 6052 |
+
b_val = float(tensors[bkey].float().item())
|
| 6053 |
+
sgn = 1.0 if w_val > 0 else -1.0
|
| 6054 |
+
x0_target = 1 if b_val >= 0 else 0
|
| 6055 |
+
x1_target = 1 if (w_val + b_val) >= 0 else 0
|
| 6056 |
+
chosen = None
|
| 6057 |
+
# Prefer keeping the bias unchanged when possible
|
| 6058 |
+
for b_new in [int(b_val), int(b_val) - 1, -1, 0, -2, 1, -3, 2]:
|
| 6059 |
+
x0 = 1 if b_new >= 0 else 0
|
| 6060 |
+
x1 = 1 if (sgn + b_new) >= 0 else 0
|
| 6061 |
+
if x0 == x0_target and x1 == x1_target:
|
| 6062 |
+
chosen = b_new
|
| 6063 |
+
break
|
| 6064 |
+
if chosen is not None:
|
| 6065 |
+
new_tensors[wkey] = torch.tensor([sgn], dtype=torch.float64)
|
| 6066 |
+
new_tensors[bkey] = torch.tensor([float(chosen)], dtype=torch.float64)
|
| 6067 |
+
fixed += 1
|
| 6068 |
+
continue
|
| 6069 |
+
|
| 6070 |
+
failed_names.append(wkey)
|
| 6071 |
+
|
| 6072 |
+
return new_tensors, {"fixed": fixed, "failed_names": failed_names}
|
| 6073 |
+
|
| 6074 |
+
|
| 6075 |
+
def quantize_tensors(
|
| 6076 |
+
tensors: Dict[str, torch.Tensor],
|
| 6077 |
+
ternary: bool = False,
|
| 6078 |
+
) -> Tuple[Dict[str, torch.Tensor], Dict[str, int], Tuple[int, int], Dict]:
|
| 6079 |
+
"""Quantize a dict of tensors."""
|
| 6080 |
+
ternary_stats: Dict = {"applied": False, "fixed": 0, "failed_names": []}
|
| 6081 |
+
if ternary:
|
| 6082 |
+
tensors, ternary_stats = _ternarize_buffers(tensors)
|
| 6083 |
+
ternary_stats["applied"] = True
|
| 6084 |
+
# Also rebuild modular detectors and drop pattern_recognition stragglers.
|
| 6085 |
+
tensors, mod_stats = _ternarize_modular_and_patterns(tensors)
|
| 6086 |
+
ternary_stats["modular_gates_added"] = mod_stats["modular_gates_added"]
|
| 6087 |
+
ternary_stats["pattern_recognition_dropped"] = mod_stats["pattern_recognition_dropped"]
|
| 6088 |
+
|
| 6089 |
+
new_tensors: Dict[str, torch.Tensor] = {}
|
| 6090 |
+
counts: Dict[str, int] = {"int8": 0, "int16": 0, "int32": 0, "int64": 0,
|
| 6091 |
+
"manifest_kept": 0, "skipped": 0}
|
| 6092 |
+
bytes_before = 0
|
| 6093 |
+
bytes_after = 0
|
| 6094 |
+
|
| 6095 |
+
for name, t in tensors.items():
|
| 6096 |
+
bytes_before += t.numel() * t.element_size()
|
| 6097 |
+
|
| 6098 |
+
if name.startswith("manifest."):
|
| 6099 |
+
new_tensors[name] = t
|
| 6100 |
+
counts["manifest_kept"] += 1
|
| 6101 |
+
bytes_after += t.numel() * t.element_size()
|
| 6102 |
+
continue
|
| 6103 |
+
|
| 6104 |
+
try:
|
| 6105 |
+
normalized = _normalize_to_int(t)
|
| 6106 |
+
except ValueError:
|
| 6107 |
+
new_tensors[name] = t
|
| 6108 |
+
counts["skipped"] += 1
|
| 6109 |
+
bytes_after += t.numel() * t.element_size()
|
| 6110 |
+
continue
|
| 6111 |
+
|
| 6112 |
+
target = _min_signed_int_dtype(normalized)
|
| 6113 |
+
cast = normalized.to(target)
|
| 6114 |
+
new_tensors[name] = cast
|
| 6115 |
+
bytes_after += cast.numel() * cast.element_size()
|
| 6116 |
+
counts[str(target).replace("torch.", "")] += 1
|
| 6117 |
+
|
| 6118 |
+
return new_tensors, counts, (bytes_before, bytes_after), ternary_stats
|
| 6119 |
+
|
| 6120 |
+
|
| 6121 |
+
def quantize_file(in_path: Path, out_path: Path, verbose: bool = False,
|
| 6122 |
+
ternary: bool = False, strict_ternary: bool = False) -> Dict:
|
| 6123 |
+
file_before = in_path.stat().st_size
|
| 6124 |
+
tensors: Dict[str, torch.Tensor] = {}
|
| 6125 |
+
metadata: Dict[str, str] = {}
|
| 6126 |
+
with safe_open(str(in_path), framework="pt") as f:
|
| 6127 |
+
meta = f.metadata()
|
| 6128 |
+
if meta:
|
| 6129 |
+
metadata = dict(meta)
|
| 6130 |
+
for name in f.keys():
|
| 6131 |
+
# clone so the source mmap can be released before we write
|
| 6132 |
+
tensors[name] = f.get_tensor(name).clone()
|
| 6133 |
+
|
| 6134 |
+
new_tensors, counts, (before, after), tstats = quantize_tensors(tensors, ternary=ternary)
|
| 6135 |
+
|
| 6136 |
+
# Audit final ternary status (count of weight tensors with |w| > 1)
|
| 6137 |
+
final_nonternary = []
|
| 6138 |
+
for k, v in new_tensors.items():
|
| 6139 |
+
if not k.endswith(".weight"):
|
| 6140 |
+
continue
|
| 6141 |
+
if k.startswith("manifest."):
|
| 6142 |
+
continue
|
| 6143 |
+
vf = v.float() if v.dtype.is_floating_point else v.to(torch.float64).float()
|
| 6144 |
+
if (vf.abs() > 1.0).any():
|
| 6145 |
+
final_nonternary.append(k)
|
| 6146 |
+
|
| 6147 |
+
if ternary and strict_ternary and final_nonternary:
|
| 6148 |
+
raise ValueError(
|
| 6149 |
+
f"--strict failed: {len(final_nonternary)} weight tensors are not "
|
| 6150 |
+
f"ternary after transformation; first: {final_nonternary[:5]}"
|
| 6151 |
+
)
|
| 6152 |
+
|
| 6153 |
+
# Drop the original mmap-backed tensors before writing in-place.
|
| 6154 |
+
del tensors
|
| 6155 |
+
out_path.parent.mkdir(parents=True, exist_ok=True)
|
| 6156 |
+
if ternary:
|
| 6157 |
+
# Note ternary mode in metadata so downstream tools can see it
|
| 6158 |
+
if metadata is None:
|
| 6159 |
+
metadata = {}
|
| 6160 |
+
metadata = dict(metadata)
|
| 6161 |
+
metadata["weight_quantization"] = (
|
| 6162 |
+
"ternary_partial" if final_nonternary else "ternary"
|
| 6163 |
+
)
|
| 6164 |
+
save_file(new_tensors, str(out_path), metadata=metadata or None)
|
| 6165 |
+
|
| 6166 |
+
file_after = out_path.stat().st_size
|
| 6167 |
+
return {
|
| 6168 |
+
"in_path": str(in_path),
|
| 6169 |
+
"out_path": str(out_path),
|
| 6170 |
+
"tensor_counts": counts,
|
| 6171 |
+
"tensor_bytes_before": before,
|
| 6172 |
+
"tensor_bytes_after": after,
|
| 6173 |
+
"file_size_before": file_before,
|
| 6174 |
+
"file_size_after": file_after,
|
| 6175 |
+
"ternary": tstats,
|
| 6176 |
+
"final_nonternary": final_nonternary,
|
| 6177 |
+
}
|
| 6178 |
+
|
| 6179 |
+
|
| 6180 |
+
def _print_summary(label: str, info: Dict) -> None:
|
| 6181 |
+
cb = info["tensor_bytes_before"]
|
| 6182 |
+
ca = info["tensor_bytes_after"]
|
| 6183 |
+
fb = info["file_size_before"]
|
| 6184 |
+
fa = info["file_size_after"]
|
| 6185 |
+
counts = info["tensor_counts"]
|
| 6186 |
+
bucket_str = " ".join(f"{k}={v}" for k, v in counts.items() if v)
|
| 6187 |
+
ratio_t = cb / ca if ca else 1.0
|
| 6188 |
+
ratio_f = fb / fa if fa else 1.0
|
| 6189 |
+
print(
|
| 6190 |
+
f" {label}: file {fb / 1e6:6.1f} MB -> {fa / 1e6:6.1f} MB "
|
| 6191 |
+
f"({ratio_f:.2f}x); tensor data {cb / 1e6:6.1f} MB -> {ca / 1e6:6.1f} MB "
|
| 6192 |
+
f"({ratio_t:.2f}x)"
|
| 6193 |
+
)
|
| 6194 |
+
print(f" {bucket_str}")
|
| 6195 |
+
if info.get("ternary", {}).get("applied"):
|
| 6196 |
+
ts = info["ternary"]
|
| 6197 |
+
nt = info["final_nonternary"]
|
| 6198 |
+
print(f" ternary: {ts['fixed']} buffer gates rewritten; "
|
| 6199 |
+
f"{len(nt)} weight tensors remain non-ternary")
|
| 6200 |
+
|
| 6201 |
+
|
| 6202 |
+
def main_quantize() -> int:
|
| 6203 |
+
parser = argparse.ArgumentParser(description="Quantize safetensors to min signed int dtype")
|
| 6204 |
+
parser.add_argument("input", type=Path, help=".safetensors file or directory of files")
|
| 6205 |
+
parser.add_argument("-o", "--output", type=Path, default=None,
|
| 6206 |
+
help="output file or directory (default: in-place)")
|
| 6207 |
+
parser.add_argument("-v", "--verbose", action="store_true")
|
| 6208 |
+
parser.add_argument("--ternary", action="store_true",
|
| 6209 |
+
help="Rewrite single-input weight=+/-2 buffers as +/-1 to push toward "
|
| 6210 |
+
"ternary {-1, 0, 1} weights and report remaining violations")
|
| 6211 |
+
parser.add_argument("--strict", action="store_true",
|
| 6212 |
+
help="With --ternary, fail if any weight tensor is still non-ternary")
|
| 6213 |
+
parser.add_argument("--report-violations", type=int, default=0, metavar="N",
|
| 6214 |
+
help="Print first N non-ternary weight tensor names per file")
|
| 6215 |
+
args = parser.parse_args()
|
| 6216 |
+
|
| 6217 |
+
inputs = []
|
| 6218 |
+
if args.input.is_dir():
|
| 6219 |
+
inputs = sorted(p for p in args.input.glob("*.safetensors"))
|
| 6220 |
+
elif args.input.is_file():
|
| 6221 |
+
inputs = [args.input]
|
| 6222 |
+
else:
|
| 6223 |
+
print(f"not found: {args.input}", file=sys.stderr)
|
| 6224 |
+
return 2
|
| 6225 |
+
|
| 6226 |
+
if not inputs:
|
| 6227 |
+
print(f"no .safetensors files under {args.input}", file=sys.stderr)
|
| 6228 |
+
return 2
|
| 6229 |
+
|
| 6230 |
+
if args.output is None:
|
| 6231 |
+
outputs = inputs # in-place
|
| 6232 |
+
elif args.output.suffix == ".safetensors":
|
| 6233 |
+
if len(inputs) != 1:
|
| 6234 |
+
print("output is a single file but input is a directory; pass a directory output", file=sys.stderr)
|
| 6235 |
+
return 2
|
| 6236 |
+
outputs = [args.output]
|
| 6237 |
+
else:
|
| 6238 |
+
args.output.mkdir(parents=True, exist_ok=True)
|
| 6239 |
+
outputs = [args.output / p.name for p in inputs]
|
| 6240 |
+
|
| 6241 |
+
total_before = 0
|
| 6242 |
+
total_after = 0
|
| 6243 |
+
print(f"Quantizing {len(inputs)} file(s)" + (" (ternary mode)" if args.ternary else "") + "\n")
|
| 6244 |
+
for src, dst in zip(inputs, outputs):
|
| 6245 |
+
info = quantize_file(src, dst, verbose=args.verbose,
|
| 6246 |
+
ternary=args.ternary, strict_ternary=args.strict)
|
| 6247 |
+
_print_summary(src.name, info)
|
| 6248 |
+
if args.report_violations and info.get("final_nonternary"):
|
| 6249 |
+
for name in info["final_nonternary"][: args.report_violations]:
|
| 6250 |
+
print(f" non-ternary: {name}")
|
| 6251 |
+
if len(info["final_nonternary"]) > args.report_violations:
|
| 6252 |
+
print(f" ... and {len(info['final_nonternary']) - args.report_violations} more")
|
| 6253 |
+
total_before += info["file_size_before"]
|
| 6254 |
+
total_after += info["file_size_after"]
|
| 6255 |
+
|
| 6256 |
+
print()
|
| 6257 |
+
print("=" * 76)
|
| 6258 |
+
print(
|
| 6259 |
+
f"Total: {total_before / 1e6:.1f} MB -> {total_after / 1e6:.1f} MB "
|
| 6260 |
+
f"({total_before / max(total_after, 1):.2f}x reduction)"
|
| 6261 |
+
)
|
| 6262 |
+
return 0
|
| 6263 |
+
|
| 6264 |
+
ROOT = Path(__file__).resolve().parent.parent
|
| 6265 |
+
|
| 6266 |
+
SEED = ROOT / "neural_computer.safetensors"
|
| 6267 |
+
|
| 6268 |
+
OUT_DIR = ROOT / "variants"
|
| 6269 |
+
|
| 6270 |
+
PROFILES = ["none", "registers", "scratchpad", "small", "reduced", "full"]
|
| 6271 |
+
|
| 6272 |
+
BITS = [8, 16, 32]
|
| 6273 |
+
|
| 6274 |
+
|
| 6275 |
+
def variant_filename(bits: int, profile: str) -> str:
|
| 6276 |
+
if profile == "none":
|
| 6277 |
+
return f"neural_alu{bits}.safetensors"
|
| 6278 |
+
if profile == "full":
|
| 6279 |
+
return f"neural_computer{bits}.safetensors"
|
| 6280 |
+
return f"neural_computer{bits}_{profile}.safetensors"
|
| 6281 |
+
|
| 6282 |
+
|
| 6283 |
+
def run(cmd: list[str], timeout: int = 600) -> tuple[int, str]:
|
| 6284 |
+
p = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
|
| 6285 |
+
return p.returncode, (p.stdout or "") + (p.stderr or "")
|
| 6286 |
+
|
| 6287 |
+
|
| 6288 |
+
def build_variant(bits: int, profile: str) -> Path:
|
| 6289 |
+
out = OUT_DIR / variant_filename(bits, profile)
|
| 6290 |
+
shutil.copy2(SEED, out)
|
| 6291 |
+
cmd = [
|
| 6292 |
+
sys.executable, str(ROOT / "src" / "build.py"),
|
| 6293 |
+
"--bits", str(bits),
|
| 6294 |
+
"-m", profile,
|
| 6295 |
+
"--apply",
|
| 6296 |
+
"--model", str(out),
|
| 6297 |
+
"all",
|
| 6298 |
+
]
|
| 6299 |
+
rc, log = run(cmd, timeout=2400)
|
| 6300 |
+
if rc != 0:
|
| 6301 |
+
raise RuntimeError(f"build failed for bits={bits} profile={profile}:\n{log[-1500:]}")
|
| 6302 |
+
return out
|
| 6303 |
+
|
| 6304 |
+
|
| 6305 |
+
def quantize_variant(path: Path) -> tuple[int, int]:
|
| 6306 |
+
"""Run the quantize command with --ternary --strict on a built variant."""
|
| 6307 |
+
rc, log = run([sys.executable, str(ROOT / "src" / "build.py"), "quantize", str(path),
|
| 6308 |
+
"--ternary", "--strict"], timeout=300)
|
| 6309 |
+
if rc != 0:
|
| 6310 |
+
raise RuntimeError(f"quantize failed for {path.name}:\n{log[-800:]}")
|
| 6311 |
+
# parse the "file X.X MB -> Y.Y MB" line
|
| 6312 |
+
for line in log.splitlines():
|
| 6313 |
+
if "file" in line and "->" in line and path.name in line:
|
| 6314 |
+
try:
|
| 6315 |
+
parts = line.split("file")[1].split("->")
|
| 6316 |
+
before = float(parts[0].strip().split()[0]) * 1e6
|
| 6317 |
+
after = float(parts[1].strip().split()[0]) * 1e6
|
| 6318 |
+
return int(before), int(after)
|
| 6319 |
+
except Exception:
|
| 6320 |
+
pass
|
| 6321 |
+
return 0, 0
|
| 6322 |
+
|
| 6323 |
+
|
| 6324 |
+
def measure_variant(path: Path) -> dict:
|
| 6325 |
+
"""Read tensor count, params, manifest values from the variant."""
|
| 6326 |
+
with safe_open(str(path), framework="pt") as f:
|
| 6327 |
+
keys = list(f.keys())
|
| 6328 |
+
params = sum(f.get_tensor(k).numel() for k in keys)
|
| 6329 |
+
manifest = {
|
| 6330 |
+
k.split(".", 1)[1]: f.get_tensor(k).item()
|
| 6331 |
+
for k in keys if k.startswith("manifest.") and f.get_tensor(k).numel() == 1
|
| 6332 |
+
}
|
| 6333 |
+
return {
|
| 6334 |
+
"tensors": len(keys),
|
| 6335 |
+
"params": params,
|
| 6336 |
+
"size_mb": path.stat().st_size / (1024 * 1024),
|
| 6337 |
+
"manifest": manifest,
|
| 6338 |
+
}
|
| 6339 |
+
|
| 6340 |
+
|
| 6341 |
+
def eval_variant(path: Path, device: str = "cpu", timeout: int = 600) -> dict:
|
| 6342 |
+
"""Run eval.py against a variant and parse fitness."""
|
| 6343 |
+
cmd = [
|
| 6344 |
+
sys.executable, str(ROOT / "src" / "eval.py"),
|
| 6345 |
+
"--model", str(path),
|
| 6346 |
+
"--device", device,
|
| 6347 |
+
"--quiet",
|
| 6348 |
+
]
|
| 6349 |
+
t0 = time.time()
|
| 6350 |
+
rc, log = run(cmd, timeout=timeout)
|
| 6351 |
+
dt = time.time() - t0
|
| 6352 |
+
|
| 6353 |
+
fitness = None
|
| 6354 |
+
total_tests = None
|
| 6355 |
+
status = "ERROR"
|
| 6356 |
+
for line in log.splitlines():
|
| 6357 |
+
line = line.strip()
|
| 6358 |
+
if line.startswith("Fitness:"):
|
| 6359 |
+
try:
|
| 6360 |
+
fitness = float(line.split()[1])
|
| 6361 |
+
except Exception:
|
| 6362 |
+
pass
|
| 6363 |
+
elif line.startswith("Total tests:"):
|
| 6364 |
+
try:
|
| 6365 |
+
total_tests = int(line.split()[2])
|
| 6366 |
+
except Exception:
|
| 6367 |
+
pass
|
| 6368 |
+
elif line.startswith("STATUS:"):
|
| 6369 |
+
status = line.split()[1]
|
| 6370 |
+
return {
|
| 6371 |
+
"rc": rc,
|
| 6372 |
+
"fitness": fitness,
|
| 6373 |
+
"total_tests": total_tests,
|
| 6374 |
+
"status": status,
|
| 6375 |
+
"elapsed_s": dt,
|
| 6376 |
+
"log_tail": "\n".join(log.splitlines()[-15:]),
|
| 6377 |
+
}
|
| 6378 |
+
|
| 6379 |
+
|
| 6380 |
+
def main_all() -> None:
|
| 6381 |
+
OUT_DIR.mkdir(exist_ok=True)
|
| 6382 |
+
rows = []
|
| 6383 |
+
print(f"Building 18 variants into {OUT_DIR}\n")
|
| 6384 |
+
for bits in BITS:
|
| 6385 |
+
for profile in PROFILES:
|
| 6386 |
+
label = f"bits={bits} profile={profile}"
|
| 6387 |
+
print(f"=== {label} ===", flush=True)
|
| 6388 |
+
t0 = time.time()
|
| 6389 |
+
try:
|
| 6390 |
+
path = build_variant(bits, profile)
|
| 6391 |
+
bt = time.time() - t0
|
| 6392 |
+
pre_q_meta = measure_variant(path)
|
| 6393 |
+
# Quantize in-place as the final step; weights are
|
| 6394 |
+
# integer-valued so this is exact, --strict fails the build
|
| 6395 |
+
# if any weight is non-ternary, and header metadata
|
| 6396 |
+
# (signal_registry) is carried through.
|
| 6397 |
+
qb, qa = quantize_variant(path)
|
| 6398 |
+
meta = measure_variant(path)
|
| 6399 |
+
ev = eval_variant(path, device="cpu", timeout=900)
|
| 6400 |
+
rows.append({
|
| 6401 |
+
"bits": bits, "profile": profile,
|
| 6402 |
+
"filename": path.name,
|
| 6403 |
+
"build_s": bt,
|
| 6404 |
+
**meta,
|
| 6405 |
+
**{k: ev[k] for k in ("fitness", "total_tests", "status", "elapsed_s")},
|
| 6406 |
+
"log_tail": ev["log_tail"] if ev["status"] != "PASS" else "",
|
| 6407 |
+
})
|
| 6408 |
+
q_ratio = qb / qa if qa else 1.0
|
| 6409 |
+
print(f" built in {bt:.1f}s size={pre_q_meta['size_mb']:.1f}MB -> "
|
| 6410 |
+
f"{meta['size_mb']:.1f}MB after quant ({q_ratio:.2f}x)"
|
| 6411 |
+
f" params={meta['params']:,} tensors={meta['tensors']:,}")
|
| 6412 |
+
print(f" eval: fitness={ev['fitness']} tests={ev['total_tests']}"
|
| 6413 |
+
f" status={ev['status']} ({ev['elapsed_s']:.1f}s)")
|
| 6414 |
+
if ev["status"] != "PASS":
|
| 6415 |
+
print(" --- failure tail ---")
|
| 6416 |
+
print(" " + "\n ".join(ev["log_tail"].splitlines()))
|
| 6417 |
+
print(" --------------------")
|
| 6418 |
+
except Exception as e:
|
| 6419 |
+
print(f" EXCEPTION: {e}")
|
| 6420 |
+
rows.append({"bits": bits, "profile": profile, "error": str(e)})
|
| 6421 |
+
print()
|
| 6422 |
+
|
| 6423 |
+
print("=" * 88)
|
| 6424 |
+
print(" SUMMARY")
|
| 6425 |
+
print("=" * 88)
|
| 6426 |
+
header = f"{'bits':>4} {'profile':<11} {'size_MB':>8} {'tensors':>8} {'params':>11} {'fitness':>9} {'tests':>6} {'status':>7}"
|
| 6427 |
+
print(header)
|
| 6428 |
+
print("-" * len(header))
|
| 6429 |
+
for r in rows:
|
| 6430 |
+
if "error" in r:
|
| 6431 |
+
print(f"{r['bits']:>4} {r['profile']:<11} ERROR: {r['error'][:60]}")
|
| 6432 |
+
continue
|
| 6433 |
+
fit = f"{r['fitness']:.4f}" if r['fitness'] is not None else "n/a"
|
| 6434 |
+
tests = r['total_tests'] if r['total_tests'] is not None else "?"
|
| 6435 |
+
print(f"{r['bits']:>4} {r['profile']:<11} {r['size_mb']:>8.1f} "
|
| 6436 |
+
f"{r['tensors']:>8,} {r['params']:>11,} "
|
| 6437 |
+
f"{fit:>9} {tests:>6} {r['status']:>7}")
|
| 6438 |
+
|
| 6439 |
+
fail = [r for r in rows if r.get("status") != "PASS" or "error" in r]
|
| 6440 |
+
print()
|
| 6441 |
+
if fail:
|
| 6442 |
+
print(f"FAILURES: {len(fail)}/{len(rows)}")
|
| 6443 |
+
else:
|
| 6444 |
+
print(f"ALL {len(rows)} VARIANTS PASS")
|
| 6445 |
+
|
| 6446 |
+
|
| 6447 |
+
COMMANDS = {'build': main, 'quantize': main_quantize, 'variants': main_all}
|
| 6448 |
+
|
| 6449 |
|
| 6450 |
if __name__ == "__main__":
|
| 6451 |
+
if len(sys.argv) < 2 or sys.argv[1] not in COMMANDS:
|
| 6452 |
+
sys.exit(COMMANDS['build']())
|
| 6453 |
+
cmd = sys.argv.pop(1)
|
| 6454 |
+
sys.argv[0] += ' ' + cmd
|
| 6455 |
+
sys.exit(COMMANDS[cmd]())
|
src/ca.py
DELETED
|
@@ -1,261 +0,0 @@
|
|
| 1 |
-
"""Reversible Margolus (partitioned) cellular automaton.
|
| 2 |
-
|
| 3 |
-
One fixed rule is applied to every 2x2 block, alternating the block partition
|
| 4 |
-
between even and odd alignment each step (the Margolus neighborhood). The state
|
| 5 |
-
is the lattice; there is no program counter, register file, or control circuit.
|
| 6 |
-
|
| 7 |
-
Rule (cells ordered TL,TR,BL,BR): rotate the block 180 degrees, except a pair of
|
| 8 |
-
particles on a diagonal (1001, 0110) swaps to the other diagonal. Both cases are
|
| 9 |
-
involutions and neither maps a state across the diagonal-pair boundary, so the
|
| 10 |
-
rule is a self-inverse permutation of the sixteen block states; the lattice
|
| 11 |
-
update is therefore a bijection and replaying the partition sequence in reverse
|
| 12 |
-
inverts the evolution.
|
| 13 |
-
|
| 14 |
-
Dynamics are the billiard-ball model's: isolated particles move ballistically on
|
| 15 |
-
diagonals and collisions deflect them reversibly. and_gate() computes AND from a
|
| 16 |
-
collision; ballistic transport plus this collision are the primitives of the
|
| 17 |
-
Fredkin-Toffoli universality construction (Margolus 1984).
|
| 18 |
-
"""
|
| 19 |
-
from __future__ import annotations
|
| 20 |
-
from typing import List, Tuple
|
| 21 |
-
|
| 22 |
-
Block = Tuple[int, int, int, int]
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
def rule(b: Block) -> Block:
|
| 26 |
-
tl, tr, bl, br = b
|
| 27 |
-
if b == (1, 0, 0, 1):
|
| 28 |
-
return (0, 1, 1, 0) # diagonal pair -> other diagonal (deflect)
|
| 29 |
-
if b == (0, 1, 1, 0):
|
| 30 |
-
return (1, 0, 0, 1)
|
| 31 |
-
return (br, bl, tr, tl) # otherwise rotate 180 degrees
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
def is_bijection() -> bool:
|
| 35 |
-
outs = {rule(tuple((s >> k) & 1 for k in (3, 2, 1, 0))) for s in range(16)}
|
| 36 |
-
return len(outs) == 16
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
def self_inverse() -> bool:
|
| 40 |
-
return all(rule(rule(tuple((s >> k) & 1 for k in (3, 2, 1, 0))))
|
| 41 |
-
== tuple((s >> k) & 1 for k in (3, 2, 1, 0)) for s in range(16))
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
def step(grid: List[List[int]], phase: int) -> List[List[int]]:
|
| 45 |
-
"""One Margolus update. phase 0 aligns blocks at even coordinates; phase 1
|
| 46 |
-
offsets the partition by (1,1). Toroidal, so H and W must be even."""
|
| 47 |
-
H, W = len(grid), len(grid[0])
|
| 48 |
-
out = [row[:] for row in grid]
|
| 49 |
-
o = phase
|
| 50 |
-
for r0 in range(o, o + H, 2):
|
| 51 |
-
for c0 in range(o, o + W, 2):
|
| 52 |
-
r, r1 = r0 % H, (r0 + 1) % H
|
| 53 |
-
c, c1 = c0 % W, (c0 + 1) % W
|
| 54 |
-
nb = rule((grid[r][c], grid[r][c1], grid[r1][c], grid[r1][c1]))
|
| 55 |
-
out[r][c], out[r][c1], out[r1][c], out[r1][c1] = nb
|
| 56 |
-
return out
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
def run(grid: List[List[int]], nsteps: int, start_phase: int = 0) -> List[List[int]]:
|
| 60 |
-
g = grid
|
| 61 |
-
for n in range(nsteps):
|
| 62 |
-
g = step(g, (start_phase + n) & 1)
|
| 63 |
-
return g
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
def run_back(grid: List[List[int]], nsteps: int, start_phase: int = 0) -> List[List[int]]:
|
| 67 |
-
"""Undo `run`: replay the phase sequence in reverse; the rule is self-inverse."""
|
| 68 |
-
phases = [(start_phase + n) & 1 for n in range(nsteps)]
|
| 69 |
-
g = grid
|
| 70 |
-
for p in reversed(phases):
|
| 71 |
-
g = step(g, p)
|
| 72 |
-
return g
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
# --- the block rule as Heaviside threshold gates ---
|
| 76 |
-
# rule(s) = rotate180(s) XOR is_diag(s) on every cell: rotation fixes diagonal
|
| 77 |
-
# pairs, and flipping all four cells of a rotated diagonal pair sends it to the
|
| 78 |
-
# other diagonal. is_diag detects the two diagonal-pair states.
|
| 79 |
-
def _H(x):
|
| 80 |
-
return 1 if x >= 0 else 0
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
def _and(*xs):
|
| 84 |
-
return _H(sum(xs) - len(xs))
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
def _or(*xs):
|
| 88 |
-
return _H(sum(xs) - 1)
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
def _xor(a, b):
|
| 92 |
-
return _and(_or(a, b), _H(1 - a - b)) # OR AND NAND, the family's XOR
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
def gate_rule(b: Block) -> Block:
|
| 96 |
-
tl, tr, bl, br = b
|
| 97 |
-
d = _or(_and(tl, 1 - tr, 1 - bl, br), _and(1 - tl, tr, bl, 1 - br)) # is_diag
|
| 98 |
-
return (_xor(br, d), _xor(bl, d), _xor(tr, d), _xor(tl, d))
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
def _test_gates():
|
| 102 |
-
ok = all(gate_rule(tuple((s >> k) & 1 for k in (3, 2, 1, 0)))
|
| 103 |
-
== rule(tuple((s >> k) & 1 for k in (3, 2, 1, 0))) for s in range(16))
|
| 104 |
-
print(f" block rule as Heaviside threshold gates matches over 16 states: "
|
| 105 |
-
f"{'OK' if ok else 'FAIL'}")
|
| 106 |
-
return ok
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
# --- tests ---
|
| 110 |
-
def _rand_grid(H, W, seed):
|
| 111 |
-
import random
|
| 112 |
-
rng = random.Random(seed)
|
| 113 |
-
return [[rng.randint(0, 1) for _ in range(W)] for _ in range(H)]
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
def _ball_positions(g):
|
| 117 |
-
return {(r, c) for r, row in enumerate(g) for c, v in enumerate(row) if v}
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
def _test_rule():
|
| 121 |
-
print(f" block rule is a bijection of 16 states: {'OK' if is_bijection() else 'FAIL'}")
|
| 122 |
-
print(f" block rule is self-inverse: {'OK' if self_inverse() else 'FAIL'}")
|
| 123 |
-
return is_bijection() and self_inverse()
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
def _test_reversibility():
|
| 127 |
-
bad = 0
|
| 128 |
-
for seed in range(20):
|
| 129 |
-
g = _rand_grid(8, 8, seed)
|
| 130 |
-
fwd = run(g, 25, start_phase=0)
|
| 131 |
-
back = run_back(fwd, 25, start_phase=0)
|
| 132 |
-
if back != g:
|
| 133 |
-
bad += 1
|
| 134 |
-
# particle count is conserved (the rule permutes cells within each block)
|
| 135 |
-
g = _rand_grid(8, 8, 99)
|
| 136 |
-
conserved = sum(sum(r) for r in g) == sum(sum(r) for r in run(g, 40))
|
| 137 |
-
print(f" lattice reversible (run then reverse recovers grid, 20 grids): "
|
| 138 |
-
f"{'OK' if bad == 0 else f'FAIL({bad})'}")
|
| 139 |
-
print(f" particle number conserved: {'OK' if conserved else 'FAIL'}")
|
| 140 |
-
return bad == 0 and conserved
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
def _test_ballistic():
|
| 144 |
-
# a single particle travels in a straight diagonal line
|
| 145 |
-
H = W = 16
|
| 146 |
-
g = [[0] * W for _ in range(H)]
|
| 147 |
-
g[2][2] = 1
|
| 148 |
-
positions = [next(iter(_ball_positions(g)))]
|
| 149 |
-
gg = g
|
| 150 |
-
for n in range(8):
|
| 151 |
-
gg = step(gg, n & 1)
|
| 152 |
-
p = _ball_positions(gg)
|
| 153 |
-
positions.append(next(iter(p)) if len(p) == 1 else None)
|
| 154 |
-
ok = all(p is not None for p in positions)
|
| 155 |
-
steady = ok and all(positions[i + 1] == (positions[i][0] + 1, positions[i][1] + 1)
|
| 156 |
-
for i in range(len(positions) - 1))
|
| 157 |
-
print(f" single particle stays a single particle: {'OK' if ok else 'FAIL'}")
|
| 158 |
-
print(f" and moves ballistically on the diagonal, +(1,1) per step: "
|
| 159 |
-
f"{'OK' if steady else 'FAIL'} trace={positions[:5]}")
|
| 160 |
-
return ok and steady
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
def interaction_gate(a: int, b: int) -> dict:
|
| 164 |
-
"""One billiard-ball collision as a reversible interaction gate. Input
|
| 165 |
-
particle A enters at (2,2) moving SE and B at (7,7) moving NW; at step 4
|
| 166 |
-
three output cells carry A&B (the deflected paths), A&~B and ~A&B (the
|
| 167 |
-
straight-through paths). AND plus routing by mirrors is functionally
|
| 168 |
-
complete for the billiard-ball construction (Margolus 1984)."""
|
| 169 |
-
H = W = 12
|
| 170 |
-
g = [[0] * W for _ in range(H)]
|
| 171 |
-
if a:
|
| 172 |
-
g[2][2] = 1
|
| 173 |
-
if b:
|
| 174 |
-
g[7][7] = 1
|
| 175 |
-
g = run(g, 4)
|
| 176 |
-
return {"A_and_B": g[3][6], "A_and_notB": g[6][6], "notA_and_B": g[3][3]}
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
def and_gate(a: int, b: int) -> int:
|
| 180 |
-
return interaction_gate(a, b)["A_and_B"]
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
def _test_gate():
|
| 184 |
-
ok = True
|
| 185 |
-
for a in (0, 1):
|
| 186 |
-
for b in (0, 1):
|
| 187 |
-
o = interaction_gate(a, b)
|
| 188 |
-
ok &= (o["A_and_B"] == (a & b) and o["A_and_notB"] == (a & (1 - b))
|
| 189 |
-
and o["notA_and_B"] == ((1 - a) & b))
|
| 190 |
-
print(f" billiard-ball interaction gate (A&B, A&~B, ~A&B) over all 4 inputs: "
|
| 191 |
-
f"{'OK' if ok else 'FAIL'}")
|
| 192 |
-
return ok
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
def and3(a: int, b: int, c: int) -> int:
|
| 196 |
-
"""Two composed collisions. A at (2,2) and B at (7,7) collide into an A&B
|
| 197 |
-
particle, which then collides with C launched at (0,9); the output cell
|
| 198 |
-
(3,5) at step 4 is occupied iff a, b and c. Composing gates this way builds
|
| 199 |
-
arbitrary circuits from the interaction gate."""
|
| 200 |
-
H = W = 16
|
| 201 |
-
g = [[0] * W for _ in range(H)]
|
| 202 |
-
if a:
|
| 203 |
-
g[2][2] = 1
|
| 204 |
-
if b:
|
| 205 |
-
g[7][7] = 1
|
| 206 |
-
if c:
|
| 207 |
-
g[0][9] = 1
|
| 208 |
-
return run(g, 4)[3][5]
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
def _test_compose():
|
| 212 |
-
ok = all(and3(a, b, c) == (a & b & c)
|
| 213 |
-
for a in (0, 1) for b in (0, 1) for c in (0, 1))
|
| 214 |
-
print(f" composed 3-input AND (two chained collisions) over all 8 inputs: "
|
| 215 |
-
f"{'OK' if ok else 'FAIL'}")
|
| 216 |
-
return ok
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
def _test_collision():
|
| 220 |
-
# Two particles interact (the joint evolution differs from independent
|
| 221 |
-
# motion) and the collision stays reversible: the physics that logic needs.
|
| 222 |
-
H = W = 12
|
| 223 |
-
interacted = False
|
| 224 |
-
revok = True
|
| 225 |
-
# converging pairs: an SE-mover (even,even) meets an NW-mover (odd,odd) on a
|
| 226 |
-
# shared diagonal, forming the 1001/0110 diagonal pair the rule deflects.
|
| 227 |
-
for a, b in [((2, 2), (7, 7)), ((3, 3), (8, 8)), ((2, 8), (7, 3)),
|
| 228 |
-
((4, 4), (9, 9)), ((2, 2), (9, 9))]:
|
| 229 |
-
g = [[0] * W for _ in range(H)]
|
| 230 |
-
g[a[0]][a[1]] = 1
|
| 231 |
-
g[b[0]][b[1]] = 1
|
| 232 |
-
ga = [[0] * W for _ in range(H)]
|
| 233 |
-
ga[a[0]][a[1]] = 1
|
| 234 |
-
gb = [[0] * W for _ in range(H)]
|
| 235 |
-
gb[b[0]][b[1]] = 1
|
| 236 |
-
joint = g
|
| 237 |
-
for n in range(12):
|
| 238 |
-
joint = step(joint, n & 1)
|
| 239 |
-
ga = step(ga, n & 1)
|
| 240 |
-
gb = step(gb, n & 1)
|
| 241 |
-
free = _ball_positions(ga) | _ball_positions(gb)
|
| 242 |
-
if _ball_positions(joint) != free:
|
| 243 |
-
interacted = True
|
| 244 |
-
if run_back(run(g, 12), 12) != g:
|
| 245 |
-
revok = False
|
| 246 |
-
print(f" two-particle collisions interact (joint != independent motion): "
|
| 247 |
-
f"{'OK' if interacted else 'FAIL'}")
|
| 248 |
-
print(f" collisions remain reversible: {'OK' if revok else 'FAIL'}")
|
| 249 |
-
return interacted and revok
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
if __name__ == "__main__":
|
| 253 |
-
print("Reversible Margolus cellular automaton")
|
| 254 |
-
a = _test_rule()
|
| 255 |
-
g = _test_gates()
|
| 256 |
-
b = _test_reversibility()
|
| 257 |
-
c = _test_ballistic()
|
| 258 |
-
d = _test_collision()
|
| 259 |
-
e = _test_gate()
|
| 260 |
-
f = _test_compose()
|
| 261 |
-
print("PASS" if (a and g and b and c and d and e and f) else "FAIL")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/canonical.py
DELETED
|
@@ -1,25 +0,0 @@
|
|
| 1 |
-
"""Deterministic safetensors writer: pins tensor order and metadata key order."""
|
| 2 |
-
import json, struct
|
| 3 |
-
import torch
|
| 4 |
-
|
| 5 |
-
_DT = {torch.float32:"F32", torch.float64:"F64", torch.int64:"I64",
|
| 6 |
-
torch.int32:"I32", torch.int16:"I16", torch.int8:"I8",
|
| 7 |
-
torch.uint8:"U8", torch.bool:"BOOL"}
|
| 8 |
-
|
| 9 |
-
def canonical_bytes(tensors, metadata_pairs):
|
| 10 |
-
"""tensors: list of (name, tensor) in canonical order.
|
| 11 |
-
metadata_pairs: list of (key, value) in canonical order."""
|
| 12 |
-
header = {}
|
| 13 |
-
if metadata_pairs is not None:
|
| 14 |
-
header["__metadata__"] = {k: v for k, v in metadata_pairs}
|
| 15 |
-
blobs, off = [], 0
|
| 16 |
-
for name, t in tensors:
|
| 17 |
-
t = t.contiguous()
|
| 18 |
-
b = t.numpy().tobytes()
|
| 19 |
-
header[name] = {"dtype": _DT[t.dtype], "shape": list(t.shape),
|
| 20 |
-
"data_offsets": [off, off + len(b)]}
|
| 21 |
-
blobs.append(b); off += len(b)
|
| 22 |
-
s = json.dumps(header, separators=(",", ":"), ensure_ascii=False).encode("utf-8")
|
| 23 |
-
pad = (-(8 + len(s))) % 8
|
| 24 |
-
s = s + b" " * pad
|
| 25 |
-
return struct.pack("<Q", len(s)) + s + b"".join(blobs)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/check_c.py
DELETED
|
@@ -1,199 +0,0 @@
|
|
| 1 |
-
"""The host on src/netlist_eval.c, an evaluator in C that shares no code with
|
| 2 |
-
the Python evaluators.
|
| 3 |
-
|
| 4 |
-
The host netlist is read from its serialization with the standard library,
|
| 5 |
-
written in the flat form the C evaluator reads, levelized by the construction
|
| 6 |
-
of Lemma 2.6 and written again. The C evaluator then checks one step of the
|
| 7 |
-
netlist and one step of Lev(N_host) against its own SUBLEQ on random states,
|
| 8 |
-
runs the self-describing instance and three generations of the
|
| 9 |
-
self-reproducing instance, each instantiated from the bytes the previous one
|
| 10 |
-
emitted.
|
| 11 |
-
|
| 12 |
-
python src/check_c.py [--cc gcc]
|
| 13 |
-
"""
|
| 14 |
-
import argparse
|
| 15 |
-
import concurrent.futures as cf
|
| 16 |
-
import hashlib
|
| 17 |
-
import json
|
| 18 |
-
import os
|
| 19 |
-
import struct
|
| 20 |
-
import subprocess
|
| 21 |
-
import sys
|
| 22 |
-
import tempfile
|
| 23 |
-
import time
|
| 24 |
-
from array import array
|
| 25 |
-
|
| 26 |
-
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
| 27 |
-
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
| 28 |
-
SRC = os.path.join(REPO, "src")
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
def runs_path(name: str) -> str:
|
| 32 |
-
d = os.path.join(REPO, "paper", "runs")
|
| 33 |
-
os.makedirs(d, exist_ok=True)
|
| 34 |
-
return os.path.join(d, name)
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
def parse_sigma(data: bytes):
|
| 38 |
-
"""Biases, predecessor lists and next-state indices from a serialization."""
|
| 39 |
-
hlen = struct.unpack("<Q", data[:8])[0]
|
| 40 |
-
head = json.loads(data[8:8 + hlen])
|
| 41 |
-
T = {}
|
| 42 |
-
for k, spec in head.items():
|
| 43 |
-
if k == "__metadata__":
|
| 44 |
-
continue
|
| 45 |
-
lo, hi = spec["data_offsets"]
|
| 46 |
-
a = array({"I8": "b", "I16": "h", "I32": "i"}[spec["dtype"]])
|
| 47 |
-
a.frombytes(data[8 + hlen + lo:8 + hlen + hi])
|
| 48 |
-
if sys.byteorder != "little":
|
| 49 |
-
a.byteswap()
|
| 50 |
-
T[k] = list(a)
|
| 51 |
-
bias, fanin, pred, nxt = T["net.bias"], T["net.fanin"], T["net.pred"], T["net.next"]
|
| 52 |
-
units, p = [], 0
|
| 53 |
-
for j in range(len(bias)):
|
| 54 |
-
units.append((bias[j], [(abs(e) - 1, 1 if e > 0 else -1) for e in pred[p:p + fanin[j]]]))
|
| 55 |
-
p += fanin[j]
|
| 56 |
-
return len(nxt), units, nxt
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
def write_flat(path, n, units, nxt):
|
| 60 |
-
B, F, P = array("i"), array("i"), array("i")
|
| 61 |
-
for b, ins in units:
|
| 62 |
-
B.append(b)
|
| 63 |
-
F.append(len(ins))
|
| 64 |
-
for k, w in ins:
|
| 65 |
-
P.append(k)
|
| 66 |
-
P.append(w)
|
| 67 |
-
with open(path, "wb") as f:
|
| 68 |
-
f.write(struct.pack("<iii", n, len(units), len(P) // 2))
|
| 69 |
-
f.write(B.tobytes() + F.tobytes() + P.tobytes() + array("i", nxt).tobytes())
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
def levelize(n, units, nxt):
|
| 73 |
-
"""Lemma 2.6: every row of every layer reads only rows of the layer before."""
|
| 74 |
-
v = len(units)
|
| 75 |
-
live = [False] * (n + v)
|
| 76 |
-
stack = [k for k in nxt if k >= 0]
|
| 77 |
-
while stack:
|
| 78 |
-
s = stack.pop()
|
| 79 |
-
if not live[s]:
|
| 80 |
-
live[s] = True
|
| 81 |
-
if s >= n:
|
| 82 |
-
stack.extend(k for k, _ in units[s - n][1])
|
| 83 |
-
depth = [0] * (n + v)
|
| 84 |
-
for j, (_, ins) in enumerate(units):
|
| 85 |
-
depth[n + j] = 1 + max((depth[k] for k, _ in ins), default=0)
|
| 86 |
-
d = max(depth[n + j] for j in range(v) if live[n + j])
|
| 87 |
-
lam = [0] * (n + v)
|
| 88 |
-
for j, (_, ins) in enumerate(units):
|
| 89 |
-
if live[n + j]:
|
| 90 |
-
for k, _ in ins:
|
| 91 |
-
lam[k] = max(lam[k], depth[n + j])
|
| 92 |
-
for s in nxt:
|
| 93 |
-
lam[s] = d + 1
|
| 94 |
-
layers = [[("copy", i) for i in range(n)]]
|
| 95 |
-
where = [{i: i for i in range(n)}]
|
| 96 |
-
for ell in range(1, d):
|
| 97 |
-
rows, pos = [], {}
|
| 98 |
-
for s in range(n + v):
|
| 99 |
-
if s >= n and live[s] and depth[s] == ell:
|
| 100 |
-
pos[s] = len(rows); rows.append(("unit", s))
|
| 101 |
-
for s in range(n + v):
|
| 102 |
-
if (s < n or live[s]) and depth[s] < ell < lam[s]:
|
| 103 |
-
pos[s] = len(rows); rows.append(("copy", s))
|
| 104 |
-
layers.append(rows); where.append(pos)
|
| 105 |
-
layers.append([("unit" if depth[s] == d else "copy", s) for s in nxt])
|
| 106 |
-
base, out_units = [0], []
|
| 107 |
-
for ell in range(1, d + 1):
|
| 108 |
-
base.append(base[-1] + len(layers[ell - 1]))
|
| 109 |
-
for kind, s in layers[ell]:
|
| 110 |
-
if kind == "copy":
|
| 111 |
-
out_units.append((-1, [(base[ell - 1] + where[ell - 1][s], 1)]))
|
| 112 |
-
else:
|
| 113 |
-
b, ins = units[s - n]
|
| 114 |
-
out_units.append((b, [(base[ell - 1] + where[ell - 1][k], w) for k, w in ins]))
|
| 115 |
-
return (n, out_units, [base[d] + i for i in range(n)],
|
| 116 |
-
{"depth": d, "rows": sum(len(L) for L in layers[1:]),
|
| 117 |
-
"identity_rows": sum(1 for L in layers[1:] for kind, _ in L if kind == "copy"),
|
| 118 |
-
"max_width": max(len(L) for L in layers[1:]),
|
| 119 |
-
"layer_sizes": [len(L) for L in layers[1:]]})
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
def framed_parts(s: bytes):
|
| 123 |
-
a = struct.unpack("<Q", s[:8])[0]
|
| 124 |
-
b = struct.unpack("<Q", s[8 + a:16 + a])[0]
|
| 125 |
-
return s[8:8 + a], s[16 + a:16 + a + b], s[16 + a + b:]
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
def main() -> int:
|
| 129 |
-
ap = argparse.ArgumentParser()
|
| 130 |
-
ap.add_argument("--cc", default="gcc")
|
| 131 |
-
args = ap.parse_args()
|
| 132 |
-
t_start = time.perf_counter()
|
| 133 |
-
from selfrep import M_P, M_STAR, describe, field, ser, read_host
|
| 134 |
-
work = tempfile.mkdtemp(prefix="netlist_eval_")
|
| 135 |
-
exe = os.path.join(work, "netlist_eval")
|
| 136 |
-
subprocess.run([args.cc, "-O3", "-march=native", "-fopenmp", "-o", exe,
|
| 137 |
-
os.path.join(SRC, "netlist_eval.c")], check=True)
|
| 138 |
-
J = lambda *p: os.path.join(work, *p)
|
| 139 |
-
|
| 140 |
-
def ev(*a):
|
| 141 |
-
r = subprocess.run([exe] + [str(x) for x in a], capture_output=True, text=True)
|
| 142 |
-
return r.returncode, r.stdout.strip()
|
| 143 |
-
|
| 144 |
-
sigma = read_host()
|
| 145 |
-
n, units, nxt = parse_sigma(sigma)
|
| 146 |
-
write_flat(J("host.bin"), n, units, nxt)
|
| 147 |
-
ln, lunits, lnxt, lev_info = levelize(n, units, nxt)
|
| 148 |
-
write_flat(J("lev.bin"), ln, lunits, lnxt)
|
| 149 |
-
r_H = describe(sigma)
|
| 150 |
-
tau = describe(field(sigma) + field(bytes(M_STAR)))
|
| 151 |
-
expect_star = ser(sigma, bytes(M_STAR), tau)
|
| 152 |
-
for name, b in (("mP", bytes(M_P)), ("mstar", bytes(M_STAR)), ("rH", r_H), ("tau", tau)):
|
| 153 |
-
open(J(name), "wb").write(b)
|
| 154 |
-
out = {"lev": lev_info}
|
| 155 |
-
|
| 156 |
-
with cf.ThreadPoolExecutor(max_workers=4) as ex:
|
| 157 |
-
f_rand = ex.submit(ev, "rand", J("host.bin"), 1 << 20, 1)
|
| 158 |
-
f_lrand = ex.submit(ev, "rand", J("lev.bin"), 1 << 20, 2)
|
| 159 |
-
f_desc = ex.submit(ev, "run", J("host.bin"), J("mP"), J("rH"), J("desc.out"), "lock")
|
| 160 |
-
out["netlist_random"] = f_rand.result()[1]
|
| 161 |
-
out["lev_random"] = f_lrand.result()[1]
|
| 162 |
-
out["self_describing"] = f_desc.result()[1]
|
| 163 |
-
# three generations, each rebuilt from the bytes the previous one emitted
|
| 164 |
-
gens = []
|
| 165 |
-
net_path, mem_path, tau_path = J("host.bin"), J("mstar"), J("tau")
|
| 166 |
-
for g in range(1, 4):
|
| 167 |
-
mode = "lock" if g == 1 else "net"
|
| 168 |
-
rc, msg = ev("run", net_path, mem_path, tau_path, J(f"gen{g}.out"), mode)
|
| 169 |
-
emitted = open(J(f"gen{g}.out"), "rb").read()
|
| 170 |
-
gens.append({"generation": g, "mode": mode, "result": msg,
|
| 171 |
-
"sha256": hashlib.sha256(emitted).hexdigest(),
|
| 172 |
-
"equals_ser": emitted == expect_star})
|
| 173 |
-
s_g, m_g, t_g = framed_parts(emitted)
|
| 174 |
-
n2, u2, x2 = parse_sigma(s_g)
|
| 175 |
-
net_path = J(f"host_g{g + 1}.bin")
|
| 176 |
-
write_flat(net_path, n2, u2, x2)
|
| 177 |
-
mem_path, tau_path = J(f"m_g{g + 1}"), J(f"tau_g{g + 1}")
|
| 178 |
-
open(mem_path, "wb").write(m_g)
|
| 179 |
-
open(tau_path, "wb").write(t_g)
|
| 180 |
-
out["generations"] = gens
|
| 181 |
-
desc_ok = open(J("desc.out"), "rb").read() == sigma
|
| 182 |
-
ok = (out["netlist_random"].endswith("0 halted batches not fixed")
|
| 183 |
-
and " 0 disagree" in out["netlist_random"] and " 0 disagree" in out["lev_random"]
|
| 184 |
-
and desc_ok and all(g["equals_ser"] for g in gens))
|
| 185 |
-
out["self_describing_equals_sigma"] = desc_ok
|
| 186 |
-
out["ok"] = ok
|
| 187 |
-
out["seconds"] = time.perf_counter() - t_start
|
| 188 |
-
for k in ("netlist_random", "lev_random", "self_describing"):
|
| 189 |
-
print(f" {k}: {out[k]}")
|
| 190 |
-
for g in gens:
|
| 191 |
-
print(f" generation {g['generation']} ({g['mode']}): {g['result']}; equals ser: "
|
| 192 |
-
f"{'yes' if g['equals_ser'] else 'NO'}; {g['sha256'][:16]}")
|
| 193 |
-
json.dump(out, open(runs_path("paper_c_evaluator.json"), "w"), indent=1)
|
| 194 |
-
print("C EVALUATOR:", "PASS" if ok else "FAIL")
|
| 195 |
-
return 0 if ok else 1
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
if __name__ == "__main__":
|
| 199 |
-
sys.exit(main())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/check_codec.py
DELETED
|
@@ -1,128 +0,0 @@
|
|
| 1 |
-
"""The recipe codec on every short string and at every boundary of the format.
|
| 2 |
-
|
| 3 |
-
Checked for both encoders: the round trip, the length bound of the completeness
|
| 4 |
-
lemma, and that the reserved tag 128 is never emitted. The boundaries covered
|
| 5 |
-
are the 127-byte literal cap and the run lengths that map to the extreme repeat
|
| 6 |
-
tags, together with exhaustive coverage at small lengths.
|
| 7 |
-
"""
|
| 8 |
-
import itertools
|
| 9 |
-
import json
|
| 10 |
-
import os
|
| 11 |
-
import random
|
| 12 |
-
import sys
|
| 13 |
-
|
| 14 |
-
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
| 15 |
-
from selfrep import describe, describe_literal, decode
|
| 16 |
-
|
| 17 |
-
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
def runs_path(name: str) -> str:
|
| 21 |
-
d = os.path.join(REPO, "paper", "runs")
|
| 22 |
-
os.makedirs(d, exist_ok=True)
|
| 23 |
-
return os.path.join(d, name)
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
def bound(n: int) -> int:
|
| 27 |
-
return n + -(-n // 127) + 1
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
def tags(tape: bytes):
|
| 31 |
-
i, out = 0, []
|
| 32 |
-
while True:
|
| 33 |
-
t = tape[i]
|
| 34 |
-
out.append(t)
|
| 35 |
-
i += 1
|
| 36 |
-
if t == 0:
|
| 37 |
-
return out
|
| 38 |
-
i += t if t <= 127 else 1
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
class Audit:
|
| 42 |
-
def __init__(self):
|
| 43 |
-
self.n = 0
|
| 44 |
-
self.roundtrip = []
|
| 45 |
-
self.overlong = []
|
| 46 |
-
self.not_tight = []
|
| 47 |
-
self.tag128 = []
|
| 48 |
-
|
| 49 |
-
def check(self, b: bytes) -> None:
|
| 50 |
-
self.n += 1
|
| 51 |
-
for enc in (describe, describe_literal):
|
| 52 |
-
r = enc(b)
|
| 53 |
-
if decode(r) != b:
|
| 54 |
-
self.roundtrip.append((enc.__name__, len(b)))
|
| 55 |
-
if len(r) > bound(len(b)):
|
| 56 |
-
self.overlong.append((enc.__name__, len(b), len(r)))
|
| 57 |
-
if len(describe_literal(b)) != bound(len(b)):
|
| 58 |
-
self.not_tight.append(len(b))
|
| 59 |
-
if 128 in tags(describe(b)):
|
| 60 |
-
self.tag128.append(len(b))
|
| 61 |
-
|
| 62 |
-
@property
|
| 63 |
-
def ok(self) -> bool:
|
| 64 |
-
return not (self.roundtrip or self.overlong or self.not_tight or self.tag128)
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
def main() -> int:
|
| 68 |
-
a = Audit()
|
| 69 |
-
for n in range(0, 15):
|
| 70 |
-
for t in itertools.product((0, 1), repeat=n):
|
| 71 |
-
a.check(bytes(t))
|
| 72 |
-
binary = a.n
|
| 73 |
-
for n in range(0, 10):
|
| 74 |
-
for t in itertools.product((0, 1, 2), repeat=n):
|
| 75 |
-
a.check(bytes(t))
|
| 76 |
-
ternary = a.n - binary
|
| 77 |
-
print(f" exhaustive: {binary:,} binary strings to length 14, "
|
| 78 |
-
f"{ternary:,} ternary to length 9")
|
| 79 |
-
|
| 80 |
-
edges = (list(range(0, 12))
|
| 81 |
-
+ [126, 127, 128, 129, 130, 251, 252, 253, 254, 255, 256, 257,
|
| 82 |
-
380, 381, 382, 508, 509, 510])
|
| 83 |
-
before = a.n
|
| 84 |
-
for L in edges:
|
| 85 |
-
a.check(bytes([0x41]) * L)
|
| 86 |
-
a.check(bytes([0x00]) * L)
|
| 87 |
-
a.check(bytes([0xFF]) * L)
|
| 88 |
-
a.check(bytes(range(256))[:L] if L <= 256 else bytes(range(256)) * (L // 256))
|
| 89 |
-
a.check(bytes([0x41] * L + [0x42]))
|
| 90 |
-
a.check(bytes([0x42] + [0x41] * L))
|
| 91 |
-
a.check(bytes([0x41] * L + [0x42] * L))
|
| 92 |
-
a.check(bytes(([0x41] * 4 + [0x42]) * max(1, L)))
|
| 93 |
-
a.check(bytes(([0x41] * 3 + [0x42]) * max(1, L)))
|
| 94 |
-
print(f" boundaries: {a.n - before:,} strings straddling the literal cap, "
|
| 95 |
-
f"the extreme repeat tags, and the run/literal transitions")
|
| 96 |
-
|
| 97 |
-
rng = random.Random(11)
|
| 98 |
-
before = a.n
|
| 99 |
-
for _ in range(4000):
|
| 100 |
-
n = rng.randrange(0, 900)
|
| 101 |
-
alpha = rng.choice([2, 3, 7, 256])
|
| 102 |
-
a.check(bytes(rng.randrange(alpha) for _ in range(n)))
|
| 103 |
-
for _ in range(1500):
|
| 104 |
-
n = rng.randrange(0, 900)
|
| 105 |
-
out = bytearray()
|
| 106 |
-
while len(out) < n:
|
| 107 |
-
out += bytes([rng.randrange(256)]) * rng.choice([1, 2, 3, 4, 5, 126, 127, 128, 253])
|
| 108 |
-
a.check(bytes(out[:n]))
|
| 109 |
-
print(f" random and run-structured: {a.n - before:,} strings")
|
| 110 |
-
|
| 111 |
-
print(f" total {a.n:,}: round-trip exact, neither encoder exceeds "
|
| 112 |
-
f"|f| + ceil(|f|/127) + 1, the literal encoder attains it, "
|
| 113 |
-
f"tag 128 never emitted: {'yes' if a.ok else 'NO'}")
|
| 114 |
-
if not a.ok:
|
| 115 |
-
print(" roundtrip:", a.roundtrip[:5])
|
| 116 |
-
print(" overlong:", a.overlong[:5])
|
| 117 |
-
print(" not tight:", a.not_tight[:5])
|
| 118 |
-
print(" tag128:", a.tag128[:5])
|
| 119 |
-
json.dump({"strings": a.n, "roundtrip_failures": len(a.roundtrip),
|
| 120 |
-
"bound_failures": len(a.overlong),
|
| 121 |
-
"literal_not_tight": len(a.not_tight),
|
| 122 |
-
"tag128_emitted": len(a.tag128)},
|
| 123 |
-
open(runs_path("paper_codec.json"), "w"), indent=1)
|
| 124 |
-
return 0 if a.ok else 1
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
if __name__ == "__main__":
|
| 128 |
-
sys.exit(main())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/check_device.py
DELETED
|
@@ -1,118 +0,0 @@
|
|
| 1 |
-
"""The device against Definition 2.8 of the paper.
|
| 2 |
-
|
| 3 |
-
Definition 2.8 specifies one device application on the
|
| 4 |
-
post-instruction state (x, tau, h, omega):
|
| 5 |
-
|
| 6 |
-
(i) if y[c_wr] = 1 then omega' = omega . y[R_out] and x'[R_out] = 0,
|
| 7 |
-
otherwise omega' = omega;
|
| 8 |
-
(ii) if y[c_rd] = 1 and h < |tau| then x'[R_in] = tau[h], x'[c_eot] = 0,
|
| 9 |
-
h' = h + 1; if y[c_rd] = 1 and h = |tau| then x'[c_eot] = 1 and h' = h;
|
| 10 |
-
if y[c_rd] = 0 then h' = h;
|
| 11 |
-
(iii) if y[c_rw] = 1 then h' = 0;
|
| 12 |
-
(iv) finally x'[c_rd] = x'[c_rw] = x'[c_wr] = 0.
|
| 13 |
-
|
| 14 |
-
The clauses are transcribed here independently of selfrep.Tape and the two are
|
| 15 |
-
compared over the whole cross product of request values, head positions and
|
| 16 |
-
tapes. The sweep covers non-unit values, so it also decides whether a request
|
| 17 |
-
fires on the value 1 alone.
|
| 18 |
-
"""
|
| 19 |
-
import itertools
|
| 20 |
-
import json
|
| 21 |
-
import os
|
| 22 |
-
import sys
|
| 23 |
-
|
| 24 |
-
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
| 25 |
-
from selfrep import Tape, C_WR, C_EOT, C_RW, C_RD, R_IN, R_OUT
|
| 26 |
-
|
| 27 |
-
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
def runs_path(name: str) -> str:
|
| 31 |
-
d = os.path.join(REPO, "paper", "runs")
|
| 32 |
-
os.makedirs(d, exist_ok=True)
|
| 33 |
-
return os.path.join(d, name)
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
def spec(mem, tau, h, omega):
|
| 37 |
-
"""Definition 2.8, clause by clause, on (mem, h, omega)."""
|
| 38 |
-
m = list(mem)
|
| 39 |
-
w = list(omega)
|
| 40 |
-
if m[C_WR] == 1: # (i)
|
| 41 |
-
w.append(m[R_OUT])
|
| 42 |
-
m[R_OUT] = 0
|
| 43 |
-
hp = h
|
| 44 |
-
if m[C_RD] == 1: # (ii)
|
| 45 |
-
if h < len(tau):
|
| 46 |
-
m[R_IN] = tau[h]
|
| 47 |
-
m[C_EOT] = 0
|
| 48 |
-
hp = h + 1
|
| 49 |
-
else:
|
| 50 |
-
m[C_EOT] = 1
|
| 51 |
-
if m[C_RW] == 1: # (iii)
|
| 52 |
-
hp = 0
|
| 53 |
-
m[C_WR] = m[C_RD] = m[C_RW] = 0 # (iv)
|
| 54 |
-
return m, hp, w
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
def main() -> int:
|
| 58 |
-
tapes = [b"", b"\x07", b"\x01\x02", bytes(range(5)), bytes(range(64))]
|
| 59 |
-
vals = (0, 1, 2, 3, 127, 128, 255)
|
| 60 |
-
bad, n = [], 0
|
| 61 |
-
for tau in tapes:
|
| 62 |
-
heads = sorted({0, 1, max(0, len(tau) - 1), len(tau)})
|
| 63 |
-
for h in heads:
|
| 64 |
-
for wr, rd, rw in itertools.product(vals, repeat=3):
|
| 65 |
-
for eot, rin, rout in ((0, 0, 0), (1, 9, 200), (0, 255, 1), (1, 1, 255)):
|
| 66 |
-
mem = [0] * 256
|
| 67 |
-
mem[C_WR], mem[C_RD], mem[C_RW] = wr, rd, rw
|
| 68 |
-
mem[C_EOT], mem[R_IN], mem[R_OUT] = eot, rin, rout
|
| 69 |
-
mem[0x10] = 0xAB # an ordinary cell, must not move
|
| 70 |
-
want_mem, want_h, want_out = spec(mem, tau, h, b"")
|
| 71 |
-
|
| 72 |
-
dev = Tape(tau)
|
| 73 |
-
dev.h = h
|
| 74 |
-
got_mem = list(mem)
|
| 75 |
-
dev.apply(got_mem)
|
| 76 |
-
n += 1
|
| 77 |
-
if (got_mem != want_mem or dev.h != want_h
|
| 78 |
-
or bytes(dev.out) != bytes(want_out)):
|
| 79 |
-
bad.append({"tau": len(tau), "h": h, "wr": wr, "rd": rd,
|
| 80 |
-
"rw": rw, "eot": eot,
|
| 81 |
-
"spec": (want_mem[C_EOT], want_mem[R_IN],
|
| 82 |
-
want_mem[R_OUT], want_h, list(want_out)),
|
| 83 |
-
"impl": (got_mem[C_EOT], got_mem[R_IN],
|
| 84 |
-
got_mem[R_OUT], dev.h, list(dev.out))})
|
| 85 |
-
print(f" device applications compared: {n:,}")
|
| 86 |
-
print(f" implementation agrees with Definition 2.8 on every one: "
|
| 87 |
-
f"{'yes' if not bad else f'NO ({len(bad)} disagreements)'}")
|
| 88 |
-
for b in bad[:5]:
|
| 89 |
-
print(" ", b)
|
| 90 |
-
|
| 91 |
-
# A request fires on the value 1 and on no other value. The head starts off
|
| 92 |
-
# zero so that a rewind is observable; without that a rewind of a head
|
| 93 |
-
# already at 0 changes nothing and the test would not see it fire.
|
| 94 |
-
fires = []
|
| 95 |
-
for cell in (C_WR, C_RD, C_RW):
|
| 96 |
-
for v in range(256):
|
| 97 |
-
mem = [0] * 256
|
| 98 |
-
mem[cell] = v
|
| 99 |
-
mem[R_OUT] = 0x5A
|
| 100 |
-
dev = Tape(b"\x11\x22")
|
| 101 |
-
dev.h = 1
|
| 102 |
-
before = (dev.h, len(dev.out))
|
| 103 |
-
dev.apply(mem)
|
| 104 |
-
acted = (dev.h, len(dev.out)) != before or mem[R_IN] != 0 or mem[C_EOT] != 0
|
| 105 |
-
if acted != (v == 1):
|
| 106 |
-
fires.append((cell, v, acted))
|
| 107 |
-
print(f" each request fires exactly on the value 1, over all 256 cell values: "
|
| 108 |
-
f"{'yes' if not fires else f'NO {fires[:5]}'}")
|
| 109 |
-
|
| 110 |
-
ok = not bad and not fires
|
| 111 |
-
json.dump({"applications": n, "disagreements": len(bad),
|
| 112 |
-
"value_trigger_failures": len(fires)},
|
| 113 |
-
open(runs_path("paper_device.json"), "w"), indent=1)
|
| 114 |
-
return 0 if ok else 1
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
if __name__ == "__main__":
|
| 118 |
-
sys.exit(main())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/check_environment.py
DELETED
|
@@ -1,110 +0,0 @@
|
|
| 1 |
-
"""The cost of instantiation against the cost of the network.
|
| 2 |
-
|
| 3 |
-
One generation of the host is: inst splits the emitted bytes, sigma^-1 parses
|
| 4 |
-
the first field into the netlist, Lev places the rows and entries of the
|
| 5 |
-
layered map, and the device is applied once per step. This script counts those
|
| 6 |
-
operations, one per byte read by inst and by sigma^-1, one per row and one per
|
| 7 |
-
nonzero entry placed by Lev, and one per device application, times each of
|
| 8 |
-
them, and compares the total with the T * M unit evaluations of the run.
|
| 9 |
-
"""
|
| 10 |
-
import json
|
| 11 |
-
import os
|
| 12 |
-
import sys
|
| 13 |
-
import time
|
| 14 |
-
|
| 15 |
-
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
| 16 |
-
|
| 17 |
-
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
def runs_path(name: str) -> str:
|
| 21 |
-
d = os.path.join(REPO, "paper", "runs")
|
| 22 |
-
os.makedirs(d, exist_ok=True)
|
| 23 |
-
return os.path.join(d, name)
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
def main() -> int:
|
| 27 |
-
from selfrep import (M_STAR, inst, ser, tau_star, run_reference, read_host,
|
| 28 |
-
host_netlist, sigma_host)
|
| 29 |
-
from netlist_io import net_of_sigma
|
| 30 |
-
from matrix8 import compile_net
|
| 31 |
-
|
| 32 |
-
sigma_bytes = read_host()
|
| 33 |
-
tau = tau_star(sigma_bytes, M_STAR)
|
| 34 |
-
s = ser(sigma_bytes, bytes(M_STAR), tau)
|
| 35 |
-
|
| 36 |
-
# sigma: parameters and predecessor entries stored, and the time to write
|
| 37 |
-
t0 = time.perf_counter()
|
| 38 |
-
net, inputs, outputs = host_netlist()
|
| 39 |
-
t_build = time.perf_counter() - t0
|
| 40 |
-
t0 = time.perf_counter()
|
| 41 |
-
encoded = sigma_host()
|
| 42 |
-
t_sigma = time.perf_counter() - t0
|
| 43 |
-
assert encoded == sigma_bytes
|
| 44 |
-
stored, _, _, _ = net_of_sigma(sigma_bytes)
|
| 45 |
-
entries = (2 * len(stored.gates) # bias and fan-in
|
| 46 |
-
+ sum(len(i) for i, _ in stored.gates.values())
|
| 47 |
-
+ len(outputs))
|
| 48 |
-
print(f" sigma: {entries:,} stored entries -> {len(encoded):,} bytes "
|
| 49 |
-
f"in {t_sigma * 1e3:.0f} ms ({t_build * 1e3:.0f} ms to assemble "
|
| 50 |
-
f"the netlist from source)")
|
| 51 |
-
|
| 52 |
-
# inst: one pass over the serialized instance, then sigma^-1 on its first field
|
| 53 |
-
t0 = time.perf_counter()
|
| 54 |
-
reps = 50
|
| 55 |
-
for _ in range(reps):
|
| 56 |
-
a, b, c = inst(s)
|
| 57 |
-
t_inst = (time.perf_counter() - t0) / reps
|
| 58 |
-
assert (a, b, c) == (sigma_bytes, bytes(M_STAR), tau)
|
| 59 |
-
t0 = time.perf_counter()
|
| 60 |
-
for _ in range(5):
|
| 61 |
-
N, I, O, _ = net_of_sigma(a)
|
| 62 |
-
t_sigma_inv = (time.perf_counter() - t0) / 5
|
| 63 |
-
assert len(N.gates) == len(stored.gates)
|
| 64 |
-
print(f" inst: {len(s):,} bytes parsed in {t_inst * 1e3:.2f} ms, and "
|
| 65 |
-
f"{len(N.gates):,} units rebuilt from the first field in "
|
| 66 |
-
f"{t_sigma_inv * 1e3:.0f} ms")
|
| 67 |
-
|
| 68 |
-
# Lev: the units placed, and the time to place them
|
| 69 |
-
t0 = time.perf_counter()
|
| 70 |
-
layers, info = compile_net(N, I, O)
|
| 71 |
-
t_lev = time.perf_counter() - t0
|
| 72 |
-
M = sum(int(W.shape[0]) for W, _ in layers)
|
| 73 |
-
nnz = sum(int((W != 0).sum()) for W, _ in layers)
|
| 74 |
-
print(f" Lev: {len(N.gates):,} units in, {M:,} rows and {nnz:,} nonzero "
|
| 75 |
-
f"entries placed over {info['layers']} layers in {t_lev:.1f} s")
|
| 76 |
-
|
| 77 |
-
# the device: one application per step, at most one byte moved
|
| 78 |
-
t0 = time.perf_counter()
|
| 79 |
-
emitted, T = run_reference(M_STAR, tau)
|
| 80 |
-
t_run = time.perf_counter() - t0
|
| 81 |
-
assert emitted == s
|
| 82 |
-
print(f" device: {T:,} applications, {len(emitted):,} bytes emitted "
|
| 83 |
-
f"({t_run:.2f} s for the whole reference run)")
|
| 84 |
-
|
| 85 |
-
env_ops = len(s) + len(sigma_bytes) + M + nnz + T
|
| 86 |
-
closed = 2 * len(sigma_bytes) + len(bytes(M_STAR)) + len(tau) + 16 + M + nnz + T
|
| 87 |
-
assert env_ops == closed, (env_ops, closed)
|
| 88 |
-
net_ops = T * M
|
| 89 |
-
print(f" instantiation: {env_ops:,} elementary operations per generation, "
|
| 90 |
-
f"which is the closed form 2|sigma|+|m|+|tau|+16+M+nnz+T")
|
| 91 |
-
print(f" network: {net_ops:,} unit evaluations (T = {T:,}, M = {M:,})")
|
| 92 |
-
print(f" ratio: {env_ops / net_ops:.3e}")
|
| 93 |
-
|
| 94 |
-
out = {"sigma_entries": entries, "sigma_bytes": len(encoded),
|
| 95 |
-
"sigma_seconds": t_sigma, "build_seconds": t_build,
|
| 96 |
-
"inst_bytes": len(s), "inst_seconds": t_inst,
|
| 97 |
-
"sigma_inverse_seconds": t_sigma_inv,
|
| 98 |
-
"lev_units_in": len(N.gates), "lev_units_out": M, "lev_nonzero": nnz,
|
| 99 |
-
"lev_layers": info["layers"], "lev_seconds": t_lev,
|
| 100 |
-
"lev_entries": info["total_weights"],
|
| 101 |
-
"steps": T, "emitted_bytes": len(emitted),
|
| 102 |
-
"reference_seconds": t_run,
|
| 103 |
-
"environment_ops": env_ops, "network_ops": net_ops,
|
| 104 |
-
"ratio": env_ops / net_ops}
|
| 105 |
-
json.dump(out, open(runs_path("paper_environment.json"), "w"), indent=1)
|
| 106 |
-
return 0
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
if __name__ == "__main__":
|
| 110 |
-
sys.exit(main())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/check_family.py
DELETED
|
@@ -1,64 +0,0 @@
|
|
| 1 |
-
"""The universal constructor emits every machine of the artifact, byte for byte.
|
| 2 |
-
|
| 3 |
-
The machines are run on the integer reference, one process per machine.
|
| 4 |
-
|
| 5 |
-
python src/check_family.py [--jobs N]
|
| 6 |
-
"""
|
| 7 |
-
import argparse, concurrent.futures as cf, hashlib, json, os, sys, time
|
| 8 |
-
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
| 9 |
-
from selfrep import describe, run_reference, M_P
|
| 10 |
-
|
| 11 |
-
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
def runs_path(name: str) -> str:
|
| 15 |
-
d = os.path.join(REPO, "paper", "runs")
|
| 16 |
-
os.makedirs(d, exist_ok=True)
|
| 17 |
-
return os.path.join(d, name)
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
def files():
|
| 21 |
-
out = [os.path.join(REPO, "neural_computer.safetensors")]
|
| 22 |
-
v = os.path.join(REPO, "variants")
|
| 23 |
-
out += sorted(os.path.join(v, f) for f in os.listdir(v) if f.endswith(".safetensors"))
|
| 24 |
-
return out
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
def one(p):
|
| 28 |
-
data = open(p, "rb").read()
|
| 29 |
-
r = describe(data)
|
| 30 |
-
out, steps = run_reference(M_P, r, expect=data)
|
| 31 |
-
return {"file": os.path.basename(p), "bytes": len(out), "recipe": len(r),
|
| 32 |
-
"steps": steps, "exact": out == data,
|
| 33 |
-
"sha256": hashlib.sha256(out).hexdigest()}
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
def main() -> int:
|
| 37 |
-
ap = argparse.ArgumentParser()
|
| 38 |
-
ap.add_argument("--jobs", type=int, default=os.cpu_count() or 1)
|
| 39 |
-
args = ap.parse_args()
|
| 40 |
-
t0 = time.perf_counter()
|
| 41 |
-
paths = files()
|
| 42 |
-
# the largest machines first, so the pool is not left waiting on one of them
|
| 43 |
-
order = sorted(paths, key=os.path.getsize, reverse=True)
|
| 44 |
-
with cf.ProcessPoolExecutor(max_workers=args.jobs) as ex:
|
| 45 |
-
got = dict(zip(order, ex.map(one, order)))
|
| 46 |
-
rec = [got[p] for p in paths]
|
| 47 |
-
for r in rec:
|
| 48 |
-
print(f" {'OK ' if r['exact'] else 'FAIL'} {r['file']:<44} "
|
| 49 |
-
f"{r['bytes']:>10,} B {r['steps']:>12,} steps", flush=True)
|
| 50 |
-
bad = sum(not r["exact"] for r in rec)
|
| 51 |
-
tot_b = sum(r["bytes"] for r in rec)
|
| 52 |
-
tot_s = sum(r["steps"] for r in rec)
|
| 53 |
-
tot_r = sum(r["recipe"] for r in rec)
|
| 54 |
-
dt = time.perf_counter() - t0
|
| 55 |
-
print(f"{len(rec)} machines, {tot_b:,} bytes emitted in {tot_s:,} steps "
|
| 56 |
-
f"from {tot_r:,} bytes of recipe ({dt/60:.1f} min); failures: {bad}")
|
| 57 |
-
json.dump({"machines": rec, "bytes": tot_b, "steps": tot_s,
|
| 58 |
-
"recipe_bytes": tot_r, "failures": bad, "seconds": dt},
|
| 59 |
-
open(runs_path("paper_runs_family.json"), "w"), indent=1)
|
| 60 |
-
return 0 if bad == 0 else 1
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
if __name__ == "__main__":
|
| 64 |
-
sys.exit(main())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/check_family_gate.py
DELETED
|
@@ -1,70 +0,0 @@
|
|
| 1 |
-
"""The constructor emitting other machines of the family on the netlist.
|
| 2 |
-
|
| 3 |
-
check_family.py runs the constructor on all twenty-eight serialized machines,
|
| 4 |
-
but on the integer reference. The claim that the netlist emits them is stronger
|
| 5 |
-
than the claim that the reference does, so the machines small enough to finish
|
| 6 |
-
are run here on the netlist that sigma(N_host) denotes, unit by unit, with each
|
| 7 |
-
emitted byte compared against the target as it is produced.
|
| 8 |
-
"""
|
| 9 |
-
import hashlib
|
| 10 |
-
import json
|
| 11 |
-
import os
|
| 12 |
-
import sys
|
| 13 |
-
import time
|
| 14 |
-
|
| 15 |
-
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
| 16 |
-
from selfrep import NetEvaluator, describe, read_host, run_reference, M_P
|
| 17 |
-
|
| 18 |
-
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
| 19 |
-
TARGETS = ["neural_tile.safetensors", "neural_ca.safetensors",
|
| 20 |
-
"neural_reversible.safetensors", "neural_attractor.safetensors"]
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
def runs_path(name: str) -> str:
|
| 24 |
-
d = os.path.join(REPO, "paper", "runs")
|
| 25 |
-
os.makedirs(d, exist_ok=True)
|
| 26 |
-
return os.path.join(d, name)
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
def main() -> int:
|
| 30 |
-
import torch
|
| 31 |
-
device = "cuda" if torch.cuda.is_available() else "cpu"
|
| 32 |
-
F = NetEvaluator(read_host(), device=device,
|
| 33 |
-
graph=device.startswith("cuda"))
|
| 34 |
-
rows = []
|
| 35 |
-
bad = 0
|
| 36 |
-
total_b = total_s = 0
|
| 37 |
-
t_all = time.perf_counter()
|
| 38 |
-
for name in TARGETS:
|
| 39 |
-
path = os.path.join(REPO, "variants", name)
|
| 40 |
-
data = open(path, "rb").read()
|
| 41 |
-
r = describe(data)
|
| 42 |
-
ref_out, ref_steps = run_reference(M_P, r, expect=data)
|
| 43 |
-
assert ref_out == data
|
| 44 |
-
t0 = time.perf_counter()
|
| 45 |
-
out, steps = F.run(M_P, r, max_steps=ref_steps + 8, expect=data,
|
| 46 |
-
progress=200000)
|
| 47 |
-
dt = time.perf_counter() - t0
|
| 48 |
-
ok = out == data and steps == ref_steps
|
| 49 |
-
bad += not ok
|
| 50 |
-
total_b += len(out)
|
| 51 |
-
total_s += steps
|
| 52 |
-
rows.append({"file": name, "bytes": len(out), "recipe": len(r),
|
| 53 |
-
"steps": steps, "reference_steps": ref_steps,
|
| 54 |
-
"exact": ok, "seconds": dt,
|
| 55 |
-
"steps_per_second": steps / dt if dt else None,
|
| 56 |
-
"sha256": hashlib.sha256(out).hexdigest()})
|
| 57 |
-
print(f" {'OK ' if ok else 'FAIL'} {name:<34} {len(out):>9,} B "
|
| 58 |
-
f"{steps:>10,} steps ({dt / 60:.1f} min, "
|
| 59 |
-
f"{steps / dt:,.0f} steps/s)", flush=True)
|
| 60 |
-
dt = time.perf_counter() - t_all
|
| 61 |
-
print(f" {len(rows)} machines on the netlist of sigma: {total_b:,} bytes in "
|
| 62 |
-
f"{total_s:,} steps ({dt / 60:.1f} min); failures: {bad}")
|
| 63 |
-
json.dump({"machines": rows, "bytes": total_b, "steps": total_s,
|
| 64 |
-
"failures": bad, "seconds": dt},
|
| 65 |
-
open(runs_path("paper_runs_family_gate.json"), "w"), indent=1)
|
| 66 |
-
return 0 if bad == 0 else 1
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
if __name__ == "__main__":
|
| 70 |
-
sys.exit(main())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/check_framing.py
DELETED
|
@@ -1,133 +0,0 @@
|
|
| 1 |
-
"""The framing maps ser and inst: round trip, injectivity, and rejection.
|
| 2 |
-
|
| 3 |
-
Lemma 2.14 of the paper turns on two properties of Definition 2.12: that the
|
| 4 |
-
length fields make the decomposition of ser(Omega) unique, so that ser is
|
| 5 |
-
injective and inst inverts it, and that inst is partial, rejecting a string that
|
| 6 |
-
is not a framed instance, in place of returning some other triple. Both are
|
| 7 |
-
checked here on shapes chosen to stress the framing: empty serializations and
|
| 8 |
-
tapes, payloads whose leading bytes look like length fields, memory images at
|
| 9 |
-
both extremes, and every truncation of a real instance.
|
| 10 |
-
"""
|
| 11 |
-
import hashlib
|
| 12 |
-
import json
|
| 13 |
-
import os
|
| 14 |
-
import random
|
| 15 |
-
import struct
|
| 16 |
-
import sys
|
| 17 |
-
|
| 18 |
-
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
| 19 |
-
from selfrep import ser, inst, lam, HOST_PATH, M_STAR, tau_star
|
| 20 |
-
|
| 21 |
-
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
def runs_path(name: str) -> str:
|
| 25 |
-
d = os.path.join(REPO, "paper", "runs")
|
| 26 |
-
os.makedirs(d, exist_ok=True)
|
| 27 |
-
return os.path.join(d, name)
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
def shapes():
|
| 31 |
-
rng = random.Random(5)
|
| 32 |
-
host = open(HOST_PATH, "rb").read()
|
| 33 |
-
m0 = bytes(256)
|
| 34 |
-
m1 = bytes([255]) * 256
|
| 35 |
-
mr = bytes(rng.randrange(256) for _ in range(256))
|
| 36 |
-
# A payload whose first eight bytes are a plausible length field, and a tape
|
| 37 |
-
# that begins the way a framed field begins: if inst read a length from the
|
| 38 |
-
# wrong offset these would decode to the wrong split.
|
| 39 |
-
trap = lam(1 << 40) + b"trap"
|
| 40 |
-
out = [
|
| 41 |
-
(b"", m0, b""),
|
| 42 |
-
(b"", m0, b"\x00"),
|
| 43 |
-
(b"\x00", m0, b""),
|
| 44 |
-
(b"\x00" * 3, m1, b""),
|
| 45 |
-
(trap, mr, b""),
|
| 46 |
-
(b"", mr, trap),
|
| 47 |
-
(trap, m0, trap),
|
| 48 |
-
(bytes(range(256)), mr, bytes(range(256))),
|
| 49 |
-
(host, bytes(M_STAR), tau_star(host, M_STAR)),
|
| 50 |
-
(host, m0, b""),
|
| 51 |
-
]
|
| 52 |
-
for _ in range(300):
|
| 53 |
-
a = bytes(rng.randrange(256) for _ in range(rng.randrange(0, 300)))
|
| 54 |
-
t = bytes(rng.randrange(256) for _ in range(rng.randrange(0, 300)))
|
| 55 |
-
out.append((a, bytes(rng.randrange(256) for _ in range(256)), t))
|
| 56 |
-
return out
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
def main() -> int:
|
| 60 |
-
cases = shapes()
|
| 61 |
-
rt_bad, digests = [], {}
|
| 62 |
-
for sg, m, t in cases:
|
| 63 |
-
b = ser(sg, m, t)
|
| 64 |
-
if inst(b) != (sg, m, t):
|
| 65 |
-
rt_bad.append((len(sg), len(t)))
|
| 66 |
-
digests.setdefault(hashlib.sha256(b).hexdigest(), []).append((len(sg), len(m), len(t)))
|
| 67 |
-
collisions = {k: v for k, v in digests.items() if len(v) > 1}
|
| 68 |
-
print(f" inst(ser(Omega)) = Omega on {len(cases)} instances: "
|
| 69 |
-
f"{'exact' if not rt_bad else f'{len(rt_bad)} FAILED'}")
|
| 70 |
-
print(f" ser injective over them (distinct instances, distinct bytes): "
|
| 71 |
-
f"{'yes' if not collisions else f'NO {list(collisions.values())[:2]}'}")
|
| 72 |
-
|
| 73 |
-
# Truncations. A string shorter than the framed serialization and memory
|
| 74 |
-
# image is not an instance and must be rejected; one that reaches at least
|
| 75 |
-
# that far is the instance with a correspondingly shorter tape, and must
|
| 76 |
-
# parse to exactly that.
|
| 77 |
-
host = open(HOST_PATH, "rb").read()
|
| 78 |
-
tau = tau_star(host, M_STAR)
|
| 79 |
-
full = ser(host, bytes(M_STAR), tau)
|
| 80 |
-
minimal = 16 + len(host) + 256
|
| 81 |
-
probes = sorted({p for p in
|
| 82 |
-
[0, 1, 7, 8, 9, 15, 16, 17,
|
| 83 |
-
8 + len(host) - 1, 8 + len(host), 8 + len(host) + 7,
|
| 84 |
-
8 + len(host) + 8, minimal - 256, minimal - 1, minimal,
|
| 85 |
-
minimal + 1, len(full) - 1, len(full)]
|
| 86 |
-
if 0 <= p <= len(full)})
|
| 87 |
-
wrongly_accepted, wrongly_rejected = [], []
|
| 88 |
-
for k in probes:
|
| 89 |
-
try:
|
| 90 |
-
got = inst(full[:k])
|
| 91 |
-
except (ValueError, IndexError, struct.error):
|
| 92 |
-
if k >= minimal:
|
| 93 |
-
wrongly_rejected.append(k)
|
| 94 |
-
continue
|
| 95 |
-
if k < minimal:
|
| 96 |
-
wrongly_accepted.append((k, len(got[1])))
|
| 97 |
-
elif got != (host, bytes(M_STAR), full[minimal:k]):
|
| 98 |
-
wrongly_accepted.append((k, "wrong triple"))
|
| 99 |
-
short = sum(1 for k in probes if k < minimal)
|
| 100 |
-
print(f" {short} truncations below a complete memory image rejected: "
|
| 101 |
-
f"{'yes' if not wrongly_accepted else f'NO {wrongly_accepted}'}")
|
| 102 |
-
print(f" {len(probes) - short} longer prefixes parse to the instance with "
|
| 103 |
-
f"that tape: {'yes' if not wrongly_rejected else f'NO {wrongly_rejected}'}")
|
| 104 |
-
accepted = wrongly_accepted + wrongly_rejected
|
| 105 |
-
|
| 106 |
-
# a memory field of the wrong length must be rejected
|
| 107 |
-
wrong = []
|
| 108 |
-
for size in (0, 1, 255, 257, 512):
|
| 109 |
-
b = lam(len(host)) + host + lam(size) + bytes(size) + tau
|
| 110 |
-
try:
|
| 111 |
-
inst(b)
|
| 112 |
-
wrong.append(size)
|
| 113 |
-
except (ValueError, IndexError, struct.error):
|
| 114 |
-
pass
|
| 115 |
-
print(f" memory field of length != 256 rejected: "
|
| 116 |
-
f"{'yes' if not wrong else f'NO for {wrong}'}")
|
| 117 |
-
|
| 118 |
-
# the full instance is accepted and recovers exactly
|
| 119 |
-
exact = inst(full) == (host, bytes(M_STAR), tau)
|
| 120 |
-
print(f" the distributed instance parses back exactly: {exact}")
|
| 121 |
-
|
| 122 |
-
ok = not rt_bad and not collisions and not accepted and not wrong and exact
|
| 123 |
-
json.dump({"instances": len(cases), "roundtrip_failures": len(rt_bad),
|
| 124 |
-
"digest_collisions": len(collisions),
|
| 125 |
-
"truncations_accepted": len(accepted),
|
| 126 |
-
"bad_memory_lengths_accepted": len(wrong),
|
| 127 |
-
"distributed_instance_exact": exact},
|
| 128 |
-
open(runs_path("paper_framing.json"), "w"), indent=1)
|
| 129 |
-
return 0 if ok else 1
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
if __name__ == "__main__":
|
| 133 |
-
sys.exit(main())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/check_host.py
DELETED
|
@@ -1,113 +0,0 @@
|
|
| 1 |
-
"""The reference machine against Definition 3.1, and the halt fixed point.
|
| 2 |
-
|
| 3 |
-
Definition 3.1 specifies one SUBLEQ step: read A = M[p], B = M[p+1], C = M[p+2],
|
| 4 |
-
set M[B] <- M[B] - M[A] mod 256, and set p <- C when the new M[B] is at most 0
|
| 5 |
-
in two's complement and p <- p + 3 otherwise, with indices taken modulo 256 and
|
| 6 |
-
a halt at p = 255. The clauses are transcribed here independently of
|
| 7 |
-
selfrep.run_reference and the two are compared, with the program counter swept
|
| 8 |
-
over every value so the wrapped fetches at the top of memory are covered.
|
| 9 |
-
|
| 10 |
-
Proposition 3.9 asserts in addition that a halted state is a fixed point of the
|
| 11 |
-
transition, because every architectural write is gated by the negation of the
|
| 12 |
-
halt bit. That is checked on the levelized map itself, not on the reference.
|
| 13 |
-
"""
|
| 14 |
-
import json
|
| 15 |
-
import os
|
| 16 |
-
import random
|
| 17 |
-
import sys
|
| 18 |
-
|
| 19 |
-
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
| 20 |
-
from selfrep import Tape, HALT_PC
|
| 21 |
-
|
| 22 |
-
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
def runs_path(name: str) -> str:
|
| 26 |
-
d = os.path.join(REPO, "paper", "runs")
|
| 27 |
-
os.makedirs(d, exist_ok=True)
|
| 28 |
-
return os.path.join(d, name)
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
def spec_step(mem, p):
|
| 32 |
-
"""Definition 3.1, transcribed."""
|
| 33 |
-
A = mem[p % 256]
|
| 34 |
-
B = mem[(p + 1) % 256]
|
| 35 |
-
C = mem[(p + 2) % 256]
|
| 36 |
-
m = list(mem)
|
| 37 |
-
r = (m[B % 256] - m[A % 256]) % 256
|
| 38 |
-
m[B % 256] = r
|
| 39 |
-
two_c = r - 256 if r >= 128 else r
|
| 40 |
-
return m, (C if two_c <= 0 else (p + 3) % 256)
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
def impl_step(mem, p):
|
| 44 |
-
"""The reference of selfrep.run_reference, one step, device detached."""
|
| 45 |
-
m = list(mem)
|
| 46 |
-
A = m[p]
|
| 47 |
-
B = m[(p + 1) & 0xFF]
|
| 48 |
-
C = m[(p + 2) & 0xFF]
|
| 49 |
-
r = (m[B] - m[A]) & 0xFF
|
| 50 |
-
m[B] = r
|
| 51 |
-
return m, (C if (r == 0 or r >= 0x80) else (p + 3) & 0xFF)
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
def main() -> int:
|
| 55 |
-
rng = random.Random(31)
|
| 56 |
-
bad, n = [], 0
|
| 57 |
-
mems = [[0] * 256, [255] * 256, list(range(256)), list(reversed(range(256)))]
|
| 58 |
-
mems += [[rng.randrange(256) for _ in range(256)] for _ in range(60)]
|
| 59 |
-
for mem in mems:
|
| 60 |
-
for p in range(256): # every counter, wraps included
|
| 61 |
-
if p == HALT_PC:
|
| 62 |
-
continue
|
| 63 |
-
sm, sp = spec_step(mem, p)
|
| 64 |
-
im, ip = impl_step(mem, p)
|
| 65 |
-
n += 1
|
| 66 |
-
if sm != im or sp != ip:
|
| 67 |
-
bad.append((p, sp, ip))
|
| 68 |
-
print(f" reference steps compared with Definition 3.1: {n:,}")
|
| 69 |
-
print(f" agree on every one: {'yes' if not bad else f'NO {bad[:4]}'}")
|
| 70 |
-
|
| 71 |
-
wrapped = sum(1 for p in (254, 255, 253) if p != HALT_PC) * len(mems)
|
| 72 |
-
print(f" of which {len(mems) * 3:,} fetch an operand across the top of memory")
|
| 73 |
-
|
| 74 |
-
# A halted state is a fixed point of the levelized map. The state is halted
|
| 75 |
-
# when the halt bit is set, coordinate 8 of the vector; the bit latches one
|
| 76 |
-
# step after the next counter decodes to 255, so setting the counter alone
|
| 77 |
-
# does not halt the machine.
|
| 78 |
-
from selfrep import LevEvaluator, read_host
|
| 79 |
-
import torch
|
| 80 |
-
L = LevEvaluator(read_host(), device="cpu", dense=False)
|
| 81 |
-
gen = torch.Generator().manual_seed(97)
|
| 82 |
-
V = (torch.rand(48, L.N, generator=gen) < 0.5).float()
|
| 83 |
-
V[:, 8] = 1.0
|
| 84 |
-
fixed = bool((L.step(V) == V).all())
|
| 85 |
-
print(f" halted states are fixed points of Lev(N_host), 48 random states: "
|
| 86 |
-
f"{'yes' if fixed else 'NO'}")
|
| 87 |
-
|
| 88 |
-
# And the bit latches exactly when the next counter is 255: run one step
|
| 89 |
-
# from unhalted states and compare the latched bit against the reference.
|
| 90 |
-
U = (torch.rand(64, L.N, generator=gen) < 0.5).float()
|
| 91 |
-
U[:, 8] = 0.0
|
| 92 |
-
W = L.step(U)
|
| 93 |
-
latch_bad = 0
|
| 94 |
-
for i in range(U.shape[0]):
|
| 95 |
-
mem = [int(sum(int(U[i, 9 + j * 8 + k]) << (7 - k) for k in range(8)))
|
| 96 |
-
for j in range(256)]
|
| 97 |
-
pc = int(sum(int(U[i, k]) << (7 - k) for k in range(8)))
|
| 98 |
-
_, npc = impl_step(mem, pc)
|
| 99 |
-
if int(W[i, 8]) != (1 if npc == HALT_PC else 0):
|
| 100 |
-
latch_bad += 1
|
| 101 |
-
print(f" the halt bit latches exactly when the next counter is 255, "
|
| 102 |
-
f"64 random states: {'yes' if latch_bad == 0 else f'NO ({latch_bad})'}")
|
| 103 |
-
fixed = fixed and latch_bad == 0
|
| 104 |
-
|
| 105 |
-
ok = not bad and fixed
|
| 106 |
-
json.dump({"steps_compared": n, "disagreements": len(bad),
|
| 107 |
-
"halt_fixed_point": fixed},
|
| 108 |
-
open(runs_path("paper_host.json"), "w"), indent=1)
|
| 109 |
-
return 0 if ok else 1
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
if __name__ == "__main__":
|
| 113 |
-
sys.exit(main())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/check_host_family.py
DELETED
|
@@ -1,65 +0,0 @@
|
|
| 1 |
-
"""The host family on the evaluator in C: one step of H_A against SUBLEQ_A.
|
| 2 |
-
|
| 3 |
-
For each width A the serialization of H_A is written by host_family.sigma,
|
| 4 |
-
parsed with the standard library, written in the flat form of
|
| 5 |
-
src/netlist_eval.c, and one step of the netlist is compared with the C
|
| 6 |
-
evaluator's own SUBLEQ on uniformly random full states, together with the
|
| 7 |
-
halted fixed point.
|
| 8 |
-
|
| 9 |
-
python src/check_host_family.py [--widths 3-12] [--states N]
|
| 10 |
-
"""
|
| 11 |
-
import argparse
|
| 12 |
-
import json
|
| 13 |
-
import os
|
| 14 |
-
import subprocess
|
| 15 |
-
import sys
|
| 16 |
-
import tempfile
|
| 17 |
-
import time
|
| 18 |
-
|
| 19 |
-
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
| 20 |
-
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
def runs_path(name: str) -> str:
|
| 24 |
-
d = os.path.join(REPO, "paper", "runs")
|
| 25 |
-
os.makedirs(d, exist_ok=True)
|
| 26 |
-
return os.path.join(d, name)
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
def main() -> int:
|
| 30 |
-
ap = argparse.ArgumentParser()
|
| 31 |
-
ap.add_argument("--widths", default="3-12")
|
| 32 |
-
ap.add_argument("--states", type=int, default=1 << 16)
|
| 33 |
-
ap.add_argument("--cc", default="gcc")
|
| 34 |
-
args = ap.parse_args()
|
| 35 |
-
lo, hi = (int(x) for x in args.widths.split("-"))
|
| 36 |
-
from host_family import sigma
|
| 37 |
-
from check_c import parse_sigma, write_flat
|
| 38 |
-
work = tempfile.mkdtemp(prefix="host_family_")
|
| 39 |
-
exe = os.path.join(work, "netlist_eval")
|
| 40 |
-
subprocess.run([args.cc, "-O3", "-march=native", "-fopenmp", "-o", exe,
|
| 41 |
-
os.path.join(REPO, "src", "netlist_eval.c")], check=True)
|
| 42 |
-
rows, ok = [], True
|
| 43 |
-
for A in range(lo, hi + 1):
|
| 44 |
-
t0 = time.perf_counter()
|
| 45 |
-
s = sigma(A)
|
| 46 |
-
n, units, nxt = parse_sigma(s)
|
| 47 |
-
path = os.path.join(work, f"h{A}.bin")
|
| 48 |
-
write_flat(path, n, units, nxt)
|
| 49 |
-
N = max(64, args.states >> max(0, A - 10))
|
| 50 |
-
r = subprocess.run([exe, "rand", path, str(N), str(A)], capture_output=True, text=True)
|
| 51 |
-
line = r.stdout.strip()
|
| 52 |
-
good = r.returncode == 0
|
| 53 |
-
ok &= good
|
| 54 |
-
rows.append({"A": A, "sigma_bytes": len(s), "units": len(units),
|
| 55 |
-
"edges": sum(len(i) for _, i in units), "states": N,
|
| 56 |
-
"result": line, "ok": good, "seconds": time.perf_counter() - t0})
|
| 57 |
-
print(f" A={A}: {len(units):,} units, {len(s):,} bytes; {line}", flush=True)
|
| 58 |
-
os.remove(path)
|
| 59 |
-
json.dump({"widths": rows, "ok": ok}, open(runs_path("paper_host_family.json"), "w"), indent=1)
|
| 60 |
-
print("HOST FAMILY:", "PASS" if ok else "FAIL")
|
| 61 |
-
return 0 if ok else 1
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
if __name__ == "__main__":
|
| 65 |
-
sys.exit(main())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/check_host_fold.py
DELETED
|
@@ -1,73 +0,0 @@
|
|
| 1 |
-
"""The host family with its decoders folded into the read conjunctions: depth 11.
|
| 2 |
-
|
| 3 |
-
Every fetch and read conjunction and every write enable of H_A reads the bits of
|
| 4 |
-
its address directly, so a read from memory takes two levels from the address
|
| 5 |
-
bits and not three, and the operand fetch and the operand reads each lose one
|
| 6 |
-
level. The step of the folded netlist has depth 11 for every A, against 13. It is
|
| 7 |
-
written with host_family.build(A, fold=True), serialized, and one step is
|
| 8 |
-
compared with the C evaluator's own SUBLEQ on uniformly random full states, as
|
| 9 |
-
src/check_host_family.py does for H_A.
|
| 10 |
-
|
| 11 |
-
python src/check_host_fold.py [--widths 3-12] [--states N]
|
| 12 |
-
"""
|
| 13 |
-
import argparse
|
| 14 |
-
import json
|
| 15 |
-
import os
|
| 16 |
-
import subprocess
|
| 17 |
-
import sys
|
| 18 |
-
import tempfile
|
| 19 |
-
import time
|
| 20 |
-
|
| 21 |
-
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
| 22 |
-
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
def depth_of(n, units, nxt):
|
| 26 |
-
d = [0] * (n + len(units))
|
| 27 |
-
for j, (_, ins) in enumerate(units):
|
| 28 |
-
d[n + j] = 1 + max((d[k] for k, _ in ins), default=0)
|
| 29 |
-
return max(d[s] for s in nxt)
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
def main() -> int:
|
| 33 |
-
ap = argparse.ArgumentParser()
|
| 34 |
-
ap.add_argument("--widths", default="3-12")
|
| 35 |
-
ap.add_argument("--states", type=int, default=1 << 16)
|
| 36 |
-
ap.add_argument("--cc", default="gcc")
|
| 37 |
-
args = ap.parse_args()
|
| 38 |
-
lo, hi = (int(x) for x in args.widths.split("-"))
|
| 39 |
-
from host_family import build, sigma_of_net, sigma
|
| 40 |
-
from check_c import parse_sigma, write_flat
|
| 41 |
-
work = tempfile.mkdtemp(prefix="host_fold_")
|
| 42 |
-
exe = os.path.join(work, "netlist_eval")
|
| 43 |
-
subprocess.run([args.cc, "-O3", "-march=native", "-fopenmp", "-o", exe,
|
| 44 |
-
os.path.join(REPO, "src", "netlist_eval.c")], check=True)
|
| 45 |
-
rows, ok = [], True
|
| 46 |
-
for A in range(lo, hi + 1):
|
| 47 |
-
t0 = time.perf_counter()
|
| 48 |
-
net, inputs, outputs = build(A, fold=True)
|
| 49 |
-
s = sigma_of_net(net, inputs, outputs, A)
|
| 50 |
-
n, units, nxt = parse_sigma(s)
|
| 51 |
-
n0, units0, nxt0 = parse_sigma(sigma(A))
|
| 52 |
-
path = os.path.join(work, f"f{A}.bin")
|
| 53 |
-
write_flat(path, n, units, nxt)
|
| 54 |
-
N = max(64, args.states >> max(0, A - 10))
|
| 55 |
-
r = subprocess.run([exe, "rand", path, str(N), str(A)], capture_output=True, text=True)
|
| 56 |
-
line = r.stdout.strip().splitlines()[-1] if r.stdout.strip() else r.stderr.strip()
|
| 57 |
-
good = r.returncode == 0
|
| 58 |
-
ok &= good
|
| 59 |
-
row = {"A": A, "units": len(units), "edges": sum(len(i) for _, i in units),
|
| 60 |
-
"depth": depth_of(n, units, nxt), "depth_unfolded": depth_of(n0, units0, nxt0),
|
| 61 |
-
"units_unfolded": len(units0), "edges_unfolded": sum(len(i) for _, i in units0),
|
| 62 |
-
"states": N, "result": line, "agrees": good, "seconds": round(time.perf_counter() - t0, 1)}
|
| 63 |
-
rows.append(row)
|
| 64 |
-
print(f" A={A}: {row['units']:,} units, {row['edges']:,} edges, depth {row['depth']} "
|
| 65 |
-
f"(H_A: {row['units_unfolded']:,}, {row['edges_unfolded']:,}, depth {row['depth_unfolded']}); "
|
| 66 |
-
f"{line}", flush=True)
|
| 67 |
-
json.dump({"rows": rows, "all_agree": ok},
|
| 68 |
-
open(os.path.join(REPO, "paper", "runs", "paper_host_fold.json"), "w"), indent=1)
|
| 69 |
-
return 0 if ok else 1
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
if __name__ == "__main__":
|
| 73 |
-
sys.exit(main())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/check_lev.py
DELETED
|
@@ -1,121 +0,0 @@
|
|
| 1 |
-
"""Lemma 2.6 on the host: the levelized map equals the netlist it came from.
|
| 2 |
-
|
| 3 |
-
Lev(N) is asserted to compute, for every state, the next state that N computes.
|
| 4 |
-
The netlist here is the one the serialization of the host denotes, evaluated
|
| 5 |
-
gate by gate in topological order with no reference to the levelization, and the
|
| 6 |
-
two are compared on uniformly random states, which lie off every trajectory of
|
| 7 |
-
the constructor.
|
| 8 |
-
|
| 9 |
-
The same states carry the structural facts the lemma asserts: every weight of
|
| 10 |
-
every layer lies in {-1, 0, 1}, and every pre-activation is an integer, so the
|
| 11 |
-
comparator has a margin of one half.
|
| 12 |
-
"""
|
| 13 |
-
import json
|
| 14 |
-
import os
|
| 15 |
-
import sys
|
| 16 |
-
|
| 17 |
-
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
| 18 |
-
import torch
|
| 19 |
-
|
| 20 |
-
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
def runs_path(name: str) -> str:
|
| 24 |
-
d = os.path.join(REPO, "paper", "runs")
|
| 25 |
-
os.makedirs(d, exist_ok=True)
|
| 26 |
-
return os.path.join(d, name)
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
def gate_by_gate(net, inputs, outputs, V):
|
| 30 |
-
"""Evaluate the netlist directly: topological order, one gate at a time."""
|
| 31 |
-
gates = net.gates
|
| 32 |
-
indeg, cons = {}, {}
|
| 33 |
-
for g, (ins, _) in gates.items():
|
| 34 |
-
indeg[g] = len([s for s, _ in ins if s in gates])
|
| 35 |
-
for s, _ in ins:
|
| 36 |
-
cons.setdefault(s, []).append(g)
|
| 37 |
-
order = [g for g, d in indeg.items() if d == 0]
|
| 38 |
-
i = 0
|
| 39 |
-
while i < len(order):
|
| 40 |
-
for c in cons.get(order[i], []):
|
| 41 |
-
indeg[c] -= 1
|
| 42 |
-
if indeg[c] == 0:
|
| 43 |
-
order.append(c)
|
| 44 |
-
i += 1
|
| 45 |
-
assert len(order) == len(gates)
|
| 46 |
-
|
| 47 |
-
B = V.shape[0]
|
| 48 |
-
val = {"#0": torch.zeros(B), "#1": torch.ones(B)}
|
| 49 |
-
for k, name in enumerate(inputs):
|
| 50 |
-
val[name] = V[:, k]
|
| 51 |
-
for g in order:
|
| 52 |
-
ins, bias = gates[g]
|
| 53 |
-
acc = torch.full((B,), float(bias))
|
| 54 |
-
for s, w in ins:
|
| 55 |
-
acc = acc + w * val[s]
|
| 56 |
-
val[g] = (acc >= 0).float()
|
| 57 |
-
return torch.stack([val[o] for o in outputs], dim=1)
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
def main() -> int:
|
| 61 |
-
from netlist_io import net_of_sigma
|
| 62 |
-
from selfrep import LevEvaluator, NetEvaluator, read_host
|
| 63 |
-
|
| 64 |
-
sigma = read_host()
|
| 65 |
-
net, inputs, outputs, _ = net_of_sigma(sigma)
|
| 66 |
-
L = LevEvaluator(sigma, device="cpu", dense=False)
|
| 67 |
-
|
| 68 |
-
gen = torch.Generator().manual_seed(20260910)
|
| 69 |
-
B = 96
|
| 70 |
-
V = (torch.rand(B, len(inputs), generator=gen) < 0.5).float()
|
| 71 |
-
|
| 72 |
-
direct = gate_by_gate(net, inputs, outputs, V)
|
| 73 |
-
levelled = L.step(V)
|
| 74 |
-
agree = bool((direct == levelled).all())
|
| 75 |
-
print(f" netlist has {len(net.gates):,} gates on {len(inputs):,} state bits")
|
| 76 |
-
print(f" gate-by-gate evaluation equals Lev(N_host) on {B} uniformly random "
|
| 77 |
-
f"states: {'yes' if agree else 'NO'}")
|
| 78 |
-
if not agree:
|
| 79 |
-
d = (direct != levelled).nonzero()
|
| 80 |
-
print(" first disagreements:", d[:5].tolist())
|
| 81 |
-
|
| 82 |
-
grouped = NetEvaluator(sigma, device="cpu").step(V)
|
| 83 |
-
agree_net = bool((direct == grouped).all())
|
| 84 |
-
print(f" the evaluator that carries only the netlist's own predecessor "
|
| 85 |
-
f"entries agrees on the same states: {'yes' if agree_net else 'NO'}")
|
| 86 |
-
|
| 87 |
-
# The dense form: ternary weights, integer pre-activations, margin one half.
|
| 88 |
-
D = LevEvaluator(sigma, device="cpu", dense=True)
|
| 89 |
-
nonternary = [i for i, W in enumerate(D.W)
|
| 90 |
-
if not set(torch.unique(W).tolist()) <= {-1.0, 0.0, 1.0}]
|
| 91 |
-
nonint = [i for i, b in enumerate(D.B) if not bool((b == b.round()).all())]
|
| 92 |
-
print(f" every weight of all {len(D.W)} layers lies in {{-1,0,1}}: "
|
| 93 |
-
f"{'yes' if not nonternary else f'NO at {nonternary[:3]}'}")
|
| 94 |
-
print(f" every bias is an integer: {'yes' if not nonint else f'NO at {nonint[:3]}'}")
|
| 95 |
-
|
| 96 |
-
y, worst, frac = V, float("inf"), 0.0
|
| 97 |
-
for W, b in zip(D.W, D.B):
|
| 98 |
-
pre = y @ W.T + b
|
| 99 |
-
worst = min(worst, float((pre + 0.5).abs().min()))
|
| 100 |
-
frac = max(frac, float((pre - pre.round()).abs().max()))
|
| 101 |
-
y = (pre >= 0).float()
|
| 102 |
-
same_dense = bool((y == levelled).all())
|
| 103 |
-
print(f" dense stack agrees with the sparse evaluation on the same states: "
|
| 104 |
-
f"{'yes' if same_dense else 'NO'}")
|
| 105 |
-
print(f" minimum distance of a pre-activation from -1/2 over all "
|
| 106 |
-
f"{len(D.W)} layers: {worst:.6f}")
|
| 107 |
-
print(f" maximum distance of a pre-activation from an integer: {frac:.3e}")
|
| 108 |
-
|
| 109 |
-
ok = (agree and agree_net and same_dense and not nonternary
|
| 110 |
-
and not nonint and abs(worst - 0.5) < 1e-9)
|
| 111 |
-
json.dump({"states": B, "gates": len(net.gates), "agree": agree,
|
| 112 |
-
"agree_grouped": agree_net,
|
| 113 |
-
"dense_agree": same_dense, "nonternary_layers": len(nonternary),
|
| 114 |
-
"noninteger_bias_layers": len(nonint),
|
| 115 |
-
"min_margin": worst, "max_noninteger": frac},
|
| 116 |
-
open(runs_path("paper_lev.json"), "w"), indent=1)
|
| 117 |
-
return 0 if ok else 1
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
if __name__ == "__main__":
|
| 121 |
-
sys.exit(main())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/check_optimal.py
DELETED
|
@@ -1,153 +0,0 @@
|
|
| 1 |
-
"""The length bound of the completeness lemma is optimal for the token format.
|
| 2 |
-
|
| 3 |
-
A literal token covering c bytes occupies c + 1 bytes and a repeat token
|
| 4 |
-
covering a run of c equal bytes occupies 2, so the shortest recipe for a given
|
| 5 |
-
output is a shortest path over the positions of that output. The exact optimum
|
| 6 |
-
is computed here by that recurrence and compared with |f| + ceil(|f|/127) + 1
|
| 7 |
-
on strings with no two equal adjacent bytes, where no repeat token can cover
|
| 8 |
-
more than one byte and the bound is therefore attained by every shortest
|
| 9 |
-
recipe, and with the encoders of the artifact on arbitrary strings.
|
| 10 |
-
"""
|
| 11 |
-
import itertools
|
| 12 |
-
import json
|
| 13 |
-
import os
|
| 14 |
-
import random
|
| 15 |
-
import sys
|
| 16 |
-
|
| 17 |
-
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
| 18 |
-
from selfrep import describe, describe_literal, decode
|
| 19 |
-
|
| 20 |
-
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
def runs_path(name: str) -> str:
|
| 24 |
-
d = os.path.join(REPO, "paper", "runs")
|
| 25 |
-
os.makedirs(d, exist_ok=True)
|
| 26 |
-
return os.path.join(d, name)
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
def bound(n: int) -> int:
|
| 30 |
-
return n + -(-n // 127) + 1
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
def shortest(f: bytes) -> int:
|
| 34 |
-
"""Length of a shortest r in R with delta(r) = f, by dynamic programming.
|
| 35 |
-
|
| 36 |
-
cost[i] is the least number of bytes needed to cover f[i:]; the END byte is
|
| 37 |
-
added once at the end. A literal token may carry 1..127 bytes at a cost of
|
| 38 |
-
one more than it carries; a repeat token carries 1..127 copies of one byte
|
| 39 |
-
at a cost of two.
|
| 40 |
-
"""
|
| 41 |
-
n = len(f)
|
| 42 |
-
INF = float("inf")
|
| 43 |
-
cost = [INF] * (n + 1)
|
| 44 |
-
cost[n] = 0
|
| 45 |
-
for i in range(n - 1, -1, -1):
|
| 46 |
-
best = INF
|
| 47 |
-
for c in range(1, min(127, n - i) + 1):
|
| 48 |
-
v = cost[i + c] + c + 1 # literal token
|
| 49 |
-
if v < best:
|
| 50 |
-
best = v
|
| 51 |
-
run = 0
|
| 52 |
-
while run < min(127, n - i) and f[i + run] == f[i]:
|
| 53 |
-
run += 1
|
| 54 |
-
v = cost[i + run] + 2 # repeat token
|
| 55 |
-
if v < best:
|
| 56 |
-
best = v
|
| 57 |
-
cost[i] = best
|
| 58 |
-
return cost[0] + 1
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
def run_free(f: bytes) -> bool:
|
| 62 |
-
return all(f[i] != f[i + 1] for i in range(len(f) - 1))
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
def main() -> int:
|
| 66 |
-
out = {}
|
| 67 |
-
ok = True
|
| 68 |
-
|
| 69 |
-
# Exhaustive over ternary strings with no two equal adjacent bytes.
|
| 70 |
-
tested = 0
|
| 71 |
-
bad = []
|
| 72 |
-
for n in range(0, 11):
|
| 73 |
-
for t in itertools.product((0, 1, 2), repeat=n):
|
| 74 |
-
f = bytes(t)
|
| 75 |
-
if not run_free(f):
|
| 76 |
-
continue
|
| 77 |
-
tested += 1
|
| 78 |
-
s = shortest(f)
|
| 79 |
-
if s != bound(n) or len(describe_literal(f)) != bound(n):
|
| 80 |
-
bad.append(n)
|
| 81 |
-
print(f" every ternary string of length at most 10 with no two equal "
|
| 82 |
-
f"adjacent bytes ({tested:,} of them): the shortest recipe has "
|
| 83 |
-
f"exactly |f| + ceil(|f|/127) + 1 bytes: "
|
| 84 |
-
f"{'yes' if not bad else f'NO at lengths {sorted(set(bad))[:5]}'}")
|
| 85 |
-
ok &= not bad
|
| 86 |
-
out["run_free_exhaustive"] = tested
|
| 87 |
-
out["run_free_failures"] = len(bad)
|
| 88 |
-
|
| 89 |
-
# Longer run-free strings over the full alphabet, including the lengths at
|
| 90 |
-
# which a further literal token becomes necessary.
|
| 91 |
-
rng = random.Random(4)
|
| 92 |
-
lens = [0, 1, 126, 127, 128, 253, 254, 255, 380, 381, 508, 509, 700]
|
| 93 |
-
lens += [rng.randrange(0, 900) for _ in range(120)]
|
| 94 |
-
lbad = []
|
| 95 |
-
for n in lens:
|
| 96 |
-
f = bytearray()
|
| 97 |
-
prev = -1
|
| 98 |
-
for _ in range(n):
|
| 99 |
-
c = rng.randrange(256)
|
| 100 |
-
while c == prev:
|
| 101 |
-
c = rng.randrange(256)
|
| 102 |
-
f.append(c)
|
| 103 |
-
prev = c
|
| 104 |
-
f = bytes(f)
|
| 105 |
-
if shortest(f) != bound(n) or len(describe_literal(f)) != bound(n):
|
| 106 |
-
lbad.append(n)
|
| 107 |
-
print(f" {len(lens)} further run-free strings, lengths to 900 and at every "
|
| 108 |
-
f"multiple of 127: shortest recipe equals the bound: "
|
| 109 |
-
f"{'yes' if not lbad else f'NO at {lbad[:5]}'}")
|
| 110 |
-
ok &= not lbad
|
| 111 |
-
out["run_free_sampled"] = len(lens)
|
| 112 |
-
out["run_free_sampled_failures"] = len(lbad)
|
| 113 |
-
|
| 114 |
-
# On arbitrary strings the bound is an upper bound only; record how far the
|
| 115 |
-
# encoder of the artifact is from the optimum.
|
| 116 |
-
gaps = []
|
| 117 |
-
abad = 0
|
| 118 |
-
for _ in range(300):
|
| 119 |
-
n = rng.randrange(0, 600)
|
| 120 |
-
alpha = rng.choice([1, 2, 3, 9, 256])
|
| 121 |
-
f = bytes(rng.randrange(alpha) for _ in range(n))
|
| 122 |
-
s = shortest(f)
|
| 123 |
-
d = len(describe(f))
|
| 124 |
-
if decode(describe(f)) != f or d < s or d > bound(n):
|
| 125 |
-
abad += 1
|
| 126 |
-
gaps.append(d - s)
|
| 127 |
-
print(f" 300 arbitrary strings: describe is never shorter than the optimum "
|
| 128 |
-
f"and never longer than the bound: {'yes' if abad == 0 else 'NO'}; "
|
| 129 |
-
f"excess over the optimum: max {max(gaps)}, mean {sum(gaps)/len(gaps):.2f}")
|
| 130 |
-
ok &= abad == 0
|
| 131 |
-
out["arbitrary_strings"] = len(gaps)
|
| 132 |
-
out["arbitrary_failures"] = abad
|
| 133 |
-
out["describe_excess_max"] = max(gaps)
|
| 134 |
-
out["describe_excess_mean"] = sum(gaps) / len(gaps)
|
| 135 |
-
|
| 136 |
-
# The serialization of the host is not run-free, so its optimum is shorter.
|
| 137 |
-
from selfrep import HOST_PATH
|
| 138 |
-
sigma = open(HOST_PATH, "rb").read()
|
| 139 |
-
s = shortest(sigma)
|
| 140 |
-
print(f" sigma(N_host): {len(sigma):,} bytes; bound {bound(len(sigma)):,}, "
|
| 141 |
-
f"describe {len(describe(sigma)):,}, shortest recipe {s:,}")
|
| 142 |
-
out["sigma_bytes"] = len(sigma)
|
| 143 |
-
out["sigma_bound"] = bound(len(sigma))
|
| 144 |
-
out["sigma_describe"] = len(describe(sigma))
|
| 145 |
-
out["sigma_shortest"] = s
|
| 146 |
-
ok &= s <= len(describe(sigma)) <= bound(len(sigma))
|
| 147 |
-
|
| 148 |
-
json.dump(out, open(runs_path("paper_optimal.json"), "w"), indent=1)
|
| 149 |
-
return 0 if ok else 1
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
if __name__ == "__main__":
|
| 153 |
-
sys.exit(main())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/check_rocq.py
DELETED
|
@@ -1,67 +0,0 @@
|
|
| 1 |
-
"""Build the Rocq development, print the assumptions of its main theorems, and compare
|
| 2 |
-
the extracted netlist with the generator's.
|
| 3 |
-
|
| 4 |
-
The development in rocq/ proves that the two-head netlist computes the step of the
|
| 5 |
-
machine from every state at every size, that the organism builds every record image
|
| 6 |
-
from any description of it, and that it reproduces in every generation; it also
|
| 7 |
-
evaluates three generations in the kernel. Run under an environment that provides
|
| 8 |
-
rocq and ocamlfind, for instance `eval $(opam env)`:
|
| 9 |
-
|
| 10 |
-
python src/check_rocq.py
|
| 11 |
-
"""
|
| 12 |
-
import json
|
| 13 |
-
import os
|
| 14 |
-
import re
|
| 15 |
-
import shutil
|
| 16 |
-
import subprocess
|
| 17 |
-
import sys
|
| 18 |
-
import time
|
| 19 |
-
|
| 20 |
-
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
| 21 |
-
ROCQ = os.path.join(REPO, 'rocq')
|
| 22 |
-
RECORD = os.path.join(REPO, 'paper', 'runs', 'paper_rocq.json')
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
def main():
|
| 26 |
-
for tool in ('rocq', 'make', 'ocamlfind'):
|
| 27 |
-
if shutil.which(tool) is None:
|
| 28 |
-
print(f'{tool} not found on PATH')
|
| 29 |
-
return 1
|
| 30 |
-
version = subprocess.run(['rocq', '--version'], capture_output=True, text=True).stdout.strip()
|
| 31 |
-
t0 = time.time()
|
| 32 |
-
subprocess.run(['rocq', 'makefile', '-f', '_CoqProject', '-o', 'Makefile'], cwd=ROCQ, check=True)
|
| 33 |
-
subprocess.run(['make', 'clean'], cwd=ROCQ, check=True, capture_output=True)
|
| 34 |
-
jobs = str(os.cpu_count() or 1)
|
| 35 |
-
if subprocess.run(['make', '-j', jobs], cwd=ROCQ).returncode != 0:
|
| 36 |
-
print('the build failed')
|
| 37 |
-
return 1
|
| 38 |
-
build = time.time() - t0
|
| 39 |
-
out = subprocess.run(['rocq', 'compile', '-R', '.', 'TH', 'Assumptions.v'], cwd=ROCQ,
|
| 40 |
-
capture_output=True, text=True)
|
| 41 |
-
for f in os.listdir(ROCQ):
|
| 42 |
-
if f.startswith('Assumptions.') and f != 'Assumptions.v' or f == '.Assumptions.aux':
|
| 43 |
-
os.remove(os.path.join(ROCQ, f))
|
| 44 |
-
if out.returncode != 0:
|
| 45 |
-
print(out.stdout, out.stderr)
|
| 46 |
-
return 1
|
| 47 |
-
names = re.findall(r'^Print Assumptions (\S+)\.$', open(os.path.join(ROCQ, 'Assumptions.v')).read(), re.M)
|
| 48 |
-
lines = [l.strip() for l in out.stdout.splitlines() if l.strip()]
|
| 49 |
-
if len(lines) != len(names) or any(l != 'Closed under the global context' for l in lines):
|
| 50 |
-
print(out.stdout)
|
| 51 |
-
return 1
|
| 52 |
-
ext = subprocess.run([sys.executable, os.path.join(REPO, 'src', 'twohead_net.py'), 'extract'],
|
| 53 |
-
capture_output=True, text=True)
|
| 54 |
-
identical = ext.returncode == 0 and '"identical": true' in ext.stdout
|
| 55 |
-
files = [l for l in open(os.path.join(ROCQ, '_CoqProject')).read().split() if l.endswith('.v')]
|
| 56 |
-
res = dict(rocq=version.splitlines()[0], files=len(files),
|
| 57 |
-
build_seconds=round(build, 1), theorems={n: 'closed' for n in names},
|
| 58 |
-
extraction_identical=identical)
|
| 59 |
-
rec = json.load(open(RECORD)) if os.path.exists(RECORD) else {}
|
| 60 |
-
rec.update(res)
|
| 61 |
-
json.dump(rec, open(RECORD, 'w'), indent=1)
|
| 62 |
-
print(json.dumps(res, indent=1))
|
| 63 |
-
return 0 if identical else 1
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
if __name__ == '__main__':
|
| 67 |
-
sys.exit(main())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/check_sigma.py
DELETED
|
@@ -1,89 +0,0 @@
|
|
| 1 |
-
"""The serialization sigma of the ordered host netlist, written two ways.
|
| 2 |
-
|
| 3 |
-
Units are stored in the order the family builds them, every unit after its
|
| 4 |
-
predecessors; tensor entries are ordered by (element type, name); the metadata
|
| 5 |
-
object is written first with its keys in the fixed order (machine,
|
| 6 |
-
weight_quantization, signal_registry, io); the JSON uses compact separators and
|
| 7 |
-
is padded with spaces so the tensor data begins at an eight-byte boundary.
|
| 8 |
-
Rebuilding the host from source reproduces its distributed encoding byte for
|
| 9 |
-
byte, the vectorized writer host_family.sigma produces the same bytes without
|
| 10 |
-
building the netlist, and the netlist the encoding denotes serializes back to
|
| 11 |
-
the same bytes.
|
| 12 |
-
"""
|
| 13 |
-
import json
|
| 14 |
-
import os
|
| 15 |
-
import struct
|
| 16 |
-
import sys
|
| 17 |
-
|
| 18 |
-
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
| 19 |
-
import torch
|
| 20 |
-
|
| 21 |
-
from netlist_io import net_of_sigma, sigma_of_net
|
| 22 |
-
from selfrep import HOST_PATH, STATE_LAYOUT, IO_CELLS, host_netlist, sigma_host
|
| 23 |
-
|
| 24 |
-
_DTORD = {"F64": 0, "F32": 1, "F16": 2, "I64": 3, "I32": 4, "I16": 5, "I8": 6,
|
| 25 |
-
"U8": 7, "BOOL": 8}
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
def resigma(data: bytes) -> bytes:
|
| 29 |
-
"""Serialize the netlist that `data` denotes."""
|
| 30 |
-
net, inputs, outputs, meta = net_of_sigma(data)
|
| 31 |
-
return sigma_of_net(net, inputs, outputs, meta["machine"],
|
| 32 |
-
json.loads(meta["signal_registry"]),
|
| 33 |
-
json.loads(meta["io"]))
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
def main() -> int:
|
| 37 |
-
rebuilt = sigma_host()
|
| 38 |
-
dist = open(HOST_PATH, "rb").read()
|
| 39 |
-
same = rebuilt == dist
|
| 40 |
-
n = struct.unpack("<Q", dist[:8])[0]
|
| 41 |
-
h = json.loads(dist[8:8 + n])
|
| 42 |
-
meta = h.pop("__metadata__")
|
| 43 |
-
order = list(h)
|
| 44 |
-
by_dtype_name = order == sorted(order, key=lambda k: (_DTORD[h[k]["dtype"]], k))
|
| 45 |
-
signed_int = all(h[k]["dtype"] in ("I8", "I16", "I32", "I64") for k in order)
|
| 46 |
-
print(f"rebuild from source equals the distributed encoding byte for byte: {same}")
|
| 47 |
-
print(f" bytes {len(dist):,} header {n:,} tensor entries {len(order)}")
|
| 48 |
-
print(f" metadata keys in order: {list(meta)}")
|
| 49 |
-
print(f" tensor entries ordered by (element type, name): {by_dtype_name}")
|
| 50 |
-
print(f" every element type is a signed integer type: {signed_int}")
|
| 51 |
-
print(f" data begins at offset {8+n}, which is 8-aligned: {(8+n) % 8 == 0}")
|
| 52 |
-
print(f" header padded with {len(dist[8:8+n]) - len(dist[8:8+n].rstrip(b' '))} spaces")
|
| 53 |
-
|
| 54 |
-
again = sigma_host()
|
| 55 |
-
print(f" two independent builds agree: {again == rebuilt}")
|
| 56 |
-
from host_family import sigma as family_sigma
|
| 57 |
-
vec = family_sigma(8) == dist
|
| 58 |
-
print(f" the vectorized writer produces the same bytes: {vec}")
|
| 59 |
-
|
| 60 |
-
# sigma(sigma^-1(s)) = s on the image: the encoding determines the netlist
|
| 61 |
-
# and the netlist determines the encoding.
|
| 62 |
-
round_trip = resigma(dist) == dist
|
| 63 |
-
print(f" the netlist the encoding denotes serializes back to it: {round_trip}")
|
| 64 |
-
|
| 65 |
-
net, inputs, outputs = host_netlist()
|
| 66 |
-
built, _, _, _ = net_of_sigma(dist)
|
| 67 |
-
entries = sum(len(i) for i, _ in built.gates.values())
|
| 68 |
-
print(f" {len(built.gates):,} units and {entries:,} predecessor entries "
|
| 69 |
-
f"recovered from the file alone, on {len(inputs):,} state bits")
|
| 70 |
-
|
| 71 |
-
# the netlist recovered from the file computes the transition the source
|
| 72 |
-
# netlist computes, on states no trajectory visits
|
| 73 |
-
from check_lev import gate_by_gate
|
| 74 |
-
gen = torch.Generator().manual_seed(41)
|
| 75 |
-
V = (torch.rand(64, len(inputs), generator=gen) < 0.5).float()
|
| 76 |
-
src = gate_by_gate(net, inputs, outputs, V)
|
| 77 |
-
rec_net, rec_in, rec_out, _ = net_of_sigma(dist)
|
| 78 |
-
rec = gate_by_gate(rec_net, rec_in, rec_out, V)
|
| 79 |
-
agree = bool((src == rec).all())
|
| 80 |
-
print(f" it computes the transition of the source netlist on 64 uniformly "
|
| 81 |
-
f"random states: {agree}")
|
| 82 |
-
|
| 83 |
-
ok = (same and again == rebuilt and vec and round_trip and agree and by_dtype_name
|
| 84 |
-
and signed_int)
|
| 85 |
-
return 0 if ok else 1
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
if __name__ == "__main__":
|
| 89 |
-
sys.exit(main())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/check_steps.py
DELETED
|
@@ -1,169 +0,0 @@
|
|
| 1 |
-
"""Closed forms for the number of steps of the constructor and of the copier.
|
| 2 |
-
|
| 3 |
-
Reading a recipe token by token, the decoding loop of P spends a fixed number
|
| 4 |
-
of steps on the token's tag and a fixed number on each byte it contributes to
|
| 5 |
-
the output, so the whole run is a sum over the token census:
|
| 6 |
-
|
| 7 |
-
steps(P, r) = 5 f_lit + 4 f_rep + 7 L + 9 R + 8,
|
| 8 |
-
|
| 9 |
-
where L and R count the literal and repeat tokens, f_lit and f_rep the bytes
|
| 10 |
-
each kind contributes, and the last term is the end token together with the
|
| 11 |
-
halt instruction. The copying phase of P* costs six steps per tape byte and
|
| 12 |
-
two to detect the end of tape. The formulas are compared here against the
|
| 13 |
-
reference run on the recipes of the paper and on random recipes.
|
| 14 |
-
"""
|
| 15 |
-
import json
|
| 16 |
-
import os
|
| 17 |
-
import random
|
| 18 |
-
import sys
|
| 19 |
-
|
| 20 |
-
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
| 21 |
-
from selfrep import (HOST_PATH, M_P, M_STAR, describe, describe_literal, decode,
|
| 22 |
-
run_reference, tau_star, Tape, HALT_PC)
|
| 23 |
-
|
| 24 |
-
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
def runs_path(name: str) -> str:
|
| 28 |
-
d = os.path.join(REPO, "paper", "runs")
|
| 29 |
-
os.makedirs(d, exist_ok=True)
|
| 30 |
-
return os.path.join(d, name)
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
def census(r: bytes):
|
| 34 |
-
"""(literal tokens, repeat tokens, bytes from each) of a recipe."""
|
| 35 |
-
i = 0
|
| 36 |
-
lit = rep = f_lit = f_rep = 0
|
| 37 |
-
while True:
|
| 38 |
-
t = r[i]
|
| 39 |
-
i += 1
|
| 40 |
-
if t == 0:
|
| 41 |
-
return lit, rep, f_lit, f_rep
|
| 42 |
-
if t <= 127:
|
| 43 |
-
lit += 1
|
| 44 |
-
f_lit += t
|
| 45 |
-
i += t
|
| 46 |
-
else:
|
| 47 |
-
rep += 1
|
| 48 |
-
f_rep += 256 - t
|
| 49 |
-
i += 1
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
def steps_P(r: bytes) -> int:
|
| 53 |
-
"""Closed form for the run of the constructor P on a recipe."""
|
| 54 |
-
lit, rep, f_lit, f_rep = census(r)
|
| 55 |
-
return 5 * f_lit + 4 * f_rep + 7 * lit + 9 * rep + 8
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
def steps_P_star(tau: bytes):
|
| 59 |
-
"""Closed form for the three phases of P* on its own description tape."""
|
| 60 |
-
lit, rep, f_lit, f_rep = census(tau)
|
| 61 |
-
a = 5 * f_lit + 4 * f_rep + 7 * lit + 9 * rep + 7
|
| 62 |
-
return {"A": a, "R": 1, "B": 6 * len(tau) + 2}
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
def upper_bound(r: bytes) -> int:
|
| 66 |
-
"""5|delta(r)| + 9k + 8 with k the number of tokens, k <= (|r|-1)/2."""
|
| 67 |
-
lit, rep, f_lit, f_rep = census(r)
|
| 68 |
-
return 5 * (f_lit + f_rep) + 9 * (lit + rep) + 8
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
def coarse_bound(r: bytes) -> int:
|
| 72 |
-
return 5 * len(decode(r)) + (9 * (len(r) - 1)) // 2 + 8
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
def phase_split(mem0, tau: bytes):
|
| 76 |
-
mem = list(mem0)
|
| 77 |
-
dev = Tape(tau)
|
| 78 |
-
pc = 0
|
| 79 |
-
counts = {"A": 0, "R": 0, "B": 0}
|
| 80 |
-
while pc != HALT_PC:
|
| 81 |
-
counts["A" if pc < 3 * 20 else ("R" if pc == 3 * 20 else "B")] += 1
|
| 82 |
-
A = mem[pc]
|
| 83 |
-
B = mem[(pc + 1) & 0xFF]
|
| 84 |
-
C = mem[(pc + 2) & 0xFF]
|
| 85 |
-
r = (mem[B] - mem[A]) & 0xFF
|
| 86 |
-
mem[B] = r
|
| 87 |
-
pc = C if (r == 0 or r >= 0x80) else (pc + 3) & 0xFF
|
| 88 |
-
dev.apply(mem)
|
| 89 |
-
return counts
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
def main() -> int:
|
| 93 |
-
out = {}
|
| 94 |
-
ok = True
|
| 95 |
-
sigma = open(HOST_PATH, "rb").read()
|
| 96 |
-
|
| 97 |
-
named = [("r_H = describe(sigma(N_host))", describe(sigma)),
|
| 98 |
-
("the all-literal encoding of sigma(N_host)", describe_literal(sigma)),
|
| 99 |
-
("tau* (the self-reproducing tape)", tau_star(sigma, M_STAR))]
|
| 100 |
-
rows = []
|
| 101 |
-
for label, r in named:
|
| 102 |
-
_, n = run_reference(M_P, r)
|
| 103 |
-
pred = steps_P(r)
|
| 104 |
-
rows.append({"tape": label, "recipe_bytes": len(r), "output_bytes":
|
| 105 |
-
len(decode(r)), "steps": n, "closed_form": pred,
|
| 106 |
-
"agrees": n == pred, "upper": upper_bound(r),
|
| 107 |
-
"coarse": coarse_bound(r)})
|
| 108 |
-
print(f" {label}: {n:,} steps, closed form {pred:,}, "
|
| 109 |
-
f"{'agree' if n == pred else 'DISAGREE'}; "
|
| 110 |
-
f"bound 5|f|+9k+8 = {upper_bound(r):,}", flush=True)
|
| 111 |
-
ok &= n == pred and n <= upper_bound(r) <= coarse_bound(r)
|
| 112 |
-
out["named"] = rows
|
| 113 |
-
|
| 114 |
-
# random recipes: both encoders, and strings built to force each token kind
|
| 115 |
-
rng = random.Random(1009)
|
| 116 |
-
bad = 0
|
| 117 |
-
tested = 0
|
| 118 |
-
for _ in range(400):
|
| 119 |
-
n = rng.randrange(0, 700)
|
| 120 |
-
alpha = rng.choice([1, 2, 3, 17, 256])
|
| 121 |
-
f = bytes(rng.randrange(alpha) for _ in range(n))
|
| 122 |
-
for enc in (describe, describe_literal):
|
| 123 |
-
r = enc(f)
|
| 124 |
-
_, s = run_reference(M_P, r)
|
| 125 |
-
tested += 1
|
| 126 |
-
if s != steps_P(r) or s > upper_bound(r) or upper_bound(r) > coarse_bound(r):
|
| 127 |
-
bad += 1
|
| 128 |
-
print(f" {tested:,} random runs of P: closed form exact and within the "
|
| 129 |
-
f"bound on every one: {'yes' if bad == 0 else f'NO ({bad})'}", flush=True)
|
| 130 |
-
ok &= bad == 0
|
| 131 |
-
out["random_runs"] = tested
|
| 132 |
-
out["random_failures"] = bad
|
| 133 |
-
|
| 134 |
-
# the three phases of P*
|
| 135 |
-
tau = tau_star(sigma, M_STAR)
|
| 136 |
-
measured = phase_split(M_STAR, tau)
|
| 137 |
-
pred = steps_P_star(tau)
|
| 138 |
-
agree = measured == pred
|
| 139 |
-
print(f" P* phases measured {measured}, closed form {pred}: "
|
| 140 |
-
f"{'agree' if agree else 'DISAGREE'}", flush=True)
|
| 141 |
-
print(f" total {sum(measured.values()):,} = {sum(pred.values()):,}", flush=True)
|
| 142 |
-
ok &= agree
|
| 143 |
-
out["phases_measured"] = measured
|
| 144 |
-
out["phases_closed_form"] = pred
|
| 145 |
-
out["total"] = sum(pred.values())
|
| 146 |
-
|
| 147 |
-
# phase B is six steps per tape byte and two more, over recipes of every
|
| 148 |
-
# shape; P* is run only on recipes, on which it halts
|
| 149 |
-
bbad = 0
|
| 150 |
-
probes = 0
|
| 151 |
-
for _ in range(40):
|
| 152 |
-
for enc in (describe, describe_literal):
|
| 153 |
-
probe = enc(bytes(rng.randrange(rng.choice([2, 4, 256]))
|
| 154 |
-
for _ in range(rng.randrange(0, 200))))
|
| 155 |
-
got = phase_split(M_STAR, probe)
|
| 156 |
-
probes += 1
|
| 157 |
-
if got != steps_P_star(probe):
|
| 158 |
-
bbad += 1
|
| 159 |
-
print(f" phase B = 6|tau| + 2 and phase R = 1 on {probes} further recipes: "
|
| 160 |
-
f"{'yes' if bbad == 0 else f'NO ({bbad})'}", flush=True)
|
| 161 |
-
ok &= bbad == 0
|
| 162 |
-
out["phase_b_failures"] = bbad
|
| 163 |
-
|
| 164 |
-
json.dump(out, open(runs_path("paper_steps.json"), "w"), indent=1)
|
| 165 |
-
return 0 if ok else 1
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
if __name__ == "__main__":
|
| 169 |
-
sys.exit(main())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/check_ternarity.py
DELETED
|
@@ -1,142 +0,0 @@
|
|
| 1 |
-
"""The branch predicate separates ternary weights from integer weights.
|
| 2 |
-
|
| 3 |
-
The host decides a branch from the two's-complement result r by the depth-two
|
| 4 |
-
cell OR(r_7, z(r)). With integer weights the same predicate is one unit,
|
| 5 |
-
H(128 r_7 - sum_{i<7} 2^i r_i), so the depth-two cell is where the restriction
|
| 6 |
-
to {-1,0,1} is paid for. Both halves are checked here: every ternary unit on
|
| 7 |
-
the eight result bits is enumerated and none computes the predicate, and the
|
| 8 |
-
single large-weight unit is evaluated on all 256 results.
|
| 9 |
-
"""
|
| 10 |
-
import itertools
|
| 11 |
-
import json
|
| 12 |
-
import os
|
| 13 |
-
import sys
|
| 14 |
-
|
| 15 |
-
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
| 16 |
-
import torch
|
| 17 |
-
|
| 18 |
-
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
def runs_path(name: str) -> str:
|
| 22 |
-
d = os.path.join(REPO, "paper", "runs")
|
| 23 |
-
os.makedirs(d, exist_ok=True)
|
| 24 |
-
return os.path.join(d, name)
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
def predicate(n: int):
|
| 28 |
-
"""[r <= 0] for r read in two's complement on `n` bits, as a 2^n vector."""
|
| 29 |
-
rows = []
|
| 30 |
-
for v in range(1 << n):
|
| 31 |
-
two_c = v - (1 << n) if v >> (n - 1) else v
|
| 32 |
-
rows.append(1 if two_c <= 0 else 0)
|
| 33 |
-
return torch.tensor(rows, dtype=torch.long)
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
def bitmatrix(n: int):
|
| 37 |
-
"""Row v holds the bits r_0 .. r_{n-1} of v, least significant first."""
|
| 38 |
-
return torch.tensor([[(v >> i) & 1 for i in range(n)] for v in range(1 << n)],
|
| 39 |
-
dtype=torch.long)
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
def ternary_units_computing(n: int, target, bias_range):
|
| 43 |
-
"""Every (w, b) in {-1,0,1}^n x bias_range whose unit computes `target`."""
|
| 44 |
-
X = bitmatrix(n)
|
| 45 |
-
found = []
|
| 46 |
-
total = 0
|
| 47 |
-
for w in itertools.product((-1, 0, 1), repeat=n):
|
| 48 |
-
pre = X @ torch.tensor(w, dtype=torch.long)
|
| 49 |
-
for b in bias_range:
|
| 50 |
-
total += 1
|
| 51 |
-
if bool(((pre + b >= 0).long() == target).all()):
|
| 52 |
-
found.append((w, b))
|
| 53 |
-
return found, total
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
def main() -> int:
|
| 57 |
-
out = {}
|
| 58 |
-
ok = True
|
| 59 |
-
|
| 60 |
-
# The predicate is a threshold function of the eight result bits, realized
|
| 61 |
-
# by one unit whose weights are the positional values of the bits.
|
| 62 |
-
n = 8
|
| 63 |
-
target = predicate(n)
|
| 64 |
-
X = bitmatrix(n)
|
| 65 |
-
big = torch.tensor([-(1 << i) for i in range(n - 1)] + [1 << (n - 1)],
|
| 66 |
-
dtype=torch.long)
|
| 67 |
-
exact = bool(((X @ big >= 0).long() == target).all())
|
| 68 |
-
print(f" one integer-weight unit with weights "
|
| 69 |
-
f"(-1,-2,-4,-8,-16,-32,-64,+128) and bias 0 computes [r <= 0] on all "
|
| 70 |
-
f"{1 << n} results: {'yes' if exact else 'NO'}")
|
| 71 |
-
ok &= exact
|
| 72 |
-
out["integer_unit_exact"] = exact
|
| 73 |
-
out["integer_unit_max_weight"] = int(big.abs().max())
|
| 74 |
-
|
| 75 |
-
# No ternary unit does, for any integer bias. A bias outside [-n, n] makes
|
| 76 |
-
# the unit constant, so the sweep below is exhaustive over all biases.
|
| 77 |
-
bias_range = range(-(n + 2), n + 3)
|
| 78 |
-
found, total = ternary_units_computing(n, target, bias_range)
|
| 79 |
-
print(f" ternary units on the eight result bits enumerated: {total:,} "
|
| 80 |
-
f"(all {3 ** n:,} weight vectors, biases {bias_range.start} to "
|
| 81 |
-
f"{bias_range.stop - 1})")
|
| 82 |
-
print(f" ternary units computing [r <= 0]: {len(found)}"
|
| 83 |
-
f"{' (none, as the proposition asserts)' if not found else f' {found[:3]}'}")
|
| 84 |
-
ok &= not found
|
| 85 |
-
out["ternary_units_enumerated"] = total
|
| 86 |
-
out["ternary_units_computing_predicate"] = len(found)
|
| 87 |
-
# A unit with p weights +1 and q weights -1 is constant unless
|
| 88 |
-
# -p <= b <= q - 1; those biases lie inside the swept range.
|
| 89 |
-
nonconst = sum(sum(1 for x in w if x) for w in itertools.product((-1, 0, 1), repeat=n))
|
| 90 |
-
print(f" of these, {nonconst:,} are not constant, and none of them computes "
|
| 91 |
-
f"[r <= 0]")
|
| 92 |
-
out["nonconstant_ternary_units"] = nonconst
|
| 93 |
-
|
| 94 |
-
# The separation begins at three bits: on two bits the predicate is ternary.
|
| 95 |
-
firstfail = None
|
| 96 |
-
widths = {}
|
| 97 |
-
for m in range(2, 9):
|
| 98 |
-
f, _ = ternary_units_computing(m, predicate(m), range(-(m + 2), m + 3))
|
| 99 |
-
widths[m] = len(f)
|
| 100 |
-
if not f and firstfail is None:
|
| 101 |
-
firstfail = m
|
| 102 |
-
print(f" ternary realizations by width: {widths}; the predicate stops "
|
| 103 |
-
f"being a ternary threshold function at {firstfail} bits")
|
| 104 |
-
out["ternary_realizations_by_width"] = widths
|
| 105 |
-
out["first_width_without_ternary_unit"] = firstfail
|
| 106 |
-
ok &= firstfail == 3
|
| 107 |
-
|
| 108 |
-
# Depth two suffices, by the cell the host uses.
|
| 109 |
-
z = (X.sum(dim=1) == 0).long()
|
| 110 |
-
leq = ((X[:, n - 1] + z) >= 1).long()
|
| 111 |
-
two = bool((leq == target).all())
|
| 112 |
-
print(f" the depth-two cell OR(r_7, z(r)) of the host computes it on all "
|
| 113 |
-
f"{1 << n} results: {'yes' if two else 'NO'}")
|
| 114 |
-
ok &= two
|
| 115 |
-
out["depth_two_cell_exact"] = two
|
| 116 |
-
|
| 117 |
-
# The 256-way address decoder is not an instance of the same exchange: it
|
| 118 |
-
# is one ternary unit already.
|
| 119 |
-
from matrix8 import Net
|
| 120 |
-
net = Net()
|
| 121 |
-
bits = [f"a{i}" for i in range(8)]
|
| 122 |
-
net.DECODE("sel", bits, 0xA7)
|
| 123 |
-
ins, bias = net.gates["sel"]
|
| 124 |
-
dec_ternary = all(w in (-1, 0, 1) for _, w in ins)
|
| 125 |
-
A = bitmatrix(8)
|
| 126 |
-
# DECODE reads its bits most significant first
|
| 127 |
-
w = torch.tensor([wt for _, wt in ins], dtype=torch.long)
|
| 128 |
-
pre = A.flip(1) @ w + bias
|
| 129 |
-
hit = (pre >= 0).long()
|
| 130 |
-
dec_exact = bool((hit == (torch.arange(256) == 0xA7).long()).all())
|
| 131 |
-
print(f" the address decoder of the memory is one ternary unit of fan-in "
|
| 132 |
-
f"eight and fires on exactly one address: "
|
| 133 |
-
f"{'yes' if dec_ternary and dec_exact else 'NO'}")
|
| 134 |
-
ok &= dec_ternary and dec_exact
|
| 135 |
-
out["decoder_is_one_ternary_unit"] = dec_ternary and dec_exact
|
| 136 |
-
|
| 137 |
-
json.dump(out, open(runs_path("paper_ternarity.json"), "w"), indent=1)
|
| 138 |
-
return 0 if ok else 1
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
if __name__ == "__main__":
|
| 142 |
-
sys.exit(main())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/constructor8.py
DELETED
|
@@ -1,580 +0,0 @@
|
|
| 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, not 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.dirname(os.path.abspath(__file__))) # repo root; this module lives in src/
|
| 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 |
-
from host_netlist import subleq_state_names, build_subleq_step_net
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
class MatrixHost:
|
| 273 |
-
"""The subleq8io processor as a recurrent ternary linear-threshold
|
| 274 |
-
network, with the device cells poked between recurrences."""
|
| 275 |
-
|
| 276 |
-
N = 8 + 1 + 2048
|
| 277 |
-
|
| 278 |
-
def __init__(self, device: str = "cpu"):
|
| 279 |
-
t0 = time.perf_counter()
|
| 280 |
-
net, inputs, outputs = build_subleq_step_net()
|
| 281 |
-
layers, info = compile_net(net, inputs, outputs)
|
| 282 |
-
for W, Bt in layers:
|
| 283 |
-
vals = set(torch.unique(W).tolist())
|
| 284 |
-
assert vals <= {-1, 0, 1}
|
| 285 |
-
self.W = [W.to(device=device, dtype=torch.float32) for W, _ in layers]
|
| 286 |
-
self.B = [Bt.to(device=device, dtype=torch.float32) for _, Bt in layers]
|
| 287 |
-
self.device = device
|
| 288 |
-
self.info = info
|
| 289 |
-
print(f" host compiled to {info['layers']} ternary matrices "
|
| 290 |
-
f"(max width {info['max_width']}, "
|
| 291 |
-
f"{info['total_weights']:,} weights) in "
|
| 292 |
-
f"{time.perf_counter() - t0:.1f}s on {device}")
|
| 293 |
-
|
| 294 |
-
def _vec(self, pc: int, mem: List[int], halt: bool = False) -> torch.Tensor:
|
| 295 |
-
v = torch.zeros(self.N)
|
| 296 |
-
for k in range(8):
|
| 297 |
-
v[k] = (pc >> (7 - k)) & 1
|
| 298 |
-
v[8] = 1.0 if halt else 0.0
|
| 299 |
-
for j in range(256):
|
| 300 |
-
for k in range(8):
|
| 301 |
-
v[9 + j * 8 + k] = (mem[j] >> (7 - k)) & 1
|
| 302 |
-
return v
|
| 303 |
-
|
| 304 |
-
def step(self, v: torch.Tensor) -> torch.Tensor:
|
| 305 |
-
for W, b in zip(self.W, self.B):
|
| 306 |
-
v = ((v @ W.T + b) >= 0).float()
|
| 307 |
-
return v
|
| 308 |
-
|
| 309 |
-
@staticmethod
|
| 310 |
-
def _byte(vcpu: torch.Tensor, j: int) -> int:
|
| 311 |
-
x = 0
|
| 312 |
-
for k in range(8):
|
| 313 |
-
x = (x << 1) | int(vcpu[9 + j * 8 + k])
|
| 314 |
-
return x
|
| 315 |
-
|
| 316 |
-
def run(self, tape: bytes, max_steps: int, expect: Optional[bytes] = None,
|
| 317 |
-
report_every: int = 100000) -> Tuple[bytes, int]:
|
| 318 |
-
mem = constructor_image()
|
| 319 |
-
mem[IN_DATA] = tape[0] if tape else 0
|
| 320 |
-
v = self._vec(0, mem).unsqueeze(0).to(self.device)
|
| 321 |
-
out = bytearray()
|
| 322 |
-
head = 0
|
| 323 |
-
n = 0
|
| 324 |
-
idx_pch = torch.arange(0, 9, device=self.device)
|
| 325 |
-
t0 = time.perf_counter()
|
| 326 |
-
while n < max_steps:
|
| 327 |
-
front = v[0, idx_pch].tolist()
|
| 328 |
-
if front[8] >= 0.5:
|
| 329 |
-
break
|
| 330 |
-
pcv = 0
|
| 331 |
-
for k in range(8):
|
| 332 |
-
pcv = (pcv << 1) | int(front[k])
|
| 333 |
-
bidx = (pcv + 1) & 0xFF
|
| 334 |
-
bbits = v[0, 9 + bidx * 8: 9 + bidx * 8 + 8].tolist()
|
| 335 |
-
Bc = 0
|
| 336 |
-
for k in range(8):
|
| 337 |
-
Bc = (Bc << 1) | int(bbits[k])
|
| 338 |
-
v = self.step(v)
|
| 339 |
-
n += 1
|
| 340 |
-
if Bc == OUT:
|
| 341 |
-
ob = v[0, 9 + OUT * 8: 9 + OUT * 8 + 8].tolist()
|
| 342 |
-
val = 0
|
| 343 |
-
for k in range(8):
|
| 344 |
-
val = (val << 1) | int(ob[k])
|
| 345 |
-
out.append(val)
|
| 346 |
-
if expect is not None and out[-1] != expect[len(out) - 1]:
|
| 347 |
-
raise AssertionError(
|
| 348 |
-
f"stream diverged at byte {len(out) - 1}: "
|
| 349 |
-
f"got {out[-1]:#04x} want {expect[len(out) - 1]:#04x}")
|
| 350 |
-
v[0, 9 + OUT * 8: 9 + OUT * 8 + 8] = 0.0
|
| 351 |
-
elif Bc == IN_STROBE:
|
| 352 |
-
head += 1
|
| 353 |
-
v[0, 9 + IN_STROBE * 8: 9 + IN_STROBE * 8 + 8] = 0.0
|
| 354 |
-
nxt = tape[head] if head < len(tape) else 0
|
| 355 |
-
for k in range(8):
|
| 356 |
-
v[0, 9 + IN_DATA * 8 + k] = float((nxt >> (7 - k)) & 1)
|
| 357 |
-
if report_every and n % report_every == 0:
|
| 358 |
-
rate = n / (time.perf_counter() - t0)
|
| 359 |
-
print(f" ... {n:,} recurrences, {len(out):,} bytes emitted "
|
| 360 |
-
f"({rate:,.0f} instr/s)")
|
| 361 |
-
return bytes(out), n
|
| 362 |
-
|
| 363 |
-
def exhaustive_datapath(self) -> bool:
|
| 364 |
-
"""All 65,536 (x, y) operand pairs through the matrices in one batch."""
|
| 365 |
-
xs = torch.arange(65536) % 256
|
| 366 |
-
ys = torch.arange(65536) // 256
|
| 367 |
-
V = torch.zeros(65536, self.N)
|
| 368 |
-
mem = [0] * 256
|
| 369 |
-
mem[0], mem[1], mem[2] = 0x90, 0x91, 0x30
|
| 370 |
-
v0 = self._vec(0, mem)
|
| 371 |
-
V[:] = v0
|
| 372 |
-
for k in range(8):
|
| 373 |
-
V[:, 9 + 0x90 * 8 + k] = ((xs >> (7 - k)) & 1).float()
|
| 374 |
-
V[:, 9 + 0x91 * 8 + k] = ((ys >> (7 - k)) & 1).float()
|
| 375 |
-
V = V.to(self.device)
|
| 376 |
-
for W, b in zip(self.W, self.B):
|
| 377 |
-
V = ((V @ W.T + b) >= 0).float()
|
| 378 |
-
V = V.cpu()
|
| 379 |
-
r = torch.zeros(65536, dtype=torch.long)
|
| 380 |
-
for k in range(8):
|
| 381 |
-
r = (r << 1) | V[:, 9 + 0x91 * 8 + k].long()
|
| 382 |
-
pcv = torch.zeros(65536, dtype=torch.long)
|
| 383 |
-
for k in range(8):
|
| 384 |
-
pcv = (pcv << 1) | V[:, k].long()
|
| 385 |
-
exp_r = (ys - xs) & 0xFF
|
| 386 |
-
exp_leq = (exp_r == 0) | (exp_r >= 0x80)
|
| 387 |
-
exp_pc = torch.where(exp_leq, torch.full_like(pcv, 0x30),
|
| 388 |
-
torch.full_like(pcv, 3))
|
| 389 |
-
ok = bool((r == exp_r).all()) and bool((pcv == exp_pc).all())
|
| 390 |
-
print(f" {'OK ' if ok else 'FAIL'} matrix datapath exhaustive: "
|
| 391 |
-
f"65,536/65,536 subtract results and branch decisions exact")
|
| 392 |
-
return ok
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
# =============================================================================
|
| 396 |
-
# Verification
|
| 397 |
-
# =============================================================================
|
| 398 |
-
|
| 399 |
-
def sha(b: bytes) -> str:
|
| 400 |
-
return hashlib.sha256(b).hexdigest()[:16]
|
| 401 |
-
|
| 402 |
-
|
| 403 |
-
def family_files() -> List[str]:
|
| 404 |
-
files = [os.path.join(REPO, "neural_computer.safetensors")]
|
| 405 |
-
vdir = os.path.join(REPO, "variants")
|
| 406 |
-
files += sorted(os.path.join(vdir, f) for f in os.listdir(vdir)
|
| 407 |
-
if f.endswith(".safetensors"))
|
| 408 |
-
return files
|
| 409 |
-
|
| 410 |
-
|
| 411 |
-
def verify(device: str) -> bool:
|
| 412 |
-
ok = True
|
| 413 |
-
|
| 414 |
-
print("\n[1/5] Host machine soundness (shipped netlist, exhaustive datapath)")
|
| 415 |
-
from eval import NetlistEvaluator, load_metadata
|
| 416 |
-
from machines import load_tensors
|
| 417 |
-
T = load_tensors(MODEL_PATH)
|
| 418 |
-
reg = load_metadata(MODEL_PATH)["signal_registry"]
|
| 419 |
-
ne = NetlistEvaluator(T, reg, "subleq")
|
| 420 |
-
a_v = torch.arange(65536) % 256
|
| 421 |
-
b_v = torch.arange(65536) // 256
|
| 422 |
-
ext = {}
|
| 423 |
-
for k in range(8):
|
| 424 |
-
ext[f"$a[{k}]"] = ((a_v >> k) & 1).float()
|
| 425 |
-
ext[f"$b[{k}]"] = ((b_v >> k) & 1).float()
|
| 426 |
-
ext[f"$pc[{k}]"] = torch.zeros(65536)
|
| 427 |
-
ext[f"$c[{k}]"] = torch.zeros(65536)
|
| 428 |
-
o = ne.run(ext)
|
| 429 |
-
rr = sum(o[f"subleq.sub.fa{k}.ha2.sum.layer2"][:, 0].long() << k for k in range(8))
|
| 430 |
-
lq = o["subleq.leq"][:, 0].long()
|
| 431 |
-
exp_r = (b_v - a_v) & 0xFF
|
| 432 |
-
exp_l = ((exp_r == 0) | (exp_r >= 128)).long()
|
| 433 |
-
good = bool((rr == exp_r).all()) and bool((lq == exp_l).all())
|
| 434 |
-
print(f" {'OK ' if good else 'FAIL'} gate netlist exhaustive: 65,536/65,536 exact")
|
| 435 |
-
ok &= good
|
| 436 |
-
|
| 437 |
-
print("\n[2/5] Recipe codec round-trip over the whole family")
|
| 438 |
-
total = 0
|
| 439 |
-
for p in family_files():
|
| 440 |
-
data = open(p, "rb").read()
|
| 441 |
-
tape = describe(data)
|
| 442 |
-
good = decode(tape) == data
|
| 443 |
-
ok &= good
|
| 444 |
-
total += len(data)
|
| 445 |
-
print(f" {'OK ' if good else 'FAIL'} {os.path.basename(p):<44} "
|
| 446 |
-
f"{len(data):>10,} B -> tape {len(tape):>10,} B sha {sha(data)}")
|
| 447 |
-
print(f" ({total / 1e6:.0f} MB described and decoded back, byte-identical)")
|
| 448 |
-
|
| 449 |
-
print("\n[3/5] Constructor program on the integer reference "
|
| 450 |
-
"(output stream == target file, byte for byte)")
|
| 451 |
-
targets = [MODEL_PATH,
|
| 452 |
-
os.path.join(REPO, "variants", "neural_subleq8.safetensors"),
|
| 453 |
-
os.path.join(REPO, "variants", "neural_matrix8.safetensors")]
|
| 454 |
-
for p in targets:
|
| 455 |
-
data = open(p, "rb").read()
|
| 456 |
-
tape = describe(data)
|
| 457 |
-
t0 = time.perf_counter()
|
| 458 |
-
out, steps = ref_construct(tape)
|
| 459 |
-
dt = time.perf_counter() - t0
|
| 460 |
-
good = out == data
|
| 461 |
-
ok &= good
|
| 462 |
-
tag = "SELF-REPRODUCTION" if p == MODEL_PATH else "universal build"
|
| 463 |
-
print(f" {'OK ' if good else 'FAIL'} {os.path.basename(p):<40} "
|
| 464 |
-
f"{len(out):>10,} B in {steps:>12,} instructions ({dt:.1f}s) "
|
| 465 |
-
f"[{tag}] sha {sha(out)}")
|
| 466 |
-
|
| 467 |
-
print("\n[4/5] Matrix backend (machine-1 compile of the host)")
|
| 468 |
-
mh = MatrixHost(device=device)
|
| 469 |
-
ok &= mh.exhaustive_datapath()
|
| 470 |
-
|
| 471 |
-
print("\n[5/5] Backend lockstep on a shared prefix "
|
| 472 |
-
"(gate graph vs matrices vs reference, first 600 cycles)")
|
| 473 |
-
data = open(MODEL_PATH, "rb").read()
|
| 474 |
-
tape = describe(data)
|
| 475 |
-
gh = GateHost()
|
| 476 |
-
s = {"pc": 0, "mem": constructor_image(), "halted": False}
|
| 477 |
-
s["mem"][IN_DATA] = tape[0]
|
| 478 |
-
mem2 = constructor_image()
|
| 479 |
-
mem2[IN_DATA] = tape[0]
|
| 480 |
-
v = mh._vec(0, mem2).unsqueeze(0).to(device)
|
| 481 |
-
headg = headm = 0
|
| 482 |
-
outg, outm = bytearray(), bytearray()
|
| 483 |
-
good = True
|
| 484 |
-
for n in range(600):
|
| 485 |
-
Bg = s["mem"][(s["pc"] + 1) & 0xFF]
|
| 486 |
-
s = gh.cpu.step(s)
|
| 487 |
-
if Bg == OUT:
|
| 488 |
-
outg.append(s["mem"][OUT])
|
| 489 |
-
s["mem"][OUT] = 0
|
| 490 |
-
elif Bg == IN_STROBE:
|
| 491 |
-
headg += 1
|
| 492 |
-
s["mem"][IN_STROBE] = 0
|
| 493 |
-
s["mem"][IN_DATA] = tape[headg] if headg < len(tape) else 0
|
| 494 |
-
vc = v[0].cpu()
|
| 495 |
-
pcm = 0
|
| 496 |
-
for k in range(8):
|
| 497 |
-
pcm = (pcm << 1) | int(vc[k])
|
| 498 |
-
Bm = MatrixHost._byte(vc, (pcm + 1) & 0xFF)
|
| 499 |
-
v = mh.step(v)
|
| 500 |
-
if Bm == OUT:
|
| 501 |
-
outm.append(MatrixHost._byte(v[0].cpu(), OUT))
|
| 502 |
-
v[0, 9 + OUT * 8: 9 + OUT * 8 + 8] = 0.0
|
| 503 |
-
elif Bm == IN_STROBE:
|
| 504 |
-
headm += 1
|
| 505 |
-
v[0, 9 + IN_STROBE * 8: 9 + IN_STROBE * 8 + 8] = 0.0
|
| 506 |
-
nxt = tape[headm] if headm < len(tape) else 0
|
| 507 |
-
for k in range(8):
|
| 508 |
-
v[0, 9 + IN_DATA * 8 + k] = float((nxt >> (7 - k)) & 1)
|
| 509 |
-
vc = v[0].cpu()
|
| 510 |
-
pcm = 0
|
| 511 |
-
for k in range(8):
|
| 512 |
-
pcm = (pcm << 1) | int(vc[k])
|
| 513 |
-
memm = [MatrixHost._byte(vc, j) for j in range(256)]
|
| 514 |
-
if pcm != s["pc"] or memm != s["mem"] or outg != outm:
|
| 515 |
-
print(f" FAIL lockstep diverged at cycle {n}")
|
| 516 |
-
good = False
|
| 517 |
-
break
|
| 518 |
-
print(f" {'OK ' if good else 'FAIL'} gate graph and matrix form agree "
|
| 519 |
-
f"cycle-for-cycle for 600 cycles ({len(outg)} bytes emitted identically)")
|
| 520 |
-
ok &= good
|
| 521 |
-
|
| 522 |
-
print("\nCONSTRUCTOR VERIFICATION:", "PASS" if ok else "FAIL")
|
| 523 |
-
return ok
|
| 524 |
-
|
| 525 |
-
|
| 526 |
-
def self_reproduce(device: str, gen2: bool = True) -> bool:
|
| 527 |
-
"""Full self-reproduction at threshold fidelity on the matrix backend."""
|
| 528 |
-
data = open(MODEL_PATH, "rb").read()
|
| 529 |
-
tape = describe(data)
|
| 530 |
-
print(f"\nSelf-reproduction on the threshold matrices: target "
|
| 531 |
-
f"{len(data):,} bytes (sha {sha(data)}), tape {len(tape):,} bytes")
|
| 532 |
-
ref_out, ref_steps = ref_construct(tape)
|
| 533 |
-
assert ref_out == data, "reference construction failed"
|
| 534 |
-
print(f" reference: {ref_steps:,} instructions")
|
| 535 |
-
|
| 536 |
-
mh = MatrixHost(device=device)
|
| 537 |
-
t0 = time.perf_counter()
|
| 538 |
-
out, n = mh.run(tape, max_steps=ref_steps + 64, expect=data)
|
| 539 |
-
dt = time.perf_counter() - t0
|
| 540 |
-
ok = out == data and n == ref_steps
|
| 541 |
-
print(f" GEN 1: emitted {len(out):,} bytes in {n:,} recurrences "
|
| 542 |
-
f"({dt / 60:.1f} min, {n / dt:,.0f} instr/s) sha {sha(out)}")
|
| 543 |
-
print(f" {'OK ' if ok else 'FAIL'} output stream == host safetensors file, "
|
| 544 |
-
f"byte for byte; instruction count matches reference exactly")
|
| 545 |
-
|
| 546 |
-
if ok and gen2:
|
| 547 |
-
off_path = os.path.join(REPO, "variants", "_offspring_subleq8io.safetensors")
|
| 548 |
-
with open(off_path, "wb") as f:
|
| 549 |
-
f.write(out)
|
| 550 |
-
gh = GateHost(off_path) # boot the offspring's own gates
|
| 551 |
-
probe_tape = describe(out[:2048])
|
| 552 |
-
want = out[:2048]
|
| 553 |
-
got, steps, _ = gh.run(probe_tape, max_steps=len(probe_tape) * 8 + 64)
|
| 554 |
-
good2 = got == want
|
| 555 |
-
print(f" GEN 2: offspring file booted as a machine (gate graph); "
|
| 556 |
-
f"constructed the first {len(want):,} bytes of itself in "
|
| 557 |
-
f"{steps:,} instructions: {'exact' if good2 else 'MISMATCH'}")
|
| 558 |
-
os.remove(off_path)
|
| 559 |
-
ok &= good2
|
| 560 |
-
print("SELF-REPRODUCTION:", "PASS" if ok else "FAIL")
|
| 561 |
-
return ok
|
| 562 |
-
|
| 563 |
-
|
| 564 |
-
def main() -> int:
|
| 565 |
-
ap = argparse.ArgumentParser()
|
| 566 |
-
ap.add_argument("cmd", choices=["build", "verify", "self", "all"])
|
| 567 |
-
ap.add_argument("--device", default="cuda" if torch.cuda.is_available() else "cpu")
|
| 568 |
-
args = ap.parse_args()
|
| 569 |
-
rc = 0
|
| 570 |
-
if args.cmd in ("build", "all"):
|
| 571 |
-
build_host(save=True)
|
| 572 |
-
if args.cmd in ("verify", "all"):
|
| 573 |
-
rc |= 0 if verify(args.device) else 1
|
| 574 |
-
if args.cmd in ("self", "all"):
|
| 575 |
-
rc |= 0 if self_reproduce(args.device) else 1
|
| 576 |
-
return rc
|
| 577 |
-
|
| 578 |
-
|
| 579 |
-
if __name__ == "__main__":
|
| 580 |
-
sys.exit(main())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/eval.py
CHANGED
|
@@ -1,30 +1,15 @@
|
|
| 1 |
-
"""
|
| 2 |
-
|
| 3 |
-
======================================================
|
| 4 |
-
GPU-batched evaluation with per-circuit reporting.
|
| 5 |
-
Includes CPU runtime for threshold-weight execution.
|
| 6 |
-
|
| 7 |
-
Usage:
|
| 8 |
-
python eval.py # Run circuit evaluation
|
| 9 |
-
python eval.py --device cpu # CPU mode
|
| 10 |
-
python eval.py --pop_size 1000 # Population mode for evolution
|
| 11 |
-
python eval.py --cpu-test # Run CPU smoke test
|
| 12 |
-
|
| 13 |
-
The single gate-routed CPU runtime is GenericThresholdCPU in eval_all.py
|
| 14 |
-
(manifest-sized); the pure-Python reference (CPUState / ref_step) lives here
|
| 15 |
-
for cross-checks.
|
| 16 |
-
|
| 17 |
-
API (for prune_weights.py):
|
| 18 |
-
from eval import load_model, create_population, BatchedFitnessEvaluator
|
| 19 |
-
"""
|
| 20 |
|
| 21 |
import argparse
|
| 22 |
import json
|
| 23 |
import os
|
|
|
|
| 24 |
import time
|
| 25 |
from collections import defaultdict
|
| 26 |
from dataclasses import dataclass, field
|
| 27 |
-
from
|
|
|
|
| 28 |
|
| 29 |
import torch
|
| 30 |
from safetensors import safe_open
|
|
@@ -71,11 +56,7 @@ def load_metadata(path: str = MODEL_PATH) -> Dict:
|
|
| 71 |
|
| 72 |
|
| 73 |
def get_manifest(tensors: Dict[str, torch.Tensor]) -> Dict[str, int]:
|
| 74 |
-
"""Extract manifest values from tensors.
|
| 75 |
-
|
| 76 |
-
Returns dict with data_bits, addr_bits, memory_bytes, version.
|
| 77 |
-
Defaults to 8-bit data, 16-bit addr for legacy models.
|
| 78 |
-
"""
|
| 79 |
return {
|
| 80 |
'data_bits': int(tensors.get('manifest.data_bits', torch.tensor([8.0])).item()),
|
| 81 |
'addr_bits': int(tensors.get('manifest.addr_bits',
|
|
@@ -102,16 +83,25 @@ def create_population(
|
|
| 102 |
# =============================================================================
|
| 103 |
|
| 104 |
FLAG_NAMES = ["Z", "N", "C", "V"]
|
|
|
|
| 105 |
CTRL_NAMES = ["HALT", "MEM_WE", "MEM_RE", "RESERVED"]
|
| 106 |
|
| 107 |
PC_BITS = 16
|
|
|
|
| 108 |
IR_BITS = 16
|
|
|
|
| 109 |
REG_BITS = 8
|
|
|
|
| 110 |
REG_COUNT = 4
|
|
|
|
| 111 |
FLAG_BITS = 4
|
|
|
|
| 112 |
SP_BITS = 16
|
|
|
|
| 113 |
CTRL_BITS = 4
|
|
|
|
| 114 |
MEM_BYTES = 65536
|
|
|
|
| 115 |
MEM_BITS = MEM_BYTES * 8
|
| 116 |
|
| 117 |
STATE_BITS = PC_BITS + IR_BITS + (REG_BITS * REG_COUNT) + FLAG_BITS + SP_BITS + CTRL_BITS + MEM_BITS
|
|
@@ -405,18 +395,9 @@ def _fill_smoke_program(mem: List[int]) -> None:
|
|
| 405 |
|
| 406 |
|
| 407 |
def run_smoke_test() -> int:
|
| 408 |
-
"""
|
| 409 |
-
(GenericThresholdCPU), cross-checked against the pure-Python reference:
|
| 410 |
-
|
| 411 |
-
1. LOAD 5, LOAD 7, ADD, STORE -> MEM[0x0102] = 12
|
| 412 |
-
2. LOAD 2, LOAD 131, MUL, STORE -> MEM[0x0105] = (2*131) & 0xFF = 6
|
| 413 |
-
3. Countdown loop 3+2+1 via SUB/JNZ -> MEM[0x0109] = 6
|
| 414 |
-
|
| 415 |
-
Runs on the 1 KB variant so the threshold pass finishes in seconds; the
|
| 416 |
-
reference runs at 64 KB in pure Python.
|
| 417 |
-
"""
|
| 418 |
import os
|
| 419 |
-
|
| 420 |
|
| 421 |
expected = {0x0102: 12, 0x0105: 6, 0x0109: 6}
|
| 422 |
|
|
@@ -461,18 +442,7 @@ def run_smoke_test() -> int:
|
|
| 461 |
# =============================================================================
|
| 462 |
|
| 463 |
class NetlistEvaluator:
|
| 464 |
-
"""Evaluate a self-contained circuit from its shipped wiring metadata.
|
| 465 |
-
|
| 466 |
-
The gate graph is built from the .inputs tensors and the header signal
|
| 467 |
-
registry — the same artifacts safetensors2verilog consumes — so a test
|
| 468 |
-
driven through this class proves the file itself encodes the circuit,
|
| 469 |
-
with no wiring knowledge living in Python.
|
| 470 |
-
|
| 471 |
-
External signals are names starting with '$' (plus the constants #0/#1);
|
| 472 |
-
the caller binds them per evaluation. Gate outputs are published under
|
| 473 |
-
their full gate names. Evaluation is batched over test vectors and,
|
| 474 |
-
for population dicts, over population slots.
|
| 475 |
-
"""
|
| 476 |
|
| 477 |
def __init__(self, tensors: Dict[str, torch.Tensor],
|
| 478 |
signal_registry: Dict[str, str], prefix: str,
|
|
@@ -578,8 +548,7 @@ class NetlistEvaluator:
|
|
| 578 |
return sorted(out)
|
| 579 |
|
| 580 |
def run(self, external: Dict[str, torch.Tensor]) -> Dict[str, torch.Tensor]:
|
| 581 |
-
"""Evaluate the circuit.
|
| 582 |
-
scalar); every returned gate output is [num_tests, pop_size]."""
|
| 583 |
first = next(iter(external.values()))
|
| 584 |
if not torch.is_tensor(first):
|
| 585 |
num_tests = 1
|
|
@@ -635,8 +604,7 @@ class _SlotView:
|
|
| 635 |
|
| 636 |
|
| 637 |
def float_bits_to_value(word: int, exp_bits: int, frac_bits: int) -> float:
|
| 638 |
-
"""Decode an IEEE 754 word to a Python float
|
| 639 |
-
float16/float32 value exactly, so the oracle comparisons are exact."""
|
| 640 |
sign = (word >> (exp_bits + frac_bits)) & 1
|
| 641 |
exp = (word >> frac_bits) & ((1 << exp_bits) - 1)
|
| 642 |
frac = word & ((1 << frac_bits) - 1)
|
|
@@ -653,8 +621,7 @@ def float_bits_to_value(word: int, exp_bits: int, frac_bits: int) -> float:
|
|
| 653 |
|
| 654 |
|
| 655 |
def float_test_words(exp_bits: int, frac_bits: int) -> Tuple[List[int], List[int]]:
|
| 656 |
-
"""Directed IEEE edge encodings (every category, both signs) plus seeded
|
| 657 |
-
random words for a family."""
|
| 658 |
import random
|
| 659 |
E, F = exp_bits, frac_bits
|
| 660 |
emax = (1 << E) - 1
|
|
@@ -684,10 +651,7 @@ def float_test_words(exp_bits: int, frac_bits: int) -> Tuple[List[int], List[int
|
|
| 684 |
|
| 685 |
|
| 686 |
def float_mul_oracle(aw: int, bw: int, exp_bits: int, frac_bits: int) -> int:
|
| 687 |
-
"""
|
| 688 |
-
specials (NaN, infinities, signed zeros), subnormal operands and
|
| 689 |
-
gradual-underflow subnormal results, round-to-nearest-even mantissa.
|
| 690 |
-
Pure integer arithmetic, so exact."""
|
| 691 |
E, F = exp_bits, frac_bits
|
| 692 |
emax = (1 << E) - 1
|
| 693 |
fmask = (1 << F) - 1
|
|
@@ -752,10 +716,7 @@ def float_mul_oracle(aw: int, bw: int, exp_bits: int, frac_bits: int) -> int:
|
|
| 752 |
|
| 753 |
|
| 754 |
def float_div_oracle(aw: int, bw: int, exp_bits: int, frac_bits: int) -> int:
|
| 755 |
-
"""
|
| 756 |
-
specials (NaN, infinities, x/0 -> inf, 0/0 and inf/inf -> NaN, signed
|
| 757 |
-
zeros), subnormal operands and gradual-underflow subnormal results,
|
| 758 |
-
round-to-nearest-even mantissa."""
|
| 759 |
E, F = exp_bits, frac_bits
|
| 760 |
emax = (1 << E) - 1
|
| 761 |
fmask = (1 << F) - 1
|
|
@@ -817,10 +778,7 @@ def float_div_oracle(aw: int, bw: int, exp_bits: int, frac_bits: int) -> int:
|
|
| 817 |
|
| 818 |
|
| 819 |
def float_fma_oracle(aw: int, bw: int, cw: int, exp_bits: int, frac_bits: int) -> int:
|
| 820 |
-
"""
|
| 821 |
-
specials (NaN, infinities, inf*0 and inf-inf -> NaN, signed zeros, exact
|
| 822 |
-
cancellation -> +0), subnormal operands and gradual-underflow results,
|
| 823 |
-
round-to-nearest-even. Pure integer arithmetic, so exact."""
|
| 824 |
E, F = exp_bits, frac_bits
|
| 825 |
emax = (1 << E) - 1
|
| 826 |
fmask = (1 << F) - 1
|
|
@@ -910,11 +868,7 @@ def float_fma_oracle(aw: int, bw: int, cw: int, exp_bits: int, frac_bits: int) -
|
|
| 910 |
|
| 911 |
|
| 912 |
def float_add_oracle(aw: int, bw: int, exp_bits: int, frac_bits: int) -> int:
|
| 913 |
-
"""
|
| 914 |
-
(NaN, infinities, opposite-sign infinities -> NaN, signed zeros, exact
|
| 915 |
-
cancellation -> +0), subnormal operands and gradual-underflow subnormal
|
| 916 |
-
results, round-to-nearest-even mantissa. A zero operand passes the other
|
| 917 |
-
through verbatim. Pure integer arithmetic, so exact."""
|
| 918 |
E, F = exp_bits, frac_bits
|
| 919 |
emax = (1 << E) - 1
|
| 920 |
fmask = (1 << F) - 1
|
|
@@ -998,10 +952,7 @@ def float_add_oracle(aw: int, bw: int, exp_bits: int, frac_bits: int) -> int:
|
|
| 998 |
# =============================================================================
|
| 999 |
|
| 1000 |
class BatchedFitnessEvaluator:
|
| 1001 |
-
"""
|
| 1002 |
-
GPU-batched fitness evaluator with per-circuit reporting.
|
| 1003 |
-
Tests all circuits comprehensively.
|
| 1004 |
-
"""
|
| 1005 |
|
| 1006 |
def __init__(self, device: str = 'cuda', model_path: str = MODEL_PATH, tensors: Dict[str, torch.Tensor] = None):
|
| 1007 |
self.device = device
|
|
@@ -1263,14 +1214,7 @@ class BatchedFitnessEvaluator:
|
|
| 1263 |
# =========================================================================
|
| 1264 |
|
| 1265 |
def _eval_xor(self, pop: Dict, prefix: str, a: torch.Tensor, b: torch.Tensor) -> torch.Tensor:
|
| 1266 |
-
"""Evaluate XOR gate with or/nand decomposition.
|
| 1267 |
-
|
| 1268 |
-
Args:
|
| 1269 |
-
a, b: Tensors of shape [num_tests] or [num_tests, pop_size]
|
| 1270 |
-
|
| 1271 |
-
Returns:
|
| 1272 |
-
Tensor of shape [num_tests, pop_size]
|
| 1273 |
-
"""
|
| 1274 |
pop_size = next(iter(pop.values())).shape[0]
|
| 1275 |
|
| 1276 |
# Ensure inputs are [num_tests, pop_size]
|
|
@@ -1300,14 +1244,7 @@ class BatchedFitnessEvaluator:
|
|
| 1300 |
|
| 1301 |
def _eval_single_fa(self, pop: Dict, prefix: str,
|
| 1302 |
a: torch.Tensor, b: torch.Tensor, cin: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
|
| 1303 |
-
"""Evaluate single full adder.
|
| 1304 |
-
|
| 1305 |
-
Args:
|
| 1306 |
-
a, b, cin: Tensors of shape [num_tests] or [num_tests, pop_size]
|
| 1307 |
-
|
| 1308 |
-
Returns:
|
| 1309 |
-
sum_out, cout: Both of shape [num_tests, pop_size]
|
| 1310 |
-
"""
|
| 1311 |
pop_size = next(iter(pop.values())).shape[0]
|
| 1312 |
|
| 1313 |
# Ensure inputs are [num_tests, pop_size]
|
|
@@ -1768,11 +1705,7 @@ class BatchedFitnessEvaluator:
|
|
| 1768 |
a_bits_2d: torch.Tensor,
|
| 1769 |
b_bits_2d: torch.Tensor,
|
| 1770 |
) -> Dict[str, torch.Tensor]:
|
| 1771 |
-
"""Walk the ternary bit-cascade comparator generated by
|
| 1772 |
-
build.add_bit_cascade_compare. Returns a dict with gt/lt/ge/le/eq each of
|
| 1773 |
-
shape [num_tests, pop_size]. a_bits_2d, b_bits_2d are [num_tests, bits]
|
| 1774 |
-
MSB-first.
|
| 1775 |
-
"""
|
| 1776 |
pop_size = next(iter(pop.values())).shape[0]
|
| 1777 |
|
| 1778 |
# Per-bit gt, lt, eq
|
|
@@ -2469,12 +2402,7 @@ class BatchedFitnessEvaluator:
|
|
| 2469 |
# =========================================================================
|
| 2470 |
|
| 2471 |
def _test_modular(self, pop: Dict, mod: int, debug: bool) -> Tuple[torch.Tensor, int]:
|
| 2472 |
-
"""Test modular divisibility circuit.
|
| 2473 |
-
|
| 2474 |
-
Two structures: mod 3/5/6/7/9/10/11/12 use bit-cascade equality
|
| 2475 |
-
per multiple of N (`{prefix}.eq.k{k}.*` + final OR at `{prefix}`).
|
| 2476 |
-
mod 2/4/8 use a single-layer ternary detector at `{prefix}` directly.
|
| 2477 |
-
"""
|
| 2478 |
pop_size = next(iter(pop.values())).shape[0]
|
| 2479 |
prefix = f'modular.mod{mod}'
|
| 2480 |
|
|
@@ -3041,15 +2969,7 @@ class BatchedFitnessEvaluator:
|
|
| 3041 |
return scores, total
|
| 3042 |
|
| 3043 |
def _test_priority_encoder_nbits(self, pop: Dict, bits: int, debug: bool) -> Tuple[torch.Tensor, int]:
|
| 3044 |
-
"""Test N-bit priority encoder (find highest set bit).
|
| 3045 |
-
|
| 3046 |
-
The priority encoder is a multi-layer circuit:
|
| 3047 |
-
1. any_higher{pos}: OR of bits 0 to pos-1 (all higher-priority positions)
|
| 3048 |
-
2. is_highest{0}: bit[0] directly (MSB is always highest if set)
|
| 3049 |
-
3. is_highest{pos}: bit[pos] AND NOT(any_higher{pos}) for pos > 0
|
| 3050 |
-
4. out{bit}: OR of is_highest{pos} for all pos where (pos >> bit) & 1
|
| 3051 |
-
5. valid: OR of all input bits
|
| 3052 |
-
"""
|
| 3053 |
import math
|
| 3054 |
pop_size = next(iter(pop.values())).shape[0]
|
| 3055 |
scores = torch.zeros(pop_size, device=self.device)
|
|
@@ -4013,8 +3933,7 @@ class BatchedFitnessEvaluator:
|
|
| 4013 |
|
| 4014 |
def _test_float_unpack_pack(self, pop: Dict, family: str, word_bits: int,
|
| 4015 |
debug: bool) -> Tuple[torch.Tensor, int]:
|
| 4016 |
-
"""
|
| 4017 |
-
must reproduce its binary input (0 -> 0, 1 -> 1)."""
|
| 4018 |
pop_size = next(iter(pop.values())).shape[0]
|
| 4019 |
scores = torch.zeros(pop_size, device=self.device)
|
| 4020 |
total = 0
|
|
@@ -4047,9 +3966,7 @@ class BatchedFitnessEvaluator:
|
|
| 4047 |
|
| 4048 |
def _test_float_classify(self, pop: Dict, family: str, exp_bits: int,
|
| 4049 |
frac_bits: int, debug: bool) -> Tuple[torch.Tensor, int]:
|
| 4050 |
-
"""
|
| 4051 |
-
field predicates and the is_zero / is_subnormal / is_inf / is_nan AND
|
| 4052 |
-
gates, against IEEE 754 categories over edge-case encodings."""
|
| 4053 |
pop_size = next(iter(pop.values())).shape[0]
|
| 4054 |
scores = torch.zeros(pop_size, device=self.device)
|
| 4055 |
total = 0
|
|
@@ -4136,10 +4053,7 @@ class BatchedFitnessEvaluator:
|
|
| 4136 |
|
| 4137 |
def _test_float_cmp_composed(self, pop: Dict, family: str, exp_bits: int,
|
| 4138 |
frac_bits: int, debug: bool) -> Tuple[torch.Tensor, int]:
|
| 4139 |
-
"""
|
| 4140 |
-
wiring: the netlist is reconstructed from the .inputs metadata and
|
| 4141 |
-
evaluated end to end, then checked against exact IEEE semantics
|
| 4142 |
-
(NaN unordered, +0 == -0, subnormals ordered, mixed signs)."""
|
| 4143 |
pop_size = next(iter(pop.values())).shape[0]
|
| 4144 |
scores = torch.zeros(pop_size, device=self.device)
|
| 4145 |
total = 0
|
|
@@ -4211,10 +4125,7 @@ class BatchedFitnessEvaluator:
|
|
| 4211 |
def _test_float_arith_composed(self, pop: Dict, family: str, op: str,
|
| 4212 |
oracle, exp_bits: int, frac_bits: int,
|
| 4213 |
debug: bool) -> Tuple[torch.Tensor, int]:
|
| 4214 |
-
"""
|
| 4215 |
-
netlist is rebuilt from the .inputs metadata, evaluated end to end
|
| 4216 |
-
over IEEE edge-case and random operand pairs, and the assembled
|
| 4217 |
-
output word is compared to the exact integer oracle."""
|
| 4218 |
pop_size = next(iter(pop.values())).shape[0]
|
| 4219 |
scores = torch.zeros(pop_size, device=self.device)
|
| 4220 |
total = 0
|
|
@@ -4287,9 +4198,7 @@ class BatchedFitnessEvaluator:
|
|
| 4287 |
|
| 4288 |
def _test_float_fma_composed(self, pop: Dict, family: str, exp_bits: int,
|
| 4289 |
frac_bits: int, debug: bool) -> Tuple[torch.Tensor, int]:
|
| 4290 |
-
"""
|
| 4291 |
-
evaluate round(a*b+c) end to end over edge and random triples, compare
|
| 4292 |
-
to the single-rounding oracle."""
|
| 4293 |
pop_size = next(iter(pop.values())).shape[0]
|
| 4294 |
scores = torch.zeros(pop_size, device=self.device)
|
| 4295 |
prefix = f"{family}.fma"
|
|
@@ -4756,12 +4665,7 @@ class BatchedFitnessEvaluator:
|
|
| 4756 |
|
| 4757 |
def _pop_modN(self, pop: Dict, pop_size: int, val_bits: torch.Tensor,
|
| 4758 |
modulus: int) -> torch.Tensor:
|
| 4759 |
-
"""Drive the bit-cascade modular.mod{N} divisibility detector.
|
| 4760 |
-
|
| 4761 |
-
Returns a (pop_size,) tensor: 1 iff the 8-bit value (MSB-first bits in
|
| 4762 |
-
val_bits) is divisible by ``modulus``. Walks the per-multiple match
|
| 4763 |
-
gates (modular.modN.eq.k{val}.bit{i}.match -> .all -> top-level OR).
|
| 4764 |
-
"""
|
| 4765 |
ks = [k for k in range(256) if k % modulus == 0]
|
| 4766 |
alls = []
|
| 4767 |
for k in ks:
|
|
@@ -4782,11 +4686,7 @@ class BatchedFitnessEvaluator:
|
|
| 4782 |
def _pop_cmp8bit(self, pop: Dict, pop_size: int,
|
| 4783 |
a_bits: torch.Tensor, b_bits: torch.Tensor,
|
| 4784 |
kind: str) -> torch.Tensor:
|
| 4785 |
-
"""Drive the bit-cascade comparator (cmp8bit) over a population.
|
| 4786 |
-
|
| 4787 |
-
Returns a (pop_size,) tensor of heaviside outputs for the requested
|
| 4788 |
-
comparison kind ('gt' | 'lt' | 'eq'). Bit 0 is MSB.
|
| 4789 |
-
"""
|
| 4790 |
def apply(name: str, inp: torch.Tensor, fan_in: int) -> torch.Tensor:
|
| 4791 |
w = pop[f'{name}.weight'].view(pop_size, fan_in)
|
| 4792 |
b = pop[f'{name}.bias'].view(pop_size)
|
|
@@ -5049,16 +4949,7 @@ class BatchedFitnessEvaluator:
|
|
| 5049 |
# =========================================================================
|
| 5050 |
|
| 5051 |
def evaluate(self, population: Dict[str, torch.Tensor], debug: bool = False) -> torch.Tensor:
|
| 5052 |
-
"""
|
| 5053 |
-
Evaluate population fitness with per-circuit reporting.
|
| 5054 |
-
|
| 5055 |
-
Args:
|
| 5056 |
-
population: Dict of tensors, each with shape [pop_size, ...]
|
| 5057 |
-
debug: If True, print per-circuit results
|
| 5058 |
-
|
| 5059 |
-
Returns:
|
| 5060 |
-
Tensor of fitness scores [pop_size], normalized to [0, 1]
|
| 5061 |
-
"""
|
| 5062 |
self.results = []
|
| 5063 |
self.category_scores = {}
|
| 5064 |
|
|
@@ -5475,5 +5366,1542 @@ def main():
|
|
| 5475 |
return 1
|
| 5476 |
|
| 5477 |
|
| 5478 |
-
|
| 5479 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Evaluates the threshold computers: the fitness suite, every variant, and the CPU programs."""
|
| 2 |
+
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
|
| 4 |
import argparse
|
| 5 |
import json
|
| 6 |
import os
|
| 7 |
+
import sys
|
| 8 |
import time
|
| 9 |
from collections import defaultdict
|
| 10 |
from dataclasses import dataclass, field
|
| 11 |
+
from pathlib import Path
|
| 12 |
+
from typing import Callable, Dict, List, Optional, Tuple, Any
|
| 13 |
|
| 14 |
import torch
|
| 15 |
from safetensors import safe_open
|
|
|
|
| 56 |
|
| 57 |
|
| 58 |
def get_manifest(tensors: Dict[str, torch.Tensor]) -> Dict[str, int]:
|
| 59 |
+
"""Extract manifest values from tensors."""
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
return {
|
| 61 |
'data_bits': int(tensors.get('manifest.data_bits', torch.tensor([8.0])).item()),
|
| 62 |
'addr_bits': int(tensors.get('manifest.addr_bits',
|
|
|
|
| 83 |
# =============================================================================
|
| 84 |
|
| 85 |
FLAG_NAMES = ["Z", "N", "C", "V"]
|
| 86 |
+
|
| 87 |
CTRL_NAMES = ["HALT", "MEM_WE", "MEM_RE", "RESERVED"]
|
| 88 |
|
| 89 |
PC_BITS = 16
|
| 90 |
+
|
| 91 |
IR_BITS = 16
|
| 92 |
+
|
| 93 |
REG_BITS = 8
|
| 94 |
+
|
| 95 |
REG_COUNT = 4
|
| 96 |
+
|
| 97 |
FLAG_BITS = 4
|
| 98 |
+
|
| 99 |
SP_BITS = 16
|
| 100 |
+
|
| 101 |
CTRL_BITS = 4
|
| 102 |
+
|
| 103 |
MEM_BYTES = 65536
|
| 104 |
+
|
| 105 |
MEM_BITS = MEM_BYTES * 8
|
| 106 |
|
| 107 |
STATE_BITS = PC_BITS + IR_BITS + (REG_BITS * REG_COUNT) + FLAG_BITS + SP_BITS + CTRL_BITS + MEM_BITS
|
|
|
|
| 395 |
|
| 396 |
|
| 397 |
def run_smoke_test() -> int:
|
| 398 |
+
"""Programs through the gate-routed CPU runtime against the Python reference."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 399 |
import os
|
| 400 |
+
pass # lazy: avoids eval<->eval_all cycle
|
| 401 |
|
| 402 |
expected = {0x0102: 12, 0x0105: 6, 0x0109: 6}
|
| 403 |
|
|
|
|
| 442 |
# =============================================================================
|
| 443 |
|
| 444 |
class NetlistEvaluator:
|
| 445 |
+
"""Evaluate a self-contained circuit from its shipped wiring metadata."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 446 |
|
| 447 |
def __init__(self, tensors: Dict[str, torch.Tensor],
|
| 448 |
signal_registry: Dict[str, str], prefix: str,
|
|
|
|
| 548 |
return sorted(out)
|
| 549 |
|
| 550 |
def run(self, external: Dict[str, torch.Tensor]) -> Dict[str, torch.Tensor]:
|
| 551 |
+
"""Evaluate the circuit."""
|
|
|
|
| 552 |
first = next(iter(external.values()))
|
| 553 |
if not torch.is_tensor(first):
|
| 554 |
num_tests = 1
|
|
|
|
| 604 |
|
| 605 |
|
| 606 |
def float_bits_to_value(word: int, exp_bits: int, frac_bits: int) -> float:
|
| 607 |
+
"""Decode an IEEE 754 word to a Python float, exactly."""
|
|
|
|
| 608 |
sign = (word >> (exp_bits + frac_bits)) & 1
|
| 609 |
exp = (word >> frac_bits) & ((1 << exp_bits) - 1)
|
| 610 |
frac = word & ((1 << frac_bits) - 1)
|
|
|
|
| 621 |
|
| 622 |
|
| 623 |
def float_test_words(exp_bits: int, frac_bits: int) -> Tuple[List[int], List[int]]:
|
| 624 |
+
"""Directed IEEE edge encodings (every category, both signs) plus seeded random words for a family."""
|
|
|
|
| 625 |
import random
|
| 626 |
E, F = exp_bits, frac_bits
|
| 627 |
emax = (1 << E) - 1
|
|
|
|
| 651 |
|
| 652 |
|
| 653 |
def float_mul_oracle(aw: int, bw: int, exp_bits: int, frac_bits: int) -> int:
|
| 654 |
+
"""The expected IEEE product word: exact specials, subnormals, round to nearest even."""
|
|
|
|
|
|
|
|
|
|
| 655 |
E, F = exp_bits, frac_bits
|
| 656 |
emax = (1 << E) - 1
|
| 657 |
fmask = (1 << F) - 1
|
|
|
|
| 716 |
|
| 717 |
|
| 718 |
def float_div_oracle(aw: int, bw: int, exp_bits: int, frac_bits: int) -> int:
|
| 719 |
+
"""The expected IEEE quotient word: exact specials, subnormals, round to nearest even."""
|
|
|
|
|
|
|
|
|
|
| 720 |
E, F = exp_bits, frac_bits
|
| 721 |
emax = (1 << E) - 1
|
| 722 |
fmask = (1 << F) - 1
|
|
|
|
| 778 |
|
| 779 |
|
| 780 |
def float_fma_oracle(aw: int, bw: int, cw: int, exp_bits: int, frac_bits: int) -> int:
|
| 781 |
+
"""The expected word of round(a*b + c), rounded once."""
|
|
|
|
|
|
|
|
|
|
| 782 |
E, F = exp_bits, frac_bits
|
| 783 |
emax = (1 << E) - 1
|
| 784 |
fmask = (1 << F) - 1
|
|
|
|
| 868 |
|
| 869 |
|
| 870 |
def float_add_oracle(aw: int, bw: int, exp_bits: int, frac_bits: int) -> int:
|
| 871 |
+
"""The expected IEEE sum word: exact specials, subnormals, round to nearest even."""
|
|
|
|
|
|
|
|
|
|
|
|
|
| 872 |
E, F = exp_bits, frac_bits
|
| 873 |
emax = (1 << E) - 1
|
| 874 |
fmask = (1 << F) - 1
|
|
|
|
| 952 |
# =============================================================================
|
| 953 |
|
| 954 |
class BatchedFitnessEvaluator:
|
| 955 |
+
"""GPU-batched fitness evaluator with per-circuit reporting."""
|
|
|
|
|
|
|
|
|
|
| 956 |
|
| 957 |
def __init__(self, device: str = 'cuda', model_path: str = MODEL_PATH, tensors: Dict[str, torch.Tensor] = None):
|
| 958 |
self.device = device
|
|
|
|
| 1214 |
# =========================================================================
|
| 1215 |
|
| 1216 |
def _eval_xor(self, pop: Dict, prefix: str, a: torch.Tensor, b: torch.Tensor) -> torch.Tensor:
|
| 1217 |
+
"""Evaluate XOR gate with or/nand decomposition."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1218 |
pop_size = next(iter(pop.values())).shape[0]
|
| 1219 |
|
| 1220 |
# Ensure inputs are [num_tests, pop_size]
|
|
|
|
| 1244 |
|
| 1245 |
def _eval_single_fa(self, pop: Dict, prefix: str,
|
| 1246 |
a: torch.Tensor, b: torch.Tensor, cin: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
|
| 1247 |
+
"""Evaluate single full adder."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1248 |
pop_size = next(iter(pop.values())).shape[0]
|
| 1249 |
|
| 1250 |
# Ensure inputs are [num_tests, pop_size]
|
|
|
|
| 1705 |
a_bits_2d: torch.Tensor,
|
| 1706 |
b_bits_2d: torch.Tensor,
|
| 1707 |
) -> Dict[str, torch.Tensor]:
|
| 1708 |
+
"""Walk the ternary bit-cascade comparator generated by build.add_bit_cascade_compare."""
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1709 |
pop_size = next(iter(pop.values())).shape[0]
|
| 1710 |
|
| 1711 |
# Per-bit gt, lt, eq
|
|
|
|
| 2402 |
# =========================================================================
|
| 2403 |
|
| 2404 |
def _test_modular(self, pop: Dict, mod: int, debug: bool) -> Tuple[torch.Tensor, int]:
|
| 2405 |
+
"""Test modular divisibility circuit."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2406 |
pop_size = next(iter(pop.values())).shape[0]
|
| 2407 |
prefix = f'modular.mod{mod}'
|
| 2408 |
|
|
|
|
| 2969 |
return scores, total
|
| 2970 |
|
| 2971 |
def _test_priority_encoder_nbits(self, pop: Dict, bits: int, debug: bool) -> Tuple[torch.Tensor, int]:
|
| 2972 |
+
"""Test N-bit priority encoder (find highest set bit)."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2973 |
import math
|
| 2974 |
pop_size = next(iter(pop.values())).shape[0]
|
| 2975 |
scores = torch.zeros(pop_size, device=self.device)
|
|
|
|
| 3933 |
|
| 3934 |
def _test_float_unpack_pack(self, pop: Dict, family: str, word_bits: int,
|
| 3935 |
debug: bool) -> Tuple[torch.Tensor, int]:
|
| 3936 |
+
"""Each unpack/pack identity buffer reproduces its input bit."""
|
|
|
|
| 3937 |
pop_size = next(iter(pop.values())).shape[0]
|
| 3938 |
scores = torch.zeros(pop_size, device=self.device)
|
| 3939 |
total = 0
|
|
|
|
| 3966 |
|
| 3967 |
def _test_float_classify(self, pop: Dict, family: str, exp_bits: int,
|
| 3968 |
frac_bits: int, debug: bool) -> Tuple[torch.Tensor, int]:
|
| 3969 |
+
"""The classify subcircuit against the IEEE 754 categories on edge encodings."""
|
|
|
|
|
|
|
| 3970 |
pop_size = next(iter(pop.values())).shape[0]
|
| 3971 |
scores = torch.zeros(pop_size, device=self.device)
|
| 3972 |
total = 0
|
|
|
|
| 4053 |
|
| 4054 |
def _test_float_cmp_composed(self, pop: Dict, family: str, exp_bits: int,
|
| 4055 |
frac_bits: int, debug: bool) -> Tuple[torch.Tensor, int]:
|
| 4056 |
+
"""The IEEE comparison rebuilt from the shipped wiring against exact IEEE semantics."""
|
|
|
|
|
|
|
|
|
|
| 4057 |
pop_size = next(iter(pop.values())).shape[0]
|
| 4058 |
scores = torch.zeros(pop_size, device=self.device)
|
| 4059 |
total = 0
|
|
|
|
| 4125 |
def _test_float_arith_composed(self, pop: Dict, family: str, op: str,
|
| 4126 |
oracle, exp_bits: int, frac_bits: int,
|
| 4127 |
debug: bool) -> Tuple[torch.Tensor, int]:
|
| 4128 |
+
"""A float pipeline rebuilt from the shipped wiring against the exact integer oracle."""
|
|
|
|
|
|
|
|
|
|
| 4129 |
pop_size = next(iter(pop.values())).shape[0]
|
| 4130 |
scores = torch.zeros(pop_size, device=self.device)
|
| 4131 |
total = 0
|
|
|
|
| 4198 |
|
| 4199 |
def _test_float_fma_composed(self, pop: Dict, family: str, exp_bits: int,
|
| 4200 |
frac_bits: int, debug: bool) -> Tuple[torch.Tensor, int]:
|
| 4201 |
+
"""The fused multiply-add rebuilt from the shipped wiring against the single-rounding oracle."""
|
|
|
|
|
|
|
| 4202 |
pop_size = next(iter(pop.values())).shape[0]
|
| 4203 |
scores = torch.zeros(pop_size, device=self.device)
|
| 4204 |
prefix = f"{family}.fma"
|
|
|
|
| 4665 |
|
| 4666 |
def _pop_modN(self, pop: Dict, pop_size: int, val_bits: torch.Tensor,
|
| 4667 |
modulus: int) -> torch.Tensor:
|
| 4668 |
+
"""Drive the bit-cascade modular.mod{N} divisibility detector."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4669 |
ks = [k for k in range(256) if k % modulus == 0]
|
| 4670 |
alls = []
|
| 4671 |
for k in ks:
|
|
|
|
| 4686 |
def _pop_cmp8bit(self, pop: Dict, pop_size: int,
|
| 4687 |
a_bits: torch.Tensor, b_bits: torch.Tensor,
|
| 4688 |
kind: str) -> torch.Tensor:
|
| 4689 |
+
"""Drive the bit-cascade comparator (cmp8bit) over a population."""
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4690 |
def apply(name: str, inp: torch.Tensor, fan_in: int) -> torch.Tensor:
|
| 4691 |
w = pop[f'{name}.weight'].view(pop_size, fan_in)
|
| 4692 |
b = pop[f'{name}.bias'].view(pop_size)
|
|
|
|
| 4949 |
# =========================================================================
|
| 4950 |
|
| 4951 |
def evaluate(self, population: Dict[str, torch.Tensor], debug: bool = False) -> torch.Tensor:
|
| 4952 |
+
"""Evaluate population fitness with per-circuit reporting."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4953 |
self.results = []
|
| 4954 |
self.category_scores = {}
|
| 4955 |
|
|
|
|
| 5366 |
return 1
|
| 5367 |
|
| 5368 |
|
| 5369 |
+
# ---------------------------------------------------------------------------
|
| 5370 |
+
# Variant-aware threshold ALU + CPU
|
| 5371 |
+
# ---------------------------------------------------------------------------
|
| 5372 |
+
|
| 5373 |
+
class GenericThresholdALU:
|
| 5374 |
+
"""Variant-aware threshold ALU walking the gates of a loaded variant."""
|
| 5375 |
+
|
| 5376 |
+
def __init__(self, tensors: Dict[str, torch.Tensor], data_bits: int):
|
| 5377 |
+
self.T = tensors
|
| 5378 |
+
self.data_bits = data_bits
|
| 5379 |
+
|
| 5380 |
+
def _g(self, name, inputs):
|
| 5381 |
+
w = self.T[name + ".weight"].view(-1)
|
| 5382 |
+
b = self.T[name + ".bias"].view(-1)
|
| 5383 |
+
return int(heaviside((torch.tensor(inputs, dtype=torch.float32) * w).sum() + b).item())
|
| 5384 |
+
|
| 5385 |
+
def _xor_or_nand(self, prefix, inputs):
|
| 5386 |
+
a, b_ = inputs
|
| 5387 |
+
h_or = self._g(f"{prefix}.layer1.or", [a, b_])
|
| 5388 |
+
h_nand = self._g(f"{prefix}.layer1.nand", [a, b_])
|
| 5389 |
+
return self._g(f"{prefix}.layer2", [h_or, h_nand])
|
| 5390 |
+
|
| 5391 |
+
def _fa(self, prefix, a, b, cin):
|
| 5392 |
+
s1 = self._xor_or_nand(f"{prefix}.ha1.sum", [a, b])
|
| 5393 |
+
c1 = self._g(f"{prefix}.ha1.carry", [a, b])
|
| 5394 |
+
s2 = self._xor_or_nand(f"{prefix}.ha2.sum", [s1, cin])
|
| 5395 |
+
c2 = self._g(f"{prefix}.ha2.carry", [s1, cin])
|
| 5396 |
+
cout = self._g(f"{prefix}.carry_or", [c1, c2])
|
| 5397 |
+
return s2, cout
|
| 5398 |
+
|
| 5399 |
+
def add8(self, a, b):
|
| 5400 |
+
a_lsb = list(reversed(int_to_bits(a, 8)))
|
| 5401 |
+
b_lsb = list(reversed(int_to_bits(b, 8)))
|
| 5402 |
+
carry = 0
|
| 5403 |
+
s_lsb = []
|
| 5404 |
+
for i in range(8):
|
| 5405 |
+
s, carry = self._fa(f"arithmetic.ripplecarry8bit.fa{i}", a_lsb[i], b_lsb[i], carry)
|
| 5406 |
+
s_lsb.append(s)
|
| 5407 |
+
return bits_to_int(list(reversed(s_lsb))), carry
|
| 5408 |
+
|
| 5409 |
+
def sub8(self, a, b):
|
| 5410 |
+
a_lsb = list(reversed(int_to_bits(a, 8)))
|
| 5411 |
+
b_lsb = list(reversed(int_to_bits(b, 8)))
|
| 5412 |
+
carry = 1
|
| 5413 |
+
d_lsb = []
|
| 5414 |
+
for i in range(8):
|
| 5415 |
+
notb = self._g(f"arithmetic.sub8bit.notb{i}", [b_lsb[i]])
|
| 5416 |
+
x1 = self._xor_or_nand(f"arithmetic.sub8bit.fa{i}.xor1", [a_lsb[i], notb])
|
| 5417 |
+
x2 = self._xor_or_nand(f"arithmetic.sub8bit.fa{i}.xor2", [x1, carry])
|
| 5418 |
+
and1 = self._g(f"arithmetic.sub8bit.fa{i}.and1", [a_lsb[i], notb])
|
| 5419 |
+
and2 = self._g(f"arithmetic.sub8bit.fa{i}.and2", [x1, carry])
|
| 5420 |
+
carry = self._g(f"arithmetic.sub8bit.fa{i}.or_carry", [and1, and2])
|
| 5421 |
+
d_lsb.append(x2)
|
| 5422 |
+
return bits_to_int(list(reversed(d_lsb))), carry
|
| 5423 |
+
|
| 5424 |
+
_CMP_KIND = {"greaterthan": "gt", "lessthan": "lt", "eq": "eq", "equality": "eq",
|
| 5425 |
+
"greaterorequal": "ge", "lessorequal": "le"}
|
| 5426 |
+
|
| 5427 |
+
def _cmp_bit_cascade(self, cmp_prefix: str, finals: Dict[str, str],
|
| 5428 |
+
a_bits_msb: List[int], b_bits_msb: List[int], kind: str) -> int:
|
| 5429 |
+
"""Walk an add_bit_cascade_compare structure (bit 0 is the MSB)."""
|
| 5430 |
+
bits = len(a_bits_msb)
|
| 5431 |
+
bit_gt, bit_lt, bit_eq = [], [], []
|
| 5432 |
+
for i in range(bits):
|
| 5433 |
+
ab = [a_bits_msb[i], b_bits_msb[i]]
|
| 5434 |
+
bit_gt.append(self._g(f"{cmp_prefix}.bit{i}.gt", ab))
|
| 5435 |
+
bit_lt.append(self._g(f"{cmp_prefix}.bit{i}.lt", ab))
|
| 5436 |
+
eq_and = self._g(f"{cmp_prefix}.bit{i}.eq.layer1.and", ab)
|
| 5437 |
+
eq_nor = self._g(f"{cmp_prefix}.bit{i}.eq.layer1.nor", ab)
|
| 5438 |
+
bit_eq.append(self._g(f"{cmp_prefix}.bit{i}.eq", [eq_and, eq_nor]))
|
| 5439 |
+
cas_gt, cas_lt = [bit_gt[0]], [bit_lt[0]]
|
| 5440 |
+
for i in range(1, bits):
|
| 5441 |
+
eq_pref = self._g(f"{cmp_prefix}.cascade.eq_prefix.bit{i}", bit_eq[:i])
|
| 5442 |
+
cas_gt.append(self._g(f"{cmp_prefix}.cascade.gt.bit{i}", [eq_pref, bit_gt[i]]))
|
| 5443 |
+
cas_lt.append(self._g(f"{cmp_prefix}.cascade.lt.bit{i}", [eq_pref, bit_lt[i]]))
|
| 5444 |
+
if kind == "gt":
|
| 5445 |
+
return self._g(finals["gt"], cas_gt)
|
| 5446 |
+
if kind == "lt":
|
| 5447 |
+
return self._g(finals["lt"], cas_lt)
|
| 5448 |
+
if kind == "eq":
|
| 5449 |
+
return self._g(finals["eq"], bit_eq)
|
| 5450 |
+
if kind == "ge":
|
| 5451 |
+
lt = self._g(finals["lt"], cas_lt)
|
| 5452 |
+
not_lt = self._g(finals["ge"] + ".not_lt", [lt])
|
| 5453 |
+
return self._g(finals["ge"], [not_lt])
|
| 5454 |
+
if kind == "le":
|
| 5455 |
+
gt = self._g(finals["gt"], cas_gt)
|
| 5456 |
+
not_gt = self._g(finals["le"] + ".not_gt", [gt])
|
| 5457 |
+
return self._g(finals["le"], [not_gt])
|
| 5458 |
+
raise ValueError(kind)
|
| 5459 |
+
|
| 5460 |
+
def cmp8(self, a, b, kind):
|
| 5461 |
+
return self.cmp_n(a, b, kind, 8)
|
| 5462 |
+
|
| 5463 |
+
def mul8(self, a, b):
|
| 5464 |
+
ab = int_to_bits(a, 8)
|
| 5465 |
+
bb = int_to_bits(b, 8)
|
| 5466 |
+
result = 0
|
| 5467 |
+
for j in range(8):
|
| 5468 |
+
if bb[j] == 0:
|
| 5469 |
+
continue
|
| 5470 |
+
row = 0
|
| 5471 |
+
for i in range(8):
|
| 5472 |
+
pp = self._g(f"alu.alu8bit.mul.pp.a{i}b{j}", [ab[i], bb[j]])
|
| 5473 |
+
row |= (pp << (7 - i))
|
| 5474 |
+
shift = 7 - j
|
| 5475 |
+
result, _ = self.add8(result & 0xFF, (row << shift) & 0xFF)
|
| 5476 |
+
return result & 0xFF
|
| 5477 |
+
|
| 5478 |
+
def _packed_pair_op(self, name: str, a: int, b: int) -> int:
|
| 5479 |
+
"""8 parallel 2-input gates stored packed: weight [16], bias [8]."""
|
| 5480 |
+
a_bits = int_to_bits(a, 8)
|
| 5481 |
+
b_bits = int_to_bits(b, 8)
|
| 5482 |
+
w = self.T[f"{name}.weight"].view(-1)
|
| 5483 |
+
bias = self.T[f"{name}.bias"].view(-1)
|
| 5484 |
+
out = []
|
| 5485 |
+
for bit in range(8):
|
| 5486 |
+
inp = torch.tensor([float(a_bits[bit]), float(b_bits[bit])])
|
| 5487 |
+
out.append(int(heaviside((inp * w[bit * 2:bit * 2 + 2]).sum() + bias[bit]).item()))
|
| 5488 |
+
return bits_to_int(out)
|
| 5489 |
+
|
| 5490 |
+
def and8(self, a, b):
|
| 5491 |
+
return self._packed_pair_op("alu.alu8bit.and", a, b)
|
| 5492 |
+
|
| 5493 |
+
def or8(self, a, b):
|
| 5494 |
+
return self._packed_pair_op("alu.alu8bit.or", a, b)
|
| 5495 |
+
|
| 5496 |
+
def xor8(self, a, b):
|
| 5497 |
+
a_bits = int_to_bits(a, 8)
|
| 5498 |
+
b_bits = int_to_bits(b, 8)
|
| 5499 |
+
w_or = self.T["alu.alu8bit.xor.layer1.or.weight"].view(-1)
|
| 5500 |
+
b_or = self.T["alu.alu8bit.xor.layer1.or.bias"].view(-1)
|
| 5501 |
+
w_nand = self.T["alu.alu8bit.xor.layer1.nand.weight"].view(-1)
|
| 5502 |
+
b_nand = self.T["alu.alu8bit.xor.layer1.nand.bias"].view(-1)
|
| 5503 |
+
w2 = self.T["alu.alu8bit.xor.layer2.weight"].view(-1)
|
| 5504 |
+
b2 = self.T["alu.alu8bit.xor.layer2.bias"].view(-1)
|
| 5505 |
+
out = []
|
| 5506 |
+
for bit in range(8):
|
| 5507 |
+
inp = torch.tensor([float(a_bits[bit]), float(b_bits[bit])])
|
| 5508 |
+
h_or = heaviside((inp * w_or[bit * 2:bit * 2 + 2]).sum() + b_or[bit])
|
| 5509 |
+
h_nand = heaviside((inp * w_nand[bit * 2:bit * 2 + 2]).sum() + b_nand[bit])
|
| 5510 |
+
hidden = torch.stack([h_or, h_nand])
|
| 5511 |
+
out.append(int(heaviside((hidden * w2[bit * 2:bit * 2 + 2]).sum() + b2[bit]).item()))
|
| 5512 |
+
return bits_to_int(out)
|
| 5513 |
+
|
| 5514 |
+
def shl8(self, a):
|
| 5515 |
+
a_bits = int_to_bits(a, 8)
|
| 5516 |
+
out = []
|
| 5517 |
+
for bit in range(8):
|
| 5518 |
+
src = float(a_bits[bit + 1]) if bit < 7 else 0.0
|
| 5519 |
+
out.append(self._g(f"alu.alu8bit.shl.bit{bit}", [src]))
|
| 5520 |
+
return bits_to_int(out)
|
| 5521 |
+
|
| 5522 |
+
def shr8(self, a):
|
| 5523 |
+
a_bits = int_to_bits(a, 8)
|
| 5524 |
+
out = []
|
| 5525 |
+
for bit in range(8):
|
| 5526 |
+
src = float(a_bits[bit - 1]) if bit > 0 else 0.0
|
| 5527 |
+
out.append(self._g(f"alu.alu8bit.shr.bit{bit}", [src]))
|
| 5528 |
+
return bits_to_int(out)
|
| 5529 |
+
|
| 5530 |
+
def div8(self, a, b):
|
| 5531 |
+
"""8-bit restoring division through the per-stage bit-cascade GE comparators and the sub8 subtractor."""
|
| 5532 |
+
if b == 0:
|
| 5533 |
+
return 0xFF, a
|
| 5534 |
+
a_bits = int_to_bits(a, 8)
|
| 5535 |
+
div_bits = int_to_bits(b, 8)
|
| 5536 |
+
quotient = 0
|
| 5537 |
+
remainder = 0
|
| 5538 |
+
for stage in range(8):
|
| 5539 |
+
remainder = ((remainder << 1) | a_bits[stage]) & 0xFF
|
| 5540 |
+
prefix = f"alu.alu8bit.div.stage{stage}"
|
| 5541 |
+
finals = {
|
| 5542 |
+
"gt": f"{prefix}.cmp_bc.gt",
|
| 5543 |
+
"lt": f"{prefix}.cmp_bc.lt",
|
| 5544 |
+
"eq": f"{prefix}.cmp_bc.eq",
|
| 5545 |
+
"ge": f"{prefix}.cmp",
|
| 5546 |
+
"le": f"{prefix}.cmp_bc.le",
|
| 5547 |
+
}
|
| 5548 |
+
ge = self._cmp_bit_cascade(f"{prefix}.cmp_bc", finals,
|
| 5549 |
+
int_to_bits(remainder, 8), div_bits, "ge")
|
| 5550 |
+
if ge:
|
| 5551 |
+
remainder, _ = self.sub8(remainder, b)
|
| 5552 |
+
quotient = (quotient << 1) | 1
|
| 5553 |
+
else:
|
| 5554 |
+
quotient = quotient << 1
|
| 5555 |
+
return quotient & 0xFF, remainder & 0xFF
|
| 5556 |
+
|
| 5557 |
+
# ----- N-bit primitives (for 16-bit and 32-bit variants) ----------------
|
| 5558 |
+
|
| 5559 |
+
def add_n(self, a: int, b: int, bits: int):
|
| 5560 |
+
"""Width-generic ripple-carry add via arithmetic.ripplecarry{N}bit."""
|
| 5561 |
+
prefix = f"arithmetic.ripplecarry{bits}bit"
|
| 5562 |
+
a_lsb = list(reversed(int_to_bits(a, bits)))
|
| 5563 |
+
b_lsb = list(reversed(int_to_bits(b, bits)))
|
| 5564 |
+
carry = 0
|
| 5565 |
+
s_lsb = []
|
| 5566 |
+
for i in range(bits):
|
| 5567 |
+
s, carry = self._fa(f"{prefix}.fa{i}", a_lsb[i], b_lsb[i], carry)
|
| 5568 |
+
s_lsb.append(s)
|
| 5569 |
+
return bits_to_int(list(reversed(s_lsb))), carry
|
| 5570 |
+
|
| 5571 |
+
def sub_n(self, a: int, b: int, bits: int):
|
| 5572 |
+
"""N-bit two's-complement subtract via arithmetic.sub{N}bit (N >= 16)."""
|
| 5573 |
+
prefix = f"arithmetic.sub{bits}bit"
|
| 5574 |
+
a_lsb = list(reversed(int_to_bits(a, bits)))
|
| 5575 |
+
b_lsb = list(reversed(int_to_bits(b, bits)))
|
| 5576 |
+
# NOT each B bit
|
| 5577 |
+
notb = [self._g(f"{prefix}.not_b.bit{i}", [b_lsb[i]]) for i in range(bits)]
|
| 5578 |
+
carry = 1 # carry-in = 1 for two's-complement
|
| 5579 |
+
d_lsb = []
|
| 5580 |
+
for i in range(bits):
|
| 5581 |
+
s, carry = self._fa(f"{prefix}.fa{i}", a_lsb[i], notb[i], carry)
|
| 5582 |
+
d_lsb.append(s)
|
| 5583 |
+
return bits_to_int(list(reversed(d_lsb))), carry
|
| 5584 |
+
|
| 5585 |
+
def cmp_n(self, a: int, b: int, kind: str, bits: int):
|
| 5586 |
+
"""N-bit unsigned comparison through the bit-cascade comparator."""
|
| 5587 |
+
short = self._CMP_KIND[kind]
|
| 5588 |
+
finals = {
|
| 5589 |
+
"gt": f"arithmetic.greaterthan{bits}bit",
|
| 5590 |
+
"lt": f"arithmetic.lessthan{bits}bit",
|
| 5591 |
+
"eq": f"arithmetic.equality{bits}bit",
|
| 5592 |
+
"ge": f"arithmetic.greaterorequal{bits}bit",
|
| 5593 |
+
"le": f"arithmetic.lessorequal{bits}bit",
|
| 5594 |
+
}
|
| 5595 |
+
return self._cmp_bit_cascade(f"arithmetic.cmp{bits}bit", finals,
|
| 5596 |
+
int_to_bits(a, bits), int_to_bits(b, bits), short)
|
| 5597 |
+
|
| 5598 |
+
def mul_n(self, a: int, b: int, bits: int):
|
| 5599 |
+
"""N-bit shift-add multiply (low N bits only)."""
|
| 5600 |
+
ab = int_to_bits(a, bits)
|
| 5601 |
+
bb = int_to_bits(b, bits)
|
| 5602 |
+
mask = (1 << bits) - 1
|
| 5603 |
+
result = 0
|
| 5604 |
+
for j in range(bits):
|
| 5605 |
+
if bb[j] == 0:
|
| 5606 |
+
continue
|
| 5607 |
+
row = 0
|
| 5608 |
+
for i in range(bits):
|
| 5609 |
+
pp = self._g(f"alu.alu{bits}bit.mul.pp.a{i}b{j}", [ab[i], bb[j]])
|
| 5610 |
+
row |= (pp << (bits - 1 - i))
|
| 5611 |
+
shift = (bits - 1) - j
|
| 5612 |
+
result, _ = self.add_n(result & mask, (row << shift) & mask, bits)
|
| 5613 |
+
return result & mask
|
| 5614 |
+
|
| 5615 |
+
|
| 5616 |
+
class GenericThresholdCPU:
|
| 5617 |
+
"""Variant-aware CPU runtime. Sized from the variant's manifest."""
|
| 5618 |
+
|
| 5619 |
+
def __init__(self, tensors: Dict[str, torch.Tensor]):
|
| 5620 |
+
self.T = tensors
|
| 5621 |
+
m = get_manifest(tensors)
|
| 5622 |
+
self.data_bits = m["data_bits"]
|
| 5623 |
+
self.addr_bits = m["addr_bits"]
|
| 5624 |
+
self.mem_bytes = m["memory_bytes"]
|
| 5625 |
+
# 8-bit CPU primitives (ripplecarry8bit, sub8bit, alu.alu8bit.*, memory.*,
|
| 5626 |
+
# control.*) are present in every variant regardless of manifest data_bits.
|
| 5627 |
+
# Wider data widths simply add additional standalone ALU primitives.
|
| 5628 |
+
if self.mem_bytes == 0:
|
| 5629 |
+
raise NotImplementedError(
|
| 5630 |
+
"Pure-ALU variants have no memory; cannot run CPU programs"
|
| 5631 |
+
)
|
| 5632 |
+
self.alu = GenericThresholdALU(tensors, 8)
|
| 5633 |
+
|
| 5634 |
+
def _addr_decode(self, addr):
|
| 5635 |
+
bits = torch.tensor(int_to_bits(addr, self.addr_bits), dtype=torch.float32)
|
| 5636 |
+
w = self.T["memory.addr_decode.weight"]
|
| 5637 |
+
b = self.T["memory.addr_decode.bias"]
|
| 5638 |
+
return heaviside((w * bits).sum(dim=1) + b)
|
| 5639 |
+
|
| 5640 |
+
def mem_read(self, mem, addr):
|
| 5641 |
+
sel = self._addr_decode(addr)
|
| 5642 |
+
mem_bits = torch.tensor(
|
| 5643 |
+
[int_to_bits(byte, 8) for byte in mem], dtype=torch.float32
|
| 5644 |
+
)
|
| 5645 |
+
and_w = self.T["memory.read.and.weight"]
|
| 5646 |
+
and_b = self.T["memory.read.and.bias"]
|
| 5647 |
+
or_w = self.T["memory.read.or.weight"]
|
| 5648 |
+
or_b = self.T["memory.read.or.bias"]
|
| 5649 |
+
out = []
|
| 5650 |
+
for bit in range(8):
|
| 5651 |
+
inp = torch.stack([mem_bits[:, bit], sel], dim=1)
|
| 5652 |
+
and_out = heaviside((inp * and_w[bit]).sum(dim=1) + and_b[bit])
|
| 5653 |
+
out.append(int(heaviside((and_out * or_w[bit]).sum() + or_b[bit]).item()))
|
| 5654 |
+
return bits_to_int(out)
|
| 5655 |
+
|
| 5656 |
+
def mem_write(self, mem, addr, value):
|
| 5657 |
+
sel = self._addr_decode(addr)
|
| 5658 |
+
data_bits = torch.tensor(int_to_bits(value, 8), dtype=torch.float32)
|
| 5659 |
+
mem_bits = torch.tensor(
|
| 5660 |
+
[int_to_bits(byte, 8) for byte in mem], dtype=torch.float32
|
| 5661 |
+
)
|
| 5662 |
+
sel_w = self.T["memory.write.sel.weight"]
|
| 5663 |
+
sel_b = self.T["memory.write.sel.bias"]
|
| 5664 |
+
nsel_w = self.T["memory.write.nsel.weight"].squeeze(1)
|
| 5665 |
+
nsel_b = self.T["memory.write.nsel.bias"]
|
| 5666 |
+
and_old_w = self.T["memory.write.and_old.weight"]
|
| 5667 |
+
and_old_b = self.T["memory.write.and_old.bias"]
|
| 5668 |
+
and_new_w = self.T["memory.write.and_new.weight"]
|
| 5669 |
+
and_new_b = self.T["memory.write.and_new.bias"]
|
| 5670 |
+
or_w = self.T["memory.write.or.weight"]
|
| 5671 |
+
or_b = self.T["memory.write.or.bias"]
|
| 5672 |
+
we = torch.ones_like(sel)
|
| 5673 |
+
sel_inp = torch.stack([sel, we], dim=1)
|
| 5674 |
+
write_sel = heaviside((sel_inp * sel_w).sum(dim=1) + sel_b)
|
| 5675 |
+
nsel = heaviside(write_sel * nsel_w + nsel_b)
|
| 5676 |
+
for bit in range(8):
|
| 5677 |
+
old = mem_bits[:, bit]
|
| 5678 |
+
data_bit = data_bits[bit].expand(self.mem_bytes)
|
| 5679 |
+
inp_old = torch.stack([old, nsel], dim=1)
|
| 5680 |
+
inp_new = torch.stack([data_bit, write_sel], dim=1)
|
| 5681 |
+
and_old = heaviside((inp_old * and_old_w[:, bit]).sum(dim=1) + and_old_b[:, bit])
|
| 5682 |
+
and_new = heaviside((inp_new * and_new_w[:, bit]).sum(dim=1) + and_new_b[:, bit])
|
| 5683 |
+
or_inp = torch.stack([and_old, and_new], dim=1)
|
| 5684 |
+
new_bit = heaviside((or_inp * or_w[:, bit]).sum(dim=1) + or_b[:, bit])
|
| 5685 |
+
mem_bits[:, bit] = new_bit
|
| 5686 |
+
return [bits_to_int([int(b) for b in mem_bits[i].tolist()]) for i in range(self.mem_bytes)]
|
| 5687 |
+
|
| 5688 |
+
# Conditions 0..7 map to (mux circuit, flag index into [Z, N, C, V]).
|
| 5689 |
+
# Odd conditions are the negated forms; the mux select is the complemented
|
| 5690 |
+
# flag (the circuits are plain per-bit muxes -- see control.* .inputs,
|
| 5691 |
+
# which name the select $not_zero / $not_carry / ... for the odd forms).
|
| 5692 |
+
_JCC = [("control.jz", 0), ("control.jnz", 0), ("control.jc", 2), ("control.jnc", 2),
|
| 5693 |
+
("control.jn", 1), ("control.jp", 1), ("control.jv", 3), ("control.jnv", 3)]
|
| 5694 |
+
|
| 5695 |
+
def _jcc_pc(self, circuit: str, pc: int, target: int, sel: int) -> int:
|
| 5696 |
+
"""Per-bit 2:1 mux over the PC: sel ? target : pc, addr_bits wide."""
|
| 5697 |
+
pc_bits = int_to_bits(pc, self.addr_bits)
|
| 5698 |
+
t_bits = int_to_bits(target, self.addr_bits)
|
| 5699 |
+
out = []
|
| 5700 |
+
for bit in range(self.addr_bits):
|
| 5701 |
+
bp = f"{circuit}.bit{bit}"
|
| 5702 |
+
not_sel = self.alu._g(f"{bp}.not_sel", [float(sel)])
|
| 5703 |
+
and_a = self.alu._g(f"{bp}.and_a", [float(pc_bits[bit]), not_sel])
|
| 5704 |
+
and_b = self.alu._g(f"{bp}.and_b", [float(t_bits[bit]), float(sel)])
|
| 5705 |
+
out.append(self.alu._g(f"{bp}.or", [and_a, and_b]))
|
| 5706 |
+
return bits_to_int(out)
|
| 5707 |
+
|
| 5708 |
+
def _sp_dec(self, sp: int) -> int:
|
| 5709 |
+
"""SP - 1 through the control.push.sp_dec borrow chain."""
|
| 5710 |
+
bits = int_to_bits(sp, self.addr_bits)
|
| 5711 |
+
out = [0] * self.addr_bits
|
| 5712 |
+
borrow = 1
|
| 5713 |
+
for bit in range(self.addr_bits - 1, -1, -1):
|
| 5714 |
+
bp = f"control.push.sp_dec.bit{bit}"
|
| 5715 |
+
h_or = self.alu._g(f"{bp}.xor.layer1.or", [float(bits[bit]), float(borrow)])
|
| 5716 |
+
h_nand = self.alu._g(f"{bp}.xor.layer1.nand", [float(bits[bit]), float(borrow)])
|
| 5717 |
+
out[bit] = self.alu._g(f"{bp}.xor.layer2", [h_or, h_nand])
|
| 5718 |
+
borrow = self.alu._g(f"{bp}.borrow", [float(1 - bits[bit]), float(borrow)])
|
| 5719 |
+
return bits_to_int(out)
|
| 5720 |
+
|
| 5721 |
+
def _decode_op(self, opcode):
|
| 5722 |
+
"""4-to-16 opcode one-hot through the gate decoder."""
|
| 5723 |
+
ob = [float((opcode >> j) & 1) for j in range(4)]
|
| 5724 |
+
return [self.alu._g(f"control.decode.op{n}", ob) for n in range(16)]
|
| 5725 |
+
|
| 5726 |
+
def _inc(self, pc, name, start):
|
| 5727 |
+
"""PC + 2^start through the gate increment chain (LSB-first)."""
|
| 5728 |
+
ab = self.addr_bits
|
| 5729 |
+
pcb = [(pc >> k) & 1 for k in range(ab)]
|
| 5730 |
+
out = [0] * ab
|
| 5731 |
+
carry = 0
|
| 5732 |
+
for k in range(ab):
|
| 5733 |
+
if k < start:
|
| 5734 |
+
out[k] = self.alu._g(f"control.pcnext.{name}.bit{k}", [float(pcb[k])])
|
| 5735 |
+
else:
|
| 5736 |
+
cin = 1 if k == start else carry
|
| 5737 |
+
o = self.alu._g(f"control.pcnext.{name}.bit{k}.xor.layer1.or", [float(pcb[k]), float(cin)])
|
| 5738 |
+
nd = self.alu._g(f"control.pcnext.{name}.bit{k}.xor.layer1.nand", [float(pcb[k]), float(cin)])
|
| 5739 |
+
out[k] = self.alu._g(f"control.pcnext.{name}.bit{k}.xor.layer2", [o, nd])
|
| 5740 |
+
carry = self.alu._g(f"control.pcnext.{name}.bit{k}.carry", [float(pcb[k]), float(cin)])
|
| 5741 |
+
return sum(out[k] << k for k in range(ab))
|
| 5742 |
+
|
| 5743 |
+
def _pcnext(self, pc2, pc4, addr, jcc, use_pc4, use_addr, use_jcc):
|
| 5744 |
+
"""Next-PC priority mux through the gates: use_addr ? addr : (use_jcc ? jcc : (use_pc4 ? pc4 : pc2))."""
|
| 5745 |
+
ab = self.addr_bits
|
| 5746 |
+
p2 = [(pc2 >> k) & 1 for k in range(ab)]
|
| 5747 |
+
p4 = [(pc4 >> k) & 1 for k in range(ab)]
|
| 5748 |
+
aB = [(addr >> k) & 1 for k in range(ab)]
|
| 5749 |
+
jB = [(jcc >> k) & 1 for k in range(ab)]
|
| 5750 |
+
out = [0] * ab
|
| 5751 |
+
for k in range(ab):
|
| 5752 |
+
ns = self.alu._g(f"control.pcnext.m_pc4.bit{k}.not_sel", [float(use_pc4)])
|
| 5753 |
+
aa = self.alu._g(f"control.pcnext.m_pc4.bit{k}.and_a", [float(p2[k]), ns])
|
| 5754 |
+
bb = self.alu._g(f"control.pcnext.m_pc4.bit{k}.and_b", [float(p4[k]), float(use_pc4)])
|
| 5755 |
+
m1 = self.alu._g(f"control.pcnext.m_pc4.bit{k}.or", [aa, bb])
|
| 5756 |
+
ns = self.alu._g(f"control.pcnext.m_jcc.bit{k}.not_sel", [float(use_jcc)])
|
| 5757 |
+
aa = self.alu._g(f"control.pcnext.m_jcc.bit{k}.and_a", [float(m1), ns])
|
| 5758 |
+
bb = self.alu._g(f"control.pcnext.m_jcc.bit{k}.and_b", [float(jB[k]), float(use_jcc)])
|
| 5759 |
+
m2 = self.alu._g(f"control.pcnext.m_jcc.bit{k}.or", [aa, bb])
|
| 5760 |
+
ns = self.alu._g(f"control.pcnext.m_addr.bit{k}.not_sel", [float(use_addr)])
|
| 5761 |
+
aa = self.alu._g(f"control.pcnext.m_addr.bit{k}.and_a", [float(m2), ns])
|
| 5762 |
+
bb = self.alu._g(f"control.pcnext.m_addr.bit{k}.and_b", [float(aB[k]), float(use_addr)])
|
| 5763 |
+
out[k] = self.alu._g(f"control.pcnext.m_addr.bit{k}.or", [aa, bb])
|
| 5764 |
+
return sum(out[k] << k for k in range(ab))
|
| 5765 |
+
|
| 5766 |
+
def step(self, state):
|
| 5767 |
+
if state["halted"]:
|
| 5768 |
+
return state
|
| 5769 |
+
s = dict(state)
|
| 5770 |
+
s["mem"] = state["mem"][:]
|
| 5771 |
+
s["regs"] = state["regs"][:]
|
| 5772 |
+
s["flags"] = state["flags"][:]
|
| 5773 |
+
addr_mask = (1 << self.addr_bits) - 1
|
| 5774 |
+
pc = s["pc"]
|
| 5775 |
+
hi = self.mem_read(s["mem"], pc & addr_mask)
|
| 5776 |
+
lo = self.mem_read(s["mem"], (pc + 1) & addr_mask)
|
| 5777 |
+
ir = ((hi & 0xFF) << 8) | (lo & 0xFF)
|
| 5778 |
+
opcode = (ir >> 12) & 0xF
|
| 5779 |
+
oh = self._decode_op(opcode) # gate opcode decode (one-hots)
|
| 5780 |
+
rd = (ir >> 10) & 0x3
|
| 5781 |
+
rs = (ir >> 8) & 0x3
|
| 5782 |
+
imm = ir & 0xFF
|
| 5783 |
+
addr = None
|
| 5784 |
+
if oh[0xA] or oh[0xB] or oh[0xC] or oh[0xD] or oh[0xE]:
|
| 5785 |
+
ah = self.mem_read(s["mem"], (pc + 2) & addr_mask)
|
| 5786 |
+
al = self.mem_read(s["mem"], (pc + 3) & addr_mask)
|
| 5787 |
+
addr = (((ah & 0xFF) << 8) | (al & 0xFF)) & addr_mask
|
| 5788 |
+
pc2 = self._inc(pc, "pc2", 1) # PC+2 through the gate chain
|
| 5789 |
+
pc4 = self._inc(pc, "pc4", 2) # PC+4 through the gate chain
|
| 5790 |
+
a = s["regs"][rd]
|
| 5791 |
+
b = s["regs"][rs]
|
| 5792 |
+
result = a
|
| 5793 |
+
carry = 0
|
| 5794 |
+
overflow = 0
|
| 5795 |
+
write_result = True
|
| 5796 |
+
jcc_val = 0
|
| 5797 |
+
if oh[0x0]:
|
| 5798 |
+
result, carry = self.alu.add8(a, b)
|
| 5799 |
+
overflow = 1 if (((a ^ result) & (b ^ result)) & 0x80) else 0
|
| 5800 |
+
elif oh[0x1]:
|
| 5801 |
+
result, carry = self.alu.sub8(a, b)
|
| 5802 |
+
overflow = 1 if (((a ^ b) & (a ^ result)) & 0x80) else 0
|
| 5803 |
+
elif oh[0x2]:
|
| 5804 |
+
result = self.alu.and8(a, b)
|
| 5805 |
+
elif oh[0x3]:
|
| 5806 |
+
result = self.alu.or8(a, b)
|
| 5807 |
+
elif oh[0x4]:
|
| 5808 |
+
result = self.alu.xor8(a, b)
|
| 5809 |
+
elif oh[0x5]:
|
| 5810 |
+
result = self.alu.shl8(a)
|
| 5811 |
+
elif oh[0x6]:
|
| 5812 |
+
result = self.alu.shr8(a)
|
| 5813 |
+
elif oh[0x7]:
|
| 5814 |
+
result = self.alu.mul8(a, b)
|
| 5815 |
+
elif oh[0x8]:
|
| 5816 |
+
result, _ = self.alu.div8(a, b)
|
| 5817 |
+
elif oh[0x9]: # CMP: set flags only
|
| 5818 |
+
r2, carry = self.alu.sub8(a, b)
|
| 5819 |
+
z = 1 if r2 == 0 else 0
|
| 5820 |
+
n = 1 if (r2 & 0x80) else 0
|
| 5821 |
+
v = 1 if (((a ^ b) & (a ^ r2)) & 0x80) else 0
|
| 5822 |
+
s["flags"] = [z, n, carry, v]
|
| 5823 |
+
write_result = False
|
| 5824 |
+
elif oh[0xA]: # LOAD
|
| 5825 |
+
result = self.mem_read(s["mem"], addr)
|
| 5826 |
+
elif oh[0xB]: # STORE
|
| 5827 |
+
s["mem"] = self.mem_write(s["mem"], addr, b & 0xFF)
|
| 5828 |
+
write_result = False
|
| 5829 |
+
elif oh[0xC]: # JMP (PC = addr via pcnext)
|
| 5830 |
+
write_result = False
|
| 5831 |
+
elif oh[0xD]: # JCC (PC via jcc mux + pcnext)
|
| 5832 |
+
cond = imm & 0x7
|
| 5833 |
+
circuit, flag_idx = self._JCC[cond]
|
| 5834 |
+
flag = s["flags"][flag_idx]
|
| 5835 |
+
sel = flag if cond % 2 == 0 else 1 - flag
|
| 5836 |
+
jcc_val = self._jcc_pc(circuit, pc4, addr, sel)
|
| 5837 |
+
write_result = False
|
| 5838 |
+
elif oh[0xE]: # CALL: push PC+4, PC = addr
|
| 5839 |
+
ret_addr = pc4 & 0xFFFF
|
| 5840 |
+
sp = s.get("sp", addr_mask)
|
| 5841 |
+
sp = self._sp_dec(sp)
|
| 5842 |
+
s["mem"] = self.mem_write(s["mem"], sp, (ret_addr >> 8) & 0xFF)
|
| 5843 |
+
sp = self._sp_dec(sp)
|
| 5844 |
+
s["mem"] = self.mem_write(s["mem"], sp, ret_addr & 0xFF)
|
| 5845 |
+
s["sp"] = sp
|
| 5846 |
+
write_result = False
|
| 5847 |
+
elif oh[0xF]: # HALT
|
| 5848 |
+
s["halted"] = True
|
| 5849 |
+
return s
|
| 5850 |
+
|
| 5851 |
+
if write_result:
|
| 5852 |
+
s["regs"][rd] = result & 0xFF
|
| 5853 |
+
if oh[0x0] or oh[0x1] or oh[0x7]:
|
| 5854 |
+
z = 1 if (result & 0xFF) == 0 else 0
|
| 5855 |
+
n = 1 if (result & 0x80) else 0
|
| 5856 |
+
s["flags"] = [z, n, carry, overflow]
|
| 5857 |
+
# PC sequencing through the gate mux.
|
| 5858 |
+
use_pc4 = 1 if (oh[0xA] or oh[0xB]) else 0
|
| 5859 |
+
use_addr = 1 if (oh[0xC] or oh[0xE]) else 0
|
| 5860 |
+
s["pc"] = self._pcnext(pc2, pc4, addr or 0, jcc_val, use_pc4, use_addr, oh[0xD])
|
| 5861 |
+
return s
|
| 5862 |
+
|
| 5863 |
+
def run(self, state, max_cycles=200):
|
| 5864 |
+
s = state
|
| 5865 |
+
cycles = 0
|
| 5866 |
+
while not s["halted"] and cycles < max_cycles:
|
| 5867 |
+
s = self.step(s)
|
| 5868 |
+
cycles += 1
|
| 5869 |
+
return s, cycles
|
| 5870 |
+
|
| 5871 |
+
|
| 5872 |
+
def _encode_instr(opcode, rd, rs, imm):
|
| 5873 |
+
return ((opcode & 0xF) << 12) | ((rd & 0x3) << 10) | ((rs & 0x3) << 8) | (imm & 0xFF)
|
| 5874 |
+
|
| 5875 |
+
|
| 5876 |
+
def _w16(mem, addr, value):
|
| 5877 |
+
mem[addr] = (value >> 8) & 0xFF
|
| 5878 |
+
mem[addr + 1] = value & 0xFF
|
| 5879 |
+
|
| 5880 |
+
|
| 5881 |
+
PROGRAM_MIN_BYTES = 0x84 # code 0x00..0x1F + data 0x80..0x83
|
| 5882 |
+
|
| 5883 |
+
|
| 5884 |
+
def builtin_program(addr_bits: int) -> Tuple[List[int], int]:
|
| 5885 |
+
"""Sum 5+4+3+2+1 via a loop."""
|
| 5886 |
+
if (1 << addr_bits) < PROGRAM_MIN_BYTES:
|
| 5887 |
+
raise ValueError(f"addr_bits={addr_bits} too small for builtin program")
|
| 5888 |
+
mem = [0] * (1 << addr_bits)
|
| 5889 |
+
mem[0x80] = 5 # initial counter
|
| 5890 |
+
mem[0x81] = 1 # decrement
|
| 5891 |
+
mem[0x82] = 0 # zero (for compare and accumulator init)
|
| 5892 |
+
# mem[0x83] is the output
|
| 5893 |
+
_w16(mem, 0x0000, _encode_instr(0xA, 1, 0, 0)); _w16(mem, 0x0002, 0x0080)
|
| 5894 |
+
_w16(mem, 0x0004, _encode_instr(0xA, 2, 0, 0)); _w16(mem, 0x0006, 0x0081)
|
| 5895 |
+
_w16(mem, 0x0008, _encode_instr(0xA, 3, 0, 0)); _w16(mem, 0x000A, 0x0082)
|
| 5896 |
+
_w16(mem, 0x000C, _encode_instr(0xA, 0, 0, 0)); _w16(mem, 0x000E, 0x0082)
|
| 5897 |
+
_w16(mem, 0x0010, _encode_instr(0x0, 0, 1, 0))
|
| 5898 |
+
_w16(mem, 0x0012, _encode_instr(0x1, 1, 2, 0))
|
| 5899 |
+
_w16(mem, 0x0014, _encode_instr(0x9, 1, 3, 0))
|
| 5900 |
+
_w16(mem, 0x0016, _encode_instr(0xD, 0, 0, 0x01)); _w16(mem, 0x0018, 0x0010)
|
| 5901 |
+
_w16(mem, 0x001A, _encode_instr(0xB, 0, 0, 0)); _w16(mem, 0x001C, 0x0083)
|
| 5902 |
+
_w16(mem, 0x001E, _encode_instr(0xF, 0, 0, 0))
|
| 5903 |
+
return mem, 15
|
| 5904 |
+
|
| 5905 |
+
|
| 5906 |
+
# ---------------------------------------------------------------------------
|
| 5907 |
+
# Eval driver
|
| 5908 |
+
# ---------------------------------------------------------------------------
|
| 5909 |
+
|
| 5910 |
+
def _file_fingerprint(path: Path) -> str:
|
| 5911 |
+
"""Stable cache key for a safetensors file: sha256 of its content."""
|
| 5912 |
+
import hashlib
|
| 5913 |
+
h = hashlib.sha256()
|
| 5914 |
+
with open(path, "rb") as f:
|
| 5915 |
+
for chunk in iter(lambda: f.read(1 << 20), b""):
|
| 5916 |
+
h.update(chunk)
|
| 5917 |
+
return h.hexdigest()
|
| 5918 |
+
|
| 5919 |
+
|
| 5920 |
+
def _cache_key(path: Path, opts: Dict[str, Any]) -> str:
|
| 5921 |
+
"""Cache key combining file content with the relevant evaluation options."""
|
| 5922 |
+
fp = _file_fingerprint(path)
|
| 5923 |
+
opt_str = json.dumps(opts, sort_keys=True)
|
| 5924 |
+
import hashlib
|
| 5925 |
+
suffix = hashlib.sha256(opt_str.encode("utf-8")).hexdigest()[:8]
|
| 5926 |
+
return f"{fp}_{suffix}"
|
| 5927 |
+
|
| 5928 |
+
|
| 5929 |
+
def _load_cache(cache_dir: Path, key: str) -> Dict[str, Any] | None:
|
| 5930 |
+
p = cache_dir / f"{key}.json"
|
| 5931 |
+
if not p.exists():
|
| 5932 |
+
return None
|
| 5933 |
+
try:
|
| 5934 |
+
return json.loads(p.read_text(encoding="utf-8"))
|
| 5935 |
+
except (json.JSONDecodeError, OSError):
|
| 5936 |
+
return None
|
| 5937 |
+
|
| 5938 |
+
|
| 5939 |
+
def _save_cache(cache_dir: Path, key: str, payload: Dict[str, Any]) -> None:
|
| 5940 |
+
cache_dir.mkdir(parents=True, exist_ok=True)
|
| 5941 |
+
p = cache_dir / f"{key}.json"
|
| 5942 |
+
try:
|
| 5943 |
+
p.write_text(json.dumps(payload, indent=2, default=str), encoding="utf-8")
|
| 5944 |
+
except OSError:
|
| 5945 |
+
pass
|
| 5946 |
+
|
| 5947 |
+
|
| 5948 |
+
def list_safetensors(path: Path) -> List[Path]:
|
| 5949 |
+
if path.is_file():
|
| 5950 |
+
return [path]
|
| 5951 |
+
if path.is_dir():
|
| 5952 |
+
return sorted(p for p in path.glob("*.safetensors") if p.is_file())
|
| 5953 |
+
return []
|
| 5954 |
+
|
| 5955 |
+
|
| 5956 |
+
# Standalone machines carry their own circuit inventory and are verified by
|
| 5957 |
+
# their own scripts, not the gate-fitness suite. Each maps to the tool that
|
| 5958 |
+
# checks it.
|
| 5959 |
+
MACHINE_VERIFIER = {
|
| 5960 |
+
"subleq8": "src/machines.py",
|
| 5961 |
+
"rv32": "src/machines.py",
|
| 5962 |
+
"matrix8": "src/machines.py matrix8",
|
| 5963 |
+
"subleq8io": "src/host.py constructor8",
|
| 5964 |
+
"reflect": "src/reflect.py",
|
| 5965 |
+
"attractor": "src/automata.py attractor",
|
| 5966 |
+
"reversible": "src/automata.py reversible",
|
| 5967 |
+
"ca": "src/automata.py ca",
|
| 5968 |
+
"tile": "src/automata.py tile",
|
| 5969 |
+
}
|
| 5970 |
+
|
| 5971 |
+
|
| 5972 |
+
def evaluate_one(path: Path, device: str, pop_size: int, debug: bool, run_cpu_program: bool) -> Dict:
|
| 5973 |
+
out: Dict = {"path": str(path), "filename": path.name}
|
| 5974 |
+
# Skip standalone machines cleanly rather than error on missing standard gates.
|
| 5975 |
+
with safe_open(str(path), framework="pt") as f:
|
| 5976 |
+
meta = f.metadata() or {}
|
| 5977 |
+
if meta.get("machine"):
|
| 5978 |
+
m = meta["machine"]
|
| 5979 |
+
out.update(status="SKIP", machine=m,
|
| 5980 |
+
note=f"standalone machine — verify with {MACHINE_VERIFIER.get(m, 'machines.py')}")
|
| 5981 |
+
return out
|
| 5982 |
+
try:
|
| 5983 |
+
tensors = load_model(str(path))
|
| 5984 |
+
except Exception as e:
|
| 5985 |
+
out.update(error=f"load failed: {e}", status="ERROR")
|
| 5986 |
+
return out
|
| 5987 |
+
|
| 5988 |
+
manifest = get_manifest(tensors)
|
| 5989 |
+
out.update(
|
| 5990 |
+
size_mb=path.stat().st_size / (1024 * 1024),
|
| 5991 |
+
tensors=len(tensors),
|
| 5992 |
+
params=sum(t.numel() for t in tensors.values()),
|
| 5993 |
+
manifest=manifest,
|
| 5994 |
+
)
|
| 5995 |
+
|
| 5996 |
+
# Move to device
|
| 5997 |
+
tensors = {k: v.to(device) for k, v in tensors.items()}
|
| 5998 |
+
|
| 5999 |
+
try:
|
| 6000 |
+
evaluator = BatchedFitnessEvaluator(device=device, model_path=str(path), tensors=tensors)
|
| 6001 |
+
population = create_population(tensors, pop_size=pop_size, device=device)
|
| 6002 |
+
t0 = time.perf_counter()
|
| 6003 |
+
fitness = evaluator.evaluate(population, debug=debug)
|
| 6004 |
+
elapsed = time.perf_counter() - t0
|
| 6005 |
+
f0 = float(fitness[0].item()) if pop_size == 1 else float(fitness.mean().item())
|
| 6006 |
+
out.update(
|
| 6007 |
+
fitness=f0,
|
| 6008 |
+
total_tests=evaluator.total_tests,
|
| 6009 |
+
elapsed_s=elapsed,
|
| 6010 |
+
categories={k: (float(v[0]), int(v[1])) for k, v in evaluator.category_scores.items()},
|
| 6011 |
+
status="PASS" if f0 >= 0.9999 else "FAIL",
|
| 6012 |
+
)
|
| 6013 |
+
except Exception as e:
|
| 6014 |
+
out.update(error=f"eval failed: {type(e).__name__}: {e}", status="ERROR")
|
| 6015 |
+
return out
|
| 6016 |
+
|
| 6017 |
+
# Optional: CPU program test (8-bit CPU primitives are in every variant)
|
| 6018 |
+
if run_cpu_program:
|
| 6019 |
+
if manifest["memory_bytes"] >= PROGRAM_MIN_BYTES:
|
| 6020 |
+
try:
|
| 6021 |
+
cpu_tensors = {k: v.cpu() for k, v in tensors.items()}
|
| 6022 |
+
cpu = GenericThresholdCPU(cpu_tensors)
|
| 6023 |
+
mem, expected = builtin_program(manifest["addr_bits"])
|
| 6024 |
+
state = {"pc": 0, "regs": [0] * 4, "flags": [0] * 4, "mem": mem, "halted": False}
|
| 6025 |
+
t0 = time.perf_counter()
|
| 6026 |
+
final, cycles = cpu.run(state, max_cycles=200)
|
| 6027 |
+
cpu_elapsed = time.perf_counter() - t0
|
| 6028 |
+
got = final["mem"][0x83]
|
| 6029 |
+
out["cpu_program"] = {
|
| 6030 |
+
"ok": got == expected,
|
| 6031 |
+
"got": got,
|
| 6032 |
+
"expected": expected,
|
| 6033 |
+
"cycles": cycles,
|
| 6034 |
+
"elapsed_s": cpu_elapsed,
|
| 6035 |
+
}
|
| 6036 |
+
if got != expected:
|
| 6037 |
+
out["status"] = "FAIL"
|
| 6038 |
+
except Exception as e:
|
| 6039 |
+
out["cpu_program"] = {"error": str(e)}
|
| 6040 |
+
else:
|
| 6041 |
+
out["cpu_program"] = {"skipped": f"mem={manifest['memory_bytes']}B < {PROGRAM_MIN_BYTES}"}
|
| 6042 |
+
|
| 6043 |
+
# Wider-ALU chain test for 16/32-bit variants
|
| 6044 |
+
bits = manifest["data_bits"]
|
| 6045 |
+
if bits in (16, 32):
|
| 6046 |
+
try:
|
| 6047 |
+
alu_tensors = {k: v.cpu() for k, v in tensors.items()}
|
| 6048 |
+
alu = GenericThresholdALU(alu_tensors, bits)
|
| 6049 |
+
t0 = time.perf_counter()
|
| 6050 |
+
if bits == 16:
|
| 6051 |
+
x, y = 1234, 5678
|
| 6052 |
+
z, _ = alu.add_n(x, y, 16); assert z == (x + y) & 0xFFFF
|
| 6053 |
+
w, _ = alu.sub_n(z, x, 16); assert w == (z - x) & 0xFFFF, (w, z - x)
|
| 6054 |
+
gt = alu.cmp_n(z, x, "greaterthan", 16); assert gt == 1
|
| 6055 |
+
lt = alu.cmp_n(x, z, "lessthan", 16); assert lt == 1
|
| 6056 |
+
eq = alu.cmp_n(w, y, "eq", 16); assert eq == 1
|
| 6057 |
+
p = alu.mul_n(123, 5, 16); assert p == (123 * 5) & 0xFFFF
|
| 6058 |
+
else: # 32
|
| 6059 |
+
x, y = 1_000_000, 999_000
|
| 6060 |
+
z, _ = alu.sub_n(x, y, 32); assert z == 1_000
|
| 6061 |
+
s, _ = alu.add_n(z, x, 32); assert s == 1_001_000
|
| 6062 |
+
p = alu.mul_n(z, 100, 32); assert p == 100_000
|
| 6063 |
+
gt = alu.cmp_n(x, y, "greaterthan", 32); assert gt == 1
|
| 6064 |
+
lt = alu.cmp_n(y, x, "lessthan", 32); assert lt == 1
|
| 6065 |
+
eq = alu.cmp_n(p, 100_000, "equality", 32); assert eq == 1
|
| 6066 |
+
chain_dt = time.perf_counter() - t0
|
| 6067 |
+
out[f"alu_chain_{bits}"] = {"ok": True, "elapsed_s": chain_dt}
|
| 6068 |
+
except AssertionError as e:
|
| 6069 |
+
out[f"alu_chain_{bits}"] = {"ok": False, "error": f"chain mismatch: {e}"}
|
| 6070 |
+
out["status"] = "FAIL"
|
| 6071 |
+
except Exception as e:
|
| 6072 |
+
out[f"alu_chain_{bits}"] = {"ok": False, "error": f"{type(e).__name__}: {e}"}
|
| 6073 |
+
out["status"] = "FAIL"
|
| 6074 |
+
|
| 6075 |
+
return out
|
| 6076 |
+
|
| 6077 |
+
|
| 6078 |
+
def print_row(r: Dict, show_cpu: bool) -> None:
|
| 6079 |
+
if r.get("status") == "SKIP":
|
| 6080 |
+
m = r.get("machine", "machine")
|
| 6081 |
+
print(f" {r['filename']:<48} SKIP ({m} — verify with {MACHINE_VERIFIER.get(m, 'machines.py')})")
|
| 6082 |
+
return
|
| 6083 |
+
if "error" in r:
|
| 6084 |
+
print(f" {r['filename']:<48} ERROR: {r['error'][:80]}")
|
| 6085 |
+
return
|
| 6086 |
+
m = r["manifest"]
|
| 6087 |
+
fit = f"{r['fitness']:.4f}" if r.get("fitness") is not None else "n/a"
|
| 6088 |
+
cpu_col = ""
|
| 6089 |
+
if show_cpu and "cpu_program" in r:
|
| 6090 |
+
cp = r["cpu_program"]
|
| 6091 |
+
if cp.get("ok"):
|
| 6092 |
+
cpu_col = f" CPU OK ({cp['cycles']}cyc/{cp['elapsed_s']:.1f}s)"
|
| 6093 |
+
elif "skipped" in cp:
|
| 6094 |
+
cpu_col = f" CPU SKIP"
|
| 6095 |
+
elif "error" in cp:
|
| 6096 |
+
cpu_col = f" CPU ERR"
|
| 6097 |
+
else:
|
| 6098 |
+
cpu_col = f" CPU FAIL ({cp.get('got')}!={cp.get('expected')})"
|
| 6099 |
+
chain_col = ""
|
| 6100 |
+
if show_cpu:
|
| 6101 |
+
for bits in (16, 32):
|
| 6102 |
+
key = f"alu_chain_{bits}"
|
| 6103 |
+
if key in r:
|
| 6104 |
+
ch = r[key]
|
| 6105 |
+
if ch.get("ok"):
|
| 6106 |
+
chain_col = f" ALU{bits} OK ({ch['elapsed_s']:.2f}s)"
|
| 6107 |
+
else:
|
| 6108 |
+
chain_col = f" ALU{bits} FAIL"
|
| 6109 |
+
print(
|
| 6110 |
+
f" {r['filename']:<48} d={m['data_bits']:>2}b a={m['addr_bits']:>2}b "
|
| 6111 |
+
f"mem={m['memory_bytes']:>6}B size={r['size_mb']:>6.1f}MB "
|
| 6112 |
+
f"params={r['params']:>10,} fit={fit:>6} tests={r['total_tests']:>5} "
|
| 6113 |
+
f"{r['status']:>5}{cpu_col}{chain_col}"
|
| 6114 |
+
)
|
| 6115 |
+
|
| 6116 |
+
|
| 6117 |
+
def main_all() -> int:
|
| 6118 |
+
parser = argparse.ArgumentParser(description="Variant-agnostic eval harness")
|
| 6119 |
+
parser.add_argument("path", help="Path to .safetensors file or directory of files")
|
| 6120 |
+
parser.add_argument("--device", default="cpu", help="cpu (default) or cuda")
|
| 6121 |
+
parser.add_argument("--pop_size", type=int, default=1)
|
| 6122 |
+
parser.add_argument("--debug", action="store_true", help="Per-circuit detail per file")
|
| 6123 |
+
parser.add_argument("--cpu-program", action="store_true",
|
| 6124 |
+
help="Also run a small assembled program through the threshold CPU "
|
| 6125 |
+
"(any variant with >= 132 B memory), plus a chained 16- or "
|
| 6126 |
+
"32-bit ALU sequence on wider variants")
|
| 6127 |
+
parser.add_argument("--json", action="store_true", help="Emit JSON results to stdout instead of a table")
|
| 6128 |
+
parser.add_argument("--cache-dir", default=".eval_cache",
|
| 6129 |
+
help="Directory for hash-keyed result cache "
|
| 6130 |
+
"(default: ./.eval_cache). Set to '' to disable.")
|
| 6131 |
+
parser.add_argument("--no-cache", action="store_true",
|
| 6132 |
+
help="Disable the result cache for this run.")
|
| 6133 |
+
args = parser.parse_args()
|
| 6134 |
+
|
| 6135 |
+
files = list_safetensors(Path(args.path))
|
| 6136 |
+
if not files:
|
| 6137 |
+
print(f"No .safetensors files found under {args.path}", file=sys.stderr)
|
| 6138 |
+
return 2
|
| 6139 |
+
|
| 6140 |
+
print(f"Evaluating {len(files)} file(s) on {args.device}\n")
|
| 6141 |
+
cache_enabled = bool(args.cache_dir) and not args.no_cache
|
| 6142 |
+
cache_dir = Path(args.cache_dir) if cache_enabled else None
|
| 6143 |
+
cache_opts = {
|
| 6144 |
+
"device": args.device,
|
| 6145 |
+
"pop_size": args.pop_size,
|
| 6146 |
+
"cpu_program": bool(args.cpu_program),
|
| 6147 |
+
}
|
| 6148 |
+
cache_hits = 0
|
| 6149 |
+
results = []
|
| 6150 |
+
fail_count = 0
|
| 6151 |
+
for f in files:
|
| 6152 |
+
print(f"=== {f.name}")
|
| 6153 |
+
cached = None
|
| 6154 |
+
key = None
|
| 6155 |
+
if cache_enabled:
|
| 6156 |
+
try:
|
| 6157 |
+
key = _cache_key(f, cache_opts)
|
| 6158 |
+
cached = _load_cache(cache_dir, key)
|
| 6159 |
+
except OSError:
|
| 6160 |
+
cached = None
|
| 6161 |
+
if cached is not None:
|
| 6162 |
+
r = cached
|
| 6163 |
+
cache_hits += 1
|
| 6164 |
+
print(f" (cache hit)")
|
| 6165 |
+
else:
|
| 6166 |
+
r = evaluate_one(f, device=args.device, pop_size=args.pop_size,
|
| 6167 |
+
debug=args.debug, run_cpu_program=args.cpu_program)
|
| 6168 |
+
# Don't cache ERROR results: a transient failure (OOM, interrupt)
|
| 6169 |
+
# would otherwise be replayed on every rerun of the same file.
|
| 6170 |
+
if cache_enabled and key is not None and r.get("status") != "ERROR":
|
| 6171 |
+
_save_cache(cache_dir, key, r)
|
| 6172 |
+
results.append(r)
|
| 6173 |
+
print_row(r, show_cpu=args.cpu_program)
|
| 6174 |
+
if r.get("status") not in ("PASS", "SKIP"):
|
| 6175 |
+
fail_count += 1
|
| 6176 |
+
|
| 6177 |
+
if args.json:
|
| 6178 |
+
# Make it serialisable
|
| 6179 |
+
for r in results:
|
| 6180 |
+
r["manifest"] = {k: (int(v) if isinstance(v, float) and v.is_integer() else v)
|
| 6181 |
+
for k, v in r.get("manifest", {}).items()}
|
| 6182 |
+
print(json.dumps(results, indent=2, default=str))
|
| 6183 |
+
return fail_count
|
| 6184 |
+
|
| 6185 |
+
# Summary
|
| 6186 |
+
print()
|
| 6187 |
+
print("=" * 100)
|
| 6188 |
+
print(" SUMMARY")
|
| 6189 |
+
print("=" * 100)
|
| 6190 |
+
for r in results:
|
| 6191 |
+
print_row(r, show_cpu=args.cpu_program)
|
| 6192 |
+
|
| 6193 |
+
print()
|
| 6194 |
+
if fail_count == 0:
|
| 6195 |
+
print(f"ALL {len(files)} variants PASS")
|
| 6196 |
+
else:
|
| 6197 |
+
print(f"{fail_count}/{len(files)} variants FAIL")
|
| 6198 |
+
if cache_enabled:
|
| 6199 |
+
print(f"(cache: {cache_hits}/{len(files)} hits, dir={cache_dir})")
|
| 6200 |
+
return fail_count
|
| 6201 |
+
|
| 6202 |
+
|
| 6203 |
+
# ----------------------------------------------------------------------------
|
| 6204 |
+
# Mini assembler
|
| 6205 |
+
# ----------------------------------------------------------------------------
|
| 6206 |
+
|
| 6207 |
+
_OPCODE_NAMES = {
|
| 6208 |
+
"add": 0x0, "sub": 0x1, "and": 0x2, "or": 0x3, "xor": 0x4,
|
| 6209 |
+
"shl": 0x5, "shr": 0x6, "mul": 0x7, "div": 0x8, "cmp": 0x9,
|
| 6210 |
+
"load": 0xA, "store": 0xB, "jmp": 0xC, "jcc": 0xD,
|
| 6211 |
+
"call": 0xE, "halt": 0xF,
|
| 6212 |
+
}
|
| 6213 |
+
|
| 6214 |
+
_COND = {"jz": 0, "jnz": 1, "jc": 2, "jnc": 3,
|
| 6215 |
+
"jn": 4, "jp": 5, "jv": 6, "jnv": 7}
|
| 6216 |
+
|
| 6217 |
+
|
| 6218 |
+
def _enc(opcode: int, rd: int = 0, rs: int = 0, imm: int = 0) -> int:
|
| 6219 |
+
return ((opcode & 0xF) << 12) | ((rd & 0x3) << 10) | ((rs & 0x3) << 8) | (imm & 0xFF)
|
| 6220 |
+
|
| 6221 |
+
|
| 6222 |
+
class Asm:
|
| 6223 |
+
"""Tiny assembler for the threshold-computer ISA."""
|
| 6224 |
+
|
| 6225 |
+
def __init__(self, size: int):
|
| 6226 |
+
self.mem: List[int] = [0] * size
|
| 6227 |
+
self.pc: int = 0
|
| 6228 |
+
self.labels: Dict[str, int] = {}
|
| 6229 |
+
self.fixups: List[Tuple[int, str]] = []
|
| 6230 |
+
|
| 6231 |
+
def org(self, addr: int) -> None:
|
| 6232 |
+
self.pc = addr
|
| 6233 |
+
|
| 6234 |
+
def label(self, name: str) -> None:
|
| 6235 |
+
if name in self.labels:
|
| 6236 |
+
raise ValueError(f"duplicate label: {name}")
|
| 6237 |
+
self.labels[name] = self.pc
|
| 6238 |
+
|
| 6239 |
+
def db(self, *values: int) -> None:
|
| 6240 |
+
for v in values:
|
| 6241 |
+
self.mem[self.pc] = v & 0xFF
|
| 6242 |
+
self.pc += 1
|
| 6243 |
+
|
| 6244 |
+
def dw(self, value: int) -> None:
|
| 6245 |
+
self.mem[self.pc] = (value >> 8) & 0xFF
|
| 6246 |
+
self.mem[self.pc + 1] = value & 0xFF
|
| 6247 |
+
self.pc += 2
|
| 6248 |
+
|
| 6249 |
+
def daddr(self, label: str) -> None:
|
| 6250 |
+
self.fixups.append((self.pc, label))
|
| 6251 |
+
self.dw(0)
|
| 6252 |
+
|
| 6253 |
+
# --- ALU ops (no immediate) ---
|
| 6254 |
+
def _alu(self, op: int, rd: int, rs: int) -> None:
|
| 6255 |
+
self.dw(_enc(op, rd, rs))
|
| 6256 |
+
|
| 6257 |
+
def add(self, rd: int, rs: int) -> None: self._alu(0x0, rd, rs)
|
| 6258 |
+
def sub(self, rd: int, rs: int) -> None: self._alu(0x1, rd, rs)
|
| 6259 |
+
def and_(self, rd: int, rs: int) -> None: self._alu(0x2, rd, rs)
|
| 6260 |
+
def or_(self, rd: int, rs: int) -> None: self._alu(0x3, rd, rs)
|
| 6261 |
+
def xor(self, rd: int, rs: int) -> None: self._alu(0x4, rd, rs)
|
| 6262 |
+
def shl(self, rd: int) -> None: self._alu(0x5, rd, 0)
|
| 6263 |
+
def shr(self, rd: int) -> None: self._alu(0x6, rd, 0)
|
| 6264 |
+
def mul(self, rd: int, rs: int) -> None: self._alu(0x7, rd, rs)
|
| 6265 |
+
def cmp(self, rd: int, rs: int) -> None: self._alu(0x9, rd, rs)
|
| 6266 |
+
|
| 6267 |
+
# --- Memory + control (address-extended) ---
|
| 6268 |
+
def load(self, rd: int, label: str) -> None:
|
| 6269 |
+
self.dw(_enc(0xA, rd, 0)); self.daddr(label)
|
| 6270 |
+
|
| 6271 |
+
def store(self, rs: int, label: str) -> None:
|
| 6272 |
+
self.dw(_enc(0xB, 0, rs)); self.daddr(label)
|
| 6273 |
+
|
| 6274 |
+
def jmp(self, label: str) -> None:
|
| 6275 |
+
self.dw(_enc(0xC)); self.daddr(label)
|
| 6276 |
+
|
| 6277 |
+
def jcc(self, cond: str, label: str) -> None:
|
| 6278 |
+
self.dw(_enc(0xD, 0, 0, _COND[cond])); self.daddr(label)
|
| 6279 |
+
|
| 6280 |
+
def jz(self, label: str) -> None: self.jcc("jz", label)
|
| 6281 |
+
def jnz(self, label: str) -> None: self.jcc("jnz", label)
|
| 6282 |
+
def jc(self, label: str) -> None: self.jcc("jc", label)
|
| 6283 |
+
def jnc(self, label: str) -> None: self.jcc("jnc", label)
|
| 6284 |
+
def jn(self, label: str) -> None: self.jcc("jn", label)
|
| 6285 |
+
def jp(self, label: str) -> None: self.jcc("jp", label)
|
| 6286 |
+
def jv(self, label: str) -> None: self.jcc("jv", label)
|
| 6287 |
+
def jnv(self, label: str) -> None: self.jcc("jnv", label)
|
| 6288 |
+
|
| 6289 |
+
def call(self, label: str) -> None:
|
| 6290 |
+
self.dw(_enc(0xE)); self.daddr(label)
|
| 6291 |
+
|
| 6292 |
+
def halt(self) -> None:
|
| 6293 |
+
self.dw(_enc(0xF))
|
| 6294 |
+
|
| 6295 |
+
def assemble(self) -> List[int]:
|
| 6296 |
+
for offset, label in self.fixups:
|
| 6297 |
+
if label not in self.labels:
|
| 6298 |
+
raise ValueError(f"undefined label: {label}")
|
| 6299 |
+
target = self.labels[label]
|
| 6300 |
+
self.mem[offset] = (target >> 8) & 0xFF
|
| 6301 |
+
self.mem[offset + 1] = target & 0xFF
|
| 6302 |
+
return self.mem
|
| 6303 |
+
|
| 6304 |
+
|
| 6305 |
+
# ----------------------------------------------------------------------------
|
| 6306 |
+
# Programs
|
| 6307 |
+
# ----------------------------------------------------------------------------
|
| 6308 |
+
|
| 6309 |
+
ProgramResult = Tuple[List[int], Dict[int, int], int, str]
|
| 6310 |
+
|
| 6311 |
+
|
| 6312 |
+
def fib(n: int = 11, mem_size: int = 256) -> ProgramResult:
|
| 6313 |
+
"""Iterative Fibonacci F(N), 8-bit wrap."""
|
| 6314 |
+
a = Asm(mem_size)
|
| 6315 |
+
|
| 6316 |
+
a.org(0)
|
| 6317 |
+
a.load(2, "n_addr") # R2 = n
|
| 6318 |
+
a.load(0, "zero_addr") # R0 = 0 = F(0)
|
| 6319 |
+
a.load(1, "one_addr") # R1 = 1 = F(1)
|
| 6320 |
+
a.label("loop")
|
| 6321 |
+
a.load(3, "zero_addr") # R3 = 0
|
| 6322 |
+
a.cmp(2, 3) # n == 0?
|
| 6323 |
+
a.jz("done")
|
| 6324 |
+
a.load(3, "zero_addr") # R3 = 0
|
| 6325 |
+
a.add(3, 1) # R3 = b (saved old b)
|
| 6326 |
+
a.add(1, 0) # R1 = a + b (new b)
|
| 6327 |
+
a.load(0, "zero_addr") # R0 = 0
|
| 6328 |
+
a.add(0, 3) # R0 = old b (new a)
|
| 6329 |
+
a.load(3, "one_addr") # R3 = 1
|
| 6330 |
+
a.sub(2, 3) # n--
|
| 6331 |
+
a.jmp("loop")
|
| 6332 |
+
a.label("done")
|
| 6333 |
+
a.store(0, "out") # OUT = a
|
| 6334 |
+
a.halt()
|
| 6335 |
+
|
| 6336 |
+
a.org(0x80)
|
| 6337 |
+
a.label("zero_addr"); a.db(0)
|
| 6338 |
+
a.label("one_addr"); a.db(1)
|
| 6339 |
+
a.label("n_addr"); a.db(n)
|
| 6340 |
+
a.label("out"); a.db(0)
|
| 6341 |
+
|
| 6342 |
+
mem = a.assemble()
|
| 6343 |
+
expected_a = 0
|
| 6344 |
+
aa, bb = 0, 1
|
| 6345 |
+
for _ in range(n):
|
| 6346 |
+
aa, bb = bb, (aa + bb) & 0xFF
|
| 6347 |
+
expected_a = aa
|
| 6348 |
+
return mem, {a.labels["out"]: expected_a}, 16 * (n + 2), f"Fibonacci F({n}) = {expected_a}"
|
| 6349 |
+
|
| 6350 |
+
|
| 6351 |
+
def sum_n(n: int = 10, mem_size: int = 256) -> ProgramResult:
|
| 6352 |
+
"""Compute 1 + 2 + ... + N using the Z flag from SUB to terminate."""
|
| 6353 |
+
a = Asm(mem_size)
|
| 6354 |
+
|
| 6355 |
+
a.org(0)
|
| 6356 |
+
a.load(0, "zero_addr") # acc = 0
|
| 6357 |
+
a.load(1, "n_addr") # i = n
|
| 6358 |
+
a.load(2, "one_addr") # 1
|
| 6359 |
+
a.label("loop")
|
| 6360 |
+
a.add(0, 1) # acc += i
|
| 6361 |
+
a.sub(1, 2) # i-- (Z set when i becomes 0)
|
| 6362 |
+
a.jnz("loop")
|
| 6363 |
+
a.store(0, "out")
|
| 6364 |
+
a.halt()
|
| 6365 |
+
|
| 6366 |
+
a.org(0x80)
|
| 6367 |
+
a.label("zero_addr"); a.db(0)
|
| 6368 |
+
a.label("one_addr"); a.db(1)
|
| 6369 |
+
a.label("n_addr"); a.db(n)
|
| 6370 |
+
a.label("out"); a.db(0)
|
| 6371 |
+
|
| 6372 |
+
mem = a.assemble()
|
| 6373 |
+
expected = (n * (n + 1) // 2) & 0xFF
|
| 6374 |
+
return mem, {a.labels["out"]: expected}, 4 + 4 * n, f"sum 1..{n} = {expected}"
|
| 6375 |
+
|
| 6376 |
+
|
| 6377 |
+
def self_mod_jmp(mem_size: int = 256) -> ProgramResult:
|
| 6378 |
+
"""Self-modifying code: writes the JMP target's low byte at runtime."""
|
| 6379 |
+
a = Asm(mem_size)
|
| 6380 |
+
|
| 6381 |
+
a.org(0)
|
| 6382 |
+
a.label("start")
|
| 6383 |
+
|
| 6384 |
+
# Forward-declare the_jmp's LSB address. The first two instructions are
|
| 6385 |
+
# each 4 bytes; the_jmp follows them, so the_jmp = pc + 8 from start.
|
| 6386 |
+
# The JMP's address word is at the_jmp + 2; the LSB byte is at the_jmp + 3.
|
| 6387 |
+
a.labels["jmp_target_lsb"] = a.pc + 8 + 3
|
| 6388 |
+
|
| 6389 |
+
a.load(0, "new_lsb") # R0 = LSB of path_b address (4 bytes)
|
| 6390 |
+
a.store(0, "jmp_target_lsb") # patch the JMP's LSB (4 bytes)
|
| 6391 |
+
a.label("the_jmp")
|
| 6392 |
+
a.jmp("path_a") # initially -> path_a; after patch -> path_b
|
| 6393 |
+
a.label("path_a")
|
| 6394 |
+
a.load(1, "val_a"); a.store(1, "out"); a.halt()
|
| 6395 |
+
a.label("path_b")
|
| 6396 |
+
a.load(1, "val_b"); a.store(1, "out"); a.halt()
|
| 6397 |
+
|
| 6398 |
+
a.org(0x80)
|
| 6399 |
+
a.label("val_a"); a.db(0xAA)
|
| 6400 |
+
a.label("val_b"); a.db(0xBB)
|
| 6401 |
+
# new_lsb_word stores path_b's full 16-bit address; new_lsb labels the LSB byte.
|
| 6402 |
+
a.label("new_lsb_word"); a.daddr("path_b")
|
| 6403 |
+
a.labels["new_lsb"] = a.labels["new_lsb_word"] + 1
|
| 6404 |
+
a.label("out"); a.db(0)
|
| 6405 |
+
|
| 6406 |
+
mem = a.assemble()
|
| 6407 |
+
return mem, {a.labels["out"]: 0xBB}, 30, "self-modifying JMP target"
|
| 6408 |
+
|
| 6409 |
+
|
| 6410 |
+
def all_branches(mem_size: int = 256) -> ProgramResult:
|
| 6411 |
+
"""Drive all eight conditional jumps; each path writes a unique marker."""
|
| 6412 |
+
a = Asm(mem_size)
|
| 6413 |
+
|
| 6414 |
+
a.org(0)
|
| 6415 |
+
# ----- JZ: equal compare -> Z=1 -----
|
| 6416 |
+
a.load(0, "v5"); a.load(1, "v5"); a.cmp(0, 1); a.jz("ok_jz"); a.jmp("fail")
|
| 6417 |
+
a.label("ok_jz"); a.load(2, "m_a0"); a.store(2, "out0")
|
| 6418 |
+
|
| 6419 |
+
# ----- JNZ: unequal compare -> Z=0 -----
|
| 6420 |
+
a.load(0, "v5"); a.load(1, "v3"); a.cmp(0, 1); a.jnz("ok_jnz"); a.jmp("fail")
|
| 6421 |
+
a.label("ok_jnz"); a.load(2, "m_a1"); a.store(2, "out1")
|
| 6422 |
+
|
| 6423 |
+
# ----- JC: 255+1 = 0 with carry -----
|
| 6424 |
+
a.load(0, "v255"); a.load(1, "v1"); a.add(0, 1); a.jc("ok_jc"); a.jmp("fail")
|
| 6425 |
+
a.label("ok_jc"); a.load(2, "m_a2"); a.store(2, "out2")
|
| 6426 |
+
|
| 6427 |
+
# ----- JNC: 1+1 = 2, no carry -----
|
| 6428 |
+
a.load(0, "v1"); a.load(1, "v1"); a.add(0, 1); a.jnc("ok_jnc"); a.jmp("fail")
|
| 6429 |
+
a.label("ok_jnc"); a.load(2, "m_a3"); a.store(2, "out3")
|
| 6430 |
+
|
| 6431 |
+
# ----- JN: 0 - 1 = 0xFF, MSB set -----
|
| 6432 |
+
a.load(0, "v0"); a.load(1, "v1"); a.sub(0, 1); a.jn("ok_jn"); a.jmp("fail")
|
| 6433 |
+
a.label("ok_jn"); a.load(2, "m_a4"); a.store(2, "out4")
|
| 6434 |
+
|
| 6435 |
+
# ----- JP: 0 + 1 = 1, MSB clear -----
|
| 6436 |
+
a.load(0, "v0"); a.load(1, "v1"); a.add(0, 1); a.jp("ok_jp"); a.jmp("fail")
|
| 6437 |
+
a.label("ok_jp"); a.load(2, "m_a5"); a.store(2, "out5")
|
| 6438 |
+
|
| 6439 |
+
# ----- JV: 127 + 1 = 128, signed overflow -----
|
| 6440 |
+
a.load(0, "v127"); a.load(1, "v1"); a.add(0, 1); a.jv("ok_jv"); a.jmp("fail")
|
| 6441 |
+
a.label("ok_jv"); a.load(2, "m_a6"); a.store(2, "out6")
|
| 6442 |
+
|
| 6443 |
+
# ----- JNV: 1 + 1 = 2, no signed overflow -----
|
| 6444 |
+
a.load(0, "v1"); a.load(1, "v1"); a.add(0, 1); a.jnv("ok_jnv"); a.jmp("fail")
|
| 6445 |
+
a.label("ok_jnv"); a.load(2, "m_a7"); a.store(2, "out7")
|
| 6446 |
+
|
| 6447 |
+
a.jmp("end")
|
| 6448 |
+
a.label("fail")
|
| 6449 |
+
a.load(2, "v_fail"); a.store(2, "fail_addr"); a.halt()
|
| 6450 |
+
a.label("end")
|
| 6451 |
+
a.halt()
|
| 6452 |
+
|
| 6453 |
+
# Code runs to ~0xDF; data starts safely after that.
|
| 6454 |
+
a.org(0xE0)
|
| 6455 |
+
a.label("v0"); a.db(0)
|
| 6456 |
+
a.label("v1"); a.db(1)
|
| 6457 |
+
a.label("v3"); a.db(3)
|
| 6458 |
+
a.label("v5"); a.db(5)
|
| 6459 |
+
a.label("v127"); a.db(127)
|
| 6460 |
+
a.label("v255"); a.db(255)
|
| 6461 |
+
a.label("m_a0"); a.db(0xA0)
|
| 6462 |
+
a.label("m_a1"); a.db(0xA1)
|
| 6463 |
+
a.label("m_a2"); a.db(0xA2)
|
| 6464 |
+
a.label("m_a3"); a.db(0xA3)
|
| 6465 |
+
a.label("m_a4"); a.db(0xA4)
|
| 6466 |
+
a.label("m_a5"); a.db(0xA5)
|
| 6467 |
+
a.label("m_a6"); a.db(0xA6)
|
| 6468 |
+
a.label("m_a7"); a.db(0xA7)
|
| 6469 |
+
a.label("v_fail"); a.db(0xEE)
|
| 6470 |
+
a.label("out0"); a.db(0)
|
| 6471 |
+
a.label("out1"); a.db(0)
|
| 6472 |
+
a.label("out2"); a.db(0)
|
| 6473 |
+
a.label("out3"); a.db(0)
|
| 6474 |
+
a.label("out4"); a.db(0)
|
| 6475 |
+
a.label("out5"); a.db(0)
|
| 6476 |
+
a.label("out6"); a.db(0)
|
| 6477 |
+
a.label("out7"); a.db(0)
|
| 6478 |
+
a.label("fail_addr"); a.db(0)
|
| 6479 |
+
|
| 6480 |
+
mem = a.assemble()
|
| 6481 |
+
expected = {
|
| 6482 |
+
a.labels["out0"]: 0xA0,
|
| 6483 |
+
a.labels["out1"]: 0xA1,
|
| 6484 |
+
a.labels["out2"]: 0xA2,
|
| 6485 |
+
a.labels["out3"]: 0xA3,
|
| 6486 |
+
a.labels["out4"]: 0xA4,
|
| 6487 |
+
a.labels["out5"]: 0xA5,
|
| 6488 |
+
a.labels["out6"]: 0xA6,
|
| 6489 |
+
a.labels["out7"]: 0xA7,
|
| 6490 |
+
a.labels["fail_addr"]: 0, # must remain zero
|
| 6491 |
+
}
|
| 6492 |
+
return mem, expected, 200, "all 8 conditional jumps (JZ/JNZ/JC/JNC/JN/JP/JV/JNV)"
|
| 6493 |
+
|
| 6494 |
+
|
| 6495 |
+
def call_pushes_pc(mem_size: int = 256) -> ProgramResult:
|
| 6496 |
+
"""Verify CALL pushes the return address (next-instruction PC) onto the stack."""
|
| 6497 |
+
a = Asm(mem_size)
|
| 6498 |
+
|
| 6499 |
+
a.org(0)
|
| 6500 |
+
a.label("caller")
|
| 6501 |
+
a.load(0, "marker_val")
|
| 6502 |
+
a.store(0, "marker_addr") # write before CALL
|
| 6503 |
+
a.label("call_site")
|
| 6504 |
+
a.call("callee")
|
| 6505 |
+
# If CALL did not transfer control, this fallthrough store would write 0xDD:
|
| 6506 |
+
a.load(0, "noret_val")
|
| 6507 |
+
a.store(0, "noret_addr")
|
| 6508 |
+
a.halt()
|
| 6509 |
+
|
| 6510 |
+
a.label("callee")
|
| 6511 |
+
a.load(0, "callee_val")
|
| 6512 |
+
a.store(0, "callee_addr")
|
| 6513 |
+
a.halt()
|
| 6514 |
+
|
| 6515 |
+
a.org(0x40)
|
| 6516 |
+
a.label("marker_val"); a.db(0x11)
|
| 6517 |
+
a.label("marker_addr"); a.db(0)
|
| 6518 |
+
a.label("noret_val"); a.db(0xDD)
|
| 6519 |
+
a.label("noret_addr"); a.db(0)
|
| 6520 |
+
a.label("callee_val"); a.db(0x22)
|
| 6521 |
+
a.label("callee_addr"); a.db(0)
|
| 6522 |
+
|
| 6523 |
+
mem = a.assemble()
|
| 6524 |
+
addr_mask = mem_size - 1
|
| 6525 |
+
return_addr = a.labels["call_site"] + 4 # 4-byte CALL instruction
|
| 6526 |
+
expected = {
|
| 6527 |
+
a.labels["marker_addr"]: 0x11, # pre-CALL store ran
|
| 6528 |
+
a.labels["callee_addr"]: 0x22, # callee reached
|
| 6529 |
+
a.labels["noret_addr"]: 0, # fallthrough did not run
|
| 6530 |
+
(addr_mask - 2) & addr_mask: return_addr & 0xFF, # ret LSB on stack
|
| 6531 |
+
(addr_mask - 1) & addr_mask: (return_addr >> 8) & 0xFF, # ret MSB
|
| 6532 |
+
}
|
| 6533 |
+
return mem, expected, 30, "CALL pushes return PC onto stack"
|
| 6534 |
+
|
| 6535 |
+
|
| 6536 |
+
def bubble_sort_4(mem_size: int = 256) -> ProgramResult:
|
| 6537 |
+
"""Sort a 4-byte array using unrolled compare-swap (3 passes of 3 compares)."""
|
| 6538 |
+
a = Asm(mem_size)
|
| 6539 |
+
|
| 6540 |
+
addrs = ["a0", "a1", "a2", "a3"]
|
| 6541 |
+
|
| 6542 |
+
a.org(0)
|
| 6543 |
+
for _outer in range(3):
|
| 6544 |
+
for i in range(3):
|
| 6545 |
+
x, y = addrs[i], addrs[i + 1]
|
| 6546 |
+
# Load pair
|
| 6547 |
+
a.load(0, x) # R0 = A[i]
|
| 6548 |
+
a.load(1, y) # R1 = A[i+1]
|
| 6549 |
+
a.cmp(0, 1) # compare A[i] - A[i+1]
|
| 6550 |
+
# If A[i] <= A[i+1] (Z=1 or C=0 from sub-style cmp), skip swap
|
| 6551 |
+
# SUB sets carry when no borrow (a >= b). So:
|
| 6552 |
+
# a > b iff Z=0 and a >= b -> Z=0 and C=1 (the sub didn't borrow)
|
| 6553 |
+
# We want to swap when a > b. JNC (no carry / borrow) means a < b -> skip swap.
|
| 6554 |
+
# If a == b (Z=1) we also skip. So: jump-skip when JZ OR JNC.
|
| 6555 |
+
# Easier: compute (a > b) by checking C=1 AND Z=0. Use JZ to skip on equal,
|
| 6556 |
+
# then JNC to skip on a < b. Otherwise fall through to swap.
|
| 6557 |
+
skip_lbl = f"skip_{_outer}_{i}"
|
| 6558 |
+
a.jz(skip_lbl) # equal -> skip
|
| 6559 |
+
a.jnc(skip_lbl) # a < b (sub borrowed) -> skip
|
| 6560 |
+
# swap: store R0 -> y, R1 -> x
|
| 6561 |
+
a.store(1, x)
|
| 6562 |
+
a.store(0, y)
|
| 6563 |
+
a.label(skip_lbl)
|
| 6564 |
+
a.halt()
|
| 6565 |
+
|
| 6566 |
+
# Initial unsorted array; code runs to ~0xEC, so data starts at 0xF0.
|
| 6567 |
+
initial = [42, 7, 200, 19]
|
| 6568 |
+
a.org(0xF0)
|
| 6569 |
+
for name, val in zip(addrs, initial):
|
| 6570 |
+
a.label(name); a.db(val)
|
| 6571 |
+
|
| 6572 |
+
mem = a.assemble()
|
| 6573 |
+
sorted_vals = sorted(initial)
|
| 6574 |
+
expected = {a.labels[name]: v for name, v in zip(addrs, sorted_vals)}
|
| 6575 |
+
return mem, expected, 800, f"bubble sort {initial} -> {sorted_vals}"
|
| 6576 |
+
|
| 6577 |
+
|
| 6578 |
+
def cross_check_mul(mem_size: int = 256) -> ProgramResult:
|
| 6579 |
+
"""Cross-check the threshold MUL circuit against repeated ADD."""
|
| 6580 |
+
a = Asm(mem_size)
|
| 6581 |
+
A_VAL = 17
|
| 6582 |
+
B_VAL = 9
|
| 6583 |
+
expected_product = (A_VAL * B_VAL) & 0xFF
|
| 6584 |
+
|
| 6585 |
+
a.org(0)
|
| 6586 |
+
# --- direct multiply ---
|
| 6587 |
+
a.load(0, "A")
|
| 6588 |
+
a.load(1, "B")
|
| 6589 |
+
a.mul(0, 1)
|
| 6590 |
+
a.store(0, "out_mul")
|
| 6591 |
+
# --- repeated-add multiply ---
|
| 6592 |
+
a.load(0, "zero") # acc = 0
|
| 6593 |
+
a.load(1, "A") # addend
|
| 6594 |
+
a.load(2, "B") # counter
|
| 6595 |
+
a.load(3, "one") # 1
|
| 6596 |
+
a.label("rep_loop")
|
| 6597 |
+
a.add(0, 1) # acc += A
|
| 6598 |
+
a.sub(2, 3) # B--
|
| 6599 |
+
a.jnz("rep_loop")
|
| 6600 |
+
a.store(0, "out_add")
|
| 6601 |
+
a.halt()
|
| 6602 |
+
|
| 6603 |
+
a.org(0x80)
|
| 6604 |
+
a.label("A"); a.db(A_VAL)
|
| 6605 |
+
a.label("B"); a.db(B_VAL)
|
| 6606 |
+
a.label("zero"); a.db(0)
|
| 6607 |
+
a.label("one"); a.db(1)
|
| 6608 |
+
a.label("out_mul"); a.db(0)
|
| 6609 |
+
a.label("out_add"); a.db(0)
|
| 6610 |
+
|
| 6611 |
+
mem = a.assemble()
|
| 6612 |
+
expected = {
|
| 6613 |
+
a.labels["out_mul"]: expected_product,
|
| 6614 |
+
a.labels["out_add"]: expected_product,
|
| 6615 |
+
}
|
| 6616 |
+
return mem, expected, 80, f"MUL vs repeated ADD: {A_VAL} * {B_VAL} = {expected_product}"
|
| 6617 |
+
|
| 6618 |
+
|
| 6619 |
+
def div_via_repeated_sub(mem_size: int = 256) -> ProgramResult:
|
| 6620 |
+
"""Compute floor(A/B) and (A mod B) by repeated subtraction."""
|
| 6621 |
+
A_VAL = 100
|
| 6622 |
+
B_VAL = 7
|
| 6623 |
+
expected_q = A_VAL // B_VAL # 14
|
| 6624 |
+
expected_r = A_VAL % B_VAL # 2
|
| 6625 |
+
|
| 6626 |
+
a = Asm(mem_size)
|
| 6627 |
+
|
| 6628 |
+
a.org(0)
|
| 6629 |
+
# ---- Repeated-subtraction division ----
|
| 6630 |
+
a.load(0, "A") # R0 = A (will become remainder)
|
| 6631 |
+
a.load(1, "B") # R1 = B (divisor)
|
| 6632 |
+
a.load(2, "ZERO") # R2 = 0 (will become quotient)
|
| 6633 |
+
a.load(3, "ONE") # R3 = 1 (increment)
|
| 6634 |
+
|
| 6635 |
+
a.label("loop")
|
| 6636 |
+
a.cmp(0, 1) # CMP R0, R1; carry=1 (no-borrow) iff R0 >= R1
|
| 6637 |
+
a.jnc("done") # if R0 < R1 (carry=0), exit loop
|
| 6638 |
+
a.sub(0, 1) # R0 -= B
|
| 6639 |
+
a.add(2, 3) # quotient += 1
|
| 6640 |
+
a.jmp("loop")
|
| 6641 |
+
|
| 6642 |
+
a.label("done")
|
| 6643 |
+
a.store(2, "OUT_Q_RPT") # quotient via repeated sub
|
| 6644 |
+
a.store(0, "OUT_R_RPT") # remainder via repeated sub
|
| 6645 |
+
|
| 6646 |
+
# ---- Direct DIV opcode for cross-check ----
|
| 6647 |
+
a.load(0, "A")
|
| 6648 |
+
a.load(1, "B")
|
| 6649 |
+
a.dw(_enc(0x8, 0, 1, 0)) # DIV R0, R1 -> R0 = R0 / R1 (8-bit DIV)
|
| 6650 |
+
a.store(0, "OUT_Q_DIV")
|
| 6651 |
+
a.halt()
|
| 6652 |
+
|
| 6653 |
+
a.org(0x80)
|
| 6654 |
+
a.label("A"); a.db(A_VAL)
|
| 6655 |
+
a.label("B"); a.db(B_VAL)
|
| 6656 |
+
a.label("ZERO"); a.db(0)
|
| 6657 |
+
a.label("ONE"); a.db(1)
|
| 6658 |
+
a.label("OUT_Q_RPT"); a.db(0)
|
| 6659 |
+
a.label("OUT_R_RPT"); a.db(0)
|
| 6660 |
+
a.label("OUT_Q_DIV"); a.db(0)
|
| 6661 |
+
|
| 6662 |
+
mem = a.assemble()
|
| 6663 |
+
expected = {
|
| 6664 |
+
a.labels["OUT_Q_RPT"]: expected_q,
|
| 6665 |
+
a.labels["OUT_R_RPT"]: expected_r,
|
| 6666 |
+
a.labels["OUT_Q_DIV"]: expected_q,
|
| 6667 |
+
}
|
| 6668 |
+
return mem, expected, 4 * (A_VAL // B_VAL + 4) + 12, (
|
| 6669 |
+
f"{A_VAL} / {B_VAL}: quotient {expected_q} (repeated SUB) "
|
| 6670 |
+
f"matches DIV opcode result; remainder {expected_r}"
|
| 6671 |
+
)
|
| 6672 |
+
|
| 6673 |
+
|
| 6674 |
+
def bitwise_chain(mem_size: int = 256) -> ProgramResult:
|
| 6675 |
+
"""Run a chain of bitwise ops and verify each intermediate value."""
|
| 6676 |
+
A = 0xCC # 11001100
|
| 6677 |
+
B = 0xF0 # 11110000
|
| 6678 |
+
C = 0x0F # 00001111
|
| 6679 |
+
D = 0xAA # 10101010
|
| 6680 |
+
|
| 6681 |
+
s1 = A & B # 0xC0
|
| 6682 |
+
s2 = s1 | C # 0xCF
|
| 6683 |
+
s3 = s2 ^ D # 0x65
|
| 6684 |
+
s4 = (s3 << 1) & 0xFF # 0xCA
|
| 6685 |
+
s5 = s4 >> 1 # 0x65
|
| 6686 |
+
|
| 6687 |
+
a = Asm(mem_size)
|
| 6688 |
+
a.org(0)
|
| 6689 |
+
a.load(0, "A"); a.load(1, "B"); a.and_(0, 1); a.store(0, "S1")
|
| 6690 |
+
a.load(1, "C"); a.or_(0, 1); a.store(0, "S2")
|
| 6691 |
+
a.load(1, "D"); a.xor(0, 1); a.store(0, "S3")
|
| 6692 |
+
a.shl(0); a.store(0, "S4")
|
| 6693 |
+
a.shr(0); a.store(0, "S5")
|
| 6694 |
+
a.halt()
|
| 6695 |
+
|
| 6696 |
+
a.org(0x80)
|
| 6697 |
+
a.label("A"); a.db(A)
|
| 6698 |
+
a.label("B"); a.db(B)
|
| 6699 |
+
a.label("C"); a.db(C)
|
| 6700 |
+
a.label("D"); a.db(D)
|
| 6701 |
+
a.label("S1"); a.db(0)
|
| 6702 |
+
a.label("S2"); a.db(0)
|
| 6703 |
+
a.label("S3"); a.db(0)
|
| 6704 |
+
a.label("S4"); a.db(0)
|
| 6705 |
+
a.label("S5"); a.db(0)
|
| 6706 |
+
|
| 6707 |
+
mem = a.assemble()
|
| 6708 |
+
expected = {
|
| 6709 |
+
a.labels["S1"]: s1,
|
| 6710 |
+
a.labels["S2"]: s2,
|
| 6711 |
+
a.labels["S3"]: s3,
|
| 6712 |
+
a.labels["S4"]: s4,
|
| 6713 |
+
a.labels["S5"]: s5,
|
| 6714 |
+
}
|
| 6715 |
+
return mem, expected, 30, (
|
| 6716 |
+
f"bitwise chain AND/OR/XOR/SHL/SHR -> {s1:#x},{s2:#x},{s3:#x},{s4:#x},{s5:#x}"
|
| 6717 |
+
)
|
| 6718 |
+
|
| 6719 |
+
|
| 6720 |
+
def flags_policy(mem_size: int = 256) -> ProgramResult:
|
| 6721 |
+
"""Only ADD, SUB, MUL and CMP write FLAGS; every other instruction leaves them."""
|
| 6722 |
+
a = Asm(mem_size)
|
| 6723 |
+
|
| 6724 |
+
a.org(0)
|
| 6725 |
+
# ----- Probe 1: Z=1 survives AND (nonzero result) -----
|
| 6726 |
+
a.load(0, "v5"); a.load(1, "v5"); a.cmp(0, 1) # Z=1
|
| 6727 |
+
a.load(2, "v3"); a.load(3, "v1"); a.and_(2, 3) # R2 = 3 & 1 = 1
|
| 6728 |
+
a.jz("p1_ok")
|
| 6729 |
+
a.load(2, "m_fail"); a.store(2, "out1"); a.jmp("p2")
|
| 6730 |
+
a.label("p1_ok")
|
| 6731 |
+
a.load(2, "m_b1"); a.store(2, "out1")
|
| 6732 |
+
|
| 6733 |
+
# ----- Probe 2: Z=0 survives XOR (zero result) -----
|
| 6734 |
+
a.label("p2")
|
| 6735 |
+
a.load(0, "v5"); a.load(1, "v3"); a.cmp(0, 1) # Z=0
|
| 6736 |
+
a.xor(2, 2) # R2 = 0
|
| 6737 |
+
a.jz("p2_fail")
|
| 6738 |
+
a.load(2, "m_b2"); a.store(2, "out2"); a.jmp("p3")
|
| 6739 |
+
a.label("p2_fail")
|
| 6740 |
+
a.load(2, "m_fail"); a.store(2, "out2")
|
| 6741 |
+
|
| 6742 |
+
# ----- Probe 3: C=1 survives SHR -----
|
| 6743 |
+
a.label("p3")
|
| 6744 |
+
a.load(0, "v255"); a.load(1, "v1"); a.add(0, 1) # C=1
|
| 6745 |
+
a.load(2, "v3"); a.shr(2) # R2 = 1
|
| 6746 |
+
a.jc("p3_ok")
|
| 6747 |
+
a.load(2, "m_fail"); a.store(2, "out3"); a.jmp("done")
|
| 6748 |
+
a.label("p3_ok")
|
| 6749 |
+
a.load(2, "m_b3"); a.store(2, "out3")
|
| 6750 |
+
a.label("done")
|
| 6751 |
+
a.halt()
|
| 6752 |
+
|
| 6753 |
+
a.org(0xA0)
|
| 6754 |
+
a.label("v1"); a.db(1)
|
| 6755 |
+
a.label("v3"); a.db(3)
|
| 6756 |
+
a.label("v5"); a.db(5)
|
| 6757 |
+
a.label("v255"); a.db(255)
|
| 6758 |
+
a.label("m_b1"); a.db(0xB1)
|
| 6759 |
+
a.label("m_b2"); a.db(0xB2)
|
| 6760 |
+
a.label("m_b3"); a.db(0xB3)
|
| 6761 |
+
a.label("m_fail"); a.db(0xEE)
|
| 6762 |
+
a.label("out1"); a.db(0)
|
| 6763 |
+
a.label("out2"); a.db(0)
|
| 6764 |
+
a.label("out3"); a.db(0)
|
| 6765 |
+
|
| 6766 |
+
mem = a.assemble()
|
| 6767 |
+
expected = {
|
| 6768 |
+
a.labels["out1"]: 0xB1,
|
| 6769 |
+
a.labels["out2"]: 0xB2,
|
| 6770 |
+
a.labels["out3"]: 0xB3,
|
| 6771 |
+
}
|
| 6772 |
+
return mem, expected, 60, "flag policy: AND/XOR/SHR leave FLAGS unchanged"
|
| 6773 |
+
|
| 6774 |
+
|
| 6775 |
+
SUITE = [
|
| 6776 |
+
("fib", lambda mem_size: fib(11, mem_size)),
|
| 6777 |
+
("sum_n", lambda mem_size: sum_n(10, mem_size)),
|
| 6778 |
+
("self_mod_jmp", lambda mem_size: self_mod_jmp(mem_size)),
|
| 6779 |
+
("all_branches", lambda mem_size: all_branches(mem_size)),
|
| 6780 |
+
("call_pushes_pc", lambda mem_size: call_pushes_pc(mem_size)),
|
| 6781 |
+
("bubble_sort_4", lambda mem_size: bubble_sort_4(mem_size)),
|
| 6782 |
+
("cross_check_mul", lambda mem_size: cross_check_mul(mem_size)),
|
| 6783 |
+
("div_via_repeated_sub", lambda mem_size: div_via_repeated_sub(mem_size)),
|
| 6784 |
+
("bitwise_chain", lambda mem_size: bitwise_chain(mem_size)),
|
| 6785 |
+
("flags_policy", lambda mem_size: flags_policy(mem_size)),
|
| 6786 |
+
]
|
| 6787 |
+
|
| 6788 |
+
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # this file lives in tools/
|
| 6789 |
+
|
| 6790 |
+
|
| 6791 |
+
def load_tensors(path: Path):
|
| 6792 |
+
out = {}
|
| 6793 |
+
with safe_open(str(path), framework="pt") as f:
|
| 6794 |
+
for name in f.keys():
|
| 6795 |
+
out[name] = f.get_tensor(name).float()
|
| 6796 |
+
return out
|
| 6797 |
+
|
| 6798 |
+
|
| 6799 |
+
def run_program(cpu: GenericThresholdCPU, mem, max_cycles: int):
|
| 6800 |
+
addr_mask = (1 << cpu.addr_bits) - 1
|
| 6801 |
+
state = {
|
| 6802 |
+
"pc": 0,
|
| 6803 |
+
"regs": [0] * 4,
|
| 6804 |
+
"flags": [0] * 4,
|
| 6805 |
+
"mem": list(mem),
|
| 6806 |
+
"halted": False,
|
| 6807 |
+
"sp": addr_mask,
|
| 6808 |
+
}
|
| 6809 |
+
t0 = time.perf_counter()
|
| 6810 |
+
final, cycles = cpu.run(state, max_cycles=max_cycles)
|
| 6811 |
+
return final, cycles, time.perf_counter() - t0
|
| 6812 |
+
|
| 6813 |
+
|
| 6814 |
+
def check_expected(final, expected: dict) -> tuple[bool, list[str]]:
|
| 6815 |
+
failures = []
|
| 6816 |
+
for addr, want in expected.items():
|
| 6817 |
+
got = final["mem"][addr]
|
| 6818 |
+
if got != want:
|
| 6819 |
+
failures.append(f"M[0x{addr:04X}] = {got} (expected {want})")
|
| 6820 |
+
return (len(failures) == 0), failures
|
| 6821 |
+
|
| 6822 |
+
|
| 6823 |
+
def main_cpu() -> int:
|
| 6824 |
+
parser = argparse.ArgumentParser(description="Run the CPU program suite")
|
| 6825 |
+
parser.add_argument(
|
| 6826 |
+
"--model", type=str,
|
| 6827 |
+
default=os.path.join(REPO, "variants", "neural_computer8_small.safetensors"),
|
| 6828 |
+
help="Path to .safetensors variant",
|
| 6829 |
+
)
|
| 6830 |
+
parser.add_argument(
|
| 6831 |
+
"--only", type=str, default="",
|
| 6832 |
+
help="Comma-separated subset of program names to run",
|
| 6833 |
+
)
|
| 6834 |
+
args = parser.parse_args()
|
| 6835 |
+
|
| 6836 |
+
print(f"Loading {args.model}")
|
| 6837 |
+
tensors = load_tensors(Path(args.model))
|
| 6838 |
+
manifest = get_manifest(tensors)
|
| 6839 |
+
print(f"Manifest: data={manifest['data_bits']}-bit, addr={manifest['addr_bits']}-bit, "
|
| 6840 |
+
f"mem={manifest['memory_bytes']}B")
|
| 6841 |
+
|
| 6842 |
+
if manifest["memory_bytes"] < 256:
|
| 6843 |
+
print(f"ERROR: variant has {manifest['memory_bytes']}B memory; "
|
| 6844 |
+
f"the suite needs at least 256B (scratchpad).")
|
| 6845 |
+
return 2
|
| 6846 |
+
|
| 6847 |
+
cpu = GenericThresholdCPU(tensors)
|
| 6848 |
+
only = set(s.strip() for s in args.only.split(",") if s.strip())
|
| 6849 |
+
|
| 6850 |
+
print()
|
| 6851 |
+
print("=" * 80)
|
| 6852 |
+
print(f" CPU PROGRAM SUITE ({manifest['memory_bytes']}B mem, {manifest['data_bits']}-bit ALU)")
|
| 6853 |
+
print("=" * 80)
|
| 6854 |
+
|
| 6855 |
+
pass_count = 0
|
| 6856 |
+
fail_count = 0
|
| 6857 |
+
skip_count = 0
|
| 6858 |
+
|
| 6859 |
+
for name, builder in SUITE:
|
| 6860 |
+
if only and name not in only:
|
| 6861 |
+
skip_count += 1
|
| 6862 |
+
continue
|
| 6863 |
+
try:
|
| 6864 |
+
mem, expected, max_cycles, desc = builder(manifest["memory_bytes"])
|
| 6865 |
+
except Exception as e:
|
| 6866 |
+
print(f" {name:18} BUILD ERROR: {e}")
|
| 6867 |
+
fail_count += 1
|
| 6868 |
+
continue
|
| 6869 |
+
|
| 6870 |
+
if len(mem) != manifest["memory_bytes"]:
|
| 6871 |
+
print(f" {name:18} SKIP (program built {len(mem)}B, "
|
| 6872 |
+
f"variant has {manifest['memory_bytes']}B)")
|
| 6873 |
+
skip_count += 1
|
| 6874 |
+
continue
|
| 6875 |
+
|
| 6876 |
+
final, cycles, elapsed = run_program(cpu, mem, max_cycles)
|
| 6877 |
+
ok, failures = check_expected(final, expected)
|
| 6878 |
+
if ok and not final["halted"]:
|
| 6879 |
+
ok = False
|
| 6880 |
+
failures.append(f"did not HALT within {max_cycles} cycles")
|
| 6881 |
+
|
| 6882 |
+
status = "PASS" if ok else "FAIL"
|
| 6883 |
+
if ok:
|
| 6884 |
+
pass_count += 1
|
| 6885 |
+
else:
|
| 6886 |
+
fail_count += 1
|
| 6887 |
+
print(f" {name:18} {status} ({cycles:>3} cyc, {elapsed:>5.2f}s) {desc}")
|
| 6888 |
+
if not ok:
|
| 6889 |
+
for f in failures[:6]:
|
| 6890 |
+
print(f" - {f}")
|
| 6891 |
+
|
| 6892 |
+
print()
|
| 6893 |
+
print("=" * 80)
|
| 6894 |
+
total = pass_count + fail_count
|
| 6895 |
+
print(f" PASS: {pass_count}/{total} FAIL: {fail_count}/{total} SKIP: {skip_count}")
|
| 6896 |
+
return 0 if fail_count == 0 else 1
|
| 6897 |
+
|
| 6898 |
+
|
| 6899 |
+
COMMANDS = {'eval': main, 'variants': main_all, 'cpu': main_cpu}
|
| 6900 |
+
|
| 6901 |
+
|
| 6902 |
+
if __name__ == "__main__":
|
| 6903 |
+
if len(sys.argv) < 2 or sys.argv[1] not in COMMANDS:
|
| 6904 |
+
sys.exit(COMMANDS['eval']())
|
| 6905 |
+
cmd = sys.argv.pop(1)
|
| 6906 |
+
sys.argv[0] += ' ' + cmd
|
| 6907 |
+
sys.exit(COMMANDS[cmd]())
|
src/eval_all.py
DELETED
|
@@ -1,912 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Unified evaluation harness for any threshold-computer variant.
|
| 3 |
-
|
| 4 |
-
Drops the `--cpu-test` smoke test (which was hardcoded to 16-bit/64KB) and
|
| 5 |
-
adds variant-aware sweep modes. The same harness handles every (data_bits,
|
| 6 |
-
addr_bits) configuration: it reads the manifest from each safetensors file,
|
| 7 |
-
runs the BatchedFitnessEvaluator at the right device, and reports per-file
|
| 8 |
-
plus per-category results.
|
| 9 |
-
|
| 10 |
-
Usage:
|
| 11 |
-
python eval_all.py path/to/file.safetensors # one file
|
| 12 |
-
python eval_all.py variants/ # every .safetensors in dir
|
| 13 |
-
python eval_all.py --device cpu variants/ # CPU only (default)
|
| 14 |
-
python eval_all.py --pop_size 32 variants/ # batched pop eval
|
| 15 |
-
python eval_all.py --debug path/to/file.safetensors # per-circuit detail
|
| 16 |
-
python eval_all.py --cpu-program PATH # also run an assembled program
|
| 17 |
-
# through the threshold CPU
|
| 18 |
-
# sized to the file's manifest
|
| 19 |
-
|
| 20 |
-
Exit code:
|
| 21 |
-
0 if all files PASS (fitness >= 0.9999)
|
| 22 |
-
N where N is the number of FAILing files
|
| 23 |
-
"""
|
| 24 |
-
|
| 25 |
-
from __future__ import annotations
|
| 26 |
-
|
| 27 |
-
import argparse
|
| 28 |
-
import json
|
| 29 |
-
import os
|
| 30 |
-
import sys
|
| 31 |
-
import time
|
| 32 |
-
from pathlib import Path
|
| 33 |
-
from typing import Any, Dict, List, Optional, Tuple
|
| 34 |
-
|
| 35 |
-
import torch
|
| 36 |
-
from safetensors import safe_open
|
| 37 |
-
|
| 38 |
-
# Reuse eval.py's evaluator (variant-aware)
|
| 39 |
-
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
| 40 |
-
from eval import (
|
| 41 |
-
BatchedFitnessEvaluator,
|
| 42 |
-
create_population,
|
| 43 |
-
load_model,
|
| 44 |
-
get_manifest,
|
| 45 |
-
heaviside,
|
| 46 |
-
int_to_bits,
|
| 47 |
-
bits_to_int,
|
| 48 |
-
bits_msb_to_lsb,
|
| 49 |
-
)
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
# ---------------------------------------------------------------------------
|
| 53 |
-
# Variant-aware threshold ALU + CPU
|
| 54 |
-
# ---------------------------------------------------------------------------
|
| 55 |
-
|
| 56 |
-
class GenericThresholdALU:
|
| 57 |
-
"""Variant-aware threshold ALU walking the gates of a loaded variant.
|
| 58 |
-
|
| 59 |
-
8-bit primitives: add8/sub8/mul8/div8, packed bitwise and8/or8/xor8,
|
| 60 |
-
shl8/shr8, and the bit-cascade comparators via cmp8. Width-generic
|
| 61 |
-
add_n/sub_n/mul_n/cmp_n cover the 16- and 32-bit circuit families.
|
| 62 |
-
"""
|
| 63 |
-
|
| 64 |
-
def __init__(self, tensors: Dict[str, torch.Tensor], data_bits: int):
|
| 65 |
-
self.T = tensors
|
| 66 |
-
self.data_bits = data_bits
|
| 67 |
-
|
| 68 |
-
def _g(self, name, inputs):
|
| 69 |
-
w = self.T[name + ".weight"].view(-1)
|
| 70 |
-
b = self.T[name + ".bias"].view(-1)
|
| 71 |
-
return int(heaviside((torch.tensor(inputs, dtype=torch.float32) * w).sum() + b).item())
|
| 72 |
-
|
| 73 |
-
def _xor_or_nand(self, prefix, inputs):
|
| 74 |
-
a, b_ = inputs
|
| 75 |
-
h_or = self._g(f"{prefix}.layer1.or", [a, b_])
|
| 76 |
-
h_nand = self._g(f"{prefix}.layer1.nand", [a, b_])
|
| 77 |
-
return self._g(f"{prefix}.layer2", [h_or, h_nand])
|
| 78 |
-
|
| 79 |
-
def _fa(self, prefix, a, b, cin):
|
| 80 |
-
s1 = self._xor_or_nand(f"{prefix}.ha1.sum", [a, b])
|
| 81 |
-
c1 = self._g(f"{prefix}.ha1.carry", [a, b])
|
| 82 |
-
s2 = self._xor_or_nand(f"{prefix}.ha2.sum", [s1, cin])
|
| 83 |
-
c2 = self._g(f"{prefix}.ha2.carry", [s1, cin])
|
| 84 |
-
cout = self._g(f"{prefix}.carry_or", [c1, c2])
|
| 85 |
-
return s2, cout
|
| 86 |
-
|
| 87 |
-
def add8(self, a, b):
|
| 88 |
-
a_lsb = list(reversed(int_to_bits(a, 8)))
|
| 89 |
-
b_lsb = list(reversed(int_to_bits(b, 8)))
|
| 90 |
-
carry = 0
|
| 91 |
-
s_lsb = []
|
| 92 |
-
for i in range(8):
|
| 93 |
-
s, carry = self._fa(f"arithmetic.ripplecarry8bit.fa{i}", a_lsb[i], b_lsb[i], carry)
|
| 94 |
-
s_lsb.append(s)
|
| 95 |
-
return bits_to_int(list(reversed(s_lsb))), carry
|
| 96 |
-
|
| 97 |
-
def sub8(self, a, b):
|
| 98 |
-
a_lsb = list(reversed(int_to_bits(a, 8)))
|
| 99 |
-
b_lsb = list(reversed(int_to_bits(b, 8)))
|
| 100 |
-
carry = 1
|
| 101 |
-
d_lsb = []
|
| 102 |
-
for i in range(8):
|
| 103 |
-
notb = self._g(f"arithmetic.sub8bit.notb{i}", [b_lsb[i]])
|
| 104 |
-
x1 = self._xor_or_nand(f"arithmetic.sub8bit.fa{i}.xor1", [a_lsb[i], notb])
|
| 105 |
-
x2 = self._xor_or_nand(f"arithmetic.sub8bit.fa{i}.xor2", [x1, carry])
|
| 106 |
-
and1 = self._g(f"arithmetic.sub8bit.fa{i}.and1", [a_lsb[i], notb])
|
| 107 |
-
and2 = self._g(f"arithmetic.sub8bit.fa{i}.and2", [x1, carry])
|
| 108 |
-
carry = self._g(f"arithmetic.sub8bit.fa{i}.or_carry", [and1, and2])
|
| 109 |
-
d_lsb.append(x2)
|
| 110 |
-
return bits_to_int(list(reversed(d_lsb))), carry
|
| 111 |
-
|
| 112 |
-
_CMP_KIND = {"greaterthan": "gt", "lessthan": "lt", "eq": "eq", "equality": "eq",
|
| 113 |
-
"greaterorequal": "ge", "lessorequal": "le"}
|
| 114 |
-
|
| 115 |
-
def _cmp_bit_cascade(self, cmp_prefix: str, finals: Dict[str, str],
|
| 116 |
-
a_bits_msb: List[int], b_bits_msb: List[int], kind: str) -> int:
|
| 117 |
-
"""Walk an add_bit_cascade_compare structure (bit 0 is the MSB)."""
|
| 118 |
-
bits = len(a_bits_msb)
|
| 119 |
-
bit_gt, bit_lt, bit_eq = [], [], []
|
| 120 |
-
for i in range(bits):
|
| 121 |
-
ab = [a_bits_msb[i], b_bits_msb[i]]
|
| 122 |
-
bit_gt.append(self._g(f"{cmp_prefix}.bit{i}.gt", ab))
|
| 123 |
-
bit_lt.append(self._g(f"{cmp_prefix}.bit{i}.lt", ab))
|
| 124 |
-
eq_and = self._g(f"{cmp_prefix}.bit{i}.eq.layer1.and", ab)
|
| 125 |
-
eq_nor = self._g(f"{cmp_prefix}.bit{i}.eq.layer1.nor", ab)
|
| 126 |
-
bit_eq.append(self._g(f"{cmp_prefix}.bit{i}.eq", [eq_and, eq_nor]))
|
| 127 |
-
cas_gt, cas_lt = [bit_gt[0]], [bit_lt[0]]
|
| 128 |
-
for i in range(1, bits):
|
| 129 |
-
eq_pref = self._g(f"{cmp_prefix}.cascade.eq_prefix.bit{i}", bit_eq[:i])
|
| 130 |
-
cas_gt.append(self._g(f"{cmp_prefix}.cascade.gt.bit{i}", [eq_pref, bit_gt[i]]))
|
| 131 |
-
cas_lt.append(self._g(f"{cmp_prefix}.cascade.lt.bit{i}", [eq_pref, bit_lt[i]]))
|
| 132 |
-
if kind == "gt":
|
| 133 |
-
return self._g(finals["gt"], cas_gt)
|
| 134 |
-
if kind == "lt":
|
| 135 |
-
return self._g(finals["lt"], cas_lt)
|
| 136 |
-
if kind == "eq":
|
| 137 |
-
return self._g(finals["eq"], bit_eq)
|
| 138 |
-
if kind == "ge":
|
| 139 |
-
lt = self._g(finals["lt"], cas_lt)
|
| 140 |
-
not_lt = self._g(finals["ge"] + ".not_lt", [lt])
|
| 141 |
-
return self._g(finals["ge"], [not_lt])
|
| 142 |
-
if kind == "le":
|
| 143 |
-
gt = self._g(finals["gt"], cas_gt)
|
| 144 |
-
not_gt = self._g(finals["le"] + ".not_gt", [gt])
|
| 145 |
-
return self._g(finals["le"], [not_gt])
|
| 146 |
-
raise ValueError(kind)
|
| 147 |
-
|
| 148 |
-
def cmp8(self, a, b, kind):
|
| 149 |
-
return self.cmp_n(a, b, kind, 8)
|
| 150 |
-
|
| 151 |
-
def mul8(self, a, b):
|
| 152 |
-
ab = int_to_bits(a, 8)
|
| 153 |
-
bb = int_to_bits(b, 8)
|
| 154 |
-
result = 0
|
| 155 |
-
for j in range(8):
|
| 156 |
-
if bb[j] == 0:
|
| 157 |
-
continue
|
| 158 |
-
row = 0
|
| 159 |
-
for i in range(8):
|
| 160 |
-
pp = self._g(f"alu.alu8bit.mul.pp.a{i}b{j}", [ab[i], bb[j]])
|
| 161 |
-
row |= (pp << (7 - i))
|
| 162 |
-
shift = 7 - j
|
| 163 |
-
result, _ = self.add8(result & 0xFF, (row << shift) & 0xFF)
|
| 164 |
-
return result & 0xFF
|
| 165 |
-
|
| 166 |
-
def _packed_pair_op(self, name: str, a: int, b: int) -> int:
|
| 167 |
-
"""8 parallel 2-input gates stored packed: weight [16], bias [8]."""
|
| 168 |
-
a_bits = int_to_bits(a, 8)
|
| 169 |
-
b_bits = int_to_bits(b, 8)
|
| 170 |
-
w = self.T[f"{name}.weight"].view(-1)
|
| 171 |
-
bias = self.T[f"{name}.bias"].view(-1)
|
| 172 |
-
out = []
|
| 173 |
-
for bit in range(8):
|
| 174 |
-
inp = torch.tensor([float(a_bits[bit]), float(b_bits[bit])])
|
| 175 |
-
out.append(int(heaviside((inp * w[bit * 2:bit * 2 + 2]).sum() + bias[bit]).item()))
|
| 176 |
-
return bits_to_int(out)
|
| 177 |
-
|
| 178 |
-
def and8(self, a, b):
|
| 179 |
-
return self._packed_pair_op("alu.alu8bit.and", a, b)
|
| 180 |
-
|
| 181 |
-
def or8(self, a, b):
|
| 182 |
-
return self._packed_pair_op("alu.alu8bit.or", a, b)
|
| 183 |
-
|
| 184 |
-
def xor8(self, a, b):
|
| 185 |
-
a_bits = int_to_bits(a, 8)
|
| 186 |
-
b_bits = int_to_bits(b, 8)
|
| 187 |
-
w_or = self.T["alu.alu8bit.xor.layer1.or.weight"].view(-1)
|
| 188 |
-
b_or = self.T["alu.alu8bit.xor.layer1.or.bias"].view(-1)
|
| 189 |
-
w_nand = self.T["alu.alu8bit.xor.layer1.nand.weight"].view(-1)
|
| 190 |
-
b_nand = self.T["alu.alu8bit.xor.layer1.nand.bias"].view(-1)
|
| 191 |
-
w2 = self.T["alu.alu8bit.xor.layer2.weight"].view(-1)
|
| 192 |
-
b2 = self.T["alu.alu8bit.xor.layer2.bias"].view(-1)
|
| 193 |
-
out = []
|
| 194 |
-
for bit in range(8):
|
| 195 |
-
inp = torch.tensor([float(a_bits[bit]), float(b_bits[bit])])
|
| 196 |
-
h_or = heaviside((inp * w_or[bit * 2:bit * 2 + 2]).sum() + b_or[bit])
|
| 197 |
-
h_nand = heaviside((inp * w_nand[bit * 2:bit * 2 + 2]).sum() + b_nand[bit])
|
| 198 |
-
hidden = torch.stack([h_or, h_nand])
|
| 199 |
-
out.append(int(heaviside((hidden * w2[bit * 2:bit * 2 + 2]).sum() + b2[bit]).item()))
|
| 200 |
-
return bits_to_int(out)
|
| 201 |
-
|
| 202 |
-
def shl8(self, a):
|
| 203 |
-
a_bits = int_to_bits(a, 8)
|
| 204 |
-
out = []
|
| 205 |
-
for bit in range(8):
|
| 206 |
-
src = float(a_bits[bit + 1]) if bit < 7 else 0.0
|
| 207 |
-
out.append(self._g(f"alu.alu8bit.shl.bit{bit}", [src]))
|
| 208 |
-
return bits_to_int(out)
|
| 209 |
-
|
| 210 |
-
def shr8(self, a):
|
| 211 |
-
a_bits = int_to_bits(a, 8)
|
| 212 |
-
out = []
|
| 213 |
-
for bit in range(8):
|
| 214 |
-
src = float(a_bits[bit - 1]) if bit > 0 else 0.0
|
| 215 |
-
out.append(self._g(f"alu.alu8bit.shr.bit{bit}", [src]))
|
| 216 |
-
return bits_to_int(out)
|
| 217 |
-
|
| 218 |
-
def div8(self, a, b):
|
| 219 |
-
"""8-bit restoring division through the per-stage bit-cascade GE
|
| 220 |
-
comparators and the sub8 subtractor. Returns (quotient, remainder);
|
| 221 |
-
divide-by-zero yields (0xFF, a)."""
|
| 222 |
-
if b == 0:
|
| 223 |
-
return 0xFF, a
|
| 224 |
-
a_bits = int_to_bits(a, 8)
|
| 225 |
-
div_bits = int_to_bits(b, 8)
|
| 226 |
-
quotient = 0
|
| 227 |
-
remainder = 0
|
| 228 |
-
for stage in range(8):
|
| 229 |
-
remainder = ((remainder << 1) | a_bits[stage]) & 0xFF
|
| 230 |
-
prefix = f"alu.alu8bit.div.stage{stage}"
|
| 231 |
-
finals = {
|
| 232 |
-
"gt": f"{prefix}.cmp_bc.gt",
|
| 233 |
-
"lt": f"{prefix}.cmp_bc.lt",
|
| 234 |
-
"eq": f"{prefix}.cmp_bc.eq",
|
| 235 |
-
"ge": f"{prefix}.cmp",
|
| 236 |
-
"le": f"{prefix}.cmp_bc.le",
|
| 237 |
-
}
|
| 238 |
-
ge = self._cmp_bit_cascade(f"{prefix}.cmp_bc", finals,
|
| 239 |
-
int_to_bits(remainder, 8), div_bits, "ge")
|
| 240 |
-
if ge:
|
| 241 |
-
remainder, _ = self.sub8(remainder, b)
|
| 242 |
-
quotient = (quotient << 1) | 1
|
| 243 |
-
else:
|
| 244 |
-
quotient = quotient << 1
|
| 245 |
-
return quotient & 0xFF, remainder & 0xFF
|
| 246 |
-
|
| 247 |
-
# ----- N-bit primitives (for 16-bit and 32-bit variants) ----------------
|
| 248 |
-
|
| 249 |
-
def add_n(self, a: int, b: int, bits: int):
|
| 250 |
-
"""Width-generic ripple-carry add via arithmetic.ripplecarry{N}bit."""
|
| 251 |
-
prefix = f"arithmetic.ripplecarry{bits}bit"
|
| 252 |
-
a_lsb = list(reversed(int_to_bits(a, bits)))
|
| 253 |
-
b_lsb = list(reversed(int_to_bits(b, bits)))
|
| 254 |
-
carry = 0
|
| 255 |
-
s_lsb = []
|
| 256 |
-
for i in range(bits):
|
| 257 |
-
s, carry = self._fa(f"{prefix}.fa{i}", a_lsb[i], b_lsb[i], carry)
|
| 258 |
-
s_lsb.append(s)
|
| 259 |
-
return bits_to_int(list(reversed(s_lsb))), carry
|
| 260 |
-
|
| 261 |
-
def sub_n(self, a: int, b: int, bits: int):
|
| 262 |
-
"""N-bit two's-complement subtract via arithmetic.sub{N}bit (N >= 16).
|
| 263 |
-
|
| 264 |
-
Structure (per build.add_sub_nbits): N NOT gates + N standard full adders.
|
| 265 |
-
"""
|
| 266 |
-
prefix = f"arithmetic.sub{bits}bit"
|
| 267 |
-
a_lsb = list(reversed(int_to_bits(a, bits)))
|
| 268 |
-
b_lsb = list(reversed(int_to_bits(b, bits)))
|
| 269 |
-
# NOT each B bit
|
| 270 |
-
notb = [self._g(f"{prefix}.not_b.bit{i}", [b_lsb[i]]) for i in range(bits)]
|
| 271 |
-
carry = 1 # carry-in = 1 for two's-complement
|
| 272 |
-
d_lsb = []
|
| 273 |
-
for i in range(bits):
|
| 274 |
-
s, carry = self._fa(f"{prefix}.fa{i}", a_lsb[i], notb[i], carry)
|
| 275 |
-
d_lsb.append(s)
|
| 276 |
-
return bits_to_int(list(reversed(d_lsb))), carry
|
| 277 |
-
|
| 278 |
-
def cmp_n(self, a: int, b: int, kind: str, bits: int):
|
| 279 |
-
"""N-bit unsigned comparison via the bit-cascade comparator family
|
| 280 |
-
(arithmetic.cmp{N}bit.* per build.add_bit_cascade_compare)."""
|
| 281 |
-
short = self._CMP_KIND[kind]
|
| 282 |
-
finals = {
|
| 283 |
-
"gt": f"arithmetic.greaterthan{bits}bit",
|
| 284 |
-
"lt": f"arithmetic.lessthan{bits}bit",
|
| 285 |
-
"eq": f"arithmetic.equality{bits}bit",
|
| 286 |
-
"ge": f"arithmetic.greaterorequal{bits}bit",
|
| 287 |
-
"le": f"arithmetic.lessorequal{bits}bit",
|
| 288 |
-
}
|
| 289 |
-
return self._cmp_bit_cascade(f"arithmetic.cmp{bits}bit", finals,
|
| 290 |
-
int_to_bits(a, bits), int_to_bits(b, bits), short)
|
| 291 |
-
|
| 292 |
-
def mul_n(self, a: int, b: int, bits: int):
|
| 293 |
-
"""N-bit shift-add multiply (low N bits only)."""
|
| 294 |
-
ab = int_to_bits(a, bits)
|
| 295 |
-
bb = int_to_bits(b, bits)
|
| 296 |
-
mask = (1 << bits) - 1
|
| 297 |
-
result = 0
|
| 298 |
-
for j in range(bits):
|
| 299 |
-
if bb[j] == 0:
|
| 300 |
-
continue
|
| 301 |
-
row = 0
|
| 302 |
-
for i in range(bits):
|
| 303 |
-
pp = self._g(f"alu.alu{bits}bit.mul.pp.a{i}b{j}", [ab[i], bb[j]])
|
| 304 |
-
row |= (pp << (bits - 1 - i))
|
| 305 |
-
shift = (bits - 1) - j
|
| 306 |
-
result, _ = self.add_n(result & mask, (row << shift) & mask, bits)
|
| 307 |
-
return result & mask
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
class GenericThresholdCPU:
|
| 311 |
-
"""Variant-aware CPU runtime. Sized from the variant's manifest."""
|
| 312 |
-
|
| 313 |
-
def __init__(self, tensors: Dict[str, torch.Tensor]):
|
| 314 |
-
self.T = tensors
|
| 315 |
-
m = get_manifest(tensors)
|
| 316 |
-
self.data_bits = m["data_bits"]
|
| 317 |
-
self.addr_bits = m["addr_bits"]
|
| 318 |
-
self.mem_bytes = m["memory_bytes"]
|
| 319 |
-
# 8-bit CPU primitives (ripplecarry8bit, sub8bit, alu.alu8bit.*, memory.*,
|
| 320 |
-
# control.*) are present in every variant regardless of manifest data_bits.
|
| 321 |
-
# Wider data widths simply add additional standalone ALU primitives.
|
| 322 |
-
if self.mem_bytes == 0:
|
| 323 |
-
raise NotImplementedError(
|
| 324 |
-
"Pure-ALU variants have no memory; cannot run CPU programs"
|
| 325 |
-
)
|
| 326 |
-
self.alu = GenericThresholdALU(tensors, 8)
|
| 327 |
-
|
| 328 |
-
def _addr_decode(self, addr):
|
| 329 |
-
bits = torch.tensor(int_to_bits(addr, self.addr_bits), dtype=torch.float32)
|
| 330 |
-
w = self.T["memory.addr_decode.weight"]
|
| 331 |
-
b = self.T["memory.addr_decode.bias"]
|
| 332 |
-
return heaviside((w * bits).sum(dim=1) + b)
|
| 333 |
-
|
| 334 |
-
def mem_read(self, mem, addr):
|
| 335 |
-
sel = self._addr_decode(addr)
|
| 336 |
-
mem_bits = torch.tensor(
|
| 337 |
-
[int_to_bits(byte, 8) for byte in mem], dtype=torch.float32
|
| 338 |
-
)
|
| 339 |
-
and_w = self.T["memory.read.and.weight"]
|
| 340 |
-
and_b = self.T["memory.read.and.bias"]
|
| 341 |
-
or_w = self.T["memory.read.or.weight"]
|
| 342 |
-
or_b = self.T["memory.read.or.bias"]
|
| 343 |
-
out = []
|
| 344 |
-
for bit in range(8):
|
| 345 |
-
inp = torch.stack([mem_bits[:, bit], sel], dim=1)
|
| 346 |
-
and_out = heaviside((inp * and_w[bit]).sum(dim=1) + and_b[bit])
|
| 347 |
-
out.append(int(heaviside((and_out * or_w[bit]).sum() + or_b[bit]).item()))
|
| 348 |
-
return bits_to_int(out)
|
| 349 |
-
|
| 350 |
-
def mem_write(self, mem, addr, value):
|
| 351 |
-
sel = self._addr_decode(addr)
|
| 352 |
-
data_bits = torch.tensor(int_to_bits(value, 8), dtype=torch.float32)
|
| 353 |
-
mem_bits = torch.tensor(
|
| 354 |
-
[int_to_bits(byte, 8) for byte in mem], dtype=torch.float32
|
| 355 |
-
)
|
| 356 |
-
sel_w = self.T["memory.write.sel.weight"]
|
| 357 |
-
sel_b = self.T["memory.write.sel.bias"]
|
| 358 |
-
nsel_w = self.T["memory.write.nsel.weight"].squeeze(1)
|
| 359 |
-
nsel_b = self.T["memory.write.nsel.bias"]
|
| 360 |
-
and_old_w = self.T["memory.write.and_old.weight"]
|
| 361 |
-
and_old_b = self.T["memory.write.and_old.bias"]
|
| 362 |
-
and_new_w = self.T["memory.write.and_new.weight"]
|
| 363 |
-
and_new_b = self.T["memory.write.and_new.bias"]
|
| 364 |
-
or_w = self.T["memory.write.or.weight"]
|
| 365 |
-
or_b = self.T["memory.write.or.bias"]
|
| 366 |
-
we = torch.ones_like(sel)
|
| 367 |
-
sel_inp = torch.stack([sel, we], dim=1)
|
| 368 |
-
write_sel = heaviside((sel_inp * sel_w).sum(dim=1) + sel_b)
|
| 369 |
-
nsel = heaviside(write_sel * nsel_w + nsel_b)
|
| 370 |
-
for bit in range(8):
|
| 371 |
-
old = mem_bits[:, bit]
|
| 372 |
-
data_bit = data_bits[bit].expand(self.mem_bytes)
|
| 373 |
-
inp_old = torch.stack([old, nsel], dim=1)
|
| 374 |
-
inp_new = torch.stack([data_bit, write_sel], dim=1)
|
| 375 |
-
and_old = heaviside((inp_old * and_old_w[:, bit]).sum(dim=1) + and_old_b[:, bit])
|
| 376 |
-
and_new = heaviside((inp_new * and_new_w[:, bit]).sum(dim=1) + and_new_b[:, bit])
|
| 377 |
-
or_inp = torch.stack([and_old, and_new], dim=1)
|
| 378 |
-
new_bit = heaviside((or_inp * or_w[:, bit]).sum(dim=1) + or_b[:, bit])
|
| 379 |
-
mem_bits[:, bit] = new_bit
|
| 380 |
-
return [bits_to_int([int(b) for b in mem_bits[i].tolist()]) for i in range(self.mem_bytes)]
|
| 381 |
-
|
| 382 |
-
# Conditions 0..7 map to (mux circuit, flag index into [Z, N, C, V]).
|
| 383 |
-
# Odd conditions are the negated forms; the mux select is the complemented
|
| 384 |
-
# flag (the circuits are plain per-bit muxes -- see control.* .inputs,
|
| 385 |
-
# which name the select $not_zero / $not_carry / ... for the odd forms).
|
| 386 |
-
_JCC = [("control.jz", 0), ("control.jnz", 0), ("control.jc", 2), ("control.jnc", 2),
|
| 387 |
-
("control.jn", 1), ("control.jp", 1), ("control.jv", 3), ("control.jnv", 3)]
|
| 388 |
-
|
| 389 |
-
def _jcc_pc(self, circuit: str, pc: int, target: int, sel: int) -> int:
|
| 390 |
-
"""Per-bit 2:1 mux over the PC: sel ? target : pc, addr_bits wide."""
|
| 391 |
-
pc_bits = int_to_bits(pc, self.addr_bits)
|
| 392 |
-
t_bits = int_to_bits(target, self.addr_bits)
|
| 393 |
-
out = []
|
| 394 |
-
for bit in range(self.addr_bits):
|
| 395 |
-
bp = f"{circuit}.bit{bit}"
|
| 396 |
-
not_sel = self.alu._g(f"{bp}.not_sel", [float(sel)])
|
| 397 |
-
and_a = self.alu._g(f"{bp}.and_a", [float(pc_bits[bit]), not_sel])
|
| 398 |
-
and_b = self.alu._g(f"{bp}.and_b", [float(t_bits[bit]), float(sel)])
|
| 399 |
-
out.append(self.alu._g(f"{bp}.or", [and_a, and_b]))
|
| 400 |
-
return bits_to_int(out)
|
| 401 |
-
|
| 402 |
-
def _sp_dec(self, sp: int) -> int:
|
| 403 |
-
"""SP - 1 through the control.push.sp_dec borrow chain. Gate bit index
|
| 404 |
-
is MSB-first (bit addr_bits-1 is the LSB); the bit complement feeding
|
| 405 |
-
each borrow AND is fixed wiring, as in the gate fitness suite."""
|
| 406 |
-
bits = int_to_bits(sp, self.addr_bits)
|
| 407 |
-
out = [0] * self.addr_bits
|
| 408 |
-
borrow = 1
|
| 409 |
-
for bit in range(self.addr_bits - 1, -1, -1):
|
| 410 |
-
bp = f"control.push.sp_dec.bit{bit}"
|
| 411 |
-
h_or = self.alu._g(f"{bp}.xor.layer1.or", [float(bits[bit]), float(borrow)])
|
| 412 |
-
h_nand = self.alu._g(f"{bp}.xor.layer1.nand", [float(bits[bit]), float(borrow)])
|
| 413 |
-
out[bit] = self.alu._g(f"{bp}.xor.layer2", [h_or, h_nand])
|
| 414 |
-
borrow = self.alu._g(f"{bp}.borrow", [float(1 - bits[bit]), float(borrow)])
|
| 415 |
-
return bits_to_int(out)
|
| 416 |
-
|
| 417 |
-
def _decode_op(self, opcode):
|
| 418 |
-
"""4-to-16 opcode one-hot through the gate decoder."""
|
| 419 |
-
ob = [float((opcode >> j) & 1) for j in range(4)]
|
| 420 |
-
return [self.alu._g(f"control.decode.op{n}", ob) for n in range(16)]
|
| 421 |
-
|
| 422 |
-
def _inc(self, pc, name, start):
|
| 423 |
-
"""PC + 2^start through the gate increment chain (LSB-first)."""
|
| 424 |
-
ab = self.addr_bits
|
| 425 |
-
pcb = [(pc >> k) & 1 for k in range(ab)]
|
| 426 |
-
out = [0] * ab
|
| 427 |
-
carry = 0
|
| 428 |
-
for k in range(ab):
|
| 429 |
-
if k < start:
|
| 430 |
-
out[k] = self.alu._g(f"control.pcnext.{name}.bit{k}", [float(pcb[k])])
|
| 431 |
-
else:
|
| 432 |
-
cin = 1 if k == start else carry
|
| 433 |
-
o = self.alu._g(f"control.pcnext.{name}.bit{k}.xor.layer1.or", [float(pcb[k]), float(cin)])
|
| 434 |
-
nd = self.alu._g(f"control.pcnext.{name}.bit{k}.xor.layer1.nand", [float(pcb[k]), float(cin)])
|
| 435 |
-
out[k] = self.alu._g(f"control.pcnext.{name}.bit{k}.xor.layer2", [o, nd])
|
| 436 |
-
carry = self.alu._g(f"control.pcnext.{name}.bit{k}.carry", [float(pcb[k]), float(cin)])
|
| 437 |
-
return sum(out[k] << k for k in range(ab))
|
| 438 |
-
|
| 439 |
-
def _pcnext(self, pc2, pc4, addr, jcc, use_pc4, use_addr, use_jcc):
|
| 440 |
-
"""Next-PC priority mux through the gates: use_addr ? addr :
|
| 441 |
-
(use_jcc ? jcc : (use_pc4 ? pc4 : pc2))."""
|
| 442 |
-
ab = self.addr_bits
|
| 443 |
-
p2 = [(pc2 >> k) & 1 for k in range(ab)]
|
| 444 |
-
p4 = [(pc4 >> k) & 1 for k in range(ab)]
|
| 445 |
-
aB = [(addr >> k) & 1 for k in range(ab)]
|
| 446 |
-
jB = [(jcc >> k) & 1 for k in range(ab)]
|
| 447 |
-
out = [0] * ab
|
| 448 |
-
for k in range(ab):
|
| 449 |
-
ns = self.alu._g(f"control.pcnext.m_pc4.bit{k}.not_sel", [float(use_pc4)])
|
| 450 |
-
aa = self.alu._g(f"control.pcnext.m_pc4.bit{k}.and_a", [float(p2[k]), ns])
|
| 451 |
-
bb = self.alu._g(f"control.pcnext.m_pc4.bit{k}.and_b", [float(p4[k]), float(use_pc4)])
|
| 452 |
-
m1 = self.alu._g(f"control.pcnext.m_pc4.bit{k}.or", [aa, bb])
|
| 453 |
-
ns = self.alu._g(f"control.pcnext.m_jcc.bit{k}.not_sel", [float(use_jcc)])
|
| 454 |
-
aa = self.alu._g(f"control.pcnext.m_jcc.bit{k}.and_a", [float(m1), ns])
|
| 455 |
-
bb = self.alu._g(f"control.pcnext.m_jcc.bit{k}.and_b", [float(jB[k]), float(use_jcc)])
|
| 456 |
-
m2 = self.alu._g(f"control.pcnext.m_jcc.bit{k}.or", [aa, bb])
|
| 457 |
-
ns = self.alu._g(f"control.pcnext.m_addr.bit{k}.not_sel", [float(use_addr)])
|
| 458 |
-
aa = self.alu._g(f"control.pcnext.m_addr.bit{k}.and_a", [float(m2), ns])
|
| 459 |
-
bb = self.alu._g(f"control.pcnext.m_addr.bit{k}.and_b", [float(aB[k]), float(use_addr)])
|
| 460 |
-
out[k] = self.alu._g(f"control.pcnext.m_addr.bit{k}.or", [aa, bb])
|
| 461 |
-
return sum(out[k] << k for k in range(ab))
|
| 462 |
-
|
| 463 |
-
def step(self, state):
|
| 464 |
-
if state["halted"]:
|
| 465 |
-
return state
|
| 466 |
-
s = dict(state)
|
| 467 |
-
s["mem"] = state["mem"][:]
|
| 468 |
-
s["regs"] = state["regs"][:]
|
| 469 |
-
s["flags"] = state["flags"][:]
|
| 470 |
-
addr_mask = (1 << self.addr_bits) - 1
|
| 471 |
-
pc = s["pc"]
|
| 472 |
-
hi = self.mem_read(s["mem"], pc & addr_mask)
|
| 473 |
-
lo = self.mem_read(s["mem"], (pc + 1) & addr_mask)
|
| 474 |
-
ir = ((hi & 0xFF) << 8) | (lo & 0xFF)
|
| 475 |
-
opcode = (ir >> 12) & 0xF
|
| 476 |
-
oh = self._decode_op(opcode) # gate opcode decode (one-hots)
|
| 477 |
-
rd = (ir >> 10) & 0x3
|
| 478 |
-
rs = (ir >> 8) & 0x3
|
| 479 |
-
imm = ir & 0xFF
|
| 480 |
-
addr = None
|
| 481 |
-
if oh[0xA] or oh[0xB] or oh[0xC] or oh[0xD] or oh[0xE]:
|
| 482 |
-
ah = self.mem_read(s["mem"], (pc + 2) & addr_mask)
|
| 483 |
-
al = self.mem_read(s["mem"], (pc + 3) & addr_mask)
|
| 484 |
-
addr = (((ah & 0xFF) << 8) | (al & 0xFF)) & addr_mask
|
| 485 |
-
pc2 = self._inc(pc, "pc2", 1) # PC+2 through the gate chain
|
| 486 |
-
pc4 = self._inc(pc, "pc4", 2) # PC+4 through the gate chain
|
| 487 |
-
a = s["regs"][rd]
|
| 488 |
-
b = s["regs"][rs]
|
| 489 |
-
result = a
|
| 490 |
-
carry = 0
|
| 491 |
-
overflow = 0
|
| 492 |
-
write_result = True
|
| 493 |
-
jcc_val = 0
|
| 494 |
-
if oh[0x0]:
|
| 495 |
-
result, carry = self.alu.add8(a, b)
|
| 496 |
-
overflow = 1 if (((a ^ result) & (b ^ result)) & 0x80) else 0
|
| 497 |
-
elif oh[0x1]:
|
| 498 |
-
result, carry = self.alu.sub8(a, b)
|
| 499 |
-
overflow = 1 if (((a ^ b) & (a ^ result)) & 0x80) else 0
|
| 500 |
-
elif oh[0x2]:
|
| 501 |
-
result = self.alu.and8(a, b)
|
| 502 |
-
elif oh[0x3]:
|
| 503 |
-
result = self.alu.or8(a, b)
|
| 504 |
-
elif oh[0x4]:
|
| 505 |
-
result = self.alu.xor8(a, b)
|
| 506 |
-
elif oh[0x5]:
|
| 507 |
-
result = self.alu.shl8(a)
|
| 508 |
-
elif oh[0x6]:
|
| 509 |
-
result = self.alu.shr8(a)
|
| 510 |
-
elif oh[0x7]:
|
| 511 |
-
result = self.alu.mul8(a, b)
|
| 512 |
-
elif oh[0x8]:
|
| 513 |
-
result, _ = self.alu.div8(a, b)
|
| 514 |
-
elif oh[0x9]: # CMP: set flags only
|
| 515 |
-
r2, carry = self.alu.sub8(a, b)
|
| 516 |
-
z = 1 if r2 == 0 else 0
|
| 517 |
-
n = 1 if (r2 & 0x80) else 0
|
| 518 |
-
v = 1 if (((a ^ b) & (a ^ r2)) & 0x80) else 0
|
| 519 |
-
s["flags"] = [z, n, carry, v]
|
| 520 |
-
write_result = False
|
| 521 |
-
elif oh[0xA]: # LOAD
|
| 522 |
-
result = self.mem_read(s["mem"], addr)
|
| 523 |
-
elif oh[0xB]: # STORE
|
| 524 |
-
s["mem"] = self.mem_write(s["mem"], addr, b & 0xFF)
|
| 525 |
-
write_result = False
|
| 526 |
-
elif oh[0xC]: # JMP (PC = addr via pcnext)
|
| 527 |
-
write_result = False
|
| 528 |
-
elif oh[0xD]: # JCC (PC via jcc mux + pcnext)
|
| 529 |
-
cond = imm & 0x7
|
| 530 |
-
circuit, flag_idx = self._JCC[cond]
|
| 531 |
-
flag = s["flags"][flag_idx]
|
| 532 |
-
sel = flag if cond % 2 == 0 else 1 - flag
|
| 533 |
-
jcc_val = self._jcc_pc(circuit, pc4, addr, sel)
|
| 534 |
-
write_result = False
|
| 535 |
-
elif oh[0xE]: # CALL: push PC+4, PC = addr
|
| 536 |
-
ret_addr = pc4 & 0xFFFF
|
| 537 |
-
sp = s.get("sp", addr_mask)
|
| 538 |
-
sp = self._sp_dec(sp)
|
| 539 |
-
s["mem"] = self.mem_write(s["mem"], sp, (ret_addr >> 8) & 0xFF)
|
| 540 |
-
sp = self._sp_dec(sp)
|
| 541 |
-
s["mem"] = self.mem_write(s["mem"], sp, ret_addr & 0xFF)
|
| 542 |
-
s["sp"] = sp
|
| 543 |
-
write_result = False
|
| 544 |
-
elif oh[0xF]: # HALT
|
| 545 |
-
s["halted"] = True
|
| 546 |
-
return s
|
| 547 |
-
|
| 548 |
-
if write_result:
|
| 549 |
-
s["regs"][rd] = result & 0xFF
|
| 550 |
-
if oh[0x0] or oh[0x1] or oh[0x7]:
|
| 551 |
-
z = 1 if (result & 0xFF) == 0 else 0
|
| 552 |
-
n = 1 if (result & 0x80) else 0
|
| 553 |
-
s["flags"] = [z, n, carry, overflow]
|
| 554 |
-
# PC sequencing through the gate mux.
|
| 555 |
-
use_pc4 = 1 if (oh[0xA] or oh[0xB]) else 0
|
| 556 |
-
use_addr = 1 if (oh[0xC] or oh[0xE]) else 0
|
| 557 |
-
s["pc"] = self._pcnext(pc2, pc4, addr or 0, jcc_val, use_pc4, use_addr, oh[0xD])
|
| 558 |
-
return s
|
| 559 |
-
|
| 560 |
-
def run(self, state, max_cycles=200):
|
| 561 |
-
s = state
|
| 562 |
-
cycles = 0
|
| 563 |
-
while not s["halted"] and cycles < max_cycles:
|
| 564 |
-
s = self.step(s)
|
| 565 |
-
cycles += 1
|
| 566 |
-
return s, cycles
|
| 567 |
-
|
| 568 |
-
|
| 569 |
-
def _encode_instr(opcode, rd, rs, imm):
|
| 570 |
-
return ((opcode & 0xF) << 12) | ((rd & 0x3) << 10) | ((rs & 0x3) << 8) | (imm & 0xFF)
|
| 571 |
-
|
| 572 |
-
|
| 573 |
-
def _w16(mem, addr, value):
|
| 574 |
-
mem[addr] = (value >> 8) & 0xFF
|
| 575 |
-
mem[addr + 1] = value & 0xFF
|
| 576 |
-
|
| 577 |
-
|
| 578 |
-
PROGRAM_MIN_BYTES = 0x84 # code 0x00..0x1F + data 0x80..0x83
|
| 579 |
-
|
| 580 |
-
|
| 581 |
-
def builtin_program(addr_bits: int) -> Tuple[List[int], int]:
|
| 582 |
-
"""Sum 5+4+3+2+1 via a loop. Returns (mem, expected_result_at_0x83).
|
| 583 |
-
|
| 584 |
-
Compact layout: code at 0x00..0x1F (32 bytes), data at 0x80..0x83 (4 bytes).
|
| 585 |
-
Total footprint 132 bytes -- fits within scratchpad (256 B) and larger.
|
| 586 |
-
Requires addr_bits >= 8.
|
| 587 |
-
"""
|
| 588 |
-
if (1 << addr_bits) < PROGRAM_MIN_BYTES:
|
| 589 |
-
raise ValueError(f"addr_bits={addr_bits} too small for builtin program")
|
| 590 |
-
mem = [0] * (1 << addr_bits)
|
| 591 |
-
mem[0x80] = 5 # initial counter
|
| 592 |
-
mem[0x81] = 1 # decrement
|
| 593 |
-
mem[0x82] = 0 # zero (for compare and accumulator init)
|
| 594 |
-
# mem[0x83] is the output
|
| 595 |
-
_w16(mem, 0x0000, _encode_instr(0xA, 1, 0, 0)); _w16(mem, 0x0002, 0x0080)
|
| 596 |
-
_w16(mem, 0x0004, _encode_instr(0xA, 2, 0, 0)); _w16(mem, 0x0006, 0x0081)
|
| 597 |
-
_w16(mem, 0x0008, _encode_instr(0xA, 3, 0, 0)); _w16(mem, 0x000A, 0x0082)
|
| 598 |
-
_w16(mem, 0x000C, _encode_instr(0xA, 0, 0, 0)); _w16(mem, 0x000E, 0x0082)
|
| 599 |
-
_w16(mem, 0x0010, _encode_instr(0x0, 0, 1, 0))
|
| 600 |
-
_w16(mem, 0x0012, _encode_instr(0x1, 1, 2, 0))
|
| 601 |
-
_w16(mem, 0x0014, _encode_instr(0x9, 1, 3, 0))
|
| 602 |
-
_w16(mem, 0x0016, _encode_instr(0xD, 0, 0, 0x01)); _w16(mem, 0x0018, 0x0010)
|
| 603 |
-
_w16(mem, 0x001A, _encode_instr(0xB, 0, 0, 0)); _w16(mem, 0x001C, 0x0083)
|
| 604 |
-
_w16(mem, 0x001E, _encode_instr(0xF, 0, 0, 0))
|
| 605 |
-
return mem, 15
|
| 606 |
-
|
| 607 |
-
|
| 608 |
-
# ---------------------------------------------------------------------------
|
| 609 |
-
# Eval driver
|
| 610 |
-
# ---------------------------------------------------------------------------
|
| 611 |
-
|
| 612 |
-
def _file_fingerprint(path: Path) -> str:
|
| 613 |
-
"""Stable cache key for a safetensors file: sha256 of its content.
|
| 614 |
-
|
| 615 |
-
Hashes are content-addressed so renaming a file doesn't blow the cache,
|
| 616 |
-
but mtime-only would re-key on every clone of the repo. The sha256 of a
|
| 617 |
-
30 MB safetensors finishes in tens of milliseconds — small compared to
|
| 618 |
-
a 5,900-test fitness run.
|
| 619 |
-
"""
|
| 620 |
-
import hashlib
|
| 621 |
-
h = hashlib.sha256()
|
| 622 |
-
with open(path, "rb") as f:
|
| 623 |
-
for chunk in iter(lambda: f.read(1 << 20), b""):
|
| 624 |
-
h.update(chunk)
|
| 625 |
-
return h.hexdigest()
|
| 626 |
-
|
| 627 |
-
|
| 628 |
-
def _cache_key(path: Path, opts: Dict[str, Any]) -> str:
|
| 629 |
-
"""Cache key combining file content with the relevant evaluation options."""
|
| 630 |
-
fp = _file_fingerprint(path)
|
| 631 |
-
opt_str = json.dumps(opts, sort_keys=True)
|
| 632 |
-
import hashlib
|
| 633 |
-
suffix = hashlib.sha256(opt_str.encode("utf-8")).hexdigest()[:8]
|
| 634 |
-
return f"{fp}_{suffix}"
|
| 635 |
-
|
| 636 |
-
|
| 637 |
-
def _load_cache(cache_dir: Path, key: str) -> Dict[str, Any] | None:
|
| 638 |
-
p = cache_dir / f"{key}.json"
|
| 639 |
-
if not p.exists():
|
| 640 |
-
return None
|
| 641 |
-
try:
|
| 642 |
-
return json.loads(p.read_text(encoding="utf-8"))
|
| 643 |
-
except (json.JSONDecodeError, OSError):
|
| 644 |
-
return None
|
| 645 |
-
|
| 646 |
-
|
| 647 |
-
def _save_cache(cache_dir: Path, key: str, payload: Dict[str, Any]) -> None:
|
| 648 |
-
cache_dir.mkdir(parents=True, exist_ok=True)
|
| 649 |
-
p = cache_dir / f"{key}.json"
|
| 650 |
-
try:
|
| 651 |
-
p.write_text(json.dumps(payload, indent=2, default=str), encoding="utf-8")
|
| 652 |
-
except OSError:
|
| 653 |
-
pass
|
| 654 |
-
|
| 655 |
-
|
| 656 |
-
def list_safetensors(path: Path) -> List[Path]:
|
| 657 |
-
if path.is_file():
|
| 658 |
-
return [path]
|
| 659 |
-
if path.is_dir():
|
| 660 |
-
return sorted(p for p in path.glob("*.safetensors") if p.is_file())
|
| 661 |
-
return []
|
| 662 |
-
|
| 663 |
-
|
| 664 |
-
# Standalone machines carry their own circuit inventory and are verified by
|
| 665 |
-
# their own scripts, not the gate-fitness suite. Each maps to the tool that
|
| 666 |
-
# checks it.
|
| 667 |
-
MACHINE_VERIFIER = {
|
| 668 |
-
"subleq8": "machines.py",
|
| 669 |
-
"rv32": "machines.py",
|
| 670 |
-
"matrix8": "matrix8.py",
|
| 671 |
-
"subleq8io": "constructor8.py",
|
| 672 |
-
"reflect": "reflect.py",
|
| 673 |
-
"attractor": "tools/test_attractor.py",
|
| 674 |
-
"reversible": "src/reversible.py",
|
| 675 |
-
"ca": "src/ca.py",
|
| 676 |
-
"tile": "src/tile.py",
|
| 677 |
-
}
|
| 678 |
-
|
| 679 |
-
|
| 680 |
-
def evaluate_one(path: Path, device: str, pop_size: int, debug: bool, run_cpu_program: bool) -> Dict:
|
| 681 |
-
out: Dict = {"path": str(path), "filename": path.name}
|
| 682 |
-
# Skip standalone machines cleanly rather than error on missing standard gates.
|
| 683 |
-
with safe_open(str(path), framework="pt") as f:
|
| 684 |
-
meta = f.metadata() or {}
|
| 685 |
-
if meta.get("machine"):
|
| 686 |
-
m = meta["machine"]
|
| 687 |
-
out.update(status="SKIP", machine=m,
|
| 688 |
-
note=f"standalone machine — verify with {MACHINE_VERIFIER.get(m, 'machines.py')}")
|
| 689 |
-
return out
|
| 690 |
-
try:
|
| 691 |
-
tensors = load_model(str(path))
|
| 692 |
-
except Exception as e:
|
| 693 |
-
out.update(error=f"load failed: {e}", status="ERROR")
|
| 694 |
-
return out
|
| 695 |
-
|
| 696 |
-
manifest = get_manifest(tensors)
|
| 697 |
-
out.update(
|
| 698 |
-
size_mb=path.stat().st_size / (1024 * 1024),
|
| 699 |
-
tensors=len(tensors),
|
| 700 |
-
params=sum(t.numel() for t in tensors.values()),
|
| 701 |
-
manifest=manifest,
|
| 702 |
-
)
|
| 703 |
-
|
| 704 |
-
# Move to device
|
| 705 |
-
tensors = {k: v.to(device) for k, v in tensors.items()}
|
| 706 |
-
|
| 707 |
-
try:
|
| 708 |
-
evaluator = BatchedFitnessEvaluator(device=device, model_path=str(path), tensors=tensors)
|
| 709 |
-
population = create_population(tensors, pop_size=pop_size, device=device)
|
| 710 |
-
t0 = time.perf_counter()
|
| 711 |
-
fitness = evaluator.evaluate(population, debug=debug)
|
| 712 |
-
elapsed = time.perf_counter() - t0
|
| 713 |
-
f0 = float(fitness[0].item()) if pop_size == 1 else float(fitness.mean().item())
|
| 714 |
-
out.update(
|
| 715 |
-
fitness=f0,
|
| 716 |
-
total_tests=evaluator.total_tests,
|
| 717 |
-
elapsed_s=elapsed,
|
| 718 |
-
categories={k: (float(v[0]), int(v[1])) for k, v in evaluator.category_scores.items()},
|
| 719 |
-
status="PASS" if f0 >= 0.9999 else "FAIL",
|
| 720 |
-
)
|
| 721 |
-
except Exception as e:
|
| 722 |
-
out.update(error=f"eval failed: {type(e).__name__}: {e}", status="ERROR")
|
| 723 |
-
return out
|
| 724 |
-
|
| 725 |
-
# Optional: CPU program test (8-bit CPU primitives are in every variant)
|
| 726 |
-
if run_cpu_program:
|
| 727 |
-
if manifest["memory_bytes"] >= PROGRAM_MIN_BYTES:
|
| 728 |
-
try:
|
| 729 |
-
cpu_tensors = {k: v.cpu() for k, v in tensors.items()}
|
| 730 |
-
cpu = GenericThresholdCPU(cpu_tensors)
|
| 731 |
-
mem, expected = builtin_program(manifest["addr_bits"])
|
| 732 |
-
state = {"pc": 0, "regs": [0] * 4, "flags": [0] * 4, "mem": mem, "halted": False}
|
| 733 |
-
t0 = time.perf_counter()
|
| 734 |
-
final, cycles = cpu.run(state, max_cycles=200)
|
| 735 |
-
cpu_elapsed = time.perf_counter() - t0
|
| 736 |
-
got = final["mem"][0x83]
|
| 737 |
-
out["cpu_program"] = {
|
| 738 |
-
"ok": got == expected,
|
| 739 |
-
"got": got,
|
| 740 |
-
"expected": expected,
|
| 741 |
-
"cycles": cycles,
|
| 742 |
-
"elapsed_s": cpu_elapsed,
|
| 743 |
-
}
|
| 744 |
-
if got != expected:
|
| 745 |
-
out["status"] = "FAIL"
|
| 746 |
-
except Exception as e:
|
| 747 |
-
out["cpu_program"] = {"error": str(e)}
|
| 748 |
-
else:
|
| 749 |
-
out["cpu_program"] = {"skipped": f"mem={manifest['memory_bytes']}B < {PROGRAM_MIN_BYTES}"}
|
| 750 |
-
|
| 751 |
-
# Wider-ALU chain test for 16/32-bit variants
|
| 752 |
-
bits = manifest["data_bits"]
|
| 753 |
-
if bits in (16, 32):
|
| 754 |
-
try:
|
| 755 |
-
alu_tensors = {k: v.cpu() for k, v in tensors.items()}
|
| 756 |
-
alu = GenericThresholdALU(alu_tensors, bits)
|
| 757 |
-
t0 = time.perf_counter()
|
| 758 |
-
if bits == 16:
|
| 759 |
-
x, y = 1234, 5678
|
| 760 |
-
z, _ = alu.add_n(x, y, 16); assert z == (x + y) & 0xFFFF
|
| 761 |
-
w, _ = alu.sub_n(z, x, 16); assert w == (z - x) & 0xFFFF, (w, z - x)
|
| 762 |
-
gt = alu.cmp_n(z, x, "greaterthan", 16); assert gt == 1
|
| 763 |
-
lt = alu.cmp_n(x, z, "lessthan", 16); assert lt == 1
|
| 764 |
-
eq = alu.cmp_n(w, y, "eq", 16); assert eq == 1
|
| 765 |
-
p = alu.mul_n(123, 5, 16); assert p == (123 * 5) & 0xFFFF
|
| 766 |
-
else: # 32
|
| 767 |
-
x, y = 1_000_000, 999_000
|
| 768 |
-
z, _ = alu.sub_n(x, y, 32); assert z == 1_000
|
| 769 |
-
s, _ = alu.add_n(z, x, 32); assert s == 1_001_000
|
| 770 |
-
p = alu.mul_n(z, 100, 32); assert p == 100_000
|
| 771 |
-
gt = alu.cmp_n(x, y, "greaterthan", 32); assert gt == 1
|
| 772 |
-
lt = alu.cmp_n(y, x, "lessthan", 32); assert lt == 1
|
| 773 |
-
eq = alu.cmp_n(p, 100_000, "equality", 32); assert eq == 1
|
| 774 |
-
chain_dt = time.perf_counter() - t0
|
| 775 |
-
out[f"alu_chain_{bits}"] = {"ok": True, "elapsed_s": chain_dt}
|
| 776 |
-
except AssertionError as e:
|
| 777 |
-
out[f"alu_chain_{bits}"] = {"ok": False, "error": f"chain mismatch: {e}"}
|
| 778 |
-
out["status"] = "FAIL"
|
| 779 |
-
except Exception as e:
|
| 780 |
-
out[f"alu_chain_{bits}"] = {"ok": False, "error": f"{type(e).__name__}: {e}"}
|
| 781 |
-
out["status"] = "FAIL"
|
| 782 |
-
|
| 783 |
-
return out
|
| 784 |
-
|
| 785 |
-
|
| 786 |
-
def print_row(r: Dict, show_cpu: bool) -> None:
|
| 787 |
-
if r.get("status") == "SKIP":
|
| 788 |
-
m = r.get("machine", "machine")
|
| 789 |
-
print(f" {r['filename']:<48} SKIP ({m} — verify with {MACHINE_VERIFIER.get(m, 'machines.py')})")
|
| 790 |
-
return
|
| 791 |
-
if "error" in r:
|
| 792 |
-
print(f" {r['filename']:<48} ERROR: {r['error'][:80]}")
|
| 793 |
-
return
|
| 794 |
-
m = r["manifest"]
|
| 795 |
-
fit = f"{r['fitness']:.4f}" if r.get("fitness") is not None else "n/a"
|
| 796 |
-
cpu_col = ""
|
| 797 |
-
if show_cpu and "cpu_program" in r:
|
| 798 |
-
cp = r["cpu_program"]
|
| 799 |
-
if cp.get("ok"):
|
| 800 |
-
cpu_col = f" CPU OK ({cp['cycles']}cyc/{cp['elapsed_s']:.1f}s)"
|
| 801 |
-
elif "skipped" in cp:
|
| 802 |
-
cpu_col = f" CPU SKIP"
|
| 803 |
-
elif "error" in cp:
|
| 804 |
-
cpu_col = f" CPU ERR"
|
| 805 |
-
else:
|
| 806 |
-
cpu_col = f" CPU FAIL ({cp.get('got')}!={cp.get('expected')})"
|
| 807 |
-
chain_col = ""
|
| 808 |
-
if show_cpu:
|
| 809 |
-
for bits in (16, 32):
|
| 810 |
-
key = f"alu_chain_{bits}"
|
| 811 |
-
if key in r:
|
| 812 |
-
ch = r[key]
|
| 813 |
-
if ch.get("ok"):
|
| 814 |
-
chain_col = f" ALU{bits} OK ({ch['elapsed_s']:.2f}s)"
|
| 815 |
-
else:
|
| 816 |
-
chain_col = f" ALU{bits} FAIL"
|
| 817 |
-
print(
|
| 818 |
-
f" {r['filename']:<48} d={m['data_bits']:>2}b a={m['addr_bits']:>2}b "
|
| 819 |
-
f"mem={m['memory_bytes']:>6}B size={r['size_mb']:>6.1f}MB "
|
| 820 |
-
f"params={r['params']:>10,} fit={fit:>6} tests={r['total_tests']:>5} "
|
| 821 |
-
f"{r['status']:>5}{cpu_col}{chain_col}"
|
| 822 |
-
)
|
| 823 |
-
|
| 824 |
-
|
| 825 |
-
def main() -> int:
|
| 826 |
-
parser = argparse.ArgumentParser(description="Variant-agnostic eval harness")
|
| 827 |
-
parser.add_argument("path", help="Path to .safetensors file or directory of files")
|
| 828 |
-
parser.add_argument("--device", default="cpu", help="cpu (default) or cuda")
|
| 829 |
-
parser.add_argument("--pop_size", type=int, default=1)
|
| 830 |
-
parser.add_argument("--debug", action="store_true", help="Per-circuit detail per file")
|
| 831 |
-
parser.add_argument("--cpu-program", action="store_true",
|
| 832 |
-
help="Also run a small assembled program through the threshold CPU "
|
| 833 |
-
"(any variant with >= 132 B memory), plus a chained 16- or "
|
| 834 |
-
"32-bit ALU sequence on wider variants")
|
| 835 |
-
parser.add_argument("--json", action="store_true", help="Emit JSON results to stdout instead of a table")
|
| 836 |
-
parser.add_argument("--cache-dir", default=".eval_cache",
|
| 837 |
-
help="Directory for hash-keyed result cache "
|
| 838 |
-
"(default: ./.eval_cache). Set to '' to disable.")
|
| 839 |
-
parser.add_argument("--no-cache", action="store_true",
|
| 840 |
-
help="Disable the result cache for this run.")
|
| 841 |
-
args = parser.parse_args()
|
| 842 |
-
|
| 843 |
-
files = list_safetensors(Path(args.path))
|
| 844 |
-
if not files:
|
| 845 |
-
print(f"No .safetensors files found under {args.path}", file=sys.stderr)
|
| 846 |
-
return 2
|
| 847 |
-
|
| 848 |
-
print(f"Evaluating {len(files)} file(s) on {args.device}\n")
|
| 849 |
-
cache_enabled = bool(args.cache_dir) and not args.no_cache
|
| 850 |
-
cache_dir = Path(args.cache_dir) if cache_enabled else None
|
| 851 |
-
cache_opts = {
|
| 852 |
-
"device": args.device,
|
| 853 |
-
"pop_size": args.pop_size,
|
| 854 |
-
"cpu_program": bool(args.cpu_program),
|
| 855 |
-
}
|
| 856 |
-
cache_hits = 0
|
| 857 |
-
results = []
|
| 858 |
-
fail_count = 0
|
| 859 |
-
for f in files:
|
| 860 |
-
print(f"=== {f.name}")
|
| 861 |
-
cached = None
|
| 862 |
-
key = None
|
| 863 |
-
if cache_enabled:
|
| 864 |
-
try:
|
| 865 |
-
key = _cache_key(f, cache_opts)
|
| 866 |
-
cached = _load_cache(cache_dir, key)
|
| 867 |
-
except OSError:
|
| 868 |
-
cached = None
|
| 869 |
-
if cached is not None:
|
| 870 |
-
r = cached
|
| 871 |
-
cache_hits += 1
|
| 872 |
-
print(f" (cache hit)")
|
| 873 |
-
else:
|
| 874 |
-
r = evaluate_one(f, device=args.device, pop_size=args.pop_size,
|
| 875 |
-
debug=args.debug, run_cpu_program=args.cpu_program)
|
| 876 |
-
# Don't cache ERROR results: a transient failure (OOM, interrupt)
|
| 877 |
-
# would otherwise be replayed on every rerun of the same file.
|
| 878 |
-
if cache_enabled and key is not None and r.get("status") != "ERROR":
|
| 879 |
-
_save_cache(cache_dir, key, r)
|
| 880 |
-
results.append(r)
|
| 881 |
-
print_row(r, show_cpu=args.cpu_program)
|
| 882 |
-
if r.get("status") not in ("PASS", "SKIP"):
|
| 883 |
-
fail_count += 1
|
| 884 |
-
|
| 885 |
-
if args.json:
|
| 886 |
-
# Make it serialisable
|
| 887 |
-
for r in results:
|
| 888 |
-
r["manifest"] = {k: (int(v) if isinstance(v, float) and v.is_integer() else v)
|
| 889 |
-
for k, v in r.get("manifest", {}).items()}
|
| 890 |
-
print(json.dumps(results, indent=2, default=str))
|
| 891 |
-
return fail_count
|
| 892 |
-
|
| 893 |
-
# Summary
|
| 894 |
-
print()
|
| 895 |
-
print("=" * 100)
|
| 896 |
-
print(" SUMMARY")
|
| 897 |
-
print("=" * 100)
|
| 898 |
-
for r in results:
|
| 899 |
-
print_row(r, show_cpu=args.cpu_program)
|
| 900 |
-
|
| 901 |
-
print()
|
| 902 |
-
if fail_count == 0:
|
| 903 |
-
print(f"ALL {len(files)} variants PASS")
|
| 904 |
-
else:
|
| 905 |
-
print(f"{fail_count}/{len(files)} variants FAIL")
|
| 906 |
-
if cache_enabled:
|
| 907 |
-
print(f"(cache: {cache_hits}/{len(files)} hits, dir={cache_dir})")
|
| 908 |
-
return fail_count
|
| 909 |
-
|
| 910 |
-
|
| 911 |
-
if __name__ == "__main__":
|
| 912 |
-
sys.exit(main())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|