Skip to content

feat(hub): resume interrupted model downloads via HTTP Range (Node)#1715

Draft
renezander030 wants to merge 1 commit into
huggingface:mainfrom
renezander030:feat/resumable-downloads
Draft

feat(hub): resume interrupted model downloads via HTTP Range (Node)#1715
renezander030 wants to merge 1 commit into
huggingface:mainfrom
renezander030:feat/resumable-downloads

Conversation

@renezander030

Copy link
Copy Markdown

Follow-up to the discussion in #1220 (thanks @emojiiii for handing it over, and @sroussey for the Xet/CDC pointer).

This is step 1: HTTP Range-resume for the Node file-cache path, which is where large model weights are streamed to disk. It ships resume for how downloads work today, with no new transport dependency. Xet-native chunk caching (via the huggingface.js download path) is the natural follow-up and is intentionally out of scope here.

What changes

  • FileCache streams large downloads to a deterministic <key>.incomplete file plus a small sidecar ({ etag, total }), and keeps the partial on failure instead of deleting it.
    • getResumeInfo(request) reports { size, etag, total } when a consistent partial exists.
    • put() branches on the response status: 206 appends to the partial (offset/total parsed from Content-Range); 200 restarts cleanly (truncates any stale partial). A truncated body (loaded < Content-Length) is rejected and the partial retained for the next attempt.
    • A per-key lock preserves the previous concurrent-writer guarantee: if the partial is already held by another writer, put() falls back to the original unique-temp path (correct, non-resumable, never corrupts the shared partial). Stale locks are stolen after a TTL.
  • hub.js only sends Range/If-Range on the streaming path (IS_NODE_ENV && return_path), since a 206 on the buffered path would yield only the trailing bytes. It now accepts 206 alongside 200 and caches both. If-Range: <etag> makes the server fall back to a full 200 if the file changed upstream.
  • CacheInterface gains an optional getResumeInfo so custom caches can opt in.

Scope / notes

  • Node-only by design. The browser Cache API cannot store partials; OPFS/Xet is the follow-up (step 2).
  • Incremental sha256 integrity verification is deliberately left for a follow-up so this PR stays focused on the resume mechanics.

Tests

New tests/utils/file_cache.test.js covers: no-partial, full 200 (no leftovers), truncated body (partial + sidecar retained, getResumeInfo correct), 206 append to completion, and 200 restart over a stale partial. Existing cache/custom_cache suites still pass; typecheck, prettier, and build are green.

Closes #1220

Opening as a draft to get maintainer direction on the Xet question in #1220 before polishing. Happy to adjust.

FileCache now streams large downloads to a deterministic <key>.incomplete
file plus an etag/size sidecar, and keeps the partial on failure instead
of deleting it. On the next attempt the Node streaming path (return_path)
asks getResumeInfo for the offset and sends Range + If-Range: the server
appends via 206, or restarts cleanly via 200 if the file changed upstream.

A per-key lock preserves the previous concurrent-writer guarantee by
falling back to the legacy unique-temp path when the partial is held.
Adds FileCache unit tests for resume, truncation, and restart.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support resuming model downloads from where they were interrupted.

1 participant