Skip to content

[Jobs] Support commands in hf jobs uv run #3303

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

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion docs/source/en/guides/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -753,10 +753,16 @@ Run UV scripts (Python scripts with inline dependencies) on HF infrastructure:
>>> hf jobs uv run ml_training.py --flavor gpu-t4-small

# Pass arguments to script
>>> hf jobs uv run process.py input.csv output.parquet --repo data-scripts
>>> hf jobs uv run process.py input.csv output.parquet

# Add dependencies
>>> hf jobs uv run --with transformers --with torch train.py

# Run a script directly from a URL
>>> hf jobs uv run https://huggingface.co/datasets/username/scripts/resolve/main/example.py

# Run a command
>>> hf jobs uv run --with lighteval python -c "import lighteval"
```

UV scripts are Python scripts that include their dependencies directly in the file using a special comment syntax. This makes them perfect for self-contained tasks that don't require complex project setups. Learn more about UV scripts in the [UV documentation](https://docs.astral.sh/uv/guides/scripts/).
3 changes: 3 additions & 0 deletions docs/source/en/guides/jobs.md
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,9 @@ Run UV scripts (Python scripts with inline dependencies) on HF infrastructure:

# Run a script directly from a URL
>>> run_uv_job("https://huggingface.co/datasets/username/scripts/resolve/main/example.py")

# Run a command
>>> run_uv_job("python", script_args=["-c", "import lighteval"], dependencies=["lighteval"])
```

