log: a severity, a source, and a message
[Log] Info: FunPack Diffusion Model Loader | flux.safetensors loaded, attention sage
[Log] Alert: MiniMaxH3ImageToVideo node | no "first_image" input is connected
[Log] Warning: FunPack LoRA Loader | style.safetensors is empty and will not load
[Log] Critical: torch | cannot allocate 14 GB (3 GB free); the run will not start
The difference between the middle two is the one that matters. Alert is a
choice you may not have made on purpose. Warning is something that will NOT
do what its name says -- the level v4 needed and did not have, which is how
560 handlers came to swallow a failure and carry on, so a feature could be
switched on, do nothing, and never say so.
Info is not decoration either: "loaded on GPU using sage attention" is how
an attention setting that did nothing gets noticed months before the
pictures look wrong enough to investigate.
Every call site moved -- there is no note() left to be vague through -- and
the loaders now say what they actually loaded rather than only what they
were asked for. Records are kept in a bounded ring as well as printed, so a
long rental session cannot turn the log into a memory leak, and served at
/funpack/api/log so a person can read it somewhere other than a terminal
full of ComfyUI's own output. There is a page for it.
Also dropped the preflight this was traded for: min and max are already
declared and already enforced at both doors, and a second protection layer
around a check that exists is ceremony.
Two mistakes worth keeping: a log line reached into the model's shape to
describe it and could have failed the load it was only narrating; and both
new pages passed `children` to an element that takes `rows`, which the kit
accepted and silently dropped. It throws on an unknown element and not on an
unknown prop.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
def940d9d5
commit
74671329f9
@@ -0,0 +1,6 @@
|
||||
<link rel="stylesheet" href="/funpack/app/composer/composer.css">
|
||||
<link rel="stylesheet" href="/funpack/app/catalogue/catalogue.css">
|
||||
<title>FunPack log</title>
|
||||
<div id="app"></div>
|
||||
<script src="/funpack/app/shell/theme.js"></script>
|
||||
<script type="module" src="/funpack/app/catalogue/log.js"></script>
|
||||
@@ -0,0 +1,70 @@
|
||||
// The log, where a person can read it.
|
||||
//
|
||||
// The terminal is where this ends up by default, buried among ComfyUI's own
|
||||
// output. The whole reason for a severity and a source is that a line can be
|
||||
// found later, so there has to be somewhere to look.
|
||||
|
||||
import { composer } from "../composer/composer.js";
|
||||
|
||||
const root = document.querySelector("#app");
|
||||
|
||||
// Severity -> the chip tone that already exists in the kit. Info is deliberately
|
||||
// quiet: most lines are Info, and a wall of coloured chips says nothing.
|
||||
const TONE = { Info: "neutral", Alert: "info", Warning: "warn", Critical: "danger" };
|
||||
|
||||
let filter = null;
|
||||
|
||||
async function load() {
|
||||
const url = "/funpack/api/log" + (filter ? `?level=${encodeURIComponent(filter)}` : "");
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) throw new Error(`${url} answered ${response.status}`);
|
||||
return response.json();
|
||||
}
|
||||
|
||||
function render(data) {
|
||||
const counts = {};
|
||||
for (const level of data.levels) counts[level] = 0;
|
||||
for (const record of data.records) counts[record.level] = (counts[record.level] || 0) + 1;
|
||||
|
||||
const filters = composer.segmented.md({
|
||||
label: "Show",
|
||||
value: filter || "all",
|
||||
options: [{ value: "all", label: "Everything" },
|
||||
...data.levels.map((l) => ({ value: l, label: `${l} (${counts[l] || 0})` }))],
|
||||
onChange: (value) => { filter = value === "all" ? null : value; start(); },
|
||||
});
|
||||
|
||||
// Newest last, like a terminal: the eye expects the end to be the present.
|
||||
const items = data.records.map((record, i) => ({
|
||||
id: `r${i}`,
|
||||
label: record.message,
|
||||
hint: `${record.level} · ${record.source || "FunPack"}`,
|
||||
}));
|
||||
|
||||
root.replaceChildren(composer.panel.default({
|
||||
title: "Log",
|
||||
body: composer.group.default({
|
||||
rows: [
|
||||
filters,
|
||||
items.length
|
||||
? composer.list.rows({ items, reorder: false })
|
||||
: composer.emptyState.default({
|
||||
icon: "○", title: "Nothing logged yet",
|
||||
hint: filter ? `No ${filter} messages.` : "Run a generation and come back.",
|
||||
}),
|
||||
],
|
||||
}),
|
||||
}).node);
|
||||
}
|
||||
|
||||
async function start() {
|
||||
try {
|
||||
render(await load());
|
||||
} catch (err) {
|
||||
root.replaceChildren(composer.emptyState.default({
|
||||
icon: "▲", title: "Could not read the log", hint: err.message,
|
||||
}).node);
|
||||
}
|
||||
}
|
||||
|
||||
start();
|
||||
@@ -75,10 +75,10 @@ async function start() {
|
||||
function composal(title, hint, items) {
|
||||
return composer.panel.default({
|
||||
title,
|
||||
body: items.length
|
||||
? composer.group.default({ children: [
|
||||
composer.hint.default({ text: hint }), rows(items)] })
|
||||
: composer.hint.default({ text: "None." }),
|
||||
body: composer.group.default({
|
||||
hint,
|
||||
rows: items.length ? [rows(items)] : [composer.hint.default({ text: "None." })],
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+101
-8
@@ -1,17 +1,110 @@
|
||||
"""One-line logging with a fixed prefix.
|
||||
"""Saying what happened, at a severity, from somewhere.
|
||||
|
||||
`failed()` exists so a module that silently no-ops is impossible: every swallowed
|
||||
exception says what stopped working, once.
|
||||
[Log] Info: FunPack Diffusion Model Loader | flux.safetensors loaded, attention sage
|
||||
[Log] Alert: FunPack Sampler | no image is connected, starting from noise
|
||||
[Log] Warning: FunPack LoRA Loader | style.safetensors is empty and will not load
|
||||
[Log] Critical: torch | cannot allocate 14 GB (3 GB free); the run will not start
|
||||
|
||||
Four levels, and the difference between the middle two is the one that matters:
|
||||
|
||||
* **Info** -- it worked, and here is what it actually did. Not decoration: "loaded
|
||||
on GPU using sage attention" is how you find out the attention setting did
|
||||
nothing, months before the pictures look wrong enough to investigate.
|
||||
* **Alert** -- something is unset or unconnected and the run continues without
|
||||
it. A choice you may not have made on purpose.
|
||||
* **Warning** -- something will NOT do what its name says. This is the level v4
|
||||
needed and did not have: 560 handlers swallowed a failure and carried on, so a
|
||||
feature could be switched on, do nothing, and never say so.
|
||||
* **Critical** -- the run cannot proceed.
|
||||
|
||||
Records are kept in a small ring as well as printed, so the app can show them.
|
||||
The buffer is bounded because a long rental session must not turn a log into a
|
||||
memory leak.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import time
|
||||
from collections import deque
|
||||
from typing import Optional
|
||||
|
||||
PREFIX = "[FunPack]"
|
||||
LABEL = "[Log]"
|
||||
|
||||
INFO = "Info"
|
||||
ALERT = "Alert"
|
||||
WARNING = "Warning"
|
||||
CRITICAL = "Critical"
|
||||
|
||||
LEVELS = (INFO, ALERT, WARNING, CRITICAL)
|
||||
|
||||
# Enough to cover a long generation and its aftermath, small enough to be free.
|
||||
HISTORY = 500
|
||||
_records: deque = deque(maxlen=HISTORY)
|
||||
|
||||
# Keys already said once. Cleared per run rather than per process: v4's dedup
|
||||
# lasted the life of the interpreter, so a session reported the first generation
|
||||
# that went inert and stayed quiet for every one after it.
|
||||
_said: set = set()
|
||||
|
||||
|
||||
def note(message: str) -> None:
|
||||
print(f"{PREFIX} {message}", file=sys.stderr, flush=True)
|
||||
def line(level: str, source: str, message: str) -> str:
|
||||
"""The one place the format is decided."""
|
||||
where = f"{source} | " if source else ""
|
||||
return f"{LABEL} {level}: {where}{message}"
|
||||
|
||||
|
||||
def failed(what: str, exc: BaseException) -> None:
|
||||
note(f"{what} failed to load: {type(exc).__name__}: {exc}")
|
||||
def record(level: str, source: str, message: str) -> dict:
|
||||
entry = {"at": time.time(), "level": level, "source": source, "message": message}
|
||||
_records.append(entry)
|
||||
print(line(level, source, message), file=sys.stderr, flush=True)
|
||||
return entry
|
||||
|
||||
|
||||
def info(source: str, message: str) -> dict:
|
||||
return record(INFO, source, message)
|
||||
|
||||
|
||||
def alert(source: str, message: str) -> dict:
|
||||
return record(ALERT, source, message)
|
||||
|
||||
|
||||
def warning(source: str, message: str) -> dict:
|
||||
return record(WARNING, source, message)
|
||||
|
||||
|
||||
def critical(source: str, message: str) -> dict:
|
||||
return record(CRITICAL, source, message)
|
||||
|
||||
|
||||
def once(key: str, level: str, source: str, message: str) -> Optional[dict]:
|
||||
"""Say it the first time only, until the next run.
|
||||
|
||||
For anything that would otherwise repeat every sampling step. Thirty copies
|
||||
of one line say nothing the first did not, and burying the console is its own
|
||||
way of hiding a failure.
|
||||
"""
|
||||
if key in _said:
|
||||
return None
|
||||
_said.add(key)
|
||||
return record(level, source, message)
|
||||
|
||||
|
||||
def failed(what: str, exc: BaseException) -> dict:
|
||||
"""Something did not load. Always a Warning: it means a feature is absent."""
|
||||
return warning(what, f"did not load -- {type(exc).__name__}: {exc}")
|
||||
|
||||
|
||||
def new_run() -> None:
|
||||
"""A generation is starting: whatever was said once may be said again."""
|
||||
_said.clear()
|
||||
|
||||
|
||||
def history(level: Optional[str] = None, limit: int = HISTORY) -> list:
|
||||
"""Recent records, newest last. For the app, and for the modules page."""
|
||||
items = [r for r in _records if level is None or r["level"] == level]
|
||||
return items[-limit:]
|
||||
|
||||
|
||||
def _reset() -> None:
|
||||
"""Tests only."""
|
||||
_records.clear()
|
||||
_said.clear()
|
||||
|
||||
+8
-8
@@ -56,14 +56,14 @@ def collect(registry=None) -> Tuple[List[type], List[Tuple[str, str]]]:
|
||||
|
||||
if schema is None:
|
||||
rejected.append((where, "is not a comfy_api io.ComfyNode (no GET_SCHEMA)"))
|
||||
log.note(f"{where} is not a ComfyUI V3 node; not registered.")
|
||||
log.warning(where, "is not a ComfyUI V3 node and was not registered")
|
||||
continue
|
||||
|
||||
node_id = getattr(schema, "node_id", None)
|
||||
if not isinstance(node_id, str) or not node_id.startswith(PREFIX):
|
||||
rejected.append((where, f"node_id {node_id!r} does not start with {PREFIX!r}"))
|
||||
log.note(f"{where} declares node_id {node_id!r}, which is not "
|
||||
f"prefixed {PREFIX!r}; not registered.")
|
||||
log.warning(where, f"declares node_id {node_id!r}, which is not prefixed "
|
||||
f"{PREFIX!r}, and was not registered")
|
||||
continue
|
||||
|
||||
if node_id in claimed:
|
||||
@@ -71,8 +71,8 @@ def collect(registry=None) -> Tuple[List[type], List[Tuple[str, str]]]:
|
||||
# and which one wins would depend on import order.
|
||||
rejected.append((where, f"duplicate node_id {node_id!r}, already "
|
||||
f"declared by {claimed[node_id]}"))
|
||||
log.note(f"{where} reuses node_id {node_id!r}, already declared by "
|
||||
f"{claimed[node_id]}; not registered.")
|
||||
log.warning(where, f"reuses node_id {node_id!r}, already declared by "
|
||||
f"{claimed[node_id]}, and was not registered")
|
||||
continue
|
||||
|
||||
claimed[node_id] = where
|
||||
@@ -96,7 +96,7 @@ def extension():
|
||||
return list(nodes)
|
||||
|
||||
if rejected:
|
||||
log.note(f"{len(rejected)} node(s) were not registered; "
|
||||
+ "; ".join(f"{where}: {why}" for where, why in rejected))
|
||||
log.note(f"registered {len(nodes)} node(s)")
|
||||
log.warning("nodes", f"{len(rejected)} node(s) were not registered; "
|
||||
+ "; ".join(f"{where}: {why}" for where, why in rejected))
|
||||
log.info("nodes", f"registered {len(nodes)} node(s)")
|
||||
return FunPackExtension()
|
||||
|
||||
+4
-3
@@ -136,9 +136,10 @@ def guard(fn, key: str, neutral, dropped: Dropped):
|
||||
except Exception as exc: # noqa: BLE001 -- the whole point
|
||||
if dropped.record(key, exc):
|
||||
from . import log
|
||||
log.note(
|
||||
f"{key} failed during sampling and is now OFF for the rest of "
|
||||
f"this run. The run continues without it.\n"
|
||||
log.warning(
|
||||
key,
|
||||
"failed during sampling and is now OFF for the rest of this run; "
|
||||
"the run continues without it\n"
|
||||
+ "".join(traceback.format_exception(exc)).rstrip())
|
||||
return neutral(*args, **kwargs)
|
||||
|
||||
|
||||
+1
-1
@@ -122,7 +122,7 @@ def scan(root: Optional[Path] = None, package: Optional[str] = None) -> Registry
|
||||
try:
|
||||
registry.add(validate(raw, source=dotted))
|
||||
except SchemaError as exc:
|
||||
log.note(f"{dotted} declares an invalid module: {exc}")
|
||||
log.warning(dotted, f"is not a valid module and was not loaded -- {exc}")
|
||||
registry.failed.append((dotted, str(exc)))
|
||||
|
||||
return registry
|
||||
|
||||
+2
-1
@@ -39,7 +39,8 @@ def order(specs: Iterable[ModuleSpec]) -> Tuple[List[ModuleSpec], List[Tuple[Mod
|
||||
# that would put a whole stage out of sequence to satisfy one module.
|
||||
# The relation is dropped and said out loud rather than silently obeyed.
|
||||
if stage_of[later] < stage_of[earlier]:
|
||||
log.note(
|
||||
log.alert(
|
||||
"module ordering",
|
||||
f"ignoring {later!r} after {earlier!r}: {later!r} is in stage "
|
||||
f"{STAGES[stage_of[later]]!r}, which runs before {STAGES[stage_of[earlier]]!r}"
|
||||
)
|
||||
|
||||
+11
-1
@@ -92,6 +92,16 @@ def register(routes, prefix=None):
|
||||
traits = [t for t in raw.split(",") if t] if raw is not None else None
|
||||
return web.json_response(manifest(traits))
|
||||
|
||||
@routes.get(P + "/api/log")
|
||||
async def _log(req):
|
||||
level = req.query.get("level") or None
|
||||
try:
|
||||
limit = int(req.query.get("limit", log.HISTORY))
|
||||
except ValueError:
|
||||
limit = log.HISTORY
|
||||
return web.json_response({"levels": list(log.LEVELS),
|
||||
"records": log.history(level, limit)})
|
||||
|
||||
@routes.get(P + "/app/{tail:.*}")
|
||||
async def _app_asset(req):
|
||||
return _serve_under(req, config.APP_DIR, config.APP_EXTS)
|
||||
@@ -121,4 +131,4 @@ if web is not None and PromptServer is not None:
|
||||
except Exception as exc: # noqa: BLE001
|
||||
log.failed("route registration", exc)
|
||||
else:
|
||||
log.note(f"serving the app at {config.UI_PREFIX}/")
|
||||
log.info("routes", f"serving the app at {config.UI_PREFIX}/")
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
"""The log: a severity, a source, and a message.
|
||||
|
||||
The format is the contract -- the app will parse it and a person will scan it --
|
||||
so it is pinned rather than left to drift line by line.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from core import log
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def clean():
|
||||
log._reset()
|
||||
yield
|
||||
log._reset()
|
||||
|
||||
|
||||
def test_the_shape_is_label_severity_source_message():
|
||||
assert log.line("Warning", "FunPack LoRA Loader", "style.safetensors is empty") == (
|
||||
"[Log] Warning: FunPack LoRA Loader | style.safetensors is empty")
|
||||
|
||||
|
||||
def test_a_message_with_no_source_does_not_print_an_empty_one():
|
||||
assert log.line("Info", "", "ready") == "[Log] Info: ready"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("fn,level", [
|
||||
(log.info, "Info"), (log.alert, "Alert"),
|
||||
(log.warning, "Warning"), (log.critical, "Critical"),
|
||||
])
|
||||
def test_each_level_records_itself(fn, level):
|
||||
fn("somewhere", "something")
|
||||
assert log.history()[-1] == {
|
||||
"at": log.history()[-1]["at"], "level": level,
|
||||
"source": "somewhere", "message": "something"}
|
||||
|
||||
|
||||
def test_it_prints_as_well_as_records(capsys):
|
||||
log.warning("loader", "a file went missing")
|
||||
assert "[Log] Warning: loader | a file went missing" in capsys.readouterr().err
|
||||
|
||||
|
||||
def test_history_can_be_filtered_by_level():
|
||||
log.info("a", "one")
|
||||
log.warning("b", "two")
|
||||
log.info("c", "three")
|
||||
assert [r["source"] for r in log.history(log.INFO)] == ["a", "c"]
|
||||
|
||||
|
||||
def test_the_history_is_bounded():
|
||||
"""A long rental session must not turn the log into a memory leak."""
|
||||
for i in range(log.HISTORY + 50):
|
||||
log.info("loop", str(i))
|
||||
assert len(log.history()) == log.HISTORY
|
||||
assert log.history()[-1]["message"] == str(log.HISTORY + 49)
|
||||
|
||||
|
||||
def test_failed_is_a_warning_because_it_means_a_feature_is_absent():
|
||||
log.failed("some.module", ValueError("bad"))
|
||||
entry = log.history()[-1]
|
||||
assert entry["level"] == log.WARNING
|
||||
assert "ValueError" in entry["message"] and "bad" in entry["message"]
|
||||
|
||||
|
||||
# --- once, and what "once" means -------------------------------------------
|
||||
|
||||
def test_once_says_it_a_single_time():
|
||||
for _ in range(10):
|
||||
log.once("k", log.ALERT, "src", "inert")
|
||||
assert len(log.history()) == 1
|
||||
|
||||
|
||||
def test_once_is_per_run_not_per_process():
|
||||
"""v4 deduped for the life of the interpreter, so a session reported the
|
||||
first generation that went inert and stayed quiet for every one after."""
|
||||
log.once("k", log.ALERT, "src", "inert")
|
||||
log.new_run()
|
||||
log.once("k", log.ALERT, "src", "inert")
|
||||
assert len(log.history()) == 2
|
||||
|
||||
|
||||
def test_different_keys_are_independent():
|
||||
log.once("a", log.ALERT, "src", "one")
|
||||
log.once("b", log.ALERT, "src", "two")
|
||||
assert len(log.history()) == 2
|
||||
@@ -28,6 +28,8 @@ import folder_paths
|
||||
import torch
|
||||
from comfy_api.latest import io
|
||||
|
||||
from ..._core import log
|
||||
|
||||
MAX_ENCODERS = 4
|
||||
|
||||
|
||||
@@ -118,5 +120,8 @@ class FunPackCLIPLoader(io.ComfyNode):
|
||||
raise RuntimeError(
|
||||
f"Could not load a text encoder from {', '.join(names)} as type {type!r}.")
|
||||
|
||||
log.info("FunPack CLIP Loader",
|
||||
f"{len(names)} encoder file(s) loaded as type {type}, device {device}: "
|
||||
+ ", ".join(names))
|
||||
listing = "\n".join(f" {i + 1}. {name}" for i, name in enumerate(names))
|
||||
return io.NodeOutput(clip, f"FunPack CLIP Loader | type={type} device={device}\n{listing}")
|
||||
|
||||
@@ -10,10 +10,10 @@ which the loader cannot be handed by anyone else. Feature knobs -- strengths,
|
||||
sigma windows, anything a modifier owns -- are settings, and never appear here.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
import torch
|
||||
|
||||
from .._core import log
|
||||
|
||||
# Named exactly as core's UNETLoader names them where they overlap, so a value
|
||||
# copied from a stock workflow means the same thing here.
|
||||
WEIGHT_DTYPES = ["default", "fp8_e4m3fn", "fp8_e4m3fn_fast", "fp8_e5m2", "fp16", "bf16", "fp32"]
|
||||
@@ -80,11 +80,13 @@ def attention_override(name):
|
||||
try:
|
||||
from comfy.ldm.modules.attention import get_attention_function
|
||||
except ImportError:
|
||||
logging.warning("[FunPack] this ComfyUI has no attention registry; leaving attention alone")
|
||||
log.warning("attention", "this ComfyUI has no attention registry, so the "
|
||||
"chosen backend cannot be applied")
|
||||
return None
|
||||
chosen = get_attention_function(name, None)
|
||||
if chosen is None:
|
||||
logging.warning("[FunPack] attention backend %r is not available; leaving attention alone", name)
|
||||
log.warning("attention", f"backend {name!r} is not available on this machine, "
|
||||
f"so it was not applied")
|
||||
return None
|
||||
inner = getattr(chosen, "__wrapped__", chosen)
|
||||
|
||||
@@ -124,7 +126,7 @@ def set_fp16_accumulation(enabled):
|
||||
before = bool(getattr(matmul, "allow_fp16_accumulation", False))
|
||||
matmul.allow_fp16_accumulation = wanted
|
||||
if before != wanted:
|
||||
logging.warning(
|
||||
"[FunPack] fp16 accumulation is now %s for EVERY model in this process, "
|
||||
"not just this one -- torch has a single process-wide flag.", wanted)
|
||||
log.alert("fp16 accumulation",
|
||||
f"now {wanted} for EVERY model in this process, not just this one -- "
|
||||
f"torch has a single process-wide flag")
|
||||
return wanted
|
||||
|
||||
@@ -10,6 +10,7 @@ import comfy.utils
|
||||
import folder_paths
|
||||
from comfy_api.latest import io
|
||||
|
||||
from ..._core import log
|
||||
from ..common import (COMPUTE_DTYPES, WEIGHT_DTYPES, attention_choices,
|
||||
attention_override, dtype_of, set_fp16_accumulation,
|
||||
weight_model_options)
|
||||
@@ -90,4 +91,11 @@ class FunPackDiffusionModelLoader(io.ComfyNode):
|
||||
else:
|
||||
notes.append("attention: default (as launched)")
|
||||
|
||||
# Never reach into the model's shape to describe it: a log line that
|
||||
# assumes structure can fail the load it was only meant to narrate.
|
||||
kind = type(getattr(model, "model", model)).__name__
|
||||
log.info("FunPack Diffusion Model Loader",
|
||||
f"{model_name} loaded as {kind}, weights "
|
||||
f"{weight_dtype}, compute {compute_dtype}, attention "
|
||||
f"{attention if override is not None else 'default (as launched)'}")
|
||||
return io.NodeOutput(model, "\n".join(notes))
|
||||
|
||||
@@ -12,6 +12,8 @@ import comfy.utils
|
||||
import folder_paths
|
||||
from comfy_api.latest import io
|
||||
|
||||
from ..._core import log
|
||||
|
||||
|
||||
class FunPackLoraLoader(io.ComfyNode):
|
||||
@classmethod
|
||||
@@ -45,6 +47,10 @@ class FunPackLoraLoader(io.ComfyNode):
|
||||
# rather than clones: a clone here would silently drop patches a later
|
||||
# loader in the chain applied to the original.
|
||||
if strength_model == 0 and (clip is None or strength_clip == 0):
|
||||
# Loud, because "applied a LoRA at strength 0" is indistinguishable
|
||||
# from "forgot to set the strength" by looking at the picture.
|
||||
log.alert("FunPack LoRA Loader",
|
||||
f"{lora_name} has both strengths at 0 and was not applied")
|
||||
return io.NodeOutput(model, clip, f"{lora_name}: both strengths are 0, not applied")
|
||||
|
||||
path = folder_paths.get_full_path_or_raise("loras", lora_name)
|
||||
@@ -54,5 +60,6 @@ class FunPackLoraLoader(io.ComfyNode):
|
||||
|
||||
applied = [f"model={strength_model}"]
|
||||
applied.append(f"clip={strength_clip}" if clip is not None else "clip=not wired")
|
||||
log.info("FunPack LoRA Loader", f"{lora_name} applied ({', '.join(applied)})")
|
||||
return io.NodeOutput(patched_model, patched_clip,
|
||||
f"FunPack LoRA Loader | {lora_name} ({', '.join(applied)})")
|
||||
|
||||
@@ -164,12 +164,12 @@ def test_an_unknown_encoder_family_is_refused_not_silently_downgraded(monkeypatc
|
||||
nodes.FunPackCLIPLoader.execute(clip_name1="e.safetensors", type="not_a_family")
|
||||
|
||||
|
||||
def test_changing_the_process_wide_fp16_flag_is_never_silent(monkeypatch, caplog):
|
||||
def test_changing_the_process_wide_fp16_flag_is_never_silent(monkeypatch):
|
||||
"""torch has ONE flag for the interpreter, so this cannot mean "for this
|
||||
model". A second loader setting it differently changes the first model's
|
||||
maths, and ComfyUI's loader caching means the last value outlives its run."""
|
||||
import logging
|
||||
import torch
|
||||
from core import log
|
||||
from modules.loaders import common
|
||||
|
||||
class FakeMatmul:
|
||||
@@ -178,23 +178,21 @@ def test_changing_the_process_wide_fp16_flag_is_never_silent(monkeypatch, caplog
|
||||
fake = FakeMatmul()
|
||||
monkeypatch.setattr(torch.backends, "cuda", type("C", (), {"matmul": fake}))
|
||||
|
||||
with caplog.at_level(logging.WARNING):
|
||||
assert common.set_fp16_accumulation(True) is True
|
||||
log._reset()
|
||||
assert common.set_fp16_accumulation(True) is True
|
||||
assert fake.allow_fp16_accumulation is True
|
||||
assert any("EVERY model in this process" in r.getMessage() for r in caplog.records)
|
||||
assert any("EVERY model in this process" in r["message"] for r in log.history())
|
||||
|
||||
# No transition, nothing said.
|
||||
caplog.clear()
|
||||
with caplog.at_level(logging.WARNING):
|
||||
common.set_fp16_accumulation(True)
|
||||
assert not caplog.records
|
||||
log._reset()
|
||||
common.set_fp16_accumulation(True)
|
||||
assert log.history() == []
|
||||
|
||||
# And turning it back off is a transition too: it is another model's setting
|
||||
# being undone.
|
||||
caplog.clear()
|
||||
with caplog.at_level(logging.WARNING):
|
||||
common.set_fp16_accumulation(False)
|
||||
assert caplog.records
|
||||
log._reset()
|
||||
common.set_fp16_accumulation(False)
|
||||
assert log.history()
|
||||
|
||||
|
||||
def test_a_torch_build_without_the_flag_reports_that_rather_than_pretending():
|
||||
|
||||
@@ -12,6 +12,8 @@ import folder_paths
|
||||
import torch
|
||||
from comfy_api.latest import io
|
||||
|
||||
from ..._core import log
|
||||
|
||||
# Chosen, not inferred. bf16 is the default because fp16 is a known failure on
|
||||
# the AV VAEs, and fp32 decodes slowly for no visible gain.
|
||||
DTYPES = {
|
||||
@@ -53,4 +55,5 @@ class FunPackVAELoader(io.ComfyNode):
|
||||
state_dict = comfy.utils.load_torch_file(path)
|
||||
vae = comfy.sd.VAE(sd=state_dict, dtype=DTYPES[dtype])
|
||||
vae.throw_exception_if_invalid()
|
||||
log.info("FunPack VAE Loader", f"{vae_name} loaded as {dtype}")
|
||||
return io.NodeOutput(vae, f"{vae_name} loaded as {dtype}")
|
||||
|
||||
@@ -61,14 +61,11 @@ SETTINGS = {
|
||||
},
|
||||
}
|
||||
|
||||
_said = set()
|
||||
|
||||
|
||||
def _once(message: str) -> None:
|
||||
"""Say an inert reason once per process, not once per step."""
|
||||
if message not in _said:
|
||||
_said.add(message)
|
||||
log.note(message)
|
||||
"""Say an inert reason once per RUN, not once per step and not once per
|
||||
process -- a session that deduped for the life of the interpreter reported
|
||||
the first generation that went inert and stayed quiet for every one after."""
|
||||
log.once(f"alg:{message}", log.ALERT, "FunPack ALG", message)
|
||||
|
||||
|
||||
def install(patcher, values, key):
|
||||
@@ -87,14 +84,14 @@ def install(patcher, values, key):
|
||||
# No anchor pinned means nothing to de-static; a sampler that is not
|
||||
# KSAMPLER-shaped has no loop to stand between.
|
||||
if inner_fn is None or latent_image is None or denoise_mask is None:
|
||||
_once("[FunPack] ALG is off this run: no pinned anchor to loosen.")
|
||||
_once("off this run: there is no pinned anchor to loosen")
|
||||
return executor(model_wrap, sigmas, extra_args, callback, noise,
|
||||
latent_image, denoise_mask, disable_pbar)
|
||||
|
||||
blurred = blur_frames(latent_image, strength, frame_indices=(0,))
|
||||
if blurred is None:
|
||||
_once("[FunPack] ALG is off this run: this model's anchor is not a "
|
||||
"plain video latent, and reshaping it would be guesswork.")
|
||||
_once("off this run: this model's anchor is not a plain video latent, "
|
||||
"and reshaping it would be guesswork")
|
||||
return executor(model_wrap, sigmas, extra_args, callback, noise,
|
||||
latent_image, denoise_mask, disable_pbar)
|
||||
|
||||
|
||||
@@ -149,8 +149,14 @@ class FunPackLoadModifiers(io.ComfyNode):
|
||||
stripped = patching.strip(patched, KEY_PREFIX)
|
||||
applied, notes = [], []
|
||||
|
||||
# One per run, so a long session reports every generation that went
|
||||
# inert rather than only the first one after a restart.
|
||||
# A run starts here: whatever was said once may be said again, and the
|
||||
# dropped-modifier record starts empty. Both are per-run state and they
|
||||
# begin together so they cannot disagree about when a run began.
|
||||
#
|
||||
# The limit, stated: a graph without this node never marks a run, so
|
||||
# once-per-run reverts to once-per-process there. That is the honest
|
||||
# cost of ComfyUI having no notion of "a run started" a node can hook.
|
||||
log.new_run()
|
||||
dropped = patching.Dropped()
|
||||
patched.funpack_dropped = dropped
|
||||
|
||||
|
||||
@@ -71,7 +71,7 @@ def install(patcher, values, key):
|
||||
if amount == 0.0:
|
||||
# Zero is a request for nothing, and saying so beats reporting that a
|
||||
# modifier ran when it could not have changed a single pixel.
|
||||
log.note("[FunPack] detail is enabled at amount 0, which changes nothing")
|
||||
log.alert("FunPack Detail", "enabled at amount 0, which changes nothing")
|
||||
return None
|
||||
|
||||
radius = int(values.get("radius", 5))
|
||||
|
||||
+7
-1
@@ -78,7 +78,13 @@ class Handler(BaseHTTPRequestHandler):
|
||||
|
||||
# The same manifest the aiohttp route serves, so what you see in the
|
||||
# browser here is what ComfyUI would send.
|
||||
if path == P + "/api/modules":
|
||||
if path == P + "/api/log":
|
||||
query = parse_qs(parsed.query)
|
||||
level = (query.get("level") or [None])[0]
|
||||
body = json.dumps({"levels": list(routes.log.LEVELS),
|
||||
"records": routes.log.history(level)}).encode()
|
||||
served = static.Served(200, body, "application/json")
|
||||
elif path == P + "/api/modules":
|
||||
raw = parse_qs(parsed.query).get("traits")
|
||||
traits = [t for t in raw[0].split(",") if t] if raw else None
|
||||
body = json.dumps(routes.manifest(traits)).encode()
|
||||
|
||||
Reference in New Issue
Block a user