Skip to content

Commit 7f476d9

Browse files
authored
Add CLI option for system parameters and support for Matrix system (#27)
* Added a system definition for Matrix and Vector. * Fixed memory size * Added command line argument to express the desired number of GPUs per process (task). For most AI codes, this should be 1, which is the default, but it can now be set. Updated the FLUX and SLURM schedulers to set this field when necessary. This also addresses an issue when running on compute resources that can be shared and are not exclusive. * Ran black. * Added support for specifying a set of command line arguments for the system parameters. These will overwrite and known or autodetected system parameters.
1 parent 76dc8d8 commit 7f476d9

File tree

10 files changed

+125
-26
lines changed

10 files changed

+125
-26
lines changed

hpc_launcher/cli/common_args.py

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,18 @@
2323

2424
from dataclasses import fields
2525

26+
class ParseKVAction(argparse.Action):
27+
def __call__(self, parser, namespace, values, option_string=None):
28+
setattr(namespace, self.dest, dict())
29+
for each in values:
30+
try:
31+
key, value = each.split("=")
32+
getattr(namespace, self.dest)[key] = value
33+
except ValueError as ex:
34+
message = "\nTraceback: {}".format(ex)
35+
message += "\nError on '{}' || It should be 'key=value'".format(each)
36+
raise argparse.ArgumentError(self, str(message))
37+
2638

2739
def create_scheduler_arguments(**kwargs) -> dict[str, str]:
2840
cmdline_args = {}
@@ -68,6 +80,13 @@ def setup_arguments(parser: argparse.ArgumentParser):
6880
help="Specifies the number of requested processes per node",
6981
)
7082

83+
group.add_argument(
84+
"--gpus-per-proc",
85+
type=int,
86+
default=None, # Internally, if there are GPUs, this will default to 1
87+
help="Specifies the number of requested GPUs per process (default: 1)",
88+
)
89+
7190
group.add_argument("-q", "--queue", default=None, help="Specifies the queue to use")
7291

7392
# Constraints
@@ -98,6 +117,21 @@ def setup_arguments(parser: argparse.ArgumentParser):
98117
help="Run locally (i.e., one process without a batch " "scheduler)",
99118
)
100119

