Resume an interrupted install instead of re-downloading 448 MB - #22
Merged
Conversation
The OPFS cache now performs the download rather than only storing it. Transformers.js buffers a whole response before handing it to a cache, so a put-side backend never sees a failure and the .part file was always empty; owning the fetch in match() means the bytes that arrived stay on disk and the next attempt continues with a Range request. Writes go through a sync access handle, because a writable stream discards its swap file unless it closes cleanly. The ETag and total size are recorded beside the partial and compared here rather than with If-Range, which the Hub's CDN ignores. A body that ends before its declared length now fails instead of being published zero-padded. The gate screen reads .onnx_data as the test of installed, so a run that fetched only the small files is reported as partly downloaded with a Resume install button, and Remove model reclaims partials too. verify-model.mjs additionally proves the host can be resumed against and surveys every published variant of the model, failing if a smaller one appears: q4f16 text-only at 448 MiB is the smallest export of Qwen3.5-0.8B that Transformers.js can load. Co-authored-by: Sebastian <devbadya@users.noreply.github.com>
A streaming response was the wrong shape. Transformers.js also calls match() to ask whether a file exists and how large it is, discarding the body when it does, which left the OPFS write lock held by a reader that would never read: the next store of the same file failed with NoModificationAllowedError and the file went uncached. The download now completes, publishes and only then answers, so the lock never outlives it. One download per URL serves every caller, which is what the existence check and the load that follows it now share, and the transfer is retried three times from wherever it stopped before the file is handed back to Transformers.js to fetch itself. Since the library is given a file that is already on disk, install progress is reported by the cache. The worker maps those URLs onto the same file names the library reports, so the bar counts each file once. Verified in Chrome against a range-serving host: a fresh download, a transfer killed mid-body that recovers byte for byte, and one that never finishes leaving a valid resume point. Also against the real Hub through AutoTokenizer, where the second load makes no network requests. Co-authored-by: Sebastian <devbadya@users.noreply.github.com>
Co-authored-by: Sebastian <devbadya@users.noreply.github.com>
devbadya
marked this pull request as ready for review
August 24, 2026 17:56
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this is
Two things, in answer to "we don't have the right model, it's too big":
The research
onnx-community/Qwen3.5-0.8B-Text-ONNX(in use)pipeline('text-generation')onnx-community/Qwen3.5-0.8B-ONNX-OPTQwen3_5ForConditionalGeneration+ vision encoderonnx-community/Qwen3.5-0.8B-ONNXQwen3_5ForConditionalGeneration+ vision encoderqwen3_5_text.q4f16is the smallest of the five variants published in it:q4526 MiB, int8 896 MiB, fp16 1.4 GiB, fp32 2.9 GiB. INT4 is as far as ONNX Runtime Web goes.node tools/verify-model.mjsnow surveys every variant through the Hub API and fails if a smaller one appears, so this does not have to be re-derived by hand.The resume
Transformers.js reads a whole response into memory before handing it to a cache, so a
put-side backend never sees a failure — at 400 MB of 448 MB there is nothing on disk.opfsCache.matchtherefore performs the download: into.part, retried three times from wherever it stopped, published under the real name, and only then answered with the file. Upstream shipped this for Node's filesystem cache in #1715; the browser half is still open.Four details make it work rather than merely sound good:
createSyncAccessHandle(). AFileSystemWritableFileStreamdiscards its swap file unless it closes cleanly, which is why the old.partfile was always empty after a failure.ETagis compared here, not by the server. The Hub's CDN ignoresIf-Range— verified by hand: a stale validator still comes back206with the old range, which would splice two different files together. TheETagand total size are recorded beside the partial instead, and a mismatch restarts the file.Content-Lengthand zero-pads the remainder, so publishing a truncated transfer would mean silently corrupt weights.matchas an existence-and-size check and drops the body, which stranded the OPFS write lock and made the next store of that file fail withNoModificationAllowedError. Progress is therefore reported by the cache and mapped onto the library's own file names by the worker.Also:
modelCachednow means the weights are present rather than "at least one of the seven files", so a half-install no longer reports itself as installed; Remove model reclaims partials, whichlistCachedFilesused to hide from it; and the gate offers Resume install (155 MB left) with a Discard download alternative.Gate screen showing a partly downloaded model with a Resume install button
Verification
pnpm check(386 tests) andpnpm build.ETag, a host ignoringRange, a short body, one download shared by concurrent callers.Range: bytes=3145728-on the retry, and one that never finishes leaving a 3 MiB partial plus sidecar.AutoTokenizer: the file is published to OPFS at its exact size, progress is reported, and a second load makes zero network requests.node tools/verify-model.mjsagainst the live Hub, including the new resume preconditions and the variant survey.To show artifacts inline, enable in settings.