Skip to content

Commit 6cae778

Browse files
co42Wauplin
andauthored
[Jobs] Add network groups to hf jobs run (#4833)
* [Jobs] Add network groups to hf jobs run * Trim network groups guide sections --------- Co-authored-by: Lucain <lucain@huggingface.co>
1 parent 063b37b commit 6cae778

7 files changed

Lines changed: 157 additions & 0 deletions

File tree

docs/source/en/guides/cli.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2116,6 +2116,20 @@ Pass `--ssh` to `hf jobs run` (or `hf jobs uv run`) to make the Job's container
21162116

21172117
Only users with write access to the Job's namespace are allowed in (the Job creator, or members of the owner organization), authenticated by an SSH public key registered at https://huggingface.co/settings/keys.
21182118

2119+
### Network groups
2120+
2121+
Pass `--network-group <name>` to `hf jobs run` (or `hf jobs uv run`) to let Jobs of the same owner reach each other on every port. Inside each member, `$HF_NETWORK_GROUP_HOSTNAME` resolves to every Job in the group, and `${HF_NETWORK_GROUP_PREFIX}<alias>` to the members that claimed an alias with `--network-alias <alias>`:
2122+
2123+
```bash
2124+
# Start a server, reachable by the other members of the group as "master"
2125+
>>> hf jobs run --detach --network-group train --network-alias master python:3.12 python -m http.server 8000
2126+
2127+
# Start a client in the same group
2128+
>>> hf jobs run --detach --network-group train python:3.12 sh -c 'curl --retry 10 --retry-connrefused "http://${HF_NETWORK_GROUP_PREFIX}master:8000/"'
2129+
```
2130+
2131+
Members are resolvable before they are ready, so connect with retries.
2132+
21192133
### UV Scripts (Experimental)
21202134

21212135
Run UV scripts (Python scripts with inline dependencies) on HF infrastructure. UV scripts are Python scripts that include their dependencies directly in the file using a special comment syntax.

docs/source/en/guides/jobs.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,27 @@ Connect from a terminal with `hf jobs ssh <job_id>` (or directly with `ssh <job_
372372

373373
Only users with write access to the Job's namespace are allowed in (the Job creator, or members of the owner organization), authenticated by an SSH public key registered at https://huggingface.co/settings/keys.
374374

375+
## Network groups
376+
377+
Pass `network_group="<name>"` to [`run_job`] (or [`run_uv_job`]) to let Jobs of the same owner reach each other on every port. Inside each member, `HF_NETWORK_GROUP_HOSTNAME` resolves to every Job in the group, and `${HF_NETWORK_GROUP_PREFIX}<alias>` to the members that claimed an alias with `network_aliases=[...]`:
378+
379+
```python
380+
>>> from huggingface_hub import run_job
381+
>>> server = run_job(
382+
... image="python:3.12",
383+
... command=["python", "-m", "http.server", "8000"],
384+
... network_group="train",
385+
... network_aliases=["master"],
386+
... )
387+
>>> client = run_job(
388+
... image="python:3.12",
389+
... command=["sh", "-c", 'curl --retry 10 --retry-connrefused "http://${HF_NETWORK_GROUP_PREFIX}master:8000/"'],
390+
... network_group="train",
391+
... )
392+
```
393+
394+
Members are resolvable before they are ready, so connect with retries.
395+
375396
## Configure Job Timeout
376397

377398
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: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2505,6 +2505,8 @@ $ hf jobs run [OPTIONS] IMAGE COMMAND...
25052505
* `-d, --detach`: Run the Job in the background and print the Job ID.
25062506
* `--expose INTEGER`: Expose a container port through the jobs proxy. Repeat the flag for multiple ports (e.g. `--expose 8000 --expose 8001`). Each exposed port is reachable on the public jobs domain; access requires an HF token with read access to the job's namespace.
25072507
* `--ssh`: Make the job's container reachable over SSH. Connect with `hf jobs ssh <job_id>`. Requires an SSH public key registered on https://huggingface.co/settings/keys.
2508+
* `--network-group TEXT`: Join a network group. Jobs of the same owner sharing a group are placed together and reach each other on every port. Inside each member, `$HF_NETWORK_GROUP_HOSTNAME` resolves to every member. Lowercase alphanumerics and dashes, 46 characters max.
2509+
* `--network-alias TEXT`: Claim an alias in the network group. Members reach the jobs claiming it at `${HF_NETWORK_GROUP_PREFIX}<alias>`. Repeat the flag for several aliases. Requires `--network-group`.
25082510
* `--resource-group-id TEXT`: The ID of the resource group to create the Job in. Used to control access to resources within an organization and for cost attribution/spending-limit features.
25092511
* `--namespace TEXT`: The namespace where the job will be running. Defaults to the current user's namespace.
25102512
* `--token TEXT`: A User Access Token generated from https://huggingface.co/settings/tokens.
@@ -2977,6 +2979,8 @@ $ hf jobs uv run [OPTIONS] SCRIPT [SCRIPT_ARGS]...
29772979
* `-d, --detach`: Run the Job in the background and print the Job ID.
29782980
* `--expose INTEGER`: Expose a container port through the jobs proxy. Repeat the flag for multiple ports (e.g. `--expose 8000 --expose 8001`). Each exposed port is reachable on the public jobs domain; access requires an HF token with read access to the job's namespace.
29792981
* `--ssh`: Make the job's container reachable over SSH. Connect with `hf jobs ssh <job_id>`. Requires an SSH public key registered on https://huggingface.co/settings/keys.
2982+
* `--network-group TEXT`: Join a network group. Jobs of the same owner sharing a group are placed together and reach each other on every port. Inside each member, `$HF_NETWORK_GROUP_HOSTNAME` resolves to every member. Lowercase alphanumerics and dashes, 46 characters max.
2983+
* `--network-alias TEXT`: Claim an alias in the network group. Members reach the jobs claiming it at `${HF_NETWORK_GROUP_PREFIX}<alias>`. Repeat the flag for several aliases. Requires `--network-group`.
29802984
* `--resource-group-id TEXT`: The ID of the resource group to create the Job in. Used to control access to resources within an organization and for cost attribution/spending-limit features.
29812985
* `--namespace TEXT`: The namespace where the job will be running. Defaults to the current user's namespace.
29822986
* `--token TEXT`: A User Access Token generated from https://huggingface.co/settings/tokens.

src/huggingface_hub/_jobs_api.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -581,8 +581,12 @@ def _create_job_spec(
581581
volumes: list[Volume] | None = None,
582582
expose: list[int] | None = None,
583583
ssh: bool = False,
584+
network_group: str | None = None,
585+
network_aliases: list[str] | None = None,
584586
resource_group_id: str | None = None,
585587
) -> dict[str, Any]:
588+
if network_aliases and not network_group:
589+
raise ValueError("`network_aliases` requires `network_group`.")
586590
if name is not None:
587591
if labels is not None and "name" in labels:
588592
raise ValueError("`name` and the `name` key in `labels` cannot both be provided.")
@@ -617,6 +621,12 @@ def _create_job_spec(
617621
# make the job container reachable over SSH
618622
if ssh:
619623
job_spec["ssh"] = {"enabled": True}
624+
# join a network group, optionally claiming aliases in it
625+
if network_group:
626+
network: dict[str, Any] = {"group": network_group}
627+
if network_aliases:
628+
network["aliases"] = network_aliases
629+
job_spec["network"] = network
620630
# resource group is optional
621631
if resource_group_id:
622632
job_spec["resourceGroupId"] = resource_group_id

src/huggingface_hub/cli/jobs.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,22 @@ def _parse_and_sync_job_volumes(
216216
),
217217
]
218218

219+
NetworkGroupOpt = Annotated[
220+
str | None,
221+
Option(
222+
"--network-group",
223+
help="Join a network group. Jobs of the same owner sharing a group are placed together and reach each other on every port. Inside each member, `$HF_NETWORK_GROUP_HOSTNAME` resolves to every member. Lowercase alphanumerics and dashes, 46 characters max.",
224+
),
225+
]
226+
227+
NetworkAliasOpt = Annotated[
228+
list[str] | None,
229+
Option(
230+
"--network-alias",
231+
help="Claim an alias in the network group. Members reach the jobs claiming it at `${HF_NETWORK_GROUP_PREFIX}<alias>`. Repeat the flag for several aliases. Requires `--network-group`.",
232+
),
233+
]
234+
219235
WithOpt = Annotated[
220236
list[str] | None,
221237
Option(
@@ -353,6 +369,8 @@ def jobs_run(
353369
detach: DetachOpt = False,
354370
expose: ExposeOpt = None,
355371
ssh: SshEnabledOpt = False,
372+
network_group: NetworkGroupOpt = None,
373+
network_alias: NetworkAliasOpt = None,
356374
resource_group_id: ResourceGroupIdOpt = None,
357375
namespace: NamespaceOpt = None,
358376
token: TokenOpt = None,
@@ -373,6 +391,8 @@ def jobs_run(
373391
timeout=timeout,
374392
expose=expose,
375393
ssh=ssh,
394+
network_group=network_group,
395+
network_aliases=network_alias,
376396
resource_group_id=resource_group_id,
377397
namespace=namespace,
378398
)
@@ -388,6 +408,11 @@ def jobs_run(
388408
out.hint(f"Exposed ports are reachable at (requires an HF token with read access to the job):\n{urls}")
389409
if isinstance(job.status.ssh_url, str):
390410
out.hint(f"Use `hf jobs ssh {job.owner.name}/{job.id}` to open an SSH session into the job.")
411+
if network_group:
412+
out.hint(
413+
f"Joined network group '{network_group}'. Jobs started with `--network-group {network_group}` reach each other "
414+
"at `$HF_NETWORK_GROUP_HOSTNAME` (every member) or `${HF_NETWORK_GROUP_PREFIX}<alias>` (members claiming an alias)."
415+
)
391416
if detach:
392417
job_ref = f"{job.owner.name}/{job.id}"
393418
out.hint(f"Use `hf jobs logs -f {job_ref}` to stream logs, or `hf jobs inspect {job_ref}` to check status.")
@@ -938,6 +963,8 @@ def jobs_uv_run(
938963
detach: DetachOpt = False,
939964
expose: ExposeOpt = None,
940965
ssh: SshEnabledOpt = False,
966+
network_group: NetworkGroupOpt = None,
967+
network_alias: NetworkAliasOpt = None,
941968
resource_group_id: ResourceGroupIdOpt = None,
942969
namespace: NamespaceOpt = None,
943970
token: TokenOpt = None,
@@ -963,6 +990,8 @@ def jobs_uv_run(
963990
timeout=timeout,
964991
expose=expose,
965992
ssh=ssh,
993+
network_group=network_group,
994+
network_aliases=network_alias,
966995
resource_group_id=resource_group_id,
967996
namespace=namespace,
968997
)
@@ -978,6 +1007,11 @@ def jobs_uv_run(
9781007
out.hint(f"Exposed ports are reachable at (requires an HF token with read access to the job):\n{urls}")
9791008
if isinstance(job.status.ssh_url, str):
9801009
out.hint(f"Use `hf jobs ssh {job.owner.name}/{job.id}` to open an SSH session into the job.")
1010+
if network_group:
1011+
out.hint(
1012+
f"Joined network group '{network_group}'. Jobs started with `--network-group {network_group}` reach each other "
1013+
"at `$HF_NETWORK_GROUP_HOSTNAME` (every member) or `${HF_NETWORK_GROUP_PREFIX}<alias>` (members claiming an alias)."
1014+
)
9811015
if detach:
9821016
job_ref = f"{job.owner.name}/{job.id}"
9831017
out.hint(f"Use `hf jobs logs -f {job_ref}` to stream logs, or `hf jobs inspect {job_ref}` to check status.")

src/huggingface_hub/hf_api.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12130,6 +12130,8 @@ def run_job(
1213012130
volumes: list[Volume] | None = None,
1213112131
expose: list[int] | None = None,
1213212132
ssh: bool = False,
12133+
network_group: str | None = None,
12134+
network_aliases: list[str] | None = None,
1213312135
resource_group_id: str | None = None,
1213412136
namespace: str | None = None,
1213512137
token: bool | str | None = None,
@@ -12183,6 +12185,15 @@ def run_job(
1218312185
write access to the job's namespace and an SSH public key registered on the Hub
1218412186
(https://huggingface.co/settings/keys). Defaults to False.
1218512187

12188+
network_group (`str`, *optional*):
12189+
Name of a network group to join. Jobs of the same owner sharing a group are placed together and
12190+
can reach each other on every port. Inside each member, `HF_NETWORK_GROUP_HOSTNAME` resolves to
12191+
every member of the group. Lowercase alphanumerics and dashes, 46 characters max.
12192+
12193+
network_aliases (`list[str]`, *optional*):
12194+
Aliases this job claims in its network group. Members reach the jobs claiming an alias at
12195+
`${HF_NETWORK_GROUP_PREFIX}<alias>`. Several jobs may claim the same alias. Requires `network_group`.
12196+
1218612197
resource_group_id (`str`, *optional*):
1218712198
The ID of the resource group to create the Job in. Used to control access to resources within an
1218812199
organization and for cost attribution/spending-limit features. If not provided, the Job is created
@@ -12241,6 +12252,8 @@ def run_job(
1224112252
volumes=volumes,
1224212253
expose=expose,
1224312254
ssh=ssh,
12255+
network_group=network_group,
12256+
network_aliases=network_aliases,
1224412257
resource_group_id=resource_group_id,
1224512258
)
1224612259
response = get_session().post(
@@ -12771,6 +12784,8 @@ def run_uv_job(
1277112784
volumes: list[Volume] | None = None,
1277212785
expose: list[int] | None = None,
1277312786
ssh: bool = False,
12787+
network_group: str | None = None,
12788+
network_aliases: list[str] | None = None,
1277412789
resource_group_id: str | None = None,
1277512790
namespace: str | None = None,
1277612791
token: bool | str | None = None,
@@ -12831,6 +12846,15 @@ def run_uv_job(
1283112846
write access to the job's namespace and an SSH public key registered on the Hub
1283212847
(https://huggingface.co/settings/keys). Defaults to False.
1283312848

12849+
network_group (`str`, *optional*):
12850+
Name of a network group to join. Jobs of the same owner sharing a group are placed together and
12851+
can reach each other on every port. Inside each member, `HF_NETWORK_GROUP_HOSTNAME` resolves to
12852+
every member of the group. Lowercase alphanumerics and dashes, 46 characters max.
12853+
12854+
network_aliases (`list[str]`, *optional*):
12855+
Aliases this job claims in its network group. Members reach the jobs claiming an alias at
12856+
`${HF_NETWORK_GROUP_PREFIX}<alias>`. Several jobs may claim the same alias. Requires `network_group`.
12857+
1283412858
resource_group_id (`str`, *optional*):
1283512859
The ID of the resource group to create the Job in. Used to control access to resources within an
1283612860
organization and for cost attribution/spending-limit features. If not provided, the Job is created
@@ -12917,6 +12941,8 @@ def run_uv_job(
1291712941
volumes=volumes,
1291812942
expose=expose,
1291912943
ssh=ssh,
12944+
network_group=network_group,
12945+
network_aliases=network_aliases,
1292012946
resource_group_id=resource_group_id,
1292112947
namespace=namespace,
1292212948
token=token,

tests/test_cli.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3353,6 +3353,8 @@ def test_run(self, runner: CliRunner) -> None:
33533353
timeout=None,
33543354
expose=None,
33553355
ssh=False,
3356+
network_group=None,
3357+
network_aliases=None,
33563358
resource_group_id=None,
33573359
namespace=None,
33583360
)
@@ -3381,6 +3383,8 @@ def test_run_with_extra_args(self, runner: CliRunner) -> None:
33813383
timeout=None,
33823384
expose=None,
33833385
ssh=False,
3386+
network_group=None,
3387+
network_aliases=None,
33843388
resource_group_id=None,
33853389
namespace=None,
33863390
)
@@ -3440,6 +3444,8 @@ def test_uv_command(self, runner: CliRunner) -> None:
34403444
timeout=None,
34413445
expose=None,
34423446
ssh=False,
3447+
network_group=None,
3448+
network_aliases=None,
34433449
resource_group_id=None,
34443450
namespace=None,
34453451
)
@@ -3471,6 +3477,8 @@ def test_uv_command_with_extra_args(self, runner: CliRunner) -> None:
34713477
timeout=None,
34723478
expose=None,
34733479
ssh=False,
3480+
network_group=None,
3481+
network_aliases=None,
34743482
resource_group_id=None,
34753483
namespace=None,
34763484
)
@@ -3500,6 +3508,8 @@ def test_uv_remote_script(self, runner: CliRunner) -> None:
35003508
timeout=None,
35013509
expose=None,
35023510
ssh=False,
3511+
network_group=None,
3512+
network_aliases=None,
35033513
resource_group_id=None,
35043514
namespace=None,
35053515
)
@@ -3530,6 +3540,8 @@ def test_uv_local_script(self, runner: CliRunner, tmp_path: Path) -> None:
35303540
timeout=None,
35313541
expose=None,
35323542
ssh=False,
3543+
network_group=None,
3544+
network_aliases=None,
35333545
resource_group_id=None,
35343546
namespace=None,
35353547
)
@@ -4451,6 +4463,42 @@ def test_serialize_expose(self, expose: list[int] | None, expected: dict | None)
44514463
)
44524464
assert spec.get("expose") == expected
44534465

4466+
@pytest.mark.parametrize(
4467+
"network_group, network_aliases, expected",
4468+
[
4469+
(None, None, None),
4470+
("train", None, {"group": "train"}),
4471+
("train", [], {"group": "train"}),
4472+
("train", ["master", "worker"], {"group": "train", "aliases": ["master", "worker"]}),
4473+
],
4474+
)
4475+
def test_serialize_network(
4476+
self, network_group: str | None, network_aliases: list[str] | None, expected: dict | None
4477+
) -> None:
4478+
spec = _create_job_spec(
4479+
image="python:3.12",
4480+
command=["echo"],
4481+
env=None,
4482+
secrets=None,
4483+
flavor=None,
4484+
timeout=None,
4485+
network_group=network_group,
4486+
network_aliases=network_aliases,
4487+
)
4488+
assert spec.get("network") == expected
4489+
4490+
def test_network_aliases_require_group(self) -> None:
4491+
with pytest.raises(ValueError, match="network_aliases"):
4492+
_create_job_spec(
4493+
image="python:3.12",
4494+
command=["echo"],
4495+
env=None,
4496+
secrets=None,
4497+
flavor=None,
4498+
timeout=None,
4499+
network_aliases=["master"],
4500+
)
4501+
44544502

44554503
class TestWebhooksCommand:
44564504
def _make_webhook(self, **kwargs):

0 commit comments

Comments
 (0)