diff --git a/CHANGELOG.md b/CHANGELOG.md index 64e39e5b..6a56a9d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ ### Added (experimental) -- **Noise merging** (Settings > Engine > Sampling): "Merge" can also mix the run's starting noise with a second seed's (no extra cost, variance kept). Or, at the steps you name, the step is answered a second time on the same picture with different noise (a second seed), and the two pictures are blended at your strength. Merge order picks which one is the base. The second seed is random each run unless you type one. Picture and sound are both merged (the sound can be left out with "Merge sound too"). Costs one extra model pass per merged step. Untested on a real H3. +- **Noise merging** (Settings > Engine > Sampling): "Merge" can also mix the run's starting noise with a second seed's (no extra cost, variance kept). Or, at the steps you name, the step is answered a second time on the same picture with different noise (a second seed), and the two pictures are blended at your strength. Merge order picks which one is the base. The second seed is random each run unless you type one. Picture and sound are both merged (the sound can be left out with "Merge sound too"). "Second pass new noise" sets how much of the step's own noise the second pass swaps for the second seed's (1 all, lower keeps more of your own). Costs one extra model pass per merged step. Untested on a real H3. - **SLA rising sparsity** (diffusion model loader, `sla_sparsity_start`): skip fewer attention blocks on early steps and more on late ones, in a straight line up to `sla_sparsity`. Off at 0. One real H3 render (4 steps, 0.70 to 0.90): quality held, perhaps slightly better. Idea from GSRS (arXiv 2610.21129); only the schedule is taken, not its other parts. ## [5.0.3] "Classy Chopper" - 2026-10-25 diff --git a/modules/sampling/noise_merge/__init__.py b/modules/sampling/noise_merge/__init__.py index cfea92e4..aa987e6a 100644 --- a/modules/sampling/noise_merge/__init__.py +++ b/modules/sampling/noise_merge/__init__.py @@ -67,6 +67,12 @@ SETTINGS = { "hint": "0 keeps your own result untouched, 1 replaces it with the second seed's. For starting noise, 0.5 is an even mix.", "when": {"enabled": True}, }, + "fresh": { + "type": "float", "default": 1.0, "min": 0.0, "max": 1.0, "step": 0.05, + "label": "Second pass new noise", "ui": "slider", + "hint": "How much of the step's own noise the second pass swaps for the second seed's. 1 swaps all of it; lower keeps more of your own noise, so B stays closer to A. 0 gives B = A and the merge changes nothing.", + "when": {"enabled": True, "what": "answers"}, + }, "sound": { "type": "bool", "default": True, "label": "Merge sound too", @@ -97,11 +103,18 @@ def merge(base, other, strength): return ((mixed.float() - mean) * (base_std / mixed_std) + mean).to(base.dtype) -def renoise(x0, sigma, seed, step): - """A rectified-flow input at `sigma` for picture x0 with fresh noise: (1 - sigma) x0 + sigma noise.""" +def second_input(x, a, sigma, seed, step, fresh): + """The input for picture B. Rectified flow: x = (1 - sigma) picture + sigma noise. A's answer is the picture; + the noise is the step's own noise (what is left of x once A's picture is taken out) turned `fresh` of the + way toward the second seed's noise, at unit variance. fresh 1 is all second-seed noise, 0 hands back x.""" + if sigma < 1e-4: + return x + own = (x.float() - (1.0 - sigma) * a.float()) / sigma gen = torch.Generator().manual_seed(int(seed) + 7919 * int(step)) - noise = torch.randn(x0.shape, generator=gen).to(device=x0.device, dtype=x0.dtype) - return (1.0 - sigma) * x0 + sigma * noise + other = torch.randn(x.shape, generator=gen).to(device=x.device) + theta = fresh * torch.pi / 2 + noise = float(torch.cos(torch.tensor(theta))) * own + float(torch.sin(torch.tensor(theta))) * other + return ((1.0 - sigma) * a.float() + sigma * noise).to(x.dtype) def _sound(packed, video): @@ -157,6 +170,7 @@ def install(patcher, values, key): return None b_first = values.get("order", "b_into_a") == "b_into_a" with_sound = bool(values.get("sound", True)) + fresh = float(values.get("fresh", 1.0)) chosen = int(values.get("seed", -1)) live = {"seed": max(chosen, 0), "merged": 0} @@ -192,10 +206,11 @@ def install(patcher, values, key): video_a, rebuild_a = split_a tail_a = _sound(out, video_a) gen_step = where[0] - x_b = split_x[1](renoise(video_a.detach(), sigma, live["seed"], gen_step).to(split_x[0].dtype)) + x_b = split_x[1](second_input(split_x[0], video_a.detach(), sigma, live["seed"], gen_step, fresh)) if tail_a is not None and with_sound: n = x_b.shape[-1] - tail_a.shape[-1] - x_b = torch.cat([x_b[..., :n], renoise(tail_a.detach(), sigma, live["seed"] + 1, gen_step)], dim=-1) + x_b = torch.cat([x_b[..., :n], second_input(x[..., n:], tail_a.detach(), sigma, live["seed"] + 1, + gen_step, fresh)], dim=-1) args_b, kwargs_b = streams.with_options(args, kwargs, {**to, dit_hooks.PROBE: True}) began = time.perf_counter() out_b = executor(x_b, t, *args_b, **kwargs_b) diff --git a/modules/sampling/noise_merge/tests/test_noise_merge.py b/modules/sampling/noise_merge/tests/test_noise_merge.py index 71645d9b..6d4878b7 100644 --- a/modules/sampling/noise_merge/tests/test_noise_merge.py +++ b/modules/sampling/noise_merge/tests/test_noise_merge.py @@ -46,12 +46,19 @@ def merge_(*a): return merge(*a) -def test_renoise_is_seeded_and_lands_on_the_step_noise_level(): - from modules.sampling.noise_merge import renoise - x0 = torch.randn(1, 4, 3, 8, 8) - assert torch.equal(renoise(x0, 0.5, 1, 2), renoise(x0, 0.5, 1, 2)) - assert not torch.equal(renoise(x0, 0.5, 1, 2), renoise(x0, 0.5, 2, 2)) - assert torch.equal(renoise(x0, 0.0, 1, 2), x0) +def test_the_second_input_keeps_the_step_noise_at_zero_and_is_all_second_seed_at_one(): + from modules.sampling.noise_merge import second_input + a = torch.randn(1, 4, 3, 8, 8) + x = 0.4 * a + 0.6 * torch.randn(1, 4, 3, 8, 8) + assert torch.allclose(second_input(x, a, 0.6, 1, 2, 0.0), x, atol=1e-5) + full = second_input(x, a, 0.6, 1, 2, 1.0) + assert torch.equal(full, second_input(x, a, 0.6, 1, 2, 1.0)) + assert not torch.equal(full, second_input(x, a, 0.6, 2, 2, 1.0)) + gen = torch.Generator().manual_seed(1 + 7919 * 2) + assert torch.allclose(full, 0.4 * a + 0.6 * torch.randn(x.shape, generator=gen), atol=1e-4) + half = second_input(x, a, 0.6, 1, 2, 0.5) + assert not torch.allclose(half, x) and not torch.allclose(half, full) + assert torch.equal(second_input(x, a, 0.0, 1, 2, 1.0), x) def test_a_named_step_runs_the_model_twice_and_others_once(tiny_h3):