Skip to content

hf-cache: stop HF Hub 429s under parallel batch (resolve-once + SHA-pin) - #539

Merged
wilke0818 merged 14 commits into
alphafrom
fix/hf-cache-sha-pin
Jul 28, 2026
Merged

hf-cache: stop HF Hub 429s under parallel batch (resolve-once + SHA-pin)#539
wilke0818 merged 14 commits into
alphafrom
fix/hf-cache-sha-pin

Conversation

@wilke0818

@wilke0818 wilke0818 commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator

Need

Under a parallel batch (e.g. a SLURM array loading models across hundreds of
processes at once), every HF model load issues a per-call Hub HEAD/revision
check, and the Hub rate-limits the burst (HTTP 429). This surfaced as ASR
backends intermittently "missing" in analyze_audio runs.

Validated at scale: with this fix the ASR/alignment lane ran ~16k audios across
a 400-subject batch with zero 429s (Qwen — the original offender — plus
Canary and Whisper all clean).

Plan

Resolve a model's ref to an immutable commit SHA once (download-once via the
existing cross-process heartbeat lock in ensure_hf_model), then pin every load
to that SHA so cached files load with no Hub HEAD:

  • resolve_model(repo_id, revision) -> (sha, snapshot_path)
  • load_hf_resilient(loader, ..., repo_id, revision) — in-process loaders that
    accept revision (injects revision=<sha>; no local_files_only, which
    breaks transformers.pipeline).
  • hf_subprocess_env(repo_id, revision, ...) — subprocess-venv backends: stage
    once, run the worker with HF_HUB_OFFLINE=1.

This PR routes every HF model-load site on alpha through those helpers
(merges current alpha, which now includes #536 scene-quality/ASR + #538
cached-inference refactor):

  • In-process (SHA-pin / snapshot-path): AST scene classification, Granite
    ASR, TTS, text embeddings (HF + sentence-transformers), SSL, SER weights,
    SpeechBrain speaker embeddings (ECAPA/ResNet) + SepFormer enhancement, pyannote
    diarization + VAD, MMS forced aligner, Whisper pipeline, pyannote
    segmentation frame-posteriors, the adaptive workflow's SepFormer + ASR loads.
  • Subprocess (hf_subprocess_env): Qwen, Canary, NeMo, Sortformer
    diarization, continuous-SER, GLiNER PII, Brouhaha scene-quality, CrisperWhisper.

Plus a guard test (tests/utils/hf_load_coverage_test.py) that statically
inventories every HF load site and fails when a new one is added to a file
not on the reviewed allowlist — forcing future HF functionality to be routed
through the helpers and consciously reviewed. (It already caught otherwise-missed
gaps: Sortformer and the adaptive workflow's loads.)

Notes:

  • SSL embeddings SHA-pins only on the default HF cache; a custom cache_dir
    keeps prior behavior (resolve_model stages into the default cache).
  • SpeechBrain/pyannote pipelines that internally pull sub-models pin only the
    named repo; sub-model loads are a separate follow-up.

🤖 Generated with Claude Code

Version

Published prerelease version: 1.3.1-alpha.38

Changelog

🐛 Bug Fix

  • hf-cache: stop HF Hub 429s under parallel batch (resolve-once + SHA-pin) #539 (@wilke0818)
  • fix(adaptive): guard None history in convergence improvement delta #541 (@wilke0818)
  • refactor(cache): cached-inference layer out of analyze_audio.py (T051, parts 1-2) #538 (@satra)
  • Scene-aware presence/quality axis + ASR transcription overhaul (CrisperWhisper 2.0, Qwen) #536 (@satra @claude)
  • feat(canary): split long audio at pauses to avoid the ~40s truncation #534 (@wilke0818)
  • ScriptLine: accept empty text as "model produced no transcript" signal #520 (@wilke0818)
  • Feat/pii subprocess venv #519 (@wilke0818)
  • subprocess_venv: route torch/torchaudio via CUDA-aware PyTorch index #516 (@satra @wilke0818)
  • subprocess_venv: two-stage install to fix torch/torchaudio CUDA-tag s… #518 (@wilke0818)
  • Fix/ser wav2vec2 regression head #511 (@satra @wilke0818 @claude)
  • comparison & uncertainty stage for analyze_audio.py #512 (@satra)
  • audio analysis script + multilingual MMS aligner + 4 new ASR backends #510 (@satra)

Authors: 3

wilke0818 and others added 11 commits July 23, 2026 16:41
…ion checks

The 429s come from from_pretrained/pipeline issuing a Hub HEAD/revision check on
every call even for a cached, current model. Fix (MVP core):
- _get_cached_commit_hash now resolves the real immutable commit SHA (refs/<ref>
  pointer or snapshot dir), not the mutable branch name; a full-SHA revision is
  passed through.
- resolve_model(repo_id, revision) -> (sha, snapshot_path): download-once via
  ensure_hf_model, returns the SHA + local snapshot dir.
- load_hf_resilient(loader, ..., repo_id, revision) resolves then calls the loader
  with revision=<sha>, local_files_only=True injected (pass_revision), so
  huggingface_hub's commit-hash shortcut returns cached files with zero network;
  pass_revision=False for loaders that take a local path instead.

Tests-first (all pass): real-SHA resolution, SHA passthrough, resolve_model
returns sha+path, load wrapper pins sha+local_files_only, and zero HfApi.model_info
calls when cached. Backend migrations follow.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The transformers ASR pipeline (where Qwen/Whisper "missing" 429s originated) now
loads via load_hf_resilient: the mutable revision is dropped from the pipeline()
kwargs, and the resolved commit SHA + local_files_only=True are injected, so a
cached model triggers no per-call Hub version check.

Behavior test (tests-first): asserts _get_hf_asr_pipeline routes through
load_hf_resilient with repo_id/revision and never passes a mutable revision to
pipeline() directly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…el loads

Subprocess-venv ASR workers (Qwen, Canary-Qwen, NeMo, Granite) load HF models in
a fresh child process, so — unlike in-process — flipping HF_HUB_OFFLINE actually
works there. hf_subprocess_env(repo_id, revision, also=..., base_env=...) stages
each referenced model once (download-once via resolve_model) and returns the child
env with HF_HUB_OFFLINE/TRANSFORMERS_OFFLINE=1 so the worker's from_pretrained does
no per-call Hub version check (the 429 source). Falls back to an unchanged env when
a model can't be staged, so cold starts still download online.

Tests-first: offline-when-all-cached, unchanged-when-uncacheable, and companion
(`also`) staging (e.g. the Qwen forced aligner).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…fix its 429)

