nemo-evaluator-launcher 0.1.28__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of nemo-evaluator-launcher might be problematic. Click here for more details.

Files changed (60) hide show
  1. nemo_evaluator_launcher/__init__.py +79 -0
  2. nemo_evaluator_launcher/api/__init__.py +24 -0
  3. nemo_evaluator_launcher/api/functional.py +698 -0
  4. nemo_evaluator_launcher/api/types.py +98 -0
  5. nemo_evaluator_launcher/api/utils.py +19 -0
  6. nemo_evaluator_launcher/cli/__init__.py +15 -0
  7. nemo_evaluator_launcher/cli/export.py +267 -0
  8. nemo_evaluator_launcher/cli/info.py +512 -0
  9. nemo_evaluator_launcher/cli/kill.py +41 -0
  10. nemo_evaluator_launcher/cli/ls_runs.py +134 -0
  11. nemo_evaluator_launcher/cli/ls_tasks.py +136 -0
  12. nemo_evaluator_launcher/cli/main.py +226 -0
  13. nemo_evaluator_launcher/cli/run.py +200 -0
  14. nemo_evaluator_launcher/cli/status.py +164 -0
  15. nemo_evaluator_launcher/cli/version.py +55 -0
  16. nemo_evaluator_launcher/common/__init__.py +16 -0
  17. nemo_evaluator_launcher/common/execdb.py +283 -0
  18. nemo_evaluator_launcher/common/helpers.py +366 -0
  19. nemo_evaluator_launcher/common/logging_utils.py +357 -0
  20. nemo_evaluator_launcher/common/mapping.py +295 -0
  21. nemo_evaluator_launcher/common/printing_utils.py +93 -0
  22. nemo_evaluator_launcher/configs/__init__.py +15 -0
  23. nemo_evaluator_launcher/configs/default.yaml +28 -0
  24. nemo_evaluator_launcher/configs/deployment/generic.yaml +33 -0
  25. nemo_evaluator_launcher/configs/deployment/nim.yaml +32 -0
  26. nemo_evaluator_launcher/configs/deployment/none.yaml +16 -0
  27. nemo_evaluator_launcher/configs/deployment/sglang.yaml +38 -0
  28. nemo_evaluator_launcher/configs/deployment/trtllm.yaml +24 -0
  29. nemo_evaluator_launcher/configs/deployment/vllm.yaml +42 -0
  30. nemo_evaluator_launcher/configs/execution/lepton/default.yaml +92 -0
  31. nemo_evaluator_launcher/configs/execution/local.yaml +19 -0
  32. nemo_evaluator_launcher/configs/execution/slurm/default.yaml +34 -0
  33. nemo_evaluator_launcher/executors/__init__.py +22 -0
  34. nemo_evaluator_launcher/executors/base.py +120 -0
  35. nemo_evaluator_launcher/executors/lepton/__init__.py +16 -0
  36. nemo_evaluator_launcher/executors/lepton/deployment_helpers.py +609 -0
  37. nemo_evaluator_launcher/executors/lepton/executor.py +1004 -0
  38. nemo_evaluator_launcher/executors/lepton/job_helpers.py +398 -0
  39. nemo_evaluator_launcher/executors/local/__init__.py +15 -0
  40. nemo_evaluator_launcher/executors/local/executor.py +605 -0
  41. nemo_evaluator_launcher/executors/local/run.template.sh +103 -0
  42. nemo_evaluator_launcher/executors/registry.py +38 -0
  43. nemo_evaluator_launcher/executors/slurm/__init__.py +15 -0
  44. nemo_evaluator_launcher/executors/slurm/executor.py +1147 -0
  45. nemo_evaluator_launcher/exporters/__init__.py +36 -0
  46. nemo_evaluator_launcher/exporters/base.py +121 -0
  47. nemo_evaluator_launcher/exporters/gsheets.py +409 -0
  48. nemo_evaluator_launcher/exporters/local.py +502 -0
  49. nemo_evaluator_launcher/exporters/mlflow.py +619 -0
  50. nemo_evaluator_launcher/exporters/registry.py +40 -0
  51. nemo_evaluator_launcher/exporters/utils.py +624 -0
  52. nemo_evaluator_launcher/exporters/wandb.py +490 -0
  53. nemo_evaluator_launcher/package_info.py +38 -0
  54. nemo_evaluator_launcher/resources/mapping.toml +380 -0
  55. nemo_evaluator_launcher-0.1.28.dist-info/METADATA +494 -0
  56. nemo_evaluator_launcher-0.1.28.dist-info/RECORD +60 -0
  57. nemo_evaluator_launcher-0.1.28.dist-info/WHEEL +5 -0
  58. nemo_evaluator_launcher-0.1.28.dist-info/entry_points.txt +3 -0
  59. nemo_evaluator_launcher-0.1.28.dist-info/licenses/LICENSE +451 -0
  60. nemo_evaluator_launcher-0.1.28.dist-info/top_level.txt +1 -0
