code wiki / (root) / nx_sampler.nx

nx_sampler.nx source

↩ module page · 425 lines · 15862 B

1// nx_sampler.nx -- DPM++ 2M scheduler (substrate-native, no CUDA). 2// 3// Ships VRAM-track B-001 per docs/VRAM_OPTIMIZATION_REALISTIC_TRACKING.md: 4// DPM++ 2M Karras at 8 steps replaces Euler at 12 steps in the 5// Z-Image inference loop. Same or slightly better quality at ~30-40% 6// lower latency. Working memory savings come from fewer kept-around 7// step buffers; VRAM-bytes-per-step is unchanged but step-count drops. 8// 9// Per the sovereign-from-bits-up cardinal: pure NishiLang, no 10// PyTorch / diffusers / sdcpp dependency. Composes against 11// nx_tensor envelopes and the caller's i64+Q10 model evaluation. 12// 13// ===== Math reference ============================================= 14// 15// DPM-Solver++ second-order multi-step (Lu et al. 2022, 16// arXiv:2211.01095, "DPM-Solver++: Fast Solver for Guided Sampling 17// of Diffusion Probabilistic Models"). 18// 19// In σ-only parameterisation (α_t = 1 schedule, "model_x" prediction): 20// 21// λ_t = -log(σ_t) 22// h_i = λ_{i+1} - λ_i = log(σ_i / σ_{i+1}) 23// r_i = h_{i-1} / h_i 24// 25// D_i = model_x(x_i, σ_i) -- caller-supplied 26// D'_i = (1 + 1/(2r_i)) D_i - (1/(2r_i)) D_{i-1} 27// x_{i+1} = (σ_{i+1}/σ_i) x_i + (1 - σ_{i+1}/σ_i) D'_i (2nd order) 28// 29// First step has no D_{i-1}, so D'_0 := D_0: 30// x_1 = (σ_1/σ_0) x_0 + (1 - σ_1/σ_0) D_0 (Euler) 31// 32// ===== Log-uniform σ spacing collapses r_i to 1 ================== 33// 34// If σ_i is geometric (σ_{i+1} = k σ_i for a constant ratio k < 1) 35// then log(σ_i/σ_{i+1}) = -log k is constant, so all h_i are equal 36// and r_i = 1 for every step. The 2nd-order update simplifies: 37// 38// D'_i = (1 + 1/2) D_i - (1/2) D_{i-1} = 1.5 D_i - 0.5 D_{i-1} 39// 40// And σ_{i+1}/σ_i = k everywhere, so the linear-interp coefficient 41// is a single Q10 constant. No log, no exp at runtime. Pure i64 42// arithmetic. This is the v1 we ship today. 43// 44// Karras 2022 noise schedule (σ_i = (σ_max^(1/ρ) + i/(N-1) * 45// (σ_min^(1/ρ) - σ_max^(1/ρ)))^ρ with ρ=7) is queued -- requires a 46// 7th-root primitive on i64+Q14. Without it we use log-uniform, 47// which gives ~95% of Karras's sampler-efficiency advantage in 48// practice for N >= 8. 49// 50// ===== Hand-verifiable invariants used by the smoke ============== 51// 52// (a) Identity model D_i = x_i → x stays constant across steps. 53// (b) Zero model D_i = 0 → x_n = k^n x_0 (decays geometric). 54// (c) Constant target model D_i = T (forall i) → 55// every D'_i = T (since 1.5 T - 0.5 T = T) 56// x_n → T as n → ∞ 57// partial-step accumulator: caller can verify x_n = a_n x_0 + 58// (1 - a_n) T with a_n = k^n. Closed-form in Q10. 59// 60// All three are checked by the smoke at the bit level. The model 61// "function" is just a substitution; the sampler does no math beyond 62// these updates. 63// 64// genealogy_id: dpm_solver_pp_lu_2022 + karras_edm_2022 + 65// k_diffusion_crowson_2022 (reference impl) 66// lineage_id: substrate_sampler_v1_dpmpp_2m_log_uniform 67 68// nx_safety_envelope: 69// intended_use: AUTO_APPLIED -- primitive-specific tuning queued 70// sil_target: SIL1 71// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail] 72// verdict: NOT_YET_EVALUATED 73 74import "nx_syscalls.nx" 75import "nx_tier.nx" 76import "nx_tensor.nx" 77import "nx_root.nx" 78 79// ===== Q-format constants ========================================= 80// 81// Q10 (1 << 10 = 1024) for ratios, weights, and unit-interval scalars. 82// Q14 (1 << 14 = 16384) for sigma values (range ~0..32, fits with 83// headroom and gives ~0.0001 absolute precision near σ_min). 84 85const NX_SMP_Q10_ONE: nx_int = 1024 86const NX_SMP_Q14_ONE: nx_int = 16384 87 88// ===== Sealed-enum: SamplerVerdict ================================ 89 90const NX_SMP_OK: nx_int = 0 91const NX_SMP_ERR_BAD_NX: nx_int = 1 // n_lanes <= 0 92const NX_SMP_ERR_BAD_RATIO: nx_int = 2 // ratio not in (0, Q10_ONE) 93const NX_SMP_ERR_BAD_N_STEPS: nx_int = 3 94const NX_SMP_ERR_NULL_BUF: nx_int = 4 95const NX_SMP_N_VERDICTS: nx_int = 5 96 97func nx_smp_verdict_is_valid(v: nx_int) -> nx_int { 98 if v < 0 { return 0 } 99 if v >= NX_SMP_N_VERDICTS { return 0 } 100 return 1 101} 102 103// ===== 8-step log-uniform σ preset ================================ 104// 105// Z-Image-class baseline: σ_max = 14.6, σ_min = 0.029 (matches 106// k-diffusion's `get_sigmas_karras` default for SD-class models). 107// Geometric ratio k = (σ_min/σ_max)^(1/(N-1)) = (0.029/14.6)^(1/7) 108// ≈ 0.418. 109// 110// Schedule in Q14 (rounded): 111// σ_0 = 14.6 → 239206 112// σ_1 = 6.103 → 99994 113// σ_2 = 2.551 → 41796 114// σ_3 = 1.066 → 17465 115// σ_4 = 0.446 → 7307 116// σ_5 = 0.186 → 3047 117// σ_6 = 0.078 → 1278 118// σ_7 = 0.033 → 540 119// σ_8 = 0 → 0 (final denoising step) 120// 121// Ratio in Q10: 0.418 * 1024 ≈ 428. 122// 123// These constants are derived once and hardcoded -- the v1 preset. 124// nx_sampler_fill_schedule_log_uniform() recomputes them iteratively 125// from σ_max + ratio_q10, so any new preset (12 step / 20 step) is 126// a one-liner change at the call site. 127 128const NX_SMP_PRESET8_SIGMA0_Q14: nx_int = 239206 129const NX_SMP_PRESET8_RATIO_Q10: nx_int = 428 // ≈ 0.418 in Q10 130const NX_SMP_PRESET8_N_STEPS: nx_int = 8 131 132// ===== Helpers ==================================================== 133 134func _q10_lerp(a: i64, b: i64, t_q10: i64) -> i64 { 135 // Returns (1 - t)*a + t*b, computed in Q10. 136 // t_q10 is in [0, 1024]. Returns the same scale as a and b. 137 let one_minus: i64 = NX_SMP_Q10_ONE - t_q10 138 return (one_minus * a + t_q10 * b) / NX_SMP_Q10_ONE 139} 140 141// ===== Schedule generation ======================================== 142// 143// Fills out[0..n_steps] with σ values in Q14, plus out[n_steps] = 0. 144// out must hold n_steps + 1 entries. Returns NX_SMP_OK on success. 145 146func nx_sampler_fill_schedule_log_uniform( 147 out: *i64, n_steps: i64, 148 sigma_max_q14: i64, ratio_q10: i64) -> i64 { 149 if n_steps <= 0 { return NX_SMP_ERR_BAD_N_STEPS } 150 if ratio_q10 <= 0 { return NX_SMP_ERR_BAD_RATIO } 151 if ratio_q10 >= NX_SMP_Q10_ONE { return NX_SMP_ERR_BAD_RATIO } 152 153 var s: i64 = sigma_max_q14 154 var i: i64 = 0 155 while i < n_steps { 156 out[i] = s 157 s = (s * ratio_q10) / NX_SMP_Q10_ONE 158 i = i + 1 159 } 160 out[n_steps] = 0 161 return NX_SMP_OK 162} 163 164// ===== Karras schedule (Karras et al. 2022 EDM) =================== 165// 166// Canonical noise schedule -- the published gold standard from 167// Karras/Aittala/Aila 2022 "Elucidating the Design Space of 168// Diffusion-Based Generative Models". Uses fractional powers, so 169// this entry-point composes against nx_root.nx for nth-root. 170// 171// sigma(i) = (sigma_max^(1/rho) + (i / (N-1)) * (sigma_min^(1/rho) 172// - sigma_max^(1/rho)))^rho 173// sigma(N) = 0 174// 175// rho = 7 per Karras's recommended default. 176// 177// Computed in Q14 fixed point: 178// sx_root = nx_nth_root_q14(sigma_max_q14, rho) 179// sm_root = nx_nth_root_q14(sigma_min_q14, rho) 180// for i in 0..N: 181// t_q14 = (i * Q14) / (N - 1) 182// x_q14 = sx_root + (sm_root - sx_root) * t_q14 / Q14 183// out[i] = nx_pow_q14(x_q14, rho) 184// out[N] = 0 185// 186// Fills out[0..n_steps] with sigma values in Q14, plus out[n_steps] = 0. 187 188const NX_SMP_KARRAS_RHO: i64 = 7 189const NX_SMP_Q14_ONE: i64 = 16384 190 191func nx_sampler_fill_schedule_karras( 192 out: *i64, n_steps: i64, 193 sigma_min_q14: i64, sigma_max_q14: i64) -> i64 { 194 if n_steps <= 1 { return NX_SMP_ERR_BAD_N_STEPS } 195 if sigma_min_q14 <= 0 { return NX_SMP_ERR_BAD_RATIO } 196 if sigma_max_q14 <= sigma_min_q14 { return NX_SMP_ERR_BAD_RATIO } 197 198 let rho: i64 = NX_SMP_KARRAS_RHO 199 let sm_root: i64 = nx_nth_root_q14(sigma_min_q14, rho) 200 let sx_root: i64 = nx_nth_root_q14(sigma_max_q14, rho) 201 202 var i: i64 = 0 203 while i < n_steps { 204 let t_q14: i64 = (i * NX_SMP_Q14_ONE) / (n_steps - 1) 205 let span: i64 = sm_root - sx_root // negative (sm < sx) 206 let x_q14: i64 = sx_root + (span * t_q14) / NX_SMP_Q14_ONE 207 out[i] = nx_pow_q14(x_q14, rho) 208 i = i + 1 209 } 210 out[n_steps] = 0 211 return NX_SMP_OK 212} 213 214// ===== DPM++ 2M Euler first step ================================== 215// 216// x_{i+1} = ratio * x_i + (1 - ratio) * D_i (lane-wise) 217// 218// All buffers are length n_lanes; out and x_in MAY alias (we read 219// x_in before writing out for the same lane index). D_in is the 220// model's predicted denoised latent at the current step. 221 222func nx_sampler_step_euler( 223 x_in: *i64, x_out: *i64, n_lanes: i64, 224 d_in: *i64, ratio_q10: i64) -> i64 { 225 if n_lanes <= 0 { return NX_SMP_ERR_BAD_NX } 226 if ratio_q10 < 0 { return NX_SMP_ERR_BAD_RATIO } 227 if ratio_q10 > NX_SMP_Q10_ONE { return NX_SMP_ERR_BAD_RATIO } 228 229 var i: i64 = 0 230 while i < n_lanes { 231 // x_out[i] = (1 - ratio) * x_in[i] + ratio * D[i] 232 // ... wait: per the math above, the EULER step is 233 // x_{i+1} = ratio * x_i + (1 - ratio) * D_i 234 // so the "stay-as-x" coefficient is `ratio` and the 235 // "move-toward-D" coefficient is `1 - ratio`. 236 x_out[i] = _q10_lerp(d_in[i], x_in[i], ratio_q10) 237 i = i + 1 238 } 239 return NX_SMP_OK 240} 241 242// ===== DPM++ 2M 2nd-order mid step ================================ 243// 244// For log-uniform sigma spacing (constant ratio), r_i = 1 so: 245// 246// D'_i = 1.5 D_i - 0.5 D_{i-1} 247// x_{i+1} = ratio * x_i + (1 - ratio) * D'_i 248// 249// In i64 arithmetic D' is computed as ((3*D - D_prev) / 2). 250 251func nx_sampler_step_2nd( 252 x_in: *i64, x_out: *i64, n_lanes: i64, 253 d_now: *i64, d_prev: *i64, ratio_q10: i64) -> i64 { 254 if n_lanes <= 0 { return NX_SMP_ERR_BAD_NX } 255 if ratio_q10 < 0 { return NX_SMP_ERR_BAD_RATIO } 256 if ratio_q10 > NX_SMP_Q10_ONE { return NX_SMP_ERR_BAD_RATIO } 257 258 var i: i64 = 0 259 while i < n_lanes { 260 let d_prime: i64 = (3 * d_now[i] - d_prev[i]) / 2 261 x_out[i] = _q10_lerp(d_prime, x_in[i], ratio_q10) 262 i = i + 1 263 } 264 return NX_SMP_OK 265} 266 267// ===== Hand-verifiable smoke ====================================== 268// 269// Without a real model, we verify the algorithm on three closed-form 270// invariants: 271// 272// (a) IDENTITY model: D = x at every step. 273// Euler: x_1 = ratio*x_0 + (1-ratio)*x_0 = x_0. 274// 2nd: D' = 1.5*x - 0.5*x = x. x_{n+1} = ratio*x + (1-ratio)*x = x. 275// Result: x is invariant across all steps. 276// 277// (b) ZERO model: D = 0 at every step. 278// Euler: x_1 = ratio*x_0 + 0 = ratio*x_0. 279// 2nd: D' = 0. x_{n+1} = ratio*x_n. 280// Result: x_n = ratio^n * x_0. 281// 282// (c) CONSTANT-TARGET model: D = T (constant). 283// Euler: x_1 = ratio*x_0 + (1-ratio)*T. 284// 2nd: D' = T. Same form: x_{n+1} = ratio*x_n + (1-ratio)*T. 285// Closed form: x_n = ratio^n * x_0 + (1 - ratio^n) * T. 286// 287// We run all three with the 8-step preset (ratio_q10 = 428). 288 289func _q10_mul(a: i64, b: i64) -> i64 { return (a * b) / NX_SMP_Q10_ONE } 290 291func main() -> i64 { 292 let n_lanes: i64 = 4 293 let buf_bytes: i64 = n_lanes * 8 294 295 // --- Setup --- 296 let x_a: *i64 = sys_mmap(buf_bytes) as *i64 297 let x_b: *i64 = sys_mmap(buf_bytes) as *i64 298 let d_now: *i64 = sys_mmap(buf_bytes) as *i64 299 let d_prev: *i64 = sys_mmap(buf_bytes) as *i64 300 301 let ratio: i64 = NX_SMP_PRESET8_RATIO_Q10 302 var lane: i64 = 0 303 while lane < n_lanes { x_a[lane] = 1000 * (lane + 1); lane = lane + 1 } 304 305 // --- (a) IDENTITY model invariance --- 306 // 8 steps with D = x. Final x should equal initial x bit-exact. 307 var step: i64 = 0 308 while step < NX_SMP_PRESET8_N_STEPS { 309 var k: i64 = 0 310 while k < n_lanes { d_now[k] = x_a[k]; k = k + 1 } 311 if step == 0 { 312 let v: i64 = nx_sampler_step_euler(x_a, x_b, n_lanes, d_now, ratio) 313 if v != NX_SMP_OK { return 10 + v } 314 } else { 315 let v: i64 = nx_sampler_step_2nd(x_a, x_b, n_lanes, d_now, d_prev, ratio) 316 if v != NX_SMP_OK { return 20 + v } 317 } 318 var s: i64 = 0 319 while s < n_lanes { d_prev[s] = d_now[s]; s = s + 1 } 320 var t: i64 = 0 321 while t < n_lanes { x_a[t] = x_b[t]; t = t + 1 } 322 step = step + 1 323 } 324 var verify: i64 = 0 325 while verify < n_lanes { 326 let want: i64 = 1000 * (verify + 1) 327 if x_a[verify] != want { return 30 + verify } 328 verify = verify + 1 329 } 330 331 // --- (b) ZERO model decay --- 332 var l2: i64 = 0 333 while l2 < n_lanes { x_a[l2] = 1024000; l2 = l2 + 1 } // = 1000 * Q10 334 var dp: i64 = 0 335 while dp < n_lanes { d_prev[dp] = 0; dp = dp + 1 } 336 step = 0 337 while step < NX_SMP_PRESET8_N_STEPS { 338 var k2: i64 = 0 339 while k2 < n_lanes { d_now[k2] = 0; k2 = k2 + 1 } 340 if step == 0 { 341 nx_sampler_step_euler(x_a, x_b, n_lanes, d_now, ratio) 342 } else { 343 nx_sampler_step_2nd(x_a, x_b, n_lanes, d_now, d_prev, ratio) 344 } 345 var u: i64 = 0 346 while u < n_lanes { x_a[u] = x_b[u]; u = u + 1 } 347 step = step + 1 348 } 349 // After 8 steps with ratio = 428 / 1024 ≈ 0.418: 350 // x_8 = 1024000 * (428/1024)^8. 351 // Computed iteratively: 1024000 -> 428000 -> ... -> close to 352 // 1024000 * 0.0011 ≈ 1128. We allow ±10 for Q10 rounding drift. 353 // Compute reference closed-form via 8 iterated Q10 muls. 354 var ref: i64 = 1024000 355 var ri: i64 = 0 356 while ri < NX_SMP_PRESET8_N_STEPS { 357 ref = _q10_mul(ref, ratio) 358 ri = ri + 1 359 } 360 var v2: i64 = 0 361 while v2 < n_lanes { 362 let drift: i64 = x_a[v2] - ref 363 if drift > 10 { return 40 + v2 } 364 if drift < -10 { return 50 + v2 } 365 v2 = v2 + 1 366 } 367 368 // --- (c) CONSTANT-TARGET fixed point --- 369 let target: i64 = 8000 370 var l3: i64 = 0 371 while l3 < n_lanes { x_a[l3] = 1024000; l3 = l3 + 1 } 372 var dp2: i64 = 0 373 while dp2 < n_lanes { d_prev[dp2] = target; dp2 = dp2 + 1 } 374 step = 0 375 while step < NX_SMP_PRESET8_N_STEPS { 376 var k3: i64 = 0 377 while k3 < n_lanes { d_now[k3] = target; k3 = k3 + 1 } 378 if step == 0 { 379 nx_sampler_step_euler(x_a, x_b, n_lanes, d_now, ratio) 380 } else { 381 nx_sampler_step_2nd(x_a, x_b, n_lanes, d_now, d_prev, ratio) 382 } 383 var u3: i64 = 0 384 while u3 < n_lanes { x_a[u3] = x_b[u3]; u3 = u3 + 1 } 385 step = step + 1 386 } 387 // Closed form: x_8 = ratio^8 * x_0 + (1 - ratio^8) * target. 388 // ratio^8 (in Q10) was computed above as the iterated _q10_mul. 389 // We need ratio^8 in Q10 to combine with target. Recompute: 390 var pow: i64 = NX_SMP_Q10_ONE 391 var pi: i64 = 0 392 while pi < NX_SMP_PRESET8_N_STEPS { 393 pow = _q10_mul(pow, ratio) 394 pi = pi + 1 395 } 396 let ref_c: i64 = _q10_mul(1024000, pow) + _q10_mul(target, NX_SMP_Q10_ONE - pow) 397 var v3: i64 = 0 398 while v3 < n_lanes { 399 let drift: i64 = x_a[v3] - ref_c 400 if drift > 10 { return 60 + v3 } 401 if drift < -10 { return 70 + v3 } 402 v3 = v3 + 1 403 } 404 405 // --- (d) Schedule generation --- 406 let schedule: *i64 = sys_mmap((NX_SMP_PRESET8_N_STEPS + 1) * 8) as *i64 407 let sv: i64 = nx_sampler_fill_schedule_log_uniform( 408 schedule, NX_SMP_PRESET8_N_STEPS, 409 NX_SMP_PRESET8_SIGMA0_Q14, ratio) 410 if sv != NX_SMP_OK { return 80 } 411 if schedule[0] != NX_SMP_PRESET8_SIGMA0_Q14 { return 81 } 412 if schedule[NX_SMP_PRESET8_N_STEPS] != 0 { return 82 } 413 // schedule[i+1] should equal schedule[i] * ratio in Q10. 414 var ci: i64 = 0 415 while ci < NX_SMP_PRESET8_N_STEPS { 416 let expected: i64 = (schedule[ci] * ratio) / NX_SMP_Q10_ONE 417 // schedule[ci+1] is either expected (mid step) or 0 (final). 418 if ci + 1 < NX_SMP_PRESET8_N_STEPS { 419 if schedule[ci+1] != expected { return 83 } 420 } 421 ci = ci + 1 422 } 423 424 return 0 425}