Skip to content

Commit

Permalink
Merge pull request #14 from hiker/mpi_omp_support
Browse files Browse the repository at this point in the history
Add support for specifying MPI and OpenMP in the config file
  • Loading branch information
lukehoffmann authored Aug 16, 2024
2 parents 552e4cc + 7887d24 commit 83836e7
Show file tree
Hide file tree
Showing 49 changed files with 835 additions and 359 deletions.
6 changes: 3 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ __pycache__/
*$py.class

# Build directory for documentation
docs/build
docs/source/api
docs/source/apidoc
Documentation/build
Documentation/source/api
Documentation/source/apidoc

# C extensions
*.so
Expand Down
2 changes: 1 addition & 1 deletion run_configs/gcom/build_gcom_ar.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
if __name__ == '__main__':

with BuildConfig(project_label='gcom object archive $compiler',
tool_box=ToolBox()) as state:
mpi=False, openmp=False, tool_box=ToolBox()) as state:
common_build_steps(state)
archive_objects(state, output_fpath='$output/libgcom.a')
cleanup_prebuilds(state, all_unused=True)
4 changes: 2 additions & 2 deletions run_configs/gcom/build_gcom_so.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
parsed_args = arg_parser.parse_args()

with BuildConfig(project_label='gcom shared library $compiler',
tool_box=ToolBox()) as state:
mpi=False, openmp=False, tool_box=ToolBox()) as state:
common_build_steps(state, fpic=True)
link_shared_object(state, output_fpath='$output/libgcom.so'),
link_shared_object(state, output_fpath='$output/libgcom.so')
cleanup_prebuilds(state, all_unused=True)
2 changes: 1 addition & 1 deletion run_configs/gcom/grab_gcom.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

# we put this here so the two build configs can read its source_root
grab_config = BuildConfig(project_label=f'gcom_source {revision}',
tool_box=ToolBox())
mpi=False, openmp=False, tool_box=ToolBox())


if __name__ == '__main__':
Expand Down
19 changes: 12 additions & 7 deletions run_configs/jules/build_jules.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,15 @@ def __init__(self):
tool_box.add_tool(Linker(compiler=fc))

with BuildConfig(project_label=f'jules {revision} $compiler',
tool_box=tool_box) as state:
# grab the source. todo: use some checkouts instead of exports in these configs.
fcm_export(state, src='fcm:jules.xm_tr/src', revision=revision, dst_label='src')
fcm_export(state, src='fcm:jules.xm_tr/utils', revision=revision, dst_label='utils')
mpi=False, openmp=False, tool_box=tool_box) as state:
# grab the source. todo: use some checkouts instead of exports
# in these configs.
fcm_export(state, src='fcm:jules.xm_tr/src', revision=revision,
dst_label='src')
fcm_export(state, src='fcm:jules.xm_tr/utils', revision=revision,
dst_label='utils')

grab_pre_build(state, path='/not/a/real/folder', allow_fail=True),
grab_pre_build(state, path='/not/a/real/folder', allow_fail=True)

