Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 23 additions & 11 deletions ymmsl/v0_2/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,9 @@ def update(self, overlay: 'Configuration') -> None:
self.checkpoints.update(overlay.checkpoints)
self.resume.update(overlay.resume)

def check_consistent(self, check_runnable: bool = True) -> None:
def check_consistent(
self, check_runnable: bool = True, selected_model: Optional[str] = None
) -> None:
"""Checks that the configuration is internally consistent.

This checks:
Expand All @@ -204,6 +206,9 @@ def check_consistent(self, check_runnable: bool = True) -> None:
Args:
check_runnable: if False, skip the checks for whether component
implementations exist and whether resources have been requested.
selected_model: if set, gives the name of the root model that we intend to
run, and which must therefore have resources defined. Only used if
check_runnable is True.
"""
errors = list()

Expand All @@ -221,14 +226,14 @@ def check_consistent(self, check_runnable: bool = True) -> None:
errors.extend(self._check_custom_implementations(component_paths))
errors.extend(self._check_consistent_settings(component_paths))
if check_runnable:
errors.extend(self._check_resources(component_paths))
errors.extend(self._check_resources(component_paths, selected_model))

if errors:
raise RuntimeError(
'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.

Expand Down Expand Up @@ -325,7 +330,7 @@ def _component_paths(self) -> Dict[Reference, Component]:
"""
result = dict()
queue: List[Tuple[Model, Reference, List[Tuple[Reference, Reference]]]] = \
[(m, Reference([]), []) for m in self._root_models()]
[(m, m.name, []) for m in self._root_models()]
_logger.debug(f'cmp_paths: initial queue: {[t[0].name for t in queue]}')

while queue:
Expand Down Expand Up @@ -512,7 +517,7 @@ def _check_supported_setting(
dims = component.multiplicity

for index in itertools.product(*map(range, dims)):
instance_path = component_path + index
instance_path = component_path[1:] + index
for j in range(len(instance_path), -1, -1):
found_setting = instance_path[:j] + name
if found_setting in self.settings:
Expand All @@ -523,7 +528,8 @@ def _check_supported_setting(
val_str = f'"{val}"'
errors.append(
f'Instance "{instance_path}" of component'
f' "{component_path}" with implementation "{impl.name}"'
f' "{component_path[1:]}" with implementation'
f' "{impl.name}"'
f' has a supported setting "{name}" with type'
f' {typ.value}, but setting "{found_setting}" has value'
f' {val_str}, which does not match that type')
Expand Down Expand Up @@ -555,7 +561,8 @@ def _setting_type_matches(self, value: SettingValue, typ: SettingType) -> bool:
return False

def _check_resources(
self, component_paths: Dict[Reference, Component]) -> List[str]:
self, component_paths: Dict[Reference, Component],
selected_model: Optional[str]) -> List[str]:
"""Check that each component path has a corresponding resource request.

For non-MPI components, resources are optional: if not specified,
Expand All @@ -566,6 +573,9 @@ def _check_resources(
errors = list()
for path, component in component_paths.items():
_logger.debug(f'Checking resources for {path} {component.name}')
if selected_model is not None and path[0] != selected_model:
continue

impl_ref = self.custom_implementations.get(path, component.implementation)
_logger.debug(f'Implementation: {impl_ref}')
if impl_ref is None or impl_ref not in self.programs:
Expand All @@ -578,21 +588,23 @@ def _check_resources(

em_nompi = impl.execution_model is ExecutionModel.DIRECT

if path not in self.resources:
# can become just path again when we prefix resources with the model name
if path[1:] not in self.resources:
if em_mpi:
errors.append(f'Component "{path}" is missing a resource request')
else:
# also here
res_mpi = isinstance(
self.resources[path], (MPICoresResReq, MPINodesResReq))
self.resources[path[1:]], (MPICoresResReq, MPINodesResReq))

if em_mpi and not res_mpi:
errors.append(
f'Component "{path}" has implementation "{impl_ref}",'
f'Component "{path[1:]}" has implementation "{impl_ref}",'
' which has an MPI execution model, but the resources'
' requested for it do not ask for MPI processes.')
elif res_mpi and em_nompi:
errors.append(
f'Component "{path}" has implementation "{impl_ref}",'
f'Component "{path[1:]}" has implementation "{impl_ref}",'
' which has a non-MPI execution model, but the resources'
' requested for it ask for MPI processes.')

Expand Down
51 changes: 47 additions & 4 deletions ymmsl/v0_2/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -464,8 +464,8 @@ def config_consistent_custom_impls() -> Configuration:

return Configuration(
'testing consistency of custom implementations', None, [model1, model2], {
Reference('c1'): Reference('program1'),
Reference('c2.init_model'): Reference('initer2')},
Reference('test_model.c1'): Reference('program1'),
Reference('test_model.c2.init_model'): Reference('initer2')},
None, programs, resources)


Expand All @@ -488,8 +488,8 @@ def config_inconsistent_custom_impls() -> Configuration:

return Configuration(
'testing consistency of custom implementations', None, [model1, model2], {
Reference('cl'): Reference('program1'),
Reference('c2.init_model'): Reference('initer2')},
Reference('test_model.cl'): Reference('program1'),
Reference('test_model.c2.init_model'): Reference('initer2')},
None, None, resources)


