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
9 changes: 8 additions & 1 deletion docs/NOW.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
# NOW — feat: GF-T nonlinear (ReLU) on-chip trainer (2026-08-07)
# NOW — feat: GF-T 2-neuron hidden layer trainer (2026-08-07)

Last updated: 2026-08-07

## feat: GF-T 2-neuron hidden layer on-chip trainer — proven on AX7203 (Refs #1764)

- **NEW** spec `specs/ternary/gft_hidden2.t27` — on-chip SGD step of a 2-neuron hidden layer `y=relu(w0*x0)+relu(w1*x1)` (fixed unit output weights): two INDEPENDENT nonlinear units, each with its own relu gate; per-neuron gated grad `dw_j=e*relu'(z_j)*x_j`; updates `w_j'=w_j-eta*dw_j`; returns `(w0'<<32)|w1'`. Reuses `smul/sadd/neg/relu/relu_prime/mag*`
- `test` blocks (both active, one-dead) PASS via `icarus-simulate` per L4
- Proven on a live AX7203 (`uart_hidden2.v`): the two units gate INDEPENDENTLY — killing one neuron's activation (its input <0) freezes only that neuron's weight while the other keeps learning (MOVED vs FROZEN across 3 phases)
- Spec-only; no `gen/`/`coq/` edits; no new `*.sh`; Refs #1764

## feat: GF-T nonlinear (ReLU) on-chip trainer — proven on AX7203 (Refs #1764)

- **NEW** spec `specs/ternary/gft_train2relu.t27` — on-chip SGD step of a NONLINEAR 2-input neuron `y=relu(w0*x0+w1*x1)`: forward with `relu(z)`, error, gradient GATED by `relu'(z)` (`d=e*relu_prime(z)`, `g_i=d*x_i`), updates `w_i'=w_i-eta*g_i`, returns `(w0'<<32)|w1'`. Reuses `smul/sadd/neg/mag*` from `gft_sgd_step`
Expand Down
143 changes: 143 additions & 0 deletions specs/ternary/gft_hidden2.t27
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
module GftHidden2;
// #1764 + GF-T: a GF-T SGD weight update -- w' = w - eta * g, the final brick of an
// on-device training step (forward softmax -> loss -> gradient g -> THIS update).
// eta is the (positive) learning rate; g the gradient (signed); w the weight (signed).
// Composes the verified primitives: signed multiply (smul over the RNE magnitude
// mul) + subtract (sadd + neg). Bit-exact to the integer oracle; accuracy is to
// GF-T16 precision (<=1 ULP; ~0.03 abs at the largest magnitudes).
//
// Inputs: w, g, eta signed GF-T16 (u32). Output: updated weight w' GF-T16 (u32).

fn magadd(a: i32, b: i32) -> i32 {
var ao : i32 = a >> 9; var am : i32 = a & 511;
var bo : i32 = b >> 9; var bm : i32 = b & 511;
var ho : i32 = bo; var hm : i32 = bm; var lo : i32 = ao; var lm : i32 = am;
if (ao >= bo) { ho = ao; hm = am; lo = bo; lm = bm; }
var hs : i32 = 512 + hm; var ls : i32 = 512 + lm;
var d : i32 = ho - lo; if (d > 11) { d = 11; }
var losh : i32 = ls >> d; var rem : i32 = ls - (losh << d);
var s : i32 = hs + losh; var off : i32 = ho; var mant : i32 = s - 512;
if (s >= 1024) {
var g : i32 = s & 1; var pre : i32 = s >> 1; mant = pre - 512;
if (g == 1) { if (rem > 0) { mant = mant + 1; } else { if ((pre & 1) == 1) { mant = mant + 1; } } }
off = ho + 1; if (off >= 80) { off = 80; }
} else {
var t : i32 = rem << 1; var hf : i32 = 1 << d;
if (t > hf) { mant = mant + 1; } else { if (t == hf) { if ((s & 1) == 1) { mant = mant + 1; } } }
}
if (mant >= 512) { mant = 0; off = off + 1; if (off >= 80) { off = 80; } }
return (off << 9) | mant;
}

fn magsub(hi: i32, lo: i32) -> i32 {
if (hi == lo) { return 0; }
var ho : i32 = hi >> 9; var hm : i32 = hi & 511;
var lo_o : i32 = lo >> 9; var lm : i32 = lo & 511;
var d : i32 = ho - lo_o; var hs : i32 = (512 + hm) << 14;
var la : i32 = 0; var sticky : i32 = 0;
if (d >= 26) { la = 0; sticky = 1; }
else { var ls : i32 = (512 + lm) << 14; la = ls >> d; if ((ls - (la << d)) > 0) { sticky = 1; } }
var diff : i32 = hs - la; var off : i32 = ho;
if (diff < 8388608) { if (off > 1) { diff = diff << 1; off = off - 1; } }
if (diff < 8388608) { if (off > 1) { diff = diff << 1; off = off - 1; } }
if (diff < 8388608) { if (off > 1) { diff = diff << 1; off = off - 1; } }
if (diff < 8388608) { if (off > 1) { diff = diff << 1; off = off - 1; } }
if (diff < 8388608) { if (off > 1) { diff = diff << 1; off = off - 1; } }
if (diff < 8388608) { if (off > 1) { diff = diff << 1; off = off - 1; } }
if (diff < 8388608) { if (off > 1) { diff = diff << 1; off = off - 1; } }
if (diff < 8388608) { if (off > 1) { diff = diff << 1; off = off - 1; } }
if (diff < 8388608) { if (off > 1) { diff = diff << 1; off = off - 1; } }
if (diff < 8388608) { if (off > 1) { diff = diff << 1; off = off - 1; } }
if (diff < 8388608) { if (off > 1) { diff = diff << 1; off = off - 1; } }
if (diff < 8388608) { if (off > 1) { diff = diff << 1; off = off - 1; } }
var q : i32 = diff >> 14; var rem : i32 = diff - (q << 14); var half : i32 = 8192; var mant : i32 = q - 512;
if (rem > half) { mant = mant + 1; }
else { if (rem == half) { if (sticky == 1) { mant = mant + 1; } else { if ((q & 1) == 1) { mant = mant + 1; } } } }
if (mant >= 512) { mant = 0; off = off + 1; if (off >= 80) { off = 80; } }
return (off << 9) | mant;
}