Qwen/Qwen3-ASR-1.7B (+ its Qwen3-ForcedAligner companion) is the backend that
429'd under parallel analyze_audio batches: the worker loaded both HF models with
a plain _clean_subprocess_env(), so every job HEADed the Hub. Now the parent builds
the child env via hf_subprocess_env(model, also=[(aligner, "main")]): both models
are staged once and the child runs offline, so from_pretrained loads from cache
with no Hub version check.

Behavior test (tests-first): transcribe_with_qwen builds the worker env via
hf_subprocess_env staging the ASR model + the forced-aligner companion.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…o_exists)

check_hf_repo_exists' model branch caught every exception and returned False, so a
transient 429/network error during verification made a real model report as "not
found" (the confusing "Qwen missing" symptom). Now it matches the function's own
non-model branch: only Repository/RevisionNotFoundError -> False (genuinely absent);
GatedRepoError and transient errors are logged and re-raised so they surface as
real, actionable errors instead of a false "missing".

Tests-first: genuine-not-found -> False; transient -> logged + re-raised;
gated -> re-raised.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nned loads

The auto-align stage loads facebook/mms-1b-all (and any per-language Wav2Vec2
aligner) in-process for every timestamp-less ASR output, so each load HEADed the
Hub. Route _load_mms_aligner and the per-language loader in align_transcriptions
through load_hf_resilient: resolve ref->SHA once and load revision=<sha>,
local_files_only=True, so cached aligners make no per-call Hub version check.

Behavior test (tests-first): _load_mms_aligner loads processor + model via
load_hf_resilient pinned to MMS_MODEL_ID.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…eline)

Regression found in the 400-subject stress run: load_hf_resilient injected
local_files_only=True, but transformers.pipeline routes unknown kwargs into the
model's generate params -> "ValueError: model_kwargs are not used by the model:
['local_files_only']" at inference, so Whisper failed on every lexical audio.
A full-SHA revision alone triggers huggingface_hub's commit-hash shortcut (no
HEAD), so local_files_only was never needed. Inject only revision=<sha>.

Test updated to assert revision=<sha> is injected and local_files_only is NOT.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…(Groups A–C)

Extend the HF-429 fix beyond the ASR/alignment lane to the other model-load
sites, reusing the existing heartbeat-lock + version-resolution helpers
(resolve_model / load_hf_resilient / hf_subprocess_env). No new mechanism.

Group A (loaders that accept `revision` -> pin revision=<sha>):
- classification/huggingface.py (AST scene classification)
- speech_to_text/granite.py (Granite ASR)
- text_to_speech/huggingface.py (TTS pipeline)
- text/embeddings_extraction/{huggingface,sentence_transformers}.py
- ssl_embeddings/self_supervised_features.py (transformers feat-extractor + model)
- classification/speech_emotion_recognition/api.py (EmotionModel + feat-extractor)

Group B (SpeechBrain/pyannote -> stage under lock, load from immutable snapshot
path / pin pyannote revision; also fixes latent `revision=f"{None}"`):
- speaker_embeddings/speechbrain.py (ECAPA/ResNet)
- speech_enhancement/speechbrain.py (SepFormer)
- speaker_diarization/pyannote.py + voice_activity_detection/pyannote_vad.py
- ssl_embeddings + SER SpeechBrain loaders

