ci: make model prefetch fail loudly instead of stalling - #119
Conversation
The audio, omni and opencode integration jobs have been failing with Gemma4 weight errors — "Key language_model.model.layers.24.self_attn.k_proj.weight not found", "Mismatched parameter ... Actual [10752, 320], expected [10752, 2560]" — and dflash with "Server failed to start within 180 seconds" while its log showed a download frozen at 0.0 MB/s. Three PRs touching disjoint files reproduced all of it identically, so it is the pipeline, not the changes under test. Three causes, all in how models reach the runner: 1. integration_matrix had no prefetch step at all. The SwiftLM server downloaded the model itself inside the test's 180 s startup window. The three failing modalities are exactly the three that load the 4.9 GB gemma-4-e4b-it-4bit; server (0.5 B), vision (2 B) and graph (no model) pass. A model downloading while the loader reads it also explains why the missing weight key differs between retries within one job. 2. The cache key was the static `mlx-model-qwen2.5-0.5b-4bit`, named for a model only one modality uses. actions/cache never overwrites an existing key, so once created it could never pick up gemma-4 — every run restored a cache that had never contained the model it needed. 3. The other jobs prefetched with `hf download <repo> || true`, which swallows every failure, and set HF_HUB_DOWNLOAD_TIMEOUT only on the test step, not on the download. An unbounded request can hang indefinitely — reproduced locally as sockets in CLOSE_WAIT with zero bytes moving for over ten minutes and no timeout to break it. Changes: - scripts/ci-download-models.sh: retries with backoff, bounds each request via HF_HUB_DOWNLOAD_TIMEOUT (resume continues from the existing .incomplete blobs), treats leftover .incomplete files as failure, and exits non-zero so a partial model never reaches the tests or the cache. - integration_matrix: per-modality model prefetch via matrix include, so each job fetches exactly what its test script loads. - Cache keys are per-modality and derived from the test script, so changing a test's model rotates the key automatically; restore-keys allows reuse. The four static keys in the other jobs are bumped to -v2 to drop any entry poisoned by a partial download. - Every prefetch now runs the script instead of `|| true`. Verified locally against all four paths: cached model succeeds, unknown repo exits 1, a planted .incomplete file is rejected rather than accepted, and multi-model invocation reports each repo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
CI results on this branch: the download hypothesis was wrong, and this PR does not turn the jobs green. It did do what it was built for — the failures are now clean and specific instead of ambiguous. Prefetch worksComplete, verified, no The real failuresaudio / omni / opencode still fail, identically: 2560 / 320 = 8, which is exactly 4-bit packing density (eight values per Correction to this PR's description. I attributed the varying missing key across retries to "a file growing underneath the loader". With the model now verified complete before the run, that explanation is dead. Swift dflash also fails cleanly now, and it is a different bug entirely: The download reaches 100%; What this PR is still worthIt does not fix CI. It fixes three real fragilities and removes the download as a variable:
Merging it means the next person reading these logs sees the actual bug instead of a download that may or may not have finished. Both remaining failures now reproduce deterministically and deserve their own issues. Happy to close this instead if you would rather fix the model-loading bugs first — the CI hardening stands on its own either way. |
Fixes the integration failures currently red on #114, #115 and #116. None of them are caused by those PRs — all three touch disjoint files and reproduce the identical four failures, so the problem is the pipeline.
What was failing
Qwen2.5-0.5B-Instruct-4bitQwen2-VL-2B-Instruct-4bitgemma-4-e4b-it-4bit(~4.9 GB)The three failing modalities are exactly the three that need the large model. Their errors:
The missing key changes between retries inside a single job —
k_proj, thenv_proj, thenk_norm. That is a file growing underneath the loader, not a model-definition bug.dflashfails differently but from the same cause; its log shows the download frozen:Byte count pinned, the total recomputed against a moving fraction, 0.0 MB/s throughout.
Root causes
1.
integration_matrixhad no prefetch step. The server downloaded the model itself inside the test's 180 s startup window. 0.5 B and 2 B make it; 4.9 GB does not.2. The cache key could never be refreshed. It was the static
mlx-model-qwen2.5-0.5b-4bit— named for a model only one modality uses.actions/cachedoes not overwrite an existing key, so once that entry was created it was frozen: the gemma-4 jobs kept restoring a cache that had never held their model.3.
hf download <repo> || trueswallowed every failure, andHF_HUB_DOWNLOAD_TIMEOUTwas set on the test step but not on the download. An unbounded request can hang forever — I reproduced exactly this locally while fetching a model for #108: sockets stuck inCLOSE_WAIT, zero bytes for more than ten minutes, no timeout to break it, process alive at 0% CPU.Changes
scripts/ci-download-models.sh— retries with backoff, bounds each request viaHF_HUB_DOWNLOAD_TIMEOUT(resume continues from existing.incompleteblobs), treats leftover.incompletefiles as failure, and exits non-zero so a partial model never reaches the tests or gets saved into the cache.integration_matrix— per-modality prefetch driven by a matrixinclude, so each job fetches exactly what its test script loads.hashFiles, so changing a test's model rotates the key automatically;restore-keysstill allows reuse. The four static keys in the other jobs are bumped to-v2to drop any entry poisoned by a partial download.|| true.Verification
The script was exercised locally on all four paths:
::error::.incompletefileci.ymlparses, and the matrix resolves to the six modalities with the right model each.What this does not fix
If the gemma-4 weight errors turn out to be a genuine model-definition mismatch rather than a truncated download, this will surface it as a clean, reproducible failure instead of one that varies per retry. That is the point — the current setup cannot tell the two apart.
I cannot run this pipeline locally, so the proof is in the next CI run on this branch.
🤖 Generated with Claude Code