fix: block sweep was running on every Settings panel open, stalling the server
block_sweep() is a 9-block permutation test (2000 trials each by default) -- heavy enough on CPU that running it synchronously inside the status route (which fires on every panel mount) blocked the same event loop ComfyUI's own server runs on. Confirmed live: opening Settings > Refinement & Taste froze an in-progress generation. Split the same way trajectory_probe already is: status stays cheap (no sweep), the sweep moves to its own POST route, run off the event loop via asyncio.to_thread, triggered only by an explicit "Read" button instead of automatically on mount.
This commit is contained in:
@@ -200,6 +200,7 @@
|
||||
|
||||
// H3 representation steering (Settings ▸ Learning) — per-KEY, unlike the probe above.
|
||||
reinsStatus: (key) => j("GET", API("/h3_repr_steering") + `?key=${encodeURIComponent(key || "default")}`),
|
||||
reinsSweep: (key, trials) => j("POST", API("/h3_repr_steering/sweep"), { key: key || "default", trials: trials || 2000 }),
|
||||
reinsClear: (key) => j("POST", API("/h3_repr_steering/clear"), { key: key || "default" }),
|
||||
async reinsExport(key) {
|
||||
const k = key || "default";
|
||||
|
||||
@@ -393,7 +393,13 @@
|
||||
// ── H3 representation steering ──────────────────────────────────────────
|
||||
// Per-KEY (unlike the probe above, which pools across every key): one sidecar per
|
||||
// refinement key, so status/export/clear all act on whichever key this project is using.
|
||||
let reinsState = null, reinsError = "";
|
||||
//
|
||||
// The block sweep is a 9-block permutation test -- heavy enough on CPU that running it on
|
||||
// every panel mount (status was doing this) stalled the event loop ComfyUI's own server
|
||||
// shares with this one, confirmed live as a generation freezing while the panel was open.
|
||||
// Split the same way the probe already is: cheap status auto-fetched, the actual sweep
|
||||
// only on explicit request (the "Read" button below).
|
||||
let reinsState = null, reinsSweep = null, reinsSweepBusy = false, reinsError = "";
|
||||
|
||||
function reinsSweepTable(sweep) {
|
||||
const entries = Object.entries(sweep || {}).sort((a, b) => a[1].p_value - b[1].p_value);
|
||||
@@ -439,17 +445,27 @@
|
||||
if (!window.confirm(`Throw away all REINS data for '${key}'?\n\nThis cannot be `
|
||||
+ "undone. Download it first if you want to keep it.")) return;
|
||||
reinsError = "";
|
||||
try { reinsState = await window.MovieEditorAPI.reinsClear(key); }
|
||||
try { reinsState = await window.MovieEditorAPI.reinsClear(key); reinsSweep = null; }
|
||||
catch (e) { reinsError = String(e.message || e); }
|
||||
paint();
|
||||
}, { disabled: !nLiked && !nDisliked, danger: true }));
|
||||
rows.append(actionRow("Read the block sweep",
|
||||
"Runs a permutation test at every candidate block to see which one actually "
|
||||
+ "separates liked from disliked. Heavy enough on CPU to notice — not run "
|
||||
+ "automatically, only when you ask for it.",
|
||||
reinsSweepBusy ? "Reading…" : "Read", async () => {
|
||||
reinsSweepBusy = true; reinsError = ""; paint();
|
||||
try { reinsSweep = await window.MovieEditorAPI.reinsSweep(key, 2000); }
|
||||
catch (e) { reinsError = String(e.message || e); }
|
||||
reinsSweepBusy = false; paint();
|
||||
}, { disabled: reinsSweepBusy || (!nLiked && !nDisliked) }));
|
||||
box.append(rows);
|
||||
if (reinsError) box.append(el("div", "sw-hint", reinsError));
|
||||
const sweepTbl = reinsState && reinsSweepTable(reinsState.sweep);
|
||||
const sweepTbl = reinsSweep && reinsSweepTable(reinsSweep.sweep);
|
||||
if (sweepTbl) {
|
||||
box.append(el("div", "sw-rows-label",
|
||||
`Block sweep — which of H3's blocks separates liked from disliked (block `
|
||||
+ `${reinsState.default_block} is the one that actually steers)`));
|
||||
+ `${reinsState ? reinsState.default_block : "?"} is the one that actually steers)`));
|
||||
box.append(sweepTbl);
|
||||
}
|
||||
};
|
||||
|
||||
+26
-2
@@ -2099,9 +2099,14 @@ if web is not None and PromptServer is not None:
|
||||
return rs
|
||||
|
||||
def _repr_steer_state(key):
|
||||
# Deliberately NOT block_sweep() -- that's a 9-block x trials permutation test, heavy
|
||||
# enough on CPU to stall the event loop this route shares with ComfyUI's own server
|
||||
# (confirmed live: opening the panel froze a generation in progress). Same split
|
||||
# trajectory_probe already uses -- cheap status here, the actual analysis behind its
|
||||
# own explicitly-triggered route below, so a status fetch (which fires on every panel
|
||||
# mount, not just on request) can never be the expensive one.
|
||||
rs = _repr_steer_module()
|
||||
_direction, n_liked, n_disliked = rs.direction(key)
|
||||
sweep = rs.block_sweep(key)
|
||||
return rs, {
|
||||
"key": key,
|
||||
"n_liked": n_liked,
|
||||
@@ -2109,7 +2114,6 @@ if web is not None and PromptServer is not None:
|
||||
"ready": _direction is not None,
|
||||
"min_per_group": rs.MIN_PER_GROUP,
|
||||
"default_block": rs.DEFAULT_BLOCK,
|
||||
"sweep": {str(b): r for b, r in sweep.items()},
|
||||
}
|
||||
|
||||
@routes.get(UI_PREFIX + "/api/h3_repr_steering")
|
||||
@@ -2122,6 +2126,26 @@ if web is not None and PromptServer is not None:
|
||||
return web.json_response({"key": key, "n_liked": 0, "n_disliked": 0,
|
||||
"ready": False, "error": str(e)})
|
||||
|
||||
@routes.post(UI_PREFIX + "/api/h3_repr_steering/sweep")
|
||||
async def _repr_steer_sweep(req):
|
||||
try:
|
||||
body = await req.json()
|
||||
except Exception:
|
||||
body = {}
|
||||
key = str(body.get("key") or "default").strip() or "default"
|
||||
trials = max(100, min(20000, int(body.get("trials") or 2000)))
|
||||
rs = _repr_steer_module()
|
||||
try:
|
||||
# Off the event loop -- this is the actual CPU-heavy permutation test, and even
|
||||
# triggered explicitly (not on every panel mount, unlike before) it should not be
|
||||
# able to stall other requests, including a generation's own progress messages,
|
||||
# while it runs.
|
||||
import asyncio
|
||||
sweep = await asyncio.to_thread(rs.block_sweep, key, trials)
|
||||
return web.json_response({"key": key, "sweep": {str(b): r for b, r in sweep.items()}})
|
||||
except Exception as e:
|
||||
return web.json_response({"key": key, "sweep": {}, "error": str(e)}, status=500)
|
||||
|
||||
# Export streams the sidecar FILE directly rather than reconstructing it from rows: this
|
||||
# module's state already IS the export format (rows keyed by block + weight + prompt_hash),
|
||||
# unlike the probe's aggregate-across-keys log.
|
||||
|
||||
Reference in New Issue
Block a user