nemo-evaluator-launcher 0.1.21__tar.gz → 0.1.64__tar.gz

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.
Files changed (77) hide show
  1. {nemo_evaluator_launcher-0.1.21 → nemo_evaluator_launcher-0.1.64}/PKG-INFO +4 -3
  2. {nemo_evaluator_launcher-0.1.21 → nemo_evaluator_launcher-0.1.64}/pyproject.toml +5 -2
  3. {nemo_evaluator_launcher-0.1.21 → nemo_evaluator_launcher-0.1.64}/src/nemo_evaluator_launcher/api/functional.py +159 -5
  4. {nemo_evaluator_launcher-0.1.21 → nemo_evaluator_launcher-0.1.64}/src/nemo_evaluator_launcher/api/types.py +21 -14
  5. nemo_evaluator_launcher-0.1.64/src/nemo_evaluator_launcher/cli/logs.py +102 -0
  6. nemo_evaluator_launcher-0.1.64/src/nemo_evaluator_launcher/cli/ls_task.py +280 -0
  7. nemo_evaluator_launcher-0.1.64/src/nemo_evaluator_launcher/cli/ls_tasks.py +289 -0
  8. {nemo_evaluator_launcher-0.1.21 → nemo_evaluator_launcher-0.1.64}/src/nemo_evaluator_launcher/cli/main.py +29 -2
  9. {nemo_evaluator_launcher-0.1.21 → nemo_evaluator_launcher-0.1.64}/src/nemo_evaluator_launcher/cli/run.py +77 -28
  10. {nemo_evaluator_launcher-0.1.21 → nemo_evaluator_launcher-0.1.64}/src/nemo_evaluator_launcher/cli/version.py +26 -23
  11. nemo_evaluator_launcher-0.1.64/src/nemo_evaluator_launcher/common/container_metadata/__init__.py +61 -0
  12. nemo_evaluator_launcher-0.1.64/src/nemo_evaluator_launcher/common/container_metadata/intermediate_repr.py +530 -0
  13. nemo_evaluator_launcher-0.1.64/src/nemo_evaluator_launcher/common/container_metadata/loading.py +1126 -0
  14. nemo_evaluator_launcher-0.1.64/src/nemo_evaluator_launcher/common/container_metadata/registries.py +824 -0
  15. nemo_evaluator_launcher-0.1.64/src/nemo_evaluator_launcher/common/container_metadata/utils.py +63 -0
  16. {nemo_evaluator_launcher-0.1.21 → nemo_evaluator_launcher-0.1.64}/src/nemo_evaluator_launcher/common/helpers.py +115 -37
  17. {nemo_evaluator_launcher-0.1.21 → nemo_evaluator_launcher-0.1.64}/src/nemo_evaluator_launcher/common/logging_utils.py +4 -1
  18. nemo_evaluator_launcher-0.1.64/src/nemo_evaluator_launcher/common/mapping.py +284 -0
  19. {nemo_evaluator_launcher-0.1.21 → nemo_evaluator_launcher-0.1.64}/src/nemo_evaluator_launcher/common/printing_utils.py +25 -12
  20. {nemo_evaluator_launcher-0.1.21 → nemo_evaluator_launcher-0.1.64}/src/nemo_evaluator_launcher/configs/deployment/nim.yaml +3 -1
  21. {nemo_evaluator_launcher-0.1.21 → nemo_evaluator_launcher-0.1.64}/src/nemo_evaluator_launcher/configs/deployment/sglang.yaml +4 -2
  22. {nemo_evaluator_launcher-0.1.21 → nemo_evaluator_launcher-0.1.64}/src/nemo_evaluator_launcher/configs/deployment/trtllm.yaml +2 -3
  23. {nemo_evaluator_launcher-0.1.21 → nemo_evaluator_launcher-0.1.64}/src/nemo_evaluator_launcher/configs/deployment/vllm.yaml +0 -1
  24. {nemo_evaluator_launcher-0.1.21 → nemo_evaluator_launcher-0.1.64}/src/nemo_evaluator_launcher/configs/execution/slurm/default.yaml +14 -0
  25. {nemo_evaluator_launcher-0.1.21 → nemo_evaluator_launcher-0.1.64}/src/nemo_evaluator_launcher/executors/base.py +31 -1
  26. {nemo_evaluator_launcher-0.1.21 → nemo_evaluator_launcher-0.1.64}/src/nemo_evaluator_launcher/executors/lepton/deployment_helpers.py +36 -1
  27. {nemo_evaluator_launcher-0.1.21 → nemo_evaluator_launcher-0.1.64}/src/nemo_evaluator_launcher/executors/lepton/executor.py +107 -9
  28. {nemo_evaluator_launcher-0.1.21 → nemo_evaluator_launcher-0.1.64}/src/nemo_evaluator_launcher/executors/local/executor.py +383 -24
  29. {nemo_evaluator_launcher-0.1.21 → nemo_evaluator_launcher-0.1.64}/src/nemo_evaluator_launcher/executors/local/run.template.sh +54 -2
  30. {nemo_evaluator_launcher-0.1.21 → nemo_evaluator_launcher-0.1.64}/src/nemo_evaluator_launcher/executors/slurm/executor.py +680 -78
  31. nemo_evaluator_launcher-0.1.64/src/nemo_evaluator_launcher/executors/slurm/proxy.cfg.template +26 -0
  32. {nemo_evaluator_launcher-0.1.21 → nemo_evaluator_launcher-0.1.64}/src/nemo_evaluator_launcher/exporters/utils.py +32 -46
  33. {nemo_evaluator_launcher-0.1.21 → nemo_evaluator_launcher-0.1.64}/src/nemo_evaluator_launcher/package_info.py +1 -1
  34. nemo_evaluator_launcher-0.1.64/src/nemo_evaluator_launcher/resources/all_tasks_irs.yaml +17016 -0
  35. nemo_evaluator_launcher-0.1.64/src/nemo_evaluator_launcher/resources/mapping.toml +93 -0
  36. {nemo_evaluator_launcher-0.1.21 → nemo_evaluator_launcher-0.1.64}/src/nemo_evaluator_launcher.egg-info/PKG-INFO +4 -3
  37. {nemo_evaluator_launcher-0.1.21 → nemo_evaluator_launcher-0.1.64}/src/nemo_evaluator_launcher.egg-info/SOURCES.txt +9 -0
  38. {nemo_evaluator_launcher-0.1.21 → nemo_evaluator_launcher-0.1.64}/src/nemo_evaluator_launcher.egg-info/entry_points.txt +1 -0
  39. {nemo_evaluator_launcher-0.1.21 → nemo_evaluator_launcher-0.1.64}/src/nemo_evaluator_launcher.egg-info/requires.txt +2 -1
  40. nemo_evaluator_launcher-0.1.21/src/nemo_evaluator_launcher/cli/ls_tasks.py +0 -136
  41. nemo_evaluator_launcher-0.1.21/src/nemo_evaluator_launcher/common/mapping.py +0 -295
  42. nemo_evaluator_launcher-0.1.21/src/nemo_evaluator_launcher/resources/mapping.toml +0 -380
  43. {nemo_evaluator_launcher-0.1.21 → nemo_evaluator_launcher-0.1.64}/LICENSE +0 -0
  44. {nemo_evaluator_launcher-0.1.21 → nemo_evaluator_launcher-0.1.64}/README.md +0 -0
  45. {nemo_evaluator_launcher-0.1.21 → nemo_evaluator_launcher-0.1.64}/setup.cfg +0 -0
  46. {nemo_evaluator_launcher-0.1.21 → nemo_evaluator_launcher-0.1.64}/src/nemo_evaluator_launcher/__init__.py +0 -0
  47. {nemo_evaluator_launcher-0.1.21 → nemo_evaluator_launcher-0.1.64}/src/nemo_evaluator_launcher/api/__init__.py +0 -0
  48. {nemo_evaluator_launcher-0.1.21 → nemo_evaluator_launcher-0.1.64}/src/nemo_evaluator_launcher/api/utils.py +0 -0
  49. {nemo_evaluator_launcher-0.1.21 → nemo_evaluator_launcher-0.1.64}/src/nemo_evaluator_launcher/cli/__init__.py +0 -0
  50. {nemo_evaluator_launcher-0.1.21 → nemo_evaluator_launcher-0.1.64}/src/nemo_evaluator_launcher/cli/export.py +0 -0
  51. {nemo_evaluator_launcher-0.1.21 → nemo_evaluator_launcher-0.1.64}/src/nemo_evaluator_launcher/cli/info.py +0 -0
  52. {nemo_evaluator_launcher-0.1.21 → nemo_evaluator_launcher-0.1.64}/src/nemo_evaluator_launcher/cli/kill.py +0 -0
  53. {nemo_evaluator_launcher-0.1.21 → nemo_evaluator_launcher-0.1.64}/src/nemo_evaluator_launcher/cli/ls_runs.py +0 -0
  54. {nemo_evaluator_launcher-0.1.21 → nemo_evaluator_launcher-0.1.64}/src/nemo_evaluator_launcher/cli/status.py +0 -0
  55. {nemo_evaluator_launcher-0.1.21 → nemo_evaluator_launcher-0.1.64}/src/nemo_evaluator_launcher/common/__init__.py +0 -0
  56. {nemo_evaluator_launcher-0.1.21 → nemo_evaluator_launcher-0.1.64}/src/nemo_evaluator_launcher/common/execdb.py +0 -0
  57. {nemo_evaluator_launcher-0.1.21 → nemo_evaluator_launcher-0.1.64}/src/nemo_evaluator_launcher/configs/__init__.py +0 -0
  58. {nemo_evaluator_launcher-0.1.21 → nemo_evaluator_launcher-0.1.64}/src/nemo_evaluator_launcher/configs/default.yaml +0 -0
  59. {nemo_evaluator_launcher-0.1.21 → nemo_evaluator_launcher-0.1.64}/src/nemo_evaluator_launcher/configs/deployment/generic.yaml +0 -0
  60. {nemo_evaluator_launcher-0.1.21 → nemo_evaluator_launcher-0.1.64}/src/nemo_evaluator_launcher/configs/deployment/none.yaml +0 -0
  61. {nemo_evaluator_launcher-0.1.21 → nemo_evaluator_launcher-0.1.64}/src/nemo_evaluator_launcher/configs/execution/lepton/default.yaml +0 -0
  62. {nemo_evaluator_launcher-0.1.21 → nemo_evaluator_launcher-0.1.64}/src/nemo_evaluator_launcher/configs/execution/local.yaml +0 -0
  63. {nemo_evaluator_launcher-0.1.21 → nemo_evaluator_launcher-0.1.64}/src/nemo_evaluator_launcher/executors/__init__.py +0 -0
  64. {nemo_evaluator_launcher-0.1.21 → nemo_evaluator_launcher-0.1.64}/src/nemo_evaluator_launcher/executors/lepton/__init__.py +0 -0
  65. {nemo_evaluator_launcher-0.1.21 → nemo_evaluator_launcher-0.1.64}/src/nemo_evaluator_launcher/executors/lepton/job_helpers.py +0 -0
  66. {nemo_evaluator_launcher-0.1.21 → nemo_evaluator_launcher-0.1.64}/src/nemo_evaluator_launcher/executors/local/__init__.py +0 -0
  67. {nemo_evaluator_launcher-0.1.21 → nemo_evaluator_launcher-0.1.64}/src/nemo_evaluator_launcher/executors/registry.py +0 -0
  68. {nemo_evaluator_launcher-0.1.21 → nemo_evaluator_launcher-0.1.64}/src/nemo_evaluator_launcher/executors/slurm/__init__.py +0 -0
  69. {nemo_evaluator_launcher-0.1.21 → nemo_evaluator_launcher-0.1.64}/src/nemo_evaluator_launcher/exporters/__init__.py +0 -0
  70. {nemo_evaluator_launcher-0.1.21 → nemo_evaluator_launcher-0.1.64}/src/nemo_evaluator_launcher/exporters/base.py +0 -0
  71. {nemo_evaluator_launcher-0.1.21 → nemo_evaluator_launcher-0.1.64}/src/nemo_evaluator_launcher/exporters/gsheets.py +0 -0
  72. {nemo_evaluator_launcher-0.1.21 → nemo_evaluator_launcher-0.1.64}/src/nemo_evaluator_launcher/exporters/local.py +0 -0
  73. {nemo_evaluator_launcher-0.1.21 → nemo_evaluator_launcher-0.1.64}/src/nemo_evaluator_launcher/exporters/mlflow.py +0 -0
  74. {nemo_evaluator_launcher-0.1.21 → nemo_evaluator_launcher-0.1.64}/src/nemo_evaluator_launcher/exporters/registry.py +0 -0
  75. {nemo_evaluator_launcher-0.1.21 → nemo_evaluator_launcher-0.1.64}/src/nemo_evaluator_launcher/exporters/wandb.py +0 -0
  76. {nemo_evaluator_launcher-0.1.21 → nemo_evaluator_launcher-0.1.64}/src/nemo_evaluator_launcher.egg-info/dependency_links.txt +0 -0
  77. {nemo_evaluator_launcher-0.1.21 → nemo_evaluator_launcher-0.1.64}/src/nemo_evaluator_launcher.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nemo-evaluator-launcher
