From 16bcbd4e47e850c6c7f7bc56da9e8dc58a18aeea Mon Sep 17 00:00:00 2001 From: Iris van der Werf Date: Thu, 7 May 2026 10:14:25 +0200 Subject: [PATCH 1/7] Allow no resources for non-mpi simulations --- ymmsl/v0_2/configuration.py | 20 ++++++++++++-------- ymmsl/v0_2/tests/test_configuration.py | 23 +++++++++++++++++++---- 2 files changed, 31 insertions(+), 12 deletions(-) diff --git a/ymmsl/v0_2/configuration.py b/ymmsl/v0_2/configuration.py index 9638a12..1ae0591 100644 --- a/ymmsl/v0_2/configuration.py +++ b/ymmsl/v0_2/configuration.py @@ -535,6 +535,9 @@ def _check_resources( self, component_paths: Dict[Reference, Component]) -> List[str]: """Check that each component path has a corresponding resource request. + For non-MPI components, resources are optional: if not specified, + a default of 1 thread will be assumed at runtime (e.g. by muscle3). + Returns a list of errors, empty if all is ok. """ errors = list() @@ -543,16 +546,17 @@ def _check_resources( if impl_ref is None or impl_ref not in self.programs: continue - if path not in self.resources: - errors.append(f'Component "{path}" is missing a resource request') - else: - impl = self.programs[impl_ref] - em_mpi = impl.execution_model in ( - ExecutionModel.OPENMPI, ExecutionModel.INTELMPI, - ExecutionModel.SRUNMPI) + impl = self.programs[impl_ref] + em_mpi = impl.execution_model in ( + ExecutionModel.OPENMPI, ExecutionModel.INTELMPI, + ExecutionModel.SRUNMPI) - em_nompi = impl.execution_model is ExecutionModel.DIRECT + em_nompi = impl.execution_model is ExecutionModel.DIRECT + if path not in self.resources: + if em_mpi: + errors.append(f'Component "{path}" is missing a resource request') + else: res_mpi = isinstance( self.resources[path], (MPICoresResReq, MPINodesResReq)) diff --git a/ymmsl/v0_2/tests/test_configuration.py b/ymmsl/v0_2/tests/test_configuration.py index f01166b..b4612b4 100644 --- a/ymmsl/v0_2/tests/test_configuration.py +++ b/ymmsl/v0_2/tests/test_configuration.py @@ -5,9 +5,9 @@ from ymmsl.io import load, dump from ymmsl.v0_2 import ( - Configuration, Checkpoints, Component, Conduit, Identifier, ImportStatement, - Model, Operator, Port, Ports, Program, Reference, Settings, SettingType, - SettingValue, ThreadedResReq, Timeline) + Configuration, Checkpoints, Component, Conduit, ExecutionModel, Identifier, + ImportStatement, Model, Operator, Port, Ports, Program, Reference, Settings, + SettingType, SettingValue, ThreadedResReq, Timeline) Ref = Reference @@ -456,6 +456,21 @@ def test_check_inconsistent_resources( with pytest.raises(RuntimeError) as e: config_inconsistent_resources.check_consistent() - assert len(str(e.value).split('\n')) == 4 + # The missing resource for the non-MPI component is no longer an error. + assert len(str(e.value).split('\n')) == 3 config_inconsistent_resources.check_consistent(False) + + +def test_check_no_resources_for_non_mpi() -> None: + """Non-MPI components without resources should pass check_consistent.""" + model = Model( + 'no_resources_test', None, 'description', None, + [Component('worker', Ports(), 'description', 'worker_prog')]) + + programs = [Program('worker_prog', script='worker', + execution_model=ExecutionModel.DIRECT)] + + # No resources specified - should be fine for non-MPI + config = Configuration('test', None, [model], None, None, programs, resources=None) + config.check_consistent() From 9a790fd13f30dc4b3c89fdae6a24bfa81d0cdb5a Mon Sep 17 00:00:00 2001 From: Iris van der Werf Date: Thu, 7 May 2026 11:04:44 +0200 Subject: [PATCH 2/7] Add documentation and update docstrings --- docs/describing_runs.rst | 3 ++- ymmsl/v0_1/execution.py | 7 +++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/describing_runs.rst b/docs/describing_runs.rst index 0ce438b..2b6738f 100644 --- a/docs/describing_runs.rst +++ b/docs/describing_runs.rst @@ -17,7 +17,8 @@ threads or processes can be specified; memory and GPUs are future work. Resources are specified per component, and apply to each instance of that component. For single- or multithreaded components, or components that use multiple local processes (for example with OpenMP, or Python's ``multiprocessing``), you specify the number of -threads: +threads. If no resource is defined for a non-MPI component, a default of 1 thread will +be assigned to it at runtime (e.g. by MUSCLE3): .. code-block:: yaml :caption: Resources for threaded processes diff --git a/ymmsl/v0_1/execution.py b/ymmsl/v0_1/execution.py index d8191bb..502de51 100644 --- a/ymmsl/v0_1/execution.py +++ b/ymmsl/v0_1/execution.py @@ -326,6 +326,10 @@ def _yatiml_sweeten(cls, node: yatiml.Node) -> None: class ResourceRequirements: """Describes resources to allocate for components. + For non-MPI components, specifying resources is optional. If no + resource is defined for a non-MPI component, a default of 1 thread + will be assigned to it at runtime (e.g. by MUSCLE3). + Attributes: name: Name of the component to configure. """ @@ -350,6 +354,9 @@ class ThreadedResReq(ResourceRequirements): that do not support MPI. As many cores as specified will be allocated on a single node, for each instance. + If no resource is defined for a non-MPI component, a default of + 1 thread will be assigned to it at runtime (e.g. by MUSCLE3). + Attributes: name: Name of the component to configure. threads: Number of threads/cores per instance. From ceb3736aa73c505a383f6210c8a8c059f7bf9ae8 Mon Sep 17 00:00:00 2001 From: Iris van der Werf Date: Thu, 7 May 2026 11:07:57 +0200 Subject: [PATCH 3/7] update documentation --- docs/describing_runs.rst | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/docs/describing_runs.rst b/docs/describing_runs.rst index 2b6738f..4882a98 100644 --- a/docs/describing_runs.rst +++ b/docs/describing_runs.rst @@ -17,8 +17,7 @@ threads or processes can be specified; memory and GPUs are future work. Resources are specified per component, and apply to each instance of that component. For single- or multithreaded components, or components that use multiple local processes (for example with OpenMP, or Python's ``multiprocessing``), you specify the number of -threads. If no resource is defined for a non-MPI component, a default of 1 thread will -be assigned to it at runtime (e.g. by MUSCLE3): +threads: .. code-block:: yaml :caption: Resources for threaded processes @@ -30,6 +29,15 @@ be assigned to it at runtime (e.g. by MUSCLE3): micro: threads: 8 +If no resource is defined for a non-MPI component, a default of 1 thread will +be assigned to it at runtime (e.g. by MUSCLE3). So, you could also define: + +.. code-block:: yaml + :caption: Resources for threaded processes + + resources: + micro: + threads: 8 On the Python side, this is represented by :class:`.ymmsl.v0_2.ThreadedResReq` (short for ThreadedResourceRequirements), which holds the name of the component it specifies From bb84a53d55d3976f4e6b4320bac1f584ea4ae11e Mon Sep 17 00:00:00 2001 From: Iris van der Werf Date: Fri, 8 May 2026 10:26:40 +0200 Subject: [PATCH 4/7] use inline yMMSL config for test --- ymmsl/v0_2/tests/test_configuration.py | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/ymmsl/v0_2/tests/test_configuration.py b/ymmsl/v0_2/tests/test_configuration.py index b4612b4..a1ac76f 100644 --- a/ymmsl/v0_2/tests/test_configuration.py +++ b/ymmsl/v0_2/tests/test_configuration.py @@ -3,7 +3,7 @@ import pytest -from ymmsl.io import load, dump +from ymmsl.io import load, load_as, dump from ymmsl.v0_2 import ( Configuration, Checkpoints, Component, Conduit, ExecutionModel, Identifier, ImportStatement, Model, Operator, Port, Ports, Program, Reference, Settings, @@ -456,7 +456,6 @@ def test_check_inconsistent_resources( with pytest.raises(RuntimeError) as e: config_inconsistent_resources.check_consistent() - # The missing resource for the non-MPI component is no longer an error. assert len(str(e.value).split('\n')) == 3 config_inconsistent_resources.check_consistent(False) @@ -464,13 +463,18 @@ def test_check_inconsistent_resources( def test_check_no_resources_for_non_mpi() -> None: """Non-MPI components without resources should pass check_consistent.""" - model = Model( - 'no_resources_test', None, 'description', None, - [Component('worker', Ports(), 'description', 'worker_prog')]) - - programs = [Program('worker_prog', script='worker', - execution_model=ExecutionModel.DIRECT)] - - # No resources specified - should be fine for non-MPI - config = Configuration('test', None, [model], None, None, programs, resources=None) + ymmsl_text = """ + ymmsl_version: v0.2 + models: + no_resources_test: + components: + worker: + ports: {} + description: description + implementation: worker_prog + programs: + worker_prog: + script: worker + """ + config = load_as(Configuration, ymmsl_text) config.check_consistent() From a9ed45ad4032e458007c8f91f22045376f226d61 Mon Sep 17 00:00:00 2001 From: Iris van der Werf Date: Fri, 8 May 2026 10:32:35 +0200 Subject: [PATCH 5/7] update config --- ymmsl/v0_2/tests/test_configuration.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/ymmsl/v0_2/tests/test_configuration.py b/ymmsl/v0_2/tests/test_configuration.py index a1ac76f..098f243 100644 --- a/ymmsl/v0_2/tests/test_configuration.py +++ b/ymmsl/v0_2/tests/test_configuration.py @@ -463,18 +463,18 @@ def test_check_inconsistent_resources( def test_check_no_resources_for_non_mpi() -> None: """Non-MPI components without resources should pass check_consistent.""" - ymmsl_text = """ - ymmsl_version: v0.2 - models: - no_resources_test: - components: - worker: - ports: {} - description: description - implementation: worker_prog - programs: - worker_prog: - script: worker - """ + ymmsl_text = ( + 'ymmsl_version: v0.2\n' + 'models:\n' + ' no_resources_test:\n' + ' components:\n' + ' worker:\n' + ' ports: {}\n' + ' description: description\n' + ' implementation: worker_prog\n' + 'programs:\n' + ' worker_prog:\n' + ' script: worker\n' + ) config = load_as(Configuration, ymmsl_text) config.check_consistent() From ae82ff4cffe127e244c0b64e8c64e7c415f326e1 Mon Sep 17 00:00:00 2001 From: Iris van der Werf Date: Fri, 8 May 2026 10:35:11 +0200 Subject: [PATCH 6/7] remove ExecutionModel input --- ymmsl/v0_2/tests/test_configuration.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ymmsl/v0_2/tests/test_configuration.py b/ymmsl/v0_2/tests/test_configuration.py index 098f243..993a969 100644 --- a/ymmsl/v0_2/tests/test_configuration.py +++ b/ymmsl/v0_2/tests/test_configuration.py @@ -5,7 +5,7 @@ from ymmsl.io import load, load_as, dump from ymmsl.v0_2 import ( - Configuration, Checkpoints, Component, Conduit, ExecutionModel, Identifier, + Configuration, Checkpoints, Component, Conduit, Identifier, ImportStatement, Model, Operator, Port, Ports, Program, Reference, Settings, SettingType, SettingValue, ThreadedResReq, Timeline) From 3359f42f5442a2f254d51dfb32ea98a2a43d54eb Mon Sep 17 00:00:00 2001 From: Iris van der Werf Date: Mon, 11 May 2026 09:14:31 +0200 Subject: [PATCH 7/7] Move get_resources to the configuration class --- ymmsl/v0_2/configuration.py | 23 ++++++++++++++++++++++- ymmsl/v0_2/tests/test_configuration.py | 24 ++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/ymmsl/v0_2/configuration.py b/ymmsl/v0_2/configuration.py index 1ae0591..59d6320 100644 --- a/ymmsl/v0_2/configuration.py +++ b/ymmsl/v0_2/configuration.py @@ -11,7 +11,8 @@ from ymmsl.v0_2.checkpoint import Checkpoints from ymmsl.v0_2.execution import ExecutionModel -from ymmsl.v0_2.resources import MPICoresResReq, MPINodesResReq, ResourceRequirements +from ymmsl.v0_2.resources import ( + MPICoresResReq, MPINodesResReq, ResourceRequirements, ThreadedResReq) from ymmsl.v0_2.identity import Identifier, Reference from ymmsl.v0_2.implementation import Implementation # noqa: F401 from ymmsl.v0_2.imports import ImportStatement @@ -227,6 +228,26 @@ def check_consistent(self, check_runnable: bool = True) -> None: 'The configuration is internally inconsistent. The following' ' problems were found:\n- ' + '\n- '.join(errors)) + + def get_resources(self, name: Reference) -> ResourceRequirements: + """Get the resource requirements for a component. + + If no resources are defined for this component and + it uses a non-MPI execution model (DIRECT), a default of 1 thread is + returned. + + Args: + name: The name of the component to get resources for. + + Returns: + The resource requirements for the component. + """ + res_req = self.resources.get(name) + if res_req is None: + _logger.debug( + f'No resources defined for {name}, using default of 1 thread.') + res_req = ThreadedResReq(name, 1) + return res_req def root_model(self, selected_model: Optional[Reference] = None) -> Model: """Return the root model of this configuration. diff --git a/ymmsl/v0_2/tests/test_configuration.py b/ymmsl/v0_2/tests/test_configuration.py index 993a969..9a0d002 100644 --- a/ymmsl/v0_2/tests/test_configuration.py +++ b/ymmsl/v0_2/tests/test_configuration.py @@ -461,6 +461,30 @@ def test_check_inconsistent_resources( config_inconsistent_resources.check_consistent(False) +def test_get_resources_defined() -> None: + """Test that get_resources returns the defined resource for a component.""" + resources = [ + ThreadedResReq(Ref('macro'), 10), + ThreadedResReq(Ref('micro'), 1)] + config = Configuration('test', resources=resources) + + res = config.get_resources(Ref('macro')) + assert isinstance(res, ThreadedResReq) + assert res.name == Ref('macro') + assert res.threads == 10 + + +def test_get_resources_default() -> None: + """Test that get_resources returns a default of 1 thread for a DIRECT component + with no resources defined.""" + config = Configuration('test') + + res = config.get_resources(Ref('micro')) + assert isinstance(res, ThreadedResReq) + assert res.name == Ref('micro') + assert res.threads == 1 + + def test_check_no_resources_for_non_mpi() -> None: """Non-MPI components without resources should pass check_consistent.""" ymmsl_text = (