diff --git a/smartsim/_core/control/manifest.py b/smartsim/_core/control/manifest.py index cb47af14ea..89b80c2178 100644 --- a/smartsim/_core/control/manifest.py +++ b/smartsim/_core/control/manifest.py @@ -31,8 +31,9 @@ from smartsim.entity._mock import Mock +from ...builders import Ensemble from ...database import FeatureStore -from ...entity import Application, Ensemble, FSNode, SmartSimEntity +from ...entity import Application, FSNode, SmartSimEntity from ...error import SmartSimError from ..config import CONFIG from ..utils import helpers as _helpers diff --git a/smartsim/_core/launcher/step/slurm_step.py b/smartsim/_core/launcher/step/slurm_step.py index 3f178d9745..2a9046a3ae 100644 --- a/smartsim/_core/launcher/step/slurm_step.py +++ b/smartsim/_core/launcher/step/slurm_step.py @@ -29,7 +29,8 @@ import typing as t from shlex import split as sh_split -from ....entity import Application, Ensemble, FSNode +from ....builders import Ensemble +from ....entity import Application, FSNode from ....error import AllocationError from ....log import get_logger from ....settings import RunSettings, SbatchSettings, Singularity, SrunSettings diff --git a/smartsim/_core/launcher/step/step.py b/smartsim/_core/launcher/step/step.py index 46bcebf7fa..b5e79a3638 100644 --- a/smartsim/_core/launcher/step/step.py +++ b/smartsim/_core/launcher/step/step.py @@ -38,7 +38,8 @@ from smartsim._core.config import CONFIG from smartsim.error.errors import SmartSimError, UnproxyableStepError -from ....entity import Application, Ensemble, FSNode +from ....builders import Ensemble +from ....entity import Application, FSNode from ....log import get_logger from ....settings import RunSettings, SettingsBase from ...utils.helpers import encode_cmd, get_base_36_repr diff --git a/smartsim/_core/utils/serialize.py b/smartsim/_core/utils/serialize.py index aad38c7787..46c0a2c1da 100644 --- a/smartsim/_core/utils/serialize.py +++ b/smartsim/_core/utils/serialize.py @@ -36,8 +36,9 @@ if t.TYPE_CHECKING: from smartsim._core.control.manifest import LaunchedManifest as _Manifest + from smartsim.builders import Ensemble from smartsim.database.orchestrator import FeatureStore - from smartsim.entity import Application, Ensemble, FSNode + from smartsim.entity import Application, FSNode from smartsim.entity.dbobject import FSModel, FSScript from smartsim.settings.base import BatchSettings, RunSettings diff --git a/smartsim/builders/__init__.py b/smartsim/builders/__init__.py new file mode 100644 index 0000000000..866269f201 --- /dev/null +++ b/smartsim/builders/__init__.py @@ -0,0 +1,28 @@ +# BSD 2-Clause License +# +# Copyright (c) 2021-2024, Hewlett Packard Enterprise +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +from .ensemble import Ensemble +from .utils.strategies import ParamSet diff --git a/smartsim/entity/ensemble.py b/smartsim/builders/ensemble.py similarity index 76% rename from smartsim/entity/ensemble.py rename to smartsim/builders/ensemble.py index 191730df76..c4a57175f5 100644 --- a/smartsim/entity/ensemble.py +++ b/smartsim/builders/ensemble.py @@ -32,10 +32,11 @@ import os.path import typing as t -from smartsim.entity import entity, strategies +from smartsim.builders.utils import strategies +from smartsim.builders.utils.strategies import ParamSet +from smartsim.entity import entity from smartsim.entity.application import Application from smartsim.entity.files import EntityFiles -from smartsim.entity.strategies import ParamSet from smartsim.launchable.job import Job if t.TYPE_CHECKING: @@ -43,8 +44,8 @@ class Ensemble(entity.CompoundEntity): - """Entity to help parameterize the creation multiple application - instances. + """An Ensemble is a builder class that parameterizes the creation of multiple + Applications. """ def __init__( @@ -59,7 +60,60 @@ def __init__( max_permutations: int = -1, replicas: int = 1, ) -> None: - """Initialize an ``Ensemble`` of application instances + """Initialize an ``Ensemble`` of Application instances + + An Ensemble can be tailored to align with one of the following + creation strategies: parameter expansion or replicas. + + **Parameter Expansion** + + Parameter expansion allows users to assign different parameter values to + multiple Applications. This is done by specifying input to `Ensemble.file_parameters`, + `Ensemble.exe_arg_parameters` and `Ensemble.permutation_strategy`. The `permutation_strategy` + argument accepts three options: + + 1. "all_perm": Generates all possible parameter permutations for exhaustive exploration. + 2. "step": Collects identically indexed values across parameter lists to create parameter sets. + 3. "random": Enables random selection from predefined parameter spaces. + + The example below demonstrates creating an Ensemble via parameter expansion, resulting in + the creation of two Applications: + + .. highlight:: python + .. code-block:: python + + file_params={"SPAM": ["a", "b"], "EGGS": ["c", "d"]} + exe_arg_parameters = {"EXE": [["a"], ["b", "c"]], "ARGS": [["d"], ["e", "f"]]} + ensemble = Ensemble(name="name",exe="python",exe_arg_parameters=exe_arg_parameters, + file_parameters=file_params,permutation_strategy="step") + + This configuration will yield the following permutations: + + .. highlight:: python + .. code-block:: python + [ParamSet(params={'SPAM': 'a', 'EGGS': 'c'}, exe_args={'EXE': ['a'], 'ARGS': ['d']}), + ParamSet(params={'SPAM': 'b', 'EGGS': 'd'}, exe_args={'EXE': ['b', 'c'], 'ARGS': ['e', 'f']})] + + Each ParamSet contains the parameters assigned from file_params and the corresponding executable + arguments from exe_arg_parameters. + + **Replication** + The replication strategy involves creating identical Applications within an Ensemble. + This is achieved by specifying the `replicas` argument in the Ensemble. + + For example, by applying the `replicas` argument to the previous parameter expansion + example, we can double our Application output: + + .. highlight:: python + .. code-block:: python + + file_params={"SPAM": ["a", "b"], "EGGS": ["c", "d"]} + exe_arg_parameters = {"EXE": [["a"], ["b", "c"]], "ARGS": [["d"], ["e", "f"]]} + ensemble = Ensemble(name="name",exe="python",exe_arg_parameters=exe_arg_parameters, + file_parameters=file_params,permutation_strategy="step", replicas=2) + + This configuration will result in each ParamSet being replicated, effectively doubling + the number of Applications created. :param name: name of the ensemble :param exe: executable to run @@ -259,7 +313,7 @@ def _create_applications(self) -> tuple[Application, ...]: for i, permutation in enumerate(permutations_) ) - def as_jobs(self, settings: LaunchSettings) -> tuple[Job, ...]: + def build_jobs(self, settings: LaunchSettings) -> tuple[Job, ...]: """Expand an Ensemble into a list of deployable Jobs and apply identical LaunchSettings to each Job. @@ -281,9 +335,9 @@ def as_jobs(self, settings: LaunchSettings) -> tuple[Job, ...]: # Initialize the Ensemble ensemble = Ensemble("my_name", "echo", "hello world", replicas=3) # Expand Ensemble into Jobs - ensemble_as_jobs = ensemble.as_jobs(my_launch_settings) + ensemble_as_jobs = ensemble.build_jobs(my_launch_settings) - By calling `as_jobs` on `ensemble`, three Jobs are returned because + By calling `build_jobs` on `ensemble`, three Jobs are returned because three replicas were specified. Each Job will have the provided LaunchSettings. :param settings: LaunchSettings to apply to each Job diff --git a/smartsim/entity/strategies.py b/smartsim/builders/utils/strategies.py similarity index 100% rename from smartsim/entity/strategies.py rename to smartsim/builders/utils/strategies.py diff --git a/smartsim/entity/__init__.py b/smartsim/entity/__init__.py index 2f75e8ecd1..a12d737bb3 100644 --- a/smartsim/entity/__init__.py +++ b/smartsim/entity/__init__.py @@ -27,6 +27,5 @@ from .application import Application from .dbnode import FSNode from .dbobject import * -from .ensemble import Ensemble from .entity import SmartSimEntity, TelemetryConfiguration from .files import TaggedFilesHierarchy diff --git a/smartsim/entity/entity.py b/smartsim/entity/entity.py index f3e5b17f3a..3f5a9eabd0 100644 --- a/smartsim/entity/entity.py +++ b/smartsim/entity/entity.py @@ -135,6 +135,6 @@ class CompoundEntity(abc.ABC): """ @abc.abstractmethod - def as_jobs(self, settings: LaunchSettings) -> t.Collection[Job]: ... + def build_jobs(self, settings: LaunchSettings) -> t.Collection[Job]: ... def as_job_group(self, settings: LaunchSettings) -> JobGroup: - return JobGroup(list(self.as_jobs(settings))) + return JobGroup(list(self.build_jobs(settings))) diff --git a/tests/_legacy/test_controller.py b/tests/_legacy/test_controller.py index 19325c9334..ad0c98fe88 100644 --- a/tests/_legacy/test_controller.py +++ b/tests/_legacy/test_controller.py @@ -30,8 +30,8 @@ from smartsim._core.control.controller import Controller from smartsim._core.launcher.step import Step +from smartsim.builders.ensemble import Ensemble from smartsim.database.orchestrator import FeatureStore -from smartsim.entity.ensemble import Ensemble from smartsim.settings.slurmSettings import SbatchSettings, SrunSettings controller = Controller() diff --git a/tests/_legacy/test_controller_errors.py b/tests/_legacy/test_controller_errors.py index 4814ce4950..5ae05d70ad 100644 --- a/tests/_legacy/test_controller_errors.py +++ b/tests/_legacy/test_controller_errors.py @@ -30,9 +30,9 @@ from smartsim._core.control import Controller, Manifest from smartsim._core.launcher.step import Step from smartsim._core.launcher.step.dragon_step import DragonStep +from smartsim.builders.ensemble import Ensemble from smartsim.database import FeatureStore from smartsim.entity import Application -from smartsim.entity.ensemble import Ensemble from smartsim.error import SmartSimError, SSUnsupportedError from smartsim.error.errors import SSUnsupportedError from smartsim.settings import RunSettings, SrunSettings diff --git a/tests/_legacy/test_ensemble.py b/tests/_legacy/test_ensemble.py index 86146c8e47..62c7d8d4f7 100644 --- a/tests/_legacy/test_ensemble.py +++ b/tests/_legacy/test_ensemble.py @@ -30,7 +30,8 @@ import pytest from smartsim import Experiment -from smartsim.entity import Application, Ensemble +from smartsim.builders import Ensemble +from smartsim.entity import Application from smartsim.error import EntityExistsError, SSUnsupportedError, UserStrategyError from smartsim.settings import RunSettings diff --git a/tests/_legacy/test_model.py b/tests/_legacy/test_model.py index f32a27a072..5adf8070f1 100644 --- a/tests/_legacy/test_model.py +++ b/tests/_legacy/test_model.py @@ -31,7 +31,8 @@ from smartsim import Experiment from smartsim._core.control.manifest import LaunchedManifestBuilder from smartsim._core.launcher.step import SbatchStep, SrunStep -from smartsim.entity import Application, Ensemble +from smartsim.builders import Ensemble +from smartsim.entity import Application from smartsim.error import EntityExistsError, SSUnsupportedError from smartsim.settings import RunSettings, SbatchSettings, SrunSettings from smartsim.settings.mpiSettings import _BaseMPISettings diff --git a/tests/_legacy/test_output_files.py b/tests/_legacy/test_output_files.py index 713001feb4..55ecfd90a5 100644 --- a/tests/_legacy/test_output_files.py +++ b/tests/_legacy/test_output_files.py @@ -33,9 +33,9 @@ from smartsim._core.config import CONFIG from smartsim._core.control.controller import Controller, _AnonymousBatchJob from smartsim._core.launcher.step import Step +from smartsim.builders.ensemble import Ensemble from smartsim.database.orchestrator import FeatureStore from smartsim.entity.application import Application -from smartsim.entity.ensemble import Ensemble from smartsim.settings.base import RunSettings from smartsim.settings.slurmSettings import SbatchSettings, SrunSettings diff --git a/tests/_legacy/test_smartredis.py b/tests/_legacy/test_smartredis.py index ca8d1e0fae..f09cc8ca89 100644 --- a/tests/_legacy/test_smartredis.py +++ b/tests/_legacy/test_smartredis.py @@ -29,8 +29,9 @@ from smartsim import Experiment from smartsim._core.utils import installed_redisai_backends +from smartsim.builders import Ensemble from smartsim.database import FeatureStore -from smartsim.entity import Application, Ensemble +from smartsim.entity import Application from smartsim.status import JobStatus # The tests in this file belong to the group_b group diff --git a/tests/_legacy/test_symlinking.py b/tests/_legacy/test_symlinking.py index 4447a49d1c..95aa187e6b 100644 --- a/tests/_legacy/test_symlinking.py +++ b/tests/_legacy/test_symlinking.py @@ -32,9 +32,9 @@ from smartsim import Experiment from smartsim._core.config import CONFIG from smartsim._core.control.controller import Controller, _AnonymousBatchJob +from smartsim.builders.ensemble import Ensemble from smartsim.database.orchestrator import FeatureStore from smartsim.entity.application import Application -from smartsim.entity.ensemble import Ensemble from smartsim.settings.base import RunSettings from smartsim.settings.slurmSettings import SbatchSettings, SrunSettings diff --git a/tests/test_ensemble.py b/tests/test_ensemble.py index 3f0840711c..9c90152514 100644 --- a/tests/test_ensemble.py +++ b/tests/test_ensemble.py @@ -31,9 +31,9 @@ import pytest -from smartsim.entity.ensemble import Ensemble +from smartsim.builders.ensemble import Ensemble +from smartsim.builders.utils.strategies import ParamSet from smartsim.entity.files import EntityFiles -from smartsim.entity.strategies import ParamSet from smartsim.settings.launch_settings import LaunchSettings pytestmark = pytest.mark.group_a @@ -109,7 +109,7 @@ def test_ensemble_user_created_strategy(mock_launcher_settings, test_dir): "echo", ("hello", "world"), permutation_strategy=user_created_function, - ).as_jobs(mock_launcher_settings) + ).build_jobs(mock_launcher_settings) assert len(jobs) == 1 @@ -125,7 +125,7 @@ def test_ensemble_without_any_members_raises_when_cast_to_jobs( permutation_strategy="random", max_permutations=30, replicas=0, - ).as_jobs(mock_launcher_settings) + ).build_jobs(mock_launcher_settings) def test_strategy_error_raised_if_a_strategy_that_dne_is_requested(test_dir): @@ -208,7 +208,7 @@ def test_all_perm_strategy( permutation_strategy="all_perm", max_permutations=max_perms, replicas=replicas, - ).as_jobs(mock_launcher_settings) + ).build_jobs(mock_launcher_settings) assert len(jobs) == expected_num_jobs @@ -222,7 +222,7 @@ def test_all_perm_strategy_contents(): permutation_strategy="all_perm", max_permutations=16, replicas=1, - ).as_jobs(mock_launcher_settings) + ).build_jobs(mock_launcher_settings) assert len(jobs) == 16 @@ -262,7 +262,7 @@ def test_step_strategy( permutation_strategy="step", max_permutations=max_perms, replicas=replicas, - ).as_jobs(mock_launcher_settings) + ).build_jobs(mock_launcher_settings) assert len(jobs) == expected_num_jobs @@ -301,5 +301,5 @@ def test_random_strategy( permutation_strategy="random", max_permutations=max_perms, replicas=replicas, - ).as_jobs(mock_launcher_settings) + ).build_jobs(mock_launcher_settings) assert len(jobs) == expected_num_jobs diff --git a/tests/test_generator.py b/tests/test_generator.py index ff24018ca7..8f5a02f0b6 100644 --- a/tests/test_generator.py +++ b/tests/test_generator.py @@ -11,7 +11,8 @@ from smartsim import Experiment from smartsim._core.generation.generator import Generator -from smartsim.entity import Application, Ensemble +from smartsim.builders import Ensemble +from smartsim.entity import Application from smartsim.entity.files import EntityFiles from smartsim.launchable import Job from smartsim.settings import LaunchSettings @@ -226,7 +227,7 @@ def test_exp_private_generate_method_ensemble(test_dir, wlmutils, generator_inst """Test that Job directory was created from Experiment.""" ensemble = Ensemble("ensemble-name", "echo", replicas=2) launch_settings = LaunchSettings(wlmutils.get_test_launcher()) - job_list = ensemble.as_jobs(launch_settings) + job_list = ensemble.build_jobs(launch_settings) exp = Experiment(name="exp_name", exp_path=test_dir) for i, job in enumerate(job_list): job_run_path, _, _ = exp._generate(generator_instance, job, i) @@ -239,7 +240,7 @@ def test_exp_private_generate_method_ensemble(test_dir, wlmutils, generator_inst def test_generate_ensemble_directory(wlmutils, generator_instance): ensemble = Ensemble("ensemble-name", "echo", replicas=2) launch_settings = LaunchSettings(wlmutils.get_test_launcher()) - job_list = ensemble.as_jobs(launch_settings) + job_list = ensemble.build_jobs(launch_settings) for i, job in enumerate(job_list): # Call Generator.generate_job path, _, _ = generator_instance.generate_job(job, i) @@ -263,7 +264,7 @@ def test_generate_ensemble_directory_start(test_dir, wlmutils, monkeypatch): ) ensemble = Ensemble("ensemble-name", "echo", replicas=2) launch_settings = LaunchSettings(wlmutils.get_test_launcher()) - job_list = ensemble.as_jobs(launch_settings) + job_list = ensemble.build_jobs(launch_settings) exp = Experiment(name="exp_name", exp_path=test_dir) exp.start(*job_list) run_dir = listdir(test_dir) @@ -285,7 +286,7 @@ def test_generate_ensemble_copy(test_dir, wlmutils, monkeypatch, get_gen_copy_di "ensemble-name", "echo", replicas=2, files=EntityFiles(copy=get_gen_copy_dir) ) launch_settings = LaunchSettings(wlmutils.get_test_launcher()) - job_list = ensemble.as_jobs(launch_settings) + job_list = ensemble.build_jobs(launch_settings) exp = Experiment(name="exp_name", exp_path=test_dir) exp.start(*job_list) run_dir = listdir(test_dir) @@ -310,7 +311,7 @@ def test_generate_ensemble_symlink( files=EntityFiles(symlink=get_gen_symlink_dir), ) launch_settings = LaunchSettings(wlmutils.get_test_launcher()) - job_list = ensemble.as_jobs(launch_settings) + job_list = ensemble.build_jobs(launch_settings) exp = Experiment(name="exp_name", exp_path=test_dir) exp.start(*job_list) run_dir = listdir(test_dir) @@ -341,7 +342,7 @@ def test_generate_ensemble_configure( file_parameters=params, ) launch_settings = LaunchSettings(wlmutils.get_test_launcher()) - job_list = ensemble.as_jobs(launch_settings) + job_list = ensemble.build_jobs(launch_settings) exp = Experiment(name="exp_name", exp_path=test_dir) id = exp.start(*job_list) run_dir = listdir(test_dir) diff --git a/tests/test_permutation_strategies.py b/tests/test_permutation_strategies.py index b14514c99b..314c21063b 100644 --- a/tests/test_permutation_strategies.py +++ b/tests/test_permutation_strategies.py @@ -28,8 +28,8 @@ import pytest -from smartsim.entity import strategies -from smartsim.entity.strategies import ParamSet +from smartsim.builders.utils import strategies +from smartsim.builders.utils.strategies import ParamSet from smartsim.error import errors pytestmark = pytest.mark.group_a