feat: the model loader shows each SLA field only in the mode it belongs to (sparsity for sla/vsa, tau for sol-attn, block size and stabilize for Triton, tail for comfy_kitchen)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
8eaf5be5f4
commit
fc92e5869b
@@ -53,7 +53,7 @@ export const models = (app) => function mount() {
|
||||
if (JSON.stringify((live.inputs || {})[name]) === JSON.stringify(value)) return; // a blur that changed nothing saves nothing
|
||||
await ps.save({ inputs: { [slot.id]: { [name]: value } } });
|
||||
const spec = specs[slot.node];
|
||||
if (spec && (spec.widgets || []).some((w) => w.shows && w.shows.input === name)) { // a choice that brings fields: what they show is what runs
|
||||
if (spec && (spec.widgets || []).some((w) => (w.shows || []).some((c) => c.input === name))) { // a choice that brings fields: what they show is what runs
|
||||
const now = slotsNow().find((x) => x.id === slot.id);
|
||||
const fill = now && shownValues(now, spec);
|
||||
if (fill && Object.keys(fill).length) await ps.save({ inputs: { [slot.id]: fill } });
|
||||
|
||||
@@ -19,18 +19,18 @@ export function sourcesFor(slot, type, slots, specs) {
|
||||
}
|
||||
|
||||
// The dropdowns' own values as the form would fill them, so a new node's revealed fields follow its default choice.
|
||||
const shownParents = (slot, spec) => Object.fromEntries(((spec && spec.widgets) || []).filter((w) => w.type === "COMBO" && !(slot.inputs && slot.inputs[w.name] !== undefined))
|
||||
.map((w) => [w.name, w.default !== undefined ? w.default : (w.choices || [])[0]]));
|
||||
const shownParents = (slot, spec) => Object.fromEntries(((spec && spec.widgets) || []).filter((w) => !(slot.inputs && slot.inputs[w.name] !== undefined))
|
||||
.map((w) => [w.name, w.default !== undefined ? w.default : w.type === "BOOLEAN" ? false : (w.choices || [])[0]]));
|
||||
|
||||
/** A node that is new, or whose input was just unwired, has nothing SET on it while its form shows defaults:
|
||||
* these are the values to send so that what runs is what is seen. */
|
||||
/** Whether a field a dynamic dropdown's choice brings is showing: its dropdown (as set, else its default) is on one of its choices. */
|
||||
export const showing = (w, slot, spec) => {
|
||||
if (!w.shows) return true;
|
||||
const parent = ((spec && spec.widgets) || []).find((x) => x.name === w.shows.input);
|
||||
const picked = slot.inputs && slot.inputs[w.shows.input] !== undefined ? slot.inputs[w.shows.input] : parent && (parent.default !== undefined ? parent.default : (parent.choices || [])[0]);
|
||||
return w.shows.when.includes(picked);
|
||||
};
|
||||
/** Whether a field is drawn: every input its `shows` names ([{input, when}]: a dynamic dropdown's choice, or a
|
||||
* node's own rule) holds one of the listed values -- as set, else as the form shows it by default. */
|
||||
export const showing = (w, slot, spec) => (w.shows || []).every(({ input, when }) => {
|
||||
const parent = ((spec && spec.widgets) || []).find((x) => x.name === input);
|
||||
const picked = slot.inputs && slot.inputs[input] !== undefined ? slot.inputs[input] : parent && (parent.default !== undefined ? parent.default : parent.type === "BOOLEAN" ? false : (parent.choices || [])[0]);
|
||||
return when.includes(picked);
|
||||
});
|
||||
|
||||
export function shownValues(slot, spec, onlyInput) {
|
||||
const out = {};
|
||||
|
||||
@@ -21,3 +21,18 @@ test("what the form shows is sent for a slot with nothing set, and nothing alrea
|
||||
assert.deepEqual(shownValues({ id: "c", inputs: { steps: 4 } }, specs.C), { mode: "x", on: false });
|
||||
assert.deepEqual(shownValues({ id: "c", inputs: {} }, specs.C, "steps"), { steps: 8 });
|
||||
});
|
||||
|
||||
test("a field shows only in the mode it belongs to: every rule it names must hold, defaults count", async () => {
|
||||
const { showing, shownValues } = await import("./wiring.js");
|
||||
const spec = { widgets: [
|
||||
{ name: "sla", type: "BOOLEAN", default: false },
|
||||
{ name: "sla_method", type: "COMBO", choices: ["sla", "sol-attn", "vsa"], default: "sla", shows: [{ input: "sla", when: [true] }] },
|
||||
{ name: "sla_sparsity", type: "FLOAT", default: 0.9, shows: [{ input: "sla", when: [true] }, { input: "sla_method", when: ["sla", "vsa"] }] },
|
||||
{ name: "sla_tau", type: "FLOAT", default: 1.3, shows: [{ input: "sla", when: [true] }, { input: "sla_method", when: ["sol-attn"] }] },
|
||||
] };
|
||||
const drawn = (inputs) => spec.widgets.filter((w) => showing(w, { inputs }, spec)).map((w) => w.name);
|
||||
assert.deepEqual(drawn({}), ["sla"]);
|
||||
assert.deepEqual(drawn({ sla: true }), ["sla", "sla_method", "sla_sparsity"]);
|
||||
assert.deepEqual(drawn({ sla: true, sla_method: "sol-attn" }), ["sla", "sla_method", "sla_tau"]);
|
||||
assert.deepEqual(shownValues({ inputs: { sla: true, sla_method: "sol-attn" } }, spec), { sla_tau: 1.3 });
|
||||
});
|
||||
|
||||
+4
-1
@@ -53,6 +53,9 @@ def _one(name: str, declared: Any, required: bool) -> Dict[str, Any]:
|
||||
for key in KEPT:
|
||||
if key in options:
|
||||
widget[key] = options[key]
|
||||
# [{input, when}]: the form draws this only while every named input holds one of its values.
|
||||
if isinstance(options.get("funpack_shows"), list):
|
||||
widget["shows"] = options["funpack_shows"]
|
||||
return widget
|
||||
|
||||
|
||||
@@ -96,7 +99,7 @@ def describe(class_type: str) -> Optional[dict]:
|
||||
kind, options = comfy_types.declared(declared)
|
||||
if comfy_types.is_combo(kind) and comfy_types.reveals(options):
|
||||
for dotted, decl, req, parent, keys in comfy_types.revealed(name, options):
|
||||
sub = dict(_one(dotted, decl, req), shows={"input": parent, "when": keys})
|
||||
sub = dict(_one(dotted, decl, req), shows=[{"input": parent, "when": keys}])
|
||||
(widgets if sub["type"] in PRIMITIVE and not sub.get("forceInput") else sockets).append(sub)
|
||||
|
||||
outputs = list(getattr(node, "RETURN_TYPES", ()) or ())
|
||||
|
||||
@@ -31,6 +31,12 @@ def _model_file_choices():
|
||||
gguf_support.gguf_names("diffusion_models")
|
||||
|
||||
|
||||
|
||||
def shows(*conditions):
|
||||
"""Draw an input only while each (other input, values) holds: SLA's fields belong to one method
|
||||
or engine each, and a field that does nothing in the mode picked is noise. Read by core/widgets."""
|
||||
return {"funpack_shows": [{"input": name, "when": list(values)} for name, values in conditions]}
|
||||
|
||||
class FunPackDiffusionModelLoader(io.ComfyNode):
|
||||
@classmethod
|
||||
def define_schema(cls) -> io.Schema:
|
||||
@@ -64,14 +70,14 @@ class FunPackDiffusionModelLoader(io.ComfyNode):
|
||||
"that is not MiniMax H3 or on a machine without Triton."),
|
||||
# SLA settings. Every one is validated at its default (see sla_attention);
|
||||
# they are here because a knob whose value was measured is still a knob.
|
||||
io.Float.Input("sla_sparsity", default=sla_attention.SLA_DEFAULTS["sparsity_ratio"],
|
||||
io.Float.Input("sla_sparsity", extra_dict=shows(("sla", [True]), ("sla_method", ["sla", "vsa"])), default=sla_attention.SLA_DEFAULTS["sparsity_ratio"],
|
||||
min=0.0, max=0.95, step=0.05, optional=True,
|
||||
tooltip="Fraction of key blocks skipped, when attention is "
|
||||
"sla_h3. 0.90 is validated; 0.85 is lightx2v's own value "
|
||||
"and ~15% slower. Break-even is around 0.60 -- below that "
|
||||
"the kernel is SLOWER than dense, so a low value is a "
|
||||
"loss, not a safe fallback."),
|
||||
io.Combo.Input("sla_block_size",
|
||||
io.Combo.Input("sla_block_size", extra_dict=shows(("sla", [True]), ("sla_method", ["sla"]), ("sla_engine", ["triton"])),
|
||||
options=["32", "64", "128"],
|
||||
default=str(sla_attention.SLA_DEFAULTS["block_size"]),
|
||||
optional=True,
|
||||
@@ -81,21 +87,21 @@ class FunPackDiffusionModelLoader(io.ComfyNode):
|
||||
"the same rows are 3% of a video frame. Use 128 only "
|
||||
"when the audio does not matter; 32 routes finer still for "
|
||||
"about the same time. Triton engine only."),
|
||||
io.Boolean.Input("sla_protect_audio",
|
||||
io.Boolean.Input("sla_protect_audio", extra_dict=shows(("sla", [True]), ("sla_method", ["sla", "sol-attn"])),
|
||||
default=sla_attention.SLA_DEFAULTS["protect_audio"], optional=True,
|
||||
tooltip="Always attend the prompt's words and every audio "
|
||||
"stream, whatever top-k picks. Audio is ~1% of the packed "
|
||||
"sequence, so plain top-k regularly drops all of it "
|
||||
"and the soundtrack degrades while the video still "
|
||||
"looks fine. Reference images stay sparse: see sla_references."),
|
||||
io.Int.Input("sla_min_seq_len",
|
||||
io.Int.Input("sla_min_seq_len", extra_dict=shows(("sla", [True])),
|
||||
default=sla_attention.SLA_DEFAULTS["min_seq_len"],
|
||||
min=0, max=1000000, step=1024, optional=True,
|
||||
tooltip="Sequences shorter than this stay dense. Guards the short "
|
||||
"text refiner, which must never be sparsified, and "
|
||||
"low-resolution runs where block selection costs more "
|
||||
"than it saves."),
|
||||
io.Int.Input("sla_dense_last_steps",
|
||||
io.Int.Input("sla_dense_last_steps", extra_dict=shows(("sla", [True]), ("sla_method", ["sla", "sol-attn"])),
|
||||
default=sla_attention.SLA_DEFAULTS["dense_last_steps"],
|
||||
min=0, max=8, optional=True,
|
||||
tooltip="Run the last N sampling steps at full attention. 0 "
|
||||
@@ -112,38 +118,38 @@ class FunPackDiffusionModelLoader(io.ComfyNode):
|
||||
"always holds; the speed-up is lost on layers a LoRA is applied "
|
||||
"to and when compute dtype is forced."),
|
||||
# Added later, so after everything: saved workflows can store widget values by position.
|
||||
io.Combo.Input("sla_engine", options=list(sla_attention.ENGINES),
|
||||
io.Combo.Input("sla_engine", extra_dict=shows(("sla", [True]), ("sla_method", ["sla"])), options=list(sla_attention.ENGINES),
|
||||
default=sla_attention.SLA_DEFAULTS["engine"], optional=True,
|
||||
tooltip="What runs the sparse calls. comfy_kitchen: ComfyUI's compiled "
|
||||
"int8 sol_attn (needs comfy-kitchen 0.2.32+; the Triton kernel "
|
||||
"runs instead where it is missing, and the status says so). "
|
||||
"triton: FunPack's own kernel, the only one with block size and "
|
||||
"stabilize motion."),
|
||||
io.String.Input("sla_dense_steps", default=sla_attention.SLA_DEFAULTS["dense_steps"],
|
||||
io.String.Input("sla_dense_steps", extra_dict=shows(("sla", [True]), ("sla_method", ["sla", "sol-attn"])), default=sla_attention.SLA_DEFAULTS["dense_steps"],
|
||||
optional=True,
|
||||
tooltip="Steps at full attention, counted from 0: '0' or '0,1' or "
|
||||
"'0-2'. The first steps set the layout and how closely the "
|
||||
"prompt is followed, so the first one is kept exact by "
|
||||
"default. Blank: none. Adds to dense last steps. Not used by vsa."),
|
||||
io.Combo.Input("sla_references", options=list(sla_attention.REFERENCES),
|
||||
io.Combo.Input("sla_references", extra_dict=shows(("sla", [True]), ("sla_method", ["sla", "sol-attn"])), options=list(sla_attention.REFERENCES),
|
||||
default=sla_attention.SLA_DEFAULTS["references"], optional=True,
|
||||
tooltip="Reference images, conditioning frames and the prompt's vision "
|
||||
"tokens. off: sparse like the video. light: each keeps its best "
|
||||
"15% for every query block. heavy: all of them exact, slow with "
|
||||
"big references. On comfy_kitchen, light picks one 15% for all "
|
||||
"query blocks."),
|
||||
io.Boolean.Input("sla_tail", default=sla_attention.SLA_DEFAULTS["tail"], optional=True,
|
||||
io.Boolean.Input("sla_tail", extra_dict=shows(("sla", [True]), ("sla_method", ["sla"]), ("sla_engine", ["comfy_kitchen"])), default=sla_attention.SLA_DEFAULTS["tail"], optional=True,
|
||||
tooltip="comfy_kitchen engine: the skipped blocks still count, as one "
|
||||
"averaged term per query block, instead of not at all. Off "
|
||||
"matches what the SLA turbo LoRA was trained against; untested "
|
||||
"on H3."),
|
||||
io.Boolean.Input("sla_stabilize_motion",
|
||||
io.Boolean.Input("sla_stabilize_motion", extra_dict=shows(("sla", [True]), ("sla_method", ["sla"]), ("sla_engine", ["triton"])),
|
||||
default=sla_attention.SLA_DEFAULTS["stabilize_motion"], optional=True,
|
||||
tooltip="Triton engine: each layer leans toward the blocks it picked last "
|
||||
"step, so near ties stop flipping and showing as a faint double "
|
||||
"exposure on fast motion. A fix for that one symptom; a little "
|
||||
"more memory."),
|
||||
io.Combo.Input("sla_method", options=list(sla_attention.METHODS),
|
||||
io.Combo.Input("sla_method", extra_dict=shows(("sla", [True])), options=list(sla_attention.METHODS),
|
||||
default=sla_attention.SLA_DEFAULTS["method"], optional=True,
|
||||
tooltip="How the blocks worth attending are chosen. sla: a fixed share "
|
||||
"(sla_sparsity), what SLA turbo LoRAs were trained against. "
|
||||
@@ -152,7 +158,7 @@ class FunPackDiffusionModelLoader(io.ComfyNode):
|
||||
"for it (the status says when the model lacks VSA's layers); run "
|
||||
"by ComfyUI's own Model Sparse Attention, keeping 1 - sla_sparsity "
|
||||
"of the cubes."),
|
||||
io.Float.Input("sla_tau", default=sla_attention.SLA_DEFAULTS["tau"], min=0.0, max=4.0,
|
||||
io.Float.Input("sla_tau", extra_dict=shows(("sla", [True]), ("sla_method", ["sol-attn"])), default=sla_attention.SLA_DEFAULTS["tau"], min=0.0, max=4.0,
|
||||
step=0.05, optional=True,
|
||||
tooltip="sol-attn only: higher skips more. About 1.0 keeps ~16% of key "
|
||||
"blocks exact, 1.5 ~7%, 2.0 ~3%."),
|
||||
|
||||
@@ -490,3 +490,15 @@ def test_sol_attn_routes_by_threshold_and_vsa_hands_over_to_comfyui(monkeypatch)
|
||||
assert installed and got["vsa"] is True and abs(got["topk_ratio"] - 0.1) < 1e-9
|
||||
assert got["override"] is backend # dense calls still go to the chosen backend
|
||||
assert "no VSA layers" in note
|
||||
|
||||
|
||||
def test_the_loader_draws_each_sla_field_only_in_its_mode():
|
||||
"""'All values are fucked up': every SLA field was drawn whatever the method, sparsity beside
|
||||
sol-attn's own threshold."""
|
||||
from modules.loaders.diffusion_model.nodes import FunPackDiffusionModelLoader as L
|
||||
rules = {i.id: {c["input"]: c["when"] for c in (i.extra_dict or {}).get("funpack_shows", [])}
|
||||
for i in L.define_schema().inputs}
|
||||
assert rules["sla_sparsity"]["sla_method"] == ["sla", "vsa"] and rules["sla_tau"]["sla_method"] == ["sol-attn"]
|
||||
assert rules["sla_block_size"]["sla_engine"] == ["triton"] and rules["sla_tail"]["sla_engine"] == ["comfy_kitchen"]
|
||||
assert all(r.get("sla") == [True] for name, r in rules.items() if name.startswith("sla_"))
|
||||
assert rules["sla"] == {} and rules["int8_convrot"] == {}
|
||||
|
||||
Reference in New Issue
Block a user