Skip to content

Commit b485649

Browse files
committed
fix(perf): keep recipe imports off login nodes
Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com>
1 parent 40a49d8 commit b485649

5 files changed

Lines changed: 224 additions & 47 deletions

File tree

scripts/performance/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,7 @@ Mounting cached files is not enough by itself. If `HF_HUB_OFFLINE` remains `0`,
260260

261261
##### Logging arguments
262262

263+
- `--experiment-name`: NeMo-Run experiment name used for scheduler bookkeeping. This is independent of W&B naming.
263264
- `-l/--log_dir`: Directory for logging experiment results. Defaults to `NEMORUN_HOME`.
264265
- Make sure the environment variable `NEMORUN_HOME=<log_dir>` is accessible and set correctly in your virtual environment.
265266
- You can run `export NEMORUN_HOME=<log_dir>` in your terminal. You can add it your bashrc file (or equivalent for your OS/Linux distro) for setting it permanently.

scripts/performance/argument_parser.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -874,6 +874,14 @@ def parse_cli_args():
874874

875875
# Logging
876876
logging_args = parser.add_argument_group("Logging arguments")
877+
logging_args.add_argument(
878+
"--experiment-name",
879+
"--experiment_name",
880+
dest="experiment_name",
881+
type=str,
882+
help="NeMo-Run experiment name. Independent of the Weights & Biases run name.",
883+
required=False,
884+
)
877885
logging_args.add_argument(
878886
"-wdk",
879887
"--wandb_key",

scripts/performance/setup_experiment.py

Lines changed: 43 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@
2323
import tempfile
2424
import time
2525
from pathlib import Path
26-
from types import SimpleNamespace
2726
from typing import Any, Dict, List, Optional
2827

2928
import nemo_run as run
@@ -38,7 +37,7 @@
3837
kubeflow_executor,
3938
slurm_executor,
4039
)
41-
from utils.utils import configure_slurm_gpu_tuning, get_exp_name_config, select_config_variant_interactive
40+
from utils.utils import configure_slurm_gpu_tuning, select_config_variant_interactive
4241
except (ImportError, ModuleNotFoundError):
4342
from .argument_parser import NUM_GPUS_PER_NODE_MAP, parse_cli_args
4443
from .utils.executors import (
@@ -47,7 +46,7 @@
4746
kubeflow_executor,
4847
slurm_executor,
4948
)
50-
from .utils.utils import configure_slurm_gpu_tuning, get_exp_name_config, select_config_variant_interactive
49+
from .utils.utils import configure_slurm_gpu_tuning, select_config_variant_interactive
5150

5251
try:
5352
import wandb
@@ -79,6 +78,7 @@ def _filter_run_script_args(argv: List[str]) -> List[str]:
7978
* ``--additional_slurm_params`` — Slurm orchestration only.
8079
* ``--enable_vboost`` / ``--lock_gpu_freq`` — applied directly to the
8180
Slurm executor before submission.
81+
* ``--experiment-name`` — selects the NeMo-Run experiment name.
8282
* ``--csp`` — launcher-only; selects the CSP fabric plugin. The rank-local
8383
script forwards unrecognized args to Hydra, which rejects ``--csp``.
8484
* ``--kubeflow_*`` — consumed here to build the Kubeflow TrainJob. Several
@@ -97,6 +97,8 @@ def _is_launcher_only(flag: str) -> bool:
9797
"--additional_slurm_params",
9898
"--csp",
9999
"--enable_vboost",
100+
"--experiment-name",
101+
"--experiment_name",
100102
"--lock_gpu_freq",
101103
) or flag.startswith("--kubeflow_")
102104

@@ -115,6 +117,26 @@ def _is_launcher_only(flag: str) -> bool:
115117
return filtered_args
116118

117119