3
- Version: 0.1.21
3
+ Version: 0.1.64
4
4
  Summary: Launcher for the evaluations provided by NeMo Evaluator containers with different runtime backends
5
5
  Author: NVIDIA
6
6
  Author-email: nemo-toolkit@nvidia.com
@@ -458,7 +458,7 @@ License:
458
458
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
459
459
  SOFTWARE.
460
460
 
461
- Project-URL: homepage, https://github.com/NVIDIA-NeMo/Eval
461
+ Project-URL: homepage, https://github.com/NVIDIA-NeMo/Evaluator
462
462
  Project-URL: repository, https://github.com/NVIDIA-NeMo/Evaluator/packages/nemo-evaluator-launcher
463
463
  Keywords: deep learning,evaluations,machine learning,gpu,NLP,pytorch,torch
464
464
  Requires-Python: <3.14,>=3.10
@@ -467,6 +467,7 @@ License-File: LICENSE
467
467
  Requires-Dist: hydra-core<2.0.0,>=1.3.2
468
468
  Requires-Dist: jinja2<4.0.0,>=3.1.6
469
469
  Requires-Dist: leptonai>=0.25.0
470
+ Requires-Dist: nemo-evaluator
470
471
  Requires-Dist: pyyaml>=6.0.0
471
472
  Requires-Dist: requests>=2.32.4