# find the source files
find_source_files(state, path_filters=[
Expand All @@ -61,9 +64,11 @@ def __init__(self):
# move inc files to the root for easy tool use
root_inc_files(state)

preprocess_fortran(state, common_flags=['-P', '-DMPI_DUMMY', '-DNCDF_DUMMY', '-I$output'])
preprocess_fortran(state, common_flags=['-P', '-DMPI_DUMMY',
'-DNCDF_DUMMY', '-I$output'])

analyse(state, root_symbol='jules', unreferenced_deps=['imogen_update_carb'])
analyse(state, root_symbol='jules',
unreferenced_deps=['imogen_update_carb'])

compile_fortran(state)

Expand Down
2 changes: 1 addition & 1 deletion run_configs/lfric/atm.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ def file_filtering(config):
gpl_utils_source = gpl_utils_source_config.source_root / 'gpl_utils'

with BuildConfig(project_label='atm $compiler $two_stage',
tool_box=ToolBox()) as state:
mpi=False, openmp=False, tool_box=ToolBox()) as state:

# todo: use different dst_labels because they all go into the same folder,
# making it hard to see what came from where?
Expand Down
10 changes: 6 additions & 4 deletions run_configs/lfric/grab_lfric.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,12 @@
# these configs are interrogated by the build scripts
# todo: doesn't need two separate configs, they use the same project workspace
tool_box = ToolBox()
lfric_source_config = BuildConfig(project_label=f'lfric source {LFRIC_REVISION}',
tool_box=tool_box)
gpl_utils_source_config = BuildConfig(project_label=f'lfric source {LFRIC_REVISION}',
tool_box=tool_box)
lfric_source_config = BuildConfig(
project_label=f'lfric source {LFRIC_REVISION}',
mpi=False, openmp=False, tool_box=tool_box)
gpl_utils_source_config = BuildConfig(
project_label=f'lfric source {LFRIC_REVISION}',
mpi=False, openmp=False, tool_box=tool_box)


if __name__ == '__main__':
Expand Down
2 changes: 1 addition & 1 deletion run_configs/lfric/gungho.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
gpl_utils_source = gpl_utils_source_config.source_root / 'gpl_utils'

with BuildConfig(project_label='gungho $compiler $two_stage',
tool_box=ToolBox()) as state:
mpi=False, openmp=False, tool_box=ToolBox()) as state:
grab_folder(state, src=lfric_source / 'infrastructure/source/', dst_label='')
grab_folder(state, src=lfric_source / 'components/driver/source/', dst_label='')
grab_folder(state, src=lfric_source / 'components' / 'inventory' / 'source', dst_label='')
Expand Down
2 changes: 1 addition & 1 deletion run_configs/lfric/mesh_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
psyclone_overrides = Path(__file__).parent / 'mesh_tools_overrides'

with BuildConfig(project_label='mesh tools $compiler $two_stage',
tool_box=ToolBox()) as state:
mpi=False, openmp=False, tool_box=ToolBox()) as state:
grab_folder(state, src=lfric_source / 'infrastructure/source/', dst_label='')
grab_folder(state, src=lfric_source / 'mesh_tools/source/', dst_label='')
grab_folder(state, src=lfric_source / 'components/science/source/', dst_label='')
Expand Down
14 changes: 7 additions & 7 deletions run_configs/tiny_fortran/build_tiny_fortran.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,15 @@ def __init__(self):
tool_box.add_tool(Linker(compiler=fc))

with BuildConfig(project_label='tiny_fortran $compiler',
tool_box=tool_box) as state:
mpi=False, openmp=False, tool_box=tool_box) as state:
git_checkout(state, src='https://github.com/metomi/fab-test-data.git',
revision='main', dst_label='src'),
revision='main', dst_label='src')

find_source_files(state),
find_source_files(state)

preprocess_fortran(state),
preprocess_fortran(state)

analyse(state, root_symbol='my_prog'),
analyse(state, root_symbol='my_prog')

compile_fortran(state),
link_exe(state),
compile_fortran(state)
link_exe(state)
5 changes: 3 additions & 2 deletions run_configs/um/build_um.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,9 @@ def replace_in_file(inpath, outpath, find, replace):
revision = 'vn12.1'
um_revision = revision.replace('vn', 'um')

state = BuildConfig(project_label=f'um atmos safe {revision} $compiler $two_stage',
tool_box=ToolBox())
state = BuildConfig(
project_label=f'um atmos safe {revision} $compiler $two_stage',
mpi=False, openmp=False, tool_box=ToolBox())

# compiler-specific flags
compiler = state.tool_box[Category.FORTRAN_COMPILER]
Expand Down
105 changes: 74 additions & 31 deletions source/fab/build_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@

