nx_silu.nx source
↩ module page · 222 lines · 7679 B
1// nx_silu.nx -- SiLU / Swish activation: x * sigmoid(x).
2//
3// Closes the FFN activation gap. Together with nx_rmsnorm (shipped
4// dfce1e32) this lets NishiLang run a full Llama-class transformer
5// block end-to-end: input -> RMSNorm -> attention -> RMSNorm -> FFN
6// (matmul -> SiLU -> matmul) -> output.
7//
8// SiLU = Sigmoid Linear Unit = x * sigmoid(x).
9// * Ramachandran et al. 2017 _Searching for Activation Functions_
10// * Elfwing et al. 2018 _Sigmoid-Weighted Linear Units for Neural
11// Network Function Approximation in Reinforcement Learning_
12//
13// Modern transformer FFN uses SiLU exclusively (or its closely-
14// related variant GeGLU which gates by gelu instead). Used by:
15//
16// Llama 2/3 SwiGLU FFN (uses SiLU as the gate)
17// Mistral, Mixtral, Qwen, Gemma SwiGLU FFN
18// Z-Image / Flux / Stable Diffusion 3 attention FFN
19//
20// ===== Math =======================================================
21//
22// sigmoid(x) = 1 / (1 + exp(-x))
23// silu(x) = x * sigmoid(x)
24//
25// Q10 fixed-point:
26//
27// For x >= 0: e_neg = exp(-x) ; sigmoid = Q10 / (Q10 + e_neg)
28// For x < 0: e_neg = exp(x) ; sigmoid = e_neg / (Q10 + e_neg)
29//
30// (Both branches use only the negative-domain exp lookup -- no
31// overflow risk.) Then silu = x * sigmoid / Q10.
32//
33// Per the bits-up cardinal: composes against
34// nx_exp_q10_neg (canonical exp primitive)
35// NxTensor (canonical L1 container)
36// LoopVerdict (bounded-loop discipline)
37//
38// genealogy_id: ramachandran_2017_silu + elfwing_2018_sil +
39// hendrycks_gimpel_2016_gelu (sibling activation)
40// lineage_id: substrate_silu_v1
41
42// nx_safety_envelope:
43// intended_use: AUTO_APPLIED -- primitive-specific tuning queued
44// sil_target: SIL1
45// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail]
46// verdict: NOT_YET_EVALUATED
47
48import "nx_syscalls.nx"
49import "nx_tier.nx"
50import "nx_loop.nx"
51import "nx_tensor.nx"
52import "nx_exp.nx"
53
54const NX_SILU_Q10: nx_int = 1024
55
56// ===== Sealed-enum: SiluVerdict ===================================
57
58const NX_SILU_OK: nx_int = 0
59const NX_SILU_ERR_BAD_DTYPE: nx_int = 1
60const NX_SILU_ERR_SHAPE_MISMATCH: nx_int = 2
61const NX_SILU_ERR_NOT_CONTIGUOUS: nx_int = 3
62const NX_SILU_N_VERDICTS: nx_int = 4
63
64func nx_silu_verdict_is_valid(v: nx_int) -> nx_int {
65 if v < 0 { return 0 }
66 if v >= NX_SILU_N_VERDICTS { return 0 }
67 return 1
68}
69
70// ===== Sigmoid scalar (Q10) =======================================
71//
72// Returns Q10 in [0, 1024].
73
74func nx_sigmoid_q10(x_q10: nx_int) -> nx_int {
75 if x_q10 == 0 { return NX_SILU_Q10 / 2 }
76 if x_q10 > 0 {
77 let e_neg: nx_int = nx_exp_q10_neg(0 - x_q10)
78 let denom: nx_int = NX_SILU_Q10 + e_neg
79 if denom <= 0 { return NX_SILU_Q10 }
80 return (NX_SILU_Q10 * NX_SILU_Q10) / denom
81 }
82 let e_neg: nx_int = nx_exp_q10_neg(x_q10)
83 let denom: nx_int = NX_SILU_Q10 + e_neg
84 if denom <= 0 { return 0 }
85 return (e_neg * NX_SILU_Q10) / denom
86}
87
88// ===== SiLU scalar (Q10) ==========================================
89//
90// silu(x_q10) = x_q10 * sigmoid_q10(x_q10) / Q10.
91
92func nx_silu_q10(x_q10: nx_int) -> nx_int {
93 let s: nx_int = nx_sigmoid_q10(x_q10)
94 return (x_q10 * s) / NX_SILU_Q10
95}
96
97// ===== SiLU forward over a tensor =================================
98//
99// Applies SiLU elementwise. In-place supported.
100
101func nx_silu_forward(x: *NxTensor, out: *NxTensor) -> nx_int {
102 if x.dtype != NX_DT_I64 { return NX_SILU_ERR_BAD_DTYPE }
103 if out.dtype != NX_DT_I64 { return NX_SILU_ERR_BAD_DTYPE }
104 if x.numel != out.numel { return NX_SILU_ERR_SHAPE_MISMATCH }
105 if nx_t_is_contiguous(x) == 0 { return NX_SILU_ERR_NOT_CONTIGUOUS }
106 if nx_t_is_contiguous(out) == 0 { return NX_SILU_ERR_NOT_CONTIGUOUS }
107
108 let px: *i64 = x.storage as *i64
109 let po: *i64 = out.storage as *i64
110 let n: nx_int = x.numel
111
112 var i: nx_int = 0
113 var iter: nx_int = 0
114 var verdict: nx_int = NX_LOOP_RUNNING
115 let BUDGET: nx_int = n
116 while verdict == NX_LOOP_RUNNING && iter < BUDGET {
117 po[i] = nx_silu_q10(px[i])
118 i = i + 1
119 iter = iter + 1
120 }
121 return NX_SILU_OK
122}
123
124// ===== Self-test ==================================================
125//
126// Reference values (float) cross-checked:
127//
128// x sigmoid(x) silu(x)
129// -inf 0 0
130// -2.0 0.119 -0.238
131// -1.0 0.269 -0.269
132// -0.5 0.378 -0.189
133// 0.0 0.500 0.000
134// 0.5 0.622 0.311
135// 1.0 0.731 0.731
136// 2.0 0.881 1.762
137// +inf 1 ~x (linear regime)
138//
139// Closed-form invariants:
140// (a) silu(0) == 0
141// (b) sigmoid(0) == Q10 / 2 == 512
142// (c) sigmoid + sigmoid(-x) == Q10 (sigmoid is anti-symmetric
143// around 0.5)
144// (d) silu(x) - silu(-x) == x (silu(x) = silu(-x) + x;
145// identity from x*sigmoid(x) + x*(1 - sigmoid(x)) = x; only
146// approximately holds in Q10 due to exp interpolation rounding)
147// (e) silu(x) for large positive x ≈ x (sigmoid -> 1)
148// (f) silu(x) for large negative x ≈ 0
149
150func main() -> i64 {
151 // --- (a) silu(0) = 0 ---
152 if nx_silu_q10(0) != 0 { return 10 }
153
154 // --- (b) sigmoid(0) = Q10/2 ---
155 if nx_sigmoid_q10(0) != NX_SILU_Q10 / 2 { return 11 }
156
157 // --- (c) sigmoid(x) + sigmoid(-x) = Q10 (within rounding) ---
158 let s_p1: nx_int = nx_sigmoid_q10(1024) // sigmoid(1.0)
159 let s_n1: nx_int = nx_sigmoid_q10(-1024) // sigmoid(-1.0)
160 let sum1: nx_int = s_p1 + s_n1
161 let drift1: nx_int = sum1 - NX_SILU_Q10
162 if drift1 > 8 { return 20 }
163 if drift1 < -8 { return 21 }
164
165 let s_p2: nx_int = nx_sigmoid_q10(2048)
166 let s_n2: nx_int = nx_sigmoid_q10(-2048)
167 let sum2: nx_int = s_p2 + s_n2
168 let drift2: nx_int = sum2 - NX_SILU_Q10
169 if drift2 > 8 { return 22 }
170 if drift2 < -8 { return 23 }
171
172 // --- (d) sigmoid(1.0) ≈ 0.731 -> 748 Q10 ---
173 if s_p1 < 740 { return 30 }
174 if s_p1 > 755 { return 31 }
175
176 // --- (e) silu(1.0) = 1.0 * 0.731 = 0.731 -> 748 Q10 ---
177 let sl_p1: nx_int = nx_silu_q10(1024)
178 if sl_p1 < 740 { return 40 }
179 if sl_p1 > 755 { return 41 }
180
181 // --- (f) silu(-1.0) = -1.0 * 0.269 = -0.269 -> -276 Q10 ---
182 let sl_n1: nx_int = nx_silu_q10(-1024)
183 if sl_n1 < -280 { return 50 }
184 if sl_n1 > -260 { return 51 }
185
186 // --- (g) silu(large positive) approaches x ---
187 let sl_big: nx_int = nx_silu_q10(8192) // x = 8.0
188 let drift_big: nx_int = sl_big - 8192
189 if drift_big > 0 { return 60 } // silu(x) <= x for finite x
190 if drift_big < -50 { return 61 } // close to x
191
192 // --- (h) silu(large negative) approaches 0 ---
193 let sl_nbig: nx_int = nx_silu_q10(-8192)
194 if sl_nbig > 0 { return 70 }
195 if sl_nbig < -100 { return 71 }
196
197 // --- (i) Tensor forward ---
198 let sh: *nx_int = sys_mmap(8) as *nx_int
199 sh[0] = 5
200 let err: *nx_int = sys_mmap(8) as *nx_int
201 err[0] = 0
202 let xt: *NxTensor = nx_t_alloc(NX_DT_I64, sh, 1, err)
203 let yt: *NxTensor = nx_t_alloc(NX_DT_I64, sh, 1, err)
204 if err[0] != 0 { return 80 }
205
206 let px: *i64 = xt.storage as *i64
207 px[0] = -2048
208 px[1] = -1024
209 px[2] = 0
210 px[3] = 1024
211 px[4] = 2048
212
213 let v: nx_int = nx_silu_forward(xt, yt)
214 if v != NX_SILU_OK { return 90 + v }
215
216 let py: *i64 = yt.storage as *i64
217 if py[2] != 0 { return 100 } // silu(0) = 0
218 if py[0] >= 0 { return 101 } // silu(-2) < 0
219 if py[4] <= 0 { return 102 } // silu(2) > 0
220
221 return 0
222}