Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
f0eb8d0
Add nemo-skills-core subpackage for lightweight installs
gwarmstrong Feb 13, 2026
9adc401
Merge branch 'main' into georgea/refactor-separable-pipeline
gwarmstrong Feb 13, 2026
0a4f056
Suggestions from code review addressed
gwarmstrong Feb 13, 2026
e2361e6
Revert sentinel string back to None in register_evaluator
gwarmstrong Feb 13, 2026
cc70501
Fix extra_datasets silently ignored when cluster_config is None
gwarmstrong Feb 13, 2026
a0a2aa7
Add all benchmark evaluator deps to core/requirements.txt
gwarmstrong Feb 14, 2026
7cf5fb6
Eliminate requirements/main.txt duplication
gwarmstrong Feb 14, 2026
9e84d39
Add tests/test_dependency_isolation.py
gwarmstrong Feb 14, 2026
ae191da
Revise CONTRIBUTING.md dependency boundary guidance
gwarmstrong Feb 14, 2026
8118b5a
Simplify pipeline/dataset.py to eliminate local logic duplication
gwarmstrong Feb 14, 2026
fdcfdb1
Remove section comments from core/requirements.txt
gwarmstrong Feb 17, 2026
49aed5e
Remove summarize-results note from CONTRIBUTING.md
gwarmstrong Feb 17, 2026
d1c4195
Properly eliminate duplicated import logic from pipeline/dataset.py
gwarmstrong Feb 17, 2026
b7e258f
Simplify pipeline/dataset.py to a single thin function
gwarmstrong Feb 17, 2026
c5d12cf
Move wandb from pipeline to core
gwarmstrong Feb 17, 2026
3b025ec
Fix Dockerfile.nemo-skills to use core + pipeline requirements
gwarmstrong Feb 17, 2026
836160f
fix: use nemo_run import guard instead of package metadata
gwarmstrong Feb 18, 2026
cf4b94c
fix: remove huggingface_hub<1 pin from BFCL requirements
gwarmstrong Feb 19, 2026
6255b1a
address feedback from code review
gwarmstrong Feb 20, 2026
b76b8bd
add dependency boundary guide and requirements symlink
gwarmstrong Feb 20, 2026
49f3cde
Merge remote-tracking branch 'origin/main' into georgea/refactor-sepa…
gwarmstrong Feb 20, 2026
305cde7
maint: add back core dependency guidelines and adjust links
gwarmstrong Feb 20, 2026
28de048
fix dataset resolution logic after merge
gwarmstrong Feb 20, 2026
ded4a2c
remove boundary arch mention completely
gwarmstrong Feb 20, 2026
d173121
fix: use importlib for hyphenated module names in isolation test
gwarmstrong Feb 20, 2026
d91add1
Merge origin/main: add critpt/dsbench evaluators and pandas deps
gwarmstrong Feb 24, 2026
97bac87
fix: add torchcodec to core requirements for numb3rs dataset
gwarmstrong Feb 24, 2026
cc4118c
bump CACHEBUST to rebuild container with torchcodec
gwarmstrong Feb 24, 2026
1c0eb66
Merge branch 'main' into georgea/refactor-separable-pipeline
gwarmstrong Feb 25, 2026
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
2 changes: 2 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ jobs:
with:
python-version: "3.10"
cache: pip
- name: Install uv

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we use it anywhere?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good catch, it was from an old testing strategy, removed

uses: astral-sh/setup-uv@v4
- name: Install dependencies
run: |
python -m pip install --upgrade pip
Expand Down
43 changes: 43 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,49 @@ The following things are required when adding new benchmarks
the dataset into slurm tests. This is the most comprehensive test we can do by running full
evaluation on cluster with arbitrary model and check that results are as expected.

### Respect the Core / Pipeline dependency boundary
Comment thread
Kipok marked this conversation as resolved.

NeMo Skills is split into **Core** (agent runtime) and **Pipeline** (orchestration). The rule is simple:

```
Pipeline can import from Core.
Core CANNOT import from Pipeline.
```

Core modules are everything under `nemo_skills/` **except** `nemo_skills/pipeline/`. They must never have top-level imports from `nemo_skills.pipeline` or `nemo_run`. This boundary is enforced by `tests/test_dependency_isolation.py` which verifies that core modules import successfully without pipeline dependencies installed.

**When adding a new dependency**, put it in the right requirements file:

| If the dependency is needed for... | Add it to |
|---|---|
| Inference, evaluation, math/code grading, MCP clients, prompt formatting | `core/requirements.txt` |
| CLI commands, cluster orchestration, experiment tracking | `requirements/pipeline.txt` |
| Everything else (dataset-specific deps, benchmark-specific packages) | `requirements/main.txt` only |

Dependencies in `core/requirements.txt` should be things that a typical `GenerationTask` run with PythonTool would need. Dataset-specific or benchmark-specific packages (e.g., `faiss-cpu`, `sacrebleu`, `func-timeout`) go only in `requirements/main.txt`.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this part I don't fully understand - I think benchmark-specific packages should go to core for now as otherwise the code will fail when those benchmarks are used e.g. in evaluator. Eventually we should migrate to jit install, but it's not done yet, so I'd put those into core

@gwarmstrong gwarmstrong Feb 14, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair. My original scope was pretty PythonTool specific, but I think we can come up with something that makes a little more sense in terms of aligning the core code with core dependencies.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah it's now in core and there is a clearer description of what's in pipeline vs core


All core and pipeline deps must also appear in `requirements/main.txt` (the monolithic file used for default installs).

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we not link multiple requirements listed in pyproject.toml? We duplicate?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should be able to do that. It should be implemented that way now--I updated this at one point so it links against the file rather than duplicating. Will fix.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done


**Examples of correct placement:**

- `httpx` -> `core/requirements.txt` (used by model inference clients)
- `sympy` -> `core/requirements.txt` (used by math graders)
Comment thread
Kipok marked this conversation as resolved.
Outdated
- `nemo_run` -> `requirements/pipeline.txt` (cluster job orchestration)
- `wandb` -> `requirements/pipeline.txt` (experiment tracking for cluster jobs)
- `faiss-cpu` -> `requirements/main.txt` only (only needed for BFCL benchmark)

**Examples of mistakes to avoid:**

- Adding `nemo_run` to `core/requirements.txt` -- it is a pipeline/orchestration dependency, core must not depend on it.
- Adding `typer` to `core/requirements.txt` -- it is the CLI framework, only used by the pipeline layer.
- Adding a new dependency only to a subpackage file but forgetting `requirements/main.txt` -- the default install would be missing it.

**When writing new core code:**

- If you need something from `nemo_skills.pipeline`, your code probably belongs in pipeline, not core. Move it.
- If you have a function that works locally but *also* needs a cluster variant, put the local version in core and a cluster-aware wrapper in `nemo_skills/pipeline/` (see `pipeline/dataset.py` for the pattern).

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually think that if we have a case like this, it means we need to redesign something. Ideally separation should be clean, and we shouldn't need to duplicate functionality. E.g. the dataset module part is a bit messy and there is probably a way to do it better, such that there is a pipeline level that only manages pulling from cluster and then there is a local level that always assumes things are present locally and is being called inside pipeline directly

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense, updated the docs here to reflect that and made the implementation more consistent with the guidance here/there.

- If you absolutely must reference pipeline code from core for backwards compatibility, use a lazy import inside a function body with a `DeprecationWarning` (see `dataset/utils.py:get_dataset_module` for the pattern). Never add a top-level import.

### Keep the code elegant
When adding new features, try to keep the code simple and elegant.
- Can you reuse / extend an existing functionality?
Expand Down
53 changes: 53 additions & 0 deletions core/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# 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.

[build-system]
requires = [
"setuptools",
"wheel"
]
build-backend = "setuptools.build_meta"

