nemo-evaluator-launcher 0.1.0rc6__py3-none-any.whl → 0.1.41__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.
- nemo_evaluator_launcher/__init__.py +15 -1
- nemo_evaluator_launcher/api/functional.py +188 -27
- nemo_evaluator_launcher/api/types.py +9 -0
- nemo_evaluator_launcher/cli/export.py +131 -12
- nemo_evaluator_launcher/cli/info.py +477 -82
- nemo_evaluator_launcher/cli/kill.py +5 -3
- nemo_evaluator_launcher/cli/logs.py +102 -0
- nemo_evaluator_launcher/cli/ls_runs.py +31 -10
- nemo_evaluator_launcher/cli/ls_tasks.py +105 -3
- nemo_evaluator_launcher/cli/main.py +101 -5
- nemo_evaluator_launcher/cli/run.py +153 -30
- nemo_evaluator_launcher/cli/status.py +49 -5
- nemo_evaluator_launcher/cli/version.py +26 -23
- nemo_evaluator_launcher/common/execdb.py +121 -27
- nemo_evaluator_launcher/common/helpers.py +213 -33
- nemo_evaluator_launcher/common/logging_utils.py +16 -5
- nemo_evaluator_launcher/common/printing_utils.py +100 -0
- nemo_evaluator_launcher/configs/deployment/generic.yaml +33 -0
- nemo_evaluator_launcher/configs/deployment/sglang.yaml +4 -2
- nemo_evaluator_launcher/configs/deployment/trtllm.yaml +23 -0
- nemo_evaluator_launcher/configs/deployment/vllm.yaml +2 -2
- nemo_evaluator_launcher/configs/execution/local.yaml +2 -0
- nemo_evaluator_launcher/configs/execution/slurm/default.yaml +19 -4
- nemo_evaluator_launcher/executors/base.py +54 -1
- nemo_evaluator_launcher/executors/lepton/deployment_helpers.py +60 -5
- nemo_evaluator_launcher/executors/lepton/executor.py +240 -101
- nemo_evaluator_launcher/executors/lepton/job_helpers.py +15 -11
- nemo_evaluator_launcher/executors/local/executor.py +492 -56
- nemo_evaluator_launcher/executors/local/run.template.sh +76 -9
- nemo_evaluator_launcher/executors/slurm/executor.py +571 -98
- nemo_evaluator_launcher/executors/slurm/proxy.cfg.template +26 -0
- nemo_evaluator_launcher/exporters/base.py +9 -0
- nemo_evaluator_launcher/exporters/gsheets.py +27 -9
- nemo_evaluator_launcher/exporters/local.py +30 -16
- nemo_evaluator_launcher/exporters/mlflow.py +245 -74
- nemo_evaluator_launcher/exporters/utils.py +139 -184
- nemo_evaluator_launcher/exporters/wandb.py +157 -43
- nemo_evaluator_launcher/package_info.py +6 -3
- nemo_evaluator_launcher/resources/mapping.toml +56 -15
- nemo_evaluator_launcher-0.1.41.dist-info/METADATA +494 -0
- nemo_evaluator_launcher-0.1.41.dist-info/RECORD +62 -0
- {nemo_evaluator_launcher-0.1.0rc6.dist-info → nemo_evaluator_launcher-0.1.41.dist-info}/entry_points.txt +1 -0
- nemo_evaluator_launcher-0.1.0rc6.dist-info/METADATA +0 -35
- nemo_evaluator_launcher-0.1.0rc6.dist-info/RECORD +0 -57
- {nemo_evaluator_launcher-0.1.0rc6.dist-info → nemo_evaluator_launcher-0.1.41.dist-info}/WHEEL +0 -0
- {nemo_evaluator_launcher-0.1.0rc6.dist-info → nemo_evaluator_launcher-0.1.41.dist-info}/licenses/LICENSE +0 -0
- {nemo_evaluator_launcher-0.1.0rc6.dist-info → nemo_evaluator_launcher-0.1.41.dist-info}/top_level.txt +0 -0
|
@@ -20,6 +20,7 @@ It automatically initializes logging and conditionally loads internal components
|
|
|
20
20
|
"""
|
|
21
21
|
|
|
22
22
|
import importlib
|
|
23
|
+
import warnings
|
|
23
24
|
|
|
24
25
|
from nemo_evaluator_launcher.common.logging_utils import logger
|
|
25
26
|
from nemo_evaluator_launcher.package_info import (
|
|
@@ -32,9 +33,22 @@ from nemo_evaluator_launcher.package_info import (
|
|
|
32
33
|
__version__,
|
|
33
34
|
)
|
|
34
35
|
|
|
35
|
-
|
|
36
|
+
# Suppress pydantic warnings from third-party libraries (e.g., wandb) that are not
|
|
37
|
+
# compatible with Pydantic 2.x field metadata on Python 3.13+
|
|
38
|
+
warnings.filterwarnings(
|
|
39
|
+
"ignore",
|
|
40
|
+
message=r"The 'repr' attribute.*Field\(\).*",
|
|
41
|
+
category=Warning,
|
|
42
|
+
)
|
|
43
|
+
warnings.filterwarnings(
|
|
44
|
+
"ignore",
|
|
45
|
+
message=r"The 'frozen' attribute.*Field\(\).*",
|
|
46
|
+
category=Warning,
|
|
47
|
+
)
|
|
36
48
|
|
|
37
49
|
|
|
50
|
+
logger.info("Version info", pkg=__package_name__, ver=__version__)
|
|
51
|
+
|
|
38
52
|
try:
|
|
39
53
|
importlib.import_module("nemo_evaluator_launcher_internal")
|
|
40
54
|
logger.debug(
|
|
@@ -18,8 +18,10 @@
|
|
|
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
|
-
from
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Any, Dict, Iterator, List, Optional, Tuple, Union
|
|
22
23
|
|
|
24
|
+
import yaml
|
|
23
25
|
from omegaconf import DictConfig, OmegaConf
|
|
24
26
|
|
|
25
27
|
from nemo_evaluator_launcher.api.types import RunConfig
|
|
@@ -97,11 +99,13 @@ def run_eval(cfg: RunConfig, dry_run: bool = False) -> Optional[str]:
|
|
|
97
99
|
return get_executor(cfg.execution.type).execute_eval(cfg, dry_run)
|
|
98
100
|
|
|
99
101
|
|
|
100
|
-
def get_status(
|
|
102
|
+
def get_status(ids_or_prefixes: list[str]) -> list[dict[str, Any]]:
|
|
101
103
|
"""Get status of jobs by their IDs or invocation IDs.
|
|
102
104
|
|
|
103
105
|
Args:
|
|
104
|
-
job_ids: List of job IDs or invocation IDs to check status for.
|
|
106
|
+
job_ids: List of job IDs or invocation IDs to check status for. Short ones are allowed,
|
|
107
|
+
we would try to match the full ones from prefixes if no collisions are
|
|
108
|
+
present.
|
|
105
109
|
|
|
106
110
|
Returns:
|
|
107
111
|
list[dict[str, Any]]: List of status dictionaries for each job or invocation.
|
|
@@ -112,14 +116,15 @@ def get_status(job_ids: list[str]) -> list[dict[str, Any]]:
|
|
|
112
116
|
db = ExecutionDB()
|
|
113
117
|
results: List[dict[str, Any]] = []
|
|
114
118
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
+
# TODO(agronskiy): refactor the `.`-checking job in all the functions.
|
|
120
|
+
for id_or_prefix in ids_or_prefixes:
|
|
121
|
+
# If id looks like an invocation_id (no dot), get all jobs for it
|
|
122
|
+
if "." not in id_or_prefix:
|
|
123
|
+
jobs = db.get_jobs(id_or_prefix)
|
|
119
124
|
if not jobs:
|
|
120
125
|
results.append(
|
|
121
126
|
{
|
|
122
|
-
"invocation":
|
|
127
|
+
"invocation": id_or_prefix,
|
|
123
128
|
"job_id": None,
|
|
124
129
|
"status": "not_found",
|
|
125
130
|
"data": {},
|
|
@@ -134,7 +139,7 @@ def get_status(job_ids: list[str]) -> list[dict[str, Any]]:
|
|
|
134
139
|
except ValueError as e:
|
|
135
140
|
results.append(
|
|
136
141
|
{
|
|
137
|
-
"invocation":
|
|
142
|
+
"invocation": id_or_prefix,
|
|
138
143
|
"job_id": None,
|
|
139
144
|
"status": "error",
|
|
140
145
|
"data": {"error": str(e)},
|
|
@@ -144,7 +149,7 @@ def get_status(job_ids: list[str]) -> list[dict[str, Any]]:
|
|
|
144
149
|
|
|
145
150
|
# Get status from the executor for all jobs in the invocation
|
|
146
151
|
try:
|
|
147
|
-
status_list = executor_cls.get_status(
|
|
152
|
+
status_list = executor_cls.get_status(id_or_prefix)
|
|
148
153
|
|
|
149
154
|
# Create a result for each job in the invocation
|
|
150
155
|
for job_id_in_invocation, job_data in jobs.items():
|
|
@@ -159,7 +164,7 @@ def get_status(job_ids: list[str]) -> list[dict[str, Any]]:
|
|
|
159
164
|
|
|
160
165
|
results.append(
|
|
161
166
|
{
|
|
162
|
-
"invocation":
|
|
167
|
+
"invocation": job_data.invocation_id,
|
|
163
168
|
"job_id": job_id_in_invocation,
|
|
164
169
|
"status": (
|
|
165
170
|
job_status if job_status is not None else "unknown"
|
|
@@ -174,7 +179,7 @@ def get_status(job_ids: list[str]) -> list[dict[str, Any]]:
|
|
|
174
179
|
except Exception as e:
|
|
175
180
|
results.append(
|
|
176
181
|
{
|
|
177
|
-
"invocation":
|
|
182
|
+
"invocation": id_or_prefix,
|
|
178
183
|
"job_id": None,
|
|
179
184
|
"status": "error",
|
|
180
185
|
"data": {"error": str(e)},
|
|
@@ -182,13 +187,13 @@ def get_status(job_ids: list[str]) -> list[dict[str, Any]]:
|
|
|
182
187
|
)
|
|
183
188
|
else:
|
|
184
189
|
# Otherwise, treat as job_id
|
|
185
|
-
single_job_data: Optional[JobData] = db.get_job(
|
|
190
|
+
single_job_data: Optional[JobData] = db.get_job(id_or_prefix)
|
|
186
191
|
|
|
187
192
|
if single_job_data is None:
|
|
188
193
|
results.append(
|
|
189
194
|
{
|
|
190
195
|
"invocation": None,
|
|
191
|
-
"job_id":
|
|
196
|
+
"job_id": id_or_prefix,
|
|
192
197
|
"status": "not_found",
|
|
193
198
|
"data": {},
|
|
194
199
|
}
|
|
@@ -202,7 +207,7 @@ def get_status(job_ids: list[str]) -> list[dict[str, Any]]:
|
|
|
202
207
|
results.append(
|
|
203
208
|
{
|
|
204
209
|
"invocation": None,
|
|
205
|
-
"job_id":
|
|
210
|
+
"job_id": id_or_prefix,
|
|
206
211
|
"status": "error",
|
|
207
212
|
"data": {"error": str(e)},
|
|
208
213
|
}
|
|
@@ -211,13 +216,13 @@ def get_status(job_ids: list[str]) -> list[dict[str, Any]]:
|
|
|
211
216
|
|
|
212
217
|
# Get status from the executor
|
|
213
218
|
try:
|
|
214
|
-
status_list = executor_cls.get_status(
|
|
219
|
+
status_list = executor_cls.get_status(id_or_prefix)
|
|
215
220
|
|
|
216
221
|
if not status_list:
|
|
217
222
|
results.append(
|
|
218
223
|
{
|
|
219
224
|
"invocation": single_job_data.invocation_id,
|
|
220
|
-
"job_id": job_id,
|
|
225
|
+
"job_id": single_job_data.job_id,
|
|
221
226
|
"status": "unknown",
|
|
222
227
|
"data": single_job_data.data,
|
|
223
228
|
}
|
|
@@ -227,7 +232,7 @@ def get_status(job_ids: list[str]) -> list[dict[str, Any]]:
|
|
|
227
232
|
results.append(
|
|
228
233
|
{
|
|
229
234
|
"invocation": single_job_data.invocation_id,
|
|
230
|
-
"job_id": job_id,
|
|
235
|
+
"job_id": single_job_data.job_id,
|
|
231
236
|
"status": (
|
|
232
237
|
status_list[0].state.value if status_list else "unknown"
|
|
233
238
|
),
|
|
@@ -244,7 +249,9 @@ def get_status(job_ids: list[str]) -> list[dict[str, Any]]:
|
|
|
244
249
|
"invocation": (
|
|
245
250
|
single_job_data.invocation_id if single_job_data else None
|
|
246
251
|
),
|
|
247
|
-
"job_id":
|
|
252
|
+
"job_id": (
|
|
253
|
+
single_job_data.job_id if single_job_data else id_or_prefix
|
|
254
|
+
),
|
|
248
255
|
"status": "error",
|
|
249
256
|
"data": {"error": str(e)},
|
|
250
257
|
}
|
|
@@ -253,6 +260,108 @@ def get_status(job_ids: list[str]) -> list[dict[str, Any]]:
|
|
|
253
260
|
return results
|
|
254
261
|
|
|
255
262
|
|
|
263
|
+
def stream_logs(
|
|
264
|
+
ids_or_prefixes: Union[str, list[str]],
|
|
265
|
+
) -> Iterator[Tuple[str, str, str]]:
|
|
266
|
+
"""Stream logs from jobs or invocations by their IDs or invocation IDs.
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
ids_or_prefixes: Single ID/prefix or list of job IDs or invocation IDs to stream logs from.
|
|
270
|
+
Short prefixes are allowed, we would try to match the full ones from
|
|
271
|
+
prefixes if no collisions are present.
|
|
272
|
+
|
|
273
|
+
Yields:
|
|
274
|
+
Tuple[str, str, str]: Tuples of (job_id, task_name, log_line) for each log line.
|
|
275
|
+
Empty lines are yielded as empty strings.
|
|
276
|
+
|
|
277
|
+
Raises:
|
|
278
|
+
ValueError: If the executor doesn't support log streaming.
|
|
279
|
+
"""
|
|
280
|
+
db = ExecutionDB()
|
|
281
|
+
|
|
282
|
+
# Normalize to list for consistent processing
|
|
283
|
+
if isinstance(ids_or_prefixes, str):
|
|
284
|
+
ids_or_prefixes = [ids_or_prefixes]
|
|
285
|
+
|
|
286
|
+
# Collect all jobs from all IDs, grouped by executor
|
|
287
|
+
executor_to_jobs: Dict[str, Dict[str, JobData]] = {}
|
|
288
|
+
executor_to_invocations: Dict[str, List[str]] = {}
|
|
289
|
+
|
|
290
|
+
# TODO(agronskiy): refactor the `.`-checking job in all the functions.
|
|
291
|
+
for id_or_prefix in ids_or_prefixes:
|
|
292
|
+
# Determine if this is a job ID or invocation ID
|
|
293
|
+
if "." in id_or_prefix:
|
|
294
|
+
# This is a job ID
|
|
295
|
+
job_data = db.get_job(id_or_prefix)
|
|
296
|
+
if job_data is None:
|
|
297
|
+
continue
|
|
298
|
+
|
|
299
|
+
executor = job_data.executor
|
|
300
|
+
if executor not in executor_to_jobs:
|
|
301
|
+
executor_to_jobs[executor] = {}
|
|
302
|
+
executor_to_jobs[executor][id_or_prefix] = job_data
|
|
303
|
+
else:
|
|
304
|
+
# This is an invocation ID
|
|
305
|
+
jobs = db.get_jobs(id_or_prefix)
|
|
306
|
+
if not jobs:
|
|
307
|
+
continue
|
|
308
|
+
|
|
309
|
+
# Get the executor class from the first job
|
|
310
|
+
first_job_data = next(iter(jobs.values()))
|
|
311
|
+
executor = first_job_data.executor
|
|
312
|
+
if executor not in executor_to_invocations:
|
|
313
|
+
executor_to_invocations[executor] = []
|
|
314
|
+
executor_to_invocations[executor].append(id_or_prefix)
|
|
315
|
+
|
|
316
|
+
# Stream logs from each executor simultaneously
|
|
317
|
+
# For each executor, collect all job IDs and stream them together
|
|
318
|
+
for executor, jobs_dict in executor_to_jobs.items():
|
|
319
|
+
try:
|
|
320
|
+
executor_cls = get_executor(executor)
|
|
321
|
+
except ValueError:
|
|
322
|
+
continue
|
|
323
|
+
|
|
324
|
+
# For local executor with multiple jobs, pass list to stream simultaneously
|
|
325
|
+
# For other executors or single jobs, pass individual job IDs
|
|
326
|
+
if executor == "local" and len(jobs_dict) > 1:
|
|
327
|
+
# Pass all job IDs as a list to stream simultaneously
|
|
328
|
+
try:
|
|
329
|
+
yield from executor_cls.stream_logs(
|
|
330
|
+
list(jobs_dict.keys()), executor_name=executor
|
|
331
|
+
)
|
|
332
|
+
except NotImplementedError:
|
|
333
|
+
raise ValueError(
|
|
334
|
+
f"Log streaming is not yet implemented for executor '{executor}'"
|
|
335
|
+
)
|
|
336
|
+
else:
|
|
337
|
+
# Single job or non-local executor
|
|
338
|
+
for job_id in jobs_dict.keys():
|
|
339
|
+
try:
|
|
340
|
+
yield from executor_cls.stream_logs(job_id, executor_name=executor)
|
|
341
|
+
except NotImplementedError:
|
|
342
|
+
raise ValueError(
|
|
343
|
+
f"Log streaming is not yet implemented for executor '{executor}'"
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
# Stream logs from invocation IDs
|
|
347
|
+
for executor, invocation_ids in executor_to_invocations.items():
|
|
348
|
+
try:
|
|
349
|
+
executor_cls = get_executor(executor)
|
|
350
|
+
except ValueError:
|
|
351
|
+
continue
|
|
352
|
+
|
|
353
|
+
# Stream each invocation (each invocation already handles multiple jobs internally)
|
|
354
|
+
for invocation_id in invocation_ids:
|
|
355
|
+
try:
|
|
356
|
+
yield from executor_cls.stream_logs(
|
|
357
|
+
invocation_id, executor_name=executor
|
|
358
|
+
)
|
|
359
|
+
except NotImplementedError:
|
|
360
|
+
raise ValueError(
|
|
361
|
+
f"Log streaming is not yet implemented for executor '{executor}'"
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
|
|
256
365
|
def list_all_invocations_summary() -> list[dict[str, Any]]:
|
|
257
366
|
"""Return a concise per-invocation summary from the exec DB.
|
|
258
367
|
|
|
@@ -372,6 +481,7 @@ def kill_job_or_invocation(id: str) -> list[dict[str, Any]]:
|
|
|
372
481
|
"data": {"error": f"Unexpected error: {str(e)}"},
|
|
373
482
|
}
|
|
374
483
|
|
|
484
|
+
# TODO(agronskiy): refactor the `.`-checking job in all the functions.
|
|
375
485
|
# Determine if this is a job ID or invocation ID
|
|
376
486
|
if "." in id:
|
|
377
487
|
# This is a job ID - kill single job
|
|
@@ -434,14 +544,66 @@ def export_results(
|
|
|
434
544
|
single_id = invocation_ids[0]
|
|
435
545
|
|
|
436
546
|
if "." in single_id: # job_id
|
|
437
|
-
|
|
547
|
+
# Try reading config from artifacts working dir (auto-export on remote node)
|
|
548
|
+
cfg_file = None
|
|
549
|
+
for name in ("run_config.yml", "config.yml"):
|
|
550
|
+
p = Path(name)
|
|
551
|
+
if p.exists():
|
|
552
|
+
cfg_file = p
|
|
553
|
+
break
|
|
554
|
+
|
|
555
|
+
md_job_data = None
|
|
556
|
+
if cfg_file:
|
|
557
|
+
try:
|
|
558
|
+
cfg_yaml = (
|
|
559
|
+
yaml.safe_load(cfg_file.read_text(encoding="utf-8")) or {}
|
|
560
|
+
)
|
|
561
|
+
|
|
562
|
+
# Merge exporter override file if present
|
|
563
|
+
ypath_export = Path("export_config.yml")
|
|
564
|
+
if ypath_export.exists():
|
|
565
|
+
exp_yaml = (
|
|
566
|
+
yaml.safe_load(ypath_export.read_text(encoding="utf-8"))
|
|
567
|
+
or {}
|
|
568
|
+
)
|
|
569
|
+
exec_cfg = cfg_yaml.get("execution") or {}
|
|
570
|
+
auto_exp = (exp_yaml.get("execution") or {}).get(
|
|
571
|
+
"auto_export"
|
|
572
|
+
)
|
|
573
|
+
if auto_exp is not None:
|
|
574
|
+
exec_cfg["auto_export"] = auto_exp
|
|
575
|
+
cfg_yaml["execution"] = exec_cfg
|
|
576
|
+
if "export" in exp_yaml:
|
|
577
|
+
cfg_yaml["export"] = exp_yaml["export"]
|
|
578
|
+
if "evaluation" in exp_yaml and exp_yaml["evaluation"]:
|
|
579
|
+
eval_cfg = cfg_yaml.get("evaluation") or {}
|
|
580
|
+
eval_cfg.update(exp_yaml["evaluation"])
|
|
581
|
+
cfg_yaml["evaluation"] = eval_cfg
|
|
582
|
+
|
|
583
|
+
executor_name = (cfg_yaml.get("execution") or {}).get(
|
|
584
|
+
"type", "local"
|
|
585
|
+
)
|
|
586
|
+
md_job_data = JobData(
|
|
587
|
+
invocation_id=single_id.split(".")[0],
|
|
588
|
+
job_id=single_id,
|
|
589
|
+
timestamp=0.0,
|
|
590
|
+
executor=executor_name, # ensures slurm tag is preserved
|
|
591
|
+
data={
|
|
592
|
+
"output_dir": str(Path.cwd().parent),
|
|
593
|
+
"storage_type": "remote_local", # no SSH in auto-export path
|
|
594
|
+
},
|
|
595
|
+
config=cfg_yaml,
|
|
596
|
+
)
|
|
597
|
+
except Exception:
|
|
598
|
+
md_job_data = None
|
|
599
|
+
|
|
600
|
+
job_data = md_job_data or ExecutionDB().get_job(single_id)
|
|
438
601
|
if job_data is None:
|
|
439
602
|
return {
|
|
440
603
|
"success": False,
|
|
441
604
|
"error": f"Job {single_id} not found in ExecutionDB",
|
|
442
605
|
}
|
|
443
606
|
|
|
444
|
-
# Convert single job result to invocation-like structure
|
|
445
607
|
job_result = exporter.export_job(job_data)
|
|
446
608
|
return {
|
|
447
609
|
"success": job_result.success,
|
|
@@ -451,14 +613,14 @@ def export_results(
|
|
|
451
613
|
"success": job_result.success,
|
|
452
614
|
"message": job_result.message,
|
|
453
615
|
"metadata": job_result.metadata or {},
|
|
616
|
+
"dest": getattr(job_result, "dest", None),
|
|
454
617
|
}
|
|
455
618
|
},
|
|
456
619
|
"metadata": job_result.metadata or {},
|
|
457
620
|
}
|
|
621
|
+
|
|
458
622
|
elif single_id.isdigit(): # pipeline_id
|
|
459
|
-
# Find job by pipeline_id
|
|
460
623
|
db = ExecutionDB()
|
|
461
|
-
# Search all jobs for matching pipeline_id
|
|
462
624
|
for job_id, job_data in db._jobs.items():
|
|
463
625
|
if job_data.data.get("pipeline_id") == int(single_id):
|
|
464
626
|
job_result = exporter.export_job(job_data)
|
|
@@ -475,14 +637,13 @@ def export_results(
|
|
|
475
637
|
"metadata": job_result.metadata or {},
|
|
476
638
|
}
|
|
477
639
|
return {"success": False, "error": f"Pipeline {single_id} not found"}
|
|
640
|
+
|
|
478
641
|
else: # invocation_id
|
|
479
642
|
result = exporter.export_invocation(single_id)
|
|
480
|
-
# Ensure metadata is present in job results to prevent KeyError
|
|
481
643
|
if "jobs" in result:
|
|
482
644
|
for job_id, job_result in result["jobs"].items():
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
return result # type: ignore[no-any-return]
|
|
645
|
+
job_result.setdefault("metadata", {})
|
|
646
|
+
return result
|
|
486
647
|
else:
|
|
487
648
|
# Multiple IDs - parse and group
|
|
488
649
|
db = ExecutionDB()
|
|
@@ -19,9 +19,18 @@ This module defines data structures and helpers for configuration and type safet
|
|
|
19
19
|
"""
|
|
20
20
|
|
|
21
21
|
import os
|
|
22
|
+
import warnings
|
|
22
23
|
from dataclasses import dataclass
|
|
23
24
|
from typing import cast
|
|
24
25
|
|
|
26
|
+
# ruff: noqa: E402
|
|
27
|
+
# Later when adding optional module to hydra, since the internal package is optional,
|
|
28
|
+
# will generate a hydra warning. We suppress it as distraction and bad UX, before hydra gets invoked.
|
|
29
|
+
warnings.filterwarnings(
|
|
30
|
+
"ignore",
|
|
31
|
+
message="provider=hydra.searchpath.*path=nemo_evaluator_launcher_internal.*is not available\\.",
|
|
32
|
+
)
|
|
33
|
+
|
|
25
34
|
import hydra
|
|
26
35
|
from hydra.core.global_hydra import GlobalHydra
|
|
27
36
|
from omegaconf import DictConfig, OmegaConf
|
|
@@ -20,8 +20,6 @@ from typing import Any, List, Optional
|
|
|
20
20
|
|
|
21
21
|
from simple_parsing import field
|
|
22
22
|
|
|
23
|
-
from nemo_evaluator_launcher.api.functional import export_results
|
|
24
|
-
|
|
25
23
|
|
|
26
24
|
@dataclass
|
|
27
25
|
class ExportCmd:
|
|
@@ -29,8 +27,8 @@ class ExportCmd:
|
|
|
29
27
|
|
|
30
28
|
# Short usage examples will show up in -h as the class docstring:
|
|
31
29
|
# Examples:
|
|
32
|
-
# nemo-evaluator-launcher export 8abcd123 --dest local --format json
|
|
33
|
-
# nemo-evaluator-launcher export 8abcd123.0 9ef01234 --dest local --format csv
|
|
30
|
+
# nemo-evaluator-launcher export 8abcd123 --dest local --format json --out .
|
|
31
|
+
# nemo-evaluator-launcher export 8abcd123.0 9ef01234 --dest local --format csv --out results/ -fname processed_results.csv
|
|
34
32
|
# nemo-evaluator-launcher export 8abcd123 --dest jet
|
|
35
33
|
|
|
36
34
|
invocation_ids: List[str] = field(
|
|
@@ -43,9 +41,17 @@ class ExportCmd:
|
|
|
43
41
|
choices=["local", "wandb", "mlflow", "gsheets", "jet"],
|
|
44
42
|
help="Export destination.",
|
|
45
43
|
)
|
|
44
|
+
# overrides for exporter config; use -o similar to run command
|
|
45
|
+
override: List[str] = field(
|
|
46
|
+
default_factory=list,
|
|
47
|
+
action="append",
|
|
48
|
+
nargs="?",
|
|
49
|
+
alias=["-o", "--override"],
|
|
50
|
+
help="Hydra-style overrides for exporter config. Use `export.<dest>.key=value` (e.g., -o export.wandb.entity=org-name).",
|
|
51
|
+
)
|
|
46
52
|
output_dir: Optional[str] = field(
|
|
47
53
|
default=".",
|
|
48
|
-
alias=["--output-dir", "-
|
|
54
|
+
alias=["--output-dir", "-out"],
|
|
49
55
|
help="Output directory (default: current directory).",
|
|
50
56
|
)
|
|
51
57
|
output_filename: Optional[str] = field(
|
|
@@ -69,17 +75,29 @@ class ExportCmd:
|
|
|
69
75
|
alias=["--log-metrics"],
|
|
70
76
|
help="Filter metrics by name (repeatable). Examples: score, f1, mmlu_score_micro.",
|
|
71
77
|
)
|
|
72
|
-
only_required: bool = field(
|
|
73
|
-
default=
|
|
78
|
+
only_required: Optional[bool] = field(
|
|
79
|
+
default=None,
|
|
74
80
|
alias=["--only-required"],
|
|
75
81
|
help="Copy only required+optional artifacts (default: True). Set to False to copy all available artifacts.",
|
|
76
82
|
)
|
|
77
83
|
|
|
78
84
|
def execute(self) -> None:
|
|
79
85
|
"""Execute export."""
|
|
86
|
+
# Import heavy dependencies only when needed
|
|
87
|
+
from omegaconf import OmegaConf
|
|
88
|
+
|
|
89
|
+
from nemo_evaluator_launcher.api.functional import export_results
|
|
90
|
+
|
|
91
|
+
# Validation: ensure IDs are provided
|
|
92
|
+
if not self.invocation_ids:
|
|
93
|
+
print("Error: No IDs provided. Specify one or more invocation or job IDs.")
|
|
94
|
+
print(
|
|
95
|
+
"Usage: nemo-evaluator-launcher export <id> [<id>...] --dest <destination>"
|
|
96
|
+
)
|
|
97
|
+
return
|
|
98
|
+
|
|
80
99
|
config: dict[str, Any] = {
|
|
81
100
|
"copy_logs": self.copy_logs,
|
|
82
|
-
"only_required": self.only_required,
|
|
83
101
|
}
|
|
84
102
|
|
|
85
103
|
# Output handling
|
|
@@ -94,20 +112,90 @@ class ExportCmd:
|
|
|
94
112
|
if self.log_metrics:
|
|
95
113
|
config["log_metrics"] = self.log_metrics
|
|
96
114
|
|
|
115
|
+
# Add only_required if explicitly passed via CLI
|
|
116
|
+
if self.only_required is not None:
|
|
117
|
+
config["only_required"] = self.only_required
|
|
118
|
+
|
|
119
|
+
# Parse and validate overrides
|
|
120
|
+
if self.override:
|
|
121
|
+
# Flatten possible list-of-lists from parser
|
|
122
|
+
flat_overrides: list[str] = []
|
|
123
|
+
for item in self.override:
|
|
124
|
+
if isinstance(item, list):
|
|
125
|
+
flat_overrides.extend(str(x) for x in item)
|
|
126
|
+
else:
|
|
127
|
+
flat_overrides.append(str(item))
|
|
128
|
+
|
|
129
|
+
try:
|
|
130
|
+
self._validate_overrides(flat_overrides, self.dest)
|
|
131
|
+
except ValueError as e:
|
|
132
|
+
print(f"Error: {e}")
|
|
133
|
+
return
|
|
134
|
+
|
|
135
|
+
# Expand env vars in override vals ($VAR / ${VAR})
|
|
136
|
+
import os
|
|
137
|
+
|
|
138
|
+
from omegaconf import OmegaConf
|
|
139
|
+
|
|
140
|
+
expanded_overrides: list[str] = []
|
|
141
|
+
for ov in flat_overrides:
|
|
142
|
+
if "=" in ov:
|
|
143
|
+
k, v = ov.split("=", 1)
|
|
144
|
+
expanded_overrides.append(f"{k}={os.path.expandvars(v)}")
|
|
145
|
+
else:
|
|
146
|
+
expanded_overrides.append(os.path.expandvars(ov))
|
|
147
|
+
|
|
148
|
+
dot_cfg = OmegaConf.from_dotlist(expanded_overrides)
|
|
149
|
+
as_dict = OmegaConf.to_container(dot_cfg, resolve=True) or {}
|
|
150
|
+
if isinstance(as_dict, dict) and "export" in as_dict:
|
|
151
|
+
export_map = as_dict.get("export") or {}
|
|
152
|
+
if isinstance(export_map, dict) and self.dest in export_map:
|
|
153
|
+
config.update(export_map[self.dest] or {})
|
|
154
|
+
else:
|
|
155
|
+
config.update(as_dict)
|
|
156
|
+
else:
|
|
157
|
+
config.update(as_dict)
|
|
158
|
+
|
|
97
159
|
if self.format and self.dest != "local":
|
|
98
160
|
print(
|
|
99
161
|
"Note: --format is only used by --dest local. It will be ignored for other destinations."
|
|
100
162
|
)
|
|
101
163
|
|
|
102
|
-
|
|
164
|
+
if "only_required" in config and self.only_required is True:
|
|
165
|
+
config.pop("only_required", None)
|
|
166
|
+
|
|
103
167
|
print(
|
|
104
168
|
f"Exporting {len(self.invocation_ids)} {'invocations' if len(self.invocation_ids) > 1 else 'invocation'} to {self.dest}..."
|
|
105
169
|
)
|
|
106
170
|
|
|
107
171
|
result = export_results(self.invocation_ids, self.dest, config)
|
|
108
172
|
|
|
109
|
-
if not result
|
|
110
|
-
|
|
173
|
+
if not result.get("success", False):
|
|
174
|
+
err = result.get("error", "Unknown error")
|
|
175
|
+
print(f"\nExport failed: {err}")
|
|
176
|
+
# Provide actionable guidance for common configuration issues
|
|
177
|
+
if self.dest == "mlflow":
|
|
178
|
+
if "tracking_uri" in str(err).lower():
|
|
179
|
+
print("\nMLflow requires 'tracking_uri' to be configured.")
|
|
180
|
+
print(
|
|
181
|
+
"Set it via: -o export.mlflow.tracking_uri=http://mlflow-server:5000"
|
|
182
|
+
)
|
|
183
|
+
elif "not installed" in str(err).lower():
|
|
184
|
+
print("\nMLflow package not installed.")
|
|
185
|
+
print("Install via: pip install nemo-evaluator-launcher[mlflow]")
|
|
186
|
+
elif self.dest == "wandb":
|
|
187
|
+
if "entity" in str(err).lower() or "project" in str(err).lower():
|
|
188
|
+
print("\nW&B requires 'entity' and 'project' to be configured.")
|
|
189
|
+
print(
|
|
190
|
+
"Set via: -o export.wandb.entity=my-org -o export.wandb.project=my-proj"
|
|
191
|
+
)
|
|
192
|
+
elif "not installed" in str(err).lower():
|
|
193
|
+
print("\nW&B package not installed.")
|
|
194
|
+
print("Install via: pip install nemo-evaluator-launcher[wandb]")
|
|
195
|
+
elif self.dest == "gsheets":
|
|
196
|
+
if "not installed" in str(err).lower():
|
|
197
|
+
print("\nGoogle Sheets package not installed.")
|
|
198
|
+
print("Install via: pip install nemo-evaluator-launcher[gsheets]")
|
|
111
199
|
return
|
|
112
200
|
|
|
113
201
|
# Success path
|
|
@@ -124,6 +212,9 @@ class ExportCmd:
|
|
|
124
212
|
print(f" URL: {metadata['run_url']}")
|
|
125
213
|
if metadata.get("summary_path"):
|
|
126
214
|
print(f" Summary: {metadata['summary_path']}")
|
|
215
|
+
path_hint = job_result.get("dest") or metadata.get("output_dir")
|
|
216
|
+
if self.dest == "local" and path_hint:
|
|
217
|
+
print(f" Path: {path_hint}")
|
|
127
218
|
else:
|
|
128
219
|
print(f" {job_id} failed: {job_result.get('message', '')}")
|
|
129
220
|
else:
|
|
@@ -136,7 +227,6 @@ class ExportCmd:
|
|
|
136
227
|
# Show summary path if available
|
|
137
228
|
if metadata.get("summary_path"):
|
|
138
229
|
print(f"Summary: {metadata['summary_path']}")
|
|
139
|
-
|
|
140
230
|
# Show per-invocation status
|
|
141
231
|
for invocation_id, inv_result in result["invocations"].items():
|
|
142
232
|
if inv_result.get("success"):
|
|
@@ -146,3 +236,32 @@ class ExportCmd:
|
|
|
146
236
|
print(
|
|
147
237
|
f" {invocation_id}: failed, {inv_result.get('error', 'Unknown error')}"
|
|
148
238
|
)
|
|
239
|
+
|
|
240
|
+
def _validate_overrides(self, overrides: List[str], dest: str) -> None:
|
|
241
|
+
"""Validate override list for destination consistency.
|
|
242
|
+
|
|
243
|
+
Raises:
|
|
244
|
+
ValueError: If overrides specify wrong destination or have other issues.
|
|
245
|
+
"""
|
|
246
|
+
if not overrides:
|
|
247
|
+
return # nothing to validate
|
|
248
|
+
|
|
249
|
+
# Check each override for destination mismatch
|
|
250
|
+
for override_str in overrides:
|
|
251
|
+
if override_str.startswith(
|
|
252
|
+
"export."
|
|
253
|
+
): # check if override starts with export.
|
|
254
|
+
# Extract destination from override path
|
|
255
|
+
try:
|
|
256
|
+
key_part = override_str.split("=")[0] # Get left side before =
|
|
257
|
+
parts = key_part.split(".")
|
|
258
|
+
if len(parts) >= 2:
|
|
259
|
+
override_dest = parts[1]
|
|
260
|
+
if override_dest != dest:
|
|
261
|
+
raise ValueError(
|
|
262
|
+
f"Override destination mismatch: override specifies 'export.{override_dest}' but --dest is '{dest}'. "
|
|
263
|
+
f"Either change --dest to '{override_dest}' or use 'export.{dest}' in overrides."
|
|
264
|
+
)
|
|
265
|
+
except (IndexError, AttributeError):
|
|
266
|
+
# miconstructed override -> OmegaConf handles this
|
|
267
|
+
pass
|