Skip to content
23 changes: 19 additions & 4 deletions easybuild/framework/easyconfig/easyconfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -1459,17 +1459,32 @@ def generate_template_values(self):

def _generate_template_values(self, ignore=None):
"""Actual code to generate the template values"""
if self.template_values is None:
self.template_values = {}

# step 0. self.template_values can/should be updated from outside easyconfig
# (eg the run_setp code in EasyBlock)
# (eg the run_step code in EasyBlock)

# step 1-3 work with easyconfig.templates constants
# disable templating with creating dict with template values to avoid looping back to here via __getitem__
prev_enable_templating = self.enable_templating

self.enable_templating = False

if self.template_values is None:
# if no template values are set yet, initiate with a minimal set of template values;
# this is important for easyconfig that use %(version_minor)s to define 'toolchain',
# which is a pretty weird use case, but fine...
self.template_values = template_constant_dict(self, ignore=ignore)

self.enable_templating = prev_enable_templating

# grab toolchain instance with templating support enabled,
# which is important in case the Toolchain instance was not created yet
toolchain = self.toolchain

# get updated set of template values, now with toolchain instance
# (which is used to define the %(mpi_cmd_prefix)s template)
self.enable_templating = False
template_values = template_constant_dict(self, ignore=ignore)
template_values = template_constant_dict(self, ignore=ignore, toolchain=toolchain)
self.enable_templating = prev_enable_templating

# update the template_values dict
Expand Down
13 changes: 12 additions & 1 deletion easybuild/framework/easyconfig/templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@
# versionmajor, versionminor, versionmajorminor (eg '.'.join(version.split('.')[:2])) )


def template_constant_dict(config, ignore=None, skip_lower=None):
def template_constant_dict(config, ignore=None, skip_lower=None, toolchain=None):
"""Create a dict for templating the values in the easyconfigs.
- config is a dict with the structure of EasyConfig._config
"""
Expand Down Expand Up @@ -257,6 +257,17 @@ def template_constant_dict(config, ignore=None, skip_lower=None):
except Exception:
_log.warning("Failed to get .lower() for name %s value %s (type %s)", name, value, type(value))

# step 5. add additional conditional templates
if toolchain is not None and hasattr(toolchain, 'mpi_cmd_prefix'):
try:
# get prefix for commands to be run with mpi runtime using default number of ranks
mpi_cmd_prefix = toolchain.mpi_cmd_prefix()
if mpi_cmd_prefix is not None:
template_values['mpi_cmd_prefix'] = mpi_cmd_prefix
except EasyBuildError as err:
# don't fail just because we couldn't resolve this template
_log.warning("Failed to create mpi_cmd_prefix template, error was:\n%s", err)

return template_values


Expand Down
36 changes: 31 additions & 5 deletions easybuild/tools/toolchain/mpi.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,22 @@ def mpi_family(self):
else:
raise EasyBuildError("mpi_family: MPI_FAMILY is undefined.")

def mpi_cmd_prefix(self, nr_ranks=1):
"""Construct an MPI command prefix to precede an executable"""

# Verify that the command appears at the end of mpi_cmd_for
test_cmd = 'xxx_command_xxx'
mpi_cmd = self.mpi_cmd_for(test_cmd, nr_ranks)
if mpi_cmd.rstrip().endswith(test_cmd):
result = mpi_cmd.replace(test_cmd, '').rstrip()
else:
warning_msg = "mpi_cmd_for cannot be used by mpi_cmd_prefix, "
warning_msg += "requires that %(cmd)s template appears at the end"
self.log.warning(warning_msg)
result = None

return result

def mpi_cmd_for(self, cmd, nr_ranks):
"""Construct an MPI command for the given command and number of ranks."""