[project]
dynamic = ["version", "dependencies"]

name = "nemo-skills-core"
description = "NeMo Skills core runtime -- inference, evaluation, and tool calling"
readme = {text = "NeMo Skills core runtime for inference, evaluation, and tool calling. See https://nvidia-nemo.github.io/Skills for full documentation.", content-type = "text/plain"}
classifiers = [
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"License :: OSI Approved :: Apache Software License",
"Operating System :: OS Independent",
]
requires-python = ">=3.10"

[project.urls]
homepage = "https://nvidia-nemo.github.io/Skills"
source = "https://github.com/NVIDIA-NeMo/Skills"
issues = "https://github.com/NVIDIA-NeMo/Skills/issues"

[project.scripts]
ns = "nemo_skills._cli_stub:main"

[tool.setuptools]
include-package-data = true

[tool.setuptools.packages.find]
where = [".."]
exclude = ["tests", "tests.*", "core", "core.*"]

[tool.setuptools.dynamic]
version = { attr = "nemo_skills.version.__version__" }
dependencies = {file = ["requirements.txt"]}
27 changes: 27 additions & 0 deletions core/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Core dependencies for agent runtime (inference, evaluation, tool calling, math/code grading).
# No cluster orchestration deps (nemo_run, typer, etc.)


# --- code evaluation ---

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are you sure this covers all benchmarks? Generally, we should move to keeping this reqs really simple and move most benchmark-specific requirements to install at runtime, but for now probably we might need some more packages here? E.g. datasets is almost certainly needed and then other benchmark specific things, like sacrebleu, etc.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I revisited the separation. This should contain all the reqs not needed for cluster orchestration now.

compute-eval @ git+https://github.com/NVIDIA/compute-eval.git@2d14770
evalplus @ git+https://github.com/evalplus/evalplus@c91370f
fire
flask
# --- agent runtime ---
httpx
huggingface_hub
hydra-core
ipython
litellm[caching]

# --- math evaluation ---
math-verify[antlr4_9_3]
mcp
numpy
openai
pyyaml
requests
rich
sympy
tqdm
transformers
74 changes: 74 additions & 0 deletions docs/basics/installation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Installation & Dependency Groups

NeMo Skills provides two installable packages:

- **`nemo-skills`** (root) -- full install with CLI, cluster orchestration, all benchmarks
- **`nemo-skills-core`** (`core/` subdirectory) -- lightweight runtime only

## Default installation

`pip install nemo-skills` gives you **everything** (inference, evaluation, CLI,
cluster orchestration, benchmarks):

```bash
pip install git+https://github.com/NVIDIA-NeMo/Skills.git
# or, from a local clone:
pip install -e .
```

## Lightweight installation

If you only need inference, evaluation, and tool calling (no cluster orchestration):

```bash
pip install "nemo-skills-core @ git+https://github.com/NVIDIA-NeMo/Skills.git#subdirectory=core"
# or, from a local clone:
pip install -e core/
```

## Extras (dependency groups)

| Extra | Requirements file | What it provides |
|-------|-------------------|------------------|
| `core` | `core/requirements.txt` | Agent runtime: inference, evaluation, tool calling (MCP), prompt formatting, math/code grading. No cluster orchestration. |
| `pipeline` | `requirements/pipeline.txt` | CLI (`ns` command), cluster management, experiment tracking (`nemo_run`, `typer`, `wandb`). |
| `dev` | `requirements/common-tests.txt`, `requirements/common-dev.txt` | Development and testing tools (`pytest`, `ruff`, `pre-commit`). |

### Examples

```bash
# Full install (default)
pip install -e .

# Core only -- lightweight runtime for downstream integrations
pip install -e core/

# Development (everything + dev tools)
pip install -e ".[dev]"
```

## Core / Pipeline architecture boundary

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd maybe move this part somewhere else (e.g. only keep in contributing.md or better in a new .md where extra details from contributing can go as well). It's helpful, but probably a bit too dense for the "basics" part of the docs. More oriented towards people who'd need to modify our code

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

