|
14 | 14 | # limitations under the License. |
15 | 15 |
|
16 | 16 | from dataclasses import dataclass |
| 17 | +from unittest.mock import MagicMock |
17 | 18 |
|
18 | 19 | import pytest |
19 | 20 | from torchx import specs |
20 | 21 |
|
21 | | -from nemo_run.config import Partial, Script |
| 22 | +from nemo_run.config import RUNDIR_NAME, Partial, Script |
22 | 23 | from nemo_run.core.execution.base import Executor |
23 | 24 | from nemo_run.core.execution.launcher import FaultTolerance, Torchrun |
24 | 25 | from nemo_run.core.execution.local import LocalExecutor |
| 26 | +from nemo_run.core.execution.slurm import SlurmExecutor |
25 | 27 | from nemo_run.core.packaging.base import Packager |
26 | | -from nemo_run.run.torchx_backend.packaging import merge_executables, package |
| 28 | +from nemo_run.core.tunnel.client import LocalTunnel |
| 29 | +from nemo_run.run.torchx_backend.packaging import ( |
| 30 | + merge_executables, |
| 31 | + package, |
| 32 | +) |
27 | 33 |
|
28 | 34 |
|
29 | 35 | @dataclass(kw_only=True) |
@@ -301,3 +307,132 @@ def test_merge_executables(): |
301 | 307 | assert len(merged_app_def.roles) == 2 |
302 | 308 | assert merged_app_def.roles[0].name == "role1" |
303 | 309 | assert merged_app_def.roles[1].name == "role2" |
| 310 | + |
| 311 | + |
| 312 | +class TestPackagingNonContainerMode: |
| 313 | + """Tests for non-container mode path substitution in packaging.""" |
| 314 | + |
| 315 | + def test_package_script_inline_with_slurm_non_container_mode(self, tmp_path): |
| 316 | + """Test that inline scripts have /nemo_run paths substituted in non-container mode.""" |
| 317 | + # Create a SlurmExecutor without container |
| 318 | + executor = SlurmExecutor( |
| 319 | + account="test_account", |
| 320 | + partition="gpu", |
| 321 | + nodes=1, |
| 322 | + ntasks_per_node=8, |
| 323 | + container_image=None, # Non-container mode |
| 324 | + ) |
| 325 | + executor.job_dir = str(tmp_path / "test-job") |
| 326 | + executor.experiment_id = "exp-123" |
| 327 | + |
| 328 | + # Mock tunnel |
| 329 | + tunnel = MagicMock(spec=LocalTunnel) |
| 330 | + tunnel.job_dir = "/remote/experiments/exp-123" |
| 331 | + executor.tunnel = tunnel |
| 332 | + |
| 333 | + # Create an inline script with /nemo_run paths |
| 334 | + fn_or_script = Script( |
| 335 | + inline=f"cd /{RUNDIR_NAME}/code && python /{RUNDIR_NAME}/scripts/run.py" |
| 336 | + ) |
| 337 | + |
| 338 | + app_def = package( |
| 339 | + name="test", |
| 340 | + fn_or_script=fn_or_script, |
| 341 | + executor=executor, |
| 342 | + ) |
| 343 | + |
| 344 | + assert app_def.name == "test" |
| 345 | + assert len(app_def.roles) == 1 |
| 346 | + |
| 347 | + # Read the generated script file and verify paths were substituted |
| 348 | + script_file = tmp_path / "test-job" / "scripts" / "test.sh" |
| 349 | + assert script_file.exists() |
| 350 | + |
| 351 | + content = script_file.read_text() |
| 352 | + actual_job_dir = "/remote/experiments/exp-123/test-job" |
| 353 | + |
| 354 | + # Should NOT contain /nemo_run paths |
| 355 | + assert f"/{RUNDIR_NAME}" not in content |
| 356 | + # Should contain the actual job directory path |
| 357 | + assert f"{actual_job_dir}/code" in content |
| 358 | + assert f"{actual_job_dir}/scripts/run.py" in content |
| 359 | + |
| 360 | + def test_package_script_inline_with_slurm_container_mode(self, tmp_path): |
| 361 | + """Test that inline scripts preserve /nemo_run paths in container mode.""" |
| 362 | + # Create a SlurmExecutor with container |
| 363 | + executor = SlurmExecutor( |
| 364 | + account="test_account", |
| 365 | + partition="gpu", |
| 366 | + nodes=1, |
| 367 | + ntasks_per_node=8, |
| 368 | + container_image="nvcr.io/nvidia/pytorch:24.01-py3", |
| 369 | + ) |
| 370 | + executor.job_dir = str(tmp_path / "test-job") |
| 371 | + executor.experiment_id = "exp-123" |
| 372 | + |
| 373 | + # Mock tunnel |
| 374 | + tunnel = MagicMock(spec=LocalTunnel) |
| 375 | + tunnel.job_dir = "/remote/experiments/exp-123" |
| 376 | + executor.tunnel = tunnel |
| 377 | + |
| 378 | + # Create an inline script with /nemo_run paths |
| 379 | + fn_or_script = Script( |
| 380 | + inline=f"cd /{RUNDIR_NAME}/code && python /{RUNDIR_NAME}/scripts/run.py" |
| 381 | + ) |
| 382 | + |
| 383 | + app_def = package( |
| 384 | + name="test", |
| 385 | + fn_or_script=fn_or_script, |
| 386 | + executor=executor, |
| 387 | + ) |
| 388 | + |
| 389 | + assert app_def.name == "test" |
| 390 | + assert len(app_def.roles) == 1 |
| 391 | + |
| 392 | + # Read the generated script file and verify paths were NOT substituted |
| 393 | + script_file = tmp_path / "test-job" / "scripts" / "test.sh" |
| 394 | + assert script_file.exists() |
| 395 | + |
| 396 | + content = script_file.read_text() |
| 397 | + |
| 398 | + # Should contain /nemo_run paths (not substituted) |
| 399 | + assert f"/{RUNDIR_NAME}/code" in content |
| 400 | + assert f"/{RUNDIR_NAME}/scripts/run.py" in content |
| 401 | + |
| 402 | + def test_package_script_path_not_affected_by_non_container_mode(self, tmp_path): |
| 403 | + """Test that path-based scripts are not affected by non-container mode substitution.""" |
| 404 | + # Create a SlurmExecutor without container |
| 405 | + executor = SlurmExecutor( |
| 406 | + account="test_account", |
| 407 | + partition="gpu", |
| 408 | + nodes=1, |
| 409 | + ntasks_per_node=8, |
| 410 | + container_image=None, # Non-container mode |
| 411 | + ) |
| 412 | + executor.job_dir = str(tmp_path / "test-job") |
| 413 | + executor.experiment_id = "exp-123" |
| 414 | + |
| 415 | + # Mock tunnel |
| 416 | + tunnel = MagicMock(spec=LocalTunnel) |
| 417 | + tunnel.job_dir = "/remote/experiments/exp-123" |
| 418 | + executor.tunnel = tunnel |
| 419 | + |
| 420 | + # Create a path-based script (not inline) |
| 421 | + fn_or_script = Script( |
| 422 | + path="test.py", |
| 423 | + args=["--config", f"/{RUNDIR_NAME}/configs/config.yaml"], |
| 424 | + ) |
| 425 | + |
| 426 | + app_def = package( |
| 427 | + name="test", |
| 428 | + fn_or_script=fn_or_script, |
| 429 | + executor=executor, |
| 430 | + ) |
| 431 | + |
| 432 | + assert app_def.name == "test" |
| 433 | + assert len(app_def.roles) == 1 |
| 434 | + role = app_def.roles[0] |
| 435 | + |
| 436 | + # Path-based scripts don't write files, so args should remain unchanged |
| 437 | + # (the substitution only affects inline script file content) |
| 438 | + assert role.args == ["test.py", "--config", f"/{RUNDIR_NAME}/configs/config.yaml"] |
0 commit comments