472
473
  Requires-Dist: simple-parsing<0.2.0,>=0.1.7
@@ -478,7 +479,7 @@ Requires-Dist: mlflow>=2.8.0; extra == "mlflow"
478
479
  Provides-Extra: wandb
479
480
  Requires-Dist: wandb>=0.15.0; extra == "wandb"
480
481
  Provides-Extra: gsheets
481
- Requires-Dist: gsheets>=0.1.0; extra == "gsheets"
482
+ Requires-Dist: gspread>=5.0.0; extra == "gsheets"
482
483
  Provides-Extra: exporters
483
484
  Requires-Dist: mlflow; extra == "exporters"
484
485
  Requires-Dist: wandb; extra == "exporters"
@@ -4,6 +4,7 @@ dependencies = [
4
4
  "hydra-core>=1.3.2,<2.0.0",
5
5
  "jinja2>=3.1.6,<4.0.0",
6
6
  "leptonai>=0.25.0",
7
+ "nemo-evaluator",
7
8
  "pyyaml>=6.0.0",
8
9
  "requests>=2.32.4",
9
10
  "simple-parsing>=0.1.7,<0.2.0",
@@ -33,20 +34,21 @@ keywords = [
33
34
 
34
35
  [project.urls]
35
36
  # BEGIN(if-changed): check package_info.py
36
- homepage = "https://github.com/NVIDIA-NeMo/Eval"
37
+ homepage = "https://github.com/NVIDIA-NeMo/Evaluator"
37
38
  repository = "https://github.com/NVIDIA-NeMo/Evaluator/packages/nemo-evaluator-launcher"
38
39
  # END(if-changed)
39
40
 
40
41
  [project.optional-dependencies]
41
42
  mlflow = ["mlflow>=2.8.0"]
42
43
  wandb = ["wandb>=0.15.0"]
43
- gsheets = ["gsheets>=0.1.0"]
44
+ gsheets = ["gspread>=5.0.0"]
44
45
  exporters = ["mlflow", "wandb", "gsheets"]
45
46
  all = ["mlflow", "wandb", "gsheets"]
46
47
 
47
48
  [project.scripts]
48
49
  nemo-evaluator-launcher = "nemo_evaluator_launcher.cli.main:main"
49
50
  nv-eval = "nemo_evaluator_launcher.cli.main:main"
51
+ nel = "nemo_evaluator_launcher.cli.main:main"
50
52
 
51
53
  [dependency-groups]
52
54
  dev = [
@@ -75,6 +77,7 @@ where = ["src"]
75
77
  "resources/**/*",
76
78
  "configs/**/*",
77
79
  "executors/**/*.sh",
80
+ "executors/**/*.template",
78
81
  ]
79
82
 
80
83
  [tool.setuptools.dynamic]
@@ -18,8 +18,9 @@
18
18
  This module provides the main functional entry points for running evaluations, querying job status, and listing available tasks. These functions are intended to be used by CLI commands and external integrations.
19
19
  """
20
20
 
21
+ import copy
21
22
  from pathlib import Path
22
- from typing import Any, List, Optional, Union
23
+ from typing import Any, Dict, Iterator, List, Optional, Tuple, Union
23
24
 
24
25
  import yaml
25
26
  from omegaconf import DictConfig, OmegaConf
@@ -35,7 +36,7 @@ def get_tasks_list() -> list[list[Any]]:
35
36
  """Get a list of available tasks from the mapping.
36
37
 
37
38
  Returns:
38
- list[list[Any]]: Each sublist contains task name, endpoint type, harness, and container.
39
+ list[list[Any]]: Each sublist contains task name, endpoint type, harness, container, arch, description, and type.
39
40
  """
40
41
  mapping = load_tasks_mapping()
41
42
  data = [
@@ -44,6 +45,9 @@ def get_tasks_list() -> list[list[Any]]:
44
45
  task_data.get("endpoint_type"),
45
46
  task_data.get("harness"),
46
47
  task_data.get("container"),
48
+ task_data.get("arch", ""),
49
+ task_data.get("description", ""),
50
+ task_data.get("type", ""),
47
51
  ]
48
52
  for task_data in mapping.values()
49
53
  ]
@@ -75,12 +79,54 @@ def _validate_no_missing_values(cfg: Any, path: str = "") -> None:
75
79
  _validate_no_missing_values(value, current_path)
76
80
 
77
81
 
78
- def run_eval(cfg: RunConfig, dry_run: bool = False) -> Optional[str]:
82
+ def filter_tasks(cfg: RunConfig, task_names: list[str]) -> RunConfig:
83
+ """Filter evaluation tasks to only include specified task names.
84
+
85
+ Args:
86
+ cfg: The configuration object for the evaluation run.
87
+ task_names: List of task names to include (e.g., ["ifeval", "gsm8k"]).
88
+
89
+ Returns:
90
+ RunConfig: A new configuration with filtered tasks (input is not mutated).
91
+
92
+ Raises:
93
+ ValueError: If any requested task is not found in config or no tasks defined.
94
+ """
95
+ if not task_names:
96
+ return cfg
97
+
98
+ if not hasattr(cfg.evaluation, "tasks") or not cfg.evaluation.tasks:
99
+ raise ValueError("No tasks defined in config. Cannot filter tasks.")
100
+
101
+ requested_tasks = set(task_names)
102
+ original_tasks = cfg.evaluation.tasks
103
+ filtered_tasks = [task for task in original_tasks if task.name in requested_tasks]
104
+
105
+ # Fail if ANY requested tasks are not found
106
+ found_names = {task.name for task in filtered_tasks}
107
+ not_found = requested_tasks - found_names
108
+ if not_found:
109
+ available = [task.name for task in original_tasks]
110
+ raise ValueError(
111
+ f"Requested task(s) not found in config: {sorted(not_found)}. "
112
+ f"Available tasks: {available}"
113
+ )
114
+
115
+ # Create a deep copy to preserve input immutability
116
+ result = copy.deepcopy(cfg)
117
+ result.evaluation.tasks = filtered_tasks
118
+ return result
119
+
120
+
121
+ def run_eval(
122
+ cfg: RunConfig, dry_run: bool = False, tasks: Optional[list[str]] = None
123
+ ) -> Optional[str]:
79
124
  """Run evaluation with specified config and overrides.
80
125
 
81
126
  Args:
82
127
  cfg: The configuration object for the evaluation run.
83
128
  dry_run: If True, do not run the evaluation, just prepare scripts and save them.
129
+ tasks: Optional list of task names to run. If provided, only these tasks will be executed.
84
130
 
85
131
  Returns:
86
132
  Optional[str]: The invocation ID for the evaluation run.
@@ -89,6 +135,10 @@ def run_eval(cfg: RunConfig, dry_run: bool = False) -> Optional[str]:
89
135
  ValueError: If configuration validation fails or MISSING values are found.
90
136
  RuntimeError: If the executor fails to start the evaluation.
91
137
  """
138
+ # Filter tasks if specified
139
+ if tasks:
140
+ cfg = filter_tasks(cfg, tasks)
141
+
92
142
  # Validate that no MISSING values exist in the configuration
93
143
  _validate_no_missing_values(cfg)
94
144
 
@@ -116,6 +166,7 @@ def get_status(ids_or_prefixes: list[str]) -> list[dict[str, Any]]:
116
166
  db = ExecutionDB()
117
167
  results: List[dict[str, Any]] = []
118
168
 
169
+ # TODO(agronskiy): refactor the `.`-checking job in all the functions.
119
170
  for id_or_prefix in ids_or_prefixes:
120
171
  # If id looks like an invocation_id (no dot), get all jobs for it
121
172
  if "." not in id_or_prefix:
@@ -259,6 +310,108 @@ def get_status(ids_or_prefixes: list[str]) -> list[dict[str, Any]]:
259
310
  return results
260
311
 
261
312
 
313
+ def stream_logs(
314
+ ids_or_prefixes: Union[str, list[str]],
315
+ ) -> Iterator[Tuple[str, str, str]]:
316
+ """Stream logs from jobs or invocations by their IDs or invocation IDs.
317
+
318
+ Args:
319
+ ids_or_prefixes: Single ID/prefix or list of job IDs or invocation IDs to stream logs from.
320
+ Short prefixes are allowed, we would try to match the full ones from
321
+ prefixes if no collisions are present.
322
+
323
+ Yields:
324
+ Tuple[str, str, str]: Tuples of (job_id, task_name, log_line) for each log line.
325
+ Empty lines are yielded as empty strings.
326
+
327
+ Raises:
328
+ ValueError: If the executor doesn't support log streaming.
329
+ """
330
+ db = ExecutionDB()
331
+
332
+ # Normalize to list for consistent processing
333
+ if isinstance(ids_or_prefixes, str):
334
+ ids_or_prefixes = [ids_or_prefixes]
335
+
336
+ # Collect all jobs from all IDs, grouped by executor
337
+ executor_to_jobs: Dict[str, Dict[str, JobData]] = {}
338
+ executor_to_invocations: Dict[str, list[str]] = {}
339
+
340
+ # TODO(agronskiy): refactor the `.`-checking job in all the functions.
341
+ for id_or_prefix in ids_or_prefixes:
342
+ # Determine if this is a job ID or invocation ID
343
+ if "." in id_or_prefix:
344
+ # This is a job ID
345
+ job_data = db.get_job(id_or_prefix)
346
+ if job_data is None:
347
+ continue
348
+
349
+ executor = job_data.executor
350
+ if executor not in executor_to_jobs:
351
+ executor_to_jobs[executor] = {}
352
+ executor_to_jobs[executor][id_or_prefix] = job_data
353
+ else:
354
+ # This is an invocation ID
355
+ jobs = db.get_jobs(id_or_prefix)
356
+ if not jobs:
357
+ continue
358
+
359
+ # Get the executor class from the first job
360
+ first_job_data = next(iter(jobs.values()))
361
+ executor = first_job_data.executor
362
+ if executor not in executor_to_invocations:
363
+ executor_to_invocations[executor] = []
364
+ executor_to_invocations[executor].append(id_or_prefix)
365
+
366
+ # Stream logs from each executor simultaneously
367
+ # For each executor, collect all job IDs and stream them together
368
+ for executor, jobs_dict in executor_to_jobs.items():
369
+ try:
370
+ executor_cls = get_executor(executor)
371
+ except ValueError:
372
+ continue
373
+
374
+ # For local executor with multiple jobs, pass list to stream simultaneously
375
+ # For other executors or single jobs, pass individual job IDs
376
+ if executor == "local" and len(jobs_dict) > 1:
377
+ # Pass all job IDs as a list to stream simultaneously
378
+ try:
379
+ yield from executor_cls.stream_logs(
380
+ list(jobs_dict.keys()), executor_name=executor
381
+ )
382
+ except NotImplementedError:
383
+ raise ValueError(
384
+ f"Log streaming is not yet implemented for executor '{executor}'"
385
+ )
386
+ else:
387
+ # Single job or non-local executor
388
+ for job_id in jobs_dict.keys():
389
+ try:
390
+ yield from executor_cls.stream_logs(job_id, executor_name=executor)
391
+ except NotImplementedError:
392
+ raise ValueError(
393
+ f"Log streaming is not yet implemented for executor '{executor}'"
394
+ )
395
+
396
+ # Stream logs from invocation IDs
397
+ for executor, invocation_ids in executor_to_invocations.items():
398
+ try:
399
+ executor_cls = get_executor(executor)
400
+ except ValueError:
401
+ continue
402
+
403
+ # Stream each invocation (each invocation already handles multiple jobs internally)
404
+ for invocation_id in invocation_ids:
405
+ try:
406
+ yield from executor_cls.stream_logs(
407
+ invocation_id, executor_name=executor
408
+ )
409
+ except NotImplementedError:
410
+ raise ValueError(
411
+ f"Log streaming is not yet implemented for executor '{executor}'"
412
+ )
413
+
414
+
262
415
  def list_all_invocations_summary() -> list[dict[str, Any]]:
263
416
  """Return a concise per-invocation summary from the exec DB.
264
417
 
@@ -378,6 +531,7 @@ def kill_job_or_invocation(id: str) -> list[dict[str, Any]]:
378
531
  "data": {"error": f"Unexpected error: {str(e)}"},
379
532
  }
380
533
 
534
+ # TODO(agronskiy): refactor the `.`-checking job in all the functions.
381
535
  # Determine if this is a job ID or invocation ID
382
536
  if "." in id:
383
537
  # This is a job ID - kill single job
@@ -413,7 +567,7 @@ def kill_job_or_invocation(id: str) -> list[dict[str, Any]]:
413
567
 
414
568
 
415
569
  def export_results(
416
- invocation_ids: Union[str, List[str]],
570
+ invocation_ids: Union[str, list[str]],
417
571
  dest: str = "local",
418
572
  config: dict[Any, Any] | None = None,
419
573
  ) -> dict:
@@ -442,7 +596,7 @@ def export_results(
442
596
  if "." in single_id: # job_id
443
597
  # Try reading config from artifacts working dir (auto-export on remote node)
444
598
  cfg_file = None
445
- for name in ("run_config.yml", "config.yml"):
599
+ for name in ("config.yml", "run_config.yml"):
446
600
  p = Path(name)
447
601
  if p.exists():
448
602
  cfg_file = p
@@ -18,7 +18,7 @@
18
18
  This module defines data structures and helpers for configuration and type safety in the API layer.
19
19
  """
20
20
 
21
- import os
21
+ import pathlib
22
22
  import warnings
23
23
  from dataclasses import dataclass
24
24
  from typing import cast
@@ -42,33 +42,40 @@ from nemo_evaluator_launcher.common.logging_utils import logger
42
42
  class RunConfig(DictConfig):
43
43
  @staticmethod
44
44
  def from_hydra(
45
- config_name: str = "default",
46
- config_dir: str | None = None,
47
- hydra_overrides: list[str] = [],
48
- dict_overrides: dict = {},
45
+ config: str | None = None,
46
+ hydra_overrides: list[str] | None = None,
47
+ dict_overrides: dict | None = None,
49
48
  ) -> "RunConfig":
50
49
  """Load configuration from Hydra and merge with dictionary overrides.
51
50
 
52
51
  Args:
53
- config_name: Name of the Hydra configuration to load.
52
+ config: Optional full path to a config file (e.g. /path/to/my_config.yaml).
53
+ If omitted, loads the internal default config from
54
+ `nemo_evaluator_launcher.configs`.
54
55
  hydra_overrides: List of Hydra command-line style overrides.
55
56
  dict_overrides: Dictionary of configuration overrides to merge.
56
- config_dir: Optional path to user config directory. If provided, Hydra will
57
- search in this directory first, then fall back to internal configs.
58
57
 
59
58
  Returns:
60
59
  RunConfig: Merged configuration object.
61
60
  """
62
- overrides = hydra_overrides.copy()
61
+ overrides = list(hydra_overrides or [])
62
+ dict_overrides = dict_overrides or {}
63
+
64
+ resolved_config_path: str | None = None
65
+ config_name = "default"
66
+
63
67
  # Check if a GlobalHydra instance is already initialized and clear it
64
68
  if GlobalHydra.instance().is_initialized():
65
69
  GlobalHydra.instance().clear()
66
70
 
67
- if config_dir:
68
- # Convert relative path to absolute path if needed
69
- if not os.path.isabs(config_dir):
70
- config_dir = os.path.abspath(config_dir)
71
+ if config:
72
+ config_path = pathlib.Path(config).expanduser()
73
+ if not config_path.is_absolute():
74
+ config_path = (pathlib.Path.cwd() / config_path).resolve()
75
+ resolved_config_path = str(config_path)
71
76
 
77
+ config_dir = str(config_path.parent)
78
+ config_name = str(config_path.stem)
72
79
  hydra.initialize_config_dir(
73
80
  config_dir=config_dir,
74
81
  version_base=None,
@@ -90,7 +97,7 @@ class RunConfig(DictConfig):
90
97
  logger.debug(
91
98
  "Loaded run config from hydra",
92
99
  config_name=config_name,
93
- config_dir=config_dir,
100
+ config=resolved_config_path,
94
101
  overrides=hydra_overrides,
95
102
  dict_overrides=dict_overrides,
96
103
  result=cfg,
@@ -0,0 +1,102 @@
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
+ """Logs command for streaming logs from evaluation jobs."""
17
+
18
+ import sys
19
+ from dataclasses import dataclass
20
+ from typing import Callable, Dict
21
+
22
+ from simple_parsing import field
23
+
24
+ import nemo_evaluator_launcher.common.printing_utils as pu
25
+ from nemo_evaluator_launcher.api.functional import stream_logs
26
+ from nemo_evaluator_launcher.common.execdb import ExecutionDB
27
+ from nemo_evaluator_launcher.common.logging_utils import logger
28
+
29
+
30
+ @dataclass
31
+ class Cmd:
32
+ """Logs command configuration."""
33
+
34
+ ids: list[str] = field(
35
+ default_factory=list,
36
+ positional=True,
37
+ help="Invocation IDs or job IDs (e.g., '15b9f667' or '15b9f667.0'). Multiple IDs can be provided.",
38
+ )
39
+
40
+ def execute(self) -> None:
41
+ """Execute the logs command to stream logs from jobs."""
42
+ if not self.ids:
43
+ logger.error("At least one ID is required")
44
+ sys.exit(1)
45
+
46
+ db = ExecutionDB()
47
+
48
+ # Validate all IDs exist
49
+ all_job_ids = []
50
+ for id_or_prefix in self.ids:
51
+ if "." in id_or_prefix:
52
+ # This is a job ID - get single job
53
+ job_data = db.get_job(id_or_prefix)
54
+ if job_data is None:
55
+ logger.error(f"Job {id_or_prefix} not found")
56
+ sys.exit(1)
57
+ all_job_ids.append(id_or_prefix)
58
+ else:
59
+ # This is an invocation ID - get all jobs
60
+ jobs = db.get_jobs(id_or_prefix)
61
+ if not jobs:
62
+ logger.error(f"Invocation {id_or_prefix} not found")
63
+ sys.exit(1)
64
+ all_job_ids.extend(jobs.keys())
65
+
66
+ # Build color mapping for job IDs
67
+ colors = [pu.red, pu.green, pu.yellow, pu.magenta, pu.cyan]
68
+ job_colors: Dict[str, Callable[[str], str]] = {}
69
+ color_index = 0
70
+
71
+ for job_id in all_job_ids:
72
+ job_colors[job_id] = colors[color_index % len(colors)]
73
+ color_index += 1
74
+
75
+ # Stream logs from executor
76
+ try:
77
+ log_stream = stream_logs(self.ids)
78
+ for job_id, task_name, log_line in log_stream:
79
+ # Extract short prefix: first 6 chars of invocation ID + job number
80
+ if "." in job_id:
81
+ inv_id, job_num = job_id.split(".", 1)
82
+ short_prefix = f"{inv_id[:6]}.{job_num}"
83
+ else:
84
+ short_prefix = job_id[:6]
85
+ prefix = f"{short_prefix}:"
86
+ color_func = job_colors.get(job_id, pu.grey)
87
+ if log_line:
88
+ print(f"{color_func(prefix)} {log_line}")
89
+ else:
90
+ # Print empty lines without prefix
91
+ print()
92
+
93
+ except ValueError:
94
+ # Handle case where executor doesn't support streaming
95
+ # Warning already logged by BaseExecutor.stream_logs
96
+ pass
97
+ except KeyboardInterrupt:
98
+ # Clean exit on Ctrl+C
99
+ pass
100
+ except Exception as e:
101
+ logger.error(f"Error streaming logs: {e}")
102
+ sys.exit(1)