okay, put it in the core/README.md


The codebase enforces a one-way dependency rule: pipeline modules can import
from core, but core modules must not import from pipeline.

This boundary is enforced by `tests/test_dependency_isolation.py` which creates
Comment thread
Kipok marked this conversation as resolved.
Outdated
fresh virtualenvs and verifies that core modules import successfully without

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this true? I don't think we create fresh envs in tests?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah this is outdated from old tests, good cattch

pipeline dependencies installed.

### Dataset loading example

The boundary shows up concretely in dataset loading:

```python
# Core: local-only dataset loading (no cluster deps)
from nemo_skills.dataset.utils import get_dataset_module
module, path, on_cluster = get_dataset_module("gsm8k")

# Pipeline: cluster-aware dataset loading (SSH tunnels, mount resolution)
from nemo_skills.pipeline.dataset import get_dataset_module
module, path, on_cluster = get_dataset_module("gsm8k", cluster_config=cfg)
```

If you pass `cluster_config` to the core version, it will emit a
`DeprecationWarning` and redirect to the pipeline version.
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ nav:
- Nemo-Skills: index.md
- Getting started:
- basics/index.md
- Installation & Dependencies: basics/installation.md
- Cluster configs: basics/cluster-configs.md
- Code packaging: basics/code-packaging.md
- Prompt format: basics/prompt-format.md
Expand Down
20 changes: 20 additions & 0 deletions nemo_skills/_cli_stub.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# 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.

import sys


def main():
print("nemo-skills-core is installed (lightweight mode).\nFor the full ns CLI, run: pip install nemo-skills")
sys.exit(1)
119 changes: 53 additions & 66 deletions nemo_skills/dataset/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,15 @@
import json
import os
import sys
import tempfile
import time
import urllib.request
import warnings
from enum import Enum
from pathlib import Path
from typing import Dict
from urllib.error import URLError

from nemo_skills.evaluation.math_grader import extract_answer
from nemo_skills.pipeline.utils import cluster_download_file, get_unmounted_path


@contextlib.contextmanager
Expand Down Expand Up @@ -72,80 +71,68 @@ class ExtraDatasetType(str, Enum):
cluster = "cluster"


def _get_dataset_module_from_cluster(cluster_config, mounted_path):
# getting tmp path to download init.py
with tempfile.TemporaryDirectory() as tmpdir:
tmp_path = str(Path(tmpdir) / "init.py")
cluster_dataset_path = get_unmounted_path(cluster_config, mounted_path)
try:
cluster_download_file(cluster_config, cluster_dataset_path, tmp_path)
except FileNotFoundError:
raise RuntimeError(
f"Init file {mounted_path} not found on the cluster. "
f"Please check the dataset name you're using. Did you forget to run prepare data commands?"
)
return import_from_path(tmp_path)


def get_default_dataset_module(dataset, data_dir=None, cluster_config=None):
is_on_cluster = False
if data_dir is None:
data_path = "/nemo_run/code/nemo_skills/dataset"
dataset_module = importlib.import_module(f"nemo_skills.dataset.{dataset}")
else:
data_path = data_dir
if cluster_config is None or cluster_config["executor"] == "none":
with add_to_path(data_dir):
dataset_module = importlib.import_module(dataset)
else:
if cluster_config["executor"] == "local":
with add_to_path(get_unmounted_path(cluster_config, data_dir)):
dataset_module = importlib.import_module(dataset)
else:
dataset = dataset.replace(".", "/")
dataset_module = _get_dataset_module_from_cluster(cluster_config, f"{data_dir}/{dataset}/__init__.py")
is_on_cluster = True
return dataset_module, data_path, is_on_cluster


def get_dataset_module(dataset, data_dir=None, cluster_config=None, extra_datasets=None, extra_datasets_type=None):
"""
Get dataset module either in default folder or in extra datasets folder.

If cluster_config is provided, the data_dir will be resolved as a mounted
path and appropriately downloaded from the cluster.
"""Load dataset module from the local filesystem.

