-
Notifications
You must be signed in to change notification settings - Fork 10
fix(hf): load HF models offline-first to avoid Hub 429 rate-limiting #527
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: alpha
Are you sure you want to change the base?
Changes from 1 commit
f5ef5c4
c8763fe
e33c4c4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -8,7 +8,7 @@ | |||||||||||||||||||||||||||||
| import time | ||||||||||||||||||||||||||||||
| from functools import lru_cache | ||||||||||||||||||||||||||||||
| from pathlib import Path | ||||||||||||||||||||||||||||||
| from typing import Callable, Iterator, Optional, TypeVar | ||||||||||||||||||||||||||||||
| from typing import Callable, Iterable, Iterator, Optional, TypeVar | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| logger = logging.getLogger("senselab") | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
|
|
@@ -343,6 +343,122 @@ def hf_local_files_only(repo_id: str, revision: str = "main") -> bool: | |||||||||||||||||||||||||||||
| return False | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| # Env vars that switch HuggingFace libraries to local-cache-only mode. Both are | ||||||||||||||||||||||||||||||
| # honored at call time (read fresh on each download/HEAD), so toggling them | ||||||||||||||||||||||||||||||
| # around a load reliably suppresses the network revision-check that 429s under | ||||||||||||||||||||||||||||||
| # many parallel jobs. | ||||||||||||||||||||||||||||||
| _HF_OFFLINE_ENV_VARS = ("HF_HUB_OFFLINE", "TRANSFORMERS_OFFLINE") | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| # ``HF_HUB_OFFLINE`` is process-global. ``hf_offline_loading`` serializes its | ||||||||||||||||||||||||||||||
| # temporary toggle through this lock so concurrent loaders in the same process | ||||||||||||||||||||||||||||||
| # don't observe each other's env mutation. Model *loads* (not inference) thus | ||||||||||||||||||||||||||||||
| # run sequentially while the env is flipped — acceptable since loads are | ||||||||||||||||||||||||||||||
| # one-time and cached. (Mirrors the global-state-lock precedent of | ||||||||||||||||||||||||||||||
| # ``speechbrain_loading_cwd``.) | ||||||||||||||||||||||||||||||
| _hf_offline_lock = threading.RLock() | ||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using a process-global lock ( Instead of serializing the entire loading block, we can use a reference counter and a lock to manage the environment variables. This ensures that the environment variables are set to
Suggested change
|
||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| def hf_subprocess_env( | ||||||||||||||||||||||||||||||
| repo_id: "str | os.PathLike[str]", | ||||||||||||||||||||||||||||||
| revision: str = "main", | ||||||||||||||||||||||||||||||
| *, | ||||||||||||||||||||||||||||||
| also: Optional[Iterable[tuple[str, str]]] = None, | ||||||||||||||||||||||||||||||
| base_env: Optional[dict] = None, | ||||||||||||||||||||||||||||||
| ) -> dict: | ||||||||||||||||||||||||||||||
| """Build an environment dict for a subprocess that loads HF model(s) offline. | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| Ensures every referenced snapshot is present locally first (download-once | ||||||||||||||||||||||||||||||
| across processes/nodes via :func:`ensure_hf_model`), then returns a copy of | ||||||||||||||||||||||||||||||
| ``base_env`` (default ``os.environ``) with ``HF_HUB_OFFLINE`` / | ||||||||||||||||||||||||||||||
| ``TRANSFORMERS_OFFLINE`` set to ``"1"`` so the child's ``from_pretrained`` | ||||||||||||||||||||||||||||||
| skips the network revision check that rate-limits (429) under many parallel | ||||||||||||||||||||||||||||||
| jobs. | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| The offline flag is only set when **all** referenced models are cached — if | ||||||||||||||||||||||||||||||
| any is missing, the env is returned unchanged so the child can still | ||||||||||||||||||||||||||||||
| download it online. Use ``also`` for workers that load more than one model | ||||||||||||||||||||||||||||||
| (e.g. Qwen3-ASR + its companion forced aligner). | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| This is the subprocess analogue of :func:`hf_offline_loading`: in-process | ||||||||||||||||||||||||||||||
| loaders use the context manager, subprocess-venv workers (NeMo / Qwen / | ||||||||||||||||||||||||||||||
| Granite) inherit the offline flag through this env. No caller-set env vars | ||||||||||||||||||||||||||||||
| are required. | ||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||
| env = dict(os.environ if base_env is None else base_env) | ||||||||||||||||||||||||||||||
| repos = [(os.fspath(repo_id), revision), *(also or [])] | ||||||||||||||||||||||||||||||
| if all(hf_local_files_only(rid, rev) for rid, rev in repos): | ||||||||||||||||||||||||||||||
| for var in _HF_OFFLINE_ENV_VARS: | ||||||||||||||||||||||||||||||
| env[var] = "1" | ||||||||||||||||||||||||||||||
| return env | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| @contextlib.contextmanager | ||||||||||||||||||||||||||||||
| def hf_offline_loading(repo_id: "str | os.PathLike[str]", revision: str = "main") -> Iterator[bool]: | ||||||||||||||||||||||||||||||
| """Force local-cache-only HuggingFace loading for the duration of the block. | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| Ensures the snapshot is present first (download-once across processes/nodes | ||||||||||||||||||||||||||||||
| via :func:`ensure_hf_model`: cross-process heartbeat lock + retry-on-429), | ||||||||||||||||||||||||||||||
| then sets ``HF_HUB_OFFLINE`` / ``TRANSFORMERS_OFFLINE`` so ``from_pretrained`` | ||||||||||||||||||||||||||||||
| / ``pipeline`` make **no** network revision-check calls during the load — | ||||||||||||||||||||||||||||||
| the source of 429 storms when many jobs load the same model at once. | ||||||||||||||||||||||||||||||
| Requires **no** caller configuration. | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| If the model cannot be cached (e.g. genuinely offline and never downloaded), | ||||||||||||||||||||||||||||||
| the env is left untouched and the block runs normally (online), so behavior | ||||||||||||||||||||||||||||||
| degrades gracefully rather than failing. | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| Yields: | ||||||||||||||||||||||||||||||
| ``True`` if offline mode was engaged (model is cached), else ``False``. | ||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||
| repo_id = os.fspath(repo_id) | ||||||||||||||||||||||||||||||
| if not hf_local_files_only(repo_id, revision): | ||||||||||||||||||||||||||||||
| yield False | ||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||
| with _hf_offline_lock: | ||||||||||||||||||||||||||||||
| saved = {var: os.environ.get(var) for var in _HF_OFFLINE_ENV_VARS} | ||||||||||||||||||||||||||||||
| for var in _HF_OFFLINE_ENV_VARS: | ||||||||||||||||||||||||||||||
| os.environ[var] = "1" | ||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||
| yield True | ||||||||||||||||||||||||||||||
| finally: | ||||||||||||||||||||||||||||||
| for var, prev in saved.items(): | ||||||||||||||||||||||||||||||
| if prev is None: | ||||||||||||||||||||||||||||||
| os.environ.pop(var, None) | ||||||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||||||
| os.environ[var] = prev | ||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Update the context manager to use the reference counter and lock instead of holding the global lock for the entire duration of the block. This allows multiple threads to load models concurrently while safely managing the process-global environment variables. global _hf_offline_counter, _hf_offline_saved_env
with _hf_offline_counter_lock:
if _hf_offline_counter == 0:
_hf_offline_saved_env = {var: os.environ.get(var) for var in _HF_OFFLINE_ENV_VARS}
for var in _HF_OFFLINE_ENV_VARS:
os.environ[var] = "1"
_hf_offline_counter += 1
try:
yield True
finally:
with _hf_offline_counter_lock:
_hf_offline_counter -= 1
if _hf_offline_counter == 0:
for var, prev in _hf_offline_saved_env.items():
if prev is None:
os.environ.pop(var, None)
else:
os.environ[var] = prev
_hf_offline_saved_env.clear() |
||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| def load_hf_resilient( | ||||||||||||||||||||||||||||||
| loader: Callable[..., _T], | ||||||||||||||||||||||||||||||
| *args: object, | ||||||||||||||||||||||||||||||
| repo_id: "str | os.PathLike[str]", | ||||||||||||||||||||||||||||||
| revision: str = "main", | ||||||||||||||||||||||||||||||
| **kwargs: object, | ||||||||||||||||||||||||||||||
| ) -> _T: | ||||||||||||||||||||||||||||||
| """Load an HF model resiliently: cache-once, load local-only, retry on 429. | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| Loader-agnostic wrapper for any in-process model constructor | ||||||||||||||||||||||||||||||
| (``transformers.pipeline``, ``*.from_pretrained``, SpeechBrain | ||||||||||||||||||||||||||||||
| ``from_hparams``, ...). It: | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| 1. Ensures the snapshot is present exactly once across processes/nodes | ||||||||||||||||||||||||||||||
| (:func:`ensure_hf_model` — cross-process heartbeat lock + retry). | ||||||||||||||||||||||||||||||
| 2. Runs ``loader(*args, **kwargs)`` with HF libs in local-cache-only mode | ||||||||||||||||||||||||||||||
| (:func:`hf_offline_loading`) so no network revision-check fires. | ||||||||||||||||||||||||||||||
| 3. Retries the load on any residual transient error (5xx / 429 / timeout). | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| ``repo_id`` / ``revision`` identify the model for the cache step and are | ||||||||||||||||||||||||||||||
| **not** forwarded to ``loader`` — pass the loader's own model/revision | ||||||||||||||||||||||||||||||
| arguments via ``*args`` / ``**kwargs`` as usual. | ||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| def _call() -> _T: | ||||||||||||||||||||||||||||||
| with hf_offline_loading(repo_id, revision): | ||||||||||||||||||||||||||||||
| return loader(*args, **kwargs) | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| return retry_on_transient_error(_call) | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| def _cached_error(cached: dict) -> Exception: | ||||||||||||||||||||||||||||||
| """Reconstruct an exception from a cached error result.""" | ||||||||||||||||||||||||||||||
| from huggingface_hub.errors import RepositoryNotFoundError, RevisionNotFoundError | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
revisionparameter is not passed toAutoProcessor.from_pretrainedandAutoModelForSpeechSeq2Seq.from_pretrained. If a custom revision is specified inmodel,hf_offline_loadingwill ensure that specific revision is cached and then engage offline mode. However, the subsequent loading calls will attempt to load the default revision ("main"), which will fail in offline mode (cache miss) or load the wrong version of the model.Pass
revision=revisionto both loading calls to ensure the correct model version is loaded.