Files
olivv-cs-ComfyUI-FunPack/modules/sampling/block_influence/__init__.py
T

159 lines
6.7 KiB
Python

"""Block influence: which blocks move the picture, and does it differ on clips you liked?
MEASUREMENT ONLY -- nothing about a generation changes. Each block of a residual
transformer adds `x_next = x + f(x)`. On every step this records how big f(x) is
on the PICTURE rows (raw, and relative to x), and whether each block's push goes
the same way as the block before it. A run's averages wait on the taste key for a
rating, like every other learner.
What it can and cannot say. A big push means the block moved the stream there --
not that the move reached the finished clip; later blocks can undo an early one.
The causal version is ablation, offline. What this gives, free, is the shape: if
every block moves the stream equally (flat) rating-driven per-block weighting has
nothing to aim at. A flat result is a real answer here, not a failure.
Prior art: per-block steering from ratings was built in v4 and removed (block
activity differed in the 4th decimal). That version averaged over steps before
looking. Each block's push is averaged over the steps of a run, so a per-step
signal that cancels within a run is lost (a known limit, not yet fixed).
Switched on from Settings > Refinement & Taste (on by default; it is research
data). The hooks sit on the model whenever a taste key is set and read the
switch at the start of each run, because ComfyUI caches the node that installs
them and a toggle must reach the next generation.
"""
import torch
from ..._core import dit_hooks, log, registry
from . import measure
ID = "block_influence"
TITLE = "Block influence"
MOUNT = "generation.sampling"
STAGE = "guidance"
CATEGORY = "system"
STATUS = "experimental"
REQUIRES = ["dit_block_hooks"]
USES = ["taste_store"]
# Measured before anything that changes a block's output, so the number is the
# block's OWN push and not another mechanism's injection.
BEFORE = ["reins", "block_repeat", "shadow_negative", "q_steer", "attention_temperature"]
KIND = measure.KIND
def _say(message):
log.once(f"{ID}:{message}", log.ALERT, "FunPack Block influence", message)
def install(patcher, values, key):
taste = registry.current().ask("taste_store", patcher)
if taste is None:
measure.problem = "no Taste key is set, so nothing is measured or kept: set a Taste key"
if measure.enabled():
_say("recording is on but " + measure.problem)
return None # no key: nothing to pair a profile with
n = dit_hooks.block_count(patcher)
if n == 0:
measure.problem = "this model type is not supported yet (only H3 blocks are read), so nothing is measured"
_say("off: " + measure.problem)
return None
measure.problem = None
live = {"on": False, "tally": measure.Tally(n)}
def hook_for(block):
def hook(args, extra):
to = args.get("transformer_options")
if not live["on"] or dit_hooks.probing(to):
return extra["original_block"](args)
return live["tally"].measure(block, args, extra["original_block"])
return hook
for b in range(n):
dit_hooks.add_block_hook(patcher, key, b, hook_for(b))
from comfy.patcher_extension import WrappersMP
def outer(executor, *args, **kwargs):
live["on"] = measure.enabled()
live["tally"] = measure.Tally(n)
out = executor(*args, **kwargs)
if live["on"]:
rows = live["tally"].rows()
measure.problem = None
if rows is None:
measure.problem = (f"the last recording run (key {taste.key!r}) measured nothing: no "
"picture rows found in any block")
log.warning("FunPack Block influence",
"recording is on but nothing was measured this run (no picture rows "
"found in any block); this run teaches nothing")
else:
taste.capture(KIND, rows)
log.once(f"{ID}:run", log.INFO, "FunPack Block influence",
f"Active | measured {live['tally'].blocks_seen()} of {n} blocks on key "
f"{taste.key!r}; rate the clip to keep it")
else:
measure.problem = None
log.once(f"{ID}:off", log.INFO, "FunPack Block influence",
"Inactive | recording is off (Settings > Refinement & Taste turns it on)")
return out
patcher.add_wrapper_with_key(WrappersMP.OUTER_SAMPLE, key, outer)
return "measures per-block movement when recording is on (read fresh every run)"
def routes(table, base, web):
def key_of(req, body=None):
raw = (body or {}).get("key") if body is not None else req.rel_url.query.get("key")
return str(raw or "default").strip() or "default"
@table.get(base + "/status")
async def _status(req):
try:
return web.json_response(measure.state(key_of(req)))
except ValueError as exc:
return web.json_response({"why": str(exc)}, status=400)
@table.get(base + "/groups")
async def _groups(req):
try:
return web.json_response(measure.four_groups(key_of(req)))
except ValueError as exc:
return web.json_response({"why": str(exc)}, status=400)
@table.post(base + "/enabled")
async def _enabled(req):
body = await req.json()
try:
state = measure.state(key_of(req, body)) # refuse BEFORE flipping the switch
except ValueError as exc:
return web.json_response({"why": str(exc)}, status=400)
measure.set_enabled(body.get("enabled") is True)
return web.json_response({**state, "enabled": measure.enabled()})
@table.post(base + "/clear")
async def _clear(req):
body = await req.json()
try:
key = key_of(req, body) # exactly the key named: never a fallback
measure.clear(key)
return web.json_response(measure.state(key, fallback=False))
except ValueError as exc:
return web.json_response({"why": str(exc)}, status=400)
@table.get(base + "/export")
async def _export(req):
try:
path = measure.path_of(key_of(req))
except ValueError as exc:
return web.json_response({"why": str(exc)}, status=400)
if not path.exists():
return web.json_response({"why": f"nothing recorded yet for {key_of(req)!r}"}, status=404)
return web.Response(body=path.read_bytes(), content_type="application/octet-stream",
headers={"Content-Disposition":
f'attachment; filename="{key_of(req)}.block_influence.pt"'})
PROVIDES = {"modifier": install, "routes": routes}