UV scripts are Python scripts that include their dependencies directly in the file using a special comment syntax. This makes them perfect for self-contained tasks that don't require complex project setups. Learn more about UV scripts in the [UV documentation](https://docs.astral.sh/uv/guides/scripts/).
31 changes: 26 additions & 5 deletions src/huggingface_hub/hf_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -10286,10 +10286,10 @@ def run_uv_job(

Args:
script (`str`):
Path or URL of the UV script.
Path or URL of the UV script, or a command.

script_args (`List[str]`, *optional*)
Arguments to pass to the script.
Arguments to pass to the script or command.

dependencies (`List[str]`, *optional*)
Dependencies to use to run the UV script.
Expand Down Expand Up @@ -10324,10 +10324,31 @@ def run_uv_job(

Example:

Run a script from a URL:

```python
>>> from huggingface_hub import run_uv_job
>>> script = "https://raw.githubusercontent.com/huggingface/trl/refs/heads/main/trl/scripts/sft.py"
>>> run_uv_job(script, dependencies=["trl"], flavor="a10g-small")
>>> script_args = ["--model_name_or_path", "Qwen/Qwen2-0.5B", "--dataset_name", "trl-lib/Capybara", "--push_to_hub"]
>>> run_uv_job(script, script_args=script_args, dependencies=["trl"], flavor="a10g-small")
```

Run a local script:

```python
>>> from huggingface_hub import run_uv_job
>>> script = "my_sft.py"
>>> script_args = ["--model_name_or_path", "Qwen/Qwen2-0.5B", "--dataset_name", "trl-lib/Capybara", "--push_to_hub"]
>>> run_uv_job(script, script_args=script_args, dependencies=["trl"], flavor="a10g-small")
```

Run a command:

```python
>>> from huggingface_hub import run_uv_job
>>> script = "lighteval"
>>> script_args= ["endpoint", "inference-providers", "model_name=openai/gpt-oss-20b,provider=auto", "lighteval|gsm8k|0|0"]
>>> run_uv_job(script, script_args=script_args, dependencies=["lighteval"], flavor="a10g-small")
```
"""
image = image or "ghcr.io/astral-sh/uv:python3.12-bookworm"
Expand All @@ -10346,8 +10367,8 @@ def run_uv_job(
if namespace is None:
namespace = self.whoami(token=token)["name"]

if script.startswith("http://") or script.startswith("https://"):
# Direct URL execution - no upload needed
if script.startswith("http://") or script.startswith("https://") or not script.endswith(".py"):
# Direct URL execution or command - no upload needed
command = ["uv", "run"] + uv_args + [script] + script_args
else:
# Local file - upload to HF
Expand Down
74 changes: 71 additions & 3 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

from huggingface_hub.cli.cache import CacheCommand
from huggingface_hub.cli.download import DownloadCommand
from huggingface_hub.cli.jobs import JobsCommands, RunCommand
from huggingface_hub.cli.jobs import JobsCommands, RunCommand, UvCommand
from huggingface_hub.cli.repo import RepoCommands
from huggingface_hub.cli.repo_files import DeleteFilesSubCommand, RepoFilesCommand
from huggingface_hub.cli.upload import UploadCommand
Expand Down Expand Up @@ -848,7 +848,7 @@ def setUp(self) -> None:
commands_parser = self.parser.add_subparsers()
JobsCommands.register_subcommand(commands_parser)

@patch(
patch_requests_post = patch(
"requests.Session.post",
return_value=DummyResponse(
{
Expand All @@ -862,7 +862,13 @@ def setUp(self) -> None:
}
),
)
@patch("huggingface_hub.hf_api.HfApi.whoami", return_value={"name": "my-username"})
patch_whoami = patch("huggingface_hub.hf_api.HfApi.whoami", return_value={"name": "my-username"})
patch_get_token = patch("huggingface_hub.hf_api.get_token", return_value="hf_xxx")
patch_repo_info = patch("huggingface_hub.hf_api.HfApi.repo_info")
patch_upload_file = patch("huggingface_hub.hf_api.HfApi.upload_file")

@patch_requests_post
@patch_whoami
def test_run(self, whoami: Mock, requests_post: Mock) -> None:
input_args = ["jobs", "run", "--detach", "ubuntu", "echo", "hello"]
cmd = RunCommand(self.parser.parse_args(input_args))
Expand All @@ -877,3 +883,65 @@ def test_run(self, whoami: Mock, requests_post: Mock) -> None:
"flavor": "cpu-basic",
"dockerImage": "ubuntu",
}

@patch_requests_post
@patch_whoami
def test_uv_command(self, whoami: Mock, requests_post: Mock) -> None:
input_args = ["jobs", "uv", "run", "--detach", "echo", "hello"]
cmd = UvCommand(self.parser.parse_args(input_args))
cmd.run()
assert requests_post.call_count == 1
args, kwargs = requests_post.call_args_list[0]
assert args == ("https://huggingface.co/api/jobs/my-username",)
assert kwargs["json"] == {
"command": ["uv", "run", "echo", "hello"],
"arguments": [],
"environment": {},
"flavor": "cpu-basic",
"dockerImage": "ghcr.io/astral-sh/uv:python3.12-bookworm",
}

@patch_requests_post
@patch_whoami
def test_uv_remote_script(self, whoami: Mock, requests_post: Mock) -> None:
input_args = ["jobs", "uv", "run", "--detach", "https://.../script.py"]
cmd = UvCommand(self.parser.parse_args(input_args))
cmd.run()
assert requests_post.call_count == 1
args, kwargs = requests_post.call_args_list[0]
assert args == ("https://huggingface.co/api/jobs/my-username",)
assert kwargs["json"] == {
"command": ["uv", "run", "https://.../script.py"],
"arguments": [],
"environment": {},
"flavor": "cpu-basic",
"dockerImage": "ghcr.io/astral-sh/uv:python3.12-bookworm",
}

@patch_requests_post
@patch_whoami
@patch_get_token
@patch_repo_info
@patch_upload_file
def test_uv_local_script(
self, upload_file: Mock, repo_info: Mock, get_token: Mock, whoami: Mock, requests_post: Mock
) -> None:
input_args = ["jobs", "uv", "run", "--detach", __file__]
cmd = UvCommand(self.parser.parse_args(input_args))
cmd.run()
assert requests_post.call_count == 1
args, kwargs = requests_post.call_args_list[0]
assert args == ("https://huggingface.co/api/jobs/my-username",)
command = kwargs["json"].pop("command")
assert "UV_SCRIPT_URL" in " ".join(command)
assert kwargs["json"] == {
"arguments": [],
"environment": {
"UV_SCRIPT_URL": "https://huggingface.co/datasets/my-username/hf-cli-jobs-uv-run-scripts/resolve/main/test_cli.py"
},
"secrets": {"UV_SCRIPT_HF_TOKEN": "hf_xxx"},
"flavor": "cpu-basic",
"dockerImage": "ghcr.io/astral-sh/uv:python3.12-bookworm",
}
assert repo_info.call_count == 1 # check if repo exists
assert upload_file.call_count == 2 # script and readme
Loading