code wiki / (root) / nx_activation_steer.nx

nx_activation_steer.nx source

↩ module page · 341 lines · 13586 B

1// nx_activation_steer.nx -- activation steering primitive. 2// 3// First patch-composted-into-substrate primitive per cardinal 4// feedback-loras-and-negatives-are-patches-not-systems 5// (patches-as-manure refinement 2026-05-16): 6// 7// User: "im not against patches but patches are manure that 8// should become architectural decisions to increase capability 9// speed etc in the native stack" 10// 11// THE patch we're composting: 12// - Negative prompts ("ugly, deformed, blurry, bad anatomy") -- 13// a shotgun list of anti-features that the model never had 14// measurable knowledge of. Folk-knowledge attractors. 15// 16// THE substrate capability we ship instead: 17// - Per-layer ACTIVATION STEERING (Turner 2023 ActAdd / Zou 2023 18// Representation Engineering). Given a "steering vector" -- 19// the difference between activations on two contrasting 20// prompts at a chosen layer -- ADD it to forward-pass 21// activations to bias output in the desired direction. 22// 23// Architectural advantages over negative prompts: 24// 25// 1. Per-token control. Steering is applied at every forward 26// pass, not just at sampling time. No "first token escapes 27// negative prompt" failure mode. 28// 2. Composable. Multiple steering vectors can be added 29// (e.g. "less violence + more cheerful + more detailed") 30// with per-vector strength weights. 31// 3. Measurable. The substrate can VERIFY the steering had 32// the intended effect by composing nx_monte_carlo + 33// nx_multivariate over generations with vs without 34// steering applied. 35// 4. Patent-clean. Vector arithmetic; no LoRA training, no 36// negative-prompt grimoires. 37// 38// ===== Math ====================================================== 39// 40// Given hidden activations h [n_tokens, hidden_dim] at some layer L, 41// and a steering vector s [hidden_dim], compute: 42// 43// h' = h + alpha * s (broadcast across tokens) 44// 45// Where alpha is the per-batch strength in Q10. Substrate ships 46// the canonical: add a per-batch vector to every token's hidden 47// state at a chosen layer. 48// 49// To DERIVE the steering vector for a target axis: 50// 1. Pick a contrastive prompt pair: ("happy" vs "sad", "old" 51// vs "young", "detailed" vs "simple"). 52// 2. Run forward pass on each; capture activations at layer L. 53// 3. Subtract: s = mean(act_target) - mean(act_anti_target). 54// 55// Substrate ships the APPLY primitive here; the DERIVATION is 56// caller-side (composes a forward pass + activation capture, both 57// available via the nx_transformer_block stack). 58// 59// ===== API ======================================================== 60// 61// nx_activation_steer_apply(hidden, steer_vec, alpha_q10) -> verdict 62// 63// In-place: hidden[i, d] += alpha_q10 * steer_vec[d] / Q10. 64// 65// Variant: nx_activation_steer_apply_multi for adding N steering 66// vectors with per-vector alphas (composability win). 67// 68// Bits-up composition (pure -- no new math): 69// NxTensor (L1) 70// nx_loop.LoopVerdict (bounded) 71// 72// genealogy_id: turner_2023_actadd + zou_2023_representation_engineering + 73// wang_2024_steering_vectors_emergence + 74// rimsky_2024_caa_contrastive_activation_addition 75// lineage_id: substrate_activation_steer_v1 76 77import "nx_syscalls.nx" 78import "nx_tier.nx" 79import "nx_loop.nx" 80import "nx_tensor.nx" 81const NX_MAGIC_1024: i64 = 1024 82const NX_MAGIC_2024: i64 = 2024 83const NX_MAGIC_2048: i64 = 2048 84const NX_MAGIC_3048: i64 = 3048 85 86const NX_AS_Q10: nx_int = 1024 87 88// ===== Sealed-enum: ActSteerVerdict =============================== 89 90const NX_AS_OK: nx_int = 0 91const NX_AS_ERR_BAD_DTYPE: nx_int = 1 92const NX_AS_ERR_BAD_NDIM: nx_int = 2 93const NX_AS_ERR_SHAPE_MISMATCH: nx_int = 3 94const NX_AS_ERR_NOT_CONTIGUOUS: nx_int = 4 95const NX_AS_N_VERDICTS: nx_int = 5 96 97func nx_as_verdict_is_valid(v: nx_int) -> nx_int { 98 if v < 0 { return 0 } 99 if v >= NX_AS_N_VERDICTS { return 0 } 100 return 1 101} 102 103// ===== Apply a single steering vector ============================ 104// 105// hidden: [n_tokens, hidden_dim] Q10 (in-place modified) 106// steer: [hidden_dim] Q10 107// alpha: nx_int Q10 strength (positive = move 108// toward target axis; negative 109// = move away) 110 111func nx_activation_steer_apply(hidden: *NxTensor, steer: *i64, 112 alpha_q10: nx_int) -> nx_int { 113 if hidden.dtype != NX_DT_I64 { return NX_AS_ERR_BAD_DTYPE } 114 if hidden.ndim != 2 { return NX_AS_ERR_BAD_NDIM } 115 if nx_t_is_contiguous(hidden) == 0 { return NX_AS_ERR_NOT_CONTIGUOUS } 116 117 let n_tokens: nx_int = hidden.shape[0] 118 let hidden_dim: nx_int = hidden.shape[1] 119 let ph: *i64 = hidden.storage as *i64 120 121 var t: nx_int = 0 122 var t_iter: nx_int = 0 123 var t_verdict: nx_int = NX_LOOP_RUNNING 124 let T_BUDGET: nx_int = n_tokens 125 while t_verdict == NX_LOOP_RUNNING && t_iter < T_BUDGET { 126 let row_base: nx_int = t * hidden_dim 127 var d: nx_int = 0 128 var d_iter: nx_int = 0 129 var d_verdict: nx_int = NX_LOOP_RUNNING 130 let D_BUDGET: nx_int = hidden_dim 131 while d_verdict == NX_LOOP_RUNNING && d_iter < D_BUDGET { 132 // delta = alpha * steer[d] / Q10 133 let delta: i64 = (alpha_q10 * steer[d]) / NX_AS_Q10 134 ph[row_base + d] = ph[row_base + d] + delta 135 d = d + 1 136 d_iter = d_iter + 1 137 } 138 t = t + 1 139 t_iter = t_iter + 1 140 } 141 return NX_AS_OK 142} 143 144// ===== Apply multiple steering vectors ========================== 145// 146// Composability win. Caller passes: 147// steer_vectors: [n_vecs * hidden_dim] flat Q10 148// alphas_q10: [n_vecs] per-vector strength 149// 150// Substrate accumulates: hidden += sum_i alphas[i] * steer_vectors[i] 151 152func nx_activation_steer_apply_multi(hidden: *NxTensor, 153 steer_vectors: *i64, alphas_q10: *i64, 154 n_vecs: nx_int) -> nx_int { 155 if hidden.dtype != NX_DT_I64 { return NX_AS_ERR_BAD_DTYPE } 156 if hidden.ndim != 2 { return NX_AS_ERR_BAD_NDIM } 157 if nx_t_is_contiguous(hidden) == 0 { return NX_AS_ERR_NOT_CONTIGUOUS } 158 if n_vecs <= 0 { return NX_AS_OK } // no-op 159 160 let n_tokens: nx_int = hidden.shape[0] 161 let hidden_dim: nx_int = hidden.shape[1] 162 let ph: *i64 = hidden.storage as *i64 163 164 var t: nx_int = 0 165 var t_iter: nx_int = 0 166 var t_verdict: nx_int = NX_LOOP_RUNNING 167 let T_BUDGET: nx_int = n_tokens 168 while t_verdict == NX_LOOP_RUNNING && t_iter < T_BUDGET { 169 let row_base: nx_int = t * hidden_dim 170 var d: nx_int = 0 171 var d_iter: nx_int = 0 172 var d_verdict: nx_int = NX_LOOP_RUNNING 173 while d_verdict == NX_LOOP_RUNNING && d_iter < hidden_dim { 174 var delta: i64 = 0 175 var v: nx_int = 0 176 var v_iter: nx_int = 0 177 var v_verdict: nx_int = NX_LOOP_RUNNING 178 let V_BUDGET: nx_int = n_vecs 179 while v_verdict == NX_LOOP_RUNNING && v_iter < V_BUDGET { 180 let vec_base: nx_int = v * hidden_dim 181 delta = delta + (alphas_q10[v] * steer_vectors[vec_base + d]) / NX_AS_Q10 182 v = v + 1 183 v_iter = v_iter + 1 184 } 185 ph[row_base + d] = ph[row_base + d] + delta 186 d = d + 1 187 d_iter = d_iter + 1 188 } 189 t = t + 1 190 t_iter = t_iter + 1 191 } 192 return NX_AS_OK 193} 194 195// ===== Derive a steering vector via contrastive activations ==== 196// 197// Given two activation buffers (one for the "target" prompt, one 198// for the "anti-target" prompt), at the same layer, compute: 199// steer[d] = mean_over_tokens(target[t, d]) - 200// mean_over_tokens(anti[t, d]) 201// 202// target / anti must have the same shape [n_tokens, hidden_dim]. 203// out_steer must be allocated [hidden_dim]. 204 205func nx_activation_steer_derive(target_act: *NxTensor, anti_act: *NxTensor, 206 out_steer: *i64) -> nx_int { 207 if target_act.dtype != NX_DT_I64 { return NX_AS_ERR_BAD_DTYPE } 208 if anti_act.dtype != NX_DT_I64 { return NX_AS_ERR_BAD_DTYPE } 209 if target_act.ndim != 2 { return NX_AS_ERR_BAD_NDIM } 210 if anti_act.ndim != 2 { return NX_AS_ERR_BAD_NDIM } 211 if target_act.shape[0] != anti_act.shape[0] { return NX_AS_ERR_SHAPE_MISMATCH } 212 if target_act.shape[1] != anti_act.shape[1] { return NX_AS_ERR_SHAPE_MISMATCH } 213 214 let n_tokens: nx_int = target_act.shape[0] 215 let hidden_dim: nx_int = target_act.shape[1] 216 let pt: *i64 = target_act.storage as *i64 217 let pa: *i64 = anti_act.storage as *i64 218 219 var d: nx_int = 0 220 var d_iter: nx_int = 0 221 var d_verdict: nx_int = NX_LOOP_RUNNING 222 let D_BUDGET: nx_int = hidden_dim 223 while d_verdict == NX_LOOP_RUNNING && d_iter < D_BUDGET { 224 var sum_t: i64 = 0 225 var sum_a: i64 = 0 226 var k: nx_int = 0 227 var k_iter: nx_int = 0 228 var k_verdict: nx_int = NX_LOOP_RUNNING 229 let K_BUDGET: nx_int = n_tokens 230 while k_verdict == NX_LOOP_RUNNING && k_iter < K_BUDGET { 231 sum_t = sum_t + pt[k * hidden_dim + d] 232 sum_a = sum_a + pa[k * hidden_dim + d] 233 k = k + 1 234 k_iter = k_iter + 1 235 } 236 out_steer[d] = (sum_t - sum_a) / n_tokens 237 d = d + 1 238 d_iter = d_iter + 1 239 } 240 return NX_AS_OK 241} 242 243// ===== Self-test ================================================== 244// 245// Smoke: small 4-token, 8-dim hidden tensor. Steering vector 246// (1, 1, 1, 1, -1, -1, -1, -1). Apply with alpha = Q10 (1.0). 247// Expected: every token's first 4 dims increase by Q10, last 4 248// decrease by Q10. 249// 250// Then verify multi-vector composition: 2 vectors with alphas 251// [Q10, 2*Q10] should accumulate appropriately. 252// 253// Then verify derive: target_act all 100; anti_act all 50; per-dim 254// mean diff = 50. out_steer[d] = 50 for all d. 255 256func main() -> i64 { 257 let n_tokens: nx_int = 4 258 let hidden_dim: nx_int = 8 259 260 let sh: *nx_int = sys_mmap(2 * 8) as *nx_int 261 sh[0] = n_tokens; sh[1] = hidden_dim 262 263 let err: *nx_int = sys_mmap(8) as *nx_int 264 err[0] = 0 265 let hidden: *NxTensor = nx_t_alloc(NX_DT_I64, sh, 2, err) 266 if err[0] != 0 { return 5 } 267 268 // Initialize hidden to known values: hidden[t, d] = 1000. 269 let ph: *i64 = hidden.storage as *i64 270 var i: nx_int = 0 271 while i < n_tokens * hidden_dim { ph[i] = 1000; i = i + 1 } 272 273 // Steering vector: +Q10 in dims 0..3, -Q10 in dims 4..7. 274 let steer: *i64 = sys_mmap(hidden_dim * 8) as *i64 275 steer[0]=NX_MAGIC_1024; steer[1]=NX_MAGIC_1024; steer[2]=NX_MAGIC_1024; steer[3]=NX_MAGIC_1024 276 steer[4]=-NX_MAGIC_1024; steer[5]=-NX_MAGIC_1024; steer[6]=-NX_MAGIC_1024; steer[7]=-NX_MAGIC_1024 277 278 // --- (a) Apply with alpha = Q10 --- 279 let v_a: nx_int = nx_activation_steer_apply(hidden, steer, NX_MAGIC_1024) 280 if v_a != NX_AS_OK { return 10 } 281 // Expected: dim 0..3 -> 1000 + 1024 = 2024 282 // Expected: dim 4..7 -> 1000 - 1024 = -24 283 if ph[0] != NX_MAGIC_2024 { return 11 } 284 if ph[3] != NX_MAGIC_2024 { return 12 } 285 if ph[4] != -24 { return 13 } 286 if ph[7] != -24 { return 14 } 287 // Token 1: same pattern. 288 if ph[hidden_dim + 0] != NX_MAGIC_2024 { return 15 } 289 290 // --- (b) Multi-vector compose --- 291 // Reset hidden to 1000. 292 var j: nx_int = 0 293 while j < n_tokens * hidden_dim { ph[j] = 1000; j = j + 1 } 294 // Two steer vectors flat: vec[0] = +Q10 across dims 0-3; vec[1] 295 // = +Q10 across dims 4-7 (so all dims should go up). 296 let steers: *i64 = sys_mmap(2 * hidden_dim * 8) as *i64 297 var k: nx_int = 0 298 while k < 4 { steers[k] = NX_MAGIC_1024; steers[k + 4] = 0; k = k + 1 } 299 while k < 8 { steers[k] = 0; steers[k + 4] = NX_MAGIC_1024; k = k + 1 } 300 let alphas: *i64 = sys_mmap(2 * 8) as *i64 301 alphas[0] = NX_MAGIC_1024 // alpha 1.0 302 alphas[1] = NX_MAGIC_2048 // alpha 2.0 303 304 let v_b: nx_int = nx_activation_steer_apply_multi(hidden, steers, alphas, 2) 305 if v_b != NX_AS_OK { return 20 } 306 // Expected dim 0..3: 1000 + 1*1024 = 2024 307 if ph[0] != NX_MAGIC_2024 { return 21 } 308 // Expected dim 4..7: 1000 + 2*1024 = 3048 309 if ph[4] != NX_MAGIC_3048 { return 22 } 310 311 // --- (c) Derive steering vector --- 312 let target_act: *NxTensor = nx_t_alloc(NX_DT_I64, sh, 2, err) 313 let anti_act: *NxTensor = nx_t_alloc(NX_DT_I64, sh, 2, err) 314 if err[0] != 0 { return 30 } 315 let pt: *i64 = target_act.storage as *i64 316 let pa: *i64 = anti_act.storage as *i64 317 var m: nx_int = 0 318 while m < n_tokens * hidden_dim { 319 pt[m] = 100 320 pa[m] = 50 321 m = m + 1 322 } 323 let out_steer: *i64 = sys_mmap(hidden_dim * 8) as *i64 324 let v_c: nx_int = nx_activation_steer_derive(target_act, anti_act, out_steer) 325 if v_c != NX_AS_OK { return 40 } 326 // Expected: out_steer[d] = mean_t(100) - mean_a(50) = 50 for all d 327 var n: nx_int = 0 328 while n < hidden_dim { 329 if out_steer[n] != 50 { return 50 } 330 n = n + 1 331 } 332 333 // --- (d) Verdict gate --- 334 var vi: nx_int = 0 335 while vi < NX_AS_N_VERDICTS { 336 if nx_as_verdict_is_valid(vi) != 1 { return 60 + vi } 337 vi = vi + 1 338 } 339 340 return 0 341}