Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
164 lines
14 KiB
JavaScript
164 lines
14 KiB
JavaScript
// Step 3, Train: the settings, Start and Stop. The graph, previews and progress of a run are in the Results step.
|
|
import { composer as c } from "../../composer/composer.js";
|
|
import { stillOf } from "./state.js";
|
|
|
|
/** The node's own defaults. A set remembers what was changed from them. */
|
|
export const DEFAULTS = {
|
|
name: "funui_lora", use: "all", preview_every: 0, preview_strength: 1, save_every: 0, steps: 500, learning_rate: 0.0001, rank: 16, algorithm: "LoRA", optimizer: "AdamW", loss: "MSE",
|
|
batch_size: 1, grad_accumulation: 1, resolution: 512, aspect_buckets: false, frames: 33, fps: 0, start: "start", seed: 0,
|
|
training_dtype: "bf16", lora_dtype: "bf16", gradient_checkpointing: true, offloading: false, bypass: false, swap_blocks: 0,
|
|
};
|
|
export const PRESETS = [
|
|
{ value: "check", label: "Quick check", steps: 100, rank: 8 },
|
|
{ value: "standard", label: "Standard", steps: 500, rank: 16 },
|
|
{ value: "long", label: "Thorough", steps: 2000, rank: 32 },
|
|
];
|
|
const opts = (list) => list.map((v) => ({ value: v, label: v }));
|
|
|
|
/** The settings for a run: the defaults, then what the set remembers. */
|
|
export const settingsOf = (doc) => ({ ...DEFAULTS, ...((doc && doc.settings) || {}), use: "all" }); // the Data step decides what is in; a set made before that remembers an old choice
|
|
|
|
const fmt = (n) => (Number.isFinite(n) ? (Math.abs(n) >= 0.01 ? n.toFixed(4) : n.toPrecision(3)) : "");
|
|
const clock = (s) => { s = Math.round(s || 0); return s >= 3600 ? `${Math.floor(s / 3600)} h ${Math.floor((s % 3600) / 60)} min` : s >= 60 ? `${Math.floor(s / 60)} min ${s % 60} s` : `${s} s`; };
|
|
const seconds = (s) => (s >= 10 ? `${Math.round(s)} s` : `${s.toFixed(1)} s`);
|
|
export { fmt, clock, seconds };
|
|
|
|
/** Seconds each step took, as a line. A step that also loaded the model (the first after a start or a restart) has no number and is bridged. */
|
|
export function stepTimes(times) {
|
|
const real = (times || []).filter((t) => t !== null && t !== undefined);
|
|
if (real.length < 2) return null;
|
|
let last = real[0];
|
|
const values = times.map((t) => { if (t !== null && t !== undefined) last = t; return last; });
|
|
return c.chart.line({ series: [{ name: "seconds per step", values, tone: "good" }], xLabel: "step", yLabel: "seconds", height: "sm", empty: "" });
|
|
}
|
|
|
|
export function trainStep({ app, session, go, ui }) {
|
|
const doc = session.doc;
|
|
if (!doc) return c.emptyState.default({ title: "No set open", hint: "Pick or make a training set in the Data step.", action: c.button.sm({ label: "Back to data", tone: "primary", onClick: () => go("data") }) });
|
|
const v = settingsOf(doc);
|
|
const set = (key) => (value) => { session.edit((d) => { d.settings = { ...(d.settings || {}), [key]: value }; }); };
|
|
const num = (key, label, hint, o = {}) => c.field.default({ label, hint, control: c.number.md({ label, value: v[key], onChange: set(key), ...o }) });
|
|
const pickOf = (key, label, hint, options) => c.field.default({ label, hint, control: c.select.md({ label, options, value: v[key], onChange: set(key) }) });
|
|
const flag = (key, label, hint) => c.toggle.default({ label, hint, checked: v[key], onChange: set(key) });
|
|
|
|
const items = doc.items;
|
|
const used = items;
|
|
const slots = (app.pipeline && app.pipeline.slots && app.pipeline.slots()) || [];
|
|
const hasAudioVae = slots.some((s) => s.id === "audio_vae");
|
|
const caption = used.filter((i) => i.caption.trim()).length;
|
|
|
|
const name = c.field.default({ label: "Name of the LoRA", hint: "Saved in your loras folder. An earlier file is never overwritten.", control: c.input.md({ label: "Name", value: v.name, onCommit: set("name") }) });
|
|
const preset = c.segmented.sm({ label: "Preset", options: PRESETS, value: v.preset || (PRESETS.find((p) => p.steps === v.steps && p.rank === v.rank) || {}).value, onChange: (key) => {
|
|
const p = PRESETS.find((x) => x.value === key);
|
|
session.edit((d) => { d.settings = { ...(d.settings || {}), preset: key, steps: p.steps, rank: p.rank }; });
|
|
ui.redraw();
|
|
} });
|
|
|
|
const advanced = c.collapsible.default({ label: "More settings", open: ui.advanced, onToggle: (o) => { ui.advanced = o; }, body: c.region.stack({ gap: "sm", children: [
|
|
pickOf("algorithm", "Adapter", "LoRA is the common one.", opts(["LoRA", "LoHa", "LoKr", "OFT"])),
|
|
pickOf("optimizer", "Optimizer", "How the weights are updated.", opts(["AdamW", "Adam", "SGD", "RMSprop"])),
|
|
pickOf("loss", "Loss", "How the error is measured.", opts(["MSE", "L1", "Huber", "SmoothL1"])),
|
|
num("batch_size", "Batch size", "Samples per step. H3 always uses 1.", { min: 1, max: 64, step: 1 }),
|
|
num("grad_accumulation", "Gradient accumulation", "Steps added up before the weights change: a bigger batch for less memory.", { min: 1, max: 256, step: 1 }),
|
|
pickOf("training_dtype", "Training precision", "bf16 is right for nearly everything.", opts(["bf16", "fp32", "none"])),
|
|
pickOf("lora_dtype", "LoRA precision", "How the file is stored.", opts(["bf16", "fp32"])),
|
|
num("seed", "Seed", "The same seed repeats the same run.", { min: 0, step: 1 }),
|
|
flag("gradient_checkpointing", "Save memory (gradient checkpointing)", "Slower, but needs far less memory."),
|
|
num("swap_blocks", "Blocks kept in system memory", "Block swap: this many transformer blocks wait in system memory and come to the card one layer at a time. Slower; each block frees about 0.75 GiB (H3 has 50). 0 keeps the whole model on the card.", { min: 0, max: 128, step: 1 }),
|
|
flag("offloading", "Keep weights in system memory (ComfyUI's own)", "Slower. It brought ComfyUI down with H3 on a rental: prefer Blocks kept in system memory."),
|
|
flag("bypass", "Bypass mode", "Applies the LoRA by hooks, for quantized models."),
|
|
] }) });
|
|
|
|
const run = ui.run;
|
|
const live = c.region.stack({ gap: "md", children: [notice(ui, go)].filter(Boolean) }); // redrawn on its own while a run goes on: the settings above stay as they are
|
|
ui.monitorBox = live;
|
|
const waiting = run.state === "paused" || run.state === "previewing"; // between steps: nothing is training, the run is still open
|
|
const busy = run.active || run.state === "previewing";
|
|
const start = c.button.lg({ label: busy ? "Training…" : "Start training", tone: "primary", disabled: busy || waiting || !used.length, onClick: () => ui.start() });
|
|
start.node.hidden = waiting;
|
|
const stop = c.button.lg({ label: "Stop", tone: "danger", onClick: () => ui.stop() });
|
|
stop.node.hidden = !busy && !waiting;
|
|
const pause = c.button.lg({ label: run.state === "previewing" ? "Stay paused" : "Pause", tone: "neutral", disabled: ui.holdPause && run.state !== "paused", onClick: () => ui.pause() });
|
|
pause.node.hidden = !(run.state === "training" || run.state === "previewing");
|
|
const resume = c.button.lg({ label: "Resume", tone: "primary", onClick: () => ui.resume() });
|
|
resume.node.hidden = run.state !== "paused";
|
|
const previewNow = c.button.lg({ label: "Preview now", tone: "neutral", title: "Make a picture or clip with the LoRA so far", onClick: () => ui.previewNow() });
|
|
previewNow.node.hidden = !(run.state === "paused" && run.paused && run.paused.preview);
|
|
|
|
ui.controls = [stop, pause, previewNow, resume, start]; // shown in the window's footer, with the run's state beside them
|
|
return c.region.stack({ gap: "lg", children: [
|
|
live,
|
|
c.hint.default({ text: used.length ? `${used.length} item${used.length > 1 ? "s" : ""} will be learned from (${caption} with tags). ${used.length - caption ? "Items without a caption are learned from with only the prefix and postfix." : ""}`.trim()
|
|
: `"${session.name}" is empty. Add media in the Data step.` }),
|
|
hasAudioVae ? c.banner.info({ text: "Trains picture and sound together, one clip at a time: slow, and needs a lot of memory. Start with learning rate 0.0001, rank 16, batch 1. A turbo model can lose its few-step quality." }) : null,
|
|
c.group.default({ label: "Basics", rows: [name, preset,
|
|
num("steps", "Steps", "More steps learn more and can overdo it.", { min: 1, max: 100000, step: 10 }),
|
|
num("learning_rate", "Learning rate", "How big each step is. Lower is safer and slower.", { min: 0.0000001, max: 1, step: 0.0001, precision: 7 }),
|
|
num("rank", "Rank", "How much it can learn. Higher holds more detail and makes a bigger file.", { min: 1, max: 128, step: 1 }),
|
|
num("save_every", "Save a copy every (steps)", "0 saves only the finished LoRA. Otherwise a copy of the LoRA so far goes to your loras folder every this many steps, named like yourname_step200, so you can pick the best moment.", { min: 0, max: 100000, step: 10 }),
|
|
num("preview_strength", "Preview strength", "How strongly the LoRA is applied in previews. 1 is the usual; use a negative value (for example -1) to see a LoRA trained to be subtracted. Only for previews: it is not part of the training.", { min: -10, max: 10, step: 0.1, precision: 2 }),
|
|
num("preview_every", "Preview every (steps)", "0 never. Otherwise training waits every this many steps while your pipeline makes a picture or clip with the LoRA so far (the picked scene, the project's own settings), then carries on. Needs a LoRA loader in the pipeline; each wait loads the models again.", { min: 0, max: 100000, step: 10 })] }),
|
|
c.group.default({ label: "Pictures and clips", rows: [
|
|
num("resolution", "Size", "Pixels per side. Everything is fitted to about this area.", { min: 64, max: 2048, step: 32 }),
|
|
flag("aspect_buckets", "Keep each shape", "A few size groups instead of one shape for all. Not used for H3."),
|
|
num("frames", "Frames per clip", "For video models. H3 rounds to its own grid (17 x n + 5).", { min: 1, max: 1025, step: 1 }),
|
|
num("fps", "Frames per second", "0 uses each clip's own rate (H3: 24).", { min: 0, max: 120, step: 0.5 }),
|
|
pickOf("start", "Take frames from", "", [{ value: "start", label: "The start of the clip" }, { value: "middle", label: "The middle" }])] }),
|
|
advanced,
|
|
].filter(Boolean) });
|
|
}
|
|
|
|
/** What the controls said last (Pausing, Stopping): shown beside the controls in Train, not with the graph in Results. */
|
|
const aboutControls = (message) => /^(Pausing|Stopping|It will wait)/.test(message || "");
|
|
|
|
/** The run's state in a few words, beside the controls in the footer. */
|
|
export function footNote(ui) {
|
|
const run = ui.run;
|
|
if (aboutControls(run.message)) return run.message;
|
|
if (!run.shown) return "";
|
|
return { training: `Training, step ${run.step || 0} of ${run.total || 0}`, queued: "Starting", paused: `Paused at step ${run.step || 0}`, previewing: "Making a preview", done: "Finished", stopped: "Stopped", error: "Stopped with an error" }[run.state] || "";
|
|
}
|
|
|
|
/** What the Train step says about a run: where to look, and the error if it failed. */
|
|
export function notice(ui, go) {
|
|
const run = ui.run;
|
|
if (!run.shown) return null;
|
|
const where = c.button.sm({ label: "Open Results", tone: "ghost", onClick: () => go("results") });
|
|
if (run.error) return c.region.stack({ gap: "md", children: [c.inlineError.default({ text: run.error }), where] });
|
|
const text = { done: "Training finished.", stopped: "Training stopped.", paused: `Training paused at step ${run.step || 0}.`, previewing: "Making a preview." }[run.state] || "Training started.";
|
|
return c.region.stack({ gap: "md", children: [c.banner.info({ text: `${aboutControls(run.message) ? `${run.message}. ` : ""}${text} Graphs and previews are on the Results step.` }), where] });
|
|
}
|
|
|
|
/** Progress, the message, and the loss graph. */
|
|
export function monitor(ui) {
|
|
const run = ui.run;
|
|
if (!run.shown) return null;
|
|
const chart = c.chart.line({ series: [{ name: "loss", values: run.loss, tone: "accent" }, ...(run.smooth.length > 1 ? [{ name: "smoothed", values: run.smooth, tone: "good" }] : [])],
|
|
xLabel: "step", yLabel: "loss", height: "md", empty: "The first steps are on their way" });
|
|
const done = run.step || 0, total = run.total || 1;
|
|
const left = run.stepSeconds && run.state === "training" ? run.stepSeconds * (total - done) : 0;
|
|
const lines = [
|
|
run.state === "training" || run.state === "queued" ? `Step ${done} of ${total}` : null,
|
|
run.stepSeconds ? `${seconds(run.stepSeconds)} per step on average${run.lastStep ? `, ${seconds(run.lastStep)} for the last` : ""}` : null,
|
|
run.elapsed ? `${clock(run.elapsed)} spent in all, the restarts included` : null,
|
|
left ? `about ${clock(left)} left` : null,
|
|
run.message && !aboutControls(run.message) ? run.message : null,
|
|
].filter(Boolean);
|
|
const stepChart = stepTimes(run.times);
|
|
const previews = ui.previews && ui.previews.length ? c.gallery.adaptive({ id: "training-previews", cols: 3, selection: [],
|
|
items: ui.previews.map((p) => ({ id: p.id, label: `Step ${p.step}`, thumb: stillOf(p), badge: p.media_kind })),
|
|
onActivate: (item) => ui.peek(ui.previews.find((p) => p.id === item.id)) }) : null;
|
|
const heading = { done: "Finished", error: "Stopped with an error", stopped: "Stopped", paused: `Paused at step ${done}`, previewing: `Making a preview at step ${done}` }[run.state] || "Training";
|
|
return c.group.default({ label: heading, rows: [c.region.stack({ gap: "lg", children: [
|
|
run.state === "previewing" ? c.progress.indeterminate({ label: "Making a preview" }) : null,
|
|
run.state === "training" ? c.progress.bar({ value: done, max: total, label: "Training progress" }) : run.state === "queued" ? c.progress.indeterminate({ label: "Waiting for ComfyUI" }) : null,
|
|
lines.length ? c.hint.default({ text: lines.join(" · ") }) : null,
|
|
run.error ? c.inlineError.default({ text: run.error }) : null,
|
|
run.lora ? c.banner.info({ text: `Saved as ${run.lora}. Pick it in a LoRA loader to try it.` }) : null,
|
|
previews,
|
|
chart,
|
|
stepChart,
|
|
run.loss.length ? c.hint.default({ text: `Latest loss ${fmt(run.loss[run.loss.length - 1])}. It should fall at first, then wobble; a LoRA is rarely better after the wobble stops shrinking.` }) : null,
|
|
].filter(Boolean) })] });
|
|
}
|