threshold-computers / src /machines.py
CharlesCNorton
rv32 decode and PC sequencing as threshold gates: an opcode-class detector network (exact 7-bit match per class), sign-extended immediate generation muxed over the I/S/B/U/J formats, and a next-PC mux (PC+4 / PC+imm / (rs1+imm)&~1). The threshold CPU reads these gate outputs for dispatch, immediates, and the next PC instead of slicing the instruction word in Python; verified in isolation and by the full RV32 lockstep suite plus randomized programs.
c3e47db
Raw
History Blame Contribute Delete
77.9 kB
"""neural_subleq8 and neural_rv32 — runtimes, reference emulators, assemblers,
an RV32 object loader, and test suites for the two endpoint machines of the
family. One file; run the suites with:
python machines.py # both machines
python machines.py subleq # SUBLEQ-8 only
python machines.py rv32 # RV32 assembled-program suite
python machines.py rv32-c # RV32 running stock-compiler C
Build the model files with `python build.py --apply subleq` and
`python build.py --apply rv32`.
"""
from __future__ import annotations
import argparse
import os
import shutil
import struct
import subprocess
import sys
import tempfile
from pathlib import Path
from typing import Dict, List, Optional, Tuple
import torch
from safetensors import safe_open
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from eval import NetlistEvaluator, heaviside, load_metadata
from eval_all import GenericThresholdALU, int_to_bits, bits_to_int
# ============================================================================
# RV32 relocatable-object loader
# ============================================================================
R = {
"R_RISCV_32": 1,
"R_RISCV_BRANCH": 16,
"R_RISCV_JAL": 17,
"R_RISCV_CALL": 18,
"R_RISCV_CALL_PLT": 19,
"R_RISCV_PCREL_HI20": 23,
"R_RISCV_PCREL_LO12_I": 24,
"R_RISCV_PCREL_LO12_S": 25,
"R_RISCV_HI20": 26,
"R_RISCV_LO12_I": 27,
"R_RISCV_LO12_S": 28,
"R_RISCV_RELAX": 51,
}
RREV = {v: k for k, v in R.items()}
class Elf:
def __init__(self, data: bytes):
assert data[:4] == b"\x7fELF", "not ELF"
assert data[4] == 1 and data[5] == 1, "need ELF32 LE"
(self.e_shoff,) = struct.unpack_from("<I", data, 0x20)
(self.e_shentsize, self.e_shnum, self.e_shstrndx) = struct.unpack_from("<HHH", data, 0x2E)
self.data = data
self.sections = []
for i in range(self.e_shnum):
off = self.e_shoff + i * self.e_shentsize
name, typ, flags, addr, offset, size, link, info, align, entsz = \
struct.unpack_from("<IIIIIIIIII", data, off)
self.sections.append(dict(name=name, type=typ, flags=flags, addr=addr,
offset=offset, size=size, link=link, info=info,
align=align, entsize=entsz))
shstr = self.sections[self.e_shstrndx]
self.shstr = data[shstr["offset"]:shstr["offset"] + shstr["size"]]
for s in self.sections:
s["sname"] = self._str(self.shstr, s["name"])
@staticmethod
def _str(tab: bytes, off: int) -> str:
end = tab.find(b"\x00", off)
return tab[off:end].decode()
def symbols(self):
symtab = next(s for s in self.sections if s["sname"] == ".symtab")
strtab = self.sections[symtab["link"]]
st = self.data[strtab["offset"]:strtab["offset"] + strtab["size"]]
syms = []
n = symtab["size"] // 16
for i in range(n):
off = symtab["offset"] + i * 16
name, value, size, info, other, shndx = struct.unpack_from("<IIIBBH", self.data, off)
syms.append(dict(name=self._str(st, name), value=value, size=size,
info=info, shndx=shndx))
return syms
def load(obj: bytes, base: int = 0, mem_size: int = 65536,
entry_symbol: str = "_start") -> Tuple[List[int], int]:
elf = Elf(obj)
mem = [0] * mem_size
# Lay out allocatable progbits/nobits sections (SHF_ALLOC = 0x2).
placed: Dict[int, int] = {} # section index -> load address
cursor = base
for i, s in enumerate(elf.sections):
if not (s["flags"] & 0x2):
continue
align = max(s["align"], 1)
cursor = (cursor + align - 1) & ~(align - 1)
placed[i] = cursor
if s["type"] == 1: # PROGBITS
body = elf.data[s["offset"]:s["offset"] + s["size"]]
for j, byte in enumerate(body):
mem[cursor + j] = byte
cursor += s["size"]
syms = elf.symbols()
def sym_addr(idx: int) -> int:
sym = syms[idx]
if sym["shndx"] in placed:
return placed[sym["shndx"]] + sym["value"]
return sym["value"]
# Apply relocations. PCREL_LO12 references the symbol of its paired HI20
# site, so first index HI20/PCREL_HI20 results by their location.
hi_at: Dict[int, int] = {}
for s in elf.sections:
if s["type"] != 4: # RELA
continue
target = elf.sections[s["info"]]
if s["info"] not in placed:
continue
tbase = placed[s["info"]]
n = s["size"] // 12
for i in range(n):
off = s["offset"] + i * 12
r_offset, r_info, r_addend = struct.unpack_from("<IIi", elf.data, off)
r_type = r_info & 0xFF
r_sym = r_info >> 8
loc = tbase + r_offset
if r_type in (R["R_RISCV_PCREL_HI20"], R["R_RISCV_HI20"]):
hi_at[loc] = sym_addr(r_sym) + r_addend
def rd(a):
return mem[a] | (mem[a + 1] << 8) | (mem[a + 2] << 16) | (mem[a + 3] << 24)
def wr(a, v):
for k in range(4):
mem[a + k] = (v >> (8 * k)) & 0xFF
for s in elf.sections:
if s["type"] != 4 or s["info"] not in placed:
continue
tbase = placed[s["info"]]
n = s["size"] // 12
for i in range(n):
off = s["offset"] + i * 12
r_offset, r_info, r_addend = struct.unpack_from("<IIi", elf.data, off)
r_type = r_info & 0xFF
r_sym = r_info >> 8
loc = tbase + r_offset
S = sym_addr(r_sym) + r_addend
w = rd(loc)
if r_type == R["R_RISCV_32"]:
wr(loc, S & 0xFFFFFFFF)
elif r_type == R["R_RISCV_HI20"]:
wr(loc, (w & 0xFFF) | (((S + 0x800) >> 12) << 12) & 0xFFFFF000)
elif r_type == R["R_RISCV_LO12_I"]:
wr(loc, (w & 0xFFFFF) | ((S & 0xFFF) << 20))
elif r_type == R["R_RISCV_LO12_S"]:
imm = S & 0xFFF
wr(loc, (w & 0x1FFF07F) | ((imm >> 5) << 25) | ((imm & 31) << 7))
elif r_type == R["R_RISCV_PCREL_HI20"]:
delta = S - loc
wr(loc, (w & 0xFFF) | (((delta + 0x800) >> 12) << 12) & 0xFFFFF000)
elif r_type in (R["R_RISCV_PCREL_LO12_I"], R["R_RISCV_PCREL_LO12_S"]):
hi = hi_at[sym_addr(r_sym) + r_addend]
delta = hi - (sym_addr(r_sym) + r_addend)
if r_type == R["R_RISCV_PCREL_LO12_I"]:
wr(loc, (w & 0xFFFFF) | ((delta & 0xFFF) << 20))
else:
imm = delta & 0xFFF
wr(loc, (w & 0x1FFF07F) | ((imm >> 5) << 25) | ((imm & 31) << 7))
elif r_type in (R["R_RISCV_CALL"], R["R_RISCV_CALL_PLT"]):
delta = S - loc
hi = (delta + 0x800) >> 12
lo = delta - (hi << 12)
w2 = rd(loc + 4)
wr(loc, (w & 0xFFF) | ((hi << 12) & 0xFFFFF000))
wr(loc + 4, (w2 & 0xFFFFF) | ((lo & 0xFFF) << 20))
elif r_type == R["R_RISCV_JAL"]:
d = S - loc
imm = (((d >> 20) & 1) << 31) | (((d >> 1) & 0x3FF) << 21) | \
(((d >> 11) & 1) << 20) | (((d >> 12) & 0xFF) << 12)
wr(loc, (w & 0xFFF) | imm)
elif r_type == R["R_RISCV_BRANCH"]:
d = S - loc
imm = (((d >> 12) & 1) << 31) | (((d >> 5) & 0x3F) << 25) | \
(((d >> 1) & 0xF) << 8) | (((d >> 11) & 1) << 7)
wr(loc, (w & 0x1FFF07F) | imm)
elif r_type == R["R_RISCV_RELAX"]:
pass
else:
raise NotImplementedError(f"reloc {RREV.get(r_type, r_type)}")
entry = 0
for sym in syms:
if sym["name"] == entry_symbol:
entry = sym_addr(sym["shndx"] and syms.index(sym))
entry = placed.get(sym["shndx"], base) + sym["value"]
break
return mem, entry
# ============================================================================
# neural_rv32: assembler, reference emulator, threshold runtime
# ============================================================================
RV32_MODEL = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
"variants", "neural_rv32.safetensors")
MASK32 = 0xFFFFFFFF
MMIO_CONSOLE = 0xFF00
def fcvt_s_w_ref(x: int) -> int:
"""Signed int32 -> float32 word, round-to-nearest-even (host float()
then pack to float32 is exact RNE for the int32 range)."""
xi = x if (x & MASK32) < 0x80000000 else (x & MASK32) - (1 << 32)
return struct.unpack("<I", struct.pack("<f", float(xi)))[0]
RM_NAMES = {"rne": 0, "rtz": 1, "rdn": 2, "rup": 3, "rmm": 4, "dyn": 7}
def _fcvt_round_up(rm: int, sign: int, guard: int, sticky: int, lsb: int) -> int:
"""Whether the truncated magnitude rounds up by one ULP under the RISC-V
rounding mode (000 RNE, 001 RTZ, 010 RDN, 011 RUP, 100 RMM; 111 DYN has no
fcsr in this machine and follows RNE). guard is the half bit just below the
integer, sticky the OR of everything beneath it."""
if rm == 1: # RTZ: truncate
return 0
if rm == 2: # RDN: toward -inf
return 1 if (sign and (guard or sticky)) else 0
if rm == 3: # RUP: toward +inf
return 1 if ((not sign) and (guard or sticky)) else 0
if rm == 4: # RMM: nearest, ties away
return 1 if guard else 0
return 1 if (guard and (sticky or lsb)) else 0 # RNE / DYN
def fcvt_w_s_ref(w: int, rm: int = 1) -> int:
"""float32 word -> signed int32 under rounding mode `rm`, saturating;
NaN -> INT_MAX, infinities -> INT_MIN/INT_MAX."""
s, e, f = (w >> 31) & 1, (w >> 23) & 0xFF, w & 0x7FFFFF
if e == 0xFF:
return 0x7FFFFFFF if f else (0x80000000 if s else 0x7FFFFFFF)
mant = ((1 << 23) | f) if e else f # subnormal: no implicit 1
E = (e - 127) if e else -126 # subnormal exponent 1-bias
if E >= 31: # |value| >= 2^31: saturate
if s and E == 31 and f == 0:
return 0x80000000 # exactly -2^31
return 0x80000000 if s else 0x7FFFFFFF
if E >= 23:
mag = mant << (E - 23) # exact integer, no fraction
else:
sh = 23 - E
mag = mant >> sh
guard = (mant >> (sh - 1)) & 1
sticky = 1 if (mant & ((1 << (sh - 1)) - 1)) else 0
mag += _fcvt_round_up(rm, s, guard, sticky, mag & 1)
if s:
return 0x80000000 if mag >= 0x80000000 else (-mag) & MASK32
return 0x7FFFFFFF if mag > 0x7FFFFFFF else mag & MASK32
def sext(v: int, bits: int) -> int:
v &= (1 << bits) - 1
return v - (1 << bits) if v & (1 << (bits - 1)) else v
def u32(v: int) -> int:
return v & MASK32
# =============================================================================
# Assembler
# =============================================================================
R_FUNCT = {"add": (0, 0), "sub": (0, 32), "sll": (1, 0), "slt": (2, 0),
"sltu": (3, 0), "xor": (4, 0), "srl": (5, 0), "sra": (5, 32),
"or": (6, 0), "and": (7, 0),
"mul": (0, 1), "mulh": (1, 1), "mulhsu": (2, 1), "mulhu": (3, 1),
"div": (4, 1), "divu": (5, 1), "rem": (6, 1), "remu": (7, 1)}
I_FUNCT = {"addi": 0, "slti": 2, "sltiu": 3, "xori": 4, "ori": 6, "andi": 7}
B_FUNCT = {"beq": 0, "bne": 1, "blt": 4, "bge": 5, "bltu": 6, "bgeu": 7}
L_FUNCT = {"lb": 0, "lh": 1, "lw": 2, "lbu": 4, "lhu": 5}
S_FUNCT = {"sb": 0, "sh": 1, "sw": 2}
class Asm:
def __init__(self):
self.words: List = []
self.labels: Dict[str, int] = {}
self.data_here = None
def label(self, name):
self.labels[name] = len(self.words) * 4
def emit(self, *parts):
self.words.append(parts)
def __getattr__(self, name):
if name.startswith("__"):
raise AttributeError(name)
mn = name.rstrip("_").replace("_", ".")
def f(*args):
self.emit(mn, *args)
return f
def word(self, v):
self.emit(".word", v)
def _enc(self, parts, pc):
mn = parts[0]
a = parts[1:]
def imm(x, pcrel=False):
if isinstance(x, str):
t = self.labels[x]
return t - pc if pcrel else t
return x
if mn == ".word":
return imm(a[0]) & MASK32
if mn in R_FUNCT:
f3, f7 = R_FUNCT[mn]
return (f7 << 25) | (a[2] << 20) | (a[1] << 15) | (f3 << 12) | (a[0] << 7) | 0x33
if mn in I_FUNCT:
return ((imm(a[2]) & 0xFFF) << 20) | (a[1] << 15) | (I_FUNCT[mn] << 12) | (a[0] << 7) | 0x13
if mn in ("slli", "srli", "srai"):
f7 = 32 if mn == "srai" else 0
f3 = 1 if mn == "slli" else 5
return (f7 << 25) | ((a[2] & 31) << 20) | (a[1] << 15) | (f3 << 12) | (a[0] << 7) | 0x13
if mn in L_FUNCT:
return ((imm(a[2]) & 0xFFF) << 20) | (a[1] << 15) | (L_FUNCT[mn] << 12) | (a[0] << 7) | 0x03
if mn in S_FUNCT:
i = imm(a[2]) & 0xFFF
return ((i >> 5) << 25) | (a[0] << 20) | (a[1] << 15) | (S_FUNCT[mn] << 12) | ((i & 31) << 7) | 0x23
if mn in B_FUNCT:
i = imm(a[2], pcrel=True) & 0x1FFF
return (((i >> 12) & 1) << 31) | (((i >> 5) & 0x3F) << 25) | (a[1] << 20) | \
(a[0] << 15) | (B_FUNCT[mn] << 12) | (((i >> 1) & 0xF) << 8) | (((i >> 11) & 1) << 7) | 0x63
if mn == "lui":
return ((imm(a[1]) & 0xFFFFF) << 12) | (a[0] << 7) | 0x37
if mn == "auipc":
return ((imm(a[1]) & 0xFFFFF) << 12) | (a[0] << 7) | 0x17
if mn == "jal":
i = imm(a[1], pcrel=True) & 0x1FFFFF
return (((i >> 20) & 1) << 31) | (((i >> 1) & 0x3FF) << 21) | (((i >> 11) & 1) << 20) | \
(((i >> 12) & 0xFF) << 12) | (a[0] << 7) | 0x6F
if mn == "jalr":
return ((imm(a[2]) & 0xFFF) << 20) | (a[1] << 15) | (a[0] << 7) | 0x67
if mn == "flw":
return ((imm(a[2]) & 0xFFF) << 20) | (a[1] << 15) | (2 << 12) | (a[0] << 7) | 0x07
if mn == "fsw":
i = imm(a[2]) & 0xFFF
return ((i >> 5) << 25) | (a[0] << 20) | (a[1] << 15) | (2 << 12) | ((i & 31) << 7) | 0x27
if mn in ("fadd.s", "fsub.s", "fmul.s", "fdiv.s"):
f7 = {"fadd.s": 0, "fsub.s": 4, "fmul.s": 8, "fdiv.s": 12}[mn]
return (f7 << 25) | (a[2] << 20) | (a[1] << 15) | (a[0] << 7) | 0x53
if mn in ("fmadd.s", "fmsub.s", "fnmsub.s", "fnmadd.s"): # R4-type: rd,rs1,rs2,rs3
op = {"fmadd.s": 0x43, "fmsub.s": 0x47, "fnmsub.s": 0x4b, "fnmadd.s": 0x4f}[mn]
return (a[3] << 27) | (a[2] << 20) | (a[1] << 15) | (a[0] << 7) | op
if mn in ("fle.s", "flt.s", "feq.s"):
f3 = {"fle.s": 0, "flt.s": 1, "feq.s": 2}[mn]
return (0x50 << 25) | (a[2] << 20) | (a[1] << 15) | (f3 << 12) | (a[0] << 7) | 0x53
if mn == "fmv.w.x":
return (0x78 << 25) | (a[1] << 15) | (a[0] << 7) | 0x53
if mn == "fmv.x.w":
return (0x70 << 25) | (a[1] << 15) | (a[0] << 7) | 0x53
if mn == "fcvt.w.s":
rm = RM_NAMES.get(a[2], a[2]) if len(a) > 2 else 1 # default rtz
return (0x60 << 25) | (a[1] << 15) | (rm << 12) | (a[0] << 7) | 0x53
if mn == "fcvt.s.w":
return (0x68 << 25) | (a[1] << 15) | (a[0] << 7) | 0x53
if mn in ("fsgnj.s", "fsgnjn.s", "fsgnjx.s"):
f3 = {"fsgnj.s": 0, "fsgnjn.s": 1, "fsgnjx.s": 2}[mn]
return (0x10 << 25) | (a[2] << 20) | (a[1] << 15) | (f3 << 12) | (a[0] << 7) | 0x53
if mn == "neur":
return (a[2] << 20) | (a[1] << 15) | (a[0] << 7) | 0x0B
if mn == "ecall":
return 0x73
if mn == "fence":
return 0x0F
raise ValueError(f"unknown mnemonic {mn}")
def assemble(self, mem_size=65536) -> List[int]:
mem = [0] * mem_size
for idx, parts in enumerate(self.words):
w = self._enc(parts, idx * 4)
for b in range(4):
mem[idx * 4 + b] = (w >> (8 * b)) & 0xFF # little-endian
return mem
# =============================================================================
# Reference emulator (architectural contract)
# =============================================================================
class RefRV32:
@staticmethod
def step(s):
if s["halted"]:
return s
s = dict(s, mem=s["mem"][:], regs=s["regs"][:], fregs=s["fregs"][:],
console=s["console"])
pc = s["pc"]
w = RefRV32._fetch(s, pc)
RefRV32._exec(s, pc, w)
return s
@staticmethod
def _fetch(s, pc):
m = s["mem"]
return m[pc] | (m[pc + 1] << 8) | (m[pc + 2] << 16) | (m[pc + 3] << 24)
@staticmethod
def _load(s, addr, width, signed):
v = 0
for b in range(width):
v |= s["mem"][(addr + b) & 0xFFFF] << (8 * b)
return u32(sext(v, width * 8)) if signed else v
@staticmethod
def _store(s, addr, val, width):
for b in range(width):
s["mem"][(addr + b) & 0xFFFF] = (val >> (8 * b)) & 0xFF
if (addr & 0xFFFF) == MMIO_CONSOLE:
s["console"] += chr(val & 0xFF)
@staticmethod
def _exec(s, pc, w):
op = w & 0x7F
rd = (w >> 7) & 31
f3 = (w >> 12) & 7
rs1 = (w >> 15) & 31
rs2 = (w >> 20) & 31
f7 = w >> 25
R = s["regs"]
FR = s["fregs"]
a, b = R[rs1], R[rs2]
nxt = (pc + 4) & MASK32
def wr(v):
if rd:
R[rd] = u32(v)
if op == 0x33: # OP (+ M)
if f7 == 1:
sa, sb = sext(a, 32), sext(b, 32)
if f3 == 0: wr(a * b)
elif f3 == 1: wr((sa * sb) >> 32)
elif f3 == 2: wr((sa * b) >> 32)
elif f3 == 3: wr((a * b) >> 32)
elif f3 == 4: wr(MASK32 if b == 0 else (0x80000000 if (sa, sb) == (-2**31, -1) else int(abs(sa) // abs(sb)) * (1 if (sa < 0) == (sb < 0) else -1)))
elif f3 == 5: wr(MASK32 if b == 0 else a // b)
elif f3 == 6: wr(sa if b == 0 else (0 if (sa, sb) == (-2**31, -1) else sa - (int(abs(sa) // abs(sb)) * (1 if (sa < 0) == (sb < 0) else -1)) * sb))
elif f3 == 7: wr(a if b == 0 else a % b)
else:
sh = b & 31
if f3 == 0: wr(a - b if f7 == 32 else a + b)
elif f3 == 1: wr(a << sh)
elif f3 == 2: wr(1 if sext(a, 32) < sext(b, 32) else 0)
elif f3 == 3: wr(1 if a < b else 0)
elif f3 == 4: wr(a ^ b)
elif f3 == 5: wr(u32(sext(a, 32) >> sh) if f7 == 32 else a >> sh)
elif f3 == 6: wr(a | b)
elif f3 == 7: wr(a & b)
elif op == 0x13: # OP-IMM
imm = sext(w >> 20, 12)
sh = (w >> 20) & 31
if f3 == 0: wr(a + imm)
elif f3 == 1: wr(a << sh)
elif f3 == 2: wr(1 if sext(a, 32) < imm else 0)
elif f3 == 3: wr(1 if a < u32(imm) else 0)
elif f3 == 4: wr(a ^ u32(imm))
elif f3 == 5: wr(u32(sext(a, 32) >> sh) if f7 == 32 else a >> sh)
elif f3 == 6: wr(a | u32(imm))
elif f3 == 7: wr(a & u32(imm))
elif op == 0x37: wr(w & 0xFFFFF000) # LUI
elif op == 0x17: wr(pc + (w & 0xFFFFF000)) # AUIPC
elif op == 0x6F: # JAL
imm = sext((((w >> 31) & 1) << 20) | (((w >> 12) & 0xFF) << 12)
| (((w >> 20) & 1) << 11) | (((w >> 21) & 0x3FF) << 1), 21)
wr(nxt)
nxt = u32(pc + imm)
elif op == 0x67: # JALR
t = u32(a + sext(w >> 20, 12)) & ~1
wr(nxt)
nxt = t
elif op == 0x63: # BRANCH
imm = sext((((w >> 31) & 1) << 12) | (((w >> 7) & 1) << 11)
| (((w >> 25) & 0x3F) << 5) | (((w >> 8) & 0xF) << 1), 13)
sa, sb = sext(a, 32), sext(b, 32)
take = [a == b, a != b, None, None, sa < sb, sa >= sb, a < b, a >= b][f3]
if take:
nxt = u32(pc + imm)
elif op == 0x03: # LOAD
addr = u32(a + sext(w >> 20, 12)) & 0xFFFF
width = [1, 2, 4, 0, 1, 2][f3]
wr(RefRV32._load(s, addr, width, f3 < 3))
elif op == 0x23: # STORE
imm = sext(((w >> 25) << 5) | ((w >> 7) & 31), 12)
addr = u32(a + imm) & 0xFFFF
RefRV32._store(s, addr, b, [1, 2, 4][f3])
elif op == 0x07: # FLW
addr = u32(a + sext(w >> 20, 12)) & 0xFFFF
FR[rd] = RefRV32._load(s, addr, 4, False)
elif op == 0x27: # FSW
imm = sext(((w >> 25) << 5) | ((w >> 7) & 31), 12)
addr = u32(a + imm) & 0xFFFF
RefRV32._store(s, addr, FR[rs2], 4)
elif op == 0x53: # OP-FP
from eval import float_add_oracle, float_mul_oracle, float_div_oracle
fa, fb = FR[rs1], FR[rs2]
if f7 == 0x00: FR[rd] = float_add_oracle(fa, fb, 8, 23)
elif f7 == 0x04: FR[rd] = float_add_oracle(fa, fb ^ 0x80000000, 8, 23)
elif f7 == 0x08: FR[rd] = float_mul_oracle(fa, fb, 8, 23)
elif f7 == 0x0C: FR[rd] = float_div_oracle(fa, fb, 8, 23)
elif f7 == 0x10:
sa, sb = (fa >> 31) & 1, (fb >> 31) & 1
so = [sb, 1 - sb, sa ^ sb][f3]
FR[rd] = (fa & 0x7FFFFFFF) | (so << 31)
elif f7 == 0x50:
from eval import float_bits_to_value
x, y = float_bits_to_value(fa, 8, 23), float_bits_to_value(fb, 8, 23)
wr(1 if [x <= y, x < y, x == y][f3] else 0)
elif f7 == 0x60: wr(fcvt_w_s_ref(FR[rs1], f3)) # FCVT.W.S (rm=funct3)
elif f7 == 0x68: FR[rd] = fcvt_s_w_ref(R[rs1]) # FCVT.S.W
elif f7 == 0x78: FR[rd] = a
elif f7 == 0x70: wr(FR[rs1])
elif op in (0x43, 0x47, 0x4b, 0x4f): # FMADD/FMSUB/FNMSUB/FNMADD.S
from eval import float_fma_oracle
rs3 = (w >> 27) & 0x1F
fa, fb, fc = FR[rs1], FR[rs2], FR[rs3]
if op in (0x4b, 0x4f): fa ^= 0x80000000 # negate product
if op in (0x47, 0x4f): fc ^= 0x80000000 # negate addend
FR[rd] = float_fma_oracle(fa, fb, fc, 8, 23)
elif op == 0x0B: # NEUR (custom-0)
x = a & 0xFF
pos, neg = b & 0xFF, (b >> 8) & 0xFF
bias = sext(b >> 16, 5)
acc = bin(x & pos).count("1") - bin(x & neg).count("1") + bias
wr(1 if acc >= 0 else 0)
elif op == 0x73:
s["halted"] = True
elif op == 0x0F:
pass
else:
raise ValueError(f"illegal instruction {w:#010x} at {pc:#06x}")
s["pc"] = nxt
@staticmethod
def run(state, max_cycles=10000):
s = state
n = 0
while not s["halted"] and n < max_cycles:
s = RefRV32.step(s)
n += 1
return s, n
def rv_state(mem):
return {"pc": 0, "regs": [0] * 32, "fregs": [0] * 32, "mem": mem[:],
"halted": False, "console": ""}
# =============================================================================
# Threshold runtime
# =============================================================================
PAIRABLE = (0x33, 0x13, 0x37, 0x17)
DECODE_CLASSES = ("op", "op_imm", "load", "store", "branch", "lui", "auipc",
"jal", "jalr", "system", "fp", "flw", "fsw", "fma")
class Rv32ThresholdCPU:
def __init__(self, path=RV32_MODEL):
self.T = {}
with safe_open(path, framework="pt") as f:
for name in f.keys():
self.T[name] = f.get_tensor(name).float()
self.registry = load_metadata(path)["signal_registry"]
self.alu = GenericThresholdALU(self.T, 32)
self.fpu = {op: NetlistEvaluator(self.T, self.registry, f"float32.{op}")
for op in ("add", "mul", "div", "cmp", "fma")}
self.decode = NetlistEvaluator(self.T, self.registry, "rv32.decode")
self.pcseq = NetlistEvaluator(self.T, self.registry, "rv32.pcnext")
self.pairs_issued = 0
# ---- gate helpers -------------------------------------------------
def _g(self, name, inputs):
return self.alu._g(name, inputs)
def _not1(self, x):
return self._g("alu.alu32bit.not.bit0", [float(x)])
def _bitwise(self, opname, a, b):
ab = int_to_bits(a, 32)
bb = int_to_bits(b, 32)
out = []
for k in range(32):
if opname == "xor":
p = f"alu.alu32bit.xor.bit{k}"
h1 = self._g(f"{p}.layer1.or", [ab[k], bb[k]])
h2 = self._g(f"{p}.layer1.nand", [ab[k], bb[k]])
out.append(self._g(f"{p}.layer2", [h1, h2]))
else:
out.append(self._g(f"alu.alu32bit.{opname}.bit{k}", [ab[k], bb[k]]))
return bits_to_int(out)
def _not32(self, a):
ab = int_to_bits(a, 32)
return bits_to_int([self._g(f"alu.alu32bit.not.bit{k}", [ab[k]]) for k in range(32)])
def _barrel_left(self, val, sh):
layer_in = int_to_bits(val, 32)
for layer in range(5):
amount = 1 << (4 - layer)
sel = (sh >> (4 - layer)) & 1
nxt = []
for bit in range(32):
p = f"combinational.barrelshifter32.layer{layer}.bit{bit}"
ns = self._g(f"{p}.not_sel", [float(sel)])
src = layer_in[bit + amount] if bit + amount < 32 else 0
a_ = self._g(f"{p}.and_a", [layer_in[bit], ns])
b_ = self._g(f"{p}.and_b", [float(src), float(sel)])
nxt.append(self._g(f"{p}.or", [a_, b_]))
layer_in = nxt
return bits_to_int(layer_in)
def sll(self, a, sh):
return self._barrel_left(a, sh & 31)
def srl(self, a, sh):
rev = int("{:032b}".format(u32(a))[::-1], 2)
return int("{:032b}".format(self._barrel_left(rev, sh & 31))[::-1], 2)
def sra(self, a, sh):
sign = (a >> 31) & 1
x1 = self.srl(a, sh)
x2 = self._not32(self.srl(self._not32(a), sh))
o1, o2 = int_to_bits(x1, 32), int_to_bits(x2, 32)
out = []
for k in range(32):
p = f"rv32.sra_mux.bit{k}"
ns = self._g(f"{p}.not_sel", [float(sign)])
a_ = self._g(f"{p}.and_a", [o1[k], ns])
b_ = self._g(f"{p}.and_b", [o2[k], float(sign)])
out.append(self._g(f"{p}.or", [a_, b_]))
return bits_to_int(out)
def add32(self, a, b):
return self.alu.add_n(a, b, 32)[0]
def sub32(self, a, b):
return self.alu.sub_n(a, b, 32)[0]
def ucmp(self, a, b, kind):
return self.alu.cmp_n(a, b, kind, 32)
def scmp_lt(self, a, b):
fa = (a & 0x7FFFFFFF) | (self._not1((a >> 31) & 1) << 31)
fb = (b & 0x7FFFFFFF) | (self._not1((b >> 31) & 1) << 31)
return self.ucmp(fa, fb, "lessthan")
def mulhu(self, a, b):
"""Full 64-bit product high word via pp gates + rv32.mulacc chains."""
ab = int_to_bits(a, 32)
bb = int_to_bits(b, 32)
def pp(i_lsb, j_lsb): # LSB-first indices onto MSB-first gate names
return self._g(f"alu.alu32bit.mul.pp.a{31 - i_lsb}b{31 - j_lsb}",
[ab[31 - i_lsb], bb[31 - j_lsb]])
acc = [pp(k, 0) if k <= 31 else 0.0 for k in range(64)]
for t in range(31):
row = t + 1
carry = 0.0
nxt = []
for k in range(64):
b_in = pp(k - row, row) if row <= k <= row + 31 else 0.0
s, carry = self.alu._fa(f"rv32.mulacc.s{t}.fa{k}", acc[k], b_in, carry)
nxt.append(s)
acc = nxt
return bits_to_int(list(reversed([int(x) for x in acc[32:]])))
def _cond_mask(self, val, cond_bit):
vb = int_to_bits(val, 32)
return bits_to_int([self._g(f"alu.alu32bit.and.bit{k}", [vb[k], float(cond_bit)])
for k in range(32)])
def mulh(self, a, b, sign_a, sign_b):
h = self.mulhu(a, b)
if sign_a:
h = self.sub32(h, self._cond_mask(b, (a >> 31) & 1))
if sign_b:
h = self.sub32(h, self._cond_mask(a, (b >> 31) & 1))
return h
def divu(self, a, b):
if b == 0:
return MASK32, a
a_bits = int_to_bits(a, 32)
q = 0
rem = 0
for stage in range(32):
rem = ((rem << 1) | a_bits[stage]) & MASK32
prefix = f"alu.alu32bit.div.stage{stage}"
finals = {"gt": f"{prefix}.cmp_bc.gt", "lt": f"{prefix}.cmp_bc.lt",
"eq": f"{prefix}.cmp_bc.eq", "ge": f"{prefix}.cmp",
"le": f"{prefix}.cmp_bc.le"}
ge = self.alu._cmp_bit_cascade(f"{prefix}.cmp_bc", finals,
int_to_bits(rem, 32), int_to_bits(b, 32), "ge")
if ge:
rem = self.sub32(rem, b)
q = (q << 1) | 1
else:
q <<= 1
return u32(q), rem
def neg32(self, a):
ab = int_to_bits(a, 32)
nb = [self._g(f"alu.alu32bit.neg.not.bit{k}", [ab[k]]) for k in range(32)]
carry = 1.0
out = [0] * 32
for k in range(31, -1, -1):
p = f"alu.alu32bit.neg.inc.bit{k}"
h1 = self._g(f"{p}.xor.layer1.or", [nb[k], carry])
h2 = self._g(f"{p}.xor.layer1.nand", [nb[k], carry])
out[k] = self._g(f"{p}.xor.layer2", [h1, h2])
carry = self._g(f"{p}.carry", [nb[k], carry])
return bits_to_int(out)
def divs(self, a, b):
if b == 0:
return MASK32, a
if a == 0x80000000 and b == MASK32:
return 0x80000000, 0
na, nb_ = (a >> 31) & 1, (b >> 31) & 1
ua = self.neg32(a) if na else a
ub = self.neg32(b) if nb_ else b
q, r = self.divu(ua, ub)
if na != nb_:
q = self.neg32(q)
if na:
r = self.neg32(r)
return q, r
def priority_encode(self, v):
"""MSB-first position (0..31) of the highest set bit, via the
combinational.priorityencoder32 gates. v must be nonzero."""
vb = int_to_bits(v, 32) # MSB-first, bit index 0 = MSB
any_higher = [0]
for pos in range(1, 32):
any_higher.append(self._g(f"combinational.priorityencoder32.any_higher{pos}",
[float(vb[i]) for i in range(pos)]))
is_high = [float(vb[0])]
for pos in range(1, 32):
nh = self._g(f"combinational.priorityencoder32.is_highest{pos}.not_higher",
[float(any_higher[pos])])
is_high.append(self._g(f"combinational.priorityencoder32.is_highest{pos}.and",
[float(vb[pos]), nh]))
out = 0
for bit in range(5):
rel = [is_high[pos] for pos in range(32) if (pos >> bit) & 1]
out |= self._g(f"combinational.priorityencoder32.out{bit}", [float(x) for x in rel]) << bit
return out
def fcvt_s_w(self, x):
"""Signed int32 -> float32, round-to-nearest-even. Sign, leading-one
detect (priority encoder), left-normalize (barrel shifter), round."""
if x == 0:
return 0
s = (x >> 31) & 1
u = self.neg32(x) if s else x
p = self.priority_encode(u) # leading one at MSB-first pos p
shifted = self.sll(u, p) # leading one now at bit 31
exp = 127 + (31 - p)
frac = (shifted >> 8) & 0x7FFFFF
guard = (shifted >> 7) & 1
below = shifted & 0x7F
if guard and ((frac & 1) or below):
frac += 1
if frac > 0x7FFFFF:
frac = 0
exp += 1
return (s << 31) | (exp << 23) | frac
def fcvt_w_s(self, w, rm=1):
"""float32 -> signed int32 under rounding mode `rm`, saturating. The
shifts run through the barrel shifter; the round decision is control
logic, as in fcvt_s_w. NaN -> INT_MAX; overflow saturates."""
s, e, f = (w >> 31) & 1, (w >> 23) & 0xFF, w & 0x7FFFFF
if e == 0xFF:
return 0x7FFFFFFF if f else (0x80000000 if s else 0x7FFFFFFF)
mant = ((1 << 23) | f) if e else f
E = (e - 127) if e else -126
if E >= 31:
if s and E == 31 and f == 0:
return 0x80000000 # exactly -2^31
return 0x80000000 if s else 0x7FFFFFFF
if E >= 23:
mag = self.sll(mant, E - 23)
else:
sh = 23 - E
mag = self.srl(mant, sh) if sh <= 31 else 0
guard = (mant >> (sh - 1)) & 1
sticky = 1 if (mant & ((1 << (sh - 1)) - 1)) else 0
mag += _fcvt_round_up(rm, s, guard, sticky, mag & 1)
if s:
return 0x80000000 if mag >= 0x80000000 else self.neg32(mag)
return 0x7FFFFFFF if mag > 0x7FFFFFFF else (mag & MASK32)
def neur(self, x_reg, w_reg):
x = int_to_bits(x_reg & 0xFF, 8) # MSB-first bits of low byte
pos = int_to_bits(w_reg & 0xFF, 8)
neg = int_to_bits((w_reg >> 8) & 0xFF, 8)
p_bits = [self._g(f"rv32.neur.pos.bit{i}", [x[7 - i], pos[7 - i]]) for i in range(8)]
n_bits = [self._g(f"rv32.neur.neg.bit{i}", [x[7 - i], neg[7 - i]]) for i in range(8)]
def pcount(side, bits8):
fa = lambda t, a_, b_, c_: self.alu._fa(f"rv32.neur.{side}.fa{t}", a_, b_, c_)
s0, c0 = fa(0, bits8[0], bits8[1], bits8[2])
s1, c1 = fa(1, bits8[3], bits8[4], bits8[5])
s2, c2 = fa(2, bits8[6], bits8[7], 0.0)
S, C2 = fa(3, s0, s1, s2)
T, C4 = fa(4, c0, c1, c2)
U, C5 = fa(5, C2, T, 0.0)
V, W = fa(6, C4, C5, 0.0)
return [S, U, V, W] # LSB-first count bits
P = pcount("pcnt", p_bits)
N = pcount("ncnt", n_bits)
nnot = [self._g(f"rv32.neur.nnot.bit{k}", [N[k] if k < 4 else 0.0]) for k in range(6)]
carry = 1.0
diff = []
for k in range(6):
a_ = P[k] if k < 4 else 0.0
s, carry = self.alu._fa(f"rv32.neur.diff.fa{k}", a_, nnot[k], carry)
diff.append(s)
bias = (w_reg >> 16) & 0x1F
bias6 = [(bias >> k) & 1 if k < 4 else (bias >> 4) & 1 for k in range(6)] # sext5
carry = 0.0
act = []
for k in range(6):
s, carry = self.alu._fa(f"rv32.neur.act.fa{k}", diff[k], float(bias6[k]), carry)
act.append(s)
return int(self._g("rv32.neur.out", [act[5]]))
def hazard_eq(self, pair, i, j):
ib = int_to_bits(i, 5)
jb = int_to_bits(j, 5)
eqs = []
for k in range(5):
p = f"rv32.hazard.{pair}.bit{k}"
h_and = self._g(f"{p}.eq.layer1.and", [ib[k], jb[k]])
h_nor = self._g(f"{p}.eq.layer1.nor", [ib[k], jb[k]])
eqs.append(self._g(f"{p}.eq", [h_and, h_nor]))
return int(self._g(f"rv32.hazard.{pair}.all", eqs))
# ---- memory through the packed circuits ---------------------------
def _addr_decode(self, addr):
bits = torch.tensor(int_to_bits(addr, 16), dtype=torch.float32)
return heaviside((self.T["memory.addr_decode.weight"] * bits).sum(dim=1)
+ self.T["memory.addr_decode.bias"])
def mem_read_byte(self, mem_bits, addr):
sel = self._addr_decode(addr & 0xFFFF)
and_w = self.T["memory.read.and.weight"]
and_b = self.T["memory.read.and.bias"]
or_w = self.T["memory.read.or.weight"]
or_b = self.T["memory.read.or.bias"]
v = 0
for bit in range(8):
inp = torch.stack([mem_bits[:, bit], sel], dim=1)
and_out = heaviside((inp * and_w[bit]).sum(dim=1) + and_b[bit])
v = (v << 1) | int(heaviside((and_out * or_w[bit]).sum() + or_b[bit]).item())
return v
def mem_write_byte(self, mem_bits, addr, value):
sel = self._addr_decode(addr & 0xFFFF)
data_bits = torch.tensor(int_to_bits(value & 0xFF, 8), dtype=torch.float32)
we = torch.ones_like(sel)
write_sel = heaviside((torch.stack([sel, we], dim=1)
* self.T["memory.write.sel.weight"]).sum(dim=1)
+ self.T["memory.write.sel.bias"])
nsel = heaviside(write_sel * self.T["memory.write.nsel.weight"].squeeze(1)
+ self.T["memory.write.nsel.bias"])
for bit in range(8):
old = mem_bits[:, bit]
dbit = data_bits[bit].expand(65536)
a_old = heaviside((torch.stack([old, nsel], dim=1)
* self.T["memory.write.and_old.weight"][:, bit]).sum(dim=1)
+ self.T["memory.write.and_old.bias"][:, bit])
a_new = heaviside((torch.stack([dbit, write_sel], dim=1)
* self.T["memory.write.and_new.weight"][:, bit]).sum(dim=1)
+ self.T["memory.write.and_new.bias"][:, bit])
mem_bits[:, bit] = heaviside((torch.stack([a_old, a_new], dim=1)
* self.T["memory.write.or.weight"][:, bit]).sum(dim=1)
+ self.T["memory.write.or.bias"][:, bit])
# ---- execution ----------------------------------------------------
def _fetch32(self, mem_bits, pc):
return sum(self.mem_read_byte(mem_bits, pc + b) << (8 * b) for b in range(4))
def _load(self, s, addr, width, signed):
v = sum(self.mem_read_byte(s["_mem_bits"], (addr + b) & 0xFFFF) << (8 * b)
for b in range(width))
return u32(sext(v, width * 8)) if signed else v
def _store(self, s, addr, val, width):
for b in range(width):
byte = (val >> (8 * b)) & 0xFF
self.mem_write_byte(s["_mem_bits"], (addr + b) & 0xFFFF, byte)
s["mem"][(addr + b) & 0xFFFF] = byte
if (addr & 0xFFFF) == MMIO_CONSOLE:
s["console"] += chr(val & 0xFF)
def _exec_alu_class(self, s, pc, w):
"""Execute one OP / OP-IMM / LUI / AUIPC instruction; returns (rd, value)."""
op = w & 0x7F
rd = (w >> 7) & 31
f3 = (w >> 12) & 7
rs1 = (w >> 15) & 31
rs2 = (w >> 20) & 31
f7 = w >> 25
R = s["regs"]
a = R[rs1]
if op in (0x37, 0x17, 0x13):
_, imm_g = self._decode(w) # gate-decoded immediate
if op == 0x37:
return rd, imm_g
if op == 0x17:
return rd, self.add32(pc, imm_g)
if op == 0x13:
b = imm_g
sh = imm_g & 31
imm_mode = True
else:
b = R[rs2]
sh = b & 31
imm_mode = False
if f3 == 0:
if not imm_mode and f7 == 32:
return rd, self.sub32(a, b)
return rd, self.add32(a, b)
if f3 == 1:
return rd, self.sll(a, sh)
if f3 == 2:
return rd, self.scmp_lt(a, b)
if f3 == 3:
return rd, self.ucmp(a, b, "lessthan")
if f3 == 4:
return rd, self._bitwise("xor", a, b)
if f3 == 5:
return rd, (self.sra(a, sh) if f7 == 32 else self.srl(a, sh))
if f3 == 6:
return rd, self._bitwise("or", a, b)
return rd, self._bitwise("and", a, b)
def step(self, s):
if s["halted"]:
return s
s = dict(s, mem=s["mem"][:], regs=s["regs"][:], fregs=s["fregs"][:],
console=s["console"], _mem_bits=s["_mem_bits"].clone())
pc = s["pc"]
w = self._fetch32(s["_mem_bits"], pc)
op = w & 0x7F
def plain_alu(word):
return (word & 0x7F) in PAIRABLE and not \
((word & 0x7F) == 0x33 and (word >> 25) == 1)
# Dual issue: both slots plain ALU-class and hazard-free by gates.
if plain_alu(w):
w2 = self._fetch32(s["_mem_bits"], (pc + 4) & 0xFFFF)
if plain_alu(w2):
rd1 = (w >> 7) & 31
rs1b = (w2 >> 15) & 31
rs2b = (w2 >> 20) & 31
rd2 = (w2 >> 7) & 31
uses_rs1 = (w2 & 0x7F) in (0x33, 0x13)
uses_rs2 = (w2 & 0x7F) == 0x33
raw = (uses_rs1 and self.hazard_eq("rd1_rs1b", rd1, rs1b)) or \
(uses_rs2 and self.hazard_eq("rd1_rs2b", rd1, rs2b))
waw = self.hazard_eq("rd1_rd2", rd1, rd2)
if rd1 != 0 and not raw and not waw:
d1, v1 = self._exec_alu_class(s, pc, w)
d2, v2 = self._exec_alu_class(s, (pc + 4) & MASK32, w2)
if d1:
s["regs"][d1] = u32(v1)
if d2:
s["regs"][d2] = u32(v2)
s["pc"] = (pc + 8) & MASK32
self.pairs_issued += 1
return s
rd = (w >> 7) & 31
f3 = (w >> 12) & 7
rs1 = (w >> 15) & 31
rs2 = (w >> 20) & 31
f7 = w >> 25
R = s["regs"]
FR = s["fregs"]
a, b = R[rs1], R[rs2]
cls, imm_g = self._decode(w) # gate decode: classes + immediate
pc4 = self.add32(pc, 4) # PC+4 through the gate adder
branch_taken = 0
def wr(v):
if rd:
R[rd] = u32(v)
if plain_alu(w):
d, v = self._exec_alu_class(s, pc, w)
if d:
R[d] = u32(v)
if cls["op"] and f7 == 1:
if f3 == 0:
wr(self.alu.mul_n(a, b, 32))
elif f3 == 1:
wr(self.mulh(a, b, True, True))
elif f3 == 2:
wr(self.mulh(a, b, True, False))
elif f3 == 3:
wr(self.mulhu(a, b))
elif f3 == 4:
wr(self.divs(a, b)[0])
elif f3 == 5:
wr(self.divu(a, b)[0])
elif f3 == 6:
wr(self.divs(a, b)[1])
elif f3 == 7:
wr(self.divu(a, b)[1])
elif cls["jal"]:
wr(pc4) # return address
elif cls["jalr"]:
wr(pc4)
elif cls["branch"]:
eq = self.ucmp(a, b, "eq")
sl = self.scmp_lt(a, b)
ul = self.ucmp(a, b, "lessthan")
branch_taken = [eq, 1 - eq, 0, 0, sl, 1 - sl, ul, 1 - ul][f3]
elif cls["load"]:
addr = self.add32(a, imm_g) & 0xFFFF
width = [1, 2, 4, 0, 1, 2][f3]
wr(self._load(s, addr, width, f3 < 3))
elif cls["store"]:
addr = self.add32(a, imm_g) & 0xFFFF
self._store(s, addr, b, [1, 2, 4][f3])
elif cls["flw"]:
addr = self.add32(a, imm_g) & 0xFFFF
FR[rd] = self._load(s, addr, 4, False)
elif cls["fsw"]:
addr = self.add32(a, imm_g) & 0xFFFF
self._store(s, addr, FR[rs2], 4)
elif cls["fp"]:
fa, fb = FR[rs1], FR[rs2]
if f7 in (0x00, 0x04, 0x08, 0x0C):
circ = {0x00: "add", 0x04: "add", 0x08: "mul", 0x0C: "div"}[f7]
if f7 == 0x04:
fb ^= 0x80000000 # subtraction: flip the addend's sign
FR[rd] = self._fpu_word(circ, fa, fb)
elif f7 == 0x50:
out = self.fpu["cmp"].run(self._fpu_ext(fa, fb))
gate = {0: "le", 1: "lt", 2: "eq"}[f3]
wr(int(out[f"float32.cmp.{gate}.result"][0, 0].item()))
elif f7 == 0x10: # FSGNJ family: result = {sign, rs1[30:0]}
sa = (FR[rs1] >> 31) & 1
sb = (FR[rs2] >> 31) & 1
if f3 == 0:
s_out = sb # FSGNJ.S
elif f3 == 1:
s_out = self._not1(sb) # FSGNJN.S
else:
s_out = self._bitwise("xor", sa << 31, sb << 31) >> 31 # FSGNJX.S
FR[rd] = (FR[rs1] & 0x7FFFFFFF) | (s_out << 31)
elif f7 == 0x60: # FCVT.W.S: float -> signed int, gate-routed
wr(self.fcvt_w_s(FR[rs1], f3))
elif f7 == 0x68: # FCVT.S.W: signed int -> float, gate-routed
FR[rd] = self.fcvt_s_w(R[rs1])
elif f7 == 0x78:
FR[rd] = a
elif f7 == 0x70:
wr(FR[rs1])
elif cls["fma"]: # FMADD/FMSUB/FNMSUB/FNMADD.S
rs3 = (w >> 27) & 0x1F
fa, fb, fc = FR[rs1], FR[rs2], FR[rs3]
if op in (0x4b, 0x4f): # negate the product
fa ^= 0x80000000
if op in (0x47, 0x4f): # negate the addend
fc ^= 0x80000000
FR[rd] = self._fpu_word3(fa, fb, fc)
elif op == 0x0B:
wr(self.neur(a, b))
elif cls["system"]:
s["halted"] = True
elif op == 0x0F:
pass
elif op not in PAIRABLE:
raise ValueError(f"illegal instruction {w:#010x} at {pc:#06x}")
# PC sequencing through the gate mux: pc4 / pc+imm (branch,jal) / (rs1+imm)&~1 (jalr).
pcimm = self.add32(pc, imm_g)
jalr_t = self.add32(a, imm_g) & ~1
sel_target = 1 if (branch_taken or cls["jal"]) else 0
s["pc"] = self._pcnext(pc4, pcimm, jalr_t, sel_target, cls["jalr"])
return s
def _fpu_ext(self, fa, fb):
ext = {}
for i in range(32):
ext[f"$a[{i}]"] = (fa >> (31 - i)) & 1
ext[f"$b[{i}]"] = (fb >> (31 - i)) & 1
return ext
def _fpu_word(self, circ, fa, fb):
out = self.fpu[circ].run(self._fpu_ext(fa, fb))
p = f"float32.{circ}"
v = int(out[f"{p}.sign_out"][0, 0].item()) << 31
for k in range(8):
v |= int(out[f"{p}.exp_out.bit{k}"][0, 0].item()) << (23 + k)
for k in range(23):
v |= int(out[f"{p}.frac_out.bit{k}"][0, 0].item()) << k
return v
def _fpu_ext3(self, fa, fb, fc):
ext = {}
for i in range(32):
ext[f"$a[{i}]"] = (fa >> (31 - i)) & 1
ext[f"$b[{i}]"] = (fb >> (31 - i)) & 1
ext[f"$c[{i}]"] = (fc >> (31 - i)) & 1
return ext
def _fpu_word3(self, fa, fb, fc):
out = self.fpu["fma"].run(self._fpu_ext3(fa, fb, fc))
p = "float32.fma"
v = int(out[f"{p}.sign_out"][0, 0].item()) << 31
for k in range(8):
v |= int(out[f"{p}.exp_out.bit{k}"][0, 0].item()) << (23 + k)
for k in range(23):
v |= int(out[f"{p}.frac_out.bit{k}"][0, 0].item()) << k
return v
def _decode(self, w):
"""Instruction decode through the gate network: opcode-class one-hots
and the sign-extended immediate for the active format."""
ext = {f"$rv32_instr[{k}]": (w >> k) & 1 for k in range(32)}
out = self.decode.run(ext)
cls = {n: int(out[f"rv32.decode.is_{n}"][0, 0].item()) for n in DECODE_CLASSES}
imm = 0
for k in range(32):
imm |= int(out[f"rv32.decode.imm.bit{k}"][0, 0].item()) << k
return cls, imm
def _pcnext(self, pc4, pcimm, jalr, sel_target, sel_jalr):
"""Next-PC selection through the gate mux (pc4 / pc+imm / (rs1+imm)&~1)."""
ext = {"$rv32_sel_target": sel_target, "$rv32_sel_jalr": sel_jalr}
for k in range(32):
ext[f"$rv32_pc4[{k}]"] = (pc4 >> k) & 1
ext[f"$rv32_pcimm[{k}]"] = (pcimm >> k) & 1
ext[f"$rv32_jalr[{k}]"] = (jalr >> k) & 1
out = self.pcseq.run(ext)
v = 0
for k in range(32):
v |= int(out[f"rv32.pcnext.bit{k}.or"][0, 0].item()) << k
return v
def run(self, state, max_cycles=400):
s = dict(state)
s["_mem_bits"] = torch.tensor(
[int_to_bits(byte, 8) for byte in s["mem"]], dtype=torch.float32)
n = 0
while not s["halted"] and n < max_cycles:
s = self.step(s)
n += 1
s.pop("_mem_bits", None)
return s, n
# ============================================================================
# neural_subleq8: threshold runtime, reference, assembler
# ============================================================================
SUBLEQ_MODEL = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
"variants", "neural_subleq8.safetensors")
HALT_PC = 0xFF
def load_tensors(path):
out = {}
with safe_open(path, framework="pt") as f:
for name in f.keys():
out[name] = f.get_tensor(name).float()
return out
class SubleqThresholdCPU:
"""SUBLEQ-8 runtime: memory traffic through the packed memory circuits,
the subtract/branch datapath through the subleq gates via the shipped
netlist. Instruction sequencing (three consecutive fetches) is wiring."""
def __init__(self, tensors, registry):
self.T = tensors
self.ne = NetlistEvaluator(tensors, registry, "subleq")
def _addr_decode(self, addr):
bits = torch.tensor([(addr >> (7 - i)) & 1 for i in range(8)],
dtype=torch.float32)
w = self.T["memory.addr_decode.weight"]
b = self.T["memory.addr_decode.bias"]
return heaviside((w * bits).sum(dim=1) + b)
def mem_read(self, mem, addr):
sel = self._addr_decode(addr)
mem_bits = torch.tensor([[(byte >> (7 - i)) & 1 for i in range(8)]
for byte in mem], dtype=torch.float32)
and_w = self.T["memory.read.and.weight"]
and_b = self.T["memory.read.and.bias"]
or_w = self.T["memory.read.or.weight"]
or_b = self.T["memory.read.or.bias"]
out = 0
for bit in range(8):
inp = torch.stack([mem_bits[:, bit], sel], dim=1)
and_out = heaviside((inp * and_w[bit]).sum(dim=1) + and_b[bit])
out = (out << 1) | int(heaviside((and_out * or_w[bit]).sum() + or_b[bit]).item())
return out
def mem_write(self, mem, addr, value):
sel = self._addr_decode(addr)
data_bits = torch.tensor([(value >> (7 - i)) & 1 for i in range(8)],
dtype=torch.float32)
mem_bits = torch.tensor([[(byte >> (7 - i)) & 1 for i in range(8)]
for byte in mem], dtype=torch.float32)
sel_w = self.T["memory.write.sel.weight"]
sel_b = self.T["memory.write.sel.bias"]
nsel_w = self.T["memory.write.nsel.weight"].squeeze(1)
nsel_b = self.T["memory.write.nsel.bias"]
we = torch.ones_like(sel)
write_sel = heaviside((torch.stack([sel, we], dim=1) * sel_w).sum(dim=1) + sel_b)
nsel = heaviside(write_sel * nsel_w + nsel_b)
ao_w = self.T["memory.write.and_old.weight"]
ao_b = self.T["memory.write.and_old.bias"]
an_w = self.T["memory.write.and_new.weight"]
an_b = self.T["memory.write.and_new.bias"]
or_w = self.T["memory.write.or.weight"]
or_b = self.T["memory.write.or.bias"]
for bit in range(8):
old = mem_bits[:, bit]
dbit = data_bits[bit].expand(256)
a_old = heaviside((torch.stack([old, nsel], dim=1) * ao_w[:, bit]).sum(dim=1) + ao_b[:, bit])
a_new = heaviside((torch.stack([dbit, write_sel], dim=1) * an_w[:, bit]).sum(dim=1) + an_b[:, bit])
mem_bits[:, bit] = heaviside(
(torch.stack([a_old, a_new], dim=1) * or_w[:, bit]).sum(dim=1) + or_b[:, bit])
return [int(sum(int(mem_bits[i, b].item()) << (7 - b) for b in range(8)))
for i in range(256)]
def step(self, state):
if state["halted"]:
return state
s = dict(state)
s["mem"] = state["mem"][:]
pc = s["pc"]
A = self.mem_read(s["mem"], pc)
B = self.mem_read(s["mem"], (pc + 1) & 0xFF)
C = self.mem_read(s["mem"], (pc + 2) & 0xFF)
a = self.mem_read(s["mem"], A)
b = self.mem_read(s["mem"], B)
ext = {}
for k in range(8):
ext[f"$a[{k}]"] = (a >> k) & 1
ext[f"$b[{k}]"] = (b >> k) & 1
ext[f"$pc[{k}]"] = (pc >> k) & 1
ext[f"$c[{k}]"] = (C >> k) & 1
out = self.ne.run(ext)
r = sum(int(out[f"subleq.sub.fa{k}.ha2.sum.layer2"][0, 0].item()) << k
for k in range(8))
next_pc = sum(int(out[f"subleq.pc_mux.bit{k}.or"][0, 0].item()) << k
for k in range(8))
s["mem"] = self.mem_write(s["mem"], B, r)
s["pc"] = next_pc
if next_pc == HALT_PC:
s["halted"] = True
return s
def run(self, state, max_cycles=200):
s = state
n = 0
while not s["halted"] and n < max_cycles:
s = self.step(s)
n += 1
return s, n
def ref_step(state):
if state["halted"]:
return state
s = dict(state)
s["mem"] = state["mem"][:]
pc = s["pc"]
A, B, C = s["mem"][pc], s["mem"][(pc + 1) & 0xFF], s["mem"][(pc + 2) & 0xFF]
r = (s["mem"][B] - s["mem"][A]) & 0xFF
s["mem"][B] = r
s["pc"] = C if (r == 0 or r >= 0x80) else (pc + 3) & 0xFF
if s["pc"] == HALT_PC:
s["halted"] = True
return s
def sq(*triples):
"""Assemble SUBLEQ triples into a 256-byte image."""
mem = [0] * 256
i = 0
for t in triples:
for v in t:
mem[i] = v & 0xFF
i += 1
return mem
def subleq_test() -> int:
T = load_tensors(SUBLEQ_MODEL)
reg = load_metadata(SUBLEQ_MODEL)["signal_registry"]
# 1. Exhaustive datapath: all 65,536 (a, b) pairs through the shipped
# wiring in one batched netlist evaluation.
ne = NetlistEvaluator(T, reg, "subleq")
a_v = torch.arange(65536, dtype=torch.long) % 256
b_v = torch.arange(65536, dtype=torch.long) // 256
ext = {}
for k in range(8):
ext[f"$a[{k}]"] = ((a_v >> k) & 1).float()
ext[f"$b[{k}]"] = ((b_v >> k) & 1).float()
ext[f"$pc[{k}]"] = torch.zeros(65536)
ext[f"$c[{k}]"] = torch.zeros(65536)
out = ne.run(ext)
r = torch.zeros(65536, dtype=torch.float64)
for k in range(8):
r += out[f"subleq.sub.fa{k}.ha2.sum.layer2"][:, 0].double() * (1 << k)
exp_r = ((b_v - a_v) & 0xFF).double()
leq = out["subleq.leq"][:, 0].double()
exp_leq = ((exp_r == 0) | (exp_r >= 128)).double()
ok_r = int((r == exp_r).sum().item())
ok_l = int((leq == exp_leq).sum().item())
print(f"datapath sub exhaustive: {ok_r}/65536")
print(f"datapath leq exhaustive: {ok_l}/65536")
if ok_r != 65536 or ok_l != 65536:
return 1
# 2. PC mux + increment across representative PC/target/leq settings.
pcs = torch.tensor([0, 3, 5, 100, 250, 252], dtype=torch.long).repeat_interleave(2)
cs = torch.tensor([7, 7, 42, 42, 255, 255, 0, 0, 9, 9, 30, 30], dtype=torch.long)
a2 = torch.tensor([0, 1] * 6, dtype=torch.long) # a=0,b=0 -> leq; a=1,b=0 -> r=255 leq... use b to vary
b2 = torch.tensor([0, 1] * 6, dtype=torch.long) # (0,0)->r0 leq taken; (1,1)->r0 leq taken
# craft: even rows leq taken (a=b), odd rows not taken (b-a=1 -> r=1 >0)
a2 = torch.tensor([5, 4] * 6, dtype=torch.long)
b2 = torch.tensor([5, 5] * 6, dtype=torch.long)
ext = {}
for k in range(8):
ext[f"$a[{k}]"] = ((a2 >> k) & 1).float()
ext[f"$b[{k}]"] = ((b2 >> k) & 1).float()
ext[f"$pc[{k}]"] = ((pcs >> k) & 1).float()
ext[f"$c[{k}]"] = ((cs >> k) & 1).float()
out = ne.run(ext)
npc = torch.zeros(12, dtype=torch.float64)
for k in range(8):
npc += out[f"subleq.pc_mux.bit{k}.or"][:, 0].double() * (1 << k)
exp_npc = torch.where(torch.arange(12) % 2 == 0, cs.double(), ((pcs + 3) & 0xFF).double())
ok_pc = int((npc == exp_npc).sum().item())
print(f"datapath pc mux/inc : {ok_pc}/12")
if ok_pc != 12:
return 1
# 3. Program suite, threshold vs reference, full-state lockstep.
cpu = SubleqThresholdCPU(T, reg)
Z = 0x80 # scratch zero cell
progs = []
# negate-copy: M[dst] = -M[src] (dst pre-zeroed): subleq src, dst; halt.
m = sq((0x90, 0x91, 3), (Z, Z, HALT_PC))
m[0x90], m[0x91] = 42, 0
progs.append(("neg_copy", m, 30, lambda mm: mm[0x91] == ((-42) & 0xFF)))
# add: M[d] += M[s] via double negation through Z.
m = sq((0x90, Z, 3), (Z, 0x91, 6), (Z, Z, 9), (Z, Z, HALT_PC))
m[0x90], m[0x91] = 17, 25
progs.append(("add", m, 30, lambda mm: mm[0x91] == 42))
# countdown loop: decrement M[n] by M[one] until <= 0, counting into 0x92
# via add idiom is long; simplest loop: subleq one, n, done at 0.
m = sq((0x93, 0x90, 6), (Z, Z, 0), (Z, Z, HALT_PC))
m[0x90], m[0x93] = 5, 1
progs.append(("countdown", m, 60, lambda mm: mm[0x90] == 0))
# halt-immediately
m = sq((Z, Z, HALT_PC))
progs.append(("halt", m, 5, lambda mm: True))
all_ok = True
for name, mem, budget, check in progs:
ts = {"pc": 0, "mem": mem[:], "halted": False}
rs = {"pc": 0, "mem": mem[:], "halted": False}
cyc = 0
ok = True
while not ts["halted"] and cyc < budget:
ts = cpu.step(ts)
rs = ref_step(rs)
cyc += 1
if ts["pc"] != rs["pc"] or ts["mem"] != rs["mem"]:
ok = False
break
ok = ok and ts["halted"] and rs["halted"] and check(ts["mem"])
all_ok &= ok
print(f" {name:12} {'PASS' if ok else 'FAIL'} ({cyc} cyc, lockstep)")
print("SUBLEQ:", "PASS" if all_ok else "FAIL")
return 0 if all_ok else 1
# ============================================================================
# neural_rv32 test suites
# ============================================================================
F1_0 = struct.unpack("<I", struct.pack("<f", 1.5))[0]
F2_5 = struct.unpack("<I", struct.pack("<f", 2.5))[0]
F10 = struct.unpack("<I", struct.pack("<f", 10.0))[0]
def lockstep(cpu, mem, budget, name, check=None):
ts = rv_state(mem)
rs = rv_state(mem)
import torch
from eval_all import int_to_bits
ts["_mem_bits"] = torch.tensor([int_to_bits(b, 8) for b in ts["mem"]],
dtype=torch.float32)
cyc = 0
ok = True
while not ts["halted"] and cyc < budget:
ts = cpu.step(ts)
# reference is strictly single-issue; advance it until PCs realign
rs = RefRV32.step(rs)
if rs["pc"] != ts["pc"] and not rs["halted"]:
rs = RefRV32.step(rs)
cyc += 1
if (ts["pc"] != rs["pc"] or ts["regs"] != rs["regs"]
or ts["fregs"] != rs["fregs"] or ts["halted"] != rs["halted"]):
ok = False
break
if ok:
ok = ts["halted"] and ts["mem"] == rs["mem"] and ts["console"] == rs["console"]
if ok and check is not None:
ok = check(ts)
print(f" {name:22} {'PASS' if ok else 'FAIL'} ({cyc} cyc)")
if not ok:
print(f" pc t={ts['pc']:#x} r={rs['pc']:#x} halted t={ts['halted']} r={rs['halted']}")
for i in range(32):
if ts["regs"][i] != rs["regs"][i]:
print(f" x{i}: t={ts['regs'][i]:#x} r={rs['regs'][i]:#x}")
return ok
def prog_int_alu():
a = Asm()
a.addi(1, 0, 100) # x1 = 100
a.addi(2, 0, -7) # x2 = -7
a.add(3, 1, 2) # 93
a.sub(4, 1, 2) # 107
a.xor(5, 1, 2)
a.or_(6, 1, 2)
a.and_(7, 1, 2)
a.slt(8, 2, 1) # 1 (signed)
a.sltu(9, 2, 1) # 0 (unsigned: -7 is huge)
a.slli(10, 1, 3) # 800
a.srli(11, 2, 4)
a.srai(12, 2, 4) # -1
a.slti(13, 2, 0) # 1
a.sltiu(14, 1, 200) # 1
a.xori(15, 1, 0xFF)
a.ori(16, 1, 0x0F)
a.andi(17, 1, 0x3C)
a.lui(18, 0x12345)
a.auipc(19, 1)
a.ecall()
return a.assemble(), 40, lambda s: s["regs"][3] == 93 and s["regs"][12] == u32(-1)
def prog_branches():
a = Asm()
a.addi(1, 0, 5)
a.addi(2, 0, -3)
a.addi(10, 0, 0)
a.blt(2, 1, "l1") # signed taken
a.addi(10, 10, 100)
a.label("l1")
a.bltu(1, 2, "l2") # unsigned taken (5 < 0xFFFF..FD)
a.addi(10, 10, 200)
a.label("l2")
a.beq(1, 1, "l3")
a.addi(10, 10, 400)
a.label("l3")
a.bge(1, 2, "l4") # taken
a.addi(10, 10, 800)
a.label("l4")
a.bne(1, 2, "done")
a.addi(10, 10, 1600)
a.label("done")
a.ecall()
return a.assemble(), 30, lambda s: s["regs"][10] == 0
def prog_fib():
a = Asm()
a.addi(1, 0, 0) # a
a.addi(2, 0, 1) # b
a.addi(3, 0, 10) # n
a.label("loop")
a.beq(3, 0, "done")
a.add(4, 1, 2)
a.add(1, 0, 2) # a = b (independent pair candidates)
a.add(2, 0, 4) # b = t
a.addi(3, 3, -1)
a.jal(0, "loop")
a.label("done")
a.ecall()
return a.assemble(), 200, lambda s: s["regs"][1] == 55
def prog_mem_call():
a = Asm()
a.addi(2, 0, 0x800) # sp
a.addi(1, 0, 0x77)
a.sb(1, 0, 0x400)
a.lb(3, 0, 0x400)
a.lbu(4, 0, 0x400)
a.addi(1, 0, -2)
a.sh(1, 0, 0x404)
a.lh(5, 0, 0x404) # sign-extended
a.lhu(6, 0, 0x404)
a.jal(1, "fn") # call
a.ecall()
a.label("fn")
a.addi(2, 2, -4)
a.sw(1, 2, 0) # push ra
a.lw(7, 2, 0)
a.addi(2, 2, 4)
a.jalr(0, 1, 0) # ret
return a.assemble(), 40, lambda s: s["regs"][5] == u32(-2) and s["regs"][6] == 0xFFFE
def prog_muldiv():
a = Asm()
a.addi(1, 0, -50)
a.addi(2, 0, 7)
a.mul(3, 1, 2) # -350
a.mulh(4, 1, 2) # high of signed product = -1
a.mulhu(5, 1, 2)
a.div(6, 1, 2) # -7
a.rem(7, 1, 2) # -1
a.divu(8, 1, 2)
a.remu(9, 1, 2)
a.addi(10, 0, 0)
a.div(11, 1, 10) # div by zero -> -1
a.rem(12, 1, 10) # rem by zero -> dividend
a.ecall()
return a.assemble(), 30, lambda s: (s["regs"][3] == u32(-350)
and s["regs"][6] == u32(-7)
and s["regs"][7] == u32(-1)
and s["regs"][11] == 0xFFFFFFFF)
def prog_float():
a = Asm()
a.lui(1, 0x1) # x1 = 0x1000 data base
a.flw(1, 1, 0) # f1 = 1.5
a.flw(2, 1, 4) # f2 = 2.5
a.fadd_s(3, 1, 2) # 4.0
a.fmul_s(4, 1, 2) # 3.75
a.fdiv_s(5, 2, 1) # 2.5 / 1.5
a.fsub_s(6, 2, 1) # 1.0
a.fsw(3, 1, 8)
a.fle_s(10, 1, 2) # 1
a.flt_s(11, 2, 1) # 0
a.feq_s(12, 1, 1) # 1
a.fmv_x_w(13, 3)
a.ecall()
mem = a.assemble()
for i, wv in ((0, F1_0), (4, F2_5)):
for b in range(4):
mem[0x1000 + i + b] = (wv >> (8 * b)) & 0xFF
f4 = struct.unpack("<I", struct.pack("<f", 4.0))[0]
return mem, 30, lambda s: (s["regs"][10] == 1 and s["regs"][11] == 0
and s["regs"][12] == 1 and s["regs"][13] == f4)
def prog_fma():
a = Asm()
a.lui(1, 0x1)
a.flw(1, 1, 0) # f1 = 1.5
a.flw(2, 1, 4) # f2 = 2.5
a.flw(3, 1, 8) # f3 = 3.0
a.fmadd_s(4, 1, 2, 3) # 1.5*2.5 + 3.0 = 6.75
a.fmsub_s(5, 1, 2, 3) # 1.5*2.5 - 3.0 = 0.75
a.fnmadd_s(6, 1, 2, 3) # -(1.5*2.5) - 3.0 = -6.75
a.fnmsub_s(7, 1, 2, 3) # -(1.5*2.5) + 3.0 = -0.75
a.fmv_x_w(10, 4)
a.fmv_x_w(11, 5)
a.fmv_x_w(12, 6)
a.fmv_x_w(13, 7)
a.ecall()
mem = a.assemble()
f3_0 = struct.unpack("<I", struct.pack("<f", 3.0))[0]
for i, wv in ((0, F1_0), (4, F2_5), (8, f3_0)):
for b in range(4):
mem[0x1000 + i + b] = (wv >> (8 * b)) & 0xFF
def word(x):
return struct.unpack("<I", struct.pack("<f", x))[0]
return mem, 30, lambda s: (s["regs"][10] == word(6.75) and s["regs"][11] == word(0.75)
and s["regs"][12] == word(-6.75) and s["regs"][13] == word(-0.75))
def prog_neur():
# XOR of two bits computed by a two-layer network of NEUR instructions:
# h1 = OR(a,b) = H(a + b - 1); h2 = NAND(a,b) = H(-a - b + 1);
# y = AND(h1,h2) = H(h1 + h2 - 2).
def const(a, reg, v):
a.lui(reg, v >> 12)
a.ori(reg, reg, v & 0x7FF)
a = Asm()
a.addi(1, 0, 0b10) # inputs: bit1=a=1, bit0=b=0
const(a, 2, (0x1F << 16) | 0x03) # OR: pos=both, bias=-1 (0x1F sext5)
a.neur(3, 1, 2)
const(a, 4, (0x01 << 16) | (0x03 << 8)) # NAND: neg=both, bias=+1
a.neur(5, 1, 4)
a.slli(6, 3, 1)
a.or_(6, 6, 5) # pack h1,h2 into bits 1,0
const(a, 7, (0x1E << 16) | 0x03) # AND: pos=both, bias=-2 (0x1E sext5)
a.neur(8, 6, 7) # y = XOR(a,b) = 1
a.ecall()
return a.assemble(), 20, lambda s: s["regs"][8] == 1 and s["regs"][3] == 1 and s["regs"][5] == 1
def prog_mmio():
a = Asm()
a.lui(1, 0x10) # 0x10000 > 0xFFFF? keep in range: use addi chain
a.addi(1, 0, 0x7F8)
a.slli(1, 1, 5) # 0xFF00
a.addi(2, 0, ord("H"))
a.sb(2, 1, 0)
a.addi(2, 0, ord("I"))
a.sb(2, 1, 0)
a.ecall()
return a.assemble(), 20, lambda s: s["console"] == "HI"
def prog_fcvt():
# int -> float, add 1.0, float -> int: (7 + 1.0) truncated = 8; and a
# round-trip of -5 through float and back = -5.
import struct as _st
a = Asm()
a.addi(1, 0, 7)
a.fcvt_s_w(0, 1) # f0 = 7.0
a.lui(2, 0x1); a.flw(1, 2, 0) # f1 = 1.0 from data
a.fadd_s(2, 0, 1) # f2 = 8.0
a.fcvt_w_s(3, 2) # x3 = 8
a.addi(4, 0, -5)
a.fcvt_s_w(4, 4) # f4 = -5.0
a.fcvt_w_s(5, 4) # x5 = -5
a.ecall()
mem = a.assemble()
one = _st.unpack("<I", _st.pack("<f", 1.0))[0]
for b in range(4):
mem[0x1000 + b] = (one >> (8 * b)) & 0xFF
return mem, 30, lambda s: s["regs"][3] == 8 and s["regs"][5] == (-5 & 0xFFFFFFFF)
def prog_fcvt_rm():
# FCVT.W.S honors the instruction's rounding-mode field. Convert 2.5, 3.5,
# -2.5, 2.75 under several modes and check each against the IEEE result.
import struct as _st
a = Asm()
a.lui(10, 0x1) # x10 = 0x1000 (data base)
a.flw(1, 10, 0) # f1 = 2.5
a.flw(2, 10, 4) # f2 = 3.5
a.flw(3, 10, 8) # f3 = -2.5
a.flw(4, 10, 12) # f4 = 2.75
a.fcvt_w_s(1, 1, "rup") # x1 = ceil(2.5) = 3
a.fcvt_w_s(2, 2, "rne") # x2 = 3.5 ties to even = 4
a.fcvt_w_s(3, 1, "rne") # x3 = 2.5 ties to even = 2
a.fcvt_w_s(4, 3, "rdn") # x4 = floor(-2.5) = -3
a.fcvt_w_s(5, 4, "rtz") # x5 = trunc(2.75) = 2
a.fcvt_w_s(6, 1, "rmm") # x6 = 2.5 ties away = 3
a.ecall()
mem = a.assemble()
for i, v in enumerate((2.5, 3.5, -2.5, 2.75)):
wb = _st.unpack("<I", _st.pack("<f", v))[0]
for b in range(4):
mem[0x1000 + i * 4 + b] = (wb >> (8 * b)) & 0xFF
return mem, 40, lambda s: (s["regs"][1] == 3 and s["regs"][2] == 4
and s["regs"][3] == 2 and s["regs"][4] == (-3 & 0xFFFFFFFF)
and s["regs"][5] == 2 and s["regs"][6] == 3)
def rv32_netlist_check() -> bool:
"""Verify the rv32-specific sub-circuits (NEUR, hazard, MULHU accumulator)
are recoverable and correct from the shipped .inputs metadata alone."""
import random
T = {}
with safe_open(RV32_MODEL, framework="pt") as f:
for name in f.keys():
T[name] = f.get_tensor(name).float()
reg = load_metadata(RV32_MODEL)["signal_registry"]
rng = random.Random(0)
ok = True
ne = NetlistEvaluator(T, reg, "rv32.neur")
N = 512
xs = [rng.getrandbits(8) for _ in range(N)]
ps = [rng.getrandbits(8) for _ in range(N)]
ns = [rng.getrandbits(8) for _ in range(N)]
bs = [rng.randint(-16, 15) for _ in range(N)]
ext = {}
for i in range(8):
ext[f"$rv32_neur_x[{i}]"] = torch.tensor([(x >> (7 - i)) & 1 for x in xs]).float()
ext[f"$rv32_neur_pos[{i}]"] = torch.tensor([(p >> (7 - i)) & 1 for p in ps]).float()
ext[f"$rv32_neur_neg[{i}]"] = torch.tensor([(n >> (7 - i)) & 1 for n in ns]).float()
for k in range(6):
ext[f"$rv32_neur_bias[{k}]"] = torch.tensor(
[((b & 0x1F) >> k) & 1 if k < 5 else (b >> 4) & 1 for b in bs]).float()
got = ne.run(ext)["rv32.neur.out"][:, 0]
exp = torch.tensor([1.0 if (bin(x & p).count("1") - bin(x & n).count("1") + b) >= 0
else 0.0 for x, p, n, b in zip(xs, ps, ns, bs)])
ok &= bool((got == exp).all())
ne = NetlistEvaluator(T, reg, "rv32.hazard.rd1_rs1b")
ii = [rng.getrandbits(5) for _ in range(N)]
jj = [rng.getrandbits(5) for _ in range(N)]
ext = {}
for k in range(5):
ext[f"$rv32_hz_rd1_rs1b_i[{k}]"] = torch.tensor([(i >> k) & 1 for i in ii]).float()
ext[f"$rv32_hz_rd1_rs1b_j[{k}]"] = torch.tensor([(j >> k) & 1 for j in jj]).float()
got = ne.run(ext)["rv32.hazard.rd1_rs1b.all"][:, 0]
exp = torch.tensor([1.0 if i == j else 0.0 for i, j in zip(ii, jj)])
ok &= bool((got == exp).all())
ne = NetlistEvaluator(T, reg, "rv32.mulacc")
A = [rng.getrandbits(32) for _ in range(256)]
B = [rng.getrandbits(32) for _ in range(256)]
ext = {}
for I in range(32):
for J in range(32):
ext[f"alu.alu32bit.mul.pp.a{I}b{J}"] = torch.tensor(
[((a >> (31 - I)) & 1) & ((b >> (31 - J)) & 1) for a, b in zip(A, B)]).float()
out = ne.run(ext)
got = torch.zeros(len(A), dtype=torch.float64)
for k in range(32):
got += out[f"rv32.mulacc.s30.fa{32 + k}.ha2.sum.layer2"][:, 0].double() * (2.0 ** k)
exp = torch.tensor([float(((a * b) >> 32) & 0xFFFFFFFF) for a, b in zip(A, B)],
dtype=torch.float64)
ok &= bool((got == exp).all())
print(f" rv32 netlist self-check (NEUR/hazard/MULHU from metadata) "
f"{'PASS' if ok else 'FAIL'}")
return ok
def fcvt_rm_check(cpu) -> bool:
"""FCVT.W.S under every rounding mode against an exact rational reference,
for directed edge encodings (subnormals, ties, saturation) and randoms;
the emulator reference and the gate-routed method must both match."""
from fractions import Fraction
import random
def value(w):
s, e, f = (w >> 31) & 1, (w >> 23) & 0xFF, w & 0x7FFFFF
if e == 0:
v = Fraction(f, (1 << 23) * (1 << 126))
else:
v = Fraction((1 << 23) | f, 1 << 23) * (Fraction(2) ** (e - 127))
return -v if s else v
def round_frac(v, rm):
fl = v.numerator // v.denominator # floor (correct for negatives)
frac = v - fl
if frac == 0:
return fl
if rm == 1: # RTZ
return fl + 1 if v < 0 else fl
if rm == 2: # RDN
return fl
if rm == 3: # RUP
return fl + 1
if frac < Fraction(1, 2):
return fl
if frac > Fraction(1, 2):
return fl + 1
if rm == 4: # RMM: ties away from zero
return fl + 1 if v > 0 else fl
return fl if fl % 2 == 0 else fl + 1 # RNE: ties to even
def sat(iv):
if iv > 0x7FFFFFFF:
return 0x7FFFFFFF
if iv < -0x80000000:
return 0x80000000
return iv & MASK32
words = []
for s in (0, 1):
for e in (0, 1, 100, 125, 126, 127, 128, 130, 149, 150, 157, 158, 254, 255):
for f in (0, 1, 0x400000, 0x7FFFFF):
words.append((s << 31) | (e << 23) | f)
rng = random.Random(0xFCF7)
words += [rng.getrandbits(32) for _ in range(600)]
bad = None
for w in words:
s, e, f = (w >> 31) & 1, (w >> 23) & 0xFF, w & 0x7FFFFF
for rm in (0, 1, 2, 3, 4):
if e == 0xFF:
exp = 0x7FFFFFFF if f else (0x80000000 if s else 0x7FFFFFFF)
else:
exp = sat(round_frac(value(w), rm))
ref = fcvt_w_s_ref(w, rm)
gate = cpu.fcvt_w_s(w, rm)
if not (ref == gate == exp):
bad = (w, rm, ref, gate, exp)
break
if bad:
break
if bad:
w, rm, ref, gate, exp = bad
print(f" fcvt rounding-mode check FAIL w={w:08x} rm={rm} "
f"ref={ref:08x} gate={gate:08x} exp={exp:08x}")
return False
print(f" fcvt rounding-mode check ({len(words)} words x 5 modes vs exact) PASS")
return True
def rv32_test() -> int:
print("Loading neural_rv32...")
cpu = Rv32ThresholdCPU()
suite = [
("int_alu", prog_int_alu),
("branches", prog_branches),
("fib", prog_fib),
("mem_call", prog_mem_call),
("muldiv", prog_muldiv),
("float_subset", prog_float),
("fma", prog_fma),
("fcvt_roundtrip", prog_fcvt),
("fcvt_rounding_modes", prog_fcvt_rm),
("neur_xor_net", prog_neur),
("mmio_print", prog_mmio),
]
all_ok = True
for name, builder in suite:
mem, budget, check = builder()
all_ok &= lockstep(cpu, mem, budget, name, check)
all_ok &= rv32_netlist_check()
all_ok &= fcvt_rm_check(cpu)
print(f"dual-issue pairs retired: {cpu.pairs_issued}")
print("RV32:", "PASS" if all_ok else "FAIL")
return 0 if all_ok else 1
PROG = r"""
__attribute__((naked)) void _start(void){
__asm__ volatile("li sp, 0xF000\n call main\n ecall\n");
}
static int gcd(int a,int b){ while(b){ int t=a%b; a=b; b=t;} return a; }
int fib(int n){ int a=0,b=1; for(int i=0;i<n;i++){int t=a+b;a=b;b=t;} return a; }
int main(void){
int arr[8]={5,2,9,1,7,3,8,4};
for(int i=0;i<8;i++) for(int j=i+1;j<8;j++)
if(arr[j]<arr[i]){int t=arr[i];arr[i]=arr[j];arr[j]=t;}
int s=0; for(int i=0;i<8;i++) s+=arr[i]*(i+1);
return gcd(48,36) + fib(10) + s + (arr[0]==1?1000:0);
}
"""
EXPECTED = 1292 # gcd(48,36)=12, fib(10)=55, weighted sorted sum=225... verified natively
def have_riscv_clang() -> bool:
if not shutil.which("clang"):
return False
with tempfile.TemporaryDirectory() as d:
c = os.path.join(d, "t.c")
o = os.path.join(d, "t.o")
open(c, "w").write("int f(void){return 1;}")
r = subprocess.run(
["clang", "-target", "riscv32-unknown-elf", "-march=rv32im",
"-mabi=ilp32", "-c", c, "-o", o],
capture_output=True)
return r.returncode == 0
def rv32c_test() -> int:
if not have_riscv_clang():
print("SKIP: no riscv32 clang target available")
return 0
with tempfile.TemporaryDirectory() as d:
c = os.path.join(d, "prog.c")
o = os.path.join(d, "prog.o")
open(c, "w").write(PROG)
subprocess.run(
["clang", "-target", "riscv32-unknown-elf", "-march=rv32im",
"-mabi=ilp32", "-O1", "-nostdlib", "-ffreestanding",
"-c", c, "-o", o], check=True, capture_output=True)
obj = open(o, "rb").read()
mem, entry = load(obj, base=0, mem_size=65536)
cpu = Rv32ThresholdCPU()
st = rv_state(mem)
st["pc"] = entry
fin, cyc = cpu.run(st, max_cycles=5000)
got = fin["regs"][10]
ok = fin["halted"] and got == EXPECTED
print(f"compiled C (gcd+fib+sort, rv32im): a0={got} "
f"expected {EXPECTED} {cyc} instrs {'PASS' if ok else 'FAIL'}")
return 0 if ok else 1
# ============================================================================
# Entry point
# ============================================================================
def main() -> int:
ap = argparse.ArgumentParser(description="neural_subleq8 / neural_rv32 test suites")
ap.add_argument("which", nargs="?", default="all",
choices=["all", "subleq", "rv32", "rv32-c"])
args = ap.parse_args()
rc = 0
if args.which in ("all", "subleq"):
rc |= subleq_test()
if args.which in ("all", "rv32"):
rc |= rv32_test()
if args.which in ("all", "rv32-c"):
rc |= rv32c_test()
return rc
if __name__ == "__main__":
sys.exit(main())