Skip to content

Commit 3d1bff8

Browse files
committed
Allow passing arch-specific optarch on cmdline/config
1 parent ec51396 commit 3d1bff8

File tree

5 files changed

+114
-41
lines changed

5 files changed

+114
-41
lines changed

easybuild/tools/options.py

+59-20
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,8 @@
9797
from easybuild.tools.toolchain.compiler import DEFAULT_OPT_LEVEL, OPTARCH_MAP_CHAR, OPTARCH_SEP, Compiler
9898
from easybuild.tools.toolchain.toolchain import SYSTEM_TOOLCHAIN_NAME
9999
from easybuild.tools.repository.repository import avail_repositories
100-
from easybuild.tools.systemtools import UNKNOWN, check_python_version, get_cpu_architecture, get_cpu_family
100+
from easybuild.tools.systemtools import UNKNOWN, CPU_ARCHITECTURES, CPU_FAMILIES, CPU_VECTOR_EXTS
101+
from easybuild.tools.systemtools import check_python_version, get_cpu_architecture, get_cpu_family
101102
from easybuild.tools.systemtools import get_cpu_features, get_system_info
102103
from easybuild.tools.version import this_is_easybuild
103104

@@ -930,29 +931,67 @@ def postprocess(self):
930931
cleanup_and_exit(self.tmpdir)
931932

932933
def _postprocess_optarch(self):
933-
"""Postprocess --optarch option."""
934-
optarch_parts = self.options.optarch.split(OPTARCH_SEP)
935-
936-
# we expect to find a ':' in every entry in optarch, in case optarch is specified on a per-compiler basis
937-
n_parts = len(optarch_parts)
938-
map_char_cnts = [p.count(OPTARCH_MAP_CHAR) for p in optarch_parts]
939-
if (n_parts > 1 and any(c != 1 for c in map_char_cnts)) or (n_parts == 1 and map_char_cnts[0] > 1):
934+
"""
935+
Postprocess --optarch option.
936+
937+
Format: map := <entry>(;<space>*<entry>)*
938+
entry := <compiler>(,<arch-spec)*:<flag-string>
939+
Example values:
940+
"GENERIC"
941+
"march=native"
942+
"Intel,x86:xHost; Intel,x86,AMD,AVX2:mavx2 -fma; GCC:march=native"
943+
"""
944+
# Split into entries, and each entry into key-value pairs
945+
optarch_parts = [i.strip().split(OPTARCH_MAP_CHAR) for i in self.options.optarch.split(OPTARCH_SEP)]
946+
947+
# We should either have a single value or a list of key-value pairs, and nothing else
948+
is_single_value = len(optarch_parts) == 1 and len(optarch_parts[0]) == 1
949+
if not is_single_value and any(len(i) != 2 for i in optarch_parts):
940950
raise EasyBuildError("The optarch option has an incorrect syntax: %s", self.options.optarch)
951+
952+
# if there are options for different compilers, we set up a dict
953+
if is_single_value:
954+
# if optarch is not in mapping format, we do nothing and just keep the string
955+
self.log.info("Keeping optarch raw: %s", self.options.optarch)
941956
else:
942-
# if there are options for different compilers, we set up a dict
943-
if OPTARCH_MAP_CHAR in optarch_parts[0]:
944-
optarch_dict = {}
945-
for compiler, compiler_opt in [p.split(OPTARCH_MAP_CHAR) for p in optarch_parts]:
946-
if compiler in optarch_dict:
947-
raise EasyBuildError("The optarch option contains duplicated entries for compiler %s: %s",
948-
compiler, self.options.optarch)
957+
optarch_dict = {}
958+
for key, compiler_opt in optarch_parts:
959+
# key can be either a compiler (only) or compiler and archspec(s)
960+
key_parts = key.split(',')
961+
compiler = key_parts.pop(0)
962+
if key_parts:
963+
for part, allowed_values in zip(key_parts, (CPU_ARCHITECTURES, CPU_FAMILIES, CPU_VECTOR_EXTS)):
964+
if part not in allowed_values:
965+
raise EasyBuildError("The optarch option has an incorrect syntax: %s\n"
966+
"'%s' of '%s' is not in allowed values: %s",
967+
self.options.optarch, part, key, allowed_values)
968+
arch_specs = tuple(key_parts) if len(key_parts) > 1 else key_parts[0]
969+
else:
970+
arch_specs = None
971+
try:
972+
compiler_dict = optarch_dict[compiler]
973+
# Convert single entry to dict if required
974+
if not isinstance(compiler_dict, dict):
975+
compiler_dict = {None: compiler_dict}
976+
optarch_dict[compiler] = compiler_dict
977+
if arch_specs in compiler_dict:
978+
if arch_specs is None:
979+
raise EasyBuildError("The optarch option contains a duplicated entry for compiler %s: %s",
980+
compiler, self.options.optarch)
981+
else:
982+
raise EasyBuildError("The optarch option contains a duplicated entry %s "
983+
"for compiler %s: %s",
984+
arch_specs, compiler, self.options.optarch)
949985
else:
986+
compiler_dict[arch_specs] = compiler_opt
987+
except KeyError:
988+
# Keep the dict flat when no archspecs are given
989+
if arch_specs is None:
950990
optarch_dict[compiler] = compiler_opt
951-
self.options.optarch = optarch_dict
952-
self.log.info("Transforming optarch into a dict: %s", self.options.optarch)
953-
# if optarch is not in mapping format, we do nothing and just keep the string
954-
else:
955-
self.log.info("Keeping optarch raw: %s", self.options.optarch)
991+
else:
992+
optarch_dict[compiler] = {arch_specs: compiler_opt}
993+
self.options.optarch = optarch_dict
994+
self.log.info("Transforming optarch into a dict: %s", self.options.optarch)
956995