Expand All @@ -180,10 +196,10 @@ def mpi_cmd_for(self, cmd, nr_ranks):
self.log.info("Using specified template for MPI commands: %s", mpi_cmd_template)
else:
# different known mpirun commands
mpirun_n_cmd = "mpirun -n %(nr_ranks)d %(cmd)s"
mpirun_n_cmd = "mpirun -n %(nr_ranks)s %(cmd)s"
mpi_cmds = {
toolchain.OPENMPI: mpirun_n_cmd,
toolchain.QLOGICMPI: "mpirun -H localhost -np %(nr_ranks)d %(cmd)s",
toolchain.QLOGICMPI: "mpirun -H localhost -np %(nr_ranks)s %(cmd)s",
toolchain.INTELMPI: mpirun_n_cmd,
toolchain.MVAPICH2: mpirun_n_cmd,
toolchain.MPICH: mpirun_n_cmd,
Expand All @@ -201,7 +217,7 @@ def mpi_cmd_for(self, cmd, nr_ranks):
impi_ver = self.get_software_version(self.MPI_MODULE_NAME)[0]
if LooseVersion(impi_ver) <= LooseVersion('4.1'):

mpi_cmds[toolchain.INTELMPI] = "mpirun %(mpdbf)s %(nodesfile)s -np %(nr_ranks)d %(cmd)s"
mpi_cmds[toolchain.INTELMPI] = "mpirun %(mpdbf)s %(nodesfile)s -np %(nr_ranks)s %(cmd)s"

# set temporary dir for MPD
# note: this needs to be kept *short*,
Expand Down Expand Up @@ -230,7 +246,7 @@ def mpi_cmd_for(self, cmd, nr_ranks):

# create nodes file
nodes = os.path.join(tmpdir, 'nodes')
write_file(nodes, "localhost\n" * nr_ranks)
write_file(nodes, "localhost\n" * int(nr_ranks))

params.update({'nodesfile': "-machinefile %s" % nodes})

Expand All @@ -240,9 +256,19 @@ def mpi_cmd_for(self, cmd, nr_ranks):
else:
raise EasyBuildError("Don't know which template MPI command to use for MPI family '%s'", mpi_family)

missing = []
for key in sorted(params.keys()):
tmpl = '%(' + key + ')s'
if tmpl not in mpi_cmd_template:
missing.append(tmpl)
if missing:
raise EasyBuildError("Missing templates in mpi-cmd-template value '%s': %s",
mpi_cmd_template, ', '.join(missing))

try:
res = mpi_cmd_template % params
except KeyError as err:
raise EasyBuildError("Failed to complete MPI cmd template '%s' with %s: %s", mpi_cmd_template, params, err)
raise EasyBuildError("Failed to complete MPI cmd template '%s' with %s: KeyError %s",
mpi_cmd_template, params, err)

return res
13 changes: 13 additions & 0 deletions test/framework/easyconfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -1008,6 +1008,19 @@ def test_templating(self):
eb['description'] = "test easyconfig % %% %s% %%% %(name)s %%(name)s %%%(name)s %%%%(name)s"
self.assertEqual(eb['description'], "test easyconfig % %% %s% %%% PI %(name)s %PI %%(name)s")

# test use of %(mpi_cmd_prefix)s template
test_ecs_dir = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'easyconfigs', 'test_ecs')
gompi_ec = os.path.join(test_ecs_dir, 't', 'toy', 'toy-0.0-gompi-2018a.eb')
test_ec = os.path.join(self.test_prefix, 'test.eb')
write_file(test_ec, read_file(gompi_ec) + "\nsanity_check_commands = ['%(mpi_cmd_prefix)s toy']")

ec = EasyConfig(test_ec)
self.assertEqual(ec['sanity_check_commands'], ['mpirun -n 1 toy'])

init_config(build_options={'mpi_cmd_template': "mpiexec -np %(nr_ranks)s -- %(cmd)s "})
ec = EasyConfig(test_ec)
self.assertEqual(ec['sanity_check_commands'], ['mpiexec -np 1 -- toy'])

def test_templating_doc(self):
"""test templating documentation"""
doc = avail_easyconfig_templates()
Expand Down
6 changes: 3 additions & 3 deletions test/framework/robot.py
Original file line number Diff line number Diff line change
Expand Up @@ -424,14 +424,14 @@ def test_resolve_dependencies_minimal(self):
# to test resolving of dependencies with minimal toolchain
# for each of these, we know test easyconfigs are available (which are required here)
"dependencies = [",
" ('OpenMPI', '2.1.2'),", # available with GCC/6.4.0-2.28
# the use of %(version_minor)s here is mainly to check if templates are being handled correctly
# (it doesn't make much sense, but it serves the purpose)
" ('OpenMPI', '%(version_minor)s.1.2'),", # available with GCC/6.4.0-2.28
" ('OpenBLAS', '0.2.20'),", # available with GCC/6.4.0-2.28
" ('ScaLAPACK', '2.0.2', '-OpenBLAS-0.2.20'),", # available with gompi/2018a
" ('SQLite', '3.8.10.2'),",
"]",
# toolchain as list line, for easy modification later;
# the use of %(version_minor)s here is mainly to check if templates are being handled correctly
# (it doesn't make much sense, but it serves the purpose)
"toolchain = {'name': 'foss', 'version': '%(version_minor)s018a'}",
]
write_file(barec, '\n'.join(barec_lines))
Expand Down
53 changes: 53 additions & 0 deletions test/framework/toolchain.py
Original file line number Diff line number Diff line change
Expand Up @@ -948,6 +948,48 @@ def test_nosuchtoolchain(self):
tc = self.get_toolchain('intel', version='1970.01')
self.assertErrorRegex(EasyBuildError, "No module found for toolchain", tc.prepare)

