Initial release
This commit is contained in:
@@ -0,0 +1,360 @@
|
||||
-- ============================================================================
|
||||
-- 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
|
||||
Reference in New Issue
Block a user