Repository navigation
[Feat] Per Stage Runtime Env #1623
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
Changes from 6 commits
5271c7c
9d1fd4e
b3624fc
e2ae9a2
b25df9f
eaaf1d4
e243755
98470ca
b7b2351
fc8f510
2666b24
f4ef209
e971625
fab1b90
e89badb
8062427
a60e271
297b8e4
d88d678
dcf7143
7164dd0
beb11ac
952e77f
a882ad4
6792826
00dd3b9
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 |
|---|---|---|
|
|
@@ -19,7 +19,7 @@ | |
| import time | ||
| from abc import ABC, ABCMeta, abstractmethod | ||
| from inspect import isabstract | ||
| from typing import TYPE_CHECKING, Any, Generic, TypeVar, final | ||
| from typing import TYPE_CHECKING, Any, ClassVar, Generic, TypeVar, final | ||
|
|
||
| from loguru import logger | ||
|
|
||
|
|
@@ -85,6 +85,7 @@ class ProcessingStage(ABC, Generic[X, Y], metaclass=StageMeta): | |
| name = "ProcessingStage" | ||
| resources = Resources(cpus=1.0) | ||
| batch_size = 1 | ||
| pip_specs: ClassVar[list[str] | None] = None | ||
|
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. Whether we go ahead with this or runtime_env, do you think we need to support |
||
|
|
||
| @property | ||
| @final | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,140 @@ | ||
| # Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved. | ||
| # | ||
| # Licensed under the Apache License, Version 2.0 (the "License"); | ||
| # you may not use this file except in compliance with the License. | ||
| # You may obtain a copy of the License at | ||
| # | ||
| # http://www.apache.org/licenses/LICENSE-2.0 | ||
| # | ||
| # Unless required by applicable law or agreed to in writing, software | ||
| # distributed under the License is distributed on an "AS IS" BASIS, | ||
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| # See the License for the specific language governing permissions and | ||
| # limitations under the License. | ||
|
|
||
| """Resolve per-stage pip_specs into virtualenvs and set _resolved_site_packages_path on stages. | ||
|
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. I'm intrigued about this design choice. Is there a reason to not go with
Contributor
Author
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. I have refactored it so we now use runtime_env instead |
||
|
|
||
| Uses the `uv` CLI to create venvs (no Python dependency on uv). When a stage defines | ||
| pip_specs (e.g. ["vllm==0.6.0"]), the resolver creates a venv with those packages | ||
| and sets the stage's _resolved_site_packages_path so executors can inject PYTHONPATH. | ||
| """ | ||
|
|
||
| import atexit | ||
| import shutil | ||
| import sysconfig | ||
| from pathlib import Path | ||
| from typing import TYPE_CHECKING | ||
|
|
||
| from loguru import logger | ||
|
|
||
| if TYPE_CHECKING: | ||
| from nemo_curator.stages.base import ProcessingStage | ||
|
|
||
|
|
||
| def _site_packages_for_venv(venv_dir: Path) -> Path: | ||
|
suiyoubi marked this conversation as resolved.
Outdated
|
||
| # Windows uses "Lib" (capital), Unix uses "lib" | ||
| for lib_name in ("lib", "Lib"): | ||
| lib = venv_dir / lib_name | ||
| if not lib.exists(): | ||
| continue | ||
| # On Windows, site-packages may live directly under Lib\ (no python3.x subdir) | ||
| site_direct = lib / "site-packages" | ||
| if site_direct.exists(): | ||
| return site_direct.resolve() | ||
| for p in lib.iterdir(): | ||
| if p.is_dir() and p.name.startswith("python"): | ||
| site = p / "site-packages" | ||
| if site.exists(): | ||
| return site.resolve() | ||
| msg = f"no site-packages found under {venv_dir}" | ||
| raise RuntimeError(msg) | ||
|
|
||
|
|
||
| def _venv_python_exe(venv_path: Path) -> Path: | ||
|
suiyoubi marked this conversation as resolved.
Outdated
|
||
| """Path to the venv Python interpreter (portable: Windows Scripts/python.exe, Unix bin/python).""" | ||
| if sysconfig.get_platform().startswith("win"): | ||
| return venv_path / "Scripts" / "python.exe" | ||
| return venv_path / "bin" / "python" | ||
|
|
||
|
|
||
| def _create_venv_for_specs( | ||
|
suiyoubi marked this conversation as resolved.
Outdated
|
||
| specs: list[str], base_dir: Path, subdir_name: str, uv_exe: str | ||
| ) -> Path: | ||
| import subprocess | ||
|
|
||
| venv_root = base_dir / subdir_name | ||
| venv_root.mkdir(parents=True, exist_ok=True) | ||
| venv_path = venv_root / ".venv" | ||
| python_path = _venv_python_exe(venv_path) | ||
| subprocess.run( # noqa: S603 | ||
| [uv_exe, "venv", str(venv_path)], | ||
| check=True, | ||
| capture_output=True, | ||
| text=True, | ||
| ) | ||
| # specs come from stage pip_specs (pipeline author configuration) | ||
| subprocess.run( # noqa: S603 | ||
| [uv_exe, "pip", "install", "--python", str(python_path), *specs], | ||
| check=True, | ||
| capture_output=True, | ||
| text=True, | ||
| ) | ||
|
suiyoubi marked this conversation as resolved.
Outdated
|
||
| return _site_packages_for_venv(venv_path) | ||
|
|
||
|
|
||
| def resolve_stage_pip_envs( | ||
| stages: list["ProcessingStage"], | ||
| base_dir: Path | None = None, | ||
| ) -> None: | ||
| """Create venvs for stages that have pip_specs and set _resolved_site_packages_path. | ||
|
|
||
| Stages with the same pip_specs (order-independent) share one venv. Uses the `uv` | ||
| CLI; must be on PATH. Modifies each stage instance in place. | ||
|
|
||
| Args: | ||
| stages: Flat list of execution stages (e.g. after pipeline build). | ||
| base_dir: Directory for venv roots. If None, a temp directory is used. | ||
| """ | ||
| import subprocess | ||
| import tempfile | ||
|
|
||
| uv_exe = shutil.which("uv") | ||
| if not uv_exe: | ||
| logger.warning( | ||
| "resolve_stage_pip_envs requires `uv` on PATH; skipping.", | ||
| ) | ||
| return | ||
|
suiyoubi marked this conversation as resolved.
Outdated
|
||
| try: | ||
| subprocess.run( # noqa: S603 | ||
| [uv_exe, "--version"], | ||
| check=True, | ||
| capture_output=True, | ||
| text=True, | ||
| ) | ||
|
suiyoubi marked this conversation as resolved.
Outdated
|
||
| except subprocess.CalledProcessError as e: | ||
| logger.warning( | ||
| "resolve_stage_pip_envs: uv check failed; skipping. Error: %s", | ||
| e, | ||
| ) | ||
| return | ||
|
|
||
| if base_dir is None: | ||
| tmp_dir = tempfile.mkdtemp(prefix="curator_pip_envs_") | ||
| atexit.register(shutil.rmtree, tmp_dir, ignore_errors=True) | ||
| base = Path(tmp_dir) | ||
| else: | ||
| base = Path(base_dir) | ||
| base.mkdir(parents=True, exist_ok=True) | ||
|
suiyoubi marked this conversation as resolved.
Outdated
|
||
|
|
||
| # Dedupe: same sorted specs -> one venv | ||
| unique_specs: dict[tuple[str, ...], Path] = {} | ||
| for stage in stages: | ||
| specs = getattr(stage, "pip_specs", None) | ||
| if not specs or not isinstance(specs, list): | ||
| continue | ||
| key = tuple(sorted(specs)) | ||
|
suiyoubi marked this conversation as resolved.
Outdated
|
||
| if key not in unique_specs: | ||
| subdir = f"env_{len(unique_specs)}" | ||
| unique_specs[key] = _create_venv_for_specs(list(specs), base, subdir, uv_exe) | ||
| logger.info("Created venv for pip_specs %s at %s", list(key), unique_specs[key]) | ||
| stage._resolved_site_packages_path = unique_specs[key] | ||
|
suiyoubi marked this conversation as resolved.
Outdated
suiyoubi marked this conversation as resolved.
Outdated
|
||
Uh oh!
There was an error while loading. Please reload this page.