Skip to content

Commit cbb0dfd

Browse files
committed
feat(perf): add RunAIPlugin for NVIDIA Run:ai (RoCE/SR-IOV) Kubeflow fabric
Add a new CSP fabric plugin (`--csp runai`) for on-prem and colocation clusters managed by NVIDIA Run:ai. The plugin injects RoCE/GDR rail extended resources, Multus network-attachment annotations, a memory-backed /dev/shm, and an optional workspace PVC onto the KubeflowExecutor — following the same pattern as EKSEnvPlugin (EFA) and GKEEnvPlugin (gIB). Changes: - csp_plugins.py: new RunAIPlugin dataclass with setup() method - argument_parser.py: add "runai" to --csp choices; add --runai_* args (extended_resources_json, annotations_json, pvc_claim_name, pvc_mount_path, large_shm, env_json) - setup_experiment.py: import RunAIPlugin, wire --runai_* args to main() signature and CSP plugin selection, filter --runai_* from rank-local script args Validated on an 8-node B300 NVL72 cluster (56 GPUs, RoCE fabric) running Nemotron 3 30B BF16 pretraining at ~698 TFLOP/s/GPU. Signed-off-by: Rafael M. Koike <koike.rafael@gmail.com>
1 parent ab7f61f commit cbb0dfd

3 files changed

Lines changed: 139 additions & 9 deletions

File tree

