Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Tw9fjW75aYB8UyVRZo6fCr
139 lines
5.7 KiB
Python
139 lines
5.7 KiB
Python
"""Taste attention: tilt what chosen blocks' picture rows look FOR, toward what you liked.
|
|
|
|
Same learning as Taste steering (liked-minus-disliked on the taste key), but on
|
|
the attention queries instead of the block output. Only the query can move
|
|
anything: a constant added to every key is cancelled by softmax, and one added
|
|
to every value is just Taste steering again.
|
|
|
|
Learned and applied where the rows are NOT yet position-rotated (the model
|
|
module supplies the rotation): an average of rotated rows mostly cancels, and a
|
|
push added after rotation points somewhere different at every position --
|
|
steering by WHERE, not WHAT. v4's first version made exactly that mistake.
|
|
|
|
Picture rows only. Captured at every block, steers only the named ones.
|
|
"""
|
|
|
|
import torch
|
|
|
|
from ..._core import dit_hooks, log, registry
|
|
|
|
ID = "q_steer"
|
|
TITLE = "Taste attention"
|
|
MOUNT = "generation.sampling"
|
|
STAGE = "guidance"
|
|
CATEGORY = "guidance"
|
|
STATUS = "experimental"
|
|
REQUIRES = ["dit_block_hooks"]
|
|
USES = ["taste_store"]
|
|
|
|
SETTINGS = {
|
|
"enabled": {
|
|
"type": "bool", "default": False,
|
|
"label": "Taste attention",
|
|
"hint": "Learns from your ratings and shifts what the picture pays attention to. Does nothing until the Taste key has 2+ liked and 2+ disliked runs.",
|
|
},
|
|
"strength": {
|
|
"type": "float", "default": 2.0, "min": 0.0, "max": 10.0, "step": 0.05,
|
|
"label": "Strength", "ui": "slider",
|
|
"hint": "0 = only learn.",
|
|
"when": {"enabled": True},
|
|
},
|
|
"blocks": {
|
|
"type": "text", "model_blocks": True, "default": "43-44",
|
|
"label": "Blocks",
|
|
"hint": "Where it steers, e.g. 43-44 or 0,1,47-49. Every block keeps learning either way.",
|
|
"when": {"enabled": True},
|
|
},
|
|
}
|
|
|
|
KIND = "q_steer"
|
|
_SCOPE = "funui_q_steer"
|
|
|
|
|
|
def _say(message):
|
|
log.once(f"{ID}:{message}", log.ALERT, "FunUI Taste attention", message)
|
|
|
|
|
|
def install(patcher, values, key):
|
|
if not values.get("enabled"):
|
|
return None
|
|
taste = registry.current().ask("taste_store", patcher)
|
|
if taste is None:
|
|
_say("off: set a Taste key first -- it is what your ratings teach")
|
|
return None
|
|
n = dit_hooks.block_count(patcher)
|
|
steer, problems = dit_hooks.parse_blocks(values.get("blocks"), n)
|
|
for problem in problems:
|
|
_say(f"ignored {problem}")
|
|
strength = float(values.get("strength", 0.0))
|
|
|
|
live = {"dirs": {}}
|
|
|
|
def fresh():
|
|
live["dirs"], summary = taste.directions(KIND, steer, strength)
|
|
log.once(f"{ID}:state", log.INFO, "FunUI Taste attention", summary)
|
|
|
|
captured = taste.collect(patcher, key, KIND, fresh=fresh)
|
|
layout = {} # seq_len -> (lo, hi, rotate) | None
|
|
|
|
def scoped(block):
|
|
def hook(args, extra):
|
|
to = dict(args.get("transformer_options") or {})
|
|
to[_SCOPE] = (block, args.get("mod_segments"), args.get("rope_freqs"))
|
|
return extra["original_block"]({**args, "transformer_options": to})
|
|
return hook
|
|
|
|
for b in range(n):
|
|
dit_hooks.add_block_hook(patcher, key, b, scoped(b))
|
|
|
|
def resolve(segs, rope, seq, device):
|
|
if seq not in layout:
|
|
span = dit_hooks.row_span(dit_hooks.target_rows(segs, seq, device, "video"))
|
|
rotate = registry.current().ask("query_rotation", rope, seq)
|
|
layout[seq] = None if span is None or rotate is None else (*span, rotate)
|
|
if layout[seq] is None:
|
|
_say("could not find the picture rows or their rotation; "
|
|
"not learning or steering this run")
|
|
return layout[seq]
|
|
|
|
def override(func, q, k, v, *args, **kwargs):
|
|
to = kwargs.get("transformer_options") or {}
|
|
scope = to.get(_SCOPE)
|
|
if scope is None or q.dim() != 4 or q.shape[0] != 1:
|
|
return func(q, k, v, *args, **kwargs)
|
|
block, segs, rope = scope
|
|
seq = int(q.shape[2])
|
|
found = resolve(segs, rope, seq, q.device)
|
|
if found is None:
|
|
return func(q, k, v, *args, **kwargs)
|
|
lo, hi, rotate = found
|
|
rows = q[0, :, lo:hi] # [H, n, D]
|
|
heads, count, dim = rows.shape
|
|
if dit_hooks.last_step(to):
|
|
natural = rotate(rows, slice(lo, hi), inverse=True)
|
|
captured[block] = natural.transpose(0, 1).reshape(count, -1).mean(0, dtype=torch.float32)
|
|
direction = live["dirs"].get(block)
|
|
if direction is None or strength <= 0.0:
|
|
return func(q, k, v, *args, **kwargs)
|
|
if direction.numel() != heads * dim:
|
|
_say(f"block {block}'s learned direction is from a different model; learning only there")
|
|
return func(q, k, v, *args, **kwargs)
|
|
# Rotation keeps length, so the rotated rows' size is the natural one's.
|
|
norm = torch.linalg.vector_norm(rows, dim=-1, dtype=torch.float32).mean()
|
|
push_strength = dit_hooks.block_map_value(to, "attention", block, strength, steer) # nudged by the Block map
|
|
scale = torch.where(torch.isfinite(norm) & (norm > 0), norm * push_strength, torch.zeros_like(norm))
|
|
push = (direction.to(device=q.device, dtype=q.dtype).view(heads, 1, dim)
|
|
* scale.to(q.dtype)).expand(heads, count, dim)
|
|
q = q.clone()
|
|
q[0, :, lo:hi] += rotate(push, slice(lo, hi))
|
|
return func(q, k, v, *args, **kwargs)
|
|
|
|
dit_hooks.add_attention_override(patcher, key, override)
|
|
# Changes what the hooked blocks allocate: see dit_hooks.without_compiler.
|
|
dit_hooks.without_compiler(patcher, key)
|
|
return (f"learning at every block; steers {','.join(map(str, steer))} at {strength:g} "
|
|
"once each has 2 liked + 2 disliked (read fresh every run)")
|
|
|
|
|
|
PROVIDES = {"modifier": install}
|