Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions docs/NOW.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,32 @@
# NOW — feat: emit_verilog can emit the SILICON-READY variant (clk_div) (2026-08-08)

Last updated: 2026-08-08

## feat: fold the capstone silicon fix into the verified generator (Refs #1764)

- The capstone (full backprop trains XOR on live AX7203) ran on a HAND-WRITTEN board wrapper, not the CI-verified generator. This closes that gap: `emit_verilog` / `emit_verilog_deep` now take `clk_div` (default 1 = unchanged). clk_div>1 emits the SILICON-READY variant -- the exact fix that trains on silicon: register file forced to flip-flops (`ram_style=registers`, since distributed LUTRAM can't do the parallel weight init) + a /N clock-enable so the deep shared-core path gets ~clk_div x SETTLE cycles to settle (the open-source P&R can't express a multicycle constraint)
- **clk_div only changes TIMING, not values** -- verified bit-exact to the model in iverilog (clk_div=4, 12 training steps, RTL == model); the default clk_div=1 output is byte-identical to before, so the existing bit-exact/synth/datapath CI gate is unaffected (still ALL SYNTHESIZE). Self-test asserts clk_div=16 emits ram_style + the /N clock-enable and that clk_div=1 does not
- => the CI-verified generator now emits the same RTL that trains a neural net on real silicon: `emit_verilog(2,2,1,"m",clk_div=16)` -> flash via the seed-searched openXC7 flow -> trains XOR 4/4 on the AX7203. Unifies the verification and silicon threads. Tool-only; Refs #1764

## docs: the capstone (backprop trains XOR on silicon) lands in the whitepaper (Refs #1764)