Same will be done for extra_datasets if extra_datasets_type is cluster.
For loading datasets from a remote cluster, use
nemo_skills.pipeline.dataset.get_dataset_module instead.

Search priority:
1. data_dir (or `nemo_skills.dataset` if None) folder
3. extra_datasets parameter if defined
4. `NEMO_SKILLS_EXTRA_DATASETS` environment variable
1. data_dir (or ``nemo_skills.dataset`` if None) folder
2. extra_datasets parameter if defined
3. ``NEMO_SKILLS_EXTRA_DATASETS`` environment variable

Args:
dataset: Name of the dataset (e.g., "gsm8k", "math").
data_dir: Optional custom directory containing dataset module.
cluster_config: Deprecated. Use nemo_skills.pipeline.dataset.get_dataset_module for cluster support.
extra_datasets: Optional path to extra datasets.
extra_datasets_type: Type of extra datasets location ("local" or "cluster").

Returns:
Tuple of (dataset_module, data_path, is_on_cluster).
"""
if cluster_config is not None:
warnings.warn(
"Passing cluster_config to nemo_skills.dataset.utils.get_dataset_module is deprecated. "
"Use nemo_skills.pipeline.dataset.get_dataset_module instead.",
DeprecationWarning,
stacklevel=2,
)
from nemo_skills.pipeline.dataset import get_dataset_module as _pipeline_get_dataset_module

return _pipeline_get_dataset_module(dataset, data_dir, cluster_config, extra_datasets, extra_datasets_type)

# Local-only loading
try:
dataset_module, data_path, is_on_cluster = get_default_dataset_module(dataset, data_dir, cluster_config)
if data_dir is None:
data_path = "/nemo_run/code/nemo_skills/dataset"
dataset_module = importlib.import_module(f"nemo_skills.dataset.{dataset}")
else:
data_path = data_dir
with add_to_path(data_dir):
dataset_module = importlib.import_module(dataset)
return dataset_module, data_path, False
except ModuleNotFoundError:
try:
dataset = dataset.replace(".", "/")
extra_datasets = extra_datasets or os.environ.get("NEMO_SKILLS_EXTRA_DATASETS")
is_on_cluster = False
data_path = extra_datasets
if extra_datasets is None:
raise RuntimeError(f"Dataset {dataset} not found in {data_dir if data_dir else 'nemo_skills.dataset'}")
if extra_datasets_type == ExtraDatasetType.local:
with add_to_path(extra_datasets):
dataset = dataset.replace(".", "/")
extra_datasets = extra_datasets or os.environ.get("NEMO_SKILLS_EXTRA_DATASETS")
if extra_datasets is None:
raise RuntimeError(f"Dataset {dataset} not found in {data_dir if data_dir else 'nemo_skills.dataset'}")
if extra_datasets_type == ExtraDatasetType.local or extra_datasets_type is None:
with add_to_path(extra_datasets):
try:
dataset_module = importlib.import_module(dataset)
else:
dataset_module = _get_dataset_module_from_cluster(
cluster_config, f'{extra_datasets}/{dataset}/"__init__.py"'
)
is_on_cluster = True
except ModuleNotFoundError:
except ModuleNotFoundError:
raise RuntimeError(
f"Dataset {dataset} not found in any of the searched locations: "
f"{data_dir if data_dir else 'nemo_skills.dataset'}, {extra_datasets}"
)
return dataset_module, extra_datasets, False
else:
raise RuntimeError(
f"Dataset {dataset} not found in any of the searched locations: "
f"{data_dir if data_dir else 'nemo_skills.dataset'}, {extra_datasets}"
f"Cluster-based extra_datasets_type='{extra_datasets_type}' requires cluster_config. "
"Use nemo_skills.pipeline.dataset.get_dataset_module instead."
)
return dataset_module, data_path, is_on_cluster


def get_lean4_header():
Expand Down
Loading