Skip to content

Commit 96f4bd8

Browse files
committed
Add schedule_job to ray cluster
Signed-off-by: Hemil Desai <hemild@nvidia.com>
1 parent aeb4902 commit 96f4bd8

3 files changed

Lines changed: 132 additions & 17 deletions

File tree

nemo_run/run/ray/cluster.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,14 @@ def start(self, wait_until_ready: bool = True, timeout: int = 1000, dryrun: bool
5555
name=self.name, executor=self.executor, timeout=timeout
5656
)
5757

58+
def schedule_job(
59+
self, name: str, executor: Executor, command: str, workdir: str, dryrun: bool = False
60+
):
61+
assert isinstance(self.executor, self.backend.EXECUTOR_CLS)
62+
self.backend.schedule_ray_job(
63+
name=name, executor=executor, command=command, workdir=workdir, dryrun=dryrun
64+
)
65+
5866
def port_forward(self, port: int = 8265, target_port: int = 8265, wait: bool = False):
5967
assert isinstance(self.executor, self.backend.EXECUTOR_CLS)
6068
if self._port_forward_map.get(port) is not None:

nemo_run/run/ray/kuberay.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,15 @@ def create_ray_cluster(
209209
logger.error(f"Error creating Ray cluster '{name}' in namespace '{namespace}': {e}")
210210
return None
211211

212+
def schedule_ray_job(
213+
self,
214+
name: str,
215+
executor: KubeRayExecutor,
216+
command: str,
217+
workdir: str,
218+
):
219+
raise NotImplementedError("KubeRay does not support scheduling jobs")
220+
212221
def delete_ray_cluster(
213222
self,
214223
name: str,

nemo_run/run/ray/slurm.py

Lines changed: 115 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,14 @@
2525
import time
2626
import warnings
2727
from dataclasses import asdict, dataclass
28+
from pathlib import Path
2829
from typing import Any, Dict, Optional, TypeAlias, Union
2930

3031
from nemo_run.core.execution.slurm import SlurmExecutor, _as_sbatch_flag
3132
from nemo_run.core.execution.utils import fill_template
33+
from nemo_run.core.packaging.git import GitArchivePackager
34+
from nemo_run.core.tunnel.client import SSHTunnel
35+
from nemo_run.core.tunnel.rsync import rsync
3236

3337
noquote: TypeAlias = str
3438

@@ -42,6 +46,8 @@ class SlurmRayRequest:
4246
template_path: str
4347
executor: SlurmExecutor
4448
pre_ray_start_commands: Optional[list[str]] = None
49+
command: Optional[str] = None
50+
workdir: Optional[str] = None
4551

4652
@staticmethod
4753
def get_job_name(executor: SlurmExecutor, name: str) -> str:
@@ -135,6 +141,8 @@ def get_srun_flags(mounts: list[str], container_image: Optional[str]) -> str:
135141
"common_srun_args": get_srun_flags(
136142
self.executor.container_mounts, self.executor.container_image
137143
),
144+
"command": self.command,
145+
"command_workdir": self.workdir,
138146
}
139147

140148
if self.pre_ray_start_commands:
@@ -239,41 +247,53 @@ def create_ray_cluster(
239247
executor: SlurmExecutor,
240248
pre_ray_start_commands: Optional[list[str]] = None,
241249
dryrun: bool = False,
250+
command: Optional[str] = None,
251+
workdir: Optional[str] = None,
242252
) -> Any:
253+
cluster_dir = os.path.join(executor.tunnel.job_dir, name)
254+
ray_sbatch = SlurmRayRequest(
255+
name=name,
256+
cluster_dir=cluster_dir,
257+
template_path=os.path.join(
258+
os.path.dirname(os.path.abspath(__file__)), "templates", "ray.sub.j2"
259+
),
260+
executor=executor,
261+
pre_ray_start_commands=pre_ray_start_commands,
262+
command=command,
263+
workdir=workdir,
264+
).materialize()
265+
266+
if dryrun:
267+
logger.info(f"Dry run: Ray cluster '{name}'")
268+
print(ray_sbatch)
269+
return
270+
243271
logger.info(f"Creating Ray cluster '{name}'")
244272
# Check if a cluster with this name already exists
245273
status = self.get_ray_cluster_status(name, executor)
246274

247275
if status["job_id"] is not None:
248276
job_state = status["state"]
249-
if job_state in ["PENDING", "RUNNING", "CONFIGURING", "COMPLETING"]:
277+
if job_state in ["PENDING", "RUNNING", "CONFIGURING"]:
250278
logger.info(
251279
f"Ray cluster '{name}' already exists with job ID {status['job_id']} "
252280
f"and is currently in {job_state} state. "
253281
f"Skipping creation."
254282
)
255283
return None
256-
elif job_state not in ["COMPLETED", "CANCELLED", "FAILED", "TIMEOUT", "NOT_FOUND"]:
284+
elif job_state not in [
285+
"COMPLETING",
286+
"COMPLETED",
287+
"CANCELLED",
288+
"FAILED",
289+
"TIMEOUT",
290+
"NOT_FOUND",
291+
]:
257292
logger.warning(
258293
f"Ray cluster '{name}' exists with job ID {status['job_id']} "
259294
f"in state {job_state}. Creating new cluster anyway."
260295
)
261296

262-
cluster_dir = os.path.join(executor.tunnel.job_dir, name)
263-
ray_sbatch = SlurmRayRequest(
264-
name=name,
265-
cluster_dir=cluster_dir,
266-
template_path=os.path.join(
267-
os.path.dirname(os.path.abspath(__file__)), "templates", "ray.sub.j2"
268-
),
269-
executor=executor,
270-
pre_ray_start_commands=pre_ray_start_commands,
271-
).materialize()
272-
273-
if dryrun:
274-
print(ray_sbatch)
275-
return
276-
277297
executor.tunnel.connect()
278298
executor.tunnel.run(f"mkdir -p {cluster_dir}")
279299

@@ -294,6 +314,84 @@ def create_ray_cluster(
294314

295315
return job_id
296316

317+
def schedule_ray_job(
318+
self,
319+
name: str,
320+
executor: SlurmExecutor,
321+
command: str,
322+
workdir: Optional[str] = None,
323+
pre_ray_start_commands: Optional[list[str]] = None,
324+
dryrun: bool = False,
325+
):
326+
remote_workdir = None
327+
if workdir:
328+
if isinstance(executor.tunnel, SSHTunnel):
329+
# Rsync workdir honoring .gitignore
330+
remote_workdir = os.path.join(executor.tunnel.job_dir, name, "code")
331+
if not dryrun:
332+
executor.tunnel.connect()
333+
assert executor.tunnel.session is not None, "Tunnel session is not connected"
334+
rsync(
335+
executor.tunnel.session,
336+
workdir,
337+
remote_workdir,
338+
rsync_opts="--filter=':- .gitignore'",
339+
)
340+
else:
341+
remote_workdir = workdir
342+
elif executor.packager:
343+
if not dryrun:
344+
if isinstance(executor.tunnel, SSHTunnel):
345+
package_dir_ref = tempfile.TemporaryDirectory()
346+
package_dir = package_dir_ref.name
347+
else:
348+
package_dir_ref = None
349+
package_dir = os.path.join(executor.tunnel.job_dir, name)
350+
351+
if isinstance(executor.packager, GitArchivePackager):
352+
output = subprocess.run(
353+
["git", "rev-parse", "--show-toplevel"],
354+
check=True,
355+
stdout=subprocess.PIPE,
356+
)
357+
path = output.stdout.splitlines()[0].decode()
358+
base_path = Path(path).absolute()
359+
else:
360+
base_path = Path(os.getcwd()).absolute()
361+
362+
local_tar_file = executor.packager.package(base_path, package_dir, name)
363+
local_code_extraction_path = os.path.join(package_dir, "code")
364+
os.makedirs(local_code_extraction_path, exist_ok=True)
365+
subprocess.run(
366+
f"tar -xvzf {local_tar_file} -C {local_code_extraction_path} --ignore-zeros",
367+
shell=True,
368+
check=True,
369+
)
370+
371+
if isinstance(executor.tunnel, SSHTunnel):
372+
remote_workdir = os.path.join(executor.tunnel.job_dir, name, "code")
373+
executor.tunnel.connect()
374+
assert executor.tunnel.session is not None, "Tunnel session is not connected"
375+
rsync(
376+
executor.tunnel.session,
377+
os.path.join(local_code_extraction_path, ""),
378+
remote_workdir,
379+
rsync_opts="--filter=':- .gitignore'",
380+
)
381+
else:
382+
remote_workdir = local_code_extraction_path
383+
384+
assert remote_workdir is not None, "workdir is not set"
385+
job_id = self.create_ray_cluster(
386+
name,
387+
executor,
388+
pre_ray_start_commands=pre_ray_start_commands,
389+
dryrun=dryrun,
390+
command=command,
391+
workdir=remote_workdir,
392+
)
393+
return job_id
394+
297395
def wait_until_ray_cluster_running(
298396
self,
299397
name: str,

0 commit comments

Comments
 (0)