Skip to content

Commit 701fb0b

Browse files
XciDlhoestqdavanstrienWauplinhanouticelina
authored
feat(jobs): add volume mounting support for buckets and repos (#3936)
* feat(jobs): add volume mounting support for buckets and repos Add `volumes` parameter to `run_job`, `create_scheduled_job`, `run_uv_job`, and `create_scheduled_uv_job` to mount HuggingFace Buckets and Repos (models, datasets, spaces) as volumes in job containers. - Add `JobVolume` dataclass and `JobVolumeType` enum - Add `volumes` field to `JobInfo` and `JobSpec` responses - Add `-v/--volume` CLI option with Docker-like syntax (e.g. `-v models/gpt2:/data` or `-v buckets/org/bucket:/mnt:ro`) - Serialize volumes to camelCase for the Hub API * style: fix ruff formatting * docs: regenerate CLI reference for volume option * feat: default volume type to model, remove plural aliases * refactor: replace _VOLUME_TYPE_ALIASES dict with _VOLUME_TYPES set * test: add tests for JobVolume, _parse_volumes, and volume serialization * refactor: improve code quality per review - Remove dead isinstance check in _create_job_spec serialization - Add volumes field to JobInfo docstring - Preserve original input in _parse_volumes error messages - Restructure tests: parametrize, merge into existing classes, top-level imports * docs * Apply suggestions from code review Co-authored-by: Daniel van Strien <davanstrien@users.noreply.github.com> * Rename JobVolume to Volume + remove JobVolumeType enum * fix * better test * check tests * updated parsing logic * useless line * Update docs/source/en/guides/jobs.md Co-authored-by: célina <hanouticelina@gmail.com> * Update src/huggingface_hub/cli/jobs.py Co-authored-by: célina <hanouticelina@gmail.com> * no more misleading REPO_TYPE_BUCKET * Update docs/source/en/guides/cli.md Co-authored-by: célina <hanouticelina@gmail.com> * Update docs/source/en/guides/cli.md Co-authored-by: célina <hanouticelina@gmail.com> * added examples --------- Co-authored-by: Quentin Lhoest <lhoest.q@gmail.com> Co-authored-by: Quentin Lhoest <42851186+lhoestq@users.noreply.github.com> Co-authored-by: Daniel van Strien <davanstrien@users.noreply.github.com> Co-authored-by: Lucain Pouget <lucainp@gmail.com> Co-authored-by: Lucain <lucain@huggingface.co> Co-authored-by: célina <hanouticelina@gmail.com>
1 parent 98e9a94 commit 701fb0b

10 files changed

Lines changed: 482 additions & 4 deletions

File tree

docs/source/en/guides/cli.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1571,6 +1571,37 @@ Available `--flavor` options:
15711571

15721572
(updated in 07/2025 from Hugging Face [suggested_hardware docs](https://huggingface.co/docs/hub/en/spaces-config-reference))
15731573

1574+
## Volumes
1575+
1576+
Mount a volume on the Job's disk using `-v` or `--volume`.
1577+
1578+
You can mount any Hugging Face Repository (model/dataset/space) or [Storage Bucket](/docs/hub/storage-buckets) using the `hf://` URL scheme. For example:
1579+
1580+
* mount a model repository: `-v hf://openai/gpt-oss-120b:/model`
1581+
* mount a model repository (explicit type): `-v hf://models/openai/gpt-oss-120b:/model`
1582+
* mount a dataset repository: `-v hf://datasets/HuggingFaceFW/fineweb:/data`
1583+
* mount a storage bucket: `-v hf://buckets/username/my-bucket:/mnt`
1584+
* mount a space: `-v hf://spaces/username/my-space:/app`
1585+
* mount a subfolder inside a repo: `-v hf://datasets/org/ds/train:/data`
1586+
1587+
Then you can use the mounted volume as a local directory:
1588+
1589+
```bash
1590+
# Docker Job with a mounted volume as input
1591+
>>> hf jobs run -v hf://datasets/HuggingFaceFW/fineweb:/dataset \
1592+
... duckdb/duckdb duckdb -c "SELECT * FROM '/dataset/**/*.parquet' LIMIT 5"
1593+
1594+
# UV Job with a mounted volume to save checkpoints when training a model
1595+
>>> hf jobs uv run -v hf://buckets/username/my-bucket:/training-outputs \
1596+
... sft.py --output-dir /training-outputs/training-v3-final ...
1597+
```
1598+
1599+
Models, datasets and spaces are always mounted read-only. Storage buckets are read+write by default — this is especially useful for data that changes frequently, as files can be overwritten or deleted in place.
1600+
1601+
Use `:ro` to enable read-only:
1602+
1603+
* mount a storage bucket in read-only: `-v hf://buckets/username/my-bucket:/mnt:ro`
1604+
15741605
### Labels
15751606

15761607
Add labels to a Job using `-l` or `--label`. Labels are a key=value pairs that applies metadata to a Job. To label a Job with two labels, repeat the label flag (`-l` or `--label`):

docs/source/en/guides/jobs.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,42 @@ Available `flavor` options:
216216

217217
That's it! You're now running code on Hugging Face's infrastructure.
218218

219+
## Mount a volume
220+
221+
Mount a volume on the Jobs's disk using a list of [`Volume`].
222+
223+
You can mount any Hugging Face Repository (model/dataset/space) or [Storage Bucket](/docs/hub/storage-buckets). For example:
224+
225+
* mount a model repository: `Volume(type="model", source="openai/gpt-oss-120b", mount_path="/model")`
226+
* mount a dataset repository: `Volume(type="dataset", source="HuggingFaceFW/fineweb", mount_path="/data")`
227+
* mount a storage bucket: `Volume(type="bucket", source="username/my-bucket", mount_path="/mnt")`
228+
229+
Then you can use the mounted volume as a local directory:
230+
231+
```python
232+
>>> from huggingface_hub import run_job, Volume
233+
>>> job = run_job(
234+
... image="duckdb/duckdb",
235+
... command=["duckdb", "-c", "SELECT * FROM '/data/**/*.parquet' LIMIT 5"],
236+
... volumes=[Volume(type="dataset", source="HuggingFaceFW/fineweb", mount_path="/data")],
237+
... )
238+
```
239+
240+
You can also write to a mounted bucket, for example, to save checkpoints when training a model:
241+
242+
```python
243+
>>> from huggingface_hub import run_uv_job, Volume
244+
>>> script = "my_sft.py"
245+
>>> script_args = ["--output_dir", "/training-outputs/training-v3-final", ...]
246+
>>> checkpoints_bucket = Volume(type="bucket", source="username/my-bucket", mount_path="/training-outputs")
247+
>>> run_uv_job(script, script_args=script_args, volumes=[checkpoints_bucket])
248+
```
249+
250+
By default, mounted storage buckets have read+write abilities.
251+
This is especially useful for storage buckets, which provide fast, mutable storage for data that changes frequently — files can be overwritten or deleted in place.
252+
253+
Use `read_only=True` to enable read-only: `Volume(type="bucket", read_only=True, ...)`.
254+
219255
## Configure Job Timeout
220256

221257
Jobs have a default timeout (30 minutes), after which they will automatically stop. This is important to know when running long-running tasks like model training.

docs/source/en/package_reference/cli.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2157,6 +2157,7 @@ $ hf jobs run [OPTIONS] IMAGE COMMAND...
21572157
* `-e, --env TEXT`: Set environment variables. E.g. --env ENV=value
21582158
* `-s, --secrets TEXT`: Set secret environment variables. E.g. --secrets SECRET=value or `--secrets HF_TOKEN` to pass your Hugging Face token.
21592159
* `-l, --label TEXT`: Set labels. E.g. --label KEY=VALUE or --label LABEL
2160+
* `-v, --volume TEXT`: Mount a volume. Format: hf://[TYPE/]SOURCE:/MOUNT_PATH[:ro]. TYPE is one of: models, datasets, spaces, buckets. TYPE defaults to models if omitted. models, datasets and spaces are always mounted read-only. buckets are read+write by default.E.g. -v hf://gpt2:/data or -v hf://datasets/org/ds:/data or -v hf://buckets/org/b:/mnt:ro
21602161
* `--env-file TEXT`: Read in a file of environment variables.
21612162
* `--secrets-file TEXT`: Read in a file of secret environment variables.
21622163
* `--flavor [cpu-basic|cpu-upgrade|cpu-performance|cpu-xl|sprx8|zero-a10g|t4-small|t4-medium|l4x1|l4x4|l40sx1|l40sx4|l40sx8|a10g-small|a10g-large|a10g-largex2|a10g-largex4|a100-large|a100x4|a100x8|h200|h200x2|h200x4|h200x8|inf2x6]`: Flavor for the hardware, as in HF Spaces. Run 'hf jobs hardware' to list available flavors. Defaults to `cpu-basic`.
@@ -2170,6 +2171,7 @@ Examples
21702171
$ hf jobs run python:3.12 python -c 'print("Hello!")'
21712172
$ hf jobs run -e FOO=foo python:3.12 python script.py
21722173
$ hf jobs run --secrets HF_TOKEN python:3.12 python script.py
2174+
$ hf jobs run -v hf://gpt2:/data -v hf://buckets/org/b:/mnt python:3.12 python script.py
21732175

21742176
Learn more
21752177
Use `hf <command> --help` for more information about a command.
@@ -2335,6 +2337,7 @@ $ hf jobs scheduled run [OPTIONS] SCHEDULE IMAGE COMMAND...
23352337
* `-e, --env TEXT`: Set environment variables. E.g. --env ENV=value
23362338
* `-s, --secrets TEXT`: Set secret environment variables. E.g. --secrets SECRET=value or `--secrets HF_TOKEN` to pass your Hugging Face token.
23372339
* `-l, --label TEXT`: Set labels. E.g. --label KEY=VALUE or --label LABEL
2340+
* `-v, --volume TEXT`: Mount a volume. Format: hf://[TYPE/]SOURCE:/MOUNT_PATH[:ro]. TYPE is one of: models, datasets, spaces, buckets. TYPE defaults to models if omitted. models, datasets and spaces are always mounted read-only. buckets are read+write by default.E.g. -v hf://gpt2:/data or -v hf://datasets/org/ds:/data or -v hf://buckets/org/b:/mnt:ro
23382341
* `--env-file TEXT`: Read in a file of environment variables.
23392342
* `--secrets-file TEXT`: Read in a file of secret environment variables.
23402343
* `--flavor [cpu-basic|cpu-upgrade|cpu-performance|cpu-xl|sprx8|zero-a10g|t4-small|t4-medium|l4x1|l4x4|l40sx1|l40sx4|l40sx8|a10g-small|a10g-large|a10g-largex2|a10g-largex4|a100-large|a100x4|a100x8|h200|h200x2|h200x4|h200x8|inf2x6]`: Flavor for the hardware, as in HF Spaces. Run 'hf jobs hardware' to list available flavors. Defaults to `cpu-basic`.
@@ -2422,6 +2425,7 @@ $ hf jobs scheduled uv run [OPTIONS] SCHEDULE SCRIPT [SCRIPT_ARGS]...
24222425
* `-e, --env TEXT`: Set environment variables. E.g. --env ENV=value
24232426
* `-s, --secrets TEXT`: Set secret environment variables. E.g. --secrets SECRET=value or `--secrets HF_TOKEN` to pass your Hugging Face token.
24242427
* `-l, --label TEXT`: Set labels. E.g. --label KEY=VALUE or --label LABEL
2428+
* `-v, --volume TEXT`: Mount a volume. Format: hf://[TYPE/]SOURCE:/MOUNT_PATH[:ro]. TYPE is one of: models, datasets, spaces, buckets. TYPE defaults to models if omitted. models, datasets and spaces are always mounted read-only. buckets are read+write by default.E.g. -v hf://gpt2:/data or -v hf://datasets/org/ds:/data or -v hf://buckets/org/b:/mnt:ro
24252429
* `--env-file TEXT`: Read in a file of environment variables.
24262430
* `--secrets-file TEXT`: Read in a file of secret environment variables.
24272431
* `--timeout TEXT`: Max duration: int/float with s (seconds, default), m (minutes), h (hours) or d (days).
@@ -2508,6 +2512,7 @@ $ hf jobs uv run [OPTIONS] SCRIPT [SCRIPT_ARGS]...
25082512
* `-e, --env TEXT`: Set environment variables. E.g. --env ENV=value
25092513
* `-s, --secrets TEXT`: Set secret environment variables. E.g. --secrets SECRET=value or `--secrets HF_TOKEN` to pass your Hugging Face token.
25102514
* `-l, --label TEXT`: Set labels. E.g. --label KEY=VALUE or --label LABEL
2515+
* `-v, --volume TEXT`: Mount a volume. Format: hf://[TYPE/]SOURCE:/MOUNT_PATH[:ro]. TYPE is one of: models, datasets, spaces, buckets. TYPE defaults to models if omitted. models, datasets and spaces are always mounted read-only. buckets are read+write by default.E.g. -v hf://gpt2:/data or -v hf://datasets/org/ds:/data or -v hf://buckets/org/b:/mnt:ro
25112516
* `--env-file TEXT`: Read in a file of environment variables.
25122517
* `--secrets-file TEXT`: Read in a file of secret environment variables.
25132518
* `--timeout TEXT`: Max duration: int/float with s (seconds, default), m (minutes), h (hours) or d (days).
@@ -2522,6 +2527,7 @@ Examples
25222527
$ hf jobs uv run my_script.py
25232528
$ hf jobs uv run ml_training.py --flavor a10g-small
25242529
$ hf jobs uv run --with transformers train.py
2530+
$ hf jobs uv run -v hf://gpt2:/data -v hf://buckets/org/b:/mnt script.py
25252531

25262532
Learn more
25272533
Use `hf <command> --help` for more information about a command.

docs/source/en/package_reference/jobs.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,7 @@ Check the [`HfApi`] documentation page for the reference of methods to manage yo
3232
### JobStatus
3333

3434
[[autodoc]] JobStatus
35+
36+
### Volume
37+
38+
[[autodoc]] Volume

src/huggingface_hub/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@
8383
"JobOwner",
8484
"JobStage",
8585
"JobStatus",
86+
"Volume",
8687
],
8788
"_login": [
8889
"auth_list",
@@ -858,6 +859,7 @@
858859
"VisualQuestionAnsweringInputData",
859860
"VisualQuestionAnsweringOutputElement",
860861
"VisualQuestionAnsweringParameters",
862+
"Volume",
861863
"WebhookInfo",
862864
"WebhookPayload",
863865
"WebhookPayloadComment",
@@ -1205,6 +1207,7 @@ def __dir__():
12051207
JobOwner, # noqa: F401
12061208
JobStage, # noqa: F401
12071209
JobStatus, # noqa: F401
1210+
Volume, # noqa: F401
12081211
)
12091212
from ._login import (
12101213
auth_list, # noqa: F401

src/huggingface_hub/_jobs_api.py

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,51 @@
1515
from dataclasses import dataclass
1616
from datetime import datetime
1717
from enum import Enum
18-
from typing import Any, Optional, Union
18+
from typing import Any, Literal, Optional, Union
1919

2020
from huggingface_hub import constants
2121
from huggingface_hub._space_api import SpaceHardware
2222
from huggingface_hub.utils._datetime import parse_datetime
2323

2424

25+
@dataclass
26+
class Volume:
27+
"""
28+
Describes a volume to mount in a Job container.
29+
30+
Args:
31+
type (`str`):
32+
Type of volume: `"bucket"`, `"model"`, `"dataset"`, or `"space"`.
33+
source (`str`):
34+
Source identifier, e.g. `"username/my-bucket"` or `"username/my-model"`.
35+
mount_path (`str`):
36+
Mount path inside the container, e.g. `"/data"`. Must start with `/`.
37+
revision (`str` or `None`):
38+
Git revision (only for repos, defaults to `"main"`).
39+
read_only (`bool` or `None`):
40+
Read-only mount. Forced `True` for repos, defaults to `False` for buckets.
41+
path (`str` or `None`):
42+
Subfolder prefix inside the bucket/repo to mount, e.g. `"path/to/dir"`.
43+
"""
44+
45+
type: Literal["bucket", "model", "dataset", "space"]
46+
source: str
47+
mount_path: str
48+
revision: Optional[str] = None
49+
read_only: Optional[bool] = None
50+
path: Optional[str] = None
51+
52+
def __init__(self, **kwargs) -> None:
53+
self.type = kwargs.get("type", "model")
54+
self.source = kwargs["source"]
55+
mount_path = kwargs.get("mountPath")
56+
self.mount_path = mount_path if mount_path is not None else kwargs["mount_path"]
57+
self.revision = kwargs.get("revision")
58+
read_only = kwargs.get("readOnly")
59+
self.read_only = read_only if read_only is not None else kwargs.get("read_only")
60+
self.path = kwargs.get("path")
61+
62+
2563
class JobStage(str, Enum):
2664
"""
2765
Enumeration of possible stage of a Job on the Hub.
@@ -84,6 +122,8 @@ class JobInfo:
84122
E.g. `"cpu-basic"`.
85123
labels (`dict[str, str]` or `None`):
86124
Labels to attach to the job (key-value pairs).
125+
volumes (`list[Volume]` or `None`):
126+
Volumes mounted in the job container (buckets, models, datasets, spaces).
87127
status: (`JobStatus` or `None`):
88128
Status of the Job, e.g. `JobStatus(stage="RUNNING", message=None)`
89129
See [`JobStage`] for possible stage values.
@@ -119,6 +159,7 @@ class JobInfo:
119159
secrets: Optional[dict[str, Any]]
120160
flavor: Optional[SpaceHardware]
121161
labels: Optional[dict[str, str]]
162+
volumes: Optional[list[Volume]]
122163
status: JobStatus
123164
owner: JobOwner
124165

@@ -140,6 +181,8 @@ def __init__(self, **kwargs) -> None:
140181
self.secrets = kwargs.get("secrets")
141182
self.flavor = kwargs.get("flavor")
142183
self.labels = kwargs.get("labels")
184+
volumes = kwargs.get("volumes")
185+
self.volumes = [Volume(**v) for v in volumes] if volumes else None
143186
status = kwargs.get("status", {})
144187
self.status = JobStatus(stage=status["stage"], message=status.get("message"))
145188

@@ -161,6 +204,7 @@ class JobSpec:
161204
tags: Optional[list[str]]
162205
arch: Optional[str]
163206
labels: Optional[dict[str, str]]
207+
volumes: Optional[list[Volume]]
164208

165209
def __init__(self, **kwargs) -> None:
166210
self.docker_image = kwargs.get("dockerImage") or kwargs.get("docker_image")
@@ -174,6 +218,8 @@ def __init__(self, **kwargs) -> None:
174218
self.tags = kwargs.get("tags")
175219
self.arch = kwargs.get("arch")
176220
self.labels = kwargs.get("labels")
221+
volumes = kwargs.get("volumes")
222+
self.volumes = [Volume(**v) for v in volumes] if volumes else None
177223

178224

179225
@dataclass
@@ -363,6 +409,7 @@ def _create_job_spec(
363409
flavor: Optional[SpaceHardware],
364410
timeout: Optional[Union[int, float, str]],
365411
labels: Optional[dict[str, str]] = None,
412+
volumes: Optional[list[Volume]] = None,
366413
) -> dict[str, Any]:
367414
# prepare job spec to send to HF Jobs API
368415
job_spec: dict[str, Any] = {
@@ -384,6 +431,19 @@ def _create_job_spec(
384431
# labels are optional
385432
if labels:
386433
job_spec["labels"] = labels
434+
# volumes are optional
435+
if volumes:
436+
job_spec["volumes"] = [
437+
{
438+
"type": vol.type,
439+
"source": vol.source,
440+
"mountPath": vol.mount_path,
441+
**({"revision": vol.revision} if vol.revision is not None else {}),
442+
**({"readOnly": vol.read_only} if vol.read_only is not None else {}),
443+
**({"path": vol.path} if vol.path is not None else {}),
444+
}
445+
for vol in volumes
446+
]
387447
# input is either from docker hub or from HF spaces
388448
for prefix in (
389449
"https://huggingface.co/spaces/",

0 commit comments

Comments
 (0)