|
47 | 47 | get_packager, |
48 | 48 | get_registered_external_repo, |
49 | 49 | ) |
| 50 | +from nemo_skills.pipeline.utils.ray_executor import ( |
| 51 | + RayExecutor, |
| 52 | + RayJobConfig, |
| 53 | + get_ray_client, |
| 54 | +) |
50 | 55 | from nemo_skills.pipeline.utils.server import get_free_port, get_server_command |
51 | 56 | from nemo_skills.utils import get_logger_name, remove_handlers |
52 | 57 |
|
@@ -270,6 +275,35 @@ def get_executor( |
270 | 275 | additional_kwargs={"entrypoint": ""}, |
271 | 276 | ) |
272 | 277 |
|
| 278 | + if cluster_config["executor"] == "ray": |
| 279 | + # Ray top-level scheduler executor (standalone Ray clusters or Ray-on-Slurm). |
| 280 | + # Distinct from the existing `with_ray=True` flag, which is "Ray inside a Slurm |
| 281 | + # allocation" via heterogeneous Slurm jobs. Here, Ray IS the scheduler. |
| 282 | + # Actual job submission is performed in `add_task()` via `RayJobClient`; this |
| 283 | + # branch returns a `RayExecutor` config object for callers that introspect the |
| 284 | + # executor type (e.g., `Pipeline._create_executor`). |
| 285 | + ray_config = cluster_config.get("ray", {}) |
| 286 | + # gpus_per_node convention (per get_executor docstring): 0 or None for |
| 287 | + # CPU-only jobs. Treat None as "caller didn't say" → default 1, but |
| 288 | + # respect an explicit 0 so CPU-only Ray jobs don't silently get a GPU. |
| 289 | + if gpus_per_node is None: |
| 290 | + ray_num_gpus = 1 |
| 291 | + else: |
| 292 | + ray_num_gpus = int(gpus_per_node) * num_nodes |
| 293 | + return RayExecutor( |
| 294 | + ray_address=ray_config.get("address", "auto"), |
| 295 | + ray_namespace=ray_config.get("namespace", "nemo"), |
| 296 | + num_gpus=ray_num_gpus, |
| 297 | + # cluster_config.ray.default_num_cpus is per-node; multiply by |
| 298 | + # num_nodes to get the per-job total RayExecutor expects. |
| 299 | + num_cpus=ray_config.get("default_num_cpus", 8) * num_nodes, |
| 300 | + num_nodes=num_nodes, |
| 301 | + ntasks_per_node=tasks_per_node, |
| 302 | + log_dir=cluster_config.get("jobs", {}).get("log_dir", "/tmp/ray_jobs"), |
| 303 | + env_vars=env_vars, |
| 304 | + packager=packager, |
| 305 | + ) |
| 306 | + |
273 | 307 | if not heterogeneous: |
274 | 308 | env_vars["SLURM_MASTER_NODE"] = "$(scontrol show hostnames $SLURM_JOB_NODELIST | head -n1)" |
275 | 309 | else: |
@@ -533,6 +567,78 @@ def add_task( |
533 | 567 | if not is_mounted_filepath(cluster_config, env_vars["HF_HOME"]): |
534 | 568 | raise RuntimeError(f"Invalid cluster_config: HF_HOME={env_vars['HF_HOME']} is not a mounted path.") |
535 | 569 |
|
| 570 | + # Ray executor path: bypass nemo_run.Experiment.add() and submit directly via |
| 571 | + # RayJobClient. Scoped to single-command Ray jobs; unsupported modes raise |
| 572 | + # NotImplementedError to fail fast. Distinct from `with_ray=True`, which runs |
| 573 | + # Ray inside a Slurm allocation rather than as the top-level scheduler. |
| 574 | + if cluster_config["executor"] == "ray": |
| 575 | + if with_sandbox: |
| 576 | + raise NotImplementedError( |
| 577 | + "Ray executor does not support sandbox containers in this release. " |
| 578 | + "Sandbox judge containers are out of scope for the initial Ray support. " |
| 579 | + "Use cluster_config.executor='slurm' for sandbox workflows." |
| 580 | + ) |
| 581 | + if server_config is not None: |
| 582 | + raise NotImplementedError( |
| 583 | + "Ray executor does not support co-scheduled servers (vLLM/SGLang/TRT-LLM) " |
| 584 | + "alongside the main job in this release. For Ray-based workflows that " |
| 585 | + "require an inference endpoint, use a separately-deployed external endpoint " |
| 586 | + "and call it via server_type='openai' (OpenAI-compatible)." |
| 587 | + ) |
| 588 | + if heterogeneous: |
| 589 | + raise NotImplementedError("Ray executor does not support heterogeneous tasks in this release.") |
| 590 | + if isinstance(cmd, list) and len(cmd) > 1: |
| 591 | + raise NotImplementedError( |
| 592 | + f"Ray executor only supports single-command tasks in this release. Got {len(cmd)} commands." |
| 593 | + ) |
| 594 | + if with_ray: |
| 595 | + raise NotImplementedError( |
| 596 | + "with_ray=True (Ray-inside-Slurm-allocation, heterogeneous Slurm jobs) is " |
| 597 | + "not compatible with cluster_config.executor='ray' (Ray-as-top-level-scheduler). " |
| 598 | + "Use exactly one path per task." |
| 599 | + ) |
| 600 | + |
| 601 | + # Validate task_name length already happened above. Build the Ray submission. |
| 602 | + ray_cmd = cmd if isinstance(cmd, str) else cmd[0] |
| 603 | + ray_dependencies = [] |
| 604 | + for dep in task_dependencies or []: |
| 605 | + # task_dependencies for the Ray path must be Ray submission IDs (strings). |
| 606 | + # Slurm callers commonly pass nemo-run task handles; silently dropping those |
| 607 | + # would cause the new job to run without waiting for its dependency, defeating |
| 608 | + # the contract without any signal. Fail fast instead. |
| 609 | + if not isinstance(dep, str): |
| 610 | + raise NotImplementedError( |
| 611 | + f"Ray executor task_dependencies must be Ray submission IDs (str); " |
| 612 | + f"got {type(dep).__name__}. If you copied a slurm pattern, replace the " |
| 613 | + f"nemo-run task handle with the string returned by the prior " |
| 614 | + f"add_task() call under cluster_config.executor='ray'." |
| 615 | + ) |
| 616 | + ray_dependencies.append(dep) |
| 617 | + ray_cluster_config = cluster_config.get("ray", {}) |
| 618 | + # default_num_cpus is per-node; multiply by num_nodes to get the per-job total. |
| 619 | + ray_default_cpus_per_node = ray_cluster_config.get("default_num_cpus", 8) |
| 620 | + ray_log_dir = log_dir or cluster_config.get("jobs", {}).get("log_dir", "/tmp/ray_jobs") |
| 621 | + |
| 622 | + ray_job_config = RayJobConfig( |
| 623 | + name=task_name, |
| 624 | + command=ray_cmd, |
| 625 | + num_gpus=num_gpus if num_gpus is not None else 1, |
| 626 | + num_cpus=ray_default_cpus_per_node * num_nodes, |
| 627 | + num_nodes=num_nodes, |
| 628 | + env_vars=env_vars, |
| 629 | + log_dir=ray_log_dir, |
| 630 | + dependencies=ray_dependencies if ray_dependencies else None, |
| 631 | + ) |
| 632 | + |
| 633 | + if dry_run: |
| 634 | + LOG.info("Dry run mode: would submit Ray job %s with command: %s", task_name, ray_cmd) |
| 635 | + return f"<dry-run-ray:{task_name}>" |
| 636 | + |
| 637 | + ray_client = get_ray_client(cluster_config) |
| 638 | + submission_id = ray_client.submit_job(ray_job_config) |
| 639 | + LOG.info("Ray job submitted: task=%s submission_id=%s", task_name, submission_id) |
| 640 | + return submission_id |
| 641 | + |
536 | 642 | het_group = 0 |
537 | 643 | het_group_indices = [] |
538 | 644 | total_het_groups = (n_servers if server_config is not None else 0) + bool(cmd) + with_sandbox |
|
0 commit comments