Fix root-only pipeline index detection, seed range overflow, local safetensors loads
Require the model_index.json to sit at the snapshot ROOT before flagging a cached repo as pipeline-loadable: CachedFileInfo.file_name is the basename, so the previous name match also claimed nested copies (subdir/model_index.json) and the picker then sent a from_pretrained load that fails only after the GPU handoff. Scope by file_path against the revision's snapshot_path. Validate the maximum derived seed before the multi-run image loop: an explicit seed near 2**53-1 plus the per-run offset (base + i*batchSize) exceeded the backend cap and 422'd a later run after earlier images had already generated. Route local single-file .safetensors picks on the Images and Video pages through the single_file load path (parent dir + basename), matching the local GGUF branch: the pipeline route rejects a bare file with no model_index.json, and only after evicting the resident model.
This commit is contained in:
parent
de2f22df2b
commit
39b256f8c9
4 changed files with 96 additions and 4 deletions
|
|
@ -3386,13 +3386,23 @@ async def list_cached_gguf(current_subject: str = Depends(get_current_subject)):
|
|||
|
||||
|
||||
def _repo_has_pipeline_index(repo_info) -> bool:
|
||||
"""Whether the cached snapshot carries a model_index.json, i.e. is loadable as a
|
||||
full diffusers pipeline (from_pretrained). Single-file / ComfyUI checkpoints ship
|
||||
none and need a checkpoint filename + from_single_file instead."""
|
||||
"""Whether the cached snapshot carries a ROOT model_index.json, i.e. is loadable
|
||||
as a full diffusers pipeline (from_pretrained reads only the repo root). A nested
|
||||
subdir/model_index.json does not count: loading the repo root still fails, so the
|
||||
row must keep its single_file flag. CachedFileInfo.file_name is the basename, so
|
||||
a name match alone would also claim nested copies -- scope by file_path when the
|
||||
scan provides it."""
|
||||
try:
|
||||
for rev in repo_info.revisions:
|
||||
snapshot = getattr(rev, "snapshot_path", None)
|
||||
for f in rev.files:
|
||||
if f.file_name == "model_index.json" or f.file_name.endswith("/model_index.json"):
|
||||
name = str(getattr(f, "file_name", "") or "")
|
||||
path = getattr(f, "file_path", None)
|
||||
if path is not None and snapshot is not None:
|
||||
p = Path(path)
|
||||
if p.name == "model_index.json" and p.parent == Path(snapshot):
|
||||
return True
|
||||
elif name == "model_index.json":
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -1078,6 +1078,33 @@ def test_cached_repo_partial_scopes_probe_to_snapshot_dir(monkeypatch):
|
|||
assert models_route._cached_repo_partial("Org/Repo", snapshot_dir) is False
|
||||
|
||||
|
||||
def test_repo_has_pipeline_index_requires_root_model_index(tmp_path):
|
||||
# Only a ROOT model_index.json makes a repo pipeline-loadable: from_pretrained
|
||||
# reads the repo root, so a nested subdir/model_index.json must NOT clear the
|
||||
# single_file flag. CachedFileInfo.file_name is the basename, so the helper has
|
||||
# to scope by file_path/snapshot_path -- a name-only match would claim both.
|
||||
snap = tmp_path / "snapshots" / "abc"
|
||||
nested = SimpleNamespace(
|
||||
file_name = "model_index.json",
|
||||
file_path = snap / "prior" / "model_index.json",
|
||||
)
|
||||
repo_nested = SimpleNamespace(
|
||||
repo_id = "unsloth/nested-index",
|
||||
revisions = [SimpleNamespace(files = [nested], snapshot_path = snap)],
|
||||
)
|
||||
assert models_route._repo_has_pipeline_index(repo_nested) is False
|
||||
|
||||
root = SimpleNamespace(
|
||||
file_name = "model_index.json",
|
||||
file_path = snap / "model_index.json",
|
||||
)
|
||||
repo_root = SimpleNamespace(
|
||||
repo_id = "unsloth/root-index",
|
||||
revisions = [SimpleNamespace(files = [root], snapshot_path = snap)],
|
||||
)
|
||||
assert models_route._repo_has_pipeline_index(repo_root) is True
|
||||
|
||||
|
||||
def test_list_cached_models_flags_single_file_diffusion_repos(monkeypatch, tmp_path):
|
||||
# A diffusion-tagged repo with NO top-level model_index.json is a single-file
|
||||
# checkpoint: the task pickers must not offer it as a pipeline load (from_pretrained
|
||||
|
|
|
|||
|
|
@ -1660,6 +1660,29 @@ export function ImagesPage({ active = true }: { active?: boolean }) {
|
|||
});
|
||||
return;
|
||||
}
|
||||
// A direct local single-file .safetensors pick (custom folder / on-device file)
|
||||
// must load via from_single_file: the pipeline route rejects a bare file (no
|
||||
// model_index.json) and only after evicting the resident model. Split into
|
||||
// (parent dir, basename) exactly like the local GGUF branch above.
|
||||
if (meta.source === "local" && id.toLowerCase().endsWith(".safetensors")) {
|
||||
const norm = id.replace(/\\/g, "/");
|
||||
const slash = norm.lastIndexOf("/");
|
||||
const filename = slash >= 0 ? norm.slice(slash + 1) : norm;
|
||||
const dir = slash >= 0 ? norm.slice(0, slash) : ".";
|
||||
const prevQuant = quant;
|
||||
quantRevert.current = { prev: prevQuant };
|
||||
setQuant(filename);
|
||||
const dsf = defaultsFor(id);
|
||||
setSteps(dsf.steps);
|
||||
setGuidance(dsf.guidance);
|
||||
void handleLoad(dir, { kind: "single_file", filename }).then((started) => {
|
||||
if (!started) {
|
||||
setQuant(prevQuant);
|
||||
quantRevert.current = null;
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
// Otherwise treat it as a full diffusers repo (safetensors / bnb-4bit). The backend
|
||||
// infers the family + base repo from the id and gates loads to unsloth/* repos or
|
||||
// on-device paths, so only attempt those; other Hub orgs can't be assembled here.
|
||||
|
|
@ -1839,6 +1862,15 @@ export function ImagesPage({ active = true }: { active?: boolean }) {
|
|||
const runs = Number.isFinite(count) && count >= 1 ? Math.floor(count) : 1;
|
||||
if (runs !== count) setCount(runs);
|
||||
|
||||
// An explicit seed near the 2**53-1 backend cap can overflow once the per-run
|
||||
// offset (base + i*batchSize) and the engine's in-batch +j offsets are added,
|
||||
// 422ing a later run AFTER earlier images already generated. Fail before any
|
||||
// GPU work. Subtraction keeps the comparison exact where the sum would round.
|
||||
if (baseSeed > Number.MAX_SAFE_INTEGER - (runs * batchSize - 1)) {
|
||||
toast.error("Seed too large for this run count and batch size; use a smaller seed");
|
||||
return;
|
||||
}
|
||||
|
||||
setBusy("generating");
|
||||
setGenDone(0);
|
||||
setGenStep(null);
|
||||
|
|
|
|||
|
|
@ -1044,6 +1044,29 @@ export function VideoPage({ active = true }: { active?: boolean }) {
|
|||
});
|
||||
return;
|
||||
}
|
||||
// A direct local single-file .safetensors pick must load via from_single_file:
|
||||
// the pipeline route rejects a bare file (no model_index.json) and only after
|
||||
// evicting the resident model. Split into (parent dir, basename) exactly like
|
||||
// the local GGUF branch above.
|
||||
if (meta.source === "local" && id.toLowerCase().endsWith(".safetensors")) {
|
||||
const norm = id.replace(/\\/g, "/");
|
||||
const slash = norm.lastIndexOf("/");
|
||||
const filename = slash >= 0 ? norm.slice(slash + 1) : norm;
|
||||
const dir = slash >= 0 ? norm.slice(0, slash) : ".";
|
||||
const prevQuant = quant;
|
||||
quantRevert.current = { prev: prevQuant };
|
||||
setQuant(filename);
|
||||
const dsf = defaultsFor(id);
|
||||
setSteps(dsf.steps);
|
||||
setGuidance(dsf.guidance);
|
||||
void handleLoad(dir, { kind: "single_file", filename }).then((started) => {
|
||||
if (!started) {
|
||||
setQuant(prevQuant);
|
||||
quantRevert.current = null;
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
// Otherwise treat it as a full diffusers repo. The backend gates loads to unsloth/*
|
||||
// repos, the family bases, or on-device paths, so only attempt those.
|
||||
if (meta.source !== "local" && !id.toLowerCase().startsWith("unsloth/")) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue