Repository navigation
hf: a large file downloads over up to 16 ranges at once, each resumed where it stopped - #1037
Conversation
… where it stopped A file was one curl transfer with --continue-at -, so a large shard came at one connection's rate, and its sha256 was read from disk again after the last byte landed. A fetch now cuts the file into ranges of at least 64 MiB, at most 16, and fills <file>.part with one curl transfer per range, each writing at its own offset; a transfer whose range is full takes the second half of the range with the most bytes left. <file>.part.ranges names what each range holds under the listing's digest and size, so an interrupted fetch resumes every range where it stopped; a sidecar of another digest or size, or one that does not tile the file, is stale. A part with no sidecar is still the file's first bytes. A transfer cut by the network, stalled for 60 s, or answered 429 or 5xx is asked again from the byte it reached, after the response's Retry-After seconds (at most 300) or a 1, 2, 4, 8, 16 s backoff, until five attempts in a row bring no byte. The digest is hashed on the calling thread as the bytes from the file's start complete, from the page cache the transfers wrote, so no second read of the file follows the download.
There was a problem hiding this comment.
Verdict: CLEAN: No blocking defect found in the changed code.
Non-blocking
⚠ Refresh the verification evidence: crates/hf/src/fetch/ranges.rs:393. The supplied runs name base 6a9680a and head b1e7fd9; this review covers 5658c58 and 192d54b.
Attach just gate-hf output for the reviewed head and FAIL-first evidence for the new contracts. Add the nvidia-smi card and driver line to the download-speed evidence required by CONTRIBUTING.md.
Checked
Duplicates: curl setup and digest state have shared owners in fetch.rs; range downloads use them.
Complexity: changed paths handle loading; no decode, prefill or kernel path changes.
Silent failure: invalid range records trigger refetch; transfer and storage failures reach the caller; cleanup failures do not publish unchecked bytes.
Tests: static mutation checks for all eight new and three adjusted tests. In ranges.rs: retain the old end at 247 → scheduler tiling assertion fails; force one worker at 420 → parallel fetch fails; omit the initial record at 427 → kill test fails; remove the failure guard at 575 → peer-position assertion fails; omit resume at 548 → resume test fails; keep the idle counter after progress at 621 → retry assertion fails. In fetch.rs: restore suffix temporaries at 1001 → collision assertion fails; use the full basename at 958 → long-name fetch fails; allow .lock at 1011 → reserved-name assertion fails; publish the marker before the rename at 545 → publication-order assertion fails; overwrite in place at 1002 → old-inode assertion fails. Tests were not executed.
Gates: no existing band, tolerance, lint level or test contract removed or relaxed.
Escape catalog: C1–C4 do not match. Bounds, concurrent splits, ordered hashing, empty files, stale records and disk-error publication paths checked.
🔨 Shingler, bloomery's review bot: approves a clean pull request and requests changes on a must-fix (how it works)
Filed outside this PR: #1038
|
The review is done and the contribution check passed, so I'm merging this. |
--hf가 큰 모델 파일을 최대 16개 range로 동시에 받고, range마다 끊긴 곳에서 이어 받으며, sha256은 받는 동안 읽어 끝난 뒤 파일을 다시 읽지 않게 합니다.Part of #565.
What and why
--hfdownloaded each file over one connection and read the whole file again for the sha256 check after the last byte. A multi-GB GGUF used one connection's speed, and a cut download lost its place in the file.Now a file of 64 MiB or more is cut into up to 16 ranges, each fetched by its own
curlwith--continue-at. A connection that finishes its range takes the back half of the widest remaining range, so no connection idles while others work. A saved record beside the part file keeps each range's position; a restarted download resumes every range where it stopped, and the record is discarded and the file fetched from the start when it does not match the listing or the part file. A cut connection, a 429 or a 5xx asks again for that range from its received byte, waiting the hub'sRetry-After(at most 300 s) or, without one, 1 s after a try that got bytes and 2, 4, 8, 16 s after tries that got none; the fifth try in a row with no bytes fails. Any other failure (404, 403, a disk error) stops every range. The sha256 is computed from the part file in order while the ranges arrive, so the check ends with the last byte. A part file with no record is still read as the front of the file, so a part left by an earlier version resumes.crates/hf/src/fetch/ranges.rs(new): the range split, the per-range resume and retry, the record file and the in-order hash.crates/hf/src/fetch.rs: the one-connection download is replaced by the ranges;curlassembly, the header file name and the hasher (Sha256,GitSha1) are each one shared function. The record and the temporary file for marker writes end in.lock.lock, a namelock_filealready keeps out of any listed file name, so a listed file can never be read as another file's record.docs/serving.md: the--hfdescription matches the new behavior.Release notes: Changed:
--hfdownloads a large file over up to 16 connections, resumes each where it stopped and checks the sha256 while downloading, so a large GGUF downloads in about half the time.Code shape
crates/hf/src/fetch/ranges.rs; the hash code thatdigestused for whole files is the one the streaming hash uses.hfcrate; no model file gained a branch.downloadand its whole-file re-read are removed.cargo fmtandcargo clippyare clean on what I touched; the lint count did not go up.Duplication found or removed (
path:line): none. The duplicate scan reported no candidates.What I ran
Base
6a9680ae320f0cfdb7a262376b825958a2225122, headb1e7fd9283fb6c227adca52216cbc2c908e3ace1.The host gate for the
hfcrate (just gate-hf):The five tests cover: four ranges start at once; each range resumes from its saved position and a record of another file is discarded; a 503 with
Retry-After: 2and a body cut in half are asked again from the received byte; an idle connection takes the back half of the widest range; and listed files named like each other's records keep their own bytes.Download time, base against head, on an RTX 5090 machine, with the real
bloomery-servebuilt from each commit (--features glm5next,clef,vision,mimo2,solar2,minimaxm3,flint,gemma-embedding2,hf,checkpoint,sm_120a). The base binary is the head tree withcrates/hf/src/fetch.rsrestored to the base. Each run fetchesggml-org/lev-GGUFlev-Q8_0.gguf(4,482,405,312 B) into a new cache, alternating base and head three times each (base, head, head, base, base, head):hf filetohf done(median of 3)The time runs from the
hf fileline to thehf doneline and includes the sha256 check. Both arms produce the same digest.Resume after a kill, same machine and binary: the head server was killed at its first progress line and started again with the same command; it asked only for what the saved ranges lacked, answered
/health, and answered a/v1/systemonerequest fromdocs/serving.md:The resumed download fetched 3,545,023,424 B, which is the file size minus the 937,381,888 B already held, and ended on the same digest. No part or record file was left.
AI help
AI 코딩 에이전트가 코드를 작성한 후 설명을 썼고, 테스트를 마쳤습니다. diff 리뷰와 PR 초안 리뷰는 제가 직접 했습니다.