@@ -0,0 +1,136 @@
1
+ # SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+ from collections import defaultdict
17
+ from dataclasses import dataclass
18
+
19
+ from simple_parsing import field
20
+
21
+
22
+ @dataclass
23
+ class Cmd:
24
+ """List command configuration."""
25
+
26
+ json: bool = field(
27
+ default=False,
28
+ action="store_true",
29
+ help="Print output as JSON instead of table format",
30
+ )
31
+
32
+ def execute(self) -> None:
33
+ # Import heavy dependencies only when needed
34
+ import json
35
+
36
+ from nemo_evaluator_launcher.api.functional import get_tasks_list
37
+
38
+ # TODO(dfridman): modify `get_tasks_list` to return a list of dicts in the first place
39
+ data = get_tasks_list()
40
+ headers = ["task", "endpoint_type", "harness", "container"]
41
+ supported_benchmarks = []
42
+ for task_data in data:
43
+ assert len(task_data) == len(headers)
44
+ supported_benchmarks.append(dict(zip(headers, task_data)))
45
+
46
+ if self.json:
47
+ print(json.dumps({"tasks": supported_benchmarks}, indent=2))
48
+ else:
49
+ self._print_table(supported_benchmarks)
50
+
51
+ def _print_table(self, tasks: list[dict]) -> None:
52
+ """Print tasks grouped by harness and container in table format."""
53
+ if not tasks:
54
+ print("No tasks found.")
55
+ return
56
+
57
+ # Group tasks by harness and container
58
+ grouped = defaultdict(lambda: defaultdict(list))
59
+ for task in tasks:
60
+ harness = task["harness"]
61
+ container = task["container"]
62
+ grouped[harness][container].append(task)
63
+
64
+ # Print grouped tables
65
+ for i, (harness, containers) in enumerate(grouped.items()):
66
+ if i > 0:
67
+ print() # Extra spacing between harnesses
68
+
69
+ for j, (container, container_tasks) in enumerate(containers.items()):
70
+ if j > 0:
71
+ print() # Spacing between containers
72
+
73
+ # Prepare task table first to get column widths
74
+ task_headers = ["task", "endpoint_type"]
75
+ rows = []
76
+ for task in container_tasks:
77
+ rows.append([task["task"], task["endpoint_type"]])
78
+
79
+ # Sort tasks alphabetically for better readability
80
+ rows.sort(key=lambda x: x[0])
81
+
82
+ # Calculate column widths with some padding
83
+ widths = [
84
+ max(len(task_headers[i]), max(len(str(row[i])) for row in rows)) + 2
85
+ for i in range(len(task_headers))
86
+ ]
87
+
88
+ # Calculate minimum table width based on task columns
89
+ min_table_width = sum(widths) + len(widths) + 1
90
+
91
+ # Calculate required width for header content
92
+ harness_line = f"harness: {harness}"
93
+ container_line = f"container: {container}"
94
+ header_content_width = (
95
+ max(len(harness_line), len(container_line)) + 4
96
+ ) # +4 for "| " and " |"
97
+
98
+ # Use the larger of the two widths
99
+ table_width = max(min_table_width, header_content_width)
100
+
101
+ # Print combined header with harness and container info
102
+ print("=" * table_width)
103
+ print(f"{harness_line}")
104
+ print(f"{container_line}")
105
+
106
+ # Adjust column widths to fill the full table width
107
+ available_width = table_width
108
+ # Give more space to the first column (task names can be long)
109
+ adjusted_widths = [
110
+ max(
111
+ widths[0], available_width * 2 // 3
112
+ ), # 2/3 of available width for task
113
+ 0, # Will be calculated as remainder
114
+ ]
115
+ adjusted_widths[1] = (
116
+ available_width - adjusted_widths[0]
117
+ ) # Remainder for endpoint_type
118
+
119
+ # Print task table header separator
120
+ print(" " * table_width)
121
+ header_row = f"{task_headers[0]:<{adjusted_widths[0]}}{task_headers[1]:<{adjusted_widths[1]}}"
122
+ print(header_row)
123
+ print("-" * table_width)
124
+
125
+ # Print task rows
126
+ for row in rows:
127
+ data_row = f"{str(row[0]):<{adjusted_widths[0]}}{str(row[1]):<{adjusted_widths[1]}}"
128
+ print(data_row)
129
+
130
+ print("-" * table_width)
131
+ # Show task count
132
+ task_count = len(rows)
133
+ print(f" {task_count} task{'s' if task_count != 1 else ''} available")
134
+ print("=" * table_width)
135
+
136
+ print()
@@ -0,0 +1,226 @@
1
+ # SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+ """Main CLI module using simple-parsing with subcommands."""
17
+
18
+ import os
19
+
20
+ from simple_parsing import ArgumentParser
21
+
22
+ import nemo_evaluator_launcher.cli.export as export
23
+ import nemo_evaluator_launcher.cli.info as info
24
+ import nemo_evaluator_launcher.cli.kill as kill
25
+ import nemo_evaluator_launcher.cli.ls_runs as ls_runs
26
+ import nemo_evaluator_launcher.cli.ls_tasks as ls_tasks
27
+ import nemo_evaluator_launcher.cli.run as run
28
+ import nemo_evaluator_launcher.cli.status as status
29
+ import nemo_evaluator_launcher.cli.version as version
30
+ from nemo_evaluator_launcher.common.logging_utils import logger
31
+
32
+ VERSION_HELP = "Show version information"
33
+
34
+
35
+ def is_verbose_enabled(args) -> bool:
36
+ """Check if verbose flag is enabled in any subcommand."""
37
+ # Check global verbose flag
38
+ if hasattr(args, "verbose") and args.verbose:
39
+ return True
40
+
41
+ # Check subcommand verbose flags
42
+ subcommands = [
43
+ "run",
44
+ "status",
45
+ "info",
46
+ "kill",
47
+ "tasks_alias",
48
+ "tasks",
49
+ "runs",
50
+ "export",
51
+ ]
52
+ for subcmd in subcommands:
53
+ if hasattr(args, subcmd) and hasattr(getattr(args, subcmd), "verbose"):
54
+ if getattr(getattr(args, subcmd), "verbose"):
55
+ return True
56
+
57
+ return False
58
+
59
+
60
+ def create_parser() -> ArgumentParser:
61
+ """Create and configure the CLI argument parser with subcommands."""
62
+ parser = ArgumentParser()
63
+
64
+ # Add --version flag at the top level
65
+ parser.add_argument("--version", action="store_true", help=VERSION_HELP)
66
+
67
+ # Add --verbose/-v flag for debug logging
68
+ parser.add_argument(
69
+ "-v",
70
+ "--verbose",
71
+ action="store_true",
72
+ help="Enable verbose logging (sets LOG_LEVEL=DEBUG)",
73
+ )
74
+
75
+ subparsers = parser.add_subparsers(dest="command", required=False)
76
+
77
+ # Version subcommand
78
+ version_parser = subparsers.add_parser(
79
+ "version",
80
+ help=VERSION_HELP,
81
+ description=VERSION_HELP,
82
+ )
83
+ version_parser.add_arguments(version.Cmd, dest="version")
84
+
85
+ # Run subcommand
86
+ run_parser = subparsers.add_parser(
87
+ "run", help="Run evaluation", description="Run evaluation"
88
+ )
89
+ run_parser.add_argument(
90
+ "-v",
91
+ "--verbose",
92
+ action="store_true",
93
+ help="Enable verbose logging (sets LOG_LEVEL=DEBUG)",
94
+ )
95
+ run_parser.add_arguments(run.Cmd, dest="run")
96
+
97
+ # Status subcommand
98
+ status_parser = subparsers.add_parser(
99
+ "status", help="Check job status", description="Check job status"
100
+ )
101
+ status_parser.add_argument(
102
+ "-v",
103
+ "--verbose",
104
+ action="store_true",
105
+ help="Enable verbose logging (sets LOG_LEVEL=DEBUG)",
106
+ )
107
+ status_parser.add_arguments(status.Cmd, dest="status")
108
+
109
+ # Kill subcommand
110
+ kill_parser = subparsers.add_parser(
111
+ "kill",
112
+ help="Kill a job or invocation",
113
+ description="Kill a job (e.g., aefc4819.0) or entire invocation (e.g., aefc4819) by its ID",
114
+ )
115
+ kill_parser.add_argument(
116
+ "-v",
117
+ "--verbose",
118
+ action="store_true",
119
+ help="Enable verbose logging (sets LOG_LEVEL=DEBUG)",
120
+ )
121
+ kill_parser.add_arguments(kill.Cmd, dest="kill")
122
+
123
+ # Ls subcommand (with nested subcommands)
124
+ ls_parser = subparsers.add_parser(
125
+ "ls", help="List resources", description="List tasks or runs"
126
+ )
127
+ ls_parser.add_argument(
128
+ "-v",
129
+ "--verbose",
130
+ action="store_true",
131
+ help="Enable verbose logging (sets LOG_LEVEL=DEBUG)",
132
+ )
133
+ # Add arguments from `ls tasks` so that they work with `ls` as default alias
134
+ ls_parser.add_arguments(ls_tasks.Cmd, dest="tasks_alias")
135
+
136
+ ls_sub = ls_parser.add_subparsers(dest="ls_command", required=False)
137
+
138
+ # ls tasks (default)
139
+ ls_tasks_parser = ls_sub.add_parser(
140
+ "tasks", help="List available tasks", description="List available tasks"
141
+ )
142
+ ls_tasks_parser.add_arguments(ls_tasks.Cmd, dest="tasks")
143
+
144
+ # ls runs (invocations summary)
145
+ ls_runs_parser = ls_sub.add_parser(
146
+ "runs",
147
+ help="List invocations (runs)",
148
+ description="Show a concise table of invocations from the exec DB",
149
+ )
150
+ ls_runs_parser.add_arguments(ls_runs.Cmd, dest="runs")
151
+
152
+ # Export subcommand
153
+ export_parser = subparsers.add_parser(
154
+ "export",
155
+ help="Export evaluation results",
156
+ description="Export evaluation results takes a List of invocation ids and a list of destinations(local, gitlab, wandb)",
157
+ )
158
+ export_parser.add_argument(
159
+ "-v",
160
+ "--verbose",
161
+ action="store_true",
162
+ help="Enable verbose logging (sets LOG_LEVEL=DEBUG)",
163
+ )
164
+ export_parser.add_arguments(export.ExportCmd, dest="export")
165
+
166
+ # Info subcommand
167
+ info_parser = subparsers.add_parser(
168
+ "info",
169
+ help="Display evaluation job information",
170
+ description="Info functionalities for nemo-evaluator-launcher",
171
+ )
172
+ info_parser.add_argument(
173
+ "-v", "--verbose", action="store_true", help="Enable verbose logging"
174
+ )
175
+ info_parser.add_arguments(info.InfoCmd, dest="info")
176
+
177
+ return parser
178
+
179
+
180
+ def main() -> None:
181
+ """Main CLI entry point with subcommands."""
182
+ parser = create_parser()
183
+ args = parser.parse_args()
184
+
185
+ # Handle --verbose flag
186
+ if is_verbose_enabled(args):
187
+ os.environ["LOG_LEVEL"] = "DEBUG"
188
+
189
+ # Handle --version flag
190
+ if hasattr(args, "version") and args.version:
191
+ version_cmd = version.Cmd()
192
+ version_cmd.execute()
193
+ return
194
+
195
+ # Handle case where no command is provided but --version wasn't used
196
+ if not hasattr(args, "command") or args.command is None:
197
+ parser.print_help()
198
+ return
199
+
200
+ logger.debug("Parsed arguments", args=args)
201
+ if args.command == "version":
202
+ args.version.execute()
203
+ elif args.command == "run":
204
+ args.run.execute()
205
+ elif args.command == "status":
206
+ args.status.execute()
207
+ elif args.command == "kill":
208
+ args.kill.execute()
209
+ elif args.command == "ls":
210
+ # Dispatch nested ls subcommands
211
+ if args.ls_command is None or args.ls_command == "tasks":
212
+ # Default to tasks when no subcommand specified
213
+ if hasattr(args, "tasks_alias"):
214
+ args.tasks_alias.execute()
215
+ else:
216
+ args.tasks.execute()
217
+ elif args.ls_command == "runs":
218
+ args.runs.execute()
219
+ elif args.command == "export":
220
+ args.export.execute()
221
+ elif args.command == "info":
222
+ args.info.execute()
223
+
224
+
225
+ if __name__ == "__main__":
226
+ main()
@@ -0,0 +1,200 @@
1
+ # SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+ import pathlib
17
+ import time
18
+ from dataclasses import dataclass
19
+
20
+ from simple_parsing import field
21
+
22
+ from nemo_evaluator_launcher.common.logging_utils import logger
23
+ from nemo_evaluator_launcher.common.printing_utils import (
24
+ bold,
25
+ cyan,
26
+ green,
27
+ magenta,
28
+ red,
29
+ )
30
+
31
+
32
+ @dataclass
33
+ class Cmd:
34
+ """Run command parameters"""
35
+
36
+ config_name: str = field(
37
+ default="default",
38
+ alias=["-c", "--config-name"],
39
+ metadata={
40
+ "help": "Config name to use. Consult `nemo_evaluator_launcher.configs`"
41
+ },
42
+ )
43
+ config_dir: str | None = field(
44
+ default=None,
45
+ alias=["-d", "--config-dir"],
46
+ metadata={
47
+ "help": "Path to user config directory. If provided, searches here first, then falls back to internal configs."
48
+ },
49
+ )
50
+ run_config_file: str | None = field(
51
+ default=None,
52
+ alias=["-f", "--run-config-file"],
53
+ metadata={
54
+ "help": "Path to a run config file to load directly (bypasses Hydra config loading)."
55
+ },
56
+ )
57
+ override: list[str] = field(
58
+ default_factory=list,
59
+ action="append",
60
+ nargs="?",
61
+ alias=["-o"],
62
+ metadata={
63
+ "help": "Hydra override in the form some.param.path=value (pass multiple `-o` for multiple overrides).",
64
+ },
65
+ )
66
+ dry_run: bool = field(
67
+ default=False,
68
+ alias=["-n", "--dry-run"],
69
+ metadata={"help": "Do not run the evaluation, just print the config."},
70
+ )
71
+ config_output: str | None = field(
72
+ default=None,
73
+ alias=["--config-output"],
74
+ metadata={
75
+ "help": "Directory to save the complete run config. Defaults to ~/.nemo-evaluator/run_configs/"
76
+ },
77
+ )
78
+
79
+ def execute(self) -> None:
80
+ # Import heavy dependencies only when needed
81
+ import yaml
82
+ from omegaconf import OmegaConf
83
+
84
+ from nemo_evaluator_launcher.api.functional import RunConfig, run_eval
85
+
86
+ # Load configuration either from Hydra or from a run config file
87
+ if self.run_config_file:
88
+ # Validate that run config file is not used with other config options
89
+ if self.config_name != "default":
90
+ raise ValueError("Cannot use --run-config-file with --config-name")
91
+ if self.config_dir is not None:
92
+ raise ValueError("Cannot use --run-config-file with --config-dir")
93
+ if self.override:
94
+ raise ValueError("Cannot use --run-config-file with --override")
95
+
96
+ # Load from run config file
97
+ with open(self.run_config_file, "r") as f:
98
+ config_dict = yaml.safe_load(f)
99
+
100
+ # Create RunConfig from the loaded data
101
+ config = OmegaConf.create(config_dict)
102
+ else:
103
+ # Load the complete Hydra configuration
104
+ config = RunConfig.from_hydra(
105
+ config_name=self.config_name,
106
+ hydra_overrides=self.override,
107
+ config_dir=self.config_dir,
108
+ )
109
+
110
+ try:
111
+ invocation_id = run_eval(config, self.dry_run)
112
+ except Exception as e:
113
+ print(red(f"✗ Job submission failed, see logs | Error: {e}"))
114
+ logger.error("Job submission failed", error=e)
115
+ raise
116
+
117
+ # Save the complete configuration
118
+ if not self.dry_run and invocation_id is not None:
119
+ # Determine config output directory
120
+ if self.config_output:
121
+ # Use custom directory specified by --config-output
122
+ config_dir = pathlib.Path(self.config_output)
123
+ else:
124
+ # Default to original location: ~/.nemo-evaluator/run_configs
125
+ home_dir = pathlib.Path.home()
126
+ config_dir = home_dir / ".nemo-evaluator" / "run_configs"
127
+
128
+ # Ensure the directory exists
129
+ config_dir.mkdir(parents=True, exist_ok=True)
130
+
131
+ # Convert DictConfig to dict and save as YAML
132
+ config_dict = OmegaConf.to_container(config, resolve=True)
133
+ config_yaml = yaml.dump(
134
+ config_dict, default_flow_style=False, sort_keys=False, indent=2
135
+ )
136
+
137
+ # Create config filename with invocation ID
138
+ config_filename = f"{invocation_id}_config.yml"
139
+ config_path = config_dir / config_filename
140
+
141
+ # Save the complete Hydra configuration
142
+ with open(config_path, "w") as f:
143
+ f.write("# Complete configuration from nemo-evaluator-launcher\n")
144
+ f.write(
145
+ f"# Generated at: {time.strftime('%Y-%m-%d %H:%M:%S UTC', time.gmtime())}\n"
146
+ )
147
+ f.write(f"# Invocation ID: {invocation_id}\n")
148
+ f.write("#\n")
149
+ f.write("# This is the complete raw configuration\n")
150
+ f.write("#\n")
151
+ f.write("# To rerun this exact configuration:\n")
152
+ f.write(
153
+ f"# nemo-evaluator-launcher run --run-config-file {config_path}\n"
154
+ )
155
+ f.write("#\n")
156
+ f.write(config_yaml)
157
+
158
+ print(bold(cyan("Complete run config saved to: ")) + f"\n {config_path}\n")
159
+ logger.info("Saved complete config", path=config_path)
160
+
161
+ # Print general success message with invocation ID and helpful commands
162
+ if invocation_id is not None and not self.dry_run:
163
+ print(
164
+ bold(cyan("To check status: "))
165
+ + f"nemo-evaluator-launcher status {invocation_id}"
166
+ )
167
+ print(
168
+ bold(cyan("To kill all jobs: "))
169
+ + f"nemo-evaluator-launcher kill {invocation_id}"
170
+ )
171
+
172
+ # Show actual job IDs and task names
173
+ print(bold(cyan("To kill individual jobs:")))
174
+ # Access tasks - will work after normalization in run_eval
175
+ tasks = (
176
+ config.evaluation.tasks
177
+ if hasattr(config.evaluation, "tasks")
178
+ else config.evaluation
179
+ )
180
+ for idx, task in enumerate(tasks):
181
+ job_id = f"{invocation_id}.{idx}"
182
+ print(f" nemo-evaluator-launcher kill {job_id} # {task.name}")
183
+
184
+ print(
185
+ magenta(
186
+ "(all commands accept shortened IDs as long as there are no conflicts)"
187
+ )
188
+ )
189
+ print(
190
+ bold(cyan("To print all jobs: ")) + "nemo-evaluator-launcher ls runs"
191
+ "\n (--since 1d or --since 6h for time span, see --help)"
192
+ )
193
+
194
+ print(
195
+ green(
196
+ bold(
197
+ f"✓ Job submission successful | Invocation ID: {invocation_id}"
198
+ )
199
+ )
200
+ )