from fab.artefacts import ArtefactSet, ArtefactStore
from fab.constants import BUILD_OUTPUT, SOURCE_ROOT, PREBUILD
from fab.metrics import send_metric, init_metrics, stop_metrics, metrics_summary
from fab.metrics import (send_metric, init_metrics, stop_metrics,
metrics_summary)
from fab.tools.category import Category
from fab.tools.tool_box import ToolBox
from fab.steps.cleanup_prebuilds import CLEANUP_COUNT, cleanup_prebuilds
Expand All @@ -41,36 +42,50 @@ class BuildConfig():
"""
def __init__(self, project_label: str,
tool_box: ToolBox,
multiprocessing: bool = True, n_procs: Optional[int] = None,
mpi: bool,
openmp: bool,
multiprocessing: bool = True,
n_procs: Optional[int] = None,
reuse_artefacts: bool = False,
fab_workspace: Optional[Path] = None, two_stage=False,
verbose=False):
fab_workspace: Optional[Path] = None,
two_stage: bool = False,
verbose: bool = False):
"""
:param project_label:
Name of the build project. The project workspace folder is created from this name, with spaces replaced
by underscores.
Name of the build project. The project workspace folder is
created from this name, with spaces replaced by underscores.
:param tool_box: The ToolBox with all tools to use in the build.
:param mpi: whether the project uses MPI or not. This is used to
pick a default compiler (if not explicitly set in the ToolBox),
and controls PSyclone parameters.
:param openmp: whether the project should use OpenMP or not.
:param multiprocessing:
An option to disable multiprocessing to aid debugging.
:param n_procs:
The number of cores to use for multiprocessing operations. Defaults to the number of available cores.
The number of cores to use for multiprocessing operations.
Defaults to the number of available cores.
:param reuse_artefacts:
A flag to avoid reprocessing certain files on subsequent runs.
WARNING: Currently unsophisticated, this flag should only be used by Fab developers.
The logic behind flag will soon be improved, in a work package called "incremental build".
WARNING: Currently unsophisticated, this flag should only be
used by Fab developers. The logic behind flag will soon be
improved, in a work package called "incremental build".
:param fab_workspace:
Overrides the FAB_WORKSPACE environment variable.
If not set, and FAB_WORKSPACE is not set, the fab workspace defaults to *~/fab-workspace*.
If not set, and FAB_WORKSPACE is not set, the fab workspace
defaults to *~/fab-workspace*.
:param two_stage:
Compile .mod files first in a separate pass. Theoretically faster in some projects..
Compile .mod files first in a separate pass. Theoretically faster
in some projects.
:param verbose:
DEBUG level logging.
"""
self._tool_box = tool_box
self._mpi = mpi
self._openmp = openmp
self.two_stage = two_stage
self.verbose = verbose
compiler = tool_box[Category.FORTRAN_COMPILER]
compiler = tool_box.get_tool(Category.FORTRAN_COMPILER, mpi=mpi)
project_label = Template(project_label).safe_substitute(
compiler=compiler.name,
two_stage=f'{int(two_stage)+1}stage')
Expand All @@ -83,7 +98,8 @@ def __init__(self, project_label: str,
logger.info(f"fab workspace is {fab_workspace}")

self.project_workspace: Path = fab_workspace / self.project_label
self.metrics_folder: Path = self.project_workspace / 'metrics' / self.project_label
self.metrics_folder: Path = (self.project_workspace / 'metrics' /
self.project_label)

# source config
self.source_root: Path = self.project_workspace / SOURCE_ROOT
Expand All @@ -93,7 +109,8 @@ def __init__(self, project_label: str,
self.multiprocessing = multiprocessing

# turn off multiprocessing when debugging
# todo: turn off multiprocessing when running tests, as a good test runner will run using mp
# todo: turn off multiprocessing when running tests, as a good test
# runner will run using mp
if 'pydevd' in str(sys.gettrace()):
logger.info('debugger detected, running without multiprocessing')
self.multiprocessing = False
Expand Down Expand Up @@ -129,7 +146,8 @@ def __enter__(self):
self._start_time = datetime.now().replace(microsecond=0)
self._run_prep()

with TimerLogger(f'running {self.project_label} build steps') as build_timer:
with TimerLogger(f'running {self.project_label} '
f'build steps') as build_timer:
# this will return to the build script
self._build_timer = build_timer
return self
Expand All @@ -138,10 +156,12 @@ def __exit__(self, exc_type, exc_val, exc_tb):

if not exc_type: # None if there's no error.
if CLEANUP_COUNT not in self.artefact_store:
logger.info("no housekeeping step was run, using a default hard cleanup")
logger.info("no housekeeping step was run, using a "
"default hard cleanup")
cleanup_prebuilds(config=self, all_unused=True)

logger.info(f"Building '{self.project_label}' took {datetime.now() - self._start_time}")
logger.info(f"Building '{self.project_label}' took "
f"{datetime.now() - self._start_time}")

# always
self._finalise_metrics(self._start_time, self._build_timer)
Expand All @@ -164,9 +184,20 @@ def build_output(self) -> Path:
'''
return self.project_workspace / BUILD_OUTPUT

@property
def mpi(self) -> bool:
''':returns: whether MPI is requested or not in this config.'''
return self._mpi

@property
def openmp(self) -> bool:
''':returns: whether OpenMP is requested or not in this config.'''
return self._openmp

def add_current_prebuilds(self, artefacts: Iterable[Path]):
"""
Mark the given file paths as being current prebuilds, not to be cleaned during housekeeping.
Mark the given file paths as being current prebuilds, not to be
cleaned during housekeeping.
"""
self.artefact_store[ArtefactSet.CURRENT_PREBUILDS].update(artefacts)
Expand All @@ -193,7 +224,8 @@ def _prep_folders(self):
def _init_logging(self):
# add a file logger for our run
self.project_workspace.mkdir(parents=True, exist_ok=True)
log_file_handler = RotatingFileHandler(self.project_workspace / 'log.txt', backupCount=5, delay=True)
log_file_handler = RotatingFileHandler(
self.project_workspace / 'log.txt', backupCount=5, delay=True)
log_file_handler.doRollover()
logging.getLogger('fab').addHandler(log_file_handler)

Expand All @@ -207,9 +239,11 @@ def _init_logging(self):
def _finalise_logging(self):
# remove our file logger
fab_logger = logging.getLogger('fab')
log_file_handlers = list(by_type(fab_logger.handlers, RotatingFileHandler))
log_file_handlers = list(by_type(fab_logger.handlers,
RotatingFileHandler))
if len(log_file_handlers) != 1:
warnings.warn(f'expected to find 1 RotatingFileHandler for removal, found {len(log_file_handlers)}')
warnings.warn(f'expected to find 1 RotatingFileHandler for '
f'removal, found {len(log_file_handlers)}')
fab_logger.removeHandler(log_file_handlers[0])

def _finalise_metrics(self, start_time, steps_timer):
Expand Down Expand Up @@ -249,14 +283,16 @@ def __init__(self, match: str, flags: List[str]):
# For source in the um folder, add an absolute include path
AddFlags(match="$source/um/*", flags=['-I$source/include']),
# For source in the um folder, add an include path relative to each source file.
# For source in the um folder, add an include path relative to
# each source file.
AddFlags(match="$source/um/*", flags=['-I$relative/include']),
"""
self.match: str = match
self.flags: List[str] = flags

