-
Notifications
You must be signed in to change notification settings - Fork 615
Feature/optimizer module and registry #1614
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 all commits
4a01d3e
66b7f54
6949d01
ecd465c
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 |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| # nvidia-nat-config-optimizer | ||
|
|
||
| Workflow config and prompt optimization for [NVIDIA NeMo Agent Toolkit](https://github.com/NVIDIA/NeMo-Agent-Toolkit). Provides genetic-algorithm and numeric (Optuna) optimizers for workflow configs and prompts. Scoped to config-level optimization (hyperparameters, prompts); excludes runtime/inference optimizations. | ||
|
|
||
| Install with NAT: `pip install nvidia-nat` (config optimizer is included by default), or `pip install nvidia-nat-core nvidia-nat-config-optimizer`. | ||
|
|
||
| Config-optimizer-only (minimal deps): `pip install nvidia-nat-config-optimizer` (requires `nvidia-nat-core` for eval contracts). | ||
|
|
||
| ## Development / testing | ||
|
|
||
| From **repo root** (install test deps, then run optimizer tests): | ||
|
|
||
| ```bash | ||
| uv sync --extra test | ||
| uv run pytest packages/nvidia_nat_config_optimizer/tests/ -v | ||
| ``` | ||
|
|
||
| For Pareto/visualization tests (matplotlib), install the optimizer with the visualization extra first: | ||
|
|
||
| ```bash | ||
| cd packages/nvidia_nat_config_optimizer && uv sync --extra test --extra visualization && uv run pytest tests/ -v | ||
| ``` | ||
|
|
||
| Or run the full repo test suite (all packages): `python ci/scripts/run_tests.py` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,82 @@ | ||
| # SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. | ||
| # All rights reserved. | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
| # | ||
| # 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] | ||
| build-backend = "setuptools.build_meta" | ||
| requires = ["setuptools>=64", "setuptools-scm>=8", "setuptools_dynamic_dependencies>=1.0.0"] | ||
|
|
||
| [tool.setuptools.packages.find] | ||
| where = ["src"] | ||
| include = ["nat.*"] | ||
|
|
||
| [tool.setuptools_scm] | ||
| git_describe_command = "git describe --long --first-parent" | ||
| root = "../.." | ||
|
|
||
| [project] | ||
| name = "nvidia-nat-config-optimizer" | ||
| dynamic = ["version", "dependencies", "optional-dependencies"] | ||
| requires-python = ">=3.11,<3.14" | ||
| description = "Workflow config and prompt optimizer for NVIDIA NeMo Agent Toolkit" | ||
| readme = "README.md" | ||
| license = { text = "Apache-2.0" } | ||
| keywords = ["ai", "rag", "agents", "optimization"] | ||
| classifiers = [ | ||
| "Programming Language :: Python", | ||
| "Programming Language :: Python :: 3.11", | ||
| "Programming Language :: Python :: 3.12", | ||
| "Programming Language :: Python :: 3.13", | ||
| ] | ||
| authors = [{ name = "NVIDIA Corporation" }] | ||
| maintainers = [{ name = "NVIDIA Corporation" }] | ||
|
|
||
| [project.urls] | ||
| documentation = "https://docs.nvidia.com/nemo/agent-toolkit/latest/" | ||
| source = "https://github.com/NVIDIA/NeMo-Agent-Toolkit" | ||
|
|
||
| [tool.setuptools_dynamic_dependencies] | ||
| dependencies = [ | ||
| "nvidia-nat-core == {version}", | ||
| "numpy~=2.3", | ||
| "optuna~=4.4.0", | ||
| "pandas~=2.2", | ||
| "pydantic~=2.11", | ||
| "PyYAML~=6.0", | ||
| ] | ||
|
|
||
| [tool.setuptools_dynamic_dependencies.optional-dependencies] | ||
| visualization = [ | ||
| "matplotlib~=3.9", | ||
| ] | ||
| test = [ | ||
| "nvidia-nat-test == {version}", | ||
| ] | ||
|
|
||
| [tool.uv] | ||
| build-constraint-dependencies = ["setuptools>=64", "setuptools-scm>=8", "setuptools_dynamic_dependencies>=1.0.0"] | ||
| managed = true | ||
| config-settings = { editable_mode = "compat" } | ||
|
|
||
| [tool.setuptools] | ||
| include-package-data = true | ||
|
|
||
| [tool.pytest.ini_options] | ||
| testpaths = ["tests"] | ||
| asyncio_mode = "auto" | ||
|
|
||
| [tool.uv.sources] | ||
| nvidia-nat-core = { path = "../nvidia_nat_core", editable = true } | ||
| nvidia-nat-test = { path = "../nvidia_nat_test", editable = true } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| # SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. | ||
| # All rights reserved. | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
| # | ||
| # 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. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,124 @@ | ||
| # SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. | ||
| # All rights reserved. | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
| # | ||
| # Licensed under the Apache License, Version 2.0 (the "License"); | ||
| # you may not use it 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. | ||
|
|
||
| """Abstract base class for evolutionary prompt optimizers (GA and related strategies).""" | ||
|
|
||
| import asyncio | ||
| from abc import ABC | ||
| from abc import abstractmethod | ||
| from typing import Any | ||
|
|
||
| from nat.data_models.config import Config | ||
| from nat.data_models.optimizable import SearchSpace | ||
| from nat.data_models.optimizer import OptimizerConfig | ||
| from nat.data_models.optimizer import OptimizerRunConfig | ||
| from nat.eval.evaluate import EvaluationRun | ||
| from nat.eval.evaluate import EvaluationRunConfig | ||
| from nat.optimizer.ga_individual import Individual | ||
| from nat.optimizer.update_helpers import apply_suggestions | ||
|
|
||
|
|
||
| class BaseEvolutionaryPromptOptimizer(ABC): | ||
|
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. Seems like we need one more level of interface above this. Something like |
||
| """ | ||
| Base class for evolutionary prompt optimizers. | ||
|
|
||
| Provides evaluation infrastructure: apply_suggestions + EvaluationRun, | ||
| concurrent population evaluation, and a _post_evaluate_single hook for | ||
| subclasses. Fitness computation and persistence are implementation-specific. | ||
| """ | ||
|
|
||
| @abstractmethod | ||
| async def run( | ||
| self, | ||
| *, | ||
| base_cfg: Config, | ||
| full_space: dict[str, SearchSpace], | ||
| optimizer_config: OptimizerConfig, | ||
| opt_run_config: OptimizerRunConfig, | ||
| ) -> None: | ||
| """Run the evolutionary optimization loop.""" | ||
| ... | ||
|
|
||
| # ---------- evaluation ---------- # | ||
|
|
||
| async def _evaluate_single_given_trial( | ||
| self, | ||
| ind: Individual, | ||
| cfg_trial: Config, | ||
| optimizer_config: OptimizerConfig, | ||
| opt_run_config: OptimizerRunConfig, | ||
| ) -> None: | ||
| """Run EvaluationRun for an already-built trial config; fill ind.metrics. Subclasses may extend via _post_evaluate_single.""" | ||
| eval_cfg = EvaluationRunConfig( | ||
| config_file=cfg_trial, | ||
| dataset=opt_run_config.dataset, | ||
| result_json_path=opt_run_config.result_json_path, | ||
| endpoint=opt_run_config.endpoint, | ||
| endpoint_timeout=opt_run_config.endpoint_timeout, | ||
| override=opt_run_config.override, | ||
| ) | ||
| metric_cfg = optimizer_config.eval_metrics or {} | ||
| eval_metrics = [v.evaluator_name for v in metric_cfg.values()] | ||
| reps = max(1, getattr(optimizer_config, "reps_per_param_set", 1)) | ||
|
|
||
| all_results: list[list[tuple[str, Any]]] = [] | ||
| for _ in range(reps): | ||
| res = (await EvaluationRun(config=eval_cfg).run_and_evaluate()).evaluation_results | ||
| all_results.append(res) | ||
|
|
||
| metrics: dict[str, float] = {} | ||
| for metric_name in eval_metrics: | ||
| scores: list[float] = [] | ||
| for run_results in all_results: | ||
| for name, result in run_results: | ||
| if name == metric_name: | ||
| scores.append(result.average_score) | ||
| break | ||
| metrics[metric_name] = float(sum(scores) / len(scores)) if scores else 0.0 | ||
| ind.metrics = metrics | ||
| await self._post_evaluate_single(ind, all_results, optimizer_config, opt_run_config) | ||
|
|
||
| async def _post_evaluate_single( | ||
| self, | ||
| ind: Individual, | ||
| all_results: list[list[tuple[str, Any]]], | ||
| optimizer_config: OptimizerConfig, | ||
| opt_run_config: OptimizerRunConfig, | ||
| ) -> None: | ||
| """Override in subclasses to add post-evaluation logic (e.g. oracle feedback). Default no-op.""" | ||
| pass | ||
|
|
||
| async def _evaluate_population( | ||
| self, | ||
| population: list[Individual], | ||
| base_cfg: Config, | ||
| optimizer_config: OptimizerConfig, | ||
| opt_run_config: OptimizerRunConfig, | ||
| max_concurrency: int = 8, | ||
| ) -> None: | ||
| """Evaluate all individuals (concurrently). Semaphore wraps only apply_suggestions.""" | ||
| unevaluated = [ind for ind in population if not ind.metrics] | ||
| if unevaluated: | ||
| sem = asyncio.Semaphore(max_concurrency) | ||
|
|
||
| async def _eval_one(ind: Individual) -> None: | ||
| async with sem: | ||
| cfg_trial = apply_suggestions(base_cfg, ind.prompts) | ||
| await self._evaluate_single_given_trial( | ||
| ind, cfg_trial, optimizer_config, opt_run_config | ||
| ) | ||
|
|
||
| await asyncio.gather(*[_eval_one(ind) for ind in unevaluated]) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| # SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. | ||
| # All rights reserved. | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
| # | ||
| # Licensed under the Apache License, Version 2.0 (the "License"); | ||
| # you may not use it 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. | ||
|
|
||
| """ | ||
| GA-specific optimizer config subtype. | ||
|
|
||
| Base types live in nat.data_models.optimizer (core). This package subclasses | ||
| BaseOptimizerConfig with the GA-specific nest (.prompt) and implementation- | ||
| specific fields (oracle feedback) that may not exist in other GA implementations. | ||
| """ | ||
|
|
||
| from typing import Literal | ||
|
|
||
| from pydantic import Field | ||
|
|
||
| from nat.data_models.optimizer import BaseOptimizerConfig | ||
| from nat.data_models.optimizer import PromptGAOptimizationConfig | ||
|
|
||
|
|
||
| class GAOptimizerConfig(BaseOptimizerConfig): | ||
|
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. Why do we have |
||
| """ | ||
| Optimizer config for this GA prompt implementation. | ||
|
|
||
| Extends the shared base with .prompt (population, generations, init/recombine, etc.) | ||
| and with oracle-feedback fields that are specific to this implementation— | ||
| another GA might not use oracle feedback at all, so these live on the subtype. | ||
| """ | ||
| prompt: PromptGAOptimizationConfig = Field(default_factory=PromptGAOptimizationConfig) | ||
|
|
||
| # Oracle feedback: implementation-specific to this GA (inject failure reasoning into mutations). | ||
| oracle_feedback_mode: Literal["never", "always", "failing_only", "adaptive"] = Field( | ||
| description="When to inject failure reasoning into mutations.", | ||
| default="never", | ||
| ) | ||
| oracle_feedback_worst_n: int = Field( | ||
| description="Number of worst-scoring items to extract reasoning from.", | ||
| default=5, | ||
| ge=1, | ||
| ) | ||
| oracle_feedback_max_chars: int = Field( | ||
| description="Maximum characters for oracle feedback in mutation prompt.", | ||
| default=4000, | ||
| ge=1, | ||
| ) | ||
| oracle_feedback_fitness_threshold: float = Field( | ||
| description="For 'failing_only' mode: normalized fitness threshold below which feedback is injected.", | ||
| default=0.3, | ||
| ge=0.0, | ||
| le=1.0, | ||
| ) | ||
| oracle_feedback_stagnation_generations: int = Field( | ||
| description="For 'adaptive' mode: generations without improvement before enabling feedback.", | ||
| default=3, | ||
| ge=1, | ||
| ) | ||
| oracle_feedback_fitness_variance_threshold: float = Field( | ||
| description="For 'adaptive' mode: fitness variance threshold for collapse detection.", | ||
| default=0.01, | ||
| ge=0.0, | ||
| ) | ||
| oracle_feedback_diversity_threshold: float = Field( | ||
| description="For 'adaptive' mode: prompt duplication ratio threshold (0-1).", | ||
| default=0.5, | ||
| ge=0.0, | ||
| le=1.0, | ||
| ) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another suggestion/note on the directory structure. Perhaps the dilenation would be clearer if we have two directories under
packages/nvidia_nat_optimizer/src/nat/optimizer. One for parameters and one for prompts