* feat(downloads): Xet fast path + accurate progress (FDL W0–W2)
Make model downloads fast and show accurate downloaded/remaining/speed.
Research confirmed hf-xet already implements the IDM/uGet technique
(content-defined chunking, parallel byte-range gets, dedup, resume), and
the spike found all 25 catalog repos are Xet-backed — so the win is
driving Xet well + accurate progress, not a custom downloader.
W1 — maximize + guarantee Xet:
- pin huggingface_hub>=1.7 + hf-xet>=1.1 (was transitive); no hf_transfer
- drive snapshot_download with explicit tqdm_class + max_workers + endpoint
- opt-in HF_XET_HIGH_PERFORMANCE / HDD sequential-write knobs (default off)
- /system/info reports fast_download {xet_enabled, xet_version, high_perf}
W2 — accurate progress:
- dry_run preflight -> install_plan event (exact total/cached/remaining)
- utils/download_aggregator.py: one overall bar; byte bars (by id) vs the
"Fetching N files" count bar; windowed rate; emits one 'aggregate' event
- frontend overall bar (speed/remaining/ETA), cached-skip, ⚡ fast badge
Known limit (verified live): under Xet+hf_hub 1.7.2 per-file byte bars
never advance/close via tqdm, so mid-download the bar is file-granular and
bytes flush to the exact total on completion. Classic-LFS/mirror repos get
true byte progress (W4).
Drive-by: download.py used os.walk without importing os (latent NameError
in _validate_snapshot_has_weights on every install) — fixed.
Tests: tests/backend/setup/test_download_preflight.py (10). Spike + plan
under .planning/quick/260613-fdl-fast-model-downloads/.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(downloads): opt-in mirror + cancel + docs (FDL W4)
- mirror (FDL-10): snapshot_download(endpoint=) honours prefs hf_endpoint /
env HF_ENDPOINT on preflight + download (per-call, no process-wide env).
Documented as the classic-LFS path (no Xet) for restricted networks.
- cancel (FDL-11): POST /models/install/cancel {repo_id} stops further
retries at the next boundary, emits install_cancelled, clears the cooldown
(cancel is intent, not failure). Frontend treats it as a terminator.
- docs (FDL-12): docs/downloading-models.md (Xet fast path, progress
semantics + byte-speed limitation, opt-in tuning, mirror, cancel,
troubleshooting) + README pointer. Docs-sync rule satisfied.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(planning): model-management v2 cleanup plan (mm2)
GSD plan for cleaning the model-management subsystem: registry unload-on-
switch + per-engine unload() (fixes VRAM leak), model_lifecycle facade,
unified idle/timeout config, bounded cooldowns, sidecar VRAM self-report,
cache-fallback logging. Planning artifact only — no code.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(downloads): reconcile with main's HF_HUB_DISABLE_XET; honest status
Rebasing onto main surfaced that main forces HF_HUB_DISABLE_XET=1 (classic
LFS) because Xet progress bypasses the tqdm hook — the same limitation found
here. Reconcile instead of fight:
- /system/info fast_download now reports runtime truth: xet_installed +
xet_active (installed AND not HF_HUB_DISABLE_XET) + xet_enabled alias. The
⚡ badge only shows when Xet actually runs; startup log says
"downloads: Xet disabled → legacy LFS".
- complete(): clear the rate window before the final flush so crediting the
full size in one step can't emit an absurd instantaneous rate.
- docs/downloading-models.md rewritten: default is legacy LFS for accurate
progress; Xet is opt-in via HF_HUB_DISABLE_XET=0. hf-xet pin stays (ready
for a future Xet progress hook).
W2 (preflight total/remaining + aggregate bar + exact completion) is the
value on either path; W1's "maximize Xet" is dormant by main's design.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(downloads): opt-in segmented multi-connection accelerator (FDL W3)
Since main forces Xet off (HF_HUB_DISABLE_XET=1), the default path is
single-stream legacy LFS — so a segmented downloader is the way to get BOTH
parallel speed and live byte progress.
- services/segmented_download.py: async multi-connection Range downloader for
one file — parallel byte-ranges, resume (.part + manifest), per-segment
short-read truncation guard, optional sha256/etag verify, cancel, and a
single-stream fallback when the server won't range. Auth-safe: the HF
Authorization header is sent only to huggingface.co/hf.co and never
forwarded to a CDN host on redirect (unit-tested).
- dispatch (download.py): opt-in via prefs segmented_downloader / env
OMNIVOICE_SEGMENTED_DOWNLOAD (default off). When on and Xet inactive,
fetches each file into the HF cache mirroring hf_hub_download (blobs +
snapshot symlinks + refs/main), feeding real bytes to the aggregator. Any
failure falls back to snapshot_download — never breaks a correct install.
- fix: complete() was adding a full total on top of accumulated segmented
bytes (2x); now replaces byte bars so the sum is exactly total.
Verified live (accelerator on): real byte progress to ~16.6 MB/s, final
bytes==total, /models installed=True, delete frees correctly.
Tests: test_segmented_download.py (7) + aggregator double-count regression.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(downloads): relocate FDL tests to top-level; loop-isolate segmented test
CI runs the full suite, which exposed a pre-existing test-isolation leak:
several tests/backend/** fixtures purge core.*/services.* from sys.modules
under a temp OMNIVOICE_DATA_DIR and never restore, leaving core.config/core.db
bound to a dead temp dir. It only bites when collection order puts a purging
test ahead of a real-DB reader (test_longform_jobs). Adding tests under
tests/backend/setup/ reordered collection and tripped it.
Fix without touching the shared (fragile) fixtures or risking class-identity
breakage from a blanket sys.modules restore:
- move the two FDL test files to top-level tests/ (tests/test_fdl_*.py) so
tests/backend/** collection order is identical to main — longform passes.
- rewrite the segmented test to run each case under asyncio.run() (fresh loop)
instead of asyncio.get_event_loop(), which an earlier async test can leave
closed in the full suite.
Full suite green locally: 1364 passed, 0 failed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
111 lines
4.0 KiB
Python
111 lines
4.0 KiB
Python
"""FDL-08: segmented (multi-connection) downloader — correctness + auth safety.
|
|
|
|
Each test runs a single self-contained coroutine via ``asyncio.run`` (fresh
|
|
event loop per call). Do NOT use ``asyncio.get_event_loop()`` here: in the full
|
|
suite an earlier async test can leave the global loop closed, which would make
|
|
these RuntimeError even though they pass standalone.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import os
|
|
|
|
import httpx
|
|
import pytest
|
|
|
|
from services.segmented_download import segmented_download, DownloadCancelled
|
|
|
|
|
|
PAYLOAD = bytes((i % 256) for i in range(1_000_000)) # 1 MB deterministic body
|
|
|
|
|
|
def _ranged_handler(payload=PAYLOAD, *, accept_ranges=True, record=None):
|
|
"""A mock origin that honours Range requests over `payload`."""
|
|
def handler(request: httpx.Request) -> httpx.Response:
|
|
if record is not None:
|
|
record.append(request)
|
|
if request.method == "HEAD":
|
|
h = {"content-length": str(len(payload))}
|
|
if accept_ranges:
|
|
h["accept-ranges"] = "bytes"
|
|
return httpx.Response(200, headers=h)
|
|
rng = request.headers.get("range")
|
|
if rng and accept_ranges:
|
|
lo, hi = rng.replace("bytes=", "").split("-")
|
|
lo, hi = int(lo), int(hi)
|
|
return httpx.Response(206, content=payload[lo:hi + 1])
|
|
return httpx.Response(200, content=payload)
|
|
return handler
|
|
|
|
|
|
def _client(handler):
|
|
return httpx.AsyncClient(transport=httpx.MockTransport(handler), follow_redirects=False)
|
|
|
|
|
|
def _download(handler, dest, **kw):
|
|
"""Run one segmented_download against a mock origin in a fresh event loop."""
|
|
url = kw.pop("url", "https://cdn.example.com/f.bin")
|
|
|
|
async def _do():
|
|
async with _client(handler) as client:
|
|
return await segmented_download(url, dest, client=client, **kw)
|
|
|
|
return asyncio.run(_do())
|
|
|
|
|
|
def test_parallel_ranges_reassemble_exactly(tmp_path):
|
|
dest = str(tmp_path / "model.bin")
|
|
_download(_ranged_handler(), dest, expected_size=len(PAYLOAD), num_connections=8)
|
|
with open(dest, "rb") as f:
|
|
assert f.read() == PAYLOAD
|
|
assert not os.path.exists(dest + ".part")
|
|
assert not os.path.exists(dest + ".part.done")
|
|
|
|
|
|
def test_single_stream_fallback_when_no_range(tmp_path):
|
|
dest = str(tmp_path / "f.bin")
|
|
_download(_ranged_handler(accept_ranges=False), dest, expected_size=len(PAYLOAD))
|
|
with open(dest, "rb") as f:
|
|
assert f.read() == PAYLOAD
|
|
|
|
|
|
def test_auth_header_never_sent_to_cdn_host(tmp_path):
|
|
record = []
|
|
dest = str(tmp_path / "f.bin")
|
|
_download(_ranged_handler(record=record), dest,
|
|
url="https://cdn.cloudfront.net/blob", # NOT a huggingface.co host
|
|
token="hf_secrettoken", expected_size=len(PAYLOAD))
|
|
assert record
|
|
assert all("authorization" not in {k.lower() for k in r.headers} for r in record), \
|
|
"Authorization must never be sent to a non-huggingface.co host"
|
|
|
|
|
|
def test_auth_header_sent_to_hf_host(tmp_path):
|
|
record = []
|
|
dest = str(tmp_path / "f.bin")
|
|
_download(_ranged_handler(record=record), dest,
|
|
url="https://huggingface.co/api/x/resolve/main/f",
|
|
token="hf_tok", expected_size=len(PAYLOAD))
|
|
assert any(r.headers.get("authorization") == "Bearer hf_tok" for r in record)
|
|
|
|
|
|
def test_size_mismatch_raises(tmp_path):
|
|
dest = str(tmp_path / "f.bin")
|
|
with pytest.raises(ValueError):
|
|
_download(_ranged_handler(), dest, expected_size=len(PAYLOAD) + 999)
|
|
assert not os.path.exists(dest)
|
|
|
|
|
|
def test_cancel_raises_and_leaves_no_commit(tmp_path):
|
|
dest = str(tmp_path / "f.bin")
|
|
with pytest.raises(DownloadCancelled):
|
|
_download(_ranged_handler(), dest, expected_size=len(PAYLOAD), cancel_check=lambda: True)
|
|
assert not os.path.exists(dest)
|
|
|
|
|
|
def test_on_bytes_reports_total(tmp_path):
|
|
dest = str(tmp_path / "f.bin")
|
|
seen = []
|
|
_download(_ranged_handler(), dest, expected_size=len(PAYLOAD), on_bytes=lambda d: seen.append(d))
|
|
assert sum(seen) == len(PAYLOAD)
|