Files
2026-08-16 18:24:52 +07:00

361 lines
16 KiB
Lua
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
-- ============================================================================
-- SPDX-License-Identifier: GPL-3.0-or-later
-- Copyright (C) 2026 Alexander Allan (MDMAchine) -- A&E Concepts
--
-- This program is free software: you can redistribute it and/or modify
-- it under the terms of the GNU General Public License as published by
-- the Free Software Foundation, either version 3 of the License, or
-- (at your option) any later version.
--
-- This program is distributed in the hope that it will be useful,
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-- GNU General Public License for more details: https://www.gnu.org/licenses/
-- ============================================================================
-- MD PingPong Simple v1.1 — Ancestral Euler + Momentum + Look-Back Smoother
-- MDMAchine | A&E Concepts © 2026
--
-- Port of MD_PingPong_Samplers.py (single-branch path) to HOT-Step-CPP Lua solver.
--
-- WHAT THIS DOES:
-- Standard ODE solvers (Euler, Heun, STORM) advance the latent deterministically.
-- PingPong injects ancestral (stochastic) noise at each step — the "ping" is
-- the clean denoising step, the "pong" is the noise re-injection that keeps
-- the trajectory alive and stochastic.
--
-- CORE STEP MATH:
-- dt = t_prev - t_curr (negative — stepping down)
-- x_euler = xt + dt * vt (standard Euler, matches STORM/OmniRelational)
-- x_next = x_euler + noise * |dt| * ancestral_strength
--
-- MOMENTUM:
-- Latent velocity (x - x_prev) carried forward at each step. Maintains
-- flow continuity across the ODE trajectory — reduces erratic jumps between
-- steps, especially at low step counts.
--
-- NOISE COHERENCE:
-- Blends fresh Gaussian noise with the previous step's noise at ratio
-- noise_coherence. 0=fully fresh, 1=fully carried. Useful for temporal
-- smoothness in audio; keep low (0-0.2) to avoid spectral smearing.
--
-- LOOK-BACK SNR SMOOTHER:
-- λ(σ) = lambda_base * (σ/σ_max)^snr_power — heavy at high sigma, fades to
-- zero at low sigma. Suppresses ODE manifold shearing and harmonic hum.
-- Reference: arXiv:2602.09449
--
-- RMS SERVO:
-- Downward-only energy ceiling that follows a smooth curve from rms_max
-- (high sigma) to rms_min (low sigma). Domain-tunable: image latents
-- typically sit around 0.75-0.97; audio latents around 0.3-0.7.
-- Servo is DOWNWARD ONLY — never boosts energy, only clamps excess.
--
-- SOLVER API NOTE:
-- HOT-Step-CPP passes velocity vt where dt = t_prev - t_curr is NEGATIVE
-- (stepping from high sigma to low sigma). Euler update is:
-- x_next = xt + dt * vt (same convention as STORM and OmniRelational)
-- Ancestral noise is added as: noise * |dt| * strength
-- Do NOT use xt - t_curr * vt for denoised — sign convention mismatch.
--
-- PARAMS:
-- ancestral_strength — noise injection scale. 1.0=standard ancestral, 0=pure ODE
-- noise_coherence — step-to-step noise carry. 0=fresh, 0.2=subtle temporal link
-- momentum_strength — latent velocity carry-over. 0.15=subtle, 0.3=strong
-- look_back_enabled — SNR smoother toggle
-- look_back_lambda — max smoothing weight (0.55=25-step, 0.35=35-step)
-- look_back_snr_power — falloff exponent (1.3=25-step, 1.5=35-step)
-- rms_servo — energy ceiling toggle
-- rms_target_min — servo floor at low sigma (audio: ~0.3, image: ~0.75)
-- rms_target_max — servo ceiling at high sigma (audio: ~0.7, image: ~0.97)
-- rms_servo_gain — correction aggressiveness (0.6=default, 1.0=hard snap)
-- seed — RNG seed
-- ============================================================================
solver = {
name = "md_pingpong_simple",
display = "MD PingPong Simple (Ancestral)",
description = "Ancestral Euler with momentum, noise coherence, look-back SNR smoother, and domain-tunable RMS servo. Single-branch stochastic sampler. Port of MD_PingPong_Samplers v3.5.",
nfe = 1,
order = 1,
needs_model = false,
stateful = true,
stochastic = true,
params = {
{
key = "ancestral_strength",
type = "slider",
label = "Ancestral Strength",
default = 0.2,
min = 0.0,
max = 1.5,
step = 0.05,
hint = "Noise injection strength. 1.0=standard ancestral. 0=pure ODE (no noise).",
},
{
key = "noise_coherence",
type = "slider",
label = "Noise Coherence",
default = 0.0,
min = 0.0,
max = 1.0,
step = 0.05,
hint = "Step-to-step noise correlation. 0=fresh noise each step. 0.2=subtle temporal link. Keep low for audio to avoid smearing.",
},
{
key = "momentum_strength",
type = "slider",
label = "Momentum",
default = 0.1,
min = 0.0,
max = 0.5,
step = 0.01,
hint = "Latent velocity carry-over. 0.1=subtle flow continuity. 0.3=strong.",
},
{
key = "look_back_enabled",
type = "toggle",
label = "Look-Back Smoother",
default = true,
hint = "SNR-adaptive latent EMA. Suppresses ODE manifold shearing and harmonic hum. arXiv:2602.09449.",
},
{
key = "look_back_lambda",
type = "slider",
label = "Look-Back Lambda",
default = 0.55,
min = 0.1,
max = 1.0,
step = 0.05,
hint = "Max smoothing weight. Active when Look-Back Smoother is on. 0.55=25-step, 0.35=35-step.",
},
{
key = "look_back_snr_power",
type = "slider",
label = "SNR Power",
default = 1.3,
min = 0.5,
max = 3.0,
step = 0.1,
hint = "Falloff exponent. Active when Look-Back Smoother is on. Higher=smoother fade at low sigma.",
},
{
key = "rms_servo",
type = "toggle",
label = "RMS Servo",
default = true,
hint = "Downward-only energy ceiling. Prevents latent energy accumulation. Off by default — tune min/max for your domain before enabling.",
},
{
key = "rms_target_min",
type = "slider",
label = "RMS Target Min",
default = 1.0,
min = 0.1,
max = 3.0,
step = 0.05,
hint = "RMS ceiling at low sigma (late steps). Active when RMS Servo is on. ACE-Step latents ~2.0 RMS. Start at 1.2-1.8.",
},
{
key = "rms_target_max",
type = "slider",
label = "RMS Target Max",
default = 2.2,
min = 0.5,
max = 4.0,
step = 0.05,
hint = "RMS ceiling at high sigma (early steps). Active when RMS Servo is on. ACE-Step latents ~2.0 RMS. Start at 2.0-2.5.",
},
{
key = "rms_servo_gain",
type = "slider",
label = "Servo Gain",
default = 0.75,
min = 0.1,
max = 1.0,
step = 0.05,
hint = "Servo correction aggressiveness. Active when RMS Servo is on. 0.6=soft, 1.0=hard snap.",
},
{
key = "seed",
type = "slider",
label = "Seed",
default = 42,
min = 0,
max = 999999,
step = 1,
hint = "RNG seed for noise generation.",
},
},
}
-- ── State (file-level locals, reset when n changes) ──────────────────────────
local _prev_x = nil -- for momentum
local _prev_noise = nil -- for noise coherence
local _look_back_xp = nil -- for look-back smoother
local _sigma_max = nil -- captured at step 0
local _last_n = 0
-- Hoisted scratch tables — reused every step to avoid GC pressure
-- Initialized on first step or when n changes
local _noise_buf = {} -- reusable noise array
local _x_next_buf = {} -- reusable output array
local _x_copy_buf = {} -- reusable momentum copy
local EPSILON = 1e-8
-- ── Helpers ──────────────────────────────────────────────────────────────────
local function clamp(v, lo, hi)
if v < lo then return lo end
if v > hi then return hi end
return v
end
-- Seeded LCG RNG — deterministic, no dependency on math.random state
local function make_rng(seed)
local state = math.floor(seed) % 2147483647
if state <= 0 then state = state + 2147483646 end
return function()
state = (state * 1664525 + 1013904223) % 2147483648
return state / 2147483648.0
end
end
-- Box-Muller: two uniform [0,1] → one standard normal sample
local function normal(u1, u2)
return math.sqrt(-2.0 * math.log(math.max(u1, EPSILON))) * math.cos(2.0 * math.pi * u2)
end
-- Array RMS
local function rms(arr, n)
local s = 0.0
for i = 0, n - 1 do s = s + arr[i] * arr[i] end
return math.sqrt(s / n + EPSILON)
end
-- ── Required step() function ──────────────────────────────────────────────────
function step(xt, vt, t_curr, t_prev, n)
local step_idx_ = step_index or 0
-- Reset state on new generation: n change OR step 0 of any new run.
-- Must check step_idx_==0 because same-length generations won't trigger n change,
-- causing momentum/look-back to bleed finished audio from the previous run into
-- the noise of the new one — explosive velocity on step 1.
if n ~= _last_n or step_idx_ == 0 then
_prev_x = nil
_prev_noise = nil
_look_back_xp = nil
_sigma_max = nil
_last_n = n
-- Pre-size scratch tables for this n
for i = 0, n - 1 do
_noise_buf[i] = 0.0
_x_next_buf[i] = 0.0
_x_copy_buf[i] = 0.0
end
end
-- Read params with safe fallbacks
local anc_strength = (params and params.ancestral_strength) or 1.0
local noise_coh = (params and params.noise_coherence) or 0.0
local mom_str = (params and params.momentum_strength) or 0.1
local lb_enabled = (params and params.look_back_enabled) or false
local lb_lambda = (params and params.look_back_lambda) or 0.55
local lb_snr_power = (params and params.look_back_snr_power) or 1.3
local rms_servo_on = (params and params.rms_servo) or true
local rms_tgt_min = (params and params.rms_target_min) or 1.2
local rms_tgt_max = (params and params.rms_target_max) or 2.2
local rms_servo_gain = (params and params.rms_servo_gain) or 0.6
local seed = math.floor((params and params.seed) or 42)
-- Capture sigma_max on first step for ratio computation
if _sigma_max == nil then _sigma_max = t_curr end
local sigma_max = _sigma_max
local sigma_ratio = clamp(t_curr / math.max(sigma_max, EPSILON), 0.0, 1.0)
-- dt = t_prev - t_curr. In flow-matching, t steps DOWN (1→0),
-- HOT-Step API: t_curr=high sigma, t_prev=lower target. t_curr > t_prev. dt=t_prev-t_curr is NEGATIVE.
local dt = t_prev - t_curr
-- Save current xt for momentum (reuse hoisted buffer)
for i = 0, n - 1 do _x_copy_buf[i] = xt[i] end
-- ── NOISE GENERATION ──────────────────────────────────────────────────────
-- Write into hoisted buffer — no table allocation per step
local rng = make_rng(seed + step_idx_ * 7919)
for i = 0, n - 1 do
local u1 = math.max(rng(), EPSILON)
local u2 = rng()
_noise_buf[i] = normal(u1, u2)
end
-- Noise coherence: blend with carried noise from previous step
if noise_coh > 0.0 and _prev_noise ~= nil then
for i = 0, n - 1 do
_noise_buf[i] = _noise_buf[i] * (1.0 - noise_coh) + _prev_noise[i] * noise_coh
end
end
-- Store for next step — reuse _prev_noise table if same size
if _prev_noise == nil then _prev_noise = {} end
for i = 0, n - 1 do _prev_noise[i] = _noise_buf[i] end
-- ── ANCESTRAL STEP ────────────────────────────────────────────────────────
-- Variance-preserving SDE noise for flow matching:
-- noise_scale = sqrt(t_prev^2 - t_curr^2) * anc_strength
-- t_curr > t_prev, so t_curr^2 - t_prev^2 > 0. Confirmed numerically.
local noise_scale = math.sqrt(math.max(t_curr * t_curr - t_prev * t_prev, 0.0)) * anc_strength
if noise_scale > EPSILON then
for i = 0, n - 1 do
_x_next_buf[i] = xt[i] + dt * vt[i] + _noise_buf[i] * noise_scale
end
else
for i = 0, n - 1 do _x_next_buf[i] = xt[i] + dt * vt[i] end
end
-- ── MOMENTUM ──────────────────────────────────────────────────────────────
if mom_str > 0.0 and _prev_x ~= nil then
for i = 0, n - 1 do
local vel = _x_copy_buf[i] - _prev_x[i]
_x_next_buf[i] = _x_next_buf[i] + vel * mom_str
end
end
-- ── LOOK-BACK SNR SMOOTHER ────────────────────────────────────────────────
-- λ(σ) = lb_lambda * (σ/σ_max)^lb_snr_power — heavy early, fades late.
if lb_enabled then
local lb_w = lb_lambda * (sigma_ratio ^ lb_snr_power)
if _look_back_xp == nil then
_look_back_xp = {}
for i = 0, n - 1 do
local u1 = math.max(rng(), EPSILON)
local u2 = rng()
_look_back_xp[i] = _x_next_buf[i] + normal(u1, u2) * sigma_max * 0.1
end
end
for i = 0, n - 1 do
_x_next_buf[i] = _x_next_buf[i] * (1.0 - lb_w) + _look_back_xp[i] * lb_w
end
-- Update look-back buffer in-place
for i = 0, n - 1 do _look_back_xp[i] = _x_next_buf[i] end
end
-- ── RMS SERVO (DOWNWARD ONLY) ─────────────────────────────────────────────
if rms_servo_on then
local rms_target = rms_tgt_min + (sigma_ratio ^ 0.6) * (rms_tgt_max - rms_tgt_min)
local cur_rms = rms(_x_next_buf, n)
if cur_rms > rms_target then
local servo_rms = cur_rms + rms_servo_gain * (rms_target - cur_rms)
local scale = servo_rms / cur_rms
for i = 0, n - 1 do _x_next_buf[i] = _x_next_buf[i] * scale end
end
end
-- ── UPDATE STATE & WRITE OUTPUT ───────────────────────────────────────────
-- Store momentum reference — reuse table, copy values
if _prev_x == nil then _prev_x = {} end
for i = 0, n - 1 do _prev_x[i] = _x_copy_buf[i] end
for i = 0, n - 1 do xt[i] = _x_next_buf[i] end
end