scripts/performance/argument_parser.py

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -540,13 +540,57 @@ def parse_cli_args():
540540
kubeflow_args.add_argument(
541541
"--csp",
542542
type=str,
543-
choices=["aws", "gcp"],
543+
choices=["aws", "gcp", "runai"],
544544
default=None,
545-
help="Cloud provider of the Kubeflow cluster. Selects the CSP fabric plugin "
546-
"(aws -> EKSEnvPlugin/EFA, gcp -> GKEEnvPlugin/gIB). Omit on Slurm or when no "
547-
"CSP-specific fabric config is needed.",
545+
help="Cloud / platform provider of the Kubeflow cluster. Selects the CSP fabric plugin "
546+
"(aws -> EKSEnvPlugin/EFA, gcp -> GKEEnvPlugin/gIB, runai -> RunAIPlugin/RoCE). "
547+
"Omit on Slurm or when no CSP-specific fabric config is needed.",
548548
required=False,
549549
)
550+
kubeflow_args.add_argument(
551+
"--runai_extended_resources_json",
552+
type=str,
553+
help="JSON-encoded dict of Run:ai extended resource requests per pod "
554+
'(e.g. \'{"nvidia.com/r0-p0": "1", "nvidia.com/r1-p0": "1"}\').',
555+
required=False,
556+
default=None,
557+
)
558+
kubeflow_args.add_argument(
559+
"--runai_annotations_json",
560+
type=str,
561+
help="JSON-encoded dict of pod annotations for Run:ai networking "
562+
'(e.g. \'{"k8s.v1.cni.cncf.io/networks": "default/r0-p0,..."}\').',
563+
required=False,
564+
default=None,
565+
)
566+
kubeflow_args.add_argument(
567+
"--runai_pvc_claim_name",
568+
type=str,
569+
help="PVC claim name for the shared workspace on Run:ai clusters.",
570+
required=False,
571+
default=None,
572+
)
573+
kubeflow_args.add_argument(
574+
"--runai_pvc_mount_path",
575+
type=str,
576+
help="Container mount path for the Run:ai workspace PVC. Defaults to '/nemo-workspace'.",
577+
required=False,
578+
default="/nemo-workspace",
579+
)
580+
kubeflow_args.add_argument(
581+
"--runai_large_shm",
582+
type=bool_arg,
583+
help="Mount a memory-backed /dev/shm for NCCL shared-memory collectives. Defaults to true.",
584+
required=False,
585+
default=True,
586+
)
587+
kubeflow_args.add_argument(
588+
"--runai_env_json",
589+
type=str,
590+
help="JSON-encoded dict of additional environment variables for the Run:ai training container.",
591+
required=False,
592+
default=None,
593+
)
550594
kubeflow_args.add_argument(
551595
"--kubeflow_workdir_pvc",
552596
type=str,

scripts/performance/setup_experiment.py

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,9 @@
5454
from .perf_plugins import NsysPlugin, PerfEnvPlugin, PyTorchProfilerPlugin
5555

5656
try:
57-
from utils.csp_plugins import EKSEnvPlugin, GKEEnvPlugin
57+
from utils.csp_plugins import EKSEnvPlugin, GKEEnvPlugin, RunAIPlugin
5858
except (ImportError, ModuleNotFoundError):
59-
from .utils.csp_plugins import EKSEnvPlugin, GKEEnvPlugin
59+
from .utils.csp_plugins import EKSEnvPlugin, GKEEnvPlugin, RunAIPlugin
6060

6161

6262
SCRIPT_DIR = Path(__file__).parent.resolve()
@@ -88,7 +88,7 @@ def _filter_run_script_args(argv: List[str]) -> List[str]:
8888
"""
8989

9090
def _is_launcher_only(flag: str) -> bool:
91-
return flag in ("--additional_slurm_params", "--csp") or flag.startswith("--kubeflow_")
91+
return flag in ("--additional_slurm_params", "--csp") or flag.startswith(("--kubeflow_", "--runai_"))
9292

9393
filtered_args = []
9494
skip_next = False
@@ -459,6 +459,12 @@ def main(
459459
kubeflow_container_kwargs_json: Optional[str],
460460
kubeflow_labels_json: Optional[str],
461461
kubeflow_pod_annotations_json: Optional[str],
462+
runai_extended_resources_json: Optional[str] = None,
463+
runai_annotations_json: Optional[str] = None,
464+
runai_pvc_claim_name: Optional[str] = None,
465+
runai_pvc_mount_path: str = "/nemo-workspace",
466+
runai_large_shm: bool = True,
467+
runai_env_json: Optional[str] = None,
462468
deterministic: bool = False,
463469
config_variant: str = "v1",
464470
gres: Optional[str] = None,
@@ -643,12 +649,24 @@ def main(
643649
plugins = []
644650

645651
# CSP fabric plugins (Kubeflow only; inert on Slurm via their isinstance guard):
646-
# aws -> EKSEnvPlugin (EFA), gcp -> GKEEnvPlugin (gIB). Networking/fabric only;
652+
# aws -> EKSEnvPlugin (EFA), gcp -> GKEEnvPlugin (gIB),
653+
# runai -> RunAIPlugin (RoCE/SR-IOV). Networking/fabric only;
647654
# arch/recipe/perf env stays in PerfEnvPlugin / the recipe.
648655
if csp == "aws":
649656
plugins.append(EKSEnvPlugin())
650657
elif csp == "gcp":
651658
plugins.append(GKEEnvPlugin())
659+
elif csp == "runai":
660+
plugins.append(
661+
RunAIPlugin(
662+
extended_resources=json.loads(runai_extended_resources_json) if runai_extended_resources_json else {},
663+
annotations=json.loads(runai_annotations_json) if runai_annotations_json else {},
664+
large_shm=runai_large_shm,
665+
pvc_claim_name=runai_pvc_claim_name,
666+
pvc_mount_path=runai_pvc_mount_path,
667+
env_vars=json.loads(runai_env_json) if runai_env_json else {},
668+
)
669+
)
652670

653671
if not use_recipes:
654672
plugins.append(

scripts/performance/utils/csp_plugins.py

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727

2828
import json
2929
from dataclasses import dataclass, field
30-
from typing import List, Union
30+
from typing import Dict, List, Optional, Union
3131

3232
import nemo_run as run
3333
from nemo_run import Plugin
@@ -100,3 +100,71 @@ def setup(self, task: Union["run.Partial", "run.Script"], executor: "run.Executo
100100
"networking.gke.io/interfaces": json.dumps(interfaces, separators=(",", ":")),
101101
}
102102
executor.env_vars.setdefault("NCCL_NET", "gIB")
103+
104+
105+
@dataclass(kw_only=True)
106+
class RunAIPlugin(Plugin):
107+
"""NVIDIA Run:ai (RoCE / SR-IOV) fabric.
108+
109+
Attaches RoCE/GDR rails as Kubernetes extended resources and Multus
110+
network-attachment annotations, and optionally enlarges ``/dev/shm`` via an
111+
``emptyDir`` volume. This is the on-prem / colocation equivalent of the
112+
cloud CSP plugins: the networking topology is expressed through Multus
113+
NetworkAttachmentDefinitions and SR-IOV device-plugin resources instead of
114+
cloud-provider device plugins (EFA, gIB).
115+
116+
Typical B300 NVL72 topology exposes 8 RoCE rails (``r0-p0`` … ``r7-p0``)
117+
each with one SR-IOV VF. A single Multus annotation attaches all rails.
118+
119+
Attributes:
120+
extended_resources: Kubernetes extended-resource requests per pod,
121+
e.g. ``{"nvidia.com/r0-p0": "1", "nvidia.com/r1-p0": "1", …}``.
122+
Maps directly to ``extra_resource_requests`` / ``limits``.
123+
annotations: Pod annotations dict, typically a single
124+
``k8s.v1.cni.cncf.io/networks`` key listing comma-separated Multus
125+
NetworkAttachmentDefinitions.
126+
large_shm: Mount a memory-backed ``/dev/shm`` (``emptyDir.medium:
127+
Memory``) to avoid the default 64 MiB limit, which is too small for
128+
NCCL shared-memory collectives on multi-GPU nodes.
129+
pvc_claim_name: If set, attach the named PersistentVolumeClaim to the
130+
pod and mount it at ``pvc_mount_path``. Typically the shared
131+
workspace PVC that holds model assets, HuggingFace cache, and
132+
experiment logs.
133+
pvc_mount_path: Container mount path for the PVC.
134+
env_vars: Additional environment variables injected into the training
135+
container (e.g. ``TRANSFORMERS_OFFLINE``, ``HF_HOME``).
136+
"""
137+
138+
extended_resources: Dict[str, str] = field(default_factory=dict)
139+
annotations: Dict[str, str] = field(default_factory=dict)
140+
large_shm: bool = True
141+
pvc_claim_name: Optional[str] = None
142+
pvc_mount_path: str = "/nemo-workspace"
143+
env_vars: Dict[str, str] = field(default_factory=dict)
144+
145+
def setup(self, task: Union["run.Partial", "run.Script"], executor: "run.Executor") -> None:
146+
"""Layer the Run:ai RoCE/SR-IOV fabric onto a Kubeflow executor."""
147+
if not isinstance(executor, KubeflowExecutor):
148+
return
149+
150+
if self.extended_resources:
151+
executor.extra_resource_requests = {**executor.extra_resource_requests, **self.extended_resources}
152+
executor.extra_resource_limits = {**executor.extra_resource_limits, **self.extended_resources}
153+
154+
if self.annotations:
155+
executor.pod_annotations = {**executor.pod_annotations, **self.annotations}
156+
157+
if self.large_shm and not any(v.get("name") == "dshm" for v in executor.volumes):
158+
executor.volumes.append({"name": "dshm", "emptyDir": {"medium": "Memory"}})
159+
executor.volume_mounts.append({"name": "dshm", "mountPath": "/dev/shm"})
160+
161+
if self.pvc_claim_name:
162+
vol_name = "runai-workspace"
163+
if not any(v.get("name") == vol_name for v in executor.volumes):
164+
executor.volumes.append(
165+
{"name": vol_name, "persistentVolumeClaim": {"claimName": self.pvc_claim_name}}
166+
)
167+
executor.volume_mounts.append({"name": vol_name, "mountPath": self.pvc_mount_path})
168+
169+
if self.env_vars:
170+
executor.env_vars.update(self.env_vars)

0 commit comments

Comments
 (0)