fix: harden per-GPU preview outputs after review
- Typed serverless pools validate the unpruned workflow, so a preview on any new slot failed the whole run; both serverless compilers now accept every Collector output slot (COLLECTOR_OUTPUT_COUNT). - Serverless pools are config workers: their serverless:<pool>:<job> participants now share the pool's worker_N slot instead of overflowing. - A worker returning only empty frames no longer raises in the preview split (the combined batch already skipped them). - Worker previews are views into the combined batch, not a second copy, and a frame-count guard skips them rather than misattribute frames. - Frontend: one shared config fetch per load; unnamed slots drop their saved label instead of storing a redundant one. - Tooltip notes that the per-GPU outputs are unavailable downstream of Ultimate SD Upscale Distributed. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
18ff755001
commit
339719ece7
+20
-2
@@ -7,6 +7,7 @@ const WORKERS_CHANGED_EVENT = "distributed:workers-changed";
|
||||
|
||||
const trackedNodes = new Set();
|
||||
let workers = [];
|
||||
let workersPromise = null;
|
||||
let listenerAttached = false;
|
||||
|
||||
async function fetchWorkers() {
|
||||
@@ -20,13 +21,30 @@ async function fetchWorkers() {
|
||||
}
|
||||
}
|
||||
|
||||
// Collectors created together (a workflow load) share one config fetch.
|
||||
function loadWorkers() {
|
||||
if (!workersPromise) {
|
||||
workersPromise = fetchWorkers().then((list) => {
|
||||
workers = list;
|
||||
if (list.length === 0) workersPromise = null;
|
||||
return list;
|
||||
});
|
||||
}
|
||||
return workersPromise;
|
||||
}
|
||||
|
||||
// Only the displayed label changes; slot names stay fixed so links survive.
|
||||
// Unnamed slots drop the label so saved workflows carry no redundant copies.
|
||||
function labelWorkerOutputs(node, configWorkers) {
|
||||
for (const output of node.outputs || []) {
|
||||
if (!output?.name?.startsWith(WORKER_OUTPUT_PREFIX)) continue;
|
||||
const index = parseInt(output.name.slice(WORKER_OUTPUT_PREFIX.length), 10) - 1;
|
||||
const worker = configWorkers[index];
|
||||
output.label = worker?.name ? `${output.name} (${worker.name})` : output.name;
|
||||
if (worker?.name) {
|
||||
output.label = `${output.name} (${worker.name})`;
|
||||
} else {
|
||||
delete output.label;
|
||||
}
|
||||
}
|
||||
node.setDirtyCanvas?.(true, true);
|
||||
}
|
||||
@@ -67,7 +85,7 @@ app.registerExtension({
|
||||
return originalOnRemoved ? originalOnRemoved.call(this) : undefined;
|
||||
};
|
||||
|
||||
if (workers.length === 0) workers = await fetchWorkers();
|
||||
if (workers.length === 0) await loadWorkers();
|
||||
labelWorkerOutputs(node, workers);
|
||||
} catch (error) {
|
||||
console.error("Error in DistributedCollector outputs extension:", error);
|
||||
|
||||
@@ -5,14 +5,15 @@ import { describe, expect, it } from "vitest";
|
||||
const source = readFileSync(new URL("../collectorOutputs.js", import.meta.url), "utf8")
|
||||
.replace(/^import .*;\r?\n/gm, "");
|
||||
|
||||
function setup(workers) {
|
||||
function setup(workers, onFetch = () => {}) {
|
||||
let extension;
|
||||
const listeners = new Map();
|
||||
vm.runInNewContext(source, {
|
||||
app: { registerExtension: (ext) => { extension = ext; } },
|
||||
ENDPOINTS: { CONFIG: "/distributed/config" },
|
||||
NODE_CLASSES: { DISTRIBUTED_COLLECTOR: "DistributedCollector" },
|
||||
fetch: async () => ({ ok: true, json: async () => ({ workers }) }),
|
||||
fetch: async () => { onFetch(); return { ok: true, json: async () => ({ workers }) }; },
|
||||
Promise,
|
||||
window: { addEventListener: (name, fn) => listeners.set(name, fn) },
|
||||
parseInt,
|
||||
Array,
|
||||
@@ -32,10 +33,26 @@ describe("DistributedCollector per-GPU outputs", () => {
|
||||
expect(node.outputs.map((o) => o.name)).toEqual(
|
||||
["images", "audio", "master", "worker_1", "worker_2", "worker_3"]);
|
||||
expect(node.outputs.slice(3).map((o) => o.label)).toEqual(
|
||||
["worker_1 (GPU A)", "worker_2 (GPU B)", "worker_3"]);
|
||||
["worker_1 (GPU A)", "worker_2 (GPU B)", undefined]);
|
||||
expect(node.outputs[2].label).toBeUndefined();
|
||||
});
|
||||
|
||||
it("shares one config fetch across collectors created together", async () => {
|
||||
let fetches = 0;
|
||||
const { extension, node } = setup([{ id: "a", name: "GPU A" }], () => { fetches += 1; });
|
||||
const second = { ...node, outputs: node.outputs.map((o) => ({ ...o })) };
|
||||
await Promise.all([extension.nodeCreated(node), extension.nodeCreated(second)]);
|
||||
expect(fetches).toBe(1);
|
||||
expect(second.outputs[3].label).toBe("worker_1 (GPU A)");
|
||||
});
|
||||
|
||||
it("clears a stale saved label when the slot has no configured worker", async () => {
|
||||
const { extension, node } = setup([]);
|
||||
node.outputs[3].label = "worker_1 (Old PC)";
|
||||
await extension.nodeCreated(node);
|
||||
expect("label" in node.outputs[3]).toBe(false);
|
||||
});
|
||||
|
||||
it("relabels when the worker list changes", async () => {
|
||||
const { extension, listeners, node } = setup([{ id: "a", name: "GPU A" }]);
|
||||
await extension.nodeCreated(node);
|
||||
|
||||
Reference in New Issue
Block a user