- The full 2-layer backprop microsequencer now TRAINS XOR to 4/4 on a live Artix-7 (25/25 epochs, both layers learning on-chip, weight trajectory bit-exact to the independent Python model -- ep0 0.000/0.551/0.936/0.232 == model). Updated the whitepaper to state this as fact:
- S3 Training: added the capstone (full backprop microsequencer trains XOR on the chip, model-exact)
- S4(b): "trains XOR to 4/4 ... with the silicon bitstream built and validated" -> now "flashed to a real Artix-7 and trains XOR to 4/4 across 25/25 epochs, both layers learning on-chip, bit-exact to the model" -- a full forward+loss+backward+update loop on live silicon
- S5 honesty: the "pending one physical JTAG re-connect" item is RESOLVED; replaced with the honest seed-search caveat (nextpnr-xilinx can't express a multicycle constraint, so the deep shared-core path is placement-dependent -- an open-toolchain limitation, pick a stable seed; a commercial P&R would close it directly)
- Docs only. Refs #1764

## test: verify_multitarget covers exact/near cancellation (a, -a) (Refs #1764)

- Added a targeted edge to the cross-target proof: `gen_pairs` now injects ~20% CANCELLATION pairs `(v, neg(v))` -> `sadd` exact-cancels to 0, plus near-cancellation `(v, neg(v'))`. This is the historically buggy magsub path (cycle 17 found a negative-zero bug when the larger operand is negative and the result is exactly 0) and the magsub-normalize hot path
- Result: model `smul`/`sadd` == the C and Rust emissions BIT-EXACT on the cancellation edge too -- the fix holds across all backends. Combined with last cycle's extreme-operand coverage, the cross-target proof now spans moderate + extreme + cancellation operands
- Context: this closes out the operand-space hardening motivated by (but orthogonal to) the bpseq silicon debug, which is confirmed a TIMING issue (nextpnr-xilinx XDC supports only create_clock -- no multicycle/generated-clock -- so bpseq needs a pipelined core or a real divided clock; documented). Tool-only. Refs #1764

## test: verify_multitarget covers the full GF-T range, not just [-4,4] (Refs #1764)

- Motivated by the bpseq silicon debug: a hypothesis was that the microsequencer diverges on silicon because training-grown weights push operands into a saturation range where the Python GF-T model and the RTL might disagree (the cross-target proof only used moderate [-4,4] operands). Tested it: model smul/sadd vs the C emission over 1500 EXTREME operands (full offset span 0..127, both signs, saturation-adjacent) -- **0 mismatches, ALL MATCH.** So the model is a faithful RTL reference across the WHOLE range; the bpseq silicon divergence is NOT an arithmetic/operand-range bug (it is confirmed TIMING: iverilog stable, board core == verified gen-verilog, model == RTL on all operands)
- Turned the negative result into a real coverage improvement: `gen_pairs` now draws from BOTH the moderate range AND extreme raw GF-T u32 operands (full offset span, both signs, saturation-adjacent) -- overflow/underflow/carry edges the [-4,4] sweep never reached. Cross-target bit-exactness now proven on the full representable range
- Tool-only; still ALL TARGETS BIT-EXACT. Refs #1764
# NOW — feat: extend IGLA RACE cross-target to systolic PE + 2 more gen findings (2026-08-07)

Last updated: 2026-08-07
Expand Down
35 changes: 28 additions & 7 deletions tools/gft_backprop_microcode.py
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ def run(steps, rf):
av = _mod(rf[a], am); bv = _mod(rf[b], bm)
rf[d] = smul(av, bv) if op == "MUL" else (sadd(av, bv) if op == "ADD" else av)

def emit_verilog(n_in, n_hid, n_out, modname):
def emit_verilog(n_in, n_hid, n_out, modname, clk_div=1):
"""Emit a synthesizable microsequencer Verilog module for the given arch.
One shared GftSmul + one shared GftSadd, a register file, and a case(pc) ROM.
Fully parametric interface: one x{k}i input port per input, one t{o}i target
Expand All @@ -246,10 +246,10 @@ def emit_verilog(n_in, n_hid, n_out, modname):
for j in range(n_hid): initv[f"v{o}_{j}"] = round(random.uniform(-1.0, 1.0), 3)
for j in range(n_hid): initv[f"b{j}"] = round(random.uniform(-0.5, 0.5), 3)
for o in range(n_out): initv[f"bo{o}"] = 0.0
return _emit_module(reg, steps, initv, n_in, n_out, modname)
return _emit_module(reg, steps, initv, n_in, n_out, modname, clk_div)


def emit_verilog_deep(sizes, modname):
def emit_verilog_deep(sizes, modname, clk_div=1):
"""Emit the microsequencer for an arbitrary-DEPTH net (see gen_deep). `sizes` =
[n_in, h1, ..., n_out]. Same one-smul/one-sadd datapath and parametric interface
as emit_verilog; depth costs microcode steps (time), not area. A 2-entry-hidden
Expand All @@ -265,10 +265,18 @@ def emit_verilog_deep(sizes, modname):
for k in range(sizes[l - 1]): initv[f"W{l}_{j}_{k}"] = round(random.uniform(-0.8, 0.8), 3)
for l in range(1, L + 1):
for j in range(sizes[l]): initv[f"b{l}_{j}"] = round(random.uniform(-0.5, 0.5), 3)
return _emit_module(reg, steps, initv, sizes[0], sizes[-1], modname)
return _emit_module(reg, steps, initv, sizes[0], sizes[-1], modname, clk_div)


def _emit_module(reg, steps, initv, n_in, n_out, modname):
def _emit_module(reg, steps, initv, n_in, n_out, modname, clk_div=1):
"""clk_div > 1 emits the SILICON-READY variant: the register file is forced to
flip-flops (ram_style=registers -- distributed LUTRAM can't do the parallel weight
init) and the sequencer steps once per clk_div cycles via a clock-enable, giving the
shared combinational core ~clk_div x SETTLE cycles to settle (the open-source P&R
can't express a multicycle constraint, so the deep muxed path is timing-relaxed;
slowing the stepping is what lets it settle). Computed VALUES are identical to
clk_div=1 -- only the timing changes -- so the bit-exact model check is unaffected.
On real AX7203 silicon, clk_div=16 trains XOR 4/4, bit-exact to the model."""
"""Shared Verilog emitter: one GftSmul + one GftSadd + register file + case(pc)
microcode ROM. Parametric ports (x{k}i / t{o}i / packed yout). Zero-inits the
whole rf on reset (RTL == model, no x-propagation)."""
Expand All @@ -278,7 +286,8 @@ def _emit_module(reg, steps, initv, n_in, n_out, modname):
tports = ", ".join(f"input [31:0] t{o}i" for o in range(n_out))
L.append(f"module {modname}(input clk, input rst, input start, {xports}, {tports},"
f" output reg [{32*n_out-1}:0] yout, output reg done);")
L.append(f" reg [31:0] rf [0:{N-1}];")
rs = '(* ram_style = "registers" *) ' if clk_div > 1 else ''
L.append(f" {rs}reg [31:0] rf [0:{N-1}];")
L.append(" function [31:0] modf(input [31:0] v, input [2:0] m); reg neg0; reg [6:0] off;"
" reg [8:0] mant; begin neg0=v[16]; case(m)")
L.append(" 3'd0:modf=v; 3'd1:modf=(v==0||neg0)?32'd0:v; 3'd2:modf=(v==0||neg0)?32'd0:32'd20480;")
Expand All @@ -287,6 +296,10 @@ def _emit_module(reg, steps, initv, n_in, n_out, modname):
" if(off<3+1)modf=0; else modf=(v&32'h10000)^32'h10000|(((off-3)<<9)|mant); end end")
L.append(" default:modf=v; endcase end endfunction")
L.append(f" reg [{pcw-1}:0] pc; reg [7:0] settle; reg running; reg op; reg [7:0] ai,bi,di; reg [2:0] am,bm; integer gi;")
if clk_div > 1:
dcw = max(1, (clk_div - 1).bit_length())
L.append(f" reg [{dcw-1}:0] dc = 0; wire cen = (dc == {dcw}'d{clk_div-1});")
L.append(" always @(posedge clk) dc <= dc + 1'b1;")
L.append(" always @(*) begin op=0; ai=0; am=0; bi=0; bm=0; di=0; case(pc)")
for i, (o, a, amod, b, bmod, d) in enumerate(steps):
if o == "MOV": L.append(f" {pcw}'d{i}: begin op=2; ai={a}; di={d}; end")
Expand All @@ -304,7 +317,8 @@ def _emit_module(reg, steps, initv, n_in, n_out, modname):
+ " " + " ".join(f"rf[{reg[f't{o}']}]<=t{o}i;" for o in range(n_out))
L.append(f" if(!running) begin if(start) begin {loads} pc<=0; settle<=SETTLE; running<=1; end end")
ypack = "{" + ", ".join(f"rf[{reg[f'y{o}']}]" for o in range(n_out - 1, -1, -1)) + "}"
L.append(" else begin if(settle==0) begin rf[di] <= (op==2)? a_val : (op? add_r : mul_r);")
step_gate = "else if (cen) begin" if clk_div > 1 else "else begin" # step once per clk_div cycles
L.append(f" {step_gate} if(settle==0) begin rf[di] <= (op==2)? a_val : (op? add_r : mul_r);")
L.append(f" if(pc=={pcw}'d{NP-1}) begin running<=0; done<=1; yout<={ypack}; end"
" else begin pc<=pc+1'b1; settle<=SETTLE; end")
L.append(" end else settle<=settle-1; end end end")
Expand Down Expand Up @@ -425,3 +439,10 @@ def _predd(a, b):
vd = emit_verilog_deep([2, 4, 3, 1], "deep431")
assert "module deep431" in vd and "for(gi=0;gi<" in vd
print("emit_verilog_deep: [2,4,3,1] module generated -- OK")
# silicon-ready variant: clk_div>1 adds ram_style=registers + a /N clock-enable
# (the fix that trains XOR on real AX7203 silicon). Values are unchanged (only
# timing), so the bit-exact model check is unaffected; default clk_div=1 is intact.
vc = emit_verilog(2, 2, 1, "bpx", clk_div=16)
assert 'ram_style = "registers"' in vc and "else if (cen)" in vc and "wire cen = (dc ==" in vc
assert 'ram_style' not in emit_verilog(2, 2, 1, "bpx") and "else if (cen)" not in emit_verilog(2, 2, 1, "bpx")
print("emit_verilog: clk_div=16 emits silicon-ready ram_style + /N clock-enable (default clk_div=1 intact) -- OK")
Loading