Files
digital-garbage 1b9f1cb6aa Review: Brownian noise is the stock noise again; block map profile survives audio+video blocks and cancel
Queries are clamped into torchsde's span instead of moving the span. A cancelled profile saves nothing.
Attention explorer temperature stays above zero under the map; unmeasured groups are not given 3x.
2026-11-05 15:10:18 +02:00

416 lines
20 KiB
Python

"""The attention explorer: the learner's maths, then the real modifier loader on a real H3 forward."""
import random
import pytest
import torch
@pytest.fixture(autouse=True)
def _needs_comfy(comfyui):
"""Imports comfy."""
@pytest.fixture(autouse=True)
def root(tmp_path, monkeypatch):
from modules.system.taste import store
monkeypatch.setattr(store, "ROOT", tmp_path / "taste")
monkeypatch.setattr(store, "current_prompt_id", lambda: "run-1")
def _row(reward, arms, sig):
return {"reward": reward, "rows": {"arms": torch.tensor(arms), "sig": torch.tensor(sig)}}
def test_split_is_by_depth_and_even():
from modules.sampling.attention_bandit import split
parts = split(list(range(10)), 4)
assert [len(p) for p in parts] == [3, 3, 2, 2] and sum(parts, []) == list(range(10))
assert split([5, 6], 4) == [[5], [6]]
def test_a_fresh_key_mostly_stays_as_is_and_a_liked_arm_takes_over():
from modules.sampling.attention_bandit import STAY, counts, draw
rng = random.Random(0)
table = counts([], [2, 0, 9], 2)
stay = sum(a == STAY for _ in range(300) for a in draw(table, rng))
assert stay > 0.5 * 600 # head start keeps a new key close to the plain model
rows = [_row(1.0, [0, 2], [2, 0, 9]) for _ in range(12)]
liked = counts(rows, [2, 0, 9], 2)
picks = [draw(liked, rng) for _ in range(200)]
assert sum(p[0] == 0 for p in picks) > 120 and sum(p[1] == 2 for p in picks) > 120
def test_a_dislike_pushes_away_and_other_layouts_and_neutral_ratings_are_ignored():
from modules.sampling.attention_bandit import counts
rows = [_row(-1.0, [0, 1], [2, 0, 9]), _row(1.0, [2, 2], [2, 0, 10]), _row(0.0, [2, 2], [2, 0, 9])]
t = counts(rows, [2, 0, 9], 2)
assert t[0][0] == [0.0, 1.0] and t[0][2] == [0.0, 0.0] and t[1][1] == [0.0, 1.0]
def _srow(reward, arms, spread, sig=(2, 0, 9)):
return {"reward": reward, "rows": {"arms": torch.tensor(arms), "sig": torch.tensor(list(sig)),
"spread": torch.tensor(spread)}}
def test_the_size_moves_toward_liked_and_away_from_disliked_and_ignores_unchanged_runs():
from modules.sampling.attention_bandit import LEARN_MAX, SPREAD_MIN, START_SPREAD, learned_spread
sig = [2, 0, 9]
assert learned_spread([], sig) == START_SPREAD
assert learned_spread([_srow(1.0, [0, 1], 0.4)], sig) > START_SPREAD
assert learned_spread([_srow(-1.0, [0, 1], 0.4)], sig) < START_SPREAD
assert learned_spread([_srow(1.0, [1, 1], 0.4)], sig) == START_SPREAD # nothing changed
assert learned_spread([_srow(1.0, [0, 1], 0.4, sig=(3, 0, 9))], sig) == START_SPREAD # other layout
assert learned_spread([_srow(1.0, [0, 0], 0.5)] * 30, sig) == pytest.approx(LEARN_MAX, abs=1e-3)
assert SPREAD_MIN <= learned_spread([_srow(-1.0, [0, 0], 0.5)] * 30, sig) <= LEARN_MAX
def test_dislikes_of_small_changes_never_grow_the_size():
from modules.sampling.attention_bandit import START_SPREAD, learned_spread
assert learned_spread([_srow(-1.0, [0, 1], 0.1)] * 20, [2, 0, 9]) == START_SPREAD
assert learned_spread([_srow(-1.0, [0, 1], 0.4)] * 20, [2, 0, 9]) < START_SPREAD
def test_the_log_names_exact_blocks_and_skips_the_unchanged():
from modules.sampling.attention_bandit import describe
groups = [[2, 3, 4], [9, 10, 29, 30], [34, 35], [41, 42]]
assert describe(groups, [1, 1, 1, 1], 0.2) == ""
assert describe(groups, [0, 1, 2, 0], 0.2) == "sharper 0.8x at blocks 2-4,41-42; looser 1.2x at blocks 34-35"
def test_a_learned_run_banks_its_size(tiny_h3, monkeypatch):
from modules.sampling import attention_bandit as m
from modules.system.taste import store
patched, status = _load(tiny_h3, blocks="0-3")
assert "size learned from ratings" in status
monkeypatch.setattr(m, "draw", lambda table, rng=random, **kw: [0] * len(table))
_run(_outer(patched), lambda: tiny_h3.run(patched))
store.rate("run-1", "liked")
row = store.load("fox", "attention_bandit")["rows"][0]
assert 0.05 <= float(row["rows"]["spread"]) <= 0.5
def _load(tiny, **values):
from modules.sampling.modifiers.nodes import FunUILoadModifiers
settings = {"taste": {"key": "fox"}, "attention_bandit": {"enabled": True, **values}}
return FunUILoadModifiers.execute(tiny.patcher, settings).result
def _outer(patched):
from comfy.patcher_extension import WrappersMP
return [w for ws in patched.wrappers[WrappersMP.OUTER_SAMPLE].values() for w in ws]
def _run(outer, fn):
call = fn
for o in reversed(outer):
call = (lambda w, inner: (lambda: w(lambda: inner())))(o, call)
return call()
def test_a_run_changes_the_output_banks_its_arms_and_a_rating_teaches_them(tiny_h3, monkeypatch):
from modules.system.taste import store
base = tiny_h3.run()
patched, status = _load(tiny_h3, mode="manual", range=0.4, blocks="0-3")
assert "4 depth groups over 4 blocks" in status
from modules.sampling import attention_bandit as m
monkeypatch.setattr(m, "draw", lambda table, rng=random, **kw: [2] * len(table))
outs = _run(_outer(patched), lambda: tiny_h3.run(patched))
assert not torch.equal(outs[0], base[0]), "the explorer ran and changed nothing"
assert store.rate("run-1", "liked")["recorded"] == ["attention_bandit"]
row = store.load("fox", "attention_bandit")["rows"][0]
assert row["rows"]["arms"].tolist() == [2, 2, 2, 2] and row["reward"] == 1.0
def test_stay_everywhere_is_the_plain_model(tiny_h3, monkeypatch):
from modules.sampling import attention_bandit as m
base = tiny_h3.run()
patched, _ = _load(tiny_h3, blocks="0-3")
monkeypatch.setattr(m, "draw", lambda table, rng=random, **kw: [m.STAY] * len(table))
outs = _run(_outer(patched), lambda: tiny_h3.run(patched))
assert torch.equal(outs[0], base[0])
def test_no_taste_key_installs_nothing_and_says_so(tiny_h3):
from core import log
from modules.sampling.modifiers.nodes import FunUILoadModifiers
log.new_run()
FunUILoadModifiers.execute(tiny_h3.patcher, {"attention_bandit": {"enabled": True}})
assert any("Taste key" in e["message"] for e in log.history())
def test_a_block_list_with_nothing_usable_is_refused(tiny_h3):
_p, status = _load(tiny_h3, blocks="40-49")
assert "attention_bandit: failed to install" in status
def test_a_rating_changes_the_taste_cache_key_so_the_same_seed_runs_again(tiny_h3, monkeypatch):
from modules.sampling import attention_bandit as m
from modules.system.taste import cache_key, store
patched, _ = _load(tiny_h3, blocks="0-3")
assert cache_key() == ""
monkeypatch.setattr(m, "draw", lambda table, rng=random, **kw: [0] * len(table))
_run(_outer(patched), lambda: tiny_h3.run(patched))
assert cache_key() == "" # a waiting capture is not a rating
store.rate("run-1", "liked")
first = cache_key()
assert first
monkeypatch.setattr(store, "current_prompt_id", lambda: "run-2")
_run(_outer(patched), lambda: tiny_h3.run(patched))
store.rate("run-2", "liked")
assert cache_key() != first
def test_the_log_counts_rated_runs():
from modules.sampling.attention_bandit import tally
rows = [_row(1.0, [1], [1, 0, 0]), _row(-1.0, [1], [1, 0, 0]), _row(-1.0, [1], [1, 0, 0])]
assert tally(rows) == "3 rated runs (1 liked, 2 disliked)"
assert tally([]) == "0 rated runs (0 liked, 0 disliked)"
def test_the_log_says_how_many_attention_calls_were_changed(tiny_h3, monkeypatch):
from core import log
from modules.sampling import attention_bandit as m
patched, _ = _load(tiny_h3, mode="manual", blocks="0-3")
monkeypatch.setattr(m, "draw", lambda table, rng=random, **kw: [0] * len(table))
log.new_run()
_run(_outer(patched), lambda: tiny_h3.run(patched))
assert any("applied to" in e["message"] and "attention calls" in e["message"] for e in log.history())
# ── prompt weighting, recipes, prompt focus, clip regions ─────────────────────
def _cond(seed, n=16):
g = torch.Generator().manual_seed(seed)
return torch.randn(n, generator=g)
def test_the_same_prompt_counts_fully_another_prompt_counts_for_the_floor_and_unknown_in_between():
from modules.sampling.attention_bandit import FLOOR, UNKNOWN, similarity, weights_of
a, b = _cond(1), _cond(2)
assert similarity(a, a) == pytest.approx(1.0)
assert similarity(a, b) < 0.01 and similarity(a, None) is None and similarity(None, a) is None
rows = [{"reward": 1.0, "rows": {"cond": a.half()}}, {"reward": 1.0, "rows": {"cond": b.half()}},
{"reward": 1.0, "rows": {}}]
w, sims = weights_of(rows, a)
assert w[0] == pytest.approx(1.0, abs=1e-3) and w[1] == pytest.approx(FLOOR, abs=0.02) and w[2] == UNKNOWN
def test_counts_use_the_weights():
from modules.sampling.attention_bandit import counts
rows = [_row(1.0, [0], [1, 0, 0]), _row(1.0, [0], [1, 0, 0])]
assert counts(rows, [1, 0, 0], 1, weights=[1.0, 0.1])[0][0][0] == pytest.approx(1.1)
rows[0]["rows"]["tarms"] = torch.tensor([2])
assert counts(rows, [1, 0, 0], 1, "tarms")[0][2] == [1.0, 0.0] # only the row that has the field
def _combo_row(reward, cond, arms=(0, 1), tarms=(2, 1), rarms=(1, 1, 1, 1, 1), spread=0.3, sig=(2, 0, 9)):
return {"reward": reward, "rows": {"arms": torch.tensor(list(arms)), "tarms": torch.tensor(list(tarms)),
"rarms": torch.tensor(list(rarms)), "sig": torch.tensor(list(sig)),
"spread": torch.tensor(spread), "cond": cond.half()}}
def test_a_combination_liked_on_this_prompt_is_reused_and_one_liked_on_another_is_not():
from modules.sampling.attention_bandit import make_plan
here, other = _cond(1), _cond(2)
rows = [_combo_row(1.0, here) for _ in range(8)]
rng = random.Random(0)
plans = [make_plan(rows, here, [2, 0, 9], 2, True, regions=True, rng=rng) for _ in range(40)]
reused = [p for p in plans if p["recipe"]]
assert len(reused) > 30 and all(p["same"] == 8 for p in plans)
assert sum(p["arms"][0] == 0 for p in reused) > 0.6 * len(reused) # mostly the recipe, some groups re-drawn
away = [make_plan(rows, other, [2, 0, 9], 2, True, regions=True, rng=rng) for _ in range(40)]
assert not any(p["recipe"] for p in away) and all(p["same"] == 0 for p in away)
def test_disliked_combinations_are_not_a_recipe():
from modules.sampling.attention_bandit import make_plan
here = _cond(1)
rows = [_combo_row(-1.0, here) for _ in range(8)]
assert not any(make_plan(rows, here, [2, 0, 9], 2, True, regions=True, rng=random.Random(i))["recipe"] for i in range(30))
def _force(monkeypatch, arms=1, tarms=1, rarms=1):
from modules.sampling import attention_bandit as m
def fixed(table, rng=random, **kw):
n = len(table)
return [{4: arms, 5: rarms}.get(n, arms)] * n if n == 5 else [arms] * n
seq = iter([])
calls = []
def draw(table, rng=random, **kw):
calls.append(len(table))
# the first draw is the plain groups, the second the prompt groups, the third the regions
pick = (arms, tarms, rarms)[min(len(calls) - 1, 2)]
return [pick] * len(table)
monkeypatch.setattr(m, "draw", draw)
def test_prompt_focus_alone_changes_the_output_and_says_where(tiny_h3, monkeypatch):
from core import log
base = tiny_h3.run()
patched, status = _load(tiny_h3, mode="manual", range=0.5, blocks="0-3", regions=False)
assert "prompt focus on" in status
_force(monkeypatch, arms=1, tarms=2)
log.new_run()
outs = _run(_outer(patched), lambda: tiny_h3.run(patched))
assert not torch.equal(outs[0], base[0])
messages = [e["message"] for e in log.history()]
assert any("prompt stronger" in m and "blocks 0-3" in m for m in messages)
assert any("prompt keys scaled in" in m for m in messages)
def test_with_prompt_focus_off_nothing_about_the_prompt_is_drawn_or_stored(tiny_h3, monkeypatch):
from modules.system.taste import store
patched, _ = _load(tiny_h3, blocks="0-3", prompt_focus=False, regions=False)
_force(monkeypatch, arms=1)
outs = _run(_outer(patched), lambda: tiny_h3.run(patched))
assert torch.equal(outs[0], tiny_h3.run()[0])
store.rate("run-1", "liked")
assert "tarms" not in store.load("fox", "attention_bandit")["rows"][0]["rows"]
def test_clip_regions_change_only_the_rows_they_name(tiny_h3, monkeypatch):
from modules.sampling.attention_bandit import REGIONS, region_spans
captured = {}
def spy(func, q, k, v, *a, **kw):
options = kw.get("transformer_options") or {}
# the prompt refiner also runs with the layout in its options, on the prompt rows alone
if "minimax_h3_layout" in options and q.shape[2] > captured.get("rows", 0):
captured["spans"] = region_spans(options)
captured["rows"] = q.shape[2]
return func(q, k, v, *a, **kw)
from core import dit_hooks
probe = tiny_h3.patcher.clone()
dit_hooks.add_attention_override(probe, "test.spy", spy)
tiny_h3.run(probe)
spans = captured["spans"]
assert len(spans) == len(REGIONS)
flat = sorted(spans)
assert all(a[1] <= b[0] for a, b in zip(flat, flat[1:])) # no row in two regions
assert all(0 <= a < b <= captured["rows"] for a, b in spans)
def test_clip_regions_change_the_output_and_stay_off_when_all_as_is(tiny_h3, monkeypatch):
from core import log
base = tiny_h3.run()
patched, _ = _load(tiny_h3, mode="manual", range=0.5, blocks="0-3", prompt_focus=False)
_force(monkeypatch, arms=1, rarms=0)
# prompt focus is off, so the draws are groups then regions
from modules.sampling import attention_bandit as m
picks = iter([1, 0])
monkeypatch.setattr(m, "draw", lambda table, rng=random, **kw: [next(picks)] * len(table))
log.new_run()
outs = _run(_outer(patched), lambda: tiny_h3.run(patched))
assert not torch.equal(outs[0], base[0])
messages = [e["message"] for e in log.history()]
assert any("clip regions sharper" in m and "sound" in m for m in messages)
assert any("clip regions in" in m for m in messages)
def test_the_real_sla_override_underneath_still_gets_a_normal_call(tiny_h3, monkeypatch):
"""SLA hands anything off its fast path to dense and records a kernel failure if a call is odd.
Regions rescale rows of q in place on a copy, so shape, dtype and layout must be what SLA saw before."""
from core import dit_hooks
from modules.loaders import sla_attention as sla
seen = []
def spy(func, q, k, v, *a, **kw):
# strides are left out on purpose: the copy of q is compact, which SLA reads as well as the fused-buffer view
seen.append((tuple(q.shape), q.dtype, tuple(k.shape), kw.get("skip_reshape"), kw.get("mask")))
return func(q, k, v, *a, **kw)
def run_with(on):
probe = tiny_h3.patcher.clone()
state = sla.new_state()
to = dit_hooks._transformer_options(probe)
cfg = {**sla.SLA_DEFAULTS, "engine": "triton", "dense_steps": frozenset(), "min_seq_len": 0}
to["optimized_attention_override"] = sla.make_override(state, cfg)
dit_hooks.add_attention_override(probe, "test.spy", spy) # sits on top: sees exactly what SLA would get
patched, _ = _load(type("T", (), {"patcher": probe})(), mode="manual", range=0.5, blocks="0-3")
picks = iter([1, 1, 0 if on else 1])
from modules.sampling import attention_bandit as m
monkeypatch.setattr(m, "draw", lambda table, rng=random, **kw: [next(picks)] * len(table))
seen.clear()
_run(_outer(patched), lambda: tiny_h3.run(patched))
return list(seen), state
plain, state_plain = run_with(False)
scaled, state_scaled = run_with(True)
assert plain and scaled == plain # the same calls, same shapes, strides and flags
assert state_scaled["failed"] is None and state_scaled["calls"] == state_plain["calls"]
def test_spare_sound_keeps_the_sound_rows_and_follows_device_and_dtype():
from modules.sampling.attention_bandit import spare_sound
class Layout:
segments = [(0, 2, "text"), (2, 6, "video"), (6, 8, "audio")]
options = {"minimax_h3_layout": Layout()}
for device, dtype in (("cpu", torch.float32), ("meta", torch.bfloat16)):
q = torch.ones(1, 2, 8, 4, device=device, dtype=dtype)
out = spare_sound(q, 2.0, options, False, None)
assert out.device == q.device and out.dtype == q.dtype
if device == "cpu":
assert torch.equal(out[:, :, :6], torch.full((1, 2, 6, 4), 0.5)) and torch.equal(out[:, :, 6:], q[:, :, 6:])
assert spare_sound(torch.ones(1, 2, 8, 4), 2.0, {}, False, None) is None # layout unknown: the caller says so
# a model with a stream of its own for the sound: its call stays whole, the picture's is cooled
assert torch.equal(spare_sound(torch.ones(1, 8, 16), 2.0, {}, True, 16), torch.ones(1, 8, 16))
assert torch.equal(spare_sound(torch.ones(1, 8, 32), 2.0, {}, True, 16), torch.full((1, 8, 32), 0.5))
def test_with_the_sound_kept_the_sound_is_left_alone_and_the_log_says_so(tiny_h3, monkeypatch):
from core import log
from modules.sampling import attention_bandit as m
base = tiny_h3.run()
seen = {}
def spy(func, q, k, v, *a, **kw):
options = kw.get("transformer_options") or {}
audio = m._segments(options).get("audio")
if audio and q.shape[2] >= audio[1]:
seen.setdefault("q", []).append(q[:, :, audio[0]:audio[1]].clone())
return func(q, k, v, *a, **kw)
from core import dit_hooks
probe = tiny_h3.patcher.clone()
dit_hooks.add_attention_override(probe, "test.spy", spy)
patched, status = _load(type("T", (), {"patcher": probe})(), mode="manual", range=0.5, blocks="0-3", sound=False)
assert "sound kept as is" in status and "prompt focus" not in status
monkeypatch.setattr(m, "draw", lambda table, rng=random, **kw: [2] * len(table))
log.new_run()
outs = _run(_outer(patched), lambda: tiny_h3.run(patched))
assert not torch.equal(outs[0], base[0]) # the picture still changed
messages = [e["message"] for e in log.history()]
assert any("sound left alone in" in x for x in messages)
drawn = [x for x in messages if "clip regions looser" in x]
assert drawn and not any("end of the clip, sound" in x for x in drawn) # the sound is never among the changed regions
def test_the_block_map_spreads_the_change_between_blocks_around_one():
from modules.sampling.attention_bandit import followed
from modules.sampling.block_map import measure
grid = measure.groups(10)
data = {"blocks": 10, "groups": grid, "points": list(measure.POINTS),
"attention": {"leverage": [[1.0] * 3, [4.0] * 3], "fragility": [[0.1] * 3, [0.1] * 3]}}
options = {"funui_block_map": measure.MapView(data), "sample_sigmas": torch.linspace(1, 0, 5), "sigmas": torch.tensor([0.5])}
blocks = [1, 2, 7, 8]
low, high = followed((1.2, 1.0), options, 1, blocks), followed((1.2, 1.0), options, 7, blocks)
assert low[0] > 1.2 > high[0] > 1.0 and low[1] == high[1] == 1.0 # the sharper change is larger where the block takes it
assert followed((1.2, 1.0), {}, 1, blocks) == (1.2, 1.0) # no map: as set
assert followed((1.0, 1.0), options, 1, blocks) == (1.0, 1.0)
def test_a_block_map_can_never_turn_a_temperature_to_zero_or_negative():
from modules.sampling.attention_bandit import followed
from modules.sampling.block_map import measure
grid = measure.groups(10)
lev = [[0.1] * 3] + [[1.0] * 3] # the first blocks barely move the picture: the biggest spread
data = {"blocks": 10, "groups": grid, "points": list(measure.POINTS),
"attention": {"leverage": lev, "fragility": [[0.0] * 3, [0.0] * 3]}}
options = {"funui_block_map": measure.MapView(data), "sample_sigmas": torch.linspace(1, 0, 5), "sigmas": torch.tensor([0.5])}
for size in (0.2, 0.35, 0.5, 0.9):
assert min(followed((1.0 - size, 1.0 - size), options, 1, [1, 7])) >= 0.05