Three items from the latest review round. A scoped download job carries a deliberate file subset, and every file set of one repo rides the same "@scope" slot. A client that adopts a live job from the backend had no file list to compare against: the active-downloads response never carried one, so an adopted job's set was unknown and any later scoped request for the same repo read as "already started". Selecting a different checkpoint then waited on the wrong transfer and tried to load a file nobody fetched. The response now publishes the scoped file list, adoption records it, and an unknown set no longer satisfies a scoped request. A gallery record can be deleted while its blob is still downloading. The delete revokes the URL present at that moment, so the fetch that lands afterwards inserted a fresh object URL for a record no card renders and nothing can revoke: a full MP4, tens to hundreds of MB, pinned for the rest of the session, and once per raced fetch. Both galleries now discard a blob whose record went away, with an epoch covering the video page's Clear all. The video backend keeps the last completed job until the next one starts, and the Video page merges that record on mount to cover a job that finished after the gallery fetch. Deleting the clip left the record in place, so every reload prepended a ghost card whose file request 404s until another generation replaced it. Deleting the clip, or clearing the gallery, now clears the matching terminal record, and the page skips a record it deleted itself.
324 lines
14 KiB
Python
324 lines
14 KiB
Python
# SPDX-License-Identifier: AGPL-3.0-only
|
|
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
|
|
|
|
"""The scoped download flavour: fetch an explicit file list through the normal download
|
|
manager, so the Images/Video pages stage models the same way Chat and the Hub do.
|
|
|
|
A diffusion load reads a deliberate subset of a repo (no packaged root single, no
|
|
transformer/ shards, no fp16 twins), so a plain snapshot would pull tens of GB it never
|
|
opens. These cover the scoping, the separate job key, and the XET -> HTTP retry.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
from types import SimpleNamespace
|
|
|
|
import pytest
|
|
|
|
_BACKEND_DIR = str(Path(__file__).resolve().parent.parent)
|
|
if _BACKEND_DIR not in sys.path:
|
|
sys.path.insert(0, _BACKEND_DIR)
|
|
|
|
from fastapi import HTTPException
|
|
|
|
from hub.schemas.downloads import DownloadModelRequest
|
|
from hub.services import download_lifecycle
|
|
from hub.services.models import downloads as dl
|
|
from hub.utils.paths import is_valid_gguf_variant
|
|
|
|
|
|
FILES = ["model_index.json", "vae/diffusion_pytorch_model.safetensors"]
|
|
|
|
|
|
def _request(**over) -> DownloadModelRequest:
|
|
body = {
|
|
"repo_id": "black-forest-labs/FLUX.1-dev",
|
|
"scope_id": "diffusion",
|
|
"files": list(FILES),
|
|
"use_xet": False,
|
|
}
|
|
body.update(over)
|
|
return DownloadModelRequest(**body)
|
|
|
|
|
|
def test_scope_keys_apart_from_the_full_snapshot():
|
|
# Same repo, two jobs: the scoped one must not adopt or overwrite the full snapshot's manifest, or
|
|
# the repo would read as partial against expectations it never had.
|
|
full = dl._download_job_key("black-forest-labs/FLUX.1-dev", None)
|
|
scoped = dl._download_job_key("black-forest-labs/FLUX.1-dev", dl._scope_variant("diffusion"))
|
|
assert full != scoped
|
|
assert scoped.endswith("@diffusion")
|
|
# It rides the variant slot, so it must satisfy the same validator.
|
|
assert is_valid_gguf_variant("@diffusion")
|
|
# The "@" prefix keeps a scope out of the quant namespace: a job scoped "diffusion" and a
|
|
# (hypothetical) quant named "diffusion" stay distinct.
|
|
assert dl._download_job_key("org/m", "diffusion") != dl._download_job_key(
|
|
"org/m", dl._scope_variant("diffusion")
|
|
)
|
|
|
|
|
|
def test_scope_requires_files_and_rejects_a_variant(monkeypatch):
|
|
monkeypatch.setattr(dl, "_reject_if_load_in_flight", lambda repo_id: None)
|
|
monkeypatch.setattr(dl, "resolve_cached_repo_id_case", lambda repo, **k: repo)
|
|
|
|
with pytest.raises(Exception) as no_files:
|
|
asyncio.run(dl.download_model_response(_request(files = [])))
|
|
assert "files" in str(no_files.value)
|
|
|
|
with pytest.raises(Exception) as both:
|
|
asyncio.run(dl.download_model_response(_request(gguf_variant = "Q4_K_M")))
|
|
assert "mutually exclusive" in str(both.value)
|
|
|
|
|
|
def test_scoped_start_spawns_a_file_scoped_worker(monkeypatch):
|
|
spawned: dict = {}
|
|
|
|
monkeypatch.setattr(dl, "_reject_if_load_in_flight", lambda repo_id: None)
|
|
monkeypatch.setattr(dl, "resolve_cached_repo_id_case", lambda repo, **k: repo)
|
|
monkeypatch.setattr(dl, "scoped_file_blob_hashes", lambda *a, **k: frozenset({"h1"}))
|
|
|
|
def _fake_launch(registry, key, *, spawn, **kwargs):
|
|
spawn()
|
|
return "running"
|
|
|
|
def _fake_spawn(args, hf_token, **kwargs):
|
|
spawned["args"] = args
|
|
return object()
|
|
|
|
monkeypatch.setattr(download_lifecycle, "launch_worker", _fake_launch)
|
|
monkeypatch.setattr(download_lifecycle, "spawn_worker", _fake_spawn)
|
|
|
|
result = asyncio.run(dl.download_model_response(_request()))
|
|
assert result["accepted"] is True
|
|
scope_variant = dl._scope_variant("diffusion")
|
|
assert result["job_key"].endswith(scope_variant)
|
|
|
|
args = spawned["args"]
|
|
assert "--variant" in args and args[args.index("--variant") + 1] == scope_variant
|
|
# The file list travels in a temp JSON file, not argv: a pipeline repo lists hundreds.
|
|
manifest_path = args[args.index("--files-json") + 1]
|
|
assert json.loads(Path(manifest_path).read_text(encoding = "utf-8")) == FILES
|
|
Path(manifest_path).unlink(missing_ok = True)
|
|
|
|
|
|
def test_scoped_files_survive_into_the_registry(monkeypatch):
|
|
# The XET to HTTP retry rebuilds worker args from registry metadata alone, so without the file list
|
|
# there a retried scoped job would silently become a full snapshot.
|
|
captured: dict = {}
|
|
real_claim = dl._registry.claim
|
|
|
|
def _spy_claim(key, transport, **kwargs):
|
|
captured.update(kwargs)
|
|
return real_claim(key, transport, **kwargs)
|
|
|
|
monkeypatch.setattr(dl, "_reject_if_load_in_flight", lambda repo_id: None)
|
|
monkeypatch.setattr(dl, "resolve_cached_repo_id_case", lambda repo, **k: repo)
|
|
monkeypatch.setattr(dl, "scoped_file_blob_hashes", lambda *a, **k: frozenset())
|
|
monkeypatch.setattr(dl._registry, "claim", _spy_claim)
|
|
monkeypatch.setattr(download_lifecycle, "launch_worker", lambda *a, **k: "running")
|
|
|
|
asyncio.run(dl.download_model_response(_request()))
|
|
assert captured["scoped_files"] == FILES
|
|
|
|
metadata = dl._registry.get_job_metadata(
|
|
dl._download_job_key("black-forest-labs/FLUX.1-dev", dl._scope_variant("diffusion"))
|
|
)
|
|
assert metadata is not None and list(metadata.scoped_files) == FILES
|
|
|
|
|
|
def test_files_manifest_round_trips():
|
|
path = download_lifecycle.write_files_manifest(FILES)
|
|
try:
|
|
assert json.loads(Path(path).read_text(encoding = "utf-8")) == FILES
|
|
finally:
|
|
Path(path).unlink(missing_ok = True)
|
|
|
|
|
|
def test_a_different_file_set_is_not_adopted(monkeypatch):
|
|
# Two quants of one repo are two different downloads that share the "@diffusion" slot. Adopting
|
|
# the running one made the UI wait on the wrong file set and then load a file that had never been
|
|
# fetched, so the second request is refused while the first runs.
|
|
monkeypatch.setattr(dl, "_reject_if_load_in_flight", lambda repo_id: None)
|
|
monkeypatch.setattr(dl, "resolve_cached_repo_id_case", lambda repo, **k: repo)
|
|
monkeypatch.setattr(dl, "scoped_file_blob_hashes", lambda *a, **k: frozenset())
|
|
monkeypatch.setattr(download_lifecycle, "launch_worker", lambda *a, **k: "running")
|
|
|
|
key = dl._download_job_key("black-forest-labs/FLUX.1-dev", dl._scope_variant("diffusion"))
|
|
try:
|
|
first = asyncio.run(dl.download_model_response(_request()))
|
|
assert first["accepted"] is True
|
|
|
|
with pytest.raises(HTTPException) as other_files:
|
|
asyncio.run(
|
|
dl.download_model_response(
|
|
_request(files = ["model_index.json", "flux1-dev-Q2_K.gguf"])
|
|
)
|
|
)
|
|
assert other_files.value.status_code == 409
|
|
assert "different" in other_files.value.detail
|
|
|
|
# The same file set is still the same download: it adopts the live job as before, in any order and
|
|
# with duplicates collapsed.
|
|
same = asyncio.run(
|
|
dl.download_model_response(_request(files = [FILES[1], FILES[0], FILES[0]]))
|
|
)
|
|
assert same["accepted"] is True and same["job_key"] == key
|
|
finally:
|
|
dl._registry.set_job(key, "complete")
|
|
|
|
|
|
def test_the_http_retry_keeps_the_scoped_file_list_on_the_record(monkeypatch):
|
|
# The retry reclaims the same slot with replace_active, and that claim OVERWRITES the stored
|
|
# metadata. Dropping the file list there left the record claiming an empty scope, so the next
|
|
# identical scoped start compared [] against the real list and 409'd instead of adopting.
|
|
monkeypatch.setattr(dl, "_reject_if_load_in_flight", lambda repo_id: None)
|
|
monkeypatch.setattr(dl, "resolve_cached_repo_id_case", lambda repo, **k: repo)
|
|
monkeypatch.setattr(dl, "scoped_file_blob_hashes", lambda *a, **k: frozenset())
|
|
monkeypatch.setattr(download_lifecycle, "launch_worker", lambda *a, **k: "running")
|
|
|
|
class _Proc:
|
|
pid = 4242
|
|
|
|
def poll(self):
|
|
return None
|
|
|
|
monkeypatch.setattr(download_lifecycle, "spawn_worker", lambda *a, **k: _Proc())
|
|
monkeypatch.setattr(download_lifecycle, "register_worker", lambda *a, **k: True)
|
|
|
|
key = dl._download_job_key("black-forest-labs/FLUX.1-dev", dl._scope_variant("diffusion"))
|
|
try:
|
|
# The retry only exists for a job that started on XET.
|
|
assert asyncio.run(dl.download_model_response(_request(use_xet = True)))["accepted"] is True
|
|
|
|
retried = download_lifecycle._try_http_retry(
|
|
dl._registry,
|
|
key,
|
|
hf_token = None,
|
|
label = "FLUX.1-dev [@diffusion]",
|
|
log_prefix = "[test]",
|
|
logger = download_lifecycle.logging.getLogger("test"),
|
|
repo_type = "model",
|
|
repo_id = "black-forest-labs/FLUX.1-dev",
|
|
watch_name = "test",
|
|
)
|
|
assert retried is True
|
|
|
|
metadata = dl._registry.get_job_metadata(key)
|
|
assert metadata is not None and list(metadata.scoped_files) == FILES
|
|
# And the retried job is still adoptable by the page that asked for those files.
|
|
again = asyncio.run(dl.download_model_response(_request()))
|
|
assert again["accepted"] is True and again["job_key"] == key
|
|
finally:
|
|
dl._registry.set_job(key, "complete")
|
|
|
|
|
|
def test_scope_key_stays_derivable_from_the_scope_alone():
|
|
# The download manager builds this key client-side (it polls and cancels before any server
|
|
# round-trip tells it a key), so the scope name alone must produce it.
|
|
assert dl._scope_variant("diffusion") == "@diffusion"
|
|
assert dl._scope_variant("video") == "@video"
|
|
assert dl._scope_variant(" ") is None
|
|
assert is_valid_gguf_variant("@diffusion")
|
|
|
|
|
|
def _fake_backend(*loading: str):
|
|
return SimpleNamespace(loading_repo_ids = lambda: tuple(loading))
|
|
|
|
|
|
def test_an_images_load_staging_a_repo_blocks_a_download_of_it(monkeypatch):
|
|
# The Images and Video backends stage their snapshots through the same HF cache as the
|
|
# download worker, so starting a download for a repo one of them is already fetching puts
|
|
# two writers on the same blobs. Chat was guarded; these were not.
|
|
from core.inference import diffusion_engine_router, video as video_backend
|
|
|
|
monkeypatch.setattr(
|
|
diffusion_engine_router,
|
|
"get_active_diffusion_engine",
|
|
lambda: _fake_backend("Tongyi-MAI/Z-Image-Turbo", "unsloth/Z-Image-Turbo-GGUF"),
|
|
)
|
|
monkeypatch.setattr(video_backend, "get_video_backend", lambda: _fake_backend())
|
|
|
|
# Both the checkpoint and the companion base it is pulling are covered, case-insensitively
|
|
# (the repo id arrives from the URL exactly as the user typed it).
|
|
assert dl._load_in_flight("Tongyi-MAI/Z-Image-Turbo") is True
|
|
assert dl._load_in_flight("tongyi-mai/z-image-turbo") is True
|
|
assert dl._load_in_flight("unsloth/Z-Image-Turbo-GGUF") is True
|
|
assert dl._load_in_flight("Org/Unrelated") is False
|
|
|
|
|
|
def test_a_video_load_staging_a_repo_blocks_a_download_of_it(monkeypatch):
|
|
from core.inference import diffusion_engine_router, video as video_backend
|
|
|
|
monkeypatch.setattr(diffusion_engine_router, "get_active_diffusion_engine", _fake_backend)
|
|
monkeypatch.setattr(
|
|
video_backend,
|
|
"get_video_backend",
|
|
lambda: _fake_backend("Wan-AI/Wan2.2-TI2V-5B-Diffusers"),
|
|
)
|
|
|
|
assert dl._load_in_flight("Wan-AI/Wan2.2-TI2V-5B-Diffusers") is True
|
|
assert dl._load_in_flight("Org/Unrelated") is False
|
|
|
|
|
|
def test_an_unavailable_backend_never_blocks_a_download(monkeypatch):
|
|
# Fail open: a probe that raises must not make the repo undownloadable.
|
|
from core.inference import diffusion_engine_router, video as video_backend
|
|
|
|
def _boom():
|
|
raise RuntimeError("no engine")
|
|
|
|
monkeypatch.setattr(diffusion_engine_router, "get_active_diffusion_engine", _boom)
|
|
monkeypatch.setattr(video_backend, "get_video_backend", _boom)
|
|
|
|
assert dl._load_in_flight("Org/Anything") is False
|
|
|
|
|
|
def test_active_downloads_publish_the_scoped_file_list(monkeypatch):
|
|
"""An adopting client (a second browser profile, or a tab opened before the throttled state
|
|
write) has no local record of what a live job is fetching. Every file set of one repo shares
|
|
the "@scope" slot, so without this list it cannot tell its own transfer from a sibling
|
|
checkpoint's and would report a never-fetched file as already downloading."""
|
|
monkeypatch.setattr(dl, "_reject_if_load_in_flight", lambda repo_id: None)
|
|
monkeypatch.setattr(dl, "resolve_cached_repo_id_case", lambda repo, **k: repo)
|
|
monkeypatch.setattr(dl, "scoped_file_blob_hashes", lambda *a, **k: frozenset())
|
|
monkeypatch.setattr(download_lifecycle, "launch_worker", lambda *a, **k: "running")
|
|
|
|
key = dl._download_job_key("black-forest-labs/FLUX.1-dev", dl._scope_variant("diffusion"))
|
|
try:
|
|
asyncio.run(dl.download_model_response(_request()))
|
|
rows = download_lifecycle.active_download_refs(
|
|
dl._registry, "black-forest-labs/FLUX.1-dev", with_variant = True
|
|
)
|
|
scoped = [r for r in rows if r.variant == "@diffusion"]
|
|
assert scoped, f"no scoped row in {rows}"
|
|
assert list(scoped[0].files or []) == FILES
|
|
finally:
|
|
dl._registry.set_job(key, "complete")
|
|
|
|
|
|
def test_a_full_snapshot_download_reports_no_file_list(monkeypatch):
|
|
# Only a scoped job has a deliberate subset; a full snapshot must not claim one, or the
|
|
# client would match its whole-repo job against a scoped request.
|
|
monkeypatch.setattr(dl, "_reject_if_load_in_flight", lambda repo_id: None)
|
|
monkeypatch.setattr(dl, "resolve_cached_repo_id_case", lambda repo, **k: repo)
|
|
monkeypatch.setattr(download_lifecycle, "launch_worker", lambda *a, **k: "running")
|
|
|
|
key = dl._download_job_key("black-forest-labs/FLUX.1-dev", None)
|
|
try:
|
|
asyncio.run(
|
|
dl.download_model_response(
|
|
DownloadModelRequest(repo_id = "black-forest-labs/FLUX.1-dev", use_xet = False)
|
|
)
|
|
)
|
|
rows = download_lifecycle.active_download_refs(
|
|
dl._registry, "black-forest-labs/FLUX.1-dev", with_variant = True
|
|
)
|
|
full = [r for r in rows if r.variant is None]
|
|
assert full, f"no full-snapshot row in {rows}"
|
|
assert full[0].files is None
|
|
finally:
|
|
dl._registry.set_job(key, "complete")
|