Expand Down Expand Up @@ -619,6 +619,49 @@ def config_inconsistent_resources() -> Configuration:
'test_config', None, [model1, model2], None, None, programs, resources)


@pytest.fixture
def config_consistent_partial_resources() -> Configuration:
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I usually prefer to keep test data close to the tests where they're used (i.e. define this fixture in test_configuration.py), and only move it to a shared conftest when it's used in multiple test files.

I'm also fine to keep it like this though!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that's a good point. The whole test_configuration.py is set up like this though, so maybe better to make a separate PR for that, where we change everything in one go?

model1 = Model(
'resources_test',
None,
'description',
None,
[
Component('singlethreaded', Ports(), 'description', 'a'),
Component('multithreaded', Ports(), 'description', 'b'),
])

model2 = Model(
'resources_test2',
None,
'description',
None,
[
Component('mpi_cores1', Ports(), 'description', 'c'),
Component('mpi_cores2', Ports(), 'description', 'd'),
Component('mpi_nodes1', Ports(), 'description', 'c'),
Component('mpi_nodes2', Ports(), 'description', 'd')],
[])

em = {
'a': ExecutionModel.DIRECT,
'b': ExecutionModel.DIRECT,
'c': ExecutionModel.OPENMPI,
'd': ExecutionModel.OPENMPI}
programs = [Program(x, script='script', execution_model=em[x]) for x in 'abcd']

resources = [
ThreadedResReq(Reference('singlethreaded'), 1),
ThreadedResReq(Reference('multithreaded'), 4),
MPICoresResReq(Reference('mpi_cores1'), 16),
MPINodesResReq(Reference('mpi_nodes1'), 10, 16),
MPINodesResReq(Reference('mpi_nodes2'), 10, 4, 4)]

return Configuration(
'consistent_resources', None, [model1, model2], None, None, programs,
resources)


@pytest.fixture
def config_component_loop() -> Configuration:
main = Model(
Expand Down
10 changes: 10 additions & 0 deletions ymmsl/v0_2/tests/test_configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,16 @@ def test_check_inconsistent_resources(
config_inconsistent_resources.check_consistent(False)


def test_check_partial_resources(
config_consistent_partial_resources: Configuration) -> None:
config_consistent_partial_resources.check_consistent(True, 'resources_test')

with pytest.raises(RuntimeError) as e:
config_consistent_partial_resources.check_consistent(True, 'resources_test2')

assert len(str(e.value).split('\n')) == 2


def test_get_resources_defined() -> None:
"""Test that get_resources returns the defined resource for a component."""
resources = [
Expand Down
Loading