def test_mpi_cmd_prefix(self):
"""Test mpi_exec_nranks function."""
self.modtool.prepend_module_path(self.test_prefix)

tc = self.get_toolchain('gompi', version='2018a')
tc.prepare()
self.assertEqual(tc.mpi_cmd_prefix(nr_ranks=2), "mpirun -n 2")
self.assertEqual(tc.mpi_cmd_prefix(nr_ranks='2'), "mpirun -n 2")
self.assertEqual(tc.mpi_cmd_prefix(), "mpirun -n 1")
self.modtool.purge()

self.setup_sandbox_for_intel_fftw(self.test_prefix)
tc = self.get_toolchain('intel', version='2018a')
tc.prepare()
self.assertEqual(tc.mpi_cmd_prefix(nr_ranks=2), "mpirun -n 2")
self.assertEqual(tc.mpi_cmd_prefix(nr_ranks='2'), "mpirun -n 2")
self.assertEqual(tc.mpi_cmd_prefix(), "mpirun -n 1")
self.modtool.purge()

self.setup_sandbox_for_intel_fftw(self.test_prefix, imklver='10.2.6.038')
tc = self.get_toolchain('intel', version='2012a')
tc.prepare()

mpi_exec_nranks_re = re.compile("^mpirun --file=.*/mpdboot -machinefile .*/nodes -np 4")
self.assertTrue(mpi_exec_nranks_re.match(tc.mpi_cmd_prefix(nr_ranks=4)))
mpi_exec_nranks_re = re.compile("^mpirun --file=.*/mpdboot -machinefile .*/nodes -np 1")
self.assertTrue(mpi_exec_nranks_re.match(tc.mpi_cmd_prefix()))

# test specifying custom template for MPI commands
init_config(build_options={'mpi_cmd_template': "mpiexec -np %(nr_ranks)s -- %(cmd)s", 'silent': True})
self.assertEqual(tc.mpi_cmd_prefix(nr_ranks="7"), "mpiexec -np 7 --")
self.assertEqual(tc.mpi_cmd_prefix(), "mpiexec -np 1 --")

# check that we return None when command does not appear at the end of the template
init_config(build_options={'mpi_cmd_template': "mpiexec -np %(nr_ranks)s -- %(cmd)s option", 'silent': True})
self.assertEqual(tc.mpi_cmd_prefix(nr_ranks="7"), None)
self.assertEqual(tc.mpi_cmd_prefix(), None)

# template with extra spaces at the end if fine though
init_config(build_options={'mpi_cmd_template': "mpirun -np %(nr_ranks)s %(cmd)s ", 'silent': True})
self.assertEqual(tc.mpi_cmd_prefix(), "mpirun -np 1")

def test_mpi_cmd_for(self):
"""Test mpi_cmd_for function."""
self.modtool.prepend_module_path(self.test_prefix)
Expand All @@ -974,6 +1016,17 @@ def test_mpi_cmd_for(self):
init_config(build_options={'mpi_cmd_template': "mpiexec -np %(nr_ranks)s -- %(cmd)s", 'silent': True})
self.assertEqual(tc.mpi_cmd_for('test123', '7'), "mpiexec -np 7 -- test123")

# check whether expected error is raised when a template with missing keys is used;
# %(ranks)s should be %(nr_ranks)s
init_config(build_options={'mpi_cmd_template': "mpiexec -np %(ranks)s -- %(cmd)s", 'silent': True})
error_pattern = \
r"Missing templates in mpi-cmd-template value 'mpiexec -np %\(ranks\)s -- %\(cmd\)s': %\(nr_ranks\)s"
self.assertErrorRegex(EasyBuildError, error_pattern, tc.mpi_cmd_for, 'test', 1)

init_config(build_options={'mpi_cmd_template': "mpirun %(foo)s -np %(nr_ranks)s %(cmd)s", 'silent': True})
error_pattern = "Failed to complete MPI cmd template .* with .*: KeyError 'foo'"
self.assertErrorRegex(EasyBuildError, error_pattern, tc.mpi_cmd_for, 'test', 1)

def test_prepare_deps(self):
"""Test preparing for a toolchain when dependencies are involved."""
tc = self.get_toolchain('GCC', version='6.4.0-2.28')
Expand Down