code wiki / _hdl_build / nx_fnet_grad_gate.nx
nx_fnet_grad_gate.nx source
↩ module page · 178 lines · 7515 B
1// nx_fnet_grad_gate.nx -- GATE for FNET-002: the FNet mixer's backward pass (VJP). Proves, by RUNNING, that
2// the mixer is SELF-ADJOINT, so dL/dx = fnet_mix(dL/dy) -- backprop is one forward mix, zero stored
3// activations, zero parameters. Three independent proofs (n=8, d=4 -> a 32-dim operator):
4//
5// G1 SYMMETRY extract the operator matrix M by impulse responses (M[:,k] = fnet_mix(S*e_k)); the
6// real-linear FNet operator must be SYMMETRIC (A = A^T) because it is the real part of a
7// Kronecker product of symmetric DFT matrices. Measure max|M[i,j]-M[j,i]|.
8// G2 ADJOINT TEST the relationship backprop relies on: <A x, g> == <x, A* g> with A* = A. Measure
9// |<Ax,g> - <x,Ag>| -- zero (to rounding) IFF using fnet_mix as the backward is the
10// correct VJP.
11// G3 GRADCHECK finite-difference the scalar loss L(x)=sum a*fnet_mix(x): central differences vs the
12// analytic backward fnet_mix_backward(a). Max |fd - analytic|.
13//
14// All three are robust to the Q14 fixed-point rounding (tolerances are a small % of signal, actuals reported).
15// Evidence -> knowledge/status/fnet_grad.log (FNETGRADGATE authored=organ ... verdict=GREEN). license_tier: ORIGINAL
16import "nx_fnet_mix.nx"
17import "nx_syscalls.nx"
18import "nx_gate_verdict.nx"
19
20const FG_LOG: *u8 = "knowledge/status/fnet_grad.log"
21
22func fg_w(fd: i64, s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(fd, s, n); return 0 }
23func fg_wn(fd: i64, v: i64) -> i64 {
24 let bb: *u8 = sys_mmap(28); var m: i64 = v
25 if m < 0 { m = 0 - m; sys_write(fd, "-" as *u8, 1) }
26 let t: *u8 = sys_mmap(28); var k: i64 = 0
27 if m == 0 { t[0] = 48; k = 1 }
28 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 }
29 var i: i64 = 0
30 while i < k { bb[i] = t[k - 1 - i]; i = i + 1 }
31 sys_write(fd, bb, k); return 0
32}
33func fg_abs(x: i64) -> i64 { if x < 0 { return 0 - x } return x }
34
35// real part of fnet_mix applied to a real vector src[nd] -> out[nd] (fresh buffers; src untouched).
36func fg_forward(src: *i64, n: i64, d: i64, out: *i64) -> i64 {
37 let nd: i64 = n * d
38 let xr: *i64 = (sys_mmap(nd * 8)) as *i64
39 let xi: *i64 = (sys_mmap(nd * 8)) as *i64
40 var i: i64 = 0
41 while i < nd { xr[i] = src[i]; xi[i] = 0; i = i + 1 }
42 let opc: *i64 = (sys_mmap(8)) as *i64
43 *opc = 0
44 fnet_mix(xr, xi, n, d, opc)
45 i = 0
46 while i < nd { out[i] = xr[i]; i = i + 1 }
47 return 0
48}
49
50func main() -> i64 {
51 let n: i64 = 8
52 let d: i64 = 4
53 let nd: i64 = n * d
54 var ok: i64 = 1
55
56 // ---------- G1: extract operator matrix by impulse responses; measure asymmetry ----------
57 let S: i64 = 65536 // large impulse so Q14 truncation is negligible vs entries
58 let mtx: *i64 = (sys_mmap(nd * nd * 8)) as *i64
59 let e: *i64 = (sys_mmap(nd * 8)) as *i64
60 let yk: *i64 = (sys_mmap(nd * 8)) as *i64
61 var col: i64 = 0
62 while col < nd {
63 var i: i64 = 0
64 while i < nd { e[i] = 0; i = i + 1 }
65 e[col] = S
66 fg_forward(e, n, d, yk)
67 i = 0
68 while i < nd { mtx[i * nd + col] = yk[i]; i = i + 1 }
69 col = col + 1
70 }
71 var asym: i64 = 0
72 var symmax: i64 = 0
73 var r: i64 = 0
74 while r < nd {
75 var c: i64 = 0
76 while c < nd {
77 let av: i64 = fg_abs(mtx[r * nd + c])
78 if av > symmax { symmax = av }
79 let df: i64 = fg_abs(mtx[r * nd + c] - mtx[c * nd + r])
80 if df > asym { asym = df }
81 c = c + 1
82 }
83 r = r + 1
84 }
85
86 // ---------- G2: adjoint dot-product test <Ax,g> vs <x,Ag> ----------
87 let xv: *i64 = (sys_mmap(nd * 8)) as *i64
88 let gv: *i64 = (sys_mmap(nd * 8)) as *i64
89 var j: i64 = 0
90 while j < nd {
91 xv[j] = (((j * 37 + 11) % 211) - 105) * 15
92 gv[j] = (((j * 53 + 7) % 197) - 98) * 15
93 j = j + 1
94 }
95 let ax: *i64 = (sys_mmap(nd * 8)) as *i64
96 let ag: *i64 = (sys_mmap(nd * 8)) as *i64
97 fg_forward(xv, n, d, ax)
98 fg_forward(gv, n, d, ag)
99 var lhs: i64 = 0
100 var rhs: i64 = 0
101 j = 0
102 while j < nd { lhs = lhs + ax[j] * gv[j]; rhs = rhs + xv[j] * ag[j]; j = j + 1 }
103 let adjdiff: i64 = fg_abs(lhs - rhs)
104
105 // ---------- G3: finite-difference gradcheck of the self-adjoint backward ----------
106 let a: *i64 = (sys_mmap(nd * 8)) as *i64 // loss weights (also the upstream gradient dL/dy)
107 let x0: *i64 = (sys_mmap(nd * 8)) as *i64
108 j = 0
109 while j < nd {
110 a[j] = (((j * 29 + 13) % 131) - 65) * 64
111 x0[j] = (((j * 41 + 5) % 233) - 116) * 18
112 j = j + 1
113 }
114 let analytic: *i64 = (sys_mmap(nd * 8)) as *i64
115 fg_forward(a, n, d, analytic) // dL/dx = fnet_mix_backward(a) = fnet_mix(a)
116
117 let eps: i64 = 512
118 let xp: *i64 = (sys_mmap(nd * 8)) as *i64
119 let xm: *i64 = (sys_mmap(nd * 8)) as *i64
120 let yp: *i64 = (sys_mmap(nd * 8)) as *i64
121 let ym: *i64 = (sys_mmap(nd * 8)) as *i64
122 var fdmax: i64 = 0
123 var fdana: i64 = 0
124 var k: i64 = 0
125 while k < nd {
126 var i: i64 = 0
127 while i < nd { xp[i] = x0[i]; xm[i] = x0[i]; i = i + 1 }
128 xp[k] = xp[k] + eps
129 xm[k] = xm[k] - eps
130 fg_forward(xp, n, d, yp)
131 fg_forward(xm, n, d, ym)
132 var lp: i64 = 0
133 var lm: i64 = 0
134 i = 0
135 while i < nd { lp = lp + a[i] * yp[i]; lm = lm + a[i] * ym[i]; i = i + 1 }
136 let fd: i64 = (lp - lm) / (2 * eps)
137 let df: i64 = fg_abs(fd - analytic[k])
138 if df > fdmax { fdmax = df }
139 let av: i64 = fg_abs(analytic[k])
140 if av > fdana { fdana = av }
141 k = k + 1
142 }
143
144 // ---------- verdicts (tolerances = small % of signal, robust to fixed-point rounding) ----------
145 if symmax <= 0 { ok = 0 }
146 if fdana <= 0 { ok = 0 }
147 if asym * 100 > symmax { ok = 0 } // G1: asymmetry < 1% of peak entry
148 if adjdiff * 100 > fg_abs(lhs) { ok = 0 } // G2: adjoint residual < 1% of <Ax,g>
149 if fdmax * 20 > fdana { ok = 0 } // G3: gradcheck error < 5% of peak gradient
150
151 // emit
152 var f: i64 = 1
153 while f >= 0 {
154 let fd2: i64 = f
155 var lf: i64 = 1
156 if f == 0 { lf = sys_openat_append(FG_LOG, 420) }
157 if lf >= 0 {
158 fg_w(lf, "FNETGRADGATE authored=organ kernel=fnet-backward n=8 d=4 self-adjoint" as *u8)
159 fg_w(lf, " G1_asym=" as *u8); fg_wn(lf, asym); fg_w(lf, " sym_peak=" as *u8); fg_wn(lf, symmax)
160 fg_w(lf, " | G2_adj_lhs=" as *u8); fg_wn(lf, lhs); fg_w(lf, " adj_rhs=" as *u8); fg_wn(lf, rhs)
161 fg_w(lf, " adj_residual=" as *u8); fg_wn(lf, adjdiff)
162 fg_w(lf, " | G3_gradcheck_maxdiff=" as *u8); fg_wn(lf, fdmax); fg_w(lf, " grad_peak=" as *u8); fg_wn(lf, fdana)
163 if ok == 1 { fg_w(lf, " verdict=GREEN\n" as *u8) } else { fg_w(lf, " verdict=RED\n" as *u8) }
164 if f == 0 { sys_close(lf) }
165 }
166 f = f - 1
167 }
168
169 // MIGRATED onto nx_gate_verdict by nx_gate_dry_apply (D001, minimal form): every check
170 // row above is untouched, so the PASS/FAIL vector cannot change; only the hand-rolled
171 // verdict emission is replaced by the ONE shared base class. Proven by nx_gate_migrate verify.
172 let ctr__dry: *i64 = gv_ctr()
173 ctr__dry[0] = ok
174 ctr__dry[1] = 1
175 let rc__dry: i64 = gv_verdict("FNET-GRAD-GATE" as *u8, ctr__dry, "teeth unchanged; verdict emission migrated onto the shared base class" as *u8)
176 sys_exit(rc__dry)
177 return rc__dry
178}