120+
def _default_experiment_name(
121+
*,
122+
use_recipes: bool,
123+
model_recipe_name: str,
124+
task: str,
125+
compute_dtype: str,
126+
num_gpus: int,
127+
gpu: str,
128+
config_variant: str | None,
129+
) -> str:
130+
"""Build a stable experiment name without importing a performance recipe."""
131+
if use_recipes:
132+
return f"{model_recipe_name}_{task}_{num_gpus}gpu_{gpu}"
133+
134+
fields = [task, model_recipe_name, compute_dtype, f"gpus{num_gpus}", gpu]
135+
if config_variant and config_variant.lower() not in {"v1", "v2"}:
136+
fields.append(config_variant.lower())
137+
return "_".join(fields)
138+
139+
118140
def _build_nemorun_script(
119141
*,
120142
script_path: str,
@@ -456,18 +478,11 @@ def main(
456478
export_nsys_sqlite: bool,
457479
pytorch_profiler: bool,
458480
moe_a2a_overlap: bool,
459-
tp_size: Optional[int],
460-
pp_size: Optional[int],
461-
cp_size: Optional[int],
462-
vp_size: Optional[int],
463-
ep_size: Optional[int],
464-
etp_size: Optional[int],
465-
micro_batch_size: Optional[int],
466-
global_batch_size: Optional[int],
467481
wandb_key: str,
468482
wandb_project_name: str,
469483
wandb_experiment_name: str,
470484
wandb_entity_name: str,
485+
experiment_name: Optional[str],
471486
profiling_start_step: int,
472487
profiling_stop_step: int,
473488
record_memory_history: bool,
@@ -564,35 +579,23 @@ def main(
564579
logger.warning("--export_nsys_sqlite was set without --enable_nsys; no Nsys SQLite export will be generated.")
565580

566581
script_name = ENTRYPOINT_BOOTSTRAP
567-
if use_recipes:
568-
exp_name = (
569-
wandb_experiment_name
570-
if wandb_experiment_name is not None
571-
else f"{model_recipe_name}_{task}_{num_gpus}gpu_{gpu}"
582+
# Keep the historical W&B-name fallback for callers that relied on it, but
583+
# let scheduling use a neutral name. The lightweight default deliberately
584+
# avoids resolving a recipe: effective parallelism, batches, and process
585+
# environment are finalized by bootstrap.py inside the submitted container.
586+
exp_name = (
587+
experiment_name
588+
or wandb_experiment_name
589+
or _default_experiment_name(
590+
use_recipes=use_recipes,
591+
model_recipe_name=model_recipe_name,
592+
task=task,
593+
compute_dtype=compute_dtype,
594+
num_gpus=num_gpus,
595+
gpu=gpu,
596+
config_variant=config_variant,
572597
)
573-
574-
else:
575-
if wandb_experiment_name is not None:
576-
# CI supplies the complete experiment name. Avoid resolving a perf recipe on the
577-
# login node in this path: recipe imports belong in the training container.
578-
exp_name = wandb_experiment_name
579-
else:
580-
# Create a simple namespace with the args needed by get_exp_name_config
581-
args_for_config = SimpleNamespace(
582-
num_gpus=num_gpus,
583-
tensor_model_parallel_size=tp_size,
584-
pipeline_model_parallel_size=pp_size,
585-
context_parallel_size=cp_size,
586-
virtual_pipeline_model_parallel_size=vp_size,
587-
expert_model_parallel_size=ep_size,
588-
expert_tensor_parallel_size=etp_size,
589-
micro_batch_size=micro_batch_size,
590-
global_batch_size=global_batch_size,
591-
)
592-
exp_config = get_exp_name_config(
593-
args_for_config, model_family_name, model_recipe_name, gpu, compute_dtype, task, config_variant
594-
)
595-
exp_name = f"{task}_{model_recipe_name}_{compute_dtype}_{exp_config}"
598+
)
596599

597600
if pretrained_checkpoint is not None:
598601
custom_mounts.append(f"{pretrained_checkpoint}:{pretrained_checkpoint}")
@@ -1002,18 +1005,11 @@ def main(
10021005
export_nsys_sqlite=args.export_nsys_sqlite,
10031006
pytorch_profiler=args.pytorch_profiler,
10041007
moe_a2a_overlap=args.moe_a2a_overlap,
1005-
tp_size=args.tensor_model_parallel_size,
1006-
pp_size=args.pipeline_model_parallel_size,
1007-
cp_size=args.context_parallel_size,
1008-
vp_size=args.virtual_pipeline_model_parallel_size,
1009-
ep_size=args.expert_model_parallel_size,
1010-
etp_size=args.expert_tensor_parallel_size,
1011-
micro_batch_size=args.micro_batch_size,
1012-
global_batch_size=args.global_batch_size,
10131008
wandb_key=args.wandb_key,
10141009
wandb_project_name=args.wandb_project_name,
10151010
wandb_experiment_name=args.wandb_experiment_name,
10161011
wandb_entity_name=args.wandb_entity_name,
1012+
experiment_name=args.experiment_name,
10171013
profiling_start_step=args.profiling_start_step,
10181014
profiling_stop_step=args.profiling_stop_step,
10191015
record_memory_history=args.record_memory_history,

scripts/training/recipe_metadata.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
"llama3_70b_peft_8gpu_h100_bf16_config",
5757
"llama3_70b_sft_32gpu_h100_bf16_config",
5858
"qwen3_235b_a22b_pretrain_256gpu_h100_bf16_config",
59+
"qwen3_30b_a3b_pretrain_16gpu_h100_bf16_config",
5960
}
6061
)
6162

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
# Copyright (c) 2026, NVIDIA CORPORATION. All rights reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Tests for lightweight performance submission from a Slurm login node."""
16+
17+
from __future__ import annotations
18+
19+
import os
20+
import subprocess
21+
import sys
22+
from pathlib import Path
23+
24+
import pytest
25+
26+
27+
pytestmark = pytest.mark.unit
28+
29+
_REPO_ROOT = Path(__file__).resolve().parents[4]
30+
_PERF_SCRIPTS_DIR = _REPO_ROOT / "scripts" / "performance"
31+
if str(_PERF_SCRIPTS_DIR) not in sys.path:
32+
sys.path.insert(0, str(_PERF_SCRIPTS_DIR))
33+
34+
import setup_experiment
35+
from argument_parser import parse_cli_args
36+
37+
38+
def test_default_experiment_name_uses_only_cli_metadata() -> None:
39+
assert (
40+
setup_experiment._default_experiment_name(
41+
use_recipes=False,
42+
model_recipe_name="deepseek_v3",
43+
task="pretrain",
44+
compute_dtype="fp8_mx",
45+
num_gpus=256,
46+
gpu="vr200",
47+
config_variant="large_scale",
48+
)
49+
== "pretrain_deepseek_v3_fp8_mx_gpus256_vr200_large_scale"
50+
)
51+
assert (
52+
setup_experiment._default_experiment_name(
53+
use_recipes=True,
54+
model_recipe_name="llama3_8b",
55+
task="pretrain",
56+
compute_dtype="bf16",
57+
num_gpus=8,
58+
gpu="h100",
59+
config_variant=None,
60+
)
61+
== "llama3_8b_pretrain_8gpu_h100"
62+
)
63+
64+
65+
def test_experiment_name_is_launcher_only_and_recipe_arguments_are_unchanged() -> None:
66+
recipe_args = [
67+
"--tensor_model_parallel_size",
68+
"2",
69+
"--global_batch_size",
70+
"64",
71+
"++model.num_layers=2",
72+
"++env_vars.TEST_SENTINEL=1",
73+
]
74+
argv = [
75+
"--experiment-name",
76+
"scheduler-name",
77+
"--wandb_experiment_name",
78+
"wandb-name",
79+
*recipe_args,
80+
]
81+
82+
assert setup_experiment._filter_run_script_args(argv) == [
83+
"--wandb_experiment_name",
84+
"wandb-name",
85+
*recipe_args,
86+
]
87+
88+
89+
@pytest.mark.parametrize("flag", ["--experiment-name", "--experiment_name"])
90+
def test_experiment_name_cli_aliases(flag: str) -> None:
91+
parser = parse_cli_args()
92+
args, unknown = parser.parse_known_args(
93+
[
94+
"--model_family_name",
95+
"llama",
96+
"--model_recipe_name",
97+
"llama3_8b",
98+
"--num_gpus",
99+
"8",
100+
"--gpu",
101+
"h100",
102+
flag,
103+
"scheduler-name",
104+
]
105+
)
106+
107+
assert unknown == []
108+
assert args.experiment_name == "scheduler-name"
109+
110+
111+
def test_submission_dry_run_does_not_import_bridge_or_mcore(tmp_path: Path) -> None:
112+
blocker_dir = tmp_path / "login_node"
113+
blocker_dir.mkdir()
114+
(blocker_dir / "sitecustomize.py").write_text(
115+
"""
116+
import importlib.abc
117+
import sys
118+
119+
class _RejectMegatron(importlib.abc.MetaPathFinder):
120+
def find_spec(self, fullname, path=None, target=None):
121+
if fullname == "megatron" or fullname.startswith("megatron."):
122+
raise RuntimeError(f"login-node import forbidden: {fullname}")
123+
return None
124+
125+
sys.meta_path.insert(0, _RejectMegatron())
126+
"""
127+
)
128+
environment = os.environ.copy()
129+
environment["NEMORUN_HOME"] = str(tmp_path / "nemorun")
130+
environment["PYTHONPATH"] = os.pathsep.join(
131+
value for value in (str(blocker_dir), environment.get("PYTHONPATH")) if value
132+
)
133+
command = [
134+
sys.executable,
135+
str(_PERF_SCRIPTS_DIR / "setup_experiment.py"),
136+
"--model_family_name",
137+
"llama",
138+
"--model_recipe_name",
139+
"llama3_8b",
140+
"--num_gpus",
141+
"8",
142+
"--gpus_per_node",
143+
"8",
144+
"--gpu",
145+
"h100",
146+
"--account",
147+
"test-account",
148+
"--partition",
149+
"batch",
150+
"--container_image",
151+
"example.invalid/nemo:test",
152+
"--packager",
153+
"none",
154+
"--dryrun",
155+
]
156+
157+
result = subprocess.run(
158+
command,
159+
cwd=_REPO_ROOT,
160+
env=environment,
161+
capture_output=True,
162+
text=True,
163+
check=False,
164+
timeout=30,
165+
)
166+
167+
output = result.stdout + result.stderr
168+
assert result.returncode == 0, output
169+
assert "bootstrap.py" in output
170+
assert "pretrain_llama3_8b_bf16_gpus8_h100" in output
171+
assert "login-node import forbidden" not in output

0 commit comments

Comments
 (0)