mirror of
https://github.com/unslothai/unsloth.git
synced 2026-08-23 15:53:46 +00:00
* Fix FLUX.2 Klein LoRA training quality * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Fix FLUX.2 Klein training review gaps * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Fix diffusion training mirror preflight * Drop base_specs on a DiT block so the hardware reason still reaches the UI A dit_block clears the family-level chip fields (params, qlora_vram_gb, gated, note) precisely so FamilyFacts falls through to vram_note, which carries the actionable reason: no CUDA, or no native bf16 on this GPU. base_specs was still published though, and the per-base entry wins in resolveDiffusionTrainingFacts, so selecting Klein base-9B on a blocked host put the 9B and 18 GB chips straight back. FamilyFacts renders vram_note only when there are no chips, so the user lost the explanation and got a generic refusal plus a size they cannot act on. Clear base_specs on the same condition. The overlay only ever feeds those chips (resolveDiffusionTrainingFacts is its sole consumer), so nothing functional depends on it being present. Covered by a test that pins a bf16-unsupported host and asserts both halves: the overlay is empty and vram_note still carries the reason. It also asserts a per-base overlay exists on an unblocked host first, so it cannot pass vacuously. Verified it fails with the fix reverted. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Keep the mirror out of the gate and out of the resume identity Two consequences of routing the fetch through prefer_ungated_mirror that the mirror was not meant to have. The gated-repo name check ran on the canonical id while the start route preflighted the fetch repo. For FLUX.1-dev or FLUX.2-dev without a token that means the route probes the public mirror, answers 200, frees the resident models, and only then does the child refuse the run by name. It now checks the repo the run will actually fetch, so the two agree: a mirror-backed run proceeds, and a genuinely gated fetch still fails in the route, before anything is evicted. base_revision was reading the fetch repo too. Which repo that is depends on local cache state, so the same base records a different rev- value once the upstream snapshot is evicted or the run moves to another machine, and mismatch_reason then refuses the checkpoint as a different base revision though the weights are byte identical. Every checkpoint written before mirrors existed holds the canonical value, so those would be refused as well. Both identity_for_config and the post-load re-read go back to the canonical id; a canonical repo with no local ref records "unresolved", which is already treated as not comparable. Tests, both mutation checked: the gate assertion fails when the canonical id is restored, and the revision assertion fails when the fetch repo is. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Pair the recorded base revision with the repo it was read from A mirror-backed run resolves the revision from the mirror, so recording the canonical repo's revision instead lost validation entirely: source_revision(canonical) is "unresolved" on exactly the hosts a mirror is selected for, and mismatch_reason skips non-comparable revisions, so the check silently never fired. Record both halves and compare them only when the repos match, which still catches a moved mirror and no longer refuses a legitimate cross-repo resume. Bundles written before the field exist keep comparing as they did. Also take the mirror on a token-less run even when the vendor repo is cached: only gated repos are in the mirror table, so a mirror existing means the upstream needs credentials this run does not have. UNSLOTH_DIFFUSION_NO_MIRROR still pins the vendor repo. And preselect the training base a family pairs with a loaded distilled checkpoint: the distilled half is never in base_repos, so opening Train with the 9B model loaded seeded the 4B base. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Only override a cached base when its upstream is actually gated The mirror table stopped being gated-only when the redistributable bases joined it, so "a mirror exists" no longer implies "the upstream needs credentials". Klein base-4B is both a default trainable base and mirrored, and the Hub serves it anonymously, so a token-less run with a complete local cache was re-pulling it from the mirror and an offline run failed outright. Split the table rather than keep a second list in sync: the 12 genuinely gated pairs stay in _GATED_MIRROR_PAIRS, the redistributable ones move to _UNGATED_MIRROR_PAIRS, and mirror_repo and canonical_base build from the union so redirection and the base-keyed table normalisation are unchanged. upstream_is_gated reads the gated half, and the token-less override now asks it. * Keep a local clone named like a gated base out of the mirror override * Carry the local-clone and mirror exceptions through every base gate --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Daniel Han <danielhanchen@gmail.com>
111 lines
4.2 KiB
TypeScript
111 lines
4.2 KiB
TypeScript
// SPDX-License-Identifier: AGPL-3.0-only
|
|
// Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
|
|
|
|
import assert from "node:assert/strict";
|
|
import { readFile } from "node:fs/promises";
|
|
import test from "node:test";
|
|
|
|
import {
|
|
resolveDiffusionDeployBase,
|
|
resolveDiffusionTrainingBase,
|
|
} from "../src/features/images/train/diffusion-train-deploy.ts";
|
|
|
|
const klein = {
|
|
name: "flux.2-klein",
|
|
label: "FLUX.2 Klein",
|
|
default_base: "black-forest-labs/FLUX.2-klein-base-4B",
|
|
base_repos: [
|
|
"black-forest-labs/FLUX.2-klein-base-4B",
|
|
"black-forest-labs/FLUX.2-klein-base-9B",
|
|
],
|
|
deploy_bases: {
|
|
"black-forest-labs/FLUX.2-klein-base-4B":
|
|
"black-forest-labs/FLUX.2-klein-4B",
|
|
"black-forest-labs/FLUX.2-klein-base-9B":
|
|
"black-forest-labs/FLUX.2-klein-9B",
|
|
"unsloth/FLUX.2-klein-base-9B": "unsloth/FLUX.2-klein-9B",
|
|
},
|
|
};
|
|
|
|
test("deploys each Klein base adapter on its matching distilled checkpoint", () => {
|
|
assert.equal(
|
|
resolveDiffusionDeployBase(klein, "black-forest-labs/FLUX.2-klein-base-4B"),
|
|
"black-forest-labs/FLUX.2-klein-4B",
|
|
);
|
|
assert.equal(
|
|
resolveDiffusionDeployBase(klein, "BLACK-FOREST-LABS/FLUX.2-KLEIN-BASE-9B"),
|
|
"black-forest-labs/FLUX.2-klein-9B",
|
|
);
|
|
assert.equal(
|
|
resolveDiffusionDeployBase(klein, "unsloth/FLUX.2-klein-base-9B"),
|
|
"unsloth/FLUX.2-klein-9B",
|
|
);
|
|
});
|
|
|
|
test("keeps custom bases and the legacy family-wide mapping working", () => {
|
|
assert.equal(
|
|
resolveDiffusionDeployBase(klein, "/models/custom-klein"),
|
|
"/models/custom-klein",
|
|
);
|
|
assert.equal(
|
|
resolveDiffusionDeployBase(
|
|
{
|
|
...klein,
|
|
base_repos: ["krea/Krea-2-Raw"],
|
|
deploy_bases: {},
|
|
deploy_base: "krea/Krea-2-Turbo",
|
|
},
|
|
"krea/Krea-2-Raw",
|
|
),
|
|
"krea/Krea-2-Turbo",
|
|
);
|
|
});
|
|
|
|
test("preselects the training base paired with a loaded distilled checkpoint", () => {
|
|
assert.equal(
|
|
resolveDiffusionTrainingBase(klein, "black-forest-labs/FLUX.2-klein-9B"),
|
|
"black-forest-labs/FLUX.2-klein-base-9B",
|
|
);
|
|
assert.equal(
|
|
resolveDiffusionTrainingBase(klein, "BLACK-FOREST-LABS/FLUX.2-KLEIN-4B"),
|
|
"black-forest-labs/FLUX.2-klein-base-4B",
|
|
);
|
|
});
|
|
|
|
test("returns null rather than inventing a base the backend would refuse", () => {
|
|
assert.equal(resolveDiffusionTrainingBase(undefined, "black-forest-labs/FLUX.2-klein-9B"), null);
|
|
assert.equal(resolveDiffusionTrainingBase(klein, ""), null);
|
|
// Loaded checkpoint the family declares no pairing for.
|
|
assert.equal(resolveDiffusionTrainingBase(klein, "krea/Krea-2-Turbo"), null);
|
|
// A repo whose name matches nothing the family offers stays null.
|
|
assert.equal(resolveDiffusionTrainingBase(klein, "unsloth/FLUX.2-klein-42B"), null);
|
|
});
|
|
|
|
test("a mirror-loaded checkpoint preselects the vendor base it copies", () => {
|
|
// Deploy hands a LoRA trained on unsloth/FLUX.2-klein-base-9B the mirror checkpoint
|
|
// unsloth/FLUX.2-klein-9B, so that is what /images/status reports afterwards. Its pairing names
|
|
// the mirror TRAINING id, which base_repos does not offer, and the panel then fell back to the
|
|
// first base: the 4B, seeding a 9B workflow from 4B weights. A mirror keeps the upstream name.
|
|
assert.equal(
|
|
resolveDiffusionTrainingBase(klein, "unsloth/FLUX.2-klein-9B"),
|
|
"black-forest-labs/FLUX.2-klein-base-9B",
|
|
);
|
|
assert.equal(
|
|
resolveDiffusionTrainingBase(klein, "UNSLOTH/FLUX.2-KLEIN-9B"),
|
|
"black-forest-labs/FLUX.2-klein-base-9B",
|
|
);
|
|
});
|
|
|
|
test("the Train panel preselect actually consults the pairing", async () => {
|
|
// The helper on its own changes nothing: the bug was in the preselect chain, which fell from an
|
|
// exact base_repos match straight to base_repos[0]. Assert the pairing sits BETWEEN the two.
|
|
const source = await readFile(
|
|
new URL("../src/features/images/train/diffusion-train-panel.tsx", import.meta.url),
|
|
"utf8",
|
|
);
|
|
const paired = source.indexOf("pairedTrainingBase ??");
|
|
const first = source.indexOf("family.base_repos[0]");
|
|
assert.ok(paired > 0, "the preselect no longer falls back to the paired training base");
|
|
assert.ok(paired < first && first - paired < 40, "base_repos[0] is no longer the last resort");
|
|
assert.match(source, /resolveDiffusionTrainingBase\(reportedFamily, loadedBaseRepo\)/);
|
|
});
|