Group C (subprocess -> hf_subprocess_env stages model + sets HF_HUB_OFFLINE):
- workflows/audio_analysis/pii_subprocess.py (GLiNER, only when gliner requested)
- SER continuous-emotion subprocess worker

Validated: SpeechBrain snapshot-path load (ECAPA -> 192-D) and transformers
revision=sha load (MiniLM) work end-to-end; ruff + 12/12 module imports + the
dependencies helper tests pass.

Residual (not migrated, tracked separately): SER metadata-introspection calls
(_load_config_cached AutoConfig/hf_hub_download; _peek_head_final_layer
HfApi.list_repo_files / get_safetensors_metadata) — online API peeks that don't
fit the model-load helper pattern; already memoized + per-(path,rev) locked and
not on the default analyze_audio path. #536-only files (frame_posteriors,
brouhaha, crisperwhisper) handled on the stress branch.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…s_env

Close the ASR-lane gap on the alpha branch: canary_qwen.py and nemo.py are
alpha files but were only fixed on the stress branch, not here. Stage the model
once (cross-process heartbeat lock) + run the worker offline so SALM/ASRModel
from_pretrained make no per-call Hub version check — the 429 source under batch.
Same proven pattern as qwen.py.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- speaker_diarization/nvidia.py: stage the Sortformer model + run the worker
  offline via hf_subprocess_env (was a raw subprocess load — surfaced by the
  new guard test below).
- tests/utils/hf_load_coverage_test.py: static inventory guard. Walks
  src/senselab, finds every in-process HF loader (from_pretrained/from_hparams/
  pipeline/snapshot_download/hf_hub_download/SentenceTransformer/Inference) and
  every subprocess worker that loads a model, and FAILS when one appears in a
  file not on the reviewed allowlist. Forces any newly-added HF functionality to
  be routed through resolve_model / load_hf_resilient / hf_subprocess_env and
  consciously allowlisted. Also asserts allowlisted files still reference a
  helper (catches silent regressions) and that the allowlists carry no stale
  entries. Verified it fails on a planted raw pipeline() load.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…mits)

- ssl_embeddings/self_supervised_features.py: only SHA-pin when cache_dir is
  None. resolve_model stages into the default HF cache, so a custom cache_dir
  would miss it and re-download (double download); the custom-cache_dir path now
  keeps prior ref-based behavior instead of staging into the wrong place. The
  common cache_dir=None path keeps the full fix.
- tests/utils/hf_load_coverage_test.py: document the guard's two by-design
  limitations (file-granular, not per-load inside an already-reviewed file;
  subprocess detection assumes worker string + launch share a module).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@wilke0818
wilke0818 marked this pull request as ready for review July 27, 2026 20:27
wilke0818 and others added 2 commits July 27, 2026 20:11
#536 (scene-quality/utterance) + #538 (cached-inference refactor) merged to
alpha, so their model-load sites now live here too. Route them through the
helpers (cherry-picked from the hf-cache-on-536 integration branch), guard-driven:

In-process (SHA-pin / snapshot-path):
- voice_activity_detection/frame_posteriors.py (use the SHA ensure_hf_model returns)
- workflows/audio_analysis/adaptive/audio_io.py (SepFormer via snapshot path)
- workflows/audio_analysis/adaptive/backends.py (ASR pipeline via load_hf_resilient)

Subprocess (hf_subprocess_env):
- scene_quality/brouhaha.py (gated; token forwarded, online fallback preserved)
- speech_to_text/crisperwhisper.py (online fallback preserved)

Guard-test allowlist updated to cover all five; PR #1 now covers every HF load
site on alpha.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… patching)

Three unrelated CI failures on the merged branch:

1. load_hf_resilient's named `token` param shadowed the loader's token: the HF
   ASR pipeline put `token` in pipeline_kwargs, it bound to the helper's param
   (used only for resolve_model) and never reached `pipeline()`. Forward `token`
   to the loader too (setdefault). Fixes test_hf_asr_pipeline_forwards_hf_token.

2. test_check_hf_repo_exists_false raised a generic Exception and expected False,
   but check_hf_repo_exists now (correctly) re-raises transient errors and only
   returns False for a definitive RepositoryNotFoundError — update the test to
   raise that (built with a mock response, since it's an HfHubHTTPError).

3. dependencies_test.py monkeypatched string targets
   ("senselab.utils.dependencies.X"); under the full suite `senselab.utils` isn't
   resolvable as an attribute mid-run, so pytest's resolver raised AttributeError.
   Patch via the imported module object (`monkeypatch.setattr(dep, "X", ...)`),
   which needs no attribute walk. Fixes all 7 dependencies_test failures.

Verified: the 8 formerly-failing tests + full utils dir + speech_to_text + guard
pass locally; ruff clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@wilke0818
wilke0818 merged commit 22da0e4 into alpha Jul 28, 2026
9 checks passed
github-actions Bot added a commit that referenced this pull request Jul 28, 2026
@wilke0818 wilke0818 mentioned this pull request Jul 29, 2026
6 tasks
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.

1 participant