2525import time
2626import warnings
2727from dataclasses import asdict , dataclass
28+ from pathlib import Path
2829from typing import Any , Dict , Optional , TypeAlias , Union
2930
3031from nemo_run .core .execution .slurm import SlurmExecutor , _as_sbatch_flag
3132from 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
3337noquote : 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