fn sadd(a: u32, b: u32) -> u32 {
if (a == 0) { return b; }
if (b == 0) { return a; }
var sa : i32 = (a >> 16) as i32; var ma : i32 = (a & 65535) as i32;
var sb : i32 = (b >> 16) as i32; var mb : i32 = (b & 65535) as i32;
if (sa == sb) { return ((sa << 16) | magadd(ma, mb)) as u32; }
var bsign : i32 = sa;
var r : i32 = magsub(ma, mb);
if (ma < mb) { r = magsub(mb, ma); bsign = sb; }
if (r == 0) { return 0; }
return ((bsign << 16) | r) as u32;
}

fn neg(v: u32) -> u32 {
if (v == 0) { return 0; }
return v ^ 65536;
}

fn magmul(a16: i32, b16: i32) -> i32 {
var ao : i32 = a16 >> 9; var am : i32 = a16 & 511;
var bo : i32 = b16 >> 9; var bm : i32 = b16 & 511;
var prod : i32 = (512 + am) * (512 + bm);
var carry : i32 = 0; if (prod >= 524288) { carry = 1; }
var q : i32 = prod >> 9; var r : i32 = prod & 511; var half : i32 = 256;
if (carry == 1) { q = prod >> 10; r = prod & 1023; half = 512; }
var mant : i32 = q - 512;
if (r > half) { mant = mant + 1; }
if (r == half) { if ((q & 1) == 1) { mant = mant + 1; } }
var sm : i32 = ao + bo + carry;
var out_off : i32 = 0;
if (sm >= 40) { var res : i32 = sm - 40; if (res >= 80) { out_off = 80; } else { out_off = res; } }
if (mant >= 512) { mant = 0; out_off = out_off + 1; if (out_off >= 80) { out_off = 80; } }
return (out_off << 9) | mant;
}

// softmax: p_sel = 2^(l_sel - M) / sum_i 2^(l_i - M), M = max logit.

// signed GF-T multiply: sign = xor of signs, magnitude = RNE magnitude mul.
fn smul(a: u32, b: u32) -> u32 {
if (a == 0) { return 0; }
if (b == 0) { return 0; }
var sgn : i32 = ((a >> 16) & 1) as i32;
var sb : i32 = ((b >> 16) & 1) as i32;
if (sgn != sb) { sgn = 1; } else { sgn = 0; }
var mag : i32 = magmul((a & 65535) as i32, (b & 65535) as i32);
if (mag == 0) { return 0; }
return ((sgn << 16) | mag) as u32;
}

// GF-T ReLU: max(0,z) — zero if z is zero or has the sign bit set, else z.
fn relu(z: u32) -> u32 {
if (z == 0) { return 0; }
if (((z >> 16) & 1) == 1) { return 0; }
return z;
}
// ReLU derivative as a GF-T gate: 1.0 (=20480) if z>0, else 0.
fn relu_prime(z: u32) -> u32 {
if (z == 0) { return 0; }
if (((z >> 16) & 1) == 1) { return 0; }
return 20480;
}
// One on-chip SGD step of a 2-NEURON HIDDEN LAYER with fixed unit output weights:
// h0=relu(w0*x0), h1=relu(w1*x1), y=h0+h1 ; error e=y-t ;
// per-neuron gated grad dw_j = e*relu'(z_j)*x_j ; update w_j'=w_j-eta*dw_j.
// Each hidden unit has its OWN relu gate -> one can learn while the other is dead.
// Returns (w0'<<32)|w1'.
fn on_comb(w0: u32, w1: u32, x0: u32, x1: u32, t: u32, eta: u32) -> u64 {
var z0 : u32 = smul(w0, x0);
var z1 : u32 = smul(w1, x1);
var y : u32 = sadd(relu(z0), relu(z1));
var e : u32 = sadd(y, neg(t));
var d0 : u32 = smul(e, relu_prime(z0));
var d1 : u32 = smul(e, relu_prime(z1));
var g0 : u32 = smul(d0, x0);
var g1 : u32 = smul(d1, x1);
var w0n : u32 = sadd(w0, neg(smul(eta, g0)));
var w1n : u32 = sadd(w1, neg(smul(eta, g1)));
return ((w0n as u64) << 32) | (w1n as u64);
}
// both active: w=[.5,.5],x=[1,1],t=1.5 -> z0=z1=.5>0, h0=h1=.5, y=1.0, e=-.5,
// d_j=-.5, g_j=-.5, w'=[.625,.625]. (placeholder 0 -> PROBE prints truth)
test both { assert_eq(on_comb(19968,19968,20480,20480,20736,19456), 86311662800512); }
// neuron1 dead: x1 negative so z1<0 -> h1=0, only w0 learns, w1 frozen.
test onedead { assert_eq(on_comb(19968,19968,20480,86016,20736,19456), 86861418614272); }
Loading