# todo: we don't need the project_workspace, we could just pass in the output folder
# todo: we don't need the project_workspace, we could just pass in the
# output folder
def run(self, fpath: Path, input_flags: List[str], config):
"""
Check if our filter matches a given file. If it does, add our flags.
Expand All @@ -269,12 +305,16 @@ def run(self, fpath: Path, input_flags: List[str], config):
Contains the folders for templating `$source` and `$output`.
"""
params = {'relative': fpath.parent, 'source': config.source_root, 'output': config.build_output}
params = {'relative': fpath.parent,
'source': config.source_root,
'output': config.build_output}

# does the file path match our filter?
if not self.match or fnmatch(str(fpath), Template(self.match).substitute(params)):
if not self.match or fnmatch(str(fpath),
Template(self.match).substitute(params)):
# use templating to render any relative paths in our flags
add_flags = [Template(flag).substitute(params) for flag in self.flags]
add_flags = [Template(flag).substitute(params)
for flag in self.flags]

# add our flags
input_flags += add_flags
Expand All @@ -284,15 +324,18 @@ class FlagsConfig():
"""
Return command-line flags for a given path.
Simply allows appending flags but may evolve to also replace and remove flags.
Simply allows appending flags but may evolve to also replace and
remove flags.
"""
def __init__(self, common_flags: Optional[List[str]] = None, path_flags: Optional[List[AddFlags]] = None):
def __init__(self, common_flags: Optional[List[str]] = None,
path_flags: Optional[List[AddFlags]] = None):
"""
:param common_flags:
List of flags to apply to all files. E.g `['-O2']`.
:param path_flags:
List of :class:`~fab.build_config.AddFlags` objects which apply flags to selected paths.
List of :class:`~fab.build_config.AddFlags` objects which apply
flags to selected paths.
"""
self.common_flags = common_flags or []
Expand All @@ -311,8 +354,8 @@ def flags_for_path(self, path: Path, config):
"""
# We COULD make the user pass these template params to the constructor
# but we have a design requirement to minimise the config burden on the user,
# so we take care of it for them here instead.
# but we have a design requirement to minimise the config burden on
# the user, so we take care of it for them here instead.
params = {'source': config.source_root, 'output': config.build_output}
flags = [Template(i).substitute(params) for i in self.common_flags]

Expand Down
Loading

0 comments on commit 83836e7

Please sign in to comment.