957996
def _postprocess_close_pr_reasons(self):
958997
"""Postprocess --close-pr-reasons options"""

easybuild/tools/systemtools.py

+2
Original file line numberDiff line numberDiff line change
@@ -1131,6 +1131,8 @@ def create_possible_keys():
11311131
yield (arch, )
11321132
# Also allow single string entry
11331133
yield arch
1134+
# Default fallback for any arch
1135+
yield None
11341136

11351137
result = None
11361138
for key in create_possible_keys():

easybuild/tools/toolchain/compiler.py

+10-9
Original file line numberDiff line numberDiff line change
@@ -333,7 +333,7 @@ def _set_optimal_architecture(self, default_optarch=None):
333333
:param default_optarch: default value to use for optarch, rather than using default value based on architecture
334334
(--optarch and --optarch=GENERIC still override this value)
335335
"""
336-
ec_optarch = self.options.get('optarch', False)
336+
ec_optarch = self.options.get('optarch')
337337
if isinstance(ec_optarch, string_type):
338338
if OPTARCH_MAP_CHAR in ec_optarch:
339339
error_msg = "When setting optarch in the easyconfig (found %s), " % ec_optarch
@@ -346,20 +346,21 @@ def _set_optimal_architecture(self, default_optarch=None):
346346
optarch = build_option('optarch')
347347

348348
# --optarch is specified with flags to use
349-
if optarch is not None and isinstance(optarch, dict):
349+
if isinstance(optarch, dict):
350350
# optarch has been validated as complex string with multiple compilers and converted to a dictionary
351351
# first try module names, then the family in optarch
352-
current_compiler_names = (getattr(self, 'COMPILER_MODULE_NAME', []) +
353-
[getattr(self, 'COMPILER_FAMILY', None)])
352+
current_compiler_names = self.COMPILER_MODULE_NAME or []
353+
if self.COMPILER_FAMILY:
354+
current_compiler_names.append(self.COMPILER_FAMILY)
355+
compiler_optarch = None
354356
for current_compiler in current_compiler_names:
355357
if current_compiler in optarch:
356-
optarch = optarch[current_compiler]
358+
compiler_optarch = optarch[current_compiler]
357359
break
358-
# still a dict: no option for this compiler
359-
if isinstance(optarch, dict):
360-
optarch = None
360+
if compiler_optarch is None:
361361
self.log.info("_set_optimal_architecture: no optarch found for compiler %s. Ignoring option.",
362-
current_compiler)
362+
current_compiler_names)
363+
optarch = self._pick_optarch_entry(compiler_optarch)
363364

364365
use_generic = False
365366
if optarch is not None:

test/framework/options.py

+25-2
Original file line numberDiff line numberDiff line change
@@ -5226,10 +5226,14 @@ def test_parse_optarch(self):
52265226
options.options.optarch = 'Intel:something:somethingelse'
52275227
self.assertErrorRegex(EasyBuildError, error_msg, options.postprocess)
52285228

5229-
error_msg = "The optarch option contains duplicated entries for compiler"
5229+
error_msg = "The optarch option contains a duplicated entry for compiler"
52305230
options.options.optarch = 'Intel:something;GCC:somethingelse;Intel:anothersomething'
52315231
self.assertErrorRegex(EasyBuildError, error_msg, options.postprocess)
52325232

5233+
error_msg = "'x86' of 'x86' is not in allowed values"
5234+
options.options.optarch = 'Intel,x86:something'
5235+
self.assertErrorRegex(EasyBuildError, error_msg, options.postprocess)
5236+
52335237
# Check the parsing itself
52345238
gcc_generic_flags = "march=x86-64 -mtune=generic"
52355239
test_cases = [
@@ -5239,7 +5243,26 @@ def test_parse_optarch(self):
52395243
('Intel:xHost', {'Intel': 'xHost'}),
52405244
('Intel:GENERIC', {'Intel': 'GENERIC'}),
52415245
('Intel:xHost;GCC:%s' % gcc_generic_flags, {'Intel': 'xHost', 'GCC': gcc_generic_flags}),
5242-
('Intel:;GCC:%s' % gcc_generic_flags, {'Intel': '', 'GCC': gcc_generic_flags}),
5246+
# Allow empty values and spaces
5247+
('Intel:; GCC:%s' % gcc_generic_flags, {'Intel': '', 'GCC': gcc_generic_flags}),
5248+
# Arch specific values. With Compiler only selection at different places in the list
5249+
(
5250+
'Intel,x86_64,Intel,sse:i86iSSE; Intel,x86_64,AMD:i86AMD; Intel,POWER:iPwr; Intel:xHost; '
5251+
'GCC:GENERIC; Clang:cl; Clang,POWER:clPwr',
5252+
{
5253+
'Intel': {
5254+
('x86_64', 'Intel', 'sse'): 'i86iSSE',
5255+
('x86_64', 'AMD'): 'i86AMD',
5256+
'POWER': 'iPwr',
5257+
None: 'xHost',
5258+
},
5259+
'GCC': 'GENERIC',
5260+
'Clang': {
5261+
None: 'cl',
5262+
'POWER': 'clPwr',
5263+
},
5264+
},
5265+
),
52435266
]
52445267

52455268
for optarch_string, optarch_parsed in test_cases:

test/framework/systemtools.py

+18-10
Original file line numberDiff line numberDiff line change
@@ -923,16 +923,24 @@ def test_pick_opt_arch(self):
923923
avx = [st.SSE, st.SSE2, st.AVX]
924924
avx2 = avx + [st.AVX2]
925925
avx512 = avx2 + [st.AVX512F]
926-
self.assertEqual(st.pick_opt_arch(options, st.X86_64, st.INTEL, avx2), 'opt-x86')
927-
self.assertEqual(st.pick_opt_arch(options, st.X86_64, st.INTEL, []), 'opt-x86')
928-
self.assertEqual(st.pick_opt_arch(options, st.X86_64, st.AMD, avx), 'opt-amd')
929-
self.assertEqual(st.pick_opt_arch(options, st.X86_64, st.AMD, avx2), 'opt-amd-avx2')
930-
self.assertEqual(st.pick_opt_arch(options, st.X86_64, st.AMD, avx512), 'opt-amd-avx2')
931-
self.assertEqual(st.pick_opt_arch(options, st.AARCH64, st.AMD, avx2), 'opt-aarch64')
932-
self.assertEqual(st.pick_opt_arch(options, st.AARCH64, st.INTEL, avx2), 'opt-aarch64-sse')
933-
self.assertEqual(st.pick_opt_arch(options, st.AARCH64, st.INTEL, [st.SSE]), 'opt-aarch64-sse')
934-
self.assertEqual(st.pick_opt_arch(options, st.AARCH32, st.AMD, avx2), None)
935-
self.assertEqual(st.pick_opt_arch(options, st.POWER, st.IBM, []), None)
926+
for include_none in (False, True):
927+
if include_none:
928+
# Special fallback option when no entry matches
929+
options[None] = 'opt-none'
930+
self.assertEqual(st.pick_opt_arch(options, st.X86_64, st.INTEL, avx2), 'opt-x86')
931+
self.assertEqual(st.pick_opt_arch(options, st.X86_64, st.INTEL, []), 'opt-x86')
932+
self.assertEqual(st.pick_opt_arch(options, st.X86_64, st.AMD, avx), 'opt-amd')
933+
self.assertEqual(st.pick_opt_arch(options, st.X86_64, st.AMD, avx2), 'opt-amd-avx2')
934+
self.assertEqual(st.pick_opt_arch(options, st.X86_64, st.AMD, avx512), 'opt-amd-avx2')
935+
self.assertEqual(st.pick_opt_arch(options, st.AARCH64, st.AMD, avx2), 'opt-aarch64')
936+
self.assertEqual(st.pick_opt_arch(options, st.AARCH64, st.INTEL, avx2), 'opt-aarch64-sse')
937+
self.assertEqual(st.pick_opt_arch(options, st.AARCH64, st.INTEL, [st.SSE]), 'opt-aarch64-sse')
938+
if include_none:
939+
self.assertEqual(st.pick_opt_arch(options, st.AARCH32, st.AMD, avx2), 'opt-none')
940+
self.assertEqual(st.pick_opt_arch(options, st.POWER, st.IBM, []), 'opt-none')
941+
else:
942+
self.assertEqual(st.pick_opt_arch(options, st.AARCH32, st.AMD, avx2), None)
943+
self.assertEqual(st.pick_opt_arch(options, st.POWER, st.IBM, []), None)
936944

937945
def test_check_os_dependency(self):
938946
"""Test check_os_dependency."""

0 commit comments

Comments
 (0)