diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml index 09b680374..fc817cba9 100644 --- a/.github/workflows/run_tests.yml +++ b/.github/workflows/run_tests.yml @@ -44,23 +44,17 @@ env: jobs: run_tests: - name: Run tests with ${{ matrix.os }}, Python ${{ matrix.py_v}}, RedisAI ${{ matrix.rai }} + name: Run tests ${{ matrix.subset }} with ${{ matrix.os }}, Python ${{ matrix.py_v}}, RedisAI ${{ matrix.rai }} runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: + subset: [backends, slow_tests, group_a, group_b] os: [macos-12, ubuntu-20.04] # Operating systems compiler: [8] # GNU compiler version - rai: [1.2.5, 1.2.7] # Redis AI versions + rai: [1.2.7] # Redis AI versions py_v: [3.8, 3.9, '3.10'] # Python versions - exclude: - # Do not build with Redis AI 1.2.5 on MacOS - - os: macos-12 - rai: 1.2.5 - # Do not build Redis AI 1.2.5 with py3.10 - # as wheels for dependecies are not availble - - py_v: '3.10' - rai: 1.2.5 + env: SMARTSIM_REDISAI: ${{ matrix.rai }} @@ -86,7 +80,7 @@ jobs: echo "$(brew --prefix)/opt/make/libexec/gnubin" >> $GITHUB_PATH - name: Build Singularity from source - if: contains( matrix.os, 'ubuntu' ) && matrix.py_v == 3.9 && matrix.rai == '1.2.5' + if: contains( matrix.os, 'ubuntu' ) && matrix.py_v == 3.9 run: | sudo apt-get install -y libseccomp-dev pkg-config squashfs-tools cryptsetup curl git # wget build-essential echo 'export PATH=/usr/local/go/bin:$PATH' >> ~/.bashrc @@ -100,16 +94,18 @@ jobs: sudo make -C builddir install - name: singularity pull test container # This lets us time how long the pull takes - if: contains( matrix.os, 'ubuntu' ) && matrix.py_v == 3.9 && matrix.rai == '1.2.5' + if: contains( matrix.os, 'ubuntu' ) && matrix.py_v == 3.9 run: singularity pull docker://alrigazzi/smartsim-testing # Note: The develop branch of smartredis is installed first to ensure that any tests that depend # on developments of the client are brought in. - name: Install SmartSim (with ML backends) run: | + python -m pip install git+https://github.com/CrayLabs/SmartRedis.git@develop#egg=smartredis python -m pip install .[dev,ml] + - name: Install ML Runtimes with Smart (with pt, tf, and onnx support) if: (matrix.py_v != '3.10') run: smart build --device cpu --onnx -v @@ -119,8 +115,6 @@ jobs: run: smart build --device cpu -v - name: Run mypy - # TF 2.6.2 has a dep conflict with new mypy versions - if: (matrix.rai != '1.2.5') run: | python -m pip install .[mypy] make check-mypy @@ -128,13 +122,36 @@ jobs: - name: Run Pylint run: make check-lint + # Run pytest (backends subdirectory) - name: Run Pytest + if: (matrix.subset == 'backends') run: | echo "SMARTSIM_LOG_LEVEL=debug" >> $GITHUB_ENV - py.test -s --import-mode=importlib -o log_cli=true --cov=$(smart site) --cov-report=xml --cov-config=./tests/test_configs/cov/local_cov.cfg --ignore=tests/full_wlm/ ./tests/ + py.test -s --import-mode=importlib -o log_cli=true --cov=$(smart site) --cov-report=xml --cov-config=./tests/test_configs/cov/local_cov.cfg --ignore=tests/full_wlm/ ./tests/backends + + # Run pytest (test subsets) + - name: Run Pytest + if: "!contains(matrix.subset, 'backends')" # if not running backend tests + run: | + echo "SMARTSIM_LOG_LEVEL=debug" >> $GITHUB_ENV + py.test -s --import-mode=importlib -o log_cli=true --cov=$(smart site) --cov-report=xml --cov-config=./tests/test_configs/cov/local_cov.cfg --ignore=tests/full_wlm/ -m ${{ matrix.subset }} ./tests + + # Upload artifacts on failure, ignoring binary files + - name: Upload Artifact + if: failure() + uses: actions/upload-artifact@v3 + with: + name: test_artifact + path: | + tests/test_output + !**/*.so + !**/*.pb + !**/*.pt + !**/core + retention-days: 5 - name: Upload Pytest coverage to Codecov uses: codecov/codecov-action@v2 with: - fail_ci_if_error: true + fail_ci_if_error: false files: ./coverage.xml diff --git a/.gitignore b/.gitignore index d67b476fe..428e439b3 100644 --- a/.gitignore +++ b/.gitignore @@ -7,7 +7,7 @@ __pycache__ htmlcov smartsim.egg-info tests/test_output -docs/* +.DS_Store # Dependencies smartsim/_core/.third-party @@ -31,3 +31,8 @@ smartsim/_core/bin/*-cli # created upon install smartsim/_core/lib + +**/manifest/ +**/*.err +**/*.out +**/.smartsim/* diff --git a/.pylintrc b/.pylintrc index 9ac79811c..da0886ba2 100644 --- a/.pylintrc +++ b/.pylintrc @@ -325,7 +325,7 @@ valid-metaclass-classmethod-first-arg=mcs max-args=9 # Maximum number of locals for function / method body -max-locals=19 +max-locals=20 # Maximum number of return / yield for function / method body max-returns=11 diff --git a/.wci.yml b/.wci.yml index 1c7ef5d72..55b5ddda1 100644 --- a/.wci.yml +++ b/.wci.yml @@ -22,8 +22,8 @@ language: Python release: - version: 0.5.1 - date: 2023-09-13 + version: 0.6.0 + date: 2023-12-18 documentation: general: https://www.craylabs.org/docs/overview.html diff --git a/MANIFEST.in b/MANIFEST.in index 885a8dbb4..20ba2dbe5 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,4 @@ include Makefile graft smartsim/ - +global-exclude __pycache__ +global-exclude *.py[co] diff --git a/Makefile b/Makefile index 7b60f8874..fef69eab3 100644 --- a/Makefile +++ b/Makefile @@ -146,11 +146,11 @@ tutorials-dev: @docker compose build tutorials-dev @docker run -p 8888:8888 smartsim-tutorials:dev-latest -# help: tutorials-prod - Build and start a docker container to run the tutorials (v0.4.2) +# help: tutorials-prod - Build and start a docker container to run the tutorials (v0.6.0) .PHONY: tutorials-prod tutorials-prod: @docker compose build tutorials-prod - @docker run -p 8888:8888 smartsim-tutorials:v0.4.2 + @docker run -p 8888:8888 smartsim-tutorials:v0.6.0 # help: diff --git a/README.md b/README.md index 9e1902784..df671ef02 100644 --- a/README.md +++ b/README.md @@ -6,10 +6,11 @@
Home    - Install    + Install    Documentation    - Slack Invite    Cray Labs    + Contact    + Join us on Slack!   


@@ -649,18 +650,6 @@ from C, C++, Fortran and Python with the SmartRedis Clients: ONNX 1.11.x - 1.2.5 - PyTorch - 1.9.x - - - TensorFlow\Keras - 2.6.x - - - ONNX - 1.9.x - diff --git a/conftest.py b/conftest.py index f20f48766..ff4e56ee1 100644 --- a/conftest.py +++ b/conftest.py @@ -28,7 +28,6 @@ import json import os -import inspect import pytest import psutil import shutil @@ -41,31 +40,37 @@ AprunSettings, JsrunSettings, MpirunSettings, + MpiexecSettings, + PalsMpiexecSettings, RunSettings, ) from smartsim._core.config import CONFIG from smartsim.error import SSConfigError from subprocess import run import sys +import tempfile import typing as t +import uuid +import warnings # pylint: disable=redefined-outer-name,invalid-name,global-statement # Globals, yes, but its a testing file test_path = os.path.dirname(os.path.abspath(__file__)) -test_dir = os.path.join(test_path, "tests", "test_output") +test_output_root = os.path.join(test_path, "tests", "test_output") test_launcher = CONFIG.test_launcher -test_device = CONFIG.test_device +test_device = CONFIG.test_device.upper() test_num_gpus = CONFIG.test_num_gpus test_nic = CONFIG.test_interface test_alloc_specs_path = os.getenv("SMARTSIM_TEST_ALLOC_SPEC_SHEET_PATH", None) test_port = CONFIG.test_port test_account = CONFIG.test_account or "" +test_batch_resources: t.Dict[t.Any,t.Any] = CONFIG.test_batch_resources # Fill this at runtime if needed test_hostlist = None - +has_aprun = shutil.which("aprun") is not None def get_account() -> str: return test_account @@ -77,14 +82,21 @@ def print_test_configuration() -> None: print("TEST_LAUNCHER:", test_launcher) if test_account != "": print("TEST_ACCOUNT:", test_account) - print("TEST_DEVICE:", test_device) + test_device_msg = f"TEST_DEVICE: {test_device}" + if test_device == "GPU": + test_device_msg += f"x{test_num_gpus}" + print(test_device_msg) print("TEST_NETWORK_INTERFACE (WLM only):", test_nic) if test_alloc_specs_path: print("TEST_ALLOC_SPEC_SHEET_PATH:", test_alloc_specs_path) - print("TEST_DIR:", test_dir) + print("TEST_DIR:", test_output_root) print("Test output will be located in TEST_DIR if there is a failure") - print("TEST_PORT", test_port) - print("TEST_PORT + 1", test_port + 1) + print( + "TEST_PORTS:", ", ".join(str(port) for port in range(test_port, test_port + 3)) + ) + if test_batch_resources: + print("TEST_BATCH_RESOURCES: ") + print(json.dumps(test_batch_resources, indent=2)) def pytest_configure() -> None: @@ -92,6 +104,8 @@ def pytest_configure() -> None: pytest.wlm_options = ["slurm", "pbs", "cobalt", "lsf", "pals"] account = get_account() pytest.test_account = account + pytest.test_device = test_device + pytest.has_aprun = has_aprun def pytest_sessionstart( @@ -101,9 +115,9 @@ def pytest_sessionstart( Called after the Session object has been created and before performing collection and entering the run test loop. """ - if os.path.isdir(test_dir): - shutil.rmtree(test_dir) - os.makedirs(test_dir) + if os.path.isdir(test_output_root): + shutil.rmtree(test_output_root) + os.makedirs(test_output_root) print_test_configuration() @@ -115,7 +129,7 @@ def pytest_sessionfinish( returning the exit status to the system. """ if exitstatus == 0: - shutil.rmtree(test_dir) + shutil.rmtree(test_output_root) else: # kill all spawned processes in case of error kill_all_test_spawned_processes() @@ -144,6 +158,12 @@ def get_hostlist() -> t.Optional[t.List[str]]: return _parse_hostlist_file(os.environ["COBALT_NODEFILE"]) except FileNotFoundError: return None + elif "PBS_NODEFILE" in os.environ and test_launcher == "pals": + # with PALS, we need a hostfile even if `aprun` is available + try: + return _parse_hostlist_file(os.environ["PBS_NODEFILE"]) + except FileNotFoundError: + return None elif "PBS_NODEFILE" in os.environ and not shutil.which("aprun"): try: return _parse_hostlist_file(os.environ["PBS_NODEFILE"]) @@ -216,6 +236,10 @@ def get_test_interface() -> t.List[str]: def get_test_hostlist() -> t.Optional[t.List[str]]: return get_hostlist() + @staticmethod + def get_batch_resources() -> t.Dict: + return test_batch_resources + @staticmethod def get_base_run_settings( exe: str, args: t.List[str], nodes: int = 1, ntasks: int = 1, **kwargs: t.Any @@ -320,7 +344,7 @@ def get_run_settings( @staticmethod def get_orchestrator(nodes: int = 1, batch: bool = False) -> Orchestrator: - if test_launcher in ["pbs", "cobalt", "pals"]: + if test_launcher in ["pbs", "cobalt"]: if not shutil.which("aprun"): hostlist = get_hostlist() else: @@ -333,6 +357,16 @@ def get_orchestrator(nodes: int = 1, batch: bool = False) -> Orchestrator: launcher=test_launcher, hosts=hostlist, ) + if test_launcher == "pals": + hostlist = get_hostlist() + return Orchestrator( + db_nodes=nodes, + port=test_port, + batch=batch, + interface=test_nic, + launcher=test_launcher, + hosts=hostlist, + ) if test_launcher == "slurm": return Orchestrator( db_nodes=nodes, @@ -355,18 +389,23 @@ def get_orchestrator(nodes: int = 1, batch: bool = False) -> Orchestrator: return Orchestrator(port=test_port, interface="lo") + @staticmethod + def choose_host(rs: RunSettings) -> t.Optional[str]: + if isinstance(rs, (MpirunSettings, MpiexecSettings)): + hl = get_hostlist() + if hl is not None: + return hl[0] + + return None @pytest.fixture def local_db( - fileutils: FileUtils, request: t.Any, wlmutils: t.Type[WLMUtils] + request: t.Any, wlmutils: t.Type[WLMUtils], test_dir: str ) -> t.Generator[Orchestrator, None, None]: """Yield fixture for startup and teardown of an local orchestrator""" exp_name = request.function.__name__ - exp = Experiment(exp_name, launcher="local") - test_dir = fileutils.make_test_dir( - caller_function=exp_name, caller_fspath=request.fspath - ) + exp = Experiment(exp_name, launcher="local", exp_path=test_dir) db = Orchestrator(port=wlmutils.get_test_port(), interface="lo") db.set_path(test_dir) exp.start(db) @@ -379,16 +418,13 @@ def local_db( @pytest.fixture def db( - fileutils: t.Type[FileUtils], wlmutils: t.Type[WLMUtils], request: t.Any + request: t.Any, wlmutils: t.Type[WLMUtils], test_dir: str ) -> t.Generator[Orchestrator, None, None]: """Yield fixture for startup and teardown of an orchestrator""" launcher = wlmutils.get_test_launcher() exp_name = request.function.__name__ - exp = Experiment(exp_name, launcher=launcher) - test_dir = fileutils.make_test_dir( - caller_function=exp_name, caller_fspath=request.fspath - ) + exp = Experiment(exp_name, launcher=launcher, exp_path=test_dir) db = wlmutils.get_orchestrator() db.set_path(test_dir) exp.start(db) @@ -401,7 +437,7 @@ def db( @pytest.fixture def db_cluster( - fileutils: t.Type[FileUtils], wlmutils: t.Type[WLMUtils], request: t.Any + test_dir: str, wlmutils: t.Type[WLMUtils], request: t.Any ) -> t.Generator[Orchestrator, None, None]: """ Yield fixture for startup and teardown of a clustered orchestrator. @@ -410,10 +446,7 @@ def db_cluster( launcher = wlmutils.get_test_launcher() exp_name = request.function.__name__ - exp = Experiment(exp_name, launcher=launcher) - test_dir = fileutils.make_test_dir( - caller_function=exp_name, caller_fspath=request.fspath - ) + exp = Experiment(exp_name, launcher=launcher, exp_path=test_dir) db = wlmutils.get_orchestrator(nodes=3) db.set_path(test_dir) exp.start(db) @@ -426,7 +459,9 @@ def db_cluster( @pytest.fixture(scope="function", autouse=True) def environment_cleanup(monkeypatch: pytest.MonkeyPatch) -> None: - monkeypatch.delenv("SSDB", raising=False) + for key in os.environ.keys(): + if key.startswith("SSDB"): + monkeypatch.delenv(key, raising=False) monkeypatch.delenv("SSKEYIN", raising=False) monkeypatch.delenv("SSKEYOUT", raising=False) @@ -513,6 +548,38 @@ def get_config_edit_method( return config_edit_methods.get(config_setting, None) +def _sanitize_caller_function(caller_function: str) -> str: + # Parametrized test functions end with a list of all + # parameter values. The list is enclosed in square brackets. + # We split at the opening bracket, sanitize the string + # to its right and then merge the function name and + # the sanitized list with a dot. + caller_function = caller_function.replace("]","") + caller_function_list = caller_function.split("[", maxsplit=1) + + def is_accepted_char(char: str) -> bool: + return char.isalnum() or char in "-._" + + if len(caller_function_list) > 1: + caller_function_list[1] = "".join( + filter(is_accepted_char, caller_function_list[1]) + ) + + return ".".join(caller_function_list) + + +@pytest.fixture +def test_dir(request: pytest.FixtureRequest) -> str: + caller_function = _sanitize_caller_function(request.node.name) + dir_path = FileUtils.get_test_output_path(caller_function, str(request.path)) + + try: + os.makedirs(dir_path) + except Exception: + return dir_path + return dir_path + + @pytest.fixture def fileutils() -> t.Type[FileUtils]: return FileUtils @@ -520,86 +587,10 @@ def fileutils() -> t.Type[FileUtils]: class FileUtils: @staticmethod - def _test_dir_path(caller_function: str, caller_fspath: str) -> str: + def get_test_output_path(caller_function: str, caller_fspath: str) -> str: caller_file_to_dir = os.path.splitext(str(caller_fspath))[0] - rel_path = os.path.relpath(caller_file_to_dir, os.path.dirname(test_dir)) - dir_path = os.path.join(test_dir, rel_path, caller_function) - return dir_path - - @staticmethod - def get_test_dir( - caller_function: t.Optional[str] = None, - caller_fspath: t.Optional[str] = None, - level: int = 1, - ) -> str: - """Get path to test output. - - This function should be called without arguments from within - a test: the returned directory will be - `test_output///`. - When called from other functions (e.g. from functions in this file), - the caller function and the caller file path should be provided. - The directory will not be created, but the parent (and all the needed - tree) will. This is to allow tests to create the directory. - - :param caller_function: caller function name defaults to None - :type caller_function: str, optional - :param caller_fspath: absolute path to file containing caller, defaults to None - :type caller_fspath: str or Path, optional - :return: String path to test output directory - :rtype: str - """ - if not caller_function or not caller_fspath: - caller_frame = inspect.stack()[level] - caller_fspath = caller_frame.filename - caller_function = caller_frame.function - - dir_path = FileUtils._test_dir_path(caller_function, caller_fspath) - if not os.path.exists(os.path.dirname(dir_path)): - os.makedirs(os.path.dirname(dir_path)) - # dir_path = os.path.join(test_dir, dir_name) - return dir_path - - @staticmethod - def make_test_dir( - caller_function: t.Optional[str] = None, - caller_fspath: t.Optional[str] = None, - level: int = 1, - sub_dir: t.Optional[str] = None, - ) -> str: - """Create test output directory and return path to it. - - This function should be called without arguments from within - a test: the directory will be created as - `test_output///`. - When called from other functions (e.g. from functions in this file), - the caller function and the caller file path should be provided. - - :param caller_function: caller function name defaults to None - :type caller_function: str, optional - :param caller_fspath: absolute path to file containing caller, defaults to None - :type caller_fspath: str or Path, optional - :param level: indicate depth in the call stack relative to test method. - :type level: int, optional - :param sub_dir: a relative path to create in the test directory - :type sub_dir: str or Path, optional - - :return: String path to test output directory - :rtype: str - """ - if not caller_function or not caller_fspath: - caller_frame = inspect.stack()[level] - caller_fspath = caller_frame.filename - caller_function = caller_frame.function - - dir_path = FileUtils._test_dir_path(caller_function, caller_fspath) - if sub_dir: - dir_path = os.path.join(dir_path, sub_dir) - - try: - os.makedirs(dir_path) - except Exception: - return dir_path + rel_path = os.path.relpath(caller_file_to_dir, os.path.dirname(test_output_root)) + dir_path = os.path.join(test_output_root, rel_path, caller_function) return dir_path @staticmethod @@ -613,21 +604,23 @@ def get_test_dir_path(dirname: str) -> str: return dir_path @staticmethod - def make_test_file(file_name: str, file_dir: t.Optional[str] = None) -> str: + def make_test_file(file_name: str, file_dir: str, file_content: t.Optional[str] = None) -> str: """Create a dummy file in the test output directory. :param file_name: name of file to create, e.g. "file.txt" :type file_name: str - :param file_dir: path relative to test output directory, e.g. "deps/libs" + :param file_dir: path :type file_dir: str :return: String path to test output file :rtype: str """ - test_dir = FileUtils.make_test_dir(level=2, sub_dir=file_dir) - file_path = os.path.join(test_dir, file_name) - + file_path = os.path.join(file_dir, file_name) + os.makedirs(file_dir) with open(file_path, "w+", encoding="utf-8") as dummy_file: - dummy_file.write("dummy\n") + if not file_content: + dummy_file.write("dummy\n") + else: + dummy_file.write(file_content) return file_path @@ -658,32 +651,49 @@ def setup_test_colo( fileutils: t.Type[FileUtils], db_type: str, exp: Experiment, + application_file: str, db_args: t.Dict[str, t.Any], - colo_settings: t.Optional[t.Dict[str, t.Any]] = None, + colo_settings: t.Optional[RunSettings] = None, + colo_model_name: str = "colocated_model", + port: int = test_port, + on_wlm: bool = False, ) -> Model: - """Setup things needed for setting up the colo pinning tests""" + """Setup database needed for the colo pinning tests""" + # get test setup - test_dir = fileutils.make_test_dir(level=2) - sr_test_script = fileutils.get_test_conf_path("send_data_local_smartredis.py") + sr_test_script = fileutils.get_test_conf_path(application_file) # Create an app with a colo_db which uses 1 db_cpu if colo_settings is None: colo_settings = exp.create_run_settings( exe=sys.executable, exe_args=[sr_test_script] ) - colo_model = exp.create_model("colocated_model", colo_settings) - colo_model.set_path(test_dir) + if on_wlm: + colo_settings.set_tasks(1) + colo_settings.set_nodes(1) + colo_model = exp.create_model(colo_model_name, colo_settings) if db_type in ["tcp", "deprecated"]: - db_args["port"] = 6780 + db_args["port"] = port db_args["ifname"] = "lo" + if db_type == "uds" and colo_model_name is not None: + tmp_dir = tempfile.gettempdir() + socket_suffix = str(uuid.uuid4())[:7] + db_args["unix_socket"] = os.path.join(tmp_dir, + f"{colo_model_name}_{socket_suffix}.socket") colocate_fun: t.Dict[str, t.Callable[..., None]] = { "tcp": colo_model.colocate_db_tcp, "deprecated": colo_model.colocate_db, "uds": colo_model.colocate_db_uds, } - colocate_fun[db_type](**db_args) + with warnings.catch_warnings(): + if db_type == "deprecated": + warnings.filterwarnings( + "ignore", + message="`colocate_db` has been deprecated" + ) + colocate_fun[db_type](**db_args) # assert model will launch with colocated db assert colo_model.colocated # Check to make sure that limit_db_cpus made it into the colo settings diff --git a/doc/_static/version_names.json b/doc/_static/version_names.json index b6d8984e2..8ae78ebdb 100644 --- a/doc/_static/version_names.json +++ b/doc/_static/version_names.json @@ -1,7 +1,9 @@ { "version_names":[ "develop (unstable)", - "0.5.0 (stable)", + "0.6.0 (stable)", + "0.5.1", + "0.5.0", "0.4.2", "0.4.1", "0.4.0", @@ -10,6 +12,8 @@ "version_urls": [ "https://www.craylabs.org/develop/overview.html", "https://www.craylabs.org/docs/overview.html", + "https://www.craylabs.org/docs/versions/0.5.1/overview.html", + "https://www.craylabs.org/docs/versions/0.5.0/overview.html", "https://www.craylabs.org/docs/versions/0.4.2/overview.html", "https://www.craylabs.org/docs/versions/0.4.1/overview.html", "https://www.craylabs.org/docs/versions/0.4.0/overview.html", diff --git a/doc/api/smartredis_api.rst b/doc/api/smartredis_api.rst index 3a2d77e22..27838bf7b 100644 --- a/doc/api/smartredis_api.rst +++ b/doc/api/smartredis_api.rst @@ -1,7 +1,10 @@ +.. _smartredis-api: + ************** SmartRedis API ************** + Python ****** diff --git a/doc/api/smartsim_api.rst b/doc/api/smartsim_api.rst index 315c37b4e..5136c8aa5 100644 --- a/doc/api/smartsim_api.rst +++ b/doc/api/smartsim_api.rst @@ -4,10 +4,11 @@ SmartSim API ************* +.. _experiment_api: + Experiment ========== -.. _experiment_api: .. currentmodule:: smartsim.experiment @@ -73,10 +74,11 @@ Types of Containers: Singularity +.. _rs-api: + RunSettings ----------- -.. _rs-api: When running SmartSim on laptops and single node workstations, the base ``RunSettings`` object is used to parameterize jobs. @@ -96,10 +98,11 @@ launches that utilize a parallel launch binary like :members: +.. _srun_api: + SrunSettings ------------ -.. _srun_api: ``SrunSettings`` can be used for running on existing allocations, running jobs in interactive allocations, and for adding srun @@ -126,11 +129,11 @@ steps to a batch. :members: +.. _aprun_api: AprunSettings ------------- -.. _aprun_api: ``AprunSettings`` can be used on any system that supports the Cray ALPS layer. SmartSim supports using ``AprunSettings`` @@ -159,11 +162,11 @@ and within batch launches (e.g., ``QsubBatchSettings``) +.. _jsrun_api: JsrunSettings ------------- -.. _jsrun_api: ``JsrunSettings`` can be used on any system that supports the IBM LSF launcher. @@ -194,11 +197,11 @@ and within batch launches (i.e. ``BsubBatchSettings``) :undoc-members: :members: +.. _openmpi_run_api: MpirunSettings -------------- -.. _openmpi_run_api: ``MpirunSettings`` are for launching with OpenMPI. ``MpirunSettings`` are supported on Slurm, PBSpro, and Cobalt. @@ -221,11 +224,11 @@ supported on Slurm, PBSpro, and Cobalt. :undoc-members: :members: +.. _openmpi_exec_api: MpiexecSettings --------------- -.. _openmpi_exec_api: ``MpiexecSettings`` are for launching with OpenMPI's ``mpiexec``. ``MpirunSettings`` are supported on Slurm, PBSpro, and Cobalt. @@ -248,11 +251,11 @@ supported on Slurm, PBSpro, and Cobalt. :undoc-members: :members: +.. _openmpi_orte_api: OrterunSettings --------------- -.. _openmpi_orte_api: ``OrterunSettings`` are for launching with OpenMPI's ``orterun``. ``OrterunSettings`` are supported on Slurm, PBSpro, and Cobalt. @@ -279,11 +282,11 @@ supported on Slurm, PBSpro, and Cobalt. ------------------------------------------ +.. _sbatch_api: SbatchSettings -------------- -.. _sbatch_api: ``SbatchSettings`` are used for launching batches onto Slurm WLM systems. @@ -305,11 +308,11 @@ WLM systems. :undoc-members: :members: +.. _qsub_api: QsubBatchSettings ----------------- -.. _qsub_api: ``QsubBatchSettings`` are used to configure jobs that should be launched as a batch on PBSPro systems. @@ -333,12 +336,12 @@ be launched as a batch on PBSPro systems. :members: +.. _cqsub_api: + CobaltBatchSettings ------------------- -.. _cqsub_api: - ``CobaltBatchSettings`` are used to configure jobs that should be launched as a batch on Cobalt Systems. They closely mimic that of the ``QsubBatchSettings`` for PBSPro. @@ -359,11 +362,11 @@ that of the ``QsubBatchSettings`` for PBSPro. :members: +.. _bsub_api: BsubBatchSettings ----------------- -.. _bsub_api: ``BsubBatchSettings`` are used to configure jobs that should be launched as a batch on LSF systems. @@ -386,11 +389,11 @@ be launched as a batch on LSF systems. :undoc-members: :members: +.. _singularity_api: Singularity ----------- -.. _singularity_api: ``Singularity`` is a type of ``Container`` that can be passed to a ``RunSettings`` class or child class to enable running the workload in a @@ -407,11 +410,11 @@ Orchestrator .. currentmodule:: smartsim.database +.. _orc_api: Orchestrator ------------ -.. _orc_api: .. autoclass:: Orchestrator :members: @@ -446,11 +449,11 @@ Model :show-inheritance: :inherited-members: +.. _ensemble_api: Ensemble ======== -.. _ensemble_api: .. currentmodule:: smartsim.entity.ensemble @@ -472,31 +475,27 @@ Ensemble :show-inheritance: :inherited-members: +.. _ml_api: Machine Learning ================ -.. _ml_api: SmartSim includes built-in utilities for supporting TensorFlow, Keras, and Pytorch. +.. _smartsim_tf_api: + TensorFlow ---------- -.. _smartsim_tf_api: SmartSim includes built-in utilities for supporting TensorFlow and Keras in training and inference. .. currentmodule:: smartsim.ml.tf.utils -.. autosummary:: - - freeze_model - .. automodule:: smartsim.ml.tf.utils :members: - .. currentmodule:: smartsim.ml.tf .. autoclass:: StaticDataGenerator @@ -509,11 +508,11 @@ SmartSim includes built-in utilities for supporting TensorFlow and Keras in trai :show-inheritance: :inherited-members: +.. _smartsim_torch_api: + PyTorch ---------- -.. _smartsim_torch_api: - SmartSim includes built-in utilities for supporting PyTorch in training and inference. .. currentmodule:: smartsim.ml.torch @@ -533,11 +532,11 @@ SmartSim includes built-in utilities for supporting PyTorch in training and infe :show-inheritance: :inherited-members: +.. _slurm_module_api: + Slurm ===== -.. _slurm_module_api: - .. currentmodule:: smartsim.slurm @@ -548,4 +547,3 @@ Slurm .. automodule:: smartsim.slurm :members: - diff --git a/doc/changelog.rst b/doc/changelog.rst index 375e1f17e..befb9ee37 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -5,13 +5,88 @@ Changelog Listed here are the changes between each release of SmartSim and SmartRedis. -Jump to :ref:`SmartRedis Changelog ` +Jump to :ref:`SmartRedis Changelog ` SmartSim ======== +Development branch +------------------ + +To be released at some future point in time + + +0.6.0 +----- + +Released on 18 December, 2023 + +Description + +- Conflicting directives in the SmartSim packaging instructions were fixed +- `sacct` and `sstat` errors are now fatal for Slurm-based workflow executions +- Added documentation section about ML features and TorchScript +- Added TorchScript functions to Online Analysis tutorial +- Added multi-DB example to documentation +- Improved test stability on HPC systems +- Added support for producing & consuming telemetry outputs +- Split tests into groups for parallel execution in CI/CD pipeline +- Change signature of `Experiment.summary()` +- Expose first_device parameter for scripts, functions, models +- Added support for MINBATCHTIMEOUT in model execution +- Remove support for RedisAI 1.2.5, use RedisAI 1.2.7 commit +- Add support for multiple databases + +Detailed Notes + +- Several conflicting directives between the `setup.py` and the `setup.cfg` were fixed + to mitigate warnings issued when building the pip wheel. (SmartSim-PR435_) +- When the Slurm functions `sacct` and `sstat` returned an error, it would be ignored + and SmartSim's state could become inconsistent. To prevent this, errors + raised by `sacct` or `sstat` now result in an exception. (SmartSim-PR392_) +- A section named *ML Features* was added to documentation. It contains multiple + examples of how ML models and functions can be added to and executed on the DB. + TorchScript-based post-processing was added to the *Online Analysis* tutorial (SmartSim-PR411_) +- An example of how to use multiple Orchestrators concurrently was added to the documentation (SmartSim-PR409_) +- The test infrastructure was improved. Tests on HPC system are now stable, and issues such + as non-stopped `Orchestrators` or experiments created in the wrong paths have been fixed (SmartSim-PR381_) +- A telemetry monitor was added to check updates and produce events for SmartDashboard (SmartSim-PR426_) +- Split tests into `group_a`, `group_b`, `slow_tests` for parallel execution in CI/CD pipeline (SmartSim-PR417_, SmartSim-PR424_) +- Change `format` argument to `style` in `Experiment.summary()`, this is + an API break (SmartSim-PR391_) +- Added support for first_device parameter for scripts, functions, + and models. This causes them to be loaded to the first num_devices + beginning with first_device (SmartSim-PR394_) +- Added support for MINBATCHTIMEOUT in model execution, which caps the delay + waiting for a minimium number of model execution operations to accumulate + before executing them as a batch (SmartSim-PR387_) +- RedisAI 1.2.5 is not supported anymore. The only RedisAI version + is now 1.2.7. Since the officially released RedisAI 1.2.7 has a + bug which breaks the build process on Mac OSX, it was decided to + use commit 634916c_ from RedisAI's GitHub repository, where such + bug has been fixed. This applies to all operating systems. (SmartSim-PR383_) +- Add support for creation of multiple databases with unique identifiers. (SmartSim-PR342_) + + +.. _SmartSim-PR435: https://github.com/CrayLabs/SmartSim/pull/435 +.. _SmartSim-PR392: https://github.com/CrayLabs/SmartSim/pull/392 +.. _SmartSim-PR411: https://github.com/CrayLabs/SmartSim/pull/411 +.. _SmartSim-PR409: https://github.com/CrayLabs/SmartSim/pull/409 +.. _SmartSim-PR381: https://github.com/CrayLabs/SmartSim/pull/381 +.. _SmartSim-PR426: https://github.com/CrayLabs/SmartSim/pull/426 +.. _SmartSim-PR424: https://github.com/CrayLabs/SmartSim/pull/424 +.. _SmartSim-PR417: https://github.com/CrayLabs/SmartSim/pull/417 +.. _SmartSim-PR391: https://github.com/CrayLabs/SmartSim/pull/391 +.. _SmartSim-PR342: https://github.com/CrayLabs/SmartSim/pull/342 +.. _SmartSim-PR394: https://github.com/CrayLabs/SmartSim/pull/394 +.. _SmartSim-PR387: https://github.com/CrayLabs/SmartSim/pull/387 +.. _SmartSim-PR383: https://github.com/CrayLabs/SmartSim/pull/383 +.. _634916c: https://github.com/RedisAI/RedisAI/commit/634916c722e718cc6ea3fad46e63f7d798f9adc2 +.. _SmartSim-PR342: https://github.com/CrayLabs/SmartSim/pull/342 + + 0.5.1 ----- @@ -36,53 +111,53 @@ Description Detailed Notes -- Add methods to allow users to inspect files attached to models and ensembles. (PR352_) -- Add a `smart info` target to provide rudimentary information about the SmartSim installation. (PR350_) -- Remove unnecessary generation producing unexpected directories in the test suite. (PR349_) -- Add support for heterogeneous jobs to `SrunSettings` by allowing users to set the `--het-group` parameter. (PR346_) -- Provide clearer guidelines on how to contribute to SmartSim. (PR344_) -- Integrate `PalsMpiexecSettings` into the `Experiment` factory methods when using the `"pals"` launcher. (PR343_) -- Create public properties where appropriate to mitigate `protected-access` errors. (PR341_) -- Fix a failure to execute `_prep_colocated_db` due to incorrect named attr check. (PR339_) -- Enabled and mitigated mypy `disallow_any_generics` and `warn_return_any`. (PR338_) -- Add a `smart validate` target to provide a simple smoke test to assess a SmartSim build. (PR336_, PR351_) -- Add typehints to `smartsim._core.launcher.step.*`. (PR334_) -- Log errors reported from slurm WLM when attempts to retrieve status fail. (PR331_, PR332_) -- Fix incorrectly formatted positional arguments in log format strings. (PR330_) -- Ensure that launchers pass environment variables to unmanaged job steps. (PR329_) -- Add additional tests surrounding the `RAI_PATH` configuration environment variable. (PR328_) -- Remove unnecessary execution of unescaped shell commands. (PR327_) -- Add error if user calls get_allocation with reserved keywords in slurm get_allocation. (PR325_) -- Add error when user requests CPU with devices greater than 1 within add_ml_model and add_script. (PR324_) -- Update documentation surrounding ensemble key prefixing. (PR322_) -- Fix formatting of the Frontier site installation. (PR321_) -- Update pylint dependency, update .pylintrc, mitigate non-breaking issues, suppress api breaks. (PR311_) -- Refactor the `smart` CLI to use subparsers for better documentation and extension. (PR308_) - -.. _PR352: https://github.com/CrayLabs/SmartSim/pull/352 -.. _PR351: https://github.com/CrayLabs/SmartSim/pull/351 -.. _PR350: https://github.com/CrayLabs/SmartSim/pull/350 -.. _PR349: https://github.com/CrayLabs/SmartSim/pull/349 -.. _PR346: https://github.com/CrayLabs/SmartSim/pull/346 -.. _PR344: https://github.com/CrayLabs/SmartSim/pull/344 -.. _PR343: https://github.com/CrayLabs/SmartSim/pull/343 -.. _PR341: https://github.com/CrayLabs/SmartSim/pull/341 -.. _PR339: https://github.com/CrayLabs/SmartSim/pull/339 -.. _PR338: https://github.com/CrayLabs/SmartSim/pull/338 -.. _PR336: https://github.com/CrayLabs/SmartSim/pull/336 -.. _PR334: https://github.com/CrayLabs/SmartSim/pull/334 -.. _PR332: https://github.com/CrayLabs/SmartSim/pull/332 -.. _PR331: https://github.com/CrayLabs/SmartSim/pull/331 -.. _PR330: https://github.com/CrayLabs/SmartSim/pull/330 -.. _PR329: https://github.com/CrayLabs/SmartSim/pull/329 -.. _PR328: https://github.com/CrayLabs/SmartSim/pull/328 -.. _PR327: https://github.com/CrayLabs/SmartSim/pull/327 -.. _PR325: https://github.com/CrayLabs/SmartSim/pull/325 -.. _PR324: https://github.com/CrayLabs/SmartSim/pull/324 -.. _PR322: https://github.com/CrayLabs/SmartSim/pull/322 -.. _PR321: https://github.com/CrayLabs/SmartSim/pull/321 -.. _PR311: https://github.com/CrayLabs/SmartSim/pull/311 -.. _PR308: https://github.com/CrayLabs/SmartSim/pull/308 +- Add methods to allow users to inspect files attached to models and ensembles. (SmartSim-PR352_) +- Add a `smart info` target to provide rudimentary information about the SmartSim installation. (SmartSim-PR350_) +- Remove unnecessary generation producing unexpected directories in the test suite. (SmartSim-PR349_) +- Add support for heterogeneous jobs to `SrunSettings` by allowing users to set the `--het-group` parameter. (SmartSim-PR346_) +- Provide clearer guidelines on how to contribute to SmartSim. (SmartSim-PR344_) +- Integrate `PalsMpiexecSettings` into the `Experiment` factory methods when using the `"pals"` launcher. (SmartSim-PR343_) +- Create public properties where appropriate to mitigate `protected-access` errors. (SmartSim-PR341_) +- Fix a failure to execute `_prep_colocated_db` due to incorrect named attr check. (SmartSim-PR339_) +- Enabled and mitigated mypy `disallow_any_generics` and `warn_return_any`. (SmartSim-PR338_) +- Add a `smart validate` target to provide a simple smoke test to assess a SmartSim build. (SmartSim-PR336_, SmartSim-PR351_) +- Add typehints to `smartsim._core.launcher.step.*`. (SmartSim-PR334_) +- Log errors reported from slurm WLM when attempts to retrieve status fail. (SmartSim-PR331_, SmartSim-PR332_) +- Fix incorrectly formatted positional arguments in log format strings. (SmartSim-PR330_) +- Ensure that launchers pass environment variables to unmanaged job steps. (SmartSim-PR329_) +- Add additional tests surrounding the `RAI_PATH` configuration environment variable. (SmartSim-PR328_) +- Remove unnecessary execution of unescaped shell commands. (SmartSim-PR327_) +- Add error if user calls get_allocation with reserved keywords in slurm get_allocation. (SmartSim-PR325_) +- Add error when user requests CPU with devices greater than 1 within add_ml_model and add_script. (SmartSim-PR324_) +- Update documentation surrounding ensemble key prefixing. (SmartSim-PR322_) +- Fix formatting of the Frontier site installation. (SmartSim-PR321_) +- Update pylint dependency, update .pylintrc, mitigate non-breaking issues, suppress api breaks. (SmartSim-PR311_) +- Refactor the `smart` CLI to use subparsers for better documentation and extension. (SmartSim-PR308_) + +.. _SmartSim-PR352: https://github.com/CrayLabs/SmartSim/pull/352 +.. _SmartSim-PR351: https://github.com/CrayLabs/SmartSim/pull/351 +.. _SmartSim-PR350: https://github.com/CrayLabs/SmartSim/pull/350 +.. _SmartSim-PR349: https://github.com/CrayLabs/SmartSim/pull/349 +.. _SmartSim-PR346: https://github.com/CrayLabs/SmartSim/pull/346 +.. _SmartSim-PR344: https://github.com/CrayLabs/SmartSim/pull/344 +.. _SmartSim-PR343: https://github.com/CrayLabs/SmartSim/pull/343 +.. _SmartSim-PR341: https://github.com/CrayLabs/SmartSim/pull/341 +.. _SmartSim-PR339: https://github.com/CrayLabs/SmartSim/pull/339 +.. _SmartSim-PR338: https://github.com/CrayLabs/SmartSim/pull/338 +.. _SmartSim-PR336: https://github.com/CrayLabs/SmartSim/pull/336 +.. _SmartSim-PR334: https://github.com/CrayLabs/SmartSim/pull/334 +.. _SmartSim-PR332: https://github.com/CrayLabs/SmartSim/pull/332 +.. _SmartSim-PR331: https://github.com/CrayLabs/SmartSim/pull/331 +.. _SmartSim-PR330: https://github.com/CrayLabs/SmartSim/pull/330 +.. _SmartSim-PR329: https://github.com/CrayLabs/SmartSim/pull/329 +.. _SmartSim-PR328: https://github.com/CrayLabs/SmartSim/pull/328 +.. _SmartSim-PR327: https://github.com/CrayLabs/SmartSim/pull/327 +.. _SmartSim-PR325: https://github.com/CrayLabs/SmartSim/pull/325 +.. _SmartSim-PR324: https://github.com/CrayLabs/SmartSim/pull/324 +.. _SmartSim-PR322: https://github.com/CrayLabs/SmartSim/pull/322 +.. _SmartSim-PR321: https://github.com/CrayLabs/SmartSim/pull/321 +.. _SmartSim-PR311: https://github.com/CrayLabs/SmartSim/pull/311 +.. _SmartSim-PR308: https://github.com/CrayLabs/SmartSim/pull/308 0.5.0 @@ -112,70 +187,70 @@ A full list of changes and detailed notes can be found below: Detailed notes -- Updates SmartRedis to the most current release (PR316_) -- Fixes and enhancements to documentation (PR317_, PR314_, PR287_) -- Various fixes and enhancements to the test suite (PR315_, PR312_, PR310_, PR302_, PR283_) +- Updates SmartRedis to the most current release (SmartSim-PR316_) +- Fixes and enhancements to documentation (SmartSim-PR317_, SmartSim-PR314_, SmartSim-PR287_) +- Various fixes and enhancements to the test suite (SmartSim-PR315_, SmartSim-PR312_, SmartSim-PR310_, SmartSim-PR302_, SmartSim-PR283_) - Fix a defect in the tests related to database models and scripts that was - causing key collisions when testing on workload managers (PR313_) -- Remove `requirements.txt` and other places where dependencies were defined. (PR307_) + causing key collisions when testing on workload managers (SmartSim-PR313_) +- Remove `requirements.txt` and other places where dependencies were defined. (SmartSim-PR307_) - Fix defect where dictionaries used to create run settings can be changed - unexpectedly due to copy-by-ref (PR305_) + unexpectedly due to copy-by-ref (SmartSim-PR305_) - The underlying code for Model.add_ml_model() and Model.add_script() was fixed to correctly handle multi-GPU configurations. Tests were updated to run on non-local launchers. Documentation was updated and fixed. Also, the default - testing interface has been changed to lo instead of ipogif. (PR304_) + testing interface has been changed to lo instead of ipogif. (SmartSim-PR304_) - Typehints have been added. A makefile target `make check-mypy` executes static - analysis with mypy. (PR295_, PR301_, PR303_) + analysis with mypy. (SmartSim-PR295_, SmartSim-PR301_, SmartSim-PR303_) - Replace `limit_app_cpus` with `limit_db_cpus` for co-located orchestrators. This resolves some incorrect behavior/assumptions about how the application would be pinned. Instead, users should directly specify the binding options in - their application using the options appropriate for their launcher (PR306_) -- Simplify code in `random_permutations` parameter generation strategy (PR300_) -- Remove wait time associated with Experiment launch summary (PR298_) -- Update Redis conf file to conform with Redis v7.0.5 conf file (PR293_) -- Migrate from redis-py-cluster to redis-py for cluster status checks (PR292_) -- Update full test suite to no longer require a tensorflow wheel to be available at test time. (PR291_) -- Correct spelling of colocated in doc strings (PR290_) + their application using the options appropriate for their launcher (SmartSim-PR306_) +- Simplify code in `random_permutations` parameter generation strategy (SmartSim-PR300_) +- Remove wait time associated with Experiment launch summary (SmartSim-PR298_) +- Update Redis conf file to conform with Redis v7.0.5 conf file (SmartSim-PR293_) +- Migrate from redis-py-cluster to redis-py for cluster status checks (SmartSim-PR292_) +- Update full test suite to no longer require a tensorflow wheel to be available at test time. (SmartSim-PR291_) +- Correct spelling of colocated in doc strings (SmartSim-PR290_) - Deprecated launcher-specific orchestrators, constants, and ML - utilities were removed. (PR289_) -- Relax the coloredlogs version to be greater than 10.0 (PR288_) + utilities were removed. (SmartSim-PR289_) +- Relax the coloredlogs version to be greater than 10.0 (SmartSim-PR288_) - Update the Github Actions runner image from `macos-10.15`` to `macos-12``. The - former began deprecation in May 2022 and was finally removed in May 2023. (PR285_) + former began deprecation in May 2022 and was finally removed in May 2023. (SmartSim-PR285_) - The Fortran tutorials had not been fully updated to show how to handle - return/error codes. These have now all been updated. (PR284_) + return/error codes. These have now all been updated. (SmartSim-PR284_) - Orchestrator and Colocated DB now accept a list of interfaces to bind to. The - argument name is still `interface` for backward compatibility reasons. (PR281_) + argument name is still `interface` for backward compatibility reasons. (SmartSim-PR281_) - Typehints have been added to public APIs. A makefile target to execute static - analysis with mypy is available `make check-mypy`. (PR295_) - -.. _PR317: https://github.com/CrayLabs/SmartSim/pull/317 -.. _PR316: https://github.com/CrayLabs/SmartSim/pull/316 -.. _PR315: https://github.com/CrayLabs/SmartSim/pull/314 -.. _PR314: https://github.com/CrayLabs/SmartSim/pull/314 -.. _PR313: https://github.com/CrayLabs/SmartSim/pull/313 -.. _PR312: https://github.com/CrayLabs/SmartSim/pull/312 -.. _PR310: https://github.com/CrayLabs/SmartSim/pull/310 -.. _PR307: https://github.com/CrayLabs/SmartSim/pull/307 -.. _PR306: https://github.com/CrayLabs/SmartSim/pull/306 -.. _PR305: https://github.com/CrayLabs/SmartSim/pull/305 -.. _PR304: https://github.com/CrayLabs/SmartSim/pull/304 -.. _PR303: https://github.com/CrayLabs/SmartSim/pull/303 -.. _PR302: https://github.com/CrayLabs/SmartSim/pull/302 -.. _PR301: https://github.com/CrayLabs/SmartSim/pull/301 -.. _PR300: https://github.com/CrayLabs/SmartSim/pull/300 -.. _PR298: https://github.com/CrayLabs/SmartSim/pull/298 -.. _PR295: https://github.com/CrayLabs/SmartSim/pull/295 -.. _PR293: https://github.com/CrayLabs/SmartSim/pull/293 -.. _PR292: https://github.com/CrayLabs/SmartSim/pull/292 -.. _PR291: https://github.com/CrayLabs/SmartSim/pull/291 -.. _PR290: https://github.com/CrayLabs/SmartSim/pull/290 -.. _PR289: https://github.com/CrayLabs/SmartSim/pull/289 -.. _PR288: https://github.com/CrayLabs/SmartSim/pull/288 -.. _PR287: https://github.com/CrayLabs/SmartSim/pull/287 -.. _PR285: https://github.com/CrayLabs/SmartSim/pull/285 -.. _PR284: https://github.com/CrayLabs/SmartSim/pull/284 -.. _PR283: https://github.com/CrayLabs/SmartSim/pull/283 -.. _PR281: https://github.com/CrayLabs/SmartSim/pull/281 + analysis with mypy is available `make check-mypy`. (SmartSim-PR295_) + +.. _SmartSim-PR317: https://github.com/CrayLabs/SmartSim/pull/317 +.. _SmartSim-PR316: https://github.com/CrayLabs/SmartSim/pull/316 +.. _SmartSim-PR315: https://github.com/CrayLabs/SmartSim/pull/314 +.. _SmartSim-PR314: https://github.com/CrayLabs/SmartSim/pull/314 +.. _SmartSim-PR313: https://github.com/CrayLabs/SmartSim/pull/313 +.. _SmartSim-PR312: https://github.com/CrayLabs/SmartSim/pull/312 +.. _SmartSim-PR310: https://github.com/CrayLabs/SmartSim/pull/310 +.. _SmartSim-PR307: https://github.com/CrayLabs/SmartSim/pull/307 +.. _SmartSim-PR306: https://github.com/CrayLabs/SmartSim/pull/306 +.. _SmartSim-PR305: https://github.com/CrayLabs/SmartSim/pull/305 +.. _SmartSim-PR304: https://github.com/CrayLabs/SmartSim/pull/304 +.. _SmartSim-PR303: https://github.com/CrayLabs/SmartSim/pull/303 +.. _SmartSim-PR302: https://github.com/CrayLabs/SmartSim/pull/302 +.. _SmartSim-PR301: https://github.com/CrayLabs/SmartSim/pull/301 +.. _SmartSim-PR300: https://github.com/CrayLabs/SmartSim/pull/300 +.. _SmartSim-PR298: https://github.com/CrayLabs/SmartSim/pull/298 +.. _SmartSim-PR295: https://github.com/CrayLabs/SmartSim/pull/295 +.. _SmartSim-PR293: https://github.com/CrayLabs/SmartSim/pull/293 +.. _SmartSim-PR292: https://github.com/CrayLabs/SmartSim/pull/292 +.. _SmartSim-PR291: https://github.com/CrayLabs/SmartSim/pull/291 +.. _SmartSim-PR290: https://github.com/CrayLabs/SmartSim/pull/290 +.. _SmartSim-PR289: https://github.com/CrayLabs/SmartSim/pull/289 +.. _SmartSim-PR288: https://github.com/CrayLabs/SmartSim/pull/288 +.. _SmartSim-PR287: https://github.com/CrayLabs/SmartSim/pull/287 +.. _SmartSim-PR285: https://github.com/CrayLabs/SmartSim/pull/285 +.. _SmartSim-PR284: https://github.com/CrayLabs/SmartSim/pull/284 +.. _SmartSim-PR283: https://github.com/CrayLabs/SmartSim/pull/283 +.. _SmartSim-PR281: https://github.com/CrayLabs/SmartSim/pull/281 0.4.2 ----- @@ -211,38 +286,38 @@ Detailed Notes - Running some tests could result in some SmartSim-specific environment variables to be set. Such environment variables are now reset after each test execution. Also, a warning for environment variable usage in Slurm was added, to make the user aware in case an environment - variable will not be assigned the desired value with `--export`. (PR270_) -- The PyTorch and TensorFlow data loaders were update to make use of aggregation lists. This breaks their API, but makes them easier to use. (PR264_) + variable will not be assigned the desired value with `--export`. (SmartSim-PR270_) +- The PyTorch and TensorFlow data loaders were update to make use of aggregation lists. This breaks their API, but makes them easier to use. (SmartSim-PR264_) - The support for Ray was dropped, as its most recent versions caused problems when deployed through SmartSim. We plan to release a separate add-on library to accomplish the same results. If - you are interested in getting the Ray launch functionality back in your workflow, please get in touch with us! (PR263_) -- Update from Redis version 6.0.8 to 7.0.5. (PR258_) + you are interested in getting the Ray launch functionality back in your workflow, please get in touch with us! (SmartSim-PR263_) +- Update from Redis version 6.0.8 to 7.0.5. (SmartSim-PR258_) - Adds support for Python 3.10 without the ONNX machine learning backend. Deprecates support for Python 3.7 as it will stop receiving security updates. Deprecates support for RedisAI 1.2.3. Update the build process to be able to correctly fetch supported dependencies. If a user attempts to build an unsupported dependency, an error message is shown highlighting the - discrepancy. (PR256_) + discrepancy. (SmartSim-PR256_) - Models were given a `batch_settings` attribute. When launching a model through `Experiment.start` the `Experiment` will first check for a non-nullish value at that attribute. If the check is satisfied, the `Experiment` will attempt to wrap the underlying run command in a batch job using the object referenced at `Model.batch_settings` as the batch settings for the job. If the check - is not satisfied, the `Model` is launched in the traditional manner as a job step. (PR245_) -- Fix bug in colocated database entrypoint stemming from uninitialized variables. This bug affects PyTorch models being loaded into the database. (PR237_) -- The release of RedisAI 1.2.7 allows us to update support for recent versions of PyTorch, Tensorflow, and ONNX (PR234_) + is not satisfied, the `Model` is launched in the traditional manner as a job step. (SmartSim-PR245_) +- Fix bug in colocated database entrypoint stemming from uninitialized variables. This bug affects PyTorch models being loaded into the database. (SmartSim-PR237_) +- The release of RedisAI 1.2.7 allows us to update support for recent versions of PyTorch, Tensorflow, and ONNX (SmartSim-PR234_) - Make installation of correct Torch backend more reliable according to instruction from PyTorch - In addition to TCP, add UDS support for colocating an orchestrator with models. Methods `Model.colocate_db_tcp` and `Model.colocate_db_uds` were added to expose this functionality. - The `Model.colocate_db` method remains and uses TCP for backward compatibility (PR246_) + The `Model.colocate_db` method remains and uses TCP for backward compatibility (SmartSim-PR246_) -.. _PR270: https://github.com/CrayLabs/SmartSim/pull/270 -.. _PR264: https://github.com/CrayLabs/SmartSim/pull/264 -.. _PR263: https://github.com/CrayLabs/SmartSim/pull/263 -.. _PR258: https://github.com/CrayLabs/SmartSim/pull/258 -.. _PR256: https://github.com/CrayLabs/SmartSim/pull/256 -.. _PR246: https://github.com/CrayLabs/SmartSim/pull/246 -.. _PR245: https://github.com/CrayLabs/SmartSim/pull/245 -.. _PR237: https://github.com/CrayLabs/SmartSim/pull/237 -.. _PR234: https://github.com/CrayLabs/SmartSim/pull/234 +.. _SmartSim-PR270: https://github.com/CrayLabs/SmartSim/pull/270 +.. _SmartSim-PR264: https://github.com/CrayLabs/SmartSim/pull/264 +.. _SmartSim-PR263: https://github.com/CrayLabs/SmartSim/pull/263 +.. _SmartSim-PR258: https://github.com/CrayLabs/SmartSim/pull/258 +.. _SmartSim-PR256: https://github.com/CrayLabs/SmartSim/pull/256 +.. _SmartSim-PR246: https://github.com/CrayLabs/SmartSim/pull/246 +.. _SmartSim-PR245: https://github.com/CrayLabs/SmartSim/pull/245 +.. _SmartSim-PR237: https://github.com/CrayLabs/SmartSim/pull/237 +.. _SmartSim-PR234: https://github.com/CrayLabs/SmartSim/pull/234 0.4.1 @@ -487,10 +562,10 @@ Description: --------------------------------------------------------------- +.. _sr_changelog: + SmartRedis ========== -.. _changelog: - .. include:: ../smartredis/doc/changelog.rst :start-line: 3 diff --git a/doc/code_of_conduct.rst b/doc/code_of_conduct.rst index 1ba818875..77d278b66 100644 --- a/doc/code_of_conduct.rst +++ b/doc/code_of_conduct.rst @@ -24,30 +24,33 @@ as Zoom, Teams, Google Meet, etc. * **Be welcoming, friendly, and patient.** * **Be considerate.** -Your work will be used by other people, and you in turn will depend on the work -of others. Any decision you make will affect users and colleagues, and you -should take those consequences into account when making decisions. + Your work will be used by other people, and you in turn will depend on the work + of others. Any decision you make will affect users and colleagues, and you + should take those consequences into account when making decisions. + * **Be respectful.** -Not all of us will agree all the time, but disagreement is no excuse for poor -behaviour and poor manners. We might all experience some frustration now and -then, but we cannot allow that frustration to turn into a personal attack. It is -important to remember that a community where people feel uncomfortable or -threatened is not a productive one. Members of the SmartSim community should be -respectful when dealing with other members as well as with people outside the -SmartSim community. + Not all of us will agree all the time, but disagreement is no excuse for poor + behaviour and poor manners. We might all experience some frustration now and + then, but we cannot allow that frustration to turn into a personal attack. It is + important to remember that a community where people feel uncomfortable or + threatened is not a productive one. Members of the SmartSim community should be + respectful when dealing with other members as well as with people outside the + SmartSim community. + * **Be careful in the words that you choose.** -Sexist, racist, and other exclusionary jokes and comments can be offensive to -those around you. Be kind to others. Do not insult or put down other -participants. Behave professionally. Remember that harassment and sexist, -racist, or exclusionary jokes are not appropriate for the community. + Sexist, racist, and other exclusionary jokes and comments can be offensive to + those around you. Be kind to others. Do not insult or put down other + participants. Behave professionally. Remember that harassment and sexist, + racist, or exclusionary jokes are not appropriate for the community. + * **When we disagree, we try to understand why.** -Disagreements, both social and technical, are a natural part of collaborative -development. It is important that we resolve disagreements and differing views -constructively. Remember that we are different. The strength of SmartSim comes -from its varied community, people from a wide range of backgrounds. Different -people have different perspectives on issues. Being unable to understand why -someone holds a viewpoint does not mean that they are wrong. Do not forget that -it is human to err and blaming each other does not get us anywhere. Rather, -offer to help resolve issues and to help learn from mistakes. + Disagreements, both social and technical, are a natural part of collaborative + development. It is important that we resolve disagreements and differing views + constructively. Remember that we are different. The strength of SmartSim comes + from its varied community, people from a wide range of backgrounds. Different + people have different perspectives on issues. Being unable to understand why + someone holds a viewpoint does not mean that they are wrong. Do not forget that + it is human to err and blaming each other does not get us anywhere. Rather, + offer to help resolve issues and to help learn from mistakes. Original text courtesy of the `Speak Up! project `_. \ No newline at end of file diff --git a/doc/conf.py b/doc/conf.py index 637e6cba6..908b9534f 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -10,6 +10,8 @@ # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # +# pylint: skip-file + import os import sys sys.path.insert(0, os.path.abspath('.')) @@ -24,7 +26,7 @@ import smartsim version = smartsim.__version__ except ImportError: - version = "0.5.1" + version = "0.6.0" # The full version, including alpha/beta/rc tags release = version @@ -37,6 +39,7 @@ # ones. extensions = [ 'sphinx.ext.autodoc', + 'sphinx.ext.autosectionlabel', 'sphinx.ext.todo', 'sphinx.ext.coverage', 'sphinx.ext.imgmath', @@ -47,9 +50,12 @@ 'sphinxfortran.fortran_domain', 'sphinxfortran.fortran_autodoc', 'breathe', - 'nbsphinx' + 'nbsphinx', + 'sphinx_copybutton', + 'sphinx_tabs.tabs' ] +suppress_warnings = ['autosectionlabel'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] @@ -82,10 +88,16 @@ # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] +slack_invite ="https://join.slack.com/t/craylabs/shared_invite/zt-nw3ag5z5-5PS4tIXBfufu1bIvvr71UA" +extra_footer = ('Questions? You can contact contact us or ' + f'join us on Slack!' + ) + html_theme_options = { "repository_url": "https://github.com/CrayLabs/SmartSim", "use_repository_button": True, "use_issues_button": True, + "extra_footer": extra_footer, } autoclass_content = 'both' diff --git a/doc/contributing.rst b/doc/contributing.rst index cb7965269..a8a860045 100644 --- a/doc/contributing.rst +++ b/doc/contributing.rst @@ -1,5 +1,5 @@ -********* +****************** Contributing Guide ****************** @@ -48,30 +48,30 @@ How to contribute We welcome contributions from the broader community. These generally fall under two categories: -- Developments to the main codebases: These contributions directly modify the -SmartSim and SmartRedis repositories to fix bugs in the code, improve -the documentation, or add new features. Before embarking on major development, -please contact the developers first to make sure that your proposal aligns -with the internal development by opening an issue or reach out to us. Please -follow the :ref:`Developer Guidelines `. +- Contributions to the main codebases: These contributions directly modify the + SmartSim and SmartRedis repositories to fix bugs in the code, improve + the documentation, or add new features. Before embarking on major development, + please contact the developers first to make sure that your proposal aligns + with the internal development by opening an issue or reaching out to us. Please + follow the :ref:`Developer Guidelines `. - Applications of SmartSim: Many of our users build scientific/engineering -applications using SmartSim and SmartRedis. Please feel free to make -contributions to the :ref:`SmartSim Zoo `. + applications using SmartSim and SmartRedis. Please feel free to make + contributions to the :ref:`SmartSim Zoo `. In both cases, contributors can expect the following: - A quick response to your pull requests detailing why we are accepting or -rejecting your contribution + rejecting your contribution. - If we accept your contribution, a SmartSim developer will be assigned to help -bring your contribution into the codebases by testing it across a variety of -platforms and ensuring code quality. + bring your contribution into the codebases by testing it across a variety of + platforms and ensuring code quality. -- You will be credited as a co-author when the contribution is merged +- You will be credited as a co-author when the contribution is merged. .. note:: - The last point serves as a `Developer Certificate of Origin`_. + The last point serves as a `Developer Certificate of Origin `_. More specifically, we will ask contributors to signoff on the final state of the PR before merging. This signoff will then be propagated into the final squash merge commit. @@ -95,4 +95,3 @@ through GitHub. For issues related to `SmartSim post here 4. **Contact the Development Team** - For all other inquiries including collaboration opportunities, please contact SmartSim at hpe dot com. - diff --git a/doc/developer.rst b/doc/developer.rst index cff124b2e..4009819c3 100644 --- a/doc/developer.rst +++ b/doc/developer.rst @@ -1,9 +1,9 @@ +.. _developer_guide: ********* Developer ********* -.. _developer_guide: This section details common practices and tips for contributors to SmartSim and SmartRedis. diff --git a/doc/index.rst b/doc/index.rst index d61fdb1ce..91a7ee1ba 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -32,6 +32,7 @@ experiment orchestrator launchers + ml_features api/smartsim_api .. toctree:: @@ -46,8 +47,15 @@ sr_data_structures sr_dataset_conversions sr_runtime + sr_advanced_topics api/smartredis_api +.. toctree:: + :maxdepth: 2 + :caption: SmartDashboard + + smartdashboard + .. toctree:: :maxdepth: 2 :caption: Reference diff --git a/doc/installation_instructions/basic.rst b/doc/installation_instructions/basic.rst index f18cc3ec6..3874eb961 100644 --- a/doc/installation_instructions/basic.rst +++ b/doc/installation_instructions/basic.rst @@ -33,7 +33,6 @@ The base prerequisites to install SmartSim and SmartRedis are: GCC 5-9, 11, and 12 is recommended. There are known bugs with GCC 10. -.. _GPU Support: GPU Support =========== @@ -80,17 +79,13 @@ Supported Versions Native support for various machine learning libraries and their -versions is dictated by our dependency on RedisAI_ 1.2.7. Users -can also select RedisAI 1.2.5 (though that also limits -the version of the ML libraries). +versions is dictated by our dependency on RedisAI_ 1.2.7. +------------------+----------+-------------+---------------+ | RedisAI | PyTorch | Tensorflow | ONNX Runtime | +==================+==========+=============+===============+ | 1.2.7 (default) | 1.11.0 | 2.8.0 | 1.11.1 | +------------------+----------+-------------+---------------+ -| 1.2.5 | 1.9.0 | 2.6.0 | 1.9.0 | -+------------------+----------+-------------+---------------+ TensorFlow_ 2.0 and Keras_ are supported through `graph freezing`_. @@ -304,5 +299,3 @@ Build the SmartRedis library ============================ .. include:: ../../smartredis/doc/install/lib.rst - - diff --git a/doc/installation_instructions/platform/olcf-summit.rst b/doc/installation_instructions/platform/olcf-summit.rst index bbc48f810..dfd7b9666 100644 --- a/doc/installation_instructions/platform/olcf-summit.rst +++ b/doc/installation_instructions/platform/olcf-summit.rst @@ -11,23 +11,28 @@ instructions to get a working SmartSim build with PyTorch and TensorFlow for GPU on Summit. Note that SmartSim and SmartRedis will be downloaded to the working directory from which these instructions are executed. +Note that the available PyTorch version (1.10.2) does not match +the one expected by RedisAI 1.2.7 (1.11): it is still compatible and should +work, but please open an issue on SmartSim's GitHub repo if you run +into problems. + .. code-block:: bash # setup Python and build environment - export ENV_NAME=smartsim-0.5.1 + export ENV_NAME=smartsim-0.6.0 git clone https://github.com/CrayLabs/SmartRedis.git smartredis git clone https://github.com/CrayLabs/SmartSim.git smartsim - conda config --prepend channels https://ftp.osuosl.org/pub/open-ce/1.4.1/ + conda config --prepend channels https://ftp.osuosl.org/pub/open-ce/1.6.1/ conda create --name $ENV_NAME -y python=3.9 \ git-lfs \ cmake \ make \ cudnn=8.1.1_11.2 \ cudatoolkit=11.2.2 \ - tensorflow=2.6.2 \ - libtensorflow=2.6.2 \ - pytorch=1.9.0 \ - torchvision=0.10.0 + tensorflow=2.8.1 \ + libtensorflow \ + pytorch=1.10.2 \ + torchvision=0.11.3 conda activate $ENV_NAME export CC=$(which gcc) export CXX=$(which g++) @@ -50,7 +55,7 @@ directory from which these instructions are executed. # install PyTorch and TensorFlow backend for the Orchestrator database. export Torch_DIR=/ccs/home/$USER/.conda/envs/$ENV_NAME/lib/python3.9/site-packages/torch/share/cmake/Torch/ export CFLAGS="$CFLAGS -I/ccs/home/$USER/.conda/envs/$ENV_NAME/lib/python3.9/site-packages/tensorflow/include" - export SMARTSIM_REDISAI=1.2.5 + export SMARTSIM_REDISAI=1.2.7 export Tensorflow_BUILD_DIR=/ccs/home/$USER/.conda/envs/$ENV_NAME/lib/python3.9/site-packages/tensorflow/ smart build --device=gpu --torch_dir $Torch_DIR --libtensorflow_dir $Tensorflow_BUILD_DIR -v diff --git a/doc/ml_features.rst b/doc/ml_features.rst new file mode 100644 index 000000000..51027e7ae --- /dev/null +++ b/doc/ml_features.rst @@ -0,0 +1,505 @@ +########### +ML Features +########### + +In this section, we illustrate features which +users are expected to use in HPC workloads, especially when +simulation and AI are required to interact. The topics are +explained through code snippets, +with code that goes beyond SmartSim and SmartRedis API +(e.g. code showing how to jit-script a PyTorch model): the +intention is that of showing *one* simple way of leveraging +a feature, but more optimized ways of using third-party +libraries may exist. + +Examples are written in Python, but the same +result can be achieved with any SmartRedis client (C, C++, +Fortran and Python). Please refer to SmartRedis API +for language-specific details. + +ML Model Deployment and Execution in the Database +=================================================== + +The combination of SmartSim and SmartRedis enables users +to store more than simple tensors on the database (DB). +In the upcoming subsections, we demonstrate how to use a +SmartRedis client to upload executable code, in the +form of ML model, scripts, and functions, to the DB. +Once store, the code can be executed using the SmartRedis client +methods and used to process tensors directly in the DB. +The tensors generated from running the stored code will also be stored +in the database and can be retrieved with standard SmartRedis ``Client.get_tensor()`` calls. + +SmartRedis offers two ways to upload serialized code +to the DB: from memory and from file. We will go through examples +demonstrating how to upload from each. We provide the following examples: + +- :ref:`TensorFlow and PyTorch `: Serialize a TensorFlow/Keras or PyTorch model, optionally + save it to file, upload it to the DB, then execute it on tensors stored on the DB. +- :ref:`TorchScript Functions `: Serialize TorchScript functions, optionally + save them to file, upload them to the DB, then execute them on tensors stored on the DB. +- :ref:`ONNX Runtime `: Convert a Scikit-Learn model to ONNX + format, upload it to the DB, then execute it on tensors stored on the DB. + + +.. note:: + In all examples, we will assume that a SmartSim ``Orchestator`` + is up and running, and that the code we will show is run as part + of a SmartSim-launched application ``Model``. + + +.. _ml_features_TF_PT: + +TensorFlow and PyTorch +---------------------- + +In this section, we will see how a TensorFlow/Keras or a PyTorch model +can be serialized using SmartSim's helper functions. +Once the model is serialized, we will use the SmartRedis client to upload it to the DB, +and execute it on data stored on the DB. +We will also see how the model can be optionally saved to file. The +workflow for TensorFlow and PyTorch is almost identical, but we provide +the code for each toolkit in a dedicated tab, for completeness. + +We begin by defining the ML model that we will use in both examples of +this section. + +.. tabs:: + + .. group-tab:: TensorFlow + + .. code-block:: python + + import numpy as np + from smartredis import Client + from tensorflow import keras + from smartsim.ml.tf import freeze_model + + model = keras.Sequential( + layers=[ + keras.layers.InputLayer(input_shape=(28, 28), name="input"), + keras.layers.Flatten(input_shape=(28, 28), name="flatten"), + keras.layers.Dense(128, activation="relu", name="dense"), + keras.layers.Dense(10, activation="softmax", name="output"), + ], + name="FCN", + ) + + # Compile model with optimizer + model.compile( + optimizer="adam", loss="sparse_categorical_crossentropy", metrics=["accuracy"] + ) + + .. group-tab:: PyTorch + + .. code-block:: python + + import io + + import numpy as np + import torch + import torch.nn as nn + import torch.nn.functional as F + from smartredis import Client + + # simple MNIST in PyTorch + class Net(nn.Module): + def __init__(self): + super(Net, self).__init__() + self.conv1 = nn.Conv2d(1, 32, 3, 1) + self.conv2 = nn.Conv2d(32, 64, 3, 1) + self.dropout1 = nn.Dropout(0.25) + self.dropout2 = nn.Dropout(0.5) + self.fc1 = nn.Linear(9216, 128) + self.fc2 = nn.Linear(128, 10) + + def forward(self, x): + x = self.conv1(x) + x = F.relu(x) + x = self.conv2(x) + x = F.relu(x) + x = F.max_pool2d(x, 2) + x = self.dropout1(x) + x = torch.flatten(x, 1) + x = self.fc1(x) + x = F.relu(x) + x = self.dropout2(x) + x = self.fc2(x) + output = F.log_softmax(x, dim=1) + return output + + # Instantiate the model + n = Net() + + +============================================================ +Serializing the model and uploading it to the DB from memory +============================================================ +Once the model is instantiated, it needs to be serialized to be uploaded +to the DB using the SmartRedis client. + +.. tabs:: + + .. group-tab:: TensorFlow + + As part of its :ref:`TensorFlow helper functions `, + SmartSim provides ``serialize_model()`` to serialize a TensorFlow or Keras + model. + + .. code-block:: python + + serialized_model, inputs, outputs = serialize_model(model) + + + Note that ``serialize_model()`` conveniently returns the model as bytestring + and the names of the input and output layers, which are now needed to upload the TensorFlow + model to the DB using ``Client.set_model()``. + We also use ``Client.put_tensor()`` to upload a batch of 20 synthetic MNIST samples to the DB. + + .. code-block:: python + + # Instantiate and connect SmartRedis client to communicate with DB + client = Client(cluster=False) + model_key = "mnist_cnn" + # Set device to CPU if GPU not available to DB + client.set_model( + model_key, serialized_model, "TF", device="GPU", inputs=inputs, outputs=outputs + ) + + + .. group-tab:: PyTorch + + PyTorch requires models to be `jit-traced `__. + The method ``torch.jit.save()`` can either store the model in memory or on file. Here, + we will keep it in memory as a bytestring. + + .. code-block:: python + + # Example input needed for jit tracing + example_forward_input = torch.rand(20, 1, 28, 28) + module = torch.jit.trace(n, mnist_images) + model_buffer = io.BytesIO() + torch.jit.save(module, model_buffer) + serialized_model = model_buffer.getvalue() + + Now that we have the serialized model, we can upload it to the DB using the ``Client.set_model()``. + + + We also use ``Client.put_tensor()`` to upload a batch of 20 synthetic MNIST samples to the DB. + + .. code-block:: python + + # Instantiate and connect SmartRedis client to communicate with DB + client = Client(cluster=False) + model_key = "mnist_cnn" + # Set device to CPU if GPU not available to DB + client.set_model(model_key, serialized_model, "TORCH", device="GPU") + + +For details about ``Client.set_model()``, please +refer to :ref:`SmartRedis API `. + + +===================================================== +Saving the model to a file and uploading it to the DB +===================================================== + +Once the model is compiled, it can be serialized and stored on the filesystem. This is +useful if the model has to be used at a later time. Once the model is saved to file, +it can be uploaded to the DB using the SmartRedis client. + +.. tabs:: + + .. group-tab:: TensorFlow + + As part of its :ref:`TensorFlow helper functions `, + SmartSim provides ``freeze_model()`` to serialize a TensorFlow or Keras + model and save it to file. In this example, the file will be named ``mnist.pb``. + + .. code-block:: python + + filename = "mnist.pb" + model_path, inputs, outputs = freeze_model(model, '.', filename) + + + Note that ``freeze_model()`` conveniently returns the path to the serialized model file, + and the names of the input and output layers, which are noew needed to upload the TensorFlow + model to the DB using ``Client.set_model_from_file()``. We also use + ``Client.put_tensor()`` to upload a synthetic MNIST sample to the DB. + + + .. code-block:: python + + client = Client(cluster=False) + model_key = "mnist_cnn" + client.set_model_from_file( + model_key, model_path, "TF", device="GPU", inputs=inputs, outputs=outputs + ) + + + .. group-tab:: PyTorch + + PyTorch requires models to be `jit-traced `__. + The method ``torch.jit.save()`` can either store the model in memory or on file. Here, + we will save it to a file located at ``./traced_model.pt``. + + .. code-block:: python + + # Example input needed for jit tracing + example_forward_input = torch.rand(20, 1, 28, 28) + module = torch.jit.trace(n, example_forward_input) + model_path = "./traced_model.pt" + torch.jit.save(module, modelpath) + + + Now that we have the serialized model, we can upload it to the DB using + ``Client.set_model_from_file()`` method. + + .. code-block:: python + + client = Client(cluster=False) + model_key = "mnist_cnn" + + client.set_model_from_file(model_key, model_path, "TORCH", device="CPU") + + +For details about ``Client.set_model_from_file()``, please +refer to :ref:`SmartRedis API `. + +=============================================== +Executing the model on tensors stored in the DB +=============================================== + +Now that the model is available for execution on the DB, we use the SmartRedis client +to upload a tensor representing a batch of 20 synthetic MNIST images. + +.. tabs:: + + .. group-tab:: TensorFlow + + .. code-block:: python + + # 20 samples of "image" data + mnist_images = np.random.rand(20, 28, 28, 1).astype(np.float32) + # client was instantiated previously + client.put_tensor("mnist_images", mnist_image) + + + .. group-tab:: PyTorch + + .. code-block:: python + + + # 20 samples of "image" data + mnist_images = torch.rand(20, 1, 28, 28) + # client was instantiated previously + client.put_tensor("mnist_images", mnist_images.numpy()) + + +Now we can use ``Client.run_model()`` to execute the model on the data we have +just stored and ``Client.get_tensor()`` to download the output of the model execution. +Notice that, for this part, the code is identical for models uploaded from file and from memory, and +with TensorFlow or PyTorch backends. + +.. code-block:: python + + client.run_model(model_key, inputs=["mnist_imagse"], outputs=["mnist_output"]) + output = client.get_tensor("mnist_output") + + +For details about ``Client.run_model()``, please +refer to :ref:`SmartRedis API `. + +.. _ml_features_torchscript: + +TorchScript Functions +--------------------- +Instead of Neural Networks, or, in general, Machine Learning models, it is +possible to upload to the DB (collections of) functions which can be used e.g. +to perform pre- or post-processing operations on tensors stored on the DB. + +Since the functions are going to be stored as TorchScript modules, they + +- need to be jit-traceable +- can use ``torch`` as a built-in module +- can **not** import modules + +In this section we will see how to + +- save a collection of functions to a script file, upload them to the DB, + and execute them on tensors stored on the DB. +- define and upload a function on-the-fly from a Python script and + execute it on tensors stored on the DB. + + +================================================================= +Uploading a script containing a collection of functions to the DB +================================================================= + +The easiest way of defining and storing functions on the DB is to create a +dedicated file. In that file, we can define functions which will be callable +through the SmartRedis client, but also from other functions in the +same file. A typical script file would look like this: + +.. code-block:: python + + def rescale(tensor, mu: float, sigma: float): + mean = tensor.mean() + std = tensor.std() + + normalized = (tensor-mean)/std + return tensor*sigma + mu + + def shift_y_to_x(x, y): + mu_x = x.mean() + sigma_x = x.std() + y_rescaled = rescale(y, mu_x, sigma_x) + + return y_rescaled + +In the script, we defined ``shift_y_to_x``, +a function which returns a modified copy of a tensor ``y``, +which matches the statistical distribution of the tensor ``x``. +Notice that we are not importing ``torch`` in the script, as it will +be recognized as a built-in by the TorchScript compiler. Because +of the discrepancy between TorchScript's and Python's syntaxes, TorchScript +scripts cannot be run as standalone Python scripts. + +Here is the code which allows us to run the function ``shift_y_to_x`` on +tensors stored in the DB. We will assume that the above script is stored +as ``"./shift.script"``. + +.. code-block:: python + + import numpy as np + from smartredis import Client + + # Generate tensors according to two different random distributions + x = np.random.rand(100, 100).astype(np.float32) + y = np.random.rand(100, 100).astype(np.float32) * 2 + 10 + + # Instantiate and connect SmartRedis client + client = Client(cluster=False) + + # Upload tensors to DB + client.put_tensor("X_rand", x) + client.put_tensor("Y_rand", y) + + # Upload script containing functions to DB + client.set_script_from_file("shifter", "./shift.script", device="CPU") + # Run the function ``shift_y_to_x`` on ``X_rand`` and ``Y_rand`` + client.run_script("shifter", "shift_y_to_x", inputs=["X_rand", "Y_rand"], outputs=["Y_scaled"]) + # Download output + y_scaled = client.get_tensor("Y_scaled") + + +In the above code, we used ``Client.put_tensor()`` to upload tensors to the DB, and +``Client.set_script_from_file()`` to upload the script containing the collection of functions. +We then used ``Client.run_script()`` to run the function ``shift_y_to_x`` on the stored +tensors, and downloaded the result with ``Client.get_tensor()``. + +For details about ``Client.set_script_from_file()`` and ``Client.run_script()``, please +refer to :ref:`SmartRedis API `. + + +========================================= +Uploading a function to the DB on-the-fly +========================================= + +Simpler functions (or functions that do not require calling other user-defined +or imported functions), can be defined inline and uploaded to the DB using the SmartRedis client. +For example: + +.. code-block:: python + + import numpy as np + from smartredis import Client + + def normalize(X): + """Simple function to normalize a tensor""" + mean = X.mean() + std = X.std() + + return (X-mean)/std + + # Generate random tensor + x = np.random.rand(100, 100).astype(np.float32) * 2 + 10 + + # Instantiate and connect SmartRedis client + client = Client(cluster=False) + + # Upload tensor to DB + client.put_tensor("X_rand", x) + + # Upload function to DB, ``normalizer`` is the name of the collection + # of functions containing the function ``normalize`` only. It mimics + # the way `set_script` works. + client.set_function("normalizer", normalize) + # Run the function ``normalize`` on ``X_rand`` + client.run_script("normalizer", "normalize", inputs=["X_rand"], outputs=["X_norm"]) + # Download output + x_norm = client.get_tensor("X_norm") + +Notice that the key ``"normalizer"`` represents the script containing the function (similar to +``"shifter"`` in the previous example), while the function name is ``"normalize"``. + + +For details about ``Client.set_function()`` and ``Client.run_script()``, please +refer to :ref:`SmartRedis API `. + +.. _ml_features_ONNX: + +ONNX Runtime +------------ + +In the following example, we will see how, thanks to the ONNX runtime, +Machine Learning and Data Analysis functions defined in +Scikit-Learn can be serialized and then put on the DB using the SmartRedis client. + +We start by defining a Scikit-Learn ``LinearRegression`` model and serialize it, +keeping it into memory. + +.. code-block:: python + + import numpy as np + from skl2onnx import to_onnx + from sklearn.linear_model import LinearRegression + from smartredis import Client + + def build_lin_reg(): + """Generates sklearn linear regression model and serialize it""" + x = np.array([[1.0], [2.0], [6.0], [4.0], [3.0], [5.0]]).astype(np.float32) + y = np.array([[2.0], [3.0], [7.0], [5.0], [4.0], [6.0]]).astype(np.float32) + + linreg = LinearRegression() + linreg.fit(x, y) + linreg = to_onnx(linreg, x.astype(np.float32), target_opset=13) + return linreg.SerializeToString() + + linreg = build_lin_reg() + +Once the model is serialized, we can use ``Client.set_model()`` to upload it +to the DB. + +.. code-block:: python + + # connect a client to the database + client = Client(cluster=False) + client.set_model("linreg", linreg, "ONNX", device="GPU") + + +Finally, we can upload a tensor to the DB using ``Client.put_tensor()``, run the +stored model on it using ``Client.run_model()``, and download the output calling +``Client.get_tensor()``. + +.. code-block:: python + + # linreg test + X = np.array([[1.0], [2.0], [3.0], [4.0], [5.0]]).astype(np.float32) + client.put_tensor("X", X) + client.run_model("linreg", inputs=["X"], outputs=["Y"]) + Y = client.get_tensor("Y") + + +For details about ``Client.run_model()``, please +refer to :ref:`SmartRedis API `. diff --git a/doc/orchestrator.rst b/doc/orchestrator.rst index cf3cabc96..456d9a814 100644 --- a/doc/orchestrator.rst +++ b/doc/orchestrator.rst @@ -41,7 +41,7 @@ be used to execute the AI models, and Torchscript code on data stored within it. Users do not need to know how the data is stored in a clustered configuration and can address the cluster with the SmartRedis clients like a single block of memory using simple put/get semantics in SmartRedis. SmartRedis will ensure that data -is evenly distributed amoungst all nodes in the cluster. +is evenly distributed amongst all nodes in the cluster. The cluster deployment is optimal for high data throughput scenarios such as online analysis, training and processing. @@ -116,7 +116,7 @@ occupied by the database. .. note:: Pinning _only_ affects the co-located deployment because both the application and the database - are sharing the same compute node. For the clustered deployment, a shard occupies the entirerty + are sharing the same compute node. For the clustered deployment, a shard occupies the entirety of the node. Redis @@ -165,3 +165,475 @@ A full example of configuring KeyDB to run in SmartSim is shown below export REDIS_CONF=/path/to/keydb.conf # run smartsim workload + +Multiple Orchestrator Example +============================= +SmartSim offers functionality to automate the deployment of multiple +databases, supporting workloads that require multiple +``Orchestrators`` for a ``Experiment``. For instance, a workload may consist of a +simulation with high inference performance demands (necessitating a co-located deployment), +along with an analysis and +visualization workflow connected to the simulation (requiring a standard orchestrator). +In the following example, we simulate a simple version of this use case. + +The example is comprised of two script files: + +* The :ref:`Application Script` +* The :ref:`Experiment Driver Script` + +**The Application Script Overview:** +In this example, the application script is a python file that +contains instructions to complete computational +tasks. Applications are not limited to Python +and can also be written in C, C++ and Fortran. +This script specifies creating a Python SmartRedis client for each +standard orchestrator and a colocated orchestrator. We use the +clients to request data from both standard databases, then +transfer the data to the colocated database. The application +file is launched by the experiment driver script +through a ``Model`` stage. + +**The Application Script Contents:** + +1. Connecting SmartRedis clients within the application to retrieve tensors + from the standard databases to store in a colocated database. Details in section: + :ref:`Initialize the Clients`. + +**The Experiment Driver Script Overview:** +The experiment driver script holds the stages of the workflow +and manages their execution through the ``Experiment`` API. +We initialize an Experiment +at the beginning of the Python file and use the ``Experiment`` to +iteratively create, configure and launch computational kernels +on the system through the `slurm` launcher. +In the driver script, we use the ``Experiment`` to create and launch a ``Model`` instance that +runs the application. + +**The Experiment Driver Script Contents:** + +1. Launching two standard Orchestrators with unique identifiers. Details in section: + :ref:`Launch Multiple Orchestrators`. +2. Launching the application script with a co-located database. Details in section: + :ref:`Initialize a Colocated Model`. +3. Connecting SmartRedis clients within the driver script to send tensors to standard Orchestrators + for retrieval within the application. Details in section: + :ref:`Create Client Connections to Orchestrators`. + +Setup and run instructions can be found :ref:`here` + +The Application Script +---------------------- +Applications interact with the databases +through a SmartRedis client. +In this section, we write an application script +to demonstrate how to connect SmartRedis +clients in the context of multiple +launched databases. Using the clients, we retrieve tensors +from two databases launched in the driver script, then store +the tensors in the colocated database. + +.. note:: + The Experiment must be started to use the Orchestrators within the + application script. Otherwise, it will fail to connect. + Find the instructions on how to launch :ref:`here` + +To begin, import the necessary packages: + +.. literalinclude:: ../tutorials/getting_started/multi_db_example/application_script.py + :language: python + :linenos: + :lines: 1-3 + +Initialize the Clients +^^^^^^^^^^^^^^^^^^^^^^ +To establish a connection with each database, +we need to initialize a new SmartRedis client for each +``Orchestrator``. + +Step 1: Initialize ConfigOptions +"""""""""""""""""""""""""""""""" +Since we are launching multiple databases within the experiment, +the SmartRedis ``ConfigOptions`` object is required when initializing +a client in the application. +We use the ``ConfigOptions.create_from_environment()`` +function to create three instances of ``ConfigOptions``, +with one instance associated with each launched ``Orchestrator``. +Most importantly, to associate each launched Orchestrator to a ConfigOptions object, +the ``create_from_environment()`` function requires specifying the unique database identifier +argument named `db_identifier`. + +For the single-sharded database: + +.. literalinclude:: ../tutorials/getting_started/multi_db_example/application_script.py + :language: python + :linenos: + :lines: 5-6 + +For the multi-sharded database: + +.. literalinclude:: ../tutorials/getting_started/multi_db_example/application_script.py + :language: python + :linenos: + :lines: 10-11 + +For the colocated database: + +.. literalinclude:: ../tutorials/getting_started/multi_db_example/application_script.py + :language: python + :linenos: + :lines: 15-16 + +Step 2: Initialize the Client Connections +""""""""""""""""""""""""""""""""""""""""" +Now that we have three ``ConfigOptions`` objects, we have the +tools necessary to initialize three SmartRedis clients and +establish a connection with the three databases. +We use the SmartRedis ``Client`` API to create the client instances by passing in +the ``ConfigOptions`` objects and assigning a `logger_name` argument. + +Single-sharded database: + +.. literalinclude:: ../tutorials/getting_started/multi_db_example/application_script.py + :language: python + :linenos: + :lines: 7-8 + +Multi-sharded database: + +.. literalinclude:: ../tutorials/getting_started/multi_db_example/application_script.py + :language: python + :linenos: + :lines: 12-13 + +Colocated database: + +.. literalinclude:: ../tutorials/getting_started/multi_db_example/application_script.py + :language: python + :linenos: + :lines: 17-18 + +Retrieve Data and Store Using SmartRedis Client Objects +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +To confirm a successful connection to each database, we will retrieve the tensors +that we plan to store in the python driver script. After retrieving, we +store both tensors in the colocated database. +The ``Client.get_tensor()`` method allows +retrieval of a tensor. It requires the `name` of the tensor assigned +when sent to the database via ``Client.put_tensor()``. + +.. literalinclude:: ../tutorials/getting_started/multi_db_example/application_script.py + :language: python + :linenos: + :lines: 20-26 + +Later, when you run the experiment driver script the following output will appear in ``tutorial_model.out`` +located in ``getting-started-multidb/tutorial_model/``:: + + Model: single shard logger@00-00-00:The single sharded db tensor is: [1 2 3 4] + Model: multi shard logger@00-00-00:The multi sharded db tensor is: [5 6 7 8] + +This output showcases that we have established a connection with multiple Orchestrators. + +Next, take the tensors retrieved from the standard deployment databases and +store them in the colocated database using ``Client.put_tensor(name, data)``. + +.. literalinclude:: ../tutorials/getting_started/multi_db_example/application_script.py + :language: python + :linenos: + :lines: 28-30 + +Next, check if the tensors exist in the colocated database using ``Client.poll_tensor()``. +This function queries for data in the database. The function requires the tensor name (`name`), +how many milliseconds to wait in between queries (`poll_frequency_ms`), +and the total number of times to query (`num_tries`): + +.. literalinclude:: ../tutorials/getting_started/multi_db_example/application_script.py + :language: python + :linenos: + :lines: 32-37 + +The output will be as follows:: + + Model: colo logger@00-00-00:The colocated db has tensor_1: True + Model: colo logger@00-00-00:The colocated db has tensor_2: True + +The Experiment Driver Script +---------------------------- +To run the previous application, we must define workflow stages within a workload. +Defining workflow stages requires the utilization of functions associated +with the ``Experiment`` object. The Experiment object is intended to be instantiated +once and utilized throughout the workflow runtime. +In this example, we instantiate an ``Experiment`` object with the name ``getting-started-multidb``. +We setup the SmartSim ``logger`` to output information from the Experiment. + +.. literalinclude:: ../tutorials/getting_started/multi_db_example/multidb_driver.py + :language: python + :linenos: + :lines: 1-10 + +Launch Multiple Orchestrators +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +In the context of this ``Experiment``, it's essential to create and launch +the databases as a preliminary step before any other components since +the application script requests tensors from the launched databases. + +We aim to showcase the multi-database automation capabilities of SmartSim, so we +create two databases in the workflow: a single-sharded database and a +multi-sharded database. + +Step 1: Initialize Orchestrators +"""""""""""""""""""""""""""""""" +To create an database, utilize the ``Experiment.create_database()`` function. +The function requires specifying a unique +database identifier argument named `db_identifier` to launch multiple databases. +This step is necessary to connect to databases outside of the driver script. +We will use the `db_identifier` names we specified in the application script. + +For the single-sharded database: + +.. literalinclude:: ../tutorials/getting_started/multi_db_example/multidb_driver.py + :language: python + :linenos: + :lines: 12-14 + +For the multi-sharded database: + +.. literalinclude:: ../tutorials/getting_started/multi_db_example/multidb_driver.py + :language: python + :linenos: + :lines: 16-18 + +.. note:: + Calling ``exp.generate()`` will create two subfolders + (one for each Orchestrator created in the previous step) + whose names are based on the db_identifier of that Orchestrator. + In this example, the Experiment folder is + named ``getting-started-multidb/``. Within this folder, two Orchestrator subfolders will + be created, namely ``single_shard_db_identifier/`` and ``multi_shard_db_identifier/``. + +Step 2: Start Databases +""""""""""""""""""""""" +Next, to launch the databases, +pass the database instances to ``Experiment.start()``. + +.. literalinclude:: ../tutorials/getting_started/multi_db_example/multidb_driver.py + :language: python + :linenos: + :lines: 20-21 + +The ``Experiment.start()`` function launches the ``Orchestrators`` for use within the workflow. In other words, the function +deploys the databases on the allocated compute resources. + +.. note:: + By setting `summary=True`, SmartSim will print a summary of the + experiment before it is launched. After printing the experiment summary, + the experiment is paused for 10 seconds giving the user time to + briefly scan the summary contents. If we set `summary=False`, then the experiment + would be launched immediately with no summary. + +Create Client Connections to Orchestrators +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +The SmartRedis ``Client`` object contains functions that manipulate, send, and receive +data within the database. Each database has a single, dedicated SmartRedis ``Client``. +Begin by initializing a SmartRedis ``Client`` object per launched database. + +To create a designated SmartRedis ``Client``, you need to specify the address of the target +running database. You can easily retrieve this address using the ``Orchestrator.get_address()`` function. + +For the single-sharded database: + +.. literalinclude:: ../tutorials/getting_started/multi_db_example/multidb_driver.py + :language: python + :linenos: + :lines: 23-24 + +For the multi-sharded database: + +.. literalinclude:: ../tutorials/getting_started/multi_db_example/multidb_driver.py + :language: python + :linenos: + :lines: 25-26 + +Store Data Using Clients +^^^^^^^^^^^^^^^^^^^^^^^^ +In the application script, we retrieved two NumPy tensors. +To support the apps functionality, we will create two +NumPy arrays in the python driver script and send them to the a database. To +accomplish this, we use the ``Client.put_tensor()`` function with the respective +database client instances. + +For the single-sharded database: + +.. literalinclude:: ../tutorials/getting_started/multi_db_example/multidb_driver.py + :language: python + :linenos: + :lines: 28-31 + +For the multi-sharded database: + +.. literalinclude:: ../tutorials/getting_started/multi_db_example/multidb_driver.py + :language: python + :linenos: + :lines: 33-36 + +Lets check to make sure the database tensors do not exist in the incorrect databases: + +.. literalinclude:: ../tutorials/getting_started/multi_db_example/multidb_driver.py + :language: python + :linenos: + :lines: 38-42 + +When you run the experiment, the following output will appear:: + + 00:00:00 system.host.com SmartSim[#####] INFO The multi shard array key exists in the incorrect database: False + 00:00:00 system.host.com SmartSim[#####] INFO The single shard array key exists in the incorrect database: False + +Initialize a Colocated Model +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +In the next stage of the experiment, we +launch the application script with a co-located database +by configuring and creating +a SmartSim colocated ``Model``. + +Step 1: Configure +""""""""""""""""" +You can specify the run settings of a model. +In this experiment, we invoke the Python interpreter to run +the python script defined in section: :ref:`The Application Script`. +To configure this into a ``Model``, we use the ``Experiment.create_run_settings()`` function. +The function returns a ``RunSettings`` object. +When initializing the RunSettings object, +we specify the path to the application file, +`application_script.py`, for +``exe_args``, and the run command for ``exe``. + +.. literalinclude:: ../tutorials/getting_started/multi_db_example/multidb_driver.py + :language: python + :linenos: + :lines: 44-45 + +.. note:: + You will have to change the `exe_args` argument to the path of the application script + on your machine to run the example. + +With the ``RunSettings`` instance, +configure the the distribution of computational tasks (``RunSettings.set_nodes()``) and the number of instances +the script is execute on each node (``RunSettings.set_tasks_per_node()``). In this +example, we specify to SmartSim that we intend to execute the script once on a single node. + +.. literalinclude:: ../tutorials/getting_started/multi_db_example/multidb_driver.py + :language: python + :linenos: + :lines: 46-48 + +Step 2: Initialize +"""""""""""""""""" +Next, create a ``Model`` instance using the ``Experiment.create_model()``. +Pass the ``model_settings`` object as an argument +to the ``create_model()`` function and assign to the variable ``model``. + +.. literalinclude:: ../tutorials/getting_started/multi_db_example/multidb_driver.py + :language: python + :linenos: + :lines: 49-50 + +Step 2: Colocate +"""""""""""""""" +To colocate the model, use the ``Model.colocate_db_uds()`` function to +Colocate an Orchestrator instance with this Model over +a Unix domain socket connection. + +.. literalinclude:: ../tutorials/getting_started/multi_db_example/multidb_driver.py + :language: python + :linenos: + :lines: 51-52 + +This method will initialize settings which add an unsharded +database to this Model instance. Only this Model will be able +to communicate with this colocated database by using the loopback TCP interface. + +Step 3: Start +""""""""""""" +Next, launch the colocated model instance using the ``Experiment.start()`` function. + +.. literalinclude:: ../tutorials/getting_started/multi_db_example/multidb_driver.py + :language: python + :linenos: + :lines: 53-54 + +.. note:: + We set `block=True`, + so that ``Experiment.start()`` waits until the last Model has finished + before returning: it will act like a job monitor, letting us know + if processes run, complete, or fail. + +Cleanup Experiment +^^^^^^^^^^^^^^^^^^ +Finally, use the ``Experiment.stop()`` function to stop the database instances. Print the +workflow summary with ``Experiment.summary()``. + +.. literalinclude:: ../tutorials/getting_started/multi_db_example/multidb_driver.py + :language: python + :linenos: + :lines: 56-59 + +When you run the experiment, the following output will appear:: + + 00:00:00 system.host.com SmartSim[#####]INFO + | | Name | Entity-Type | JobID | RunID | Time | Status | Returncode | + |----|------------------------------|---------------|-------------|---------|---------|-----------|--------------| + | 0 | colo_model | Model | 1556529.5 | 0 | 1.7437 | Completed | 0 | + | 1 | single_shard_db_identifier_0 | DBNode | 1556529.3 | 0 | 68.8732 | Cancelled | 0 | + | 2 | multi_shard_db_identifier_0 | DBNode | 1556529.4+2 | 0 | 45.5139 | Cancelled | 0 | + +How to Run the Example +---------------------- +Below are the steps to run the experiment. Find the +:ref:`experiment source code` +and :ref:`application source code` +below in the respective subsections. + +.. note:: + The example assumes that you have already installed and built + SmartSim and SmartRedis. Please refer to Section :ref:`Basic Installation` + for further details. For simplicity, we assume that you are + running on a SLURM-based HPC-platform. Refer to the steps below + for more details. + +Step 1 : Setup your directory tree + Your directory tree should look similar to below:: + + SmartSim/ + SmartRedis/ + Multi-db-example/ + application_script.py + experiment_script.py + + You can find the application and experiment source code in subsections below. + +Step 2 : Install and Build SmartSim + This example assumes you have installed SmartSim and SmartRedis in your + Python environment. We also assume that you have built SmartSim with + the necessary modules for the machine you are running on. + +Step 3 : Change the `exe_args` file path + When configuring the colocated model in `experiment_script.py`, + we pass the file path of `application_script.py` to the `exe_args` argument + on line 33 in :ref:`experiment_script.py`. + Edit this argument to the file path of your `application_script.py` + +Step 4 : Run the Experiment + Finally, run the experiment with ``python experiment_script.py``. + + +Application Source Code +^^^^^^^^^^^^^^^^^^^^^^^ +.. literalinclude:: ../tutorials/getting_started/multi_db_example/application_script.py + :language: python + :linenos: + +Experiment Source Code +^^^^^^^^^^^^^^^^^^^^^^ +.. literalinclude:: ../tutorials/getting_started/multi_db_example/multidb_driver.py + :language: python + :linenos: \ No newline at end of file diff --git a/doc/overview.rst b/doc/overview.rst index 3c3e36691..3ef046bb0 100644 --- a/doc/overview.rst +++ b/doc/overview.rst @@ -59,8 +59,8 @@ deploying HPC workloads alongside an in-memory database: Redis. The key features of the IL are: - An API to start, monitor, and stop HPC jobs from Python or from a Jupyter notebook. - - Automated deployment of in-memory data staging (Redis) and computational - storage (RedisAI). + - Automated deployment of in-memory data staging (`Redis `_) and computational + storage (`RedisAI `_). - Programmatic launches of batch and in-allocation jobs on PBS, Slurm, LSF, and Cobalt systems. - Creating and configuring ensembles of workloads with isolated communication channels. diff --git a/doc/requirements-doc.txt b/doc/requirements-doc.txt index 643d17992..38d9c8052 100644 --- a/doc/requirements-doc.txt +++ b/doc/requirements-doc.txt @@ -1,10 +1,14 @@ -sphinx==4.4.0 -breathe==4.31.0 -sphinx-book-theme==0.2.0 +Sphinx==6.2.1 +breathe==4.35.0 sphinx-fortran==1.1.1 -nbsphinx>=0.8.2 -docutils==0.17 -torch==1.7.1 -tensorflow==2.5.2 +sphinx-book-theme==1.0.1 +sphinx-copybutton==0.5.2 +sphinx-tabs==3.4.4 +nbsphinx==0.9.3 +docutils==0.18.1 +torch==1.11.0 +tensorflow==2.8.1 ipython -jinja2==3.0.3 \ No newline at end of file +jinja2==3.1.2 +protobuf +numpy \ No newline at end of file diff --git a/doc/smartdashboard.rst b/doc/smartdashboard.rst new file mode 100644 index 000000000..532fa6db0 --- /dev/null +++ b/doc/smartdashboard.rst @@ -0,0 +1,7 @@ + +************** +SmartDashboard +************** + +.. include:: ../smartdashboard/doc/overview.rst + :start-line: 4 \ No newline at end of file diff --git a/doc/smartsim_zoo.rst b/doc/smartsim_zoo.rst index ce39fbaaa..1ce803bf6 100644 --- a/doc/smartsim_zoo.rst +++ b/doc/smartsim_zoo.rst @@ -1,51 +1,55 @@ -********************* +##################### Contributing Examples -********************* +##################### .. _smartsim_zoo: +========================= What Is the SmartSim Zoo? -######################### +========================= Given that SmartSim is a community developed and maintained project, we have -introduced a `SmartSim Example Zoo `_ +introduced a `SmartSim Example Zoo `__ that contains CrayLabs and user contributed examples of using SmartSim for various simulation and machine learning applications. +-------------------------------------- The Two Categories of the SmartSim Zoo -************************************** +-------------------------------------- 1. SmartSim Deployments (running SmartSim on various HPC Systems) * The source code for the repository serves the purpose of showing diverse - * examples of how to get SmartSim running on - different HPC Systems. If you are looking for working examples on a specific - machine, then the source code in the SmartSim-Zoo repository is for you. The - SmartSim development team strives to keep these examples updated with each - release so that users will always have robust examples for their needs. + + * examples of how to get SmartSim running on different HPC Systems. + If you are looking for working examples on a specific machine, then the source + code in the SmartSim-Zoo repository is for you. The + SmartSim development team strives to keep these examples updated with each + release so that users will always have robust examples for their needs. 2. SmartSim Applications (completed projects that use SmartSim) * The README for the repository describes some of the larger applications of * SmartSim. These examples fall under two categories: - examples by paper and examples by simulation model. The examples by paper are - based on existing research papers, and the examples by simulation models are - integrations of SmartSim with existing simulation models. + examples by paper and examples by simulation model. The examples by paper are + based on existing research papers, and the examples by simulation models are + integrations of SmartSim with existing simulation models. +================= How To Contribute -################# +================= We support, encourage, and welcome all contributions to the `SmartSim Zoo -`_ repository. Instructions for +`__ repository. Instructions for contributing examples varies whether you are contributing a SmartSim deployment or a SmartSim application. 1. Contributing SmartSim Deployment Examples * For contributing examples of SmartSim running on a HPC System, we ask that - you include a description and all references to code and relevant previous - implementations or open source code that the work is based on for the benefit - of anyone who would like to try out your example. + you include a description and all references to code and relevant previous + implementations or open source code that the work is based on for the benefit + of anyone who would like to try out your example. 2. Contributing SmartSim Application Examples @@ -60,12 +64,13 @@ or a SmartSim application. 7. Contact information 8. Citation +================= Existing Examples -################# +================= The subsequent tables summarize the examples provided in the SmartSim Zoo. You can find a more detailed description of each example in the `SmartSim Zoo -`_. +`__. .. list-table:: SmartSim Deployment Examples :widths: 50 100 @@ -95,33 +100,34 @@ can find a more detailed description of each example in the `SmartSim Zoo - Links * - DeepDriveMD - CrayLabs, Argonne National Lab, Oak Ridge National Lab - - `Implementation `_ `Original Paper `_ + - `Implementation `__ `Original Paper `__ * - TensorFlowFoam - CrayLabs, Argonne National Lab - - `Implementation `_ `Original Paper `_ + - `Implementation `__ `Original Paper `__ * - ML-EKE - CrayLabs, NCAR, University of Victoria - - `Implementation `_ `Paper `_ + - `Implementation `__ `Original Paper `__ * - LAMMPS + SmartSim - CrayLabs, Sandia National Laboratories - - `Implementation `_ `Forked Model `_ + - `Implementation `__ `Forked Model `__ +---------------------------------------- Summary of SmartSim Application Examples -######################################## +---------------------------------------- * **DeepDriveMD:** Based on the original DeepDriveMD work, extended to -orchestrate complex workflows with coupled applications without using the -filesystem for exchanging information. + orchestrate complex workflows with coupled applications without using the + filesystem for exchanging information. * **TensorFlowFoam:** Uses TensorFlow inside of OpenFOAM simulations using -SmartSim. Displays SmartSim's capability to evaluate a machine learning model -from within a simulation with minimal external library code and minimal API -calls. + SmartSim. Displays SmartSim's capability to evaluate a machine learning model + from within a simulation with minimal external library code and minimal API + calls. * **ML-EKE:** Runs an ensemble of simulations all using the SmartSim -architecture to replace a parameterization (MEKE) within each global ocean -simulation (MOM6). + architecture to replace a parameterization (MEKE) within each global ocean + simulation (MOM6). * **LAMMPS + SmartSim:** Implementation of a ``SMARTSIM`` dump style which uses -the SmartRedis clients to stream data to an Orchestrator database created by -SmartSim. + the SmartRedis clients to stream data to an Orchestrator database created by + SmartSim. diff --git a/doc/sr_advanced_topics.rst b/doc/sr_advanced_topics.rst new file mode 100644 index 000000000..30da2c578 --- /dev/null +++ b/doc/sr_advanced_topics.rst @@ -0,0 +1,2 @@ + +.. include:: ../smartredis/doc/advanced_topics.rst \ No newline at end of file diff --git a/doc/sr_cpp_walkthrough.rst b/doc/sr_cpp_walkthrough.rst index d5282bd6d..db27189e0 100644 --- a/doc/sr_cpp_walkthrough.rst +++ b/doc/sr_cpp_walkthrough.rst @@ -3,9 +3,9 @@ C++ *** -This section presents examples that use the SmartRedis C++ API to -interact with the RedisAI tensor, model, and script data types. -Additionally, this section demonstrates an example of utilizing +This section presents examples that use the SmartRedis C++ API to +interact with the RedisAI tensor, model, and script data types. +Additionally, this section demonstrates an example of utilizing the SmartRedis ``DataSet`` API. @@ -16,7 +16,7 @@ the SmartRedis ``DataSet`` API. .. note:: - The C++ API examples are written to connect to a clustered database + The C++ API examples are written to connect to a clustered database or clustered SmartSim Orchestrator. Update the ``Client`` constructor ``cluster`` flag to `false` to connect to a single shard (single compute host) database. @@ -39,7 +39,7 @@ DataSets The C++ client can store and retrieve tensors and metadata in datasets. For further information about datasets, please refer to the :ref:`Dataset -section of the Data Structures documentation page `. +section of the Data Structures documentation page `. The code below shows how to store and retrieve tensors and metadata that belong to a ``DataSet``. @@ -69,8 +69,8 @@ executes a preprocessing script. Scripts ======= -The example in :ref:`SR CPP Models` shows how to store and use -a PyTorch script in the database with the C++ Client. +The example in :ref:`SR CPP Models` shows how to store and use +a PyTorch script in the database with the C++ Client. The script is stored as a file in the ``../../../common/mnist_data/`` path relative to the compiled executable. Note that this example also sets and @@ -104,4 +104,3 @@ source code is also shown. :linenos: :language: Python :lines: 15-20 - diff --git a/doc/sr_fortran_walkthrough.rst b/doc/sr_fortran_walkthrough.rst index d2cf78576..f01545db8 100644 --- a/doc/sr_fortran_walkthrough.rst +++ b/doc/sr_fortran_walkthrough.rst @@ -1,9 +1,10 @@ +.. _fortran_client_examples: + ******* Fortran ******* -.. _fortran_client_examples: In this section, examples are presented using the SmartRedis Fortran diff --git a/doc/sr_python_walkthrough.rst b/doc/sr_python_walkthrough.rst index 3c7a66106..00c5bd0f2 100644 --- a/doc/sr_python_walkthrough.rst +++ b/doc/sr_python_walkthrough.rst @@ -16,7 +16,7 @@ This section details the SmartRedis Python client to demonstrate its general use Tensors ======= -The Python client can send and receive tensors from the Redis database, +The Python client can send and receive tensors from the Redis database, where they are stored as RedisAI data structures. Additionally, Python client API functions involving tensor data are compatible with Numpy arrays and do not require other data types. @@ -32,14 +32,14 @@ Datasets The Python client can store and retrieve tensors and metadata in datasets. For further information about datasets, please refer to the :ref:`Dataset -section of the Data Structures documentation page `. +section of the Data Structures documentation page `. The code below shows how to store and retrieve tensors that belong to a ``DataSet``. .. literalinclude:: ../smartredis/examples/serial/python/example_put_get_dataset.py :language: python :linenos: - :lines: 26-52 + :lines: 27-51 Models ====== @@ -52,14 +52,14 @@ jit-traced PyTorch model can be used with the Python client library. .. literalinclude:: ../smartredis/examples/serial/python/example_model_torch.py :language: python :linenos: - :lines: 26-71 + :lines: 27-70 Users can set models from a file, as shown in the code below. .. literalinclude:: ../smartredis/examples/serial/python/example_model_file_torch.py :language: python :linenos: - :lines: 26-69 + :lines: 27-68 Scripts ======= @@ -67,7 +67,7 @@ Scripts Scripts are a way to store python-executable code in the database. The Python client can send scripts to the dataset from a file or directly from memory. -The code below illustrates how to avoid storing a function in an intermediate file. +The code below illustrates how to avoid storing a function in an intermediate file. With this technique, we can define and send a function to the database on the fly. .. literalinclude:: ../smartredis/examples/serial/python/example_script.py @@ -75,7 +75,7 @@ With this technique, we can define and send a function to the database on the fl :linenos: :lines: 26-66 -The code below shows how to set a script from a file. Running the script set from +The code below shows how to set a script from a file. Running the script set from the file uses the same API calls as in the example shown above. .. literalinclude:: ../smartredis/examples/serial/python/example_script_file.py @@ -89,4 +89,3 @@ looks like this: .. literalinclude:: ../smartredis/examples/serial/python/data_processing_script.txt :language: python :linenos: - diff --git a/doc/testing.rst b/doc/testing.rst index c04b613f8..bdaa473d7 100644 --- a/doc/testing.rst +++ b/doc/testing.rst @@ -66,23 +66,23 @@ of the tests located within the ``on_wlm`` directory. To run the ``on_wlm`` test suite, users will have to be on a system with one of the supported workload managers. Additionally, users will -need to obtain an allocation of **at least 3 nodes**. +need to obtain an allocation of **at least 4 nodes**. Examples of how to obtain allocations on systems with the launchers: .. code:: bash # for slurm (with srun) - salloc -N 3 -A account --exclusive -t 00:10:00 + salloc -N 4 -A account --exclusive -t 00:10:00 # for PBSPro (with aprun) - qsub -l select=3 -l place=scatter -l walltime=00:10:00 -q queue + qsub -l select=4 -l place=scatter -l walltime=00:10:00 -q queue # for Cobalt (with aprun) - qsub -n 3 -t 00:10:00 -A account -q queue -I + qsub -n 4 -t 00:10:00 -A account -q queue -I # for LSF (with jsrun) - bsub -Is -W 00:30 -nnodes 3 -P project $SHELL + bsub -Is -W 00:30 -nnodes 4 -P project $SHELL Values for queue, account, or project should be substituted appropriately. @@ -123,7 +123,7 @@ A full example on an internal SLURM system .. code:: bash - salloc -N 3 -A account --exclusive -t 03:00:00 + salloc -N 4 -A account --exclusive -t 03:00:00 export SMARTSIM_TEST_LAUNCHER=slurm export SMARTSIM_TEST_INTERFACE=ipogif0 export SMARTSIM_TEST_DEVICE=gpu diff --git a/doc/tutorials/training.rst b/doc/tutorials/training.rst deleted file mode 100644 index 495ebbc09..000000000 --- a/doc/tutorials/training.rst +++ /dev/null @@ -1,153 +0,0 @@ - -=============== -Online Training -=============== - -Online training provides the ability to use dynamic processes as your training -data set. In SmartSim, training data can be any process using the SmartRedis clients -to store data inside of a deployed `Orchestrator` database. - -SmartSim includes utilities to help with online training workflows in PyTorch and TensorFlow -In this example, we show how to use ``smartsim.ml.tf`` to train a Neural Network implemented -in TensorFlow and Keras. - -In particular, we will be using two classes: -- ``smartsim.ml.data.TrainingUploader`` which streamlines the uploading of samples and corresponding targets to the DB -- ``smartsim.ml.tf.DataGenerator`` which is a Keras ``Generator`` which can be used to train a DNN, -and will download the samples from the DB updating the training set at the end of each epoch. - -The SmartSim ``Experiment`` will consist in one mock simulation (the ``producer``) uploading samples, -and one application (the ``training_service``) downloading the samples to train a DNN. - -A richer example, entirely implemented in Python, is available as a Jupyter Notebook in the -``tutorials`` section of the SmartSim repository. An equivalent example using PyTorch -instead of TensorFlow will soon be available in the same directory. - - -Producing and uploading the samples ------------------------------------ - -.. _ml_training_producer_code: - -The first application in the workflow, the ``producer`` will upload batches of samples at regular intervals, -mimicking the behavior of an iterative simulation. - -Since the ``training_service`` will use a ``smartsim.ml.tf.DynamicDataGenerator`` two download the samples, their -keys need to follow a pre-defined format. Assuming that only one process in the simulation -uploads the data, this format is ``_``. And for targets -(which can also be integer labels), the key format is ``_``. Both ```` -and ```` are user-defined, and will need to be used to initialize the -``smartsim.ml.tf.DynamicDataGenerator`` object. - -Assuming the simulation is written in Python, then the code would look like - -.. code-block:: python - - from SmartRedis import Client - # simulation initialization code - client = Client(cluster=False, address=None) - - for iteration in range(num_iterations): - # simulation code producing two tensors, data_points - # and data_values - client.put_tensor(f"points_{iteration}", data_points) - client.put_tensor(f"values_{iteration}", data_values) - - -For simple simulations, this is sufficient. But if the simulation -uses MPI, then each rank could upload a portion of the data set. In that case, -the format for sample and target keys will be ``__`` -and ``__``, where ```` can be, e.g. -the MPI rank id. - - -Downloading the samples and training the model ----------------------------------------------- - -The second part of the workflow is the ``training_service``, an application that -downloads the data uploaded by the ``producer`` and uses them to train a ML model. -Most importantly, the ``training_service`` needs to keep looking for new samples, -and download them as they are available. The training data set size thus needs to grow at -each ``producer`` iteration. - -In Keras, a ``Sequence`` represents a data set and can be passed to ``model.fit()``. -The class ``smartsim.ml.tf.DynamicDataGenerator`` is a Keras ``Sequence``, which updates -its data set at the end of each training epoch, looking for newly produced batches of samples. -A current limitation of the TensorFlow training algorithm is that it does not take -into account changes of size in the data sets once the training has started, i.e. it is always -assumed that the training (and validation) data does not change during the training. To -overcome this limitation, we need to train one epoch at the time. Thus, -following what we defined in the :ref:`producer section `, -the ``training_service`` would look like - -.. code-block:: python - - from smartsim.ml.tf import DynamicDataGenerator - generator = DynamicDataGenerator( - sample_prefix="points", - target_prefix="value", - batch_size=32, - cluster=False) - - model = # some ML model - # model initialization - - for epoch in range(100): - model.fit(generator, - steps_per_epoch=None, - epochs=epoch+1, - initial_epoch=epoch, - batch_size=generator.batch_size, - verbose=2) - - -Again, this is enough for simple simulations. If the simulation uses MPI, -then the ``DynamicDataGenerator`` needs to know about the possible sub-indices. For example, -if the simulation runs 8 MPI ranks, the ``DynamicDataGenerator`` initialization will -need to be adapted as follows - -.. code-block:: python - - generator = DynamicDataGenerator( - sample_prefix="points", - target_prefix="value", - batch_size=32, - cluster=False, - uploader_ranks=8) - - -Launching the experiment ------------------------- - -To launch the ``producer`` and the ``training_service`` as models -within a SmartSim ``Experiment``, we can use the following code: - -.. code-block:: python - - from smartsim import Experiment - from smartsim.database import Orchestrator - - db = Orchestrator(port=6780) - exp = Experiment("online-training", launcher="local") - - # producer - producer_script = "producer.py" - settings = exp.create_run_settings("python", exe_args=producer_script) - uploader_model = exp.create_model("producer", settings, enable_key_prefixing=True) - uploader_model.attach_generator_files(to_copy=producer_script) - - # training_service - training_script = "training_service.py" - settings = exp.create_run_settings("python", exe_args=training_script) - trainer_model = exp.create_model("training_service", settings) - trainer_model.register_incoming_entity(uploader_model) - - exp.start(db, uploader_model, block=False, summary=False) - exp.start(trainer_model, block=True, summary=False) - - -Two lines require attention, as they are needed by the ``DataGenerator`` to work: - - ``uploader_model.enable_key_prefixing()`` will ensure that the ``producer`` prefixes all tensor keys with its name - - ``trainer_model.register_incoming_entity(uploader_model)`` enables the ``DataGenerator`` in - the ``training_service`` to know that it needs to download samples produced by the ``producer`` - diff --git a/docker/dev/Dockerfile b/docker/dev/Dockerfile index 8d4440ce7..6a5f82642 100644 --- a/docker/dev/Dockerfile +++ b/docker/dev/Dockerfile @@ -27,14 +27,15 @@ FROM ubuntu:20.04 LABEL maintainer="Cray Labs" + + ARG DEBIAN_FRONTEND="noninteractive" ENV TZ=US/Seattle -RUN useradd --system --create-home --shell /bin/bash -g root -G sudo craylabs - -RUN apt-get update \ +RUN useradd --system --create-home --shell /bin/bash -g root -G sudo craylabs && \ + apt-get update \ && apt-get install --no-install-recommends -y build-essential \ - git gcc make git-lfs wget libopenmpi-dev openmpi-bin \ + git gcc make git-lfs wget libopenmpi-dev openmpi-bin unzip \ python3-pip python3 python3-dev cmake \ && rm -rf /var/lib/apt/lists/* \ && ln -s /usr/bin/python3 /usr/bin/python @@ -45,16 +46,15 @@ RUN git clone https://github.com/CrayLabs/SmartRedis.git --branch develop --dept && cd smartredis \ && python -m pip install . - COPY . /home/craylabs/SmartSim RUN chown craylabs:root -R SmartSim USER craylabs -RUN cd SmartSim && SMARTSIM_SUFFIX=dev python -m pip install . +RUN cd SmartSim && SMARTSIM_SUFFIX=dev python -m pip install .[ml] -RUN python -m pip install smartsim[ml,dev]==0.5.1 jupyter jupyterlab matplotlib && \ +RUN export PATH=/home/craylabs/.local/bin:$PATH && \ echo "export PATH=/home/craylabs/.local/bin:$PATH" >> /home/craylabs/.bashrc && \ - export PATH=/home/craylabs/.local/bin:$PATH && \ + python -m pip install jupyter jupyterlab matplotlib && \ smart clobber && \ smart build --device cpu -v && \ chown craylabs:root -R /home/craylabs/.local && \ diff --git a/docker/docs/dev/Dockerfile b/docker/docs/dev/Dockerfile index a27ae03c1..eee809910 100644 --- a/docker/docs/dev/Dockerfile +++ b/docker/docs/dev/Dockerfile @@ -42,17 +42,25 @@ RUN ln -s /usr/bin/python3 /usr/bin/python COPY . /usr/local/src/SmartSim/ WORKDIR /usr/local/src/SmartSim/ -# Install docs dependencies and SmartSim -RUN python -m pip install -r doc/requirements-doc.txt && \ - NO_CHECKS=1 SMARTSIM_SUFFIX=dev python -m pip install . - # Install smartredis RUN git clone https://github.com/CrayLabs/SmartRedis.git --branch develop --depth=1 smartredis \ && cd smartredis \ && python -m pip install . \ && rm -rf ~/.cache/pip -RUN cd doc/tutorials/ && \ - ln -s ../../tutorials/* . +# Install smartdashboard +RUN git clone https://github.com/CrayLabs/SmartDashboard.git --branch develop --depth=1 smartdashboard \ + && cd smartdashboard \ + && python -m pip install . \ + && rm -rf ~/.cache/pip + +# Install docs dependencies and SmartSim +RUN python -m pip install -r doc/requirements-doc.txt \ + && NO_CHECKS=1 SMARTSIM_SUFFIX=dev python -m pip install . + +RUN mkdir -p doc/tutorials/ \ + && cd doc/tutorials/ \ + && rm -rf * \ + && ln -s ../../tutorials/* . RUN make docs diff --git a/docker/prod/Dockerfile b/docker/prod/Dockerfile index adb8593a4..628d9af60 100644 --- a/docker/prod/Dockerfile +++ b/docker/prod/Dockerfile @@ -35,7 +35,7 @@ ENV TZ=US/Seattle RUN useradd --system --create-home --shell /bin/bash -g root -G sudo craylabs && \ apt-get update \ && apt-get install --no-install-recommends -y build-essential \ - git gcc make git-lfs wget libopenmpi-dev openmpi-bin \ + git gcc make git-lfs wget libopenmpi-dev openmpi-bin unzip \ python3-pip python3 python3-dev cmake \ && rm -rf /var/lib/apt/lists/* \ && ln -s /usr/bin/python3 /usr/bin/python @@ -44,12 +44,11 @@ WORKDIR /home/craylabs COPY --chown=craylabs:root ./tutorials/ /home/craylabs/tutorials/ USER craylabs -RUN python -m pip install smartsim[ml]==0.5.1 jupyter jupyterlab matplotlib && \ +RUN export PATH=/home/craylabs/.local/bin:$PATH && \ echo "export PATH=/home/craylabs/.local/bin:$PATH" >> /home/craylabs/.bashrc && \ - export PATH=/home/craylabs/.local/bin:$PATH && \ + python -m pip install smartsim[ml]==0.6.0 jupyter jupyterlab matplotlib && \ smart build --device cpu -v && \ chown craylabs:root -R /home/craylabs/.local && \ rm -rf ~/.cache/pip -# remove non-jupyter notebook tutorials CMD ["/bin/bash", "-c", "PATH=/home/craylabs/.local/bin:$PATH /home/craylabs/.local/bin/jupyter lab --port 8888 --no-browser --ip=0.0.0.0"] diff --git a/pyproject.toml b/pyproject.toml index 6d952892b..60c33bee5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,7 +31,7 @@ build-backend = "setuptools.build_meta" [tool.black] line-length = 88 -target-version = ['py37', 'py38'] +target-version = ['py38', 'py39', 'py310'] exclude = ''' ( | \.egg @@ -51,6 +51,11 @@ exclude = ''' [tool.pytest.ini_options] log_cli = true log_cli_level = "debug" +markers = [ + "group_a: fast test subset a", + "group_b: fast test subset b", + "slow_tests: tests that take a long duration to complete", +] [tool.isort] # supress circular import warning @@ -102,6 +107,7 @@ module = [ "keras", "torch", "smartsim.ml.torch.*", # must solve/ignore inheritance issues + "watchdog", ] ignore_missing_imports = true ignore_errors = true diff --git a/setup.cfg b/setup.cfg index 297531e03..49419c7eb 100644 --- a/setup.cfg +++ b/setup.cfg @@ -57,11 +57,15 @@ setup_requires = include_package_data = True python_requires = >=3.8,<3.11 - - [options.packages.find] +include = + smartsim* exclude = .third-party tests doc smartredis + +[options.package_data] +smartsim._core.bin = + * diff --git a/setup.py b/setup.py index 74e85b461..66a534456 100644 --- a/setup.py +++ b/setup.py @@ -167,6 +167,7 @@ def has_ext_modules(_placeholder): "tqdm>=4.50.2", "filelock>=3.4.2", "protobuf~=3.20", + "watchdog>=3.0.0", ] # Add SmartRedis at specific version @@ -174,9 +175,9 @@ def has_ext_modules(_placeholder): extras_require = { "dev": [ - "black>=20.8b1", + "black==24.1a1", "isort>=5.6.4", - "pylint>=2.10.0", + "pylint>=2.10.0,<3", "pytest>=6.0.0", "pytest-cov>=2.10.1", "click==8.0.2", @@ -187,7 +188,7 @@ def has_ext_modules(_placeholder): "types-redis", "types-tabulate", "types-tqdm", - "types-tensorflow", + "types-tensorflow==2.12.0.9", "types-setuptools", ], # see smartsim/_core/_install/buildenv.py for more details @@ -199,10 +200,6 @@ def has_ext_modules(_placeholder): setup( version=smartsim_version, install_requires=deps, - packages=["smartsim"], - package_data={"smartsim": [ - "_core/bin/*", - ]}, cmdclass={ "build_py": SmartSimBuild, "install": InstallPlatlib, diff --git a/smartsim/_core/_cli/__main__.py b/smartsim/_core/_cli/__main__.py index 68d22d14f..47df07048 100644 --- a/smartsim/_core/_cli/__main__.py +++ b/smartsim/_core/_cli/__main__.py @@ -24,14 +24,30 @@ # 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. +import os import sys from smartsim._core._cli.cli import default_cli +from smartsim._core._cli.utils import SMART_LOGGER_FORMAT +from smartsim.error.errors import SmartSimCLIActionCancelled +from smartsim.log import get_logger + +logger = get_logger("Smart", fmt=SMART_LOGGER_FORMAT) def main() -> int: smart_cli = default_cli() - return smart_cli.execute(sys.argv) + exception_trace_back_msg = "SmartSim exited with the following exception info:" + + try: + return smart_cli.execute(sys.argv) + except SmartSimCLIActionCancelled as ssi: + logger.info(str(ssi)) + logger.debug(exception_trace_back_msg, exc_info=ssi) + except KeyboardInterrupt as e: + logger.info("SmartSim was terminated by user") + logger.debug(exception_trace_back_msg, exc_info=e) + return os.EX_OK if __name__ == "__main__": diff --git a/smartsim/_core/_cli/build.py b/smartsim/_core/_cli/build.py index 4218d79db..474d96c8a 100644 --- a/smartsim/_core/_cli/build.py +++ b/smartsim/_core/_cli/build.py @@ -65,11 +65,8 @@ def check_py_onnx_version(versions: Versioner) -> None: msg = ( "An onnx wheel is not available for " f"Python {py_version.major}.{py_version.minor}. " - "Instead consider using Python 3.8 or 3.9 with Onnx " + "Instead consider using Python 3.8 or 3.9 for ONNX 1.11 support" ) - if sys.platform == "linux": - msg += "1.2.5 or " - msg += "1.2.7." raise SetupError(msg) _check_packages_in_python_env( { @@ -343,8 +340,8 @@ def _format_incompatible_python_env_message( missing: t.Iterable[str], conflicting: t.Iterable[str] ) -> str: indent = "\n\t" - fmt_list: t.Callable[[str, t.Iterable[str]], str] = ( - lambda n, l: f"{n}:{indent}{indent.join(l)}" if l else "" + fmt_list: t.Callable[[str, t.Iterable[str]], str] = lambda n, l: ( + f"{n}:{indent}{indent.join(l)}" if l else "" ) missing_str = fmt_list("Missing", missing) conflict_str = fmt_list("Conflicting", conflicting) @@ -359,7 +356,9 @@ def _format_incompatible_python_env_message( ) -def execute(args: argparse.Namespace) -> int: +def execute( + args: argparse.Namespace, _unparsed_args: t.Optional[t.List[str]] = None, / +) -> int: verbose = args.v keydb = args.keydb device: _TDeviceStr = args.device @@ -419,7 +418,7 @@ def execute(args: argparse.Namespace) -> int: ) except (SetupError, BuildError) as e: logger.error(str(e)) - return 1 + return os.EX_SOFTWARE backends = installed_redisai_backends() backends_str = ", ".join(s.capitalize() for s in backends) if backends else "No" @@ -434,10 +433,10 @@ def execute(args: argparse.Namespace) -> int: check_py_onnx_version(versions) except (SetupError, BuildError) as e: logger.error(str(e)) - return 1 + return os.EX_SOFTWARE logger.info("SmartSim build complete!") - return 0 + return os.EX_OK def configure_parser(parser: argparse.ArgumentParser) -> None: diff --git a/smartsim/_core/_cli/clean.py b/smartsim/_core/_cli/clean.py index fcf051f0c..d8a85f8a9 100644 --- a/smartsim/_core/_cli/clean.py +++ b/smartsim/_core/_cli/clean.py @@ -25,6 +25,7 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import argparse +import typing as t from smartsim._core._cli.utils import clean, get_install_path @@ -39,10 +40,14 @@ def configure_parser(parser: argparse.ArgumentParser) -> None: ) -def execute(args: argparse.Namespace) -> int: +def execute( + args: argparse.Namespace, _unparsed_args: t.Optional[t.List[str]] = None, / +) -> int: return clean(get_install_path() / "_core", _all=args.clobber) -def execute_all(args: argparse.Namespace) -> int: +def execute_all( + args: argparse.Namespace, _unparsed_args: t.Optional[t.List[str]] = None, / +) -> int: args.clobber = True return execute(args) diff --git a/smartsim/_core/_cli/cli.py b/smartsim/_core/_cli/cli.py index ce2376c15..ef4c113e1 100644 --- a/smartsim/_core/_cli/cli.py +++ b/smartsim/_core/_cli/cli.py @@ -27,6 +27,7 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import argparse +import os import typing as t from smartsim._core._cli.build import configure_parser as build_parser @@ -36,51 +37,69 @@ from smartsim._core._cli.clean import execute_all as clobber_execute from smartsim._core._cli.dbcli import execute as dbcli_execute from smartsim._core._cli.info import execute as info_execute +from smartsim._core._cli.plugin import plugins from smartsim._core._cli.site import execute as site_execute -from smartsim._core._cli.validate import ( - execute as validate_execute, - configure_parser as validate_parser, -) from smartsim._core._cli.utils import MenuItemConfig +from smartsim._core._cli.validate import configure_parser as validate_parser +from smartsim._core._cli.validate import execute as validate_execute class SmartCli: def __init__(self, menu: t.List[MenuItemConfig]) -> None: - self.menu: t.Dict[str, MenuItemConfig] = {item.command: item for item in menu} - parser = argparse.ArgumentParser( + self.menu: t.Dict[str, MenuItemConfig] = {} + self.parser = argparse.ArgumentParser( prog="smart", description="SmartSim command line interface", ) - self.parser = parser - self.args: t.Optional[argparse.Namespace] = None - subparsers = parser.add_subparsers( + self.subparsers = self.parser.add_subparsers( dest="command", required=True, metavar="", help="Available commands", ) - for cmd, item in self.menu.items(): - parser = subparsers.add_parser( - cmd, description=item.description, help=item.description - ) - if item.configurator: - item.configurator(parser) + self.register_menu_items(menu) + self.register_menu_items([plugin() for plugin in plugins]) def execute(self, cli_args: t.List[str]) -> int: if len(cli_args) < 2: self.parser.print_help() - return 0 + return os.EX_USAGE - app_args = cli_args[1:] - self.args = self.parser.parse_args(app_args) + app_args = cli_args[1:] # exclude the path to executable + subcommand = cli_args[1] # first positional arg is the subcommand - if not (menu_item := self.menu.get(app_args[0], None)): + menu_item = self.menu.get(subcommand, None) + if not menu_item: self.parser.print_help() - return 0 + return os.EX_USAGE - return menu_item.handler(self.args) + args = argparse.Namespace() + unparsed_args = [] + + if menu_item.is_plugin: + unparsed_args = app_args[1:] + else: + args = self.parser.parse_args(app_args) + + return menu_item.handler(args, unparsed_args) + + def _register_menu_item(self, item: MenuItemConfig) -> None: + parser = self.subparsers.add_parser( + item.command, description=item.description, help=item.description + ) + if item.configurator: + item.configurator(parser) + + if item.command in self.menu: + raise ValueError(f"{item.command} cannot overwrite existing CLI command") + + self.menu[item.command] = item + + def register_menu_items(self, menu_items: t.List[MenuItemConfig]) -> None: + for item in menu_items: + self._register_menu_item(item) def default_cli() -> SmartCli: diff --git a/smartsim/_core/_cli/dbcli.py b/smartsim/_core/_cli/dbcli.py index 22a376588..ce0975bc4 100644 --- a/smartsim/_core/_cli/dbcli.py +++ b/smartsim/_core/_cli/dbcli.py @@ -25,13 +25,17 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import argparse +import os +import typing as t from smartsim._core._cli.utils import get_db_path -def execute(_args: argparse.Namespace) -> int: +def execute( + _args: argparse.Namespace, _unparsed_args: t.Optional[t.List[str]] = None, / +) -> int: if db_path := get_db_path(): print(db_path) - return 0 + return os.EX_OK print("Database (Redis or KeyDB) dependencies not found") - return 1 + return os.EX_SOFTWARE diff --git a/smartsim/_core/_cli/info.py b/smartsim/_core/_cli/info.py index 35ee9b9ec..c08fcb1a3 100644 --- a/smartsim/_core/_cli/info.py +++ b/smartsim/_core/_cli/info.py @@ -1,5 +1,6 @@ import argparse import importlib.metadata +import os import pathlib import typing as t @@ -12,7 +13,9 @@ _MISSING_DEP = _helpers.colorize("Not Installed", "red") -def execute(_args: argparse.Namespace, /) -> int: +def execute( + _args: argparse.Namespace, _unparsed_args: t.Optional[t.List[str]] = None, / +) -> int: print("\nSmart Python Packages:") print( tabulate( @@ -66,7 +69,7 @@ def execute(_args: argparse.Namespace, /) -> int: ), end="\n\n", ) - return 0 + return os.EX_OK def _fmt_installed_db(db_path: t.Optional[pathlib.Path]) -> str: diff --git a/smartsim/_core/_cli/plugin.py b/smartsim/_core/_cli/plugin.py new file mode 100644 index 000000000..32c69b7e9 --- /dev/null +++ b/smartsim/_core/_cli/plugin.py @@ -0,0 +1,55 @@ +import argparse +import importlib.util +import os +import subprocess as sp +import sys +import typing as t + +import smartsim.log +from smartsim._core._cli.utils import SMART_LOGGER_FORMAT, MenuItemConfig +from smartsim.error.errors import SmartSimCLIActionCancelled + +_LOGGER = smartsim.log.get_logger("Smart", fmt=SMART_LOGGER_FORMAT) + + +def dynamic_execute( + cmd: str, plugin_name: str +) -> t.Callable[[argparse.Namespace, t.List[str]], int]: + def process_execute( + _args: argparse.Namespace, unparsed_args: t.List[str], / + ) -> int: + try: + spec = importlib.util.find_spec(cmd) + if spec is None: + raise AttributeError + except (ModuleNotFoundError, AttributeError): + _LOGGER.error(f"{cmd} plugin not found. Please ensure it is installed") + return os.EX_CONFIG + + combined_cmd = [sys.executable, "-m", cmd] + unparsed_args + + try: + completed_proc = sp.run(combined_cmd, check=False) + except KeyboardInterrupt as ex: + msg = f"{plugin_name} terminated by user" + raise SmartSimCLIActionCancelled(msg) from ex + return completed_proc.returncode + + return process_execute + + +def dashboard() -> MenuItemConfig: + return MenuItemConfig( + "dashboard", + ( + "Start the SmartSim dashboard to monitor experiment output from a " + "graphical user interface. This requires that the SmartSim Dashboard " + "Package be installed. For more infromation please visit " + "https://github.com/CrayLabs/SmartDashboard" + ), + dynamic_execute("smartdashboard", "Dashboard"), + is_plugin=True, + ) + + +plugins = (dashboard,) diff --git a/smartsim/_core/_cli/site.py b/smartsim/_core/_cli/site.py index 5fe667cde..c86e0341b 100644 --- a/smartsim/_core/_cli/site.py +++ b/smartsim/_core/_cli/site.py @@ -25,10 +25,12 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import argparse +import os +import typing as t from smartsim._core._cli.utils import get_install_path -def execute(_args: argparse.Namespace) -> int: +def execute(_args: argparse.Namespace, _unparsed_args: t.List[str], /) -> int: print(get_install_path()) - return 0 + return os.EX_OK diff --git a/smartsim/_core/_cli/utils.py b/smartsim/_core/_cli/utils.py index d8fd4b68c..e31d0aed2 100644 --- a/smartsim/_core/_cli/utils.py +++ b/smartsim/_core/_cli/utils.py @@ -24,7 +24,8 @@ # 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. -import importlib +import importlib.util +import os import shutil import subprocess as sp import sys @@ -110,7 +111,7 @@ def clean(core_path: Path, _all: bool = False) -> int: if removed: logger.info("Successfully removed SmartSim database installation") - return 0 + return os.EX_OK def get_db_path() -> t.Optional[Path]: @@ -121,7 +122,7 @@ def get_db_path() -> t.Optional[Path]: return None -_CliHandler = t.Callable[[Namespace], int] +_CliHandler = t.Callable[[Namespace, t.List[str]], int] _CliParseConfigurator = t.Callable[[ArgumentParser], None] @@ -132,8 +133,10 @@ def __init__( description: str, handler: _CliHandler, configurator: t.Optional[_CliParseConfigurator] = None, + is_plugin: bool = False, ): self.command = cmd self.description = description self.handler = handler self.configurator = configurator + self.is_plugin = is_plugin diff --git a/smartsim/_core/_cli/validate.py b/smartsim/_core/_cli/validate.py index df1d331e0..bda254859 100644 --- a/smartsim/_core/_cli/validate.py +++ b/smartsim/_core/_cli/validate.py @@ -82,7 +82,9 @@ def __exit__( self._finalizer.detach() # type: ignore[attr-defined] -def execute(args: argparse.Namespace, /) -> int: +def execute( + args: argparse.Namespace, _unparsed_args: t.Optional[t.List[str]] = None, / +) -> int: """Validate the SmartSim installation works as expected given a simple experiment """ @@ -101,10 +103,11 @@ def execute(args: argparse.Namespace, /) -> int: logger.error( "SmartSim failed to run a simple experiment!\n" f"Experiment failed due to the following exception:\n{e}\n\n" - f"Output files are available at `{temp_dir}`" + f"Output files are available at `{temp_dir}`", + exc_info=True, ) - return 2 - return 0 + return os.EX_SOFTWARE + return os.EX_OK def configure_parser(parser: argparse.ArgumentParser) -> None: @@ -138,6 +141,7 @@ def test_install( with_onnx: bool, ) -> None: exp = Experiment("ValidationExperiment", exp_path=location, launcher="local") + exp.disable_telemetry() port = _find_free_port() if port is None else port with _make_managed_local_orc(exp, port) as client: logger.info("Verifying Tensor Transfer") @@ -164,7 +168,7 @@ def _make_managed_local_orc( exp.start(orc) try: (client_addr,) = orc.get_address() - yield Client(address=client_addr, cluster=False) + yield Client(False, address=client_addr) finally: exp.stop(orc) diff --git a/smartsim/_core/_install/buildenv.py b/smartsim/_core/_install/buildenv.py index c6a050b4d..eaa2c68bd 100644 --- a/smartsim/_core/_install/buildenv.py +++ b/smartsim/_core/_install/buildenv.py @@ -46,9 +46,12 @@ # to remove # https://setuptools.pypa.io/en/latest/pkg_resources.html +# isort: off import pkg_resources from pkg_resources import packaging # type: ignore +# isort: on + Version = packaging.version.Version InvalidVersion = packaging.version.InvalidVersion DbEngine = t.Literal["REDIS", "KEYDB"] @@ -189,17 +192,6 @@ class RedisAIVersion(Version_): """ defaults = { - "1.2.5": { - "tensorflow": "2.6.2", - "onnx": "1.9.0", - "skl2onnx": "1.10.3", - "onnxmltools": "1.10.0", - "scikit-learn": "1.0.2", - "torch": "1.9.1", - "torch_cpu_suffix": "+cpu", - "torch_cuda_suffix": "+cu111", - "torchvision": "0.10.1", - }, "1.2.7": { "tensorflow": "2.8.0", "onnx": "1.11.0", @@ -214,14 +206,10 @@ class RedisAIVersion(Version_): } # Remove options with unsported wheels for python>=3.10 if sys.version_info >= (3, 10): - defaults.pop("1.2.5") defaults["1.2.7"].pop("onnx") defaults["1.2.7"].pop("skl2onnx") defaults["1.2.7"].pop("onnxmltools") defaults["1.2.7"].pop("scikit-learn") - # Remove incompatible RAI versions for OSX - if sys.platform == "darwin": - defaults.pop("1.2.5", None) def __init__(self, vers: str) -> None: # pylint: disable=super-init-not-called min_rai_version = min(Version_(ver) for ver in self.defaults) @@ -288,8 +276,8 @@ class Versioner: PYTHON_MIN = Version_("3.8.0") # Versions - SMARTSIM = Version_(get_env("SMARTSIM_VERSION", "0.5.1")) - SMARTREDIS = Version_(get_env("SMARTREDIS_VERSION", "0.4.2")) + SMARTSIM = Version_(get_env("SMARTSIM_VERSION", "0.6.0")) + SMARTREDIS = Version_(get_env("SMARTREDIS_VERSION", "0.5.0")) SMARTSIM_SUFFIX = get_env("SMARTSIM_SUFFIX", "") # Redis @@ -362,9 +350,7 @@ def ml_extras_required(self) -> t.Dict[str, t.List[str]]: for field in _torch_fields: ml_defaults.pop(field) - return { - "ml": [f"{lib}=={vers}" for lib, vers in ml_defaults.items()] - } + return {"ml": [f"{lib}=={vers}" for lib, vers in ml_defaults.items()]} @staticmethod def get_sha(setup_py_dir: Path) -> str: diff --git a/smartsim/_core/_install/builder.py b/smartsim/_core/_install/builder.py index 1e9125626..f96a9bb5f 100644 --- a/smartsim/_core/_install/builder.py +++ b/smartsim/_core/_install/builder.py @@ -351,21 +351,6 @@ def build_onnx(self) -> bool: def fetch_onnx(self) -> bool: return self.build_onnx - def copy_tf_cmake(self) -> None: - """Copy the FindTensorFlow.cmake file to the build directory - as the version included in RedisAI is out of date for us. - Note: opt/cmake/modules removed in RedisAI v1.2.5 - """ - # remove the previous version - tf_cmake = self.rai_build_path / "opt/cmake/modules/FindTensorFlow.cmake" - tf_cmake.resolve() - if tf_cmake.is_file(): - tf_cmake.unlink() - # copy ours in - self.copy_file( - self.bin_path / "modules/FindTensorFlow.cmake", tf_cmake, set_exe=False - ) - def symlink_libtf(self, device: str) -> None: """Add symbolic link to available libtensorflow in RedisAI deps. @@ -453,7 +438,7 @@ def build_from_git(self, git_url: str, branch: str, device: str = "cpu") -> None # Circumvent a bad `get_deps.sh` script from RAI on 1.2.7 with ONNX # TODO: Look for a better way to do this or wait for RAI patch - if sys.platform == "darwin" and branch == "v1.2.7" and self.build_onnx: + if branch == "v1.2.7": # Clone RAI patch commit for OSX clone_cmd += ["RedisAI"] checkout_osx_fix = [ @@ -462,7 +447,7 @@ def build_from_git(self, git_url: str, branch: str, device: str = "cpu") -> None "634916c722e718cc6ea3fad46e63f7d798f9adc2", ] else: - # Clone RAI release commit + # Clone RAI release commit for versions > 1.2.7 clone_cmd += [ "--branch", branch, @@ -476,9 +461,6 @@ def build_from_git(self, git_url: str, branch: str, device: str = "cpu") -> None checkout_osx_fix, out=subprocess.DEVNULL, cwd=self.rai_build_path ) - # copy FindTensorFlow.cmake to RAI cmake dir - self.copy_tf_cmake() - # get RedisAI dependencies dep_cmd = self._rai_build_env_prefix( with_pt=self.build_torch, diff --git a/smartsim/_core/bin/modules/FindTensorFlow.cmake b/smartsim/_core/bin/modules/FindTensorFlow.cmake deleted file mode 100644 index b7c9f8cb5..000000000 --- a/smartsim/_core/bin/modules/FindTensorFlow.cmake +++ /dev/null @@ -1,359 +0,0 @@ -# Patrick Wieschollek, -# FindTensorFlow.cmake -# https://github.com/PatWie/tensorflow-cmake/blob/master/cmake/modules/FindTensorFlow.cmake -# ------------- -# -# Find TensorFlow library and includes -# -# Automatically set variables have prefix "TensorFlow_", -# while environmental variables you can specify have prefix "TENSORFLOW_" -# This module will set the following variables in your project: -# -# ``TensorFlow_VERSION`` -# exact TensorFlow version obtained from runtime -# ``TensorFlow_ABI`` -# ABI specification of TensorFlow library obtained from runtime -# ``TensorFlow_INCLUDE_DIR`` -# where to find tensorflow header files obtained from runtime -# ``TensorFlow_LIBRARY`` -# the libraries to link against to use TENSORFLOW obtained from runtime -# ``TensorFlow_FOUND TRUE`` -# If false, do not try to use TENSORFLOW. -# ``TensorFlow_C_LIBRARY`` -# Path to tensorflow_cc library (libtensorflow[.so,.dylib,.dll], or similar) -# -# for some examples, you will need to specify on of the following cmake variables: -# ``TensorFlow_BUILD_DIR`` Is the directory containing the tensorflow_cc library, which can be initialized -# with env-var 'TENSORFLOW_BUILD_DIR' environmental variable -# ``TensorFlow_SOURCE_DIR`` Is the path to source of TensorFlow, which can be initialized -# with env-var 'TENSORFLOW_SOURCE_DIR' environmental variable -# -# -# USAGE -# ------ -# add "list(APPEND CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}../../path/to/this/file)" to your project -# -# "add_tensorflow_gpu_operation" is a macro to compile a custom operation -# -# add_tensorflow_gpu_operation("") expects the following files to exists: -# - kernels/_kernel.cc -# - kernels/_kernel_gpu.cu.cc (kernels/_kernel.cu is supported as well) -# - kernels/_op.cc -# - kernels/_op.h -# - ops/.cc - -if(APPLE) - message(WARNING "This FindTensorflow.cmake is not tested on APPLE\n" - "Please report if this works\n" - "https://github.com/PatWie/tensorflow-cmake") -endif() - -if(WIN32) - message(WARNING "This FindTensorflow.cmake is not tested on WIN32\n" - "Please report if this works\n" - "https://github.com/PatWie/tensorflow-cmake") -endif() - -set(PYTHON_EXECUTABLE "python3" CACHE STRING "specify the python version TensorFlow is installed on.") - -if(TensorFlow_FOUND AND EXISTS "${TensorFlow_LIBRARY}" AND IS_DIRECTORY "${TensorFlow_INCLUDE_DIR}") - # reuse cached variables - message(STATUS "Reuse cached information from TensorFlow ${TensorFlow_VERSION} ") -else() - message(STATUS "Detecting TensorFlow using ${PYTHON_EXECUTABLE}" - " (use -DPYTHON_EXECUTABLE=... otherwise)") - execute_process( - COMMAND ${PYTHON_EXECUTABLE} -c "import tensorflow as tf; print(tf.__version__); print(tf.__cxx11_abi_flag__); print(tf.sysconfig.get_include()); print(tf.sysconfig.get_lib());" - OUTPUT_VARIABLE TF_INFORMATION_STRING - OUTPUT_STRIP_TRAILING_WHITESPACE - RESULT_VARIABLE retcode) - - if(NOT "${retcode}" STREQUAL "0") - message(FATAL_ERROR "Detecting TensorFlow info - failed \n Did you installed TensorFlow?") - else() - message(STATUS "Detecting TensorFlow info - done") - endif() - - string(REPLACE "\n" ";" TF_INFORMATION_LIST ${TF_INFORMATION_STRING}) - list(GET TF_INFORMATION_LIST 0 TF_DETECTED_VERSION) - list(GET TF_INFORMATION_LIST 1 TF_DETECTED_ABI) - list(GET TF_INFORMATION_LIST 2 TF_DETECTED_INCLUDE_DIR) - list(GET TF_INFORMATION_LIST 3 TF_DETECTED_LIBRARY_PATH) - - # set(TF_DETECTED_VERSION 1.8) - - set(_packageName "TF") - if (DEFINED TF_DETECTED_VERSION) - string (REGEX MATCHALL "[0-9]+" _versionComponents "${TF_DETECTED_VERSION}") - list (LENGTH _versionComponents _len) - if (${_len} GREATER 0) - list(GET _versionComponents 0 TF_DETECTED_VERSION_MAJOR) - endif() - if (${_len} GREATER 1) - list(GET _versionComponents 1 TF_DETECTED_VERSION_MINOR) - endif() - if (${_len} GREATER 2) - list(GET _versionComponents 2 TF_DETECTED_VERSION_PATCH) - endif() - if (${_len} GREATER 3) - list(GET _versionComponents 3 TF_DETECTED_VERSION_TWEAK) - endif() - set (TF_DETECTED_VERSION_COUNT ${_len}) - else() - set (TF_DETECTED_VERSION_COUNT 0) - endif() - - - # -- prevent pre 1.9 versions - # Note: TensorFlow 1.7 supported custom ops and all header files. - # TensorFlow 1.8 broke that promise and 1.9, 1.10 are fine again. - # This cmake-file is only tested against 1.9+. - if("${TF_DETECTED_VERSION}" VERSION_LESS "1.9") - message(FATAL_ERROR "Your installed TensorFlow version ${TF_DETECTED_VERSION} is too old.") - endif() - - if(TF_FIND_VERSION_EXACT) - # User requested exact match of TensorFlow. - # TensorFlow release cycles are currently just depending on (major, minor) - # But we test against both. - set(_TensorFlow_TEST_VERSIONS - "${TF_FIND_VERSION_MAJOR}.${TF_FIND_VERSION_MINOR}.${TF_FIND_VERSION_PATCH}" - "${TF_FIND_VERSION_MAJOR}.${TF_FIND_VERSION_MINOR}") - else() # TF_FIND_VERSION_EXACT - # User requested not an exact TensorFlow version. - # However, only TensorFlow versions 1.9, 1.10 support all header files - # for custom ops. - set(_TensorFlow_KNOWN_VERSIONS ${TensorFlow_ADDITIONAL_VERSIONS} - "1.9" "1.9.0" "1.10" "1.10.0" "1.11" "1.11.0" "1.12" "1.12.0" "1.13" "1.13.1" "1.14" "2.4" "2.5" "2.6") - set(_TensorFlow_TEST_VERSIONS) - - if(TF_FIND_VERSION) - set(_TF_FIND_VERSION_SHORT "${TF_FIND_VERSION_MAJOR}.${TF_FIND_VERSION_MINOR}") - # Select acceptable versions. - foreach(version ${_TensorFlow_KNOWN_VERSIONS}) - if(NOT "${version}" VERSION_LESS "${TF_FIND_VERSION}") - # This version is high enough. - list(APPEND _TensorFlow_TEST_VERSIONS "${version}") - endif() - endforeach() - else() # TF_FIND_VERSION - # Any version is acceptable. - set(_TensorFlow_TEST_VERSIONS "${_TensorFlow_KNOWN_VERSIONS}") - endif() - endif() - - #### ---- Configure TensorFlow_SOURCE_DIR - # Order of precidence is 1) CMake variable value, 2) Environmental Variable value - if(IS_DIRECTORY "${TensorFlow_SOURCE_DIR}") - set(TensorFlow_SOURCE_DIR "${TensorFlow_SOURCE_DIR}" CACHE PATH "directory containing the file 'libtensorflow_cc${CMAKE_SHARED_LIBRARY_SUFFIX}'") - else() - if(IS_DIRECTORY "$ENV{TENSORFLOW_SOURCE_DIR}") - set(TensorFlow_SOURCE_DIR "$ENV{TENSORFLOW_SOURCE_DIR}" CACHE PATH "source code for tensorflow (i.e. the git checkout directory of the source code)") - else() - set(TensorFlow_SOURCE_DIR "TensorFlow_SOURCE_DIR-NOTFOUND" CACHE PATH "source code for tensorflow (i.e. the git checkout directory of the source code)") - endif() - endif() - - # Report on status of cmake cache variable for TensorFlow_SOURCE_DIR - if(IS_DIRECTORY ${TensorFlow_SOURCE_DIR}) - message(STATUS "TensorFlow_SOURCE_DIR is ${TensorFlow_SOURCE_DIR}") - else() - # NOTE This is not a fatal error for backward compatibility ("custom_op test") - message(STATUS "No directory at 'TensorFlow_SOURCE_DIR:PATH=${TensorFlow_SOURCE_DIR}' detected,\n" - "please specify the path in ENV 'export TENSORFLOW_SOURCE_DIR=...'\n or cmake -DTensorFlow_SOURCE_DIR:PATH=...\n" - "to the directory containing the source code for tensorflow\n (i.e. the git checkout directory of the source code)" - ) - endif() - - #### ---- Configure TensorFlow_BUILD_DIR - # Order of precidence is 1) CMake variable value, 2) Environmental Variable value - if(IS_DIRECTORY "${TensorFlow_BUILD_DIR}") - set(TensorFlow_BUILD_DIR "${TensorFlow_BUILD_DIR}" CACHE PATH "directory containing the file 'libtensorflow_cc${CMAKE_SHARED_LIBRARY_SUFFIX}'") - else() - if(IS_DIRECTORY "$ENV{TENSORFLOW_BUILD_DIR}") - set(TensorFlow_BUILD_DIR "$ENV{TENSORFLOW_BUILD_DIR}" CACHE PATH "directory containing the file 'libtensorflow_cc${CMAKE_SHARED_LIBRARY_SUFFIX}'") - else() - set(TensorFlow_BUILD_DIR "TensorFlow_BUILD_DIR-NOTFOUND" CACHE PATH "directory containing the file 'libtensorflow_cc${CMAKE_SHARED_LIBRARY_SUFFIX}'") - endif() - endif() - - # Report on status of cmake cache variable for TensorFlow_BUILD_DIR - if(IS_DIRECTORY ${TensorFlow_BUILD_DIR}) - message(STATUS "TensorFlow_BUILD_DIR is ${TensorFlow_BUILD_DIR}") - else() - # NOTE This is not a fatal error for backward compatibility ("custom_op test") - message(STATUS "No directory at 'TensorFlow_BUILD_DIR:PATH=${TensorFlow_BUILD_DIR}' detected,\n" - "please specify the path in ENV 'export TENSORFLOW_BUILD_DIR=...'\n or cmake -DTensorFlow_BUILD_DIR:PATH=...\n" - "to the directory containing the file 'libtensorflow_cc${CMAKE_SHARED_LIBRARY_SUFFIX}'" - ) - endif() - - if(IS_DIRECTORY ${TensorFlow_BUILD_DIR}) - file(GLOB_RECURSE TF_LIBRARY_SEARCH_PATHS - LIST_DIRECTORIES FALSE - "${TensorFlow_BUILD_DIR}/*libtensorflow_cc${CMAKE_SHARED_LIBRARY_SUFFIX}" - ) - list(LENGTH TF_LIBRARY_SEARCH_PATHS TF_LIBRARY_SEARCH_PATHS_LENGTH) - if( NOT ${TF_LIBRARY_SEARCH_PATHS_LENGTH} EQUAL 1 ) - message(FATAL_ERROR "Incorrect number of items matching 'libtensorflow_cc${CMAKE_SHARED_LIBRARY_SUFFIX}' in '${TF_LIBRARY_SEARCH_PATHS}'\n" - "( ${TF_LIBRARY_SEARCH_PATHS_LENGTH} != 1 ).\n" - "Change 'TensorFlow_BUILD_DIR' to have more specific path." - ) - endif() - list(GET TF_LIBRARY_SEARCH_PATHS 0 TF_LIBRARY_SEARCH_ONEPATH) - get_filename_component(TensorFlow_C_LIBRARY_DIR "${TF_LIBRARY_SEARCH_ONEPATH}" DIRECTORY ) - - if( IS_DIRECTORY "${TensorFlow_C_LIBRARY_DIR}") - find_library(TensorFlow_C_LIBRARY - NAMES tensorflow_cc - PATHS "${TensorFlow_C_LIBRARY_DIR}" - DOC "TensorFlow CC library." ) - endif() - if( TensorFlow_C_LIBRARY ) - message(STATUS "TensorFlow-CC-LIBRARY is ${TensorFlow_C_LIBRARY}") - else() - # NOTE This is not a fatal error for backward compatibility ("custom_op test") - message(STATUS "No TensorFlow-CC-LIBRARY detected") - endif() - endif() - - find_library( TF_DETECTED_LIBRARY - NAMES tensorflow - PATHS "${TensorFlow_C_LIBRARY_DIR}" # Prefer the library from the build tree, if TensorFlow_C_LIBRARY is detected. - "${TF_DETECTED_LIBRARY_PATH}" # use copy of file from the python install tree (This often has a .so.1 extension only for installed version) - DOC "The tensorflow_framework library path." - ) - if( TF_DETECTED_LIBRARY ) - message(STATUS "Found: ${TF_DETECTED_LIBRARY}") - else() - message(FATAL_ERROR "Required library for tensorflow_framework not found in ${TF_DETECTED_LIBRARY_PATH}!") - endif() - - # test all given versions - set(TensorFlow_FOUND FALSE) - foreach(_TensorFlow_VER ${_TensorFlow_TEST_VERSIONS}) - if("${TF_DETECTED_VERSION_MAJOR}.${TF_DETECTED_VERSION_MINOR}" STREQUAL "${_TensorFlow_VER}") - # found appropriate version - set(TensorFlow_VERSION ${TF_DETECTED_VERSION}) - set(TensorFlow_ABI ${TF_DETECTED_ABI}) - set(TensorFlow_INCLUDE_DIR ${TF_DETECTED_INCLUDE_DIR}) - set(TensorFlow_LIBRARY ${TF_DETECTED_LIBRARY}) - set(TensorFlow_FOUND TRUE) - message(STATUS "Found TensorFlow: (found appropriate version \"${TensorFlow_VERSION}\")") - message(STATUS "TensorFlow-ABI is ${TensorFlow_ABI}") - message(STATUS "TensorFlow-INCLUDE_DIR is ${TensorFlow_INCLUDE_DIR}") - message(STATUS "TensorFlow-LIBRARY is ${TensorFlow_LIBRARY}") - - add_definitions("-DTENSORFLOW_ABI=${TensorFlow_ABI}") - add_definitions("-DTENSORFLOW_VERSION=${TensorFlow_VERSION}") - break() - endif() - endforeach() - - if(NOT TensorFlow_FOUND) - message(FATAL_ERROR "Your installed TensorFlow version ${TF_DETECTED_VERSION_MAJOR}.${TF_DETECTED_VERSION_MINOR} is not supported\n" - "We tested against ${_TensorFlow_TEST_VERSIONS}") - endif() - - # test 1.11 version - if("${TF_DETECTED_VERSION}" VERSION_EQUAL "1.11") - set(TF_DISABLE_ASSERTS "TRUE") - endif() - - if("${TF_DETECTED_VERSION}" VERSION_EQUAL "1.12") - set(TF_DISABLE_ASSERTS "TRUE") - endif() - - if("${TF_DETECTED_VERSION}" VERSION_EQUAL "1.12.0") - set(TF_DISABLE_ASSERTS "TRUE") - endif() - - if("${TF_DETECTED_VERSION}" VERSION_EQUAL "1.13") - set(TF_DISABLE_ASSERTS "TRUE") - endif() - - if("${TF_DETECTED_VERSION}" VERSION_EQUAL "1.13.1") - set(TF_DISABLE_ASSERTS "TRUE") - endif() - -endif() #-- End detection - -if(${TF_DISABLE_ASSERTS}) - message(STATUS "[WARNING] The TensorFlow version ${TF_DETECTED_VERSION} has a bug (see \#22766). We disable asserts using -DNDEBUG=True ") - add_definitions("-DNDEBUG=True") -endif() -macro(TensorFlow_REQUIRE_C_LIBRARY) - if(NOT EXISTS "${TensorFlow_C_LIBRARY}") - message(FATAL_ERROR "Project requires libtensorflow_cc${CMAKE_SHARED_LIBRARY_SUFFIX}, please specify the path in ENV 'export TENSORFLOW_BUILD_DIR=...' or cmake -DTensorFlow_BUILD_DIR:PATH=...") - endif() -endmacro() - -macro(TensorFlow_REQUIRE_SOURCE) - if(NOT IS_DIRECTORY "${TensorFlow_SOURCE_DIR}") - message(FATAL_ERROR "Project requires TensorFlow source directory, please specify the path in ENV 'export TENSORFLOW_SOURCE_DIR=...' or cmake -DTensorFlow_SOURCE_DIR:PATH=...") - endif() -endmacro() - -macro(add_tensorflow_cpu_operation op_name) - # Compiles a CPU-only operation without invoking NVCC - message(STATUS "will build custom TensorFlow operation \"${op_name}\" (CPU only)") - - add_library(${op_name}_op SHARED kernels/${op_name}_op.cc kernels/${op_name}_kernel.cc ops/${op_name}.cc ) - - set_target_properties(${op_name}_op PROPERTIES PREFIX "") - target_link_libraries(${op_name}_op LINK_PUBLIC ${TensorFlow_LIBRARY}) -endmacro() - - -macro(add_tensorflow_gpu_operation op_name) -# Compiles a CPU + GPU operation with invoking NVCC - message(STATUS "will build custom TensorFlow operation \"${op_name}\" (CPU+GPU)") - - set(kernel_file "") - if(EXISTS "kernels/${op_name}_kernel.cu") - message(WARNING "you should rename your file ${op_name}_kernel.cu to ${op_name}_kernel_gpu.cu.cc") - set(kernel_file kernels/${op_name}_kernel.cu) - else() - set_source_files_properties(kernels/${op_name}_kernel_gpu.cu.cc PROPERTIES CUDA_SOURCE_PROPERTY_FORMAT OBJ) - set(kernel_file kernels/${op_name}_kernel_gpu.cu.cc) - endif() - - cuda_add_library(${op_name}_op_cu SHARED ${kernel_file}) - set_target_properties(${op_name}_op_cu PROPERTIES PREFIX "") - - add_library(${op_name}_op SHARED kernels/${op_name}_op.cc kernels/${op_name}_kernel.cc ops/${op_name}.cc ) - - set_target_properties(${op_name}_op PROPERTIES PREFIX "") - set_target_properties(${op_name}_op PROPERTIES COMPILE_FLAGS "-DGOOGLE_CUDA") - target_link_libraries(${op_name}_op LINK_PUBLIC ${op_name}_op_cu ${TensorFlow_LIBRARY}) -endmacro() - -# simplify TensorFlow dependencies -add_library(TensorFlow_DEP INTERFACE) -target_include_directories(TensorFlow_DEP SYSTEM INTERFACE ${TensorFlow_SOURCE_DIR}) -target_include_directories(TensorFlow_DEP SYSTEM INTERFACE ${TensorFlow_INCLUDE_DIR}) -target_link_libraries(TensorFlow_DEP INTERFACE -Wl,--allow-multiple-definition -Wl,--whole-archive ${TensorFlow_C_LIBRARY} -Wl,--no-whole-archive) -target_link_libraries(TensorFlow_DEP INTERFACE -Wl,--allow-multiple-definition -Wl,--whole-archive ${TensorFlow_LIBRARY} -Wl,--no-whole-archive) - -include(FindPackageHandleStandardArgs) -find_package_handle_standard_args( - TENSORFLOW - FOUND_VAR TENSORFLOW_FOUND - REQUIRED_VARS - TensorFlow_LIBRARY - TensorFlow_INCLUDE_DIR - VERSION_VAR - TensorFlow_VERSION - ) - -mark_as_advanced(TF_INFORMATION_STRING TF_DETECTED_VERSION TF_DETECTED_VERSION_MAJOR TF_DETECTED_VERSION_MINOR TF_DETECTED_VERSION TF_DETECTED_ABI - TF_DETECTED_INCLUDE_DIR TF_DETECTED_LIBRARY TF_DISABLE_ASSERTS - TensorFlow_C_LIBRARY TensorFlow_LIBRARY TensorFlow_SOURCE_DIR TensorFlow_INCLUDE_DIR TensorFlow_ABI) - -set(TensorFlow_INCLUDE_DIR "${TensorFlow_INCLUDE_DIR}" CACHE PATH "The path to tensorflow header files") -set(TensorFlow_VERSION "${TensorFlow_VERSION}" CACHE INTERNAL "The Tensorflow version") -set(TensorFlow_ABI "${TensorFlow_ABI}" CACHE STRING "The ABI version used by TensorFlow") -set(TensorFlow_LIBRARY "${TensorFlow_LIBRARY}" CACHE FILEPATH "The C++ library of TensorFlow") -set(TensorFlow_C_LIBRARY "${TensorFlow_C_LIBRARY}" CACHE STRING "The C library of TensorFlow") -set(TensorFlow_FOUND "${TensorFlow_FOUND}" CACHE BOOL "A flag stating if TensorFlow has been found") -set(TF_DISABLE_ASSERTS "${TF_DISABLE_ASSERTS}" CACHE BOOL "A flag to enable workarounds") diff --git a/smartsim/_core/config/config.py b/smartsim/_core/config/config.py index 3b0905021..af5ebe508 100644 --- a/smartsim/_core/config/config.py +++ b/smartsim/_core/config/config.py @@ -24,13 +24,14 @@ # 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. +import json import os -import psutil import typing as t - from functools import lru_cache from pathlib import Path +import psutil + from ...error import SSConfigError from ..utils.helpers import expand_exe_path @@ -143,6 +144,14 @@ def database_cli(self) -> str: "Specified Redis binary at REDIS_CLI_PATH could not be used" ) from e + @property + def database_file_parse_trials(self) -> int: + return int(os.getenv("SMARTSIM_DB_FILE_PARSE_TRIALS", "10")) + + @property + def database_file_parse_interval(self) -> int: + return int(os.getenv("SMARTSIM_DB_FILE_PARSE_INTERVAL", "2")) + @property def log_level(self) -> str: return os.environ.get("SMARTSIM_LOG_LEVEL", "info") @@ -171,6 +180,20 @@ def test_num_gpus(self) -> int: # pragma: no cover def test_port(self) -> int: # pragma: no cover return int(os.environ.get("SMARTSIM_TEST_PORT", 6780)) + @property + def test_batch_resources(self) -> t.Dict[t.Any, t.Any]: # pragma: no cover + resource_str = os.environ.get("SMARTSIM_TEST_BATCH_RESOURCES", "{}") + resources = json.loads(resource_str) + if not isinstance(resources, dict): + raise TypeError( + ( + "SMARTSIM_TEST_BATCH_RESOURCES was not interpreted as a " + "dictionary, check to make sure that it is a valid " + f"JSON string: {resource_str}" + ) + ) + return resources + @property def test_interface(self) -> t.List[str]: # pragma: no cover if interfaces_cfg := os.environ.get("SMARTSIM_TEST_INTERFACE", None): @@ -196,6 +219,18 @@ def test_account(self) -> t.Optional[str]: # pragma: no cover # no account by default return os.environ.get("SMARTSIM_TEST_ACCOUNT", None) + @property + def telemetry_frequency(self) -> int: + return int(os.environ.get("SMARTSIM_TELEMETRY_FREQUENCY", 5)) + + @property + def telemetry_enabled(self) -> bool: + return int(os.environ.get("SMARTSIM_FLAG_TELEMETRY", "0")) > 0 + + @property + def telemetry_cooldown(self) -> int: + return int(os.environ.get("SMARTSIM_TELEMETRY_COOLDOWN", 90)) + @lru_cache(maxsize=128, typed=False) def get_config() -> Config: diff --git a/smartsim/_core/control/controller.py b/smartsim/_core/control/controller.py index b3aae3fbc..e3e463c51 100644 --- a/smartsim/_core/control/controller.py +++ b/smartsim/_core/control/controller.py @@ -26,36 +26,58 @@ from __future__ import annotations +import itertools import os.path as osp +import pathlib import pickle import signal +import subprocess +import sys import threading import time import typing as t +from os import environ -from smartredis import Client +from smartredis import Client, ConfigOptions + +from smartsim._core.utils.network import get_ip_from_host from ..._core.launcher.step import Step -from ..._core.utils.redis import db_is_active, set_ml_model, set_script +from ..._core.utils.helpers import unpack_colo_db_identifier, unpack_db_identifier +from ..._core.utils.redis import ( + db_is_active, + set_ml_model, + set_script, + shutdown_db_node, +) from ...database import Orchestrator -from ...entity import EntityList, SmartSimEntity, Model, Ensemble -from ...error import LauncherError, SmartSimError, SSInternalError, SSUnsupportedError +from ...entity import Ensemble, EntityList, EntitySequence, Model, SmartSimEntity +from ...error import ( + LauncherError, + SmartSimError, + SSDBIDConflictError, + SSInternalError, + SSUnsupportedError, +) from ...log import get_logger -from ...status import STATUS_RUNNING, TERMINAL_STATUSES +from ...servertype import CLUSTERED, STANDALONE +from ...status import STATUS_CANCELLED, STATUS_RUNNING, TERMINAL_STATUSES from ..config import CONFIG from ..launcher import ( - SlurmLauncher, - PBSLauncher, - LocalLauncher, CobaltLauncher, + LocalLauncher, LSFLauncher, + PBSLauncher, + SlurmLauncher, ) from ..launcher.launcher import Launcher -from ..utils import check_cluster_status, create_cluster -from .jobmanager import JobManager -from .manifest import Manifest +from ..utils import check_cluster_status, create_cluster, serialize from .job import Job -from ...settings.base import BatchSettings +from .jobmanager import JobManager +from .manifest import LaunchedManifest, LaunchedManifestBuilder, Manifest + +if t.TYPE_CHECKING: + from ..utils.serialize import TStepLaunchMetaData logger = get_logger(__name__) @@ -78,9 +100,15 @@ def __init__(self, launcher: str = "local") -> None: """ self._jobs = JobManager(JM_LOCK) self.init_launcher(launcher) + self._telemetry_monitor: t.Optional[subprocess.Popen[bytes]] = None def start( - self, manifest: Manifest, block: bool = True, kill_on_interrupt: bool = True + self, + exp_name: str, + exp_path: str, + manifest: Manifest, + block: bool = True, + kill_on_interrupt: bool = True, ) -> None: """Start the passed SmartSim entities @@ -93,12 +121,20 @@ def start( self._jobs.kill_on_interrupt = kill_on_interrupt # register custom signal handler for ^C (SIGINT) signal.signal(signal.SIGINT, self._jobs.signal_interrupt) - self._launch(manifest) + launched = self._launch(exp_name, exp_path, manifest) # start the job manager thread if not already started if not self._jobs.actively_monitoring: self._jobs.start() + serialize.save_launch_manifest( + launched.map(_look_up_launched_data(self._launcher)) + ) + + # launch a telemetry monitor to track job progress + if CONFIG.telemetry_enabled: + self._start_telemetry_monitor(exp_path) + # block until all non-database jobs are complete if block: # poll handles its own keyboard interrupt as @@ -136,23 +172,25 @@ def poll( for job in to_monitor.values(): logger.info(job) - def finished(self, entity: t.Union[SmartSimEntity, EntityList]) -> bool: + def finished( + self, entity: t.Union[SmartSimEntity, EntitySequence[SmartSimEntity]] + ) -> bool: """Return a boolean indicating wether a job has finished or not :param entity: object launched by SmartSim. - :type entity: Entity | EntityList + :type entity: Entity | EntitySequence :returns: bool :raises ValueError: if entity has not been launched yet """ try: if isinstance(entity, Orchestrator): raise TypeError("Finished() does not support Orchestrator instances") - if isinstance(entity, EntityList): + if isinstance(entity, EntitySequence): return all(self.finished(ent) for ent in entity.entities) if not isinstance(entity, SmartSimEntity): raise TypeError( f"Argument was of type {type(entity)} not derived " - "from SmartSimEntity or EntityList" + "from SmartSimEntity or EntitySequence" ) return self._jobs.is_finished(entity) @@ -161,14 +199,16 @@ def finished(self, entity: t.Union[SmartSimEntity, EntityList]) -> bool: f"Entity {entity.name} has not been launched in this experiment" ) from None - def stop_entity(self, entity: t.Union[SmartSimEntity, EntityList]) -> None: + def stop_entity( + self, entity: t.Union[SmartSimEntity, EntitySequence[SmartSimEntity]] + ) -> None: """Stop an instance of an entity This function will also update the status of the job in the jobmanager so that the job appears as "cancelled". :param entity: entity to be stopped - :type entity: Entity | EntityList + :type entity: Entity | EntitySequence """ with JM_LOCK: job = self._jobs[entity.name] @@ -189,12 +229,38 @@ def stop_entity(self, entity: t.Union[SmartSimEntity, EntityList]) -> None: ) self._jobs.move_to_completed(job) - def stop_entity_list(self, entity_list: EntityList) -> None: + def stop_db(self, db: Orchestrator) -> None: + """Stop an orchestrator + :param db: orchestrator to be stopped + :type db: Orchestrator + """ + if db.batch: + self.stop_entity(db) + else: + with JM_LOCK: + for node in db.entities: + for host_ip, port in itertools.product( + (get_ip_from_host(host) for host in node.hosts), db.ports + ): + retcode, _, _ = shutdown_db_node(host_ip, port) + # Sometimes the DB will not shutdown (unless we force NOSAVE) + if retcode != 0: + self.stop_entity(node) + continue + + job = self._jobs[node.name] + job.set_status(STATUS_CANCELLED, "", 0, output=None, error=None) + self._jobs.move_to_completed(job) + + db.reset_hosts() + + def stop_entity_list(self, entity_list: EntitySequence[SmartSimEntity]) -> None: """Stop an instance of an entity list :param entity_list: entity list to be stopped - :type entity_list: EntityList + :type entity_list: EntitySequence """ + if entity_list.batch: self.stop_entity(entity_list) else: @@ -209,34 +275,40 @@ def get_jobs(self) -> t.Dict[str, Job]: with JM_LOCK: return self._jobs.completed - def get_entity_status(self, entity: t.Union[SmartSimEntity, EntityList]) -> str: + def get_entity_status( + self, entity: t.Union[SmartSimEntity, EntitySequence[SmartSimEntity]] + ) -> str: """Get the status of an entity :param entity: entity to get status of - :type entity: SmartSimEntity | EntityList - :raises TypeError: if not SmartSimEntity | EntityList + :type entity: SmartSimEntity | EntitySequence + :raises TypeError: if not SmartSimEntity | EntitySequence :return: status of entity :rtype: str """ - if not isinstance(entity, (SmartSimEntity, EntityList)): + if not isinstance(entity, (SmartSimEntity, EntitySequence)): raise TypeError( - "Argument must be of type SmartSimEntity or EntityList, " + "Argument must be of type SmartSimEntity or EntitySequence, " f"not {type(entity)}" ) return self._jobs.get_status(entity) - def get_entity_list_status(self, entity_list: EntityList) -> t.List[str]: + def get_entity_list_status( + self, entity_list: EntitySequence[SmartSimEntity] + ) -> t.List[str]: """Get the statuses of an entity list :param entity_list: entity list containing entities to get statuses of - :type entity_list: EntityList - :raises TypeError: if not EntityList + :type entity_list: EntitySequence + :raises TypeError: if not EntitySequence :return: list of str statuses :rtype: list """ - if not isinstance(entity_list, EntityList): - raise TypeError(f"Argument was of type {type(entity_list)} not EntityList") + if not isinstance(entity_list, EntitySequence): + raise TypeError( + f"Argument was of type {type(entity_list)} not EntitySequence" + ) if entity_list.batch: return [self.get_entity_status(entity_list)] statuses = [] @@ -275,63 +347,96 @@ def init_launcher(self, launcher: str) -> None: else: raise TypeError("Must provide a 'launcher' argument") - def _launch(self, manifest: Manifest) -> None: + def _launch( + self, exp_name: str, exp_path: str, manifest: Manifest + ) -> LaunchedManifest[t.Tuple[str, Step]]: """Main launching function of the controller Orchestrators are always launched first so that the address of the database can be given to following entities + :param exp_name: The name of the launching experiment + :type exp_name: str + :param exp_path: path to location of ``Experiment`` directory if generated + :type exp_path: str :param manifest: Manifest of deployables to launch :type manifest: Manifest """ - orchestrator = manifest.db - if orchestrator: + + manifest_builder = LaunchedManifestBuilder[t.Tuple[str, Step]]( + exp_name=exp_name, exp_path=exp_path, launcher_name=str(self._launcher) + ) + # Loop over deployables to launch and launch multiple orchestrators + for orchestrator in manifest.dbs: + for key in self._jobs.get_db_host_addresses(): + _, db_id = unpack_db_identifier(key, "_") + if orchestrator.db_identifier == db_id: + raise SSDBIDConflictError( + f"Database identifier {orchestrator.db_identifier}" + " has already been used. Pass in a unique" + " name for db_identifier" + ) + if orchestrator.num_shards > 1 and isinstance( self._launcher, LocalLauncher ): raise SmartSimError( "Local launcher does not support multi-host orchestrators" ) - if self.orchestrator_active: - msg = "Attempted to launch a second Orchestrator instance. " - msg += "Only 1 Orchestrator can be active at a time" - raise SmartSimError(msg) - self._launch_orchestrator(orchestrator) + self._launch_orchestrator(orchestrator, manifest_builder) if self.orchestrator_active: self._set_dbobjects(manifest) # create all steps prior to launch - steps: t.List[t.Tuple[Step, t.Union[SmartSimEntity, EntityList]]] = [] - all_entity_lists = manifest.ensembles - for elist in all_entity_lists: + steps: t.List[ + t.Tuple[Step, t.Union[SmartSimEntity, EntitySequence[SmartSimEntity]]] + ] = [] + for elist in manifest.ensembles: + ens_telem_dir = manifest_builder.run_telemetry_subdirectory / "ensemble" if elist.batch: - batch_step = self._create_batch_job_step(elist) + batch_step, substeps = self._create_batch_job_step(elist, ens_telem_dir) + manifest_builder.add_ensemble( + elist, [(batch_step.name, step) for step in substeps] + ) steps.append((batch_step, elist)) else: # if ensemble is to be run as separate job steps, aka not in a batch - job_steps = [(self._create_job_step(e), e) for e in elist.entities] + job_steps = [ + (self._create_job_step(e, ens_telem_dir / elist.name), e) + for e in elist.entities + ] + manifest_builder.add_ensemble( + elist, [(step.name, step) for step, _ in job_steps] + ) steps.extend(job_steps) - # models themselves cannot be batch steps. If batch settings are # attached, wrap them in an anonymous batch job step for model in manifest.models: + model_telem_dir = manifest_builder.run_telemetry_subdirectory / "model" if model.batch_settings: - anon_entity_list = _AnonymousBatchJob( - model.name, model.path, model.batch_settings + anon_entity_list = _AnonymousBatchJob(model) + batch_step, _ = self._create_batch_job_step( + anon_entity_list, model_telem_dir ) - anon_entity_list.entities.append(model) - batch_step = self._create_batch_job_step(anon_entity_list) + manifest_builder.add_model(model, (batch_step.name, batch_step)) steps.append((batch_step, model)) else: - job_step = self._create_job_step(model) + job_step = self._create_job_step(model, model_telem_dir) + manifest_builder.add_model(model, (job_step.name, job_step)) steps.append((job_step, model)) # launch steps for step, entity in steps: self._launch_step(step, entity) - def _launch_orchestrator(self, orchestrator: Orchestrator) -> None: + return manifest_builder.finalize() + + def _launch_orchestrator( + self, + orchestrator: Orchestrator, + manifest_builder: LaunchedManifestBuilder[t.Tuple[str, Step]], + ) -> None: """Launch an Orchestrator instance This function will launch the Orchestrator instance and @@ -340,17 +445,32 @@ def _launch_orchestrator(self, orchestrator: Orchestrator) -> None: :param orchestrator: orchestrator to launch :type orchestrator: Orchestrator + :param manifest_builder: An `LaunchedManifestBuilder` to record the + names and `Step`s of the launched orchestrator + :type manifest_builder: LaunchedManifestBuilder[tuple[str, Step]] """ orchestrator.remove_stale_files() + orc_telem_dir = manifest_builder.run_telemetry_subdirectory / "database" # if the orchestrator was launched as a batch workload if orchestrator.batch: - orc_batch_step = self._create_batch_job_step(orchestrator) + orc_batch_step, substeps = self._create_batch_job_step( + orchestrator, orc_telem_dir + ) + manifest_builder.add_database( + orchestrator, [(orc_batch_step.name, step) for step in substeps] + ) self._launch_step(orc_batch_step, orchestrator) # if orchestrator was run on existing allocation, locally, or in allocation else: - db_steps = [(self._create_job_step(db), db) for db in orchestrator.dbnodes] + db_steps = [ + (self._create_job_step(db, orc_telem_dir / orchestrator.name), db) + for db in orchestrator.entities + ] + manifest_builder.add_database( + orchestrator, [(step.name, step) for step, _ in db_steps] + ) for db_step in db_steps: self._launch_step(*db_step) @@ -386,7 +506,9 @@ def _launch_orchestrator(self, orchestrator: Orchestrator) -> None: logger.debug(f"Orchestrator launched on nodes: {orchestrator.hosts}") def _launch_step( - self, job_step: Step, entity: t.Union[SmartSimEntity, EntityList] + self, + job_step: Step, + entity: t.Union[SmartSimEntity, EntitySequence[SmartSimEntity]], ) -> None: """Use the launcher to launch a job step @@ -418,35 +540,52 @@ def _launch_step( self._jobs.add_job(job_step.name, job_id, entity, is_task) def _create_batch_job_step( - self, entity_list: t.Union[Orchestrator, Ensemble, _AnonymousBatchJob] - ) -> Step: + self, + entity_list: t.Union[Orchestrator, Ensemble, _AnonymousBatchJob], + telemetry_dir: pathlib.Path, + ) -> t.Tuple[Step, t.List[Step]]: """Use launcher to create batch job step :param entity_list: EntityList to launch as batch :type entity_list: EntityList - :return: job step instance - :rtype: Step + :param telemetry_dir: Path to a directory in which the batch job step + may write telemetry events + :type telemetry_dir: pathlib.Path + :return: batch job step instance and a list of run steps to be + executed within the batch job + :rtype: tuple[Step, list[Step]] """ if not entity_list.batch_settings: raise ValueError( "EntityList must have batch settings to be launched as batch" ) + telemetry_dir = telemetry_dir / entity_list.name batch_step = self._launcher.create_step( entity_list.name, entity_list.path, entity_list.batch_settings ) + batch_step.meta["entity_type"] = str(type(entity_list).__name__).lower() + batch_step.meta["status_dir"] = str(telemetry_dir / entity_list.name) + + substeps = [] for entity in entity_list.entities: # tells step creation not to look for an allocation entity.run_settings.in_batch = True - step = self._create_job_step(entity) + step = self._create_job_step(entity, telemetry_dir) + substeps.append(step) batch_step.add_to_batch(step) - return batch_step + return batch_step, substeps - def _create_job_step(self, entity: SmartSimEntity) -> Step: + def _create_job_step( + self, entity: SmartSimEntity, telemetry_dir: pathlib.Path + ) -> Step: """Create job steps for all entities with the launcher :param entity: an entity to create a step for :type entity: SmartSimEntity + :param telemetry_dir: Path to a directory in which the job step + may write telemetry events + :type telemetry_dir: pathlib.Path :return: the job step :rtype: Step """ @@ -455,6 +594,10 @@ def _create_job_step(self, entity: SmartSimEntity) -> Step: self._prep_entity_client_env(entity) step = self._launcher.create_step(entity.name, entity.path, entity.run_settings) + + step.meta["entity_type"] = str(type(entity).__name__).lower() + step.meta["status_dir"] = str(telemetry_dir / entity.name) + return step def _prep_entity_client_env(self, entity: Model) -> None: @@ -463,23 +606,42 @@ def _prep_entity_client_env(self, entity: Model) -> None: :param entity: The entity to retrieve connections from :type entity: Model """ + client_env: t.Dict[str, t.Union[str, int, float, bool]] = {} - addresses = self._jobs.get_db_host_addresses() - if addresses: - if len(addresses) <= 128: - client_env["SSDB"] = ",".join(addresses) - else: + address_dict = self._jobs.get_db_host_addresses() + + for db_id, addresses in address_dict.items(): + db_name, _ = unpack_db_identifier(db_id, "_") + if addresses: # Cap max length of SSDB - client_env["SSDB"] = ",".join(addresses[:128]) - if entity.incoming_entities: - client_env["SSKEYIN"] = ",".join( - [in_entity.name for in_entity in entity.incoming_entities] + client_env[f"SSDB{db_name}"] = ",".join(addresses[:128]) + + # Retrieve num_shards to append to client env + client_env[f"SR_DB_TYPE{db_name}"] = ( + CLUSTERED if len(addresses) > 1 else STANDALONE ) - if entity.query_key_prefixing(): - client_env["SSKEYOUT"] = entity.name + + if entity.incoming_entities: + client_env["SSKEYIN"] = ",".join( + [in_entity.name for in_entity in entity.incoming_entities] + ) + if entity.query_key_prefixing(): + client_env["SSKEYOUT"] = entity.name # Set address to local if it's a colocated model - if entity.colocated: + if entity.colocated and entity.run_settings.colocated_db_settings is not None: + db_name_colo = entity.run_settings.colocated_db_settings["db_identifier"] + + for key in address_dict: + _, db_id = unpack_db_identifier(key, "_") + if db_name_colo == db_id: + raise SSDBIDConflictError( + f"Database identifier {db_name_colo}" + " has already been used. Pass in a unique" + " name for db_identifier" + ) + + db_name_colo = unpack_colo_db_identifier(db_name_colo) if colo_cfg := entity.run_settings.colocated_db_settings: port = colo_cfg.get("port", None) socket = colo_cfg.get("unix_socket", None) @@ -488,13 +650,15 @@ def _prep_entity_client_env(self, entity: Model) -> None: "Co-located was configured for both TCP/IP and UDS" ) if port: - client_env["SSDB"] = f"127.0.0.1:{str(port)}" + client_env[f"SSDB{db_name_colo}"] = f"127.0.0.1:{str(port)}" elif socket: - client_env["SSDB"] = f"unix://{socket}" + client_env[f"SSDB{db_name_colo}"] = f"unix://{socket}" else: raise SSInternalError( "Colocated database was not configured for either TCP or UDS" ) + client_env[f"SR_DB_TYPE{db_name_colo}"] = STANDALONE + entity.run_settings.update_env(client_env) def _save_orchestrator(self, orchestrator: Orchestrator) -> None: @@ -550,7 +714,7 @@ def _orchestrator_launch_wait(self, orchestrator: Orchestrator) -> None: # TODO remove in favor of by node status check time.sleep(CONFIG.jm_interval) elif any(stat in TERMINAL_STATUSES for stat in statuses): - self.stop_entity_list(orchestrator) + self.stop_db(orchestrator) msg = "Orchestrator failed during startup" msg += f" See {orchestrator.path} for details" raise SmartSimError(msg) @@ -558,7 +722,7 @@ def _orchestrator_launch_wait(self, orchestrator: Orchestrator) -> None: logger.debug("Waiting for orchestrator instances to spin up...") except KeyboardInterrupt: logger.info("Orchestrator launch cancelled - requesting to stop") - self.stop_entity_list(orchestrator) + self.stop_db(orchestrator) # re-raise keyboard interrupt so the job manager will display # any running and un-killed jobs as this method is only called @@ -625,47 +789,119 @@ def _set_dbobjects(self, manifest: Manifest) -> None: if not manifest.has_db_objects: return - db_addresses = self._jobs.get_db_host_addresses() + address_dict = self._jobs.get_db_host_addresses() + for ( + db_id, + db_addresses, + ) in address_dict.items(): + db_name, name = unpack_db_identifier(db_id, "_") - hosts = list({address.split(":")[0] for address in db_addresses}) - ports = list({int(address.split(":")[-1]) for address in db_addresses}) + hosts = list({address.split(":")[0] for address in db_addresses}) + ports = list({int(address.split(":")[-1]) for address in db_addresses}) - if not db_is_active(hosts=hosts, ports=ports, num_shards=len(db_addresses)): - raise SSInternalError("Cannot set DB Objects, DB is not running") + if not db_is_active(hosts=hosts, ports=ports, num_shards=len(db_addresses)): + raise SSInternalError("Cannot set DB Objects, DB is not running") - client = Client(address=db_addresses[0], cluster=len(db_addresses) > 1) + environ[f"SSDB{db_name}"] = db_addresses[0] - for model in manifest.models: - if not model.colocated: - for db_model in model.db_models: + environ[f"SR_DB_TYPE{db_name}"] = ( + CLUSTERED if len(db_addresses) > 1 else STANDALONE + ) + + options = ConfigOptions.create_from_environment(name) + client = Client(options, logger_name="SmartSim") + + for model in manifest.models: + if not model.colocated: + for db_model in model.db_models: + set_ml_model(db_model, client) + for db_script in model.db_scripts: + set_script(db_script, client) + + for ensemble in manifest.ensembles: + for db_model in ensemble.db_models: set_ml_model(db_model, client) - for db_script in model.db_scripts: + for db_script in ensemble.db_scripts: set_script(db_script, client) + for entity in ensemble.models: + if not entity.colocated: + # Set models which could belong only + # to the entities and not to the ensemble + # but avoid duplicates + for db_model in entity.db_models: + if db_model not in ensemble.db_models: + set_ml_model(db_model, client) + for db_script in entity.db_scripts: + if db_script not in ensemble.db_scripts: + set_script(db_script, client) + + def _start_telemetry_monitor(self, exp_dir: str) -> None: + """Spawns a telemetry monitor process to keep track of the life times + of the processes launched through this controller. + + :param exp_dir: An experiment directory + :type exp_dir: str + """ + logger.debug("Starting telemetry monitor process") + if ( + self._telemetry_monitor is None + or self._telemetry_monitor.returncode is not None + ): + cmd = [ + sys.executable, + "-m", + "smartsim._core.entrypoints.telemetrymonitor", + "-exp_dir", + exp_dir, + "-frequency", + str(CONFIG.telemetry_frequency), + "-cooldown", + str(CONFIG.telemetry_cooldown), + ] + # pylint: disable-next=consider-using-with + self._telemetry_monitor = subprocess.Popen( + cmd, + stderr=sys.stderr, + stdout=sys.stdout, + cwd=str(pathlib.Path(__file__).parent.parent.parent), + shell=False, + ) - for ensemble in manifest.ensembles: - for db_model in ensemble.db_models: - set_ml_model(db_model, client) - for db_script in ensemble.db_scripts: - set_script(db_script, client) - for entity in ensemble.models: - if not entity.colocated: - # Set models which could belong only - # to the entities and not to the ensemble - # but avoid duplicates - for db_model in entity.db_models: - if db_model not in ensemble.db_models: - set_ml_model(db_model, client) - for db_script in entity.db_scripts: - if db_script not in ensemble.db_scripts: - set_script(db_script, client) - - -class _AnonymousBatchJob(EntityList): - def __init__( - self, name: str, path: str, batch_settings: BatchSettings, **kwargs: t.Any - ) -> None: - super().__init__(name, path) - self.batch_settings = batch_settings - def _initialize_entities(self, **kwargs: t.Any) -> None: - ... +class _AnonymousBatchJob(EntityList[Model]): + @staticmethod + def _validate(model: Model) -> None: + if model.batch_settings is None: + msg = "Unable to create _AnonymousBatchJob without batch_settings" + raise SmartSimError(msg) + + def __init__(self, model: Model) -> None: + self._validate(model) + super().__init__(model.name, model.path) + self.entities = [model] + self.batch_settings = model.batch_settings + + def _initialize_entities(self, **kwargs: t.Any) -> None: ... + + +def _look_up_launched_data( + launcher: Launcher, +) -> t.Callable[[t.Tuple[str, Step]], "TStepLaunchMetaData"]: + def _unpack_launched_data(data: t.Tuple[str, Step]) -> "TStepLaunchMetaData": + # NOTE: we cannot assume that the name of the launched step + # ``launched_step_name`` is equal to the name of the step referring to + # the entity ``step.name`` as is the case when an entity list is + # launched as a batch job + launched_step_name, step = data + launched_step_map = launcher.step_mapping[launched_step_name] + out_file, err_file = step.get_output_files() + return ( + launched_step_map.step_id, + launched_step_map.task_id, + launched_step_map.managed, + out_file, + err_file, + pathlib.Path(step.meta.get("status_dir", step.cwd)), + ) + + return _unpack_launched_data diff --git a/smartsim/_core/control/job.py b/smartsim/_core/control/job.py index 41b60d709..aa4ecce76 100644 --- a/smartsim/_core/control/job.py +++ b/smartsim/_core/control/job.py @@ -26,11 +26,45 @@ import time import typing as t +from dataclasses import dataclass -from ...entity import SmartSimEntity, EntityList +from ...entity import EntitySequence, SmartSimEntity from ...status import STATUS_NEW +@dataclass(frozen=True) +class _JobKey: + step_id: str + task_id: str + + +class JobEntity: + """API required for a job processed in the JobManager with support for + telemetry monitoring + """ + + def __init__(self) -> None: + self.name: str = "" + self.path: str = "" + self.step_id: str = "" + self.task_id: str = "" + self.type: str = "" + self.timestamp: int = 0 + self.status_dir: str = "" + + @property + def is_db(self) -> bool: + return self.type in ["orchestrator", "dbnode"] + + @property + def is_managed(self) -> bool: + return bool(self.step_id) + + @property + def key(self) -> _JobKey: + return _JobKey(self.step_id, self.task_id) + + class Job: """Keep track of various information for the controller. In doing so, continuously add various fields of information @@ -42,7 +76,7 @@ def __init__( self, job_name: str, job_id: t.Optional[str], - entity: t.Union[SmartSimEntity, EntityList], + entity: t.Union[SmartSimEntity, EntitySequence[SmartSimEntity], JobEntity], launcher: str, is_task: bool, ) -> None: @@ -53,7 +87,7 @@ def __init__( :param job_id: The id associated with the job :type job_id: str :param entity: The SmartSim entity(list) associated with the job - :type entity: SmartSimEntity | EntityList + :type entity: SmartSimEntity | EntitySequence | JobEntity :param launcher: Launcher job was started with :type launcher: str :param is_task: process monitored by TaskManager (True) or the WLM (True) diff --git a/smartsim/_core/control/jobmanager.py b/smartsim/_core/control/jobmanager.py index 21fcf1223..d23543030 100644 --- a/smartsim/_core/control/jobmanager.py +++ b/smartsim/_core/control/jobmanager.py @@ -24,21 +24,22 @@ # 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. + import itertools import time import typing as t -from threading import Thread, RLock +from collections import ChainMap +from threading import RLock, Thread from types import FrameType from ...database import Orchestrator -from ...entity import DBNode, SmartSimEntity, EntityList -from ...error import SmartSimError +from ...entity import DBNode, EntitySequence, SmartSimEntity from ...log import get_logger -from ...status import TERMINAL_STATUSES +from ...status import STATUS_NEVER_STARTED, TERMINAL_STATUSES from ..config import CONFIG -from ..launcher import LocalLauncher, Launcher +from ..launcher import Launcher, LocalLauncher from ..utils.network import get_ip_from_host -from .job import Job +from .job import Job, JobEntity logger = get_logger(__name__) @@ -145,13 +146,8 @@ def __getitem__(self, entity_name: str) -> Job: :rtype: Job """ with self._lock: - if entity_name in self.db_jobs: - return self.db_jobs[entity_name] - if entity_name in self.jobs: - return self.jobs[entity_name] - if entity_name in self.completed: - return self.completed[entity_name] - raise KeyError + entities = ChainMap(self.db_jobs, self.jobs, self.completed) + return entities[entity_name] def __call__(self) -> t.Dict[str, Job]: """Returns dictionary all jobs for () operator @@ -162,11 +158,18 @@ def __call__(self) -> t.Dict[str, Job]: all_jobs = {**self.jobs, **self.db_jobs} return all_jobs + def __contains__(self, key: str) -> bool: + try: + self[key] # pylint: disable=pointless-statement + return True + except KeyError: + return False + def add_job( self, job_name: str, job_id: t.Optional[str], - entity: t.Union[SmartSimEntity, EntityList], + entity: t.Union[SmartSimEntity, EntitySequence[SmartSimEntity], JobEntity], is_task: bool = True, ) -> None: """Add a job to the job manager which holds specific jobs by type. @@ -176,7 +179,7 @@ def add_job( :param job_id: job step id created by launcher :type job_id: str :param entity: entity that was launched on job step - :type entity: SmartSimEntity | EntityList + :type entity: SmartSimEntity | EntitySequence :param is_task: process monitored by TaskManager (True) or the WLM (True) :type is_task: bool """ @@ -185,6 +188,8 @@ def add_job( job = Job(job_name, job_id, entity, launcher, is_task) if isinstance(entity, (DBNode, Orchestrator)): self.db_jobs[entity.name] = job + elif isinstance(entity, JobEntity) and entity.is_db: + self.db_jobs[entity.name] = job else: self.jobs[entity.name] = job @@ -231,25 +236,25 @@ def check_jobs(self) -> None: output=status.output, ) - def get_status(self, entity: t.Union[SmartSimEntity, EntityList]) -> str: + def get_status( + self, + entity: t.Union[SmartSimEntity, EntitySequence[SmartSimEntity]], + ) -> str: """Return the status of a job. - :param entity: SmartSimEntity or EntityList instance - :type entity: SmartSimEntity | EntityList + :param entity: SmartSimEntity or EntitySequence instance + :type entity: SmartSimEntity | EntitySequence :returns: tuple of status """ with self._lock: - try: - if entity.name in self.completed: - return self.completed[entity.name].status + if entity.name in self.completed: + return self.completed[entity.name].status + if entity.name in self: job: Job = self[entity.name] # locked - except KeyError: - raise SmartSimError( - f"Entity {entity.name} has not been launched in this Experiment" - ) from None + return job.status - return job.status + return STATUS_NEVER_STARTED def set_launcher(self, launcher: Launcher) -> None: """Set the launcher of the job manager to a specific launcher instance @@ -301,21 +306,28 @@ def restart_job( else: self.jobs[entity_name] = job - def get_db_host_addresses(self) -> t.List[str]: + def get_db_host_addresses(self) -> t.Dict[str, t.List[str]]: """Retrieve the list of hosts for the database + for corresponding database identifiers - :return: list of host ip addresses - :rtype: list[str] + :return: dictionary of host ip addresses + :rtype: Dict[str, list] """ - addresses = [] + + address_dict: t.Dict[str, t.List[str]] = {} for db_job in self.db_jobs.values(): + addresses = [] if isinstance(db_job.entity, (DBNode, Orchestrator)): db_entity = db_job.entity - for combine in itertools.product(db_job.hosts, db_entity.ports): ip_addr = get_ip_from_host(combine[0]) addresses.append(":".join((ip_addr, str(combine[1])))) - return addresses + + dict_entry: t.List[str] = address_dict.get(db_entity.db_identifier, []) + dict_entry.extend(addresses) + address_dict[db_entity.db_identifier] = dict_entry + + return address_dict def set_db_hosts(self, orchestrator: Orchestrator) -> None: """Set the DB hosts in db_jobs so future entities can query this @@ -324,11 +336,13 @@ def set_db_hosts(self, orchestrator: Orchestrator) -> None: :type orchestrator: Orchestrator """ # should only be called during launch in the controller + with self._lock: if orchestrator.batch: self.db_jobs[orchestrator.name].hosts = orchestrator.hosts + else: - for dbnode in orchestrator.dbnodes: + for dbnode in orchestrator.entities: if not dbnode.is_mpmd: self.db_jobs[dbnode.name].hosts = [dbnode.host] else: diff --git a/smartsim/_core/control/manifest.py b/smartsim/_core/control/manifest.py index 0ebac3c2e..62ab013e5 100644 --- a/smartsim/_core/control/manifest.py +++ b/smartsim/_core/control/manifest.py @@ -24,43 +24,52 @@ # 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. +import pathlib import typing as t +from dataclasses import dataclass, field from ...database import Orchestrator -from ...entity import EntityList, SmartSimEntity, Model, Ensemble +from ...entity import DBNode, Ensemble, EntitySequence, Model, SmartSimEntity from ...error import SmartSimError -from ..utils.helpers import fmt_dict +from ..utils import helpers as _helpers +from ..utils import serialize as _serialize + +_T = t.TypeVar("_T") +_U = t.TypeVar("_U") +_AtomicLaunchableT = t.TypeVar("_AtomicLaunchableT", Model, DBNode) + +if t.TYPE_CHECKING: + import os class Manifest: - """This class is used to keep track of all deployables generated by an experiment. - Different types of deployables (i.e. different `SmartSimEntity`-derived objects or - `EntityList`-derived objects) can be accessed by using the corresponding accessor. + """This class is used to keep track of all deployables generated by an + experiment. Different types of deployables (i.e. different + `SmartSimEntity`-derived objects or `EntitySequence`-derived objects) can + be accessed by using the corresponding accessor. Instances of ``Model``, ``Ensemble`` and ``Orchestrator`` can all be passed as arguments """ - def __init__(self, *args: SmartSimEntity) -> None: + def __init__( + self, *args: t.Union[SmartSimEntity, EntitySequence[SmartSimEntity]] + ) -> None: self._deployables = list(args) self._check_types(self._deployables) self._check_names(self._deployables) self._check_entity_lists_nonempty() @property - def db(self) -> t.Optional[Orchestrator]: - """Return Orchestrator instances in Manifest + def dbs(self) -> t.List[Orchestrator]: + """Return a list of Orchestrator instances in Manifest :raises SmartSimError: if user added to databases to manifest - :return: orchestrator instances - :rtype: Orchestrator | None + :return: List of orchestrator instances + :rtype: list[Orchestrator] """ dbs = [item for item in self._deployables if isinstance(item, Orchestrator)] - - if len(dbs) > 1: - raise SmartSimError("User attempted to create more than one Orchestrator") - - return dbs[0] if dbs else None + return dbs @property def models(self) -> t.List[Model]: @@ -84,18 +93,16 @@ def ensembles(self) -> t.List[Ensemble]: return [e for e in self._deployables if isinstance(e, Ensemble)] @property - def all_entity_lists(self) -> t.List[EntityList]: + def all_entity_lists(self) -> t.List[EntitySequence[SmartSimEntity]]: """All entity lists, including ensembles and exceptional ones like Orchestrator :return: list of entity lists - :rtype: List[EntityList] + :rtype: List[EntitySequence[SmartSimEntity]] """ - _all_entity_lists: t.List[EntityList] = [] - _all_entity_lists.extend(self.ensembles) + _all_entity_lists: t.List[EntitySequence[SmartSimEntity]] = list(self.ensembles) - db = self.db - if db is not None: + for db in self.dbs: _all_entity_lists.append(db) return _all_entity_lists @@ -114,10 +121,10 @@ def _check_names(deployables: t.List[t.Any]) -> None: @staticmethod def _check_types(deployables: t.List[t.Any]) -> None: for deployable in deployables: - if not isinstance(deployable, (SmartSimEntity, EntityList)): + if not isinstance(deployable, (SmartSimEntity, EntitySequence)): raise TypeError( f"Entity has type {type(deployable)}, not " - + "SmartSimEntity or EntityList" + + "SmartSimEntity or EntitySequence" ) def _check_entity_lists_nonempty(self) -> None: @@ -152,17 +159,17 @@ def __str__(self) -> str: output += f"{model.batch_settings}\n" output += f"{model.run_settings}\n" if model.params: - output += f"Parameters: \n{fmt_dict(model.params)}\n" + output += f"Parameters: \n{_helpers.fmt_dict(model.params)}\n" output += "\n" - if self.db: + for adb in self.dbs: output += db_header - output += f"Shards: {self.db.num_shards}\n" - output += f"Port: {str(self.db.ports[0])}\n" - output += f"Network: {self.db._interfaces}\n" - output += f"Batch Launch: {self.db.batch}\n" - if self.db.batch: - output += f"{str(self.db.batch_settings)}\n" + output += f"Shards: {adb.num_shards}\n" + output += f"Port: {str(adb.ports[0])}\n" + output += f"Network: {adb._interfaces}\n" + output += f"Batch Launch: {adb.batch}\n" + if adb.batch: + output += f"{str(adb.batch_settings)}\n" output += "\n" return output @@ -171,15 +178,17 @@ def __str__(self) -> str: def has_db_objects(self) -> bool: """Check if any entity has DBObjects to set""" - def has_db_models(entity: t.Union[EntityList, Model]) -> bool: + def has_db_models( + entity: t.Union[EntitySequence[SmartSimEntity], Model] + ) -> bool: return len(list(entity.db_models)) > 0 - def has_db_scripts(entity: t.Union[EntityList, Model]) -> bool: + def has_db_scripts( + entity: t.Union[EntitySequence[SmartSimEntity], Model] + ) -> bool: return len(list(entity.db_scripts)) > 0 has_db_objects = False - for model in self.models: - has_db_objects |= hasattr(model, "_db_models") # Check if any model has either a DBModel or a DBScript # we update has_db_objects so that as soon as one check @@ -214,3 +223,130 @@ def has_db_scripts(entity: t.Union[EntityList, Model]) -> bool: # `has_db_objects` should be False here return has_db_objects + + +class _LaunchedManifestMetadata(t.NamedTuple): + run_id: str + exp_name: str + exp_path: str + launcher_name: str + + @property + def exp_telemetry_subdirectory(self) -> pathlib.Path: + return _format_exp_telemetry_path(self.exp_path) + + @property + def run_telemetry_subdirectory(self) -> pathlib.Path: + return _format_run_telemetry_path(self.exp_path, self.exp_name, self.run_id) + + @property + def manifest_file_path(self) -> pathlib.Path: + return self.exp_telemetry_subdirectory / _serialize.MANIFEST_FILENAME + + +@dataclass(frozen=True) +class LaunchedManifest(t.Generic[_T]): + """Immutable manifest mapping launched entities or collections of launched + entities to other pieces of external data. This is commonly used to map a + launch-able entity to its constructed ``Step`` instance without assuming + that ``step.name == job.name`` or querying the ``JobManager`` which itself + can be ephemeral. + """ + + metadata: _LaunchedManifestMetadata + models: t.Tuple[t.Tuple[Model, _T], ...] + ensembles: t.Tuple[t.Tuple[Ensemble, t.Tuple[t.Tuple[Model, _T], ...]], ...] + databases: t.Tuple[t.Tuple[Orchestrator, t.Tuple[t.Tuple[DBNode, _T], ...]], ...] + + def map(self, func: t.Callable[[_T], _U]) -> "LaunchedManifest[_U]": + def _map_entity_data( + fn: t.Callable[[_T], _U], + entity_list: t.Sequence[t.Tuple[_AtomicLaunchableT, _T]], + ) -> t.Tuple[t.Tuple[_AtomicLaunchableT, _U], ...]: + return tuple((entity, fn(data)) for entity, data in entity_list) + + return LaunchedManifest( + metadata=self.metadata, + models=_map_entity_data(func, self.models), + ensembles=tuple( + (ens, _map_entity_data(func, model_data)) + for ens, model_data in self.ensembles + ), + databases=tuple( + (db_, _map_entity_data(func, node_data)) + for db_, node_data in self.databases + ), + ) + + +@dataclass(frozen=True) +class LaunchedManifestBuilder(t.Generic[_T]): + """A class comprised of mutable collections of SmartSim entities that is + used to build a ``LaunchedManifest`` while going through the launching + process. + """ + + exp_name: str + exp_path: str + launcher_name: str + run_id: str = field(default_factory=_helpers.create_short_id_str) + + _models: t.List[t.Tuple[Model, _T]] = field(default_factory=list, init=False) + _ensembles: t.List[t.Tuple[Ensemble, t.Tuple[t.Tuple[Model, _T], ...]]] = field( + default_factory=list, init=False + ) + _databases: t.List[t.Tuple[Orchestrator, t.Tuple[t.Tuple[DBNode, _T], ...]]] = ( + field(default_factory=list, init=False) + ) + + @property + def exp_telemetry_subdirectory(self) -> pathlib.Path: + return _format_exp_telemetry_path(self.exp_path) + + @property + def run_telemetry_subdirectory(self) -> pathlib.Path: + return _format_run_telemetry_path(self.exp_path, self.exp_name, self.run_id) + + def add_model(self, model: Model, data: _T) -> None: + self._models.append((model, data)) + + def add_ensemble(self, ens: Ensemble, data: t.Sequence[_T]) -> None: + self._ensembles.append((ens, self._entities_to_data(ens.entities, data))) + + def add_database(self, db_: Orchestrator, data: t.Sequence[_T]) -> None: + self._databases.append((db_, self._entities_to_data(db_.entities, data))) + + @staticmethod + def _entities_to_data( + entities: t.Sequence[_AtomicLaunchableT], data: t.Sequence[_T] + ) -> t.Tuple[t.Tuple[_AtomicLaunchableT, _T], ...]: + if not entities: + raise ValueError("Cannot map data to an empty entity sequence") + if len(entities) != len(data): + raise ValueError( + f"Cannot map data sequence of length {len(data)} to entity " + f"sequence of length {len(entities)}" + ) + return tuple(zip(entities, data)) + + def finalize(self) -> LaunchedManifest[_T]: + return LaunchedManifest( + metadata=_LaunchedManifestMetadata( + self.run_id, self.exp_name, self.exp_path, self.launcher_name + ), + models=tuple(self._models), + ensembles=tuple(self._ensembles), + databases=tuple(self._databases), + ) + + +def _format_exp_telemetry_path( + exp_path: t.Union[str, "os.PathLike[str]"] +) -> pathlib.Path: + return pathlib.Path(exp_path, _serialize.TELMON_SUBDIR) + + +def _format_run_telemetry_path( + exp_path: t.Union[str, "os.PathLike[str]"], exp_name: str, run_id: str +) -> pathlib.Path: + return _format_exp_telemetry_path(exp_path) / f"{exp_name}/{run_id}" diff --git a/smartsim/_core/entrypoints/colocated.py b/smartsim/_core/entrypoints/colocated.py index ba82d355f..332d6e019 100644 --- a/smartsim/_core/entrypoints/colocated.py +++ b/smartsim/_core/entrypoints/colocated.py @@ -31,15 +31,13 @@ import sys import tempfile import typing as t - from pathlib import Path from subprocess import PIPE, STDOUT from types import FrameType - import filelock import psutil -from smartredis import Client +from smartredis import Client, ConfigOptions from smartredis.error import RedisConnectionError, RedisReplyError from smartsim._core.utils.network import current_ip @@ -75,15 +73,14 @@ def launch_db_model(client: Client, db_model: t.List[str]) -> str: parser.add_argument("--file", type=str) parser.add_argument("--backend", type=str) parser.add_argument("--device", type=str) - parser.add_argument("--devices_per_node", type=int) + parser.add_argument("--devices_per_node", type=int, default=1) + parser.add_argument("--first_device", type=int, default=0) parser.add_argument("--batch_size", type=int, default=0) parser.add_argument("--min_batch_size", type=int, default=0) + parser.add_argument("--min_batch_timeout", type=int, default=0) parser.add_argument("--tag", type=str, default="") parser.add_argument("--inputs", nargs="+", default=None) parser.add_argument("--outputs", nargs="+", default=None) - - # Unused if we use SmartRedis - parser.add_argument("--min_batch_timeout", type=int, default=None) args = parser.parse_args(db_model) inputs = None @@ -99,28 +96,30 @@ def launch_db_model(client: Client, db_model: t.List[str]) -> str: # devices_per_node being greater than one only applies to GPU devices if args.devices_per_node > 1 and args.device.lower() == "gpu": client.set_model_from_file_multigpu( - name, - args.file, - args.backend, - 0, - args.devices_per_node, - args.batch_size, - args.min_batch_size, - args.tag, - inputs, - outputs, + name=name, + model_file=args.file, + backend=args.backend, + first_gpu=args.first_device, + num_gpus=args.devices_per_node, + batch_size=args.batch_size, + min_batch_size=args.min_batch_size, + min_batch_timeout=args.min_batch_timeout, + tag=args.tag, + inputs=inputs, + outputs=outputs, ) else: client.set_model_from_file( - name, - args.file, - args.backend, - args.device, - args.batch_size, - args.min_batch_size, - args.tag, - inputs, - outputs, + name=name, + model_file=args.file, + backend=args.backend, + device=args.device, + batch_size=args.batch_size, + min_batch_size=args.min_batch_size, + min_batch_timeout=args.min_batch_timeout, + tag=args.tag, + inputs=inputs, + outputs=outputs, ) return name @@ -142,7 +141,8 @@ def launch_db_script(client: Client, db_script: t.List[str]) -> str: parser.add_argument("--file", type=str) parser.add_argument("--backend", type=str) parser.add_argument("--device", type=str) - parser.add_argument("--devices_per_node", type=int) + parser.add_argument("--devices_per_node", type=int, default=1) + parser.add_argument("--first_device", type=int, default=0) args = parser.parse_args(db_script) if args.file and args.func: @@ -151,13 +151,15 @@ def launch_db_script(client: Client, db_script: t.List[str]) -> str: if args.func: func = args.func.replace("\\n", "\n") if args.devices_per_node > 1 and args.device.lower() == "gpu": - client.set_script_multigpu(args.name, func, 0, args.devices_per_node) + client.set_script_multigpu( + args.name, func, args.first_device, args.devices_per_node + ) else: client.set_script(args.name, func, args.device) elif args.file: if args.devices_per_node > 1 and args.device.lower() == "gpu": client.set_script_from_file_multigpu( - args.name, args.file, 0, args.devices_per_node + args.name, args.file, args.first_device, args.devices_per_node ) else: client.set_script_from_file(args.name, args.file, args.device) @@ -173,18 +175,19 @@ def main( command: t.List[str], db_models: t.List[t.List[str]], db_scripts: t.List[t.List[str]], + db_identifier: str, ) -> None: global DBPID # pylint: disable=global-statement lo_address = current_ip("lo") - try: - ip_addresses = [ - current_ip(interface) for interface in network_interface.split(",") - ] - - except ValueError as e: - logger.warning(e) - ip_addresses = [] + ip_addresses = [] + if network_interface: + try: + ip_addresses = [ + current_ip(interface) for interface in network_interface.split(",") + ] + except ValueError as e: + logger.warning(e) if all(lo_address == ip_address for ip_address in ip_addresses) or not ip_addresses: cmd = command + [f"--bind {lo_address}"] @@ -212,6 +215,7 @@ def main( f"\n\tIP Address(es): {' '.join(ip_addresses + [lo_address])}" f"\n\tCommand: {' '.join(cmd)}\n\n" f"\n\t# of Database CPUs: {db_cpus}" + f"\n\tDatabase Identifier: {db_identifier}" ) except Exception as e: cleanup() @@ -233,7 +237,8 @@ def launch_db_scripts(client: Client, db_scripts: t.List[t.List[str]]) -> None: try: if db_models or db_scripts: try: - client = Client(cluster=False) + options = ConfigOptions.create_from_environment(db_identifier) + client = Client(options, logger_name="SmartSim") launch_models(client, db_models) launch_db_scripts(client, db_scripts) except (RedisConnectionError, RedisReplyError) as ex: @@ -291,6 +296,11 @@ def register_signal_handlers() -> None: arg_parser.add_argument( "+db_cpus", type=int, default=2, help="Number of CPUs to use for DB" ) + + arg_parser.add_argument( + "+db_identifier", type=str, default="", help="Database Identifier" + ) + arg_parser.add_argument("+command", nargs="+", help="Command to run") arg_parser.add_argument( "+db_model", @@ -317,7 +327,7 @@ def register_signal_handlers() -> None: LOCK.acquire(timeout=0.1) logger.debug(f"Starting colocated database on host: {socket.gethostname()}") - # make sure to register the cleanup before the start + # make sure to register the cleanup before we start # the proecss so our signaller will be able to stop # the database process. register_signal_handlers() @@ -328,6 +338,7 @@ def register_signal_handlers() -> None: parsed_args.command, parsed_args.db_model, parsed_args.db_script, + parsed_args.db_identifier, ) # gracefully exit the processes in the distributed application that diff --git a/smartsim/_core/entrypoints/indirect.py b/smartsim/_core/entrypoints/indirect.py new file mode 100644 index 000000000..18d27601f --- /dev/null +++ b/smartsim/_core/entrypoints/indirect.py @@ -0,0 +1,242 @@ +# BSD 2-Clause License +# +# Copyright (c) 2021-2023 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. + +import argparse +import logging +import os +import pathlib +import signal +import sys +import typing as t +from types import FrameType + +import coloredlogs +import psutil + +import smartsim.log +from smartsim._core.entrypoints.telemetrymonitor import track_event +from smartsim._core.utils.helpers import decode_cmd, get_ts + +STEP_PID: t.Optional[int] = None +logger = smartsim.log.get_logger(__name__) + +# kill is not catchable +SIGNALS = [signal.SIGINT, signal.SIGTERM, signal.SIGQUIT, signal.SIGABRT] + + +def main( + cmd: str, + etype: str, + cwd: str, + status_dir: str, +) -> int: + """The main function of the entrypoint. This function takes an encoded step + command and runs it in a subprocess. In the background, this entrypoint + will then monitor the subprocess and write out status events such as when + the subprocess has started or stopped and write these events to a status + directory. + """ + global STEP_PID # pylint: disable=global-statement + proxy_pid = os.getpid() + + status_path = pathlib.Path(status_dir) + if not status_path.exists(): + status_path.mkdir(parents=True, exist_ok=True) + + if not cmd.strip(): + raise ValueError("Invalid cmd supplied") + + cleaned_cmd = decode_cmd(cmd) + ret_code: int = 1 + logger.debug("Indirect step starting") + + start_detail = f"Proxy process {proxy_pid}" + start_rc: t.Optional[int] = None + + try: + process = psutil.Popen( + cleaned_cmd, + cwd=cwd, + stdout=sys.stdout, + stderr=sys.stderr, + ) + STEP_PID = process.pid + logger.info(f"Indirect proxy {proxy_pid} child process {STEP_PID} started") + start_detail += f" started child process {STEP_PID}" + + except Exception as ex: + start_detail += f" failed to start child process. {ex}" + start_rc = 1 + logger.error("Failed to create process", exc_info=True) + cleanup() + return 1 + finally: + track_event( + get_ts(), + proxy_pid, + "", # step_id for unmanaged task is always empty + etype, + "start", + status_path, + logger, + detail=start_detail, + return_code=start_rc, + ) + + logger.info(f"Waiting for child process {STEP_PID} to complete") + ret_code = process.wait() + + logger.info( + f"Indirect proxy {proxy_pid} child process {STEP_PID} complete." + f" return code: {ret_code}" + ) + msg = f"Process {STEP_PID} finished with return code: {ret_code}" + track_event( + get_ts(), + proxy_pid, + "", # step_id for unmanaged task is always empty + etype, + "stop", + status_path, + logger, + detail=msg, + return_code=ret_code, + ) + cleanup() + + return ret_code + + +def cleanup() -> None: + """Perform cleanup required for clean termination""" + logger.info("Performing cleanup") + global STEP_PID # pylint: disable=global-statement + if STEP_PID is None: + return + + try: + # attempt to stop the subprocess performing step-execution + if psutil.pid_exists(STEP_PID): + process = psutil.Process(STEP_PID) + process.terminate() + except psutil.NoSuchProcess: + # swallow exception to avoid overwriting outputs from cmd + ... + + except OSError as ex: + logger.warning(f"Failed to clean up step executor gracefully: {ex}") + finally: + STEP_PID = None + + +def handle_signal(signo: int, _frame: t.Optional[FrameType]) -> None: + """Helper function to ensure clean process termination""" + logger.info(f"handling signal {signo}") + if not signo: + logger.warning("Received signal with no signo") + + cleanup() + + +def register_signal_handlers() -> None: + """Register a signal handling function for all termination events""" + for sig in SIGNALS: + signal.signal(sig, handle_signal) + + +def get_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + prefix_chars="+", description="SmartSim Step Executor" + ) + parser.add_argument( + "+name", type=str, help="Name of the step being executed", required=True + ) + parser.add_argument( + "+command", type=str, help="The command to execute", required=True + ) + parser.add_argument( + "+entity_type", + type=str, + help="The type of entity related to the step", + required=True, + ) + parser.add_argument( + "+working_dir", + type=str, + help="The working directory of the executable", + required=True, + ) + parser.add_argument( + "+telemetry_dir", + type=str, + help="Directory for telemetry output", + required=True, + ) + return parser + + +if __name__ == "__main__": + arg_parser = get_parser() + os.environ["PYTHONUNBUFFERED"] = "1" + parsed_args = arg_parser.parse_args() + + # Set up a local private logger for when this module is run as an entry point + level = logger.getEffectiveLevel() + logger = logging.getLogger(f"{__name__}.{parsed_args.name}") + logger.propagate = False + logger.setLevel(level) + + fh = logging.FileHandler(f"{parsed_args.name}.indirect.log") + coloredlogs.HostNameFilter.install(fh) + fh.setFormatter( + logging.Formatter( + smartsim.log.DEFAULT_LOG_FORMAT, + datefmt=smartsim.log.DEFAULT_DATE_FORMAT, + ) + ) + logger.addHandler(fh) + + try: + logger.debug("Starting indirect step execution") + + # make sure to register the cleanup before the start the process + # so our signaller will be able to stop the database process. + register_signal_handlers() + + rc = main( + cmd=parsed_args.command, + etype=parsed_args.entity_type, + cwd=parsed_args.working_dir, + status_dir=parsed_args.telemetry_dir, + ) + sys.exit(rc) + + # gracefully exit the processes in the distributed application that + # we do not want to have start a colocated process. Only one process + # per node should be running. + except Exception as e: + logger.exception(f"An unexpected error caused step execution to fail: {e}") + sys.exit(1) diff --git a/smartsim/_core/entrypoints/redis.py b/smartsim/_core/entrypoints/redis.py index 782b4c583..ef9911829 100644 --- a/smartsim/_core/entrypoints/redis.py +++ b/smartsim/_core/entrypoints/redis.py @@ -25,16 +25,20 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import argparse +import json import os -import psutil import signal +import textwrap import typing as t +from subprocess import PIPE, STDOUT +from types import FrameType + +import psutil from smartsim._core.utils.network import current_ip +from smartsim.entity.dbnode import LaunchedShardData from smartsim.error import SSInternalError from smartsim.log import get_logger -from subprocess import PIPE, STDOUT -from types import FrameType logger = get_logger(__name__) @@ -42,7 +46,7 @@ Redis/KeyDB entrypoint script """ -DBPID = None +DBPID: t.Optional[int] = None # kill is not catchable SIGNALS = [signal.SIGINT, signal.SIGQUIT, signal.SIGTERM, signal.SIGABRT] @@ -54,31 +58,62 @@ def handle_signal(signo: int, _frame: t.Optional[FrameType]) -> None: cleanup() -def build_bind_args(ip_addresses: t.List[str]) -> t.List[str]: - bind_arg = f"--bind {' '.join(ip_addresses)}" - # pin source address to avoid random selection by Redis - bind_src_arg = f"--bind-source-addr {ip_addresses[0]}" - return [bind_arg, bind_src_arg] +def build_bind_args(source_addr: str, *addrs: str) -> t.Tuple[str, ...]: + return ( + "--bind", + source_addr, + *addrs, + # pin source address to avoid random selection by Redis + "--bind-source-addr", + source_addr, + ) -def print_summary(cmd: t.List[str], ip_address: str, network_interface: str) -> None: - print("-" * 10, " Running Command ", "-" * 10, "\n", flush=True) - print(f"COMMAND: {' '.join(cmd)}\n", flush=True) - print(f"IPADDRESS: {ip_address}\n", flush=True) - print(f"NETWORK: {network_interface}\n", flush=True) - print("-" * 30, "\n\n", flush=True) - print("-" * 10, " Output ", "-" * 10, "\n\n", flush=True) +def build_cluster_args(shard_data: LaunchedShardData) -> t.Tuple[str, ...]: + if cluster_conf_file := shard_data.cluster_conf_file: + return ("--cluster-enabled", "yes", "--cluster-config-file", cluster_conf_file) + return () -def main(network_interface: str, command: t.List[str]) -> None: +def print_summary( + cmd: t.List[str], network_interface: str, shard_data: LaunchedShardData +) -> None: + print( + textwrap.dedent(f"""\ + ----------- Running Command ---------- + COMMAND: {' '.join(cmd)} + IPADDRESS: {shard_data.hostname} + NETWORK: {network_interface} + SMARTSIM_ORC_SHARD_INFO: {json.dumps(shard_data.to_dict())} + -------------------------------------- + + --------------- Output --------------- + + """), + flush=True, + ) + + +def main(args: argparse.Namespace) -> int: global DBPID # pylint: disable=global-statement - try: - ip_addresses = [current_ip(net_if) for net_if in network_interface.split(",")] - cmd = command + build_bind_args(ip_addresses) + src_addr, *bind_addrs = (current_ip(net_if) for net_if in args.ifname.split(",")) + shard_data = LaunchedShardData( + name=args.name, hostname=src_addr, port=args.port, cluster=args.cluster + ) - print_summary(cmd, ip_addresses[0], network_interface) + cmd = [ + args.orc_exe, + args.conf_file, + *args.rai_module, + "--port", + str(args.port), + *build_cluster_args(shard_data), + *build_bind_args(src_addr, *bind_addrs), + ] + print_summary(cmd, args.ifname, shard_data) + try: process = psutil.Popen(cmd, stdout=PIPE, stderr=STDOUT) DBPID = process.pid @@ -87,18 +122,17 @@ def main(network_interface: str, command: t.List[str]) -> None: except Exception as e: cleanup() raise SSInternalError("Database process starter raised an exception") from e + return 0 def cleanup() -> None: + logger.debug("Cleaning up database instance") try: - logger.debug("Cleaning up database instance") # attempt to stop the database process - db_proc = psutil.Process(DBPID) - db_proc.terminate() - + if DBPID is not None: + psutil.Process(DBPID).terminate() except psutil.NoSuchProcess: logger.warning("Couldn't find database process to kill.") - except OSError as e: logger.warning(f"Failed to clean up database gracefully: {str(e)}") @@ -110,15 +144,47 @@ def cleanup() -> None: prefix_chars="+", description="SmartSim Process Launcher" ) parser.add_argument( - "+ifname", type=str, help="Network Interface name", default="lo" + "+orc-exe", type=str, help="Path to the orchestrator executable", required=True + ) + parser.add_argument( + "+conf-file", + type=str, + help="Path to the orchestrator configuration file", + required=True, + ) + parser.add_argument( + "+rai-module", + nargs="+", + type=str, + help=( + "Command for the orcestrator to load the Redis AI module with " + "symbols seperated by whitespace" + ), + required=True, + ) + parser.add_argument( + "+name", type=str, help="Name to identify the shard", required=True + ) + parser.add_argument( + "+port", + type=int, + help="The port on which to launch the shard of the orchestrator", + required=True, + ) + parser.add_argument( + "+ifname", type=str, help="Network Interface name", required=True + ) + parser.add_argument( + "+cluster", + action="store_true", + help="Specify if this orchestrator shard is part of a cluster", ) - parser.add_argument("+command", nargs="+", help="Command to run") - args = parser.parse_args() + args_ = parser.parse_args() # make sure to register the cleanup before the start - # the proecss so our signaller will be able to stop + # the process so our signaller will be able to stop # the database process. for sig in SIGNALS: signal.signal(sig, handle_signal) - main(args.ifname, args.command) + raise SystemExit(main(args_)) diff --git a/smartsim/_core/entrypoints/telemetrymonitor.py b/smartsim/_core/entrypoints/telemetrymonitor.py new file mode 100644 index 000000000..86d6fe72f --- /dev/null +++ b/smartsim/_core/entrypoints/telemetrymonitor.py @@ -0,0 +1,690 @@ +# BSD 2-Clause License +# +# Copyright (c) 2021-2023 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. + +import argparse +import json +import logging +import os +import pathlib +import signal +import sys +import threading +import time +import typing as t +from dataclasses import dataclass, field +from types import FrameType + +from watchdog.events import ( + FileCreatedEvent, + FileModifiedEvent, + LoggingEventHandler, + PatternMatchingEventHandler, +) +from watchdog.observers import Observer +from watchdog.observers.api import BaseObserver + +from smartsim._core.config import CONFIG +from smartsim._core.control.job import JobEntity, _JobKey +from smartsim._core.control.jobmanager import JobManager +from smartsim._core.launcher.cobalt.cobaltLauncher import CobaltLauncher +from smartsim._core.launcher.launcher import Launcher +from smartsim._core.launcher.local.local import LocalLauncher +from smartsim._core.launcher.lsf.lsfLauncher import LSFLauncher +from smartsim._core.launcher.pbs.pbsLauncher import PBSLauncher +from smartsim._core.launcher.slurm.slurmLauncher import SlurmLauncher +from smartsim._core.launcher.stepInfo import StepInfo +from smartsim._core.utils.helpers import get_ts +from smartsim._core.utils.serialize import MANIFEST_FILENAME, TELMON_SUBDIR +from smartsim.error.errors import SmartSimError +from smartsim.status import STATUS_COMPLETED, TERMINAL_STATUSES + +"""Telemetry Monitor entrypoint""" + +# kill is not catchable +SIGNALS = [signal.SIGINT, signal.SIGQUIT, signal.SIGTERM, signal.SIGABRT] +_EventClass = t.Literal["start", "stop", "timestep"] +_MAX_MANIFEST_LOAD_ATTEMPTS: t.Final[int] = 6 + + +@dataclass +class Run: + """Model containing entities of an individual start call for an experiment""" + + timestamp: int + models: t.List[JobEntity] + orchestrators: t.List[JobEntity] + ensembles: t.List[JobEntity] + + def flatten( + self, filter_fn: t.Optional[t.Callable[[JobEntity], bool]] = None + ) -> t.List[JobEntity]: + """Flatten runs into a list of SmartSimEntity run events""" + entities = self.models + self.orchestrators + self.ensembles + if filter_fn: + entities = [entity for entity in entities if filter_fn(entity)] + return entities + + +@dataclass +class RuntimeManifest: + """The runtime manifest holds meta information about the experiment entities created + at runtime to satisfy the experiment requirements. + """ + + name: str + path: pathlib.Path + launcher: str + runs: t.List[Run] = field(default_factory=list) + + +def _hydrate_persistable( + persistable_entity: t.Dict[str, t.Any], + entity_type: str, + exp_dir: str, +) -> JobEntity: + """Populate JobEntity instance with supplied metdata and instance details""" + entity = JobEntity() + + metadata = persistable_entity["telemetry_metadata"] + status_dir = pathlib.Path(metadata.get("status_dir")) + + entity.type = entity_type + entity.name = persistable_entity["name"] + entity.step_id = str(metadata.get("step_id") or "") + entity.task_id = str(metadata.get("task_id") or "") + entity.timestamp = int(persistable_entity.get("timestamp", "0")) + entity.path = str(exp_dir) + entity.status_dir = str(status_dir) + + return entity + + +def hydrate_persistable( + entity_type: str, + persistable_entity: t.Dict[str, t.Any], + exp_dir: pathlib.Path, +) -> t.List[JobEntity]: + """Map entity data persisted in a manifest file to an object""" + entities = [] + + # an entity w/parent key creates persistables for entities it contains + parent_keys = {"shards", "models"} + parent_keys = parent_keys.intersection(persistable_entity.keys()) + if parent_keys: + container = "shards" if "shards" in parent_keys else "models" + child_type = "orchestrator" if container == "shards" else "model" + for child_entity in persistable_entity[container]: + entity = _hydrate_persistable(child_entity, child_type, str(exp_dir)) + entities.append(entity) + + return entities + + entity = _hydrate_persistable(persistable_entity, entity_type, str(exp_dir)) + entities.append(entity) + return entities + + +def hydrate_persistables( + entity_type: str, + run: t.Dict[str, t.Any], + exp_dir: pathlib.Path, +) -> t.Dict[str, t.List[JobEntity]]: + """Map a collection of entity data persisted in a manifest file to an object""" + persisted: t.Dict[str, t.List[JobEntity]] = { + "model": [], + "orchestrator": [], + } + for item in run[entity_type]: + entities = hydrate_persistable(entity_type, item, exp_dir) + for new_entity in entities: + persisted[new_entity.type].append(new_entity) + + return persisted + + +def hydrate_runs( + persisted_runs: t.List[t.Dict[str, t.Any]], exp_dir: pathlib.Path +) -> t.List[Run]: + """Map run data persisted in a manifest file to an object""" + the_runs: t.List[Run] = [] + for run_instance in persisted_runs: + run_entities: t.Dict[str, t.List[JobEntity]] = { + "model": [], + "orchestrator": [], + "ensemble": [], + } + + for key in run_entities: + _entities = hydrate_persistables(key, run_instance, exp_dir) + for entity_type, new_entities in _entities.items(): + if new_entities: + run_entities[entity_type].extend(new_entities) + + run = Run( + run_instance["timestamp"], + run_entities["model"], + run_entities["orchestrator"], + run_entities["ensemble"], + ) + the_runs.append(run) + + return the_runs + + +def load_manifest(file_path: str) -> t.Optional[RuntimeManifest]: + """Load a persisted manifest and return the content""" + manifest_dict: t.Optional[t.Dict[str, t.Any]] = None + try_count = 1 + + while manifest_dict is None and try_count < _MAX_MANIFEST_LOAD_ATTEMPTS: + source = pathlib.Path(file_path) + source = source.resolve() + + try: + if text := source.read_text(encoding="utf-8").strip(): + manifest_dict = json.loads(text) + except json.JSONDecodeError as ex: + print(f"Error loading manifest: {ex}") + # hack/fix: handle issues reading file before it is fully written + time.sleep(0.5 * try_count) + finally: + try_count += 1 + + if not manifest_dict: + return None + + exp = manifest_dict.get("experiment", None) + if not exp: + raise ValueError("Manifest missing required experiment") + + runs = manifest_dict.get("runs", None) + if runs is None: + raise ValueError("Manifest missing required runs") + + exp_dir = pathlib.Path(exp["path"]) + runs = hydrate_runs(runs, exp_dir) + + manifest = RuntimeManifest( + name=exp["name"], + path=exp_dir, + launcher=exp["launcher"], + runs=runs, + ) + return manifest + + +def track_event( + timestamp: int, + task_id: t.Union[int, str], + step_id: str, + etype: str, + action: _EventClass, + status_dir: pathlib.Path, + logger: logging.Logger, + detail: str = "", + return_code: t.Optional[int] = None, +) -> None: + """Persist a tracking event for an entity""" + tgt_path = status_dir / f"{action}.json" + tgt_path.parent.mkdir(parents=True, exist_ok=True) + + try: + task_id = int(task_id) + except ValueError: + pass + + entity_dict = { + "timestamp": timestamp, + "job_id": task_id, + "step_id": step_id, + "type": etype, + "action": action, + } + + if detail is not None: + entity_dict["detail"] = detail + + if return_code is not None: + entity_dict["return_code"] = return_code + + try: + if not tgt_path.exists(): + # Don't overwrite existing tracking files + bytes_written = tgt_path.write_text(json.dumps(entity_dict, indent=2)) + if bytes_written < 1: + logger.warning("event tracking failed to write tracking file.") + except Exception: + logger.error("Unable to write tracking file.", exc_info=True) + + +def faux_return_code(step_info: StepInfo) -> t.Optional[int]: + """Create a faux return code for a task run by the WLM. Must not be + called with non-terminal statuses or results may be confusing + """ + if step_info.status not in TERMINAL_STATUSES: + return None + + if step_info.status == STATUS_COMPLETED: + return os.EX_OK + + return 1 + + +class ManifestEventHandler(PatternMatchingEventHandler): + """The ManifestEventHandler monitors an experiment for changes and updates + a telemetry datastore as needed. + + It contains event handlers that are triggered by changes to a runtime experiment + manifest. The runtime manifest differs from a standard manifest. A runtime manifest + may contain multiple experiment executions in a `runs` collection. + + It also contains a long-polling loop that checks experiment entities for updates + at each timestep. + """ + + def __init__( + self, + pattern: str, + logger: logging.Logger, + ignore_patterns: t.Any = None, + ignore_directories: bool = True, + case_sensitive: bool = False, + ) -> None: + super().__init__( + [pattern], ignore_patterns, ignore_directories, case_sensitive + ) # type: ignore + self._logger = logger + self._tracked_runs: t.Dict[int, Run] = {} + self._tracked_jobs: t.Dict[_JobKey, JobEntity] = {} + self._completed_jobs: t.Dict[_JobKey, JobEntity] = {} + self._launcher: t.Optional[Launcher] = None + self.job_manager: JobManager = JobManager(threading.RLock()) + self._launcher_map: t.Dict[str, t.Type[Launcher]] = { + "slurm": SlurmLauncher, + "pbs": PBSLauncher, + "cobalt": CobaltLauncher, + "lsf": LSFLauncher, + "local": LocalLauncher, + } + + def init_launcher(self, launcher: str) -> Launcher: + """Initialize the controller with a specific type of launcher. + SmartSim currently supports slurm, pbs(pro), cobalt, lsf, + and local launching + + :param launcher: which launcher to initialize + :type launcher: str + :raises SSUnsupportedError: if a string is passed that is not + a supported launcher + :raises TypeError: if no launcher argument is provided. + """ + if not launcher: + raise TypeError("Must provide a 'launcher' argument") + + if launcher_type := self._launcher_map.get(launcher.lower(), None): + return launcher_type() + + raise ValueError("Launcher type not supported: " + launcher) + + def set_launcher(self, launcher_type: str) -> None: + """Set the launcher for the experiment""" + self._launcher = self.init_launcher(launcher_type) + self.job_manager.set_launcher(self._launcher) + self.job_manager.start() + + def process_manifest(self, manifest_path: str) -> None: + """Read the runtime manifest for the experiment and track new entities + + :param manifest_path: The full path to the manifest file + :type manifest_path: str + """ + try: + manifest = load_manifest(manifest_path) + if not manifest: + return + except json.JSONDecodeError: + self._logger.error(f"Malformed manifest encountered: {manifest_path}") + return + except ValueError: + self._logger.error("Manifest content error", exc_info=True) + return + + if self._launcher is None: + self.set_launcher(manifest.launcher) + + if not self._launcher: + raise SmartSimError(f"Unable to set launcher from {manifest_path}") + + runs = [run for run in manifest.runs if run.timestamp not in self._tracked_runs] + + exp_dir = pathlib.Path(manifest_path).parent.parent.parent + + for run in runs: + for entity in run.flatten( + filter_fn=lambda e: e.key not in self._tracked_jobs and e.is_managed + ): + entity.path = str(exp_dir) + + self._tracked_jobs[entity.key] = entity + track_event( + run.timestamp, + entity.task_id, + entity.step_id, + entity.type, + "start", + pathlib.Path(entity.status_dir), + self._logger, + ) + + if entity.is_managed: + self.job_manager.add_job( + entity.name, + entity.task_id, + entity, + False, + ) + self._launcher.step_mapping.add( + entity.name, entity.step_id, entity.task_id, True + ) + self._tracked_runs[run.timestamp] = run + + def on_modified(self, event: FileModifiedEvent) -> None: + """Event handler for when a file or directory is modified. + + :param event: Event representing file/directory modification. + :type event: FileModifiedEvent + """ + super().on_modified(event) # type: ignore + self._logger.info(f"processing manifest modified @ {event.src_path}") + self.process_manifest(event.src_path) + + def on_created(self, event: FileCreatedEvent) -> None: + """Event handler for when a file or directory is created. + + :param event: Event representing file/directory creation. + :type event: FileCreatedEvent + """ + super().on_created(event) # type: ignore + self._logger.info(f"processing manifest created @ {event.src_path}") + self.process_manifest(event.src_path) + + def _to_completed( + self, + timestamp: int, + entity: JobEntity, + step_info: StepInfo, + ) -> None: + """Move a monitored entity from the active to completed collection to + stop monitoring for updates during timesteps. + + :param timestamp: the current timestamp for event logging + :type timestamp: int + :param entity: the running SmartSim Job + :type entity: JobEntity + :param experiment_dir: the experiement directory to monitor for changes + :type experiment_dir: pathlib.Path + :param entity: the StepInfo received when requesting a Job status update + :type entity: StepInfo + """ + inactive_entity = self._tracked_jobs.pop(entity.key) + if entity.key not in self._completed_jobs: + self._completed_jobs[entity.key] = inactive_entity + + job = self.job_manager[entity.name] + self.job_manager.move_to_completed(job) + + status_clause = f"status: {step_info.status}" + error_clause = f", error: {step_info.error}" if step_info.error else "" + detail = f"{status_clause}{error_clause}" + + if hasattr(job.entity, "status_dir"): + write_path = pathlib.Path(job.entity.status_dir) + + track_event( + timestamp, + entity.task_id, + entity.step_id, + entity.type, + "stop", + write_path, + self._logger, + detail=detail, + return_code=faux_return_code(step_info), + ) + + def on_timestep(self, timestamp: int) -> None: + """Called at polling frequency to request status updates on + monitored entities + + :param timestamp: the current timestamp for event logging + :type timestamp: int + :param experiment_dir: the experiement directory to monitor for changes + :type experiment_dir: pathlib.Path + """ + entity_map = self._tracked_jobs + + if not self._launcher: + return + + # consider not using name to avoid collisions + names = {entity.name: entity for entity in entity_map.values()} + + if names: + step_updates = self._launcher.get_step_update(list(names.keys())) + + for step_name, step_info in step_updates: + if step_info and step_info.status in TERMINAL_STATUSES: + completed_entity = names[step_name] + self._to_completed(timestamp, completed_entity, step_info) + + +def can_shutdown(action_handler: ManifestEventHandler, logger: logging.Logger) -> bool: + jobs = action_handler.job_manager.jobs + db_jobs = action_handler.job_manager.db_jobs + + has_jobs = bool(jobs) + has_dbs = bool(db_jobs) + has_running_jobs = has_jobs or has_dbs + + if has_jobs: + logger.debug(f"telemetry monitor is monitoring {len(jobs)} jobs") + if has_dbs: + logger.debug(f"telemetry monitor is monitoring {len(db_jobs)} dbs") + + return not has_running_jobs + + +def event_loop( + observer: BaseObserver, + action_handler: ManifestEventHandler, + frequency: t.Union[int, float], + logger: logging.Logger, + cooldown_duration: int, +) -> None: + """Executes all attached timestep handlers every seconds + + :param observer: (optional) a preconfigured watchdog Observer to inject + :type observer: t.Optional[BaseObserver] + :param action_handler: The manifest event processor instance + :type action_handler: ManifestEventHandler + :param frequency: frequency (in seconds) of update loop + :type frequency: t.Union[int, float] + :param logger: a preconfigured Logger instance + :type logger: logging.Logger + :param cooldown_duration: number of seconds the telemetry monitor should + poll for new jobs before attempting to shutdown + :type cooldown_duration: int + """ + elapsed: int = 0 + last_ts: int = get_ts() + + while observer.is_alive(): + timestamp = get_ts() + logger.debug(f"Telemetry timestep: {timestamp}") + action_handler.on_timestep(timestamp) + + elapsed += timestamp - last_ts + last_ts = timestamp + + if can_shutdown(action_handler, logger): + if elapsed >= cooldown_duration: + logger.info("beginning telemetry manager shutdown") + observer.stop() # type: ignore + else: + # reset cooldown any time there are still jobs running + elapsed = 0 + + time.sleep(frequency) + + +def main( + frequency: t.Union[int, float], + experiment_dir: pathlib.Path, + logger: logging.Logger, + observer: t.Optional[BaseObserver] = None, + cooldown_duration: t.Optional[int] = 0, +) -> int: + """Setup the monitoring entities and start the timer-based loop that + will poll for telemetry data + + :param frequency: frequency (in seconds) of update loop + :type frequency: t.Union[int, float] + :param experiment_dir: the experiement directory to monitor for changes + :type experiment_dir: pathlib.Path + :param logger: a preconfigured Logger instance + :type logger: logging.Logger + :param observer: (optional) a preconfigured Observer to inject + :type observer: t.Optional[BaseObserver] + :param cooldown_duration: number of seconds the telemetry monitor should + poll for new jobs before attempting to shutdown + :type cooldown_duration: int + """ + manifest_relpath = pathlib.Path(TELMON_SUBDIR) / MANIFEST_FILENAME + manifest_path = experiment_dir / manifest_relpath + monitor_pattern = str(manifest_relpath) + + logger.info( + f"Executing telemetry monitor with frequency: {frequency}s" + f", on target directory: {experiment_dir}" + f" matching pattern: {monitor_pattern}" + ) + + cooldown_duration = cooldown_duration or CONFIG.telemetry_cooldown + log_handler = LoggingEventHandler(logger) # type: ignore + action_handler = ManifestEventHandler(monitor_pattern, logger) + + if observer is None: + observer = Observer() + + try: + if manifest_path.exists(): + # a manifest may not exist depending on startup timing + action_handler.process_manifest(str(manifest_path)) + + observer.schedule(log_handler, experiment_dir, recursive=True) # type:ignore + observer.schedule(action_handler, experiment_dir, recursive=True) # type:ignore + observer.start() # type: ignore + + event_loop(observer, action_handler, frequency, logger, cooldown_duration) + return os.EX_OK + except Exception as ex: + logger.error(ex) + finally: + if observer.is_alive(): + observer.stop() # type: ignore + observer.join() + + return os.EX_SOFTWARE + + +def handle_signal(signo: int, _frame: t.Optional[FrameType]) -> None: + """Helper function to ensure clean process termination""" + if not signo: + logger = logging.getLogger() + logger.warning("Received signal with no signo") + + +def register_signal_handlers() -> None: + """Register a signal handling function for all termination events""" + for sig in SIGNALS: + signal.signal(sig, handle_signal) + + +def get_parser() -> argparse.ArgumentParser: + """Instantiate a parser to process command line arguments""" + arg_parser = argparse.ArgumentParser(description="SmartSim Telemetry Monitor") + arg_parser.add_argument( + "-frequency", + type=int, + help="Frequency of telemetry updates (in seconds))", + required=True, + ) + arg_parser.add_argument( + "-exp_dir", + type=str, + help="Experiment root directory", + required=True, + ) + arg_parser.add_argument( + "-cooldown", + type=int, + help="Default lifetime of telemetry monitor (in seconds) before auto-shutdown", + default=CONFIG.telemetry_cooldown, + ) + return arg_parser + + +if __name__ == "__main__": + os.environ["PYTHONUNBUFFERED"] = "1" + + parser = get_parser() + args = parser.parse_args() + + log = logging.getLogger(f"{__name__}.TelemetryMonitor") + log.setLevel(logging.DEBUG) + log.propagate = False + + log_path = os.path.join(args.exp_dir, TELMON_SUBDIR, "telemetrymonitor.log") + fh = logging.FileHandler(log_path, "a") + log.addHandler(fh) + + # Must register cleanup before the main loop is running + register_signal_handlers() + + try: + main( + int(args.frequency), + pathlib.Path(args.exp_dir), + log, + cooldown_duration=args.cooldown, + ) + sys.exit(0) + except Exception: + log.exception( + "Shutting down telemetry monitor due to unexpected error", exc_info=True + ) + + sys.exit(1) diff --git a/smartsim/_core/generation/generator.py b/smartsim/_core/generation/generator.py index 86c96f4e9..79cea06b7 100644 --- a/smartsim/_core/generation/generator.py +++ b/smartsim/_core/generation/generator.py @@ -27,17 +27,19 @@ import pathlib import shutil import typing as t - +from datetime import datetime from distutils import dir_util # pylint: disable=deprecated-module +from logging import DEBUG, INFO from os import mkdir, path, symlink +from os.path import join, relpath + +from tabulate import tabulate -from ...entity import Model, TaggedFilesHierarchy +from ...database import Orchestrator +from ...entity import Ensemble, Model, TaggedFilesHierarchy from ...log import get_logger from ..control import Manifest from .modelwriter import ModelWriter -from ...database import Orchestrator -from ...entity import Ensemble - logger = get_logger(__name__) logger.propagate = False @@ -49,7 +51,9 @@ class Generator: and writing into configuration files as well. """ - def __init__(self, gen_path: str, overwrite: bool = False) -> None: + def __init__( + self, gen_path: str, overwrite: bool = False, verbose: bool = True + ) -> None: """Initialize a generator object if overwrite is true, replace any existing @@ -59,12 +63,28 @@ def __init__(self, gen_path: str, overwrite: bool = False) -> None: is false, raises EntityExistsError when there is a name collision between entities. + :param gen_path: Path in which files need to be generated + :type gen_path: str :param overwrite: toggle entity replacement, defaults to False :type overwrite: bool, optional + :param verbose: Whether generation information should be logged to std out + :type verbose: bool, optional """ self._writer = ModelWriter() self.gen_path = gen_path self.overwrite = overwrite + self.log_level = DEBUG if not verbose else INFO + + @property + def log_file(self) -> str: + """Returns the location of the file + summarizing the parameters used for the last generation + of all generated entities. + + :returns: path to file with parameter settings + :rtype: str + """ + return join(self.gen_path, "smartsim_params.txt") def generate_experiment(self, *args: t.Any) -> None: """Run ensemble and experiment file structure generation @@ -87,8 +107,9 @@ def generate_experiment(self, *args: t.Any) -> None: """ generator_manifest = Manifest(*args) + self._gen_exp_dir() - self._gen_orc_dir(generator_manifest.db) + self._gen_orc_dir(generator_manifest.dbs) self._gen_entity_list_dir(generator_manifest.ensembles) self._gen_entity_dirs(generator_manifest.models) @@ -129,31 +150,39 @@ def _gen_exp_dir(self) -> None: # keep exists ok for race conditions on NFS pathlib.Path(self.gen_path).mkdir(exist_ok=True) else: - logger.info("Working in previously created experiment") + logger.log( + level=self.log_level, msg="Working in previously created experiment" + ) - def _gen_orc_dir(self, orchestrator: t.Optional[Orchestrator]) -> None: + # The log_file only keeps track of the last generation + # this is to avoid gigantic files in case the user repeats + # generation several times. The information is anyhow + # redundant, as it is also written in each entity's dir + with open(self.log_file, mode="w", encoding="utf-8") as log_file: + dt_string = datetime.now().strftime("%d/%m/%Y %H:%M:%S") + log_file.write(f"Generation start date and time: {dt_string}\n") + + def _gen_orc_dir(self, orchestrator_list: t.List[Orchestrator]) -> None: """Create the directory that will hold the error, output and configuration files for the orchestrator. :param orchestrator: Orchestrator instance :type orchestrator: Orchestrator | None """ + # Loop through orchestrators + for orchestrator in orchestrator_list: + orc_path = path.join(self.gen_path, orchestrator.name) - if not orchestrator: - return - - orc_path = path.join(self.gen_path, "database") - orchestrator.set_path(orc_path) - - # Always remove orchestrator files if present. - if path.isdir(orc_path): - shutil.rmtree(orc_path, ignore_errors=True) - pathlib.Path(orc_path).mkdir(exist_ok=True) + orchestrator.set_path(orc_path) + # Always remove orchestrator files if present. + if path.isdir(orc_path): + shutil.rmtree(orc_path, ignore_errors=True) + pathlib.Path(orc_path).mkdir(exist_ok=self.overwrite) def _gen_entity_list_dir(self, entity_lists: t.List[Ensemble]) -> None: - """Generate directories for EntityList instances + """Generate directories for Ensemble instances - :param entity_lists: list of EntityList instances + :param entity_lists: list of Ensemble instances :type entity_lists: list """ @@ -249,10 +278,55 @@ def _build_tagged_files(tagged: TaggedFilesHierarchy) -> None: # write in changes to configurations if isinstance(entity, Model): - logger.debug( - f"Configuring model {entity.name} with params {entity.params}" + files_to_params = self._writer.configure_tagged_model_files( + to_write, entity.params ) - self._writer.configure_tagged_model_files(to_write, entity.params) + self._log_params(entity, files_to_params) + + def _log_params( + self, entity: Model, files_to_params: t.Dict[str, t.Dict[str, str]] + ) -> None: + """Log which files were modified during generation + + and what values were set to the parameters + + :param entity: the model being generated + :type entity: Model + :param files_to_params: a dict connecting each file to its parameter settings + :type files_to_params: t.Dict[str, t.Dict[str, str]] + """ + used_params: t.Dict[str, str] = {} + file_to_tables: t.Dict[str, str] = {} + for file, params in files_to_params.items(): + used_params.update(params) + table = tabulate(params.items(), headers=["Name", "Value"]) + file_to_tables[relpath(file, self.gen_path)] = table + + if used_params: + used_params_str = ", ".join( + [f"{name}={value}" for name, value in used_params.items()] + ) + logger.log( + level=self.log_level, + msg=f"Configured model {entity.name} with params {used_params_str}", + ) + file_table = tabulate( + file_to_tables.items(), + headers=["File name", "Parameters"], + ) + log_entry = f"Model name: {entity.name}\n{file_table}\n\n" + with open(self.log_file, mode="a", encoding="utf-8") as logfile: + logfile.write(log_entry) + with open( + join(entity.path, "smartsim_params.txt"), mode="w", encoding="utf-8" + ) as local_logfile: + local_logfile.write(log_entry) + + else: + logger.log( + level=self.log_level, + msg=f"Configured model {entity.name} with no parameters", + ) @staticmethod def _copy_entity_files(entity: Model) -> None: diff --git a/smartsim/_core/generation/modelwriter.py b/smartsim/_core/generation/modelwriter.py index eb0e389aa..0cf071082 100644 --- a/smartsim/_core/generation/modelwriter.py +++ b/smartsim/_core/generation/modelwriter.py @@ -63,7 +63,7 @@ def configure_tagged_model_files( tagged_files: t.List[str], params: t.Dict[str, str], make_missing_tags_fatal: bool = False, - ) -> None: + ) -> t.Dict[str, t.Dict[str, str]]: """Read, write and configure tagged files attached to a Model instance. @@ -71,13 +71,19 @@ def configure_tagged_model_files( :type model: list[str] :param params: model parameters :type params: dict[str, str] - :param make_missing_tags_fatal: blow up if a tag is missing + :param make_missing_tags_fatal: raise an error if a tag is missing :type make_missing_tags_fatal: bool + :returns: A dict connecting each file to its parameter settings + :rtype: dict[str,dict[str,str]] """ + files_to_tags: t.Dict[str, t.Dict[str, str]] = {} for tagged_file in tagged_files: self._set_lines(tagged_file) - self._replace_tags(params, make_missing_tags_fatal) + used_tags = self._replace_tags(params, make_missing_tags_fatal) self._write_changes(tagged_file) + files_to_tags[tagged_file] = used_tags + + return files_to_tags def _set_lines(self, file_path: str) -> None: """Set the lines for the modelwrtter to iterate over @@ -104,8 +110,10 @@ def _write_changes(self, file_path: str) -> None: except (IOError, OSError) as e: raise ParameterWriterError(file_path, read=False) from e - def _replace_tags(self, params: t.Dict[str, str], make_fatal: bool = False) -> None: - """Replace the tagged within the tagged file attached to this + def _replace_tags( + self, params: t.Dict[str, str], make_fatal: bool = False + ) -> t.Dict[str, str]: + """Replace the tagged parameters within the file attached to this model. The tag defaults to ";" :param model: The model instance @@ -113,9 +121,12 @@ def _replace_tags(self, params: t.Dict[str, str], make_fatal: bool = False) -> N :param make_fatal: (Optional) Set to True to force a fatal error if a tag is not matched :type make_fatal: bool + :returns: A dict of parameter names and values set for the file + :rtype: dict[str,str] """ edited = [] unused_tags: t.Dict[str, t.List[int]] = {} + used_params: t.Dict[str, str] = {} for i, line in enumerate(self.lines): search = re.search(self.regex, line) if search: @@ -126,6 +137,7 @@ def _replace_tags(self, params: t.Dict[str, str], make_fatal: bool = False) -> N new_val = str(params[previous_value]) new_line = re.sub(self.regex, new_val, line, 1) search = re.search(self.regex, new_line) + used_params[previous_value] = new_val if not search: edited.append(new_line) else: @@ -143,13 +155,12 @@ def _replace_tags(self, params: t.Dict[str, str], make_fatal: bool = False) -> N else: edited.append(line) for tag, value in unused_tags.items(): - missing_tag_message = ( - f"Unused tag {tag} on line(s): {str(value)}" - ) + missing_tag_message = f"Unused tag {tag} on line(s): {str(value)}" if make_fatal: raise SmartSimError(missing_tag_message) logger.warning(missing_tag_message) self.lines = edited + return used_params def _is_ensemble_spec( self, tagged_line: str, model_params: t.Dict[str, str] diff --git a/smartsim/_core/launcher/__init__.py b/smartsim/_core/launcher/__init__.py index d28cb2588..6e1aa724e 100644 --- a/smartsim/_core/launcher/__init__.py +++ b/smartsim/_core/launcher/__init__.py @@ -24,8 +24,8 @@ # 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 .launcher import Launcher from .cobalt.cobaltLauncher import CobaltLauncher +from .launcher import Launcher from .local.local import LocalLauncher from .lsf.lsfLauncher import LSFLauncher from .pbs.pbsLauncher import PBSLauncher diff --git a/smartsim/_core/launcher/cobalt/cobaltLauncher.py b/smartsim/_core/launcher/cobalt/cobaltLauncher.py index ca0b88a3b..56ebe12cc 100644 --- a/smartsim/_core/launcher/cobalt/cobaltLauncher.py +++ b/smartsim/_core/launcher/cobalt/cobaltLauncher.py @@ -31,13 +31,13 @@ from smartsim._core.launcher.step import Step from smartsim.settings import ( - RunSettings, - SettingsBase, AprunSettings, CobaltBatchSettings, - MpirunSettings, MpiexecSettings, + MpirunSettings, OrterunSettings, + RunSettings, + SettingsBase, ) from ....error import LauncherError @@ -47,13 +47,13 @@ from ..launcher import WLMLauncher from ..pbs.pbsCommands import qdel, qstat from ..step import ( - Step, AprunStep, CobaltBatchStep, LocalStep, MpiexecStep, MpirunStep, OrterunStep, + Step, ) from ..stepInfo import CobaltStepInfo, StepInfo from .cobaltParser import parse_cobalt_step_id, parse_cobalt_step_status, parse_qsub_out @@ -117,16 +117,13 @@ def run(self, step: Step) -> t.Optional[str]: # aprun doesn't direct output for us. out, err = step.get_output_files() - # LocalStep.run_command omits env, include it here - passed_env = step.env if isinstance(step, LocalStep) else None - # pylint: disable-next=consider-using-with output = open(out, "w+", encoding="utf-8") # pylint: disable-next=consider-using-with error = open(err, "w+", encoding="utf-8") task_id = self.task_manager.start_task( - cmd_list, step.cwd, passed_env, out=output.fileno(), err=error.fileno() + cmd_list, step.cwd, step.env, out=output.fileno(), err=error.fileno() ) # if batch submission did not successfully retrieve job ID diff --git a/smartsim/_core/launcher/cobalt/cobaltParser.py b/smartsim/_core/launcher/cobalt/cobaltParser.py index ab205491e..c76509d36 100644 --- a/smartsim/_core/launcher/cobalt/cobaltParser.py +++ b/smartsim/_core/launcher/cobalt/cobaltParser.py @@ -78,7 +78,7 @@ def parse_qsub_out(output: str) -> str: for line in output.split("\n"): try: value = line.strip() - int(value) # if the cast works, return original string + int(value) # if the cast works, return original string step_id = value break except ValueError: diff --git a/smartsim/_core/launcher/colocated.py b/smartsim/_core/launcher/colocated.py index 84e269c1f..ea331023c 100644 --- a/smartsim/_core/launcher/colocated.py +++ b/smartsim/_core/launcher/colocated.py @@ -27,11 +27,10 @@ import sys import typing as t - +from ...entity.dbobject import DBModel, DBScript from ...error import SSInternalError from ..config import CONFIG from ..utils.helpers import create_lockfile_name -from ...entity.dbobject import DBModel, DBScript def write_colocated_launch_script( @@ -119,14 +118,14 @@ def _build_colocated_wrapper_cmd( # up the backgrounded db process cmd = [ - sys.executable, - "-m", - "smartsim._core.entrypoints.colocated", - "+lockfile", - lockfile, - "+db_cpus", - str(cpus), - ] + sys.executable, + "-m", + "smartsim._core.entrypoints.colocated", + "+lockfile", + lockfile, + "+db_cpus", + str(cpus), + ] # Add in the interface if using TCP/IP if ifname: if isinstance(ifname, str): @@ -137,16 +136,9 @@ def _build_colocated_wrapper_cmd( db_cmd = [] if custom_pinning: - db_cmd.extend([ - 'taskset', '-c', custom_pinning - ]) + db_cmd.extend(["taskset", "-c", custom_pinning]) db_cmd.extend( - [ - CONFIG.database_exe, - CONFIG.database_conf, - "--loadmodule", - CONFIG.redisai - ] + [CONFIG.database_exe, CONFIG.database_conf, "--loadmodule", CONFIG.redisai] ) # add extra redisAI configurations @@ -218,6 +210,7 @@ def _build_db_model_cmd(db_models: t.List[DBModel]) -> t.List[str]: cmd.append(f"--backend={db_model.backend}") cmd.append(f"--device={db_model.device}") cmd.append(f"--devices_per_node={db_model.devices_per_node}") + cmd.append(f"--first_device={db_model.first_device}") if db_model.batch_size: cmd.append(f"--batch_size={db_model.batch_size}") if db_model.min_batch_size: @@ -254,5 +247,5 @@ def _build_db_script_cmd(db_scripts: t.List[DBScript]) -> t.List[str]: cmd.append(f"--file={db_script.file}") cmd.append(f"--device={db_script.device}") cmd.append(f"--devices_per_node={db_script.devices_per_node}") - + cmd.append(f"--first_device={db_script.first_device}") return cmd diff --git a/smartsim/_core/launcher/launcher.py b/smartsim/_core/launcher/launcher.py index ec8bb0120..61f0460f9 100644 --- a/smartsim/_core/launcher/launcher.py +++ b/smartsim/_core/launcher/launcher.py @@ -28,11 +28,11 @@ import typing as t from ...error import AllocationError, LauncherError, SSUnsupportedError -from .stepInfo import UnmanagedStepInfo, StepInfo +from ...settings import SettingsBase +from .step import Step +from .stepInfo import StepInfo, UnmanagedStepInfo from .stepMapping import StepMapping from .taskManager import TaskManager -from .step import Step -from ...settings import SettingsBase class Launcher(abc.ABC): # pragma: no cover @@ -47,11 +47,6 @@ class Launcher(abc.ABC): # pragma: no cover step_mapping: StepMapping task_manager: TaskManager - @property - @abc.abstractmethod - def supported_rs(self) -> t.Dict[t.Type[SettingsBase], t.Type[Step]]: - raise NotImplementedError - @abc.abstractmethod def create_step(self, name: str, cwd: str, step_settings: SettingsBase) -> Step: raise NotImplementedError @@ -86,6 +81,11 @@ def __init__(self) -> None: self.task_manager = TaskManager() self.step_mapping = StepMapping() + @property + @abc.abstractmethod + def supported_rs(self) -> t.Dict[t.Type[SettingsBase], t.Type[Step]]: + raise NotImplementedError + # every launcher utilizing this interface must have a map # of supported RunSettings types (see slurmLauncher.py for ex) def create_step( @@ -176,6 +176,6 @@ def _get_unmanaged_step_update( # pylint: disable-next=no-self-use def _get_managed_step_update( self, - step_ids: t.List[str], # pylint: disable=unused-argument + step_ids: t.List[str], # pylint: disable=unused-argument ) -> t.List[StepInfo]: # pragma: no cover return [] diff --git a/tests/test_configs/test_dir/test.py b/smartsim/_core/launcher/local/__init__.py similarity index 100% rename from tests/test_configs/test_dir/test.py rename to smartsim/_core/launcher/local/__init__.py diff --git a/smartsim/_core/launcher/local/local.py b/smartsim/_core/launcher/local/local.py index 7e5c56f7b..fee058d16 100644 --- a/smartsim/_core/launcher/local/local.py +++ b/smartsim/_core/launcher/local/local.py @@ -26,27 +26,17 @@ import typing as t -from ..launcher import Launcher -from ....log import get_logger from ....settings import RunSettings, SettingsBase -from ..step import LocalStep -from ..step import Step -from ..stepInfo import UnmanagedStepInfo, StepInfo +from ..launcher import Launcher +from ..step import LocalStep, Step +from ..stepInfo import StepInfo, UnmanagedStepInfo from ..stepMapping import StepMapping from ..taskManager import TaskManager -logger = get_logger(__name__) - class LocalLauncher(Launcher): """Launcher used for spawning proceses on a localhost machine.""" - @property - def supported_rs(self) -> t.Dict[t.Type[SettingsBase], t.Type[Step]]: - return { - RunSettings: LocalStep, - } - def __init__(self) -> None: self.task_manager = TaskManager() self.step_mapping = StepMapping() @@ -58,26 +48,28 @@ def create_step(self, name: str, cwd: str, step_settings: SettingsBase) -> Step: """ if not isinstance(step_settings, RunSettings): raise TypeError( - f"Local Launcher only supports entities with RunSettings, not {type(step_settings)}" + "Local Launcher only supports entities with RunSettings, " + f"not {type(step_settings)}" ) - step = LocalStep(name, cwd, step_settings) - return step + return LocalStep(name, cwd, step_settings) - def get_step_update(self, step_names: t.List[str]) -> t.List[t.Tuple[str, t.Optional[StepInfo]]]: + def get_step_update( + self, step_names: t.List[str] + ) -> t.List[t.Tuple[str, t.Optional[StepInfo]]]: """Get status updates of each job step name provided :param step_names: list of step_names :type step_names: list[str] :return: list of tuples for update - :rtype: list[(str, UnmanagedStepInfo)] + :rtype: list[tuple[str, StepInfo | None]] """ # step ids are process ids of the tasks # as there is no WLM intermediary updates: t.List[t.Tuple[str, t.Optional[StepInfo]]] = [] s_names, s_ids = self.step_mapping.get_ids(step_names, managed=False) for step_name, step_id in zip(s_names, s_ids): - status, rc, out, err = self.task_manager.get_task_update(str(step_id)) - step_info = UnmanagedStepInfo(status, rc, out, err) + status, ret_code, out, err = self.task_manager.get_task_update(str(step_id)) + step_info = UnmanagedStepInfo(status, ret_code, out, err) update = (step_name, step_info) updates.append(update) return updates @@ -85,8 +77,12 @@ def get_step_update(self, step_names: t.List[str]) -> t.List[t.Tuple[str, t.Opti def get_step_nodes(self, step_names: t.List[str]) -> t.List[t.List[str]]: """Return the address of nodes assigned to the step + :param step_names: list of step_names + :type step_names: list[str] + :return: list of node addresses + :rtype: list[list[str]] + TODO: Use socket to find the actual Lo address? - :return: a list containing the local host address """ return [["127.0.0.1"] * len(step_names)] @@ -104,16 +100,17 @@ def run(self, step: Step) -> str: self.task_manager.start() out, err = step.get_output_files() - output = open(out, "w+") - error = open(err, "w+") cmd = step.get_launch_cmd() - # LocalStep.run_command omits env, include it here - passed_env = step.env if isinstance(step, LocalStep) else None + # pylint: disable-next=consider-using-with + output = open(out, "w+", encoding="utf-8") + # pylint: disable-next=consider-using-with + error = open(err, "w+", encoding="utf-8") task_id = self.task_manager.start_task( - cmd, step.cwd, env=passed_env, out=output.fileno(), err=error.fileno() + cmd, step.cwd, env=step.env, out=output.fileno(), err=error.fileno() ) + self.step_mapping.add(step.name, task_id=task_id, managed=False) return task_id @@ -127,10 +124,10 @@ def stop(self, step_name: str) -> UnmanagedStepInfo: """ # step_id is task_id for local. Naming for consistency step_id = self.step_mapping[step_name].task_id - + self.task_manager.remove_task(str(step_id)) - _, rc, out, err = self.task_manager.get_task_update(str(step_id)) - step_info = UnmanagedStepInfo("Cancelled", rc, out, err) + _, ret_code, out, err = self.task_manager.get_task_update(str(step_id)) + step_info = UnmanagedStepInfo("Cancelled", ret_code, out, err) return step_info def __str__(self) -> str: diff --git a/smartsim/_core/launcher/lsf/lsfLauncher.py b/smartsim/_core/launcher/lsf/lsfLauncher.py index a8d0e27aa..c2f432807 100644 --- a/smartsim/_core/launcher/lsf/lsfLauncher.py +++ b/smartsim/_core/launcher/lsf/lsfLauncher.py @@ -30,25 +30,25 @@ from ....error import LauncherError from ....log import get_logger from ....settings import ( - SettingsBase, BsubBatchSettings, JsrunSettings, MpiexecSettings, MpirunSettings, OrterunSettings, RunSettings, + SettingsBase, ) from ....status import STATUS_CANCELLED, STATUS_COMPLETED from ...config import CONFIG from ..launcher import WLMLauncher from ..step import ( - Step, BsubBatchStep, JsrunStep, LocalStep, MpiexecStep, MpirunStep, OrterunStep, + Step, ) from ..stepInfo import LSFBatchStepInfo, LSFJsrunStepInfo, StepInfo from .lsfCommands import bjobs, bkill, jskill, jslist @@ -115,19 +115,16 @@ def run(self, step: Step) -> t.Optional[str]: time.sleep(1) step_id = self._get_lsf_step_id(step) logger.debug(f"Gleaned jsrun step id: {step_id} for {step.name}") - else: # isinstance(step, MpirunStep) or isinstance(step, LocalStep) + else: # mpirun and local launch don't direct output for us out, err = step.get_output_files() - # LocalStep.run_command omits env, include it here - passed_env = step.env if isinstance(step, LocalStep) else None - # pylint: disable-next=consider-using-with output = open(out, "w+", encoding="utf-8") # pylint: disable-next=consider-using-with error = open(err, "w+", encoding="utf-8") task_id = self.task_manager.start_task( - cmd_list, step.cwd, passed_env, out=output.fileno(), err=error.fileno() + cmd_list, step.cwd, step.env, out=output.fileno(), err=error.fileno() ) self.step_mapping.add(step.name, step_id, task_id, step.managed) diff --git a/smartsim/_core/launcher/pbs/pbsLauncher.py b/smartsim/_core/launcher/pbs/pbsLauncher.py index cbb85337c..1b77ffd81 100644 --- a/smartsim/_core/launcher/pbs/pbsLauncher.py +++ b/smartsim/_core/launcher/pbs/pbsLauncher.py @@ -30,26 +30,26 @@ from ....error import LauncherError from ....log import get_logger from ....settings import ( - SettingsBase, AprunSettings, - QsubBatchSettings, MpiexecSettings, MpirunSettings, OrterunSettings, - RunSettings, PalsMpiexecSettings, + QsubBatchSettings, + RunSettings, + SettingsBase, ) from ....status import STATUS_CANCELLED, STATUS_COMPLETED from ...config import CONFIG from ..launcher import WLMLauncher from ..step import ( - Step, AprunStep, LocalStep, MpiexecStep, MpirunStep, OrterunStep, QsubBatchStep, + Step, ) from ..stepInfo import PBSStepInfo, StepInfo from .pbsCommands import qdel, qstat @@ -111,15 +111,12 @@ def run(self, step: Step) -> t.Optional[str]: # aprun/local doesn't direct output for us. out, err = step.get_output_files() - # LocalStep.run_command omits env, include it here - passed_env = step.env if isinstance(step, LocalStep) else None - # pylint: disable-next=consider-using-with output = open(out, "w+", encoding="utf-8") # pylint: disable-next=consider-using-with error = open(err, "w+", encoding="utf-8") task_id = self.task_manager.start_task( - cmd_list, step.cwd, passed_env, out=output.fileno(), err=error.fileno() + cmd_list, step.cwd, step.env, out=output.fileno(), err=error.fileno() ) # if batch submission did not successfully retrieve job ID diff --git a/smartsim/_core/launcher/slurm/slurmCommands.py b/smartsim/_core/launcher/slurm/slurmCommands.py index e28bf482f..ecf545b91 100644 --- a/smartsim/_core/launcher/slurm/slurmCommands.py +++ b/smartsim/_core/launcher/slurm/slurmCommands.py @@ -27,76 +27,69 @@ import typing as t from ....error import LauncherError +from ....log import get_logger from ...utils.helpers import expand_exe_path from ..util.shell import execute_cmd +logger = get_logger(__name__) -def sstat(args: t.List[str]) -> t.Tuple[str, str]: + +def sstat(args: t.List[str], *, raise_on_err: bool = False) -> t.Tuple[str, str]: """Calls sstat with args :param args: List of command arguments :type args: List of str :returns: Output and error of sstat """ - _sstat = _find_slurm_command("sstat") - cmd = [_sstat] + args - _, out, error = execute_cmd(cmd) - return out, error + _, out, err = _execute_slurm_cmd("sstat", args, raise_on_err=raise_on_err) + return out, err -def sacct(args: t.List[str]) -> t.Tuple[str, str]: +def sacct(args: t.List[str], *, raise_on_err: bool = False) -> t.Tuple[str, str]: """Calls sacct with args :param args: List of command arguments :type args: List of str :returns: Output and error of sacct """ - _sacct = _find_slurm_command("sacct") - cmd = [_sacct] + args - _, out, error = execute_cmd(cmd) - return out, error + _, out, err = _execute_slurm_cmd("sacct", args, raise_on_err=raise_on_err) + return out, err -def salloc(args: t.List[str]) -> t.Tuple[str, str]: +def salloc(args: t.List[str], *, raise_on_err: bool = False) -> t.Tuple[str, str]: """Calls slurm salloc with args :param args: List of command arguments :type args: List of str :returns: Output and error of salloc """ - _salloc = _find_slurm_command("salloc") - cmd = [_salloc] + args - _, out, error = execute_cmd(cmd) - return out, error + _, out, err = _execute_slurm_cmd("salloc", args, raise_on_err=raise_on_err) + return out, err -def sinfo(args: t.List[str]) -> t.Tuple[str, str]: +def sinfo(args: t.List[str], *, raise_on_err: bool = False) -> t.Tuple[str, str]: """Calls slurm sinfo with args :param args: List of command arguments :type args: List of str :returns: Output and error of sinfo """ - _sinfo = _find_slurm_command("sinfo") - cmd = [_sinfo] + args - _, out, error = execute_cmd(cmd) - return out, error + _, out, err = _execute_slurm_cmd("sinfo", args, raise_on_err=raise_on_err) + return out, err -def scontrol(args: t.List[str]) -> t.Tuple[str, str]: +def scontrol(args: t.List[str], *, raise_on_err: bool = False) -> t.Tuple[str, str]: """Calls slurm scontrol with args :param args: List of command arguments :type args: List of str :returns: Output and error of sinfo """ - _scontrol = _find_slurm_command("scontrol") - cmd = [_scontrol] + args - _, out, error = execute_cmd(cmd) - return out, error + _, out, err = _execute_slurm_cmd("scontrol", args, raise_on_err=raise_on_err) + return out, err -def scancel(args: t.List[str]) -> t.Tuple[int, str, str]: +def scancel(args: t.List[str], *, raise_on_err: bool = False) -> t.Tuple[int, str, str]: """Calls slurm scancel with args. returncode is also supplied in this function. @@ -106,10 +99,7 @@ def scancel(args: t.List[str]) -> t.Tuple[int, str, str]: :return: output and error :rtype: str """ - _scancel = _find_slurm_command("scancel") - cmd = [_scancel] + args - returncode, out, error = execute_cmd(cmd) - return returncode, out, error + return _execute_slurm_cmd("scancel", args, raise_on_err=raise_on_err) def _find_slurm_command(cmd: str) -> str: @@ -120,3 +110,17 @@ def _find_slurm_command(cmd: str) -> str: raise LauncherError( f"Slurm Launcher could not find path of {cmd} command" ) from e + + +def _execute_slurm_cmd( + command: str, args: t.List[str], raise_on_err: bool = False +) -> t.Tuple[int, str, str]: + cmd_exe = _find_slurm_command(command) + cmd = [cmd_exe] + args + returncode, out, error = execute_cmd(cmd) + if returncode != 0: + msg = f"An error occurred while calling {command}: {error}" + if raise_on_err: + raise LauncherError(msg) + logger.error(msg) + return returncode, out, error diff --git a/smartsim/_core/launcher/slurm/slurmLauncher.py b/smartsim/_core/launcher/slurm/slurmLauncher.py index 70bdab5a2..cba8df4f1 100644 --- a/smartsim/_core/launcher/slurm/slurmLauncher.py +++ b/smartsim/_core/launcher/slurm/slurmLauncher.py @@ -107,10 +107,7 @@ def get_step_nodes(self, step_names: t.List[str]) -> t.List[t.List[str]]: """ _, step_ids = self.step_mapping.get_ids(step_names, managed=True) step_str = _create_step_id_str([val for val in step_ids if val is not None]) - output, error = sstat([step_str, "-i", "-n", "-p", "-a"]) - - if "error:" in error.split(" "): - raise LauncherError("Failed to retrieve nodelist from stat") + output, _ = sstat([step_str, "-i", "-n", "-p", "-a"], raise_on_err=True) # parse node list for each step node_lists = [] @@ -155,15 +152,12 @@ def run(self, step: Step) -> t.Optional[str]: # MPI/local steps don't direct output like slurm steps out, err = step.get_output_files() - # LocalStep.run_command omits env, include it here - passed_env = step.env if isinstance(step, LocalStep) else None - # pylint: disable-next=consider-using-with output = open(out, "w+", encoding="utf-8") # pylint: disable-next=consider-using-with error = open(err, "w+", encoding="utf-8") task_id = self.task_manager.start_task( - cmd_list, step.cwd, passed_env, out=output.fileno(), err=error.fileno() + cmd_list, step.cwd, step.env, out=output.fileno(), err=error.fileno() ) if not step_id and step.managed: @@ -243,9 +237,9 @@ def _get_slurm_step_id(step: Step, interval: int = 2) -> str: step_id: t.Optional[str] = None trials = CONFIG.wlm_trials while trials > 0: - output, err = sacct(["--noheader", "-p", "--format=jobname,jobid"]) - if err: - logger.warning(f"An error occurred while calling sacct: {err}") + output, _ = sacct( + ["--noheader", "-p", "--format=jobname,jobid"], raise_on_err=True + ) step_id = parse_step_id_from_sacct(output, step.name) if step_id: @@ -266,7 +260,10 @@ def _get_managed_step_update(self, step_ids: t.List[str]) -> t.List[StepInfo]: :rtype: list[StepInfo] """ step_str = _create_step_id_str(step_ids) - sacct_out, _ = sacct(["--noheader", "-p", "-b", "--jobs", step_str]) + sacct_out, _ = sacct( + ["--noheader", "-p", "-b", "--jobs", step_str], raise_on_err=True + ) + # (status, returncode) stat_tuples = [parse_sacct(sacct_out, step_id) for step_id in step_ids] diff --git a/smartsim/_core/launcher/step/__init__.py b/smartsim/_core/launcher/step/__init__.py index 5a36701a8..98dd1a921 100644 --- a/smartsim/_core/launcher/step/__init__.py +++ b/smartsim/_core/launcher/step/__init__.py @@ -24,7 +24,6 @@ # 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 .step import Step from .alpsStep import AprunStep from .cobaltStep import CobaltBatchStep from .localStep import LocalStep @@ -32,3 +31,4 @@ from .mpiStep import MpiexecStep, MpirunStep, OrterunStep from .pbsStep import QsubBatchStep from .slurmStep import SbatchStep, SrunStep +from .step import Step diff --git a/smartsim/_core/launcher/step/alpsStep.py b/smartsim/_core/launcher/step/alpsStep.py index 80e7e7658..d675f703f 100644 --- a/smartsim/_core/launcher/step/alpsStep.py +++ b/smartsim/_core/launcher/step/alpsStep.py @@ -31,8 +31,8 @@ from ....error import AllocationError from ....log import get_logger -from .step import Step from ....settings import AprunSettings, RunSettings, Singularity +from .step import Step, proxyable_launch_cmd logger = get_logger(__name__) @@ -56,9 +56,11 @@ def __init__(self, name: str, cwd: str, run_settings: AprunSettings) -> None: def _get_mpmd(self) -> t.List[RunSettings]: """Temporary convenience function to return a typed list - of attached RunSettings""" + of attached RunSettings + """ return self.run_settings.mpmd + @proxyable_launch_cmd def get_launch_cmd(self) -> t.List[str]: """Get the command to launch this step diff --git a/smartsim/_core/launcher/step/localStep.py b/smartsim/_core/launcher/step/localStep.py index d15a48381..2f10bc79d 100644 --- a/smartsim/_core/launcher/step/localStep.py +++ b/smartsim/_core/launcher/step/localStep.py @@ -28,17 +28,22 @@ import shutil import typing as t -from .step import Step -from ....settings.base import RunSettings from ....settings import Singularity +from ....settings.base import RunSettings +from .step import Step, proxyable_launch_cmd class LocalStep(Step): def __init__(self, name: str, cwd: str, run_settings: RunSettings): super().__init__(name, cwd, run_settings) self.run_settings = run_settings - self.env = self._set_env() + self._env = self._set_env() + + @property + def env(self) -> t.Dict[str, str]: + return self._env + @proxyable_launch_cmd def get_launch_cmd(self) -> t.List[str]: cmd = [] diff --git a/smartsim/_core/launcher/step/lsfStep.py b/smartsim/_core/launcher/step/lsfStep.py index ae6c3525b..953ab9c45 100644 --- a/smartsim/_core/launcher/step/lsfStep.py +++ b/smartsim/_core/launcher/step/lsfStep.py @@ -30,9 +30,9 @@ from ....error import AllocationError from ....log import get_logger -from .step import Step from ....settings import BsubBatchSettings, JsrunSettings from ....settings.base import RunSettings +from .step import Step logger = get_logger(__name__) @@ -213,7 +213,8 @@ def _set_alloc(self) -> None: def _get_mpmd(self) -> t.List[RunSettings]: """Temporary convenience function to return a typed list - of attached RunSettings""" + of attached RunSettings + """ if isinstance(self.step_settings, JsrunSettings): return self.step_settings.mpmd return [] diff --git a/smartsim/_core/launcher/step/mpiStep.py b/smartsim/_core/launcher/step/mpiStep.py index 9a0796c0f..7971fb732 100644 --- a/smartsim/_core/launcher/step/mpiStep.py +++ b/smartsim/_core/launcher/step/mpiStep.py @@ -26,14 +26,14 @@ import os import shutil -from shlex import split as sh_split import typing as t +from shlex import split as sh_split from ....error import AllocationError, SmartSimError from ....log import get_logger -from .step import Step -from ....settings import MpirunSettings, MpiexecSettings, OrterunSettings +from ....settings import MpiexecSettings, MpirunSettings, OrterunSettings from ....settings.base import RunSettings +from .step import Step, proxyable_launch_cmd logger = get_logger(__name__) @@ -59,6 +59,7 @@ def __init__(self, name: str, cwd: str, run_settings: RunSettings) -> None: _supported_launchers = ["PBS", "COBALT", "SLURM", "LSB"] + @proxyable_launch_cmd def get_launch_cmd(self) -> t.List[str]: """Get the command to launch this step @@ -118,7 +119,8 @@ def _set_alloc(self) -> None: def _get_mpmd(self) -> t.List[RunSettings]: """Temporary convenience function to return a typed list - of attached RunSettings""" + of attached RunSettings + """ if hasattr(self.run_settings, "mpmd") and self.run_settings.mpmd: rs_mpmd: t.List[RunSettings] = self.run_settings.mpmd return rs_mpmd diff --git a/smartsim/_core/launcher/step/pbsStep.py b/smartsim/_core/launcher/step/pbsStep.py index 7a2f47a69..9218894f9 100644 --- a/smartsim/_core/launcher/step/pbsStep.py +++ b/smartsim/_core/launcher/step/pbsStep.py @@ -27,8 +27,8 @@ import typing as t from ....log import get_logger -from .step import Step from ....settings import QsubBatchSettings +from .step import Step logger = get_logger(__name__) diff --git a/smartsim/_core/launcher/step/slurmStep.py b/smartsim/_core/launcher/step/slurmStep.py index 18575e4e9..cb0db483b 100644 --- a/smartsim/_core/launcher/step/slurmStep.py +++ b/smartsim/_core/launcher/step/slurmStep.py @@ -26,13 +26,13 @@ import os import shutil -from shlex import split as sh_split import typing as t +from shlex import split as sh_split from ....error import AllocationError from ....log import get_logger +from ....settings import RunSettings, SbatchSettings, Singularity, SrunSettings from .step import Step -from ....settings import SrunSettings, SbatchSettings, RunSettings, Singularity logger = get_logger(__name__) @@ -189,13 +189,15 @@ def _set_alloc(self) -> None: def _get_mpmd(self) -> t.List[RunSettings]: """Temporary convenience function to return a typed list - of attached RunSettings""" + of attached RunSettings + """ return self.run_settings.mpmd @staticmethod def _get_exe_args_list(run_setting: RunSettings) -> t.List[str]: """Convenience function to encapsulate checking the - runsettings.exe_args type to always return a list""" + runsettings.exe_args type to always return a list + """ exe_args = run_setting.exe_args args: t.List[str] = exe_args if isinstance(exe_args, list) else [exe_args] return args diff --git a/smartsim/_core/launcher/step/step.py b/smartsim/_core/launcher/step/step.py index b3c9a808f..ebbdd074e 100644 --- a/smartsim/_core/launcher/step/step.py +++ b/smartsim/_core/launcher/step/step.py @@ -26,16 +26,20 @@ from __future__ import annotations +import functools import os.path as osp +import sys import time import typing as t +from os import makedirs -from smartsim.error.errors import SmartSimError +from smartsim._core.config import CONFIG +from smartsim.error.errors import SmartSimError, UnproxyableStepError from ....log import get_logger -from ...utils.helpers import get_base_36_repr +from ....settings.base import RunSettings, SettingsBase +from ...utils.helpers import encode_cmd, get_base_36_repr from ..colocated import write_colocated_launch_script -from ....settings.base import SettingsBase, RunSettings logger = get_logger(__name__) @@ -47,6 +51,12 @@ def __init__(self, name: str, cwd: str, step_settings: SettingsBase) -> None: self.cwd = cwd self.managed = False self.step_settings = step_settings + self.meta: t.Dict[str, str] = {} + + @property + def env(self) -> t.Optional[t.Dict[str, str]]: + """Overridable, read only property for step to specify its environment""" + return None def get_launch_cmd(self) -> t.List[str]: raise NotImplementedError @@ -67,7 +77,8 @@ def get_step_file( ) -> str: """Get the name for a file/script created by the step class - Used for Batch scripts, mpmd scripts, etc""" + Used for Batch scripts, mpmd scripts, etc. + """ if script_name: script_name = script_name if "." in script_name else script_name + ending return osp.join(self.cwd, script_name) @@ -75,7 +86,12 @@ def get_step_file( def get_colocated_launch_script(self) -> str: # prep step for colocated launch if specifed in run settings - script_path = self.get_step_file(script_name=".colocated_launcher.sh") + script_path = self.get_step_file( + script_name=osp.join( + ".smartsim", f"colocated_launcher_{self.entity_name}.sh" + ) + ) + makedirs(osp.dirname(script_path), exist_ok=True) db_settings: t.Dict[str, str] = {} if isinstance(self.step_settings, RunSettings): @@ -101,3 +117,49 @@ def add_to_batch(self, step: Step) -> None: :type step: Step """ raise SmartSimError("add_to_batch not implemented for this step type") + + +_StepT = t.TypeVar("_StepT", bound=Step) + + +def proxyable_launch_cmd( + fn: t.Callable[[_StepT], t.List[str]], / +) -> t.Callable[[_StepT], t.List[str]]: + @functools.wraps(fn) + def _get_launch_cmd(self: _StepT) -> t.List[str]: + original_cmd_list = fn(self) + + if not CONFIG.telemetry_enabled: + return original_cmd_list + + if self.managed: + raise UnproxyableStepError( + f"Attempting to proxy managed step of type {type(self)}" + "through the unmanaged step proxy entry point" + ) + + proxy_module = "smartsim._core.entrypoints.indirect" + etype = self.meta["entity_type"] + status_dir = self.meta["status_dir"] + encoded_cmd = encode_cmd(original_cmd_list) + + # NOTE: this is NOT safe. should either 1) sign cmd and verify OR 2) + # serialize step and let the indirect entrypoint rebuild the + # cmd... for now, test away... + return [ + sys.executable, + "-m", + proxy_module, + "+name", + self.name, + "+command", + encoded_cmd, + "+entity_type", + etype, + "+telemetry_dir", + status_dir, + "+working_dir", + self.cwd, + ] + + return _get_launch_cmd diff --git a/smartsim/_core/launcher/stepInfo.py b/smartsim/_core/launcher/stepInfo.py index 0f46f1ab0..b33dac5ec 100644 --- a/smartsim/_core/launcher/stepInfo.py +++ b/smartsim/_core/launcher/stepInfo.py @@ -24,9 +24,10 @@ # 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. -import psutil import typing as t +import psutil + from ...status import ( SMARTSIM_STATUS, STATUS_CANCELLED, diff --git a/smartsim/_core/launcher/stepMapping.py b/smartsim/_core/launcher/stepMapping.py index 9c5e2bec4..665404b1b 100644 --- a/smartsim/_core/launcher/stepMapping.py +++ b/smartsim/_core/launcher/stepMapping.py @@ -32,10 +32,12 @@ class StepMap: - def __init__(self, - step_id: t.Optional[str] = None, - task_id: t.Optional[str] = None, - managed: t.Optional[bool] = None) -> None: + def __init__( + self, + step_id: t.Optional[str] = None, + task_id: t.Optional[str] = None, + managed: t.Optional[bool] = None, + ) -> None: self.step_id = step_id self.task_id = task_id self.managed = managed diff --git a/smartsim/_core/launcher/taskManager.py b/smartsim/_core/launcher/taskManager.py index d244db304..2ad84493f 100644 --- a/smartsim/_core/launcher/taskManager.py +++ b/smartsim/_core/launcher/taskManager.py @@ -26,13 +26,13 @@ from __future__ import annotations -import psutil import time import typing as t - from subprocess import PIPE from threading import RLock, Thread +import psutil + from ...error import LauncherError from ...log import get_logger from ..utils.helpers import check_dev_log_level diff --git a/smartsim/_core/launcher/util/shell.py b/smartsim/_core/launcher/util/shell.py index 9961042a6..1fc243c15 100644 --- a/smartsim/_core/launcher/util/shell.py +++ b/smartsim/_core/launcher/util/shell.py @@ -24,11 +24,12 @@ # 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. -import psutil import time import typing as t from subprocess import PIPE, TimeoutExpired +import psutil + from ....error import ShellError from ....log import get_logger from ...utils.helpers import check_dev_log_level diff --git a/smartsim/_core/utils/helpers.py b/smartsim/_core/utils/helpers.py index 1bac9cb20..8d7edf722 100644 --- a/smartsim/_core/utils/helpers.py +++ b/smartsim/_core/utils/helpers.py @@ -27,30 +27,59 @@ """ A file of helper functions for SmartSim """ +import base64 import os -import uuid import typing as t +import uuid +from datetime import datetime from functools import lru_cache from pathlib import Path from shutil import which from smartsim._core._install.builder import TRedisAIBackendStr as _TRedisAIBackendStr + +def unpack_db_identifier(db_id: str, token: str) -> t.Tuple[str, str]: + """Unpack the unformatted database identifier + and format for env variable suffix using the token + :param db_id: the unformatted database identifier eg. identifier_1 + :type db_id: str + :param token: character to use to construct the db suffix + :type token: str + :return: db id suffix and formatted db_id e.g. ("_identifier_1", "identifier_1") + :rtype: (str, str) + """ + + if db_id == "orchestrator": + return "", "" + db_name_suffix = token + db_id + return db_name_suffix, db_id + + +def unpack_colo_db_identifier(db_id: str) -> str: + """Create database identifier suffix for colocated database + :param db_id: the unformatted database identifier + :type db_id: str + :return: db suffix + :rtype: str + """ + return "_" + db_id if db_id else "" + + +def create_short_id_str() -> str: + return str(uuid.uuid4())[:7] + + def create_lockfile_name() -> str: """Generate a unique lock filename using UUID""" - lock_suffix = str(uuid.uuid4())[:7] + lock_suffix = create_short_id_str() return f"smartsim-{lock_suffix}.lock" @lru_cache(maxsize=20, typed=False) def check_dev_log_level() -> bool: - try: - lvl = os.environ["SMARTSIM_LOG_LEVEL"] - if lvl == "developer": - return True - return False - except KeyError: - return False + lvl = os.environ.get("SMARTSIM_LOG_LEVEL", "") + return lvl == "developer" def fmt_dict(value: t.Dict[str, t.Any]) -> str: @@ -245,3 +274,31 @@ def installed_redisai_backends( } return {backend for backend in backends if _installed(base_path, backend)} + + +def get_ts() -> int: + """Return the current timestamp (accurate to seconds) cast to an integer""" + return int(datetime.timestamp(datetime.now())) + + +def encode_cmd(cmd: t.List[str]) -> str: + """Transform a standard command list into an encoded string safe for providing as an + argument to a proxy entrypoint + """ + if not cmd: + raise ValueError("Invalid cmd supplied") + + ascii_cmd = "|".join(cmd).encode("ascii") + encoded_cmd = base64.b64encode(ascii_cmd).decode("ascii") + return encoded_cmd + + +def decode_cmd(encoded_cmd: str) -> t.List[str]: + """Decode an encoded command string to the original command list format""" + if not encoded_cmd.strip(): + raise ValueError("Invalid cmd supplied") + + decoded_cmd = base64.b64decode(encoded_cmd.encode("ascii")) + cleaned_cmd = decoded_cmd.decode("ascii").split("|") + + return cleaned_cmd diff --git a/smartsim/_core/utils/network.py b/smartsim/_core/utils/network.py index ed5aed9c9..f18be208e 100644 --- a/smartsim/_core/utils/network.py +++ b/smartsim/_core/utils/network.py @@ -58,7 +58,6 @@ def get_ip_from_interface(interface: str) -> str: # pragma: no cover """ net_if_addrs = psutil.net_if_addrs() if interface not in net_if_addrs: - available = list(net_if_addrs.keys()) raise ValueError( f"{interface} is not a valid network interface. " diff --git a/smartsim/_core/utils/redis.py b/smartsim/_core/utils/redis.py index 9645a367e..6c592d0f3 100644 --- a/smartsim/_core/utils/redis.py +++ b/smartsim/_core/utils/redis.py @@ -25,12 +25,12 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import logging -import redis import time import typing as t - from itertools import product -from redis.cluster import RedisCluster, ClusterNode + +import redis +from redis.cluster import ClusterNode, RedisCluster from redis.exceptions import ClusterDownError, RedisClusterException from smartredis import Client from smartredis.error import RedisReplyError @@ -171,6 +171,7 @@ def set_ml_model(db_model: DBModel, client: Client) -> None: device=device, batch_size=db_model.batch_size, min_batch_size=db_model.min_batch_size, + min_batch_timeout=db_model.min_batch_timeout, tag=db_model.tag, inputs=db_model.inputs, outputs=db_model.outputs, @@ -183,6 +184,7 @@ def set_ml_model(db_model: DBModel, client: Client) -> None: device=device, batch_size=db_model.batch_size, min_batch_size=db_model.min_batch_size, + min_batch_timeout=db_model.min_batch_timeout, tag=db_model.tag, inputs=db_model.inputs, outputs=db_model.outputs, @@ -214,3 +216,30 @@ def set_script(db_script: DBScript, client: Client) -> None: except RedisReplyError as error: # pragma: no cover logger.error("Error while setting model on orchestrator.") raise error + + +def shutdown_db_node(host_ip: str, port: int) -> t.Tuple[int, str, str]: # cov-wlm + """Send shutdown signal to DB node. + + Should only be used in the case where cluster deallocation + needs to occur manually. Usually, the SmartSim job manager + will take care of this automatically. + + :param host_ip: IP of host to connect to + :type hosts: str + :param ports: Port to which node is listening + :type ports: int + :return: returncode, output, and error of the process + :rtype: tuple of (int, str, str) + """ + redis_cli = CONFIG.database_cli + cmd = [redis_cli, "-h", host_ip, "-p", str(port), "shutdown"] + returncode, out, err = execute_cmd(cmd, proc_input="yes", shell=False, timeout=10) + + if returncode != 0: + logger.error(out) + logger.error(err) + elif out: + logger.debug(out) + + return returncode, out, err diff --git a/smartsim/_core/utils/serialize.py b/smartsim/_core/utils/serialize.py new file mode 100644 index 000000000..75f9aef66 --- /dev/null +++ b/smartsim/_core/utils/serialize.py @@ -0,0 +1,255 @@ +# BSD 2-Clause License +# +# Copyright (c) 2021-2023, 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 __future__ import annotations + +import json +import time +import typing as t +from pathlib import Path + +import smartsim._core._cli.utils as _utils +import smartsim.log +from smartsim._core.config import CONFIG + +if t.TYPE_CHECKING: + from smartsim import Experiment + from smartsim._core.control.manifest import LaunchedManifest as _Manifest + from smartsim.database.orchestrator import Orchestrator + from smartsim.entity import DBNode, Ensemble, Model + from smartsim.entity.dbobject import DBModel, DBScript + from smartsim.settings.base import BatchSettings, RunSettings + + +TStepLaunchMetaData = t.Tuple[ + t.Optional[str], t.Optional[str], t.Optional[bool], str, str, Path +] +TELMON_SUBDIR: t.Final[str] = ".smartsim/telemetry" +MANIFEST_FILENAME: t.Final[str] = "manifest.json" + +_LOGGER = smartsim.log.get_logger(__name__) + + +def save_launch_manifest(manifest: _Manifest[TStepLaunchMetaData]) -> None: + if not CONFIG.telemetry_enabled: + return + + manifest.metadata.run_telemetry_subdirectory.mkdir(parents=True, exist_ok=True) + + new_run = { + "run_id": manifest.metadata.run_id, + "timestamp": int(time.time_ns()), + "model": [ + _dictify_model(model, *telemetry_metadata) + for model, telemetry_metadata in manifest.models + ], + "orchestrator": [ + _dictify_db(db, nodes_info) for db, nodes_info in manifest.databases + ], + "ensemble": [ + _dictify_ensemble(ens, member_info) + for ens, member_info in manifest.ensembles + ], + } + try: + with open(manifest.metadata.manifest_file_path, "r", encoding="utf-8") as file: + manifest_dict = json.load(file) + except (FileNotFoundError, json.JSONDecodeError): + manifest_dict = { + "schema info": { + "schema_name": "entity manifest", + "version": "0.0.2", + }, + "experiment": { + "name": manifest.metadata.exp_name, + "path": manifest.metadata.exp_path, + "launcher": manifest.metadata.launcher_name, + }, + "runs": [new_run], + } + else: + manifest_dict["runs"].append(new_run) + finally: + with open(manifest.metadata.manifest_file_path, "w", encoding="utf-8") as file: + json.dump(manifest_dict, file, indent=2) + + +def _dictify_model( + model: Model, + step_id: t.Optional[str], + task_id: t.Optional[str], + managed: t.Optional[bool], + out_file: str, + err_file: str, + telemetry_data_path: Path, +) -> t.Dict[str, t.Any]: + colo_settings = (model.run_settings.colocated_db_settings or {}).copy() + db_scripts = t.cast("t.List[DBScript]", colo_settings.pop("db_scripts", [])) + db_models = t.cast("t.List[DBModel]", colo_settings.pop("db_models", [])) + return { + "name": model.name, + "path": model.path, + "exe_args": model.run_settings.exe_args, + "run_settings": _dictify_run_settings(model.run_settings), + "batch_settings": ( + _dictify_batch_settings(model.batch_settings) + if model.batch_settings + else {} + ), + "params": model.params, + "files": ( + { + "Symlink": model.files.link, + "Configure": model.files.tagged, + "Copy": model.files.copy, + } + if model.files + else { + "Symlink": [], + "Configure": [], + "Copy": [], + } + ), + "colocated_db": ( + { + "settings": colo_settings, + "scripts": [ + { + script.name: { + "backend": "TORCH", + "device": script.device, + } + } + for script in db_scripts + ], + "models": [ + { + model.name: { + "backend": model.backend, + "device": model.device, + } + } + for model in db_models + ], + } + if colo_settings + else {} + ), + "telemetry_metadata": { + "status_dir": str(telemetry_data_path), + "step_id": step_id, + "task_id": task_id, + "managed": managed, + }, + "out_file": out_file, + "err_file": err_file, + } + + +def _dictify_ensemble( + ens: Ensemble, + members: t.Sequence[t.Tuple[Model, TStepLaunchMetaData]], +) -> t.Dict[str, t.Any]: + return { + "name": ens.name, + "params": ens.params, + "batch_settings": ( + _dictify_batch_settings(ens.batch_settings) + # FIXME: Typehint here is wrong, ``ens.batch_settings`` can + # also be an empty dict for no discernible reason... + if ens.batch_settings + else {} + ), + "models": [ + _dictify_model(model, *launching_metadata) + for model, launching_metadata in members + ], + } + + +def _dictify_run_settings(run_settings: RunSettings) -> t.Dict[str, t.Any]: + # TODO: remove this downcast + if hasattr(run_settings, "mpmd") and run_settings.mpmd: + _LOGGER.warning( + "SmartSim currently cannot properly serialize all information in " + "MPMD run settings" + ) + return { + "exe": run_settings.exe, + # TODO: We should try to move this back + # "exe_args": run_settings.exe_args, + "run_command": run_settings.run_command, + "run_args": run_settings.run_args, + # TODO: We currently do not have a way to represent MPMD commands! + # Maybe add a ``"mpmd"`` key here that is a + # ``list[TDictifiedRunSettings]``? + } + + +def _dictify_batch_settings(batch_settings: BatchSettings) -> t.Dict[str, t.Any]: + return { + "batch_command": batch_settings.batch_cmd, + "batch_args": batch_settings.batch_args, + } + + +def _dictify_db( + db: Orchestrator, + nodes: t.Sequence[t.Tuple[DBNode, TStepLaunchMetaData]], +) -> t.Dict[str, t.Any]: + db_path = _utils.get_db_path() + if db_path: + db_type, _ = db_path.name.split("-", 1) + else: + db_type = "Unknown" + return { + "name": db.name, + "type": db_type, + "interface": db._interfaces, # pylint: disable=protected-access + "shards": [ + { + **shard.to_dict(), + "conf_file": shard.cluster_conf_file, + "out_file": out_file, + "err_file": err_file, + "telemetry_metadata": { + "status_dir": str(status_dir), + "step_id": step_id, + "task_id": task_id, + "managed": managed, + }, + } + for dbnode, ( + step_id, + task_id, + managed, + out_file, + err_file, + status_dir, + ) in nodes + for shard in dbnode.get_launched_shard_info() + ], + } diff --git a/smartsim/database/orchestrator.py b/smartsim/database/orchestrator.py index 6be4f09a9..07a1a1bfd 100644 --- a/smartsim/database/orchestrator.py +++ b/smartsim/database/orchestrator.py @@ -24,24 +24,23 @@ # 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. import itertools -import psutil import sys import typing as t - -from os import getcwd, getenv +from os import environ, getcwd, getenv from shlex import split as sh_split -from smartredis import Client +import psutil +from smartredis import Client, ConfigOptions from smartredis.error import RedisReplyError from .._core.config import CONFIG from .._core.utils import db_is_active -from .._core.utils.helpers import is_valid_cmd +from .._core.utils.helpers import is_valid_cmd, unpack_db_identifier from .._core.utils.network import get_ip_from_host from ..entity import DBNode, EntityList from ..error import SmartSimError, SSConfigError, SSUnsupportedError from ..log import get_logger -from ..settings.base import BatchSettings, RunSettings +from ..servertype import CLUSTERED, STANDALONE from ..settings import ( AprunSettings, BsubBatchSettings, @@ -55,12 +54,12 @@ SbatchSettings, SrunSettings, ) +from ..settings.base import BatchSettings, RunSettings from ..settings.settings import create_batch_settings, create_run_settings from ..wlm import detect_launcher logger = get_logger(__name__) - by_launcher: t.Dict[str, t.List[str]] = { "slurm": ["srun", "mpirun", "mpiexec"], "pbs": ["aprun", "mpirun", "mpiexec"], @@ -141,7 +140,7 @@ def _check_local_constraints(launcher: str, batch: bool) -> None: raise SmartSimError(msg) -class Orchestrator(EntityList): +class Orchestrator(EntityList[DBNode]): """The Orchestrator is an in-memory database that can be launched alongside entities in SmartSim. Data can be transferred between entities by using one of the Python, C, C++ or Fortran clients @@ -156,11 +155,16 @@ def __init__( run_command: str = "auto", db_nodes: int = 1, batch: bool = False, - hosts: t.Optional[t.List[str]] = None, + hosts: t.Optional[t.Union[t.List[str], str]] = None, account: t.Optional[str] = None, time: t.Optional[str] = None, alloc: t.Optional[str] = None, single_cmd: bool = False, + *, + threads_per_queue: t.Optional[int] = None, + inter_op_threads: t.Optional[int] = None, + intra_op_threads: t.Optional[int] = None, + db_identifier: str = "orchestrator", **kwargs: t.Any, ) -> None: """Initialize an Orchestrator reference for local launch @@ -181,46 +185,43 @@ def __init__( :param intra_op_threads: threads per CPU operation :type intra_op_threads: int, optional """ - launcher, run_command = _autodetect(launcher, run_command) - - _check_run_command(launcher, run_command) - _check_local_constraints(launcher, batch) - - single_cmd = _get_single_command(run_command, batch, single_cmd) - - self.launcher = launcher - self.run_command = run_command - + self.launcher, self.run_command = _autodetect(launcher, run_command) + _check_run_command(self.launcher, self.run_command) + _check_local_constraints(self.launcher, batch) + single_cmd = _get_single_command(self.run_command, batch, single_cmd) self.ports: t.List[int] = [] - self.path = getcwd() self._hosts: t.List[str] = [] + self._user_hostlist: t.List[str] = [] if isinstance(interface, str): interface = [interface] self._interfaces = interface self._check_network_interface() - self.queue_threads = kwargs.get("threads_per_queue", None) - self.inter_threads = kwargs.get("inter_op_threads", None) - self.intra_threads = kwargs.get("intra_op_threads", None) + self.queue_threads = threads_per_queue + self.inter_threads = inter_op_threads + self.intra_threads = intra_op_threads + + gpus_per_shard: t.Optional[int] = None + cpus_per_shard: t.Optional[int] = None if self.launcher == "lsf": - gpus_per_shard = kwargs.pop("gpus_per_shard", 0) - cpus_per_shard = kwargs.pop("cpus_per_shard", 4) - else: - gpus_per_shard = None - cpus_per_shard = None + gpus_per_shard = int(kwargs.pop("gpus_per_shard", 0)) + cpus_per_shard = int(kwargs.pop("cpus_per_shard", 4)) super().__init__( - "orchestrator", - self.path, + name=db_identifier, + path=getcwd(), port=port, interface=interface, db_nodes=db_nodes, batch=batch, - launcher=launcher, - run_command=run_command, + launcher=self.launcher, + run_command=self.run_command, alloc=alloc, single_cmd=single_cmd, gpus_per_shard=gpus_per_shard, cpus_per_shard=cpus_per_shard, + threads_per_queue=threads_per_queue, + inter_op_threads=inter_op_threads, + intra_op_threads=intra_op_threads, **kwargs, ) @@ -234,25 +235,26 @@ def __init__( self._redis_conf # pylint: disable=W0104 CONFIG.database_cli # pylint: disable=W0104 except SSConfigError as e: - msg = "SmartSim not installed with pre-built extensions (Redis)\n" - msg += "Use the `smart` cli tool to install needed extensions\n" - msg += "or set REDIS_PATH and REDIS_CLI_PATH in your environment\n" - msg += "See documentation for more information" - raise SSConfigError(msg) from e + raise SSConfigError( + "SmartSim not installed with pre-built extensions (Redis)\n" + "Use the `smart` cli tool to install needed extensions\n" + "or set REDIS_PATH and REDIS_CLI_PATH in your environment\n" + "See documentation for more information" + ) from e - if launcher != "local": + if self.launcher != "local": self.batch_settings = self._build_batch_settings( db_nodes, alloc or "", batch, account or "", time or "", - launcher=launcher, + launcher=self.launcher, **kwargs, ) if hosts: self.set_hosts(hosts) - elif not hosts and run_command == "mpirun": + elif not hosts and self.run_command == "mpirun": raise SmartSimError( "hosts argument is required when launching Orchestrator with mpirun" ) @@ -260,6 +262,15 @@ def __init__( self._reserved_batch_args: t.Dict[t.Type[BatchSettings], t.List[str]] = {} self._fill_reserved() + @property + def db_identifier(self) -> str: + """Return the DB identifier, which is common to a DB and all of its nodes + + :return: DB identifier + :rtype: str + """ + return self.name + @property def num_shards(self) -> int: """Return the number of DB shards contained in the orchestrator. @@ -269,7 +280,19 @@ def num_shards(self) -> int: :returns: num_shards :rtype: int """ - return self.db_nodes + return sum(node.num_shards for node in self.entities) + + @property + def db_nodes(self) -> int: + """Read only property for the number of nodes an ``Orchestrator`` is + launched across. Notice that SmartSim currently assumes that each shard + will be launched on its own node. Therefore this property is currently + an alias to the ``num_shards`` attribute. + + :returns: Number of database nodes + :rtype: int + """ + return self.num_shards @property def hosts(self) -> t.List[str]: @@ -285,10 +308,19 @@ def hosts(self) -> t.List[str]: self._hosts = self._get_db_hosts() return self._hosts + def reset_hosts(self) -> None: + """Clear hosts or reset them to last user choice""" + for node in self.entities: + node.clear_hosts() + self._hosts = [] + # This is only needed on LSF + if self._user_hostlist: + self.set_hosts(self._user_hostlist) + def remove_stale_files(self) -> None: """Can be used to remove database files of a previous launch""" - for db in self.dbnodes: + for db in self.entities: db.remove_stale_dbnode_files() def get_address(self) -> t.List[str]: @@ -306,10 +338,10 @@ def get_address(self) -> t.List[str]: return self._get_address() def _get_address(self) -> t.List[str]: - addresses: t.List[str] = [] - for ip_address, port in itertools.product(self._hosts, self.ports): - addresses.append(":".join((ip_address, str(port)))) - return addresses + return [ + f"{host}:{port}" + for host, port in itertools.product(self._hosts, self.ports) + ] def is_active(self) -> bool: """Check if the database is active @@ -323,20 +355,21 @@ def is_active(self) -> bool: return db_is_active(self._hosts, self.ports, self.num_shards) @property - def _rai_module(self) -> str: + def _rai_module(self) -> t.Tuple[str, ...]: """Get the RedisAI module from third-party installations - :return: path to module or "" if not found - :rtype: str + :return: Tuple of args to pass to the orchestrator exe + to load and configure the RedisAI + :rtype: tuple[str] """ module = ["--loadmodule", CONFIG.redisai] if self.queue_threads: - module.append(f"THREADS_PER_QUEUE {self.queue_threads}") + module.extend(("THREADS_PER_QUEUE", str(self.queue_threads))) if self.inter_threads: - module.append(f"INTER_OP_PARALLELISM {self.inter_threads}") + module.extend(("INTER_OP_PARALLELISM", str(self.inter_threads))) if self.intra_threads: - module.append(f"INTRA_OP_PARALLELISM {self.intra_threads}") - return " ".join(module) + module.extend(("INTRA_OP_PARALLELISM", str(self.intra_threads))) + return tuple(module) @property def _redis_exe(self) -> str: @@ -365,7 +398,7 @@ def set_cpus(self, num_cpus: int) -> None: if hasattr(self.batch_settings, "set_cpus_per_task"): self.batch_settings.set_cpus_per_task(num_cpus) - for db in self.dbnodes: + for db in self.entities: db.run_settings.set_cpus_per_task(num_cpus) if db.is_mpmd and hasattr(db.run_settings, "mpmd"): for mpmd in db.run_settings.mpmd: @@ -386,7 +419,7 @@ def set_walltime(self, walltime: str) -> None: if hasattr(self, "batch_settings") and self.batch_settings: self.batch_settings.set_walltime(walltime) - def set_hosts(self, host_list: t.List[str]) -> None: + def set_hosts(self, host_list: t.Union[t.List[str], str]) -> None: """Specify the hosts for the ``Orchestrator`` to launch on :param host_list: list of host (compute node names) @@ -399,16 +432,24 @@ def set_hosts(self, host_list: t.List[str]) -> None: raise TypeError("host_list argument must be a list of strings") if not all(isinstance(host, str) for host in host_list): raise TypeError("host_list argument must be list of strings") + self._user_hostlist = host_list.copy() # TODO check length if self.batch: if hasattr(self, "batch_settings") and self.batch_settings: self.batch_settings.set_hostlist(host_list) if self.launcher == "lsf": - for db in self.dbnodes: + for db in self.entities: db.set_hosts(host_list) + elif ( + self.launcher == "pals" + and isinstance(self.entities[0].run_settings, PalsMpiexecSettings) + and self.entities[0].is_mpmd + ): + # In this case, --hosts is a global option, set it to first run command + self.entities[0].run_settings.set_hostlist(host_list) else: - for host, db in zip(host_list, self.dbnodes): + for host, db in zip(host_list, self.entities): if isinstance(db.run_settings, AprunSettings): if not self.batch: db.run_settings.set_hostlist([host]) @@ -416,8 +457,8 @@ def set_hosts(self, host_list: t.List[str]) -> None: db.run_settings.set_hostlist([host]) if db.is_mpmd and hasattr(db.run_settings, "mpmd"): - for i, mpmd_runsettings in enumerate(db.run_settings.mpmd): - mpmd_runsettings.set_hostlist(host_list[i + 1]) + for i, mpmd_runsettings in enumerate(db.run_settings.mpmd, 1): + mpmd_runsettings.set_hostlist(host_list[i]) def set_batch_arg(self, arg: str, value: t.Optional[str] = None) -> None: """Set a batch argument the orchestrator should launch with @@ -462,26 +503,24 @@ def set_run_arg(self, arg: str, value: t.Optional[str] = None) -> None: "it is a reserved keyword in Orchestrator" ) else: - for db in self.dbnodes: + for db in self.entities: db.run_settings.run_args[arg] = value if db.is_mpmd and hasattr(db.run_settings, "mpmd"): for mpmd in db.run_settings.mpmd: mpmd.run_args[arg] = value def enable_checkpoints(self, frequency: int) -> None: - """Sets the database's save configuration to save the - DB every 'frequency' seconds given that at least one - write operation against the DB occurred in that time. - For example, if `frequency` is 900, then the database - will save to disk after 900 seconds if there is at least - 1 change to the dataset. + """Sets the database's save configuration to save the DB every 'frequency' + seconds given that at least one write operation against the DB occurred in + that time. E.g., if `frequency` is 900, then the database will save to disk + after 900 seconds if there is at least 1 change to the dataset. :param frequency: the given number of seconds before the DB saves :type frequency: int """ - self.set_db_conf("save", str(frequency) + " 1") + self.set_db_conf("save", f"{frequency} 1") - def set_max_memory(self, mem: int) -> None: + def set_max_memory(self, mem: str) -> None: """Sets the max memory configuration. By default there is no memory limit. Setting max memory to zero also results in no memory limit. Once a limit is surpassed, keys will be removed according to the eviction strategy. The @@ -495,7 +534,6 @@ def set_max_memory(self, mem: int) -> None: :param mem: the desired max memory size e.g. 3gb :type mem: str - :raises SmartSimError: If 'mem' is an invalid memory value :raises SmartSimError: If database is not active """ @@ -508,7 +546,6 @@ def set_eviction_strategy(self, strategy: str) -> None: :param strategy: The max memory policy to use e.g. "volatile-lru", "allkeys-lru", etc. :type strategy: str - :raises SmartSimError: If 'strategy' is an invalid maxmemory policy :raises SmartSimError: If database is not active """ @@ -553,11 +590,18 @@ def set_db_conf(self, key: str, value: t.Union[int, str]) -> None: addresses = [] for host in self.hosts: for port in self.ports: - address = ":".join([get_ip_from_host(host), str(port)]) - addresses.append(address) + addresses.append(":".join([get_ip_from_host(host), str(port)])) + + db_name, name = unpack_db_identifier(self.db_identifier, "_") + + environ[f"SSDB{db_name}"] = addresses[0] + + db_type = CLUSTERED if self.num_shards > 2 else STANDALONE + environ[f"SR_DB_TYPE{db_name}"] = db_type + + options = ConfigOptions.create_from_environment(name) + client = Client(options) - is_cluster = self.num_shards > 2 - client = Client(address=addresses[0], cluster=is_cluster) try: for address in addresses: client.config_set(key, value, address) @@ -584,10 +628,14 @@ def _build_batch_settings( batch: bool, account: str, time: str, + *, + launcher: t.Optional[str] = None, **kwargs: t.Any, ) -> t.Optional[BatchSettings]: batch_settings = None - launcher = kwargs.pop("launcher") + + if launcher is None: + raise ValueError("Expected param `launcher` of type `str`") # enter this conditional if user has not specified an allocation to run # on or if user specified batch=False (alloc will be found through env) @@ -599,11 +647,16 @@ def _build_batch_settings( return batch_settings def _build_run_settings( - self, exe: str, exe_args: t.List[t.List[str]], **kwargs: t.Any + self, + exe: str, + exe_args: t.List[t.List[str]], + *, + run_args: t.Optional[t.Dict[str, t.Any]] = None, + db_nodes: int = 1, + single_cmd: bool = True, + **kwargs: t.Any, ) -> RunSettings: - run_args = kwargs.pop("run_args", {}) - db_nodes = kwargs.get("db_nodes", 1) - single_cmd = kwargs.get("single_cmd", True) + run_args = {} if run_args is None else run_args mpmd_nodes = single_cmd and db_nodes > 1 if mpmd_nodes: @@ -632,20 +685,28 @@ def _build_run_settings( if self.launcher != "local": run_settings.set_tasks_per_node(1) - # Put it back in case it is needed again - kwargs["run_args"] = run_args - return run_settings @staticmethod def _build_run_settings_lsf( - exe: str, exe_args: t.List[t.List[str]], **kwargs: t.Any + exe: str, + exe_args: t.List[t.List[str]], + *, + run_args: t.Optional[t.Dict[str, t.Any]] = None, + cpus_per_shard: t.Optional[int] = None, + gpus_per_shard: t.Optional[int] = None, + **_kwargs: t.Any, # Needed to ensure no API break and do not want to + # introduce that possibility, even if this method is + # protected, without running the test suite. ) -> t.Optional[JsrunSettings]: - run_args = kwargs.pop("run_args", {}) - cpus_per_shard = kwargs.get("cpus_per_shard", None) - gpus_per_shard = kwargs.get("gpus_per_shard", None) + run_args = {} if run_args is None else run_args erf_rs: t.Optional[JsrunSettings] = None + if cpus_per_shard is None: + raise ValueError("Expected an integer number of cpus per shard") + if gpus_per_shard is None: + raise ValueError("Expected an integer number of gpus per shard") + # We always run the DB on cpus 0:cpus_per_shard-1 # and gpus 0:gpus_per_shard-1 for shard_id, args in enumerate(exe_args): @@ -666,9 +727,9 @@ def _build_run_settings_lsf( } if gpus_per_shard > 1: # pragma: no-cover - erf_sets["gpu"] = "{" + f"0-{gpus_per_shard-1}" + "}" + erf_sets["gpu"] = f"{{0-{gpus_per_shard-1}}}" elif gpus_per_shard > 0: - erf_sets["gpu"] = "{" + str(0) + "}" + erf_sets["gpu"] = "{0}" run_settings.set_erf_sets(erf_sets) @@ -678,31 +739,35 @@ def _build_run_settings_lsf( erf_rs.make_mpmd(run_settings) - kwargs["run_args"] = run_args - return erf_rs - def _initialize_entities(self, **kwargs: t.Any) -> None: - self.db_nodes = int(kwargs.get("db_nodes", 1)) - single_cmd = kwargs.get("single_cmd", True) - - if int(self.db_nodes) == 2: + def _initialize_entities( + self, + *, + db_nodes: int = 1, + single_cmd: bool = True, + port: int = 6379, + **kwargs: t.Any, + ) -> None: + db_nodes = int(db_nodes) + if db_nodes == 2: raise SSUnsupportedError("Orchestrator does not support clusters of size 2") - if self.launcher == "local" and self.db_nodes > 1: + if self.launcher == "local" and db_nodes > 1: raise ValueError( "Local Orchestrator does not support multiple database shards" ) - mpmd_nodes = (single_cmd and self.db_nodes > 1) or self.launcher == "lsf" + mpmd_nodes = (single_cmd and db_nodes > 1) or self.launcher == "lsf" if mpmd_nodes: - self._initialize_entities_mpmd(**kwargs) + self._initialize_entities_mpmd( + db_nodes=db_nodes, single_cmd=single_cmd, port=port, **kwargs + ) else: - port = kwargs.get("port", 6379) - cluster = not bool(self.db_nodes < 3) + cluster = db_nodes >= 3 - for db_id in range(self.db_nodes): + for db_id in range(db_nodes): db_node_name = "_".join((self.name, str(db_id))) # create the exe_args list for launching multiple databases @@ -710,11 +775,10 @@ def _initialize_entities(self, **kwargs: t.Any) -> None: start_script_args = self._get_start_script_args( db_node_name, port, cluster ) - # if only launching 1 db per command, we don't need a # list of exe args lists run_settings = self._build_run_settings( - sys.executable, [start_script_args], **kwargs + sys.executable, [start_script_args], port=port, **kwargs ) node = DBNode( @@ -723,18 +787,20 @@ def _initialize_entities(self, **kwargs: t.Any) -> None: run_settings, [port], [db_node_name + ".out"], + self.db_identifier, ) self.entities.append(node) self.ports = [port] - def _initialize_entities_mpmd(self, **kwargs: t.Any) -> None: - port = kwargs.get("port", 6379) - cluster = not bool(self.db_nodes < 3) - + def _initialize_entities_mpmd( + self, *, db_nodes: int = 1, port: int = 6379, **kwargs: t.Any + ) -> None: + cluster = db_nodes >= 3 + mpmd_node_name = self.name + "_0" exe_args_mpmd: t.List[t.List[str]] = [] - for db_id in range(self.db_nodes): + for db_id in range(db_nodes): db_shard_name = "_".join((self.name, str(db_id))) # create the exe_args list for launching multiple databases # per node. also collect port range for dbnode @@ -743,70 +809,51 @@ def _initialize_entities_mpmd(self, **kwargs: t.Any) -> None: ) exe_args = " ".join(start_script_args) exe_args_mpmd.append(sh_split(exe_args)) - run_settings: t.Optional[RunSettings] = None - if self.launcher == "lsf": run_settings = self._build_run_settings_lsf( - sys.executable, exe_args_mpmd, **kwargs + sys.executable, exe_args_mpmd, db_nodes=db_nodes, port=port, **kwargs ) - output_files = [ - "_".join((self.name, str(db_id))) + ".out" - for db_id in range(self.db_nodes) - ] + output_files = [f"{self.name}_{db_id}.out" for db_id in range(db_nodes)] else: run_settings = self._build_run_settings( - sys.executable, exe_args_mpmd, **kwargs + sys.executable, exe_args_mpmd, db_nodes=db_nodes, port=port, **kwargs ) - output_files = [self.name + ".out"] - + output_files = [mpmd_node_name + ".out"] if not run_settings: raise ValueError(f"Could not build run settings for {self.launcher}") - - node = DBNode(self.name, self.path, run_settings, [port], output_files) - node.is_mpmd = True - node.num_shards = self.db_nodes + node = DBNode( + mpmd_node_name, + self.path, + run_settings, + [port], + output_files, + db_identifier=self.db_identifier, + ) self.entities.append(node) - self.ports = [port] - @staticmethod - def _get_cluster_args(name: str, port: int) -> t.List[str]: - """Create the arguments necessary for cluster creation""" - cluster_conf = "".join(("nodes-", name, "-", str(port), ".conf")) - db_args = ["--cluster-enabled yes", "--cluster-config-file", cluster_conf] - return db_args - def _get_start_script_args( self, name: str, port: int, cluster: bool ) -> t.List[str]: - start_script_args = [ + cmd = [ "-m", "smartsim._core.entrypoints.redis", # entrypoint - "+ifname=" + ",".join(self._interfaces), # pass interface to start script - "+command", # command flag for argparser - self._redis_exe, # redis-server - self._redis_conf, # redis.conf file - self._rai_module, # redisai.so - "--port", # redis port - str(port), # port number + f"+orc-exe={self._redis_exe}", # redis-server + f"+conf-file={self._redis_conf}", # redis.conf file + "+rai-module", # load redisai.so + *self._rai_module, + f"+name={name}", # name of node + f"+port={port}", # redis port + f"+ifname={','.join(self._interfaces)}", # pass interface to start script ] if cluster: - start_script_args += self._get_cluster_args(name, port) - - return start_script_args - - @property - def dbnodes(self) -> t.List[DBNode]: - """ - Helper property to cast self.entities to DBNode type for type correctness - """ - dbnodes = [node for node in self.entities if isinstance(node, DBNode)] - return dbnodes + cmd.append("+cluster") # is the shard part of a cluster + return cmd def _get_db_hosts(self) -> t.List[str]: hosts = [] - for db in self.dbnodes: + for db in self.entities: if not db.is_mpmd: hosts.append(db.host) else: diff --git a/smartsim/entity/__init__.py b/smartsim/entity/__init__.py index 3cc07f202..4ec28f2d4 100644 --- a/smartsim/entity/__init__.py +++ b/smartsim/entity/__init__.py @@ -28,6 +28,6 @@ from .dbobject import * from .ensemble import Ensemble from .entity import SmartSimEntity +from .entityList import EntityList, EntitySequence from .files import TaggedFilesHierarchy -from .entityList import EntityList from .model import Model diff --git a/smartsim/entity/dbnode.py b/smartsim/entity/dbnode.py index bc04df834..403984d16 100644 --- a/smartsim/entity/dbnode.py +++ b/smartsim/entity/dbnode.py @@ -24,16 +24,20 @@ # 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. +import fileinput +import itertools +import json import os import os.path as osp import time import typing as t +from dataclasses import dataclass +from .._core.config import CONFIG from ..error import SmartSimError from ..log import get_logger -from .entity import SmartSimEntity from ..settings.base import RunSettings - +from .entity import SmartSimEntity logger = get_logger(__name__) @@ -54,13 +58,11 @@ def __init__( run_settings: RunSettings, ports: t.List[int], output_files: t.List[str], + db_identifier: str = "", ) -> None: """Initialize a database node within an orchestrator.""" - self.ports = ports - self._host: t.Optional[str] = None super().__init__(name, path, run_settings) - self._mpmd = False - self._num_shards: int = 0 + self.ports = ports self._hosts: t.Optional[t.List[str]] = None if not output_files: @@ -70,20 +72,25 @@ def __init__( ): raise ValueError("output_files must be of type list[str]") self._output_files = output_files + self.db_identifier = db_identifier @property def num_shards(self) -> int: - return self._num_shards + if not hasattr(self.run_settings, "mpmd"): + # return default number of shards if mpmd is not set + return 1 - @num_shards.setter - def num_shards(self, value: int) -> None: - self._num_shards = value + return len(self.run_settings.mpmd) + 1 @property def host(self) -> str: - if not self._host: - self._host = self._parse_db_host() - return self._host + try: + (host,) = self.hosts + except ValueError: + raise ValueError( + f"Multiple hosts detected for this DB Node: {', '.join(self.hosts)}" + ) from None + return host @property def hosts(self) -> t.List[str]: @@ -91,16 +98,16 @@ def hosts(self) -> t.List[str]: self._hosts = self._parse_db_hosts() return self._hosts + def clear_hosts(self) -> None: + self._hosts = None + @property def is_mpmd(self) -> bool: - return self._mpmd - - @is_mpmd.setter - def is_mpmd(self, value: bool) -> None: - self._mpmd = value + if not hasattr(self.run_settings, "mpmd"): + # missing mpmd property guarantees this is not an mpmd run + return False - def set_host(self, host: str) -> None: - self._host = str(host) + return bool(self.run_settings.mpmd) def set_hosts(self, hosts: t.List[str]) -> None: self._hosts = [str(host) for host in hosts] @@ -112,42 +119,27 @@ def remove_stale_dbnode_files(self) -> None: """ for port in self.ports: - if not self._mpmd: - conf_file = osp.join(self.path, self._get_cluster_conf_filename(port)) + for conf_file in ( + osp.join(self.path, filename) + for filename in self._get_cluster_conf_filenames(port) + ): if osp.exists(conf_file): os.remove(conf_file) - else: # cov-lsf - conf_files = [ - osp.join(self.path, filename) - for filename in self._get_cluster_conf_filenames(port) - ] - for conf_file in conf_files: - if osp.exists(conf_file): - os.remove(conf_file) for file_ending in [".err", ".out", ".mpmd"]: file_name = osp.join(self.path, self.name + file_ending) if osp.exists(file_name): os.remove(file_name) - if self._mpmd: + + if self.is_mpmd: for file_ending in [".err", ".out"]: - for shard_id in range(self._num_shards): + for shard_id in range(self.num_shards): file_name = osp.join( self.path, self.name + "_" + str(shard_id) + file_ending ) if osp.exists(file_name): os.remove(file_name) - def _get_cluster_conf_filename(self, port: int) -> str: - """Returns the .conf file name for the given port number - - :param port: port number - :type port: int - :return: the dbnode configuration file name - :rtype: str - """ - return "".join(("nodes-", self.name, "-", str(port), ".conf")) - def _get_cluster_conf_filenames(self, port: int) -> t.List[str]: # cov-lsf """Returns the .conf file name for the given port number @@ -158,108 +150,98 @@ def _get_cluster_conf_filenames(self, port: int) -> t.List[str]: # cov-lsf :return: the dbnode configuration file name :rtype: str """ + if self.num_shards == 1: + return [f"nodes-{self.name}-{port}.conf"] return [ - "".join(("nodes-", self.name + f"_{shard_id}", "-", str(port), ".conf")) - for shard_id in range(self._num_shards) + f"nodes-{self.name}_{shard_id}-{port}.conf" + for shard_id in range(self.num_shards) ] @staticmethod - def _parse_ips(filepath: str, num_ips: t.Optional[int] = None) -> t.List[str]: - ips = [] - with open(filepath, "r", encoding="utf-8") as dbnode_file: - lines = dbnode_file.readlines() - for line in lines: - content = line.split() - if "IPADDRESS:" in content: - ips.append(content[-1]) - if num_ips and len(ips) == num_ips: - break - - return ips - - def _parse_db_host(self, filepath: t.Optional[str] = None) -> str: - """Parse the database host/IP from the output file - - If no file is passed as argument, then the first - file in self._output_files is used. - - :param filepath: Path to file to parse - :type filepath: str, optional - :raises SmartSimError: if host/ip could not be found - :return: ip address | hostname - :rtype: str + def _parse_launched_shard_info_from_iterable( + stream: t.Iterable[str], num_shards: t.Optional[int] = None + ) -> "t.List[LaunchedShardData]": + lines = (line.strip() for line in stream) + lines = (line for line in lines if line) + tokenized = (line.split(maxsplit=1) for line in lines) + tokenized = (tokens for tokens in tokenized if len(tokens) > 1) + shard_data_jsons = ( + kwjson for first, kwjson in tokenized if "SMARTSIM_ORC_SHARD_INFO" in first + ) + shard_data_kwargs = (json.loads(kwjson) for kwjson in shard_data_jsons) + shard_data: "t.Iterable[LaunchedShardData]" = ( + LaunchedShardData(**kwargs) for kwargs in shard_data_kwargs + ) + if num_shards: + shard_data = itertools.islice(shard_data, num_shards) + return list(shard_data) + + @classmethod + def _parse_launched_shard_info_from_files( + cls, file_paths: t.List[str], num_shards: t.Optional[int] = None + ) -> "t.List[LaunchedShardData]": + with fileinput.FileInput(file_paths) as ifstream: + return cls._parse_launched_shard_info_from_iterable(ifstream, num_shards) + + def get_launched_shard_info(self) -> "t.List[LaunchedShardData]": + """Parse the launched database shard info from the output files + + :raises SmartSimError: if all shard info could not be found + :return: The found launched shard info + :rtype: list[LaunchedShardData] """ - if not filepath: - filepath = osp.join(self.path, self._output_files[0]) - trials = 5 - ip_address = None - - # try a few times to give the database files time to - # populate on busy systems. - while not ip_address and trials > 0: + ips: "t.List[LaunchedShardData]" = [] + trials = CONFIG.database_file_parse_trials + interval = CONFIG.database_file_parse_interval + output_files = [osp.join(self.path, file) for file in self._output_files] + + while len(ips) < self.num_shards and trials > 0: try: - if ip_addresses := self._parse_ips(filepath, 1): - ip_address = ip_addresses[0] - # suppress error + ips = self._parse_launched_shard_info_from_files( + output_files, self.num_shards + ) except FileNotFoundError: - pass - - logger.debug("Waiting for Redis output files to populate...") - if not ip_address: - time.sleep(1) + ... + if len(ips) < self.num_shards: + logger.debug("Waiting for output files to populate...") + time.sleep(interval) trials -= 1 - if not ip_address: - logger.error(f"IP address lookup strategy failed for file {filepath}.") - raise SmartSimError("Failed to obtain database hostname") - - return ip_address + if len(ips) < self.num_shards: + msg = ( + f"Failed to parse the launched DB shard information from file(s) " + f"{', '.join(output_files)}. Found the information for " + f"{len(ips)} out of {self.num_shards} DB shards." + ) + logger.error(msg) + raise SmartSimError(msg) + return ips def _parse_db_hosts(self) -> t.List[str]: """Parse the database hosts/IPs from the output files - this uses the RedisIP module that is built as a dependency The IP address is preferred, but if hostname is only present then a lookup to /etc/hosts is done through the socket library. - This function must be called only if ``_mpmd==True``. :raises SmartSimError: if host/ip could not be found :return: ip addresses | hostnames :rtype: list[str] """ - ips: t.List[str] = [] - - # Find out if all shards' output streams are piped to separate files - if len(self._output_files) > 1: - for output_file in self._output_files: - filepath = osp.join(self.path, output_file) - _ = self._parse_db_host(filepath) - else: - filepath = osp.join(self.path, self._output_files[0]) - trials = 10 - ips = [] - while len(ips) < self._num_shards and trials > 0: - ips = [] - try: - ip_address = self._parse_ips(filepath, self._num_shards) - ips.extend(ip_address) - - # suppress error - except FileNotFoundError: - pass - - if len(ips) < self._num_shards: - logger.debug("Waiting for RedisIP files to populate...") - # Larger sleep time, as this seems to be needed for - # multihost setups - time.sleep(2) - trials -= 1 - - if len(ips) < self._num_shards: - msg = f"IP address lookup strategy failed for file {filepath}. " - msg += f"Found {len(ips)} out of {self._num_shards} IPs." - logger.error(msg) - raise SmartSimError("Failed to obtain database hostname") - - ips = list(dict.fromkeys(ips)) - return ips + return list({shard.hostname for shard in self.get_launched_shard_info()}) + + +@dataclass(frozen=True) +class LaunchedShardData: + """Data class to write and parse data about a launched database shard""" + + name: str + hostname: str + port: int + cluster: bool + + @property + def cluster_conf_file(self) -> t.Optional[str]: + return f"nodes-{self.name}-{self.port}.conf" if self.cluster else None + + def to_dict(self) -> t.Dict[str, t.Any]: + return dict(self.__dict__) diff --git a/smartsim/entity/dbobject.py b/smartsim/entity/dbobject.py index 7fe2051ea..bebedb12c 100644 --- a/smartsim/entity/dbobject.py +++ b/smartsim/entity/dbobject.py @@ -25,12 +25,11 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import typing as t - from pathlib import Path + from .._core.utils import init_default from ..error import SSUnsupportedError - __all__ = ["DBObject", "DBModel", "DBScript"] @@ -46,17 +45,19 @@ def __init__( file_path: t.Optional[str], device: t.Literal["CPU", "GPU"], devices_per_node: int, + first_device: int, ) -> None: self.name = name self.func = func - self.file: t.Optional[ - Path - ] = None # Need to have this explicitly to check on it + self.file: t.Optional[Path] = ( + None # Need to have this explicitly to check on it + ) if file_path: self.file = self._check_filepath(file_path) self.device = self._check_device(device) self.devices_per_node = devices_per_node - self._check_devices(device, devices_per_node) + self.first_device = first_device + self._check_devices(device, devices_per_node, first_device) @property def devices(self) -> t.List[str]: @@ -118,16 +119,30 @@ def _enumerate_devices(self) -> t.List[str]: if self.device == "GPU" and self.devices_per_node > 1: return [ - f"{self.device}:{str(device_num)}" - for device_num in range(self.devices_per_node) + f"{self.device}:{device_num}" + for device_num in range( + self.first_device, self.first_device + self.devices_per_node + ) ] return [self.device] @staticmethod def _check_devices( - device: t.Literal["CPU", "GPU"], devices_per_node: int + device: t.Literal["CPU", "GPU"], + devices_per_node: int, + first_device: int, ) -> None: + if device == "CPU" and devices_per_node > 1: + raise SSUnsupportedError( + "Cannot set devices_per_node>1 if CPU is specified under devices" + ) + + if device == "CPU" and first_device > 0: + raise SSUnsupportedError( + "Cannot set first_device>0 if CPU is specified under devices" + ) + if devices_per_node == 1: return @@ -136,10 +151,6 @@ def _check_devices( msg += f"the device was set to {device} and \ devices_per_node=={devices_per_node}" raise ValueError(msg) - if device == "CPU": - raise SSUnsupportedError( - "Cannot set devices_per_node>1 if CPU is specified under devices" - ) class DBScript(DBObject): @@ -150,6 +161,7 @@ def __init__( script_path: t.Optional[str] = None, device: t.Literal["CPU", "GPU"] = "CPU", devices_per_node: int = 1, + first_device: int = 0, ): """TorchScript code represenation @@ -157,7 +169,9 @@ def __init__( present, a number can be passed for specification e.g. "GPU:1". Setting ``devices_per_node=N``, with N greater than one will result - in the model being stored on the first N devices of type ``device``. + in the script being stored on the first N devices of type ``device``; + additionally setting ``first_device=M`` will instead result in the + script being stored on devices M through M + N -1. One of either script (in memory representation) or script_path (file) must be provided @@ -172,8 +186,12 @@ def __init__( :type device: str, optional :param devices_per_node: number of devices to store the script on :type devices_per_node: int + :param first_device: first devices to store the script on + :type first_device: int """ - super().__init__(name, script, script_path, device, devices_per_node) + super().__init__( + name, script, script_path, device, devices_per_node, first_device + ) if not script and not script_path: raise ValueError("Either script or script_path must be provided") @@ -191,6 +209,8 @@ def __str__(self) -> str: "s per node\n" if self.devices_per_node > 1 else " per node\n" ) desc_str += "Devices: " + str(self.devices_per_node) + " " + devices_str + if self.first_device > 0: + desc_str += "First device: " + str(self.first_device) + "\n" return desc_str @@ -203,6 +223,7 @@ def __init__( model_file: t.Optional[str] = None, device: t.Literal["CPU", "GPU"] = "CPU", devices_per_node: int = 1, + first_device: int = 0, batch_size: int = 0, min_batch_size: int = 0, min_batch_timeout: int = 0, @@ -227,6 +248,8 @@ def __init__( :type device: str, optional :param devices_per_node: number of devices to store the model on :type devices_per_node: int + :param first_device: The first device to store the model on + :type first_device: int :param batch_size: batch size for execution, defaults to 0 :type batch_size: int, optional :param min_batch_size: minimum batch size for model execution, defaults to 0 @@ -240,7 +263,9 @@ def __init__( :param outputs: model outupts (TF only), defaults to None :type outputs: list[str], optional """ - super().__init__(name, model, model_file, device, devices_per_node) + super().__init__( + name, model, model_file, device, devices_per_node, first_device + ) self.backend = self._check_backend(backend) if not model and not model_file: raise ValueError("Either model or model_file must be provided") @@ -264,6 +289,8 @@ def __str__(self) -> str: "s per node\n" if self.devices_per_node > 1 else " per node\n" ) desc_str += "Devices: " + str(self.devices_per_node) + " " + devices_str + if self.first_device > 0: + desc_str += "First_device: " + str(self.first_device) + "\n" desc_str += "Backend: " + str(self.backend) + "\n" if self.batch_size: desc_str += "Batch size: " + str(self.batch_size) + "\n" diff --git a/smartsim/entity/ensemble.py b/smartsim/entity/ensemble.py index 366be2053..28ada31de 100644 --- a/smartsim/entity/ensemble.py +++ b/smartsim/entity/ensemble.py @@ -25,7 +25,6 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import typing as t - from copy import deepcopy from os import getcwd @@ -41,9 +40,9 @@ from ..log import get_logger from ..settings.base import BatchSettings, RunSettings from .dbobject import DBModel, DBScript +from .entity import SmartSimEntity from .entityList import EntityList from .model import Model -from .entity import SmartSimEntity from .strategies import create_all_permutations, random_permutations, step_values logger = get_logger(__name__) @@ -53,7 +52,7 @@ ] -class Ensemble(EntityList): +class Ensemble(EntityList[Model]): """``Ensemble`` is a group of ``Model`` instances that can be treated as a reference to a single instance. """ @@ -362,8 +361,10 @@ def add_ml_model( model_path: t.Optional[str] = None, device: t.Literal["CPU", "GPU"] = "CPU", devices_per_node: int = 1, + first_device: int = 0, batch_size: int = 0, min_batch_size: int = 0, + min_batch_timeout: int = 0, tag: str = "", inputs: t.Optional[t.List[str]] = None, outputs: t.Optional[t.List[str]] = None, @@ -387,10 +388,18 @@ def add_ml_model( :type backend: str :param device: name of device for execution, defaults to "CPU" :type device: str, optional + :param devices_per_node: number of GPUs per node in multiGPU nodes, + defaults to 1 + :type devices_per_node: int, optional + :param first_device: first device in multi-GPU nodes to use for execution, + defaults to 0; ignored if devices_per_node is 1 + :type first_device: int, optional :param batch_size: batch size for execution, defaults to 0 :type batch_size: int, optional :param min_batch_size: minimum batch size for model execution, defaults to 0 :type min_batch_size: int, optional + :param min_batch_timeout: time to wait for minimum batch size, defaults to 0 + :type min_batch_timeout: int, optional :param tag: additional tag for model information, defaults to "" :type tag: str, optional :param inputs: model inputs (TF only), defaults to None @@ -405,8 +414,10 @@ def add_ml_model( model_file=model_path, device=device, devices_per_node=devices_per_node, + first_device=first_device, batch_size=batch_size, min_batch_size=min_batch_size, + min_batch_timeout=min_batch_timeout, tag=tag, inputs=inputs, outputs=outputs, @@ -422,6 +433,7 @@ def add_script( script_path: t.Optional[str] = None, device: t.Literal["CPU", "GPU"] = "CPU", devices_per_node: int = 1, + first_device: int = 0, ) -> None: """TorchScript to launch with every entity belonging to this ensemble @@ -448,6 +460,8 @@ def add_script( :type device: str, optional :param devices_per_node: number of devices on each host :type devices_per_node: int + :param first_device: first device to use on each host + :type first_device: int """ db_script = DBScript( name=name, @@ -455,6 +469,7 @@ def add_script( script_path=script_path, device=device, devices_per_node=devices_per_node, + first_device=first_device, ) self._db_scripts.append(db_script) for entity in self.models: @@ -466,6 +481,7 @@ def add_function( function: t.Optional[str] = None, device: t.Literal["CPU", "GPU"] = "CPU", devices_per_node: int = 1, + first_device: int = 0, ) -> None: """TorchScript function to launch with every entity belonging to this ensemble @@ -479,7 +495,9 @@ def add_function( present, a number can be passed for specification e.g. "GPU:1". Setting ``devices_per_node=N``, with N greater than one will result - in the model being stored in the first N devices of type ``device``. + in the script being stored in the first N devices of type ``device``; + alternatively, setting ``first_device=M`` will result in the script + being stored on nodes M through M + N - 1. :param name: key to store function under :type name: str @@ -489,9 +507,15 @@ def add_function( :type device: str, optional :param devices_per_node: number of devices on each host :type devices_per_node: int + :param first_device: first device to use on each host + :type first_device: int """ db_script = DBScript( - name=name, script=function, device=device, devices_per_node=devices_per_node + name=name, + script=function, + device=device, + devices_per_node=devices_per_node, + first_device=first_device, ) self._db_scripts.append(db_script) for entity in self.models: diff --git a/smartsim/entity/entity.py b/smartsim/entity/entity.py index fa46215cf..0d126c907 100644 --- a/smartsim/entity/entity.py +++ b/smartsim/entity/entity.py @@ -23,12 +23,17 @@ # 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. -import smartsim.settings.base + +import typing as t + +if t.TYPE_CHECKING: + # pylint: disable-next=unused-import + import smartsim.settings.base class SmartSimEntity: def __init__( - self, name: str, path: str, run_settings: smartsim.settings.base.RunSettings + self, name: str, path: str, run_settings: "smartsim.settings.base.RunSettings" ) -> None: """Initialize a SmartSim entity. diff --git a/smartsim/entity/entityList.py b/smartsim/entity/entityList.py index 1d3a991d4..4eaf3faa0 100644 --- a/smartsim/entity/entityList.py +++ b/smartsim/entity/entityList.py @@ -26,20 +26,53 @@ import typing as t +from .entity import SmartSimEntity + if t.TYPE_CHECKING: - # pylint: disable=unused-import + # pylint: disable-next=unused-import import smartsim +_T = t.TypeVar("_T", bound=SmartSimEntity) +# Old style pyint from TF 2.6.x does not know about pep484 style ``TypeVar`` names +# pylint: disable-next=invalid-name +_T_co = t.TypeVar("_T_co", bound=SmartSimEntity, covariant=True) + -class EntityList: +class EntitySequence(t.Generic[_T_co]): """Abstract class for containers for SmartSimEntities""" def __init__(self, name: str, path: str, **kwargs: t.Any) -> None: self.name: str = name self.path: str = path - self.entities: t.List["smartsim.entity.SmartSimEntity"] = [] - self._db_models: t.List["smartsim.entity.DBModel"] = [] - self._db_scripts: t.List["smartsim.entity.DBScript"] = [] + + # >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> + # WARNING: This class cannot be made truly covariant until the + # following properties are made read-only. It is currently + # designed for in-house type checking only!! + # + # Despite the fact that these properties are type hinted as + # ``Sequence``s, the underlying types must remain ``list``s as that is + # what subclasses are expecting when implementing their + # ``_initialize_entities`` methods. + # + # I'm leaving it "as is" for now as to not introduce a potential API + # break in case any users subclassed the invariant version of this + # class (``EntityList``), but a "proper" solution would be to turn + # ``EntitySequence``/``EntityList`` into proper ``abc.ABC``s and have + # the properties we expect to be initialized represented as abstract + # properties. An additional benefit of this solution is would be that + # users could actually initialize their entities in the ``__init__`` + # method, and it would remove the need for the cumbersome and + # un-type-hint-able ``_initialize_entities`` method by returning all + # object construction into the class' constructor. + # --------------------------------------------------------------------- + # + self.entities: t.Sequence[_T_co] = [] + self._db_models: t.Sequence["smartsim.entity.DBModel"] = [] + self._db_scripts: t.Sequence["smartsim.entity.DBScript"] = [] + # + # <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< + self._initialize_entities(**kwargs) def _initialize_entities(self, **kwargs: t.Any) -> None: @@ -79,15 +112,35 @@ def set_path(self, new_path: str) -> None: for entity in self.entities: entity.path = new_path - def __getitem__(self, name: str) -> t.Optional["smartsim.entity.SmartSimEntity"]: + def __getitem__(self, name: str) -> t.Optional[_T_co]: for entity in self.entities: if entity.name == name: return entity return None - def __iter__(self) -> t.Iterator["smartsim.entity.SmartSimEntity"]: + def __iter__(self) -> t.Iterator[_T_co]: for entity in self.entities: yield entity def __len__(self) -> int: return len(self.entities) + + +class EntityList(EntitySequence[_T]): + """An invariant subclass of an ``EntitySequence`` with mutable containers""" + + def __init__(self, name: str, path: str, **kwargs: t.Any) -> None: + super().__init__(name, path, **kwargs) + # Change container types to be invariant ``list``s + self.entities: t.List[_T] = list(self.entities) + self._db_models: t.List["smartsim.entity.DBModel"] = list(self._db_models) + self._db_scripts: t.List["smartsim.entity.DBScript"] = list(self._db_scripts) + + def _initialize_entities(self, **kwargs: t.Any) -> None: + """Initialize the SmartSimEntity objects in the container""" + # Need to identically re-define this "abstract method" or pylint + # complains that we are trying to define a concrete implementation of + # an abstract class despite the fact that we want this class to also be + # abstract. All the more reason to turn both of these classes into + # ``abc.ABC``s in my opinion. + raise NotImplementedError diff --git a/smartsim/entity/files.py b/smartsim/entity/files.py index 300452073..3aae9402b 100644 --- a/smartsim/entity/files.py +++ b/smartsim/entity/files.py @@ -25,8 +25,8 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import os import typing as t - from os import path + from tabulate import tabulate diff --git a/smartsim/entity/model.py b/smartsim/entity/model.py index a86be546b..6b97cbf2e 100644 --- a/smartsim/entity/model.py +++ b/smartsim/entity/model.py @@ -27,21 +27,23 @@ from __future__ import annotations import collections.abc +import re import sys import typing as t import warnings +from os import path as osp from .._core.utils.helpers import cat_arg_and_value, init_default from ..error import EntityExistsError, SSUnsupportedError +from ..log import get_logger +from ..settings.base import BatchSettings, RunSettings from .dbobject import DBModel, DBScript from .entity import SmartSimEntity from .files import EntityFiles -from ..settings.base import BatchSettings, RunSettings -from ..log import get_logger - logger = get_logger(__name__) + class Model(SmartSimEntity): def __init__( self, @@ -163,6 +165,19 @@ def attach_generator_files( to_copy = init_default([], to_copy, (list, str)) to_symlink = init_default([], to_symlink, (list, str)) to_configure = init_default([], to_configure, (list, str)) + + # Check that no file collides with the parameter file written + # by Generator. We check the basename, even though it is more + # restrictive than what we need (but it avoids relative path issues) + for strategy in [to_copy, to_symlink, to_configure]: + if strategy is not None and any( + osp.basename(filename) == "smartsim_params.txt" for filename in strategy + ): + raise ValueError( + "`smartsim_params.txt` is a file automatically " + + "generated by SmartSim and cannot be ovewritten." + ) + self.files = EntityFiles(to_configure, to_copy, to_symlink) @property @@ -177,8 +192,7 @@ def attached_files_table(self) -> str: return str(self.files) def print_attached_files(self) -> None: - """Print a table of the attached files on std out - """ + """Print a table of the attached files on std out""" print(self.attached_files_table) def colocate_db(self, *args: t.Any, **kwargs: t.Any) -> None: @@ -187,7 +201,8 @@ def colocate_db(self, *args: t.Any, **kwargs: t.Any) -> None: ( "`colocate_db` has been deprecated and will be removed in a \n" "future release. Please use `colocate_db_tcp` or `colocate_db_uds`." - ), FutureWarning + ), + FutureWarning, ) self.colocate_db_tcp(*args, **kwargs) @@ -198,6 +213,7 @@ def colocate_db_uds( db_cpus: int = 1, custom_pinning: t.Optional[t.Iterable[t.Union[int, t.Iterable[int]]]] = None, debug: bool = False, + db_identifier: str = "", **kwargs: t.Any, ) -> None: """Colocate an Orchestrator instance with this Model over UDS. @@ -237,6 +253,12 @@ def colocate_db_uds( :type kwargs: dict, optional """ + if not re.match(r"^[a-zA-Z0-9.:\,_\-/]*$", unix_socket): + raise ValueError( + f"Invalid name for unix socket: {unix_socket}. Must only " + "contain alphanumeric characters or . : _ - /" + ) + uds_options = { "unix_socket": unix_socket, "socket_permissions": socket_permissions, @@ -247,6 +269,7 @@ def colocate_db_uds( "cpus": db_cpus, "custom_pinning": custom_pinning, "debug": debug, + "db_identifier": db_identifier, } self._set_colocated_db_settings(uds_options, common_options, **kwargs) @@ -257,6 +280,7 @@ def colocate_db_tcp( db_cpus: int = 1, custom_pinning: t.Optional[t.Iterable[t.Union[int, t.Iterable[int]]]] = None, debug: bool = False, + db_identifier: str = "", **kwargs: t.Any, ) -> None: """Colocate an Orchestrator instance with this Model over TCP/IP. @@ -302,6 +326,7 @@ def colocate_db_tcp( "cpus": db_cpus, "custom_pinning": custom_pinning, "debug": debug, + "db_identifier": db_identifier, } self._set_colocated_db_settings(tcp_options, common_options, **kwargs) @@ -333,8 +358,7 @@ def _set_colocated_db_settings( # TODO list which db settings can be extras common_options["custom_pinning"] = self._create_pinning_string( - common_options["custom_pinning"], - common_options["cpus"] + common_options["custom_pinning"], common_options["cpus"] ) colo_db_config = {} @@ -358,13 +382,13 @@ def _set_colocated_db_settings( @staticmethod def _create_pinning_string( - pin_ids: t.Optional[t.Iterable[t.Union[int, t.Iterable[int]]]], - cpus: int - ) -> t.Optional[str]: + pin_ids: t.Optional[t.Iterable[t.Union[int, t.Iterable[int]]]], cpus: int + ) -> t.Optional[str]: """Create a comma-separated string CPU ids. By default, None returns 0,1,...,cpus-1; an empty iterable will disable pinning altogether, and an iterable constructs a comma separate string (e.g. 0,2,5) """ + def _stringify_id(_id: int) -> str: """Return the cPU id as a string if an int, otherwise raise a ValueError""" if isinstance(_id, int): @@ -389,14 +413,14 @@ def _stringify_id(_id: int) -> str: warnings.warn( "CPU pinning is not supported on MacOSX. Ignoring pinning " "specification.", - RuntimeWarning + RuntimeWarning, ) return None raise TypeError(_invalid_input_message) # Flatten the iterable into a list and check to make sure that the resulting # elements are all ints if pin_ids is None: - return ','.join(_stringify_id(i) for i in range(cpus)) + return ",".join(_stringify_id(i) for i in range(cpus)) if not pin_ids: return None if isinstance(pin_ids, collections.abc.Iterable): @@ -406,7 +430,7 @@ def _stringify_id(_id: int) -> str: pin_list.extend([_stringify_id(j) for j in pin_id]) else: pin_list.append(_stringify_id(pin_id)) - return ','.join(sorted(set(pin_list))) + return ",".join(sorted(set(pin_list))) raise TypeError(_invalid_input_message) def params_to_args(self) -> None: @@ -433,10 +457,12 @@ def add_ml_model( backend: str, model: t.Optional[str] = None, model_path: t.Optional[str] = None, - device: t.Literal["CPU","GPU"] = "CPU", + device: t.Literal["CPU", "GPU"] = "CPU", devices_per_node: int = 1, + first_device: int = 0, batch_size: int = 0, min_batch_size: int = 0, + min_batch_timeout: int = 0, tag: str = "", inputs: t.Optional[t.List[str]] = None, outputs: t.Optional[t.List[str]] = None, @@ -462,12 +488,18 @@ def add_ml_model( :type device: str, optional :param devices_per_node: The number of GPU devices available on the host. This parameter only applies to GPU devices and will be ignored if device - is specified as GPU. + is specified as CPU. :type devices_per_node: int + :param first_device: The first GPU device to use on the host. + This parameter only applies to GPU devices and will be ignored if device + is specified as CPU. + :type first_device: int :param batch_size: batch size for execution, defaults to 0 :type batch_size: int, optional :param min_batch_size: minimum batch size for model execution, defaults to 0 :type min_batch_size: int, optional + :param min_batch_timeout: time to wait for minimum batch size, defaults to 0 + :type min_batch_timeout: int, optional :param tag: additional tag for model information, defaults to "" :type tag: str, optional :param inputs: model inputs (TF only), defaults to None @@ -482,8 +514,10 @@ def add_ml_model( model_file=model_path, device=device, devices_per_node=devices_per_node, + first_device=first_device, batch_size=batch_size, min_batch_size=min_batch_size, + min_batch_timeout=min_batch_timeout, tag=tag, inputs=inputs, outputs=outputs, @@ -495,8 +529,9 @@ def add_script( name: str, script: t.Optional[str] = None, script_path: t.Optional[str] = None, - device: t.Literal["CPU","GPU"] = "CPU", + device: t.Literal["CPU", "GPU"] = "CPU", devices_per_node: int = 1, + first_device: int = 0, ) -> None: """TorchScript to launch with this Model instance @@ -508,7 +543,9 @@ def add_script( present, a number can be passed for specification e.g. "GPU:1". Setting ``devices_per_node=N``, with N greater than one will result - in the model being stored in the first N devices of type ``device``. + in the script being stored in the first N devices of type ``device``; + alternatively, setting ``first_device=M`` will result in the script + being stored on nodes M through M + N - 1. One of either script (in memory string representation) or script_path (file) must be provided @@ -523,8 +560,12 @@ def add_script( :type device: str, optional :param devices_per_node: The number of GPU devices available on the host. This parameter only applies to GPU devices and will be ignored if device - is specified as GPU. + is specified as CPU. :type devices_per_node: int + :param first_device: The first GPU device to use on the host. + This parameter only applies to GPU devices and will be ignored if device + is specified as CPU. + :type first_device: int """ db_script = DBScript( name=name, @@ -532,6 +573,7 @@ def add_script( script_path=script_path, device=device, devices_per_node=devices_per_node, + first_device=first_device, ) self.add_script_object(db_script) @@ -539,8 +581,9 @@ def add_function( self, name: str, function: t.Optional[str] = None, - device: t.Literal["CPU","GPU"] = "CPU", + device: t.Literal["CPU", "GPU"] = "CPU", devices_per_node: int = 1, + first_device: int = 0, ) -> None: """TorchScript function to launch with this Model instance @@ -564,11 +607,19 @@ def add_function( :type device: str, optional :param devices_per_node: The number of GPU devices available on the host. This parameter only applies to GPU devices and will be ignored if device - is specified as GPU. + is specified as CPU. :type devices_per_node: int + :param first_device: The first GPU device to use on the host. + This parameter only applies to GPU devices and will be ignored if device + is specified as CPU. + :type first_device: int """ db_script = DBScript( - name=name, script=function, device=device, devices_per_node=devices_per_node + name=name, + script=function, + device=device, + devices_per_node=devices_per_node, + first_device=first_device, ) self.add_script_object(db_script) diff --git a/smartsim/entity/strategies.py b/smartsim/entity/strategies.py index 7aec57765..e9db30c8f 100644 --- a/smartsim/entity/strategies.py +++ b/smartsim/entity/strategies.py @@ -27,7 +27,6 @@ # Generation Strategies import random import typing as t - from itertools import product diff --git a/smartsim/error/__init__.py b/smartsim/error/__init__.py index 383f82bb4..a04f5d91e 100644 --- a/smartsim/error/__init__.py +++ b/smartsim/error/__init__.py @@ -32,9 +32,9 @@ ShellError, SmartSimError, SSConfigError, + SSDBIDConflictError, SSInternalError, + SSReservedKeywordError, SSUnsupportedError, UserStrategyError, - SSReservedKeywordError, - ) diff --git a/smartsim/error/errors.py b/smartsim/error/errors.py index 95bc92420..d71ae3f71 100644 --- a/smartsim/error/errors.py +++ b/smartsim/error/errors.py @@ -39,12 +39,14 @@ class SSUnsupportedError(Exception): class EntityExistsError(SmartSimError): """Raised when a user tries to create an entity or files/directories for - an entity and either the entity/files/directories already exist""" + an entity and either the entity/files/directories already exist + """ class UserStrategyError(SmartSimError): """Raised when there is an error with model creation inside an ensemble - that is from a user provided permutation strategy""" + that is from a user provided permutation strategy + """ def __init__(self, perm_strat: str) -> None: message = self.create_message(perm_strat) @@ -79,13 +81,17 @@ class SSReservedKeywordError(SmartSimError): """Raised when a Reserved Keyword is used incorrectly""" +class SSDBIDConflictError(SmartSimError): + """Raised in the event that a database identifier + is not unique when multiple databases are created + """ + + # Internal Exceptions class SSInternalError(Exception): - """ - SSInternalError is raised when an internal error is encountered. - """ + """SSInternalError is raised when an internal error is encountered""" class SSConfigError(SSInternalError): @@ -102,7 +108,8 @@ class AllocationError(LauncherError): class ShellError(LauncherError): """Raised when error arises from function within launcher.shell - Closely related to error from subprocess(Popen) commands""" + Closely related to error from subprocess(Popen) commands + """ def __init__( self, @@ -126,3 +133,19 @@ def create_message( if details: msg += f"\nError from shell: {details}" return msg + + +class TelemetryError(SSInternalError): + """Raised when SmartSim runs into trouble establishing or communicating + telemetry information + """ + + +class UnproxyableStepError(TelemetryError): + """Raised when a user attempts to proxy a managed ``Step`` through the + unmanaged step proxy entry point + """ + + +class SmartSimCLIActionCancelled(SmartSimError): + """Raised when a `smart` CLI command is terminated""" diff --git a/smartsim/experiment.py b/smartsim/experiment.py index 639d0e7c9..9de33419a 100644 --- a/smartsim/experiment.py +++ b/smartsim/experiment.py @@ -24,6 +24,7 @@ # 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. +import os import os.path as osp import typing as t from os import getcwd @@ -36,7 +37,7 @@ from .entity import Ensemble, Model, SmartSimEntity from .error import SmartSimError from .log import get_logger -from .settings import settings, base, Container +from .settings import Container, base, settings from .wlm import detect_launcher logger = get_logger(__name__) @@ -62,7 +63,10 @@ class Experiment: """ def __init__( - self, name: str, exp_path: t.Optional[str] = None, launcher: str = "local" + self, + name: str, + exp_path: t.Optional[str] = None, + launcher: str = "local", ): """Initialize an Experiment instance @@ -126,6 +130,7 @@ def __init__( self._control = Controller(launcher=launcher) self._launcher = launcher.lower() + self.db_identifiers: t.Set[str] = set() def start( self, @@ -184,11 +189,14 @@ def start( :type kill_on_interrupt: bool, optional """ + start_manifest = Manifest(*args) try: if summary: self._launch_summary(start_manifest) self._control.start( + exp_name=self.name, + exp_path=self.exp_path, manifest=start_manifest, block=block, kill_on_interrupt=kill_on_interrupt, @@ -220,18 +228,25 @@ def stop(self, *args: t.Any) -> None: :raises TypeError: if wrong type :raises SmartSimError: if stop request fails """ + stop_manifest = Manifest(*args) try: - stop_manifest = Manifest(*args) for entity in stop_manifest.models: self._control.stop_entity(entity) - for entity_list in stop_manifest.all_entity_lists: + for entity_list in stop_manifest.ensembles: self._control.stop_entity_list(entity_list) + dbs = stop_manifest.dbs + for db in dbs: + self._control.stop_db(db) except SmartSimError as e: logger.error(e) raise def generate( - self, *args: t.Any, tag: t.Optional[str] = None, overwrite: bool = False + self, + *args: t.Any, + tag: t.Optional[str] = None, + overwrite: bool = False, + verbose: bool = False, ) -> None: """Generate the file structure for an ``Experiment`` @@ -251,9 +266,11 @@ def generate( :param overwrite: overwrite existing folders and contents, defaults to False :type overwrite: bool, optional + :param verbose: log parameter settings to std out + :type verbose: bool """ try: - generator = Generator(self.exp_path, overwrite=overwrite) + generator = Generator(self.exp_path, overwrite=overwrite, verbose=verbose) if tag: generator.set_tag(tag) generator.generate_experiment(*args) @@ -496,7 +513,7 @@ def create_model( "epoch": 10, "lr": 0.001 } - model = exp.create_model("pytorch_model", run_settings, params=params) + model = exp.create_model("pytorch_model", run_settings, params=train_params) model.attach_generator_files(to_configure="./train.cfg") exp.generate(model) @@ -682,13 +699,14 @@ def create_database( port: int = 6379, db_nodes: int = 1, batch: bool = False, - hosts: t.Optional[t.List[str]] = None, + hosts: t.Optional[t.Union[t.List[str], str]] = None, run_command: str = "auto", interface: str = "ipogif0", account: t.Optional[str] = None, time: t.Optional[str] = None, queue: t.Optional[str] = None, single_cmd: bool = True, + db_identifier: str = "orchestrator", **kwargs: t.Any, ) -> Orchestrator: """Initialize an Orchestrator database @@ -731,12 +749,18 @@ def create_database( :type queue: str, optional :param single_cmd: run all shards with one (MPMD) command, defaults to True :type single_cmd: bool, optional + :param db_identifier: an identifier to distinguish this orchestrator in + multiple-database experiments, defaults to "orchestrator" + :type db_identifier: str, optional :raises SmartSimError: if detection of launcher or of run command fails :raises SmartSimError: if user indicated an incompatible run command for the launcher :return: Orchestrator :rtype: Orchestrator or derived class """ + + self.append_to_db_identifier_list(db_identifier) + return Orchestrator( port=port, db_nodes=db_nodes, @@ -749,6 +773,7 @@ def create_database( queue=queue, single_cmd=single_cmd, launcher=self._launcher, + db_identifier=db_identifier, **kwargs, ) @@ -772,18 +797,17 @@ def reconnect_orchestrator(self, checkpoint: str) -> Orchestrator: logger.error(e) raise - # pylint: disable-next=redefined-builtin - def summary(self, format: str = "github") -> str: + def summary(self, style: str = "github") -> str: """Return a summary of the ``Experiment`` The summary will show each instance that has been launched and completed in this ``Experiment`` - :param format: the style in which the summary table is formatted, + :param style: the style in which the summary table is formatted, for a full list of styles see: https://github.com/astanin/python-tabulate#table-format, defaults to "github" - :type format: str, optional + :type style: str, optional :return: tabulate string of ``Experiment`` history :rtype: str """ @@ -814,7 +838,7 @@ def summary(self, format: str = "github") -> str: values, headers, showindex=True, - tablefmt=format, + tablefmt=style, missingval="None", disable_numparse=True, ) @@ -835,7 +859,7 @@ def _launch_summary(self, manifest: Manifest) -> None: if self._control.orchestrator_active: summary += "Database Status: active\n" - elif manifest.db: + elif manifest.dbs: summary += "Database Status: launching\n" else: summary += "Database Status: inactive\n" @@ -846,3 +870,46 @@ def _launch_summary(self, manifest: Manifest) -> None: def __str__(self) -> str: return self.name + + def append_to_db_identifier_list(self, db_identifier: str) -> None: + """Check if db_identifier already exists when calling create_database""" + if db_identifier in self.db_identifiers: + logger.warning( + f"A database with the identifier {db_identifier} has already been made " + "An error will be raised if multiple databases are started " + "with the same identifier" + ) + # Otherwise, add + self.db_identifiers.add(db_identifier) + + def enable_telemetry(self) -> None: + """Experiments will start producing telemetry for all entities run + through ``Experiment.start`` + + .. warning:: + + This method is currently implemented so that ALL ``Experiment`` + instances will begin producing telemetry data. In the future it + is planned to have this method work on a "per instance" basis! + """ + self._set_telemetry(True) + + def disable_telemetry(self) -> None: + """Experiments will stop producing telemetry for all entities run + through ``Experiment.start`` + + .. warning:: + + This method is currently implemented so that ALL ``Experiment`` + instances will stop producing telemetry data. In the future it + is planned to have this method work on a "per instance" basis! + """ + self._set_telemetry(False) + + @staticmethod + def _set_telemetry(switch: bool, /) -> None: + tm_key = "SMARTSIM_FLAG_TELEMETRY" + if switch: + os.environ[tm_key] = "1" + else: + os.environ[tm_key] = "0" diff --git a/smartsim/log.py b/smartsim/log.py index 9011b3d1b..baf54f068 100644 --- a/smartsim/log.py +++ b/smartsim/log.py @@ -23,19 +23,23 @@ # 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. -import typing as t import logging import os import sys +import typing as t import coloredlogs -# constants for logging -coloredlogs.DEFAULT_DATE_FORMAT = "%H:%M:%S" -coloredlogs.DEFAULT_LOG_FORMAT = ( +# constants +DEFAULT_DATE_FORMAT: t.Final[str] = "%H:%M:%S" +DEFAULT_LOG_FORMAT: t.Final[str] = ( "%(asctime)s %(hostname)s %(name)s[%(process)d] %(levelname)s %(message)s" ) +# configure colored loggs +coloredlogs.DEFAULT_DATE_FORMAT = DEFAULT_DATE_FORMAT +coloredlogs.DEFAULT_LOG_FORMAT = DEFAULT_LOG_FORMAT + def _get_log_level() -> str: """Get the logging level based on environment variable @@ -121,5 +125,7 @@ def log_to_file(filename: str, log_level: str = "debug") -> None: :type log_level: int | str """ logger = logging.getLogger("SmartSim") - stream = open(filename, "w+", encoding="utf-8") # pylint: disable=consider-using-with + stream = open( # pylint: disable=consider-using-with + filename, "w+", encoding="utf-8" + ) coloredlogs.install(stream=stream, logger=logger, level=log_level) diff --git a/smartsim/ml/data.py b/smartsim/ml/data.py index 8e617f914..3dfca9f0c 100644 --- a/smartsim/ml/data.py +++ b/smartsim/ml/data.py @@ -35,7 +35,6 @@ from ..error import SSInternalError from ..log import get_logger - logger = get_logger(__name__) @@ -180,7 +179,7 @@ def __init__( if not sample_name: raise ValueError("Sample name can not be empty") - self.client = Client(address=address, cluster=cluster) + self.client = Client(cluster, address=address) self.verbose = verbose self.batch_idx = 0 self.rank = rank @@ -323,7 +322,7 @@ def __init__( self._info = data_info_or_list_name elif isinstance(data_info_or_list_name, str): self._info = DataInfo(list_name=data_info_or_list_name) - client = Client(self.address, self.cluster) + client = Client(self.cluster, self.address) self._info.download(client) else: raise TypeError("data_info_or_list_name must be either DataInfo or str") @@ -410,7 +409,7 @@ def init_samples(self, init_trials: int = -1) -> None: :param init_trials: maximum number of attempts to fetch data :type init_trials: int """ - self._client = Client(self.address, self.cluster) + self._client = Client(self.cluster, self.address) num_trials = 0 max_trials = init_trials or -1 diff --git a/smartsim/ml/tf/data.py b/smartsim/ml/tf/data.py index 823553786..ae0b9aadd 100644 --- a/smartsim/ml/tf/data.py +++ b/smartsim/ml/tf/data.py @@ -24,8 +24,9 @@ # 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. -import numpy as np import typing as t + +import numpy as np from tensorflow import keras from smartsim.ml import DataDownloader diff --git a/smartsim/ml/tf/utils.py b/smartsim/ml/tf/utils.py index 7ef8fb3c6..c8018ac32 100644 --- a/smartsim/ml/tf/utils.py +++ b/smartsim/ml/tf/utils.py @@ -24,11 +24,11 @@ # 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. +import typing as t from pathlib import Path -import tensorflow as tf import keras -import typing as t +import tensorflow as tf from tensorflow.python.framework.convert_to_constants import ( convert_variables_to_constants_v2, ) diff --git a/smartsim/ml/torch/data.py b/smartsim/ml/torch/data.py index 69d054476..166a29e05 100644 --- a/smartsim/ml/torch/data.py +++ b/smartsim/ml/torch/data.py @@ -24,9 +24,10 @@ # 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. +import typing as t + import numpy as np import torch -import typing as t from smartredis import Client, Dataset from smartsim.ml.data import DataDownloader @@ -45,7 +46,7 @@ def __init__(self, **kwargs: t.Any) -> None: def _add_samples(self, indices: t.List[int]) -> None: if self.client is None: - client = Client(self.address, self.cluster) + client = Client(self.cluster, self.address) else: client = self.client diff --git a/smartsim/servertype.py b/smartsim/servertype.py new file mode 100644 index 000000000..a83149c23 --- /dev/null +++ b/smartsim/servertype.py @@ -0,0 +1,30 @@ +# BSD 2-Clause License +# +# Copyright (c) 2021-2023, 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. + + +# Server Types +CLUSTERED = "Clustered" +STANDALONE = "Standalone" diff --git a/smartsim/settings/alpsSettings.py b/smartsim/settings/alpsSettings.py index dc19dc475..b36c3d333 100644 --- a/smartsim/settings/alpsSettings.py +++ b/smartsim/settings/alpsSettings.py @@ -25,6 +25,7 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. from __future__ import annotations + import typing as t from ..error import SSUnsupportedError diff --git a/smartsim/settings/base.py b/smartsim/settings/base.py index b1e57ad37..a6df4eed4 100644 --- a/smartsim/settings/base.py +++ b/smartsim/settings/base.py @@ -34,9 +34,11 @@ logger = get_logger(__name__) - +# fmt: off class SettingsBase: ... +# fmt: on + # pylint: disable=too-many-public-methods class RunSettings(SettingsBase): diff --git a/smartsim/settings/cobaltSettings.py b/smartsim/settings/cobaltSettings.py index d0ad1a05f..5a0e07b40 100644 --- a/smartsim/settings/cobaltSettings.py +++ b/smartsim/settings/cobaltSettings.py @@ -25,6 +25,7 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import typing as t + from .base import BatchSettings diff --git a/smartsim/settings/lsfSettings.py b/smartsim/settings/lsfSettings.py index eeb0e4bf1..47fe91802 100644 --- a/smartsim/settings/lsfSettings.py +++ b/smartsim/settings/lsfSettings.py @@ -99,9 +99,7 @@ def set_cpus_per_rs(self, cpus_per_rs: int) -> None: if self.colocated_db_settings: db_cpus = int(self.colocated_db_settings.get("db_cpus", 0)) if not db_cpus: - raise ValueError( - "db_cpus must be configured on colocated_db_settings" - ) + raise ValueError("db_cpus must be configured on colocated_db_settings") if cpus_per_rs < db_cpus: raise ValueError( diff --git a/smartsim/settings/mpiSettings.py b/smartsim/settings/mpiSettings.py index 4a3a5afc4..5b6b520e3 100644 --- a/smartsim/settings/mpiSettings.py +++ b/smartsim/settings/mpiSettings.py @@ -81,6 +81,7 @@ def __init__( **kwargs, ) self.mpmd: t.List[RunSettings] = [] + self.affinity_script: t.List[str] = [] if not shutil.which(self._run_command): msg = ( diff --git a/smartsim/settings/mpirunSettings.py b/smartsim/settings/mpirunSettings.py index f061dc2a7..b290e2355 100644 --- a/smartsim/settings/mpirunSettings.py +++ b/smartsim/settings/mpirunSettings.py @@ -27,6 +27,7 @@ from warnings import simplefilter, warn from ..log import get_logger + # pylint: disable-next=unused-import from .mpiSettings import MpiexecSettings, MpirunSettings, OrterunSettings diff --git a/smartsim/settings/palsSettings.py b/smartsim/settings/palsSettings.py index bee381a84..bcfee1ff1 100644 --- a/smartsim/settings/palsSettings.py +++ b/smartsim/settings/palsSettings.py @@ -182,6 +182,16 @@ def set_walltime(self, walltime: str) -> None: """ logger.warning("set_walltime not supported under PALS") + def set_gpu_affinity_script(self, affinity: str, *args: t.Any) -> None: + """Set the GPU affinity through a bash script + + :param affinity: path to the affinity script + :type affinity: str + """ + self.affinity_script.append(str(affinity)) + for arg in args: + self.affinity_script.append(str(arg)) + def format_run_args(self) -> t.List[str]: """Return a list of MPI-standard formatted run arguments @@ -199,6 +209,10 @@ def format_run_args(self) -> t.List[str]: args += [prefix + opt] else: args += [prefix + opt, str(value)] + + if self.affinity_script: + args += self.affinity_script + return args def format_env_vars(self) -> t.List[str]: @@ -221,3 +235,20 @@ def format_env_vars(self) -> t.List[str]: formatted += ["--envlist", ",".join(export_vars)] return formatted + + def set_hostlist(self, host_list: t.Union[str, t.List[str]]) -> None: + """Set the hostlist for the PALS ``mpiexec`` command + + This sets ``--hosts`` + + :param host_list: list of host names + :type host_list: str | list[str] + :raises TypeError: if not str or list of str + """ + if isinstance(host_list, str): + host_list = [host_list.strip()] + if not isinstance(host_list, list): + raise TypeError("host_list argument must be a list of strings") + if not all(isinstance(host, str) for host in host_list): + raise TypeError("host_list argument must be list of strings") + self.run_args["hosts"] = ",".join(host_list) diff --git a/smartsim/settings/pbsSettings.py b/smartsim/settings/pbsSettings.py index e5e7f30e4..0a4b0868a 100644 --- a/smartsim/settings/pbsSettings.py +++ b/smartsim/settings/pbsSettings.py @@ -26,10 +26,12 @@ import typing as t -from .._core.utils import init_default -from ..error import SmartSimError +from ..error import SSConfigError +from ..log import get_logger from .base import BatchSettings +logger = get_logger(__name__) + class QsubBatchSettings(BatchSettings): def __init__( @@ -39,7 +41,7 @@ def __init__( time: t.Optional[str] = None, queue: t.Optional[str] = None, account: t.Optional[str] = None, - resources: t.Optional[t.Dict[str, t.Optional[str]]] = None, + resources: t.Optional[t.Dict[str, t.Union[str, int]]] = None, batch_args: t.Optional[t.Dict[str, t.Optional[str]]] = None, **kwargs: t.Any, ): @@ -66,10 +68,18 @@ def __init__( :param batch_args: overrides for PBS batch arguments, defaults to None :type batch_args: dict[str, str], optional """ - self._time: t.Optional[str] = None - self._nodes: t.Optional[int] = None + self._ncpus = ncpus + self.resources = resources or {} + resource_nodes = self.resources.get("nodes", None) + + if nodes and resource_nodes: + raise ValueError( + "nodes was incorrectly specified as a constructor parameter and also " + "as a key in the resource mapping" + ) + # time, queue, nodes, and account set in parent class init super().__init__( "qsub", @@ -80,20 +90,33 @@ def __init__( time=time, **kwargs, ) - self.resources = init_default({}, resources, dict) + self._hosts: t.List[str] = [] + @property + def resources(self) -> t.Dict[str, t.Union[str, int]]: + return self._resources.copy() + + @resources.setter + def resources(self, resources: t.Dict[str, t.Union[str, int]]) -> None: + self._sanity_check_resources(resources) + self._resources = resources.copy() + def set_nodes(self, num_nodes: int) -> None: """Set the number of nodes for this batch job - If a select argument is provided in ``QsubBatchSettings.resources`` - this value will be overridden + In PBS, 'select' is the more primitive way of describing how + many nodes to allocate for the job. 'nodes' is equivalent to + 'select' with a 'place' statement. Assuming that only advanced + users would use 'set_resource' instead, defining the number of + nodes here is sets the 'nodes' resource. :param num_nodes: number of nodes :type num_nodes: int """ + if num_nodes: - self._nodes = int(num_nodes) + self.set_resource("nodes", num_nodes) def set_hostlist(self, host_list: t.Union[str, t.List[str]]) -> None: """Specify the hostlist for this job @@ -123,7 +146,7 @@ def set_walltime(self, walltime: str) -> None: :type walltime: str """ if walltime: - self._time = walltime + self.set_resource("walltime", walltime) def set_queue(self, queue: str) -> None: """Set the queue for the batch job @@ -155,7 +178,7 @@ def set_account(self, account: str) -> None: if account: self.batch_args["A"] = str(account) - def set_resource(self, resource_name: str, value: str) -> None: + def set_resource(self, resource_name: str, value: t.Union[str, int]) -> None: """Set a resource value for the Qsub batch If a select statement is provided, the nodes and ncpus @@ -168,7 +191,10 @@ def set_resource(self, resource_name: str, value: str) -> None: """ # TODO add error checking here # TODO include option to overwrite place (warning for orchestrator?) - self.resources[resource_name] = value + updated_dict = self.resources + updated_dict.update({resource_name: value}) + self._sanity_check_resources(updated_dict) + self.resources = updated_dict def format_batch_args(self) -> t.List[str]: """Get the formatted batch arguments for a preview @@ -185,40 +211,69 @@ def format_batch_args(self) -> t.List[str]: opts += [" ".join((prefix + opt, str(value)))] return opts + def _sanity_check_resources( + self, resources: t.Optional[t.Dict[str, t.Union[str, int]]] = None + ) -> None: + """Check that only select or nodes was specified in resources + + Note: For PBS Pro, nodes is equivalent to 'select' and 'place' so + they are not quite synonyms. Here we assume that + """ + # Note: isinstance check here to avoid collision with default + checked_resources = resources if isinstance(resources, dict) else self.resources + + has_select = checked_resources.get("select", None) + has_nodes = checked_resources.get("nodes", None) + + if has_select and has_nodes: + raise SSConfigError( + "'select' and 'nodes' cannot both be specified. This can happen " + "if nodes were specified using the 'set_nodes' method and " + "'select' was set using 'set_resource'. Please only specify one." + ) + + if has_select and not isinstance(has_select, int): + raise TypeError("The value for 'select' must be an integer") + if has_nodes and not isinstance(has_nodes, int): + raise TypeError("The value for 'nodes' must be an integer") + + for key, value in checked_resources.items(): + if not isinstance(key, str): + raise TypeError( + f"The type of {key=} is {type(key)}. Only int and str " + "are allowed." + ) + if not isinstance(value, (str, int)): + raise TypeError( + f"The value associated with {key=} is {type(value)}. Only int " + "and str are allowed." + ) + def _create_resource_list(self) -> t.List[str]: + self._sanity_check_resources() res = [] - # get select statement from resources or kwargs - if "select" in self.resources: - res += [f"-l select={str(self.resources['select'])}"] - else: - select = "-l select=" - if self._nodes: - select += str(self._nodes) - else: - raise SmartSimError( - "Insufficient resource specification: no nodes or select statement" - ) - if self._ncpus: - select += f":ncpus={self._ncpus}" - if self._hosts: - hosts = ["=".join(("host", str(host))) for host in self._hosts] - select += f":{'+'.join(hosts)}" - res += [select] - - if "place" in self.resources: - res += [f"-l place={str(self.resources['place'])}"] - else: - res += ["-l place=scatter"] + # Pop off some specific keywords that need to be treated separately + resources = self.resources # Note this is a copy so not modifying original - # get time from resources or kwargs - if "walltime" in self.resources: - res += [f"-l walltime={str(self.resources['walltime'])}"] + # Construct the basic select/nodes statement + if select := resources.pop("select", None): + select_command = f"-l select={select}" + elif nodes := resources.pop("nodes", None): + select_command = f"-l nodes={nodes}" else: - if self._time: - res += [f"-l walltime={self._time}"] + raise SSConfigError( + "Insufficient resource specification: no nodes or select statement" + ) + if self._ncpus: + select_command += f":ncpus={self._ncpus}" + if self._hosts: + hosts = ["=".join(("host", str(host))) for host in self._hosts] + select_command += f":{'+'.join(hosts)}" + res += [select_command] + + # All other "standard" resource specs + for resource, value in resources.items(): + res += [f"-l {resource}={value}"] - for resource, value in self.resources.items(): - if resource not in ["select", "walltime", "place"]: - res += [f"-l {resource}={str(value)}"] return res diff --git a/smartsim/settings/settings.py b/smartsim/settings/settings.py index c3ee98593..b09286e8c 100644 --- a/smartsim/settings/settings.py +++ b/smartsim/settings/settings.py @@ -28,23 +28,23 @@ from .._core.utils.helpers import is_valid_cmd from ..error import SmartSimError -from ..wlm import detect_launcher from ..settings import ( - base, - CobaltBatchSettings, - QsubBatchSettings, - SbatchSettings, + AprunSettings, BsubBatchSettings, + CobaltBatchSettings, Container, - RunSettings, - AprunSettings, - SrunSettings, - MpirunSettings, + JsrunSettings, MpiexecSettings, + MpirunSettings, OrterunSettings, - JsrunSettings, PalsMpiexecSettings, + QsubBatchSettings, + RunSettings, + SbatchSettings, + SrunSettings, + base, ) +from ..wlm import detect_launcher _TRunSettingsSelector = t.Callable[[str], t.Callable[..., RunSettings]] diff --git a/smartsim/settings/slurmSettings.py b/smartsim/settings/slurmSettings.py index 60280fce9..8da8659e1 100644 --- a/smartsim/settings/slurmSettings.py +++ b/smartsim/settings/slurmSettings.py @@ -336,7 +336,7 @@ def check_env_vars(self) -> None: # If a variable is defined, it will take precedence over --export # we warn the user preexisting_var = os.environ.get(k, None) - if preexisting_var is not None: + if preexisting_var is not None and preexisting_var != v: msg = ( f"Variable {k} is set to {preexisting_var} in current " "environment. If the job is running in an interactive " diff --git a/smartsim/status.py b/smartsim/status.py index 4d1749e71..74d440b8e 100644 --- a/smartsim/status.py +++ b/smartsim/status.py @@ -32,6 +32,7 @@ STATUS_FAILED = "Failed" STATUS_NEW = "New" STATUS_PAUSED = "Paused" +STATUS_NEVER_STARTED = "NeverStarted" # SmartSim status mapping SMARTSIM_STATUS = { @@ -41,6 +42,7 @@ "Cancelled": STATUS_CANCELLED, "Failed": STATUS_FAILED, "New": STATUS_NEW, + "NeverStarted": STATUS_NEVER_STARTED, } # Status groupings diff --git a/smartsim/wlm/__init__.py b/smartsim/wlm/__init__.py index 8f3b1b097..d7dd298be 100644 --- a/smartsim/wlm/__init__.py +++ b/smartsim/wlm/__init__.py @@ -25,9 +25,9 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import os +import typing as t from shutil import which from subprocess import run -import typing as t from ..error import SSUnsupportedError from . import pbs as _pbs diff --git a/smartsim/wlm/slurm.py b/smartsim/wlm/slurm.py index 8fe12b3f9..ba46fb64c 100644 --- a/smartsim/wlm/slurm.py +++ b/smartsim/wlm/slurm.py @@ -237,7 +237,8 @@ def _get_alloc_cmd( options: t.Optional[t.Dict[str, str]] = None, ) -> t.List[str]: """Return the command to request an allocation from Slurm with - the class variables as the slurm options.""" + the class variables as the slurm options. + """ salloc_args = [ "--no-shell", diff --git a/tests/backends/run_sklearn_onnx.py b/tests/backends/run_sklearn_onnx.py index a2aa2aaca..d4377bbde 100644 --- a/tests/backends/run_sklearn_onnx.py +++ b/tests/backends/run_sklearn_onnx.py @@ -45,7 +45,6 @@ def build_lin_reg(): def build_kmeans(): - X = np.arange(20, dtype=np.float32).reshape(10, 2) tr = KMeans(n_clusters=2) tr.fit(X) @@ -76,7 +75,6 @@ def run_model(client, model_name, device, model, model_input, in_name, out_names def run(device): - # connect a client to the database client = Client(cluster=False) diff --git a/tests/backends/run_tf.py b/tests/backends/run_tf.py index f32dacb99..c9cf0ee04 100644 --- a/tests/backends/run_tf.py +++ b/tests/backends/run_tf.py @@ -34,7 +34,6 @@ def create_tf_mnist_model(): - model = keras.Sequential( layers=[ keras.layers.InputLayer(input_shape=(28, 28), name="input"), @@ -53,7 +52,6 @@ def create_tf_mnist_model(): def run(device): - model = create_tf_mnist_model() client = Client(cluster=False) diff --git a/tests/backends/run_torch.py b/tests/backends/run_torch.py index 2ea808189..d57cfad9d 100644 --- a/tests/backends/run_torch.py +++ b/tests/backends/run_torch.py @@ -75,7 +75,6 @@ def calc_svd(input_tensor): def run(device): - # connect a client to the database client = Client(cluster=False) diff --git a/tests/backends/test_cli_mini_exp.py b/tests/backends/test_cli_mini_exp.py index b3897c73b..7c793e915 100644 --- a/tests/backends/test_cli_mini_exp.py +++ b/tests/backends/test_cli_mini_exp.py @@ -27,15 +27,28 @@ from contextlib import contextmanager +import pytest import smartredis import smartsim._core._cli.validate from smartsim._core.utils.helpers import installed_redisai_backends +sklearn_available = True +try: + from skl2onnx import to_onnx + from sklearn.cluster import KMeans + from sklearn.datasets import load_iris + from sklearn.ensemble import RandomForestRegressor + from sklearn.linear_model import LinearRegression + from sklearn.model_selection import train_test_split + +except ImportError: + sklearn_available = False + def test_cli_mini_exp_doesnt_error_out_with_dev_build( local_db, - fileutils, + test_dir, monkeypatch, ): """Presumably devs running the test suite have built SS correctly. @@ -45,8 +58,8 @@ def test_cli_mini_exp_doesnt_error_out_with_dev_build( @contextmanager def _mock_make_managed_local_orc(*a, **kw): - client_addr ,= local_db.get_address() - yield smartredis.Client(address=client_addr, cluster=False) + (client_addr,) = local_db.get_address() + yield smartredis.Client(False, address=client_addr) monkeypatch.setattr( smartsim._core._cli.validate, @@ -54,17 +67,17 @@ def _mock_make_managed_local_orc(*a, **kw): _mock_make_managed_local_orc, ) backends = installed_redisai_backends() - db_port ,= local_db.ports + (db_port,) = local_db.ports smartsim._core._cli.validate.test_install( # Shouldn't matter bc we are stubbing creation of orc # but best to give it "correct" vals for safety - location=fileutils.get_test_dir(), + location=test_dir, port=db_port, # Always test on CPU, heads don't always have GPU device="CPU", # Test the backends the dev has installed with_tf="tensorflow" in backends, with_pt="torch" in backends, - with_onnx="onnxruntime" in backends, + with_onnx="onnxruntime" in backends and sklearn_available, ) diff --git a/tests/backends/test_dataloader.py b/tests/backends/test_dataloader.py index 5dd1fe4ed..37c4296ef 100644 --- a/tests/backends/test_dataloader.py +++ b/tests/backends/test_dataloader.py @@ -33,18 +33,32 @@ from smartsim.database import Orchestrator from smartsim.error.errors import SSInternalError from smartsim.experiment import Experiment +from smartsim.log import get_logger from smartsim.ml.data import DataInfo, TrainingDataUploader from smartsim.status import STATUS_COMPLETED +logger = get_logger(__name__) + shouldrun_tf = True if shouldrun_tf: try: + import tensorflow as tf from tensorflow import keras from smartsim.ml.tf import DynamicDataGenerator as TFDataGenerator from smartsim.ml.tf import StaticDataGenerator as TFStaticDataGenerator except: shouldrun_tf = False + else: + if pytest.test_device == "GPU": + try: + for device in tf.config.list_physical_devices("GPU"): + tf.config.set_logical_device_configuration( + device, + [tf.config.LogicalDeviceConfiguration(memory_limit=5_000)], + ) + except Exception: + logger.warning("Could not set TF max memory limit for GPU") shouldrun_torch = True if shouldrun_torch: @@ -52,10 +66,8 @@ import torch from smartsim.ml.torch import DataLoader - from smartsim.ml.torch import \ - DynamicDataGenerator as TorchDataGenerator - from smartsim.ml.torch import \ - StaticDataGenerator as TorchStaticDataGenerator + from smartsim.ml.torch import DynamicDataGenerator as TorchDataGenerator + from smartsim.ml.torch import StaticDataGenerator as TorchStaticDataGenerator except: shouldrun_torch = False @@ -155,9 +167,10 @@ def train_tf(generator): @pytest.mark.skipif(not shouldrun_tf, reason="Test needs TensorFlow to run") -def test_tf_dataloaders(fileutils, wlmutils): - test_dir = fileutils.make_test_dir() - exp = Experiment("test_tf_dataloaders", test_dir, launcher=wlmutils.get_test_launcher()) +def test_tf_dataloaders(test_dir, wlmutils): + exp = Experiment( + "test_tf_dataloaders", test_dir, launcher=wlmutils.get_test_launcher() + ) orc: Orchestrator = wlmutils.get_orchestrator() exp.generate(orc) exp.start(orc) @@ -221,9 +234,10 @@ def create_trainer_torch(experiment: Experiment, filedir, wlmutils): @pytest.mark.skipif(not shouldrun_torch, reason="Test needs Torch to run") -def test_torch_dataloaders(fileutils, wlmutils): - test_dir = fileutils.make_test_dir() - exp = Experiment("test_tf_dataloaders", test_dir, launcher=wlmutils.get_test_launcher()) +def test_torch_dataloaders(fileutils, test_dir, wlmutils): + exp = Experiment( + "test_tf_dataloaders", test_dir, launcher=wlmutils.get_test_launcher() + ) orc: Orchestrator = wlmutils.get_orchestrator() config_dir = fileutils.get_test_dir_path("ml") exp.generate(orc) @@ -271,10 +285,10 @@ def test_torch_dataloaders(fileutils, wlmutils): for _ in range(2): for _ in torch_static: continue - + trainer = create_trainer_torch(exp, config_dir, wlmutils) exp.start(trainer, block=True) - + assert exp.get_status(trainer)[0] == STATUS_COMPLETED except Exception as e: @@ -317,9 +331,12 @@ def test_data_info_repr(): @pytest.mark.skipif( not (shouldrun_torch or shouldrun_tf), reason="Requires TF or PyTorch" ) -def test_wrong_dataloaders(fileutils, wlmutils): - test_dir = fileutils.make_test_dir() - exp = Experiment("test-wrong-dataloaders", exp_path=test_dir, launcher=wlmutils.get_test_launcher()) +def test_wrong_dataloaders(test_dir, wlmutils): + exp = Experiment( + "test-wrong-dataloaders", + exp_path=test_dir, + launcher=wlmutils.get_test_launcher(), + ) orc = wlmutils.get_orchestrator() exp.generate(orc) exp.start(orc) diff --git a/tests/backends/test_dbmodel.py b/tests/backends/test_dbmodel.py index 83a2e119b..1cfc1efcb 100644 --- a/tests/backends/test_dbmodel.py +++ b/tests/backends/test_dbmodel.py @@ -26,18 +26,16 @@ import sys -import time import pytest -import smartsim from smartsim import Experiment, status from smartsim._core.utils import installed_redisai_backends +from smartsim.entity import Ensemble +from smartsim.entity.dbobject import DBModel from smartsim.error.errors import SSUnsupportedError from smartsim.log import get_logger -from smartsim.entity.dbobject import DBModel - logger = get_logger(__name__) should_run_tf = True @@ -45,8 +43,10 @@ # Check TensorFlow is available for tests try: - import tensorflow.keras as keras + import tensorflow as tf + from tensorflow import keras from tensorflow.keras.layers import Conv2D, Input + except ImportError: should_run_tf = False else: @@ -60,6 +60,14 @@ def call(self, x): y = self.conv(x) return y + if pytest.test_device == "GPU": + try: + for device in tf.config.list_physical_devices("GPU"): + tf.config.set_logical_device_configuration( + device, [tf.config.LogicalDeviceConfiguration(memory_limit=5_000)] + ) + except: + logger.warning("Could not set TF max memory limit for GPU") should_run_tf &= "tensorflow" in installed_redisai_backends() @@ -136,7 +144,7 @@ def save_torch_cnn(path, file_name): @pytest.mark.skipif(not should_run_tf, reason="Test needs TF to run") -def test_tf_db_model(fileutils, wlmutils, mlutils): +def test_tf_db_model(fileutils, test_dir, wlmutils, mlutils): """Test TensorFlow DB Models on remote DB""" # Set experiment name @@ -147,8 +155,8 @@ def test_tf_db_model(fileutils, wlmutils, mlutils): test_interface = wlmutils.get_test_interface() test_port = wlmutils.get_test_port() test_device = mlutils.get_test_device() - test_num_gpus = mlutils.get_test_num_gpus() - test_dir = fileutils.make_test_dir() + test_num_gpus = 1 # TF backend fails on multiple GPUs + test_script = fileutils.get_test_conf_path("run_tf_dbmodel_smartredis.py") # Create the SmartSim Experiment @@ -157,14 +165,14 @@ def test_tf_db_model(fileutils, wlmutils, mlutils): # Create RunSettings run_settings = exp.create_run_settings(exe=sys.executable, exe_args=test_script) run_settings.set_nodes(1) - run_settings.set_tasks_per_node(1) + run_settings.set_tasks(1) # Create Model smartsim_model = exp.create_model("smartsim_model", run_settings) - smartsim_model.set_path(test_dir) # Create database - db = exp.create_database(port=test_port, interface=test_interface) + host = wlmutils.choose_host(run_settings) + db = exp.create_database(port=test_port, interface=test_interface, hosts=host) exp.generate(db) # Create and save ML model to filesystem @@ -178,6 +186,7 @@ def test_tf_db_model(fileutils, wlmutils, mlutils): model=model, device=test_device, devices_per_node=test_num_gpus, + first_device=0, inputs=inputs, outputs=outputs, tag="test", @@ -188,6 +197,7 @@ def test_tf_db_model(fileutils, wlmutils, mlutils): model_path=model_file2, device=test_device, devices_per_node=test_num_gpus, + first_device=0, inputs=inputs2, outputs=outputs2, tag="test", @@ -200,17 +210,21 @@ def test_tf_db_model(fileutils, wlmutils, mlutils): # Assert we have added both models assert len(smartsim_model._db_models) == 2 + exp.generate(smartsim_model) + # Launch and check successful completion try: exp.start(db, smartsim_model, block=True) statuses = exp.get_status(smartsim_model) - assert all([stat == status.STATUS_COMPLETED for stat in statuses]) + assert all( + stat == status.STATUS_COMPLETED for stat in statuses + ), f"Statuses: {statuses}" finally: exp.stop(db) @pytest.mark.skipif(not should_run_pt, reason="Test needs PyTorch to run") -def test_pt_db_model(fileutils, wlmutils, mlutils): +def test_pt_db_model(fileutils, test_dir, wlmutils, mlutils): """Test PyTorch DB Models on remote DB""" # Set experiment name @@ -221,8 +235,8 @@ def test_pt_db_model(fileutils, wlmutils, mlutils): test_interface = wlmutils.get_test_interface() test_port = wlmutils.get_test_port() test_device = mlutils.get_test_device() - test_num_gpus = mlutils.get_test_num_gpus() - test_dir = fileutils.make_test_dir() + test_num_gpus = mlutils.get_test_num_gpus() if pytest.test_device == "GPU" else 1 + test_script = fileutils.get_test_conf_path("run_pt_dbmodel_smartredis.py") # Create the SmartSim Experiment @@ -231,14 +245,14 @@ def test_pt_db_model(fileutils, wlmutils, mlutils): # Create RunSettings run_settings = exp.create_run_settings(exe=sys.executable, exe_args=test_script) run_settings.set_nodes(1) - run_settings.set_tasks_per_node(1) + run_settings.set_tasks(1) # Create Model smartsim_model = exp.create_model("smartsim_model", run_settings) - smartsim_model.set_path(test_dir) # Create database - db = exp.create_database(port=test_port, interface=test_interface) + host = wlmutils.choose_host(run_settings) + db = exp.create_database(port=test_port, interface=test_interface, hosts=host) exp.generate(db) # Create and save ML model to filesystem @@ -252,6 +266,7 @@ def test_pt_db_model(fileutils, wlmutils, mlutils): model_path=model_path, device=test_device, devices_per_node=test_num_gpus, + first_device=0, tag="test", ) @@ -259,21 +274,24 @@ def test_pt_db_model(fileutils, wlmutils, mlutils): for db_model in smartsim_model._db_models: logger.debug(db_model) - # Assert we have added both models assert len(smartsim_model._db_models) == 1 + exp.generate(smartsim_model) + # Launch and check successful completion try: exp.start(db, smartsim_model, block=True) statuses = exp.get_status(smartsim_model) - assert all([stat == status.STATUS_COMPLETED for stat in statuses]) + assert all( + stat == status.STATUS_COMPLETED for stat in statuses + ), f"Statuses: {statuses}" finally: exp.stop(db) @pytest.mark.skipif(not should_run_tf, reason="Test needs TF to run") -def test_db_model_ensemble(fileutils, wlmutils, mlutils): +def test_db_model_ensemble(fileutils, test_dir, wlmutils, mlutils): """Test DBModels on remote DB, with an ensemble""" # Set experiment name @@ -284,8 +302,8 @@ def test_db_model_ensemble(fileutils, wlmutils, mlutils): test_interface = wlmutils.get_test_interface() test_port = wlmutils.get_test_port() test_device = mlutils.get_test_device() - test_num_gpus = mlutils.get_test_num_gpus() - test_dir = fileutils.make_test_dir() + test_num_gpus = 1 # TF backend fails on multiple GPUs + test_script = fileutils.get_test_conf_path("run_tf_dbmodel_smartredis.py") # Create the SmartSim Experiment @@ -294,20 +312,19 @@ def test_db_model_ensemble(fileutils, wlmutils, mlutils): # Create RunSettings run_settings = exp.create_run_settings(exe=sys.executable, exe_args=test_script) run_settings.set_nodes(1) - run_settings.set_tasks_per_node(1) + run_settings.set_tasks(1) # Create ensemble smartsim_ensemble = exp.create_ensemble( "smartsim_model", run_settings=run_settings, replicas=2 ) - smartsim_ensemble.set_path(test_dir) # Create Model smartsim_model = exp.create_model("smartsim_model", run_settings) - smartsim_model.set_path(test_dir) # Create database - db = exp.create_database(port=test_port, interface=test_interface) + host = wlmutils.choose_host(run_settings) + db = exp.create_database(port=test_port, interface=test_interface, hosts=host) exp.generate(db) # Create and save ML model to filesystem @@ -321,8 +338,9 @@ def test_db_model_ensemble(fileutils, wlmutils, mlutils): model=model, device=test_device, devices_per_node=test_num_gpus, + first_device=0, inputs=inputs, - outputs=outputs + outputs=outputs, ) # Add the second ML model individually to each SmartSim model @@ -334,6 +352,7 @@ def test_db_model_ensemble(fileutils, wlmutils, mlutils): model_path=model_file2, device=test_device, devices_per_node=test_num_gpus, + first_device=0, inputs=inputs2, outputs=outputs2, ) @@ -342,13 +361,14 @@ def test_db_model_ensemble(fileutils, wlmutils, mlutils): smartsim_ensemble.add_model(smartsim_model) # Add the second ML model to the newly added entity. This is - # because the test script run both ML models for all entities. + # because the test script runs both ML models for all entities. smartsim_model.add_ml_model( "cnn2", "TF", model_path=model_file2, device=test_device, devices_per_node=test_num_gpus, + first_device=0, inputs=inputs2, outputs=outputs2, ) @@ -358,17 +378,21 @@ def test_db_model_ensemble(fileutils, wlmutils, mlutils): # Assert we have added two models to each entity assert all([len(entity._db_models) == 2 for entity in smartsim_ensemble]) + exp.generate(smartsim_ensemble) + # Launch and check successful completion try: exp.start(db, smartsim_ensemble, block=True) statuses = exp.get_status(smartsim_ensemble) - assert all([stat == status.STATUS_COMPLETED for stat in statuses]) + assert all( + stat == status.STATUS_COMPLETED for stat in statuses + ), f"Statuses: {statuses}" finally: exp.stop(db) @pytest.mark.skipif(not should_run_tf, reason="Test needs TF to run") -def test_colocated_db_model_tf(fileutils, wlmutils, mlutils): +def test_colocated_db_model_tf(fileutils, test_dir, wlmutils, mlutils): """Test DB Models on colocated DB (TensorFlow backend)""" # Set experiment name @@ -379,26 +403,22 @@ def test_colocated_db_model_tf(fileutils, wlmutils, mlutils): test_interface = wlmutils.get_test_interface() test_port = wlmutils.get_test_port() test_device = mlutils.get_test_device() - test_num_gpus = mlutils.get_test_num_gpus() - test_dir = fileutils.make_test_dir() + test_num_gpus = 1 # TF backend fails on multiple GPUs + test_script = fileutils.get_test_conf_path("run_tf_dbmodel_smartredis.py") # Create SmartSim Experience - exp = Experiment(exp_name, launcher=test_launcher) + exp = Experiment(exp_name, launcher=test_launcher, exp_path=test_dir) # Create RunSettings colo_settings = exp.create_run_settings(exe=sys.executable, exe_args=test_script) colo_settings.set_nodes(1) - colo_settings.set_tasks_per_node(1) + colo_settings.set_tasks(1) # Create colocated Model colo_model = exp.create_model("colocated_model", colo_settings) - colo_model.set_path(test_dir) colo_model.colocate_db_tcp( - port=test_port, - db_cpus=1, - debug=True, - ifname=test_interface + port=test_port, db_cpus=1, debug=True, ifname=test_interface ) # Create and save ML model to filesystem @@ -412,8 +432,9 @@ def test_colocated_db_model_tf(fileutils, wlmutils, mlutils): model_path=model_file, device=test_device, devices_per_node=test_num_gpus, + first_device=0, inputs=inputs, - outputs=outputs + outputs=outputs, ) colo_model.add_ml_model( "cnn2", @@ -421,6 +442,7 @@ def test_colocated_db_model_tf(fileutils, wlmutils, mlutils): model_path=model_file2, device=test_device, devices_per_node=test_num_gpus, + first_device=0, inputs=inputs2, outputs=outputs2, ) @@ -428,16 +450,21 @@ def test_colocated_db_model_tf(fileutils, wlmutils, mlutils): # Assert we have added both models assert len(colo_model._db_models) == 2 + exp.generate(colo_model) + # Launch and check successful completion try: exp.start(colo_model, block=True) statuses = exp.get_status(colo_model) - assert all([stat == status.STATUS_COMPLETED for stat in statuses]) + assert all( + stat == status.STATUS_COMPLETED for stat in statuses + ), f"Statuses: {statuses}" finally: exp.stop(colo_model) + @pytest.mark.skipif(not should_run_pt, reason="Test needs PyTorch to run") -def test_colocated_db_model_pytorch(fileutils, wlmutils, mlutils): +def test_colocated_db_model_pytorch(fileutils, test_dir, wlmutils, mlutils): """Test DB Models on colocated DB (PyTorch backend)""" # Set experiment name @@ -448,26 +475,22 @@ def test_colocated_db_model_pytorch(fileutils, wlmutils, mlutils): test_interface = wlmutils.get_test_interface() test_port = wlmutils.get_test_port() test_device = mlutils.get_test_device() - test_num_gpus = mlutils.get_test_num_gpus() - test_dir = fileutils.make_test_dir() + test_num_gpus = mlutils.get_test_num_gpus() if pytest.test_device == "GPU" else 1 + test_script = fileutils.get_test_conf_path("run_pt_dbmodel_smartredis.py") # Create the SmartSim Experiment - exp = Experiment(exp_name, launcher=test_launcher) + exp = Experiment(exp_name, launcher=test_launcher, exp_path=test_dir) # Create colocated RunSettings colo_settings = exp.create_run_settings(exe=sys.executable, exe_args=test_script) colo_settings.set_nodes(1) - colo_settings.set_tasks_per_node(1) + colo_settings.set_tasks(1) # Create colocated SmartSim Model colo_model = exp.create_model("colocated_model", colo_settings) - colo_model.set_path(test_dir) colo_model.colocate_db_tcp( - port=test_port, - db_cpus=1, - debug=True, - ifname=test_interface + port=test_port, db_cpus=1, debug=True, ifname=test_interface ) # Create and save ML model to filesystem @@ -475,26 +498,33 @@ def test_colocated_db_model_pytorch(fileutils, wlmutils, mlutils): model_file = test_dir + "/model1.pt" # Add the ML model to the SmartSim Model - colo_model.add_ml_model("cnn", - "TORCH", - model_path=model_file, - device=test_device, - devices_per_node=test_num_gpus) + colo_model.add_ml_model( + "cnn", + "TORCH", + model_path=model_file, + device=test_device, + devices_per_node=test_num_gpus, + first_device=0, + ) # Assert we have added both models assert len(colo_model._db_models) == 1 + exp.generate(colo_model) + # Launch and check successful completion try: exp.start(colo_model, block=True) statuses = exp.get_status(colo_model) - assert all([stat == status.STATUS_COMPLETED for stat in statuses]) + assert all( + stat == status.STATUS_COMPLETED for stat in statuses + ), f"Statuses: {statuses}" finally: exp.stop(colo_model) @pytest.mark.skipif(not should_run_tf, reason="Test needs TF to run") -def test_colocated_db_model_ensemble(fileutils, wlmutils, mlutils): +def test_colocated_db_model_ensemble(fileutils, test_dir, wlmutils, mlutils): """Test DBModel on colocated ensembles, first colocating DB, then adding DBModel. """ @@ -507,8 +537,8 @@ def test_colocated_db_model_ensemble(fileutils, wlmutils, mlutils): test_interface = wlmutils.get_test_interface() test_port = wlmutils.get_test_port() test_device = mlutils.get_test_device() - test_num_gpus = mlutils.get_test_num_gpus() - test_dir = fileutils.make_test_dir() + test_num_gpus = 1 # TF backend fails on multiple GPUs + test_script = fileutils.get_test_conf_path("run_tf_dbmodel_smartredis.py") # Create the SmartSim Experiment @@ -517,22 +547,18 @@ def test_colocated_db_model_ensemble(fileutils, wlmutils, mlutils): # Create RunSettings for colocated model colo_settings = exp.create_run_settings(exe=sys.executable, exe_args=test_script) colo_settings.set_nodes(1) - colo_settings.set_tasks_per_node(1) + colo_settings.set_tasks(1) # Create ensemble of two identical models - colo_ensemble = exp.create_ensemble( + colo_ensemble: Ensemble = exp.create_ensemble( "colocated_ens", run_settings=colo_settings, replicas=2 ) - colo_ensemble.set_path(test_dir) # Create a third model with a colocated database colo_model = exp.create_model("colocated_model", colo_settings) colo_model.set_path(test_dir) colo_model.colocate_db_tcp( - port=test_port, - db_cpus=1, - debug=True, - ifname=test_interface + port=test_port, db_cpus=1, debug=True, ifname=test_interface ) # Create and save the ML models to the filesystem @@ -542,10 +568,7 @@ def test_colocated_db_model_ensemble(fileutils, wlmutils, mlutils): # Colocate a database with the ensemble with two ensemble members for i, entity in enumerate(colo_ensemble): entity.colocate_db_tcp( - port=test_port + i + 1, - db_cpus=1, - debug=True, - ifname=test_interface + port=test_port + i + 1, db_cpus=1, debug=True, ifname=test_interface ) # Add ML model to each ensemble member individual to test that they # do not conflict with models add to the Ensemble object @@ -555,9 +578,11 @@ def test_colocated_db_model_ensemble(fileutils, wlmutils, mlutils): model_path=model_file2, device=test_device, devices_per_node=test_num_gpus, + first_device=0, inputs=inputs2, outputs=outputs2, ) + entity.disable_key_prefixing() # Test adding a model from Ensemble object colo_ensemble.add_ml_model( @@ -566,6 +591,7 @@ def test_colocated_db_model_ensemble(fileutils, wlmutils, mlutils): model_path=model_file, device=test_device, devices_per_node=test_num_gpus, + first_device=0, inputs=inputs, outputs=outputs, tag="test", @@ -581,21 +607,26 @@ def test_colocated_db_model_ensemble(fileutils, wlmutils, mlutils): model_path=model_file2, device=test_device, devices_per_node=test_num_gpus, + first_device=0, inputs=inputs2, outputs=outputs2, ) + exp.generate(colo_ensemble) + # Launch and check successful completion try: exp.start(colo_ensemble, block=True) statuses = exp.get_status(colo_ensemble) - assert all([stat == status.STATUS_COMPLETED for stat in statuses]) + assert all( + stat == status.STATUS_COMPLETED for stat in statuses + ), f"Statuses: {statuses}" finally: exp.stop(colo_ensemble) @pytest.mark.skipif(not should_run_tf, reason="Test needs TF to run") -def test_colocated_db_model_ensemble_reordered(fileutils, wlmutils, mlutils): +def test_colocated_db_model_ensemble_reordered(fileutils, test_dir, wlmutils, mlutils): """Test DBModel on colocated ensembles, first adding the DBModel to the ensemble, then colocating DB. """ @@ -608,27 +639,25 @@ def test_colocated_db_model_ensemble_reordered(fileutils, wlmutils, mlutils): test_interface = wlmutils.get_test_interface() test_port = wlmutils.get_test_port() test_device = mlutils.get_test_device() - test_num_gpus = mlutils.get_test_num_gpus() - test_dir = fileutils.make_test_dir() + test_num_gpus = 1 # TF backend fails on multiple GPUs + test_script = fileutils.get_test_conf_path("run_tf_dbmodel_smartredis.py") # Create the SmartSim Experiment - exp = Experiment(exp_name, launcher=test_launcher) + exp = Experiment(exp_name, launcher=test_launcher, exp_path=test_dir) # Create colocated RunSettings colo_settings = exp.create_run_settings(exe=sys.executable, exe_args=test_script) colo_settings.set_nodes(1) - colo_settings.set_tasks_per_node(1) + colo_settings.set_tasks(1) # Create the ensemble of two identical SmartSim Model colo_ensemble = exp.create_ensemble( "colocated_ens", run_settings=colo_settings, replicas=2 ) - colo_ensemble.set_path(test_dir) # Create colocated SmartSim Model colo_model = exp.create_model("colocated_model", colo_settings) - colo_model.set_path(test_dir) # Create and save ML model to filesystem model_file, inputs, outputs = save_tf_cnn(test_dir, "model1.pb") @@ -641,17 +670,15 @@ def test_colocated_db_model_ensemble_reordered(fileutils, wlmutils, mlutils): model_path=model_file, device=test_device, devices_per_node=test_num_gpus, + first_device=0, inputs=inputs, - outputs=outputs + outputs=outputs, ) # Colocate a database with the first ensemble members for i, entity in enumerate(colo_ensemble): entity.colocate_db_tcp( - port = test_port + i, - db_cpus=1, - debug=True, - ifname=test_interface + port=test_port + i, db_cpus=1, debug=True, ifname=test_interface ) # Add ML models to each ensemble member to make sure they # do not conflict with other ML models @@ -661,19 +688,21 @@ def test_colocated_db_model_ensemble_reordered(fileutils, wlmutils, mlutils): model_path=model_file2, device=test_device, devices_per_node=test_num_gpus, + first_device=0, inputs=inputs2, outputs=outputs2, ) + entity.disable_key_prefixing() # Add another ensemble member colo_ensemble.add_model(colo_model) # Colocate a database with the new ensemble member colo_model.colocate_db_tcp( - port=test_port + len(colo_ensemble), + port=test_port + len(colo_ensemble) - 1, db_cpus=1, debug=True, - ifname=test_interface + ifname=test_interface, ) # Add a ML model to the new ensemble member colo_model.add_ml_model( @@ -682,21 +711,26 @@ def test_colocated_db_model_ensemble_reordered(fileutils, wlmutils, mlutils): model_path=model_file2, device=test_device, devices_per_node=test_num_gpus, + first_device=0, inputs=inputs2, outputs=outputs2, ) + exp.generate(colo_ensemble) + # Launch and check successful completion try: exp.start(colo_ensemble, block=True) statuses = exp.get_status(colo_ensemble) - assert all([stat == status.STATUS_COMPLETED for stat in statuses]) + assert all( + stat == status.STATUS_COMPLETED for stat in statuses + ), f"Statuses: {statuses}" finally: exp.stop(colo_ensemble) @pytest.mark.skipif(not should_run_tf, reason="Test needs TF to run") -def test_colocated_db_model_errors(fileutils, wlmutils, mlutils): +def test_colocated_db_model_errors(fileutils, test_dir, wlmutils, mlutils): """Test error when colocated db model has no file.""" # Set experiment name @@ -707,26 +741,23 @@ def test_colocated_db_model_errors(fileutils, wlmutils, mlutils): test_interface = wlmutils.get_test_interface() test_port = wlmutils.get_test_port() test_device = mlutils.get_test_device() - test_num_gpus = mlutils.get_test_num_gpus() - test_dir = fileutils.make_test_dir() + test_num_gpus = 1 # TF backend fails on multiple GPUs + test_script = fileutils.get_test_conf_path("run_tf_dbmodel_smartredis.py") # Create SmartSim Experiment - exp = Experiment(exp_name, launcher=test_launcher) + exp = Experiment(exp_name, launcher=test_launcher, exp_path=test_dir) # Create colocated RunSettings colo_settings = exp.create_run_settings(exe=sys.executable, exe_args=test_script) colo_settings.set_nodes(1) - colo_settings.set_tasks_per_node(1) + colo_settings.set_tasks(1) # Create colocated SmartSim Model colo_model = exp.create_model("colocated_model", colo_settings) colo_model.set_path(test_dir) colo_model.colocate_db_tcp( - port=test_port, - db_cpus=1, - debug=True, - ifname=test_interface + port=test_port, db_cpus=1, debug=True, ifname=test_interface ) # Get and save TF model @@ -736,40 +767,46 @@ def test_colocated_db_model_errors(fileutils, wlmutils, mlutils): # are only supported for non-colocated deployments with pytest.raises(SSUnsupportedError): colo_model.add_ml_model( - "cnn", "TF", model=model, device=test_device, - devices_per_node=test_num_gpus, inputs=inputs, outputs=outputs + "cnn", + "TF", + model=model, + device=test_device, + devices_per_node=test_num_gpus, + first_device=0, + inputs=inputs, + outputs=outputs, ) # Create an ensemble with two identical replicas colo_ensemble = exp.create_ensemble( "colocated_ens", run_settings=colo_settings, replicas=2 ) - colo_ensemble.set_path(test_dir) # Colocate a db with each ensemble member for i, entity in enumerate(colo_ensemble): entity.colocate_db_tcp( - port=test_port + i, - db_cpus=1, - debug=True, - ifname=test_interface + port=test_port + i, db_cpus=1, debug=True, ifname=test_interface ) # Check that an error is raised because in-memory models # are only supported for non-colocated deployments with pytest.raises(SSUnsupportedError): colo_ensemble.add_ml_model( - "cnn", "TF", model=model, device=test_device, - devices_per_node=test_num_gpus, inputs=inputs, outputs=outputs + "cnn", + "TF", + model=model, + device=test_device, + devices_per_node=test_num_gpus, + first_device=0, + inputs=inputs, + outputs=outputs, ) # Check error is still thrown if an in-memory model is used # with a colocated deployment. This test varies by adding # the SmartSIm model with a colocated database to the ensemble # after the ML model was been added to the ensemble. - colo_settings2 = exp.create_run_settings( - exe=sys.executable, exe_args=test_script - ) + colo_settings2 = exp.create_run_settings(exe=sys.executable, exe_args=test_script) # Reverse order of DBModel and model colo_ensemble2 = exp.create_ensemble( @@ -777,8 +814,14 @@ def test_colocated_db_model_errors(fileutils, wlmutils, mlutils): ) colo_ensemble2.set_path(test_dir) colo_ensemble2.add_ml_model( - "cnn", "TF", model=model, device=test_device, - devices_per_node=test_num_gpus, inputs=inputs, outputs=outputs + "cnn", + "TF", + model=model, + device=test_device, + devices_per_node=test_num_gpus, + first_device=0, + inputs=inputs, + outputs=outputs, ) for i, entity in enumerate(colo_ensemble2): with pytest.raises(SSUnsupportedError): @@ -792,24 +835,26 @@ def test_colocated_db_model_errors(fileutils, wlmutils, mlutils): with pytest.raises(SSUnsupportedError): colo_ensemble.add_model(colo_model) + @pytest.mark.skipif(not should_run_tf, reason="Test needs TensorFlow to run") def test_inconsistent_params_db_model(): """Test error when devices_per_node parameter>1 when devices is set to CPU in DBModel""" - + # Create and save ML model to filesystem model, inputs, outputs = create_tf_cnn() with pytest.raises(SSUnsupportedError) as ex: - db_model = DBModel( + DBModel( "cnn", "TF", model=model, device="CPU", devices_per_node=2, + first_device=0, tag="test", inputs=inputs, outputs=outputs, ) assert ( - ex.value.args[0] - == "Cannot set devices_per_node>1 if CPU is specified under devices" - ) + ex.value.args[0] + == "Cannot set devices_per_node>1 if CPU is specified under devices" + ) diff --git a/tests/backends/test_dbscript.py b/tests/backends/test_dbscript.py index 06492f60f..e6cacd4d0 100644 --- a/tests/backends/test_dbscript.py +++ b/tests/backends/test_dbscript.py @@ -24,21 +24,25 @@ # 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. +import os import sys import pytest +from smartredis import * from smartsim import Experiment, status from smartsim._core.utils import installed_redisai_backends +from smartsim.entity.dbobject import DBScript from smartsim.error.errors import SSUnsupportedError from smartsim.log import get_logger - -from smartsim.entity.dbobject import DBScript +from smartsim.settings import MpiexecSettings, MpirunSettings logger = get_logger(__name__) should_run = True +supported_dbs = ["uds", "tcp"] + try: import torch except ImportError: @@ -52,7 +56,7 @@ def timestwo(x): @pytest.mark.skipif(not should_run, reason="Test needs Torch to run") -def test_db_script(fileutils, wlmutils, mlutils): +def test_db_script(fileutils, test_dir, wlmutils, mlutils): """Test DB scripts on remote DB""" # Set experiment name @@ -63,8 +67,8 @@ def test_db_script(fileutils, wlmutils, mlutils): test_interface = wlmutils.get_test_interface() test_port = wlmutils.get_test_port() test_device = mlutils.get_test_device() - test_num_gpus = mlutils.get_test_num_gpus() - test_dir = fileutils.make_test_dir() + test_num_gpus = mlutils.get_test_num_gpus() if pytest.test_device == "GPU" else 1 + test_script = fileutils.get_test_conf_path("run_dbscript_smartredis.py") torch_script = fileutils.get_test_conf_path("torchscript.py") @@ -74,15 +78,15 @@ def test_db_script(fileutils, wlmutils, mlutils): # Create the RunSettings run_settings = exp.create_run_settings(exe=sys.executable, exe_args=test_script) run_settings.set_nodes(1) - run_settings.set_tasks_per_node(1) + run_settings.set_tasks(1) # Create the SmartSim Model smartsim_model = exp.create_model("smartsim_model", run_settings) - smartsim_model.set_path(test_dir) # Create the SmartSim database - db = exp.create_database(port=test_port, interface=test_interface) - exp.generate(db) + host = wlmutils.choose_host(run_settings) + db = exp.create_database(port=test_port, interface=test_interface, hosts=host) + exp.generate(db, smartsim_model) # Define the torch script string torch_script_str = "def negate(x):\n\treturn torch.neg(x)\n" @@ -92,7 +96,8 @@ def test_db_script(fileutils, wlmutils, mlutils): "test_script1", script_path=torch_script, device=test_device, - devices_per_node=test_num_gpus + devices_per_node=test_num_gpus, + first_device=0, ) # Add script via string @@ -100,7 +105,8 @@ def test_db_script(fileutils, wlmutils, mlutils): "test_script2", script=torch_script_str, device=test_device, - devices_per_node=test_num_gpus + devices_per_node=test_num_gpus, + first_device=0, ) # Add script function @@ -108,7 +114,8 @@ def test_db_script(fileutils, wlmutils, mlutils): "test_func", function=timestwo, device=test_device, - devices_per_node=test_num_gpus + devices_per_node=test_num_gpus, + first_device=0, ) # Assert we have all three scripts @@ -124,7 +131,7 @@ def test_db_script(fileutils, wlmutils, mlutils): @pytest.mark.skipif(not should_run, reason="Test needs Torch to run") -def test_db_script_ensemble(fileutils, wlmutils, mlutils): +def test_db_script_ensemble(fileutils, test_dir, wlmutils, mlutils): """Test DB scripts on remote DB""" # Set experiment name @@ -135,8 +142,8 @@ def test_db_script_ensemble(fileutils, wlmutils, mlutils): test_interface = wlmutils.get_test_interface() test_port = wlmutils.get_test_port() test_device = mlutils.get_test_device() - test_num_gpus = mlutils.get_test_num_gpus() - test_dir = fileutils.make_test_dir() + test_num_gpus = mlutils.get_test_num_gpus() if pytest.test_device == "GPU" else 1 + test_script = fileutils.get_test_conf_path("run_dbscript_smartredis.py") torch_script = fileutils.get_test_conf_path("torchscript.py") @@ -146,20 +153,19 @@ def test_db_script_ensemble(fileutils, wlmutils, mlutils): # Create RunSettings run_settings = exp.create_run_settings(exe=sys.executable, exe_args=test_script) run_settings.set_nodes(1) - run_settings.set_tasks_per_node(1) + run_settings.set_tasks(1) # Create Ensemble with two identical models ensemble = exp.create_ensemble( "dbscript_ensemble", run_settings=run_settings, replicas=2 ) - ensemble.set_path(test_dir) # Create SmartSim model smartsim_model = exp.create_model("smartsim_model", run_settings) - smartsim_model.set_path(test_dir) # Create SmartSim database - db = exp.create_database(port=test_port, interface=test_interface) + host = wlmutils.choose_host(run_settings) + db = exp.create_database(port=test_port, interface=test_interface, hosts=host) exp.generate(db) # Create the script string @@ -170,7 +176,8 @@ def test_db_script_ensemble(fileutils, wlmutils, mlutils): "test_script1", script_path=torch_script, device=test_device, - devices_per_node=test_num_gpus + devices_per_node=test_num_gpus, + first_device=0, ) # Add script via string for each ensemble entity @@ -180,7 +187,8 @@ def test_db_script_ensemble(fileutils, wlmutils, mlutils): "test_script2", script=torch_script_str, device=test_device, - devices_per_node=test_num_gpus + devices_per_node=test_num_gpus, + first_device=0, ) # Add script via function @@ -188,7 +196,8 @@ def test_db_script_ensemble(fileutils, wlmutils, mlutils): "test_func", function=timestwo, device=test_device, - devices_per_node=test_num_gpus + devices_per_node=test_num_gpus, + first_device=0, ) # Add an additional ensemble member and attach a script to the new member @@ -197,7 +206,8 @@ def test_db_script_ensemble(fileutils, wlmutils, mlutils): "test_script2", script=torch_script_str, device=test_device, - devices_per_node=test_num_gpus + devices_per_node=test_num_gpus, + first_device=0, ) # Assert we have added both models to the ensemble @@ -206,6 +216,8 @@ def test_db_script_ensemble(fileutils, wlmutils, mlutils): # Assert we have added all three models to entities in ensemble assert all([len(entity._db_scripts) == 3 for entity in ensemble]) + exp.generate(ensemble) + try: exp.start(db, ensemble, block=True) statuses = exp.get_status(ensemble) @@ -215,7 +227,7 @@ def test_db_script_ensemble(fileutils, wlmutils, mlutils): @pytest.mark.skipif(not should_run, reason="Test needs Torch to run") -def test_colocated_db_script(fileutils, wlmutils, mlutils): +def test_colocated_db_script(fileutils, test_dir, wlmutils, mlutils): """Test DB Scripts on colocated DB""" # Set the experiment name @@ -226,27 +238,23 @@ def test_colocated_db_script(fileutils, wlmutils, mlutils): test_interface = wlmutils.get_test_interface() test_port = wlmutils.get_test_port() test_device = mlutils.get_test_device() - test_num_gpus = mlutils.get_test_num_gpus() - test_dir = fileutils.make_test_dir() + test_num_gpus = mlutils.get_test_num_gpus() if pytest.test_device == "GPU" else 1 + test_script = fileutils.get_test_conf_path("run_dbscript_smartredis.py") torch_script = fileutils.get_test_conf_path("torchscript.py") # Create the SmartSim Experiment - exp = Experiment(exp_name, launcher=test_launcher) + exp = Experiment(exp_name, launcher=test_launcher, exp_path=test_dir) # Create RunSettings colo_settings = exp.create_run_settings(exe=sys.executable, exe_args=test_script) colo_settings.set_nodes(1) - colo_settings.set_tasks_per_node(1) + colo_settings.set_tasks(1) # Create model with colocated database colo_model = exp.create_model("colocated_model", colo_settings) - colo_model.set_path(test_dir) colo_model.colocate_db_tcp( - port=test_port, - db_cpus=1, - debug=True, - ifname=test_interface, + port=test_port, db_cpus=1, debug=True, ifname=test_interface ) # Create string for script creation @@ -257,19 +265,23 @@ def test_colocated_db_script(fileutils, wlmutils, mlutils): "test_script1", script_path=torch_script, device=test_device, - devices_per_node=test_num_gpus + devices_per_node=test_num_gpus, + first_device=0, ) # Add script via string colo_model.add_script( "test_script2", script=torch_script_str, device=test_device, - devices_per_node=test_num_gpus + devices_per_node=test_num_gpus, + first_device=0, ) # Assert we have added both models assert len(colo_model._db_scripts) == 2 + exp.generate(colo_model) + for db_script in colo_model._db_scripts: logger.debug(db_script) @@ -282,7 +294,7 @@ def test_colocated_db_script(fileutils, wlmutils, mlutils): @pytest.mark.skipif(not should_run, reason="Test needs Torch to run") -def test_colocated_db_script_ensemble(fileutils, wlmutils, mlutils): +def test_colocated_db_script_ensemble(fileutils, test_dir, wlmutils, mlutils): """Test DB Scripts on colocated DB from ensemble, first colocating DB, then adding script. """ @@ -295,28 +307,26 @@ def test_colocated_db_script_ensemble(fileutils, wlmutils, mlutils): test_interface = wlmutils.get_test_interface() test_port = wlmutils.get_test_port() test_device = mlutils.get_test_device() - test_num_gpus = mlutils.get_test_num_gpus() - test_dir = fileutils.make_test_dir() + test_num_gpus = mlutils.get_test_num_gpus() if pytest.test_device == "GPU" else 1 + test_script = fileutils.get_test_conf_path("run_dbscript_smartredis.py") torch_script = fileutils.get_test_conf_path("torchscript.py") # Create SmartSim Experiment - exp = Experiment(exp_name, launcher=test_launcher) + exp = Experiment(exp_name, launcher=test_launcher, exp_path=test_dir) # Create RunSettings colo_settings = exp.create_run_settings(exe=sys.executable, exe_args=test_script) colo_settings.set_nodes(1) - colo_settings.set_tasks_per_node(1) + colo_settings.set_tasks(1) # Create SmartSim Ensemble with two identical models colo_ensemble = exp.create_ensemble( "colocated_ensemble", run_settings=colo_settings, replicas=2 ) - colo_ensemble.set_path(test_dir) # Create a SmartSim model colo_model = exp.create_model("colocated_model", colo_settings) - colo_model.set_path(test_dir) # Colocate a db with each ensemble entity and add a script # to each entity via file @@ -333,7 +343,8 @@ def test_colocated_db_script_ensemble(fileutils, wlmutils, mlutils): "test_script1", script_path=torch_script, device=test_device, - devices_per_node=test_num_gpus + devices_per_node=test_num_gpus, + first_device=0, ) # Colocate a db with the non-ensemble Model @@ -350,7 +361,8 @@ def test_colocated_db_script_ensemble(fileutils, wlmutils, mlutils): "test_script2", script=torch_script_str, device=test_device, - devices_per_node=test_num_gpus + devices_per_node=test_num_gpus, + first_device=0, ) # Add the third SmartSim model to the ensemble @@ -361,7 +373,8 @@ def test_colocated_db_script_ensemble(fileutils, wlmutils, mlutils): "test_script1", script_path=torch_script, device=test_device, - devices_per_node=test_num_gpus + devices_per_node=test_num_gpus, + first_device=0, ) # Assert we have added one model to the ensemble @@ -369,6 +382,8 @@ def test_colocated_db_script_ensemble(fileutils, wlmutils, mlutils): # Assert we have added both models to each entity assert all([len(entity._db_scripts) == 2 for entity in colo_ensemble]) + exp.generate(colo_ensemble) + # Launch and check successful completion try: exp.start(colo_ensemble, block=True) @@ -379,7 +394,7 @@ def test_colocated_db_script_ensemble(fileutils, wlmutils, mlutils): @pytest.mark.skipif(not should_run, reason="Test needs Torch to run") -def test_colocated_db_script_ensemble_reordered(fileutils, wlmutils, mlutils): +def test_colocated_db_script_ensemble_reordered(fileutils, test_dir, wlmutils, mlutils): """Test DB Scripts on colocated DB from ensemble, first adding the script to the ensemble, then colocating the DB""" @@ -391,28 +406,26 @@ def test_colocated_db_script_ensemble_reordered(fileutils, wlmutils, mlutils): test_interface = wlmutils.get_test_interface() test_port = wlmutils.get_test_port() test_device = mlutils.get_test_device() - test_num_gpus = mlutils.get_test_num_gpus() - test_dir = fileutils.make_test_dir() + test_num_gpus = mlutils.get_test_num_gpus() if pytest.test_device == "GPU" else 1 + test_script = fileutils.get_test_conf_path("run_dbscript_smartredis.py") torch_script = fileutils.get_test_conf_path("torchscript.py") # Create SmartSim Experiment - exp = Experiment(exp_name, launcher=test_launcher) + exp = Experiment(exp_name, launcher=test_launcher, exp_path=test_dir) # Create RunSettings colo_settings = exp.create_run_settings(exe=sys.executable, exe_args=test_script) colo_settings.set_nodes(1) - colo_settings.set_tasks_per_node(1) + colo_settings.set_tasks(1) # Create Ensemble with two identical SmartSim Model colo_ensemble = exp.create_ensemble( "colocated_ensemble", run_settings=colo_settings, replicas=2 ) - colo_ensemble.set_path(test_dir) # Create an additional SmartSim Model entity colo_model = exp.create_model("colocated_model", colo_settings) - colo_model.set_path(test_dir) # Add a script via string to the ensemble members torch_script_str = "def negate(x):\n\treturn torch.neg(x)\n" @@ -420,7 +433,8 @@ def test_colocated_db_script_ensemble_reordered(fileutils, wlmutils, mlutils): "test_script2", script=torch_script_str, device=test_device, - devices_per_node=test_num_gpus + devices_per_node=test_num_gpus, + first_device=0, ) # Add a colocated database to the ensemble members @@ -438,7 +452,8 @@ def test_colocated_db_script_ensemble_reordered(fileutils, wlmutils, mlutils): "test_script1", script_path=torch_script, device=test_device, - devices_per_node=test_num_gpus + devices_per_node=test_num_gpus, + first_device=0, ) # Add a colocated database to the non-ensemble SmartSim Model @@ -446,7 +461,7 @@ def test_colocated_db_script_ensemble_reordered(fileutils, wlmutils, mlutils): port=test_port + len(colo_ensemble), db_cpus=1, debug=True, - ifname=test_interface + ifname=test_interface, ) # Add the non-ensemble SmartSim Model to the Ensemble @@ -456,7 +471,8 @@ def test_colocated_db_script_ensemble_reordered(fileutils, wlmutils, mlutils): "test_script1", script_path=torch_script, device=test_device, - devices_per_node=test_num_gpus + devices_per_node=test_num_gpus, + first_device=0, ) # Assert we have added one model to the ensemble @@ -464,6 +480,8 @@ def test_colocated_db_script_ensemble_reordered(fileutils, wlmutils, mlutils): # Assert we have added both models to each entity assert all([len(entity._db_scripts) == 2 for entity in colo_ensemble]) + exp.generate(colo_ensemble) + # Launch and check successful completion try: exp.start(colo_ensemble, block=True) @@ -474,7 +492,7 @@ def test_colocated_db_script_ensemble_reordered(fileutils, wlmutils, mlutils): @pytest.mark.skipif(not should_run, reason="Test needs Torch to run") -def test_db_script_errors(fileutils, wlmutils, mlutils): +def test_db_script_errors(fileutils, test_dir, wlmutils, mlutils): """Test DB Scripts error when setting a serialized function on colocated DB""" # Set Experiment name @@ -485,22 +503,20 @@ def test_db_script_errors(fileutils, wlmutils, mlutils): test_interface = wlmutils.get_test_interface() test_port = wlmutils.get_test_port() test_device = mlutils.get_test_device() - test_num_gpus = mlutils.get_test_num_gpus() - test_dir = fileutils.make_test_dir() + test_num_gpus = mlutils.get_test_num_gpus() if pytest.test_device == "GPU" else 1 + test_script = fileutils.get_test_conf_path("run_dbscript_smartredis.py") - torch_script = fileutils.get_test_conf_path("torchscript.py") # Create SmartSim experiment - exp = Experiment(exp_name, launcher=test_launcher) + exp = Experiment(exp_name, launcher=test_launcher, exp_path=test_dir) # Create RunSettings colo_settings = exp.create_run_settings(exe=sys.executable, exe_args=test_script) colo_settings.set_nodes(1) - colo_settings.set_tasks_per_node(1) + colo_settings.set_tasks(1) # Create a SmartSim model with a colocated database colo_model = exp.create_model("colocated_model", colo_settings) - colo_model.set_path(test_dir) colo_model.colocate_db_tcp( port=test_port, db_cpus=1, @@ -515,7 +531,8 @@ def test_db_script_errors(fileutils, wlmutils, mlutils): "test_func", function=timestwo, device=test_device, - devices_per_node=test_num_gpus + devices_per_node=test_num_gpus, + first_device=0, ) # Create ensemble with two identical SmartSim Model entities @@ -523,7 +540,6 @@ def test_db_script_errors(fileutils, wlmutils, mlutils): colo_ensemble = exp.create_ensemble( "colocated_ensemble", run_settings=colo_settings, replicas=2 ) - colo_ensemble.set_path(test_dir) # Add a colocated database for each ensemble member for i, entity in enumerate(colo_ensemble): @@ -541,7 +557,8 @@ def test_db_script_errors(fileutils, wlmutils, mlutils): "test_func", function=timestwo, device=test_device, - devices_per_node=test_num_gpus + devices_per_node=test_num_gpus, + first_device=0, ) # Create an ensemble with two identical SmartSim Model entities @@ -549,14 +566,14 @@ def test_db_script_errors(fileutils, wlmutils, mlutils): colo_ensemble = exp.create_ensemble( "colocated_ensemble", run_settings=colo_settings, replicas=2 ) - colo_ensemble.set_path(test_dir) # Add an in-memory function to the ensemble colo_ensemble.add_function( "test_func", function=timestwo, device=test_device, - devices_per_node=test_num_gpus + devices_per_node=test_num_gpus, + first_device=0, ) # Check that an error is raised when trying to add @@ -576,19 +593,33 @@ def test_db_script_errors(fileutils, wlmutils, mlutils): # an in-memory script with pytest.raises(SSUnsupportedError): colo_ensemble.add_model(colo_model) - + + def test_inconsistent_params_db_script(fileutils): """Test error when devices_per_node>1 and when devices is set to CPU in DBScript constructor""" torch_script = fileutils.get_test_conf_path("torchscript.py") with pytest.raises(SSUnsupportedError) as ex: - db_script = DBScript( + _ = DBScript( name="test_script_db", - script_path = torch_script, + script_path=torch_script, device="CPU", devices_per_node=2, + first_device=0, ) assert ( - ex.value.args[0] - == "Cannot set devices_per_node>1 if CPU is specified under devices" - ) \ No newline at end of file + ex.value.args[0] + == "Cannot set devices_per_node>1 if CPU is specified under devices" + ) + with pytest.raises(SSUnsupportedError) as ex: + _ = DBScript( + name="test_script_db", + script_path=torch_script, + device="CPU", + devices_per_node=1, + first_device=5, + ) + assert ( + ex.value.args[0] + == "Cannot set first_device>0 if CPU is specified under devices" + ) diff --git a/tests/backends/test_onnx.py b/tests/backends/test_onnx.py index 312d56953..19c40017e 100644 --- a/tests/backends/test_onnx.py +++ b/tests/backends/test_onnx.py @@ -56,7 +56,7 @@ ) -def test_sklearn_onnx(fileutils, mlutils, wlmutils): +def test_sklearn_onnx(test_dir, mlutils, wlmutils): """This test needs two free nodes, 1 for the db and 1 some sklearn models here we test the following sklearn models: @@ -75,7 +75,7 @@ def test_sklearn_onnx(fileutils, mlutils, wlmutils): """ exp_name = "test_sklearn_onnx" - test_dir = fileutils.make_test_dir() + exp = Experiment(exp_name, exp_path=test_dir, launcher=wlmutils.get_test_launcher()) test_device = mlutils.get_test_device() diff --git a/tests/backends/test_tf.py b/tests/backends/test_tf.py index e30ad4f24..06c148a95 100644 --- a/tests/backends/test_tf.py +++ b/tests/backends/test_tf.py @@ -50,7 +50,7 @@ (not tf_backend_available) or (not tf_available), reason="Requires RedisAI TF backend", ) -def test_keras_model(fileutils, mlutils, wlmutils): +def test_keras_model(test_dir, mlutils, wlmutils): """This test needs two free nodes, 1 for the db and 1 for a keras model script this test can run on CPU/GPU by setting SMARTSIM_TEST_DEVICE=GPU @@ -61,7 +61,7 @@ def test_keras_model(fileutils, mlutils, wlmutils): """ exp_name = "test_keras_model" - test_dir = fileutils.make_test_dir() + exp = Experiment(exp_name, exp_path=test_dir, launcher=wlmutils.get_test_launcher()) test_device = mlutils.get_test_device() @@ -91,7 +91,6 @@ def test_keras_model(fileutils, mlutils, wlmutils): def create_tf_model(): - model = keras.Sequential( layers=[ keras.layers.InputLayer(input_shape=(28, 28), name="input"), @@ -110,9 +109,7 @@ def create_tf_model(): @pytest.mark.skipif(not tf_available, reason="Requires Tensorflow and Keras") -def test_freeze_model(fileutils): - test_dir = fileutils.make_test_dir() - +def test_freeze_model(test_dir): model = create_tf_model() model_path, inputs, outputs = freeze_model(model, test_dir, "mnist.pb") assert len(inputs) == 1 diff --git a/tests/backends/test_torch.py b/tests/backends/test_torch.py index ba663a433..71a63adb9 100644 --- a/tests/backends/test_torch.py +++ b/tests/backends/test_torch.py @@ -48,7 +48,7 @@ ) -def test_torch_model_and_script(fileutils, mlutils, wlmutils): +def test_torch_model_and_script(test_dir, mlutils, wlmutils): """This test needs two free nodes, 1 for the db and 1 for a torch model script Here we test both the torchscipt API and the NN API from torch @@ -61,7 +61,7 @@ def test_torch_model_and_script(fileutils, mlutils, wlmutils): """ exp_name = "test_torch_model_and_script" - test_dir = fileutils.make_test_dir() + exp = Experiment(exp_name, exp_path=test_dir, launcher=wlmutils.get_test_launcher()) test_device = mlutils.get_test_device() diff --git a/tests/full_wlm/test_generic_batch_launch.py b/tests/full_wlm/test_generic_batch_launch.py index 4beccd41b..2b7db11e1 100644 --- a/tests/full_wlm/test_generic_batch_launch.py +++ b/tests/full_wlm/test_generic_batch_launch.py @@ -29,23 +29,35 @@ import pytest from smartsim import Experiment, status +from smartsim.settings import QsubBatchSettings # retrieved from pytest fixtures if pytest.test_launcher not in pytest.wlm_options: pytestmark = pytest.mark.skip(reason="Not testing WLM integrations") +if (pytest.test_launcher == "pbs") and (not pytest.has_aprun): + pytestmark = pytest.mark.skip( + reason="Launching batch jobs is not supported on PBS without ALPS" + ) + + +def add_batch_resources(wlmutils, batch_settings): + if isinstance(batch_settings, QsubBatchSettings): + for key, value in wlmutils.get_batch_resources().items(): + batch_settings.set_resource(key, value) + -def test_batch_model(fileutils, wlmutils): +def test_batch_model(fileutils, test_dir, wlmutils): """Test the launch of a manually construced batch model""" exp_name = "test-batch-model" - exp = Experiment(exp_name, launcher=wlmutils.get_test_launcher()) - test_dir = fileutils.make_test_dir() + exp = Experiment(exp_name, launcher=wlmutils.get_test_launcher(), exp_path=test_dir) script = fileutils.get_test_conf_path("sleep.py") batch_settings = exp.create_batch_settings(nodes=1, time="00:01:00") batch_settings.set_account(wlmutils.get_test_account()) + add_batch_resources(wlmutils, batch_settings) if wlmutils.get_test_launcher() == "cobalt": batch_settings.set_queue("debug-flat-quad") run_settings = wlmutils.get_run_settings("python", f"{script} --time=5") @@ -60,12 +72,11 @@ def test_batch_model(fileutils, wlmutils): assert statuses[0] == status.STATUS_COMPLETED -def test_batch_ensemble(fileutils, wlmutils): +def test_batch_ensemble(fileutils, test_dir, wlmutils): """Test the launch of a manually constructed batch ensemble""" exp_name = "test-batch-ensemble" - exp = Experiment(exp_name, launcher=wlmutils.get_test_launcher()) - test_dir = fileutils.make_test_dir() + exp = Experiment(exp_name, launcher=wlmutils.get_test_launcher(), exp_path=test_dir) script = fileutils.get_test_conf_path("sleep.py") settings = wlmutils.get_run_settings("python", f"{script} --time=5") @@ -73,6 +84,7 @@ def test_batch_ensemble(fileutils, wlmutils): M2 = exp.create_model("m2", path=test_dir, run_settings=settings) batch = exp.create_batch_settings(nodes=1, time="00:01:00") + add_batch_resources(wlmutils, batch) batch.set_account(wlmutils.get_test_account()) if wlmutils.get_test_launcher() == "cobalt": @@ -87,15 +99,15 @@ def test_batch_ensemble(fileutils, wlmutils): assert all([stat == status.STATUS_COMPLETED for stat in statuses]) -def test_batch_ensemble_replicas(fileutils, wlmutils): +def test_batch_ensemble_replicas(fileutils, test_dir, wlmutils): exp_name = "test-batch-ensemble-replicas" - exp = Experiment(exp_name, launcher=wlmutils.get_test_launcher()) - test_dir = fileutils.make_test_dir() + exp = Experiment(exp_name, launcher=wlmutils.get_test_launcher(), exp_path=test_dir) script = fileutils.get_test_conf_path("sleep.py") settings = wlmutils.get_run_settings("python", f"{script} --time=5") batch = exp.create_batch_settings(nodes=1, time="00:01:00") + add_batch_resources(wlmutils, batch) batch.set_account(wlmutils.get_test_account()) if wlmutils.get_test_launcher() == "cobalt": diff --git a/tests/full_wlm/test_generic_orc_launch_batch.py b/tests/full_wlm/test_generic_orc_launch_batch.py index 7e5591a30..f1f5952b3 100644 --- a/tests/full_wlm/test_generic_orc_launch_batch.py +++ b/tests/full_wlm/test_generic_orc_launch_batch.py @@ -35,19 +35,26 @@ if pytest.test_launcher not in pytest.wlm_options: pytestmark = pytest.mark.skip(reason="Not testing WLM integrations") +if (pytest.test_launcher == "pbs") and (not pytest.has_aprun): + pytestmark = pytest.mark.skip( + reason="Launching orchestrators in a batch job is not supported on PBS without ALPS" + ) + -def test_launch_orc_auto_batch(fileutils, wlmutils): +def test_launch_orc_auto_batch(test_dir, wlmutils): """test single node orchestrator""" launcher = wlmutils.get_test_launcher() exp_name = "test-launch-auto-orc-batch" - exp = Experiment(exp_name, launcher=launcher) - test_dir = fileutils.make_test_dir() + exp = Experiment(exp_name, launcher=launcher, exp_path=test_dir) # batch = False to launch on existing allocation network_interface = wlmutils.get_test_interface() orc = exp.create_database( - wlmutils.get_test_port(), batch=True, interface=network_interface, single_cmd=False + wlmutils.get_test_port(), + batch=True, + interface=network_interface, + single_cmd=False, ) orc.batch_settings.set_account(wlmutils.get_test_account()) @@ -55,7 +62,7 @@ def test_launch_orc_auto_batch(fileutils, wlmutils): orc.batch_settings.set_walltime("00:02:00") if wlmutils.get_test_launcher() == "cobalt": orc.batch_settings.set_queue("debug-flat-quad") - + orc.set_path(test_dir) exp.start(orc, block=True) @@ -71,19 +78,22 @@ def test_launch_orc_auto_batch(fileutils, wlmutils): assert all([stat == status.STATUS_CANCELLED for stat in statuses]) -def test_launch_cluster_orc_batch_single(fileutils, wlmutils): +def test_launch_cluster_orc_batch_single(test_dir, wlmutils): """test clustered 3-node orchestrator with single command""" # TODO detect number of nodes in allocation and skip if not sufficent launcher = wlmutils.get_test_launcher() exp_name = "test-launch-auto-cluster-orc-batch-single" - exp = Experiment(exp_name, launcher=launcher) - test_dir = fileutils.make_test_dir() + exp = Experiment(exp_name, launcher=launcher, exp_path=test_dir) # batch = False to launch on existing allocation network_interface = wlmutils.get_test_interface() orc = exp.create_database( - wlmutils.get_test_port(), db_nodes=3, batch=True, interface=network_interface, single_cmd=True + wlmutils.get_test_port(), + db_nodes=3, + batch=True, + interface=network_interface, + single_cmd=True, ) orc.batch_settings.set_account(wlmutils.get_test_account()) @@ -110,23 +120,26 @@ def test_launch_cluster_orc_batch_single(fileutils, wlmutils): assert all([stat == status.STATUS_CANCELLED for stat in statuses]) -def test_launch_cluster_orc_batch_multi(fileutils, wlmutils): +def test_launch_cluster_orc_batch_multi(test_dir, wlmutils): """test clustered 3-node orchestrator""" # TODO detect number of nodes in allocation and skip if not sufficent launcher = wlmutils.get_test_launcher() exp_name = "test-launch-auto-cluster-orc-batch-multi" - exp = Experiment(exp_name, launcher=launcher) - test_dir = fileutils.make_test_dir() + exp = Experiment(exp_name, launcher=launcher, exp_path=test_dir) # batch = False to launch on existing allocation network_interface = wlmutils.get_test_interface() orc = exp.create_database( - wlmutils.get_test_port(), db_nodes=3, batch=True, interface=network_interface, single_cmd=False + wlmutils.get_test_port(), + db_nodes=3, + batch=True, + interface=network_interface, + single_cmd=False, ) - + orc.batch_settings.set_account(wlmutils.get_test_account()) - + orc.batch_settings.set_walltime("00:03:00") if wlmutils.get_test_launcher() == "cobalt": # As Cobalt won't allow us to run two @@ -149,16 +162,17 @@ def test_launch_cluster_orc_batch_multi(fileutils, wlmutils): assert all([stat == status.STATUS_CANCELLED for stat in statuses]) -def test_launch_cluster_orc_reconnect(fileutils, wlmutils): +def test_launch_cluster_orc_reconnect(test_dir, wlmutils): """test reconnecting to clustered 3-node orchestrator""" launcher = wlmutils.get_test_launcher() exp_name = "test-launch-cluster-orc-batch-reconect" - exp = Experiment(exp_name, launcher=launcher) - test_dir = fileutils.make_test_dir() + exp = Experiment(exp_name, launcher=launcher, exp_path=test_dir) # batch = False to launch on existing allocation network_interface = wlmutils.get_test_interface() - orc = exp.create_database(wlmutils.get_test_port(), db_nodes=3, batch=True, interface=network_interface) + orc = exp.create_database( + wlmutils.get_test_port(), db_nodes=3, batch=True, interface=network_interface + ) orc.set_path(test_dir) orc.batch_settings.set_account(wlmutils.get_test_account()) diff --git a/tests/full_wlm/test_mpmd.py b/tests/full_wlm/test_mpmd.py index 19f4660c2..18e918cfd 100644 --- a/tests/full_wlm/test_mpmd.py +++ b/tests/full_wlm/test_mpmd.py @@ -36,7 +36,7 @@ pytestmark = pytest.mark.skip(reason="Not testing WLM integrations") -def test_mpmd(fileutils, wlmutils): +def test_mpmd(fileutils, test_dir, wlmutils): """Run an MPMD model twice and check that it always gets executed the same way. @@ -61,7 +61,7 @@ def test_mpmd(fileutils, wlmutils): "cobalt": ["mpirun"], } - exp = Experiment(exp_name, launcher=launcher) + exp = Experiment(exp_name, launcher=launcher, exp_path=test_dir) def prune_commands(launcher): available_commands = [] @@ -77,7 +77,6 @@ def prune_commands(launcher): f"MPMD on {launcher} only supported for run commands {by_launcher[launcher]}" ) - test_dir = fileutils.make_test_dir() for run_command in run_commands: script = fileutils.get_test_conf_path("sleep.py") settings = exp.create_run_settings( diff --git a/tests/on_wlm/test_base_settings_on_wlm.py b/tests/on_wlm/test_base_settings_on_wlm.py index 3aa77983f..d75cc635f 100644 --- a/tests/on_wlm/test_base_settings_on_wlm.py +++ b/tests/on_wlm/test_base_settings_on_wlm.py @@ -40,10 +40,9 @@ pytestmark = pytest.mark.skip(reason="Not testing WLM integrations") -def test_model_on_wlm(fileutils, wlmutils): +def test_model_on_wlm(fileutils, test_dir, wlmutils): exp_name = "test-base-settings-model-launch" - exp = Experiment(exp_name, launcher=wlmutils.get_test_launcher()) - test_dir = fileutils.make_test_dir() + exp = Experiment(exp_name, launcher=wlmutils.get_test_launcher(), exp_path=test_dir) script = fileutils.get_test_conf_path("sleep.py") settings1 = wlmutils.get_base_run_settings("python", f"{script} --time=5") @@ -58,10 +57,9 @@ def test_model_on_wlm(fileutils, wlmutils): assert all([stat == status.STATUS_COMPLETED for stat in statuses]) -def test_model_stop_on_wlm(fileutils, wlmutils): +def test_model_stop_on_wlm(fileutils, test_dir, wlmutils): exp_name = "test-base-settings-model-stop" - exp = Experiment(exp_name, launcher=wlmutils.get_test_launcher()) - test_dir = fileutils.make_test_dir() + exp = Experiment(exp_name, launcher=wlmutils.get_test_launcher(), exp_path=test_dir) script = fileutils.get_test_conf_path("sleep.py") settings1 = wlmutils.get_base_run_settings("python", f"{script} --time=5") diff --git a/tests/on_wlm/test_colocated_model.py b/tests/on_wlm/test_colocated_model.py index 6a3945115..fa05eb513 100644 --- a/tests/on_wlm/test_colocated_model.py +++ b/tests/on_wlm/test_colocated_model.py @@ -36,142 +36,158 @@ else: supported_dbs = ["uds", "tcp", "deprecated"] +# Set to true if DB logs should be generated for debugging +DEBUG_DB = False + # retrieved from pytest fixtures launcher = pytest.test_launcher if launcher not in pytest.wlm_options: pytestmark = pytest.mark.skip(reason="Not testing WLM integrations") + @pytest.mark.parametrize("db_type", supported_dbs) -def test_launch_colocated_model_defaults(fileutils, coloutils, db_type): +def test_launch_colocated_model_defaults(fileutils, test_dir, coloutils, db_type): """Test the launch of a model with a colocated database and local launcher""" - db_args = { } + db_args = {"debug": DEBUG_DB} - exp = Experiment("colocated_model_defaults", launcher=launcher) + exp = Experiment("colocated_model_defaults", launcher=launcher, exp_path=test_dir) colo_model = coloutils.setup_test_colo( - fileutils, - db_type, - exp, - db_args, + fileutils, db_type, exp, "send_data_local_smartredis.py", db_args, on_wlm=True ) - + exp.generate(colo_model) assert colo_model.run_settings.colocated_db_settings["custom_pinning"] == "0" exp.start(colo_model, block=True) statuses = exp.get_status(colo_model) - assert all([stat == status.STATUS_COMPLETED for stat in statuses]) + assert all( + stat == status.STATUS_COMPLETED for stat in statuses + ), f"Statuses: {statuses}" # test restarting the colocated model exp.start(colo_model, block=True) statuses = exp.get_status(colo_model) - assert all([stat == status.STATUS_COMPLETED for stat in statuses]) + assert all( + stat == status.STATUS_COMPLETED for stat in statuses + ), f"Statuses: {statuses}" -@pytest.mark.parametrize("db_type", supported_dbs) -def test_colocated_model_disable_pinning(fileutils, coloutils, db_type): - exp = Experiment("colocated_model_pinning_auto_1cpu", launcher=launcher) +@pytest.mark.parametrize("db_type", supported_dbs) +def test_colocated_model_disable_pinning(fileutils, test_dir, coloutils, db_type): + exp = Experiment( + "colocated_model_pinning_auto_1cpu", launcher=launcher, exp_path=test_dir + ) db_args = { "db_cpus": 1, "custom_pinning": [], + "debug": DEBUG_DB, } # Check to make sure that the CPU mask was correctly generated colo_model = coloutils.setup_test_colo( - fileutils, - db_type, - exp, - db_args, + fileutils, db_type, exp, "send_data_local_smartredis.py", db_args, on_wlm=True ) assert colo_model.run_settings.colocated_db_settings["custom_pinning"] is None + exp.generate(colo_model) exp.start(colo_model, block=True) statuses = exp.get_status(colo_model) - assert all([stat == status.STATUS_COMPLETED for stat in statuses]) + assert all( + stat == status.STATUS_COMPLETED for stat in statuses + ), f"Statuses: {statuses}" -@pytest.mark.parametrize("db_type", supported_dbs) -def test_colocated_model_pinning_auto_2cpu(fileutils, coloutils, db_type): - exp = Experiment("colocated_model_pinning_auto_2cpu", launcher=launcher) +@pytest.mark.parametrize("db_type", supported_dbs) +def test_colocated_model_pinning_auto_2cpu(fileutils, test_dir, coloutils, db_type): + exp = Experiment( + "colocated_model_pinning_auto_2cpu", + launcher=launcher, + exp_path=test_dir, + ) - db_args = { - "db_cpus": 2, - } + db_args = {"db_cpus": 2, "debug": DEBUG_DB} # Check to make sure that the CPU mask was correctly generated colo_model = coloutils.setup_test_colo( - fileutils, - db_type, - exp, - db_args, + fileutils, db_type, exp, "send_data_local_smartredis.py", db_args, on_wlm=True ) assert colo_model.run_settings.colocated_db_settings["custom_pinning"] == "0,1" + exp.generate(colo_model) exp.start(colo_model, block=True) statuses = exp.get_status(colo_model) - assert all([stat == status.STATUS_COMPLETED for stat in statuses]) + assert all( + stat == status.STATUS_COMPLETED for stat in statuses + ), f"Statuses: {statuses}" + @pytest.mark.parametrize("db_type", supported_dbs) -def test_colocated_model_pinning_range(fileutils, coloutils, db_type): +def test_colocated_model_pinning_range(fileutils, test_dir, coloutils, db_type): # Check to make sure that the CPU mask was correctly generated # Assume that there are at least 4 cpus on the node - exp = Experiment("colocated_model_pinning_manual", launcher=launcher) + exp = Experiment( + "colocated_model_pinning_manual", + launcher=launcher, + exp_path=test_dir, + ) - db_args = { - "db_cpus": 4, - "custom_pinning": range(4) - } + db_args = {"db_cpus": 4, "custom_pinning": range(4), "debug": DEBUG_DB} colo_model = coloutils.setup_test_colo( - fileutils, - db_type, - exp, - db_args, + fileutils, db_type, exp, "send_data_local_smartredis.py", db_args, on_wlm=True ) assert colo_model.run_settings.colocated_db_settings["custom_pinning"] == "0,1,2,3" + exp.generate(colo_model) exp.start(colo_model, block=True) statuses = exp.get_status(colo_model) - assert all([stat == status.STATUS_COMPLETED for stat in statuses]) + assert all( + stat == status.STATUS_COMPLETED for stat in statuses + ), f"Statuses: {statuses}" + @pytest.mark.parametrize("db_type", supported_dbs) -def test_colocated_model_pinning_list(fileutils, coloutils, db_type): +def test_colocated_model_pinning_list(fileutils, test_dir, coloutils, db_type): # Check to make sure that the CPU mask was correctly generated # note we presume that this has more than 2 CPUs on the supercomputer node - exp = Experiment("colocated_model_pinning_manual", launcher=launcher) + exp = Experiment( + "colocated_model_pinning_manual", + launcher=launcher, + exp_path=test_dir, + ) - db_args = { - "db_cpus": 2, - "custom_pinning": [0,2] - } + db_args = {"db_cpus": 2, "custom_pinning": [0, 2]} colo_model = coloutils.setup_test_colo( - fileutils, - db_type, - exp, - db_args, + fileutils, db_type, exp, "send_data_local_smartredis.py", db_args, on_wlm=True ) assert colo_model.run_settings.colocated_db_settings["custom_pinning"] == "0,2" + exp.generate(colo_model) exp.start(colo_model, block=True) statuses = exp.get_status(colo_model) - assert all([stat == status.STATUS_COMPLETED for stat in statuses]) + assert all( + stat == status.STATUS_COMPLETED for stat in statuses + ), f"Statuses: {statuses}" + @pytest.mark.parametrize("db_type", supported_dbs) -def test_colocated_model_pinning_mixed(fileutils, coloutils, db_type): +def test_colocated_model_pinning_mixed(fileutils, test_dir, coloutils, db_type): # Check to make sure that the CPU mask was correctly generated # note we presume that this at least 4 CPUs on the supercomputer node - exp = Experiment("colocated_model_pinning_manual", launcher=launcher) + exp = Experiment( + "colocated_model_pinning_manual", + launcher=launcher, + exp_path=test_dir, + ) - db_args = { - "db_cpus": 2, - "custom_pinning": [range(2), 3] - } + db_args = {"db_cpus": 2, "custom_pinning": [range(2), 3]} colo_model = coloutils.setup_test_colo( - fileutils, - db_type, - exp, - db_args, + fileutils, db_type, exp, "send_data_local_smartredis.py", db_args, on_wlm=True ) assert colo_model.run_settings.colocated_db_settings["custom_pinning"] == "0,1,3" + exp.generate(colo_model) exp.start(colo_model, block=True) statuses = exp.get_status(colo_model) - assert all([stat == status.STATUS_COMPLETED for stat in statuses]) + assert all( + stat == status.STATUS_COMPLETED for stat in statuses + ), f"Statuses: {statuses}" diff --git a/tests/on_wlm/test_containers_wlm.py b/tests/on_wlm/test_containers_wlm.py index 414303df4..198a92f43 100644 --- a/tests/on_wlm/test_containers_wlm.py +++ b/tests/on_wlm/test_containers_wlm.py @@ -40,7 +40,7 @@ @pytest.mark.skipif(not singularity_exists, reason="Test needs singularity to run") -def test_singularity_wlm_smartredis(fileutils, wlmutils): +def test_singularity_wlm_smartredis(fileutils, test_dir, wlmutils): """Run two processes, each process puts a tensor on the DB, then accesses the other process's tensor. Finally, the tensor is used to run a model. @@ -55,13 +55,14 @@ def test_singularity_wlm_smartredis(fileutils, wlmutils): f"Test only runs on systems with PBS or Slurm as WLM. Current launcher: {launcher}" ) - test_dir = fileutils.make_test_dir() exp = Experiment( "smartredis_ensemble_exchange", exp_path=test_dir, launcher=launcher ) # create and start a database - orc = exp.create_database(port=wlmutils.get_test_port(), interface=wlmutils.get_test_interface()) + orc = exp.create_database( + port=wlmutils.get_test_port(), interface=wlmutils.get_test_interface() + ) exp.generate(orc) exp.start(orc, block=False) diff --git a/tests/on_wlm/test_generic_orc_launch.py b/tests/on_wlm/test_generic_orc_launch.py index 919317c73..ab100d1a7 100644 --- a/tests/on_wlm/test_generic_orc_launch.py +++ b/tests/on_wlm/test_generic_orc_launch.py @@ -33,13 +33,12 @@ pytestmark = pytest.mark.skip(reason="Not testing WLM integrations") -def test_launch_orc_auto(fileutils, wlmutils): +def test_launch_orc_auto(test_dir, wlmutils): """test single node orchestrator""" launcher = wlmutils.get_test_launcher() exp_name = "test-launch-auto-orc" - exp = Experiment(exp_name, launcher=launcher) - test_dir = fileutils.make_test_dir() + exp = Experiment(exp_name, launcher=launcher, exp_path=test_dir) # batch = False to launch on existing allocation network_interface = wlmutils.get_test_interface() @@ -65,14 +64,13 @@ def test_launch_orc_auto(fileutils, wlmutils): assert all([stat == status.STATUS_CANCELLED for stat in statuses]) -def test_launch_cluster_orc_single(fileutils, wlmutils): +def test_launch_cluster_orc_single(test_dir, wlmutils): """test clustered 3-node orchestrator with single command""" # TODO detect number of nodes in allocation and skip if not sufficent launcher = wlmutils.get_test_launcher() exp_name = "test-launch-auto-cluster-orc-single" - exp = Experiment(exp_name, launcher=launcher) - test_dir = fileutils.make_test_dir() + exp = Experiment(exp_name, launcher=launcher, exp_path=test_dir) # batch = False to launch on existing allocation network_interface = wlmutils.get_test_interface() @@ -99,14 +97,13 @@ def test_launch_cluster_orc_single(fileutils, wlmutils): assert all([stat == status.STATUS_CANCELLED for stat in statuses]) -def test_launch_cluster_orc_multi(fileutils, wlmutils): +def test_launch_cluster_orc_multi(test_dir, wlmutils): """test clustered 3-node orchestrator with multiple commands""" # TODO detect number of nodes in allocation and skip if not sufficent launcher = wlmutils.get_test_launcher() exp_name = "test-launch-auto-cluster-orc-multi" - exp = Experiment(exp_name, launcher=launcher) - test_dir = fileutils.make_test_dir() + exp = Experiment(exp_name, launcher=launcher, exp_path=test_dir) # batch = False to launch on existing allocation network_interface = wlmutils.get_test_interface() diff --git a/tests/on_wlm/test_het_job.py b/tests/on_wlm/test_het_job.py index 63d78fb23..e8f20d1ee 100644 --- a/tests/on_wlm/test_het_job.py +++ b/tests/on_wlm/test_het_job.py @@ -29,11 +29,11 @@ from smartsim import Experiment from smartsim.settings import SrunSettings - # retrieved from pytest fixtures if pytest.test_launcher != "slurm": pytestmark = pytest.mark.skip(reason="Test is only for Slurm WLM systems") + def test_mpmd_errors(monkeypatch): monkeypatch.setenv("SLURM_HET_SIZE", "1") exp_name = "test-het-job-errors" @@ -50,15 +50,14 @@ def test_mpmd_errors(monkeypatch): def test_set_het_groups(monkeypatch): - """Test ability to set one or more het groups to run setting - """ + """Test ability to set one or more het groups to run setting""" monkeypatch.setenv("SLURM_HET_SIZE", "4") exp_name = "test-set-het-group" exp = Experiment(exp_name, launcher="slurm") rs: SrunSettings = exp.create_run_settings("sleep", "1", run_command="srun") rs.set_het_group([1]) assert rs.run_args["het-group"] == "1" - rs.set_het_group([3,2]) + rs.set_het_group([3, 2]) assert rs.run_args["het-group"] == "3,2" with pytest.raises(ValueError): rs.set_het_group([4]) diff --git a/tests/on_wlm/test_launch_errors.py b/tests/on_wlm/test_launch_errors.py index 77ba8a69a..7281cb568 100644 --- a/tests/on_wlm/test_launch_errors.py +++ b/tests/on_wlm/test_launch_errors.py @@ -36,12 +36,11 @@ pytestmark = pytest.mark.skip(reason="Not testing WLM integrations") -def test_failed_status(fileutils, wlmutils): +def test_failed_status(fileutils, test_dir, wlmutils): """Test when a failure occurs deep into model execution""" exp_name = "test-report-failure" - exp = Experiment(exp_name, launcher=wlmutils.get_test_launcher()) - test_dir = fileutils.make_test_dir() + exp = Experiment(exp_name, launcher=wlmutils.get_test_launcher(), exp_path=test_dir) script = fileutils.get_test_conf_path("bad.py") settings = exp.create_run_settings( @@ -58,7 +57,7 @@ def test_failed_status(fileutils, wlmutils): assert stat[0] == status.STATUS_FAILED -def test_bad_run_command_args(fileutils, wlmutils): +def test_bad_run_command_args(fileutils, test_dir, wlmutils): """Should fail because of incorrect arguments given to the run command @@ -69,8 +68,7 @@ def test_bad_run_command_args(fileutils, wlmutils): pytest.skip(f"Only fails with slurm. Launcher is {launcher}") exp_name = "test-bad-run-command-args" - exp = Experiment(exp_name, launcher=launcher) - test_dir = fileutils.make_test_dir() + exp = Experiment(exp_name, launcher=launcher, exp_path=test_dir) script = fileutils.get_test_conf_path("bad.py") diff --git a/tests/on_wlm/test_launch_ompi_lsf.py b/tests/on_wlm/test_launch_ompi_lsf.py index e3327514a..ed082d22e 100644 --- a/tests/on_wlm/test_launch_ompi_lsf.py +++ b/tests/on_wlm/test_launch_ompi_lsf.py @@ -34,13 +34,12 @@ @pytest.mark.skip("OpenMPI currently not working on LSF systems") -def test_launch_openmpi_lsf(wlmutils, fileutils): +def test_launch_openmpi_lsf(fileutils, test_dir, wlmutils): launcher = wlmutils.get_test_launcher() if launcher != "lsf": pytest.skip("Test only runs on systems with LSF as WLM") exp_name = "test-launch-openmpi-lsf" - exp = Experiment(exp_name, launcher=launcher) - test_dir = fileutils.make_test_dir() + exp = Experiment(exp_name, launcher=launcher, exp_path=test_dir) script = fileutils.get_test_conf_path("sleep.py") settings = exp.create_run_settings("python", script, "mpirun") diff --git a/tests/on_wlm/test_local_step.py b/tests/on_wlm/test_local_step.py index 0613f41c2..02e619ebf 100644 --- a/tests/on_wlm/test_local_step.py +++ b/tests/on_wlm/test_local_step.py @@ -26,6 +26,7 @@ import os import uuid + import pytest from smartsim import Experiment, status @@ -40,13 +41,12 @@ """ -def test_local_env_pass_implicit(fileutils) -> None: +def test_local_env_pass_implicit(fileutils, test_dir) -> None: """Ensure implicitly exported env is available to running task""" exp_value = str(uuid.uuid4()) env_key = "test_local_env_pass_implicit" os.environ[env_key] = exp_value - test_dir = fileutils.make_test_dir() exp_dir = f"{test_dir}/exp" os.makedirs(exp_dir) script = fileutils.get_test_conf_path("check_env.py") @@ -72,19 +72,18 @@ def test_local_env_pass_implicit(fileutils) -> None: with open(f"{exp_dir}/{app_name}/{app_name}.out") as app_outfile: app_output = app_outfile.read() - + # verify application was able to access the env var assert f"{env_key}=={exp_value}" in app_output -def test_local_env_pass_explicit(fileutils) -> None: +def test_local_env_pass_explicit(fileutils, test_dir) -> None: """Ensure explicitly exported env is available to running task""" exp_value = str(uuid.uuid4()) env_key = "test_local_env_pass_explicit" assert env_key not in os.environ - test_dir = fileutils.make_test_dir() script = fileutils.get_test_conf_path("check_env.py") exp_dir = f"{test_dir}/exp" @@ -111,6 +110,6 @@ def test_local_env_pass_explicit(fileutils) -> None: with open(f"{exp_dir}/{app_name}/{app_name}.out") as app_outfile: app_output = app_outfile.read() - + # verify application was able to access the env var assert f"{env_key}=={exp_value}" in app_output diff --git a/tests/on_wlm/test_restart.py b/tests/on_wlm/test_restart.py index 86d883358..b1997961d 100644 --- a/tests/on_wlm/test_restart.py +++ b/tests/on_wlm/test_restart.py @@ -35,11 +35,9 @@ pytestmark = pytest.mark.skip(reason="Not testing WLM integrations") -def test_restart(fileutils, wlmutils): - +def test_restart(fileutils, test_dir, wlmutils): exp_name = "test-restart" - exp = Experiment(exp_name, launcher=wlmutils.get_test_launcher()) - test_dir = fileutils.make_test_dir() + exp = Experiment(exp_name, launcher=wlmutils.get_test_launcher(), exp_path=test_dir) script = fileutils.get_test_conf_path("sleep.py") settings = exp.create_run_settings("python", f"{script} --time=5") diff --git a/tests/on_wlm/test_simple_base_settings_on_wlm.py b/tests/on_wlm/test_simple_base_settings_on_wlm.py index d46a46aae..08bf875e2 100644 --- a/tests/on_wlm/test_simple_base_settings_on_wlm.py +++ b/tests/on_wlm/test_simple_base_settings_on_wlm.py @@ -48,7 +48,7 @@ pytestmark = pytest.mark.skip(reason="Not testing WLM integrations") -def test_simple_model_on_wlm(fileutils, wlmutils): +def test_simple_model_on_wlm(fileutils, test_dir, wlmutils): launcher = wlmutils.get_test_launcher() if launcher not in ["pbs", "slurm", "cobalt", "lsf"]: pytest.skip( @@ -56,8 +56,7 @@ def test_simple_model_on_wlm(fileutils, wlmutils): ) exp_name = "test-simplebase-settings-model-launch" - exp = Experiment(exp_name, launcher=wlmutils.get_test_launcher()) - test_dir = fileutils.make_test_dir() + exp = Experiment(exp_name, launcher=wlmutils.get_test_launcher(), exp_path=test_dir) script = fileutils.get_test_conf_path("sleep.py") settings = RunSettings("python", exe_args=f"{script} --time=5") @@ -69,7 +68,7 @@ def test_simple_model_on_wlm(fileutils, wlmutils): assert exp.get_status(M)[0] == status.STATUS_COMPLETED -def test_simple_model_stop_on_wlm(fileutils, wlmutils): +def test_simple_model_stop_on_wlm(fileutils, test_dir, wlmutils): launcher = wlmutils.get_test_launcher() if launcher not in ["pbs", "slurm", "cobalt", "lsf"]: pytest.skip( @@ -77,8 +76,7 @@ def test_simple_model_stop_on_wlm(fileutils, wlmutils): ) exp_name = "test-simplebase-settings-model-stop" - exp = Experiment(exp_name, launcher=wlmutils.get_test_launcher()) - test_dir = fileutils.make_test_dir() + exp = Experiment(exp_name, launcher=wlmutils.get_test_launcher(), exp_path=test_dir) script = fileutils.get_test_conf_path("sleep.py") settings = RunSettings("python", exe_args=f"{script} --time=5") diff --git a/tests/on_wlm/test_simple_entity_launch.py b/tests/on_wlm/test_simple_entity_launch.py index 14fd2e4ae..a05d7be0f 100644 --- a/tests/on_wlm/test_simple_entity_launch.py +++ b/tests/on_wlm/test_simple_entity_launch.py @@ -46,10 +46,9 @@ pytestmark = pytest.mark.skip(reason="Not testing WLM integrations") -def test_models(fileutils, wlmutils): +def test_models(fileutils, test_dir, wlmutils): exp_name = "test-models-launch" - exp = Experiment(exp_name, launcher=wlmutils.get_test_launcher()) - test_dir = fileutils.make_test_dir() + exp = Experiment(exp_name, launcher=wlmutils.get_test_launcher(), exp_path=test_dir) script = fileutils.get_test_conf_path("sleep.py") settings = exp.create_run_settings("python", f"{script} --time=5") @@ -63,10 +62,9 @@ def test_models(fileutils, wlmutils): assert all([stat == status.STATUS_COMPLETED for stat in statuses]) -def test_ensemble(fileutils, wlmutils): +def test_ensemble(fileutils, test_dir, wlmutils): exp_name = "test-ensemble-launch" - exp = Experiment(exp_name, launcher=wlmutils.get_test_launcher()) - test_dir = fileutils.make_test_dir() + exp = Experiment(exp_name, launcher=wlmutils.get_test_launcher(), exp_path=test_dir) script = fileutils.get_test_conf_path("sleep.py") settings = exp.create_run_settings("python", f"{script} --time=5") @@ -80,12 +78,11 @@ def test_ensemble(fileutils, wlmutils): assert all([stat == status.STATUS_COMPLETED for stat in statuses]) -def test_summary(fileutils, wlmutils): +def test_summary(fileutils, test_dir, wlmutils): """Fairly rudimentary test of the summary dataframe""" exp_name = "test-launch-summary" - exp = Experiment(exp_name, launcher=wlmutils.get_test_launcher()) - test_dir = fileutils.make_test_dir() + exp = Experiment(exp_name, launcher=wlmutils.get_test_launcher(), exp_path=test_dir) sleep = fileutils.get_test_conf_path("sleep.py") bad = fileutils.get_test_conf_path("bad.py") @@ -103,7 +100,7 @@ def test_summary(fileutils, wlmutils): assert exp.get_status(bad)[0] == status.STATUS_FAILED assert exp.get_status(sleep)[0] == status.STATUS_COMPLETED - summary_str = exp.summary(format="plain") + summary_str = exp.summary(style="plain") print(summary_str) rows = [s.split() for s in summary_str.split("\n")] diff --git a/tests/on_wlm/test_slurm_commands.py b/tests/on_wlm/test_slurm_commands.py new file mode 100644 index 000000000..d3ebbcd31 --- /dev/null +++ b/tests/on_wlm/test_slurm_commands.py @@ -0,0 +1,55 @@ +# BSD 2-Clause License +# +# Copyright (c) 2021-2023, 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. +import pytest + +from smartsim._core.launcher.slurm.slurmCommands import * +from smartsim.error.errors import LauncherError + +# retrieved from pytest fixtures +if pytest.test_launcher != "slurm": + pytestmark = pytest.mark.skip(reason="Test is only for Slurm WLM systems") + + +# Test that common ways of launching commands +# raise when expected to do so +@pytest.mark.parametrize( + "cmd,raises", + [ + (sacct, True), + (sstat, True), + (sinfo, False), + (salloc, False), + (scancel, False), + (scontrol, False), + ], +) +def test_error_raises(cmd, raises): + args = ["--non_existing_arg"] + if raises: + with pytest.raises(LauncherError): + cmd(args, raise_on_err=True) + else: + cmd(args) diff --git a/tests/on_wlm/test_stop.py b/tests/on_wlm/test_stop.py index a786ce1a4..383c6c4bd 100644 --- a/tests/on_wlm/test_stop.py +++ b/tests/on_wlm/test_stop.py @@ -42,10 +42,9 @@ pytestmark = pytest.mark.skip(reason="Not testing WLM integrations") -def test_stop_entity(fileutils, wlmutils): +def test_stop_entity(fileutils, test_dir, wlmutils): exp_name = "test-launch-stop-model" - exp = Experiment(exp_name, launcher=wlmutils.get_test_launcher()) - test_dir = fileutils.make_test_dir() + exp = Experiment(exp_name, launcher=wlmutils.get_test_launcher(), exp_path=test_dir) script = fileutils.get_test_conf_path("sleep.py") settings = exp.create_run_settings("python", f"{script} --time=10") @@ -59,11 +58,9 @@ def test_stop_entity(fileutils, wlmutils): assert exp.get_status(M1)[0] == status.STATUS_CANCELLED -def test_stop_entity_list(fileutils, wlmutils): - +def test_stop_entity_list(fileutils, test_dir, wlmutils): exp_name = "test-launch-stop-ensemble" - exp = Experiment(exp_name, launcher=wlmutils.get_test_launcher()) - test_dir = fileutils.make_test_dir() + exp = Experiment(exp_name, launcher=wlmutils.get_test_launcher(), exp_path=test_dir) script = fileutils.get_test_conf_path("sleep.py") settings = exp.create_run_settings("python", f"{script} --time=10") diff --git a/tests/test_alps_settings.py b/tests/test_alps_settings.py index 2d7577e9b..012f27fce 100644 --- a/tests/test_alps_settings.py +++ b/tests/test_alps_settings.py @@ -30,6 +30,9 @@ from smartsim.error import SSUnsupportedError from smartsim.settings import AprunSettings +# The tests in this file belong to the group_a group +pytestmark = pytest.mark.group_a + def test_aprun_settings(): settings = AprunSettings("python") diff --git a/tests/test_batch_settings.py b/tests/test_batch_settings.py index abf685146..cb2096727 100644 --- a/tests/test_batch_settings.py +++ b/tests/test_batch_settings.py @@ -30,6 +30,9 @@ from smartsim.settings import BsubBatchSettings, QsubBatchSettings, SbatchSettings from smartsim.settings.settings import create_batch_settings +# The tests in this file belong to the group_a group +pytestmark = pytest.mark.group_a + def test_create_pbs_batch(): pbs_batch = create_batch_settings( @@ -38,8 +41,7 @@ def test_create_pbs_batch(): args = pbs_batch.format_batch_args() assert isinstance(pbs_batch, QsubBatchSettings) assert args == [ - "-l select=1:ncpus=10", - "-l place=scatter", + "-l nodes=1:ncpus=10", "-l walltime=10:00:00", "-q default", "-A myproject", @@ -102,7 +104,7 @@ def test_existing_batch_args_mutation(): queue="default", batch_args=batch_args, ) - + # verify initial expectations assert "k1" in bsub.batch_args assert "k2" in bsub.batch_args @@ -114,6 +116,7 @@ def test_existing_batch_args_mutation(): assert bsub.batch_args["k1"] == orig_bargs["k1"] assert bsub.batch_args["k1"] != batch_args["k1"] + def test_direct_set_batch_args_mutation(): """ Ensure that if the batch_args dict is set directly, any previously @@ -129,7 +132,7 @@ def test_direct_set_batch_args_mutation(): queue="default", ) bsub.batch_args = batch_args - + # verify initial expectations assert "k1" in bsub.batch_args assert "k2" in bsub.batch_args diff --git a/tests/test_buildenv.py b/tests/test_buildenv.py index 91ae4d221..d362ca1dd 100644 --- a/tests/test_buildenv.py +++ b/tests/test_buildenv.py @@ -26,9 +26,12 @@ import pytest +from pkg_resources import packaging # type: ignore from smartsim._core._install.buildenv import Version_ -from pkg_resources import packaging # type: ignore + +# The tests in this file belong to the group_a group +pytestmark = pytest.mark.group_a def test_version_hash_eq(): diff --git a/tests/test_cli.py b/tests/test_cli.py index ee07bc853..899caa1e0 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -25,13 +25,16 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import argparse -from contextlib import contextmanager +import logging +import os +import pathlib import typing as t +from contextlib import contextmanager import pytest import smartsim -from smartsim._core._cli import build, cli +from smartsim._core._cli import build, cli, plugin from smartsim._core._cli.build import configure_parser as build_parser from smartsim._core._cli.build import execute as build_execute from smartsim._core._cli.clean import configure_parser as clean_parser @@ -40,10 +43,21 @@ from smartsim._core._cli.dbcli import execute as dbcli_execute from smartsim._core._cli.site import execute as site_execute from smartsim._core._cli.utils import MenuItemConfig -from smartsim._core._cli.validate import ( - execute as validate_execute, - configure_parser as validate_parser, -) +from smartsim._core._cli.validate import configure_parser as validate_parser +from smartsim._core._cli.validate import execute as validate_execute + +# The tests in this file belong to the group_a group +pytestmark = pytest.mark.group_a + +_TEST_LOGGER = logging.getLogger(__name__) + +try: + import smartdashboard +except: + test_dash_plugin = False +else: + test_dash_plugin = True + def mock_execute_custom(msg: str = None, good: bool = True) -> int: retval = 0 if good else 1 @@ -51,28 +65,32 @@ def mock_execute_custom(msg: str = None, good: bool = True) -> int: return retval -def mock_execute_good(_ns: argparse.Namespace) -> int: - return mock_execute_custom("GOOD THINGS", good = True) +def mock_execute_good( + _ns: argparse.Namespace, _unparsed: t.Optional[t.List[str]] = None +) -> int: + return mock_execute_custom("GOOD THINGS", good=True) -def mock_execute_fail(_ns: argparse.Namespace) -> int: - return mock_execute_custom("BAD THINGS", good = False) +def mock_execute_fail( + _ns: argparse.Namespace, _unparsed: t.Optional[t.List[str]] = None +) -> int: + return mock_execute_custom("BAD THINGS", good=False) def test_cli_default_args_parsing(capsys): """Test default parser behaviors with no subparsers""" menu: t.List[cli.MenuItemConfig] = [] - smart_cli = cli.SmartCli(menu) - + smart_cli = cli.SmartCli(menu) + captured = capsys.readouterr() # throw away existing output - with pytest.raises(SystemExit) as e: + with pytest.raises(SystemExit) as e: # the parser shouldn't get the `smart` CLI argument build_args = ["smart", "-h"] smart_cli.parser.parse_args(build_args) captured = capsys.readouterr() - assert "invalid choice: \'smart\'" in captured.err + assert "invalid choice: 'smart'" in captured.err assert e.value.code == 2 @@ -81,19 +99,18 @@ def test_cli_invalid_command(capsys): exp_help = "this is my mock help text for build" exp_cmd = "build" actual_cmd = f"not{exp_cmd}" - menu = [cli.MenuItemConfig(exp_cmd, - exp_help, - mock_execute_good, - build.configure_parser)] - smart_cli = cli.SmartCli(menu) - + menu = [ + cli.MenuItemConfig(exp_cmd, exp_help, mock_execute_good, build.configure_parser) + ] + smart_cli = cli.SmartCli(menu) + captured = capsys.readouterr() # throw away existing output with pytest.raises(SystemExit) as e: build_args = [actual_cmd, "-h"] smart_cli.parser.parse_args(build_args) - - captured = capsys.readouterr() # capture new output - + + captured = capsys.readouterr() # capture new output + # show that the command was not recognized assert "invalid choice" in captured.err assert e.value.code == 2 @@ -102,15 +119,15 @@ def test_cli_invalid_command(capsys): def test_cli_bad_default_args_parsing_bad_help(capsys): """Test passing an argument name that is incorrect""" menu: t.List[cli.MenuItemConfig] = [] - smart_cli = cli.SmartCli(menu) - + smart_cli = cli.SmartCli(menu) + captured = capsys.readouterr() # throw away existing output with pytest.raises(SystemExit) as e: build_args = ["--halp"] # <-- HELP vs HALP smart_cli.parser.parse_args(build_args) - - captured = capsys.readouterr() # capture new output - + + captured = capsys.readouterr() # capture new output + assert "smart: error:" in captured.err assert e.value.code == 2 @@ -118,39 +135,38 @@ def test_cli_bad_default_args_parsing_bad_help(capsys): def test_cli_bad_default_args_parsing_good_help(capsys): """Test passing an argument name that is correct""" menu: t.List[cli.MenuItemConfig] = [] - smart_cli = cli.SmartCli(menu) - + smart_cli = cli.SmartCli(menu) + captured = capsys.readouterr() # throw away existing output with pytest.raises(SystemExit) as e: build_args = ["-h"] smart_cli.parser.parse_args(build_args) - - captured = capsys.readouterr() # capture new output - + + captured = capsys.readouterr() # capture new output + assert "smart: error:" not in captured.out assert "usage: smart" in captured.out assert e.value.code == 0 def test_cli_add_subparser(capsys): - """Test that passing configuration for a command causes the command + """Test that passing configuration for a command causes the command to be added to the CLI """ exp_help = "this is my mock help text for build" exp_cmd = "build" - menu = [cli.MenuItemConfig(exp_cmd, - exp_help, - mock_execute_good, - build.configure_parser)] - smart_cli = cli.SmartCli(menu) - + menu = [ + cli.MenuItemConfig(exp_cmd, exp_help, mock_execute_good, build.configure_parser) + ] + smart_cli = cli.SmartCli(menu) + captured = capsys.readouterr() # throw away existing output with pytest.raises(SystemExit) as e: - build_args = [exp_cmd, "-h"] # <--- -h only + build_args = [exp_cmd, "-h"] # <--- -h only smart_cli.parser.parse_args(build_args) - - captured = capsys.readouterr() # capture new output - + + captured = capsys.readouterr() # capture new output + # show that -h showed the expected help text assert exp_help in captured.out assert e.value.code == 0 @@ -159,9 +175,9 @@ def test_cli_add_subparser(capsys): with pytest.raises(SystemExit) as e: build_args = [exp_cmd, "--help"] smart_cli.parser.parse_args(build_args) - - captured = capsys.readouterr() # capture new output - + + captured = capsys.readouterr() # capture new output + # show that --help ALSO works assert exp_help in captured.out assert e.value.code == 0 @@ -175,23 +191,23 @@ def test_cli_subparser_selection(capsys): exp_b_help = "this is my mock help text for build" exp_b_cmd = "build" - menu = [cli.MenuItemConfig(exp_a_cmd, - exp_a_help, - mock_execute_good, - build.configure_parser), - cli.MenuItemConfig(exp_b_cmd, - exp_b_help, - mock_execute_good, - build.configure_parser)] - smart_cli = cli.SmartCli(menu) - + menu = [ + cli.MenuItemConfig( + exp_a_cmd, exp_a_help, mock_execute_good, build.configure_parser + ), + cli.MenuItemConfig( + exp_b_cmd, exp_b_help, mock_execute_good, build.configure_parser + ), + ] + smart_cli = cli.SmartCli(menu) + captured = capsys.readouterr() # throw away existing output with pytest.raises(SystemExit) as e: - build_args = [exp_a_cmd, "-h"] # <--- -h only + build_args = [exp_a_cmd, "-h"] # <--- -h only smart_cli.parser.parse_args(build_args) - - captured = capsys.readouterr() # capture new output - + + captured = capsys.readouterr() # capture new output + # show that -h showed the expected help text for `smart dbcli -h` assert exp_a_help in captured.out assert e.value.code == 0 @@ -200,9 +216,9 @@ def test_cli_subparser_selection(capsys): with pytest.raises(SystemExit) as e: build_args = [exp_b_cmd, "--help"] smart_cli.parser.parse_args(build_args) - - captured = capsys.readouterr() # capture new output - + + captured = capsys.readouterr() # capture new output + # show that -h showed the expected help text for `smart build -h` assert exp_b_help in captured.out assert e.value.code == 0 @@ -215,27 +231,23 @@ def test_cli_command_execution(capsys): exp_b_help = "this is my mock help text for build" exp_b_cmd = "build" - - dbcli_exec = lambda x: mock_execute_custom(msg="Database", good=True) - build_exec = lambda x: mock_execute_custom(msg="Builder", good=True) - - menu = [cli.MenuItemConfig(exp_a_cmd, - exp_a_help, - dbcli_exec, - lambda x: None), - cli.MenuItemConfig(exp_b_cmd, - exp_b_help, - build_exec, - lambda x: None)] - smart_cli = cli.SmartCli(menu) - + + dbcli_exec = lambda x, y: mock_execute_custom(msg="Database", good=True) + build_exec = lambda x, y: mock_execute_custom(msg="Builder", good=True) + + menu = [ + cli.MenuItemConfig(exp_a_cmd, exp_a_help, dbcli_exec, lambda x: None), + cli.MenuItemConfig(exp_b_cmd, exp_b_help, build_exec, lambda x: None), + ] + smart_cli = cli.SmartCli(menu) + captured = capsys.readouterr() # throw away existing output - + build_args = ["smart", exp_a_cmd] ret_val = smart_cli.execute(build_args) - captured = capsys.readouterr() # capture new output - + captured = capsys.readouterr() # capture new output + # show that `smart dbcli` calls the build parser and build execute function assert "Database" in captured.out assert ret_val == 0 @@ -243,8 +255,8 @@ def test_cli_command_execution(capsys): build_args = ["smart", exp_b_cmd] ret_val = smart_cli.execute(build_args) - captured = capsys.readouterr() # capture new output - + captured = capsys.readouterr() # capture new output + # show that `smart build` calls the build parser and build execute function assert "Builder" in captured.out assert ret_val == 0 @@ -253,83 +265,139 @@ def test_cli_command_execution(capsys): def test_cli_default_cli(capsys): """Ensure the default CLI supports expected top-level commands""" smart_cli = cli.default_cli() - + captured = capsys.readouterr() # throw away existing output - + # execute with no argument, expect full help text build_args = ["smart"] ret_val = smart_cli.execute(build_args) - captured = capsys.readouterr() # capture new output - + captured = capsys.readouterr() # capture new output + # show that `smart dbcli` calls the build parser and build execute function assert "usage: smart [-h] " in captured.out assert "Available commands" in captured.out - assert ret_val == 0 + assert ret_val == os.EX_USAGE # execute with `build` argument, expect build-specific help text with pytest.raises(SystemExit) as e: build_args = ["smart", "build", "-h"] ret_val = smart_cli.execute(build_args) - captured = capsys.readouterr() # capture new output - + captured = capsys.readouterr() # capture new output + assert "usage: smart build [-h]" in captured.out assert "Build SmartSim dependencies" in captured.out assert "optional arguments:" in captured.out or "options:" in captured.out - assert ret_val == 0 + assert ret_val == os.EX_USAGE # execute with `clean` argument, expect clean-specific help text with pytest.raises(SystemExit) as e: build_args = ["smart", "clean", "-h"] ret_val = smart_cli.execute(build_args) - captured = capsys.readouterr() # capture new output - + captured = capsys.readouterr() # capture new output + assert "usage: smart clean [-h]" in captured.out assert "Remove previous ML runtime installation" in captured.out assert "optional arguments:" in captured.out or "options:" in captured.out assert "--clobber" in captured.out - assert ret_val == 0 + assert ret_val == os.EX_USAGE # execute with `dbcli` argument, expect dbcli-specific help text with pytest.raises(SystemExit) as e: build_args = ["smart", "dbcli", "-h"] ret_val = smart_cli.execute(build_args) - captured = capsys.readouterr() # capture new output - + captured = capsys.readouterr() # capture new output + assert "usage: smart dbcli [-h]" in captured.out assert "Print the path to the redis-cli binary" in captured.out assert "optional arguments:" in captured.out or "options:" in captured.out - assert ret_val == 0 + assert ret_val == os.EX_USAGE # execute with `site` argument, expect site-specific help text with pytest.raises(SystemExit) as e: build_args = ["smart", "site", "-h"] ret_val = smart_cli.execute(build_args) - captured = capsys.readouterr() # capture new output - + captured = capsys.readouterr() # capture new output + assert "usage: smart site [-h]" in captured.out assert "Print the installation site of SmartSim" in captured.out assert "optional arguments:" in captured.out or "options:" in captured.out - assert ret_val == 0 + assert ret_val == os.EX_USAGE # execute with `clobber` argument, expect clobber-specific help text with pytest.raises(SystemExit) as e: build_args = ["smart", "clobber", "-h"] ret_val = smart_cli.execute(build_args) - captured = capsys.readouterr() # capture new output - + captured = capsys.readouterr() # capture new output + assert "usage: smart clobber [-h]" in captured.out assert "Remove all previous dependency installations" in captured.out assert "optional arguments:" in captured.out or "options:" in captured.out # assert "--clobber" not in captured.out - assert ret_val == 0 + assert ret_val == os.EX_USAGE + + +@pytest.mark.skipif(not test_dash_plugin, reason="plugin not found") +def test_cli_plugin_dashboard(capfd): + """Ensure expected dashboard CLI plugin commands are supported""" + smart_cli = cli.default_cli() + capfd.readouterr() # throw away existing output + + # execute with `dashboard` argument, expect dashboard-specific help text + build_args = ["smart", "dashboard", "-h"] + rc = smart_cli.execute(build_args) + captured = capfd.readouterr() # capture new output + assert "[-d DIRECTORY]" in captured.out + assert "[-p PORT]" in captured.out + + assert "optional arguments:" in captured.out + assert rc == 0 + + +def test_cli_plugin_invalid( + monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture +): + """Ensure unexpected CLI plugins are reported""" + import smartsim._core._cli.cli + import smartsim._core._cli.plugin + + plugin_module = "notinstalled.Experiment_Overview" + bad_plugins = [ + lambda: MenuItemConfig( + "dashboard", + "Start the SmartSim dashboard", + plugin.dynamic_execute(plugin_module, "Dashboard!"), + is_plugin=True, + ) + ] + monkeypatch.setattr(smartsim._core._cli.cli, "plugins", bad_plugins) + # Coloredlogs doesn't play nice with caplog + monkeypatch.setattr( + smartsim._core._cli.plugin, + "_LOGGER", + _TEST_LOGGER, + ) + + smart_cli = cli.default_cli() + + # execute with `dashboard` argument, expect failure to find dashboard plugin + build_args = ["smart", "dashboard", "-h"] + + rc = smart_cli.execute(build_args) + + assert plugin_module in caplog.text + assert "not found" in caplog.text + assert rc == os.EX_CONFIG + + +# fmt: off @pytest.mark.parametrize( "command,mock_location,exp_output", [ @@ -342,28 +410,31 @@ def test_cli_default_cli(capsys): pytest.param("info", "info_execute", "mocked-validate", id="ensure info action is executed"), ] ) +# fmt: on def test_cli_action(capsys, monkeypatch, command, mock_location, exp_output): """Ensure the default CLI executes the build action""" - def mock_execute(ns: argparse.Namespace): + + def mock_execute(ns: argparse.Namespace, _unparsed: t.Optional[t.List[str]] = None): print(exp_output) return 0 monkeypatch.setattr(smartsim._core._cli.cli, mock_location, mock_execute) - + smart_cli = cli.default_cli() - + captured = capsys.readouterr() # throw away existing output - + # execute with `` argument, expect -specific output text build_args = ["smart", command] ret_val = smart_cli.execute(build_args) - captured = capsys.readouterr() # capture new output - + captured = capsys.readouterr() # capture new output + assert exp_output in captured.out assert ret_val == 0 +# fmt: off @pytest.mark.parametrize( "command,mock_location,exp_output,optional_arg,exp_valid,exp_err_msg,check_prop,exp_prop_val", [ @@ -385,47 +456,49 @@ def mock_execute(ns: argparse.Namespace): pytest.param("validate", "validate_execute", "gpuX mocked-validate", "--device=gpux", False, "invalid choice: 'gpux'", "", "", id="validate: set bad device 'gpuX'"), ] ) -def test_cli_optional_args(capsys, - monkeypatch, - command: str, - mock_location: str, - exp_output: str, - optional_arg: str, - exp_valid: bool, - exp_err_msg: str, - check_prop: str, - exp_prop_val: t.Any): +# fmt: on +def test_cli_optional_args( + capsys, + monkeypatch, + command: str, + mock_location: str, + exp_output: str, + optional_arg: str, + exp_valid: bool, + exp_err_msg: str, + check_prop: str, + exp_prop_val: t.Any, +): """Ensure the parser for a command handles expected optional arguments""" - def mock_execute(ns: argparse.Namespace): + + def mock_execute(ns: argparse.Namespace, _unparsed: t.Optional[t.List[str]] = None): print(exp_output) return 0 monkeypatch.setattr(smartsim._core._cli.cli, mock_location, mock_execute) - + smart_cli = cli.default_cli() - + captured = capsys.readouterr() # throw away existing output - + build_args = ["smart", command] + optional_arg.split() if exp_valid: ret_val = smart_cli.execute(build_args) - captured = capsys.readouterr() # capture new output + captured = capsys.readouterr() # capture new output assert exp_output in captured.out # did the expected execution method occur? assert ret_val == 0 # is the retval is non-failure code? - - # is the value from the optional argument set in the parsed args? - assert smart_cli.args.__dict__[check_prop] == exp_prop_val else: with pytest.raises(SystemExit) as e: ret_val = smart_cli.execute(build_args) assert ret_val > 0 - captured = capsys.readouterr() # capture new output + captured = capsys.readouterr() # capture new output assert exp_err_msg in captured.err +# fmt: off @pytest.mark.parametrize( "command,mock_location,mock_output,exp_output", [ @@ -438,33 +511,38 @@ def mock_execute(ns: argparse.Namespace): pytest.param("info", "info_execute", "helpful mocked-validate", "usage: smart info", id="info"), ] ) -def test_cli_help_support(capsys, - monkeypatch, - command: str, - mock_location: str, - mock_output: str, - exp_output: str): +# fmt: on +def test_cli_help_support( + capsys, + monkeypatch, + command: str, + mock_location: str, + mock_output: str, + exp_output: str, +): """Ensure the parser supports help optional for commands as expected""" - def mock_execute(ns: argparse.Namespace): + + def mock_execute(ns: argparse.Namespace, unparsed: t.Optional[t.List[str]] = None): print(mock_output) return 0 monkeypatch.setattr(smartsim._core._cli.cli, mock_location, mock_execute) - + smart_cli = cli.default_cli() - + captured = capsys.readouterr() # throw away existing output - + # execute with `` argument, expect -specific help text build_args = ["smart", command] + ["-h"] with pytest.raises(SystemExit) as e: ret_val = smart_cli.execute(build_args) assert ret_val == 0 - captured = capsys.readouterr() # capture new output + captured = capsys.readouterr() # capture new output assert exp_output in captured.out +# fmt: off @pytest.mark.parametrize( "command,mock_location,exp_output", [ @@ -477,29 +555,29 @@ def mock_execute(ns: argparse.Namespace): pytest.param("info", "info_execute", "verbose mocked-validate", id="validate"), ] ) -def test_cli_invalid_optional_args(capsys, - monkeypatch, - command: str, - mock_location: str, - exp_output: str): +# fmt: on +def test_cli_invalid_optional_args( + capsys, monkeypatch, command: str, mock_location: str, exp_output: str +): """Ensure the parser throws expected error for an invalid argument""" - def mock_execute(ns: argparse.Namespace): + + def mock_execute(ns: argparse.Namespace, unparsed: t.Optional[t.List[str]] = None): print(exp_output) return 0 monkeypatch.setattr(smartsim._core._cli.cli, mock_location, mock_execute) - + smart_cli = cli.default_cli() - + captured = capsys.readouterr() # throw away existing output - + # execute with `` argument, expect CLI to raise invalid arg error build_args = ["smart", command] + ["-xyz"] with pytest.raises(SystemExit) as e: ret_val = smart_cli.execute(build_args) assert ret_val > 0 - captured = capsys.readouterr() # capture new output + captured = capsys.readouterr() # capture new output assert "unrecognized argument" in captured.err @@ -513,57 +591,56 @@ def mock_execute(ns: argparse.Namespace): pytest.param("site", id="site"), pytest.param("validate", id="validate"), pytest.param("info", id="info"), - ] + ], ) def test_cli_invalid_optional_args(capsys, command): """Ensure the parser throws expected error for an invalid command""" smart_cli = cli.default_cli() - + captured = capsys.readouterr() # throw away existing output - + # execute with `` argument, expect CLI to raise invalid arg error build_args = ["smart", command] + ["-xyz"] with pytest.raises(SystemExit) as e: ret_val = smart_cli.execute(build_args) assert ret_val > 0 - captured = capsys.readouterr() # capture new output + captured = capsys.readouterr() # capture new output assert "unrecognized argument" in captured.err def test_cli_full_clean_execute(capsys, monkeypatch): - """Ensure that the execute method of clean is called """ + """Ensure that the execute method of clean is called""" exp_retval = 0 exp_output = "mocked-clean utility" - def mock_operation(*args, **kwargs) -> int: + # mock out the internal clean method so we don't actually delete anything + def mock_clean(core_path: pathlib.Path, _all: bool = False) -> int: print(exp_output) return exp_retval - # mock out the internal clean method so we don't actually delete anything - monkeypatch.setattr(smartsim._core._cli.clean, "clean", mock_operation) + monkeypatch.setattr(smartsim._core._cli.clean, "clean", mock_clean) command = "clean" - cfg = MenuItemConfig(command, - f"test {command} help text", - clean_execute, - clean_parser) + cfg = MenuItemConfig( + command, f"test {command} help text", clean_execute, clean_parser + ) menu = [cfg] smart_cli = cli.SmartCli(menu) - + captured = capsys.readouterr() # throw away existing output - + build_args = ["smart", command] actual_retval = smart_cli.execute(build_args) - captured = capsys.readouterr() # capture new output - + captured = capsys.readouterr() # capture new output + assert exp_output in captured.out assert actual_retval == exp_retval def test_cli_full_clobber_execute(capsys, monkeypatch): - """Ensure that the execute method of clobber is called """ + """Ensure that the execute method of clobber is called""" exp_retval = 0 exp_output = "mocked-clobber utility" @@ -575,25 +652,23 @@ def mock_operation(*args, **kwargs) -> int: monkeypatch.setattr(smartsim._core._cli.clean, "clean", mock_operation) command = "clobber" - cfg = MenuItemConfig(command, - f"test {command} help text", - clobber_execute) + cfg = MenuItemConfig(command, f"test {command} help text", clobber_execute) menu = [cfg] smart_cli = cli.SmartCli(menu) - + captured = capsys.readouterr() # throw away existing output - + build_args = ["smart", command] actual_retval = smart_cli.execute(build_args) - captured = capsys.readouterr() # capture new output - + captured = capsys.readouterr() # capture new output + assert exp_output in captured.out assert actual_retval == exp_retval def test_cli_full_dbcli_execute(capsys, monkeypatch): - """Ensure that the execute method of dbcli is called """ + """Ensure that the execute method of dbcli is called""" exp_retval = 0 exp_output = "mocked-get_db_path utility" @@ -604,25 +679,23 @@ def mock_operation(*args, **kwargs) -> int: monkeypatch.setattr(smartsim._core._cli.dbcli, "get_db_path", mock_operation) command = "dbcli" - cfg = MenuItemConfig(command, - f"test {command} help text", - dbcli_execute) + cfg = MenuItemConfig(command, f"test {command} help text", dbcli_execute) menu = [cfg] smart_cli = cli.SmartCli(menu) - + captured = capsys.readouterr() # throw away existing output - + build_args = ["smart", command] actual_retval = smart_cli.execute(build_args) - captured = capsys.readouterr() # capture new output - + captured = capsys.readouterr() # capture new output + assert exp_output in captured.out assert actual_retval == exp_retval def test_cli_full_site_execute(capsys, monkeypatch): - """Ensure that the execute method of site is called """ + """Ensure that the execute method of site is called""" exp_retval = 0 exp_output = "mocked-get_install_path utility" @@ -634,25 +707,23 @@ def mock_operation(*args, **kwargs) -> int: monkeypatch.setattr(smartsim._core._cli.site, "get_install_path", mock_operation) command = "site" - cfg = MenuItemConfig(command, - f"test {command} help text", - site_execute) + cfg = MenuItemConfig(command, f"test {command} help text", site_execute) menu = [cfg] smart_cli = cli.SmartCli(menu) - + captured = capsys.readouterr() # throw away existing output - + build_args = ["smart", command] actual_retval = smart_cli.execute(build_args) - captured = capsys.readouterr() # capture new output - + captured = capsys.readouterr() # capture new output + assert exp_output in captured.out assert actual_retval == exp_retval def test_cli_full_build_execute(capsys, monkeypatch): - """Ensure that the execute method of build is called """ + """Ensure that the execute method of build is called""" exp_retval = 0 exp_output = "mocked-execute-build utility" @@ -664,31 +735,36 @@ def mock_operation(*args, **kwargs) -> int: monkeypatch.setattr(smartsim._core._cli.build, "tabulate", mock_operation) monkeypatch.setattr(smartsim._core._cli.build, "build_database", mock_operation) monkeypatch.setattr(smartsim._core._cli.build, "build_redis_ai", mock_operation) - monkeypatch.setattr(smartsim._core._cli.build, "check_py_torch_version", mock_operation) - monkeypatch.setattr(smartsim._core._cli.build, "check_py_tf_version", mock_operation) - monkeypatch.setattr(smartsim._core._cli.build, "check_py_onnx_version", mock_operation) + monkeypatch.setattr( + smartsim._core._cli.build, "check_py_torch_version", mock_operation + ) + monkeypatch.setattr( + smartsim._core._cli.build, "check_py_tf_version", mock_operation + ) + monkeypatch.setattr( + smartsim._core._cli.build, "check_py_onnx_version", mock_operation + ) command = "build" - cfg = MenuItemConfig(command, - f"test {command} help text", - build_execute, - build_parser) + cfg = MenuItemConfig( + command, f"test {command} help text", build_execute, build_parser + ) menu = [cfg] smart_cli = cli.SmartCli(menu) - + captured = capsys.readouterr() # throw away existing output - + build_args = ["smart", command] actual_retval = smart_cli.execute(build_args) - captured = capsys.readouterr() # capture new output - + captured = capsys.readouterr() # capture new output + assert exp_output in captured.out assert actual_retval == exp_retval def _good_build(*args, **kwargs): - print("LGTM") + _TEST_LOGGER.info("LGTM") def _bad_build(*args, **kwargs): @@ -703,17 +779,17 @@ def _mock_temp_dir(*a, **kw): @pytest.mark.parametrize( "mock_verify_fn, expected_stdout, expected_retval", [ - pytest.param(_good_build, 'LGTM', 0, id="Configured Correctly"), + pytest.param(_good_build, "LGTM", os.EX_OK, id="Configured Correctly"), pytest.param( _bad_build, - "SmartSim failed to run a simple experiment", - 2, + "SmartSim failed to run a simple experiment", + os.EX_SOFTWARE, id="Configured Incorrectly", - ) + ), ], ) -def test_cli_build_test_execute( - capsys, +def test_cli_validation_test_execute( + caplog, monkeypatch, mock_verify_fn, expected_stdout, @@ -724,6 +800,7 @@ def test_cli_build_test_execute( checks that if at any point the test raises an exception an appropriate error code and error msg are returned. """ + caplog.set_level(logging.INFO) # Mock out the verification tests/avoid file system ops monkeypatch.setattr(smartsim._core._cli.validate, "test_install", mock_verify_fn) @@ -732,27 +809,22 @@ def test_cli_build_test_execute( "_VerificationTempDir", _mock_temp_dir, ) - # Coloredlogs doesn't play nice with capsys + # Coloredlogs doesn't play nice with caplog monkeypatch.setattr( - smartsim._core._cli.validate.logger, - "error", - print, + smartsim._core._cli.validate, + "logger", + _TEST_LOGGER, ) command = "validate" - cfg = MenuItemConfig(command, - f"test {command} help text", - validate_execute, - validate_parser) + cfg = MenuItemConfig( + command, f"test {command} help text", validate_execute, validate_parser + ) menu = [cfg] smart_cli = cli.SmartCli(menu) - captured = capsys.readouterr() # throw away existing output - verify_args = ["smart", command] actual_retval = smart_cli.execute(verify_args) - captured = capsys.readouterr() # capture new output - - assert expected_stdout in captured.out + assert expected_stdout in caplog.text assert actual_retval == expected_retval diff --git a/tests/test_cobalt_parser.py b/tests/test_cobalt_parser.py index 16f8c0b81..e91c95100 100644 --- a/tests/test_cobalt_parser.py +++ b/tests/test_cobalt_parser.py @@ -24,9 +24,13 @@ # 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. +import pytest from smartsim._core.launcher.cobalt import cobaltParser +# The tests in this file belong to the group_a group +pytestmark = pytest.mark.group_a + def test_parse_step_id(): output = "JobName JobId \n" "=====================\n" "smartsim 507975 \n" diff --git a/tests/test_colo_model_local.py b/tests/test_colo_model_local.py index 376c71f26..e68801762 100644 --- a/tests/test_colo_model_local.py +++ b/tests/test_colo_model_local.py @@ -29,84 +29,103 @@ import pytest from smartsim import Experiment, status -from smartsim.error import SSUnsupportedError from smartsim.entity import Model +from smartsim.error import SSUnsupportedError + +# The tests in this file belong to the slow_tests group +pytestmark = pytest.mark.slow_tests + if sys.platform == "darwin": supported_dbs = ["tcp", "deprecated"] else: supported_dbs = ["uds", "tcp", "deprecated"] -is_mac = sys.platform == 'darwin' +is_mac = sys.platform == "darwin" + -@pytest.mark.skipif(not is_mac, reason='MacOS-only test') -def test_macosx_warning(fileutils, coloutils): - db_args = {"custom_pinning":[1]} - db_type = 'uds' # Test is insensitive to choice of db +@pytest.mark.skipif(not is_mac, reason="MacOS-only test") +def test_macosx_warning(fileutils, test_dir, coloutils): + db_args = {"custom_pinning": [1]} + db_type = "uds" # Test is insensitive to choice of db - exp = Experiment("colocated_model_defaults", launcher="local") + exp = Experiment("colocated_model_defaults", launcher="local", exp_path=test_dir) with pytest.warns( RuntimeWarning, - match="CPU pinning is not supported on MacOSX. Ignoring pinning specification." + match="CPU pinning is not supported on MacOSX. Ignoring pinning specification.", ): - colo_model = coloutils.setup_test_colo( + _ = coloutils.setup_test_colo( fileutils, db_type, exp, + "send_data_local_smartredis.py", db_args, ) -def test_unsupported_limit_app(fileutils, coloutils): - db_args = {"limit_app_cpus":True} - db_type = 'uds' # Test is insensitive to choice of db - exp = Experiment("colocated_model_defaults", launcher="local") +def test_unsupported_limit_app(fileutils, test_dir, coloutils): + db_args = {"limit_app_cpus": True} + db_type = "uds" # Test is insensitive to choice of db + + exp = Experiment("colocated_model_defaults", launcher="local", exp_path=test_dir) with pytest.raises(SSUnsupportedError): - colo_model = coloutils.setup_test_colo( + coloutils.setup_test_colo( fileutils, db_type, exp, + "send_data_local_smartredis.py", db_args, ) + @pytest.mark.skipif(is_mac, reason="Unsupported on MacOSX") -@pytest.mark.parametrize("custom_pinning", [1,"10","#",1.,['a'],[1.]]) -def test_unsupported_custom_pinning(fileutils, coloutils, custom_pinning): - db_type = "uds" # Test is insensitive to choice of db +@pytest.mark.parametrize("custom_pinning", [1, "10", "#", 1.0, ["a"], [1.0]]) +def test_unsupported_custom_pinning(fileutils, test_dir, coloutils, custom_pinning): + db_type = "uds" # Test is insensitive to choice of db db_args = {"custom_pinning": custom_pinning} - exp = Experiment("colocated_model_defaults", launcher="local") + exp = Experiment("colocated_model_defaults", launcher="local", exp_path=test_dir) with pytest.raises(TypeError): - colo_model = coloutils.setup_test_colo( + coloutils.setup_test_colo( fileutils, db_type, exp, + "send_data_local_smartredis.py", db_args, ) + @pytest.mark.skipif(is_mac, reason="Unsupported on MacOSX") -@pytest.mark.parametrize("pin_list, num_cpus, expected", [ - pytest.param(None, 2, "0,1", id="Automatic creation of pinned cpu list"), - pytest.param([1,2], 2, "1,2", id="Individual ids only"), - pytest.param([range(2),3], 3, "0,1,3", id="Mixed ranges and individual ids"), - pytest.param(range(3), 3, "0,1,2", id="Range only"), - pytest.param([range(8, 10), range(6, 1, -2)], 4, "2,4,6,8,9", id="Multiple ranges"), -]) +@pytest.mark.parametrize( + "pin_list, num_cpus, expected", + [ + pytest.param(None, 2, "0,1", id="Automatic creation of pinned cpu list"), + pytest.param([1, 2], 2, "1,2", id="Individual ids only"), + pytest.param([range(2), 3], 3, "0,1,3", id="Mixed ranges and individual ids"), + pytest.param(range(3), 3, "0,1,2", id="Range only"), + pytest.param( + [range(8, 10), range(6, 1, -2)], 4, "2,4,6,8,9", id="Multiple ranges" + ), + ], +) def test_create_pinning_string(pin_list, num_cpus, expected): assert Model._create_pinning_string(pin_list, num_cpus) == expected @pytest.mark.parametrize("db_type", supported_dbs) -def test_launch_colocated_model_defaults(fileutils, coloutils, db_type, launcher="local"): +def test_launch_colocated_model_defaults( + fileutils, test_dir, coloutils, db_type, launcher="local" +): """Test the launch of a model with a colocated database and local launcher""" - db_args = { } + db_args = {} - exp = Experiment("colocated_model_defaults", launcher=launcher) + exp = Experiment("colocated_model_defaults", launcher=launcher, exp_path=test_dir) colo_model = coloutils.setup_test_colo( fileutils, db_type, exp, + "send_data_local_smartredis.py", db_args, ) @@ -114,20 +133,69 @@ def test_launch_colocated_model_defaults(fileutils, coloutils, db_type, launcher true_pinning = None else: true_pinning = "0" - assert colo_model.run_settings.colocated_db_settings["custom_pinning"] == true_pinning + assert ( + colo_model.run_settings.colocated_db_settings["custom_pinning"] == true_pinning + ) + exp.generate(colo_model) exp.start(colo_model, block=True) statuses = exp.get_status(colo_model) - assert all([stat == status.STATUS_COMPLETED for stat in statuses]) + assert all(stat == status.STATUS_COMPLETED for stat in statuses) # test restarting the colocated model exp.start(colo_model, block=True) statuses = exp.get_status(colo_model) - assert all([stat == status.STATUS_COMPLETED for stat in statuses]) + assert all( + stat == status.STATUS_COMPLETED for stat in statuses + ), f"Statuses {statuses}" + @pytest.mark.parametrize("db_type", supported_dbs) -def test_colocated_model_disable_pinning(fileutils, coloutils, db_type, launcher="local"): +def test_launch_multiple_colocated_models( + fileutils, test_dir, coloutils, wlmutils, db_type, launcher="local" +): + """Test the concurrent launch of two models with a colocated database and local launcher""" + + db_args = {} + + exp = Experiment("multi_colo_models", launcher=launcher, exp_path=test_dir) + colo_models = [ + coloutils.setup_test_colo( + fileutils, + db_type, + exp, + "send_data_local_smartredis.py", + db_args, + colo_model_name="colo0", + port=wlmutils.get_test_port(), + ), + coloutils.setup_test_colo( + fileutils, + db_type, + exp, + "send_data_local_smartredis.py", + db_args, + colo_model_name="colo1", + port=wlmutils.get_test_port() + 1, + ), + ] + exp.generate(*colo_models) + exp.start(*colo_models, block=True) + statuses = exp.get_status(*colo_models) + assert all(stat == status.STATUS_COMPLETED for stat in statuses) + + # test restarting the colocated model + exp.start(*colo_models, block=True) + statuses = exp.get_status(*colo_models) + assert all([stat == status.STATUS_COMPLETED for stat in statuses]) + - exp = Experiment("colocated_model_pinning_auto_1cpu", launcher=launcher) +@pytest.mark.parametrize("db_type", supported_dbs) +def test_colocated_model_disable_pinning( + fileutils, test_dir, coloutils, db_type, launcher="local" +): + exp = Experiment( + "colocated_model_pinning_auto_1cpu", launcher=launcher, exp_path=test_dir + ) db_args = { "db_cpus": 1, "custom_pinning": [], @@ -137,17 +205,23 @@ def test_colocated_model_disable_pinning(fileutils, coloutils, db_type, launcher fileutils, db_type, exp, + "send_data_local_smartredis.py", db_args, ) assert colo_model.run_settings.colocated_db_settings["custom_pinning"] is None + exp.generate(colo_model) exp.start(colo_model, block=True) statuses = exp.get_status(colo_model) assert all([stat == status.STATUS_COMPLETED for stat in statuses]) -@pytest.mark.parametrize("db_type", supported_dbs) -def test_colocated_model_pinning_auto_2cpu(fileutils, coloutils, db_type, launcher="local"): - exp = Experiment("colocated_model_pinning_auto_2cpu", launcher=launcher) +@pytest.mark.parametrize("db_type", supported_dbs) +def test_colocated_model_pinning_auto_2cpu( + fileutils, test_dir, coloutils, db_type, launcher="local" +): + exp = Experiment( + "colocated_model_pinning_auto_2cpu", launcher=launcher, exp_path=test_dir + ) db_args = { "db_cpus": 2, @@ -158,59 +232,82 @@ def test_colocated_model_pinning_auto_2cpu(fileutils, coloutils, db_type, launch fileutils, db_type, exp, + "send_data_local_smartredis.py", db_args, ) if is_mac: true_pinning = None else: true_pinning = "0,1" - assert colo_model.run_settings.colocated_db_settings["custom_pinning"] == true_pinning + assert ( + colo_model.run_settings.colocated_db_settings["custom_pinning"] == true_pinning + ) + exp.generate(colo_model) exp.start(colo_model, block=True) statuses = exp.get_status(colo_model) assert all([stat == status.STATUS_COMPLETED for stat in statuses]) + @pytest.mark.skipif(is_mac, reason="unsupported on MacOSX") @pytest.mark.parametrize("db_type", supported_dbs) -def test_colocated_model_pinning_range(fileutils, coloutils, db_type, launcher="local"): +def test_colocated_model_pinning_range( + fileutils, test_dir, coloutils, db_type, launcher="local" +): # Check to make sure that the CPU mask was correctly generated - exp = Experiment("colocated_model_pinning_manual", launcher=launcher) + exp = Experiment( + "colocated_model_pinning_manual", launcher=launcher, exp_path=test_dir + ) - db_args = { - "db_cpus": 2, - "custom_pinning": range(2) - } + db_args = {"db_cpus": 2, "custom_pinning": range(2)} colo_model = coloutils.setup_test_colo( fileutils, db_type, exp, + "send_data_local_smartredis.py", db_args, ) assert colo_model.run_settings.colocated_db_settings["custom_pinning"] == "0,1" + exp.generate(colo_model) exp.start(colo_model, block=True) statuses = exp.get_status(colo_model) assert all([stat == status.STATUS_COMPLETED for stat in statuses]) + @pytest.mark.skipif(is_mac, reason="unsupported on MacOSX") @pytest.mark.parametrize("db_type", supported_dbs) -def test_colocated_model_pinning_list(fileutils, coloutils, db_type, launcher="local"): +def test_colocated_model_pinning_list( + fileutils, test_dir, coloutils, db_type, launcher="local" +): # Check to make sure that the CPU mask was correctly generated - exp = Experiment("colocated_model_pinning_manual", launcher=launcher) + exp = Experiment( + "colocated_model_pinning_manual", launcher=launcher, exp_path=test_dir + ) - db_args = { - "db_cpus": 1, - "custom_pinning": [1] - } + db_args = {"db_cpus": 1, "custom_pinning": [1]} colo_model = coloutils.setup_test_colo( fileutils, db_type, exp, + "send_data_local_smartredis.py", db_args, ) assert colo_model.run_settings.colocated_db_settings["custom_pinning"] == "1" + exp.generate(colo_model) exp.start(colo_model, block=True) statuses = exp.get_status(colo_model) - assert all([stat == status.STATUS_COMPLETED for stat in statuses]) \ No newline at end of file + assert all([stat == status.STATUS_COMPLETED for stat in statuses]) + + +def test_colo_uds_verifies_socket_file_name(test_dir, launcher="local"): + exp = Experiment(f"colo_uds_wrong_name", launcher=launcher, exp_path=test_dir) + + colo_settings = exp.create_run_settings(exe=sys.executable, exe_args=["--version"]) + + colo_model = exp.create_model("wrong_uds_socket_name", colo_settings) + + with pytest.raises(ValueError): + colo_model.colocate_db_uds(unix_socket="this is not a valid name!") diff --git a/tests/test_colo_model_lsf.py b/tests/test_colo_model_lsf.py index 7c4fd0e01..e77eeedec 100644 --- a/tests/test_colo_model_lsf.py +++ b/tests/test_colo_model_lsf.py @@ -28,17 +28,21 @@ import pytest +import smartsim.settings.base from smartsim import Experiment from smartsim.entity import Model -import smartsim.settings.base from smartsim.settings.lsfSettings import JsrunSettings +# The tests in this file belong to the group_a group +pytestmark = pytest.mark.group_a -is_mac = sys.platform == 'darwin' + +is_mac = sys.platform == "darwin" class ExpectationMet(Exception): """Use this to break a test when we verify a call path is achieved""" + ... @@ -52,8 +56,8 @@ def test_jsrun_prep(fileutils, coloutils, monkeypatch): # mock the prep method to raise an exception that short circuits test when goal is met monkeypatch.setattr(JsrunSettings, "_prep_colocated_db", show_expectation_met) - db_args = {"custom_pinning":[1]} - db_type = 'uds' # Test is insensitive to choice of db + db_args = {"custom_pinning": [1]} + db_type = "uds" # Test is insensitive to choice of db exp = Experiment("colocated_model_lsf", launcher="lsf") @@ -64,19 +68,20 @@ def test_jsrun_prep(fileutils, coloutils, monkeypatch): fileutils, db_type, exp, + "send_data_local_smartredis.py", db_args, - colo_settings = run_settings, + colo_settings=run_settings, ) def test_non_js_run_prep(fileutils, coloutils, monkeypatch): - """Ensure that RunSettings does not attempt to call a prep method""" + """Ensure that RunSettings does not attempt to call a prep method""" monkeypatch.setattr(smartsim.settings.base, "expand_exe_path", lambda x: "/bin/{x}") # mock prep method to ensure that the exception isn't thrown w/non-JsrunSettings arg monkeypatch.setattr(JsrunSettings, "_prep_colocated_db", show_expectation_met) - db_args = {"custom_pinning":[1]} - db_type = 'tcp' # Test is insensitive to choice of db + db_args = {"custom_pinning": [1]} + db_type = "tcp" # Test is insensitive to choice of db exp = Experiment("colocated_model_lsf", launcher="lsf") @@ -86,31 +91,42 @@ def test_non_js_run_prep(fileutils, coloutils, monkeypatch): fileutils, db_type, exp, + "send_data_local_smartredis.py", db_args, - colo_settings = run_settings, + colo_settings=run_settings, ) assert colo_model @pytest.mark.parametrize( - "exp_run_arg_key,run_arg_key,exp_value,test_value", - [ - pytest.param("cpu_per_rs", "cpu_per_rs", 11, 11, id="cpu_per_rs matches input"), - pytest.param("c", "c", 22, 22, id="c matches input"), - pytest.param("cpu_per_rs", "cpu_per_rsx", 1, 33, id="key typo: cpu_per_rsx gives default"), - pytest.param("cpu_per_rs", "cx", 1, 44, id="key typo: cx gives default"), - ] + "exp_run_arg_key,run_arg_key,exp_value,test_value", + [ + pytest.param("cpu_per_rs", "cpu_per_rs", 11, 11, id="cpu_per_rs matches input"), + pytest.param("c", "c", 22, 22, id="c matches input"), + pytest.param( + "cpu_per_rs", "cpu_per_rsx", 1, 33, id="key typo: cpu_per_rsx gives default" + ), + pytest.param("cpu_per_rs", "cx", 1, 44, id="key typo: cx gives default"), + ], ) -def test_jsrun_prep_cpu_per_flag_set_check(fileutils, coloutils, monkeypatch, exp_run_arg_key, run_arg_key, exp_value, test_value): +def test_jsrun_prep_cpu_per_flag_set_check( + fileutils, + coloutils, + monkeypatch, + exp_run_arg_key, + run_arg_key, + exp_value, + test_value, +): """Ensure that _prep_colocated_db honors basic cpu_per_rs config and allows a - valid input parameter to result in the correct output. If no expected input (or + valid input parameter to result in the correct output. If no expected input (or incorrect key) is given, the default should be returned using default config key""" monkeypatch.setattr(smartsim.settings.base, "expand_exe_path", lambda x: "/bin/{x}") # excluding "db_cpus" should result in default value in comparison & output - db_args = {"custom_pinning":[1]} - db_type = 'uds' # Test is insensitive to choice of db + db_args = {"custom_pinning": [1]} + db_type = "uds" # Test is insensitive to choice of db exp = Experiment("colocated_model_lsf", launcher="lsf") @@ -121,29 +137,42 @@ def test_jsrun_prep_cpu_per_flag_set_check(fileutils, coloutils, monkeypatch, ex fileutils, db_type, exp, + "send_data_local_smartredis.py", db_args, - colo_settings = run_settings, + colo_settings=run_settings, ) assert colo_model.run_settings.run_args[exp_run_arg_key] == exp_value @pytest.mark.parametrize( - "exp_run_arg_key,run_arg_key,exp_value,test_value", - [ - pytest.param("cpu_per_rs", "cpu_per_rs", 11, 11, id="cpu_per_rs matches input"), - pytest.param("c", "c", 22, 22, id="c matches input"), - pytest.param("cpu_per_rs", "cpu_per_rsx", 3, 33, id="key typo: db_cpus out (not default)"), - pytest.param("cpu_per_rs", "cx", 3, 44, id="key typo: get db_cpus out (not default)"), - ] + "exp_run_arg_key,run_arg_key,exp_value,test_value", + [ + pytest.param("cpu_per_rs", "cpu_per_rs", 11, 11, id="cpu_per_rs matches input"), + pytest.param("c", "c", 22, 22, id="c matches input"), + pytest.param( + "cpu_per_rs", "cpu_per_rsx", 3, 33, id="key typo: db_cpus out (not default)" + ), + pytest.param( + "cpu_per_rs", "cx", 3, 44, id="key typo: get db_cpus out (not default)" + ), + ], ) -def test_jsrun_prep_db_cpu_override(fileutils, coloutils, monkeypatch, exp_run_arg_key, run_arg_key, exp_value, test_value): +def test_jsrun_prep_db_cpu_override( + fileutils, + coloutils, + monkeypatch, + exp_run_arg_key, + run_arg_key, + exp_value, + test_value, +): """Ensure that both cpu_per_rs and c input config override db_cpus""" monkeypatch.setattr(smartsim.settings.base, "expand_exe_path", lambda x: "/bin/{x}") # setting "db_cpus" should result in non-default value in comparison & output - db_args = {"custom_pinning":[1], "db_cpus": 3} - db_type = 'tcp' # Test is insensitive to choice of db + db_args = {"custom_pinning": [1], "db_cpus": 3} + db_type = "tcp" # Test is insensitive to choice of db exp = Experiment("colocated_model_lsf", launcher="lsf") @@ -154,29 +183,40 @@ def test_jsrun_prep_db_cpu_override(fileutils, coloutils, monkeypatch, exp_run_a fileutils, db_type, exp, + "send_data_local_smartredis.py", db_args, - colo_settings = run_settings, + colo_settings=run_settings, ) assert colo_model.run_settings.run_args[exp_run_arg_key] == exp_value @pytest.mark.parametrize( - "exp_run_arg_key,run_arg_key,exp_value,test_value", - [ - pytest.param("cpu_per_rs", "cpu_per_rs", 8, 3, id="cpu_per_rs swaps to db_cpus"), - pytest.param("c", "c", 8, 4, id="c swaps to db_cpus"), - pytest.param("cpu_per_rs", "cpu_per_rsx", 8, 5, id="key typo: db_cpus out"), - pytest.param("cpu_per_rs", "cx", 8, 6, id="key typo: get db_cpus out"), - ] + "exp_run_arg_key,run_arg_key,exp_value,test_value", + [ + pytest.param( + "cpu_per_rs", "cpu_per_rs", 8, 3, id="cpu_per_rs swaps to db_cpus" + ), + pytest.param("c", "c", 8, 4, id="c swaps to db_cpus"), + pytest.param("cpu_per_rs", "cpu_per_rsx", 8, 5, id="key typo: db_cpus out"), + pytest.param("cpu_per_rs", "cx", 8, 6, id="key typo: get db_cpus out"), + ], ) -def test_jsrun_prep_db_cpu_replacement(fileutils, coloutils, monkeypatch, exp_run_arg_key, run_arg_key, exp_value, test_value): +def test_jsrun_prep_db_cpu_replacement( + fileutils, + coloutils, + monkeypatch, + exp_run_arg_key, + run_arg_key, + exp_value, + test_value, +): """Ensure that db_cpus default is used if user config suggests underutilizing resources""" monkeypatch.setattr(smartsim.settings.base, "expand_exe_path", lambda x: "/bin/{x}") # setting "db_cpus" should result in non-default value in comparison & output - db_args = {"custom_pinning":[1], "db_cpus": 8} - db_type = 'uds' # Test is insensitive to choice of db + db_args = {"custom_pinning": [1], "db_cpus": 8} + db_type = "uds" # Test is insensitive to choice of db exp = Experiment("colocated_model_lsf", launcher="lsf") @@ -187,31 +227,46 @@ def test_jsrun_prep_db_cpu_replacement(fileutils, coloutils, monkeypatch, exp_ru fileutils, db_type, exp, + "send_data_local_smartredis.py", db_args, - colo_settings = run_settings, + colo_settings=run_settings, ) assert colo_model.run_settings.run_args[exp_run_arg_key] == exp_value @pytest.mark.parametrize( - "exp_run_arg_key,run_arg_key,exp_value,test_value", - [ - pytest.param("rs_per_host", "rs_per_host", 1, 1, id="rs_per_host is 1"), - pytest.param("r", "r", 1, 1, id="r is 1"), - pytest.param("rs_per_host", "rs_per_host", 1, 2, id="rs_per_host replaced w/1"), - pytest.param("r", "r", 1, 3, id="r replaced w/1"), - pytest.param("rs_per_host", "rs_per_hostx", 1, 4, id="key typo: rs_per_hostx gets default"), - pytest.param("rs_per_host", "rx", 1, 5, id="key typo: rx gets default"), - ] + "exp_run_arg_key,run_arg_key,exp_value,test_value", + [ + pytest.param("rs_per_host", "rs_per_host", 1, 1, id="rs_per_host is 1"), + pytest.param("r", "r", 1, 1, id="r is 1"), + pytest.param("rs_per_host", "rs_per_host", 1, 2, id="rs_per_host replaced w/1"), + pytest.param("r", "r", 1, 3, id="r replaced w/1"), + pytest.param( + "rs_per_host", + "rs_per_hostx", + 1, + 4, + id="key typo: rs_per_hostx gets default", + ), + pytest.param("rs_per_host", "rx", 1, 5, id="key typo: rx gets default"), + ], ) -def test_jsrun_prep_rs_per_host(fileutils, coloutils, monkeypatch, exp_run_arg_key, run_arg_key, exp_value, test_value): +def test_jsrun_prep_rs_per_host( + fileutils, + coloutils, + monkeypatch, + exp_run_arg_key, + run_arg_key, + exp_value, + test_value, +): """Ensure that resource-per-host settings are configured and are modified as required to meet limitations (e.g. rs_per_host MUST equal 1)""" monkeypatch.setattr(smartsim.settings.base, "expand_exe_path", lambda x: "/bin/{x}") - db_args = {"custom_pinning":[1]} - db_type = 'tcp' # Test is insensitive to choice of db + db_args = {"custom_pinning": [1]} + db_type = "tcp" # Test is insensitive to choice of db exp = Experiment("colocated_model_lsf", launcher="lsf") @@ -222,8 +277,9 @@ def test_jsrun_prep_rs_per_host(fileutils, coloutils, monkeypatch, exp_run_arg_k fileutils, db_type, exp, + "send_data_local_smartredis.py", db_args, - colo_settings = run_settings, + colo_settings=run_settings, ) # NOTE: _prep_colocated_db sets this to a string & not an integer diff --git a/tests/test_config.py b/tests/test_config.py index 5e28202b1..bbbb54526 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -25,17 +25,18 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +import os +import typing as t from pathlib import Path -import os -from re import A, L import pytest -import shutil -import typing as t from smartsim._core.config.config import Config from smartsim.error import SSConfigError +# The tests in this file belong to the group_a group +pytestmark = pytest.mark.group_a + def test_all_config_defaults(): config = Config() @@ -54,8 +55,10 @@ def test_all_config_defaults(): config.test_device -def get_redisai_env(rai_path: t.Optional[str], lib_path: t.Optional[str]) -> t.Dict[str, str]: - """Convenience method to create a set of environment variables +def get_redisai_env( + rai_path: t.Optional[str], lib_path: t.Optional[str] +) -> t.Dict[str, str]: + """Convenience method to create a set of environment variables that include RedisAI-specific variables :param rai_path: The path to the RedisAI library :type: str (optional) @@ -68,21 +71,26 @@ def get_redisai_env(rai_path: t.Optional[str], lib_path: t.Optional[str]) -> t.D env["RAI_PATH"] = rai_path else: env.pop("RAI_PATH", None) - + if lib_path is not None: env["SMARTSIM_DEP_INSTALL_PATH"] = lib_path else: env.pop("SMARTSIM_DEP_INSTALL_PATH", None) - + return env -def test_redisai_invalid_rai_path(fileutils, monkeypatch): +def make_file(filepath: str) -> None: + os.makedirs(os.path.dirname(filepath)) + with open(filepath, "w+", encoding="utf-8") as dummy_file: + dummy_file.write("dummy\n") + + +def test_redisai_invalid_rai_path(test_dir, monkeypatch): """An invalid RAI_PATH and valid SMARTSIM_DEP_INSTALL_PATH should fail""" - test_dir = fileutils.make_test_dir() - rai_file_path = os.path.join(test_dir, "lib", "mock-redisai.so") - _ = fileutils.make_test_file("redisai.so", "lib") + rai_file_path = os.path.join(test_dir, "lib", "mock-redisai.so") + make_file(os.path.join(test_dir, "lib", "redisai.so")) env = get_redisai_env(rai_file_path, test_dir) monkeypatch.setattr(os, "environ", env) @@ -92,14 +100,14 @@ def test_redisai_invalid_rai_path(fileutils, monkeypatch): with pytest.raises(SSConfigError) as ex: _ = config.redisai - assert 'RedisAI dependency not found' in ex.value.args[0] + assert "RedisAI dependency not found" in ex.value.args[0] -def test_redisai_valid_rai_path(fileutils, monkeypatch): +def test_redisai_valid_rai_path(test_dir, monkeypatch): """A valid RAI_PATH should override valid SMARTSIM_DEP_INSTALL_PATH and succeed""" - test_dir = fileutils.make_test_dir() - rai_file_path = fileutils.make_test_file("mock-redisai.so", "lib") - _ = fileutils.make_test_file("redisai.so", "deps") + + rai_file_path = os.path.join(test_dir, "lib", "mock-redisai.so") + make_file(rai_file_path) env = get_redisai_env(rai_file_path, test_dir) monkeypatch.setattr(os, "environ", env) @@ -110,45 +118,45 @@ def test_redisai_valid_rai_path(fileutils, monkeypatch): assert config.redisai == rai_file_path -def test_redisai_invalid_lib_path(fileutils, monkeypatch): +def test_redisai_invalid_lib_path(test_dir, monkeypatch): """Invalid RAI_PATH and invalid SMARTSIM_DEP_INSTALL_PATH should fail""" - test_dir = fileutils.make_test_dir() + rai_file_path = f"{test_dir}/railib/redisai.so" env = get_redisai_env(rai_file_path, test_dir) monkeypatch.setattr(os, "environ", env) - + config = Config() # Fail when no files exist @ either location with pytest.raises(SSConfigError) as ex: _ = config.redisai - assert 'RedisAI dependency not found' in ex.value.args[0] + assert "RedisAI dependency not found" in ex.value.args[0] -def test_redisai_valid_lib_path(fileutils, monkeypatch): +def test_redisai_valid_lib_path(test_dir, monkeypatch): """Valid RAI_PATH and invalid SMARTSIM_DEP_INSTALL_PATH should succeed""" - test_dir = fileutils.make_test_dir() - rai_file_path = fileutils.make_test_file("mock-redisai.so", "lib") + rai_file_path = os.path.join(test_dir, "lib", "mock-redisai.so") + make_file(rai_file_path) env = get_redisai_env(rai_file_path, test_dir) monkeypatch.setattr(os, "environ", env) - + config = Config() assert config.redisai assert Path(config.redisai).is_file() assert config.redisai == rai_file_path -def test_redisai_valid_lib_path_null_rai(fileutils, monkeypatch): +def test_redisai_valid_lib_path_null_rai(test_dir, monkeypatch): """Missing RAI_PATH and valid SMARTSIM_DEP_INSTALL_PATH should succeed""" - test_dir = fileutils.make_test_dir() - rai_file_path: t.Optional[str] = None - lib_file_path = fileutils.make_test_file("redisai.so", "lib") + rai_file_path: t.Optional[str] = None + lib_file_path = os.path.join(test_dir, "lib", "redisai.so") + make_file(lib_file_path) env = get_redisai_env(rai_file_path, test_dir) monkeypatch.setattr(os, "environ", env) - + config = Config() assert config.redisai assert Path(config.redisai).is_file() @@ -189,3 +197,61 @@ def test_redis_cli(): with pytest.raises(SSConfigError): config.database_cli os.environ.pop("REDIS_CLI_PATH") + + +@pytest.mark.parametrize( + "value, exp_result", + [ + pytest.param("0", False, id="letter zero"), + pytest.param("1", True, id="letter one"), + pytest.param("-1", False, id="letter negative one"), + pytest.param(None, False, id="not in env"), + ], +) +def test_telemetry_flag( + monkeypatch: pytest.MonkeyPatch, value: t.Optional[str], exp_result: bool +): + if value is not None: + monkeypatch.setenv("SMARTSIM_FLAG_TELEMETRY", value) + else: + monkeypatch.delenv("SMARTSIM_FLAG_TELEMETRY", raising=False) + config = Config() + assert config.telemetry_enabled == exp_result + + +@pytest.mark.parametrize( + "value, exp_result", + [ + pytest.param("1", 1, id="1"), + pytest.param("123", 123, id="123"), + pytest.param(None, 5, id="not in env"), + ], +) +def test_telemetry_frequency( + monkeypatch: pytest.MonkeyPatch, value: t.Optional[str], exp_result: int +): + if value is not None: + monkeypatch.setenv("SMARTSIM_TELEMETRY_FREQUENCY", value) + else: + monkeypatch.delenv("SMARTSIM_TELEMETRY_FREQUENCY", raising=False) + config = Config() + assert config.telemetry_frequency == exp_result + + +@pytest.mark.parametrize( + "value, exp_result", + [ + pytest.param("30", 30, id="30"), + pytest.param("123", 123, id="123"), + pytest.param(None, 90, id="not in env"), + ], +) +def test_telemetry_cooldown( + monkeypatch: pytest.MonkeyPatch, value: t.Optional[str], exp_result: bool +): + if value is not None: + monkeypatch.setenv("SMARTSIM_TELEMETRY_COOLDOWN", value) + else: + monkeypatch.delenv("SMARTSIM_TELEMETRY_COOLDOWN", raising=False) + config = Config() + assert config.telemetry_cooldown == exp_result diff --git a/tests/test_configs/bad.py b/tests/test_configs/bad.py index 8eff7e2cb..93e4864ff 100644 --- a/tests/test_configs/bad.py +++ b/tests/test_configs/bad.py @@ -35,7 +35,6 @@ def divide_by_zero(time_to_wait): if __name__ == "__main__": - parser = argparse.ArgumentParser() parser.add_argument("--time", type=int, default=0) args = parser.parse_args() diff --git a/tests/test_configs/check_env.py b/tests/test_configs/check_env.py index 72a2107ea..fd706ee83 100644 --- a/tests/test_configs/check_env.py +++ b/tests/test_configs/check_env.py @@ -8,4 +8,4 @@ print(f"{var_name}=={env_value}") sys.exit(0) -print('env var not found') +print("env var not found") diff --git a/tests/test_configs/cov/cobalt_cov.cfg b/tests/test_configs/cov/cobalt_cov.cfg index 84e727b68..1d02e9f52 100644 --- a/tests/test_configs/cov/cobalt_cov.cfg +++ b/tests/test_configs/cov/cobalt_cov.cfg @@ -24,6 +24,9 @@ exclude_lines= raise AssertionError raise NotImplementedError + # Don't complain about type-checking only blocks + if (t(yping)?\.)?TYPE_CHECKING: + # Don't complain if non-runnable code isn't run: if 0: if __name__ == .__main__.: diff --git a/tests/test_configs/cov/local_cov.cfg b/tests/test_configs/cov/local_cov.cfg index cb922f98e..d25e9f83a 100644 --- a/tests/test_configs/cov/local_cov.cfg +++ b/tests/test_configs/cov/local_cov.cfg @@ -6,6 +6,7 @@ omit = *mpirun* *alps* *lsf* + *pals* *redis_starter.py* */_cli/* */_install/* @@ -25,6 +26,9 @@ exclude_lines= raise AssertionError raise NotImplementedError + # Don't complain about type-checking only blocks + if (t(yping)?\.)?TYPE_CHECKING: + # Don't complain if non-runnable code isn't run: if 0: if __name__ == .__main__.: @@ -47,3 +51,4 @@ exclude_lines= launcher == "pbs" launcher == "cobalt" launcher == "lsf" + launcher == "pals" diff --git a/tests/test_configs/cov/lsf_cov.cfg b/tests/test_configs/cov/lsf_cov.cfg index fb0cb033e..6e5f52eb4 100644 --- a/tests/test_configs/cov/lsf_cov.cfg +++ b/tests/test_configs/cov/lsf_cov.cfg @@ -24,6 +24,9 @@ exclude_lines= raise AssertionError raise NotImplementedError + # Don't complain about type-checking only blocks + if (t(yping)?\.)?TYPE_CHECKING: + # Don't complain if non-runnable code isn't run: if 0: if __name__ == .__main__.: diff --git a/tests/test_configs/cov/pbs_cov.cfg b/tests/test_configs/cov/pbs_cov.cfg index 275c46454..99e7bcfd6 100644 --- a/tests/test_configs/cov/pbs_cov.cfg +++ b/tests/test_configs/cov/pbs_cov.cfg @@ -25,6 +25,9 @@ exclude_lines= raise AssertionError raise NotImplementedError + # Don't complain about type-checking only blocks + if (t(yping)?\.)?TYPE_CHECKING: + # Don't complain if non-runnable code isn't run: if 0: if __name__ == .__main__.: diff --git a/tests/test_configs/cov/slurm_cov.cfg b/tests/test_configs/cov/slurm_cov.cfg index edbafcda9..59405bc35 100644 --- a/tests/test_configs/cov/slurm_cov.cfg +++ b/tests/test_configs/cov/slurm_cov.cfg @@ -25,6 +25,9 @@ exclude_lines= raise AssertionError raise NotImplementedError + # Don't complain about type-checking only blocks + if (t(yping)?\.)?TYPE_CHECKING: + # Don't complain if non-runnable code isn't run: if 0: if __name__ == .__main__.: diff --git a/tests/test_configs/echo.py b/tests/test_configs/echo.py new file mode 100644 index 000000000..6523f4e4f --- /dev/null +++ b/tests/test_configs/echo.py @@ -0,0 +1,42 @@ +# BSD 2-Clause License +# +# Copyright (c) 2021-2023, 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. + +import argparse +import time + + +def echo(message: str, sleep_time: int): + if sleep_time > 0: + time.sleep(sleep_time) + print(f"Echoing: {message}") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("--message", type=str, default="Lorem ipsum") + parser.add_argument("--sleep_time", type=int, default=0) + args = parser.parse_args() + echo(args.message, args.sleep_time) diff --git a/tests/test_configs/h2o.inp b/tests/test_configs/generator_files/circular_config/h2o.inp similarity index 100% rename from tests/test_configs/h2o.inp rename to tests/test_configs/generator_files/circular_config/h2o.inp diff --git a/tests/test_configs/circular_config/sub_dir/circle b/tests/test_configs/generator_files/circular_config/sub_dir/circle similarity index 100% rename from tests/test_configs/circular_config/sub_dir/circle rename to tests/test_configs/generator_files/circular_config/sub_dir/circle diff --git a/tests/test_configs/circular_config/sub_dir/hello.sh b/tests/test_configs/generator_files/circular_config/sub_dir/hello.sh similarity index 100% rename from tests/test_configs/circular_config/sub_dir/hello.sh rename to tests/test_configs/generator_files/circular_config/sub_dir/hello.sh diff --git a/tests/test_configs/easy/correct/MOM_input b/tests/test_configs/generator_files/easy/correct/MOM_input similarity index 100% rename from tests/test_configs/easy/correct/MOM_input rename to tests/test_configs/generator_files/easy/correct/MOM_input diff --git a/tests/test_configs/easy/correct/example_input.i b/tests/test_configs/generator_files/easy/correct/example_input.i similarity index 100% rename from tests/test_configs/easy/correct/example_input.i rename to tests/test_configs/generator_files/easy/correct/example_input.i diff --git a/tests/test_configs/easy/correct/in.airebo b/tests/test_configs/generator_files/easy/correct/in.airebo similarity index 100% rename from tests/test_configs/easy/correct/in.airebo rename to tests/test_configs/generator_files/easy/correct/in.airebo diff --git a/tests/test_configs/easy/correct/in.atm b/tests/test_configs/generator_files/easy/correct/in.atm similarity index 100% rename from tests/test_configs/easy/correct/in.atm rename to tests/test_configs/generator_files/easy/correct/in.atm diff --git a/tests/test_configs/easy/correct/in.crack b/tests/test_configs/generator_files/easy/correct/in.crack similarity index 100% rename from tests/test_configs/easy/correct/in.crack rename to tests/test_configs/generator_files/easy/correct/in.crack diff --git a/tests/test_configs/easy/correct/in.ellipse.gayberne b/tests/test_configs/generator_files/easy/correct/in.ellipse.gayberne similarity index 100% rename from tests/test_configs/easy/correct/in.ellipse.gayberne rename to tests/test_configs/generator_files/easy/correct/in.ellipse.gayberne diff --git a/tests/test_configs/easy/correct/input-file.inp b/tests/test_configs/generator_files/easy/correct/input-file.inp similarity index 100% rename from tests/test_configs/easy/correct/input-file.inp rename to tests/test_configs/generator_files/easy/correct/input-file.inp diff --git a/tests/test_configs/easy/correct/input.nml b/tests/test_configs/generator_files/easy/correct/input.nml similarity index 100% rename from tests/test_configs/easy/correct/input.nml rename to tests/test_configs/generator_files/easy/correct/input.nml diff --git a/tests/test_configs/easy/correct/simple-H20.xml b/tests/test_configs/generator_files/easy/correct/simple-H20.xml similarity index 100% rename from tests/test_configs/easy/correct/simple-H20.xml rename to tests/test_configs/generator_files/easy/correct/simple-H20.xml diff --git a/tests/test_configs/easy/marked/MOM_input b/tests/test_configs/generator_files/easy/marked/MOM_input similarity index 100% rename from tests/test_configs/easy/marked/MOM_input rename to tests/test_configs/generator_files/easy/marked/MOM_input diff --git a/tests/test_configs/easy/marked/example_input.i b/tests/test_configs/generator_files/easy/marked/example_input.i similarity index 100% rename from tests/test_configs/easy/marked/example_input.i rename to tests/test_configs/generator_files/easy/marked/example_input.i diff --git a/tests/test_configs/easy/marked/in.airebo b/tests/test_configs/generator_files/easy/marked/in.airebo similarity index 100% rename from tests/test_configs/easy/marked/in.airebo rename to tests/test_configs/generator_files/easy/marked/in.airebo diff --git a/tests/test_configs/easy/marked/in.atm b/tests/test_configs/generator_files/easy/marked/in.atm similarity index 100% rename from tests/test_configs/easy/marked/in.atm rename to tests/test_configs/generator_files/easy/marked/in.atm diff --git a/tests/test_configs/easy/marked/in.crack b/tests/test_configs/generator_files/easy/marked/in.crack similarity index 100% rename from tests/test_configs/easy/marked/in.crack rename to tests/test_configs/generator_files/easy/marked/in.crack diff --git a/tests/test_configs/easy/marked/in.ellipse.gayberne b/tests/test_configs/generator_files/easy/marked/in.ellipse.gayberne similarity index 100% rename from tests/test_configs/easy/marked/in.ellipse.gayberne rename to tests/test_configs/generator_files/easy/marked/in.ellipse.gayberne diff --git a/tests/test_configs/easy/marked/input-file.inp b/tests/test_configs/generator_files/easy/marked/input-file.inp similarity index 100% rename from tests/test_configs/easy/marked/input-file.inp rename to tests/test_configs/generator_files/easy/marked/input-file.inp diff --git a/tests/test_configs/easy/marked/input.nml b/tests/test_configs/generator_files/easy/marked/input.nml similarity index 100% rename from tests/test_configs/easy/marked/input.nml rename to tests/test_configs/generator_files/easy/marked/input.nml diff --git a/tests/test_configs/easy/marked/simple-H20.xml b/tests/test_configs/generator_files/easy/marked/simple-H20.xml similarity index 100% rename from tests/test_configs/easy/marked/simple-H20.xml rename to tests/test_configs/generator_files/easy/marked/simple-H20.xml diff --git a/tests/test_configs/in.atm b/tests/test_configs/generator_files/in.atm similarity index 100% rename from tests/test_configs/in.atm rename to tests/test_configs/generator_files/in.atm diff --git a/tests/test_configs/generator_files/log_params/dir_test/dir_test_0/smartsim_params.txt b/tests/test_configs/generator_files/log_params/dir_test/dir_test_0/smartsim_params.txt new file mode 100644 index 000000000..373cec87e --- /dev/null +++ b/tests/test_configs/generator_files/log_params/dir_test/dir_test_0/smartsim_params.txt @@ -0,0 +1,8 @@ +Model name: dir_test_0 +File name Parameters +-------------------------- --------------- +dir_test/dir_test_0/in.atm Name Value + ------ ------- + THERMO 10 + STEPS 10 + diff --git a/tests/test_configs/generator_files/log_params/dir_test/dir_test_1/smartsim_params.txt b/tests/test_configs/generator_files/log_params/dir_test/dir_test_1/smartsim_params.txt new file mode 100644 index 000000000..e45ebb6bf --- /dev/null +++ b/tests/test_configs/generator_files/log_params/dir_test/dir_test_1/smartsim_params.txt @@ -0,0 +1,8 @@ +Model name: dir_test_1 +File name Parameters +-------------------------- --------------- +dir_test/dir_test_1/in.atm Name Value + ------ ------- + THERMO 10 + STEPS 20 + diff --git a/tests/test_configs/generator_files/log_params/dir_test/dir_test_2/smartsim_params.txt b/tests/test_configs/generator_files/log_params/dir_test/dir_test_2/smartsim_params.txt new file mode 100644 index 000000000..081dc56c6 --- /dev/null +++ b/tests/test_configs/generator_files/log_params/dir_test/dir_test_2/smartsim_params.txt @@ -0,0 +1,8 @@ +Model name: dir_test_2 +File name Parameters +-------------------------- --------------- +dir_test/dir_test_2/in.atm Name Value + ------ ------- + THERMO 20 + STEPS 10 + diff --git a/tests/test_configs/generator_files/log_params/dir_test/dir_test_3/smartsim_params.txt b/tests/test_configs/generator_files/log_params/dir_test/dir_test_3/smartsim_params.txt new file mode 100644 index 000000000..3403f7c71 --- /dev/null +++ b/tests/test_configs/generator_files/log_params/dir_test/dir_test_3/smartsim_params.txt @@ -0,0 +1,8 @@ +Model name: dir_test_3 +File name Parameters +-------------------------- --------------- +dir_test/dir_test_3/in.atm Name Value + ------ ------- + THERMO 20 + STEPS 20 + diff --git a/tests/test_configs/generator_files/log_params/smartsim_params.txt b/tests/test_configs/generator_files/log_params/smartsim_params.txt new file mode 100644 index 000000000..6ac92049f --- /dev/null +++ b/tests/test_configs/generator_files/log_params/smartsim_params.txt @@ -0,0 +1,32 @@ +Generation start date and time: 08/09/2023 18:22:44 +Model name: dir_test_0 +File name Parameters +-------------------------- --------------- +dir_test/dir_test_0/in.atm Name Value + ------ ------- + THERMO 10 + STEPS 10 + +Model name: dir_test_1 +File name Parameters +-------------------------- --------------- +dir_test/dir_test_1/in.atm Name Value + ------ ------- + THERMO 10 + STEPS 20 + +Model name: dir_test_2 +File name Parameters +-------------------------- --------------- +dir_test/dir_test_2/in.atm Name Value + ------ ------- + THERMO 20 + STEPS 10 + +Model name: dir_test_3 +File name Parameters +-------------------------- --------------- +dir_test/dir_test_3/in.atm Name Value + ------ ------- + THERMO 20 + STEPS 20 diff --git a/tests/test_configs/med/correct/MOM_input b/tests/test_configs/generator_files/med/correct/MOM_input similarity index 100% rename from tests/test_configs/med/correct/MOM_input rename to tests/test_configs/generator_files/med/correct/MOM_input diff --git a/tests/test_configs/med/correct/diag_table b/tests/test_configs/generator_files/med/correct/diag_table similarity index 100% rename from tests/test_configs/med/correct/diag_table rename to tests/test_configs/generator_files/med/correct/diag_table diff --git a/tests/test_configs/med/correct/example_input.i b/tests/test_configs/generator_files/med/correct/example_input.i similarity index 100% rename from tests/test_configs/med/correct/example_input.i rename to tests/test_configs/generator_files/med/correct/example_input.i diff --git a/tests/test_configs/med/correct/in.airebo b/tests/test_configs/generator_files/med/correct/in.airebo similarity index 100% rename from tests/test_configs/med/correct/in.airebo rename to tests/test_configs/generator_files/med/correct/in.airebo diff --git a/tests/test_configs/med/correct/in.atm b/tests/test_configs/generator_files/med/correct/in.atm similarity index 100% rename from tests/test_configs/med/correct/in.atm rename to tests/test_configs/generator_files/med/correct/in.atm diff --git a/tests/test_configs/med/correct/in.crack b/tests/test_configs/generator_files/med/correct/in.crack similarity index 100% rename from tests/test_configs/med/correct/in.crack rename to tests/test_configs/generator_files/med/correct/in.crack diff --git a/tests/test_configs/med/correct/in.ellipse.gayberne b/tests/test_configs/generator_files/med/correct/in.ellipse.gayberne similarity index 100% rename from tests/test_configs/med/correct/in.ellipse.gayberne rename to tests/test_configs/generator_files/med/correct/in.ellipse.gayberne diff --git a/tests/test_configs/med/correct/input-file.inp b/tests/test_configs/generator_files/med/correct/input-file.inp similarity index 100% rename from tests/test_configs/med/correct/input-file.inp rename to tests/test_configs/generator_files/med/correct/input-file.inp diff --git a/tests/test_configs/med/correct/input.nml b/tests/test_configs/generator_files/med/correct/input.nml similarity index 100% rename from tests/test_configs/med/correct/input.nml rename to tests/test_configs/generator_files/med/correct/input.nml diff --git a/tests/test_configs/med/correct/simple-H20.xml b/tests/test_configs/generator_files/med/correct/simple-H20.xml similarity index 100% rename from tests/test_configs/med/correct/simple-H20.xml rename to tests/test_configs/generator_files/med/correct/simple-H20.xml diff --git a/tests/test_configs/med/marked/MOM_input b/tests/test_configs/generator_files/med/marked/MOM_input similarity index 100% rename from tests/test_configs/med/marked/MOM_input rename to tests/test_configs/generator_files/med/marked/MOM_input diff --git a/tests/test_configs/med/marked/diag_table b/tests/test_configs/generator_files/med/marked/diag_table similarity index 100% rename from tests/test_configs/med/marked/diag_table rename to tests/test_configs/generator_files/med/marked/diag_table diff --git a/tests/test_configs/med/marked/example_input.i b/tests/test_configs/generator_files/med/marked/example_input.i similarity index 100% rename from tests/test_configs/med/marked/example_input.i rename to tests/test_configs/generator_files/med/marked/example_input.i diff --git a/tests/test_configs/med/marked/in.airebo b/tests/test_configs/generator_files/med/marked/in.airebo similarity index 100% rename from tests/test_configs/med/marked/in.airebo rename to tests/test_configs/generator_files/med/marked/in.airebo diff --git a/tests/test_configs/med/marked/in.atm b/tests/test_configs/generator_files/med/marked/in.atm similarity index 100% rename from tests/test_configs/med/marked/in.atm rename to tests/test_configs/generator_files/med/marked/in.atm diff --git a/tests/test_configs/med/marked/in.crack b/tests/test_configs/generator_files/med/marked/in.crack similarity index 100% rename from tests/test_configs/med/marked/in.crack rename to tests/test_configs/generator_files/med/marked/in.crack diff --git a/tests/test_configs/med/marked/in.ellipse.gayberne b/tests/test_configs/generator_files/med/marked/in.ellipse.gayberne similarity index 100% rename from tests/test_configs/med/marked/in.ellipse.gayberne rename to tests/test_configs/generator_files/med/marked/in.ellipse.gayberne diff --git a/tests/test_configs/med/marked/input-file.inp b/tests/test_configs/generator_files/med/marked/input-file.inp similarity index 100% rename from tests/test_configs/med/marked/input-file.inp rename to tests/test_configs/generator_files/med/marked/input-file.inp diff --git a/tests/test_configs/med/marked/input.nml b/tests/test_configs/generator_files/med/marked/input.nml similarity index 100% rename from tests/test_configs/med/marked/input.nml rename to tests/test_configs/generator_files/med/marked/input.nml diff --git a/tests/test_configs/med/marked/simple-H20.xml b/tests/test_configs/generator_files/med/marked/simple-H20.xml similarity index 100% rename from tests/test_configs/med/marked/simple-H20.xml rename to tests/test_configs/generator_files/med/marked/simple-H20.xml diff --git a/tests/test_configs/multi_tags_template.sh b/tests/test_configs/generator_files/multi_tags_template.sh similarity index 100% rename from tests/test_configs/multi_tags_template.sh rename to tests/test_configs/generator_files/multi_tags_template.sh diff --git a/tests/test_configs/new-tag/correct/MOM_input b/tests/test_configs/generator_files/new-tag/correct/MOM_input similarity index 100% rename from tests/test_configs/new-tag/correct/MOM_input rename to tests/test_configs/generator_files/new-tag/correct/MOM_input diff --git a/tests/test_configs/new-tag/correct/diag_table b/tests/test_configs/generator_files/new-tag/correct/diag_table similarity index 100% rename from tests/test_configs/new-tag/correct/diag_table rename to tests/test_configs/generator_files/new-tag/correct/diag_table diff --git a/tests/test_configs/new-tag/correct/example_input.i b/tests/test_configs/generator_files/new-tag/correct/example_input.i similarity index 100% rename from tests/test_configs/new-tag/correct/example_input.i rename to tests/test_configs/generator_files/new-tag/correct/example_input.i diff --git a/tests/test_configs/new-tag/correct/in.airebo b/tests/test_configs/generator_files/new-tag/correct/in.airebo similarity index 100% rename from tests/test_configs/new-tag/correct/in.airebo rename to tests/test_configs/generator_files/new-tag/correct/in.airebo diff --git a/tests/test_configs/new-tag/correct/in.atm b/tests/test_configs/generator_files/new-tag/correct/in.atm similarity index 100% rename from tests/test_configs/new-tag/correct/in.atm rename to tests/test_configs/generator_files/new-tag/correct/in.atm diff --git a/tests/test_configs/new-tag/correct/in.crack b/tests/test_configs/generator_files/new-tag/correct/in.crack similarity index 100% rename from tests/test_configs/new-tag/correct/in.crack rename to tests/test_configs/generator_files/new-tag/correct/in.crack diff --git a/tests/test_configs/new-tag/correct/in.ellipse.gayberne b/tests/test_configs/generator_files/new-tag/correct/in.ellipse.gayberne similarity index 100% rename from tests/test_configs/new-tag/correct/in.ellipse.gayberne rename to tests/test_configs/generator_files/new-tag/correct/in.ellipse.gayberne diff --git a/tests/test_configs/new-tag/correct/input-file.inp b/tests/test_configs/generator_files/new-tag/correct/input-file.inp similarity index 100% rename from tests/test_configs/new-tag/correct/input-file.inp rename to tests/test_configs/generator_files/new-tag/correct/input-file.inp diff --git a/tests/test_configs/new-tag/correct/input.nml b/tests/test_configs/generator_files/new-tag/correct/input.nml similarity index 100% rename from tests/test_configs/new-tag/correct/input.nml rename to tests/test_configs/generator_files/new-tag/correct/input.nml diff --git a/tests/test_configs/new-tag/correct/simple-H20.xml b/tests/test_configs/generator_files/new-tag/correct/simple-H20.xml similarity index 100% rename from tests/test_configs/new-tag/correct/simple-H20.xml rename to tests/test_configs/generator_files/new-tag/correct/simple-H20.xml diff --git a/tests/test_configs/new-tag/marked/MOM_input b/tests/test_configs/generator_files/new-tag/marked/MOM_input similarity index 100% rename from tests/test_configs/new-tag/marked/MOM_input rename to tests/test_configs/generator_files/new-tag/marked/MOM_input diff --git a/tests/test_configs/new-tag/marked/diag_table b/tests/test_configs/generator_files/new-tag/marked/diag_table similarity index 100% rename from tests/test_configs/new-tag/marked/diag_table rename to tests/test_configs/generator_files/new-tag/marked/diag_table diff --git a/tests/test_configs/new-tag/marked/example_input.i b/tests/test_configs/generator_files/new-tag/marked/example_input.i similarity index 100% rename from tests/test_configs/new-tag/marked/example_input.i rename to tests/test_configs/generator_files/new-tag/marked/example_input.i diff --git a/tests/test_configs/new-tag/marked/in.airebo b/tests/test_configs/generator_files/new-tag/marked/in.airebo similarity index 100% rename from tests/test_configs/new-tag/marked/in.airebo rename to tests/test_configs/generator_files/new-tag/marked/in.airebo diff --git a/tests/test_configs/new-tag/marked/in.atm b/tests/test_configs/generator_files/new-tag/marked/in.atm similarity index 100% rename from tests/test_configs/new-tag/marked/in.atm rename to tests/test_configs/generator_files/new-tag/marked/in.atm diff --git a/tests/test_configs/new-tag/marked/in.crack b/tests/test_configs/generator_files/new-tag/marked/in.crack similarity index 100% rename from tests/test_configs/new-tag/marked/in.crack rename to tests/test_configs/generator_files/new-tag/marked/in.crack diff --git a/tests/test_configs/new-tag/marked/in.ellipse.gayberne b/tests/test_configs/generator_files/new-tag/marked/in.ellipse.gayberne similarity index 100% rename from tests/test_configs/new-tag/marked/in.ellipse.gayberne rename to tests/test_configs/generator_files/new-tag/marked/in.ellipse.gayberne diff --git a/tests/test_configs/new-tag/marked/input-file.inp b/tests/test_configs/generator_files/new-tag/marked/input-file.inp similarity index 100% rename from tests/test_configs/new-tag/marked/input-file.inp rename to tests/test_configs/generator_files/new-tag/marked/input-file.inp diff --git a/tests/test_configs/new-tag/marked/input.nml b/tests/test_configs/generator_files/new-tag/marked/input.nml similarity index 100% rename from tests/test_configs/new-tag/marked/input.nml rename to tests/test_configs/generator_files/new-tag/marked/input.nml diff --git a/tests/test_configs/new-tag/marked/simple-H20.xml b/tests/test_configs/generator_files/new-tag/marked/simple-H20.xml similarity index 100% rename from tests/test_configs/new-tag/marked/simple-H20.xml rename to tests/test_configs/generator_files/new-tag/marked/simple-H20.xml diff --git a/tests/test_configs/tag_dir_template/nested_0/tagged_0.sh b/tests/test_configs/generator_files/tag_dir_template/nested_0/tagged_0.sh similarity index 100% rename from tests/test_configs/tag_dir_template/nested_0/tagged_0.sh rename to tests/test_configs/generator_files/tag_dir_template/nested_0/tagged_0.sh diff --git a/tests/test_configs/tag_dir_template/nested_1/tagged_1.sh b/tests/test_configs/generator_files/tag_dir_template/nested_1/tagged_1.sh similarity index 100% rename from tests/test_configs/tag_dir_template/nested_1/tagged_1.sh rename to tests/test_configs/generator_files/tag_dir_template/nested_1/tagged_1.sh diff --git a/tests/test_configs/generator_files/test_dir/test.in b/tests/test_configs/generator_files/test_dir/test.in new file mode 100644 index 000000000..8a0a76ee2 --- /dev/null +++ b/tests/test_configs/generator_files/test_dir/test.in @@ -0,0 +1,28 @@ +# BSD 2-Clause License +# +# Copyright (c) 2021-2023, 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. + +thermo = @THERMO@ +steps = @STEPS@ \ No newline at end of file diff --git a/tests/test_configs/test_dir/test_dir_1/config.txt b/tests/test_configs/generator_files/test_dir/test_dir_1/config.txt similarity index 100% rename from tests/test_configs/test_dir/test_dir_1/config.txt rename to tests/test_configs/generator_files/test_dir/test_dir_1/config.txt diff --git a/tests/test_configs/to_copy_dir/mock.txt b/tests/test_configs/generator_files/to_copy_dir/mock.txt similarity index 100% rename from tests/test_configs/to_copy_dir/mock.txt rename to tests/test_configs/generator_files/to_copy_dir/mock.txt diff --git a/tests/test_configs/to_symlink_dir/mock2.txt b/tests/test_configs/generator_files/to_symlink_dir/mock2.txt similarity index 100% rename from tests/test_configs/to_symlink_dir/mock2.txt rename to tests/test_configs/generator_files/to_symlink_dir/mock2.txt diff --git a/tests/test_configs/ml/training_service_torch.py b/tests/test_configs/ml/training_service_torch.py index 8c1091820..575940031 100644 --- a/tests/test_configs/ml/training_service_torch.py +++ b/tests/test_configs/ml/training_service_torch.py @@ -68,7 +68,6 @@ def forward(self, x): print("Started training") for epoch in range(1): # loop over the dataset multiple times - running_loss = 0.0 epoch_running_loss = 0.0 output_period = 1 diff --git a/tests/test_configs/printing_model.py b/tests/test_configs/printing_model.py new file mode 100644 index 000000000..a43f12a67 --- /dev/null +++ b/tests/test_configs/printing_model.py @@ -0,0 +1,18 @@ +import sys +import time + + +def main() -> int: + print(";START;") + time.sleep(20) + print(";MID;") + print("This is an error msg", file=sys.stderr) + time.sleep(20) + print(";END;") + + print("yay!!") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tests/test_configs/reconnect_node.py b/tests/test_configs/reconnect_node.py index dd07d2d47..3ff3d71ef 100644 --- a/tests/test_configs/reconnect_node.py +++ b/tests/test_configs/reconnect_node.py @@ -29,7 +29,6 @@ from smartsim import Client if __name__ == "__main__": - import argparse argparser = argparse.ArgumentParser() diff --git a/tests/test_configs/reconnect_sim.py b/tests/test_configs/reconnect_sim.py index a6340adf5..eda634517 100644 --- a/tests/test_configs/reconnect_sim.py +++ b/tests/test_configs/reconnect_sim.py @@ -31,14 +31,12 @@ def create_data(seed, size): - np.random.seed(seed) x = np.random.uniform(-15.0, 15.0, size=size) return x if __name__ == "__main__": - import argparse argparser = argparse.ArgumentParser() diff --git a/tests/test_configs/send_data_local_smartredis.py b/tests/test_configs/send_data_local_smartredis.py index 48acf0915..0c318736f 100644 --- a/tests/test_configs/send_data_local_smartredis.py +++ b/tests/test_configs/send_data_local_smartredis.py @@ -39,7 +39,7 @@ def main(): returned = client.get_tensor("test_array") np.testing.assert_array_equal(array, returned) - print(f"Test worked! Sent and received array: {str(array)}") + print(f"Test worked! Sent {str(array)} and received {str(returned)}") if __name__ == "__main__": diff --git a/tests/test_configs/sleep.py b/tests/test_configs/sleep.py index 7875e9a3a..778b8946e 100644 --- a/tests/test_configs/sleep.py +++ b/tests/test_configs/sleep.py @@ -36,7 +36,6 @@ def sleep(time_to_sleep): if __name__ == "__main__": - parser = argparse.ArgumentParser() parser.add_argument("--time", type=int, default=15) args = parser.parse_args() diff --git a/tests/test_configs/smartredis/consumer.py b/tests/test_configs/smartredis/consumer.py index 8c5c1528b..7833147c6 100644 --- a/tests/test_configs/smartredis/consumer.py +++ b/tests/test_configs/smartredis/consumer.py @@ -40,7 +40,6 @@ parser.add_argument("--exchange", action="store_true") args = parser.parse_args() - # get model and set into database c = Client(False) keyin = os.getenv("SSKEYIN") diff --git a/tests/test_configs/smartredis/dbid.py b/tests/test_configs/smartredis/dbid.py new file mode 100644 index 000000000..a0aa0993a --- /dev/null +++ b/tests/test_configs/smartredis/dbid.py @@ -0,0 +1,49 @@ +# BSD 2-Clause License +# +# Copyright (c) 2021-2023, 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. + +import argparse +import os + +from smartredis import Client, ConfigOptions + +if __name__ == "__main__": + """For inclusion in test with single database identifier in a single Client + constructor""" + + parser = argparse.ArgumentParser(description="SmartRedis") + parser.add_argument("--exchange", action="store_true") + args = parser.parse_args() + + env_vars = [ + "SSDB_testdb_colo", + "SR_DB_TYPE_testdb_colo", + ] + + assert all([var in os.environ for var in env_vars]) + + opts1 = ConfigOptions.create_from_environment("testdb_colo") + + client = Client(opts1, logger_name="SmartSim") diff --git a/tests/test_configs/smartredis/multidbid.py b/tests/test_configs/smartredis/multidbid.py new file mode 100644 index 000000000..24c8768ec --- /dev/null +++ b/tests/test_configs/smartredis/multidbid.py @@ -0,0 +1,52 @@ +# BSD 2-Clause License +# +# Copyright (c) 2021-2023, 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. + +import argparse +import os + +from smartredis import Client, ConfigOptions + +if __name__ == "__main__": + """For inclusion in test with two unique database identifiers with multiple databases""" + + parser = argparse.ArgumentParser(description="SmartRedis") + parser.add_argument("--exchange", action="store_true") + args = parser.parse_args() + + env_vars = [ + "SSDB_testdb_reg", + "SR_DB_TYPE_testdb_reg", + "SSDB_testdb_colo", + "SR_DB_TYPE_testdb_colo", + ] + + assert all([var in os.environ for var in env_vars]) + + opts1 = ConfigOptions.create_from_environment("testdb_reg") + opts2 = ConfigOptions.create_from_environment("testdb_colo") + + c1 = Client(opts1, logger_name="SmartSim") + c2 = Client(opts2, logger_name="SmartSim") diff --git a/tests/test_configs/telemetry/colocatedmodel.json b/tests/test_configs/telemetry/colocatedmodel.json new file mode 100644 index 000000000..f3e93ac76 --- /dev/null +++ b/tests/test_configs/telemetry/colocatedmodel.json @@ -0,0 +1,69 @@ +{ + "schema info": { + "schema_name": "entity manifest", + "version": "0.0.1" + }, + "experiment": { + "name": "my-exp", + "path": "/tmp/my-exp", + "launcher": "Slurm" + }, + "runs": [ + { + "run_id": "002816b", + "timestamp": 1699037041106269774, + "model": [ + { + "name": "colocated_model", + "path": "/tmp/my-exp/colocated_model", + "exe_args": [ + "/path/to/my/script.py" + ], + "run_settings": { + "exe": [ + "/path/to/my/python" + ], + "run_command": "/opt/slurm/20.11.5/bin/srun", + "run_args": {} + }, + "batch_settings": {}, + "params": {}, + "files": { + "Symlink": [], + "Configure": [], + "Copy": [] + }, + "colocated_db": { + "settings": { + "unix_socket": "/tmp/redis.socket", + "socket_permissions": 755, + "port": 0, + "cpus": 1, + "custom_pinning": "0", + "debug": false, + "db_identifier": "", + "rai_args": { + "threads_per_queue": null, + "inter_op_parallelism": null, + "intra_op_parallelism": null + }, + "extra_db_args": {} + }, + "scripts": [], + "models": [] + }, + "telemetry_metadata": { + "status_dir": "/tmp/my-exp/.smartsim/telemetry/telemetry_ensemble/002816b/model/colocated_model", + "step_id": "4139111.21", + "task_id": "21529", + "managed": true + }, + "out_file": "/tmp/my-exp/colocated_model/colocated_model.out", + "err_file": "/tmp/my-exp/colocated_model/colocated_model.err" + } + ], + "orchestrator": [], + "ensemble": [] + } + ] +} diff --git a/tests/test_configs/telemetry/db_and_model.json b/tests/test_configs/telemetry/db_and_model.json new file mode 100644 index 000000000..58c1c841a --- /dev/null +++ b/tests/test_configs/telemetry/db_and_model.json @@ -0,0 +1,86 @@ +{ + "schema info": { + "schema_name": "entity manifest", + "version": "0.0.1" + }, + "experiment": { + "name": "my-exp", + "path": "/tmp/my-exp", + "launcher": "Slurm" + }, + "runs": [ + { + "run_id": "2ca19ad", + "timestamp": 1699038647234488933, + "model": [], + "orchestrator": [ + { + "name": "orchestrator", + "type": "redis", + "interface": [ + "ipogif0" + ], + "shards": [ + { + "name": "orchestrator_0", + "hostname": "10.128.0.4", + "port": 6780, + "cluster": false, + "conf_file": null, + "out_file": "/path/to/some/file.out", + "err_file": "/path/to/some/file.err", + "telemetry_metadata": { + "status_dir": "/tmp/my-exp/.smartsim/telemetry/telemetry_db_and_model/2ca19ad/database/orchestrator/orchestrator_0", + "step_id": "4139111.27", + "task_id": "1452", + "managed": true + } + } + ] + } + ], + "ensemble": [] + }, + { + "run_id": "4b5507a", + "timestamp": 1699038661491043211, + "model": [ + { + "name": "perroquet", + "path": "/tmp/my-exp/perroquet", + "exe_args": [ + "/path/to/my/script.py" + ], + "run_settings": { + "exe": [ + "/path/to/my/python" + ], + "run_command": "/opt/slurm/20.11.5/bin/srun", + "run_args": { + "nodes": 1, + "ntasks-per-node": 1 + } + }, + "batch_settings": {}, + "params": {}, + "files": { + "Symlink": [], + "Configure": [], + "Copy": [] + }, + "colocated_db": {}, + "telemetry_metadata": { + "status_dir": "/tmp/my-exp/.smartsim/telemetry/telemetry_db_and_model/4b5507a/model/perroquet", + "step_id": "4139111.28", + "task_id": "2929", + "managed": true + }, + "out_file": "/tmp/my-exp/perroquet/perroquet.out", + "err_file": "/tmp/my-exp/perroquet/perroquet.err" + } + ], + "orchestrator": [], + "ensemble": [] + } + ] +} diff --git a/tests/test_configs/telemetry/db_and_model_1run.json b/tests/test_configs/telemetry/db_and_model_1run.json new file mode 100644 index 000000000..44e32bfe4 --- /dev/null +++ b/tests/test_configs/telemetry/db_and_model_1run.json @@ -0,0 +1,79 @@ +{ + "schema info": { + "schema_name": "entity manifest", + "version": "0.0.1" + }, + "experiment": { + "name": "my-exp", + "path": "/tmp/my-exp", + "launcher": "Slurm" + }, + "runs": [ + { + "run_id": "4b5507a", + "timestamp": 1699038661491043211, + "model": [ + { + "name": "perroquet", + "path": "/tmp/my-exp/perroquet", + "exe_args": [ + "/path/to/my/script.py" + ], + "run_settings": { + "exe": [ + "/path/to/my/python" + ], + "run_command": "/opt/slurm/20.11.5/bin/srun", + "run_args": { + "nodes": 1, + "ntasks-per-node": 1 + } + }, + "batch_settings": {}, + "params": {}, + "files": { + "Symlink": [], + "Configure": [], + "Copy": [] + }, + "colocated_db": {}, + "telemetry_metadata": { + "status_dir": "/tmp/my-exp/.smartsim/telemetry/telemetry_db_and_model/4b5507a/model/perroquet", + "step_id": "4139111.28", + "task_id": "2929", + "managed": true + }, + "out_file": "/tmp/my-exp/perroquet/perroquet.out", + "err_file": "/tmp/my-exp/perroquet/perroquet.err" + } + ], + "orchestrator": [ + { + "name": "orchestrator", + "type": "redis", + "interface": [ + "ipogif0" + ], + "shards": [ + { + "name": "orchestrator_0", + "hostname": "10.128.0.4", + "port": 6780, + "cluster": false, + "conf_file": null, + "out_file": "/path/to/some/file.out", + "err_file": "/path/to/some/file.err", + "telemetry_metadata": { + "status_dir": "/tmp/my-exp/.smartsim/telemetry/telemetry_db_and_model/2ca19ad/database/orchestrator/orchestrator_0", + "step_id": "4139111.27", + "task_id": "1452", + "managed": true + } + } + ] + } + ], + "ensemble": [] + } + ] +} diff --git a/tests/test_configs/telemetry/ensembles.json b/tests/test_configs/telemetry/ensembles.json new file mode 100644 index 000000000..841324ec6 --- /dev/null +++ b/tests/test_configs/telemetry/ensembles.json @@ -0,0 +1,329 @@ +{ + "schema info": { + "schema_name": "entity manifest", + "version": "0.0.1" + }, + "experiment": { + "name": "my-exp", + "path": "/home/someuser/code/ss/my-exp", + "launcher": "Local" + }, + "runs": [ + { + "run_id": "d041b90", + "timestamp": 1698679830384608928, + "model": [], + "orchestrator": [], + "ensemble": [ + { + "name": "my-ens", + "params": { + "START": [ + "spam", + "foo" + ], + "MID": [ + "eggs", + "bar" + ], + "END": [ + "ham", + "baz" + ] + }, + "batch_settings": {}, + "models": [ + { + "name": "my-ens_0", + "path": "/home/someuser/code/ss", + "exe_args": [ + "yo.py" + ], + "run_settings": { + "exe": [ + "/home/someuser/.pyenv/versions/3.9.16/envs/ss/bin/python" + ], + "run_command": null, + "run_args": {} + }, + "batch_settings": {}, + "params": { + "START": "spam", + "MID": "eggs", + "END": "ham" + }, + "files": { + "Symlink": [], + "Configure": [ + "/home/someuser/code/ss/manifest/demo/yo.py" + ], + "Copy": [] + }, + "colocated_db": {}, + "telemetry_metadata": { + "status_dir": "/home/someuser/code/ss/my-exp/.smartsim/telemetry/my-exp/d041b90/ensemble/my-ens/my-ens_0", + "step_id": null, + "task_id": "88118", + "managed": false + }, + "out_file": "/home/someuser/code/ss/my-ens_0.out", + "err_file": "/home/someuser/code/ss/my-ens_0.err" + }, + { + "name": "my-ens_1", + "path": "/home/someuser/code/ss", + "exe_args": [ + "yo.py" + ], + "run_settings": { + "exe": [ + "/home/someuser/.pyenv/versions/3.9.16/envs/ss/bin/python" + ], + "run_command": null, + "run_args": {} + }, + "batch_settings": {}, + "params": { + "START": "spam", + "MID": "eggs", + "END": "baz" + }, + "files": { + "Symlink": [], + "Configure": [ + "/home/someuser/code/ss/manifest/demo/yo.py" + ], + "Copy": [] + }, + "colocated_db": {}, + "telemetry_metadata": { + "status_dir": "/home/someuser/code/ss/my-exp/.smartsim/telemetry/my-exp/d041b90/ensemble/my-ens/my-ens_1", + "step_id": null, + "task_id": "88131", + "managed": false + }, + "out_file": "/home/someuser/code/ss/my-ens_1.out", + "err_file": "/home/someuser/code/ss/my-ens_1.err" + }, + { + "name": "my-ens_2", + "path": "/home/someuser/code/ss", + "exe_args": [ + "yo.py" + ], + "run_settings": { + "exe": [ + "/home/someuser/.pyenv/versions/3.9.16/envs/ss/bin/python" + ], + "run_command": null, + "run_args": {} + }, + "batch_settings": {}, + "params": { + "START": "spam", + "MID": "bar", + "END": "ham" + }, + "files": { + "Symlink": [], + "Configure": [ + "/home/someuser/code/ss/manifest/demo/yo.py" + ], + "Copy": [] + }, + "colocated_db": {}, + "telemetry_metadata": { + "status_dir": "/home/someuser/code/ss/my-exp/.smartsim/telemetry/my-exp/d041b90/ensemble/my-ens/my-ens_2", + "step_id": null, + "task_id": "88146", + "managed": false + }, + "out_file": "/home/someuser/code/ss/my-ens_2.out", + "err_file": "/home/someuser/code/ss/my-ens_2.err" + }, + { + "name": "my-ens_3", + "path": "/home/someuser/code/ss", + "exe_args": [ + "yo.py" + ], + "run_settings": { + "exe": [ + "/home/someuser/.pyenv/versions/3.9.16/envs/ss/bin/python" + ], + "run_command": null, + "run_args": {} + }, + "batch_settings": {}, + "params": { + "START": "spam", + "MID": "bar", + "END": "baz" + }, + "files": { + "Symlink": [], + "Configure": [ + "/home/someuser/code/ss/manifest/demo/yo.py" + ], + "Copy": [] + }, + "colocated_db": {}, + "telemetry_metadata": { + "status_dir": "/home/someuser/code/ss/my-exp/.smartsim/telemetry/my-exp/d041b90/ensemble/my-ens/my-ens_3", + "step_id": null, + "task_id": "88170", + "managed": false + }, + "out_file": "/home/someuser/code/ss/my-ens_3.out", + "err_file": "/home/someuser/code/ss/my-ens_3.err" + }, + { + "name": "my-ens_4", + "path": "/home/someuser/code/ss", + "exe_args": [ + "yo.py" + ], + "run_settings": { + "exe": [ + "/home/someuser/.pyenv/versions/3.9.16/envs/ss/bin/python" + ], + "run_command": null, + "run_args": {} + }, + "batch_settings": {}, + "params": { + "START": "foo", + "MID": "eggs", + "END": "ham" + }, + "files": { + "Symlink": [], + "Configure": [ + "/home/someuser/code/ss/manifest/demo/yo.py" + ], + "Copy": [] + }, + "colocated_db": {}, + "telemetry_metadata": { + "status_dir": "/home/someuser/code/ss/my-exp/.smartsim/telemetry/my-exp/d041b90/ensemble/my-ens/my-ens_4", + "step_id": null, + "task_id": "88178", + "managed": false + }, + "out_file": "/home/someuser/code/ss/my-ens_4.out", + "err_file": "/home/someuser/code/ss/my-ens_4.err" + }, + { + "name": "my-ens_5", + "path": "/home/someuser/code/ss", + "exe_args": [ + "yo.py" + ], + "run_settings": { + "exe": [ + "/home/someuser/.pyenv/versions/3.9.16/envs/ss/bin/python" + ], + "run_command": null, + "run_args": {} + }, + "batch_settings": {}, + "params": { + "START": "foo", + "MID": "eggs", + "END": "baz" + }, + "files": { + "Symlink": [], + "Configure": [ + "/home/someuser/code/ss/manifest/demo/yo.py" + ], + "Copy": [] + }, + "colocated_db": {}, + "telemetry_metadata": { + "status_dir": "/home/someuser/code/ss/my-exp/.smartsim/telemetry/my-exp/d041b90/ensemble/my-ens/my-ens_5", + "step_id": null, + "task_id": "88193", + "managed": false + }, + "out_file": "/home/someuser/code/ss/my-ens_5.out", + "err_file": "/home/someuser/code/ss/my-ens_5.err" + }, + { + "name": "my-ens_6", + "path": "/home/someuser/code/ss", + "exe_args": [ + "yo.py" + ], + "run_settings": { + "exe": [ + "/home/someuser/.pyenv/versions/3.9.16/envs/ss/bin/python" + ], + "run_command": null, + "run_args": {} + }, + "batch_settings": {}, + "params": { + "START": "foo", + "MID": "bar", + "END": "ham" + }, + "files": { + "Symlink": [], + "Configure": [ + "/home/someuser/code/ss/manifest/demo/yo.py" + ], + "Copy": [] + }, + "colocated_db": {}, + "telemetry_metadata": { + "status_dir": "/home/someuser/code/ss/my-exp/.smartsim/telemetry/my-exp/d041b90/ensemble/my-ens/my-ens_6", + "step_id": null, + "task_id": "88221", + "managed": false + }, + "out_file": "/home/someuser/code/ss/my-ens_6.out", + "err_file": "/home/someuser/code/ss/my-ens_6.err" + }, + { + "name": "my-ens_7", + "path": "/home/someuser/code/ss", + "exe_args": [ + "yo.py" + ], + "run_settings": { + "exe": [ + "/home/someuser/.pyenv/versions/3.9.16/envs/ss/bin/python" + ], + "run_command": null, + "run_args": {} + }, + "batch_settings": {}, + "params": { + "START": "foo", + "MID": "bar", + "END": "baz" + }, + "files": { + "Symlink": [], + "Configure": [ + "/home/someuser/code/ss/manifest/demo/yo.py" + ], + "Copy": [] + }, + "colocated_db": {}, + "telemetry_metadata": { + "status_dir": "/home/someuser/code/ss/my-exp/.smartsim/telemetry/my-exp/d041b90/ensemble/my-ens/my-ens_7", + "step_id": null, + "task_id": "88241", + "managed": false + }, + "out_file": "/home/someuser/code/ss/my-ens_7.out", + "err_file": "/home/someuser/code/ss/my-ens_7.err" + } + ] + } + ] + } + ] + } diff --git a/tests/test_configs/telemetry/serialmodels.json b/tests/test_configs/telemetry/serialmodels.json new file mode 100644 index 000000000..40337eceb --- /dev/null +++ b/tests/test_configs/telemetry/serialmodels.json @@ -0,0 +1,186 @@ +{ + "schema info": { + "schema_name": "entity manifest", + "version": "0.0.1" + }, + "experiment": { + "name": "my-exp", + "path": "/tmp/my-exp", + "launcher": "Slurm" + }, + "runs": [ + { + "run_id": "8c0fbb1", + "timestamp": 1699037881502730708, + "model": [ + { + "name": "perroquet_0", + "path": "/tmp/my-exp/perroquet_0", + "exe_args": [ + "/tmp/echo.py" + ], + "run_settings": { + "exe": [ + "/path/to/some/python" + ], + "run_command": "/opt/slurm/20.11.5/bin/srun", + "run_args": { + "nodes": 1, + "ntasks-per-node": 1 + } + }, + "batch_settings": {}, + "params": {}, + "files": { + "Symlink": [], + "Configure": [], + "Copy": [] + }, + "colocated_db": {}, + "telemetry_metadata": { + "status_dir": "/tmp/my-exp/.smartsim/telemetry/telemetry_serial_models/8c0fbb1/model/perroquet_0", + "step_id": "4139111.22", + "task_id": "17966", + "managed": true + }, + "out_file": "/tmp/my-exp/perroquet_0/perroquet_0.out", + "err_file": "/tmp/my-exp/perroquet_0/perroquet_0.err" + }, + { + "name": "perroquet_1", + "path": "/tmp/my-exp/perroquet_1", + "exe_args": [ + "/tmp/echo.py" + ], + "run_settings": { + "exe": [ + "/path/to/some/python" + ], + "run_command": "/opt/slurm/20.11.5/bin/srun", + "run_args": { + "nodes": 1, + "ntasks-per-node": 1 + } + }, + "batch_settings": {}, + "params": {}, + "files": { + "Symlink": [], + "Configure": [], + "Copy": [] + }, + "colocated_db": {}, + "telemetry_metadata": { + "status_dir": "/tmp/my-exp/.smartsim/telemetry/telemetry_serial_models/8c0fbb1/model/perroquet_1", + "step_id": "4139111.23", + "task_id": "18100", + "managed": true + }, + "out_file": "/tmp/my-exp/perroquet_1/perroquet_1.out", + "err_file": "/tmp/my-exp/perroquet_1/perroquet_1.err" + }, + { + "name": "perroquet_2", + "path": "/tmp/my-exp/perroquet_2", + "exe_args": [ + "/tmp/echo.py" + ], + "run_settings": { + "exe": [ + "/path/to/some/python" + ], + "run_command": "/opt/slurm/20.11.5/bin/srun", + "run_args": { + "nodes": 1, + "ntasks-per-node": 1 + } + }, + "batch_settings": {}, + "params": {}, + "files": { + "Symlink": [], + "Configure": [], + "Copy": [] + }, + "colocated_db": {}, + "telemetry_metadata": { + "status_dir": "/tmp/my-exp/.smartsim/telemetry/telemetry_serial_models/8c0fbb1/model/perroquet_2", + "step_id": "4139111.24", + "task_id": "18159", + "managed": true + }, + "out_file": "/tmp/my-exp/perroquet_2/perroquet_2.out", + "err_file": "/tmp/my-exp/perroquet_2/perroquet_2.err" + }, + { + "name": "perroquet_3", + "path": "/tmp/my-exp/perroquet_3", + "exe_args": [ + "/tmp/echo.py" + ], + "run_settings": { + "exe": [ + "/path/to/some/python" + ], + "run_command": "/opt/slurm/20.11.5/bin/srun", + "run_args": { + "nodes": 1, + "ntasks-per-node": 1 + } + }, + "batch_settings": {}, + "params": {}, + "files": { + "Symlink": [], + "Configure": [], + "Copy": [] + }, + "colocated_db": {}, + "telemetry_metadata": { + "status_dir": "/tmp/my-exp/.smartsim/telemetry/telemetry_serial_models/8c0fbb1/model/perroquet_3", + "step_id": "4139111.25", + "task_id": "18499", + "managed": true + }, + "out_file": "/tmp/my-exp/perroquet_3/perroquet_3.out", + "err_file": "/tmp/my-exp/perroquet_3/perroquet_3.err" + }, + { + "name": "perroquet_4", + "path": "/tmp/my-exp/perroquet_4", + "exe_args": [ + "/tmp/echo.py" + ], + "run_settings": { + "exe": [ + "/path/to/some/python" + ], + "run_command": "/opt/slurm/20.11.5/bin/srun", + "run_args": { + "nodes": 1, + "ntasks-per-node": 1 + } + }, + "batch_settings": {}, + "params": {}, + "files": { + "Symlink": [], + "Configure": [], + "Copy": [] + }, + "colocated_db": {}, + "telemetry_metadata": { + "status_dir": "/tmp/my-exp/.smartsim/telemetry/telemetry_serial_models/8c0fbb1/model/perroquet_4", + "step_id": "4139111.26", + "task_id": "18832", + "managed": true + }, + "out_file": "/tmp/my-exp/perroquet_4/perroquet_4.out", + "err_file": "/tmp/my-exp/perroquet_4/perroquet_4.err" + } + ], + "orchestrator": [], + "ensemble": [] + } + ] +} diff --git a/tests/test_configs/telemetry/telemetry.json b/tests/test_configs/telemetry/telemetry.json new file mode 100644 index 000000000..a380bc5fb --- /dev/null +++ b/tests/test_configs/telemetry/telemetry.json @@ -0,0 +1,946 @@ +{ + "experiment": { + "name": "my-exp", + "path": "/path/to/my-exp", + "launcher": "Slurm" + }, + "runs": [ + { + "run_id": "d999ad89-020f-4e6a-b834-dbd88658ce84", + "timestamp": 1697824072792854287, + "model": [ + { + "name": "my-model", + "path": "/path/to/my-exp/my-model", + "exe_args": [ + "hello", + "world" + ], + "run_settings": { + "exe": [ + "/usr/bin/echo" + ], + "run_command": "/opt/slurm/20.11.5/bin/srun", + "run_args": { + "nodes": 1, + "ntasks": 1 + } + }, + "batch_settings": {}, + "params": {}, + "files": { + "Symlink": [], + "Configure": [], + "Copy": [] + }, + "colocated_db": { + "settings": { + "port": 5757, + "ifname": "lo", + "cpus": 1, + "custom_pinning": "0", + "debug": false, + "db_identifier": "COLO", + "rai_args": { + "threads_per_queue": null, + "inter_op_parallelism": null, + "intra_op_parallelism": null + }, + "extra_db_args": {} + }, + "scripts": [], + "models": [ + { + "cnn": { + "backend": "TORCH", + "device": "CPU" + } + } + ] + }, + "telemetry_metadata": { + "status_dir": "/path/to/my-exp/.smartsim/telemetry/my-exp/d999ad89-020f-4e6a-b834-dbd88658ce84/model/my-model", + "step_id": "4121050.30", + "task_id": "25230", + "managed": true + }, + "out_file": "/path/to/my-exp/my-model/my-model.out", + "err_file": "/path/to/my-exp/my-model/my-model.err" + } + ], + "orchestrator": [], + "ensemble": [] + }, + { + "run_id": "fd3cd1a8-cb8f-4f61-b847-73a8eb0881fa", + "timestamp": 1697824102122439975, + "model": [], + "orchestrator": [ + { + "name": "orchestrator", + "type": "redis", + "interface": [ + "ipogif0" + ], + "shards": [ + { + "name": "orchestrator_1", + "hostname": "10.128.0.70", + "port": 2424, + "cluster": true, + "conf_file": "nodes-orchestrator_1-2424.conf", + "out_file": "/path/to/my-exp/orchestrator/orchestrator.out", + "err_file": "/path/to/my-exp/orchestrator/orchestrator.err", + "telemetry_metadata": { + "status_dir": "/path/to/my-exp/.smartsim/telemetry/my-exp/fd3cd1a8-cb8f-4f61-b847-73a8eb0881fa/database/orchestrator/orchestrator", + "step_id": "4121050.31+2", + "task_id": "25241", + "managed": true + } + }, + { + "name": "orchestrator_2", + "hostname": "10.128.0.71", + "port": 2424, + "cluster": true, + "conf_file": "nodes-orchestrator_2-2424.conf", + "out_file": "/path/to/my-exp/orchestrator/orchestrator.out", + "err_file": "/path/to/my-exp/orchestrator/orchestrator.err", + "telemetry_metadata": { + "status_dir": "/path/to/my-exp/.smartsim/telemetry/my-exp/fd3cd1a8-cb8f-4f61-b847-73a8eb0881fa/database/orchestrator/orchestrator", + "step_id": "4121050.31+2", + "task_id": "25241", + "managed": true + } + }, + { + "name": "orchestrator_0", + "hostname": "10.128.0.69", + "port": 2424, + "cluster": true, + "conf_file": "nodes-orchestrator_0-2424.conf", + "out_file": "/path/to/my-exp/orchestrator/orchestrator.out", + "err_file": "/path/to/my-exp/orchestrator/orchestrator.err", + "telemetry_metadata": { + "status_dir": "/path/to/my-exp/.smartsim/telemetry/my-exp/fd3cd1a8-cb8f-4f61-b847-73a8eb0881fa/database/orchestrator/orchestrator", + "step_id": "4121050.31+2", + "task_id": "25241", + "managed": true + } + } + ] + } + ], + "ensemble": [] + }, + { + "run_id": "d65ae1df-cb5e-45d9-ab09-6fa641755997", + "timestamp": 1697824127962219505, + "model": [], + "orchestrator": [], + "ensemble": [ + { + "name": "my-ens", + "params": { + "START": [ + "spam", + "foo" + ], + "MID": [ + "eggs", + "bar" + ], + "END": [ + "ham", + "baz" + ] + }, + "batch_settings": {}, + "models": [ + { + "name": "my-ens_0", + "path": "/path/to/my-exp/my-ens/my-ens_0", + "exe_args": [ + "yo.py" + ], + "run_settings": { + "exe": [ + "/path/to/my/python3" + ], + "run_command": "/opt/slurm/20.11.5/bin/srun", + "run_args": { + "nodes": 1, + "ntasks": 1 + } + }, + "batch_settings": {}, + "params": { + "START": "spam", + "MID": "eggs", + "END": "ham" + }, + "files": { + "Symlink": [], + "Configure": [ + "/path/to/yo.py" + ], + "Copy": [] + }, + "colocated_db": {}, + "telemetry_metadata": { + "status_dir": "/path/to/my-exp/.smartsim/telemetry/my-exp/d65ae1df-cb5e-45d9-ab09-6fa641755997/ensemble/my-ens/my-ens_0", + "step_id": "4121050.32", + "task_id": "25639", + "managed": true + }, + "out_file": "/path/to/my-exp/my-ens/my-ens_0/my-ens_0.out", + "err_file": "/path/to/my-exp/my-ens/my-ens_0/my-ens_0.err" + }, + { + "name": "my-ens_1", + "path": "/path/to/my-exp/my-ens/my-ens_1", + "exe_args": [ + "yo.py" + ], + "run_settings": { + "exe": [ + "/path/to/my/python3" + ], + "run_command": "/opt/slurm/20.11.5/bin/srun", + "run_args": { + "nodes": 1, + "ntasks": 1 + } + }, + "batch_settings": {}, + "params": { + "START": "spam", + "MID": "eggs", + "END": "baz" + }, + "files": { + "Symlink": [], + "Configure": [ + "/path/to/yo.py" + ], + "Copy": [] + }, + "colocated_db": {}, + "telemetry_metadata": { + "status_dir": "/path/to/my-exp/.smartsim/telemetry/my-exp/d65ae1df-cb5e-45d9-ab09-6fa641755997/ensemble/my-ens/my-ens_1", + "step_id": "4121050.33", + "task_id": "25768", + "managed": true + }, + "out_file": "/path/to/my-exp/my-ens/my-ens_1/my-ens_1.out", + "err_file": "/path/to/my-exp/my-ens/my-ens_1/my-ens_1.err" + }, + { + "name": "my-ens_2", + "path": "/path/to/my-exp/my-ens/my-ens_2", + "exe_args": [ + "yo.py" + ], + "run_settings": { + "exe": [ + "/path/to/my/python3" + ], + "run_command": "/opt/slurm/20.11.5/bin/srun", + "run_args": { + "nodes": 1, + "ntasks": 1 + } + }, + "batch_settings": {}, + "params": { + "START": "spam", + "MID": "bar", + "END": "ham" + }, + "files": { + "Symlink": [], + "Configure": [ + "/path/to/yo.py" + ], + "Copy": [] + }, + "colocated_db": {}, + "telemetry_metadata": { + "status_dir": "/path/to/my-exp/.smartsim/telemetry/my-exp/d65ae1df-cb5e-45d9-ab09-6fa641755997/ensemble/my-ens/my-ens_2", + "step_id": "4121050.34", + "task_id": "25817", + "managed": true + }, + "out_file": "/path/to/my-exp/my-ens/my-ens_2/my-ens_2.out", + "err_file": "/path/to/my-exp/my-ens/my-ens_2/my-ens_2.err" + }, + { + "name": "my-ens_3", + "path": "/path/to/my-exp/my-ens/my-ens_3", + "exe_args": [ + "yo.py" + ], + "run_settings": { + "exe": [ + "/path/to/my/python3" + ], + "run_command": "/opt/slurm/20.11.5/bin/srun", + "run_args": { + "nodes": 1, + "ntasks": 1 + } + }, + "batch_settings": {}, + "params": { + "START": "spam", + "MID": "bar", + "END": "baz" + }, + "files": { + "Symlink": [], + "Configure": [ + "/path/to/yo.py" + ], + "Copy": [] + }, + "colocated_db": {}, + "telemetry_metadata": { + "status_dir": "/path/to/my-exp/.smartsim/telemetry/my-exp/d65ae1df-cb5e-45d9-ab09-6fa641755997/ensemble/my-ens/my-ens_3", + "step_id": "4121050.35", + "task_id": "25837", + "managed": true + }, + "out_file": "/path/to/my-exp/my-ens/my-ens_3/my-ens_3.out", + "err_file": "/path/to/my-exp/my-ens/my-ens_3/my-ens_3.err" + }, + { + "name": "my-ens_4", + "path": "/path/to/my-exp/my-ens/my-ens_4", + "exe_args": [ + "yo.py" + ], + "run_settings": { + "exe": [ + "/path/to/my/python3" + ], + "run_command": "/opt/slurm/20.11.5/bin/srun", + "run_args": { + "nodes": 1, + "ntasks": 1 + } + }, + "batch_settings": {}, + "params": { + "START": "foo", + "MID": "eggs", + "END": "ham" + }, + "files": { + "Symlink": [], + "Configure": [ + "/path/to/yo.py" + ], + "Copy": [] + }, + "colocated_db": {}, + "telemetry_metadata": { + "status_dir": "/path/to/my-exp/.smartsim/telemetry/my-exp/d65ae1df-cb5e-45d9-ab09-6fa641755997/ensemble/my-ens/my-ens_4", + "step_id": "4121050.36", + "task_id": "25872", + "managed": true + }, + "out_file": "/path/to/my-exp/my-ens/my-ens_4/my-ens_4.out", + "err_file": "/path/to/my-exp/my-ens/my-ens_4/my-ens_4.err" + }, + { + "name": "my-ens_5", + "path": "/path/to/my-exp/my-ens/my-ens_5", + "exe_args": [ + "yo.py" + ], + "run_settings": { + "exe": [ + "/path/to/my/python3" + ], + "run_command": "/opt/slurm/20.11.5/bin/srun", + "run_args": { + "nodes": 1, + "ntasks": 1 + } + }, + "batch_settings": {}, + "params": { + "START": "foo", + "MID": "eggs", + "END": "baz" + }, + "files": { + "Symlink": [], + "Configure": [ + "/path/to/yo.py" + ], + "Copy": [] + }, + "colocated_db": {}, + "telemetry_metadata": { + "status_dir": "/path/to/my-exp/.smartsim/telemetry/my-exp/d65ae1df-cb5e-45d9-ab09-6fa641755997/ensemble/my-ens/my-ens_5", + "step_id": "4121050.37", + "task_id": "25930", + "managed": true + }, + "out_file": "/path/to/my-exp/my-ens/my-ens_5/my-ens_5.out", + "err_file": "/path/to/my-exp/my-ens/my-ens_5/my-ens_5.err" + }, + { + "name": "my-ens_6", + "path": "/path/to/my-exp/my-ens/my-ens_6", + "exe_args": [ + "yo.py" + ], + "run_settings": { + "exe": [ + "/path/to/my/python3" + ], + "run_command": "/opt/slurm/20.11.5/bin/srun", + "run_args": { + "nodes": 1, + "ntasks": 1 + } + }, + "batch_settings": {}, + "params": { + "START": "foo", + "MID": "bar", + "END": "ham" + }, + "files": { + "Symlink": [], + "Configure": [ + "/path/to/yo.py" + ], + "Copy": [] + }, + "colocated_db": {}, + "telemetry_metadata": { + "status_dir": "/path/to/my-exp/.smartsim/telemetry/my-exp/d65ae1df-cb5e-45d9-ab09-6fa641755997/ensemble/my-ens/my-ens_6", + "step_id": "4121050.38", + "task_id": "25945", + "managed": true + }, + "out_file": "/path/to/my-exp/my-ens/my-ens_6/my-ens_6.out", + "err_file": "/path/to/my-exp/my-ens/my-ens_6/my-ens_6.err" + }, + { + "name": "my-ens_7", + "path": "/path/to/my-exp/my-ens/my-ens_7", + "exe_args": [ + "yo.py" + ], + "run_settings": { + "exe": [ + "/path/to/my/python3" + ], + "run_command": "/opt/slurm/20.11.5/bin/srun", + "run_args": { + "nodes": 1, + "ntasks": 1 + } + }, + "batch_settings": {}, + "params": { + "START": "foo", + "MID": "bar", + "END": "baz" + }, + "files": { + "Symlink": [], + "Configure": [ + "/path/to/yo.py" + ], + "Copy": [] + }, + "colocated_db": {}, + "telemetry_metadata": { + "status_dir": "/path/to/my-exp/.smartsim/telemetry/my-exp/d65ae1df-cb5e-45d9-ab09-6fa641755997/ensemble/my-ens/my-ens_7", + "step_id": "4121050.39", + "task_id": "25967", + "managed": true + }, + "out_file": "/path/to/my-exp/my-ens/my-ens_7/my-ens_7.out", + "err_file": "/path/to/my-exp/my-ens/my-ens_7/my-ens_7.err" + } + ] + } + ] + }, + { + "run_id": "e41f8e17-c4b2-441d-adf9-707443ee2c72", + "timestamp": 1697835227560376025, + "model": [ + { + "name": "my-model", + "path": "/path/to/my-exp/my-model", + "exe_args": [ + "hello", + "world" + ], + "run_settings": { + "exe": [ + "/usr/bin/echo" + ], + "run_command": "/opt/slurm/20.11.5/bin/srun", + "run_args": { + "nodes": 1, + "ntasks": 1 + } + }, + "batch_settings": {}, + "params": {}, + "files": { + "Symlink": [], + "Configure": [], + "Copy": [] + }, + "colocated_db": { + "settings": { + "port": 5757, + "ifname": "lo", + "cpus": 1, + "custom_pinning": "0", + "debug": false, + "db_identifier": "COLO", + "rai_args": { + "threads_per_queue": null, + "inter_op_parallelism": null, + "intra_op_parallelism": null + }, + "extra_db_args": {} + }, + "scripts": [], + "models": [ + { + "cnn": { + "backend": "TORCH", + "device": "CPU" + } + } + ] + }, + "telemetry_metadata": { + "status_dir": "/path/to/my-exp/.smartsim/telemetry/my-exp/e41f8e17-c4b2-441d-adf9-707443ee2c72/model/my-model", + "step_id": "4121904.0", + "task_id": "28277", + "managed": true + }, + "out_file": "/path/to/my-exp/my-model/my-model.out", + "err_file": "/path/to/my-exp/my-model/my-model.err" + } + ], + "orchestrator": [], + "ensemble": [] + }, + { + "run_id": "b33a5d27-6822-4795-8e0e-cfea18551fa4", + "timestamp": 1697835261956135240, + "model": [], + "orchestrator": [ + { + "name": "orchestrator", + "type": "redis", + "interface": [ + "ipogif0" + ], + "shards": [ + { + "name": "orchestrator_0", + "hostname": "10.128.0.2", + "port": 2424, + "cluster": true, + "conf_file": "nodes-orchestrator_0-2424.conf", + "out_file": "/path/to/my-exp/orchestrator/orchestrator.out", + "err_file": "/path/to/my-exp/orchestrator/orchestrator.err", + "telemetry_metadata": { + "status_dir": "/path/to/my-exp/.smartsim/telemetry/my-exp/b33a5d27-6822-4795-8e0e-cfea18551fa4/database/orchestrator/orchestrator", + "step_id": "4121904.1+2", + "task_id": "28289", + "managed": true + } + }, + { + "name": "orchestrator_2", + "hostname": "10.128.0.4", + "port": 2424, + "cluster": true, + "conf_file": "nodes-orchestrator_2-2424.conf", + "out_file": "/path/to/my-exp/orchestrator/orchestrator.out", + "err_file": "/path/to/my-exp/orchestrator/orchestrator.err", + "telemetry_metadata": { + "status_dir": "/path/to/my-exp/.smartsim/telemetry/my-exp/b33a5d27-6822-4795-8e0e-cfea18551fa4/database/orchestrator/orchestrator", + "step_id": "4121904.1+2", + "task_id": "28289", + "managed": true + } + }, + { + "name": "orchestrator_1", + "hostname": "10.128.0.3", + "port": 2424, + "cluster": true, + "conf_file": "nodes-orchestrator_1-2424.conf", + "out_file": "/path/to/my-exp/orchestrator/orchestrator.out", + "err_file": "/path/to/my-exp/orchestrator/orchestrator.err", + "telemetry_metadata": { + "status_dir": "/path/to/my-exp/.smartsim/telemetry/my-exp/b33a5d27-6822-4795-8e0e-cfea18551fa4/database/orchestrator/orchestrator", + "step_id": "4121904.1+2", + "task_id": "28289", + "managed": true + } + } + ] + } + ], + "ensemble": [] + }, + { + "run_id": "45772df2-fd80-43fd-adf0-d5e319870182", + "timestamp": 1697835287798613875, + "model": [], + "orchestrator": [], + "ensemble": [ + { + "name": "my-ens", + "params": { + "START": [ + "spam", + "foo" + ], + "MID": [ + "eggs", + "bar" + ], + "END": [ + "ham", + "baz" + ] + }, + "batch_settings": {}, + "models": [ + { + "name": "my-ens_0", + "path": "/path/to/my-exp/my-ens/my-ens_0", + "exe_args": [ + "yo.py" + ], + "run_settings": { + "exe": [ + "/path/to/my/python3" + ], + "run_command": "/opt/slurm/20.11.5/bin/srun", + "run_args": { + "nodes": 1, + "ntasks": 1 + } + }, + "batch_settings": {}, + "params": { + "START": "spam", + "MID": "eggs", + "END": "ham" + }, + "files": { + "Symlink": [], + "Configure": [ + "/path/to/yo.py" + ], + "Copy": [] + }, + "colocated_db": {}, + "telemetry_metadata": { + "status_dir": "/path/to/my-exp/.smartsim/telemetry/my-exp/45772df2-fd80-43fd-adf0-d5e319870182/ensemble/my-ens/my-ens_0", + "step_id": "4121904.2", + "task_id": "28333", + "managed": true + }, + "out_file": "/path/to/my-exp/my-ens/my-ens_0/my-ens_0.out", + "err_file": "/path/to/my-exp/my-ens/my-ens_0/my-ens_0.err" + }, + { + "name": "my-ens_1", + "path": "/path/to/my-exp/my-ens/my-ens_1", + "exe_args": [ + "yo.py" + ], + "run_settings": { + "exe": [ + "/path/to/my/python3" + ], + "run_command": "/opt/slurm/20.11.5/bin/srun", + "run_args": { + "nodes": 1, + "ntasks": 1 + } + }, + "batch_settings": {}, + "params": { + "START": "spam", + "MID": "eggs", + "END": "baz" + }, + "files": { + "Symlink": [], + "Configure": [ + "/path/to/yo.py" + ], + "Copy": [] + }, + "colocated_db": {}, + "telemetry_metadata": { + "status_dir": "/path/to/my-exp/.smartsim/telemetry/my-exp/45772df2-fd80-43fd-adf0-d5e319870182/ensemble/my-ens/my-ens_1", + "step_id": "4121904.3", + "task_id": "28342", + "managed": true + }, + "out_file": "/path/to/my-exp/my-ens/my-ens_1/my-ens_1.out", + "err_file": "/path/to/my-exp/my-ens/my-ens_1/my-ens_1.err" + }, + { + "name": "my-ens_2", + "path": "/path/to/my-exp/my-ens/my-ens_2", + "exe_args": [ + "yo.py" + ], + "run_settings": { + "exe": [ + "/path/to/my/python3" + ], + "run_command": "/opt/slurm/20.11.5/bin/srun", + "run_args": { + "nodes": 1, + "ntasks": 1 + } + }, + "batch_settings": {}, + "params": { + "START": "spam", + "MID": "bar", + "END": "ham" + }, + "files": { + "Symlink": [], + "Configure": [ + "/path/to/yo.py" + ], + "Copy": [] + }, + "colocated_db": {}, + "telemetry_metadata": { + "status_dir": "/path/to/my-exp/.smartsim/telemetry/my-exp/45772df2-fd80-43fd-adf0-d5e319870182/ensemble/my-ens/my-ens_2", + "step_id": "4121904.4", + "task_id": "28353", + "managed": true + }, + "out_file": "/path/to/my-exp/my-ens/my-ens_2/my-ens_2.out", + "err_file": "/path/to/my-exp/my-ens/my-ens_2/my-ens_2.err" + }, + { + "name": "my-ens_3", + "path": "/path/to/my-exp/my-ens/my-ens_3", + "exe_args": [ + "yo.py" + ], + "run_settings": { + "exe": [ + "/path/to/my/python3" + ], + "run_command": "/opt/slurm/20.11.5/bin/srun", + "run_args": { + "nodes": 1, + "ntasks": 1 + } + }, + "batch_settings": {}, + "params": { + "START": "spam", + "MID": "bar", + "END": "baz" + }, + "files": { + "Symlink": [], + "Configure": [ + "/path/to/yo.py" + ], + "Copy": [] + }, + "colocated_db": {}, + "telemetry_metadata": { + "status_dir": "/path/to/my-exp/.smartsim/telemetry/my-exp/45772df2-fd80-43fd-adf0-d5e319870182/ensemble/my-ens/my-ens_3", + "step_id": "4121904.5", + "task_id": "28362", + "managed": true + }, + "out_file": "/path/to/my-exp/my-ens/my-ens_3/my-ens_3.out", + "err_file": "/path/to/my-exp/my-ens/my-ens_3/my-ens_3.err" + }, + { + "name": "my-ens_4", + "path": "/path/to/my-exp/my-ens/my-ens_4", + "exe_args": [ + "yo.py" + ], + "run_settings": { + "exe": [ + "/path/to/my/python3" + ], + "run_command": "/opt/slurm/20.11.5/bin/srun", + "run_args": { + "nodes": 1, + "ntasks": 1 + } + }, + "batch_settings": {}, + "params": { + "START": "foo", + "MID": "eggs", + "END": "ham" + }, + "files": { + "Symlink": [], + "Configure": [ + "/path/to/yo.py" + ], + "Copy": [] + }, + "colocated_db": {}, + "telemetry_metadata": { + "status_dir": "/path/to/my-exp/.smartsim/telemetry/my-exp/45772df2-fd80-43fd-adf0-d5e319870182/ensemble/my-ens/my-ens_4", + "step_id": "4121904.6", + "task_id": "28371", + "managed": true + }, + "out_file": "/path/to/my-exp/my-ens/my-ens_4/my-ens_4.out", + "err_file": "/path/to/my-exp/my-ens/my-ens_4/my-ens_4.err" + }, + { + "name": "my-ens_5", + "path": "/path/to/my-exp/my-ens/my-ens_5", + "exe_args": [ + "yo.py" + ], + "run_settings": { + "exe": [ + "/path/to/my/python3" + ], + "run_command": "/opt/slurm/20.11.5/bin/srun", + "run_args": { + "nodes": 1, + "ntasks": 1 + } + }, + "batch_settings": {}, + "params": { + "START": "foo", + "MID": "eggs", + "END": "baz" + }, + "files": { + "Symlink": [], + "Configure": [ + "/path/to/yo.py" + ], + "Copy": [] + }, + "colocated_db": {}, + "telemetry_metadata": { + "status_dir": "/path/to/my-exp/.smartsim/telemetry/my-exp/45772df2-fd80-43fd-adf0-d5e319870182/ensemble/my-ens/my-ens_5", + "step_id": "4121904.7", + "task_id": "28380", + "managed": true + }, + "out_file": "/path/to/my-exp/my-ens/my-ens_5/my-ens_5.out", + "err_file": "/path/to/my-exp/my-ens/my-ens_5/my-ens_5.err" + }, + { + "name": "my-ens_6", + "path": "/path/to/my-exp/my-ens/my-ens_6", + "exe_args": [ + "yo.py" + ], + "run_settings": { + "exe": [ + "/path/to/my/python3" + ], + "run_command": "/opt/slurm/20.11.5/bin/srun", + "run_args": { + "nodes": 1, + "ntasks": 1 + } + }, + "batch_settings": {}, + "params": { + "START": "foo", + "MID": "bar", + "END": "ham" + }, + "files": { + "Symlink": [], + "Configure": [ + "/path/to/yo.py" + ], + "Copy": [] + }, + "colocated_db": {}, + "telemetry_metadata": { + "status_dir": "/path/to/my-exp/.smartsim/telemetry/my-exp/45772df2-fd80-43fd-adf0-d5e319870182/ensemble/my-ens/my-ens_6", + "step_id": "4121904.8", + "task_id": "28389", + "managed": true + }, + "out_file": "/path/to/my-exp/my-ens/my-ens_6/my-ens_6.out", + "err_file": "/path/to/my-exp/my-ens/my-ens_6/my-ens_6.err" + }, + { + "name": "my-ens_7", + "path": "/path/to/my-exp/my-ens/my-ens_7", + "exe_args": [ + "yo.py" + ], + "run_settings": { + "exe": [ + "/path/to/my/python3" + ], + "run_command": "/opt/slurm/20.11.5/bin/srun", + "run_args": { + "nodes": 1, + "ntasks": 1 + } + }, + "batch_settings": {}, + "params": { + "START": "foo", + "MID": "bar", + "END": "baz" + }, + "files": { + "Symlink": [], + "Configure": [ + "/path/to/yo.py" + ], + "Copy": [] + }, + "colocated_db": {}, + "telemetry_metadata": { + "status_dir": "/path/to/my-exp/.smartsim/telemetry/my-exp/45772df2-fd80-43fd-adf0-d5e319870182/ensemble/my-ens/my-ens_7", + "step_id": "4121904.9", + "task_id": "28398", + "managed": true + }, + "out_file": "/path/to/my-exp/my-ens/my-ens_7/my-ens_7.out", + "err_file": "/path/to/my-exp/my-ens/my-ens_7/my-ens_7.err" + } + ] + } + ] + } + ] + } + diff --git a/tests/test_containers.py b/tests/test_containers.py index 0c6db8d49..e35b4f309 100644 --- a/tests/test_containers.py +++ b/tests/test_containers.py @@ -32,11 +32,14 @@ import pytest from smartsim import Experiment, status -from smartsim._core.utils import installed_redisai_backends from smartsim.database import Orchestrator -from smartsim.entity import Ensemble, Model +from smartsim.entity import Ensemble from smartsim.settings.containers import Singularity +# The tests in this file belong to the group_a group +pytestmark = pytest.mark.group_a + + # Check if singularity is available as command line tool singularity_exists = which("singularity") is not None containerURI = "docker://alrigazzi/smartsim-testing:latest" @@ -87,9 +90,8 @@ def test_singularity_commands(fileutils): @pytest.mark.skipif(not singularity_exists, reason="Test needs singularity to run") -def test_singularity_basic(fileutils): +def test_singularity_basic(fileutils, test_dir): """Basic argument-less Singularity test""" - test_dir = fileutils.make_test_dir() container = Singularity(containerURI) @@ -113,9 +115,9 @@ def test_singularity_basic(fileutils): @pytest.mark.skipif(not singularity_exists, reason="Test needs singularity to run") -def test_singularity_args(fileutils): +def test_singularity_args(fileutils, test_dir): """Test combinations of args and mount arguments for Singularity""" - test_dir = fileutils.make_test_dir() + hometest_dir = os.path.join(str(Path.home()), "test") # $HOME/test mount_paths = {test_dir + "/singularity_args": hometest_dir} container = Singularity(containerURI, args="--contain", mount=mount_paths) @@ -140,7 +142,7 @@ def test_singularity_args(fileutils): @pytest.mark.skipif(not singularity_exists, reason="Test needs singularity to run") -def test_singularity_smartredis(fileutils, wlmutils): +def test_singularity_smartredis(test_dir, fileutils, wlmutils): """Run two processes, each process puts a tensor on the DB, then accesses the other process's tensor. Finally, the tensor is used to run a model. @@ -148,7 +150,6 @@ def test_singularity_smartredis(fileutils, wlmutils): Note: This is a containerized port of test_smartredis.py """ - test_dir = fileutils.make_test_dir() exp = Experiment( "smartredis_ensemble_exchange", exp_path=test_dir, launcher="local" ) diff --git a/tests/test_controller.py b/tests/test_controller.py new file mode 100644 index 000000000..65687ec59 --- /dev/null +++ b/tests/test_controller.py @@ -0,0 +1,75 @@ +# BSD 2-Clause License +# +# Copyright (c) 2021-2023, 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. + +import pathlib + +import pytest + +from smartsim._core.control.controller import Controller +from smartsim._core.launcher.step import Step +from smartsim.database.orchestrator import Orchestrator +from smartsim.entity.ensemble import Ensemble +from smartsim.settings.slurmSettings import SbatchSettings, SrunSettings + +controller = Controller() + +rs = SrunSettings("echo", ["spam", "eggs"]) +bs = SbatchSettings() + +ens = Ensemble("ens", params={}, run_settings=rs, batch_settings=bs, replicas=3) +orc = Orchestrator(db_nodes=3, batch=True, launcher="slurm", run_command="srun") + + +class MockStep(Step): + @staticmethod + def _create_unique_name(name): + return name + + def add_to_batch(self, step): ... + + def get_launch_cmd(self): + return [] + + +@pytest.mark.parametrize( + "collection", + [ + pytest.param(ens, id="Ensemble"), + pytest.param(orc, id="Database"), + ], +) +def test_controller_batch_step_creation_preserves_entity_order(collection, monkeypatch): + monkeypatch.setattr( + controller._launcher, + "create_step", + lambda name, path, settings: MockStep(name, path, settings), + ) + entity_names = [x.name for x in collection.entities] + assert len(entity_names) == len(set(entity_names)) + _, steps = controller._create_batch_job_step( + collection, pathlib.Path("mock/exp/path") + ) + assert entity_names == [step.name for step in steps] diff --git a/tests/test_controller_errors.py b/tests/test_controller_errors.py index e9e6cb0f5..a40ccdf66 100644 --- a/tests/test_controller_errors.py +++ b/tests/test_controller_errors.py @@ -34,6 +34,9 @@ from smartsim.error.errors import SSUnsupportedError from smartsim.settings import RunSettings +# The tests in this file belong to the group_a group +pytestmark = pytest.mark.group_a + def test_finished_entity_orc_error(): """Orchestrators are never 'finished', either run forever or stopped by user""" @@ -97,7 +100,7 @@ def test_wrong_orchestrator(wlmutils): cont = Controller(launcher="local") manifest = Manifest(orc) with pytest.raises(SmartSimError): - cont._launch(manifest) + cont._launch("exp_name", "exp_path", manifest) def test_bad_orc_checkpoint(): diff --git a/tests/test_dbnode.py b/tests/test_dbnode.py index a4f4f641e..ec0ed23ea 100644 --- a/tests/test_dbnode.py +++ b/tests/test_dbnode.py @@ -24,13 +24,22 @@ # 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. +import io +import json +import random +import string +import textwrap import pytest from smartsim import Experiment from smartsim.database import Orchestrator +from smartsim.entity.dbnode import DBNode, LaunchedShardData from smartsim.error.errors import SmartSimError +# The tests in this file belong to the group_a group +pytestmark = pytest.mark.group_a + def test_parse_db_host_error(): orc = Orchestrator() @@ -40,30 +49,88 @@ def test_parse_db_host_error(): orc.entities[0].host -def test_hosts(fileutils, wlmutils): +def test_hosts(test_dir, wlmutils): exp_name = "test_hosts" - exp = Experiment(exp_name) - test_dir = fileutils.make_test_dir() + exp = Experiment(exp_name, exp_path=test_dir) orc = Orchestrator(port=wlmutils.get_test_port(), interface="lo", launcher="local") orc.set_path(test_dir) exp.start(orc) - thrown = False hosts = [] try: hosts = orc.hosts - except SmartSimError: - thrown = True + assert len(hosts) == orc.db_nodes == 1 finally: # stop the database even if there is an error raised exp.stop(orc) orc.remove_stale_files() - assert not thrown - assert hosts == orc.hosts + + +def _random_shard_info(): + rand_string = lambda: "".join(random.choices(string.ascii_letters, k=10)) + rand_num = lambda: random.randint(1000, 9999) + flip_coin = lambda: random.choice((True, False)) + + return LaunchedShardData( + name=rand_string(), + hostname=rand_string(), + port=rand_num(), + cluster=flip_coin(), + ) + + +def test_launched_shard_info_can_be_serialized(): + shard_data = _random_shard_info() + shard_data_from_str = LaunchedShardData( + **json.loads(json.dumps(shard_data.to_dict())) + ) + + assert shard_data is not shard_data_from_str + assert shard_data == shard_data_from_str + + +@pytest.mark.parametrize("limit", [None, 1]) +def test_db_node_can_parse_launched_shard_info(limit): + rand_shards = [_random_shard_info() for _ in range(3)] + with io.StringIO(textwrap.dedent("""\ + This is some file like str + -------------------------- + + SMARTSIM_ORC_SHARD_INFO: {} + ^^^^^^^^^^^^^^^^^^^^^^^ + We should be able to parse the serialized + launched db info from this file if the line is + prefixed with this tag. + + Here are two more for good measure: + SMARTSIM_ORC_SHARD_INFO: {} + SMARTSIM_ORC_SHARD_INFO: {} + + All other lines should be ignored. + """).format(*(json.dumps(s.to_dict()) for s in rand_shards))) as stream: + parsed_shards = DBNode._parse_launched_shard_info_from_iterable(stream, limit) + if limit is not None: + rand_shards = rand_shards[:limit] + assert rand_shards == parsed_shards def test_set_host(): orc = Orchestrator() - orc.entities[0].set_host("host") - assert orc.entities[0]._host == "host" + orc.entities[0].set_hosts(["host"]) + assert orc.entities[0].host == "host" + + +@pytest.mark.parametrize("nodes, mpmd", [[3, False], [3, True], [1, False]]) +def test_db_id_and_name(mpmd, nodes, wlmutils): + if nodes > 1 and wlmutils.get_test_launcher() not in pytest.wlm_options: + pytest.skip(reason="Clustered DB can only be checked on WLMs") + orc = Orchestrator( + db_identifier="test_db", + db_nodes=nodes, + single_cmd=mpmd, + launcher=wlmutils.get_test_launcher(), + ) + for i, node in enumerate(orc.entities): + assert node.name == f"{orc.name}_{i}" + assert node.db_identifier == orc.db_identifier diff --git a/tests/test_ensemble.py b/tests/test_ensemble.py index 2326eb826..4545e80bf 100644 --- a/tests/test_ensemble.py +++ b/tests/test_ensemble.py @@ -34,6 +34,10 @@ from smartsim.error import EntityExistsError, SSUnsupportedError, UserStrategyError from smartsim.settings import RunSettings +# The tests in this file belong to the slow_tests group +pytestmark = pytest.mark.slow_tests + + """ Test ensemble creation @@ -45,7 +49,7 @@ # ---- helpers ------------------------------------------------------ -def step_values(param_names, param_values, n_models = 0): +def step_values(param_names, param_values, n_models=0): permutations = [] for p in zip(*param_values): permutations.append(dict(zip(param_names, p))) @@ -54,13 +58,13 @@ def step_values(param_names, param_values, n_models = 0): # bad permutation strategy that doesn't return # a list of dictionaries -def bad_strategy(names, values, n_models = 0): +def bad_strategy(names, values, n_models=0): return -1 # test bad perm strategy that returns a list but of lists # not dictionaries -def bad_strategy_2(names, values, n_models = 0): +def bad_strategy_2(names, values, n_models=0): return [values] @@ -226,6 +230,7 @@ def test_arg_and_model_params_all_perms(): # ----- Error Handling -------------------------------------- + # unknown permuation strategy def test_unknown_perm_strat(): bad_strat = "not-a-strategy" diff --git a/tests/test_entitylist.py b/tests/test_entitylist.py index afb88bd57..675e84426 100644 --- a/tests/test_entitylist.py +++ b/tests/test_entitylist.py @@ -33,6 +33,9 @@ from smartsim.entity import EntityList from smartsim.settings import RunSettings +# The tests in this file belong to the group_a group +pytestmark = pytest.mark.group_a + def test_entity_list_init(): with pytest.raises(NotImplementedError): diff --git a/tests/test_experiment.py b/tests/test_experiment.py index dbaa51bdc..c0185ab6d 100644 --- a/tests/test_experiment.py +++ b/tests/test_experiment.py @@ -23,21 +23,25 @@ # 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. - -import contextlib +import os import pytest from smartsim import Experiment +from smartsim._core.config import CONFIG from smartsim.entity import Model from smartsim.error import SmartSimError from smartsim.settings import RunSettings +from smartsim.status import STATUS_NEVER_STARTED + +# The tests in this file belong to the slow_tests group +pytestmark = pytest.mark.slow_tests -def test_model_prefix(fileutils): +def test_model_prefix(test_dir): exp_name = "test_prefix" exp = Experiment(exp_name) - test_dir = fileutils.make_test_dir() + model = exp.create_model( "model", path=test_dir, @@ -83,8 +87,7 @@ def test_status_typeerror(): def test_status_pre_launch(): model = Model("name", {}, "./", RunSettings("python")) exp = Experiment("test") - with pytest.raises(SmartSimError): - exp.get_status(model) + assert exp.get_status(model)[0] == STATUS_NEVER_STARTED def test_bad_ensemble_init_no_rs(): @@ -108,10 +111,9 @@ def test_bad_ensemble_init_no_rs_bs(): exp.create_ensemble("name") -def test_stop_entity(fileutils): +def test_stop_entity(test_dir): exp_name = "test_stop_entity" - exp = Experiment(exp_name) - test_dir = fileutils.make_test_dir() + exp = Experiment(exp_name, exp_path=test_dir) m = exp.create_model("model", path=test_dir, run_settings=RunSettings("sleep", "5")) exp.start(m, block=False) assert exp.finished(m) == False @@ -119,11 +121,10 @@ def test_stop_entity(fileutils): assert exp.finished(m) == True -def test_poll(fileutils): +def test_poll(test_dir): # Ensure that a SmartSimError is not raised exp_name = "test_exp_poll" - exp = Experiment(exp_name) - test_dir = fileutils.make_test_dir() + exp = Experiment(exp_name, exp_path=test_dir) model = exp.create_model( "model", path=test_dir, run_settings=RunSettings("sleep", "5") ) @@ -132,15 +133,14 @@ def test_poll(fileutils): exp.stop(model) -def test_summary(fileutils): +def test_summary(test_dir): exp_name = "test_exp_summary" - exp = Experiment(exp_name) - test_dir = fileutils.make_test_dir() + exp = Experiment(exp_name, exp_path=test_dir) m = exp.create_model( "model", path=test_dir, run_settings=RunSettings("echo", "Hello") ) exp.start(m) - summary_str = exp.summary(format="plain") + summary_str = exp.summary(style="plain") print(summary_str) summary_lines = summary_str.split("\n") @@ -155,6 +155,7 @@ def test_summary(fileutils): assert 0 == int(row["RunID"]) assert 0 == int(row["Returncode"]) + def test_launcher_detection(wlmutils, monkeypatch): if wlmutils.get_test_launcher() == "pals": pytest.skip(reason="Launcher detection cannot currently detect pbs vs pals") @@ -164,3 +165,16 @@ def test_launcher_detection(wlmutils, monkeypatch): exp = Experiment("test-launcher-detection", launcher="auto") assert exp._launcher == wlmutils.get_test_launcher() + + +def test_enable_disable_telemtery(monkeypatch): + # TODO: Currently these are implemented by setting an environment variable + # so that ALL experiments instanced in a driver script will begin + # producing telemetry data. In the future it is planned to have this + # work on a "per-instance" basis + monkeypatch.setattr(os, "environ", {}) + exp = Experiment("my-exp") + exp.enable_telemetry() + assert CONFIG.telemetry_enabled + exp.disable_telemetry() + assert not CONFIG.telemetry_enabled diff --git a/tests/test_generator.py b/tests/test_generator.py index 4307b2c2c..e4618f9cd 100644 --- a/tests/test_generator.py +++ b/tests/test_generator.py @@ -24,17 +24,20 @@ # 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. +import filecmp from os import path as osp import pytest +from tabulate import tabulate from smartsim import Experiment from smartsim._core.generation import Generator from smartsim.database import Orchestrator from smartsim.settings import RunSettings -from tabulate import tabulate -from smartsim.settings import SbatchSettings +# The tests in this file belong to the group_a group +pytestmark = pytest.mark.group_a + rs = RunSettings("python", exe_args="sleep.py") @@ -50,15 +53,18 @@ """ -def test_ensemble(fileutils): +def get_gen_file(fileutils, filename): + return fileutils.get_test_conf_path(osp.join("generator_files", filename)) + + +def test_ensemble(fileutils, test_dir): exp = Experiment("gen-test", launcher="local") - test_dir = fileutils.get_test_dir() - gen = Generator(test_dir) + gen = Generator(test_dir) params = {"THERMO": [10, 20, 30], "STEPS": [10, 20, 30]} ensemble = exp.create_ensemble("test", params=params, run_settings=rs) - config = fileutils.get_test_conf_path("in.atm") + config = get_gen_file(fileutils, "in.atm") ensemble.attach_generator_files(to_configure=config) gen.generate_experiment(ensemble) @@ -68,20 +74,20 @@ def test_ensemble(fileutils): assert osp.isdir(osp.join(test_dir, "test/test_" + str(i))) -def test_ensemble_overwrite(fileutils): +def test_ensemble_overwrite(fileutils, test_dir): exp = Experiment("gen-test-overwrite", launcher="local") - test_dir = fileutils.get_test_dir() + gen = Generator(test_dir, overwrite=True) params = {"THERMO": [10, 20, 30], "STEPS": [10, 20, 30]} ensemble = exp.create_ensemble("test", params=params, run_settings=rs) - config = fileutils.get_test_conf_path("in.atm") + config = get_gen_file(fileutils, "in.atm") ensemble.attach_generator_files(to_configure=[config]) gen.generate_experiment(ensemble) # re generate without overwrite - config = fileutils.get_test_conf_path("in.atm") + config = get_gen_file(fileutils, "in.atm") ensemble.attach_generator_files(to_configure=[config]) gen.generate_experiment(ensemble) @@ -91,27 +97,26 @@ def test_ensemble_overwrite(fileutils): assert osp.isdir(osp.join(test_dir, "test/test_" + str(i))) -def test_ensemble_overwrite_error(fileutils): +def test_ensemble_overwrite_error(fileutils, test_dir): exp = Experiment("gen-test-overwrite-error", launcher="local") - test_dir = fileutils.get_test_dir() + gen = Generator(test_dir) params = {"THERMO": [10, 20, 30], "STEPS": [10, 20, 30]} ensemble = exp.create_ensemble("test", params=params, run_settings=rs) - config = fileutils.get_test_conf_path("in.atm") + config = get_gen_file(fileutils, "in.atm") ensemble.attach_generator_files(to_configure=[config]) gen.generate_experiment(ensemble) # re generate without overwrite - config = fileutils.get_test_conf_path("in.atm") + config = get_gen_file(fileutils, "in.atm") ensemble.attach_generator_files(to_configure=[config]) with pytest.raises(FileExistsError): gen.generate_experiment(ensemble) -def test_full_exp(fileutils, wlmutils): - test_dir = fileutils.make_test_dir() +def test_full_exp(fileutils, test_dir, wlmutils): exp = Experiment("gen-test", test_dir, launcher="local") model = exp.create_model("model", run_settings=rs) @@ -122,7 +127,7 @@ def test_full_exp(fileutils, wlmutils): params = {"THERMO": [10, 20, 30], "STEPS": [10, 20, 30]} ensemble = exp.create_ensemble("test_ens", params=params, run_settings=rs) - config = fileutils.get_test_conf_path("in.atm") + config = get_gen_file(fileutils, "in.atm") ensemble.attach_generator_files(to_configure=config) exp.generate(orc, ensemble, model) @@ -132,40 +137,38 @@ def test_full_exp(fileutils, wlmutils): assert osp.isdir(osp.join(test_dir, "test_ens/test_ens_" + str(i))) # test for orc dir - assert osp.isdir(osp.join(test_dir, "database")) + assert osp.isdir(osp.join(test_dir, orc.name)) # test for model file assert osp.isdir(osp.join(test_dir, "model")) assert osp.isfile(osp.join(test_dir, "model/sleep.py")) -def test_dir_files(fileutils): +def test_dir_files(fileutils, test_dir): """test the generate of models with files that are directories with subdirectories and files """ - test_dir = fileutils.make_test_dir() exp = Experiment("gen-test", test_dir, launcher="local") params = {"THERMO": [10, 20, 30], "STEPS": [10, 20, 30]} ensemble = exp.create_ensemble("dir_test", params=params, run_settings=rs) - conf_dir = fileutils.get_test_dir_path("test_dir") + conf_dir = get_gen_file(fileutils, "test_dir") ensemble.attach_generator_files(to_configure=conf_dir) - exp.generate(ensemble) + exp.generate(ensemble, tag="@") assert osp.isdir(osp.join(test_dir, "dir_test/")) for i in range(9): model_path = osp.join(test_dir, "dir_test/dir_test_" + str(i)) assert osp.isdir(model_path) assert osp.isdir(osp.join(model_path, "test_dir_1")) - assert osp.isfile(osp.join(model_path, "test.py")) + assert osp.isfile(osp.join(model_path, "test.in")) -def test_print_files(fileutils, capsys): +def test_print_files(fileutils, test_dir, capsys): """Test the stdout print of files attached to an ensemble""" - test_dir = fileutils.make_test_dir() exp = Experiment("print-attached-files-test", test_dir, launcher="local") ensemble = exp.create_ensemble("dir_test", replicas=1, run_settings=rs) @@ -177,9 +180,9 @@ def test_print_files(fileutils, capsys): params = {"THERMO": [10, 20], "STEPS": [20, 30]} ensemble = exp.create_ensemble("dir_test", params=params, run_settings=rs) - gen_dir = fileutils.get_test_dir_path("test_dir") - symlink_dir = fileutils.get_test_dir_path("to_symlink_dir") - copy_dir = fileutils.get_test_dir_path("to_copy_dir") + gen_dir = get_gen_file(fileutils, "test_dir") + symlink_dir = get_gen_file(fileutils, "to_symlink_dir") + copy_dir = get_gen_file(fileutils, "to_copy_dir") ensemble.print_attached_files() captured = capsys.readouterr() @@ -243,9 +246,8 @@ def test_print_files(fileutils, capsys): assert captured.out == expected_out_multi -def test_multiple_tags(fileutils): +def test_multiple_tags(fileutils, test_dir): """Test substitution of multiple tagged parameters on same line""" - test_dir = fileutils.make_test_dir() exp = Experiment("test-multiple-tags", test_dir) model_params = {"port": 6379, "password": "unbreakable_password"} @@ -253,30 +255,65 @@ def test_multiple_tags(fileutils): parameterized_model = exp.create_model( "multi-tags", run_settings=model_settings, params=model_params ) - config = fileutils.get_test_conf_path("multi_tags_template.sh") + config = get_gen_file(fileutils, "multi_tags_template.sh") parameterized_model.attach_generator_files(to_configure=[config]) exp.generate(parameterized_model, overwrite=True) exp.start(parameterized_model, block=True) with open(osp.join(parameterized_model.path, "multi-tags.out")) as f: - line = f.readline() - assert ( - line.strip() == "My two parameters are 6379 and unbreakable_password, OK?" + log_content = f.read() + assert "My two parameters are 6379 and unbreakable_password, OK?" in log_content + + +def test_generation_log(fileutils, test_dir): + """Test that an error is issued when a tag is unused and make_fatal is True""" + + exp = Experiment("gen-log-test", test_dir, launcher="local") + + params = {"THERMO": [10, 20], "STEPS": [10, 20]} + ensemble = exp.create_ensemble("dir_test", params=params, run_settings=rs) + conf_file = get_gen_file(fileutils, "in.atm") + ensemble.attach_generator_files(to_configure=conf_file) + + def not_header(line): + """you can add other general checks in here""" + return not line.startswith("Generation start date and time:") + + exp.generate(ensemble, verbose=True) + + log_file = osp.join(test_dir, "smartsim_params.txt") + ground_truth = get_gen_file( + fileutils, osp.join("log_params", "smartsim_params.txt") + ) + + with open(log_file) as f1, open(ground_truth) as f2: + assert not not_header(f1.readline()) + f1 = filter(not_header, f1) + f2 = filter(not_header, f2) + assert all(x == y for x, y in zip(f1, f2)) + + for entity in ensemble: + assert filecmp.cmp( + osp.join(entity.path, "smartsim_params.txt"), + get_gen_file( + fileutils, + osp.join("log_params", "dir_test", entity.name, "smartsim_params.txt"), + ), ) -def test_config_dir(fileutils): +def test_config_dir(fileutils, test_dir): """Test the generation and configuration of models with tagged files that are directories with subdirectories and files """ exp = Experiment("config-dir", launcher="local") - test_dir = fileutils.make_test_dir() + gen = Generator(test_dir) params = {"PARAM0": [0, 1], "PARAM1": [2, 3]} ensemble = exp.create_ensemble("test", params=params, run_settings=rs) - config = fileutils.get_test_conf_path("tag_dir_template") + config = get_gen_file(fileutils, "tag_dir_template") ensemble.attach_generator_files(to_configure=config) gen.generate_experiment(ensemble) @@ -308,7 +345,7 @@ def test_no_gen_if_file_not_exist(fileutils): """ exp = Experiment("file-not-found", launcher="local") ensemble = exp.create_ensemble("test", params={"P": [0, 1]}, run_settings=rs) - config = fileutils.get_test_conf_path("path_not_exist") + config = get_gen_file(fileutils, "path_not_exist") with pytest.raises(FileNotFoundError): ensemble.attach_generator_files(to_configure=config) @@ -320,6 +357,23 @@ def test_no_gen_if_symlink_to_dir(fileutils): """ exp = Experiment("circular-config-files", launcher="local") ensemble = exp.create_ensemble("test", params={"P": [0, 1]}, run_settings=rs) - config = fileutils.get_test_conf_path("circular_config") + config = get_gen_file(fileutils, "circular_config") with pytest.raises(ValueError): ensemble.attach_generator_files(to_configure=config) + + +def test_no_file_overwrite(): + exp = Experiment("test_no_file_overwrite", launcher="local") + ensemble = exp.create_ensemble("test", params={"P": [0, 1]}, run_settings=rs) + with pytest.raises(ValueError): + ensemble.attach_generator_files( + to_configure=["/normal/file.txt", "/path/to/smartsim_params.txt"] + ) + with pytest.raises(ValueError): + ensemble.attach_generator_files( + to_symlink=["/normal/file.txt", "/path/to/smartsim_params.txt"] + ) + with pytest.raises(ValueError): + ensemble.attach_generator_files( + to_copy=["/normal/file.txt", "/path/to/smartsim_params.txt"] + ) diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 66af2dc0f..784219f82 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -24,22 +24,47 @@ # 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. +import pytest +from smartsim._core.utils import helpers from smartsim._core.utils.helpers import cat_arg_and_value +# The tests in this file belong to the group_a group +pytestmark = pytest.mark.group_a + def test_double_dash_concat(): result = cat_arg_and_value("--foo", "FOO") assert result == "--foo=FOO" + def test_single_dash_concat(): result = cat_arg_and_value("-foo", "FOO") assert result == "-foo FOO" + def test_single_char_concat(): result = cat_arg_and_value("x", "FOO") assert result == "-x FOO" + def test_fallthrough_concat(): result = cat_arg_and_value("xx", "FOO") # <-- no dashes, > 1 char assert result == "--xx=FOO" + + +def test_encode_decode_cmd_round_trip(): + orig_cmd = ["this", "is", "a", "cmd"] + decoded_cmd = helpers.decode_cmd(helpers.encode_cmd(orig_cmd)) + assert orig_cmd == decoded_cmd + assert orig_cmd is not decoded_cmd + + +def test_encode_raises_on_empty(): + with pytest.raises(ValueError): + helpers.encode_cmd([]) + + +def test_decode_raises_on_empty(): + with pytest.raises(ValueError): + helpers.decode_cmd("") diff --git a/tests/test_indirect.py b/tests/test_indirect.py new file mode 100644 index 000000000..f8af88266 --- /dev/null +++ b/tests/test_indirect.py @@ -0,0 +1,204 @@ +# BSD 2-Clause License +# +# Copyright (c) 2021-2023, 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. + + +import pathlib +import sys + +import psutil +import pytest + +from smartsim._core.entrypoints.indirect import cleanup, get_parser, get_ts, main +from smartsim._core.utils.helpers import encode_cmd +from smartsim._core.utils.serialize import MANIFEST_FILENAME, TELMON_SUBDIR + +ALL_ARGS = { + "+command", + "+entity_type", + "+telemetry_dir", + "+output_file", + "+error_file", + "+working_dir", +} + +# The tests in this file belong to the group_a group +pytestmark = pytest.mark.group_a + + +# fmt: off +@pytest.mark.parametrize( + ["cmd", "missing"], + [ + pytest.param("indirect.py", {"+name", "+command", "+entity_type", "+telemetry_dir", "+working_dir"}, id="no args"), + pytest.param("indirect.py -c echo +entity_type ttt +telemetry_dir ddd +output_file ooo +working_dir www +error_file eee", {"+command"}, id="cmd typo"), + pytest.param("indirect.py -t orchestrator +command ccc +telemetry_dir ddd +output_file ooo +working_dir www +error_file eee", {"+entity_type"}, id="etype typo"), + pytest.param("indirect.py -d /foo/bar +entity_type ttt +command ccc +output_file ooo +working_dir www +error_file eee", {"+telemetry_dir"}, id="dir typo"), + pytest.param("indirect.py +entity_type ttt +telemetry_dir ddd +output_file ooo +working_dir www +error_file eee", {"+command"}, id="no cmd"), + pytest.param("indirect.py +command ccc +telemetry_dir ddd +output_file ooo +working_dir www +error_file eee", {"+entity_type"}, id="no etype"), + pytest.param("indirect.py +command ccc +entity_type ttt +output_file ooo +working_dir www +error_file eee", {"+telemetry_dir"}, id="no dir"), + ] +) +# fmt: on +def test_parser(capsys, cmd, missing): + """Test that the parser reports any missing required arguments""" + parser = get_parser() + + args = cmd.split() + + captured = capsys.readouterr() # throw away existing output + with pytest.raises(SystemExit) as ex: + ns = parser.parse_args(args) + + captured = capsys.readouterr() + assert "the following arguments are required" in captured.err + for arg in missing: + assert arg in captured.err + + expected = ALL_ARGS - missing + msg_tuple = captured.err.split("the following arguments are required: ") + if len(msg_tuple) < 2: + assert False, "error message indicates no missing arguments" + + actual_missing = msg_tuple[1].strip() + for exp in expected: + assert f"{exp}/" not in actual_missing + + +def test_cleanup(capsys, monkeypatch): + """Ensure cleanup attempts termination of correct process""" + mock_pid = 123 + create_msg = "creating: {0}" + term_msg = "terminating: {0}" + + class MockProc: + def __init__(self, pid: int): + print(create_msg.format(pid)) + + def terminate(self): + print(term_msg.format(mock_pid)) + + captured = capsys.readouterr() # throw away existing output + + with monkeypatch.context() as ctx: + ctx.setattr("psutil.pid_exists", lambda pid: True) + ctx.setattr("psutil.Process", MockProc) + ctx.setattr("smartsim._core.entrypoints.indirect.STEP_PID", mock_pid) + cleanup() + + captured = capsys.readouterr() + assert create_msg.format(mock_pid) in captured.out + assert term_msg.format(mock_pid) in captured.out + + +def test_cleanup_late(capsys, monkeypatch): + """Ensure cleanup exceptions are swallowed if a process is already terminated""" + mock_pid = 123 + create_msg = "creating: {0}" + term_msg = "terminating: {0}" + + class MockMissingProc: + def __init__(self, pid: int) -> None: + print(create_msg.format(mock_pid)) + raise psutil.NoSuchProcess(pid) + + def terminate(self) -> None: + print(term_msg.format(mock_pid)) + + captured = capsys.readouterr() # throw away existing output + + with monkeypatch.context() as ctx: + ctx.setattr("psutil.pid_exists", lambda pid: True) + ctx.setattr("psutil.Process", MockMissingProc) + ctx.setattr("smartsim._core.entrypoints.indirect.STEP_PID", mock_pid) + cleanup() + + captured = capsys.readouterr() + assert create_msg.format(mock_pid) in captured.out + + +def test_ts(): + """Ensure expected output type""" + ts = get_ts() + assert isinstance(ts, int) + + +def test_indirect_main_dir_check(test_dir): + """Ensure that the proxy validates the test directory exists""" + exp_dir = pathlib.Path(test_dir) + + cmd = ["echo", "unit-test"] + encoded_cmd = encode_cmd(cmd) + + status_path = exp_dir / TELMON_SUBDIR + + # show that a missing status_path is created when missing + main(encoded_cmd, "application", exp_dir, status_path) + + assert status_path.exists() + + +def test_indirect_main_cmd_check(capsys, test_dir, monkeypatch): + """Ensure that the proxy validates the cmd is not empty or whitespace-only""" + exp_dir = pathlib.Path(test_dir) + + captured = capsys.readouterr() # throw away existing output + with monkeypatch.context() as ctx, pytest.raises(ValueError) as ex: + ctx.setattr("smartsim._core.entrypoints.indirect.logger.error", print) + _ = main("", "application", exp_dir, exp_dir / TELMON_SUBDIR) + + captured = capsys.readouterr() + assert "Invalid cmd supplied" in ex.value.args[0] + + # test with non-emptystring cmd + with monkeypatch.context() as ctx, pytest.raises(ValueError) as ex: + ctx.setattr("smartsim._core.entrypoints.indirect.logger.error", print) + _ = main(" \n \t ", "application", exp_dir, exp_dir / TELMON_SUBDIR) + + captured = capsys.readouterr() + assert "Invalid cmd supplied" in ex.value.args[0] + + +def test_complete_process(fileutils, test_dir): + """Ensure the happy-path completes and returns a success return code""" + script = fileutils.get_test_conf_path("sleep.py") + + exp_dir = pathlib.Path(test_dir) + + raw_cmd = f"{sys.executable} {script} --time=1" + cmd = encode_cmd(raw_cmd.split()) + + rc = main(cmd, "application", exp_dir, exp_dir / TELMON_SUBDIR) + assert rc == 0 + + assert exp_dir.exists() + + # NOTE: don't have a manifest so we're falling back to default event path + data_dir = exp_dir / TELMON_SUBDIR + start_events = list(data_dir.rglob("start.json")) + stop_events = list(data_dir.rglob("stop.json")) + + assert start_events + assert stop_events diff --git a/tests/test_init.py b/tests/test_init.py index 2b1befa0f..76f58b59a 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -25,6 +25,12 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +import pytest + +# The tests in this file belong to the group_a group +pytestmark = pytest.mark.group_a + + __author__ = "Sam Partee" diff --git a/tests/test_interrupt.py b/tests/test_interrupt.py index f51d7fc4d..73616a848 100644 --- a/tests/test_interrupt.py +++ b/tests/test_interrupt.py @@ -24,15 +24,19 @@ # 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. - import os import signal import time from threading import Thread +import pytest + from smartsim import Experiment from smartsim.settings import RunSettings +# The tests in this file belong to the group_a group +pytestmark = pytest.mark.group_a + def keyboard_interrupt(pid): """Interrupt main thread""" @@ -40,13 +44,13 @@ def keyboard_interrupt(pid): os.kill(pid, signal.SIGINT) -def test_interrupt_blocked_jobs(fileutils): +def test_interrupt_blocked_jobs(test_dir): """ Launches and polls a model and an ensemble with two more models. Once polling starts, the SIGINT signal is sent to the main thread, and consequently, all running jobs are killed. """ - test_dir = fileutils.make_test_dir() + exp_name = "test_interrupt_blocked_jobs" exp = Experiment(exp_name, exp_path=test_dir) model = exp.create_model( @@ -77,7 +81,7 @@ def test_interrupt_blocked_jobs(fileutils): assert len(completed_jobs) == num_jobs -def test_interrupt_multi_experiment_unblocked_jobs(fileutils): +def test_interrupt_multi_experiment_unblocked_jobs(test_dir): """ Starts two Experiments, each having one model and an ensemble with two more models. Since @@ -85,7 +89,7 @@ def test_interrupt_multi_experiment_unblocked_jobs(fileutils): the SIGINT signal is sent, resulting in both Experiment's running jobs to be killed. """ - test_dir = fileutils.make_test_dir() + exp_names = ["test_interrupt_jobs_0", "test_interrupt_jobs_1"] experiments = [Experiment(exp_names[i], exp_path=test_dir) for i in range(2)] jobs_per_experiment = [0] * len(experiments) diff --git a/tests/test_launch_errors.py b/tests/test_launch_errors.py index 3f0c5a252..51d8b60a6 100644 --- a/tests/test_launch_errors.py +++ b/tests/test_launch_errors.py @@ -29,9 +29,12 @@ from smartsim import Experiment, status from smartsim.database import Orchestrator -from smartsim.error import SmartSimError, SSUnsupportedError +from smartsim.error import SSUnsupportedError from smartsim.settings import JsrunSettings, RunSettings +# The tests in this file belong to the group_a group +pytestmark = pytest.mark.group_a + def test_unsupported_run_settings(): exp_name = "test-unsupported-run-settings" @@ -43,10 +46,9 @@ def test_unsupported_run_settings(): exp.start(model) -def test_model_failure(fileutils): +def test_model_failure(fileutils, test_dir): exp_name = "test-model-failure" - exp = Experiment(exp_name, launcher="local") - test_dir = fileutils.make_test_dir() + exp = Experiment(exp_name, launcher="local", exp_path=test_dir) script = fileutils.get_test_conf_path("bad.py") settings = RunSettings("python", f"{script} --time=3") @@ -58,19 +60,18 @@ def test_model_failure(fileutils): assert all([stat == status.STATUS_FAILED for stat in statuses]) -def test_orchestrator_relaunch(fileutils, wlmutils): - """Test error when users try to launch second orchestrator""" - exp_name = "test-orc-error-on-relaunch" - exp = Experiment(exp_name, launcher="local") - test_dir = fileutils.make_test_dir() +def test_orchestrator_relaunch(test_dir, wlmutils): + """Test when users try to launch second orchestrator""" + exp_name = "test-orc-on-relaunch" + exp = Experiment(exp_name, launcher="local", exp_path=test_dir) orc = Orchestrator(port=wlmutils.get_test_port()) orc.set_path(test_dir) orc_1 = Orchestrator(port=wlmutils.get_test_port() + 1) orc_1.set_path(test_dir) - - exp.start(orc) - with pytest.raises(SmartSimError): + try: + exp.start(orc) exp.start(orc_1) - - exp.stop(orc) + finally: + exp.stop(orc) + exp.stop(orc_1) diff --git a/tests/test_local_launch.py b/tests/test_local_launch.py index 857855205..e4d593b6f 100644 --- a/tests/test_local_launch.py +++ b/tests/test_local_launch.py @@ -24,18 +24,22 @@ # 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. +import pytest from smartsim import Experiment, status +# The tests in this file belong to the group_a group +pytestmark = pytest.mark.group_a + + """ Test the launch of simple entity types with local launcher """ -def test_models(fileutils): +def test_models(fileutils, test_dir): exp_name = "test-models-local-launch" - exp = Experiment(exp_name, launcher="local") - test_dir = fileutils.make_test_dir() + exp = Experiment(exp_name, launcher="local", exp_path=test_dir) script = fileutils.get_test_conf_path("sleep.py") settings = exp.create_run_settings("python", f"{script} --time=3") @@ -48,10 +52,9 @@ def test_models(fileutils): assert all([stat == status.STATUS_COMPLETED for stat in statuses]) -def test_ensemble(fileutils): +def test_ensemble(fileutils, test_dir): exp_name = "test-ensemble-launch" - exp = Experiment(exp_name, launcher="local") - test_dir = fileutils.make_test_dir() + exp = Experiment(exp_name, launcher="local", exp_path=test_dir) script = fileutils.get_test_conf_path("sleep.py") settings = exp.create_run_settings("python", f"{script} --time=3") diff --git a/tests/test_local_multi_run.py b/tests/test_local_multi_run.py index 76bfc898c..b6eaba56a 100644 --- a/tests/test_local_multi_run.py +++ b/tests/test_local_multi_run.py @@ -24,18 +24,22 @@ # 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. +import pytest from smartsim import Experiment, status +# The tests in this file belong to the group_a group +pytestmark = pytest.mark.group_a + + """ Test the launch of simple entity types with local launcher """ -def test_models(fileutils): +def test_models(fileutils, test_dir): exp_name = "test-models-local-launch" - exp = Experiment(exp_name, launcher="local") - test_dir = fileutils.make_test_dir() + exp = Experiment(exp_name, launcher="local", exp_path=test_dir) script = fileutils.get_test_conf_path("sleep.py") settings = exp.create_run_settings("python", f"{script} --time=5") diff --git a/tests/test_local_restart.py b/tests/test_local_restart.py index 99c6afd5e..b05401138 100644 --- a/tests/test_local_restart.py +++ b/tests/test_local_restart.py @@ -24,19 +24,22 @@ # 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. +import pytest from smartsim import Experiment, status +# The tests in this file belong to the group_b group +pytestmark = pytest.mark.group_b + + """ Test restarting ensembles and models. """ -def test_restart(fileutils): - +def test_restart(fileutils, test_dir): exp_name = "test-models-local-restart" - exp = Experiment(exp_name, launcher="local") - test_dir = fileutils.make_test_dir() + exp = Experiment(exp_name, launcher="local", exp_path=test_dir) script = fileutils.get_test_conf_path("sleep.py") settings = exp.create_run_settings("python", f"{script} --time=3") @@ -53,10 +56,9 @@ def test_restart(fileutils): assert all([stat == status.STATUS_COMPLETED for stat in statuses]) -def test_ensemble(fileutils): +def test_ensemble(fileutils, test_dir): exp_name = "test-ensemble-restart" - exp = Experiment(exp_name, launcher="local") - test_dir = fileutils.make_test_dir() + exp = Experiment(exp_name, launcher="local", exp_path=test_dir) script = fileutils.get_test_conf_path("sleep.py") settings = exp.create_run_settings("python", f"{script} --time=3") diff --git a/tests/test_lsf_parser.py b/tests/test_lsf_parser.py index 3b90eb0f3..f41de54d8 100644 --- a/tests/test_lsf_parser.py +++ b/tests/test_lsf_parser.py @@ -24,9 +24,14 @@ # 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. +import pytest from smartsim._core.launcher.lsf import lsfParser +# The tests in this file belong to the group_b group +pytestmark = pytest.mark.group_b + + # -- bsub --------------------------------------------------------- diff --git a/tests/test_lsf_settings.py b/tests/test_lsf_settings.py index 2770bde21..a71d658cb 100644 --- a/tests/test_lsf_settings.py +++ b/tests/test_lsf_settings.py @@ -25,13 +25,14 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -from pprint import pformat - import pytest from smartsim.error import SSUnsupportedError from smartsim.settings import BsubBatchSettings, JsrunSettings +# The tests in this file belong to the group_b group +pytestmark = pytest.mark.group_b + # ------ Jsrun ------------------------------------------------ @@ -105,16 +106,16 @@ def test_jsrun_args_mutation(): "np": 100, } settings = JsrunSettings("python", run_args=run_args) - + erf_settings = {"foo": "1", "bar": "2"} - + settings.set_erf_sets(erf_settings) assert settings.erf_sets["foo"] == "1" assert settings.erf_sets["bar"] == "2" erf_settings["foo"] = "111" erf_settings["bar"] = "111" - + assert settings.erf_sets["foo"] == "1" assert settings.erf_sets["bar"] == "2" @@ -241,7 +242,7 @@ def test_bsub_batch_manual(): def test_bsub_batch_alloc_flag_formatting_by_smt(): """Ensure that alloc_flags are formatted correctly when smts is changed""" - + # Check when no smt is set in the constructor sbatch = BsubBatchSettings() sbatch._format_alloc_flags() @@ -276,9 +277,9 @@ def test_bsub_batch_alloc_flag_formatting_by_smt(): # check multi-smt flag, with prefix sbatch = BsubBatchSettings(batch_args={"alloc_flags": '"smt3 smt4"'}, smts=4) sbatch._format_alloc_flags() - assert sbatch.batch_args["alloc_flags"] == "\"smt3 smt4\"" # <-- wrap in quotes - + assert sbatch.batch_args["alloc_flags"] == '"smt3 smt4"' # <-- wrap in quotes + # show that mismatched alloc_flags and smts are NOT touched - sbatch = BsubBatchSettings(batch_args={"alloc_flags": 'smt10'}, smts=2) + sbatch = BsubBatchSettings(batch_args={"alloc_flags": "smt10"}, smts=2) sbatch._format_alloc_flags() assert sbatch.batch_args["alloc_flags"] == "smt10" # <-- not smt2 diff --git a/tests/test_manifest.py b/tests/test_manifest.py index 7a856aea7..ea9920fad 100644 --- a/tests/test_manifest.py +++ b/tests/test_manifest.py @@ -25,20 +25,26 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +import os.path from copy import deepcopy import pytest from smartsim import Experiment -from smartsim._core.control import Manifest +from smartsim._core.control.manifest import ( + LaunchedManifest, + LaunchedManifestBuilder, + Manifest, +) +from smartsim._core.control.manifest import ( + _LaunchedManifestMetadata as LaunchedManifestMetadata, +) from smartsim.database import Orchestrator from smartsim.error import SmartSimError from smartsim.settings import RunSettings -try: - import tensorflow -except ImportError: - pass +# The tests in this file belong to the group_b group +pytestmark = pytest.mark.group_b # ---- create entities for testing -------- @@ -50,7 +56,6 @@ model_2 = exp.create_model("model_1", run_settings=rs) ensemble = exp.create_ensemble("ensemble", run_settings=rs, replicas=1) - orc = Orchestrator() orc_1 = deepcopy(orc) orc_1.name = "orc2" @@ -63,7 +68,7 @@ def test_separate(): assert len(manifest.models) == 1 assert manifest.ensembles[0] == ensemble assert len(manifest.ensembles) == 1 - assert manifest.db == orc + assert manifest.dbs[0] == orc def test_no_name(): @@ -71,12 +76,6 @@ def test_no_name(): _ = Manifest(model_no_name) -def test_two_orc(): - with pytest.raises(SmartSimError): - manifest = Manifest(orc, orc_1) - manifest.db - - def test_separate_type(): with pytest.raises(TypeError): _ = Manifest([1, 2, 3]) @@ -91,7 +90,7 @@ def test_catch_empty_ensemble(): e = deepcopy(ensemble) e.entities = [] with pytest.raises(ValueError): - manifest = Manifest(e) + _ = Manifest(e) def test_corner_case(): @@ -105,3 +104,79 @@ class Person: p = Person() with pytest.raises(TypeError): _ = Manifest(p) + + +def test_launched_manifest_transform_data(): + models = [(model, 1), (model_2, 2)] + ensembles = [(ensemble, [(m, i) for i, m in enumerate(ensemble.entities)])] + dbs = [(orc, [(n, i) for i, n in enumerate(orc.entities)])] + launched = LaunchedManifest( + metadata=LaunchedManifestMetadata("name", "path", "launcher", "run_id"), + models=models, + ensembles=ensembles, + databases=dbs, + ) + transformed = launched.map(lambda x: str(x)) + assert transformed.models == tuple((m, str(i)) for m, i in models) + assert transformed.ensembles[0][1] == tuple((m, str(i)) for m, i in ensembles[0][1]) + assert transformed.databases[0][1] == tuple((n, str(i)) for n, i in dbs[0][1]) + + +def test_launched_manifest_builder_correctly_maps_data(): + lmb = LaunchedManifestBuilder("name", "path", "launcher name") + lmb.add_model(model, 1) + lmb.add_model(model_2, 1) + lmb.add_ensemble(ensemble, [i for i in range(len(ensemble.entities))]) + lmb.add_database(orc, [i for i in range(len(orc.entities))]) + + manifest = lmb.finalize() + assert len(manifest.models) == 2 + assert len(manifest.ensembles) == 1 + assert len(manifest.databases) == 1 + + +def test_launced_manifest_builder_raises_if_lens_do_not_match(): + lmb = LaunchedManifestBuilder("name", "path", "launcher name") + with pytest.raises(ValueError): + lmb.add_ensemble(ensemble, list(range(123))) + with pytest.raises(ValueError): + lmb.add_database(orc, list(range(123))) + + +def test_launched_manifest_builer_raises_if_attaching_data_to_empty_collection( + monkeypatch, +): + lmb = LaunchedManifestBuilder("name", "path", "launcher") + monkeypatch.setattr(ensemble, "entities", []) + with pytest.raises(ValueError): + lmb.add_ensemble(ensemble, []) + + +def test_lmb_and_launched_manifest_have_same_paths_for_launched_metadata(): + exp_path = "/path/to/some/exp" + lmb = LaunchedManifestBuilder("exp_name", exp_path, "launcher") + manifest = lmb.finalize() + assert ( + lmb.exp_telemetry_subdirectory == manifest.metadata.exp_telemetry_subdirectory + ) + assert ( + lmb.run_telemetry_subdirectory == manifest.metadata.run_telemetry_subdirectory + ) + assert ( + os.path.commonprefix( + [ + manifest.metadata.run_telemetry_subdirectory, + manifest.metadata.exp_telemetry_subdirectory, + manifest.metadata.manifest_file_path, + exp_path, + ] + ) + == exp_path + ) + assert os.path.commonprefix( + [ + manifest.metadata.run_telemetry_subdirectory, + manifest.metadata.exp_telemetry_subdirectory, + manifest.metadata.manifest_file_path, + ] + ) == str(manifest.metadata.exp_telemetry_subdirectory) diff --git a/tests/test_model.py b/tests/test_model.py index feab88a90..88700ad23 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -27,12 +27,16 @@ import pytest from smartsim import Experiment +from smartsim._core.control.manifest import LaunchedManifestBuilder from smartsim._core.launcher.step import SbatchStep, SrunStep from smartsim.entity import Ensemble, Model from smartsim.error import EntityExistsError, SSUnsupportedError from smartsim.settings import RunSettings, SbatchSettings, SrunSettings from smartsim.settings.mpiSettings import _BaseMPISettings +# The tests in this file belong to the slow_tests group +pytestmark = pytest.mark.slow_tests + def test_register_incoming_entity_preexists(): exp = Experiment("experiment", launcher="local") @@ -85,8 +89,11 @@ def monkeypatch_exp_controller(monkeypatch): def _monkeypatch_exp_controller(exp): entity_steps = [] - def start_wo_job_manager(self, manifest, block=True, kill_on_interrupt=True): - self._launch(manifest) + def start_wo_job_manager( + self, exp_name, exp_path, manifest, block=True, kill_on_interrupt=True + ): + self._launch(exp_name, exp_path, manifest) + return LaunchedManifestBuilder("name", "path", "launcher").finalize() def launch_step_nop(self, step, entity): entity_steps.append((step, entity)) diff --git a/tests/test_modelwriter.py b/tests/test_modelwriter.py index ba5393c4f..4554a8b5a 100644 --- a/tests/test_modelwriter.py +++ b/tests/test_modelwriter.py @@ -32,16 +32,21 @@ import pytest from smartsim._core.generation.modelwriter import ModelWriter -from smartsim.error.errors import ParameterWriterError +from smartsim.error.errors import ParameterWriterError, SmartSimError from smartsim.settings import RunSettings +# The tests in this file belong to the group_b group +pytestmark = pytest.mark.group_b + + mw_run_settings = RunSettings("python", exe_args="sleep.py") -def test_write_easy_configs(fileutils): +def get_gen_file(fileutils, filename): + return fileutils.get_test_conf_path(path.join("generator_files", filename)) - test_dir = fileutils.make_test_dir() +def test_write_easy_configs(fileutils, test_dir): param_dict = { "5": 10, # MOM_input "FIRST": "SECOND", # example_input.i @@ -51,8 +56,8 @@ def test_write_easy_configs(fileutils): "1200": "120", # input.nml } - conf_path = fileutils.get_test_dir_path("easy/marked/") - correct_path = fileutils.get_test_dir_path("easy/correct/") + conf_path = get_gen_file(fileutils, "easy/marked/") + correct_path = get_gen_file(fileutils, "easy/correct/") # copy confs to gen directory dir_util.copy_tree(conf_path, test_dir) assert path.isdir(test_dir) @@ -68,10 +73,7 @@ def test_write_easy_configs(fileutils): assert filecmp.cmp(written, correct) -def test_write_med_configs(fileutils): - - test_dir = fileutils.make_test_dir() - +def test_write_med_configs(fileutils, test_dir): param_dict = { "1 0 0 0": "3 0 0 0", # in.ellipse.gayberne "'noleap'": "'leap'", # input.nml @@ -81,8 +83,8 @@ def test_write_med_configs(fileutils): "3*12.0": "3*14.0", # MOM_input } - conf_path = fileutils.get_test_dir_path("med/marked/") - correct_path = fileutils.get_test_dir_path("med/correct/") + conf_path = get_gen_file(fileutils, "med/marked/") + correct_path = get_gen_file(fileutils, "med/correct/") # copy confs to gen directory dir_util.copy_tree(conf_path, test_dir) @@ -101,11 +103,9 @@ def test_write_med_configs(fileutils): assert filecmp.cmp(written, correct) -def test_write_new_tag_configs(fileutils): +def test_write_new_tag_configs(fileutils, test_dir): """sets the tag to the dollar sign""" - test_dir = fileutils.make_test_dir() - param_dict = { "1 0 0 0": "3 0 0 0", # in.ellipse.gayberne "'noleap'": "'leap'", # input.nml @@ -115,8 +115,8 @@ def test_write_new_tag_configs(fileutils): "3*12.0": "3*14.0", # MOM_input } - conf_path = fileutils.get_test_dir_path("new-tag/marked/") - correct_path = fileutils.get_test_dir_path("new-tag/correct/") + conf_path = get_gen_file(fileutils, "new-tag/marked/") + correct_path = get_gen_file(fileutils, "new-tag/correct/") # copy confs to gen directory dir_util.copy_tree(conf_path, test_dir) @@ -144,3 +144,22 @@ def test_mw_error_2(): writer = ModelWriter() with pytest.raises(ParameterWriterError): writer._write_changes("[not/a/path]") + + +def test_write_mw_error_3(fileutils, test_dir): + param_dict = { + "5": 10, # MOM_input + } + + conf_path = get_gen_file(fileutils, "easy/marked/") + + # copy confs to gen directory + dir_util.copy_tree(conf_path, test_dir) + assert path.isdir(test_dir) + + # init modelwriter + writer = ModelWriter() + with pytest.raises(SmartSimError): + writer.configure_tagged_model_files( + glob(test_dir + "/*"), param_dict, make_missing_tags_fatal=True + ) diff --git a/tests/test_mpi_settings.py b/tests/test_mpi_settings.py index 275e8b75c..4e0bc48c8 100644 --- a/tests/test_mpi_settings.py +++ b/tests/test_mpi_settings.py @@ -40,6 +40,10 @@ _BaseMPISettings, ) +# The tests in this file belong to the group_b group +pytestmark = pytest.mark.group_b + + # Throw a warning instead of failing on machines without an MPI implementation default_mpi_args = (sys.executable,) default_mpi_kwargs = {"fail_if_missing_exec": False} @@ -115,7 +119,6 @@ def test_expected_openmpi_instance_without_warning( def test_error_if_slurm_mpiexec(fileutils): - stubs_path = osp.join("mpi_impl_stubs", "slurm") stubs_path = fileutils.get_test_dir_path(stubs_path) stub_exe = osp.join(stubs_path, "mpiexec") diff --git a/tests/test_multidb.py b/tests/test_multidb.py new file mode 100644 index 000000000..c4336294e --- /dev/null +++ b/tests/test_multidb.py @@ -0,0 +1,490 @@ +# BSD 2-Clause License +# +# Copyright (c) 2021-2023, 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 contextlib import contextmanager + +import pytest + +from smartsim import Experiment, status +from smartsim.database import Orchestrator +from smartsim.entity.entity import SmartSimEntity +from smartsim.error.errors import SSDBIDConflictError +from smartsim.log import get_logger + +# The tests in this file belong to the group_b group +pytestmark = pytest.mark.group_b + + +logger = get_logger(__name__) + +supported_dbs = ["uds", "tcp"] + +on_wlm = (pytest.test_launcher in pytest.wlm_options,) + + +@contextmanager +def make_entity_context(exp: Experiment, entity: SmartSimEntity): + """Start entity in a context to ensure that it is always stopped""" + exp.generate(entity, overwrite=True) + try: + yield entity + finally: + if exp.get_status(entity)[0] == status.STATUS_RUNNING: + exp.stop(entity) + + +def choose_host(wlmutils, index=0): + hosts = wlmutils.get_test_hostlist() + if hosts: + return hosts[index] + else: + return None + + +def check_not_failed(exp, *args): + statuses = exp.get_status(*args) + assert all(stat is not status.STATUS_FAILED for stat in statuses) + + +@pytest.mark.parametrize("db_type", supported_dbs) +def test_db_identifier_standard_then_colo_error( + fileutils, wlmutils, coloutils, db_type, test_dir +): + """Test that it is possible to create_database then colocate_db_uds/colocate_db_tcp + with unique db_identifiers""" + + # Set experiment name + exp_name = "test_db_identifier_standard_then_colo" + + # Retrieve parameters from testing environment + test_launcher = wlmutils.get_test_launcher() + test_interface = wlmutils.get_test_interface() + test_port = wlmutils.get_test_port() + + test_script = fileutils.get_test_conf_path("smartredis/db_id_err.py") + + # Create SmartSim Experiment + exp = Experiment(exp_name, launcher=test_launcher, exp_path=test_dir) + + # create regular database + orc = exp.create_database( + port=test_port, + interface=test_interface, + db_identifier="testdb_colo", + hosts=choose_host(wlmutils), + ) + assert orc.name == "testdb_colo" + + db_args = { + "port": test_port + 1, + "db_cpus": 1, + "debug": True, + "db_identifier": "testdb_colo", + } + + smartsim_model = coloutils.setup_test_colo( + fileutils, db_type, exp, test_script, db_args, on_wlm=on_wlm + ) + + assert ( + smartsim_model.run_settings.colocated_db_settings["db_identifier"] + == "testdb_colo" + ) + + with make_entity_context(exp, orc), make_entity_context(exp, smartsim_model): + exp.start(orc) + with pytest.raises(SSDBIDConflictError) as ex: + exp.start(smartsim_model) + + assert ( + "has already been used. Pass in a unique name for db_identifier" + in ex.value.args[0] + ) + check_not_failed(exp, orc) + + +@pytest.mark.parametrize("db_type", supported_dbs) +def test_db_identifier_colo_then_standard( + fileutils, wlmutils, coloutils, db_type, test_dir +): + """Test colocate_db_uds/colocate_db_tcp then create_database with database + identifiers. + """ + + # Set experiment name + exp_name = "test_db_identifier_colo_then_standard" + + # Retrieve parameters from testing environment + test_launcher = wlmutils.get_test_launcher() + test_interface = wlmutils.get_test_interface() + test_port = wlmutils.get_test_port() + test_script = fileutils.get_test_conf_path("smartredis/dbid.py") + + # Create SmartSim Experiment + exp = Experiment(exp_name, launcher=test_launcher, exp_path=test_dir) + + # Create run settings + colo_settings = exp.create_run_settings("python", test_script) + colo_settings.set_nodes(1) + colo_settings.set_tasks_per_node(1) + + # Create the SmartSim Model + smartsim_model = exp.create_model("colocated_model", colo_settings) + smartsim_model.set_path(test_dir) + + db_args = { + "port": test_port, + "db_cpus": 1, + "debug": True, + "db_identifier": "testdb_colo", + } + + smartsim_model = coloutils.setup_test_colo( + fileutils, + db_type, + exp, + test_script, + db_args, + on_wlm=on_wlm, + ) + + assert ( + smartsim_model.run_settings.colocated_db_settings["db_identifier"] + == "testdb_colo" + ) + + # Create Database + orc = exp.create_database( + port=test_port + 1, + interface=test_interface, + db_identifier="testdb_colo", + hosts=choose_host(wlmutils), + ) + + assert orc.name == "testdb_colo" + + with make_entity_context(exp, orc), make_entity_context(exp, smartsim_model): + exp.start(smartsim_model, block=True) + exp.start(orc) + + check_not_failed(exp, orc, smartsim_model) + + +def test_db_identifier_standard_twice_not_unique(wlmutils, test_dir): + """Test uniqueness of db_identifier several calls to create_database, with non unique names, + checking error is raised before exp start is called""" + + # Set experiment name + exp_name = "test_db_identifier_multiple_create_database_not_unique" + + # Retrieve parameters from testing environment + test_launcher = wlmutils.get_test_launcher() + test_interface = wlmutils.get_test_interface() + test_port = wlmutils.get_test_port() + + # Create SmartSim Experiment + exp = Experiment(exp_name, launcher=test_launcher, exp_path=test_dir) + + # CREATE DATABASE with db_identifier + orc = exp.create_database( + port=test_port, + interface=test_interface, + db_identifier="my_db", + hosts=choose_host(wlmutils), + ) + + assert orc.name == "my_db" + + orc2 = exp.create_database( + port=test_port + 1, + interface=test_interface, + db_identifier="my_db", + hosts=choose_host(wlmutils, index=1), + ) + + assert orc2.name == "my_db" + + # CREATE DATABASE with db_identifier + with make_entity_context(exp, orc), make_entity_context(exp, orc2): + exp.start(orc) + with pytest.raises(SSDBIDConflictError) as ex: + exp.start(orc2) + assert ( + "has already been used. Pass in a unique name for db_identifier" + in ex.value.args[0] + ) + check_not_failed(exp, orc) + + +def test_db_identifier_create_standard_once(test_dir, wlmutils): + """One call to create database with a database identifier""" + + # Set experiment name + exp_name = "test_db_identifier_create_standard_once" + + # Retrieve parameters from testing environment + test_launcher = wlmutils.get_test_launcher() + test_interface = wlmutils.get_test_interface() + test_port = wlmutils.get_test_port() + + # Create the SmartSim Experiment + exp = Experiment(exp_name, exp_path=test_dir, launcher=test_launcher) + + # Create the SmartSim database + db = exp.create_database( + port=test_port, + db_nodes=1, + interface=test_interface, + db_identifier="testdb_reg", + hosts=choose_host(wlmutils), + ) + with make_entity_context(exp, db): + exp.start(db) + + check_not_failed(exp, db) + + +def test_multidb_create_standard_twice(wlmutils, test_dir): + """Multiple calls to create database with unique db_identifiers""" + + # Retrieve parameters from testing environment + test_launcher = wlmutils.get_test_launcher() + test_interface = wlmutils.get_test_interface() + test_port = wlmutils.get_test_port() + + # start a new Experiment for this section + exp = Experiment( + "test_multidb_create_standard_twice", exp_path=test_dir, launcher=test_launcher + ) + + # create and start an instance of the Orchestrator database + db = exp.create_database( + port=test_port, + interface=test_interface, + db_identifier="testdb_reg", + hosts=choose_host(wlmutils, 1), + ) + + # create database with different db_id + db2 = exp.create_database( + port=test_port + 1, + interface=test_interface, + db_identifier="testdb_reg2", + hosts=choose_host(wlmutils, 2), + ) + + # launch + with make_entity_context(exp, db), make_entity_context(exp, db2): + exp.start(db, db2) + + with make_entity_context(exp, db), make_entity_context(exp, db2): + exp.start(db, db2) + + +@pytest.mark.parametrize("db_type", supported_dbs) +def test_multidb_colo_once(fileutils, test_dir, wlmutils, coloutils, db_type): + """create one model with colocated database with db_identifier""" + + # Retrieve parameters from testing environment + test_launcher = wlmutils.get_test_launcher() + test_port = wlmutils.get_test_port() + + test_script = fileutils.get_test_conf_path("smartredis/dbid.py") + + # start a new Experiment for this section + exp = Experiment( + "test_multidb_colo_once", launcher=test_launcher, exp_path=test_dir + ) + + # create run settings + run_settings = exp.create_run_settings("python", test_script) + run_settings.set_nodes(1) + run_settings.set_tasks_per_node(1) + + # Create the SmartSim Model + smartsim_model = exp.create_model("smartsim_model", run_settings) + smartsim_model.set_path(test_dir) + + db_args = { + "port": test_port + 1, + "db_cpus": 1, + "debug": True, + "db_identifier": "testdb_colo", + } + # Create model with colocated database + + smartsim_model = coloutils.setup_test_colo( + fileutils, + db_type, + exp, + test_script, + db_args, + on_wlm=on_wlm, + ) + + with make_entity_context(exp, smartsim_model): + exp.start(smartsim_model) + + check_not_failed(exp, smartsim_model) + + +@pytest.mark.parametrize("db_type", supported_dbs) +def test_multidb_standard_then_colo(fileutils, test_dir, wlmutils, coloutils, db_type): + """Create regular database then colocate_db_tcp/uds with unique db_identifiers""" + + # Retrieve parameters from testing environment + test_port = wlmutils.get_test_port() + + test_script = fileutils.get_test_conf_path("smartredis/multidbid.py") + test_interface = wlmutils.get_test_interface() + test_launcher = wlmutils.get_test_launcher() + + # start a new Experiment for this section + exp = Experiment( + "test_multidb_standard_then_colo", exp_path=test_dir, launcher=test_launcher + ) + + # create and generate an instance of the Orchestrator database + db = exp.create_database( + port=test_port, + interface=test_interface, + db_identifier="testdb_reg", + hosts=choose_host(wlmutils), + ) + + db_args = { + "port": test_port + 1, + "db_cpus": 1, + "debug": True, + "db_identifier": "testdb_colo", + } + # Create model with colocated database + smartsim_model = coloutils.setup_test_colo( + fileutils, + db_type, + exp, + test_script, + db_args, + on_wlm=on_wlm, + ) + + with make_entity_context(exp, db), make_entity_context(exp, smartsim_model): + exp.start(db) + exp.start(smartsim_model, block=True) + + check_not_failed(exp, smartsim_model, db) + + +@pytest.mark.parametrize("db_type", supported_dbs) +def test_multidb_colo_then_standard(fileutils, test_dir, wlmutils, coloutils, db_type): + """create regular database then colocate_db_tcp/uds with unique db_identifiers""" + + # Retrieve parameters from testing environment + test_port = wlmutils.get_test_port() + + test_script = fileutils.get_test_conf_path("smartredis/multidbid.py") + test_interface = wlmutils.get_test_interface() + test_launcher = wlmutils.get_test_launcher() + + # start a new Experiment + exp = Experiment( + "test_multidb_colo_then_standard", exp_path=test_dir, launcher=test_launcher + ) + + db_args = { + "port": test_port, + "db_cpus": 1, + "debug": True, + "db_identifier": "testdb_colo", + } + + # Create model with colocated database + smartsim_model = coloutils.setup_test_colo( + fileutils, db_type, exp, test_script, db_args, on_wlm=on_wlm + ) + + # create and start an instance of the Orchestrator database + db = exp.create_database( + port=test_port + 1, + interface=test_interface, + db_identifier="testdb_reg", + hosts=choose_host(wlmutils), + ) + + with make_entity_context(exp, db), make_entity_context(exp, smartsim_model): + exp.start(db) + exp.start(smartsim_model, block=True) + + check_not_failed(exp, db, smartsim_model) + + +@pytest.mark.skipif( + pytest.test_launcher not in pytest.wlm_options, + reason="Not testing WLM integrations", +) +@pytest.mark.parametrize("db_type", supported_dbs) +def test_launch_cluster_orc_single_dbid( + test_dir, coloutils, fileutils, wlmutils, db_type +): + """test clustered 3-node orchestrator with single command with a database identifier""" + # TODO detect number of nodes in allocation and skip if not sufficent + + exp_name = "test_launch_cluster_orc_single_dbid" + launcher = wlmutils.get_test_launcher() + test_port = wlmutils.get_test_port() + test_script = fileutils.get_test_conf_path("smartredis/multidbid.py") + exp = Experiment(exp_name, launcher=launcher, exp_path=test_dir) + + # batch = False to launch on existing allocation + network_interface = wlmutils.get_test_interface() + orc: Orchestrator = exp.create_database( + wlmutils.get_test_port(), + db_nodes=3, + batch=False, + interface=network_interface, + single_cmd=True, + hosts=wlmutils.get_test_hostlist(), + db_identifier="testdb_reg", + ) + + db_args = { + "port": test_port, + "db_cpus": 1, + "debug": True, + "db_identifier": "testdb_colo", + } + + # Create model with colocated database + smartsim_model = coloutils.setup_test_colo( + fileutils, db_type, exp, test_script, db_args, on_wlm=on_wlm + ) + + with make_entity_context(exp, orc), make_entity_context(exp, smartsim_model): + exp.start(orc, block=True) + exp.start(smartsim_model, block=True) + job_dict = exp._control._jobs.get_db_host_addresses() + assert len(job_dict[orc.entities[0].db_identifier]) == 3 + + check_not_failed(exp, orc, smartsim_model) diff --git a/tests/test_orc_config_settings.py b/tests/test_orc_config_settings.py index 18286c463..f08467be0 100644 --- a/tests/test_orc_config_settings.py +++ b/tests/test_orc_config_settings.py @@ -36,6 +36,9 @@ except AttributeError: pytestmark = pytest.mark.skip(reason="SmartRedis version is < 0.3.1") +# The tests in this file belong to the group_b group +pytestmark = pytest.mark.group_b + def test_config_methods(dbutils, local_db): """Test all configuration file edit methods on an active db""" diff --git a/tests/test_orchestrator.py b/tests/test_orchestrator.py index a634009d9..4a1b08367 100644 --- a/tests/test_orchestrator.py +++ b/tests/test_orchestrator.py @@ -33,6 +33,9 @@ from smartsim.error import SmartSimError from smartsim.error.errors import SSUnsupportedError +# The tests in this file belong to the slow_tests group +pytestmark = pytest.mark.slow_tests + def test_orc_parameters(): threads_per_queue = 2 @@ -65,10 +68,9 @@ def test_inactive_orc_get_address(): db.get_address() -def test_orc_active_functions(fileutils, wlmutils): +def test_orc_active_functions(test_dir, wlmutils): exp_name = "test_orc_active_functions" - exp = Experiment(exp_name, launcher="local") - test_dir = fileutils.make_test_dir() + exp = Experiment(exp_name, launcher="local", exp_path=test_dir) db = Orchestrator(port=wlmutils.get_test_port()) db.set_path(test_dir) @@ -93,10 +95,9 @@ def test_orc_active_functions(fileutils, wlmutils): db.get_address() -def test_multiple_interfaces(fileutils, wlmutils): +def test_multiple_interfaces(test_dir, wlmutils): exp_name = "test_multiple_interfaces" - exp = Experiment(exp_name, launcher="local") - test_dir = fileutils.make_test_dir() + exp = Experiment(exp_name, launcher="local", exp_path=test_dir) net_if_addrs = psutil.net_if_addrs() net_if_addrs = [ @@ -226,6 +227,35 @@ def test_slurm_set_batch_arg(wlmutils): assert orc2.batch_settings.batch_args["account"] == "ACCOUNT" +@pytest.mark.parametrize( + "single_cmd", + [ + pytest.param(True, id="Single MPMD `srun`"), + pytest.param(False, id="Multiple `srun`s"), + ], +) +def test_orc_results_in_correct_number_of_shards(single_cmd): + num_shards = 5 + orc = Orchestrator( + port=12345, + launcher="slurm", + run_command="srun", + db_nodes=num_shards, + batch=False, + single_cmd=single_cmd, + ) + if single_cmd: + assert len(orc.entities) == 1 + (node,) = orc.entities + assert len(node.run_settings.mpmd) == num_shards - 1 + else: + assert len(orc.entities) == num_shards + assert all(node.run_settings.mpmd == [] for node in orc.entities) + assert ( + orc.num_shards == orc.db_nodes == sum(node.num_shards for node in orc.entities) + ) + + ###### Cobalt ###### diff --git a/tests/test_pals_settings.py b/tests/test_pals_settings.py index 7bc3a6520..2cd725f65 100644 --- a/tests/test_pals_settings.py +++ b/tests/test_pals_settings.py @@ -31,14 +31,26 @@ import pytest -from smartsim.error import SSUnsupportedError -from smartsim.settings import PalsMpiexecSettings +import smartsim._core.config.config from smartsim._core.launcher import PBSLauncher from smartsim._core.launcher.step.mpiStep import MpiexecStep +from smartsim.error import SSUnsupportedError +from smartsim.settings import PalsMpiexecSettings + +# The tests in this file belong to the group_b group +pytestmark = pytest.mark.group_b + default_exe = sys.executable default_kwargs = {"fail_if_missing_exec": False} + +@pytest.fixture(autouse=True) +def turn_off_telemetry_indirect(monkeypatch): + monkeypatch.setattr(smartsim._core.config.config.Config, "telemetry_enabled", False) + yield + + # Uncomment when # @pytest.mark.parametrize( # "function_name",[ @@ -55,6 +67,12 @@ # func(None) +def test_affinity_script(): + settings = PalsMpiexecSettings(default_exe, **default_kwargs) + settings.set_gpu_affinity_script("/path/to/set_affinity_gpu.sh", 1, 2) + assert settings.format_run_args() == ["/path/to/set_affinity_gpu.sh", "1", "2"] + + def test_cpu_binding_type(): settings = PalsMpiexecSettings(default_exe, **default_kwargs) settings.set_cpu_binding_type("numa") @@ -115,7 +133,7 @@ def set_env_var_to_inherit(rs): ], ) def test_pbs_can_make_step_from_pals_settings_fmt_cmd( - monkeypatch, mock_mpiexec, fileutils, rs_mutation, run_args + monkeypatch, mock_mpiexec, test_dir, rs_mutation, run_args ): # Setup run settings exe_args = ["-c", """'print("Hello")'"""] @@ -126,7 +144,7 @@ def test_pbs_can_make_step_from_pals_settings_fmt_cmd( launcher = PBSLauncher() monkeypatch.setenv(f"PBS_JOBID", "mock-job") - wdir = fileutils.make_test_dir() + wdir = test_dir step = launcher.create_step("my_step", wdir, rs) assert isinstance(step, MpiexecStep) assert step.get_launch_cmd() == [ @@ -139,7 +157,7 @@ def test_pbs_can_make_step_from_pals_settings_fmt_cmd( ] -def test_pals_settings_can_be_correctly_made_mpmd(monkeypatch, fileutils, mock_mpiexec): +def test_pals_settings_can_be_correctly_made_mpmd(monkeypatch, test_dir, mock_mpiexec): # Setup run settings def make_rs(exe, exe_args): return PalsMpiexecSettings(exe, exe_args), [exe] + exe_args @@ -166,7 +184,7 @@ def set_tasks(rs, num): launcher = PBSLauncher() monkeypatch.setenv(f"PBS_JOBID", "mock-job") - wdir = fileutils.make_test_dir() + wdir = test_dir step = launcher.create_step("my_step", wdir, rs_1) assert isinstance(step, MpiexecStep) assert step.get_launch_cmd() == [ diff --git a/tests/test_pbs_parser.py b/tests/test_pbs_parser.py index 638fa496c..554780cd7 100644 --- a/tests/test_pbs_parser.py +++ b/tests/test_pbs_parser.py @@ -24,11 +24,16 @@ # 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 os.path import dirname from pathlib import Path +import pytest + from smartsim._core.launcher.pbs import pbsParser +# The tests in this file belong to the group_b group +pytestmark = pytest.mark.group_b + + # -- qsub --------------------------------------------------------- diff --git a/tests/test_pbs_settings.py b/tests/test_pbs_settings.py new file mode 100644 index 000000000..ed450bd82 --- /dev/null +++ b/tests/test_pbs_settings.py @@ -0,0 +1,128 @@ +# BSD 2-Clause License +# +# Copyright (c) 2021-2023, 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. + +import pytest + +from smartsim.error import SSConfigError +from smartsim.settings import QsubBatchSettings + +# The tests in this file belong to the group_b group +pytestmark = pytest.mark.group_b + + +def test_node_formatting(): + def validate_settings(settings, spec, num_nodes, num_cpus): + assert settings._create_resource_list() == [ + f"-l {spec}={num_nodes}:ncpus={num_cpus}" + ] + assert settings._ncpus == num_cpus + + num_nodes = 10 + num_cpus = 36 + + # Test by specifying the number of nodes via setting a resource + for spec in ["nodes", "select"]: + # Test by setting nodes + settings = QsubBatchSettings() + settings.set_resource(spec, num_nodes) + settings.set_ncpus(36) + validate_settings(settings, spec, num_nodes, num_cpus) + + # Test when setting nodes through the constructor + settings = QsubBatchSettings(ncpus=num_cpus, nodes=num_nodes) + validate_settings(settings, "nodes", num_nodes, num_cpus) + + # Test when setting nodes through the constructor via resource + settings = QsubBatchSettings(ncpus=num_cpus, resources={"nodes": num_nodes}) + validate_settings(settings, "nodes", num_nodes, num_cpus) + + # Test when setting select through the constructor via resource + settings = QsubBatchSettings(ncpus=num_cpus, resources={"select": num_nodes}) + validate_settings(settings, "select", num_nodes, num_cpus) + + +def test_select_nodes_error(): + # # Test failure on initialization + with pytest.raises(SSConfigError): + QsubBatchSettings(nodes=10, resources={"select": 10}) + + # Test setting via nodes and then select + settings = QsubBatchSettings() + settings.set_nodes(10) + with pytest.raises(SSConfigError): + settings.set_resource("select", 10) + + # Manually put "select" in the resource dictionary and + # make sure the resource formatter catches the error + settings = QsubBatchSettings() + with pytest.raises(SSConfigError): + settings.resources = {"nodes": 10, "select": 20} + + # # Test setting via select and then nodes + settings = QsubBatchSettings() + settings.set_resource("select", 10) + with pytest.raises(SSConfigError): + settings.set_nodes(10) + + +def test_resources_is_a_copy(): + settings = QsubBatchSettings() + resources = settings.resources + assert resources is not settings._resources + + +def test_nodes_and_select_not_ints_error(): + expected_error = TypeError + with pytest.raises(expected_error): + settings = QsubBatchSettings() + settings.set_nodes("10") + with pytest.raises(expected_error): + settings = QsubBatchSettings() + settings.set_resource("nodes", "10") + with pytest.raises(expected_error): + settings = QsubBatchSettings() + settings.set_resource("select", "10") + with pytest.raises(expected_error): + settings = QsubBatchSettings() + settings.resources = {"nodes": "10"} + with pytest.raises(expected_error): + settings = QsubBatchSettings() + settings.resources = {"select": "10"} + + +def test_resources_not_set_on_error(): + settings = QsubBatchSettings(nodes=10) + unaltered_resources = settings.resources + with pytest.raises(SSConfigError): + settings.resources = {"nodes": 10, "select": 10} + + assert unaltered_resources == settings.resources + + +def test_valid_types_in_resources(): + settings = QsubBatchSettings(nodes=10) + with pytest.raises(TypeError): + settings.set_resource("foo", None) diff --git a/tests/test_reconnect_orchestrator.py b/tests/test_reconnect_orchestrator.py index 609ba48e7..0faa92242 100644 --- a/tests/test_reconnect_orchestrator.py +++ b/tests/test_reconnect_orchestrator.py @@ -24,25 +24,29 @@ # 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. - import os.path as osp import time +import pytest + from smartsim import Experiment, status from smartsim.database import Orchestrator +# The tests in this file belong to the group_b group +pytestmark = pytest.mark.group_b + + first_dir = "" # TODO ensure database is shutdown # use https://stackoverflow.com/questions/22627659/run-code-before-and-after-each-test-in-py-test -def test_local_orchestrator(fileutils, wlmutils): +def test_local_orchestrator(test_dir, wlmutils): """Test launching orchestrator locally""" global first_dir exp_name = "test-orc-launch-local" - exp = Experiment(exp_name, launcher="local") - test_dir = fileutils.make_test_dir() + exp = Experiment(exp_name, launcher="local", exp_path=test_dir) first_dir = test_dir orc = Orchestrator(port=wlmutils.get_test_port()) @@ -57,12 +61,12 @@ def test_local_orchestrator(fileutils, wlmutils): exp._control._launcher.task_manager.actively_monitoring = False -def test_reconnect_local_orc(): +def test_reconnect_local_orc(test_dir): """Test reconnecting to orchestrator from first experiment""" global first_dir # start new experiment exp_name = "test-orc-local-reconnect-2nd" - exp_2 = Experiment(exp_name, launcher="local") + exp_2 = Experiment(exp_name, launcher="local", exp_path=test_dir) checkpoint = osp.join(first_dir, "smartsim_db.dat") reloaded_orc = exp_2.reconnect_orchestrator(checkpoint) diff --git a/tests/test_run_settings.py b/tests/test_run_settings.py index 8b47cb5d6..7bcd6d874 100644 --- a/tests/test_run_settings.py +++ b/tests/test_run_settings.py @@ -42,6 +42,9 @@ ) from smartsim.settings.settings import create_run_settings +# The tests in this file belong to the slow_tests group +pytestmark = pytest.mark.slow_tests + def test_create_run_settings_local(): # no run command provided diff --git a/tests/test_serialize.py b/tests/test_serialize.py new file mode 100644 index 000000000..167e7e445 --- /dev/null +++ b/tests/test_serialize.py @@ -0,0 +1,175 @@ +# BSD 2-Clause License +# +# Copyright (c) 2021-2023, 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. + +import json +import logging +from pathlib import Path + +import pytest + +import smartsim._core.config.config +from smartsim import Experiment +from smartsim._core._cli import utils +from smartsim._core.control.manifest import LaunchedManifestBuilder +from smartsim._core.utils import serialize +from smartsim.database.orchestrator import Orchestrator + +_REL_MANIFEST_PATH = f"{serialize.TELMON_SUBDIR}/{serialize.MANIFEST_FILENAME}" +_CFG_TM_ENABLED_ATTR = "telemetry_enabled" + +# The tests in this file belong to the group_b group +pytestmark = pytest.mark.group_b + + +@pytest.fixture(autouse=True) +def turn_on_tm(monkeypatch): + monkeypatch.setattr( + smartsim._core.config.config.Config, + _CFG_TM_ENABLED_ATTR, + property(lambda self: True), + ) + yield + + +def test_serialize_creates_a_manifest_json_file_if_dne(test_dir): + lmb = LaunchedManifestBuilder("exp", test_dir, "launcher") + serialize.save_launch_manifest(lmb.finalize()) + manifest_json = Path(test_dir) / _REL_MANIFEST_PATH + + assert manifest_json.is_file() + with open(manifest_json, "r") as f: + manifest = json.load(f) + assert manifest["experiment"]["name"] == "exp" + assert manifest["experiment"]["launcher"] == "launcher" + assert isinstance(manifest["runs"], list) + assert len(manifest["runs"]) == 1 + + +def test_serialize_does_not_write_manifest_json_if_telemetry_monitor_is_off( + test_dir, monkeypatch +): + monkeypatch.setattr( + smartsim._core.config.config.Config, + _CFG_TM_ENABLED_ATTR, + property(lambda self: False), + ) + lmb = LaunchedManifestBuilder("exp", test_dir, "launcher") + serialize.save_launch_manifest(lmb.finalize()) + manifest_json = Path(test_dir) / _REL_MANIFEST_PATH + assert not manifest_json.exists() + + +def test_serialize_appends_a_manifest_json_exists(test_dir): + manifest_json = Path(test_dir) / _REL_MANIFEST_PATH + serialize.save_launch_manifest( + LaunchedManifestBuilder("exp", test_dir, "launcher").finalize() + ) + serialize.save_launch_manifest( + LaunchedManifestBuilder("exp", test_dir, "launcher").finalize() + ) + serialize.save_launch_manifest( + LaunchedManifestBuilder("exp", test_dir, "launcher").finalize() + ) + + assert manifest_json.is_file() + with open(manifest_json, "r") as f: + manifest = json.load(f) + assert isinstance(manifest["runs"], list) + assert len(manifest["runs"]) == 3 + assert len({run["run_id"] for run in manifest["runs"]}) == 3 + + +def test_serialize_overwites_file_if_not_json(test_dir): + manifest_json = Path(test_dir) / _REL_MANIFEST_PATH + manifest_json.parent.mkdir(parents=True, exist_ok=True) + with open(manifest_json, "w") as f: + f.write("This is not a json\n") + + lmb = LaunchedManifestBuilder("exp", test_dir, "launcher") + serialize.save_launch_manifest(lmb.finalize()) + with open(manifest_json, "r") as f: + assert isinstance(json.load(f), dict) + + +def test_started_entities_are_serialized(test_dir): + exp_name = "test-exp" + test_dir = Path(test_dir) / exp_name + test_dir.mkdir(parents=True) + exp = Experiment(exp_name, exp_path=str(test_dir), launcher="local") + + rs1 = exp.create_run_settings("echo", ["hello", "world"]) + rs2 = exp.create_run_settings("echo", ["spam", "eggs"]) + + hello_world_model = exp.create_model("echo-hello", run_settings=rs1) + spam_eggs_model = exp.create_model("echo-spam", run_settings=rs2) + hello_ensemble = exp.create_ensemble("echo-ensemble", run_settings=rs1, replicas=3) + + exp.generate(hello_world_model, spam_eggs_model, hello_ensemble) + exp.start(hello_world_model, spam_eggs_model, block=False) + exp.start(hello_ensemble, block=False) + + manifest_json = Path(exp.exp_path) / _REL_MANIFEST_PATH + try: + with open(manifest_json, "r") as f: + manifest = json.load(f) + assert len(manifest["runs"]) == 2 + assert len(manifest["runs"][0]["model"]) == 2 + assert len(manifest["runs"][0]["ensemble"]) == 0 + assert len(manifest["runs"][1]["model"]) == 0 + assert len(manifest["runs"][1]["ensemble"]) == 1 + assert len(manifest["runs"][1]["ensemble"][0]["models"]) == 3 + finally: + exp.stop(hello_world_model, spam_eggs_model, hello_ensemble) + + +def test_serialzed_database_does_not_break_if_using_a_non_standard_install(monkeypatch): + monkeypatch.setattr(utils, "get_db_path", lambda: None) + db = Orchestrator() + dict_ = serialize._dictify_db(db, []) + assert dict_["type"] == "Unknown" + + +def test_dictify_run_settings_warns_when_attepting_to_dictify_mpmd( + monkeypatch, caplog, test_dir +): + # TODO: Eventually this test should be removed and we should be able to + # handle MPMD run settings as part of the output dict + exp_name = "test-exp" + test_dir = Path(test_dir) / exp_name + test_dir.mkdir(parents=True) + exp = Experiment(exp_name, exp_path=str(test_dir), launcher="local") + + rs1 = exp.create_run_settings("echo", ["hello", "world"]) + rs2 = exp.create_run_settings("echo", ["spam", "eggs"]) + + # Make rs "MPMD" + monkeypatch.setattr(rs1, "mpmd", [rs2], raising=False) + # Make work with colored logs + monkeypatch.setattr(serialize, "_LOGGER", logging.getLogger()) + serialize._dictify_run_settings(rs1) + (rec,) = caplog.records + assert rec.levelno == logging.WARNING + assert "MPMD run settings" in rec.msg diff --git a/tests/test_shell_util.py b/tests/test_shell_util.py index 4ba3566d4..7b7ac55b7 100644 --- a/tests/test_shell_util.py +++ b/tests/test_shell_util.py @@ -30,6 +30,9 @@ from smartsim._core.launcher.util.shell import * +# The tests in this file belong to the group_b group +pytestmark = pytest.mark.group_b + def test_execute_cmd(): returncode, out, err = execute_cmd(["hostname"]) diff --git a/tests/test_slurm_get_alloc.py b/tests/test_slurm_get_alloc.py index de488b25f..270bbf014 100644 --- a/tests/test_slurm_get_alloc.py +++ b/tests/test_slurm_get_alloc.py @@ -24,9 +24,13 @@ # 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. +import pytest from smartsim.slurm import _get_alloc_cmd +# The tests in this file belong to the group_b group +pytestmark = pytest.mark.group_b + def test_get_alloc_format(): time = "10:00:00" diff --git a/tests/test_slurm_parser.py b/tests/test_slurm_parser.py index 0674d9fe9..30c6c5b31 100644 --- a/tests/test_slurm_parser.py +++ b/tests/test_slurm_parser.py @@ -24,8 +24,14 @@ # 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. +import pytest + from smartsim._core.launcher.slurm import slurmParser +# The tests in this file belong to the group_b group +pytestmark = pytest.mark.group_b + + # -- Salloc --------------------------------------------------------- diff --git a/tests/test_slurm_settings.py b/tests/test_slurm_settings.py index da18a0133..d6bfd5063 100644 --- a/tests/test_slurm_settings.py +++ b/tests/test_slurm_settings.py @@ -24,14 +24,15 @@ # 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. -import logging -import os - import pytest from smartsim.error import SSUnsupportedError from smartsim.settings import SbatchSettings, SrunSettings +# The tests in this file belong to the group_b group +pytestmark = pytest.mark.group_b + + # ------ Srun ------------------------------------------------ diff --git a/tests/test_slurm_validation.py b/tests/test_slurm_validation.py index 585ae2d16..c3f796ba6 100644 --- a/tests/test_slurm_validation.py +++ b/tests/test_slurm_validation.py @@ -30,6 +30,10 @@ from smartsim.error.errors import LauncherError from smartsim.slurm import _get_system_partition_info, get_default_partition, validate +# The tests in this file belong to the group_b group +pytestmark = pytest.mark.group_b + + # retrieved from pytest fixtures if pytest.test_launcher != "slurm": pytestmark = pytest.mark.skip(reason="Test is only for Slurm WLM systems") diff --git a/tests/test_smartredis.py b/tests/test_smartredis.py index c44969ce1..2f234c217 100644 --- a/tests/test_smartredis.py +++ b/tests/test_smartredis.py @@ -32,6 +32,10 @@ from smartsim.database import Orchestrator from smartsim.entity import Ensemble, Model +# The tests in this file belong to the group_b group +pytestmark = pytest.mark.group_b + + """Test smartredis integration for ensembles. Two copies of the same program will be executed concurrently, and name collisions will be avoided through smartredis prefixing: @@ -55,13 +59,12 @@ ) -def test_exchange(fileutils, wlmutils): +def test_exchange(fileutils, test_dir, wlmutils): """Run two processes, each process puts a tensor on the DB, then accesses the other process's tensor. Finally, the tensor is used to run a model. """ - test_dir = fileutils.make_test_dir() exp = Experiment( "smartredis_ensemble_exchange", exp_path=test_dir, launcher="local" ) @@ -93,24 +96,21 @@ def test_exchange(fileutils, wlmutils): # get and confirm statuses statuses = exp.get_status(ensemble) - if not all([stat == status.STATUS_COMPLETED for stat in statuses]): + try: + assert all([stat == status.STATUS_COMPLETED for stat in statuses]) + finally: + # stop the orchestrator exp.stop(orc) - assert False # client ensemble failed - - # stop the orchestrator - exp.stop(orc) - - print(exp.summary()) -def test_consumer(fileutils, wlmutils): +def test_consumer(fileutils, test_dir, wlmutils): """Run three processes, each one of the first two processes puts a tensor on the DB; the third process accesses the tensors put by the two producers. Finally, the tensor is used to run a model by each producer and the consumer accesses the two results. """ - test_dir = fileutils.make_test_dir() + exp = Experiment( "smartredis_ensemble_consumer", exp_path=test_dir, launcher="local" ) @@ -145,11 +145,8 @@ def test_consumer(fileutils, wlmutils): # get and confirm statuses statuses = exp.get_status(ensemble) - if not all([stat == status.STATUS_COMPLETED for stat in statuses]): + try: + assert all([stat == status.STATUS_COMPLETED for stat in statuses]) + finally: + # stop the orchestrator exp.stop(orc) - assert False # client ensemble failed - - # stop the orchestrator - exp.stop(orc) - - print(exp.summary()) diff --git a/tests/test_step_info.py b/tests/test_step_info.py index 75b93f15e..eee920192 100644 --- a/tests/test_step_info.py +++ b/tests/test_step_info.py @@ -24,10 +24,14 @@ # 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. +import pytest from smartsim import status from smartsim._core.launcher.stepInfo import * +# The tests in this file belong to the group_b group +pytestmark = pytest.mark.group_b + def test_str(): step_info = StepInfo( diff --git a/tests/test_telemetry_monitor.py b/tests/test_telemetry_monitor.py new file mode 100644 index 000000000..3f804b077 --- /dev/null +++ b/tests/test_telemetry_monitor.py @@ -0,0 +1,1114 @@ +# BSD 2-Clause License +# +# Copyright (c) 2021-2023, 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. + + +import logging +import pathlib +import sys +import time +import typing as t +import uuid + +import pytest + +import smartsim._core.config.config as cfg +from conftest import FileUtils, WLMUtils +from smartsim import Experiment +from smartsim._core.control.job import Job, JobEntity +from smartsim._core.control.jobmanager import JobManager +from smartsim._core.entrypoints.telemetrymonitor import ( + ManifestEventHandler, + can_shutdown, + event_loop, + faux_return_code, + get_parser, + get_ts, + hydrate_persistable, + load_manifest, + track_event, +) +from smartsim._core.launcher.launcher import WLMLauncher +from smartsim._core.launcher.slurm.slurmLauncher import SlurmLauncher +from smartsim._core.launcher.step.step import Step, proxyable_launch_cmd +from smartsim._core.launcher.stepInfo import StepInfo +from smartsim._core.utils import serialize +from smartsim.error.errors import UnproxyableStepError +from smartsim.settings.base import RunSettings +from smartsim.status import ( + STATUS_CANCELLED, + STATUS_COMPLETED, + STATUS_FAILED, + STATUS_NEW, + STATUS_PAUSED, + STATUS_RUNNING, +) + +ALL_ARGS = {"-exp_dir", "-frequency"} +PROXY_ENTRY_POINT = "smartsim._core.entrypoints.indirect" +CFG_TM_ENABLED_ATTR = "telemetry_enabled" + + +for_all_wlm_launchers = pytest.mark.parametrize( + "wlm_launcher", + [pytest.param(cls(), id=cls.__name__) for cls in WLMLauncher.__subclasses__()], +) + +requires_wlm = pytest.mark.skipif( + pytest.test_launcher == "local", reason="Test requires WLM" +) + + +logger = logging.getLogger() + +# The tests in this file belong to the slow_tests group +pytestmark = pytest.mark.slow_tests + + +@pytest.fixture(autouse=True) +def turn_on_tm(monkeypatch): + monkeypatch.setattr(cfg.Config, CFG_TM_ENABLED_ATTR, property(lambda self: True)) + yield + + +def snooze_nonblocking(test_dir: str, max_delay: int = 20, post_data_delay: int = 2): + telmon_subdir = pathlib.Path(test_dir) / serialize.TELMON_SUBDIR + # let the non-blocking experiment complete. + for _ in range(max_delay): + time.sleep(1) + if telmon_subdir.exists(): + time.sleep(post_data_delay) + break + + +@pytest.mark.parametrize( + ["cmd", "missing"], + [ + pytest.param("", {"-exp_dir", "-frequency"}, id="no args"), + pytest.param("-exp_dir /foo/bar", {"-frequency"}, id="no freq"), + pytest.param("-frequency 123", {"-exp_dir"}, id="no dir"), + ], +) +def test_parser_reqd_args(capsys, cmd, missing): + """Test that the parser reports any missing required arguments""" + parser = get_parser() + + args = cmd.split() + + captured = capsys.readouterr() # throw away existing output + with pytest.raises(SystemExit) as ex: + ns = parser.parse_args(args) + + captured = capsys.readouterr() + assert "the following arguments are required" in captured.err + err_desc = captured.err.split("the following arguments are required:")[-1] + for arg in missing: + assert arg in err_desc + + expected = ALL_ARGS - missing + for exp in expected: + assert exp not in err_desc + + +def test_parser(): + """Test that the parser succeeds when receiving expected args""" + parser = get_parser() + + test_dir = "/foo/bar" + test_freq = 123 + + cmd = f"-exp_dir {test_dir} -frequency {test_freq}" + args = cmd.split() + + ns = parser.parse_args(args) + + assert ns.exp_dir == test_dir + assert ns.frequency == test_freq + + +def test_ts(): + """Ensure expected output type""" + ts = get_ts() + assert isinstance(ts, int) + + +@pytest.mark.parametrize( + ["etype", "task_id", "step_id", "timestamp", "evt_type"], + [ + pytest.param("ensemble", "", "123", get_ts(), "start", id="start event"), + pytest.param("ensemble", "", "123", get_ts(), "stop", id="stop event"), + ], +) +def test_track_event( + etype: str, + task_id: str, + step_id: str, + timestamp: int, + evt_type: str, + test_dir: str, +): + """Ensure that track event writes a file to the expected location""" + exp_path = pathlib.Path(test_dir) + track_event(timestamp, task_id, step_id, etype, evt_type, exp_path, logger) + + expected_output = exp_path / f"{evt_type}.json" + + assert expected_output.exists() + assert expected_output.is_file() + + +def test_load_manifest(fileutils: FileUtils, test_dir: str): + """Ensure that the runtime manifest loads correctly""" + sample_manifest_path = fileutils.get_test_conf_path("telemetry/telemetry.json") + sample_manifest = pathlib.Path(sample_manifest_path) + assert sample_manifest.exists() + + test_manifest_path = fileutils.make_test_file( + serialize.MANIFEST_FILENAME, + pathlib.Path(test_dir) / serialize.TELMON_SUBDIR, + sample_manifest.read_text(), + ) + test_manifest = pathlib.Path(test_manifest_path) + assert test_manifest.exists() + + manifest = load_manifest(test_manifest_path) + assert manifest.name == "my-exp" + assert str(manifest.path) == "/path/to/my-exp" + assert manifest.launcher == "Slurm" + assert len(manifest.runs) == 6 + + assert len(manifest.runs[0].models) == 1 + assert len(manifest.runs[2].models) == 8 # 8 models in ensemble + assert len(manifest.runs[0].orchestrators) == 0 + assert len(manifest.runs[1].orchestrators) == 3 # 3 shards in db + + +def test_load_manifest_colo_model(fileutils: FileUtils): + """Ensure that the runtime manifest loads correctly when containing a colocated model""" + # NOTE: for regeneration, this manifest can use `test_telemetry_colo` + sample_manifest_path = fileutils.get_test_conf_path("telemetry/colocatedmodel.json") + sample_manifest = pathlib.Path(sample_manifest_path) + assert sample_manifest.exists() + + manifest = load_manifest(sample_manifest_path) + assert manifest.name == "my-exp" + assert str(manifest.path) == "/tmp/my-exp" + assert manifest.launcher == "Slurm" + assert len(manifest.runs) == 1 + + assert len(manifest.runs[0].models) == 1 + + +def test_load_manifest_serial_models(fileutils: FileUtils): + """Ensure that the runtime manifest loads correctly when containing multiple models""" + # NOTE: for regeneration, this manifest can use `test_telemetry_colo` + sample_manifest_path = fileutils.get_test_conf_path("telemetry/serialmodels.json") + sample_manifest = pathlib.Path(sample_manifest_path) + assert sample_manifest.exists() + + manifest = load_manifest(sample_manifest_path) + assert manifest.name == "my-exp" + assert str(manifest.path) == "/tmp/my-exp" + assert manifest.launcher == "Slurm" + assert len(manifest.runs) == 1 + + assert len(manifest.runs[0].models) == 5 + + +def test_load_manifest_db_and_models(fileutils: FileUtils): + """Ensure that the runtime manifest loads correctly when containing models & + orchestrator across 2 separate runs""" + # NOTE: for regeneration, this manifest can use `test_telemetry_colo` + sample_manifest_path = fileutils.get_test_conf_path("telemetry/db_and_model.json") + sample_manifest = pathlib.Path(sample_manifest_path) + assert sample_manifest.exists() + + manifest = load_manifest(sample_manifest_path) + assert manifest.name == "my-exp" + assert str(manifest.path) == "/tmp/my-exp" + assert manifest.launcher == "Slurm" + assert len(manifest.runs) == 2 + + assert len(manifest.runs[0].orchestrators) == 1 + assert len(manifest.runs[1].models) == 1 + + +def test_load_manifest_db_and_models_1run(fileutils: FileUtils): + """Ensure that the runtime manifest loads correctly when containing models & + orchestrator in a single run""" + # NOTE: for regeneration, this manifest can use `test_telemetry_colo` + sample_manifest_path = fileutils.get_test_conf_path( + "telemetry/db_and_model_1run.json" + ) + sample_manifest = pathlib.Path(sample_manifest_path) + assert sample_manifest.exists() + + manifest = load_manifest(sample_manifest_path) + assert manifest.name == "my-exp" + assert str(manifest.path) == "/tmp/my-exp" + assert manifest.launcher == "Slurm" + assert len(manifest.runs) == 1 + + assert len(manifest.runs[0].orchestrators) == 1 + assert len(manifest.runs[0].models) == 1 + + +@pytest.mark.parametrize( + ["task_id", "step_id", "etype", "exp_isorch", "exp_ismanaged"], + [ + pytest.param("123", "", "model", False, False, id="unmanaged, non-orch"), + pytest.param("456", "123", "ensemble", False, True, id="managed, non-orch"), + pytest.param("789", "987", "orchestrator", True, True, id="managed, orch"), + pytest.param("987", "", "orchestrator", True, False, id="unmanaged, orch"), + ], +) +def test_persistable_computed_properties( + task_id: str, step_id: str, etype: str, exp_isorch: bool, exp_ismanaged: bool +): + name = f"test-{etype}-{uuid.uuid4()}" + timestamp = get_ts() + exp_dir = pathlib.Path("/foo/bar") + stored = { + "name": name, + "run_id": timestamp, + "telemetry_metadata": { + "status_dir": str(exp_dir), + "task_id": task_id, + "step_id": step_id, + }, + } + persistables = hydrate_persistable(etype, stored, exp_dir) + persistable = persistables[0] if persistables else None + + assert persistable.is_managed == exp_ismanaged + assert persistable.is_db == exp_isorch + + +def test_deserialize_ensemble(fileutils: FileUtils): + """Ensure that the children of ensembles (models) are correctly + placed in the models collection""" + sample_manifest_path = fileutils.get_test_conf_path("telemetry/ensembles.json") + sample_manifest = pathlib.Path(sample_manifest_path) + assert sample_manifest.exists() + + manifest = load_manifest(sample_manifest_path) + assert manifest + + assert len(manifest.runs) == 1 + + # NOTE: no longer returning ensembles, only children... + # assert len(manifest.runs[0].ensembles) == 1 + assert len(manifest.runs[0].models) == 8 + + +def test_shutdown_conditions(): + """Ensure conditions to shutdown telemetry monitor are correctly evaluated""" + job_entity1 = JobEntity() + job_entity1.name = "xyz" + job_entity1.step_id = "123" + job_entity1.task_id = "" + + logger = logging.getLogger() + + # show that an event handler w/no monitored jobs can shutdown + mani_handler = ManifestEventHandler("xyz", logger) + assert can_shutdown(mani_handler, logger) + + # show that an event handler w/a monitored job cannot shutdown + mani_handler = ManifestEventHandler("xyz", logger) + mani_handler.job_manager.add_job( + job_entity1.name, job_entity1.step_id, job_entity1, False + ) + assert not can_shutdown(mani_handler, logger) + assert not bool(mani_handler.job_manager.db_jobs) + assert bool(mani_handler.job_manager.jobs) + + # show that an event handler w/a monitored db cannot shutdown + mani_handler = ManifestEventHandler("xyz", logger) + job_entity1.type = "orchestrator" + mani_handler.job_manager.add_job( + job_entity1.name, job_entity1.step_id, job_entity1, False + ) + assert not can_shutdown(mani_handler, logger) + assert bool(mani_handler.job_manager.db_jobs) + assert not bool(mani_handler.job_manager.jobs) + + # show that an event handler w/a dbs & tasks cannot shutdown + job_entity2 = JobEntity() + job_entity2.name = "xyz" + job_entity2.step_id = "123" + job_entity2.task_id = "" + + mani_handler = ManifestEventHandler("xyz", logger) + job_entity1.type = "orchestrator" + mani_handler.job_manager.add_job( + job_entity1.name, job_entity1.step_id, job_entity1, False + ) + + mani_handler.job_manager.add_job( + job_entity2.name, job_entity2.step_id, job_entity2, False + ) + assert not can_shutdown(mani_handler, logger) + assert bool(mani_handler.job_manager.db_jobs) + assert bool(mani_handler.job_manager.jobs) + + # ... now, show that removing 1 of 2 jobs still doesn't shutdown + mani_handler.job_manager.db_jobs.popitem() + assert not can_shutdown(mani_handler, logger) + + # ... now, show that removing final job will allow shutdown + mani_handler.job_manager.jobs.popitem() + assert can_shutdown(mani_handler, logger) + + +def test_auto_shutdown(): + """Ensure that the cooldown timer is respected""" + + class FauxObserver: + def __init__(self): + self.stop_count = 0 + + def stop(self): + self.stop_count += 1 + + def is_alive(self) -> bool: + if self.stop_count > 0: + return False + + return True + + job_entity1 = JobEntity() + job_entity1.name = "xyz" + job_entity1.step_id = "123" + job_entity1.task_id = "" + + frequency = 1 + + # show that an event handler w/out a monitored task will automatically stop + mani_handler = ManifestEventHandler("xyz", logger) + observer = FauxObserver() + duration = 2 + + ts0 = get_ts() + event_loop(observer, mani_handler, frequency, logger, cooldown_duration=duration) + ts1 = get_ts() + + assert ts1 - ts0 >= duration + assert observer.stop_count == 1 + + # show that the new cooldown duration is respected + mani_handler = ManifestEventHandler("xyz", logger) + observer = FauxObserver() + duration = 5 + + ts0 = get_ts() + event_loop(observer, mani_handler, frequency, logger, cooldown_duration=duration) + ts1 = get_ts() + + assert ts1 - ts0 >= duration + assert observer.stop_count == 1 + + +def test_telemetry_single_model(fileutils, test_dir, wlmutils): + """Test that it is possible to create_database then colocate_db_uds/colocate_db_tcp + with unique db_identifiers""" + + # Set experiment name + exp_name = "telemetry_single_model" + + # Retrieve parameters from testing environment + test_launcher = wlmutils.get_test_launcher() + test_script = fileutils.get_test_conf_path("echo.py") + + # Create SmartSim Experiment + exp = Experiment(exp_name, launcher=test_launcher, exp_path=test_dir) + + # create run settings + app_settings = exp.create_run_settings("python", test_script) + app_settings.set_nodes(1) + app_settings.set_tasks_per_node(1) + + # Create the SmartSim Model + smartsim_model = exp.create_model("perroquet", app_settings) + exp.generate(smartsim_model) + exp.start(smartsim_model, block=True) + assert exp.get_status(smartsim_model)[0] == STATUS_COMPLETED + + telemetry_output_path = pathlib.Path(test_dir) / serialize.TELMON_SUBDIR + start_events = list(telemetry_output_path.rglob("start.json")) + stop_events = list(telemetry_output_path.rglob("stop.json")) + + assert len(start_events) == 1 + assert len(stop_events) == 1 + + +def test_telemetry_single_model_nonblocking(fileutils, test_dir, wlmutils, monkeypatch): + """Ensure that the telemetry monitor logs exist when the experiment + is non-blocking""" + with monkeypatch.context() as ctx: + ctx.setattr(cfg.Config, "telemetry_frequency", 1) + + # Set experiment name + exp_name = "test_telemetry_single_model_nonblocking" + + # Retrieve parameters from testing environment + test_launcher = wlmutils.get_test_launcher() + test_script = fileutils.get_test_conf_path("echo.py") + + # Create SmartSim Experiment + exp = Experiment(exp_name, launcher=test_launcher, exp_path=test_dir) + + # create run settings + app_settings = exp.create_run_settings("python", test_script) + app_settings.set_nodes(1) + app_settings.set_tasks_per_node(1) + + # Create the SmartSim Model + smartsim_model = exp.create_model("perroquet", app_settings) + exp.generate(smartsim_model) + exp.start(smartsim_model) + + snooze_nonblocking(test_dir, max_delay=60, post_data_delay=30) + + assert exp.get_status(smartsim_model)[0] == STATUS_COMPLETED + + telemetry_output_path = pathlib.Path(test_dir) / serialize.TELMON_SUBDIR + start_events = list(telemetry_output_path.rglob("start.json")) + stop_events = list(telemetry_output_path.rglob("stop.json")) + + assert len(start_events) == 1 + assert len(stop_events) == 1 + + +def test_telemetry_serial_models(fileutils, test_dir, wlmutils, monkeypatch): + """ + Test telemetry with models being run in serial (one after each other) + """ + with monkeypatch.context() as ctx: + ctx.setattr(cfg.Config, "telemetry_frequency", 1) + + # Set experiment name + exp_name = "telemetry_serial_models" + + # Retrieve parameters from testing environment + test_launcher = wlmutils.get_test_launcher() + test_script = fileutils.get_test_conf_path("echo.py") + + # Create SmartSim Experiment + exp = Experiment(exp_name, launcher=test_launcher, exp_path=test_dir) + + # create run settings + app_settings = exp.create_run_settings("python", test_script) + app_settings.set_nodes(1) + app_settings.set_tasks_per_node(1) + + # Create the SmartSim Model + smartsim_models = [ + exp.create_model(f"perroquet_{i}", app_settings) for i in range(5) + ] + exp.generate(*smartsim_models) + exp.start(*smartsim_models, block=True) + assert all( + [status == STATUS_COMPLETED for status in exp.get_status(*smartsim_models)] + ) + + telemetry_output_path = pathlib.Path(test_dir) / serialize.TELMON_SUBDIR + start_events = list(telemetry_output_path.rglob("start.json")) + stop_events = list(telemetry_output_path.rglob("stop.json")) + + assert len(start_events) == 5 + assert len(stop_events) == 5 + + +def test_telemetry_serial_models_nonblocking( + fileutils, test_dir, wlmutils, monkeypatch +): + """ + Test telemetry with models being run in serial (one after each other) + in a non-blocking experiment + """ + with monkeypatch.context() as ctx: + ctx.setattr(cfg.Config, "telemetry_frequency", 1) + + # Set experiment name + exp_name = "telemetry_serial_models" + + # Retrieve parameters from testing environment + test_launcher = wlmutils.get_test_launcher() + test_script = fileutils.get_test_conf_path("echo.py") + + # Create SmartSim Experiment + exp = Experiment(exp_name, launcher=test_launcher, exp_path=test_dir) + + # create run settings + app_settings = exp.create_run_settings("python", test_script) + app_settings.set_nodes(1) + app_settings.set_tasks_per_node(1) + + # Create the SmartSim Model + smartsim_models = [ + exp.create_model(f"perroquet_{i}", app_settings) for i in range(5) + ] + exp.generate(*smartsim_models) + exp.start(*smartsim_models) + + snooze_nonblocking(test_dir, max_delay=60, post_data_delay=10) + + assert all( + [status == STATUS_COMPLETED for status in exp.get_status(*smartsim_models)] + ) + + telemetry_output_path = pathlib.Path(test_dir) / serialize.TELMON_SUBDIR + start_events = list(telemetry_output_path.rglob("start.json")) + stop_events = list(telemetry_output_path.rglob("stop.json")) + + assert len(start_events) == 5 + assert len(stop_events) == 5 + + +def test_telemetry_db_only_with_generate(test_dir, wlmutils, monkeypatch): + """ + Test telemetry with only a database running + """ + with monkeypatch.context() as ctx: + ctx.setattr(cfg.Config, "telemetry_frequency", 1) + + # Set experiment name + exp_name = "telemetry_db_with_generate" + + # Retrieve parameters from testing environment + test_launcher = wlmutils.get_test_launcher() + test_interface = wlmutils.get_test_interface() + test_port = wlmutils.get_test_port() + + # Create SmartSim Experiment + exp = Experiment(exp_name, launcher=test_launcher, exp_path=test_dir) + + # create regular database + orc = exp.create_database(port=test_port, interface=test_interface) + exp.generate(orc) + try: + exp.start(orc, block=True) + + snooze_nonblocking(test_dir, max_delay=60, post_data_delay=10) + + telemetry_output_path = pathlib.Path(test_dir) / serialize.TELMON_SUBDIR + start_events = list(telemetry_output_path.rglob("start.json")) + stop_events = list(telemetry_output_path.rglob("stop.json")) + + assert len(start_events) == 1 + assert len(stop_events) <= 1 + finally: + exp.stop(orc) + snooze_nonblocking(test_dir, max_delay=60, post_data_delay=10) + + assert exp.get_status(orc)[0] == STATUS_CANCELLED + + stop_events = list(telemetry_output_path.rglob("stop.json")) + assert len(stop_events) == 1 + + +def test_telemetry_db_only_without_generate(test_dir, wlmutils, monkeypatch): + """ + Test telemetry with only a non-generated database running + """ + with monkeypatch.context() as ctx: + ctx.setattr(cfg.Config, "telemetry_frequency", 1) + + # Set experiment name + exp_name = "telemetry_db_only_without_generate" + + # Retrieve parameters from testing environment + test_launcher = wlmutils.get_test_launcher() + test_interface = wlmutils.get_test_interface() + test_port = wlmutils.get_test_port() + + # Create SmartSim Experiment + exp = Experiment(exp_name, launcher=test_launcher, exp_path=test_dir) + + # create regular database + orc = exp.create_database(port=test_port, interface=test_interface) + orc.set_path(test_dir) + + try: + exp.start(orc) + + snooze_nonblocking(test_dir, max_delay=60, post_data_delay=30) + + telemetry_output_path = pathlib.Path(test_dir) / serialize.TELMON_SUBDIR + start_events = list(telemetry_output_path.rglob("start.json")) + stop_events = list(telemetry_output_path.rglob("stop.json")) + + assert len(start_events) == 1 + assert len(stop_events) == 0 + finally: + exp.stop(orc) + + snooze_nonblocking(test_dir, max_delay=60, post_data_delay=10) + assert exp.get_status(orc)[0] == STATUS_CANCELLED + + stop_events = list(telemetry_output_path.rglob("stop.json")) + assert len(stop_events) == 1 + + +def test_telemetry_db_and_model(fileutils, test_dir, wlmutils, monkeypatch): + """ + Test telemetry with only a database and a model running + """ + + with monkeypatch.context() as ctx: + ctx.setattr(cfg.Config, "telemetry_frequency", 1) + + # Set experiment name + exp_name = "telemetry_db_and_model" + + # Retrieve parameters from testing environment + test_launcher = wlmutils.get_test_launcher() + test_interface = wlmutils.get_test_interface() + test_port = wlmutils.get_test_port() + test_script = fileutils.get_test_conf_path("echo.py") + + # Create SmartSim Experiment + exp = Experiment(exp_name, launcher=test_launcher, exp_path=test_dir) + + # create regular database + orc = exp.create_database(port=test_port, interface=test_interface) + exp.generate(orc) + try: + exp.start(orc) + + # create run settings + app_settings = exp.create_run_settings("python", test_script) + app_settings.set_nodes(1) + app_settings.set_tasks_per_node(1) + + # Create the SmartSim Model + smartsim_model = exp.create_model("perroquet", app_settings) + exp.generate(smartsim_model) + exp.start(smartsim_model, block=True) + finally: + exp.stop(orc) + + snooze_nonblocking(test_dir, max_delay=60, post_data_delay=30) + + assert exp.get_status(orc)[0] == STATUS_CANCELLED + assert exp.get_status(smartsim_model)[0] == STATUS_COMPLETED + + telemetry_output_path = pathlib.Path(test_dir) / serialize.TELMON_SUBDIR + + start_events = list(telemetry_output_path.rglob("database/**/start.json")) + stop_events = list(telemetry_output_path.rglob("database/**/stop.json")) + + assert len(start_events) == 1 + assert len(stop_events) == 1 + + start_events = list(telemetry_output_path.rglob("model/**/start.json")) + stop_events = list(telemetry_output_path.rglob("model/**/stop.json")) + assert len(start_events) == 1 + assert len(stop_events) == 1 + + +def test_telemetry_ensemble(fileutils, test_dir, wlmutils, monkeypatch): + """ + Test telemetry with only an ensemble + """ + + with monkeypatch.context() as ctx: + ctx.setattr(cfg.Config, "telemetry_frequency", 1) + + # Set experiment name + exp_name = "telemetry_ensemble" + + # Retrieve parameters from testing environment + test_launcher = wlmutils.get_test_launcher() + test_script = fileutils.get_test_conf_path("echo.py") + + # Create SmartSim Experiment + exp = Experiment(exp_name, launcher=test_launcher, exp_path=test_dir) + + app_settings = exp.create_run_settings("python", test_script) + app_settings.set_nodes(1) + app_settings.set_tasks_per_node(1) + + ens = exp.create_ensemble("troupeau", run_settings=app_settings, replicas=5) + exp.generate(ens) + exp.start(ens, block=True) + assert all([status == STATUS_COMPLETED for status in exp.get_status(ens)]) + + snooze_nonblocking(test_dir, max_delay=60, post_data_delay=30) + telemetry_output_path = pathlib.Path(test_dir) / serialize.TELMON_SUBDIR + start_events = list(telemetry_output_path.rglob("start.json")) + stop_events = list(telemetry_output_path.rglob("stop.json")) + + assert len(start_events) == 5 + assert len(stop_events) == 5 + + +def test_telemetry_colo(fileutils, test_dir, wlmutils, coloutils, monkeypatch): + """ + Test telemetry with only a colocated model running + """ + + with monkeypatch.context() as ctx: + ctx.setattr(cfg.Config, "telemetry_frequency", 1) + + # Set experiment name + exp_name = "telemetry_colo" + + # Retrieve parameters from testing environment + test_launcher = wlmutils.get_test_launcher() + + # Create SmartSim Experiment + exp = Experiment(exp_name, launcher=test_launcher, exp_path=test_dir) + + smartsim_model = coloutils.setup_test_colo( + fileutils, + "uds", + exp, + "echo.py", + {}, + ) + + exp.generate(smartsim_model) + exp.start(smartsim_model, block=True) + assert all( + [status == STATUS_COMPLETED for status in exp.get_status(smartsim_model)] + ) + + telemetry_output_path = pathlib.Path(test_dir) / serialize.TELMON_SUBDIR + start_events = list(telemetry_output_path.rglob("start.json")) + stop_events = list(telemetry_output_path.rglob("stop.json")) + + # the colodb does NOT show up as a unique entity in the telemetry + assert len(start_events) == 1 + assert len(stop_events) == 1 + + +@pytest.mark.parametrize( + "frequency, cooldown", + [ + pytest.param(1, 1, id="1s shutdown"), + pytest.param(1, 5, id="5s shutdown"), + pytest.param(1, 15, id="15s shutdown"), + ], +) +def test_telemetry_autoshutdown(test_dir, wlmutils, monkeypatch, frequency, cooldown): + """ + Ensure that the telemetry monitor process shuts down after the desired + cooldown period + """ + + with monkeypatch.context() as ctx: + ctx.setattr(cfg.Config, "telemetry_frequency", frequency) + ctx.setattr(cfg.Config, "telemetry_cooldown", cooldown) + + # Set experiment name + exp_name = "telemetry_ensemble" + + # Retrieve parameters from testing environment + test_launcher = wlmutils.get_test_launcher() + + # Create SmartSim Experiment + exp = Experiment(exp_name, launcher=test_launcher, exp_path=test_dir) + + start_time = get_ts() + stop_time = start_time + exp.start(block=False) + + telemetry_output_path = pathlib.Path(test_dir) / serialize.TELMON_SUBDIR + empty_mani = list(telemetry_output_path.rglob("manifest.json")) + assert len(empty_mani) == 1, "an manifest.json should be created" + + popen = exp._control._telemetry_monitor + assert popen.pid > 0 + assert popen.returncode is None + + # give some leeway during testing for the cooldown to get hit + for i in range(10): + if popen.poll() is not None: + stop_time = get_ts() + print(f"Completed polling for telemetry shutdown after {i} attempts") + break + time.sleep(3) + + assert popen.returncode is not None + assert stop_time >= (start_time + cooldown) + + +class MockStep(Step): + """Mock step to implement any abstract methods so that it can be + instanced for test purposes + """ + + def get_launch_cmd(self): + return ["spam", "eggs"] + + +@pytest.fixture +def mock_step_meta_dict(test_dir): + telemetry_output_path = pathlib.Path(test_dir) / serialize.TELMON_SUBDIR + yield { + "entity_type": "mock", + "status_dir": telemetry_output_path, + } + + +@pytest.fixture +def mock_step(test_dir, mock_step_meta_dict): + rs = RunSettings("echo") + step = MockStep("mock-step", test_dir, rs) + step.meta = mock_step_meta_dict + yield step + + +def test_proxy_launch_cmd_decorator_reformats_cmds(mock_step, monkeypatch): + monkeypatch.setattr(cfg.Config, CFG_TM_ENABLED_ATTR, True) + get_launch_cmd = proxyable_launch_cmd(lambda step: ["some", "cmd", "list"]) + cmd = get_launch_cmd(mock_step) + assert cmd != ["some", "cmd", "list"] + assert sys.executable in cmd + assert PROXY_ENTRY_POINT in cmd + + +def test_proxy_launch_cmd_decorator_does_not_reformat_cmds_if_the_tm_is_off( + mock_step, monkeypatch +): + monkeypatch.setattr(cfg.Config, CFG_TM_ENABLED_ATTR, False) + get_launch_cmd = proxyable_launch_cmd(lambda step: ["some", "cmd", "list"]) + cmd = get_launch_cmd(mock_step) + assert cmd == ["some", "cmd", "list"] + + +def test_proxy_launch_cmd_decorator_errors_if_attempt_to_proxy_a_managed_step( + mock_step, monkeypatch +): + monkeypatch.setattr(cfg.Config, CFG_TM_ENABLED_ATTR, True) + mock_step.managed = True + get_launch_cmd = proxyable_launch_cmd(lambda step: ["some", "cmd", "list"]) + with pytest.raises(UnproxyableStepError): + get_launch_cmd(mock_step) + + +@for_all_wlm_launchers +def test_unmanaged_steps_are_proxyed_through_indirect( + wlm_launcher, mock_step_meta_dict, test_dir, monkeypatch +): + monkeypatch.setattr(cfg.Config, CFG_TM_ENABLED_ATTR, True) + rs = RunSettings("echo", ["hello", "world"]) + step = wlm_launcher.create_step("test-step", test_dir, rs) + step.meta = mock_step_meta_dict + assert isinstance(step, Step) + assert not step.managed + cmd = step.get_launch_cmd() + assert sys.executable in cmd + assert PROXY_ENTRY_POINT in cmd + assert "hello" not in cmd + assert "world" not in cmd + + +@for_all_wlm_launchers +def test_unmanaged_steps_are_not_proxied_if_the_telemetry_monitor_is_disabled( + wlm_launcher, mock_step_meta_dict, test_dir, monkeypatch +): + monkeypatch.setattr(cfg.Config, CFG_TM_ENABLED_ATTR, False) + rs = RunSettings("echo", ["hello", "world"]) + step = wlm_launcher.create_step("test-step", test_dir, rs) + step.meta = mock_step_meta_dict + assert isinstance(step, Step) + assert not step.managed + cmd = step.get_launch_cmd() + assert PROXY_ENTRY_POINT not in cmd + assert "hello" in cmd + assert "world" in cmd + + +@requires_wlm +@pytest.mark.parametrize( + "run_command", + [ + pytest.param("", id="Unmanaged"), + pytest.param("auto", id="Managed"), + ], +) +def test_multistart_experiment( + wlmutils: WLMUtils, + fileutils: FileUtils, + test_dir: str, + monkeypatch: pytest.MonkeyPatch, + run_command: str, +): + """Run an experiment with multiple start calls to ensure that telemetry is + saved correctly for each run + """ + + exp_name = "my-exp" + exp = Experiment(exp_name, launcher=wlmutils.get_test_launcher(), exp_path=test_dir) + rs_e = exp.create_run_settings( + sys.executable, ["printing_model.py"], run_command=run_command + ) + rs_e.set_nodes(1) + rs_e.set_tasks(1) + ens = exp.create_ensemble( + "my-ens", + run_settings=rs_e, + perm_strategy="all_perm", + params={ + "START": ["spam"], + "MID": ["eggs"], + "END": ["sausage", "and spam"], + }, + ) + + test_script_path = fileutils.get_test_conf_path("printing_model.py") + ens.attach_generator_files(to_configure=[test_script_path]) + + rs_m = exp.create_run_settings("echo", ["hello", "world"], run_command=run_command) + rs_m.set_nodes(1) + rs_m.set_tasks(1) + model = exp.create_model("my-model", run_settings=rs_m) + + db = exp.create_database( + db_nodes=1, + port=wlmutils.get_test_port(), + interface=wlmutils.get_test_interface(), + ) + + exp.generate(db, ens, model, overwrite=True) + + with monkeypatch.context() as ctx: + ctx.setattr(cfg.Config, "telemetry_frequency", 1) + ctx.setattr(cfg.Config, "telemetry_cooldown", 45) + + exp.start(model, block=False) + + # track PID to see that telmon cooldown avoids restarting process + tm_pid = exp._control._telemetry_monitor.pid + + exp.start(db, block=False) + # check that same TM proc is active + assert tm_pid == exp._control._telemetry_monitor.pid + try: + exp.start(ens, block=True, summary=True) + finally: + exp.stop(db) + assert tm_pid == exp._control._telemetry_monitor.pid + time.sleep(3) # time for telmon to write db stop event + + telemetry_output_path = pathlib.Path(test_dir) / serialize.TELMON_SUBDIR + + db_start_events = list(telemetry_output_path.rglob("database/**/start.json")) + db_stop_events = list(telemetry_output_path.rglob("database/**/stop.json")) + assert len(db_start_events) == 1 + assert len(db_stop_events) == 1 + + m_start_events = list(telemetry_output_path.rglob("model/**/start.json")) + m_stop_events = list(telemetry_output_path.rglob("model/**/stop.json")) + assert len(m_start_events) == 1 + assert len(m_stop_events) == 1 + + e_start_events = list(telemetry_output_path.rglob("ensemble/**/start.json")) + e_stop_events = list(telemetry_output_path.rglob("ensemble/**/stop.json")) + assert len(e_start_events) == 2 + assert len(e_stop_events) == 2 + + +@pytest.mark.parametrize( + "status_in, expected_out", + [ + pytest.param(STATUS_CANCELLED, 1, id="failure on cancellation"), + pytest.param(STATUS_COMPLETED, 0, id="success on completion"), + pytest.param(STATUS_FAILED, 1, id="failure on failed"), + pytest.param(STATUS_NEW, None, id="failure on new"), + pytest.param(STATUS_PAUSED, None, id="failure on paused"), + pytest.param(STATUS_RUNNING, None, id="failure on running"), + ], +) +def test_faux_rc(status_in: str, expected_out: t.Optional[int]): + """Ensure faux response codes match expectations.""" + step_info = StepInfo(status=status_in) + + rc = faux_return_code(step_info) + assert rc == expected_out + + +@pytest.mark.parametrize( + "status_in, expected_out, expected_has_jobs", + [ + pytest.param(STATUS_CANCELLED, 1, False, id="failure on cancellation"), + pytest.param(STATUS_COMPLETED, 0, False, id="success on completion"), + pytest.param(STATUS_FAILED, 1, False, id="failure on failed"), + pytest.param(STATUS_NEW, None, True, id="failure on new"), + pytest.param(STATUS_PAUSED, None, True, id="failure on paused"), + pytest.param(STATUS_RUNNING, None, True, id="failure on running"), + ], +) +def test_wlm_completion_handling( + test_dir: str, + monkeypatch: pytest.MonkeyPatch, + status_in: str, + expected_out: t.Optional[int], + expected_has_jobs: bool, +): + def get_faux_update(status: str) -> t.Callable: + def _faux_updates(_self: WLMLauncher, _names: t.List[str]) -> t.List[StepInfo]: + return [("faux-name", StepInfo(status=status))] + + return _faux_updates + + ts = get_ts() + with monkeypatch.context() as ctx: + # don't actually start a job manager + ctx.setattr(JobManager, "start", lambda x: ...) + ctx.setattr(SlurmLauncher, "get_step_update", get_faux_update(status_in)) + + mani_handler = ManifestEventHandler("xyz", logger) + mani_handler.set_launcher("slurm") + + # prep a fake job to request updates for + job_entity = JobEntity() + job_entity.name = "faux-name" + job_entity.step_id = "faux-step-id" + job_entity.task_id = 1234 + job_entity.status_dir = test_dir + job_entity.type = "orchestrator" + + job = Job(job_entity.name, job_entity.step_id, job_entity, "slurm", True) + + # populate our tracking collections + mani_handler._tracked_jobs = {job_entity.key: job_entity} + mani_handler.job_manager.jobs[job.name] = job + + mani_handler.on_timestep(ts) + + # see that the job queue was properly manipulated + has_jobs = bool(mani_handler._tracked_jobs) + assert expected_has_jobs == has_jobs + + # see that the event was properly written + stop_event_path = pathlib.Path(test_dir) / "stop.json" + + # if a status wasn't terminal, no stop event should have been written + should_have_stop_event = False if expected_out is None else True + assert should_have_stop_event == stop_event_path.exists() diff --git a/tutorials/getting_started/consumer.py b/tutorials/getting_started/consumer.py index 0a16c8a26..aef71f220 100644 --- a/tutorials/getting_started/consumer.py +++ b/tutorials/getting_started/consumer.py @@ -1,13 +1,16 @@ import argparse import os -from smartredis import Client +from smartredis import Client, ConfigOptions parser = argparse.ArgumentParser(description="SmartRedis ensemble consumer process.") parser.add_argument("--redis-port") args = parser.parse_args() # get model and set into database -c = Client(address="127.0.0.1:"+str(args.redis_port), cluster=False) +address = "127.0.0.1:" + str(args.redis_port) +os.environ["SSDB"] = address +c = Client(None, logger_name="SmartSim") + # Incoming entity prefixes are stored as a comma-separated list # in the env variable SSKEYIN diff --git a/tutorials/getting_started/multi_db_example/application_script.py b/tutorials/getting_started/multi_db_example/application_script.py new file mode 100644 index 000000000..239c16684 --- /dev/null +++ b/tutorials/getting_started/multi_db_example/application_script.py @@ -0,0 +1,37 @@ +from smartredis import ConfigOptions, Client +from smartredis import * +from smartredis.error import * + +# Initialize a ConfigOptions object +single_shard_config = ConfigOptions.create_from_environment("single_shard_db_identifier") +# Initialize a SmartRedis client for the single sharded database +app_single_shard_client = Client(single_shard_config, logger_name="Model: single shard logger") + +# Initialize a ConfigOptions object +multi_shard_config = ConfigOptions.create_from_environment("multi_shard_db_identifier") +# Initialize a SmartRedis client for the multi sharded database +app_multi_shard_client = Client(multi_shard_config, logger_name="Model: multi shard logger") + +# Initialize a ConfigOptions object +colo_config = ConfigOptions.create_from_environment("colo_db_identifier") +# Initialize a SmartRedis client for the colocated database +colo_client = Client(colo_config, logger_name="Model: colo logger") + +# Retrieve the tensor placed in driver script using the associated client +val1 = app_single_shard_client.get_tensor("tensor_1") +val2 = app_multi_shard_client.get_tensor("tensor_2") + +# Print message to stdout using SmartRedis Client logger +app_single_shard_client.log_data(LLInfo, f"The single sharded db tensor is: {val1}") +app_multi_shard_client.log_data(LLInfo, f"The multi sharded db tensor is: {val2}") + +# Place retrieved tensors in colocated database +colo_client.put_tensor("tensor_1", val1) +colo_client.put_tensor("tensor_2", val2) + +# Check that tensors are in colocated database +colo_val1 = colo_client.poll_tensor("tensor_1", 10, 10) +colo_val2 = colo_client.poll_tensor("tensor_2", 10, 10) +# Print message to stdout using SmartRedis Client logger +colo_client.log_data(LLInfo, f"The colocated db has tensor_1: {colo_val1}") +colo_client.log_data(LLInfo, f"The colocated db has tensor_2: {colo_val2}") \ No newline at end of file diff --git a/tutorials/getting_started/multi_db_example/multidb_driver.py b/tutorials/getting_started/multi_db_example/multidb_driver.py new file mode 100644 index 000000000..fae6a9b15 --- /dev/null +++ b/tutorials/getting_started/multi_db_example/multidb_driver.py @@ -0,0 +1,59 @@ +import numpy as np +from smartredis import Client +from smartsim import Experiment +from smartsim.log import get_logger +import sys + +exe_ex = sys.executable +logger = get_logger("Multidb Experiment Log") +# Initialize the Experiment +exp = Experiment("getting-started-multidb", launcher="auto") + +# Initialize a single sharded database +single_shard_db = exp.create_database(port=6379, db_nodes=1, interface="ib0", db_identifier="single_shard_db_identifier") +exp.generate(single_shard_db, overwrite=True) + +# Initialize a multi sharded database +multi_shard_db = exp.create_database(port=6380, db_nodes=3, interface="ib0", db_identifier="multi_shard_db_identifier") +exp.generate(multi_shard_db, overwrite=True) + +# Launch the single and multi sharded database +exp.start(single_shard_db, multi_shard_db, summary=True) + +# Initialize SmartRedis client for single sharded database +driver_client_single_shard = Client(cluster=False, address=single_shard_db.get_address()[0], logger_name="Single shard db logger") +# Initialize SmartRedis client for multi sharded database +driver_client_multi_shard = Client(cluster=True, address=multi_shard_db.get_address()[0], logger_name="Multi shard db logger") + +# Create NumPy array +array_1 = np.array([1, 2, 3, 4]) +# Use single shard db SmartRedis client to place tensor in single sharded db +driver_client_single_shard.put_tensor("tensor_1", array_1) + +# Create NumPy array +array_2 = np.array([5, 6, 7, 8]) +# Use single shard db SmartRedis client to place tensor in multi sharded db +driver_client_multi_shard.put_tensor("tensor_2", array_2) + +# Check that tensors are in correct databases +check_single_shard_db_tensor_incorrect = driver_client_single_shard.key_exists("tensor_2") +check_multi_shard_db_tensor_incorrect = driver_client_multi_shard.key_exists("tensor_1") +logger.info(f"The multi shard array key exists in the incorrect database: {check_single_shard_db_tensor_incorrect}") +logger.info(f"The single shard array key exists in the incorrect database: {check_multi_shard_db_tensor_incorrect}") + +# Initialize a RunSettings object +model_settings = exp.create_run_settings(exe=exe_ex, exe_args="./path/to/application_script.py") +# Configure RunSettings object +model_settings.set_nodes(1) +model_settings.set_tasks_per_node(1) +# Initialize a SmartSim Model +model = exp.create_model("colo_model", model_settings) +# Colocate the Model +model.colocate_db_tcp(db_identifier="colo_db_identifier") +# Launch the colocated Model +exp.start(model, block=True, summary=True) + +# Tear down the single and multi sharded databases +exp.stop(single_shard_db, multi_shard_db) +# Print the Experiment summary +logger.info(exp.summary()) \ No newline at end of file diff --git a/tutorials/getting_started/producer.py b/tutorials/getting_started/producer.py index 731bfe402..9622ea7b4 100644 --- a/tutorials/getting_started/producer.py +++ b/tutorials/getting_started/producer.py @@ -1,14 +1,18 @@ import numpy as np import argparse import time +import os -from smartredis import Client +from smartredis import Client, ConfigOptions parser = argparse.ArgumentParser(description="SmartRedis ensemble producer process.") parser.add_argument("--redis-port") args = parser.parse_args() time.sleep(2) -c = Client(address="127.0.0.1:"+str(args.redis_port), cluster=False) +address = "127.0.0.1:" + str(args.redis_port) +os.environ["SSDB"] = address +c = Client(None, logger_name="SmartSim") + data = np.random.rand(1, 1, 3, 3) c.put_tensor("product", data) \ No newline at end of file diff --git a/tutorials/online_analysis/lattice/fv_sim.py b/tutorials/online_analysis/lattice/fv_sim.py index 7f5fcf06d..c9c75b88d 100644 --- a/tutorials/online_analysis/lattice/fv_sim.py +++ b/tutorials/online_analysis/lattice/fv_sim.py @@ -49,11 +49,15 @@ def finite_volume_simulation(steps=4000, x_res=400, y_res=100, # send every 5 time_step to reduce memory consumption if time_step % 5 == 0: - dataset = create_dataset(time_step, ux, uy, Feq) + dataset = create_dataset(time_step, ux, uy) client.put_dataset(dataset) + # send last time step to see final result + dataset = create_dataset(time_step, ux, uy) + client.put_dataset(dataset) -def create_dataset(time_step, ux, uy, feq): + +def create_dataset(time_step, ux, uy): """Create SmartRedis Dataset containing multiple NumPy arrays to be stored at a single key within the database""" dataset = Dataset(f"data_{time_step}") diff --git a/tutorials/online_analysis/lattice/online_analysis.ipynb b/tutorials/online_analysis/lattice/online_analysis.ipynb index 857fd2aee..48ddf6032 100644 --- a/tutorials/online_analysis/lattice/online_analysis.ipynb +++ b/tutorials/online_analysis/lattice/online_analysis.ipynb @@ -90,7 +90,7 @@ "\n", "from smartredis import Client\n", "from smartsim import Experiment\n", - "from vishelpers import plot_lattice_vorticity" + "from vishelpers import plot_lattice_vorticity, plot_lattice_norm, plot_lattice_probes" ] }, { @@ -139,7 +139,7 @@ } ], "source": [ - "# create an Orchestrator database reference, \n", + "# create an Orchestrator database reference,\n", "# generate it's output directory, and launch it locally\n", "db = exp.create_database(port=6780, interface=\"lo\")\n", "exp.generate(db, overwrite=True)\n", @@ -171,15 +171,7 @@ "execution_count": 4, "id": "537a1489-b4c3-4736-a628-b7af433a9cbf", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "22:50:07 e3fbeabfdb3e SmartSim[1216] INFO Working in previously created experiment\n" - ] - } - ], + "outputs": [], "source": [ "# set simulation parameters we can pass as executable arguments\n", "time_steps, seed = 3000, 42\n", @@ -193,7 +185,7 @@ "\n", "# Create the Model reference to our simulation and\n", "# attach needed files to be copied, configured, or symlinked into\n", - "# the Model directory at runtime. \n", + "# the Model directory at runtime.\n", "model = exp.create_model(\"fv_simulation\", settings)\n", "model.attach_generator_files(to_copy=\"fv_sim.py\")\n", "exp.generate(model, overwrite=True)" @@ -209,30 +201,23 @@ "name": "stdout", "output_type": "stream", "text": [ - "22:50:07 e3fbeabfdb3e SmartSim[1216] INFO \n", + "19:49:59 C02YR4ANLVCJ SmartSim[54122] INFO \n", "\n", "=== Launch Summary ===\n", "Experiment: finite_volume_simulation\n", - "Experiment Path: /home/craylabs/tutorials/online_analysis/lattice/finite_volume_simulation\n", + "Experiment Path: /Users/arigazzi/Documents/DeepLearning/smartsim-dev/SmartSim/tutorials/online_analysis/lattice/finite_volume_simulation\n", "Launcher: local\n", "Models: 1\n", "Database Status: active\n", "\n", "=== Models ===\n", "fv_simulation\n", - "Executable: /usr/bin/python\n", + "Executable: /usr/local/anaconda3/envs/ss-py3.10/bin/python\n", "Executable Arguments: fv_sim.py --seed=42 --steps=3000\n", "\n", "\n", "\n" ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - " \r" - ] } ], "source": [ @@ -264,104 +249,69 @@ "id": "5aac0fa2-88a4-4c70-a187-764c0c97e255", "metadata": {}, "outputs": [ - { - "data": { - "image/png": "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", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - }, { "name": "stdout", "output_type": "stream", "text": [ - "Vorticity plot at timestep: 0\n", - "\n" + "SmartRedis Library@19-49-59:WARNING: Environment variable SR_LOG_FILE is not set. Defaulting to stdout\n", + "SmartRedis Library@19-49-59:WARNING: Environment variable SR_LOG_LEVEL is not set. Defaulting to INFO\n" ] }, { "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAvsAAADNCAYAAAAmPb/XAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjUuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/YYfK9AAAACXBIWXMAAAxOAAAMTgF/d4wjAABhnElEQVR4nO2dTY8lyVX3z71161Z1dU0/PTMeY+wxmMFCFrBiYSEkZD4DK38ELLH0h/ECCZasEBsEQiCQLTCyBmvAxgy2ZqZtj9vj8Uy7pru6Xm/VfRbRp/LkuSfeMiMyIjPPT7rKm++RkW//OPGPyMV2uwVFURRFURRFUabHsnQCFEVRFEVRFEXJg4p9RVEURVEURZkoKvYVRVEURVEUZaKo2FcURVEURVGUiaJiX1EURVEURVEmiop9RVEURVEURZkoq9gVDg4Otq+99lqOtCiKoiiKoiiKEsnPfvazq+12eyDNixb7r732Gvz0p+/DArawhcXddDq+ANN3v2u8y7r4n4/7lnVB0zFXQvNqqviuJRy3XbPKuODXOz+Pfe4H6Zrgz62UDHnv6vUeTt932hSo8XqZUv76qDH/kbGdB5tG4Pc0X7bP/mJZwBYWy+UvbfOjxb4tMXTcNS92XToNM5GPK/HMOd9K3YxKHdgKcKm2Gzq9D3O+f5XpFxRyM5X8yvUsmzOhz3GuW1NeT10CRL7lOov9oehaUJjKzdyVLrUcYyK2pgfHaUGRby9mXBk/rod6LfdM6XTodV8O1zONzrc9q1LVqPPx0EJGzhqtueHLP81fN0PlT+7ATp/tawPdibI1lTqt//THlxkyXVIaQ+aN2QKh1Eep8x+zX37PDg19aihl4ecAx2PODV8nZF26jC0NNkpeuxK1pSeUKdx/+hzpTorrtvrIvpIHl3UqtWfZt0yMBYzPk9LqeiFhNIu+7EJqBZQ66HtepGsj5fUfu/8YpOiuUicpapljRHbXZWPXjaH2Z2dNNXghTMWq0/ea6lILFbvPVPaomDTmPrcq9pWkDzzbtlJdyLHtPbpsJ3ZbSl5yvpBLRsxD9j0mMaLE4xKcMfN8tsaYZQHS2EDHEjCpvRBde/7FkuKa4v+lcbqvrnmYIu9D02wLPKZilmK/7wUwBYY+ft9La87nYqrEnNfYCPtYrxn+onN5shUFibk+pPuCP2djgh3SMzqmMMDnDxnN9FF6/z5qT98YKOnXj32euwoGauNROpHLxmPbZqpllfL0edGnWta3fGj0yCZiUhNiN6PUKP7HWsCqgS75lsPGw6OHmLZY775rGkcS9zVeR7XZemrMo664rs+uuCwy/JnuGk+FtF1fTdmQFuLZiv0p3Ug5iBUbNeRnqipvRSalnzc3JdJme4nUnE+xqODvT+n8K3Eflz7mMTGVvErxTg15N8dYcG3XfiqPfghSLRvXIzmYhdhXMeeHR3xSRlkBhrHruLatHn03UxKkXSmZB2MoSM39HhmamEhzaMQwRYQzdQ1cTeSo5Z4rKWpKfDUCIde5y8WQUgfEWFZxyDVXyPF0ZRZiX3HTVeB33UdtD/g5oi8xRamH2GeiS0iliID69o2kFHN90pSLGu10SoOel3BU7CvWaEafkvmQ1WKxDOXhK4k+BMNJmVdTzvccVc1zuBdD6NJA3TZecy3RGM9t7vwbY56EkCKI2MWV4VuO3mspnz8uaw5Pn62WwrZ8CmbzUa2p3lC5mFJ+uRqW0aH0GxNjTbeiKEqtTOldOEZy53+p92VoY/VU6Zt8ZN/l11IMXXtZCN2mrap2qCieq/p6TNeEinilNEP1YjEn+POIRx67bm+Idmqp23ZRaromUlqXOLVbmUrCdUSK3mtiPPwx9PXs52byYl+pD1c1mtJmVgL/9jbNdpazqbAcnL4C0iVkZ3Wte0hlJ8iJnq/06LswnrnlWdf7W8W+InpFbT42X6la8vuHlMBtPUmEpD2kF4q+2/Uty/fjY5IvylRivS+x6cDCgbSeFhxEUovRkJrAOeDrfWTo7eRmzOc2RZu2qZH6WpPyqW/e+fRMVw9/6LKpo/mh+5202J/qDZUD6SLn82PGXduW8DVacd2UvOq7y374vJibcVYRyq6CvoaCgE24u9LG56n4T0Ls/aXP8nR0rVXN8Vyba8EuR2P3KZPyGRBj3ZnSuZm02O9DTAlvbD1K2B7aOXslkbx3ErZGKzHjMcsO4WsdHaHCPGS5LiI/dJ3cwlvavor/KCZd8M1ASs9+quV8y+d8b8SkY0hSpmFqwr/PtSth87inyKsSnv0uhGgmHyr2E5DyghmCFC+UvtiqmX2NVkJsO3Q7/L80ztMwC/oKdds833ZTRvht23JF8KV5ko1nudwd96VBhX82piaIfPS1SVFy5ddQz8zaznfq9NR2fF3pUiseAtcrU8mvVGS18WgDy/HDI+GpfGwhfn8fPkuQbTykNoFHCSZPnyh96mmh6XEt4xPUXKTzdWyC37fPkIj+zIR/KQE+l9q41JHREEoFgMauJ3z5ljoiPQdctfWpKK11uqbXFxSV6CT2hzgJNWDL0LHftKE2nlAhLW3HF533bWMISu8/GX0ENJ/uGrf97zLuSlMorii9NB/HbdNtBQFpHz7xPzO7T5eXjyLjajvVd3s51kmRxpgGxTW+d31pdh1fjcdTkiGfI7F24Zh5OY4hpn0iR208LwgRwD7BW3N0ouaXcExURIF84t73P3Sabz++NIZgE+wh//mQpxvFvK0wIUXwfVF9nD9h0V/qHq3xeVuSofNi6OeziuXy5DrfoVbcGimRvph7T208L+gSTenbYGJoUkfUparJLladmKhIH6ouMMQI3xiLTKiAzzW0pS2V0Jei9TZRT4f4s22fzndtmxcKfF7/mVl9clL1/ZyArsfnq1FNyZAR2Bpr1Lu2f6vpGLowRO18CQdJ37YyfdIZsv7gNh5lfLgKJn0bw4buC5n6SzqImsW9b5pvGzc3Zrgl5zmF2LdF7Bcvrre9vfY8LthDxD6N6uPPZe+h4p7/B3BH9GcQ7Ve6keMZmaqNxRDPb9u7Ziwiueu7c2zktKsMgU9gx3b0QefVRm+xP5ULt8vJca2T+mEau72hG8PSaXgT2B4EUkPgGm+OaFJ0WRlidwmx4sQIdvrj025uGtFuWw7/u0R+VxuPz3dPhb5NzOP/xaJZjs6nYh6nr1bhhQb8H2v9kfJBhX8npmbtSBE5zZUH0nZTP79t7xrf+6SW8x7z7qwlzT4m8Y5mxHj2Y5etLb80sp+JLg+ekEavtVLbhZ2dvr3KdI3W0/EYkR8yTsW9VAjYbMyQLkO34UoHggWCGDBiDxBmzeEFAPq7uWkLf44k8mmkH4/JVytAl5Gi/bHdgyqzpKvor/ldkYPS75/S+89J7mPr0+hUCaeX2JeqBed80qZ+3C6PvvRSopF+SkzPPsVIIdT7jLuEPJ8WErGngl2ad3trn3d9bdZFoW6rDaDL0NoAiZBodoxVB8dXq2Z5FPso4GnUXtovzlutmvVxWTrkBQhXjYJtPxrpVwJI6dvP3X3hUEhao+S7V2rv53o/jk0n5K7J4Y6AIfFZc3xWndBl+Xox6QtdzrdvjexXQpWCd470tdHw8VhBb/vPp4VacjAaT4c0in9zsyv2eXSfb4PPl9LpygMbtmi5b0iF/WazG8W/vm6EO18G04fbwWNarcwx3tyYdSWrjyT4pfTzaL9G+rMzRlElUaMdQFGU7gXMUvd0L7EvVb+M7QGbo4TKSdWIw9XAZ4iLJ6YbUkrqfqOT4BP1PtHqE7khVpsu20ExDrAruLmQ54Ido/S+6ThNKiDYChkAbYvO1nOeF8I9Idl16H9b1Fzy4kuRfBT7PLKPgnq1Alivm+UWC4D9/XbtwN5eu+BAI/28FoCmgYt2Lvp5AYDimqdYGTK6nZMu4mDI4xz6+V6b/10qWI7dsz9kL0sl6dPPfuy2YtfHaSHnImRfGtnPTK7W2tWI5giqSHNIpN0laEPn+ZZxrWezwkgRfIC2zUay4LjEPRX2UrSf/iSvvpSn9BgkXzzCxSstLOA8l30Hh5IXnwptjORjlB7HEYzk4zHu7zcifrttIv7LpSkM4HmhaVitmu3y7eMyNnFPCwIq+pUETKVmI4TStR9d8nku58bHlK/T2GMLKTT2obPY5yXVED/3HAg91j4PqFwPtpDuwlzjrh4Uil4DNoHvssC4pruWlXqv4fuUhi6BbxP7UmNabsXhYl4aSnYeAHeD3BCk6DxAO6rvanxLl3WJfVxOiu4DtD34toa26NWnPxrJx4j/5WVTGFit2pF+XnMg+ftxn752Cir6kzKndxAyVAS5pGefTytNLenoi687ypTUdB67+vBjlk2Vf75eqjidxX6IWC194koQWsCpIsrN8FU91t438x0ucY9D389mYeGRclv0m+7HlgY6lCwwvhqDEMGPIt4W0aeFAr6tkP7yAXZFJwp0jKTzZTFabluGinBc1teLD0bUaS87XGBj9F/aj03sr9fNvjebJrKPeYTefp4XNMKPtQYr9ri1+fltaeS45iktahIUQzH0cQ4VYbfto3hQSaDGNMVSo1bJRUobj2u5FHkqBWddJPXsl65OiyF3OseSDzH4xH+svyxrHlFRisJdEvRSpJtP32zaItnmf5ei/VT803TZCiS2/9L2pH3yNNF0Y15Iy8VE76WIPY2c0+m8gSsOqeCX4IIfQeEt2YRw+evrdvSc1w7wbWFkfr3e9eSv12b9e/eaxr77+02kn/8WC4CDg7afH9Mm2YfofJpH9Hh8UX4V/FGM2cvve8aWOI7S/YvXVIss1XSP4XoroVdqy4PUxLYHqNqzz7tMmqLATUGVDVQDyHVOk2+Ti2Mc5yL/6qo93GzMfxze3hqbxs1Ne1k6n4p9KqgB2hFgW9pCj4PDI/+81kCKyrsi9nwbUhRfsttwEc2tMXyaz4Zjs/SE4op68+Xo9UCHtAaEWnHQq48ReUwfXRYj/zTSf3np7iKUR/htxxUa5XcVBhQnU3tvlarBKKkBJLtpKcbaWUlOr7iNMTRUTkkfi1DstiWSePb5uM3DXxO5H0w1HjNAnA+fLp/rIk2GJPJ51P7iwvw/P28E/eWlEetXV2Z4eWmWOTsz69BlaUEhJDoeQ4xQs0X7pRoFV+TeZo3hwh5tNNgzjSTo9/fb6+I4XZ4ep83PH3LcXaa7alsw/dvtbjedWAOBkX1MLwp7HN7cGOHPrT28ES/AbtefHF+UX0V9Eqp7hjFSWidzHqsrgp0D+p6irgIlHSXF/1jPZahOirGhufI9pkdEgISe/drFPWeuNp7Y7jNttTeuZQEyF6aocKPjNOKOgp2K+2fPjJi7uDCi/eKi+X96aoZnZ2aZ8/Mmon99vet97yruJWsJFYS2SLgtHxBXA19Xl5jS12bpB6WosKcWF9otpdQ41mbr8R2T6xj5cdmOnc+n54suj8IZ7Tk4jpF+fg5oAYaK/b09Y92h0X7cJ90GrTnwRfh5ZJ//p+nix6yFgSBs9o8qgxmBlHgP04DQ0BHhMQQWx0AtemXM5y/U7y8FUG3Lu+6pIp79rjtX6mbISE3wvmx+dmqvwej72Zn5//RpI+gvLsx0FPrPnxuhd3Jixp89M0IPCwqXl2ENWG2+cC5ycTkeCZfEtiSUJZ87F/A+6xDfrk/Mr9eNOOXzcB0q9GnUOsauw2saXG0d6BBFN43cc2FMt8mtS5hOLvJ5dJ6O4/IAJn8uL5ttof2H7p9+9ItG+Kngt6U5Nqqv0f9O1CJ4+lLatz80UzlvQ1NDvs1FI7qOM0UhOatnPyYKPIWISRdq8IP27U4Tx22l0BRVuJJ3b2d7XDzxBrQYlT8/N4L94qIR8Ccnjeg/OwP45BOzzMcfm+HpqZmGUX9eQ8DTwQUsCneAXWHuE9LSx5qkqDlu2yf8bYUNHPLIMu95hk5Dkb+31+57XurK0ibypbRKH9yyFeL4UGpjwNsv0B8uh+nhPfLQfdJl6DqYdrwuMI/RrkOtO1jw4PlAt0kj/DhNivZTGw8d90X4+fEo0Yw50j90lLuGd5wSTwmrDscVtR47/BkCsNt+M5VGzOrZl3bmGp8DkpAeuoqTp4OnwTWfjvcl6bFyMYdDFPn4Oz01Yv7jj43o/+ijZhyj9s+fA/zqVwBPngB8+KEpCJye7or6GM7P7fNQuKNg5qL+4KAZ0sg5ilL0gwO0awEA5Oi5rZcaas2Rfvv77bSgNQULAVIBxVcLwcdtlhz6HyPrvg+KUcFLBbx0HrnViotmaXna2BoLPHT7KPz5sVJrEHYFinmGkXzpesZ1+biUVhXyg2ELatUqRub23q31PIyVQXrKC6Dme2ysZPXsz7G1Nf3v87gPkQ46LqVJKoSU8j9iWhaw3RWCOI4WHbTjPHlixPqzZ+Z3dgbw85+b/x9+aCL2jx4Zgf/o0aDHYw5qa9IMYAoFKJoxio7RYOyeEqAdHUYhyy03NqHNI+5c1GP3koeHJh0o8lHw824lqbjHqLPU4FYixFfvit5LBQDb8vhbLJrlqCDHfKS1NS7bEJ+Pwp3uE88T9ejznnuoZQeXo7350DzEAgId4vQuEX4tFGQh13NRigRKy7goUZtcsgZ7bhqjL77AY2mRn+P8lSo4hHa1Wb2NB3dQ+uKIYeiTPqa88VHkhpEEP3aFeXZmbDknJ2aI/589A/jZz8z/H//YjL/9dr/IfUpojy8o2rh3HIdUuLsaxFI7EV8WrTiHh2Y6ivzDQyM2Dw8bcU+H1K6DNiUuTm1IgpwKVm6ToevZtn1z00TJ+X74OryAxC080j5w+wB2mxE9BmoTwmuL749Px4IGjeLbIvyuYUh+xSyjTA4VvQqnROCxRkpomVT766O5s3W96Vqny4GPobV9SO1Gif1KNh6+vrRN13xXGrpyF9GXRD42tkVrzpMnRuD/8pfGpvOLXwB88IH5/9ZbZl6toKVnvTZiGyPtKL5plB2XlewztJEvtehQ/z1u//79trjH/azX9ki+zZpj89TTLi2pqKXLIDxKb4vy43ZxHWlbUv7SyDitKcHoP08Dj5JLx4vHRSP81LeP86UhrZnBdXNE+NW/nwzXOy2mfdOQ8FrbIfc5NDVrgRqxefRrCtrmqKkJ7SWnLy6PfirPvktbD+bZH4rSD9OpEJOPNr9/9gcERvJR7F9cGFvO8+dG9J+cGMvOz39uoviPHpn/Fxd509UHFPLrddsyEyP2eQ84dDquc3DQFvVHR+0hFft0H75GogBt37xNzNNl6ZD3aGQT+S6BbysUINL3A6TIOG+sK1ljcF3XcdEIP/Xm8yHt8pPmny3CL+1TI/mTIce7rCbhptSFLWBa8/UyJr1Xezp7efYB5NKhK6pde4b0QfLsd8XXa06XGzTGo8/TL42Hpj8ILnawl52TEyP0P/zQDD/4wEx7910T1X/0COB73zO+/do5PAQ4PjbDoyMjtOkQI/DUR09/Uv/36Pff22ui9LTGALdHI/tYmKAfj5KixzxajV8KRrgnn4p42/cI+NeH+bq4XZe4t+0fwB3958dIbVS07YNN9NN50teSpbTy2gksSPHj5+nDoa0vfn4MdD0JLQT0RnqmpY4axj5nbQz5ni1VwFC/vh9bA/OxEHJOuxxP7mslxj0Rc/9wl4Zr+5xekf0+VQop9lMbtUdVQnvjocuGkuS4qQjEj13RSP7pKcBPf9pYdT74AOCHPwxM3zBsAczXVrFvfoy4Hx6aH4pv9M5z0c9tNrx7TBrZl7rMpGKfinwa0adeflsE2ea3p2Ka/riY598l4CI/NMIP4G9IK63D/3Nskf7t1m+LkfZNCwF4vLyRLs8rqYbEdlyYZjpdOoaYdCu9qTXyOAcbD6XGc6D0J6SXwBB7Uonrg9t6Sl+jWTz7fLxPxDv3g6RP1Nwmkms4saEtwaVxVyk0NG+CzxuP5mJ3mtgl5uPH5v+jR2b4/e8DvPMOwP/+b9j2h+bP/sx8YOn5c1MYQWGLkXoEhfbxsZl+fLwbgad+emrdod562rAW1/GJfNw/hQt1HAI045Kgl9bBj1PR6dfXTQGIC3xXZN8VzQ/FJnxdjYFd8wHa3XMCtL35WMhbLpvemFarxpLG0yA1usZ5rsi+Cy0EJMf3TrMFUEJ8uznSmBMV+HUS4hsfK6H3Smx7w1T4etPiz4k+nn2f80IiS9eb0rI0obUR0rjYtx6fllvw+woVNRQ4KOL1wcUdRvNpA9zHj83///ovI56//W2nJ7/0ES++8Q1x+vZrXzN/Li4a0YuFAN5LzsGBqR2gthxXRJ+LfaxF4N58FJI0cu2KMHNBLgl8LvZR0NpEPg6xQACw25iVR/RdXwhGuIilXzSWbC6S6MXj39vz984jwdPN85fn42LR+Pmx8CAdm0240zTbovzKYEgdISQPjgSmITc1vdNre9fVRg3nKBVTONd97lHbutltPL4d9/XsD3GR5txHDr9lbP7ZqpH6RgBsy3ayBKFIPDsz0fynT01D21/9CuBHPzIi/5//2fj2x8pXvtL+qu/pafMxMPyQ1Xq96+mn0X0U7HTIrTu0RmC9hu1qf8cls4QXZYfVthHs3KvvEvLX102DVC78baLfVgsA0I724/5pzz44zQWKXP4NAGyEix8co12e0nVtvncq+gGaNOE0qTEwL8RwLz6dTnsOwl6DcBnei5B0vFzgq+AfBNtzNbbGVKLvO2lovz5AGUHpql0ZuyBU7KS41nJcH6HuCD4eIv5TtA0a3LMfWjLz1RKkIufDKkeUxZZ/Uk0Ln28bj/XC+dIXMm3Hn48C+KOPjCf///7P9Jf/n/9pfPoOoT+Gx/riq1+9+7/9m78xBZqrKzPcbIyA5h+04gKef/RKiui/EPnXm4WpPLhsa2barne5XDR5Jwl2FPaSRQeXo8L/9tZYmNC6Im0T16HRfu7fpxF9nwefC2npGwT0OwEYUecNcqXtYpQfoEnTIvBq47582jMPpo2LfFrgwXTZCiEcFfmDYrOmxgRVcqZtDn79FG3NpsgUot+x1HC8vnvddl5CdVXfYxzcs187PnuSy4tZmpQXfBehb9uOdV2MAOPXcJ8+NUL/5z83DW/ffRfgW9/asVGUv637sfjTP22Nb//qr5rvAiwWTXeYKOBpI16pm84Xw5vlvqkseLobLEcduF7D3Ud775DsJpKQ51F6Ov/qyvy/vm7OKx3ny1NfP4p9m52HCmAU3SjC8eDwALGLS9q2YbttRDouK33IjEMLA1z0c6RCCfX+0y44bdYeWtDhx26r2YjtgUcLBVlwRZlLiK+c+6xJ4NfSRk4ZlhrOt3TdSYV9aT3b+iH7tO1HPfs96VOIiTne0FqAISM2Xaw6vkIgADSiESP6Jyfm9/ix+drtD35gbDs//3my46ma3/kdU6OBtRsoUKlap3YdbNT7ohBwebWAq4vm48KoqSnYlT7VegvYtiPv/EeFPo3wUyHP18GIPiZGEv2uRrwAbcFr8+1TTz7mDw6pPYb2ZITLS41eXQKY2mjQz++ClrC4kMdtUesUnYaFFSry1Y9fPX06a0j9Xsz9jvCJmZzY3r9q42kzRq0VQ6ztJQcxVp1UBVTbutk9+yGJkqbXTE03SYwVJ5YuJcO++7wDhQ161k9Pjdj/8EMTzf+f/xGFft1XTncWf/RHAACw/ad/andZCdD2jFNv/noNN6sDuHpRKYLZiVoaoNGH+OM2dTGiT+061MpDe+LhQl2y9XAbD07HIff1A+x218kj5BJox0E/Po3Cc/8Snca7HY0R0y7BL3ns8VikrkbpT+o7X9omTzefpgyCRpnrCuSp+FeQoWzgNmIsZiGB8xTprr7rTcpQpbXQGovc5HxQhXr4pXYAIVF9Md9oRH+zMdaVp0+NwH/3XYDvfhfgO98x3WzOkS98wUTr0dIEYMTx5aX5v9m0Gt+enTbtmjcbk628XSsGtpG7Xh6XW4DN7W5kXvLlU2HPrTs0on993UT7Ly/NcjjkYp+Kft5zj+Td59DIPu+piDbGxe2gb39vr5mPGRJq6fE1EpYEO+/5iEf6pag/3xYdx/9Sem37VStPFkKjzL51x0ptxyCJ+7mK/JK6ZShKn1uXjYfabVzzpPNks/50Cc5SskX2+5DrIuWCtu9+5nBDITFC3wmN6p+fm6j+L35hLDw/+lFL6M/tMb344hdb49u//dtGTFOvNwDc3C5aOpu3q73b5mJXp97Z3W0RZh5Zp8tyMc6j/9wCJAl83qsPXRegXTUhRfdpbzvUj4+FAt6LzXJp9oHCHgsEdxlNtuESwjY7DxYsbNgEPM1L+t/WcFhF+mhIFeBS7Mzp/au0qe1+Cr0OfQGArpa/QT37qajt5o2JzqQuPAx5Qbs8+jZCPGsA0IgWVKXYxeajR6bHnX/7N9OPfq0fyirFq68aexP63Z8+Nf8fPoS91Qpub/fuAunYzhmgbd9BDYnjd5Z1m0+fevOpDYeKeyro8ZyiuMfeeDCiT737tuh+iNinoHDnH51yCW7aQBb98rgOevppbYAkql2Rfb4uL6jYvm0gFWh4pD/EbqQFgaKE9MYzNUp696V9xrSns52vqVCbjkqN6/iGtOrQ/y4vvdSpC6198l2HvmVDjlnfDkonQluct6KaKBSx952PPzZ96P/4x6Z7zbt15xfVl1j88R+bj2thY9PLSyN0UUC/gNvtuYYU8S1AbTQ4bovq0/8o2rnHH+dJNh5clvf8w/vz5+0FuNWH9u4jDfE4bMcLsNsbDp+PuNoR+PYRsn1FGRFjFMxTF8SKQknm2Y9dN/bhMIRv3bZMTKS7b76Uwheld1UfWaehgOG973zwgWmM+x//YfrT//a3+x/AVPnsZ01ePXsG8Py5EfxHRwBXV3D8yq8DgJkF0Ojana41obHz7O+/mHdmierbrCX8xwU33w5G+Lmnn9p4qJ2HflWY9q/PRTDvOpN+OEvyy9/etr37APZ10c5Do/SpCN2WZOPh9h6lOlxR5aGixyW690xNrjZztojqFKmp0XQOajxvkkffNx67rOT3t60roW8PDzE3zFRvrlCsNyH16WP3kicnRvSzXnfqu43Lsvjd34XFn/yJ+cIurRm5uLj7plYvDShFmCWvPv0vdY1JCwDb7e7ytPBA5/HIPk1TqECWGvDGzKf7or0gSWDNgqI4sHV44Jo2R4bqlnQBW1GQzf2drcyHKhvozpWaXgC9e+Shgg1F/pMnAJ98AvDOO8a289Zb8+15J5ZPfcpE+K+vTUFps4HFK6/A8fH/awl+qpvpx2Wx05rVCmBxSyLxXHQjXPRzgStF/7mVBtsa0Om0BkCyAOG2Y6CNbUNKPpie1ardqAGHNMIfg6+hbh9SRvW1hqA3IZ7vlF0lhzC0/1zqiQSnu6LzNfR4N2V//xQLMLU3xg4ttPa9lvpcf8XEPm8QOoabaAhqyIvQlxdn52ZEEccj+r/8pYnq/+IXZF3FxeIP/gAAALb/+I+tPN07OoL12nw5l0M1c6tbeZdfXBLafFpIpLwPUjeXtOSyWDSlF/4foPEs0XEJyRrDBb8NqQ9/3zG5xqXp9Jhj1lOyU2NvOzWkwccY0qgoXfEVSLrUKkmNfLsUfDSyXxE1llx9ff3vTEeRtlw21p2zM2Ms//BD83v7bdOv/vl5zqRPk+NjU2i6uDC986zXcHj4MlxdtfWtxJ3g3zD/PYBf8OP0kMIARbLw2FgsGhFtE8co7rFRgiT6bUO6H9sx8Ah/CFgwsEX1aVehtNRFxzGdOJ3+DyW0cKD+/+S4ajpDetmg24htJ1YKW3pTvseGiuh2qampkdI9JOWk1vz3aaRU27eNh1KN2J/ahdmFGquqYj760IL79M/PTTT//feN0H///ZzJni7HxyY/l8u7Xo2OX3kZLi/b7Tqp7qT6csdmJSF9bpc2ZJWWt7G314hhFOV0CNAW1TYrDBfMVPQvFqblMfqU6PYlLxPdHt8ntfT4js0GF/E83dJ2bdP5dl3jMelTktKnNrSPWC7VVTPfH/+gVZf3mJT+od6HXWw9ioLY2uWkalyewgbX6anvanzkGi95M4dSIj2+PCqNz7/fAoUEFfrYr/6Pfwzwwx8CfP/7RvgjX/1qrqRPjwcPAA4PTT6/+NLuvdU13L9vdC5qVtTPO3Z23qsOQD+PvASPVNOoO4LjXJjv75sffhEXx/FTwCjw6TQU+rgdXJcXKnzYajlsPyk/eFSdHhstcNgKJDT/pEIDnS/tUxkE09yz+fWhtndgV2heSEP+nrO992p8B1IWO2e/+ZWmprRwpPPPrw/bbyzYenryXe+ucWl7vukS1UT2AeqMbA/BGNsv2BrqAsBuVP/pU4CPPjIf0PrJT5qvPy0WsNhuAf76rwdM+bhZfOELAACw/c53TPeWV1cAp6dwdPQyHB62vxdFsVp8XN54GuHGL8aiYMX/VLBihP36upmOdpubm2Y+3Q/vyx/HeXSfimSA3cg+texIEX8urDnSR7F4nrig6ZNqH2gacBzTxv/b0mqbpgxOiue0rYFrn22UxGdncOXZWN57tVOzjSem28mx6SHElee2eSHHmOJcFhf7tQj8GtJRev8+gr1pVOxfXRmxf3IC8PjxnX1nAZC/oeeEWXz5ywDwQvSfncHBgwdweLh3V44CaLqsb2Hz3dvgop9PR5sOLQSg4Kb2HS7e8Su4CG7DZROivnhcFgsTkrjnfn7E12MO97RLlh6aJim9PIpvi+qj8OfL0e10jeRrQSArqS00YxI1SA4xVvt7cCygpvE18Kyhd6QxIV3zfJo03mf7KZbNKvZDfEy5GvbEUsPFXfvDPkjkU6H/wmYCjx8bn/4HH5hxJR2r1Z1V6sGDl+46PQJoC31Jb9/NsPnJuQWFCmzsuhKFMI3gAzSiG6D1tV8AaEQvvV4AZBuNZIuh2+DCnlp4+Dxb9Qb/mBamwSaWeeFDyp/FohHydBytSGhDooIf59sKAdzyIxUAfAJfCwBJyPGsHmNkv6ul17edGt7HXSjZhoKDafFF0VM1APXB23XQfY25UTRA+LUfkrcx/v/R2njmylgu6CBoX+po48HuNp8+NeOHh9AKQSvdWa3u8vng3ks7X87dwRfVp5H8vb1GiNNoNEA7ok9FLi5PI/tYOKD7AGh/DADH6XyAtjinvn8esed2Hi70+bZCRC8V/Hx5XmjghSZuy5HaHbhEPd+uDV4AULJS27O6JnGptClhQSldSJIEPZ/vGp8jQ+VBVWJ/zCX6PuTuuik11nPEvfqffGK62nz/fePXPzszQucP/xDgX/5l2ERPlfX6zi517xW4+6oufrMqGGrD4QKeN0qlJQqctl7vfiALBTj69TFyvt2aBKLPiH+Z14XUKw9Od02j63NxbLPiSENcFtseULFO2wtIP97YGE8W/fEIv+9nQ4V/FnI+q8fy/LfhszLEbGcqDP1uL+3Zdzk1XHah2j36qa7tkG3noiqxP3dqvdA5zkIZtWacn5to/ump6WcfABbn5yr0E7L40pcAAGD7/vuwt9zC3t7Cr/Mk/72toS6dLxUIqO0FoCkcYDQfRTHd1mbT1BKgyEeRHFLzAGAX9JKvny7rwte3vbRt+qNCX7Ln0M8Yc/sPFfd0+/S4fQJfUQYmpcAfw7uvKyWOrzbP/pQKcWNExX4ljP5BJ/n1f/ELgHfeMV79kxOA1183vfIo6VkuAc7O4P79+3D/vpl0cRHQHlcSl2j2d4lvKj7RuoOCFQsGm03TO8/NTRPNxxoAXA+HGNmnjQ1s/d1zAW+LwHNxTue51ueFBxznEXepj//12j60Rf1tth4+zo9FsvBogSALQz6jQ4VR6fdGjGc/ZjsqDLsh5VtOz77PthOyvmt8KHJG7n3M0sajjBjeb/vVleka8vlzozpvbkwf8Uo+NhvY3wc4ODCVKl7NxxegApdHkXljXfTi0yg+Cn2bJQcLANgWAJfHL+fi/kL8R5K4jS0ASMvYRL4UxaeRehzaGuZK45iPMTYdn9BXFKVKpl57odRNtWI/pvVyH2qJIIztQdCKvlBPN0b1T04AnjwxDXPPzkx09403AL75zaLpniy3twAnJ3D82SN4+HAfLi5MWQtnWfWzJBjRqkP9+lz0YsMA9OJz4Y7zaTQfGxLc3prrgfr1aQFhS64rH7aIPx4HX8ZVAJAi59g+AbdFG95SAU9718GPgtFI/2JhSmHUxkNrBKSIPt2/FN0PyQ8lGVOM9sVis4Xw8RBvtjQ+dYZoUB2bxzG2npBal5BzXSqKXiPq2VfGBRVtm41RmpuNEW57e7B47z2A994rncrJsnj9dQAA2F5ctDqmEfUyikZXF5cAbXsOLnt93Qh53vUPLkN78KHRfNwnjeqjyEfhT7cT+2VfW00Fr6EAkKP/UhSfNsiVRD7vWUcS8dyiI9UKSBF9KW2uYw4R+q4uRRXFQ1dRIgnIkOWmTAqRF9rnu28bMeNdCO12tUamcj1WK/b7fG1sTIw5srET3ad+/WfPzHCzAXj11bIJnRObDRweHrTaxfIeLu+wiUgq7mnUHtfBiD5ab2j3P9KQ+vTpPEnkczsYEir8bYKf/peGUoNf/qPWHm7N4QJf8vLz3np44cEm+G0R/RmJ9iG+MNln/zno2if3ULj81iHdLvrWHeM7MYZUPRWFTktBSF/vfb630IeQD1uF9u8/BLwBdW6qFfsA8ynlT+IYqY0Hu94EMILn6Khs2uYEEYBWgY9DW5QXp+M8FPy0EEALALaPYeF2qOhHiw8V9j4bjyT0bbUSlrywRvdDIvu0AMD99lzQ84KArUDgEvld7Doxwn9k0f2Ql+EU3xW1B7VibDxdtz1lpK4pcfrYCjxTC8JOkWrEfmiUIOUFVcONVEMaYrGmGe07FxemYe7NDcD9+wCf+cywCZwzqxXApq0XafAcAPyCnza8pYKddrmJAh2XQQ8+7WGHinga2ecRfJ+NJzbCHxvdt0X1MS9wHkC4nYcWFOh03KZL3NusPCHHF8pIBH9sjy45I5pjFTNDt3sb6ousU8PVR/2QhLaxsN0PpTz6Y7s/hz7H1Yh938WT46MLNUSDakhDb6gou742gh9bh65WAMfH5dI2N66u4N7RCu7dW9xVrqCWvbmB5o4PifDj/A0pPVDRT0U7juPO+HzJn8/tO/ifDmmvPtvI+4R/LMvVzSYX2CFdbtp89zZR72p0a6thoEP+f8L08YYDpH+Rjk1IUFxpH/27R0mOr9Dhu2ZqsfEobaoR+5w5PYQmIfgB2l1uXl0BAMDiG98onKh5sTg+hu3Tp3D//kv0NDTYosJc8EvRe5zORT9Au1EuHefi3ibwXT3x5PLu2z7MJYlwm0BfLu1R/5Dova3g4Uq36zhjqDC6P6VATgpi+zyPJWadrvnZ55xO4RxOCVePPiUIrTUIuY6GLiwM/YyqVuyPzbM2J3Ya5iJcuPCvpyrDcHICDz77Emw25gPG6KjZbqEt2kPgEXqA3UIAjehzC5Ak7rEAgNOloW0axydYfcKZimuXlz9G/LuW4fv0DaW0u6aNmFqjcqUb8fWl9Du09P6VdNSmy1LWGtT6/ElFtWKfktqzX8uFitSWHo63xGsT/LxrRmUYTk9h7+oc7t27B2dnnmB4F9GP/wHa03AcwO3Ddwl8m20nthtOTDPF99EtmxhHyw6d54vS+6L2fUS+a/pIyeXhrf3ZmpM5HrvPR67EE9q3fs1iucZnwWw9+y5Se/Zj+qAdghovREqw0AcwAv/wUL+WW5qrK7h3fG/HoQIA7QlSZBzFvG2I63UZcrsOncf/S+MxhFh5AORCQGhBoIuQl4R9qMj3zYuhggJDboGQIhJZu5CxUft7RaLv+Rrb8Y6BmrqAjelek68Xu+3cTN7GE3pwetOODGoPWa+N4NfIfjlub2FvuYW9vUXbpUIFOxf8voa6rgIAj+zbhtT/T6cDmIIACm4ktlEuwhvnAri/qEv/hwr9kGHoPIoK/Sz7GvM7JTbtmLe1Hrfv3HcVbMp8GPJbAqHUer8BVBzZt5XwU3xxLsV25srOhcyFy3pt+tX/1KdU7JfihSjdwgJWK9N0Yr0mOpqH+iXBbpuG/5E+/13TQuZJhNpefIKf/u8yLWR6SLpD54dSgcgHKPP87RI1ruU9EduQtlbBgXRJ39S63Vb6UaOwri09lKxqLFW1SI4H7lirZqtHI/vlIWJ+uTQif7EQrDy8rQWN+NuEvm+/fBv437WdEEFvWyY0XaHjsYI9xopTUuRXxFieu2NJ5xxIfS5qa2haM5Ko1ntjfFSlxmz9u6b07A/9iWJp/wATfMjQqD6dpgzPi/7dUVuvVqSDmRCvPniWC4nOp4zW9yVUZIdM6yPguxRM+lLJPViTOAiNCErLlDyOyb0zIF8gT0lHrfnpShfOm7oPP4aqxP6cqell6MJ6MWM0ebUyon+9ln3TSn5YZJ+7du5mcCEfMo7rUnwFByl9fP0U9BHTfQoEvn3PWOTXSs0vZRtjTHMJ+LtU80xBJPE/9PVRSut1EvshJSppPOTLa75PLXfJKF4zIG23NLWlhyKdg9a5RJEPYES+VWEqg4DnAJpTYxX8LrhH37V8rGBPHd2PvdZSCfCY/ea4Hyq/x2p9pgHUnTYbIWkek7h1pTXlB9ZCGVPeDcEY7xEX0vEMXYjuovVs6YvZVtHIvqtgkJupXcQ5Cc4rKvpRYSrDQ9pKrFbtruvvkBrb2iLuvvPY5UusXb37IelJvY0cBYkujOh+GuPztaYuBrsyBbFa8musSl2kDoJK53gu57232B+i+6OUn2guURVq22fqj4UVAQUI7VJRI/tlOTwEWC53ovp3p0Py5kuE2m2mcJ5zH8MU8sjBGJ9fXd4DQ0SiY/c7Nmq4VqaUnzmpwXGAaaCuDD4/Zjx02a7H7tJ1ru25tGkK3To6z35IA1fXSdKbPBNU9NMIvzI8L87BArawXC66WXgAdsX9xAWryByPOZDSIqAvqTtLyCmMpmTfqaHhs/bjH0Yt93ipczO0vSdkXldHTHWKLPTi8i3X9UtrShhiIxemKjV/C3F4ePd3b9mjy7Tlnn+ZGTAHETDnezXkXRJKSLu0XIy5pzea5tI95XHGmJ9d0d6m7Ni+0ZSid0hpP7bxrlQn9qX+XKUHgdTo1ldF4hpX+rGFBdzewt1PKcf1xnxMa3FLzPodItRzO4+2LNJnxbSpXWTMgVrvsbldG0MGQSUNNyb65BVdl3cHT/VsynypTuxzujaoiD0JKTNW6lWoFnLdzCjyN5vmpw6IMlxevviYFoA5EQA7DaZDhXwuwZ/qw7hjYMxpnzJjFBhjp7b3oUSf66KP+KuFUoK/ZkL0YQ73CNeSPPAdUyNWndi3RetdBykxlotoaHIL/aurZqgipwzX1wD37kFzUgDIF1/3gr9/1efDtjGIvQVZ2EvgLLJdl32bKIRut8+2lHSUiCoO2dVkTQISSdnZRi5qSsuQ5D5uWyS/xus0NGBr05k2K07JtiJViP3QqHqfi1HFfzpoXqKQub01og2j+ir2y7HZmPOxR31V63VrmVSC3yfUt4mfY1h2sRHyHbebG3+hwfVNMJzPSdlZUey+le7waJwyPLXWhM+FuUfyS/TSOHRedBb7IVUKKXs5wGHX7Za09ZQkZUNlXJYvf3trosk3N0bkX1wAPH+eJgqrxHN5ScQiKv+7rjabk+IT/FzI24S7JEyHqhWghH4jDKB9bNJ6tkIDXtOuzxPw6b7jjBXvU+4BdUhqe76nFrw1v8NqFHwlETu8UCZNynsgZFtVRPYptZb8Yqn5QZsL6tlH8a8Mz50op2qTffiK1shQfAI/VNh3LQCELh8aWafL2QS67ZMDfHyx2M2fvT27/SfUFhRScAgh1XaUsqR8B9b0HprCe30oajpvyjQoIvZ9Hw/wLa83QjqkKuwuvjK08FxcAJyeAjx5ol3tlwILWwcH0JS+Xgh+GuRH6xVChT0Vjq4agJD/AHLBz2fxiSkcuMQtj9LzGie6bux/egy4HzzWEKsQJ4fw77utqVLre2SqorjUB8kUN3PJ+9ru99HYeGKoLZOVOEIvStpI9/wcYH9/gMQpO9wJPR7Zl5Z5gST0Y0Q9nWYrQEj7dQn6VA1b+TQu0Kk4p1F+Whli+y/txyb6+Xq+jxjb9tOFlNtSlNRMpUZ/TMw5v+eoSXuJ/ZgM65O5XT4yYLuQfbUKXdsD+PyWOS8u3zcHfF8UpvOk9VwPBR4pvroykf1f/lIj+6XYbF4IzeWyKX1hi+nVwd35urkxwtcl7m1DXB+gEc8h69j+h7YPiMEV0Zci9a4hbgu3EbIODrFRMRYseDpc0fyU1hxf4WIu1P6STy18a2mAPGdx2YXc523OBazU+Vn7MwWgQs++xFhLYbXfSNIHzELX4cvSqCfVl0oZlktoSmJEdXOhj/iEOl0PIEzg29al69P5/L80HgMXtbh/HoEHcIt4+h/TTcU/rQVwRf23W3dNAoBbkKeKzmuUf76UfJdqjzt1MefzMFZN2YdeYn/ozOJfHKPTS564mi6a3DUIHB6ZRN/+yYmK/ZKs1wBwdtZ8+ICcpNvb3Yi+JM6lggFfjv+nhQGX8A8pCHRp5BtirwGQxb3t/2LRXl5alkf/XdvD/1I7Bp/oV6Hen9Lvi5LUcNxj6Gu/NuZ8zXbFl2dzy9PBbDwpCK12mvNDo8/56Bp54T5k/Pn6RFfycHQEcLDeAjy98J4ESehLUfxQkY9D23almgFfGwBpPARb7zrLpWnAbLPpSEKe1gqg+KfzqYjHCD7dluT5p4UQtPdIkX4V/Gmp/QWf8/1Vq8CZ8ztbUYYgi41niC+xucY5PD02j7pvG30fkqnypa/XrkuhyWXf4eIQx6+vOydR6cHLL4PpDunkxHS6T8LSvM0u/wHIXn7fctLPNp9uk24Hp9Oh678NVxeaPh++LXLvEvg4Hxukr1ZNAcG2Lhf8tEDhaihM80EFfzw1Ct2hKfGFXRXzClLLPVhLOmJAHdrlfsoi9nM3/JBsPGNnCo1l9vaM0Fmvmw+2fu1rW/jt3wb4+tfHfWxj4dmzLdy/DwCfXBihv902qhPkiDlvKMsj+Vg54BLxuMz1dVwBgG5XEvqunn1c2Brn+jz5LtvOctk0OkdBT38Y0UchTpfl0X6X3Yg24qVtAegydJ6i1I5ad/pTopCWg1pql2pJRwz8A7MxVNtA19bfvmt+TsZ2UZRguTR9ux8evujjHYz4OzkpmqxZce8ewOLsufHrn5+bk4IlL6IMYyL1KOTpUFpGGsdtSutI1h/XkKbdRmhUnw9dAp9H77n4x3WxAIAif2+vidJTOxBO57UIfDnchmQBonkRI/jnWjjQ53ebWvJDSocWANyMUaQq5anWxuPzj+eKhPMCBU3H2G+w2PSHevipaEKxf++eESoXFwA/+1mn5Cod2GwA9gAaxY1Cn8DFNU5Doc9FPRXsdDoV9XR4c2N+XPzzwoRUYwDgtvSEIol8ALs3H4dcgPNIPnr9UbDTCD8KfywQrFZNXtBl8Rglew9PM/XypxL8Sr0MJXRre5+pwFdSU9P1nZpqbDy5mILVJRTed76r0DMEfP+8ILBYGDGDNp6jI7Mc9syjDMP1NcDBAtrqmXtaoC2kpYg+nWcT7nQ6frUXp6H4R1sPLyCE2HloOhGpBxvpS7WuiD7+59Mlmw0V+3Q+jeKjwN9s2pYdauehDXxvb9vC34YK/nkwh3faHI5RaWMLFtYiwmtJRwx9CunViv2523hCj7VkhIama2/PiJL9fRPZPz42Uf7lUsX+kOzvA8DZ1W6n8i+QhLTNY++L2m82u9Ourtrr4Dj18nNLj+3jXjER/RD7TsiHsXwNc2kkH0X+amWOjwp/FPjooqINbalwx+VoTQEWBnAI0C7k2AR/bB7Nhdpf6lNrfyYR24mGMn5KfGC0NLkD0n3yrZoGuiF9oirlcfXKs1waobNeG8G/v99Yed54Ywtf+QrAX/6lnscc/PSn26Yd7mbTbs0qhL6pTx/HuQB3Na5FoU8LAhjd324bkY9thHnBAQsIAI2dR+qVh6fZJli5oOeHzXu9kQoANtEvNZiljXJpI1oqxOl+UbTjetfX5v6gy9F0SQ1zMa0axVemxhwKPIqSimINdPnHrqZ0s5ZqAc9tPDQtdL6rdXaKNId+shujlKuVEfoPHgC88grAq68az/7TpwCPHvVOjmJhbw/g/n2A/c25yeyLC2v3NXSyFNWXvPUozqmod42j2KeRfSxA8B57bJF9FyGRe9sXc6XIvq8AQCPsNOJP7TwYnUc7Dw75DyP2vKBAG/ZuNu3GvjSdtGbAVcBQxkGp92UNtcJT0wtKQ802npg0pNZWfdatruvNKTK2qqe+aY1Zn0dE0bd/eGi8+4eHZvqzZ72SpHjY3wcj8s/PGyXNW34SJNvM7e1uAUDy9HNLDhf+3PpzfS17+alHn0b2aboQW0NbKniXS7MP/qEqmg20FyCbcKbboeIbhxjVx2WpgJfylh8H1mbQaD1uGwsU9BRSaE0Drhsi8LVGQCmJ9A5Voa8ow5BE7PMocwwlffgxjEnohxDioaTL+Hrm4UL/6Ajg058GeOMNgA8+0Mh+bm5uTGQfHp8BPH9uQuoAYlcv1OrChT5vdEun0Wg9jeDzSD6fjrUCUgNeKbLvwhZ9lxrZAjSNZalAjhX+KOCl3nNQoOO6GM2nBQAcrtfNMdMPZ9EaCJo2ugw9fjweOk2FfJupPa9zUKK2OnTfNWuBqTFEz4Z8eon7c+7PhCw2ntB15p75Q+LK69Q3O4r+oyNj57l/33TFudkAHB0Z7/7f/70+zFPwwx9u4eLC/F/c3jQqOgK+OI2282UkLz8tHHAvPwp9yc7Dawx4Tzso1BEpio/bx6/X4jT8j8vzDolwOt8HnceH+H0yF7SgQdOBtQQ4jtvFwgHvDpVbiyjS8UjpVxRFKYVatOqik9h3ReN9kfoxn3yffz/m2FxeeJ8w77KeD9u6eMOGFM6w55HbW9MTz8OHRsx8/vPGWfLOOybC/61vdU6mwri4MGLx4ODFCIbULT4SmwiUxDfvNlPy5UuRfDq8vLSLfRrdB9jtZx+RvPa8pxzebz0dx3WkHm0ovCGuBBfu9CNbtMDCffi8loAXqDDNNLpPG+jydGPBQ+p2FLevgr/OoFItfvXcecO/UwMgdNnM8qB0nihpkVwfJduJuHRO6His3z8FrlqSUAb17OtN7GZMD7qYXnleegng5ZdNpH+1Ajg9LZHiaUItIncKG3H49V3bs03nUXiX9YdbgKjQp4UIqQceLoJxSL34GMkH2B2n63KBzy1Mruyh86QoP/alz8FoPfXh4zj/UX++lAe8kbHUCFeyHilKaXyBqdDvyIzlnTgVcuR5iQ+UDrWflEHXnBRtoKs3cb8ux4aOWoV8SRej+8ayY4Td5z9vxNj3vmeE/tnZQAmeAVdXpsej42MA+MmJie5L3hXJmwNt4cmjzrb+9yWxT+072ECX9tJDff68b/4Qzz6N5AM0Ihkj59SOg/8x6k4FMz3GFXv6SV11SnBbD/1CLhfrNELvitTTaD7djmTnwe3gty1sbQ2Uel68Ic/qoYVt6byxFQZKp2su8DyXevfrWxMTcj3n0jE1ivCSadDeeCoi5GUQs3wpULCgveT42Hj3P/Up83v/fQCALRwdAZydaYGvC2++uYWzM5PH9+4B7N1eN14Zm+K7vb1rTBoKLSNwb730HwsJ9D8vIHChL22PQsUsinUqiBeLXdsL/c/7yJfg3XFSLFm54/nn26fHxaP3NKLPRTrmjQ1XewMfWhgoQ8izeujgV40WJ6TmtM2BrtdiTbYsfg3VeE1JBS4fXfNTxX4Pcl88NVyYthvXVQWL0f3DQyNcXnnFTPut3zJi4+23TQBaI/zdOTtr8vjBA2iqTLAXHoDd/hkJNKJM4cIeoSKcCnj+n4p4KcLPh1JknwtdGpnH6D4XrdRWs7dnovpop+HHbPO62/IkBJo/tHtOrPXA7WIhhdtzaL/8vGDACzGhqLCv8wU/VbqIEJunP2ZZl6hztRWIWbYGF0Lq6zj1vVGirSFlKve6qx1sH1TsKyJ9PzqBwubgoOlv/9d/3Yi8N94AePwY4OQEAGALr7wC8ORJ+YfpGPje97bw8cdGyGG+7t9eNvYdKTROlCO18fsEv7C6GHGm83njWqmHHV6I4HYhafuSXx4FO4/sL5fNF2oxgk73y4W+zb7TVyhTsS3Zn+h+Je++ZMuRCjmuhrqKMhQpO4gIjXjaPugZ0qd/l14ES5MyrV0boXYp/Lg8+32OaQzR+xhyFixV7GeglkhAV7rc6Dgdxcpdo1EwgvTmBuD118203/99I1S/+10z/8mT1EcwXZ49M8F77Nr0+BiaqD5tnMt5cWIWsIXlchEsZG12EltDUpzG2wBQsU0FPv9Sr22f3NuOkXA6D8U9QBNBl3zyFN4dJt1eKJKVh9ufpHm2GhLeGJfXBNDjmXvk3kcNPX+ERK1x/hDvDVvnCmPDFwEN7TVwLJQsnEj765IGW+GMz4+ptZkCQ5xTFfs9GXtJUsJ2TK6uVHF5HhldrYywv701fv3l0kT2l0uAt95yCzFll7Mzk3eHh0boHx8DwEcXTfc32H+j1Hn8i99yuddqEAogNwDl0ykuYYzbtfnJ8Yu0APZlfNcF/dos7vP6ut2gVRri/m3g8bi88LauQWnafQ18cSjVZnS9J6TaACktc7L31PABn9D9DylexiaUXFHoWBtPl+4XbQ1Wh6RWnRFTSLU1BObLxGIr2EmFCd4T1FxQsa84sd2Mrug+QCMoUOwDvPCWA8BnP2vE2uuvA/zkJ7j29m4ryi7f/e4WPvzQiP0HD5qvFB+stybUf31tX9liho+NZNPCAWdvz95gFIV/yH6kiLgvTSjmJS8/2mW4bcaFr9FrbKNYatFx1ZTQ2hHegDdkH2rlUfrgKxjlELhdumP0eb5z+cO7NKZMxdhFaU4/fxdLEf0/9rwNRcV+YqZy4bhuAinCwRszUaGyXpvhw4fm/2/8hikE/N7vmemPHg1xROPm5MTk53ptxD7+4PTUiH3Jq89V7wuvzN56CXt7xnJF/e5UkHMPPN28tA5tPIuVC9hQ9vq6mY72GtqbDkbK+0abacQ9RtzTWg7+AS+O7wu6ofSN3ivjoqSokKLRXSKqtXZGUdq3PXbbbhdi89fnFohpJJ06r0vcmyVqiVTsF4Y/gGuk78MMI/xHR+b/K68Yffq5zxkPelvsb0Gj+w1vv20i+p980vTlfnhoGj4vri7b3W0CNOqbq1VLuBz7aqdINh5cDjdt+xotCmZaCMDlqT+eila6LE2uz28fg1SLQRv5hiLlVwhqV5sXNVg+bLWvUyTUA56aqeZnLmztDqRxab0h0pfjnHbZbupCpIp9C3OupsNjlwoiIXnCo/sYxcUI/6c/bf5/+cvGx392BvDf/+1uXzpXHj82He1st8aff3Rk8uz4GNpdbVK1jKpcMmu/iO7v7+/fRdpvbnY/CMWj9nQXKODpOthlJEbX6ceecByh26YieLGAuy/rYjroENelaUHBTrdJyzu03EOHvIAiRfV54QPpIvj56eCnJsZKpdRPF8/+EMQ0jMTxlO/BHHmRy7YTs98xbj8lPosT9+jbxvnyQ3V/mlvr9W2HkAIV+4qI7+bCGzTEvw/QFl2HhwD37xvRf3FhbD0nJwDvvttsHbcyZ/793003mwBGiK7XJqK/Xr/o6ejsth2C9ynBViPdcO85j/RzwU3Fs1RAoIUCACPo9/ebggFafFBE8+WlxqZ83zRKT9MjFVRwnIt+XJeuJ+VBSH5J26TpkNJE9+36yJeijJUUjTFrhIrWuVl6JKZyXqeEiv2epLqoa2wo0kXwu3rn2d83Iub42Ay/8AXzf7MB+MxnAL75TYAf/CDb4YyOjz4ygfvj48arf3RkCkt7y22jpLHKhHbvwtU3nXd7C6u1+Xt9vds1Ju0yE3u4QQGOH36iEX2AptvLg4MmMo9DbDu8XDa96FCbDop9vh51J/GedLhAxmtrf79JL/5wHp1OCyW8sBAisHl0n68rFS5oAYvvQ5oeE/3Xxrl1k/v5HtrDiKsXFN+2a3s/1crcBb90HfKCkDQubcM2npOuPfb4eoVSG88I0IecPZLvi/BTUKzgx7aOj42Yfe010870N38T4MMPjcjFrbeZ9gP00SPjzz89NTUdtB95FLEtUYcKEhX3et10wYlIKvD2FhbQ9LfvsuxQUcuX4z5+XIZ+DRbXlT62RedjY2G6HG4boN1vPkDbcoP754IerUWSzYfXRtAsDUES+jZsol3Kcxxy0e/64i8f19qAcTBmQajifxfND6VmVOwnZIo3u1QaDxH80osMBdvhYSNeUKc+fGh+v/ZrAG++CfD97+c8qjp5911T2MEI+uEhwL17zRDzytj0F3CApSUMmV9dmSEV/DzETKP7q72W9x6FNvXxA+x+3RWgqUygu8DCCRXwOB0LBtfXzXB/v/0xrc1m94u6dHscKthR7GPUHqP5OI1G9Pf3m3SHRvRDuwGl+cELR7TgsVo1/+l0aR2+jxDHFk+TUhabr3xowR9rNfH1Wd6VKb4rJWJrUKaAdByxHv0xEtMFLF9OOm717CvVEnJxolDBLhnXaxPhv7w0Hv7XXzdR7UePTISb78Ew3geCxJtvmoj+yYlpwwAgCzrsix074Fke7cE+fsSAin78IZJCvL0FWO61ovMoMrnIp8vQggFdlnenif9tIhmTSgsSi0V7Pfo1XAku1DE9mGZq6UkV0cd8seFqA+Bq8OwS91JvRVIehKIFgLKU/KBPrI0npZVgKmJWSc+ca4q0682BCemLWBp3ba/Lxx5qvOBdnnxf37dSPtAIPwoxbHx6dGQ+uvXyywDvvQfwr/+a6aAq4vvfB3j61PxfLEwU/+CgKRQBGGF8dtYE7ZdLo+9XqwUcHt6D9fE92IMXJQGM8HMPP4vwL2ALq9Vip1EsF/u4f+rrl0QoFee0gIDRe4zqY1sA/M+j+XQccRUaaHokWw+N8AO4e+Xxfc02pBceXvDA4f7+rsWI5yM9BlprIR2z4qZklDCk682+kXH+LPb1RNP13SJFY2t9V02BKeRtl640a4/qS9rH132o1KuQbXmOevaVInS98FzrUKv50ZHRqA8fmuEbbxhxdHJivrL75El7qzx1Y+If/mELT5+aKP7JiTk2DMyj7R6HOB2FMOr4xaLxxV9dmeX39/fg4OCeifZTSw9FCCFLfeij755H9FGgS9F2buPBcfrRLB7N5zYeOgz5ki73qXOxjLYdSTxLNSe2cZ/It0Xopem0BoVbeOh/XJZvB8d9vSlpoaAcrir83F0I2gREVxHpalQpFTqmIFb70uf4p5B3NitP7YLeR0w3rz4bz9DnWcW+QEhvBnNEetB3WRegESLo/UbxiB/fWq8BPv95gFdfBfjgA4C/+zvjZ58Cb71lPpKFlpzFoulS8/h41x6Doh7zCofPnzfT12tTU2KGC9jfP4CDgwPYP9q2I/wALdVIRSaP6GOBg0bZuaAHaPvQUcSbWof2F3NvbprCAq144JF97tXnjXslpD7ypag/jiOkoqOF1F6ApkMS2lyk0+g9/Y95jdN5pJ/+pO5BKVIvQC7mVACoVVyk8OxLbaZ8fedLy8ZGYF3jLn8/Z07vUV4gmlvhiIteV8S7lvvV1p14KqQ8yc2sxX5IJk/5JuxLzAXqWhZFz9GRGceo8nptot9HR+YDXI8fA3z3u/LWG+p4WHC+/vUt3NyY43n33eZbWBh5vrlpR7Y3GzMdCwGSBx1FPzZ4vnevEf1okTo8XMBqtQer1d5OIQJuG0sNFbKSR1+K6KMgpQWA9brdreZd+wISyb++bnvz+c8m8n2NZKmQ5eKfzndF9H0in06TLDfcikPFPNqbpIg+LxBwe49UOyAdt2KQxHAtpHixS0IxxLYT21CyjxCpKc9rpG/XjGNBEvclu9asgRLnu7PYD/Gz13wCbRmdq/TWpfeDWh4EufaPApBbGrDN6dGRmf/woZl+cmJ663nvPYC337anVmaYa/HP/3wLFxemcfEHHzQR7DfflIXy8XEjfLE/erTdr9em4TKKRB5BRwG5XqOwRzuP+WgZFqBwHi0gULHI/fE0jQDtr+PSygHaNSieS27TwSEWKGhDXptH3zZ04Up/KK4aBXrcNnjknnv2scDGhb+v33/bfl1efi0AlGWod19s7WrXfbgi00o4cxW5UzjOXDoopremrm1IAXqI/djqvhqQCiBjeGDVlMbUhTguSrDLxAcPmq4ZX3rJzHv61Mz73OeMQHr8mHv5y/MXf2EKJ7e37Z510JrURNzb/dHj8OLCHDcKdy4aadeRVDTu7zdC/+DARPn39xuxf3Rkljs4aAtMqScX7plHqNcc0wsgi3su+qnAx0IN9/ZTcW/rdtMX7Xc16rWN031JAl+CR/VpntLzQgU9b6BLzy2vteFRfwA5qi9hE/paABgO1zO7xncjJbS7zdBa8dLBqpqYY2FpyDYrOSlh45E6R0FiNfekbDy11ybEUNODQKrqzZXPKGhQHN/cGOF7c2MKAKuV+QjX/j7Axx+bLjrfequxxdgZJj8vLkxEn3NyYo7j+LgZ0nYKt7eNR35/v4nsc/sOLk8bntKIMffvX1yY+RcXjehH6w+3jkjw73OhgOdRf/6lXYAmbdyTj+uiyMcv6IZE9n3eeurBp9YhaR1bJN9Wo0ALNHQaFeF4HniEn3r4aYHAZd/h26f7jfXqK/WQ4vnZRUDb7BQp2hDY9mfbbk3vt6GY4zFL5LrexswQ18akxL70QPF1T1Yrc/HzIdQKgoJqvTZRcvTvP39uROrpqRHLJycmyv8bv2FE/w9/2HRhWSMXF03EH8U8NmTF4cFBE9nnkWHJ201FP0aP6Y9H9tHegzULWJNC9wEgi0cp8o7QggBAO5KP55W3A6DL8O2GCHA6jYp86SNg0jZoJJ8fE/3PxT1G9alYx1oTyV7FPfu0doVH+HmNARf6UkPjEKGvhYFypBQ0qXp56erZd43P6X0Vgy0yO0c0D7pTxMYTcyMPfXLH1nbAha2RWamH6ZD7RXFyeNgIL2zIip7+4+Mmiv3woYn4/+pXphGvP9JfBt5glYr39dosg41hadSeWzyoIMToPRWOWCuAhQi0B11eynYiGoHmXVVKuPz0LhsJFeN0Gq0NoPmD+YHRf0nES3D7D00vLUi4CjB0XX5MPPLOG+TSQgC3W/EIPv1JDX6RkEa50nQV+sMzhndO32f5EO3exo7rOpiTnWcM98MY4IV0W6FdopPYz3XiUrX8D+mVYGi6HFttD1NbFCfl9pfLxU6En9p6Dg6MYP3Up0wU/+FDgM98xkT5v/hFE/X/4hebr/CenAC8/36yJCYBPe0ATXR7uWx64ZGiwdwfjusCtKPBtDYAu/RcLIyoxxoDHtnnPf5gI17+MSpeAKD/bVFxHrlHUOj6Gr7e3ja1BovFruCX9sVtQ9hI2taPP8Bu7zu0Nx8qxnnjaG6hsg19EX0pr+kwJKKvor4ebM/oWkSPLSLYtatNaZxTOlhVG3PKA9+x1nJf1ECIfu3SRrYaGw+32cSc/DndNHOC+68x8o0iC6Cx+GB3k2dnZtmnT03kHy0/T57U2U+/1PUk/dEPU6Hgw/zAiDf+5xaTxaKZjsty2xD2+INefykKjfnu+vKsdFz8v82aY8sXRFpPEvmS0KfTbV59um0btki8rdFt14g+7osOfQ1yfelWlFpQUacoZchu45EIKdnHRMLHEimIje7bGjXVcLw5Htq01mC5NP+5hQIFKUapHzxoPPBPnxqx/5nPGH//xx+beV/6khl+/DHA+TnARx814v/p0/yWHymSyz+QRT8uJlloaISbIn3oSfqwFI8go1cfvf1U6NOINApa7lGnBQvclzTkuGoAANxefdt/m8CXIvu8m08KT7N0zPR80V52aB7S4WJhekbi+WiL6Eu1NpgWWwErxtIzV4Z8Zuas+c69rRjPvrRs1/1OhS7v96nniYQW+rrT9XqpJrIvMWa/vY0UxzSnh4NPsNDIKEAjkrBB6sVF0xD2pZfM+MsvG3vPpz9thqenjejHRrS27ielfa9W7WnSfMnPLYlIqSGuJPhsXm6AJopP005tQ/glWyw8XF+3e/+5umo3OOVp433+02PtEo2WIv2unnkk3z2fzyP6rm3za4w3kuWReN4+Qoru83PJff22iL50Xl396StKamK63rRZDOb0jlKUMVDMs5/ygTC2B0uo4K/xuIYogEnb5yIaI/4YUT08ND3NbDYAr75qRODJiRGup6dN5B/HLy7M7+zMDD/5xAyfPzfi9/zcbAsb1NIIsc2HLkXkJfHPI+6SuHT5s/kHsTghohm3GeL3p3YeSay6eo+RxL/Utz+AvdbCdjzSsdksP7wGAPdH85nmOy+g8Yg+H1JPPo30S/kmNYSWCkv8GnD9p2hhYJchn6W1+/UB7GmJTaOtYBDq3587Q+RDzEebcPnc6Qr18NcQ8B1D4TWbZ19qASz14esarz3zaqDWfCp5A6LQx/93aVq0RejtrfHqY4NX2tXl0VETxcffgwdm2tlZM7y9NfMwEs4Fv80S4hJbXMRxcefza/usPDZfusuTzr3rtPcb6vvfbORCARf70rGEiFQKz1Nbg1wp/bblAZooPYWnmR4jb6xMhbutO03e1z7NG1ue8Wu5q9BXZEo+S1M8K1Onndtxhnie1yDcctDn2hpKWPv0mauDk7kzhvzI1huPtPGY3nBiMm+qD4hQpF4T5tD1pgsujnhXjfiFVrSYoEC/uDD/z8/NMptNM803xAg/9phDxT71gmN6KCH+cN90aXtU7HI7C5/GCyZcKGOBBsAITfxI1nLZfAkYI9FSTQR+1RdFKo5zGxI9RlctiHS8sfN826a1K9xmhePosw/pYcfXDsNVC5Iin7QA4Cbn8yvnOypXmqXtxr5vY/vZT3ksfQKOqfY7lj70a+ytUMpHaZlS1KB1UlK1Zx/J0c3j2Kil5F2L4OdQobO/34hAqavG1aoR7YeHbTGPnv3zcyN+cfrVVVvoS7YebvFxReBjkAoPIaJeGqdtESTbS0g6lsu273+5bGoBaL4DtKPU/Cu3IdYeiVBRy5fDbk5xSJejXWjSCDy2Wzg4MMvgl4e56Ke+fVrzIUXzpXYOKvTzIInRXPtBxvKekp7lKW08uYNTfQKOc8ZVSBoS1/mqwcZTq9bpSmexX8LGM6Uof0gBpvSFZit513YOuDBCIYuiCsf393eFL0blqU2Hinr8f33dRPFxHk6zCX5JSNvsJZI9RfovffVV2i8eD9ZycBsSny7VTEhRcz6Ni3qsEcBChatxMZ2H6fGJW7QVIbSQwLdvs0jx46BRfS76XZF9KvrpsrZG2Dn8+b55yvDvjdSe/RyiY4iIdBcP/9QEVil8UfOQftolu/ZQ+IKbIbUCKZjS9Vi9jWfOSBaeIeHntDaR70ISjbQwcHPTRPipULaJYy6iaZScroPbtzUgpcsBxPdEQ9Mf2t0kTSvvhpKLfV4jwNMWmue+SLXUW49N6NtqACRxb/sYFsDuB6toTYXtY1e2Brr8h1Ymui795gHfvyua7/rvyi+lPDmekbmi4kMJJr5P/G9bJpSSEera34XSu1sKvvq6Va1Fqw19rQLUc+ypGIWNZ8qEiughqkWngk8cUSsHCjBJPAO0bS8+ge3qDtI1dPUtz4cuC4/UeFgS/XxoK8Tg0Ob3D0Gyo/jsOzGFBil6b/PD03YEHNqLjtR9ps2PTz+YFerNp+l15ZMtra7pyi5DRa9zMJT1iE7reky5PftdI9R8/z6B6xqvXeTbsAVjeUGgFl0hFQp9uifUSTJX/TRKsT81D7/vIVKqZqREaToH3L5BhRWN7uJ8gEaAr9e7Ylv6j+u4vvLqGvosP7b/tgKIFMl3iXteWJBqLmyFGJr+EFz9xkv/XR8L4yKaR9TpfN54mGKz8fBuMqVuNOn2fd2QqsgfnpwizfUsrv2ZmcKzb1s3ZDu+xr22WoC+9uC5OQ9Cg4mUHA2bY0jVHsNmVwu1lU2J0Yn9KZ4EAPmFVMOx1v7CikWycOCQFwTocgBxEXgkVtRL01zbcNUy0Mg+tSlJEX9eAKAFA2k/PG2SNcmH1K0oIol+Lpxx6Ivqc9GP4zSvbY1s+TpSJJ/WHLhsQ+rNz09NwYkUQjpXFLKGXmRCGvdK45ShOq6o4V0cQ9/rpob7pySlLdQ5GJ3YnxOlxX+NLeRT4RNOktjnvbjw+S7BKwn52P+2D0bR/9xShEMe6ZcKA1zcczuPFOEPKfBQeA2Az8ZDx22iP8TCw73zOI8W+ly+e6ng4Nov3SY9ThX56SndY0euNAwpXkt59m3554rE9uksQrLx4HZcaRrb+y6VZ39sxw0wXAFwbKjYV6KY8s3Do718mmsdWitA10d4DzJ9CgOhkX5chhYA0JbELTw2ke+yCtHtYzqkGgvbuITLzmLr2UeKplOPvhTZt+2LN9Cl2/WJfJoenl66v1DLjm/eXJlCsGEOhDTItS2fi9BChDQ+Zqb83lbCGLXYn9pDv7YbMqRB1FRxeaYl0cqX58usVvZ5tmkhX8ql/6UhF+K2mgAu6gF2I/vSMjbhz9Moefptdh+ppx5fA10uukOj/RzuxZd695H254vih4z7pivdyfVsraWLwC4MKax9QjplWlKf69rOWyy29McUfKaEq7OTKTfeHbXYV4ZhaoWqvkhijIt1m2DjNQB0eds2pFoCFJT40TC6nDTky0hf3eXiH/fBawP4srg9TI+UBnr8XZCi4jaBbRPlvL99+h0Gvjz17PP98+0DhNt1pPHQeUocU31pK26mLNhKo3k7XkYv9qd64cX0zpOLEN+fYggVabaaAds2pMKB1HYAl0Vs7Qdc0X/JHiR1uelbh++Tp62v4Of/XXYZn8/ftg9u9fGJe2kZW9pj5indGUKYhHj2uzwrh3jGS779qQR2pnAMqej67p7KtcDp2rPP2Bm92J8qthut9MWoJfv+hNQM2Ja1tSXghQJeqEARju0FcD5djg+l2gCAbr0RuY7R122nrxGvy9pDhzZLjrRtKXrPt+FKi23cdRxKWkp0aDDWTgzG2hBVyYdeC9NCxX7FlBbVKSNVipu+os8n/n09CQH4o/IxYt7VQBkJ7aKT4/L0A7gLArZpFMkeJG07ZFzatpKfErWffNyXhtLP0dL7z02Ka2AKeaQFuXCmHMxUsa9Y4Q1XlPL4GgJLy4X2LMSRagP4Pn0FCBz3NVCWsLVvoMSIb58NaLttFyRCG9rapsXMV7pR+4u5Zuvj1J/vfftKn2KeKG5qf570QcV+xYR+hXAISvTLrPjxiUiXYOYFgi5tAwDio/ZdPfs+pOP01QLQ9aQ8UGFfnpotMSH9xnfpC35opv58n9rxxJDq4270f+xXi+eAq2efGvJJxX7FlL44OHN+YI6VLjUBdLkQ4WrrNYjCvzPgwlVQkMS7hG0frp6Q+njvQ5dR4qn5uSM9o3Ha2Do1qD19KQl5t04xP8b04bex0cfSNwQq9hUrU3zYzZ0QQeqzz9jEfWx3o0MREonnDZpV3CtdqP2FT5n6871vfg9hcxrimlCBH04NEfhcqNhXrLgu+qm/KOaMS7SHLMfXqUUU97Xf1HIcyi61Wn2kyL5rfEihYdtXjfnYBdtx1CTmhjjnqQsttdlTUtLnWGr/QJeK/RFR08Nr6h5PZZdQcR+7TmliayqUYalVyEtI4l4SAdJ6tTD1Zzs/npoEWW5S3ku2L/DOKT9t+PKmRMFAxf6IKH0TTe2hr6Sni0DO0WC3SzpU3NdJzEeAStP1gz01pB1gus/4VJaelPlT4pynTn/fHo+mii9vSnzMS8W+4mWqLwClDlRkK0o5pvx8Ty2iUon+sQt9ZGoCP2WEvba8UbGveJlrzwWKotTJmKw9lBr9zjFpGNsXgnNZdvqK/hLnPqU9S8rX0m1QUpAqvbW1zwEA0JjaiKnpYbuA7d1PUeYKvQ/0flB4/+Shy9bMmK/pseTx2LDlq+v613PRMEReaGR/xEyhKnCoY6jhwVLqJTmmvr5LMJYopTI+Yjz8NYjo0I+E1ZDWEIbs6Qagrg9husjV9qA2n3op1LOvzJ5SN3gpQVfDA80lOMZWLZ+LFMdfw7lWlD7oNTwPcjzv5/z+GAMq9kdMie6bxvwyGCq6PaYouhYEGnId55iuB0Wh+GombN8RqOVaH7qbzSE+xFUbPntOaHeTNbZn6UqNXW+qZ3/EzKlaLCWSr5r7q6Vxvg1pWdc6Y8N2LGM/LkVR5kfJBrHafsePvmPyMiuxP5eS9pCgTKbjY8Yl+nmhwFcgmAK2Qk7tx901TTmPpcZ8Gitjf86EwJ+rtnHXPL4d33jMuiGNMktTuqH8HN4RnNDgVw15EXNv+Nal9Ake5mJWYr+Gi2tqzPFhptSH9IKxFcr4esr4mMN5C23UJ02PERYxosS1nZB1lOnDC6J0em3E3Bu+dWtHPfuFyeFz5F5KRUlNrQ9xn5AJ7S2D96Tgi+LEpK+m/AJop6nG9Cl19OYRs99arqOhPfvKLvxdwZ83dJmSnvaU9Dm+XHmhYr8wuaLiY7oxlPFRq0AMeYiGbMM17tvn2Ki14OZiLOlMRUzjRqVB88RP7ue3tH0poMLnu8ZrJ/Z4utbExdDJxtPVO1iamtKiKGNDesj4LARDIVUdS1XJ0nqp9q8Mg5TXtb1rUhDqJ5aGqXz4XT3NUzsXSj9qbeuVsi1MqeMJvdc6RfZjvIM1UUN6UjwEazgOZX5IkfzYKPgQ2J5HrmX5NFs1bOg2lOlQKmruE0b0OqdpHDKqWLuNR/GT+zxJNh7+vxQlIux9KWbjKX2yaifHjRSzTT0/Sh9qeCCnItZb7xofU770SWftgs12HvqcH9+6LsEy1HVRi/AISUNJ4V9L9FVpYwsSlb6v+pDKh+9btiudxD5vZIEJi22UMGVye+AURZGJvfckIc/n26wj0vq1Ubtg70vJ48spunOJhVzvYdv9UNP5UeqgVs9+n/sqVS1BzLHH3FudI/sxvj++3FTFf46XP6+iVZShmLpIRHzHGTJ/6PtTCq5w5nL+5ooUdPMt65sXW1DQd9P4KVXzUoMNxhXECRkf0/XfWezHPAz4OrbxMWB7MIYWdrrsL8U2x5jXYyc2SsDHU0XuQvdru1dr9OWnYMz3hO154/N5991X7YT4g333Hl2ePn8lUZQziu47lzitb5S0j085x/WWErXxyOTSK7H7LnE+ar9mOakKFNk/qjWGzOzLGF6GW1jc/ZTu+Er5tvl98j00ctd13ZTr1cyidRdM/7mUirHnVcw9OJbrfgqBM2V4xnJ9z5lcOmKx3cY9JBaLxSUA/LLzHhVFURRFURRFSclr2+32QJoRLfYVRVEURVEURRkH2W08iqIoiqIoiqKUQcW+oiiKoiiKokwUFfuKoiiKoiiKMlFU7CuKoiiKoijKRFGxryiKoiiKoigTRcW+oiiKoiiKokwUFfuKoiiKoiiKMlFU7CuKoiiKoijKRFGxryiKoiiKoigTRcW+oiiKoiiKokyU/w9Y16vDh9JAtQAAAABJRU5ErkJggg==", + "image/png": "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", "text/plain": [ "
" ] }, - "metadata": { - "needs_background": "light" - }, + "metadata": {}, "output_type": "display_data" }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Vorticity plot at timestep: 700\n", - "\n" - ] - }, { "data": { - "image/png": "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", + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAvgAAADpCAYAAABRN0P0AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/bCgiHAAAACXBIWXMAAAxOAAAMTgF/d4wjAABu+klEQVR4nO2de5BlV1X/1+3H7Z7unskk83CYDElMAsHwUkL0F0kgFiFQChYgFJQRibwTYiGiPAQJZYHiz6QMJSgIEiU8qn5gWcUPhRiU+hGFCPwiYvIjwCSEJCTDZB49PT093bcf5/fHnnXvuqvX3mfv89zn3PWpunXuee/z/u51vnudTpIkCSiKoiiKoiiK0grG6i6AoiiKoiiKoijFoQJfURRFURRFUVqECnxFURRFURRFaREq8BVFURRFURSlRajAVxRFURRFUZQWoQJfURRFURRFUVqECnxFURRFURRFaREq8BVFaR2f+tSn4IILLvCa9vbbb4e5uTlYX18vuVTDvOc974FLL7200nXm5Q1veAO85jWvqbsYiqIoSgoq8BVFKZ0XvehF8MIXvlAc9/a3vx0uvPDCzMu+/PLL4V3vetfQsKuuugq+973vec1/2WWXweLiIoyPjwNAvMK7qHJdffXV8Bu/8Rup03U6Hfjyl788NOzDH/4wfOxjH8tdhqz4lj0vn/rUp2Bubm7oNzExAU996lP70yRJAtdffz3s3bsXZmdn4ZnPfCbcddddQ8v5zne+A8985jNhdnYW9u7dC+95z3tAvy2pKEoVqMBXFKV0rrnmGvjCF74ADz300NDwXq8HH//4x+Gaa64JXmav1yuqeIoyxFVXXQWLi4v939GjR2Hnzp3wile8oj/NDTfcAB//+Mfh1ltvhUOHDsEznvEMeO5znwuLi4sAAHD8+HF47nOfC894xjPg0KFDcOutt8LHPvYxuOmmm2raKkVRRopEURSlZDY2NpLHPe5xybvf/e6h4Z/61KeS2dnZ5NixY8nJkyeTt771rck555yTbN++Pbn00kuTO+64oz/tzTffnJx55pnJBz/4weTss89O5ubmkte//vXJ2NhYMjk5mczOziazs7ND0yKrq6vJjTfemPzMz/xMMjc3l5x55pnJ+9///iRJkuQrX/lKAgDJ6upq8slPfjKZnJxMxsbG+sv76le/mlx66aXJe97znqGyf/azn012796drKysiNsMAMmNN96YPP3pT09mZ2eTiy++OPnmN7/ZH3/99dcnz3jGM/r9R48eTV772tcmZ555ZrJjx47kec97XnLPPfckSZJYyyXxh3/4h8njHve4ZG5uLtm3b19y3XXXJSdOnEiSJEne9773JRMTE8nExER/OT/60Y82LePCCy9MACCZnp5OZmdnk+c973lJkiTJK1/5yuSqq67qT3f22Wcn119/fXLllVcms7Ozyfnnn5/8y7/8S/KVr3wlefKTn5zMzc0lz372s5NHHnmkP8/JkyeTd7zjHcm5556bbN++PbnsssuSO++8sz/+X//1X5OLLroo2bZtW3LGGWckv/iLv5gcOXLEWfY77rgjedaznpWcccYZyVlnnZW8613vSlZXV72PRRqf+cxnki1btiSHDx/uDzvnnHOSm266qd+/urqa7Ny5M/nEJz6RJEmS/O3f/m2ya9euoXLcdNNNybnnnuu9XkVRlKyowFcUpRJuvPHGZO/evUOC57LLLkte97rXJUmSJNddd13ypCc9KfnBD36QrKysJDfccEMyNzeXPPjgg0mSGNE+Pj6evPa1r02OHz/eF63Petazkne+851D6+IC/53vfGdy3nnnJf/xH/+RrK+vJ4cPH06+9rWvJUkyLPCTZLPwThJTEXnsYx+brK+v94ddccUVydve9jbr9gJAct555yV33313sry8nFx//fXJzp07k/n5eXE9z3/+85PLL788eeSRR5ITJ04kb3rTm5J9+/Ylx48ft5ZL4hOf+ETyox/9KNnY2Ejuuuuu5Lzzzkve/va398dzke4q/2233TY0TBL4Z511VvLtb387WVtbS373d3832bNnT/KiF70oOXjwYLKwsJBccskl/WOMy3j2s5+dPPjgg8nq6mryF3/xF8muXbuSo0ePJkmSJHv37k0+/vGPJxsbG8nKykryta99LVlcXLSW/Z577klmZ2eTz3zmM8nq6mpy//33J095ylOS9773vUPb4joWaVx22WXJ1Vdf3e+fn59PAKB/DiHPec5zkje/+c1JkiTJ7/zO7yRXXnnl0Ph///d/TwAgOXbsmNd6FUVRsqIWHUVRKuG3fuu34OjRo/D5z38eAADuvvtuuP322+Haa6+FjY0N+Ju/+Rt473vfC+effz50u114y1veAueeey588pOfHFrOTTfdBHNzczAzM+O13iRJ4AMf+AC8//3vh5//+Z+HsbExOOOMM+CSSy7xLvtLXvISWF5ehi9+8YsAAHDvvffCV77yFXjd617nnO9Nb3oTXHjhhTA1NQXvfve7YWJior/9lEceeQS+8IUvwE033QR79uyBmZkZ+LM/+zM4efIkfOELX/AuJwDAK17xCjjrrLOg0+nAE5/4RHjjG98I//zP/xy0jBBe85rXwFOf+lQYHx+H3/zN34QDBw7A7//+78OuXbtg69at8Gu/9mvwjW98AwAADh8+DH/3d38HH/rQh2Dfvn0wMTEB1113HZx22mn97ex2u3DvvffCww8/DN1uFy655BKYnZ21rv9DH/oQvOAFL4CXv/zlMDExAWeffTa89a1vhZtvvnloOt9jwbnrrrv65ymysLAAAADbt28fmvb000/vj1tYWBDH0/kVRVHKQgW+oiiVcPrpp8PLXvYy+PCHPwwApsHmJZdcAk996lPh0KFDcPLkSTjvvPOG5jn//PPhgQce6Pfv3r3bW9gjhw4dgsXFRe+sOhLdbhde/epXw0c+8hEAAPjrv/5rePaznw3nnnuuc76f/umf7v8fGxuDs88+Gx588MFN0+Ewuv2Tk5Nw9tlnD22/Dx/5yEfgaU97GuzYsQNOO+00eOc73wkHDx4MWkYIj3nMY/r/UYjzYcePHwcAgP379wMAwC/8wi/A9u3b+78f//jH/fYZn//85+G+++6Diy66CM4//3y4/vrrYW1tzbr+H/zgB/AP//APQ8u75ppr4MCBA0PT+R4Lzl/+5V/C05/+dLj44ov7w7Zt2wYAAPPz80PTHj16tD9u27Zt4ng6v6IoSlmowFcUpTKuvfZa+PKXvwz/9V//Bbfccks/Krpz506Ynp6Ge++9d2j6e++9F84666x+/9jY5luWNIyyc+dOmJubg+9///teZbQt7/Wvfz3ceuutcN9998HNN98Mb3jDG1KXdf/99/f/b2xswAMPPAD79u3bNN1jH/tYAICh7V9bW4MHHnigv/1p2wkA8PWvfx2uu+46uPHGG+HAgQNw7NgxeN/73jeUucVnOQAmi07R7NmzBwBMdpn5+fn+b2lpCd7+9rcDAMCTn/xk+PSnPw0HDhyAz33uc/DhD3+4H42Xyr5nzx749V//9aHlLSws9Bu7Ir7HgnL8+HH45Cc/ORS9BwA47bTT4JxzzoFvfvOb/WFra2vw7W9/G37u534OAAB+9md/Fv7zP/9zqHLyrW99C84991wV+IqilI4KfEVRKuPiiy+Giy66CF784hdDt9uFl770pQBghNurXvUqePe73w333Xcf9Ho9+PM//3PYv38/XHXVVc5l7tmzxyneO50O/PZv/za84x3vgG9961uQJAkcOXIEvv71r1uX98ADD8Dy8vLQ8HPOOQee85znwEtf+lKYnJyEF7zgBanb+4EPfAC++93vQq/Xg/e9733Q6/XgV3/1VzdN95jHPAZ++Zd/Gd7ylrfAT37yEzh58iS87W1vg263C7/yK7/iLBfl2LFjMD4+Drt27YLJyUm488474YMf/OCm7bv33ntT8/7v2bPHO9WoL2effTa88IUvhDe+8Y3wox/9CACMiP7iF78IjzzyCPR6Pbj55pvh0UcfBQAjpMfHx2FiYsJa9muvvRY+97nPwWc/+1no9Xqwvr4O+/fvhy996UtD6/Y9FpRbbrkFJicn4eUvf/mmcddeey3ccMMNcNddd8HJkyfh+uuvh8nJSXjRi14EAAAvfvGLYXx8HK6//no4efIk3HXXXXDDDTfAG9/4xuw7UFEUxRMV+IqiVMq1114L9913H7zqVa+Cqamp/vAbbrgBrrzySvilX/ol2L17N/z93/893Hbbbf3oto23vOUt8L3vfQ9OP/30TZ5n5I/+6I/gNa95DVx11VWwdetWeMpTngK33367OO3LXvYyuOCCC2Dv3r2wfft2+Ld/+7f+uGuuuQbuvPNOePWrX90XnS6uueYaeMUrXgFnnHEGfP7zn4d/+qd/spbxlltugXPOOQee9rSnwb59++Duu++GL3/5y7B169bUciFXXnklvOENb4DLL78cTjvtNPiDP/gDeOUrXzk0DbYb2LlzJ2zfvt1qAfqTP/kT+NM//VPYvn07PP/5z0/dVl8+/elPw0UXXQTPec5zYOvWrXDBBRfARz/60f5bhs997nPwxCc+EWZnZ+FZz3oWXH311f1tkMp+8cUXw2233QYf/ehH4cwzz4QdO3bAS17ykn4FAgk5Fshf/dVfwdVXXw1btmzZNO73fu/34Oqrr4YrrrgCduzYAbfffjt86Utfgrm5OQAA2Lp1K9x6663w1a9+FXbs2AFXXHEFvOpVr4I3v/nNeXehoihKKp0k0a9uKIqi+PDd734XnvSkJ8EPf/jD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", "text/plain": [ "
" ] }, - "metadata": { - "needs_background": "light" - }, + "metadata": {}, "output_type": "display_data" }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Vorticity plot at timestep: 1400\n", - "\n" - ] - }, { "data": { - "image/png": "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", + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAvgAAADpCAYAAABRN0P0AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/bCgiHAAAACXBIWXMAAAxOAAAMTgF/d4wjAABqz0lEQVR4nO2de7BlR1X/1z33MTP33sxMJpP8BjLJQB7yM/KIBiLhUVACQhGgCA+1jC/kncTCAoVQQII/QaE0JShUEtQEEbRKqNKiYqGCjuVYECBQEQnEvMjMhGScTCY3M5M7992/P3b6nj7rrtW9unfvsx9nfapOnb179+7d+3HO/vbq1avHjDEGFEVRFEVRFEXpBL26K6AoiqIoiqIoSj5U4CuKoiiKoihKh1CBryiKoiiKoigdQgW+oiiKoiiKonQIFfiKoiiKoiiK0iFU4CuKoiiKoihKh1CBryiKoiiKoigdQgW+oiit5gtf+AI87WlPE+Xdt28fzM7OwurqasW1GuTDH/4wvOAFLxjqMcvyjne8A97ylrfUXQ1FURQlARX4iqJk5bLLLoPXvva15Larr74aLrjgguSyX/ziF8MHP/jBgbTLL78c/ud//ke0/wtf+EI4ceIEjI+PA0BzhXeuev3Gb/wG/Mqv/Eow39jYGHzta18bSLvhhhvgL/7iL0rXIRVp3cty8uRJeOMb3wjnn38+9Hq9Dc8X5rLLLiOv17//+7/Dz/zMz8D09DQ89alPheuvv35g++LiIlx55ZWwc+dOOOWUU+BVr3oVHDx4MPv5KIqiAKjAVxQlM+985zvhlltugQceeGAgfWlpCW666SZ45zvfGV3m0tJSruopygBjY2PwvOc9Dz7zmc/AxRdf7M37uc99Dubn5zek79+/Hy699FJ485vfDHNzc/DZz34Wrr76avj7v//79Tzvfve7Yd++ffCd73wHfvzjH8OOHTvgNa95DaytrWU/J0VRFDCKoigZWVtbM+eff7655pprBtK/8IUvmJmZGfPYY4+ZkydPmve+973mKU95itm+fbt5wQteYG699db1vDfffLM588wzzac+9SmzZ88eMzs7a97+9rebXq9nJicnzczMjJmZmRnIa1leXjbXXXed+cmf/EkzOztrzjzzTPOxj33MGGPM3r17DQCY5eVl8/nPf95MTk6aXq+3Xt5//Md/mBe84AXmwx/+8EDdv/jFL5ozzjjDLC4ukucMAOa6664zz372s83MzIx5znOeY7797W+vb7/22mvN85///PX1Rx991Lz1rW81Z555pjnttNPMK17xCnPnnXcaYwxbL4oPfehD5vzzzzezs7Nm9+7d5qqrrjKPP/64McaYj370o2ZiYsJMTEysl7N///4NZVxwwQUGAMzmzZvNzMyMecUrXmGMMebXf/3XzeWXX76eb8+ePebaa681P//zP29mZmbMeeedZ/71X//V7N271zzjGc8ws7Oz5iUveYl56KGH1vc5efKkef/732/OOeccs337dvPCF77QfPe7313f/m//9m/moosuMlu3bjU7duwwz3ve88zRo0e9db/11lvNi170IrNjxw5z9tlnmw9+8INmeXlZfC98vOhFLzIf+MAHyG0HDx40Z511ltm/f78BAPPVr351fduHP/xhc+GFFw7k/+3f/m3zcz/3c+vXYcuWLeYf/uEf1rc//PDDZmJigr23iqIoZVCBryhKdq677jrz5Cc/eUB4vfCFLzRve9vbjDHGXHXVVebpT3+6ufvuu83i4qL54z/+YzM7O2sOHjxojClE+/j4uHnrW99qjh8/vi5aKQGGBf4HPvABc+6555pvfvObZnV11TzyyCPm61//ujFmUOAbs1F4G1M0RM466yyzurq6nvbSl77UvO9972PPFwDMueeea+644w6zsLBgrr32WrNz504zNzdHHudVr3qVefGLX2weeugh8/jjj5t3vetdZvfu3eb48eNsvSg+97nPmf3795u1tTXz/e9/35x77rnm6quvXt+ORbqv/q5gpfbds2ePOfvss83tt99uVlZWzLvf/W6za9cuc9lll5nDhw+bY8eOmUsuuWT9HtsyXvKSl5iDBw+a5eVl82d/9mfm9NNPN48++qgxxpgnP/nJ5qabbjJra2tmcXHRfP3rXzcnTpxg637nnXeamZkZ87d/+7dmeXnZ3H///eaZz3ym+chHPjJwLr574YMT+Gtra+ZlL3uZufHGG8nr9drXvnbgvI0pnqNTTz3VGGPM7bffbgDAPPjggwN5zj//fPPJT34yWC9FUZRY1EVHUZTsvOlNb4JHH30UvvzlLwMAwB133AH79u2DK664AtbW1uAv//Iv4SMf+Qicd955MDU1Be95z3vgnHPOgc9//vMD5XziE5+A2dlZmJ6eFh3XGAOf/OQn4WMf+xhcfPHF0Ov1YMeOHXDJJZeI6/6GN7wBFhYW4Ctf+QoAANx7772wd+9eeNvb3ubd713vehdccMEFsGnTJrjmmmtgYmJi/fxdHnroIbjlllvgE5/4BOzatQump6fhj/7oj+DkyZNwyy23iOsJAPCrv/qrcPbZZ8PY2Bj81E/9FFx55ZXwL//yL1FlxPCWt7wFnvWsZ8H4+Dj82q/9Ghw6dAh+93d/F04//XQ45ZRT4PWvfz1861vfAgCARx55BP7qr/4KPv3pT8Pu3bthYmICrrrqKti2bdv6eU5NTcG9994LDz74IExNTcEll1wCMzMz7PE//elPw6tf/Wr4pV/6JZiYmIA9e/bAe9/7Xrj55psH8knvhZTrr78ejDHsM3Ds2DHYvn37QNqpp54Kx44dW98OAN48iqIoOZmouwKKonSPU089FX7xF38RbrjhBnjd614HN9xwA1xyySXwrGc9Cw4fPgwnT56Ec889d2Cf8847Dw4cOLC+fsYZZ4iFveXIkSNw4sQJcVQdiqmpKXjzm98MN954I1x66aXwmc98Bl7ykpfAOeec493vqU996vpyr9eDPXv2kIMobZp7/pOTk7Bnz56B85dw4403wo033gj79++HlZUVWF5ehtNOOy2qjBie9KQnrS9bIY7Tjh8/DgAA99xzDwAA/OzP/uxAGUtLS+vjM7785S/DH/7hH8JFF10Es7OzcPnll8OHPvQhmJigX01333037N27d0Aor62tbfBjl94LCffeey/8/u//Ptx6661snq1bt8Lc3NxA2qOPPgpbt25d3w4AMDc3B1u2bCHzKIqi5EQt+IqiVMIVV1wBX/va1+C//uu/4K//+q/hiiuuAACAnTt3wubNm+Hee+8dyH/vvffC2Wefvb7e6238e6LSXHbu3Amzs7Nw1113ierIlff2t78d/vmf/xnuu+8+uPnmm+Ed73hHsKz7779/fXltbQ0OHDgAu3fv3pDvrLPOAgAYOP+VlRU4cODA+vmHzhMA4Bvf+AZcddVVcN1118GhQ4fgscceg49+9KNgjAmeH2ZsbEyUL4Zdu3YBAMD3vvc9mJubW//Mz8/D1VdfDQAAz3jGM+Bv/uZv4NChQ/ClL30JbrjhhnVrPFX3Xbt2wS//8i8PlHfs2DE4ceLEQD7pvZCwb98+eOSRR+Ciiy6CnTt3ws6dOwEA4PWvf/26Rf/CCy+Eb3/72wP73XbbbfDTP/3TAADwtKc9DbZs2TKQ58iRI3D//fev51EURcmJCnxFUSrhOc95Dlx00UXwute9DqampuCNb3wjABTC7Td/8zfhmmuugfvuuw+WlpbgT/7kT+Cee+6Byy+/3Fvmrl27vOJ9bGwMfuu3fgve//73w2233QbGGDh69Ch84xvfYMs7cOAALCwsDKQ/5SlPgZe97GXwxje+ESYnJ+HVr3518Hw/+clPwg9/+ENYWlqCj370o7C0tASvec1rNuR70pOeBK985SvhPe95D/zv//4vnDx5Et73vvfB1NQUXHrppd56uTz22GMwPj4Op59+OkxOTsJ3v/td+NSnPrXh/O69995g3P9du3aJQ41K2bNnD7z2ta+FK6+8Evbv3w8AAMePH4evfOUr8NBDD8HS0hLcfPPN8PDDDwMAwLZt22B8fHzdek/V/YorroAvfelL8MUvfhGWlpZgdXUV7rnnHvinf/qngWNL74VlcXERFhYWYG1tDVZXV2FhYWE9ctMv/MIvwH333Qe33377+geg6D352Mc+BgBFSM8777wTrr/+elhaWoJ9+/bBTTfdBFdeeSUAAGzevBne9KY3wTXXXAMHDhyA48ePw3ve8x644IIL4PnPf36Gq60oioKodwiAoihd5qabbjIAsGGA6vz8vPmd3/kdc/bZZ5tt27aZ5z//+esDYY3ZOHDWctttt5lnPvOZZvv27Wbbtm1k3uXlZfPxj3/c/MRP/ISZmZkxZ555pvn4xz9ujNk4yHZubs689KUvNaeeeqrZtm2b2bdv33o5t9xyiwEA86EPfSh4nvBE5JaLLrrIzMzMmGc/+9nmm9/85vp2PGj2kUceMW9+85vNk5/8ZLNjxw7z8pe/3PzgBz9Y3+6rl2V1ddW8613vMqeddprZunWrefnLX25+7/d+b+Ba/OhHPzLPfe5z168XFUXHGGM++9nPmrPOOsts27bNXHrppcYYepDtn//5n6+v33333QYAzI9+9KP1tOuvv96ce+656+vz8/Pm2muvXY/086QnPclcdtll5sc//rFZXFw0r3zlK83pp59upqenzVlnnWXe//73rw9u5up+6623mpe97GVm586dZtu2beZZz3qWueGGG8T3gmLPnj0GAAY+L3rRi9j8QAxK3rt3r7nwwgvN5s2bzZ49e8ynP/3pge0LCwvmiiuuMDt27DAzMzPmla98pTlw4IC3XoqiKKmMGeP05yqKoigAAPDDH/4Qnv70p8OPfvSjAdchirGxMfjqV78KL33pS4dUO4VD74WiKIq66CiKomxgaWkJ/uAP/gDe8IY3BMW9oiiKojQNFfiKoigO//iP/winnnoq3HHHHXDdddfVXR1FURRFiUZddBRFURRFURSlQ6gFX1EURVEURVE6hAp8RVEURVEURekQKvAVRVEURVEUpUOowFcURVEURVGUDqECX1EURVEURVE6hAp8RVEURVEURekQKvAVRVEURVEUpUOowFcURVEURVGUDqECX1EURVEURVE6hAp8RVEURVEURekQKvAVRVEURVEUpUOowFcURVEURVGUDqECX1EURVEURVE6hAp8RVEURVEURekQKvAVRVEURVEUpUOowFcURVEURVGUDqECX1EURVEURVE6xEQow6ZNm+D0008fRl0URVEURVEURQnw8MMPw+LiIrs9KPBPP/10eODgwaiDGhgDAIAxMOvLbWQMTK3H565jmevq7mvPj1r3bePyhtZj83LHzXUtFBm+a87d2zaCn6O2n4+ilEH/V5tF0/+PpO/sttOkd97us87ybg8KfCnUybb9psbWnxI+VJlSUUuVjbfFCuIQNm8VDy9Xd2o9dRtAesMBr7f9+c0Fvg7cdfE15tpIF86h8ayt1V0Dmp56r3btfd52qOvfpP+nmHd2iCa/h936cEahprw7sgn8UcfeTMlNDQkm33qZfUNlpcI9zNSxpRb80DFCDSO7nWuwxF43hYa6Tviahxq9TaVt9a2dpor1FELnog0ApQE0RUiWhXqf4+WmQjVCmnJPsgr8NtyMspS9cU1skVpCjYHQuUvOixPe7npK/VLWlfzENCibZu1QnqBLQr0qRrABoBb9ZuL7X8XrISNZnTShDhIoo1VTdV0Wgd/EE6uKUTrXKhmWy5HSLJps7XBpYp1KU6dwb1KjYRjimzrfjol+X4+oMlwk/6s5XWhSkLjKtgHK8BnyVqiLZIHfphuiDAdqMCbezq3H5u3Kn4XSHFrbeBymeG6SUE8l5RxyiHN73I4JfYAW/3Y6RNPvQew4Opve5HMC4HupmzAmTX3wFTEh37iQ77WvyzBl8Ku64LSLmHEqikMVoroqod7UBkBZUZ3TIr+21lmR76L/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", "text/plain": [ "
" ] }, - "metadata": { - "needs_background": "light" - }, + "metadata": {}, "output_type": "display_data" }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "Vorticity plot at timestep: 2100\n", - "\n" - ] + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAvgAAADpCAYAAABRN0P0AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/bCgiHAAAACXBIWXMAAAxOAAAMTgF/d4wjAABywklEQVR4nO2de5AkR3Xuz/T0zL5md0er3WXR20g8LN7I2Fe2dCWHgMBYwmDg4rB4GfGQhGwcVvDGEkGAjQMUgA1hMLZ0QSBHXHBAgADbEoaQHCCMTAC2DAjtSqxeK2m1mn3Ne6buH7U5nX3mZOY5mVnVVT3nFzHRXZlZmVnVPV1ffnUya6QoigIURVEURVEURRkKOoPugKIoiqIoiqIo+VCBryiKoiiKoihDhAp8RVEURVEURRkiVOAriqIoiqIoyhChAl9RFEVRFEVRhggV+IqiKIqiKIoyRKjAVxRFURRFUZQhQgW+oiit5gtf+AI8+clPZpW99dZbYWJiApaWliruVT/ve9/74Jxzzqm1zVQuvfRSeMMb3jDobiiKoigRqMBXFCUrL33pS+ElL3kJmffOd74TzjzzzOi6zz//fHjve9/bl3bxxRfDz3/+c9b+5557Lhw5cgRGR0cBoLnCO1e/Xve618GrXvWqYLmRkRG4+eab+9I+9alPwd///d8n9yEWbt9T+f73vw8XXXQR7Nq1C7Zs2QJPf/rT4brrrusr861vfQsuuOACOP7442FkZATuuuuuVfXs3bsXLrzwQti8eTNs374drrjiCpifn+8r88lPfhJOO+002LhxIzznOc+BW265pdJjUxRl7aICX1GUrFx22WVw4403wn333deXPj8/D9deey1cdtll4jqxUFKUXDz66KPwspe9DH7yk5/AwYMH4a//+q/hrW99K3zlK19ZKbNp0yZ4zWteA5/73OfIOpaXl+HCCy+Ebdu2wf333w//+Z//Cbfccgu87W1vWynzxS9+Ed797nfDZz/7WZiamoJLLrkEXvSiF8G9995b9SEqirIWKRRFUTKyvLxcPPGJTyyuuuqqvvQvfOELxaZNm4qDBw8WMzMzxdvf/vbitNNOKyYnJ4tzzjmnuO2221bKXnfddcWJJ55YfOITnyhOPfXUYmJionjzm99cdDqdYmxsrNi0aVOxadOmvrKGhYWF4pprril+9Vd/tZiYmChOPPHE4kMf+lBRFEXx7W9/uwCAYmFhofj85z9fjI2NFZ1OZ6W+W265pTjnnHOK973vfX19/+IXv1js3LmzmJubI48ZAIprrrmm+LVf+7Vi06ZNxXOf+9ziBz/4wUr+1VdfXfzWb/3WyvZjjz1WvPGNbyxOPPHE4vjjjy9e+MIXFj/72c+Koiic/aL48z//8+KJT3xiMTExUZx00knFFVdcURw9erQoiqL44Ac/WHS73aLb7a7U88tf/nJVHWeeeWYBAMX69euLTZs2FS984QuLoiiK1772tcXFF1+8Uu7UU08trr766uIFL3hBsWnTpuKMM84ovvWtbxXf/va3i6c//enFxMREccEFFxQPPvjgyj4zMzPFu971ruIJT3hCMTk5WZx77rnFD3/4w5X8f/u3fyvOOuusYsuWLcW2bduK3/zN3ywOHDjg7fttt91WnHfeecW2bduKU045pXjve99bLCwssD8LDr/3e79X/Mmf/Mmq9LvvvrsAgOIXv/hFX/p3vvOdotvtFo888shK2le+8pVi48aNxczMTFEURXH++ecXf/qnf9q337Oe9azi/e9/v6hviqIoHFTgK4qSnWuuuaY44YQT+oTXueeeW7zpTW8qiqIorrjiiuJpT3ta8Ytf/KKYm5srPvKRjxQTExPFvffeWxRFKdpHR0eLN77xjcXhw4dXROt5551XvOc97+lrCwv897znPcXpp59efP/73y+WlpaKRx99tPjud79bFEW/wC+K1cK7KMqByMknn1wsLS2tpD3vec8r3vGOdziPFwCK008/vbjjjjuK2dnZ4uqrry62b99eTE1Nke1ceOGFxfnnn188+OCDxdGjR4u3vvWtxUknnVQcPnzY2S+Kz33uc8Uvf/nLYnl5ufjv//7v4vTTTy/e+c53ruRjke7r/0033dSXRgn8U045pfjRj35ULC4uFn/2Z39W7Nq1q3jpS19aPPzww8WhQ4eKs88+e+UzNnVccMEFxb333lssLCwUf/M3f1Ps2LGjeOyxx4qiKIoTTjihuPbaa4vl5eVibm6u+O53v1scOXLE2fef/exnxaZNm4p//Md/LBYWFop77rmneMYznlF84AMf6DsW32cR4uDBg8WuXbuK6667blWeS+B/7GMfK570pCf1pd1///0FABQ//vGPi6IoisnJyeKGG27oK/PGN76xeOlLX8rql6IoigQN0VEUJTt/9Ed/BI899hh89atfBQCAO+64A2699Va4/PLLYXl5Gf7hH/4BPvCBD8AZZ5wB4+PjcOWVV8ITnvAE+PznP99Xz8c+9jGYmJiAjRs3stotigI+/vGPw4c+9CH49V//deh0OrBt2zY4++yz2X1/+ctfDrOzs/DNb34TAAB2794N3/72t+FNb3qTd7+3vvWtcOaZZ8K6devgqquugm63u3L8Ng8++CDceOON8LGPfQx27doFGzduhA9/+MMwMzMDN954I7ufAACvfvWr4ZRTToGRkRF46lOfCm95y1vgX//1X0V1SHjDG94Az3zmM2F0dBRe85rXwL59++Btb3sb7NixAzZv3gwve9nL4D/+4z8AoAx9+exnPwuf/OQn4aSTToJutwtXXHEFbN26deU4x8fHYffu3fDAAw/A+Pg4nH322bBp0yZn+5/85Cfhoosugj/4gz+AbrcLp556Krz97W9fFTPP/Sww8/Pz8MpXvhKe8pSniOL/Dx06BJOTk31pxx133Eqer4zJVxRFyYkKfEVRsnPcccfBK1/5SvjUpz4FAOWEzbPPPhue+cxnwv79+2FmZgZOP/30vn3OOOMM2Lt378r2zp072cLesH//fjhy5Ah7VR2K8fFxuOSSS+DTn/40AAD83d/9HVxwwQXwhCc8wbvfr/zKr6y873Q6cOqpp5Lx1SbNPv6xsTE49dRT+46fw6c//Wl4znOeA8cffzxs3boV3vOe98DDDz8sqkPC4x//+JX3RojjtMOHDwMArExE/Y3f+A2YnJxc+bv//vtX5md89atfhT179sBZZ50FZ5xxBlx99dWwuLjobP8Xv/gFfPnLX+6r77LLLoN9+/b1leN+FjbT09Pw4he/GObm5uBrX/sadLtdzikBAIAtW7bA1NRUX9pjjz22kucrY/IVRVFyogJfUZRKuPzyy+Hmm2+GH//4x3D99dfD5ZdfDgAA27dvh/Xr18Pu3bv7yu/evRtOOeWUle1OZ/XPE5Vms337dpiYmIA777yT1UdXfW9+85vhX/7lX2DPnj1w3XXXwaWXXhqs65577ll5v7y8DHv37oWTTjppVbmTTz4ZAKDv+BcXF2Hv3r0rxx86TgCA733ve3DFFVfANddcA/v27YODBw/CBz/4QSiKInh8mJGREVY5Cbt27QIAgJ/85CcwNTW18jc9PQ3vfOc7AQDg6U9/Otxwww2wb98++NKXvgSf+tSnVtx4qu+7du2CP/zDP+yr79ChQ3DkyJG+ctzPwvDYY4/B8573POh2u/CNb3wDJiYmRMf6rGc9C+6++2549NFHV9Juv/122LhxIzzpSU9aKfODH/ygb7/bb78dnv3sZ4vaUhRF4aACX1GUSnjuc58LZ511Fvz+7/8+jI+Pwyte8QoAKIXb61//erjqqqtgz549MD8/Dx/96Efhrrvugosvvthb565du7zifWRkBP74j/8Y3vWud8Htt98ORVHAgQMH4Hvf+56zvr1798Ls7Gxf+mmnnQbPf/7z4RWveAWMjY3BRRddFDzej3/84/DTn/4U5ufn4YMf/CDMz8/Di1/84lXlHv/4x8OLXvQiuPLKK+Ghhx6CmZkZeMc73gHj4+Pwu7/7u95+2Rw8eBBGR0dhx44dMDY2Bj/84Q/hE5/4xKrj2717d3Dd/127drGXGuVy6qmnwkte8hJ4y1veAr/85S8BAODw4cPwzW9+Ex588EGYn5+H6667Dh555BEAANi6dSuMjo6uOOdU3y+//HL40pe+BF/84hdhfn4elpaW4K677oJ//ud/7mub+1kAAOzbtw/OO+88OPnkk+HLX/4yrF+/flWZ5eVlmJ2dhbm5OQAoQ3lmZ2dX+nbuuefCU57yFLjyyivh8OHDsHfvXrjqqqvgkksuWanv8ssvh2uvvRZuvfVWmJ+fh7/927+FO++8E173utclnGVFURQaFfiKolTG5ZdfDnv27IHXv/71sG7dupX0j3zkI/CCF7wAfvu3fxt27twJ//RP/wQ33XTTirvt4sorr4Sf//zncNxxx62KZza8//3vhze84Q1w8cUXw+bNm+EZz3gG3HrrrWTZV77ylfDkJz8ZTjjhBJicnIR///d/X8m77LLL4Ic//CFccsklrHCNyy67DF796lfDtm3b4Ktf/Sp84xvfcPbx+uuvh9NOOw2e85znwEknnQR33HEH3HzzzbB58+ZgvwwveMEL4NJLL4Xzzz8ftm7dCu9+97vhta99bV8ZM29g+/btMDk56QwB+su//Ev4q7/6K5icnIQLL7wweKxcbrjhBjjrrLPg+c9/PmzevBme/OQnw2c+85mVuwxf+tKX4KlPfSps2rQJzjvvPHjd6163cgxU35/73OfCTTfdBJ/5zGfgxBNPhOOPPx5e/vKXrwwgDJLP4tOf/jT813/9F9x4441w3HHHwcTEBExMTMDv/M7vrJS55ZZbYMOGDfCUpzwFAACe+tSnwoYNG+D6668HgHLQ+rWvfQ0eeeQRePzjHw/Pfvaz4ZxzzoEPf/jDK3W84hWvgA984APwqle9CrZu3Qqf+cxn4Otf/3rwO68oihLDSGHfz1UURVEAAOCnP/0pPO1pT4O77767L3SIYmRkBG666SZ43vOeV1PvFBf6WSiKoqiDryiKsor5+Xn4i7/4C3j5y18eFPeKoiiK0jRU4CuKolh8/etfh+OOOw7uuOMOuOaaawbdHUVRFEURoyE6iqIoiqIoijJEqIOvKIqiKIqiKEOECnxFURRFURRFGSJU4CuKoiiKoijKEKECX1EURVEURVGGCBX4iqIoiqIoijJEqMBXFEVRFEVRlCFCBb6iKIqiKIqiDBEq8BVFURRFURRliFCBryiKoiiKoihDhAp8RVEURVEURRkiVOAriqIoiqIoyhChAl9RFEVRFEVRhggV+IqiKIqiKIoyRKjAVxRFURRFUZQhQgW+oiiKoiiKogwRKvAVRVEURVEUZYhQga8oiqIoiqIoQ0Q3VGDdunWwY8eOOvqiKIqiKIqiKEqARx55BObm5pz5QYG/Y8cOuO/ee6GAERiBYiXdt13ACAAAue3Lc5WltocZ+7wOI2vlc6wb6v9LUZJYXh50D5pJR29+KwqFXt/T8GlfrJNPPvkkb11BgW83wt2OzZOWxXAGHW0gpa9VCrs2ncO1CPX54P8JpWXUIbCrbqPOQUIO4c2pI3RMOgBQ1ihYgyn54RrebIGfUyiEhHjsnQI73+StBYFT1/G1cbCkKI2iLWK6yn6m1B0Szpy6pXXEiHW7DhX7yhplLUVe5IJzvrjnlC3wTaUpYTa+Dtrbvrwc222jaQMUX3/afq7bDPX/1rTvzlBRhQhOrTNm/7r2yYGrXa6I7nTk7jtVXiLafe2p+FeGHHX008DaWHJNFwl8gyQ+Hg8IcsXgr6WRYeg4zbmoQsytlXOsKCRNcMSl+0rKV1W2aRghzRHrHPc9l0OfOmBRFEVx0OoYfLxNhfOYcsMsVO1wJEVRhDTdNeeW5ZTLVSambA5CglsijF2i366DE66TI6QnVOewogMZRWEjNXFFMfh2A6mr6OBtqWPvEu3cNEVpO9QdMQ3JcRArmAbphucoU3V+ankfMWLaJfSXl3likgrhcQl+jrvvK6dU/31RGsWwm61VIz13LIHvimkPTWiVxMavtbj6nOQO0dFzm4+qV3XSH0wHVQnTOl3yFPFdRZ6kTA647nmMcKe27TYpUc8dTFB9xO0o+dHBQivQa1Z9sAS+yz13uYZU6AyuJ9Xt55ZdC2iITrNwiXhqACb5rrrunq1p1z6na16VoK5q35g6qxT+knIUXKEeSrPFuy3csWiXhvO4hLrEsdcJt+2gbSFSLfvurDWNNihEk2xjRQXeLyTG7YEDHkhwJvNK8hSlKnz/I77/CSVALrHZFIGcO6/qdG6+lFjHXirWTd32IIAaEEi2cX9ceT5yLPGprE1aeJdIr3vVIwrRwSE5AH5nnVMnta3LZCptIoeDHjtgHmpShXyO9BxlY/oSs09q/2P7w83n4BMnlFPPfU8Jepe772vfF+aD265ihZy2OcuYlojPVhM7wFSGDvEymT6xrEJaUZRkUuPAuUK3jrRQXi6hPSjRz8mX4HMipWE2LgHuSguF9nDa54irFrqt2Qh9V9ba+aiSmDtbNaMufrWIBT4VBxxyFLkr7OBtSQy+b19FycmacdDrIFbMx6albqemhfLqvhshrXdpiU43FBn+N0Ycv92jo733LpfeldbprHbuDba4TxVEkvX2XeWawKCEYez5aLiQHRgq8tc0UQ+6isG3rKadllK/oigtIefEUolIr0vwx6TnrCtV4FNCPiTec4tVLEwWF93i37SPRT525l3tYJHvC+ex+0a5+lTf7TxXfpOQfJZNOJbQ57uWUZG/ZkkK0ZF+ILni7jXmXqkTdewTqcql5wr31H0k/fOlV5EnSeeK9hzhPtx8Cs5KOeZYTLoR/sblx849TrPzKHHPDf3hhgHhYxikc59b7KUcSx3CU9q/hovhKFTkr0lqc/AVpU2oqM9ALpeeI7ylZbj7+PqaW+DH7pvqwtcdhhSCcsE5ohsgHEJkl6dcepeTj/dLWTcf97lumhQGww1lqpNhvRvQApGv5CVLDH6IKp56K9lXUbiosE+gakFPveemcd9ztmPTc5ZxCVlXGE2sYE8R+lyxHYIbe2+79eY8jIz0u/o4tMa8xjj5Ppru4gNUs4xnahuSdgYhVqvoW5OPt2ZyPwhyrZMcosMV0zmXutQQHSUXKugT4F6YpEIyRciH0nxtcbZD6dIy3HI+sZwSZhMj8jli3hevzzleW8wsLpavIyO9dkZHaSGORXmnI3f1ua92P13b+FhCx16niKsjdMXVRo7jbKL7b6hi8Ob6TknraMo58qAmbR6SQ3T0Q1Caiv5IZCZVsErDa3A+Jy9V4Ev670J6cecKUIk770pPFfghIc8dIPnyXaEx9ntb9Nti3+XKG6G/tFSWd02CdYmf0ERdqoxvRR18THYfBkVI+OV0mqsKg2nSAKpKcoj9hqPX73Q0Bl9pNFSoloF6ArLvB0HdegYxAiPWLU4R8Jx9fe9d/Y4RpFwkISuxK9akfhbUtk/Up9TrghL3tkNuC3Ej9o27b4t37Ox3u/Tx+KBEO+5LqByuz3WcdeATg5L+xNaTIv6rcv1z1T0oWuLKx6AiP41sMfgxcfW58MXrK+3HfJ4cga7fBQY5hEWsa5xDxKeIfF/fQ3kUueLMOevHS/stdedxnn1sLlEvqS/2e4eFOhUyY0+ydbn0i4v9bj4mtDoOt6zLyTf9z0WV8e2h+mOFcmjQU9W+g6y7DiQiv2UDAo3LjyfrMpmDio3XGPz2ExLvVD7XkR8a575uxy/GbeUIPslrTsfeleYjl4Cn4D4UalBuvSv8JsfdEsl5NSE4AOHJsFjA4wGATVH0r6ufQ/hwQnly4RpEpBIKLXKV5+wjCUsaZKiPr+6miuOWCXcpatbJ0RAdpXZ8YTe56m8dg7hdH9N2jKjMKei5Qt/XV0OV4t1HqrB35Ukde7wtceylIl8yCRcjmSRr2jUi36ygg4/bXm3HXlPf5eRSgwq8jzQGH5dLIfb3I4fgdp3bUFmp25+66k9Vbn+TRLWKfMVCBb5SO1WK+4GSW6TXKfq5bYVEpFTY53btbQYl4H00LRQn1rEPfRacwYKvv5RIwZNrAeiYe3tf49LbS22asiYmP7TSju+OgQ9b+HPKxZI6KTWXIOQMaLhuv3QC8iBj+6v4nR5ikZ6Kbx6e0o8KfGVgtFbcp/ygD+L2fRV1VOHYp7j1AM0U9FJSxX1Vjr393vd5cIV96PtJCX5bMNvhNRzhhr8bWKgbJ98VDkT1j+Pe+/qHy8Ui+V/nCmdOOVyGM0jjOvgSpz+mfK59qyLlLsGQu/g2Gp/vRwW+UilDK+I5F9UYkV2nay9tm+sKpwj5kJjPLeKxsytB2peYteFzOPZ4OyTCYwS+qdPUJ7nbwhGYlGixV9DxYU/ApdrmOPmUe+96bx9T1e69TU4hnCoSfXMRQk5/aODgKhdbPte+VcL9PknqG/QxZURDd2hU4CuV0DphL3VOU/NylK+KGOfeJQ45Ir5OYZ8i6Km6UtewB8gr7kNCHwtxXC6HY++rw9UvDCVEscg2r7b77qrfJRopJ9++MxCa1IvfU33OSczKNq79QwMriWPvO7++fvryuYMj6So8KasPDVoUc4X5kAl4Dhq6sxoV+Ep2XMulNoYqXFFXHb70psI9Do4YlIh8Q6qYzyniOW1JnzILkCbqY9163J+QEM/p2EsFvgTKoaccZOzQ+ybVhr6DlLCXiONYuOeN425LxbVLNMasHOQbAOVw+KUDIekgIXb/HKxB8S5FQ3dKVOAra4MU55PzntNGm5GcH6nYN6QI+5CoT3HtfEjFfV3CHvctdslLql+uuwCSeiWEBI3JxzH51D54gEOJdLs+Oy7ft3oOdWzcOPxccNrjDkJcA4HQXQquk89x+EODlKbE8OP9U+qRtKciP8haD91Rga9kAzv1A3fuYx1nbhmp2PelU3B+wLk/8rEXA+7ASPrqE58YKsaaEvS5L3ghZ5IS91xRHyPoOWWkk1wlA1mOa58yGMZgYekKkzFQoTq4LY47j9s35am67fq5seOpSGLPpcJaks7Fd6dE6uBLQoxc5aTzInL+dvqImRysIj/IWg7dUYGvZGHgYt7AFTIhd1niPscIMwrXj3Wu9FCej5TBksul9wn7FFEvXXFCmhcr7OsQ9bgvnM/N9d43GKta3JtyqQJGWgd29U2Mv/k+ugYHMWEqMUjakfbJFdYEwHfuJQ5/29z9qsV06l0ERUGowFeyU3vsfUjUUyLdrKdt0nzpeH+Oo+nqG4YjULHYtYUuLhvadqVJSBGGHOzjxaLe7nuOi6BLBFFpoYc1xYjz3II+tL/0PRWO4xP03LZz4RPwVJ5Jcy2xGXKVDfgzMNtVz//gutZ2WUl8ecz/lGtgEDMA4KSH8jj5VLm6nH0OdQ0q1hhrLTZfBb6SnYEIe58T7/szgn5xsT99aakUNdQ+pm7pw3xsfCLeiARKzHLTJIMCV5oLn2jzxWf7cIn6kKBPnRyXW0xz6owR+SmiHm9LhL3dFtehD6Vz4QzmYt15XzgPx/l2tWt+TwDCy3dK72C5yDW4oEQ5R6T7ylTh7Pvycjj7sb8pVYjx0KArZVC2hlkrQl8FviICi/cCRuoNz3GJoZCwNwLefgUAmJ8vL5BLS6sFP0fkA8THk1NClnrFafZTPKl9qDS8egjVH6pfGE54ilQcuY7H914KJdp82xJBXYdLH+qDdFs6SEu5C0Btx0J91w0+UewSQq4HYOH3g4bz3U+ZpB5zrJwBQYyLHxoA2HmSQQAnn1smtJ9N3WFmCpthn4SrAl8Rgf8ZGuHWh8T8/Hxv27x3veJ9saOP26f6Z+MKmbHFui1sKWFvHsRjC2FXWfs1NBBwieeFhbgLSsjRogYWnEEKVZekH1yhCyB/umuObdwubjulXu6xS5fT9L2n4ApI310ajovPcbGNYDKx9fgJtpz+St3eVKi+cO9qcB4GJhWRMeK+CrHPGQRQeXY+x9UPlfNRlfBXsjDMbv5QCXw8Ghv20dmaxo6Vp9x5I9qNqF9cLP/m5soLui3sbeEfK/BdcNx3SqwbgW7EPED/EzftMqY+l9A34o0S/VQf8Xtfmg+XaMB9ju1DjLvKEbgxwl4qvmMEPTctpS8xE3S5+ERz6LP23Z0CoAWsS9SZfrhEPnbxzX5U3U1x+H2Y47TB3z97gIOPjdp2iXfOvtR2apovPSWPKscpy0EySFQXX4lgqAQ+FvMq7vNBjXJj3XtRWI8tsu1tW4AbcT493S/ozfbsbC99Zqa8uOGypg4s6CkxT10cMS6nnnLlbXHe6fTyRkdX59n51L6++k1/KFHtGoDY5TjbLsGO71S4+kLV74ISjJQIlYj6GLc6VczHCvmUtJwhSBQuMc/5bDl3cUKuvgRb5GNCYrJOuIKWI6bxPvaxu5YDdfXJN2hrk6vvGsRRSMqGqPtukEIyjMtpDpXAV+ojJTRHLO7Ne/Nq3Hjs1BvRPjNThpkcPdoT8/Pz5d/sbPlqi397wLCwULZji3u7fRuf0KWcdVvkGgHe7a525MfG+vNw2cXFXvmlpdVuv33h7HZXlzF9pe4amG3XbX3XRdrnwFKDDKoPvnNr8Amr0Odl0iRufayD30RRz+mXa78YfOKPKutLk4h7jhOLXWfXUpjm/2XQTr1vwBS6u8Bx4M33AJ8Dl7Pva4OT59q2y1NlfGl4X1+6qx5JPtWOqy1uHbn6oyQxLNEfQyXwQyE6w/KhDYLQeeO48iznHjv19p8R9bagn58HOHy4FOaHDpXpR470RPzsbLlt/o4eLdMWFnqi3m6XgnKjAdxhLragNWWMaMeuvC3wjbjH6WYfPBjAdQH0DwaoV+z244GBXca8ty/+xt1zCXJqMEMNIDjCXirmQ2m5HtTkE/ODEPK+9KofxhVCKkh83wfqvW9JVVvIY0HvEru4HXtVnLqxj81119DnbHOFtY3P2XchcfFD9Zj+UduhNGpfbjqVh/NdZUL7SPfNIeJ9deggYc0wVALfR2MexDQEULeyOOc3i7g3DrwR8DMzpcCfny8F/uxs+TozU4r56ely+8iRXr4ZIFBOncEWoa44dyqe3ohu45obUW4cwuXl/lvg5gfdLtftlummLoCeKLPFsqnL7B/C5ZKaVYTMIMTO49w+dol7StRTwt4nNriuvE+sS9dy5+Tb9eK6qbKhbV96DlEPIHPrY4R8isstcfBjwM6yS0zaaYPC9IX6vDh3GXyOfSgPl6PubNhzFVztxgw2fH2kyofS8L6uspy8UL0cuOJdnfpG0WZjeKgEvi8GX/oBtflDrRJKpFNp4uUzbTEPsHoFnNnZUqxPTZXvzfbsbJlm8o4eBbj//lL0m9dHH+0J+76OjwCsW9f/OjbW74xTLjkWsOPjPZFt/nwOPUD/QGB0tBd2Y96bepaWeuEyxt3Hbn+n09vflLW37VdXXL99QbGFPr7QuPbFx0fdMbDr811kfencV198Pdfxp9x/uz5Xn2PEfYzQlgh5Tn0hYRvKl7qjnDpwmqsee4AeEvMUMU5tqE4uvvPm6gu+y4DdflcMfYx4tMu75iq4BgrSPNNHzravjCE0gIv9zub4vlTt1CtJ2PqlrXpwqAR+Cm39AJuKKM7evLocexOOMz1dOvH4/cMPl+8feqgU+Hv3lmL/nnv8bRdFWT9Azwk3F0c7VMaOYQfoCfPx8f76ut2yDCWkqUmm+D0+L/bqN/a5Mk4/JRxDTr45RpxmzkdoOT3KkccDGWrgEBL2FFJR73LsYwS9b6DgK8/d9qVLnXmqb5z6cuW7SBGS0jqwqI1x6VNc/Jg7HqF9OYLWpKXE0EveY0cf34nE76V5rn5S+dwyVLlQuq+u1LJV04Q+DDFt0oqNF/i+OHq8sou9jcNIXILT9UG15QOsE05oTvQKOdi5N/H1xrE/dGh1uM3+/eXrnj2lS797d7kdg3H35+ZKcW9ceVuoG/AAYHS0vAOwYUMvHbv8rommOF4f3y1YObGWWLXPFW7HOPVmH5NvhL+5O2AGLaZOu08GSsBjIe9KxwMBHz6RiweArjyAsLDnOPh2PVRdVPlYh55KixXxrvpiykjK5cD1/ZAKFfxEV+pzkbiyVRIS9XYZn6AN1ZHy8Cu7PmqQ4XLzfftLHftYUc9N86XjfF+ZlLLc77k6/oqQxgt8adiNSTNC07W/K9REhT0NNzSHjS3Y7G3j2B850hP2s7MABw70wnAefrh06++/v3y/Z0+8sDdhJevX94R6t9sL1TGuPJ5Q2+kATEyU5TZvLgcE69e7BwVmP9w2wOrJqKHBgSkreZy9D1e7LhFvnxNqpR+7TurYQ242V9THCntXWm5hnyrqh1XQp4DFHXauAfzL2Db1OCVuPUcgxzru1HuXEKXOvUF6Z4DadtVHHbNrf26aq86YMjFllVbQtrmcjRf4EmJj8HGsVa7224pvoONz7ql0Epdzbwt749wbYb9/f5m3f38p6O+8s/yLYcMGgMnJUsxPTJSidHy8J/CpWHzqR9oI/MnJ1YMBs485Pnv5TXwubGzh7lrSEmD1oIOL2Z8S8ibNiHZ79R47zY799zn39qt9rPZ5CYl6X7kcjn1oyczcAl8q5teiiDeEhBhAv5B1YVbC4QyIQ4OqFKQDcnz82Em38bnkeBUsgNWhO7731EDD5AOsfo6AaxDhq9M1AMB59jZ1jiTCH5fj5EnKxJSVIK1PBxpJuGLym2wMD5XAx/jCeewyPuyQk6Z+iLmJPc6oVXLskBwj7I8eLV9nZkqnfmqqDL85cADggQdK5/7BBwN98TAzU/5loPjrvy6dewMWueY45+fLC+DCwuoBDrXKC4B/CUCXkLaFtmvZTVuwmxCj0VH3ZGG7jC8khxL1FDHiHpelxH2sY1+1sOcuVUnty82LKVfV/gAyMcERW6662zRocZ0TKtSFWhnHJ3g5731CP8bBl94pwPviY+GIfs62q4zrGKj0UJ6kTA649auIr5WmO/qNF/icmPvQ/r5tDCXo14qwd+E7h+xzQ4kv+4FVZsKsEfiPPlpuP/QQwL59AD/6UfkeCftBfzIjf/InZHrxne+Ub4zwpJ6ga86BWabSFqlcsMNntl0CnBLuY2P96UboY8FvC3sqJAf3xxASrRKBH+vapwp7ieC32zBIBX1O5z62fAout1eyj2u/Nol6g8t9t4W3weWG43o47jj1nhLjkjpcZbnhQLjvLiFvk0Pk+9oItW33OaWMCvChpkozmJqPGqLxAt8XZhM6idTgIIQkXGctCH/OoErk3AP0BK5Z7tI49yYE58gRgLvuKh37//qvUuzHhuMMitNO6/8xxw/nsh/GtbhYOvu2yKccfox9QaUeKkUJexxq4xP2tsCXuvYSl97n1GNxnyMcJ2Y9fPye2k55SmyK0JeUGxSS/g3KoY9pK0WwUaLf5bQD+MNqAMICnCPGDZyBgl3W7rtL5Nt95bZFHR+1n69vXNc+JNKbJOKpNnTw0Dhyhu9I9S9ACwR+CnhwECP4fUjqaMNgoJZYMtu5NyLfrIpjXg8dArjvvtK5v+OOVRNom38mAUZOO23lfWE79nYoknm172SYP9vVx0/T9DmdlLDHr1js+4Q9R9xzwWLZtU0JfVvoSF370ORZ6r3EsQ8Je+rz8onJkNCMEaKDHgBIviuD6iunXZcoDJW1y2EBi9MoEQ4gnyRrt82pP6Vem9DgwbevNI/aBggL/1A6roPbP05+iCqEuor/7DQ9PAeghQLfFbIT49Cn0AbBLkU6PyFYBosl42Ib1/7w4VLgTk2VoTcHDpRr1xvnft++1avjdLuDfXx8BEdmRmFubsOxFSo3QXcLwNZdu/of3GWcfXNuFhZ68fpmnoJvTXbqabs+gc8NzXHdEXDBEe3m8+M4+b6QHI57L3XtucI+l6hPce5zC+Ec9YXcz6qpuh1KlHMJCXuzjR1xAPna8758qmzuelNW75HkufqI8bn2MY596iBAAq5H6t6ruK8crjkqMVF9S8Sb7RBsgT+omcIc172ukdQwinoXUXH2Powgs537ubkyNv3w4TI055FHykm0Bw6U7x977FhfLFom7gEANm/uP3979hQAMAYbN26FsU6nPBfj473X0dFS3M/O9tarxyIYg0NlKGEfcvCpybWUsPdd8PB7Vx4l0PF7yURa6ry4Bgeh/oaOQSrucwp7qYCt2w0f9J0CF6nrwbtcaJtQyIfEXfblhSaxSoQ9x20P1ZuLVOdeKvrtsgBxx1PFebBRcd56fNo5djUeruZlC/xBidu6Y/BD9cXSpsEB94FWVPoKtvAyYSf2w6ump0sRf++95ZNn/+d/yteUNe1bwpEj5d/ICMCGDZth48bNsHnHDpjYsFRmHDjQm3Rs7nrYoTuuJ8661rDHq+m4hLx5Mi+eSOsS9i6x7tr2lcH12OI8JOrxq8+1p8q70vA296m2nO3YdGkZFzkegDRoqlzSEuM7X64Ha7mEvcStNnCeMm0TEp4utz3UJ8mAIuapt5JjCJV39RWg3pAdHxx3PpSvg4BGUGfIDldPti5Ep820bblNzgCKBIslO+bejrs3ITr795fO/X339Yn7dpwlOc94Rnlkt99erNzEmJ8HWJocha1btvSeqmsuHHNzPcFOufi2ALcdfGqZTPwUXiz0zb4hx54DddeB2sbpKeLe1Q+qPSrftV9ucV+Xg99kIV+nQE/F91AnvOINh7od8LaQ4uJL6pOmc9vz0fbPRmkd0SE6dYXspLrwVY6qpLdW2iDsqUEIx9EnMbHjs7O9FWOOHClj680Dq3bvBrj7boDbb+/f9+yzAb73vfQDajCLi+WDeA07dwJs3z4Cj3vcDpjYtq3MNIMhM0haXu49NMvgW0HHtX49FYbjegotxufG2/H1g3Luzf52HlU+lIbrtOvF5TjbkjRfuqtvUgYlsFPuPAwC+3/BPmdY7OMQl7oFd1sFZIqo57r2OR37XITce429by2c1QZdZSidKF3u3RDt4A9SrOYMu0nB9WSzYUJ8jrFos8NLpqfLv4MHyzCU/fvLJTAfeuhYWxZDLu4BAP7X/+r/vtx8cwHdbvkw3aIYhc1btvQmFdsr8VC3t23nHmC1sA+Je8qxxxdO+9W8D7nxrrJUuuuBX7h8iFDMvaQuql5q3zrFfYywr0PMVy3cq6o/1gVuMjGhHsNCDpEvqTsHMeJeUQIMfQy+KVfHYCA1Rn+QITyhtlmzt7HAs9d+n5npifq77y7DcX72s9LFv+++rMfSVh5+uDxF09MAk5MAJ520CSYmN/Um4k5P9zv4eB18HHsPQAt81x+AO2YVp1FhWHaez7WnBgvcB1jhflHuPdVfXAfnvSssRyruU8U+7ouLHCK+SpHeVOc+l2Preto0hZ3Hee96qnXI1XW9t6HCjqR1DBKu6M4l8jlIBHusmG/iZ6FUAtawXE3b6hh87pNtB+30Yzgiuk64t4VcaX3g0IzFxVLcG4F/4ECpZB95pHydnx/C+x5x/OEfjsD11xdw8GB56jZuLE/fjskt5QRYcz7tWHyXUMdiPrTkJfcC6QtvofJzu+ghcjnVseEvnONpkrivSnQ3Vcy7kIY6YFHsE/dccZci7mPFnqvutpNDtOcS/qluvDQ0J6YNpTZSlsq00zmIYvAB3HHnvuV+QmVxvWa77rXtUxi28BzxMplY1BsHf3YW4OjRcjLt3r2lqN+9u1z3fs8egPXrqzuIFrK0VEYtHVshFLZsAeh0xmBiYgzWTVhLjAL0O494YqzErXe59pQL70rjOvVUPte9d71SAtgnMDnuvQ3XvecId6nwDYl7qbDPLbzrEvKcdmJFjWu/VGGPt2OEvlTcS9/XUXduUhz7VBefI7Ylg8WY0BwV90NPbMw9RuTgc8NlQqE0sfUA5F/6MhehvsSsdVoXvv5IH6wAAL14cROaY9aFnJoqnfv77y9X0Nm4EUamptI6P2S87nXl+b722gL27y9P4ZYt5fVnx8SxwRB2xgHCgp4j7CmotlyC3s7H7zlCWqkec/enqfX52qmjLm4YS53CHrcX2p/TnmvJzViRS8G9O9BUQRpzLlJCcXKIe6WRSLVqrrDyxsfgY3wDgLpj7iXgCbk+XHc36kIcmoMFn1kO04TmHDlSLn/56KOlwL/nHhgZ8rXuU3n963tCf9u28rRu376h/CZQolso7oNPLXaF24SEfci1rxLKza5zMFGX2G0a5rvVxGNPEayuZ0349o0R9ng7xbXntkfVH7M/d4BSNVXE0aeS08nP0aay5hjKZTKbJu4B4hz8Jk62XQUWckbcT0+XoTnT073Y+6mpMrxkcnLoH2aVi+XlcnzU7ZavGzdugFF7Qqt59YXmADiFPYD1/+ILheEKe9z5OhkZWS3yKdHtEuKudLteXCZ1O8ToqD9MxxZS3HAdlwhI/byaKi5C/XKJUUr8priwXFGP2+YKe46DHxPPnxqaI5mfMCikA8EqnXtO+zH5SiOhNGEuDSsK0QkF/Evi6iWivYmCPUTTJtKmwJpYa16NyJ+b68XfGwf/6NEyfGfLluo7PURMT5e6fXq6vD5PmAm3lNh2OPfi7x0l7qk83751ERLBPlyCO6XOmDZ9LrgRSKH+xIh9qg8umujQGyTiJkXQc9Ikgi5G2PvyfPvUJe59oTk5RGru0BVOfZL6U8tqaM7QExtjLzXWs8Tg54q559DUGHxMSszVIOP1RSNJ+4K/aE0AnZ0tBf7+/aVzv3dvz7Hfvh3g2c8G+O//rqD3w8dJJ/VO6759ABMTAPC4dbBhA8Do4tzqW9NI3OPPExfvc+99E2ipdJNmV14HRiD73PCQc895b4t87OID9O8j2Xal2elUniucghL+KSESrsHBoEVG7DGFVofhhJ+40qRlYlffqfPOQGxeyjwFatuVxiW2vlxufMrdHk66tIzSeqR6sHXLZFIH2MSYeymuGH3f6kQ5206q1wg++6FW8/O9ybWHDpWTajsdGPnSl/J1fA3wwheWn8vXvlbA4cNlmrkBMrHOcQGznHuDV3s3MdyGwifoDaEwHU4dkrolSJ17n9i3oQRsyt2HuuOnU5As7egTQVWLfImrnUtQctvMIfQxUnFPUYe4jxXdoTZz1avifs2RU8tGhei4FtyXLn2ZS5i3QdznEOlVufmcdVZXfVZUWI6xmqenS2G/f3+5LOa+fWWZk0+upP9rhYceKqOcRkfLaQwbHj8Go92i95ApgL4f+wJGvAbxqs/TF3uPy9jkFv/Yoec49ZTbbotnSuTjfOo9Do8xosmuH+9HbbvSfOncfIqUgUxV1CFEUgVRitBPnZQr3Q61H7s2v7RsqnMfWyYlLVSmTqdenXvFg8vw5cAS+CkC2rdvG4R5CsNwZ8HgfXotdvCN0J+ZKd8vL8PIjTfW2+Eh46KLyn/qr361gLm58mG2i4sAnfERGCF+5LlPeXbG12PqEPYch9wn+l0hNXjfmPeU0Lfrx/2hnHmXWx9y6l2DghBtuvjn7GsVIt9VPoeo55TJJexDdUm36xL3FFW59HWKexdt+t9VakWiKVkC3whVIxqobVyeek9tS2lLDD6AP+wmBe5dk5RVj1jn2Bb2toM/NVW690ePltvbt0sOTwlgVtWZmCj/NqznXQxWXTNcoTm5J9G6HHnbfQcoxbPtvuOypm1fntTJt9N97+36DTgMRnLxlswdcJVzkWvgVZfIqFvch8r5QpRyTcR1pUkFNdWnKgUt1Yccqw250lL2bbqwV+deiUBiHGdZRadKKEHfdGGfm0bfCcCx94uLvYm2JnRkcnKgXRwmut3+0zw+DrB+vez74QzNMds2MZNofS57VWXs977JsVjAu+rA76ltX/iOvY+BcvM5eZjQ51DFxb9KQTEIgS8V8aH6c4p8Kq0KUV3H4MKVxilT58CpSjGfW9wrChNxiE5OJ5pLY8UtIsZZl9SdczsJX1iOmVh75EgpfLpdgCc9KV/ba5zJyfIGiZnm0O0CrF8P0O2OyK6tlLB3iXmuAI8V61iY+0Q5dRyUQ4/vCgDQbj5+nwtqkuvoKG+FmkEIeA652q1iEm+uybahMr59U8U/R0BT++YSvVRaLlFftYCP6UeVol+S5kuPLacMLdlDdGyqmig7DPgGP9REiZjJyo1aR98W+mai7dGj5eviIsD69TDy0Y8OupdDxW/+Zvn533JL0RcVtX596eaHyPK/6hPyuIxP0PvcdyzKJXXYwh27+Xadpgzl6OM817Zd3k7DIgj3wQWevNsEUgW5RHxziTlHOUIfcgl8gHhBn5KWc7AR215Kv+pw8GMc+Zg+SepTlAiilskMxeDnpE0x9xJCAwBuHoYaOJj9XAMHqm94vsUKdnw2dvGPHCmXw5yeLpXn1q3evirxjIz0FiwaHS2vEfafF59z7yoHIBP29ntKeHNEvjlQSvRSrjvHlXc5+jH4Bh5UWbufOcB3CaoQ0gDp5ygndfQldSAQE/6T6v6nOsgxIUBNSsst8mO2JWm+9NSyinIMscAPOdE+Yt3+YRL2OQg9ERgPwKj9Q/UbWKvnGPfehOqY2Ht9Ym1ljI725jXPzZWnPEukCRVz7hP2PvEeqsO3LzXxNrSPy5XHjr7BFsg5l70MwRkocevJRWxdTRL/sXVy9wndycj5QK1c6SmCXlJnU9LqEPqxZbh5KWUVxSL5QVfRk/uGAEpoA8hCaQYVk8/tG4nt4uOHW5llMd/3PnF7Ch8TqnPbbcWK2B8b610LyLvu1J2YWCgRH8qXDAZMPp7I6gqvoQjF7qeuiBNy633nmKo/l7Pvqr/KOlLbq0rESMOL6orlj82TpruOn+vQu9KrcK85oplbrg7hL+kjNy+lrKIgWvck2ybBDauR1JNKrmUyg33CITrHhD0AlAHhSi0sL5dr4i8slBqV0tFZoJz4kDjn5OODCTnvIaGf68Bd4S/U2vqmXQx1fD6qvJhX6WwDpMXpVxVaZKj62LnlQ2ViRaFExPvqyi3QpWVzCvrY/aoYuHDyUsoqioOsk2xzTA51PfVW4ZESQiUCx+KbGBEA3mxPJQtG1JtXG1Lr4vh7DOW2c9x5/F5S1if+cR9ck3CdB0ycDE74DYXdLkYq8HK69ZI+uJAI9Nwudyy56q56AJBD/OeM6/fltUn8Vyn0JWm+9FBejvKK4iApBh9vp0wODbXTBkKDn1yrD0kHVbEhQGT/cOy9HabT6ZTuvTr4tbFpU/lqpkGMjZXbnU7/dSLp/8nl3puGqXQKl5CWuu52e7bTHlqGkuo7tR1Ks/stxfQ3h2udUkdVbmKKOKlL2NTdR+7gifN5xgrJqsV+rvQmDQRi07n5qeUVJYCG6CTAEdOhp/r6VrcJiXYs2nKvZhQchGChD1AuzL5hQ9Z+KG663f6bKUVRQXiODeXSm3TTCVyOcug56ZwyOI7e5ey7+mio9KRZuM6fr3zOtnOXbbKIydVWXeFHOZz+tTgAqPPugi+dm59rH0UJoAI/gVwP/QrF78feJcEhOtmXMzW2sXHwAUrFuXlz3nYUJ91uuVzm/Hy5baKjuin/2ZyQGeq92ddXLqU/vvfUNnbL7ZV4DK47D6E7EnUNCGKoO3zEJsdDrKqOybfJ/RlWLe6GZRAQs8+g0kN5nPxc+yiKABX4Q44rJj9K7LucR+Pgdzow8n/+T2RPlRjOPHMEfvjDAhYWevrW+jj8+ASyz6l3iW2AuLzQ4MCX53LhsbsPsHrirGsd/NDJa6K4r8JJryouX9KH3FTVZuoANnfZVEHaZKEfs08VgxpOfu79FEWACnwBdT9R1rcSjnRisyQmn303otPp/0uyjZVY5ufL02/Ww+92hdePkEPuE9E5wlx8YSuSPF+/cL6LpaX49cyrItYV54ru3OE4uc7PMA0AUuuvYgDHKTeIuwFV1ZtLrKuoV1qCKjIBlBteRRspy1v66qoMI+6rWBlECRJjQpOVhBx7vB0KkQntazqKt2MHFJxYex+U64/xraSTm9hQlUEI9roc7JzU3W7d56jOz3dQA4XUfbllJOVy7acoiajAT6CqlX5iY/A5+2abnGu79mNjAOvW1Rs7q6ywfn2pOUdHex9LENudp5a2BEgXzLnxOfb2tknD/fT1nzsiyjWQzXEOc4d25Bbyub8nTRNKdfSnzkHBIAZ8dX4v6/zuNu27qqxJVOCvYcQhRy4xaAS+hugMBBOSQ+lZsYtvduRsx+wTcuClIT8uIS858JhlOgdBFX1Uh3Pw7WMGGeI0qO9YFeWqLFtlHYqSCVVkAZImpTYIvAY/le9bgpPEOPjj4wATE/rjNiDGx/tvqHhxDdJCoTUAbpFOpeW6WHL6avfDlYb7aOc39XtbhQM7SEHfdMe7KVR1DHW70nV8P+rap8p6FKUiVOAHMEIXT3Bt8iRb3wO1fEKfvZymUZNGUS4v91SmUjvGwR8ZEYToAPSLZYlwttNzpnGceMngwSX0uflVU6U4UQc/jSb2KZacxzKIz3hQ+1ZZl6LUAEvgp7rYLvHZNnKtex/bZo7tmDb7MEIQW8b64zcwRkf74+9Fc0CxyAcIi3pXenKMkKdvvjRfut2vUP6gqMrpbLqjn7rfsNLE89FUoVzFuWri+VeUCEQOfqqobaO4b0uIjsvRz73PCrbAF9nGSm7GxvpvqgQn23JDcgwxD4OSTnKV4BP5uC1XPtWvVOpyGpsq8KVlc+ynVEvdn0td7en3TWkpEqOcJfDbKMxzwF03vgngPvr67Po8RbH3RsDp8pgDx76J4nLvV322nJAcnIfzXQLelecaCFDlfftw8Dn6VNm6aHJcfRVOfe5zO0zCrO2/m039LJraL0UR4tJxXK3GDtGRsFYHBIMk5ZyzY+8BesLJCHvzpz+qA8MW+FFLZeI0jE+422Vi8jihNbgPVD8kjv0gaKpD3xQH/xhtMFNyMAKOpygrSoto2v/rsGlPX/QI59xXMsmW03CbPogqQ3RyTJzNCStcx3bwcbpSO0mnnTPRNEZMcwcFnHxfGVOuid+9QYj6AYTlxPw2tt28Tqf/nNX99W3T9Vepl6aJdgl4UZK2k/pZZBH4w3AibSThLjnb8rWb0ifu5yOOwbdfmyqy1gD20pi+ZTK9A0POZ5d7WcmYya9NdupzC/oMZTi/E+TpJ9IkojxVwLdlANDECCReHfJrWlP+zRSFg2s58KZRpb7UZTLXACmOP/vL19Gn2A4KPA3CdyFOuvvDFdy+TkgFOieEqAkMQNyH/jdDIpkrousS9ktL8fsOCrvPg36QN+dmHGd/FymfbVP/bZW1RdSCIhVRx52SLAI/taODPtltviXlQnJOKdHHFQ8mBF8ZHLZrzxH6Ob7vK9+XlEmjnC9OU5UBs1/cc73qVCwH8pl53DJccV1E/FS33c3nfgV951D6NZYsdWsGFqlzyiVRek2hqf1Smk0TlmyXaq4YBubgD/rkKvFgcb+8XF7cBu1grVXsuRp1XfCyzLNJ7axU+WU6OdGivYIyqcKdK9ilpzpFlA9a0GO4d8dCdVQ1V5k7OJMMBDjTbnzlMVUNHto4KFGaQRNEvqEqkzm7wG/KCZMgWkWmJbieUkvhe7otxvygLi6WrwsLpUhYXGznLfah4NiHMXJs+ZzmTHqseCJhZFhYzLHmDFNJEeUhQZ7adtXhODm+Z1V8V3NPK8lVNudCSeY3mwLfMaC+gz4DJ3YQJFnNltrXh6ReHQysTVzXyip07CD0ZXaB39ZZzMMk7qvGdvCNuF9c1B/JgYFO/ohA5NfpljbFma3Kja7STU9x8QcRix9T3qZus8DXnuTOpEToclz9OtYu6HT83z0j/rnnKHVBLldZTM759m2Z6qNUT1W6dRAPTK00RCfnwXCXk8Rlqe21RFWx+MvLpa6cny/fz81piM7AMAK/01kJyA+J/NSY7qZQRygIR2ymOOt153HyAWQiOyY2n9uPQUOdB4kA5MTT+347fW1xHiMheXZdqD1Xvrk7IJk7EKo/JiwoV8iOdO0AZTio2pSuI+7eplUx+Hgf33aorM0wi39JnJnky7e01Iu/n5srQ3U0RGdA2AKf8ZQr6uJVpzubk5h+5JpQmiKiY/IGHYtfZ0hOE75fksWdOPv4yvgEsvnsqEGAb+XaUF4Vz6VzfY9CdwE4dwBSn22Xw6VXp3+4qToCZagc/LaE6Ay7wx/60koGAUbUF0W/uJ+d1R+7gYEd/GNXaOp7jS9QMWIyRJXiLGYQmUO81umyu9JjY/PbFqLTFHEfM4eb4yDHDA46HTqG3iWcXYLZ5dy7xDM33eT5BgY+OOE/nPMmObc5QnxU8A8vPk3oevBo03TkwEJ0Bi3+XeE8w4zrnLtEfwg7Bn962v+QJaVC5ud7An98fOWqIxX3OSdgYlLv7khCQFJXpvHl5xbzrvMSI9irDtmpe1LtIO4Ipiw5WWWoDCe23063BwP2XQHX3QCfoJeIeWm6aZP6vnNdf45Q5wrxVMEumYOhtIOQVvVFjtgMxSRbLk0R1U3pR1XEDKR8cfeYxcXSxZ+ZARgbEzel5MB28B0qI0bg5Y7TtqlyPfUmuO0AcvEubaMKoc8tY6jqjkqImIEDR3D5VpoBiFttJla4S110n7uO01whQdRyxz5xzsX10+RL94l+01cbX5iPT/Bzw3pixb4K/XaS24SuO/4eoGaBT02UbQLDLPKrjCkz4ToLC6XIn5vLWr3CZX6+dO4BnMrHFz7AcfJzrafuayO23KBDZCSCPYdYz3mnwFDVevix++Tc30WM8OLsExOHPjrKF8CuOwJUui+N2heAFv/UE3slA41cdwhc539kxD/Y8rn8Me5+yoRdFfztgNKFUg01aG1Zm8A3J6ZJwt6wlsJ0cmF+tIzIX1xsRuzsmsR8CNzlJzxFcHrKmuzMrlQaUuPLkwxkmiLipYOvlNh8aTlp2Zz75miPI7xSXVnXfk1coIDqq0tIU/uGBhpUOVfZkBi3v+ehgYkhNECh8kL7cshxR0QZDG3TibUJfOmJ8S11qctkypAOqrgPvjK60vzYhW5vKxVhJtd2u6xVdGzsi5n9Hl/EpWElVefncrGp4+IK7lTBnjrAqCJEpwrRn3PfOohdCSclz+VW499USmBTLrXUzZe47jjN5/hz3P7YNBfcgYnL3efOi+Dku0gJ/VHqJacpPVSr6KSAT4S97cvjbK8lcn45Q6tELC8D/L//V8CuXQD/+3+v3XNeJ8U996xeJjMRjrjPHRYTm5cq5l11cMKWUkKeANIGHKl9kuZLy2Gqcqjx+cqxDntVSOLQXWnU8VLCNVSfS+xz+0HVRbnoLqEv7VdqmulbyoRjkxeK2Y+J1VeRvzbgmqe5qD0Gv6kM6yCgyhh8YxyPj/dW0FlcBDhyJGszig9jn5kVdIyTD7I7V+aiZosFLCaqCkeJSc/pxucqU8cdAsln4MvL8fAuST/qrMMFV0QtLYXL+gYS1Hr1KaEo0jj6WEddkiZ1+rmTeqtw9Kn+4D7YhJz90OcQ486nhn4p1RG6hjY5QmQgDr5k3XWl2WDjeHGxXBNfqQnqIVeOq4TLQeSIqpyOdkx6SuhKjICvUtDXJfBT5k9IhHYuUd700B2Da4UXgHhHPSXNJTx9jjrVL0kIT2yYj9Tlz90nqo/Y3fc5+5wJxaF9fKib3y5cc0ubIvhrFfi+MBulGlJGl9R+5sfHDvs2f8vLpbifnk7psSJidra3gs6xGPzYz9tc2OyLsFQIpwr/KkVzTN9D4UpVnZ+Yvvj29aVz86Xlqto/hZQ4bpPuCrNxpYXi6DnC1ieaOS471c/FxdUuf0jUcgQoV6TiMrg/rj5VATUg4n5GdnoOR1/d/GYRY0Zz9hm6EB0JOsk2D1Udt/khHhnpiX3zgzQ/X0mTiguGe58D+0JVpXAFGJyor1rQDyLUx5fOzeeWqWLfugnNNYot73KxOUIyxp3m7MNxr7ntcAYhof04sfI5nPvQwKUowvH6VTv66uYPL3Xp0dqXyUzZR7IdKotZC0+2zRkaNTrac1dMHL75MdIY/JqxV9DJiEvQS4R+rBOdWzjnFPJVDyI4dUjSfOmhPEOdTyLOje/hSDbSNdNDMdkcIYy3XfHzPmEZ47JjcHlOmFEMnMEQFQLFWZ6TGgj5tjl9tdsH4IUT2ek+R1/d/HYgicHHZvOgGegymYN88JVL0Dflg6mCmOUyfefDdu5xuM5VVxVwwgkAl146vOdzkBQ/+lHvAVdoci3G/hzNRU5yscsh9AHkoppTr6QPvvZztmNvV3HMkjRfehUTbUNtDgqOQHKdj5wx9JxtgLCzLnXVpds5nP2cZVKc/RyuPvd8UI6+uvnDBaWjQoayba7WqTEHGqLTBGE9zIIeIP2OBPWlNJEgdnjO+DjA+vXlnxH9U1OpvVecGHFvTvqxWyj4s7YnAbm+B1yxHyP06wx5ySXouWK+qgFEShmqH1R/OPWklKurHgMnLpxbnooNB4gP2Uhx20POOi4rHUyEtiXhRDEhM9IyMROHXdtUu6Gy0oGXK83OU5HfbHKYz77okio1aGNi8Otw8KlR1DCH5ACkf3nw/rYDPDra796vXw8wMVG+zs5qqE7lGPd+40aAbpd9KxFfuKRiiyOGJcK+Kpc+tk1uOa6gTx3EUGmp6/z70qVlcu4XCyUGQ+WlziqVVmfYSEhwx7aby9V3iduQ2I1x+an+cPpk90fi6Fcp9H0DAIyK/MGRK8R56Bz8piyJ6RpFGeHTtPip3JhjzPVFNU6++bMFfrdbCvwrrijgaU/TUJ1cFLffDnDoUPlLb0ZVx9z7Kn/8Q6KzauFbtdB25eUYQKQMWHCbVLuu/XzCMSQqUwZ9gyb1/0ASfhNyuGMddI677BOZEvGeQ+iH5gjY+/vSYvZzCX1Xn3KeG2oybmzYDvd7qyK/XnCkiXTBF2q7LmoR+DkEs3RVnZgVd4ZV2Btca7ZKsX/s7Em2Y2OlmTwxAXD0aJk+Owtw112pPVdWOHKk/3bJ+DgU3bE+QWo+H1esoHHxTXmA3j4hRzIkJF0COIfgTRX1Ma49R9DnOB7clq+9mDRfeigvptygSRFBsftKwmmofV3nNjQQ4Ah9aj+AdKGf4uZTaZLBAE4LrXwjPT7uoCDHnQ3fcVGoyK8PWzO6NJR0AZi6DOXGhOiEwCfBF7/PzaO2h40qQ5DsMJ2REYB16wAWFkqxv2FD7wdo//5Kml+bzM4CbNnSC88ZHydFZNU//iGhV4fgrqsd6UClSlHfBJFvSF1ZpwqoSaAYKsbbTsd5HEc/Nl471kWOFZpcMcsRo+YYpSv+2O266o9N47r5dh+kwr7KcyFBRX51ULopp/vuM99yUrvAb0q4DmZYQ3Rct5ckD2JwffFMaE63W4r7paXSWJ6cLPMfeKDUow88kOVQFMPGjb1bJePjsGw9SIf8wUdXEuzi28XM8qdYaLocRpMmEcRNFdzSNqTvUybhxpYJpUtE+iCXuuTAWUpRilTcu+rg9FHisKc6+iFhLxG90pAdipwi3/RJOik6ZsCV41y40ux6XajIrwZK71BRIFw9i8tS+1ahO9dUDL7S+xKFvpzU6NUl8kdGSpFvh+vYi7ssLAD8zu8UcPrpAJ/4xPAMnuqk+Nd/LR8R3On0ufdLyyNe902CfcEyKyRJXV0bn/gOlRmU8G6asM/t4IcEcIqIT/muuJB8l6m10ykosQfgdpeptNhtn8sb67DjY4hpi+PeS35bpGv3S9IoOCI/pl/cc03dSUgV+VxU5DcDbBK79NXQTbJtiyPeln7GIhlo2V9Ul7A3F6GxsfIHbd268nViokzfvr0sNzVVho7ff3/qEaxhlpcBdu4sT+727QATE7CwWIp77OYZVn5kzIdlqQbKxbcvFEYE+RwsitAE0JCjHxLeHDee4+Q3YdDg25+z7UrzifiQgK9CpKfA7Q8lcKhj9T30yvfwIpeja9K427bDbvrDnRzLdZFDbn5oQBHr8seEqMQIU9d+OUU+AP/Ohtn2nY+UOQq4HIWK/LxwQnRiH7RK5VW1wmNrYvCrYtiXybRxzQBPwfywmbXvjZvf7ZZRJLOzpSZdWipF/h/8QQFnnAHwgQ+sjXOeSvGd7/Sc+4mJXnjOsdAcLF7N58H5scffffsiRYXqSBz9kBgN5fsmt1L7x7rYTbgjIMnzpUmWzvTVE6LKQUCKi8nZnxJ8oXqljr6kPhuX+ObWx92nynak59fXTp2E2o39jOsa9CgKRbTA565q04bwHKqPayEm35fnekCS66moJnIEoAzJWVoqDeeNG8tnMu3fD/DoowAPPQRw550VHNiwsndv6dhv2LDi4M91N8HyfE/Q2S6d+Sx6QuTY/6TjqjECxUoZG7O/qdNOo1xMFz5xGuvc57wjwKkjd92++rnbUjEf+1nVDdUHieDhCCSXCMXiS1In1+XFZSUOuzR8x9eOr+/SUCBcThKeMihB6/qsbXx9zfUZh9qx0+z6Of1V4omJwfeF6ITKUu3m0J3RXwdu4014Wq3CwzUYw19MjPlRsd37devKGHx7Puj69WU5fcKtgOXl3rJExyY1LCyUT9csivJiYX78i6J8j/9i6HToC6B9ATGDiZxInPscdwQ4dVRVt6ucbxu3R7WL6w61nfI9qZKY7xfrzlVLL0M577ZwJyNzv8PDQO7j89WHzz/nrl1qm0o6PsPaF4YTU38OczxbiE7sspVNA6+LT61fytmWrL8vKUttc4+Hc0fClOfefcGx+KOjpQ4dGQHYvLkU+zt3lq/HH19Gm+iymQI6nXJZzMlJKLZshdlZgLm5Msv+MTchUouL5bk3y5f2qll9d4ZqimJpieckxq5gYuqJce45gh/f6Yipg3rvmjwb49rndOy5g4mmUoWoBwgLe5ejW4Ur6nNbBxGqw+0bp31uO7mp43OSOPd4O5eTry59tbh0UigGn1Mntc2th9KBIdgCP5fIxNuSJ4HV9VQw7lKSudbfl67VLxkkUctkUmVC6aHP2/6B63bLVzPp1sTib90KcPgwwK5dAFu2FDA7C7B/f7MHfIOi+L//tzxxGzaUAn9iAubny3CnxcV+oWaEvXE8bcE9Nta7KJi/0I8EFWtPhepg7Lh9O3yrblHpctddZUL5IRczt9CuStw3lUEJegC/8HS1Q6VXdQzKaiTnzvUdqHLAIRHhMYOfUP06CEhDqpOqbDMVkYPvEnkc4Z3y5C+8XUdcfxvmDsTgi/eixDxH5OMVBMbGeiK/2y316fJyGUq+vAxw+ukADz4I8D//U9VRDgGjo2YktBJ7P30EVkS+Caswot04+OaH3WwbsNAHWC32TRp1gVha6hf5NtRgIGW5RV9ojkR4h0JnYp17XLevD1R6Fc69ZMAySCTCI6eQBwgLKV97KWJfKrbsfqpQ4yE5T3XcqfHd6fRNRA7dMYhpX5HjMjO5cfaxMfi5EQl8zkFT5TkuPOdBAIMC33HIETrTBHxfMF+oEoX9Y2Kc3G63FJYbN5Y/OI97XJlmYvAPHQI4dKhQF9+iuOGG3mo5O3cCbNkCxeRxcPQowNGj5Xm1HXx7Um2n0wvPMdtG6JuLigndcQl5jJ2PxaZrIpidP0j33uBbppKzP04PzRPg1hND0x82ZRiUkOe4oZz2qhL7OA0fU8wdBZzHPZ91hM2E2gkNhlI+qzrnWkgFtXSd/NT2FBkhMxu/l+RRZaW6kauNK1kmMzYcJSUUJRbuspGuDy8lLitETEx+yqAjNOeAU58tKhcWyrSNG8sfs8c9rhT8U1PlD9Thw2XoDqy0ubaFfnHDDeWJm5ws/7ZvB9iyBQ4cKF37ubnyvC0ee3KtuTDY4TedDh3D6xL6Bq7Qx/H4Js++4FADgSonsKXWHSvGc4fJcNz7WELhVTnqzlW2TiHPKVeV2A8dZ0y7seVc++RuJ1WUpg4Gpe59Sn9TQnU0Hn9wcLVeVW3a29xwcRdZQ3TwdpNdeZvUPoXuTJjt2AFLznh+V79xeV96KFTH/OiMjZXv160rX7dtK4Xm9HS5MIwdN37ffc5urh22bStDcnbtApichLmNx8H0kfJOBw7NAaBDbrrH/qONoDeC3A7h6XZ76djV54BDdVwhPaZP1ARAn9BMDfGx27eRDApc+bEx/TGYO2E2rnPDHUTVIQhyiPiUcBpuH7jlYtviOsuc1ap89brK+gYUrjCgmLsE3HZ89cVsu9Ko71auuRYp4N/JqtfI1wFAHK5Qmzpj8M22JIqCIsrBd8VkS+PqhwnqA8L5TSTUL+nAxBZ05sdq3brydePG8nXbtlLcHzpUiv3paSPwe+fv/PMBvvOdZp6zXBS33Qawb19PmW3aVJ6cbdug2LIVDu0vzw0OzaFCYcyrvcKLHapjO/lmboQt6s2EXLs+u37KoXeF6LicfNxHu97lZVrQNoG6woxS73RU6dJz2vXhE/RVxMZLy+Sqy5fPEZ6xYl7SNjfGP0Yc1hkWwxX30jpiiQmtqfrBYCry4+BoHonobkUMvgGPLux0ANmyj1Lnm7tvnYOJqm7jVBHrz50oHZp0y+mDHRbS7fZed+1aWdId1q8v/2ZnAe65p3zaLQDAd78bdXjtotMp4+zNAwOOheYcnF0Hh+4DOHiwDHOan+89PMwW+NTFfnS035E3n8H4eP+2eV6BnWYv44ZDeGzwU25dQt+Uwy6U2QcPPrgOdN2r83Daqqo/LhcfwH2Xw/W5cftXhbCLjS0P5TdJ/MecA64rHevo++4WcOvk5nHbSmnPlZYrJCd3nlRgh1bV0VCdeqBCY1IMa+miMQMJ0UlBMkE3R8w+FTvuaq+pUPHwri9D6ijQ9Tm4Jk9LQnXssJHx8d6k2/n50sU/eBDghBPKtLvu6oWjABTHxP9wOfnFj37Ui08yTwA7ttb9kZlROHSoHOjMzJTFTGiO7eBTF0Fz3u3wG+PWmzL2NkDvwmjfBeh0eE66CdVxrfRg0u2wkpDLbASt2Ycj6Km8Qd4NkPQ1tA3gPpaQ0KfazkVVS1AOIk9SxlDF8XPSJMJYIri5dfryXG356g/Vw9lf2ofcdy2qRMV7/djiuu3UFqIjnVVcFYP+0FJj8ql9JPVIQ3JsMY/3D022xRM8N24sBT5Az0E2y2hu3Qrw0EMAJ50EcOBAKfSnpswk3CHDuPXr18PSxs0wPV0K+ul7SlF/9Ggp6mdnewMhe3IthRHmRtgbB96ce/MQMvu8Y3ffuP+2s2/q9gkazgXIDDpcFywqVMcl8n2inyuuXe9D9dhi2zdXwFc/Z9ucE7tdg8vR9xEaBMSECaROdHXl5dzHV1euybyctjjlcol+Xww8V7DHDiSqDjniOPe5J9KmDA45++aIxfcNBHSQIKMJ5nCutmpz8JUegx5khHCJfBvp4MQISIBSUBrBOjFRCtidO8syMzOloW1WjpmeLlszbN/evodjFVNTpYI3Cn18HGByEhZgDA7sL4X89HT5av7ssBxX7L0NduDti4bJM8LQ1GXEtNkHoCeoAXo3Gew6JEKQEqohke0T6KG2XPvhOwih8+gT/gBukZ/q3AOE00z7ALI7FDlio3OsXFOHeHelp8wDCNUdWzZF7Esce1w+VWhTbaaIew6c8rnXuE/5DJuEivx0BhX2nUJWgZ8rjj423pzbziAFds47Fa7VeyTny94Xr5Rjp/nqcJXBrsO6datd/W63F4e/c2cp4KemSpN73z6A//gPI/JL9u9nHVqzmJiAYstWWF4uj2V+HuDAPaWQn5rqheGYp9QaQb+wUIpH27mnBCQW95SDbxz7+fnVDv7CQr+Tb4Q9dvTtgUCMY4Un53KcKGpCr28/fCfA1EG57VJH37QDwBP5eB/Xtq8MLof7MAhSRU/OPCpNEo8d25eqykr+rySCXrrty6uyXc42pw++fX3pobzcZWLRWPzqGLQBm7P92hx8yWQB7kRQSoTidkJhRG3FN6chRx25z5MtfEZGestkmpjxTZvKvJmZsuyuXaWw3L+//Nu3r7+HPZrp5u/fX6wI4oNHeuLdCPxDh0qBf/hwz6VfXOyJejskxxeaA7Ba4APQDr45/y4H3wwOcD123XZeqE+USOWswBMTcy8pIxX2nDsKPic/tO0qA+Af0Lnyc8IVDDkETw6nPmalmlB6KK+qsjmPL8Wx9/WxCeLeRZvFfczDr5Q8DNoEzkmywM8VZ5+rLGe7qeSKyc9BaNlPm5Dbb4s2gF48eKfTW0Bm3bpS/Nrr5B89WsbkP/QQwA9+APDww+VfG9izp3xdXCx/rOfmegJ/cbEXX2879ktL/WEXHBFrv9rOvXHh8Tr4i4vl+9HR3nMKTBm8D155x16hx47Pt/vjcr5tXCLfRuLk41ech0Na7Lh+3G9XPdR7XK896RULdNe2JA3nufKrIrfIjRH8MaKXIxq5aZL82PLSUKKY45Nu5w7FySXsU1Ym4uRJykjKYTjLZWosfn0M0hTOOcBIFvjDNNoZNKHzmOOhWblxPRgLg4UTQPmDZoTi+vXl65YtZdriYpm2sFBuHz688mBX2Levt5wmONut5/zccEOxMnEYoOdwP/BAuW1E/Nxc+SNuT5wFWB1fH3Js7TRK2JoLhVnT3o6jN2vg23cEbAefeugVFvL4QmTKcp1uLNqp1XVcIt31hFyfuKf6hSfv2nXFvKeEvmvVILwdSsPpOM8mh5sfKwDqFv8+uCu45BD6Mf2sagWiKoR1irCPaS/UB1dfJPVx8zj50rIqsNvFILRtY0N0cq2DX3UMvr3dJiTx+zHnk5pc6+qDKy+0so7N2FgpSuxVdjqdcuLt6Ggp7sfHS2E/OVmK+v37AXbvLv/uvNPZVG3cdls56Oh0eqLaXkPeOPMm7IYS8rZwNRcuW2SbV3Ohs8tQZe3zbFx6uy/2ecdC1NxFGB3thVGZeH3zvtPpCX08IOAORuxwLc5FD4v8kEtFCX8DXqHH3s/1nhLoWJjjmH8A+lkBXOfN5+LjMqFyKeSoN7dQMsSsve4TmRwBSrXrQjpPIkVw5hDcuR/GlbJPimMfk87Nr6IsdayhNfFjUBc/TNO0IbVcOheWwPcdsPRBAJzYeFccPQ4ZoSbODku4jhRuTH6O9fIB3CvtcL6AlJAxP2RmVZ2lpVLgA/SehDsxUYrObhdg8+ZS9O/bB7B3L93TEOPjZZ1moq957XR67XQ6vVe7/yMjZR/uvbdc4rPb7V+KEqD/R9u3TKEt3G0Bix84ZYS0PSCgxKdpj7pomIEGQM/NN+Vst9+IX1vE4oGF3Y7pZ+gCgsvgCbAuNx5jBk2uwQM1mDDv7WU47WO12/OF7Njp9vnwCX3TDj5/KeE5rjJNIKVfoePnfM85/XEJ+xQHOdRm6j5SN5qbJp04S6XlEPpUX6j+uPaNSefmc8vk3C+ENExHCdNmExgjdvBTDloitpuybn5TqCMkR9oGZzlNF9QPkB2Xb1Z8ASid/bGxMrxl/fpSWO/cCXDiiWXozu7dAI8+Wq6db6+4E2J+vlyO0wYLfWoFGeNoHzxYvk5NlYOQDRt6At8W7fa+lAuPxbzZHhvruTimvHHeAfwTVrHotEWTvVymqQeLT/tYTZvLyz1BZbfR6fQLfZznwxbbXJfbFum2OA/hisvnDjB87+1j9YXuAPQP9vA+dprdb1eeq1wV1C0aXEJFKmAo8R5KixG8nLzUfWLajRH01H6xgj12kMHtVyg9JU9SJna/HEvYKvlpyuIsrXiSLQA9cdMXzpOj7LAgPZ6q4/VzrOJjX1hN+Eq3WwoiI7QBevH409O9h74eOVIK/ZmZ8vXwYYBTTinT77mnFN1TU/zjMY69ebWXkbQdfHvb9Nle+cZeWhJg9asRwfbTZm2X3j43lJCxBwp4BRz7z5UPQLdngx1+E55DCXrTR3N8RrxSYt8F9WArLtTEWYngd03ANfmh91SdeKCCH1JFXdC59bnK+OpoM755CWbbdvF9YQ0cce+KOY8Vsr5+SIh1n1OXDM0l6iX9qyO+Poebn3M/ikEtg6uUNEVDpkShiAV+SEz7hDiux9eGXRfepupwlR2mGHwJvjscvmVGQ/VRT3mLicc3+H4Q8covZjLr+Hhv9R0j+mdny1j96WmA444rBf999/Xi9l1PxDUrxdjC3ghW7N5TfwC9ibIAPefdhL4YIWKEB3a7zYDGpJv3PmLFvcmzz73LJTfvzfHZx2UPchYW+u9GmHxzzBjXsYWeXmv2dcXWu9bYD72aspywHdf54eaZ48R9BnCHc1GfD0YyoGkSrv5IBk+UyOfCEffUYMDXx0GK/JgHecWKeipNIsCrfAJtncI+VdBX5d5rmE4cTVrAJAfJITqxy1vmXBYztn/DDOdLmmvN/NS6AVZfPG1hZ8S2EfbmoVDm6a+Tk+Xrtm2lo79jR7lqzdRU72FSR46Ua8+bfY37ji/y9qstgClhbwsDW1DbAwNbyJtjMa9GNJty5j1eltJs23VJRH0onxIzFNQEYTNR1x7A4D/b1edMHKNEPhfOEpwmn4pnt8OW7PQ68T2hVtqXqifhSgYN3LK+QQw+HnsgZUS++Y75JlpjfBPYcR/we2rblRZCKvhC/0s5xHFqWcldBGl7qXmcfGm5ELlFvfQ3UsW/G0q3tDkipNYQHaVa8F2LOuG0K11lB6DncJv8TqcUwuPjsPLArE6nFPKdTjlpdt260knesqUU+tu2lY7+4cPl2vqzs6XzbK9u4+sLjqcP5YeEvXm1Rbw9OMDiHw8A7LI4xAe3Yy6iZoUfO9yoa/33U6v3YOzPwRZPdmiVD1NvqBzeh+vGYyfeNQkXgDcR1/TVtewlFp1UnmvbLm+nAbhdfgD3xGnf+eMQ6+5Twjtn/Xh/33mNwd7fN9ANCX7XfgDxgk4SohErYqscAEgnJ6uw76HhOUoOGh2Db7Yly29y6m3raCyEayUhDilhNq79Y8rgC6i5iBvhZuLBTVy+EfKLi6VLbz9Myjw11rzOzpZ/c3Nl2tJSz82316UH6I/J9uGarGYLZWoCrWvbFu3YqbfFOuWUA/SvW0/l4/ZwWfszsPtvHxvAasFmQnNwDL6rD/ar61wauC4sxnyGo6Or172n6sYi31Uf1XcJvn5wylJIBk0Aq7/XqQMBrtDn1udqwzWYMq8uF9/VX4DVYWuSV7y/IUbIcpDsm1sYxwxg6nLpc+RLy2F0smz7oTRPm/Xi0Dr4PiE/zCI/ljqd/5ilNE2afYG3BbGJzzcPxrInh5r13I3It8X/3FwpBswTZ5eWyjZsgW+LEE4MNL74YzFNiV8qjQrNAXBP3HWJaVMHAL1uvUTY42P1Odlc0WnKhwZTdnlfTD3G5ejb/ffF+5tyrnXuXcfvc+opQUyVw+k4z0AJKd/5d4mR0GfAHXDYuMrH1EOdW5fID2GfV+o7LhX3kpV2JGVy7Bsrfn375Qq9ScnLkS8tZ1OFqFf3XslFrQK/rph86dr8a5WQyK5K8MeutAOwOhwDoBTwy8u9VzPp1X5qrBH45tWstT83V5YxT5o1jr4R/HjdeCoOXXoMtqj2uemU+AegXXq7LtxGqojnXHzxIMgW7b7BBfUqQbIPdvRxPS7hb+fj97heADqcB7eD+085+lR6KC8G12Rfg8/tj+lDjOD3DZpywfmf4ayy4zInJP1IIZegjXHmQ/VWKeS5ZSTlDFW783WLeo3Dp3HF4AO0UzsOrYOvhGnLjHEsrnzp2M3DTv78fBnaYwt6I/rtwQEW+Fjch1x93H/83iX6KZFOiX/XvICQeHZNJsTvqe0QrvJ22ERKPbgM57xTFzLfk2x9Dr7rfWi9e584DcXj+8Q0Pq5QPlUGYLW4cMX9c+6wcD4XTp+oMvhc2J9RrGCx/98Mvv9XQ+z/T6yoShFjnH1DAjZ2Mi+nfRX2SlPwrQrYRgYq8KUx+Hg7tSzuw1rEF8aUu42UOrG4srfthzEB9OLojVg3f2ZirZ1mhL4t7O1X6g+g/+FQWKBwnEj83vUALLucVLRzhIpUhHDdVyyYTb32k3PxsVIimwtHiFP14VV3OK69771pi2oDO/yufWJcfdyXGMy+LsHveyqv7y4F1YZrm9M/12Ae57tWbHJ9512DaYDw/1jKQDmXkxojRrkCsykiPeY3gUsOMV+lYFfHfTC0WR8OVODnCtGx04yox9uSupS85Bws+Fw8g1nRxQh+I9hNWbNtytqC3gh3SuBTcflUyA5HtPhEQIzr7hIYPuGRIgi48fIU1ERIDDU4kMIR5bHr37v6g8uZNgz4QVdYKNv7uRx6n3MvcfW5Awh8hwKgf2WhpuIT951O73Og/hddg2+usI9173M6xjFis04hXoXrXreor8uBj/n90zAcZShCdHTt+zy0LdbMJZJtV99sj4+vDrkB6Al9W8T7wnKw0Dd59qtB6qaGnD9KdBg4E/xy/Ni7BjLUqkOu89Hp9MoZR59au5zTZ4mrzRHonJVyfA61ayDhA4fEUHMEcuM6B1Q6HqjYjr7PyTfEnBNpH2NxhcOZNO78Ed97gPh4dgk5zkkVYroJQh4gTcwPMpSmrjtACk2boz6GQuAreaDumrQlRh8gLAqNuDBiEru1lKAHWB2Ow3XvU8IluALdd6zS+jFU/+1jpkIlAPyTWSmkMfoAYWEv2Rc7+gBuV9/s5xosSMXn8jIdEkOJEYnYldwFCIUHUXckKJGP+8q9k+UbLEnTfW48fnWte2/2wfNhcPs+sQ8QHoTjNnOTWqd0/6oEeuxxtFXQG+r+/BQ3voiSJtMYgR8TVy/FFZPvWptfaafQp9JtYYPdfYCwKx96teuw67GJFfySH+rct/jxcVADGDwoosQ+gF/s23nSNd1tpMKBcvcBaKFv+skJz6G2U6BEv6svPheeSg+l+UKbAFY7+aGwKImLz0ESDmJecXiO7eDbzr2dR5XH7ccum1mlGMtVd0w9dQh5gPaLeYOK8ubRZj3YGIFf11KXGs6zduCEZOCLNBbsVMw5J+7eJWByChuK2AuEr1+uQQw+Z9TdD1yGI/jtpTVjnnqbgk/oA/DFfh24zo3k7oxLyLu27fe+ux0Skc+9G8Nx0QHo50LYZWxxT/0ZfHW46kxdbrYqctcfW9+gRLyhCjE/yN8AHRBUT5tN38YIfKV5tPmLbeMTMa6yLhcTwL8qCi7LSa8TSThESOABrA53sh187ObbYFGIsdNw+A6emOsSiCkXv5DQN/2y++oSxr60FEc9ZbUbqbA321w3H39nOE5+6ueInXeqDvPqE3p4gGDXIRX2VN8ldxzqJGebgxbyAMMn5pXBgVd8bIsuGgqBzwnv0WUy5fjuorQhZIeCG9LgE0SuH3mfgHct2Yepc2WS0IAjFIMPsDrcCS876ZrX4HP2DeZc4CeTpoTwpIJXxAEIu/pcIV8F+FxxBg+ubd977ObjkJ1QHWab6iPVd8pJdz1Dwv5zPfXZl4/rA+A/TyJG4DdBRFbVhyaLd0MTzn+INvRxWGhzlEcrBb5LXOJ0e9snSNs6Ohskrtj8tjw8y4YSPa68UD0hhzzVxc8pbkOTYEPHQ+WZdFvou1xb290NOeSmn5RAwHdUDLGfIxfbuTZQdyU4Tj4u60uT7Bv7EC5piA1286UhO/i4fLjEvZ2P/+xj54p7XB+G89yJ0LHl+F42SexV8WCoqmPkm3T+FCUnrRT4OQV4m0dng8Q1CXqQ4j7XhOCQQ++7IPgGC6F9Xe3F7s+tk4ISzdRqMvg95dYbwW8GFK4yALRI5Q5qXOVca+3ngBL5AHSYDADPsea6+5JyvqfV+lYJkrbnis3PJaKwiHY9NK1K5z7nw65yisuqn7qagyavG990hvGY2kCbF2JppcBXmsEgv+ShB5hVQUj4S/bJsW/uOinn1+BbTYYj/GxnHwshytG366bcfQkuhz8XLpFv8C0lCeAeBIbSfeU4zr7P1fcNLl0OPE7jrrRjkHxv7W38ACqfc0+92uIet2GnxYh76v+C83/dBrFOUfeKNE0Tvdz+SAyHmGNs2nlpO20S9TatFPhVuMQaoiMjtC4sDnty1ZHzs6Qe1FXHHQXpj2nVoSMugcbZD4An7gx4pSFcDrv0Jmbfjr2396HuBJhX6rgkoj9UNkaccNv3TSh2iVxuOucOABXv7nrCLncpUOk8AjxRGp8LTr2u2HeX8x7j3FN1mDZdwl7q3nMFfJOWcKySpgrSqvpV5fE29Vy2lTZHebRS4Nct4JRm4/uHa+KcAK6LXlUboXYocYjzbJcWwB137QtNwBNsjaDinoNQ6IkUat+c9QO43XwD1z33leek4W2uox/z3vQ7tOQoPlaD64FRWIzb+0vFva8Ou027nNS9D4n6YRTzbRGbbemnokhopcC3yT2aatPorG2kxOtT+3I/qzYNCHNdaEIiWXIr2RVm4BOMtrNPufr4lROvj18p0esbIMSI8ypW7OGIfAC+0Kecfk6ay9GnhH6sa08Rc4fJJ6Kx0HfF03ePXe3McbrEP67HbpMaVEhFPVfIq+jko+eqhHse9HzxafPKi60X+EozcYXwuGLnqdV4cHrIjef805k62vSE3hh8IRax9fgmDPvi5jkTLV0O8NISvca+SyS6wlMA/MKq7qU3QyIfICz0uelSR58r9PF+1CCAM7jgQIloSnT7Qm7sfaTi3iXqXeJeKuxVcK1Gz4kMPV/V0RZBj4kW+HXNLB6UGKPW0FfyQJ1Pl8teRfybb8Ax7HAuAq5BALWvayUYex/K2cdiEYt2SjRSa+zjV58opvptGIT454h8APcxhdLtPJ/z7yrDEfoYLN65Aw4K3IZL5LvWv8fbOCTHOPq4jDn2GNfeFvau71SsEFMBp7jQ70Z1rMkY/LoOOjSZ08a1dCO3HSzo2/RBDhN1n/dheYhXLlwOuK8sZxIoFQZiynMceuzsU0/QtdsMiVvXcUnEv03MQAA7vTGhO6G7LD5RT6Vxhb7pr09c+EK8qP7aea407NbbaS5XPmUyLUfcu+YJhI4rpozSPCSfW655VjFtx5Rf67QpJAczVCE6ksFAaH8Obf7gFTdrzdUPwRH8rjIcZ99expIKx7Hrc4lQU4drJR/cpgtf+EhoX9dAQCL8favtGDjiOCVG31VGsnSq3TZngMjJw+64S4T7hD1VxnUXwO6D6zUk7CXHpwyOOj+LUFsxd7eUavE9DLXJOnCoBH6Kg4/hhOg09UNV0mnTxNy6kQp+joA0hMS+T7zjvlAhPS6321cPPibOBdguw7kD4HoiLsa32ozdLnfAxYmHl8TMc1YiAug/Dt/qMtTqNfZ7SribOmOEPVUn9Yr7zRX2KcKsaaIuhxNN3Wlb61R5HvQcx+Ezj5usA4cyBj+HIFNRpxjU0ffjCxGx86UTLl1i39UH3+RbvF6/vQ9+T21z+utyzX3lDFz3PxTS4/osJOmhz6cOQUbdCeC8Uktfhtx9qpzv1ewLwBP2kvPUJgGWq69tOmZl7dFkAR9iqGLwMdTgIGbCrm+FlzZ/+AofdfR5+MS+1D12hfHYdVEuPJVmHrCF830PX8L9d/U35JpjQgMiXDZUxhUCRD1MylcnlR4K+QnlxeAS9/Z7HCITEvV4H85Dq6oW9ipslTrR71scukxmQwkNDijRxnFrqfXYdcUdRelH6uxTaa4wHt/Smz5nPpTvE/wcsc0l5IJzRDOeBGuwj8Hl+EtceUmITgo+YW9vh55m60rLNYnWFTbkS5PkK4oyGGLMuyYL/laF6ODt1Jh7ah9OPaHBgjLc1LHqDjVobPOdA4nrK3H2DTgExzW4CL03Sye6ypm2bFxOuW8wEMrjhPhIxLivPfvcYUKr5HDqp/DV6Zu4KnXdXTH2nFe7LyHHPvR9rYLUenOv5qI0Ex1Q8nFpyTUXgz8IXAKnzcJHUQDo23+hH5I2f+8p9x7n2fm+uHC8wostwHM8hRUPKFwDDGq/3ITmAEjbpFbHseGuAITnN2B8E2lxHTaUm26/9wl7u06fY0/t7+tzneK+SnEWW7cODNqDivu1Tatj8Dnr4odi8GPcf0n5Jo/ulOYh/b603d0Pxa1z3H1KgFN1YbEaaluyTTn/vn1dfQJw3xEI1VVFXrcbLhODRBSHRD4VPsMJuwmJel8oTk5R3zYR1rb+No06Bkj6GcVBhV/73lPbTaJVDn4VtFUYKQr1RN5hEfzSCao+15+KVQ+t4Z7L7U/B5Z7HXrx9feP0OzSngtO+NN8l7kNCPJewp/rlOo5hF/ZKHqQhdDnqV9YmlcTgh0Y43Lh6vG2LFq7zjsvkFj5NHbkp9RGzwk4V3xvf3a6cz4ioA85FkBuDTg0aQgMJbsiNaylNFxKHP3RXIJTnW5Ped4eAU3cqkhh8gPB6+Hg75PrjduoQ9Sq8FBf63Wge3Gt0kzVgNgdfEkrj2/ZNYMUuJc4fhIhxLaGprB1SvmtNvr3XNDgOsq9MKLbfTg89tRWAPwGVQ6y7nurKA/iX2aTgDA5sOPH3vva4ITu+PF9sfcyqOCrsFUUBaPY1PFsMflVIBgOckIWqBwMan69wGISLjgfK+H2biHX4TbmQI8tZDx6D1+rH+AYE3JVzpHmcMpw7BVUidcdD7jre9gl6zv7cPEkZRVGajW+VRbw8Os6PjWbJTeti8KWinTqhuL46UDGvDBLO0q7DEr9v4Lj8rrKuulLEm29FmtDE2pBTb8hVBpND/HPPnVRgSwU9tx5OfyRlFEVRbKrWhWyBnxpHnzPmPiTSByXibVTQKyEG+R2RhNS1dfKuZDKbz1n35Uv7wxXKvvh5GzwoCB2Hr0yOfbjkEPuhWP1QPerUK4oiAUeGUDoVl/dtp8C5FrMFfkocfa4wm9iwm0GEIYTa0wFAe8n1XfL9MCjVIF3BQuJq5xR8MavxhO4QYEKx9IMQsLHr5RtiRbsKekVRfFBLaA4yRIezX+NDdHINDnCZQTuROgCIw+Um+wZxkgHfoAeHTZ6wM6xwXG/Jfr79Je55nYKS+0CrKvAJdopUMa4TZBVFSQULfld+aDBgb+eOz68kRCdlX98IqIpwnkGhIi6Oqgdxg/6+NPF7EVrlykdT/t9i4Ii7WPc/VF/sGvy4nph9m0bV68vXdexN/N+umzb/HiiKTcgMzG0WhuaTUmRx8EO3KST7UnnSibJN/BHRH/f6aOP3Y1jDdahjaeL5jyX3xFSJ2Mw9uGgLscdW5TkZpv/ZKsl1nobpN0RpPzm/1zl/S7LE4EsnFkjCbHz1NDkGH5PS/jBfPHJ8LoP+bHMjPZ62fT+aEiZXNbkFZZUx/02i6uNq2/+Lspph+gyH/XdwWPGF6Pi2c4n4oYjB9+Fb+s8XvtG2fyiqv239gWvbuW8DbXX/2/i/OEhShG+da9wPeuDRpv8BRUmNdMiN/bscMk+VZjNSFP61FNatWwc7duyoqz+KoiiKoiiKonh45JFHYG5uzpkfFPiKoiiKoiiKorSHIY3iVBRFURRFUZS1iQp8RVEURVEURRkiVOAriqIoiqIoyhChAl9RFEVRFEVRhggV+IqiKIqiKIoyRKjAVxRFURRFUZQh4v8D2J6x/z753soAAAAASUVORK5CYII=", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" }, { "data": { - "image/png": "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", + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAvgAAADpCAYAAABRN0P0AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/bCgiHAAAACXBIWXMAAAxOAAAMTgF/d4wjAAB8nUlEQVR4nO2de7BlRXX/1zn33DuXmTvDMA9+44BAAF/4IJGYFBHElIgmPiKIRSrEaHyDJL4SH1HBSmk0Faig0SqIDxJfSZUmpgxRCUZ+kcRH9GeMAUWFCa/IMAzDneEyc+fcx/79safv6dN3re61uns/zrnrW3Xq7N3du3fvfc7e+9PfvXbvTlEUBahUKpVKpVKpVKqxULfpBqhUKpVKpVKpVKp8UsBXqVQqlUqlUqnGSAr4KpVKpVKpVCrVGEkBX6VSqVQqlUqlGiMp4KtUKpVKpVKpVGMkBXyVSqVSqVQqlWqMpICvUqlUKpVKpVKNkRTwVSrVSOszn/kMPO5xj2OVvfnmm2FmZgaWlpYqbtWw3vOe98BZZ51V6zpT9brXvQ5e9apXNd0MlUqlUkVIAV+lUmXV+eefDy960YvQvLe//e1w2mmnRdf9zGc+E971rncNpV188cXw4x//mLX82WefDXNzczAxMQEA7QXvXO16+ctfDr/9278dLNfpdOCrX/3qUNo111wDH/vYx5LbECtu21P17W9/G17wghfAjh07YNOmTfDkJz8ZrrvuuqEyjzzyCFxyySWwc+dO2LRpE5x++unw93//90NlfvCDH8AznvEM2LBhA+zcuRPe8573gP0eyaIo4IorroCdO3fChg0b4BnPeAbccsstlW+fSqVam1LAV6lUWXXJJZfA9ddfD/fee+9Qer/fh0984hNwySWXiOvs9/u5mqdSDenBBx+EF7/4xfCDH/wA9u/fDx/60IfgDW94A/zDP/zDSpnLL78cbrrpJvjmN78Js7Oz8M53vhMuuugi+OEPfwgAAA8//DA85znPgac//emwd+9euOGGG+BjH/sYXH311St1XHnllfCJT3wCbrjhBti7dy88/elPh+c85zkwNzdX8xarVKo1oUKlUqkyanl5uXjMYx5TXH755UPpn/nMZ4oNGzYU+/fvLw4dOlS89a1vLU466aRi8+bNxVlnnVV861vfWil73XXXFccdd1zx4Q9/uDjxxBOLmZmZ4rWvfW3R7XaLycnJYsOGDcWGDRuGyhotLCwUV111VfGEJzyhmJmZKY477rjiAx/4QFEURXHTTTcVAFAsLCwUn/70p4vJycmi2+2u1Pf1r3+9OOuss4r3vOc9Q23/3Oc+Vxx77LHF4cOH0W0GgOKqq64qfvEXf7HYsGFD8bSnPa34zne+s5J/xRVXFE9/+tNX5h966KHi1a9+dXHccccVW7duLZ773OcWt912W1EUBdkuTO9+97uLxzzmMcXMzExx/PHHF5dddlnxyCOPFEVRFO973/uKXq9X9Hq9lXruuuuuVXWcdtppBQAU09PTxYYNG4rnPve5RVEUxcte9rLi4osvXil34oknFldccUVx3nnnFRs2bChOPfXU4l/+5V+Km266qXjyk59czMzMFM961rOK++67b2WZQ4cOFe94xzuKk08+udi8eXNx9tlnF9/73vdW8r/2ta8VZ5xxRrFp06Ziy5Ytxa/8yq8U+/bt87b9W9/6VnHOOecUW7ZsKU444YTiXe96V7GwsMD+LTj6jd/4jeL3f//3V+Zf+MIXFm984xuHymzdurX427/926IoiuKv/uqviu3btw+14+qrry5OPvnklfmTTjqpuPrqq1fmFxYWim3bthWf/OQnRW1TqVQqjhTwVSpVdl111VXFzp07h4Dn7LPPLl7zmtcURVEUl112WfGkJz2p+OlPf1ocPny4uPLKK4uZmZninnvuKYqihPaJiYni1a9+dfHwww+vQOs555xTvPOd7xxalwv473znO4tTTjml+Pa3v10sLS0VDz74YPGNb3yjKIphwC+K1eBdFGVH5NGPfnSxtLS0knbuuecWb3vb28jtBYDilFNOKW699dZifn6+uOKKK4pt27YVs7Oz6Hqe//znF8985jOL++67r3jkkUeKN7zhDcXxxx9fPPzww2S7MH3yk58s7rrrrmJ5ebm45ZZbilNOOaV4+9vfvpLvQrqv/TfeeONQGgb4J5xwQvH973+/WFxcLN785jcXO3bsKM4///xiz549xYEDB4ozzzxz5Tc2dTzrWc8q7rnnnmJhYaH4i7/4i2L79u3FQw89VBRFUezcubP4xCc+USwvLxeHDx8uvvGNbxRzc3Nk22+77bZiw4YNxd/8zd8UCwsLxZ133lk85SlPKd773vcObYvvtwhp//79xY4dO4rrrrtuJe1rX/ta8dSnPrW4/fbbi8XFxeIzn/lMsXXr1pXOzBvf+MbivPPOG6rn3//93wsAKPbv31/Mzs4WALDyPzR69rOfXbzpTW9itUulUqkk0hAdlUqVXb/7u78LDz30EHzxi18EAIBbb70Vbr75Zrj00ktheXkZPv7xj8N73/teOPXUU2Fqagre8pa3wMknnwyf/vSnh+q5+uqrYWZmBtavX89ab1EU8MEPfhA+8IEPwC/90i9Bt9uFLVu2wJlnnslu+4UXXgjz8/Pw5S9/GQAA7rjjDrjpppvgNa95jXe5N7zhDXDaaafBunXr4PLLL4der7ey/bbuu+8+uP766+Hqq6+GHTt2wPr16+HP/uzP4NChQ3D99dez2wkA8NKXvhROOOEE6HQ68MQnPhFe//rXwz//8z+L6pDoVa96FZx++ukwMTEBv/M7vwO7d++GP/zDP4Tt27fDxo0b4cUvfjH8x3/8BwCUoS9//dd/DR/5yEfg+OOPh16vB5dddhkcffTRK9s5NTUFd9xxB/zsZz+DqakpOPPMM2HDhg3k+j/ykY/AC17wAvjN3/xN6PV6cOKJJ8Jb3/rWVTHz3N/CVb/fh4suugge//jHD8X/P+UpT4HTTjsNTj31VFi3bh289rWvhY9+9KOwY8cOAAA4cOAAbN68eaiuY445ZiXvwIEDAABoGZOnUqlUOdVrugEqlWr8dMwxx8BFF10E11xzDVxwwQVwzTXXwJlnngmnn3467NmzBw4dOgSnnHLK0DKnnnoq3H333Svzxx57LBvsjfbu3Qtzc3PsUXUwTU1NwStf+Uq49tpr4XnPex785V/+JTzrWc+Ck08+2bvcz/3cz61Md7tdOPHEE+Gee+5ZVc6k2ds/OTkJJ5544tD2c3TttdfCtddeC3fddRcsLi7CwsICbN26VVSHRI961KNWpg2Iu2kPP/wwAADcfvvtAADwy7/8y0N19Pv9leczvvjFL8L73/9+OOOMM2BmZgYuvvhiePe73w29Hn5p+ulPfwo33XTTECgvLy/D8vLyUDnub2Hr4MGDcMEFF0C/34d//Md/HGrDhRdeCNPT0/C///u/sGPHDvjGN74B559/PkxNTcHznvc82LRp06pnTh566CEAANi0adPKw7azs7Oryhx33HHedqlUKlWM1MFXqVSV6NJLL4WvfvWr8F//9V/wqU99Ci699FIAANi2bRtMT0/DHXfcMVT+jjvugBNOOGFlvttdfXrC0mxt27YNZmZm4Cc/+QmrjVR9r33ta+GGG26AXbt2wXXXXQeve93rgnXdeeedK9PLy8tw9913w/HHH7+q3KMf/WgAgKHtX1xchLvvvntl+0PbCQDwzW9+Ey677DK46qqrYPfu3bB//3543/veNzRyC6cegHIUndwy7vYPfvADmJ2dXfkcPHgQ3v72twMAwJOf/GT47Gc/C7t374bPf/7zcM0116y48Vjbd+zYAb/1W781VN+BAwdWPajK/S2MHnroITj33HOh1+vBl770JZiZmRnK/+53vwuvec1rYOfOndDtduGss86Cs88+e+VOxM///M/Df/7nf8Li4uLQMieffDJs2rQJjj76aDjppJPgO9/5zkr+4uIifP/734df+IVf4OxOlUqlEkkBX6VSVaKnPe1pcMYZZ8AFF1wAU1NT8JKXvAQASnB7xSteAZdffjns2rUL+v0+/Pmf/zncfvvtcPHFF3vr3LFjhxfeO50O/N7v/R684x3vgO9+97tQFAXs27cPvvnNb5L13X333TA/Pz+UftJJJ8Gzn/1seMlLXgKTk5Pwghe8ILi9H/zgB+FHP/oR9Pt9eN/73gf9fh9e+MIXrir3qEc9Cn79138d3vKWt8D9998Phw4dgre97W0rbrCvXbb2798PExMTsH37dpicnITvfe978OEPf3jV9t1xxx3Bcf937NjBHmqUqxNPPBFe9KIXwetf/3q46667AKAcbebLX/4y3HfffdDv9+G6666DBx54AAAAjj76aJiYmFhxzrG2X3rppfD5z38ePve5z0G/34elpSW4/fbb4Stf+crQurm/BQDA7t274ZxzzoFHP/rR8IUvfAGmp6dXlTn77LPh4x//OOzZsweKooBvfetb8K//+q9wxhlnAADABRdcABMTE3DFFVfAoUOH4JZbboErr7wSXv/61w+1/corr4RbbrkFDh06BFdccQVMTk7C+eefn7CXVSqVCpcCvkqlqkyXXnop7Nq1C17xilfAunXrVtKvvPJKOO+88+BXf/VX4dhjj4W/+7u/gxtvvHHF3ab0lre8BX784x/DMcccsyqe2eiP//iP4VWvehVcfPHFsHHjRnjKU54CN998M1r2oosugsc97nGwc+dO2Lx5M/zbv/3bSt4ll1wC3/ve9+CVr3wlGTJi65JLLoGXvvSlsGXLFvjiF78IX/rSl8g2fupTn4KTTjoJnvrUp8Lxxx8Pt956K3z1q1+FjRs3BttldN5558HrXvc6eOYznwlHH300/NEf/RG87GUvGypjnhvYtm0bbN68mQwBev/73w9/+qd/Cps3b4bnP//5wW3l6rOf/SycccYZ8OxnPxs2btwIj3vc4+CjH/3oyl2Gz3/+8/DEJz4RNmzYAOeccw68/OUvX9kGrO1Pe9rT4MYbb4SPfvSjcNxxx8HWrVvhwgsvXOlAGEl+i2uvvRb++7//G66//no45phjYGZmBmZmZuDXfu3XVspcd911sGXLFjj99NNh06ZN8NKXvhTe/OY3wytf+UoAANi4cSPccMMN8PWvfx22bt0K5557LrziFa+AN73pTSt1/MEf/AG8/OUvh3PPPRe2bt0KN998M3zlK19ZdbdApVKpcqhT2PdzVSqVSgUAAD/60Y/gSU96EvzP//zPUOgQpk6nAzfeeCOce+65NbVORUl/C5VKpVIHX6VSqVap3+/Dn/zJn8CFF14YhHuVSqVSqdomBXyVSqWy9E//9E9wzDHHwK233gpXXXVV081RqVQqlUosDdFRqVQqlUqlUqnGSOrgq1QqlUqlUqlUYyQFfJVKpVKpVCqVaoykgK9SqVQqlUqlUo2RFPBVKpVKpVKpVKoxkgK+SqVSqVQqlUo1RlLAV6lUKpVKpVKpxkgK+CqVSqVSqVQq1RhJAV+lUqlUKpVKpRojKeCrVCqVSqVSqVRjJAV8lUqlUqlUKpVqjKSAr1KpVCqVSqVSjZEU8FUqlUqlUqlUqjGSAr5KpVKpVCqVSjVGUsBXqVQqlUqlUqnGSAr4KpVKpVKpVCrVGEkBX6VSqVQqlUqlGiMp4KtUKpVKpVKpVGOkXqjAunXrYPv27XW0RaVSqVSqFRVF0y1IV6fTdAtaqJQftq4/Rd1/vrr+KLnWo3/sxvXAAw/A4cOHyfwg4G/fvh3uveeeqJUXoH8AlWpU1IHyglZAZ2VaVa/aeM5cXq52GW7ZmHY0pa7g3ji3rKTOmPKsY973I2B5blpMGXteUpZaH7cd3OW4Sv2hsXROWpXLxdYtzVehOv7Rj/bmBwE/RdgJo40XsFGTwhdPNqjq/y4sex8p5Dcjzj6v+7+MXXtDnOO7XrvLhq7tpnxOBpC2IUVVg3nsMuzjWwLJWHougM8B9zFgn6tnyfkjd7v4+rB0blqoLtMeN81XhpqXpNmq4iBXVQv4mBS40uXbd2sNykIQr/8zv9ba/2Uc5P5mTfzHsWt3VcvGdDBi6kxVSp11QT3ACIB9lY4+leZLjy0XglmsHJWXAtLLy7J5qs3SOrhpqspUO+AbKehXo7W4P9uyzR0ohtpiz7v/d2xeUtbMK6CrfP+BOo4NLsdIlg0tP2qMkNre1OWToZ7KqwLqqWVSwJ9K86Vz80PLxf54Vbrx0nm7TqmbL4F87QBkVWOAb+RCkUo1qnL/x27IS66ydpp7B0OBX2XL/j80GdoTw0gpHYemlJNNaoN6ABnYS0FfGoJThdMvTaMUE5tGLUPBOLZcTshuep5K4+SpRGoc8AEU8lNl9t9ah7u18h/idADWrNYK/UWoyWeicsJ6rt3oi5SoW7nWXRnUY2kxoTOc9JTOATVPpfnSpWW4wsDepHNDdKh5rB57PrajoJA/kmoF4AMo5KfIdnPbLGkIS9u3J6c4ITprVm2GdolSY1YqUNPx/FXE16esexTXWSnUY+kpYTdSkE8Ff2kaJslxKw2podJjQ23sZTA4TwH73J0AVeVqDeADrD2oW2tKDUtpm7ix8dy7K26ZNQf2VZBdE52DlIsYdfGvSU0DP8Bohua4qvonE58bUt16LE0SUlMX+HPbTqXFihu6E3LoTVqO8JwQnHPhO6VsirRDkKxWAT6AQn6dGoXY7VFz9Dmx9m3d15Ur5YKa42JcFSFit55Tlnflq6/iC2CbhjqO2dQcP3kbGCPqnJEb6nOE16Qsw20PNW+0tISn25K85Cr2hU9USI7UtfeBfhV5Zp6bl+LqK+QnqXWADzA6MDcKinGO2yS7bW4Yyyj+R9q8r7Mrlq6kyzVt7XLdO8ny3Isa5vRVrCYf3JVqlNkg+lyROwxH6txLwV4C+Ny2GVEwn/MttVRdNvjb7ZiYKL9dMMbSJM57LGhL8mxx4VshvjG1EvBV+dT2C3Cs6touLDYeK4NN+5YZecVANXcZTjnJ+qvsAHAvcNyynFjemBiWii6g1P97XM87VSnpPBH6f0vCbbD5GIddmpbi3LsQjwF37nMKQPiYovJDdxBCLn2sgy917FPc+hwdg9jyqhW1FvBH1aFVjY/MRVf/i0dUFdTHAIp0HSnlbUnDcTB3TlJf6ClUqdOvLn9rlNz5jz0uYh3xKuA9FuxDUM/d9hSFjmfqODVtNQ6/2RbK2Y916d22SuPvuXXXoabXP6JqLeADKFhVIY4j3SbV/ftTL2AbhX2VRblh2ZcvvQjH1BVbjpLEhcfWRwF9zMWLGpHDV5+0fAY1/SKuppX13JHDrXfTYqdD36llAYZB3ob40B2IUDo3H1OucBO7XKezOozH14EIufSSZex2xzj00ulY1WxSjINaDfiq/KJemtR2gPUNsVnV+taMJBe5uoA+J/xLylQp6qE6AF66pHMgdfkbuniOo9NfybmjCrfenY915mOAPgbsOVAvOZfEiuvcc9LMPObqG0cfW7cEorlwneLkpwK8ZHl189lqPeCri1+dfMNU1i3f2O9uqAx2AfX9Tzjj74+96oD42AtubJmY9NyiLjRYOieNuhVPpXHaFgISW5KymcU9Fps8V9V2vojtsErcens+Fe5TwN7APOXQc9rvKqdzn+v/74ufN/M26GOx+pI4e+m0qTfk5BuldAhSxfn9tBPQfsAHUMhf6+KMKy/9f4w92EsBt0rnXeIicuYlaZJ8n0JAzLn4SS92GABg66Tceo77j+W5kt4NqFicYzfmetGKc0LKnSiuO2/Pcx38KgDfBftYqK+6k0/VI3XtOfOUo++rM9bNj3XjYwG+DvB31xejFpzjcmkkAF+VT6PsXue64zAKIUksxZzAqgL5GLiQlOO2g0qrQhJHnjPPcdd88yYNW4e7TyTPCdjC9m0LL4gjcXxz/6exxyXnuPOBfu5vDtDHnhuMOGPc5xAVI++K49r7jvmiGHbz7QdxuZDP6RRwjIqqAb0O6Oe2I5ca3p6RAXx18fMod1hO7gupb5hJLETHnsfeKFtVO2tV7Akn1tGWgnYqzMfWh81z82IVcsh9F287v6oH1DhA4bbTt11uHiasfhWt3GAfc3xWBfZ1Qn0dY9zbosa1B/CPgOPOUx1yatpAvllvDOT7pjHFnH/aAuhtUsOGyMgAPoBCfhsl+T1yvDk31AHwlW21UmA0F8hjaanAHrqgc9I57QqlS8twTsKpbr1vWhI3a0t6kQ2F+bh51HqpsqHya0GSYzvXsco9BnPCfAjqpR0NI84499SyuWTHx9tyh7oEWO3uS+7MUdNubH4OyDeKLauKU42GyEgBPoBCftvFgfic4D1S4TaxF59Ux4/Kywnzqfm+ac48QN5X0IdeP4/doo8B/NDFH8vn3l4PKcbdd/O5zh627nGU9BjPeczGHIMxAI99h6Bech6ghsbEykrypOLc1bLbxxnXPgT6XDffXkcdYC+VdgR4ij13MzVygK/KpyrBWFK3BNKxkJxWqu0w76Zx4DoX2McAPtfFq8rBs9XtAiwurk53OwX2MHdckMfgnZNmi7q4Um2g5u363DQ3j8qnFOpEjIqqOM5HAezNtA32Meuz6zD1UNvpS4sp40py5wo7JjgvsMoRUmOH7NjtSQV7n3zlFObTVdE+HEnAVxc/j3Lvw1xhN9yyjTv3kotIDnAP5ecGA3s6FhxCaQD5LvB1wD0AfSJ20zF3z8hc9G3Ad+vxpVEOv53Gcf9T5XM53XyfQr9dkwCR+r+KOaZ9x607H3NsciCeKuuD+hSgD20zleYTVT7UWcXKYp1ZLM03pr17DJrlpJBvxAnXsbevrvCb2HrWcmehgm0fScAHUMhvo0YqXCZGuYE+N8xj6RwwyOkAcurFxr32lfel+dIpSS76vuVDF0jshG3mzT4wF2jsYu+7CPvKpCoUusN19+18XxmOUn+zlHVUVV8VYG9Px8B7VWDPgXop0Kf8Xr5lOSFp1LHgOvohyHfXwwV7Sd2hc4Qvfy0DdxPKvL9HFvABFPLr0igPrRmlFPAO5cdAKvfClwID9rQE3mMduiphPvbCz1kuxrHGnHh7emlpMG07/ZTLL/2m8iRA4ZPbIXDzjKqA9NxQXlUbJP9n37ERA/JuGhfwsbh6KdBzYb5OuMfk+5/6OrSheRfEAfC4ebNM6Ji0y1KQb7cVW77Ku3g5yq31TkXG7R9pwAdQyK9KIz0iTYzqhPYc6TnBAEuLhfpcb6NM7WT5xHkYF3tNfGi9GOj64Jc6kbsufx3CwnmwaQB63k4zwpxOVz6gaqu4/7+UY5rbEcfSON8+0F9awh+YlYI9d5uw+VB6Tkn/w9gxQLn5WMw8tkwonascgBhTR8jcqHr9qlUaecAHwMc9V6WJuy9HEvylsC69GKcul7O+XDCPufKxAJ/TmasKgmPqdR+qA1gNDtS8z+XHHP5c7j7l7NvTKY4f1gnA8m1x/gNVXvxjgTK14845nqRwzwX7UPhNDNBzzw9UGicvhzghOXY6BfI+6I8dy546Fo0kLn5dyrU+yTlj3JTpdxsLwFepWMrlpuWYz51Whfvnc+UlzhyVxsmjVKe7LVXoJTgAfmffp7rc/dDdCG6cvs8Z5eRT4u47TohQimL+15JjRwr01DeVxg3DiQH72HNC1UBPrY/rprtpPhjjQH5sm9dqXL30/zHK25qosQJ89w2nqjiNpCuPSQrGVUxz5iVp3LIhEHcv1AC8F9P46uS0M5dCF8dcAJzjrZiLi6tv0XPGyjbz9rebRrn7Esfedgfdb9M2jpvoEydO3/d/SbnVnyLu8rHHqgTosXwKyH1lKLfet5zk5VWhbaTSXFXVieWE33GcezsNO14w5Rzi0ldnqrDzTmwdTUtyDmhLmwGydMTGCvBtaWx+mux9N3LAL7mocp0niUNVNeBzy1NtynFbvS3uG6VcHQD7wpkC++6y0jdgcmS7+6kOoVsvBvdYpyQlTh/Lw8q4bcsl6X9Xegxwj6cQ0FPfnDQTW89dPuattNQ2UmlGdd2Vw0LpXHHhmsrnAn+KsHpzHve2UoG/TeDsE/b/HJW2IxpbwAdQRz9W7r7C9l3roN93MZFeMCUXVap+7jyVJsnHLoycISiptnFhPpfqPoEuL8d1AHLBPrW86/SHYnVdiHa/bWfPF7Mvjc/3xebHwkysi1/1fzP22MxlMnDPXdQ0FYLjqyP1rbTYvFGbQutCoF8VmNvr545h3zZR54hxVB3mQkUaa8BX5VGrh8nkOGEcl8s3Ty3LnQagL2w5QkC4kMFtaxXQxDkZVnXCdLeH4xK7F33398sJ+7bcl2PlACITykMpJ1BIQ3EosJfCfEr7Jf93X1kJ1FN5ucAeAHfrQ2Avdep922cU+x+OOa5iQlUkrnfssdImaK+yHdLnaNqyT2KEncdapjUD+Ormh4WNRmTSpCFPlb/0SuKAUQDvfgBKFxUrB8C7XY3NU2l1idOe2IswdWHETnjUSbDqkyPHHaYuNmYZH/AbqAgBieQ/0O2urs9ep2/EDOrbbmOnMzwet10mFJePLeO2gbuN2D4JhSbV4eSH6kvtVNvzXJg33xJID3UGOA/Zhtpqi3seydkp9tXLBf7Y0Ja6ILWK9UiOU8m3pO5xUVWwn/i7rxnAt6Xx+bh8+0S6vyqDe4mzhX0WF1fPA5TftuNFdQyMqJc5cdoNwDtoqYtTbIxlDjcYW7e7LVy4lz6k6aqJThMG/Ga/hlx9TkfDXY+9XE53v9sdhATFdhaw8iny7R/sAce6FOoYhtK4zjeVFoJ8ThiOW4fPrae+qe0L/Q+rgvmQJG4+95zKOddxlpOW5S4fc23gAHtVqquDVKdatE1rEvAB1NHnqjX7xndxdC9kNrS7YG9/m4cR7fSFhXJZc9GiLrChNhqFDnTOydx1RuwLapUnEvcCaV88Qm2k8rB5V7EXzZzgZ+pywRIDTbNfuK5+yKGm2mIvb9eLARbXfTfbYz+Yy7lApbr47j7A9gkH6nO7ZTFlQgDMBXupay8F+9BY95y2AviBvimYd1UF3LdNvv++9LiQuvQ5142da0ddLdmmNQv4qrB8sfeVh+AYcVwtF+jNd78/mF9YKC9M/X4575bp94fr4EK9LQmsU0BspkMPR1JpvrZwytgXRgroOW3g3K7NPbSb3d5UB10K4ZiWlmhXX9IWVy4Yp8K+/Rtirn5KyI5vG3ydJ45y/EZYu6T5UqC3p2PgPjQSDtet57THqMpniFKUeg7xwX0sJPumqXWn3PWkrhX2dA6XXnoN8rUZU+yxPE4dg8xSwAcN2XHVmgdpfXDvfmxoN9MG5s33wkL5WVwEOHx4APzGuS+K1TH4WFu4MIud/KSjmrjjnNvLScEq5LRicM+5WPimsQtw1Y6ZC9ZGBkY4jrMPQF13hpp3nf1OZzUQpUIqBft2CI87/GZItqtfleztDu1Ht11GueEeE7WOGNc+FfBD8fVSsA+1sS1Qn9sIMJLCfYyJ4Sq0LbF3L31luHAv7QS0BaypY7QN7YsxLjJKAf+INGRnWKFx8Gt9gBZz7F2nfnERYH6+vCgdPlzOHzxY5s3Pl/OHDpUg3+8Pw799F8AHDZi77gI5Nu2CuZ3vzrtl7TK93gD43TZQJ5LQyZnT4fB923XY9XDWnarQiZ3Kx940K4EWCZQvL+Px+kZFsbojESsbkEOuvrucz9nHwnek3/Z6fNPudmDtrEupcC9x7ynA545bv7zMfyst1UazPltVwnxV4I6JYypwz1eSecl5UmIUuXVihoxP0vO8rw2hdTYFudjx21Q7GoJ8BXzVKnEetq3tIVrKrTdgv7w8APj5+WGgn5sr5w3oHz5cfkwZUwfAIPbelQvbAAMIp+Acg3hz4fRBvPmYi2qvV7axd+QwXVwcrtt2aX2Qb/LdaR/ch07w7rLuOrD5UDpHvjspvnw7nRMzH3KVbcj3lXXzJiZWr9cN3Ul1pt122OPhY6DurhtLN8tSZUPf7rZR0/b6MNCv6qId2ucxjj01zQF8N14+1BnguvVU2+oC+7qgXnKX0Pf/yQX3VNtC/10f/Pv2pQTQY1S1eVOVQtePMZMCPqK1HrJDxd774D45Jp+66GFQb5x348g/8kj5feBAmW7A/sCBEuTn5spvA/qmY+BepF14npgo4doGdzcPoPzGIN9Om5wc1O1+sGU4sGyWxdJ9YI51WqhlJM8C+NqaSy78YfkUBHLA2QWd0MU/FcaxOP0Q0HIVcvTd0XM4LrzprPocfVPO/ZY4+Ng+oH6L2Is2Z99iZaRwz/kOue/YtC9kx/dtlrVVBdDHwHyV4Xuc/wZ1ng2l+c6Hqc49th6fwRLzzekQcM79lBp0sUm1xeH3KbE9CvgeYePCr1WFnPusjj4G9iau3rjzhw+XgG8A3gb62dky/+GHy3mTPj9fptu3vM0BhEF6UZQXwsnJgQtqwN6E9HS7gwcTJycHAGRk4IpyK00a5o6ak6Lt0vvkdgzseTekx87HTvRUWJFdNzbtS0sR1Rnz5WHpPqcYAI+bB/A7+1yXH5uWri9G7sXVdvTdMtxvu82uQsu7+wD7XUK/Ewf2Y0QtH+Pgc7857jvVGeC49XWCfVtGr5Gcf0Kd+JR5SfhiCkynwD1HOc7nbYR8VyEzYcSkgE+oNQ+aNiBJGE5W5z4E9rYbb08/+GAJ7g8+WEL/7t2lW79vXwn+Dz88eIC22y1PtOvWlSffdetKYJ+cLD8TE8NuOzVsIOdhKReuJfskdDK3OyI2qNsfXyiQWw8A/kyB/e1OY/OhdHc7OfLtDyoPS+ekueANEHbZc6sKV9+9cBmwSx1LH2D1yDuczoHPwcfajv1udhtSxQV7N80H9m6aDfQAYffd59b7lnPbIH1nh0Shc1vs+PK5lAP0OQaGPc8dXlhinGBGTagOyTenHt80No/Jd+5qE1RzzARpfQ1snwI+Q2s9ZMco+8O2FNzbgG8c+0ceKeF9bm4wfeBACfJ795YQ/9BD5fzu3eVy+/YN3H4Dr9PT5Tp7vfIzPT347nYBpqYGeeZjQN/UYU6uLhDbdwEwAAfAIZuK3TdyIZ5K962bu17Ot1HqCYsCc1+5WLeeKuuWw8pI4+bted+0vR47D1sfQF5X3yfKdafk62BJHXzO75Ha2QkpBPXufAjs7W8JqPvcemp5d11G3Lcsc49pH9iHoL6K80buOjhQj6VRjr1dVgL0WMfB90BtDNxjagK2Y84zdWgU7jwQUsBnapxH2cFCkdztxcbBd8tRZVFhUA8w/OCsCbsx34cOlSB/4ED52bu3DMe5554yfc+eEvBnZwex9kYzMwCbN5cAb6anpweAv379wNk3jr4JxzEwbDv7mHzOCBYzSZU1sf2mU2HaYLfFTjd5XEff19nAvjnbTEni0lP1ceAOSw+V9ZVzy1ChNAC4w577gsC9iyAFXt+wmhh4+4C/2x24+W6b7OXtNKz9oX1Z1QWX4+JL3XuJW28vE+PWc8JvUhxUyZC3MQAtUdXLc893OV4GGDrf5o63D60zlIbNA1QPwr7zTtVqAvIzrE8BX7Wqw8J15aMcfZ9rv7w8eIjWxNU//HDpwM/Olmmzs6Uzv2cPwP33l679/feXrv7Bg4M31BronZ4G2LixBPrt28vvDRsAjj66hP3168ty69aVAD81VS5rvrGPuy3UNkplgzjAIKbfhXST7gI+B+xdoPcBvt2uWHEh1AVqN0+STrUj5ndxl6NG4JE4+gByhz8Uq2+vK5cwIPcBt/k2Mf72Q7w+yPftG3ueSrPF2f7Q/yDk4HOdegAZ2JvlOMNiuu0IufWxYJ8D6jm/Sa7/bUw9vmWwvNA+kYI9Ne17L4kPwiVQT9VBlWujKKOnivWMyj45IgX8CI2zm++K2r4o594H9ybW3oC9ceFnZwdhOPv3AzzwQOnY33MPwL33DuLsVxrWKQF+crKE+61bAU46CWDLFoBjjy3BfsOG0sW3Ad917G23HGD1C6YA8IuxvT1mtBHK3XPTjMxJxB6lhwv2rsvv66RgcO9OU22TwrLvJCxx6N31xkCgdFmsc8AJ27Hr8c1T+xtzs33PBgDkibHGwNyexn4DCtZjX7ZVpXztcPOoeexYxqDefHOgnlMWg3op0Nui/ns+Z9q3bNOQnxPwqTCk0L5IBfxcYB9bRrI9rpqG4KphP3b7GtonCvgqVKEXXbmKgnv7QVo7JMeMhDM/Xzr1Bw4A/M//lGn33Tdw8PfuHYxmAwBw6qmlW/9//g90vvKVMu2++wBuuUW28QIVP/nJ6ou96ayY+H/7Dbnut9tJMLI7F66THwJ+ANrRN3WnOjahkzxVPtWhp8pylHvZUKw8tlwM+IdcfQA8dMhtC6YcD4D73Hn3YV4b9N0OQYxz7+sshraDkx4CfB/Um28sTTIsptuOVLCPhXou0HMMAp9SYUi6vPR5As42xwC/FOyxfAnkS+qXKHSM1qW6nP0WSwE/UeP0AG5lo+ZwnPuDB8uPDfjmgdm9ewF+9KMS7O+5ZzA8JsDwnr/9dlm7Mqjz2Mei6cW99w7G63ffmLu8XI7bb8Afu6ADDE5KsU6+u6xdJ3Zid9drxIEGjgtv53HBnQv5KfBetTjt5Tr82DwA/bbcHC8Wolwrn7vvAr8dtmPaGwJ9M2/qt+XrMEq2K5SGOfUAfLfe/c4RX++CvWQfYL+jFOw5ji4FVFzQSgEy6X/e91Awdzsk8yGot8tIwV6a55vOJd//sy7wds+rqXVV3e5M9SvgZ9C4jZfvc++j4N69kBmoN2+UNWPVm1CcgwcB7ryzBPrbbiu/d+0aDIs5NQWwbVv5ue22xK2tSFu2lNtqwN4FfdvZtyHfBQd7qEvXnbdH9rFDi9yyAPGuvQ/ubFUF7uOi2I4K1/G30zBg4Q6HyXUs7XVSnRG3ndgbdd0y7rZXAfZu2zjzHKfe/cZcfk55jltfJ9TXCfixHVLpmPqhdkjaL90/nLHxpYCfuoxEOQGX+h9XBdB1wHmqMrZPAT+jxsHNp9ovGRt/Rb7b4PbY9gcPlvA+N1eC/twcwM9+VsL+7beX49s/9NDK4p1+v8zbu5fflprVWb9+ZbowI/qYbV5eLjspBvwXFgax+vaF3pbtzne7sodtzfLYNyUfQNrpud31toJ/jvWn3I2gwN9uG/VbAVT7UiFMlGNGjdqDLcPZLiPq/xz63ThQb7c7FuzdPJ9rjy3L3R5XPrgE4IN9TsjH2oEp5j8rgaM6IZ8zfCZnOifQx/ymRlWDMnWuy1V32yE/kxTwM2sU3XzTMZHG3XuFXchsyO33B0NaHjhQhuLMzgLcdVf5fdttJdjbYTfHHgtw2mkA//f/prWtZi11J6G7frLcp+vXl/vDjO9vf0zHx3XzjbCHZjHn3gf4HLknQAq8fGm5wT+HpCEZvjQA+UuifHDKcapDnS07nXPXJaSc7p7bLtfNB1gN+qZ8qN1YGSnYUyEwHKC3v6kQHPPtW863fokkUC+BS+48tk5s3a64/7eY/yVnGV8ZLuzXNcIOZ5q7LmyeI9+5JqeqgP262l7XOggp4KuCEndWMEfKXNxMmIoJzzHu/b595Qg5u3eXbv2DD5ZpYMXZ79lTfkZMvd7w/ivMvuj1yv3Q6w0g30xjbr4L7phrH4q598nnoOaArpRyIcXUwVlGCve53xRK7R9fOoA/z1aVTpmpn6rXhnyA1aDPbR/3t/dBvWmPW1YC9qaOlGWptnLFdY5TwZ4L9THj5Ev/h7nLS9vG2e6Y/VrFNGdeqqrPIe661oj7nkMK+BVplIbS9I2DHxWaAzB8EbMfpDXOvRnT/t57y8+PfgTwv/9bxt4fOFAuNzEB8PjHtzfOPlILix2YnJ4eQL7r3LuAj7m7FOBTzr3E0cXAPgeI1+nYhxzdWOceA3sf1EudZIkkMO+7eyBdX0jYHSCs0wgwvO9cR98Ic/a54vxerhHB+Y59yNZtU9NQHwOIMaPt+NJDeZIyucvGDpVJpcW66qmdAm77bMXAtO8/nNt9z1FnzDZW9T+MKR+QAr6KVFTHhHLt7Y+JvTeQv3dv6dz/7Gelaz8/D7BlC3T27BmKvR8XTU2V+7WwTy4G9E2aC/q2bID3veDKlLW/AWin3q5f6tBjy9QJ9LnFhXtpHZJ8rri/V+w6c7qtRtiF1XX0jbD97gIW57ehwF4C+LHDYrrtTI2tB8gH9lQ6t36sLJXmS+fmc8tIy0uHy6TqlTrmsWAfKts2VRVq0/bt5qqC7VDAr0Gj8vAtddeB7ei7FzQDqCbe3ITk7NkzeGHVrl2la////l/p3AOUJ9qnPx3gC1/Iun1tUwEd6FAnaNfBt8HBvhj7PoB30jruegwghiA9BJIxHYPYMj6luvehUA6AuDHIJdsk3X7uPmvqYuj+d6k07rj9dqw7Z73YPAfsY0bPwZa368DaJZG9v3INs8it1y3HmafSOHmcfFvcUXg4D/BK2xybVvU8lcZRVeE3TbvwdagFbVLAr1mjAvtGGMizwnVcMLUB/8CB0rl/4IHSvd+zB2B+frBXimLs4R4AoNt1YvOPjO1/JHPwjYXqhCAf/CMirfoNXcivA8TboDrgnrOPUvdjE79D7lvunPpCUM8JteI49wDhB2Z932Z5t82h/4kEKt23bLtlfBAvBftcUJkD5iVDaaaCfCi/TtDPtUysqoT9UYF87jqquMMUIQX8mpQ8Kk1N8nU+vC+5cm9N26E5JiRndrYMwbnzzjIc57bbSgd/165W9HYbl3m7rYH0Xg/Pt09m5tt6qLaATtBoX/ktq4b0Jhx7blkp3HPBPhX2q1i2blHQjt0hovJi1udL84E9gCz8Bsvnht/E3J0xkr7tNJSGdRKo5WLnQ+l2Oyhxh8yUXEdyg700PQbgpfWFFAvG2H845Rou6eQ3paraVuE2K+CrVsR52DYo17W3X2hlAH/vXoD77y/j7g8ehI4B1zWuzswMFLOzqwHfhXjqZHgE7luvOp1/H9BT5QDi4J6qv8rQnCbFgYPQhZva3lDHgFsXx6UHCI+gwwF7qp5QGwHCwOYDe3valxYCey7Up0BoDpDPCfHccm2A/Zh1cZTL/R4lJ96oinW1qJOigN+ARiFMRxSa44bjuHBvwnLuvbcMx/npT8sRc378Y4B16yrekhFTvz84QUxNrX6IFsDvgsJqHnGLJp1DQ3CO5Y96uE+MI1uVi982cZ03qeOXuk9DLj0AP/wGS+OE33B+Zy7Y++LrOXAvidWn6uLOu+uzFQJ4zompCsDnlI2BfF9eLnc+F5znqC9HPSl3FZp22VsSmmOkgK9iyQv3rswLrYxz/8gjg5da3X9/GaIzOwuwbh10Zmera/QIqnPssQAAUPzsZ+W+7fUApqcBej0ousa5K7+xOywSdvSGXHFVFdBXDfy+uilHFluOC6KhbWkj9EsuPjG37GO2mXuXROrSY2lSxz/URlc+UI4BewzKY+L0qTLUvL0eWykvtWoaqFLblxP4U9oRo1zhMm1346v6j+Vef4IU8FWrFDXmveveG7ifnwc4fHjwMqvduwF++lPozM1V0/gxUWfnTgAAKO69t3Tyl5eHnPnS0B++qFbGiDlgvAmHnrs+yuEN1Sedj2mbdJmcF43Qb56yfKw4nSfpW2lDeTmhHqA6sPe59RSwxwB+nS+0qtIRzQl4KdtZhzsf64intqNuyG9KLd1GBfyG1PYXYbFecIXFptpwf/Bg+b1v3yD2/tAhgJmZEvhVYZm3/gIA9MpwJrPb3Wdwa1Ms0LtpknnfOqm81E4FF+hyuPi5YDhXPVW47ymi1seJf/eBPJWWG+oBZGCfEoYjjdH3Tdv1GnFf8uRL5+ZLy6Usk7tzUOVdAKlSQXvcQH0NuPcACvgqSwbmxR0O+wJpv9DKgP7cXBmDPzdXzh91VOaWj7HMfux2V3jV5ZEmzPFVqipUp42ShtvEhOwYSV+u5RN3JBKA+u8WcNYZGrI0BurtaWlngascYO/LC9XBmc4F9SnhJ3WGqDQZzlHVvggpNfSmjbHxRk3duZGuO6Z8ghTwVSuiXm6Fyr3Q2VBvPg88APDww4OY+/l5gM2bAR77WIC//MvczR9PTU2V+3J5GTpTUzDZ6wH0Jlf6Uq7JLZEoFCvktnPTUsJ9uI5+nfLBnwTsY97EKlGnk7ezkEOx28y9m5ID6LF5riiwzxmGk2OoTLs+t11YWUkaJy+mXK7lUutoEixjlQLcMcu2qXzuzlWL4R5AAV+VItsZM8RpTx86NHip1ZG31Hb+4R8aa+4oqnPSSQAAUNx5Z9lBmpqC3vpJWFwEWFgoy5hzRu13UXO59jHhNylt5CgFpilh7aAgN/f6JfVxXySUu8NAtTHUUZJOVw32ALRjLg3FSQnDyQ32OR37Ot36nPW0pROgKtXkvm053AMo4LdCbRs2k+Xeu3CPjZyzd2/5UO2uXWX6kRFiVBGany/34fQ0dHo96E2tg/n5klWWlsprc8jNF59fcrr2APHx9lQeZ5pqVwj8O52we8yNqXfLcd+Oy1lP1aoyVECSH4JvKcxz6sTyuC51LqjnvNiKC/RcmE917KsKQWlTmEfuOkYF7EfNjW+yXOoyGaSAr0qXgXwb9hcWBqPo9PvQ+ed/brqVI63O4x8PAADF7beX+7PXg05n+CLNdfBF55pckC9VqqsvWX9qWwHS4rQlzjVn/T5JLzRVdy64nSTpfA6wl2w7BsgxcN7tht16DtiH2uErz50PpYfyUspK1ea6x7VTMI5wH6MGfycF/BaoTe59UC7IuA/VLi6W4Tj79pWhOQ8/DLBpU2PNHTuZF4d1u7Bu3QY4fLhkmaLAIyuizi0h4KWAHiDs0ttlOPNUHlanrwzXuZ+YGMAh5uJLOwM+954TX06lcdfHza/6IsTZBu62UyAPUB3MhxxsqWNPQXqs24/VYdfja2/MPJUmyY8tW4XqWH+TdwhSl28DrEvKVgH2I+TcGyngt0BtC9ERyXXvFxYGYTr9fnnB3bix6VaOl448xNxbvwEWF2t4djLFoa/KyZe6+FXcZZAq9ENJ4T5n2+vcD9Jt4jy7kPttw7a4cB8D5znc+liwzwn1OWG/yjrqVM72Nrn/FO5HEu4BFPBbozaMiy8aVcV+mNZ28O0Haw8eLNOf8ITK2rzm1OutvEOgs3499HplLD5AeT4x8fiuxOcazH3nuvahtFQnPzUeH8u302wXP7T93DsDrnIDaZNx+j7FOvcAvGcVJPtJuo98wEuBuZ1HOfWpbj0X6Lkw31awbwEg1a62dApGDeolZdcA2Bsp4LdMI+HkmwulG65jwkeMez8xAZ2PfayZNo6pOo99LAAcGVVncRF669dBrzfob5nrPDcef0ghSJW46Zw0yXxVYM/ZPs725lQVMeJNdwA46+c+fFwl1APIwB5AHopTBdhzXPqq3PtQHid/rajq/ZBaf5VgLynfdCdAWjZlmQqlgN8SjRTYu2nmc/DgIO7+4EENzalSR/Z3p9eD6enSxTfj4hsXPwrybXFdeyqdSqsb8u12hNx7I+PiG5gqirROgU9uR5nK86X50lPLpihlfH/pdqZuU4xjz4V6d9lYqJc49XXAvYJ9vWqDwz/O7r60bI7lKpQCfksU/RbZumW79/bHHiLz8OFyWh+urU7Lyyvj4k9umoLDRwDAhXoW5PucbZ9rb1ZA1UWlucti83U7+VhnAAvV8W1nqL051EToTo6HPDjj8MeAe47tywX25rtKsMfapoCvCqnNzn5b6k7ZRy39nyvgt0xNPnBbQIcXh2+DPQCsvHXJhOgcPAid97632saucXVOPRUAAIrduwGmp2Fyct0Khy0v07H4UaJgnsrL4eb7OgFUOY5TH8qngNGMqOPrMITA3tdhwBRy81Oc7FRoj3kRV6hdVYO8qzoce3u5qqFe4T6vmg5r4yr3fq7DwW4L1MeUz7VsDVLAb5la7+AbuU7+0tLgTbZTU822bS3pyJ2T3vp1+eqUuvZUHictZt6FdbfNMUCPOfnGxQcYhOvYkC/tJFDK5fL76kgJlUldt7RslWAlgXoAHOxDbn2qy48tE0qT5MfOh9JDeTmXcTUqMN4G5djfdfzOVbvvYwz2Rgr4LdHIgL0tN3bYQERP/1a16cgIRoYZTH/Lde+DoTo+Nz1UNpQnde9D87HTsZDv204OnGNlqDfl5pYP7KXrl0IUt3yueqWOMgXZAGGwd9O4YThSsFfAp4UZBymizls566xSVUFnbL1t6wSkLJNz+RqlJNYStX6YTCw8YHl5eIjMiYnSvT/22GobqhroyN2SDhTQ63Wg3x9kLS2tNrNtBUOyMDB384yoi22Me2/SqPnYaWka5eQD4E4+tbPdergv0uJ0Hux1uOKOSsOpK6VcleUlsEnBrg+yfbAegnpq2Rwvs4oF+xiojwF9Tn6qOPVL/nO+c9q4Kedv0+YOQOpyuZZvSAr4LVad8fiiMfBdLS+XMfgApXs/PZ2nUaqwDAQuL0O3O5F2HqKA0gf6nGVDjj5Wv7scBb7utKkHK+MDebdcrJPPKRsaZ18C9jnFWWfOUJzUZbhwT0Fs1fH1VYJ9CPA56aE8X5ovXVqmCmGmAXcZI+6yofPiuCllm+oG9Bz7f4R/QwX8lggD+daH7Swvr7xVFRYWSicfAGD9+mbbtdZ05A7KuulpWF7uwOJiadR2OuHQnFUuvg/mQw4XlR9Klzr4PvkcdHs65LSH1sV18qlORMwwnJzOQ4xCy+UG/5jlpG4xBrExITjmG4Pz0LKp491L82KmOfNUGicvppxEseFanPNbaB3UclV1zlPrrQtUm+gA5Fo+dz0NSwFflS4TrrO8DJ3f+q2mW7Om1NmxA4qf/WzIxc8S3s0BRl9nwFeHm47Vg5XhrAebpuA+5Nz7ZAO6FPLdOmK2qQ5VFaojWS4H2APEwbbdMagqJp9qc0xZ7nTMPJXmS/coxrxC7zJL1o2dc0LlpGW5bYo5ZtoGnW2B6Zz7pW37OFEK+C0RFoNfZ4iOeIhM+2IGMHDvVfXL3EXpdqHbO2ooCztfuQY1+dtTC0vKUe6WnZ7q4FN5GNxLlvfJdeElMflUHQB4Z8Sk+zoubhkA+bCcMUrpbMRcTLlgyn3TrPl202JDcKR3BtxtaDHg+65F9fQ5ZdfCVZvQxccNXnXu8x2vwZUQ5XzLNRGOV6faDOBjBvSuFPBbrNaH6JiLl/lkG3hdJdaKgx/HdewOHvfCRDnNLpRy03wwS+VRsOy2kWorVsb95kK+r86cTr5vWyjlrKsKcdzkWLA33+7yrvMeswxVPgboY8A+AfCpa4/v79C2vwr3BqTvOouGL7riOv6x4US+ZdukqmC5inrHHOyNFPBboiZh3n6LLvthW3OA9HqDB2t1/PtmNDWVBcbs/6AY9o1Ct8G5Dr5Jd8v5Lp4u0IeW4UiyvIH0UHmqo4DV4euoYB0ZOx1gdb12zL+tWMjP6UT69psE6O18Cs7dOnKNd99WwHfm3esN9tNRPyfnZ66DR0Onn1B5niHfCeQDuHcGyHMnpwGU3GN8XFQXaK8RoHelgK9akQjqbJjo9RTum5RzQc9xLnMBQNzxA5A7+Fh6yNXHXHwJELugHepA5HDzqW9b2Jtzfe337Q+A1XcJMNCnIIILF1zi4v5BfS4zB+zNtw/szfISsMeW8a0rBtpzgj6EgT434ANUEx1m/9Tcdkj+viGTnWPCU2YdOxSIuyKf2tQRaAKu1yjQu1LAb4ncGHzJPCU7lh9LFwk7+5mL19QUwIYN8jpVeWTuonS76HmN4i7JOTD0n2HF8PvcfcrZx8qFIJyjHPVIIV8iSVw+1blxl7XpyOfoh0Df3v6QONtNlYkFet937AOzqW59ru9QGgwfq/ZPFIJ66ufM+a60VFGPenU8pye3/VQkKXUj0c4L5fv/7h1GmSMlYwA11Mhx01rYxkQp4LdEGMS7+b75UN1ZZV9gjHt/1FHtcg3WkgxQdLtixjWLp4rl+HOBnxPGE3LuTZ7PscfSOHncnUyF20jqdTsL7v6jtt+3nwB4oTshoM95gcXqCkG9Pe375r7IKqYzUCXkM/M4QB+CeQrifQDfltN9t8vvaHQ6aXcW7P51zLIAvDCiLIZKSG35AX1SiE+SAn4LZA7mul9qleTkmwOv1yvnp6dH44QxjrIAH8uyvzHFhIKGhP23greoOWBv0nzzoXSOYqEekw36nHqpOwIA9YG+kQ/47fWHFNp3mAXrc+qxtBhAd9MkD9mm5km+rekQ0Ptg3oVbCox9P2nbTvPSQzNU1tcBWFqqbiwJTueB2wGwxX4mQDV2UsAfE7nQzgnLET9ca5+BbMA3atuZf63IOlGnsmhI0rhXW0GXPwT2Jj0X7MfsKC7wU5Dug2QOecSAfmhdbvgOtj5Mhg45+9BXj7tutz4J3EtGtElZpkqYd74xoOc49SGYx47lUQL7qpTSAUhZJ8fZj+lLZ+kMqEZSCvgtkBnvPgTp3Lok6UZRI+jY8z39KzUmx73nOEFVC1t/6MLjHZKOE6LjzvumQ2lcSZ3+0Eg7nHqoN+BynftQB8nIBW+XbHzQTgnrSFBAT037oN7Nj3HrsWVrdvDNsRFy6CmgD8E8dX4YJcCn/rah8lRZDkR3u/hdD/NXwuDf/C19601tky1JWVfaGRgvsais6fHYx/2PRIXopOx3+yVZlJtv0kVOvgsT9nfbrgBrRchZO5eDL/1JQ6a2bxlvWI9kg6q8hZFL3CE1ufVMTAw76jbYS2OwUmiDWoaTxwV7NzZfAtgxYTs5IF4I9CGwp2A+BPLj5NrnPsxT66OWXVyM6wtz18kN6wFIuwPrKjcXjjvn5ZB0n4+E7Ro9ZN8aVqWdMtex7PXKs1jboWqclcnBT72gx7hH7jJ2OXJsfq5zj+VJHf3c37ZSHH07tIYK3+E6+1SanW4Uc5xjy0gB3+fUu99csMfK5wD7SKfeB/gxUJ/DuefkVy3uucNXnnNXMVTGdyh4z2MOstjA7xvlJ3ToxW4TVi51mVTlZJa2M2JdpvlIAL4r9jizLRbmllMhOqLwGYZiw3iG5F6wzMO2qvpl/Rbmzg0X8uv8ybhOE3XRQh3+kENMVYZdOam03N+YXNDnLGunufkcZx9geBnqB8h1VQ9Bvj3twrydzwHp1Lh6LrxzQB6DeQLoJTDPmZamGVURZ56iKh5u5ZwjuedRTKE7AlSez/HPsR9851mqrFGum3dVq+mok7ZoJAGfkvhNnCpScSPsVDS8gMov4kwaujhJL1ypnYHQCR9jzVDeqjA0zKGOcfLd5XN++2TIijOGvs+dDzn77jK+NDffp9D2ufkY1AOsdtvtaR9gYy5/DKRnBHxOPL0L9lKoD+VRafY6fapzvHsf3GLCgJfTR81RRpIvybP3t7s/7P0Q2vbY7Y4pG1pWurwqXWMF+LZyu95VSDLWPXdc3Jjtjnmo174wmY+qITlnTfu/QEF+TvDniltvjGvW7TrHR3diGPqlTr4P/FO/uRtv7DrXgfe10a0Ds+tsd9/INyJOiBio7cBEjW1vLxOCevubC/RYHhfsU4DeOje63wCrgV7q0Psg367f1aiMb89RjrsNoUMz5HpLXHGpfPvBt+0TE2nbhK07tX8vuQao0jW2gA+wNh391O3kOPcY3Lftlu6alQV+VLhO066+9CLAcb6wtKGOK+V0Uw52TgdfIntZd4z60Ig5oTrNNDY6DmabSobBdEXZsL4hMSWAHztyjqQDIF0Wyv8cBfSuSw+wGuyrgvqY8e4lZVLFcZqpsr74dqOcrndsH9jtc3OXz+H0A6zebkmbQ+UkdXKUq561rrEGfFusF+/UsH57dBt3PscwmTFKce4XFsqTDPUKcVUNcinXdyUhVKejL6kvxs1fXYd1vIX2Cea2cyHeR3bSDgG2rJEL/HY57rS9DqlNxxG2n900DOTtaQ7Mu98p8C5NAwvoPU49x6WPAfuYce596aG8KiUBulCf1pfne5OtbzhL7LCR5NtlUvJ9eVQnzjeEJwDf6fe1zS0nLetTrnrWotYM4LdBGEiHwnDqhH1u3L3t3Bu4V8BvTqnhaDEgQElyJ4fzsFiKm4/Ns119X1rI+U9190ProZx9aluakATwKdDngH3AUWdBf8SyEqc+Bex9UJ8C9G1x7m1xARgrw83jON4+hz/GnZfWQZWpwuUHSL+74ZbjnHKkdwY49Sjsr9aaBfy6Q3Zi1hcTg1+l3AvQ0tIA7ufnK1+9ilDJcZ3SnTY9rSOA2Ol2h/47OS7sucKxpPX4nCa5098ZXBDseH2sQgrQfXkxDr2vrG/dRvYQG6EHaqnOg6k3VSHAx0AeIPxwLQfoOWWFjr1vOMsQ0EsgHrtRY9eJlcPmJWmS/JhluH8nLuBSeZK/LeV4Uw4/5e5zXOsqyvjyfXkxLr9v2E63XVQZrFxoGelpKEcd46Y1C/h1x+fncOJ9L69KlcS9ByhPBEVRzi8uAvT7WZujEgi9uFmJHSig2+1EXfBtSYA8dtQN3wthpBdeFxio+a7t7Eud8BiXPNVZ90G+kbuzOOuT95LwOkJp1IO2FJD78mIceEF5G+ylTr0P4KVQHwP0Ekefk0epDc9fUX9dLB07FxhRbjfl7nMcZIm7z6mHKhPr8APkG7WHqp/ThtTyVdUxylqzgG+rCnecqpOzLl/IRRWdEemQmPZFqt8vP4cOZW+WiikDH91up/wlMReYWM43DxC+eOcePo9bX8hxiuXUlXh9zig8oTyqLKc+310Car1U58SWbwQdu45UuZQgebiWC/bud4pjb32nhN9I4T7XG2lTHXwupMcc71j4ZujNrr7x3qmbT7n6sJjbjZ1vpLDPhU0p7GPlfPkhpzv2pVzctnHLpJQP1bFWYF8B31EugHbr4UK0KVfnQ8Huw71cLS+XB7wCfrOy3cUJO5EB+T7FDLGHtS2H3E2gbq8D4Bchn5tPr9PzcC6XFHIDPZUGgMM/lmfPY8r1IAW2z3zTsWBPfTPzQm+Tde9a2mmc5dxp6UusJHm+tNjjOdcx7BN2ijIdAypuHAPsKsCt242H/RRXP6YcVTYlP8Xd56ybWyalfO7lR0UK+JaqDNWJhWhbvmVztN0d4ceWe9EyB31RABw+XMbg13EhUK3W0tIws8TAPWX02moSBHx8bEQ5T+5FiAP7LiN7Q3hixIkdiK0HS6fiEkJ2nlTYsrGgXyHUA+Bx9U069RKI95V114et17esJL8OUX/H3OPfh/rCrrMvCeHJBfo5ytYB+wDxD+w2BfvjCPoK+JaqcM1TX5ncxvH77QvY4mJ5YM/P60g6TWlpCWBy8siM/eNkgvvUl+JUAQkcIxrbfC4UuMb4oK4jD+diITyYoy7J49Rlb7TPlXd/f+z/UIXlGQJ8yXQq0DvfKU49B+RDQM+FdAnoc45VrsPvS5eWocSBOrccB+BMuuv6u+DtOyw4h4y9Pomrnwv07bKc8hKYx8pwYR8g/xt3Oe2LqZOzbMzybZQCfkCpgB6jNkK9T8b50gdtmxF5wfVciaksLtw37fxxHH3shO1elCl3H2Pu1dPOkJs+WnAbH5OXWzF3DEL1hdJ8II+lZXDzOWCf26mXuvQSh146/n1uwI8py+mQ22WxcpKbVb7jnFoWq4dzPgCgXX1pGJH08JeU53QifGVCy2P7wMg3BCm3bZJy0rLU8qMO+Qr4HjU1Ug2nXFMP29onavOx01X1q9crPxPdYvCj2O4mU7FuoC9dWiZFMbxq76LFxWEnDnP98OnOkbrKBYaA326YxJV303zLuOVDeZx0iWLBnpqmvkNlBEAPwHPqU4A+h2OfMlRmCuiH8qTy1eVzje18yp11r0HUNQlz+W3wxOoJTfu2BTMQfO2zt0cK+b76qPKhNlBlQk53rrfrxrYvpWzOZdsgBfwRVerLjXLKZsnPfraAbdsAzjuv/jsfa1EHDhSDk2jFBC0FBE5+ToUuDtiF0OfEURd/trPvI4IQMVRp5eUUh3Tc+VjAD3xLwB5gGO45QJ8K9lzg50J9TqAPHae5h8IMja0OQPdjsY68z32n5mOOdV+bTDrl6ruOfsgtj4HRWICNdfVD+T5XHyCPs1+Xqz+Kjr4CvkfcIS2p+Vi1BdxD6nYBpqbKg9N8Ly0BzM013bK1o+Xlcr9PdAuA+f7wi66Yy7vivCGzzU4+Z/0cnrYdPyym13fx9zr7ppC7YoogYqzfKntjvv+WD+zd+VinHhyYRwDdB/O+b18dWD2p0zHDZOZw7nOMkJUi+3ktKfxxOuqceeyvGbqLRwGir79upqWOfp1AKQFpqhwH9AGqe5tujKtfRYeoTVLAVyWp0ykf8Owd+ScVhb7Vtk4ZwB+yIWtYJydNkl+FfI4+djGgHD/X7QNYffF31+N19m3Q52wEJzagLoXWn8O5x9IyuPT2t6QOrB7OtA+0U8Fe0gFw12er7pGxQvBnRI29Tjn/ErA386GyHGffXr+vrx46h7jL+NKqFBdgfe0K1VEU4Rca+kbfzenqjzvoK+B7xHHS3TKhZTgOv+QuQE6333cHArvF2e2WB2KvVzr4Bu7Vwa9Pi4tHRtDBAN86+4Qu1BQAxLiDnPVxy9Sh0EXU5/YBrL5gUxCx2tkHAMrdDzn5kjxOeqpinHtqmnDpV6aR75xOfWzYTYpLHwP27jIpz9Bw81PFBSMq33bYQ+txO/TUfKhNPmffrY/TH/c5+lhbUvdZjHI6+lh+qqMvbeNaBX0FfI9MnPvKGNgJb7Qd1/AdA/rd7uBA1dF06lO3aw2R6WbY3xHiMmNouZg6YpbjnsSx8pR7H5p3nTmu47e67UJ3vw1uPoCfiiKnQy69PV0n2EunU8a+5wJ/7mEyMcUcs5JjMRYOQ+PQS9x8d51uOnWc22VtyMfSJI5+G9x8iWJMEiOOow9QH+jH7OO2/jYK+AHZUC6NyafqwcqmdiTqlIF5G+4nJ8tPt6uAX6e63SPhUf3FwVkm4UzDHRZTAg11uYaSejjQbdfrc/8od18at1+md4bqCTr8VCPqcPB99iRj3ufQ29MSEI/pFITKc6dzAX0szHM75FXAPffvhy1DlQ2BNzYOe+gt1hL5jnMz7a7PB3oSR9/n5tcdssNdXwiiffkhRx+gPtBPcfPbBvkK+AK5b6O156lQnRQHv83APzExOChNqM7UFMC6deX3299ejqbzB3+go+lUoXvuKWBiouxUrQyPSSj1zlEs3Od08lNG8Qg9tGWEAQWV5oMS7ALhG4ubIxv8h16yRTWIamSIqniN4aU78/b/0Afy9rQE6kN5lNvPmW4j1Kd0urn5XIUgPbSMz8WnOg9VOvtYns+Fx8DcVcjld+8QcF7OVbWqWE+o/Tli9HM49TGgH9s5qEoK+B5RYO2Luw/lcRx+t4wP0OqGf/uEZFx8E4M/OQmwYQPA+vVlLP7sbK1NW3Navx7gqKOgvGVin1nsT6S47l9OJz/3UHySelOcPlsc189uk+9NmxRolOoMz3cHV7yhN+y6jcMaEyNiefdcJQHaECxzQN4H9KE6pG3k1E2tJ7V+zjyVFlOGo1CoDbccF+ipZXygD0CPXuNCt113aB9Rjn5oGaxfbgCXO0a/UW7wj60rh0vepuE1Y/ZrW9x8BXyPsLAaO51y8O15rguPhei0zcGnTkjmc9RRAJs2leB58CDA/fc30861oJWhSbvF8NCYCWCfY4g8KWRwL4g5h+8LPdRlhEG/JLTHFQf4zTTXvcMvUh0kDYY6AcOl8Z3LufPD+b1j3W8ujHOhPtaxp+rlLht7F8C3DGeem5dL2LHiKyfN9y2HARXlBIfGo+eAv28aQHbHjrqu2vVgnRJsf3DBsg3wCVCPmw8QBv1xhXwFfIGwC55Jcx/IlYh7p6AOuZ0LTOZPOzEBsLAwSFu3DmBmpgT86enSxf/d3y3gCU8AeOtbNVQnh3btKlZOHL0elHC/uBgMj4hVKmBgaaELX9Vjcfvqp1w/AB7wx3YA3J+LAn4AvnvHN+xlx6bkDo0E6KlpX1pMJ4E7HRomk9NeKr2K8e9D6dIyIXFDHKiyXAcVW04C+QD4G1WpkXCo9tn5LmhToTs2oHPuApjl3Xp8jrUUJOuCTm67OJAPEO/mc9aR0/GXrLdqKeAHRDnrZtrk2/NYHb55SpxY/yZlIHN5uTy41q0rD8aZmRLuN2woOwC33950S8dHy8vlfp6eBpjsFQDz1htjPA4+Fv/pWwenHZJ5AD/Yc6C+ShfS7DJfO7D2h+DbN08BA9Yu98VAvpADd1ksL1VUfTEhJhLHu26gT60Tq5/r0sc69nW49dh6YuHIB+yhelPEgXw3jXL3jVxn3QX0WFEhQJJzCZXWBnFAeFTd/CYhXwHfo5gYfG4dPvli8HMM3ZlTJg7fjIff65XwOT09GFXngQcabeJYyQD+xASU1GeftRA65L7XoCnV/aIdyXooN9AIGyXHrSuXw28v77aDgofQBS9W3Jcmcd3oWDiOrc9tVwp4x9Yfs2+w+VA61o4Ycf5HKUCeC5qodG64DlVHqONupwPgoO+u33XrqW9M9l0BDPJHUbkgHyDdzR8XyFfA9yj0QCzmrEvi6KmQHo5jXyXYh8Jz7BPX5GR50Bm47/XKOHwAgGOOKZ38Xbsqa+qak+lAHXUUAMz3B4mue5/hbBIDHRznnjsUZ2yZHOKAuLuL3RdfAaTF+9rzWJt8PzHm/OcSFzJjITYnzLvT0nh36V2HlPbHzNuq6iF1qu6YBxyrduWpejkvxwrJB/nYcUvlcUbcoWTqCI3Fj60X24YqVdV6QpAPkO7mc93+UBm3fN2Qr4AvkC8Gvwo17c5zZVz8Tmcwoo5x8detK8s84xkFnHACwKc/rbH4MfrJT8r/gtm/E11nmEQC7nO9YI1S6ALFhQ5fPU3caeCEH1Cwjb3Axig0godJ8936D6nquzMSF5kLs3UON5nyNlmqDKfjIOkYc8BeCvScMDgJCHNGMmm7y1nFHS6j0N0EF/QpFx+TC/puHXaZFFGhSJxlqlQOyOcot5tf939cAd8jrgsvmZeIGkO/6hCdEBS6LoUZ0WV5uTzwpqfL/GOPLQ/C2VmA/fsBHnwwe1PXjKamyjsjk5MA66Y8I+d4zh6p4BgS5cQahUI4Qunc/JziXuB8J2473XX5Q7H0VAcAa1PMxThGsYDP/T/EOP++/16Kk84BemodsXcwpDCf86F07gPotnI+AMqVJL6cajcFfymdEq7LL3kIlzpH26E/vpAd390FrE5Jem7l/r+0MS6/TshXwPeobQ+1AtTn6nNH0zF/VntMfPNZvx5g40aAzZvL8g89BPDCFxZw0kkAH/qQOvkc3Xln+TuYl1qtWweD2HsK7p2zh9thS4F67nKx4QKx0F+VqAtrqKx7kXfTsTc3xsC+nSdx+DFJ9jH3d4od113qrLvrihluMnU9KXcYsHl3XbaqHmmKWmeOUBdKXOiRwFHO9sYCnA/oXEC3y7vf1HowyDftDS07yuL+H9eyk6+AH5DvgdfUGHxXsS/QakIuUJhYfIDSbV5cBNi2rYT8xUWAvXsBdu8G2LcP4Prrm2nzKKrfL537qalyVKLO8hJAf3HQm7LPEolnjE5HBg4+WLElefiSU19TsC9xyGMurJjTD4B3AADw2P5QvaGykjIYfGL/H0n4CReA29BxyA303P3pqyOHfKcR35CFFEilwEwK9Od07lOdbcxZNwqF7HBExfdj4T4cJ78pSduTq9OZG+BzrTNVCvhChcJXsHwJjLudhaZBniNz8jAx+IY9p6fLE87MDMChQ+WDoRMTAAcONN3i0ZLZnx0o/M59S5T6sJ8U+Ln1pO4m3y30UHnK0bfzqPTQ6D2YuPAfUsw7C0K/XyzwY+1JiacP5bUR7Ovo3HLCFiSj0sSKG4ZTJdzngn6sk50ClJTLT4XstBHm6xTnf5kD8qX7uerfRQHfo9hhMmOdd+ohXk4Mfs4x86m7FK6wWHyAEuw7nUH6li1l+o4dJegr4PNlHliemoJhuAeAQvhmUgDaFZqYqGYUDq6zypkPpYeUCkc5Qluo5Sm337deX7vqHD2Hykv9vWNdenc+FejtcjF3B2KBPtedFp9CsOIrkzNkhwM5bplUsI+B95Q8n+x9SYXruOuJhXz7d5UApuRuhUR1dTxyQX5IbepMKeB7VPeY8z73H3uhFgbi1HxsOzgdBfdEYV5+ZR68nZ4uQ3WOProcOvOEEwDm5go4cADg4EGNxcd0550FTE0Nnm8AgKGzhu8uErdzlxKLTyl21JxY2Ofmh8Q5IYfWwYElqrwvP3TnoA5XN7Qe7l2XFKh3y+eAenedqXcEYu8+UMtI86XidCKbABYOzLvC4L4Oxz4ljxIG+bYouI9x8u006reWbENb4LYq5XD6Y8tKpYAfkPuSKVuUW+4LswnBl8T9ryt8B7uLYIRBonnBlfnjzsyU08ceW34/6UkAd99dxuWrcJlhRs3Dtd3u4P+HuYncaB0OIHDA3wcyRrnDJ0LrMwp1Mqg44ZBy7Fu7rhzOv6mrKsUCJ+d3lI6ylPK/oZx6d71cWK8D6uvquNnrCoWEuOK4+CFYT50HWN0GCdinwHndTj/mvvsc+pCTj4F9mxzokKp46DsXwLcB8hXwBYoJ2UmB9FDnoMoYfepugi9Ux1XvyL9renoA+N1u+aAtQBmqMztbwN696uIb7dlTrEThTEyUH7N/7RfX+mR3BtqsVNAHiAsr4ixTVSeAU1fdzjylXHAPwAtLibmLI/nPcJx6X53cdVUJ9Tn+EylhObnXnTpfBdinQH2OfEwUAPqgHYAXrjOKYA9Q7YhOuQG+KSnge9TEA65ciKdG98kZkx/zkiT7D28DPkAJ+L1eOS5+twswNwdw++0Ae/eadrUfSqvUvn3lfuj1ypPX5ORwvomrpsadlp6oqwRIrnvPKeODJ2x9sXLfQIuJE8MpEXXRlixb1e8YC5u5RtfhlImFerdsrEMvAfuYu1FV/LbYnT+sjC+Ug6uU0I4ccJ8L7HNAu69MzpFg3G8J5Lt1qdLUtIuvgO9Rymg4seK6/9I7A6ltp166ZYTBpQF8A6qbNpXfJ5xQxuf3+7DiVu/aldS8sZDZF+Zkv7AwyCuK1Rd735B1PkmhgVs+5IxLndFcrihXnAswd+Qarpp26V1x2hPz0qUc8M4pU8WDuSkhOBKoj/kvuMtIASEVKrgOOsDwenzwngr2XKhvEugB/OdtycOg2HQs5MeE7YSuP7nf2VClcy9V20N1FPCFwh64HZXhLLnyPcxLpRnZJwkzbKYB/PXry+8tW0o3em4OYH6+/Nx9N8Di4mAfnn8+wBe+0KIjuQL1+wUcPjyAdxvuAQYnxuXlwcfI3s/d7jBoUK6MqQuTvbwENHI6kBy4574Nl7tuex9h5U0+NVSlrRyjEEk6CVV3DmKGyASQAWwMxGNpVYO9O1832HN/a99/2LdMFW6tD9qpcqE8KdzXGZrDLRMC1FzDjLqKgfwcwrY3BvrbBPajIgX8gDhOeN0Pu2LzoZAcafw+BfncN9suL5dgv7xchuj0eoODetu20sG3P/0+wJ13AuzZUy7zzW8KdsyIyh3K0FycbaC3wd4eWcGAvf0xJ3ATtx+CfLtegPrejumun5qODa+g0nxtkYYSUPsq9SJUxVClXHF//5hOVSzcY2mhECDpnQEu1LvrjnmBmy89lBcr1xiIlQS2KbjnOvf2dFVQX7WDD8A/H4SgPqaTZl+LTR3c0XVCjr5R7PmuCVivquPUZhdfAd+jpl15H4RzYuN9o99w1uNbP2dUHfMnNQdWr1fC/Lp15cg6/X7p5s/NATzqUSXw9vvlw7f79gHMzBRwwgkAP/zheHXdFxeLlQdmXTCxPyZEx3b4FxeHT9A95wh23fyc4sKHNHwmBOu+elKW9bn3WHy7L83I3vbYi1hdHa2QUpzlKkA/56g7oXnJHQJO3VSaL70KVeXWx8ATtxPgW480bt9Xf91QDxAP9r62cENrQsv60lSjIQV8j5qIwfetLzYGX3rXARsH371TEOo02L3/ycmBW2DH5RvneWam/JxyCsDxx5cP4d55Z/n9wx96VzOS6vfLjw3zi4uDUXKMs4+9rMh27JeWBqBv0sz+XVpaHfJDCXM/pS4j1qmQOus+554TRhG7PlsuxGOdAMzpwuqo+45InfXlcqe5/6WYh3RT5psG+pTftA4Ys88rUude6tr7oL4J9z4nwEvXTZXhLGfOVSmhOm5azpedVaWqnHupmnDxFfBVQUmfMXBPEibNwOfExMDJP+oogM2bAQ4eLMs88kgZ0jM7WwLu7GzZAoDyTbi/8AsAX/5yy88ojmzH3tylsOcNqLuAv7S0+kFaDOaLYjC9uOi/6GEx/kah0BmOuO49p95QXRLQp+rALmB2ns/BD81j9ceo6rCN1LI5oB6gutF3pKBfFdyn7EeJQnDAgQfuOYS7PJbODcnhtIkzz20XplyhNjHrji2bU6Pq4rcF7puSAr5HTYfouJLG4Jt594231IPCdhm3HqkwB2BqqgTRbncwgs7ERAn5U1NlbP7mzWXIzsxMGY8PUEL+3BzA7t0AX/5yVHMa1dzcwLFfWCiBwcB8vz8AfAP3lLPsfsy+7PUGYO+OnW/AP3QLnBuuwAVpH+Bgdbjuvc+556bZosKWsHT7DZJGWOhZ6KLndhDcPFdSwEsBQu6yueEeQDbUaSw8tx3qq+i0YfXngDKOsx4C9xTXXuLYp0B+7odfU/Y9d1mpm+9z8UPr8Tn7riT7qoqQ0rrBntsBqtvFV8D3CAPhJhVqBxWmY6C+LdthoHRqanBi6fcH370ewPbtZbkTTihhf9euQZlut4Bt28pwnu99r51u/uxssQLsBw8OAz0G+ubbdu2N7AdnbbhfXByGezs0xCxj7gbY+dSJQ+KE+sIZuMLgXFI2BPfSMfOpoTDtC6AL+lRnzG5LDpiPXTZ3pyE2vw6ox9LaBvahvDokde+NOM56yEiQwL0P1nOAvrteVxJITAUx6fI53XR78IYmZK87FfbXumPvSgGfoaZj8Snlisl3nXxsBJ0YJ586CZkXOXW7JeSvX1+mHTxYfs/MlDH6hw6VL8fau7d09/ftA7j33rLcnj0Dh7+N2ru3hGvMsQdYDfYYrLouPObYmzTj3NvpCwuDZQ3w2/W605i4gBR6sRUHaij3nnM3gAJ7ybCatuz9YjpJxvECWA397jpCzr5UOcI76nDtpZ0qicMdA/RYWo4hLnO79THQHwPqEhCOddclDr4dehhqQwrw2+tylTqCTWr5XHVJ1yuJn3fde/sc59aTAtltAPRRDEWipIDPUFuAPqeacvTtg8eOHTdvuzWhO4uLZYz+/HzZCTh4sAzfMfH5+/YNx+f3egCnnVaW2bkT4G//th5n/7bbiqHwEhNqs2/fsFNvIN583AdpKSe40xk+mZpvDNht5949EZvbsmZZAD+k2gqFvEjeWpvDuY6Be25bXPfdTrfr8zlNlMuP1c29rRubnwMucwK9r74UaM4B9px6c4J9qpOf4xa+rVi4jwnJkTj2KWCPASz3ZVw+5drvuUOoQnkcQ0cajuOq6TsC46TUY1wBn9AoQH1qDL7vzoTUsQ+9/ApTrzcY7aXbLeG92x2E6fT7w7H5+/cDPPBA+VKsu+8u3fy9ewfA/IMflMvPzIianqS77y6/bdg0YTY20LtlMCh1L3R2OI59sjWhOebBXNfRt0fQcUN77LpNOQD5SAiSUAYfkNudI7veEMy736GRd7B5XxnMlXfTzbRZtzsqhVnO56RKOhmhtofSU/IAqod5aTo3rU2OfSrUY/XFQinHKQ+BuBTyufVxp33tddfjitpvTTr2udaRo00U7HNc/FFUXc597o65Twr4hFwobqO4YTduDL5bluoAhNZtl5N0CNw/twHNXm8QrrO0NBgH3rj709Nl6M7UFMCmTeUY+vv2lZB/8OAg/GV5uRxDH6Csb8uWsoNwzDHl98aN5fKTk+UDvr3eoHMxNTUM1ZSLZLR79/A8BwwoGRBx4dxev+vcu+nm5GGPsAMw/LCt24Fwt4978glBD9fNz/mirdhRV0LLAAy78W66kQuT2DJuGtWZyKkYGPXdpZCG3MS0IQWqOSPyjDLYG3EhNQXuuSE5vryYuwDcabd+dx1Y+VC6tEyVy+daD+fuBoAMQOuE1So1Kr+xVAr4DLU1Bl8iyQO62NtwMYDHOgMxTr6RAVoD2caVnp8fxOabYTW3bSvj82dnSxf9wIEyJv/gwXLaDEU5N1emz8wMQH9mBuDoo0uoN52G9evLE56Zt0ejsePYTTvd7bFh2c6jOghYXaaMDeemnF1PUZQAY94lsLw8eFMwFaJjvt3hNjlt9f1uEkgKAT9VnuveS8bMD7UhVq6rzylP3TWIUYrTDdA+l55K57Y/xzCbMe3zpedSU3DPAXppXVSaLz0U943tH98xFnsnpC7FrreK9saCfWiZqo8ZV20Fc1spnSgFfIZGEehd+cJ57HwXzilYl6a7wuB4ebm8IE9NlekmPn39+hK8Dx4cfG/aVH7m5gZDa+7dW46j/8ADZafgwIHBC6QMNPf7Zdnl5XI9ZujO6eny28C9+TYPBBuYxhx0zG33pdvTWDiN6djYcG5DoLm7sLBQtsvsK1OnDfru+rD1AgyXxX4fiUIAxQmriYV7bD3c9vhEufdmebOP7GnzW/pGH7F/V5+rn1OcMBaAPA/Gxi4jSa9q/PycZXMrBu7bCPax7XPrdcth81QaJy/nMlWL06bYcBoubGJx+DF3S6o0YdaCFPAJjTrUYzH4GHxzw2woJ58b0oMJO9BMeI4dImOP8W4c/unp8nvjxmHoN52AQ4fK8J1+v0xbXh5+WyzAIMa/2wU4fHgA9jbkG6A2oG9Dvx3fbs/b7r8L0gbIbdg283aIjg319snShf2FhUGbjJNvTsI26Jtl7fXa8zaIYnHk1O/lkw+oODHzMQrVwb2LEKqXgnO3Y2b/DtibI7Hl7fWlXow4UBsLyE2np7R7VF17n7hwT5XhOu1Unvn2xdmHHHsp2KdAfejYagsI5mxHlWBfRRy+vc6UY6vK37It/xNMCviEsBj8No0lH5IvPp8qZxQKzXHLpsh3QrZDZLrdwZtv+/0BuM/Pl+E28/Nl2I4ZWrPfL8N35udLx/7w4TJveXkQ2w8wAG4byg2kAwxOWMvLwxeWmHhx1/m3Pz7HnSNz8rPfZOsCJtYGM21/2x0NgHxDoFHhE6GQmlzufQjuuU4+t6wphzn6oZEmqPVwoMS9E+CbzxWrHpOeA+gB0t+enAr7obyc4gBsU669pJ7YdvnKc+a5eZz8tosD29zQJkyukVG12vh7tLFNthTwGXKBd1QgP1Yx25fi5BvZB8vk5GoX04C3cdSNm9/rDeL0+/2yI3D4cBnaY0J15ucHD+IePjwY5cZet+12Yy69DeBmHH8XzqkLDOVkuZ0ZDOx9jhXAaiD3OcM2JHFPzHYZX8gJV5xhNSmo90lSllqWW851ltxOlS+NcvRD6+G2MwT1PnE6rlU63XXCfY7OSCgvp+qAew7IY5BO1cN17OsA+yqAfpRHjkmBe0rjMJoOV3XCfWwnSgGfkA2s1OgzoySJK2+UMmymVNQJ2cBJpzMIP5maKkFzerqcn58vXflDh4bfHDs3N3D6+/2y3Pz8APLNSD0u7Lttwtx1DOqxdMyRx+L43Y/b2bC/7TIAq+PnjdxQEHu7MCeZezGmfi8qnQtSHLed495L6vO1zycKoLkwjXW4sDKxoTq+dth5ExPDbeh0/O8P4O67qhz7ql6YJS3LzU8V5ziTQn5sCI37ndI5CLWHsx3ceSrNKAVIqxjvPfVtrq64beSeYyhDok7gbVKjtJ0K+KosytUBcN1PW1hYg7nAmgdzTcz7wsIAZKemBmE9BvDNKDv9flmH+VDrckEdYDVgU0BvyvpCcLjwb9J9o+FgwhxlA0ucoR1DsusOSQKNobAaTl6dwvYzlu9z8wFWHwepF9CY3zRWqQ563W/D9aX71Aa49+U1AfecOrjt8U1z5qk0d72UmnpZUxPrrQJax/GFV6ME9wAK+KR8D6SOopPPjckHGHb7q3i4NiQXagycGAffxMtPTQ0c/eXl0tE3D9IuLg5cegPzBujNZ3l5+Btg+M2yoQs41/HmXhSxToQL877l3Glfmg2MLujbL96inGnOxZUrCfBjzn2oE1A3/FPhOQDD4E6FT9n73SjGzXc7XVgd5niy10+tlyOOw19VGE4d6dz8WHGhngPB9nQKkOeAel9bQnWEtg2b9wE8BzrbDnIx/z/JNnHP7zmc+9DyTZs3bf8v+KSAL9Qown2M2vCsAQYX9sHmji5j8k04j1nehCEYN39hofwYsLdfqmVG2XFDQTjijO7AuRhKLqS+aUpUGTd+kgJ9ADpEJUUcsK9CdbrbRtiF0XXzqfLSiyrWwfDJDdOpQzHj73OV07WvSjFg7877YBoAd8ipZWPNCU4dVFtizm8csA8B/SgCXJVtrnN/SK5XdR+vo/i/cKWAT4h6idMoS+q4+8pXHZNv5Lvw2Y4+QAkJRTFw4W1YN9P299JS+bHT3Zh8G/bd71zbE3MRtxUbQ4rBE/aQlA1fvjCpnC4+tm7TPmoZbjiPr+Mo+W25UEbJDdcBCIfsYNOStlIdBfc4siFf2kHwCVu2Cec+VSn7IfSbhc4RoWkuSNcJ9RK3ngv3XJiP2d+jqKpcfa6bX7Xc9eY8ttvWqcklBfw1JKqDgo2Z3zZhUIM5mWYMfVv2i6MmJgadABvsTefAAL39MevCxm63lQMOAWhg5wyrKGlPrCtOxVbmPOGG3kjKCQUxksAYdRGJvTPiWy5Up3uHivqNYy8YFLibO17mf1gU/g5GFRBd9x2EGNn7PfT/k9RFpXPuCnJi7GPAnNspiO1kcKfduu36baV2vpuE/qo6jlXU2eR+SgH+cenUhaSAT8h9ARQ2qk4bwlhyKBSf725nU50A94KKXfBs+LdfmmXSKIB3Y+8xwAdYHQPOBc3cEMSFC6qMu/98dWAdATct5mEqTgcj9o2kIXEd+9gLnOSujFScji4ld7t9br75fYyb75b13VGgOgApHYKc7n2Vx2NKecn/pinH3gfnkpF1uNMhoJd0rGOOu6pgkPoPpq4v9x1mST5Ac0NljgK0191GBXyGKAAeB7jHhA2p2TZnn+voGxk31CzngrsJ97FB3oZ4LDyHCtlx52NBNlbYCd5tt6+zVGWsI/eugXTElJAooM91ws0F+lW/Qt5dr8+VD4XsUJ0DbF0+yHeH6gTAnwOgOgfS9FCeyQeoJwwgBKgc8G0C7LG2SJ8bkmwbVs6X5ksP5dUhyfrb4E5zTQrqHJZyt3Ec1MS2K+ATGscYfCMOrLcN6DGFTt5YjHPIice+KRffXt7Od6ex+VB6jjJ2OymAx/aNxHXlXngk4UA5Ojs+6DNKcf4l+dw0TuhBblGQ7rr5AMPHjM/Bj/lPYZBftTh3E9zfKfWOj68cBbvuPOeNsVhabrCPfQMule/7/3PgMtW1bxo+U8O8OPVIlbpPfENlNr2/61RT26qAT4gaJnMcIF+6DaMA+wA4VGBlfMCPASAF7hwHP8fLemLyTIiSm++2zTyTgOVzOiwhp9OkUyd5bP/Ysd+usPVR0B6Cs5iTbowjKHG6uPHEMW69K2xfSUDft0woBMjk+SAf+x9Q/zdpupvvK0OV50jyf/EBft1Qn1JHKA2ry902tzxnnkrj5OVcxqdcEO+rqw6Y5Lr54zYOfoya7Mgo4DPkhqyMWwx+SL5tbOMDuj7Qp24TYiBgDxOJQXDIpbchG8un0jDoddsQqoMD6lgnx5yQfS4tR9S+d+Ubg903XGOMQ5/zRBsDEtIh/CQdhhj3kuosUXBud9bssB0M4o18Lr87bddtSxKuI9lWqkyoHGd5bj7HqQeoHuyrqoOa5oJ9LNRzjvUmwIu7ztTOZlVhlin7bC059kZNb7MCPiHsoVoXdMcV7mNAvS1wbwuDDc5FOORO2+APkPYgKBf6JekuqGNlfXcisDyqTklnx+eo+4CNCt3wdXpCdUrl+9/kGvVIkh8qE+vwu79T6JihwsBcUXcGsHL2frMf9AXA3XyzHNYGTicv1FGUSPqbSIDeno5x2n1lpVDvW4ZKk2wfd55K4+TZauKhUFfUf5ujKs2MkKpYVxOdlxyqar/H1quAv8bVRjCvSpRjaUvaCbBlw44Lw1RnIdQmSbqvY+LejcBcVHc527l1t9kN04hx+EMgSYX3YGEcRpjTm0shCIiJNeW2LxbYpfL9lzBHn/Mgri/sxy7rTgPwwnawdrvpWB5WJqeoet30qsDeri8V7DnLUGl1gn3ot2wDyFPytS30XBK345pTTcEsdaw3qaadekoK+ISoGHyA8XLuc23LKHUUfCdD6qLBAYeQe2iLGmbSdyLnuv2+UCEqXAd7cDjW7afq85XH5qkyPve3SsW67qlufKokdUvD2nwdXV9nklsXJeo9CdJjuyph64p5iJTrmNcxEg63wyEF+hi496VzIb7t8eE+I4NSyl0Ao9g7WpJBAlLOlb6yVV4D2grxPingq7LIjcFvU0w+RyFXwAfvVJ7vBCR56JRaj0nDOh++uOrQtA/Q3DoxVxdrA/Y8gxTqmlYs4HOWjS2bW76Oq8/Vt5fhjqFPOfm+9Rm5z27YcBG6i5P7P+f7vULPXcSAvT3dpjCcVKc+1rV3140pB8znBGaOsDaHRprC9oN0VLJRBFkj7PyVo65RlQI+oXEeJpNSTiAfJbi35TuofVDv1hHTUaDqDd0J4Iy84nPLqbyQOx+KxQ9Nu+uNeVNw6CRex0k+BfzbJu5/nBIF6VxX39dJlHQIfSDEuVvmUwxMcsE2NJ0D6LF6JCCP1YPV5U5L8rB5bJ2uOCBf9/GYY30hY8hVqvvPFff5oxznUalG6bzrU8p2KOCrVuR2YEYV0quSxAnkgjqnnOROAOZoUnWE5IMtd332xZ8LbpSDi9UPgD9Um3LnhVM+JE75JuJ+Y+GVc6eKE59vT2Ox+pLl7bb45u22cdzPHL+LJAwhBfIV7OMeZvfVx1GVkCgxH6TnMd9zS5Ta/HyCKk4K+IRsuKWc/HEfJjP3tlXRYWgyFCh08ueAeuq6Q+ugoNwXJsB1+2Nj+0N1ctviS7OFPfwcI8mydbpHHFCQvkTKF9PuitOR84Vu+YZmBZA5+r4OnxSSfMtyy8RAfeobau3p1KEtfZ0D7vbEAH2uIWVjy9WhmLZInifBQtoo5XrJHPc9HjFl1qJS94sCPkMU0I8z3KdK3f94oKAO6pg7AFgbQu6O5GIpDbOgYvFN23wusds26mJXZcfKpzZepDi39n0vG7OFQT/Xzafa5nP2OXVQ9YbW666HK98yKYCfc8x7u75RAvumhputYtkYca4PkmuKpGzMM2Gc5TG18TzZRuXYTwr4hNZyDH6bH5DF3k9g5wG0t3PBPWC54SbUstITA+V6uuP9Y22QxvaHHH+zXnud1Fj+rqqOx3fV9IWK08HBYNwV5oS7sgHcXZYSx9k37cLagL17IdQBdNvE7RTHigP1bhpnNB0piHOGxsTSfEAvaZs7zZmXAn0umK/quJUYM5LlfXVxOrIxZY1S7nhyjw1VdVLAJ0QNkznOkJ9yd6KJzkHo92hzR8WnWOefs6zP8eecfCVuvwttLuSZdfsc2xD8G9kjtbhKcWxzKMaB5tZLAawk3dcpDMW053jWI9Q+s66Y9XA6LzGSOPnc4TFzQj1WXloXp33udCjPXRe2TqouSZ6kTM7lctUlNXlin++KKcuRZJsV+lcr1z5RwGfIjcdfKzH4EuUIXXL3LXeZULtGEfJthQ52yW1abDnpydj3sK3rvIfg3Tdvt9Go14t38HPFmVYtn2vmGwLS90Csnc5JM8v6gAIL9UoF/pDr74rzVmnOXQxOu3ziQCsHikMQHevUU2k5wm9CTr3bbnd9oWU5eZIyMWW5oq590muQ9JzPBfRU6MeWTblzonC/Wjn3iQI+QxqDH1YMnFP1cBVy76m0UQd+V7G3hkMneypfcgLivN2XqpebZuf5NEoXk5gHZzHIxUCfC/lUJ0EiahQk6m4OlscJx+E6/7leblT36Dn2OrmuP7c+aZs4eVyXPgXoq4b5nNf3FNMLE+ccyD1Pprj3KXAvUY5OettVxfVJAV+gcQf6VEivA5w56/DBvTs/brBvK+bEHQP9McLAizMEpu9EH3L/uY5UG8SBa1+ZUEgL5Yi7Sul4uS6/6/BL4CDUQeEum0NUuzkOJQeeY6DenvY59XadVNmYNmPrcddF1clJ5+ZLywFkvq7HPCnrkfSOQOgcGFuGKhdSFXdkOO0fJVVpPCngM7RWhskc17sTnDj9cQjl4ch3Mom5VRsb384BWCzsx8gXc2/qlbYpZpmcir3QYp2ekOPFeYA1RxoHzEMjK/nmTZpvP1Xxm8ZCvs/pjn2gNRXouesJtd9dl2+5UDo3n1sGIOKaVhU5ZogT822LfR2LdfqbeDg9dl2j6O7XdZ1RwI/UWoDBtSJzsnRH6Flrv3GMMxLjmnMvLtKhHW1hdwRCqsLtDSnn3ZEUZ91WqA5fJ8IH3qG25H5ot0rFAn7KA7fUdKz7T9URms/xkGwVzi4mEdSnHPx1PJHKvCXJeWFlCIqrdMlzwr1v2bbBfhPnMgV8hjDHfpwcbq6wEJ6mQdg3bKZvGSNq2bUG95hyu/3S9UoeHo45mWOdhFwx2tzORWh/SS6+XLgPQbgtbMhSH9j7XP3QcpK7DlR7bFV5gafWWcXDtlR6VfH0vvVg66PqyJHHyQcQXI8lf4q6CFG6HuEJEts3IZefG97Ytlh9bl1VPWzfNingC7TWoJ6Kyfe95Ve1NpQC2NxlJReVGFBOgfnQHYTY1777Rslx5QN7Nz3GYQ/F0IfCpKg6pe3A2oK1xxanbSmSjOHODYfx5TUB9u663PVRy4TSQ3mSMqxrMvfkFEN9VXQCJCezyBgczOTCmpDyzJb0+KsTnkcN1GOlgM8QZyhGU26cOgFUTH7btjGlPdTDt0ZtuVPRdnFOmDlc/9g4+dy3nGM6FDHydSSooSGrCKfhOP1Ym6QuvlsGWzeVRrWtCsWG6ITmfYDNgXlfnhTm3XVSy4XSQ3mcfIBMMF8l7McuF3Nykrj4gROvz+GPOcbaDPZrTQr4KpWlceuktUk5IDvXbVfuRaXqW7mh+qkXTNnCwAwbitSsrwrYlzr9nI6aBPgl9eaQBGal0E9BvVtOwf6IOAdprjKSclzF0LLkYadQHR6HnzNaTx2PH6jipIAfIeqEM+pg6MLtuLnX3O3ARtUZh+1vm6Qn95jbxbGqcgSWVHFvn1PPGHDGybfTYuexNlIvRAvV40sP5VUlKfBy3XmsbEqYTc6wm5Q8Tj5AItDncPCbjtOX0LPvZMB08H3LhuL37Woo1dXpVq2WAn6kpA92joJ84SrS7azzgVxJ20JlFeTbqzpDMqRhQ1WtJ+VBYyxmHWC1u+7WlcPVp8rEvv22Da49Jgnkh8A6Fdy5oT7U+rHlQumhPEmZysC+buhPleQhI1+sfcjlj+wxS98ho3DfnBTwIyRxdsc1Pj+kUYnfd+W2ddzuYoyjUi4gOa7bOS5gKc8ncNxrDK6NuG/Elbj8nGWoNKxd2Og5bh2ucjEZC04zPXTLLdPmsBtumUaAPqUjwC2TU5zbdRwXH8unlqOWcX5UHXmu/VLAzyyqd9t2sFWVok5SevIaT1Udi5+jHSkjWVBl7Hyu0x8TR1+VQqMYAcS9C8EVdzQkyTjwOYAeS2t6fPpWQn3Vzr6knE+SXqrEqec4/NJ0or2hkXlU9UsBP4Owk9YoAL0bZqQHZf1y7wzonYJ2qsm7BDFhPBHP1a3kcx7s5bTLlMH4IzbNXXfdhirVDm5eLuAHaB7ouWUqgfpc6aE8Tn4OSXrxOZx6TieAG/4TgH0AZYumpICfQb4/b5tDdEJDRK4FNX3icdffdHtU+SUJs8lZPxf8A9dqduy8ry2x8kG+vb6mIickeZJ0LtAD5Au94eRzyzQO9aMK+rZyQL2bFwrLSYF9zx8j5hk+VboU8CtWKF4/5k2sqnzijBKgUlWtGJc+ps5cd/w5YT0A9YT2NBEqFAvK0vSqXXpOPrcMQI1gnwPqYzoBMeU4ijlYY0JzJGE5sSE8GQ5GvQOQRwr4Dcr8iUcF7Dkj42iIiUqVXykx+qF6QvH6djnptZszVKe9Xh/TcB9ErsJo5W53bMiLxJ1PWQ8nX1rOe/2qG+irBP/YciGFblOZMtQ6Y1z52LKSDgC2TKQ0vj9OCvg1q24AxqA8tT7JvEqlqlZc84+7PAekueahxOnH2mLXx+3kVO3m5wBkKdBz1lunUw+Q4NbXAfY5wnm4+amS3lZLcdR9Zblp3Dp99SRInxnkSwG/ZsUMsZmiURqisk7pyWG0lPLfXYu/tSQuP2bZmGs59QCvETUyDmc0nCpdfCmbcEbeqRLkuWUk5QAqCL/JBe9NO/nUMrFQG3q6nOoQcFx5X9kY955TlmpDotTVD0sBv8XS2PzqpDF+7VNV//PYesfxP5HyUCpn2ZhIAiMDvRjoU45/qB11KRXqAep34LPF1QNU49bHgHwdYJ8rhCemJy114LllsTakPDxT84Mx6urj6hSF/1S5bt062L59e13tUalUKpVKpVKpVB498MADcPjwYTI/CPgqlUqlUqlUKpVqdFTzjUyVSqVSqVQqlUpVpRTwVSqVSqVSqVSqMZICvkqlUqlUKpVKNUZSwFepVCqVSqVSqcZICvgqlUqlUqlUKtUYSQFfpVKpVCqVSqUaI/1/4m/3XRRGw7QAAAAASUVORK5CYII=", "text/plain": [ "
" ] }, - "metadata": { - "needs_background": "light" - }, + "metadata": {}, "output_type": "display_data" }, { "name": "stdout", "output_type": "stream", "text": [ - "Vorticity plot at timestep: 2800\n", - "\n" + "19:50:37 C02YR4ANLVCJ SmartSim[54122] INFO fv_simulation(54161): Completed\n" ] } ], @@ -376,75 +326,209 @@ "cylinder = client.get_tensor(\"cylinder\").astype(bool)\n", "\n", "# plot every 700th timestep\n", - "for i in range(0, time_steps + 1, 700): \n", + "for i in range(0, time_steps, 700):\n", " client.poll_dataset(f\"data_{i}\", 300, 1000)\n", " dataset = client.get_dataset(f\"data_{i}\")\n", " ux, uy = dataset.get_tensor(\"ux\"), dataset.get_tensor(\"uy\")\n", "\n", - " plot_lattice_vorticity(i, ux, uy, cylinder)" + " plot_lattice_vorticity(i, ux, uy, cylinder)\n", + "\n", + "# Use the Experiment API to wait until the model is finished\n", + "while not exp.finished(model):\n", + " time.sleep(5)" ] }, { - "cell_type": "code", - "execution_count": 7, - "id": "7bbce88c-6f63-407a-8912-5787139f015b", + "cell_type": "markdown", + "id": "d9999bd1", "metadata": {}, - "outputs": [], "source": [ - "# Optionally clear the database\n", - "client.flush_db(db.get_address())" + "## Post-processing with TorchScript\n", + "\n", + "We can upload [TorchScript functions](https://pytorch.org/docs/1.11/jit.html) to the DB. Tensors which are stored on the DB can be passed as arguments to uploaded functions and the results will be stored on the DB. This makes it possible to perform pre- and post-processing operations on tensors localli, *in the DB*, reducing the number of data transfers.\n", + "\n", + "### Uploading a script\n", + "We can load a file containing TorchScript-compatible functionsto the DB. For example, the file `./probe.script` contains the function `probe_points` which interpolates the values of `ux` and `uy` at some user-provided probe points. This is useful when we are interested in the value of a given fields only at specific locations.\n", + "\n", + "The script looks like this:\n", + "\n", + "```python\n", + "def multi_unsqueeze(tensor, axes: List[int]):\n", + " for axis in axes:\n", + " tensor = torch.unsqueeze(tensor, axis)\n", + "\n", + " return tensor\n", + "\n", + "def probe_points(ux, uy, probe_x, probe_y, cylinder):\n", + " ux[cylinder>0] = 0.0\n", + " uy[cylinder>0] = 0.0\n", + " ux = multi_unsqueeze(ux, [0, 0])\n", + " uy = multi_unsqueeze(uy, [0, 0])\n", + " probe_xy = multi_unsqueeze(torch.stack((probe_x/200 - 1, probe_y/50 - 1), 2), [0])\n", + " u_probex = torch.grid_sampler(ux.double(), probe_xy.double(), 0, 0, False).squeeze()\n", + " u_probey = torch.grid_sampler(uy.double(), probe_xy.double(), 0, 0, False).squeeze()\n", + "\n", + " return torch.stack((u_probex, u_probey), 2)\n", + "```\n", + "\n", + "Note that we don't have to import `torch`, as the TorchScript interpreter will recognize it as a builtin module.\n", + "\n", + "We then proceed to upload the script to the DB under the key `probe` and add the probe points as tensors to the DB." ] }, { "cell_type": "code", - "execution_count": 8, - "id": "7d9f2669-4efb-4f38-97e9-869a070ab79c", + "execution_count": 7, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "22:50:59 e3fbeabfdb3e SmartSim[1216] INFO fv_simulation(1337): Completed\n", - "22:51:01 e3fbeabfdb3e SmartSim[1216] INFO Stopping model orchestrator_0 with job name orchestrator_0-CI23BKDK76I0\n", - "22:51:11 e3fbeabfdb3e SmartSim[1216] WARNING Unable to kill emitted process 1327\n" + "Default@19-50-39:ERROR: Redis IO error when executing command: Failed to get reply: Resource temporarily unavailable\n", + "Default@19-50-40:ERROR: Redis IO error when executing command: Failed to get reply: Resource temporarily unavailable\n", + "Default@19-50-41:ERROR: Redis IO error when executing command: Failed to get reply: Resource temporarily unavailable\n", + "Default@19-50-43:ERROR: Redis IO error when executing command: Failed to get reply: Resource temporarily unavailable\n" ] } ], "source": [ - "# Use the Experiment API to wait until the model\n", - "# is finished and then terminate the database and\n", - "# release it's resources\n", - "while not exp.finished(model):\n", - " time.sleep(5)\n", - " \n", - "exp.stop(db)\n" + "client.set_script_from_file(\"probe\", \"./probe.script\", device=\"CPU\")\n", + "\n", + "probe_x, probe_y = np.meshgrid(range(20, 400, 20), range(20, 100, 20))\n", + "client.put_tensor(\"probe_x\", probe_x)\n", + "client.put_tensor(\"probe_y\", probe_y)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We then apply the function `probe_points` to the `ux` and `uy` tensors computed in the last time step of the previous simulation. Note that all tensors are already on the DB, thus we can reference them by name. Finally, we download and plot the output (a 2D velocity field), which is stored as `probe_u` on the DB." ] }, { "cell_type": "code", - "execution_count": 9, - "id": "2bca8a25-6e1b-4540-9d1e-932eb52d7b1e", + "execution_count": 8, "metadata": {}, "outputs": [ { "data": { + "image/png": "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Hz+O8uXLa5bZvn07atSoARsbG/Tp0wf9+/dHSEjIK1nXRERPMxLhF/ASEQFAYWEh3Nzc8OWXX2qODtDrZfbs2di/f/8r20khInodcQSfiOj/fP/99zA2NtacTkRERPQ64kW2RPSvl5GRAVdXV9jZ2WH16tUwMzNTOhIREdE/xlN0iIiIiIgMCE/RISIiIiIyICz4REREREQGhAWfiIiIiMiAsOATERERERkQFnwiIiIiIgPCgk9EREREZEBY8ImIiIiIDAgLPhERERGRAWHBJyIiIiIyICz4REREREQGhAWfiIiIiMiAsOATERERERkQFnwiIiIiIgPCgk9EREREZEBY8ImIiIiIDAgLPhERERGRAWHBJyIiIiIyICYlLWBubo6KFSvqIgsREREREZUgJSUFeXl5z72/xIJfsWJFxMXFvdJQRERERET0z7i6ur7wfp6iQ0RERERkQFjwiYiIiIgMCAs+EREREZEBYcEnIiIiIjIgLPhERERERAaEBZ+IiIiIyICw4BMRERERGRAWfCIiIiIiA8KCT0RERERkQFjwiYiIiIgMCAs+EREREZEBYcEnIiIiIjIgLPhERERERAaEBZ+IiIiIyICw4BMRERERGRAWfCIiIiIiA8KCT0RERERkQFjwiYiIiIgMCAs+EREREZEBYcEnIiIiIjIgLPhERERERAaEBZ+IiIiIyICw4BMRERERGRAWfCIiIiIiA8KCT0RERERkQFjwiYiIiIgMCAs+EREREZEBYcEnIiIiIjIgLPhERPTK/fjjj7h8+bLSMbSo1WqMHDkSe/fuhYgoHYeIqNSw4BMR0SuVkJCAUaNGoW7dumjWrBlWrVqFzMxMpWNh9+7dWLlyJTp16oTatWtj+fLlepGLiIDc3Fykp6crHUPjdR8EYMGnfx21Wv3a/+LqSk5OjtIR6BX566+/EB4eDrVaXerPdffuXXh6egIATp48iREjRsDJyQkjR47EqVOnFPv9K1++PDp06AAAuHr1KsaOHQtXV1dMmjQJN2/eLPbf6MO2IiYmBps3b0Zubq7SUV6KiKCgoEDRDIWFhYo+P/19qampGDp0qN7sdE+YMAEbN258bd9LRlLC1svV1RVxcXG6ylNEYWEhrl27hurVq8PExESxHE/btWsX7t27h3r16qF27dqwtLRUNI9arcaJEydQsWJFODo6wtbWFkZG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RHrk0qVLqFGjhl6cdkyvp5L6+WvxPfhEREREhqJu3bpKRyADx+PGREREREQGhAWfiIiIiMiAsOATERERERkQFnwiIiIiIgPCgk9EREREZEBY8ImIiIiIDAgLPhERERGRAWHBJyIiIiIyICz4REREREQGhAWfiIiIiMiAsOATERERERkQFnwiIiIiIgPCgk9EREREZEBY8ImIiIiIDAgLPhERERGRAWHBJyIiIiIyICz4REREREQGhAWfiIiIiMiAsOATERERERkQFnwiIiIiIgPCgk9EREREZEBY8ImIiF5zeXl5SkcgIj3Cgk9ERPSS0tLScOLECaVjaNm5cydq1aqFq1evKh2FiPQECz4REdFLuHfvHlq3bo0OHTrg+PHjSscBABw6dAh9+/bFrVu38NFHHykdh4j0BAs+ERFRCeLj4xEQEIDIyEgUFBTg/v37SkfCqVOnEBgYiLy8PNSuXRtr165VOhIR6QkWfCIiohe4ffs2AgICEB0dDQsLC2zfvh2BgYGKZrp06RK6dOmCzMxMeHh4YN++fShfvryimQAgNjZW6QhEBBZ8IqJXSqVSKR1BIyoqCmq1WukYL02tVmP9+vU4deqU0lE0rl+/joCAANy8eRPW1tbYs2cPOnXqpGimmJgYdOjQAenp6XB2dsb+/fvh7OysaCYA2LRpE3r37q10DL0mIsjIyFA6Bv0LvBYFPykpCYWFhUrH0BIaGopr164pHaOIrVu36tWHI9G/zeHDhxEeHq50DACPt501atTAggULkJycrHSc5xIR7Nq1C76+vhg6dCiqV6+uWJbo6GjN/1++fBkBAQGIjY2FnZ0d9u3bh1atWuk8U35+vub/4+Li0L59eyQlJaF8+fLYt28f3N3ddZ7paSqVCh988AEGDBgAY2NjHDhwQNE8Z8+eVfT5X+S7777D7du3lY5RRF5eHpKSkpSOQa/Qa1Hwr169iubNm2PhwoW4deuW0nEAABkZGWjcuDFat26NFStWICUlRelIEBE8ePAAffv2xbvvvovr168rHYnoX8fX1xcjR47Epk2blI6CNm3aIDAwEFOnToWrqyv69euHffv26dWo/okTJ9CqVSt069YNFy9eRLly5TBz5kz4+fnhypUrOs2iVqsxaNAgXL9+HefOnUPr1q01RfrgwYPw9/fXaZ4nxo8fj/z8fKSkpKBDhw64c+cO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wsPDJTIystjecvr0afHw8JDVq1crOhL9xIcffiitW7eWVatW6cVn4LBhw6RFixayfPlySU1NVTpOEa/9CD7Rv4VKpdK7vxOQnp4Oa2trvcqlVqshInr39x7Onz+PevXq6V0u+nuSk5Ph6OiIvXv3okOHDkrHKeK7777DqFGjFHv+hw8f4tKlS5qvn3zvvfcUyZGWloagoCCEhYUBADw9PbFhwwY0btxYkTxDhgzB+vXrAQDOzs74z3/+g5EjR8La2lprOX3azicmJqJChQp6c/QsNTUVFSpUUDqGRmJiIpycnJSO8Vwl9XMWfCIiov/z6NEjjBgxAps2bVI6SrEKCwsV34k8deoUkpOTERgYqMjzHz16FIMHD9Z0k7fffhtLlixB2bJlFckDAJcuXULPnj0RHByMYcOGKX5aHhm+kvo5/5ItERHR/7G0tMTixYuVjvFcSpd7AIr/FdiLFy8iLi4OZcuWxYoVK/DWW28pmgcA6tati2vXril6fSDR01jwiYiI/o+JiQlcXV2VjkEv8P777yMxMREjRoyAh4eH0nE0WO5Jn/AUHSIiIiKi10hJ/Zy7m0REREREBoQFn4iIiIjIgLDgExEREREZEBZ8IiIiIiIDwoJPRERERGRAWPCJiIiIiAwICz4RERERkQFhwSciIiIiMiAs+EREREREBoQFn4iIiIjIgLDgExEREREZEBZ8IiIiIiIDwoJPRERERGRAWPCJiIiIiAwICz4RERERkQFhwSciIiIiMiAs+EREREREBoQFn4iIiIjIgLDgExEREREZEBZ8IiIiIiIDwoJPRERERGRAWPCJiIiIiAwICz4RERERkQFhwSciIiIiMiAs+EREREREBoQFn4iIiIjIgLDgExEREREZEBZ8IiIiIiIDwoJPRERERGRAjEREXrSAubk5KlasqKs8RERERET0AikpKcjLy3vu/SUWfCIiIiIien3wFB0iIiIiIgPCgk9EREREZEBY8ImIiIiIDAgLPhERERGRAWHBJyIiIiIyICz4REREREQG5P8B5SPc1bTNpZQAAAAASUVORK5CYII=", "text/plain": [ - "['Completed']" + "
" ] }, - "execution_count": 9, "metadata": {}, - "output_type": "execute_result" + "output_type": "display_data" } ], "source": [ - "exp.get_status(model)" + "ux_name = f\"{{data_{time_steps-1}}}.ux\"\n", + "uy_name = f\"{{data_{time_steps-1}}}.uy\"\n", + "\n", + "client.run_script(\"probe\", \"probe_points\", inputs=[ux_name, uy_name , \"probe_x\", \"probe_y\", \"cylinder\"], outputs=[\"probe_u\"])\n", + "\n", + "probe_u = client.get_tensor(\"probe_u\")\n", + "plot_lattice_probes(time_steps-1, probe_x, probe_y, probe_u)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Uploading a function inline\n", + "In some cases, it makes sense to define the TorchScript function directly in a Python script or in a Jupyter notebook, like in this case. Let us define a simple function which computes the norm of the velocity field." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "f43cc359", + "metadata": {}, + "outputs": [], + "source": [ + "import torch\n", + "\n", + "def compute_norm(ux: torch.Tensor, uy: torch.Tensor):\n", + " return torch.sqrt(ux*ux + uy*uy)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We then store the function on the DB under the key `norm_function`." ] }, { "cell_type": "code", "execution_count": 10, + "id": "48ba5e40", + "metadata": {}, + "outputs": [], + "source": [ + "client.set_function(\"norm_function\", compute_norm)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Note that the key we used identifies a functional unit containing the function itself: this is similar to the key used to store the `probe` script above. When we want to run the function, we just call it with `run_script`, by indicating the `script` key as `\"norm_function\"` and the name of the function itself as `\"compute_norm\"`." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "9b556de6", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAvgAAADoCAYAAACaa5BRAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/bCgiHAAAACXBIWXMAAAxOAAAMTgF/d4wjAAEAAElEQVR4nOydd3hU1daH36npvfcECL33XgVUsFFEBRXLtfer3mu52EU/e7nX3guCIqKI9N57b0lIQjrpbfrM/v7YmSFREAiTynmf5zxJZs7ss87MyZm1117rt1RCCIGCgoKCgoKCgoKCQqtA3dQGKCgoKCgoKCgoKCi4D8XBV1BQUFBQUFBQUGhFKA6+goKCgoKCgoKCQitCcfAVFBQUFBQUFBQUWhGKg6+goKCgoKCgoKDQilAcfAUFBQUFBQUFBYVWhOLgKygoKCgoKCgoKLQiFAdfQUGh3nz55ZfExsa6bTxfX1/WrFnjtvGaM9999x0dOnRw+7g2mw2VStVs38eXX36ZcePGNbUZCgoKCq0axcFXULhIueaaa7j66qtP+9y///1vOnfu3LgGAVVVVYwcORKANWvWoFKpsNlsjW5HYzB9+nSOHj3q+nvmzJnMmDGjCS06d5599lmGDh161v0SExP59NNP6zz25JNPsmzZsoYy7aycq+0XyrFjx7j22muJjY3Fz8+P9u3b89prr1G7t6TD4eC1116jXbt2+Pr60q1bNxYtWlRnnOzsbKZNm0ZERAT+/v5MnjyZ3NzcOvssWrSIPn364O/vT3x8PLNnz27w81NQUGjeKA6+gsJFyt13382iRYvIzs6u87jFYuHzzz/n7rvvbiLLFBRaPqWlpQwbNowtW7ZQUVHB3Llzefvtt3nnnXdc+7zzzju8//77LFy4kPLycp566ikmTZrErl27ADkBuPLKK/Hy8iItLY3s7Gw0Gg1XXnmla6Kwfft2pkyZwqxZsygrK2PhwoW88847vPvuu01y3goKCs0EoaCgcFHicDhEcnKymDVrVp3Hv/vuO+Hj4yPKy8uFzWYTr7/+uujYsaPw9/cXvXv3FitWrHDt+8UXX4iYmBjX30ajUTz++OMiMTFRBAYGiqFDh4otW7bUGX/RokViwIABIjAwUAQHB4vJkye7ngPE8uXLRWZmpvD09BSA8PHxET4+PuKll14STz/9tBg5cmSd8fLy8oROpxO7d+8+7XmOGDFC3HfffeLaa68Vfn5+IiYmRsyZM0fs27dPDBw4UPj6+op+/fqJI0eOuF4zb9480bt3bxEYGChCQkLEFVdcIY4fP17nvZs9e7aIi4sTAQEB4rbbbhNTp04VN998s2ufhIQE8dxzz4nLLrtM+Pr6ijZt2oiff/75tO/dSy+9JLRardBqta7zzczM/Mv7K4QQzzzzjBgyZIjr74KCAnHNNdeIgIAAkZSUJL799lsBiNWrV7v22bJlixgxYoQIDg4W8fHx4umnnxZWq/W075cQQrz//vuiS5cuws/PT0RERIgZM2aIwsJCIYQQ3377rdDpdEKtVrtsXbdu3V/GuPTSS4VKpRIeHh7Cx8dHdO7c+bT21+fzOdt1uWfPHjF8+HAREBAgAgMDRe/evcWRI0f+1vbDhw+LCRMmiPDwcBEdHS3uvvtuUVVVVefznDVrlhg1apTw8fERXbp0EUuWLDnje3g6HnzwQXHllVe6/u7fv7+YPXt2nX2GDBkibr/9diGEEIcOHRKAyMvLcz2fkpIiALFhwwYhhBCPP/64GD9+fJ0xnnrqKdGuXbvzsk1BQaF1oTj4CgoXMW+88YaIjo6u4+wNGzZM3HHHHUII6Yz16NFDHDlyRNjtdvHzzz8Lb29vkZqaKoT4q4N/3333ia5du4qUlBRhNpvF66+/Lnx9fUVWVpYQQohly5YJT09PMW/ePGE2m4XRaKzjmDkdfCGEWL16tQDq2HbixAmh1WrFsWPHXI+9+OKLYsCAAWc8xxEjRoiAgACxdu1aYbfbxdtvvy28vb3F5ZdfLtLT04XZbBaTJk0S48aNc73mjz/+EHv27BE2m00UFhaKiRMnioEDB7qe/+qrr0RwcLDYsmWLsFqt4tNPPxVarfYvDn5cXJzYuXOnsNvt4o033hB+fn6ivLz8tO/dzTffLKZPn17H9nNx8MeOHSvGjx8viouLRXFxsZgwYUIdB//IkSPCx8dHzJkzR1itVpGRkSG6d+8uXnzxxTO+Zz/99JM4evSosNvtIiMjQ/Tv319cd911Z7ThTCQkJIhPPvnkb+2vz+dztuty8ODB4rnnnhNWq1VYrVaxe/dukZ+ff0bbCwsLRWhoqHjzzTeFyWQShYWFYsyYMS5H23kuoaGhYsOGDa7PXK/X15n4/R1Wq1V0795dPPPMM67H+vXrJ15++eU6+w0aNEj07t1bCCHEwYMHBSByc3Ndzx89elQA4t133xVCCPHYY4/VeW+EEOKJJ54QgKioqDgn2xQUFFofioOvoHARU1JSIry8vMT8+fOFEEIcOHBAAGLPnj1CCCH8/f3/EqW85JJLxAsvvCCEqOuA2u124eXlJX755Zc6+3fv3t0VpZwwYYK49957z2jP2Rx8IYS48sorxaOPPuo6ZkJCgvjiiy/OOOaIESPErbfe6vq7rKxMAOL77793PfbTTz+JwMDAM46xa9euOg7TmDFjxGOPPVZnnz59+pw2gu+kqqpKAK4VDXc4+NnZ2QIQ+/btcz2/b9++Og7+/fffX8c5F0JG4du2bXvG8/0zP//8swgODj6tDX/HuTr45/v5nO26HDlypLjttttcDv/fHV8IOdGtPYETQogNGzYIvV4vbDab61weeeSROvv0799fPP/882d+A2pwOBzitttuE506darjdL/00ksiNjZW7NmzR1gsFvHtt98KtVrtir5brVbRqVMnMWPGDFFWViaKi4vFpEmThEqlck3Q1q9fL3Q6nZg/f76wWq1i+/btIiIiQgAiOzv7rLYpKCi0TpQcfAWFi5igoCCmTZvGhx9+CMCHH37IoEGD6NGjBwUFBVRUVDB16lQCAwNd26ZNm8jJyfnLWEVFRRiNRtq2bVvn8Xbt2nHixAkA0tPTL1g55p577uHLL7/EYrGwdOlSysvLmTZt2t++JioqyvW7j4/PaR+rrKx0/b127VrGjBlDVFQU/v7+jBgxAoCTJ08CkJOTQ0JCQp1jJCYm/uW40dHRfzlu7eNcKM76iaSkJNdjtX8HSElJYcGCBXU+w7vvvpv8/Pwzjvvzzz8zePBgwsPD8ff358Ybb6SkpAS73e4222tzPp/PuVyXX375JSqVitGjRxMbG8tDDz1EVVXVGY+fkpLCzp0764x3+eWXo1Kp6rxPf35vk5KSyMrK+ttzs9vt3HrrrWzdupVVq1bh5+fneu7xxx/ntttuY8qUKURERLBw4UKuv/56QkNDAdBqtfz2229UVVXRsWNHevbsyahRo/Dx8XHtM3ToUL799ltefPFFwsPDuffee7n77rtRq9UEBQX9rW0KCgqtF8XBV1C4yLnnnntYsWIFe/fu5ZtvvuGee+4BIDAwEE9PTxYtWkRZWZlrq66u5oMPPvjLOKGhoXh6epKWllbn8bS0NOLj4wHpBB87duyc7FKrT397GjduHAEBAfz888989NFH3HTTTXh5eZ3PKf8tFouFiRMncumll3Ls2DEqKipYu3YtgKuwMSYmhszMzDqv+/Pf58vpztfPz4/q6uo6j9VWUHFKlGZkZLgeq/07QGRkJDfccEOdz7CiouKMDm92djZTp07l/vvv58SJE1RUVPDNN98Ap87/TJ/NuZzThXIu12VCQgKffPIJmZmZrFmzhuXLl7uUZU5nU2RkJEOHDq0zXnl5OSaTiZiYGNd+f35vMzIy/lYm1mw2M2XKFA4ePMjatWuJjIys87xWq+XZZ58lJSWFkpIS5s2bx6FDhxgzZoxrn7Zt27JgwQLy8vI4ceIEw4YNo7q6mtGjR7v2ufbaa9m1axclJSVs3bqVsrIyBg0ahLe397m/sQoKCq0KxcFXULjI6devH3369GHSpEno9XqmTp0KgIeHB3fddRePP/44hw8fRgiB0Whk3bp1p3XS1Wo1t956K7NmzeL48eNYLBbeeustUlNTmT59OgAPPvggn332GfPnz8disWAymVi5cuVp7XI6Q7WlJAFUKhV33XUXs2fP5vfff+fOO+9059uBxWLBaDQSFBSEn58fubm5PP3003X2ufHGG/n888/Zvn07NpuNL774gj179lzQcSMjI0lLS6sTJe/VqxeVlZXMnTsXh8PBmjVr+PHHH13Px8TEMGbMGB5//HFKS0spLS3lySefrDPuPffcw08//cSPP/6IxWLBbreTmprKkiVLTmtHVVUVDofDNWFLSUn5i+xiZGQkJ06cwGQynfWc/vz5XSjncl1++eWXZGdnI4TA398frVaLVqs9o+233HILu3fv5n//+x8GgwEhBFlZWfzyyy91jv3111+zefNmbDYbX375Jbt373Zd23+mqqqKyy+/nJKSElauXElwcPBf9ikoKCA1NRUhBMXFxfzzn/+kqKiIhx9+2LXPvn37KCsrw+FwsHfvXm655RbuvvtukpOTAam0s23bNmw2GwaDga+//prPP/+cV1999YLeZwUFhZaN4uArKChwzz33cPz4cW699VY8PDxcj7/++utcf/31rnSIxMREZs+ejdVqPe04r7/+OuPGjWPUqFGEh4czf/58li9fTlxcHCCj73PmzOGVV14hLCyM2NhYPvroo9OO1b59e+6//35GjRpFYGAgr7zy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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "dataset = client.get_dataset(f\"data_{time_steps-1}\")\n", + "client.run_script(\"norm_function\", \"compute_norm\", [f\"{{data_{i}}}.uy\", f\"{{data_{i}}}.ux\"], [\"u\"])\n", + "u = client.get_tensor(\"u\")\n", + "\n", + "plot_lattice_norm(time_steps-1, u, cylinder)" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "7bbce88c-6f63-407a-8912-5787139f015b", + "metadata": {}, + "outputs": [], + "source": [ + "# Optionally clear the database\n", + "client.flush_db(db.get_address())" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "7d9f2669-4efb-4f38-97e9-869a070ab79c", + "metadata": {}, + "outputs": [], + "source": [ + "# terminate the database and\n", + "# release its resources\n", + "exp.stop(db)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 14, "id": "50b42065-6356-4a5a-b742-daca17b8bd6e", "metadata": {}, "outputs": [ @@ -453,34 +537,26 @@ "text/html": [ "\n", "\n", - "\n", + "\n", "\n", "\n", - "\n", - "\n", + "\n", + "\n", "\n", "
Name Entity-Type JobID RunID TimeStatus Returncode
Name Entity-Type JobID RunID Time Status Returncode
0fv_simulation Model 1337 041.7211Completed 0
1orchestrator_0DBNode 1322 083.3787Cancelled -9
0 fv_simulation Model 54161 0 38.1561Completed0
1 orchestrator_0DBNode 54134 0 66.5750Cancelled0
" ], "text/plain": [ - "'\\n\\n\\n\\n\\n\\n\\n\\n
Name Entity-Type JobID RunID TimeStatus Returncode
0fv_simulation Model 1337 041.7211Completed 0
1orchestrator_0DBNode 1322 083.3787Cancelled -9
'" + "'\\n\\n\\n\\n\\n\\n\\n\\n
Name Entity-Type JobID RunID Time Status Returncode
0 fv_simulation Model 54161 0 38.1561Completed0
1 orchestrator_0DBNode 54134 0 66.5750Cancelled0
'" ] }, - "execution_count": 10, + "execution_count": 14, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "exp.summary(format=\"html\")" + "exp.summary(style=\"html\")" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "326352c9-d10d-47b5-a3b3-032b033b55f6", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { @@ -499,7 +575,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.10" + "version": "3.10.10" } }, "nbformat": 4, diff --git a/tutorials/online_analysis/lattice/probe.script b/tutorials/online_analysis/lattice/probe.script new file mode 100644 index 000000000..ea5de7248 --- /dev/null +++ b/tutorials/online_analysis/lattice/probe.script @@ -0,0 +1,16 @@ +def multi_unsqueeze(tensor, axes: List[int]): + for axis in axes: + tensor = torch.unsqueeze(tensor, axis) + + return tensor + +def probe_points(ux, uy, probe_x, probe_y, cylinder): + ux[cylinder>0] = 0.0 + uy[cylinder>0] = 0.0 + ux = multi_unsqueeze(ux, [0, 0]) + uy = multi_unsqueeze(uy, [0, 0]) + probe_xy = multi_unsqueeze(torch.stack((probe_x/200 - 1, probe_y/50 - 1), 2), [0]) + u_probex = torch.grid_sampler(ux.double(), probe_xy.double(), 0, 0, False).squeeze() + u_probey = torch.grid_sampler(uy.double(), probe_xy.double(), 0, 0, False).squeeze() + + return torch.stack((u_probex, u_probey), 2) diff --git a/tutorials/online_analysis/lattice/vishelpers.py b/tutorials/online_analysis/lattice/vishelpers.py index 69f1ed05a..725c690fd 100644 --- a/tutorials/online_analysis/lattice/vishelpers.py +++ b/tutorials/online_analysis/lattice/vishelpers.py @@ -3,8 +3,7 @@ def plot_lattice_vorticity(timestep, ux, uy, cylinder): - - fig = plt.figure(figsize=(12,6), dpi=80) + fig = plt.figure(figsize=(12, 6), dpi=80) plt.cla() ux[cylinder], uy[cylinder] = 0, 0 @@ -13,13 +12,62 @@ def plot_lattice_vorticity(timestep, ux, uy, cylinder): ) vorticity[cylinder] = np.nan cmap = plt.cm.get_cmap("bwr").copy() - cmap.set_bad(color='black') + cmap.set_bad(color="black") plt.imshow(vorticity, cmap=cmap) - plt.clim(-.1, .1) + plt.clim(-0.1, 0.1) ax = plt.gca() ax.invert_yaxis() ax.get_xaxis().set_visible(False) ax.get_yaxis().set_visible(False) - ax.set_aspect('equal') + ax.set_aspect("equal") + ax.set_title(f"Vorticity plot at timestep {timestep}\n") + plt.pause(0.001) + + +def plot_lattice_norm(timestep, u, cylinder): + fig = plt.figure(figsize=(12, 6), dpi=80) + + plt.cla() + + u[cylinder] = np.nan + cmap = plt.cm.get_cmap("jet").copy() + cmap.set_bad(color="black") + plt.contour(u, cmap=cmap) + plt.clim(-0.1, 0.1) + ax = plt.gca() + ax.invert_yaxis() + ax.get_xaxis().set_visible(False) + ax.get_yaxis().set_visible(False) + ax.set_aspect("equal") + ax.set_title(f"Velocity magnitude at timestep {timestep}\n") + plt.pause(0.001) + + +def plot_lattice_probes(timestep, probe_x, probe_y, probe_u): + fig = plt.figure(figsize=(12, 6), dpi=80) + + plt.cla() + cmap = plt.cm.get_cmap("binary").copy() + cmap.set_bad(color="black") + plt.quiver( + probe_x, + probe_y, + probe_u[:, :, 0], + probe_u[:, :, 1], + np.linalg.norm(probe_u, axis=2), + cmap=cmap, + scale=7, + pivot='mid', + angles='uv', + width=0.003 + ) + plt.clim(-0.1, 0.1) + ax = plt.gca() + ax.invert_yaxis() + ax.get_xaxis().set_visible(False) + ax.get_yaxis().set_visible(False) + ax.set_aspect("equal") + ax.set_xlim([0, 399]) + ax.set_ylim([0, 99]) + ax.set_title(f"Velocity field at timestep {timestep}\n") plt.pause(0.001) - print(f"Vorticity plot at timestep: {timestep}\n") \ No newline at end of file