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:
co-authored by
Claude Opus 5
parent
5bb36dfc07
commit
2b3a97ec8d
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user