120+
# System
121+
group = parser.add_argument_group(
122+
"System",
123+
"Provide system parameters from the CLI -- overrides built-in system descriptions and autodetection",
124+
)
125+
group.add_argument(
126+
"-p",
127+
"--system-params",
128+
dest="system_params",
129+
nargs='+',
130+
action=ParseKVAction,
131+
help="Specifies some or all of the parameters of a system as a dictionary (note it will override any known or autodetected parameters): -p cores_per_node=<int> gpus_per_node=<int> gpu_arch=<str> mem_per_gpu=<float> numa_domains=<int> scheduler=<str>",
132+
metavar="KEY1=VALUE1",
133+
)
134+
101135
# Schedule
102136
group = parser.add_argument_group(
103137
"Schedule", "Arguments that determine when a job will run"
@@ -206,7 +240,7 @@ def setup_arguments(parser: argparse.ArgumentParser):
206240

207241
def validate_arguments(args: argparse.Namespace):
208242
"""
209-
Validation checks for the commong arguments. Raises exceptions on failure.
243+
Validation checks for the common arguments. Raises exceptions on failure.
210244
211245
:param args: The parsed arguments.
212246
"""
@@ -259,12 +293,16 @@ def process_arguments(args: argparse.Namespace, logger: logging.Logger) -> Syste
259293
validate_arguments(args)
260294

261295
# Set system and launch configuration based on arguments
262-
system, args.nodes, args.procs_per_node = configure.configure_launch(
263-
args.queue,
264-
args.nodes,
265-
args.procs_per_node,
266-
args.gpus_at_least,
267-
args.gpumem_at_least,
296+
system, args.nodes, args.procs_per_node, args.gpus_per_proc = (
297+
configure.configure_launch(
298+
args.queue,
299+
args.nodes,
300+
args.procs_per_node,
301+
args.gpus_per_proc,
302+
args.gpus_at_least,
303+
args.gpumem_at_least,
304+
args.system_params,
305+
)
268306
)
269307

270308
return system

hpc_launcher/schedulers/flux.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,13 @@ def build_command_string_and_batch_script(
8080
if not blocking:
8181
header.write(f"# FLUX: {tmp}\n")
8282

83+
# Set the Number of GPUs per task
84+
if self.gpus_per_proc > 0:
85+
tmp = f"--gpus-per-task={self.gpus_per_proc}"
86+
cmd_args += [tmp]
87+
if not blocking:
88+
header.write(f"#FLUX: {tmp}\n")
89+
8390
if self.work_dir:
8491
tmp = [f"--setattr=system.cwd={os.path.abspath(self.work_dir)}"]
8592
self.select_interactive_or_batch(tmp, header, cmd_args, blocking)

hpc_launcher/schedulers/scheduler.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ class Scheduler:
4242
nodes: int
4343
# Processes per node
4444
procs_per_node: int
45+
# GPUs per Process (or task) if any
46+
gpus_per_proc: int
4547
# Job name
4648
job_name: Optional[str] = None
4749
# Working directory (by default, uses current working directory)

hpc_launcher/schedulers/slurm.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,11 +93,18 @@ def build_command_string_and_batch_script(
9393
header.write(f"#SBATCH {tmp}\n")
9494

9595
# Number of Tasks per node
96-
tmp = f"--ntasks-per-node={self.nodes * self.procs_per_node}"
96+
tmp = f"--ntasks-per-node={self.procs_per_node}"
9797
cmd_args += [tmp]
9898
if not blocking:
9999
header.write(f"#SBATCH {tmp}\n")
100100

101+
# Set the Number of GPUs per task
102+
if self.gpus_per_proc > 0:
103+
tmp = f"--gpus-per-task={self.gpus_per_proc}"
104+
cmd_args += [tmp]
105+
if not blocking:
106+
header.write(f"#SBATCH {tmp}\n")
107+
101108
if self.work_dir:
102109
tmp = [f"--chdir={os.path.abspath(self.work_dir)}"]
103110
self.select_interactive_or_batch(tmp, header, cmd_args, blocking)

hpc_launcher/systems/autodetect.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,7 @@ def autodetect_current_system(quiet: bool = False) -> System:
203203
if sys in ("tioga", "tuolumne", "elcap", "rzadams", "tenaya"):
204204
return ElCapitan(sys)
205205

206-
if sys == "ipa":
206+
if sys in ("ipa", "matrix", "vector"):
207207
return CTS2(sys)
208208

209209
if sys in ("lassen", "sierra", "rzanzel"):

hpc_launcher/systems/configure.py

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,20 +12,26 @@
1212
#
1313
# SPDX-License-Identifier: (Apache-2.0)
1414
import logging
15+
from typing import Optional
16+
from dataclasses import dataclass, fields, asdict
1517
from hpc_launcher.systems import autodetect
16-
from hpc_launcher.systems.system import System
18+
from hpc_launcher.systems.system import System, SystemParams
1719
from hpc_launcher.utils import ceildiv
1820

1921
logger = logging.getLogger(__name__)
2022

23+
def convert_to_type_of_another(variable_to_convert, reference_variable):
24+
return type(reference_variable)(variable_to_convert)
2125

2226
def configure_launch(
2327
queue: str,
2428
nodes: int,
2529
procs_per_node: int,
30+
gpus_per_proc: int,
2631
gpus_at_least: int,
2732
gpumem_at_least: int,
28-
) -> tuple[System, int, int]:
33+
cli_system_params: Optional[tuple[int, int, str, float, int, str, Optional[float]]],
34+
) -> tuple[System, int, int, int]:
2935
"""
3036
See if the system can be autodetected and then process some special
3137
arguments that can autoselect the number of ranks / GPUs.
@@ -47,9 +53,37 @@ def configure_launch(
4753
)
4854
system_params = system.system_parameters(queue)
4955

56+
# If any system parameters were provided on the command line, potentially overriding any known or discovered system parameters
57+
if cli_system_params:
58+
if not system_params: # Use a default set of system parameters
59+
system_params = SystemParams()
60+
_cli_system_params_dict = asdict(system_params)
61+
for field in fields(system_params):
62+
if field.name in cli_system_params:
63+
_cli_system_params_dict[field.name] = convert_to_type_of_another(cli_system_params[field.name], _cli_system_params_dict[field.name])
64+
# Create a new system_params with the proper fields overwritten
65+
system_params = SystemParams(**_cli_system_params_dict)
66+
67+
if not gpus_per_proc:
68+
gpus_per_proc = 0
69+
if system_params is not None:
70+
if gpus_per_proc == 0 and system_params.gpus_per_node > 0:
71+
# If gpus_per_proc wasn't set and there are gpus on the node set it to a default of 1
72+
gpus_per_proc = 1
73+
if gpus_per_proc > system_params.gpus_per_node:
74+
logger.info(
75+
f"Requested number of GPUs per process {gpus_per_proc} exceeds the number of GPUs per node {system_params.gpus_per_node}"
76+
)
77+
gpus_per_proc = system_params.gpus_per_node
78+
79+
if procs_per_node * gpus_per_proc > system_params.gpus_per_node:
80+
logger.info(
81+
f"The combination of {procs_per_node} processes per node and {gpus_per_proc} GPUs per process exceeds the number of GPUs per node {system_params.gpus_per_node}"
82+
)
83+
5084
# If the user requested a specific number of processes per node, honor that
5185
if nodes and procs_per_node:
52-
return system, nodes, procs_per_node
86+
return system, nodes, procs_per_node, gpus_per_proc
5387

5488
# Otherwise, if there is a valid set of system parameters, try to fill in
5589
# the blanks provided by the user
@@ -69,5 +103,7 @@ def configure_launch(
69103
nodes = 1
70104
if not procs_per_node:
71105
procs_per_node = 1
106+
if not gpus_per_proc:
107+
gpus_per_proc = 1
72108

73-
return system, nodes, procs_per_node
109+
return system, nodes, procs_per_node, gpus_per_proc

hpc_launcher/systems/lc/cts2.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,25 @@
1616
from hpc_launcher.systems.system import System, SystemParams
1717
import os
1818

19+
_h100_node = SystemParams(112, 4, "sm_90", 80.0, 8, "slurm")
1920

2021
# Known LC systems
2122
_system_params = {
2223
"ipa": (
2324
"a100",
2425
{
25-
"a100": SystemParams(32, 2, "sm_80", 40, 1, "slurm"),
26-
"aa100": SystemParams(16, 2, "sm_80", 40, 2, "slurm"),
27-
"av100": SystemParams(32, 2, "sm_70", 32, 2, "slurm"),
28-
"v100": SystemParams(16, 2, "sm_70", 32, 2, "slurm"),
26+
"a100": SystemParams(32, 2, "sm_80", 40.0, 1, "slurm"),
27+
"aa100": SystemParams(16, 2, "sm_80", 40.0, 2, "slurm"),
28+
"av100": SystemParams(32, 2, "sm_70", 32.0, 2, "slurm"),
29+
"v100": SystemParams(16, 2, "sm_70", 32.0, 2, "slurm"),
30+
},
31+
),
32+
"matrix": (
33+
"pbatch",
34+
{
35+
"pbatch": _h100_node,
36+
"pdebug": _h100_node,
37+
"erl": _h100_node,
2938
},
3039
),
3140
}

hpc_launcher/systems/lc/el_capitan_family.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@
1818

1919

2020
# Known LC systems
21-
_mi250x_node = SystemParams(64, 8, "gfx90a", 64, 4, "flux")
21+
_mi250x_node = SystemParams(64, 8, "gfx90a", 64.0, 4, "flux")
2222
# APUs can run into a snarl where they OOM if too much GPU memory is allocated
23-
_mi300a_node = SystemParams(96, 4, "gfx942", 128, 4, "flux", 0.8)
23+
_mi300a_node = SystemParams(96, 4, "gfx942", 128.0, 4, "flux", 0.8)
2424
_system_params = {
2525
"tioga": (
2626
"pdebug",

hpc_launcher/systems/lc/sierra_family.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66

77
# Supported LC systems
8-
_sierra_node = SystemParams(16, 4, "sm_70", 16, 2, "lsf")
8+
_sierra_node = SystemParams(16, 4, "sm_70", 16.0, 2, "lsf")
99
_system_params = {
1010
"lassen": (
1111
"pbatch",

hpc_launcher/systems/system.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,17 +29,17 @@ class SystemParams:
2929
"""Simple data structure to describe an LC system."""
3030

3131
# Number of CPU cores per compute node
32-
cores_per_node: int
32+
cores_per_node: int = 1
3333
# Number of GPUs per node
34-
gpus_per_node: int
34+
gpus_per_node: int = 0
3535
# Vendor specific GPU compiler architecture
36-
gpu_arch: str
36+
gpu_arch: str = None
3737
# Number of GB of memory per GPU
38-
mem_per_gpu: float
38+
mem_per_gpu: float = 0.0
3939
# Number of NUMA domains
40-
numa_domains: int
40+
numa_domains: int = 1
4141
# String name of the Scheduler class
42-
scheduler: str
42+
scheduler: str = None
4343
# Optional system level guard to limit GPU/APU memory utilization
4444
fraction_max_gpu_mem: Optional[float] = 1.0
4545

0 commit comments

Comments
 (0)