cached-hub 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cached_hub-0.1.0/.github/workflows/publish.yml +50 -0
- cached_hub-0.1.0/.github/workflows/pytest.yml +47 -0
- cached_hub-0.1.0/.gitignore +16 -0
- cached_hub-0.1.0/.pre-commit-config.yaml +36 -0
- cached_hub-0.1.0/.python-version +1 -0
- cached_hub-0.1.0/LICENSE +21 -0
- cached_hub-0.1.0/PKG-INFO +193 -0
- cached_hub-0.1.0/README.md +157 -0
- cached_hub-0.1.0/cached_hub/__init__.py +82 -0
- cached_hub-0.1.0/cached_hub/__main__.py +5 -0
- cached_hub-0.1.0/cached_hub/_version.py +24 -0
- cached_hub-0.1.0/cached_hub/cli.py +143 -0
- cached_hub-0.1.0/cached_hub/config.py +158 -0
- cached_hub-0.1.0/cached_hub/datamaestro.py +33 -0
- cached_hub-0.1.0/cached_hub/hf.py +351 -0
- cached_hub-0.1.0/cached_hub/pyterrier.py +29 -0
- cached_hub-0.1.0/cached_hub/resources.py +196 -0
- cached_hub-0.1.0/cliff.toml +68 -0
- cached_hub-0.1.0/pyproject.toml +87 -0
- cached_hub-0.1.0/release-notes.md +10 -0
- cached_hub-0.1.0/tests/test_cli.py +91 -0
- cached_hub-0.1.0/tests/test_config.py +98 -0
- cached_hub-0.1.0/tests/test_hf.py +240 -0
- cached_hub-0.1.0/tests/test_resources.py +108 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Build and publish the package to PyPI when a GitHub release is created.
|
|
2
|
+
# Uses PyPI trusted publishing (OIDC): no API token needed.
|
|
3
|
+
|
|
4
|
+
name: Upload Python Package
|
|
5
|
+
|
|
6
|
+
on:
|
|
7
|
+
release:
|
|
8
|
+
types: [created]
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
deploy:
|
|
12
|
+
permissions:
|
|
13
|
+
contents: write # Required to update release notes
|
|
14
|
+
id-token: write # Required for PyPI trusted publishing
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v4
|
|
19
|
+
with:
|
|
20
|
+
fetch-depth: 0 # Needed for hatch-vcs to determine version
|
|
21
|
+
|
|
22
|
+
- name: Set up Python
|
|
23
|
+
uses: actions/setup-python@v5
|
|
24
|
+
with:
|
|
25
|
+
python-version: "3.x"
|
|
26
|
+
|
|
27
|
+
- name: Install hatch
|
|
28
|
+
run: |
|
|
29
|
+
python -m pip install --upgrade pip
|
|
30
|
+
pip install hatch
|
|
31
|
+
|
|
32
|
+
- name: Generate changelog for this release
|
|
33
|
+
uses: orhun/git-cliff-action@v4
|
|
34
|
+
with:
|
|
35
|
+
config: cliff.toml
|
|
36
|
+
args: --latest --strip header
|
|
37
|
+
env:
|
|
38
|
+
OUTPUT: release-notes.md
|
|
39
|
+
|
|
40
|
+
- name: Update release notes
|
|
41
|
+
uses: softprops/action-gh-release@v2
|
|
42
|
+
with:
|
|
43
|
+
body_path: release-notes.md
|
|
44
|
+
|
|
45
|
+
- name: Build python package
|
|
46
|
+
run: |
|
|
47
|
+
hatch build
|
|
48
|
+
|
|
49
|
+
- name: Publish package to PyPI
|
|
50
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
name: Python package
|
|
2
|
+
|
|
3
|
+
concurrency:
|
|
4
|
+
group: ${{ github.workflow }}-${{ github.ref }}
|
|
5
|
+
cancel-in-progress: true
|
|
6
|
+
|
|
7
|
+
on:
|
|
8
|
+
push:
|
|
9
|
+
branches: [main]
|
|
10
|
+
pull_request:
|
|
11
|
+
branches: [main]
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
build:
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
strategy:
|
|
17
|
+
matrix:
|
|
18
|
+
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
|
19
|
+
|
|
20
|
+
steps:
|
|
21
|
+
- uses: actions/checkout@v4
|
|
22
|
+
with:
|
|
23
|
+
fetch-depth: 0 # Needed for hatch-vcs to determine version
|
|
24
|
+
|
|
25
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
26
|
+
uses: actions/setup-python@v5
|
|
27
|
+
with:
|
|
28
|
+
python-version: ${{ matrix.python-version }}
|
|
29
|
+
|
|
30
|
+
- name: Install uv
|
|
31
|
+
uses: astral-sh/setup-uv@v5
|
|
32
|
+
with:
|
|
33
|
+
enable-cache: true
|
|
34
|
+
|
|
35
|
+
- name: Install dependencies
|
|
36
|
+
run: |
|
|
37
|
+
uv sync --extra dev
|
|
38
|
+
|
|
39
|
+
- name: Lint with ruff
|
|
40
|
+
run: |
|
|
41
|
+
uv run ruff check cached_hub tests
|
|
42
|
+
uv run ruff format --check cached_hub tests
|
|
43
|
+
|
|
44
|
+
- name: Test with pytest
|
|
45
|
+
timeout-minutes: 10
|
|
46
|
+
run: |
|
|
47
|
+
uv run pytest
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
default_install_hook_types:
|
|
2
|
+
- pre-commit
|
|
3
|
+
- commit-msg
|
|
4
|
+
|
|
5
|
+
repos:
|
|
6
|
+
- hooks:
|
|
7
|
+
- id: check-yaml
|
|
8
|
+
- id: check-toml
|
|
9
|
+
- id: end-of-file-fixer
|
|
10
|
+
- id: trailing-whitespace
|
|
11
|
+
repo: https://github.com/pre-commit/pre-commit-hooks
|
|
12
|
+
rev: v6.0.0
|
|
13
|
+
- hooks:
|
|
14
|
+
- id: python-check-blanket-noqa
|
|
15
|
+
- id: python-no-eval
|
|
16
|
+
repo: https://github.com/pre-commit/pygrep-hooks
|
|
17
|
+
rev: v1.10.0
|
|
18
|
+
- hooks:
|
|
19
|
+
- id: no-fixme-todo
|
|
20
|
+
name: No FIXME or TODO markers
|
|
21
|
+
entry: '(FIXME:|TODO:)'
|
|
22
|
+
language: pygrep
|
|
23
|
+
types: [python]
|
|
24
|
+
repo: local
|
|
25
|
+
- hooks:
|
|
26
|
+
- id: ruff
|
|
27
|
+
args: [--fix]
|
|
28
|
+
- id: ruff-format
|
|
29
|
+
repo: https://github.com/astral-sh/ruff-pre-commit
|
|
30
|
+
rev: v0.14.10
|
|
31
|
+
- hooks:
|
|
32
|
+
- id: conventional-pre-commit
|
|
33
|
+
stages:
|
|
34
|
+
- commit-msg
|
|
35
|
+
repo: https://github.com/compilerla/conventional-pre-commit
|
|
36
|
+
rev: v4.3.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.12
|
cached_hub-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Benjamin Piwowarski
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: cached-hub
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Load HuggingFace models/datasets (and other course resources) from a shared local cache, falling back to the Hub; declare and pre-download them for classrooms
|
|
5
|
+
Project-URL: Homepage, https://github.com/bpiwowar/cached-hub
|
|
6
|
+
Project-URL: Repository, https://github.com/bpiwowar/cached-hub
|
|
7
|
+
Author-email: Benjamin Piwowarski <benjamin@piwowarski.fr>
|
|
8
|
+
License: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: cache,datasets,huggingface,teaching,transformers
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Education
|
|
13
|
+
Classifier: Intended Audience :: Science/Research
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Education
|
|
22
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
23
|
+
Requires-Python: >=3.10
|
|
24
|
+
Provides-Extra: datamaestro
|
|
25
|
+
Requires-Dist: datamaestro>=1.5; extra == 'datamaestro'
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: pre-commit>=3.5; extra == 'dev'
|
|
28
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
29
|
+
Requires-Dist: ruff>=0.8; extra == 'dev'
|
|
30
|
+
Provides-Extra: hf
|
|
31
|
+
Requires-Dist: datasets>=2.7; extra == 'hf'
|
|
32
|
+
Requires-Dist: transformers>=4.30; extra == 'hf'
|
|
33
|
+
Provides-Extra: pyterrier
|
|
34
|
+
Requires-Dist: python-terrier>=0.10; extra == 'pyterrier'
|
|
35
|
+
Description-Content-Type: text/markdown
|
|
36
|
+
|
|
37
|
+
# cached-hub
|
|
38
|
+
|
|
39
|
+
**Load HuggingFace models and datasets from a shared local cache, fall back to
|
|
40
|
+
the Hub, and pre-populate that cache from a declarative list of what a course
|
|
41
|
+
needs.**
|
|
42
|
+
|
|
43
|
+
## Why
|
|
44
|
+
|
|
45
|
+
In a classroom, every student pulling `gpt2`, `SmolLM2` and `imdb` from the Hub
|
|
46
|
+
at the same minute is slow, fragile, and sometimes impossible (no home directory
|
|
47
|
+
quota, shaky proxy, offline lab). The usual answer is a shared read-only
|
|
48
|
+
directory pre-filled by the instructor. `cached-hub` makes that directory a
|
|
49
|
+
first-class thing:
|
|
50
|
+
|
|
51
|
+
- notebook code calls `load_hf_model("gpt2")` and gets the cached copy when it
|
|
52
|
+
exists, the Hub otherwise, with a one-line note saying which;
|
|
53
|
+
- the instructor declares the resources once, and `cached-hub download` fills
|
|
54
|
+
the cache (each shared resource once, optional ones on demand);
|
|
55
|
+
- an *enforce* mode turns any cache miss into an error, so you can check that
|
|
56
|
+
every notebook runs fully from the cache before the session.
|
|
57
|
+
|
|
58
|
+
The package has **no required dependency**: `transformers`, `datasets`,
|
|
59
|
+
`pyterrier` and `datamaestro` are imported only by the functions that use them.
|
|
60
|
+
It was extracted from the Sorbonne Master MIND `master-mind` tool so that a
|
|
61
|
+
course only needs this package (plus
|
|
62
|
+
[jupytext-notebook-helper](https://github.com/bpiwowar/jupytext-notebook-helper)
|
|
63
|
+
for building notebooks).
|
|
64
|
+
|
|
65
|
+
## Install
|
|
66
|
+
|
|
67
|
+
```sh
|
|
68
|
+
pip install cached-hub # loaders only (bring your own transformers/datasets)
|
|
69
|
+
pip install "cached-hub[hf]" # + transformers, datasets
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Supported libraries
|
|
73
|
+
|
|
74
|
+
| Library | cached-hub function | Equivalent to | Cached under `$CACHED_HUB_PATH` | Pre-download with |
|
|
75
|
+
|------------------------|------------------------------------|-------------------------------------------------|----------------------------------------|--------------------------------|
|
|
76
|
+
| transformers | `load_hf_model(id, cls=AutoModel, **kw)` | `cls.from_pretrained(id, **kw)` | `huggingface/models/<id>/` | `make_hf_model_resource` |
|
|
77
|
+
| transformers | `load_hf_tokenizer(id, cls=AutoTokenizer, **kw)`| `cls.from_pretrained(id, **kw)` | `huggingface/tokenizers/<id>/` | `make_hf_tokenizer_resource` |
|
|
78
|
+
| transformers | `load_hf_processor(id, cls=AutoProcessor, **kw)`| `cls.from_pretrained(id, **kw)` | `huggingface/processors/<id>/` | `make_hf_processor_resource` |
|
|
79
|
+
| datasets | `load_hf_dataset(id, name=None, split=None, **kw)` | `datasets.load_dataset(id, name, split=split, **kw)` | `huggingface/datasets/<id>[-<name>]/<split>/` | `make_hf_dataset_resource` |
|
|
80
|
+
| transformers | `HFModel(id, tok_cls, model_cls, **kw)` | lazy `.tokenizer` / `.model` via the two loaders above | as above | model + tokenizer resources |
|
|
81
|
+
| pyterrier / ir-datasets| (use `pt.get_dataset` directly) | `pt.get_dataset(id)` | pyterrier's own home (`PYTERRIER_HOME`, `IR_DATASETS_HOME`) | `make_pyterrier_dataset_resource` |
|
|
82
|
+
| datamaestro | (use `datamaestro.prepare_dataset` directly) | `prepare_dataset(id)` | datamaestro's own store (`DATAMAESTRO_DIR`) | `make_datamaestro_resource` |
|
|
83
|
+
|
|
84
|
+
The loaders differ from their equivalents in one way only: with
|
|
85
|
+
`CACHED_HUB_PATH` set, they first look for the resource in the cache layout
|
|
86
|
+
above, log where it came from, and on a miss forward to the equivalent call
|
|
87
|
+
with `cache_dir=$CACHED_HUB_PATH/huggingface` added (unless you passed one), or
|
|
88
|
+
raise `CacheMissError` in enforce mode. PyTerrier and datamaestro manage their
|
|
89
|
+
own caches, so there is no loader for them: `cached-hub` only declares them as
|
|
90
|
+
resources so that `cached-hub download` fetches everything a course needs in
|
|
91
|
+
one go, and you keep calling those libraries as usual.
|
|
92
|
+
|
|
93
|
+
## In notebooks
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
from cached_hub import load_hf_model, load_hf_tokenizer, load_hf_dataset, HFModel
|
|
97
|
+
from transformers import AutoModelForCausalLM
|
|
98
|
+
|
|
99
|
+
tokenizer = load_hf_tokenizer("HuggingFaceTB/SmolLM2-1.7B-Instruct")
|
|
100
|
+
model = load_hf_model("HuggingFaceTB/SmolLM2-1.7B-Instruct", AutoModelForCausalLM, device_map="auto")
|
|
101
|
+
train = load_hf_dataset("imdb", split="train")
|
|
102
|
+
sst2 = load_hf_dataset("glue", name="sst2") # DatasetDict of the cached splits
|
|
103
|
+
|
|
104
|
+
hf = HFModel("gpt2") # lazy: nothing is loaded yet
|
|
105
|
+
hf.tokenizer, hf.model # AutoTokenizer / AutoModel, loaded on first access
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Each loader checks the local cache first, then falls back to the Hub with a
|
|
109
|
+
warning. Extra keyword arguments go to `from_pretrained` / `load_dataset`.
|
|
110
|
+
|
|
111
|
+
## Configuration
|
|
112
|
+
|
|
113
|
+
| Variable | Effect |
|
|
114
|
+
|----------------------|------------------------------------------------------------------------|
|
|
115
|
+
| `CACHED_HUB_PATH` | Root of the shared cache. Unset: the library does nothing (see below). |
|
|
116
|
+
| `CACHED_HUB_ENFORCE` | If set (any value), a cache miss raises `CacheMissError` instead of falling back. |
|
|
117
|
+
|
|
118
|
+
Layout under the root (`org/name` becomes `org-name`):
|
|
119
|
+
|
|
120
|
+
```
|
|
121
|
+
$CACHED_HUB_PATH/huggingface/models/<id>/ model.save_pretrained()
|
|
122
|
+
$CACHED_HUB_PATH/huggingface/tokenizers/<id>/
|
|
123
|
+
$CACHED_HUB_PATH/huggingface/processors/<id>/
|
|
124
|
+
$CACHED_HUB_PATH/huggingface/datasets/<id>[-<name>]/<split>/ dataset.save_to_disk()
|
|
125
|
+
$CACHED_HUB_PATH/huggingface/ HF cache_dir used for fallbacks
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
A directory is used only when it contains the marker `.downloaded.ok`, written
|
|
129
|
+
after a successful download, so a half-copied model is never picked up.
|
|
130
|
+
|
|
131
|
+
**Without `CACHED_HUB_PATH`, `cached-hub` adds no caching of its own.**
|
|
132
|
+
`load_hf_model("gpt2", cls, **kw)` is then exactly `cls.from_pretrained("gpt2", **kw)`,
|
|
133
|
+
and `load_hf_dataset(...)` exactly `datasets.load_dataset(...)`: the usual
|
|
134
|
+
HuggingFace cache (`~/.cache/huggingface`, `HF_HOME`) applies as it always does,
|
|
135
|
+
and `cached-hub download` merely warms it. Notebooks can therefore import from
|
|
136
|
+
`cached_hub` unconditionally and run unchanged on a laptop or on Colab; only the
|
|
137
|
+
classroom machines set the variable.
|
|
138
|
+
|
|
139
|
+
## Declaring and downloading resources
|
|
140
|
+
|
|
141
|
+
A course lists what it needs as `{section: [resources]}`:
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
# mycourse/resources.py
|
|
145
|
+
from cached_hub import (
|
|
146
|
+
make_hf_model_resource, make_hf_tokenizer_resource, make_hf_processor_resource,
|
|
147
|
+
make_hf_dataset_resource, make_pyterrier_dataset_resource, make_datamaestro_resource,
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
RESOURCES = {
|
|
151
|
+
"practical1": [
|
|
152
|
+
make_hf_model_resource("gpt2", model_class="GPT2LMHeadModel"),
|
|
153
|
+
make_hf_tokenizer_resource("gpt2", tokenizer_class="GPT2Tokenizer"),
|
|
154
|
+
make_hf_dataset_resource("imdb", ["train", "test"]),
|
|
155
|
+
],
|
|
156
|
+
"practical2": [
|
|
157
|
+
make_hf_model_resource("Qwen/Qwen2.5-7B-Instruct", model_class="AutoModelForCausalLM", optional=True),
|
|
158
|
+
make_pyterrier_dataset_resource("irds:lotte/technology/dev/search", "LoTTE technology"),
|
|
159
|
+
],
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
then, on the machine that hosts the cache:
|
|
164
|
+
|
|
165
|
+
```sh
|
|
166
|
+
export CACHED_HUB_PATH=/shared/cache
|
|
167
|
+
cached-hub info
|
|
168
|
+
cached-hub list --from mycourse.resources:RESOURCES
|
|
169
|
+
cached-hub download --from mycourse.resources:RESOURCES # everything but optional
|
|
170
|
+
cached-hub download --from mycourse.resources:RESOURCES --section practical2 --optional
|
|
171
|
+
cached-hub download --from mycourse.resources:RESOURCES --key gpt2
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
`--from MODULE:ATTR` imports `MODULE` and reads `ATTR` from it: a
|
|
175
|
+
`{section: [resources]}` mapping, or a zero-argument callable returning one
|
|
176
|
+
(dotted attributes such as `plugin.Course.resources` are followed). No source
|
|
177
|
+
scanning is involved; `RESOURCES` above is only a naming convention. The option
|
|
178
|
+
can be repeated. Resources are identified by `(type, key)`, so a model shared
|
|
179
|
+
by several practicals is downloaded once. `HF_HUB_OFFLINE` is lifted for the
|
|
180
|
+
duration of a download.
|
|
181
|
+
|
|
182
|
+
The same helpers are available from Python (`download_resources`,
|
|
183
|
+
`select_resources`, `format_resources`, `merge_resources`), and any object with
|
|
184
|
+
`resource_type`, `key`, `description`, `optional` and `download()` is a valid
|
|
185
|
+
resource (`FunctionalResource` wraps a plain function).
|
|
186
|
+
|
|
187
|
+
## Checking a cache before class
|
|
188
|
+
|
|
189
|
+
```sh
|
|
190
|
+
CACHED_HUB_PATH=/shared/cache CACHED_HUB_ENFORCE=1 python practical1.py
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
fails at the first resource that would have gone to the Hub.
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# cached-hub
|
|
2
|
+
|
|
3
|
+
**Load HuggingFace models and datasets from a shared local cache, fall back to
|
|
4
|
+
the Hub, and pre-populate that cache from a declarative list of what a course
|
|
5
|
+
needs.**
|
|
6
|
+
|
|
7
|
+
## Why
|
|
8
|
+
|
|
9
|
+
In a classroom, every student pulling `gpt2`, `SmolLM2` and `imdb` from the Hub
|
|
10
|
+
at the same minute is slow, fragile, and sometimes impossible (no home directory
|
|
11
|
+
quota, shaky proxy, offline lab). The usual answer is a shared read-only
|
|
12
|
+
directory pre-filled by the instructor. `cached-hub` makes that directory a
|
|
13
|
+
first-class thing:
|
|
14
|
+
|
|
15
|
+
- notebook code calls `load_hf_model("gpt2")` and gets the cached copy when it
|
|
16
|
+
exists, the Hub otherwise, with a one-line note saying which;
|
|
17
|
+
- the instructor declares the resources once, and `cached-hub download` fills
|
|
18
|
+
the cache (each shared resource once, optional ones on demand);
|
|
19
|
+
- an *enforce* mode turns any cache miss into an error, so you can check that
|
|
20
|
+
every notebook runs fully from the cache before the session.
|
|
21
|
+
|
|
22
|
+
The package has **no required dependency**: `transformers`, `datasets`,
|
|
23
|
+
`pyterrier` and `datamaestro` are imported only by the functions that use them.
|
|
24
|
+
It was extracted from the Sorbonne Master MIND `master-mind` tool so that a
|
|
25
|
+
course only needs this package (plus
|
|
26
|
+
[jupytext-notebook-helper](https://github.com/bpiwowar/jupytext-notebook-helper)
|
|
27
|
+
for building notebooks).
|
|
28
|
+
|
|
29
|
+
## Install
|
|
30
|
+
|
|
31
|
+
```sh
|
|
32
|
+
pip install cached-hub # loaders only (bring your own transformers/datasets)
|
|
33
|
+
pip install "cached-hub[hf]" # + transformers, datasets
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Supported libraries
|
|
37
|
+
|
|
38
|
+
| Library | cached-hub function | Equivalent to | Cached under `$CACHED_HUB_PATH` | Pre-download with |
|
|
39
|
+
|------------------------|------------------------------------|-------------------------------------------------|----------------------------------------|--------------------------------|
|
|
40
|
+
| transformers | `load_hf_model(id, cls=AutoModel, **kw)` | `cls.from_pretrained(id, **kw)` | `huggingface/models/<id>/` | `make_hf_model_resource` |
|
|
41
|
+
| transformers | `load_hf_tokenizer(id, cls=AutoTokenizer, **kw)`| `cls.from_pretrained(id, **kw)` | `huggingface/tokenizers/<id>/` | `make_hf_tokenizer_resource` |
|
|
42
|
+
| transformers | `load_hf_processor(id, cls=AutoProcessor, **kw)`| `cls.from_pretrained(id, **kw)` | `huggingface/processors/<id>/` | `make_hf_processor_resource` |
|
|
43
|
+
| datasets | `load_hf_dataset(id, name=None, split=None, **kw)` | `datasets.load_dataset(id, name, split=split, **kw)` | `huggingface/datasets/<id>[-<name>]/<split>/` | `make_hf_dataset_resource` |
|
|
44
|
+
| transformers | `HFModel(id, tok_cls, model_cls, **kw)` | lazy `.tokenizer` / `.model` via the two loaders above | as above | model + tokenizer resources |
|
|
45
|
+
| pyterrier / ir-datasets| (use `pt.get_dataset` directly) | `pt.get_dataset(id)` | pyterrier's own home (`PYTERRIER_HOME`, `IR_DATASETS_HOME`) | `make_pyterrier_dataset_resource` |
|
|
46
|
+
| datamaestro | (use `datamaestro.prepare_dataset` directly) | `prepare_dataset(id)` | datamaestro's own store (`DATAMAESTRO_DIR`) | `make_datamaestro_resource` |
|
|
47
|
+
|
|
48
|
+
The loaders differ from their equivalents in one way only: with
|
|
49
|
+
`CACHED_HUB_PATH` set, they first look for the resource in the cache layout
|
|
50
|
+
above, log where it came from, and on a miss forward to the equivalent call
|
|
51
|
+
with `cache_dir=$CACHED_HUB_PATH/huggingface` added (unless you passed one), or
|
|
52
|
+
raise `CacheMissError` in enforce mode. PyTerrier and datamaestro manage their
|
|
53
|
+
own caches, so there is no loader for them: `cached-hub` only declares them as
|
|
54
|
+
resources so that `cached-hub download` fetches everything a course needs in
|
|
55
|
+
one go, and you keep calling those libraries as usual.
|
|
56
|
+
|
|
57
|
+
## In notebooks
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
from cached_hub import load_hf_model, load_hf_tokenizer, load_hf_dataset, HFModel
|
|
61
|
+
from transformers import AutoModelForCausalLM
|
|
62
|
+
|
|
63
|
+
tokenizer = load_hf_tokenizer("HuggingFaceTB/SmolLM2-1.7B-Instruct")
|
|
64
|
+
model = load_hf_model("HuggingFaceTB/SmolLM2-1.7B-Instruct", AutoModelForCausalLM, device_map="auto")
|
|
65
|
+
train = load_hf_dataset("imdb", split="train")
|
|
66
|
+
sst2 = load_hf_dataset("glue", name="sst2") # DatasetDict of the cached splits
|
|
67
|
+
|
|
68
|
+
hf = HFModel("gpt2") # lazy: nothing is loaded yet
|
|
69
|
+
hf.tokenizer, hf.model # AutoTokenizer / AutoModel, loaded on first access
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Each loader checks the local cache first, then falls back to the Hub with a
|
|
73
|
+
warning. Extra keyword arguments go to `from_pretrained` / `load_dataset`.
|
|
74
|
+
|
|
75
|
+
## Configuration
|
|
76
|
+
|
|
77
|
+
| Variable | Effect |
|
|
78
|
+
|----------------------|------------------------------------------------------------------------|
|
|
79
|
+
| `CACHED_HUB_PATH` | Root of the shared cache. Unset: the library does nothing (see below). |
|
|
80
|
+
| `CACHED_HUB_ENFORCE` | If set (any value), a cache miss raises `CacheMissError` instead of falling back. |
|
|
81
|
+
|
|
82
|
+
Layout under the root (`org/name` becomes `org-name`):
|
|
83
|
+
|
|
84
|
+
```
|
|
85
|
+
$CACHED_HUB_PATH/huggingface/models/<id>/ model.save_pretrained()
|
|
86
|
+
$CACHED_HUB_PATH/huggingface/tokenizers/<id>/
|
|
87
|
+
$CACHED_HUB_PATH/huggingface/processors/<id>/
|
|
88
|
+
$CACHED_HUB_PATH/huggingface/datasets/<id>[-<name>]/<split>/ dataset.save_to_disk()
|
|
89
|
+
$CACHED_HUB_PATH/huggingface/ HF cache_dir used for fallbacks
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
A directory is used only when it contains the marker `.downloaded.ok`, written
|
|
93
|
+
after a successful download, so a half-copied model is never picked up.
|
|
94
|
+
|
|
95
|
+
**Without `CACHED_HUB_PATH`, `cached-hub` adds no caching of its own.**
|
|
96
|
+
`load_hf_model("gpt2", cls, **kw)` is then exactly `cls.from_pretrained("gpt2", **kw)`,
|
|
97
|
+
and `load_hf_dataset(...)` exactly `datasets.load_dataset(...)`: the usual
|
|
98
|
+
HuggingFace cache (`~/.cache/huggingface`, `HF_HOME`) applies as it always does,
|
|
99
|
+
and `cached-hub download` merely warms it. Notebooks can therefore import from
|
|
100
|
+
`cached_hub` unconditionally and run unchanged on a laptop or on Colab; only the
|
|
101
|
+
classroom machines set the variable.
|
|
102
|
+
|
|
103
|
+
## Declaring and downloading resources
|
|
104
|
+
|
|
105
|
+
A course lists what it needs as `{section: [resources]}`:
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
# mycourse/resources.py
|
|
109
|
+
from cached_hub import (
|
|
110
|
+
make_hf_model_resource, make_hf_tokenizer_resource, make_hf_processor_resource,
|
|
111
|
+
make_hf_dataset_resource, make_pyterrier_dataset_resource, make_datamaestro_resource,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
RESOURCES = {
|
|
115
|
+
"practical1": [
|
|
116
|
+
make_hf_model_resource("gpt2", model_class="GPT2LMHeadModel"),
|
|
117
|
+
make_hf_tokenizer_resource("gpt2", tokenizer_class="GPT2Tokenizer"),
|
|
118
|
+
make_hf_dataset_resource("imdb", ["train", "test"]),
|
|
119
|
+
],
|
|
120
|
+
"practical2": [
|
|
121
|
+
make_hf_model_resource("Qwen/Qwen2.5-7B-Instruct", model_class="AutoModelForCausalLM", optional=True),
|
|
122
|
+
make_pyterrier_dataset_resource("irds:lotte/technology/dev/search", "LoTTE technology"),
|
|
123
|
+
],
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
then, on the machine that hosts the cache:
|
|
128
|
+
|
|
129
|
+
```sh
|
|
130
|
+
export CACHED_HUB_PATH=/shared/cache
|
|
131
|
+
cached-hub info
|
|
132
|
+
cached-hub list --from mycourse.resources:RESOURCES
|
|
133
|
+
cached-hub download --from mycourse.resources:RESOURCES # everything but optional
|
|
134
|
+
cached-hub download --from mycourse.resources:RESOURCES --section practical2 --optional
|
|
135
|
+
cached-hub download --from mycourse.resources:RESOURCES --key gpt2
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
`--from MODULE:ATTR` imports `MODULE` and reads `ATTR` from it: a
|
|
139
|
+
`{section: [resources]}` mapping, or a zero-argument callable returning one
|
|
140
|
+
(dotted attributes such as `plugin.Course.resources` are followed). No source
|
|
141
|
+
scanning is involved; `RESOURCES` above is only a naming convention. The option
|
|
142
|
+
can be repeated. Resources are identified by `(type, key)`, so a model shared
|
|
143
|
+
by several practicals is downloaded once. `HF_HUB_OFFLINE` is lifted for the
|
|
144
|
+
duration of a download.
|
|
145
|
+
|
|
146
|
+
The same helpers are available from Python (`download_resources`,
|
|
147
|
+
`select_resources`, `format_resources`, `merge_resources`), and any object with
|
|
148
|
+
`resource_type`, `key`, `description`, `optional` and `download()` is a valid
|
|
149
|
+
resource (`FunctionalResource` wraps a plain function).
|
|
150
|
+
|
|
151
|
+
## Checking a cache before class
|
|
152
|
+
|
|
153
|
+
```sh
|
|
154
|
+
CACHED_HUB_PATH=/shared/cache CACHED_HUB_ENFORCE=1 python practical1.py
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
fails at the first resource that would have gone to the Hub.
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""cached-hub: HuggingFace (and other) resources from a shared local cache.
|
|
2
|
+
|
|
3
|
+
Notebook side::
|
|
4
|
+
|
|
5
|
+
from cached_hub import load_hf_model, load_hf_tokenizer, load_hf_dataset, HFModel
|
|
6
|
+
|
|
7
|
+
Course side (declare what to pre-download)::
|
|
8
|
+
|
|
9
|
+
from cached_hub import make_hf_model_resource, make_hf_dataset_resource
|
|
10
|
+
RESOURCES = {"practical1": [make_hf_model_resource("gpt2"), ...]}
|
|
11
|
+
|
|
12
|
+
then ``cached-hub download --from mycourse.resources:RESOURCES``.
|
|
13
|
+
|
|
14
|
+
Configure the cache root with ``CACHED_HUB_PATH`` (see :mod:`cached_hub.config`).
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from .config import (
|
|
18
|
+
ENV_ENFORCE,
|
|
19
|
+
ENV_PATH,
|
|
20
|
+
CacheMissError,
|
|
21
|
+
get_cache_path,
|
|
22
|
+
is_enforce_mode,
|
|
23
|
+
)
|
|
24
|
+
from .datamaestro import make_datamaestro_resource
|
|
25
|
+
from .hf import (
|
|
26
|
+
HFModel,
|
|
27
|
+
load_hf_dataset,
|
|
28
|
+
load_hf_model,
|
|
29
|
+
load_hf_processor,
|
|
30
|
+
load_hf_tokenizer,
|
|
31
|
+
make_hf_dataset_resource,
|
|
32
|
+
make_hf_model_resource,
|
|
33
|
+
make_hf_processor_resource,
|
|
34
|
+
make_hf_tokenizer_resource,
|
|
35
|
+
)
|
|
36
|
+
from .pyterrier import make_pyterrier_dataset_resource
|
|
37
|
+
from .resources import (
|
|
38
|
+
DownloadableResource,
|
|
39
|
+
FunctionalResource,
|
|
40
|
+
Resource,
|
|
41
|
+
Resources,
|
|
42
|
+
download_resources,
|
|
43
|
+
format_resources,
|
|
44
|
+
merge_resources,
|
|
45
|
+
select_resources,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
from ._version import __version__
|
|
50
|
+
except ImportError: # pragma: no cover - not installed from a build
|
|
51
|
+
__version__ = "0.0.0+unknown"
|
|
52
|
+
|
|
53
|
+
__all__ = [
|
|
54
|
+
"__version__",
|
|
55
|
+
# config
|
|
56
|
+
"ENV_PATH",
|
|
57
|
+
"ENV_ENFORCE",
|
|
58
|
+
"CacheMissError",
|
|
59
|
+
"get_cache_path",
|
|
60
|
+
"is_enforce_mode",
|
|
61
|
+
# loading
|
|
62
|
+
"HFModel",
|
|
63
|
+
"load_hf_dataset",
|
|
64
|
+
"load_hf_model",
|
|
65
|
+
"load_hf_processor",
|
|
66
|
+
"load_hf_tokenizer",
|
|
67
|
+
# resources
|
|
68
|
+
"Resource",
|
|
69
|
+
"DownloadableResource",
|
|
70
|
+
"FunctionalResource",
|
|
71
|
+
"Resources",
|
|
72
|
+
"download_resources",
|
|
73
|
+
"format_resources",
|
|
74
|
+
"merge_resources",
|
|
75
|
+
"select_resources",
|
|
76
|
+
"make_hf_dataset_resource",
|
|
77
|
+
"make_hf_model_resource",
|
|
78
|
+
"make_hf_processor_resource",
|
|
79
|
+
"make_hf_tokenizer_resource",
|
|
80
|
+
"make_pyterrier_dataset_resource",
|
|
81
|
+
"make_datamaestro_resource",
|
|
82
|
+
]
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# file generated by vcs-versioning
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"__version__",
|
|
7
|
+
"__version_tuple__",
|
|
8
|
+
"version",
|
|
9
|
+
"version_tuple",
|
|
10
|
+
"__commit_id__",
|
|
11
|
+
"commit_id",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
version: str
|
|
15
|
+
__version__: str
|
|
16
|
+
__version_tuple__: tuple[int | str, ...]
|
|
17
|
+
version_tuple: tuple[int | str, ...]
|
|
18
|
+
commit_id: str | None
|
|
19
|
+
__commit_id__: str | None
|
|
20
|
+
|
|
21
|
+
__version__ = version = '0.1.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 1, 0)
|
|
23
|
+
|
|
24
|
+
__commit_id__ = commit_id = None
|