fix(studio): cap the negative_erase renorm gain

A token pointing along the negative is left with rounding error after
projection; restoring its norm amplified that noise ~1000x into the
conditioning. Cap the gain and refuse a non-finite result.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
digital-garbage
2026-08-23 03:52:45 +03:00
co-authored by Claude Opus 5
parent 5bb36dfc07
commit 2b3a97ec8d
3 changed files with 66 additions and 2 deletions
+7
View File
@@ -3,6 +3,13 @@
## [Unreleased]
### Fixed
- **negative_erase could put an amplified-noise vector into the conditioning.** "Keep prompt
strength" restored each token's norm after the erase — but a token pointing almost exactly
along the negative is left with nothing but rounding error, and restoring its norm scaled
that residue by thousands. The gain is now capped: past it the token stays quiet, because
it really was mostly the thing you asked to remove. A non-finite result is refused outright
rather than passed on, since the sampler captures conditioning for the refinement key and
would have banked the bad vector.
- **Bypass now works as an A/B switch.** Wire two alternatives at one input — MiniMax H3's
ref-to-video and first-last-to-video both feeding the sampler's latent — and bypass the
one you are not using. A bypassed slot no longer claims the input, no longer counts as a
+18 -2
View File
@@ -22,6 +22,10 @@ Pure tensor math, no ComfyUI imports, so it is testable without a model.
MODES = ("project", "subtract")
# How far a token may be scaled back up after the erase. See the note in `erase`: without a
# cap, a token that WAS the unwanted concept comes back as amplified rounding error.
RENORM_MAX_GAIN = 2.0
def _torch():
import torch
@@ -107,8 +111,20 @@ def erase(pos_tensor, unit, strength, meta=None, mode="project", renorm=True):
rows = rows - float(strength) * before * u
if renorm:
after = rows.norm(dim=-1, keepdim=True).clamp_min(1e-6)
rows = rows * (before / after)
# The gain is CAPPED, and that cap is the whole safety of this operation. A token
# pointing almost exactly along the negative is left with nothing but rounding error
# after projection; scaling that back to its original norm multiplies noise by
# thousands and puts a garbage vector at full prompt strength into the conditioning.
# Past the cap the honest answer is that the token really was mostly the thing you
# asked to remove, so it stays quiet instead of being refilled with residue.
after = rows.norm(dim=-1, keepdim=True).clamp_min(1e-12)
rows = rows * (before / after).clamp(max=RENORM_MAX_GAIN)
# Never hand a non-finite conditioning onward. It does not fail here — it fails deep in
# the model, and worse, a run that captures conditioning for the refinement key would
# bank the bad vector and keep steering toward it long after this setting was turned off.
if not bool(torch.isfinite(rows).all()):
return pos_tensor
if idx is None:
work = rows
+41
View File
@@ -155,3 +155,44 @@ def test_a_dimension_mismatch_leaves_the_conditioning_alone():
out, note = ne.apply(pos, _cond(torch.tensor([[[1.0, 0.0]]])), 1.0)
assert "no conditioning entry could be modified" in note
assert out is pos
# --- the renorm gain cap ---------------------------------------------------------------
# Restoring a token's norm after projection is fine until the projection removed almost all
# of it. Then what is left is rounding error, and "restore the original norm" means putting
# amplified noise into the conditioning at full prompt strength — which the sampler will
# happily bank into the refinement key and keep steering toward afterwards.
def test_a_token_that_WAS_the_negative_goes_quiet_instead_of_exploding():
u = torch.tensor([1.0, 0.0])
pos = torch.tensor([[[5.0, 0.001]]]) # almost exactly the negative's direction
out = ne.erase(pos, u, 1.0, renorm=True)
assert float(out.norm()) < 0.01 # not 5.0 of amplified residue
assert float(out[0, 0, 1]) == pytest.approx(0.002, abs=1e-4) # capped at 2x, no more
def test_an_ordinary_token_still_gets_its_norm_back():
"""The cap must not break the normal case it exists to serve."""
u = torch.tensor([1.0, 0.0])
out = ne.erase(torch.tensor([[[3.0, 4.0]]]), u, 1.0, renorm=True)
assert float(out.norm()) == pytest.approx(5.0, abs=1e-4)
def test_the_gain_never_exceeds_the_cap():
u = torch.tensor([1.0, 0.0])
for orth in (0.001, 0.01, 0.1, 1.0, 4.0):
pos = torch.tensor([[[5.0, orth]]])
before = float(pos.norm())
after = float(ne.erase(pos, u, 1.0, renorm=True).norm())
residue = float(torch.tensor([0.0, orth]).norm())
assert after <= residue * ne.RENORM_MAX_GAIN + 1e-4
assert after <= before + 1e-4
def test_a_non_finite_result_is_refused_rather_than_passed_on():
"""It would not fail here — it fails deep in the model, and a run that captures
conditioning for the refinement key would bank the bad vector first."""
u = torch.tensor([float("nan"), 0.0])
pos = torch.tensor([[[3.0, 4.0]]])
assert ne.erase(pos, u, 1.0) is pos